@hex-core/components 1.6.0 → 1.8.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.
- package/dist/_tsup-dts-rollup.d.ts +1597 -0
- package/dist/arc.d.ts +4 -0
- package/dist/arc.js +147 -0
- package/dist/arc.js.map +1 -0
- package/dist/audio-player.d.ts +2 -0
- package/dist/audio-player.js +119 -0
- package/dist/audio-player.js.map +1 -0
- package/dist/audio-waveform.d.ts +2 -0
- package/dist/audio-waveform.js +72 -0
- package/dist/audio-waveform.js.map +1 -0
- package/dist/canvas.d.ts +2 -0
- package/dist/canvas.js +73 -0
- package/dist/canvas.js.map +1 -0
- package/dist/chord.d.ts +4 -0
- package/dist/chord.js +230 -0
- package/dist/chord.js.map +1 -0
- package/dist/cloze.d.ts +3 -0
- package/dist/cloze.js +98 -0
- package/dist/cloze.js.map +1 -0
- package/dist/compare-table.d.ts +4 -0
- package/dist/compare-table.js +109 -0
- package/dist/compare-table.js.map +1 -0
- package/dist/deck.d.ts +3 -0
- package/dist/deck.js +231 -0
- package/dist/deck.js.map +1 -0
- package/dist/dendrogram.d.ts +3 -0
- package/dist/dendrogram.js +162 -0
- package/dist/dendrogram.js.map +1 -0
- package/dist/diagram.d.ts +2 -0
- package/dist/diagram.js +70 -0
- package/dist/diagram.js.map +1 -0
- package/dist/flashcard.d.ts +2 -0
- package/dist/flashcard.js +107 -0
- package/dist/flashcard.js.map +1 -0
- package/dist/flowchart.d.ts +4 -0
- package/dist/flowchart.js +275 -0
- package/dist/flowchart.js.map +1 -0
- package/dist/funnel.d.ts +3 -0
- package/dist/funnel.js +157 -0
- package/dist/funnel.js.map +1 -0
- package/dist/gantt.d.ts +3 -0
- package/dist/gantt.js +279 -0
- package/dist/gantt.js.map +1 -0
- package/dist/image-occlusion.d.ts +3 -0
- package/dist/image-occlusion.js +106 -0
- package/dist/image-occlusion.js.map +1 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.js +4085 -2
- package/dist/index.js.map +1 -1
- package/dist/matrix.d.ts +3 -0
- package/dist/matrix.js +155 -0
- package/dist/matrix.js.map +1 -0
- package/dist/mind-map.d.ts +3 -0
- package/dist/mind-map.js +167 -0
- package/dist/mind-map.js.map +1 -0
- package/dist/org-chart.d.ts +3 -0
- package/dist/org-chart.js +215 -0
- package/dist/org-chart.js.map +1 -0
- package/dist/pyramid.d.ts +3 -0
- package/dist/pyramid.js +150 -0
- package/dist/pyramid.js.map +1 -0
- package/dist/quiz.d.ts +3 -0
- package/dist/quiz.js +128 -0
- package/dist/quiz.js.map +1 -0
- package/dist/sankey.d.ts +4 -0
- package/dist/sankey.js +190 -0
- package/dist/sankey.js.map +1 -0
- package/dist/schemas.d.ts +23 -0
- package/dist/schemas.js +2211 -4
- package/dist/schemas.js.map +1 -1
- package/dist/sequence.d.ts +4 -0
- package/dist/sequence.js +229 -0
- package/dist/sequence.js.map +1 -0
- package/dist/spaced-repetition.d.ts +3 -0
- package/dist/spaced-repetition.js +73 -0
- package/dist/spaced-repetition.js.map +1 -0
- package/dist/speech-recognition.d.ts +2 -0
- package/dist/speech-recognition.js +152 -0
- package/dist/speech-recognition.js.map +1 -0
- package/dist/sunburst.d.ts +3 -0
- package/dist/sunburst.js +205 -0
- package/dist/sunburst.js.map +1 -0
- package/dist/terminal.d.ts +2 -0
- package/dist/terminal.js +153 -0
- package/dist/terminal.js.map +1 -0
- package/dist/time-axis.d.ts +3 -0
- package/dist/time-axis.js +233 -0
- package/dist/time-axis.js.map +1 -0
- package/dist/tool-call.js +6 -1
- package/dist/tool-call.js.map +1 -1
- package/dist/tree-map.d.ts +3 -0
- package/dist/tree-map.js +171 -0
- package/dist/tree-map.js.map +1 -0
- package/dist/venn.d.ts +3 -0
- package/dist/venn.js +196 -0
- package/dist/venn.js.map +1 -0
- package/package.json +47 -3
package/dist/pyramid.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { clsx } from 'clsx';
|
|
4
|
+
import { twMerge } from 'tailwind-merge';
|
|
5
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
6
|
+
|
|
7
|
+
// src/lib/chart-palette.ts
|
|
8
|
+
var CHART_PALETTE = [
|
|
9
|
+
"hsl(var(--chart-1, var(--primary)))",
|
|
10
|
+
"hsl(var(--chart-2, var(--primary)))",
|
|
11
|
+
"hsl(var(--chart-3, var(--primary)))",
|
|
12
|
+
"hsl(var(--chart-4, var(--primary)))",
|
|
13
|
+
"hsl(var(--chart-5, var(--primary)))",
|
|
14
|
+
"hsl(var(--chart-6, var(--primary)))"
|
|
15
|
+
];
|
|
16
|
+
function pickChartHue(index) {
|
|
17
|
+
const safe = (index % CHART_PALETTE.length + CHART_PALETTE.length) % CHART_PALETTE.length;
|
|
18
|
+
return CHART_PALETTE[safe];
|
|
19
|
+
}
|
|
20
|
+
function cn(...inputs) {
|
|
21
|
+
return twMerge(clsx(inputs));
|
|
22
|
+
}
|
|
23
|
+
function Pyramid({
|
|
24
|
+
tiers,
|
|
25
|
+
shape = "widening",
|
|
26
|
+
width = 480,
|
|
27
|
+
height = 360,
|
|
28
|
+
gap = 4,
|
|
29
|
+
showValues = true,
|
|
30
|
+
onTierClick,
|
|
31
|
+
className,
|
|
32
|
+
...rest
|
|
33
|
+
}) {
|
|
34
|
+
const laidOut = layout(tiers, shape, width, height, gap);
|
|
35
|
+
const desc = `Pyramid with ${tiers.length} tier${tiers.length === 1 ? "" : "s"} (${shape})`;
|
|
36
|
+
const warnedRef = React.useRef(false);
|
|
37
|
+
const nodeEnv = globalThis.process?.env?.NODE_ENV;
|
|
38
|
+
if (nodeEnv !== "production" && !warnedRef.current && tiers.length > 7) {
|
|
39
|
+
warnedRef.current = true;
|
|
40
|
+
console.warn(
|
|
41
|
+
`[hex-core/Pyramid] ${tiers.length} tiers \u2014 labels become unreadable past ~7. Group adjacent tiers or switch to TreeMap / OrgChart.`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return /* @__PURE__ */ jsxs(
|
|
45
|
+
"svg",
|
|
46
|
+
{
|
|
47
|
+
...rest,
|
|
48
|
+
"data-hex-pyramid": true,
|
|
49
|
+
"data-shape": shape,
|
|
50
|
+
role: "img",
|
|
51
|
+
width,
|
|
52
|
+
height,
|
|
53
|
+
viewBox: `0 0 ${width} ${height}`,
|
|
54
|
+
className: cn("block", className),
|
|
55
|
+
children: [
|
|
56
|
+
/* @__PURE__ */ jsx("title", { children: "Pyramid chart" }),
|
|
57
|
+
/* @__PURE__ */ jsx("desc", { children: desc }),
|
|
58
|
+
/* @__PURE__ */ jsx("g", { "data-hex-pyramid-tiers": true, children: laidOut.map((t) => {
|
|
59
|
+
const valueText = showValues && t.tier.value != null ? ` \xB7 ${t.tier.value.toLocaleString()}` : "";
|
|
60
|
+
const interactive = Boolean(onTierClick);
|
|
61
|
+
const handleActivate = () => onTierClick?.(t.tier);
|
|
62
|
+
return /* @__PURE__ */ jsxs(
|
|
63
|
+
"g",
|
|
64
|
+
{
|
|
65
|
+
"data-hex-pyramid-tier": true,
|
|
66
|
+
"data-depth": t.depth,
|
|
67
|
+
role: interactive ? "button" : void 0,
|
|
68
|
+
tabIndex: interactive ? 0 : void 0,
|
|
69
|
+
"aria-label": interactive ? `${t.tier.label}${valueText}` : void 0,
|
|
70
|
+
style: interactive ? { cursor: "pointer" } : void 0,
|
|
71
|
+
onClick: interactive ? handleActivate : void 0,
|
|
72
|
+
onKeyDown: interactive ? (e) => activateOnKey(e, handleActivate) : void 0,
|
|
73
|
+
children: [
|
|
74
|
+
/* @__PURE__ */ jsx(
|
|
75
|
+
"polygon",
|
|
76
|
+
{
|
|
77
|
+
points: `${t.topLeft},${t.yTop} ${t.topRight},${t.yTop} ${t.bottomRight},${t.yBottom} ${t.bottomLeft},${t.yBottom}`,
|
|
78
|
+
fill: pickChartHue(t.depth),
|
|
79
|
+
fillOpacity: 0.85,
|
|
80
|
+
stroke: "hsl(var(--background))",
|
|
81
|
+
strokeWidth: 1
|
|
82
|
+
}
|
|
83
|
+
),
|
|
84
|
+
/* @__PURE__ */ jsx(
|
|
85
|
+
"text",
|
|
86
|
+
{
|
|
87
|
+
x: width / 2,
|
|
88
|
+
y: (t.yTop + t.yBottom) / 2,
|
|
89
|
+
dy: "0.35em",
|
|
90
|
+
textAnchor: "middle",
|
|
91
|
+
fontSize: 12,
|
|
92
|
+
fontWeight: 600,
|
|
93
|
+
fill: "hsl(var(--background))",
|
|
94
|
+
style: {
|
|
95
|
+
pointerEvents: "none",
|
|
96
|
+
paintOrder: "stroke",
|
|
97
|
+
stroke: "hsl(var(--foreground) / 0.45)",
|
|
98
|
+
strokeWidth: 2
|
|
99
|
+
},
|
|
100
|
+
children: `${t.tier.label}${valueText}`
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
]
|
|
104
|
+
},
|
|
105
|
+
t.tier.id
|
|
106
|
+
);
|
|
107
|
+
}) })
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
function layout(tiers, shape, width, height, gap) {
|
|
113
|
+
if (tiers.length === 0) return [];
|
|
114
|
+
const usableHeight = height - gap * Math.max(0, tiers.length - 1);
|
|
115
|
+
const tierHeight = usableHeight / tiers.length;
|
|
116
|
+
const cx = width / 2;
|
|
117
|
+
const total = tiers.length;
|
|
118
|
+
const ratioAt = (i) => {
|
|
119
|
+
const t = total === 1 ? 0 : i / (total - 1);
|
|
120
|
+
return shape === "widening" ? 0.2 + 0.8 * t : 1 - 0.8 * t;
|
|
121
|
+
};
|
|
122
|
+
return tiers.map((tier, i) => {
|
|
123
|
+
const yTop = i * (tierHeight + gap);
|
|
124
|
+
const yBottom = yTop + tierHeight;
|
|
125
|
+
const topRatio = ratioAt(i);
|
|
126
|
+
const bottomRatio = i + 1 < total ? ratioAt(i + 1) : topRatio;
|
|
127
|
+
const topHalfWidth = width / 2 * topRatio;
|
|
128
|
+
const bottomHalfWidth = width / 2 * bottomRatio;
|
|
129
|
+
return {
|
|
130
|
+
tier,
|
|
131
|
+
depth: i,
|
|
132
|
+
topLeft: cx - topHalfWidth,
|
|
133
|
+
topRight: cx + topHalfWidth,
|
|
134
|
+
bottomLeft: cx - bottomHalfWidth,
|
|
135
|
+
bottomRight: cx + bottomHalfWidth,
|
|
136
|
+
yTop,
|
|
137
|
+
yBottom
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function activateOnKey(e, fn) {
|
|
142
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
fn();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export { Pyramid };
|
|
149
|
+
//# sourceMappingURL=pyramid.js.map
|
|
150
|
+
//# sourceMappingURL=pyramid.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/chart-palette.ts","../src/lib/utils.ts","../src/artifacts/pyramid/pyramid.tsx"],"names":[],"mappings":"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","file":"pyramid.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 * Ranked-tier pyramid. Tiers stack top-to-bottom; each tier's width can\n * either grow toward the base (\"widening\" — Maslow's hierarchy, fewer\n * elites at the top) or shrink (\"narrowing\" — population pyramid by age).\n * Pure SVG; no heavy peer dependency.\n *\n * Distinct from Funnel: Pyramid encodes RANK (each tier is a distinct\n * categorical level), Funnel encodes FLOW (each stage is a subset of the\n * previous, with a conversion ratio).\n *\n * @example\n * <Pyramid\n * tiers={[\n * { id: \"self-actualization\", label: \"Self-actualization\" },\n * { id: \"esteem\", label: \"Esteem\" },\n * { id: \"love\", label: \"Love & belonging\" },\n * { id: \"safety\", label: \"Safety\" },\n * { id: \"physiological\", label: \"Physiological\" },\n * ]}\n * shape=\"widening\"\n * />\n */\nexport type PyramidTier = {\n\tid: string;\n\tlabel: string;\n\tvalue?: number;\n};\n\nexport interface PyramidProps extends Omit<React.SVGAttributes<SVGSVGElement>, \"children\"> {\n\t/** Ordered top-to-bottom tiers. The first entry is the apex. */\n\ttiers: PyramidTier[];\n\t/** Tier-width direction. \"widening\" grows toward the base; \"narrowing\" shrinks toward it. Default \"widening\". */\n\tshape?: \"widening\" | \"narrowing\";\n\t/** Pixel width of the rendered SVG. Default 480. */\n\twidth?: number;\n\t/** Pixel height of the rendered SVG. Default 360. */\n\theight?: number;\n\t/** Pixel gap between tiers. Default 4. */\n\tgap?: number;\n\t/** Show each tier's `value` next to its label when present. Default true. */\n\tshowValues?: boolean;\n\t/** Fired when a tier is clicked. */\n\tonTierClick?: (tier: PyramidTier) => void;\n}\n\ninterface LaidOutTier {\n\ttier: PyramidTier;\n\tdepth: number;\n\ttopLeft: number;\n\ttopRight: number;\n\tbottomLeft: number;\n\tbottomRight: number;\n\tyTop: number;\n\tyBottom: number;\n}\n\nfunction Pyramid({\n\ttiers,\n\tshape = \"widening\",\n\twidth = 480,\n\theight = 360,\n\tgap = 4,\n\tshowValues = true,\n\tonTierClick,\n\tclassName,\n\t...rest\n}: PyramidProps) {\n\tconst laidOut = layout(tiers, shape, width, height, gap);\n\tconst desc = `Pyramid with ${tiers.length} tier${tiers.length === 1 ? \"\" : \"s\"} (${shape})`;\n\n\t// Pyramid's `whenNotToUse` warns about >7 tiers — surface a dev-only\n\t// console warning so consumers feel the friction in development without\n\t// punishing prod runtime. Uses a ref so the warning fires once per mount,\n\t// not on every render. Read NODE_ENV via globalThis to avoid pulling\n\t// `@types/node` into the components package — bundlers inline the value\n\t// in browser builds; in tests vitest/Node provides `process` at runtime.\n\tconst warnedRef = React.useRef(false);\n\tconst nodeEnv = (globalThis as { process?: { env?: { NODE_ENV?: string } } }).process?.env?.NODE_ENV;\n\tif (nodeEnv !== \"production\" && !warnedRef.current && tiers.length > 7) {\n\t\twarnedRef.current = true;\n\t\t// eslint-disable-next-line no-console\n\t\tconsole.warn(\n\t\t\t`[hex-core/Pyramid] ${tiers.length} tiers — labels become unreadable past ~7. Group adjacent tiers or switch to TreeMap / OrgChart.`,\n\t\t);\n\t}\n\n\treturn (\n\t\t<svg\n\t\t\t{...rest}\n\t\t\tdata-hex-pyramid\n\t\t\tdata-shape={shape}\n\t\t\trole=\"img\"\n\t\t\twidth={width}\n\t\t\theight={height}\n\t\t\tviewBox={`0 0 ${width} ${height}`}\n\t\t\tclassName={cn(\"block\", className)}\n\t\t>\n\t\t\t<title>Pyramid chart</title>\n\t\t\t<desc>{desc}</desc>\n\t\t\t<g data-hex-pyramid-tiers>\n\t\t\t\t{laidOut.map((t) => {\n\t\t\t\t\tconst valueText =\n\t\t\t\t\t\tshowValues && t.tier.value != null ? ` · ${t.tier.value.toLocaleString()}` : \"\";\n\t\t\t\t\tconst interactive = Boolean(onTierClick);\n\t\t\t\t\tconst handleActivate = () => onTierClick?.(t.tier);\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<g\n\t\t\t\t\t\t\tkey={t.tier.id}\n\t\t\t\t\t\t\tdata-hex-pyramid-tier\n\t\t\t\t\t\t\tdata-depth={t.depth}\n\t\t\t\t\t\t\trole={interactive ? \"button\" : undefined}\n\t\t\t\t\t\t\ttabIndex={interactive ? 0 : undefined}\n\t\t\t\t\t\t\taria-label={interactive ? `${t.tier.label}${valueText}` : undefined}\n\t\t\t\t\t\t\tstyle={interactive ? { cursor: \"pointer\" } : undefined}\n\t\t\t\t\t\t\tonClick={interactive ? handleActivate : undefined}\n\t\t\t\t\t\t\tonKeyDown={interactive ? (e) => activateOnKey(e, handleActivate) : undefined}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<polygon\n\t\t\t\t\t\t\t\tpoints={`${t.topLeft},${t.yTop} ${t.topRight},${t.yTop} ${t.bottomRight},${t.yBottom} ${t.bottomLeft},${t.yBottom}`}\n\t\t\t\t\t\t\t\tfill={pickChartHue(t.depth)}\n\t\t\t\t\t\t\t\tfillOpacity={0.85}\n\t\t\t\t\t\t\t\tstroke=\"hsl(var(--background))\"\n\t\t\t\t\t\t\t\tstrokeWidth={1}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<text\n\t\t\t\t\t\t\t\t// Page-bg fill + foreground-tinted stroke gives\n\t\t\t\t\t\t\t\t// a readable \"outline label\" on any chart-1..6\n\t\t\t\t\t\t\t\t// fill — same technique as Funnel.\n\t\t\t\t\t\t\t\tx={width / 2}\n\t\t\t\t\t\t\t\ty={(t.yTop + t.yBottom) / 2}\n\t\t\t\t\t\t\t\tdy=\"0.35em\"\n\t\t\t\t\t\t\t\ttextAnchor=\"middle\"\n\t\t\t\t\t\t\t\tfontSize={12}\n\t\t\t\t\t\t\t\tfontWeight={600}\n\t\t\t\t\t\t\t\tfill=\"hsl(var(--background))\"\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\tpointerEvents: \"none\",\n\t\t\t\t\t\t\t\t\tpaintOrder: \"stroke\",\n\t\t\t\t\t\t\t\t\tstroke: \"hsl(var(--foreground) / 0.45)\",\n\t\t\t\t\t\t\t\t\tstrokeWidth: 2,\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{`${t.tier.label}${valueText}`}\n\t\t\t\t\t\t\t</text>\n\t\t\t\t\t\t</g>\n\t\t\t\t\t);\n\t\t\t\t})}\n\t\t\t</g>\n\t\t</svg>\n\t);\n}\n\nfunction layout(\n\ttiers: PyramidTier[],\n\tshape: \"widening\" | \"narrowing\",\n\twidth: number,\n\theight: number,\n\tgap: number,\n): LaidOutTier[] {\n\tif (tiers.length === 0) return [];\n\tconst usableHeight = height - gap * Math.max(0, tiers.length - 1);\n\tconst tierHeight = usableHeight / tiers.length;\n\tconst cx = width / 2;\n\tconst total = tiers.length;\n\n\t// Linear ramp: top tier ratio 0.2, bottom tier ratio 1.0 (or reverse).\n\tconst ratioAt = (i: number): number => {\n\t\tconst t = total === 1 ? 0 : i / (total - 1);\n\t\treturn shape === \"widening\" ? 0.2 + 0.8 * t : 1.0 - 0.8 * t;\n\t};\n\n\treturn tiers.map((tier, i) => {\n\t\tconst yTop = i * (tierHeight + gap);\n\t\tconst yBottom = yTop + tierHeight;\n\t\tconst topRatio = ratioAt(i);\n\t\tconst bottomRatio = i + 1 < total ? ratioAt(i + 1) : topRatio;\n\t\t// For widening, the visual base of each tier should match the top of\n\t\t// the next tier — using the next tier's ratio at the bottom edge.\n\t\t// For narrowing, same logic. The penultimate-to-last bottom matches\n\t\t// `topRatio` of the last tier we don't have, so we hold steady.\n\t\tconst topHalfWidth = (width / 2) * topRatio;\n\t\tconst bottomHalfWidth = (width / 2) * bottomRatio;\n\t\treturn {\n\t\t\ttier,\n\t\t\tdepth: i,\n\t\t\ttopLeft: cx - topHalfWidth,\n\t\t\ttopRight: cx + topHalfWidth,\n\t\t\tbottomLeft: cx - bottomHalfWidth,\n\t\t\tbottomRight: cx + bottomHalfWidth,\n\t\t\tyTop,\n\t\t\tyBottom,\n\t\t};\n\t});\n}\n\nfunction activateOnKey(e: React.KeyboardEvent, fn: () => void): void {\n\tif (e.key === \"Enter\" || e.key === \" \") {\n\t\te.preventDefault();\n\t\tfn();\n\t}\n}\n\nexport { Pyramid };\n"]}
|
package/dist/quiz.d.ts
ADDED
package/dist/quiz.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { clsx } from 'clsx';
|
|
4
|
+
import { twMerge } from 'tailwind-merge';
|
|
5
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
6
|
+
|
|
7
|
+
function cn(...inputs) {
|
|
8
|
+
return twMerge(clsx(inputs));
|
|
9
|
+
}
|
|
10
|
+
function Quiz({
|
|
11
|
+
question,
|
|
12
|
+
options,
|
|
13
|
+
selectionMode = "single",
|
|
14
|
+
submitLabel = "Submit",
|
|
15
|
+
onAnswer,
|
|
16
|
+
className,
|
|
17
|
+
...rest
|
|
18
|
+
}) {
|
|
19
|
+
const [selected, setSelected] = React.useState(() => /* @__PURE__ */ new Set());
|
|
20
|
+
const [submitted, setSubmitted] = React.useState(false);
|
|
21
|
+
const onAnswerRef = React.useRef(onAnswer);
|
|
22
|
+
onAnswerRef.current = onAnswer;
|
|
23
|
+
const groupName = React.useId();
|
|
24
|
+
const correctIds = React.useMemo(
|
|
25
|
+
() => new Set(options.filter((o) => o.correct).map((o) => o.id)),
|
|
26
|
+
[options]
|
|
27
|
+
);
|
|
28
|
+
const allCorrect = React.useMemo(() => {
|
|
29
|
+
if (selected.size !== correctIds.size) return false;
|
|
30
|
+
for (const id of correctIds) if (!selected.has(id)) return false;
|
|
31
|
+
return true;
|
|
32
|
+
}, [selected, correctIds]);
|
|
33
|
+
const stateFor = (option) => {
|
|
34
|
+
if (!submitted) return "unanswered";
|
|
35
|
+
const picked = selected.has(option.id);
|
|
36
|
+
const isCorrect = correctIds.has(option.id);
|
|
37
|
+
if (picked && isCorrect) return "correct";
|
|
38
|
+
if (picked && !isCorrect) return "incorrect";
|
|
39
|
+
if (!picked && isCorrect) return "missed";
|
|
40
|
+
return "unanswered";
|
|
41
|
+
};
|
|
42
|
+
const toggle = (id) => {
|
|
43
|
+
setSelected((prev) => {
|
|
44
|
+
if (selectionMode === "single") return /* @__PURE__ */ new Set([id]);
|
|
45
|
+
const next = new Set(prev);
|
|
46
|
+
if (next.has(id)) next.delete(id);
|
|
47
|
+
else next.add(id);
|
|
48
|
+
return next;
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
const handleSubmit = () => {
|
|
52
|
+
setSubmitted(true);
|
|
53
|
+
onAnswerRef.current?.(Array.from(selected), allCorrect);
|
|
54
|
+
};
|
|
55
|
+
const canSubmit = selected.size > 0;
|
|
56
|
+
const statusText = submitted ? allCorrect ? "Correct \u2014 well done." : "Some selections are incorrect or missed. Review the highlighted options." : "";
|
|
57
|
+
return /* @__PURE__ */ jsxs(
|
|
58
|
+
"div",
|
|
59
|
+
{
|
|
60
|
+
...rest,
|
|
61
|
+
"data-hex-quiz": true,
|
|
62
|
+
"data-submitted": submitted,
|
|
63
|
+
"data-all-correct": submitted && allCorrect,
|
|
64
|
+
className: cn("rounded-lg border bg-card p-4 text-card-foreground", className),
|
|
65
|
+
children: [
|
|
66
|
+
/* @__PURE__ */ jsx("div", { "data-hex-quiz-question": true, className: "mb-3 text-base font-medium", children: question }),
|
|
67
|
+
/* @__PURE__ */ jsx("div", { "data-hex-quiz-options": true, className: "space-y-2", role: selectionMode === "single" ? "radiogroup" : "group", children: options.map((option) => {
|
|
68
|
+
const state = stateFor(option);
|
|
69
|
+
const isPicked = selected.has(option.id);
|
|
70
|
+
return /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsxs(
|
|
71
|
+
"label",
|
|
72
|
+
{
|
|
73
|
+
"data-hex-quiz-option": true,
|
|
74
|
+
"data-state": state,
|
|
75
|
+
"data-picked": isPicked,
|
|
76
|
+
className: cn(
|
|
77
|
+
"flex cursor-pointer items-start gap-2 rounded-md border bg-background p-2 transition-colors",
|
|
78
|
+
"hover:bg-muted/50",
|
|
79
|
+
state === "correct" && "border-primary bg-primary/10",
|
|
80
|
+
state === "incorrect" && "border-destructive bg-destructive/10",
|
|
81
|
+
state === "missed" && "border-primary/60 bg-primary/5 ring-1 ring-primary/40"
|
|
82
|
+
),
|
|
83
|
+
children: [
|
|
84
|
+
/* @__PURE__ */ jsx(
|
|
85
|
+
"input",
|
|
86
|
+
{
|
|
87
|
+
type: selectionMode === "single" ? "radio" : "checkbox",
|
|
88
|
+
name: selectionMode === "single" ? groupName : void 0,
|
|
89
|
+
value: option.id,
|
|
90
|
+
checked: isPicked,
|
|
91
|
+
onChange: () => toggle(option.id),
|
|
92
|
+
className: "mt-0.5"
|
|
93
|
+
}
|
|
94
|
+
),
|
|
95
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
|
|
96
|
+
/* @__PURE__ */ jsx("div", { "data-hex-quiz-label": true, children: option.label }),
|
|
97
|
+
submitted && option.explanation && (state === "correct" || state === "incorrect" || state === "missed") ? /* @__PURE__ */ jsx("div", { "data-hex-quiz-explanation": true, className: "mt-1 text-xs text-muted-foreground", children: option.explanation }) : null
|
|
98
|
+
] })
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
) }, option.id);
|
|
102
|
+
}) }),
|
|
103
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-3 flex items-center gap-3", children: [
|
|
104
|
+
/* @__PURE__ */ jsx(
|
|
105
|
+
"button",
|
|
106
|
+
{
|
|
107
|
+
type: "button",
|
|
108
|
+
"data-hex-quiz-submit": true,
|
|
109
|
+
disabled: !canSubmit,
|
|
110
|
+
onClick: handleSubmit,
|
|
111
|
+
className: cn(
|
|
112
|
+
"inline-flex h-9 items-center justify-center rounded-md bg-primary px-3 text-sm font-medium text-primary-foreground transition-opacity",
|
|
113
|
+
"hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50",
|
|
114
|
+
"focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
115
|
+
),
|
|
116
|
+
children: submitLabel
|
|
117
|
+
}
|
|
118
|
+
),
|
|
119
|
+
/* @__PURE__ */ jsx("div", { "data-hex-quiz-status": true, role: "status", "aria-live": "polite", className: "text-sm text-muted-foreground", children: statusText })
|
|
120
|
+
] })
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export { Quiz };
|
|
127
|
+
//# sourceMappingURL=quiz.js.map
|
|
128
|
+
//# sourceMappingURL=quiz.js.map
|
package/dist/quiz.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/utils.ts","../src/artifacts/quiz/quiz.tsx"],"names":[],"mappings":";;;;;AAQO,SAAS,MAAM,MAAA,EAAsB;AAC3C,EAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAC,CAAA;AAC5B;ACyCA,SAAS,IAAA,CAAK;AAAA,EACb,QAAA;AAAA,EACA,OAAA;AAAA,EACA,aAAA,GAAgB,QAAA;AAAA,EAChB,WAAA,GAAc,QAAA;AAAA,EACd,QAAA;AAAA,EACA,SAAA;AAAA,EACA,GAAG;AACJ,CAAA,EAAc;AACb,EAAA,MAAM,CAAC,UAAU,WAAW,CAAA,GAAU,eAAsB,sBAAM,IAAI,KAAK,CAAA;AAC3E,EAAA,MAAM,CAAC,SAAA,EAAW,YAAY,CAAA,GAAU,eAAS,KAAK,CAAA;AACtD,EAAA,MAAM,WAAA,GAAoB,aAAO,QAAQ,CAAA;AACzC,EAAA,WAAA,CAAY,OAAA,GAAU,QAAA;AACtB,EAAA,MAAM,YAAkB,KAAA,CAAA,KAAA,EAAM;AAE9B,EAAA,MAAM,UAAA,GAAmB,KAAA,CAAA,OAAA;AAAA,IACxB,MAAM,IAAI,GAAA,CAAI,OAAA,CAAQ,OAAO,CAAC,CAAA,KAAM,CAAA,CAAE,OAAO,EAAE,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,EAAE,CAAC,CAAA;AAAA,IAC/D,CAAC,OAAO;AAAA,GACT;AAEA,EAAA,MAAM,UAAA,GAAmB,cAAQ,MAAM;AACtC,IAAA,IAAI,QAAA,CAAS,IAAA,KAAS,UAAA,CAAW,IAAA,EAAM,OAAO,KAAA;AAC9C,IAAA,KAAA,MAAW,EAAA,IAAM,YAAY,IAAI,CAAC,SAAS,GAAA,CAAI,EAAE,GAAG,OAAO,KAAA;AAC3D,IAAA,OAAO,IAAA;AAAA,EACR,CAAA,EAAG,CAAC,QAAA,EAAU,UAAU,CAAC,CAAA;AAEzB,EAAA,MAAM,QAAA,GAAW,CAAC,MAAA,KAAkC;AACnD,IAAA,IAAI,CAAC,WAAW,OAAO,YAAA;AACvB,IAAA,MAAM,MAAA,GAAS,QAAA,CAAS,GAAA,CAAI,MAAA,CAAO,EAAE,CAAA;AACrC,IAAA,MAAM,SAAA,GAAY,UAAA,CAAW,GAAA,CAAI,MAAA,CAAO,EAAE,CAAA;AAC1C,IAAA,IAAI,MAAA,IAAU,WAAW,OAAO,SAAA;AAChC,IAAA,IAAI,MAAA,IAAU,CAAC,SAAA,EAAW,OAAO,WAAA;AACjC,IAAA,IAAI,CAAC,MAAA,IAAU,SAAA,EAAW,OAAO,QAAA;AACjC,IAAA,OAAO,YAAA;AAAA,EACR,CAAA;AAEA,EAAA,MAAM,MAAA,GAAS,CAAC,EAAA,KAAe;AAM9B,IAAA,WAAA,CAAY,CAAC,IAAA,KAAS;AACrB,MAAA,IAAI,kBAAkB,QAAA,EAAU,2BAAW,GAAA,CAAI,CAAC,EAAE,CAAC,CAAA;AACnD,MAAA,MAAM,IAAA,GAAO,IAAI,GAAA,CAAI,IAAI,CAAA;AACzB,MAAA,IAAI,KAAK,GAAA,CAAI,EAAE,CAAA,EAAG,IAAA,CAAK,OAAO,EAAE,CAAA;AAAA,WAC3B,IAAA,CAAK,IAAI,EAAE,CAAA;AAChB,MAAA,OAAO,IAAA;AAAA,IACR,CAAC,CAAA;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,eAAe,MAAM;AAC1B,IAAA,YAAA,CAAa,IAAI,CAAA;AACjB,IAAA,WAAA,CAAY,OAAA,GAAU,KAAA,CAAM,IAAA,CAAK,QAAQ,GAAG,UAAU,CAAA;AAAA,EACvD,CAAA;AAEA,EAAA,MAAM,SAAA,GAAY,SAAS,IAAA,GAAO,CAAA;AAClC,EAAA,MAAM,UAAA,GAAa,SAAA,GAChB,UAAA,GACC,2BAAA,GACA,0EAAA,GACD,EAAA;AAEH,EAAA,uBACC,IAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,GAAG,IAAA;AAAA,MACJ,eAAA,EAAa,IAAA;AAAA,MACb,gBAAA,EAAgB,SAAA;AAAA,MAChB,oBAAkB,SAAA,IAAa,UAAA;AAAA,MAC/B,SAAA,EAAW,EAAA,CAAG,oDAAA,EAAsD,SAAS,CAAA;AAAA,MAE7E,QAAA,EAAA;AAAA,wBAAA,GAAA,CAAC,KAAA,EAAA,EAAI,wBAAA,EAAsB,IAAA,EAAC,SAAA,EAAU,8BACpC,QAAA,EAAA,QAAA,EACF,CAAA;AAAA,wBACA,GAAA,CAAC,KAAA,EAAA,EAAI,uBAAA,EAAqB,IAAA,EAAC,WAAU,WAAA,EAAY,IAAA,EAAM,aAAA,KAAkB,QAAA,GAAW,YAAA,GAAe,OAAA,EACjG,QAAA,EAAA,OAAA,CAAQ,GAAA,CAAI,CAAC,MAAA,KAAW;AACxB,UAAA,MAAM,KAAA,GAAQ,SAAS,MAAM,CAAA;AAC7B,UAAA,MAAM,QAAA,GAAW,QAAA,CAAS,GAAA,CAAI,MAAA,CAAO,EAAE,CAAA;AACvC,UAAA,2BACE,KAAA,EAAA,EACA,QAAA,kBAAA,IAAA;AAAA,YAAC,OAAA;AAAA,YAAA;AAAA,cACA,sBAAA,EAAoB,IAAA;AAAA,cACpB,YAAA,EAAY,KAAA;AAAA,cACZ,aAAA,EAAa,QAAA;AAAA,cACb,SAAA,EAAW,EAAA;AAAA,gBACV,6FAAA;AAAA,gBACA,mBAAA;AAAA,gBACA,UAAU,SAAA,IAAa,8BAAA;AAAA,gBACvB,UAAU,WAAA,IAAe,sCAAA;AAAA,gBACzB,UAAU,QAAA,IAAY;AAAA,eACvB;AAAA,cAEA,QAAA,EAAA;AAAA,gCAAA,GAAA;AAAA,kBAAC,OAAA;AAAA,kBAAA;AAAA,oBACA,IAAA,EAAM,aAAA,KAAkB,QAAA,GAAW,OAAA,GAAU,UAAA;AAAA,oBAC7C,IAAA,EAAM,aAAA,KAAkB,QAAA,GAAW,SAAA,GAAY,MAAA;AAAA,oBAC/C,OAAO,MAAA,CAAO,EAAA;AAAA,oBACd,OAAA,EAAS,QAAA;AAAA,oBACT,QAAA,EAAU,MAAM,MAAA,CAAO,MAAA,CAAO,EAAE,CAAA;AAAA,oBAChC,SAAA,EAAU;AAAA;AAAA,iBACX;AAAA,gCACA,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,QAAA,EACd,QAAA,EAAA;AAAA,kCAAA,GAAA,CAAC,KAAA,EAAA,EAAI,qBAAA,EAAmB,IAAA,EAAE,QAAA,EAAA,MAAA,CAAO,KAAA,EAAM,CAAA;AAAA,kBACtC,aAAa,MAAA,CAAO,WAAA,KAAgB,KAAA,KAAU,SAAA,IAAa,UAAU,WAAA,IAAe,KAAA,KAAU,QAAA,CAAA,mBAC9F,GAAA,CAAC,SAAI,2BAAA,EAAyB,IAAA,EAAC,WAAU,oCAAA,EACvC,QAAA,EAAA,MAAA,CAAO,aACT,CAAA,GACG;AAAA,iBAAA,EACL;AAAA;AAAA;AAAA,WACD,EAAA,EA7BS,OAAO,EA8BjB,CAAA;AAAA,QAEF,CAAC,CAAA,EACF,CAAA;AAAA,wBACA,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,8BAAA,EACd,QAAA,EAAA;AAAA,0BAAA,GAAA;AAAA,YAAC,QAAA;AAAA,YAAA;AAAA,cACA,IAAA,EAAK,QAAA;AAAA,cACL,sBAAA,EAAoB,IAAA;AAAA,cACpB,UAAU,CAAC,SAAA;AAAA,cACX,OAAA,EAAS,YAAA;AAAA,cACT,SAAA,EAAW,EAAA;AAAA,gBACV,uIAAA;AAAA,gBACA,kEAAA;AAAA,gBACA;AAAA,eACD;AAAA,cAEC,QAAA,EAAA;AAAA;AAAA,WACF;AAAA,0BACA,GAAA,CAAC,KAAA,EAAA,EAAI,sBAAA,EAAoB,IAAA,EAAC,IAAA,EAAK,UAAS,WAAA,EAAU,QAAA,EAAS,SAAA,EAAU,+BAAA,EACnE,QAAA,EAAA,UAAA,EACF;AAAA,SAAA,EACD;AAAA;AAAA;AAAA,GACD;AAEF","file":"quiz.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 * Single-question multiple-choice quiz. Renders the question + options\n * as native radio (single-select) or checkbox (multi-select) inputs;\n * after Submit, each option flips to data-state=\"correct|incorrect|missed\"\n * so consumers can theme right / wrong / unselected-but-correct\n * differently. Pure HTML; no heavy peer.\n *\n * Headless on grading: the schema honors what the consumer passes for\n * `correct`. Reset between runs by clearing the `selectedIds` you\n * forwarded as `value` (controlled) or unmounting (uncontrolled).\n *\n * @example\n * <Quiz\n * question=\"Which planets are gas giants?\"\n * selectionMode=\"multi\"\n * options={[\n * { id: \"j\", label: \"Jupiter\", correct: true },\n * { id: \"v\", label: \"Venus\" },\n * { id: \"s\", label: \"Saturn\", correct: true, explanation: \"Saturn's atmosphere is mostly hydrogen and helium.\" },\n * { id: \"m\", label: \"Mercury\" },\n * ]}\n * onAnswer={(ids, allCorrect) => track(ids, allCorrect)}\n * />\n */\nexport type QuizOption = {\n\tid: string;\n\tlabel: React.ReactNode;\n\tcorrect?: boolean;\n\texplanation?: React.ReactNode;\n};\n\nexport interface QuizProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\"> {\n\t/** Question prompt (any ReactNode — supports rich content). */\n\tquestion: React.ReactNode;\n\t/** Options the learner picks from. */\n\toptions: QuizOption[];\n\t/** \"single\" — radio inputs (one selection). \"multi\" — checkboxes. Default \"single\". */\n\tselectionMode?: \"single\" | \"multi\";\n\t/** Custom Submit button label. Default \"Submit\". */\n\tsubmitLabel?: string;\n\t/** Fired with the selected option ids and a boolean indicating whether the entire selection matches the correct set. */\n\tonAnswer?: (selectedIds: string[], allCorrect: boolean) => void;\n}\n\ntype CellState = \"correct\" | \"incorrect\" | \"missed\" | \"unanswered\";\n\nfunction Quiz({\n\tquestion,\n\toptions,\n\tselectionMode = \"single\",\n\tsubmitLabel = \"Submit\",\n\tonAnswer,\n\tclassName,\n\t...rest\n}: QuizProps) {\n\tconst [selected, setSelected] = React.useState<Set<string>>(() => new Set());\n\tconst [submitted, setSubmitted] = React.useState(false);\n\tconst onAnswerRef = React.useRef(onAnswer);\n\tonAnswerRef.current = onAnswer;\n\tconst groupName = React.useId();\n\n\tconst correctIds = React.useMemo(\n\t\t() => new Set(options.filter((o) => o.correct).map((o) => o.id)),\n\t\t[options],\n\t);\n\n\tconst allCorrect = React.useMemo(() => {\n\t\tif (selected.size !== correctIds.size) return false;\n\t\tfor (const id of correctIds) if (!selected.has(id)) return false;\n\t\treturn true;\n\t}, [selected, correctIds]);\n\n\tconst stateFor = (option: QuizOption): CellState => {\n\t\tif (!submitted) return \"unanswered\";\n\t\tconst picked = selected.has(option.id);\n\t\tconst isCorrect = correctIds.has(option.id);\n\t\tif (picked && isCorrect) return \"correct\";\n\t\tif (picked && !isCorrect) return \"incorrect\";\n\t\tif (!picked && isCorrect) return \"missed\";\n\t\treturn \"unanswered\";\n\t};\n\n\tconst toggle = (id: string) => {\n\t\t// Submit is sticky: data-state always reflects the CURRENT selection\n\t\t// against the correct set, so re-picking after submit auto-updates\n\t\t// the correct/incorrect/missed indicators without clearing the\n\t\t// \"I've tried this\" affordance. Next Submit re-grades and re-fires\n\t\t// onAnswer with the updated selection.\n\t\tsetSelected((prev) => {\n\t\t\tif (selectionMode === \"single\") return new Set([id]);\n\t\t\tconst next = new Set(prev);\n\t\t\tif (next.has(id)) next.delete(id);\n\t\t\telse next.add(id);\n\t\t\treturn next;\n\t\t});\n\t};\n\n\tconst handleSubmit = () => {\n\t\tsetSubmitted(true);\n\t\tonAnswerRef.current?.(Array.from(selected), allCorrect);\n\t};\n\n\tconst canSubmit = selected.size > 0;\n\tconst statusText = submitted\n\t\t? allCorrect\n\t\t\t? \"Correct — well done.\"\n\t\t\t: \"Some selections are incorrect or missed. Review the highlighted options.\"\n\t\t: \"\";\n\n\treturn (\n\t\t<div\n\t\t\t{...rest}\n\t\t\tdata-hex-quiz\n\t\t\tdata-submitted={submitted}\n\t\t\tdata-all-correct={submitted && allCorrect}\n\t\t\tclassName={cn(\"rounded-lg border bg-card p-4 text-card-foreground\", className)}\n\t\t>\n\t\t\t<div data-hex-quiz-question className=\"mb-3 text-base font-medium\">\n\t\t\t\t{question}\n\t\t\t</div>\n\t\t\t<div data-hex-quiz-options className=\"space-y-2\" role={selectionMode === \"single\" ? \"radiogroup\" : \"group\"}>\n\t\t\t\t{options.map((option) => {\n\t\t\t\t\tconst state = stateFor(option);\n\t\t\t\t\tconst isPicked = selected.has(option.id);\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<div key={option.id}>\n\t\t\t\t\t\t\t<label\n\t\t\t\t\t\t\t\tdata-hex-quiz-option\n\t\t\t\t\t\t\t\tdata-state={state}\n\t\t\t\t\t\t\t\tdata-picked={isPicked}\n\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\"flex cursor-pointer items-start gap-2 rounded-md border bg-background p-2 transition-colors\",\n\t\t\t\t\t\t\t\t\t\"hover:bg-muted/50\",\n\t\t\t\t\t\t\t\t\tstate === \"correct\" && \"border-primary bg-primary/10\",\n\t\t\t\t\t\t\t\t\tstate === \"incorrect\" && \"border-destructive bg-destructive/10\",\n\t\t\t\t\t\t\t\t\tstate === \"missed\" && \"border-primary/60 bg-primary/5 ring-1 ring-primary/40\",\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<input\n\t\t\t\t\t\t\t\t\ttype={selectionMode === \"single\" ? \"radio\" : \"checkbox\"}\n\t\t\t\t\t\t\t\t\tname={selectionMode === \"single\" ? groupName : undefined}\n\t\t\t\t\t\t\t\t\tvalue={option.id}\n\t\t\t\t\t\t\t\t\tchecked={isPicked}\n\t\t\t\t\t\t\t\t\tonChange={() => toggle(option.id)}\n\t\t\t\t\t\t\t\t\tclassName=\"mt-0.5\"\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t<div className=\"flex-1\">\n\t\t\t\t\t\t\t\t\t<div data-hex-quiz-label>{option.label}</div>\n\t\t\t\t\t\t\t\t\t{submitted && option.explanation && (state === \"correct\" || state === \"incorrect\" || state === \"missed\") ? (\n\t\t\t\t\t\t\t\t\t\t<div data-hex-quiz-explanation className=\"mt-1 text-xs text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t\t\t{option.explanation}\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t);\n\t\t\t\t})}\n\t\t\t</div>\n\t\t\t<div className=\"mt-3 flex items-center gap-3\">\n\t\t\t\t<button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tdata-hex-quiz-submit\n\t\t\t\t\tdisabled={!canSubmit}\n\t\t\t\t\tonClick={handleSubmit}\n\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\"inline-flex h-9 items-center justify-center rounded-md bg-primary px-3 text-sm font-medium text-primary-foreground transition-opacity\",\n\t\t\t\t\t\t\"hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50\",\n\t\t\t\t\t\t\"focus:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n\t\t\t\t\t)}\n\t\t\t\t>\n\t\t\t\t\t{submitLabel}\n\t\t\t\t</button>\n\t\t\t\t<div data-hex-quiz-status role=\"status\" aria-live=\"polite\" className=\"text-sm text-muted-foreground\">\n\t\t\t\t\t{statusText}\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n\nexport { Quiz };\n"]}
|
package/dist/sankey.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { SankeyNode_alias_1 as SankeyNode } from './_tsup-dts-rollup.js';
|
|
2
|
+
export { SankeyLink_alias_1 as SankeyLink } from './_tsup-dts-rollup.js';
|
|
3
|
+
export { SankeyProps_alias_1 as SankeyProps } from './_tsup-dts-rollup.js';
|
|
4
|
+
export { Sankey_alias_1 as Sankey } from './_tsup-dts-rollup.js';
|
package/dist/sankey.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { clsx } from 'clsx';
|
|
4
|
+
import { twMerge } from 'tailwind-merge';
|
|
5
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
6
|
+
|
|
7
|
+
// src/lib/chart-palette.ts
|
|
8
|
+
var CHART_PALETTE = [
|
|
9
|
+
"hsl(var(--chart-1, var(--primary)))",
|
|
10
|
+
"hsl(var(--chart-2, var(--primary)))",
|
|
11
|
+
"hsl(var(--chart-3, var(--primary)))",
|
|
12
|
+
"hsl(var(--chart-4, var(--primary)))",
|
|
13
|
+
"hsl(var(--chart-5, var(--primary)))",
|
|
14
|
+
"hsl(var(--chart-6, var(--primary)))"
|
|
15
|
+
];
|
|
16
|
+
function pickChartHue(index) {
|
|
17
|
+
const safe = (index % CHART_PALETTE.length + CHART_PALETTE.length) % CHART_PALETTE.length;
|
|
18
|
+
return CHART_PALETTE[safe];
|
|
19
|
+
}
|
|
20
|
+
function cn(...inputs) {
|
|
21
|
+
return twMerge(clsx(inputs));
|
|
22
|
+
}
|
|
23
|
+
function Sankey({
|
|
24
|
+
nodes,
|
|
25
|
+
links,
|
|
26
|
+
width = 720,
|
|
27
|
+
height = 420,
|
|
28
|
+
nodeAlign = "justify",
|
|
29
|
+
nodeWidth = 12,
|
|
30
|
+
nodePadding = 8,
|
|
31
|
+
onLinkHover,
|
|
32
|
+
onNodeClick,
|
|
33
|
+
className,
|
|
34
|
+
...rest
|
|
35
|
+
}) {
|
|
36
|
+
const [d3s, setD3s] = React.useState(null);
|
|
37
|
+
React.useEffect(() => {
|
|
38
|
+
let cancelled = false;
|
|
39
|
+
void import('d3-sankey').then((mod) => {
|
|
40
|
+
if (!cancelled) setD3s(mod);
|
|
41
|
+
});
|
|
42
|
+
return () => {
|
|
43
|
+
cancelled = true;
|
|
44
|
+
};
|
|
45
|
+
}, []);
|
|
46
|
+
const laidOut = React.useMemo(() => {
|
|
47
|
+
if (!d3s) return null;
|
|
48
|
+
return layout(d3s, nodes, links, width, height, nodeAlign, nodeWidth, nodePadding);
|
|
49
|
+
}, [d3s, nodes, links, width, height, nodeAlign, nodeWidth, nodePadding]);
|
|
50
|
+
if (!d3s || !laidOut) {
|
|
51
|
+
return /* @__PURE__ */ jsx(
|
|
52
|
+
"div",
|
|
53
|
+
{
|
|
54
|
+
"data-hex-sankey-loading": true,
|
|
55
|
+
"aria-busy": "true",
|
|
56
|
+
"aria-label": "Loading Sankey diagram",
|
|
57
|
+
className: cn("inline-block bg-muted/20", className),
|
|
58
|
+
style: { width, height }
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
const { nodes: laidOutNodes, links: laidOutLinks } = laidOut;
|
|
63
|
+
const desc = `Sankey diagram with ${nodes.length} node${nodes.length === 1 ? "" : "s"} and ${links.length} link${links.length === 1 ? "" : "s"}`;
|
|
64
|
+
return /* @__PURE__ */ jsxs(
|
|
65
|
+
"svg",
|
|
66
|
+
{
|
|
67
|
+
...rest,
|
|
68
|
+
"data-hex-sankey": true,
|
|
69
|
+
"data-node-align": nodeAlign,
|
|
70
|
+
role: "img",
|
|
71
|
+
width,
|
|
72
|
+
height,
|
|
73
|
+
viewBox: `0 0 ${width} ${height}`,
|
|
74
|
+
className: cn("block", className),
|
|
75
|
+
children: [
|
|
76
|
+
/* @__PURE__ */ jsx("title", { children: "Sankey diagram" }),
|
|
77
|
+
/* @__PURE__ */ jsx("desc", { children: desc }),
|
|
78
|
+
/* @__PURE__ */ jsx("g", { "data-hex-sankey-links": true, fill: "none", children: laidOutLinks.map((l, i) => {
|
|
79
|
+
const interactive = Boolean(onLinkHover);
|
|
80
|
+
const fireHover = (link) => onLinkHover?.(link);
|
|
81
|
+
const stroke = pickChartHue(l.sourceIdx);
|
|
82
|
+
return /* @__PURE__ */ jsx(
|
|
83
|
+
"path",
|
|
84
|
+
{
|
|
85
|
+
"data-hex-sankey-link": true,
|
|
86
|
+
d: l.d,
|
|
87
|
+
stroke,
|
|
88
|
+
strokeOpacity: 0.45,
|
|
89
|
+
strokeWidth: Math.max(1, l.width),
|
|
90
|
+
role: interactive ? "button" : void 0,
|
|
91
|
+
tabIndex: interactive ? 0 : void 0,
|
|
92
|
+
"aria-label": interactive ? `Flow: ${l.original.source} \u2192 ${l.original.target} (${l.original.value})` : void 0,
|
|
93
|
+
style: {
|
|
94
|
+
cursor: interactive ? "pointer" : void 0,
|
|
95
|
+
transition: "stroke-opacity 120ms ease"
|
|
96
|
+
},
|
|
97
|
+
onMouseEnter: interactive ? () => fireHover(l.original) : void 0,
|
|
98
|
+
onMouseLeave: interactive ? () => fireHover(null) : void 0,
|
|
99
|
+
onFocus: interactive ? () => fireHover(l.original) : void 0,
|
|
100
|
+
onBlur: interactive ? () => fireHover(null) : void 0
|
|
101
|
+
},
|
|
102
|
+
`${l.original.source}-${l.original.target}-${i}`
|
|
103
|
+
);
|
|
104
|
+
}) }),
|
|
105
|
+
/* @__PURE__ */ jsx("g", { "data-hex-sankey-nodes": true, children: laidOutNodes.map((n) => {
|
|
106
|
+
const w = n.x1 - n.x0;
|
|
107
|
+
const h = n.y1 - n.y0;
|
|
108
|
+
const isRightSide = n.x0 > width / 2;
|
|
109
|
+
const interactive = Boolean(onNodeClick);
|
|
110
|
+
const handleActivate = () => onNodeClick?.(n.original);
|
|
111
|
+
const fill = pickChartHue(n.idx);
|
|
112
|
+
return /* @__PURE__ */ jsxs(
|
|
113
|
+
"g",
|
|
114
|
+
{
|
|
115
|
+
"data-hex-sankey-node": true,
|
|
116
|
+
transform: `translate(${n.x0},${n.y0})`,
|
|
117
|
+
role: interactive ? "button" : void 0,
|
|
118
|
+
tabIndex: interactive ? 0 : void 0,
|
|
119
|
+
"aria-label": interactive ? n.original.label : void 0,
|
|
120
|
+
style: interactive ? { cursor: "pointer" } : void 0,
|
|
121
|
+
onClick: interactive ? handleActivate : void 0,
|
|
122
|
+
onKeyDown: interactive ? (e) => activateOnKey(e, handleActivate) : void 0,
|
|
123
|
+
children: [
|
|
124
|
+
/* @__PURE__ */ jsx("rect", { width: w, height: h, fill, stroke: "hsl(var(--background))" }),
|
|
125
|
+
/* @__PURE__ */ jsx(
|
|
126
|
+
"text",
|
|
127
|
+
{
|
|
128
|
+
x: isRightSide ? -6 : w + 6,
|
|
129
|
+
y: h / 2,
|
|
130
|
+
dy: "0.35em",
|
|
131
|
+
fontSize: 12,
|
|
132
|
+
fontWeight: 500,
|
|
133
|
+
fill: "hsl(var(--foreground))",
|
|
134
|
+
textAnchor: isRightSide ? "end" : "start",
|
|
135
|
+
style: { pointerEvents: "none" },
|
|
136
|
+
children: n.original.label
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
]
|
|
140
|
+
},
|
|
141
|
+
n.original.id
|
|
142
|
+
);
|
|
143
|
+
}) })
|
|
144
|
+
]
|
|
145
|
+
}
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
function layout(d3s, nodes, links, width, height, align, nodeWidth, nodePadding) {
|
|
149
|
+
const alignFn = align === "left" ? d3s.sankeyLeft : align === "right" ? d3s.sankeyRight : align === "center" ? d3s.sankeyCenter : d3s.sankeyJustify;
|
|
150
|
+
const nodesClone = nodes.map((n) => ({ ...n }));
|
|
151
|
+
const linksClone = links.map((l) => ({ ...l }));
|
|
152
|
+
const sankeyGen = d3s.sankey().nodeId((d) => d.id).nodeAlign(alignFn).nodeWidth(nodeWidth).nodePadding(nodePadding).extent([
|
|
153
|
+
[1, 1],
|
|
154
|
+
[width - 1, height - 1]
|
|
155
|
+
]);
|
|
156
|
+
const result = sankeyGen({ nodes: nodesClone, links: linksClone });
|
|
157
|
+
const linkPath = d3s.sankeyLinkHorizontal();
|
|
158
|
+
const idByIndex = /* @__PURE__ */ new Map();
|
|
159
|
+
nodes.forEach((n, i) => idByIndex.set(n.id, i));
|
|
160
|
+
return {
|
|
161
|
+
nodes: result.nodes.map((n, i) => ({
|
|
162
|
+
original: { ...nodes[i] },
|
|
163
|
+
x0: n.x0 ?? 0,
|
|
164
|
+
x1: n.x1 ?? 0,
|
|
165
|
+
y0: n.y0 ?? 0,
|
|
166
|
+
y1: n.y1 ?? 0,
|
|
167
|
+
idx: i
|
|
168
|
+
})),
|
|
169
|
+
links: result.links.map((l, i) => {
|
|
170
|
+
const sourceId = typeof l.source === "string" ? l.source : l.source.id;
|
|
171
|
+
const targetId = typeof l.target === "string" ? l.target : l.target.id;
|
|
172
|
+
return {
|
|
173
|
+
original: { ...links[i], source: sourceId, target: targetId, value: l.value },
|
|
174
|
+
d: linkPath(l) ?? "",
|
|
175
|
+
width: l.width ?? 1,
|
|
176
|
+
sourceIdx: idByIndex.get(sourceId) ?? 0
|
|
177
|
+
};
|
|
178
|
+
})
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function activateOnKey(e, fn) {
|
|
182
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
183
|
+
e.preventDefault();
|
|
184
|
+
fn();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export { Sankey };
|
|
189
|
+
//# sourceMappingURL=sankey.js.map
|
|
190
|
+
//# sourceMappingURL=sankey.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/chart-palette.ts","../src/lib/utils.ts","../src/artifacts/sankey/sankey.tsx"],"names":[],"mappings":";;;;;;AAiBO,IAAM,aAAA,GAAgB;AAAA,EAC5B,qCAAA;AAAA,EACA,qCAAA;AAAA,EACA,qCAAA;AAAA,EACA,qCAAA;AAAA,EACA,qCAAA;AAAA,EACA;AACD,CAAA;AASO,SAAS,aAAa,KAAA,EAAuB;AACnD,EAAA,MAAM,QAAS,KAAA,GAAQ,aAAA,CAAc,MAAA,GAAU,aAAA,CAAc,UAAU,aAAA,CAAc,MAAA;AAErF,EAAA,OAAO,cAAc,IAAI,CAAA;AAC1B;AC7BO,SAAS,MAAM,MAAA,EAAsB;AAC3C,EAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAC,CAAA;AAC5B;ACyEA,SAAS,MAAA,CAAO;AAAA,EACf,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA,GAAQ,GAAA;AAAA,EACR,MAAA,GAAS,GAAA;AAAA,EACT,SAAA,GAAY,SAAA;AAAA,EACZ,SAAA,GAAY,EAAA;AAAA,EACZ,WAAA,GAAc,CAAA;AAAA,EACd,WAAA;AAAA,EACA,WAAA;AAAA,EACA,SAAA;AAAA,EACA,GAAG;AACJ,CAAA,EAAgB;AACf,EAAA,MAAM,CAAC,GAAA,EAAK,MAAM,CAAA,GAAU,eAA6B,IAAI,CAAA;AAE7D,EAAM,gBAAU,MAAM;AACrB,IAAA,IAAI,SAAA,GAAY,KAAA;AAChB,IAAA,KAAK,OAAO,WAAW,CAAA,CAAE,IAAA,CAAK,CAAC,GAAA,KAAQ;AACtC,MAAA,IAAI,CAAC,SAAA,EAAW,MAAA,CAAO,GAAG,CAAA;AAAA,IAC3B,CAAC,CAAA;AACD,IAAA,OAAO,MAAM;AACZ,MAAA,SAAA,GAAY,IAAA;AAAA,IACb,CAAA;AAAA,EACD,CAAA,EAAG,EAAE,CAAA;AAML,EAAA,MAAM,OAAA,GAAgB,cAAQ,MAAM;AACnC,IAAA,IAAI,CAAC,KAAK,OAAO,IAAA;AACjB,IAAA,OAAO,MAAA,CAAO,KAAK,KAAA,EAAO,KAAA,EAAO,OAAO,MAAA,EAAQ,SAAA,EAAW,WAAW,WAAW,CAAA;AAAA,EAClF,CAAA,EAAG,CAAC,GAAA,EAAK,KAAA,EAAO,KAAA,EAAO,OAAO,MAAA,EAAQ,SAAA,EAAW,SAAA,EAAW,WAAW,CAAC,CAAA;AAExE,EAAA,IAAI,CAAC,GAAA,IAAO,CAAC,OAAA,EAAS;AACrB,IAAA,uBACC,GAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACA,yBAAA,EAAuB,IAAA;AAAA,QACvB,WAAA,EAAU,MAAA;AAAA,QACV,YAAA,EAAW,wBAAA;AAAA,QACX,SAAA,EAAW,EAAA,CAAG,0BAAA,EAA4B,SAAS,CAAA;AAAA,QACnD,KAAA,EAAO,EAAE,KAAA,EAAO,MAAA;AAAO;AAAA,KACxB;AAAA,EAEF;AAEA,EAAA,MAAM,EAAE,KAAA,EAAO,YAAA,EAAc,KAAA,EAAO,cAAa,GAAI,OAAA;AACrD,EAAA,MAAM,OAAO,CAAA,oBAAA,EAAuB,KAAA,CAAM,MAAM,CAAA,KAAA,EAAQ,KAAA,CAAM,WAAW,CAAA,GAAI,EAAA,GAAK,GAAG,CAAA,KAAA,EAAQ,MAAM,MAAM,CAAA,KAAA,EAAQ,MAAM,MAAA,KAAW,CAAA,GAAI,KAAK,GAAG,CAAA,CAAA;AAE9I,EAAA,uBACC,IAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,GAAG,IAAA;AAAA,MACJ,iBAAA,EAAe,IAAA;AAAA,MACf,iBAAA,EAAiB,SAAA;AAAA,MACjB,IAAA,EAAK,KAAA;AAAA,MACL,KAAA;AAAA,MACA,MAAA;AAAA,MACA,OAAA,EAAS,CAAA,IAAA,EAAO,KAAK,CAAA,CAAA,EAAI,MAAM,CAAA,CAAA;AAAA,MAC/B,SAAA,EAAW,EAAA,CAAG,OAAA,EAAS,SAAS,CAAA;AAAA,MAEhC,QAAA,EAAA;AAAA,wBAAA,GAAA,CAAC,WAAM,QAAA,EAAA,gBAAA,EAAc,CAAA;AAAA,wBACrB,GAAA,CAAC,UAAM,QAAA,EAAA,IAAA,EAAK,CAAA;AAAA,wBACZ,GAAA,CAAC,GAAA,EAAA,EAAE,uBAAA,EAAqB,IAAA,EAAC,IAAA,EAAK,QAC5B,QAAA,EAAA,YAAA,CAAa,GAAA,CAAI,CAAC,CAAA,EAAG,CAAA,KAAM;AAC3B,UAAA,MAAM,WAAA,GAAc,QAAQ,WAAW,CAAA;AACvC,UAAA,MAAM,SAAA,GAAY,CAAC,IAAA,KAA4B,WAAA,GAAc,IAAI,CAAA;AACjE,UAAA,MAAM,MAAA,GAAS,YAAA,CAAa,CAAA,CAAE,SAAS,CAAA;AACvC,UAAA,uBACC,GAAA;AAAA,YAAC,MAAA;AAAA,YAAA;AAAA,cAEA,sBAAA,EAAoB,IAAA;AAAA,cACpB,GAAG,CAAA,CAAE,CAAA;AAAA,cACL,MAAA;AAAA,cACA,aAAA,EAAe,IAAA;AAAA,cACf,WAAA,EAAa,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,EAAE,KAAK,CAAA;AAAA,cAChC,IAAA,EAAM,cAAc,QAAA,GAAW,MAAA;AAAA,cAC/B,QAAA,EAAU,cAAc,CAAA,GAAI,MAAA;AAAA,cAC5B,YAAA,EAAY,WAAA,GAAc,CAAA,MAAA,EAAS,CAAA,CAAE,SAAS,MAAM,CAAA,QAAA,EAAM,CAAA,CAAE,QAAA,CAAS,MAAM,CAAA,EAAA,EAAK,CAAA,CAAE,QAAA,CAAS,KAAK,CAAA,CAAA,CAAA,GAAM,MAAA;AAAA,cACtG,KAAA,EAAO;AAAA,gBACN,MAAA,EAAQ,cAAc,SAAA,GAAY,MAAA;AAAA,gBAClC,UAAA,EAAY;AAAA,eACb;AAAA,cACA,cAAc,WAAA,GAAc,MAAM,SAAA,CAAU,CAAA,CAAE,QAAQ,CAAA,GAAI,MAAA;AAAA,cAC1D,YAAA,EAAc,WAAA,GAAc,MAAM,SAAA,CAAU,IAAI,CAAA,GAAI,MAAA;AAAA,cACpD,SAAS,WAAA,GAAc,MAAM,SAAA,CAAU,CAAA,CAAE,QAAQ,CAAA,GAAI,MAAA;AAAA,cACrD,MAAA,EAAQ,WAAA,GAAc,MAAM,SAAA,CAAU,IAAI,CAAA,GAAI;AAAA,aAAA;AAAA,YAhBzC,CAAA,EAAG,EAAE,QAAA,CAAS,MAAM,IAAI,CAAA,CAAE,QAAA,CAAS,MAAM,CAAA,CAAA,EAAI,CAAC,CAAA;AAAA,WAiBpD;AAAA,QAEF,CAAC,CAAA,EACF,CAAA;AAAA,4BACC,GAAA,EAAA,EAAE,uBAAA,EAAqB,MACtB,QAAA,EAAA,YAAA,CAAa,GAAA,CAAI,CAAC,CAAA,KAAM;AACxB,UAAA,MAAM,CAAA,GAAI,CAAA,CAAE,EAAA,GAAK,CAAA,CAAE,EAAA;AACnB,UAAA,MAAM,CAAA,GAAI,CAAA,CAAE,EAAA,GAAK,CAAA,CAAE,EAAA;AACnB,UAAA,MAAM,WAAA,GAAc,CAAA,CAAE,EAAA,GAAK,KAAA,GAAQ,CAAA;AACnC,UAAA,MAAM,WAAA,GAAc,QAAQ,WAAW,CAAA;AACvC,UAAA,MAAM,cAAA,GAAiB,MAAM,WAAA,GAAc,CAAA,CAAE,QAAQ,CAAA;AACrD,UAAA,MAAM,IAAA,GAAO,YAAA,CAAa,CAAA,CAAE,GAAG,CAAA;AAC/B,UAAA,uBACC,IAAA;AAAA,YAAC,GAAA;AAAA,YAAA;AAAA,cAEA,sBAAA,EAAoB,IAAA;AAAA,cACpB,WAAW,CAAA,UAAA,EAAa,CAAA,CAAE,EAAE,CAAA,CAAA,EAAI,EAAE,EAAE,CAAA,CAAA,CAAA;AAAA,cACpC,IAAA,EAAM,cAAc,QAAA,GAAW,MAAA;AAAA,cAC/B,QAAA,EAAU,cAAc,CAAA,GAAI,MAAA;AAAA,cAC5B,YAAA,EAAY,WAAA,GAAc,CAAA,CAAE,QAAA,CAAS,KAAA,GAAQ,MAAA;AAAA,cAC7C,KAAA,EAAO,WAAA,GAAc,EAAE,MAAA,EAAQ,WAAU,GAAI,MAAA;AAAA,cAC7C,OAAA,EAAS,cAAc,cAAA,GAAiB,MAAA;AAAA,cACxC,WAAW,WAAA,GAAc,CAAC,MAAM,aAAA,CAAc,CAAA,EAAG,cAAc,CAAA,GAAI,MAAA;AAAA,cAEnE,QAAA,EAAA;AAAA,gCAAA,GAAA,CAAC,UAAK,KAAA,EAAO,CAAA,EAAG,QAAQ,CAAA,EAAG,IAAA,EAAY,QAAO,wBAAA,EAAyB,CAAA;AAAA,gCACvE,GAAA;AAAA,kBAAC,MAAA;AAAA,kBAAA;AAAA,oBACA,CAAA,EAAG,WAAA,GAAc,EAAA,GAAK,CAAA,GAAI,CAAA;AAAA,oBAC1B,GAAG,CAAA,GAAI,CAAA;AAAA,oBACP,EAAA,EAAG,QAAA;AAAA,oBACH,QAAA,EAAU,EAAA;AAAA,oBACV,UAAA,EAAY,GAAA;AAAA,oBACZ,IAAA,EAAK,wBAAA;AAAA,oBACL,UAAA,EAAY,cAAc,KAAA,GAAQ,OAAA;AAAA,oBAClC,KAAA,EAAO,EAAE,aAAA,EAAe,MAAA,EAAO;AAAA,oBAE9B,YAAE,QAAA,CAAS;AAAA;AAAA;AACb;AAAA,aAAA;AAAA,YAtBK,EAAE,QAAA,CAAS;AAAA,WAuBjB;AAAA,QAEF,CAAC,CAAA,EACF;AAAA;AAAA;AAAA,GACD;AAEF;AAEA,SAAS,MAAA,CACR,KACA,KAAA,EACA,KAAA,EACA,OACA,MAAA,EACA,KAAA,EACA,WACA,WAAA,EACiD;AACjD,EAAA,MAAM,OAAA,GACL,KAAA,KAAU,MAAA,GACP,GAAA,CAAI,UAAA,GACJ,KAAA,KAAU,OAAA,GACV,GAAA,CAAI,WAAA,GACJ,KAAA,KAAU,QAAA,GACV,GAAA,CAAI,eACJ,GAAA,CAAI,aAAA;AAGR,EAAA,MAAM,UAAA,GAAa,MAAM,GAAA,CAAI,CAAC,OAAO,EAAE,GAAG,GAAE,CAAE,CAAA;AAC9C,EAAA,MAAM,UAAA,GAAa,MAAM,GAAA,CAAI,CAAC,OAAO,EAAE,GAAG,GAAE,CAAE,CAAA;AAW9C,EAAA,MAAM,YAAY,GAAA,CAChB,MAAA,GACA,MAAA,CAAO,CAAC,MAAM,CAAA,CAAE,EAAE,EAClB,SAAA,CAAU,OAAO,EACjB,SAAA,CAAU,SAAS,EACnB,WAAA,CAAY,WAAW,EACvB,MAAA,CAAO;AAAA,IACP,CAAC,GAAG,CAAC,CAAA;AAAA,IACL,CAAC,KAAA,GAAQ,CAAA,EAAG,MAAA,GAAS,CAAC;AAAA,GACtB,CAAA;AAEF,EAAA,MAAM,SAAS,SAAA,CAAU,EAAE,OAAO,UAAA,EAAY,KAAA,EAAO,YAAY,CAAA;AACjE,EAAA,MAAM,QAAA,GAAW,IAAI,oBAAA,EAA+C;AAKpE,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAAoB;AAC1C,EAAA,KAAA,CAAM,OAAA,CAAQ,CAAC,CAAA,EAAG,CAAA,KAAM,UAAU,GAAA,CAAI,CAAA,CAAE,EAAA,EAAI,CAAC,CAAC,CAAA;AAE9C,EAAA,OAAO;AAAA,IACN,OAAO,MAAA,CAAO,KAAA,CAAM,GAAA,CAAI,CAAC,GAAG,CAAA,MAAO;AAAA,MAClC,QAAA,EAAU,EAAE,GAAG,KAAA,CAAM,CAAC,CAAA,EAAE;AAAA,MACxB,EAAA,EAAI,EAAE,EAAA,IAAM,CAAA;AAAA,MACZ,EAAA,EAAI,EAAE,EAAA,IAAM,CAAA;AAAA,MACZ,EAAA,EAAI,EAAE,EAAA,IAAM,CAAA;AAAA,MACZ,EAAA,EAAI,EAAE,EAAA,IAAM,CAAA;AAAA,MACZ,GAAA,EAAK;AAAA,KACN,CAAE,CAAA;AAAA,IACF,OAAO,MAAA,CAAO,KAAA,CAAM,GAAA,CAAI,CAAC,GAAG,CAAA,KAAM;AAIjC,MAAA,MAAM,QAAA,GAAW,OAAO,CAAA,CAAE,MAAA,KAAW,WAAW,CAAA,CAAE,MAAA,GAAU,EAAE,MAAA,CAAuB,EAAA;AACrF,MAAA,MAAM,QAAA,GAAW,OAAO,CAAA,CAAE,MAAA,KAAW,WAAW,CAAA,CAAE,MAAA,GAAU,EAAE,MAAA,CAAuB,EAAA;AACrF,MAAA,OAAO;AAAA,QACN,QAAA,EAAU,EAAE,GAAG,KAAA,CAAM,CAAC,CAAA,EAAG,MAAA,EAAQ,QAAA,EAAU,MAAA,EAAQ,QAAA,EAAU,KAAA,EAAO,CAAA,CAAE,KAAA,EAAM;AAAA,QAC5E,CAAA,EAAG,QAAA,CAAS,CAAC,CAAA,IAAK,EAAA;AAAA,QAClB,KAAA,EAAO,EAAE,KAAA,IAAS,CAAA;AAAA,QAClB,SAAA,EAAW,SAAA,CAAU,GAAA,CAAI,QAAQ,CAAA,IAAK;AAAA,OACvC;AAAA,IACD,CAAC;AAAA,GACF;AACD;AAEA,SAAS,aAAA,CAAc,GAAwB,EAAA,EAAsB;AACpE,EAAA,IAAI,CAAA,CAAE,GAAA,KAAQ,OAAA,IAAW,CAAA,CAAE,QAAQ,GAAA,EAAK;AACvC,IAAA,CAAA,CAAE,cAAA,EAAe;AACjB,IAAA,EAAA,EAAG;AAAA,EACJ;AACD","file":"sankey.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 * Weighted-flow diagram. Nodes are arranged in horizontal columns by\n * topological depth; links between them are drawn as smooth curves whose\n * thickness encodes the flow value. Common for funnels, energy/material\n * flows, money flows, or any bipartite/multipartite \"value moving from A\n * to B\" visualization.\n *\n * Heavy peer: requires `d3-sankey` (~6 KB gzip; pulls in a small slice of\n * d3-shape too). The hex-core CLI's `add` flow prompts before installing.\n *\n * @example\n * <Sankey\n * nodes={[\n * { id: \"src-a\", label: \"Source A\" },\n * { id: \"src-b\", label: \"Source B\" },\n * { id: \"sink\", label: \"Sink\" },\n * ]}\n * links={[\n * { source: \"src-a\", target: \"sink\", value: 30 },\n * { source: \"src-b\", target: \"sink\", value: 10 },\n * ]}\n * />\n */\nexport type SankeyNode = {\n\tid: string;\n\tlabel: string;\n};\n\nexport type SankeyLink = {\n\tsource: string;\n\ttarget: string;\n\tvalue: number;\n};\n\nexport interface SankeyProps extends Omit<React.SVGAttributes<SVGSVGElement>, \"children\"> {\n\t/** Node definitions. Every link's `source` and `target` MUST match an `id` in this array. */\n\tnodes: SankeyNode[];\n\t/** Weighted links between nodes. Values must be positive. */\n\tlinks: SankeyLink[];\n\t/** Pixel width of the rendered SVG. Default 720. */\n\twidth?: number;\n\t/** Pixel height of the rendered SVG. Default 420. */\n\theight?: number;\n\t/** How nodes within a column are aligned. Default \"justify\". */\n\tnodeAlign?: \"left\" | \"right\" | \"center\" | \"justify\";\n\t/** Pixel width of each node rectangle. Default 12. */\n\tnodeWidth?: number;\n\t/** Vertical pixel gap between nodes in the same column. Default 8. */\n\tnodePadding?: number;\n\t/** Fired when a user hovers a link (or hover ends, with `null`). */\n\tonLinkHover?: (link: SankeyLink | null) => void;\n\t/** Fired when a node is clicked. */\n\tonNodeClick?: (node: SankeyNode) => void;\n}\n\ninterface LaidOutNode {\n\toriginal: SankeyNode;\n\tx0: number;\n\tx1: number;\n\ty0: number;\n\ty1: number;\n\t/** Index in the consumer-supplied `nodes` array — used to pick a hue\n\t * from CHART_PALETTE so adjacent columns read as distinct categories. */\n\tidx: number;\n}\n\ninterface LaidOutLink {\n\toriginal: SankeyLink;\n\td: string;\n\twidth: number;\n\t/** Index of the source node — links inherit their source's hue so the\n\t * eye can trace \"where did this volume come from\". */\n\tsourceIdx: number;\n}\n\ntype D3SankeyMod = typeof import(\"d3-sankey\");\n\nfunction Sankey({\n\tnodes,\n\tlinks,\n\twidth = 720,\n\theight = 420,\n\tnodeAlign = \"justify\",\n\tnodeWidth = 12,\n\tnodePadding = 8,\n\tonLinkHover,\n\tonNodeClick,\n\tclassName,\n\t...rest\n}: SankeyProps) {\n\tconst [d3s, setD3s] = React.useState<D3SankeyMod | null>(null);\n\n\tReact.useEffect(() => {\n\t\tlet cancelled = false;\n\t\tvoid import(\"d3-sankey\").then((mod) => {\n\t\t\tif (!cancelled) setD3s(mod);\n\t\t});\n\t\treturn () => {\n\t\t\tcancelled = true;\n\t\t};\n\t}, []);\n\n\t// Memoize the d3-sankey layout pass: it mutates clones every call (heavy\n\t// for large flows) and parents often re-render with stable nodes/links\n\t// identity. Hook order is stable since `d3s` only ever transitions\n\t// null → resolved once.\n\tconst laidOut = React.useMemo(() => {\n\t\tif (!d3s) return null;\n\t\treturn layout(d3s, nodes, links, width, height, nodeAlign, nodeWidth, nodePadding);\n\t}, [d3s, nodes, links, width, height, nodeAlign, nodeWidth, nodePadding]);\n\n\tif (!d3s || !laidOut) {\n\t\treturn (\n\t\t\t<div\n\t\t\t\tdata-hex-sankey-loading\n\t\t\t\taria-busy=\"true\"\n\t\t\t\taria-label=\"Loading Sankey diagram\"\n\t\t\t\tclassName={cn(\"inline-block bg-muted/20\", className)}\n\t\t\t\tstyle={{ width, height }}\n\t\t\t/>\n\t\t);\n\t}\n\n\tconst { nodes: laidOutNodes, links: laidOutLinks } = laidOut;\n\tconst desc = `Sankey diagram with ${nodes.length} node${nodes.length === 1 ? \"\" : \"s\"} and ${links.length} link${links.length === 1 ? \"\" : \"s\"}`;\n\n\treturn (\n\t\t<svg\n\t\t\t{...rest}\n\t\t\tdata-hex-sankey\n\t\t\tdata-node-align={nodeAlign}\n\t\t\trole=\"img\"\n\t\t\twidth={width}\n\t\t\theight={height}\n\t\t\tviewBox={`0 0 ${width} ${height}`}\n\t\t\tclassName={cn(\"block\", className)}\n\t\t>\n\t\t\t<title>Sankey diagram</title>\n\t\t\t<desc>{desc}</desc>\n\t\t\t<g data-hex-sankey-links fill=\"none\">\n\t\t\t\t{laidOutLinks.map((l, i) => {\n\t\t\t\t\tconst interactive = Boolean(onLinkHover);\n\t\t\t\t\tconst fireHover = (link: SankeyLink | null) => onLinkHover?.(link);\n\t\t\t\t\tconst stroke = pickChartHue(l.sourceIdx);\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<path\n\t\t\t\t\t\t\tkey={`${l.original.source}-${l.original.target}-${i}`}\n\t\t\t\t\t\t\tdata-hex-sankey-link\n\t\t\t\t\t\t\td={l.d}\n\t\t\t\t\t\t\tstroke={stroke}\n\t\t\t\t\t\t\tstrokeOpacity={0.45}\n\t\t\t\t\t\t\tstrokeWidth={Math.max(1, l.width)}\n\t\t\t\t\t\t\trole={interactive ? \"button\" : undefined}\n\t\t\t\t\t\t\ttabIndex={interactive ? 0 : undefined}\n\t\t\t\t\t\t\taria-label={interactive ? `Flow: ${l.original.source} → ${l.original.target} (${l.original.value})` : undefined}\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\tcursor: interactive ? \"pointer\" : undefined,\n\t\t\t\t\t\t\t\ttransition: \"stroke-opacity 120ms ease\",\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tonMouseEnter={interactive ? () => fireHover(l.original) : undefined}\n\t\t\t\t\t\t\tonMouseLeave={interactive ? () => fireHover(null) : undefined}\n\t\t\t\t\t\t\tonFocus={interactive ? () => fireHover(l.original) : undefined}\n\t\t\t\t\t\t\tonBlur={interactive ? () => fireHover(null) : undefined}\n\t\t\t\t\t\t/>\n\t\t\t\t\t);\n\t\t\t\t})}\n\t\t\t</g>\n\t\t\t<g data-hex-sankey-nodes>\n\t\t\t\t{laidOutNodes.map((n) => {\n\t\t\t\t\tconst w = n.x1 - n.x0;\n\t\t\t\t\tconst h = n.y1 - n.y0;\n\t\t\t\t\tconst isRightSide = n.x0 > width / 2;\n\t\t\t\t\tconst interactive = Boolean(onNodeClick);\n\t\t\t\t\tconst handleActivate = () => onNodeClick?.(n.original);\n\t\t\t\t\tconst fill = pickChartHue(n.idx);\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<g\n\t\t\t\t\t\t\tkey={n.original.id}\n\t\t\t\t\t\t\tdata-hex-sankey-node\n\t\t\t\t\t\t\ttransform={`translate(${n.x0},${n.y0})`}\n\t\t\t\t\t\t\trole={interactive ? \"button\" : undefined}\n\t\t\t\t\t\t\ttabIndex={interactive ? 0 : undefined}\n\t\t\t\t\t\t\taria-label={interactive ? n.original.label : undefined}\n\t\t\t\t\t\t\tstyle={interactive ? { cursor: \"pointer\" } : undefined}\n\t\t\t\t\t\t\tonClick={interactive ? handleActivate : undefined}\n\t\t\t\t\t\t\tonKeyDown={interactive ? (e) => activateOnKey(e, handleActivate) : undefined}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<rect width={w} height={h} fill={fill} stroke=\"hsl(var(--background))\" />\n\t\t\t\t\t\t\t<text\n\t\t\t\t\t\t\t\tx={isRightSide ? -6 : w + 6}\n\t\t\t\t\t\t\t\ty={h / 2}\n\t\t\t\t\t\t\t\tdy=\"0.35em\"\n\t\t\t\t\t\t\t\tfontSize={12}\n\t\t\t\t\t\t\t\tfontWeight={500}\n\t\t\t\t\t\t\t\tfill=\"hsl(var(--foreground))\"\n\t\t\t\t\t\t\t\ttextAnchor={isRightSide ? \"end\" : \"start\"}\n\t\t\t\t\t\t\t\tstyle={{ pointerEvents: \"none\" }}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{n.original.label}\n\t\t\t\t\t\t\t</text>\n\t\t\t\t\t\t</g>\n\t\t\t\t\t);\n\t\t\t\t})}\n\t\t\t</g>\n\t\t</svg>\n\t);\n}\n\nfunction layout(\n\td3s: D3SankeyMod,\n\tnodes: SankeyNode[],\n\tlinks: SankeyLink[],\n\twidth: number,\n\theight: number,\n\talign: \"left\" | \"right\" | \"center\" | \"justify\",\n\tnodeWidth: number,\n\tnodePadding: number,\n): { nodes: LaidOutNode[]; links: LaidOutLink[] } {\n\tconst alignFn =\n\t\talign === \"left\"\n\t\t\t? d3s.sankeyLeft\n\t\t\t: align === \"right\"\n\t\t\t? d3s.sankeyRight\n\t\t\t: align === \"center\"\n\t\t\t? d3s.sankeyCenter\n\t\t\t: d3s.sankeyJustify;\n\n\t// d3-sankey mutates its input — clone so consumer arrays stay pristine.\n\tconst nodesClone = nodes.map((n) => ({ ...n }));\n\tconst linksClone = links.map((l) => ({ ...l }));\n\n\ttype WorkingNode = SankeyNode & {\n\t\tindex?: number;\n\t\tx0?: number;\n\t\tx1?: number;\n\t\ty0?: number;\n\t\ty1?: number;\n\t};\n\ttype WorkingLink = SankeyLink & { width?: number };\n\n\tconst sankeyGen = d3s\n\t\t.sankey<WorkingNode, WorkingLink>()\n\t\t.nodeId((d) => d.id)\n\t\t.nodeAlign(alignFn)\n\t\t.nodeWidth(nodeWidth)\n\t\t.nodePadding(nodePadding)\n\t\t.extent([\n\t\t\t[1, 1],\n\t\t\t[width - 1, height - 1],\n\t\t]);\n\n\tconst result = sankeyGen({ nodes: nodesClone, links: linksClone });\n\tconst linkPath = d3s.sankeyLinkHorizontal<WorkingNode, WorkingLink>();\n\n\t// Carry the consumer-supplied node/link by index so future widenings of\n\t// SankeyNode / SankeyLink (color, group, metadata) round-trip into\n\t// callbacks without us having to re-cherry-pick fields here.\n\tconst idByIndex = new Map<string, number>();\n\tnodes.forEach((n, i) => idByIndex.set(n.id, i));\n\n\treturn {\n\t\tnodes: result.nodes.map((n, i) => ({\n\t\t\toriginal: { ...nodes[i] },\n\t\t\tx0: n.x0 ?? 0,\n\t\t\tx1: n.x1 ?? 0,\n\t\t\ty0: n.y0 ?? 0,\n\t\t\ty1: n.y1 ?? 0,\n\t\t\tidx: i,\n\t\t})),\n\t\tlinks: result.links.map((l, i) => {\n\t\t\t// Re-pin source/target to ids — d3-sankey replaced them with the\n\t\t\t// resolved node objects in place, but the consumer-facing shape\n\t\t\t// is `{ source: string, target: string, value: number }`.\n\t\t\tconst sourceId = typeof l.source === \"string\" ? l.source : (l.source as WorkingNode).id;\n\t\t\tconst targetId = typeof l.target === \"string\" ? l.target : (l.target as WorkingNode).id;\n\t\t\treturn {\n\t\t\t\toriginal: { ...links[i], source: sourceId, target: targetId, value: l.value },\n\t\t\t\td: linkPath(l) ?? \"\",\n\t\t\t\twidth: l.width ?? 1,\n\t\t\t\tsourceIdx: idByIndex.get(sourceId) ?? 0,\n\t\t\t};\n\t\t}),\n\t};\n}\n\nfunction activateOnKey(e: React.KeyboardEvent, fn: () => void): void {\n\tif (e.key === \"Enter\" || e.key === \" \") {\n\t\te.preventDefault();\n\t\tfn();\n\t}\n}\n\nexport { Sankey };\n"]}
|
package/dist/schemas.d.ts
CHANGED
|
@@ -77,3 +77,26 @@ export { messageActionsSchema } from './_tsup-dts-rollup.js';
|
|
|
77
77
|
export { citationSchema } from './_tsup-dts-rollup.js';
|
|
78
78
|
export { markdownSchema } from './_tsup-dts-rollup.js';
|
|
79
79
|
export { codeBlockSchema } from './_tsup-dts-rollup.js';
|
|
80
|
+
export { mindMapSchema } from './_tsup-dts-rollup.js';
|
|
81
|
+
export { treeMapSchema } from './_tsup-dts-rollup.js';
|
|
82
|
+
export { orgChartSchema } from './_tsup-dts-rollup.js';
|
|
83
|
+
export { sunburstSchema } from './_tsup-dts-rollup.js';
|
|
84
|
+
export { dendrogramSchema } from './_tsup-dts-rollup.js';
|
|
85
|
+
export { sankeySchema } from './_tsup-dts-rollup.js';
|
|
86
|
+
export { funnelSchema } from './_tsup-dts-rollup.js';
|
|
87
|
+
export { pyramidSchema } from './_tsup-dts-rollup.js';
|
|
88
|
+
export { flowchartSchema } from './_tsup-dts-rollup.js';
|
|
89
|
+
export { vennSchema } from './_tsup-dts-rollup.js';
|
|
90
|
+
export { chordSchema } from './_tsup-dts-rollup.js';
|
|
91
|
+
export { arcSchema } from './_tsup-dts-rollup.js';
|
|
92
|
+
export { matrixSchema } from './_tsup-dts-rollup.js';
|
|
93
|
+
export { timeAxisSchema } from './_tsup-dts-rollup.js';
|
|
94
|
+
export { ganttSchema } from './_tsup-dts-rollup.js';
|
|
95
|
+
export { sequenceSchema } from './_tsup-dts-rollup.js';
|
|
96
|
+
export { flashcardSchema } from './_tsup-dts-rollup.js';
|
|
97
|
+
export { clozeSchema } from './_tsup-dts-rollup.js';
|
|
98
|
+
export { imageOcclusionSchema } from './_tsup-dts-rollup.js';
|
|
99
|
+
export { quizSchema } from './_tsup-dts-rollup.js';
|
|
100
|
+
export { compareTableSchema } from './_tsup-dts-rollup.js';
|
|
101
|
+
export { deckSchema } from './_tsup-dts-rollup.js';
|
|
102
|
+
export { spacedRepetitionSchema } from './_tsup-dts-rollup.js';
|