@burdenoff/microfe-mybodyshield 2026.705.1 → 2026.705.2
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/mybodyshield/components/DataTable.js +8 -4
- package/dist/mybodyshield/components/DataTable.js.map +1 -1
- package/dist/mybodyshield/components/EvidenceUpload.js +92 -4
- package/dist/mybodyshield/components/EvidenceUpload.js.map +1 -0
- package/dist/mybodyshield/components/FileLink.js +35 -2
- package/dist/mybodyshield/components/FileLink.js.map +1 -0
- package/dist/mybodyshield/components/MybDiscussion.js +107 -6
- package/dist/mybodyshield/components/MybDiscussion.js.map +1 -0
- package/dist/mybodyshield/components/MybTags.js +148 -4
- package/dist/mybodyshield/components/MybTags.js.map +1 -0
- package/dist/mybodyshield/components/Pagination.js.map +1 -1
- package/dist/mybodyshield/components/UserSearchSelect.js +148 -0
- package/dist/mybodyshield/components/UserSearchSelect.js.map +1 -0
- package/dist/mybodyshield/components/index.js +1 -0
- package/dist/mybodyshield/hooks/index.js +1 -0
- package/dist/mybodyshield/hooks/useMyBodyShieldApi.js +47 -27
- package/dist/mybodyshield/hooks/useMyBodyShieldApi.js.map +1 -1
- package/dist/mybodyshield/hooks/useMybComments.js +44 -2
- package/dist/mybodyshield/hooks/useMybComments.js.map +1 -0
- package/dist/mybodyshield/hooks/useMybFiles.js +80 -3
- package/dist/mybodyshield/hooks/useMybFiles.js.map +1 -0
- package/dist/mybodyshield/hooks/useMybTags.js +88 -2
- package/dist/mybodyshield/hooks/useMybTags.js.map +1 -0
- package/dist/mybodyshield/hooks/useMybUsers.js +36 -0
- package/dist/mybodyshield/hooks/useMybUsers.js.map +1 -0
- package/dist/mybodyshield/pages/DashboardPage.js +41 -36
- package/dist/mybodyshield/pages/DashboardPage.js.map +1 -1
- package/dist/mybodyshield/pages/_shared/visuals.js +15 -9
- package/dist/mybodyshield/pages/_shared/visuals.js.map +1 -1
- package/dist/mybodyshield/pages/coaching/CoachingPage.js +171 -147
- package/dist/mybodyshield/pages/coaching/CoachingPage.js.map +1 -1
- package/dist/mybodyshield/pages/goals/GoalsPage.js +0 -8
- package/dist/mybodyshield/pages/goals/GoalsPage.js.map +1 -1
- package/dist/mybodyshield/pages/nutrition/NutritionPage.js +352 -278
- package/dist/mybodyshield/pages/nutrition/NutritionPage.js.map +1 -1
- package/dist/mybodyshield/pages/programs/ProgramDetailPage.js +600 -296
- package/dist/mybodyshield/pages/programs/ProgramDetailPage.js.map +1 -1
- package/dist/mybodyshield/pages/progress/ProgressPage.js +88 -51
- package/dist/mybodyshield/pages/progress/ProgressPage.js.map +1 -1
- package/dist/mybodyshield/pages/train/SessionDetailPage.js +146 -126
- package/dist/mybodyshield/pages/train/SessionDetailPage.js.map +1 -1
- package/dist/mybodyshield/pages/train/SessionsPage.js +1 -4
- package/dist/mybodyshield/pages/train/SessionsPage.js.map +1 -1
- package/package.json +2 -2
|
@@ -44,12 +44,16 @@ function u({ columns: u, rows: d, rowKey: f, onRowClick: p, className: m, defaul
|
|
|
44
44
|
children: u.map((t) => {
|
|
45
45
|
let n = !!t.sortValue, c = g?.key === t.key;
|
|
46
46
|
return /* @__PURE__ */ o("th", {
|
|
47
|
-
onClick: n ? () => y(t.key) : void 0,
|
|
48
47
|
"aria-sort": c ? g?.dir === "asc" ? "ascending" : "descending" : void 0,
|
|
49
|
-
className: e("px-4 py-3 font-medium text-text-secondary whitespace-nowrap", l[t.align ?? "left"], t.hideOnMobile && "hidden md:table-cell"
|
|
50
|
-
children: /* @__PURE__ */ s("
|
|
48
|
+
className: e("px-4 py-3 font-medium text-text-secondary whitespace-nowrap", l[t.align ?? "left"], t.hideOnMobile && "hidden md:table-cell"),
|
|
49
|
+
children: n ? /* @__PURE__ */ s("button", {
|
|
50
|
+
type: "button",
|
|
51
|
+
onClick: () => y(t.key),
|
|
52
|
+
className: e("inline-flex items-center gap-1 select-none rounded-sm transition-colors hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent", t.align === "right" && "flex-row-reverse"),
|
|
53
|
+
children: [t.header, c ? g?.dir === "asc" ? /* @__PURE__ */ o(i, { className: "h-3.5 w-3.5 text-accent" }) : /* @__PURE__ */ o(r, { className: "h-3.5 w-3.5 text-accent" }) : /* @__PURE__ */ o(a, { className: "h-3.5 w-3.5 opacity-40" })]
|
|
54
|
+
}) : /* @__PURE__ */ o("span", {
|
|
51
55
|
className: e("inline-flex items-center gap-1", t.align === "right" && "flex-row-reverse"),
|
|
52
|
-
children:
|
|
56
|
+
children: t.header
|
|
53
57
|
})
|
|
54
58
|
}, t.key);
|
|
55
59
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DataTable.js","names":[],"sources":["../../../src/mybodyshield/components/DataTable.tsx"],"sourcesContent":["/**\n * DataTable — a lightweight, semantic-token table for list pages.\n * @module mybodyshield/components\n *\n * Deliberately minimal (no external table lib). Renders a responsive,\n * horizontally-scrollable table wrapped in a GlassCard. Columns declare a\n * header, a render fn, optional alignment, and optionally a `sortValue` to make\n * the column click-sortable.\n */\n\nimport { useMemo, useState, type ReactNode } from 'react';\nimport { ChevronUp, ChevronDown, ChevronsUpDown } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\n\nexport interface Column<T> {\n key: string;\n header: ReactNode;\n render: (row: T) => ReactNode;\n align?: 'left' | 'right' | 'center';\n className?: string;\n /** Hide below the `md` breakpoint for a mobile-friendly layout. */\n hideOnMobile?: boolean;\n /** Provide to make this column click-sortable (returns a comparable value). */\n sortValue?: (row: T) => string | number;\n}\n\ninterface DataTableProps<T> {\n columns: Column<T>[];\n rows: T[];\n rowKey: (row: T) => string;\n onRowClick?: (row: T) => void;\n className?: string;\n /** Default sort: a column key + direction. */\n defaultSort?: { key: string; dir: 'asc' | 'desc' };\n}\n\nconst alignClass = {\n left: 'text-left',\n right: 'text-right',\n center: 'text-center',\n} as const;\n\nexport function DataTable<T>({\n columns,\n rows,\n rowKey,\n onRowClick,\n className,\n defaultSort,\n}: DataTableProps<T>) {\n const [sort, setSort] = useState<{ key: string; dir: 'asc' | 'desc' } | null>(\n defaultSort ?? null\n );\n\n const sortedRows = useMemo(() => {\n if (!sort) return rows;\n const col = columns.find((c) => c.key === sort.key);\n if (!col?.sortValue) return rows;\n const dir = sort.dir === 'asc' ? 1 : -1;\n return [...rows].sort((a, b) => {\n const va = col.sortValue!(a);\n const vb = col.sortValue!(b);\n if (va < vb) return -1 * dir;\n if (va > vb) return 1 * dir;\n return 0;\n });\n }, [rows, sort, columns]);\n\n const toggleSort = (key: string) => {\n setSort((prev) =>\n prev?.key === key ? { key, dir: prev.dir === 'asc' ? 'desc' : 'asc' } : { key, dir: 'asc' }\n );\n };\n\n return (\n <GlassCard treatment=\"plain\" className={cn('overflow-hidden', className)}>\n <div className=\"overflow-x-auto\">\n <table className=\"w-full border-collapse text-sm\">\n <thead>\n <tr className=\"border-b border-border-seam\">\n {columns.map((col) => {\n const sortable = Boolean(col.sortValue);\n const active = sort?.key === col.key;\n return (\n <th\n key={col.key}\n
|
|
1
|
+
{"version":3,"file":"DataTable.js","names":[],"sources":["../../../src/mybodyshield/components/DataTable.tsx"],"sourcesContent":["/**\n * DataTable — a lightweight, semantic-token table for list pages.\n * @module mybodyshield/components\n *\n * Deliberately minimal (no external table lib). Renders a responsive,\n * horizontally-scrollable table wrapped in a GlassCard. Columns declare a\n * header, a render fn, optional alignment, and optionally a `sortValue` to make\n * the column click-sortable.\n */\n\nimport { useMemo, useState, type ReactNode } from 'react';\nimport { ChevronUp, ChevronDown, ChevronsUpDown } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\n\nexport interface Column<T> {\n key: string;\n header: ReactNode;\n render: (row: T) => ReactNode;\n align?: 'left' | 'right' | 'center';\n className?: string;\n /** Hide below the `md` breakpoint for a mobile-friendly layout. */\n hideOnMobile?: boolean;\n /** Provide to make this column click-sortable (returns a comparable value). */\n sortValue?: (row: T) => string | number;\n}\n\ninterface DataTableProps<T> {\n columns: Column<T>[];\n rows: T[];\n rowKey: (row: T) => string;\n onRowClick?: (row: T) => void;\n className?: string;\n /** Default sort: a column key + direction. */\n defaultSort?: { key: string; dir: 'asc' | 'desc' };\n}\n\nconst alignClass = {\n left: 'text-left',\n right: 'text-right',\n center: 'text-center',\n} as const;\n\nexport function DataTable<T>({\n columns,\n rows,\n rowKey,\n onRowClick,\n className,\n defaultSort,\n}: DataTableProps<T>) {\n const [sort, setSort] = useState<{ key: string; dir: 'asc' | 'desc' } | null>(\n defaultSort ?? null\n );\n\n const sortedRows = useMemo(() => {\n if (!sort) return rows;\n const col = columns.find((c) => c.key === sort.key);\n if (!col?.sortValue) return rows;\n const dir = sort.dir === 'asc' ? 1 : -1;\n return [...rows].sort((a, b) => {\n const va = col.sortValue!(a);\n const vb = col.sortValue!(b);\n if (va < vb) return -1 * dir;\n if (va > vb) return 1 * dir;\n return 0;\n });\n }, [rows, sort, columns]);\n\n const toggleSort = (key: string) => {\n setSort((prev) =>\n prev?.key === key ? { key, dir: prev.dir === 'asc' ? 'desc' : 'asc' } : { key, dir: 'asc' }\n );\n };\n\n return (\n <GlassCard treatment=\"plain\" className={cn('overflow-hidden', className)}>\n <div className=\"overflow-x-auto\">\n <table className=\"w-full border-collapse text-sm\">\n <thead>\n <tr className=\"border-b border-border-seam\">\n {columns.map((col) => {\n const sortable = Boolean(col.sortValue);\n const active = sort?.key === col.key;\n return (\n <th\n key={col.key}\n aria-sort={\n active ? (sort?.dir === 'asc' ? 'ascending' : 'descending') : undefined\n }\n className={cn(\n 'px-4 py-3 font-medium text-text-secondary whitespace-nowrap',\n alignClass[col.align ?? 'left'],\n col.hideOnMobile && 'hidden md:table-cell'\n )}\n >\n {sortable ? (\n <button\n type=\"button\"\n onClick={() => toggleSort(col.key)}\n className={cn(\n 'inline-flex items-center gap-1 select-none rounded-sm transition-colors hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent',\n col.align === 'right' && 'flex-row-reverse'\n )}\n >\n {col.header}\n {active ? (\n sort?.dir === 'asc' ? (\n <ChevronUp className=\"h-3.5 w-3.5 text-accent\" />\n ) : (\n <ChevronDown className=\"h-3.5 w-3.5 text-accent\" />\n )\n ) : (\n <ChevronsUpDown className=\"h-3.5 w-3.5 opacity-40\" />\n )}\n </button>\n ) : (\n <span\n className={cn(\n 'inline-flex items-center gap-1',\n col.align === 'right' && 'flex-row-reverse'\n )}\n >\n {col.header}\n </span>\n )}\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody>\n {sortedRows.map((row) => (\n <tr\n key={rowKey(row)}\n onClick={onRowClick ? () => onRowClick(row) : undefined}\n className={cn(\n 'group border-b border-border-seam/60 last:border-0',\n onRowClick && 'cursor-pointer transition-colors hover:bg-bg-sunken/60'\n )}\n >\n {columns.map((col) => (\n <td\n key={col.key}\n className={cn(\n 'px-4 py-3 text-text-primary align-middle',\n alignClass[col.align ?? 'left'],\n col.hideOnMobile && 'hidden md:table-cell',\n col.className\n )}\n >\n {col.render(row)}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </GlassCard>\n );\n}\n"],"mappings":";;;;;;AAqCA,IAAM,IAAa;CACjB,MAAM;CACN,OAAO;CACP,QAAQ;CACT;AAED,SAAgB,EAAa,EAC3B,YACA,SACA,WACA,eACA,cACA,kBACoB;CACpB,IAAM,CAAC,GAAM,KAAW,EACtB,KAAe,KAChB,EAEK,IAAa,QAAc;AAC/B,MAAI,CAAC,EAAM,QAAO;EAClB,IAAM,IAAM,EAAQ,MAAM,MAAM,EAAE,QAAQ,EAAK,IAAI;AACnD,MAAI,CAAC,GAAK,UAAW,QAAO;EAC5B,IAAM,IAAM,EAAK,QAAQ,QAAQ,IAAI;AACrC,SAAO,CAAC,GAAG,EAAK,CAAC,MAAM,GAAG,MAAM;GAC9B,IAAM,IAAK,EAAI,UAAW,EAAE,EACtB,IAAK,EAAI,UAAW,EAAE;AAG5B,UAFI,IAAK,IAAW,KAAK,IACrB,IAAK,IAAW,IAAI,IACjB;IACP;IACD;EAAC;EAAM;EAAM;EAAQ,CAAC,EAEnB,KAAc,MAAgB;AAClC,KAAS,MACP,GAAM,QAAQ,IAAM;GAAE;GAAK,KAAK,EAAK,QAAQ,QAAQ,SAAS;GAAO,GAAG;GAAE;GAAK,KAAK;GAAO,CAC5F;;AAGH,QACE,kBAAC,GAAD;EAAW,WAAU;EAAQ,WAAW,EAAG,mBAAmB,EAAU;YACtE,kBAAC,OAAD;GAAK,WAAU;aACb,kBAAC,SAAD;IAAO,WAAU;cAAjB,CACE,kBAAC,SAAD,EAAA,UACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAQ,KAAK,MAAQ;MACpB,IAAM,IAAW,EAAQ,EAAI,WACvB,IAAS,GAAM,QAAQ,EAAI;AACjC,aACE,kBAAC,MAAD;OAEE,aACE,IAAU,GAAM,QAAQ,QAAQ,cAAc,eAAgB,KAAA;OAEhE,WAAW,EACT,+DACA,EAAW,EAAI,SAAS,SACxB,EAAI,gBAAgB,uBACrB;iBAEA,IACC,kBAAC,UAAD;QACE,MAAK;QACL,eAAe,EAAW,EAAI,IAAI;QAClC,WAAW,EACT,6KACA,EAAI,UAAU,WAAW,mBAC1B;kBANH,CAQG,EAAI,QACJ,IACC,GAAM,QAAQ,QACZ,kBAAC,GAAD,EAAW,WAAU,2BAA4B,CAAA,GAEjD,kBAAC,GAAD,EAAa,WAAU,2BAA4B,CAAA,GAGrD,kBAAC,GAAD,EAAgB,WAAU,0BAA2B,CAAA,CAEhD;YAET,kBAAC,QAAD;QACE,WAAW,EACT,kCACA,EAAI,UAAU,WAAW,mBAC1B;kBAEA,EAAI;QACA,CAAA;OAEN,EAxCE,EAAI,IAwCN;OAEP;KACC,CAAA,EACC,CAAA,EACR,kBAAC,SAAD,EAAA,UACG,EAAW,KAAK,MACf,kBAAC,MAAD;KAEE,SAAS,UAAmB,EAAW,EAAI,GAAG,KAAA;KAC9C,WAAW,EACT,sDACA,KAAc,yDACf;eAEA,EAAQ,KAAK,MACZ,kBAAC,MAAD;MAEE,WAAW,EACT,4CACA,EAAW,EAAI,SAAS,SACxB,EAAI,gBAAgB,wBACpB,EAAI,UACL;gBAEA,EAAI,OAAO,EAAI;MACb,EATE,EAAI,IASN,CACL;KACC,EApBE,EAAO,EAAI,CAoBb,CACL,EACI,CAAA,CACF;;GACJ,CAAA;EACI,CAAA"}
|
|
@@ -1,5 +1,93 @@
|
|
|
1
|
+
import { useMybFileUpload as e } from "../hooks/useMybFiles.js";
|
|
1
2
|
import "../hooks/index.js";
|
|
2
|
-
import "react";
|
|
3
|
-
import "lucide-react";
|
|
4
|
-
import "react/jsx-runtime";
|
|
5
|
-
import "@burdenoff/fe-libs/ui";
|
|
3
|
+
import { useRef as t } from "react";
|
|
4
|
+
import { Paperclip as n, Upload as r, X as i } from "lucide-react";
|
|
5
|
+
import { jsx as a, jsxs as o } from "react/jsx-runtime";
|
|
6
|
+
import { Button as s, Label as c, Spinner as l } from "@burdenoff/fe-libs/ui";
|
|
7
|
+
//#region src/mybodyshield/components/EvidenceUpload.tsx
|
|
8
|
+
function u({ id: u, label: d, value: f, onChange: p, accept: m = "image/*,application/pdf,audio/*", disabled: h = !1, hint: g }) {
|
|
9
|
+
let _ = t(null), { upload: v, isUploading: y, progress: b, error: x, reset: S } = e();
|
|
10
|
+
return /* @__PURE__ */ o("div", {
|
|
11
|
+
className: "grid gap-1.5",
|
|
12
|
+
children: [
|
|
13
|
+
/* @__PURE__ */ a(c, {
|
|
14
|
+
htmlFor: u,
|
|
15
|
+
children: d
|
|
16
|
+
}),
|
|
17
|
+
/* @__PURE__ */ a("input", {
|
|
18
|
+
ref: _,
|
|
19
|
+
id: u,
|
|
20
|
+
type: "file",
|
|
21
|
+
accept: m,
|
|
22
|
+
className: "hidden",
|
|
23
|
+
disabled: h || y,
|
|
24
|
+
onChange: async (e) => {
|
|
25
|
+
let t = e.target.files?.[0];
|
|
26
|
+
if (e.target.value = "", t) try {
|
|
27
|
+
p(await v(t));
|
|
28
|
+
} catch {}
|
|
29
|
+
}
|
|
30
|
+
}),
|
|
31
|
+
f ? /* @__PURE__ */ o("div", {
|
|
32
|
+
className: "flex items-center gap-2 rounded-lg border border-border-seam bg-bg-sunken/60 px-3 py-2",
|
|
33
|
+
children: [
|
|
34
|
+
/* @__PURE__ */ a(n, { className: "h-4 w-4 shrink-0 text-text-tertiary" }),
|
|
35
|
+
/* @__PURE__ */ a("span", {
|
|
36
|
+
className: "min-w-0 flex-1 truncate text-sm text-text-primary",
|
|
37
|
+
title: f.fileName,
|
|
38
|
+
children: f.fileName
|
|
39
|
+
}),
|
|
40
|
+
/* @__PURE__ */ a("button", {
|
|
41
|
+
type: "button",
|
|
42
|
+
onClick: () => {
|
|
43
|
+
S(), p(null);
|
|
44
|
+
},
|
|
45
|
+
disabled: h,
|
|
46
|
+
"aria-label": "Remove attachment",
|
|
47
|
+
className: "rounded-md p-1 text-text-tertiary hover:bg-bg-sunken hover:text-text-primary",
|
|
48
|
+
children: /* @__PURE__ */ a(i, { className: "h-4 w-4" })
|
|
49
|
+
})
|
|
50
|
+
]
|
|
51
|
+
}) : y ? /* @__PURE__ */ o("div", {
|
|
52
|
+
className: "flex items-center gap-3 rounded-lg border border-border-seam bg-bg-sunken/60 px-3 py-2",
|
|
53
|
+
children: [
|
|
54
|
+
/* @__PURE__ */ a(l, { size: "sm" }),
|
|
55
|
+
/* @__PURE__ */ o("span", {
|
|
56
|
+
className: "text-sm text-text-secondary",
|
|
57
|
+
children: [
|
|
58
|
+
"Uploading… ",
|
|
59
|
+
b,
|
|
60
|
+
"%"
|
|
61
|
+
]
|
|
62
|
+
}),
|
|
63
|
+
/* @__PURE__ */ a("div", {
|
|
64
|
+
className: "ml-auto h-1.5 w-24 overflow-hidden rounded-full bg-bg-sunken",
|
|
65
|
+
children: /* @__PURE__ */ a("div", {
|
|
66
|
+
className: "h-full rounded-full bg-action-primary-bg transition-[width] duration-150",
|
|
67
|
+
style: { width: `${b}%` }
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
]
|
|
71
|
+
}) : /* @__PURE__ */ o(s, {
|
|
72
|
+
type: "button",
|
|
73
|
+
variant: "outline",
|
|
74
|
+
disabled: h,
|
|
75
|
+
onClick: () => _.current?.click(),
|
|
76
|
+
className: "justify-start",
|
|
77
|
+
children: [/* @__PURE__ */ a(r, { className: "mr-1.5 h-4 w-4" }), " Choose file"]
|
|
78
|
+
}),
|
|
79
|
+
g && !x && /* @__PURE__ */ a("p", {
|
|
80
|
+
className: "text-xs text-text-tertiary",
|
|
81
|
+
children: g
|
|
82
|
+
}),
|
|
83
|
+
x && /* @__PURE__ */ a("p", {
|
|
84
|
+
className: "text-xs text-status-error-text",
|
|
85
|
+
children: x
|
|
86
|
+
})
|
|
87
|
+
]
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
91
|
+
export { u as EvidenceUpload };
|
|
92
|
+
|
|
93
|
+
//# sourceMappingURL=EvidenceUpload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EvidenceUpload.js","names":[],"sources":["../../../src/mybodyshield/components/EvidenceUpload.tsx"],"sourcesContent":["/**\n * EvidenceUpload — a controlled file-attach control (Integration A).\n * @module mybodyshield/components/EvidenceUpload\n *\n * Wraps `useMybFileUpload` (the wspace-files 3-step flow). On file select it\n * uploads, shows byte-progress + the filename, and emits `{ fileId, fileName }`\n * via `onChange`. The parent stores `fileId` and passes it to the eco mutation\n * (`MybRecordMetricInput.photoFileId`, `MybCreateExerciseInput.demoFileId`).\n */\n\nimport { useRef, type ChangeEvent } from 'react';\nimport { Paperclip, Upload, X } from 'lucide-react';\nimport { Button, Label, Spinner } from '@burdenoff/fe-libs/ui';\nimport { useMybFileUpload, type UploadedMybFile } from '../hooks';\n\ninterface EvidenceUploadProps {\n id: string;\n label: string;\n value: UploadedMybFile | null;\n onChange: (value: UploadedMybFile | null) => void;\n accept?: string;\n disabled?: boolean;\n hint?: string;\n}\n\nexport function EvidenceUpload({\n id,\n label,\n value,\n onChange,\n accept = 'image/*,application/pdf,audio/*',\n disabled = false,\n hint,\n}: EvidenceUploadProps) {\n const inputRef = useRef<HTMLInputElement>(null);\n const { upload, isUploading, progress, error, reset } = useMybFileUpload();\n\n const handleSelect = async (event: ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0];\n // Clear the native input so the same file can be re-selected after a remove.\n event.target.value = '';\n if (!file) return;\n try {\n const uploaded = await upload(file);\n onChange(uploaded);\n } catch {\n // Error surfaced inline via `error`; keep the form usable.\n }\n };\n\n const handleRemove = () => {\n reset();\n onChange(null);\n };\n\n return (\n <div className=\"grid gap-1.5\">\n <Label htmlFor={id}>{label}</Label>\n\n <input\n ref={inputRef}\n id={id}\n type=\"file\"\n accept={accept}\n className=\"hidden\"\n disabled={disabled || isUploading}\n onChange={handleSelect}\n />\n\n {value ? (\n <div className=\"flex items-center gap-2 rounded-lg border border-border-seam bg-bg-sunken/60 px-3 py-2\">\n <Paperclip className=\"h-4 w-4 shrink-0 text-text-tertiary\" />\n <span\n className=\"min-w-0 flex-1 truncate text-sm text-text-primary\"\n title={value.fileName}\n >\n {value.fileName}\n </span>\n <button\n type=\"button\"\n onClick={handleRemove}\n disabled={disabled}\n aria-label=\"Remove attachment\"\n className=\"rounded-md p-1 text-text-tertiary hover:bg-bg-sunken hover:text-text-primary\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </div>\n ) : isUploading ? (\n <div className=\"flex items-center gap-3 rounded-lg border border-border-seam bg-bg-sunken/60 px-3 py-2\">\n <Spinner size=\"sm\" />\n <span className=\"text-sm text-text-secondary\">Uploading… {progress}%</span>\n <div className=\"ml-auto h-1.5 w-24 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-action-primary-bg transition-[width] duration-150\"\n style={{ width: `${progress}%` }}\n />\n </div>\n </div>\n ) : (\n <Button\n type=\"button\"\n variant=\"outline\"\n disabled={disabled}\n onClick={() => inputRef.current?.click()}\n className=\"justify-start\"\n >\n <Upload className=\"mr-1.5 h-4 w-4\" /> Choose file\n </Button>\n )}\n\n {hint && !error && <p className=\"text-xs text-text-tertiary\">{hint}</p>}\n {error && <p className=\"text-xs text-status-error-text\">{error}</p>}\n </div>\n );\n}\n"],"mappings":";;;;;;;AAyBA,SAAgB,EAAe,EAC7B,OACA,UACA,UACA,aACA,YAAS,mCACT,cAAW,IACX,WACsB;CACtB,IAAM,IAAW,EAAyB,KAAK,EACzC,EAAE,WAAQ,gBAAa,aAAU,UAAO,aAAU,GAAkB;AAoB1E,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IAAO,SAAS;cAAK;IAAc,CAAA;GAEnC,kBAAC,SAAD;IACE,KAAK;IACD;IACJ,MAAK;IACG;IACR,WAAU;IACV,UAAU,KAAY;IACtB,UA7Be,OAAO,MAAyC;KACnE,IAAM,IAAO,EAAM,OAAO,QAAQ;AAElC,WAAM,OAAO,QAAQ,IAChB,EACL,KAAI;AAEF,QADiB,MAAM,EAAO,EAAK,CACjB;aACZ;;IAsBJ,CAAA;GAED,IACC,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD,EAAW,WAAU,uCAAwC,CAAA;KAC7D,kBAAC,QAAD;MACE,WAAU;MACV,OAAO,EAAM;gBAEZ,EAAM;MACF,CAAA;KACP,kBAAC,UAAD;MACE,MAAK;MACL,eA9BiB;AAEzB,OADA,GAAO,EACP,EAAS,KAAK;;MA6BI;MACV,cAAW;MACX,WAAU;gBAEV,kBAAC,GAAD,EAAG,WAAU,WAAY,CAAA;MAClB,CAAA;KACL;QACJ,IACF,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD,EAAS,MAAK,MAAO,CAAA;KACrB,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OAA8C;OAAY;OAAS;OAAQ;;KAC3E,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,OAAD;OACE,WAAU;OACV,OAAO,EAAE,OAAO,GAAG,EAAS,IAAI;OAChC,CAAA;MACE,CAAA;KACF;QAEN,kBAAC,GAAD;IACE,MAAK;IACL,SAAQ;IACE;IACV,eAAe,EAAS,SAAS,OAAO;IACxC,WAAU;cALZ,CAOE,kBAAC,GAAD,EAAQ,WAAU,kBAAmB,CAAA,EAAA,eAC9B;;GAGV,KAAQ,CAAC,KAAS,kBAAC,KAAD;IAAG,WAAU;cAA8B;IAAS,CAAA;GACtE,KAAS,kBAAC,KAAD;IAAG,WAAU;cAAkC;IAAU,CAAA;GAC/D"}
|
|
@@ -1,3 +1,36 @@
|
|
|
1
|
+
import { useMybFile as e } from "../hooks/useMybFiles.js";
|
|
1
2
|
import "../hooks/index.js";
|
|
2
|
-
import "lucide-react";
|
|
3
|
-
import "react/jsx-runtime";
|
|
3
|
+
import { FileText as t, Loader2 as n } from "lucide-react";
|
|
4
|
+
import { jsx as r, jsxs as i } from "react/jsx-runtime";
|
|
5
|
+
//#region src/mybodyshield/components/FileLink.tsx
|
|
6
|
+
function a({ fileId: a, className: o }) {
|
|
7
|
+
let { data: s, isLoading: c, isError: l } = e(a);
|
|
8
|
+
return a ? c ? /* @__PURE__ */ i("span", {
|
|
9
|
+
className: "inline-flex items-center gap-1.5 text-sm text-text-tertiary",
|
|
10
|
+
children: [/* @__PURE__ */ r(n, { className: "h-3.5 w-3.5 animate-spin" }), " Loading attachment…"]
|
|
11
|
+
}) : l || !s ? /* @__PURE__ */ i("span", {
|
|
12
|
+
className: "inline-flex items-center gap-1.5 text-sm text-text-tertiary",
|
|
13
|
+
children: [/* @__PURE__ */ r(t, { className: "h-3.5 w-3.5" }), " Attachment unavailable"]
|
|
14
|
+
}) : s.downloadUrl ? /* @__PURE__ */ i("a", {
|
|
15
|
+
href: s.downloadUrl,
|
|
16
|
+
target: "_blank",
|
|
17
|
+
rel: "noopener noreferrer",
|
|
18
|
+
className: o ?? "inline-flex items-center gap-1.5 text-sm font-medium text-action-primary-bg hover:underline",
|
|
19
|
+
children: [
|
|
20
|
+
/* @__PURE__ */ r(t, { className: "h-3.5 w-3.5" }),
|
|
21
|
+
" ",
|
|
22
|
+
s.name
|
|
23
|
+
]
|
|
24
|
+
}) : /* @__PURE__ */ i("span", {
|
|
25
|
+
className: o ?? "inline-flex items-center gap-1.5 text-sm text-text-secondary",
|
|
26
|
+
children: [
|
|
27
|
+
/* @__PURE__ */ r(t, { className: "h-3.5 w-3.5" }),
|
|
28
|
+
" ",
|
|
29
|
+
s.name
|
|
30
|
+
]
|
|
31
|
+
}) : null;
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { a as FileLink };
|
|
35
|
+
|
|
36
|
+
//# sourceMappingURL=FileLink.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FileLink.js","names":[],"sources":["../../../src/mybodyshield/components/FileLink.tsx"],"sourcesContent":["/**\n * FileLink — resolves a federated File id to a download link (Integration A).\n * @module mybodyshield/components/FileLink\n *\n * The eco queries select `evidence { id }` / `file { id }` (just the id). This\n * resolves the platform File via `getFile(id)` for its name + signed\n * `downloadUrl` so evidence/report artifacts render as a real link.\n */\n\nimport { FileText, Loader2 } from 'lucide-react';\nimport { useMybFile } from '../hooks';\n\ninterface FileLinkProps {\n fileId: string | null | undefined;\n className?: string;\n}\n\nexport function FileLink({ fileId, className }: FileLinkProps) {\n const { data, isLoading, isError } = useMybFile(fileId);\n\n if (!fileId) return null;\n\n if (isLoading) {\n return (\n <span className=\"inline-flex items-center gap-1.5 text-sm text-text-tertiary\">\n <Loader2 className=\"h-3.5 w-3.5 animate-spin\" /> Loading attachment…\n </span>\n );\n }\n\n if (isError || !data) {\n return (\n <span className=\"inline-flex items-center gap-1.5 text-sm text-text-tertiary\">\n <FileText className=\"h-3.5 w-3.5\" /> Attachment unavailable\n </span>\n );\n }\n\n const base =\n 'inline-flex items-center gap-1.5 text-sm font-medium text-action-primary-bg hover:underline';\n\n if (!data.downloadUrl) {\n return (\n <span className={className ?? 'inline-flex items-center gap-1.5 text-sm text-text-secondary'}>\n <FileText className=\"h-3.5 w-3.5\" /> {data.name}\n </span>\n );\n }\n\n return (\n <a\n href={data.downloadUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className={className ?? base}\n >\n <FileText className=\"h-3.5 w-3.5\" /> {data.name}\n </a>\n );\n}\n"],"mappings":";;;;;AAiBA,SAAgB,EAAS,EAAE,WAAQ,gBAA4B;CAC7D,IAAM,EAAE,SAAM,cAAW,eAAY,EAAW,EAAO;AA+BvD,QA7BK,IAED,IAEA,kBAAC,QAAD;EAAM,WAAU;YAAhB,CACE,kBAAC,GAAD,EAAS,WAAU,4BAA6B,CAAA,EAAA,uBAC3C;MAIP,KAAW,CAAC,IAEZ,kBAAC,QAAD;EAAM,WAAU;YAAhB,CACE,kBAAC,GAAD,EAAU,WAAU,eAAgB,CAAA,EAAA,0BAC/B;MAON,EAAK,cASR,kBAAC,KAAD;EACE,MAAM,EAAK;EACX,QAAO;EACP,KAAI;EACJ,WAAW,KAAa;YAJ1B;GAME,kBAAC,GAAD,EAAU,WAAU,eAAgB,CAAA;;GAAE,EAAK;GACzC;MAdF,kBAAC,QAAD;EAAM,WAAW,KAAa;YAA9B;GACE,kBAAC,GAAD,EAAU,WAAU,eAAgB,CAAA;;GAAE,EAAK;GACtC;MAzBS"}
|
|
@@ -1,8 +1,109 @@
|
|
|
1
|
-
import "
|
|
2
|
-
import "
|
|
1
|
+
import { useMyBodyShield as e } from "../context/MyBodyShieldContext.js";
|
|
2
|
+
import { formatDateTime as t } from "../utils/formatters.js";
|
|
3
|
+
import { ErrorView as n, LoadingView as r, Section as i } from "./StateViews.js";
|
|
4
|
+
import { EmptyState as a } from "./EmptyState.js";
|
|
3
5
|
import "../context/index.js";
|
|
6
|
+
import { useMybComments as o, useMybCreateComment as s } from "../hooks/useMybComments.js";
|
|
4
7
|
import "../hooks/index.js";
|
|
5
|
-
import "react";
|
|
6
|
-
import "lucide-react";
|
|
7
|
-
import "react/jsx-runtime";
|
|
8
|
-
import "@burdenoff/fe-libs/ui";
|
|
8
|
+
import { useState as c } from "react";
|
|
9
|
+
import { MessageSquare as l, Send as u } from "lucide-react";
|
|
10
|
+
import { jsx as d, jsxs as f } from "react/jsx-runtime";
|
|
11
|
+
import { Button as p, Textarea as m } from "@burdenoff/fe-libs/ui";
|
|
12
|
+
//#region src/mybodyshield/components/MybDiscussion.tsx
|
|
13
|
+
function h(e) {
|
|
14
|
+
if (!e) return "You";
|
|
15
|
+
let t = [e.firstName, e.lastName].filter(Boolean).join(" ").trim();
|
|
16
|
+
return e.name || t || e.username || e.email || "You";
|
|
17
|
+
}
|
|
18
|
+
function g(e) {
|
|
19
|
+
let t = e.trim();
|
|
20
|
+
return t ? t[0].toUpperCase() : "?";
|
|
21
|
+
}
|
|
22
|
+
function _({ entityType: _, entityId: v, title: y = "Discussion", description: b = "Coordinate review and verification with your team." }) {
|
|
23
|
+
let { currentUser: x } = e(), { data: S, isLoading: C, isError: w, refetch: T } = o(_, v), E = s(_, v), [D, O] = c(""), [k, A] = c(null), j = h(x), M = (e) => x?.id && e.authorId === x.id ? j : "Workspace member", N = async () => {
|
|
24
|
+
let e = D.trim();
|
|
25
|
+
if (e) {
|
|
26
|
+
A(null);
|
|
27
|
+
try {
|
|
28
|
+
await E.mutateAsync(e), O("");
|
|
29
|
+
} catch (e) {
|
|
30
|
+
A(e instanceof Error ? e.message : "Failed to post comment.");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}, P = S?.items ?? [];
|
|
34
|
+
return /* @__PURE__ */ d(i, {
|
|
35
|
+
title: y,
|
|
36
|
+
description: b,
|
|
37
|
+
children: /* @__PURE__ */ f("div", {
|
|
38
|
+
className: "rounded-xl border border-border-seam bg-bg-surface/40 p-4",
|
|
39
|
+
children: [/* @__PURE__ */ f("div", {
|
|
40
|
+
className: "flex gap-3",
|
|
41
|
+
children: [/* @__PURE__ */ d("div", {
|
|
42
|
+
className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-action-primary-bg/15 text-sm font-semibold text-action-primary-bg",
|
|
43
|
+
children: g(j)
|
|
44
|
+
}), /* @__PURE__ */ f("div", {
|
|
45
|
+
className: "flex-1",
|
|
46
|
+
children: [/* @__PURE__ */ d(m, {
|
|
47
|
+
value: D,
|
|
48
|
+
onChange: (e) => O(e.target.value),
|
|
49
|
+
placeholder: "Add a comment for your team…",
|
|
50
|
+
rows: 2
|
|
51
|
+
}), /* @__PURE__ */ f("div", {
|
|
52
|
+
className: "mt-2 flex items-center justify-end gap-2",
|
|
53
|
+
children: [k && /* @__PURE__ */ d("span", {
|
|
54
|
+
className: "mr-auto text-xs text-status-error-text",
|
|
55
|
+
children: k
|
|
56
|
+
}), /* @__PURE__ */ f(p, {
|
|
57
|
+
size: "sm",
|
|
58
|
+
onClick: N,
|
|
59
|
+
disabled: !D.trim() || E.isPending,
|
|
60
|
+
children: [/* @__PURE__ */ d(u, { className: "mr-1.5 h-4 w-4" }), E.isPending ? "Posting…" : "Comment"]
|
|
61
|
+
})]
|
|
62
|
+
})]
|
|
63
|
+
})]
|
|
64
|
+
}), /* @__PURE__ */ d("div", {
|
|
65
|
+
className: "mt-4 border-t border-border-seam pt-4",
|
|
66
|
+
children: C ? /* @__PURE__ */ d(r, { label: "Loading discussion…" }) : w ? /* @__PURE__ */ d(n, {
|
|
67
|
+
message: "Couldn't load the discussion.",
|
|
68
|
+
onRetry: () => T()
|
|
69
|
+
}) : P.length === 0 ? /* @__PURE__ */ d(a, {
|
|
70
|
+
illustration: "empty-data",
|
|
71
|
+
icon: /* @__PURE__ */ d(l, { className: "h-9 w-9" }),
|
|
72
|
+
title: "No comments yet",
|
|
73
|
+
description: "Start the discussion by adding the first comment."
|
|
74
|
+
}) : /* @__PURE__ */ d("ul", {
|
|
75
|
+
className: "flex flex-col gap-4",
|
|
76
|
+
children: P.map((e) => {
|
|
77
|
+
let n = M(e), r = e.content.text ?? e.content.markdown ?? "";
|
|
78
|
+
return /* @__PURE__ */ f("li", {
|
|
79
|
+
className: "flex gap-3",
|
|
80
|
+
children: [/* @__PURE__ */ d("div", {
|
|
81
|
+
className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-sm font-semibold text-text-secondary",
|
|
82
|
+
children: g(n)
|
|
83
|
+
}), /* @__PURE__ */ f("div", {
|
|
84
|
+
className: "min-w-0 flex-1",
|
|
85
|
+
children: [/* @__PURE__ */ f("div", {
|
|
86
|
+
className: "flex flex-wrap items-baseline gap-x-2",
|
|
87
|
+
children: [/* @__PURE__ */ d("span", {
|
|
88
|
+
className: "text-sm font-semibold text-text-primary",
|
|
89
|
+
children: n
|
|
90
|
+
}), /* @__PURE__ */ f("span", {
|
|
91
|
+
className: "text-xs text-text-tertiary",
|
|
92
|
+
children: [t(e.createdAt), e.edited ? " · edited" : ""]
|
|
93
|
+
})]
|
|
94
|
+
}), /* @__PURE__ */ d("p", {
|
|
95
|
+
className: "mt-0.5 whitespace-pre-wrap break-words text-sm text-text-secondary",
|
|
96
|
+
children: r
|
|
97
|
+
})]
|
|
98
|
+
})]
|
|
99
|
+
}, e.id);
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
})]
|
|
103
|
+
})
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
//#endregion
|
|
107
|
+
export { _ as MybDiscussion };
|
|
108
|
+
|
|
109
|
+
//# sourceMappingURL=MybDiscussion.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MybDiscussion.js","names":[],"sources":["../../../src/mybodyshield/components/MybDiscussion.tsx"],"sourcesContent":["/**\n * MybDiscussion — a comment thread on any mybodyshield entity (Integration B).\n * @module mybodyshield/components/MybDiscussion\n *\n * Reuses the platform EntityComment API (`getEntityComments` /\n * `createEntityComment`) keyed by `(entityType, entityId)` — no mybodyshield-svc change.\n * Renders root comments (author, body, time) + a composer. Robust to empty\n * state and errors.\n */\n\nimport { useState } from 'react';\nimport { MessageSquare, Send } from 'lucide-react';\nimport { Button, Textarea } from '@burdenoff/fe-libs/ui';\nimport { Section, LoadingView, ErrorView } from './StateViews';\nimport { EmptyState } from './EmptyState';\nimport { useMyBodyShield } from '../context';\nimport { useMybComments, useMybCreateComment, type MybComment } from '../hooks';\nimport { formatDateTime } from '../utils/formatters';\n\ninterface MybDiscussionProps {\n entityType: string;\n entityId: string;\n title?: string;\n description?: string;\n}\n\nfunction displayName(user: ReturnType<typeof useMyBodyShield>['currentUser']): string {\n if (!user) return 'You';\n const full = [user.firstName, user.lastName].filter(Boolean).join(' ').trim();\n return user.name || full || user.username || user.email || 'You';\n}\n\nfunction initialFor(label: string): string {\n const trimmed = label.trim();\n return trimmed ? trimmed[0]!.toUpperCase() : '?';\n}\n\nexport function MybDiscussion({\n entityType,\n entityId,\n title = 'Discussion',\n description = 'Coordinate review and verification with your team.',\n}: MybDiscussionProps) {\n const { currentUser } = useMyBodyShield();\n const { data, isLoading, isError, refetch } = useMybComments(entityType, entityId);\n const createComment = useMybCreateComment(entityType, entityId);\n\n const [draft, setDraft] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const selfName = displayName(currentUser);\n\n const authorLabel = (comment: MybComment): string =>\n currentUser?.id && comment.authorId === currentUser.id ? selfName : 'Workspace member';\n\n const handleSubmit = async () => {\n const text = draft.trim();\n if (!text) return;\n setError(null);\n try {\n await createComment.mutateAsync(text);\n setDraft('');\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to post comment.');\n }\n };\n\n const comments = data?.items ?? [];\n\n return (\n <Section title={title} description={description}>\n <div className=\"rounded-xl border border-border-seam bg-bg-surface/40 p-4\">\n {/* Composer */}\n <div className=\"flex gap-3\">\n <div className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-action-primary-bg/15 text-sm font-semibold text-action-primary-bg\">\n {initialFor(selfName)}\n </div>\n <div className=\"flex-1\">\n <Textarea\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n placeholder=\"Add a comment for your team…\"\n rows={2}\n />\n <div className=\"mt-2 flex items-center justify-end gap-2\">\n {error && <span className=\"mr-auto text-xs text-status-error-text\">{error}</span>}\n <Button\n size=\"sm\"\n onClick={handleSubmit}\n disabled={!draft.trim() || createComment.isPending}\n >\n <Send className=\"mr-1.5 h-4 w-4\" />\n {createComment.isPending ? 'Posting…' : 'Comment'}\n </Button>\n </div>\n </div>\n </div>\n\n {/* Thread */}\n <div className=\"mt-4 border-t border-border-seam pt-4\">\n {isLoading ? (\n <LoadingView label=\"Loading discussion…\" />\n ) : isError ? (\n <ErrorView message=\"Couldn't load the discussion.\" onRetry={() => refetch()} />\n ) : comments.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<MessageSquare className=\"h-9 w-9\" />}\n title=\"No comments yet\"\n description=\"Start the discussion by adding the first comment.\"\n />\n ) : (\n <ul className=\"flex flex-col gap-4\">\n {comments.map((comment) => {\n const label = authorLabel(comment);\n const body = comment.content.text ?? comment.content.markdown ?? '';\n return (\n <li key={comment.id} className=\"flex gap-3\">\n <div className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-sm font-semibold text-text-secondary\">\n {initialFor(label)}\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-baseline gap-x-2\">\n <span className=\"text-sm font-semibold text-text-primary\">{label}</span>\n <span className=\"text-xs text-text-tertiary\">\n {formatDateTime(comment.createdAt)}\n {comment.edited ? ' · edited' : ''}\n </span>\n </div>\n <p className=\"mt-0.5 whitespace-pre-wrap break-words text-sm text-text-secondary\">\n {body}\n </p>\n </div>\n </li>\n );\n })}\n </ul>\n )}\n </div>\n </div>\n </Section>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,149 @@
|
|
|
1
|
+
import { useMybAssignTag as e, useMybCreateAndAssignTag as t, useMybEntityTags as n, useMybRemoveTagAssignment as r, useMybTagSearch as i } from "../hooks/useMybTags.js";
|
|
1
2
|
import "../hooks/index.js";
|
|
2
|
-
import "react";
|
|
3
|
-
import "lucide-react";
|
|
4
|
-
import "react/jsx-runtime";
|
|
5
|
-
import "@burdenoff/fe-libs/ui";
|
|
3
|
+
import { useEffect as a, useState as o } from "react";
|
|
4
|
+
import { Loader2 as s, Plus as c, Tag as l, X as u } from "lucide-react";
|
|
5
|
+
import { Fragment as d, jsx as f, jsxs as p } from "react/jsx-runtime";
|
|
6
|
+
import { Button as m, Input as h, Popover as g, PopoverContent as _, PopoverTrigger as v, Spinner as y } from "@burdenoff/fe-libs/ui";
|
|
7
|
+
//#region src/mybodyshield/components/MybTags.tsx
|
|
8
|
+
function b({ entityType: b, entityId: x, label: S = "Tags" }) {
|
|
9
|
+
let C = n(b, x), w = e(b, x), T = r(b, x), E = t(b, x), [D, O] = o(!1), [k, A] = o(""), [j, M] = o(""), [N, P] = o(null);
|
|
10
|
+
a(() => {
|
|
11
|
+
let e = setTimeout(() => M(k), 250);
|
|
12
|
+
return () => clearTimeout(e);
|
|
13
|
+
}, [k]);
|
|
14
|
+
let F = i(j), I = C.data ?? [], L = new Set(I.map((e) => e.tag.id)), R = j.trim(), z = (F.data ?? []).filter((e) => !L.has(e.id)), B = R.length > 0 && [...I.map((e) => e.tag.label), ...(F.data ?? []).map((e) => e.label)].some((e) => e.toLowerCase() === R.toLowerCase()), V = R.length > 0 && !B, H = w.isPending || E.isPending, U = () => {
|
|
15
|
+
A(""), M(""), P(null);
|
|
16
|
+
}, W = async (e) => {
|
|
17
|
+
P(null);
|
|
18
|
+
try {
|
|
19
|
+
await w.mutateAsync(e), U(), O(!1);
|
|
20
|
+
} catch (e) {
|
|
21
|
+
P(e instanceof Error ? e.message : "Failed to add tag.");
|
|
22
|
+
}
|
|
23
|
+
}, G = async () => {
|
|
24
|
+
if (R) {
|
|
25
|
+
P(null);
|
|
26
|
+
try {
|
|
27
|
+
await E.mutateAsync(R), U(), O(!1);
|
|
28
|
+
} catch (e) {
|
|
29
|
+
P(e instanceof Error ? e.message : "Failed to create tag.");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}, K = async (e) => {
|
|
33
|
+
P(null);
|
|
34
|
+
try {
|
|
35
|
+
await T.mutateAsync(e);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
P(e instanceof Error ? e.message : "Failed to remove tag.");
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
return /* @__PURE__ */ p("div", { children: [
|
|
41
|
+
/* @__PURE__ */ f("p", {
|
|
42
|
+
className: "mb-1.5 text-xs text-text-tertiary",
|
|
43
|
+
children: S
|
|
44
|
+
}),
|
|
45
|
+
/* @__PURE__ */ p("div", {
|
|
46
|
+
className: "flex flex-wrap items-center gap-2",
|
|
47
|
+
children: [C.isLoading ? /* @__PURE__ */ p("span", {
|
|
48
|
+
className: "inline-flex items-center gap-1.5 text-sm text-text-tertiary",
|
|
49
|
+
children: [/* @__PURE__ */ f(s, { className: "h-3.5 w-3.5 animate-spin" }), " Loading tags…"]
|
|
50
|
+
}) : I.map((e) => /* @__PURE__ */ p("span", {
|
|
51
|
+
className: "inline-flex items-center gap-1.5 rounded-full border border-border-seam bg-bg-sunken px-2.5 py-1 text-xs text-text-secondary",
|
|
52
|
+
children: [
|
|
53
|
+
e.tag.color ? /* @__PURE__ */ f("span", {
|
|
54
|
+
className: "h-2 w-2 shrink-0 rounded-full",
|
|
55
|
+
style: { backgroundColor: e.tag.color },
|
|
56
|
+
"aria-hidden": !0
|
|
57
|
+
}) : /* @__PURE__ */ f(l, { className: "h-3 w-3 text-text-tertiary" }),
|
|
58
|
+
/* @__PURE__ */ f("span", {
|
|
59
|
+
className: "max-w-[10rem] truncate",
|
|
60
|
+
children: e.tag.label
|
|
61
|
+
}),
|
|
62
|
+
/* @__PURE__ */ f("button", {
|
|
63
|
+
type: "button",
|
|
64
|
+
onClick: () => K(e.id),
|
|
65
|
+
disabled: T.isPending,
|
|
66
|
+
"aria-label": `Remove tag ${e.tag.label}`,
|
|
67
|
+
className: "rounded-full p-0.5 text-text-tertiary hover:bg-bg-canvas hover:text-text-primary",
|
|
68
|
+
children: /* @__PURE__ */ f(u, { className: "h-3 w-3" })
|
|
69
|
+
})
|
|
70
|
+
]
|
|
71
|
+
}, e.id)), /* @__PURE__ */ p(g, {
|
|
72
|
+
open: D,
|
|
73
|
+
onOpenChange: (e) => {
|
|
74
|
+
O(e), e || U();
|
|
75
|
+
},
|
|
76
|
+
children: [/* @__PURE__ */ f(v, {
|
|
77
|
+
asChild: !0,
|
|
78
|
+
children: /* @__PURE__ */ p(m, {
|
|
79
|
+
size: "sm",
|
|
80
|
+
variant: "outline",
|
|
81
|
+
className: "h-7 gap-1 px-2 text-xs",
|
|
82
|
+
children: [/* @__PURE__ */ f(c, { className: "h-3.5 w-3.5" }), " Add tag"]
|
|
83
|
+
})
|
|
84
|
+
}), /* @__PURE__ */ p(_, {
|
|
85
|
+
align: "start",
|
|
86
|
+
className: "w-64 p-2",
|
|
87
|
+
children: [/* @__PURE__ */ f(h, {
|
|
88
|
+
autoFocus: !0,
|
|
89
|
+
value: k,
|
|
90
|
+
onChange: (e) => A(e.target.value),
|
|
91
|
+
placeholder: "Search or create a tag…",
|
|
92
|
+
className: "h-8"
|
|
93
|
+
}), /* @__PURE__ */ f("div", {
|
|
94
|
+
className: "mt-2 max-h-56 overflow-y-auto",
|
|
95
|
+
children: F.isFetching && R.length > 0 ? /* @__PURE__ */ p("div", {
|
|
96
|
+
className: "flex items-center gap-2 px-1 py-2 text-sm text-text-tertiary",
|
|
97
|
+
children: [/* @__PURE__ */ f(y, { size: "sm" }), " Searching…"]
|
|
98
|
+
}) : /* @__PURE__ */ p(d, { children: [
|
|
99
|
+
z.map((e) => /* @__PURE__ */ p("button", {
|
|
100
|
+
type: "button",
|
|
101
|
+
onClick: () => W(e.id),
|
|
102
|
+
disabled: H,
|
|
103
|
+
className: "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm text-text-secondary hover:bg-bg-sunken disabled:opacity-60",
|
|
104
|
+
children: [e.color ? /* @__PURE__ */ f("span", {
|
|
105
|
+
className: "h-2 w-2 shrink-0 rounded-full",
|
|
106
|
+
style: { backgroundColor: e.color },
|
|
107
|
+
"aria-hidden": !0
|
|
108
|
+
}) : /* @__PURE__ */ f(l, { className: "h-3.5 w-3.5 text-text-tertiary" }), /* @__PURE__ */ f("span", {
|
|
109
|
+
className: "truncate",
|
|
110
|
+
children: e.label
|
|
111
|
+
})]
|
|
112
|
+
}, e.id)),
|
|
113
|
+
V && /* @__PURE__ */ p("button", {
|
|
114
|
+
type: "button",
|
|
115
|
+
onClick: G,
|
|
116
|
+
disabled: H,
|
|
117
|
+
className: "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm text-action-primary-bg hover:bg-bg-sunken disabled:opacity-60",
|
|
118
|
+
children: [/* @__PURE__ */ f(c, { className: "h-3.5 w-3.5 shrink-0" }), /* @__PURE__ */ p("span", {
|
|
119
|
+
className: "truncate",
|
|
120
|
+
children: [
|
|
121
|
+
"Create “",
|
|
122
|
+
R,
|
|
123
|
+
"”"
|
|
124
|
+
]
|
|
125
|
+
})]
|
|
126
|
+
}),
|
|
127
|
+
!V && z.length === 0 && R.length > 0 && /* @__PURE__ */ f("p", {
|
|
128
|
+
className: "px-2 py-2 text-sm text-text-tertiary",
|
|
129
|
+
children: "Already added."
|
|
130
|
+
}),
|
|
131
|
+
R.length === 0 && /* @__PURE__ */ f("p", {
|
|
132
|
+
className: "px-2 py-2 text-sm text-text-tertiary",
|
|
133
|
+
children: "Type to search existing tags or create a new one."
|
|
134
|
+
})
|
|
135
|
+
] })
|
|
136
|
+
})]
|
|
137
|
+
})]
|
|
138
|
+
})]
|
|
139
|
+
}),
|
|
140
|
+
N && /* @__PURE__ */ f("p", {
|
|
141
|
+
className: "mt-1.5 text-xs text-status-error-text",
|
|
142
|
+
children: N
|
|
143
|
+
})
|
|
144
|
+
] });
|
|
145
|
+
}
|
|
146
|
+
//#endregion
|
|
147
|
+
export { b as MybTags };
|
|
148
|
+
|
|
149
|
+
//# sourceMappingURL=MybTags.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MybTags.js","names":[],"sources":["../../../src/mybodyshield/components/MybTags.tsx"],"sourcesContent":["/**\n * MybTags — a lightweight tag control for mybodyshield entities (Integration C).\n * @module mybodyshield/components/MybTags\n *\n * The mybodyshield svc exposes no tags field, so this drives the platform tag store\n * directly: `getEntityTags` (display), `searchTags` (+ `createTag`) to pick or\n * create, `assignTag`/`removeTagAssignment` to link — all via the workspace\n * gateway. No mybodyshield-svc change.\n */\n\nimport { useEffect, useState } from 'react';\nimport { Loader2, Plus, Tag as TagIcon, X } from 'lucide-react';\nimport {\n Button,\n Input,\n Popover,\n PopoverContent,\n PopoverTrigger,\n Spinner,\n} from '@burdenoff/fe-libs/ui';\nimport {\n useMybEntityTags,\n useMybTagSearch,\n useMybAssignTag,\n useMybRemoveTagAssignment,\n useMybCreateAndAssignTag,\n} from '../hooks';\n\ninterface MybTagsProps {\n entityType: string;\n entityId: string;\n label?: string;\n}\n\nexport function MybTags({ entityType, entityId, label = 'Tags' }: MybTagsProps) {\n const tags = useMybEntityTags(entityType, entityId);\n const assign = useMybAssignTag(entityType, entityId);\n const remove = useMybRemoveTagAssignment(entityType, entityId);\n const createAndAssign = useMybCreateAndAssignTag(entityType, entityId);\n\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState('');\n const [debounced, setDebounced] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n useEffect(() => {\n const timer = setTimeout(() => setDebounced(query), 250);\n return () => clearTimeout(timer);\n }, [query]);\n\n const search = useMybTagSearch(debounced);\n\n const assignments = tags.data ?? [];\n const assignedIds = new Set(assignments.map((a) => a.tag.id));\n const q = debounced.trim();\n const searchResults = (search.data ?? []).filter((t) => !assignedIds.has(t.id));\n const hasExact =\n q.length > 0 &&\n [...assignments.map((a) => a.tag.label), ...(search.data ?? []).map((t) => t.label)].some(\n (l) => l.toLowerCase() === q.toLowerCase()\n );\n const showCreate = q.length > 0 && !hasExact;\n const busy = assign.isPending || createAndAssign.isPending;\n\n const resetInput = () => {\n setQuery('');\n setDebounced('');\n setError(null);\n };\n\n const handleAssign = async (tagId: string) => {\n setError(null);\n try {\n await assign.mutateAsync(tagId);\n resetInput();\n setOpen(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to add tag.');\n }\n };\n\n const handleCreate = async () => {\n if (!q) return;\n setError(null);\n try {\n await createAndAssign.mutateAsync(q);\n resetInput();\n setOpen(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to create tag.');\n }\n };\n\n const handleRemove = async (assignmentId: string) => {\n setError(null);\n try {\n await remove.mutateAsync(assignmentId);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to remove tag.');\n }\n };\n\n return (\n <div>\n <p className=\"mb-1.5 text-xs text-text-tertiary\">{label}</p>\n <div className=\"flex flex-wrap items-center gap-2\">\n {tags.isLoading ? (\n <span className=\"inline-flex items-center gap-1.5 text-sm text-text-tertiary\">\n <Loader2 className=\"h-3.5 w-3.5 animate-spin\" /> Loading tags…\n </span>\n ) : (\n assignments.map((assignment) => (\n <span\n key={assignment.id}\n className=\"inline-flex items-center gap-1.5 rounded-full border border-border-seam bg-bg-sunken px-2.5 py-1 text-xs text-text-secondary\"\n >\n {assignment.tag.color ? (\n <span\n className=\"h-2 w-2 shrink-0 rounded-full\"\n style={{ backgroundColor: assignment.tag.color }}\n aria-hidden\n />\n ) : (\n <TagIcon className=\"h-3 w-3 text-text-tertiary\" />\n )}\n <span className=\"max-w-[10rem] truncate\">{assignment.tag.label}</span>\n <button\n type=\"button\"\n onClick={() => handleRemove(assignment.id)}\n disabled={remove.isPending}\n aria-label={`Remove tag ${assignment.tag.label}`}\n className=\"rounded-full p-0.5 text-text-tertiary hover:bg-bg-canvas hover:text-text-primary\"\n >\n <X className=\"h-3 w-3\" />\n </button>\n </span>\n ))\n )}\n\n <Popover\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (!next) resetInput();\n }}\n >\n <PopoverTrigger asChild>\n <Button size=\"sm\" variant=\"outline\" className=\"h-7 gap-1 px-2 text-xs\">\n <Plus className=\"h-3.5 w-3.5\" /> Add tag\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-64 p-2\">\n <Input\n autoFocus\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search or create a tag…\"\n className=\"h-8\"\n />\n <div className=\"mt-2 max-h-56 overflow-y-auto\">\n {search.isFetching && q.length > 0 ? (\n <div className=\"flex items-center gap-2 px-1 py-2 text-sm text-text-tertiary\">\n <Spinner size=\"sm\" /> Searching…\n </div>\n ) : (\n <>\n {searchResults.map((tag) => (\n <button\n key={tag.id}\n type=\"button\"\n onClick={() => handleAssign(tag.id)}\n disabled={busy}\n className=\"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm text-text-secondary hover:bg-bg-sunken disabled:opacity-60\"\n >\n {tag.color ? (\n <span\n className=\"h-2 w-2 shrink-0 rounded-full\"\n style={{ backgroundColor: tag.color }}\n aria-hidden\n />\n ) : (\n <TagIcon className=\"h-3.5 w-3.5 text-text-tertiary\" />\n )}\n <span className=\"truncate\">{tag.label}</span>\n </button>\n ))}\n\n {showCreate && (\n <button\n type=\"button\"\n onClick={handleCreate}\n disabled={busy}\n className=\"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm text-action-primary-bg hover:bg-bg-sunken disabled:opacity-60\"\n >\n <Plus className=\"h-3.5 w-3.5 shrink-0\" />\n <span className=\"truncate\">Create “{q}”</span>\n </button>\n )}\n\n {!showCreate && searchResults.length === 0 && q.length > 0 && (\n <p className=\"px-2 py-2 text-sm text-text-tertiary\">Already added.</p>\n )}\n {q.length === 0 && (\n <p className=\"px-2 py-2 text-sm text-text-tertiary\">\n Type to search existing tags or create a new one.\n </p>\n )}\n </>\n )}\n </div>\n </PopoverContent>\n </Popover>\n </div>\n\n {error && <p className=\"mt-1.5 text-xs text-status-error-text\">{error}</p>}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Pagination.js","names":[],"sources":["../../../src/mybodyshield/components/Pagination.tsx"],"sourcesContent":["/**\n * Pagination —
|
|
1
|
+
{"version":3,"file":"Pagination.js","names":[],"sources":["../../../src/mybodyshield/components/Pagination.tsx"],"sourcesContent":["/**\n * Pagination — offset-based Prev/Next control for Myb* connection lists.\n * @module mybodyshield/components\n *\n * The svc list connections return `pageInfo { cursor, hasMore }` and paginate by\n * cursor (offset is a no-op). Pages track a 0-indexed `page` for display and a\n * cursor stack for navigation; this control exposes Prev/Next, disabling Prev on\n * page 0 and Next when the server reports no more rows.\n */\n\nimport { ChevronLeft, ChevronRight } from 'lucide-react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\n\ninterface PaginationProps {\n page: number;\n pageSize: number;\n /** Number of rows on the current page. */\n itemCount: number;\n /** Server flag: are there more rows after this page? */\n hasMore: boolean;\n onPageChange: (page: number) => void;\n className?: string;\n}\n\nexport function Pagination({\n page,\n pageSize,\n itemCount,\n hasMore,\n onPageChange,\n className,\n}: PaginationProps) {\n // Nothing to paginate: single page that also has no more rows.\n if (page === 0 && !hasMore) return null;\n\n const from = itemCount === 0 ? 0 : page * pageSize + 1;\n const to = page * pageSize + itemCount;\n\n return (\n <div\n className={cn('mt-4 flex flex-col items-center justify-between gap-3 sm:flex-row', className)}\n >\n <p className=\"text-sm text-text-secondary tabular-nums\" aria-live=\"polite\">\n Showing {from}–{to}\n </p>\n <div className=\"flex items-center gap-2\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => onPageChange(page - 1)}\n disabled={page === 0}\n aria-label=\"Previous page\"\n >\n <ChevronLeft className=\"mr-1 h-4 w-4\" /> Previous\n </Button>\n <span className=\"min-w-[4.5rem] text-center text-sm text-text-secondary tabular-nums\">\n Page {page + 1}\n </span>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => onPageChange(page + 1)}\n disabled={!hasMore}\n aria-label=\"Next page\"\n >\n Next <ChevronRight className=\"ml-1 h-4 w-4\" />\n </Button>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;AAyBA,SAAgB,EAAW,EACzB,SACA,aACA,cACA,YACA,iBACA,gBACkB;AAElB,KAAI,MAAS,KAAK,CAAC,EAAS,QAAO;CAEnC,IAAM,IAAO,MAAc,IAAI,IAAI,IAAO,IAAW,GAC/C,IAAK,IAAO,IAAW;AAE7B,QACE,kBAAC,OAAD;EACE,WAAW,EAAG,qEAAqE,EAAU;YAD/F,CAGE,kBAAC,KAAD;GAAG,WAAU;GAA2C,aAAU;aAAlE;IAA2E;IAChE;IAAK;IAAE;IACd;MACJ,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,GAAD;KACE,SAAQ;KACR,MAAK;KACL,eAAe,EAAa,IAAO,EAAE;KACrC,UAAU,MAAS;KACnB,cAAW;eALb,CAOE,kBAAC,GAAD,EAAa,WAAU,gBAAiB,CAAA,EAAA,YACjC;;IACT,kBAAC,QAAD;KAAM,WAAU;eAAhB,CAAsF,SAC9E,IAAO,EACR;;IACP,kBAAC,GAAD;KACE,SAAQ;KACR,MAAK;KACL,eAAe,EAAa,IAAO,EAAE;KACrC,UAAU,CAAC;KACX,cAAW;eALb,CAMC,SACM,kBAAC,GAAD,EAAc,WAAU,gBAAiB,CAAA,CACvC;;IACL;KACF"}
|