@burdenoff/microfe-brainyrich 2026.525.1 → 2026.706.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/BrainyRichRoot.js +55 -90
- package/dist/brainyrich/BrainyRichRoot.js.map +1 -1
- package/dist/brainyrich/BrainyRichRoutes.js +15 -35
- package/dist/brainyrich/BrainyRichRoutes.js.map +1 -1
- package/dist/brainyrich/components/ActorName.js +38 -0
- package/dist/brainyrich/components/ActorName.js.map +1 -0
- package/dist/brainyrich/components/ConfirmDialog.js +25 -0
- package/dist/brainyrich/components/ConfirmDialog.js.map +1 -0
- package/dist/brainyrich/components/LoadMore.js +24 -0
- package/dist/brainyrich/components/LoadMore.js.map +1 -0
- package/dist/brainyrich/components/index.js +5 -0
- package/dist/brainyrich/components/notify.js +38 -0
- package/dist/brainyrich/components/notify.js.map +1 -0
- package/dist/brainyrich/components/pickers.js +232 -0
- package/dist/brainyrich/components/pickers.js.map +1 -0
- package/dist/brainyrich/components/reuse/CommentThread.js +208 -0
- package/dist/brainyrich/components/reuse/CommentThread.js.map +1 -0
- package/dist/brainyrich/components/reuse/DocumentsPanel.js +237 -0
- package/dist/brainyrich/components/reuse/DocumentsPanel.js.map +1 -0
- package/dist/brainyrich/components/reuse/TagField.js +206 -0
- package/dist/brainyrich/components/reuse/TagField.js.map +1 -0
- package/dist/brainyrich/components/reuse/transport.js +24 -0
- package/dist/brainyrich/components/reuse/transport.js.map +1 -0
- package/dist/brainyrich/hooks/useBrainyRichApi.js +815 -0
- package/dist/brainyrich/hooks/useBrainyRichApi.js.map +1 -0
- package/dist/brainyrich/index.js +0 -1
- package/dist/brainyrich/pages/AccountsPage.js +743 -160
- package/dist/brainyrich/pages/AccountsPage.js.map +1 -1
- package/dist/brainyrich/pages/AiAssistantPage.js +556 -363
- package/dist/brainyrich/pages/AiAssistantPage.js.map +1 -1
- package/dist/brainyrich/pages/AnalyticsPage.js +601 -268
- package/dist/brainyrich/pages/AnalyticsPage.js.map +1 -1
- package/dist/brainyrich/pages/DashboardPage.js +375 -231
- package/dist/brainyrich/pages/DashboardPage.js.map +1 -1
- package/dist/brainyrich/pages/FamilyPage.js +599 -337
- package/dist/brainyrich/pages/FamilyPage.js.map +1 -1
- package/dist/brainyrich/pages/GoalsPage.js +810 -141
- package/dist/brainyrich/pages/GoalsPage.js.map +1 -1
- package/dist/brainyrich/pages/InvestmentsPage.js +410 -311
- package/dist/brainyrich/pages/InvestmentsPage.js.map +1 -1
- package/dist/brainyrich/pages/PortfoliosPage.js +867 -156
- package/dist/brainyrich/pages/PortfoliosPage.js.map +1 -1
- package/dist/brainyrich/pages/ReportsPage.js +421 -251
- package/dist/brainyrich/pages/ReportsPage.js.map +1 -1
- package/dist/brainyrich/pages/RiskPage.js +557 -194
- package/dist/brainyrich/pages/RiskPage.js.map +1 -1
- package/dist/brainyrich/pages/TaxPage.js +655 -221
- package/dist/brainyrich/pages/TaxPage.js.map +1 -1
- package/dist/brainyrich/pages/_shared.js +222 -41
- package/dist/brainyrich/pages/_shared.js.map +1 -1
- package/dist/index.js +2 -3
- package/dist/node_modules/sonner/dist/index.js +163 -0
- package/dist/node_modules/sonner/dist/index.js.map +1 -0
- package/package.json +3 -2
- package/dist/brainyrich/pages/AdvisorsPage.js +0 -375
- package/dist/brainyrich/pages/AdvisorsPage.js.map +0 -1
- package/dist/brainyrich/pages/AssistantPage.js +0 -332
- package/dist/brainyrich/pages/AssistantPage.js.map +0 -1
- package/dist/brainyrich/pages/EstatePage.js +0 -269
- package/dist/brainyrich/pages/EstatePage.js.map +0 -1
- package/dist/brainyrich/pages/InsurancePage.js +0 -181
- package/dist/brainyrich/pages/InsurancePage.js.map +0 -1
- package/dist/brainyrich/pages/MarketplacePage.js +0 -450
- package/dist/brainyrich/pages/MarketplacePage.js.map +0 -1
- package/dist/brainyrich/store/brainyrichStore.js +0 -272
- package/dist/brainyrich/store/brainyrichStore.js.map +0 -1
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { useBrainyRich as e } from "../providers/BrainyRichProvider.js";
|
|
2
|
+
import { useAccounts as t, useGoals as n, useHouseholds as r, usePortfolios as i } from "../hooks/useBrainyRichApi.js";
|
|
3
|
+
import { joinPath as a } from "../utils/navigation.js";
|
|
4
|
+
import { Loader2 as o } from "lucide-react";
|
|
5
|
+
import { jsx as s, jsxs as c } from "react/jsx-runtime";
|
|
6
|
+
import { Button as l, Label as u, Select as d, SelectContent as f, SelectGroup as p, SelectItem as m, SelectLabel as h, SelectTrigger as g, SelectValue as _ } from "@burdenoff/fe-libs/ui";
|
|
7
|
+
//#region src/brainyrich/components/pickers.tsx
|
|
8
|
+
var v = [
|
|
9
|
+
"ACCOUNT",
|
|
10
|
+
"PORTFOLIO",
|
|
11
|
+
"GOAL",
|
|
12
|
+
"HOUSEHOLD"
|
|
13
|
+
], y = {
|
|
14
|
+
ACCOUNT: "Account",
|
|
15
|
+
PORTFOLIO: "Portfolio",
|
|
16
|
+
GOAL: "Goal",
|
|
17
|
+
HOUSEHOLD: "Household"
|
|
18
|
+
};
|
|
19
|
+
function b(e, t) {
|
|
20
|
+
return `${e}:${t}`;
|
|
21
|
+
}
|
|
22
|
+
function x(e) {
|
|
23
|
+
if (!e) return null;
|
|
24
|
+
let t = e.indexOf(":");
|
|
25
|
+
if (t < 0) return null;
|
|
26
|
+
let n = e.slice(0, t), r = e.slice(t + 1);
|
|
27
|
+
return !v.includes(n) || !r ? null : {
|
|
28
|
+
subjectType: n,
|
|
29
|
+
subjectId: r
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function S({ message: t, cta: n, relativePath: r }) {
|
|
33
|
+
let { basePath: i = "/", navigate: o } = e(), u = a(i, r);
|
|
34
|
+
return /* @__PURE__ */ c("div", {
|
|
35
|
+
className: "rounded-lg border border-dashed border-border-subtle bg-bg-surface px-4 py-3 text-sm",
|
|
36
|
+
children: [/* @__PURE__ */ s("p", {
|
|
37
|
+
className: "text-text-secondary",
|
|
38
|
+
children: t
|
|
39
|
+
}), o ? /* @__PURE__ */ s(l, {
|
|
40
|
+
variant: "outline",
|
|
41
|
+
size: "sm",
|
|
42
|
+
className: "mt-2",
|
|
43
|
+
onClick: () => o(u),
|
|
44
|
+
children: n
|
|
45
|
+
}) : null]
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function C({ id: e, label: t }) {
|
|
49
|
+
return /* @__PURE__ */ c("div", {
|
|
50
|
+
className: "space-y-2",
|
|
51
|
+
children: [t ? /* @__PURE__ */ s(u, {
|
|
52
|
+
htmlFor: e,
|
|
53
|
+
children: t
|
|
54
|
+
}) : null, /* @__PURE__ */ c("div", {
|
|
55
|
+
className: "flex h-9 items-center gap-2 rounded-md border border-border-subtle bg-bg-surface px-3 text-sm text-text-secondary",
|
|
56
|
+
"aria-busy": "true",
|
|
57
|
+
children: [/* @__PURE__ */ s(o, {
|
|
58
|
+
className: "size-4 motion-safe:animate-spin",
|
|
59
|
+
"aria-hidden": "true"
|
|
60
|
+
}), "Loading…"]
|
|
61
|
+
})]
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function w({ value: e, onChange: a, id: o = "subject-picker", label: l = "Subject", placeholder: v = "Select a subject", disabled: x = !1 }) {
|
|
65
|
+
let w = t(), T = i(), E = n(), D = r(), O = w.isLoading || T.isLoading || E.isLoading || D.isLoading, k = w.data.length + T.data.length + E.data.length + D.data.length;
|
|
66
|
+
if (O) return /* @__PURE__ */ s(C, {
|
|
67
|
+
id: o,
|
|
68
|
+
label: l
|
|
69
|
+
});
|
|
70
|
+
if (k === 0) return /* @__PURE__ */ c("div", {
|
|
71
|
+
className: "space-y-2",
|
|
72
|
+
children: [l ? /* @__PURE__ */ s(u, {
|
|
73
|
+
htmlFor: o,
|
|
74
|
+
children: l
|
|
75
|
+
}) : null, /* @__PURE__ */ s(S, {
|
|
76
|
+
message: "No accounts, portfolios, goals or households yet — create one to attach this insight.",
|
|
77
|
+
cta: "Create an account",
|
|
78
|
+
relativePath: "accounts"
|
|
79
|
+
})]
|
|
80
|
+
});
|
|
81
|
+
let A = [
|
|
82
|
+
{
|
|
83
|
+
kind: "ACCOUNT",
|
|
84
|
+
items: w.data.map((e) => ({
|
|
85
|
+
id: e.id,
|
|
86
|
+
name: e.name
|
|
87
|
+
}))
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
kind: "PORTFOLIO",
|
|
91
|
+
items: T.data.map((e) => ({
|
|
92
|
+
id: e.id,
|
|
93
|
+
name: e.name
|
|
94
|
+
}))
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
kind: "GOAL",
|
|
98
|
+
items: E.data.map((e) => ({
|
|
99
|
+
id: e.id,
|
|
100
|
+
name: e.name
|
|
101
|
+
}))
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
kind: "HOUSEHOLD",
|
|
105
|
+
items: D.data.map((e) => ({
|
|
106
|
+
id: e.id,
|
|
107
|
+
name: e.name
|
|
108
|
+
}))
|
|
109
|
+
}
|
|
110
|
+
];
|
|
111
|
+
return /* @__PURE__ */ c("div", {
|
|
112
|
+
className: "space-y-2",
|
|
113
|
+
children: [l ? /* @__PURE__ */ s(u, {
|
|
114
|
+
htmlFor: o,
|
|
115
|
+
children: l
|
|
116
|
+
}) : null, /* @__PURE__ */ c(d, {
|
|
117
|
+
value: e || void 0,
|
|
118
|
+
onValueChange: a,
|
|
119
|
+
disabled: x,
|
|
120
|
+
children: [/* @__PURE__ */ s(g, {
|
|
121
|
+
id: o,
|
|
122
|
+
"aria-label": l,
|
|
123
|
+
children: /* @__PURE__ */ s(_, { placeholder: v })
|
|
124
|
+
}), /* @__PURE__ */ s(f, { children: A.filter((e) => e.items.length > 0).map((e) => /* @__PURE__ */ c(p, { children: [/* @__PURE__ */ s(h, { children: y[e.kind] }), e.items.map((t) => /* @__PURE__ */ s(m, {
|
|
125
|
+
value: b(e.kind, t.id),
|
|
126
|
+
children: t.name
|
|
127
|
+
}, `${e.kind}:${t.id}`))] }, e.kind)) })]
|
|
128
|
+
})]
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
var T = {
|
|
132
|
+
account: {
|
|
133
|
+
label: "Account",
|
|
134
|
+
placeholder: "Select an account",
|
|
135
|
+
relativePath: "accounts",
|
|
136
|
+
cta: "Create an account",
|
|
137
|
+
emptyMessage: "No accounts yet — create one first."
|
|
138
|
+
},
|
|
139
|
+
household: {
|
|
140
|
+
label: "Household",
|
|
141
|
+
placeholder: "Select a household",
|
|
142
|
+
relativePath: "family",
|
|
143
|
+
cta: "Create a household",
|
|
144
|
+
emptyMessage: "No households yet — create one first."
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
function E({ kind: e, value: n, onChange: i, id: a, label: o, placeholder: l, disabled: p = !1 }) {
|
|
148
|
+
let h = T[e], v = t(), y = r(), b = e === "account" ? v : y, x = a ?? `${e}-picker`, w = o ?? h.label;
|
|
149
|
+
if (b.isLoading) return /* @__PURE__ */ s(C, {
|
|
150
|
+
id: x,
|
|
151
|
+
label: w
|
|
152
|
+
});
|
|
153
|
+
let E = b.data.map((e) => ({
|
|
154
|
+
id: e.id,
|
|
155
|
+
name: e.name
|
|
156
|
+
}));
|
|
157
|
+
return E.length === 0 ? /* @__PURE__ */ c("div", {
|
|
158
|
+
className: "space-y-2",
|
|
159
|
+
children: [w ? /* @__PURE__ */ s(u, {
|
|
160
|
+
htmlFor: x,
|
|
161
|
+
children: w
|
|
162
|
+
}) : null, /* @__PURE__ */ s(S, {
|
|
163
|
+
message: h.emptyMessage,
|
|
164
|
+
cta: h.cta,
|
|
165
|
+
relativePath: h.relativePath
|
|
166
|
+
})]
|
|
167
|
+
}) : /* @__PURE__ */ c("div", {
|
|
168
|
+
className: "space-y-2",
|
|
169
|
+
children: [w ? /* @__PURE__ */ s(u, {
|
|
170
|
+
htmlFor: x,
|
|
171
|
+
children: w
|
|
172
|
+
}) : null, /* @__PURE__ */ c(d, {
|
|
173
|
+
value: n || void 0,
|
|
174
|
+
onValueChange: i,
|
|
175
|
+
disabled: p,
|
|
176
|
+
children: [/* @__PURE__ */ s(g, {
|
|
177
|
+
id: x,
|
|
178
|
+
"aria-label": w,
|
|
179
|
+
children: /* @__PURE__ */ s(_, { placeholder: l ?? h.placeholder })
|
|
180
|
+
}), /* @__PURE__ */ s(f, { children: E.map((e) => /* @__PURE__ */ s(m, {
|
|
181
|
+
value: e.id,
|
|
182
|
+
children: e.name
|
|
183
|
+
}, e.id)) })]
|
|
184
|
+
})]
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
function D(e) {
|
|
188
|
+
return /* @__PURE__ */ s(E, {
|
|
189
|
+
kind: "account",
|
|
190
|
+
...e
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
var O = "__none__";
|
|
194
|
+
function k({ value: e, onChange: t, id: n = "household-picker", label: i = "Household", noneLabel: a = "No household", disabled: o = !1 }) {
|
|
195
|
+
let l = r();
|
|
196
|
+
return l.isLoading ? /* @__PURE__ */ s(C, {
|
|
197
|
+
id: n,
|
|
198
|
+
label: i
|
|
199
|
+
}) : /* @__PURE__ */ c("div", {
|
|
200
|
+
className: "space-y-2",
|
|
201
|
+
children: [
|
|
202
|
+
i ? /* @__PURE__ */ s(u, {
|
|
203
|
+
htmlFor: n,
|
|
204
|
+
children: i
|
|
205
|
+
}) : null,
|
|
206
|
+
/* @__PURE__ */ c(d, {
|
|
207
|
+
value: e ?? O,
|
|
208
|
+
onValueChange: (e) => t(e === O ? null : e),
|
|
209
|
+
disabled: o,
|
|
210
|
+
children: [/* @__PURE__ */ s(g, {
|
|
211
|
+
id: n,
|
|
212
|
+
"aria-label": i,
|
|
213
|
+
children: /* @__PURE__ */ s(_, {})
|
|
214
|
+
}), /* @__PURE__ */ c(f, { children: [/* @__PURE__ */ s(m, {
|
|
215
|
+
value: O,
|
|
216
|
+
children: a
|
|
217
|
+
}), l.data.map((e) => /* @__PURE__ */ s(m, {
|
|
218
|
+
value: e.id,
|
|
219
|
+
children: e.name
|
|
220
|
+
}, e.id))] })]
|
|
221
|
+
}),
|
|
222
|
+
l.data.length === 0 ? /* @__PURE__ */ s("p", {
|
|
223
|
+
className: "text-xs text-text-secondary",
|
|
224
|
+
children: "No households yet — you can create one in Households and link this account later."
|
|
225
|
+
}) : null
|
|
226
|
+
]
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
//#endregion
|
|
230
|
+
export { D as AccountPicker, E as EntityPicker, k as HouseholdPicker, v as SUBJECT_KINDS, w as SubjectPicker, b as encodeSubject, x as parseSubject };
|
|
231
|
+
|
|
232
|
+
//# sourceMappingURL=pickers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pickers.js","names":[],"sources":["../../../src/brainyrich/components/pickers.tsx"],"sourcesContent":["/**\n * Reusable entity / subject pickers.\n * @module brainyrich/components/pickers\n *\n * - `SubjectPicker` — encodes `SUBJECT_TYPE:id` across accounts, portfolios,\n * goals and households (for the AiAssistant insight subject).\n * - `EntityPicker` — generic account / household Select returning a bare id.\n * - `AccountPicker` — convenience wrapper (kind=\"account\").\n * - `HouseholdPicker` — OPTIONAL household Select with a \"None\" option, for the\n * account create/edit form (accounts can be linked to a\n * household). Never a dead-end when no households exist.\n *\n * When a *required* source list is empty every picker renders a CTA that routes\n * the user to create the missing entity instead of showing an empty dropdown.\n */\n\nimport { Loader2 } from 'lucide-react';\nimport {\n Button,\n Label,\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectLabel,\n SelectTrigger,\n SelectValue,\n} from '@burdenoff/fe-libs/ui';\nimport { useAccounts, useGoals, useHouseholds, usePortfolios } from '../hooks/useBrainyRichApi';\nimport { useBrainyRich } from '../providers/BrainyRichProvider';\nimport { joinPath } from '../utils/navigation';\n\n// ============================================================================\n// Subject encoding (matches the SDL `subjectType` convention — uppercase)\n// ============================================================================\n\nexport const SUBJECT_KINDS = ['ACCOUNT', 'PORTFOLIO', 'GOAL', 'HOUSEHOLD'] as const;\nexport type SubjectKind = (typeof SUBJECT_KINDS)[number];\n\nconst SUBJECT_KIND_LABEL: Record<SubjectKind, string> = {\n ACCOUNT: 'Account',\n PORTFOLIO: 'Portfolio',\n GOAL: 'Goal',\n HOUSEHOLD: 'Household',\n};\n\n/** Encode a subject selection as the `SUBJECT_TYPE:id` token a SubjectPicker emits. */\nexport function encodeSubject(kind: SubjectKind, id: string): string {\n return `${kind}:${id}`;\n}\n\n/** Decode a `SUBJECT_TYPE:id` token back into `{ subjectType, subjectId }`. */\nexport function parseSubject(\n value: string | null | undefined\n): { subjectType: SubjectKind; subjectId: string } | null {\n if (!value) return null;\n const idx = value.indexOf(':');\n if (idx < 0) return null;\n const kind = value.slice(0, idx) as SubjectKind;\n const id = value.slice(idx + 1);\n if (!SUBJECT_KINDS.includes(kind) || !id) return null;\n return { subjectType: kind, subjectId: id };\n}\n\n// ============================================================================\n// Shared empty-state CTA (routes to the create surface, never a dead-end)\n// ============================================================================\n\nfunction PickerCTA({ message, cta, relativePath }: { message: string; cta: string; relativePath: string }) {\n const { basePath = '/', navigate } = useBrainyRich();\n const target = joinPath(basePath, relativePath);\n return (\n <div className=\"rounded-lg border border-dashed border-border-subtle bg-bg-surface px-4 py-3 text-sm\">\n <p className=\"text-text-secondary\">{message}</p>\n {navigate ? (\n <Button variant=\"outline\" size=\"sm\" className=\"mt-2\" onClick={() => navigate(target)}>\n {cta}\n </Button>\n ) : null}\n </div>\n );\n}\n\nfunction PickerLoading({ id, label }: { id?: string; label?: string }) {\n return (\n <div className=\"space-y-2\">\n {label ? <Label htmlFor={id}>{label}</Label> : null}\n <div\n className=\"flex h-9 items-center gap-2 rounded-md border border-border-subtle bg-bg-surface px-3 text-sm text-text-secondary\"\n aria-busy=\"true\"\n >\n <Loader2 className=\"size-4 motion-safe:animate-spin\" aria-hidden=\"true\" />\n Loading…\n </div>\n </div>\n );\n}\n\n// ============================================================================\n// SubjectPicker — accounts + portfolios + goals + households\n// ============================================================================\n\nexport interface SubjectPickerProps {\n /** Current `SUBJECT_TYPE:id` token, or empty. */\n value?: string;\n onChange: (value: string) => void;\n id?: string;\n label?: string;\n placeholder?: string;\n disabled?: boolean;\n}\n\nexport function SubjectPicker({\n value,\n onChange,\n id = 'subject-picker',\n label = 'Subject',\n placeholder = 'Select a subject',\n disabled = false,\n}: SubjectPickerProps) {\n const accounts = useAccounts();\n const portfolios = usePortfolios();\n const goals = useGoals();\n const households = useHouseholds();\n\n const loading =\n accounts.isLoading || portfolios.isLoading || goals.isLoading || households.isLoading;\n const total =\n accounts.data.length + portfolios.data.length + goals.data.length + households.data.length;\n\n if (loading) return <PickerLoading id={id} label={label} />;\n\n if (total === 0) {\n return (\n <div className=\"space-y-2\">\n {label ? <Label htmlFor={id}>{label}</Label> : null}\n <PickerCTA\n message=\"No accounts, portfolios, goals or households yet — create one to attach this insight.\"\n cta=\"Create an account\"\n relativePath=\"accounts\"\n />\n </div>\n );\n }\n\n const groups: Array<{ kind: SubjectKind; items: Array<{ id: string; name: string }> }> = [\n { kind: 'ACCOUNT', items: accounts.data.map((a) => ({ id: a.id, name: a.name })) },\n { kind: 'PORTFOLIO', items: portfolios.data.map((p) => ({ id: p.id, name: p.name })) },\n { kind: 'GOAL', items: goals.data.map((g) => ({ id: g.id, name: g.name })) },\n { kind: 'HOUSEHOLD', items: households.data.map((h) => ({ id: h.id, name: h.name })) },\n ];\n\n return (\n <div className=\"space-y-2\">\n {label ? <Label htmlFor={id}>{label}</Label> : null}\n <Select value={value || undefined} onValueChange={onChange} disabled={disabled}>\n <SelectTrigger id={id} aria-label={label}>\n <SelectValue placeholder={placeholder} />\n </SelectTrigger>\n <SelectContent>\n {groups\n .filter((group) => group.items.length > 0)\n .map((group) => (\n <SelectGroup key={group.kind}>\n <SelectLabel>{SUBJECT_KIND_LABEL[group.kind]}</SelectLabel>\n {group.items.map((item) => (\n <SelectItem key={`${group.kind}:${item.id}`} value={encodeSubject(group.kind, item.id)}>\n {item.name}\n </SelectItem>\n ))}\n </SelectGroup>\n ))}\n </SelectContent>\n </Select>\n </div>\n );\n}\n\n// ============================================================================\n// EntityPicker — generic account / household id picker\n// ============================================================================\n\nexport interface EntityPickerProps {\n kind: 'account' | 'household';\n value?: string | null;\n onChange: (id: string) => void;\n id?: string;\n label?: string;\n placeholder?: string;\n disabled?: boolean;\n}\n\nconst ENTITY_META: Record<\n EntityPickerProps['kind'],\n { label: string; placeholder: string; relativePath: string; cta: string; emptyMessage: string }\n> = {\n account: {\n label: 'Account',\n placeholder: 'Select an account',\n relativePath: 'accounts',\n cta: 'Create an account',\n emptyMessage: 'No accounts yet — create one first.',\n },\n household: {\n label: 'Household',\n placeholder: 'Select a household',\n relativePath: 'family',\n cta: 'Create a household',\n emptyMessage: 'No households yet — create one first.',\n },\n};\n\nexport function EntityPicker({\n kind,\n value,\n onChange,\n id,\n label,\n placeholder,\n disabled = false,\n}: EntityPickerProps) {\n const meta = ENTITY_META[kind];\n const accounts = useAccounts();\n const households = useHouseholds();\n const source = kind === 'account' ? accounts : households;\n const fieldId = id ?? `${kind}-picker`;\n const fieldLabel = label ?? meta.label;\n\n if (source.isLoading) return <PickerLoading id={fieldId} label={fieldLabel} />;\n\n const options = source.data.map((entity) => ({ id: entity.id, name: entity.name }));\n\n if (options.length === 0) {\n return (\n <div className=\"space-y-2\">\n {fieldLabel ? <Label htmlFor={fieldId}>{fieldLabel}</Label> : null}\n <PickerCTA message={meta.emptyMessage} cta={meta.cta} relativePath={meta.relativePath} />\n </div>\n );\n }\n\n return (\n <div className=\"space-y-2\">\n {fieldLabel ? <Label htmlFor={fieldId}>{fieldLabel}</Label> : null}\n <Select value={value || undefined} onValueChange={onChange} disabled={disabled}>\n <SelectTrigger id={fieldId} aria-label={fieldLabel}>\n <SelectValue placeholder={placeholder ?? meta.placeholder} />\n </SelectTrigger>\n <SelectContent>\n {options.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n );\n}\n\n/** Convenience wrapper — a required account picker. */\nexport function AccountPicker(props: Omit<EntityPickerProps, 'kind'>) {\n return <EntityPicker kind=\"account\" {...props} />;\n}\n\n// ============================================================================\n// HouseholdPicker — OPTIONAL household link (account form, item 6)\n// ============================================================================\n\nconst HOUSEHOLD_NONE = '__none__';\n\nexport interface HouseholdPickerProps {\n /** Current householdId, or null/undefined for \"no household\". */\n value?: string | null;\n onChange: (householdId: string | null) => void;\n id?: string;\n label?: string;\n /** Label for the \"unlinked\" option (default \"No household\"). */\n noneLabel?: string;\n disabled?: boolean;\n}\n\n/**\n * OPTIONAL household selector. Always offers a \"No household\" option, so it is\n * never a dead-end even when zero households exist (it just shows a hint). Emits\n * `null` for the none option and a bare householdId otherwise.\n */\nexport function HouseholdPicker({\n value,\n onChange,\n id = 'household-picker',\n label = 'Household',\n noneLabel = 'No household',\n disabled = false,\n}: HouseholdPickerProps) {\n const households = useHouseholds();\n\n if (households.isLoading) return <PickerLoading id={id} label={label} />;\n\n return (\n <div className=\"space-y-2\">\n {label ? <Label htmlFor={id}>{label}</Label> : null}\n <Select\n value={value ?? HOUSEHOLD_NONE}\n onValueChange={(next) => onChange(next === HOUSEHOLD_NONE ? null : next)}\n disabled={disabled}\n >\n <SelectTrigger id={id} aria-label={label}>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={HOUSEHOLD_NONE}>{noneLabel}</SelectItem>\n {households.data.map((household) => (\n <SelectItem key={household.id} value={household.id}>\n {household.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {households.data.length === 0 ? (\n <p className=\"text-xs text-text-secondary\">\n No households yet — you can create one in Households and link this account later.\n </p>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { useBrainyRich as e } from "../../providers/BrainyRichProvider.js";
|
|
2
|
+
import { formatDate as t } from "../../pages/_shared.js";
|
|
3
|
+
import { wspaceFetch as n, wspaceMutate as r } from "./transport.js";
|
|
4
|
+
import { useState as i } from "react";
|
|
5
|
+
import { MessagesSquare as a, Send as o, Trash2 as s } from "lucide-react";
|
|
6
|
+
import { useMutation as c, useQuery as l, useQueryClient as u } from "@tanstack/react-query";
|
|
7
|
+
import { jsx as d, jsxs as f } from "react/jsx-runtime";
|
|
8
|
+
import { Button as p, Skeleton as m, Textarea as h } from "@burdenoff/fe-libs/ui";
|
|
9
|
+
//#region src/brainyrich/components/reuse/CommentThread.tsx
|
|
10
|
+
var g = "\n id authorId authorType content { text markdown } createdAt edited status\n", _ = (e, t) => [
|
|
11
|
+
"brainyrich",
|
|
12
|
+
"entity-comments",
|
|
13
|
+
e,
|
|
14
|
+
t
|
|
15
|
+
], v = (t, r) => {
|
|
16
|
+
let { apiGatewayUrl: i } = e();
|
|
17
|
+
return l({
|
|
18
|
+
queryKey: _(t, r ?? ""),
|
|
19
|
+
enabled: !!r,
|
|
20
|
+
queryFn: async () => (await n(`query BraGetEntityComments($input: GetCommentsInput!) {
|
|
21
|
+
getEntityComments(input: $input) {
|
|
22
|
+
items { ${g} }
|
|
23
|
+
total
|
|
24
|
+
}
|
|
25
|
+
}`, { input: {
|
|
26
|
+
entityType: t,
|
|
27
|
+
entityId: r,
|
|
28
|
+
take: 50,
|
|
29
|
+
sortBy: "createdAt",
|
|
30
|
+
sortOrder: "desc"
|
|
31
|
+
} }, i)).getEntityComments
|
|
32
|
+
});
|
|
33
|
+
}, y = (t, n) => {
|
|
34
|
+
let { apiGatewayUrl: i } = e(), a = u();
|
|
35
|
+
return c({
|
|
36
|
+
mutationFn: async (e) => {
|
|
37
|
+
let a = e.trim();
|
|
38
|
+
if (!a) throw Error("A comment is required.");
|
|
39
|
+
return (await r("mutation BraCreateEntityComment($input: CreateEntityCommentInput!) {\n createEntityComment(input: $input) { id }\n }", { input: {
|
|
40
|
+
entityType: t,
|
|
41
|
+
entityId: n,
|
|
42
|
+
content: { text: a }
|
|
43
|
+
} }, i)).createEntityComment;
|
|
44
|
+
},
|
|
45
|
+
onSuccess: () => a.invalidateQueries({ queryKey: _(t, n) })
|
|
46
|
+
});
|
|
47
|
+
}, b = (t, n) => {
|
|
48
|
+
let { apiGatewayUrl: i } = e(), a = u();
|
|
49
|
+
return c({
|
|
50
|
+
mutationFn: async (e) => (await r("mutation BraDeleteEntityComment($input: DeleteCommentInput!) {\n deleteEntityComment(input: $input)\n }", { input: {
|
|
51
|
+
id: e,
|
|
52
|
+
soft: !0
|
|
53
|
+
} }, i)).deleteEntityComment,
|
|
54
|
+
onSuccess: () => a.invalidateQueries({ queryKey: _(t, n) })
|
|
55
|
+
});
|
|
56
|
+
}, x = (e, t) => t ? "You" : e.slice(0, 2).toUpperCase();
|
|
57
|
+
function S({ entityType: n, entityId: r, heading: c = "Discussion", hideHeading: l = !1 }) {
|
|
58
|
+
let { currentUser: u } = e(), [g, _] = i(""), S = v(n, r), C = y(n, r), w = b(n, r), T = (S.data?.items ?? []).filter((e) => e.status !== "DELETED"), E = u?.name || [u?.firstName, u?.lastName].filter(Boolean).join(" ") || u?.email || "You", D = async () => {
|
|
59
|
+
!g.trim() || C.isPending || (await C.mutateAsync(g), _(""));
|
|
60
|
+
};
|
|
61
|
+
return /* @__PURE__ */ f("div", {
|
|
62
|
+
className: "space-y-3",
|
|
63
|
+
children: [
|
|
64
|
+
!l && /* @__PURE__ */ f("h3", {
|
|
65
|
+
className: "flex items-center gap-2 text-sm font-semibold text-text-primary",
|
|
66
|
+
children: [
|
|
67
|
+
/* @__PURE__ */ d(a, {
|
|
68
|
+
className: "size-4",
|
|
69
|
+
"aria-hidden": "true"
|
|
70
|
+
}),
|
|
71
|
+
c,
|
|
72
|
+
S.data ? /* @__PURE__ */ f("span", {
|
|
73
|
+
className: "text-xs font-normal text-text-secondary",
|
|
74
|
+
children: [
|
|
75
|
+
"(",
|
|
76
|
+
T.length,
|
|
77
|
+
")"
|
|
78
|
+
]
|
|
79
|
+
}) : null
|
|
80
|
+
]
|
|
81
|
+
}),
|
|
82
|
+
/* @__PURE__ */ f("div", {
|
|
83
|
+
className: "space-y-2",
|
|
84
|
+
children: [
|
|
85
|
+
/* @__PURE__ */ d("label", {
|
|
86
|
+
htmlFor: `comment-${r}`,
|
|
87
|
+
className: "sr-only",
|
|
88
|
+
children: "Add a comment"
|
|
89
|
+
}),
|
|
90
|
+
/* @__PURE__ */ d(h, {
|
|
91
|
+
id: `comment-${r}`,
|
|
92
|
+
value: g,
|
|
93
|
+
onChange: (e) => _(e.target.value),
|
|
94
|
+
onKeyDown: (e) => {
|
|
95
|
+
(e.metaKey || e.ctrlKey) && e.key === "Enter" && (e.preventDefault(), D());
|
|
96
|
+
},
|
|
97
|
+
placeholder: "Add a note or rationale…",
|
|
98
|
+
rows: 3,
|
|
99
|
+
disabled: C.isPending
|
|
100
|
+
}),
|
|
101
|
+
/* @__PURE__ */ f("div", {
|
|
102
|
+
className: "flex items-center justify-between",
|
|
103
|
+
children: [/* @__PURE__ */ d("span", {
|
|
104
|
+
className: "text-xs text-text-secondary",
|
|
105
|
+
children: "⌘/Ctrl + Enter to post"
|
|
106
|
+
}), /* @__PURE__ */ f(p, {
|
|
107
|
+
type: "button",
|
|
108
|
+
size: "sm",
|
|
109
|
+
onClick: D,
|
|
110
|
+
disabled: !g.trim() || C.isPending,
|
|
111
|
+
children: [/* @__PURE__ */ d(o, {
|
|
112
|
+
className: "size-4",
|
|
113
|
+
"aria-hidden": "true"
|
|
114
|
+
}), C.isPending ? "Posting…" : "Post"]
|
|
115
|
+
})]
|
|
116
|
+
}),
|
|
117
|
+
C.isError && /* @__PURE__ */ d("p", {
|
|
118
|
+
className: "text-sm text-status-error-text",
|
|
119
|
+
children: "Could not post your comment. Please try again."
|
|
120
|
+
})
|
|
121
|
+
]
|
|
122
|
+
}),
|
|
123
|
+
S.isLoading ? /* @__PURE__ */ f("div", {
|
|
124
|
+
className: "space-y-3",
|
|
125
|
+
"aria-busy": "true",
|
|
126
|
+
children: [/* @__PURE__ */ d(m, { className: "h-16 w-full rounded-md" }), /* @__PURE__ */ d(m, { className: "h-16 w-full rounded-md" })]
|
|
127
|
+
}) : S.isError ? /* @__PURE__ */ f("p", {
|
|
128
|
+
className: "text-sm text-status-error-text",
|
|
129
|
+
children: [
|
|
130
|
+
"Could not load the discussion.",
|
|
131
|
+
" ",
|
|
132
|
+
/* @__PURE__ */ d("button", {
|
|
133
|
+
type: "button",
|
|
134
|
+
className: "underline underline-offset-2",
|
|
135
|
+
onClick: () => S.refetch(),
|
|
136
|
+
children: "Retry"
|
|
137
|
+
})
|
|
138
|
+
]
|
|
139
|
+
}) : T.length === 0 ? /* @__PURE__ */ f("div", {
|
|
140
|
+
className: "rounded-lg border border-dashed border-border-subtle bg-bg-surface px-4 py-8 text-center",
|
|
141
|
+
children: [
|
|
142
|
+
/* @__PURE__ */ d(a, {
|
|
143
|
+
className: "mx-auto mb-2 size-6 text-text-secondary",
|
|
144
|
+
"aria-hidden": "true"
|
|
145
|
+
}),
|
|
146
|
+
/* @__PURE__ */ d("p", {
|
|
147
|
+
className: "text-sm text-text-primary",
|
|
148
|
+
children: "No comments yet"
|
|
149
|
+
}),
|
|
150
|
+
/* @__PURE__ */ d("p", {
|
|
151
|
+
className: "mt-0.5 text-xs text-text-secondary",
|
|
152
|
+
children: "Start the discussion with a note or rationale."
|
|
153
|
+
})
|
|
154
|
+
]
|
|
155
|
+
}) : /* @__PURE__ */ d("ul", {
|
|
156
|
+
className: "space-y-3",
|
|
157
|
+
children: T.map((e) => {
|
|
158
|
+
let n = !!u?.id && e.authorId === u?.id, r = e.content.text ?? e.content.markdown ?? "";
|
|
159
|
+
return /* @__PURE__ */ f("li", {
|
|
160
|
+
className: "flex gap-3",
|
|
161
|
+
children: [/* @__PURE__ */ d("div", {
|
|
162
|
+
className: "flex size-8 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-xs font-medium text-text-secondary",
|
|
163
|
+
"aria-hidden": "true",
|
|
164
|
+
children: x(e.authorId, n)
|
|
165
|
+
}), /* @__PURE__ */ f("div", {
|
|
166
|
+
className: "min-w-0 flex-1 rounded-lg border border-border-subtle bg-bg-surface p-3",
|
|
167
|
+
children: [/* @__PURE__ */ f("div", {
|
|
168
|
+
className: "mb-1 flex items-center gap-2",
|
|
169
|
+
children: [
|
|
170
|
+
/* @__PURE__ */ d("span", {
|
|
171
|
+
className: "text-sm font-medium text-text-primary",
|
|
172
|
+
children: n ? E : "Team member"
|
|
173
|
+
}),
|
|
174
|
+
/* @__PURE__ */ d("span", {
|
|
175
|
+
className: "text-xs text-text-secondary",
|
|
176
|
+
children: t(e.createdAt)
|
|
177
|
+
}),
|
|
178
|
+
e.edited ? /* @__PURE__ */ d("span", {
|
|
179
|
+
className: "text-xs text-text-secondary",
|
|
180
|
+
children: "(edited)"
|
|
181
|
+
}) : null,
|
|
182
|
+
n && /* @__PURE__ */ d("button", {
|
|
183
|
+
type: "button",
|
|
184
|
+
"aria-label": "Delete comment",
|
|
185
|
+
className: "ml-auto rounded p-1 text-text-secondary hover:bg-bg-sunken hover:text-status-error-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-border-focus disabled:opacity-50",
|
|
186
|
+
disabled: w.isPending,
|
|
187
|
+
onClick: () => w.mutate(e.id),
|
|
188
|
+
children: /* @__PURE__ */ d(s, {
|
|
189
|
+
className: "size-3.5",
|
|
190
|
+
"aria-hidden": "true"
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
]
|
|
194
|
+
}), /* @__PURE__ */ d("p", {
|
|
195
|
+
className: "whitespace-pre-wrap break-words text-sm text-text-primary",
|
|
196
|
+
children: r
|
|
197
|
+
})]
|
|
198
|
+
})]
|
|
199
|
+
}, e.id);
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
]
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
//#endregion
|
|
206
|
+
export { S as CommentThread };
|
|
207
|
+
|
|
208
|
+
//# sourceMappingURL=CommentThread.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CommentThread.js","names":[],"sources":["../../../../src/brainyrich/components/reuse/CommentThread.tsx"],"sourcesContent":["/**\n * CommentThread — REUSE of `wspace-conversations-svc` EntityComment as a real\n * comment thread.\n * @module brainyrich/components/reuse/CommentThread\n *\n * Replaces the single-field notes / rationale fork on Households / Portfolios /\n * RebalanceProposals with a threaded, multi-author comment log keyed by\n * `(entityType, entityId)`.\n *\n * Why not the fe-libs `contextConversationWidget`? That folder ships a kit of\n * sub-components + Apollo hooks (subscriptions, `sendContextMessage`, a\n * conversationId-resolution flow) but exports NO mountable root widget and no\n * single mount hook — it is not wrappable as `<CommentThread>` without importing\n * its internals. Per the task's fallback, we build a minimal thread directly on\n * the platform EntityComment API, which is purpose-built for exactly this\n * \"comments on an arbitrary entity\" case:\n * - `getEntityComments(input)` → list (newest first)\n * - `createEntityComment(input)` → post\n * - `deleteEntityComment(input)` → remove own comment (soft)\n *\n * All ops route through the workspace supergraph the MFE already uses.\n * Presentational + self-contained: owns its hooks, reads gateway base URL +\n * current user from `useBrainyRich()`, uses fe-libs/ui + semantic tokens.\n */\n\nimport { useState, type KeyboardEvent } from 'react';\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\nimport { MessagesSquare, Send, Trash2 } from 'lucide-react';\nimport { Button, Skeleton, Textarea } from '@burdenoff/fe-libs/ui';\n\nimport { useBrainyRich } from '../../providers/BrainyRichProvider';\nimport { formatDate } from '../../pages/_shared';\nimport { wspaceFetch, wspaceMutate } from './transport';\n\ninterface EntityComment {\n id: string;\n authorId: string;\n authorType: string;\n content: { text: string | null; markdown: string | null };\n createdAt: string;\n edited: boolean;\n status: string;\n}\n\nconst COMMENT_FIELDS = `\n id authorId authorType content { text markdown } createdAt edited status\n`;\n\nconst commentsKey = (entityType: string, entityId: string) =>\n ['brainyrich', 'entity-comments', entityType, entityId] as const;\n\nconst useEntityComments = (entityType: string, entityId: string | undefined) => {\n const { apiGatewayUrl } = useBrainyRich();\n return useQuery({\n queryKey: commentsKey(entityType, entityId ?? ''),\n enabled: Boolean(entityId),\n queryFn: async () => {\n const data = await wspaceFetch<{ getEntityComments: { items: EntityComment[]; total: number } }>(\n `query BraGetEntityComments($input: GetCommentsInput!) {\n getEntityComments(input: $input) {\n items { ${COMMENT_FIELDS} }\n total\n }\n }`,\n {\n input: { entityType, entityId, take: 50, sortBy: 'createdAt', sortOrder: 'desc' },\n },\n apiGatewayUrl\n );\n return data.getEntityComments;\n },\n });\n};\n\nconst useCreateComment = (entityType: string, entityId: string) => {\n const { apiGatewayUrl } = useBrainyRich();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (text: string) => {\n const trimmed = text.trim();\n if (!trimmed) throw new Error('A comment is required.');\n const data = await wspaceMutate<{ createEntityComment: { id: string } }>(\n `mutation BraCreateEntityComment($input: CreateEntityCommentInput!) {\n createEntityComment(input: $input) { id }\n }`,\n { input: { entityType, entityId, content: { text: trimmed } } },\n apiGatewayUrl\n );\n return data.createEntityComment;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: commentsKey(entityType, entityId) }),\n });\n};\n\nconst useDeleteComment = (entityType: string, entityId: string) => {\n const { apiGatewayUrl } = useBrainyRich();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (id: string) => {\n const data = await wspaceMutate<{ deleteEntityComment: boolean }>(\n `mutation BraDeleteEntityComment($input: DeleteCommentInput!) {\n deleteEntityComment(input: $input)\n }`,\n { input: { id, soft: true } },\n apiGatewayUrl\n );\n return data.deleteEntityComment;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: commentsKey(entityType, entityId) }),\n });\n};\n\nconst authorInitials = (authorId: string, isSelf: boolean): string => {\n if (isSelf) return 'You';\n return authorId.slice(0, 2).toUpperCase();\n};\n\nexport interface CommentThreadProps {\n /** Polymorphic entity type, e.g. \"brainyrich-household\". */\n entityType: string;\n /** The bra entity's id (an ID scalar). */\n entityId: string;\n /** Optional heading; hide with `hideHeading`. */\n heading?: string;\n hideHeading?: boolean;\n}\n\nexport function CommentThread({\n entityType,\n entityId,\n heading = 'Discussion',\n hideHeading = false,\n}: CommentThreadProps) {\n const { currentUser } = useBrainyRich();\n const [draft, setDraft] = useState('');\n const comments = useEntityComments(entityType, entityId);\n const create = useCreateComment(entityType, entityId);\n const remove = useDeleteComment(entityType, entityId);\n\n const items = (comments.data?.items ?? []).filter((c) => c.status !== 'DELETED');\n const selfName =\n currentUser?.name ||\n [currentUser?.firstName, currentUser?.lastName].filter(Boolean).join(' ') ||\n currentUser?.email ||\n 'You';\n\n const submit = async () => {\n if (!draft.trim() || create.isPending) return;\n await create.mutateAsync(draft);\n setDraft('');\n };\n\n const onKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault();\n void submit();\n }\n };\n\n return (\n <div className=\"space-y-3\">\n {!hideHeading && (\n <h3 className=\"flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <MessagesSquare className=\"size-4\" aria-hidden=\"true\" />\n {heading}\n {comments.data ? (\n <span className=\"text-xs font-normal text-text-secondary\">({items.length})</span>\n ) : null}\n </h3>\n )}\n\n {/* Composer */}\n <div className=\"space-y-2\">\n <label htmlFor={`comment-${entityId}`} className=\"sr-only\">\n Add a comment\n </label>\n <Textarea\n id={`comment-${entityId}`}\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n onKeyDown={onKeyDown}\n placeholder=\"Add a note or rationale…\"\n rows={3}\n disabled={create.isPending}\n />\n <div className=\"flex items-center justify-between\">\n <span className=\"text-xs text-text-secondary\">⌘/Ctrl + Enter to post</span>\n <Button type=\"button\" size=\"sm\" onClick={submit} disabled={!draft.trim() || create.isPending}>\n <Send className=\"size-4\" aria-hidden=\"true\" />\n {create.isPending ? 'Posting…' : 'Post'}\n </Button>\n </div>\n {create.isError && (\n <p className=\"text-sm text-status-error-text\">Could not post your comment. Please try again.</p>\n )}\n </div>\n\n {/* Thread */}\n {comments.isLoading ? (\n <div className=\"space-y-3\" aria-busy=\"true\">\n <Skeleton className=\"h-16 w-full rounded-md\" />\n <Skeleton className=\"h-16 w-full rounded-md\" />\n </div>\n ) : comments.isError ? (\n <p className=\"text-sm text-status-error-text\">\n Could not load the discussion.{' '}\n <button\n type=\"button\"\n className=\"underline underline-offset-2\"\n onClick={() => comments.refetch()}\n >\n Retry\n </button>\n </p>\n ) : items.length === 0 ? (\n <div className=\"rounded-lg border border-dashed border-border-subtle bg-bg-surface px-4 py-8 text-center\">\n <MessagesSquare className=\"mx-auto mb-2 size-6 text-text-secondary\" aria-hidden=\"true\" />\n <p className=\"text-sm text-text-primary\">No comments yet</p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n Start the discussion with a note or rationale.\n </p>\n </div>\n ) : (\n <ul className=\"space-y-3\">\n {items.map((c) => {\n const isSelf = Boolean(currentUser?.id) && c.authorId === currentUser?.id;\n const body = c.content.text ?? c.content.markdown ?? '';\n return (\n <li key={c.id} className=\"flex gap-3\">\n <div\n className=\"flex size-8 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-xs font-medium text-text-secondary\"\n aria-hidden=\"true\"\n >\n {authorInitials(c.authorId, isSelf)}\n </div>\n <div className=\"min-w-0 flex-1 rounded-lg border border-border-subtle bg-bg-surface p-3\">\n <div className=\"mb-1 flex items-center gap-2\">\n <span className=\"text-sm font-medium text-text-primary\">\n {isSelf ? selfName : 'Team member'}\n </span>\n <span className=\"text-xs text-text-secondary\">{formatDate(c.createdAt)}</span>\n {c.edited ? <span className=\"text-xs text-text-secondary\">(edited)</span> : null}\n {isSelf && (\n <button\n type=\"button\"\n aria-label=\"Delete comment\"\n className=\"ml-auto rounded p-1 text-text-secondary hover:bg-bg-sunken hover:text-status-error-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-border-focus disabled:opacity-50\"\n disabled={remove.isPending}\n onClick={() => remove.mutate(c.id)}\n >\n <Trash2 className=\"size-3.5\" aria-hidden=\"true\" />\n </button>\n )}\n </div>\n <p className=\"whitespace-pre-wrap break-words text-sm text-text-primary\">{body}</p>\n </div>\n </li>\n );\n })}\n </ul>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;AA4CA,IAAM,IAAiB,kFAIjB,KAAe,GAAoB,MACvC;CAAC;CAAc;CAAmB;CAAY;CAAS,EAEnD,KAAqB,GAAoB,MAAiC;CAC9E,IAAM,EAAE,qBAAkB,GAAe;AACzC,QAAO,EAAS;EACd,UAAU,EAAY,GAAY,KAAY,GAAG;EACjD,SAAS,EAAQ;EACjB,SAAS,aACM,MAAM,EACjB;;sBAEc,EAAe;;;YAI7B,EACE,OAAO;GAAE;GAAY;GAAU,MAAM;GAAI,QAAQ;GAAa,WAAW;GAAQ,EAClF,EACD,EACD,EACW;EAEf,CAAC;GAGE,KAAoB,GAAoB,MAAqB;CACjE,IAAM,EAAE,qBAAkB,GAAe,EACnC,IAAK,GAAgB;AAC3B,QAAO,EAAY;EACjB,YAAY,OAAO,MAAiB;GAClC,IAAM,IAAU,EAAK,MAAM;AAC3B,OAAI,CAAC,EAAS,OAAU,MAAM,yBAAyB;AAQvD,WAPa,MAAM,EACjB,wIAGA,EAAE,OAAO;IAAE;IAAY;IAAU,SAAS,EAAE,MAAM,GAAS;IAAE,EAAE,EAC/D,EACD,EACW;;EAEd,iBAAiB,EAAG,kBAAkB,EAAE,UAAU,EAAY,GAAY,EAAS,EAAE,CAAC;EACvF,CAAC;GAGE,KAAoB,GAAoB,MAAqB;CACjE,IAAM,EAAE,qBAAkB,GAAe,EACnC,IAAK,GAAgB;AAC3B,QAAO,EAAY;EACjB,YAAY,OAAO,OACJ,MAAM,EACjB,2HAGA,EAAE,OAAO;GAAE;GAAI,MAAM;GAAM,EAAE,EAC7B,EACD,EACW;EAEd,iBAAiB,EAAG,kBAAkB,EAAE,UAAU,EAAY,GAAY,EAAS,EAAE,CAAC;EACvF,CAAC;GAGE,KAAkB,GAAkB,MACpC,IAAe,QACZ,EAAS,MAAM,GAAG,EAAE,CAAC,aAAa;AAa3C,SAAgB,EAAc,EAC5B,eACA,aACA,aAAU,cACV,iBAAc,MACO;CACrB,IAAM,EAAE,mBAAgB,GAAe,EACjC,CAAC,GAAO,KAAY,EAAS,GAAG,EAChC,IAAW,EAAkB,GAAY,EAAS,EAClD,IAAS,EAAiB,GAAY,EAAS,EAC/C,IAAS,EAAiB,GAAY,EAAS,EAE/C,KAAS,EAAS,MAAM,SAAS,EAAE,EAAE,QAAQ,MAAM,EAAE,WAAW,UAAU,EAC1E,IACJ,GAAa,QACb,CAAC,GAAa,WAAW,GAAa,SAAS,CAAC,OAAO,QAAQ,CAAC,KAAK,IAAI,IACzE,GAAa,SACb,OAEI,IAAS,YAAY;AACrB,GAAC,EAAM,MAAM,IAAI,EAAO,cAC5B,MAAM,EAAO,YAAY,EAAM,EAC/B,EAAS,GAAG;;AAUd,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACG,CAAC,KACA,kBAAC,MAAD;IAAI,WAAU;cAAd;KACE,kBAAC,GAAD;MAAgB,WAAU;MAAS,eAAY;MAAS,CAAA;KACvD;KACA,EAAS,OACR,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OAA0D;OAAE,EAAM;OAAO;OAAQ;UAC/E;KACD;;GAIP,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,SAAD;MAAO,SAAS,WAAW;MAAY,WAAU;gBAAU;MAEnD,CAAA;KACR,kBAAC,GAAD;MACE,IAAI,WAAW;MACf,OAAO;MACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;MAC9B,YA5BA,MAA8C;AAC/D,QAAK,EAAM,WAAW,EAAM,YAAY,EAAM,QAAQ,YACpD,EAAM,gBAAgB,EACjB,GAAQ;;MA0BT,aAAY;MACZ,MAAM;MACN,UAAU,EAAO;MACjB,CAAA;KACF,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,QAAD;OAAM,WAAU;iBAA8B;OAA6B,CAAA,EAC3E,kBAAC,GAAD;OAAQ,MAAK;OAAS,MAAK;OAAK,SAAS;OAAQ,UAAU,CAAC,EAAM,MAAM,IAAI,EAAO;iBAAnF,CACE,kBAAC,GAAD;QAAM,WAAU;QAAS,eAAY;QAAS,CAAA,EAC7C,EAAO,YAAY,aAAa,OAC1B;SACL;;KACL,EAAO,WACN,kBAAC,KAAD;MAAG,WAAU;gBAAiC;MAAkD,CAAA;KAE9F;;GAGL,EAAS,YACR,kBAAC,OAAD;IAAK,WAAU;IAAY,aAAU;cAArC,CACE,kBAAC,GAAD,EAAU,WAAU,0BAA2B,CAAA,EAC/C,kBAAC,GAAD,EAAU,WAAU,0BAA2B,CAAA,CAC3C;QACJ,EAAS,UACX,kBAAC,KAAD;IAAG,WAAU;cAAb;KAA8C;KACb;KAC/B,kBAAC,UAAD;MACE,MAAK;MACL,WAAU;MACV,eAAe,EAAS,SAAS;gBAClC;MAEQ,CAAA;KACP;QACF,EAAM,WAAW,IACnB,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MAAgB,WAAU;MAA0C,eAAY;MAAS,CAAA;KACzF,kBAAC,KAAD;MAAG,WAAU;gBAA4B;MAAmB,CAAA;KAC5D,kBAAC,KAAD;MAAG,WAAU;gBAAqC;MAE9C,CAAA;KACA;QAEN,kBAAC,MAAD;IAAI,WAAU;cACX,EAAM,KAAK,MAAM;KAChB,IAAM,IAAS,EAAQ,GAAa,MAAO,EAAE,aAAa,GAAa,IACjE,IAAO,EAAE,QAAQ,QAAQ,EAAE,QAAQ,YAAY;AACrD,YACE,kBAAC,MAAD;MAAe,WAAU;gBAAzB,CACE,kBAAC,OAAD;OACE,WAAU;OACV,eAAY;iBAEX,EAAe,EAAE,UAAU,EAAO;OAC/B,CAAA,EACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf;SACE,kBAAC,QAAD;UAAM,WAAU;oBACb,IAAS,IAAW;UAChB,CAAA;SACP,kBAAC,QAAD;UAAM,WAAU;oBAA+B,EAAW,EAAE,UAAU;UAAQ,CAAA;SAC7E,EAAE,SAAS,kBAAC,QAAD;UAAM,WAAU;oBAA8B;UAAe,CAAA,GAAG;SAC3E,KACC,kBAAC,UAAD;UACE,MAAK;UACL,cAAW;UACX,WAAU;UACV,UAAU,EAAO;UACjB,eAAe,EAAO,OAAO,EAAE,GAAG;oBAElC,kBAAC,GAAD;WAAQ,WAAU;WAAW,eAAY;WAAS,CAAA;UAC3C,CAAA;SAEP;WACN,kBAAC,KAAD;QAAG,WAAU;kBAA6D;QAAS,CAAA,CAC/E;SACH;QA5BI,EAAE,GA4BN;MAEP;IACC,CAAA;GAEH"}
|