@rebasepro/plugin-insights 0.9.1-canary.fd3754b → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.es.js CHANGED
@@ -153,10 +153,12 @@ function formatNumber(value, format) {
153
153
  if (value === null || value === void 0) return "N/A";
154
154
  const options = {
155
155
  style: format?.style ?? "decimal",
156
- notation: format?.notation ?? "standard",
157
- maximumFractionDigits: format?.decimals ?? 1,
158
- minimumFractionDigits: format?.decimals ?? 1
156
+ notation: format?.notation ?? "standard"
159
157
  };
158
+ if (format?.decimals !== void 0) {
159
+ options.maximumFractionDigits = format.decimals;
160
+ options.minimumFractionDigits = format.decimals;
161
+ }
160
162
  if (format?.style === "currency") options.currency = format.currency ?? "USD";
161
163
  let formatted = new Intl.NumberFormat("en-US", options).format(value);
162
164
  if (format?.showSign && value > 0) formatted = "+" + formatted;
@@ -573,6 +575,8 @@ function useInsightsPlugin(config) {
573
575
  slot: "collection.insights",
574
576
  Component: (props) => {
575
577
  if ((props.path?.split("/").filter(Boolean).pop() ?? "") !== slug) return null;
578
+ const parentEntityIds = props.parentEntityIds;
579
+ if (parentEntityIds && parentEntityIds.length > 0) return null;
576
580
  return /* @__PURE__ */ jsx(CollectionInsightsInline, {
577
581
  ...props,
578
582
  insights: collectionInsights
@@ -1 +1 @@
1
- {"version":3,"file":"index.es.js","names":[],"sources":["../src/engine/InsightsCache.ts","../src/engine/InsightsProvider.tsx","../src/engine/useInsightsData.ts","../src/components/InsightsScorecardView.tsx","../src/components/InsightWidgetSkeleton.tsx","../src/components/InsightWidget.tsx","../src/components/HomeCardInsightSlot.tsx","../src/components/HomeInsightsSlot.tsx","../src/components/CollectionInsightsInline.tsx","../src/useInsightsPlugin.tsx"],"sourcesContent":["import type { InsightDataResult } from \"../types\";\n\ninterface CacheEntry {\n data: InsightDataResult;\n timestamp: number;\n}\n\n/**\n * In-memory cache for insight query results.\n * Supports TTL-based expiry and inflight request deduplication\n * to prevent redundant network requests when multiple widgets\n * share the same query.\n */\nexport class InsightsCache {\n private cache = new Map<string, CacheEntry>();\n private inflight = new Map<string, Promise<InsightDataResult>>();\n\n constructor(private ttl = 60_000) {}\n\n get(key: string): InsightDataResult | null {\n const entry = this.cache.get(key);\n if (!entry) return null;\n if (Date.now() - entry.timestamp > this.ttl) {\n this.cache.delete(key);\n return null;\n }\n return entry.data;\n }\n\n set(key: string, data: InsightDataResult): void {\n this.cache.set(key, { data,\ntimestamp: Date.now() });\n this.inflight.delete(key);\n }\n\n getInflight(key: string): Promise<InsightDataResult> | null {\n return this.inflight.get(key) ?? null;\n }\n\n setInflight(key: string, promise: Promise<InsightDataResult>): void {\n this.inflight.set(key, promise);\n }\n\n invalidate(key?: string): void {\n if (key) {\n this.cache.delete(key);\n this.inflight.delete(key);\n } else {\n this.cache.clear();\n this.inflight.clear();\n }\n }\n}\n","import React, { createContext, useContext, useMemo, type PropsWithChildren } from \"react\";\nimport { InsightsCache } from \"./InsightsCache\";\n\ninterface InsightsContextValue {\n cache: InsightsCache;\n}\n\nconst InsightsContext = createContext<InsightsContextValue | null>(null);\n\n/**\n * Root-level provider for the insights data engine.\n * Injected automatically by the plugin via `providers: [{ scope: \"root\" }]`.\n *\n * Manages a single `InsightsCache` instance shared by all insight widgets\n * for TTL-based caching and inflight request deduplication.\n */\nexport function InsightsProvider({\n cacheTTL,\n children\n}: PropsWithChildren<{ cacheTTL?: number }>) {\n const cache = useMemo(() => new InsightsCache(cacheTTL), [cacheTTL]);\n const value = useMemo(() => ({ cache }), [cache]);\n\n return (\n <InsightsContext.Provider value={value}>\n {children}\n </InsightsContext.Provider>\n );\n}\n\n/**\n * Access the insights cache (for advanced usage).\n * Returns null when called outside of an `InsightsProvider`\n * (e.g. during auth-loading phase before plugin providers mount).\n */\nexport function useInsightsEngine(): InsightsContextValue | null {\n return useContext(InsightsContext);\n}\n","import { useEffect, useState } from \"react\";\nimport type { InsightDefinition, InsightDataResult, InsightContext } from \"../types\";\nimport { useInsightsEngine } from \"./InsightsProvider\";\nimport { useAuthController } from \"@rebasepro/app\";\n\n/**\n * Hook that fetches and caches data for a single insight definition.\n *\n * Calls the definition's own `data()` callback and manages:\n * - TTL-based caching via InsightsCache\n * - Inflight request deduplication (multiple mounts of the same widget)\n * - Loading and error state management\n *\n * @param definition - The insight to fetch data for\n * @param collectionSlug - Optional collection context for cache key scoping\n */\nexport function useInsightsData(\n definition: InsightDefinition,\n context: InsightContext\n): {\n data: InsightDataResult | null;\n loading: boolean;\n error: Error | null;\n} {\n const engine = useInsightsEngine();\n const cache = engine?.cache ?? null;\n const { initialLoading, authLoading, user, loginSkipped } = useAuthController();\n const authReady = !initialLoading && !authLoading && (Boolean(user) || loginSkipped);\n const [data, setData] = useState<InsightDataResult | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const cacheKey = `${definition.id}:${context.path ?? context.collectionSlug ?? \"global\"}`;\n\n useEffect(() => {\n // Keep showing skeleton until both auth and engine are ready\n if (!authReady || !cache) {\n return;\n }\n\n let cancelled = false;\n\n // 1. Check cache\n const cached = cache.get(cacheKey);\n if (cached) {\n setData(cached);\n setLoading(false);\n return;\n }\n\n // 2. Check inflight — deduplicate concurrent requests for the same widget\n const inflight = cache.getInflight(cacheKey);\n if (inflight) {\n setLoading(true);\n inflight\n .then((result) => {\n if (!cancelled) {\n setData(result);\n }\n })\n .catch((err) => {\n if (!cancelled) setError(err instanceof Error ? err : new Error(String(err)));\n })\n .finally(() => {\n if (!cancelled) setLoading(false);\n });\n return;\n }\n\n // 3. Fresh fetch — invoke the definition's own data callback\n setLoading(true);\n setError(null);\n\n const promise = definition.data(context);\n\n cache.setInflight(cacheKey, promise);\n\n promise\n .then((result) => {\n cache.set(cacheKey, result);\n if (!cancelled) {\n setData(result);\n }\n })\n .catch((err) => {\n cache.invalidate(cacheKey);\n if (!cancelled) {\n setError(err instanceof Error ? err : new Error(String(err)));\n }\n })\n .finally(() => {\n if (!cancelled) setLoading(false);\n });\n\n return () => {\n cancelled = true;\n };\n }, [definition.id, definition.data, context.path, context.collectionSlug, cacheKey, cache, authReady]);\n\n return { data,\nloading,\nerror };\n}\n","import React, { useRef, useState } from \"react\";\nimport { getIcon } from \"@rebasepro/app\";\nimport { cls, defaultBorderMixin } from \"@rebasepro/ui\";\nimport type { DataRow, ScorecardConfig, ScorecardFormat } from \"../types\";\n\nfunction formatNumber(value: number, format?: ScorecardFormat): string {\n if (value === null || value === undefined) return \"N/A\";\n\n const options: Intl.NumberFormatOptions = {\n style: format?.style ?? \"decimal\",\n notation: format?.notation ?? \"standard\",\n maximumFractionDigits: format?.decimals ?? 1,\n minimumFractionDigits: format?.decimals ?? 1\n };\n\n if (format?.style === \"currency\") {\n options.currency = format.currency ?? \"USD\";\n }\n\n let formatted = new Intl.NumberFormat(\"en-US\", options).format(value);\n\n if (format?.showSign && value > 0) {\n formatted = \"+\" + formatted;\n }\n\n return formatted;\n}\n\n/**\n * Scorecard widget for the Rebase design system.\n *\n * Renders a single KPI metric with optional comparison value and icon.\n * Uses Tailwind `dark:` classes — no JS dark mode detection.\n * Icons are resolved via `getIcon` from `@rebasepro/app`.\n */\nexport function InsightsScorecardView({\n config,\n data,\n title,\n compact = false,\n embedded = false,\n fixedHeight\n}: {\n config: ScorecardConfig;\n data: DataRow;\n title: string;\n compact?: boolean;\n /** When true, skip own border/bg since the parent card provides them. */\n embedded?: boolean;\n /** Explicit height to prevent layout shift between skeleton → loaded. */\n fixedHeight?: number;\n}) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [containerWidth, setContainerWidth] = useState<number | null>(null);\n\n React.useLayoutEffect(() => {\n if (!containerRef.current) return;\n // Read initial width synchronously before paint\n setContainerWidth(containerRef.current.offsetWidth);\n const observer = new ResizeObserver((entries) => {\n for (const entry of entries) {\n setContainerWidth(entry.contentRect.width);\n }\n });\n observer.observe(containerRef.current);\n return () => observer.disconnect();\n }, []);\n\n const mainValue = data[config.value.field];\n const formattedValue = typeof mainValue === \"number\"\n ? formatNumber(mainValue, config.value.format)\n : String(mainValue ?? \"N/A\");\n\n // Comparison rendering\n let comparisonElement: React.ReactNode = null;\n if (config.comparison) {\n const comparisonValue = data[config.comparison.field];\n if (typeof comparisonValue === \"number\") {\n const formattedComparison = formatNumber(comparisonValue, config.comparison.format);\n const isPositive = comparisonValue > 0;\n const isNegative = comparisonValue < 0;\n\n let colorClass = \"text-surface-500 dark:text-surface-400\";\n if (config.comparison.intent === \"increase_is_good\") {\n if (isPositive) colorClass = \"text-emerald-500\";\n if (isNegative) colorClass = \"text-red-500\";\n } else if (config.comparison.intent === \"decrease_is_good\") {\n if (isPositive) colorClass = \"text-red-500\";\n if (isNegative) colorClass = \"text-emerald-500\";\n }\n\n comparisonElement = (\n <span className={`font-medium ${compact ? \"text-[10px]\" : \"text-xs\"} ${colorClass}`}>\n {formattedComparison}\n </span>\n );\n }\n }\n\n const isSmall = compact || (containerWidth !== null && containerWidth < 200);\n\n // Resolve icon via getIcon (Lucide-based resolution)\n const iconElement = config.icon\n ? getIcon(config.icon, \"text-surface-400 dark:text-surface-500\", undefined, isSmall ? 14 : 18)\n : null;\n\n // ── Compact card-inline layout ──────────────────────────────────────\n if (compact) {\n return (\n <div className={cls(\"flex flex-col gap-0.5 px-2.5 py-2 rounded-md bg-transparent border min-w-0\", defaultBorderMixin)}>\n <span className=\"text-[10px] uppercase tracking-wider text-surface-400 dark:text-surface-500 truncate\">\n {title}\n </span>\n <div className=\"flex items-baseline gap-1.5\">\n <span className=\"text-sm font-semibold tabular-nums text-surface-800 dark:text-surface-100\">\n {formattedValue}\n </span>\n {comparisonElement}\n </div>\n </div>\n );\n }\n\n // ── Standard scorecard layout ───────────────────────────────────────\n const baseClass = embedded\n ? `flex flex-col min-w-0 h-full ${isSmall ? \"px-3.5 py-3\" : \"px-5 py-4\"}`\n : cls(\"rounded-lg flex flex-col min-w-0 bg-transparent border\", defaultBorderMixin, isSmall ? \"px-3.5 py-3\" : \"px-5 py-4\");\n\n return (\n <div ref={containerRef} className={baseClass} style={embedded ? undefined : fixedHeight ? { height: fixedHeight } : { minHeight: isSmall ? 68 : 92 }}>\n {/* Title row */}\n <div className={`flex items-center justify-between ${isSmall ? \"mb-1\" : \"mb-2\"}`}>\n <div className=\"flex flex-col min-w-0\">\n <span className={`font-medium leading-snug truncate text-surface-500 dark:text-surface-400 ${isSmall ? \"text-[11px]\" : \"text-xs\"}`}>\n {title}\n </span>\n {config.dateRange && !isSmall && (\n <span className=\"text-[10px] text-surface-400 dark:text-surface-500 truncate mt-0.5\">\n {config.dateRange}\n </span>\n )}\n </div>\n {iconElement && (\n <span className=\"ml-2 shrink-0\">{iconElement}</span>\n )}\n </div>\n\n {/* Main value */}\n <div className={`font-semibold leading-tight tracking-tight break-all text-surface-800 dark:text-surface-100 ${isSmall ? \"text-lg\" : (containerWidth !== null && containerWidth < 300) ? \"text-xl\" : \"text-2xl\"}`}>\n {formattedValue}\n </div>\n\n {/* Comparison */}\n {comparisonElement && (\n <div className={isSmall ? \"mt-0.5\" : \"mt-1\"}>\n {comparisonElement}\n </div>\n )}\n </div>\n );\n}\n\nInsightsScorecardView.displayName = \"InsightsScorecardView\";\n","import React, { useRef, useState } from \"react\";\nimport { cls, defaultBorderMixin } from \"@rebasepro/ui\";\nimport type { ScorecardConfig } from \"../types\";\n\n/**\n * Skeleton loader for scorecard insight widgets — displays animated\n * shimmer placeholders that exactly match the final rendered layout\n * of InsightsScorecardView for a given config, preventing layout shift.\n *\n * The skeleton receives the scorecard config so it can conditionally\n * render placeholder lines for comparison, dateRange, and icon —\n * only when the loaded view will also render them.\n *\n * The standard skeleton mirrors InsightsScorecardView's responsive\n * container-width breakpoints (ResizeObserver → isSmall / isMedium)\n * and uses placeholder heights that exactly match the **computed**\n * Tailwind line-heights (accounting for `leading-*` overrides).\n * This guarantees a pixel-perfect skeleton → loaded transition.\n */\nexport function InsightWidgetSkeleton({\n config,\n compact = false,\n embedded = false,\n fixedHeight\n}: {\n /** Scorecard config — used to match optional elements (comparison, dateRange, icon). */\n config: ScorecardConfig;\n compact?: boolean;\n /** When true, skip own border since the parent card provides it. */\n embedded?: boolean;\n /** Explicit height to prevent layout shift between skeleton → loaded. */\n fixedHeight?: number;\n}) {\n const hasComparison = Boolean(config.comparison);\n const hasIcon = Boolean(config.icon);\n const hasDateRange = Boolean(config.dateRange);\n\n // ── Compact scorecard skeleton ──────────────────────────────────────\n // Matches InsightsScorecardView compact layout:\n // container: flex flex-col gap-0.5 px-2.5 py-2 rounded-md border\n // title: text-[10px] uppercase → line-height ~14px\n // value row: text-sm font-semibold → line-height 20px\n // + optional comparison text-[10px] inside value row\n if (compact) {\n return (\n <div\n className={cls(\n \"animate-pulse\",\n embedded\n ? \"h-full px-2.5 py-2\"\n : \"flex flex-col gap-0.5 rounded-md bg-transparent border min-w-0 px-2.5 py-2\",\n !embedded && defaultBorderMixin\n )}\n >\n {/* Title line */}\n <div className=\"bg-surface-200 dark:bg-surface-700 rounded-sm\"\n style={{ height: 14,\nwidth: 48 }}\n />\n {/* Value + optional comparison row */}\n <div className=\"flex items-baseline gap-1.5\">\n <div className=\"bg-surface-200 dark:bg-surface-700 rounded-sm\"\n style={{ height: 20,\nwidth: 40 }}\n />\n {hasComparison && (\n <div className=\"bg-surface-200/60 dark:bg-surface-700/60 rounded-sm\"\n style={{ height: 14,\nwidth: 28 }}\n />\n )}\n </div>\n </div>\n );\n }\n\n // ── Standard scorecard skeleton ─────────────────────────────────────\n return <StandardSkeleton\n hasComparison={hasComparison}\n hasIcon={hasIcon}\n hasDateRange={hasDateRange}\n embedded={embedded}\n fixedHeight={fixedHeight}\n />;\n}\n\n// ── Tailwind line-height reference ──────────────────────────────────────\n// All heights below are the **computed** CSS line-heights, accounting\n// for `leading-*` overrides that InsightsScorecardView applies.\n//\n// Title:\n// text-xs (12px) + leading-snug (1.375) → 12 × 1.375 = 16.5px\n// text-[11px] + leading-snug (1.375) → 11 × 1.375 = 15.125px\n//\n// DateRange:\n// text-[10px] with no explicit LH → normal ≈ 14px (browser)\n//\n// Value:\n// text-2xl (24px) + leading-tight (1.25) → 24 × 1.25 = 30px\n// text-xl (20px) + leading-tight (1.25) → 20 × 1.25 = 25px\n// text-lg (18px) + leading-tight (1.25) → 18 × 1.25 = 22.5px\n//\n// Comparison:\n// text-xs (12px) → built-in LH 1rem = 16px\n\n/**\n * Inner component for the standard scorecard skeleton.\n *\n * Mirrors InsightsScorecardView's layout by:\n * 1. Using the same ResizeObserver + containerWidth pattern for\n * responsive breakpoints (isSmall < 200px, isMedium < 300px).\n * 2. Using placeholder heights derived from the exact computed\n * Tailwind line-heights that InsightsScorecardView renders.\n * 3. Matching all container classes, margins, paddings, and flex\n * layout properties identically.\n */\nfunction StandardSkeleton({\n hasComparison,\n hasIcon,\n hasDateRange,\n embedded,\n fixedHeight\n}: {\n hasComparison: boolean;\n hasIcon: boolean;\n hasDateRange: boolean;\n embedded: boolean;\n fixedHeight?: number;\n}) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [containerWidth, setContainerWidth] = useState<number | null>(null);\n\n React.useLayoutEffect(() => {\n if (!containerRef.current) return;\n setContainerWidth(containerRef.current.offsetWidth);\n const observer = new ResizeObserver((entries) => {\n for (const entry of entries) {\n setContainerWidth(entry.contentRect.width);\n }\n });\n observer.observe(containerRef.current);\n return () => observer.disconnect();\n }, []);\n\n // Mirror InsightsScorecardView's responsive breakpoints exactly\n const isSmall = containerWidth !== null && containerWidth < 200;\n\n // Computed line-heights for each breakpoint\n // Title: text-xs + leading-snug = 16.5px, text-[11px] + leading-snug = 15.125px\n const titleHeight = isSmall ? 15 : 16.5;\n // Value: leading-tight (×1.25) applied on top of font-size\n const valueHeight = isSmall\n ? 22.5 // text-lg: 18 × 1.25\n : (containerWidth !== null && containerWidth < 300)\n ? 25 // text-xl: 20 × 1.25\n : 30; // text-2xl: 24 × 1.25\n // Comparison: text-xs = 12px / 16px line-height (no leading override)\n const comparisonHeight = 16;\n // Icon: 14px when small, 18px otherwise\n const iconSize = isSmall ? 14 : 18;\n\n const baseClass = embedded\n ? `flex flex-col min-w-0 h-full ${isSmall ? \"px-3.5 py-3\" : \"px-5 py-4\"}`\n : cls(\"rounded-lg flex flex-col min-w-0 bg-transparent border\", defaultBorderMixin, isSmall ? \"px-3.5 py-3\" : \"px-5 py-4\");\n\n return (\n <div\n ref={containerRef}\n className={cls(\"animate-pulse\", baseClass)}\n style={embedded ? undefined : fixedHeight ? { height: fixedHeight } : { minHeight: isSmall ? 68 : 92 }}\n >\n {/* Title row — identical flex structure to InsightsScorecardView */}\n <div className={`flex items-center justify-between ${isSmall ? \"mb-1\" : \"mb-2\"}`}>\n <div className=\"flex flex-col min-w-0\">\n {/* Title placeholder */}\n <div className=\"bg-surface-200 dark:bg-surface-700 rounded\"\n style={{ height: titleHeight,\nwidth: \"60%\" }}\n />\n {/* DateRange — hidden when isSmall, same as real view (line 134) */}\n {hasDateRange && !isSmall && (\n <div className=\"bg-surface-200/60 dark:bg-surface-700/60 rounded mt-0.5\"\n style={{ height: 14,\nwidth: \"40%\" }}\n />\n )}\n </div>\n {/* Icon placeholder — same wrapper as real view */}\n {hasIcon && (\n <span className=\"ml-2 shrink-0\">\n <div className=\"bg-surface-200 dark:bg-surface-700 rounded\"\n style={{ height: iconSize,\nwidth: iconSize }}\n />\n </span>\n )}\n </div>\n\n {/* Main value placeholder */}\n <div className=\"bg-surface-200 dark:bg-surface-700 rounded\"\n style={{ height: valueHeight,\nwidth: \"40%\" }}\n />\n\n {/* Comparison placeholder */}\n {hasComparison && (\n <div className={isSmall ? \"mt-0.5\" : \"mt-1\"}>\n <div className=\"bg-surface-200/60 dark:bg-surface-700/60 rounded\"\n style={{ height: comparisonHeight,\nwidth: \"25%\" }}\n />\n </div>\n )}\n </div>\n );\n}\n\nInsightWidgetSkeleton.displayName = \"InsightWidgetSkeleton\";\n","import React from \"react\";\nimport type { InsightDefinition, DataRow, ScorecardConfig } from \"../types\";\nimport { useInsightsData } from \"../engine/useInsightsData\";\nimport { InsightsScorecardView } from \"./InsightsScorecardView\";\nimport { InsightWidgetSkeleton } from \"./InsightWidgetSkeleton\";\n\n/**\n * Compute a deterministic fixed height for a standard scorecard based\n * on which optional elements the config declares. This eliminates\n * layout shift between skeleton and loaded states.\n *\n * Breakdown (non-compact, non-small):\n * py-4 padding: 16 + 16 = 32\n * title row: 16.5 (text-xs leading-snug)\n * mb-2 margin: 8\n * value: 30 (text-2xl leading-tight)\n * ---\n * base: 86.5\n * + dateRange: +16 (14px text + 2px mt-0.5)\n * + comparison: +20 (16px text + 4px mt-1)\n */\nfunction computeFixedHeight(config: ScorecardConfig): number {\n let h = 86.5; // base: padding + title + mb-2 + value\n if (config.dateRange) h += 16;\n if (config.comparison) h += 20;\n return Math.ceil(h);\n}\n\n/**\n * Single insight widget orchestrator.\n *\n * Wraps skeleton and loaded states in a fixed-height container\n * (computed from the scorecard config) to prevent layout shift.\n *\n * All theme-awareness is handled via Tailwind `dark:` classes.\n */\nexport function InsightWidget({\n definition,\n collectionSlug,\n path,\n parentCollectionSlugs, parentEntityIds,\n compact = false,\n embedded = false\n}: {\n definition: InsightDefinition;\n collectionSlug?: string;\n path?: string;\n parentCollectionSlugs?: string[], parentEntityIds?: string[];\n compact?: boolean;\n /** When true, inner views skip their own borders since the parent card provides them. */\n embedded?: boolean;\n}) {\n const { data, loading, error } = useInsightsData(definition, { path,\ncollectionSlug,\nparentCollectionSlugs });\n\n // For non-compact, non-embedded standard scorecards, use a fixed height\n // derived from the config to prevent layout shift between skeleton → loaded.\n const fixedHeight = (!compact && !embedded) ? computeFixedHeight(definition.scorecard) : undefined;\n\n if (loading) {\n return <InsightWidgetSkeleton config={definition.scorecard} compact={compact} embedded={embedded} fixedHeight={fixedHeight} />;\n }\n\n if (error) {\n return (\n <div\n className={`text-red-500/70 dark:text-red-400/70 text-[0.8125rem] ${embedded ? \"px-5 py-4 h-full\" : `rounded-lg bg-red-500/5 dark:bg-red-400/5 border border-red-500/10 dark:border-red-400/10 ${compact ? \"px-3.5 py-3\" : \"px-5 py-4\"}`}`}\n style={fixedHeight ? { height: fixedHeight } : undefined}\n >\n <div className=\"font-semibold mb-1\">{definition.title}</div>\n <div>{error.message}</div>\n </div>\n );\n }\n\n if (!data || data.rows.length === 0) {\n return (\n <div\n className={`text-surface-400 dark:text-surface-500 text-[0.8125rem] ${embedded ? \"px-5 py-4 h-full\" : `rounded-lg bg-surface-100 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 ${compact ? \"px-3.5 py-3\" : \"px-5 py-4\"}`}`}\n style={fixedHeight ? { height: fixedHeight } : undefined}\n >\n {definition.title} — No data\n </div>\n );\n }\n\n return (\n <InsightsScorecardView\n config={definition.scorecard}\n data={data.rows[0] as DataRow}\n title={definition.title}\n compact={compact}\n embedded={embedded}\n fixedHeight={fixedHeight}\n />\n );\n}\n\nInsightWidget.displayName = \"InsightWidget\";\n","import React from \"react\";\nimport type { InsightDefinition } from \"../types\";\nimport { InsightWidget } from \"./InsightWidget\";\n\n/**\n * Renders compact insight widgets inline within a home page collection card.\n * Injected via the `home.card.insight` slot.\n *\n * Uses a horizontal flex layout so multiple cards sit side by side.\n */\nexport function HomeCardInsightSlot({\n slug,\n insights\n}: {\n slug: string;\n collection: unknown;\n context: unknown;\n insights: InsightDefinition[];\n}) {\n if (!insights || insights.length === 0) return null;\n\n // Each compact card row is ~42px; estimate 2 cards per row for wrapping\n const estimatedRows = Math.ceil(insights.length / 2);\n const minHeight = estimatedRows * 42 + (estimatedRows - 1) * 6; // 6px = gap-1.5\n\n return (\n <div className=\"flex flex-wrap items-center gap-1.5 mt-2\" style={{ minHeight }}>\n {insights.map((def) => (\n <InsightWidget\n key={def.id}\n definition={def}\n collectionSlug={slug}\n compact={true}\n />\n ))}\n </div>\n );\n}\n\nHomeCardInsightSlot.displayName = \"HomeCardInsightSlot\";\n","import React from \"react\";\nimport type { InsightDefinition } from \"../types\";\nimport { InsightWidget } from \"./InsightWidget\";\n\n/**\n * Scorecard insights panel rendered at the top of the home page.\n * Injected via the `home.children.start` slot.\n *\n * Renders scorecards in a responsive grid (up to 4 columns).\n */\nexport function HomeInsightsSlot({\n insights\n}: {\n insights: InsightDefinition[];\n}) {\n if (!insights || insights.length === 0) return null;\n\n return (\n <div\n className=\"grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3 pb-6\"\n style={{ minHeight: 92 }}\n >\n {insights.map((def) => (\n <InsightWidget key={def.id} definition={def} />\n ))}\n </div>\n );\n}\n\nHomeInsightsSlot.displayName = \"HomeInsightsSlot\";\n","import React from \"react\";\nimport type { InsightDefinition } from \"../types\";\nimport { InsightWidget } from \"./InsightWidget\";\n\n/**\n * Renders scorecard insight widgets inline within a collection's list view,\n * positioned below the title and above the main data list.\n *\n * Injected via the `collection.insights` slot.\n */\nexport function CollectionInsightsInline({\n insights,\n path,\n parentCollectionSlugs,\n parentEntityIds\n}: {\n path: string;\n collection: unknown;\n parentCollectionSlugs: string[], parentEntityIds: string[];\n insights: InsightDefinition[];\n}) {\n if (!insights || insights.length === 0) return null;\n\n return (\n <div className=\"grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3 pb-4\">\n {insights.map((def) => (\n <InsightWidget\n key={def.id}\n definition={def}\n path={path}\n parentCollectionSlugs={parentCollectionSlugs} parentEntityIds={parentEntityIds}\n />\n ))}\n </div>\n );\n}\n\nCollectionInsightsInline.displayName = \"CollectionInsightsInline\";\n","import React from \"react\";\nimport type { RebasePlugin, SlotContribution } from \"@rebasepro/types\";\nimport type { InsightsPluginConfig } from \"./types\";\nimport { InsightsProvider } from \"./engine/InsightsProvider\";\nimport { HomeCardInsightSlot } from \"./components/HomeCardInsightSlot\";\nimport { HomeInsightsSlot } from \"./components/HomeInsightsSlot\";\nimport { CollectionInsightsInline } from \"./components/CollectionInsightsInline\";\n\n/**\n * Creates the Insights plugin for Rebase.\n *\n * This plugin injects scorecard widgets into key UI locations:\n * - **Home page header**: KPI overview via `home.children.start` slot\n * - **Collection list view**: Scorecards inline (below title, above list) via `collection.insights` slot\n * - **Home page cards**: Compact scorecard metrics auto-extracted from collection insights via `home.card.insight` slot\n *\n * Collection-level insights (`collections.<slug>`) are the single source of truth:\n * scorecards render in the collection list view and are automatically extracted\n * to show as compact widgets on the corresponding home page card.\n *\n * Each insight owns its own `data()` callback — use the Rebase client SDK,\n * call a custom function, or hit any external API. Full flexibility, zero new endpoints.\n *\n * @example\n * ```typescript\n * import { useInsightsPlugin } from \"@rebasepro/plugin-insights\";\n *\n * const insightsPlugin = useInsightsPlugin({\n * cacheTTL: 120_000,\n * insights: {\n * home: [\n * { id: \"revenue\", title: \"Revenue\", data: async () => ..., scorecard: { ... } },\n * ],\n * collections: {\n * orders: [\n * { id: \"total\", title: \"Total Orders\", data: async () => ..., scorecard: { ... } },\n * ],\n * },\n * },\n * });\n * ```\n */\nexport function useInsightsPlugin(config: InsightsPluginConfig): RebasePlugin {\n const { insights, cacheTTL } = config;\n\n return React.useMemo(() => {\n const slots: SlotContribution[] = [];\n\n // ── Home page insights ────────────────────────────────────────────\n if (insights.home && insights.home.length > 0) {\n const homeInsights = insights.home;\n slots.push({\n slot: \"home.children.start\" as const,\n Component: (props: Record<string, unknown>) => (\n <HomeInsightsSlot\n {...props}\n insights={homeInsights}\n />\n ),\n order: 10\n });\n }\n\n // ── Per-collection insights ───────────────────────────────────────\n // A single `collections.<slug>` definition serves two slots:\n // 1. collection.insights → inline scorecards in the list view\n // 2. home.card.insight → compact scorecards on the home card\n if (insights.collections) {\n for (const [slug, defs] of Object.entries(insights.collections)) {\n if (defs.length === 0) continue;\n const collectionInsights = defs;\n\n // 1. Inline in collection list view\n slots.push({\n slot: \"collection.insights\" as const,\n Component: (props: Record<string, unknown>) => {\n const path = props.path as string;\n const collectionSlug = path?.split(\"/\").filter(Boolean).pop() ?? \"\";\n if (collectionSlug !== slug) return null;\n return (\n <CollectionInsightsInline\n {...props as { path: string; collection: unknown; parentCollectionSlugs: string[], parentEntityIds: string[] }}\n insights={collectionInsights}\n />\n );\n },\n order: 10\n });\n\n // 2. Auto-extract scorecards for home page card\n slots.push({\n slot: \"home.card.insight\" as const,\n Component: (props: Record<string, unknown>) => {\n const cardSlug = props.slug as string;\n if (cardSlug !== slug) return null;\n return (\n <HomeCardInsightSlot\n {...props as { slug: string; collection: unknown; context: unknown }}\n insights={collectionInsights}\n />\n );\n },\n order: 10\n });\n }\n }\n\n return {\n key: \"plugin-insights\",\n slots,\n providers: [\n {\n scope: \"root\" as const,\n Component: InsightsProvider as React.ComponentType<React.PropsWithChildren<Record<string, unknown>>>,\n props: { cacheTTL }\n }\n ]\n };\n }, [insights, cacheTTL]);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.es.js","names":[],"sources":["../src/engine/InsightsCache.ts","../src/engine/InsightsProvider.tsx","../src/engine/useInsightsData.ts","../src/components/InsightsScorecardView.tsx","../src/components/InsightWidgetSkeleton.tsx","../src/components/InsightWidget.tsx","../src/components/HomeCardInsightSlot.tsx","../src/components/HomeInsightsSlot.tsx","../src/components/CollectionInsightsInline.tsx","../src/useInsightsPlugin.tsx"],"sourcesContent":["import type { InsightDataResult } from \"../types\";\n\ninterface CacheEntry {\n data: InsightDataResult;\n timestamp: number;\n}\n\n/**\n * In-memory cache for insight query results.\n * Supports TTL-based expiry and inflight request deduplication\n * to prevent redundant network requests when multiple widgets\n * share the same query.\n */\nexport class InsightsCache {\n private cache = new Map<string, CacheEntry>();\n private inflight = new Map<string, Promise<InsightDataResult>>();\n\n constructor(private ttl = 60_000) {}\n\n get(key: string): InsightDataResult | null {\n const entry = this.cache.get(key);\n if (!entry) return null;\n if (Date.now() - entry.timestamp > this.ttl) {\n this.cache.delete(key);\n return null;\n }\n return entry.data;\n }\n\n set(key: string, data: InsightDataResult): void {\n this.cache.set(key, { data,\ntimestamp: Date.now() });\n this.inflight.delete(key);\n }\n\n getInflight(key: string): Promise<InsightDataResult> | null {\n return this.inflight.get(key) ?? null;\n }\n\n setInflight(key: string, promise: Promise<InsightDataResult>): void {\n this.inflight.set(key, promise);\n }\n\n invalidate(key?: string): void {\n if (key) {\n this.cache.delete(key);\n this.inflight.delete(key);\n } else {\n this.cache.clear();\n this.inflight.clear();\n }\n }\n}\n","import React, { createContext, useContext, useMemo, type PropsWithChildren } from \"react\";\nimport { InsightsCache } from \"./InsightsCache\";\n\ninterface InsightsContextValue {\n cache: InsightsCache;\n}\n\nconst InsightsContext = createContext<InsightsContextValue | null>(null);\n\n/**\n * Root-level provider for the insights data engine.\n * Injected automatically by the plugin via `providers: [{ scope: \"root\" }]`.\n *\n * Manages a single `InsightsCache` instance shared by all insight widgets\n * for TTL-based caching and inflight request deduplication.\n */\nexport function InsightsProvider({\n cacheTTL,\n children\n}: PropsWithChildren<{ cacheTTL?: number }>) {\n const cache = useMemo(() => new InsightsCache(cacheTTL), [cacheTTL]);\n const value = useMemo(() => ({ cache }), [cache]);\n\n return (\n <InsightsContext.Provider value={value}>\n {children}\n </InsightsContext.Provider>\n );\n}\n\n/**\n * Access the insights cache (for advanced usage).\n * Returns null when called outside of an `InsightsProvider`\n * (e.g. during auth-loading phase before plugin providers mount).\n */\nexport function useInsightsEngine(): InsightsContextValue | null {\n return useContext(InsightsContext);\n}\n","import { useEffect, useState } from \"react\";\nimport type { InsightDefinition, InsightDataResult, InsightContext } from \"../types\";\nimport { useInsightsEngine } from \"./InsightsProvider\";\nimport { useAuthController } from \"@rebasepro/app\";\n\n/**\n * Hook that fetches and caches data for a single insight definition.\n *\n * Calls the definition's own `data()` callback and manages:\n * - TTL-based caching via InsightsCache\n * - Inflight request deduplication (multiple mounts of the same widget)\n * - Loading and error state management\n *\n * @param definition - The insight to fetch data for\n * @param collectionSlug - Optional collection context for cache key scoping\n */\nexport function useInsightsData(\n definition: InsightDefinition,\n context: InsightContext\n): {\n data: InsightDataResult | null;\n loading: boolean;\n error: Error | null;\n} {\n const engine = useInsightsEngine();\n const cache = engine?.cache ?? null;\n const { initialLoading, authLoading, user, loginSkipped } = useAuthController();\n const authReady = !initialLoading && !authLoading && (Boolean(user) || loginSkipped);\n const [data, setData] = useState<InsightDataResult | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const cacheKey = `${definition.id}:${context.path ?? context.collectionSlug ?? \"global\"}`;\n\n useEffect(() => {\n // Keep showing skeleton until both auth and engine are ready\n if (!authReady || !cache) {\n return;\n }\n\n let cancelled = false;\n\n // 1. Check cache\n const cached = cache.get(cacheKey);\n if (cached) {\n setData(cached);\n setLoading(false);\n return;\n }\n\n // 2. Check inflight — deduplicate concurrent requests for the same widget\n const inflight = cache.getInflight(cacheKey);\n if (inflight) {\n setLoading(true);\n inflight\n .then((result) => {\n if (!cancelled) {\n setData(result);\n }\n })\n .catch((err) => {\n if (!cancelled) setError(err instanceof Error ? err : new Error(String(err)));\n })\n .finally(() => {\n if (!cancelled) setLoading(false);\n });\n return;\n }\n\n // 3. Fresh fetch — invoke the definition's own data callback\n setLoading(true);\n setError(null);\n\n const promise = definition.data(context);\n\n cache.setInflight(cacheKey, promise);\n\n promise\n .then((result) => {\n cache.set(cacheKey, result);\n if (!cancelled) {\n setData(result);\n }\n })\n .catch((err) => {\n cache.invalidate(cacheKey);\n if (!cancelled) {\n setError(err instanceof Error ? err : new Error(String(err)));\n }\n })\n .finally(() => {\n if (!cancelled) setLoading(false);\n });\n\n return () => {\n cancelled = true;\n };\n }, [definition.id, definition.data, context.path, context.collectionSlug, cacheKey, cache, authReady]);\n\n return { data,\nloading,\nerror };\n}\n","import React, { useRef, useState } from \"react\";\nimport { getIcon } from \"@rebasepro/app\";\nimport { cls, defaultBorderMixin } from \"@rebasepro/ui\";\nimport type { DataRow, ScorecardConfig, ScorecardFormat } from \"../types\";\n\nfunction formatNumber(value: number, format?: ScorecardFormat): string {\n if (value === null || value === undefined) return \"N/A\";\n\n const options: Intl.NumberFormatOptions = {\n style: format?.style ?? \"decimal\",\n notation: format?.notation ?? \"standard\"\n };\n\n // Only pin the fraction digits when the config asks for a specific count.\n // Without this, Intl's per-style defaults apply: integers stay integers\n // (\"80\", not \"80.0\") while currency keeps its two decimals (\"$452.95\").\n if (format?.decimals !== undefined) {\n options.maximumFractionDigits = format.decimals;\n options.minimumFractionDigits = format.decimals;\n }\n\n if (format?.style === \"currency\") {\n options.currency = format.currency ?? \"USD\";\n }\n\n let formatted = new Intl.NumberFormat(\"en-US\", options).format(value);\n\n if (format?.showSign && value > 0) {\n formatted = \"+\" + formatted;\n }\n\n return formatted;\n}\n\n/**\n * Scorecard widget for the Rebase design system.\n *\n * Renders a single KPI metric with optional comparison value and icon.\n * Uses Tailwind `dark:` classes — no JS dark mode detection.\n * Icons are resolved via `getIcon` from `@rebasepro/app`.\n */\nexport function InsightsScorecardView({\n config,\n data,\n title,\n compact = false,\n embedded = false,\n fixedHeight\n}: {\n config: ScorecardConfig;\n data: DataRow;\n title: string;\n compact?: boolean;\n /** When true, skip own border/bg since the parent card provides them. */\n embedded?: boolean;\n /** Explicit height to prevent layout shift between skeleton → loaded. */\n fixedHeight?: number;\n}) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [containerWidth, setContainerWidth] = useState<number | null>(null);\n\n React.useLayoutEffect(() => {\n if (!containerRef.current) return;\n // Read initial width synchronously before paint\n setContainerWidth(containerRef.current.offsetWidth);\n const observer = new ResizeObserver((entries) => {\n for (const entry of entries) {\n setContainerWidth(entry.contentRect.width);\n }\n });\n observer.observe(containerRef.current);\n return () => observer.disconnect();\n }, []);\n\n const mainValue = data[config.value.field];\n const formattedValue = typeof mainValue === \"number\"\n ? formatNumber(mainValue, config.value.format)\n : String(mainValue ?? \"N/A\");\n\n // Comparison rendering\n let comparisonElement: React.ReactNode = null;\n if (config.comparison) {\n const comparisonValue = data[config.comparison.field];\n if (typeof comparisonValue === \"number\") {\n const formattedComparison = formatNumber(comparisonValue, config.comparison.format);\n const isPositive = comparisonValue > 0;\n const isNegative = comparisonValue < 0;\n\n let colorClass = \"text-surface-500 dark:text-surface-400\";\n if (config.comparison.intent === \"increase_is_good\") {\n if (isPositive) colorClass = \"text-emerald-500\";\n if (isNegative) colorClass = \"text-red-500\";\n } else if (config.comparison.intent === \"decrease_is_good\") {\n if (isPositive) colorClass = \"text-red-500\";\n if (isNegative) colorClass = \"text-emerald-500\";\n }\n\n comparisonElement = (\n <span className={`font-medium ${compact ? \"text-[10px]\" : \"text-xs\"} ${colorClass}`}>\n {formattedComparison}\n </span>\n );\n }\n }\n\n const isSmall = compact || (containerWidth !== null && containerWidth < 200);\n\n // Resolve icon via getIcon (Lucide-based resolution)\n const iconElement = config.icon\n ? getIcon(config.icon, \"text-surface-400 dark:text-surface-500\", undefined, isSmall ? 14 : 18)\n : null;\n\n // ── Compact card-inline layout ──────────────────────────────────────\n if (compact) {\n return (\n <div className={cls(\"flex flex-col gap-0.5 px-2.5 py-2 rounded-md bg-transparent border min-w-0\", defaultBorderMixin)}>\n <span className=\"text-[10px] uppercase tracking-wider text-surface-400 dark:text-surface-500 truncate\">\n {title}\n </span>\n <div className=\"flex items-baseline gap-1.5\">\n <span className=\"text-sm font-semibold tabular-nums text-surface-800 dark:text-surface-100\">\n {formattedValue}\n </span>\n {comparisonElement}\n </div>\n </div>\n );\n }\n\n // ── Standard scorecard layout ───────────────────────────────────────\n const baseClass = embedded\n ? `flex flex-col min-w-0 h-full ${isSmall ? \"px-3.5 py-3\" : \"px-5 py-4\"}`\n : cls(\"rounded-lg flex flex-col min-w-0 bg-transparent border\", defaultBorderMixin, isSmall ? \"px-3.5 py-3\" : \"px-5 py-4\");\n\n return (\n <div ref={containerRef} className={baseClass} style={embedded ? undefined : fixedHeight ? { height: fixedHeight } : { minHeight: isSmall ? 68 : 92 }}>\n {/* Title row */}\n <div className={`flex items-center justify-between ${isSmall ? \"mb-1\" : \"mb-2\"}`}>\n <div className=\"flex flex-col min-w-0\">\n <span className={`font-medium leading-snug truncate text-surface-500 dark:text-surface-400 ${isSmall ? \"text-[11px]\" : \"text-xs\"}`}>\n {title}\n </span>\n {config.dateRange && !isSmall && (\n <span className=\"text-[10px] text-surface-400 dark:text-surface-500 truncate mt-0.5\">\n {config.dateRange}\n </span>\n )}\n </div>\n {iconElement && (\n <span className=\"ml-2 shrink-0\">{iconElement}</span>\n )}\n </div>\n\n {/* Main value */}\n <div className={`font-semibold leading-tight tracking-tight break-all text-surface-800 dark:text-surface-100 ${isSmall ? \"text-lg\" : (containerWidth !== null && containerWidth < 300) ? \"text-xl\" : \"text-2xl\"}`}>\n {formattedValue}\n </div>\n\n {/* Comparison */}\n {comparisonElement && (\n <div className={isSmall ? \"mt-0.5\" : \"mt-1\"}>\n {comparisonElement}\n </div>\n )}\n </div>\n );\n}\n\nInsightsScorecardView.displayName = \"InsightsScorecardView\";\n","import React, { useRef, useState } from \"react\";\nimport { cls, defaultBorderMixin } from \"@rebasepro/ui\";\nimport type { ScorecardConfig } from \"../types\";\n\n/**\n * Skeleton loader for scorecard insight widgets — displays animated\n * shimmer placeholders that exactly match the final rendered layout\n * of InsightsScorecardView for a given config, preventing layout shift.\n *\n * The skeleton receives the scorecard config so it can conditionally\n * render placeholder lines for comparison, dateRange, and icon —\n * only when the loaded view will also render them.\n *\n * The standard skeleton mirrors InsightsScorecardView's responsive\n * container-width breakpoints (ResizeObserver → isSmall / isMedium)\n * and uses placeholder heights that exactly match the **computed**\n * Tailwind line-heights (accounting for `leading-*` overrides).\n * This guarantees a pixel-perfect skeleton → loaded transition.\n */\nexport function InsightWidgetSkeleton({\n config,\n compact = false,\n embedded = false,\n fixedHeight\n}: {\n /** Scorecard config — used to match optional elements (comparison, dateRange, icon). */\n config: ScorecardConfig;\n compact?: boolean;\n /** When true, skip own border since the parent card provides it. */\n embedded?: boolean;\n /** Explicit height to prevent layout shift between skeleton → loaded. */\n fixedHeight?: number;\n}) {\n const hasComparison = Boolean(config.comparison);\n const hasIcon = Boolean(config.icon);\n const hasDateRange = Boolean(config.dateRange);\n\n // ── Compact scorecard skeleton ──────────────────────────────────────\n // Matches InsightsScorecardView compact layout:\n // container: flex flex-col gap-0.5 px-2.5 py-2 rounded-md border\n // title: text-[10px] uppercase → line-height ~14px\n // value row: text-sm font-semibold → line-height 20px\n // + optional comparison text-[10px] inside value row\n if (compact) {\n return (\n <div\n className={cls(\n \"animate-pulse\",\n embedded\n ? \"h-full px-2.5 py-2\"\n : \"flex flex-col gap-0.5 rounded-md bg-transparent border min-w-0 px-2.5 py-2\",\n !embedded && defaultBorderMixin\n )}\n >\n {/* Title line */}\n <div className=\"bg-surface-200 dark:bg-surface-700 rounded-sm\"\n style={{ height: 14,\nwidth: 48 }}\n />\n {/* Value + optional comparison row */}\n <div className=\"flex items-baseline gap-1.5\">\n <div className=\"bg-surface-200 dark:bg-surface-700 rounded-sm\"\n style={{ height: 20,\nwidth: 40 }}\n />\n {hasComparison && (\n <div className=\"bg-surface-200/60 dark:bg-surface-700/60 rounded-sm\"\n style={{ height: 14,\nwidth: 28 }}\n />\n )}\n </div>\n </div>\n );\n }\n\n // ── Standard scorecard skeleton ─────────────────────────────────────\n return <StandardSkeleton\n hasComparison={hasComparison}\n hasIcon={hasIcon}\n hasDateRange={hasDateRange}\n embedded={embedded}\n fixedHeight={fixedHeight}\n />;\n}\n\n// ── Tailwind line-height reference ──────────────────────────────────────\n// All heights below are the **computed** CSS line-heights, accounting\n// for `leading-*` overrides that InsightsScorecardView applies.\n//\n// Title:\n// text-xs (12px) + leading-snug (1.375) → 12 × 1.375 = 16.5px\n// text-[11px] + leading-snug (1.375) → 11 × 1.375 = 15.125px\n//\n// DateRange:\n// text-[10px] with no explicit LH → normal ≈ 14px (browser)\n//\n// Value:\n// text-2xl (24px) + leading-tight (1.25) → 24 × 1.25 = 30px\n// text-xl (20px) + leading-tight (1.25) → 20 × 1.25 = 25px\n// text-lg (18px) + leading-tight (1.25) → 18 × 1.25 = 22.5px\n//\n// Comparison:\n// text-xs (12px) → built-in LH 1rem = 16px\n\n/**\n * Inner component for the standard scorecard skeleton.\n *\n * Mirrors InsightsScorecardView's layout by:\n * 1. Using the same ResizeObserver + containerWidth pattern for\n * responsive breakpoints (isSmall < 200px, isMedium < 300px).\n * 2. Using placeholder heights derived from the exact computed\n * Tailwind line-heights that InsightsScorecardView renders.\n * 3. Matching all container classes, margins, paddings, and flex\n * layout properties identically.\n */\nfunction StandardSkeleton({\n hasComparison,\n hasIcon,\n hasDateRange,\n embedded,\n fixedHeight\n}: {\n hasComparison: boolean;\n hasIcon: boolean;\n hasDateRange: boolean;\n embedded: boolean;\n fixedHeight?: number;\n}) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [containerWidth, setContainerWidth] = useState<number | null>(null);\n\n React.useLayoutEffect(() => {\n if (!containerRef.current) return;\n setContainerWidth(containerRef.current.offsetWidth);\n const observer = new ResizeObserver((entries) => {\n for (const entry of entries) {\n setContainerWidth(entry.contentRect.width);\n }\n });\n observer.observe(containerRef.current);\n return () => observer.disconnect();\n }, []);\n\n // Mirror InsightsScorecardView's responsive breakpoints exactly\n const isSmall = containerWidth !== null && containerWidth < 200;\n\n // Computed line-heights for each breakpoint\n // Title: text-xs + leading-snug = 16.5px, text-[11px] + leading-snug = 15.125px\n const titleHeight = isSmall ? 15 : 16.5;\n // Value: leading-tight (×1.25) applied on top of font-size\n const valueHeight = isSmall\n ? 22.5 // text-lg: 18 × 1.25\n : (containerWidth !== null && containerWidth < 300)\n ? 25 // text-xl: 20 × 1.25\n : 30; // text-2xl: 24 × 1.25\n // Comparison: text-xs = 12px / 16px line-height (no leading override)\n const comparisonHeight = 16;\n // Icon: 14px when small, 18px otherwise\n const iconSize = isSmall ? 14 : 18;\n\n const baseClass = embedded\n ? `flex flex-col min-w-0 h-full ${isSmall ? \"px-3.5 py-3\" : \"px-5 py-4\"}`\n : cls(\"rounded-lg flex flex-col min-w-0 bg-transparent border\", defaultBorderMixin, isSmall ? \"px-3.5 py-3\" : \"px-5 py-4\");\n\n return (\n <div\n ref={containerRef}\n className={cls(\"animate-pulse\", baseClass)}\n style={embedded ? undefined : fixedHeight ? { height: fixedHeight } : { minHeight: isSmall ? 68 : 92 }}\n >\n {/* Title row — identical flex structure to InsightsScorecardView */}\n <div className={`flex items-center justify-between ${isSmall ? \"mb-1\" : \"mb-2\"}`}>\n <div className=\"flex flex-col min-w-0\">\n {/* Title placeholder */}\n <div className=\"bg-surface-200 dark:bg-surface-700 rounded\"\n style={{ height: titleHeight,\nwidth: \"60%\" }}\n />\n {/* DateRange — hidden when isSmall, same as real view (line 134) */}\n {hasDateRange && !isSmall && (\n <div className=\"bg-surface-200/60 dark:bg-surface-700/60 rounded mt-0.5\"\n style={{ height: 14,\nwidth: \"40%\" }}\n />\n )}\n </div>\n {/* Icon placeholder — same wrapper as real view */}\n {hasIcon && (\n <span className=\"ml-2 shrink-0\">\n <div className=\"bg-surface-200 dark:bg-surface-700 rounded\"\n style={{ height: iconSize,\nwidth: iconSize }}\n />\n </span>\n )}\n </div>\n\n {/* Main value placeholder */}\n <div className=\"bg-surface-200 dark:bg-surface-700 rounded\"\n style={{ height: valueHeight,\nwidth: \"40%\" }}\n />\n\n {/* Comparison placeholder */}\n {hasComparison && (\n <div className={isSmall ? \"mt-0.5\" : \"mt-1\"}>\n <div className=\"bg-surface-200/60 dark:bg-surface-700/60 rounded\"\n style={{ height: comparisonHeight,\nwidth: \"25%\" }}\n />\n </div>\n )}\n </div>\n );\n}\n\nInsightWidgetSkeleton.displayName = \"InsightWidgetSkeleton\";\n","import React from \"react\";\nimport type { InsightDefinition, DataRow, ScorecardConfig } from \"../types\";\nimport { useInsightsData } from \"../engine/useInsightsData\";\nimport { InsightsScorecardView } from \"./InsightsScorecardView\";\nimport { InsightWidgetSkeleton } from \"./InsightWidgetSkeleton\";\n\n/**\n * Compute a deterministic fixed height for a standard scorecard based\n * on which optional elements the config declares. This eliminates\n * layout shift between skeleton and loaded states.\n *\n * Breakdown (non-compact, non-small):\n * py-4 padding: 16 + 16 = 32\n * title row: 16.5 (text-xs leading-snug)\n * mb-2 margin: 8\n * value: 30 (text-2xl leading-tight)\n * ---\n * base: 86.5\n * + dateRange: +16 (14px text + 2px mt-0.5)\n * + comparison: +20 (16px text + 4px mt-1)\n */\nfunction computeFixedHeight(config: ScorecardConfig): number {\n let h = 86.5; // base: padding + title + mb-2 + value\n if (config.dateRange) h += 16;\n if (config.comparison) h += 20;\n return Math.ceil(h);\n}\n\n/**\n * Single insight widget orchestrator.\n *\n * Wraps skeleton and loaded states in a fixed-height container\n * (computed from the scorecard config) to prevent layout shift.\n *\n * All theme-awareness is handled via Tailwind `dark:` classes.\n */\nexport function InsightWidget({\n definition,\n collectionSlug,\n path,\n parentCollectionSlugs, parentEntityIds,\n compact = false,\n embedded = false\n}: {\n definition: InsightDefinition;\n collectionSlug?: string;\n path?: string;\n parentCollectionSlugs?: string[], parentEntityIds?: string[];\n compact?: boolean;\n /** When true, inner views skip their own borders since the parent card provides them. */\n embedded?: boolean;\n}) {\n const { data, loading, error } = useInsightsData(definition, { path,\ncollectionSlug,\nparentCollectionSlugs });\n\n // For non-compact, non-embedded standard scorecards, use a fixed height\n // derived from the config to prevent layout shift between skeleton → loaded.\n const fixedHeight = (!compact && !embedded) ? computeFixedHeight(definition.scorecard) : undefined;\n\n if (loading) {\n return <InsightWidgetSkeleton config={definition.scorecard} compact={compact} embedded={embedded} fixedHeight={fixedHeight} />;\n }\n\n if (error) {\n return (\n <div\n className={`text-red-500/70 dark:text-red-400/70 text-[0.8125rem] ${embedded ? \"px-5 py-4 h-full\" : `rounded-lg bg-red-500/5 dark:bg-red-400/5 border border-red-500/10 dark:border-red-400/10 ${compact ? \"px-3.5 py-3\" : \"px-5 py-4\"}`}`}\n style={fixedHeight ? { height: fixedHeight } : undefined}\n >\n <div className=\"font-semibold mb-1\">{definition.title}</div>\n <div>{error.message}</div>\n </div>\n );\n }\n\n if (!data || data.rows.length === 0) {\n return (\n <div\n className={`text-surface-400 dark:text-surface-500 text-[0.8125rem] ${embedded ? \"px-5 py-4 h-full\" : `rounded-lg bg-surface-100 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 ${compact ? \"px-3.5 py-3\" : \"px-5 py-4\"}`}`}\n style={fixedHeight ? { height: fixedHeight } : undefined}\n >\n {definition.title} — No data\n </div>\n );\n }\n\n return (\n <InsightsScorecardView\n config={definition.scorecard}\n data={data.rows[0] as DataRow}\n title={definition.title}\n compact={compact}\n embedded={embedded}\n fixedHeight={fixedHeight}\n />\n );\n}\n\nInsightWidget.displayName = \"InsightWidget\";\n","import React from \"react\";\nimport type { InsightDefinition } from \"../types\";\nimport { InsightWidget } from \"./InsightWidget\";\n\n/**\n * Renders compact insight widgets inline within a home page collection card.\n * Injected via the `home.card.insight` slot.\n *\n * Uses a horizontal flex layout so multiple cards sit side by side.\n */\nexport function HomeCardInsightSlot({\n slug,\n insights\n}: {\n slug: string;\n collection: unknown;\n context: unknown;\n insights: InsightDefinition[];\n}) {\n if (!insights || insights.length === 0) return null;\n\n // Each compact card row is ~42px; estimate 2 cards per row for wrapping\n const estimatedRows = Math.ceil(insights.length / 2);\n const minHeight = estimatedRows * 42 + (estimatedRows - 1) * 6; // 6px = gap-1.5\n\n return (\n <div className=\"flex flex-wrap items-center gap-1.5 mt-2\" style={{ minHeight }}>\n {insights.map((def) => (\n <InsightWidget\n key={def.id}\n definition={def}\n collectionSlug={slug}\n compact={true}\n />\n ))}\n </div>\n );\n}\n\nHomeCardInsightSlot.displayName = \"HomeCardInsightSlot\";\n","import React from \"react\";\nimport type { InsightDefinition } from \"../types\";\nimport { InsightWidget } from \"./InsightWidget\";\n\n/**\n * Scorecard insights panel rendered at the top of the home page.\n * Injected via the `home.children.start` slot.\n *\n * Renders scorecards in a responsive grid (up to 4 columns).\n */\nexport function HomeInsightsSlot({\n insights\n}: {\n insights: InsightDefinition[];\n}) {\n if (!insights || insights.length === 0) return null;\n\n return (\n <div\n className=\"grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3 pb-6\"\n style={{ minHeight: 92 }}\n >\n {insights.map((def) => (\n <InsightWidget key={def.id} definition={def} />\n ))}\n </div>\n );\n}\n\nHomeInsightsSlot.displayName = \"HomeInsightsSlot\";\n","import React from \"react\";\nimport type { InsightDefinition } from \"../types\";\nimport { InsightWidget } from \"./InsightWidget\";\n\n/**\n * Renders scorecard insight widgets inline within a collection's list view,\n * positioned below the title and above the main data list.\n *\n * Injected via the `collection.insights` slot.\n */\nexport function CollectionInsightsInline({\n insights,\n path,\n parentCollectionSlugs,\n parentEntityIds\n}: {\n path: string;\n collection: unknown;\n parentCollectionSlugs: string[], parentEntityIds: string[];\n insights: InsightDefinition[];\n}) {\n if (!insights || insights.length === 0) return null;\n\n return (\n <div className=\"grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3 pb-4\">\n {insights.map((def) => (\n <InsightWidget\n key={def.id}\n definition={def}\n path={path}\n parentCollectionSlugs={parentCollectionSlugs} parentEntityIds={parentEntityIds}\n />\n ))}\n </div>\n );\n}\n\nCollectionInsightsInline.displayName = \"CollectionInsightsInline\";\n","import React from \"react\";\nimport type { RebasePlugin, SlotContribution } from \"@rebasepro/types\";\nimport type { InsightsPluginConfig } from \"./types\";\nimport { InsightsProvider } from \"./engine/InsightsProvider\";\nimport { HomeCardInsightSlot } from \"./components/HomeCardInsightSlot\";\nimport { HomeInsightsSlot } from \"./components/HomeInsightsSlot\";\nimport { CollectionInsightsInline } from \"./components/CollectionInsightsInline\";\n\n/**\n * Creates the Insights plugin for Rebase.\n *\n * This plugin injects scorecard widgets into key UI locations:\n * - **Home page header**: KPI overview via `home.children.start` slot\n * - **Collection list view**: Scorecards inline (below title, above list) via `collection.insights` slot\n * - **Home page cards**: Compact scorecard metrics auto-extracted from collection insights via `home.card.insight` slot\n *\n * Collection-level insights (`collections.<slug>`) are the single source of truth:\n * scorecards render in the collection list view and are automatically extracted\n * to show as compact widgets on the corresponding home page card.\n *\n * Each insight owns its own `data()` callback — use the Rebase client SDK,\n * call a custom function, or hit any external API. Full flexibility, zero new endpoints.\n *\n * @example\n * ```typescript\n * import { useInsightsPlugin } from \"@rebasepro/plugin-insights\";\n *\n * const insightsPlugin = useInsightsPlugin({\n * cacheTTL: 120_000,\n * insights: {\n * home: [\n * { id: \"revenue\", title: \"Revenue\", data: async () => ..., scorecard: { ... } },\n * ],\n * collections: {\n * orders: [\n * { id: \"total\", title: \"Total Orders\", data: async () => ..., scorecard: { ... } },\n * ],\n * },\n * },\n * });\n * ```\n */\nexport function useInsightsPlugin(config: InsightsPluginConfig): RebasePlugin {\n const { insights, cacheTTL } = config;\n\n return React.useMemo(() => {\n const slots: SlotContribution[] = [];\n\n // ── Home page insights ────────────────────────────────────────────\n if (insights.home && insights.home.length > 0) {\n const homeInsights = insights.home;\n slots.push({\n slot: \"home.children.start\" as const,\n Component: (props: Record<string, unknown>) => (\n <HomeInsightsSlot\n {...props}\n insights={homeInsights}\n />\n ),\n order: 10\n });\n }\n\n // ── Per-collection insights ───────────────────────────────────────\n // A single `collections.<slug>` definition serves two slots:\n // 1. collection.insights → inline scorecards in the list view\n // 2. home.card.insight → compact scorecards on the home card\n if (insights.collections) {\n for (const [slug, defs] of Object.entries(insights.collections)) {\n if (defs.length === 0) continue;\n const collectionInsights = defs;\n\n // 1. Inline in collection list view\n slots.push({\n slot: \"collection.insights\" as const,\n Component: (props: Record<string, unknown>) => {\n const path = props.path as string;\n const collectionSlug = path?.split(\"/\").filter(Boolean).pop() ?? \"\";\n if (collectionSlug !== slug) return null;\n\n // Skip relation-scoped views (e.g. a single product's Orders\n // tab). These aggregations are collection-wide — `InsightContext`\n // carries no parent entity id, so a definition cannot narrow to\n // the parent — and rendering \"Revenue $36.2K\" above one product's\n // two orders reads as a figure for those orders.\n const parentEntityIds = props.parentEntityIds as string[] | undefined;\n if (parentEntityIds && parentEntityIds.length > 0) return null;\n\n return (\n <CollectionInsightsInline\n {...props as { path: string; collection: unknown; parentCollectionSlugs: string[], parentEntityIds: string[] }}\n insights={collectionInsights}\n />\n );\n },\n order: 10\n });\n\n // 2. Auto-extract scorecards for home page card\n slots.push({\n slot: \"home.card.insight\" as const,\n Component: (props: Record<string, unknown>) => {\n const cardSlug = props.slug as string;\n if (cardSlug !== slug) return null;\n return (\n <HomeCardInsightSlot\n {...props as { slug: string; collection: unknown; context: unknown }}\n insights={collectionInsights}\n />\n );\n },\n order: 10\n });\n }\n }\n\n return {\n key: \"plugin-insights\",\n slots,\n providers: [\n {\n scope: \"root\" as const,\n Component: InsightsProvider as React.ComponentType<React.PropsWithChildren<Record<string, unknown>>>,\n props: { cacheTTL }\n }\n ]\n };\n }, [insights, cacheTTL]);\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@rebasepro/plugin-insights",
3
3
  "type": "module",
4
- "version": "0.9.1-canary.fd3754b",
5
- "main": "./dist/index.umd.js",
4
+ "version": "0.10.0",
5
+ "main": "./dist/index.es.js",
6
6
  "module": "./dist/index.es.js",
7
7
  "types": "./dist/index.d.ts",
8
8
  "source": "src/index.ts",
@@ -10,15 +10,14 @@
10
10
  ".": {
11
11
  "types": "./dist/index.d.ts",
12
12
  "development": "./dist/index.es.js",
13
- "import": "./dist/index.es.js",
14
- "require": "./dist/index.umd.js"
13
+ "import": "./dist/index.es.js"
15
14
  },
16
15
  "./package.json": "./package.json"
17
16
  },
18
17
  "dependencies": {
19
- "@rebasepro/app": "0.9.1-canary.fd3754b",
20
- "@rebasepro/types": "0.9.1-canary.fd3754b",
21
- "@rebasepro/ui": "0.9.1-canary.fd3754b"
18
+ "@rebasepro/app": "0.10.0",
19
+ "@rebasepro/types": "0.10.0",
20
+ "@rebasepro/ui": "0.10.0"
22
21
  },
23
22
  "peerDependencies": {
24
23
  "react": ">=19.0.0",
@@ -83,7 +82,7 @@
83
82
  },
84
83
  "scripts": {
85
84
  "dev": "vite",
86
- "build": "vite build && tsc --emitDeclarationOnly -p tsconfig.prod.json",
85
+ "build": "vite build && tsc --emitDeclarationOnly -p tsconfig.prod.json && node ../../scripts/assert-build-output.mjs",
87
86
  "test": "jest --passWithNoTests"
88
87
  }
89
88
  }
@@ -8,11 +8,17 @@ function formatNumber(value: number, format?: ScorecardFormat): string {
8
8
 
9
9
  const options: Intl.NumberFormatOptions = {
10
10
  style: format?.style ?? "decimal",
11
- notation: format?.notation ?? "standard",
12
- maximumFractionDigits: format?.decimals ?? 1,
13
- minimumFractionDigits: format?.decimals ?? 1
11
+ notation: format?.notation ?? "standard"
14
12
  };
15
13
 
14
+ // Only pin the fraction digits when the config asks for a specific count.
15
+ // Without this, Intl's per-style defaults apply: integers stay integers
16
+ // ("80", not "80.0") while currency keeps its two decimals ("$452.95").
17
+ if (format?.decimals !== undefined) {
18
+ options.maximumFractionDigits = format.decimals;
19
+ options.minimumFractionDigits = format.decimals;
20
+ }
21
+
16
22
  if (format?.style === "currency") {
17
23
  options.currency = format.currency ?? "USD";
18
24
  }
@@ -77,6 +77,15 @@ export function useInsightsPlugin(config: InsightsPluginConfig): RebasePlugin {
77
77
  const path = props.path as string;
78
78
  const collectionSlug = path?.split("/").filter(Boolean).pop() ?? "";
79
79
  if (collectionSlug !== slug) return null;
80
+
81
+ // Skip relation-scoped views (e.g. a single product's Orders
82
+ // tab). These aggregations are collection-wide — `InsightContext`
83
+ // carries no parent entity id, so a definition cannot narrow to
84
+ // the parent — and rendering "Revenue $36.2K" above one product's
85
+ // two orders reads as a figure for those orders.
86
+ const parentEntityIds = props.parentEntityIds as string[] | undefined;
87
+ if (parentEntityIds && parentEntityIds.length > 0) return null;
88
+
80
89
  return (
81
90
  <CollectionInsightsInline
82
91
  {...props as { path: string; collection: unknown; parentCollectionSlugs: string[], parentEntityIds: string[] }}
package/dist/index.umd.js DELETED
@@ -1,646 +0,0 @@
1
- (function(global, factory) {
2
- typeof exports === "object" && typeof module !== "undefined" ? factory(exports, require("react"), require("react/jsx-runtime"), require("@rebasepro/app"), require("@rebasepro/ui")) : typeof define === "function" && define.amd ? define([
3
- "exports",
4
- "react",
5
- "react/jsx-runtime",
6
- "@rebasepro/app",
7
- "@rebasepro/ui"
8
- ], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global.Rebase = {}, global.react, global.react_jsx_runtime, global._rebasepro_app, global._rebasepro_ui));
9
- })(this, function(exports, react, react_jsx_runtime, _rebasepro_app, _rebasepro_ui) {
10
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
11
- //#region \0rolldown/runtime.js
12
- var __create = Object.create;
13
- var __defProp = Object.defineProperty;
14
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
15
- var __getOwnPropNames = Object.getOwnPropertyNames;
16
- var __getProtoOf = Object.getPrototypeOf;
17
- var __hasOwnProp = Object.prototype.hasOwnProperty;
18
- var __copyProps = (to, from, except, desc) => {
19
- if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
20
- key = keys[i];
21
- if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
22
- get: ((k) => from[k]).bind(null, key),
23
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
24
- });
25
- }
26
- return to;
27
- };
28
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
29
- value: mod,
30
- enumerable: true
31
- }) : target, mod));
32
- //#endregion
33
- react = __toESM(react, 1);
34
- //#region src/engine/InsightsCache.ts
35
- /**
36
- * In-memory cache for insight query results.
37
- * Supports TTL-based expiry and inflight request deduplication
38
- * to prevent redundant network requests when multiple widgets
39
- * share the same query.
40
- */
41
- var InsightsCache = class {
42
- ttl;
43
- cache = /* @__PURE__ */ new Map();
44
- inflight = /* @__PURE__ */ new Map();
45
- constructor(ttl = 6e4) {
46
- this.ttl = ttl;
47
- }
48
- get(key) {
49
- const entry = this.cache.get(key);
50
- if (!entry) return null;
51
- if (Date.now() - entry.timestamp > this.ttl) {
52
- this.cache.delete(key);
53
- return null;
54
- }
55
- return entry.data;
56
- }
57
- set(key, data) {
58
- this.cache.set(key, {
59
- data,
60
- timestamp: Date.now()
61
- });
62
- this.inflight.delete(key);
63
- }
64
- getInflight(key) {
65
- return this.inflight.get(key) ?? null;
66
- }
67
- setInflight(key, promise) {
68
- this.inflight.set(key, promise);
69
- }
70
- invalidate(key) {
71
- if (key) {
72
- this.cache.delete(key);
73
- this.inflight.delete(key);
74
- } else {
75
- this.cache.clear();
76
- this.inflight.clear();
77
- }
78
- }
79
- };
80
- //#endregion
81
- //#region src/engine/InsightsProvider.tsx
82
- var InsightsContext = (0, react.createContext)(null);
83
- /**
84
- * Root-level provider for the insights data engine.
85
- * Injected automatically by the plugin via `providers: [{ scope: "root" }]`.
86
- *
87
- * Manages a single `InsightsCache` instance shared by all insight widgets
88
- * for TTL-based caching and inflight request deduplication.
89
- */
90
- function InsightsProvider({ cacheTTL, children }) {
91
- const cache = (0, react.useMemo)(() => new InsightsCache(cacheTTL), [cacheTTL]);
92
- const value = (0, react.useMemo)(() => ({ cache }), [cache]);
93
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InsightsContext.Provider, {
94
- value,
95
- children
96
- });
97
- }
98
- /**
99
- * Access the insights cache (for advanced usage).
100
- * Returns null when called outside of an `InsightsProvider`
101
- * (e.g. during auth-loading phase before plugin providers mount).
102
- */
103
- function useInsightsEngine() {
104
- return (0, react.useContext)(InsightsContext);
105
- }
106
- //#endregion
107
- //#region src/engine/useInsightsData.ts
108
- /**
109
- * Hook that fetches and caches data for a single insight definition.
110
- *
111
- * Calls the definition's own `data()` callback and manages:
112
- * - TTL-based caching via InsightsCache
113
- * - Inflight request deduplication (multiple mounts of the same widget)
114
- * - Loading and error state management
115
- *
116
- * @param definition - The insight to fetch data for
117
- * @param collectionSlug - Optional collection context for cache key scoping
118
- */
119
- function useInsightsData(definition, context) {
120
- const cache = useInsightsEngine()?.cache ?? null;
121
- const { initialLoading, authLoading, user, loginSkipped } = (0, _rebasepro_app.useAuthController)();
122
- const authReady = !initialLoading && !authLoading && (Boolean(user) || loginSkipped);
123
- const [data, setData] = (0, react.useState)(null);
124
- const [loading, setLoading] = (0, react.useState)(true);
125
- const [error, setError] = (0, react.useState)(null);
126
- const cacheKey = `${definition.id}:${context.path ?? context.collectionSlug ?? "global"}`;
127
- (0, react.useEffect)(() => {
128
- if (!authReady || !cache) return;
129
- let cancelled = false;
130
- const cached = cache.get(cacheKey);
131
- if (cached) {
132
- setData(cached);
133
- setLoading(false);
134
- return;
135
- }
136
- const inflight = cache.getInflight(cacheKey);
137
- if (inflight) {
138
- setLoading(true);
139
- inflight.then((result) => {
140
- if (!cancelled) setData(result);
141
- }).catch((err) => {
142
- if (!cancelled) setError(err instanceof Error ? err : new Error(String(err)));
143
- }).finally(() => {
144
- if (!cancelled) setLoading(false);
145
- });
146
- return;
147
- }
148
- setLoading(true);
149
- setError(null);
150
- const promise = definition.data(context);
151
- cache.setInflight(cacheKey, promise);
152
- promise.then((result) => {
153
- cache.set(cacheKey, result);
154
- if (!cancelled) setData(result);
155
- }).catch((err) => {
156
- cache.invalidate(cacheKey);
157
- if (!cancelled) setError(err instanceof Error ? err : new Error(String(err)));
158
- }).finally(() => {
159
- if (!cancelled) setLoading(false);
160
- });
161
- return () => {
162
- cancelled = true;
163
- };
164
- }, [
165
- definition.id,
166
- definition.data,
167
- context.path,
168
- context.collectionSlug,
169
- cacheKey,
170
- cache,
171
- authReady
172
- ]);
173
- return {
174
- data,
175
- loading,
176
- error
177
- };
178
- }
179
- //#endregion
180
- //#region src/components/InsightsScorecardView.tsx
181
- function formatNumber(value, format) {
182
- if (value === null || value === void 0) return "N/A";
183
- const options = {
184
- style: format?.style ?? "decimal",
185
- notation: format?.notation ?? "standard",
186
- maximumFractionDigits: format?.decimals ?? 1,
187
- minimumFractionDigits: format?.decimals ?? 1
188
- };
189
- if (format?.style === "currency") options.currency = format.currency ?? "USD";
190
- let formatted = new Intl.NumberFormat("en-US", options).format(value);
191
- if (format?.showSign && value > 0) formatted = "+" + formatted;
192
- return formatted;
193
- }
194
- /**
195
- * Scorecard widget for the Rebase design system.
196
- *
197
- * Renders a single KPI metric with optional comparison value and icon.
198
- * Uses Tailwind `dark:` classes — no JS dark mode detection.
199
- * Icons are resolved via `getIcon` from `@rebasepro/app`.
200
- */
201
- function InsightsScorecardView({ config, data, title, compact = false, embedded = false, fixedHeight }) {
202
- const containerRef = (0, react.useRef)(null);
203
- const [containerWidth, setContainerWidth] = (0, react.useState)(null);
204
- react.default.useLayoutEffect(() => {
205
- if (!containerRef.current) return;
206
- setContainerWidth(containerRef.current.offsetWidth);
207
- const observer = new ResizeObserver((entries) => {
208
- for (const entry of entries) setContainerWidth(entry.contentRect.width);
209
- });
210
- observer.observe(containerRef.current);
211
- return () => observer.disconnect();
212
- }, []);
213
- const mainValue = data[config.value.field];
214
- const formattedValue = typeof mainValue === "number" ? formatNumber(mainValue, config.value.format) : String(mainValue ?? "N/A");
215
- let comparisonElement = null;
216
- if (config.comparison) {
217
- const comparisonValue = data[config.comparison.field];
218
- if (typeof comparisonValue === "number") {
219
- const formattedComparison = formatNumber(comparisonValue, config.comparison.format);
220
- const isPositive = comparisonValue > 0;
221
- const isNegative = comparisonValue < 0;
222
- let colorClass = "text-surface-500 dark:text-surface-400";
223
- if (config.comparison.intent === "increase_is_good") {
224
- if (isPositive) colorClass = "text-emerald-500";
225
- if (isNegative) colorClass = "text-red-500";
226
- } else if (config.comparison.intent === "decrease_is_good") {
227
- if (isPositive) colorClass = "text-red-500";
228
- if (isNegative) colorClass = "text-emerald-500";
229
- }
230
- comparisonElement = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
231
- className: `font-medium ${compact ? "text-[10px]" : "text-xs"} ${colorClass}`,
232
- children: formattedComparison
233
- });
234
- }
235
- }
236
- const isSmall = compact || containerWidth !== null && containerWidth < 200;
237
- const iconElement = config.icon ? (0, _rebasepro_app.getIcon)(config.icon, "text-surface-400 dark:text-surface-500", void 0, isSmall ? 14 : 18) : null;
238
- if (compact) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
239
- className: (0, _rebasepro_ui.cls)("flex flex-col gap-0.5 px-2.5 py-2 rounded-md bg-transparent border min-w-0", _rebasepro_ui.defaultBorderMixin),
240
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
241
- className: "text-[10px] uppercase tracking-wider text-surface-400 dark:text-surface-500 truncate",
242
- children: title
243
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
244
- className: "flex items-baseline gap-1.5",
245
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
246
- className: "text-sm font-semibold tabular-nums text-surface-800 dark:text-surface-100",
247
- children: formattedValue
248
- }), comparisonElement]
249
- })]
250
- });
251
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
252
- ref: containerRef,
253
- className: embedded ? `flex flex-col min-w-0 h-full ${isSmall ? "px-3.5 py-3" : "px-5 py-4"}` : (0, _rebasepro_ui.cls)("rounded-lg flex flex-col min-w-0 bg-transparent border", _rebasepro_ui.defaultBorderMixin, isSmall ? "px-3.5 py-3" : "px-5 py-4"),
254
- style: embedded ? void 0 : fixedHeight ? { height: fixedHeight } : { minHeight: isSmall ? 68 : 92 },
255
- children: [
256
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
257
- className: `flex items-center justify-between ${isSmall ? "mb-1" : "mb-2"}`,
258
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
259
- className: "flex flex-col min-w-0",
260
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
261
- className: `font-medium leading-snug truncate text-surface-500 dark:text-surface-400 ${isSmall ? "text-[11px]" : "text-xs"}`,
262
- children: title
263
- }), config.dateRange && !isSmall && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
264
- className: "text-[10px] text-surface-400 dark:text-surface-500 truncate mt-0.5",
265
- children: config.dateRange
266
- })]
267
- }), iconElement && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
268
- className: "ml-2 shrink-0",
269
- children: iconElement
270
- })]
271
- }),
272
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
273
- className: `font-semibold leading-tight tracking-tight break-all text-surface-800 dark:text-surface-100 ${isSmall ? "text-lg" : containerWidth !== null && containerWidth < 300 ? "text-xl" : "text-2xl"}`,
274
- children: formattedValue
275
- }),
276
- comparisonElement && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
277
- className: isSmall ? "mt-0.5" : "mt-1",
278
- children: comparisonElement
279
- })
280
- ]
281
- });
282
- }
283
- InsightsScorecardView.displayName = "InsightsScorecardView";
284
- //#endregion
285
- //#region src/components/InsightWidgetSkeleton.tsx
286
- /**
287
- * Skeleton loader for scorecard insight widgets — displays animated
288
- * shimmer placeholders that exactly match the final rendered layout
289
- * of InsightsScorecardView for a given config, preventing layout shift.
290
- *
291
- * The skeleton receives the scorecard config so it can conditionally
292
- * render placeholder lines for comparison, dateRange, and icon —
293
- * only when the loaded view will also render them.
294
- *
295
- * The standard skeleton mirrors InsightsScorecardView's responsive
296
- * container-width breakpoints (ResizeObserver → isSmall / isMedium)
297
- * and uses placeholder heights that exactly match the **computed**
298
- * Tailwind line-heights (accounting for `leading-*` overrides).
299
- * This guarantees a pixel-perfect skeleton → loaded transition.
300
- */
301
- function InsightWidgetSkeleton({ config, compact = false, embedded = false, fixedHeight }) {
302
- const hasComparison = Boolean(config.comparison);
303
- const hasIcon = Boolean(config.icon);
304
- const hasDateRange = Boolean(config.dateRange);
305
- if (compact) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
306
- className: (0, _rebasepro_ui.cls)("animate-pulse", embedded ? "h-full px-2.5 py-2" : "flex flex-col gap-0.5 rounded-md bg-transparent border min-w-0 px-2.5 py-2", !embedded && _rebasepro_ui.defaultBorderMixin),
307
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
308
- className: "bg-surface-200 dark:bg-surface-700 rounded-sm",
309
- style: {
310
- height: 14,
311
- width: 48
312
- }
313
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
314
- className: "flex items-baseline gap-1.5",
315
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
316
- className: "bg-surface-200 dark:bg-surface-700 rounded-sm",
317
- style: {
318
- height: 20,
319
- width: 40
320
- }
321
- }), hasComparison && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
322
- className: "bg-surface-200/60 dark:bg-surface-700/60 rounded-sm",
323
- style: {
324
- height: 14,
325
- width: 28
326
- }
327
- })]
328
- })]
329
- });
330
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StandardSkeleton, {
331
- hasComparison,
332
- hasIcon,
333
- hasDateRange,
334
- embedded,
335
- fixedHeight
336
- });
337
- }
338
- /**
339
- * Inner component for the standard scorecard skeleton.
340
- *
341
- * Mirrors InsightsScorecardView's layout by:
342
- * 1. Using the same ResizeObserver + containerWidth pattern for
343
- * responsive breakpoints (isSmall < 200px, isMedium < 300px).
344
- * 2. Using placeholder heights derived from the exact computed
345
- * Tailwind line-heights that InsightsScorecardView renders.
346
- * 3. Matching all container classes, margins, paddings, and flex
347
- * layout properties identically.
348
- */
349
- function StandardSkeleton({ hasComparison, hasIcon, hasDateRange, embedded, fixedHeight }) {
350
- const containerRef = (0, react.useRef)(null);
351
- const [containerWidth, setContainerWidth] = (0, react.useState)(null);
352
- react.default.useLayoutEffect(() => {
353
- if (!containerRef.current) return;
354
- setContainerWidth(containerRef.current.offsetWidth);
355
- const observer = new ResizeObserver((entries) => {
356
- for (const entry of entries) setContainerWidth(entry.contentRect.width);
357
- });
358
- observer.observe(containerRef.current);
359
- return () => observer.disconnect();
360
- }, []);
361
- const isSmall = containerWidth !== null && containerWidth < 200;
362
- const titleHeight = isSmall ? 15 : 16.5;
363
- const valueHeight = isSmall ? 22.5 : containerWidth !== null && containerWidth < 300 ? 25 : 30;
364
- const comparisonHeight = 16;
365
- const iconSize = isSmall ? 14 : 18;
366
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
367
- ref: containerRef,
368
- className: (0, _rebasepro_ui.cls)("animate-pulse", embedded ? `flex flex-col min-w-0 h-full ${isSmall ? "px-3.5 py-3" : "px-5 py-4"}` : (0, _rebasepro_ui.cls)("rounded-lg flex flex-col min-w-0 bg-transparent border", _rebasepro_ui.defaultBorderMixin, isSmall ? "px-3.5 py-3" : "px-5 py-4")),
369
- style: embedded ? void 0 : fixedHeight ? { height: fixedHeight } : { minHeight: isSmall ? 68 : 92 },
370
- children: [
371
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
372
- className: `flex items-center justify-between ${isSmall ? "mb-1" : "mb-2"}`,
373
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
374
- className: "flex flex-col min-w-0",
375
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
376
- className: "bg-surface-200 dark:bg-surface-700 rounded",
377
- style: {
378
- height: titleHeight,
379
- width: "60%"
380
- }
381
- }), hasDateRange && !isSmall && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
382
- className: "bg-surface-200/60 dark:bg-surface-700/60 rounded mt-0.5",
383
- style: {
384
- height: 14,
385
- width: "40%"
386
- }
387
- })]
388
- }), hasIcon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
389
- className: "ml-2 shrink-0",
390
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
391
- className: "bg-surface-200 dark:bg-surface-700 rounded",
392
- style: {
393
- height: iconSize,
394
- width: iconSize
395
- }
396
- })
397
- })]
398
- }),
399
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
400
- className: "bg-surface-200 dark:bg-surface-700 rounded",
401
- style: {
402
- height: valueHeight,
403
- width: "40%"
404
- }
405
- }),
406
- hasComparison && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
407
- className: isSmall ? "mt-0.5" : "mt-1",
408
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
409
- className: "bg-surface-200/60 dark:bg-surface-700/60 rounded",
410
- style: {
411
- height: comparisonHeight,
412
- width: "25%"
413
- }
414
- })
415
- })
416
- ]
417
- });
418
- }
419
- InsightWidgetSkeleton.displayName = "InsightWidgetSkeleton";
420
- //#endregion
421
- //#region src/components/InsightWidget.tsx
422
- /**
423
- * Compute a deterministic fixed height for a standard scorecard based
424
- * on which optional elements the config declares. This eliminates
425
- * layout shift between skeleton and loaded states.
426
- *
427
- * Breakdown (non-compact, non-small):
428
- * py-4 padding: 16 + 16 = 32
429
- * title row: 16.5 (text-xs leading-snug)
430
- * mb-2 margin: 8
431
- * value: 30 (text-2xl leading-tight)
432
- * ---
433
- * base: 86.5
434
- * + dateRange: +16 (14px text + 2px mt-0.5)
435
- * + comparison: +20 (16px text + 4px mt-1)
436
- */
437
- function computeFixedHeight(config) {
438
- let h = 86.5;
439
- if (config.dateRange) h += 16;
440
- if (config.comparison) h += 20;
441
- return Math.ceil(h);
442
- }
443
- /**
444
- * Single insight widget orchestrator.
445
- *
446
- * Wraps skeleton and loaded states in a fixed-height container
447
- * (computed from the scorecard config) to prevent layout shift.
448
- *
449
- * All theme-awareness is handled via Tailwind `dark:` classes.
450
- */
451
- function InsightWidget({ definition, collectionSlug, path, parentCollectionSlugs, parentEntityIds, compact = false, embedded = false }) {
452
- const { data, loading, error } = useInsightsData(definition, {
453
- path,
454
- collectionSlug,
455
- parentCollectionSlugs
456
- });
457
- const fixedHeight = !compact && !embedded ? computeFixedHeight(definition.scorecard) : void 0;
458
- if (loading) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InsightWidgetSkeleton, {
459
- config: definition.scorecard,
460
- compact,
461
- embedded,
462
- fixedHeight
463
- });
464
- if (error) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
465
- className: `text-red-500/70 dark:text-red-400/70 text-[0.8125rem] ${embedded ? "px-5 py-4 h-full" : `rounded-lg bg-red-500/5 dark:bg-red-400/5 border border-red-500/10 dark:border-red-400/10 ${compact ? "px-3.5 py-3" : "px-5 py-4"}`}`,
466
- style: fixedHeight ? { height: fixedHeight } : void 0,
467
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
468
- className: "font-semibold mb-1",
469
- children: definition.title
470
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: error.message })]
471
- });
472
- if (!data || data.rows.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
473
- className: `text-surface-400 dark:text-surface-500 text-[0.8125rem] ${embedded ? "px-5 py-4 h-full" : `rounded-lg bg-surface-100 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 ${compact ? "px-3.5 py-3" : "px-5 py-4"}`}`,
474
- style: fixedHeight ? { height: fixedHeight } : void 0,
475
- children: [definition.title, " — No data"]
476
- });
477
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InsightsScorecardView, {
478
- config: definition.scorecard,
479
- data: data.rows[0],
480
- title: definition.title,
481
- compact,
482
- embedded,
483
- fixedHeight
484
- });
485
- }
486
- InsightWidget.displayName = "InsightWidget";
487
- //#endregion
488
- //#region src/components/HomeCardInsightSlot.tsx
489
- /**
490
- * Renders compact insight widgets inline within a home page collection card.
491
- * Injected via the `home.card.insight` slot.
492
- *
493
- * Uses a horizontal flex layout so multiple cards sit side by side.
494
- */
495
- function HomeCardInsightSlot({ slug, insights }) {
496
- if (!insights || insights.length === 0) return null;
497
- const estimatedRows = Math.ceil(insights.length / 2);
498
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
499
- className: "flex flex-wrap items-center gap-1.5 mt-2",
500
- style: { minHeight: estimatedRows * 42 + (estimatedRows - 1) * 6 },
501
- children: insights.map((def) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InsightWidget, {
502
- definition: def,
503
- collectionSlug: slug,
504
- compact: true
505
- }, def.id))
506
- });
507
- }
508
- HomeCardInsightSlot.displayName = "HomeCardInsightSlot";
509
- //#endregion
510
- //#region src/components/HomeInsightsSlot.tsx
511
- /**
512
- * Scorecard insights panel rendered at the top of the home page.
513
- * Injected via the `home.children.start` slot.
514
- *
515
- * Renders scorecards in a responsive grid (up to 4 columns).
516
- */
517
- function HomeInsightsSlot({ insights }) {
518
- if (!insights || insights.length === 0) return null;
519
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
520
- className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3 pb-6",
521
- style: { minHeight: 92 },
522
- children: insights.map((def) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InsightWidget, { definition: def }, def.id))
523
- });
524
- }
525
- HomeInsightsSlot.displayName = "HomeInsightsSlot";
526
- //#endregion
527
- //#region src/components/CollectionInsightsInline.tsx
528
- /**
529
- * Renders scorecard insight widgets inline within a collection's list view,
530
- * positioned below the title and above the main data list.
531
- *
532
- * Injected via the `collection.insights` slot.
533
- */
534
- function CollectionInsightsInline({ insights, path, parentCollectionSlugs, parentEntityIds }) {
535
- if (!insights || insights.length === 0) return null;
536
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
537
- className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3 pb-4",
538
- children: insights.map((def) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InsightWidget, {
539
- definition: def,
540
- path,
541
- parentCollectionSlugs,
542
- parentEntityIds
543
- }, def.id))
544
- });
545
- }
546
- CollectionInsightsInline.displayName = "CollectionInsightsInline";
547
- //#endregion
548
- //#region src/useInsightsPlugin.tsx
549
- /**
550
- * Creates the Insights plugin for Rebase.
551
- *
552
- * This plugin injects scorecard widgets into key UI locations:
553
- * - **Home page header**: KPI overview via `home.children.start` slot
554
- * - **Collection list view**: Scorecards inline (below title, above list) via `collection.insights` slot
555
- * - **Home page cards**: Compact scorecard metrics auto-extracted from collection insights via `home.card.insight` slot
556
- *
557
- * Collection-level insights (`collections.<slug>`) are the single source of truth:
558
- * scorecards render in the collection list view and are automatically extracted
559
- * to show as compact widgets on the corresponding home page card.
560
- *
561
- * Each insight owns its own `data()` callback — use the Rebase client SDK,
562
- * call a custom function, or hit any external API. Full flexibility, zero new endpoints.
563
- *
564
- * @example
565
- * ```typescript
566
- * import { useInsightsPlugin } from "@rebasepro/plugin-insights";
567
- *
568
- * const insightsPlugin = useInsightsPlugin({
569
- * cacheTTL: 120_000,
570
- * insights: {
571
- * home: [
572
- * { id: "revenue", title: "Revenue", data: async () => ..., scorecard: { ... } },
573
- * ],
574
- * collections: {
575
- * orders: [
576
- * { id: "total", title: "Total Orders", data: async () => ..., scorecard: { ... } },
577
- * ],
578
- * },
579
- * },
580
- * });
581
- * ```
582
- */
583
- function useInsightsPlugin(config) {
584
- const { insights, cacheTTL } = config;
585
- return react.default.useMemo(() => {
586
- const slots = [];
587
- if (insights.home && insights.home.length > 0) {
588
- const homeInsights = insights.home;
589
- slots.push({
590
- slot: "home.children.start",
591
- Component: (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HomeInsightsSlot, {
592
- ...props,
593
- insights: homeInsights
594
- }),
595
- order: 10
596
- });
597
- }
598
- if (insights.collections) for (const [slug, defs] of Object.entries(insights.collections)) {
599
- if (defs.length === 0) continue;
600
- const collectionInsights = defs;
601
- slots.push({
602
- slot: "collection.insights",
603
- Component: (props) => {
604
- if ((props.path?.split("/").filter(Boolean).pop() ?? "") !== slug) return null;
605
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollectionInsightsInline, {
606
- ...props,
607
- insights: collectionInsights
608
- });
609
- },
610
- order: 10
611
- });
612
- slots.push({
613
- slot: "home.card.insight",
614
- Component: (props) => {
615
- if (props.slug !== slug) return null;
616
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HomeCardInsightSlot, {
617
- ...props,
618
- insights: collectionInsights
619
- });
620
- },
621
- order: 10
622
- });
623
- }
624
- return {
625
- key: "plugin-insights",
626
- slots,
627
- providers: [{
628
- scope: "root",
629
- Component: InsightsProvider,
630
- props: { cacheTTL }
631
- }]
632
- };
633
- }, [insights, cacheTTL]);
634
- }
635
- //#endregion
636
- exports.InsightWidget = InsightWidget;
637
- exports.InsightWidgetSkeleton = InsightWidgetSkeleton;
638
- exports.InsightsCache = InsightsCache;
639
- exports.InsightsProvider = InsightsProvider;
640
- exports.InsightsScorecardView = InsightsScorecardView;
641
- exports.useInsightsData = useInsightsData;
642
- exports.useInsightsEngine = useInsightsEngine;
643
- exports.useInsightsPlugin = useInsightsPlugin;
644
- });
645
-
646
- //# sourceMappingURL=index.umd.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.umd.js","names":[],"sources":["../src/engine/InsightsCache.ts","../src/engine/InsightsProvider.tsx","../src/engine/useInsightsData.ts","../src/components/InsightsScorecardView.tsx","../src/components/InsightWidgetSkeleton.tsx","../src/components/InsightWidget.tsx","../src/components/HomeCardInsightSlot.tsx","../src/components/HomeInsightsSlot.tsx","../src/components/CollectionInsightsInline.tsx","../src/useInsightsPlugin.tsx"],"sourcesContent":["import type { InsightDataResult } from \"../types\";\n\ninterface CacheEntry {\n data: InsightDataResult;\n timestamp: number;\n}\n\n/**\n * In-memory cache for insight query results.\n * Supports TTL-based expiry and inflight request deduplication\n * to prevent redundant network requests when multiple widgets\n * share the same query.\n */\nexport class InsightsCache {\n private cache = new Map<string, CacheEntry>();\n private inflight = new Map<string, Promise<InsightDataResult>>();\n\n constructor(private ttl = 60_000) {}\n\n get(key: string): InsightDataResult | null {\n const entry = this.cache.get(key);\n if (!entry) return null;\n if (Date.now() - entry.timestamp > this.ttl) {\n this.cache.delete(key);\n return null;\n }\n return entry.data;\n }\n\n set(key: string, data: InsightDataResult): void {\n this.cache.set(key, { data,\ntimestamp: Date.now() });\n this.inflight.delete(key);\n }\n\n getInflight(key: string): Promise<InsightDataResult> | null {\n return this.inflight.get(key) ?? null;\n }\n\n setInflight(key: string, promise: Promise<InsightDataResult>): void {\n this.inflight.set(key, promise);\n }\n\n invalidate(key?: string): void {\n if (key) {\n this.cache.delete(key);\n this.inflight.delete(key);\n } else {\n this.cache.clear();\n this.inflight.clear();\n }\n }\n}\n","import React, { createContext, useContext, useMemo, type PropsWithChildren } from \"react\";\nimport { InsightsCache } from \"./InsightsCache\";\n\ninterface InsightsContextValue {\n cache: InsightsCache;\n}\n\nconst InsightsContext = createContext<InsightsContextValue | null>(null);\n\n/**\n * Root-level provider for the insights data engine.\n * Injected automatically by the plugin via `providers: [{ scope: \"root\" }]`.\n *\n * Manages a single `InsightsCache` instance shared by all insight widgets\n * for TTL-based caching and inflight request deduplication.\n */\nexport function InsightsProvider({\n cacheTTL,\n children\n}: PropsWithChildren<{ cacheTTL?: number }>) {\n const cache = useMemo(() => new InsightsCache(cacheTTL), [cacheTTL]);\n const value = useMemo(() => ({ cache }), [cache]);\n\n return (\n <InsightsContext.Provider value={value}>\n {children}\n </InsightsContext.Provider>\n );\n}\n\n/**\n * Access the insights cache (for advanced usage).\n * Returns null when called outside of an `InsightsProvider`\n * (e.g. during auth-loading phase before plugin providers mount).\n */\nexport function useInsightsEngine(): InsightsContextValue | null {\n return useContext(InsightsContext);\n}\n","import { useEffect, useState } from \"react\";\nimport type { InsightDefinition, InsightDataResult, InsightContext } from \"../types\";\nimport { useInsightsEngine } from \"./InsightsProvider\";\nimport { useAuthController } from \"@rebasepro/app\";\n\n/**\n * Hook that fetches and caches data for a single insight definition.\n *\n * Calls the definition's own `data()` callback and manages:\n * - TTL-based caching via InsightsCache\n * - Inflight request deduplication (multiple mounts of the same widget)\n * - Loading and error state management\n *\n * @param definition - The insight to fetch data for\n * @param collectionSlug - Optional collection context for cache key scoping\n */\nexport function useInsightsData(\n definition: InsightDefinition,\n context: InsightContext\n): {\n data: InsightDataResult | null;\n loading: boolean;\n error: Error | null;\n} {\n const engine = useInsightsEngine();\n const cache = engine?.cache ?? null;\n const { initialLoading, authLoading, user, loginSkipped } = useAuthController();\n const authReady = !initialLoading && !authLoading && (Boolean(user) || loginSkipped);\n const [data, setData] = useState<InsightDataResult | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const cacheKey = `${definition.id}:${context.path ?? context.collectionSlug ?? \"global\"}`;\n\n useEffect(() => {\n // Keep showing skeleton until both auth and engine are ready\n if (!authReady || !cache) {\n return;\n }\n\n let cancelled = false;\n\n // 1. Check cache\n const cached = cache.get(cacheKey);\n if (cached) {\n setData(cached);\n setLoading(false);\n return;\n }\n\n // 2. Check inflight — deduplicate concurrent requests for the same widget\n const inflight = cache.getInflight(cacheKey);\n if (inflight) {\n setLoading(true);\n inflight\n .then((result) => {\n if (!cancelled) {\n setData(result);\n }\n })\n .catch((err) => {\n if (!cancelled) setError(err instanceof Error ? err : new Error(String(err)));\n })\n .finally(() => {\n if (!cancelled) setLoading(false);\n });\n return;\n }\n\n // 3. Fresh fetch — invoke the definition's own data callback\n setLoading(true);\n setError(null);\n\n const promise = definition.data(context);\n\n cache.setInflight(cacheKey, promise);\n\n promise\n .then((result) => {\n cache.set(cacheKey, result);\n if (!cancelled) {\n setData(result);\n }\n })\n .catch((err) => {\n cache.invalidate(cacheKey);\n if (!cancelled) {\n setError(err instanceof Error ? err : new Error(String(err)));\n }\n })\n .finally(() => {\n if (!cancelled) setLoading(false);\n });\n\n return () => {\n cancelled = true;\n };\n }, [definition.id, definition.data, context.path, context.collectionSlug, cacheKey, cache, authReady]);\n\n return { data,\nloading,\nerror };\n}\n","import React, { useRef, useState } from \"react\";\nimport { getIcon } from \"@rebasepro/app\";\nimport { cls, defaultBorderMixin } from \"@rebasepro/ui\";\nimport type { DataRow, ScorecardConfig, ScorecardFormat } from \"../types\";\n\nfunction formatNumber(value: number, format?: ScorecardFormat): string {\n if (value === null || value === undefined) return \"N/A\";\n\n const options: Intl.NumberFormatOptions = {\n style: format?.style ?? \"decimal\",\n notation: format?.notation ?? \"standard\",\n maximumFractionDigits: format?.decimals ?? 1,\n minimumFractionDigits: format?.decimals ?? 1\n };\n\n if (format?.style === \"currency\") {\n options.currency = format.currency ?? \"USD\";\n }\n\n let formatted = new Intl.NumberFormat(\"en-US\", options).format(value);\n\n if (format?.showSign && value > 0) {\n formatted = \"+\" + formatted;\n }\n\n return formatted;\n}\n\n/**\n * Scorecard widget for the Rebase design system.\n *\n * Renders a single KPI metric with optional comparison value and icon.\n * Uses Tailwind `dark:` classes — no JS dark mode detection.\n * Icons are resolved via `getIcon` from `@rebasepro/app`.\n */\nexport function InsightsScorecardView({\n config,\n data,\n title,\n compact = false,\n embedded = false,\n fixedHeight\n}: {\n config: ScorecardConfig;\n data: DataRow;\n title: string;\n compact?: boolean;\n /** When true, skip own border/bg since the parent card provides them. */\n embedded?: boolean;\n /** Explicit height to prevent layout shift between skeleton → loaded. */\n fixedHeight?: number;\n}) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [containerWidth, setContainerWidth] = useState<number | null>(null);\n\n React.useLayoutEffect(() => {\n if (!containerRef.current) return;\n // Read initial width synchronously before paint\n setContainerWidth(containerRef.current.offsetWidth);\n const observer = new ResizeObserver((entries) => {\n for (const entry of entries) {\n setContainerWidth(entry.contentRect.width);\n }\n });\n observer.observe(containerRef.current);\n return () => observer.disconnect();\n }, []);\n\n const mainValue = data[config.value.field];\n const formattedValue = typeof mainValue === \"number\"\n ? formatNumber(mainValue, config.value.format)\n : String(mainValue ?? \"N/A\");\n\n // Comparison rendering\n let comparisonElement: React.ReactNode = null;\n if (config.comparison) {\n const comparisonValue = data[config.comparison.field];\n if (typeof comparisonValue === \"number\") {\n const formattedComparison = formatNumber(comparisonValue, config.comparison.format);\n const isPositive = comparisonValue > 0;\n const isNegative = comparisonValue < 0;\n\n let colorClass = \"text-surface-500 dark:text-surface-400\";\n if (config.comparison.intent === \"increase_is_good\") {\n if (isPositive) colorClass = \"text-emerald-500\";\n if (isNegative) colorClass = \"text-red-500\";\n } else if (config.comparison.intent === \"decrease_is_good\") {\n if (isPositive) colorClass = \"text-red-500\";\n if (isNegative) colorClass = \"text-emerald-500\";\n }\n\n comparisonElement = (\n <span className={`font-medium ${compact ? \"text-[10px]\" : \"text-xs\"} ${colorClass}`}>\n {formattedComparison}\n </span>\n );\n }\n }\n\n const isSmall = compact || (containerWidth !== null && containerWidth < 200);\n\n // Resolve icon via getIcon (Lucide-based resolution)\n const iconElement = config.icon\n ? getIcon(config.icon, \"text-surface-400 dark:text-surface-500\", undefined, isSmall ? 14 : 18)\n : null;\n\n // ── Compact card-inline layout ──────────────────────────────────────\n if (compact) {\n return (\n <div className={cls(\"flex flex-col gap-0.5 px-2.5 py-2 rounded-md bg-transparent border min-w-0\", defaultBorderMixin)}>\n <span className=\"text-[10px] uppercase tracking-wider text-surface-400 dark:text-surface-500 truncate\">\n {title}\n </span>\n <div className=\"flex items-baseline gap-1.5\">\n <span className=\"text-sm font-semibold tabular-nums text-surface-800 dark:text-surface-100\">\n {formattedValue}\n </span>\n {comparisonElement}\n </div>\n </div>\n );\n }\n\n // ── Standard scorecard layout ───────────────────────────────────────\n const baseClass = embedded\n ? `flex flex-col min-w-0 h-full ${isSmall ? \"px-3.5 py-3\" : \"px-5 py-4\"}`\n : cls(\"rounded-lg flex flex-col min-w-0 bg-transparent border\", defaultBorderMixin, isSmall ? \"px-3.5 py-3\" : \"px-5 py-4\");\n\n return (\n <div ref={containerRef} className={baseClass} style={embedded ? undefined : fixedHeight ? { height: fixedHeight } : { minHeight: isSmall ? 68 : 92 }}>\n {/* Title row */}\n <div className={`flex items-center justify-between ${isSmall ? \"mb-1\" : \"mb-2\"}`}>\n <div className=\"flex flex-col min-w-0\">\n <span className={`font-medium leading-snug truncate text-surface-500 dark:text-surface-400 ${isSmall ? \"text-[11px]\" : \"text-xs\"}`}>\n {title}\n </span>\n {config.dateRange && !isSmall && (\n <span className=\"text-[10px] text-surface-400 dark:text-surface-500 truncate mt-0.5\">\n {config.dateRange}\n </span>\n )}\n </div>\n {iconElement && (\n <span className=\"ml-2 shrink-0\">{iconElement}</span>\n )}\n </div>\n\n {/* Main value */}\n <div className={`font-semibold leading-tight tracking-tight break-all text-surface-800 dark:text-surface-100 ${isSmall ? \"text-lg\" : (containerWidth !== null && containerWidth < 300) ? \"text-xl\" : \"text-2xl\"}`}>\n {formattedValue}\n </div>\n\n {/* Comparison */}\n {comparisonElement && (\n <div className={isSmall ? \"mt-0.5\" : \"mt-1\"}>\n {comparisonElement}\n </div>\n )}\n </div>\n );\n}\n\nInsightsScorecardView.displayName = \"InsightsScorecardView\";\n","import React, { useRef, useState } from \"react\";\nimport { cls, defaultBorderMixin } from \"@rebasepro/ui\";\nimport type { ScorecardConfig } from \"../types\";\n\n/**\n * Skeleton loader for scorecard insight widgets — displays animated\n * shimmer placeholders that exactly match the final rendered layout\n * of InsightsScorecardView for a given config, preventing layout shift.\n *\n * The skeleton receives the scorecard config so it can conditionally\n * render placeholder lines for comparison, dateRange, and icon —\n * only when the loaded view will also render them.\n *\n * The standard skeleton mirrors InsightsScorecardView's responsive\n * container-width breakpoints (ResizeObserver → isSmall / isMedium)\n * and uses placeholder heights that exactly match the **computed**\n * Tailwind line-heights (accounting for `leading-*` overrides).\n * This guarantees a pixel-perfect skeleton → loaded transition.\n */\nexport function InsightWidgetSkeleton({\n config,\n compact = false,\n embedded = false,\n fixedHeight\n}: {\n /** Scorecard config — used to match optional elements (comparison, dateRange, icon). */\n config: ScorecardConfig;\n compact?: boolean;\n /** When true, skip own border since the parent card provides it. */\n embedded?: boolean;\n /** Explicit height to prevent layout shift between skeleton → loaded. */\n fixedHeight?: number;\n}) {\n const hasComparison = Boolean(config.comparison);\n const hasIcon = Boolean(config.icon);\n const hasDateRange = Boolean(config.dateRange);\n\n // ── Compact scorecard skeleton ──────────────────────────────────────\n // Matches InsightsScorecardView compact layout:\n // container: flex flex-col gap-0.5 px-2.5 py-2 rounded-md border\n // title: text-[10px] uppercase → line-height ~14px\n // value row: text-sm font-semibold → line-height 20px\n // + optional comparison text-[10px] inside value row\n if (compact) {\n return (\n <div\n className={cls(\n \"animate-pulse\",\n embedded\n ? \"h-full px-2.5 py-2\"\n : \"flex flex-col gap-0.5 rounded-md bg-transparent border min-w-0 px-2.5 py-2\",\n !embedded && defaultBorderMixin\n )}\n >\n {/* Title line */}\n <div className=\"bg-surface-200 dark:bg-surface-700 rounded-sm\"\n style={{ height: 14,\nwidth: 48 }}\n />\n {/* Value + optional comparison row */}\n <div className=\"flex items-baseline gap-1.5\">\n <div className=\"bg-surface-200 dark:bg-surface-700 rounded-sm\"\n style={{ height: 20,\nwidth: 40 }}\n />\n {hasComparison && (\n <div className=\"bg-surface-200/60 dark:bg-surface-700/60 rounded-sm\"\n style={{ height: 14,\nwidth: 28 }}\n />\n )}\n </div>\n </div>\n );\n }\n\n // ── Standard scorecard skeleton ─────────────────────────────────────\n return <StandardSkeleton\n hasComparison={hasComparison}\n hasIcon={hasIcon}\n hasDateRange={hasDateRange}\n embedded={embedded}\n fixedHeight={fixedHeight}\n />;\n}\n\n// ── Tailwind line-height reference ──────────────────────────────────────\n// All heights below are the **computed** CSS line-heights, accounting\n// for `leading-*` overrides that InsightsScorecardView applies.\n//\n// Title:\n// text-xs (12px) + leading-snug (1.375) → 12 × 1.375 = 16.5px\n// text-[11px] + leading-snug (1.375) → 11 × 1.375 = 15.125px\n//\n// DateRange:\n// text-[10px] with no explicit LH → normal ≈ 14px (browser)\n//\n// Value:\n// text-2xl (24px) + leading-tight (1.25) → 24 × 1.25 = 30px\n// text-xl (20px) + leading-tight (1.25) → 20 × 1.25 = 25px\n// text-lg (18px) + leading-tight (1.25) → 18 × 1.25 = 22.5px\n//\n// Comparison:\n// text-xs (12px) → built-in LH 1rem = 16px\n\n/**\n * Inner component for the standard scorecard skeleton.\n *\n * Mirrors InsightsScorecardView's layout by:\n * 1. Using the same ResizeObserver + containerWidth pattern for\n * responsive breakpoints (isSmall < 200px, isMedium < 300px).\n * 2. Using placeholder heights derived from the exact computed\n * Tailwind line-heights that InsightsScorecardView renders.\n * 3. Matching all container classes, margins, paddings, and flex\n * layout properties identically.\n */\nfunction StandardSkeleton({\n hasComparison,\n hasIcon,\n hasDateRange,\n embedded,\n fixedHeight\n}: {\n hasComparison: boolean;\n hasIcon: boolean;\n hasDateRange: boolean;\n embedded: boolean;\n fixedHeight?: number;\n}) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [containerWidth, setContainerWidth] = useState<number | null>(null);\n\n React.useLayoutEffect(() => {\n if (!containerRef.current) return;\n setContainerWidth(containerRef.current.offsetWidth);\n const observer = new ResizeObserver((entries) => {\n for (const entry of entries) {\n setContainerWidth(entry.contentRect.width);\n }\n });\n observer.observe(containerRef.current);\n return () => observer.disconnect();\n }, []);\n\n // Mirror InsightsScorecardView's responsive breakpoints exactly\n const isSmall = containerWidth !== null && containerWidth < 200;\n\n // Computed line-heights for each breakpoint\n // Title: text-xs + leading-snug = 16.5px, text-[11px] + leading-snug = 15.125px\n const titleHeight = isSmall ? 15 : 16.5;\n // Value: leading-tight (×1.25) applied on top of font-size\n const valueHeight = isSmall\n ? 22.5 // text-lg: 18 × 1.25\n : (containerWidth !== null && containerWidth < 300)\n ? 25 // text-xl: 20 × 1.25\n : 30; // text-2xl: 24 × 1.25\n // Comparison: text-xs = 12px / 16px line-height (no leading override)\n const comparisonHeight = 16;\n // Icon: 14px when small, 18px otherwise\n const iconSize = isSmall ? 14 : 18;\n\n const baseClass = embedded\n ? `flex flex-col min-w-0 h-full ${isSmall ? \"px-3.5 py-3\" : \"px-5 py-4\"}`\n : cls(\"rounded-lg flex flex-col min-w-0 bg-transparent border\", defaultBorderMixin, isSmall ? \"px-3.5 py-3\" : \"px-5 py-4\");\n\n return (\n <div\n ref={containerRef}\n className={cls(\"animate-pulse\", baseClass)}\n style={embedded ? undefined : fixedHeight ? { height: fixedHeight } : { minHeight: isSmall ? 68 : 92 }}\n >\n {/* Title row — identical flex structure to InsightsScorecardView */}\n <div className={`flex items-center justify-between ${isSmall ? \"mb-1\" : \"mb-2\"}`}>\n <div className=\"flex flex-col min-w-0\">\n {/* Title placeholder */}\n <div className=\"bg-surface-200 dark:bg-surface-700 rounded\"\n style={{ height: titleHeight,\nwidth: \"60%\" }}\n />\n {/* DateRange — hidden when isSmall, same as real view (line 134) */}\n {hasDateRange && !isSmall && (\n <div className=\"bg-surface-200/60 dark:bg-surface-700/60 rounded mt-0.5\"\n style={{ height: 14,\nwidth: \"40%\" }}\n />\n )}\n </div>\n {/* Icon placeholder — same wrapper as real view */}\n {hasIcon && (\n <span className=\"ml-2 shrink-0\">\n <div className=\"bg-surface-200 dark:bg-surface-700 rounded\"\n style={{ height: iconSize,\nwidth: iconSize }}\n />\n </span>\n )}\n </div>\n\n {/* Main value placeholder */}\n <div className=\"bg-surface-200 dark:bg-surface-700 rounded\"\n style={{ height: valueHeight,\nwidth: \"40%\" }}\n />\n\n {/* Comparison placeholder */}\n {hasComparison && (\n <div className={isSmall ? \"mt-0.5\" : \"mt-1\"}>\n <div className=\"bg-surface-200/60 dark:bg-surface-700/60 rounded\"\n style={{ height: comparisonHeight,\nwidth: \"25%\" }}\n />\n </div>\n )}\n </div>\n );\n}\n\nInsightWidgetSkeleton.displayName = \"InsightWidgetSkeleton\";\n","import React from \"react\";\nimport type { InsightDefinition, DataRow, ScorecardConfig } from \"../types\";\nimport { useInsightsData } from \"../engine/useInsightsData\";\nimport { InsightsScorecardView } from \"./InsightsScorecardView\";\nimport { InsightWidgetSkeleton } from \"./InsightWidgetSkeleton\";\n\n/**\n * Compute a deterministic fixed height for a standard scorecard based\n * on which optional elements the config declares. This eliminates\n * layout shift between skeleton and loaded states.\n *\n * Breakdown (non-compact, non-small):\n * py-4 padding: 16 + 16 = 32\n * title row: 16.5 (text-xs leading-snug)\n * mb-2 margin: 8\n * value: 30 (text-2xl leading-tight)\n * ---\n * base: 86.5\n * + dateRange: +16 (14px text + 2px mt-0.5)\n * + comparison: +20 (16px text + 4px mt-1)\n */\nfunction computeFixedHeight(config: ScorecardConfig): number {\n let h = 86.5; // base: padding + title + mb-2 + value\n if (config.dateRange) h += 16;\n if (config.comparison) h += 20;\n return Math.ceil(h);\n}\n\n/**\n * Single insight widget orchestrator.\n *\n * Wraps skeleton and loaded states in a fixed-height container\n * (computed from the scorecard config) to prevent layout shift.\n *\n * All theme-awareness is handled via Tailwind `dark:` classes.\n */\nexport function InsightWidget({\n definition,\n collectionSlug,\n path,\n parentCollectionSlugs, parentEntityIds,\n compact = false,\n embedded = false\n}: {\n definition: InsightDefinition;\n collectionSlug?: string;\n path?: string;\n parentCollectionSlugs?: string[], parentEntityIds?: string[];\n compact?: boolean;\n /** When true, inner views skip their own borders since the parent card provides them. */\n embedded?: boolean;\n}) {\n const { data, loading, error } = useInsightsData(definition, { path,\ncollectionSlug,\nparentCollectionSlugs });\n\n // For non-compact, non-embedded standard scorecards, use a fixed height\n // derived from the config to prevent layout shift between skeleton → loaded.\n const fixedHeight = (!compact && !embedded) ? computeFixedHeight(definition.scorecard) : undefined;\n\n if (loading) {\n return <InsightWidgetSkeleton config={definition.scorecard} compact={compact} embedded={embedded} fixedHeight={fixedHeight} />;\n }\n\n if (error) {\n return (\n <div\n className={`text-red-500/70 dark:text-red-400/70 text-[0.8125rem] ${embedded ? \"px-5 py-4 h-full\" : `rounded-lg bg-red-500/5 dark:bg-red-400/5 border border-red-500/10 dark:border-red-400/10 ${compact ? \"px-3.5 py-3\" : \"px-5 py-4\"}`}`}\n style={fixedHeight ? { height: fixedHeight } : undefined}\n >\n <div className=\"font-semibold mb-1\">{definition.title}</div>\n <div>{error.message}</div>\n </div>\n );\n }\n\n if (!data || data.rows.length === 0) {\n return (\n <div\n className={`text-surface-400 dark:text-surface-500 text-[0.8125rem] ${embedded ? \"px-5 py-4 h-full\" : `rounded-lg bg-surface-100 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 ${compact ? \"px-3.5 py-3\" : \"px-5 py-4\"}`}`}\n style={fixedHeight ? { height: fixedHeight } : undefined}\n >\n {definition.title} — No data\n </div>\n );\n }\n\n return (\n <InsightsScorecardView\n config={definition.scorecard}\n data={data.rows[0] as DataRow}\n title={definition.title}\n compact={compact}\n embedded={embedded}\n fixedHeight={fixedHeight}\n />\n );\n}\n\nInsightWidget.displayName = \"InsightWidget\";\n","import React from \"react\";\nimport type { InsightDefinition } from \"../types\";\nimport { InsightWidget } from \"./InsightWidget\";\n\n/**\n * Renders compact insight widgets inline within a home page collection card.\n * Injected via the `home.card.insight` slot.\n *\n * Uses a horizontal flex layout so multiple cards sit side by side.\n */\nexport function HomeCardInsightSlot({\n slug,\n insights\n}: {\n slug: string;\n collection: unknown;\n context: unknown;\n insights: InsightDefinition[];\n}) {\n if (!insights || insights.length === 0) return null;\n\n // Each compact card row is ~42px; estimate 2 cards per row for wrapping\n const estimatedRows = Math.ceil(insights.length / 2);\n const minHeight = estimatedRows * 42 + (estimatedRows - 1) * 6; // 6px = gap-1.5\n\n return (\n <div className=\"flex flex-wrap items-center gap-1.5 mt-2\" style={{ minHeight }}>\n {insights.map((def) => (\n <InsightWidget\n key={def.id}\n definition={def}\n collectionSlug={slug}\n compact={true}\n />\n ))}\n </div>\n );\n}\n\nHomeCardInsightSlot.displayName = \"HomeCardInsightSlot\";\n","import React from \"react\";\nimport type { InsightDefinition } from \"../types\";\nimport { InsightWidget } from \"./InsightWidget\";\n\n/**\n * Scorecard insights panel rendered at the top of the home page.\n * Injected via the `home.children.start` slot.\n *\n * Renders scorecards in a responsive grid (up to 4 columns).\n */\nexport function HomeInsightsSlot({\n insights\n}: {\n insights: InsightDefinition[];\n}) {\n if (!insights || insights.length === 0) return null;\n\n return (\n <div\n className=\"grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3 pb-6\"\n style={{ minHeight: 92 }}\n >\n {insights.map((def) => (\n <InsightWidget key={def.id} definition={def} />\n ))}\n </div>\n );\n}\n\nHomeInsightsSlot.displayName = \"HomeInsightsSlot\";\n","import React from \"react\";\nimport type { InsightDefinition } from \"../types\";\nimport { InsightWidget } from \"./InsightWidget\";\n\n/**\n * Renders scorecard insight widgets inline within a collection's list view,\n * positioned below the title and above the main data list.\n *\n * Injected via the `collection.insights` slot.\n */\nexport function CollectionInsightsInline({\n insights,\n path,\n parentCollectionSlugs,\n parentEntityIds\n}: {\n path: string;\n collection: unknown;\n parentCollectionSlugs: string[], parentEntityIds: string[];\n insights: InsightDefinition[];\n}) {\n if (!insights || insights.length === 0) return null;\n\n return (\n <div className=\"grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3 pb-4\">\n {insights.map((def) => (\n <InsightWidget\n key={def.id}\n definition={def}\n path={path}\n parentCollectionSlugs={parentCollectionSlugs} parentEntityIds={parentEntityIds}\n />\n ))}\n </div>\n );\n}\n\nCollectionInsightsInline.displayName = \"CollectionInsightsInline\";\n","import React from \"react\";\nimport type { RebasePlugin, SlotContribution } from \"@rebasepro/types\";\nimport type { InsightsPluginConfig } from \"./types\";\nimport { InsightsProvider } from \"./engine/InsightsProvider\";\nimport { HomeCardInsightSlot } from \"./components/HomeCardInsightSlot\";\nimport { HomeInsightsSlot } from \"./components/HomeInsightsSlot\";\nimport { CollectionInsightsInline } from \"./components/CollectionInsightsInline\";\n\n/**\n * Creates the Insights plugin for Rebase.\n *\n * This plugin injects scorecard widgets into key UI locations:\n * - **Home page header**: KPI overview via `home.children.start` slot\n * - **Collection list view**: Scorecards inline (below title, above list) via `collection.insights` slot\n * - **Home page cards**: Compact scorecard metrics auto-extracted from collection insights via `home.card.insight` slot\n *\n * Collection-level insights (`collections.<slug>`) are the single source of truth:\n * scorecards render in the collection list view and are automatically extracted\n * to show as compact widgets on the corresponding home page card.\n *\n * Each insight owns its own `data()` callback — use the Rebase client SDK,\n * call a custom function, or hit any external API. Full flexibility, zero new endpoints.\n *\n * @example\n * ```typescript\n * import { useInsightsPlugin } from \"@rebasepro/plugin-insights\";\n *\n * const insightsPlugin = useInsightsPlugin({\n * cacheTTL: 120_000,\n * insights: {\n * home: [\n * { id: \"revenue\", title: \"Revenue\", data: async () => ..., scorecard: { ... } },\n * ],\n * collections: {\n * orders: [\n * { id: \"total\", title: \"Total Orders\", data: async () => ..., scorecard: { ... } },\n * ],\n * },\n * },\n * });\n * ```\n */\nexport function useInsightsPlugin(config: InsightsPluginConfig): RebasePlugin {\n const { insights, cacheTTL } = config;\n\n return React.useMemo(() => {\n const slots: SlotContribution[] = [];\n\n // ── Home page insights ────────────────────────────────────────────\n if (insights.home && insights.home.length > 0) {\n const homeInsights = insights.home;\n slots.push({\n slot: \"home.children.start\" as const,\n Component: (props: Record<string, unknown>) => (\n <HomeInsightsSlot\n {...props}\n insights={homeInsights}\n />\n ),\n order: 10\n });\n }\n\n // ── Per-collection insights ───────────────────────────────────────\n // A single `collections.<slug>` definition serves two slots:\n // 1. collection.insights → inline scorecards in the list view\n // 2. home.card.insight → compact scorecards on the home card\n if (insights.collections) {\n for (const [slug, defs] of Object.entries(insights.collections)) {\n if (defs.length === 0) continue;\n const collectionInsights = defs;\n\n // 1. Inline in collection list view\n slots.push({\n slot: \"collection.insights\" as const,\n Component: (props: Record<string, unknown>) => {\n const path = props.path as string;\n const collectionSlug = path?.split(\"/\").filter(Boolean).pop() ?? \"\";\n if (collectionSlug !== slug) return null;\n return (\n <CollectionInsightsInline\n {...props as { path: string; collection: unknown; parentCollectionSlugs: string[], parentEntityIds: string[] }}\n insights={collectionInsights}\n />\n );\n },\n order: 10\n });\n\n // 2. Auto-extract scorecards for home page card\n slots.push({\n slot: \"home.card.insight\" as const,\n Component: (props: Record<string, unknown>) => {\n const cardSlug = props.slug as string;\n if (cardSlug !== slug) return null;\n return (\n <HomeCardInsightSlot\n {...props as { slug: string; collection: unknown; context: unknown }}\n insights={collectionInsights}\n />\n );\n },\n order: 10\n });\n }\n }\n\n return {\n key: \"plugin-insights\",\n slots,\n providers: [\n {\n scope: \"root\" as const,\n Component: InsightsProvider as React.ComponentType<React.PropsWithChildren<Record<string, unknown>>>,\n props: { cacheTTL }\n }\n ]\n };\n }, [insights, cacheTTL]);\n}\n"],"mappings":"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"}