@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.
- package/dist/brainyrich/components/viz.d.ts.map +1 -1
- package/dist/brainyrich/components/viz.js +148 -147
- package/dist/brainyrich/components/viz.js.map +1 -1
- package/dist/brainyrich/hooks/useTr.d.ts +17 -0
- package/dist/brainyrich/hooks/useTr.d.ts.map +1 -0
- package/dist/brainyrich/hooks/useTr.js +14 -0
- package/dist/brainyrich/hooks/useTr.js.map +1 -0
- package/dist/brainyrich/pages/AnalyticsPage.d.ts.map +1 -1
- package/dist/brainyrich/pages/AnalyticsPage.js +253 -252
- package/dist/brainyrich/pages/AnalyticsPage.js.map +1 -1
- package/dist/brainyrich/pages/PortfoliosPage.d.ts.map +1 -1
- package/dist/brainyrich/pages/PortfoliosPage.js +482 -476
- package/dist/brainyrich/pages/PortfoliosPage.js.map +1 -1
- package/dist/brainyrich/pages/ReportsPage.js +36 -36
- package/dist/brainyrich/pages/ReportsPage.js.map +1 -1
- package/dist/brainyrich/pages/RiskPage.d.ts.map +1 -1
- package/dist/brainyrich/pages/RiskPage.js +272 -271
- package/dist/brainyrich/pages/RiskPage.js.map +1 -1
- package/dist/brainyrich/pages/TaxPage.d.ts.map +1 -1
- package/dist/brainyrich/pages/TaxPage.js +198 -197
- package/dist/brainyrich/pages/TaxPage.js.map +1 -1
- package/dist/brainyrich/pages/_shared.d.ts.map +1 -1
- package/dist/brainyrich/pages/_shared.js +84 -82
- package/dist/brainyrich/pages/_shared.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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
|
|
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
|
|
22
|
-
return Number.isFinite(e) ?
|
|
22
|
+
function p(e, t = "USD") {
|
|
23
|
+
return Number.isFinite(e) ? f(e, t, { maximumFractionDigits: 0 }) : "—";
|
|
23
24
|
}
|
|
24
|
-
function
|
|
25
|
-
return Number.isFinite(e) ?
|
|
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
|
|
31
|
+
function h(e, t = 1) {
|
|
31
32
|
return e == null || !Number.isFinite(e) ? "—" : `${e.toFixed(t)}%`;
|
|
32
33
|
}
|
|
33
|
-
function
|
|
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
|
|
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
|
|
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
|
|
75
|
-
return /* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
83
|
+
}), /* @__PURE__ */ s("p", {
|
|
83
84
|
className: "text-sm text-text-secondary",
|
|
84
85
|
children: t
|
|
85
|
-
})] }), /* @__PURE__ */
|
|
86
|
+
})] }), /* @__PURE__ */ c("div", {
|
|
86
87
|
className: "flex items-center gap-2",
|
|
87
88
|
children: [
|
|
88
|
-
a.map((e) => /* @__PURE__ */
|
|
89
|
+
a.map((e) => /* @__PURE__ */ s(l, {
|
|
89
90
|
variant: "secondary",
|
|
90
91
|
children: e
|
|
91
92
|
}, e)),
|
|
92
93
|
i,
|
|
93
|
-
n ? /* @__PURE__ */
|
|
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
|
-
}),
|
|
101
|
+
}), o]
|
|
101
102
|
});
|
|
102
103
|
}
|
|
103
|
-
var
|
|
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
|
|
111
|
-
let a =
|
|
112
|
-
return
|
|
113
|
-
let
|
|
114
|
-
return () => cancelAnimationFrame(
|
|
115
|
-
}, [
|
|
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: `${
|
|
119
|
+
width: `${o}%`,
|
|
119
120
|
transition: a ? "none" : "width 500ms ease"
|
|
120
121
|
}
|
|
121
122
|
});
|
|
122
123
|
}
|
|
123
|
-
function
|
|
124
|
-
let
|
|
125
|
-
return
|
|
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__ */
|
|
128
|
+
children: n("brainyrich.common.noAllocationData", "No allocation data.")
|
|
129
|
+
}) : /* @__PURE__ */ s("div", {
|
|
129
130
|
className: "space-y-3.5",
|
|
130
|
-
children:
|
|
131
|
+
children: r.map((e) => {
|
|
131
132
|
let t = Math.max(0, Math.min(100, e.value));
|
|
132
|
-
return /* @__PURE__ */
|
|
133
|
+
return /* @__PURE__ */ c("div", {
|
|
133
134
|
className: "space-y-1.5",
|
|
134
|
-
children: [/* @__PURE__ */
|
|
135
|
+
children: [/* @__PURE__ */ c("div", {
|
|
135
136
|
className: "flex items-center justify-between text-xs",
|
|
136
|
-
children: [/* @__PURE__ */
|
|
137
|
+
children: [/* @__PURE__ */ s("span", {
|
|
137
138
|
className: "font-medium text-text-primary",
|
|
138
139
|
children: e.label
|
|
139
|
-
}), /* @__PURE__ */
|
|
140
|
+
}), /* @__PURE__ */ s("span", {
|
|
140
141
|
className: "tabular-nums text-text-secondary",
|
|
141
|
-
children: e.caption ??
|
|
142
|
+
children: e.caption ?? h(e.value)
|
|
142
143
|
})]
|
|
143
|
-
}), /* @__PURE__ */
|
|
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__ */
|
|
151
|
+
children: /* @__PURE__ */ s(x, {
|
|
151
152
|
width: t,
|
|
152
|
-
className: `h-full rounded-full ${
|
|
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
|
|
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
|
|
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
|
|
173
|
-
return e.length === 0 ? null : /* @__PURE__ */
|
|
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__ */
|
|
176
|
+
children: e.map(([e, t]) => /* @__PURE__ */ c("div", {
|
|
176
177
|
className: "min-w-0",
|
|
177
|
-
children: [/* @__PURE__ */
|
|
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__ */
|
|
181
|
+
}), /* @__PURE__ */ s("dd", {
|
|
181
182
|
className: "truncate text-sm font-medium tabular-nums text-text-primary",
|
|
182
|
-
title:
|
|
183
|
-
children:
|
|
183
|
+
title: C(t),
|
|
184
|
+
children: C(t)
|
|
184
185
|
})]
|
|
185
186
|
}, e))
|
|
186
187
|
});
|
|
187
188
|
}
|
|
188
|
-
function
|
|
189
|
-
return /* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
195
|
+
children: e ?? /* @__PURE__ */ s(i, {
|
|
195
196
|
className: "size-6",
|
|
196
197
|
"aria-hidden": "true"
|
|
197
198
|
})
|
|
198
199
|
}),
|
|
199
|
-
/* @__PURE__ */
|
|
200
|
+
/* @__PURE__ */ s("h3", {
|
|
200
201
|
className: "text-base font-semibold text-text-primary",
|
|
201
202
|
children: t
|
|
202
203
|
}),
|
|
203
|
-
n ? /* @__PURE__ */
|
|
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
|
-
|
|
208
|
+
r ? /* @__PURE__ */ s("div", {
|
|
208
209
|
className: "mt-4",
|
|
209
|
-
children:
|
|
210
|
+
children: r
|
|
210
211
|
}) : null
|
|
211
212
|
]
|
|
212
213
|
});
|
|
213
214
|
}
|
|
214
|
-
function
|
|
215
|
-
return /* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
223
|
-
/* @__PURE__ */
|
|
223
|
+
/* @__PURE__ */ s(d, { className: "size-10 rounded-full" }),
|
|
224
|
+
/* @__PURE__ */ c("div", {
|
|
224
225
|
className: "flex-1 space-y-2",
|
|
225
|
-
children: [/* @__PURE__ */
|
|
226
|
+
children: [/* @__PURE__ */ s(d, { className: "h-4 w-1/3" }), /* @__PURE__ */ s(d, { className: "h-3 w-1/2" })]
|
|
226
227
|
}),
|
|
227
|
-
/* @__PURE__ */
|
|
228
|
+
/* @__PURE__ */ s(d, { className: "h-4 w-16" })
|
|
228
229
|
]
|
|
229
230
|
}, t))
|
|
230
231
|
});
|
|
231
232
|
}
|
|
232
|
-
function
|
|
233
|
-
|
|
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__ */
|
|
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
|
|
240
|
+
children: /* @__PURE__ */ s(o, {
|
|
239
241
|
className: "size-6",
|
|
240
242
|
"aria-hidden": "true"
|
|
241
243
|
})
|
|
242
244
|
}),
|
|
243
|
-
/* @__PURE__ */
|
|
245
|
+
/* @__PURE__ */ s("h3", {
|
|
244
246
|
className: "text-base font-semibold text-text-primary",
|
|
245
|
-
children:
|
|
247
|
+
children: l
|
|
246
248
|
}),
|
|
247
|
-
/* @__PURE__ */
|
|
249
|
+
/* @__PURE__ */ s("p", {
|
|
248
250
|
className: "mt-1 max-w-md text-sm text-text-secondary",
|
|
249
|
-
children:
|
|
251
|
+
children: d
|
|
250
252
|
}),
|
|
251
|
-
|
|
253
|
+
r ? /* @__PURE__ */ c(u, {
|
|
252
254
|
variant: "outline",
|
|
253
255
|
size: "sm",
|
|
254
256
|
className: "mt-4",
|
|
255
|
-
onClick:
|
|
256
|
-
children: [/* @__PURE__ */
|
|
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 {
|
|
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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"}
|