From cbc1978bfa7b93de10ac8eac4ce3fd0508c843b6 Mon Sep 17 00:00:00 2001 From: widlestudiollp Date: Fri, 24 Apr 2026 12:47:52 +0530 Subject: [PATCH 1/2] added component --- components/dynamic-cards/README.md | 88 ++ components/dynamic-cards/metadata.json | 17 + components/dynamic-cards/package.json | 54 + .../dynamic-cards/src/components/card.tsx | 943 ++++++++++++++++++ components/dynamic-cards/src/index.tsx | 1 + 5 files changed, 1103 insertions(+) create mode 100644 components/dynamic-cards/README.md create mode 100644 components/dynamic-cards/metadata.json create mode 100644 components/dynamic-cards/package.json create mode 100644 components/dynamic-cards/src/components/card.tsx create mode 100644 components/dynamic-cards/src/index.tsx diff --git a/components/dynamic-cards/README.md b/components/dynamic-cards/README.md new file mode 100644 index 0000000..1021d2a --- /dev/null +++ b/components/dynamic-cards/README.md @@ -0,0 +1,88 @@ +# Create a README.md file for download + +content = """# 📊 Dynamic KPI Cards + +An intelligent, highly customizable KPI card component built with React and Retool Custom Components. +It automatically detects fields from your data and renders responsive KPI cards with trends and styling options. + +--- + +## ✨ Features + +- Auto field detection (label, value, trend, secondary) +- Manual override support +- Responsive KPI card layout +- Trend indicators with color coding +- Fully customizable UI (colors, fonts, layout) +- Retool integration with state + events + +--- + +## 🏗️ Tech Stack + +- React +- TypeScript +- Retool Custom Component API + +--- + +## 📦 Installation + +npm install @tryretool/custom-component-support + +--- + +## 🚀 Usage + +Import: + +import DynamicKpiCards from "./DynamicKpiCards"; + +Provide data: + +[ + { "name": "Revenue", "value": 120000, "trend": 12.5, "target": 150000 }, + { "name": "Users", "value": 5400, "trend": -2.1, "target": 6000 } +] + +Render: + + + +--- + +## 🧠 Smart Detection + +Automatically maps: +- Label → name, label, title +- Value → value, revenue, count +- Secondary → target, baseline +- Trend → trend, change, growth + +--- + +## 🎯 Interaction + +- Click a card → updates selectedCard +- Triggers Retool event: cardClick + +--- + +## ⚠️ Notes + +- Requires Retool environment +- Data must be an array of objects +- Only flat structures supported + +--- + +## 📄 License + +MIT License +""" + +file_path = "/mnt/data/Dynamic_KPI_Cards_README.md" +with open(file_path, "w") as f: + f.write(content) + +file_path \ No newline at end of file diff --git a/components/dynamic-cards/metadata.json b/components/dynamic-cards/metadata.json new file mode 100644 index 0000000..c47df84 --- /dev/null +++ b/components/dynamic-cards/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "dynamic-kpi-cards", + "title": "Dynamic KPI Cards", + "author": "@widlestudiollp", + "shortDescription": "Auto-detects fields and renders responsive KPI cards with trends, secondary metrics, and full customization.", + "tags": [ + "Charts", + "React", + "UI Components", + "Analytics", + "Dashboard", + "Data Visualization", + "Custom", + "Retool", + "Interactive" + ] +} \ No newline at end of file diff --git a/components/dynamic-cards/package.json b/components/dynamic-cards/package.json new file mode 100644 index 0000000..3ed2041 --- /dev/null +++ b/components/dynamic-cards/package.json @@ -0,0 +1,54 @@ +{ + "name": "my-react-app", + "version": "0.1.0", + "private": true, + "dependencies": { + "@fontsource/inter": "^5.2.8", + "@fontsource/lato": "^5.2.7", + "@fontsource/montserrat": "^5.2.8", + "@fontsource/open-sans": "^5.2.7", + "@fontsource/playfair-display": "^5.2.8", + "@fontsource/poppins": "^5.2.7", + "@fontsource/roboto": "^5.2.10", + "@tryretool/custom-component-support": "latest", + "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": "Cards", + "label": "Cards", + "description": "Dynamic Cards", + "entryPoint": "src/index.tsx", + "outputPath": "dist" + } +} \ No newline at end of file diff --git a/components/dynamic-cards/src/components/card.tsx b/components/dynamic-cards/src/components/card.tsx new file mode 100644 index 0000000..99f0dc1 --- /dev/null +++ b/components/dynamic-cards/src/components/card.tsx @@ -0,0 +1,943 @@ +import React, { FC, useEffect, useMemo } from "react"; +import { Retool } from "@tryretool/custom-component-support"; + +type GenericRow = Record; + +type FieldMeta = { + allFields: string[]; + numericFields: string[]; + textFields: string[]; +}; + +type CardsPerRowOption = "one" | "two" | "three" | "four" | "five" | "six"; +type FontFamilyOption = + | "inter" + | "roboto" + | "openSans" + | "lato" + | "poppins" + | "montserrat" + | "playfairDisplay"; + +function isFiniteNumber(value: unknown): value is number { + return typeof value === "number" && Number.isFinite(value); +} + +function formatCompactNumber(value: number, decimals = 1) { + return new Intl.NumberFormat("en-US", { + notation: "compact", + maximumFractionDigits: decimals, + }).format(value); +} + +function clampNumber(value: number, min: number, max: number) { + return Math.min(max, Math.max(min, value)); +} + +function cardsPerRowFromOption(option: CardsPerRowOption): number { + switch (option) { + case "one": + return 1; + case "two": + return 2; + case "three": + return 3; + case "four": + return 4; + case "five": + return 5; + case "six": + return 6; + default: + return 4; + } +} + +function fontFamilyFromOption(option: FontFamilyOption): string { + switch (option) { + case "inter": + return "'Inter', sans-serif"; + case "roboto": + return "'Roboto', sans-serif"; + case "openSans": + return "'Open Sans', sans-serif"; + case "lato": + return "'Lato', sans-serif"; + case "poppins": + return "'Poppins', sans-serif"; + case "montserrat": + return "'Montserrat', sans-serif"; + case "playfairDisplay": + return "'Playfair Display', serif"; + default: + return "'Inter', sans-serif"; + } +} + +export const DynamicKpiCards: FC = () => { + Retool.useComponentSettings({ + defaultHeight: 24, + defaultWidth: 8, + }); + + const [data] = Retool.useStateArray({ + name: "data", + initialValue: [], + inspector: "text", + label: "Data", + description: "Array of objects used to generate the KPI cards.", + }); + + const [selectedLabelField, setSelectedLabelField] = Retool.useStateString({ + name: "selectedLabelField", + initialValue: "", + inspector: "text", + label: "Label field", + description: "Field used as the card title or label. Leave blank to auto-detect.", + }); + + const [selectedValueField, setSelectedValueField] = Retool.useStateString({ + name: "selectedValueField", + initialValue: "", + inspector: "text", + label: "Value field", + description: "Numeric field used as the main KPI value. Leave blank to auto-detect.", + }); + + const [selectedSecondaryField, setSelectedSecondaryField] = Retool.useStateString({ + name: "selectedSecondaryField", + initialValue: "", + inspector: "text", + label: "Secondary field", + description: "Optional numeric field shown below the primary row.", + }); + + const [selectedTrendField, setSelectedTrendField] = Retool.useStateString({ + name: "selectedTrendField", + initialValue: "", + inspector: "text", + label: "Trend field", + description: "Optional numeric field shown beside the main value.", + }); + + const [title] = Retool.useStateString({ + name: "title", + initialValue: "Dynamic KPI Cards", + inspector: "text", + label: "Title", + description: "Main heading displayed above the KPI cards.", + }); + + const [subtitle] = Retool.useStateString({ + name: "subtitle", + initialValue: "", + inspector: "text", + label: "Subtitle", + description: "Optional helper text displayed below the title.", + }); + + const [showDetectedKeys] = Retool.useStateBoolean({ + name: "showDetectedKeys", + initialValue: true, + inspector: "checkbox", + label: "Show detected keys", + description: "Shows available fields and resolved field mappings for debugging or setup.", + }); + + const [showSecondaryValue] = Retool.useStateBoolean({ + name: "showSecondaryValue", + initialValue: true, + inspector: "checkbox", + label: "Show secondary value", + description: "Controls whether the secondary metric is displayed on each card.", + }); + + const [showTrend] = Retool.useStateBoolean({ + name: "showTrend", + initialValue: true, + inspector: "checkbox", + label: "Show trend", + description: "Controls whether the trend indicator is displayed beside the main value.", + }); + + const [cardsPerRowSelect] = Retool.useStateEnumeration< + ["one", "two", "three", "four", "five", "six"] + >({ + name: "cardsPerRowSelect", + initialValue: "four", + enumDefinition: ["one", "two", "three", "four", "five", "six"], + enumLabels: { + one: "1", + two: "2", + three: "3", + four: "4", + five: "5", + six: "6", + }, + inspector: "select", + label: "Cards per row", + description: "Select the number of cards per row.", + }); + + const [roundDecimals] = Retool.useStateNumber({ + name: "roundDecimals", + initialValue: 1, + inspector: "text", + label: "Decimals", + description: "Maximum number of decimal places used when formatting values.", + }); + + const [fontFamilySelect] = Retool.useStateEnumeration< + ["inter", "roboto", "openSans", "lato", "poppins", "montserrat", "playfairDisplay"] + >({ + name: "fontFamilySelect", + initialValue: "inter", + enumDefinition: ["inter", "roboto", "openSans", "lato", "poppins", "montserrat", "playfairDisplay"], + enumLabels: { + inter: "Inter", + roboto: "Roboto", + openSans: "Open Sans", + lato: "Lato", + poppins: "Poppins", + montserrat: "Montserrat", + playfairDisplay: "Playfair Display", + }, + inspector: "select", + label: "Font family", + description: "Select a predefined font family.", + }); + + const [titleColor] = Retool.useStateString({ + name: "titleColor", + initialValue: "#111827", + inspector: "text", + label: "Title color", + description: "Color used for the component title.", + }); + + const [mutedTextColor] = Retool.useStateString({ + name: "mutedTextColor", + initialValue: "#6b7280", + inspector: "text", + label: "Muted text color", + description: "Color used for labels and helper text.", + }); + + const [backgroundColor] = Retool.useStateString({ + name: "backgroundColor", + initialValue: "#ffffff", + inspector: "text", + label: "Background color", + description: "Background color of the overall component container.", + }); + + const [borderColor] = Retool.useStateString({ + name: "borderColor", + initialValue: "#e5e7eb", + inspector: "text", + label: "Border color", + description: "Border color for the component and unselected cards.", + }); + + const [cardBg] = Retool.useStateString({ + name: "cardBg", + initialValue: "#ffffff", + inspector: "text", + label: "Card background", + description: "Background color of each KPI card.", + }); + + const [cardShadow] = Retool.useStateString({ + name: "cardShadow", + initialValue: "0 1px 2px rgba(0,0,0,0.06)", + inspector: "text", + label: "Card shadow", + description: "CSS box-shadow applied to unselected cards.", + }); + + const [valueColor] = Retool.useStateString({ + name: "valueColor", + initialValue: "#111827", + inspector: "text", + label: "Primary value color", + description: "Color used for the main KPI value.", + }); + + const [secondaryValueColor] = Retool.useStateString({ + name: "secondaryValueColor", + initialValue: "#6b7280", + inspector: "text", + label: "Secondary value color", + description: "Color used for the optional secondary value.", + }); + + const [positiveTrendColor] = Retool.useStateString({ + name: "positiveTrendColor", + initialValue: "#15803d", + inspector: "text", + label: "Positive trend color", + description: "Color used when the trend value is positive.", + }); + + const [negativeTrendColor] = Retool.useStateString({ + name: "negativeTrendColor", + initialValue: "#dc2626", + inspector: "text", + label: "Negative trend color", + description: "Color used when the trend value is negative.", + }); + + const [neutralTrendColor] = Retool.useStateString({ + name: "neutralTrendColor", + initialValue: "#6b7280", + inspector: "text", + label: "Neutral trend color", + description: "Color used when the trend value is zero or unavailable.", + }); + + const [selectedCardBorderColor] = Retool.useStateString({ + name: "selectedCardBorderColor", + initialValue: "#111827", + inspector: "text", + label: "Selected card border", + description: "Border color used for the selected card.", + }); + + const [validationMessage, setValidationMessage] = Retool.useStateString({ + name: "validationMessage", + initialValue: "", + inspector: "hidden", + label: "Validation message", + }); + + const [resolvedConfig, setResolvedConfig] = Retool.useStateObject({ + name: "resolvedConfig", + initialValue: {}, + inspector: "hidden", + label: "Resolved config", + }); + + const [selectedCard, setSelectedCard] = Retool.useStateObject({ + name: "selectedCard", + initialValue: {}, + inspector: "hidden", + label: "Selected card", + }); + + const cardClick = Retool.useEventCallback({ name: "cardClick" }); + + const safeRows: GenericRow[] = useMemo(() => { + return Array.isArray(data) + ? (data.filter((row) => row && typeof row === "object") as GenericRow[]) + : []; + }, [data]); + + const fieldMeta: FieldMeta = useMemo(() => { + const keyOrder: string[] = []; + const keySet = new Set(); + const numeric = new Set(); + const text = new Set(); + + for (const row of safeRows) { + for (const key of Object.keys(row)) { + if (!keySet.has(key)) { + keySet.add(key); + keyOrder.push(key); + } + } + + for (const [k, value] of Object.entries(row)) { + if (isFiniteNumber(value)) numeric.add(k); + if (typeof value === "string" && value.trim() !== "") text.add(k); + } + } + + return { + allFields: keyOrder, + numericFields: keyOrder.filter((k) => numeric.has(k)), + textFields: keyOrder.filter((k) => text.has(k)), + }; + }, [safeRows]); + + const autoDetectedFields = useMemo(() => { + const normalize = (value: string) => value.toLowerCase().replace(/[\s_-]+/g, ""); + + const pickByPriority = (candidates: string[], priorities: string[]) => { + if (!candidates.length) return ""; + + const normalizedCandidates = candidates.map((c) => ({ + raw: c, + normalized: normalize(c), + })); + + for (const priority of priorities) { + const exact = normalizedCandidates.find((c) => c.normalized === normalize(priority)); + if (exact) return exact.raw; + } + + for (const priority of priorities) { + const partial = normalizedCandidates.find((c) => c.normalized.includes(normalize(priority))); + if (partial) return partial.raw; + } + + return candidates[0] || ""; + }; + + const labelCandidates = fieldMeta.textFields.length + ? fieldMeta.textFields + : fieldMeta.allFields.filter((f) => !fieldMeta.numericFields.includes(f)); + + const value = pickByPriority(fieldMeta.numericFields, [ + "value", + "amount", + "count", + "total", + "revenue", + "sales", + "users", + "score", + "metricValue", + "ret", + "step", + "emp", + ]); + + const label = pickByPriority( + labelCandidates.length ? labelCandidates : fieldMeta.allFields, + ["label", "name", "title", "metric", "kpi", "category", "segment", "date", "cohort", "type"] + ); + + const secondary = pickByPriority( + fieldMeta.numericFields.filter((f) => f !== value), + ["secondaryValue", "previous", "target", "baseline", "benchmark", "users", "count"] + ); + + const trend = pickByPriority( + fieldMeta.numericFields.filter((f) => f !== value && f !== secondary), + ["trend", "change", "delta", "percentChange", "growth", "increase", "decrease"] + ); + + return { label, value, secondary, trend }; + }, [fieldMeta]); + + useEffect(() => { + if (!selectedLabelField && autoDetectedFields.label) { + setSelectedLabelField(autoDetectedFields.label); + } + }, [selectedLabelField, autoDetectedFields.label, setSelectedLabelField]); + + useEffect(() => { + if (!selectedValueField && autoDetectedFields.value) { + setSelectedValueField(autoDetectedFields.value); + } + }, [selectedValueField, autoDetectedFields.value, setSelectedValueField]); + + useEffect(() => { + if (!selectedSecondaryField && autoDetectedFields.secondary) { + setSelectedSecondaryField(autoDetectedFields.secondary); + } + }, [selectedSecondaryField, autoDetectedFields.secondary, setSelectedSecondaryField]); + + useEffect(() => { + if (!selectedTrendField && autoDetectedFields.trend) { + setSelectedTrendField(autoDetectedFields.trend); + } + }, [selectedTrendField, autoDetectedFields.trend, setSelectedTrendField]); + + useEffect(() => { + if (selectedLabelField && !fieldMeta.allFields.includes(selectedLabelField)) { + setSelectedLabelField(autoDetectedFields.label || ""); + } + }, [selectedLabelField, fieldMeta.allFields, autoDetectedFields.label, setSelectedLabelField]); + + useEffect(() => { + if (selectedValueField && !fieldMeta.numericFields.includes(selectedValueField)) { + setSelectedValueField(autoDetectedFields.value || ""); + } + }, [selectedValueField, fieldMeta.numericFields, autoDetectedFields.value, setSelectedValueField]); + + useEffect(() => { + if (selectedSecondaryField && !fieldMeta.numericFields.includes(selectedSecondaryField)) { + setSelectedSecondaryField(autoDetectedFields.secondary || ""); + } + }, [selectedSecondaryField, fieldMeta.numericFields, autoDetectedFields.secondary, setSelectedSecondaryField]); + + useEffect(() => { + if (selectedTrendField && !fieldMeta.numericFields.includes(selectedTrendField)) { + setSelectedTrendField(autoDetectedFields.trend || ""); + } + }, [selectedTrendField, fieldMeta.numericFields, autoDetectedFields.trend, setSelectedTrendField]); + + const resolveFields = ( + meta: FieldMeta, + rawLabel: string, + rawValue: string, + rawSecondary: string, + rawTrend: string, + autoLabel: string, + autoValue: string, + autoSecondary: string, + autoTrend: string + ) => { + const allFields = meta.allFields; + const numericFields = meta.numericFields; + + let resolvedLabel = rawLabel || autoLabel; + let resolvedValue = rawValue || autoValue; + let resolvedSecondary = rawSecondary || autoSecondary; + let resolvedTrend = rawTrend || autoTrend; + + if (!resolvedLabel || !allFields.includes(resolvedLabel)) resolvedLabel = autoLabel; + if (!resolvedValue || !numericFields.includes(resolvedValue)) resolvedValue = autoValue; + if (resolvedSecondary && !numericFields.includes(resolvedSecondary)) resolvedSecondary = autoSecondary; + if (resolvedTrend && !numericFields.includes(resolvedTrend)) resolvedTrend = autoTrend; + + if (resolvedSecondary === resolvedValue) { + resolvedSecondary = numericFields.find((f) => f !== resolvedValue) || ""; + } + + if (resolvedTrend === resolvedValue || resolvedTrend === resolvedSecondary) { + resolvedTrend = numericFields.find((f) => f !== resolvedValue && f !== resolvedSecondary) || ""; + } + + return { + resolvedLabel, + resolvedValue, + resolvedSecondary, + resolvedTrend, + }; + }; + + const resolvedFields = useMemo(() => { + return resolveFields( + fieldMeta, + selectedLabelField, + selectedValueField, + selectedSecondaryField, + selectedTrendField, + autoDetectedFields.label, + autoDetectedFields.value, + autoDetectedFields.secondary, + autoDetectedFields.trend + ); + }, [ + fieldMeta, + selectedLabelField, + selectedValueField, + selectedSecondaryField, + selectedTrendField, + autoDetectedFields, + ]); + + const resolvedLabelField = resolvedFields.resolvedLabel; + const resolvedValueField = resolvedFields.resolvedValue; + const resolvedSecondaryField = resolvedFields.resolvedSecondary; + const resolvedTrendField = resolvedFields.resolvedTrend; + + const normalizedCardsPerRow = cardsPerRowFromOption(cardsPerRowSelect); + + const safeRoundDecimals = useMemo(() => { + if (!Number.isFinite(roundDecimals)) return 1; + return clampNumber(Math.round(roundDecimals), 0, 6); + }, [roundDecimals]); + + const resolvedFontFamily = useMemo(() => { + return fontFamilyFromOption(fontFamilySelect); + }, [fontFamilySelect]); + + const validation = useMemo(() => { + if (!Array.isArray(data)) { + return { ok: false, message: "Data must be an array of objects." }; + } + + if (safeRows.length === 0) { + return { ok: false, message: "No rows found. Pass an array of objects." }; + } + + if (!resolvedLabelField) { + return { ok: false, message: "Could not resolve label field." }; + } + + if (!resolvedValueField) { + return { ok: false, message: "Could not resolve value field." }; + } + + if (!fieldMeta.allFields.includes(resolvedLabelField)) { + return { ok: false, message: `Label field "${resolvedLabelField}" is not present in data.` }; + } + + if (!fieldMeta.numericFields.includes(resolvedValueField)) { + return { ok: false, message: `Value field "${resolvedValueField}" is not numeric.` }; + } + + return { ok: true, message: "" }; + }, [data, safeRows, resolvedLabelField, resolvedValueField, fieldMeta]); + + useEffect(() => { + setValidationMessage(validation.message); + }, [validation.message, setValidationMessage]); + + useEffect(() => { + setResolvedConfig({ + selectedLabelField, + selectedValueField, + selectedSecondaryField, + selectedTrendField, + resolvedLabelField, + resolvedValueField, + resolvedSecondaryField, + resolvedTrendField, + availableFields: fieldMeta.allFields, + numericFields: fieldMeta.numericFields, + textFields: fieldMeta.textFields, + autoDetectedFields, + cardsPerRowSelect, + normalizedCardsPerRow, + safeRoundDecimals, + fontFamilySelect, + resolvedFontFamily, + }); + }, [ + selectedLabelField, + selectedValueField, + selectedSecondaryField, + selectedTrendField, + resolvedLabelField, + resolvedValueField, + resolvedSecondaryField, + resolvedTrendField, + fieldMeta, + autoDetectedFields, + cardsPerRowSelect, + normalizedCardsPerRow, + safeRoundDecimals, + fontFamilySelect, + resolvedFontFamily, + setResolvedConfig, + ]); + + const cards = useMemo(() => { + if (!validation.ok) { + return []; + } + + return safeRows.map((row, index) => { + const label = row[resolvedLabelField]; + const value = row[resolvedValueField]; + const secondary = resolvedSecondaryField ? row[resolvedSecondaryField] : null; + const trend = resolvedTrendField ? row[resolvedTrendField] : null; + + return { + key: `${String(label ?? index)}__${index}`, + label: label == null || String(label).trim() === "" ? `Item ${index + 1}` : String(label), + value: isFiniteNumber(value) ? value : null, + secondary: isFiniteNumber(secondary) ? secondary : null, + trend: isFiniteNumber(trend) ? trend : null, + row, + }; + }); + }, [ + validation.ok, + safeRows, + resolvedLabelField, + resolvedValueField, + resolvedSecondaryField, + resolvedTrendField, + ]); + + const validCardCount = useMemo(() => { + return cards.filter((card) => card.value != null).length; + }, [cards]); + + const hasRenderableData = validation.ok && cards.length > 0 && validCardCount > 0; + + const selectedCardKey = useMemo(() => { + if (!selectedCard || typeof selectedCard !== "object") return ""; + const k = (selectedCard as Record).key; + return typeof k === "string" ? k : ""; + }, [selectedCard]); + + const trendColor = (trend: number | null) => { + if (trend == null) return neutralTrendColor; + if (trend > 0) return positiveTrendColor; + if (trend < 0) return negativeTrendColor; + return neutralTrendColor; + }; + + const trendPrefix = (trend: number | null) => { + if (trend == null) return ""; + if (trend > 0) return "+"; + return ""; + }; + + return ( +
+ + +
+
+
+ {title} +
+ {subtitle ? ( +
{subtitle}
+ ) : null} +
+ + {showDetectedKeys ? ( +
+
+ Fields: {fieldMeta.allFields.join(", ") || "None"} +
+
+ Numeric: {fieldMeta.numericFields.join(", ") || "None"} +
+
+ Resolved: Label = {resolvedLabelField || "-"}, Value = {resolvedValueField || "-"}, + Secondary = {resolvedSecondaryField || "-"}, Trend = {resolvedTrendField || "-"} +
+
+ Cards/row: {normalizedCardsPerRow} +
+
+ Font: {fontFamilySelect} +
+
+ ) : null} + + {!validation.ok ? ( +
+
Invalid data
+
{validation.message}
+
+ ) : !hasRenderableData ? ( +
+
+
+ No data +
+
+ Rows are present, but no valid numeric values were found for the selected value field. +
+
+
+ ) : ( +
+
+ {cards.map((card) => { + const isSelected = card.key === selectedCardKey; + + return ( + + ); + })} +
+
+ )} +
+
+ ); +}; + +export default DynamicKpiCards; \ No newline at end of file diff --git a/components/dynamic-cards/src/index.tsx b/components/dynamic-cards/src/index.tsx new file mode 100644 index 0000000..b61badf --- /dev/null +++ b/components/dynamic-cards/src/index.tsx @@ -0,0 +1 @@ +export { default as Card } from './components/card'; \ No newline at end of file From eb4d93e78890df0aafdf574e21d757eda1fbde5d Mon Sep 17 00:00:00 2001 From: widlestudiollp Date: Fri, 24 Apr 2026 12:50:30 +0530 Subject: [PATCH 2/2] Changes --- components/dynamic-cards/cover.png | Bin 0 -> 31432 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 components/dynamic-cards/cover.png diff --git a/components/dynamic-cards/cover.png b/components/dynamic-cards/cover.png new file mode 100644 index 0000000000000000000000000000000000000000..5eb3de223e0e1dcbf59c53f1c43eaf00868d59c2 GIT binary patch literal 31432 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 K$4kHSy816@=-z1n literal 0 HcmV?d00001