diff --git a/components/Smart-Data-viewer/README.md b/components/Smart-Data-viewer/README.md new file mode 100644 index 0000000..3d663cf --- /dev/null +++ b/components/Smart-Data-viewer/README.md @@ -0,0 +1,94 @@ +## Username + +widlestudiollp + +## Project Name + +Smart Data Viewer + +## About + +Smart Data Viewer is an intelligent data inspection component for Retool that transforms raw object and JSON data into a clean, structured, and interactive viewing experience. It automatically analyzes incoming data and renders each field using context-aware UI patterns such as badges, tags, nested object explorers, links, and expandable content blocks. + +The component supports multiple viewing layouts including list, card, and table modes, allowing users to inspect structured data in the format best suited to their workflow. Smart Data Viewer helps developers and internal teams quickly explore records, API responses, payloads, and deeply nested objects without manually formatting raw data. + +## Preview + +![Smart Data Viewer Preview](preview.png) + +## How it works + +The component receives data through Retool state (`data`) and dynamically inspects each field to determine how it should be rendered. + +It evaluates the type and structure of each value, then automatically applies the most appropriate visual representation for improved readability and usability. + +### Rendering logic + +* Boolean values → Active / Inactive status chips +* Arrays → Tag pills +* Objects → Expandable nested object viewer +* URLs → Clickable external links +* Emails → Inline email formatting +* Long text → Truncated preview with Show more / Show less +* Null values → Muted null badge +* Standard text / numbers → Plain formatted text + +The component recursively renders nested objects, allowing users to expand and inspect deeply structured data without losing context. + +Users can switch between multiple view modes depending on how they want to inspect the data. + +### View modes + +* List view → Key-value row layout for detailed scanning +* Card view → Compact card-based layout for dashboard browsing +* Table view → Structured table layout for dense data inspection + +### Example input + +```json +{ + "id": 8421, + "fullName": "Ada Lovelace", + "email": "ada@analyticalengine.io", + "website": "https://analyticalengine.io/profile/ada", + "is_active": true, + "is_verified": false, + "role": "Senior Engineer", + "skills": ["React", "TypeScript", "Retool", "PostgreSQL"], + "bio": "Ada is a passionate engineer with over a decade of experience building scalable systems and delightful user interfaces.", + "metadata": { + "createdAt": "2024-01-12T10:24:00Z", + "lastLogin": "2026-04-21T08:11:53Z", + "preferences": { + "theme": "dark", + "notifications": true + } + } +} +``` + +## Build process + +The component is built using React and integrates with Retool through `@tryretool/custom-component-support`. It uses custom rendering logic and adaptive UI patterns to intelligently display structured data in a polished and readable format. + +### Key implementation details + +* Uses React state for layout switching and expandable content +* Integrates directly with Retool state using `Retool.useStateObject` +* Implements intelligent type detection for adaptive rendering +* Recursively renders nested objects for deep inspection +* Supports expandable long-text previews and collapsible object trees +* Automatically adapts layout across list, card, and table views +* Uses custom CSS for a modern dark UI with responsive behavior + +### Extensibility + +The component is designed to be easily extendable. Developers can: + +* Add additional render types (e.g. dates, images, code blocks) +* Add custom field formatters +* Extend nested object rendering behavior +* Add field grouping and sorting +* Add search and filtering controls +* Customize themes and layout styles +* Integrate advanced schema-aware rendering diff --git a/components/Smart-Data-viewer/metadata.json b/components/Smart-Data-viewer/metadata.json new file mode 100644 index 0000000..cb06559 --- /dev/null +++ b/components/Smart-Data-viewer/metadata.json @@ -0,0 +1,7 @@ +{ + "id": "smart-data-viewer", + "title": "Smart Data Viewer", + "author": "@widlestudiollp", + "shortDescription": "An intelligent data inspection component for Retool that analyzes structured input and automatically renders fields using adaptive UI patterns like cards, tables, tags, badges, and nested object explorers.", + "tags": ["Data Viewer", "JSON Viewer", "Object Inspector", "Retool", "Developer Tools"] +} diff --git a/components/Smart-Data-viewer/package.json b/components/Smart-Data-viewer/package.json new file mode 100644 index 0000000..516376d --- /dev/null +++ b/components/Smart-Data-viewer/package.json @@ -0,0 +1,48 @@ +{ + "name": "custom-component-collection", + "version": "0.1.0", + "private": true, + "dependencies": { + "@tryretool/custom-component-support": "latest", + "qrcode.react": "^4.2.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "scripts": { + "dev": "npx retool-ccl dev", + "deploy": "npx retool-ccl deploy", + "test": "vitest" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@types/react": "^18.2.55", + "@typescript-eslint/eslint-plugin": "^7.3.1", + "@typescript-eslint/parser": "^7.3.1", + "eslint": "^8.57.0", + "eslint-plugin-react": "^7.34.1", + "postcss-modules": "^6.0.0", + "prettier": "^3.0.3", + "vitest": "^4.0.17" + }, + "retoolCustomComponentLibraryConfig": { + "name": "SmartForm", + "label": "Smart Form", + "description": "form", + "entryPoint": "src/index.tsx", + "outputPath": "dist" + } +} \ No newline at end of file diff --git a/components/Smart-Data-viewer/preview.png b/components/Smart-Data-viewer/preview.png new file mode 100644 index 0000000..31a5c25 Binary files /dev/null and b/components/Smart-Data-viewer/preview.png differ diff --git a/components/Smart-Data-viewer/src/components/keyViewer.tsx b/components/Smart-Data-viewer/src/components/keyViewer.tsx new file mode 100644 index 0000000..d61d929 --- /dev/null +++ b/components/Smart-Data-viewer/src/components/keyViewer.tsx @@ -0,0 +1,222 @@ +import React, { useState } from "react"; +import { Retool } from "@tryretool/custom-component-support"; +import "./styles.css"; + +const SmartDataViewer = () => { + + const capitalize = (text: string) => { + if (!text) return ""; + return text.charAt(0).toUpperCase() + text.slice(1); + }; + + const [data] = Retool.useStateObject({ + name: "data", + initialValue: {} + }); + + const [viewMode, setViewMode] = useState("list"); + const [search, setSearch] = useState(""); + const [expanded, setExpanded] = useState>({}); + + if (!data || typeof data !== "object") { + return
No data
; + } + + const entries = Object.entries(data).filter(([k]) => + k.toLowerCase().includes(search.toLowerCase()) + ); + + const getType = (v : any) => { + if (v === null || v === undefined) return "null"; + if (typeof v === "boolean") return "boolean"; + if (Array.isArray(v)) return "array"; + if (typeof v === "object") return "object"; + if (typeof v === "string" && v.startsWith("http")) return "url"; + if (typeof v === "string" && v.includes("@")) return "email"; + if (typeof v === "string" && v.length > 120) return "long"; + return "text"; + }; + + const renderValue = (value: any, key: string) => { + const type = getType(value); + switch (type) { + case "boolean": + return ( + + + {value ? "Active" : "Inactive"} + + ); + + case "array": + return value.map((v: any, i: any) => ( + + {capitalize(String(v))} + + )); + + case "object": + if (!value) + return null; + + return ( +
+ +
+ setExpanded({ ...expanded, [key]: !expanded[key] }) + } > + {expanded[key] ? "▾" : "▸"} + Object + + ({Object.keys(value).length}) + +
+ + + {expanded[key] && ( +
+ {Object.entries(value).map(([k, v]) => ( +
+
{capitalize(k)}:
+
+ {renderValue(v, k)} +
+
+ ))} +
+ )} +
+ ); + + + case "null": + return Null; + case "url": + return ( + + {value} ↗ + + ); + + case "email": return ✉ {value} ; + + case "long": + return ( + <> + + {expanded[key] + ? capitalize(value) + : capitalize(value.substring(0, 120)) + "..."} + +
+ setExpanded({ ...expanded, [key]: !expanded[key] }) + }> + {expanded[key] ? "Show less" : "Show more"} +
+ + ); + + default: + return {capitalize(String(value))}; + } + }; + + return ( +
+
+
+
+
🗄
+
+
Data Viewer
+ {entries.length} fields +
+
+
+ +
+
+ + + + + + + +
+
+
+ + + {viewMode === "list" && ( +
+ {entries.map(([k, v]) => ( +
+
+ {capitalize(k)} +
{k.toLowerCase()}
+
+
{renderValue(v, k)}
+
+ ))} +
+ )} + + {viewMode === "card" && ( +
+ {entries.map(([k, v]) => ( +
+
{capitalize(k)}
+
{renderValue(v, k)}
+
+ ))} +
+ )} + + + {viewMode === "table" && ( + + + {entries.map(([k, v]) => ( + + + + + ))} + +
{capitalize(k)}{renderValue(v, k)}
+ )} +
+ ); +}; + +export default SmartDataViewer; \ No newline at end of file diff --git a/components/Smart-Data-viewer/src/components/styles.css b/components/Smart-Data-viewer/src/components/styles.css new file mode 100644 index 0000000..ed4a598 --- /dev/null +++ b/components/Smart-Data-viewer/src/components/styles.css @@ -0,0 +1,387 @@ +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: Inter, sans-serif; + background: transparent; + color: #e2e8f0; +} +.wrapper { + position: relative; + min-height: 100vh; + padding: 22px; + border-radius: 24px; + overflow: hidden; + color: #e2e8f0; + font-family: Inter, sans-serif; + background: + radial-gradient(circle at top left, rgba(99, 102, 241, 0.18), transparent 28%), + radial-gradient(circle at top right, rgba(14, 165, 233, 0.12), transparent 30%), + radial-gradient(circle at bottom right, rgba(168, 85, 247, 0.12), transparent 30%), + linear-gradient(180deg, #08111f 0%, #091423 100%); + border: 1px solid rgba(51, 65, 85, 0.55); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.03), + 0 0 0 1px rgba(15, 23, 42, 0.4), + 0 30px 80px rgba(2, 8, 23, 0.65); +} +.wrapper::before { + content: ""; + position: absolute; + inset: 18px; + border-radius: 22px; + pointer-events: none; +} +.wrapper::after { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; + border-radius: 24px; + box-shadow: + inset 0 0 80px rgba(59, 130, 246, 0.06), + inset 0 -30px 80px rgba(168, 85, 247, 0.05); +} +.header { + position: relative; + z-index: 2; + display: flex; + align-items: center; + justify-content: space-between; + gap: 18px; + margin-bottom: 20px; + padding: 2px 2px 16px; + border-bottom: 1px solid rgba(30, 41, 59, 0.75); +} +.title { + display: flex; + align-items: center; +} +.title-top { + display: flex; + align-items: flex-start; + gap: 12px; +} +.title-icon { + display: flex; + align-items: center; + justify-content: center; + width: 38px; + height: 38px; + border-radius: 10px; + flex-shrink: 0; + font-size: 18px; + background: linear-gradient(135deg, #8b5cf6, #38bdf8); + box-shadow: + 0 10px 25px rgba(56, 189, 248, 0.18), + inset 0 1px 0 rgba(255, 255, 255, 0.18); +} +.title-content { + display: flex; + flex-direction: column; + justify-content: center; + gap: 3px; +} +.title-text { + font-size: 18px; + font-weight: 600; + line-height: 1; + color: #f8fafc; +} +.title-content span { + display: block; + font-size: 13px; + font-weight: 400; + color: #97a4b7; + line-height: 1.2; +} +.actions { + display: flex; + align-items: center; + gap: 12px; +} +.actions input { + width: 280px; + height: 40px; + border-radius: 999px; + border: 1px solid rgba(51, 65, 85, 0.65); + outline: none; + background: rgba(2, 8, 23, 0.8); + color: #f8fafc; + padding: 0 14px; + font-size: 14px; + transition: all 0.2s ease; +} +.actions input::placeholder { + color: #64748b; +} +.actions input:focus { + border-color: rgba(59, 130, 246, 0.5); + box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.08); +} +.view-toggle { + display: flex; + align-items: center; + gap: 4px; + padding: 4px; + border-radius: 999px; + background: rgba(15, 23, 42, 0.9); + border: 1px solid rgba(30, 41, 59, 0.8); +} +.view-toggle button { + width: 34px; + height: 34px; + border: none; + border-radius: 10px; + background: transparent; + color: #64748b; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; +} +.view-toggle button:hover { + color: #e2e8f0; + background: rgba(30, 41, 59, 0.8); +} +.view-toggle button.active { + color: #93c5fd; + background: linear-gradient(180deg, #1d4ed8, #2563eb); + box-shadow: + 0 8px 20px rgba(37, 99, 235, 0.28), + inset 0 1px 0 rgba(255, 255, 255, 0.12); +} +.list { + position: relative; + z-index: 2; +} +.row { + display: grid; + grid-template-columns: 220px 1fr; + gap: 22px; + margin-bottom: 14px; + padding: 18px 18px; + border-radius: 18px; + background: rgba(7, 14, 28, 0.72); + border: 1px solid rgba(30, 41, 59, 0.72); + backdrop-filter: blur(10px); + transition: all 0.22s ease; +} +.row:hover { + border-color: rgba(51, 65, 85, 1); + background: rgba(10, 18, 34, 0.9); + transform: translateY(-1px); +} +.key { + font-size: 15px; + font-weight: 600; + color: #f8fafc; + line-height: 1.3; +} +.sub { + margin-top: 4px; + font-size: 12px; + font-weight: 400; + color: #64748b; + text-transform: none; +} +.value { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + gap: 8px; + font-size: 14px; + line-height: 1.7; + color: #e2e8f0; +} +.value a { + color: #f8fafc; + text-decoration: none; +} +.value a:hover { + color: #93c5fd; +} +.grid { + position: relative; + z-index: 2; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 16px; +} +.card { + padding: 18px; + border-radius: 18px; + background: rgba(7, 14, 28, 0.76); + border: 1px solid rgba(30, 41, 59, 0.72); + transition: all 0.2s ease; + min-height: unset; + height: fit-content; + align-self: start; +} +.card:hover { + border-color: rgba(51, 65, 85, 1); + background: rgba(10, 18, 34, 0.9); +} +.card-title { + font-size: 15px; + font-weight: 600; + color: #f8fafc; + margin-bottom: 10px; +} +.table { + width: 100%; + border-collapse: collapse; + position: relative; + z-index: 2; + border-radius: 16px; + overflow: hidden; + background: rgba(7, 14, 28, 0.76); + border: 1px solid rgba(30, 41, 59, 0.72); +} +.table td { + padding: 16px; + border-bottom: 1px solid rgba(30, 41, 59, 0.7); + vertical-align: top; +} +.table tr:last-child td { + border-bottom: none; +} +.tag { + display: inline-flex; + align-items: center; + justify-content: center; + width: fit-content; + height: 28px; + padding: 0 12px; + margin: 2px; + border-radius: 999px; + white-space: nowrap; + font-size: 12px; + font-weight: 500; + color: #e2e8f0; + background: rgba(15, 23, 42, 0.9); + border: 1px solid rgba(71, 85, 105, 0.65); +} +.chip { + display: inline-flex; + align-items: center; + gap: 7px; + width: fit-content; + white-space: nowrap; + padding: 5px 12px; + border-radius: 999px; + font-size: 12px; + font-weight: 600; +} + +.chip .dot { + width: 7px; + height: 7px; + border-radius: 50%; +} + +.chip.active { + color: #4ade80; + background: rgba(34, 197, 94, 0.12); + border: 1px solid rgba(34, 197, 94, 0.18); +} + +.chip.active .dot { + background: #22c55e; +} + +.chip.inactive { + color: #94a3b8; + background: rgba(148, 163, 184, 0.12); + border: 1px solid rgba(148, 163, 184, 0.14); +} + +.chip.inactive .dot { + background: #94a3b8; +} + +.badge.gray { + color: #94a3b8; + border-radius: 999px; + font-size: 14px; + font-weight: 500; + padding: 5px 12px; +} +.expand { + margin-top: 4px; + font-size: 12px; + font-weight: 600; + color: #e2e8f0; + cursor: pointer; +} +.object-container { + width: 100%; +} + +.object-header { + display: flex; + align-items: center; + gap: 6px; + cursor: pointer; + font-size: 13px; + color: #94a3b8; +} + +.arrow { + font-size: 11px; + color: #64748b; +} + +.object-label { + color: #cbd5e1; +} + +.object-count { + color: #64748b; +} + +.object-children { + margin-top: 10px; + padding-left: 16px; + border-left: 2px solid rgba(30, 41, 59, 0.8); +} + +.object-row { + display: grid; + grid-template-columns: 140px 1fr; + gap: 10px; + padding: 6px 0; + align-items: start; +} + +.object-key { + color: #64748b; + font-size: 12px; +} + +.object-value { + display: flex; + flex-wrap: wrap; + gap: 6px; + font-size: 13px; + color: #e2e8f0; +} +.null-text { + color: #64748b; + font-size: 13px; + font-style: italic; +} + +.empty { + padding: 30px; + text-align: center; + color: #64748b; +} +a { + color: #fff; + text-decoration: none; + font-size: 14px; +} diff --git a/components/Smart-Data-viewer/src/index.tsx b/components/Smart-Data-viewer/src/index.tsx new file mode 100644 index 0000000..a011c38 --- /dev/null +++ b/components/Smart-Data-viewer/src/index.tsx @@ -0,0 +1 @@ +export { default as SmartDataViewer } from './components/keyViewer'