@burdenoff/microfe-buildmyiq 2026.716.1 → 2026.720.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.
Files changed (85) hide show
  1. package/dist/buildmyiq/BuildmyiqAdminRoot.d.ts.map +1 -1
  2. package/dist/buildmyiq/BuildmyiqAdminRoot.js +149 -135
  3. package/dist/buildmyiq/BuildmyiqAdminRoot.js.map +1 -1
  4. package/dist/buildmyiq/BuildmyiqRoot.d.ts.map +1 -1
  5. package/dist/buildmyiq/BuildmyiqRoot.js +59 -54
  6. package/dist/buildmyiq/BuildmyiqRoot.js.map +1 -1
  7. package/dist/buildmyiq/BuildmyiqRoutes.js.map +1 -1
  8. package/dist/buildmyiq/components/Avatar.js.map +1 -1
  9. package/dist/buildmyiq/components/BuiDiscussion.js.map +1 -1
  10. package/dist/buildmyiq/components/BuiPermissionButton.d.ts +25 -0
  11. package/dist/buildmyiq/components/BuiPermissionButton.d.ts.map +1 -0
  12. package/dist/buildmyiq/components/BuiPermissionButton.js +15 -0
  13. package/dist/buildmyiq/components/BuiPermissionButton.js.map +1 -0
  14. package/dist/buildmyiq/components/BuiTags.js.map +1 -1
  15. package/dist/buildmyiq/components/DataTable.js.map +1 -1
  16. package/dist/buildmyiq/components/EmptyState.js.map +1 -1
  17. package/dist/buildmyiq/components/EvidenceUpload.js.map +1 -1
  18. package/dist/buildmyiq/components/FileLink.js.map +1 -1
  19. package/dist/buildmyiq/components/FilterTabs.js.map +1 -1
  20. package/dist/buildmyiq/components/MetricsCard.js.map +1 -1
  21. package/dist/buildmyiq/components/MiniBar.js.map +1 -1
  22. package/dist/buildmyiq/components/PageLayout.js.map +1 -1
  23. package/dist/buildmyiq/components/Pagination.js.map +1 -1
  24. package/dist/buildmyiq/components/StateViews.js.map +1 -1
  25. package/dist/buildmyiq/components/StatusBadge.js.map +1 -1
  26. package/dist/buildmyiq/components/Tabs.js.map +1 -1
  27. package/dist/buildmyiq/components/Viz.js.map +1 -1
  28. package/dist/buildmyiq/components/index.d.ts +1 -0
  29. package/dist/buildmyiq/components/index.d.ts.map +1 -1
  30. package/dist/buildmyiq/components/index.js +1 -0
  31. package/dist/buildmyiq/context/BuildmyiqContext.js.map +1 -1
  32. package/dist/buildmyiq/hooks/index.d.ts +1 -0
  33. package/dist/buildmyiq/hooks/index.d.ts.map +1 -1
  34. package/dist/buildmyiq/hooks/index.js +1 -0
  35. package/dist/buildmyiq/hooks/useBuiComments.js.map +1 -1
  36. package/dist/buildmyiq/hooks/useBuiFiles.js.map +1 -1
  37. package/dist/buildmyiq/hooks/useBuiPermissions.d.ts +54 -0
  38. package/dist/buildmyiq/hooks/useBuiPermissions.d.ts.map +1 -0
  39. package/dist/buildmyiq/hooks/useBuiPermissions.js +2 -0
  40. package/dist/buildmyiq/hooks/useBuiTags.js.map +1 -1
  41. package/dist/buildmyiq/hooks/useBuildmyiqApi.js.map +1 -1
  42. package/dist/buildmyiq/hooks/useCursorPager.js.map +1 -1
  43. package/dist/buildmyiq/pages/DashboardPage.js.map +1 -1
  44. package/dist/buildmyiq/pages/_shared/CourseCard.js.map +1 -1
  45. package/dist/buildmyiq/pages/assessments/AssessmentDetailPage.js.map +1 -1
  46. package/dist/buildmyiq/pages/assessments/AssessmentsPage.js.map +1 -1
  47. package/dist/buildmyiq/pages/assessments/TakeAssessmentPage.js.map +1 -1
  48. package/dist/buildmyiq/pages/authoring/AuthoringPage.d.ts.map +1 -1
  49. package/dist/buildmyiq/pages/authoring/AuthoringPage.js +97 -86
  50. package/dist/buildmyiq/pages/authoring/AuthoringPage.js.map +1 -1
  51. package/dist/buildmyiq/pages/authoring/CourseBuilderPage.d.ts.map +1 -1
  52. package/dist/buildmyiq/pages/authoring/CourseBuilderPage.js +159 -118
  53. package/dist/buildmyiq/pages/authoring/CourseBuilderPage.js.map +1 -1
  54. package/dist/buildmyiq/pages/authoring/_forms.js.map +1 -1
  55. package/dist/buildmyiq/pages/catalog/CatalogPage.js.map +1 -1
  56. package/dist/buildmyiq/pages/catalog/CourseDetailPage.d.ts.map +1 -1
  57. package/dist/buildmyiq/pages/catalog/CourseDetailPage.js +75 -69
  58. package/dist/buildmyiq/pages/catalog/CourseDetailPage.js.map +1 -1
  59. package/dist/buildmyiq/pages/certificates/CertificatesPage.d.ts.map +1 -1
  60. package/dist/buildmyiq/pages/certificates/CertificatesPage.js +186 -170
  61. package/dist/buildmyiq/pages/certificates/CertificatesPage.js.map +1 -1
  62. package/dist/buildmyiq/pages/cohorts/CohortsPage.d.ts.map +1 -1
  63. package/dist/buildmyiq/pages/cohorts/CohortsPage.js +177 -166
  64. package/dist/buildmyiq/pages/cohorts/CohortsPage.js.map +1 -1
  65. package/dist/buildmyiq/pages/insights/InsightsPage.js.map +1 -1
  66. package/dist/buildmyiq/pages/learning/LessonPlayerPage.js.map +1 -1
  67. package/dist/buildmyiq/pages/learning/MyLearningPage.js.map +1 -1
  68. package/dist/buildmyiq/pages/paths/PathsPage.js.map +1 -1
  69. package/dist/buildmyiq/pages/skills/SkillsPage.js.map +1 -1
  70. package/dist/buildmyiq/pages/submissions/SubmissionsPage.d.ts.map +1 -1
  71. package/dist/buildmyiq/pages/submissions/SubmissionsPage.js +160 -154
  72. package/dist/buildmyiq/pages/submissions/SubmissionsPage.js.map +1 -1
  73. package/dist/buildmyiq/shared/hooks/useTr.js.map +1 -1
  74. package/dist/buildmyiq/types/index.js.map +1 -1
  75. package/dist/buildmyiq/utils/cn.js.map +1 -1
  76. package/dist/buildmyiq/utils/formatters.js.map +1 -1
  77. package/dist/buildmyiq/utils/navigation.js.map +1 -1
  78. package/dist/node_modules/clsx/dist/clsx.js.map +1 -1
  79. package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js +1 -1
  80. package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -1
  81. package/dist/services/createSandboxAssistantTransport.js.map +1 -1
  82. package/dist/utils/pageContext.d.ts.map +1 -1
  83. package/dist/utils/pageContext.js +7 -2
  84. package/dist/utils/pageContext.js.map +1 -1
  85. package/package.json +2 -2
