@hex-core/components 1.9.0 → 1.10.0

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 (146) hide show
  1. package/dist/_tsup-dts-rollup.d.ts +575 -18
  2. package/dist/accordion.js.map +1 -1
  3. package/dist/alert-dialog.js.map +1 -1
  4. package/dist/alert.js.map +1 -1
  5. package/dist/arc.js.map +1 -1
  6. package/dist/attachment.js.map +1 -1
  7. package/dist/audio-player.js.map +1 -1
  8. package/dist/audio-waveform.js.map +1 -1
  9. package/dist/auth-forgot-password.js.map +1 -1
  10. package/dist/auth-reset-password.js.map +1 -1
  11. package/dist/auth-sign-in-split.js.map +1 -1
  12. package/dist/auth-sign-up-card.js.map +1 -1
  13. package/dist/auth-verify-email.js.map +1 -1
  14. package/dist/auth-verify-otp.js.map +1 -1
  15. package/dist/avatar.js.map +1 -1
  16. package/dist/badge.js.map +1 -1
  17. package/dist/branch.d.ts +2 -0
  18. package/dist/branch.js +136 -0
  19. package/dist/branch.js.map +1 -0
  20. package/dist/breadcrumb.js.map +1 -1
  21. package/dist/button.js.map +1 -1
  22. package/dist/calendar.js.map +1 -1
  23. package/dist/canvas.js.map +1 -1
  24. package/dist/card.js.map +1 -1
  25. package/dist/chain-of-thought.d.ts +3 -0
  26. package/dist/chain-of-thought.js +119 -0
  27. package/dist/chain-of-thought.js.map +1 -0
  28. package/dist/checkbox.js.map +1 -1
  29. package/dist/chord.js.map +1 -1
  30. package/dist/citation.js.map +1 -1
  31. package/dist/cloze.js.map +1 -1
  32. package/dist/cluster.js.map +1 -1
  33. package/dist/code-block-copy.js.map +1 -1
  34. package/dist/code-block.js.map +1 -1
  35. package/dist/color-picker.js.map +1 -1
  36. package/dist/combobox.js.map +1 -1
  37. package/dist/command.js.map +1 -1
  38. package/dist/compare-table.js.map +1 -1
  39. package/dist/composer.js.map +1 -1
  40. package/dist/container.js.map +1 -1
  41. package/dist/context-menu.js.map +1 -1
  42. package/dist/conversation.d.ts +3 -0
  43. package/dist/conversation.js +358 -0
  44. package/dist/conversation.js.map +1 -0
  45. package/dist/data-table.js.map +1 -1
  46. package/dist/date-picker.js.map +1 -1
  47. package/dist/deck.js.map +1 -1
  48. package/dist/dendrogram.js.map +1 -1
  49. package/dist/diagram.js.map +1 -1
  50. package/dist/dialog.js.map +1 -1
  51. package/dist/drawer.js.map +1 -1
  52. package/dist/dropdown-menu.js.map +1 -1
  53. package/dist/dropzone.js.map +1 -1
  54. package/dist/empty.js.map +1 -1
  55. package/dist/error-state.js.map +1 -1
  56. package/dist/file-tree.js.map +1 -1
  57. package/dist/flashcard.js.map +1 -1
  58. package/dist/flowchart.js.map +1 -1
  59. package/dist/form.js.map +1 -1
  60. package/dist/funnel.js.map +1 -1
  61. package/dist/gantt.js.map +1 -1
  62. package/dist/grid.js.map +1 -1
  63. package/dist/hover-card.js.map +1 -1
  64. package/dist/image-occlusion.js.map +1 -1
  65. package/dist/index.d.ts +21 -0
  66. package/dist/index.js +1011 -13
  67. package/dist/index.js.map +1 -1
  68. package/dist/inline-citation.d.ts +2 -0
  69. package/dist/inline-citation.js +108 -0
  70. package/dist/inline-citation.js.map +1 -0
  71. package/dist/input-otp.js.map +1 -1
  72. package/dist/input.js.map +1 -1
  73. package/dist/label.js.map +1 -1
  74. package/dist/loading-indicator.js.map +1 -1
  75. package/dist/loading.js.map +1 -1
  76. package/dist/markdown.d.ts +1 -0
  77. package/dist/markdown.js +784 -4
  78. package/dist/markdown.js.map +1 -1
  79. package/dist/matrix.js.map +1 -1
  80. package/dist/menubar.js.map +1 -1
  81. package/dist/message-actions.js.map +1 -1
  82. package/dist/message-list.js.map +1 -1
  83. package/dist/message.js.map +1 -1
  84. package/dist/mind-map.js.map +1 -1
  85. package/dist/multi-combobox.js.map +1 -1
  86. package/dist/navigation-menu.js.map +1 -1
  87. package/dist/org-chart.js.map +1 -1
  88. package/dist/pagination.js.map +1 -1
  89. package/dist/plan.d.ts +3 -0
  90. package/dist/plan.js +183 -0
  91. package/dist/plan.js.map +1 -0
  92. package/dist/popover.js.map +1 -1
  93. package/dist/progress.js.map +1 -1
  94. package/dist/pyramid.js.map +1 -1
  95. package/dist/quiz.js.map +1 -1
  96. package/dist/radio-group.js.map +1 -1
  97. package/dist/reasoning.js.map +1 -1
  98. package/dist/resizable.js.map +1 -1
  99. package/dist/sankey.js.map +1 -1
  100. package/dist/schemas.d.ts +8 -0
  101. package/dist/schemas.js +774 -17
  102. package/dist/schemas.js.map +1 -1
  103. package/dist/scroll-area.js.map +1 -1
  104. package/dist/select.js.map +1 -1
  105. package/dist/separator.js.map +1 -1
  106. package/dist/sequence.js.map +1 -1
  107. package/dist/sheet.js.map +1 -1
  108. package/dist/shimmer.d.ts +2 -0
  109. package/dist/shimmer.js +39 -0
  110. package/dist/shimmer.js.map +1 -0
  111. package/dist/sidebar.js.map +1 -1
  112. package/dist/skeleton.js.map +1 -1
  113. package/dist/slider.js.map +1 -1
  114. package/dist/sources.d.ts +3 -0
  115. package/dist/sources.js +164 -0
  116. package/dist/sources.js.map +1 -0
  117. package/dist/spaced-repetition.js.map +1 -1
  118. package/dist/spacer.js.map +1 -1
  119. package/dist/speech-recognition.js.map +1 -1
  120. package/dist/stack.js.map +1 -1
  121. package/dist/stepper.js.map +1 -1
  122. package/dist/suggestion.js.map +1 -1
  123. package/dist/sunburst.js.map +1 -1
  124. package/dist/switch.js.map +1 -1
  125. package/dist/table.js.map +1 -1
  126. package/dist/tabs.js.map +1 -1
  127. package/dist/tag.js.map +1 -1
  128. package/dist/task.d.ts +3 -0
  129. package/dist/task.js +189 -0
  130. package/dist/task.js.map +1 -0
  131. package/dist/terminal.js +11 -0
  132. package/dist/terminal.js.map +1 -1
  133. package/dist/textarea.js.map +1 -1
  134. package/dist/time-axis.js.map +1 -1
  135. package/dist/time-picker.js.map +1 -1
  136. package/dist/timeline.js.map +1 -1
  137. package/dist/toggle-group.js.map +1 -1
  138. package/dist/toggle.js.map +1 -1
  139. package/dist/tool-call.js +5 -6
  140. package/dist/tool-call.js.map +1 -1
  141. package/dist/toolbar.js.map +1 -1
  142. package/dist/tooltip.js.map +1 -1
  143. package/dist/tree-map.js.map +1 -1
  144. package/dist/tree.js.map +1 -1
  145. package/dist/venn.js.map +1 -1
  146. package/package.json +8 -3
