@burdenoff/microfe-cosmicintersection 2026.713.1 → 2026.720.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/cosmicintersection/CosmicIntersectionRoot.js.map +1 -1
  2. package/dist/cosmicintersection/CosmicIntersectionRoutes.js.map +1 -1
  3. package/dist/cosmicintersection/api/cosmicApi.d.ts.map +1 -1
  4. package/dist/cosmicintersection/api/cosmicApi.js.map +1 -1
  5. package/dist/cosmicintersection/components/ActionMenu.js.map +1 -1
  6. package/dist/cosmicintersection/components/ActivityFeedPanel.js.map +1 -1
  7. package/dist/cosmicintersection/components/DiscussionPanel.js.map +1 -1
  8. package/dist/cosmicintersection/components/EmptyState.js.map +1 -1
  9. package/dist/cosmicintersection/components/EntityFormShell.js.map +1 -1
  10. package/dist/cosmicintersection/components/EntityTagsField.js.map +1 -1
  11. package/dist/cosmicintersection/components/ErrorState.js.map +1 -1
  12. package/dist/cosmicintersection/components/FacetPanel.js.map +1 -1
  13. package/dist/cosmicintersection/components/FileUploadField.js.map +1 -1
  14. package/dist/cosmicintersection/components/LineageGraph.d.ts.map +1 -1
  15. package/dist/cosmicintersection/components/LineageGraph.js.map +1 -1
  16. package/dist/cosmicintersection/components/LoadingState.js.map +1 -1
  17. package/dist/cosmicintersection/components/PageLayout.js.map +1 -1
  18. package/dist/cosmicintersection/components/Pagination.js.map +1 -1
  19. package/dist/cosmicintersection/components/ResultsChart.js.map +1 -1
  20. package/dist/cosmicintersection/components/SafeLink.js.map +1 -1
  21. package/dist/cosmicintersection/components/StatusBadge.js.map +1 -1
  22. package/dist/cosmicintersection/components/StepDAG.js.map +1 -1
  23. package/dist/cosmicintersection/context/CosmicIntersectionContext.js.map +1 -1
  24. package/dist/cosmicintersection/lib/mutationResult.js.map +1 -1
  25. package/dist/cosmicintersection/lib/toast.js.map +1 -1
  26. package/dist/cosmicintersection/pages/access/AccessPage.js.map +1 -1
  27. package/dist/cosmicintersection/pages/analyses/AnalysesPage.js.map +1 -1
  28. package/dist/cosmicintersection/pages/collections/CollectionDetailPage.js.map +1 -1
  29. package/dist/cosmicintersection/pages/collections/CollectionFormPage.js.map +1 -1
  30. package/dist/cosmicintersection/pages/collections/CollectionsPage.js.map +1 -1
  31. package/dist/cosmicintersection/pages/dashboard/DashboardPage.js.map +1 -1
  32. package/dist/cosmicintersection/pages/datasets/DatasetDetailPage.js.map +1 -1
  33. package/dist/cosmicintersection/pages/datasets/DatasetFormPage.js.map +1 -1
  34. package/dist/cosmicintersection/pages/datasets/DatasetsPage.js.map +1 -1
  35. package/dist/cosmicintersection/pages/discovery/DiscoveryPage.js.map +1 -1
  36. package/dist/cosmicintersection/pages/lineage/LineagePage.js.map +1 -1
  37. package/dist/cosmicintersection/pages/notebooks/NotebookDetailPage.js.map +1 -1
  38. package/dist/cosmicintersection/pages/notebooks/NotebookFormPage.js.map +1 -1
  39. package/dist/cosmicintersection/pages/notebooks/NotebooksPage.js.map +1 -1
  40. package/dist/cosmicintersection/pages/pipelines/PipelineDetailPage.js.map +1 -1
  41. package/dist/cosmicintersection/pages/pipelines/PipelineFormPage.js.map +1 -1
  42. package/dist/cosmicintersection/pages/pipelines/PipelinesPage.js.map +1 -1
  43. package/dist/cosmicintersection/pages/releases/ReleaseFormPage.js.map +1 -1
  44. package/dist/cosmicintersection/pages/releases/ReleasesPage.js.map +1 -1
  45. package/dist/cosmicintersection/pages/runs/RunDetailPage.js.map +1 -1
  46. package/dist/cosmicintersection/pages/runs/RunsPage.js.map +1 -1
  47. package/dist/cosmicintersection/pages/sources/SourceDetailPage.js.map +1 -1
  48. package/dist/cosmicintersection/pages/sources/SourceFormPage.js.map +1 -1
  49. package/dist/cosmicintersection/pages/sources/SourcesPage.js.map +1 -1
  50. package/dist/cosmicintersection/utils/cn.js.map +1 -1
  51. package/dist/node_modules/clsx/dist/clsx.js.map +1 -1
  52. package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js +1 -1
  53. package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -1
  54. package/dist/services/createSandboxAssistantTransport.js.map +1 -1
  55. package/dist/stories/EmptyState.stories.d.ts +9 -0
  56. package/dist/stories/EmptyState.stories.d.ts.map +1 -0
  57. package/dist/stories/LoadingState.stories.d.ts +9 -0
  58. package/dist/stories/LoadingState.stories.d.ts.map +1 -0
  59. package/dist/stories/StatusBadge.stories.d.ts +12 -0
  60. package/dist/stories/StatusBadge.stories.d.ts.map +1 -0
  61. package/dist/utils/pageContext.js.map +1 -1
  62. package/package.json +14 -3
