@rebasepro/plugin-insights 0.19.1 → 0.19.2-canary.g08eed46

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
@@ -201,15 +201,15 @@ function InsightsScorecardView({ config, data, title, compact = false, embedded
201
201
  if (isNegative) colorClass = "text-emerald-500";
202
202
  }
203
203
  comparisonElement = /* @__PURE__ */ jsx("span", {
204
- className: `font-medium ${compact ? "text-[10px]" : "text-xs"} ${colorClass}`,
204
+ className: `font-mono tabular-nums font-medium ${compact ? "text-[10px]" : "text-xs"} ${colorClass}`,
205
205
  children: formattedComparison
206
206
  });
207
207
  }
208
208
  }
209
209
  const isSmall = compact || containerWidth !== null && containerWidth < 200;
210
- const iconElement = config.icon ? getIcon(config.icon, "text-surface-400 dark:text-surface-500", void 0, isSmall ? 14 : 18) : null;
210
+ const iconElement = config.icon ? getIcon(config.icon, "text-text-secondary dark:text-text-secondary-dark", void 0, 14) : null;
211
211
  if (compact) return /* @__PURE__ */ jsxs("div", {
212
- className: cls("flex flex-col gap-0.5 px-2.5 py-2 rounded-md bg-transparent border min-w-0", defaultBorderMixin),
212
+ className: "flex items-baseline gap-1.5 min-w-0",
213
213
  children: [/* @__PURE__ */ jsx("span", {
214
214
  className: "text-[10px] uppercase tracking-wider text-surface-400 dark:text-surface-500 truncate",
215
215
  children: title
@@ -223,27 +223,27 @@ function InsightsScorecardView({ config, data, title, compact = false, embedded
223
223
  });
224
224
  return /* @__PURE__ */ jsxs("div", {
225
225
  ref: containerRef,
226
- className: embedded ? `flex flex-col min-w-0 h-full ${isSmall ? "px-3.5 py-3" : "px-5 py-4"}` : cls("rounded-lg flex flex-col min-w-0 bg-transparent border", defaultBorderMixin, isSmall ? "px-3.5 py-3" : "px-5 py-4"),
226
+ className: embedded ? `flex flex-col min-w-0 h-full ${isSmall ? "px-3.5 py-3" : "px-5 py-4"}` : cls("rounded-xl flex flex-col min-w-0 bg-surface-card border", defaultBorderMixin, isSmall ? "px-3.5 py-3" : "px-5 py-4"),
227
227
  style: embedded ? void 0 : fixedHeight ? { height: fixedHeight } : { minHeight: isSmall ? 68 : 92 },
228
228
  children: [
229
229
  /* @__PURE__ */ jsxs("div", {
230
- className: `flex items-center justify-between ${isSmall ? "mb-1" : "mb-2"}`,
230
+ className: `flex flex-col min-w-0 ${isSmall ? "mb-1" : "mb-2.5"}`,
231
231
  children: [/* @__PURE__ */ jsxs("div", {
232
- className: "flex flex-col min-w-0",
233
- children: [/* @__PURE__ */ jsx("span", {
234
- className: `font-medium leading-snug truncate text-surface-500 dark:text-surface-400 ${isSmall ? "text-[11px]" : "text-xs"}`,
232
+ className: "flex items-center gap-1.5 min-w-0",
233
+ children: [iconElement && /* @__PURE__ */ jsx("span", {
234
+ className: "shrink-0 flex items-center text-text-secondary dark:text-text-secondary-dark [&>svg]:size-3.5",
235
+ children: iconElement
236
+ }), /* @__PURE__ */ jsx("span", {
237
+ className: "typography-micro truncate text-surface-400 dark:text-surface-400",
235
238
  children: title
236
- }), config.dateRange && !isSmall && /* @__PURE__ */ jsx("span", {
237
- className: "text-[10px] text-surface-400 dark:text-surface-500 truncate mt-0.5",
238
- children: config.dateRange
239
239
  })]
240
- }), iconElement && /* @__PURE__ */ jsx("span", {
241
- className: "ml-2 shrink-0",
242
- children: iconElement
240
+ }), config.dateRange && !isSmall && /* @__PURE__ */ jsx("span", {
241
+ className: "font-mono tabular-nums text-[10px] text-surface-400 dark:text-surface-500 truncate mt-1",
242
+ children: config.dateRange
243
243
  })]
244
244
  }),
245
245
  /* @__PURE__ */ jsx("div", {
246
- 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"}`,
246
+ className: `font-headers font-semibold leading-tight tracking-display tabular-nums break-all text-text-primary dark:text-text-primary-dark ${isSmall ? "text-lg" : containerWidth !== null && containerWidth < 300 ? "text-xl" : "text-2xl"}`,
247
247
  children: formattedValue
248
248
  }),
249
249
  comparisonElement && /* @__PURE__ */ jsx("div", {
@@ -467,10 +467,8 @@ InsightWidget.displayName = "InsightWidget";
467
467
  */
468
468
  function HomeCardInsightSlot({ slug, insights }) {
469
469
  if (!insights || insights.length === 0) return null;
470
- const estimatedRows = Math.ceil(insights.length / 2);
471
470
  return /* @__PURE__ */ jsx("div", {
472
- className: "flex flex-wrap items-center gap-1.5 mt-2",
473
- style: { minHeight: estimatedRows * 42 + (estimatedRows - 1) * 6 },
471
+ className: "flex flex-wrap items-baseline gap-x-4 gap-y-1 mt-1.5",
474
472
  children: insights.map((def) => /* @__PURE__ */ jsx(InsightWidget, {
475
473
  definition: def,
476
474
  collectionSlug: slug,
@@ -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 };\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.widget` 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.widgets` 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 {\n RebasePlugin,\n AnySlotContribution,\n PluginGenericProps,\n CollectionWidgetsSlotProps,\n HomeCardWidgetSlotProps\n} from \"@rebasepro/cms-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.widgets` slot\n * - **Home page cards**: Compact scorecard metrics auto-extracted from collection insights via `home.card.widget` 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: AnySlotContribution[] = [];\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: PluginGenericProps) => (\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.widgets → inline scorecards in the list view\n // 2. home.card.widget → 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.widgets\" as const,\n Component: (props: CollectionWidgetsSlotProps) => {\n const collectionSlug = props.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 if (props.parentEntityIds && props.parentEntityIds.length > 0) return null;\n\n return (\n <CollectionInsightsInline\n {...props}\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.widget\" as const,\n Component: (props: HomeCardWidgetSlotProps) => {\n if (props.slug !== slug) return null;\n return (\n <HomeCardInsightSlot\n {...props}\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":";;;;;;;;;;;AAaA,IAAa,gBAAb,MAA2B;CAIH;CAHpB,wBAAgB,IAAI,IAAwB;CAC5C,2BAAmB,IAAI,IAAwC;CAE/D,YAAY,MAAc,KAAQ;EAAd,KAAA,MAAA;CAAe;CAEnC,IAAI,KAAuC;EACvC,MAAM,QAAQ,KAAK,MAAM,IAAI,GAAG;EAChC,IAAI,CAAC,OAAO,OAAO;EACnB,IAAI,KAAK,IAAI,IAAI,MAAM,YAAY,KAAK,KAAK;GACzC,KAAK,MAAM,OAAO,GAAG;GACrB,OAAO;EACX;EACA,OAAO,MAAM;CACjB;CAEA,IAAI,KAAa,MAA+B;EAC5C,KAAK,MAAM,IAAI,KAAK;GAAE;GAC9B,WAAW,KAAK,IAAI;EAAE,CAAC;EACf,KAAK,SAAS,OAAO,GAAG;CAC5B;CAEA,YAAY,KAAgD;EACxD,OAAO,KAAK,SAAS,IAAI,GAAG,KAAK;CACrC;CAEA,YAAY,KAAa,SAA2C;EAChE,KAAK,SAAS,IAAI,KAAK,OAAO;CAClC;CAEA,WAAW,KAAoB;EAC3B,IAAI,KAAK;GACL,KAAK,MAAM,OAAO,GAAG;GACrB,KAAK,SAAS,OAAO,GAAG;EAC5B,OAAO;GACH,KAAK,MAAM,MAAM;GACjB,KAAK,SAAS,MAAM;EACxB;CACJ;AACJ;;;AC7CA,IAAM,kBAAkB,cAA2C,IAAI;;;;;;;;AASvE,SAAgB,iBAAiB,EAC7B,UACA,YACyC;CACzC,MAAM,QAAQ,cAAc,IAAI,cAAc,QAAQ,GAAG,CAAC,QAAQ,CAAC;CACnE,MAAM,QAAQ,eAAe,EAAE,MAAM,IAAI,CAAC,KAAK,CAAC;CAEhD,OACI,oBAAC,gBAAgB,UAAjB;EAAiC;EAC5B;CACqB,CAAA;AAElC;;;;;;AAOA,SAAgB,oBAAiD;CAC7D,OAAO,WAAW,eAAe;AACrC;;;;;;;;;;;;;;ACrBA,SAAgB,gBACZ,YACA,SAKF;CAEE,MAAM,QADS,kBACD,CAAA,EAAQ,SAAS;CAC/B,MAAM,EAAE,gBAAgB,aAAa,MAAM,iBAAiB,kBAAkB;CAC9E,MAAM,YAAY,CAAC,kBAAkB,CAAC,gBAAgB,QAAQ,IAAI,KAAK;CACvE,MAAM,CAAC,MAAM,WAAW,SAAmC,IAAI;CAC/D,MAAM,CAAC,SAAS,cAAc,SAAS,IAAI;CAC3C,MAAM,CAAC,OAAO,YAAY,SAAuB,IAAI;CAErD,MAAM,WAAW,GAAG,WAAW,GAAG,GAAG,QAAQ,QAAQ,QAAQ,kBAAkB;CAE/E,gBAAgB;EAEZ,IAAI,CAAC,aAAa,CAAC,OACf;EAGJ,IAAI,YAAY;EAGhB,MAAM,SAAS,MAAM,IAAI,QAAQ;EACjC,IAAI,QAAQ;GACR,QAAQ,MAAM;GACd,WAAW,KAAK;GAChB;EACJ;EAGA,MAAM,WAAW,MAAM,YAAY,QAAQ;EAC3C,IAAI,UAAU;GACV,WAAW,IAAI;GACf,SACK,MAAM,WAAW;IACd,IAAI,CAAC,WACD,QAAQ,MAAM;GAEtB,CAAC,CAAC,CACD,OAAO,QAAQ;IACZ,IAAI,CAAC,WAAW,SAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;GAChF,CAAC,CAAC,CACD,cAAc;IACX,IAAI,CAAC,WAAW,WAAW,KAAK;GACpC,CAAC;GACL;EACJ;EAGA,WAAW,IAAI;EACf,SAAS,IAAI;EAEb,MAAM,UAAU,WAAW,KAAK,OAAO;EAEvC,MAAM,YAAY,UAAU,OAAO;EAEnC,QACK,MAAM,WAAW;GACd,MAAM,IAAI,UAAU,MAAM;GAC1B,IAAI,CAAC,WACD,QAAQ,MAAM;EAEtB,CAAC,CAAC,CACD,OAAO,QAAQ;GACZ,MAAM,WAAW,QAAQ;GACzB,IAAI,CAAC,WACD,SAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;EAEpE,CAAC,CAAC,CACD,cAAc;GACX,IAAI,CAAC,WAAW,WAAW,KAAK;EACpC,CAAC;EAEL,aAAa;GACT,YAAY;EAChB;CACJ,GAAG;EAAC,WAAW;EAAI,WAAW;EAAM,QAAQ;EAAM,QAAQ;EAAgB;EAAU;EAAO;CAAS,CAAC;CAErG,OAAO;EAAE;EACb;EACA;CAAM;AACN;;;ACjGA,SAAS,aAAa,OAAe,QAAkC;CACnE,IAAI,UAAU,QAAQ,UAAU,KAAA,GAAW,OAAO;CAElD,MAAM,UAAoC;EACtC,OAAO,QAAQ,SAAS;EACxB,UAAU,QAAQ,YAAY;CAClC;CAKA,IAAI,QAAQ,aAAa,KAAA,GAAW;EAChC,QAAQ,wBAAwB,OAAO;EACvC,QAAQ,wBAAwB,OAAO;CAC3C;CAEA,IAAI,QAAQ,UAAU,YAClB,QAAQ,WAAW,OAAO,YAAY;CAG1C,IAAI,YAAY,IAAI,KAAK,aAAa,SAAS,OAAO,CAAC,CAAC,OAAO,KAAK;CAEpE,IAAI,QAAQ,YAAY,QAAQ,GAC5B,YAAY,MAAM;CAGtB,OAAO;AACX;;;;;;;;AASA,SAAgB,sBAAsB,EAClC,QACA,MACA,OACA,UAAU,OACV,WAAW,OACX,eAUD;CACC,MAAM,eAAe,OAAuB,IAAI;CAChD,MAAM,CAAC,gBAAgB,qBAAqB,SAAwB,IAAI;CAExE,MAAM,sBAAsB;EACxB,IAAI,CAAC,aAAa,SAAS;EAE3B,kBAAkB,aAAa,QAAQ,WAAW;EAClD,MAAM,WAAW,IAAI,gBAAgB,YAAY;GAC7C,KAAK,MAAM,SAAS,SAChB,kBAAkB,MAAM,YAAY,KAAK;EAEjD,CAAC;EACD,SAAS,QAAQ,aAAa,OAAO;EACrC,aAAa,SAAS,WAAW;CACrC,GAAG,CAAC,CAAC;CAEL,MAAM,YAAY,KAAK,OAAO,MAAM;CACpC,MAAM,iBAAiB,OAAO,cAAc,WACtC,aAAa,WAAW,OAAO,MAAM,MAAM,IAC3C,OAAO,aAAa,KAAK;CAG/B,IAAI,oBAAqC;CACzC,IAAI,OAAO,YAAY;EACnB,MAAM,kBAAkB,KAAK,OAAO,WAAW;EAC/C,IAAI,OAAO,oBAAoB,UAAU;GACrC,MAAM,sBAAsB,aAAa,iBAAiB,OAAO,WAAW,MAAM;GAClF,MAAM,aAAa,kBAAkB;GACrC,MAAM,aAAa,kBAAkB;GAErC,IAAI,aAAa;GACjB,IAAI,OAAO,WAAW,WAAW,oBAAoB;IACjD,IAAI,YAAY,aAAa;IAC7B,IAAI,YAAY,aAAa;GACjC,OAAO,IAAI,OAAO,WAAW,WAAW,oBAAoB;IACxD,IAAI,YAAY,aAAa;IAC7B,IAAI,YAAY,aAAa;GACjC;GAEA,oBACI,oBAAC,QAAD;IAAM,WAAW,eAAe,UAAU,gBAAgB,UAAU,GAAG;cAClE;GACC,CAAA;EAEd;CACJ;CAEA,MAAM,UAAU,WAAY,mBAAmB,QAAQ,iBAAiB;CAGxE,MAAM,cAAc,OAAO,OACrB,QAAQ,OAAO,MAAM,0CAA0C,KAAA,GAAW,UAAU,KAAK,EAAE,IAC3F;CAGN,IAAI,SACA,OACI,qBAAC,OAAD;EAAK,WAAW,IAAI,8EAA8E,kBAAkB;YAApH,CACI,oBAAC,QAAD;GAAM,WAAU;aACX;EACC,CAAA,GACN,qBAAC,OAAD;GAAK,WAAU;aAAf,CACI,oBAAC,QAAD;IAAM,WAAU;cACX;GACC,CAAA,GACL,iBACA;IACJ;;CASb,OACI,qBAAC,OAAD;EAAK,KAAK;EAAc,WALV,WACZ,gCAAgC,UAAU,gBAAgB,gBAC1D,IAAI,0DAA0D,oBAAoB,UAAU,gBAAgB,WAAW;EAG3E,OAAO,WAAW,KAAA,IAAY,cAAc,EAAE,QAAQ,YAAY,IAAI,EAAE,WAAW,UAAU,KAAK,GAAG;YAAnJ;GAEI,qBAAC,OAAD;IAAK,WAAW,qCAAqC,UAAU,SAAS;cAAxE,CACI,qBAAC,OAAD;KAAK,WAAU;eAAf,CACI,oBAAC,QAAD;MAAM,WAAW,4EAA4E,UAAU,gBAAgB;gBAClH;KACC,CAAA,GACL,OAAO,aAAa,CAAC,WAClB,oBAAC,QAAD;MAAM,WAAU;gBACX,OAAO;KACN,CAAA,CAET;QACJ,eACG,oBAAC,QAAD;KAAM,WAAU;eAAiB;IAAkB,CAAA,CAEtD;;GAGL,oBAAC,OAAD;IAAK,WAAW,+FAA+F,UAAU,YAAa,mBAAmB,QAAQ,iBAAiB,MAAO,YAAY;cAChM;GACA,CAAA;GAGJ,qBACG,oBAAC,OAAD;IAAK,WAAW,UAAU,WAAW;cAChC;GACA,CAAA;EAER;;AAEb;AAEA,sBAAsB,cAAc;;;;;;;;;;;;;;;;;;ACrJpC,SAAgB,sBAAsB,EAClC,QACA,UAAU,OACV,WAAW,OACX,eASD;CACC,MAAM,gBAAgB,QAAQ,OAAO,UAAU;CAC/C,MAAM,UAAU,QAAQ,OAAO,IAAI;CACnC,MAAM,eAAe,QAAQ,OAAO,SAAS;CAQ7C,IAAI,SACA,OACI,qBAAC,OAAD;EACI,WAAW,IACP,iBACA,WACM,uBACA,8EACN,CAAC,YAAY,kBACjB;YAPJ,CAUI,oBAAC,OAAD;GAAK,WAAU;GACX,OAAO;IAAE,QAAQ;IACrC,OAAO;GAAG;EACO,CAAA,GAED,qBAAC,OAAD;GAAK,WAAU;aAAf,CACI,oBAAC,OAAD;IAAK,WAAU;IACX,OAAO;KAAE,QAAQ;KACzC,OAAO;IAAG;GACW,CAAA,GACA,iBACG,oBAAC,OAAD;IAAK,WAAU;IACX,OAAO;KAAE,QAAQ;KAC7C,OAAO;IAAG;GACe,CAAA,CAEJ;IACJ;;CAKb,OAAO,oBAAC,kBAAD;EACY;EACN;EACK;EACJ;EACG;CAChB,CAAA;AACL;;;;;;;;;;;;AAgCA,SAAS,iBAAiB,EACtB,eACA,SACA,cACA,UACA,eAOD;CACC,MAAM,eAAe,OAAuB,IAAI;CAChD,MAAM,CAAC,gBAAgB,qBAAqB,SAAwB,IAAI;CAExE,MAAM,sBAAsB;EACxB,IAAI,CAAC,aAAa,SAAS;EAC3B,kBAAkB,aAAa,QAAQ,WAAW;EAClD,MAAM,WAAW,IAAI,gBAAgB,YAAY;GAC7C,KAAK,MAAM,SAAS,SAChB,kBAAkB,MAAM,YAAY,KAAK;EAEjD,CAAC;EACD,SAAS,QAAQ,aAAa,OAAO;EACrC,aAAa,SAAS,WAAW;CACrC,GAAG,CAAC,CAAC;CAGL,MAAM,UAAU,mBAAmB,QAAQ,iBAAiB;CAI5D,MAAM,cAAc,UAAU,KAAK;CAEnC,MAAM,cAAc,UACd,OACC,mBAAmB,QAAQ,iBAAiB,MACzC,KACA;CAEV,MAAM,mBAAmB;CAEzB,MAAM,WAAW,UAAU,KAAK;CAMhC,OACI,qBAAC,OAAD;EACI,KAAK;EACL,WAAW,IAAI,iBAPL,WACZ,gCAAgC,UAAU,gBAAgB,gBAC1D,IAAI,0DAA0D,oBAAoB,UAAU,gBAAgB,WAAW,CAK5E;EACzC,OAAO,WAAW,KAAA,IAAY,cAAc,EAAE,QAAQ,YAAY,IAAI,EAAE,WAAW,UAAU,KAAK,GAAG;YAHzG;GAMI,qBAAC,OAAD;IAAK,WAAW,qCAAqC,UAAU,SAAS;cAAxE,CACI,qBAAC,OAAD;KAAK,WAAU;eAAf,CAEI,oBAAC,OAAD;MAAK,WAAU;MACX,OAAO;OAAE,QAAQ;OACzC,OAAO;MAAM;KACQ,CAAA,GAEA,gBAAgB,CAAC,WACd,oBAAC,OAAD;MAAK,WAAU;MACX,OAAO;OAAE,QAAQ;OAC7C,OAAO;MAAM;KACY,CAAA,CAEJ;QAEJ,WACG,oBAAC,QAAD;KAAM,WAAU;eACZ,oBAAC,OAAD;MAAK,WAAU;MACX,OAAO;OAAE,QAAQ;OAC7C,OAAO;MAAS;KACS,CAAA;IACC,CAAA,CAET;;GAGL,oBAAC,OAAD;IAAK,WAAU;IACX,OAAO;KAAE,QAAQ;KACjC,OAAO;IAAM;GACA,CAAA;GAGA,iBACG,oBAAC,OAAD;IAAK,WAAW,UAAU,WAAW;cACjC,oBAAC,OAAD;KAAK,WAAU;KACX,OAAO;MAAE,QAAQ;MACzC,OAAO;KAAM;IACQ,CAAA;GACA,CAAA;EAER;;AAEb;AAEA,sBAAsB,cAAc;;;;;;;;;;;;;;;;;;ACpMpC,SAAS,mBAAmB,QAAiC;CACzD,IAAI,IAAI;CACR,IAAI,OAAO,WAAW,KAAK;CAC3B,IAAI,OAAO,YAAY,KAAK;CAC5B,OAAO,KAAK,KAAK,CAAC;AACtB;;;;;;;;;AAUA,SAAgB,cAAc,EAC1B,YACA,gBACA,MACA,uBAAuB,iBACvB,UAAU,OACV,WAAW,SASZ;CACC,MAAM,EAAE,MAAM,SAAS,UAAU,gBAAgB,YAAY;EAAE;EACnE;EACA;CAAsB,CAAC;CAInB,MAAM,cAAe,CAAC,WAAW,CAAC,WAAY,mBAAmB,WAAW,SAAS,IAAI,KAAA;CAEzF,IAAI,SACA,OAAO,oBAAC,uBAAD;EAAuB,QAAQ,WAAW;EAAoB;EAAmB;EAAuB;CAAc,CAAA;CAGjI,IAAI,OACA,OACI,qBAAC,OAAD;EACI,WAAW,yDAAyD,WAAW,qBAAqB,6FAA6F,UAAU,gBAAgB;EAC3N,OAAO,cAAc,EAAE,QAAQ,YAAY,IAAI,KAAA;YAFnD,CAII,oBAAC,OAAD;GAAK,WAAU;aAAsB,WAAW;EAAW,CAAA,GAC3D,oBAAC,OAAD,EAAA,UAAM,MAAM,QAAa,CAAA,CACxB;;CAIb,IAAI,CAAC,QAAQ,KAAK,KAAK,WAAW,GAC9B,OACI,qBAAC,OAAD;EACI,WAAW,2DAA2D,WAAW,qBAAqB,mGAAmG,UAAU,gBAAgB;EACnO,OAAO,cAAc,EAAE,QAAQ,YAAY,IAAI,KAAA;YAFnD,CAIK,WAAW,OAAM,YACjB;;CAIb,OACI,oBAAC,uBAAD;EACI,QAAQ,WAAW;EACnB,MAAM,KAAK,KAAK;EAChB,OAAO,WAAW;EACT;EACC;EACG;CAChB,CAAA;AAET;AAEA,cAAc,cAAc;;;;;;;;;ACzF5B,SAAgB,oBAAoB,EAChC,MACA,YAMD;CACC,IAAI,CAAC,YAAY,SAAS,WAAW,GAAG,OAAO;CAG/C,MAAM,gBAAgB,KAAK,KAAK,SAAS,SAAS,CAAC;CAGnD,OACI,oBAAC,OAAD;EAAK,WAAU;EAA2C,OAAO,EAAE,WAHrD,gBAAgB,MAAM,gBAAgB,KAAK,EAGoB;YACxE,SAAS,KAAK,QACX,oBAAC,eAAD;GAEI,YAAY;GACZ,gBAAgB;GAChB,SAAS;EACZ,GAJQ,IAAI,EAIZ,CACJ;CACA,CAAA;AAEb;AAEA,oBAAoB,cAAc;;;;;;;;;AC7BlC,SAAgB,iBAAiB,EAC7B,YAGD;CACC,IAAI,CAAC,YAAY,SAAS,WAAW,GAAG,OAAO;CAE/C,OACI,oBAAC,OAAD;EACI,WAAU;EACV,OAAO,EAAE,WAAW,GAAG;YAEtB,SAAS,KAAK,QACX,oBAAC,eAAD,EAA4B,YAAY,IAAM,GAA1B,IAAI,EAAsB,CACjD;CACA,CAAA;AAEb;AAEA,iBAAiB,cAAc;;;;;;;;;ACnB/B,SAAgB,yBAAyB,EACrC,UACA,MACA,uBACA,mBAMD;CACC,IAAI,CAAC,YAAY,SAAS,WAAW,GAAG,OAAO;CAE/C,OACI,oBAAC,OAAD;EAAK,WAAU;YACV,SAAS,KAAK,QACX,oBAAC,eAAD;GAEI,YAAY;GACN;GACiB;GAAwC;EAClE,GAJQ,IAAI,EAIZ,CACJ;CACA,CAAA;AAEb;AAEA,yBAAyB,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACWvC,SAAgB,kBAAkB,QAA4C;CAC1E,MAAM,EAAE,UAAU,aAAa;CAE/B,OAAO,MAAM,cAAc;EACvB,MAAM,QAA+B,CAAC;EAGtC,IAAI,SAAS,QAAQ,SAAS,KAAK,SAAS,GAAG;GAC3C,MAAM,eAAe,SAAS;GAC9B,MAAM,KAAK;IACP,MAAM;IACN,YAAY,UACR,oBAAC,kBAAD;KACI,GAAI;KACJ,UAAU;IACb,CAAA;IAEL,OAAO;GACX,CAAC;EACL;EAMA,IAAI,SAAS,aACT,KAAK,MAAM,CAAC,MAAM,SAAS,OAAO,QAAQ,SAAS,WAAW,GAAG;GAC7D,IAAI,KAAK,WAAW,GAAG;GACvB,MAAM,qBAAqB;GAG3B,MAAM,KAAK;IACP,MAAM;IACN,YAAY,UAAsC;KAE9C,KADuB,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,IAAI,KAAK,QAChD,MAAM,OAAO;KAOpC,IAAI,MAAM,mBAAmB,MAAM,gBAAgB,SAAS,GAAG,OAAO;KAEtE,OACI,oBAAC,0BAAD;MACI,GAAI;MACJ,UAAU;KACb,CAAA;IAET;IACA,OAAO;GACX,CAAC;GAGD,MAAM,KAAK;IACP,MAAM;IACN,YAAY,UAAmC;KAC3C,IAAI,MAAM,SAAS,MAAM,OAAO;KAChC,OACI,oBAAC,qBAAD;MACI,GAAI;MACJ,UAAU;KACb,CAAA;IAET;IACA,OAAO;GACX,CAAC;EACL;EAGJ,OAAO;GACH,KAAK;GACL;GACA,WAAW,CACP;IACI,OAAO;IACP,WAAW;IACX,OAAO,EAAE,SAAS;GACtB,CACJ;EACJ;CACJ,GAAG,CAAC,UAAU,QAAQ,CAAC;AAC3B"}
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-mono tabular-nums 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 // 14px in the secondary tier, beside the label — the card header grammar.\n const iconElement = config.icon\n ? getIcon(config.icon, \"text-text-secondary dark:text-text-secondary-dark\", undefined, 14)\n : null;\n\n // ── Compact card-inline layout ──────────────────────────────────────\n if (compact) {\n return (\n <div className=\"flex items-baseline gap-1.5 min-w-0\">\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 // A card on the sheet: one step up and a hairline, like every other card.\n // It was transparent with a border, a box drawn on the sheet rather\n // than an object sitting on it.\n : cls(\"rounded-xl flex flex-col min-w-0 bg-surface-card 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 — the card header grammar the reference page documents:\n a small icon in the secondary tier, then the label in the micro\n tier, on ONE line. The icon used to sit alone at the far right,\n which made every tile read as two unrelated corners. */}\n <div className={`flex flex-col min-w-0 ${isSmall ? \"mb-1\" : \"mb-2.5\"}`}>\n <div className=\"flex items-center gap-1.5 min-w-0\">\n {iconElement && (\n <span className=\"shrink-0 flex items-center text-text-secondary dark:text-text-secondary-dark [&>svg]:size-3.5\">{iconElement}</span>\n )}\n <span className=\"typography-micro truncate text-surface-400 dark:text-surface-400\">\n {title}\n </span>\n </div>\n {config.dateRange && !isSmall && (\n <span className=\"font-mono tabular-nums text-[10px] text-surface-400 dark:text-surface-500 truncate mt-1\">\n {config.dateRange}\n </span>\n )}\n </div>\n\n {/* Main value */}\n <div className={`font-headers font-semibold leading-tight tracking-display tabular-nums break-all text-text-primary dark:text-text-primary-dark ${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.widget` 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 // Text readouts, not tiles: they wrap at their own height. The old\n // estimated minHeight (42px a row) is what kept every card on the home\n // page tall and empty.\n return (\n <div className=\"flex flex-wrap items-baseline gap-x-4 gap-y-1 mt-1.5\">\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.widgets` 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 {\n RebasePlugin,\n AnySlotContribution,\n PluginGenericProps,\n CollectionWidgetsSlotProps,\n HomeCardWidgetSlotProps\n} from \"@rebasepro/cms-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.widgets` slot\n * - **Home page cards**: Compact scorecard metrics auto-extracted from collection insights via `home.card.widget` 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: AnySlotContribution[] = [];\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: PluginGenericProps) => (\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.widgets → inline scorecards in the list view\n // 2. home.card.widget → 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.widgets\" as const,\n Component: (props: CollectionWidgetsSlotProps) => {\n const collectionSlug = props.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 if (props.parentEntityIds && props.parentEntityIds.length > 0) return null;\n\n return (\n <CollectionInsightsInline\n {...props}\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.widget\" as const,\n Component: (props: HomeCardWidgetSlotProps) => {\n if (props.slug !== slug) return null;\n return (\n <HomeCardInsightSlot\n {...props}\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,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebasepro/plugin-insights",
3
- "version": "0.19.1",
3
+ "version": "0.19.2-canary.g08eed46",
4
4
  "description": "Insights plugin for the Rebase admin panel: charts and saved queries over your collections.",
5
5
  "keywords": [
6
6
  "rebase",
@@ -36,10 +36,10 @@
36
36
  "./package.json": "./package.json"
37
37
  },
38
38
  "dependencies": {
39
- "@rebasepro/cms-types": "0.19.1",
40
- "@rebasepro/app": "0.19.1",
41
- "@rebasepro/types": "0.19.1",
42
- "@rebasepro/ui": "0.19.1"
39
+ "@rebasepro/cms-types": "0.19.2-canary.g08eed46",
40
+ "@rebasepro/app": "0.19.2-canary.g08eed46",
41
+ "@rebasepro/types": "0.19.2-canary.g08eed46",
42
+ "@rebasepro/ui": "0.19.2-canary.g08eed46"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "react": "^19.2.7",