@@ -0,0 +1,15 @@
1
+ import { jsx as e } from "react/jsx-runtime";
2
+ import { PermissionButton as t } from "@burdenoff/fe-libs/shared/components";
3
+ //#region src/buildmyiq/components/BuiPermissionButton.tsx
4
+ var n = ({ children: n, resource: r, action: i, deniedBehavior: a = "tooltip", deniedTooltip: o, className: s }) => /* @__PURE__ */ e(t, {
5
+ permissions: [`${r}:${i}`, `${r}.${i}`],
6
+ requireAll: !1,
7
+ deniedBehavior: a,
8
+ deniedTooltip: o ?? `You don't have permission to ${i} this ${r.replace("buildmyiq-", "")}.`,
9
+ className: s,
10
+ children: n
11
+ });
12
+ //#endregion
13
+ export { n as BuiPermissionButton };
14
+
15
+ //# sourceMappingURL=BuiPermissionButton.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BuiPermissionButton.js","names":[],"sources":["../../../src/buildmyiq/components/BuiPermissionButton.tsx"],"sourcesContent":["/**\n * BuiPermissionButton — BuildMyIQ-flavoured wrapper over the platform\n * `PermissionButton` primitive from `@burdenoff/fe-libs`.\n *\n * Takes the same `resource` × `action` pair that the service's `@rbac`\n * directives declare, and checks both the `resource:action` and\n * `resource.action` permission-string shapes the RBAC service can emit.\n *\n * This is UI defence-in-depth only — the workspace gateway remains the\n * authoritative enforcement point.\n *\n * @module buildmyiq/components/BuiPermissionButton\n */\nimport type { FC, ReactElement } from 'react';\nimport { PermissionButton } from '@burdenoff/fe-libs/shared/components';\nimport type { BuiAction, BuiResource } from '../hooks/useBuiPermissions';\n\nexport interface BuiPermissionButtonProps {\n /** The control to gate. Must accept a `disabled` prop. */\n children: ReactElement;\n /** RBAC resource, exactly as declared on the subgraph directive. */\n resource: BuiResource;\n /** RBAC action, exactly as declared on the subgraph directive. */\n action: BuiAction;\n /**\n * What to do when the actor lacks the permission.\n * - `hide` for authoring/destructive controls a learner should never see.\n * - `tooltip` for actions a user could plausibly be granted, so the UI\n * explains itself instead of silently vanishing.\n * @default 'tooltip'\n */\n deniedBehavior?: 'disable' | 'hide' | 'tooltip';\n /** Override the default denial explanation. */\n deniedTooltip?: string;\n /** Extra class on the tooltip wrapper (only used with `tooltip`). */\n className?: string;\n}\n\nexport const BuiPermissionButton: FC<BuiPermissionButtonProps> = ({\n children,\n resource,\n action,\n deniedBehavior = 'tooltip',\n deniedTooltip,\n className,\n}) => (\n <PermissionButton\n permissions={[`${resource}:${action}`, `${resource}.${action}`]}\n requireAll={false}\n deniedBehavior={deniedBehavior}\n deniedTooltip={\n deniedTooltip ??\n `You don't have permission to ${action} this ${resource.replace('buildmyiq-', '')}.`\n }\n className={className}\n >\n {children}\n </PermissionButton>\n);\n"],"mappings":";;;AAsCA,IAAa,KAAqD,EAChE,aACA,aACA,WACA,oBAAiB,WACjB,kBACA,mBAEA,kBAAC,GAAD;CACE,aAAa,CAAC,GAAG,EAAS,GAAG,KAAU,GAAG,EAAS,GAAG,GAAQ;CAC9D,YAAY;CACI;CAChB,eACE,KACA,gCAAgC,EAAO,QAAQ,EAAS,QAAQ,cAAc,EAAE,EAAE;CAEzE;CAEV;AACe,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"BuiTags.js","names":[],"sources":["../../../src/buildmyiq/components/BuiTags.tsx"],"sourcesContent":["/**\n * BuiTags — a lightweight tag control for Buildmyiq entities (Integration C).\n * @module buildmyiq/components/BuiTags\n *\n * The buildmyiq 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 buildmyiq-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 useEcoEntityTags,\n useEcoTagSearch,\n useEcoAssignTag,\n useEcoRemoveTagAssignment,\n useEcoCreateAndAssignTag,\n} from '../hooks';\n\ninterface BuiTagsProps {\n entityType: string;\n entityId: string;\n label?: string;\n}\n\nexport function BuiTags({ entityType, entityId, label = 'Tags' }: BuiTagsProps) {\n const tags = useEcoEntityTags(entityType, entityId);\n const assign = useEcoAssignTag(entityType, entityId);\n const remove = useEcoRemoveTagAssignment(entityType, entityId);\n const createAndAssign = useEcoCreateAndAssignTag(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 = useEcoTagSearch(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":";;;;;;;AAkCA,SAAgB,EAAQ,EAAE,eAAY,aAAU,WAAQ,UAAwB;CAC9E,IAAM,IAAO,EAAiB,GAAY,EAAS,EAC7C,IAAS,EAAgB,GAAY,EAAS,EAC9C,IAAS,EAA0B,GAAY,EAAS,EACxD,IAAkB,EAAyB,GAAY,EAAS,EAEhE,CAAC,GAAM,KAAW,EAAS,GAAM,EACjC,CAAC,GAAO,KAAY,EAAS,GAAG,EAChC,CAAC,GAAW,KAAgB,EAAS,GAAG,EACxC,CAAC,GAAO,KAAY,EAAwB,KAAK;AAEvD,SAAgB;EACd,IAAM,IAAQ,iBAAiB,EAAa,EAAM,EAAE,IAAI;AACxD,eAAa,aAAa,EAAM;IAC/B,CAAC,EAAM,CAAC;CAEX,IAAM,IAAS,EAAgB,EAAU,EAEnC,IAAc,EAAK,QAAQ,EAAE,EAC7B,IAAc,IAAI,IAAI,EAAY,KAAK,MAAM,EAAE,IAAI,GAAG,CAAC,EACvD,IAAI,EAAU,MAAM,EACpB,KAAiB,EAAO,QAAQ,EAAE,EAAE,QAAQ,MAAM,CAAC,EAAY,IAAI,EAAE,GAAG,CAAC,EACzE,IACJ,EAAE,SAAS,KACX,CAAC,GAAG,EAAY,KAAK,MAAM,EAAE,IAAI,MAAM,EAAE,IAAI,EAAO,QAAQ,EAAE,EAAE,KAAK,MAAM,EAAE,MAAM,CAAC,CAAC,MAClF,MAAM,EAAE,aAAa,KAAK,EAAE,aAAa,CAC3C,EACG,IAAa,EAAE,SAAS,KAAK,CAAC,GAC9B,IAAO,EAAO,aAAa,EAAgB,WAE3C,UAAmB;AAGvB,EAFA,EAAS,GAAG,EACZ,EAAa,GAAG,EAChB,EAAS,KAAK;IAGV,IAAe,OAAO,MAAkB;AAC5C,IAAS,KAAK;AACd,MAAI;AAGF,GAFA,MAAM,EAAO,YAAY,EAAM,EAC/B,GAAY,EACZ,EAAQ,GAAM;WACP,GAAK;AACZ,KAAS,aAAe,QAAQ,EAAI,UAAU,qBAAqB;;IAIjE,IAAe,YAAY;AAC1B,SACL;KAAS,KAAK;AACd,OAAI;AAGF,IAFA,MAAM,EAAgB,YAAY,EAAE,EACpC,GAAY,EACZ,EAAQ,GAAM;YACP,GAAK;AACZ,MAAS,aAAe,QAAQ,EAAI,UAAU,wBAAwB;;;IAIpE,IAAe,OAAO,MAAyB;AACnD,IAAS,KAAK;AACd,MAAI;AACF,SAAM,EAAO,YAAY,EAAa;WAC/B,GAAK;AACZ,KAAS,aAAe,QAAQ,EAAI,UAAU,wBAAwB;;;AAI1E,QACE,kBAAC,OAAD,EAAA,UAAA;EACE,kBAAC,KAAD;GAAG,WAAU;aAAqC;GAAU,CAAA;EAC5D,kBAAC,OAAD;GAAK,WAAU;aAAf,CACG,EAAK,YACJ,kBAAC,QAAD;IAAM,WAAU;cAAhB,CACE,kBAAC,GAAD,EAAS,WAAU,4BAA6B,CAAA,EAAA,iBAC3C;QAEP,EAAY,KAAK,MACf,kBAAC,QAAD;IAEE,WAAU;cAFZ;KAIG,EAAW,IAAI,QACd,kBAAC,QAAD;MACE,WAAU;MACV,OAAO,EAAE,iBAAiB,EAAW,IAAI,OAAO;MAChD,eAAA;MACA,CAAA,GAEF,kBAAC,GAAD,EAAS,WAAU,8BAA+B,CAAA;KAEpD,kBAAC,QAAD;MAAM,WAAU;gBAA0B,EAAW,IAAI;MAAa,CAAA;KACtE,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAa,EAAW,GAAG;MAC1C,UAAU,EAAO;MACjB,cAAY,cAAc,EAAW,IAAI;MACzC,WAAU;gBAEV,kBAAC,GAAD,EAAG,WAAU,WAAY,CAAA;MAClB,CAAA;KACJ;MAtBA,EAAW,GAsBX,CACP,EAGJ,kBAAC,GAAD;IACQ;IACN,eAAe,MAAS;AAEtB,KADA,EAAQ,EAAK,EACR,KAAM,GAAY;;cAJ3B,CAOE,kBAAC,GAAD;KAAgB,SAAA;eACd,kBAAC,GAAD;MAAQ,MAAK;MAAK,SAAQ;MAAU,WAAU;gBAA9C,CACE,kBAAC,GAAD,EAAM,WAAU,eAAgB,CAAA,EAAA,WACzB;;KACM,CAAA,EACjB,kBAAC,GAAD;KAAgB,OAAM;KAAQ,WAAU;eAAxC,CACE,kBAAC,GAAD;MACE,WAAA;MACA,OAAO;MACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;MACzC,aAAY;MACZ,WAAU;MACV,CAAA,EACF,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAO,cAAc,EAAE,SAAS,IAC/B,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAS,MAAK,MAAO,CAAA,EAAA,cACjB;WAEN,kBAAA,GAAA,EAAA,UAAA;OACG,EAAc,KAAK,MAClB,kBAAC,UAAD;QAEE,MAAK;QACL,eAAe,EAAa,EAAI,GAAG;QACnC,UAAU;QACV,WAAU;kBALZ,CAOG,EAAI,QACH,kBAAC,QAAD;SACE,WAAU;SACV,OAAO,EAAE,iBAAiB,EAAI,OAAO;SACrC,eAAA;SACA,CAAA,GAEF,kBAAC,GAAD,EAAS,WAAU,kCAAmC,CAAA,EAExD,kBAAC,QAAD;SAAM,WAAU;mBAAY,EAAI;SAAa,CAAA,CACtC;UAhBF,EAAI,GAgBF,CACT;OAED,KACC,kBAAC,UAAD;QACE,MAAK;QACL,SAAS;QACT,UAAU;QACV,WAAU;kBAJZ,CAME,kBAAC,GAAD,EAAM,WAAU,wBAAyB,CAAA,EACzC,kBAAC,QAAD;SAAM,WAAU;mBAAhB;UAA2B;UAAS;UAAE;UAAQ;WACvC;;OAGV,CAAC,KAAc,EAAc,WAAW,KAAK,EAAE,SAAS,KACvD,kBAAC,KAAD;QAAG,WAAU;kBAAuC;QAAkB,CAAA;OAEvE,EAAE,WAAW,KACZ,kBAAC,KAAD;QAAG,WAAU;kBAAuC;QAEhD,CAAA;OAEL,EAAA,CAAA;MAED,CAAA,CACS;OACT;MACN;;EAEL,KAAS,kBAAC,KAAD;GAAG,WAAU;aAAyC;GAAU,CAAA;EACtE,EAAA,CAAA"}
1
+ {"version":3,"file":"BuiTags.js","names":[],"sources":["../../../src/buildmyiq/components/BuiTags.tsx"],"sourcesContent":["/**\n * BuiTags — a lightweight tag control for Buildmyiq entities (Integration C).\n * @module buildmyiq/components/BuiTags\n *\n * The buildmyiq 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 buildmyiq-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 useEcoEntityTags,\n useEcoTagSearch,\n useEcoAssignTag,\n useEcoRemoveTagAssignment,\n useEcoCreateAndAssignTag,\n} from '../hooks';\n\ninterface BuiTagsProps {\n entityType: string;\n entityId: string;\n label?: string;\n}\n\nexport function BuiTags({ entityType, entityId, label = 'Tags' }: BuiTagsProps) {\n const tags = useEcoEntityTags(entityType, entityId);\n const assign = useEcoAssignTag(entityType, entityId);\n const remove = useEcoRemoveTagAssignment(entityType, entityId);\n const createAndAssign = useEcoCreateAndAssignTag(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 = useEcoTagSearch(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":"DataTable.js","names":[],"sources":["../../../src/buildmyiq/components/DataTable.tsx"],"sourcesContent":["/**\n * DataTable — a lightweight, semantic-token table for list pages.\n * @module buildmyiq/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 onClick={sortable ? () => toggleSort(col.key) : undefined}\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 sortable && 'cursor-pointer select-none hover:text-text-primary'\n )}\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 {sortable &&\n (active ? (\n sort?.dir === 'asc' ? (\n <ChevronUp className=\"h-3.5 w-3.5 text-[var(--color-accent-primary)]\" />\n ) : (\n <ChevronDown className=\"h-3.5 w-3.5 text-[var(--color-accent-primary)]\" />\n )\n ) : (\n <ChevronsUpDown className=\"h-3.5 w-3.5 opacity-40\" />\n ))}\n </span>\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,SAAS,UAAiB,EAAW,EAAI,IAAI,GAAG,KAAA;OAChD,aACE,IAAU,GAAM,QAAQ,QAAQ,cAAc,eAAgB,KAAA;OAEhE,WAAW,EACT,+DACA,EAAW,EAAI,SAAS,SACxB,EAAI,gBAAgB,wBACpB,KAAY,qDACb;iBAED,kBAAC,QAAD;QACE,WAAW,EACT,kCACA,EAAI,UAAU,WAAW,mBAC1B;kBAJH,CAMG,EAAI,QACJ,MACE,IACC,GAAM,QAAQ,QACZ,kBAAC,GAAD,EAAW,WAAU,kDAAmD,CAAA,GAExE,kBAAC,GAAD,EAAa,WAAU,kDAAmD,CAAA,GAG5E,kBAAC,GAAD,EAAgB,WAAU,0BAA2B,CAAA,EAEpD;;OACJ,EA9BE,EAAI,IA8BN;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
+ {"version":3,"file":"DataTable.js","names":[],"sources":["../../../src/buildmyiq/components/DataTable.tsx"],"sourcesContent":["/**\n * DataTable — a lightweight, semantic-token table for list pages.\n * @module buildmyiq/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 onClick={sortable ? () => toggleSort(col.key) : undefined}\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 sortable && 'cursor-pointer select-none hover:text-text-primary'\n )}\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 {sortable &&\n (active ? (\n sort?.dir === 'asc' ? (\n <ChevronUp className=\"h-3.5 w-3.5 text-[var(--color-accent-primary)]\" />\n ) : (\n <ChevronDown className=\"h-3.5 w-3.5 text-[var(--color-accent-primary)]\" />\n )\n ) : (\n <ChevronsUpDown className=\"h-3.5 w-3.5 opacity-40\" />\n ))}\n </span>\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;AACV;AAEA,SAAgB,EAAa,EAC3B,YACA,SACA,WACA,eACA,cACA,kBACoB;CACpB,IAAM,CAAC,GAAM,KAAW,EACtB,KAAe,IACjB,GAEM,IAAa,QAAc;EAC/B,IAAI,CAAC,GAAM,OAAO;EAClB,IAAM,IAAM,EAAQ,MAAM,MAAM,EAAE,QAAQ,EAAK,GAAG;EAClD,IAAI,CAAC,GAAK,WAAW,OAAO;EAC5B,IAAM,IAAM,EAAK,QAAQ,QAAQ,IAAI;EACrC,OAAO,CAAC,GAAG,CAAI,EAAE,MAAM,GAAG,MAAM;GAC9B,IAAM,IAAK,EAAI,UAAW,CAAC,GACrB,IAAK,EAAI,UAAW,CAAC;GAG3B,OAFI,IAAK,IAAW,KAAK,IACrB,IAAK,IAAW,IAAI,IACjB;EACT,CAAC;CACH,GAAG;EAAC;EAAM;EAAM;CAAO,CAAC,GAElB,KAAc,MAAgB;EAClC,GAAS,MACP,GAAM,QAAQ,IAAM;GAAE;GAAK,KAAK,EAAK,QAAQ,QAAQ,SAAS;EAAM,IAAI;GAAE;GAAK,KAAK;EAAM,CAC5F;CACF;CAEA,OACE,kBAAC,GAAD;EAAW,WAAU;EAAQ,WAAW,EAAG,mBAAmB,CAAS;YACrE,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;MACjC,OACE,kBAAC,MAAD;OAEE,SAAS,UAAiB,EAAW,EAAI,GAAG,IAAI,KAAA;OAChD,aACE,IAAU,GAAM,QAAQ,QAAQ,cAAc,eAAgB,KAAA;OAEhE,WAAW,EACT,+DACA,EAAW,EAAI,SAAS,SACxB,EAAI,gBAAgB,wBACpB,KAAY,oDACd;iBAEA,kBAAC,QAAD;QACE,WAAW,EACT,kCACA,EAAI,UAAU,WAAW,kBAC3B;kBAJF,CAMG,EAAI,QACJ,MACE,IACC,GAAM,QAAQ,QACZ,kBAAC,GAAD,EAAW,WAAU,iDAAkD,CAAA,IAEvE,kBAAC,GAAD,EAAa,WAAU,iDAAkD,CAAA,IAG3E,kBAAC,GAAD,EAAgB,WAAU,yBAA0B,CAAA,EAEpD;;MACJ,GA9BG,EAAI,GA8BP;KAER,CAAC;IACC,CAAA,EACC,CAAA,GACP,kBAAC,SAAD,EAAA,UACG,EAAW,KAAK,MACf,kBAAC,MAAD;KAEE,SAAS,UAAmB,EAAW,CAAG,IAAI,KAAA;KAC9C,WAAW,EACT,sDACA,KAAc,wDAChB;eAEC,EAAQ,KAAK,MACZ,kBAAC,MAAD;MAEE,WAAW,EACT,4CACA,EAAW,EAAI,SAAS,SACxB,EAAI,gBAAgB,wBACpB,EAAI,SACN;gBAEC,EAAI,OAAO,CAAG;KACb,GATG,EAAI,GASP,CACL;IACC,GApBG,EAAO,CAAG,CAoBb,CACL,EACI,CAAA,CACF;;EACJ,CAAA;CACI,CAAA;AAEf"}
@@ -1 +1 @@
1
- {"version":3,"file":"EmptyState.js","names":[],"sources":["../../../src/buildmyiq/components/EmptyState.tsx"],"sourcesContent":["/**\n * Empty State Component\n * @module buildmyiq/components\n *\n * Boff UI 2.0 zero-state. When an `illustration` name is provided it renders the\n * fe-libs {@link IllustratedEmptyState} (on-brand glass illustration); otherwise\n * it falls back to the icon-based layout.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\n\ninterface EmptyStateProps {\n icon?: ReactNode;\n /**\n * On-brand illustration name → `/illustrations/<name>.webp`. Standard names:\n * empty-data, empty-search, empty-files, empty-people, empty-notifications,\n * onboarding, empty-generic.\n */\n illustration?: string;\n title: string;\n description?: string;\n action?: ReactNode;\n className?: string;\n}\n\nexport const EmptyState: FC<EmptyStateProps> = ({\n icon,\n illustration,\n title,\n description,\n action,\n className,\n}) => {\n if (illustration) {\n return (\n <IllustratedEmptyState\n illustration={illustration}\n title={title}\n description={description}\n action={action}\n className={className}\n />\n );\n }\n\n return (\n <div\n className={cn('flex flex-col items-center justify-center py-12 px-4 text-center', className)}\n >\n {icon && <div className=\"mb-4 text-text-secondary opacity-50\">{icon}</div>}\n <h3 className=\"text-lg font-medium text-text-primary\">{title}</h3>\n {description && <p className=\"mt-2 text-sm text-text-secondary max-w-sm\">{description}</p>}\n {action && <div className=\"mt-6\">{action}</div>}\n </div>\n );\n};\n"],"mappings":";;;;AA2BA,IAAa,KAAmC,EAC9C,SACA,iBACA,UACA,gBACA,WACA,mBAEI,IAEA,kBAAC,GAAD;CACgB;CACP;CACM;CACL;CACG;CACX,CAAA,GAKJ,kBAAC,OAAD;CACE,WAAW,EAAG,oEAAoE,EAAU;WAD9F;EAGG,KAAQ,kBAAC,OAAD;GAAK,WAAU;aAAuC;GAAW,CAAA;EAC1E,kBAAC,MAAD;GAAI,WAAU;aAAyC;GAAW,CAAA;EACjE,KAAe,kBAAC,KAAD;GAAG,WAAU;aAA6C;GAAgB,CAAA;EACzF,KAAU,kBAAC,OAAD;GAAK,WAAU;aAAQ;GAAa,CAAA;EAC3C"}
1
+ {"version":3,"file":"EmptyState.js","names":[],"sources":["../../../src/buildmyiq/components/EmptyState.tsx"],"sourcesContent":["/**\n * Empty State Component\n * @module buildmyiq/components\n *\n * Boff UI 2.0 zero-state. When an `illustration` name is provided it renders the\n * fe-libs {@link IllustratedEmptyState} (on-brand glass illustration); otherwise\n * it falls back to the icon-based layout.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\n\ninterface EmptyStateProps {\n icon?: ReactNode;\n /**\n * On-brand illustration name → `/illustrations/<name>.webp`. Standard names:\n * empty-data, empty-search, empty-files, empty-people, empty-notifications,\n * onboarding, empty-generic.\n */\n illustration?: string;\n title: string;\n description?: string;\n action?: ReactNode;\n className?: string;\n}\n\nexport const EmptyState: FC<EmptyStateProps> = ({\n icon,\n illustration,\n title,\n description,\n action,\n className,\n}) => {\n if (illustration) {\n return (\n <IllustratedEmptyState\n illustration={illustration}\n title={title}\n description={description}\n action={action}\n className={className}\n />\n );\n }\n\n return (\n <div\n className={cn('flex flex-col items-center justify-center py-12 px-4 text-center', className)}\n >\n {icon && <div className=\"mb-4 text-text-secondary opacity-50\">{icon}</div>}\n <h3 className=\"text-lg font-medium text-text-primary\">{title}</h3>\n {description && <p className=\"mt-2 text-sm text-text-secondary max-w-sm\">{description}</p>}\n {action && <div className=\"mt-6\">{action}</div>}\n </div>\n );\n};\n"],"mappings":";;;;AA2BA,IAAa,KAAmC,EAC9C,SACA,iBACA,UACA,gBACA,WACA,mBAEI,IAEA,kBAAC,GAAD;CACgB;CACP;CACM;CACL;CACG;AACZ,CAAA,IAKH,kBAAC,OAAD;CACE,WAAW,EAAG,oEAAoE,CAAS;WAD7F;EAGG,KAAQ,kBAAC,OAAD;GAAK,WAAU;aAAuC;EAAU,CAAA;EACzE,kBAAC,MAAD;GAAI,WAAU;aAAyC;EAAU,CAAA;EAChE,KAAe,kBAAC,KAAD;GAAG,WAAU;aAA6C;EAAe,CAAA;EACxF,KAAU,kBAAC,OAAD;GAAK,WAAU;aAAQ;EAAY,CAAA;CAC3C"}
@@ -1 +1 @@
1
- {"version":3,"file":"EvidenceUpload.js","names":[],"sources":["../../../src/buildmyiq/components/EvidenceUpload.tsx"],"sourcesContent":["/**\n * EvidenceUpload — a controlled file-attach control (Integration A).\n * @module buildmyiq/components/EvidenceUpload\n *\n * Wraps `useBuiFileUpload` (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 buildmyiq mutations\n * (`buiCreateCourse.coverFileId`, `buiIssueCertificate.fileId`).\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 { useBuiFileUpload, type UploadedBuiFile } from '../hooks';\n\ninterface EvidenceUploadProps {\n id: string;\n label: string;\n value: UploadedBuiFile | null;\n onChange: (value: UploadedBuiFile | 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 } = useBuiFileUpload();\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
+ {"version":3,"file":"EvidenceUpload.js","names":[],"sources":["../../../src/buildmyiq/components/EvidenceUpload.tsx"],"sourcesContent":["/**\n * EvidenceUpload — a controlled file-attach control (Integration A).\n * @module buildmyiq/components/EvidenceUpload\n *\n * Wraps `useBuiFileUpload` (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 buildmyiq mutations\n * (`buiCreateCourse.coverFileId`, `buiIssueCertificate.fileId`).\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 { useBuiFileUpload, type UploadedBuiFile } from '../hooks';\n\ninterface EvidenceUploadProps {\n id: string;\n label: string;\n value: UploadedBuiFile | null;\n onChange: (value: UploadedBuiFile | 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 } = useBuiFileUpload();\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,IAAI,GACxC,EAAE,WAAQ,gBAAa,aAAU,UAAO,aAAU,EAAiB;CAoBzE,OACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IAAO,SAAS;cAAK;GAAa,CAAA;GAElC,kBAAC,SAAD;IACE,KAAK;IACD;IACJ,MAAK;IACG;IACR,WAAU;IACV,UAAU,KAAY;IACtB,UAAU,OA7BY,MAAyC;KACnE,IAAM,IAAO,EAAM,OAAO,QAAQ;KAElC,MAAM,OAAO,QAAQ,IAChB,GACL,IAAI;MAEF,EAAS,MADc,EAAO,CAAI,CACjB;KACnB,QAAQ,CAER;IACF;GAmBK,CAAA;GAEA,IACC,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD,EAAW,WAAU,sCAAuC,CAAA;KAC5D,kBAAC,QAAD;MACE,WAAU;MACV,OAAO,EAAM;gBAEZ,EAAM;KACH,CAAA;KACN,kBAAC,UAAD;MACE,MAAK;MACL,eA9BiB;OAEzB,AADA,EAAM,GACN,EAAS,IAAI;MACf;MA4BoB;MACV,cAAW;MACX,WAAU;gBAEV,kBAAC,GAAD,EAAG,WAAU,UAAW,CAAA;KAClB,CAAA;IACL;QACH,IACF,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD,EAAS,MAAK,KAAM,CAAA;KACpB,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OAA8C;OAAY;OAAS;MAAO;;KAC1E,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,OAAD;OACE,WAAU;OACV,OAAO,EAAE,OAAO,GAAG,EAAS,GAAG;MAChC,CAAA;KACE,CAAA;IACF;QAEL,kBAAC,GAAD;IACE,MAAK;IACL,SAAQ;IACE;IACV,eAAe,EAAS,SAAS,MAAM;IACvC,WAAU;cALZ,CAOE,kBAAC,GAAD,EAAQ,WAAU,iBAAkB,CAAA,GAAC,cAC/B;;GAGT,KAAQ,CAAC,KAAS,kBAAC,KAAD;IAAG,WAAU;cAA8B;GAAQ,CAAA;GACrE,KAAS,kBAAC,KAAD;IAAG,WAAU;cAAkC;GAAS,CAAA;EAC/D;;AAET"}
@@ -1 +1 @@
1
- {"version":3,"file":"FileLink.js","names":[],"sources":["../../../src/buildmyiq/components/FileLink.tsx"],"sourcesContent":["/**\n * FileLink — resolves a federated File id to a download link (Integration A).\n * @module buildmyiq/components/FileLink\n *\n * The buildmyiq queries select `cover { id }` / `media { 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 { useEcoFile } 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 } = useEcoFile(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
+ {"version":3,"file":"FileLink.js","names":[],"sources":["../../../src/buildmyiq/components/FileLink.tsx"],"sourcesContent":["/**\n * FileLink — resolves a federated File id to a download link (Integration A).\n * @module buildmyiq/components/FileLink\n *\n * The buildmyiq queries select `cover { id }` / `media { 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 { useEcoFile } 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 } = useEcoFile(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,CAAM;CA+BtD,OA7BK,IAED,IAEA,kBAAC,QAAD;EAAM,WAAU;YAAhB,CACE,kBAAC,GAAD,EAAS,WAAU,2BAA4B,CAAA,GAAC,sBAC5C;MAIN,KAAW,CAAC,IAEZ,kBAAC,QAAD;EAAM,WAAU;YAAhB,CACE,kBAAC,GAAD,EAAU,WAAU,cAAe,CAAA,GAAC,yBAChC;MAOL,EAAK,cASR,kBAAC,KAAD;EACE,MAAM,EAAK;EACX,QAAO;EACP,KAAI;EACJ,WAAW,KAAa;YAJ1B;GAME,kBAAC,GAAD,EAAU,WAAU,cAAe,CAAA;GAAC;GAAE,EAAK;EAC1C;MAdD,kBAAC,QAAD;EAAM,WAAW,KAAa;YAA9B;GACE,kBAAC,GAAD,EAAU,WAAU,cAAe,CAAA;GAAC;GAAE,EAAK;EACvC;MAzBU;AAuCtB"}
@@ -1 +1 @@
1
- {"version":3,"file":"FilterTabs.js","names":[],"sources":["../../../src/buildmyiq/components/FilterTabs.tsx"],"sourcesContent":["/**\n * FilterTabs — a segmented status/enum filter control.\n * @module buildmyiq/components\n */\n\nimport { cn } from '../utils/cn';\n\nexport interface FilterOption<V extends string> {\n value: V;\n label: string;\n count?: number;\n}\n\ninterface FilterTabsProps<V extends string> {\n options: FilterOption<V>[];\n value: V;\n onChange: (value: V) => void;\n className?: string;\n}\n\nexport function FilterTabs<V extends string>({\n options,\n value,\n onChange,\n className,\n}: FilterTabsProps<V>) {\n return (\n <div\n role=\"tablist\"\n className={cn(\n 'inline-flex flex-wrap items-center gap-1 rounded-xl border border-border-seam bg-bg-surface p-1',\n className\n )}\n >\n {options.map((opt) => {\n const active = opt.value === value;\n return (\n <button\n key={opt.value}\n type=\"button\"\n role=\"tab\"\n aria-selected={active}\n onClick={() => onChange(opt.value)}\n className={cn(\n 'rounded-lg px-3 py-1.5 text-sm font-medium transition-colors',\n active\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'text-text-secondary hover:bg-bg-sunken hover:text-text-primary'\n )}\n >\n {opt.label}\n {typeof opt.count === 'number' && (\n <span\n className={cn(\n 'ml-1.5 rounded-full px-1.5 py-0.5 text-xs tabular-nums',\n active ? 'bg-black/10 dark:bg-white/15' : 'bg-bg-sunken'\n )}\n >\n {opt.count}\n </span>\n )}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":";;;AAoBA,SAAgB,EAA6B,EAC3C,YACA,UACA,aACA,gBACqB;AACrB,QACE,kBAAC,OAAD;EACE,MAAK;EACL,WAAW,EACT,mGACA,EACD;YAEA,EAAQ,KAAK,MAAQ;GACpB,IAAM,IAAS,EAAI,UAAU;AAC7B,UACE,kBAAC,UAAD;IAEE,MAAK;IACL,MAAK;IACL,iBAAe;IACf,eAAe,EAAS,EAAI,MAAM;IAClC,WAAW,EACT,gEACA,IACI,kDACA,iEACL;cAXH,CAaG,EAAI,OACJ,OAAO,EAAI,SAAU,YACpB,kBAAC,QAAD;KACE,WAAW,EACT,0DACA,IAAS,iCAAiC,eAC3C;eAEA,EAAI;KACA,CAAA,CAEF;MAvBF,EAAI,MAuBF;IAEX;EACE,CAAA"}
1
+ {"version":3,"file":"FilterTabs.js","names":[],"sources":["../../../src/buildmyiq/components/FilterTabs.tsx"],"sourcesContent":["/**\n * FilterTabs — a segmented status/enum filter control.\n * @module buildmyiq/components\n */\n\nimport { cn } from '../utils/cn';\n\nexport interface FilterOption<V extends string> {\n value: V;\n label: string;\n count?: number;\n}\n\ninterface FilterTabsProps<V extends string> {\n options: FilterOption<V>[];\n value: V;\n onChange: (value: V) => void;\n className?: string;\n}\n\nexport function FilterTabs<V extends string>({\n options,\n value,\n onChange,\n className,\n}: FilterTabsProps<V>) {\n return (\n <div\n role=\"tablist\"\n className={cn(\n 'inline-flex flex-wrap items-center gap-1 rounded-xl border border-border-seam bg-bg-surface p-1',\n className\n )}\n >\n {options.map((opt) => {\n const active = opt.value === value;\n return (\n <button\n key={opt.value}\n type=\"button\"\n role=\"tab\"\n aria-selected={active}\n onClick={() => onChange(opt.value)}\n className={cn(\n 'rounded-lg px-3 py-1.5 text-sm font-medium transition-colors',\n active\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'text-text-secondary hover:bg-bg-sunken hover:text-text-primary'\n )}\n >\n {opt.label}\n {typeof opt.count === 'number' && (\n <span\n className={cn(\n 'ml-1.5 rounded-full px-1.5 py-0.5 text-xs tabular-nums',\n active ? 'bg-black/10 dark:bg-white/15' : 'bg-bg-sunken'\n )}\n >\n {opt.count}\n </span>\n )}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":";;;AAoBA,SAAgB,EAA6B,EAC3C,YACA,UACA,aACA,gBACqB;CACrB,OACE,kBAAC,OAAD;EACE,MAAK;EACL,WAAW,EACT,mGACA,CACF;YAEC,EAAQ,KAAK,MAAQ;GACpB,IAAM,IAAS,EAAI,UAAU;GAC7B,OACE,kBAAC,UAAD;IAEE,MAAK;IACL,MAAK;IACL,iBAAe;IACf,eAAe,EAAS,EAAI,KAAK;IACjC,WAAW,EACT,gEACA,IACI,kDACA,gEACN;cAXF,CAaG,EAAI,OACJ,OAAO,EAAI,SAAU,YACpB,kBAAC,QAAD;KACE,WAAW,EACT,0DACA,IAAS,iCAAiC,cAC5C;eAEC,EAAI;IACD,CAAA,CAEF;MAvBD,EAAI,KAuBH;EAEZ,CAAC;CACE,CAAA;AAET"}
@@ -1 +1 @@
1
- {"version":3,"file":"MetricsCard.js","names":[],"sources":["../../../src/buildmyiq/components/MetricsCard.tsx"],"sourcesContent":["/**\n * Metrics Card Component\n * @module buildmyiq/components\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { TrendingUp, TrendingDown, Minus } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\nimport type { TrendDirection } from '../types';\n\ntype Accent = 'success' | 'warning' | 'error';\n\ninterface MetricsCardProps {\n icon: ReactNode;\n value: string | number;\n label: string;\n trend?: TrendDirection;\n trendValue?: string;\n className?: string;\n /** Render as the page's hero metric (glass treatment + accent glow). */\n featured?: boolean;\n /** Severity accent — tints the icon and adds a soft top-edge glow. */\n accent?: Accent;\n /** Optional pill shown in the top-right (e.g. \"Critical\"). */\n pill?: ReactNode;\n /** Optional slot under the value (e.g. a MiniBar or sub-label). */\n footer?: ReactNode;\n}\n\nconst trendConfig: Record<TrendDirection, { icon: typeof TrendingUp; color: string }> = {\n up: { icon: TrendingUp, color: 'text-status-success-text' },\n down: { icon: TrendingDown, color: 'text-status-error-text' },\n neutral: { icon: Minus, color: 'text-text-secondary' },\n};\n\nconst accentIcon: Record<Accent, string> = {\n success: 'text-status-success-text',\n warning: 'text-status-warning-text',\n error: 'text-status-error-text',\n};\n\nconst accentGlow: Record<Accent, string> = {\n success: 'shadow-[inset_0_1px_0_0_var(--color-status-success-text)]',\n warning: 'shadow-[inset_0_1px_0_0_var(--color-status-warning-text)]',\n error: 'shadow-[inset_0_1px_0_0_var(--color-status-error-text)]',\n};\n\nexport const MetricsCard: FC<MetricsCardProps> = ({\n icon,\n value,\n label,\n trend,\n trendValue,\n className,\n featured = false,\n accent,\n pill,\n footer,\n}) => {\n const trendInfo = trend ? trendConfig[trend] : null;\n const TrendIcon = trendInfo?.icon;\n\n return (\n <GlassCard\n treatment={featured ? 'glass' : 'metric'}\n glow={featured}\n className={cn('p-6', accent && accentGlow[accent], className)}\n >\n <div className=\"flex items-center justify-between\">\n <div className={cn('text-text-secondary', accent && accentIcon[accent])}>{icon}</div>\n {pill ??\n (trend && trendInfo && TrendIcon ? (\n <div className={cn('flex items-center gap-1 text-xs font-medium', trendInfo.color)}>\n <TrendIcon className=\"h-3.5 w-3.5\" />\n {trendValue && <span>{trendValue}</span>}\n </div>\n ) : null)}\n </div>\n <div className=\"mt-3\">\n <p className=\"text-2xl font-bold text-text-primary tabular-nums\">{value}</p>\n <p className=\"mt-1 text-sm text-text-secondary\">{label}</p>\n {footer && <div className=\"mt-3\">{footer}</div>}\n </div>\n </GlassCard>\n );\n};\n"],"mappings":";;;;;AA8BA,IAAM,IAAkF;CACtF,IAAI;EAAE,MAAM;EAAY,OAAO;EAA4B;CAC3D,MAAM;EAAE,MAAM;EAAc,OAAO;EAA0B;CAC7D,SAAS;EAAE,MAAM;EAAO,OAAO;EAAuB;CACvD,EAEK,IAAqC;CACzC,SAAS;CACT,SAAS;CACT,OAAO;CACR,EAEK,IAAqC;CACzC,SAAS;CACT,SAAS;CACT,OAAO;CACR,EAEY,KAAqC,EAChD,SACA,UACA,UACA,UACA,eACA,cACA,cAAW,IACX,WACA,SACA,gBACI;CACJ,IAAM,IAAY,IAAQ,EAAY,KAAS,MACzC,IAAY,GAAW;AAE7B,QACE,kBAAC,GAAD;EACE,WAAW,IAAW,UAAU;EAChC,MAAM;EACN,WAAW,EAAG,OAAO,KAAU,EAAW,IAAS,EAAU;YAH/D,CAKE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAW,EAAG,uBAAuB,KAAU,EAAW,GAAQ;cAAG;IAAW,CAAA,EACpF,MACE,KAAS,KAAa,IACrB,kBAAC,OAAD;IAAK,WAAW,EAAG,+CAA+C,EAAU,MAAM;cAAlF,CACE,kBAAC,GAAD,EAAW,WAAU,eAAgB,CAAA,EACpC,KAAc,kBAAC,QAAD,EAAA,UAAO,GAAkB,CAAA,CACpC;QACJ,MACF;MACN,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,KAAD;KAAG,WAAU;eAAqD;KAAU,CAAA;IAC5E,kBAAC,KAAD;KAAG,WAAU;eAAoC;KAAU,CAAA;IAC1D,KAAU,kBAAC,OAAD;KAAK,WAAU;eAAQ;KAAa,CAAA;IAC3C;KACI"}
1
+ {"version":3,"file":"MetricsCard.js","names":[],"sources":["../../../src/buildmyiq/components/MetricsCard.tsx"],"sourcesContent":["/**\n * Metrics Card Component\n * @module buildmyiq/components\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { TrendingUp, TrendingDown, Minus } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\nimport type { TrendDirection } from '../types';\n\ntype Accent = 'success' | 'warning' | 'error';\n\ninterface MetricsCardProps {\n icon: ReactNode;\n value: string | number;\n label: string;\n trend?: TrendDirection;\n trendValue?: string;\n className?: string;\n /** Render as the page's hero metric (glass treatment + accent glow). */\n featured?: boolean;\n /** Severity accent — tints the icon and adds a soft top-edge glow. */\n accent?: Accent;\n /** Optional pill shown in the top-right (e.g. \"Critical\"). */\n pill?: ReactNode;\n /** Optional slot under the value (e.g. a MiniBar or sub-label). */\n footer?: ReactNode;\n}\n\nconst trendConfig: Record<TrendDirection, { icon: typeof TrendingUp; color: string }> = {\n up: { icon: TrendingUp, color: 'text-status-success-text' },\n down: { icon: TrendingDown, color: 'text-status-error-text' },\n neutral: { icon: Minus, color: 'text-text-secondary' },\n};\n\nconst accentIcon: Record<Accent, string> = {\n success: 'text-status-success-text',\n warning: 'text-status-warning-text',\n error: 'text-status-error-text',\n};\n\nconst accentGlow: Record<Accent, string> = {\n success: 'shadow-[inset_0_1px_0_0_var(--color-status-success-text)]',\n warning: 'shadow-[inset_0_1px_0_0_var(--color-status-warning-text)]',\n error: 'shadow-[inset_0_1px_0_0_var(--color-status-error-text)]',\n};\n\nexport const MetricsCard: FC<MetricsCardProps> = ({\n icon,\n value,\n label,\n trend,\n trendValue,\n className,\n featured = false,\n accent,\n pill,\n footer,\n}) => {\n const trendInfo = trend ? trendConfig[trend] : null;\n const TrendIcon = trendInfo?.icon;\n\n return (\n <GlassCard\n treatment={featured ? 'glass' : 'metric'}\n glow={featured}\n className={cn('p-6', accent && accentGlow[accent], className)}\n >\n <div className=\"flex items-center justify-between\">\n <div className={cn('text-text-secondary', accent && accentIcon[accent])}>{icon}</div>\n {pill ??\n (trend && trendInfo && TrendIcon ? (\n <div className={cn('flex items-center gap-1 text-xs font-medium', trendInfo.color)}>\n <TrendIcon className=\"h-3.5 w-3.5\" />\n {trendValue && <span>{trendValue}</span>}\n </div>\n ) : null)}\n </div>\n <div className=\"mt-3\">\n <p className=\"text-2xl font-bold text-text-primary tabular-nums\">{value}</p>\n <p className=\"mt-1 text-sm text-text-secondary\">{label}</p>\n {footer && <div className=\"mt-3\">{footer}</div>}\n </div>\n </GlassCard>\n );\n};\n"],"mappings":";;;;;AA8BA,IAAM,IAAkF;CACtF,IAAI;EAAE,MAAM;EAAY,OAAO;CAA2B;CAC1D,MAAM;EAAE,MAAM;EAAc,OAAO;CAAyB;CAC5D,SAAS;EAAE,MAAM;EAAO,OAAO;CAAsB;AACvD,GAEM,IAAqC;CACzC,SAAS;CACT,SAAS;CACT,OAAO;AACT,GAEM,IAAqC;CACzC,SAAS;CACT,SAAS;CACT,OAAO;AACT,GAEa,KAAqC,EAChD,SACA,UACA,UACA,UACA,eACA,cACA,cAAW,IACX,WACA,SACA,gBACI;CACJ,IAAM,IAAY,IAAQ,EAAY,KAAS,MACzC,IAAY,GAAW;CAE7B,OACE,kBAAC,GAAD;EACE,WAAW,IAAW,UAAU;EAChC,MAAM;EACN,WAAW,EAAG,OAAO,KAAU,EAAW,IAAS,CAAS;YAH9D,CAKE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAW,EAAG,uBAAuB,KAAU,EAAW,EAAO;cAAI;GAAU,CAAA,GACnF,MACE,KAAS,KAAa,IACrB,kBAAC,OAAD;IAAK,WAAW,EAAG,+CAA+C,EAAU,KAAK;cAAjF,CACE,kBAAC,GAAD,EAAW,WAAU,cAAe,CAAA,GACnC,KAAc,kBAAC,QAAD,EAAA,UAAO,EAAiB,CAAA,CACpC;QACH,KACH;MACL,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,KAAD;KAAG,WAAU;eAAqD;IAAS,CAAA;IAC3E,kBAAC,KAAD;KAAG,WAAU;eAAoC;IAAS,CAAA;IACzD,KAAU,kBAAC,OAAD;KAAK,WAAU;eAAQ;IAAY,CAAA;GAC3C;IACI;;AAEf"}
@@ -1 +1 @@
1
- {"version":3,"file":"MiniBar.js","names":[],"sources":["../../../src/buildmyiq/components/MiniBar.tsx"],"sourcesContent":["/**\n * MiniBar — a thin inline progress bar (target progress, completion, breach %).\n * @module buildmyiq/components\n */\n\nimport { cn } from '../utils/cn';\n\ninterface MiniBarProps {\n /** 0–100. */\n value: number;\n className?: string;\n /** Bar fill token class (defaults to accent). */\n barClassName?: string;\n /** Height in px. */\n height?: number;\n}\n\nexport function MiniBar({\n value,\n className,\n barClassName = 'bg-[var(--color-accent-primary)]',\n height = 6,\n}: MiniBarProps) {\n const clamped = Math.max(0, Math.min(100, value));\n return (\n <div\n className={cn('w-full overflow-hidden rounded-full bg-bg-sunken', className)}\n style={{ height }}\n role=\"progressbar\"\n aria-valuenow={Math.round(clamped)}\n aria-valuemin={0}\n aria-valuemax={100}\n >\n <div\n className={cn('h-full rounded-full transition-[width] duration-700 ease-out', barClassName)}\n style={{ width: `${clamped}%` }}\n />\n </div>\n );\n}\n"],"mappings":";;;AAiBA,SAAgB,EAAQ,EACtB,UACA,cACA,kBAAe,oCACf,YAAS,KACM;CACf,IAAM,IAAU,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,EAAM,CAAC;AACjD,QACE,kBAAC,OAAD;EACE,WAAW,EAAG,oDAAoD,EAAU;EAC5E,OAAO,EAAE,WAAQ;EACjB,MAAK;EACL,iBAAe,KAAK,MAAM,EAAQ;EAClC,iBAAe;EACf,iBAAe;YAEf,kBAAC,OAAD;GACE,WAAW,EAAG,gEAAgE,EAAa;GAC3F,OAAO,EAAE,OAAO,GAAG,EAAQ,IAAI;GAC/B,CAAA;EACE,CAAA"}
1
+ {"version":3,"file":"MiniBar.js","names":[],"sources":["../../../src/buildmyiq/components/MiniBar.tsx"],"sourcesContent":["/**\n * MiniBar — a thin inline progress bar (target progress, completion, breach %).\n * @module buildmyiq/components\n */\n\nimport { cn } from '../utils/cn';\n\ninterface MiniBarProps {\n /** 0–100. */\n value: number;\n className?: string;\n /** Bar fill token class (defaults to accent). */\n barClassName?: string;\n /** Height in px. */\n height?: number;\n}\n\nexport function MiniBar({\n value,\n className,\n barClassName = 'bg-[var(--color-accent-primary)]',\n height = 6,\n}: MiniBarProps) {\n const clamped = Math.max(0, Math.min(100, value));\n return (\n <div\n className={cn('w-full overflow-hidden rounded-full bg-bg-sunken', className)}\n style={{ height }}\n role=\"progressbar\"\n aria-valuenow={Math.round(clamped)}\n aria-valuemin={0}\n aria-valuemax={100}\n >\n <div\n className={cn('h-full rounded-full transition-[width] duration-700 ease-out', barClassName)}\n style={{ width: `${clamped}%` }}\n />\n </div>\n );\n}\n"],"mappings":";;;AAiBA,SAAgB,EAAQ,EACtB,UACA,cACA,kBAAe,oCACf,YAAS,KACM;CACf,IAAM,IAAU,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,CAAK,CAAC;CAChD,OACE,kBAAC,OAAD;EACE,WAAW,EAAG,oDAAoD,CAAS;EAC3E,OAAO,EAAE,UAAO;EAChB,MAAK;EACL,iBAAe,KAAK,MAAM,CAAO;EACjC,iBAAe;EACf,iBAAe;YAEf,kBAAC,OAAD;GACE,WAAW,EAAG,gEAAgE,CAAY;GAC1F,OAAO,EAAE,OAAO,GAAG,EAAQ,GAAG;EAC/B,CAAA;CACE,CAAA;AAET"}
@@ -1 +1 @@
1
- {"version":3,"file":"PageLayout.js","names":[],"sources":["../../../src/buildmyiq/components/PageLayout.tsx"],"sourcesContent":["/**\n * Page Layout Component\n * @module buildmyiq/components\n *\n * Boff UI 2.0 page wrapper. Supports a living-glass {@link AuroraBackground}\n * hero backdrop and a gradient hero title for the MFE's primary page.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { AuroraBackground, GradientText } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\n\ninterface PageLayoutProps {\n title: ReactNode;\n description?: string;\n actions?: ReactNode;\n children: ReactNode;\n className?: string;\n /** Render the title as a gradient hero heading (reserve for the ONE primary page). */\n titleGradient?: boolean;\n /** Drop a subtle living-glass aurora behind the header (primary page only). */\n aurora?: boolean;\n}\n\nexport const PageLayout: FC<PageLayoutProps> = ({\n title,\n description,\n actions,\n children,\n className,\n titleGradient = false,\n aurora = false,\n}) => {\n return (\n <div className={cn('flex flex-col h-full', className)}>\n <div\n className={cn(\n 'relative flex flex-col gap-3 px-6 py-4 border-b border-border-seam sm:flex-row sm:items-center sm:justify-between sm:gap-4',\n aurora ? 'overflow-hidden bg-gloss-card' : 'bg-bg-surface'\n )}\n >\n {aurora && <AuroraBackground intensity=\"subtle\" />}\n <div className=\"min-w-0\">\n {titleGradient && typeof title === 'string' ? (\n <GradientText as=\"h1\" className=\"text-2xl font-semibold tracking-tight\">\n {title}\n </GradientText>\n ) : (\n <h1 className=\"text-2xl font-semibold text-text-primary\">{title}</h1>\n )}\n {description && <p className=\"mt-1 text-sm text-text-secondary\">{description}</p>}\n </div>\n {actions && (\n <div className=\"flex flex-wrap items-center gap-3 shrink-0 max-sm:w-full\">{actions}</div>\n )}\n </div>\n\n <div className=\"flex-1 overflow-auto p-6\">{children}</div>\n </div>\n );\n};\n"],"mappings":";;;;AAwBA,IAAa,KAAmC,EAC9C,UACA,gBACA,YACA,aACA,cACA,mBAAgB,IAChB,YAAS,SAGP,kBAAC,OAAD;CAAK,WAAW,EAAG,wBAAwB,EAAU;WAArD,CACE,kBAAC,OAAD;EACE,WAAW,EACT,8HACA,IAAS,kCAAkC,gBAC5C;YAJH;GAMG,KAAU,kBAAC,GAAD,EAAkB,WAAU,UAAW,CAAA;GAClD,kBAAC,OAAD;IAAK,WAAU;cAAf,CACG,KAAiB,OAAO,KAAU,WACjC,kBAAC,GAAD;KAAc,IAAG;KAAK,WAAU;eAC7B;KACY,CAAA,GAEf,kBAAC,MAAD;KAAI,WAAU;eAA4C;KAAW,CAAA,EAEtE,KAAe,kBAAC,KAAD;KAAG,WAAU;eAAoC;KAAgB,CAAA,CAC7E;;GACL,KACC,kBAAC,OAAD;IAAK,WAAU;cAA4D;IAAc,CAAA;GAEvF;KAEN,kBAAC,OAAD;EAAK,WAAU;EAA4B;EAAe,CAAA,CACtD"}
1
+ {"version":3,"file":"PageLayout.js","names":[],"sources":["../../../src/buildmyiq/components/PageLayout.tsx"],"sourcesContent":["/**\n * Page Layout Component\n * @module buildmyiq/components\n *\n * Boff UI 2.0 page wrapper. Supports a living-glass {@link AuroraBackground}\n * hero backdrop and a gradient hero title for the MFE's primary page.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { AuroraBackground, GradientText } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\n\ninterface PageLayoutProps {\n title: ReactNode;\n description?: string;\n actions?: ReactNode;\n children: ReactNode;\n className?: string;\n /** Render the title as a gradient hero heading (reserve for the ONE primary page). */\n titleGradient?: boolean;\n /** Drop a subtle living-glass aurora behind the header (primary page only). */\n aurora?: boolean;\n}\n\nexport const PageLayout: FC<PageLayoutProps> = ({\n title,\n description,\n actions,\n children,\n className,\n titleGradient = false,\n aurora = false,\n}) => {\n return (\n <div className={cn('flex flex-col h-full', className)}>\n <div\n className={cn(\n 'relative flex flex-col gap-3 px-6 py-4 border-b border-border-seam sm:flex-row sm:items-center sm:justify-between sm:gap-4',\n aurora ? 'overflow-hidden bg-gloss-card' : 'bg-bg-surface'\n )}\n >\n {aurora && <AuroraBackground intensity=\"subtle\" />}\n <div className=\"min-w-0\">\n {titleGradient && typeof title === 'string' ? (\n <GradientText as=\"h1\" className=\"text-2xl font-semibold tracking-tight\">\n {title}\n </GradientText>\n ) : (\n <h1 className=\"text-2xl font-semibold text-text-primary\">{title}</h1>\n )}\n {description && <p className=\"mt-1 text-sm text-text-secondary\">{description}</p>}\n </div>\n {actions && (\n <div className=\"flex flex-wrap items-center gap-3 shrink-0 max-sm:w-full\">{actions}</div>\n )}\n </div>\n\n <div className=\"flex-1 overflow-auto p-6\">{children}</div>\n </div>\n );\n};\n"],"mappings":";;;;AAwBA,IAAa,KAAmC,EAC9C,UACA,gBACA,YACA,aACA,cACA,mBAAgB,IAChB,YAAS,SAGP,kBAAC,OAAD;CAAK,WAAW,EAAG,wBAAwB,CAAS;WAApD,CACE,kBAAC,OAAD;EACE,WAAW,EACT,8HACA,IAAS,kCAAkC,eAC7C;YAJF;GAMG,KAAU,kBAAC,GAAD,EAAkB,WAAU,SAAU,CAAA;GACjD,kBAAC,OAAD;IAAK,WAAU;cAAf,CACG,KAAiB,OAAO,KAAU,WACjC,kBAAC,GAAD;KAAc,IAAG;KAAK,WAAU;eAC7B;IACW,CAAA,IAEd,kBAAC,MAAD;KAAI,WAAU;eAA4C;IAAU,CAAA,GAErE,KAAe,kBAAC,KAAD;KAAG,WAAU;eAAoC;IAAe,CAAA,CAC7E;;GACJ,KACC,kBAAC,OAAD;IAAK,WAAU;cAA4D;GAAa,CAAA;EAEvF;KAEL,kBAAC,OAAD;EAAK,WAAU;EAA4B;CAAc,CAAA,CACtD"}
@@ -1 +1 @@
1
- {"version":3,"file":"Pagination.js","names":[],"sources":["../../../src/buildmyiq/components/Pagination.tsx"],"sourcesContent":["/**\n * Pagination — cursor-aware Prev/Next control for Bui* connection lists.\n * @module buildmyiq/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"}
1
+ {"version":3,"file":"Pagination.js","names":[],"sources":["../../../src/buildmyiq/components/Pagination.tsx"],"sourcesContent":["/**\n * Pagination — cursor-aware Prev/Next control for Bui* connection lists.\n * @module buildmyiq/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;CAElB,IAAI,MAAS,KAAK,CAAC,GAAS,OAAO;CAEnC,IAAM,IAAO,MAAc,IAAI,IAAI,IAAO,IAAW,GAC/C,IAAK,IAAO,IAAW;CAE7B,OACE,kBAAC,OAAD;EACE,WAAW,EAAG,qEAAqE,CAAS;YAD9F,CAGE,kBAAC,KAAD;GAAG,WAAU;GAA2C,aAAU;aAAlE;IAA2E;IAChE;IAAK;IAAE;GACf;MACH,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,GAAD;KACE,SAAQ;KACR,MAAK;KACL,eAAe,EAAa,IAAO,CAAC;KACpC,UAAU,MAAS;KACnB,cAAW;eALb,CAOE,kBAAC,GAAD,EAAa,WAAU,eAAgB,CAAA,GAAC,WAClC;;IACR,kBAAC,QAAD;KAAM,WAAU;eAAhB,CAAsF,SAC9E,IAAO,CACT;;IACN,kBAAC,GAAD;KACE,SAAQ;KACR,MAAK;KACL,eAAe,EAAa,IAAO,CAAC;KACpC,UAAU,CAAC;KACX,cAAW;eALb,CAMC,SACM,kBAAC,GAAD,EAAc,WAAU,eAAgB,CAAA,CACvC;;GACL;IACF;;AAET"}
@@ -1 +1 @@
1
- {"version":3,"file":"StateViews.js","names":[],"sources":["../../../src/buildmyiq/components/StateViews.tsx"],"sourcesContent":["/**\n * Shared loading / error / section state views\n * @module buildmyiq/components\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { AlertCircle } from 'lucide-react';\nimport { Spinner } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\n\ninterface LoadingViewProps {\n label?: string;\n className?: string;\n}\n\nexport const LoadingView: FC<LoadingViewProps> = ({ label = 'Loading…', className }) => (\n <div className={cn('flex min-h-[40vh] flex-col items-center justify-center gap-3', className)}>\n <Spinner size=\"lg\" />\n <p className=\"text-sm text-text-secondary\">{label}</p>\n </div>\n);\n\ninterface ErrorViewProps {\n title?: string;\n message?: string;\n onRetry?: () => void;\n className?: string;\n}\n\nexport const ErrorView: FC<ErrorViewProps> = ({\n title = 'Something went wrong',\n message,\n onRetry,\n className,\n}) => (\n <div\n className={cn(\n 'flex min-h-[40vh] flex-col items-center justify-center gap-4 text-center',\n className\n )}\n >\n <AlertCircle className=\"h-10 w-10 text-status-error-text\" />\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">{title}</h2>\n {message && <p className=\"mt-1 max-w-md text-sm text-text-secondary\">{message}</p>}\n </div>\n {onRetry && (\n <button\n type=\"button\"\n onClick={onRetry}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90\"\n >\n Retry\n </button>\n )}\n </div>\n);\n\ninterface SectionProps {\n title: string;\n description?: string;\n action?: ReactNode;\n children: ReactNode;\n className?: string;\n}\n\nexport const Section: FC<SectionProps> = ({ title, description, action, children, className }) => (\n <section className={cn('mt-8 first:mt-0', className)}>\n <div className=\"mb-3 flex items-end justify-between gap-3\">\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">{title}</h2>\n {description && <p className=\"mt-0.5 text-sm text-text-secondary\">{description}</p>}\n </div>\n {action && <div className=\"shrink-0\">{action}</div>}\n </div>\n {children}\n </section>\n);\n"],"mappings":";;;;;AAeA,IAAa,KAAqC,EAAE,WAAQ,YAAY,mBACtE,kBAAC,OAAD;CAAK,WAAW,EAAG,gEAAgE,EAAU;WAA7F,CACE,kBAAC,GAAD,EAAS,MAAK,MAAO,CAAA,EACrB,kBAAC,KAAD;EAAG,WAAU;YAA+B;EAAU,CAAA,CAClD;IAUK,KAAiC,EAC5C,WAAQ,wBACR,YACA,YACA,mBAEA,kBAAC,OAAD;CACE,WAAW,EACT,4EACA,EACD;WAJH;EAME,kBAAC,GAAD,EAAa,WAAU,oCAAqC,CAAA;EAC5D,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;GAAI,WAAU;aAA2C;GAAW,CAAA,EACnE,KAAW,kBAAC,KAAD;GAAG,WAAU;aAA6C;GAAY,CAAA,CAC9E,EAAA,CAAA;EACL,KACC,kBAAC,UAAD;GACE,MAAK;GACL,SAAS;GACT,WAAU;aACX;GAEQ,CAAA;EAEP;IAWK,KAA6B,EAAE,UAAO,gBAAa,WAAQ,aAAU,mBAChF,kBAAC,WAAD;CAAS,WAAW,EAAG,mBAAmB,EAAU;WAApD,CACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;GAAI,WAAU;aAA6C;GAAW,CAAA,EACrE,KAAe,kBAAC,KAAD;GAAG,WAAU;aAAsC;GAAgB,CAAA,CAC/E,EAAA,CAAA,EACL,KAAU,kBAAC,OAAD;GAAK,WAAU;aAAY;GAAa,CAAA,CAC/C;KACL,EACO"}
1
+ {"version":3,"file":"StateViews.js","names":[],"sources":["../../../src/buildmyiq/components/StateViews.tsx"],"sourcesContent":["/**\n * Shared loading / error / section state views\n * @module buildmyiq/components\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { AlertCircle } from 'lucide-react';\nimport { Spinner } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\n\ninterface LoadingViewProps {\n label?: string;\n className?: string;\n}\n\nexport const LoadingView: FC<LoadingViewProps> = ({ label = 'Loading…', className }) => (\n <div className={cn('flex min-h-[40vh] flex-col items-center justify-center gap-3', className)}>\n <Spinner size=\"lg\" />\n <p className=\"text-sm text-text-secondary\">{label}</p>\n </div>\n);\n\ninterface ErrorViewProps {\n title?: string;\n message?: string;\n onRetry?: () => void;\n className?: string;\n}\n\nexport const ErrorView: FC<ErrorViewProps> = ({\n title = 'Something went wrong',\n message,\n onRetry,\n className,\n}) => (\n <div\n className={cn(\n 'flex min-h-[40vh] flex-col items-center justify-center gap-4 text-center',\n className\n )}\n >\n <AlertCircle className=\"h-10 w-10 text-status-error-text\" />\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">{title}</h2>\n {message && <p className=\"mt-1 max-w-md text-sm text-text-secondary\">{message}</p>}\n </div>\n {onRetry && (\n <button\n type=\"button\"\n onClick={onRetry}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90\"\n >\n Retry\n </button>\n )}\n </div>\n);\n\ninterface SectionProps {\n title: string;\n description?: string;\n action?: ReactNode;\n children: ReactNode;\n className?: string;\n}\n\nexport const Section: FC<SectionProps> = ({ title, description, action, children, className }) => (\n <section className={cn('mt-8 first:mt-0', className)}>\n <div className=\"mb-3 flex items-end justify-between gap-3\">\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">{title}</h2>\n {description && <p className=\"mt-0.5 text-sm text-text-secondary\">{description}</p>}\n </div>\n {action && <div className=\"shrink-0\">{action}</div>}\n </div>\n {children}\n </section>\n);\n"],"mappings":";;;;;AAeA,IAAa,KAAqC,EAAE,WAAQ,YAAY,mBACtE,kBAAC,OAAD;CAAK,WAAW,EAAG,gEAAgE,CAAS;WAA5F,CACE,kBAAC,GAAD,EAAS,MAAK,KAAM,CAAA,GACpB,kBAAC,KAAD;EAAG,WAAU;YAA+B;CAAS,CAAA,CAClD;IAUM,KAAiC,EAC5C,WAAQ,wBACR,YACA,YACA,mBAEA,kBAAC,OAAD;CACE,WAAW,EACT,4EACA,CACF;WAJF;EAME,kBAAC,GAAD,EAAa,WAAU,mCAAoC,CAAA;EAC3D,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;GAAI,WAAU;aAA2C;EAAU,CAAA,GAClE,KAAW,kBAAC,KAAD;GAAG,WAAU;aAA6C;EAAW,CAAA,CAC9E,EAAA,CAAA;EACJ,KACC,kBAAC,UAAD;GACE,MAAK;GACL,SAAS;GACT,WAAU;aACX;EAEO,CAAA;CAEP;IAWM,KAA6B,EAAE,UAAO,gBAAa,WAAQ,aAAU,mBAChF,kBAAC,WAAD;CAAS,WAAW,EAAG,mBAAmB,CAAS;WAAnD,CACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;GAAI,WAAU;aAA6C;EAAU,CAAA,GACpE,KAAe,kBAAC,KAAD;GAAG,WAAU;aAAsC;EAAe,CAAA,CAC/E,EAAA,CAAA,GACJ,KAAU,kBAAC,OAAD;GAAK,WAAU;aAAY;EAAY,CAAA,CAC/C;KACJ,CACM"}
@@ -1 +1 @@
1
- {"version":3,"file":"StatusBadge.js","names":[],"sources":["../../../src/buildmyiq/components/StatusBadge.tsx"],"sourcesContent":["/**\n * Status Badge Component\n * @module buildmyiq/components\n *\n * A colored badge for Buildmyiq domain statuses (course status, enrollment\n * status, assessment status, submission status, cohort status, certificate\n * status, lesson-progress status) plus course levels and skill categories, with\n * semantic color mapping. Accepts the raw server enum value.\n */\n\nimport type { FC } from 'react';\nimport { cn } from '../utils/cn';\nimport { formatEnumLabel } from '../utils/formatters';\n\ninterface StatusBadgeProps {\n status: string;\n className?: string;\n}\n\ntype BadgeVariant = 'success' | 'warning' | 'destructive' | 'secondary' | 'info' | 'outline';\n\nconst variantStyles: Record<BadgeVariant, string> = {\n success: 'bg-status-success-bg-subtle text-status-success-text',\n warning: 'bg-status-warning-bg-subtle text-status-warning-text',\n destructive: 'bg-status-error-bg-subtle text-status-error-text',\n secondary: 'bg-bg-sunken text-text-secondary',\n info: 'bg-[var(--color-accent-soft)] text-text-primary',\n outline: 'border border-border-seam text-text-primary',\n};\n\n// Statuses that represent a \"live\" / in-progress condition get a subtle,\n// opacity-only glow pulse (reduced-motion safe via motion-safe:).\nconst liveStatuses = new Set(['IN_PROGRESS', 'ACTIVE', 'GRADING']);\n\nconst statusVariantMap: Record<string, BadgeVariant> = {\n // Authoring lifecycle (course + assessment)\n DRAFT: 'secondary',\n IN_REVIEW: 'warning',\n PUBLISHED: 'success',\n ARCHIVED: 'outline',\n\n // Enrollment status\n ENROLLED: 'info',\n IN_PROGRESS: 'info',\n COMPLETED: 'success',\n DROPPED: 'destructive',\n EXPIRED: 'outline',\n\n // Lesson-progress status\n NOT_STARTED: 'secondary',\n\n // Submission status\n SUBMITTED: 'info',\n GRADING: 'warning',\n GRADED: 'success',\n RETURNED: 'warning',\n\n // Cohort status\n SCHEDULED: 'secondary',\n ENROLLING: 'info',\n ACTIVE: 'success',\n CANCELLED: 'destructive',\n\n // Certificate status\n ISSUED: 'success',\n REVOKED: 'destructive',\n\n // Course level\n BEGINNER: 'success',\n INTERMEDIATE: 'info',\n ADVANCED: 'warning',\n EXPERT: 'destructive',\n\n // Skill category (neutral badges)\n TECHNICAL: 'outline',\n BUSINESS: 'outline',\n CREATIVE: 'outline',\n COGNITIVE: 'outline',\n SOCIAL: 'outline',\n LANGUAGE: 'outline',\n};\n\nexport const StatusBadge: FC<StatusBadgeProps> = ({ status, className }) => {\n const key = status.toUpperCase();\n const variant = statusVariantMap[key] ?? 'secondary';\n const isLive = liveStatuses.has(key);\n\n return (\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',\n variantStyles[variant],\n className\n )}\n >\n {isLive && (\n <span\n aria-hidden=\"true\"\n className=\"size-1.5 rounded-full bg-current motion-safe:animate-vc-glow-pulse\"\n />\n )}\n {formatEnumLabel(status)}\n </span>\n );\n};\n"],"mappings":";;;;AAqBA,IAAM,IAA8C;CAClD,SAAS;CACT,SAAS;CACT,aAAa;CACb,WAAW;CACX,MAAM;CACN,SAAS;CACV,EAIK,IAAe,IAAI,IAAI;CAAC;CAAe;CAAU;CAAU,CAAC,EAE5D,IAAiD;CAErD,OAAO;CACP,WAAW;CACX,WAAW;CACX,UAAU;CAGV,UAAU;CACV,aAAa;CACb,WAAW;CACX,SAAS;CACT,SAAS;CAGT,aAAa;CAGb,WAAW;CACX,SAAS;CACT,QAAQ;CACR,UAAU;CAGV,WAAW;CACX,WAAW;CACX,QAAQ;CACR,WAAW;CAGX,QAAQ;CACR,SAAS;CAGT,UAAU;CACV,cAAc;CACd,UAAU;CACV,QAAQ;CAGR,WAAW;CACX,UAAU;CACV,UAAU;CACV,WAAW;CACX,QAAQ;CACR,UAAU;CACX,EAEY,KAAqC,EAAE,WAAQ,mBAAgB;CAC1E,IAAM,IAAM,EAAO,aAAa,EAC1B,IAAU,EAAiB,MAAQ,aACnC,IAAS,EAAa,IAAI,EAAI;AAEpC,QACE,kBAAC,QAAD;EACE,WAAW,EACT,mFACA,EAAc,IACd,EACD;YALH,CAOG,KACC,kBAAC,QAAD;GACE,eAAY;GACZ,WAAU;GACV,CAAA,EAEH,EAAgB,EAAO,CACnB"}
1
+ {"version":3,"file":"StatusBadge.js","names":[],"sources":["../../../src/buildmyiq/components/StatusBadge.tsx"],"sourcesContent":["/**\n * Status Badge Component\n * @module buildmyiq/components\n *\n * A colored badge for Buildmyiq domain statuses (course status, enrollment\n * status, assessment status, submission status, cohort status, certificate\n * status, lesson-progress status) plus course levels and skill categories, with\n * semantic color mapping. Accepts the raw server enum value.\n */\n\nimport type { FC } from 'react';\nimport { cn } from '../utils/cn';\nimport { formatEnumLabel } from '../utils/formatters';\n\ninterface StatusBadgeProps {\n status: string;\n className?: string;\n}\n\ntype BadgeVariant = 'success' | 'warning' | 'destructive' | 'secondary' | 'info' | 'outline';\n\nconst variantStyles: Record<BadgeVariant, string> = {\n success: 'bg-status-success-bg-subtle text-status-success-text',\n warning: 'bg-status-warning-bg-subtle text-status-warning-text',\n destructive: 'bg-status-error-bg-subtle text-status-error-text',\n secondary: 'bg-bg-sunken text-text-secondary',\n info: 'bg-[var(--color-accent-soft)] text-text-primary',\n outline: 'border border-border-seam text-text-primary',\n};\n\n// Statuses that represent a \"live\" / in-progress condition get a subtle,\n// opacity-only glow pulse (reduced-motion safe via motion-safe:).\nconst liveStatuses = new Set(['IN_PROGRESS', 'ACTIVE', 'GRADING']);\n\nconst statusVariantMap: Record<string, BadgeVariant> = {\n // Authoring lifecycle (course + assessment)\n DRAFT: 'secondary',\n IN_REVIEW: 'warning',\n PUBLISHED: 'success',\n ARCHIVED: 'outline',\n\n // Enrollment status\n ENROLLED: 'info',\n IN_PROGRESS: 'info',\n COMPLETED: 'success',\n DROPPED: 'destructive',\n EXPIRED: 'outline',\n\n // Lesson-progress status\n NOT_STARTED: 'secondary',\n\n // Submission status\n SUBMITTED: 'info',\n GRADING: 'warning',\n GRADED: 'success',\n RETURNED: 'warning',\n\n // Cohort status\n SCHEDULED: 'secondary',\n ENROLLING: 'info',\n ACTIVE: 'success',\n CANCELLED: 'destructive',\n\n // Certificate status\n ISSUED: 'success',\n REVOKED: 'destructive',\n\n // Course level\n BEGINNER: 'success',\n INTERMEDIATE: 'info',\n ADVANCED: 'warning',\n EXPERT: 'destructive',\n\n // Skill category (neutral badges)\n TECHNICAL: 'outline',\n BUSINESS: 'outline',\n CREATIVE: 'outline',\n COGNITIVE: 'outline',\n SOCIAL: 'outline',\n LANGUAGE: 'outline',\n};\n\nexport const StatusBadge: FC<StatusBadgeProps> = ({ status, className }) => {\n const key = status.toUpperCase();\n const variant = statusVariantMap[key] ?? 'secondary';\n const isLive = liveStatuses.has(key);\n\n return (\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',\n variantStyles[variant],\n className\n )}\n >\n {isLive && (\n <span\n aria-hidden=\"true\"\n className=\"size-1.5 rounded-full bg-current motion-safe:animate-vc-glow-pulse\"\n />\n )}\n {formatEnumLabel(status)}\n </span>\n );\n};\n"],"mappings":";;;;AAqBA,IAAM,IAA8C;CAClD,SAAS;CACT,SAAS;CACT,aAAa;CACb,WAAW;CACX,MAAM;CACN,SAAS;AACX,GAIM,IAAe,IAAI,IAAI;CAAC;CAAe;CAAU;AAAS,CAAC,GAE3D,IAAiD;CAErD,OAAO;CACP,WAAW;CACX,WAAW;CACX,UAAU;CAGV,UAAU;CACV,aAAa;CACb,WAAW;CACX,SAAS;CACT,SAAS;CAGT,aAAa;CAGb,WAAW;CACX,SAAS;CACT,QAAQ;CACR,UAAU;CAGV,WAAW;CACX,WAAW;CACX,QAAQ;CACR,WAAW;CAGX,QAAQ;CACR,SAAS;CAGT,UAAU;CACV,cAAc;CACd,UAAU;CACV,QAAQ;CAGR,WAAW;CACX,UAAU;CACV,UAAU;CACV,WAAW;CACX,QAAQ;CACR,UAAU;AACZ,GAEa,KAAqC,EAAE,WAAQ,mBAAgB;CAC1E,IAAM,IAAM,EAAO,YAAY,GACzB,IAAU,EAAiB,MAAQ,aACnC,IAAS,EAAa,IAAI,CAAG;CAEnC,OACE,kBAAC,QAAD;EACE,WAAW,EACT,mFACA,EAAc,IACd,CACF;YALF,CAOG,KACC,kBAAC,QAAD;GACE,eAAY;GACZ,WAAU;EACX,CAAA,GAEF,EAAgB,CAAM,CACnB;;AAEV"}
@@ -1 +1 @@
1
- {"version":3,"file":"Tabs.js","names":[],"sources":["../../../src/buildmyiq/components/Tabs.tsx"],"sourcesContent":["/**\n * Tabs — a top-level segmented navigation control for multi-section pages\n * (Monitoring: Sensors/Measurements/Thresholds; Carbon: Ledger/Factors/Targets).\n * @module buildmyiq/components\n *\n * Visually heavier than {@link FilterTabs} (underline-style page tabs vs a pill\n * filter). Purely presentational — the parent owns the active-tab state.\n */\n\nimport type { ReactNode } from 'react';\nimport { cn } from '../utils/cn';\n\nexport interface TabItem<V extends string> {\n value: V;\n label: string;\n icon?: ReactNode;\n count?: number;\n}\n\ninterface TabsProps<V extends string> {\n items: TabItem<V>[];\n value: V;\n onChange: (value: V) => void;\n className?: string;\n}\n\nexport function Tabs<V extends string>({ items, value, onChange, className }: TabsProps<V>) {\n return (\n <div\n role=\"tablist\"\n className={cn('flex flex-wrap items-center gap-1 border-b border-border-seam', className)}\n >\n {items.map((item) => {\n const active = item.value === value;\n return (\n <button\n key={item.value}\n type=\"button\"\n role=\"tab\"\n aria-selected={active}\n onClick={() => onChange(item.value)}\n className={cn(\n 'relative -mb-px inline-flex items-center gap-2 border-b-2 px-4 py-2.5 text-sm font-medium transition-colors',\n active\n ? 'border-accent text-text-primary'\n : 'border-transparent text-text-secondary hover:text-text-primary'\n )}\n >\n {item.icon}\n {item.label}\n {typeof item.count === 'number' && (\n <span\n className={cn(\n 'rounded-full px-1.5 py-0.5 text-xs tabular-nums',\n active\n ? 'bg-[var(--color-accent-soft)] text-text-primary'\n : 'bg-bg-sunken text-text-secondary'\n )}\n >\n {item.count}\n </span>\n )}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":";;;AA0BA,SAAgB,EAAuB,EAAE,UAAO,UAAO,aAAU,gBAA2B;AAC1F,QACE,kBAAC,OAAD;EACE,MAAK;EACL,WAAW,EAAG,iEAAiE,EAAU;YAExF,EAAM,KAAK,MAAS;GACnB,IAAM,IAAS,EAAK,UAAU;AAC9B,UACE,kBAAC,UAAD;IAEE,MAAK;IACL,MAAK;IACL,iBAAe;IACf,eAAe,EAAS,EAAK,MAAM;IACnC,WAAW,EACT,+GACA,IACI,oCACA,iEACL;cAXH;KAaG,EAAK;KACL,EAAK;KACL,OAAO,EAAK,SAAU,YACrB,kBAAC,QAAD;MACE,WAAW,EACT,mDACA,IACI,oDACA,mCACL;gBAEA,EAAK;MACD,CAAA;KAEF;MA1BF,EAAK,MA0BH;IAEX;EACE,CAAA"}
1
+ {"version":3,"file":"Tabs.js","names":[],"sources":["../../../src/buildmyiq/components/Tabs.tsx"],"sourcesContent":["/**\n * Tabs — a top-level segmented navigation control for multi-section pages\n * (Monitoring: Sensors/Measurements/Thresholds; Carbon: Ledger/Factors/Targets).\n * @module buildmyiq/components\n *\n * Visually heavier than {@link FilterTabs} (underline-style page tabs vs a pill\n * filter). Purely presentational — the parent owns the active-tab state.\n */\n\nimport type { ReactNode } from 'react';\nimport { cn } from '../utils/cn';\n\nexport interface TabItem<V extends string> {\n value: V;\n label: string;\n icon?: ReactNode;\n count?: number;\n}\n\ninterface TabsProps<V extends string> {\n items: TabItem<V>[];\n value: V;\n onChange: (value: V) => void;\n className?: string;\n}\n\nexport function Tabs<V extends string>({ items, value, onChange, className }: TabsProps<V>) {\n return (\n <div\n role=\"tablist\"\n className={cn('flex flex-wrap items-center gap-1 border-b border-border-seam', className)}\n >\n {items.map((item) => {\n const active = item.value === value;\n return (\n <button\n key={item.value}\n type=\"button\"\n role=\"tab\"\n aria-selected={active}\n onClick={() => onChange(item.value)}\n className={cn(\n 'relative -mb-px inline-flex items-center gap-2 border-b-2 px-4 py-2.5 text-sm font-medium transition-colors',\n active\n ? 'border-accent text-text-primary'\n : 'border-transparent text-text-secondary hover:text-text-primary'\n )}\n >\n {item.icon}\n {item.label}\n {typeof item.count === 'number' && (\n <span\n className={cn(\n 'rounded-full px-1.5 py-0.5 text-xs tabular-nums',\n active\n ? 'bg-[var(--color-accent-soft)] text-text-primary'\n : 'bg-bg-sunken text-text-secondary'\n )}\n >\n {item.count}\n </span>\n )}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":";;;AA0BA,SAAgB,EAAuB,EAAE,UAAO,UAAO,aAAU,gBAA2B;CAC1F,OACE,kBAAC,OAAD;EACE,MAAK;EACL,WAAW,EAAG,iEAAiE,CAAS;YAEvF,EAAM,KAAK,MAAS;GACnB,IAAM,IAAS,EAAK,UAAU;GAC9B,OACE,kBAAC,UAAD;IAEE,MAAK;IACL,MAAK;IACL,iBAAe;IACf,eAAe,EAAS,EAAK,KAAK;IAClC,WAAW,EACT,+GACA,IACI,oCACA,gEACN;cAXF;KAaG,EAAK;KACL,EAAK;KACL,OAAO,EAAK,SAAU,YACrB,kBAAC,QAAD;MACE,WAAW,EACT,mDACA,IACI,oDACA,kCACN;gBAEC,EAAK;KACF,CAAA;IAEF;MA1BD,EAAK,KA0BJ;EAEZ,CAAC;CACE,CAAA;AAET"}
@@ -1 +1 @@
1
- {"version":3,"file":"Viz.js","names":[],"sources":["../../../src/buildmyiq/components/Viz.tsx"],"sourcesContent":["/**\n * Viz — tiny self-contained tokenized SVG chart primitives.\n * @module buildmyiq/components\n *\n * ProgressRing (circular % meter) and Sparkline (trend line). Both are pure SVG,\n * driven by semantic CSS-variable colours (no raw hex), and take numeric data\n * only — the caller always passes REAL derived values.\n */\n\nimport type { FC } from 'react';\nimport { cn } from '../utils/cn';\n\ninterface ProgressRingProps {\n /** 0–100. */\n value: number;\n size?: number;\n stroke?: number;\n /** CSS colour for the arc (defaults to the teal accent). */\n color?: string;\n /** Center label; when omitted the rounded percent is shown. */\n label?: string;\n className?: string;\n}\n\nexport const ProgressRing: FC<ProgressRingProps> = ({\n value,\n size = 64,\n stroke = 6,\n color = 'var(--color-accent-teal)',\n label,\n className,\n}) => {\n const clamped = Math.max(0, Math.min(100, value));\n const r = (size - stroke) / 2;\n const c = 2 * Math.PI * r;\n const offset = c - (clamped / 100) * c;\n return (\n <div\n className={cn('relative inline-flex items-center justify-center', className)}\n style={{ width: size, height: size }}\n >\n <svg width={size} height={size} className=\"-rotate-90\">\n <circle\n cx={size / 2}\n cy={size / 2}\n r={r}\n fill=\"none\"\n stroke=\"var(--color-bg-sunken)\"\n strokeWidth={stroke}\n />\n <circle\n cx={size / 2}\n cy={size / 2}\n r={r}\n fill=\"none\"\n stroke={color}\n strokeWidth={stroke}\n strokeLinecap=\"round\"\n strokeDasharray={c}\n strokeDashoffset={offset}\n className=\"transition-[stroke-dashoffset] duration-700 ease-out\"\n />\n </svg>\n <span className=\"absolute text-xs font-semibold tabular-nums text-text-primary\">\n {label ?? `${Math.round(clamped)}%`}\n </span>\n </div>\n );\n};\n\ninterface SparklineProps {\n /** Series of numbers; rendered as a smooth trend line. */\n data: number[];\n width?: number;\n height?: number;\n color?: string;\n className?: string;\n}\n\nexport const Sparkline: FC<SparklineProps> = ({\n data,\n width = 96,\n height = 28,\n color = 'var(--color-accent-teal)',\n className,\n}) => {\n if (data.length < 2) {\n return (\n <svg width={width} height={height} className={className} aria-hidden=\"true\">\n <line\n x1=\"0\"\n y1={height - 2}\n x2={width}\n y2={height - 2}\n stroke=\"var(--color-border-seam)\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n </svg>\n );\n }\n const min = Math.min(...data);\n const max = Math.max(...data);\n const span = max - min || 1;\n const stepX = width / (data.length - 1);\n const pts = data.map((v, i) => {\n const x = i * stepX;\n const y = height - 3 - ((v - min) / span) * (height - 6);\n return `${x.toFixed(1)},${y.toFixed(1)}`;\n });\n return (\n <svg width={width} height={height} className={className} aria-hidden=\"true\">\n <polyline\n points={pts.join(' ')}\n fill=\"none\"\n stroke={color}\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n );\n};\n"],"mappings":";;;AAwBA,IAAa,KAAuC,EAClD,UACA,UAAO,IACP,YAAS,GACT,WAAQ,4BACR,UACA,mBACI;CACJ,IAAM,IAAU,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,EAAM,CAAC,EAC3C,KAAK,IAAO,KAAU,GACtB,IAAI,IAAI,KAAK,KAAK,GAClB,IAAS,IAAK,IAAU,MAAO;AACrC,QACE,kBAAC,OAAD;EACE,WAAW,EAAG,oDAAoD,EAAU;EAC5E,OAAO;GAAE,OAAO;GAAM,QAAQ;GAAM;YAFtC,CAIE,kBAAC,OAAD;GAAK,OAAO;GAAM,QAAQ;GAAM,WAAU;aAA1C,CACE,kBAAC,UAAD;IACE,IAAI,IAAO;IACX,IAAI,IAAO;IACR;IACH,MAAK;IACL,QAAO;IACP,aAAa;IACb,CAAA,EACF,kBAAC,UAAD;IACE,IAAI,IAAO;IACX,IAAI,IAAO;IACR;IACH,MAAK;IACL,QAAQ;IACR,aAAa;IACb,eAAc;IACd,iBAAiB;IACjB,kBAAkB;IAClB,WAAU;IACV,CAAA,CACE;MACN,kBAAC,QAAD;GAAM,WAAU;aACb,KAAS,GAAG,KAAK,MAAM,EAAQ,CAAC;GAC5B,CAAA,CACH;;GAaG,KAAiC,EAC5C,SACA,WAAQ,IACR,YAAS,IACT,WAAQ,4BACR,mBACI;AACJ,KAAI,EAAK,SAAS,EAChB,QACE,kBAAC,OAAD;EAAY;EAAe;EAAmB;EAAW,eAAY;YACnE,kBAAC,QAAD;GACE,IAAG;GACH,IAAI,IAAS;GACb,IAAI;GACJ,IAAI,IAAS;GACb,QAAO;GACP,aAAY;GACZ,eAAc;GACd,CAAA;EACE,CAAA;CAGV,IAAM,IAAM,KAAK,IAAI,GAAG,EAAK,EAEvB,IADM,KAAK,IAAI,GAAG,EAAK,GACV,KAAO,GACpB,IAAQ,KAAS,EAAK,SAAS;AAMrC,QACE,kBAAC,OAAD;EAAY;EAAe;EAAmB;EAAW,eAAY;YACnE,kBAAC,YAAD;GACE,QARM,EAAK,KAAK,GAAG,MAAM;IAC7B,IAAM,IAAI,IAAI,GACR,IAAI,IAAS,KAAM,IAAI,KAAO,KAAS,IAAS;AACtD,WAAO,GAAG,EAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,QAAQ,EAAE;KACtC,CAIgB,KAAK,IAAI;GACrB,MAAK;GACL,QAAQ;GACR,aAAY;GACZ,eAAc;GACd,gBAAe;GACf,CAAA;EACE,CAAA"}
1
+ {"version":3,"file":"Viz.js","names":[],"sources":["../../../src/buildmyiq/components/Viz.tsx"],"sourcesContent":["/**\n * Viz — tiny self-contained tokenized SVG chart primitives.\n * @module buildmyiq/components\n *\n * ProgressRing (circular % meter) and Sparkline (trend line). Both are pure SVG,\n * driven by semantic CSS-variable colours (no raw hex), and take numeric data\n * only — the caller always passes REAL derived values.\n */\n\nimport type { FC } from 'react';\nimport { cn } from '../utils/cn';\n\ninterface ProgressRingProps {\n /** 0–100. */\n value: number;\n size?: number;\n stroke?: number;\n /** CSS colour for the arc (defaults to the teal accent). */\n color?: string;\n /** Center label; when omitted the rounded percent is shown. */\n label?: string;\n className?: string;\n}\n\nexport const ProgressRing: FC<ProgressRingProps> = ({\n value,\n size = 64,\n stroke = 6,\n color = 'var(--color-accent-teal)',\n label,\n className,\n}) => {\n const clamped = Math.max(0, Math.min(100, value));\n const r = (size - stroke) / 2;\n const c = 2 * Math.PI * r;\n const offset = c - (clamped / 100) * c;\n return (\n <div\n className={cn('relative inline-flex items-center justify-center', className)}\n style={{ width: size, height: size }}\n >\n <svg width={size} height={size} className=\"-rotate-90\">\n <circle\n cx={size / 2}\n cy={size / 2}\n r={r}\n fill=\"none\"\n stroke=\"var(--color-bg-sunken)\"\n strokeWidth={stroke}\n />\n <circle\n cx={size / 2}\n cy={size / 2}\n r={r}\n fill=\"none\"\n stroke={color}\n strokeWidth={stroke}\n strokeLinecap=\"round\"\n strokeDasharray={c}\n strokeDashoffset={offset}\n className=\"transition-[stroke-dashoffset] duration-700 ease-out\"\n />\n </svg>\n <span className=\"absolute text-xs font-semibold tabular-nums text-text-primary\">\n {label ?? `${Math.round(clamped)}%`}\n </span>\n </div>\n );\n};\n\ninterface SparklineProps {\n /** Series of numbers; rendered as a smooth trend line. */\n data: number[];\n width?: number;\n height?: number;\n color?: string;\n className?: string;\n}\n\nexport const Sparkline: FC<SparklineProps> = ({\n data,\n width = 96,\n height = 28,\n color = 'var(--color-accent-teal)',\n className,\n}) => {\n if (data.length < 2) {\n return (\n <svg width={width} height={height} className={className} aria-hidden=\"true\">\n <line\n x1=\"0\"\n y1={height - 2}\n x2={width}\n y2={height - 2}\n stroke=\"var(--color-border-seam)\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n </svg>\n );\n }\n const min = Math.min(...data);\n const max = Math.max(...data);\n const span = max - min || 1;\n const stepX = width / (data.length - 1);\n const pts = data.map((v, i) => {\n const x = i * stepX;\n const y = height - 3 - ((v - min) / span) * (height - 6);\n return `${x.toFixed(1)},${y.toFixed(1)}`;\n });\n return (\n <svg width={width} height={height} className={className} aria-hidden=\"true\">\n <polyline\n points={pts.join(' ')}\n fill=\"none\"\n stroke={color}\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n );\n};\n"],"mappings":";;;AAwBA,IAAa,KAAuC,EAClD,UACA,UAAO,IACP,YAAS,GACT,WAAQ,4BACR,UACA,mBACI;CACJ,IAAM,IAAU,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,CAAK,CAAC,GAC1C,KAAK,IAAO,KAAU,GACtB,IAAI,IAAI,KAAK,KAAK,GAClB,IAAS,IAAK,IAAU,MAAO;CACrC,OACE,kBAAC,OAAD;EACE,WAAW,EAAG,oDAAoD,CAAS;EAC3E,OAAO;GAAE,OAAO;GAAM,QAAQ;EAAK;YAFrC,CAIE,kBAAC,OAAD;GAAK,OAAO;GAAM,QAAQ;GAAM,WAAU;aAA1C,CACE,kBAAC,UAAD;IACE,IAAI,IAAO;IACX,IAAI,IAAO;IACR;IACH,MAAK;IACL,QAAO;IACP,aAAa;GACd,CAAA,GACD,kBAAC,UAAD;IACE,IAAI,IAAO;IACX,IAAI,IAAO;IACR;IACH,MAAK;IACL,QAAQ;IACR,aAAa;IACb,eAAc;IACd,iBAAiB;IACjB,kBAAkB;IAClB,WAAU;GACX,CAAA,CACE;MACL,kBAAC,QAAD;GAAM,WAAU;aACb,KAAS,GAAG,KAAK,MAAM,CAAO,EAAE;EAC7B,CAAA,CACH;;AAET,GAWa,KAAiC,EAC5C,SACA,WAAQ,IACR,YAAS,IACT,WAAQ,4BACR,mBACI;CACJ,IAAI,EAAK,SAAS,GAChB,OACE,kBAAC,OAAD;EAAY;EAAe;EAAmB;EAAW,eAAY;YACnE,kBAAC,QAAD;GACE,IAAG;GACH,IAAI,IAAS;GACb,IAAI;GACJ,IAAI,IAAS;GACb,QAAO;GACP,aAAY;GACZ,eAAc;EACf,CAAA;CACE,CAAA;CAGT,IAAM,IAAM,KAAK,IAAI,GAAG,CAAI,GAEtB,IADM,KAAK,IAAI,GAAG,CACX,IAAM,KAAO,GACpB,IAAQ,KAAS,EAAK,SAAS;CAMrC,OACE,kBAAC,OAAD;EAAY;EAAe;EAAmB;EAAW,eAAY;YACnE,kBAAC,YAAD;GACE,QARM,EAAK,KAAK,GAAG,MAAM;IAC7B,IAAM,IAAI,IAAI,GACR,IAAI,IAAS,KAAM,IAAI,KAAO,KAAS,IAAS;IACtD,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,GAAG,EAAE,QAAQ,CAAC;GACvC,CAIc,EAAI,KAAK,GAAG;GACpB,MAAK;GACL,QAAQ;GACR,aAAY;GACZ,eAAc;GACd,gBAAe;EAChB,CAAA;CACE,CAAA;AAET"}
@@ -18,4 +18,5 @@ export { EvidenceUpload } from './EvidenceUpload';
18
18
  export { FileLink } from './FileLink';
19
19
  export { BuiDiscussion } from './BuiDiscussion';
20
20
  export { BuiTags } from './BuiTags';
21
+ export { BuiPermissionButton, type BuiPermissionButtonProps } from './BuiPermissionButton';
21
22
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/components/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,EAAE,SAAS,EAAE,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAC7D,OAAO,EAAE,IAAI,EAAE,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/components/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,EAAE,SAAS,EAAE,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAC7D,OAAO,EAAE,IAAI,EAAE,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,mBAAmB,EAAE,KAAK,wBAAwB,EAAE,MAAM,uBAAuB,CAAC"}
@@ -14,3 +14,4 @@ import "./EvidenceUpload.js";
14
14
  import "./FileLink.js";
15
15
  import "./BuiDiscussion.js";
16
16
  import "./BuiTags.js";
17
+ import "./BuiPermissionButton.js";
@@ -1 +1 @@
1
- {"version":3,"file":"BuildmyiqContext.js","names":[],"sources":["../../../src/buildmyiq/context/BuildmyiqContext.tsx"],"sourcesContent":["/**\n * Buildmyiq Context Provider\n * @module buildmyiq/context\n *\n * Splits standalone mode (wraps its own MemoryRouter + useNavigate) from\n * integrated mode (the host shell supplies `navigate`). Exposes gateway/auth\n * context + relative navigation helpers to every page.\n */\n\nimport type { FC, PropsWithChildren } from 'react';\nimport { createContext, useContext, useMemo, useCallback } from 'react';\nimport { MemoryRouter, useNavigate as useRouterNavigate } from 'react-router-dom';\nimport type { BuildmyiqRootProps } from '../types';\nimport { joinPath } from '../utils/navigation';\n\ninterface BuildmyiqContextValue extends Omit<\n BuildmyiqRootProps,\n 'registerNavItems' | 'registerFooterItems' | 'navigate'\n> {\n isStandalone: boolean;\n /** Shell's navigate function (only available in integrated mode). */\n navigate?: (path: string) => void;\n /** Navigate to a path relative to basePath. */\n navigateTo: (relativePath: string) => void;\n /** Get the full path for a relative path. */\n getPath: (relativePath: string) => string;\n}\n\nconst BuildmyiqContext = createContext<BuildmyiqContextValue | null>(null);\n\nexport function useBuildmyiq(): BuildmyiqContextValue {\n const context = useContext(BuildmyiqContext);\n if (!context) {\n throw new Error('useBuildmyiq must be used within an BuildmyiqProvider');\n }\n return context;\n}\n\ntype ProviderProps = Omit<BuildmyiqRootProps, 'registerNavItems' | 'registerFooterItems'> & {\n isStandalone: boolean;\n};\n\nconst StandaloneProviderInner: FC<PropsWithChildren<ProviderProps>> = ({\n children,\n basePath,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n}) => {\n const routerNavigate = useRouterNavigate();\n const effectiveBasePath = basePath || '/';\n\n const getPath = useCallback(\n (relativePath: string) => joinPath(effectiveBasePath, relativePath),\n [effectiveBasePath]\n );\n\n const navigateTo = useCallback(\n (relativePath: string) => {\n routerNavigate(joinPath(effectiveBasePath, relativePath));\n },\n [effectiveBasePath, routerNavigate]\n );\n\n const contextValue = useMemo<BuildmyiqContextValue>(\n () => ({\n basePath: effectiveBasePath,\n navigate: undefined,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n isStandalone: true,\n navigateTo,\n getPath,\n }),\n [\n effectiveBasePath,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n navigateTo,\n getPath,\n ]\n );\n\n return <BuildmyiqContext.Provider value={contextValue}>{children}</BuildmyiqContext.Provider>;\n};\n\nconst IntegratedProviderInner: FC<\n PropsWithChildren<ProviderProps & { navigate: (path: string) => void }>\n> = ({\n children,\n basePath,\n navigate,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n}) => {\n const effectiveBasePath = basePath || '/';\n\n const getPath = useCallback(\n (relativePath: string) => joinPath(effectiveBasePath, relativePath),\n [effectiveBasePath]\n );\n\n const navigateTo = useCallback(\n (relativePath: string) => {\n navigate(joinPath(effectiveBasePath, relativePath));\n },\n [effectiveBasePath, navigate]\n );\n\n const contextValue = useMemo<BuildmyiqContextValue>(\n () => ({\n basePath: effectiveBasePath,\n navigate,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n isStandalone: false,\n navigateTo,\n getPath,\n }),\n [\n effectiveBasePath,\n navigate,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n navigateTo,\n getPath,\n ]\n );\n\n return <BuildmyiqContext.Provider value={contextValue}>{children}</BuildmyiqContext.Provider>;\n};\n\nexport const BuildmyiqProvider: FC<PropsWithChildren<BuildmyiqRootProps>> = (props) => {\n const { children, navigate } = props;\n const isStandalone = !navigate;\n\n if (isStandalone) {\n return (\n <MemoryRouter>\n <StandaloneProviderInner {...props} isStandalone={true}>\n {children}\n </StandaloneProviderInner>\n </MemoryRouter>\n );\n }\n\n return (\n <IntegratedProviderInner {...props} navigate={navigate} isStandalone={false}>\n {children}\n </IntegratedProviderInner>\n );\n};\n"],"mappings":";;;;;AA4BA,IAAM,IAAmB,EAA4C,KAAK;AAE1E,SAAgB,IAAsC;CACpD,IAAM,IAAU,EAAW,EAAiB;AAC5C,KAAI,CAAC,EACH,OAAU,MAAM,wDAAwD;AAE1E,QAAO;;AAOT,IAAM,KAAiE,EACrE,aACA,aACA,gBACA,aACA,gBACA,kBACA,mBACI;CACJ,IAAM,IAAiB,GAAmB,EACpC,IAAoB,KAAY,KAEhC,IAAU,GACb,MAAyB,EAAS,GAAmB,EAAa,EACnE,CAAC,EAAkB,CACpB,EAEK,IAAa,GAChB,MAAyB;AACxB,IAAe,EAAS,GAAmB,EAAa,CAAC;IAE3D,CAAC,GAAmB,EAAe,CACpC,EAEK,IAAe,SACZ;EACL,UAAU;EACV,UAAU,KAAA;EACV;EACA;EACA;EACA;EACA;EACA,cAAc;EACd;EACA;EACD,GACD;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CACF;AAED,QAAO,kBAAC,EAAiB,UAAlB;EAA2B,OAAO;EAAe;EAAqC,CAAA;GAGzF,KAED,EACH,aACA,aACA,aACA,gBACA,aACA,gBACA,kBACA,mBACI;CACJ,IAAM,IAAoB,KAAY,KAEhC,IAAU,GACb,MAAyB,EAAS,GAAmB,EAAa,EACnE,CAAC,EAAkB,CACpB,EAEK,IAAa,GAChB,MAAyB;AACxB,IAAS,EAAS,GAAmB,EAAa,CAAC;IAErD,CAAC,GAAmB,EAAS,CAC9B,EAEK,IAAe,SACZ;EACL,UAAU;EACV;EACA;EACA;EACA;EACA;EACA;EACA,cAAc;EACd;EACA;EACD,GACD;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CACF;AAED,QAAO,kBAAC,EAAiB,UAAlB;EAA2B,OAAO;EAAe;EAAqC,CAAA;GAGlF,KAAgE,MAAU;CACrF,IAAM,EAAE,aAAU,gBAAa;AAa/B,QAZsB,IAapB,kBAAC,GAAD;EAAyB,GAAI;EAAiB;EAAU,cAAc;EACnE;EACuB,CAAA,GAXxB,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;EAAyB,GAAI;EAAO,cAAc;EAC/C;EACuB,CAAA,EACb,CAAA"}
1
+ {"version":3,"file":"BuildmyiqContext.js","names":[],"sources":["../../../src/buildmyiq/context/BuildmyiqContext.tsx"],"sourcesContent":["/**\n * Buildmyiq Context Provider\n * @module buildmyiq/context\n *\n * Splits standalone mode (wraps its own MemoryRouter + useNavigate) from\n * integrated mode (the host shell supplies `navigate`). Exposes gateway/auth\n * context + relative navigation helpers to every page.\n */\n\nimport type { FC, PropsWithChildren } from 'react';\nimport { createContext, useContext, useMemo, useCallback } from 'react';\nimport { MemoryRouter, useNavigate as useRouterNavigate } from 'react-router-dom';\nimport type { BuildmyiqRootProps } from '../types';\nimport { joinPath } from '../utils/navigation';\n\ninterface BuildmyiqContextValue extends Omit<\n BuildmyiqRootProps,\n 'registerNavItems' | 'registerFooterItems' | 'navigate'\n> {\n isStandalone: boolean;\n /** Shell's navigate function (only available in integrated mode). */\n navigate?: (path: string) => void;\n /** Navigate to a path relative to basePath. */\n navigateTo: (relativePath: string) => void;\n /** Get the full path for a relative path. */\n getPath: (relativePath: string) => string;\n}\n\nconst BuildmyiqContext = createContext<BuildmyiqContextValue | null>(null);\n\nexport function useBuildmyiq(): BuildmyiqContextValue {\n const context = useContext(BuildmyiqContext);\n if (!context) {\n throw new Error('useBuildmyiq must be used within an BuildmyiqProvider');\n }\n return context;\n}\n\ntype ProviderProps = Omit<BuildmyiqRootProps, 'registerNavItems' | 'registerFooterItems'> & {\n isStandalone: boolean;\n};\n\nconst StandaloneProviderInner: FC<PropsWithChildren<ProviderProps>> = ({\n children,\n basePath,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n}) => {\n const routerNavigate = useRouterNavigate();\n const effectiveBasePath = basePath || '/';\n\n const getPath = useCallback(\n (relativePath: string) => joinPath(effectiveBasePath, relativePath),\n [effectiveBasePath]\n );\n\n const navigateTo = useCallback(\n (relativePath: string) => {\n routerNavigate(joinPath(effectiveBasePath, relativePath));\n },\n [effectiveBasePath, routerNavigate]\n );\n\n const contextValue = useMemo<BuildmyiqContextValue>(\n () => ({\n basePath: effectiveBasePath,\n navigate: undefined,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n isStandalone: true,\n navigateTo,\n getPath,\n }),\n [\n effectiveBasePath,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n navigateTo,\n getPath,\n ]\n );\n\n return <BuildmyiqContext.Provider value={contextValue}>{children}</BuildmyiqContext.Provider>;\n};\n\nconst IntegratedProviderInner: FC<\n PropsWithChildren<ProviderProps & { navigate: (path: string) => void }>\n> = ({\n children,\n basePath,\n navigate,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n}) => {\n const effectiveBasePath = basePath || '/';\n\n const getPath = useCallback(\n (relativePath: string) => joinPath(effectiveBasePath, relativePath),\n [effectiveBasePath]\n );\n\n const navigateTo = useCallback(\n (relativePath: string) => {\n navigate(joinPath(effectiveBasePath, relativePath));\n },\n [effectiveBasePath, navigate]\n );\n\n const contextValue = useMemo<BuildmyiqContextValue>(\n () => ({\n basePath: effectiveBasePath,\n navigate,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n isStandalone: false,\n navigateTo,\n getPath,\n }),\n [\n effectiveBasePath,\n navigate,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n navigateTo,\n getPath,\n ]\n );\n\n return <BuildmyiqContext.Provider value={contextValue}>{children}</BuildmyiqContext.Provider>;\n};\n\nexport const BuildmyiqProvider: FC<PropsWithChildren<BuildmyiqRootProps>> = (props) => {\n const { children, navigate } = props;\n const isStandalone = !navigate;\n\n if (isStandalone) {\n return (\n <MemoryRouter>\n <StandaloneProviderInner {...props} isStandalone={true}>\n {children}\n </StandaloneProviderInner>\n </MemoryRouter>\n );\n }\n\n return (\n <IntegratedProviderInner {...props} navigate={navigate} isStandalone={false}>\n {children}\n </IntegratedProviderInner>\n );\n};\n"],"mappings":";;;;;AA4BA,IAAM,IAAmB,EAA4C,IAAI;AAEzE,SAAgB,IAAsC;CACpD,IAAM,IAAU,EAAW,CAAgB;CAC3C,IAAI,CAAC,GACH,MAAU,MAAM,uDAAuD;CAEzE,OAAO;AACT;AAMA,IAAM,KAAiE,EACrE,aACA,aACA,gBACA,aACA,gBACA,kBACA,mBACI;CACJ,IAAM,IAAiB,EAAkB,GACnC,IAAoB,KAAY,KAEhC,IAAU,GACb,MAAyB,EAAS,GAAmB,CAAY,GAClE,CAAC,CAAiB,CACpB,GAEM,IAAa,GAChB,MAAyB;EACxB,EAAe,EAAS,GAAmB,CAAY,CAAC;CAC1D,GACA,CAAC,GAAmB,CAAc,CACpC,GAEM,IAAe,SACZ;EACL,UAAU;EACV,UAAU,KAAA;EACV;EACA;EACA;EACA;EACA;EACA,cAAc;EACd;EACA;CACF,IACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,CACF;CAEA,OAAO,kBAAC,EAAiB,UAAlB;EAA2B,OAAO;EAAe;CAAoC,CAAA;AAC9F,GAEM,KAED,EACH,aACA,aACA,aACA,gBACA,aACA,gBACA,kBACA,mBACI;CACJ,IAAM,IAAoB,KAAY,KAEhC,IAAU,GACb,MAAyB,EAAS,GAAmB,CAAY,GAClE,CAAC,CAAiB,CACpB,GAEM,IAAa,GAChB,MAAyB;EACxB,EAAS,EAAS,GAAmB,CAAY,CAAC;CACpD,GACA,CAAC,GAAmB,CAAQ,CAC9B,GAEM,IAAe,SACZ;EACL,UAAU;EACV;EACA;EACA;EACA;EACA;EACA;EACA,cAAc;EACd;EACA;CACF,IACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,CACF;CAEA,OAAO,kBAAC,EAAiB,UAAlB;EAA2B,OAAO;EAAe;CAAoC,CAAA;AAC9F,GAEa,KAAgE,MAAU;CACrF,IAAM,EAAE,aAAU,gBAAa;CAa/B,OAZsB,IAapB,kBAAC,GAAD;EAAyB,GAAI;EAAiB;EAAU,cAAc;EACnE;CACsB,CAAA,IAXvB,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;EAAyB,GAAI;EAAO,cAAc;EAC/C;CACsB,CAAA,EACb,CAAA;AASpB"}
@@ -7,4 +7,5 @@ export * from './useCursorPager';
7
7
  export * from './useBuiFiles';
8
8
  export * from './useBuiComments';
9
9
  export * from './useBuiTags';
10
+ export * from './useBuiPermissions';
10
11
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/hooks/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/hooks/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC"}
@@ -3,3 +3,4 @@ import "./useCursorPager.js";
3
3
  import "./useBuiFiles.js";
4
4
  import "./useBuiComments.js";
5
5
  import "./useBuiTags.js";
6
+ import "./useBuiPermissions.js";
@@ -1 +1 @@
1
- {"version":3,"file":"useBuiComments.js","names":[],"sources":["../../../src/buildmyiq/hooks/useBuiComments.ts"],"sourcesContent":["/**\n * Buildmyiq — wspace-conversations-svc EntityComment REUSE hooks (Integration B).\n * @module buildmyiq/hooks/useBuiComments\n *\n * Adds a discussion surface to Buildmyiq entities WITHOUT any buildmyiq-svc change by\n * reusing the generic EntityComment API composed into the WORKSPACE supergraph.\n * Comments attach to any entity by `(entityType, entityId)`:\n * - `getEntityComments(input): EntityCommentConnection!`\n * - `createEntityComment(input): EntityComment!`\n *\n * Transport reuses `wspaceFetch` from `useBuiFiles` (workspace gateway +\n * workspace token). Lives in a separate file; `useBuildmyiqApi.ts` is\n * untouched.\n */\n\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\n\nimport { useBuildmyiq } from '../context';\nimport { wspaceFetch } from './useBuiFiles';\n\nexport interface BuiComment {\n id: string;\n authorId: string;\n authorType: string;\n content: {\n text: string | null;\n markdown: string | null;\n };\n createdAt: string;\n edited: boolean;\n parentCommentId: string | null;\n replyCount: number;\n}\n\nconst commentsKey = (entityType: string, entityId: string) =>\n ['buildmyiq', 'comments', entityType, entityId] as const;\n\n/** Top-level (root) comments for an Buildmyiq entity, oldest first. */\nexport const useBuiComments = (entityType: string, entityId: string | undefined) => {\n const { apiGatewayUrl } = useBuildmyiq();\n return useQuery({\n queryKey: commentsKey(entityType, entityId ?? ''),\n enabled: Boolean(entityId),\n queryFn: async () => {\n const data = await wspaceFetch<{\n getEntityComments: { items: BuiComment[]; total: number };\n }>(\n `query BuiGetEntityComments($input: GetCommentsInput!) {\n getEntityComments(input: $input) {\n total\n items {\n id\n authorId\n authorType\n content { text markdown }\n createdAt\n edited\n parentCommentId\n replyCount\n }\n }\n }`,\n {\n input: {\n entityType,\n entityId,\n take: 100,\n parentCommentId: null,\n },\n },\n apiGatewayUrl\n );\n // Render root comments oldest-first (server order is not guaranteed).\n const roots = data.getEntityComments.items.filter((c) => !c.parentCommentId);\n roots.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());\n return { items: roots, total: data.getEntityComments.total };\n },\n });\n};\n\nexport const useEcoCreateComment = (entityType: string, entityId: string) => {\n const { apiGatewayUrl } = useBuildmyiq();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (text: string) => {\n const data = await wspaceFetch<{ createEntityComment: { id: string } }>(\n `mutation BuiCreateEntityComment($input: CreateEntityCommentInput!) {\n createEntityComment(input: $input) { id }\n }`,\n {\n input: {\n entityType,\n entityId,\n content: { text },\n },\n },\n apiGatewayUrl\n );\n return data.createEntityComment;\n },\n onSuccess: () => {\n qc.invalidateQueries({ queryKey: commentsKey(entityType, entityId) });\n },\n });\n};\n"],"mappings":";;;;;AAkCA,IAAM,KAAe,GAAoB,MACvC;CAAC;CAAa;CAAY;CAAY;CAAS,EAGpC,KAAkB,GAAoB,MAAiC;CAClF,IAAM,EAAE,qBAAkB,GAAc;AACxC,QAAO,EAAS;EACd,UAAU,EAAY,GAAY,KAAY,GAAG;EACjD,SAAS,EAAQ;EACjB,SAAS,YAAY;GACnB,IAAM,IAAO,MAAM,EAGjB,6YAeA,EACE,OAAO;IACL;IACA;IACA,MAAM;IACN,iBAAiB;IAClB,EACF,EACD,EACD,EAEK,IAAQ,EAAK,kBAAkB,MAAM,QAAQ,MAAM,CAAC,EAAE,gBAAgB;AAE5E,UADA,EAAM,MAAM,GAAG,MAAM,IAAI,KAAK,EAAE,UAAU,CAAC,SAAS,GAAG,IAAI,KAAK,EAAE,UAAU,CAAC,SAAS,CAAC,EAChF;IAAE,OAAO;IAAO,OAAO,EAAK,kBAAkB;IAAO;;EAE/D,CAAC;GAGS,KAAuB,GAAoB,MAAqB;CAC3E,IAAM,EAAE,qBAAkB,GAAc,EAClC,IAAK,GAAgB;AAC3B,QAAO,EAAY;EACjB,YAAY,OAAO,OACJ,MAAM,EACjB,wIAGA,EACE,OAAO;GACL;GACA;GACA,SAAS,EAAE,SAAM;GAClB,EACF,EACD,EACD,EACW;EAEd,iBAAiB;AACf,KAAG,kBAAkB,EAAE,UAAU,EAAY,GAAY,EAAS,EAAE,CAAC;;EAExE,CAAC"}
1
+ {"version":3,"file":"useBuiComments.js","names":[],"sources":["../../../src/buildmyiq/hooks/useBuiComments.ts"],"sourcesContent":["/**\n * Buildmyiq — wspace-conversations-svc EntityComment REUSE hooks (Integration B).\n * @module buildmyiq/hooks/useBuiComments\n *\n * Adds a discussion surface to Buildmyiq entities WITHOUT any buildmyiq-svc change by\n * reusing the generic EntityComment API composed into the WORKSPACE supergraph.\n * Comments attach to any entity by `(entityType, entityId)`:\n * - `getEntityComments(input): EntityCommentConnection!`\n * - `createEntityComment(input): EntityComment!`\n *\n * Transport reuses `wspaceFetch` from `useBuiFiles` (workspace gateway +\n * workspace token). Lives in a separate file; `useBuildmyiqApi.ts` is\n * untouched.\n */\n\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\n\nimport { useBuildmyiq } from '../context';\nimport { wspaceFetch } from './useBuiFiles';\n\nexport interface BuiComment {\n id: string;\n authorId: string;\n authorType: string;\n content: {\n text: string | null;\n markdown: string | null;\n };\n createdAt: string;\n edited: boolean;\n parentCommentId: string | null;\n replyCount: number;\n}\n\nconst commentsKey = (entityType: string, entityId: string) =>\n ['buildmyiq', 'comments', entityType, entityId] as const;\n\n/** Top-level (root) comments for an Buildmyiq entity, oldest first. */\nexport const useBuiComments = (entityType: string, entityId: string | undefined) => {\n const { apiGatewayUrl } = useBuildmyiq();\n return useQuery({\n queryKey: commentsKey(entityType, entityId ?? ''),\n enabled: Boolean(entityId),\n queryFn: async () => {\n const data = await wspaceFetch<{\n getEntityComments: { items: BuiComment[]; total: number };\n }>(\n `query BuiGetEntityComments($input: GetCommentsInput!) {\n getEntityComments(input: $input) {\n total\n items {\n id\n authorId\n authorType\n content { text markdown }\n createdAt\n edited\n parentCommentId\n replyCount\n }\n }\n }`,\n {\n input: {\n entityType,\n entityId,\n take: 100,\n parentCommentId: null,\n },\n },\n apiGatewayUrl\n );\n // Render root comments oldest-first (server order is not guaranteed).\n const roots = data.getEntityComments.items.filter((c) => !c.parentCommentId);\n roots.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());\n return { items: roots, total: data.getEntityComments.total };\n },\n });\n};\n\nexport const useEcoCreateComment = (entityType: string, entityId: string) => {\n const { apiGatewayUrl } = useBuildmyiq();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (text: string) => {\n const data = await wspaceFetch<{ createEntityComment: { id: string } }>(\n `mutation BuiCreateEntityComment($input: CreateEntityCommentInput!) {\n createEntityComment(input: $input) { id }\n }`,\n {\n input: {\n entityType,\n entityId,\n content: { text },\n },\n },\n apiGatewayUrl\n );\n return data.createEntityComment;\n },\n onSuccess: () => {\n qc.invalidateQueries({ queryKey: commentsKey(entityType, entityId) });\n },\n });\n};\n"],"mappings":";;;;;AAkCA,IAAM,KAAe,GAAoB,MACvC;CAAC;CAAa;CAAY;CAAY;AAAQ,GAGnC,KAAkB,GAAoB,MAAiC;CAClF,IAAM,EAAE,qBAAkB,EAAa;CACvC,OAAO,EAAS;EACd,UAAU,EAAY,GAAY,KAAY,EAAE;EAChD,SAAS,EAAQ;EACjB,SAAS,YAAY;GACnB,IAAM,IAAO,MAAM,EAGjB,6YAeA,EACE,OAAO;IACL;IACA;IACA,MAAM;IACN,iBAAiB;GACnB,EACF,GACA,CACF,GAEM,IAAQ,EAAK,kBAAkB,MAAM,QAAQ,MAAM,CAAC,EAAE,eAAe;GAE3E,OADA,EAAM,MAAM,GAAG,MAAM,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ,IAAI,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ,CAAC,GAC/E;IAAE,OAAO;IAAO,OAAO,EAAK,kBAAkB;GAAM;EAC7D;CACF,CAAC;AACH,GAEa,KAAuB,GAAoB,MAAqB;CAC3E,IAAM,EAAE,qBAAkB,EAAa,GACjC,IAAK,EAAe;CAC1B,OAAO,EAAY;EACjB,YAAY,OAAO,OAcV,MAbY,EACjB,wIAGA,EACE,OAAO;GACL;GACA;GACA,SAAS,EAAE,QAAK;EAClB,EACF,GACA,CACF,GACY;EAEd,iBAAiB;GACf,EAAG,kBAAkB,EAAE,UAAU,EAAY,GAAY,CAAQ,EAAE,CAAC;EACtE;CACF,CAAC;AACH"}
@@ -1 +1 @@
1
- {"version":3,"file":"useBuiFiles.js","names":[],"sources":["../../../src/buildmyiq/hooks/useBuiFiles.ts"],"sourcesContent":["/**\n * Buildmyiq — wspace-files-svc REUSE hooks (Integration A).\n * @module buildmyiq/hooks/useBuiFiles\n *\n * Reuses the platform file store (`wspace-files-svc`) WITHOUT any buildmyiq-svc\n * change: the buildmyiq schema already exposes `BuiLesson.mediaFileId` /\n * `BuiCertificate.fileId`; this module lets the UI actually produce a `fileId` via\n * the canonical 3-step upload flow composed into the WORKSPACE supergraph:\n * 1. `initiateUpload(input)` → `{ uploadUrl, session { id } }`\n * 2. HTTP `PUT` the bytes to the returned signed `uploadUrl`\n * 3. `completeUpload(input: { sessionId })` → `{ fileId }`\n * plus `getFile(id)` to resolve an existing evidence/report file's name + URL.\n *\n * Transport mirrors `useBuildmyiqApi` exactly — `graphqlFetch({ gateway:\n * 'workspace', workspaceToken: true })` — so no ApolloProvider is required.\n * These hooks live in a SEPARATE file by design; `useBuildmyiqApi.ts` is\n * left untouched.\n */\n\nimport { useCallback, useState } from 'react';\nimport { useQuery } from '@tanstack/react-query';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\n\nimport { useBuildmyiq } from '../context';\n\n// ============================================================================\n// Shared workspace-gateway transport (reused by the comments + tags hooks too)\n// ============================================================================\n\ntype GraphqlReadCacheGlobals = {\n __graphqlFetchResponseCache?: { clear: () => void };\n __graphqlFetchPendingResponses?: { clear: () => void };\n __boffGraphQLResponseCache?: { clear: () => void };\n};\n\n/**\n * fe-libs `graphqlFetch` coalesces `query` responses in a short-lived in-memory\n * cache. After a write we drop those caches so the invalidation refetch that\n * follows reads fresh data (same remedy `useBuildmyiqApi` applies).\n */\nconst clearGraphqlReadCaches = (): void => {\n const g = globalThis as GraphqlReadCacheGlobals;\n g.__graphqlFetchResponseCache?.clear();\n g.__graphqlFetchPendingResponses?.clear();\n g.__boffGraphQLResponseCache?.clear();\n};\n\n/** Execute a workspace-gateway GraphQL operation with the workspace token. */\nexport const wspaceFetch = async <TData>(\n query: string,\n variables: Record<string, unknown>,\n baseUrl?: string\n): Promise<TData> => {\n const response = await graphqlFetch<TData>({\n gateway: 'workspace',\n baseUrl,\n query,\n variables,\n workspaceToken: true,\n });\n\n if (response.errors?.length) {\n throw new Error(response.errors[0]?.message ?? 'GraphQL request failed');\n }\n if (!response.data) {\n throw new Error('GraphQL response did not include data');\n }\n if (/^\\s*mutation\\b/.test(query)) {\n clearGraphqlReadCaches();\n }\n return response.data;\n};\n\n// ============================================================================\n// File upload (initiate → PUT → complete)\n// ============================================================================\n\nexport interface UploadedBuiFile {\n fileId: string;\n fileName: string;\n}\n\n/** `PUT` the blob to the signed URL, reporting byte-progress via `onProgress`. */\nfunction putWithProgress(\n url: string,\n file: File,\n contentType: string,\n onProgress: (percent: number) => void\n): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open('PUT', url);\n xhr.setRequestHeader('Content-Type', contentType);\n xhr.upload.onprogress = (event) => {\n if (event.lengthComputable) {\n onProgress(Math.round((event.loaded / event.total) * 100));\n }\n };\n xhr.onload = () => {\n if (xhr.status >= 200 && xhr.status < 300) {\n resolve();\n } else {\n reject(new Error(`File storage rejected the upload (HTTP ${xhr.status}).`));\n }\n };\n xhr.onerror = () => reject(new Error('Network error while uploading the file.'));\n xhr.onabort = () => reject(new Error('The upload was cancelled.'));\n xhr.send(file);\n });\n}\n\nexport interface UseBuiFileUploadResult {\n upload: (file: File) => Promise<UploadedBuiFile>;\n isUploading: boolean;\n /** 0–100 byte-progress of the signed-URL PUT. */\n progress: number;\n error: string | null;\n reset: () => void;\n}\n\n/**\n * Imperative upload hook: `await upload(file)` runs the platform 3-step flow and\n * resolves to `{ fileId, fileName }`. `fileId` is what the buildmyiq mutations accept\n * (`buiCreateCourse.coverFileId`, `buiIssueCertificate.fileId`).\n */\nexport const useBuiFileUpload = (): UseBuiFileUploadResult => {\n const { apiGatewayUrl } = useBuildmyiq();\n const [isUploading, setIsUploading] = useState(false);\n const [progress, setProgress] = useState(0);\n const [error, setError] = useState<string | null>(null);\n\n const reset = useCallback(() => {\n setIsUploading(false);\n setProgress(0);\n setError(null);\n }, []);\n\n const upload = useCallback(\n async (file: File): Promise<UploadedBuiFile> => {\n setIsUploading(true);\n setProgress(0);\n setError(null);\n try {\n const dotIndex = file.name.lastIndexOf('.');\n const extension = dotIndex > -1 ? file.name.slice(dotIndex + 1) : '';\n const mimeType = file.type || 'application/octet-stream';\n\n // 1. initiate\n const initiated = await wspaceFetch<{\n initiateUpload: { uploadUrl: string | null; session: { id: string } };\n }>(\n `mutation BuiInitiateUpload($input: InitiateUploadInput!) {\n initiateUpload(input: $input) {\n uploadUrl\n session { id }\n }\n }`,\n {\n input: {\n fileName: file.name,\n extension,\n mimeType,\n fileSize: String(file.size),\n visibility: 'WORKSPACE',\n },\n },\n apiGatewayUrl\n );\n\n const { uploadUrl } = initiated.initiateUpload;\n const sessionId = initiated.initiateUpload.session.id;\n if (!uploadUrl) {\n throw new Error('The file service did not return an upload URL.');\n }\n\n // 2. PUT the bytes\n await putWithProgress(uploadUrl, file, mimeType, setProgress);\n\n // 3. complete\n const completed = await wspaceFetch<{\n completeUpload: { fileId: string; file: { id: string; name: string } | null };\n }>(\n `mutation BuiCompleteUpload($input: CompleteUploadInput!) {\n completeUpload(input: $input) {\n fileId\n file { id name }\n }\n }`,\n { input: { sessionId } },\n apiGatewayUrl\n );\n\n setProgress(100);\n return {\n fileId: completed.completeUpload.fileId,\n fileName: completed.completeUpload.file?.name ?? file.name,\n };\n } catch (err) {\n const message = err instanceof Error ? err.message : 'The file could not be uploaded.';\n setError(message);\n throw err instanceof Error ? err : new Error(message);\n } finally {\n setIsUploading(false);\n }\n },\n [apiGatewayUrl]\n );\n\n return { upload, isUploading, progress, error, reset };\n};\n\n// ============================================================================\n// Resolve an existing file (name + download URL) for rendering evidence links\n// ============================================================================\n\nexport interface BuiFileMeta {\n id: string;\n name: string;\n downloadUrl: string | null;\n mimeType: string;\n size: string;\n}\n\nexport const useEcoFile = (id: string | null | undefined) => {\n const { apiGatewayUrl } = useBuildmyiq();\n return useQuery({\n queryKey: ['buildmyiq', 'file', id ?? ''],\n enabled: Boolean(id),\n queryFn: async () => {\n const data = await wspaceFetch<{ getFile: BuiFileMeta | null }>(\n `query BuiGetFile($id: ID!) {\n getFile(id: $id) {\n id\n name\n downloadUrl\n mimeType\n size\n }\n }`,\n { id },\n apiGatewayUrl\n );\n return data.getFile;\n },\n });\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"useBuiFiles.js","names":[],"sources":["../../../src/buildmyiq/hooks/useBuiFiles.ts"],"sourcesContent":["/**\n * Buildmyiq — wspace-files-svc REUSE hooks (Integration A).\n * @module buildmyiq/hooks/useBuiFiles\n *\n * Reuses the platform file store (`wspace-files-svc`) WITHOUT any buildmyiq-svc\n * change: the buildmyiq schema already exposes `BuiLesson.mediaFileId` /\n * `BuiCertificate.fileId`; this module lets the UI actually produce a `fileId` via\n * the canonical 3-step upload flow composed into the WORKSPACE supergraph:\n * 1. `initiateUpload(input)` → `{ uploadUrl, session { id } }`\n * 2. HTTP `PUT` the bytes to the returned signed `uploadUrl`\n * 3. `completeUpload(input: { sessionId })` → `{ fileId }`\n * plus `getFile(id)` to resolve an existing evidence/report file's name + URL.\n *\n * Transport mirrors `useBuildmyiqApi` exactly — `graphqlFetch({ gateway:\n * 'workspace', workspaceToken: true })` — so no ApolloProvider is required.\n * These hooks live in a SEPARATE file by design; `useBuildmyiqApi.ts` is\n * left untouched.\n */\n\nimport { useCallback, useState } from 'react';\nimport { useQuery } from '@tanstack/react-query';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\n\nimport { useBuildmyiq } from '../context';\n\n// ============================================================================\n// Shared workspace-gateway transport (reused by the comments + tags hooks too)\n// ============================================================================\n\ntype GraphqlReadCacheGlobals = {\n __graphqlFetchResponseCache?: { clear: () => void };\n __graphqlFetchPendingResponses?: { clear: () => void };\n __boffGraphQLResponseCache?: { clear: () => void };\n};\n\n/**\n * fe-libs `graphqlFetch` coalesces `query` responses in a short-lived in-memory\n * cache. After a write we drop those caches so the invalidation refetch that\n * follows reads fresh data (same remedy `useBuildmyiqApi` applies).\n */\nconst clearGraphqlReadCaches = (): void => {\n const g = globalThis as GraphqlReadCacheGlobals;\n g.__graphqlFetchResponseCache?.clear();\n g.__graphqlFetchPendingResponses?.clear();\n g.__boffGraphQLResponseCache?.clear();\n};\n\n/** Execute a workspace-gateway GraphQL operation with the workspace token. */\nexport const wspaceFetch = async <TData>(\n query: string,\n variables: Record<string, unknown>,\n baseUrl?: string\n): Promise<TData> => {\n const response = await graphqlFetch<TData>({\n gateway: 'workspace',\n baseUrl,\n query,\n variables,\n workspaceToken: true,\n });\n\n if (response.errors?.length) {\n throw new Error(response.errors[0]?.message ?? 'GraphQL request failed');\n }\n if (!response.data) {\n throw new Error('GraphQL response did not include data');\n }\n if (/^\\s*mutation\\b/.test(query)) {\n clearGraphqlReadCaches();\n }\n return response.data;\n};\n\n// ============================================================================\n// File upload (initiate → PUT → complete)\n// ============================================================================\n\nexport interface UploadedBuiFile {\n fileId: string;\n fileName: string;\n}\n\n/** `PUT` the blob to the signed URL, reporting byte-progress via `onProgress`. */\nfunction putWithProgress(\n url: string,\n file: File,\n contentType: string,\n onProgress: (percent: number) => void\n): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open('PUT', url);\n xhr.setRequestHeader('Content-Type', contentType);\n xhr.upload.onprogress = (event) => {\n if (event.lengthComputable) {\n onProgress(Math.round((event.loaded / event.total) * 100));\n }\n };\n xhr.onload = () => {\n if (xhr.status >= 200 && xhr.status < 300) {\n resolve();\n } else {\n reject(new Error(`File storage rejected the upload (HTTP ${xhr.status}).`));\n }\n };\n xhr.onerror = () => reject(new Error('Network error while uploading the file.'));\n xhr.onabort = () => reject(new Error('The upload was cancelled.'));\n xhr.send(file);\n });\n}\n\nexport interface UseBuiFileUploadResult {\n upload: (file: File) => Promise<UploadedBuiFile>;\n isUploading: boolean;\n /** 0–100 byte-progress of the signed-URL PUT. */\n progress: number;\n error: string | null;\n reset: () => void;\n}\n\n/**\n * Imperative upload hook: `await upload(file)` runs the platform 3-step flow and\n * resolves to `{ fileId, fileName }`. `fileId` is what the buildmyiq mutations accept\n * (`buiCreateCourse.coverFileId`, `buiIssueCertificate.fileId`).\n */\nexport const useBuiFileUpload = (): UseBuiFileUploadResult => {\n const { apiGatewayUrl } = useBuildmyiq();\n const [isUploading, setIsUploading] = useState(false);\n const [progress, setProgress] = useState(0);\n const [error, setError] = useState<string | null>(null);\n\n const reset = useCallback(() => {\n setIsUploading(false);\n setProgress(0);\n setError(null);\n }, []);\n\n const upload = useCallback(\n async (file: File): Promise<UploadedBuiFile> => {\n setIsUploading(true);\n setProgress(0);\n setError(null);\n try {\n const dotIndex = file.name.lastIndexOf('.');\n const extension = dotIndex > -1 ? file.name.slice(dotIndex + 1) : '';\n const mimeType = file.type || 'application/octet-stream';\n\n // 1. initiate\n const initiated = await wspaceFetch<{\n initiateUpload: { uploadUrl: string | null; session: { id: string } };\n }>(\n `mutation BuiInitiateUpload($input: InitiateUploadInput!) {\n initiateUpload(input: $input) {\n uploadUrl\n session { id }\n }\n }`,\n {\n input: {\n fileName: file.name,\n extension,\n mimeType,\n fileSize: String(file.size),\n visibility: 'WORKSPACE',\n },\n },\n apiGatewayUrl\n );\n\n const { uploadUrl } = initiated.initiateUpload;\n const sessionId = initiated.initiateUpload.session.id;\n if (!uploadUrl) {\n throw new Error('The file service did not return an upload URL.');\n }\n\n // 2. PUT the bytes\n await putWithProgress(uploadUrl, file, mimeType, setProgress);\n\n // 3. complete\n const completed = await wspaceFetch<{\n completeUpload: { fileId: string; file: { id: string; name: string } | null };\n }>(\n `mutation BuiCompleteUpload($input: CompleteUploadInput!) {\n completeUpload(input: $input) {\n fileId\n file { id name }\n }\n }`,\n { input: { sessionId } },\n apiGatewayUrl\n );\n\n setProgress(100);\n return {\n fileId: completed.completeUpload.fileId,\n fileName: completed.completeUpload.file?.name ?? file.name,\n };\n } catch (err) {\n const message = err instanceof Error ? err.message : 'The file could not be uploaded.';\n setError(message);\n throw err instanceof Error ? err : new Error(message);\n } finally {\n setIsUploading(false);\n }\n },\n [apiGatewayUrl]\n );\n\n return { upload, isUploading, progress, error, reset };\n};\n\n// ============================================================================\n// Resolve an existing file (name + download URL) for rendering evidence links\n// ============================================================================\n\nexport interface BuiFileMeta {\n id: string;\n name: string;\n downloadUrl: string | null;\n mimeType: string;\n size: string;\n}\n\nexport const useEcoFile = (id: string | null | undefined) => {\n const { apiGatewayUrl } = useBuildmyiq();\n return useQuery({\n queryKey: ['buildmyiq', 'file', id ?? ''],\n enabled: Boolean(id),\n queryFn: async () => {\n const data = await wspaceFetch<{ getFile: BuiFileMeta | null }>(\n `query BuiGetFile($id: ID!) {\n getFile(id: $id) {\n id\n name\n downloadUrl\n mimeType\n size\n }\n }`,\n { id },\n apiGatewayUrl\n );\n return data.getFile;\n },\n });\n};\n"],"mappings":"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"}