@@ -1 +1 @@
1
- {"version":3,"file":"EntityTagsField.js","names":[],"sources":["../../../src/cosmicintersection/components/EntityTagsField.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { Plus, Tag as TagIcon, X } from 'lucide-react';\nimport { Badge, Button, Input } from '@burdenoff/fe-libs/ui';\n\nexport interface EntityTagsFieldProps {\n /** Current tags (the `tags: [String!]` inline array on the Cos entity). */\n value: string[];\n /** Called with the next tag array whenever a chip is added or removed. */\n onChange: (tags: string[]) => void;\n label?: string;\n placeholder?: string;\n disabled?: boolean;\n className?: string;\n}\n\n/**\n * Controlled inline tag editor for the `tags: [String!]` arrays carried on every\n * Cos entity. This is intentionally a lightweight chips input — full workspace\n * tag management lives in the app-shell `/tags` route, NOT here — so it never\n * imports microfe-tags or hits a tag service. The parent form owns the array and\n * passes it straight into the entity's create/update input.\n */\nexport function EntityTagsField({\n value,\n onChange,\n label = 'Tags',\n placeholder = 'Add a tag and press Enter',\n disabled = false,\n className,\n}: EntityTagsFieldProps) {\n const [draft, setDraft] = useState('');\n\n const addTag = () => {\n const trimmed = draft.trim();\n if (!trimmed) {\n return;\n }\n // De-duplicate case-insensitively while preserving the user's casing.\n const exists = value.some((tag) => tag.toLowerCase() === trimmed.toLowerCase());\n if (!exists) {\n onChange([...value, trimmed]);\n }\n setDraft('');\n };\n\n const removeTag = (tag: string) => {\n onChange(value.filter((existing) => existing !== tag));\n };\n\n return (\n <div className={`space-y-2 ${className ?? ''}`}>\n <div className=\"flex items-center gap-2 text-sm font-medium text-text-primary\">\n <TagIcon className=\"size-4 text-text-secondary\" aria-hidden=\"true\" />\n <span>{label}</span>\n </div>\n\n <div className=\"flex flex-wrap gap-2\" role=\"list\" aria-label={label}>\n {value.length === 0 ? (\n <span className=\"text-sm text-text-secondary\">No tags yet.</span>\n ) : (\n value.map((tag) => (\n <Badge key={tag} variant=\"secondary\" className=\"gap-1.5\" role=\"listitem\">\n <span className=\"min-w-0 truncate\">{tag}</span>\n <button\n type=\"button\"\n onClick={() => removeTag(tag)}\n disabled={disabled}\n aria-label={`Remove ${tag}`}\n className=\"rounded-full text-text-secondary transition-colors hover:text-text-primary focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border-focus disabled:opacity-50\"\n >\n <X className=\"size-3\" aria-hidden=\"true\" />\n </button>\n </Badge>\n ))\n )}\n </div>\n\n <div className=\"flex items-center gap-2\">\n <Input\n value={draft}\n onChange={(event) => setDraft(event.target.value)}\n placeholder={placeholder}\n aria-label={`${label} input`}\n disabled={disabled}\n className=\"w-full max-w-xs\"\n onKeyDown={(event) => {\n if (event.key === 'Enter') {\n event.preventDefault();\n addTag();\n }\n }}\n />\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={addTag}\n disabled={disabled || !draft.trim()}\n >\n <Plus className=\"size-4\" />\n Add\n </Button>\n </div>\n </div>\n );\n}\n\nexport default EntityTagsField;\n"],"mappings":";;;;;AAsBA,SAAgB,EAAgB,EAC9B,UACA,aACA,WAAQ,QACR,iBAAc,6BACd,cAAW,IACX,gBACuB;CACvB,IAAM,CAAC,GAAO,KAAY,EAAS,GAAG,EAEhC,UAAe;EACnB,IAAM,IAAU,EAAM,MAAM;AACvB,QAIU,EAAM,MAAM,MAAQ,EAAI,aAAa,KAAK,EAAQ,aAAa,CAAC,IAE7E,EAAS,CAAC,GAAG,GAAO,EAAQ,CAAC,EAE/B,EAAS,GAAG;IAGR,KAAa,MAAgB;AACjC,IAAS,EAAM,QAAQ,MAAa,MAAa,EAAI,CAAC;;AAGxD,QACE,kBAAC,OAAD;EAAK,WAAW,aAAa,KAAa;YAA1C;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KAAS,WAAU;KAA6B,eAAY;KAAS,CAAA,EACrE,kBAAC,QAAD,EAAA,UAAO,GAAa,CAAA,CAChB;;GAEN,kBAAC,OAAD;IAAK,WAAU;IAAuB,MAAK;IAAO,cAAY;cAC3D,EAAM,WAAW,IAChB,kBAAC,QAAD;KAAM,WAAU;eAA8B;KAAmB,CAAA,GAEjE,EAAM,KAAK,MACT,kBAAC,GAAD;KAAiB,SAAQ;KAAY,WAAU;KAAU,MAAK;eAA9D,CACE,kBAAC,QAAD;MAAM,WAAU;gBAAoB;MAAW,CAAA,EAC/C,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAU,EAAI;MACnB;MACV,cAAY,UAAU;MACtB,WAAU;gBAEV,kBAAC,GAAD;OAAG,WAAU;OAAS,eAAY;OAAS,CAAA;MACpC,CAAA,CACH;OAXI,EAWJ,CACR;IAEA,CAAA;GAEN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KACE,OAAO;KACP,WAAW,MAAU,EAAS,EAAM,OAAO,MAAM;KACpC;KACb,cAAY,GAAG,EAAM;KACX;KACV,WAAU;KACV,YAAY,MAAU;AACpB,MAAI,EAAM,QAAQ,YAChB,EAAM,gBAAgB,EACtB,GAAQ;;KAGZ,CAAA,EACF,kBAAC,GAAD;KACE,MAAK;KACL,SAAQ;KACR,MAAK;KACL,SAAS;KACT,UAAU,KAAY,CAAC,EAAM,MAAM;eALrC,CAOE,kBAAC,GAAD,EAAM,WAAU,UAAW,CAAA,EAAA,MAEpB;OACL;;GACF"}
1
+ {"version":3,"file":"EntityTagsField.js","names":[],"sources":["../../../src/cosmicintersection/components/EntityTagsField.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { Plus, Tag as TagIcon, X } from 'lucide-react';\nimport { Badge, Button, Input } from '@burdenoff/fe-libs/ui';\n\nexport interface EntityTagsFieldProps {\n /** Current tags (the `tags: [String!]` inline array on the Cos entity). */\n value: string[];\n /** Called with the next tag array whenever a chip is added or removed. */\n onChange: (tags: string[]) => void;\n label?: string;\n placeholder?: string;\n disabled?: boolean;\n className?: string;\n}\n\n/**\n * Controlled inline tag editor for the `tags: [String!]` arrays carried on every\n * Cos entity. This is intentionally a lightweight chips input — full workspace\n * tag management lives in the app-shell `/tags` route, NOT here — so it never\n * imports microfe-tags or hits a tag service. The parent form owns the array and\n * passes it straight into the entity's create/update input.\n */\nexport function EntityTagsField({\n value,\n onChange,\n label = 'Tags',\n placeholder = 'Add a tag and press Enter',\n disabled = false,\n className,\n}: EntityTagsFieldProps) {\n const [draft, setDraft] = useState('');\n\n const addTag = () => {\n const trimmed = draft.trim();\n if (!trimmed) {\n return;\n }\n // De-duplicate case-insensitively while preserving the user's casing.\n const exists = value.some((tag) => tag.toLowerCase() === trimmed.toLowerCase());\n if (!exists) {\n onChange([...value, trimmed]);\n }\n setDraft('');\n };\n\n const removeTag = (tag: string) => {\n onChange(value.filter((existing) => existing !== tag));\n };\n\n return (\n <div className={`space-y-2 ${className ?? ''}`}>\n <div className=\"flex items-center gap-2 text-sm font-medium text-text-primary\">\n <TagIcon className=\"size-4 text-text-secondary\" aria-hidden=\"true\" />\n <span>{label}</span>\n </div>\n\n <div className=\"flex flex-wrap gap-2\" role=\"list\" aria-label={label}>\n {value.length === 0 ? (\n <span className=\"text-sm text-text-secondary\">No tags yet.</span>\n ) : (\n value.map((tag) => (\n <Badge key={tag} variant=\"secondary\" className=\"gap-1.5\" role=\"listitem\">\n <span className=\"min-w-0 truncate\">{tag}</span>\n <button\n type=\"button\"\n onClick={() => removeTag(tag)}\n disabled={disabled}\n aria-label={`Remove ${tag}`}\n className=\"rounded-full text-text-secondary transition-colors hover:text-text-primary focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border-focus disabled:opacity-50\"\n >\n <X className=\"size-3\" aria-hidden=\"true\" />\n </button>\n </Badge>\n ))\n )}\n </div>\n\n <div className=\"flex items-center gap-2\">\n <Input\n value={draft}\n onChange={(event) => setDraft(event.target.value)}\n placeholder={placeholder}\n aria-label={`${label} input`}\n disabled={disabled}\n className=\"w-full max-w-xs\"\n onKeyDown={(event) => {\n if (event.key === 'Enter') {\n event.preventDefault();\n addTag();\n }\n }}\n />\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={addTag}\n disabled={disabled || !draft.trim()}\n >\n <Plus className=\"size-4\" />\n Add\n </Button>\n </div>\n </div>\n );\n}\n\nexport default EntityTagsField;\n"],"mappings":";;;;;AAsBA,SAAgB,EAAgB,EAC9B,UACA,aACA,WAAQ,QACR,iBAAc,6BACd,cAAW,IACX,gBACuB;CACvB,IAAM,CAAC,GAAO,KAAY,EAAS,EAAE,GAE/B,UAAe;EACnB,IAAM,IAAU,EAAM,KAAK;EACtB,MAIU,EAAM,MAAM,MAAQ,EAAI,YAAY,MAAM,EAAQ,YAAY,CACxE,KACH,EAAS,CAAC,GAAG,GAAO,CAAO,CAAC,GAE9B,EAAS,EAAE;CACb,GAEM,KAAa,MAAgB;EACjC,EAAS,EAAM,QAAQ,MAAa,MAAa,CAAG,CAAC;CACvD;CAEA,OACE,kBAAC,OAAD;EAAK,WAAW,aAAa,KAAa;YAA1C;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KAAS,WAAU;KAA6B,eAAY;IAAQ,CAAA,GACpE,kBAAC,QAAD,EAAA,UAAO,EAAY,CAAA,CAChB;;GAEL,kBAAC,OAAD;IAAK,WAAU;IAAuB,MAAK;IAAO,cAAY;cAC3D,EAAM,WAAW,IAChB,kBAAC,QAAD;KAAM,WAAU;eAA8B;IAAkB,CAAA,IAEhE,EAAM,KAAK,MACT,kBAAC,GAAD;KAAiB,SAAQ;KAAY,WAAU;KAAU,MAAK;eAA9D,CACE,kBAAC,QAAD;MAAM,WAAU;gBAAoB;KAAU,CAAA,GAC9C,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAU,CAAG;MAClB;MACV,cAAY,UAAU;MACtB,WAAU;gBAEV,kBAAC,GAAD;OAAG,WAAU;OAAS,eAAY;MAAQ,CAAA;KACpC,CAAA,CACH;OAXK,CAWL,CACR;GAEA,CAAA;GAEL,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KACE,OAAO;KACP,WAAW,MAAU,EAAS,EAAM,OAAO,KAAK;KACnC;KACb,cAAY,GAAG,EAAM;KACX;KACV,WAAU;KACV,YAAY,MAAU;MACpB,AAAI,EAAM,QAAQ,YAChB,EAAM,eAAe,GACrB,EAAO;KAEX;IACD,CAAA,GACD,kBAAC,GAAD;KACE,MAAK;KACL,SAAQ;KACR,MAAK;KACL,SAAS;KACT,UAAU,KAAY,CAAC,EAAM,KAAK;eALpC,CAOE,kBAAC,GAAD,EAAM,WAAU,SAAU,CAAA,GAAC,KAErB;MACL;;EACF;;AAET"}
@@ -1 +1 @@
1
- {"version":3,"file":"ErrorState.js","names":[],"sources":["../../../src/cosmicintersection/components/ErrorState.tsx"],"sourcesContent":["import { AlertTriangle } from 'lucide-react';\nimport { Button } from '@burdenoff/fe-libs/ui';\n\nexport interface ErrorStateProps {\n title?: string;\n message?: string;\n onRetry?: () => void;\n retryLabel?: string;\n className?: string;\n}\n\n/**\n * Token-styled error surface for failed queries. Uses the semantic status-error\n * token family (never raw red) so it stays theme-correct and accessible.\n */\nexport function ErrorState({\n title = 'Something went wrong',\n message = 'We could not load this data. Please try again.',\n onRetry,\n retryLabel = 'Retry',\n className,\n}: ErrorStateProps) {\n return (\n <div\n role=\"alert\"\n className={`flex min-h-[200px] flex-col items-center justify-center gap-3 rounded-card border border-status-error-border bg-status-error-bg-subtle px-6 py-10 text-center ${className ?? ''}`}\n >\n <div className=\"flex size-12 items-center justify-center rounded-full bg-status-error-bg text-status-error-text\">\n <AlertTriangle className=\"size-6\" aria-hidden=\"true\" />\n </div>\n <div className=\"space-y-1\">\n <p className=\"text-sm font-semibold text-status-error-text\">{title}</p>\n <p className=\"mx-auto max-w-md text-sm text-text-secondary\">{message}</p>\n </div>\n {onRetry ? (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={onRetry}>\n {retryLabel}\n </Button>\n ) : null}\n </div>\n );\n}\n\nexport default ErrorState;\n"],"mappings":";;;;AAeA,SAAgB,EAAW,EACzB,WAAQ,wBACR,aAAU,kDACV,YACA,gBAAa,SACb,gBACkB;AAClB,QACE,kBAAC,OAAD;EACE,MAAK;EACL,WAAW,iKAAiK,KAAa;YAF3L;GAIE,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KAAe,WAAU;KAAS,eAAY;KAAS,CAAA;IACnD,CAAA;GACN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,KAAD;KAAG,WAAU;eAAgD;KAAU,CAAA,EACvE,kBAAC,KAAD;KAAG,WAAU;eAAgD;KAAY,CAAA,CACrE;;GACL,IACC,kBAAC,GAAD;IAAQ,MAAK;IAAS,SAAQ;IAAU,MAAK;IAAK,SAAS;cACxD;IACM,CAAA,GACP;GACA"}
1
+ {"version":3,"file":"ErrorState.js","names":[],"sources":["../../../src/cosmicintersection/components/ErrorState.tsx"],"sourcesContent":["import { AlertTriangle } from 'lucide-react';\nimport { Button } from '@burdenoff/fe-libs/ui';\n\nexport interface ErrorStateProps {\n title?: string;\n message?: string;\n onRetry?: () => void;\n retryLabel?: string;\n className?: string;\n}\n\n/**\n * Token-styled error surface for failed queries. Uses the semantic status-error\n * token family (never raw red) so it stays theme-correct and accessible.\n */\nexport function ErrorState({\n title = 'Something went wrong',\n message = 'We could not load this data. Please try again.',\n onRetry,\n retryLabel = 'Retry',\n className,\n}: ErrorStateProps) {\n return (\n <div\n role=\"alert\"\n className={`flex min-h-[200px] flex-col items-center justify-center gap-3 rounded-card border border-status-error-border bg-status-error-bg-subtle px-6 py-10 text-center ${className ?? ''}`}\n >\n <div className=\"flex size-12 items-center justify-center rounded-full bg-status-error-bg text-status-error-text\">\n <AlertTriangle className=\"size-6\" aria-hidden=\"true\" />\n </div>\n <div className=\"space-y-1\">\n <p className=\"text-sm font-semibold text-status-error-text\">{title}</p>\n <p className=\"mx-auto max-w-md text-sm text-text-secondary\">{message}</p>\n </div>\n {onRetry ? (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={onRetry}>\n {retryLabel}\n </Button>\n ) : null}\n </div>\n );\n}\n\nexport default ErrorState;\n"],"mappings":";;;;AAeA,SAAgB,EAAW,EACzB,WAAQ,wBACR,aAAU,kDACV,YACA,gBAAa,SACb,gBACkB;CAClB,OACE,kBAAC,OAAD;EACE,MAAK;EACL,WAAW,iKAAiK,KAAa;YAF3L;GAIE,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KAAe,WAAU;KAAS,eAAY;IAAQ,CAAA;GACnD,CAAA;GACL,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,KAAD;KAAG,WAAU;eAAgD;IAAS,CAAA,GACtE,kBAAC,KAAD;KAAG,WAAU;eAAgD;IAAW,CAAA,CACrE;;GACJ,IACC,kBAAC,GAAD;IAAQ,MAAK;IAAS,SAAQ;IAAU,MAAK;IAAK,SAAS;cACxD;GACK,CAAA,IACN;EACD;;AAET"}
@@ -1 +1 @@
1
- {"version":3,"file":"FacetPanel.js","names":[],"sources":["../../../src/cosmicintersection/components/FacetPanel.tsx"],"sourcesContent":["/**\n * FacetPanel — a grouped, multi-select facet filter with live result counts.\n *\n * Presentational only: the parent owns the selection state and computes the\n * per-option counts (typically by cross-filtering the current search pool). Each\n * option renders a checkbox, a label and a right-aligned tabular count. Styling is\n * token-only so it reads correctly in every theme. Zero-count options render\n * disabled + dimmed rather than disappearing, so the facet layout stays stable as\n * the user refines a search.\n *\n * Used by the DiscoveryPage (mockup 05) — Instrument / Access / Format /\n * Mission-Survey groups over the federated dataset catalog.\n */\nimport { Checkbox } from '@burdenoff/fe-libs/ui';\n\nimport { cn } from '../utils/cn';\n\nexport interface FacetOption {\n value: string;\n label: string;\n count: number;\n}\n\nexport interface FacetGroup {\n key: string;\n label: string;\n options: FacetOption[];\n}\n\nexport interface FacetPanelProps {\n groups: FacetGroup[];\n /** Selected values per group key. */\n selected: Record<string, string[]>;\n onToggle: (groupKey: string, value: string) => void;\n onClear?: () => void;\n className?: string;\n}\n\nexport function FacetPanel({ groups, selected, onToggle, onClear, className }: FacetPanelProps) {\n const totalSelected = Object.values(selected).reduce((sum, values) => sum + values.length, 0);\n\n return (\n <div className={cn('flex flex-col gap-6', className)}>\n <div className=\"flex items-center justify-between\">\n <span className=\"text-xs font-semibold uppercase tracking-wide text-text-secondary\">\n Facet\n </span>\n {onClear && totalSelected > 0 ? (\n <button\n type=\"button\"\n onClick={onClear}\n className=\"rounded-button text-xs font-medium text-text-link transition-colors hover:text-text-primary focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border-focus\"\n >\n Clear ({totalSelected})\n </button>\n ) : null}\n </div>\n\n {groups.map((group) => {\n const groupSelected = selected[group.key] ?? [];\n return (\n <div key={group.key} className=\"space-y-2\">\n <h3 className=\"text-sm font-semibold text-text-primary\">{group.label}</h3>\n {group.options.length === 0 ? (\n <p className=\"text-xs text-text-secondary\">No values yet</p>\n ) : (\n <ul className=\"space-y-0.5\">\n {group.options.map((option) => {\n const checked = groupSelected.includes(option.value);\n const disabled = option.count === 0 && !checked;\n return (\n <li key={option.value}>\n <label\n className={cn(\n 'flex cursor-pointer items-center gap-2 rounded-button px-2 py-1.5 text-sm transition-colors hover:bg-bg-sunken',\n disabled && 'cursor-not-allowed opacity-50 hover:bg-transparent'\n )}\n >\n <Checkbox\n checked={checked}\n disabled={disabled}\n onCheckedChange={() => onToggle(group.key, option.value)}\n aria-label={`${option.label} (${option.count})`}\n />\n <span className=\"min-w-0 flex-1 truncate text-text-primary\">\n {option.label}\n </span>\n <span className=\"shrink-0 tabular-nums text-xs text-text-secondary\">\n {option.count}\n </span>\n </label>\n </li>\n );\n })}\n </ul>\n )}\n </div>\n );\n })}\n </div>\n );\n}\n\nexport default FacetPanel;\n"],"mappings":";;;;AAsCA,SAAgB,EAAW,EAAE,WAAQ,aAAU,aAAU,YAAS,gBAA8B;CAC9F,IAAM,IAAgB,OAAO,OAAO,EAAS,CAAC,QAAQ,GAAK,MAAW,IAAM,EAAO,QAAQ,EAAE;AAE7F,QACE,kBAAC,OAAD;EAAK,WAAW,EAAG,uBAAuB,EAAU;YAApD,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,QAAD;IAAM,WAAU;cAAoE;IAE7E,CAAA,EACN,KAAW,IAAgB,IAC1B,kBAAC,UAAD;IACE,MAAK;IACL,SAAS;IACT,WAAU;cAHZ;KAIC;KACS;KAAc;KACf;QACP,KACA;MAEL,EAAO,KAAK,MAAU;GACrB,IAAM,IAAgB,EAAS,EAAM,QAAQ,EAAE;AAC/C,UACE,kBAAC,OAAD;IAAqB,WAAU;cAA/B,CACE,kBAAC,MAAD;KAAI,WAAU;eAA2C,EAAM;KAAW,CAAA,EACzE,EAAM,QAAQ,WAAW,IACxB,kBAAC,KAAD;KAAG,WAAU;eAA8B;KAAiB,CAAA,GAE5D,kBAAC,MAAD;KAAI,WAAU;eACX,EAAM,QAAQ,KAAK,MAAW;MAC7B,IAAM,IAAU,EAAc,SAAS,EAAO,MAAM,EAC9C,IAAW,EAAO,UAAU,KAAK,CAAC;AACxC,aACE,kBAAC,MAAD,EAAA,UACE,kBAAC,SAAD;OACE,WAAW,EACT,kHACA,KAAY,qDACb;iBAJH;QAME,kBAAC,GAAD;SACW;SACC;SACV,uBAAuB,EAAS,EAAM,KAAK,EAAO,MAAM;SACxD,cAAY,GAAG,EAAO,MAAM,IAAI,EAAO,MAAM;SAC7C,CAAA;QACF,kBAAC,QAAD;SAAM,WAAU;mBACb,EAAO;SACH,CAAA;QACP,kBAAC,QAAD;SAAM,WAAU;mBACb,EAAO;SACH,CAAA;QACD;UACL,EApBI,EAAO,MAoBX;OAEP;KACC,CAAA,CAEH;MAnCI,EAAM,IAmCV;IAER,CACE"}
1
+ {"version":3,"file":"FacetPanel.js","names":[],"sources":["../../../src/cosmicintersection/components/FacetPanel.tsx"],"sourcesContent":["/**\n * FacetPanel — a grouped, multi-select facet filter with live result counts.\n *\n * Presentational only: the parent owns the selection state and computes the\n * per-option counts (typically by cross-filtering the current search pool). Each\n * option renders a checkbox, a label and a right-aligned tabular count. Styling is\n * token-only so it reads correctly in every theme. Zero-count options render\n * disabled + dimmed rather than disappearing, so the facet layout stays stable as\n * the user refines a search.\n *\n * Used by the DiscoveryPage (mockup 05) — Instrument / Access / Format /\n * Mission-Survey groups over the federated dataset catalog.\n */\nimport { Checkbox } from '@burdenoff/fe-libs/ui';\n\nimport { cn } from '../utils/cn';\n\nexport interface FacetOption {\n value: string;\n label: string;\n count: number;\n}\n\nexport interface FacetGroup {\n key: string;\n label: string;\n options: FacetOption[];\n}\n\nexport interface FacetPanelProps {\n groups: FacetGroup[];\n /** Selected values per group key. */\n selected: Record<string, string[]>;\n onToggle: (groupKey: string, value: string) => void;\n onClear?: () => void;\n className?: string;\n}\n\nexport function FacetPanel({ groups, selected, onToggle, onClear, className }: FacetPanelProps) {\n const totalSelected = Object.values(selected).reduce((sum, values) => sum + values.length, 0);\n\n return (\n <div className={cn('flex flex-col gap-6', className)}>\n <div className=\"flex items-center justify-between\">\n <span className=\"text-xs font-semibold uppercase tracking-wide text-text-secondary\">\n Facet\n </span>\n {onClear && totalSelected > 0 ? (\n <button\n type=\"button\"\n onClick={onClear}\n className=\"rounded-button text-xs font-medium text-text-link transition-colors hover:text-text-primary focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border-focus\"\n >\n Clear ({totalSelected})\n </button>\n ) : null}\n </div>\n\n {groups.map((group) => {\n const groupSelected = selected[group.key] ?? [];\n return (\n <div key={group.key} className=\"space-y-2\">\n <h3 className=\"text-sm font-semibold text-text-primary\">{group.label}</h3>\n {group.options.length === 0 ? (\n <p className=\"text-xs text-text-secondary\">No values yet</p>\n ) : (\n <ul className=\"space-y-0.5\">\n {group.options.map((option) => {\n const checked = groupSelected.includes(option.value);\n const disabled = option.count === 0 && !checked;\n return (\n <li key={option.value}>\n <label\n className={cn(\n 'flex cursor-pointer items-center gap-2 rounded-button px-2 py-1.5 text-sm transition-colors hover:bg-bg-sunken',\n disabled && 'cursor-not-allowed opacity-50 hover:bg-transparent'\n )}\n >\n <Checkbox\n checked={checked}\n disabled={disabled}\n onCheckedChange={() => onToggle(group.key, option.value)}\n aria-label={`${option.label} (${option.count})`}\n />\n <span className=\"min-w-0 flex-1 truncate text-text-primary\">\n {option.label}\n </span>\n <span className=\"shrink-0 tabular-nums text-xs text-text-secondary\">\n {option.count}\n </span>\n </label>\n </li>\n );\n })}\n </ul>\n )}\n </div>\n );\n })}\n </div>\n );\n}\n\nexport default FacetPanel;\n"],"mappings":";;;;AAsCA,SAAgB,EAAW,EAAE,WAAQ,aAAU,aAAU,YAAS,gBAA8B;CAC9F,IAAM,IAAgB,OAAO,OAAO,CAAQ,EAAE,QAAQ,GAAK,MAAW,IAAM,EAAO,QAAQ,CAAC;CAE5F,OACE,kBAAC,OAAD;EAAK,WAAW,EAAG,uBAAuB,CAAS;YAAnD,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,QAAD;IAAM,WAAU;cAAoE;GAE9E,CAAA,GACL,KAAW,IAAgB,IAC1B,kBAAC,UAAD;IACE,MAAK;IACL,SAAS;IACT,WAAU;cAHZ;KAIC;KACS;KAAc;IAChB;QACN,IACD;MAEJ,EAAO,KAAK,MAAU;GACrB,IAAM,IAAgB,EAAS,EAAM,QAAQ,CAAC;GAC9C,OACE,kBAAC,OAAD;IAAqB,WAAU;cAA/B,CACE,kBAAC,MAAD;KAAI,WAAU;eAA2C,EAAM;IAAU,CAAA,GACxE,EAAM,QAAQ,WAAW,IACxB,kBAAC,KAAD;KAAG,WAAU;eAA8B;IAAgB,CAAA,IAE3D,kBAAC,MAAD;KAAI,WAAU;eACX,EAAM,QAAQ,KAAK,MAAW;MAC7B,IAAM,IAAU,EAAc,SAAS,EAAO,KAAK,GAC7C,IAAW,EAAO,UAAU,KAAK,CAAC;MACxC,OACE,kBAAC,MAAD,EAAA,UACE,kBAAC,SAAD;OACE,WAAW,EACT,kHACA,KAAY,oDACd;iBAJF;QAME,kBAAC,GAAD;SACW;SACC;SACV,uBAAuB,EAAS,EAAM,KAAK,EAAO,KAAK;SACvD,cAAY,GAAG,EAAO,MAAM,IAAI,EAAO,MAAM;QAC9C,CAAA;QACD,kBAAC,QAAD;SAAM,WAAU;mBACb,EAAO;QACJ,CAAA;QACN,kBAAC,QAAD;SAAM,WAAU;mBACb,EAAO;QACJ,CAAA;OACD;SACL,GApBK,EAAO,KAoBZ;KAER,CAAC;IACC,CAAA,CAEH;MAnCK,EAAM,GAmCX;EAET,CAAC,CACE;;AAET"}
@@ -1 +1 @@
1
- {"version":3,"file":"FileUploadField.js","names":[],"sources":["../../../src/cosmicintersection/components/FileUploadField.tsx"],"sourcesContent":["import { gql } from '@apollo/client';\nimport { useApolloClient, useQuery } from '@apollo/client/react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { Download, FileUp, Loader2, Paperclip, X } from 'lucide-react';\nimport { useId, useRef, useState } from 'react';\n\nimport { notify } from '../lib/toast';\n\n/**\n * Generic artifact uploader backed by wspace-files-svc.\n *\n * Dataset/notebook/analysis blobs are NOT stored by this MFE — they live in\n * wspace-files as `File` records, and the Cos entities only hold the returned\n * `fileId` (CosDataset.fileId / CosNotebook.fileId / cosCompleteAnalysis.outputFileId).\n * This control performs the shared 3-step presigned upload (initiateUpload → PUT\n * → completeUpload) inline — the same protocol microfe-files uses — and hands the\n * resulting `fileId` back via `onChange`. It never keeps a local blob store and\n * accepts any file type.\n */\n\n// initiateUpload returns a presigned PUT URL + the upload session id.\nconst INITIATE_UPLOAD = gql`\n mutation CosInitiateUpload($input: InitiateUploadInput!) {\n initiateUpload(input: $input) {\n session {\n id\n }\n uploadUrl\n }\n }\n`;\n\n// completeUpload finalizes the session and returns the committed File id.\nconst COMPLETE_UPLOAD = gql`\n mutation CosCompleteUpload($input: CompleteUploadInput!) {\n completeUpload(input: $input) {\n fileId\n file {\n id\n }\n }\n }\n`;\n\n// Presigned, time-limited download URL for a stored File — used to render a\n// \"Download\" affordance for an already-attached artifact.\nexport const GET_FILE_DOWNLOAD_URL = gql`\n query CosGetFileDownloadUrl($id: ID!, $expiresIn: Int) {\n getFileDownloadUrl(id: $id, expiresIn: $expiresIn)\n }\n`;\n\n// One hour — artifact links are resolved on demand when a detail page renders.\nconst DOWNLOAD_URL_TTL_SECONDS = 3_600;\n\ninterface InitiateUploadResponse {\n initiateUpload?: { session?: { id: string } | null; uploadUrl?: string | null } | null;\n}\n\ninterface CompleteUploadResponse {\n completeUpload?: { fileId?: string | null; file?: { id: string } | null } | null;\n}\n\ninterface DownloadUrlResponse {\n getFileDownloadUrl?: string | null;\n}\n\n/**\n * Resolve a presigned download URL for an attached File id. Skips when no id is\n * present. Detail pages use this to render a \"Download\" link for a dataset/\n * notebook/analysis artifact.\n */\nexport function useFileDownloadUrl(fileId: string | null | undefined) {\n return useQuery<DownloadUrlResponse, { id: string; expiresIn: number }>(GET_FILE_DOWNLOAD_URL, {\n variables: { id: fileId ?? '', expiresIn: DOWNLOAD_URL_TTL_SECONDS },\n skip: !fileId,\n fetchPolicy: 'no-cache',\n });\n}\n\nexport interface FileUploadFieldProps {\n /** Current attached wspace-files File id, if any. */\n value?: string | null;\n /** Called with the new File id once an upload completes, or '' when cleared. */\n onChange: (fileId: string) => void | Promise<void>;\n label: string;\n hint?: string;\n /** Optional wspace-files folder id; when omitted the file lands in the root. */\n folderId?: string;\n /** `accept` attribute for the file input; defaults to any file type. */\n accept?: string;\n disabled?: boolean;\n className?: string;\n}\n\nexport function FileUploadField({\n value,\n onChange,\n label,\n hint,\n folderId,\n accept,\n disabled = false,\n className,\n}: FileUploadFieldProps) {\n const client = useApolloClient();\n const inputRef = useRef<HTMLInputElement | null>(null);\n const inputId = useId();\n const [uploading, setUploading] = useState(false);\n const [fileName, setFileName] = useState<string | null>(null);\n\n const download = useFileDownloadUrl(uploading ? null : value);\n const downloadUrl = download.data?.getFileDownloadUrl ?? null;\n\n const handleSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0];\n // Reset the input so selecting the same file again re-triggers change.\n event.target.value = '';\n if (!file) {\n return;\n }\n\n setUploading(true);\n try {\n const extension = file.name.includes('.')\n ? file.name.substring(file.name.lastIndexOf('.') + 1)\n : '';\n const mimeType = file.type || 'application/octet-stream';\n\n const initiate = await client.mutate<InitiateUploadResponse>({\n mutation: INITIATE_UPLOAD,\n variables: {\n input: {\n fileName: file.name,\n fileSize: String(file.size),\n extension,\n mimeType,\n ...(folderId ? { folderId } : {}),\n },\n },\n });\n const session = initiate.data?.initiateUpload?.session;\n const uploadUrl = initiate.data?.initiateUpload?.uploadUrl;\n if (!session || !uploadUrl) {\n throw new Error(initiate.error?.message ?? 'Could not start the upload.');\n }\n\n await new Promise<void>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.addEventListener('load', () => {\n if (xhr.status >= 200 && xhr.status < 300) {\n resolve();\n } else {\n reject(new Error(`Upload failed with status ${xhr.status}`));\n }\n });\n xhr.addEventListener('error', () => reject(new Error('Upload network error')));\n xhr.addEventListener('abort', () => reject(new Error('Upload aborted')));\n xhr.open('PUT', uploadUrl);\n xhr.setRequestHeader('Content-Type', mimeType);\n xhr.send(file);\n });\n\n const complete = await client.mutate<CompleteUploadResponse>({\n mutation: COMPLETE_UPLOAD,\n variables: {\n input: {\n sessionId: session.id,\n ...(folderId ? { folderId } : {}),\n },\n },\n });\n const uploadedId =\n complete.data?.completeUpload?.fileId ?? complete.data?.completeUpload?.file?.id;\n if (!uploadedId) {\n throw new Error(complete.error?.message ?? 'The file was uploaded but not confirmed.');\n }\n\n setFileName(file.name);\n await onChange(uploadedId);\n notify.success('File uploaded');\n } catch (error) {\n notify.error('Upload failed', error instanceof Error ? error.message : 'Please try again.');\n } finally {\n setUploading(false);\n }\n };\n\n const clear = async () => {\n setFileName(null);\n await onChange('');\n };\n\n return (\n <div className={className}>\n <div className=\"flex flex-wrap items-center gap-3\">\n <div className=\"flex size-12 shrink-0 items-center justify-center rounded-card border border-dashed border-border-default bg-bg-sunken text-text-secondary\">\n <Paperclip className=\"size-5\" aria-hidden=\"true\" />\n </div>\n <div className=\"space-y-1\">\n <div className=\"flex items-center gap-2\">\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n disabled={disabled || uploading}\n onClick={() => inputRef.current?.click()}\n >\n {uploading ? (\n <Loader2 className=\"size-4 animate-spin\" />\n ) : (\n <FileUp className=\"size-4\" />\n )}\n {uploading ? 'Uploading…' : value ? 'Replace file' : label}\n </Button>\n {value && !uploading ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => void clear()}\n disabled={disabled}\n >\n <X className=\"size-4\" />\n Remove\n </Button>\n ) : null}\n </div>\n {value ? (\n <div className=\"flex flex-wrap items-center gap-2 text-xs text-text-secondary\">\n <span className=\"max-w-[16rem] truncate\">{fileName ?? `File ${value}`}</span>\n {downloadUrl ? (\n <a\n href={downloadUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1 text-text-primary underline-offset-2 hover:underline\"\n >\n <Download className=\"size-3.5\" aria-hidden=\"true\" />\n Download\n </a>\n ) : null}\n </div>\n ) : hint ? (\n <p className=\"text-xs text-text-secondary\">{hint}</p>\n ) : null}\n </div>\n </div>\n <input\n ref={inputRef}\n id={inputId}\n type=\"file\"\n accept={accept}\n className=\"sr-only\"\n aria-label={label}\n onChange={(event) => void handleSelect(event)}\n />\n </div>\n );\n}\n\nexport default FileUploadField;\n"],"mappings":"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"}
1
+ {"version":3,"file":"FileUploadField.js","names":[],"sources":["../../../src/cosmicintersection/components/FileUploadField.tsx"],"sourcesContent":["import { gql } from '@apollo/client';\nimport { useApolloClient, useQuery } from '@apollo/client/react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { Download, FileUp, Loader2, Paperclip, X } from 'lucide-react';\nimport { useId, useRef, useState } from 'react';\n\nimport { notify } from '../lib/toast';\n\n/**\n * Generic artifact uploader backed by wspace-files-svc.\n *\n * Dataset/notebook/analysis blobs are NOT stored by this MFE — they live in\n * wspace-files as `File` records, and the Cos entities only hold the returned\n * `fileId` (CosDataset.fileId / CosNotebook.fileId / cosCompleteAnalysis.outputFileId).\n * This control performs the shared 3-step presigned upload (initiateUpload → PUT\n * → completeUpload) inline — the same protocol microfe-files uses — and hands the\n * resulting `fileId` back via `onChange`. It never keeps a local blob store and\n * accepts any file type.\n */\n\n// initiateUpload returns a presigned PUT URL + the upload session id.\nconst INITIATE_UPLOAD = gql`\n mutation CosInitiateUpload($input: InitiateUploadInput!) {\n initiateUpload(input: $input) {\n session {\n id\n }\n uploadUrl\n }\n }\n`;\n\n// completeUpload finalizes the session and returns the committed File id.\nconst COMPLETE_UPLOAD = gql`\n mutation CosCompleteUpload($input: CompleteUploadInput!) {\n completeUpload(input: $input) {\n fileId\n file {\n id\n }\n }\n }\n`;\n\n// Presigned, time-limited download URL for a stored File — used to render a\n// \"Download\" affordance for an already-attached artifact.\nexport const GET_FILE_DOWNLOAD_URL = gql`\n query CosGetFileDownloadUrl($id: ID!, $expiresIn: Int) {\n getFileDownloadUrl(id: $id, expiresIn: $expiresIn)\n }\n`;\n\n// One hour — artifact links are resolved on demand when a detail page renders.\nconst DOWNLOAD_URL_TTL_SECONDS = 3_600;\n\ninterface InitiateUploadResponse {\n initiateUpload?: { session?: { id: string } | null; uploadUrl?: string | null } | null;\n}\n\ninterface CompleteUploadResponse {\n completeUpload?: { fileId?: string | null; file?: { id: string } | null } | null;\n}\n\ninterface DownloadUrlResponse {\n getFileDownloadUrl?: string | null;\n}\n\n/**\n * Resolve a presigned download URL for an attached File id. Skips when no id is\n * present. Detail pages use this to render a \"Download\" link for a dataset/\n * notebook/analysis artifact.\n */\nexport function useFileDownloadUrl(fileId: string | null | undefined) {\n return useQuery<DownloadUrlResponse, { id: string; expiresIn: number }>(GET_FILE_DOWNLOAD_URL, {\n variables: { id: fileId ?? '', expiresIn: DOWNLOAD_URL_TTL_SECONDS },\n skip: !fileId,\n fetchPolicy: 'no-cache',\n });\n}\n\nexport interface FileUploadFieldProps {\n /** Current attached wspace-files File id, if any. */\n value?: string | null;\n /** Called with the new File id once an upload completes, or '' when cleared. */\n onChange: (fileId: string) => void | Promise<void>;\n label: string;\n hint?: string;\n /** Optional wspace-files folder id; when omitted the file lands in the root. */\n folderId?: string;\n /** `accept` attribute for the file input; defaults to any file type. */\n accept?: string;\n disabled?: boolean;\n className?: string;\n}\n\nexport function FileUploadField({\n value,\n onChange,\n label,\n hint,\n folderId,\n accept,\n disabled = false,\n className,\n}: FileUploadFieldProps) {\n const client = useApolloClient();\n const inputRef = useRef<HTMLInputElement | null>(null);\n const inputId = useId();\n const [uploading, setUploading] = useState(false);\n const [fileName, setFileName] = useState<string | null>(null);\n\n const download = useFileDownloadUrl(uploading ? null : value);\n const downloadUrl = download.data?.getFileDownloadUrl ?? null;\n\n const handleSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0];\n // Reset the input so selecting the same file again re-triggers change.\n event.target.value = '';\n if (!file) {\n return;\n }\n\n setUploading(true);\n try {\n const extension = file.name.includes('.')\n ? file.name.substring(file.name.lastIndexOf('.') + 1)\n : '';\n const mimeType = file.type || 'application/octet-stream';\n\n const initiate = await client.mutate<InitiateUploadResponse>({\n mutation: INITIATE_UPLOAD,\n variables: {\n input: {\n fileName: file.name,\n fileSize: String(file.size),\n extension,\n mimeType,\n ...(folderId ? { folderId } : {}),\n },\n },\n });\n const session = initiate.data?.initiateUpload?.session;\n const uploadUrl = initiate.data?.initiateUpload?.uploadUrl;\n if (!session || !uploadUrl) {\n throw new Error(initiate.error?.message ?? 'Could not start the upload.');\n }\n\n await new Promise<void>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.addEventListener('load', () => {\n if (xhr.status >= 200 && xhr.status < 300) {\n resolve();\n } else {\n reject(new Error(`Upload failed with status ${xhr.status}`));\n }\n });\n xhr.addEventListener('error', () => reject(new Error('Upload network error')));\n xhr.addEventListener('abort', () => reject(new Error('Upload aborted')));\n xhr.open('PUT', uploadUrl);\n xhr.setRequestHeader('Content-Type', mimeType);\n xhr.send(file);\n });\n\n const complete = await client.mutate<CompleteUploadResponse>({\n mutation: COMPLETE_UPLOAD,\n variables: {\n input: {\n sessionId: session.id,\n ...(folderId ? { folderId } : {}),\n },\n },\n });\n const uploadedId =\n complete.data?.completeUpload?.fileId ?? complete.data?.completeUpload?.file?.id;\n if (!uploadedId) {\n throw new Error(complete.error?.message ?? 'The file was uploaded but not confirmed.');\n }\n\n setFileName(file.name);\n await onChange(uploadedId);\n notify.success('File uploaded');\n } catch (error) {\n notify.error('Upload failed', error instanceof Error ? error.message : 'Please try again.');\n } finally {\n setUploading(false);\n }\n };\n\n const clear = async () => {\n setFileName(null);\n await onChange('');\n };\n\n return (\n <div className={className}>\n <div className=\"flex flex-wrap items-center gap-3\">\n <div className=\"flex size-12 shrink-0 items-center justify-center rounded-card border border-dashed border-border-default bg-bg-sunken text-text-secondary\">\n <Paperclip className=\"size-5\" aria-hidden=\"true\" />\n </div>\n <div className=\"space-y-1\">\n <div className=\"flex items-center gap-2\">\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n disabled={disabled || uploading}\n onClick={() => inputRef.current?.click()}\n >\n {uploading ? (\n <Loader2 className=\"size-4 animate-spin\" />\n ) : (\n <FileUp className=\"size-4\" />\n )}\n {uploading ? 'Uploading…' : value ? 'Replace file' : label}\n </Button>\n {value && !uploading ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => void clear()}\n disabled={disabled}\n >\n <X className=\"size-4\" />\n Remove\n </Button>\n ) : null}\n </div>\n {value ? (\n <div className=\"flex flex-wrap items-center gap-2 text-xs text-text-secondary\">\n <span className=\"max-w-[16rem] truncate\">{fileName ?? `File ${value}`}</span>\n {downloadUrl ? (\n <a\n href={downloadUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1 text-text-primary underline-offset-2 hover:underline\"\n >\n <Download className=\"size-3.5\" aria-hidden=\"true\" />\n Download\n </a>\n ) : null}\n </div>\n ) : hint ? (\n <p className=\"text-xs text-text-secondary\">{hint}</p>\n ) : null}\n </div>\n </div>\n <input\n ref={inputRef}\n id={inputId}\n type=\"file\"\n accept={accept}\n className=\"sr-only\"\n aria-label={label}\n onChange={(event) => void handleSelect(event)}\n />\n </div>\n );\n}\n\nexport default FileUploadField;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"LineageGraph.d.ts","sourceRoot":"","sources":["../../../src/cosmicintersection/components/LineageGraph.tsx"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EACV,WAAW,EACX,aAAa,EACb,UAAU,EACV,WAAW,EACX,WAAW,EACX,cAAc,EACd,UAAU,EACV,SAAS,EACV,MAAM,kBAAkB,CAAC;AAO1B,MAAM,MAAM,eAAe,GACvB,QAAQ,GACR,YAAY,GACZ,SAAS,GACT,KAAK,GACL,UAAU,GACV,UAAU,GACV,SAAS,CAAC;AAEd,MAAM,WAAW,WAAW;IAC1B,wDAAwD;IACxD,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,eAAe,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wFAAwF;IACxF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,MAAM,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,KAAK,EAAE,WAAW,EAAE,CAAC;CACtB;AAED,kFAAkF;AAClF,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACtC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACpC,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAC5C,IAAI,EAAE,cAAc,EAAE,CAAC;IACvB,SAAS,EAAE,WAAW,EAAE,CAAC;IACzB,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,QAAQ,EAAE,UAAU,EAAE,CAAC;IACvB,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CAC1C;AA+MD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,GAAG,YAAY,CAS5F;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,GAAG,YAAY,CAyC5F;AAsJD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,YAAY,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,YAAY,CAAC,EAAE,KAAK,EAAE,IAAa,EAAE,UAAU,EAAE,SAAS,EAAE,EAAE,iBAAiB,kDAsL9F;AAED,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"LineageGraph.d.ts","sourceRoot":"","sources":["../../../src/cosmicintersection/components/LineageGraph.tsx"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EACV,WAAW,EACX,aAAa,EACb,UAAU,EACV,WAAW,EACX,WAAW,EACX,cAAc,EACd,UAAU,EACV,SAAS,EACV,MAAM,kBAAkB,CAAC;AAO1B,MAAM,MAAM,eAAe,GACzB,QAAQ,GAAG,YAAY,GAAG,SAAS,GAAG,KAAK,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,CAAC;AAEpF,MAAM,WAAW,WAAW;IAC1B,wDAAwD;IACxD,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,eAAe,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wFAAwF;IACxF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,MAAM,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,KAAK,EAAE,WAAW,EAAE,CAAC;CACtB;AAED,kFAAkF;AAClF,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACtC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACpC,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAC5C,IAAI,EAAE,cAAc,EAAE,CAAC;IACvB,SAAS,EAAE,WAAW,EAAE,CAAC;IACzB,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,QAAQ,EAAE,UAAU,EAAE,CAAC;IACvB,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CAC1C;AA+MD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,GAAG,YAAY,CAS5F;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,GAAG,YAAY,CAyC5F;AAsJD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,YAAY,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,YAAY,CAAC,EAAE,KAAK,EAAE,IAAa,EAAE,UAAU,EAAE,SAAS,EAAE,EAAE,iBAAiB,kDAsL9F;AAED,eAAe,YAAY,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"LineageGraph.js","names":[],"sources":["../../../src/cosmicintersection/components/LineageGraph.tsx"],"sourcesContent":["/**\n * LineageGraph — a reusable, dependency-free provenance DAG for CosmicIntersection.\n *\n * Renders a typed, left-to-right lineage graph (Source → Dataset → Pipeline Run →\n * Notebook / Analysis → Release) as custom inline SVG edges + absolutely-positioned\n * token-styled node cards. No graph library — the layout is a simple column/row\n * solver, edges are cubic-bezier `<path>`s. Every colour comes from fe-libs semantic\n * tokens (bg-*, text-*, border-*, status-*, accent-*) so it renders in every theme.\n *\n * The graph MODEL is derived client-side from existing cos entities by the pure\n * `buildDatasetLineage` / `buildReleaseLineage` builders exported below — the\n * component itself never fetches. Nodes with a `route` are clickable and call\n * `onNavigate` (basePath-relative), matching the rest of the MFE's navigation.\n *\n * Modes:\n * - `full` — page mode (larger cards, legend, dotted canvas). See mockup 20.\n * - `compact` — detail-panel mini map (smaller cards, no legend). See mockup 03.\n */\nimport { useId, useMemo } from 'react';\nimport {\n Boxes,\n Cog,\n Database,\n LineChart,\n NotebookText,\n Radar,\n Tag,\n type LucideIcon,\n} from 'lucide-react';\n\nimport type {\n CosAnalysis,\n CosCollection,\n CosDataset,\n CosNotebook,\n CosPipeline,\n CosPipelineRun,\n CosRelease,\n CosSource,\n} from '../api/cosmicApi';\nimport { cn } from '../utils/cn';\n\n// ============================================================================\n// MODEL\n// ============================================================================\n\nexport type LineageNodeType =\n | 'source'\n | 'collection'\n | 'dataset'\n | 'run'\n | 'notebook'\n | 'analysis'\n | 'release';\n\nexport interface LineageNode {\n /** Stable, unique within a model (e.g. `ds:<uuid>`). */\n id: string;\n type: LineageNodeType;\n title: string;\n /** Overrides the default type label (e.g. \"Pipeline Run #340\"). */\n typeLabel?: string;\n /** Small third line — status, version, DOI hint (hidden in compact mode). */\n subtitle?: string;\n /** basePath-relative route; when set (and `onNavigate` given) the node is clickable. */\n route?: string;\n /** Semantic column / layer index, 0 = leftmost (upstream). */\n column: number;\n /** Highlight the graph root (the entity the lineage was requested for). */\n emphasis?: boolean;\n}\n\nexport interface LineageEdge {\n from: string;\n to: string;\n}\n\nexport interface LineageModel {\n nodes: LineageNode[];\n edges: LineageEdge[];\n}\n\n/** Data pools the builders read from — resolved maps + already-filtered lists. */\nexport interface LineageDataPools {\n datasetsById: Map<string, CosDataset>;\n sourcesById: Map<string, CosSource>;\n collectionsById: Map<string, CosCollection>;\n runs: CosPipelineRun[];\n notebooks: CosNotebook[];\n analyses: CosAnalysis[];\n releases: CosRelease[];\n pipelinesById?: Map<string, CosPipeline>;\n}\n\n// ============================================================================\n// BUILDERS (pure — no hooks, no fetching)\n// ============================================================================\n\nconst COLUMN: Record<LineageNodeType, number> = {\n source: 0,\n collection: 0,\n dataset: 1,\n run: 2,\n notebook: 3,\n analysis: 4,\n release: 5,\n};\n\nfunction humanEnum(value: string): string {\n const lower = value.replace(/_/g, ' ').toLowerCase();\n return lower.charAt(0).toUpperCase() + lower.slice(1);\n}\n\n/** Incrementally assembles a de-duplicated node/edge model. */\nclass LineageBuilder {\n private readonly nodes = new Map<string, LineageNode>();\n private readonly edgeKeys = new Set<string>();\n private readonly edges: LineageEdge[] = [];\n\n addNode(node: LineageNode): void {\n const existing = this.nodes.get(node.id);\n if (!existing) {\n this.nodes.set(node.id, node);\n return;\n }\n // Preserve an emphasis flag once set (root wins over a later plain add).\n if (node.emphasis && !existing.emphasis) {\n this.nodes.set(node.id, { ...existing, emphasis: true });\n }\n }\n\n link(from: string, to: string): void {\n if (from === to) {\n return;\n }\n // Only connect endpoints that actually exist in the graph.\n if (!this.nodes.has(from) || !this.nodes.has(to)) {\n return;\n }\n const key = `${from}»${to}`;\n if (this.edgeKeys.has(key)) {\n return;\n }\n this.edgeKeys.add(key);\n this.edges.push({ from, to });\n }\n\n has(id: string): boolean {\n return this.nodes.has(id);\n }\n\n build(): LineageModel {\n return { nodes: [...this.nodes.values()], edges: this.edges };\n }\n}\n\nconst nid = {\n source: (id: string) => `src:${id}`,\n collection: (id: string) => `col:${id}`,\n dataset: (id: string) => `ds:${id}`,\n run: (id: string) => `run:${id}`,\n notebook: (id: string) => `nb:${id}`,\n analysis: (id: string) => `an:${id}`,\n release: (id: string) => `rel:${id}`,\n};\n\n/** Add a dataset node plus its upstream source/collection to the builder. */\nfunction addDatasetUpstream(\n builder: LineageBuilder,\n dataset: CosDataset,\n pools: LineageDataPools,\n emphasis: boolean\n): void {\n builder.addNode({\n id: nid.dataset(dataset.id),\n type: 'dataset',\n title: dataset.name,\n subtitle: humanEnum(dataset.status),\n route: `datasets/${dataset.id}`,\n column: COLUMN.dataset,\n emphasis,\n });\n\n if (dataset.sourceId) {\n const source = pools.sourcesById.get(dataset.sourceId);\n builder.addNode({\n id: nid.source(dataset.sourceId),\n type: 'source',\n title: source?.name ?? 'Linked source',\n subtitle: source ? humanEnum(source.type) : undefined,\n route: `sources/${dataset.sourceId}`,\n column: COLUMN.source,\n });\n builder.link(nid.source(dataset.sourceId), nid.dataset(dataset.id));\n }\n\n if (dataset.collectionId) {\n const collection = pools.collectionsById.get(dataset.collectionId);\n builder.addNode({\n id: nid.collection(dataset.collectionId),\n type: 'collection',\n title: collection?.name ?? 'Collection',\n route: `collections/${dataset.collectionId}`,\n column: COLUMN.collection,\n });\n builder.link(nid.collection(dataset.collectionId), nid.dataset(dataset.id));\n }\n}\n\n/** Add the downstream fan for a dataset: runs, notebooks, analyses, releases. */\nfunction addDatasetDownstream(\n builder: LineageBuilder,\n dataset: CosDataset,\n pools: LineageDataPools\n): void {\n const datasetNode = nid.dataset(dataset.id);\n\n for (const run of pools.runs) {\n if (run.datasetId !== dataset.id) {\n continue;\n }\n const pipelineName = run.pipelineId\n ? pools.pipelinesById?.get(run.pipelineId)?.name\n : undefined;\n builder.addNode({\n id: nid.run(run.id),\n type: 'run',\n typeLabel: `Pipeline Run #${run.runNumber}`,\n title: pipelineName ?? `Run #${run.runNumber}`,\n subtitle: humanEnum(run.status),\n route: `runs/${run.id}`,\n column: COLUMN.run,\n });\n builder.link(datasetNode, nid.run(run.id));\n }\n\n for (const notebook of pools.notebooks) {\n if (!notebook.datasetIds.includes(dataset.id)) {\n continue;\n }\n builder.addNode({\n id: nid.notebook(notebook.id),\n type: 'notebook',\n title: notebook.title,\n subtitle: humanEnum(notebook.status),\n route: `notebooks/${notebook.id}`,\n column: COLUMN.notebook,\n });\n builder.link(datasetNode, nid.notebook(notebook.id));\n }\n\n for (const analysis of pools.analyses) {\n const linkedToNotebook =\n analysis.notebookId != null && builder.has(nid.notebook(analysis.notebookId));\n if (analysis.datasetId !== dataset.id && !linkedToNotebook) {\n continue;\n }\n builder.addNode({\n id: nid.analysis(analysis.id),\n type: 'analysis',\n title: analysis.title,\n subtitle: humanEnum(analysis.status),\n // No per-analysis detail route exists — land on the analyses list.\n route: 'analyses',\n column: COLUMN.analysis,\n });\n if (linkedToNotebook && analysis.notebookId != null) {\n builder.link(nid.notebook(analysis.notebookId), nid.analysis(analysis.id));\n } else {\n builder.link(datasetNode, nid.analysis(analysis.id));\n }\n }\n\n for (const release of pools.releases) {\n if (!release.datasetIds.includes(dataset.id)) {\n continue;\n }\n addReleaseNode(builder, release);\n builder.link(datasetNode, nid.release(release.id));\n if (release.notebookId != null && builder.has(nid.notebook(release.notebookId))) {\n builder.link(nid.notebook(release.notebookId), nid.release(release.id));\n }\n }\n}\n\nfunction addReleaseNode(builder: LineageBuilder, release: CosRelease, emphasis = false): void {\n const doiSuffix = release.doi ? ' · DOI' : '';\n builder.addNode({\n id: nid.release(release.id),\n type: 'release',\n title: release.title,\n subtitle: `v${release.version}${doiSuffix}`,\n // No per-release detail route exists — land on the releases list.\n route: 'releases',\n column: COLUMN.release,\n emphasis,\n });\n}\n\n/**\n * Provenance for a single dataset: its upstream (source + collection) and its\n * full downstream fan (runs, notebooks, analyses, releases). The dataset is the\n * emphasised root.\n */\nexport function buildDatasetLineage(datasetId: string, pools: LineageDataPools): LineageModel {\n const builder = new LineageBuilder();\n const dataset = pools.datasetsById.get(datasetId);\n if (!dataset) {\n return builder.build();\n }\n addDatasetUpstream(builder, dataset, pools, true);\n addDatasetDownstream(builder, dataset, pools);\n return builder.build();\n}\n\n/**\n * Provenance for a release: the release plus its input datasets and notebook,\n * each expanded UPSTREAM (source/collection for datasets; the notebook's own\n * datasets). The release is the emphasised root.\n */\nexport function buildReleaseLineage(releaseId: string, pools: LineageDataPools): LineageModel {\n const builder = new LineageBuilder();\n const release = pools.releases.find((item) => item.id === releaseId);\n if (!release) {\n return builder.build();\n }\n\n addReleaseNode(builder, release, true);\n\n for (const datasetId of release.datasetIds) {\n const dataset = pools.datasetsById.get(datasetId);\n if (!dataset) {\n continue;\n }\n addDatasetUpstream(builder, dataset, pools, false);\n builder.link(nid.dataset(dataset.id), nid.release(release.id));\n }\n\n if (release.notebookId != null) {\n const notebook = pools.notebooks.find((item) => item.id === release.notebookId);\n builder.addNode({\n id: nid.notebook(release.notebookId),\n type: 'notebook',\n title: notebook?.title ?? 'Notebook',\n subtitle: notebook ? humanEnum(notebook.status) : undefined,\n route: `notebooks/${release.notebookId}`,\n column: COLUMN.notebook,\n });\n builder.link(nid.notebook(release.notebookId), nid.release(release.id));\n\n for (const datasetId of notebook?.datasetIds ?? []) {\n const dataset = pools.datasetsById.get(datasetId);\n if (!dataset) {\n continue;\n }\n addDatasetUpstream(builder, dataset, pools, false);\n builder.link(nid.dataset(dataset.id), nid.notebook(release.notebookId));\n }\n }\n\n return builder.build();\n}\n\n// ============================================================================\n// PRESENTATION\n// ============================================================================\n\ninterface NodeMeta {\n label: string;\n Icon: LucideIcon;\n /** Icon-chip classes (semantic accent tokens only). */\n chip: string;\n /** Card border colour class. */\n border: string;\n}\n\nconst NODE_META: Record<LineageNodeType, NodeMeta> = {\n source: {\n label: 'Source',\n Icon: Radar,\n chip: 'bg-accent-blue-subtle text-accent-blue',\n border: 'border-accent-blue',\n },\n collection: {\n label: 'Collection',\n Icon: Boxes,\n chip: 'bg-accent-teal-subtle text-accent-teal',\n border: 'border-accent-teal',\n },\n dataset: {\n label: 'Dataset',\n Icon: Database,\n chip: 'bg-accent-indigo-subtle text-accent-indigo',\n border: 'border-accent-indigo',\n },\n run: {\n label: 'Pipeline Run',\n Icon: Cog,\n chip: 'bg-accent-purple-subtle text-accent-purple',\n border: 'border-accent-purple',\n },\n notebook: {\n label: 'Notebook',\n Icon: NotebookText,\n chip: 'bg-accent-teal-subtle text-accent-teal',\n border: 'border-accent-teal',\n },\n analysis: {\n label: 'Analysis',\n Icon: LineChart,\n chip: 'bg-accent-purple-subtle text-accent-purple',\n border: 'border-accent-purple',\n },\n release: {\n label: 'Release',\n Icon: Tag,\n chip: 'bg-status-success-bg-subtle text-status-success-text',\n border: 'border-status-success-border',\n },\n};\n\ninterface Dims {\n nodeW: number;\n nodeH: number;\n colGap: number;\n rowGap: number;\n pad: number;\n iconSize: string;\n showSubtitle: boolean;\n}\n\nconst DIMS: Record<'full' | 'compact', Dims> = {\n full: {\n nodeW: 190,\n nodeH: 78,\n colGap: 74,\n rowGap: 30,\n pad: 32,\n iconSize: 'size-8',\n showSubtitle: true,\n },\n compact: {\n nodeW: 150,\n nodeH: 54,\n colGap: 46,\n rowGap: 18,\n pad: 18,\n iconSize: 'size-6',\n showSubtitle: false,\n },\n};\n\ninterface Positioned {\n node: LineageNode;\n x: number;\n y: number;\n}\n\ninterface Layout {\n positions: Map<string, Positioned>;\n width: number;\n height: number;\n}\n\nfunction computeLayout(model: LineageModel, dims: Dims): Layout {\n const positions = new Map<string, Positioned>();\n if (model.nodes.length === 0) {\n return { positions, width: dims.pad * 2, height: dims.pad * 2 };\n }\n\n // Present columns, remapped to contiguous indices so empty layers collapse.\n const presentCols = [...new Set(model.nodes.map((n) => n.column))].sort((a, b) => a - b);\n const colOrder = new Map(presentCols.map((c, i) => [c, i]));\n\n const byColumn = new Map<number, LineageNode[]>();\n for (const node of model.nodes) {\n const list = byColumn.get(node.column);\n if (list) {\n list.push(node);\n } else {\n byColumn.set(node.column, [node]);\n }\n }\n\n let contentH = dims.nodeH;\n for (const list of byColumn.values()) {\n const h = list.length * dims.nodeH + (list.length - 1) * dims.rowGap;\n contentH = Math.max(contentH, h);\n }\n\n for (const col of presentCols) {\n const list = byColumn.get(col) ?? [];\n const colH = list.length * dims.nodeH + (list.length - 1) * dims.rowGap;\n const startY = dims.pad + (contentH - colH) / 2;\n const x = dims.pad + (colOrder.get(col) ?? 0) * (dims.nodeW + dims.colGap);\n list.forEach((node, i) => {\n positions.set(node.id, { node, x, y: startY + i * (dims.nodeH + dims.rowGap) });\n });\n }\n\n const width =\n dims.pad * 2 + presentCols.length * dims.nodeW + (presentCols.length - 1) * dims.colGap;\n const height = dims.pad * 2 + contentH;\n return { positions, width, height };\n}\n\nfunction edgePath(x1: number, y1: number, x2: number, y2: number): string {\n const dx = Math.max(24, (x2 - x1) * 0.5);\n return `M ${x1} ${y1} C ${x1 + dx} ${y1}, ${x2 - dx} ${y2}, ${x2} ${y2}`;\n}\n\nexport interface LineageGraphProps {\n model: LineageModel;\n mode?: 'full' | 'compact';\n onNavigate?: (route: string) => void;\n className?: string;\n}\n\nexport function LineageGraph({ model, mode = 'full', onNavigate, className }: LineageGraphProps) {\n const markerId = `cos-lineage-arrow-${useId().replace(/:/g, '')}`;\n const dims = DIMS[mode];\n\n const layout = useMemo(() => computeLayout(model, dims), [model, dims]);\n\n const legendTypes = useMemo(() => {\n const seen = new Set<LineageNodeType>();\n const ordered: LineageNodeType[] = [];\n for (const node of [...model.nodes].sort((a, b) => a.column - b.column)) {\n if (!seen.has(node.type)) {\n seen.add(node.type);\n ordered.push(node.type);\n }\n }\n return ordered;\n }, [model.nodes]);\n\n if (model.nodes.length === 0) {\n return null;\n }\n\n return (\n <div className={cn('flex flex-col gap-3', className)}>\n {mode === 'full' && legendTypes.length > 0 ? (\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-2\">\n {legendTypes.map((type) => {\n const meta = NODE_META[type];\n return (\n <span\n key={type}\n className=\"inline-flex items-center gap-1.5 text-xs text-text-secondary\"\n >\n <span\n className={cn(\n 'flex size-5 items-center justify-center rounded-button',\n meta.chip\n )}\n >\n <meta.Icon className=\"size-3\" aria-hidden=\"true\" />\n </span>\n {meta.label}\n </span>\n );\n })}\n </div>\n ) : null}\n\n <div\n className={cn(\n 'overflow-x-auto rounded-card border border-border-seam bg-bg-canvas',\n mode === 'full' ? 'p-1' : 'p-0.5'\n )}\n >\n <div\n className=\"relative\"\n style={{\n width: layout.width,\n height: layout.height,\n minWidth: '100%',\n backgroundImage: 'radial-gradient(var(--color-border-subtle) 1px, transparent 1px)',\n backgroundSize: mode === 'full' ? '20px 20px' : '16px 16px',\n }}\n >\n {/* Edges (behind the node cards). */}\n <svg\n className=\"pointer-events-none absolute inset-0 text-border-strong\"\n width={layout.width}\n height={layout.height}\n viewBox={`0 0 ${layout.width} ${layout.height}`}\n aria-hidden=\"true\"\n >\n <defs>\n <marker\n id={markerId}\n viewBox=\"0 0 8 8\"\n refX=\"6.5\"\n refY=\"4\"\n markerWidth=\"6\"\n markerHeight=\"6\"\n orient=\"auto-start-reverse\"\n >\n <path d=\"M1 1 L7 4 L1 7 z\" fill=\"currentColor\" />\n </marker>\n </defs>\n {model.edges.map((edge) => {\n const from = layout.positions.get(edge.from);\n const to = layout.positions.get(edge.to);\n if (!from || !to) {\n return null;\n }\n const x1 = from.x + dims.nodeW;\n const y1 = from.y + dims.nodeH / 2;\n const x2 = to.x;\n const y2 = to.y + dims.nodeH / 2;\n return (\n <path\n key={`${edge.from}»${edge.to}`}\n d={edgePath(x1, y1, x2, y2)}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={mode === 'full' ? 1.5 : 1.25}\n strokeLinecap=\"round\"\n markerEnd={`url(#${markerId})`}\n opacity={0.7}\n />\n );\n })}\n </svg>\n\n {/* Node cards. */}\n {[...layout.positions.values()].map(({ node, x, y }) => {\n const meta = NODE_META[node.type];\n const clickable = Boolean(node.route && onNavigate);\n const content = (\n <div\n className={cn(\n 'flex h-full w-full items-center gap-2.5 rounded-card border bg-bg-elevated px-3 shadow-sm transition-shadow',\n meta.border,\n clickable && 'group-hover:shadow-md',\n node.emphasis && 'ring-1 ring-border-strong'\n )}\n >\n <span\n className={cn(\n 'flex shrink-0 items-center justify-center rounded-button',\n dims.iconSize,\n meta.chip\n )}\n aria-hidden=\"true\"\n >\n <meta.Icon className={mode === 'full' ? 'size-4' : 'size-3.5'} />\n </span>\n <span className=\"flex min-w-0 flex-1 flex-col text-left\">\n <span className=\"truncate text-[10px] font-medium uppercase tracking-wide text-text-tertiary\">\n {node.typeLabel ?? meta.label}\n </span>\n <span\n className={cn(\n 'truncate font-semibold text-text-primary',\n mode === 'full' ? 'text-sm' : 'text-xs'\n )}\n >\n {node.title}\n </span>\n {dims.showSubtitle && node.subtitle ? (\n <span className=\"truncate text-[11px] text-text-secondary\">\n {node.subtitle}\n </span>\n ) : null}\n </span>\n </div>\n );\n\n const style = { left: x, top: y, width: dims.nodeW, height: dims.nodeH } as const;\n\n if (clickable && node.route) {\n const route = node.route;\n return (\n <button\n key={node.id}\n type=\"button\"\n onClick={() => onNavigate?.(route)}\n className=\"group absolute rounded-card focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus\"\n style={style}\n title={`${node.typeLabel ?? meta.label}: ${node.title}`}\n >\n {content}\n </button>\n );\n }\n\n return (\n <div key={node.id} className=\"absolute\" style={style}>\n {content}\n </div>\n );\n })}\n </div>\n </div>\n </div>\n );\n}\n\nexport default LineageGraph;\n"],"mappings":"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"}
1
+ {"version":3,"file":"LineageGraph.js","names":[],"sources":["../../../src/cosmicintersection/components/LineageGraph.tsx"],"sourcesContent":["/**\n * LineageGraph — a reusable, dependency-free provenance DAG for CosmicIntersection.\n *\n * Renders a typed, left-to-right lineage graph (Source → Dataset → Pipeline Run →\n * Notebook / Analysis → Release) as custom inline SVG edges + absolutely-positioned\n * token-styled node cards. No graph library — the layout is a simple column/row\n * solver, edges are cubic-bezier `<path>`s. Every colour comes from fe-libs semantic\n * tokens (bg-*, text-*, border-*, status-*, accent-*) so it renders in every theme.\n *\n * The graph MODEL is derived client-side from existing cos entities by the pure\n * `buildDatasetLineage` / `buildReleaseLineage` builders exported below — the\n * component itself never fetches. Nodes with a `route` are clickable and call\n * `onNavigate` (basePath-relative), matching the rest of the MFE's navigation.\n *\n * Modes:\n * - `full` — page mode (larger cards, legend, dotted canvas). See mockup 20.\n * - `compact` — detail-panel mini map (smaller cards, no legend). See mockup 03.\n */\nimport { useId, useMemo } from 'react';\nimport {\n Boxes,\n Cog,\n Database,\n LineChart,\n NotebookText,\n Radar,\n Tag,\n type LucideIcon,\n} from 'lucide-react';\n\nimport type {\n CosAnalysis,\n CosCollection,\n CosDataset,\n CosNotebook,\n CosPipeline,\n CosPipelineRun,\n CosRelease,\n CosSource,\n} from '../api/cosmicApi';\nimport { cn } from '../utils/cn';\n\n// ============================================================================\n// MODEL\n// ============================================================================\n\nexport type LineageNodeType =\n 'source' | 'collection' | 'dataset' | 'run' | 'notebook' | 'analysis' | 'release';\n\nexport interface LineageNode {\n /** Stable, unique within a model (e.g. `ds:<uuid>`). */\n id: string;\n type: LineageNodeType;\n title: string;\n /** Overrides the default type label (e.g. \"Pipeline Run #340\"). */\n typeLabel?: string;\n /** Small third line — status, version, DOI hint (hidden in compact mode). */\n subtitle?: string;\n /** basePath-relative route; when set (and `onNavigate` given) the node is clickable. */\n route?: string;\n /** Semantic column / layer index, 0 = leftmost (upstream). */\n column: number;\n /** Highlight the graph root (the entity the lineage was requested for). */\n emphasis?: boolean;\n}\n\nexport interface LineageEdge {\n from: string;\n to: string;\n}\n\nexport interface LineageModel {\n nodes: LineageNode[];\n edges: LineageEdge[];\n}\n\n/** Data pools the builders read from — resolved maps + already-filtered lists. */\nexport interface LineageDataPools {\n datasetsById: Map<string, CosDataset>;\n sourcesById: Map<string, CosSource>;\n collectionsById: Map<string, CosCollection>;\n runs: CosPipelineRun[];\n notebooks: CosNotebook[];\n analyses: CosAnalysis[];\n releases: CosRelease[];\n pipelinesById?: Map<string, CosPipeline>;\n}\n\n// ============================================================================\n// BUILDERS (pure — no hooks, no fetching)\n// ============================================================================\n\nconst COLUMN: Record<LineageNodeType, number> = {\n source: 0,\n collection: 0,\n dataset: 1,\n run: 2,\n notebook: 3,\n analysis: 4,\n release: 5,\n};\n\nfunction humanEnum(value: string): string {\n const lower = value.replace(/_/g, ' ').toLowerCase();\n return lower.charAt(0).toUpperCase() + lower.slice(1);\n}\n\n/** Incrementally assembles a de-duplicated node/edge model. */\nclass LineageBuilder {\n private readonly nodes = new Map<string, LineageNode>();\n private readonly edgeKeys = new Set<string>();\n private readonly edges: LineageEdge[] = [];\n\n addNode(node: LineageNode): void {\n const existing = this.nodes.get(node.id);\n if (!existing) {\n this.nodes.set(node.id, node);\n return;\n }\n // Preserve an emphasis flag once set (root wins over a later plain add).\n if (node.emphasis && !existing.emphasis) {\n this.nodes.set(node.id, { ...existing, emphasis: true });\n }\n }\n\n link(from: string, to: string): void {\n if (from === to) {\n return;\n }\n // Only connect endpoints that actually exist in the graph.\n if (!this.nodes.has(from) || !this.nodes.has(to)) {\n return;\n }\n const key = `${from}»${to}`;\n if (this.edgeKeys.has(key)) {\n return;\n }\n this.edgeKeys.add(key);\n this.edges.push({ from, to });\n }\n\n has(id: string): boolean {\n return this.nodes.has(id);\n }\n\n build(): LineageModel {\n return { nodes: [...this.nodes.values()], edges: this.edges };\n }\n}\n\nconst nid = {\n source: (id: string) => `src:${id}`,\n collection: (id: string) => `col:${id}`,\n dataset: (id: string) => `ds:${id}`,\n run: (id: string) => `run:${id}`,\n notebook: (id: string) => `nb:${id}`,\n analysis: (id: string) => `an:${id}`,\n release: (id: string) => `rel:${id}`,\n};\n\n/** Add a dataset node plus its upstream source/collection to the builder. */\nfunction addDatasetUpstream(\n builder: LineageBuilder,\n dataset: CosDataset,\n pools: LineageDataPools,\n emphasis: boolean\n): void {\n builder.addNode({\n id: nid.dataset(dataset.id),\n type: 'dataset',\n title: dataset.name,\n subtitle: humanEnum(dataset.status),\n route: `datasets/${dataset.id}`,\n column: COLUMN.dataset,\n emphasis,\n });\n\n if (dataset.sourceId) {\n const source = pools.sourcesById.get(dataset.sourceId);\n builder.addNode({\n id: nid.source(dataset.sourceId),\n type: 'source',\n title: source?.name ?? 'Linked source',\n subtitle: source ? humanEnum(source.type) : undefined,\n route: `sources/${dataset.sourceId}`,\n column: COLUMN.source,\n });\n builder.link(nid.source(dataset.sourceId), nid.dataset(dataset.id));\n }\n\n if (dataset.collectionId) {\n const collection = pools.collectionsById.get(dataset.collectionId);\n builder.addNode({\n id: nid.collection(dataset.collectionId),\n type: 'collection',\n title: collection?.name ?? 'Collection',\n route: `collections/${dataset.collectionId}`,\n column: COLUMN.collection,\n });\n builder.link(nid.collection(dataset.collectionId), nid.dataset(dataset.id));\n }\n}\n\n/** Add the downstream fan for a dataset: runs, notebooks, analyses, releases. */\nfunction addDatasetDownstream(\n builder: LineageBuilder,\n dataset: CosDataset,\n pools: LineageDataPools\n): void {\n const datasetNode = nid.dataset(dataset.id);\n\n for (const run of pools.runs) {\n if (run.datasetId !== dataset.id) {\n continue;\n }\n const pipelineName = run.pipelineId\n ? pools.pipelinesById?.get(run.pipelineId)?.name\n : undefined;\n builder.addNode({\n id: nid.run(run.id),\n type: 'run',\n typeLabel: `Pipeline Run #${run.runNumber}`,\n title: pipelineName ?? `Run #${run.runNumber}`,\n subtitle: humanEnum(run.status),\n route: `runs/${run.id}`,\n column: COLUMN.run,\n });\n builder.link(datasetNode, nid.run(run.id));\n }\n\n for (const notebook of pools.notebooks) {\n if (!notebook.datasetIds.includes(dataset.id)) {\n continue;\n }\n builder.addNode({\n id: nid.notebook(notebook.id),\n type: 'notebook',\n title: notebook.title,\n subtitle: humanEnum(notebook.status),\n route: `notebooks/${notebook.id}`,\n column: COLUMN.notebook,\n });\n builder.link(datasetNode, nid.notebook(notebook.id));\n }\n\n for (const analysis of pools.analyses) {\n const linkedToNotebook =\n analysis.notebookId != null && builder.has(nid.notebook(analysis.notebookId));\n if (analysis.datasetId !== dataset.id && !linkedToNotebook) {\n continue;\n }\n builder.addNode({\n id: nid.analysis(analysis.id),\n type: 'analysis',\n title: analysis.title,\n subtitle: humanEnum(analysis.status),\n // No per-analysis detail route exists — land on the analyses list.\n route: 'analyses',\n column: COLUMN.analysis,\n });\n if (linkedToNotebook && analysis.notebookId != null) {\n builder.link(nid.notebook(analysis.notebookId), nid.analysis(analysis.id));\n } else {\n builder.link(datasetNode, nid.analysis(analysis.id));\n }\n }\n\n for (const release of pools.releases) {\n if (!release.datasetIds.includes(dataset.id)) {\n continue;\n }\n addReleaseNode(builder, release);\n builder.link(datasetNode, nid.release(release.id));\n if (release.notebookId != null && builder.has(nid.notebook(release.notebookId))) {\n builder.link(nid.notebook(release.notebookId), nid.release(release.id));\n }\n }\n}\n\nfunction addReleaseNode(builder: LineageBuilder, release: CosRelease, emphasis = false): void {\n const doiSuffix = release.doi ? ' · DOI' : '';\n builder.addNode({\n id: nid.release(release.id),\n type: 'release',\n title: release.title,\n subtitle: `v${release.version}${doiSuffix}`,\n // No per-release detail route exists — land on the releases list.\n route: 'releases',\n column: COLUMN.release,\n emphasis,\n });\n}\n\n/**\n * Provenance for a single dataset: its upstream (source + collection) and its\n * full downstream fan (runs, notebooks, analyses, releases). The dataset is the\n * emphasised root.\n */\nexport function buildDatasetLineage(datasetId: string, pools: LineageDataPools): LineageModel {\n const builder = new LineageBuilder();\n const dataset = pools.datasetsById.get(datasetId);\n if (!dataset) {\n return builder.build();\n }\n addDatasetUpstream(builder, dataset, pools, true);\n addDatasetDownstream(builder, dataset, pools);\n return builder.build();\n}\n\n/**\n * Provenance for a release: the release plus its input datasets and notebook,\n * each expanded UPSTREAM (source/collection for datasets; the notebook's own\n * datasets). The release is the emphasised root.\n */\nexport function buildReleaseLineage(releaseId: string, pools: LineageDataPools): LineageModel {\n const builder = new LineageBuilder();\n const release = pools.releases.find((item) => item.id === releaseId);\n if (!release) {\n return builder.build();\n }\n\n addReleaseNode(builder, release, true);\n\n for (const datasetId of release.datasetIds) {\n const dataset = pools.datasetsById.get(datasetId);\n if (!dataset) {\n continue;\n }\n addDatasetUpstream(builder, dataset, pools, false);\n builder.link(nid.dataset(dataset.id), nid.release(release.id));\n }\n\n if (release.notebookId != null) {\n const notebook = pools.notebooks.find((item) => item.id === release.notebookId);\n builder.addNode({\n id: nid.notebook(release.notebookId),\n type: 'notebook',\n title: notebook?.title ?? 'Notebook',\n subtitle: notebook ? humanEnum(notebook.status) : undefined,\n route: `notebooks/${release.notebookId}`,\n column: COLUMN.notebook,\n });\n builder.link(nid.notebook(release.notebookId), nid.release(release.id));\n\n for (const datasetId of notebook?.datasetIds ?? []) {\n const dataset = pools.datasetsById.get(datasetId);\n if (!dataset) {\n continue;\n }\n addDatasetUpstream(builder, dataset, pools, false);\n builder.link(nid.dataset(dataset.id), nid.notebook(release.notebookId));\n }\n }\n\n return builder.build();\n}\n\n// ============================================================================\n// PRESENTATION\n// ============================================================================\n\ninterface NodeMeta {\n label: string;\n Icon: LucideIcon;\n /** Icon-chip classes (semantic accent tokens only). */\n chip: string;\n /** Card border colour class. */\n border: string;\n}\n\nconst NODE_META: Record<LineageNodeType, NodeMeta> = {\n source: {\n label: 'Source',\n Icon: Radar,\n chip: 'bg-accent-blue-subtle text-accent-blue',\n border: 'border-accent-blue',\n },\n collection: {\n label: 'Collection',\n Icon: Boxes,\n chip: 'bg-accent-teal-subtle text-accent-teal',\n border: 'border-accent-teal',\n },\n dataset: {\n label: 'Dataset',\n Icon: Database,\n chip: 'bg-accent-indigo-subtle text-accent-indigo',\n border: 'border-accent-indigo',\n },\n run: {\n label: 'Pipeline Run',\n Icon: Cog,\n chip: 'bg-accent-purple-subtle text-accent-purple',\n border: 'border-accent-purple',\n },\n notebook: {\n label: 'Notebook',\n Icon: NotebookText,\n chip: 'bg-accent-teal-subtle text-accent-teal',\n border: 'border-accent-teal',\n },\n analysis: {\n label: 'Analysis',\n Icon: LineChart,\n chip: 'bg-accent-purple-subtle text-accent-purple',\n border: 'border-accent-purple',\n },\n release: {\n label: 'Release',\n Icon: Tag,\n chip: 'bg-status-success-bg-subtle text-status-success-text',\n border: 'border-status-success-border',\n },\n};\n\ninterface Dims {\n nodeW: number;\n nodeH: number;\n colGap: number;\n rowGap: number;\n pad: number;\n iconSize: string;\n showSubtitle: boolean;\n}\n\nconst DIMS: Record<'full' | 'compact', Dims> = {\n full: {\n nodeW: 190,\n nodeH: 78,\n colGap: 74,\n rowGap: 30,\n pad: 32,\n iconSize: 'size-8',\n showSubtitle: true,\n },\n compact: {\n nodeW: 150,\n nodeH: 54,\n colGap: 46,\n rowGap: 18,\n pad: 18,\n iconSize: 'size-6',\n showSubtitle: false,\n },\n};\n\ninterface Positioned {\n node: LineageNode;\n x: number;\n y: number;\n}\n\ninterface Layout {\n positions: Map<string, Positioned>;\n width: number;\n height: number;\n}\n\nfunction computeLayout(model: LineageModel, dims: Dims): Layout {\n const positions = new Map<string, Positioned>();\n if (model.nodes.length === 0) {\n return { positions, width: dims.pad * 2, height: dims.pad * 2 };\n }\n\n // Present columns, remapped to contiguous indices so empty layers collapse.\n const presentCols = [...new Set(model.nodes.map((n) => n.column))].sort((a, b) => a - b);\n const colOrder = new Map(presentCols.map((c, i) => [c, i]));\n\n const byColumn = new Map<number, LineageNode[]>();\n for (const node of model.nodes) {\n const list = byColumn.get(node.column);\n if (list) {\n list.push(node);\n } else {\n byColumn.set(node.column, [node]);\n }\n }\n\n let contentH = dims.nodeH;\n for (const list of byColumn.values()) {\n const h = list.length * dims.nodeH + (list.length - 1) * dims.rowGap;\n contentH = Math.max(contentH, h);\n }\n\n for (const col of presentCols) {\n const list = byColumn.get(col) ?? [];\n const colH = list.length * dims.nodeH + (list.length - 1) * dims.rowGap;\n const startY = dims.pad + (contentH - colH) / 2;\n const x = dims.pad + (colOrder.get(col) ?? 0) * (dims.nodeW + dims.colGap);\n list.forEach((node, i) => {\n positions.set(node.id, { node, x, y: startY + i * (dims.nodeH + dims.rowGap) });\n });\n }\n\n const width =\n dims.pad * 2 + presentCols.length * dims.nodeW + (presentCols.length - 1) * dims.colGap;\n const height = dims.pad * 2 + contentH;\n return { positions, width, height };\n}\n\nfunction edgePath(x1: number, y1: number, x2: number, y2: number): string {\n const dx = Math.max(24, (x2 - x1) * 0.5);\n return `M ${x1} ${y1} C ${x1 + dx} ${y1}, ${x2 - dx} ${y2}, ${x2} ${y2}`;\n}\n\nexport interface LineageGraphProps {\n model: LineageModel;\n mode?: 'full' | 'compact';\n onNavigate?: (route: string) => void;\n className?: string;\n}\n\nexport function LineageGraph({ model, mode = 'full', onNavigate, className }: LineageGraphProps) {\n const markerId = `cos-lineage-arrow-${useId().replace(/:/g, '')}`;\n const dims = DIMS[mode];\n\n const layout = useMemo(() => computeLayout(model, dims), [model, dims]);\n\n const legendTypes = useMemo(() => {\n const seen = new Set<LineageNodeType>();\n const ordered: LineageNodeType[] = [];\n for (const node of [...model.nodes].sort((a, b) => a.column - b.column)) {\n if (!seen.has(node.type)) {\n seen.add(node.type);\n ordered.push(node.type);\n }\n }\n return ordered;\n }, [model.nodes]);\n\n if (model.nodes.length === 0) {\n return null;\n }\n\n return (\n <div className={cn('flex flex-col gap-3', className)}>\n {mode === 'full' && legendTypes.length > 0 ? (\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-2\">\n {legendTypes.map((type) => {\n const meta = NODE_META[type];\n return (\n <span\n key={type}\n className=\"inline-flex items-center gap-1.5 text-xs text-text-secondary\"\n >\n <span\n className={cn(\n 'flex size-5 items-center justify-center rounded-button',\n meta.chip\n )}\n >\n <meta.Icon className=\"size-3\" aria-hidden=\"true\" />\n </span>\n {meta.label}\n </span>\n );\n })}\n </div>\n ) : null}\n\n <div\n className={cn(\n 'overflow-x-auto rounded-card border border-border-seam bg-bg-canvas',\n mode === 'full' ? 'p-1' : 'p-0.5'\n )}\n >\n <div\n className=\"relative\"\n style={{\n width: layout.width,\n height: layout.height,\n minWidth: '100%',\n backgroundImage: 'radial-gradient(var(--color-border-subtle) 1px, transparent 1px)',\n backgroundSize: mode === 'full' ? '20px 20px' : '16px 16px',\n }}\n >\n {/* Edges (behind the node cards). */}\n <svg\n className=\"pointer-events-none absolute inset-0 text-border-strong\"\n width={layout.width}\n height={layout.height}\n viewBox={`0 0 ${layout.width} ${layout.height}`}\n aria-hidden=\"true\"\n >\n <defs>\n <marker\n id={markerId}\n viewBox=\"0 0 8 8\"\n refX=\"6.5\"\n refY=\"4\"\n markerWidth=\"6\"\n markerHeight=\"6\"\n orient=\"auto-start-reverse\"\n >\n <path d=\"M1 1 L7 4 L1 7 z\" fill=\"currentColor\" />\n </marker>\n </defs>\n {model.edges.map((edge) => {\n const from = layout.positions.get(edge.from);\n const to = layout.positions.get(edge.to);\n if (!from || !to) {\n return null;\n }\n const x1 = from.x + dims.nodeW;\n const y1 = from.y + dims.nodeH / 2;\n const x2 = to.x;\n const y2 = to.y + dims.nodeH / 2;\n return (\n <path\n key={`${edge.from}»${edge.to}`}\n d={edgePath(x1, y1, x2, y2)}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={mode === 'full' ? 1.5 : 1.25}\n strokeLinecap=\"round\"\n markerEnd={`url(#${markerId})`}\n opacity={0.7}\n />\n );\n })}\n </svg>\n\n {/* Node cards. */}\n {[...layout.positions.values()].map(({ node, x, y }) => {\n const meta = NODE_META[node.type];\n const clickable = Boolean(node.route && onNavigate);\n const content = (\n <div\n className={cn(\n 'flex h-full w-full items-center gap-2.5 rounded-card border bg-bg-elevated px-3 shadow-sm transition-shadow',\n meta.border,\n clickable && 'group-hover:shadow-md',\n node.emphasis && 'ring-1 ring-border-strong'\n )}\n >\n <span\n className={cn(\n 'flex shrink-0 items-center justify-center rounded-button',\n dims.iconSize,\n meta.chip\n )}\n aria-hidden=\"true\"\n >\n <meta.Icon className={mode === 'full' ? 'size-4' : 'size-3.5'} />\n </span>\n <span className=\"flex min-w-0 flex-1 flex-col text-left\">\n <span className=\"truncate text-[10px] font-medium uppercase tracking-wide text-text-tertiary\">\n {node.typeLabel ?? meta.label}\n </span>\n <span\n className={cn(\n 'truncate font-semibold text-text-primary',\n mode === 'full' ? 'text-sm' : 'text-xs'\n )}\n >\n {node.title}\n </span>\n {dims.showSubtitle && node.subtitle ? (\n <span className=\"truncate text-[11px] text-text-secondary\">\n {node.subtitle}\n </span>\n ) : null}\n </span>\n </div>\n );\n\n const style = { left: x, top: y, width: dims.nodeW, height: dims.nodeH } as const;\n\n if (clickable && node.route) {\n const route = node.route;\n return (\n <button\n key={node.id}\n type=\"button\"\n onClick={() => onNavigate?.(route)}\n className=\"group absolute rounded-card focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus\"\n style={style}\n title={`${node.typeLabel ?? meta.label}: ${node.title}`}\n >\n {content}\n </button>\n );\n }\n\n return (\n <div key={node.id} className=\"absolute\" style={style}>\n {content}\n </div>\n );\n })}\n </div>\n </div>\n </div>\n );\n}\n\nexport default LineageGraph;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"LoadingState.js","names":[],"sources":["../../../src/cosmicintersection/components/LoadingState.tsx"],"sourcesContent":["import { Skeleton, Spinner } from '@burdenoff/fe-libs/ui';\n\nexport interface LoadingStateProps {\n /** `spinner` centres a spinner; `rows` renders shimmering list placeholders. */\n variant?: 'spinner' | 'rows';\n /** Number of placeholder rows for the `rows` variant. */\n rows?: number;\n label?: string;\n className?: string;\n}\n\n/**\n * Consistent loading affordance for every CosmicIntersection query surface.\n * Token-styled only — no raw palette colours.\n */\nexport function LoadingState({\n variant = 'spinner',\n rows = 5,\n label = 'Loading…',\n className,\n}: LoadingStateProps) {\n if (variant === 'rows') {\n return (\n <div className={`space-y-3 ${className ?? ''}`} role=\"status\" aria-label={label}>\n {Array.from({ length: rows }).map((_, index) => (\n <Skeleton key={index} className=\"h-14 w-full rounded-card\" />\n ))}\n </div>\n );\n }\n\n return (\n <div\n className={`flex min-h-[240px] flex-col items-center justify-center gap-3 ${className ?? ''}`}\n role=\"status\"\n aria-label={label}\n >\n <Spinner className=\"size-6 text-text-secondary\" />\n <p className=\"text-sm text-text-secondary\">{label}</p>\n </div>\n );\n}\n\nexport default LoadingState;\n"],"mappings":";;;AAeA,SAAgB,EAAa,EAC3B,aAAU,WACV,UAAO,GACP,WAAQ,YACR,gBACoB;AAWpB,QAVI,MAAY,SAEZ,kBAAC,OAAD;EAAK,WAAW,aAAa,KAAa;EAAM,MAAK;EAAS,cAAY;YACvE,MAAM,KAAK,EAAE,QAAQ,GAAM,CAAC,CAAC,KAAK,GAAG,MACpC,kBAAC,GAAD,EAAsB,WAAU,4BAA6B,EAA9C,EAA8C,CAC7D;EACE,CAAA,GAKR,kBAAC,OAAD;EACE,WAAW,iEAAiE,KAAa;EACzF,MAAK;EACL,cAAY;YAHd,CAKE,kBAAC,GAAD,EAAS,WAAU,8BAA+B,CAAA,EAClD,kBAAC,KAAD;GAAG,WAAU;aAA+B;GAAU,CAAA,CAClD"}
1
+ {"version":3,"file":"LoadingState.js","names":[],"sources":["../../../src/cosmicintersection/components/LoadingState.tsx"],"sourcesContent":["import { Skeleton, Spinner } from '@burdenoff/fe-libs/ui';\n\nexport interface LoadingStateProps {\n /** `spinner` centres a spinner; `rows` renders shimmering list placeholders. */\n variant?: 'spinner' | 'rows';\n /** Number of placeholder rows for the `rows` variant. */\n rows?: number;\n label?: string;\n className?: string;\n}\n\n/**\n * Consistent loading affordance for every CosmicIntersection query surface.\n * Token-styled only — no raw palette colours.\n */\nexport function LoadingState({\n variant = 'spinner',\n rows = 5,\n label = 'Loading…',\n className,\n}: LoadingStateProps) {\n if (variant === 'rows') {\n return (\n <div className={`space-y-3 ${className ?? ''}`} role=\"status\" aria-label={label}>\n {Array.from({ length: rows }).map((_, index) => (\n <Skeleton key={index} className=\"h-14 w-full rounded-card\" />\n ))}\n </div>\n );\n }\n\n return (\n <div\n className={`flex min-h-[240px] flex-col items-center justify-center gap-3 ${className ?? ''}`}\n role=\"status\"\n aria-label={label}\n >\n <Spinner className=\"size-6 text-text-secondary\" />\n <p className=\"text-sm text-text-secondary\">{label}</p>\n </div>\n );\n}\n\nexport default LoadingState;\n"],"mappings":";;;AAeA,SAAgB,EAAa,EAC3B,aAAU,WACV,UAAO,GACP,WAAQ,YACR,gBACoB;CAWpB,OAVI,MAAY,SAEZ,kBAAC,OAAD;EAAK,WAAW,aAAa,KAAa;EAAM,MAAK;EAAS,cAAY;YACvE,MAAM,KAAK,EAAE,QAAQ,EAAK,CAAC,EAAE,KAAK,GAAG,MACpC,kBAAC,GAAD,EAAsB,WAAU,2BAA4B,GAA7C,CAA6C,CAC7D;CACE,CAAA,IAKP,kBAAC,OAAD;EACE,WAAW,iEAAiE,KAAa;EACzF,MAAK;EACL,cAAY;YAHd,CAKE,kBAAC,GAAD,EAAS,WAAU,6BAA8B,CAAA,GACjD,kBAAC,KAAD;GAAG,WAAU;aAA+B;EAAS,CAAA,CAClD;;AAET"}
@@ -1 +1 @@
1
- {"version":3,"file":"PageLayout.js","names":[],"sources":["../../../src/cosmicintersection/components/PageLayout.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { ChevronLeft } from 'lucide-react';\nimport { useCosmicIntersection } from '../context/CosmicIntersectionContext';\nimport { cn } from '../utils/cn';\n\ninterface PageLayoutProps {\n title: ReactNode;\n description?: ReactNode;\n actions?: ReactNode;\n children: ReactNode;\n /** basePath-relative path for the header back button (e.g. \"datasets\"). */\n backPath?: string;\n}\n\n/**\n * Standard page scaffold: a sticky-feeling header (optional back button, title,\n * description, right-aligned actions) over a scrollable content region. Token\n * styling only.\n */\nexport function PageLayout({ title, description, actions, children, backPath }: PageLayoutProps) {\n const { navigate } = useCosmicIntersection();\n\n return (\n <div className={cn('flex h-full flex-col bg-bg-surface')}>\n <div\n className={cn(\n 'flex items-start justify-between gap-4 border-b border-border-seam bg-bg-surface px-6 py-5'\n )}\n >\n <div className=\"flex min-w-0 flex-col gap-1\">\n {backPath ? (\n <button\n type=\"button\"\n onClick={() => navigate(backPath)}\n className={cn(\n 'flex w-fit items-center gap-1 rounded-button text-sm text-text-secondary transition-colors hover:text-text-primary focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border-focus'\n )}\n >\n <ChevronLeft className=\"size-4\" aria-hidden=\"true\" />\n Back\n </button>\n ) : null}\n <h1 className={cn('truncate text-2xl font-semibold text-text-primary')}>{title}</h1>\n {description ? (\n <p className={cn('mt-0.5 text-sm text-text-secondary')}>{description}</p>\n ) : null}\n </div>\n {actions ? <div className=\"flex shrink-0 items-center gap-2\">{actions}</div> : null}\n </div>\n\n <div className={cn('flex-1 overflow-auto p-6')}>{children}</div>\n </div>\n );\n}\n\nexport default PageLayout;\n"],"mappings":";;;;;AAmBA,SAAgB,EAAW,EAAE,UAAO,gBAAa,YAAS,aAAU,eAA6B;CAC/F,IAAM,EAAE,gBAAa,GAAuB;AAE5C,QACE,kBAAC,OAAD;EAAK,WAAW,EAAG,qCAAqC;YAAxD,CACE,kBAAC,OAAD;GACE,WAAW,EACT,6FACD;aAHH,CAKE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACG,IACC,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAS,EAAS;MACjC,WAAW,EACT,qMACD;gBALH,CAOE,kBAAC,GAAD;OAAa,WAAU;OAAS,eAAY;OAAS,CAAA,EAAA,OAE9C;UACP;KACJ,kBAAC,MAAD;MAAI,WAAW,EAAG,oDAAoD;gBAAG;MAAW,CAAA;KACnF,IACC,kBAAC,KAAD;MAAG,WAAW,EAAG,qCAAqC;gBAAG;MAAgB,CAAA,GACvE;KACA;OACL,IAAU,kBAAC,OAAD;IAAK,WAAU;cAAoC;IAAc,CAAA,GAAG,KAC3E;MAEN,kBAAC,OAAD;GAAK,WAAW,EAAG,2BAA2B;GAAG;GAAe,CAAA,CAC5D"}
1
+ {"version":3,"file":"PageLayout.js","names":[],"sources":["../../../src/cosmicintersection/components/PageLayout.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { ChevronLeft } from 'lucide-react';\nimport { useCosmicIntersection } from '../context/CosmicIntersectionContext';\nimport { cn } from '../utils/cn';\n\ninterface PageLayoutProps {\n title: ReactNode;\n description?: ReactNode;\n actions?: ReactNode;\n children: ReactNode;\n /** basePath-relative path for the header back button (e.g. \"datasets\"). */\n backPath?: string;\n}\n\n/**\n * Standard page scaffold: a sticky-feeling header (optional back button, title,\n * description, right-aligned actions) over a scrollable content region. Token\n * styling only.\n */\nexport function PageLayout({ title, description, actions, children, backPath }: PageLayoutProps) {\n const { navigate } = useCosmicIntersection();\n\n return (\n <div className={cn('flex h-full flex-col bg-bg-surface')}>\n <div\n className={cn(\n 'flex items-start justify-between gap-4 border-b border-border-seam bg-bg-surface px-6 py-5'\n )}\n >\n <div className=\"flex min-w-0 flex-col gap-1\">\n {backPath ? (\n <button\n type=\"button\"\n onClick={() => navigate(backPath)}\n className={cn(\n 'flex w-fit items-center gap-1 rounded-button text-sm text-text-secondary transition-colors hover:text-text-primary focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border-focus'\n )}\n >\n <ChevronLeft className=\"size-4\" aria-hidden=\"true\" />\n Back\n </button>\n ) : null}\n <h1 className={cn('truncate text-2xl font-semibold text-text-primary')}>{title}</h1>\n {description ? (\n <p className={cn('mt-0.5 text-sm text-text-secondary')}>{description}</p>\n ) : null}\n </div>\n {actions ? <div className=\"flex shrink-0 items-center gap-2\">{actions}</div> : null}\n </div>\n\n <div className={cn('flex-1 overflow-auto p-6')}>{children}</div>\n </div>\n );\n}\n\nexport default PageLayout;\n"],"mappings":";;;;;AAmBA,SAAgB,EAAW,EAAE,UAAO,gBAAa,YAAS,aAAU,eAA6B;CAC/F,IAAM,EAAE,gBAAa,EAAsB;CAE3C,OACE,kBAAC,OAAD;EAAK,WAAW,EAAG,oCAAoC;YAAvD,CACE,kBAAC,OAAD;GACE,WAAW,EACT,4FACF;aAHF,CAKE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACG,IACC,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAS,CAAQ;MAChC,WAAW,EACT,oMACF;gBALF,CAOE,kBAAC,GAAD;OAAa,WAAU;OAAS,eAAY;MAAQ,CAAA,GAAC,MAE/C;UACN;KACJ,kBAAC,MAAD;MAAI,WAAW,EAAG,mDAAmD;gBAAI;KAAU,CAAA;KAClF,IACC,kBAAC,KAAD;MAAG,WAAW,EAAG,oCAAoC;gBAAI;KAAe,CAAA,IACtE;IACD;OACJ,IAAU,kBAAC,OAAD;IAAK,WAAU;cAAoC;GAAa,CAAA,IAAI,IAC5E;MAEL,kBAAC,OAAD;GAAK,WAAW,EAAG,0BAA0B;GAAI;EAAc,CAAA,CAC5D;;AAET"}
@@ -1 +1 @@
1
- {"version":3,"file":"Pagination.js","names":[],"sources":["../../../src/cosmicintersection/components/Pagination.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { ChevronLeft, ChevronRight } from 'lucide-react';\n\n/**\n * Offset/limit pagination for the CosmicIntersection list surfaces.\n *\n * The Cos* connection resolvers return `{ items, pageInfo { hasMore, cursor } }`\n * with NO total count, so paging is driven by `offset`/`limit` + the boolean\n * `hasMore` rather than a computable page count. `usePagination` owns the window;\n * the page feeds `limit`/`offset` into `PaginationInput`, then renders\n * `<Pagination>` with `hasMore` so Next gates correctly.\n */\n\nconst DEFAULT_PAGE_SIZE = 50;\n\nexport interface UsePaginationOptions {\n pageSize?: number;\n /** When this changes (a filter/search term), the window resets to page 1. */\n resetKey?: unknown;\n}\n\nexport interface UsePaginationResult {\n limit: number;\n offset: number;\n /** 1-based page index derived from offset/limit. */\n page: number;\n nextPage: () => void;\n prevPage: () => void;\n reset: () => void;\n}\n\nexport function usePagination(options: UsePaginationOptions = {}): UsePaginationResult {\n const { pageSize = DEFAULT_PAGE_SIZE, resetKey } = options;\n const [offset, setOffset] = useState(0);\n\n // Reset to the first page whenever resetKey changes (React's documented\n // derive-state-during-render pattern — no extra effect/render pass).\n const [prevResetKey, setPrevResetKey] = useState(resetKey);\n if (prevResetKey !== resetKey) {\n setPrevResetKey(resetKey);\n if (offset !== 0) {\n setOffset(0);\n }\n }\n\n return {\n limit: pageSize,\n offset,\n page: Math.floor(offset / Math.max(1, pageSize)) + 1,\n nextPage: () => setOffset((current) => current + pageSize),\n prevPage: () => setOffset((current) => Math.max(0, current - pageSize)),\n reset: () => setOffset(0),\n };\n}\n\nexport interface PaginationProps {\n offset: number;\n limit: number;\n /** Number of rows returned for the current page (`items.length`). */\n loadedCount: number;\n /** Whether the backend reports more rows after this page (`pageInfo.hasMore`). */\n hasMore: boolean;\n onPrev: () => void;\n onNext: () => void;\n /** Plural noun for the range label, e.g. \"datasets\". Defaults to \"items\". */\n itemLabel?: string;\n loading?: boolean;\n className?: string;\n}\n\nexport function Pagination({\n offset,\n limit,\n loadedCount,\n hasMore,\n onPrev,\n onNext,\n itemLabel = 'items',\n loading = false,\n className,\n}: PaginationProps) {\n // Nothing to page through: first page and no more results.\n if (offset === 0 && !hasMore && loadedCount === 0) {\n return null;\n }\n\n const rangeStart = loadedCount === 0 ? 0 : offset + 1;\n const rangeEnd = offset + loadedCount;\n const canPrev = offset > 0;\n const canNext = hasMore;\n const page = Math.floor(offset / Math.max(1, limit)) + 1;\n\n return (\n <div\n className={`flex flex-wrap items-center justify-between gap-3 pt-4 ${className ?? ''}`}\n role=\"navigation\"\n aria-label=\"Pagination\"\n >\n <p className=\"text-xs tabular-nums text-text-secondary\">\n Showing {rangeStart}–{rangeEnd} {itemLabel}\n </p>\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs tabular-nums text-text-secondary\">Page {page}</span>\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={onPrev}\n disabled={!canPrev || loading}\n aria-label=\"Previous page\"\n >\n <ChevronLeft className=\"size-4\" />\n Prev\n </Button>\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={onNext}\n disabled={!canNext || loading}\n aria-label=\"Next page\"\n >\n Next\n <ChevronRight className=\"size-4\" />\n </Button>\n </div>\n </div>\n );\n}\n\nexport default Pagination;\n"],"mappings":";;;;;AAcA,IAAM,IAAoB;AAkB1B,SAAgB,EAAc,IAAgC,EAAE,EAAuB;CACrF,IAAM,EAAE,cAAW,GAAmB,gBAAa,GAC7C,CAAC,GAAQ,KAAa,EAAS,EAAE,EAIjC,CAAC,GAAc,KAAmB,EAAS,EAAS;AAQ1D,QAPI,MAAiB,MACnB,EAAgB,EAAS,EACrB,MAAW,KACb,EAAU,EAAE,GAIT;EACL,OAAO;EACP;EACA,MAAM,KAAK,MAAM,IAAS,KAAK,IAAI,GAAG,EAAS,CAAC,GAAG;EACnD,gBAAgB,GAAW,MAAY,IAAU,EAAS;EAC1D,gBAAgB,GAAW,MAAY,KAAK,IAAI,GAAG,IAAU,EAAS,CAAC;EACvE,aAAa,EAAU,EAAE;EAC1B;;AAkBH,SAAgB,EAAW,EACzB,WACA,UACA,gBACA,YACA,WACA,WACA,eAAY,SACZ,aAAU,IACV,gBACkB;AAElB,KAAI,MAAW,KAAK,CAAC,KAAW,MAAgB,EAC9C,QAAO;CAGT,IAAM,IAAa,MAAgB,IAAI,IAAI,IAAS,GAC9C,IAAW,IAAS,GACpB,IAAU,IAAS,GACnB,IAAU,GACV,IAAO,KAAK,MAAM,IAAS,KAAK,IAAI,GAAG,EAAM,CAAC,GAAG;AAEvD,QACE,kBAAC,OAAD;EACE,WAAW,0DAA0D,KAAa;EAClF,MAAK;EACL,cAAW;YAHb,CAKE,kBAAC,KAAD;GAAG,WAAU;aAAb;IAAwD;IAC7C;IAAW;IAAE;IAAS;IAAE;IAC/B;MACJ,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,QAAD;KAAM,WAAU;eAAhB,CAA2D,SAAM,EAAY;;IAC7E,kBAAC,GAAD;KACE,MAAK;KACL,SAAQ;KACR,SAAS;KACT,UAAU,CAAC,KAAW;KACtB,cAAW;eALb,CAOE,kBAAC,GAAD,EAAa,WAAU,UAAW,CAAA,EAAA,OAE3B;;IACT,kBAAC,GAAD;KACE,MAAK;KACL,SAAQ;KACR,SAAS;KACT,UAAU,CAAC,KAAW;KACtB,cAAW;eALb,CAMC,QAEC,kBAAC,GAAD,EAAc,WAAU,UAAW,CAAA,CAC5B;;IACL;KACF"}
1
+ {"version":3,"file":"Pagination.js","names":[],"sources":["../../../src/cosmicintersection/components/Pagination.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { ChevronLeft, ChevronRight } from 'lucide-react';\n\n/**\n * Offset/limit pagination for the CosmicIntersection list surfaces.\n *\n * The Cos* connection resolvers return `{ items, pageInfo { hasMore, cursor } }`\n * with NO total count, so paging is driven by `offset`/`limit` + the boolean\n * `hasMore` rather than a computable page count. `usePagination` owns the window;\n * the page feeds `limit`/`offset` into `PaginationInput`, then renders\n * `<Pagination>` with `hasMore` so Next gates correctly.\n */\n\nconst DEFAULT_PAGE_SIZE = 50;\n\nexport interface UsePaginationOptions {\n pageSize?: number;\n /** When this changes (a filter/search term), the window resets to page 1. */\n resetKey?: unknown;\n}\n\nexport interface UsePaginationResult {\n limit: number;\n offset: number;\n /** 1-based page index derived from offset/limit. */\n page: number;\n nextPage: () => void;\n prevPage: () => void;\n reset: () => void;\n}\n\nexport function usePagination(options: UsePaginationOptions = {}): UsePaginationResult {\n const { pageSize = DEFAULT_PAGE_SIZE, resetKey } = options;\n const [offset, setOffset] = useState(0);\n\n // Reset to the first page whenever resetKey changes (React's documented\n // derive-state-during-render pattern — no extra effect/render pass).\n const [prevResetKey, setPrevResetKey] = useState(resetKey);\n if (prevResetKey !== resetKey) {\n setPrevResetKey(resetKey);\n if (offset !== 0) {\n setOffset(0);\n }\n }\n\n return {\n limit: pageSize,\n offset,\n page: Math.floor(offset / Math.max(1, pageSize)) + 1,\n nextPage: () => setOffset((current) => current + pageSize),\n prevPage: () => setOffset((current) => Math.max(0, current - pageSize)),\n reset: () => setOffset(0),\n };\n}\n\nexport interface PaginationProps {\n offset: number;\n limit: number;\n /** Number of rows returned for the current page (`items.length`). */\n loadedCount: number;\n /** Whether the backend reports more rows after this page (`pageInfo.hasMore`). */\n hasMore: boolean;\n onPrev: () => void;\n onNext: () => void;\n /** Plural noun for the range label, e.g. \"datasets\". Defaults to \"items\". */\n itemLabel?: string;\n loading?: boolean;\n className?: string;\n}\n\nexport function Pagination({\n offset,\n limit,\n loadedCount,\n hasMore,\n onPrev,\n onNext,\n itemLabel = 'items',\n loading = false,\n className,\n}: PaginationProps) {\n // Nothing to page through: first page and no more results.\n if (offset === 0 && !hasMore && loadedCount === 0) {\n return null;\n }\n\n const rangeStart = loadedCount === 0 ? 0 : offset + 1;\n const rangeEnd = offset + loadedCount;\n const canPrev = offset > 0;\n const canNext = hasMore;\n const page = Math.floor(offset / Math.max(1, limit)) + 1;\n\n return (\n <div\n className={`flex flex-wrap items-center justify-between gap-3 pt-4 ${className ?? ''}`}\n role=\"navigation\"\n aria-label=\"Pagination\"\n >\n <p className=\"text-xs tabular-nums text-text-secondary\">\n Showing {rangeStart}–{rangeEnd} {itemLabel}\n </p>\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs tabular-nums text-text-secondary\">Page {page}</span>\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={onPrev}\n disabled={!canPrev || loading}\n aria-label=\"Previous page\"\n >\n <ChevronLeft className=\"size-4\" />\n Prev\n </Button>\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={onNext}\n disabled={!canNext || loading}\n aria-label=\"Next page\"\n >\n Next\n <ChevronRight className=\"size-4\" />\n </Button>\n </div>\n </div>\n );\n}\n\nexport default Pagination;\n"],"mappings":";;;;;AAcA,IAAM,IAAoB;AAkB1B,SAAgB,EAAc,IAAgC,CAAC,GAAwB;CACrF,IAAM,EAAE,cAAW,GAAmB,gBAAa,GAC7C,CAAC,GAAQ,KAAa,EAAS,CAAC,GAIhC,CAAC,GAAc,KAAmB,EAAS,CAAQ;CAQzD,OAPI,MAAiB,MACnB,EAAgB,CAAQ,GACpB,MAAW,KACb,EAAU,CAAC,IAIR;EACL,OAAO;EACP;EACA,MAAM,KAAK,MAAM,IAAS,KAAK,IAAI,GAAG,CAAQ,CAAC,IAAI;EACnD,gBAAgB,GAAW,MAAY,IAAU,CAAQ;EACzD,gBAAgB,GAAW,MAAY,KAAK,IAAI,GAAG,IAAU,CAAQ,CAAC;EACtE,aAAa,EAAU,CAAC;CAC1B;AACF;AAiBA,SAAgB,EAAW,EACzB,WACA,UACA,gBACA,YACA,WACA,WACA,eAAY,SACZ,aAAU,IACV,gBACkB;CAElB,IAAI,MAAW,KAAK,CAAC,KAAW,MAAgB,GAC9C,OAAO;CAGT,IAAM,IAAa,MAAgB,IAAI,IAAI,IAAS,GAC9C,IAAW,IAAS,GACpB,IAAU,IAAS,GACnB,IAAU,GACV,IAAO,KAAK,MAAM,IAAS,KAAK,IAAI,GAAG,CAAK,CAAC,IAAI;CAEvD,OACE,kBAAC,OAAD;EACE,WAAW,0DAA0D,KAAa;EAClF,MAAK;EACL,cAAW;YAHb,CAKE,kBAAC,KAAD;GAAG,WAAU;aAAb;IAAwD;IAC7C;IAAW;IAAE;IAAS;IAAE;GAChC;MACH,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,QAAD;KAAM,WAAU;eAAhB,CAA2D,SAAM,CAAW;;IAC5E,kBAAC,GAAD;KACE,MAAK;KACL,SAAQ;KACR,SAAS;KACT,UAAU,CAAC,KAAW;KACtB,cAAW;eALb,CAOE,kBAAC,GAAD,EAAa,WAAU,SAAU,CAAA,GAAC,MAE5B;;IACR,kBAAC,GAAD;KACE,MAAK;KACL,SAAQ;KACR,SAAS;KACT,UAAU,CAAC,KAAW;KACtB,cAAW;eALb,CAMC,QAEC,kBAAC,GAAD,EAAc,WAAU,SAAU,CAAA,CAC5B;;GACL;IACF;;AAET"}
@@ -1 +1 @@
1
- {"version":3,"file":"ResultsChart.js","names":[],"sources":["../../../src/cosmicintersection/components/ResultsChart.tsx"],"sourcesContent":["/**\n * ResultsChart — inline visualization for a completed analysis' `results` JSON.\n *\n * There is NO chart library here: axes, gridlines, scatter dots and line series\n * are hand-drawn as SVG using the design system's dataviz tokens\n * (`var(--color-chart-*)`, `var(--color-chart-axis)`, `var(--color-chart-grid)`)\n * plus the standard text tokens — never a raw palette colour. The SVG uses a\n * `viewBox` and `width:100%`, so it scales fluidly on phones (no horizontal\n * scroll).\n *\n * The `results` payload is free-form JSON (whatever `cosCompleteAnalysis` stored),\n * so the component sniffs it for numeric series. Accepted shapes:\n * - `results.series: Array<{ name?, kind?: 'line'|'scatter', points }>`\n * - `results.points` (scatter) and/or `results.fit` / `results.model` (line)\n * - any top-level numeric arrays (rendered as line series)\n * where a `points` array may hold `{x,y}` objects, `[x,y]` pairs, `{value}` /\n * `{y}` objects, or bare numbers (index used as x).\n *\n * When nothing chartable is found, a compact, responsive key/value metrics list\n * is rendered instead. Everything comes from the real payload — nothing is\n * fabricated; an empty payload renders an honest \"no results\" message.\n */\nimport { useMemo } from 'react';\n\nimport { cn } from '../utils/cn';\n\ntype Json = Record<string, unknown>;\n\ninterface Point {\n x: number;\n y: number;\n}\n\ntype SeriesKind = 'line' | 'scatter';\n\ninterface Series {\n name: string;\n kind: SeriesKind;\n points: Point[];\n colorVar: string;\n}\n\nconst SERIES_PALETTE = [1, 2, 3, 4, 5, 6].map((n) => `var(--color-chart-${n})`);\n\nconst MAX_POINTS_PER_SERIES = 600;\n\n// ---------------------------------------------------------------------------\n// Payload parsing\n// ---------------------------------------------------------------------------\n\nfunction isFiniteNumber(value: unknown): value is number {\n return typeof value === 'number' && Number.isFinite(value);\n}\n\nfunction humanize(key: string): string {\n const spaced = key\n .replace(/[_-]+/g, ' ')\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .trim();\n if (!spaced) {\n return key;\n }\n return spaced.charAt(0).toUpperCase() + spaced.slice(1);\n}\n\n/** Coerce a single raw element into a {x,y} point (index is the fallback x). */\nfunction toPoint(element: unknown, index: number): Point | null {\n if (isFiniteNumber(element)) {\n return { x: index, y: element };\n }\n if (Array.isArray(element)) {\n const [a, b] = element;\n if (isFiniteNumber(a) && isFiniteNumber(b)) {\n return { x: a, y: b };\n }\n if (isFiniteNumber(a) && b === undefined) {\n return { x: index, y: a };\n }\n return null;\n }\n if (element && typeof element === 'object') {\n const obj = element as Record<string, unknown>;\n const yRaw = obj.y ?? obj.value ?? obj.val ?? obj.count;\n if (!isFiniteNumber(yRaw)) {\n return null;\n }\n const xRaw = obj.x ?? obj.t ?? obj.time ?? obj.index ?? obj.bin;\n return { x: isFiniteNumber(xRaw) ? xRaw : index, y: yRaw };\n }\n return null;\n}\n\n/** Map an array to points, dropping unparseable elements. */\nfunction pointsFrom(raw: unknown[]): Point[] {\n const points: Point[] = [];\n for (let i = 0; i < raw.length; i += 1) {\n const point = toPoint(raw[i], i);\n if (point) {\n points.push(point);\n }\n }\n return points;\n}\n\n/** Evenly downsample a long series so the SVG stays light on huge payloads. */\nfunction sample(points: Point[]): Point[] {\n if (points.length <= MAX_POINTS_PER_SERIES) {\n return points;\n }\n const stride = points.length / MAX_POINTS_PER_SERIES;\n const out: Point[] = [];\n for (let i = 0; i < MAX_POINTS_PER_SERIES; i += 1) {\n out.push(points[Math.floor(i * stride)]);\n }\n return out;\n}\n\nfunction kindFrom(raw: string): SeriesKind | undefined {\n if (/scatter|point|dot|observ|raw/i.test(raw)) {\n return 'scatter';\n }\n if (/line|fit|curve|model|trend|series/i.test(raw)) {\n return 'line';\n }\n return undefined;\n}\n\n/** Sniff the payload for one or more numeric series (empty = not chartable). */\nfunction extractSeries(results: Json): Series[] {\n const out: Series[] = [];\n let colorIndex = 0;\n const nextColor = (): string => {\n const color = SERIES_PALETTE[colorIndex % SERIES_PALETTE.length];\n colorIndex += 1;\n return color;\n };\n const push = (raw: unknown[], name: string, kind: SeriesKind): void => {\n const points = sample(pointsFrom(raw));\n if (points.length >= 2) {\n out.push({ name, kind, points, colorVar: nextColor() });\n }\n };\n\n // 1. Explicit `series` array — the richest shape.\n const series = results.series;\n if (Array.isArray(series)) {\n series.forEach((entry, i) => {\n if (Array.isArray(entry)) {\n push(entry, `Series ${i + 1}`, 'line');\n return;\n }\n if (entry && typeof entry === 'object') {\n const so = entry as Record<string, unknown>;\n const data = so.points ?? so.data ?? so.values;\n if (!Array.isArray(data)) {\n return;\n }\n const name =\n typeof so.name === 'string' && so.name.trim()\n ? so.name\n : typeof so.label === 'string' && so.label.trim()\n ? so.label\n : `Series ${i + 1}`;\n const kindRaw = [so.kind, so.type, so.name, so.label]\n .filter((v): v is string => typeof v === 'string')\n .join(' ');\n push(data, name, kindFrom(kindRaw) ?? 'line');\n }\n });\n }\n\n // 2. `points` (scatter) plus optional `fit` / `model` overlay line.\n if (out.length === 0) {\n if (Array.isArray(results.points)) {\n push(results.points, 'Observations', 'scatter');\n } else if (Array.isArray(results.data)) {\n push(results.data, 'Data', 'line');\n } else if (Array.isArray(results.values)) {\n push(results.values, 'Values', 'line');\n }\n if (Array.isArray(results.fit)) {\n push(results.fit, 'Fit', 'line');\n }\n if (Array.isArray(results.model)) {\n push(results.model, 'Model', 'line');\n }\n }\n\n // 3. Scan for the first few bare numeric arrays (e.g. { loss, accuracy }).\n if (out.length === 0) {\n for (const [key, value] of Object.entries(results)) {\n if (Array.isArray(value)) {\n push(value, humanize(key), 'line');\n }\n if (out.length >= SERIES_PALETTE.length) {\n break;\n }\n }\n }\n\n return out;\n}\n\nfunction readLabel(results: Json, keys: string[], axisKey: 'x' | 'y', fallback: string): string {\n for (const key of keys) {\n const value = results[key];\n if (typeof value === 'string' && value.trim()) {\n return value;\n }\n }\n const axes = results.axes ?? results.axis;\n if (axes && typeof axes === 'object') {\n const value = (axes as Record<string, unknown>)[axisKey];\n if (typeof value === 'string' && value.trim()) {\n return value;\n }\n }\n return fallback;\n}\n\n// ---------------------------------------------------------------------------\n// Axis math\n// ---------------------------------------------------------------------------\n\n/** Rounded, human-friendly tick values across [min, max]. */\nfunction niceTicks(min: number, max: number, count: number): number[] {\n if (!Number.isFinite(min) || !Number.isFinite(max) || min === max) {\n return [min];\n }\n const span = max - min;\n const rawStep = span / Math.max(1, count);\n const mag = Math.pow(10, Math.floor(Math.log10(rawStep)));\n const norm = rawStep / mag;\n const niceNorm = norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10;\n const step = niceNorm * mag;\n const start = Math.floor(min / step) * step;\n const end = Math.ceil(max / step) * step;\n const ticks: number[] = [];\n for (let v = start; v <= end + step * 1e-6; v += step) {\n ticks.push(Number(v.toFixed(12)));\n }\n return ticks;\n}\n\nfunction formatTick(value: number): string {\n if (value === 0) {\n return '0';\n }\n const abs = Math.abs(value);\n if (abs < 1e-3 || abs >= 1e6) {\n return value.toExponential(1);\n }\n const rounded = Number(value.toPrecision(4));\n return rounded.toLocaleString(undefined, { maximumFractionDigits: 4 });\n}\n\n// ---------------------------------------------------------------------------\n// Chart geometry\n// ---------------------------------------------------------------------------\n\nconst VIEW_W = 640;\nconst VIEW_H = 340;\nconst MARGIN = { top: 16, right: 20, bottom: 48, left: 62 };\nconst INNER_W = VIEW_W - MARGIN.left - MARGIN.right;\nconst INNER_H = VIEW_H - MARGIN.top - MARGIN.bottom;\n\ninterface ChartModel {\n series: Series[];\n xTicks: number[];\n yTicks: number[];\n domainX: [number, number];\n domainY: [number, number];\n xLabel: string;\n yLabel: string;\n}\n\nfunction buildModel(results: Json): ChartModel | null {\n const series = extractSeries(results);\n if (series.length === 0) {\n return null;\n }\n\n const xs: number[] = [];\n const ys: number[] = [];\n for (const s of series) {\n for (const p of s.points) {\n xs.push(p.x);\n ys.push(p.y);\n }\n }\n\n let minX = Math.min(...xs);\n let maxX = Math.max(...xs);\n let minY = Math.min(...ys);\n let maxY = Math.max(...ys);\n\n if (minX === maxX) {\n const pad = Math.abs(minX) || 1;\n minX -= pad;\n maxX += pad;\n }\n if (minY === maxY) {\n const pad = Math.abs(minY) || 1;\n minY -= pad;\n maxY += pad;\n }\n // Breathing room above/below the data so markers are not clipped.\n const yPad = (maxY - minY) * 0.08;\n minY -= yPad;\n maxY += yPad;\n\n const xTicks = niceTicks(minX, maxX, 5);\n const yTicks = niceTicks(minY, maxY, 5);\n const domainX: [number, number] = [\n Math.min(minX, xTicks[0]),\n Math.max(maxX, xTicks[xTicks.length - 1]),\n ];\n const domainY: [number, number] = [\n Math.min(minY, yTicks[0]),\n Math.max(maxY, yTicks[yTicks.length - 1]),\n ];\n\n return {\n series,\n xTicks: xTicks.filter((t) => t >= domainX[0] && t <= domainX[1]),\n yTicks: yTicks.filter((t) => t >= domainY[0] && t <= domainY[1]),\n domainX,\n domainY,\n xLabel: readLabel(results, ['xLabel', 'xlabel', 'xAxis', 'xaxis', 'xTitle', 'xUnit'], 'x', 'x'),\n yLabel: readLabel(results, ['yLabel', 'ylabel', 'yAxis', 'yaxis', 'yTitle', 'yUnit'], 'y', 'y'),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Fallback key/value table\n// ---------------------------------------------------------------------------\n\nfunction displayValue(value: unknown): string {\n if (value === null || value === undefined) {\n return '—';\n }\n if (typeof value === 'number') {\n return value.toLocaleString(undefined, { maximumFractionDigits: 6 });\n }\n if (typeof value === 'boolean') {\n return value ? 'Yes' : 'No';\n }\n if (typeof value === 'string') {\n return value;\n }\n if (Array.isArray(value)) {\n return `${value.length} item${value.length === 1 ? '' : 's'}`;\n }\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n}\n\nfunction MetricsTable({ results }: { results: Json }) {\n const entries = Object.entries(results);\n if (entries.length === 0) {\n return (\n <p className=\"rounded-card border border-border-default bg-bg-sunken px-4 py-6 text-center text-sm text-text-secondary\">\n No structured results were recorded for this analysis.\n </p>\n );\n }\n return (\n <dl className=\"grid grid-cols-1 gap-px overflow-hidden rounded-card border border-border-default bg-border-subtle sm:grid-cols-2\">\n {entries.map(([key, value]) => {\n const text = displayValue(value);\n return (\n <div\n key={key}\n className=\"flex items-baseline justify-between gap-3 bg-bg-elevated px-3 py-2\"\n >\n <dt className=\"min-w-0 truncate text-xs font-medium uppercase tracking-wide text-text-secondary\">\n {humanize(key)}\n </dt>\n <dd\n className=\"max-w-[60%] truncate text-right text-sm tabular-nums text-text-primary\"\n title={text}\n >\n {text}\n </dd>\n </div>\n );\n })}\n </dl>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\nexport interface ResultsChartProps {\n results: Record<string, unknown>;\n className?: string;\n}\n\nexport function ResultsChart({ results, className }: ResultsChartProps) {\n const model = useMemo(() => buildModel(results), [results]);\n\n if (!model) {\n return (\n <div className={className}>\n <MetricsTable results={results} />\n </div>\n );\n }\n\n const { series, xTicks, yTicks, domainX, domainY, xLabel, yLabel } = model;\n\n const sx = (x: number): number =>\n MARGIN.left + ((x - domainX[0]) / (domainX[1] - domainX[0])) * INNER_W;\n const sy = (y: number): number =>\n MARGIN.top + INNER_H - ((y - domainY[0]) / (domainY[1] - domainY[0])) * INNER_H;\n\n const showLegend = series.length > 1;\n const ariaLabel = `Chart of ${series\n .map((s) => s.name)\n .join(', ')}; x-axis ${xLabel}, y-axis ${yLabel}`;\n\n return (\n <div className={cn('space-y-3', className)}>\n <div className=\"rounded-card border border-border-default bg-bg-sunken p-3\">\n <svg\n viewBox={`0 0 ${VIEW_W} ${VIEW_H}`}\n className=\"h-auto w-full\"\n preserveAspectRatio=\"xMidYMid meet\"\n role=\"img\"\n aria-label={ariaLabel}\n >\n {/* horizontal gridlines + y tick labels */}\n {yTicks.map((tick) => {\n const y = sy(tick);\n return (\n <g key={`y-${tick}`}>\n <line\n x1={MARGIN.left}\n x2={MARGIN.left + INNER_W}\n y1={y}\n y2={y}\n stroke=\"var(--color-chart-grid)\"\n strokeWidth={1}\n />\n <text\n x={MARGIN.left - 8}\n y={y}\n textAnchor=\"end\"\n dominantBaseline=\"central\"\n fontSize={11}\n fill=\"var(--color-text-tertiary)\"\n >\n {formatTick(tick)}\n </text>\n </g>\n );\n })}\n\n {/* vertical gridlines + x tick labels */}\n {xTicks.map((tick) => {\n const x = sx(tick);\n return (\n <g key={`x-${tick}`}>\n <line\n x1={x}\n x2={x}\n y1={MARGIN.top}\n y2={MARGIN.top + INNER_H}\n stroke=\"var(--color-chart-grid)\"\n strokeWidth={1}\n strokeDasharray=\"2 4\"\n />\n <text\n x={x}\n y={MARGIN.top + INNER_H + 18}\n textAnchor=\"middle\"\n fontSize={11}\n fill=\"var(--color-text-tertiary)\"\n >\n {formatTick(tick)}\n </text>\n </g>\n );\n })}\n\n {/* plot frame */}\n <rect\n x={MARGIN.left}\n y={MARGIN.top}\n width={INNER_W}\n height={INNER_H}\n fill=\"none\"\n stroke=\"var(--color-chart-axis)\"\n strokeWidth={1}\n />\n\n {/* series */}\n {series.map((s) => {\n const ordered = [...s.points].sort((a, b) => a.x - b.x);\n const path = ordered\n .map((p, i) => `${i === 0 ? 'M' : 'L'} ${sx(p.x).toFixed(2)} ${sy(p.y).toFixed(2)}`)\n .join(' ');\n const showDots = s.kind === 'scatter' || s.points.length <= 40;\n return (\n <g key={s.name}>\n {s.kind === 'line' ? (\n <path\n d={path}\n fill=\"none\"\n stroke={s.colorVar}\n strokeWidth={2}\n strokeLinejoin=\"round\"\n strokeLinecap=\"round\"\n />\n ) : null}\n {showDots\n ? s.points.map((p, i) => (\n <circle\n key={`${s.name}-${i}`}\n cx={sx(p.x)}\n cy={sy(p.y)}\n r={s.kind === 'scatter' ? 2.4 : 2.8}\n fill={s.colorVar}\n fillOpacity={s.kind === 'scatter' ? 0.85 : 1}\n />\n ))\n : null}\n </g>\n );\n })}\n\n {/* axis titles */}\n <text\n x={MARGIN.left + INNER_W / 2}\n y={VIEW_H - 6}\n textAnchor=\"middle\"\n fontSize={12}\n fill=\"var(--color-text-secondary)\"\n >\n {xLabel}\n </text>\n <text\n x={14}\n y={MARGIN.top + INNER_H / 2}\n textAnchor=\"middle\"\n fontSize={12}\n fill=\"var(--color-text-secondary)\"\n transform={`rotate(-90 14 ${MARGIN.top + INNER_H / 2})`}\n >\n {yLabel}\n </text>\n </svg>\n </div>\n\n {showLegend ? (\n <ul className=\"flex flex-wrap gap-x-4 gap-y-1.5\">\n {series.map((s) => (\n <li key={s.name} className=\"flex items-center gap-1.5 text-xs text-text-secondary\">\n <span\n aria-hidden=\"true\"\n className=\"inline-block size-2.5 rounded-full\"\n style={{ backgroundColor: s.colorVar }}\n />\n <span className=\"truncate\">\n {s.name}\n <span className=\"ml-1 text-text-tertiary\">\n ({s.kind === 'scatter' ? 'points' : 'line'})\n </span>\n </span>\n </li>\n ))}\n </ul>\n ) : null}\n </div>\n );\n}\n\nexport default ResultsChart;\n"],"mappings":"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"}
1
+ {"version":3,"file":"ResultsChart.js","names":[],"sources":["../../../src/cosmicintersection/components/ResultsChart.tsx"],"sourcesContent":["/**\n * ResultsChart — inline visualization for a completed analysis' `results` JSON.\n *\n * There is NO chart library here: axes, gridlines, scatter dots and line series\n * are hand-drawn as SVG using the design system's dataviz tokens\n * (`var(--color-chart-*)`, `var(--color-chart-axis)`, `var(--color-chart-grid)`)\n * plus the standard text tokens — never a raw palette colour. The SVG uses a\n * `viewBox` and `width:100%`, so it scales fluidly on phones (no horizontal\n * scroll).\n *\n * The `results` payload is free-form JSON (whatever `cosCompleteAnalysis` stored),\n * so the component sniffs it for numeric series. Accepted shapes:\n * - `results.series: Array<{ name?, kind?: 'line'|'scatter', points }>`\n * - `results.points` (scatter) and/or `results.fit` / `results.model` (line)\n * - any top-level numeric arrays (rendered as line series)\n * where a `points` array may hold `{x,y}` objects, `[x,y]` pairs, `{value}` /\n * `{y}` objects, or bare numbers (index used as x).\n *\n * When nothing chartable is found, a compact, responsive key/value metrics list\n * is rendered instead. Everything comes from the real payload — nothing is\n * fabricated; an empty payload renders an honest \"no results\" message.\n */\nimport { useMemo } from 'react';\n\nimport { cn } from '../utils/cn';\n\ntype Json = Record<string, unknown>;\n\ninterface Point {\n x: number;\n y: number;\n}\n\ntype SeriesKind = 'line' | 'scatter';\n\ninterface Series {\n name: string;\n kind: SeriesKind;\n points: Point[];\n colorVar: string;\n}\n\nconst SERIES_PALETTE = [1, 2, 3, 4, 5, 6].map((n) => `var(--color-chart-${n})`);\n\nconst MAX_POINTS_PER_SERIES = 600;\n\n// ---------------------------------------------------------------------------\n// Payload parsing\n// ---------------------------------------------------------------------------\n\nfunction isFiniteNumber(value: unknown): value is number {\n return typeof value === 'number' && Number.isFinite(value);\n}\n\nfunction humanize(key: string): string {\n const spaced = key\n .replace(/[_-]+/g, ' ')\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .trim();\n if (!spaced) {\n return key;\n }\n return spaced.charAt(0).toUpperCase() + spaced.slice(1);\n}\n\n/** Coerce a single raw element into a {x,y} point (index is the fallback x). */\nfunction toPoint(element: unknown, index: number): Point | null {\n if (isFiniteNumber(element)) {\n return { x: index, y: element };\n }\n if (Array.isArray(element)) {\n const [a, b] = element;\n if (isFiniteNumber(a) && isFiniteNumber(b)) {\n return { x: a, y: b };\n }\n if (isFiniteNumber(a) && b === undefined) {\n return { x: index, y: a };\n }\n return null;\n }\n if (element && typeof element === 'object') {\n const obj = element as Record<string, unknown>;\n const yRaw = obj.y ?? obj.value ?? obj.val ?? obj.count;\n if (!isFiniteNumber(yRaw)) {\n return null;\n }\n const xRaw = obj.x ?? obj.t ?? obj.time ?? obj.index ?? obj.bin;\n return { x: isFiniteNumber(xRaw) ? xRaw : index, y: yRaw };\n }\n return null;\n}\n\n/** Map an array to points, dropping unparseable elements. */\nfunction pointsFrom(raw: unknown[]): Point[] {\n const points: Point[] = [];\n for (let i = 0; i < raw.length; i += 1) {\n const point = toPoint(raw[i], i);\n if (point) {\n points.push(point);\n }\n }\n return points;\n}\n\n/** Evenly downsample a long series so the SVG stays light on huge payloads. */\nfunction sample(points: Point[]): Point[] {\n if (points.length <= MAX_POINTS_PER_SERIES) {\n return points;\n }\n const stride = points.length / MAX_POINTS_PER_SERIES;\n const out: Point[] = [];\n for (let i = 0; i < MAX_POINTS_PER_SERIES; i += 1) {\n out.push(points[Math.floor(i * stride)]);\n }\n return out;\n}\n\nfunction kindFrom(raw: string): SeriesKind | undefined {\n if (/scatter|point|dot|observ|raw/i.test(raw)) {\n return 'scatter';\n }\n if (/line|fit|curve|model|trend|series/i.test(raw)) {\n return 'line';\n }\n return undefined;\n}\n\n/** Sniff the payload for one or more numeric series (empty = not chartable). */\nfunction extractSeries(results: Json): Series[] {\n const out: Series[] = [];\n let colorIndex = 0;\n const nextColor = (): string => {\n const color = SERIES_PALETTE[colorIndex % SERIES_PALETTE.length];\n colorIndex += 1;\n return color;\n };\n const push = (raw: unknown[], name: string, kind: SeriesKind): void => {\n const points = sample(pointsFrom(raw));\n if (points.length >= 2) {\n out.push({ name, kind, points, colorVar: nextColor() });\n }\n };\n\n // 1. Explicit `series` array — the richest shape.\n const series = results.series;\n if (Array.isArray(series)) {\n series.forEach((entry, i) => {\n if (Array.isArray(entry)) {\n push(entry, `Series ${i + 1}`, 'line');\n return;\n }\n if (entry && typeof entry === 'object') {\n const so = entry as Record<string, unknown>;\n const data = so.points ?? so.data ?? so.values;\n if (!Array.isArray(data)) {\n return;\n }\n const name =\n typeof so.name === 'string' && so.name.trim()\n ? so.name\n : typeof so.label === 'string' && so.label.trim()\n ? so.label\n : `Series ${i + 1}`;\n const kindRaw = [so.kind, so.type, so.name, so.label]\n .filter((v): v is string => typeof v === 'string')\n .join(' ');\n push(data, name, kindFrom(kindRaw) ?? 'line');\n }\n });\n }\n\n // 2. `points` (scatter) plus optional `fit` / `model` overlay line.\n if (out.length === 0) {\n if (Array.isArray(results.points)) {\n push(results.points, 'Observations', 'scatter');\n } else if (Array.isArray(results.data)) {\n push(results.data, 'Data', 'line');\n } else if (Array.isArray(results.values)) {\n push(results.values, 'Values', 'line');\n }\n if (Array.isArray(results.fit)) {\n push(results.fit, 'Fit', 'line');\n }\n if (Array.isArray(results.model)) {\n push(results.model, 'Model', 'line');\n }\n }\n\n // 3. Scan for the first few bare numeric arrays (e.g. { loss, accuracy }).\n if (out.length === 0) {\n for (const [key, value] of Object.entries(results)) {\n if (Array.isArray(value)) {\n push(value, humanize(key), 'line');\n }\n if (out.length >= SERIES_PALETTE.length) {\n break;\n }\n }\n }\n\n return out;\n}\n\nfunction readLabel(results: Json, keys: string[], axisKey: 'x' | 'y', fallback: string): string {\n for (const key of keys) {\n const value = results[key];\n if (typeof value === 'string' && value.trim()) {\n return value;\n }\n }\n const axes = results.axes ?? results.axis;\n if (axes && typeof axes === 'object') {\n const value = (axes as Record<string, unknown>)[axisKey];\n if (typeof value === 'string' && value.trim()) {\n return value;\n }\n }\n return fallback;\n}\n\n// ---------------------------------------------------------------------------\n// Axis math\n// ---------------------------------------------------------------------------\n\n/** Rounded, human-friendly tick values across [min, max]. */\nfunction niceTicks(min: number, max: number, count: number): number[] {\n if (!Number.isFinite(min) || !Number.isFinite(max) || min === max) {\n return [min];\n }\n const span = max - min;\n const rawStep = span / Math.max(1, count);\n const mag = Math.pow(10, Math.floor(Math.log10(rawStep)));\n const norm = rawStep / mag;\n const niceNorm = norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10;\n const step = niceNorm * mag;\n const start = Math.floor(min / step) * step;\n const end = Math.ceil(max / step) * step;\n const ticks: number[] = [];\n for (let v = start; v <= end + step * 1e-6; v += step) {\n ticks.push(Number(v.toFixed(12)));\n }\n return ticks;\n}\n\nfunction formatTick(value: number): string {\n if (value === 0) {\n return '0';\n }\n const abs = Math.abs(value);\n if (abs < 1e-3 || abs >= 1e6) {\n return value.toExponential(1);\n }\n const rounded = Number(value.toPrecision(4));\n return rounded.toLocaleString(undefined, { maximumFractionDigits: 4 });\n}\n\n// ---------------------------------------------------------------------------\n// Chart geometry\n// ---------------------------------------------------------------------------\n\nconst VIEW_W = 640;\nconst VIEW_H = 340;\nconst MARGIN = { top: 16, right: 20, bottom: 48, left: 62 };\nconst INNER_W = VIEW_W - MARGIN.left - MARGIN.right;\nconst INNER_H = VIEW_H - MARGIN.top - MARGIN.bottom;\n\ninterface ChartModel {\n series: Series[];\n xTicks: number[];\n yTicks: number[];\n domainX: [number, number];\n domainY: [number, number];\n xLabel: string;\n yLabel: string;\n}\n\nfunction buildModel(results: Json): ChartModel | null {\n const series = extractSeries(results);\n if (series.length === 0) {\n return null;\n }\n\n const xs: number[] = [];\n const ys: number[] = [];\n for (const s of series) {\n for (const p of s.points) {\n xs.push(p.x);\n ys.push(p.y);\n }\n }\n\n let minX = Math.min(...xs);\n let maxX = Math.max(...xs);\n let minY = Math.min(...ys);\n let maxY = Math.max(...ys);\n\n if (minX === maxX) {\n const pad = Math.abs(minX) || 1;\n minX -= pad;\n maxX += pad;\n }\n if (minY === maxY) {\n const pad = Math.abs(minY) || 1;\n minY -= pad;\n maxY += pad;\n }\n // Breathing room above/below the data so markers are not clipped.\n const yPad = (maxY - minY) * 0.08;\n minY -= yPad;\n maxY += yPad;\n\n const xTicks = niceTicks(minX, maxX, 5);\n const yTicks = niceTicks(minY, maxY, 5);\n const domainX: [number, number] = [\n Math.min(minX, xTicks[0]),\n Math.max(maxX, xTicks[xTicks.length - 1]),\n ];\n const domainY: [number, number] = [\n Math.min(minY, yTicks[0]),\n Math.max(maxY, yTicks[yTicks.length - 1]),\n ];\n\n return {\n series,\n xTicks: xTicks.filter((t) => t >= domainX[0] && t <= domainX[1]),\n yTicks: yTicks.filter((t) => t >= domainY[0] && t <= domainY[1]),\n domainX,\n domainY,\n xLabel: readLabel(results, ['xLabel', 'xlabel', 'xAxis', 'xaxis', 'xTitle', 'xUnit'], 'x', 'x'),\n yLabel: readLabel(results, ['yLabel', 'ylabel', 'yAxis', 'yaxis', 'yTitle', 'yUnit'], 'y', 'y'),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Fallback key/value table\n// ---------------------------------------------------------------------------\n\nfunction displayValue(value: unknown): string {\n if (value === null || value === undefined) {\n return '—';\n }\n if (typeof value === 'number') {\n return value.toLocaleString(undefined, { maximumFractionDigits: 6 });\n }\n if (typeof value === 'boolean') {\n return value ? 'Yes' : 'No';\n }\n if (typeof value === 'string') {\n return value;\n }\n if (Array.isArray(value)) {\n return `${value.length} item${value.length === 1 ? '' : 's'}`;\n }\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n}\n\nfunction MetricsTable({ results }: { results: Json }) {\n const entries = Object.entries(results);\n if (entries.length === 0) {\n return (\n <p className=\"rounded-card border border-border-default bg-bg-sunken px-4 py-6 text-center text-sm text-text-secondary\">\n No structured results were recorded for this analysis.\n </p>\n );\n }\n return (\n <dl className=\"grid grid-cols-1 gap-px overflow-hidden rounded-card border border-border-default bg-border-subtle sm:grid-cols-2\">\n {entries.map(([key, value]) => {\n const text = displayValue(value);\n return (\n <div\n key={key}\n className=\"flex items-baseline justify-between gap-3 bg-bg-elevated px-3 py-2\"\n >\n <dt className=\"min-w-0 truncate text-xs font-medium uppercase tracking-wide text-text-secondary\">\n {humanize(key)}\n </dt>\n <dd\n className=\"max-w-[60%] truncate text-right text-sm tabular-nums text-text-primary\"\n title={text}\n >\n {text}\n </dd>\n </div>\n );\n })}\n </dl>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\nexport interface ResultsChartProps {\n results: Record<string, unknown>;\n className?: string;\n}\n\nexport function ResultsChart({ results, className }: ResultsChartProps) {\n const model = useMemo(() => buildModel(results), [results]);\n\n if (!model) {\n return (\n <div className={className}>\n <MetricsTable results={results} />\n </div>\n );\n }\n\n const { series, xTicks, yTicks, domainX, domainY, xLabel, yLabel } = model;\n\n const sx = (x: number): number =>\n MARGIN.left + ((x - domainX[0]) / (domainX[1] - domainX[0])) * INNER_W;\n const sy = (y: number): number =>\n MARGIN.top + INNER_H - ((y - domainY[0]) / (domainY[1] - domainY[0])) * INNER_H;\n\n const showLegend = series.length > 1;\n const ariaLabel = `Chart of ${series\n .map((s) => s.name)\n .join(', ')}; x-axis ${xLabel}, y-axis ${yLabel}`;\n\n return (\n <div className={cn('space-y-3', className)}>\n <div className=\"rounded-card border border-border-default bg-bg-sunken p-3\">\n <svg\n viewBox={`0 0 ${VIEW_W} ${VIEW_H}`}\n className=\"h-auto w-full\"\n preserveAspectRatio=\"xMidYMid meet\"\n role=\"img\"\n aria-label={ariaLabel}\n >\n {/* horizontal gridlines + y tick labels */}\n {yTicks.map((tick) => {\n const y = sy(tick);\n return (\n <g key={`y-${tick}`}>\n <line\n x1={MARGIN.left}\n x2={MARGIN.left + INNER_W}\n y1={y}\n y2={y}\n stroke=\"var(--color-chart-grid)\"\n strokeWidth={1}\n />\n <text\n x={MARGIN.left - 8}\n y={y}\n textAnchor=\"end\"\n dominantBaseline=\"central\"\n fontSize={11}\n fill=\"var(--color-text-tertiary)\"\n >\n {formatTick(tick)}\n </text>\n </g>\n );\n })}\n\n {/* vertical gridlines + x tick labels */}\n {xTicks.map((tick) => {\n const x = sx(tick);\n return (\n <g key={`x-${tick}`}>\n <line\n x1={x}\n x2={x}\n y1={MARGIN.top}\n y2={MARGIN.top + INNER_H}\n stroke=\"var(--color-chart-grid)\"\n strokeWidth={1}\n strokeDasharray=\"2 4\"\n />\n <text\n x={x}\n y={MARGIN.top + INNER_H + 18}\n textAnchor=\"middle\"\n fontSize={11}\n fill=\"var(--color-text-tertiary)\"\n >\n {formatTick(tick)}\n </text>\n </g>\n );\n })}\n\n {/* plot frame */}\n <rect\n x={MARGIN.left}\n y={MARGIN.top}\n width={INNER_W}\n height={INNER_H}\n fill=\"none\"\n stroke=\"var(--color-chart-axis)\"\n strokeWidth={1}\n />\n\n {/* series */}\n {series.map((s) => {\n const ordered = [...s.points].sort((a, b) => a.x - b.x);\n const path = ordered\n .map((p, i) => `${i === 0 ? 'M' : 'L'} ${sx(p.x).toFixed(2)} ${sy(p.y).toFixed(2)}`)\n .join(' ');\n const showDots = s.kind === 'scatter' || s.points.length <= 40;\n return (\n <g key={s.name}>\n {s.kind === 'line' ? (\n <path\n d={path}\n fill=\"none\"\n stroke={s.colorVar}\n strokeWidth={2}\n strokeLinejoin=\"round\"\n strokeLinecap=\"round\"\n />\n ) : null}\n {showDots\n ? s.points.map((p, i) => (\n <circle\n key={`${s.name}-${i}`}\n cx={sx(p.x)}\n cy={sy(p.y)}\n r={s.kind === 'scatter' ? 2.4 : 2.8}\n fill={s.colorVar}\n fillOpacity={s.kind === 'scatter' ? 0.85 : 1}\n />\n ))\n : null}\n </g>\n );\n })}\n\n {/* axis titles */}\n <text\n x={MARGIN.left + INNER_W / 2}\n y={VIEW_H - 6}\n textAnchor=\"middle\"\n fontSize={12}\n fill=\"var(--color-text-secondary)\"\n >\n {xLabel}\n </text>\n <text\n x={14}\n y={MARGIN.top + INNER_H / 2}\n textAnchor=\"middle\"\n fontSize={12}\n fill=\"var(--color-text-secondary)\"\n transform={`rotate(-90 14 ${MARGIN.top + INNER_H / 2})`}\n >\n {yLabel}\n </text>\n </svg>\n </div>\n\n {showLegend ? (\n <ul className=\"flex flex-wrap gap-x-4 gap-y-1.5\">\n {series.map((s) => (\n <li key={s.name} className=\"flex items-center gap-1.5 text-xs text-text-secondary\">\n <span\n aria-hidden=\"true\"\n className=\"inline-block size-2.5 rounded-full\"\n style={{ backgroundColor: s.colorVar }}\n />\n <span className=\"truncate\">\n {s.name}\n <span className=\"ml-1 text-text-tertiary\">\n ({s.kind === 'scatter' ? 'points' : 'line'})\n </span>\n </span>\n </li>\n ))}\n </ul>\n ) : null}\n </div>\n );\n}\n\nexport default ResultsChart;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"SafeLink.js","names":[],"sources":["../../../src/cosmicintersection/components/SafeLink.tsx"],"sourcesContent":["import type { AnchorHTMLAttributes, ReactNode } from 'react';\n\n/**\n * Validate and normalise a user-supplied URL before it is ever placed in an\n * `href`. Free-text URL fields (source endpoints, external references) are\n * persisted verbatim by the backend, so rendering them directly into an anchor\n * is a stored-XSS sink: a value like `javascript:alert(1)` or `data:text/html,...`\n * would execute on click. This helper is the single choke point that neutralises\n * that.\n *\n * Rules:\n * - empty / whitespace → `null` (caller renders plain text)\n * - bare domain → prefixed with `https://` (`example.com` → `https://example.com`)\n * - `http:` / `https:` → accepted, returned normalised\n * - anything else → `null` (rejects `javascript:`, `data:`, `file:`, `mailto:`…)\n *\n * Also used for absolute in-app links (e.g. `/conversations`), which are returned\n * as-is when they are root-relative.\n */\nexport function safeUrl(raw: string | null | undefined): string | null {\n if (!raw) {\n return null;\n }\n const trimmed = raw.trim();\n if (!trimmed) {\n return null;\n }\n\n // Root-relative in-app paths (e.g. \"/conversations\") are always safe.\n if (trimmed.startsWith('/') && !trimmed.startsWith('//')) {\n return trimmed;\n }\n\n const hasScheme = /^[a-zA-Z][a-zA-Z\\d+.-]*:/.test(trimmed);\n const candidate = hasScheme ? trimmed : `https://${trimmed}`;\n\n try {\n const url = new URL(candidate);\n if (url.protocol === 'http:' || url.protocol === 'https:') {\n return url.toString();\n }\n return null;\n } catch {\n return null;\n }\n}\n\nexport interface SafeLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {\n href: string | null | undefined;\n children?: ReactNode;\n /** Class applied to the rendered plain-text span when the href is unsafe. */\n fallbackClassName?: string;\n}\n\n/**\n * Anchor that only renders a clickable link when `href` passes `safeUrl`.\n * Unsafe/blank values fall back to inert text so a malicious `javascript:` URL\n * can never be activated. External links open in a new tab with\n * `noopener noreferrer`; root-relative in-app links open in the same tab.\n */\nexport function SafeLink({ href, children, className, fallbackClassName, ...rest }: SafeLinkProps) {\n const safe = safeUrl(href);\n\n if (!safe) {\n return <span className={fallbackClassName ?? className}>{children ?? href ?? ''}</span>;\n }\n\n const isInternal = safe.startsWith('/');\n\n return (\n <a\n href={safe}\n className={className}\n {...(isInternal ? {} : { target: '_blank', rel: 'noopener noreferrer nofollow' })}\n {...rest}\n >\n {children ?? safe}\n </a>\n );\n}\n\nexport default SafeLink;\n"],"mappings":";;AAmBA,SAAgB,EAAQ,GAA+C;AACrE,KAAI,CAAC,EACH,QAAO;CAET,IAAM,IAAU,EAAI,MAAM;AAC1B,KAAI,CAAC,EACH,QAAO;AAIT,KAAI,EAAQ,WAAW,IAAI,IAAI,CAAC,EAAQ,WAAW,KAAK,CACtD,QAAO;CAIT,IAAM,IADY,2BAA2B,KAAK,EAAQ,GAC5B,IAAU,WAAW;AAEnD,KAAI;EACF,IAAM,IAAM,IAAI,IAAI,EAAU;AAI9B,SAHI,EAAI,aAAa,WAAW,EAAI,aAAa,WACxC,EAAI,UAAU,GAEhB;SACD;AACN,SAAO;;;AAiBX,SAAgB,EAAS,EAAE,SAAM,aAAU,cAAW,sBAAmB,GAAG,KAAuB;CACjG,IAAM,IAAO,EAAQ,EAAK;AAQ1B,QANK,IAOH,kBAAC,KAAD;EACE,MAAM;EACK;EACX,GANe,EAAK,WAAW,IAAI,GAMjB,EAAE,GAAG;GAAE,QAAQ;GAAU,KAAK;GAAgC;EAChF,GAAI;YAEH,KAAY;EACX,CAAA,GAbG,kBAAC,QAAD;EAAM,WAAW,KAAqB;YAAY,KAAY,KAAQ;EAAU,CAAA"}
1
+ {"version":3,"file":"SafeLink.js","names":[],"sources":["../../../src/cosmicintersection/components/SafeLink.tsx"],"sourcesContent":["import type { AnchorHTMLAttributes, ReactNode } from 'react';\n\n/**\n * Validate and normalise a user-supplied URL before it is ever placed in an\n * `href`. Free-text URL fields (source endpoints, external references) are\n * persisted verbatim by the backend, so rendering them directly into an anchor\n * is a stored-XSS sink: a value like `javascript:alert(1)` or `data:text/html,...`\n * would execute on click. This helper is the single choke point that neutralises\n * that.\n *\n * Rules:\n * - empty / whitespace → `null` (caller renders plain text)\n * - bare domain → prefixed with `https://` (`example.com` → `https://example.com`)\n * - `http:` / `https:` → accepted, returned normalised\n * - anything else → `null` (rejects `javascript:`, `data:`, `file:`, `mailto:`…)\n *\n * Also used for absolute in-app links (e.g. `/conversations`), which are returned\n * as-is when they are root-relative.\n */\nexport function safeUrl(raw: string | null | undefined): string | null {\n if (!raw) {\n return null;\n }\n const trimmed = raw.trim();\n if (!trimmed) {\n return null;\n }\n\n // Root-relative in-app paths (e.g. \"/conversations\") are always safe.\n if (trimmed.startsWith('/') && !trimmed.startsWith('//')) {\n return trimmed;\n }\n\n const hasScheme = /^[a-zA-Z][a-zA-Z\\d+.-]*:/.test(trimmed);\n const candidate = hasScheme ? trimmed : `https://${trimmed}`;\n\n try {\n const url = new URL(candidate);\n if (url.protocol === 'http:' || url.protocol === 'https:') {\n return url.toString();\n }\n return null;\n } catch {\n return null;\n }\n}\n\nexport interface SafeLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {\n href: string | null | undefined;\n children?: ReactNode;\n /** Class applied to the rendered plain-text span when the href is unsafe. */\n fallbackClassName?: string;\n}\n\n/**\n * Anchor that only renders a clickable link when `href` passes `safeUrl`.\n * Unsafe/blank values fall back to inert text so a malicious `javascript:` URL\n * can never be activated. External links open in a new tab with\n * `noopener noreferrer`; root-relative in-app links open in the same tab.\n */\nexport function SafeLink({ href, children, className, fallbackClassName, ...rest }: SafeLinkProps) {\n const safe = safeUrl(href);\n\n if (!safe) {\n return <span className={fallbackClassName ?? className}>{children ?? href ?? ''}</span>;\n }\n\n const isInternal = safe.startsWith('/');\n\n return (\n <a\n href={safe}\n className={className}\n {...(isInternal ? {} : { target: '_blank', rel: 'noopener noreferrer nofollow' })}\n {...rest}\n >\n {children ?? safe}\n </a>\n );\n}\n\nexport default SafeLink;\n"],"mappings":";;AAmBA,SAAgB,EAAQ,GAA+C;CACrE,IAAI,CAAC,GACH,OAAO;CAET,IAAM,IAAU,EAAI,KAAK;CACzB,IAAI,CAAC,GACH,OAAO;CAIT,IAAI,EAAQ,WAAW,GAAG,KAAK,CAAC,EAAQ,WAAW,IAAI,GACrD,OAAO;CAIT,IAAM,IADY,2BAA2B,KAAK,CAChC,IAAY,IAAU,WAAW;CAEnD,IAAI;EACF,IAAM,IAAM,IAAI,IAAI,CAAS;EAI7B,OAHI,EAAI,aAAa,WAAW,EAAI,aAAa,WACxC,EAAI,SAAS,IAEf;CACT,QAAQ;EACN,OAAO;CACT;AACF;AAeA,SAAgB,EAAS,EAAE,SAAM,aAAU,cAAW,sBAAmB,GAAG,KAAuB;CACjG,IAAM,IAAO,EAAQ,CAAI;CAQzB,OANK,IAOH,kBAAC,KAAD;EACE,MAAM;EACK;EACX,GANe,EAAK,WAAW,GAM1B,IAAa,CAAC,IAAI;GAAE,QAAQ;GAAU,KAAK;EAA+B;EAC/E,GAAI;YAEH,KAAY;CACZ,CAAA,IAbI,kBAAC,QAAD;EAAM,WAAW,KAAqB;YAAY,KAAY,KAAQ;CAAS,CAAA;AAe1F"}
@@ -1 +1 @@
1
- {"version":3,"file":"StatusBadge.js","names":[],"sources":["../../../src/cosmicintersection/components/StatusBadge.tsx"],"sourcesContent":["/**\n * StatusBadge — maps the Cos* enum values to the semantic status token families.\n *\n * One badge component covers every CosmicIntersection enum (dataset/source/\n * pipeline/run/step/notebook/analysis/release/grant status, plus access level).\n * Colour comes only from the `status-{success,info,warning,error,neutral}`\n * token families — never a raw palette colour — so it renders correctly in every\n * theme.\n */\n\nexport type StatusTone = 'success' | 'info' | 'warning' | 'error' | 'neutral';\n\nconst TONE_CLASS: Record<StatusTone, string> = {\n success: 'bg-status-success-bg-subtle text-status-success-text border-status-success-border',\n info: 'bg-status-info-bg-subtle text-status-info-text border-status-info-border',\n warning: 'bg-status-warning-bg-subtle text-status-warning-text border-status-warning-border',\n error: 'bg-status-error-bg-subtle text-status-error-text border-status-error-border',\n neutral: 'bg-status-neutral-bg-subtle text-status-neutral-text border-status-neutral-border',\n};\n\n// Every Cos* enum value → tone. Values are unique enough across enums that a\n// single map is unambiguous; unknown values fall back to neutral.\nconst STATUS_TONE: Record<string, StatusTone> = {\n // healthy / terminal-good\n AVAILABLE: 'success',\n CONNECTED: 'success',\n SUCCEEDED: 'success',\n COMPLETED: 'success',\n PUBLISHED: 'success',\n ACTIVE: 'success',\n PUBLIC: 'success',\n // in-flight / attention\n INGESTING: 'warning',\n SYNCING: 'warning',\n RUNNING: 'warning',\n DEGRADED: 'warning',\n PAUSED: 'warning',\n DEPRECATED: 'warning',\n EXPIRED: 'warning',\n EMBARGOED: 'warning',\n RESTRICTED: 'warning',\n UNDER_REVIEW: 'warning',\n // queued / neutral-info\n DRAFT: 'info',\n CONFIGURING: 'info',\n QUEUED: 'info',\n PENDING: 'info',\n WORKSPACE: 'info',\n // failed / removed\n ERROR: 'error',\n FAILED: 'error',\n CANCELLED: 'error',\n RETRACTED: 'error',\n REVOKED: 'error',\n DISABLED: 'error',\n // archived / inert\n ARCHIVED: 'neutral',\n};\n\n/** Turn an ENUM_VALUE into a human label, e.g. GROUND_SENSOR -> \"Ground sensor\". */\nexport function formatEnumLabel(value: string): string {\n const lower = value.replace(/_/g, ' ').toLowerCase();\n return lower.charAt(0).toUpperCase() + lower.slice(1);\n}\n\nexport interface StatusBadgeProps {\n status: string;\n /** Override the derived tone (e.g. to force a colour for a domain-specific case). */\n tone?: StatusTone;\n /** Override the rendered label; defaults to a title-cased enum value. */\n label?: string;\n className?: string;\n}\n\nexport function StatusBadge({ status, tone, label, className }: StatusBadgeProps) {\n const resolvedTone: StatusTone = tone ?? STATUS_TONE[status] ?? 'neutral';\n return (\n <span\n className={`inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium ${TONE_CLASS[resolvedTone]} ${className ?? ''}`}\n >\n {label ?? formatEnumLabel(status)}\n </span>\n );\n}\n\nexport default StatusBadge;\n"],"mappings":";;AAYA,IAAM,IAAyC;CAC7C,SAAS;CACT,MAAM;CACN,SAAS;CACT,OAAO;CACP,SAAS;CACV,EAIK,IAA0C;CAE9C,WAAW;CACX,WAAW;CACX,WAAW;CACX,WAAW;CACX,WAAW;CACX,QAAQ;CACR,QAAQ;CAER,WAAW;CACX,SAAS;CACT,SAAS;CACT,UAAU;CACV,QAAQ;CACR,YAAY;CACZ,SAAS;CACT,WAAW;CACX,YAAY;CACZ,cAAc;CAEd,OAAO;CACP,aAAa;CACb,QAAQ;CACR,SAAS;CACT,WAAW;CAEX,OAAO;CACP,QAAQ;CACR,WAAW;CACX,WAAW;CACX,SAAS;CACT,UAAU;CAEV,UAAU;CACX;AAGD,SAAgB,EAAgB,GAAuB;CACrD,IAAM,IAAQ,EAAM,QAAQ,MAAM,IAAI,CAAC,aAAa;AACpD,QAAO,EAAM,OAAO,EAAE,CAAC,aAAa,GAAG,EAAM,MAAM,EAAE;;AAYvD,SAAgB,EAAY,EAAE,WAAQ,SAAM,UAAO,gBAA+B;AAEhF,QACE,kBAAC,QAAD;EACE,WAAW,sFAAsF,EAHpE,KAAQ,EAAY,MAAW,WAG8D,GAAG,KAAa;YAEzI,KAAS,EAAgB,EAAO;EAC5B,CAAA"}
1
+ {"version":3,"file":"StatusBadge.js","names":[],"sources":["../../../src/cosmicintersection/components/StatusBadge.tsx"],"sourcesContent":["/**\n * StatusBadge — maps the Cos* enum values to the semantic status token families.\n *\n * One badge component covers every CosmicIntersection enum (dataset/source/\n * pipeline/run/step/notebook/analysis/release/grant status, plus access level).\n * Colour comes only from the `status-{success,info,warning,error,neutral}`\n * token families — never a raw palette colour — so it renders correctly in every\n * theme.\n */\n\nexport type StatusTone = 'success' | 'info' | 'warning' | 'error' | 'neutral';\n\nconst TONE_CLASS: Record<StatusTone, string> = {\n success: 'bg-status-success-bg-subtle text-status-success-text border-status-success-border',\n info: 'bg-status-info-bg-subtle text-status-info-text border-status-info-border',\n warning: 'bg-status-warning-bg-subtle text-status-warning-text border-status-warning-border',\n error: 'bg-status-error-bg-subtle text-status-error-text border-status-error-border',\n neutral: 'bg-status-neutral-bg-subtle text-status-neutral-text border-status-neutral-border',\n};\n\n// Every Cos* enum value → tone. Values are unique enough across enums that a\n// single map is unambiguous; unknown values fall back to neutral.\nconst STATUS_TONE: Record<string, StatusTone> = {\n // healthy / terminal-good\n AVAILABLE: 'success',\n CONNECTED: 'success',\n SUCCEEDED: 'success',\n COMPLETED: 'success',\n PUBLISHED: 'success',\n ACTIVE: 'success',\n PUBLIC: 'success',\n // in-flight / attention\n INGESTING: 'warning',\n SYNCING: 'warning',\n RUNNING: 'warning',\n DEGRADED: 'warning',\n PAUSED: 'warning',\n DEPRECATED: 'warning',\n EXPIRED: 'warning',\n EMBARGOED: 'warning',\n RESTRICTED: 'warning',\n UNDER_REVIEW: 'warning',\n // queued / neutral-info\n DRAFT: 'info',\n CONFIGURING: 'info',\n QUEUED: 'info',\n PENDING: 'info',\n WORKSPACE: 'info',\n // failed / removed\n ERROR: 'error',\n FAILED: 'error',\n CANCELLED: 'error',\n RETRACTED: 'error',\n REVOKED: 'error',\n DISABLED: 'error',\n // archived / inert\n ARCHIVED: 'neutral',\n};\n\n/** Turn an ENUM_VALUE into a human label, e.g. GROUND_SENSOR -> \"Ground sensor\". */\nexport function formatEnumLabel(value: string): string {\n const lower = value.replace(/_/g, ' ').toLowerCase();\n return lower.charAt(0).toUpperCase() + lower.slice(1);\n}\n\nexport interface StatusBadgeProps {\n status: string;\n /** Override the derived tone (e.g. to force a colour for a domain-specific case). */\n tone?: StatusTone;\n /** Override the rendered label; defaults to a title-cased enum value. */\n label?: string;\n className?: string;\n}\n\nexport function StatusBadge({ status, tone, label, className }: StatusBadgeProps) {\n const resolvedTone: StatusTone = tone ?? STATUS_TONE[status] ?? 'neutral';\n return (\n <span\n className={`inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium ${TONE_CLASS[resolvedTone]} ${className ?? ''}`}\n >\n {label ?? formatEnumLabel(status)}\n </span>\n );\n}\n\nexport default StatusBadge;\n"],"mappings":";;AAYA,IAAM,IAAyC;CAC7C,SAAS;CACT,MAAM;CACN,SAAS;CACT,OAAO;CACP,SAAS;AACX,GAIM,IAA0C;CAE9C,WAAW;CACX,WAAW;CACX,WAAW;CACX,WAAW;CACX,WAAW;CACX,QAAQ;CACR,QAAQ;CAER,WAAW;CACX,SAAS;CACT,SAAS;CACT,UAAU;CACV,QAAQ;CACR,YAAY;CACZ,SAAS;CACT,WAAW;CACX,YAAY;CACZ,cAAc;CAEd,OAAO;CACP,aAAa;CACb,QAAQ;CACR,SAAS;CACT,WAAW;CAEX,OAAO;CACP,QAAQ;CACR,WAAW;CACX,WAAW;CACX,SAAS;CACT,UAAU;CAEV,UAAU;AACZ;AAGA,SAAgB,EAAgB,GAAuB;CACrD,IAAM,IAAQ,EAAM,QAAQ,MAAM,GAAG,EAAE,YAAY;CACnD,OAAO,EAAM,OAAO,CAAC,EAAE,YAAY,IAAI,EAAM,MAAM,CAAC;AACtD;AAWA,SAAgB,EAAY,EAAE,WAAQ,SAAM,UAAO,gBAA+B;CAEhF,OACE,kBAAC,QAAD;EACE,WAAW,sFAAsF,EAHpE,KAAQ,EAAY,MAAW,WAG8D,GAAG,KAAa;YAEzI,KAAS,EAAgB,CAAM;CAC5B,CAAA;AAEV"}