@burdenoff/microfe-bigconsole 2026.731.8 → 2026.801.1

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.
@@ -10,7 +10,7 @@ var n = class extends e {
10
10
  render() {
11
11
  return this.state.hasError ? /* @__PURE__ */ t("div", {
12
12
  role: "alert",
13
- className: "flex h-full w-full items-center justify-center bg-surface p-4 text-center text-sm text-secondary",
13
+ className: "flex h-full w-full items-center justify-center bg-bg-surface p-4 text-center text-sm text-text-secondary",
14
14
  children: [
15
15
  "This ",
16
16
  this.props.label ?? "content",
@@ -1 +1 @@
1
- {"version":3,"file":"EmbedErrorBoundary.js","names":[],"sources":["../../../../src/bigconsole/components/embed/EmbedErrorBoundary.tsx"],"sourcesContent":["/**\n * Customer-facing error boundary for embedded widgets/dashboards (BOFF-2986).\n *\n * Renders a neutral, safe message and NEVER surfaces internal details (stack\n * traces, ids, endpoints, config). One failing widget must not tear down the\n * whole dashboard, so this wraps each widget individually as well as the page.\n */\n\nimport { Component, type ErrorInfo, type ReactNode } from 'react';\n\ninterface Props {\n readonly children: ReactNode;\n /** Short, non-sensitive label for what failed (e.g. \"widget\"). */\n readonly label?: string;\n}\n\ninterface State {\n readonly hasError: boolean;\n}\n\nexport class EmbedErrorBoundary extends Component<Props, State> {\n state: State = { hasError: false };\n\n static getDerivedStateFromError(): State {\n return { hasError: true };\n }\n\n componentDidCatch(_error: Error, _info: ErrorInfo): void {\n // Intentionally no external reporting from the anonymous embed context — the\n // safe render below is the only user-visible outcome.\n }\n\n render(): ReactNode {\n if (this.state.hasError) {\n return (\n <div\n role=\"alert\"\n className=\"flex h-full w-full items-center justify-center bg-surface p-4 text-center text-sm text-secondary\"\n >\n This {this.props.label ?? 'content'} could not be displayed.\n </div>\n );\n }\n return this.props.children;\n }\n}\n"],"mappings":";;;AAoBA,IAAa,IAAb,cAAwC,EAAwB;CAC9D,QAAe,EAAE,UAAU,IAAO;CAElC,OAAO,2BAAkC;AACvC,SAAO,EAAE,UAAU,IAAM;;CAG3B,kBAAkB,GAAe,GAAwB;CAKzD,SAAoB;AAWlB,SAVI,KAAK,MAAM,WAEX,kBAAC,OAAD;GACE,MAAK;GACL,WAAU;aAFZ;IAGC;IACO,KAAK,MAAM,SAAS;IAAU;IAChC;OAGH,KAAK,MAAM"}
1
+ {"version":3,"file":"EmbedErrorBoundary.js","names":[],"sources":["../../../../src/bigconsole/components/embed/EmbedErrorBoundary.tsx"],"sourcesContent":["/**\n * Customer-facing error boundary for embedded widgets/dashboards (BOFF-2986).\n *\n * Renders a neutral, safe message and NEVER surfaces internal details (stack\n * traces, ids, endpoints, config). One failing widget must not tear down the\n * whole dashboard, so this wraps each widget individually as well as the page.\n */\n\nimport { Component, type ErrorInfo, type ReactNode } from 'react';\n\ninterface Props {\n readonly children: ReactNode;\n /** Short, non-sensitive label for what failed (e.g. \"widget\"). */\n readonly label?: string;\n}\n\ninterface State {\n readonly hasError: boolean;\n}\n\nexport class EmbedErrorBoundary extends Component<Props, State> {\n state: State = { hasError: false };\n\n static getDerivedStateFromError(): State {\n return { hasError: true };\n }\n\n componentDidCatch(_error: Error, _info: ErrorInfo): void {\n // Intentionally no external reporting from the anonymous embed context — the\n // safe render below is the only user-visible outcome.\n }\n\n render(): ReactNode {\n if (this.state.hasError) {\n return (\n <div\n role=\"alert\"\n className=\"flex h-full w-full items-center justify-center bg-bg-surface p-4 text-center text-sm text-text-secondary\"\n >\n This {this.props.label ?? 'content'} could not be displayed.\n </div>\n );\n }\n return this.props.children;\n }\n}\n"],"mappings":";;;AAoBA,IAAa,IAAb,cAAwC,EAAwB;CAC9D,QAAe,EAAE,UAAU,IAAO;CAElC,OAAO,2BAAkC;AACvC,SAAO,EAAE,UAAU,IAAM;;CAG3B,kBAAkB,GAAe,GAAwB;CAKzD,SAAoB;AAWlB,SAVI,KAAK,MAAM,WAEX,kBAAC,OAAD;GACE,MAAK;GACL,WAAU;aAFZ;IAGC;IACO,KAAK,MAAM,SAAS;IAAU;IAChC;OAGH,KAAK,MAAM"}
@@ -19,7 +19,7 @@ function l() {
19
19
  }
20
20
  function u({ title: e }) {
21
21
  return /* @__PURE__ */ s("div", {
22
- className: "flex h-full w-full items-center justify-center bg-surface p-4 text-center text-sm text-secondary",
22
+ className: "flex h-full w-full items-center justify-center bg-bg-surface p-4 text-center text-sm text-text-tertiary",
23
23
  children: e ? `“${e}” is not available in this embed.` : "This widget is not available in this embed."
24
24
  });
25
25
  }
@@ -1 +1 @@
1
- {"version":3,"file":"EmbedWidgetRenderer.js","names":[],"sources":["../../../../src/bigconsole/components/embed/EmbedWidgetRenderer.tsx"],"sourcesContent":["/**\n * Strict embed widget dispatcher (BOFF-2986).\n *\n * Maps a sanitized widget DTO to its embed renderer via an EXACT registry.\n * There is NO silent fallback (unlike the authenticated WidgetRendererFactory):\n * a type that is not `implemented` in the coverage manifest renders a neutral\n * \"not available\" placeholder, never a guessed or partial widget. Each widget is\n * wrapped in its own error boundary so one failure can't take down the page.\n */\n\nimport type { ComponentType } from 'react';\nimport type { WidgetType } from '../../types';\nimport type { EmbedWidgetRenderProps } from './types';\nimport { EMBED_WIDGET_COVERAGE } from './renderer-manifest';\nimport { EmbedErrorBoundary } from './EmbedErrorBoundary';\nimport { MetricCardEmbed } from './widgets/MetricCardEmbed';\nimport { KpiComparisonEmbed } from './widgets/KpiComparisonEmbed';\nimport { TableEmbed } from './widgets/TableEmbed';\nimport { ListEmbed } from './widgets/ListEmbed';\nimport { TextEmbed } from './widgets/TextEmbed';\n\n/**\n * Registry of implemented renderers. Every key here MUST be marked\n * `implemented` in EMBED_WIDGET_COVERAGE, and vice-versa — the completeness test\n * asserts the two stay in lockstep, so a renderer can never be silently missing\n * for a type the manifest claims to support.\n */\nconst IMPLEMENTED_RENDERERS: Partial<Record<WidgetType, ComponentType<EmbedWidgetRenderProps>>> = {\n metric_card: MetricCardEmbed,\n kpi_card_comparison: KpiComparisonEmbed,\n table: TableEmbed,\n list: ListEmbed,\n text: TextEmbed,\n};\n\nexport function getImplementedRendererTypes(): WidgetType[] {\n return Object.keys(IMPLEMENTED_RENDERERS) as WidgetType[];\n}\n\nfunction UnavailableWidget({ title }: { title?: string }) {\n return (\n <div className=\"flex h-full w-full items-center justify-center bg-surface p-4 text-center text-sm text-secondary\">\n {title ? `“${title}” is not available in this embed.` : 'This widget is not available in this embed.'}\n </div>\n );\n}\n\nexport function EmbedWidgetRenderer(props: EmbedWidgetRenderProps) {\n const { widget } = props;\n const coverage = EMBED_WIDGET_COVERAGE[widget.type];\n const Renderer = coverage?.support === 'implemented' ? IMPLEMENTED_RENDERERS[widget.type] : undefined;\n\n if (!Renderer) {\n return <UnavailableWidget title={widget.title} />;\n }\n\n return (\n <EmbedErrorBoundary label=\"widget\">\n <Renderer {...props} />\n </EmbedErrorBoundary>\n );\n}\n"],"mappings":";;;;;;;;;AA2BA,IAAM,IAA4F;CAChG,aAAa;CACb,qBAAqB;CACrB,OAAO;CACP,MAAM;CACN,MAAM;CACP;AAED,SAAgB,IAA4C;AAC1D,QAAO,OAAO,KAAK,EAAsB;;AAG3C,SAAS,EAAkB,EAAE,YAA6B;AACxD,QACE,kBAAC,OAAD;EAAK,WAAU;YACZ,IAAQ,IAAI,EAAM,qCAAqC;EACpD,CAAA;;AAIV,SAAgB,EAAoB,GAA+B;CACjE,IAAM,EAAE,cAAW,GAEb,IADW,EAAsB,EAAO,OACnB,YAAY,gBAAgB,EAAsB,EAAO,QAAQ,KAAA;AAM5F,QAJK,IAKH,kBAAC,GAAD;EAAoB,OAAM;YACxB,kBAAC,GAAD,EAAU,GAAI,GAAS,CAAA;EACJ,CAAA,GANd,kBAAC,GAAD,EAAmB,OAAO,EAAO,OAAS,CAAA"}
1
+ {"version":3,"file":"EmbedWidgetRenderer.js","names":[],"sources":["../../../../src/bigconsole/components/embed/EmbedWidgetRenderer.tsx"],"sourcesContent":["/**\n * Strict embed widget dispatcher (BOFF-2986).\n *\n * Maps a sanitized widget DTO to its embed renderer via an EXACT registry.\n * There is NO silent fallback (unlike the authenticated WidgetRendererFactory):\n * a type that is not `implemented` in the coverage manifest renders a neutral\n * \"not available\" placeholder, never a guessed or partial widget. Each widget is\n * wrapped in its own error boundary so one failure can't take down the page.\n */\n\nimport type { ComponentType } from 'react';\nimport type { WidgetType } from '../../types';\nimport type { EmbedWidgetRenderProps } from './types';\nimport { EMBED_WIDGET_COVERAGE } from './renderer-manifest';\nimport { EmbedErrorBoundary } from './EmbedErrorBoundary';\nimport { MetricCardEmbed } from './widgets/MetricCardEmbed';\nimport { KpiComparisonEmbed } from './widgets/KpiComparisonEmbed';\nimport { TableEmbed } from './widgets/TableEmbed';\nimport { ListEmbed } from './widgets/ListEmbed';\nimport { TextEmbed } from './widgets/TextEmbed';\n\n/**\n * Registry of implemented renderers. Every key here MUST be marked\n * `implemented` in EMBED_WIDGET_COVERAGE, and vice-versa — the completeness test\n * asserts the two stay in lockstep, so a renderer can never be silently missing\n * for a type the manifest claims to support.\n */\nconst IMPLEMENTED_RENDERERS: Partial<Record<WidgetType, ComponentType<EmbedWidgetRenderProps>>> = {\n metric_card: MetricCardEmbed,\n kpi_card_comparison: KpiComparisonEmbed,\n table: TableEmbed,\n list: ListEmbed,\n text: TextEmbed,\n};\n\nexport function getImplementedRendererTypes(): WidgetType[] {\n return Object.keys(IMPLEMENTED_RENDERERS) as WidgetType[];\n}\n\nfunction UnavailableWidget({ title }: { title?: string }) {\n return (\n <div className=\"flex h-full w-full items-center justify-center bg-bg-surface p-4 text-center text-sm text-text-tertiary\">\n {title ? `“${title}” is not available in this embed.` : 'This widget is not available in this embed.'}\n </div>\n );\n}\n\nexport function EmbedWidgetRenderer(props: EmbedWidgetRenderProps) {\n const { widget } = props;\n const coverage = EMBED_WIDGET_COVERAGE[widget.type];\n const Renderer = coverage?.support === 'implemented' ? IMPLEMENTED_RENDERERS[widget.type] : undefined;\n\n if (!Renderer) {\n return <UnavailableWidget title={widget.title} />;\n }\n\n return (\n <EmbedErrorBoundary label=\"widget\">\n <Renderer {...props} />\n </EmbedErrorBoundary>\n );\n}\n"],"mappings":";;;;;;;;;AA2BA,IAAM,IAA4F;CAChG,aAAa;CACb,qBAAqB;CACrB,OAAO;CACP,MAAM;CACN,MAAM;CACP;AAED,SAAgB,IAA4C;AAC1D,QAAO,OAAO,KAAK,EAAsB;;AAG3C,SAAS,EAAkB,EAAE,YAA6B;AACxD,QACE,kBAAC,OAAD;EAAK,WAAU;YACZ,IAAQ,IAAI,EAAM,qCAAqC;EACpD,CAAA;;AAIV,SAAgB,EAAoB,GAA+B;CACjE,IAAM,EAAE,cAAW,GAEb,IADW,EAAsB,EAAO,OACnB,YAAY,gBAAgB,EAAsB,EAAO,QAAQ,KAAA;AAM5F,QAJK,IAKH,kBAAC,GAAD;EAAoB,OAAM;YACxB,kBAAC,GAAD,EAAU,GAAI,GAAS,CAAA;EACJ,CAAA,GANd,kBAAC,GAAD,EAAmB,OAAO,EAAO,OAAS,CAAA"}
@@ -6,16 +6,16 @@ import { jsx as r, jsxs as i } from "react/jsx-runtime";
6
6
  var a = 12;
7
7
  function o({ page: e, capabilities: n }) {
8
8
  return e.widgets.length === 0 ? /* @__PURE__ */ r("div", {
9
- className: "flex h-40 items-center justify-center text-sm text-secondary",
9
+ className: "flex h-40 items-center justify-center text-sm text-text-secondary",
10
10
  children: "This page has no widgets."
11
11
  }) : /* @__PURE__ */ r("div", {
12
- className: "grid w-full gap-3 p-3",
12
+ className: "grid w-full gap-4 p-4",
13
13
  style: {
14
14
  gridTemplateColumns: `repeat(${a}, minmax(0, 1fr))`,
15
15
  gridAutoRows: "minmax(80px, auto)"
16
16
  },
17
17
  children: e.widgets.map((e) => /* @__PURE__ */ r("div", {
18
- className: "overflow-hidden rounded border border-border-default",
18
+ className: "overflow-hidden rounded-lg border border-border-default bg-bg-surface shadow-sm",
19
19
  style: {
20
20
  gridColumn: `${Math.min(Math.max(e.layout.x, 0), a - 1) + 1} / span ${Math.min(Math.max(e.layout.w, 1), a)}`,
21
21
  gridRow: `${Math.max(e.layout.y, 0) + 1} / span ${Math.max(e.layout.h, 1)}`
@@ -32,15 +32,15 @@ function s({ model: t }) {
32
32
  return l ? /* @__PURE__ */ r(e, {
33
33
  label: "dashboard",
34
34
  children: /* @__PURE__ */ i("div", {
35
- className: "flex h-full w-full flex-col bg-surface",
35
+ className: "flex h-full w-full flex-col bg-bg-canvas",
36
36
  children: [c.length > 1 && /* @__PURE__ */ r("div", {
37
37
  role: "tablist",
38
- className: "flex gap-1 border-b border-border-default px-2",
38
+ className: "flex gap-1 border-b border-border-default bg-bg-surface px-2",
39
39
  children: c.map((e, t) => /* @__PURE__ */ r("button", {
40
40
  role: "tab",
41
41
  "aria-selected": t === a,
42
42
  onClick: () => s(t),
43
- className: t === a ? "border-b-2 border-primary px-3 py-2 text-sm text-primary" : "px-3 py-2 text-sm text-secondary",
43
+ className: t === a ? "-mb-px border-b-2 border-action-primary-bg px-3 py-2 text-sm font-medium text-text-primary" : "border-b-2 border-transparent px-3 py-2 text-sm text-text-secondary hover:text-text-primary",
44
44
  children: e.title ?? `Page ${t + 1}`
45
45
  }, e.id))
46
46
  }), /* @__PURE__ */ r("div", {
@@ -52,7 +52,7 @@ function s({ model: t }) {
52
52
  })]
53
53
  })
54
54
  }) : /* @__PURE__ */ r("div", {
55
- className: "flex h-full items-center justify-center bg-surface text-sm text-secondary",
55
+ className: "flex h-full items-center justify-center bg-bg-canvas text-sm text-text-secondary",
56
56
  children: "This dashboard has no pages."
57
57
  });
58
58
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ReadOnlyDashboardRenderer.js","names":[],"sources":["../../../../src/bigconsole/components/embed/ReadOnlyDashboardRenderer.tsx"],"sourcesContent":["/**\n * Read-only embedded dashboard renderer (BOFF-2986).\n *\n * Renders the sanitized render model's pages + widgets on a simple responsive\n * grid. It is STRICTLY view-only: no builder toolbar, edit toggle, sharing,\n * save, palette, inspector or comments — those authenticated affordances do not\n * exist in this component tree at all.\n */\n\nimport { useState } from 'react';\nimport type { EmbedCapabilities, EmbedPageModel, EmbedRenderModel } from './types';\nimport { EmbedWidgetRenderer } from './EmbedWidgetRenderer';\nimport { EmbedErrorBoundary } from './EmbedErrorBoundary';\n\nconst GRID_COLUMNS = 12;\n\ninterface Props {\n readonly model: EmbedRenderModel;\n}\n\nfunction PageGrid({ page, capabilities }: { page: EmbedPageModel; capabilities: EmbedCapabilities }) {\n if (page.widgets.length === 0) {\n return (\n <div className=\"flex h-40 items-center justify-center text-sm text-secondary\">This page has no widgets.</div>\n );\n }\n return (\n <div\n className=\"grid w-full gap-3 p-3\"\n style={{ gridTemplateColumns: `repeat(${GRID_COLUMNS}, minmax(0, 1fr))`, gridAutoRows: 'minmax(80px, auto)' }}\n >\n {page.widgets.map((widget) => (\n <div\n key={widget.id}\n className=\"overflow-hidden rounded border border-border-default\"\n style={{\n // Clamp both ends: negative/oversized coordinates from malformed\n // model data must never produce an invalid CSS grid line.\n gridColumn: `${Math.min(Math.max(widget.layout.x, 0), GRID_COLUMNS - 1) + 1} / span ${Math.min(\n Math.max(widget.layout.w, 1),\n GRID_COLUMNS\n )}`,\n gridRow: `${Math.max(widget.layout.y, 0) + 1} / span ${Math.max(widget.layout.h, 1)}`,\n }}\n >\n <EmbedWidgetRenderer widget={widget} capabilities={capabilities} />\n </div>\n ))}\n </div>\n );\n}\n\nexport function ReadOnlyDashboardRenderer({ model }: Props) {\n const [activePageIndex, setActivePageIndex] = useState(0);\n const pages = model.pages;\n const activePage = pages[Math.min(activePageIndex, pages.length - 1)];\n\n if (!activePage) {\n return (\n <div className=\"flex h-full items-center justify-center bg-surface text-sm text-secondary\">\n This dashboard has no pages.\n </div>\n );\n }\n\n return (\n <EmbedErrorBoundary label=\"dashboard\">\n <div className=\"flex h-full w-full flex-col bg-surface\">\n {pages.length > 1 && (\n <div role=\"tablist\" className=\"flex gap-1 border-b border-border-default px-2\">\n {pages.map((page, i) => (\n <button\n key={page.id}\n role=\"tab\"\n aria-selected={i === activePageIndex}\n onClick={() => setActivePageIndex(i)}\n className={\n i === activePageIndex\n ? 'border-b-2 border-primary px-3 py-2 text-sm text-primary'\n : 'px-3 py-2 text-sm text-secondary'\n }\n >\n {page.title ?? `Page ${i + 1}`}\n </button>\n ))}\n </div>\n )}\n <div className=\"min-h-0 flex-1 overflow-auto\">\n <PageGrid page={activePage} capabilities={model.capabilities} />\n </div>\n </div>\n </EmbedErrorBoundary>\n );\n}\n"],"mappings":";;;;;AAcA,IAAM,IAAe;AAMrB,SAAS,EAAS,EAAE,SAAM,mBAA2E;AAMnG,QALI,EAAK,QAAQ,WAAW,IAExB,kBAAC,OAAD;EAAK,WAAU;YAA+D;EAA+B,CAAA,GAI/G,kBAAC,OAAD;EACE,WAAU;EACV,OAAO;GAAE,qBAAqB,UAAU,EAAa;GAAoB,cAAc;GAAsB;YAE5G,EAAK,QAAQ,KAAK,MACjB,kBAAC,OAAD;GAEE,WAAU;GACV,OAAO;IAGL,YAAY,GAAG,KAAK,IAAI,KAAK,IAAI,EAAO,OAAO,GAAG,EAAE,EAAE,IAAe,EAAE,GAAG,EAAE,UAAU,KAAK,IACzF,KAAK,IAAI,EAAO,OAAO,GAAG,EAAE,EAC5B,EACD;IACD,SAAS,GAAG,KAAK,IAAI,EAAO,OAAO,GAAG,EAAE,GAAG,EAAE,UAAU,KAAK,IAAI,EAAO,OAAO,GAAG,EAAE;IACpF;aAED,kBAAC,GAAD;IAA6B;IAAsB;IAAgB,CAAA;GAC/D,EAbC,EAAO,GAaR,CACN;EACE,CAAA;;AAIV,SAAgB,EAA0B,EAAE,YAAgB;CAC1D,IAAM,CAAC,GAAiB,KAAsB,EAAS,EAAE,EACnD,IAAQ,EAAM,OACd,IAAa,EAAM,KAAK,IAAI,GAAiB,EAAM,SAAS,EAAE;AAUpE,QARK,IASH,kBAAC,GAAD;EAAoB,OAAM;YACxB,kBAAC,OAAD;GAAK,WAAU;aAAf,CACG,EAAM,SAAS,KACd,kBAAC,OAAD;IAAK,MAAK;IAAU,WAAU;cAC3B,EAAM,KAAK,GAAM,MAChB,kBAAC,UAAD;KAEE,MAAK;KACL,iBAAe,MAAM;KACrB,eAAe,EAAmB,EAAE;KACpC,WACE,MAAM,IACF,6DACA;eAGL,EAAK,SAAS,QAAQ,IAAI;KACpB,EAXF,EAAK,GAWH,CACT;IACE,CAAA,EAER,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KAAU,MAAM;KAAY,cAAc,EAAM;KAAgB,CAAA;IAC5D,CAAA,CACF;;EACa,CAAA,GAhCnB,kBAAC,OAAD;EAAK,WAAU;YAA4E;EAErF,CAAA"}
1
+ {"version":3,"file":"ReadOnlyDashboardRenderer.js","names":[],"sources":["../../../../src/bigconsole/components/embed/ReadOnlyDashboardRenderer.tsx"],"sourcesContent":["/**\n * Read-only embedded dashboard renderer (BOFF-2986).\n *\n * Renders the sanitized render model's pages + widgets on a simple responsive\n * grid. It is STRICTLY view-only: no builder toolbar, edit toggle, sharing,\n * save, palette, inspector or comments — those authenticated affordances do not\n * exist in this component tree at all.\n */\n\nimport { useState } from 'react';\nimport type { EmbedCapabilities, EmbedPageModel, EmbedRenderModel } from './types';\nimport { EmbedWidgetRenderer } from './EmbedWidgetRenderer';\nimport { EmbedErrorBoundary } from './EmbedErrorBoundary';\n\nconst GRID_COLUMNS = 12;\n\ninterface Props {\n readonly model: EmbedRenderModel;\n}\n\nfunction PageGrid({ page, capabilities }: { page: EmbedPageModel; capabilities: EmbedCapabilities }) {\n if (page.widgets.length === 0) {\n return (\n <div className=\"flex h-40 items-center justify-center text-sm text-text-secondary\">This page has no widgets.</div>\n );\n }\n return (\n <div\n className=\"grid w-full gap-4 p-4\"\n style={{ gridTemplateColumns: `repeat(${GRID_COLUMNS}, minmax(0, 1fr))`, gridAutoRows: 'minmax(80px, auto)' }}\n >\n {page.widgets.map((widget) => (\n <div\n key={widget.id}\n className=\"overflow-hidden rounded-lg border border-border-default bg-bg-surface shadow-sm\"\n style={{\n // Clamp both ends: negative/oversized coordinates from malformed\n // model data must never produce an invalid CSS grid line.\n gridColumn: `${Math.min(Math.max(widget.layout.x, 0), GRID_COLUMNS - 1) + 1} / span ${Math.min(\n Math.max(widget.layout.w, 1),\n GRID_COLUMNS\n )}`,\n gridRow: `${Math.max(widget.layout.y, 0) + 1} / span ${Math.max(widget.layout.h, 1)}`,\n }}\n >\n <EmbedWidgetRenderer widget={widget} capabilities={capabilities} />\n </div>\n ))}\n </div>\n );\n}\n\nexport function ReadOnlyDashboardRenderer({ model }: Props) {\n const [activePageIndex, setActivePageIndex] = useState(0);\n const pages = model.pages;\n const activePage = pages[Math.min(activePageIndex, pages.length - 1)];\n\n if (!activePage) {\n return (\n <div className=\"flex h-full items-center justify-center bg-bg-canvas text-sm text-text-secondary\">\n This dashboard has no pages.\n </div>\n );\n }\n\n return (\n <EmbedErrorBoundary label=\"dashboard\">\n <div className=\"flex h-full w-full flex-col bg-bg-canvas\">\n {pages.length > 1 && (\n <div role=\"tablist\" className=\"flex gap-1 border-b border-border-default bg-bg-surface px-2\">\n {pages.map((page, i) => (\n <button\n key={page.id}\n role=\"tab\"\n aria-selected={i === activePageIndex}\n onClick={() => setActivePageIndex(i)}\n className={\n i === activePageIndex\n ? '-mb-px border-b-2 border-action-primary-bg px-3 py-2 text-sm font-medium text-text-primary'\n : 'border-b-2 border-transparent px-3 py-2 text-sm text-text-secondary hover:text-text-primary'\n }\n >\n {page.title ?? `Page ${i + 1}`}\n </button>\n ))}\n </div>\n )}\n <div className=\"min-h-0 flex-1 overflow-auto\">\n <PageGrid page={activePage} capabilities={model.capabilities} />\n </div>\n </div>\n </EmbedErrorBoundary>\n );\n}\n"],"mappings":";;;;;AAcA,IAAM,IAAe;AAMrB,SAAS,EAAS,EAAE,SAAM,mBAA2E;AAMnG,QALI,EAAK,QAAQ,WAAW,IAExB,kBAAC,OAAD;EAAK,WAAU;YAAoE;EAA+B,CAAA,GAIpH,kBAAC,OAAD;EACE,WAAU;EACV,OAAO;GAAE,qBAAqB,UAAU,EAAa;GAAoB,cAAc;GAAsB;YAE5G,EAAK,QAAQ,KAAK,MACjB,kBAAC,OAAD;GAEE,WAAU;GACV,OAAO;IAGL,YAAY,GAAG,KAAK,IAAI,KAAK,IAAI,EAAO,OAAO,GAAG,EAAE,EAAE,IAAe,EAAE,GAAG,EAAE,UAAU,KAAK,IACzF,KAAK,IAAI,EAAO,OAAO,GAAG,EAAE,EAC5B,EACD;IACD,SAAS,GAAG,KAAK,IAAI,EAAO,OAAO,GAAG,EAAE,GAAG,EAAE,UAAU,KAAK,IAAI,EAAO,OAAO,GAAG,EAAE;IACpF;aAED,kBAAC,GAAD;IAA6B;IAAsB;IAAgB,CAAA;GAC/D,EAbC,EAAO,GAaR,CACN;EACE,CAAA;;AAIV,SAAgB,EAA0B,EAAE,YAAgB;CAC1D,IAAM,CAAC,GAAiB,KAAsB,EAAS,EAAE,EACnD,IAAQ,EAAM,OACd,IAAa,EAAM,KAAK,IAAI,GAAiB,EAAM,SAAS,EAAE;AAUpE,QARK,IASH,kBAAC,GAAD;EAAoB,OAAM;YACxB,kBAAC,OAAD;GAAK,WAAU;aAAf,CACG,EAAM,SAAS,KACd,kBAAC,OAAD;IAAK,MAAK;IAAU,WAAU;cAC3B,EAAM,KAAK,GAAM,MAChB,kBAAC,UAAD;KAEE,MAAK;KACL,iBAAe,MAAM;KACrB,eAAe,EAAmB,EAAE;KACpC,WACE,MAAM,IACF,+FACA;eAGL,EAAK,SAAS,QAAQ,IAAI;KACpB,EAXF,EAAK,GAWH,CACT;IACE,CAAA,EAER,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KAAU,MAAM;KAAY,cAAc,EAAM;KAAgB,CAAA;IAC5D,CAAA,CACF;;EACa,CAAA,GAhCnB,kBAAC,OAAD;EAAK,WAAU;YAAmF;EAE5F,CAAA"}
@@ -2,28 +2,34 @@ import { asFiniteNumber as e, asRecord as t, asString as n, formatNumber as r }
2
2
  import { jsx as i, jsxs as a } from "react/jsx-runtime";
3
3
  //#region src/bigconsole/components/embed/widgets/KpiComparisonEmbed.tsx
4
4
  function o({ widget: o }) {
5
- let s = t(o.config), c = t(o.data), l = e(c.current ?? s.current), u = e(c.previous ?? s.previous), d = n(s.label ?? o.title, "KPI"), f = null;
6
- l !== null && u !== null && u !== 0 && (f = (l - u) / Math.abs(u) * 100);
7
- let p = f !== null && f >= 0;
5
+ let s = t(o.config), c = t(o.data), l = e(c.current ?? s.current), u = e(c.previous ?? s.previous), d = n(s.label ?? o.title, "KPI"), f = n(s.comparisonLabel), p = s.positiveIsGood !== !1, m = null;
6
+ l !== null && u !== null && u !== 0 && (m = (l - u) / Math.abs(u) * 100);
7
+ let h = m === null || m === 0 ? "flat" : m > 0 ? "up" : "down", g = h === "flat" ? null : p ? h === "up" : h === "down", _ = h === "up" ? "▲" : h === "down" ? "▼" : "→", v = g === null ? "bg-bg-sunken text-text-secondary" : g ? "bg-status-success-bg-subtle text-status-success-text" : "bg-status-error-bg-subtle text-status-error-text";
8
8
  return /* @__PURE__ */ a("div", {
9
- className: "flex h-full w-full flex-col justify-center gap-1 bg-surface p-4",
9
+ className: "flex h-full w-full flex-col justify-center gap-1.5 bg-bg-surface p-4",
10
10
  children: [
11
11
  /* @__PURE__ */ i("div", {
12
- className: "text-xs uppercase tracking-wide text-secondary",
12
+ className: "truncate text-xs font-medium uppercase tracking-wide text-text-secondary",
13
13
  children: d
14
14
  }),
15
15
  /* @__PURE__ */ i("div", {
16
- className: "text-3xl font-semibold text-primary",
16
+ className: "text-3xl font-semibold tracking-tight text-text-primary",
17
17
  children: l === null ? "—" : r(l, s)
18
18
  }),
19
- f !== null && /* @__PURE__ */ a("div", {
20
- className: p ? "text-sm text-status-success-text" : "text-sm text-status-error-text",
21
- children: [
22
- p ? "▲" : "▼",
23
- " ",
24
- Math.abs(f).toFixed(1),
25
- "%"
26
- ]
19
+ m !== null && /* @__PURE__ */ a("div", {
20
+ className: "flex items-center gap-1.5",
21
+ children: [/* @__PURE__ */ a("span", {
22
+ className: `inline-flex items-center gap-1 rounded-full px-1.5 py-0.5 text-xs font-medium ${v}`,
23
+ children: [
24
+ _,
25
+ " ",
26
+ Math.abs(m).toFixed(1),
27
+ "%"
28
+ ]
29
+ }), f && /* @__PURE__ */ i("span", {
30
+ className: "text-xs text-text-tertiary",
31
+ children: f
32
+ })]
27
33
  })
28
34
  ]
29
35
  });
@@ -1 +1 @@
1
- {"version":3,"file":"KpiComparisonEmbed.js","names":[],"sources":["../../../../../src/bigconsole/components/embed/widgets/KpiComparisonEmbed.tsx"],"sourcesContent":["/** Read-only KPI comparison embed renderer (BOFF-2986). */\nimport type { EmbedWidgetRenderProps } from '../types';\nimport { asRecord, asString, asFiniteNumber, formatNumber } from './shared';\n\nexport function KpiComparisonEmbed({ widget }: EmbedWidgetRenderProps) {\n const config = asRecord(widget.config);\n const data = asRecord(widget.data);\n const current = asFiniteNumber(data.current ?? config.current);\n const previous = asFiniteNumber(data.previous ?? config.previous);\n const label = asString(config.label ?? widget.title, 'KPI');\n\n let deltaPct: number | null = null;\n if (current !== null && previous !== null && previous !== 0) {\n deltaPct = ((current - previous) / Math.abs(previous)) * 100;\n }\n const positive = deltaPct !== null && deltaPct >= 0;\n\n return (\n <div className=\"flex h-full w-full flex-col justify-center gap-1 bg-surface p-4\">\n <div className=\"text-xs uppercase tracking-wide text-secondary\">{label}</div>\n <div className=\"text-3xl font-semibold text-primary\">\n {current !== null ? formatNumber(current, config) : '—'}\n </div>\n {deltaPct !== null && (\n <div className={positive ? 'text-sm text-status-success-text' : 'text-sm text-status-error-text'}>\n {positive ? '▲' : '▼'} {Math.abs(deltaPct).toFixed(1)}%\n </div>\n )}\n </div>\n );\n}\n"],"mappings":";;;AAIA,SAAgB,EAAmB,EAAE,aAAkC;CACrE,IAAM,IAAS,EAAS,EAAO,OAAO,EAChC,IAAO,EAAS,EAAO,KAAK,EAC5B,IAAU,EAAe,EAAK,WAAW,EAAO,QAAQ,EACxD,IAAW,EAAe,EAAK,YAAY,EAAO,SAAS,EAC3D,IAAQ,EAAS,EAAO,SAAS,EAAO,OAAO,MAAM,EAEvD,IAA0B;AAC9B,CAAI,MAAY,QAAQ,MAAa,QAAQ,MAAa,MACxD,KAAa,IAAU,KAAY,KAAK,IAAI,EAAS,GAAI;CAE3D,IAAM,IAAW,MAAa,QAAQ,KAAY;AAElD,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD;IAAK,WAAU;cAAkD;IAAY,CAAA;GAC7E,kBAAC,OAAD;IAAK,WAAU;cACZ,MAAY,OAAuC,MAAhC,EAAa,GAAS,EAAO;IAC7C,CAAA;GACL,MAAa,QACZ,kBAAC,OAAD;IAAK,WAAW,IAAW,qCAAqC;cAAhE;KACG,IAAW,MAAM;KAAI;KAAE,KAAK,IAAI,EAAS,CAAC,QAAQ,EAAE;KAAC;KAClD;;GAEJ"}
1
+ {"version":3,"file":"KpiComparisonEmbed.js","names":[],"sources":["../../../../../src/bigconsole/components/embed/widgets/KpiComparisonEmbed.tsx"],"sourcesContent":["/** Read-only KPI comparison embed renderer (BOFF-2986 / BOFF-5733). */\nimport type { EmbedWidgetRenderProps } from '../types';\nimport { asRecord, asString, asFiniteNumber, formatNumber } from './shared';\n\nexport function KpiComparisonEmbed({ widget }: EmbedWidgetRenderProps) {\n const config = asRecord(widget.config);\n const data = asRecord(widget.data);\n const current = asFiniteNumber(data.current ?? config.current);\n const previous = asFiniteNumber(data.previous ?? config.previous);\n const label = asString(config.label ?? widget.title, 'KPI');\n const comparisonLabel = asString(config.comparisonLabel);\n // A lower value can be the \"good\" outcome (e.g. refund rate, churn); mirror the\n // in-app widget's `positiveIsGood` so the delta colour matches the app.\n const positiveIsGood = config.positiveIsGood !== false;\n\n let deltaPct: number | null = null;\n if (current !== null && previous !== null && previous !== 0) {\n deltaPct = ((current - previous) / Math.abs(previous)) * 100;\n }\n // A flat (0%) delta is neither up nor down — use a neutral marker so the arrow\n // doesn't imply an increase on the neutral badge.\n const direction = deltaPct === null || deltaPct === 0 ? 'flat' : deltaPct > 0 ? 'up' : 'down';\n const good = direction === 'flat' ? null : positiveIsGood ? direction === 'up' : direction === 'down';\n const arrow = direction === 'up' ? '▲' : direction === 'down' ? '▼' : '→';\n\n const badgeTone =\n good === null\n ? 'bg-bg-sunken text-text-secondary'\n : good\n ? 'bg-status-success-bg-subtle text-status-success-text'\n : 'bg-status-error-bg-subtle text-status-error-text';\n\n return (\n <div className=\"flex h-full w-full flex-col justify-center gap-1.5 bg-bg-surface p-4\">\n <div className=\"truncate text-xs font-medium uppercase tracking-wide text-text-secondary\">{label}</div>\n <div className=\"text-3xl font-semibold tracking-tight text-text-primary\">\n {current !== null ? formatNumber(current, config) : '—'}\n </div>\n {deltaPct !== null && (\n <div className=\"flex items-center gap-1.5\">\n <span\n className={`inline-flex items-center gap-1 rounded-full px-1.5 py-0.5 text-xs font-medium ${badgeTone}`}\n >\n {arrow} {Math.abs(deltaPct).toFixed(1)}%\n </span>\n {comparisonLabel && <span className=\"text-xs text-text-tertiary\">{comparisonLabel}</span>}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":";;;AAIA,SAAgB,EAAmB,EAAE,aAAkC;CACrE,IAAM,IAAS,EAAS,EAAO,OAAO,EAChC,IAAO,EAAS,EAAO,KAAK,EAC5B,IAAU,EAAe,EAAK,WAAW,EAAO,QAAQ,EACxD,IAAW,EAAe,EAAK,YAAY,EAAO,SAAS,EAC3D,IAAQ,EAAS,EAAO,SAAS,EAAO,OAAO,MAAM,EACrD,IAAkB,EAAS,EAAO,gBAAgB,EAGlD,IAAiB,EAAO,mBAAmB,IAE7C,IAA0B;AAC9B,CAAI,MAAY,QAAQ,MAAa,QAAQ,MAAa,MACxD,KAAa,IAAU,KAAY,KAAK,IAAI,EAAS,GAAI;CAI3D,IAAM,IAAY,MAAa,QAAQ,MAAa,IAAI,SAAS,IAAW,IAAI,OAAO,QACjF,IAAO,MAAc,SAAS,OAAO,IAAiB,MAAc,OAAO,MAAc,QACzF,IAAQ,MAAc,OAAO,MAAM,MAAc,SAAS,MAAM,KAEhE,IACJ,MAAS,OACL,qCACA,IACE,yDACA;AAER,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD;IAAK,WAAU;cAA4E;IAAY,CAAA;GACvG,kBAAC,OAAD;IAAK,WAAU;cACZ,MAAY,OAAuC,MAAhC,EAAa,GAAS,EAAO;IAC7C,CAAA;GACL,MAAa,QACZ,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,QAAD;KACE,WAAW,iFAAiF;eAD9F;MAGG;MAAM;MAAE,KAAK,IAAI,EAAS,CAAC,QAAQ,EAAE;MAAC;MAClC;QACN,KAAmB,kBAAC,QAAD;KAAM,WAAU;eAA8B;KAAuB,CAAA,CACrF;;GAEJ"}
@@ -1,28 +1,28 @@
1
- import { asArray as e, asRecord as t, asString as n, displayValue as r } from "./shared.js";
1
+ import { asArray as e, asRecord as t, asString as n, formatCell as r } from "./shared.js";
2
2
  import { jsx as i, jsxs as a } from "react/jsx-runtime";
3
3
  //#region src/bigconsole/components/embed/widgets/ListEmbed.tsx
4
4
  var o = 500;
5
5
  function s({ widget: s }) {
6
6
  let c = t(s.config), l = t(s.data), u = n(c.primaryField ?? c.titleField, "title"), d = n(c.secondaryField ?? c.subtitleField, "subtitle"), f = e(l.items).map((e) => t(e)).slice(0, o);
7
7
  return f.length === 0 ? /* @__PURE__ */ i("div", {
8
- className: "flex h-full items-center justify-center bg-surface p-4 text-sm text-secondary",
8
+ className: "flex h-full items-center justify-center bg-bg-surface p-4 text-sm text-text-secondary",
9
9
  children: "No items"
10
10
  }) : /* @__PURE__ */ a("div", {
11
- className: "h-full w-full overflow-auto bg-surface",
11
+ className: "h-full w-full overflow-auto bg-bg-surface",
12
12
  children: [s.title && /* @__PURE__ */ i("div", {
13
- className: "px-3 py-2 text-sm font-medium text-primary",
13
+ className: "sticky top-0 z-10 border-b border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary",
14
14
  children: s.title
15
15
  }), /* @__PURE__ */ i("ul", {
16
- className: "divide-y divide-border-default",
16
+ className: "divide-y divide-border-subtle",
17
17
  children: f.map((e, t) => {
18
18
  let n = r(e[u]), o = r(e[d]);
19
19
  return /* @__PURE__ */ a("li", {
20
- className: "px-3 py-2",
20
+ className: "px-3 py-2 hover:bg-bg-sunken/50",
21
21
  children: [/* @__PURE__ */ i("div", {
22
- className: "text-sm text-primary",
22
+ className: "truncate text-sm text-text-primary",
23
23
  children: n
24
24
  }), o && /* @__PURE__ */ i("div", {
25
- className: "text-xs text-secondary",
25
+ className: "truncate text-xs text-text-secondary",
26
26
  children: o
27
27
  })]
28
28
  }, t);
@@ -1 +1 @@
1
- {"version":3,"file":"ListEmbed.js","names":[],"sources":["../../../../../src/bigconsole/components/embed/widgets/ListEmbed.tsx"],"sourcesContent":["/** Read-only list embed renderer (BOFF-2986). */\nimport type { EmbedWidgetRenderProps } from '../types';\nimport { asRecord, asArray, asString, displayValue } from './shared';\n\nconst MAX_ITEMS = 500;\n\nexport function ListEmbed({ widget }: EmbedWidgetRenderProps) {\n const config = asRecord(widget.config);\n const data = asRecord(widget.data);\n const primaryKey = asString(config.primaryField ?? config.titleField, 'title');\n const secondaryKey = asString(config.secondaryField ?? config.subtitleField, 'subtitle');\n\n const items = asArray(data.items)\n .map((item) => asRecord(item))\n .slice(0, MAX_ITEMS);\n\n if (items.length === 0) {\n return (\n <div className=\"flex h-full items-center justify-center bg-surface p-4 text-sm text-secondary\">No items</div>\n );\n }\n\n return (\n <div className=\"h-full w-full overflow-auto bg-surface\">\n {widget.title && <div className=\"px-3 py-2 text-sm font-medium text-primary\">{widget.title}</div>}\n <ul className=\"divide-y divide-border-default\">\n {items.map((item, i) => {\n const primary = displayValue(item[primaryKey]);\n const secondary = displayValue(item[secondaryKey]);\n return (\n <li key={i} className=\"px-3 py-2\">\n <div className=\"text-sm text-primary\">{primary}</div>\n {secondary && <div className=\"text-xs text-secondary\">{secondary}</div>}\n </li>\n );\n })}\n </ul>\n </div>\n );\n}\n"],"mappings":";;;AAIA,IAAM,IAAY;AAElB,SAAgB,EAAU,EAAE,aAAkC;CAC5D,IAAM,IAAS,EAAS,EAAO,OAAO,EAChC,IAAO,EAAS,EAAO,KAAK,EAC5B,IAAa,EAAS,EAAO,gBAAgB,EAAO,YAAY,QAAQ,EACxE,IAAe,EAAS,EAAO,kBAAkB,EAAO,eAAe,WAAW,EAElF,IAAQ,EAAQ,EAAK,MAAM,CAC9B,KAAK,MAAS,EAAS,EAAK,CAAC,CAC7B,MAAM,GAAG,EAAU;AAQtB,QANI,EAAM,WAAW,IAEjB,kBAAC,OAAD;EAAK,WAAU;YAAgF;EAAc,CAAA,GAK/G,kBAAC,OAAD;EAAK,WAAU;YAAf,CACG,EAAO,SAAS,kBAAC,OAAD;GAAK,WAAU;aAA8C,EAAO;GAAY,CAAA,EACjG,kBAAC,MAAD;GAAI,WAAU;aACX,EAAM,KAAK,GAAM,MAAM;IACtB,IAAM,IAAU,EAAa,EAAK,GAAY,EACxC,IAAY,EAAa,EAAK,GAAc;AAClD,WACE,kBAAC,MAAD;KAAY,WAAU;eAAtB,CACE,kBAAC,OAAD;MAAK,WAAU;gBAAwB;MAAc,CAAA,EACpD,KAAa,kBAAC,OAAD;MAAK,WAAU;gBAA0B;MAAgB,CAAA,CACpE;OAHI,EAGJ;KAEP;GACC,CAAA,CACD"}
1
+ {"version":3,"file":"ListEmbed.js","names":[],"sources":["../../../../../src/bigconsole/components/embed/widgets/ListEmbed.tsx"],"sourcesContent":["/** Read-only list embed renderer (BOFF-2986 / BOFF-5733). */\nimport type { EmbedWidgetRenderProps } from '../types';\nimport { asRecord, asArray, asString, formatCell } from './shared';\n\nconst MAX_ITEMS = 500;\n\nexport function ListEmbed({ widget }: EmbedWidgetRenderProps) {\n const config = asRecord(widget.config);\n const data = asRecord(widget.data);\n const primaryKey = asString(config.primaryField ?? config.titleField, 'title');\n const secondaryKey = asString(config.secondaryField ?? config.subtitleField, 'subtitle');\n\n const items = asArray(data.items)\n .map((item) => asRecord(item))\n .slice(0, MAX_ITEMS);\n\n if (items.length === 0) {\n return (\n <div className=\"flex h-full items-center justify-center bg-bg-surface p-4 text-sm text-text-secondary\">\n No items\n </div>\n );\n }\n\n return (\n <div className=\"h-full w-full overflow-auto bg-bg-surface\">\n {widget.title && (\n <div className=\"sticky top-0 z-10 border-b border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary\">\n {widget.title}\n </div>\n )}\n <ul className=\"divide-y divide-border-subtle\">\n {items.map((item, i) => {\n const primary = formatCell(item[primaryKey]);\n const secondary = formatCell(item[secondaryKey]);\n return (\n <li key={i} className=\"px-3 py-2 hover:bg-bg-sunken/50\">\n <div className=\"truncate text-sm text-text-primary\">{primary}</div>\n {secondary && <div className=\"truncate text-xs text-text-secondary\">{secondary}</div>}\n </li>\n );\n })}\n </ul>\n </div>\n );\n}\n"],"mappings":";;;AAIA,IAAM,IAAY;AAElB,SAAgB,EAAU,EAAE,aAAkC;CAC5D,IAAM,IAAS,EAAS,EAAO,OAAO,EAChC,IAAO,EAAS,EAAO,KAAK,EAC5B,IAAa,EAAS,EAAO,gBAAgB,EAAO,YAAY,QAAQ,EACxE,IAAe,EAAS,EAAO,kBAAkB,EAAO,eAAe,WAAW,EAElF,IAAQ,EAAQ,EAAK,MAAM,CAC9B,KAAK,MAAS,EAAS,EAAK,CAAC,CAC7B,MAAM,GAAG,EAAU;AAUtB,QARI,EAAM,WAAW,IAEjB,kBAAC,OAAD;EAAK,WAAU;YAAwF;EAEjG,CAAA,GAKR,kBAAC,OAAD;EAAK,WAAU;YAAf,CACG,EAAO,SACN,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAO;GACJ,CAAA,EAER,kBAAC,MAAD;GAAI,WAAU;aACX,EAAM,KAAK,GAAM,MAAM;IACtB,IAAM,IAAU,EAAW,EAAK,GAAY,EACtC,IAAY,EAAW,EAAK,GAAc;AAChD,WACE,kBAAC,MAAD;KAAY,WAAU;eAAtB,CACE,kBAAC,OAAD;MAAK,WAAU;gBAAsC;MAAc,CAAA,EAClE,KAAa,kBAAC,OAAD;MAAK,WAAU;gBAAwC;MAAgB,CAAA,CAClF;OAHI,EAGJ;KAEP;GACC,CAAA,CACD"}
@@ -2,16 +2,24 @@ import { asFiniteNumber as e, asRecord as t, asString as n, formatNumber as r }
2
2
  import { jsx as i, jsxs as a } from "react/jsx-runtime";
3
3
  //#region src/bigconsole/components/embed/widgets/MetricCardEmbed.tsx
4
4
  function o({ widget: o }) {
5
- let s = t(o.config), c = e(t(o.data).value ?? s.value), l = n(s.label ?? o.title, "Metric"), u = c === null ? "—" : r(c, s);
5
+ let s = t(o.config), c = e(t(o.data).value ?? s.value), l = n(s.label ?? o.title, "Metric"), u = n(s.subtitle), d = c === null ? "—" : r(c, s);
6
6
  return /* @__PURE__ */ a("div", {
7
- className: "flex h-full w-full flex-col justify-center gap-1 bg-surface p-4",
8
- children: [/* @__PURE__ */ i("div", {
9
- className: "text-xs uppercase tracking-wide text-secondary",
10
- children: l
11
- }), /* @__PURE__ */ i("div", {
12
- className: "text-3xl font-semibold text-primary",
13
- children: u
14
- })]
7
+ className: "flex h-full w-full flex-col justify-center gap-1 bg-bg-surface p-4",
8
+ children: [
9
+ /* @__PURE__ */ i("div", {
10
+ className: "truncate text-xs font-medium uppercase tracking-wide text-text-secondary",
11
+ children: l
12
+ }),
13
+ /* @__PURE__ */ i("div", {
14
+ className: "text-3xl font-semibold tracking-tight text-text-primary",
15
+ title: d,
16
+ children: d
17
+ }),
18
+ u && /* @__PURE__ */ i("div", {
19
+ className: "truncate text-xs text-text-tertiary",
20
+ children: u
21
+ })
22
+ ]
15
23
  });
16
24
  }
17
25
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"MetricCardEmbed.js","names":[],"sources":["../../../../../src/bigconsole/components/embed/widgets/MetricCardEmbed.tsx"],"sourcesContent":["/** Read-only metric card embed renderer (BOFF-2986). */\nimport type { EmbedWidgetRenderProps } from '../types';\nimport { asRecord, asString, asFiniteNumber, formatNumber } from './shared';\n\nexport function MetricCardEmbed({ widget }: EmbedWidgetRenderProps) {\n const config = asRecord(widget.config);\n const data = asRecord(widget.data);\n const rawValue = asFiniteNumber(data.value ?? config.value);\n const label = asString(config.label ?? widget.title, 'Metric');\n const display = rawValue !== null ? formatNumber(rawValue, config) : '—';\n\n return (\n <div className=\"flex h-full w-full flex-col justify-center gap-1 bg-surface p-4\">\n <div className=\"text-xs uppercase tracking-wide text-secondary\">{label}</div>\n <div className=\"text-3xl font-semibold text-primary\">{display}</div>\n </div>\n );\n}\n"],"mappings":";;;AAIA,SAAgB,EAAgB,EAAE,aAAkC;CAClE,IAAM,IAAS,EAAS,EAAO,OAAO,EAEhC,IAAW,EADJ,EAAS,EAAO,KAAK,CACG,SAAS,EAAO,MAAM,EACrD,IAAQ,EAAS,EAAO,SAAS,EAAO,OAAO,SAAS,EACxD,IAAU,MAAa,OAAwC,MAAjC,EAAa,GAAU,EAAO;AAElE,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAAkD;GAAY,CAAA,EAC7E,kBAAC,OAAD;GAAK,WAAU;aAAuC;GAAc,CAAA,CAChE"}
1
+ {"version":3,"file":"MetricCardEmbed.js","names":[],"sources":["../../../../../src/bigconsole/components/embed/widgets/MetricCardEmbed.tsx"],"sourcesContent":["/** Read-only metric card embed renderer (BOFF-2986 / BOFF-5733). */\nimport type { EmbedWidgetRenderProps } from '../types';\nimport { asRecord, asString, asFiniteNumber, formatNumber } from './shared';\n\nexport function MetricCardEmbed({ widget }: EmbedWidgetRenderProps) {\n const config = asRecord(widget.config);\n const data = asRecord(widget.data);\n const rawValue = asFiniteNumber(data.value ?? config.value);\n const label = asString(config.label ?? widget.title, 'Metric');\n const subtitle = asString(config.subtitle);\n const display = rawValue !== null ? formatNumber(rawValue, config) : '—';\n\n return (\n <div className=\"flex h-full w-full flex-col justify-center gap-1 bg-bg-surface p-4\">\n <div className=\"truncate text-xs font-medium uppercase tracking-wide text-text-secondary\">{label}</div>\n <div className=\"text-3xl font-semibold tracking-tight text-text-primary\" title={display}>\n {display}\n </div>\n {subtitle && <div className=\"truncate text-xs text-text-tertiary\">{subtitle}</div>}\n </div>\n );\n}\n"],"mappings":";;;AAIA,SAAgB,EAAgB,EAAE,aAAkC;CAClE,IAAM,IAAS,EAAS,EAAO,OAAO,EAEhC,IAAW,EADJ,EAAS,EAAO,KAAK,CACG,SAAS,EAAO,MAAM,EACrD,IAAQ,EAAS,EAAO,SAAS,EAAO,OAAO,SAAS,EACxD,IAAW,EAAS,EAAO,SAAS,EACpC,IAAU,MAAa,OAAwC,MAAjC,EAAa,GAAU,EAAO;AAElE,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD;IAAK,WAAU;cAA4E;IAAY,CAAA;GACvG,kBAAC,OAAD;IAAK,WAAU;IAA0D,OAAO;cAC7E;IACG,CAAA;GACL,KAAY,kBAAC,OAAD;IAAK,WAAU;cAAuC;IAAe,CAAA;GAC9E"}
@@ -1,40 +1,52 @@
1
- import { asArray as e, asRecord as t, asString as n, displayValue as r } from "./shared.js";
2
- import { jsx as i, jsxs as a } from "react/jsx-runtime";
1
+ import { asArray as e, asRecord as t, asString as n, formatCell as r, isNumeric as i } from "./shared.js";
2
+ import { jsx as a, jsxs as o } from "react/jsx-runtime";
3
3
  //#region src/bigconsole/components/embed/widgets/TableEmbed.tsx
4
- var o = 500;
5
- function s(r, i) {
6
- let a = e(r.columns).map((e) => t(e)).map((e) => ({
4
+ var s = 500;
5
+ function c(i, a) {
6
+ let o = e(i.columns).map((e) => t(e)).map((e) => ({
7
7
  key: n(e.key),
8
8
  label: n(e.label ?? e.key)
9
9
  })).filter((e) => e.key.length > 0);
10
- return a.length > 0 ? a : Object.keys(i).map((e) => ({
10
+ if (o.length > 0) return o;
11
+ let s = /* @__PURE__ */ new Set(), c = [];
12
+ for (let e of a) for (let t of Object.keys(e)) s.has(t) || (s.add(t), c.push(t));
13
+ return c.filter((e) => !e.startsWith("_")).filter((e) => a.some((t) => r(t[e]) !== "")).map((e) => ({
11
14
  key: e,
12
15
  label: e
13
16
  }));
14
17
  }
15
- function c({ widget: n }) {
16
- let c = t(n.config), l = e(t(n.data).rows).map((e) => t(e)).slice(0, o), u = s(c, l[0] ?? {});
17
- return u.length === 0 ? /* @__PURE__ */ i("div", {
18
- className: "flex h-full items-center justify-center bg-surface p-4 text-sm text-secondary",
18
+ function l({ widget: n }) {
19
+ let l = t(n.config), u = e(t(n.data).rows).map((e) => t(e)).slice(0, s), d = c(l, u);
20
+ if (d.length === 0 || u.length === 0) return /* @__PURE__ */ a("div", {
21
+ className: "flex h-full items-center justify-center bg-bg-surface p-4 text-sm text-text-secondary",
19
22
  children: "No data"
20
- }) : /* @__PURE__ */ a("div", {
21
- className: "h-full w-full overflow-auto bg-surface",
22
- children: [n.title && /* @__PURE__ */ i("div", {
23
- className: "px-3 py-2 text-sm font-medium text-primary",
23
+ });
24
+ let f = new Set(d.filter((e) => {
25
+ let t = u.map((t) => t[e.key]).filter((e) => e != null && e !== "");
26
+ return t.length > 0 && t.every(i);
27
+ }).map((e) => e.key));
28
+ return /* @__PURE__ */ o("div", {
29
+ className: "h-full w-full overflow-auto bg-bg-surface",
30
+ children: [n.title && /* @__PURE__ */ a("div", {
31
+ className: "border-b border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary",
24
32
  children: n.title
25
- }), /* @__PURE__ */ a("table", {
33
+ }), /* @__PURE__ */ o("table", {
26
34
  className: "w-full border-collapse text-sm",
27
- children: [/* @__PURE__ */ i("thead", { children: /* @__PURE__ */ i("tr", { children: u.map((e) => /* @__PURE__ */ i("th", {
28
- className: "border-b border-border-default px-3 py-2 text-left font-medium text-secondary",
35
+ children: [/* @__PURE__ */ a("thead", { children: /* @__PURE__ */ a("tr", { children: d.map((e) => /* @__PURE__ */ a("th", {
36
+ scope: "col",
37
+ className: `sticky top-0 z-10 border-b border-border-default bg-bg-sunken px-3 py-2 font-medium text-text-secondary ${f.has(e.key) ? "text-right tabular-nums" : "text-left"}`,
29
38
  children: e.label
30
- }, e.key)) }) }), /* @__PURE__ */ i("tbody", { children: l.map((e, t) => /* @__PURE__ */ i("tr", { children: u.map((t) => /* @__PURE__ */ i("td", {
31
- className: "border-b border-border-default px-3 py-2 text-primary",
32
- children: r(e[t.key])
33
- }, t.key)) }, t)) })]
39
+ }, e.key)) }) }), /* @__PURE__ */ a("tbody", { children: u.map((e, t) => /* @__PURE__ */ a("tr", {
40
+ className: "hover:bg-bg-sunken/50",
41
+ children: d.map((t) => /* @__PURE__ */ a("td", {
42
+ className: `border-b border-border-subtle px-3 py-2 text-text-primary ${f.has(t.key) ? "text-right tabular-nums" : "text-left"}`,
43
+ children: r(e[t.key])
44
+ }, t.key))
45
+ }, t)) })]
34
46
  })]
35
47
  });
36
48
  }
37
49
  //#endregion
38
- export { c as TableEmbed };
50
+ export { l as TableEmbed };
39
51
 
40
52
  //# sourceMappingURL=TableEmbed.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"TableEmbed.js","names":[],"sources":["../../../../../src/bigconsole/components/embed/widgets/TableEmbed.tsx"],"sourcesContent":["/** Read-only table embed renderer (BOFF-2986). */\nimport type { EmbedWidgetRenderProps } from '../types';\nimport { asRecord, asArray, asString, displayValue } from './shared';\n\ninterface Column {\n key: string;\n label: string;\n}\n\n/** Hard client-side cap; the publication builder is expected to page/limit rows. */\nconst MAX_ROWS = 500;\n\nfunction readColumns(config: Record<string, unknown>, firstRow: Record<string, unknown>): Column[] {\n const configured = asArray(config.columns)\n .map((c) => asRecord(c))\n .map((c) => ({ key: asString(c.key), label: asString(c.label ?? c.key) }))\n .filter((c) => c.key.length > 0);\n if (configured.length > 0) return configured;\n // Derive from the first row when the publication didn't pin columns.\n return Object.keys(firstRow).map((key) => ({ key, label: key }));\n}\n\nexport function TableEmbed({ widget }: EmbedWidgetRenderProps) {\n const config = asRecord(widget.config);\n const data = asRecord(widget.data);\n const rows = asArray(data.rows)\n .map((r) => asRecord(r))\n .slice(0, MAX_ROWS);\n const columns = readColumns(config, rows[0] ?? {});\n\n if (columns.length === 0) {\n return <div className=\"flex h-full items-center justify-center bg-surface p-4 text-sm text-secondary\">No data</div>;\n }\n\n return (\n <div className=\"h-full w-full overflow-auto bg-surface\">\n {widget.title && <div className=\"px-3 py-2 text-sm font-medium text-primary\">{widget.title}</div>}\n <table className=\"w-full border-collapse text-sm\">\n <thead>\n <tr>\n {columns.map((col) => (\n <th\n key={col.key}\n className=\"border-b border-border-default px-3 py-2 text-left font-medium text-secondary\"\n >\n {col.label}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {rows.map((row, i) => (\n <tr key={i}>\n {columns.map((col) => (\n <td key={col.key} className=\"border-b border-border-default px-3 py-2 text-primary\">\n {displayValue(row[col.key])}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n"],"mappings":";;;AAUA,IAAM,IAAW;AAEjB,SAAS,EAAY,GAAiC,GAA6C;CACjG,IAAM,IAAa,EAAQ,EAAO,QAAQ,CACvC,KAAK,MAAM,EAAS,EAAE,CAAC,CACvB,KAAK,OAAO;EAAE,KAAK,EAAS,EAAE,IAAI;EAAE,OAAO,EAAS,EAAE,SAAS,EAAE,IAAI;EAAE,EAAE,CACzE,QAAQ,MAAM,EAAE,IAAI,SAAS,EAAE;AAGlC,QAFI,EAAW,SAAS,IAAU,IAE3B,OAAO,KAAK,EAAS,CAAC,KAAK,OAAS;EAAE;EAAK,OAAO;EAAK,EAAE;;AAGlE,SAAgB,EAAW,EAAE,aAAkC;CAC7D,IAAM,IAAS,EAAS,EAAO,OAAO,EAEhC,IAAO,EADA,EAAS,EAAO,KAAK,CACR,KAAK,CAC5B,KAAK,MAAM,EAAS,EAAE,CAAC,CACvB,MAAM,GAAG,EAAS,EACf,IAAU,EAAY,GAAQ,EAAK,MAAM,EAAE,CAAC;AAMlD,QAJI,EAAQ,WAAW,IACd,kBAAC,OAAD;EAAK,WAAU;YAAgF;EAAa,CAAA,GAInH,kBAAC,OAAD;EAAK,WAAU;YAAf,CACG,EAAO,SAAS,kBAAC,OAAD;GAAK,WAAU;aAA8C,EAAO;GAAY,CAAA,EACjG,kBAAC,SAAD;GAAO,WAAU;aAAjB,CACE,kBAAC,SAAD,EAAA,UACE,kBAAC,MAAD,EAAA,UACG,EAAQ,KAAK,MACZ,kBAAC,MAAD;IAEE,WAAU;cAET,EAAI;IACF,EAJE,EAAI,IAIN,CACL,EACC,CAAA,EACC,CAAA,EACR,kBAAC,SAAD,EAAA,UACG,EAAK,KAAK,GAAK,MACd,kBAAC,MAAD,EAAA,UACG,EAAQ,KAAK,MACZ,kBAAC,MAAD;IAAkB,WAAU;cACzB,EAAa,EAAI,EAAI,KAAK;IACxB,EAFI,EAAI,IAER,CACL,EACC,EANI,EAMJ,CACL,EACI,CAAA,CACF;KACJ"}
1
+ {"version":3,"file":"TableEmbed.js","names":[],"sources":["../../../../../src/bigconsole/components/embed/widgets/TableEmbed.tsx"],"sourcesContent":["/** Read-only table embed renderer (BOFF-2986 / BOFF-5733). */\nimport type { EmbedWidgetRenderProps } from '../types';\nimport { asRecord, asArray, asString, formatCell, isNumeric } from './shared';\n\ninterface Column {\n key: string;\n label: string;\n}\n\n/** Hard client-side cap; the publication builder is expected to page/limit rows. */\nconst MAX_ROWS = 500;\n\nfunction readColumns(config: Record<string, unknown>, rows: Record<string, unknown>[]): Column[] {\n const configured = asArray(config.columns)\n .map((c) => asRecord(c))\n .map((c) => ({ key: asString(c.key), label: asString(c.label ?? c.key) }))\n .filter((c) => c.key.length > 0);\n // A publisher that pinned columns chose exactly what to show — respect it.\n if (configured.length > 0) return configured;\n\n // Derive from the data when the publication didn't pin columns. Union the keys\n // across all rows (rows can be sparse), then drop noise so an embed never shows\n // a raw internal dump: `_`-prefixed technical keys and columns that are empty\n // in every row (the \"empty metadata/badge column\" gap from BOFF-5733).\n const seen = new Set<string>();\n const keys: string[] = [];\n for (const row of rows) {\n for (const key of Object.keys(row)) {\n if (!seen.has(key)) {\n seen.add(key);\n keys.push(key);\n }\n }\n }\n return keys\n .filter((key) => !key.startsWith('_'))\n .filter((key) => rows.some((row) => formatCell(row[key]) !== ''))\n .map((key) => ({ key, label: key }));\n}\n\nexport function TableEmbed({ widget }: EmbedWidgetRenderProps) {\n const config = asRecord(widget.config);\n const data = asRecord(widget.data);\n const rows = asArray(data.rows)\n .map((r) => asRecord(r))\n .slice(0, MAX_ROWS);\n const columns = readColumns(config, rows);\n\n if (columns.length === 0 || rows.length === 0) {\n return (\n <div className=\"flex h-full items-center justify-center bg-bg-surface p-4 text-sm text-text-secondary\">\n No data\n </div>\n );\n }\n\n // Right-align a column when its populated cells are all numeric (matches the\n // in-app TableCell), so currency/counts line up on the decimal edge.\n const numericColumns = new Set(\n columns\n .filter((col) => {\n const values = rows.map((r) => r[col.key]).filter((v) => v !== null && v !== undefined && v !== '');\n return values.length > 0 && values.every(isNumeric);\n })\n .map((col) => col.key)\n );\n\n return (\n <div className=\"h-full w-full overflow-auto bg-bg-surface\">\n {widget.title && (\n <div className=\"border-b border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary\">\n {widget.title}\n </div>\n )}\n <table className=\"w-full border-collapse text-sm\">\n <thead>\n <tr>\n {columns.map((col) => (\n // `sticky` lives on the cells (not the title, which scrolls away)\n // so column headers stay visible while scrolling the 500-row cap.\n // Cells carry their own opaque background so rows don't show through.\n <th\n key={col.key}\n scope=\"col\"\n className={`sticky top-0 z-10 border-b border-border-default bg-bg-sunken px-3 py-2 font-medium text-text-secondary ${\n numericColumns.has(col.key) ? 'text-right tabular-nums' : 'text-left'\n }`}\n >\n {col.label}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {rows.map((row, i) => (\n <tr key={i} className=\"hover:bg-bg-sunken/50\">\n {columns.map((col) => (\n <td\n key={col.key}\n className={`border-b border-border-subtle px-3 py-2 text-text-primary ${\n numericColumns.has(col.key) ? 'text-right tabular-nums' : 'text-left'\n }`}\n >\n {formatCell(row[col.key])}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n"],"mappings":";;;AAUA,IAAM,IAAW;AAEjB,SAAS,EAAY,GAAiC,GAA2C;CAC/F,IAAM,IAAa,EAAQ,EAAO,QAAQ,CACvC,KAAK,MAAM,EAAS,EAAE,CAAC,CACvB,KAAK,OAAO;EAAE,KAAK,EAAS,EAAE,IAAI;EAAE,OAAO,EAAS,EAAE,SAAS,EAAE,IAAI;EAAE,EAAE,CACzE,QAAQ,MAAM,EAAE,IAAI,SAAS,EAAE;AAElC,KAAI,EAAW,SAAS,EAAG,QAAO;CAMlC,IAAM,oBAAO,IAAI,KAAa,EACxB,IAAiB,EAAE;AACzB,MAAK,IAAM,KAAO,EAChB,MAAK,IAAM,KAAO,OAAO,KAAK,EAAI,CAChC,CAAK,EAAK,IAAI,EAAI,KAChB,EAAK,IAAI,EAAI,EACb,EAAK,KAAK,EAAI;AAIpB,QAAO,EACJ,QAAQ,MAAQ,CAAC,EAAI,WAAW,IAAI,CAAC,CACrC,QAAQ,MAAQ,EAAK,MAAM,MAAQ,EAAW,EAAI,GAAK,KAAK,GAAG,CAAC,CAChE,KAAK,OAAS;EAAE;EAAK,OAAO;EAAK,EAAE;;AAGxC,SAAgB,EAAW,EAAE,aAAkC;CAC7D,IAAM,IAAS,EAAS,EAAO,OAAO,EAEhC,IAAO,EADA,EAAS,EAAO,KAAK,CACR,KAAK,CAC5B,KAAK,MAAM,EAAS,EAAE,CAAC,CACvB,MAAM,GAAG,EAAS,EACf,IAAU,EAAY,GAAQ,EAAK;AAEzC,KAAI,EAAQ,WAAW,KAAK,EAAK,WAAW,EAC1C,QACE,kBAAC,OAAD;EAAK,WAAU;YAAwF;EAEjG,CAAA;CAMV,IAAM,IAAiB,IAAI,IACzB,EACG,QAAQ,MAAQ;EACf,IAAM,IAAS,EAAK,KAAK,MAAM,EAAE,EAAI,KAAK,CAAC,QAAQ,MAAM,KAAM,QAA2B,MAAM,GAAG;AACnG,SAAO,EAAO,SAAS,KAAK,EAAO,MAAM,EAAU;GACnD,CACD,KAAK,MAAQ,EAAI,IAAI,CACzB;AAED,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACG,EAAO,SACN,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAO;GACJ,CAAA,EAER,kBAAC,SAAD;GAAO,WAAU;aAAjB,CACE,kBAAC,SAAD,EAAA,UACE,kBAAC,MAAD,EAAA,UACG,EAAQ,KAAK,MAIZ,kBAAC,MAAD;IAEE,OAAM;IACN,WAAW,2GACT,EAAe,IAAI,EAAI,IAAI,GAAG,4BAA4B;cAG3D,EAAI;IACF,EAPE,EAAI,IAON,CACL,EACC,CAAA,EACC,CAAA,EACR,kBAAC,SAAD,EAAA,UACG,EAAK,KAAK,GAAK,MACd,kBAAC,MAAD;IAAY,WAAU;cACnB,EAAQ,KAAK,MACZ,kBAAC,MAAD;KAEE,WAAW,6DACT,EAAe,IAAI,EAAI,IAAI,GAAG,4BAA4B;eAG3D,EAAW,EAAI,EAAI,KAAK;KACtB,EANE,EAAI,IAMN,CACL;IACC,EAXI,EAWJ,CACL,EACI,CAAA,CACF;KACJ"}
@@ -4,12 +4,12 @@ import { jsx as n, jsxs as r } from "react/jsx-runtime";
4
4
  function i({ widget: i }) {
5
5
  let a = e(i.config), o = t(a.content ?? a.text ?? a.markdown);
6
6
  return /* @__PURE__ */ r("div", {
7
- className: "h-full w-full overflow-auto bg-surface p-4",
7
+ className: "h-full w-full overflow-auto bg-bg-surface p-4",
8
8
  children: [i.title && /* @__PURE__ */ n("div", {
9
- className: "mb-2 text-sm font-medium text-primary",
9
+ className: "mb-2 text-sm font-medium text-text-primary",
10
10
  children: i.title
11
11
  }), /* @__PURE__ */ n("div", {
12
- className: "whitespace-pre-wrap break-words text-sm text-primary",
12
+ className: "whitespace-pre-wrap break-words text-sm leading-relaxed text-text-secondary",
13
13
  children: o
14
14
  })]
15
15
  });
@@ -1 +1 @@
1
- {"version":3,"file":"TextEmbed.js","names":[],"sources":["../../../../../src/bigconsole/components/embed/widgets/TextEmbed.tsx"],"sourcesContent":["/**\n * Read-only text embed renderer (BOFF-2986).\n *\n * Renders the text content as PLAIN TEXT — React escapes it, so no HTML/script\n * from the content can execute. Rich Markdown/HTML rendering is deferred until a\n * vetted sanitizer is wired; until then plain text is the safe default rather\n * than `dangerouslySetInnerHTML`.\n */\nimport type { EmbedWidgetRenderProps } from '../types';\nimport { asRecord, asString } from './shared';\n\nexport function TextEmbed({ widget }: EmbedWidgetRenderProps) {\n const config = asRecord(widget.config);\n const content = asString(config.content ?? config.text ?? config.markdown);\n\n return (\n <div className=\"h-full w-full overflow-auto bg-surface p-4\">\n {widget.title && <div className=\"mb-2 text-sm font-medium text-primary\">{widget.title}</div>}\n <div className=\"whitespace-pre-wrap break-words text-sm text-primary\">{content}</div>\n </div>\n );\n}\n"],"mappings":";;;AAWA,SAAgB,EAAU,EAAE,aAAkC;CAC5D,IAAM,IAAS,EAAS,EAAO,OAAO,EAChC,IAAU,EAAS,EAAO,WAAW,EAAO,QAAQ,EAAO,SAAS;AAE1E,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACG,EAAO,SAAS,kBAAC,OAAD;GAAK,WAAU;aAAyC,EAAO;GAAY,CAAA,EAC5F,kBAAC,OAAD;GAAK,WAAU;aAAwD;GAAc,CAAA,CACjF"}
1
+ {"version":3,"file":"TextEmbed.js","names":[],"sources":["../../../../../src/bigconsole/components/embed/widgets/TextEmbed.tsx"],"sourcesContent":["/**\n * Read-only text embed renderer (BOFF-2986 / BOFF-5733).\n *\n * Renders the text content as PLAIN TEXT — React escapes it, so no HTML/script\n * from the content can execute. Rich Markdown/HTML rendering is deferred until a\n * vetted sanitizer is wired; until then plain text is the safe default rather\n * than `dangerouslySetInnerHTML`.\n */\nimport type { EmbedWidgetRenderProps } from '../types';\nimport { asRecord, asString } from './shared';\n\nexport function TextEmbed({ widget }: EmbedWidgetRenderProps) {\n const config = asRecord(widget.config);\n const content = asString(config.content ?? config.text ?? config.markdown);\n\n return (\n <div className=\"h-full w-full overflow-auto bg-bg-surface p-4\">\n {widget.title && <div className=\"mb-2 text-sm font-medium text-text-primary\">{widget.title}</div>}\n <div className=\"whitespace-pre-wrap break-words text-sm leading-relaxed text-text-secondary\">{content}</div>\n </div>\n );\n}\n"],"mappings":";;;AAWA,SAAgB,EAAU,EAAE,aAAkC;CAC5D,IAAM,IAAS,EAAS,EAAO,OAAO,EAChC,IAAU,EAAS,EAAO,WAAW,EAAO,QAAQ,EAAO,SAAS;AAE1E,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACG,EAAO,SAAS,kBAAC,OAAD;GAAK,WAAU;aAA8C,EAAO;GAAY,CAAA,EACjG,kBAAC,OAAD;GAAK,WAAU;aAA+E;GAAc,CAAA,CACxG"}
@@ -20,10 +20,55 @@ function i(e) {
20
20
  return e == null ? "" : typeof e == "string" ? e : typeof e == "number" ? Number.isFinite(e) ? String(e) : "" : typeof e == "boolean" ? e ? "true" : "false" : "";
21
21
  }
22
22
  function a(n, r) {
23
- let i = t(r.decimals), a = e(r.prefix), o = e(r.suffix);
24
- return `${a}${i === null ? String(n) : n.toFixed(Math.min(Math.max(i, 0), 10))}${o}`;
23
+ let i = e(r.format, "number"), a = t(r.decimals), c = e(r.prefix), l = e(r.suffix), u;
24
+ switch (i) {
25
+ case "currency": {
26
+ let e = s(r.currency), t = a === null ? 2 : o(a);
27
+ try {
28
+ u = new Intl.NumberFormat("en-US", {
29
+ style: "currency",
30
+ currency: e,
31
+ minimumFractionDigits: 0,
32
+ maximumFractionDigits: t
33
+ }).format(n);
34
+ } catch {
35
+ u = new Intl.NumberFormat("en-US", { maximumFractionDigits: t }).format(n);
36
+ }
37
+ break;
38
+ }
39
+ case "percentage": {
40
+ let e = a === null ? 1 : o(a);
41
+ u = `${new Intl.NumberFormat("en-US", {
42
+ minimumFractionDigits: e,
43
+ maximumFractionDigits: e
44
+ }).format(n * 100)}%`;
45
+ break;
46
+ }
47
+ case "compact":
48
+ u = new Intl.NumberFormat("en-US", {
49
+ notation: "compact",
50
+ maximumFractionDigits: a === null ? 1 : o(a)
51
+ }).format(n);
52
+ break;
53
+ default: u = new Intl.NumberFormat("en-US", { maximumFractionDigits: a === null ? 2 : o(a) }).format(n);
54
+ }
55
+ return c && i !== "currency" && (u = `${c}${u}`), l && (u = `${u}${l}`), u;
56
+ }
57
+ function o(e) {
58
+ return Math.min(Math.max(Math.trunc(e), 0), 10);
59
+ }
60
+ function s(t) {
61
+ let n = e(t, "USD").toUpperCase();
62
+ return /^[A-Z]{3}$/.test(n) ? n : "USD";
63
+ }
64
+ var c = new Intl.NumberFormat("en-US", { maximumFractionDigits: 20 });
65
+ function l(e) {
66
+ return typeof e == "number" && Number.isFinite(e) ? c.format(e) : i(e);
67
+ }
68
+ function u(e) {
69
+ return typeof e == "number" && Number.isFinite(e);
25
70
  }
26
71
  //#endregion
27
- export { n as asArray, t as asFiniteNumber, r as asRecord, e as asString, i as displayValue, a as formatNumber };
72
+ export { n as asArray, t as asFiniteNumber, r as asRecord, e as asString, l as formatCell, a as formatNumber, u as isNumeric };
28
73
 
29
74
  //# sourceMappingURL=shared.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"shared.js","names":[],"sources":["../../../../../src/bigconsole/components/embed/widgets/shared.ts"],"sourcesContent":["/**\n * Defensive value coercion for embed widget renderers (BOFF-2986).\n *\n * The render model is sanitized by the publication builder, but the renderer\n * re-narrows every value it reads: an embedded widget is anonymous, untrusted\n * customer-facing surface, so we never assume a field is the type we expect.\n */\n\nexport function asString(value: unknown, fallback = ''): string {\n return typeof value === 'string' ? value : fallback;\n}\n\nexport function asFiniteNumber(value: unknown): number | null {\n if (typeof value === 'number' && Number.isFinite(value)) return value;\n if (typeof value === 'string' && value.trim() !== '') {\n const n = Number(value);\n if (Number.isFinite(n)) return n;\n }\n return null;\n}\n\nexport function asArray(value: unknown): unknown[] {\n return Array.isArray(value) ? value : [];\n}\n\nexport function asRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : {};\n}\n\n/**\n * Render an arbitrary cell value as a display string. Objects/arrays collapse to\n * an empty string rather than `[object Object]`, so no internal structure leaks.\n */\nexport function displayValue(value: unknown): string {\n if (value === null || value === undefined) return '';\n if (typeof value === 'string') return value;\n if (typeof value === 'number') return Number.isFinite(value) ? String(value) : '';\n if (typeof value === 'boolean') return value ? 'true' : 'false';\n return '';\n}\n\n/** Format a number with the widget's optional locale/format hints, safely. */\nexport function formatNumber(value: number, config: Record<string, unknown>): string {\n const decimals = asFiniteNumber(config.decimals);\n const prefix = asString(config.prefix);\n const suffix = asString(config.suffix);\n const fixed = decimals !== null ? value.toFixed(Math.min(Math.max(decimals, 0), 10)) : String(value);\n return `${prefix}${fixed}${suffix}`;\n}\n"],"mappings":";AAQA,SAAgB,EAAS,GAAgB,IAAW,IAAY;AAC9D,QAAO,OAAO,KAAU,WAAW,IAAQ;;AAG7C,SAAgB,EAAe,GAA+B;AAC5D,KAAI,OAAO,KAAU,YAAY,OAAO,SAAS,EAAM,CAAE,QAAO;AAChE,KAAI,OAAO,KAAU,YAAY,EAAM,MAAM,KAAK,IAAI;EACpD,IAAM,IAAI,OAAO,EAAM;AACvB,MAAI,OAAO,SAAS,EAAE,CAAE,QAAO;;AAEjC,QAAO;;AAGT,SAAgB,EAAQ,GAA2B;AACjD,QAAO,MAAM,QAAQ,EAAM,GAAG,IAAQ,EAAE;;AAG1C,SAAgB,EAAS,GAAyC;AAChE,QAAO,KAAS,OAAO,KAAU,YAAY,CAAC,MAAM,QAAQ,EAAM,GAAI,IAAoC,EAAE;;AAO9G,SAAgB,EAAa,GAAwB;AAKnD,QAJI,KAAU,OAAoC,KAC9C,OAAO,KAAU,WAAiB,IAClC,OAAO,KAAU,WAAiB,OAAO,SAAS,EAAM,GAAG,OAAO,EAAM,GAAG,KAC3E,OAAO,KAAU,YAAkB,IAAQ,SAAS,UACjD;;AAIT,SAAgB,EAAa,GAAe,GAAyC;CACnF,IAAM,IAAW,EAAe,EAAO,SAAS,EAC1C,IAAS,EAAS,EAAO,OAAO,EAChC,IAAS,EAAS,EAAO,OAAO;AAEtC,QAAO,GAAG,IADI,MAAa,OAA4D,OAAO,EAAM,GAAlE,EAAM,QAAQ,KAAK,IAAI,KAAK,IAAI,GAAU,EAAE,EAAE,GAAG,CAAC,GACzD"}
1
+ {"version":3,"file":"shared.js","names":[],"sources":["../../../../../src/bigconsole/components/embed/widgets/shared.ts"],"sourcesContent":["/**\n * Defensive value coercion for embed widget renderers (BOFF-2986).\n *\n * The render model is sanitized by the publication builder, but the renderer\n * re-narrows every value it reads: an embedded widget is anonymous, untrusted\n * customer-facing surface, so we never assume a field is the type we expect.\n */\n\nexport function asString(value: unknown, fallback = ''): string {\n return typeof value === 'string' ? value : fallback;\n}\n\nexport function asFiniteNumber(value: unknown): number | null {\n if (typeof value === 'number' && Number.isFinite(value)) return value;\n if (typeof value === 'string' && value.trim() !== '') {\n const n = Number(value);\n if (Number.isFinite(n)) return n;\n }\n return null;\n}\n\nexport function asArray(value: unknown): unknown[] {\n return Array.isArray(value) ? value : [];\n}\n\nexport function asRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : {};\n}\n\n/**\n * Render an arbitrary cell value as a display string. Objects/arrays collapse to\n * an empty string rather than `[object Object]`, so no internal structure leaks.\n */\nexport function displayValue(value: unknown): string {\n if (value === null || value === undefined) return '';\n if (typeof value === 'string') return value;\n if (typeof value === 'number') return Number.isFinite(value) ? String(value) : '';\n if (typeof value === 'boolean') return value ? 'true' : 'false';\n return '';\n}\n\n/**\n * Format a number with the widget's optional format hints, safely — mirrors the\n * in-app MetricCardWidget `formatValue` so embedded numbers read identically\n * (thousands separators, currency, percentage, compact notation) instead of a\n * bare `1284530.5`. `config.format` is the same enum the in-app widgets use.\n */\nexport function formatNumber(value: number, config: Record<string, unknown>): string {\n const format = asString(config.format, 'number');\n const decimals = asFiniteNumber(config.decimals);\n const prefix = asString(config.prefix);\n const suffix = asString(config.suffix);\n\n let formatted: string;\n switch (format) {\n case 'currency': {\n // `Intl.NumberFormat` throws `RangeError` on a currency code that isn't a\n // well-formed ISO 4217 string (e.g. '', 'US', 'ABCD'). This is untrusted\n // customer-facing config, so validate the shape and fall back to USD;\n // a defensive try/catch keeps a malformed value from crashing the widget.\n const currency = resolveCurrencyCode(config.currency);\n const maximumFractionDigits = decimals !== null ? clampDecimals(decimals) : 2;\n try {\n formatted = new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency,\n minimumFractionDigits: 0,\n maximumFractionDigits,\n }).format(value);\n } catch {\n formatted = new Intl.NumberFormat('en-US', { maximumFractionDigits }).format(value);\n }\n break;\n }\n case 'percentage': {\n const pctDecimals = decimals !== null ? clampDecimals(decimals) : 1;\n formatted = `${new Intl.NumberFormat('en-US', {\n minimumFractionDigits: pctDecimals,\n maximumFractionDigits: pctDecimals,\n }).format(value * 100)}%`;\n break;\n }\n case 'compact':\n formatted = new Intl.NumberFormat('en-US', {\n notation: 'compact',\n maximumFractionDigits: decimals !== null ? clampDecimals(decimals) : 1,\n }).format(value);\n break;\n case 'number':\n default:\n formatted = new Intl.NumberFormat('en-US', {\n maximumFractionDigits: decimals !== null ? clampDecimals(decimals) : 2,\n }).format(value);\n }\n\n // `currency` already carries its own symbol; only apply an explicit prefix to\n // the other formats so we never render e.g. \"$$1,284,530\".\n if (prefix && format !== 'currency') formatted = `${prefix}${formatted}`;\n if (suffix) formatted = `${formatted}${suffix}`;\n return formatted;\n}\n\nfunction clampDecimals(decimals: number): number {\n return Math.min(Math.max(Math.trunc(decimals), 0), 10);\n}\n\n/**\n * Narrow an untrusted `currency` config value to a well-formed ISO 4217 alpha\n * code (three letters), defaulting to USD. Prevents the `RangeError` that\n * `Intl.NumberFormat({ style: 'currency' })` throws on a malformed code.\n */\nfunction resolveCurrencyCode(value: unknown): string {\n const code = asString(value, 'USD').toUpperCase();\n return /^[A-Z]{3}$/.test(code) ? code : 'USD';\n}\n\n/**\n * Shared cell number formatter — constructed once (a table can render hundreds of\n * cells, and `new Intl.NumberFormat` per call is a measurable hot-path cost).\n *\n * `maximumFractionDigits: 20` (the API max) is LOSSLESS for JS doubles (~17\n * significant digits), so this only adds grouping separators — it never rounds\n * away precision. A blanket round (e.g. 4 dp) would corrupt customer-facing\n * high-precision ratios/coordinates/measurements (`0.0000123` → `0`), whereas\n * embed cells with no explicit column format must preserve the raw value.\n */\nconst CELL_NUMBER_FORMAT = new Intl.NumberFormat('en-US', { maximumFractionDigits: 20 });\n\n/**\n * Format an arbitrary table cell: finite numbers get thousands separators\n * (matching the in-app TableCell) at full precision, everything else falls back\n * to the safe string coercion that never leaks internal object structure.\n */\nexport function formatCell(value: unknown): string {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return CELL_NUMBER_FORMAT.format(value);\n }\n return displayValue(value);\n}\n\n/** True when a value is a finite number — used for right-aligning numeric columns. */\nexport function isNumeric(value: unknown): boolean {\n return typeof value === 'number' && Number.isFinite(value);\n}\n"],"mappings":";AAQA,SAAgB,EAAS,GAAgB,IAAW,IAAY;AAC9D,QAAO,OAAO,KAAU,WAAW,IAAQ;;AAG7C,SAAgB,EAAe,GAA+B;AAC5D,KAAI,OAAO,KAAU,YAAY,OAAO,SAAS,EAAM,CAAE,QAAO;AAChE,KAAI,OAAO,KAAU,YAAY,EAAM,MAAM,KAAK,IAAI;EACpD,IAAM,IAAI,OAAO,EAAM;AACvB,MAAI,OAAO,SAAS,EAAE,CAAE,QAAO;;AAEjC,QAAO;;AAGT,SAAgB,EAAQ,GAA2B;AACjD,QAAO,MAAM,QAAQ,EAAM,GAAG,IAAQ,EAAE;;AAG1C,SAAgB,EAAS,GAAyC;AAChE,QAAO,KAAS,OAAO,KAAU,YAAY,CAAC,MAAM,QAAQ,EAAM,GAAI,IAAoC,EAAE;;AAO9G,SAAgB,EAAa,GAAwB;AAKnD,QAJI,KAAU,OAAoC,KAC9C,OAAO,KAAU,WAAiB,IAClC,OAAO,KAAU,WAAiB,OAAO,SAAS,EAAM,GAAG,OAAO,EAAM,GAAG,KAC3E,OAAO,KAAU,YAAkB,IAAQ,SAAS,UACjD;;AAST,SAAgB,EAAa,GAAe,GAAyC;CACnF,IAAM,IAAS,EAAS,EAAO,QAAQ,SAAS,EAC1C,IAAW,EAAe,EAAO,SAAS,EAC1C,IAAS,EAAS,EAAO,OAAO,EAChC,IAAS,EAAS,EAAO,OAAO,EAElC;AACJ,SAAQ,GAAR;EACE,KAAK,YAAY;GAKf,IAAM,IAAW,EAAoB,EAAO,SAAS,EAC/C,IAAwB,MAAa,OAAiC,IAA1B,EAAc,EAAS;AACzE,OAAI;AACF,QAAY,IAAI,KAAK,aAAa,SAAS;KACzC,OAAO;KACP;KACA,uBAAuB;KACvB;KACD,CAAC,CAAC,OAAO,EAAM;WACV;AACN,QAAY,IAAI,KAAK,aAAa,SAAS,EAAE,0BAAuB,CAAC,CAAC,OAAO,EAAM;;AAErF;;EAEF,KAAK,cAAc;GACjB,IAAM,IAAc,MAAa,OAAiC,IAA1B,EAAc,EAAS;AAC/D,OAAY,GAAG,IAAI,KAAK,aAAa,SAAS;IAC5C,uBAAuB;IACvB,uBAAuB;IACxB,CAAC,CAAC,OAAO,IAAQ,IAAI,CAAC;AACvB;;EAEF,KAAK;AACH,OAAY,IAAI,KAAK,aAAa,SAAS;IACzC,UAAU;IACV,uBAAuB,MAAa,OAAiC,IAA1B,EAAc,EAAS;IACnE,CAAC,CAAC,OAAO,EAAM;AAChB;EAEF,QACE,KAAY,IAAI,KAAK,aAAa,SAAS,EACzC,uBAAuB,MAAa,OAAiC,IAA1B,EAAc,EAAS,EACnE,CAAC,CAAC,OAAO,EAAM;;AAOpB,QAFI,KAAU,MAAW,eAAY,IAAY,GAAG,IAAS,MACzD,MAAQ,IAAY,GAAG,IAAY,MAChC;;AAGT,SAAS,EAAc,GAA0B;AAC/C,QAAO,KAAK,IAAI,KAAK,IAAI,KAAK,MAAM,EAAS,EAAE,EAAE,EAAE,GAAG;;AAQxD,SAAS,EAAoB,GAAwB;CACnD,IAAM,IAAO,EAAS,GAAO,MAAM,CAAC,aAAa;AACjD,QAAO,aAAa,KAAK,EAAK,GAAG,IAAO;;AAa1C,IAAM,IAAqB,IAAI,KAAK,aAAa,SAAS,EAAE,uBAAuB,IAAI,CAAC;AAOxF,SAAgB,EAAW,GAAwB;AAIjD,QAHI,OAAO,KAAU,YAAY,OAAO,SAAS,EAAM,GAC9C,EAAmB,OAAO,EAAM,GAElC,EAAa,EAAM;;AAI5B,SAAgB,EAAU,GAAyB;AACjD,QAAO,OAAO,KAAU,YAAY,OAAO,SAAS,EAAM"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-bigconsole",
3
- "version": "2026.731.8",
3
+ "version": "2026.801.1",
4
4
  "description": "BigConsole - AI-powered analytics and dashboard platform",
5
5
  "type": "module",
6
6
  "files": [