@burdenoff/microfe-proserviceworld 2026.706.2 → 2026.706.3

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.
Files changed (26) hide show
  1. package/dist/proserviceworld/components/DiscussionThread.js +1 -1
  2. package/dist/proserviceworld/components/DiscussionThread.js.map +1 -1
  3. package/dist/proserviceworld/components/ui.js +18 -18
  4. package/dist/proserviceworld/components/ui.js.map +1 -1
  5. package/dist/proserviceworld/hooks/useEntityComments.js.map +1 -1
  6. package/dist/proserviceworld/pages/AnalyticsPage.js +277 -111
  7. package/dist/proserviceworld/pages/AnalyticsPage.js.map +1 -1
  8. package/dist/proserviceworld/pages/ClientDetailPage.js +219 -151
  9. package/dist/proserviceworld/pages/ClientDetailPage.js.map +1 -1
  10. package/dist/proserviceworld/pages/ClientsPage.js +148 -127
  11. package/dist/proserviceworld/pages/ClientsPage.js.map +1 -1
  12. package/dist/proserviceworld/pages/CompliancePage.js +140 -96
  13. package/dist/proserviceworld/pages/CompliancePage.js.map +1 -1
  14. package/dist/proserviceworld/pages/DashboardPage.js +221 -104
  15. package/dist/proserviceworld/pages/DashboardPage.js.map +1 -1
  16. package/dist/proserviceworld/pages/EngagementDetailPage.js +211 -181
  17. package/dist/proserviceworld/pages/EngagementDetailPage.js.map +1 -1
  18. package/dist/proserviceworld/pages/EngagementsPage.js +144 -106
  19. package/dist/proserviceworld/pages/EngagementsPage.js.map +1 -1
  20. package/dist/proserviceworld/pages/InvoicesPage.js +144 -129
  21. package/dist/proserviceworld/pages/InvoicesPage.js.map +1 -1
  22. package/dist/proserviceworld/pages/MattersPage.js +163 -122
  23. package/dist/proserviceworld/pages/MattersPage.js.map +1 -1
  24. package/dist/proserviceworld/pages/TimePage.js +112 -92
  25. package/dist/proserviceworld/pages/TimePage.js.map +1 -1
  26. package/package.json +1 -1
@@ -45,7 +45,7 @@ var h = ({ entityType: h, entityId: g, title: _ = "Discussion" }) => {
45
45
  }),