@@ -0,0 +1,164 @@
1
+ "use client";
2
+ import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
3
+ import { clsx } from 'clsx';
4
+ import { twMerge } from 'tailwind-merge';
5
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
6
+
7
+ function cn(...inputs) {
8
+ return twMerge(clsx(inputs));
9
+ }
10
+ var SAFE_URL_SCHEMES = ["http:", "https:", "mailto:"];
11
+ function safeUrl(raw) {
12
+ if (raw === void 0 || raw === "") return void 0;
13
+ let parsed;
14
+ try {
15
+ parsed = new URL(raw);
16
+ } catch {
17
+ return raw.includes(":") ? void 0 : raw;
18
+ }
19
+ return SAFE_URL_SCHEMES.some((scheme) => scheme === parsed.protocol) ? raw : void 0;
20
+ }
21
+ function Citation({ title, url, page, index, className, children }) {
22
+ const baseClasses = cn(
23
+ "inline-flex items-center gap-1.5 rounded-md border border-foreground/15 bg-card px-2 py-0.5 text-xs",
24
+ "transition-all duration-[var(--duration-normal,200ms)] ease-out",
25
+ className
26
+ );
27
+ const body = /* @__PURE__ */ jsxs(Fragment, { children: [
28
+ typeof index === "number" ? /* @__PURE__ */ jsxs("span", { className: "font-mono text-[10px] font-semibold text-muted-foreground", children: [
29
+ "[",
30
+ index,
31
+ "]"
32
+ ] }) : /* @__PURE__ */ jsx(DocGlyph, {}),
33
+ /* @__PURE__ */ jsx("span", { className: "truncate text-foreground", children: title }),
34
+ typeof page === "number" ? /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
35
+ "p.",
36
+ page
37
+ ] }) : null,
38
+ children
39
+ ] });
40
+ if (url) {
41
+ return /* @__PURE__ */ jsx(
42
+ "a",
43
+ {
44
+ href: url,
45
+ target: "_blank",
46
+ rel: "noreferrer noopener",
47
+ className: cn(
48
+ baseClasses,
49
+ "hover:border-foreground/30 hover:bg-secondary/40 hover:shadow-sm",
50
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
51
+ ),
52
+ children: body
53
+ }
54
+ );
55
+ }
56
+ return /* @__PURE__ */ jsx("span", { className: baseClasses, children: body });
57
+ }
58
+ function DocGlyph() {
59
+ return /* @__PURE__ */ jsxs(
60
+ "svg",
61
+ {
62
+ "aria-hidden": true,
63
+ viewBox: "0 0 16 16",
64
+ width: "11",
65
+ height: "11",
66
+ fill: "none",
67
+ stroke: "currentColor",
68
+ strokeWidth: "1.5",
69
+ strokeLinecap: "round",
70
+ strokeLinejoin: "round",
71
+ className: "shrink-0 text-muted-foreground",
72
+ children: [
73
+ /* @__PURE__ */ jsx("path", { d: "M9 1.5H4.5A1.5 1.5 0 0 0 3 3v10a1.5 1.5 0 0 0 1.5 1.5h7A1.5 1.5 0 0 0 13 13V5.5L9 1.5z" }),
74
+ /* @__PURE__ */ jsx("path", { d: "M9 1.5V5.5h4" })
75
+ ]
76
+ }
77
+ );
78
+ }
79
+ function Sources({ sources, defaultOpen = true, className }) {
80
+ if (sources.length === 0) return null;
81
+ const count = sources.length;
82
+ const headerLabel = count === 1 ? "1 source" : `${count} sources`;
83
+ return /* @__PURE__ */ jsxs(
84
+ CollapsiblePrimitive.Root,
85
+ {
86
+ defaultOpen,
87
+ className: cn(
88
+ "overflow-hidden rounded-md border border-border bg-card text-card-foreground",
89
+ className
90
+ ),
91
+ children: [
92
+ /* @__PURE__ */ jsxs(
93
+ CollapsiblePrimitive.Trigger,
94
+ {
95
+ className: cn(
96
+ "group flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs text-muted-foreground",
97
+ "transition-all duration-[var(--duration-normal,200ms)] ease-out",
98
+ "hover:text-foreground",
99
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
100
+ ),
101
+ children: [
102
+ /* @__PURE__ */ jsx(DocStackGlyph, {}),
103
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: headerLabel }),
104
+ /* @__PURE__ */ jsx(Chevron, {})
105
+ ]
106
+ }
107
+ ),
108
+ /* @__PURE__ */ jsx(CollapsiblePrimitive.Content, { className: "overflow-hidden border-t border-foreground/[0.06]", children: /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-1.5 p-2", children: sources.map((s, i) => /* @__PURE__ */ jsx(
109
+ Citation,
110
+ {
111
+ title: s.title,
112
+ url: safeUrl(s.url),
113
+ page: s.page,
114
+ index: s.index ?? i + 1
115
+ },
116
+ `${s.url ?? s.title}-${i}`
117
+ )) }) })
118
+ ]
119
+ }
120
+ );
121
+ }
122
+ function DocStackGlyph() {
123
+ return /* @__PURE__ */ jsxs(
124
+ "svg",
125
+ {
126
+ "aria-hidden": true,
127
+ viewBox: "0 0 16 16",
128
+ width: "12",
129
+ height: "12",
130
+ fill: "none",
131
+ stroke: "currentColor",
132
+ strokeWidth: "1.5",
133
+ strokeLinecap: "round",
134
+ strokeLinejoin: "round",
135
+ className: "shrink-0",
136
+ children: [
137
+ /* @__PURE__ */ jsx("path", { d: "M5 3h7l1.5 1.5V13H5V3z" }),
138
+ /* @__PURE__ */ jsx("path", { d: "M3 5v8.5L4.5 15H11" })
139
+ ]
140
+ }
141
+ );
142
+ }
143
+ function Chevron() {
144
+ return /* @__PURE__ */ jsx(
145
+ "svg",
146
+ {
147
+ "aria-hidden": true,
148
+ viewBox: "0 0 16 16",
149
+ width: "12",
150
+ height: "12",
151
+ fill: "none",
152
+ stroke: "currentColor",
153
+ strokeWidth: "1.5",
154
+ strokeLinecap: "round",
155
+ strokeLinejoin: "round",
156
+ className: "ml-auto shrink-0 transition-transform duration-200 group-data-[state=open]:rotate-180",
157
+ children: /* @__PURE__ */ jsx("path", { d: "M4 6l4 4 4-4" })
158
+ }
159
+ );
160
+ }
161
+
162
+ export { Sources };
163
+ //# sourceMappingURL=sources.js.map
164
+ //# sourceMappingURL=sources.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/utils.ts","../src/ai/citation/citation.tsx","../src/ai/sources/sources.tsx"],"names":["jsxs","jsx"],"mappings":"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","file":"sources.js","sourcesContent":["import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge class names with Tailwind CSS conflict resolution.\n * @param inputs - Class values (strings, arrays, objects) to merge\n * @returns A single merged class string with Tailwind conflicts resolved\n */\nexport function cn(...inputs: ClassValue[]) {\n\treturn twMerge(clsx(inputs));\n}\n\nconst SAFE_URL_SCHEMES = [\"http:\", \"https:\", \"mailto:\"] as const;\n\n/**\n * Allowlist a URL for use as an `<a href>` against untrusted input.\n *\n * Returns the raw string when it's `http(s):` / `mailto:` / a relative\n * URL (no scheme); returns `undefined` otherwise. Defends against\n * `javascript:` / `data:` / `vbscript:` injection from streamed model\n * output or third-party JSON payloads that flow into citation chips.\n *\n * Inside a Markdown render path, `rehype-sanitize` already strips\n * `javascript:` from inline-link hrefs — but it does NOT introspect\n * JSON nested inside attribute values (e.g. `<sources data='[…]'/>`),\n * so the components that consume that data must guard themselves.\n *\n * @param raw - The candidate URL, or `undefined` when none was supplied.\n * @returns The URL when safe to render, otherwise `undefined`.\n */\nexport function safeUrl(raw: string | undefined): string | undefined {\n\tif (raw === undefined || raw === \"\") return undefined;\n\t// Relative URLs have no scheme — `new URL(relative)` throws without a base,\n\t// so a thrown URL means either malformed input OR a relative path. Treat\n\t// \"throws + does not contain a colon\" as a relative path (safe).\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(raw);\n\t} catch {\n\t\treturn raw.includes(\":\") ? undefined : raw;\n\t}\n\treturn SAFE_URL_SCHEMES.some((scheme) => scheme === parsed.protocol) ? raw : undefined;\n}\n","import * as React from \"react\";\nimport { cn } from \"../../lib/utils.js\";\n\n/**\n * Source attribution chip — renders a citation for a RAG hit, search\n * result, or any external reference the assistant pulled from. Becomes a\n * clickable anchor when `url` is provided; otherwise a static span.\n *\n * @example\n * <Citation title=\"auth-overview.md\" url={src.url} page={3} />\n * @example\n * <Cluster gap=\"xs\">\n * {sources.map((s, i) => (\n * <Citation key={s.id} title={s.title} url={s.url} index={i + 1} />\n * ))}\n * </Cluster>\n */\nexport interface CitationProps {\n\ttitle: string;\n\turl?: string;\n\tpage?: number;\n\t/** Numeric index for inline footnote-style display (e.g. \"[1] auth.md\"). */\n\tindex?: number;\n\tclassName?: string;\n\tchildren?: React.ReactNode;\n}\n\n/**\n * Renders a source citation chip. Uses an `<a>` when `url` is set so the\n * chip is keyboard-focusable + opens in a new tab; falls back to a\n * non-interactive span otherwise.\n *\n * @param props - title + optional url, page, index\n * @returns An anchor or span styled as a chip\n */\nfunction Citation({ title, url, page, index, className, children }: CitationProps) {\n\tconst baseClasses = cn(\n\t\t\"inline-flex items-center gap-1.5 rounded-md border border-foreground/15 bg-card px-2 py-0.5 text-xs\",\n\t\t\"transition-all duration-[var(--duration-normal,200ms)] ease-out\",\n\t\tclassName,\n\t);\n\n\tconst body = (\n\t\t<>\n\t\t\t{typeof index === \"number\" ? (\n\t\t\t\t<span className=\"font-mono text-[10px] font-semibold text-muted-foreground\">[{index}]</span>\n\t\t\t) : (\n\t\t\t\t<DocGlyph />\n\t\t\t)}\n\t\t\t<span className=\"truncate text-foreground\">{title}</span>\n\t\t\t{typeof page === \"number\" ? (\n\t\t\t\t<span className=\"text-muted-foreground\">p.{page}</span>\n\t\t\t) : null}\n\t\t\t{children}\n\t\t</>\n\t);\n\n\tif (url) {\n\t\treturn (\n\t\t\t<a\n\t\t\t\thref={url}\n\t\t\t\ttarget=\"_blank\"\n\t\t\t\trel=\"noreferrer noopener\"\n\t\t\t\tclassName={cn(\n\t\t\t\t\tbaseClasses,\n\t\t\t\t\t\"hover:border-foreground/30 hover:bg-secondary/40 hover:shadow-sm\",\n\t\t\t\t\t\"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n\t\t\t\t)}\n\t\t\t>\n\t\t\t\t{body}\n\t\t\t</a>\n\t\t);\n\t}\n\n\treturn <span className={baseClasses}>{body}</span>;\n}\n\nfunction DocGlyph() {\n\treturn (\n\t\t<svg\n\t\t\taria-hidden\n\t\t\tviewBox=\"0 0 16 16\"\n\t\t\twidth=\"11\"\n\t\t\theight=\"11\"\n\t\t\tfill=\"none\"\n\t\t\tstroke=\"currentColor\"\n\t\t\tstrokeWidth=\"1.5\"\n\t\t\tstrokeLinecap=\"round\"\n\t\t\tstrokeLinejoin=\"round\"\n\t\t\tclassName=\"shrink-0 text-muted-foreground\"\n\t\t>\n\t\t\t<path d=\"M9 1.5H4.5A1.5 1.5 0 0 0 3 3v10a1.5 1.5 0 0 0 1.5 1.5h7A1.5 1.5 0 0 0 13 13V5.5L9 1.5z\" />\n\t\t\t<path d=\"M9 1.5V5.5h4\" />\n\t\t</svg>\n\t);\n}\n\nexport { Citation };\n","\"use client\";\n\nimport * as CollapsiblePrimitive from \"@radix-ui/react-collapsible\";\nimport * as React from \"react\";\nimport { cn, safeUrl } from \"../../lib/utils.js\";\nimport { Citation } from \"../citation/citation.js\";\n\n/**\n * Bordered card listing 1–N citation chips for a RAG response.\n *\n * Header reads \"N sources\" and is a clickable Radix Collapsible trigger;\n * body renders one `<Citation>` per source (re-using the in-house chip).\n * Defaults to open so consumers don't have to expand to see what was\n * cited.\n *\n * @example\n * <Sources\n * sources={[\n * { title: \"Auth research\", url: \"https://example.com/auth\", page: 3 },\n * { title: \"OAuth 2.1 spec\", url: \"https://oauth.net/2.1\" },\n * ]}\n * />\n */\nexport interface SourcesProps {\n\t/** Citation rows. Each becomes a `<Citation>` chip. */\n\tsources: SourceRef[];\n\t/** Whether the list is expanded by default. */\n\tdefaultOpen?: boolean;\n\tclassName?: string;\n}\n\n/** Per-source data passed to a Citation chip. */\nexport interface SourceRef {\n\ttitle: string;\n\turl?: string;\n\tpage?: number;\n\t/** Optional 1-based index. Falls back to array position. Use to keep\n\t * the inline `<InlineCitation>` index aligned with the panel row when\n\t * the model emits non-1-based numbering. */\n\tindex?: number;\n}\n\n/**\n * Render a sources panel for an LLM response. Returns null when the\n * sources array is empty — consumers don't get a \"0 sources\" empty\n * card; they just don't render the panel at all.\n *\n * @param props - The list of sources + open-state default.\n * @returns A Collapsible wrapping a Citation cluster, or null if empty.\n */\nfunction Sources({ sources, defaultOpen = true, className }: SourcesProps) {\n\tif (sources.length === 0) return null;\n\tconst count = sources.length;\n\tconst headerLabel = count === 1 ? \"1 source\" : `${count} sources`;\n\n\treturn (\n\t\t<CollapsiblePrimitive.Root\n\t\t\tdefaultOpen={defaultOpen}\n\t\t\tclassName={cn(\n\t\t\t\t\"overflow-hidden rounded-md border border-border bg-card text-card-foreground\",\n\t\t\t\tclassName,\n\t\t\t)}\n\t\t>\n\t\t\t<CollapsiblePrimitive.Trigger\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"group flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs text-muted-foreground\",\n\t\t\t\t\t\"transition-all duration-[var(--duration-normal,200ms)] ease-out\",\n\t\t\t\t\t\"hover:text-foreground\",\n\t\t\t\t\t\"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n\t\t\t\t)}\n\t\t\t>\n\t\t\t\t<DocStackGlyph />\n\t\t\t\t<span className=\"font-medium\">{headerLabel}</span>\n\t\t\t\t<Chevron />\n\t\t\t</CollapsiblePrimitive.Trigger>\n\t\t\t<CollapsiblePrimitive.Content className=\"overflow-hidden border-t border-foreground/[0.06]\">\n\t\t\t\t<div className=\"flex flex-wrap items-center gap-1.5 p-2\">\n\t\t\t\t\t{sources.map((s, i) => (\n\t\t\t\t\t\t<Citation\n\t\t\t\t\t\t\tkey={`${s.url ?? s.title}-${i}`}\n\t\t\t\t\t\t\ttitle={s.title}\n\t\t\t\t\t\t\t// Gate the URL even though markdown-path callers already\n\t\t\t\t\t\t\t// route through `safeUrl` — direct-JSX consumers can hand\n\t\t\t\t\t\t\t// us anything.\n\t\t\t\t\t\t\turl={safeUrl(s.url)}\n\t\t\t\t\t\t\tpage={s.page}\n\t\t\t\t\t\t\tindex={s.index ?? i + 1}\n\t\t\t\t\t\t/>\n\t\t\t\t\t))}\n\t\t\t\t</div>\n\t\t\t</CollapsiblePrimitive.Content>\n\t\t</CollapsiblePrimitive.Root>\n\t);\n}\n\nfunction DocStackGlyph() {\n\treturn (\n\t\t<svg\n\t\t\taria-hidden\n\t\t\tviewBox=\"0 0 16 16\"\n\t\t\twidth=\"12\"\n\t\t\theight=\"12\"\n\t\t\tfill=\"none\"\n\t\t\tstroke=\"currentColor\"\n\t\t\tstrokeWidth=\"1.5\"\n\t\t\tstrokeLinecap=\"round\"\n\t\t\tstrokeLinejoin=\"round\"\n\t\t\tclassName=\"shrink-0\"\n\t\t>\n\t\t\t<path d=\"M5 3h7l1.5 1.5V13H5V3z\" />\n\t\t\t<path d=\"M3 5v8.5L4.5 15H11\" />\n\t\t</svg>\n\t);\n}\n\nfunction Chevron() {\n\treturn (\n\t\t<svg\n\t\t\taria-hidden\n\t\t\tviewBox=\"0 0 16 16\"\n\t\t\twidth=\"12\"\n\t\t\theight=\"12\"\n\t\t\tfill=\"none\"\n\t\t\tstroke=\"currentColor\"\n\t\t\tstrokeWidth=\"1.5\"\n\t\t\tstrokeLinecap=\"round\"\n\t\t\tstrokeLinejoin=\"round\"\n\t\t\tclassName=\"ml-auto shrink-0 transition-transform duration-200 group-data-[state=open]:rotate-180\"\n\t\t>\n\t\t\t<path d=\"M4 6l4 4 4-4\" />\n\t\t</svg>\n\t);\n}\n\nexport { Sources };\n"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/lib/utils.ts","../src/artifacts/spaced-repetition/spaced-repetition.tsx"],"names":[],"mappings":";;;;;AAQO,SAAS,MAAM,MAAA,EAAsB;AAC3C,EAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAC,CAAA;AAC5B;ACyBA,IAAM,OAAA,GAAuB,CAAC,OAAA,EAAS,MAAA,EAAQ,QAAQ,MAAM,CAAA;AAE7D,IAAM,cAAA,GAA4C;AAAA,EACjD,KAAA,EAAO,OAAA;AAAA,EACP,IAAA,EAAM,MAAA;AAAA,EACN,IAAA,EAAM,MAAA;AAAA,EACN,IAAA,EAAM;AACP,CAAA;AAEA,IAAM,WAAA,GAAyC;AAAA,EAC9C,KAAA,EAAO,mDAAA;AAAA,EACP,IAAA,EAAM,uDAAA;AAAA,EACN,IAAA,EAAM,uDAAA;AAAA,EACN,IAAA,EAAM;AACP,CAAA;AAKA,IAAM,cAAA,GAA4C;AAAA,EACjD,KAAA,EACC,oHAAA;AAAA,EACD,IAAA,EACC,+EAAA;AAAA,EACD,IAAA,EACC,mGAAA;AAAA,EACD,IAAA,EACC;AACF,CAAA;AAEA,SAAS,gBAAA,CAAiB;AAAA,EACzB,MAAA;AAAA,EACA,MAAA;AAAA,EACA,MAAA;AAAA,EACA,SAAA;AAAA,EACA,GAAG;AACJ,CAAA,EAA0B;AACzB,EAAA,MAAM,SAAA,GAAkB,aAAO,MAAM,CAAA;AACrC,EAAA,SAAA,CAAU,OAAA,GAAU,MAAA;AAEpB,EAAA,uBACC,GAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,GAAG,IAAA;AAAA,MACJ,4BAAA,EAA0B,IAAA;AAAA,MAC1B,cAAA,EAAc,MAAA;AAAA,MACd,IAAA,EAAK,OAAA;AAAA,MACL,YAAA,EAAW,mBAAA;AAAA,MACX,SAAA,EAAW,EAAA,CAAG,0CAAA,EAA4C,SAAS,CAAA;AAAA,MAElE,QAAA,EAAA,OAAA,CAAQ,GAAA,CAAI,CAAC,MAAA,KAAW;AACxB,QAAA,MAAM,KAAA,GAAQ,MAAA,GAAS,MAAM,CAAA,IAAK,eAAe,MAAM,CAAA;AACvD,QAAA,uBACC,GAAA;AAAA,UAAC,QAAA;AAAA,UAAA;AAAA,YAEA,IAAA,EAAK,QAAA;AAAA,YACL,mCAAA,EAAiC,IAAA;AAAA,YACjC,aAAA,EAAa,MAAA;AAAA,YACb,cAAY,CAAA,EAAG,KAAK,CAAA,EAAA,EAAK,WAAA,CAAY,MAAM,CAAC,CAAA,CAAA;AAAA,YAC5C,OAAA,EAAS,MAAM,SAAA,CAAU,OAAA,CAAQ,QAAQ,MAAM,CAAA;AAAA,YAC/C,SAAA,EAAW,EAAA;AAAA,cACV,0GAAA;AAAA,cACA,iEAAA;AAAA,cACA,eAAe,MAAM;AAAA,aACtB;AAAA,YAEC,QAAA,EAAA;AAAA,WAAA;AAAA,UAZI;AAAA,SAaN;AAAA,MAEF,CAAC;AAAA;AAAA,GACF;AAEF","file":"spaced-repetition.js","sourcesContent":["import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge class names with Tailwind CSS conflict resolution.\n * @param inputs - Class values (strings, arrays, objects) to merge\n * @returns A single merged class string with Tailwind conflicts resolved\n */\nexport function cn(...inputs: ClassValue[]) {\n\treturn twMerge(clsx(inputs));\n}\n","\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"../../lib/utils.js\";\n\n/**\n * Anki-style confidence rating row. Four buttons (Again / Hard / Good /\n * Easy) emit a rating; the consumer applies SM-2 / FSRS / hand-rolled\n * scheduling to decide when to surface the card again. Headless on\n * scheduling — this primitive doesn't compute intervals, it just\n * captures the user's signal.\n *\n * @example\n * <SpacedRepetition\n * cardId={card.id}\n * onRate={(rating, id) => scheduler.update(id, rating)}\n * />\n *\n * <SpacedRepetition\n * cardId={card.id}\n * onRate={onRate}\n * labels={{ again: \"Forgot\", hard: \"Tough\", good: \"Got it\", easy: \"Easy\" }}\n * />\n */\nexport type SrsRating = \"again\" | \"hard\" | \"good\" | \"easy\";\n\nexport interface SpacedRepetitionProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\" | \"onRate\"> {\n\t/** Identifier of the card being rated; passed back to onRate. */\n\tcardId: string;\n\t/** Called with (rating, cardId) when the learner picks a button. */\n\tonRate: (rating: SrsRating, cardId: string) => void;\n\t/** Override the default button labels. Defaults: \"Again\" / \"Hard\" / \"Good\" / \"Easy\". */\n\tlabels?: Partial<Record<SrsRating, string>>;\n}\n\nconst RATINGS: SrsRating[] = [\"again\", \"hard\", \"good\", \"easy\"];\n\nconst DEFAULT_LABELS: Record<SrsRating, string> = {\n\tagain: \"Again\",\n\thard: \"Hard\",\n\tgood: \"Good\",\n\teasy: \"Easy\",\n};\n\nconst RATING_HINT: Record<SrsRating, string> = {\n\tagain: \"Couldn't recall — show this card again soon.\",\n\thard: \"Recalled with effort — review sooner than usual.\",\n\tgood: \"Recalled correctly — keep the standard interval.\",\n\teasy: \"Recalled instantly — push the next review further out.\",\n};\n\n// Visual gradient: again (most concerning) → easy (most confident).\n// `easy` carries a faint accent border so the four buttons read as a\n// progression rather than three styled + one plain.\nconst RATING_CLASSES: Record<SrsRating, string> = {\n\tagain:\n\t\t\"border-destructive/40 bg-destructive/10 text-destructive hover:bg-destructive/15 focus-visible:ring-destructive/40\",\n\thard:\n\t\t\"border-secondary bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n\tgood:\n\t\t\"border-primary/40 bg-primary/10 text-foreground hover:bg-primary/15 focus-visible:ring-primary/40\",\n\teasy:\n\t\t\"border-accent bg-accent/10 text-accent-foreground hover:bg-accent/20 focus-visible:ring-accent/40\",\n};\n\nfunction SpacedRepetition({\n\tcardId,\n\tonRate,\n\tlabels,\n\tclassName,\n\t...rest\n}: SpacedRepetitionProps) {\n\tconst onRateRef = React.useRef(onRate);\n\tonRateRef.current = onRate;\n\n\treturn (\n\t\t<div\n\t\t\t{...rest}\n\t\t\tdata-hex-spaced-repetition\n\t\t\tdata-card-id={cardId}\n\t\t\trole=\"group\"\n\t\t\taria-label=\"Confidence rating\"\n\t\t\tclassName={cn(\"inline-flex flex-wrap items-center gap-2\", className)}\n\t\t>\n\t\t\t{RATINGS.map((rating) => {\n\t\t\t\tconst label = labels?.[rating] ?? DEFAULT_LABELS[rating];\n\t\t\t\treturn (\n\t\t\t\t\t<button\n\t\t\t\t\t\tkey={rating}\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tdata-hex-spaced-repetition-button\n\t\t\t\t\t\tdata-rating={rating}\n\t\t\t\t\t\taria-label={`${label}: ${RATING_HINT[rating]}`}\n\t\t\t\t\t\tonClick={() => onRateRef.current(rating, cardId)}\n\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\"inline-flex h-9 items-center justify-center rounded-md border px-3 text-sm font-medium transition-colors\",\n\t\t\t\t\t\t\t\"focus:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n\t\t\t\t\t\t\tRATING_CLASSES[rating],\n\t\t\t\t\t\t)}\n\t\t\t\t\t>\n\t\t\t\t\t\t{label}\n\t\t\t\t\t</button>\n\t\t\t\t);\n\t\t\t})}\n\t\t</div>\n\t);\n}\n\nexport { SpacedRepetition };\n"]}
1
+ {"version":3,"sources":["../src/lib/utils.ts","../src/artifacts/spaced-repetition/spaced-repetition.tsx"],"names":[],"mappings":";;;;;AAQO,SAAS,MAAM,MAAA,EAAsB;AAC3C,EAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAC,CAAA;AAC5B;ACyBA,IAAM,OAAA,GAAuB,CAAC,OAAA,EAAS,MAAA,EAAQ,QAAQ,MAAM,CAAA;AAE7D,IAAM,cAAA,GAA4C;AAAA,EACjD,KAAA,EAAO,OAAA;AAAA,EACP,IAAA,EAAM,MAAA;AAAA,EACN,IAAA,EAAM,MAAA;AAAA,EACN,IAAA,EAAM;AACP,CAAA;AAEA,IAAM,WAAA,GAAyC;AAAA,EAC9C,KAAA,EAAO,mDAAA;AAAA,EACP,IAAA,EAAM,uDAAA;AAAA,EACN,IAAA,EAAM,uDAAA;AAAA,EACN,IAAA,EAAM;AACP,CAAA;AAKA,IAAM,cAAA,GAA4C;AAAA,EACjD,KAAA,EACC,oHAAA;AAAA,EACD,IAAA,EACC,+EAAA;AAAA,EACD,IAAA,EACC,mGAAA;AAAA,EACD,IAAA,EACC;AACF,CAAA;AAEA,SAAS,gBAAA,CAAiB;AAAA,EACzB,MAAA;AAAA,EACA,MAAA;AAAA,EACA,MAAA;AAAA,EACA,SAAA;AAAA,EACA,GAAG;AACJ,CAAA,EAA0B;AACzB,EAAA,MAAM,SAAA,GAAkB,aAAO,MAAM,CAAA;AACrC,EAAA,SAAA,CAAU,OAAA,GAAU,MAAA;AAEpB,EAAA,uBACC,GAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,GAAG,IAAA;AAAA,MACJ,4BAAA,EAA0B,IAAA;AAAA,MAC1B,cAAA,EAAc,MAAA;AAAA,MACd,IAAA,EAAK,OAAA;AAAA,MACL,YAAA,EAAW,mBAAA;AAAA,MACX,SAAA,EAAW,EAAA,CAAG,0CAAA,EAA4C,SAAS,CAAA;AAAA,MAElE,QAAA,EAAA,OAAA,CAAQ,GAAA,CAAI,CAAC,MAAA,KAAW;AACxB,QAAA,MAAM,KAAA,GAAQ,MAAA,GAAS,MAAM,CAAA,IAAK,eAAe,MAAM,CAAA;AACvD,QAAA,uBACC,GAAA;AAAA,UAAC,QAAA;AAAA,UAAA;AAAA,YAEA,IAAA,EAAK,QAAA;AAAA,YACL,mCAAA,EAAiC,IAAA;AAAA,YACjC,aAAA,EAAa,MAAA;AAAA,YACb,cAAY,CAAA,EAAG,KAAK,CAAA,EAAA,EAAK,WAAA,CAAY,MAAM,CAAC,CAAA,CAAA;AAAA,YAC5C,OAAA,EAAS,MAAM,SAAA,CAAU,OAAA,CAAQ,QAAQ,MAAM,CAAA;AAAA,YAC/C,SAAA,EAAW,EAAA;AAAA,cACV,0GAAA;AAAA,cACA,iEAAA;AAAA,cACA,eAAe,MAAM;AAAA,aACtB;AAAA,YAEC,QAAA,EAAA;AAAA,WAAA;AAAA,UAZI;AAAA,SAaN;AAAA,MAEF,CAAC;AAAA;AAAA,GACF;AAEF","file":"spaced-repetition.js","sourcesContent":["import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge class names with Tailwind CSS conflict resolution.\n * @param inputs - Class values (strings, arrays, objects) to merge\n * @returns A single merged class string with Tailwind conflicts resolved\n */\nexport function cn(...inputs: ClassValue[]) {\n\treturn twMerge(clsx(inputs));\n}\n\nconst SAFE_URL_SCHEMES = [\"http:\", \"https:\", \"mailto:\"] as const;\n\n/**\n * Allowlist a URL for use as an `<a href>` against untrusted input.\n *\n * Returns the raw string when it's `http(s):` / `mailto:` / a relative\n * URL (no scheme); returns `undefined` otherwise. Defends against\n * `javascript:` / `data:` / `vbscript:` injection from streamed model\n * output or third-party JSON payloads that flow into citation chips.\n *\n * Inside a Markdown render path, `rehype-sanitize` already strips\n * `javascript:` from inline-link hrefs — but it does NOT introspect\n * JSON nested inside attribute values (e.g. `<sources data='[…]'/>`),\n * so the components that consume that data must guard themselves.\n *\n * @param raw - The candidate URL, or `undefined` when none was supplied.\n * @returns The URL when safe to render, otherwise `undefined`.\n */\nexport function safeUrl(raw: string | undefined): string | undefined {\n\tif (raw === undefined || raw === \"\") return undefined;\n\t// Relative URLs have no scheme — `new URL(relative)` throws without a base,\n\t// so a thrown URL means either malformed input OR a relative path. Treat\n\t// \"throws + does not contain a colon\" as a relative path (safe).\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(raw);\n\t} catch {\n\t\treturn raw.includes(\":\") ? undefined : raw;\n\t}\n\treturn SAFE_URL_SCHEMES.some((scheme) => scheme === parsed.protocol) ? raw : undefined;\n}\n","\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"../../lib/utils.js\";\n\n/**\n * Anki-style confidence rating row. Four buttons (Again / Hard / Good /\n * Easy) emit a rating; the consumer applies SM-2 / FSRS / hand-rolled\n * scheduling to decide when to surface the card again. Headless on\n * scheduling — this primitive doesn't compute intervals, it just\n * captures the user's signal.\n *\n * @example\n * <SpacedRepetition\n * cardId={card.id}\n * onRate={(rating, id) => scheduler.update(id, rating)}\n * />\n *\n * <SpacedRepetition\n * cardId={card.id}\n * onRate={onRate}\n * labels={{ again: \"Forgot\", hard: \"Tough\", good: \"Got it\", easy: \"Easy\" }}\n * />\n */\nexport type SrsRating = \"again\" | \"hard\" | \"good\" | \"easy\";\n\nexport interface SpacedRepetitionProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\" | \"onRate\"> {\n\t/** Identifier of the card being rated; passed back to onRate. */\n\tcardId: string;\n\t/** Called with (rating, cardId) when the learner picks a button. */\n\tonRate: (rating: SrsRating, cardId: string) => void;\n\t/** Override the default button labels. Defaults: \"Again\" / \"Hard\" / \"Good\" / \"Easy\". */\n\tlabels?: Partial<Record<SrsRating, string>>;\n}\n\nconst RATINGS: SrsRating[] = [\"again\", \"hard\", \"good\", \"easy\"];\n\nconst DEFAULT_LABELS: Record<SrsRating, string> = {\n\tagain: \"Again\",\n\thard: \"Hard\",\n\tgood: \"Good\",\n\teasy: \"Easy\",\n};\n\nconst RATING_HINT: Record<SrsRating, string> = {\n\tagain: \"Couldn't recall — show this card again soon.\",\n\thard: \"Recalled with effort — review sooner than usual.\",\n\tgood: \"Recalled correctly — keep the standard interval.\",\n\teasy: \"Recalled instantly — push the next review further out.\",\n};\n\n// Visual gradient: again (most concerning) → easy (most confident).\n// `easy` carries a faint accent border so the four buttons read as a\n// progression rather than three styled + one plain.\nconst RATING_CLASSES: Record<SrsRating, string> = {\n\tagain:\n\t\t\"border-destructive/40 bg-destructive/10 text-destructive hover:bg-destructive/15 focus-visible:ring-destructive/40\",\n\thard:\n\t\t\"border-secondary bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n\tgood:\n\t\t\"border-primary/40 bg-primary/10 text-foreground hover:bg-primary/15 focus-visible:ring-primary/40\",\n\teasy:\n\t\t\"border-accent bg-accent/10 text-accent-foreground hover:bg-accent/20 focus-visible:ring-accent/40\",\n};\n\nfunction SpacedRepetition({\n\tcardId,\n\tonRate,\n\tlabels,\n\tclassName,\n\t...rest\n}: SpacedRepetitionProps) {\n\tconst onRateRef = React.useRef(onRate);\n\tonRateRef.current = onRate;\n\n\treturn (\n\t\t<div\n\t\t\t{...rest}\n\t\t\tdata-hex-spaced-repetition\n\t\t\tdata-card-id={cardId}\n\t\t\trole=\"group\"\n\t\t\taria-label=\"Confidence rating\"\n\t\t\tclassName={cn(\"inline-flex flex-wrap items-center gap-2\", className)}\n\t\t>\n\t\t\t{RATINGS.map((rating) => {\n\t\t\t\tconst label = labels?.[rating] ?? DEFAULT_LABELS[rating];\n\t\t\t\treturn (\n\t\t\t\t\t<button\n\t\t\t\t\t\tkey={rating}\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tdata-hex-spaced-repetition-button\n\t\t\t\t\t\tdata-rating={rating}\n\t\t\t\t\t\taria-label={`${label}: ${RATING_HINT[rating]}`}\n\t\t\t\t\t\tonClick={() => onRateRef.current(rating, cardId)}\n\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\"inline-flex h-9 items-center justify-center rounded-md border px-3 text-sm font-medium transition-colors\",\n\t\t\t\t\t\t\t\"focus:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n\t\t\t\t\t\t\tRATING_CLASSES[rating],\n\t\t\t\t\t\t)}\n\t\t\t\t\t>\n\t\t\t\t\t\t{label}\n\t\t\t\t\t</button>\n\t\t\t\t);\n\t\t\t})}\n\t\t</div>\n\t);\n}\n\nexport { SpacedRepetition };\n"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/lib/utils.ts","../src/primitives/spacer/spacer.tsx"],"names":[],"mappings":";;;;;;AAQO,SAAS,MAAM,MAAA,EAAsB;AAC3C,EAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAC,CAAA;AAC5B;ACCA,IAAM,cAAA,GAAiB,IAAI,UAAA,EAAY;AAAA,EACtC,QAAA,EAAU;AAAA,IACT,IAAA,EAAM;AAAA,MACL,EAAA,EAAI,wCAAA;AAAA,MACJ,EAAA,EAAI,uCAAA;AAAA,MACJ,EAAA,EAAI,qCAAA;AAAA,MACJ,EAAA,EAAI,qCAAA;AAAA,MACJ,EAAA,EAAI;AAAA,KACL;AAAA,IACA,IAAA,EAAM;AAAA,MACL,QAAA,EAAU,4BAAA;AAAA,MACV,UAAA,EAAY,4BAAA;AAAA,MACZ,IAAA,EAAM;AAAA;AACP,GACD;AAAA,EACA,eAAA,EAAiB;AAAA,IAChB,IAAA,EAAM,IAAA;AAAA,IACN,IAAA,EAAM;AAAA;AAER,CAAC;AAuBD,SAAS,OAAO,EAAE,SAAA,EAAW,MAAM,IAAA,EAAM,GAAG,OAAM,EAAgB;AACjE,EAAA,uBACC,GAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACA,aAAA,EAAY,MAAA;AAAA,MACZ,SAAA,EAAW,GAAG,cAAA,CAAe,EAAE,MAAM,IAAA,EAAM,GAAG,SAAS,CAAA;AAAA,MACtD,GAAG;AAAA;AAAA,GACL;AAEF","file":"spacer.js","sourcesContent":["import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge class names with Tailwind CSS conflict resolution.\n * @param inputs - Class values (strings, arrays, objects) to merge\n * @returns A single merged class string with Tailwind conflicts resolved\n */\nexport function cn(...inputs: ClassValue[]) {\n\treturn twMerge(clsx(inputs));\n}\n","import { type VariantProps, cva } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/utils.js\";\n\n/**\n * CVA variants for Spacer — declarative whitespace.\n * `size` sets `--spacer-size` to a `--space-*` token (with inline fallback);\n * `axis` consumes that var via the bracket form used everywhere else in the\n * package, so the height or width can never collapse to zero if the size\n * variant is dropped.\n */\nconst spacerVariants = cva(\"shrink-0\", {\n\tvariants: {\n\t\tsize: {\n\t\t\txs: \"[--spacer-size:var(--space-1,0.25rem)]\",\n\t\t\tsm: \"[--spacer-size:var(--space-2,0.5rem)]\",\n\t\t\tmd: \"[--spacer-size:var(--space-4,1rem)]\",\n\t\t\tlg: \"[--spacer-size:var(--space-8,2rem)]\",\n\t\t\txl: \"[--spacer-size:var(--space-16,4rem)]\",\n\t\t},\n\t\taxis: {\n\t\t\tvertical: \"h-[var(--spacer-size)] w-0\",\n\t\t\thorizontal: \"w-[var(--spacer-size)] h-0\",\n\t\t\tboth: \"h-[var(--spacer-size)] w-[var(--spacer-size)]\",\n\t\t},\n\t},\n\tdefaultVariants: {\n\t\tsize: \"md\",\n\t\taxis: \"vertical\",\n\t},\n});\n\n/** Props for the Spacer component. */\nexport interface SpacerProps\n\textends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\">,\n\t\tVariantProps<typeof spacerVariants> {}\n\n/**\n * A declarative whitespace block. Use when you want to insert space between two\n * siblings without relying on margin or gap (e.g. inside a flex container that\n * doesn't own the spacing decision).\n *\n * Renders an empty `<div>` with `aria-hidden` since it has no semantic content.\n *\n * @param props - Spacer props including `size` and `axis` variant keys.\n * @returns An empty div with the requested dimension.\n * @example\n * ```tsx\n * <h1>Title</h1>\n * <Spacer size=\"lg\" />\n * <p>Body</p>\n * ```\n */\nfunction Spacer({ className, size, axis, ...props }: SpacerProps) {\n\treturn (\n\t\t<div\n\t\t\taria-hidden=\"true\"\n\t\t\tclassName={cn(spacerVariants({ size, axis }), className)}\n\t\t\t{...props}\n\t\t/>\n\t);\n}\n\nexport { Spacer, spacerVariants };\n"]}
1
+ {"version":3,"sources":["../src/lib/utils.ts","../src/primitives/spacer/spacer.tsx"],"names":[],"mappings":";;;;;;AAQO,SAAS,MAAM,MAAA,EAAsB;AAC3C,EAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAC,CAAA;AAC5B;ACCA,IAAM,cAAA,GAAiB,IAAI,UAAA,EAAY;AAAA,EACtC,QAAA,EAAU;AAAA,IACT,IAAA,EAAM;AAAA,MACL,EAAA,EAAI,wCAAA;AAAA,MACJ,EAAA,EAAI,uCAAA;AAAA,MACJ,EAAA,EAAI,qCAAA;AAAA,MACJ,EAAA,EAAI,qCAAA;AAAA,MACJ,EAAA,EAAI;AAAA,KACL;AAAA,IACA,IAAA,EAAM;AAAA,MACL,QAAA,EAAU,4BAAA;AAAA,MACV,UAAA,EAAY,4BAAA;AAAA,MACZ,IAAA,EAAM;AAAA;AACP,GACD;AAAA,EACA,eAAA,EAAiB;AAAA,IAChB,IAAA,EAAM,IAAA;AAAA,IACN,IAAA,EAAM;AAAA;AAER,CAAC;AAuBD,SAAS,OAAO,EAAE,SAAA,EAAW,MAAM,IAAA,EAAM,GAAG,OAAM,EAAgB;AACjE,EAAA,uBACC,GAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACA,aAAA,EAAY,MAAA;AAAA,MACZ,SAAA,EAAW,GAAG,cAAA,CAAe,EAAE,MAAM,IAAA,EAAM,GAAG,SAAS,CAAA;AAAA,MACtD,GAAG;AAAA;AAAA,GACL;AAEF","file":"spacer.js","sourcesContent":["import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge class names with Tailwind CSS conflict resolution.\n * @param inputs - Class values (strings, arrays, objects) to merge\n * @returns A single merged class string with Tailwind conflicts resolved\n */\nexport function cn(...inputs: ClassValue[]) {\n\treturn twMerge(clsx(inputs));\n}\n\nconst SAFE_URL_SCHEMES = [\"http:\", \"https:\", \"mailto:\"] as const;\n\n/**\n * Allowlist a URL for use as an `<a href>` against untrusted input.\n *\n * Returns the raw string when it's `http(s):` / `mailto:` / a relative\n * URL (no scheme); returns `undefined` otherwise. Defends against\n * `javascript:` / `data:` / `vbscript:` injection from streamed model\n * output or third-party JSON payloads that flow into citation chips.\n *\n * Inside a Markdown render path, `rehype-sanitize` already strips\n * `javascript:` from inline-link hrefs — but it does NOT introspect\n * JSON nested inside attribute values (e.g. `<sources data='[…]'/>`),\n * so the components that consume that data must guard themselves.\n *\n * @param raw - The candidate URL, or `undefined` when none was supplied.\n * @returns The URL when safe to render, otherwise `undefined`.\n */\nexport function safeUrl(raw: string | undefined): string | undefined {\n\tif (raw === undefined || raw === \"\") return undefined;\n\t// Relative URLs have no scheme — `new URL(relative)` throws without a base,\n\t// so a thrown URL means either malformed input OR a relative path. Treat\n\t// \"throws + does not contain a colon\" as a relative path (safe).\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(raw);\n\t} catch {\n\t\treturn raw.includes(\":\") ? undefined : raw;\n\t}\n\treturn SAFE_URL_SCHEMES.some((scheme) => scheme === parsed.protocol) ? raw : undefined;\n}\n","import { type VariantProps, cva } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/utils.js\";\n\n/**\n * CVA variants for Spacer — declarative whitespace.\n * `size` sets `--spacer-size` to a `--space-*` token (with inline fallback);\n * `axis` consumes that var via the bracket form used everywhere else in the\n * package, so the height or width can never collapse to zero if the size\n * variant is dropped.\n */\nconst spacerVariants = cva(\"shrink-0\", {\n\tvariants: {\n\t\tsize: {\n\t\t\txs: \"[--spacer-size:var(--space-1,0.25rem)]\",\n\t\t\tsm: \"[--spacer-size:var(--space-2,0.5rem)]\",\n\t\t\tmd: \"[--spacer-size:var(--space-4,1rem)]\",\n\t\t\tlg: \"[--spacer-size:var(--space-8,2rem)]\",\n\t\t\txl: \"[--spacer-size:var(--space-16,4rem)]\",\n\t\t},\n\t\taxis: {\n\t\t\tvertical: \"h-[var(--spacer-size)] w-0\",\n\t\t\thorizontal: \"w-[var(--spacer-size)] h-0\",\n\t\t\tboth: \"h-[var(--spacer-size)] w-[var(--spacer-size)]\",\n\t\t},\n\t},\n\tdefaultVariants: {\n\t\tsize: \"md\",\n\t\taxis: \"vertical\",\n\t},\n});\n\n/** Props for the Spacer component. */\nexport interface SpacerProps\n\textends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\">,\n\t\tVariantProps<typeof spacerVariants> {}\n\n/**\n * A declarative whitespace block. Use when you want to insert space between two\n * siblings without relying on margin or gap (e.g. inside a flex container that\n * doesn't own the spacing decision).\n *\n * Renders an empty `<div>` with `aria-hidden` since it has no semantic content.\n *\n * @param props - Spacer props including `size` and `axis` variant keys.\n * @returns An empty div with the requested dimension.\n * @example\n * ```tsx\n * <h1>Title</h1>\n * <Spacer size=\"lg\" />\n * <p>Body</p>\n * ```\n */\nfunction Spacer({ className, size, axis, ...props }: SpacerProps) {\n\treturn (\n\t\t<div\n\t\t\taria-hidden=\"true\"\n\t\t\tclassName={cn(spacerVariants({ size, axis }), className)}\n\t\t\t{...props}\n\t\t/>\n\t);\n}\n\nexport { Spacer, spacerVariants };\n"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/lib/utils.ts","../src/ai/speech-recognition/speech-recognition.tsx"],"names":[],"mappings":"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","file":"speech-recognition.js","sourcesContent":["import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge class names with Tailwind CSS conflict resolution.\n * @param inputs - Class values (strings, arrays, objects) to merge\n * @returns A single merged class string with Tailwind conflicts resolved\n */\nexport function cn(...inputs: ClassValue[]) {\n\treturn twMerge(clsx(inputs));\n}\n","\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"../../lib/utils.js\";\n\n/**\n * Browser SpeechRecognition wrapper. Renders a mic toggle button that\n * starts/stops the Web Speech API and emits transcript chunks.\n *\n * Headless on data: `isListening` + `onListeningChange` are required so\n * the consumer keeps state where it fits (a `useChat` hook, redux,\n * local state). `onTranscript` fires per result with `isFinal` so the\n * consumer can append finalized phrases and replace interim ones.\n *\n * Falls back to a disabled button labeled `notSupportedLabel` when the\n * browser lacks `SpeechRecognition` (Firefox as of 2026, older Safari).\n *\n * @example\n * const [listening, setListening] = useState(false);\n * const [text, setText] = useState(\"\");\n * <SpeechRecognition\n * isListening={listening}\n * onListeningChange={setListening}\n * onTranscript={(chunk, isFinal) => {\n * if (isFinal) setText((t) => t + chunk);\n * }}\n * />\n */\ntype SpeechRecognitionConstructor = new () => SpeechRecognitionInstance;\n\ninterface SpeechRecognitionResultLike {\n\treadonly isFinal: boolean;\n\treadonly length: number;\n\treadonly [index: number]: { readonly transcript: string };\n}\n\ninterface SpeechRecognitionResultListLike {\n\treadonly length: number;\n\treadonly [index: number]: SpeechRecognitionResultLike;\n}\n\ninterface SpeechRecognitionEventLike {\n\treadonly resultIndex: number;\n\treadonly results: SpeechRecognitionResultListLike;\n}\n\ninterface SpeechRecognitionErrorEventLike {\n\treadonly error: string;\n\treadonly message?: string;\n}\n\ninterface SpeechRecognitionInstance {\n\tcontinuous: boolean;\n\tinterimResults: boolean;\n\tlang: string;\n\tonresult: ((event: SpeechRecognitionEventLike) => void) | null;\n\tonerror: ((event: SpeechRecognitionErrorEventLike) => void) | null;\n\tonend: (() => void) | null;\n\tstart(): void;\n\tstop(): void;\n\tabort(): void;\n}\n\nfunction getSpeechRecognitionCtor(): SpeechRecognitionConstructor | null {\n\tif (typeof window === \"undefined\") return null;\n\tconst w = window as unknown as {\n\t\tSpeechRecognition?: SpeechRecognitionConstructor;\n\t\twebkitSpeechRecognition?: SpeechRecognitionConstructor;\n\t};\n\treturn w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;\n}\n\nexport interface SpeechRecognitionProps\n\textends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"onError\"> {\n\t/** Controlled listening state. */\n\tisListening: boolean;\n\t/** Called when listening starts/stops (user toggle or browser auto-end). */\n\tonListeningChange: (listening: boolean) => void;\n\t/** Called per transcript chunk. `isFinal` indicates a finalized phrase. */\n\tonTranscript: (text: string, isFinal: boolean) => void;\n\t/** Called on browser error (e.g. \"not-allowed\", \"no-speech\", \"network\"). */\n\tonError?: (error: string, message?: string) => void;\n\t/** BCP-47 language tag. Default `\"en-US\"`. */\n\tlang?: string;\n\t/** Keep listening across pauses. Default `true`. */\n\tcontinuous?: boolean;\n\t/** Emit interim (in-progress) results. Default `true`. */\n\tinterimResults?: boolean;\n\t/** Accessible name when idle. Default `\"Start dictation\"`. */\n\tstartLabel?: string;\n\t/** Accessible name when listening. Default `\"Stop dictation\"`. */\n\tstopLabel?: string;\n\t/** Accessible name + tooltip when the browser lacks the API. */\n\tnotSupportedLabel?: string;\n}\n\n/**\n * Renders a mic toggle button wired to the Web Speech API.\n * @param props - controlled listening state + transcript callback\n * @returns A button element that toggles speech recognition\n */\nfunction SpeechRecognition({\n\tisListening,\n\tonListeningChange,\n\tonTranscript,\n\tonError,\n\tlang = \"en-US\",\n\tcontinuous = true,\n\tinterimResults = true,\n\tstartLabel = \"Start dictation\",\n\tstopLabel = \"Stop dictation\",\n\tnotSupportedLabel = \"Speech recognition not supported in this browser\",\n\tdisabled,\n\tclassName,\n\t...rest\n}: SpeechRecognitionProps) {\n\tconst recognitionRef = React.useRef<SpeechRecognitionInstance | null>(null);\n\tconst [isSupported, setIsSupported] = React.useState(true);\n\n\t// \"Latest ref\" pattern, assigned synchronously in render so a Web Speech\n\t// callback firing between commit and a useEffect can never see stale\n\t// closures. React permits ref mutation during render when the assignment\n\t// is purely a latest-value mirror.\n\tconst onTranscriptRef = React.useRef(onTranscript);\n\tconst onListeningChangeRef = React.useRef(onListeningChange);\n\tconst onErrorRef = React.useRef(onError);\n\tonTranscriptRef.current = onTranscript;\n\tonListeningChangeRef.current = onListeningChange;\n\tonErrorRef.current = onError;\n\n\t// Mounted guard: the engine fires onend asynchronously, sometimes after\n\t// the React tree has been torn down. Without this, a stale handler can\n\t// invoke setState on an unmounted parent.\n\tconst mountedRef = React.useRef(true);\n\tReact.useEffect(\n\t\t() => () => {\n\t\t\tmountedRef.current = false;\n\t\t},\n\t\t[],\n\t);\n\n\t// Set when the lifecycle effect's cleanup runs because of a prop change\n\t// (lang/continuous/interimResults), not user-initiated stop. The about-\n\t// to-fire onend should NOT bubble back as `onListeningChange(false)` in\n\t// that case — the new effect run is about to re-create the engine.\n\tconst rebuildingRef = React.useRef(false);\n\t// Latest `isListening` mirror so cleanup can read the NEW value (closure\n\t// captures OLD). NEW=true means this is a prop-change rebuild; NEW=false\n\t// means the user stopped.\n\tconst isListeningRef = React.useRef(isListening);\n\tisListeningRef.current = isListening;\n\n\t// SSR: ctor lookup must run after mount.\n\tReact.useEffect(() => {\n\t\tconst Ctor = getSpeechRecognitionCtor();\n\t\tsetIsSupported(Ctor !== null);\n\t}, []);\n\n\t// Toggle the engine on `isListening` change. Recreate per session so a\n\t// stuck session can't leak — Chrome's recognition is single-use after\n\t// onend in some failure modes.\n\tReact.useEffect(() => {\n\t\tif (!isListening) return;\n\n\t\tconst Ctor = getSpeechRecognitionCtor();\n\t\tif (!Ctor) return;\n\n\t\tconst instance = new Ctor();\n\t\tinstance.continuous = continuous;\n\t\tinstance.interimResults = interimResults;\n\t\tinstance.lang = lang;\n\n\t\tinstance.onresult = (event) => {\n\t\t\tif (!mountedRef.current) return;\n\t\t\tfor (let i = event.resultIndex; i < event.results.length; i++) {\n\t\t\t\tconst result = event.results[i];\n\t\t\t\tconst transcript = result[0]?.transcript ?? \"\";\n\t\t\t\tif (transcript) onTranscriptRef.current(transcript, result.isFinal);\n\t\t\t}\n\t\t};\n\t\tinstance.onerror = (event) => {\n\t\t\tif (!mountedRef.current) return;\n\t\t\tonErrorRef.current?.(event.error, event.message);\n\t\t\t// \"aborted\" is a normal stop signal — don't toggle off twice.\n\t\t\tif (event.error !== \"aborted\") onListeningChangeRef.current(false);\n\t\t};\n\t\tinstance.onend = () => {\n\t\t\tif (!mountedRef.current) return;\n\t\t\t// Skip the off-toggle when cleanup is from a prop-change rebuild;\n\t\t\t// the next effect run will re-start with the new options.\n\t\t\tif (rebuildingRef.current) return;\n\t\t\tonListeningChangeRef.current(false);\n\t\t};\n\n\t\trecognitionRef.current = instance;\n\t\ttry {\n\t\t\tinstance.start();\n\t\t} catch (err) {\n\t\t\t// Chrome throws if start() is called twice; surface as an error\n\t\t\t// rather than letting it crash the React tree.\n\t\t\tonErrorRef.current?.(\"start-failed\", err instanceof Error ? err.message : String(err));\n\t\t\tonListeningChangeRef.current(false);\n\t\t}\n\n\t\treturn () => {\n\t\t\t// Mark this teardown as a rebuild iff isListening is STILL true\n\t\t\t// in the latest render (only lang/continuous/interimResults\n\t\t\t// changed). On a real user-stop the NEW isListening is false and\n\t\t\t// any synchronous onend-from-abort should toggle parent state.\n\t\t\trebuildingRef.current = isListeningRef.current;\n\t\t\tinstance.onresult = null;\n\t\t\tinstance.onerror = null;\n\t\t\tinstance.onend = null;\n\t\t\ttry {\n\t\t\t\tinstance.abort();\n\t\t\t} catch {\n\t\t\t\t// abort() throws if the engine never started; safe to ignore.\n\t\t\t}\n\t\t\trecognitionRef.current = null;\n\t\t\t// Reset on next microtask so a follow-up effect run sees a clean slate.\n\t\t\tqueueMicrotask(() => {\n\t\t\t\trebuildingRef.current = false;\n\t\t\t});\n\t\t};\n\t}, [isListening, continuous, interimResults, lang]);\n\n\tconst tooltip = !isSupported\n\t\t? notSupportedLabel\n\t\t: isListening\n\t\t\t? stopLabel\n\t\t\t: startLabel;\n\t// aria-label is stable when supported so screen readers don't re-announce\n\t// the entire button on each toggle. State is conveyed via aria-pressed.\n\t// Only the unsupported case swaps the accessible name.\n\tconst accessibleName = isSupported ? startLabel : notSupportedLabel;\n\tconst isDisabled = disabled || !isSupported;\n\n\treturn (\n\t\t<button\n\t\t\ttype=\"button\"\n\t\t\t{...rest}\n\t\t\tdisabled={isDisabled}\n\t\t\taria-label={accessibleName}\n\t\t\taria-pressed={isListening}\n\t\t\ttitle={tooltip}\n\t\t\tonClick={(event) => {\n\t\t\t\trest.onClick?.(event);\n\t\t\t\tif (event.defaultPrevented || isDisabled) return;\n\t\t\t\tonListeningChange(!isListening);\n\t\t\t}}\n\t\t\tclassName={cn(\n\t\t\t\t\"inline-flex h-9 w-9 items-center justify-center rounded-md border bg-background\",\n\t\t\t\t\"text-foreground transition-colors duration-[var(--duration-normal,200ms)] ease-out\",\n\t\t\t\t\"hover:bg-accent hover:text-accent-foreground\",\n\t\t\t\t\"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n\t\t\t\t\"disabled:cursor-not-allowed disabled:opacity-50\",\n\t\t\t\tisListening && \"animate-pulse border-destructive text-destructive\",\n\t\t\t\tclassName,\n\t\t\t)}\n\t\t>\n\t\t\t<svg\n\t\t\t\taria-hidden\n\t\t\t\tviewBox=\"0 0 16 16\"\n\t\t\t\twidth=\"14\"\n\t\t\t\theight=\"14\"\n\t\t\t\tfill=\"none\"\n\t\t\t\tstroke=\"currentColor\"\n\t\t\t\tstrokeWidth=\"1.5\"\n\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t>\n\t\t\t\t<rect x=\"6\" y=\"2\" width=\"4\" height=\"8\" rx=\"2\" />\n\t\t\t\t<path d=\"M3.5 7.5a4.5 4.5 0 0 0 9 0\" />\n\t\t\t\t<path d=\"M8 12v2\" />\n\t\t\t\t<path d=\"M5.5 14h5\" />\n\t\t\t</svg>\n\t\t</button>\n\t);\n}\n\nexport { SpeechRecognition };\n"]}
1
+ {"version":3,"sources":["../src/lib/utils.ts","../src/ai/speech-recognition/speech-recognition.tsx"],"names":[],"mappings":"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","file":"speech-recognition.js","sourcesContent":["import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge class names with Tailwind CSS conflict resolution.\n * @param inputs - Class values (strings, arrays, objects) to merge\n * @returns A single merged class string with Tailwind conflicts resolved\n */\nexport function cn(...inputs: ClassValue[]) {\n\treturn twMerge(clsx(inputs));\n}\n\nconst SAFE_URL_SCHEMES = [\"http:\", \"https:\", \"mailto:\"] as const;\n\n/**\n * Allowlist a URL for use as an `<a href>` against untrusted input.\n *\n * Returns the raw string when it's `http(s):` / `mailto:` / a relative\n * URL (no scheme); returns `undefined` otherwise. Defends against\n * `javascript:` / `data:` / `vbscript:` injection from streamed model\n * output or third-party JSON payloads that flow into citation chips.\n *\n * Inside a Markdown render path, `rehype-sanitize` already strips\n * `javascript:` from inline-link hrefs — but it does NOT introspect\n * JSON nested inside attribute values (e.g. `<sources data='[…]'/>`),\n * so the components that consume that data must guard themselves.\n *\n * @param raw - The candidate URL, or `undefined` when none was supplied.\n * @returns The URL when safe to render, otherwise `undefined`.\n */\nexport function safeUrl(raw: string | undefined): string | undefined {\n\tif (raw === undefined || raw === \"\") return undefined;\n\t// Relative URLs have no scheme — `new URL(relative)` throws without a base,\n\t// so a thrown URL means either malformed input OR a relative path. Treat\n\t// \"throws + does not contain a colon\" as a relative path (safe).\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(raw);\n\t} catch {\n\t\treturn raw.includes(\":\") ? undefined : raw;\n\t}\n\treturn SAFE_URL_SCHEMES.some((scheme) => scheme === parsed.protocol) ? raw : undefined;\n}\n","\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"../../lib/utils.js\";\n\n/**\n * Browser SpeechRecognition wrapper. Renders a mic toggle button that\n * starts/stops the Web Speech API and emits transcript chunks.\n *\n * Headless on data: `isListening` + `onListeningChange` are required so\n * the consumer keeps state where it fits (a `useChat` hook, redux,\n * local state). `onTranscript` fires per result with `isFinal` so the\n * consumer can append finalized phrases and replace interim ones.\n *\n * Falls back to a disabled button labeled `notSupportedLabel` when the\n * browser lacks `SpeechRecognition` (Firefox as of 2026, older Safari).\n *\n * @example\n * const [listening, setListening] = useState(false);\n * const [text, setText] = useState(\"\");\n * <SpeechRecognition\n * isListening={listening}\n * onListeningChange={setListening}\n * onTranscript={(chunk, isFinal) => {\n * if (isFinal) setText((t) => t + chunk);\n * }}\n * />\n */\ntype SpeechRecognitionConstructor = new () => SpeechRecognitionInstance;\n\ninterface SpeechRecognitionResultLike {\n\treadonly isFinal: boolean;\n\treadonly length: number;\n\treadonly [index: number]: { readonly transcript: string };\n}\n\ninterface SpeechRecognitionResultListLike {\n\treadonly length: number;\n\treadonly [index: number]: SpeechRecognitionResultLike;\n}\n\ninterface SpeechRecognitionEventLike {\n\treadonly resultIndex: number;\n\treadonly results: SpeechRecognitionResultListLike;\n}\n\ninterface SpeechRecognitionErrorEventLike {\n\treadonly error: string;\n\treadonly message?: string;\n}\n\ninterface SpeechRecognitionInstance {\n\tcontinuous: boolean;\n\tinterimResults: boolean;\n\tlang: string;\n\tonresult: ((event: SpeechRecognitionEventLike) => void) | null;\n\tonerror: ((event: SpeechRecognitionErrorEventLike) => void) | null;\n\tonend: (() => void) | null;\n\tstart(): void;\n\tstop(): void;\n\tabort(): void;\n}\n\nfunction getSpeechRecognitionCtor(): SpeechRecognitionConstructor | null {\n\tif (typeof window === \"undefined\") return null;\n\tconst w = window as unknown as {\n\t\tSpeechRecognition?: SpeechRecognitionConstructor;\n\t\twebkitSpeechRecognition?: SpeechRecognitionConstructor;\n\t};\n\treturn w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;\n}\n\nexport interface SpeechRecognitionProps\n\textends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"onError\"> {\n\t/** Controlled listening state. */\n\tisListening: boolean;\n\t/** Called when listening starts/stops (user toggle or browser auto-end). */\n\tonListeningChange: (listening: boolean) => void;\n\t/** Called per transcript chunk. `isFinal` indicates a finalized phrase. */\n\tonTranscript: (text: string, isFinal: boolean) => void;\n\t/** Called on browser error (e.g. \"not-allowed\", \"no-speech\", \"network\"). */\n\tonError?: (error: string, message?: string) => void;\n\t/** BCP-47 language tag. Default `\"en-US\"`. */\n\tlang?: string;\n\t/** Keep listening across pauses. Default `true`. */\n\tcontinuous?: boolean;\n\t/** Emit interim (in-progress) results. Default `true`. */\n\tinterimResults?: boolean;\n\t/** Accessible name when idle. Default `\"Start dictation\"`. */\n\tstartLabel?: string;\n\t/** Accessible name when listening. Default `\"Stop dictation\"`. */\n\tstopLabel?: string;\n\t/** Accessible name + tooltip when the browser lacks the API. */\n\tnotSupportedLabel?: string;\n}\n\n/**\n * Renders a mic toggle button wired to the Web Speech API.\n * @param props - controlled listening state + transcript callback\n * @returns A button element that toggles speech recognition\n */\nfunction SpeechRecognition({\n\tisListening,\n\tonListeningChange,\n\tonTranscript,\n\tonError,\n\tlang = \"en-US\",\n\tcontinuous = true,\n\tinterimResults = true,\n\tstartLabel = \"Start dictation\",\n\tstopLabel = \"Stop dictation\",\n\tnotSupportedLabel = \"Speech recognition not supported in this browser\",\n\tdisabled,\n\tclassName,\n\t...rest\n}: SpeechRecognitionProps) {\n\tconst recognitionRef = React.useRef<SpeechRecognitionInstance | null>(null);\n\tconst [isSupported, setIsSupported] = React.useState(true);\n\n\t// \"Latest ref\" pattern, assigned synchronously in render so a Web Speech\n\t// callback firing between commit and a useEffect can never see stale\n\t// closures. React permits ref mutation during render when the assignment\n\t// is purely a latest-value mirror.\n\tconst onTranscriptRef = React.useRef(onTranscript);\n\tconst onListeningChangeRef = React.useRef(onListeningChange);\n\tconst onErrorRef = React.useRef(onError);\n\tonTranscriptRef.current = onTranscript;\n\tonListeningChangeRef.current = onListeningChange;\n\tonErrorRef.current = onError;\n\n\t// Mounted guard: the engine fires onend asynchronously, sometimes after\n\t// the React tree has been torn down. Without this, a stale handler can\n\t// invoke setState on an unmounted parent.\n\tconst mountedRef = React.useRef(true);\n\tReact.useEffect(\n\t\t() => () => {\n\t\t\tmountedRef.current = false;\n\t\t},\n\t\t[],\n\t);\n\n\t// Set when the lifecycle effect's cleanup runs because of a prop change\n\t// (lang/continuous/interimResults), not user-initiated stop. The about-\n\t// to-fire onend should NOT bubble back as `onListeningChange(false)` in\n\t// that case — the new effect run is about to re-create the engine.\n\tconst rebuildingRef = React.useRef(false);\n\t// Latest `isListening` mirror so cleanup can read the NEW value (closure\n\t// captures OLD). NEW=true means this is a prop-change rebuild; NEW=false\n\t// means the user stopped.\n\tconst isListeningRef = React.useRef(isListening);\n\tisListeningRef.current = isListening;\n\n\t// SSR: ctor lookup must run after mount.\n\tReact.useEffect(() => {\n\t\tconst Ctor = getSpeechRecognitionCtor();\n\t\tsetIsSupported(Ctor !== null);\n\t}, []);\n\n\t// Toggle the engine on `isListening` change. Recreate per session so a\n\t// stuck session can't leak — Chrome's recognition is single-use after\n\t// onend in some failure modes.\n\tReact.useEffect(() => {\n\t\tif (!isListening) return;\n\n\t\tconst Ctor = getSpeechRecognitionCtor();\n\t\tif (!Ctor) return;\n\n\t\tconst instance = new Ctor();\n\t\tinstance.continuous = continuous;\n\t\tinstance.interimResults = interimResults;\n\t\tinstance.lang = lang;\n\n\t\tinstance.onresult = (event) => {\n\t\t\tif (!mountedRef.current) return;\n\t\t\tfor (let i = event.resultIndex; i < event.results.length; i++) {\n\t\t\t\tconst result = event.results[i];\n\t\t\t\tconst transcript = result[0]?.transcript ?? \"\";\n\t\t\t\tif (transcript) onTranscriptRef.current(transcript, result.isFinal);\n\t\t\t}\n\t\t};\n\t\tinstance.onerror = (event) => {\n\t\t\tif (!mountedRef.current) return;\n\t\t\tonErrorRef.current?.(event.error, event.message);\n\t\t\t// \"aborted\" is a normal stop signal — don't toggle off twice.\n\t\t\tif (event.error !== \"aborted\") onListeningChangeRef.current(false);\n\t\t};\n\t\tinstance.onend = () => {\n\t\t\tif (!mountedRef.current) return;\n\t\t\t// Skip the off-toggle when cleanup is from a prop-change rebuild;\n\t\t\t// the next effect run will re-start with the new options.\n\t\t\tif (rebuildingRef.current) return;\n\t\t\tonListeningChangeRef.current(false);\n\t\t};\n\n\t\trecognitionRef.current = instance;\n\t\ttry {\n\t\t\tinstance.start();\n\t\t} catch (err) {\n\t\t\t// Chrome throws if start() is called twice; surface as an error\n\t\t\t// rather than letting it crash the React tree.\n\t\t\tonErrorRef.current?.(\"start-failed\", err instanceof Error ? err.message : String(err));\n\t\t\tonListeningChangeRef.current(false);\n\t\t}\n\n\t\treturn () => {\n\t\t\t// Mark this teardown as a rebuild iff isListening is STILL true\n\t\t\t// in the latest render (only lang/continuous/interimResults\n\t\t\t// changed). On a real user-stop the NEW isListening is false and\n\t\t\t// any synchronous onend-from-abort should toggle parent state.\n\t\t\trebuildingRef.current = isListeningRef.current;\n\t\t\tinstance.onresult = null;\n\t\t\tinstance.onerror = null;\n\t\t\tinstance.onend = null;\n\t\t\ttry {\n\t\t\t\tinstance.abort();\n\t\t\t} catch {\n\t\t\t\t// abort() throws if the engine never started; safe to ignore.\n\t\t\t}\n\t\t\trecognitionRef.current = null;\n\t\t\t// Reset on next microtask so a follow-up effect run sees a clean slate.\n\t\t\tqueueMicrotask(() => {\n\t\t\t\trebuildingRef.current = false;\n\t\t\t});\n\t\t};\n\t}, [isListening, continuous, interimResults, lang]);\n\n\tconst tooltip = !isSupported\n\t\t? notSupportedLabel\n\t\t: isListening\n\t\t\t? stopLabel\n\t\t\t: startLabel;\n\t// aria-label is stable when supported so screen readers don't re-announce\n\t// the entire button on each toggle. State is conveyed via aria-pressed.\n\t// Only the unsupported case swaps the accessible name.\n\tconst accessibleName = isSupported ? startLabel : notSupportedLabel;\n\tconst isDisabled = disabled || !isSupported;\n\n\treturn (\n\t\t<button\n\t\t\ttype=\"button\"\n\t\t\t{...rest}\n\t\t\tdisabled={isDisabled}\n\t\t\taria-label={accessibleName}\n\t\t\taria-pressed={isListening}\n\t\t\ttitle={tooltip}\n\t\t\tonClick={(event) => {\n\t\t\t\trest.onClick?.(event);\n\t\t\t\tif (event.defaultPrevented || isDisabled) return;\n\t\t\t\tonListeningChange(!isListening);\n\t\t\t}}\n\t\t\tclassName={cn(\n\t\t\t\t\"inline-flex h-9 w-9 items-center justify-center rounded-md border bg-background\",\n\t\t\t\t\"text-foreground transition-colors duration-[var(--duration-normal,200ms)] ease-out\",\n\t\t\t\t\"hover:bg-accent hover:text-accent-foreground\",\n\t\t\t\t\"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n\t\t\t\t\"disabled:cursor-not-allowed disabled:opacity-50\",\n\t\t\t\tisListening && \"animate-pulse border-destructive text-destructive\",\n\t\t\t\tclassName,\n\t\t\t)}\n\t\t>\n\t\t\t<svg\n\t\t\t\taria-hidden\n\t\t\t\tviewBox=\"0 0 16 16\"\n\t\t\t\twidth=\"14\"\n\t\t\t\theight=\"14\"\n\t\t\t\tfill=\"none\"\n\t\t\t\tstroke=\"currentColor\"\n\t\t\t\tstrokeWidth=\"1.5\"\n\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t>\n\t\t\t\t<rect x=\"6\" y=\"2\" width=\"4\" height=\"8\" rx=\"2\" />\n\t\t\t\t<path d=\"M3.5 7.5a4.5 4.5 0 0 0 9 0\" />\n\t\t\t\t<path d=\"M8 12v2\" />\n\t\t\t\t<path d=\"M5.5 14h5\" />\n\t\t\t</svg>\n\t\t</button>\n\t);\n}\n\nexport { SpeechRecognition };\n"]}
package/dist/stack.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/lib/utils.ts","../src/primitives/_shared/layout-variants.ts","../src/primitives/stack/stack.tsx"],"names":[],"mappings":";;;;;;AAQO,SAAS,MAAM,MAAA,EAAsB;AAC3C,EAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAC,CAAA;AAC5B;;;ACCO,IAAM,WAAA,GAAc;AAAA,EAC1B,EAAA,EAAI,6BAAA;AAAA,EACJ,EAAA,EAAI,4BAAA;AAAA,EACJ,EAAA,EAAI,0BAAA;AAAA,EACJ,EAAA,EAAI,4BAAA;AAAA,EACJ,EAAA,EAAI;AACL,CAAA;AAGO,IAAM,eAAA,GAAkB;AAAA,EAC9B,KAAA,EAAO,eAAA;AAAA,EACP,MAAA,EAAQ,gBAAA;AAAA,EACR,GAAA,EAAK,aAAA;AAAA,EACL,OAAA,EAAS;AACV,CAAA;AAGO,IAAM,iBAAA,GAAoB;AAAA,EAChC,KAAA,EAAO,aAAA;AAAA,EACP,MAAA,EAAQ,cAAA;AAAA,EACR,GAAA,EAAK,WAAA;AAAA,EACL,OAAA,EAAS;AACV,CAAA;ACnBA,IAAM,aAAA,GAAgB,IAAI,eAAA,EAAiB;AAAA,EAC1C,QAAA,EAAU;AAAA,IACT,GAAA,EAAK,WAAA;AAAA,IACL,KAAA,EAAO,iBAAA;AAAA,IACP,OAAA,EAAS;AAAA,GACV;AAAA,EACA,eAAA,EAAiB;AAAA,IAChB,GAAA,EAAK,IAAA;AAAA,IACL,KAAA,EAAO,SAAA;AAAA,IACP,OAAA,EAAS;AAAA;AAEX,CAAC;AAoBD,SAAS,KAAA,CAAM,EAAE,SAAA,EAAW,GAAA,EAAK,OAAO,OAAA,EAAS,GAAG,OAAM,EAAe;AACxE,EAAA,uBACC,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,EAAA,CAAG,cAAc,EAAE,GAAA,EAAK,KAAA,EAAO,OAAA,EAAS,CAAA,EAAG,SAAS,CAAA,EAAI,GAAG,KAAA,EAAO,CAAA;AAEpF","file":"stack.js","sourcesContent":["import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge class names with Tailwind CSS conflict resolution.\n * @param inputs - Class values (strings, arrays, objects) to merge\n * @returns A single merged class string with Tailwind conflicts resolved\n */\nexport function cn(...inputs: ClassValue[]) {\n\treturn twMerge(clsx(inputs));\n}\n","/**\n * Single source of truth for layout-primitive CVA variant maps.\n *\n * Stack, Cluster, Grid all share `gap` and `justify` value sets; align values\n * differ slightly (`stretch` for column-like flows, `baseline` for row flows).\n * Centralizing the maps here keeps token names and Tailwind classes in one\n * file — when the gap scale changes (renamed token, new step, etc.), all\n * three components update together.\n */\n\n/** Gap scale bound to `--gap-*` tokens. Used by Stack, Cluster, Grid. */\nexport const gapVariants = {\n\txs: \"gap-[var(--gap-xs,0.25rem)]\",\n\tsm: \"gap-[var(--gap-sm,0.5rem)]\",\n\tmd: \"gap-[var(--gap-md,1rem)]\",\n\tlg: \"gap-[var(--gap-lg,1.5rem)]\",\n\txl: \"gap-[var(--gap-xl,2rem)]\",\n} as const;\n\n/** `justify-content` values shared by Stack and Cluster. */\nexport const justifyVariants = {\n\tstart: \"justify-start\",\n\tcenter: \"justify-center\",\n\tend: \"justify-end\",\n\tbetween: \"justify-between\",\n} as const;\n\n/** Cross-axis `align-items` values for vertical/grid flows (column-like). */\nexport const flexAlignVariants = {\n\tstart: \"items-start\",\n\tcenter: \"items-center\",\n\tend: \"items-end\",\n\tstretch: \"items-stretch\",\n} as const;\n\n/** Cross-axis `align-items` values for horizontal flows. Includes `baseline`. */\nexport const clusterAlignVariants = {\n\tstart: \"items-start\",\n\tcenter: \"items-center\",\n\tend: \"items-end\",\n\tstretch: \"items-stretch\",\n\tbaseline: \"items-baseline\",\n} as const;\n","import { type VariantProps, cva } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/utils.js\";\nimport {\n\tflexAlignVariants,\n\tgapVariants,\n\tjustifyVariants,\n} from \"../_shared/layout-variants.js\";\n\n/**\n * CVA variants for Stack — vertical flex flow. `gap`, `align`, and `justify`\n * pull from the shared layout-variant maps so any change to the gap scale\n * propagates to Cluster and Grid simultaneously.\n */\nconst stackVariants = cva(\"flex flex-col\", {\n\tvariants: {\n\t\tgap: gapVariants,\n\t\talign: flexAlignVariants,\n\t\tjustify: justifyVariants,\n\t},\n\tdefaultVariants: {\n\t\tgap: \"md\",\n\t\talign: \"stretch\",\n\t\tjustify: \"start\",\n\t},\n});\n\n/** Props for the Stack component. */\nexport interface StackProps\n\textends React.HTMLAttributes<HTMLDivElement>,\n\t\tVariantProps<typeof stackVariants> {}\n\n/**\n * Vertical flex flow with token-bound gap. Children stack top-to-bottom.\n * @param props - Stack props including `gap`, `align`, and `justify` variant keys.\n * @returns A flex column with consistent vertical spacing.\n * @example\n * ```tsx\n * <Stack gap=\"lg\">\n * <h2>Section title</h2>\n * <p>Paragraph one.</p>\n * <p>Paragraph two.</p>\n * </Stack>\n * ```\n */\nfunction Stack({ className, gap, align, justify, ...props }: StackProps) {\n\treturn (\n\t\t<div className={cn(stackVariants({ gap, align, justify }), className)} {...props} />\n\t);\n}\n\nexport { Stack, stackVariants };\n"]}
1
+ {"version":3,"sources":["../src/lib/utils.ts","../src/primitives/_shared/layout-variants.ts","../src/primitives/stack/stack.tsx"],"names":[],"mappings":";;;;;;AAQO,SAAS,MAAM,MAAA,EAAsB;AAC3C,EAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAC,CAAA;AAC5B;;;ACCO,IAAM,WAAA,GAAc;AAAA,EAC1B,EAAA,EAAI,6BAAA;AAAA,EACJ,EAAA,EAAI,4BAAA;AAAA,EACJ,EAAA,EAAI,0BAAA;AAAA,EACJ,EAAA,EAAI,4BAAA;AAAA,EACJ,EAAA,EAAI;AACL,CAAA;AAGO,IAAM,eAAA,GAAkB;AAAA,EAC9B,KAAA,EAAO,eAAA;AAAA,EACP,MAAA,EAAQ,gBAAA;AAAA,EACR,GAAA,EAAK,aAAA;AAAA,EACL,OAAA,EAAS;AACV,CAAA;AAGO,IAAM,iBAAA,GAAoB;AAAA,EAChC,KAAA,EAAO,aAAA;AAAA,EACP,MAAA,EAAQ,cAAA;AAAA,EACR,GAAA,EAAK,WAAA;AAAA,EACL,OAAA,EAAS;AACV,CAAA;ACnBA,IAAM,aAAA,GAAgB,IAAI,eAAA,EAAiB;AAAA,EAC1C,QAAA,EAAU;AAAA,IACT,GAAA,EAAK,WAAA;AAAA,IACL,KAAA,EAAO,iBAAA;AAAA,IACP,OAAA,EAAS;AAAA,GACV;AAAA,EACA,eAAA,EAAiB;AAAA,IAChB,GAAA,EAAK,IAAA;AAAA,IACL,KAAA,EAAO,SAAA;AAAA,IACP,OAAA,EAAS;AAAA;AAEX,CAAC;AAoBD,SAAS,KAAA,CAAM,EAAE,SAAA,EAAW,GAAA,EAAK,OAAO,OAAA,EAAS,GAAG,OAAM,EAAe;AACxE,EAAA,uBACC,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,EAAA,CAAG,cAAc,EAAE,GAAA,EAAK,KAAA,EAAO,OAAA,EAAS,CAAA,EAAG,SAAS,CAAA,EAAI,GAAG,KAAA,EAAO,CAAA;AAEpF","file":"stack.js","sourcesContent":["import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge class names with Tailwind CSS conflict resolution.\n * @param inputs - Class values (strings, arrays, objects) to merge\n * @returns A single merged class string with Tailwind conflicts resolved\n */\nexport function cn(...inputs: ClassValue[]) {\n\treturn twMerge(clsx(inputs));\n}\n\nconst SAFE_URL_SCHEMES = [\"http:\", \"https:\", \"mailto:\"] as const;\n\n/**\n * Allowlist a URL for use as an `<a href>` against untrusted input.\n *\n * Returns the raw string when it's `http(s):` / `mailto:` / a relative\n * URL (no scheme); returns `undefined` otherwise. Defends against\n * `javascript:` / `data:` / `vbscript:` injection from streamed model\n * output or third-party JSON payloads that flow into citation chips.\n *\n * Inside a Markdown render path, `rehype-sanitize` already strips\n * `javascript:` from inline-link hrefs — but it does NOT introspect\n * JSON nested inside attribute values (e.g. `<sources data='[…]'/>`),\n * so the components that consume that data must guard themselves.\n *\n * @param raw - The candidate URL, or `undefined` when none was supplied.\n * @returns The URL when safe to render, otherwise `undefined`.\n */\nexport function safeUrl(raw: string | undefined): string | undefined {\n\tif (raw === undefined || raw === \"\") return undefined;\n\t// Relative URLs have no scheme — `new URL(relative)` throws without a base,\n\t// so a thrown URL means either malformed input OR a relative path. Treat\n\t// \"throws + does not contain a colon\" as a relative path (safe).\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(raw);\n\t} catch {\n\t\treturn raw.includes(\":\") ? undefined : raw;\n\t}\n\treturn SAFE_URL_SCHEMES.some((scheme) => scheme === parsed.protocol) ? raw : undefined;\n}\n","/**\n * Single source of truth for layout-primitive CVA variant maps.\n *\n * Stack, Cluster, Grid all share `gap` and `justify` value sets; align values\n * differ slightly (`stretch` for column-like flows, `baseline` for row flows).\n * Centralizing the maps here keeps token names and Tailwind classes in one\n * file — when the gap scale changes (renamed token, new step, etc.), all\n * three components update together.\n */\n\n/** Gap scale bound to `--gap-*` tokens. Used by Stack, Cluster, Grid. */\nexport const gapVariants = {\n\txs: \"gap-[var(--gap-xs,0.25rem)]\",\n\tsm: \"gap-[var(--gap-sm,0.5rem)]\",\n\tmd: \"gap-[var(--gap-md,1rem)]\",\n\tlg: \"gap-[var(--gap-lg,1.5rem)]\",\n\txl: \"gap-[var(--gap-xl,2rem)]\",\n} as const;\n\n/** `justify-content` values shared by Stack and Cluster. */\nexport const justifyVariants = {\n\tstart: \"justify-start\",\n\tcenter: \"justify-center\",\n\tend: \"justify-end\",\n\tbetween: \"justify-between\",\n} as const;\n\n/** Cross-axis `align-items` values for vertical/grid flows (column-like). */\nexport const flexAlignVariants = {\n\tstart: \"items-start\",\n\tcenter: \"items-center\",\n\tend: \"items-end\",\n\tstretch: \"items-stretch\",\n} as const;\n\n/** Cross-axis `align-items` values for horizontal flows. Includes `baseline`. */\nexport const clusterAlignVariants = {\n\tstart: \"items-start\",\n\tcenter: \"items-center\",\n\tend: \"items-end\",\n\tstretch: \"items-stretch\",\n\tbaseline: \"items-baseline\",\n} as const;\n","import { type VariantProps, cva } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/utils.js\";\nimport {\n\tflexAlignVariants,\n\tgapVariants,\n\tjustifyVariants,\n} from \"../_shared/layout-variants.js\";\n\n/**\n * CVA variants for Stack — vertical flex flow. `gap`, `align`, and `justify`\n * pull from the shared layout-variant maps so any change to the gap scale\n * propagates to Cluster and Grid simultaneously.\n */\nconst stackVariants = cva(\"flex flex-col\", {\n\tvariants: {\n\t\tgap: gapVariants,\n\t\talign: flexAlignVariants,\n\t\tjustify: justifyVariants,\n\t},\n\tdefaultVariants: {\n\t\tgap: \"md\",\n\t\talign: \"stretch\",\n\t\tjustify: \"start\",\n\t},\n});\n\n/** Props for the Stack component. */\nexport interface StackProps\n\textends React.HTMLAttributes<HTMLDivElement>,\n\t\tVariantProps<typeof stackVariants> {}\n\n/**\n * Vertical flex flow with token-bound gap. Children stack top-to-bottom.\n * @param props - Stack props including `gap`, `align`, and `justify` variant keys.\n * @returns A flex column with consistent vertical spacing.\n * @example\n * ```tsx\n * <Stack gap=\"lg\">\n * <h2>Section title</h2>\n * <p>Paragraph one.</p>\n * <p>Paragraph two.</p>\n * </Stack>\n * ```\n */\nfunction Stack({ className, gap, align, justify, ...props }: StackProps) {\n\treturn (\n\t\t<div className={cn(stackVariants({ gap, align, justify }), className)} {...props} />\n\t);\n}\n\nexport { Stack, stackVariants };\n"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/lib/utils.ts","../src/components/stepper/stepper.tsx"],"names":[],"mappings":"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","file":"stepper.js","sourcesContent":["import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge class names with Tailwind CSS conflict resolution.\n * @param inputs - Class values (strings, arrays, objects) to merge\n * @returns A single merged class string with Tailwind conflicts resolved\n */\nexport function cn(...inputs: ClassValue[]) {\n\treturn twMerge(clsx(inputs));\n}\n","\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/utils.js\";\n\ntype StepStatus = \"complete\" | \"current\" | \"upcoming\" | \"error\";\n\ninterface StepperStep {\n\t/** Stable unique id used as the React key. */\n\tid: string;\n\t/** Step name shown next to the indicator. */\n\tlabel: string;\n\t/** Optional secondary text under the label. */\n\tdescription?: string;\n\t/** Disable the step (only applies when onStepClick is provided). */\n\tdisabled?: boolean;\n\t/**\n\t * Override the index-derived status. Use `\"error\"` to mark a failed step\n\t * (e.g. validation failure in a form wizard); `\"complete\"` / `\"current\"` /\n\t * `\"upcoming\"` are derived from `current` by default.\n\t */\n\tstatus?: StepStatus;\n}\n\nconst stepperRoot = cva(\n\t\"flex w-full gap-[var(--gap-md,1rem)] list-none p-0 m-0\",\n\t{\n\t\tvariants: {\n\t\t\torientation: {\n\t\t\t\thorizontal: \"flex-row items-start\",\n\t\t\t\tvertical: \"flex-col items-stretch\",\n\t\t\t},\n\t\t},\n\t\tdefaultVariants: { orientation: \"horizontal\" },\n\t},\n);\n\n/*\n * Horizontal: each step sizes to its content; the connector (`flex-1`) fills\n * the gap between steps. Going `flex-1` on the step item itself would shrink\n * labels to the point of `text-overflow: ellipsis` (\"Account\" → \"A...\").\n * Vertical: items take the full width so multi-line descriptions wrap\n * naturally below the indicator.\n */\nconst stepItem = cva(\"flex gap-[var(--space-3,0.75rem)]\", {\n\tvariants: {\n\t\torientation: {\n\t\t\thorizontal: \"flex-row items-center\",\n\t\t\tvertical: \"flex-row items-start w-full\",\n\t\t},\n\t},\n\tdefaultVariants: { orientation: \"horizontal\" },\n});\n\nconst stepIndicator = cva(\n\t\"inline-flex shrink-0 items-center justify-center rounded-full border-2 font-medium transition-colors duration-[var(--duration-normal,200ms)] ease-out\",\n\t{\n\t\tvariants: {\n\t\t\tsize: {\n\t\t\t\tsm: \"h-7 w-7 text-xs\",\n\t\t\t\tmd: \"h-[var(--control-height-sm,2.25rem)] w-[var(--control-height-sm,2.25rem)] text-sm\",\n\t\t\t},\n\t\t\tstatus: {\n\t\t\t\tcomplete: \"bg-primary border-primary text-primary-foreground\",\n\t\t\t\tcurrent: \"bg-background border-primary text-primary\",\n\t\t\t\tupcoming: \"bg-background border-input text-muted-foreground\",\n\t\t\t\terror:\n\t\t\t\t\t\"bg-destructive border-destructive text-destructive-foreground\",\n\t\t\t},\n\t\t},\n\t\tdefaultVariants: { size: \"md\", status: \"upcoming\" },\n\t},\n);\n\nconst stepConnector = cva(\"bg-input transition-colors\", {\n\tvariants: {\n\t\torientation: {\n\t\t\thorizontal: \"h-px flex-1 min-w-[var(--space-6,1.5rem)] mx-[var(--space-2,0.5rem)]\",\n\t\t\tvertical: \"w-px self-stretch ml-[1.0625rem] my-[var(--space-1,0.25rem)] min-h-[var(--space-6,1.5rem)]\",\n\t\t},\n\t\tcomplete: {\n\t\t\ttrue: \"bg-primary\",\n\t\t\tfalse: \"\",\n\t\t},\n\t},\n\tdefaultVariants: { orientation: \"horizontal\", complete: false },\n});\n\ninterface StepperProps\n\textends Omit<\n\t\tReact.HTMLAttributes<HTMLOListElement>,\n\t\t\"aria-label\" | \"onClick\"\n\t> {\n\t/** Ordered list of steps. */\n\tsteps: StepperStep[];\n\t/** Index of the current step (controlled). */\n\tcurrent: number;\n\t/** Layout direction. */\n\torientation?: \"horizontal\" | \"vertical\";\n\t/** Indicator size. */\n\tsize?: \"sm\" | \"md\";\n\t/** When provided, each step is rendered as a clickable button. */\n\tonStepClick?: (index: number) => void;\n\t/** Required accessible name for the ordered list. */\n\t\"aria-label\": string;\n}\n\n/** Map a step index against the current pointer to a `StepStatus`. */\nfunction deriveStatus(index: number, current: number): StepStatus {\n\tif (index < current) return \"complete\";\n\tif (index === current) return \"current\";\n\treturn \"upcoming\";\n}\n\n/** Checkmark glyph rendered inside the indicator for completed steps. */\nfunction StepCheck() {\n\treturn (\n\t\t<svg\n\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\tviewBox=\"0 0 24 24\"\n\t\t\tfill=\"none\"\n\t\t\tstroke=\"currentColor\"\n\t\t\tstrokeWidth=\"3\"\n\t\t\tstrokeLinecap=\"round\"\n\t\t\tstrokeLinejoin=\"round\"\n\t\t\tclassName=\"h-4 w-4\"\n\t\t\taria-hidden=\"true\"\n\t\t>\n\t\t\t<polyline points=\"20 6 9 17 4 12\" />\n\t\t</svg>\n\t);\n}\n\n/** Cross glyph rendered inside the indicator for steps with status=\"error\". */\nfunction StepError() {\n\treturn (\n\t\t<svg\n\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\tviewBox=\"0 0 24 24\"\n\t\t\tfill=\"none\"\n\t\t\tstroke=\"currentColor\"\n\t\t\tstrokeWidth=\"3\"\n\t\t\tstrokeLinecap=\"round\"\n\t\t\tstrokeLinejoin=\"round\"\n\t\t\tclassName=\"h-4 w-4\"\n\t\t\taria-hidden=\"true\"\n\t\t>\n\t\t\t<line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n\t\t\t<line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n\t\t</svg>\n\t);\n}\n\ninterface StepIndicatorProps {\n\tindex: number;\n\tstatus: StepStatus;\n\tsize: \"sm\" | \"md\";\n}\n\n/** Circular indicator that flips between number, check, and cross by `status`. */\nfunction StepIndicator({ index, status, size }: StepIndicatorProps) {\n\treturn (\n\t\t<span\n\t\t\tclassName={stepIndicator({ size, status })}\n\t\t\taria-invalid={status === \"error\" ? true : undefined}\n\t\t>\n\t\t\t{status === \"complete\" ? (\n\t\t\t\t<StepCheck />\n\t\t\t) : status === \"error\" ? (\n\t\t\t\t<StepError />\n\t\t\t) : (\n\t\t\t\tindex + 1\n\t\t\t)}\n\t\t</span>\n\t);\n}\n\n/**\n * Linear progress indicator for multi-step flows (form wizards, onboarding,\n * checkout). Pure semantic HTML — `<ol>` of `<li>` with `aria-current=\"step\"`\n * on the current item; per-step `status` overrides allow marking \"error\".\n *\n * Pass `onStepClick` to make completed/non-disabled steps interactive.\n * @returns An accessible ordered step list.\n */\nfunction Stepper({\n\tsteps,\n\tcurrent,\n\torientation = \"horizontal\",\n\tsize = \"md\",\n\tonStepClick,\n\t\"aria-label\": ariaLabel,\n\tclassName,\n\t...rest\n}: StepperProps) {\n\tconst interactive = typeof onStepClick === \"function\";\n\treturn (\n\t\t<ol\n\t\t\taria-label={ariaLabel}\n\t\t\tclassName={cn(stepperRoot({ orientation }), className)}\n\t\t\t{...rest}\n\t\t>\n\t\t\t{steps.map((step, index) => {\n\t\t\t\tconst status = step.status ?? deriveStatus(index, current);\n\t\t\t\tconst isCurrent = status === \"current\";\n\t\t\t\tconst isLast = index === steps.length - 1;\n\t\t\t\tconst labelNode = (\n\t\t\t\t\t<span className=\"flex flex-col gap-[var(--space-1,0.25rem)]\">\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\"text-sm font-medium whitespace-nowrap\",\n\t\t\t\t\t\t\t\tisCurrent && \"text-foreground\",\n\t\t\t\t\t\t\t\tstatus === \"complete\" && \"text-foreground\",\n\t\t\t\t\t\t\t\tstatus === \"upcoming\" && \"text-muted-foreground\",\n\t\t\t\t\t\t\t\tstatus === \"error\" && \"text-destructive\",\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{status === \"complete\" && (\n\t\t\t\t\t\t\t\t<span className=\"sr-only\">Completed: </span>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t{status === \"error\" && (\n\t\t\t\t\t\t\t\t<span className=\"sr-only\">Error: </span>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t{step.label}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t{step.description ? (\n\t\t\t\t\t\t\t<span className=\"text-xs text-muted-foreground\">\n\t\t\t\t\t\t\t\t{step.description}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t) : null}\n\t\t\t\t\t</span>\n\t\t\t\t);\n\n\t\t\t\tconst innerContent = (\n\t\t\t\t\t<>\n\t\t\t\t\t\t<StepIndicator index={index} status={status} size={size} />\n\t\t\t\t\t\t{labelNode}\n\t\t\t\t\t</>\n\t\t\t\t);\n\n\t\t\t\treturn (\n\t\t\t\t\t/*\n\t\t\t\t\t * `aria-current=\"step\"` lives on the <li> itself so screen\n\t\t\t\t\t * readers announce it as part of the listitem's intro\n\t\t\t\t\t * (per WAI guidance for \"step\" lists), not buried on an\n\t\t\t\t\t * interior span/button.\n\t\t\t\t\t */\n\t\t\t\t\t<li\n\t\t\t\t\t\tkey={step.id}\n\t\t\t\t\t\taria-current={isCurrent ? \"step\" : undefined}\n\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\tstepItem({ orientation }),\n\t\t\t\t\t\t\t/*\n\t\t\t\t\t\t\t * Grow only when there's a connector after this step.\n\t\t\t\t\t\t\t * The connector inside takes the extra space via\n\t\t\t\t\t\t\t * `flex-1` so the line spans to the next step.\n\t\t\t\t\t\t\t */\n\t\t\t\t\t\t\t!isLast && orientation === \"horizontal\" && \"flex-1\",\n\t\t\t\t\t\t\t!isLast && orientation === \"vertical\" && \"flex-col\",\n\t\t\t\t\t\t)}\n\t\t\t\t\t>\n\t\t\t\t\t\t{interactive ? (\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tdisabled={step.disabled}\n\t\t\t\t\t\t\t\tonClick={() => onStepClick?.(index)}\n\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\"flex items-center gap-[var(--space-3,0.75rem)] text-left rounded-md transition-colors duration-[var(--duration-normal,200ms)] ease-out\",\n\t\t\t\t\t\t\t\t\t\"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n\t\t\t\t\t\t\t\t\t\"disabled:opacity-50 disabled:pointer-events-none\",\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{innerContent}\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t<span className=\"flex items-center gap-[var(--space-3,0.75rem)]\">\n\t\t\t\t\t\t\t\t{innerContent}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{!isLast ? (\n\t\t\t\t\t\t\t/*\n\t\t\t\t\t\t\t * Connector \"complete\" iff THIS step is finished. An\n\t\t\t\t\t\t\t * explicit \"error\" status on the current step never\n\t\t\t\t\t\t\t * fills the gap to the next step — the user hasn't\n\t\t\t\t\t\t\t * cleared this milestone.\n\t\t\t\t\t\t\t */\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\t\tclassName={stepConnector({\n\t\t\t\t\t\t\t\t\torientation,\n\t\t\t\t\t\t\t\t\tcomplete: index < current && step.status !== \"error\",\n\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t) : null}\n\t\t\t\t\t</li>\n\t\t\t\t);\n\t\t\t})}\n\t\t</ol>\n\t);\n}\nStepper.displayName = \"Stepper\";\n\nexport { Stepper };\nexport type { StepperProps, StepperStep, StepStatus };\n"]}
1
+ {"version":3,"sources":["../src/lib/utils.ts","../src/components/stepper/stepper.tsx"],"names":[],"mappings":"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","file":"stepper.js","sourcesContent":["import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge class names with Tailwind CSS conflict resolution.\n * @param inputs - Class values (strings, arrays, objects) to merge\n * @returns A single merged class string with Tailwind conflicts resolved\n */\nexport function cn(...inputs: ClassValue[]) {\n\treturn twMerge(clsx(inputs));\n}\n\nconst SAFE_URL_SCHEMES = [\"http:\", \"https:\", \"mailto:\"] as const;\n\n/**\n * Allowlist a URL for use as an `<a href>` against untrusted input.\n *\n * Returns the raw string when it's `http(s):` / `mailto:` / a relative\n * URL (no scheme); returns `undefined` otherwise. Defends against\n * `javascript:` / `data:` / `vbscript:` injection from streamed model\n * output or third-party JSON payloads that flow into citation chips.\n *\n * Inside a Markdown render path, `rehype-sanitize` already strips\n * `javascript:` from inline-link hrefs — but it does NOT introspect\n * JSON nested inside attribute values (e.g. `<sources data='[…]'/>`),\n * so the components that consume that data must guard themselves.\n *\n * @param raw - The candidate URL, or `undefined` when none was supplied.\n * @returns The URL when safe to render, otherwise `undefined`.\n */\nexport function safeUrl(raw: string | undefined): string | undefined {\n\tif (raw === undefined || raw === \"\") return undefined;\n\t// Relative URLs have no scheme — `new URL(relative)` throws without a base,\n\t// so a thrown URL means either malformed input OR a relative path. Treat\n\t// \"throws + does not contain a colon\" as a relative path (safe).\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(raw);\n\t} catch {\n\t\treturn raw.includes(\":\") ? undefined : raw;\n\t}\n\treturn SAFE_URL_SCHEMES.some((scheme) => scheme === parsed.protocol) ? raw : undefined;\n}\n","\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/utils.js\";\n\ntype StepStatus = \"complete\" | \"current\" | \"upcoming\" | \"error\";\n\ninterface StepperStep {\n\t/** Stable unique id used as the React key. */\n\tid: string;\n\t/** Step name shown next to the indicator. */\n\tlabel: string;\n\t/** Optional secondary text under the label. */\n\tdescription?: string;\n\t/** Disable the step (only applies when onStepClick is provided). */\n\tdisabled?: boolean;\n\t/**\n\t * Override the index-derived status. Use `\"error\"` to mark a failed step\n\t * (e.g. validation failure in a form wizard); `\"complete\"` / `\"current\"` /\n\t * `\"upcoming\"` are derived from `current` by default.\n\t */\n\tstatus?: StepStatus;\n}\n\nconst stepperRoot = cva(\n\t\"flex w-full gap-[var(--gap-md,1rem)] list-none p-0 m-0\",\n\t{\n\t\tvariants: {\n\t\t\torientation: {\n\t\t\t\thorizontal: \"flex-row items-start\",\n\t\t\t\tvertical: \"flex-col items-stretch\",\n\t\t\t},\n\t\t},\n\t\tdefaultVariants: { orientation: \"horizontal\" },\n\t},\n);\n\n/*\n * Horizontal: each step sizes to its content; the connector (`flex-1`) fills\n * the gap between steps. Going `flex-1` on the step item itself would shrink\n * labels to the point of `text-overflow: ellipsis` (\"Account\" → \"A...\").\n * Vertical: items take the full width so multi-line descriptions wrap\n * naturally below the indicator.\n */\nconst stepItem = cva(\"flex gap-[var(--space-3,0.75rem)]\", {\n\tvariants: {\n\t\torientation: {\n\t\t\thorizontal: \"flex-row items-center\",\n\t\t\tvertical: \"flex-row items-start w-full\",\n\t\t},\n\t},\n\tdefaultVariants: { orientation: \"horizontal\" },\n});\n\nconst stepIndicator = cva(\n\t\"inline-flex shrink-0 items-center justify-center rounded-full border-2 font-medium transition-colors duration-[var(--duration-normal,200ms)] ease-out\",\n\t{\n\t\tvariants: {\n\t\t\tsize: {\n\t\t\t\tsm: \"h-7 w-7 text-xs\",\n\t\t\t\tmd: \"h-[var(--control-height-sm,2.25rem)] w-[var(--control-height-sm,2.25rem)] text-sm\",\n\t\t\t},\n\t\t\tstatus: {\n\t\t\t\tcomplete: \"bg-primary border-primary text-primary-foreground\",\n\t\t\t\tcurrent: \"bg-background border-primary text-primary\",\n\t\t\t\tupcoming: \"bg-background border-input text-muted-foreground\",\n\t\t\t\terror:\n\t\t\t\t\t\"bg-destructive border-destructive text-destructive-foreground\",\n\t\t\t},\n\t\t},\n\t\tdefaultVariants: { size: \"md\", status: \"upcoming\" },\n\t},\n);\n\nconst stepConnector = cva(\"bg-input transition-colors\", {\n\tvariants: {\n\t\torientation: {\n\t\t\thorizontal: \"h-px flex-1 min-w-[var(--space-6,1.5rem)] mx-[var(--space-2,0.5rem)]\",\n\t\t\tvertical: \"w-px self-stretch ml-[1.0625rem] my-[var(--space-1,0.25rem)] min-h-[var(--space-6,1.5rem)]\",\n\t\t},\n\t\tcomplete: {\n\t\t\ttrue: \"bg-primary\",\n\t\t\tfalse: \"\",\n\t\t},\n\t},\n\tdefaultVariants: { orientation: \"horizontal\", complete: false },\n});\n\ninterface StepperProps\n\textends Omit<\n\t\tReact.HTMLAttributes<HTMLOListElement>,\n\t\t\"aria-label\" | \"onClick\"\n\t> {\n\t/** Ordered list of steps. */\n\tsteps: StepperStep[];\n\t/** Index of the current step (controlled). */\n\tcurrent: number;\n\t/** Layout direction. */\n\torientation?: \"horizontal\" | \"vertical\";\n\t/** Indicator size. */\n\tsize?: \"sm\" | \"md\";\n\t/** When provided, each step is rendered as a clickable button. */\n\tonStepClick?: (index: number) => void;\n\t/** Required accessible name for the ordered list. */\n\t\"aria-label\": string;\n}\n\n/** Map a step index against the current pointer to a `StepStatus`. */\nfunction deriveStatus(index: number, current: number): StepStatus {\n\tif (index < current) return \"complete\";\n\tif (index === current) return \"current\";\n\treturn \"upcoming\";\n}\n\n/** Checkmark glyph rendered inside the indicator for completed steps. */\nfunction StepCheck() {\n\treturn (\n\t\t<svg\n\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\tviewBox=\"0 0 24 24\"\n\t\t\tfill=\"none\"\n\t\t\tstroke=\"currentColor\"\n\t\t\tstrokeWidth=\"3\"\n\t\t\tstrokeLinecap=\"round\"\n\t\t\tstrokeLinejoin=\"round\"\n\t\t\tclassName=\"h-4 w-4\"\n\t\t\taria-hidden=\"true\"\n\t\t>\n\t\t\t<polyline points=\"20 6 9 17 4 12\" />\n\t\t</svg>\n\t);\n}\n\n/** Cross glyph rendered inside the indicator for steps with status=\"error\". */\nfunction StepError() {\n\treturn (\n\t\t<svg\n\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\tviewBox=\"0 0 24 24\"\n\t\t\tfill=\"none\"\n\t\t\tstroke=\"currentColor\"\n\t\t\tstrokeWidth=\"3\"\n\t\t\tstrokeLinecap=\"round\"\n\t\t\tstrokeLinejoin=\"round\"\n\t\t\tclassName=\"h-4 w-4\"\n\t\t\taria-hidden=\"true\"\n\t\t>\n\t\t\t<line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n\t\t\t<line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n\t\t</svg>\n\t);\n}\n\ninterface StepIndicatorProps {\n\tindex: number;\n\tstatus: StepStatus;\n\tsize: \"sm\" | \"md\";\n}\n\n/** Circular indicator that flips between number, check, and cross by `status`. */\nfunction StepIndicator({ index, status, size }: StepIndicatorProps) {\n\treturn (\n\t\t<span\n\t\t\tclassName={stepIndicator({ size, status })}\n\t\t\taria-invalid={status === \"error\" ? true : undefined}\n\t\t>\n\t\t\t{status === \"complete\" ? (\n\t\t\t\t<StepCheck />\n\t\t\t) : status === \"error\" ? (\n\t\t\t\t<StepError />\n\t\t\t) : (\n\t\t\t\tindex + 1\n\t\t\t)}\n\t\t</span>\n\t);\n}\n\n/**\n * Linear progress indicator for multi-step flows (form wizards, onboarding,\n * checkout). Pure semantic HTML — `<ol>` of `<li>` with `aria-current=\"step\"`\n * on the current item; per-step `status` overrides allow marking \"error\".\n *\n * Pass `onStepClick` to make completed/non-disabled steps interactive.\n * @returns An accessible ordered step list.\n */\nfunction Stepper({\n\tsteps,\n\tcurrent,\n\torientation = \"horizontal\",\n\tsize = \"md\",\n\tonStepClick,\n\t\"aria-label\": ariaLabel,\n\tclassName,\n\t...rest\n}: StepperProps) {\n\tconst interactive = typeof onStepClick === \"function\";\n\treturn (\n\t\t<ol\n\t\t\taria-label={ariaLabel}\n\t\t\tclassName={cn(stepperRoot({ orientation }), className)}\n\t\t\t{...rest}\n\t\t>\n\t\t\t{steps.map((step, index) => {\n\t\t\t\tconst status = step.status ?? deriveStatus(index, current);\n\t\t\t\tconst isCurrent = status === \"current\";\n\t\t\t\tconst isLast = index === steps.length - 1;\n\t\t\t\tconst labelNode = (\n\t\t\t\t\t<span className=\"flex flex-col gap-[var(--space-1,0.25rem)]\">\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\"text-sm font-medium whitespace-nowrap\",\n\t\t\t\t\t\t\t\tisCurrent && \"text-foreground\",\n\t\t\t\t\t\t\t\tstatus === \"complete\" && \"text-foreground\",\n\t\t\t\t\t\t\t\tstatus === \"upcoming\" && \"text-muted-foreground\",\n\t\t\t\t\t\t\t\tstatus === \"error\" && \"text-destructive\",\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{status === \"complete\" && (\n\t\t\t\t\t\t\t\t<span className=\"sr-only\">Completed: </span>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t{status === \"error\" && (\n\t\t\t\t\t\t\t\t<span className=\"sr-only\">Error: </span>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t{step.label}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t{step.description ? (\n\t\t\t\t\t\t\t<span className=\"text-xs text-muted-foreground\">\n\t\t\t\t\t\t\t\t{step.description}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t) : null}\n\t\t\t\t\t</span>\n\t\t\t\t);\n\n\t\t\t\tconst innerContent = (\n\t\t\t\t\t<>\n\t\t\t\t\t\t<StepIndicator index={index} status={status} size={size} />\n\t\t\t\t\t\t{labelNode}\n\t\t\t\t\t</>\n\t\t\t\t);\n\n\t\t\t\treturn (\n\t\t\t\t\t/*\n\t\t\t\t\t * `aria-current=\"step\"` lives on the <li> itself so screen\n\t\t\t\t\t * readers announce it as part of the listitem's intro\n\t\t\t\t\t * (per WAI guidance for \"step\" lists), not buried on an\n\t\t\t\t\t * interior span/button.\n\t\t\t\t\t */\n\t\t\t\t\t<li\n\t\t\t\t\t\tkey={step.id}\n\t\t\t\t\t\taria-current={isCurrent ? \"step\" : undefined}\n\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\tstepItem({ orientation }),\n\t\t\t\t\t\t\t/*\n\t\t\t\t\t\t\t * Grow only when there's a connector after this step.\n\t\t\t\t\t\t\t * The connector inside takes the extra space via\n\t\t\t\t\t\t\t * `flex-1` so the line spans to the next step.\n\t\t\t\t\t\t\t */\n\t\t\t\t\t\t\t!isLast && orientation === \"horizontal\" && \"flex-1\",\n\t\t\t\t\t\t\t!isLast && orientation === \"vertical\" && \"flex-col\",\n\t\t\t\t\t\t)}\n\t\t\t\t\t>\n\t\t\t\t\t\t{interactive ? (\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tdisabled={step.disabled}\n\t\t\t\t\t\t\t\tonClick={() => onStepClick?.(index)}\n\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\"flex items-center gap-[var(--space-3,0.75rem)] text-left rounded-md transition-colors duration-[var(--duration-normal,200ms)] ease-out\",\n\t\t\t\t\t\t\t\t\t\"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n\t\t\t\t\t\t\t\t\t\"disabled:opacity-50 disabled:pointer-events-none\",\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{innerContent}\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t<span className=\"flex items-center gap-[var(--space-3,0.75rem)]\">\n\t\t\t\t\t\t\t\t{innerContent}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{!isLast ? (\n\t\t\t\t\t\t\t/*\n\t\t\t\t\t\t\t * Connector \"complete\" iff THIS step is finished. An\n\t\t\t\t\t\t\t * explicit \"error\" status on the current step never\n\t\t\t\t\t\t\t * fills the gap to the next step — the user hasn't\n\t\t\t\t\t\t\t * cleared this milestone.\n\t\t\t\t\t\t\t */\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\t\tclassName={stepConnector({\n\t\t\t\t\t\t\t\t\torientation,\n\t\t\t\t\t\t\t\t\tcomplete: index < current && step.status !== \"error\",\n\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t) : null}\n\t\t\t\t\t</li>\n\t\t\t\t);\n\t\t\t})}\n\t\t</ol>\n\t);\n}\nStepper.displayName = \"Stepper\";\n\nexport { Stepper };\nexport type { StepperProps, StepperStep, StepStatus };\n"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/lib/utils.ts","../src/ai/suggestion/suggestion.tsx"],"names":[],"mappings":";;;;;AAQO,SAAS,MAAM,MAAA,EAAsB;AAC3C,EAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAC,CAAA;AAC5B;ACuBA,SAAS,UAAA,CAAW;AAAA,EACnB,KAAA;AAAA,EACA,QAAA;AAAA,EACA,QAAA;AAAA,EACA,SAAA;AAAA,EACA,IAAA,GAAO,QAAA;AAAA,EACP,OAAA;AAAA,EACA,GAAG;AACJ,CAAA,EAAoB;AACnB,EAAA,SAAS,YAAY,KAAA,EAA4C;AAChE,IAAA,MAAM,OAAA,GAAU,KAAA,IAAS,WAAA,CAAY,QAAQ,CAAA;AAC7C,IAAA,QAAA,CAAS,OAAO,CAAA;AAChB,IAAA,OAAA,GAAU,KAAK,CAAA;AAAA,EAChB;AAEA,EAAA,uBACC,GAAA;AAAA,IAAC,QAAA;AAAA,IAAA;AAAA,MACA,IAAA;AAAA,MACA,OAAA,EAAS,WAAA;AAAA,MACT,SAAA,EAAW,EAAA;AAAA,QACV,qGAAA;AAAA,QACA,iEAAA;AAAA,QACA,kEAAA;AAAA,QACA,qBAAA;AAAA,QACA,qGAAA;AAAA,QACA,iDAAA;AAAA,QACA;AAAA,OACD;AAAA,MACC,GAAG,KAAA;AAAA,MAEH;AAAA;AAAA,GACF;AAEF;AAUA,SAAS,YAAY,IAAA,EAA+B;AACnD,EAAA,IAAI,IAAA,IAAQ,IAAA,IAAQ,OAAO,IAAA,KAAS,WAAW,OAAO,EAAA;AACtD,EAAA,IAAI,OAAO,SAAS,QAAA,IAAY,OAAO,SAAS,QAAA,EAAU,OAAO,OAAO,IAAI,CAAA;AAC5E,EAAA,IAAI,MAAM,OAAA,CAAQ,IAAI,CAAA,EAAG,OAAO,KAAK,GAAA,CAAI,WAAW,CAAA,CAAE,IAAA,CAAK,GAAG,CAAA,CAAE,OAAA,CAAQ,MAAA,EAAQ,GAAG,EAAE,IAAA,EAAK;AAC1F,EAAA,IAAU,KAAA,CAAA,cAAA,CAA+C,IAAI,CAAA,EAAG;AAC/D,IAAA,OAAO,WAAA,CAAY,IAAA,CAAK,KAAA,CAAM,QAAQ,CAAA;AAAA,EACvC;AACA,EAAA,OAAO,EAAA;AACR","file":"suggestion.js","sourcesContent":["import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge class names with Tailwind CSS conflict resolution.\n * @param inputs - Class values (strings, arrays, objects) to merge\n * @returns A single merged class string with Tailwind conflicts resolved\n */\nexport function cn(...inputs: ClassValue[]) {\n\treturn twMerge(clsx(inputs));\n}\n","\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"../../lib/utils.js\";\n\n/**\n * Prompt pill / quick-action chip. Click forwards `value` (or the rendered\n * string children) to `onSelect` — typically wired to drop the suggestion\n * into a `Composer` or fire it directly through `useChat.append`.\n *\n * Stateless: no submission logic, no networking. Composer (or its parent)\n * decides whether `onSelect` populates the input or auto-sends.\n *\n * @example\n * <Cluster gap=\"sm\">\n * {prompts.map((p) => (\n * <Suggestion key={p} value={p} onSelect={setInput}>{p}</Suggestion>\n * ))}\n * </Cluster>\n */\nexport interface SuggestionProps\n\textends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"onSelect\" | \"value\"> {\n\t/** Payload passed to `onSelect`. Defaults to the rendered children if a string. */\n\tvalue?: string;\n\tonSelect: (value: string) => void;\n\tchildren: React.ReactNode;\n}\n\n/**\n * Renders a clickable suggestion chip.\n * @param props - value/onSelect + children\n * @returns A styled button element\n */\nfunction Suggestion({\n\tvalue,\n\tonSelect,\n\tchildren,\n\tclassName,\n\ttype = \"button\",\n\tonClick,\n\t...props\n}: SuggestionProps) {\n\tfunction handleClick(event: React.MouseEvent<HTMLButtonElement>) {\n\t\tconst payload = value ?? extractText(children);\n\t\tonSelect(payload);\n\t\tonClick?.(event);\n\t}\n\n\treturn (\n\t\t<button\n\t\t\ttype={type}\n\t\t\tonClick={handleClick}\n\t\t\tclassName={cn(\n\t\t\t\t\"inline-flex items-center rounded-full border border-foreground/15 bg-background px-3 py-1.5 text-sm\",\n\t\t\t\t\"transition-all duration-[var(--duration-normal,200ms)] ease-out\",\n\t\t\t\t\"hover:border-foreground/30 hover:bg-secondary/40 hover:shadow-sm\",\n\t\t\t\t\"active:scale-[0.98]\",\n\t\t\t\t\"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n\t\t\t\t\"disabled:cursor-not-allowed disabled:opacity-50\",\n\t\t\t\tclassName,\n\t\t\t)}\n\t\t\t{...props}\n\t\t>\n\t\t\t{children}\n\t\t</button>\n\t);\n}\n\n/**\n * Recursively pull plain-text out of a ReactNode tree so a `<Suggestion>`\n * with JSX children (e.g. `<Icon /> Try this`) still resolves to the\n * visible label when no `value` prop was set.\n *\n * @param node - children ReactNode\n * @returns The concatenated text content, trimmed of incidental whitespace\n */\nfunction extractText(node: React.ReactNode): string {\n\tif (node == null || typeof node === \"boolean\") return \"\";\n\tif (typeof node === \"string\" || typeof node === \"number\") return String(node);\n\tif (Array.isArray(node)) return node.map(extractText).join(\" \").replace(/\\s+/g, \" \").trim();\n\tif (React.isValidElement<{ children?: React.ReactNode }>(node)) {\n\t\treturn extractText(node.props.children);\n\t}\n\treturn \"\";\n}\n\nexport { Suggestion };\n"]}
1
+ {"version":3,"sources":["../src/lib/utils.ts","../src/ai/suggestion/suggestion.tsx"],"names":[],"mappings":";;;;;AAQO,SAAS,MAAM,MAAA,EAAsB;AAC3C,EAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAC,CAAA;AAC5B;ACuBA,SAAS,UAAA,CAAW;AAAA,EACnB,KAAA;AAAA,EACA,QAAA;AAAA,EACA,QAAA;AAAA,EACA,SAAA;AAAA,EACA,IAAA,GAAO,QAAA;AAAA,EACP,OAAA;AAAA,EACA,GAAG;AACJ,CAAA,EAAoB;AACnB,EAAA,SAAS,YAAY,KAAA,EAA4C;AAChE,IAAA,MAAM,OAAA,GAAU,KAAA,IAAS,WAAA,CAAY,QAAQ,CAAA;AAC7C,IAAA,QAAA,CAAS,OAAO,CAAA;AAChB,IAAA,OAAA,GAAU,KAAK,CAAA;AAAA,EAChB;AAEA,EAAA,uBACC,GAAA;AAAA,IAAC,QAAA;AAAA,IAAA;AAAA,MACA,IAAA;AAAA,MACA,OAAA,EAAS,WAAA;AAAA,MACT,SAAA,EAAW,EAAA;AAAA,QACV,qGAAA;AAAA,QACA,iEAAA;AAAA,QACA,kEAAA;AAAA,QACA,qBAAA;AAAA,QACA,qGAAA;AAAA,QACA,iDAAA;AAAA,QACA;AAAA,OACD;AAAA,MACC,GAAG,KAAA;AAAA,MAEH;AAAA;AAAA,GACF;AAEF;AAUA,SAAS,YAAY,IAAA,EAA+B;AACnD,EAAA,IAAI,IAAA,IAAQ,IAAA,IAAQ,OAAO,IAAA,KAAS,WAAW,OAAO,EAAA;AACtD,EAAA,IAAI,OAAO,SAAS,QAAA,IAAY,OAAO,SAAS,QAAA,EAAU,OAAO,OAAO,IAAI,CAAA;AAC5E,EAAA,IAAI,MAAM,OAAA,CAAQ,IAAI,CAAA,EAAG,OAAO,KAAK,GAAA,CAAI,WAAW,CAAA,CAAE,IAAA,CAAK,GAAG,CAAA,CAAE,OAAA,CAAQ,MAAA,EAAQ,GAAG,EAAE,IAAA,EAAK;AAC1F,EAAA,IAAU,KAAA,CAAA,cAAA,CAA+C,IAAI,CAAA,EAAG;AAC/D,IAAA,OAAO,WAAA,CAAY,IAAA,CAAK,KAAA,CAAM,QAAQ,CAAA;AAAA,EACvC;AACA,EAAA,OAAO,EAAA;AACR","file":"suggestion.js","sourcesContent":["import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge class names with Tailwind CSS conflict resolution.\n * @param inputs - Class values (strings, arrays, objects) to merge\n * @returns A single merged class string with Tailwind conflicts resolved\n */\nexport function cn(...inputs: ClassValue[]) {\n\treturn twMerge(clsx(inputs));\n}\n\nconst SAFE_URL_SCHEMES = [\"http:\", \"https:\", \"mailto:\"] as const;\n\n/**\n * Allowlist a URL for use as an `<a href>` against untrusted input.\n *\n * Returns the raw string when it's `http(s):` / `mailto:` / a relative\n * URL (no scheme); returns `undefined` otherwise. Defends against\n * `javascript:` / `data:` / `vbscript:` injection from streamed model\n * output or third-party JSON payloads that flow into citation chips.\n *\n * Inside a Markdown render path, `rehype-sanitize` already strips\n * `javascript:` from inline-link hrefs — but it does NOT introspect\n * JSON nested inside attribute values (e.g. `<sources data='[…]'/>`),\n * so the components that consume that data must guard themselves.\n *\n * @param raw - The candidate URL, or `undefined` when none was supplied.\n * @returns The URL when safe to render, otherwise `undefined`.\n */\nexport function safeUrl(raw: string | undefined): string | undefined {\n\tif (raw === undefined || raw === \"\") return undefined;\n\t// Relative URLs have no scheme — `new URL(relative)` throws without a base,\n\t// so a thrown URL means either malformed input OR a relative path. Treat\n\t// \"throws + does not contain a colon\" as a relative path (safe).\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(raw);\n\t} catch {\n\t\treturn raw.includes(\":\") ? undefined : raw;\n\t}\n\treturn SAFE_URL_SCHEMES.some((scheme) => scheme === parsed.protocol) ? raw : undefined;\n}\n","\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"../../lib/utils.js\";\n\n/**\n * Prompt pill / quick-action chip. Click forwards `value` (or the rendered\n * string children) to `onSelect` — typically wired to drop the suggestion\n * into a `Composer` or fire it directly through `useChat.append`.\n *\n * Stateless: no submission logic, no networking. Composer (or its parent)\n * decides whether `onSelect` populates the input or auto-sends.\n *\n * @example\n * <Cluster gap=\"sm\">\n * {prompts.map((p) => (\n * <Suggestion key={p} value={p} onSelect={setInput}>{p}</Suggestion>\n * ))}\n * </Cluster>\n */\nexport interface SuggestionProps\n\textends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"onSelect\" | \"value\"> {\n\t/** Payload passed to `onSelect`. Defaults to the rendered children if a string. */\n\tvalue?: string;\n\tonSelect: (value: string) => void;\n\tchildren: React.ReactNode;\n}\n\n/**\n * Renders a clickable suggestion chip.\n * @param props - value/onSelect + children\n * @returns A styled button element\n */\nfunction Suggestion({\n\tvalue,\n\tonSelect,\n\tchildren,\n\tclassName,\n\ttype = \"button\",\n\tonClick,\n\t...props\n}: SuggestionProps) {\n\tfunction handleClick(event: React.MouseEvent<HTMLButtonElement>) {\n\t\tconst payload = value ?? extractText(children);\n\t\tonSelect(payload);\n\t\tonClick?.(event);\n\t}\n\n\treturn (\n\t\t<button\n\t\t\ttype={type}\n\t\t\tonClick={handleClick}\n\t\t\tclassName={cn(\n\t\t\t\t\"inline-flex items-center rounded-full border border-foreground/15 bg-background px-3 py-1.5 text-sm\",\n\t\t\t\t\"transition-all duration-[var(--duration-normal,200ms)] ease-out\",\n\t\t\t\t\"hover:border-foreground/30 hover:bg-secondary/40 hover:shadow-sm\",\n\t\t\t\t\"active:scale-[0.98]\",\n\t\t\t\t\"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n\t\t\t\t\"disabled:cursor-not-allowed disabled:opacity-50\",\n\t\t\t\tclassName,\n\t\t\t)}\n\t\t\t{...props}\n\t\t>\n\t\t\t{children}\n\t\t</button>\n\t);\n}\n\n/**\n * Recursively pull plain-text out of a ReactNode tree so a `<Suggestion>`\n * with JSX children (e.g. `<Icon /> Try this`) still resolves to the\n * visible label when no `value` prop was set.\n *\n * @param node - children ReactNode\n * @returns The concatenated text content, trimmed of incidental whitespace\n */\nfunction extractText(node: React.ReactNode): string {\n\tif (node == null || typeof node === \"boolean\") return \"\";\n\tif (typeof node === \"string\" || typeof node === \"number\") return String(node);\n\tif (Array.isArray(node)) return node.map(extractText).join(\" \").replace(/\\s+/g, \" \").trim();\n\tif (React.isValidElement<{ children?: React.ReactNode }>(node)) {\n\t\treturn extractText(node.props.children);\n\t}\n\treturn \"\";\n}\n\nexport { Suggestion };\n"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/lib/chart-palette.ts","../src/lib/utils.ts","../src/artifacts/sunburst/sunburst.tsx"],"names":[],"mappings":"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","file":"sunburst.js","sourcesContent":["/**\n * Categorical chart palette for diagram primitives that encode categorical\n * data (sunburst, treemap, sankey, chord, funnel, pyramid, venn, matrix).\n * Cycled by an integer key — node index, leaf index, depth-1 ancestor, etc.\n *\n * The values reach into `--chart-1` through `--chart-6`, the dedicated\n * diagram-encoding tokens added in `@hex-core/tokens` 1.4. Each token has a\n * `var(--primary)` fallback so consumers on theme presets that haven't been\n * updated to ship `--chart-N` (or who run a custom theme without the chart\n * family) still see a coherent monochrome rendering instead of black SVG\n * fills.\n *\n * Why a chart palette and not the semantic tokens: `--primary`, `--accent`,\n * `--secondary`, and `--muted` collapse to a single hue family in the\n * default monochrome theme — adjacent segments of a chart end up\n * indistinguishable. Chart tokens are tuned for perceptual differentiation.\n */\nexport const CHART_PALETTE = [\n\t\"hsl(var(--chart-1, var(--primary)))\",\n\t\"hsl(var(--chart-2, var(--primary)))\",\n\t\"hsl(var(--chart-3, var(--primary)))\",\n\t\"hsl(var(--chart-4, var(--primary)))\",\n\t\"hsl(var(--chart-5, var(--primary)))\",\n\t\"hsl(var(--chart-6, var(--primary)))\",\n] as const;\n\n/**\n * Return the chart hue at a stable index. Cycles modulo `CHART_PALETTE.length`\n * so the caller doesn't have to range-check.\n *\n * @param index - Integer key (node index, leaf index, depth-1 ancestor index, …).\n * @returns A `hsl(var(...))` string suitable for SVG `fill` / `stroke`.\n */\nexport function pickChartHue(index: number): string {\n\tconst safe = ((index % CHART_PALETTE.length) + CHART_PALETTE.length) % CHART_PALETTE.length;\n\t// `safe` is provably 0..CHART_PALETTE.length-1 — the assertion documents that.\n\treturn CHART_PALETTE[safe] as string;\n}\n","import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge class names with Tailwind CSS conflict resolution.\n * @param inputs - Class values (strings, arrays, objects) to merge\n * @returns A single merged class string with Tailwind conflicts resolved\n */\nexport function cn(...inputs: ClassValue[]) {\n\treturn twMerge(clsx(inputs));\n}\n","\"use client\";\n\nimport * as React from \"react\";\nimport { pickChartHue } from \"../../lib/chart-palette.js\";\nimport { cn } from \"../../lib/utils.js\";\n\n/**\n * Radial hierarchy by value. Each ring out from the center is a deeper level\n * of the tree; each segment's angular extent is proportional to its summed\n * value. Click any segment to drill into it (the clicked segment becomes the\n * new center); click the center to zoom back out.\n *\n * Heavy peers: requires `d3-hierarchy` and `d3-shape` (~3 KB + ~6 KB gzip).\n *\n * @example\n * <Sunburst\n * root={{\n * id: \"root\", label: \"Total\",\n * children: [\n * { id: \"a\", label: \"A\", value: 60, children: [\n * { id: \"a1\", label: \"A1\", value: 40 },\n * { id: \"a2\", label: \"A2\", value: 20 },\n * ]},\n * { id: \"b\", label: \"B\", value: 30 },\n * ],\n * }}\n * />\n */\nexport type SunburstNode = {\n\tid: string;\n\tlabel: string;\n\tvalue?: number;\n\tchildren?: SunburstNode[];\n};\n\nexport interface SunburstProps extends Omit<React.SVGAttributes<SVGSVGElement>, \"children\"> {\n\t/** Root of the hierarchy. Leaves require a positive `value`; internal nodes are summed. */\n\troot: SunburstNode;\n\t/** Allow a click on a segment to zoom into it as the new center. Default true. */\n\tdrillable?: boolean;\n\t/** Optional content rendered at the SVG center (e.g. total). */\n\tcenterLabel?: React.ReactNode;\n\t/** Pixel size of the rendered SVG (it's square). Default 400. */\n\tsize?: number;\n\t/** Fired when a segment is clicked. Fires before any internal drill. */\n\tonSegmentClick?: (node: SunburstNode) => void;\n}\n\ninterface LaidOutSegment {\n\tnode: SunburstNode;\n\tdepth: number;\n\tx0: number;\n\tx1: number;\n\ty0: number;\n\ty1: number;\n\t/** Index of this segment's depth-1 ancestor among its siblings.\n\t * Used to pick a hue from CHART_PALETTE so all descendants of the\n\t * same top-level branch share a color family. */\n\trootSiblingIdx: number;\n}\n\ntype D3HierarchyMod = typeof import(\"d3-hierarchy\");\ntype D3ShapeMod = typeof import(\"d3-shape\");\n\n/**\n * Sunburst opacity ramp: deeper rings render at lower opacity so they\n * read as subdivisions of their parent without obscuring the parent hue.\n * Hand-tuned: outermost ring lands ~0.45, innermost stays at 0.85.\n *\n * @param depth - Ring depth (1-indexed; 1 is the inner ring).\n * @param maxDepth - The maximum ring depth in the current focused tree.\n * @returns Opacity in [0.45, 0.85].\n */\nfunction depthOpacity(depth: number, maxDepth: number): number {\n\tif (maxDepth <= 1) return 0.85;\n\tconst t = (depth - 1) / Math.max(1, maxDepth - 1);\n\treturn 0.85 - t * 0.4;\n}\n\nfunction Sunburst({\n\troot,\n\tdrillable = true,\n\tcenterLabel,\n\tsize = 400,\n\tonSegmentClick,\n\tclassName,\n\t...rest\n}: SunburstProps) {\n\tconst [d3h, setD3h] = React.useState<D3HierarchyMod | null>(null);\n\tconst [d3s, setD3s] = React.useState<D3ShapeMod | null>(null);\n\tconst [focusId, setFocusId] = React.useState<string>(root.id);\n\n\tReact.useEffect(() => {\n\t\tlet cancelled = false;\n\t\tvoid Promise.all([import(\"d3-hierarchy\"), import(\"d3-shape\")]).then(([h, s]) => {\n\t\t\tif (cancelled) return;\n\t\t\tsetD3h(h);\n\t\t\tsetD3s(s);\n\t\t});\n\t\treturn () => {\n\t\t\tcancelled = true;\n\t\t};\n\t}, []);\n\n\t// Reset focus when root changes\n\tReact.useEffect(() => {\n\t\tsetFocusId(root.id);\n\t}, [root]);\n\n\tif (!d3h || !d3s) {\n\t\treturn (\n\t\t\t<div\n\t\t\t\tdata-hex-sunburst-loading\n\t\t\t\taria-busy=\"true\"\n\t\t\t\tclassName={cn(\"inline-block bg-muted/20\", className)}\n\t\t\t\tstyle={{ width: size, height: size }}\n\t\t\t/>\n\t\t);\n\t}\n\n\tconst focused = findNode(root, focusId) ?? root;\n\tconst radius = size / 2;\n\tconst segments = layout(d3h, focused, radius);\n\tconst maxDepth = segments.reduce((m, s) => Math.max(m, s.depth), 1);\n\tconst arc = d3s\n\t\t.arc<LaidOutSegment>()\n\t\t.startAngle((s) => s.x0)\n\t\t.endAngle((s) => s.x1)\n\t\t.innerRadius((s) => s.y0)\n\t\t.outerRadius((s) => s.y1)\n\t\t.padAngle(0.005)\n\t\t.padRadius(radius);\n\n\tconst handleSegmentClick = (segment: LaidOutSegment) => {\n\t\tonSegmentClick?.(segment.node);\n\t\tif (!drillable) return;\n\t\tconst hasChildren = segment.node.children && segment.node.children.length > 0;\n\t\tif (!hasChildren) return;\n\t\tsetFocusId(segment.node.id);\n\t};\n\n\tconst handleCenterClick = () => {\n\t\tif (!drillable || focusId === root.id) return;\n\t\tsetFocusId(root.id);\n\t};\n\n\tconst desc = `Sunburst with ${segments.length} segments, focused on \"${focused.label}\"`;\n\n\treturn (\n\t\t<svg\n\t\t\t{...rest}\n\t\t\tdata-hex-sunburst\n\t\t\tdata-focus-id={focusId}\n\t\t\trole=\"img\"\n\t\t\twidth={size}\n\t\t\theight={size}\n\t\t\tviewBox={`${-radius} ${-radius} ${size} ${size}`}\n\t\t\tclassName={cn(\"block\", className)}\n\t\t>\n\t\t\t<title>Sunburst</title>\n\t\t\t<desc>{desc}</desc>\n\t\t\t<g data-hex-sunburst-segments>\n\t\t\t\t{segments\n\t\t\t\t\t.filter((s) => s.depth > 0)\n\t\t\t\t\t.map((s) => (\n\t\t\t\t\t\t<path\n\t\t\t\t\t\t\tkey={s.node.id}\n\t\t\t\t\t\t\tdata-hex-sunburst-segment\n\t\t\t\t\t\t\tdata-depth={s.depth}\n\t\t\t\t\t\t\td={arc(s) ?? \"\"}\n\t\t\t\t\t\t\tfill={pickChartHue(s.rootSiblingIdx)}\n\t\t\t\t\t\t\tfillOpacity={depthOpacity(s.depth, maxDepth)}\n\t\t\t\t\t\t\tstroke=\"hsl(var(--background))\"\n\t\t\t\t\t\t\tstrokeWidth={1}\n\t\t\t\t\t\t\tstyle={drillable || onSegmentClick ? { cursor: \"pointer\" } : undefined}\n\t\t\t\t\t\t\tonClick={drillable || onSegmentClick ? () => handleSegmentClick(s) : undefined}\n\t\t\t\t\t\t/>\n\t\t\t\t\t))}\n\t\t\t</g>\n\t\t\t<g data-hex-sunburst-labels aria-hidden=\"true\" pointerEvents=\"none\">\n\t\t\t\t{segments\n\t\t\t\t\t.filter((s) => s.depth > 0 && s.x1 - s.x0 > 0.18)\n\t\t\t\t\t.map((s) => {\n\t\t\t\t\t\tconst angle = (s.x0 + s.x1) / 2;\n\t\t\t\t\t\tconst rMid = (s.y0 + s.y1) / 2;\n\t\t\t\t\t\tconst cx = Math.sin(angle) * rMid;\n\t\t\t\t\t\tconst cy = -Math.cos(angle) * rMid;\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<text\n\t\t\t\t\t\t\t\tkey={`${s.node.id}-label`}\n\t\t\t\t\t\t\t\tx={cx}\n\t\t\t\t\t\t\t\ty={cy}\n\t\t\t\t\t\t\t\ttextAnchor=\"middle\"\n\t\t\t\t\t\t\t\tdy=\"0.35em\"\n\t\t\t\t\t\t\t\tfontSize={11}\n\t\t\t\t\t\t\t\tfontWeight={500}\n\t\t\t\t\t\t\t\tfill=\"hsl(var(--background))\"\n\t\t\t\t\t\t\t\tstyle={{ paintOrder: \"stroke\", stroke: \"hsl(var(--foreground) / 0.25)\", strokeWidth: 2 }}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{s.node.label}\n\t\t\t\t\t\t\t</text>\n\t\t\t\t\t\t);\n\t\t\t\t\t})}\n\t\t\t</g>\n\t\t\t<g\n\t\t\t\tdata-hex-sunburst-center\n\t\t\t\tstyle={drillable && focusId !== root.id ? { cursor: \"pointer\" } : undefined}\n\t\t\t\tonClick={drillable ? handleCenterClick : undefined}\n\t\t\t>\n\t\t\t\t<circle r={radius / 4} fill=\"hsl(var(--card))\" stroke=\"hsl(var(--border))\" strokeWidth={1} />\n\t\t\t\t{centerLabel ? (\n\t\t\t\t\t<foreignObject x={-radius / 4} y={-radius / 4} width={radius / 2} height={radius / 2}>\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t\t\theight: \"100%\",\n\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\t\t\tjustifyContent: \"center\",\n\t\t\t\t\t\t\t\ttextAlign: \"center\",\n\t\t\t\t\t\t\t\tfontSize: 12,\n\t\t\t\t\t\t\t\tpadding: 4,\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{centerLabel}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</foreignObject>\n\t\t\t\t) : (\n\t\t\t\t\t<text\n\t\t\t\t\t\ttextAnchor=\"middle\"\n\t\t\t\t\t\tdy=\"0.35em\"\n\t\t\t\t\t\tfontSize={12}\n\t\t\t\t\t\tfontWeight={600}\n\t\t\t\t\t\tfill=\"hsl(var(--foreground))\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{focused.label}\n\t\t\t\t\t</text>\n\t\t\t\t)}\n\t\t\t</g>\n\t\t</svg>\n\t);\n}\n\nfunction layout(d3h: D3HierarchyMod, focused: SunburstNode, radius: number): LaidOutSegment[] {\n\tconst hierarchy = d3h\n\t\t.hierarchy<SunburstNode>(focused)\n\t\t.sum((d) => (d.children && d.children.length > 0 ? 0 : d.value ?? 0))\n\t\t.sort((a, b) => (b.value ?? 0) - (a.value ?? 0));\n\t// partition(hierarchy) returns HierarchyRectangularNode<T> with typed\n\t// x0/x1/y0/y1 (angle range × radius range for sunburst).\n\tconst layoutRoot = d3h.partition<SunburstNode>().size([2 * Math.PI, radius])(hierarchy);\n\tconst segments: LaidOutSegment[] = [];\n\tlayoutRoot.each((d) => {\n\t\t// Walk up to the depth-1 ancestor so all descendants of \"Equity\"\n\t\t// share the Equity hue, distinct from \"Fixed Income\".\n\t\tlet cursor: typeof d | null = d;\n\t\twhile (cursor && cursor.depth > 1) cursor = cursor.parent;\n\t\tconst ancestorIdx = cursor?.parent?.children?.indexOf(cursor) ?? 0;\n\t\tsegments.push({\n\t\t\tnode: d.data,\n\t\t\tdepth: d.depth,\n\t\t\tx0: d.x0,\n\t\t\tx1: d.x1,\n\t\t\ty0: d.y0,\n\t\t\ty1: d.y1,\n\t\t\trootSiblingIdx: Math.max(0, ancestorIdx),\n\t\t});\n\t});\n\treturn segments;\n}\n\nfunction findNode(root: SunburstNode, id: string): SunburstNode | null {\n\tif (root.id === id) return root;\n\tif (!root.children) return null;\n\tfor (const c of root.children) {\n\t\tconst f = findNode(c, id);\n\t\tif (f) return f;\n\t}\n\treturn null;\n}\n\nexport { Sunburst };\n"]}
1
+ {"version":3,"sources":["../src/lib/chart-palette.ts","../src/lib/utils.ts","../src/artifacts/sunburst/sunburst.tsx"],"names":[],"mappings":"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","file":"sunburst.js","sourcesContent":["/**\n * Categorical chart palette for diagram primitives that encode categorical\n * data (sunburst, treemap, sankey, chord, funnel, pyramid, venn, matrix).\n * Cycled by an integer key — node index, leaf index, depth-1 ancestor, etc.\n *\n * The values reach into `--chart-1` through `--chart-6`, the dedicated\n * diagram-encoding tokens added in `@hex-core/tokens` 1.4. Each token has a\n * `var(--primary)` fallback so consumers on theme presets that haven't been\n * updated to ship `--chart-N` (or who run a custom theme without the chart\n * family) still see a coherent monochrome rendering instead of black SVG\n * fills.\n *\n * Why a chart palette and not the semantic tokens: `--primary`, `--accent`,\n * `--secondary`, and `--muted` collapse to a single hue family in the\n * default monochrome theme — adjacent segments of a chart end up\n * indistinguishable. Chart tokens are tuned for perceptual differentiation.\n */\nexport const CHART_PALETTE = [\n\t\"hsl(var(--chart-1, var(--primary)))\",\n\t\"hsl(var(--chart-2, var(--primary)))\",\n\t\"hsl(var(--chart-3, var(--primary)))\",\n\t\"hsl(var(--chart-4, var(--primary)))\",\n\t\"hsl(var(--chart-5, var(--primary)))\",\n\t\"hsl(var(--chart-6, var(--primary)))\",\n] as const;\n\n/**\n * Return the chart hue at a stable index. Cycles modulo `CHART_PALETTE.length`\n * so the caller doesn't have to range-check.\n *\n * @param index - Integer key (node index, leaf index, depth-1 ancestor index, …).\n * @returns A `hsl(var(...))` string suitable for SVG `fill` / `stroke`.\n */\nexport function pickChartHue(index: number): string {\n\tconst safe = ((index % CHART_PALETTE.length) + CHART_PALETTE.length) % CHART_PALETTE.length;\n\t// `safe` is provably 0..CHART_PALETTE.length-1 — the assertion documents that.\n\treturn CHART_PALETTE[safe] as string;\n}\n","import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge class names with Tailwind CSS conflict resolution.\n * @param inputs - Class values (strings, arrays, objects) to merge\n * @returns A single merged class string with Tailwind conflicts resolved\n */\nexport function cn(...inputs: ClassValue[]) {\n\treturn twMerge(clsx(inputs));\n}\n\nconst SAFE_URL_SCHEMES = [\"http:\", \"https:\", \"mailto:\"] as const;\n\n/**\n * Allowlist a URL for use as an `<a href>` against untrusted input.\n *\n * Returns the raw string when it's `http(s):` / `mailto:` / a relative\n * URL (no scheme); returns `undefined` otherwise. Defends against\n * `javascript:` / `data:` / `vbscript:` injection from streamed model\n * output or third-party JSON payloads that flow into citation chips.\n *\n * Inside a Markdown render path, `rehype-sanitize` already strips\n * `javascript:` from inline-link hrefs — but it does NOT introspect\n * JSON nested inside attribute values (e.g. `<sources data='[…]'/>`),\n * so the components that consume that data must guard themselves.\n *\n * @param raw - The candidate URL, or `undefined` when none was supplied.\n * @returns The URL when safe to render, otherwise `undefined`.\n */\nexport function safeUrl(raw: string | undefined): string | undefined {\n\tif (raw === undefined || raw === \"\") return undefined;\n\t// Relative URLs have no scheme — `new URL(relative)` throws without a base,\n\t// so a thrown URL means either malformed input OR a relative path. Treat\n\t// \"throws + does not contain a colon\" as a relative path (safe).\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(raw);\n\t} catch {\n\t\treturn raw.includes(\":\") ? undefined : raw;\n\t}\n\treturn SAFE_URL_SCHEMES.some((scheme) => scheme === parsed.protocol) ? raw : undefined;\n}\n","\"use client\";\n\nimport * as React from \"react\";\nimport { pickChartHue } from \"../../lib/chart-palette.js\";\nimport { cn } from \"../../lib/utils.js\";\n\n/**\n * Radial hierarchy by value. Each ring out from the center is a deeper level\n * of the tree; each segment's angular extent is proportional to its summed\n * value. Click any segment to drill into it (the clicked segment becomes the\n * new center); click the center to zoom back out.\n *\n * Heavy peers: requires `d3-hierarchy` and `d3-shape` (~3 KB + ~6 KB gzip).\n *\n * @example\n * <Sunburst\n * root={{\n * id: \"root\", label: \"Total\",\n * children: [\n * { id: \"a\", label: \"A\", value: 60, children: [\n * { id: \"a1\", label: \"A1\", value: 40 },\n * { id: \"a2\", label: \"A2\", value: 20 },\n * ]},\n * { id: \"b\", label: \"B\", value: 30 },\n * ],\n * }}\n * />\n */\nexport type SunburstNode = {\n\tid: string;\n\tlabel: string;\n\tvalue?: number;\n\tchildren?: SunburstNode[];\n};\n\nexport interface SunburstProps extends Omit<React.SVGAttributes<SVGSVGElement>, \"children\"> {\n\t/** Root of the hierarchy. Leaves require a positive `value`; internal nodes are summed. */\n\troot: SunburstNode;\n\t/** Allow a click on a segment to zoom into it as the new center. Default true. */\n\tdrillable?: boolean;\n\t/** Optional content rendered at the SVG center (e.g. total). */\n\tcenterLabel?: React.ReactNode;\n\t/** Pixel size of the rendered SVG (it's square). Default 400. */\n\tsize?: number;\n\t/** Fired when a segment is clicked. Fires before any internal drill. */\n\tonSegmentClick?: (node: SunburstNode) => void;\n}\n\ninterface LaidOutSegment {\n\tnode: SunburstNode;\n\tdepth: number;\n\tx0: number;\n\tx1: number;\n\ty0: number;\n\ty1: number;\n\t/** Index of this segment's depth-1 ancestor among its siblings.\n\t * Used to pick a hue from CHART_PALETTE so all descendants of the\n\t * same top-level branch share a color family. */\n\trootSiblingIdx: number;\n}\n\ntype D3HierarchyMod = typeof import(\"d3-hierarchy\");\ntype D3ShapeMod = typeof import(\"d3-shape\");\n\n/**\n * Sunburst opacity ramp: deeper rings render at lower opacity so they\n * read as subdivisions of their parent without obscuring the parent hue.\n * Hand-tuned: outermost ring lands ~0.45, innermost stays at 0.85.\n *\n * @param depth - Ring depth (1-indexed; 1 is the inner ring).\n * @param maxDepth - The maximum ring depth in the current focused tree.\n * @returns Opacity in [0.45, 0.85].\n */\nfunction depthOpacity(depth: number, maxDepth: number): number {\n\tif (maxDepth <= 1) return 0.85;\n\tconst t = (depth - 1) / Math.max(1, maxDepth - 1);\n\treturn 0.85 - t * 0.4;\n}\n\nfunction Sunburst({\n\troot,\n\tdrillable = true,\n\tcenterLabel,\n\tsize = 400,\n\tonSegmentClick,\n\tclassName,\n\t...rest\n}: SunburstProps) {\n\tconst [d3h, setD3h] = React.useState<D3HierarchyMod | null>(null);\n\tconst [d3s, setD3s] = React.useState<D3ShapeMod | null>(null);\n\tconst [focusId, setFocusId] = React.useState<string>(root.id);\n\n\tReact.useEffect(() => {\n\t\tlet cancelled = false;\n\t\tvoid Promise.all([import(\"d3-hierarchy\"), import(\"d3-shape\")]).then(([h, s]) => {\n\t\t\tif (cancelled) return;\n\t\t\tsetD3h(h);\n\t\t\tsetD3s(s);\n\t\t});\n\t\treturn () => {\n\t\t\tcancelled = true;\n\t\t};\n\t}, []);\n\n\t// Reset focus when root changes\n\tReact.useEffect(() => {\n\t\tsetFocusId(root.id);\n\t}, [root]);\n\n\tif (!d3h || !d3s) {\n\t\treturn (\n\t\t\t<div\n\t\t\t\tdata-hex-sunburst-loading\n\t\t\t\taria-busy=\"true\"\n\t\t\t\tclassName={cn(\"inline-block bg-muted/20\", className)}\n\t\t\t\tstyle={{ width: size, height: size }}\n\t\t\t/>\n\t\t);\n\t}\n\n\tconst focused = findNode(root, focusId) ?? root;\n\tconst radius = size / 2;\n\tconst segments = layout(d3h, focused, radius);\n\tconst maxDepth = segments.reduce((m, s) => Math.max(m, s.depth), 1);\n\tconst arc = d3s\n\t\t.arc<LaidOutSegment>()\n\t\t.startAngle((s) => s.x0)\n\t\t.endAngle((s) => s.x1)\n\t\t.innerRadius((s) => s.y0)\n\t\t.outerRadius((s) => s.y1)\n\t\t.padAngle(0.005)\n\t\t.padRadius(radius);\n\n\tconst handleSegmentClick = (segment: LaidOutSegment) => {\n\t\tonSegmentClick?.(segment.node);\n\t\tif (!drillable) return;\n\t\tconst hasChildren = segment.node.children && segment.node.children.length > 0;\n\t\tif (!hasChildren) return;\n\t\tsetFocusId(segment.node.id);\n\t};\n\n\tconst handleCenterClick = () => {\n\t\tif (!drillable || focusId === root.id) return;\n\t\tsetFocusId(root.id);\n\t};\n\n\tconst desc = `Sunburst with ${segments.length} segments, focused on \"${focused.label}\"`;\n\n\treturn (\n\t\t<svg\n\t\t\t{...rest}\n\t\t\tdata-hex-sunburst\n\t\t\tdata-focus-id={focusId}\n\t\t\trole=\"img\"\n\t\t\twidth={size}\n\t\t\theight={size}\n\t\t\tviewBox={`${-radius} ${-radius} ${size} ${size}`}\n\t\t\tclassName={cn(\"block\", className)}\n\t\t>\n\t\t\t<title>Sunburst</title>\n\t\t\t<desc>{desc}</desc>\n\t\t\t<g data-hex-sunburst-segments>\n\t\t\t\t{segments\n\t\t\t\t\t.filter((s) => s.depth > 0)\n\t\t\t\t\t.map((s) => (\n\t\t\t\t\t\t<path\n\t\t\t\t\t\t\tkey={s.node.id}\n\t\t\t\t\t\t\tdata-hex-sunburst-segment\n\t\t\t\t\t\t\tdata-depth={s.depth}\n\t\t\t\t\t\t\td={arc(s) ?? \"\"}\n\t\t\t\t\t\t\tfill={pickChartHue(s.rootSiblingIdx)}\n\t\t\t\t\t\t\tfillOpacity={depthOpacity(s.depth, maxDepth)}\n\t\t\t\t\t\t\tstroke=\"hsl(var(--background))\"\n\t\t\t\t\t\t\tstrokeWidth={1}\n\t\t\t\t\t\t\tstyle={drillable || onSegmentClick ? { cursor: \"pointer\" } : undefined}\n\t\t\t\t\t\t\tonClick={drillable || onSegmentClick ? () => handleSegmentClick(s) : undefined}\n\t\t\t\t\t\t/>\n\t\t\t\t\t))}\n\t\t\t</g>\n\t\t\t<g data-hex-sunburst-labels aria-hidden=\"true\" pointerEvents=\"none\">\n\t\t\t\t{segments\n\t\t\t\t\t.filter((s) => s.depth > 0 && s.x1 - s.x0 > 0.18)\n\t\t\t\t\t.map((s) => {\n\t\t\t\t\t\tconst angle = (s.x0 + s.x1) / 2;\n\t\t\t\t\t\tconst rMid = (s.y0 + s.y1) / 2;\n\t\t\t\t\t\tconst cx = Math.sin(angle) * rMid;\n\t\t\t\t\t\tconst cy = -Math.cos(angle) * rMid;\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<text\n\t\t\t\t\t\t\t\tkey={`${s.node.id}-label`}\n\t\t\t\t\t\t\t\tx={cx}\n\t\t\t\t\t\t\t\ty={cy}\n\t\t\t\t\t\t\t\ttextAnchor=\"middle\"\n\t\t\t\t\t\t\t\tdy=\"0.35em\"\n\t\t\t\t\t\t\t\tfontSize={11}\n\t\t\t\t\t\t\t\tfontWeight={500}\n\t\t\t\t\t\t\t\tfill=\"hsl(var(--background))\"\n\t\t\t\t\t\t\t\tstyle={{ paintOrder: \"stroke\", stroke: \"hsl(var(--foreground) / 0.25)\", strokeWidth: 2 }}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{s.node.label}\n\t\t\t\t\t\t\t</text>\n\t\t\t\t\t\t);\n\t\t\t\t\t})}\n\t\t\t</g>\n\t\t\t<g\n\t\t\t\tdata-hex-sunburst-center\n\t\t\t\tstyle={drillable && focusId !== root.id ? { cursor: \"pointer\" } : undefined}\n\t\t\t\tonClick={drillable ? handleCenterClick : undefined}\n\t\t\t>\n\t\t\t\t<circle r={radius / 4} fill=\"hsl(var(--card))\" stroke=\"hsl(var(--border))\" strokeWidth={1} />\n\t\t\t\t{centerLabel ? (\n\t\t\t\t\t<foreignObject x={-radius / 4} y={-radius / 4} width={radius / 2} height={radius / 2}>\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t\t\theight: \"100%\",\n\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\t\t\tjustifyContent: \"center\",\n\t\t\t\t\t\t\t\ttextAlign: \"center\",\n\t\t\t\t\t\t\t\tfontSize: 12,\n\t\t\t\t\t\t\t\tpadding: 4,\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{centerLabel}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</foreignObject>\n\t\t\t\t) : (\n\t\t\t\t\t<text\n\t\t\t\t\t\ttextAnchor=\"middle\"\n\t\t\t\t\t\tdy=\"0.35em\"\n\t\t\t\t\t\tfontSize={12}\n\t\t\t\t\t\tfontWeight={600}\n\t\t\t\t\t\tfill=\"hsl(var(--foreground))\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{focused.label}\n\t\t\t\t\t</text>\n\t\t\t\t)}\n\t\t\t</g>\n\t\t</svg>\n\t);\n}\n\nfunction layout(d3h: D3HierarchyMod, focused: SunburstNode, radius: number): LaidOutSegment[] {\n\tconst hierarchy = d3h\n\t\t.hierarchy<SunburstNode>(focused)\n\t\t.sum((d) => (d.children && d.children.length > 0 ? 0 : d.value ?? 0))\n\t\t.sort((a, b) => (b.value ?? 0) - (a.value ?? 0));\n\t// partition(hierarchy) returns HierarchyRectangularNode<T> with typed\n\t// x0/x1/y0/y1 (angle range × radius range for sunburst).\n\tconst layoutRoot = d3h.partition<SunburstNode>().size([2 * Math.PI, radius])(hierarchy);\n\tconst segments: LaidOutSegment[] = [];\n\tlayoutRoot.each((d) => {\n\t\t// Walk up to the depth-1 ancestor so all descendants of \"Equity\"\n\t\t// share the Equity hue, distinct from \"Fixed Income\".\n\t\tlet cursor: typeof d | null = d;\n\t\twhile (cursor && cursor.depth > 1) cursor = cursor.parent;\n\t\tconst ancestorIdx = cursor?.parent?.children?.indexOf(cursor) ?? 0;\n\t\tsegments.push({\n\t\t\tnode: d.data,\n\t\t\tdepth: d.depth,\n\t\t\tx0: d.x0,\n\t\t\tx1: d.x1,\n\t\t\ty0: d.y0,\n\t\t\ty1: d.y1,\n\t\t\trootSiblingIdx: Math.max(0, ancestorIdx),\n\t\t});\n\t});\n\treturn segments;\n}\n\nfunction findNode(root: SunburstNode, id: string): SunburstNode | null {\n\tif (root.id === id) return root;\n\tif (!root.children) return null;\n\tfor (const c of root.children) {\n\t\tconst f = findNode(c, id);\n\t\tif (f) return f;\n\t}\n\treturn null;\n}\n\nexport { Sunburst };\n"]}