@burdenoff/microfe-brainyrich 2026.828.2 → 2026.830.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.
@@ -1,10 +1,11 @@
1
- import { useReducedMotion as e } from "../components/viz.js";
2
- import { useEffect as t, useState as n } from "react";
3
- import { Inbox as r, RefreshCw as i, TriangleAlert as a } from "lucide-react";
4
- import { jsx as o, jsxs as s } from "react/jsx-runtime";
5
- import { Badge as c, Button as l, Skeleton as u } from "@burdenoff/fe-libs/ui";
1
+ import { useTr as e } from "../hooks/useTr.js";
2
+ import { useReducedMotion as t } from "../components/viz.js";
3
+ import { useEffect as n, useState as r } from "react";
4
+ import { Inbox as i, RefreshCw as a, TriangleAlert as o } from "lucide-react";
5
+ import { jsx as s, jsxs as c } from "react/jsx-runtime";
6
+ import { Badge as l, Button as u, Skeleton as d } from "@burdenoff/fe-libs/ui";
6
7
  //#region src/brainyrich/pages/_shared.tsx
7
- function d(e, t, n) {
8
+ function f(e, t, n) {
8
9
  try {
9
10
  return new Intl.NumberFormat("en-US", {
10
11
  ...n,
@@ -18,19 +19,19 @@ function d(e, t, n) {
18
19
  return t ? `${r} ${t}` : r;
19
20
  }
20
21
  }
21
- function f(e, t = "USD") {
22
- return Number.isFinite(e) ? d(e, t, { maximumFractionDigits: 0 }) : "—";
22
+ function p(e, t = "USD") {
23
+ return Number.isFinite(e) ? f(e, t, { maximumFractionDigits: 0 }) : "—";
23
24
  }
24
- function p(e, t) {
25
- return Number.isFinite(e) ? d(e, t, {
25
+ function m(e, t) {
26
+ return Number.isFinite(e) ? f(e, t, {
26
27
  notation: "compact",
27
28
  maximumFractionDigits: 1
28
29
  }) : "—";
29
30
  }
30
- function m(e, t = 1) {
31
+ function h(e, t = 1) {
31
32
  return e == null || !Number.isFinite(e) ? "—" : `${e.toFixed(t)}%`;
32
33
  }
33
- function h(e) {
34
+ function g(e) {
34
35
  if (!e) return "—";
35
36
  let t = new Date(e);
36
37
  return Number.isNaN(t.getTime()) ? "—" : new Intl.DateTimeFormat("en-US", {
@@ -39,7 +40,7 @@ function h(e) {
39
40
  day: "numeric"
40
41
  }).format(t);
41
42
  }
42
- function g(e) {
43
+ function _(e) {
43
44
  let t = e.toUpperCase();
44
45
  return [
45
46
  "ACTIVE",
@@ -68,98 +69,98 @@ function g(e) {
68
69
  "LOW"
69
70
  ].includes(t) ? "secondary" : "outline";
70
71
  }
71
- function _(e) {
72
+ function v(e) {
72
73
  return e > 0 ? "text-status-success-text" : e < 0 ? "text-status-error-text" : "text-text-secondary";
73
74
  }
74
- function v({ title: e, description: t, actionLabel: n, onAction: r, actions: i, badges: a = [], children: u }) {
75
- return /* @__PURE__ */ s("div", {
75
+ function y({ title: e, description: t, actionLabel: n, onAction: r, actions: i, badges: a = [], children: o }) {
76
+ return /* @__PURE__ */ c("div", {
76
77
  className: "space-y-6 p-6",
77
- children: [/* @__PURE__ */ s("div", {
78
+ children: [/* @__PURE__ */ c("div", {
78
79
  className: "flex flex-col gap-4 md:flex-row md:items-start md:justify-between",
79
- children: [/* @__PURE__ */ s("div", { children: [/* @__PURE__ */ o("h1", {
80
+ children: [/* @__PURE__ */ c("div", { children: [/* @__PURE__ */ s("h1", {
80
81
  className: "text-2xl font-semibold text-text-primary",
81
82
  children: e
82
- }), /* @__PURE__ */ o("p", {
83
+ }), /* @__PURE__ */ s("p", {
83
84
  className: "text-sm text-text-secondary",
84
85
  children: t
85
- })] }), /* @__PURE__ */ s("div", {
86
+ })] }), /* @__PURE__ */ c("div", {
86
87
  className: "flex items-center gap-2",
87
88
  children: [
88
- a.map((e) => /* @__PURE__ */ o(c, {
89
+ a.map((e) => /* @__PURE__ */ s(l, {
89
90
  variant: "secondary",
90
91
  children: e
91
92
  }, e)),
92
93
  i,
93
- n ? /* @__PURE__ */ o(l, {
94
+ n ? /* @__PURE__ */ s(u, {
94
95
  size: "sm",
95
96
  onClick: r,
96
97
  children: n
97
98
  }) : null
98
99
  ]
99
100
  })]
100
- }), u]
101
+ }), o]
101
102
  });
102
103
  }
103
- var y = {
104
+ var b = {
104
105
  default: "bg-primary",
105
106
  positive: "bg-status-success-bg",
106
107
  negative: "bg-status-error-bg",
107
108
  warning: "bg-status-warning-bg",
108
109
  accent: "bg-accent-teal"
109
110
  };
110
- function b({ width: r, className: i }) {
111
- let a = e(), [s, c] = n(a ? r : 0);
112
- return t(() => {
113
- let e = requestAnimationFrame(() => c(r));
114
- return () => cancelAnimationFrame(e);
115
- }, [r]), /* @__PURE__ */ o("div", {
111
+ function x({ width: e, className: i }) {
112
+ let a = t(), [o, c] = r(a ? e : 0);
113
+ return n(() => {
114
+ let t = requestAnimationFrame(() => c(e));
115
+ return () => cancelAnimationFrame(t);
116
+ }, [e]), /* @__PURE__ */ s("div", {
116
117
  className: i,
117
118
  style: {
118
- width: `${s}%`,
119
+ width: `${o}%`,
119
120
  transition: a ? "none" : "width 500ms ease"
120
121
  }
121
122
  });
122
123
  }
123
- function x({ data: e }) {
124
- let t = e.filter((e) => Number.isFinite(e.value));
125
- return t.length === 0 ? /* @__PURE__ */ o("p", {
124
+ function S({ data: t }) {
125
+ let n = e(), r = t.filter((e) => Number.isFinite(e.value));
126
+ return r.length === 0 ? /* @__PURE__ */ s("p", {
126
127
  className: "text-sm text-text-secondary",
127
- children: "No allocation data."
128
- }) : /* @__PURE__ */ o("div", {
128
+ children: n("brainyrich.common.noAllocationData", "No allocation data.")
129
+ }) : /* @__PURE__ */ s("div", {
129
130
  className: "space-y-3.5",
130
- children: t.map((e) => {
131
+ children: r.map((e) => {
131
132
  let t = Math.max(0, Math.min(100, e.value));
132
- return /* @__PURE__ */ s("div", {
133
+ return /* @__PURE__ */ c("div", {
133
134
  className: "space-y-1.5",
134
- children: [/* @__PURE__ */ s("div", {
135
+ children: [/* @__PURE__ */ c("div", {
135
136
  className: "flex items-center justify-between text-xs",
136
- children: [/* @__PURE__ */ o("span", {
137
+ children: [/* @__PURE__ */ s("span", {
137
138
  className: "font-medium text-text-primary",
138
139
  children: e.label
139
- }), /* @__PURE__ */ o("span", {
140
+ }), /* @__PURE__ */ s("span", {
140
141
  className: "tabular-nums text-text-secondary",
141
- children: e.caption ?? m(e.value)
142
+ children: e.caption ?? h(e.value)
142
143
  })]
143
- }), /* @__PURE__ */ o("div", {
144
+ }), /* @__PURE__ */ s("div", {
144
145
  className: "h-2.5 w-full overflow-hidden rounded-full bg-bg-sunken",
145
146
  role: "progressbar",
146
147
  "aria-valuenow": Math.round(t),
147
148
  "aria-valuemin": 0,
148
149
  "aria-valuemax": 100,
149
150
  "aria-label": e.label,
150
- children: /* @__PURE__ */ o(b, {
151
+ children: /* @__PURE__ */ s(x, {
151
152
  width: t,
152
- className: `h-full rounded-full ${y[e.tone ?? "default"]}`
153
+ className: `h-full rounded-full ${b[e.tone ?? "default"]}`
153
154
  })
154
155
  })]
155
156
  }, e.label);
156
157
  })
157
158
  });
158
159
  }
159
- function S(e) {
160
+ function C(e) {
160
161
  return e == null ? "—" : typeof e == "number" ? Number.isInteger(e) ? e.toLocaleString() : e.toLocaleString(void 0, { maximumFractionDigits: 2 }) : typeof e == "boolean" ? e ? "Yes" : "No" : typeof e == "string" ? e : JSON.stringify(e);
161
162
  }
162
- function C(e) {
163
+ function w(e) {
163
164
  if (!e || typeof e != "object" || Array.isArray(e)) return null;
164
165
  let t = Object.entries(e);
165
166
  if (t.length === 0) return null;
@@ -169,99 +170,100 @@ function C(e) {
169
170
  entries: t.filter(([e]) => e !== "formula")
170
171
  };
171
172
  }
172
- function w({ entries: e }) {
173
- return e.length === 0 ? null : /* @__PURE__ */ o("dl", {
173
+ function T({ entries: e }) {
174
+ return e.length === 0 ? null : /* @__PURE__ */ s("dl", {
174
175
  className: "grid grid-cols-2 gap-x-4 gap-y-1 sm:grid-cols-3 lg:grid-cols-4",
175
- children: e.map(([e, t]) => /* @__PURE__ */ s("div", {
176
+ children: e.map(([e, t]) => /* @__PURE__ */ c("div", {
176
177
  className: "min-w-0",
177
- children: [/* @__PURE__ */ o("dt", {
178
+ children: [/* @__PURE__ */ s("dt", {
178
179
  className: "truncate text-[11px] uppercase tracking-wide text-text-muted",
179
180
  children: e.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/_/g, " ").replace(/^./, (e) => e.toUpperCase())
180
- }), /* @__PURE__ */ o("dd", {
181
+ }), /* @__PURE__ */ s("dd", {
181
182
  className: "truncate text-sm font-medium tabular-nums text-text-primary",
182
- title: S(t),
183
- children: S(t)
183
+ title: C(t),
184
+ children: C(t)
184
185
  })]
185
186
  }, e))
186
187
  });
187
188
  }
188
- function T({ icon: e, title: t, description: n, action: i }) {
189
- return /* @__PURE__ */ s("div", {
189
+ function E({ icon: e, title: t, description: n, action: r }) {
190
+ return /* @__PURE__ */ c("div", {
190
191
  className: "flex flex-col items-center justify-center rounded-lg border border-dashed border-border-subtle bg-bg-surface px-6 py-12 text-center",
191
192
  children: [
192
- /* @__PURE__ */ o("div", {
193
+ /* @__PURE__ */ s("div", {
193
194
  className: "mb-4 flex size-12 items-center justify-center rounded-full bg-bg-sunken text-text-secondary",
194
- children: e ?? /* @__PURE__ */ o(r, {
195
+ children: e ?? /* @__PURE__ */ s(i, {
195
196
  className: "size-6",
196
197
  "aria-hidden": "true"
197
198
  })
198
199
  }),
199
- /* @__PURE__ */ o("h3", {
200
+ /* @__PURE__ */ s("h3", {
200
201
  className: "text-base font-semibold text-text-primary",
201
202
  children: t
202
203
  }),
203
- n ? /* @__PURE__ */ o("p", {
204
+ n ? /* @__PURE__ */ s("p", {
204
205
  className: "mt-1 max-w-md text-sm text-text-secondary",
205
206
  children: n
206
207
  }) : null,
207
- i ? /* @__PURE__ */ o("div", {
208
+ r ? /* @__PURE__ */ s("div", {
208
209
  className: "mt-4",
209
- children: i
210
+ children: r
210
211
  }) : null
211
212
  ]
212
213
  });
213
214
  }
214
- function E({ rows: e = 4 }) {
215
- return /* @__PURE__ */ o("div", {
215
+ function D({ rows: e = 4 }) {
216
+ return /* @__PURE__ */ s("div", {
216
217
  className: "space-y-3",
217
218
  "aria-busy": "true",
218
219
  "aria-live": "polite",
219
- children: Array.from({ length: e }).map((e, t) => /* @__PURE__ */ s("div", {
220
+ children: Array.from({ length: e }).map((e, t) => /* @__PURE__ */ c("div", {
220
221
  className: "flex items-center gap-4 rounded-lg border border-border-subtle bg-bg-surface p-4",
221
222
  children: [
222
- /* @__PURE__ */ o(u, { className: "size-10 rounded-full" }),
223
- /* @__PURE__ */ s("div", {
223
+ /* @__PURE__ */ s(d, { className: "size-10 rounded-full" }),
224
+ /* @__PURE__ */ c("div", {
224
225
  className: "flex-1 space-y-2",
225
- children: [/* @__PURE__ */ o(u, { className: "h-4 w-1/3" }), /* @__PURE__ */ o(u, { className: "h-3 w-1/2" })]
226
+ children: [/* @__PURE__ */ s(d, { className: "h-4 w-1/3" }), /* @__PURE__ */ s(d, { className: "h-3 w-1/2" })]
226
227
  }),
227
- /* @__PURE__ */ o(u, { className: "h-4 w-16" })
228
+ /* @__PURE__ */ s(d, { className: "h-4 w-16" })
228
229
  ]
229
230
  }, t))
230
231
  });
231
232
  }
232
- function D({ title: e = "Something went wrong", description: t = "We could not load this data. Please try again.", onRetry: n }) {
233
- return /* @__PURE__ */ s("div", {
233
+ function O({ title: t, description: n, onRetry: r }) {
234
+ let i = e(), l = t ?? i("brainyrich.common.error.title", "Something went wrong"), d = n ?? i("brainyrich.common.error.description", "We could not load this data. Please try again.");
235
+ return /* @__PURE__ */ c("div", {
234
236
  className: "flex flex-col items-center justify-center rounded-lg border border-status-error-text/40 bg-status-error-bg px-6 py-12 text-center",
235
237
  children: [
236
- /* @__PURE__ */ o("div", {
238
+ /* @__PURE__ */ s("div", {
237
239
  className: "mb-4 flex size-12 items-center justify-center rounded-full bg-bg-surface text-status-error-text",
238
- children: /* @__PURE__ */ o(a, {
240
+ children: /* @__PURE__ */ s(o, {
239
241
  className: "size-6",
240
242
  "aria-hidden": "true"
241
243
  })
242
244
  }),
243
- /* @__PURE__ */ o("h3", {
245
+ /* @__PURE__ */ s("h3", {
244
246
  className: "text-base font-semibold text-text-primary",
245
- children: e
247
+ children: l
246
248
  }),
247
- /* @__PURE__ */ o("p", {
249
+ /* @__PURE__ */ s("p", {
248
250
  className: "mt-1 max-w-md text-sm text-text-secondary",
249
- children: t
251
+ children: d
250
252
  }),
251
- n ? /* @__PURE__ */ s(l, {
253
+ r ? /* @__PURE__ */ c(u, {
252
254
  variant: "outline",
253
255
  size: "sm",
254
256
  className: "mt-4",
255
- onClick: n,
256
- children: [/* @__PURE__ */ o(i, {
257
+ onClick: r,
258
+ children: [/* @__PURE__ */ s(a, {
257
259
  className: "size-4",
258
260
  "aria-hidden": "true"
259
- }), "Retry"]
261
+ }), i("brainyrich.common.retry", "Retry")]
260
262
  }) : null
261
263
  ]
262
264
  });
263
265
  }
264
266
  //#endregion
265
- export { x as AllocationBars, T as EmptyState, D as ErrorState, E as LoadingState, w as MetricGrid, v as SectionShell, _ as deltaToneClass, p as formatCompact, h as formatDate, f as formatMoney, m as formatPct, C as readEngineMetrics, g as statusBadgeVariant };
267
+ export { S as AllocationBars, E as EmptyState, O as ErrorState, D as LoadingState, T as MetricGrid, y as SectionShell, v as deltaToneClass, m as formatCompact, g as formatDate, p as formatMoney, h as formatPct, w as readEngineMetrics, _ as statusBadgeVariant };
266
268
 
267
269
  //# sourceMappingURL=_shared.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"_shared.js","names":[],"sources":["../../../src/brainyrich/pages/_shared.tsx"],"sourcesContent":["import { useEffect, useState, type ReactNode } from 'react';\nimport { ArrowDownRight, ArrowUpRight, Inbox, Minus, RefreshCw, TriangleAlert } from 'lucide-react';\nimport { Badge, Button, Card, CardContent, Skeleton } from '@burdenoff/fe-libs/ui';\nimport { FadeIn, useReducedMotion } from '../components/viz';\n\n// ============================================================================\n// Formatters\n// ============================================================================\n\n/**\n * `Intl.NumberFormat` throws `RangeError` on a currency code that is not three\n * ASCII letters, and `BraPortfolio.baseCurrency` / `BraHolding.currency` are\n * free-text `String!` columns — a single row saved as \"US\" would otherwise take\n * down every list that formats it. Fall back to the number plus the raw code.\n */\nfunction formatCurrencySafely(\n value: number,\n currency: string | undefined,\n options: Intl.NumberFormatOptions\n): string {\n try {\n return new Intl.NumberFormat('en-US', {\n ...options,\n ...(currency ? { style: 'currency', currency } : {}),\n }).format(value);\n } catch {\n const plain = new Intl.NumberFormat('en-US', options).format(value);\n return currency ? `${plain} ${currency}` : plain;\n }\n}\n\n/** USD currency, no cents. Pass a currency code to override. */\nexport function formatMoney(value: number, currency = 'USD'): string {\n if (!Number.isFinite(value)) return '—';\n return formatCurrencySafely(value, currency, { maximumFractionDigits: 0 });\n}\n\n/** Compact money/number, e.g. 1.2M, 950K. */\nexport function formatCompact(value: number, currency?: string): string {\n if (!Number.isFinite(value)) return '—';\n return formatCurrencySafely(value, currency, {\n notation: 'compact',\n maximumFractionDigits: 1,\n });\n}\n\n/** Percentage from a whole-number percent (e.g. 12.5 → \"12.5%\"). */\nexport function formatPct(value: number | null | undefined, fractionDigits = 1): string {\n if (value === null || value === undefined || !Number.isFinite(value)) return '—';\n return `${value.toFixed(fractionDigits)}%`;\n}\n\n/**\n * Guarded date formatter. Only wraps a present ISO string in `new Date()` at\n * render time — never calls `.getTime()` on a raw scalar and never `new Date(0)`.\n * Returns an em-dash for absent/invalid values.\n */\nexport function formatDate(iso?: string | null): string {\n if (!iso) return '—';\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return '—';\n return new Intl.DateTimeFormat('en-US', {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n }).format(d);\n}\n\n// ============================================================================\n// Status → badge variant (semantic, no raw colors)\n// ============================================================================\n\n/**\n * Map a domain status/severity string to a shadcn Badge variant. Accepts the\n * Bra* enum members (ACTIVE, AT_RISK, HIGH, …) case-insensitively so pages can\n * pass raw enum values.\n */\nexport function statusBadgeVariant(\n status: string\n): 'default' | 'secondary' | 'destructive' | 'outline' {\n const s = status.toUpperCase();\n if (['ACTIVE', 'ACHIEVED', 'ON_TRACK', 'AHEAD', 'RESOLVED', 'EXECUTED', 'APPROVED'].includes(s))\n return 'default';\n if (['AT_RISK', 'HIGH', 'CRITICAL', 'ABANDONED', 'REJECTED', 'CANCELLED', 'CLOSED'].includes(s))\n return 'destructive';\n if (\n ['ONBOARDING', 'PROSPECT', 'ON_HOLD', 'BEHIND', 'DRAFT', 'PROPOSED', 'MEDIUM', 'LOW'].includes(\n s\n )\n )\n return 'secondary';\n return 'outline';\n}\n\n/** Semantic text-color token for a signed delta (color is never the only signal). */\nexport function deltaToneClass(value: number): string {\n if (value > 0) return 'text-status-success-text';\n if (value < 0) return 'text-status-error-text';\n return 'text-text-secondary';\n}\n\n// ============================================================================\n// Section shell + KPIs\n// ============================================================================\n\ninterface SectionShellProps {\n title: string;\n description: string;\n actionLabel?: string;\n onAction?: () => void;\n actions?: ReactNode;\n badges?: string[];\n children: ReactNode;\n}\n\nexport function SectionShell({\n title,\n description,\n actionLabel,\n onAction,\n actions,\n badges = [],\n children,\n}: SectionShellProps) {\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex flex-col gap-4 md:flex-row md:items-start md:justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold text-text-primary\">{title}</h1>\n <p className=\"text-sm text-text-secondary\">{description}</p>\n </div>\n <div className=\"flex items-center gap-2\">\n {badges.map((badge) => (\n <Badge key={badge} variant=\"secondary\">\n {badge}\n </Badge>\n ))}\n {actions}\n {actionLabel ? (\n <Button size=\"sm\" onClick={onAction}>\n {actionLabel}\n </Button>\n ) : null}\n </div>\n </div>\n {children}\n </div>\n );\n}\n\nexport interface KpiItem {\n label: string;\n value: string;\n /** Optional signed delta rendered as a DeltaBadge (icon + sign + color). */\n delta?: number;\n /** Optional free-text trend line (neutral tone unless deltaTone provided). */\n trend?: string;\n deltaTone?: 'positive' | 'negative' | 'neutral';\n}\n\nexport function KpiGrid({ items }: { items: KpiItem[] }) {\n return (\n <div className=\"grid gap-4 md:grid-cols-2 xl:grid-cols-4\">\n {items.map((item, i) => (\n <FadeIn key={item.label} index={i}>\n <Card className=\"h-full transition-shadow motion-safe:hover:shadow-[var(--shadow-pop-hover,var(--shadow-elevation-2))]\">\n <CardContent className=\"space-y-1.5 p-4\">\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n {item.label}\n </p>\n <p className=\"text-2xl font-semibold tracking-tight text-text-primary\">\n {item.value}\n </p>\n {item.delta !== undefined ? (\n <DeltaBadge value={item.delta} />\n ) : item.trend ? (\n <p\n className={\n item.deltaTone === 'positive'\n ? 'text-xs text-status-success-text'\n : item.deltaTone === 'negative'\n ? 'text-xs text-status-error-text'\n : 'text-xs text-text-secondary'\n }\n >\n {item.trend}\n </p>\n ) : null}\n </CardContent>\n </Card>\n </FadeIn>\n ))}\n </div>\n );\n}\n\n// ============================================================================\n// Delta badge — icon + sign + semantic color (color never the only signal)\n// ============================================================================\n\nexport function DeltaBadge({\n value,\n suffix = '%',\n className = '',\n}: {\n value: number;\n suffix?: string;\n className?: string;\n}) {\n const positive = value > 0;\n const negative = value < 0;\n const Icon = positive ? ArrowUpRight : negative ? ArrowDownRight : Minus;\n const tone = positive\n ? 'text-status-success-text bg-status-success-bg'\n : negative\n ? 'text-status-error-text bg-status-error-bg'\n : 'text-text-secondary bg-bg-sunken';\n const sign = positive ? '+' : '';\n return (\n <span\n className={`inline-flex items-center gap-0.5 rounded-full px-1.5 py-0.5 text-xs font-medium ${tone} ${className}`}\n >\n <Icon className=\"size-3\" aria-hidden=\"true\" />\n {sign}\n {Number.isFinite(value) ? value.toFixed(1) : '—'}\n {suffix}\n </span>\n );\n}\n\n// ============================================================================\n// Allocation bars — tokenized horizontal bar primitive\n// ============================================================================\n\nexport type AllocationTone = 'default' | 'positive' | 'negative' | 'warning' | 'accent';\n\nconst TONE_FILL: Record<AllocationTone, string> = {\n default: 'bg-primary',\n positive: 'bg-status-success-bg',\n negative: 'bg-status-error-bg',\n warning: 'bg-status-warning-bg',\n accent: 'bg-accent-teal',\n};\n\nexport interface AllocationDatum {\n label: string;\n /** Percentage 0–100 for the bar width. */\n value: number;\n tone?: AllocationTone;\n /** Optional right-aligned caption (e.g. formatted money). Defaults to the %. */\n caption?: string;\n}\n\n/**\n * Single allocation bar fill. Starts at 0 width and grows to `width`% after\n * mount via a CSS transition; instant (no transition) under reduced motion.\n */\nfunction AllocationFill({ width, className }: { width: number; className: string }) {\n const reduce = useReducedMotion();\n // Reduced motion starts at the final width (instant); otherwise start at 0 and\n // grow on the next frame via the CSS width transition.\n const [w, setW] = useState(reduce ? width : 0);\n\n useEffect(() => {\n const id = requestAnimationFrame(() => setW(width));\n return () => cancelAnimationFrame(id);\n }, [width]);\n\n return (\n <div\n className={className}\n style={{ width: `${w}%`, transition: reduce ? 'none' : 'width 500ms ease' }}\n />\n );\n}\n\nexport function AllocationBars({ data }: { data: AllocationDatum[] }) {\n const rows = data.filter((d) => Number.isFinite(d.value));\n if (rows.length === 0) {\n return <p className=\"text-sm text-text-secondary\">No allocation data.</p>;\n }\n return (\n <div className=\"space-y-3.5\">\n {rows.map((d) => {\n const width = Math.max(0, Math.min(100, d.value));\n return (\n <div key={d.label} className=\"space-y-1.5\">\n <div className=\"flex items-center justify-between text-xs\">\n <span className=\"font-medium text-text-primary\">{d.label}</span>\n <span className=\"tabular-nums text-text-secondary\">\n {d.caption ?? formatPct(d.value)}\n </span>\n </div>\n <div\n className=\"h-2.5 w-full overflow-hidden rounded-full bg-bg-sunken\"\n role=\"progressbar\"\n aria-valuenow={Math.round(width)}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-label={d.label}\n >\n <AllocationFill\n width={width}\n className={`h-full rounded-full ${TONE_FILL[d.tone ?? 'default']}`}\n />\n </div>\n </div>\n );\n })}\n </div>\n );\n}\n\n// ============================================================================\n// Deterministic-engine metrics (BraRiskSignal.metrics / BraInsight.metrics)\n// ============================================================================\n\n/**\n * Render one entry of a deterministic-engine `metrics` object without inventing\n * formatting rules per-key: numbers render as plain locale numbers (2dp when\n * fractional), booleans as Yes/No, everything else as-is. This is the ONLY way\n * the engine's real inputs and formula reach the screen — never replaced by a\n * derived or opaque display value.\n */\nexport function formatMetricValue(value: unknown): string {\n if (value === null || value === undefined) return '—';\n if (typeof value === 'number') {\n return Number.isInteger(value)\n ? value.toLocaleString()\n : value.toLocaleString(undefined, { maximumFractionDigits: 2 });\n }\n if (typeof value === 'boolean') return value ? 'Yes' : 'No';\n if (typeof value === 'string') return value;\n return JSON.stringify(value);\n}\n\n/**\n * A `metrics` JSON scalar split into the engine's own `formula` string (when it\n * set one) and the remaining measured entries. Returns `null` when there is\n * nothing to show, so callers render no empty rationale block.\n */\nexport function readEngineMetrics(\n metrics: unknown\n): { formula: string | null; entries: Array<[string, unknown]> } | null {\n if (!metrics || typeof metrics !== 'object' || Array.isArray(metrics)) return null;\n const all = Object.entries(metrics as Record<string, unknown>);\n if (all.length === 0) return null;\n const formulaValue = (metrics as Record<string, unknown>).formula;\n return {\n formula: typeof formulaValue === 'string' ? formulaValue : null,\n entries: all.filter(([key]) => key !== 'formula'),\n };\n}\n\n/** Key/value grid for engine metric entries — matches the risk-signal rows. */\nexport function MetricGrid({ entries }: { entries: Array<[string, unknown]> }) {\n if (entries.length === 0) return null;\n return (\n <dl className=\"grid grid-cols-2 gap-x-4 gap-y-1 sm:grid-cols-3 lg:grid-cols-4\">\n {entries.map(([key, value]) => (\n <div key={key} className=\"min-w-0\">\n <dt className=\"truncate text-[11px] uppercase tracking-wide text-text-muted\">\n {key\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .replace(/_/g, ' ')\n .replace(/^./, (c) => c.toUpperCase())}\n </dt>\n <dd\n className=\"truncate text-sm font-medium tabular-nums text-text-primary\"\n title={formatMetricValue(value)}\n >\n {formatMetricValue(value)}\n </dd>\n </div>\n ))}\n </dl>\n );\n}\n\n// ============================================================================\n// Empty / Loading / Error states (designed, shared)\n// ============================================================================\n\nexport function EmptyState({\n icon,\n title,\n description,\n action,\n}: {\n icon?: ReactNode;\n title: string;\n description?: string;\n action?: ReactNode;\n}) {\n return (\n <div className=\"flex flex-col items-center justify-center rounded-lg border border-dashed border-border-subtle bg-bg-surface px-6 py-12 text-center\">\n <div className=\"mb-4 flex size-12 items-center justify-center rounded-full bg-bg-sunken text-text-secondary\">\n {icon ?? <Inbox className=\"size-6\" aria-hidden=\"true\" />}\n </div>\n <h3 className=\"text-base font-semibold text-text-primary\">{title}</h3>\n {description ? (\n <p className=\"mt-1 max-w-md text-sm text-text-secondary\">{description}</p>\n ) : null}\n {action ? <div className=\"mt-4\">{action}</div> : null}\n </div>\n );\n}\n\nexport function LoadingState({ rows = 4 }: { rows?: number }) {\n return (\n <div className=\"space-y-3\" aria-busy=\"true\" aria-live=\"polite\">\n {Array.from({ length: rows }).map((_, i) => (\n <div\n key={i}\n className=\"flex items-center gap-4 rounded-lg border border-border-subtle bg-bg-surface p-4\"\n >\n <Skeleton className=\"size-10 rounded-full\" />\n <div className=\"flex-1 space-y-2\">\n <Skeleton className=\"h-4 w-1/3\" />\n <Skeleton className=\"h-3 w-1/2\" />\n </div>\n <Skeleton className=\"h-4 w-16\" />\n </div>\n ))}\n </div>\n );\n}\n\nexport function ErrorState({\n title = 'Something went wrong',\n description = 'We could not load this data. Please try again.',\n onRetry,\n}: {\n title?: string;\n description?: string;\n onRetry?: () => void;\n}) {\n return (\n <div className=\"flex flex-col items-center justify-center rounded-lg border border-status-error-text/40 bg-status-error-bg px-6 py-12 text-center\">\n <div className=\"mb-4 flex size-12 items-center justify-center rounded-full bg-bg-surface text-status-error-text\">\n <TriangleAlert className=\"size-6\" aria-hidden=\"true\" />\n </div>\n <h3 className=\"text-base font-semibold text-text-primary\">{title}</h3>\n <p className=\"mt-1 max-w-md text-sm text-text-secondary\">{description}</p>\n {onRetry ? (\n <Button variant=\"outline\" size=\"sm\" className=\"mt-4\" onClick={onRetry}>\n <RefreshCw className=\"size-4\" aria-hidden=\"true\" />\n Retry\n </Button>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"_shared.js","names":[],"sources":["../../../src/brainyrich/pages/_shared.tsx"],"sourcesContent":["import { useEffect, useState, type ReactNode } from 'react';\nimport { ArrowDownRight, ArrowUpRight, Inbox, Minus, RefreshCw, TriangleAlert } from 'lucide-react';\nimport { Badge, Button, Card, CardContent, Skeleton } from '@burdenoff/fe-libs/ui';\nimport { FadeIn, useReducedMotion } from '../components/viz';\nimport { useTr } from '../hooks/useTr';\n\n// ============================================================================\n// Formatters\n// ============================================================================\n\n/**\n * `Intl.NumberFormat` throws `RangeError` on a currency code that is not three\n * ASCII letters, and `BraPortfolio.baseCurrency` / `BraHolding.currency` are\n * free-text `String!` columns — a single row saved as \"US\" would otherwise take\n * down every list that formats it. Fall back to the number plus the raw code.\n */\nfunction formatCurrencySafely(\n value: number,\n currency: string | undefined,\n options: Intl.NumberFormatOptions\n): string {\n try {\n return new Intl.NumberFormat('en-US', {\n ...options,\n ...(currency ? { style: 'currency', currency } : {}),\n }).format(value);\n } catch {\n const plain = new Intl.NumberFormat('en-US', options).format(value);\n return currency ? `${plain} ${currency}` : plain;\n }\n}\n\n/** USD currency, no cents. Pass a currency code to override. */\nexport function formatMoney(value: number, currency = 'USD'): string {\n if (!Number.isFinite(value)) return '—';\n return formatCurrencySafely(value, currency, { maximumFractionDigits: 0 });\n}\n\n/** Compact money/number, e.g. 1.2M, 950K. */\nexport function formatCompact(value: number, currency?: string): string {\n if (!Number.isFinite(value)) return '—';\n return formatCurrencySafely(value, currency, {\n notation: 'compact',\n maximumFractionDigits: 1,\n });\n}\n\n/** Percentage from a whole-number percent (e.g. 12.5 → \"12.5%\"). */\nexport function formatPct(value: number | null | undefined, fractionDigits = 1): string {\n if (value === null || value === undefined || !Number.isFinite(value)) return '—';\n return `${value.toFixed(fractionDigits)}%`;\n}\n\n/**\n * Guarded date formatter. Only wraps a present ISO string in `new Date()` at\n * render time — never calls `.getTime()` on a raw scalar and never `new Date(0)`.\n * Returns an em-dash for absent/invalid values.\n */\nexport function formatDate(iso?: string | null): string {\n if (!iso) return '—';\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return '—';\n return new Intl.DateTimeFormat('en-US', {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n }).format(d);\n}\n\n// ============================================================================\n// Status → badge variant (semantic, no raw colors)\n// ============================================================================\n\n/**\n * Map a domain status/severity string to a shadcn Badge variant. Accepts the\n * Bra* enum members (ACTIVE, AT_RISK, HIGH, …) case-insensitively so pages can\n * pass raw enum values.\n */\nexport function statusBadgeVariant(\n status: string\n): 'default' | 'secondary' | 'destructive' | 'outline' {\n const s = status.toUpperCase();\n if (['ACTIVE', 'ACHIEVED', 'ON_TRACK', 'AHEAD', 'RESOLVED', 'EXECUTED', 'APPROVED'].includes(s))\n return 'default';\n if (['AT_RISK', 'HIGH', 'CRITICAL', 'ABANDONED', 'REJECTED', 'CANCELLED', 'CLOSED'].includes(s))\n return 'destructive';\n if (\n ['ONBOARDING', 'PROSPECT', 'ON_HOLD', 'BEHIND', 'DRAFT', 'PROPOSED', 'MEDIUM', 'LOW'].includes(\n s\n )\n )\n return 'secondary';\n return 'outline';\n}\n\n/** Semantic text-color token for a signed delta (color is never the only signal). */\nexport function deltaToneClass(value: number): string {\n if (value > 0) return 'text-status-success-text';\n if (value < 0) return 'text-status-error-text';\n return 'text-text-secondary';\n}\n\n// ============================================================================\n// Section shell + KPIs\n// ============================================================================\n\ninterface SectionShellProps {\n title: string;\n description: string;\n actionLabel?: string;\n onAction?: () => void;\n actions?: ReactNode;\n badges?: string[];\n children: ReactNode;\n}\n\nexport function SectionShell({\n title,\n description,\n actionLabel,\n onAction,\n actions,\n badges = [],\n children,\n}: SectionShellProps) {\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex flex-col gap-4 md:flex-row md:items-start md:justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold text-text-primary\">{title}</h1>\n <p className=\"text-sm text-text-secondary\">{description}</p>\n </div>\n <div className=\"flex items-center gap-2\">\n {badges.map((badge) => (\n <Badge key={badge} variant=\"secondary\">\n {badge}\n </Badge>\n ))}\n {actions}\n {actionLabel ? (\n <Button size=\"sm\" onClick={onAction}>\n {actionLabel}\n </Button>\n ) : null}\n </div>\n </div>\n {children}\n </div>\n );\n}\n\nexport interface KpiItem {\n label: string;\n value: string;\n /** Optional signed delta rendered as a DeltaBadge (icon + sign + color). */\n delta?: number;\n /** Optional free-text trend line (neutral tone unless deltaTone provided). */\n trend?: string;\n deltaTone?: 'positive' | 'negative' | 'neutral';\n}\n\nexport function KpiGrid({ items }: { items: KpiItem[] }) {\n return (\n <div className=\"grid gap-4 md:grid-cols-2 xl:grid-cols-4\">\n {items.map((item, i) => (\n <FadeIn key={item.label} index={i}>\n <Card className=\"h-full transition-shadow motion-safe:hover:shadow-[var(--shadow-pop-hover,var(--shadow-elevation-2))]\">\n <CardContent className=\"space-y-1.5 p-4\">\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n {item.label}\n </p>\n <p className=\"text-2xl font-semibold tracking-tight text-text-primary\">\n {item.value}\n </p>\n {item.delta !== undefined ? (\n <DeltaBadge value={item.delta} />\n ) : item.trend ? (\n <p\n className={\n item.deltaTone === 'positive'\n ? 'text-xs text-status-success-text'\n : item.deltaTone === 'negative'\n ? 'text-xs text-status-error-text'\n : 'text-xs text-text-secondary'\n }\n >\n {item.trend}\n </p>\n ) : null}\n </CardContent>\n </Card>\n </FadeIn>\n ))}\n </div>\n );\n}\n\n// ============================================================================\n// Delta badge — icon + sign + semantic color (color never the only signal)\n// ============================================================================\n\nexport function DeltaBadge({\n value,\n suffix = '%',\n className = '',\n}: {\n value: number;\n suffix?: string;\n className?: string;\n}) {\n const positive = value > 0;\n const negative = value < 0;\n const Icon = positive ? ArrowUpRight : negative ? ArrowDownRight : Minus;\n const tone = positive\n ? 'text-status-success-text bg-status-success-bg'\n : negative\n ? 'text-status-error-text bg-status-error-bg'\n : 'text-text-secondary bg-bg-sunken';\n const sign = positive ? '+' : '';\n return (\n <span\n className={`inline-flex items-center gap-0.5 rounded-full px-1.5 py-0.5 text-xs font-medium ${tone} ${className}`}\n >\n <Icon className=\"size-3\" aria-hidden=\"true\" />\n {sign}\n {Number.isFinite(value) ? value.toFixed(1) : '—'}\n {suffix}\n </span>\n );\n}\n\n// ============================================================================\n// Allocation bars — tokenized horizontal bar primitive\n// ============================================================================\n\nexport type AllocationTone = 'default' | 'positive' | 'negative' | 'warning' | 'accent';\n\nconst TONE_FILL: Record<AllocationTone, string> = {\n default: 'bg-primary',\n positive: 'bg-status-success-bg',\n negative: 'bg-status-error-bg',\n warning: 'bg-status-warning-bg',\n accent: 'bg-accent-teal',\n};\n\nexport interface AllocationDatum {\n label: string;\n /** Percentage 0–100 for the bar width. */\n value: number;\n tone?: AllocationTone;\n /** Optional right-aligned caption (e.g. formatted money). Defaults to the %. */\n caption?: string;\n}\n\n/**\n * Single allocation bar fill. Starts at 0 width and grows to `width`% after\n * mount via a CSS transition; instant (no transition) under reduced motion.\n */\nfunction AllocationFill({ width, className }: { width: number; className: string }) {\n const reduce = useReducedMotion();\n // Reduced motion starts at the final width (instant); otherwise start at 0 and\n // grow on the next frame via the CSS width transition.\n const [w, setW] = useState(reduce ? width : 0);\n\n useEffect(() => {\n const id = requestAnimationFrame(() => setW(width));\n return () => cancelAnimationFrame(id);\n }, [width]);\n\n return (\n <div\n className={className}\n style={{ width: `${w}%`, transition: reduce ? 'none' : 'width 500ms ease' }}\n />\n );\n}\n\nexport function AllocationBars({ data }: { data: AllocationDatum[] }) {\n const tr = useTr();\n const rows = data.filter((d) => Number.isFinite(d.value));\n if (rows.length === 0) {\n return (\n <p className=\"text-sm text-text-secondary\">\n {tr('brainyrich.common.noAllocationData', 'No allocation data.')}\n </p>\n );\n }\n return (\n <div className=\"space-y-3.5\">\n {rows.map((d) => {\n const width = Math.max(0, Math.min(100, d.value));\n return (\n <div key={d.label} className=\"space-y-1.5\">\n <div className=\"flex items-center justify-between text-xs\">\n <span className=\"font-medium text-text-primary\">{d.label}</span>\n <span className=\"tabular-nums text-text-secondary\">\n {d.caption ?? formatPct(d.value)}\n </span>\n </div>\n <div\n className=\"h-2.5 w-full overflow-hidden rounded-full bg-bg-sunken\"\n role=\"progressbar\"\n aria-valuenow={Math.round(width)}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-label={d.label}\n >\n <AllocationFill\n width={width}\n className={`h-full rounded-full ${TONE_FILL[d.tone ?? 'default']}`}\n />\n </div>\n </div>\n );\n })}\n </div>\n );\n}\n\n// ============================================================================\n// Deterministic-engine metrics (BraRiskSignal.metrics / BraInsight.metrics)\n// ============================================================================\n\n/**\n * Render one entry of a deterministic-engine `metrics` object without inventing\n * formatting rules per-key: numbers render as plain locale numbers (2dp when\n * fractional), booleans as Yes/No, everything else as-is. This is the ONLY way\n * the engine's real inputs and formula reach the screen — never replaced by a\n * derived or opaque display value.\n */\nexport function formatMetricValue(value: unknown): string {\n if (value === null || value === undefined) return '—';\n if (typeof value === 'number') {\n return Number.isInteger(value)\n ? value.toLocaleString()\n : value.toLocaleString(undefined, { maximumFractionDigits: 2 });\n }\n if (typeof value === 'boolean') return value ? 'Yes' : 'No';\n if (typeof value === 'string') return value;\n return JSON.stringify(value);\n}\n\n/**\n * A `metrics` JSON scalar split into the engine's own `formula` string (when it\n * set one) and the remaining measured entries. Returns `null` when there is\n * nothing to show, so callers render no empty rationale block.\n */\nexport function readEngineMetrics(\n metrics: unknown\n): { formula: string | null; entries: Array<[string, unknown]> } | null {\n if (!metrics || typeof metrics !== 'object' || Array.isArray(metrics)) return null;\n const all = Object.entries(metrics as Record<string, unknown>);\n if (all.length === 0) return null;\n const formulaValue = (metrics as Record<string, unknown>).formula;\n return {\n formula: typeof formulaValue === 'string' ? formulaValue : null,\n entries: all.filter(([key]) => key !== 'formula'),\n };\n}\n\n/** Key/value grid for engine metric entries — matches the risk-signal rows. */\nexport function MetricGrid({ entries }: { entries: Array<[string, unknown]> }) {\n if (entries.length === 0) return null;\n return (\n <dl className=\"grid grid-cols-2 gap-x-4 gap-y-1 sm:grid-cols-3 lg:grid-cols-4\">\n {entries.map(([key, value]) => (\n <div key={key} className=\"min-w-0\">\n <dt className=\"truncate text-[11px] uppercase tracking-wide text-text-muted\">\n {key\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .replace(/_/g, ' ')\n .replace(/^./, (c) => c.toUpperCase())}\n </dt>\n <dd\n className=\"truncate text-sm font-medium tabular-nums text-text-primary\"\n title={formatMetricValue(value)}\n >\n {formatMetricValue(value)}\n </dd>\n </div>\n ))}\n </dl>\n );\n}\n\n// ============================================================================\n// Empty / Loading / Error states (designed, shared)\n// ============================================================================\n\nexport function EmptyState({\n icon,\n title,\n description,\n action,\n}: {\n icon?: ReactNode;\n title: string;\n description?: string;\n action?: ReactNode;\n}) {\n return (\n <div className=\"flex flex-col items-center justify-center rounded-lg border border-dashed border-border-subtle bg-bg-surface px-6 py-12 text-center\">\n <div className=\"mb-4 flex size-12 items-center justify-center rounded-full bg-bg-sunken text-text-secondary\">\n {icon ?? <Inbox className=\"size-6\" aria-hidden=\"true\" />}\n </div>\n <h3 className=\"text-base font-semibold text-text-primary\">{title}</h3>\n {description ? (\n <p className=\"mt-1 max-w-md text-sm text-text-secondary\">{description}</p>\n ) : null}\n {action ? <div className=\"mt-4\">{action}</div> : null}\n </div>\n );\n}\n\nexport function LoadingState({ rows = 4 }: { rows?: number }) {\n return (\n <div className=\"space-y-3\" aria-busy=\"true\" aria-live=\"polite\">\n {Array.from({ length: rows }).map((_, i) => (\n <div\n key={i}\n className=\"flex items-center gap-4 rounded-lg border border-border-subtle bg-bg-surface p-4\"\n >\n <Skeleton className=\"size-10 rounded-full\" />\n <div className=\"flex-1 space-y-2\">\n <Skeleton className=\"h-4 w-1/3\" />\n <Skeleton className=\"h-3 w-1/2\" />\n </div>\n <Skeleton className=\"h-4 w-16\" />\n </div>\n ))}\n </div>\n );\n}\n\nexport function ErrorState({\n title,\n description,\n onRetry,\n}: {\n title?: string;\n description?: string;\n onRetry?: () => void;\n}) {\n const tr = useTr();\n const resolvedTitle = title ?? tr('brainyrich.common.error.title', 'Something went wrong');\n const resolvedDescription =\n description ??\n tr('brainyrich.common.error.description', 'We could not load this data. Please try again.');\n\n return (\n <div className=\"flex flex-col items-center justify-center rounded-lg border border-status-error-text/40 bg-status-error-bg px-6 py-12 text-center\">\n <div className=\"mb-4 flex size-12 items-center justify-center rounded-full bg-bg-surface text-status-error-text\">\n <TriangleAlert className=\"size-6\" aria-hidden=\"true\" />\n </div>\n <h3 className=\"text-base font-semibold text-text-primary\">{resolvedTitle}</h3>\n <p className=\"mt-1 max-w-md text-sm text-text-secondary\">{resolvedDescription}</p>\n {onRetry ? (\n <Button variant=\"outline\" size=\"sm\" className=\"mt-4\" onClick={onRetry}>\n <RefreshCw className=\"size-4\" aria-hidden=\"true\" />\n {tr('brainyrich.common.retry', 'Retry')}\n </Button>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-brainyrich",
3
- "version": "2026.828.2",
3
+ "version": "2026.830.1",
4
4
  "description": "Burdenoff BrainyRich Microfrontend",
5
5
  "type": "module",
6
6
  "files": [