46
46
  b ? /* @__PURE__ */ f(r, { label: "Loading discussion…" }) : x ? /* @__PURE__ */ f("p", {
47
47
  className: "text-sm text-status-error-text",
48
- children: "Couldn't load discussion."
48
+ children: "Could not load discussion."
49
49
  }) : !y || y.items.length === 0 ? /* @__PURE__ */ f(n, {
50
50
  title: "No notes yet",
51
51
  description: "Start the discussion for this record.",
@@ -1 +1 @@
1
- {"version":3,"file":"DiscussionThread.js","names":[],"sources":["../../../src/proserviceworld/components/DiscussionThread.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useState } from 'react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { MessageSquare, Send } from 'lucide-react';\nimport { useEntityComments, useAddEntityComment } from '../hooks/useEntityComments';\nimport { useProServiceWorld } from '../providers/ProServiceWorldProvider';\nimport { SectionCard, TextArea, Banner, LoadingBlock, Empty, fmtDate } from './ui';\n\n/**\n * Discussion / notes for a Psw entity, backed entirely by wspace-conversations\n * (createEntityComment / getEntityComments) — no product-owned comments table.\n */\nexport const DiscussionThread: FC<{ entityType: string; entityId: string; title?: string }> = ({\n entityType,\n entityId,\n title = 'Discussion',\n}) => {\n const { currentUser } = useProServiceWorld();\n const { data, isLoading, error } = useEntityComments(entityType, entityId);\n const add = useAddEntityComment(entityType, entityId);\n const [text, setText] = useState('');\n const [err, setErr] = useState<string | null>(null);\n\n const submit = () => {\n setErr(null);\n const body = text.trim();\n if (!body) return;\n add.mutate(body, { onSuccess: () => setText(''), onError: (e) => setErr(e instanceof Error ? e.message : 'Failed to post.') });\n };\n\n const authorLabel = (authorId: string) =>\n currentUser && authorId === currentUser.id ? 'You' : 'Team member';\n\n return (\n <SectionCard title={title} description=\"Notes & discussion — shared across the platform via Conversations.\">\n <div className=\"space-y-3\">\n {err && <Banner tone=\"danger\" onClose={() => setErr(null)}>{err}</Banner>}\n <div className=\"flex items-start gap-2\">\n <TextArea\n value={text}\n onChange={(e) => setText(e.target.value)}\n placeholder=\"Add a note…\"\n className=\"min-h-[44px] flex-1\"\n onKeyDown={(e) => {\n if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') submit();\n }}\n />\n <Button onClick={submit} disabled={add.isPending || !text.trim()} aria-label=\"Post note\">\n <Send className=\"h-4 w-4\" />\n </Button>\n </div>\n\n {isLoading ? (\n <LoadingBlock label=\"Loading discussion…\" />\n ) : error ? (\n <p className=\"text-sm text-status-error-text\">Couldn't load discussion.</p>\n ) : !data || data.items.length === 0 ? (\n <Empty title=\"No notes yet\" description=\"Start the discussion for this record.\" icon={<MessageSquare className=\"h-7 w-7\" />} />\n ) : (\n <ul className=\"space-y-2.5\">\n {data.items.map((c) => (\n <li key={c.id} className=\"rounded-lg border border-border-seam bg-bg-canvas p-3\">\n <div className=\"mb-1 flex items-center justify-between text-xs text-text-tertiary\">\n <span className=\"font-medium text-text-secondary\">{authorLabel(c.authorId)}</span>\n <span>{fmtDate(c.createdAt)}</span>\n </div>\n <p className=\"whitespace-pre-wrap text-sm text-text-primary\">{c.content?.text ?? ''}</p>\n </li>\n ))}\n </ul>\n )}\n </div>\n </SectionCard>\n );\n};\n"],"mappings":";;;;;;;;AAYA,IAAa,KAAkF,EAC7F,eACA,aACA,WAAQ,mBACJ;CACJ,IAAM,EAAE,mBAAgB,GAAoB,EACtC,EAAE,SAAM,cAAW,aAAU,EAAkB,GAAY,EAAS,EACpE,IAAM,EAAoB,GAAY,EAAS,EAC/C,CAAC,GAAM,KAAW,EAAS,GAAG,EAC9B,CAAC,GAAK,KAAU,EAAwB,KAAK,EAE7C,UAAe;AACnB,IAAO,KAAK;EACZ,IAAM,IAAO,EAAK,MAAM;AACnB,OACL,EAAI,OAAO,GAAM;GAAE,iBAAiB,EAAQ,GAAG;GAAE,UAAU,MAAM,EAAO,aAAa,QAAQ,EAAE,UAAU,kBAAkB;GAAE,CAAC;IAG1H,KAAe,MACnB,KAAe,MAAa,EAAY,KAAK,QAAQ;AAEvD,QACE,kBAAC,GAAD;EAAoB;EAAO,aAAY;YACrC,kBAAC,OAAD;GAAK,WAAU;aAAf;IACG,KAAO,kBAAC,GAAD;KAAQ,MAAK;KAAS,eAAe,EAAO,KAAK;eAAG;KAAa,CAAA;IACzE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MACE,OAAO;MACP,WAAW,MAAM,EAAQ,EAAE,OAAO,MAAM;MACxC,aAAY;MACZ,WAAU;MACV,YAAY,MAAM;AAChB,QAAK,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,WAAS,GAAQ;;MAE7D,CAAA,EACF,kBAAC,GAAD;MAAQ,SAAS;MAAQ,UAAU,EAAI,aAAa,CAAC,EAAK,MAAM;MAAE,cAAW;gBAC3E,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA;MACrB,CAAA,CACL;;IAEL,IACC,kBAAC,GAAD,EAAc,OAAM,uBAAwB,CAAA,GAC1C,IACF,kBAAC,KAAD;KAAG,WAAU;eAAiC;KAA6B,CAAA,GACzE,CAAC,KAAQ,EAAK,MAAM,WAAW,IACjC,kBAAC,GAAD;KAAO,OAAM;KAAe,aAAY;KAAwC,MAAM,kBAAC,GAAD,EAAe,WAAU,WAAY,CAAA;KAAI,CAAA,GAE/H,kBAAC,MAAD;KAAI,WAAU;eACX,EAAK,MAAM,KAAK,MACf,kBAAC,MAAD;MAAe,WAAU;gBAAzB,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QAAM,WAAU;kBAAmC,EAAY,EAAE,SAAS;QAAQ,CAAA,EAClF,kBAAC,QAAD,EAAA,UAAO,EAAQ,EAAE,UAAU,EAAQ,CAAA,CAC/B;UACN,kBAAC,KAAD;OAAG,WAAU;iBAAiD,EAAE,SAAS,QAAQ;OAAO,CAAA,CACrF;QANI,EAAE,GAMN,CACL;KACC,CAAA;IAEH;;EACM,CAAA"}
1
+ {"version":3,"file":"DiscussionThread.js","names":[],"sources":["../../../src/proserviceworld/components/DiscussionThread.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useState } from 'react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { MessageSquare, Send } from 'lucide-react';\nimport { useEntityComments, useAddEntityComment } from '../hooks/useEntityComments';\nimport { useProServiceWorld } from '../providers/ProServiceWorldProvider';\nimport { SectionCard, TextArea, Banner, LoadingBlock, Empty, fmtDate } from './ui';\n\n/**\n * Discussion / notes for a Psw entity, backed entirely by wspace-conversations\n * (createEntityComment / getEntityComments) — no product-owned comments table.\n */\nexport const DiscussionThread: FC<{ entityType: string; entityId: string; title?: string }> = ({\n entityType,\n entityId,\n title = 'Discussion',\n}) => {\n const { currentUser } = useProServiceWorld();\n const { data, isLoading, error } = useEntityComments(entityType, entityId);\n const add = useAddEntityComment(entityType, entityId);\n const [text, setText] = useState('');\n const [err, setErr] = useState<string | null>(null);\n\n const submit = () => {\n setErr(null);\n const body = text.trim();\n if (!body) return;\n add.mutate(body, {\n onSuccess: () => setText(''),\n onError: (e) => setErr(e instanceof Error ? e.message : 'Failed to post.'),\n });\n };\n\n const authorLabel = (authorId: string) =>\n currentUser && authorId === currentUser.id ? 'You' : 'Team member';\n\n return (\n <SectionCard\n title={title}\n description=\"Notes & discussion — shared across the platform via Conversations.\"\n >\n <div className=\"space-y-3\">\n {err && (\n <Banner tone=\"danger\" onClose={() => setErr(null)}>\n {err}\n </Banner>\n )}\n <div className=\"flex items-start gap-2\">\n <TextArea\n value={text}\n onChange={(e) => setText(e.target.value)}\n placeholder=\"Add a note…\"\n className=\"min-h-[44px] flex-1\"\n onKeyDown={(e) => {\n if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') submit();\n }}\n />\n <Button onClick={submit} disabled={add.isPending || !text.trim()} aria-label=\"Post note\">\n <Send className=\"h-4 w-4\" />\n </Button>\n </div>\n\n {isLoading ? (\n <LoadingBlock label=\"Loading discussion…\" />\n ) : error ? (\n <p className=\"text-sm text-status-error-text\">Could not load discussion.</p>\n ) : !data || data.items.length === 0 ? (\n <Empty\n title=\"No notes yet\"\n description=\"Start the discussion for this record.\"\n icon={<MessageSquare className=\"h-7 w-7\" />}\n />\n ) : (\n <ul className=\"space-y-2.5\">\n {data.items.map((c) => (\n <li key={c.id} className=\"rounded-lg border border-border-seam bg-bg-canvas p-3\">\n <div className=\"mb-1 flex items-center justify-between text-xs text-text-tertiary\">\n <span className=\"font-medium text-text-secondary\">{authorLabel(c.authorId)}</span>\n <span>{fmtDate(c.createdAt)}</span>\n </div>\n <p className=\"whitespace-pre-wrap text-sm text-text-primary\">\n {c.content?.text ?? ''}\n </p>\n </li>\n ))}\n </ul>\n )}\n </div>\n </SectionCard>\n );\n};\n"],"mappings":";;;;;;;;AAYA,IAAa,KAAkF,EAC7F,eACA,aACA,WAAQ,mBACJ;CACJ,IAAM,EAAE,mBAAgB,GAAoB,EACtC,EAAE,SAAM,cAAW,aAAU,EAAkB,GAAY,EAAS,EACpE,IAAM,EAAoB,GAAY,EAAS,EAC/C,CAAC,GAAM,KAAW,EAAS,GAAG,EAC9B,CAAC,GAAK,KAAU,EAAwB,KAAK,EAE7C,UAAe;AACnB,IAAO,KAAK;EACZ,IAAM,IAAO,EAAK,MAAM;AACnB,OACL,EAAI,OAAO,GAAM;GACf,iBAAiB,EAAQ,GAAG;GAC5B,UAAU,MAAM,EAAO,aAAa,QAAQ,EAAE,UAAU,kBAAkB;GAC3E,CAAC;IAGE,KAAe,MACnB,KAAe,MAAa,EAAY,KAAK,QAAQ;AAEvD,QACE,kBAAC,GAAD;EACS;EACP,aAAY;YAEZ,kBAAC,OAAD;GAAK,WAAU;aAAf;IACG,KACC,kBAAC,GAAD;KAAQ,MAAK;KAAS,eAAe,EAAO,KAAK;eAC9C;KACM,CAAA;IAEX,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MACE,OAAO;MACP,WAAW,MAAM,EAAQ,EAAE,OAAO,MAAM;MACxC,aAAY;MACZ,WAAU;MACV,YAAY,MAAM;AAChB,QAAK,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,WAAS,GAAQ;;MAE7D,CAAA,EACF,kBAAC,GAAD;MAAQ,SAAS;MAAQ,UAAU,EAAI,aAAa,CAAC,EAAK,MAAM;MAAE,cAAW;gBAC3E,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA;MACrB,CAAA,CACL;;IAEL,IACC,kBAAC,GAAD,EAAc,OAAM,uBAAwB,CAAA,GAC1C,IACF,kBAAC,KAAD;KAAG,WAAU;eAAiC;KAA8B,CAAA,GAC1E,CAAC,KAAQ,EAAK,MAAM,WAAW,IACjC,kBAAC,GAAD;KACE,OAAM;KACN,aAAY;KACZ,MAAM,kBAAC,GAAD,EAAe,WAAU,WAAY,CAAA;KAC3C,CAAA,GAEF,kBAAC,MAAD;KAAI,WAAU;eACX,EAAK,MAAM,KAAK,MACf,kBAAC,MAAD;MAAe,WAAU;gBAAzB,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QAAM,WAAU;kBAAmC,EAAY,EAAE,SAAS;QAAQ,CAAA,EAClF,kBAAC,QAAD,EAAA,UAAO,EAAQ,EAAE,UAAU,EAAQ,CAAA,CAC/B;UACN,kBAAC,KAAD;OAAG,WAAU;iBACV,EAAE,SAAS,QAAQ;OAClB,CAAA,CACD;QARI,EAAE,GAQN,CACL;KACC,CAAA;IAEH;;EACM,CAAA"}
@@ -69,21 +69,21 @@ var u = (e) => (e ?? "").toLowerCase().replace(/_/g, " ").replace(/\b\w/g, (e) =
69
69
  POTENTIAL_CONFLICT: "warning",
70
70
  CONFLICT: "danger",
71
71
  COMPLETED: "success"
72
- }, _ = ({ status: t, className: n }) => /* @__PURE__ */ s("span", {
73
- className: e("inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium whitespace-nowrap", h[g[t] ?? "neutral"], n),
74
- children: u(t)
72
+ }, _ = ({ status: t, className: n }) => /* @__PURE__ */ c("span", {
73
+ className: e("inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium whitespace-nowrap", h[g[t] ?? "neutral"], n),
74
+ children: [/* @__PURE__ */ s("span", { className: "h-1.5 w-1.5 rounded-full bg-current opacity-70" }), u(t)]
75
75
  }), v = ({ title: e, subtitle: t, actions: n, icon: r }) => /* @__PURE__ */ c("div", {
76
76
  className: "flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between",
77
77
  children: [/* @__PURE__ */ c("div", {
78
- className: "flex items-start gap-3",
78
+ className: "flex items-start gap-3.5",
79
79
  children: [r ? /* @__PURE__ */ s("div", {
80
- className: "mt-0.5 text-accent",
80
+ className: "flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-bg-sunken text-accent",
81
81
  children: r
82
82
  }) : null, /* @__PURE__ */ c("div", { children: [/* @__PURE__ */ s("h1", {
83
- className: "text-xl font-semibold text-text-primary sm:text-2xl",
83
+ className: "text-2xl font-bold tracking-tight text-text-primary sm:text-3xl",
84
84
  children: e
85
85
  }), t ? /* @__PURE__ */ s("p", {
86
- className: "mt-1 max-w-2xl text-sm text-text-secondary",
86
+ className: "mt-1.5 max-w-2xl text-sm leading-relaxed text-text-secondary",
87
87
  children: t
88
88
  }) : null] })]
89
89
  }), n ? /* @__PURE__ */ s("div", {
@@ -91,20 +91,20 @@ var u = (e) => (e ?? "").toLowerCase().replace(/_/g, " ").replace(/\b\w/g, (e) =
91
91
  children: n
92
92
  }) : null]
93
93
  }), y = ({ label: t, value: n, hint: r, icon: i, tone: a = "neutral" }) => /* @__PURE__ */ c("div", {
94
- className: "rounded-xl border border-border-seam bg-bg-surface p-4",
94
+ className: "vc-lift-on-hover rounded-2xl border border-border-seam bg-bg-surface p-5 shadow-sm",
95
95
  children: [
96
96
  /* @__PURE__ */ c("div", {
97
- className: "flex items-center justify-between",
97
+ className: "flex items-center justify-between gap-2",
98
98
  children: [/* @__PURE__ */ s("span", {
99
- className: "text-xs font-medium uppercase tracking-wide text-text-tertiary",
99
+ className: "text-xs font-medium uppercase tracking-wider text-text-tertiary",
100
100
  children: t
101
101
  }), i ? /* @__PURE__ */ s("span", {
102
- className: e("rounded-lg p-1.5", h[a]),
102
+ className: e("flex h-9 w-9 items-center justify-center rounded-full", h[a]),
103
103
  children: i
104
104
  }) : null]
105
105
  }),
106
106
  /* @__PURE__ */ s("div", {
107
- className: "mt-2 text-2xl font-semibold text-text-primary",
107
+ className: "mt-3 text-2xl font-semibold tracking-tight text-text-primary sm:text-3xl",
108
108
  children: n
109
109
  }),
110
110
  r ? /* @__PURE__ */ s("div", {
@@ -113,18 +113,18 @@ var u = (e) => (e ?? "").toLowerCase().replace(/_/g, " ").replace(/\b\w/g, (e) =
113
113
  }) : null
114
114
  ]
115
115
  }), b = ({ title: t, description: n, action: r, children: i, className: a }) => /* @__PURE__ */ c("section", {
116
- className: e("rounded-xl border border-border-seam bg-bg-surface", a),
116
+ className: e("rounded-2xl border border-border-seam bg-bg-surface shadow-sm", a),
117
117
  children: [(t || r) && /* @__PURE__ */ c("header", {
118
- className: "flex items-center justify-between gap-3 border-b border-border-seam px-4 py-3",
118
+ className: "flex items-center justify-between gap-3 border-b border-border-seam px-5 py-3.5",
119
119
  children: [/* @__PURE__ */ c("div", { children: [t ? /* @__PURE__ */ s("h2", {
120
120
  className: "text-sm font-semibold text-text-primary",
121
121
  children: t
122
122
  }) : null, n ? /* @__PURE__ */ s("p", {
123
- className: "text-xs text-text-tertiary",
123
+ className: "mt-0.5 text-xs text-text-tertiary",
124
124
  children: n
125
125
  }) : null] }), r]
126
126
  }), /* @__PURE__ */ s("div", {
127
- className: "p-4",
127
+ className: "p-5",
128
128
  children: i
129
129
  })]
130
130
  }), x = "w-full rounded-lg border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary placeholder:text-text-tertiary focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/30 disabled:opacity-50", S = ({ label: t, required: n, hint: r, children: i, className: a }) => /* @__PURE__ */ c("label", {
@@ -239,10 +239,10 @@ var u = (e) => (e ?? "").toLowerCase().replace(/_/g, " ").replace(/\b\w/g, (e) =
239
239
  }) : null
240
240
  ]
241
241
  }), j = ({ title: e, description: t, icon: n, action: r }) => /* @__PURE__ */ c("div", {
242
- className: "flex min-h-[220px] flex-col items-center justify-center gap-3 rounded-xl border border-dashed border-border-seam bg-bg-surface/50 px-6 py-10 text-center",
242
+ className: "flex min-h-[240px] flex-col items-center justify-center gap-4 rounded-2xl border border-dashed border-border-seam bg-bg-surface/50 px-6 py-12 text-center",
243
243
  children: [
244
244
  n ? /* @__PURE__ */ s("div", {
245
- className: "text-text-tertiary",
245
+ className: "flex h-14 w-14 items-center justify-center rounded-2xl bg-bg-sunken text-text-tertiary",
246
246
  children: n
247
247
  }) : null,
248
248
  /* @__PURE__ */ c("div", { children: [/* @__PURE__ */ s("p", {
@@ -1 +1 @@
1
- {"version":3,"file":"ui.js","names":[],"sources":["../../../src/proserviceworld/components/ui.tsx"],"sourcesContent":["/**\n * ProServiceWorld shared UI kit.\n *\n * Self-contained, token-driven primitives (no raw colors) shared by every page:\n * page header, metric cards, section cards, a mobile-friendly modal, token-styled\n * native form controls, status pills, inline banners, and empty/loading/error\n * states. Actions reuse the fe-libs Button (brand variants); everything else is\n * built from semantic tokens so theming works across light/dark/any theme.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { useEffect } from 'react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { AlertCircle, CheckCircle2, Info, Loader2, X } from 'lucide-react';\nimport { cn } from '../utils/cn';\n\n// ---------------------------------------------------------------------------\n// Formatters\n// ---------------------------------------------------------------------------\nexport const humanize = (v: string | null | undefined): string =>\n (v ?? '')\n .toLowerCase()\n .replace(/_/g, ' ')\n .replace(/\\b\\w/g, (c) => c.toUpperCase());\n\nexport const fmtDate = (iso: string | null | undefined): string => {\n if (!iso) return '—';\n const d = new Date(iso);\n return Number.isNaN(d.getTime())\n ? '—'\n : d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });\n};\n\nexport const fmtMoney = (amount: number | null | undefined, currency = 'USD'): string => {\n const n = amount ?? 0;\n try {\n return new Intl.NumberFormat(undefined, {\n style: 'currency',\n currency,\n maximumFractionDigits: 0,\n }).format(n);\n } catch {\n return `${currency} ${n.toLocaleString()}`;\n }\n};\n\nexport const fmtHours = (h: number | null | undefined): string =>\n `${(h ?? 0).toLocaleString(undefined, { maximumFractionDigits: 1 })}h`;\nexport const fmtMinutesAsHours = (m: number | null | undefined): string => fmtHours((m ?? 0) / 60);\n\n// ---------------------------------------------------------------------------\n// Status tone mapping\n// ---------------------------------------------------------------------------\nexport type Tone = 'neutral' | 'success' | 'warning' | 'danger' | 'info' | 'accent';\n\nconst TONE_CLASS: Record<Tone, string> = {\n neutral: 'bg-bg-sunken text-text-secondary',\n success: 'bg-status-success-bg-subtle text-status-success-text',\n warning: 'bg-status-warning-bg-subtle text-status-warning-text',\n danger: 'bg-status-error-bg-subtle text-status-error-text',\n info: 'bg-bg-sunken text-text-link',\n accent: 'bg-bg-sunken text-accent',\n};\n\nconst STATUS_TONE: Record<string, Tone> = {\n // clients / generic\n ACTIVE: 'success',\n PROSPECT: 'info',\n INACTIVE: 'neutral',\n ARCHIVED: 'neutral',\n // engagements\n PROPOSAL: 'info',\n ACCEPTED: 'accent',\n IN_PROGRESS: 'accent',\n REVIEW: 'warning',\n DELIVERED: 'success',\n INVOICED: 'success',\n CLOSED: 'neutral',\n CANCELLED: 'danger',\n // matters\n OPEN: 'accent',\n ON_HOLD: 'warning',\n // priority\n LOW: 'neutral',\n MEDIUM: 'info',\n HIGH: 'warning',\n CRITICAL: 'danger',\n // deliverable / doc\n DRAFT: 'neutral',\n INTERNAL_REVIEW: 'info',\n CLIENT_REVIEW: 'warning',\n REJECTED: 'danger',\n FINAL: 'success',\n // time\n SUBMITTED: 'info',\n APPROVED: 'success',\n BILLED: 'accent',\n // invoice\n SENT: 'info',\n PAID: 'success',\n OVERDUE: 'danger',\n // compliance / conflict\n PENDING: 'warning',\n IN_REVIEW: 'info',\n PASSED: 'success',\n FAILED: 'danger',\n WAIVED: 'neutral',\n CLEAR: 'success',\n POTENTIAL_CONFLICT: 'warning',\n CONFLICT: 'danger',\n // milestone\n COMPLETED: 'success',\n};\n\nexport const StatusPill: FC<{ status: string; className?: string }> = ({ status, className }) => (\n <span\n className={cn(\n 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium whitespace-nowrap',\n TONE_CLASS[STATUS_TONE[status] ?? 'neutral'],\n className\n )}\n >\n {humanize(status)}\n </span>\n);\n\n// ---------------------------------------------------------------------------\n// Page header\n// ---------------------------------------------------------------------------\nexport const PageHeader: FC<{\n title: string;\n subtitle?: string;\n actions?: ReactNode;\n icon?: ReactNode;\n}> = ({ title, subtitle, actions, icon }) => (\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"flex items-start gap-3\">\n {icon ? <div className=\"mt-0.5 text-accent\">{icon}</div> : null}\n <div>\n <h1 className=\"text-xl font-semibold text-text-primary sm:text-2xl\">{title}</h1>\n {subtitle ? <p className=\"mt-1 max-w-2xl text-sm text-text-secondary\">{subtitle}</p> : null}\n </div>\n </div>\n {actions ? <div className=\"flex flex-wrap items-center gap-2\">{actions}</div> : null}\n </div>\n);\n\n// ---------------------------------------------------------------------------\n// Metric card\n// ---------------------------------------------------------------------------\nexport const MetricCard: FC<{\n label: string;\n value: ReactNode;\n hint?: string;\n icon?: ReactNode;\n tone?: Tone;\n}> = ({ label, value, hint, icon, tone = 'neutral' }) => (\n <div className=\"rounded-xl border border-border-seam bg-bg-surface p-4\">\n <div className=\"flex items-center justify-between\">\n <span className=\"text-xs font-medium uppercase tracking-wide text-text-tertiary\">\n {label}\n </span>\n {icon ? <span className={cn('rounded-lg p-1.5', TONE_CLASS[tone])}>{icon}</span> : null}\n </div>\n <div className=\"mt-2 text-2xl font-semibold text-text-primary\">{value}</div>\n {hint ? <div className=\"mt-1 text-xs text-text-tertiary\">{hint}</div> : null}\n </div>\n);\n\n// ---------------------------------------------------------------------------\n// Section card\n// ---------------------------------------------------------------------------\nexport const SectionCard: FC<{\n title?: string;\n description?: string;\n action?: ReactNode;\n children: ReactNode;\n className?: string;\n}> = ({ title, description, action, children, className }) => (\n <section className={cn('rounded-xl border border-border-seam bg-bg-surface', className)}>\n {(title || action) && (\n <header className=\"flex items-center justify-between gap-3 border-b border-border-seam px-4 py-3\">\n <div>\n {title ? <h2 className=\"text-sm font-semibold text-text-primary\">{title}</h2> : null}\n {description ? <p className=\"text-xs text-text-tertiary\">{description}</p> : null}\n </div>\n {action}\n </header>\n )}\n <div className=\"p-4\">{children}</div>\n </section>\n);\n\n// ---------------------------------------------------------------------------\n// Form controls (token-styled native elements — reliable inside modals)\n// ---------------------------------------------------------------------------\nconst controlClass =\n 'w-full rounded-lg border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary placeholder:text-text-tertiary focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/30 disabled:opacity-50';\n\nexport const Field: FC<{\n label: string;\n required?: boolean;\n hint?: string;\n children: ReactNode;\n className?: string;\n}> = ({ label, required, hint, children, className }) => (\n <label className={cn('block', className)}>\n <span className=\"mb-1 block text-xs font-medium text-text-secondary\">\n {label}\n {required ? <span className=\"text-status-error-text\"> *</span> : null}\n </span>\n {children}\n {hint ? <span className=\"mt-1 block text-xs text-text-tertiary\">{hint}</span> : null}\n </label>\n);\n\nexport const TextInput: FC<React.InputHTMLAttributes<HTMLInputElement>> = ({\n className,\n ...props\n}) => <input className={cn(controlClass, className)} {...props} />;\nexport const TextArea: FC<React.TextareaHTMLAttributes<HTMLTextAreaElement>> = ({\n className,\n ...props\n}) => <textarea className={cn(controlClass, 'min-h-[80px] resize-y', className)} {...props} />;\nexport const NativeSelect: FC<React.SelectHTMLAttributes<HTMLSelectElement>> = ({\n className,\n children,\n ...props\n}) => (\n <select className={cn(controlClass, 'appearance-none pr-8')} {...props}>\n {children}\n </select>\n);\n\n/** Turn an enum-value list into <option>s with humanized labels. */\nexport const enumOptions = (values: readonly string[]): ReactNode =>\n values.map((v) => (\n <option key={v} value={v}>\n {humanize(v)}\n </option>\n ));\n\n// ---------------------------------------------------------------------------\n// Inline banner\n// ---------------------------------------------------------------------------\nexport const Banner: FC<{\n tone?: 'success' | 'danger' | 'info';\n children: ReactNode;\n onClose?: () => void;\n}> = ({ tone = 'info', children, onClose }) => {\n const Icon = tone === 'success' ? CheckCircle2 : tone === 'danger' ? AlertCircle : Info;\n const toneClass =\n tone === 'success'\n ? TONE_CLASS.success\n : tone === 'danger'\n ? TONE_CLASS.danger\n : 'bg-bg-sunken text-text-secondary';\n return (\n <div className={cn('flex items-start gap-2 rounded-lg px-3 py-2 text-sm', toneClass)}>\n <Icon className=\"mt-0.5 h-4 w-4 shrink-0\" />\n <div className=\"flex-1\">{children}</div>\n {onClose ? (\n <button\n type=\"button\"\n onClick={onClose}\n className=\"opacity-70 hover:opacity-100\"\n aria-label=\"Dismiss\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n ) : null}\n </div>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Modal (centered on desktop, full-height sheet on mobile)\n// ---------------------------------------------------------------------------\nexport const Modal: FC<{\n open: boolean;\n onClose: () => void;\n title: string;\n children: ReactNode;\n footer?: ReactNode;\n wide?: boolean;\n}> = ({ open, onClose, title, children, footer, wide }) => {\n useEffect(() => {\n if (!open) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === 'Escape') onClose();\n };\n document.addEventListener('keydown', onKey);\n document.body.style.overflow = 'hidden';\n return () => {\n document.removeEventListener('keydown', onKey);\n document.body.style.overflow = '';\n };\n }, [open, onClose]);\n\n if (!open) return null;\n return (\n <div\n className=\"fixed inset-0 z-50 flex items-end justify-center sm:items-center\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={title}\n >\n <div className=\"absolute inset-0 bg-black/50 backdrop-blur-sm\" onClick={onClose} />\n <div\n className={cn(\n 'relative flex max-h-[92vh] w-full flex-col overflow-hidden rounded-t-2xl border border-border-seam bg-bg-surface shadow-xl sm:max-h-[88vh] sm:rounded-2xl',\n wide ? 'sm:max-w-3xl' : 'sm:max-w-lg'\n )}\n >\n <header className=\"flex items-center justify-between border-b border-border-seam px-4 py-3\">\n <h2 className=\"text-base font-semibold text-text-primary\">{title}</h2>\n <button\n type=\"button\"\n onClick={onClose}\n className=\"rounded-lg p-1 text-text-tertiary hover:bg-bg-sunken hover:text-text-primary\"\n aria-label=\"Close\"\n >\n <X className=\"h-5 w-5\" />\n </button>\n </header>\n <div className=\"flex-1 overflow-y-auto px-4 py-4\">{children}</div>\n {footer ? (\n <footer className=\"flex items-center justify-end gap-2 border-t border-border-seam px-4 py-3\">\n {footer}\n </footer>\n ) : null}\n </div>\n </div>\n );\n};\n\n// ---------------------------------------------------------------------------\n// States\n// ---------------------------------------------------------------------------\nexport const LoadingBlock: FC<{ label?: string }> = ({ label = 'Loading…' }) => (\n <div className=\"flex min-h-[220px] flex-col items-center justify-center gap-3 text-text-tertiary\">\n <Loader2 className=\"h-7 w-7 animate-spin text-accent\" />\n <span className=\"text-sm\">{label}</span>\n </div>\n);\n\nexport const ErrorState: FC<{ error: unknown; onRetry?: () => void }> = ({ error, onRetry }) => (\n <div className=\"flex min-h-[220px] flex-col items-center justify-center gap-3 text-center\">\n <AlertCircle className=\"h-8 w-8 text-status-error-text\" />\n <p className=\"max-w-md text-sm text-text-secondary\">\n {error instanceof Error ? error.message : 'Something went wrong.'}\n </p>\n {onRetry ? (\n <Button variant=\"outline\" size=\"sm\" onClick={onRetry}>\n Try again\n </Button>\n ) : null}\n </div>\n);\n\nexport const Empty: FC<{\n title: string;\n description?: string;\n icon?: ReactNode;\n action?: ReactNode;\n}> = ({ title, description, icon, action }) => (\n <div className=\"flex min-h-[220px] flex-col items-center justify-center gap-3 rounded-xl border border-dashed border-border-seam bg-bg-surface/50 px-6 py-10 text-center\">\n {icon ? <div className=\"text-text-tertiary\">{icon}</div> : null}\n <div>\n <p className=\"text-sm font-medium text-text-primary\">{title}</p>\n {description ? (\n <p className=\"mt-1 max-w-sm text-sm text-text-tertiary\">{description}</p>\n ) : null}\n </div>\n {action}\n </div>\n);\n\n// ---------------------------------------------------------------------------\n// Simple responsive table wrapper (scrolls horizontally on small screens)\n// ---------------------------------------------------------------------------\nexport const TableWrap: FC<{ children: ReactNode }> = ({ children }) => (\n <div className=\"overflow-x-auto rounded-xl border border-border-seam\">\n <table className=\"w-full min-w-[560px] text-left text-sm\">{children}</table>\n </div>\n);\nexport const Th: FC<{ children?: ReactNode; className?: string }> = ({ children, className }) => (\n <th\n className={cn(\n 'bg-bg-sunken px-4 py-2.5 text-xs font-semibold uppercase tracking-wide text-text-tertiary',\n className\n )}\n >\n {children}\n </th>\n);\nexport const Td: FC<{ children?: ReactNode; className?: string }> = ({ children, className }) => (\n <td className={cn('border-t border-border-seam px-4 py-3 text-text-secondary', className)}>\n {children}\n </td>\n);\n"],"mappings":"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"}
1
+ {"version":3,"file":"ui.js","names":[],"sources":["../../../src/proserviceworld/components/ui.tsx"],"sourcesContent":["/**\n * ProServiceWorld shared UI kit.\n *\n * Self-contained, token-driven primitives (no raw colors) shared by every page:\n * page header, metric cards, section cards, a mobile-friendly modal, token-styled\n * native form controls, status pills, inline banners, and empty/loading/error\n * states. Actions reuse the fe-libs Button (brand variants); everything else is\n * built from semantic tokens so theming works across light/dark/any theme.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { useEffect } from 'react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { AlertCircle, CheckCircle2, Info, Loader2, X } from 'lucide-react';\nimport { cn } from '../utils/cn';\n\n// ---------------------------------------------------------------------------\n// Formatters\n// ---------------------------------------------------------------------------\nexport const humanize = (v: string | null | undefined): string =>\n (v ?? '')\n .toLowerCase()\n .replace(/_/g, ' ')\n .replace(/\\b\\w/g, (c) => c.toUpperCase());\n\nexport const fmtDate = (iso: string | null | undefined): string => {\n if (!iso) return '—';\n const d = new Date(iso);\n return Number.isNaN(d.getTime())\n ? '—'\n : d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });\n};\n\nexport const fmtMoney = (amount: number | null | undefined, currency = 'USD'): string => {\n const n = amount ?? 0;\n try {\n return new Intl.NumberFormat(undefined, {\n style: 'currency',\n currency,\n maximumFractionDigits: 0,\n }).format(n);\n } catch {\n return `${currency} ${n.toLocaleString()}`;\n }\n};\n\nexport const fmtHours = (h: number | null | undefined): string =>\n `${(h ?? 0).toLocaleString(undefined, { maximumFractionDigits: 1 })}h`;\nexport const fmtMinutesAsHours = (m: number | null | undefined): string => fmtHours((m ?? 0) / 60);\n\n// ---------------------------------------------------------------------------\n// Status tone mapping\n// ---------------------------------------------------------------------------\nexport type Tone = 'neutral' | 'success' | 'warning' | 'danger' | 'info' | 'accent';\n\nconst TONE_CLASS: Record<Tone, string> = {\n neutral: 'bg-bg-sunken text-text-secondary',\n success: 'bg-status-success-bg-subtle text-status-success-text',\n warning: 'bg-status-warning-bg-subtle text-status-warning-text',\n danger: 'bg-status-error-bg-subtle text-status-error-text',\n info: 'bg-bg-sunken text-text-link',\n accent: 'bg-bg-sunken text-accent',\n};\n\nconst STATUS_TONE: Record<string, Tone> = {\n // clients / generic\n ACTIVE: 'success',\n PROSPECT: 'info',\n INACTIVE: 'neutral',\n ARCHIVED: 'neutral',\n // engagements\n PROPOSAL: 'info',\n ACCEPTED: 'accent',\n IN_PROGRESS: 'accent',\n REVIEW: 'warning',\n DELIVERED: 'success',\n INVOICED: 'success',\n CLOSED: 'neutral',\n CANCELLED: 'danger',\n // matters\n OPEN: 'accent',\n ON_HOLD: 'warning',\n // priority\n LOW: 'neutral',\n MEDIUM: 'info',\n HIGH: 'warning',\n CRITICAL: 'danger',\n // deliverable / doc\n DRAFT: 'neutral',\n INTERNAL_REVIEW: 'info',\n CLIENT_REVIEW: 'warning',\n REJECTED: 'danger',\n FINAL: 'success',\n // time\n SUBMITTED: 'info',\n APPROVED: 'success',\n BILLED: 'accent',\n // invoice\n SENT: 'info',\n PAID: 'success',\n OVERDUE: 'danger',\n // compliance / conflict\n PENDING: 'warning',\n IN_REVIEW: 'info',\n PASSED: 'success',\n FAILED: 'danger',\n WAIVED: 'neutral',\n CLEAR: 'success',\n POTENTIAL_CONFLICT: 'warning',\n CONFLICT: 'danger',\n // milestone\n COMPLETED: 'success',\n};\n\nexport const StatusPill: FC<{ status: string; className?: string }> = ({ status, className }) => (\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium whitespace-nowrap',\n TONE_CLASS[STATUS_TONE[status] ?? 'neutral'],\n className\n )}\n >\n <span className=\"h-1.5 w-1.5 rounded-full bg-current opacity-70\" />\n {humanize(status)}\n </span>\n);\n\n// ---------------------------------------------------------------------------\n// Page header\n// ---------------------------------------------------------------------------\nexport const PageHeader: FC<{\n title: string;\n subtitle?: string;\n actions?: ReactNode;\n icon?: ReactNode;\n}> = ({ title, subtitle, actions, icon }) => (\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"flex items-start gap-3.5\">\n {icon ? (\n <div className=\"flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-bg-sunken text-accent\">\n {icon}\n </div>\n ) : null}\n <div>\n <h1 className=\"text-2xl font-bold tracking-tight text-text-primary sm:text-3xl\">{title}</h1>\n {subtitle ? (\n <p className=\"mt-1.5 max-w-2xl text-sm leading-relaxed text-text-secondary\">{subtitle}</p>\n ) : null}\n </div>\n </div>\n {actions ? <div className=\"flex flex-wrap items-center gap-2\">{actions}</div> : null}\n </div>\n);\n\n// ---------------------------------------------------------------------------\n// Metric card\n// ---------------------------------------------------------------------------\nexport const MetricCard: FC<{\n label: string;\n value: ReactNode;\n hint?: string;\n icon?: ReactNode;\n tone?: Tone;\n}> = ({ label, value, hint, icon, tone = 'neutral' }) => (\n <div className=\"vc-lift-on-hover rounded-2xl border border-border-seam bg-bg-surface p-5 shadow-sm\">\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"text-xs font-medium uppercase tracking-wider text-text-tertiary\">\n {label}\n </span>\n {icon ? (\n <span\n className={cn('flex h-9 w-9 items-center justify-center rounded-full', TONE_CLASS[tone])}\n >\n {icon}\n </span>\n ) : null}\n </div>\n <div className=\"mt-3 text-2xl font-semibold tracking-tight text-text-primary sm:text-3xl\">\n {value}\n </div>\n {hint ? <div className=\"mt-1 text-xs text-text-tertiary\">{hint}</div> : null}\n </div>\n);\n\n// ---------------------------------------------------------------------------\n// Section card\n// ---------------------------------------------------------------------------\nexport const SectionCard: FC<{\n title?: string;\n description?: string;\n action?: ReactNode;\n children: ReactNode;\n className?: string;\n}> = ({ title, description, action, children, className }) => (\n <section\n className={cn('rounded-2xl border border-border-seam bg-bg-surface shadow-sm', className)}\n >\n {(title || action) && (\n <header className=\"flex items-center justify-between gap-3 border-b border-border-seam px-5 py-3.5\">\n <div>\n {title ? <h2 className=\"text-sm font-semibold text-text-primary\">{title}</h2> : null}\n {description ? <p className=\"mt-0.5 text-xs text-text-tertiary\">{description}</p> : null}\n </div>\n {action}\n </header>\n )}\n <div className=\"p-5\">{children}</div>\n </section>\n);\n\n// ---------------------------------------------------------------------------\n// Form controls (token-styled native elements — reliable inside modals)\n// ---------------------------------------------------------------------------\nconst controlClass =\n 'w-full rounded-lg border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary placeholder:text-text-tertiary focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/30 disabled:opacity-50';\n\nexport const Field: FC<{\n label: string;\n required?: boolean;\n hint?: string;\n children: ReactNode;\n className?: string;\n}> = ({ label, required, hint, children, className }) => (\n <label className={cn('block', className)}>\n <span className=\"mb-1 block text-xs font-medium text-text-secondary\">\n {label}\n {required ? <span className=\"text-status-error-text\"> *</span> : null}\n </span>\n {children}\n {hint ? <span className=\"mt-1 block text-xs text-text-tertiary\">{hint}</span> : null}\n </label>\n);\n\nexport const TextInput: FC<React.InputHTMLAttributes<HTMLInputElement>> = ({\n className,\n ...props\n}) => <input className={cn(controlClass, className)} {...props} />;\nexport const TextArea: FC<React.TextareaHTMLAttributes<HTMLTextAreaElement>> = ({\n className,\n ...props\n}) => <textarea className={cn(controlClass, 'min-h-[80px] resize-y', className)} {...props} />;\nexport const NativeSelect: FC<React.SelectHTMLAttributes<HTMLSelectElement>> = ({\n className,\n children,\n ...props\n}) => (\n <select className={cn(controlClass, 'appearance-none pr-8')} {...props}>\n {children}\n </select>\n);\n\n/** Turn an enum-value list into <option>s with humanized labels. */\nexport const enumOptions = (values: readonly string[]): ReactNode =>\n values.map((v) => (\n <option key={v} value={v}>\n {humanize(v)}\n </option>\n ));\n\n// ---------------------------------------------------------------------------\n// Inline banner\n// ---------------------------------------------------------------------------\nexport const Banner: FC<{\n tone?: 'success' | 'danger' | 'info';\n children: ReactNode;\n onClose?: () => void;\n}> = ({ tone = 'info', children, onClose }) => {\n const Icon = tone === 'success' ? CheckCircle2 : tone === 'danger' ? AlertCircle : Info;\n const toneClass =\n tone === 'success'\n ? TONE_CLASS.success\n : tone === 'danger'\n ? TONE_CLASS.danger\n : 'bg-bg-sunken text-text-secondary';\n return (\n <div className={cn('flex items-start gap-2 rounded-lg px-3 py-2 text-sm', toneClass)}>\n <Icon className=\"mt-0.5 h-4 w-4 shrink-0\" />\n <div className=\"flex-1\">{children}</div>\n {onClose ? (\n <button\n type=\"button\"\n onClick={onClose}\n className=\"opacity-70 hover:opacity-100\"\n aria-label=\"Dismiss\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n ) : null}\n </div>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Modal (centered on desktop, full-height sheet on mobile)\n// ---------------------------------------------------------------------------\nexport const Modal: FC<{\n open: boolean;\n onClose: () => void;\n title: string;\n children: ReactNode;\n footer?: ReactNode;\n wide?: boolean;\n}> = ({ open, onClose, title, children, footer, wide }) => {\n useEffect(() => {\n if (!open) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === 'Escape') onClose();\n };\n document.addEventListener('keydown', onKey);\n document.body.style.overflow = 'hidden';\n return () => {\n document.removeEventListener('keydown', onKey);\n document.body.style.overflow = '';\n };\n }, [open, onClose]);\n\n if (!open) return null;\n return (\n <div\n className=\"fixed inset-0 z-50 flex items-end justify-center sm:items-center\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={title}\n >\n <div className=\"absolute inset-0 bg-black/50 backdrop-blur-sm\" onClick={onClose} />\n <div\n className={cn(\n 'relative flex max-h-[92vh] w-full flex-col overflow-hidden rounded-t-2xl border border-border-seam bg-bg-surface shadow-xl sm:max-h-[88vh] sm:rounded-2xl',\n wide ? 'sm:max-w-3xl' : 'sm:max-w-lg'\n )}\n >\n <header className=\"flex items-center justify-between border-b border-border-seam px-4 py-3\">\n <h2 className=\"text-base font-semibold text-text-primary\">{title}</h2>\n <button\n type=\"button\"\n onClick={onClose}\n className=\"rounded-lg p-1 text-text-tertiary hover:bg-bg-sunken hover:text-text-primary\"\n aria-label=\"Close\"\n >\n <X className=\"h-5 w-5\" />\n </button>\n </header>\n <div className=\"flex-1 overflow-y-auto px-4 py-4\">{children}</div>\n {footer ? (\n <footer className=\"flex items-center justify-end gap-2 border-t border-border-seam px-4 py-3\">\n {footer}\n </footer>\n ) : null}\n </div>\n </div>\n );\n};\n\n// ---------------------------------------------------------------------------\n// States\n// ---------------------------------------------------------------------------\nexport const LoadingBlock: FC<{ label?: string }> = ({ label = 'Loading…' }) => (\n <div className=\"flex min-h-[220px] flex-col items-center justify-center gap-3 text-text-tertiary\">\n <Loader2 className=\"h-7 w-7 animate-spin text-accent\" />\n <span className=\"text-sm\">{label}</span>\n </div>\n);\n\nexport const ErrorState: FC<{ error: unknown; onRetry?: () => void }> = ({ error, onRetry }) => (\n <div className=\"flex min-h-[220px] flex-col items-center justify-center gap-3 text-center\">\n <AlertCircle className=\"h-8 w-8 text-status-error-text\" />\n <p className=\"max-w-md text-sm text-text-secondary\">\n {error instanceof Error ? error.message : 'Something went wrong.'}\n </p>\n {onRetry ? (\n <Button variant=\"outline\" size=\"sm\" onClick={onRetry}>\n Try again\n </Button>\n ) : null}\n </div>\n);\n\nexport const Empty: FC<{\n title: string;\n description?: string;\n icon?: ReactNode;\n action?: ReactNode;\n}> = ({ title, description, icon, action }) => (\n <div className=\"flex min-h-[240px] flex-col items-center justify-center gap-4 rounded-2xl border border-dashed border-border-seam bg-bg-surface/50 px-6 py-12 text-center\">\n {icon ? (\n <div className=\"flex h-14 w-14 items-center justify-center rounded-2xl bg-bg-sunken text-text-tertiary\">\n {icon}\n </div>\n ) : null}\n <div>\n <p className=\"text-sm font-medium text-text-primary\">{title}</p>\n {description ? (\n <p className=\"mt-1 max-w-sm text-sm text-text-tertiary\">{description}</p>\n ) : null}\n </div>\n {action}\n </div>\n);\n\n// ---------------------------------------------------------------------------\n// Simple responsive table wrapper (scrolls horizontally on small screens)\n// ---------------------------------------------------------------------------\nexport const TableWrap: FC<{ children: ReactNode }> = ({ children }) => (\n <div className=\"overflow-x-auto rounded-xl border border-border-seam\">\n <table className=\"w-full min-w-[560px] text-left text-sm\">{children}</table>\n </div>\n);\nexport const Th: FC<{ children?: ReactNode; className?: string }> = ({ children, className }) => (\n <th\n className={cn(\n 'bg-bg-sunken px-4 py-2.5 text-xs font-semibold uppercase tracking-wide text-text-tertiary',\n className\n )}\n >\n {children}\n </th>\n);\nexport const Td: FC<{ children?: ReactNode; className?: string }> = ({ children, className }) => (\n <td className={cn('border-t border-border-seam px-4 py-3 text-text-secondary', className)}>\n {children}\n </td>\n);\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"useEntityComments.js","names":[],"sources":["../../../src/proserviceworld/hooks/useEntityComments.ts"],"sourcesContent":["/**\n * Entity discussion via wspace-conversations-svc (NO duplication).\n * @module proserviceworld/hooks\n *\n * ProServiceWorld does NOT own a comments table. Discussion/notes on a client,\n * engagement or matter reuse the shared `wspace-conversations` entity-comment\n * API (createEntityComment / getEntityComments) — the same service that powers\n * comments everywhere on the platform — via the MFE's proven graphqlFetch\n * transport. `entityType` namespaces threads to this product\n * (\"proserviceworld-<entity>\"); `entityId` is the Psw row id.\n */\n\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\n\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport { clearGraphqlFetchResponseCache } from '@burdenoff/fe-libs/shared/graphql/fetch';\n\nasync function convQuery<TData>(query: string, variables: Record<string, unknown>): Promise<TData> {\n const response = await graphqlFetch<TData>({ gateway: 'workspace', query, variables, workspaceToken: true });\n if (response.errors?.length) throw new Error(response.errors[0]?.message ?? 'Conversations request failed');\n if (!response.data) throw new Error('Conversations response did not include data');\n if (query.trimStart().startsWith('mutation')) clearGraphqlFetchResponseCache();\n return response.data;\n}\n\nexport interface EntityComment {\n id: string;\n authorId: string;\n content: { text: string | null } | null;\n createdAt: string;\n}\n\nconst COMMENT_F = 'id authorId content { text } createdAt';\nconst QK = 'proserviceworld-comments' as const;\n\n/** List the discussion thread for a Psw entity (most recent first). */\nexport function useEntityComments(entityType: string, entityId: string | null) {\n return useQuery({\n queryKey: [QK, entityType, entityId],\n enabled: Boolean(entityId),\n queryFn: async () => {\n const data = await convQuery<{ getEntityComments: { items: EntityComment[]; total: number } }>(\n `query PswGetComments($input: GetCommentsInput!) {\n getEntityComments(input: $input) { items { ${COMMENT_F} } total }\n }`,\n { input: { entityType, entityId, take: 50, sortBy: 'createdAt', sortOrder: 'desc' } }\n );\n return data.getEntityComments;\n },\n });\n}\n\n/** Post a plain-text note onto a Psw entity's shared discussion thread. */\nexport function useAddEntityComment(entityType: string, entityId: string) {\n const qc = useQueryClient();\n return useMutation({\n mutationFn: (text: string) =>\n convQuery(\n `mutation PswAddComment($input: CreateEntityCommentInput!) {\n createEntityComment(input: $input) { ${COMMENT_F} }\n }`,\n { input: { entityType, entityId, content: { text } } }\n ),\n onSuccess: () => qc.invalidateQueries({ queryKey: [QK, entityType, entityId] }),\n });\n}\n"],"mappings":";;;;AAiBA,eAAe,EAAiB,GAAe,GAAoD;CACjG,IAAM,IAAW,MAAM,EAAoB;EAAE,SAAS;EAAa;EAAO;EAAW,gBAAgB;EAAM,CAAC;AAC5G,KAAI,EAAS,QAAQ,OAAQ,OAAU,MAAM,EAAS,OAAO,IAAI,WAAW,+BAA+B;AAC3G,KAAI,CAAC,EAAS,KAAM,OAAU,MAAM,8CAA8C;AAElF,QADI,EAAM,WAAW,CAAC,WAAW,WAAW,IAAE,GAAgC,EACvE,EAAS;;AAUlB,IAAM,IAAY,0CACZ,IAAK;AAGX,SAAgB,EAAkB,GAAoB,GAAyB;AAC7E,QAAO,EAAS;EACd,UAAU;GAAC;GAAI;GAAY;GAAS;EACpC,SAAS,EAAQ;EACjB,SAAS,aACM,MAAM,EACjB;uDAC+C,EAAU;YAEzD,EAAE,OAAO;GAAE;GAAY;GAAU,MAAM;GAAI,QAAQ;GAAa,WAAW;GAAQ,EAAE,CACtF,EACW;EAEf,CAAC;;AAIJ,SAAgB,EAAoB,GAAoB,GAAkB;CACxE,IAAM,IAAK,GAAgB;AAC3B,QAAO,EAAY;EACjB,aAAa,MACX,EACE;iDACyC,EAAU;YAEnD,EAAE,OAAO;GAAE;GAAY;GAAU,SAAS,EAAE,SAAM;GAAE,EAAE,CACvD;EACH,iBAAiB,EAAG,kBAAkB,EAAE,UAAU;GAAC;GAAI;GAAY;GAAS,EAAE,CAAC;EAChF,CAAC"}
1
+ {"version":3,"file":"useEntityComments.js","names":[],"sources":["../../../src/proserviceworld/hooks/useEntityComments.ts"],"sourcesContent":["/**\n * Entity discussion via wspace-conversations-svc (NO duplication).\n * @module proserviceworld/hooks\n *\n * ProServiceWorld does NOT own a comments table. Discussion/notes on a client,\n * engagement or matter reuse the shared `wspace-conversations` entity-comment\n * API (createEntityComment / getEntityComments) — the same service that powers\n * comments everywhere on the platform — via the MFE's proven graphqlFetch\n * transport. `entityType` namespaces threads to this product\n * (\"proserviceworld-<entity>\"); `entityId` is the Psw row id.\n */\n\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\n\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport { clearGraphqlFetchResponseCache } from '@burdenoff/fe-libs/shared/graphql/fetch';\n\nasync function convQuery<TData>(query: string, variables: Record<string, unknown>): Promise<TData> {\n const response = await graphqlFetch<TData>({\n gateway: 'workspace',\n query,\n variables,\n workspaceToken: true,\n });\n if (response.errors?.length)\n throw new Error(response.errors[0]?.message ?? 'Conversations request failed');\n if (!response.data) throw new Error('Conversations response did not include data');\n if (query.trimStart().startsWith('mutation')) clearGraphqlFetchResponseCache();\n return response.data;\n}\n\nexport interface EntityComment {\n id: string;\n authorId: string;\n content: { text: string | null } | null;\n createdAt: string;\n}\n\nconst COMMENT_F = 'id authorId content { text } createdAt';\nconst QK = 'proserviceworld-comments' as const;\n\n/** List the discussion thread for a Psw entity (most recent first). */\nexport function useEntityComments(entityType: string, entityId: string | null) {\n return useQuery({\n queryKey: [QK, entityType, entityId],\n enabled: Boolean(entityId),\n queryFn: async () => {\n const data = await convQuery<{\n getEntityComments: { items: EntityComment[]; total: number };\n }>(\n `query PswGetComments($input: GetCommentsInput!) {\n getEntityComments(input: $input) { items { ${COMMENT_F} } total }\n }`,\n { input: { entityType, entityId, take: 50, sortBy: 'createdAt', sortOrder: 'desc' } }\n );\n return data.getEntityComments;\n },\n });\n}\n\n/** Post a plain-text note onto a Psw entity's shared discussion thread. */\nexport function useAddEntityComment(entityType: string, entityId: string) {\n const qc = useQueryClient();\n return useMutation({\n mutationFn: (text: string) =>\n convQuery(\n `mutation PswAddComment($input: CreateEntityCommentInput!) {\n createEntityComment(input: $input) { ${COMMENT_F} }\n }`,\n { input: { entityType, entityId, content: { text } } }\n ),\n onSuccess: () => qc.invalidateQueries({ queryKey: [QK, entityType, entityId] }),\n });\n}\n"],"mappings":";;;;AAiBA,eAAe,EAAiB,GAAe,GAAoD;CACjG,IAAM,IAAW,MAAM,EAAoB;EACzC,SAAS;EACT;EACA;EACA,gBAAgB;EACjB,CAAC;AACF,KAAI,EAAS,QAAQ,OACnB,OAAU,MAAM,EAAS,OAAO,IAAI,WAAW,+BAA+B;AAChF,KAAI,CAAC,EAAS,KAAM,OAAU,MAAM,8CAA8C;AAElF,QADI,EAAM,WAAW,CAAC,WAAW,WAAW,IAAE,GAAgC,EACvE,EAAS;;AAUlB,IAAM,IAAY,0CACZ,IAAK;AAGX,SAAgB,EAAkB,GAAoB,GAAyB;AAC7E,QAAO,EAAS;EACd,UAAU;GAAC;GAAI;GAAY;GAAS;EACpC,SAAS,EAAQ;EACjB,SAAS,aACM,MAAM,EAGjB;uDAC+C,EAAU;YAEzD,EAAE,OAAO;GAAE;GAAY;GAAU,MAAM;GAAI,QAAQ;GAAa,WAAW;GAAQ,EAAE,CACtF,EACW;EAEf,CAAC;;AAIJ,SAAgB,EAAoB,GAAoB,GAAkB;CACxE,IAAM,IAAK,GAAgB;AAC3B,QAAO,EAAY;EACjB,aAAa,MACX,EACE;iDACyC,EAAU;YAEnD,EAAE,OAAO;GAAE;GAAY;GAAU,SAAS,EAAE,SAAM;GAAE,EAAE,CACvD;EACH,iBAAiB,EAAG,kBAAkB,EAAE,UAAU;GAAC;GAAI;GAAY;GAAS,EAAE,CAAC;EAChF,CAAC"}