@hex-core/components 1.7.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 +1561 -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 +84 -0
- package/dist/index.js +3946 -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 +2208 -1
- 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/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
|
@@ -0,0 +1,107 @@
|
|
|
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
|
+
function cn(...inputs) {
|
|
8
|
+
return twMerge(clsx(inputs));
|
|
9
|
+
}
|
|
10
|
+
function Flashcard({
|
|
11
|
+
front,
|
|
12
|
+
back,
|
|
13
|
+
defaultFlipped = false,
|
|
14
|
+
flipped: flippedProp,
|
|
15
|
+
onFlipChange,
|
|
16
|
+
width = 360,
|
|
17
|
+
height = 240,
|
|
18
|
+
flipDurationMs = 500,
|
|
19
|
+
className,
|
|
20
|
+
...rest
|
|
21
|
+
}) {
|
|
22
|
+
const [internalFlipped, setInternalFlipped] = React.useState(defaultFlipped);
|
|
23
|
+
const isControlled = flippedProp !== void 0;
|
|
24
|
+
const flipped = isControlled ? flippedProp : internalFlipped;
|
|
25
|
+
const toggle = React.useCallback(() => {
|
|
26
|
+
const next = !flipped;
|
|
27
|
+
if (!isControlled) setInternalFlipped(next);
|
|
28
|
+
onFlipChange?.(next);
|
|
29
|
+
}, [flipped, isControlled, onFlipChange]);
|
|
30
|
+
const handleKey = React.useCallback(
|
|
31
|
+
(e) => {
|
|
32
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
toggle();
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
[toggle]
|
|
38
|
+
);
|
|
39
|
+
return /* @__PURE__ */ jsx(
|
|
40
|
+
"div",
|
|
41
|
+
{
|
|
42
|
+
...rest,
|
|
43
|
+
"data-hex-flashcard": true,
|
|
44
|
+
"data-flipped": flipped,
|
|
45
|
+
role: "button",
|
|
46
|
+
tabIndex: 0,
|
|
47
|
+
"aria-pressed": flipped,
|
|
48
|
+
"aria-label": flipped ? "Flashcard, back side. Activate to flip to front." : "Flashcard, front side. Activate to reveal back.",
|
|
49
|
+
onClick: toggle,
|
|
50
|
+
onKeyDown: handleKey,
|
|
51
|
+
className: cn(
|
|
52
|
+
"relative inline-block cursor-pointer select-none focus:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
53
|
+
className
|
|
54
|
+
),
|
|
55
|
+
style: {
|
|
56
|
+
width,
|
|
57
|
+
height,
|
|
58
|
+
perspective: 1e3
|
|
59
|
+
},
|
|
60
|
+
children: /* @__PURE__ */ jsxs(
|
|
61
|
+
"div",
|
|
62
|
+
{
|
|
63
|
+
"data-hex-flashcard-inner": true,
|
|
64
|
+
className: "relative h-full w-full transition-transform",
|
|
65
|
+
style: {
|
|
66
|
+
transformStyle: "preserve-3d",
|
|
67
|
+
transform: flipped ? "rotateY(180deg)" : "rotateY(0deg)",
|
|
68
|
+
transitionDuration: `${flipDurationMs}ms`
|
|
69
|
+
},
|
|
70
|
+
children: [
|
|
71
|
+
/* @__PURE__ */ jsx(
|
|
72
|
+
"div",
|
|
73
|
+
{
|
|
74
|
+
"data-hex-flashcard-face": true,
|
|
75
|
+
"data-side": "front",
|
|
76
|
+
className: "absolute inset-0 flex items-center justify-center rounded-lg border bg-card p-4 text-center text-card-foreground shadow-sm",
|
|
77
|
+
style: {
|
|
78
|
+
backfaceVisibility: "hidden",
|
|
79
|
+
WebkitBackfaceVisibility: "hidden"
|
|
80
|
+
},
|
|
81
|
+
children: front
|
|
82
|
+
}
|
|
83
|
+
),
|
|
84
|
+
/* @__PURE__ */ jsx(
|
|
85
|
+
"div",
|
|
86
|
+
{
|
|
87
|
+
"data-hex-flashcard-face": true,
|
|
88
|
+
"data-side": "back",
|
|
89
|
+
className: "absolute inset-0 flex items-center justify-center rounded-lg border bg-card p-4 text-center text-card-foreground shadow-sm",
|
|
90
|
+
style: {
|
|
91
|
+
backfaceVisibility: "hidden",
|
|
92
|
+
WebkitBackfaceVisibility: "hidden",
|
|
93
|
+
transform: "rotateY(180deg)"
|
|
94
|
+
},
|
|
95
|
+
children: back
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export { Flashcard };
|
|
106
|
+
//# sourceMappingURL=flashcard.js.map
|
|
107
|
+
//# sourceMappingURL=flashcard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/utils.ts","../src/artifacts/flashcard/flashcard.tsx"],"names":[],"mappings":"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","file":"flashcard.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 * Flashcard — front/back card with a 3D flip animation. Click, Enter, or\n * Space to flip. Pure CSS 3D transform, no animation peer required.\n *\n * Headless on content: pass any ReactNode for `front` and `back`. Pair\n * with Deck (artifacts/deck) for shuffle / next / prev / progress, or\n * with SpacedRepetition (artifacts/spaced-repetition) for confidence\n * rating after each reveal.\n *\n * @example\n * <Flashcard\n * front={<>What is the capital of France?</>}\n * back={<>Paris</>}\n * />\n *\n * <Flashcard\n * flipped={isFlipped}\n * onFlipChange={setFlipped}\n * front={term}\n * back={definition}\n * />\n */\nexport interface FlashcardProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\"> {\n\t/** Content of the front face. */\n\tfront: React.ReactNode;\n\t/** Content of the back face. */\n\tback: React.ReactNode;\n\t/** Uncontrolled initial flipped state. Default false. */\n\tdefaultFlipped?: boolean;\n\t/** Controlled flipped state. */\n\tflipped?: boolean;\n\t/** Fired with the new flipped value when the user toggles. */\n\tonFlipChange?: (flipped: boolean) => void;\n\t/** Pixel width. Default 360. */\n\twidth?: number;\n\t/** Pixel height. Default 240. */\n\theight?: number;\n\t/** Flip animation duration in ms. Default 500. Set to 0 to disable the animation entirely. */\n\tflipDurationMs?: number;\n}\n\nfunction Flashcard({\n\tfront,\n\tback,\n\tdefaultFlipped = false,\n\tflipped: flippedProp,\n\tonFlipChange,\n\twidth = 360,\n\theight = 240,\n\tflipDurationMs = 500,\n\tclassName,\n\t...rest\n}: FlashcardProps) {\n\tconst [internalFlipped, setInternalFlipped] = React.useState(defaultFlipped);\n\tconst isControlled = flippedProp !== undefined;\n\tconst flipped = isControlled ? flippedProp : internalFlipped;\n\n\tconst toggle = React.useCallback(() => {\n\t\tconst next = !flipped;\n\t\tif (!isControlled) setInternalFlipped(next);\n\t\tonFlipChange?.(next);\n\t}, [flipped, isControlled, onFlipChange]);\n\n\tconst handleKey = React.useCallback(\n\t\t(e: React.KeyboardEvent) => {\n\t\t\tif (e.key === \"Enter\" || e.key === \" \") {\n\t\t\t\te.preventDefault();\n\t\t\t\ttoggle();\n\t\t\t}\n\t\t},\n\t\t[toggle],\n\t);\n\n\treturn (\n\t\t<div\n\t\t\t{...rest}\n\t\t\tdata-hex-flashcard\n\t\t\tdata-flipped={flipped}\n\t\t\trole=\"button\"\n\t\t\ttabIndex={0}\n\t\t\taria-pressed={flipped}\n\t\t\taria-label={flipped ? \"Flashcard, back side. Activate to flip to front.\" : \"Flashcard, front side. Activate to reveal back.\"}\n\t\t\tonClick={toggle}\n\t\t\tonKeyDown={handleKey}\n\t\t\tclassName={cn(\n\t\t\t\t\"relative inline-block cursor-pointer select-none focus:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n\t\t\t\tclassName,\n\t\t\t)}\n\t\t\tstyle={{\n\t\t\t\twidth,\n\t\t\t\theight,\n\t\t\t\tperspective: 1000,\n\t\t\t}}\n\t\t>\n\t\t\t<div\n\t\t\t\tdata-hex-flashcard-inner\n\t\t\t\tclassName=\"relative h-full w-full transition-transform\"\n\t\t\t\tstyle={{\n\t\t\t\t\ttransformStyle: \"preserve-3d\",\n\t\t\t\t\ttransform: flipped ? \"rotateY(180deg)\" : \"rotateY(0deg)\",\n\t\t\t\t\ttransitionDuration: `${flipDurationMs}ms`,\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<div\n\t\t\t\t\tdata-hex-flashcard-face\n\t\t\t\t\tdata-side=\"front\"\n\t\t\t\t\tclassName=\"absolute inset-0 flex items-center justify-center rounded-lg border bg-card p-4 text-center text-card-foreground shadow-sm\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tbackfaceVisibility: \"hidden\",\n\t\t\t\t\t\tWebkitBackfaceVisibility: \"hidden\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t{front}\n\t\t\t\t</div>\n\t\t\t\t<div\n\t\t\t\t\tdata-hex-flashcard-face\n\t\t\t\t\tdata-side=\"back\"\n\t\t\t\t\tclassName=\"absolute inset-0 flex items-center justify-center rounded-lg border bg-card p-4 text-center text-card-foreground shadow-sm\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tbackfaceVisibility: \"hidden\",\n\t\t\t\t\t\tWebkitBackfaceVisibility: \"hidden\",\n\t\t\t\t\t\ttransform: \"rotateY(180deg)\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t{back}\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n\nexport { Flashcard };\n"]}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { FlowchartNode_alias_1 as FlowchartNode } from './_tsup-dts-rollup.js';
|
|
2
|
+
export { FlowchartEdge_alias_1 as FlowchartEdge } from './_tsup-dts-rollup.js';
|
|
3
|
+
export { FlowchartProps_alias_1 as FlowchartProps } from './_tsup-dts-rollup.js';
|
|
4
|
+
export { Flowchart_alias_1 as Flowchart } from './_tsup-dts-rollup.js';
|
|
@@ -0,0 +1,275 @@
|
|
|
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 Flowchart({
|
|
11
|
+
nodes,
|
|
12
|
+
edges,
|
|
13
|
+
direction = "vertical",
|
|
14
|
+
width = 720,
|
|
15
|
+
height = 480,
|
|
16
|
+
nodeWidth = 140,
|
|
17
|
+
nodeHeight = 48,
|
|
18
|
+
onNodeClick,
|
|
19
|
+
className,
|
|
20
|
+
...rest
|
|
21
|
+
}) {
|
|
22
|
+
const { nodes: laidOutNodes, edges: laidOutEdges } = React.useMemo(
|
|
23
|
+
() => layout(nodes, edges, direction, width, height, nodeWidth, nodeHeight),
|
|
24
|
+
[nodes, edges, direction, width, height, nodeWidth, nodeHeight]
|
|
25
|
+
);
|
|
26
|
+
const desc = `Flowchart with ${nodes.length} node${nodes.length === 1 ? "" : "s"} and ${edges.length} edge${edges.length === 1 ? "" : "s"}, laid out ${direction}`;
|
|
27
|
+
const arrowId = React.useId().replace(/:/g, "-");
|
|
28
|
+
return /* @__PURE__ */ jsxs(
|
|
29
|
+
"svg",
|
|
30
|
+
{
|
|
31
|
+
...rest,
|
|
32
|
+
"data-hex-flowchart": true,
|
|
33
|
+
"data-direction": direction,
|
|
34
|
+
role: "img",
|
|
35
|
+
width,
|
|
36
|
+
height,
|
|
37
|
+
viewBox: `0 0 ${width} ${height}`,
|
|
38
|
+
className: cn("block", className),
|
|
39
|
+
children: [
|
|
40
|
+
/* @__PURE__ */ jsx("title", { children: "Flowchart" }),
|
|
41
|
+
/* @__PURE__ */ jsx("desc", { children: desc }),
|
|
42
|
+
/* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx(
|
|
43
|
+
"marker",
|
|
44
|
+
{
|
|
45
|
+
id: `hex-flowchart-arrow-${arrowId}`,
|
|
46
|
+
viewBox: "0 0 10 10",
|
|
47
|
+
refX: "10",
|
|
48
|
+
refY: "5",
|
|
49
|
+
markerWidth: "6",
|
|
50
|
+
markerHeight: "6",
|
|
51
|
+
orient: "auto-start-reverse",
|
|
52
|
+
children: /* @__PURE__ */ jsx("path", { d: "M 0 0 L 10 5 L 0 10 z", fill: "hsl(var(--muted-foreground))" })
|
|
53
|
+
}
|
|
54
|
+
) }),
|
|
55
|
+
/* @__PURE__ */ jsx("g", { "data-hex-flowchart-edges": true, children: laidOutEdges.map((e, i) => /* @__PURE__ */ jsxs("g", { "data-hex-flowchart-edge": true, children: [
|
|
56
|
+
/* @__PURE__ */ jsx(
|
|
57
|
+
"path",
|
|
58
|
+
{
|
|
59
|
+
d: edgePath(e.from, e.to, direction),
|
|
60
|
+
fill: "none",
|
|
61
|
+
stroke: "hsl(var(--muted-foreground))",
|
|
62
|
+
strokeOpacity: 0.7,
|
|
63
|
+
strokeWidth: 1.25,
|
|
64
|
+
markerEnd: `url(#hex-flowchart-arrow-${arrowId})`
|
|
65
|
+
}
|
|
66
|
+
),
|
|
67
|
+
e.edge.label ? /* @__PURE__ */ jsx(
|
|
68
|
+
"text",
|
|
69
|
+
{
|
|
70
|
+
x: (e.from.x + e.to.x) / 2,
|
|
71
|
+
y: (e.from.y + e.to.y) / 2 - 4,
|
|
72
|
+
textAnchor: "middle",
|
|
73
|
+
fontSize: 10,
|
|
74
|
+
fill: "hsl(var(--muted-foreground))",
|
|
75
|
+
style: {
|
|
76
|
+
paintOrder: "stroke"
|
|
77
|
+
},
|
|
78
|
+
stroke: "hsl(var(--background))",
|
|
79
|
+
strokeWidth: 3,
|
|
80
|
+
strokeLinejoin: "round",
|
|
81
|
+
children: e.edge.label
|
|
82
|
+
}
|
|
83
|
+
) : null
|
|
84
|
+
] }, `${e.edge.source}-${e.edge.target}-${i}`)) }),
|
|
85
|
+
/* @__PURE__ */ jsx("g", { "data-hex-flowchart-nodes": true, children: laidOutNodes.map((n) => {
|
|
86
|
+
const shape = n.node.shape ?? "rect";
|
|
87
|
+
const interactive = Boolean(onNodeClick);
|
|
88
|
+
const handleActivate = () => onNodeClick?.(n.node);
|
|
89
|
+
const truncated = truncate(n.node.label, Math.floor((nodeWidth - 16) / 7));
|
|
90
|
+
const isTruncated = truncated !== n.node.label;
|
|
91
|
+
return /* @__PURE__ */ jsxs(
|
|
92
|
+
"g",
|
|
93
|
+
{
|
|
94
|
+
"data-hex-flowchart-node": true,
|
|
95
|
+
"data-shape": shape,
|
|
96
|
+
"data-rank": n.rank,
|
|
97
|
+
transform: `translate(${n.x - nodeWidth / 2},${n.y - nodeHeight / 2})`,
|
|
98
|
+
role: interactive ? "button" : void 0,
|
|
99
|
+
tabIndex: interactive ? 0 : void 0,
|
|
100
|
+
"aria-label": interactive ? n.node.label : void 0,
|
|
101
|
+
style: interactive ? { cursor: "pointer" } : void 0,
|
|
102
|
+
onClick: interactive ? handleActivate : void 0,
|
|
103
|
+
onKeyDown: interactive ? (e) => activateOnKey(e, handleActivate) : void 0,
|
|
104
|
+
children: [
|
|
105
|
+
isTruncated ? /* @__PURE__ */ jsx("title", { children: n.node.label }) : null,
|
|
106
|
+
shape === "diamond" ? /* @__PURE__ */ jsx(
|
|
107
|
+
"polygon",
|
|
108
|
+
{
|
|
109
|
+
points: `${nodeWidth / 2},0 ${nodeWidth},${nodeHeight / 2} ${nodeWidth / 2},${nodeHeight} 0,${nodeHeight / 2}`,
|
|
110
|
+
fill: "hsl(var(--card))",
|
|
111
|
+
stroke: "hsl(var(--border))",
|
|
112
|
+
strokeWidth: 1
|
|
113
|
+
}
|
|
114
|
+
) : /* @__PURE__ */ jsx(
|
|
115
|
+
"rect",
|
|
116
|
+
{
|
|
117
|
+
width: nodeWidth,
|
|
118
|
+
height: nodeHeight,
|
|
119
|
+
rx: shape === "round" ? nodeHeight / 2 : 6,
|
|
120
|
+
ry: shape === "round" ? nodeHeight / 2 : 6,
|
|
121
|
+
fill: "hsl(var(--card))",
|
|
122
|
+
stroke: "hsl(var(--border))",
|
|
123
|
+
strokeWidth: 1
|
|
124
|
+
}
|
|
125
|
+
),
|
|
126
|
+
/* @__PURE__ */ jsx(
|
|
127
|
+
"text",
|
|
128
|
+
{
|
|
129
|
+
x: nodeWidth / 2,
|
|
130
|
+
y: nodeHeight / 2,
|
|
131
|
+
dy: "0.35em",
|
|
132
|
+
textAnchor: "middle",
|
|
133
|
+
fontSize: 12,
|
|
134
|
+
fontWeight: 500,
|
|
135
|
+
fill: "hsl(var(--foreground))",
|
|
136
|
+
style: { pointerEvents: "none" },
|
|
137
|
+
children: truncated
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
]
|
|
141
|
+
},
|
|
142
|
+
n.node.id
|
|
143
|
+
);
|
|
144
|
+
}) })
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
function layout(nodes, edges, direction, width, height, nodeWidth, nodeHeight) {
|
|
150
|
+
if (nodes.length === 0) return { nodes: [], edges: [] };
|
|
151
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
152
|
+
const ranks = computeRanks(nodes, edges, byId);
|
|
153
|
+
const maxRank = Math.max(...ranks.values(), 0);
|
|
154
|
+
const rankGroups = /* @__PURE__ */ new Map();
|
|
155
|
+
for (const n of nodes) {
|
|
156
|
+
const r = ranks.get(n.id) ?? 0;
|
|
157
|
+
const arr = rankGroups.get(r) ?? [];
|
|
158
|
+
arr.push(n.id);
|
|
159
|
+
rankGroups.set(r, arr);
|
|
160
|
+
}
|
|
161
|
+
const parentsOf = /* @__PURE__ */ new Map();
|
|
162
|
+
for (const n of nodes) parentsOf.set(n.id, []);
|
|
163
|
+
for (const e of edges) {
|
|
164
|
+
const arr = parentsOf.get(e.target);
|
|
165
|
+
if (arr && byId.has(e.source)) arr.push(e.source);
|
|
166
|
+
}
|
|
167
|
+
const orderInRank = /* @__PURE__ */ new Map();
|
|
168
|
+
for (let r = 0; r <= maxRank; r++) {
|
|
169
|
+
const group = rankGroups.get(r) ?? [];
|
|
170
|
+
if (r > 0) {
|
|
171
|
+
group.sort((a, b) => meanParentIndex(a, parentsOf, orderInRank) - meanParentIndex(b, parentsOf, orderInRank));
|
|
172
|
+
}
|
|
173
|
+
group.forEach((id, i) => orderInRank.set(id, i));
|
|
174
|
+
}
|
|
175
|
+
const positions = /* @__PURE__ */ new Map();
|
|
176
|
+
const margin = 32;
|
|
177
|
+
const usableMain = (direction === "vertical" ? height : width) - margin * 2;
|
|
178
|
+
const usableCross = (direction === "vertical" ? width : height) - margin * 2;
|
|
179
|
+
const rankCount = maxRank + 1;
|
|
180
|
+
const mainStep = rankCount > 1 ? usableMain / (rankCount - 1) : 0;
|
|
181
|
+
for (let r = 0; r <= maxRank; r++) {
|
|
182
|
+
const group = rankGroups.get(r) ?? [];
|
|
183
|
+
const crossStep = group.length > 0 ? usableCross / (group.length + 1) : 0;
|
|
184
|
+
group.forEach((id, i) => {
|
|
185
|
+
const cross = margin + crossStep * (i + 1);
|
|
186
|
+
const main = margin + mainStep * r;
|
|
187
|
+
if (direction === "vertical") {
|
|
188
|
+
positions.set(id, { x: cross, y: main, rank: r });
|
|
189
|
+
} else {
|
|
190
|
+
positions.set(id, { x: main, y: cross, rank: r });
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
const laidOutNodes = nodes.map((n) => {
|
|
195
|
+
const p = positions.get(n.id) ?? { x: 0, y: 0, rank: 0 };
|
|
196
|
+
return { node: n, x: p.x, y: p.y, rank: p.rank };
|
|
197
|
+
});
|
|
198
|
+
const laidOutEdges = edges.map((edge) => {
|
|
199
|
+
const sp = positions.get(edge.source);
|
|
200
|
+
const tp = positions.get(edge.target);
|
|
201
|
+
if (!sp || !tp) return null;
|
|
202
|
+
const from = direction === "vertical" ? { x: sp.x, y: sp.y + nodeHeight / 2 } : { x: sp.x + nodeWidth / 2, y: sp.y };
|
|
203
|
+
const to = direction === "vertical" ? { x: tp.x, y: tp.y - nodeHeight / 2 } : { x: tp.x - nodeWidth / 2, y: tp.y };
|
|
204
|
+
return { edge, from, to };
|
|
205
|
+
}).filter((e) => e !== null);
|
|
206
|
+
return { nodes: laidOutNodes, edges: laidOutEdges };
|
|
207
|
+
}
|
|
208
|
+
function meanParentIndex(id, parentsOf, orderInRank) {
|
|
209
|
+
const parents = parentsOf.get(id) ?? [];
|
|
210
|
+
if (parents.length === 0) return Number.POSITIVE_INFINITY;
|
|
211
|
+
let sum = 0;
|
|
212
|
+
let count = 0;
|
|
213
|
+
for (const p of parents) {
|
|
214
|
+
const idx = orderInRank.get(p);
|
|
215
|
+
if (idx != null) {
|
|
216
|
+
sum += idx;
|
|
217
|
+
count++;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return count === 0 ? Number.POSITIVE_INFINITY : sum / count;
|
|
221
|
+
}
|
|
222
|
+
function computeRanks(nodes, edges, byId) {
|
|
223
|
+
const ranks = /* @__PURE__ */ new Map();
|
|
224
|
+
const incoming = /* @__PURE__ */ new Map();
|
|
225
|
+
for (const n of nodes) incoming.set(n.id, []);
|
|
226
|
+
for (const e of edges) {
|
|
227
|
+
const arr = incoming.get(e.target);
|
|
228
|
+
if (arr && byId.has(e.source)) arr.push(e.source);
|
|
229
|
+
}
|
|
230
|
+
const memo = /* @__PURE__ */ new Map();
|
|
231
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
232
|
+
const rankOf = (id) => {
|
|
233
|
+
const cached = memo.get(id);
|
|
234
|
+
if (cached !== void 0) return cached;
|
|
235
|
+
if (visiting.has(id)) return 0;
|
|
236
|
+
visiting.add(id);
|
|
237
|
+
try {
|
|
238
|
+
const node = byId.get(id);
|
|
239
|
+
if (node?.rank != null) {
|
|
240
|
+
memo.set(id, node.rank);
|
|
241
|
+
return node.rank;
|
|
242
|
+
}
|
|
243
|
+
const parents = incoming.get(id) ?? [];
|
|
244
|
+
const r = parents.length === 0 ? 0 : 1 + Math.max(...parents.map((p) => rankOf(p)));
|
|
245
|
+
memo.set(id, r);
|
|
246
|
+
return r;
|
|
247
|
+
} finally {
|
|
248
|
+
visiting.delete(id);
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
for (const n of nodes) ranks.set(n.id, rankOf(n.id));
|
|
252
|
+
return ranks;
|
|
253
|
+
}
|
|
254
|
+
function edgePath(from, to, direction) {
|
|
255
|
+
if (direction === "vertical") {
|
|
256
|
+
const my = (from.y + to.y) / 2;
|
|
257
|
+
return `M${from.x},${from.y} C${from.x},${my} ${to.x},${my} ${to.x},${to.y}`;
|
|
258
|
+
}
|
|
259
|
+
const mx = (from.x + to.x) / 2;
|
|
260
|
+
return `M${from.x},${from.y} C${mx},${from.y} ${mx},${to.y} ${to.x},${to.y}`;
|
|
261
|
+
}
|
|
262
|
+
function truncate(s, max) {
|
|
263
|
+
if (s.length <= max) return s;
|
|
264
|
+
return `${s.slice(0, Math.max(1, max - 1))}\u2026`;
|
|
265
|
+
}
|
|
266
|
+
function activateOnKey(e, fn) {
|
|
267
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
268
|
+
e.preventDefault();
|
|
269
|
+
fn();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export { Flowchart };
|
|
274
|
+
//# sourceMappingURL=flowchart.js.map
|
|
275
|
+
//# sourceMappingURL=flowchart.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/utils.ts","../src/artifacts/flowchart/flowchart.tsx"],"names":[],"mappings":"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","file":"flowchart.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 * Typed React flowchart. Pass `nodes` (with optional `shape`/`type`) and\n * `edges`; the component runs a topological-rank auto-layout and renders\n * top-to-bottom or left-to-right with directional arrows. Pure SVG; no\n * heavy peer dependency, no DAG-layout library.\n *\n * Distinct from `<Diagram>` (Mermaid string DSL) and `<Canvas>` (free-form\n * ReactFlow). Use Flowchart when you have STRUCTURED data and want a\n * polished SVG without the bundle cost or DSL of those alternatives.\n *\n * @example\n * <Flowchart\n * nodes={[\n * { id: \"start\", label: \"Start\", shape: \"round\" },\n * { id: \"check\", label: \"Authorized?\", shape: \"diamond\" },\n * { id: \"ok\", label: \"Continue\" },\n * { id: \"denied\", label: \"Reject\", shape: \"round\" },\n * ]}\n * edges={[\n * { source: \"start\", target: \"check\" },\n * { source: \"check\", target: \"ok\", label: \"yes\" },\n * { source: \"check\", target: \"denied\", label: \"no\" },\n * ]}\n * />\n */\nexport type FlowchartNode = {\n\tid: string;\n\tlabel: string;\n\t/** Visual shape — \"rect\" (default), \"round\" (rounded rect, terminal markers), or \"diamond\" (decision). */\n\tshape?: \"rect\" | \"round\" | \"diamond\";\n\t/** Optional explicit rank/depth override; otherwise computed via topological sort. */\n\trank?: number;\n};\n\nexport type FlowchartEdge = {\n\tsource: string;\n\ttarget: string;\n\tlabel?: string;\n};\n\nexport interface FlowchartProps extends Omit<React.SVGAttributes<SVGSVGElement>, \"children\"> {\n\t/** Node definitions. Every edge's `source`/`target` MUST match an `id` here. */\n\tnodes: FlowchartNode[];\n\t/** Directional edges. The graph MUST be a DAG (no cycles). */\n\tedges: FlowchartEdge[];\n\t/** Layout direction. Default \"vertical\" (top-to-bottom). */\n\tdirection?: \"vertical\" | \"horizontal\";\n\t/** Pixel width of the rendered SVG. Default 720. */\n\twidth?: number;\n\t/** Pixel height of the rendered SVG. Default 480. */\n\theight?: number;\n\t/** Pixel width of each node. Default 140. */\n\tnodeWidth?: number;\n\t/** Pixel height of each node. Default 48. */\n\tnodeHeight?: number;\n\t/** Fired when a node is clicked. */\n\tonNodeClick?: (node: FlowchartNode) => void;\n}\n\ninterface LaidOutNode {\n\tnode: FlowchartNode;\n\tx: number;\n\ty: number;\n\trank: number;\n}\n\ninterface LaidOutEdge {\n\tedge: FlowchartEdge;\n\tfrom: { x: number; y: number };\n\tto: { x: number; y: number };\n}\n\nfunction Flowchart({\n\tnodes,\n\tedges,\n\tdirection = \"vertical\",\n\twidth = 720,\n\theight = 480,\n\tnodeWidth = 140,\n\tnodeHeight = 48,\n\tonNodeClick,\n\tclassName,\n\t...rest\n}: FlowchartProps) {\n\tconst { nodes: laidOutNodes, edges: laidOutEdges } = React.useMemo(\n\t\t() => layout(nodes, edges, direction, width, height, nodeWidth, nodeHeight),\n\t\t[nodes, edges, direction, width, height, nodeWidth, nodeHeight],\n\t);\n\tconst desc = `Flowchart with ${nodes.length} node${nodes.length === 1 ? \"\" : \"s\"} and ${edges.length} edge${edges.length === 1 ? \"\" : \"s\"}, laid out ${direction}`;\n\tconst arrowId = React.useId().replace(/:/g, \"-\");\n\n\treturn (\n\t\t<svg\n\t\t\t{...rest}\n\t\t\tdata-hex-flowchart\n\t\t\tdata-direction={direction}\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>Flowchart</title>\n\t\t\t<desc>{desc}</desc>\n\t\t\t<defs>\n\t\t\t\t<marker\n\t\t\t\t\tid={`hex-flowchart-arrow-${arrowId}`}\n\t\t\t\t\tviewBox=\"0 0 10 10\"\n\t\t\t\t\trefX=\"10\"\n\t\t\t\t\trefY=\"5\"\n\t\t\t\t\tmarkerWidth=\"6\"\n\t\t\t\t\tmarkerHeight=\"6\"\n\t\t\t\t\torient=\"auto-start-reverse\"\n\t\t\t\t>\n\t\t\t\t\t<path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"hsl(var(--muted-foreground))\" />\n\t\t\t\t</marker>\n\t\t\t</defs>\n\t\t\t<g data-hex-flowchart-edges>\n\t\t\t\t{laidOutEdges.map((e, i) => (\n\t\t\t\t\t<g key={`${e.edge.source}-${e.edge.target}-${i}`} data-hex-flowchart-edge>\n\t\t\t\t\t\t<path\n\t\t\t\t\t\t\td={edgePath(e.from, e.to, direction)}\n\t\t\t\t\t\t\tfill=\"none\"\n\t\t\t\t\t\t\tstroke=\"hsl(var(--muted-foreground))\"\n\t\t\t\t\t\t\tstrokeOpacity={0.7}\n\t\t\t\t\t\t\tstrokeWidth={1.25}\n\t\t\t\t\t\t\tmarkerEnd={`url(#hex-flowchart-arrow-${arrowId})`}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t{e.edge.label ? (\n\t\t\t\t\t\t\t<text\n\t\t\t\t\t\t\t\tx={(e.from.x + e.to.x) / 2}\n\t\t\t\t\t\t\t\ty={(e.from.y + e.to.y) / 2 - 4}\n\t\t\t\t\t\t\t\ttextAnchor=\"middle\"\n\t\t\t\t\t\t\t\tfontSize={10}\n\t\t\t\t\t\t\t\tfill=\"hsl(var(--muted-foreground))\"\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\tpaintOrder: \"stroke\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tstroke=\"hsl(var(--background))\"\n\t\t\t\t\t\t\t\tstrokeWidth={3}\n\t\t\t\t\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{e.edge.label}\n\t\t\t\t\t\t\t</text>\n\t\t\t\t\t\t) : null}\n\t\t\t\t\t</g>\n\t\t\t\t))}\n\t\t\t</g>\n\t\t\t<g data-hex-flowchart-nodes>\n\t\t\t\t{laidOutNodes.map((n) => {\n\t\t\t\t\tconst shape = n.node.shape ?? \"rect\";\n\t\t\t\t\tconst interactive = Boolean(onNodeClick);\n\t\t\t\t\tconst handleActivate = () => onNodeClick?.(n.node);\n\t\t\t\t\tconst truncated = truncate(n.node.label, Math.floor((nodeWidth - 16) / 7));\n\t\t\t\t\tconst isTruncated = truncated !== n.node.label;\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<g\n\t\t\t\t\t\t\tkey={n.node.id}\n\t\t\t\t\t\t\tdata-hex-flowchart-node\n\t\t\t\t\t\t\tdata-shape={shape}\n\t\t\t\t\t\t\tdata-rank={n.rank}\n\t\t\t\t\t\t\ttransform={`translate(${n.x - nodeWidth / 2},${n.y - nodeHeight / 2})`}\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.node.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{/* Native SVG tooltip for the full label — recovers any text\n\t\t\t\t\t\t\t truncated by the heuristic without relying on font metrics. */}\n\t\t\t\t\t\t\t{isTruncated ? <title>{n.node.label}</title> : null}\n\t\t\t\t\t\t\t{shape === \"diamond\" ? (\n\t\t\t\t\t\t\t\t<polygon\n\t\t\t\t\t\t\t\t\tpoints={`${nodeWidth / 2},0 ${nodeWidth},${nodeHeight / 2} ${nodeWidth / 2},${nodeHeight} 0,${nodeHeight / 2}`}\n\t\t\t\t\t\t\t\t\tfill=\"hsl(var(--card))\"\n\t\t\t\t\t\t\t\t\tstroke=\"hsl(var(--border))\"\n\t\t\t\t\t\t\t\t\tstrokeWidth={1}\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<rect\n\t\t\t\t\t\t\t\t\twidth={nodeWidth}\n\t\t\t\t\t\t\t\t\theight={nodeHeight}\n\t\t\t\t\t\t\t\t\trx={shape === \"round\" ? nodeHeight / 2 : 6}\n\t\t\t\t\t\t\t\t\try={shape === \"round\" ? nodeHeight / 2 : 6}\n\t\t\t\t\t\t\t\t\tfill=\"hsl(var(--card))\"\n\t\t\t\t\t\t\t\t\tstroke=\"hsl(var(--border))\"\n\t\t\t\t\t\t\t\t\tstrokeWidth={1}\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<text\n\t\t\t\t\t\t\t\tx={nodeWidth / 2}\n\t\t\t\t\t\t\t\ty={nodeHeight / 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={500}\n\t\t\t\t\t\t\t\tfill=\"hsl(var(--foreground))\"\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{truncated}\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\tnodes: FlowchartNode[],\n\tedges: FlowchartEdge[],\n\tdirection: \"vertical\" | \"horizontal\",\n\twidth: number,\n\theight: number,\n\tnodeWidth: number,\n\tnodeHeight: number,\n): { nodes: LaidOutNode[]; edges: LaidOutEdge[] } {\n\tif (nodes.length === 0) return { nodes: [], edges: [] };\n\n\tconst byId = new Map(nodes.map((n) => [n.id, n]));\n\tconst ranks = computeRanks(nodes, edges, byId);\n\tconst maxRank = Math.max(...ranks.values(), 0);\n\n\t// Group node ids by rank.\n\tconst rankGroups = new Map<number, string[]>();\n\tfor (const n of nodes) {\n\t\tconst r = ranks.get(n.id) ?? 0;\n\t\tconst arr = rankGroups.get(r) ?? [];\n\t\tarr.push(n.id);\n\t\trankGroups.set(r, arr);\n\t}\n\n\t// Single-pass barycenter sweep: at each rank > 0, sort the rank's nodes\n\t// by the mean cross-axis index of their parents at the previous rank. Cuts\n\t// most edge crossings without pulling in dagre / elkjs. The first rank\n\t// keeps insertion order — that anchors the rest of the sweep.\n\tconst parentsOf = new Map<string, string[]>();\n\tfor (const n of nodes) parentsOf.set(n.id, []);\n\tfor (const e of edges) {\n\t\tconst arr = parentsOf.get(e.target);\n\t\tif (arr && byId.has(e.source)) arr.push(e.source);\n\t}\n\tconst orderInRank = new Map<string, number>();\n\tfor (let r = 0; r <= maxRank; r++) {\n\t\tconst group = rankGroups.get(r) ?? [];\n\t\tif (r > 0) {\n\t\t\tgroup.sort((a, b) => meanParentIndex(a, parentsOf, orderInRank) - meanParentIndex(b, parentsOf, orderInRank));\n\t\t}\n\t\tgroup.forEach((id, i) => orderInRank.set(id, i));\n\t}\n\n\tconst positions = new Map<string, { x: number; y: number; rank: number }>();\n\tconst margin = 32;\n\tconst usableMain = (direction === \"vertical\" ? height : width) - margin * 2;\n\tconst usableCross = (direction === \"vertical\" ? width : height) - margin * 2;\n\tconst rankCount = maxRank + 1;\n\tconst mainStep = rankCount > 1 ? usableMain / (rankCount - 1) : 0;\n\n\tfor (let r = 0; r <= maxRank; r++) {\n\t\tconst group = rankGroups.get(r) ?? [];\n\t\tconst crossStep = group.length > 0 ? usableCross / (group.length + 1) : 0;\n\t\tgroup.forEach((id, i) => {\n\t\t\tconst cross = margin + crossStep * (i + 1);\n\t\t\tconst main = margin + mainStep * r;\n\t\t\tif (direction === \"vertical\") {\n\t\t\t\tpositions.set(id, { x: cross, y: main, rank: r });\n\t\t\t} else {\n\t\t\t\tpositions.set(id, { x: main, y: cross, rank: r });\n\t\t\t}\n\t\t});\n\t}\n\n\tconst laidOutNodes: LaidOutNode[] = nodes.map((n) => {\n\t\tconst p = positions.get(n.id) ?? { x: 0, y: 0, rank: 0 };\n\t\treturn { node: n, x: p.x, y: p.y, rank: p.rank };\n\t});\n\n\tconst laidOutEdges: LaidOutEdge[] = edges\n\t\t.map((edge) => {\n\t\t\tconst sp = positions.get(edge.source);\n\t\t\tconst tp = positions.get(edge.target);\n\t\t\tif (!sp || !tp) return null;\n\t\t\t// Connect from the appropriate edge of the source to the appropriate edge of the target\n\t\t\tconst from =\n\t\t\t\tdirection === \"vertical\"\n\t\t\t\t\t? { x: sp.x, y: sp.y + nodeHeight / 2 }\n\t\t\t\t\t: { x: sp.x + nodeWidth / 2, y: sp.y };\n\t\t\tconst to =\n\t\t\t\tdirection === \"vertical\"\n\t\t\t\t\t? { x: tp.x, y: tp.y - nodeHeight / 2 }\n\t\t\t\t\t: { x: tp.x - nodeWidth / 2, y: tp.y };\n\t\t\treturn { edge, from, to };\n\t\t})\n\t\t.filter((e): e is LaidOutEdge => e !== null);\n\n\treturn { nodes: laidOutNodes, edges: laidOutEdges };\n}\n\nfunction meanParentIndex(\n\tid: string,\n\tparentsOf: Map<string, string[]>,\n\torderInRank: Map<string, number>,\n): number {\n\tconst parents = parentsOf.get(id) ?? [];\n\tif (parents.length === 0) return Number.POSITIVE_INFINITY; // sort orphans last\n\tlet sum = 0;\n\tlet count = 0;\n\tfor (const p of parents) {\n\t\tconst idx = orderInRank.get(p);\n\t\tif (idx != null) {\n\t\t\tsum += idx;\n\t\t\tcount++;\n\t\t}\n\t}\n\treturn count === 0 ? Number.POSITIVE_INFINITY : sum / count;\n}\n\n/**\n * Compute the rank (0-indexed depth) of each node via topological longest-path.\n * Honors any explicit `rank` overrides on the input nodes.\n *\n * Cycles: nodes inside a cycle resolve to 0. Crucially the cycle-touched\n * value is NOT memoized — the previous version cached the placeholder under\n * the first node along the cycle and poisoned every later descendant of that\n * node, even paths that didn't touch the cycle. The schema's `commonMistakes`\n * still warns DAG-only, but the failure mode is now contained to the\n * actually-cyclic nodes rather than spreading downstream.\n */\nfunction computeRanks(\n\tnodes: FlowchartNode[],\n\tedges: FlowchartEdge[],\n\tbyId: Map<string, FlowchartNode>,\n): Map<string, number> {\n\tconst ranks = new Map<string, number>();\n\tconst incoming = new Map<string, string[]>();\n\tfor (const n of nodes) incoming.set(n.id, []);\n\tfor (const e of edges) {\n\t\tconst arr = incoming.get(e.target);\n\t\tif (arr && byId.has(e.source)) arr.push(e.source);\n\t}\n\tconst memo = new Map<string, number>();\n\tconst visiting = new Set<string>();\n\n\tconst rankOf = (id: string): number => {\n\t\tconst cached = memo.get(id);\n\t\tif (cached !== undefined) return cached;\n\t\tif (visiting.has(id)) return 0; // cycle short-circuit (NOT memoized)\n\t\tvisiting.add(id);\n\t\ttry {\n\t\t\tconst node = byId.get(id);\n\t\t\tif (node?.rank != null) {\n\t\t\t\tmemo.set(id, node.rank);\n\t\t\t\treturn node.rank;\n\t\t\t}\n\t\t\tconst parents = incoming.get(id) ?? [];\n\t\t\tconst r = parents.length === 0 ? 0 : 1 + Math.max(...parents.map((p) => rankOf(p)));\n\t\t\tmemo.set(id, r);\n\t\t\treturn r;\n\t\t} finally {\n\t\t\tvisiting.delete(id);\n\t\t}\n\t};\n\n\tfor (const n of nodes) ranks.set(n.id, rankOf(n.id));\n\treturn ranks;\n}\n\nfunction edgePath(\n\tfrom: { x: number; y: number },\n\tto: { x: number; y: number },\n\tdirection: \"vertical\" | \"horizontal\",\n): string {\n\tif (direction === \"vertical\") {\n\t\tconst my = (from.y + to.y) / 2;\n\t\treturn `M${from.x},${from.y} C${from.x},${my} ${to.x},${my} ${to.x},${to.y}`;\n\t}\n\tconst mx = (from.x + to.x) / 2;\n\treturn `M${from.x},${from.y} C${mx},${from.y} ${mx},${to.y} ${to.x},${to.y}`;\n}\n\nfunction truncate(s: string, max: number): string {\n\tif (s.length <= max) return s;\n\treturn `${s.slice(0, Math.max(1, max - 1))}…`;\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 { Flowchart };\n"]}
|
package/dist/funnel.d.ts
ADDED
package/dist/funnel.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { clsx } from 'clsx';
|
|
3
|
+
import { twMerge } from 'tailwind-merge';
|
|
4
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
// src/lib/chart-palette.ts
|
|
7
|
+
var CHART_PALETTE = [
|
|
8
|
+
"hsl(var(--chart-1, var(--primary)))",
|
|
9
|
+
"hsl(var(--chart-2, var(--primary)))",
|
|
10
|
+
"hsl(var(--chart-3, var(--primary)))",
|
|
11
|
+
"hsl(var(--chart-4, var(--primary)))",
|
|
12
|
+
"hsl(var(--chart-5, var(--primary)))",
|
|
13
|
+
"hsl(var(--chart-6, var(--primary)))"
|
|
14
|
+
];
|
|
15
|
+
function pickChartHue(index) {
|
|
16
|
+
const safe = (index % CHART_PALETTE.length + CHART_PALETTE.length) % CHART_PALETTE.length;
|
|
17
|
+
return CHART_PALETTE[safe];
|
|
18
|
+
}
|
|
19
|
+
function cn(...inputs) {
|
|
20
|
+
return twMerge(clsx(inputs));
|
|
21
|
+
}
|
|
22
|
+
function Funnel({
|
|
23
|
+
stages,
|
|
24
|
+
width = 480,
|
|
25
|
+
height = 360,
|
|
26
|
+
gap = 4,
|
|
27
|
+
showConversion = true,
|
|
28
|
+
onStageClick,
|
|
29
|
+
className,
|
|
30
|
+
...rest
|
|
31
|
+
}) {
|
|
32
|
+
const laidOut = layout(stages, width, height, gap);
|
|
33
|
+
const desc = `Funnel with ${stages.length} stage${stages.length === 1 ? "" : "s"}, peak value ${stages[0]?.value ?? 0}`;
|
|
34
|
+
return /* @__PURE__ */ jsxs(
|
|
35
|
+
"svg",
|
|
36
|
+
{
|
|
37
|
+
...rest,
|
|
38
|
+
"data-hex-funnel": true,
|
|
39
|
+
role: "img",
|
|
40
|
+
width,
|
|
41
|
+
height,
|
|
42
|
+
viewBox: `0 0 ${width} ${height}`,
|
|
43
|
+
className: cn("block", className),
|
|
44
|
+
children: [
|
|
45
|
+
/* @__PURE__ */ jsx("title", { children: "Funnel chart" }),
|
|
46
|
+
/* @__PURE__ */ jsx("desc", { children: desc }),
|
|
47
|
+
/* @__PURE__ */ jsx("g", { "data-hex-funnel-stages": true, children: laidOut.map((s) => {
|
|
48
|
+
const interactive = Boolean(onStageClick);
|
|
49
|
+
const handleActivate = () => onStageClick?.(s.stage);
|
|
50
|
+
return /* @__PURE__ */ jsxs(
|
|
51
|
+
"g",
|
|
52
|
+
{
|
|
53
|
+
"data-hex-funnel-stage": true,
|
|
54
|
+
"data-depth": s.depth,
|
|
55
|
+
role: interactive ? "button" : void 0,
|
|
56
|
+
tabIndex: interactive ? 0 : void 0,
|
|
57
|
+
"aria-label": interactive ? `${s.stage.label}: ${s.stage.value}` : void 0,
|
|
58
|
+
style: interactive ? { cursor: "pointer" } : void 0,
|
|
59
|
+
onClick: interactive ? handleActivate : void 0,
|
|
60
|
+
onKeyDown: interactive ? (e) => activateOnKey(e, handleActivate) : void 0,
|
|
61
|
+
children: [
|
|
62
|
+
/* @__PURE__ */ jsx(
|
|
63
|
+
"polygon",
|
|
64
|
+
{
|
|
65
|
+
points: `${s.topLeft},${s.yTop} ${s.topRight},${s.yTop} ${s.bottomRight},${s.yBottom} ${s.bottomLeft},${s.yBottom}`,
|
|
66
|
+
fill: pickChartHue(s.depth),
|
|
67
|
+
fillOpacity: 0.85,
|
|
68
|
+
stroke: "hsl(var(--background))",
|
|
69
|
+
strokeWidth: 1
|
|
70
|
+
}
|
|
71
|
+
),
|
|
72
|
+
/* @__PURE__ */ jsx(
|
|
73
|
+
"text",
|
|
74
|
+
{
|
|
75
|
+
x: width / 2,
|
|
76
|
+
y: (s.yTop + s.yBottom) / 2,
|
|
77
|
+
dy: "0.35em",
|
|
78
|
+
textAnchor: "middle",
|
|
79
|
+
fontSize: 12,
|
|
80
|
+
fontWeight: 600,
|
|
81
|
+
fill: "hsl(var(--background))",
|
|
82
|
+
style: {
|
|
83
|
+
pointerEvents: "none",
|
|
84
|
+
paintOrder: "stroke",
|
|
85
|
+
stroke: "hsl(var(--foreground) / 0.45)",
|
|
86
|
+
strokeWidth: 2
|
|
87
|
+
},
|
|
88
|
+
children: `${s.stage.label} \xB7 ${s.stage.value.toLocaleString()}`
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
]
|
|
92
|
+
},
|
|
93
|
+
s.stage.id
|
|
94
|
+
);
|
|
95
|
+
}) }),
|
|
96
|
+
showConversion ? /* @__PURE__ */ jsx("g", { "data-hex-funnel-conversions": true, children: laidOut.map(
|
|
97
|
+
(s) => s.conversionFromPrev != null ? /* @__PURE__ */ jsx(
|
|
98
|
+
"text",
|
|
99
|
+
{
|
|
100
|
+
"data-hex-funnel-conversion": true,
|
|
101
|
+
x: width - 4,
|
|
102
|
+
y: s.yTop - 2,
|
|
103
|
+
textAnchor: "end",
|
|
104
|
+
fontSize: 10,
|
|
105
|
+
fill: "hsl(var(--muted-foreground))",
|
|
106
|
+
children: formatPct(s.conversionFromPrev)
|
|
107
|
+
},
|
|
108
|
+
`conv-${s.stage.id}`
|
|
109
|
+
) : null
|
|
110
|
+
) }) : null
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
function layout(stages, width, height, gap) {
|
|
116
|
+
if (stages.length === 0) return [];
|
|
117
|
+
const peak = Math.max(...stages.map((s) => s.value), 0) || 1;
|
|
118
|
+
const usableHeight = height - gap * Math.max(0, stages.length - 1);
|
|
119
|
+
const stageHeight = usableHeight / stages.length;
|
|
120
|
+
const cx = width / 2;
|
|
121
|
+
return stages.map((stage, i) => {
|
|
122
|
+
const yTop = i * (stageHeight + gap);
|
|
123
|
+
const yBottom = yTop + stageHeight;
|
|
124
|
+
const topRatio = stage.value / peak;
|
|
125
|
+
const next = stages[i + 1];
|
|
126
|
+
const bottomRatio = next ? next.value / peak : topRatio;
|
|
127
|
+
const topHalfWidth = width / 2 * Math.max(0, Math.min(1, topRatio));
|
|
128
|
+
const bottomHalfWidth = width / 2 * Math.max(0, Math.min(1, bottomRatio));
|
|
129
|
+
const prev = stages[i - 1];
|
|
130
|
+
const conversionFromPrev = prev && prev.value > 0 ? stage.value / prev.value : null;
|
|
131
|
+
return {
|
|
132
|
+
stage,
|
|
133
|
+
depth: i,
|
|
134
|
+
topLeft: cx - topHalfWidth,
|
|
135
|
+
topRight: cx + topHalfWidth,
|
|
136
|
+
bottomLeft: cx - bottomHalfWidth,
|
|
137
|
+
bottomRight: cx + bottomHalfWidth,
|
|
138
|
+
yTop,
|
|
139
|
+
yBottom,
|
|
140
|
+
conversionFromPrev
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
function formatPct(ratio) {
|
|
145
|
+
if (!Number.isFinite(ratio)) return "\u2014";
|
|
146
|
+
return `${(ratio * 100).toFixed(ratio < 0.1 ? 1 : 0)}%`;
|
|
147
|
+
}
|
|
148
|
+
function activateOnKey(e, fn) {
|
|
149
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
fn();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export { Funnel };
|
|
156
|
+
//# sourceMappingURL=funnel.js.map
|
|
157
|
+
//# sourceMappingURL=funnel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/chart-palette.ts","../src/lib/utils.ts","../src/artifacts/funnel/funnel.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;AC8CA,SAAS,MAAA,CAAO;AAAA,EACf,MAAA;AAAA,EACA,KAAA,GAAQ,GAAA;AAAA,EACR,MAAA,GAAS,GAAA;AAAA,EACT,GAAA,GAAM,CAAA;AAAA,EACN,cAAA,GAAiB,IAAA;AAAA,EACjB,YAAA;AAAA,EACA,SAAA;AAAA,EACA,GAAG;AACJ,CAAA,EAAgB;AACf,EAAA,MAAM,OAAA,GAAU,MAAA,CAAO,MAAA,EAAQ,KAAA,EAAO,QAAQ,GAAG,CAAA;AACjD,EAAA,MAAM,IAAA,GAAO,CAAA,YAAA,EAAe,MAAA,CAAO,MAAM,SAAS,MAAA,CAAO,MAAA,KAAW,CAAA,GAAI,EAAA,GAAK,GAAG,CAAA,aAAA,EAAgB,MAAA,CAAO,CAAC,CAAA,EAAG,SAAS,CAAC,CAAA,CAAA;AAErH,EAAA,uBACC,IAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,GAAG,IAAA;AAAA,MACJ,iBAAA,EAAe,IAAA;AAAA,MACf,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,cAAA,EAAY,CAAA;AAAA,wBACnB,GAAA,CAAC,UAAM,QAAA,EAAA,IAAA,EAAK,CAAA;AAAA,4BACX,GAAA,EAAA,EAAE,wBAAA,EAAsB,MACvB,QAAA,EAAA,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAA,KAAM;AACnB,UAAA,MAAM,WAAA,GAAc,QAAQ,YAAY,CAAA;AACxC,UAAA,MAAM,cAAA,GAAiB,MAAM,YAAA,GAAe,CAAA,CAAE,KAAK,CAAA;AACnD,UAAA,uBACA,IAAA;AAAA,YAAC,GAAA;AAAA,YAAA;AAAA,cAEA,uBAAA,EAAqB,IAAA;AAAA,cACrB,cAAY,CAAA,CAAE,KAAA;AAAA,cACd,IAAA,EAAM,cAAc,QAAA,GAAW,MAAA;AAAA,cAC/B,QAAA,EAAU,cAAc,CAAA,GAAI,MAAA;AAAA,cAC5B,YAAA,EAAY,WAAA,GAAc,CAAA,EAAG,CAAA,CAAE,KAAA,CAAM,KAAK,CAAA,EAAA,EAAK,CAAA,CAAE,KAAA,CAAM,KAAK,CAAA,CAAA,GAAK,MAAA;AAAA,cACjE,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;AAAA,kBAAC,SAAA;AAAA,kBAAA;AAAA,oBACA,MAAA,EAAQ,CAAA,EAAG,CAAA,CAAE,OAAO,CAAA,CAAA,EAAI,EAAE,IAAI,CAAA,CAAA,EAAI,CAAA,CAAE,QAAQ,CAAA,CAAA,EAAI,CAAA,CAAE,IAAI,CAAA,CAAA,EAAI,CAAA,CAAE,WAAW,CAAA,CAAA,EAAI,CAAA,CAAE,OAAO,IAAI,CAAA,CAAE,UAAU,CAAA,CAAA,EAAI,CAAA,CAAE,OAAO,CAAA,CAAA;AAAA,oBACjH,IAAA,EAAM,YAAA,CAAa,CAAA,CAAE,KAAK,CAAA;AAAA,oBAC1B,WAAA,EAAa,IAAA;AAAA,oBACb,MAAA,EAAO,wBAAA;AAAA,oBACP,WAAA,EAAa;AAAA;AAAA,iBACd;AAAA,gCACA,GAAA;AAAA,kBAAC,MAAA;AAAA,kBAAA;AAAA,oBAOA,GAAG,KAAA,GAAQ,CAAA;AAAA,oBACX,CAAA,EAAA,CAAI,CAAA,CAAE,IAAA,GAAO,CAAA,CAAE,OAAA,IAAW,CAAA;AAAA,oBAC1B,EAAA,EAAG,QAAA;AAAA,oBACH,UAAA,EAAW,QAAA;AAAA,oBACX,QAAA,EAAU,EAAA;AAAA,oBACV,UAAA,EAAY,GAAA;AAAA,oBACZ,IAAA,EAAK,wBAAA;AAAA,oBACL,KAAA,EAAO;AAAA,sBACN,aAAA,EAAe,MAAA;AAAA,sBACf,UAAA,EAAY,QAAA;AAAA,sBACZ,MAAA,EAAQ,+BAAA;AAAA,sBACR,WAAA,EAAa;AAAA,qBACd;AAAA,oBAEC,QAAA,EAAA,CAAA,EAAG,EAAE,KAAA,CAAM,KAAK,SAAM,CAAA,CAAE,KAAA,CAAM,KAAA,CAAM,cAAA,EAAgB,CAAA;AAAA;AAAA;AACtD;AAAA,aAAA;AAAA,YAvCK,EAAE,KAAA,CAAM;AAAA,WAwCd;AAAA,QAED,CAAC,CAAA,EACF,CAAA;AAAA,QACC,cAAA,mBACA,GAAA,CAAC,GAAA,EAAA,EAAE,6BAAA,EAA2B,MAC5B,QAAA,EAAA,OAAA,CAAQ,GAAA;AAAA,UAAI,CAAC,CAAA,KACb,CAAA,CAAE,kBAAA,IAAsB,IAAA,mBACvB,GAAA;AAAA,YAAC,MAAA;AAAA,YAAA;AAAA,cAEA,4BAAA,EAA0B,IAAA;AAAA,cAC1B,GAAG,KAAA,GAAQ,CAAA;AAAA,cACX,CAAA,EAAG,EAAE,IAAA,GAAO,CAAA;AAAA,cACZ,UAAA,EAAW,KAAA;AAAA,cACX,QAAA,EAAU,EAAA;AAAA,cACV,IAAA,EAAK,8BAAA;AAAA,cAEJ,QAAA,EAAA,SAAA,CAAU,EAAE,kBAAkB;AAAA,aAAA;AAAA,YAR1B,CAAA,KAAA,EAAQ,CAAA,CAAE,KAAA,CAAM,EAAE,CAAA;AAAA,WASxB,GACG;AAAA,WAEN,CAAA,GACG;AAAA;AAAA;AAAA,GACL;AAEF;AAEA,SAAS,MAAA,CAAO,MAAA,EAAuB,KAAA,EAAe,MAAA,EAAgB,GAAA,EAA6B;AAClG,EAAA,IAAI,MAAA,CAAO,MAAA,KAAW,CAAA,EAAG,OAAO,EAAC;AACjC,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,GAAA,CAAI,GAAG,MAAA,CAAO,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,KAAK,CAAA,EAAG,CAAC,CAAA,IAAK,CAAA;AAC3D,EAAA,MAAM,YAAA,GAAe,SAAS,GAAA,GAAM,IAAA,CAAK,IAAI,CAAA,EAAG,MAAA,CAAO,SAAS,CAAC,CAAA;AACjE,EAAA,MAAM,WAAA,GAAc,eAAe,MAAA,CAAO,MAAA;AAC1C,EAAA,MAAM,KAAK,KAAA,GAAQ,CAAA;AAEnB,EAAA,OAAO,MAAA,CAAO,GAAA,CAAI,CAAC,KAAA,EAAO,CAAA,KAAM;AAC/B,IAAA,MAAM,IAAA,GAAO,KAAK,WAAA,GAAc,GAAA,CAAA;AAChC,IAAA,MAAM,UAAU,IAAA,GAAO,WAAA;AACvB,IAAA,MAAM,QAAA,GAAW,MAAM,KAAA,GAAQ,IAAA;AAC/B,IAAA,MAAM,IAAA,GAAO,MAAA,CAAO,CAAA,GAAI,CAAC,CAAA;AACzB,IAAA,MAAM,WAAA,GAAc,IAAA,GAAO,IAAA,CAAK,KAAA,GAAQ,IAAA,GAAO,QAAA;AAC/C,IAAA,MAAM,YAAA,GAAgB,KAAA,GAAQ,CAAA,GAAK,IAAA,CAAK,GAAA,CAAI,GAAG,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,QAAQ,CAAC,CAAA;AACpE,IAAA,MAAM,eAAA,GAAmB,KAAA,GAAQ,CAAA,GAAK,IAAA,CAAK,GAAA,CAAI,GAAG,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,WAAW,CAAC,CAAA;AAC1E,IAAA,MAAM,IAAA,GAAO,MAAA,CAAO,CAAA,GAAI,CAAC,CAAA;AACzB,IAAA,MAAM,kBAAA,GAAqB,QAAQ,IAAA,CAAK,KAAA,GAAQ,IAAI,KAAA,CAAM,KAAA,GAAQ,KAAK,KAAA,GAAQ,IAAA;AAC/E,IAAA,OAAO;AAAA,MACN,KAAA;AAAA,MACA,KAAA,EAAO,CAAA;AAAA,MACP,SAAS,EAAA,GAAK,YAAA;AAAA,MACd,UAAU,EAAA,GAAK,YAAA;AAAA,MACf,YAAY,EAAA,GAAK,eAAA;AAAA,MACjB,aAAa,EAAA,GAAK,eAAA;AAAA,MAClB,IAAA;AAAA,MACA,OAAA;AAAA,MACA;AAAA,KACD;AAAA,EACD,CAAC,CAAA;AACF;AAEA,SAAS,UAAU,KAAA,EAAuB;AACzC,EAAA,IAAI,CAAC,MAAA,CAAO,QAAA,CAAS,KAAK,GAAG,OAAO,QAAA;AACpC,EAAA,OAAO,CAAA,EAAA,CAAI,QAAQ,GAAA,EAAK,OAAA,CAAQ,QAAQ,GAAA,GAAM,CAAA,GAAI,CAAC,CAAC,CAAA,CAAA,CAAA;AACrD;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":"funnel.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 * Conversion funnel — a vertical stack of trapezoidal stages whose width\n * is proportional to each stage's value. Pure SVG; no heavy peer\n * dependency. Pair with `<Sankey>` when stage-to-stage flow detail\n * matters; use Funnel when the conversion drop-off itself is the\n * message.\n *\n * @example\n * <Funnel\n * stages={[\n * { id: \"visit\", label: \"Visited\", value: 10000 },\n * { id: \"signup\", label: \"Signed up\", value: 1200 },\n * { id: \"active\", label: \"Active\", value: 480 },\n * { id: \"paid\", label: \"Paid\", value: 95 },\n * ]}\n * />\n */\nexport type FunnelStage = {\n\tid: string;\n\tlabel: string;\n\tvalue: number;\n};\n\nexport interface FunnelProps extends Omit<React.SVGAttributes<SVGSVGElement>, \"children\"> {\n\t/** Ordered top-to-bottom stages. Values must be non-negative. */\n\tstages: FunnelStage[];\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 stages. Default 4. */\n\tgap?: number;\n\t/** Show conversion-rate annotations between stages. Default true. */\n\tshowConversion?: boolean;\n\t/** Fired when a stage is clicked. */\n\tonStageClick?: (stage: FunnelStage) => void;\n}\n\ninterface LaidOutStage {\n\tstage: FunnelStage;\n\tdepth: number;\n\ttopLeft: number;\n\ttopRight: number;\n\tbottomLeft: number;\n\tbottomRight: number;\n\tyTop: number;\n\tyBottom: number;\n\tconversionFromPrev: number | null;\n}\n\nfunction Funnel({\n\tstages,\n\twidth = 480,\n\theight = 360,\n\tgap = 4,\n\tshowConversion = true,\n\tonStageClick,\n\tclassName,\n\t...rest\n}: FunnelProps) {\n\tconst laidOut = layout(stages, width, height, gap);\n\tconst desc = `Funnel with ${stages.length} stage${stages.length === 1 ? \"\" : \"s\"}, peak value ${stages[0]?.value ?? 0}`;\n\n\treturn (\n\t\t<svg\n\t\t\t{...rest}\n\t\t\tdata-hex-funnel\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>Funnel chart</title>\n\t\t\t<desc>{desc}</desc>\n\t\t\t<g data-hex-funnel-stages>\n\t\t\t\t{laidOut.map((s) => {\n\t\t\t\t\tconst interactive = Boolean(onStageClick);\n\t\t\t\t\tconst handleActivate = () => onStageClick?.(s.stage);\n\t\t\t\t\treturn (\n\t\t\t\t\t<g\n\t\t\t\t\t\tkey={s.stage.id}\n\t\t\t\t\t\tdata-hex-funnel-stage\n\t\t\t\t\t\tdata-depth={s.depth}\n\t\t\t\t\t\trole={interactive ? \"button\" : undefined}\n\t\t\t\t\t\ttabIndex={interactive ? 0 : undefined}\n\t\t\t\t\t\taria-label={interactive ? `${s.stage.label}: ${s.stage.value}` : undefined}\n\t\t\t\t\t\tstyle={interactive ? { cursor: \"pointer\" } : undefined}\n\t\t\t\t\t\tonClick={interactive ? handleActivate : undefined}\n\t\t\t\t\t\tonKeyDown={interactive ? (e) => activateOnKey(e, handleActivate) : undefined}\n\t\t\t\t\t>\n\t\t\t\t\t\t<polygon\n\t\t\t\t\t\t\tpoints={`${s.topLeft},${s.yTop} ${s.topRight},${s.yTop} ${s.bottomRight},${s.yBottom} ${s.bottomLeft},${s.yBottom}`}\n\t\t\t\t\t\t\tfill={pickChartHue(s.depth)}\n\t\t\t\t\t\t\tfillOpacity={0.85}\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/>\n\t\t\t\t\t\t<text\n\t\t\t\t\t\t\t// Page-bg fill + foreground-tinted stroke is the\n\t\t\t\t\t\t\t// classic \"outline label\" technique: text reads on\n\t\t\t\t\t\t\t// any chart-1..6 fill regardless of hue, and stays\n\t\t\t\t\t\t\t// readable when the trapezoid narrows below the\n\t\t\t\t\t\t\t// label's width (the label spans outside the\n\t\t\t\t\t\t\t// polygon onto the page bg).\n\t\t\t\t\t\t\tx={width / 2}\n\t\t\t\t\t\t\ty={(s.yTop + s.yBottom) / 2}\n\t\t\t\t\t\t\tdy=\"0.35em\"\n\t\t\t\t\t\t\ttextAnchor=\"middle\"\n\t\t\t\t\t\t\tfontSize={12}\n\t\t\t\t\t\t\tfontWeight={600}\n\t\t\t\t\t\t\tfill=\"hsl(var(--background))\"\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\tpointerEvents: \"none\",\n\t\t\t\t\t\t\t\tpaintOrder: \"stroke\",\n\t\t\t\t\t\t\t\tstroke: \"hsl(var(--foreground) / 0.45)\",\n\t\t\t\t\t\t\t\tstrokeWidth: 2,\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{`${s.stage.label} · ${s.stage.value.toLocaleString()}`}\n\t\t\t\t\t\t</text>\n\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\t{showConversion ? (\n\t\t\t\t<g data-hex-funnel-conversions>\n\t\t\t\t\t{laidOut.map((s) =>\n\t\t\t\t\t\ts.conversionFromPrev != null ? (\n\t\t\t\t\t\t\t<text\n\t\t\t\t\t\t\t\tkey={`conv-${s.stage.id}`}\n\t\t\t\t\t\t\t\tdata-hex-funnel-conversion\n\t\t\t\t\t\t\t\tx={width - 4}\n\t\t\t\t\t\t\t\ty={s.yTop - 2}\n\t\t\t\t\t\t\t\ttextAnchor=\"end\"\n\t\t\t\t\t\t\t\tfontSize={10}\n\t\t\t\t\t\t\t\tfill=\"hsl(var(--muted-foreground))\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{formatPct(s.conversionFromPrev)}\n\t\t\t\t\t\t\t</text>\n\t\t\t\t\t\t) : null,\n\t\t\t\t\t)}\n\t\t\t\t</g>\n\t\t\t) : null}\n\t\t</svg>\n\t);\n}\n\nfunction layout(stages: FunnelStage[], width: number, height: number, gap: number): LaidOutStage[] {\n\tif (stages.length === 0) return [];\n\tconst peak = Math.max(...stages.map((s) => s.value), 0) || 1;\n\tconst usableHeight = height - gap * Math.max(0, stages.length - 1);\n\tconst stageHeight = usableHeight / stages.length;\n\tconst cx = width / 2;\n\n\treturn stages.map((stage, i) => {\n\t\tconst yTop = i * (stageHeight + gap);\n\t\tconst yBottom = yTop + stageHeight;\n\t\tconst topRatio = stage.value / peak;\n\t\tconst next = stages[i + 1];\n\t\tconst bottomRatio = next ? next.value / peak : topRatio;\n\t\tconst topHalfWidth = (width / 2) * Math.max(0, Math.min(1, topRatio));\n\t\tconst bottomHalfWidth = (width / 2) * Math.max(0, Math.min(1, bottomRatio));\n\t\tconst prev = stages[i - 1];\n\t\tconst conversionFromPrev = prev && prev.value > 0 ? stage.value / prev.value : null;\n\t\treturn {\n\t\t\tstage,\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\tconversionFromPrev,\n\t\t};\n\t});\n}\n\nfunction formatPct(ratio: number): string {\n\tif (!Number.isFinite(ratio)) return \"—\";\n\treturn `${(ratio * 100).toFixed(ratio < 0.1 ? 1 : 0)}%`;\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 { Funnel };\n"]}
|