@hex-core/components 1.7.0 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_tsup-dts-rollup.d.ts +1566 -5
- 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/color-picker.js.map +1 -1
- 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/data-table.js.map +1 -1
- 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 +2210 -3
- 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/sonner.js.map +1 -1
- 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/textarea.js.map +1 -1
- 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 +49 -5
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":"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","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"]}
|
package/dist/gantt.d.ts
ADDED
package/dist/gantt.js
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { clsx } from 'clsx';
|
|
4
|
+
import { twMerge } from 'tailwind-merge';
|
|
5
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
6
|
+
|
|
7
|
+
function cn(...inputs) {
|
|
8
|
+
return twMerge(clsx(inputs));
|
|
9
|
+
}
|
|
10
|
+
var HEADER_HEIGHT = 40;
|
|
11
|
+
var ROW_PADDING = 6;
|
|
12
|
+
function Gantt({
|
|
13
|
+
tasks,
|
|
14
|
+
width = 800,
|
|
15
|
+
rowHeight = 32,
|
|
16
|
+
labelMargin = 140,
|
|
17
|
+
tickCount = 6,
|
|
18
|
+
onTaskClick,
|
|
19
|
+
className,
|
|
20
|
+
...rest
|
|
21
|
+
}) {
|
|
22
|
+
const laidOut = React.useMemo(
|
|
23
|
+
() => layout(tasks, width, rowHeight, labelMargin, tickCount),
|
|
24
|
+
[tasks, width, rowHeight, labelMargin, tickCount]
|
|
25
|
+
);
|
|
26
|
+
const totalHeight = HEADER_HEIGHT + tasks.length * rowHeight + ROW_PADDING;
|
|
27
|
+
const desc = `Gantt with ${tasks.length} task${tasks.length === 1 ? "" : "s"}, range ${laidOut.startLabel} to ${laidOut.endLabel}`;
|
|
28
|
+
const arrowId = React.useId().replace(/:/g, "-");
|
|
29
|
+
return /* @__PURE__ */ jsxs(
|
|
30
|
+
"svg",
|
|
31
|
+
{
|
|
32
|
+
...rest,
|
|
33
|
+
"data-hex-gantt": true,
|
|
34
|
+
role: "img",
|
|
35
|
+
width,
|
|
36
|
+
height: totalHeight,
|
|
37
|
+
viewBox: `0 0 ${width} ${totalHeight}`,
|
|
38
|
+
className: cn("block", className),
|
|
39
|
+
children: [
|
|
40
|
+
/* @__PURE__ */ jsx("title", { children: "Gantt chart" }),
|
|
41
|
+
/* @__PURE__ */ jsx("desc", { children: desc }),
|
|
42
|
+
/* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx(
|
|
43
|
+
"marker",
|
|
44
|
+
{
|
|
45
|
+
id: `hex-gantt-arrow-${arrowId}`,
|
|
46
|
+
viewBox: "0 0 10 10",
|
|
47
|
+
refX: "10",
|
|
48
|
+
refY: "5",
|
|
49
|
+
markerWidth: "5",
|
|
50
|
+
markerHeight: "5",
|
|
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__ */ jsxs("g", { "data-hex-gantt-axis": true, children: [
|
|
56
|
+
/* @__PURE__ */ jsx(
|
|
57
|
+
"line",
|
|
58
|
+
{
|
|
59
|
+
x1: labelMargin,
|
|
60
|
+
x2: width - 16,
|
|
61
|
+
y1: HEADER_HEIGHT - 8,
|
|
62
|
+
y2: HEADER_HEIGHT - 8,
|
|
63
|
+
stroke: "hsl(var(--muted-foreground))",
|
|
64
|
+
strokeWidth: 1
|
|
65
|
+
}
|
|
66
|
+
),
|
|
67
|
+
laidOut.ticks.map((t, i) => /* @__PURE__ */ jsxs("g", { "data-hex-gantt-tick": true, children: [
|
|
68
|
+
/* @__PURE__ */ jsx(
|
|
69
|
+
"line",
|
|
70
|
+
{
|
|
71
|
+
x1: t.x,
|
|
72
|
+
x2: t.x,
|
|
73
|
+
y1: HEADER_HEIGHT - 8,
|
|
74
|
+
y2: totalHeight - 4,
|
|
75
|
+
stroke: "hsl(var(--muted-foreground))",
|
|
76
|
+
strokeOpacity: 0.15,
|
|
77
|
+
strokeWidth: 1
|
|
78
|
+
}
|
|
79
|
+
),
|
|
80
|
+
/* @__PURE__ */ jsx(
|
|
81
|
+
"text",
|
|
82
|
+
{
|
|
83
|
+
x: t.x,
|
|
84
|
+
y: HEADER_HEIGHT - 14,
|
|
85
|
+
textAnchor: "middle",
|
|
86
|
+
fontSize: 10,
|
|
87
|
+
fill: "hsl(var(--muted-foreground))",
|
|
88
|
+
children: t.label
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
] }, `tick-${i}`))
|
|
92
|
+
] }),
|
|
93
|
+
/* @__PURE__ */ jsx("g", { "data-hex-gantt-labels": true, children: laidOut.tasks.map((t) => /* @__PURE__ */ jsx(
|
|
94
|
+
"text",
|
|
95
|
+
{
|
|
96
|
+
x: labelMargin - 8,
|
|
97
|
+
y: t.y + t.h / 2,
|
|
98
|
+
dy: "0.35em",
|
|
99
|
+
textAnchor: "end",
|
|
100
|
+
fontSize: 11,
|
|
101
|
+
fill: "hsl(var(--foreground))",
|
|
102
|
+
children: t.task.label
|
|
103
|
+
},
|
|
104
|
+
`label-${t.task.id}`
|
|
105
|
+
)) }),
|
|
106
|
+
/* @__PURE__ */ jsx("g", { "data-hex-gantt-deps": true, fill: "none", children: laidOut.deps.map((d) => {
|
|
107
|
+
const ELBOW = 8;
|
|
108
|
+
const needsJog = d.to.x < d.from.x + ELBOW;
|
|
109
|
+
const path = needsJog ? `M${d.from.x},${d.from.y} L${d.from.x + ELBOW},${d.from.y} L${d.from.x + ELBOW},${d.to.y} L${d.to.x},${d.to.y}` : `M${d.from.x},${d.from.y} L${(d.from.x + d.to.x) / 2},${d.from.y} L${(d.from.x + d.to.x) / 2},${d.to.y} L${d.to.x},${d.to.y}`;
|
|
110
|
+
return /* @__PURE__ */ jsx(
|
|
111
|
+
"path",
|
|
112
|
+
{
|
|
113
|
+
"data-hex-gantt-dep": true,
|
|
114
|
+
d: path,
|
|
115
|
+
stroke: "hsl(var(--muted-foreground))",
|
|
116
|
+
strokeOpacity: 0.55,
|
|
117
|
+
strokeWidth: 1,
|
|
118
|
+
markerEnd: `url(#hex-gantt-arrow-${arrowId})`
|
|
119
|
+
},
|
|
120
|
+
d.id
|
|
121
|
+
);
|
|
122
|
+
}) }),
|
|
123
|
+
/* @__PURE__ */ jsx("g", { "data-hex-gantt-tasks": true, children: laidOut.tasks.map((t) => {
|
|
124
|
+
const interactive = Boolean(onTaskClick);
|
|
125
|
+
const handleActivate = () => onTaskClick?.(t.task);
|
|
126
|
+
return /* @__PURE__ */ jsxs(
|
|
127
|
+
"g",
|
|
128
|
+
{
|
|
129
|
+
"data-hex-gantt-task": true,
|
|
130
|
+
"data-row": t.rowIndex,
|
|
131
|
+
role: interactive ? "button" : void 0,
|
|
132
|
+
tabIndex: interactive ? 0 : void 0,
|
|
133
|
+
"aria-label": interactive ? `${t.task.label}, ${formatDate(toDate(t.task.start))} to ${formatDate(toDate(t.task.end))}${t.task.progress != null ? `, ${Math.round((t.task.progress ?? 0) * 100)}% complete` : ""}` : void 0,
|
|
134
|
+
style: interactive ? { cursor: "pointer" } : void 0,
|
|
135
|
+
onClick: interactive ? handleActivate : void 0,
|
|
136
|
+
onKeyDown: interactive ? (e) => activateOnKey(e, handleActivate) : void 0,
|
|
137
|
+
children: [
|
|
138
|
+
/* @__PURE__ */ jsx(
|
|
139
|
+
"rect",
|
|
140
|
+
{
|
|
141
|
+
x: t.x,
|
|
142
|
+
y: t.y,
|
|
143
|
+
width: Math.max(2, t.w),
|
|
144
|
+
height: t.h,
|
|
145
|
+
rx: 4,
|
|
146
|
+
ry: 4,
|
|
147
|
+
fill: "hsl(var(--primary))",
|
|
148
|
+
fillOpacity: 0.25,
|
|
149
|
+
stroke: "hsl(var(--primary))",
|
|
150
|
+
strokeWidth: 1
|
|
151
|
+
}
|
|
152
|
+
),
|
|
153
|
+
t.progressW > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
154
|
+
/* @__PURE__ */ jsx(
|
|
155
|
+
"rect",
|
|
156
|
+
{
|
|
157
|
+
x: t.x,
|
|
158
|
+
y: t.y,
|
|
159
|
+
width: t.progressW,
|
|
160
|
+
height: t.h,
|
|
161
|
+
rx: 4,
|
|
162
|
+
ry: 4,
|
|
163
|
+
fill: "hsl(var(--primary))",
|
|
164
|
+
fillOpacity: 0.7
|
|
165
|
+
}
|
|
166
|
+
),
|
|
167
|
+
t.progressW < t.w - 0.5 ? /* @__PURE__ */ jsx(
|
|
168
|
+
"line",
|
|
169
|
+
{
|
|
170
|
+
x1: t.x + t.progressW,
|
|
171
|
+
x2: t.x + t.progressW,
|
|
172
|
+
y1: t.y,
|
|
173
|
+
y2: t.y + t.h,
|
|
174
|
+
stroke: "hsl(var(--primary))",
|
|
175
|
+
strokeWidth: 1
|
|
176
|
+
}
|
|
177
|
+
) : null
|
|
178
|
+
] }) : null
|
|
179
|
+
]
|
|
180
|
+
},
|
|
181
|
+
t.task.id
|
|
182
|
+
);
|
|
183
|
+
}) })
|
|
184
|
+
]
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
function layout(tasks, width, rowHeight, labelMargin, tickCount) {
|
|
189
|
+
if (tasks.length === 0) {
|
|
190
|
+
return { tasks: [], deps: [], ticks: [], startLabel: "\u2014", endLabel: "\u2014" };
|
|
191
|
+
}
|
|
192
|
+
const axisLeft = labelMargin;
|
|
193
|
+
const axisRight = width - 16;
|
|
194
|
+
const usable = axisRight - axisLeft;
|
|
195
|
+
const allTimes = tasks.flatMap((t) => [toDate(t.start).getTime(), toDate(t.end).getTime()]).filter((t) => Number.isFinite(t));
|
|
196
|
+
if (allTimes.length === 0) {
|
|
197
|
+
return { tasks: [], deps: [], ticks: [], startLabel: "\u2014", endLabel: "\u2014" };
|
|
198
|
+
}
|
|
199
|
+
const minTs = Math.min(...allTimes);
|
|
200
|
+
const maxTs = Math.max(...allTimes);
|
|
201
|
+
const span = Math.max(1, maxTs - minTs);
|
|
202
|
+
const tToX = (t) => axisLeft + (t - minTs) / span * usable;
|
|
203
|
+
const laidOutTasks = tasks.map((task, i) => {
|
|
204
|
+
const startTs = toDate(task.start).getTime();
|
|
205
|
+
const endTs = toDate(task.end).getTime();
|
|
206
|
+
const x = tToX(startTs);
|
|
207
|
+
const w = Math.max(0, tToX(endTs) - x);
|
|
208
|
+
const y = HEADER_HEIGHT + i * rowHeight + ROW_PADDING / 2;
|
|
209
|
+
const h = rowHeight - ROW_PADDING;
|
|
210
|
+
const progress = clamp01(task.progress ?? 0);
|
|
211
|
+
return {
|
|
212
|
+
task,
|
|
213
|
+
rowIndex: i,
|
|
214
|
+
x,
|
|
215
|
+
y,
|
|
216
|
+
w,
|
|
217
|
+
h,
|
|
218
|
+
progressW: w * progress
|
|
219
|
+
};
|
|
220
|
+
});
|
|
221
|
+
const byId = new Map(laidOutTasks.map((t) => [t.task.id, t]));
|
|
222
|
+
const deps = [];
|
|
223
|
+
laidOutTasks.forEach((t) => {
|
|
224
|
+
(t.task.dependencies ?? []).forEach((depId, k) => {
|
|
225
|
+
const from = byId.get(depId);
|
|
226
|
+
if (!from) return;
|
|
227
|
+
deps.push({
|
|
228
|
+
from: { x: from.x + from.w, y: from.y + from.h / 2 },
|
|
229
|
+
to: { x: t.x, y: t.y + t.h / 2 },
|
|
230
|
+
id: `${depId}-${t.task.id}-${k}`
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
const rawTicks = [];
|
|
235
|
+
for (let i = 0; i < tickCount; i++) {
|
|
236
|
+
const ts = minTs + i / Math.max(1, tickCount - 1) * span;
|
|
237
|
+
rawTicks.push({ x: tToX(ts), label: formatTick(new Date(ts), span) });
|
|
238
|
+
}
|
|
239
|
+
const ticks = rawTicks.map((t, i) => ({
|
|
240
|
+
x: t.x,
|
|
241
|
+
label: i > 0 && rawTicks[i - 1]?.label === t.label ? "" : t.label
|
|
242
|
+
}));
|
|
243
|
+
return {
|
|
244
|
+
tasks: laidOutTasks,
|
|
245
|
+
deps,
|
|
246
|
+
ticks,
|
|
247
|
+
startLabel: formatDate(new Date(minTs)),
|
|
248
|
+
endLabel: formatDate(new Date(maxTs))
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function toDate(v) {
|
|
252
|
+
if (v instanceof Date) return v;
|
|
253
|
+
if (typeof v === "number") return new Date(v);
|
|
254
|
+
return new Date(v);
|
|
255
|
+
}
|
|
256
|
+
function clamp01(v) {
|
|
257
|
+
return v < 0 ? 0 : v > 1 ? 1 : v;
|
|
258
|
+
}
|
|
259
|
+
function formatDate(d) {
|
|
260
|
+
if (Number.isNaN(d.getTime())) return "\u2014";
|
|
261
|
+
return d.toISOString().slice(0, 10);
|
|
262
|
+
}
|
|
263
|
+
function formatTick(d, spanMs) {
|
|
264
|
+
if (Number.isNaN(d.getTime())) return "\u2014";
|
|
265
|
+
const ONE_DAY = 24 * 60 * 60 * 1e3;
|
|
266
|
+
if (spanMs <= 90 * ONE_DAY) return d.toISOString().slice(5, 10);
|
|
267
|
+
if (spanMs <= 730 * ONE_DAY) return d.toISOString().slice(0, 7);
|
|
268
|
+
return String(d.getUTCFullYear());
|
|
269
|
+
}
|
|
270
|
+
function activateOnKey(e, fn) {
|
|
271
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
272
|
+
e.preventDefault();
|
|
273
|
+
fn();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export { Gantt };
|
|
278
|
+
//# sourceMappingURL=gantt.js.map
|
|
279
|
+
//# sourceMappingURL=gantt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/utils.ts","../src/artifacts/gantt/gantt.tsx"],"names":[],"mappings":"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","file":"gantt.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 * Gantt chart — tasks as horizontal bars across a time axis, with\n * optional dependency arrows and progress fills. Pure SVG; no heavy\n * peer dependency.\n *\n * Distinct from TimeAxis (point events) and Sequence (actor messages):\n * Gantt encodes DURATION (start → end) per task and supports task-to-\n * task dependency arrows.\n *\n * @example\n * <Gantt\n * tasks={[\n * { id: \"design\", label: \"Design\", start: \"2025-01-01\", end: \"2025-01-15\", progress: 1 },\n * { id: \"build\", label: \"Build\", start: \"2025-01-10\", end: \"2025-02-20\", progress: 0.6, dependencies: [\"design\"] },\n * { id: \"ship\", label: \"Ship\", start: \"2025-02-15\", end: \"2025-02-28\", dependencies: [\"build\"] },\n * ]}\n * />\n */\nexport type GanttTask = {\n\tid: string;\n\tlabel: string;\n\tstart: Date | string | number;\n\tend: Date | string | number;\n\t/** Optional progress 0..1; renders as a filled portion of the bar. */\n\tprogress?: number;\n\t/** Optional list of task ids this task depends on; arrow drawn from each. */\n\tdependencies?: string[];\n};\n\nexport interface GanttProps extends Omit<React.SVGAttributes<SVGSVGElement>, \"children\"> {\n\t/** Tasks in display order (rows top-to-bottom). */\n\ttasks: GanttTask[];\n\t/** Pixel width of the rendered SVG. Default 800. */\n\twidth?: number;\n\t/** Pixel height of each task row. Default 32. */\n\trowHeight?: number;\n\t/** Pixel reserved on the left for task labels. Default 140. */\n\tlabelMargin?: number;\n\t/** Number of axis ticks to show. Default 6. */\n\ttickCount?: number;\n\t/** Fired when a task bar is clicked. */\n\tonTaskClick?: (task: GanttTask) => void;\n}\n\ninterface LaidOutTask {\n\ttask: GanttTask;\n\trowIndex: number;\n\tx: number;\n\ty: number;\n\tw: number;\n\th: number;\n\tprogressW: number;\n}\n\ninterface DepArrow {\n\tfrom: { x: number; y: number };\n\tto: { x: number; y: number };\n\tid: string;\n}\n\ninterface AxisTick {\n\tx: number;\n\tlabel: string;\n}\n\nconst HEADER_HEIGHT = 40;\nconst ROW_PADDING = 6;\n\nfunction Gantt({\n\ttasks,\n\twidth = 800,\n\trowHeight = 32,\n\tlabelMargin = 140,\n\ttickCount = 6,\n\tonTaskClick,\n\tclassName,\n\t...rest\n}: GanttProps) {\n\tconst laidOut = React.useMemo(\n\t\t() => layout(tasks, width, rowHeight, labelMargin, tickCount),\n\t\t[tasks, width, rowHeight, labelMargin, tickCount],\n\t);\n\n\tconst totalHeight = HEADER_HEIGHT + tasks.length * rowHeight + ROW_PADDING;\n\tconst desc = `Gantt with ${tasks.length} task${tasks.length === 1 ? \"\" : \"s\"}, range ${laidOut.startLabel} to ${laidOut.endLabel}`;\n\t// Per-instance arrowhead marker id keeps two <Gantt> on a page\n\t// from sharing a <defs> id.\n\tconst arrowId = React.useId().replace(/:/g, \"-\");\n\n\treturn (\n\t\t<svg\n\t\t\t{...rest}\n\t\t\tdata-hex-gantt\n\t\t\trole=\"img\"\n\t\t\twidth={width}\n\t\t\theight={totalHeight}\n\t\t\tviewBox={`0 0 ${width} ${totalHeight}`}\n\t\t\tclassName={cn(\"block\", className)}\n\t\t>\n\t\t\t<title>Gantt chart</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-gantt-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=\"5\"\n\t\t\t\t\tmarkerHeight=\"5\"\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{/* Axis header */}\n\t\t\t<g data-hex-gantt-axis>\n\t\t\t\t<line\n\t\t\t\t\tx1={labelMargin}\n\t\t\t\t\tx2={width - 16}\n\t\t\t\t\ty1={HEADER_HEIGHT - 8}\n\t\t\t\t\ty2={HEADER_HEIGHT - 8}\n\t\t\t\t\tstroke=\"hsl(var(--muted-foreground))\"\n\t\t\t\t\tstrokeWidth={1}\n\t\t\t\t/>\n\t\t\t\t{laidOut.ticks.map((t, i) => (\n\t\t\t\t\t<g key={`tick-${i}`} data-hex-gantt-tick>\n\t\t\t\t\t\t<line\n\t\t\t\t\t\t\tx1={t.x}\n\t\t\t\t\t\t\tx2={t.x}\n\t\t\t\t\t\t\ty1={HEADER_HEIGHT - 8}\n\t\t\t\t\t\t\ty2={totalHeight - 4}\n\t\t\t\t\t\t\tstroke=\"hsl(var(--muted-foreground))\"\n\t\t\t\t\t\t\tstrokeOpacity={0.15}\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\tx={t.x}\n\t\t\t\t\t\t\ty={HEADER_HEIGHT - 14}\n\t\t\t\t\t\t\ttextAnchor=\"middle\"\n\t\t\t\t\t\t\tfontSize={10}\n\t\t\t\t\t\t\tfill=\"hsl(var(--muted-foreground))\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{t.label}\n\t\t\t\t\t\t</text>\n\t\t\t\t\t</g>\n\t\t\t\t))}\n\t\t\t</g>\n\t\t\t{/* Row labels */}\n\t\t\t<g data-hex-gantt-labels>\n\t\t\t\t{laidOut.tasks.map((t) => (\n\t\t\t\t\t<text\n\t\t\t\t\t\tkey={`label-${t.task.id}`}\n\t\t\t\t\t\tx={labelMargin - 8}\n\t\t\t\t\t\ty={t.y + t.h / 2}\n\t\t\t\t\t\tdy=\"0.35em\"\n\t\t\t\t\t\ttextAnchor=\"end\"\n\t\t\t\t\t\tfontSize={11}\n\t\t\t\t\t\tfill=\"hsl(var(--foreground))\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{t.task.label}\n\t\t\t\t\t</text>\n\t\t\t\t))}\n\t\t\t</g>\n\t\t\t{/* Dependency arrows: elbow-and-jog routing — when the target\n\t\t\t task starts before (or near) the source's right edge, route\n\t\t\t out past the source first to avoid drawing through its bar. */}\n\t\t\t<g data-hex-gantt-deps fill=\"none\">\n\t\t\t\t{laidOut.deps.map((d) => {\n\t\t\t\t\tconst ELBOW = 8;\n\t\t\t\t\tconst needsJog = d.to.x < d.from.x + ELBOW;\n\t\t\t\t\tconst path = needsJog\n\t\t\t\t\t\t? `M${d.from.x},${d.from.y} L${d.from.x + ELBOW},${d.from.y} L${d.from.x + ELBOW},${d.to.y} L${d.to.x},${d.to.y}`\n\t\t\t\t\t\t: `M${d.from.x},${d.from.y} L${(d.from.x + d.to.x) / 2},${d.from.y} L${(d.from.x + d.to.x) / 2},${d.to.y} L${d.to.x},${d.to.y}`;\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<path\n\t\t\t\t\t\t\tkey={d.id}\n\t\t\t\t\t\t\tdata-hex-gantt-dep\n\t\t\t\t\t\t\td={path}\n\t\t\t\t\t\t\tstroke=\"hsl(var(--muted-foreground))\"\n\t\t\t\t\t\t\tstrokeOpacity={0.55}\n\t\t\t\t\t\t\tstrokeWidth={1}\n\t\t\t\t\t\t\tmarkerEnd={`url(#hex-gantt-arrow-${arrowId})`}\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{/* Task bars */}\n\t\t\t<g data-hex-gantt-tasks>\n\t\t\t\t{laidOut.tasks.map((t) => {\n\t\t\t\t\tconst interactive = Boolean(onTaskClick);\n\t\t\t\t\tconst handleActivate = () => onTaskClick?.(t.task);\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<g\n\t\t\t\t\t\t\tkey={t.task.id}\n\t\t\t\t\t\t\tdata-hex-gantt-task\n\t\t\t\t\t\t\tdata-row={t.rowIndex}\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={\n\t\t\t\t\t\t\t\tinteractive\n\t\t\t\t\t\t\t\t\t? `${t.task.label}, ${formatDate(toDate(t.task.start))} to ${formatDate(toDate(t.task.end))}${t.task.progress != null ? `, ${Math.round((t.task.progress ?? 0) * 100)}% complete` : \"\"}`\n\t\t\t\t\t\t\t\t\t: undefined\n\t\t\t\t\t\t\t}\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\n\t\t\t\t\t\t\t\tx={t.x}\n\t\t\t\t\t\t\t\ty={t.y}\n\t\t\t\t\t\t\t\twidth={Math.max(2, t.w)}\n\t\t\t\t\t\t\t\theight={t.h}\n\t\t\t\t\t\t\t\trx={4}\n\t\t\t\t\t\t\t\try={4}\n\t\t\t\t\t\t\t\tfill=\"hsl(var(--primary))\"\n\t\t\t\t\t\t\t\tfillOpacity={0.25}\n\t\t\t\t\t\t\t\tstroke=\"hsl(var(--primary))\"\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{t.progressW > 0 ? (\n\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t<rect\n\t\t\t\t\t\t\t\t\t\tx={t.x}\n\t\t\t\t\t\t\t\t\t\ty={t.y}\n\t\t\t\t\t\t\t\t\t\twidth={t.progressW}\n\t\t\t\t\t\t\t\t\t\theight={t.h}\n\t\t\t\t\t\t\t\t\t\trx={4}\n\t\t\t\t\t\t\t\t\t\try={4}\n\t\t\t\t\t\t\t\t\t\tfill=\"hsl(var(--primary))\"\n\t\t\t\t\t\t\t\t\t\tfillOpacity={0.7}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t{/* Hard right-edge line keeps the progress boundary visible for\n\t\t\t\t\t\t\t\t\t color-blind viewers and on low-contrast themes. Skipped at\n\t\t\t\t\t\t\t\t\t 100% progress where it overlaps the bar's own border. */}\n\t\t\t\t\t\t\t\t\t{t.progressW < t.w - 0.5 ? (\n\t\t\t\t\t\t\t\t\t\t<line\n\t\t\t\t\t\t\t\t\t\t\tx1={t.x + t.progressW}\n\t\t\t\t\t\t\t\t\t\t\tx2={t.x + t.progressW}\n\t\t\t\t\t\t\t\t\t\t\ty1={t.y}\n\t\t\t\t\t\t\t\t\t\t\ty2={t.y + t.h}\n\t\t\t\t\t\t\t\t\t\t\tstroke=\"hsl(var(--primary))\"\n\t\t\t\t\t\t\t\t\t\t\tstrokeWidth={1}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t) : null}\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\ttasks: GanttTask[],\n\twidth: number,\n\trowHeight: number,\n\tlabelMargin: number,\n\ttickCount: number,\n): {\n\ttasks: LaidOutTask[];\n\tdeps: DepArrow[];\n\tticks: AxisTick[];\n\tstartLabel: string;\n\tendLabel: string;\n} {\n\tif (tasks.length === 0) {\n\t\treturn { tasks: [], deps: [], ticks: [], startLabel: \"—\", endLabel: \"—\" };\n\t}\n\n\tconst axisLeft = labelMargin;\n\tconst axisRight = width - 16;\n\tconst usable = axisRight - axisLeft;\n\n\t// Drop NaN-bearing endpoints so the axis range stays finite. Tasks with\n\t// either bound un-parseable get rendered with their own zero-width\n\t// fallback below; we still want them in the row layout, just clipped to\n\t// the axis edges.\n\tconst allTimes = tasks\n\t\t.flatMap((t) => [toDate(t.start).getTime(), toDate(t.end).getTime()])\n\t\t.filter((t) => Number.isFinite(t));\n\tif (allTimes.length === 0) {\n\t\treturn { tasks: [], deps: [], ticks: [], startLabel: \"—\", endLabel: \"—\" };\n\t}\n\tconst minTs = Math.min(...allTimes);\n\tconst maxTs = Math.max(...allTimes);\n\tconst span = Math.max(1, maxTs - minTs);\n\n\tconst tToX = (t: number) => axisLeft + ((t - minTs) / span) * usable;\n\n\tconst laidOutTasks: LaidOutTask[] = tasks.map((task, i) => {\n\t\tconst startTs = toDate(task.start).getTime();\n\t\tconst endTs = toDate(task.end).getTime();\n\t\tconst x = tToX(startTs);\n\t\tconst w = Math.max(0, tToX(endTs) - x);\n\t\tconst y = HEADER_HEIGHT + i * rowHeight + ROW_PADDING / 2;\n\t\tconst h = rowHeight - ROW_PADDING;\n\t\tconst progress = clamp01(task.progress ?? 0);\n\t\treturn {\n\t\t\ttask,\n\t\t\trowIndex: i,\n\t\t\tx,\n\t\t\ty,\n\t\t\tw,\n\t\t\th,\n\t\t\tprogressW: w * progress,\n\t\t};\n\t});\n\n\tconst byId = new Map(laidOutTasks.map((t) => [t.task.id, t]));\n\tconst deps: DepArrow[] = [];\n\tlaidOutTasks.forEach((t) => {\n\t\t(t.task.dependencies ?? []).forEach((depId, k) => {\n\t\t\tconst from = byId.get(depId);\n\t\t\tif (!from) return;\n\t\t\tdeps.push({\n\t\t\t\tfrom: { x: from.x + from.w, y: from.y + from.h / 2 },\n\t\t\t\tto: { x: t.x, y: t.y + t.h / 2 },\n\t\t\t\tid: `${depId}-${t.task.id}-${k}`,\n\t\t\t});\n\t\t});\n\t});\n\n\t// Generate ticks, then dedupe consecutive identical labels — at year-scale\n\t// or month-scale spans the formatter often emits the same string twice\n\t// (e.g. two ticks both inside January render as \"2025-01\"). Blanking the\n\t// duplicate keeps the gridline but drops the noisy repeated text.\n\tconst rawTicks: AxisTick[] = [];\n\tfor (let i = 0; i < tickCount; i++) {\n\t\tconst ts = minTs + (i / Math.max(1, tickCount - 1)) * span;\n\t\trawTicks.push({ x: tToX(ts), label: formatTick(new Date(ts), span) });\n\t}\n\tconst ticks: AxisTick[] = rawTicks.map((t, i) => ({\n\t\tx: t.x,\n\t\tlabel: i > 0 && rawTicks[i - 1]?.label === t.label ? \"\" : t.label,\n\t}));\n\n\treturn {\n\t\ttasks: laidOutTasks,\n\t\tdeps,\n\t\tticks,\n\t\tstartLabel: formatDate(new Date(minTs)),\n\t\tendLabel: formatDate(new Date(maxTs)),\n\t};\n}\n\nfunction toDate(v: Date | string | number): Date {\n\tif (v instanceof Date) return v;\n\tif (typeof v === \"number\") return new Date(v);\n\treturn new Date(v);\n}\n\nfunction clamp01(v: number): number {\n\treturn v < 0 ? 0 : v > 1 ? 1 : v;\n}\n\nfunction formatDate(d: Date): string {\n\tif (Number.isNaN(d.getTime())) return \"—\";\n\treturn d.toISOString().slice(0, 10);\n}\n\nfunction formatTick(d: Date, spanMs: number): string {\n\tif (Number.isNaN(d.getTime())) return \"—\";\n\tconst ONE_DAY = 24 * 60 * 60 * 1000;\n\t// Switch to MM-DD up to ~3 months — at 30 days the original threshold\n\t// produced \"2025-01\" / \"2025-01\" duplicates for ticks landing in the\n\t// same month. MM-DD scales to ~90 days before adjacent ticks merge.\n\tif (spanMs <= 90 * ONE_DAY) return d.toISOString().slice(5, 10); // MM-DD\n\tif (spanMs <= 730 * ONE_DAY) return d.toISOString().slice(0, 7); // YYYY-MM\n\treturn String(d.getUTCFullYear());\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 { Gantt };\n"]}
|
|
@@ -0,0 +1,106 @@
|
|
|
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 ImageOcclusion({
|
|
11
|
+
src,
|
|
12
|
+
alt,
|
|
13
|
+
regions,
|
|
14
|
+
onRegionReveal,
|
|
15
|
+
className,
|
|
16
|
+
...rest
|
|
17
|
+
}) {
|
|
18
|
+
const [revealed, setRevealed] = React.useState(() => /* @__PURE__ */ new Set());
|
|
19
|
+
const onRevealRef = React.useRef(onRegionReveal);
|
|
20
|
+
onRevealRef.current = onRegionReveal;
|
|
21
|
+
const warnedRef = React.useRef(false);
|
|
22
|
+
const nodeEnv = globalThis.process?.env?.NODE_ENV;
|
|
23
|
+
if (nodeEnv !== "production" && !warnedRef.current) {
|
|
24
|
+
const offender = regions.find(
|
|
25
|
+
(r) => r.x < 0 || r.y < 0 || r.x + r.width > 1.0001 || r.y + r.height > 1.0001
|
|
26
|
+
);
|
|
27
|
+
if (offender) {
|
|
28
|
+
warnedRef.current = true;
|
|
29
|
+
console.warn(
|
|
30
|
+
`[hex-core/ImageOcclusion] Region "${offender.id}" coords escape [0, 1]. Pass fractions, not pixels \u2014 e.g. x: 0.42 (not 168).`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const toggle = React.useCallback((id) => {
|
|
35
|
+
setRevealed((prev) => {
|
|
36
|
+
const next = new Set(prev);
|
|
37
|
+
if (next.has(id)) {
|
|
38
|
+
next.delete(id);
|
|
39
|
+
} else {
|
|
40
|
+
next.add(id);
|
|
41
|
+
onRevealRef.current?.(id);
|
|
42
|
+
}
|
|
43
|
+
return next;
|
|
44
|
+
});
|
|
45
|
+
}, []);
|
|
46
|
+
return /* @__PURE__ */ jsxs(
|
|
47
|
+
"div",
|
|
48
|
+
{
|
|
49
|
+
...rest,
|
|
50
|
+
"data-hex-image-occlusion": true,
|
|
51
|
+
className: cn("relative inline-block", className),
|
|
52
|
+
children: [
|
|
53
|
+
/* @__PURE__ */ jsx(
|
|
54
|
+
"img",
|
|
55
|
+
{
|
|
56
|
+
src,
|
|
57
|
+
alt,
|
|
58
|
+
"data-hex-image-occlusion-img": true,
|
|
59
|
+
className: "block h-auto w-full select-none",
|
|
60
|
+
draggable: false
|
|
61
|
+
}
|
|
62
|
+
),
|
|
63
|
+
/* @__PURE__ */ jsx(
|
|
64
|
+
"div",
|
|
65
|
+
{
|
|
66
|
+
"data-hex-image-occlusion-overlay": true,
|
|
67
|
+
className: "pointer-events-none absolute inset-0",
|
|
68
|
+
children: regions.map((r, i) => {
|
|
69
|
+
const isRevealed = revealed.has(r.id);
|
|
70
|
+
return /* @__PURE__ */ jsx(
|
|
71
|
+
"button",
|
|
72
|
+
{
|
|
73
|
+
type: "button",
|
|
74
|
+
"data-hex-image-occlusion-region": true,
|
|
75
|
+
"data-region-id": r.id,
|
|
76
|
+
"data-revealed": isRevealed,
|
|
77
|
+
"aria-pressed": isRevealed,
|
|
78
|
+
"aria-label": r.label ? isRevealed ? `Region ${i + 1} revealed: ${r.label}. Activate to hide.` : `Region ${i + 1} of ${regions.length}, hidden. Activate to reveal: ${r.label}.` : isRevealed ? `Region ${i + 1} revealed. Activate to hide.` : `Region ${i + 1} of ${regions.length}, hidden. Activate to reveal.`,
|
|
79
|
+
onClick: () => toggle(r.id),
|
|
80
|
+
className: cn(
|
|
81
|
+
"pointer-events-auto absolute rounded-sm border-2 border-primary/60 transition-opacity focus:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
82
|
+
isRevealed ? "bg-transparent opacity-30 hover:opacity-60" : "bg-primary opacity-95 hover:bg-primary/90"
|
|
83
|
+
),
|
|
84
|
+
style: {
|
|
85
|
+
left: `${r.x * 100}%`,
|
|
86
|
+
top: `${r.y * 100}%`,
|
|
87
|
+
width: `${r.width * 100}%`,
|
|
88
|
+
height: `${r.height * 100}%`,
|
|
89
|
+
// Explicit z-index = array index makes stacking deterministic:
|
|
90
|
+
// later array entries always render (and click-catch) on top.
|
|
91
|
+
zIndex: i
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
r.id
|
|
95
|
+
);
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export { ImageOcclusion };
|
|
105
|
+
//# sourceMappingURL=image-occlusion.js.map
|
|
106
|
+
//# sourceMappingURL=image-occlusion.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/utils.ts","../src/artifacts/image-occlusion/image-occlusion.tsx"],"names":[],"mappings":"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","file":"image-occlusion.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 * Image occlusion — image with rectangular regions hidden behind opaque\n * overlays. Click / Enter / Space on a region reveals what's underneath.\n * Coordinates are 0–1 fractions of the rendered image so the layout\n * stays correct at any size. Pure HTML; no heavy peer.\n *\n * Common for anatomy diagrams, geographic maps, code snippets — any\n * visual where labels or sub-regions are the recall target.\n *\n * @example\n * <ImageOcclusion\n * src=\"/anatomy/heart.png\"\n * alt=\"Cross-section of a human heart\"\n * regions={[\n * { id: \"lv\", x: 0.42, y: 0.55, width: 0.18, height: 0.22, label: \"Left ventricle\" },\n * { id: \"ra\", x: 0.58, y: 0.20, width: 0.16, height: 0.18, label: \"Right atrium\" },\n * ]}\n * />\n */\nexport type OcclusionRegion = {\n\tid: string;\n\t/** All coords are 0–1 fractions of the rendered image. */\n\tx: number;\n\ty: number;\n\twidth: number;\n\theight: number;\n\tlabel?: string;\n};\n\nexport interface ImageOcclusionProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\"> {\n\t/** Image source URL. */\n\tsrc: string;\n\t/** Alt text for the underlying image. */\n\talt: string;\n\t/** Rectangular regions to hide on top of the image. */\n\tregions: OcclusionRegion[];\n\t/** Fired with the region id when a region is revealed (not when hidden again). */\n\tonRegionReveal?: (id: string) => void;\n}\n\nfunction ImageOcclusion({\n\tsrc,\n\talt,\n\tregions,\n\tonRegionReveal,\n\tclassName,\n\t...rest\n}: ImageOcclusionProps) {\n\tconst [revealed, setRevealed] = React.useState<Set<string>>(() => new Set());\n\tconst onRevealRef = React.useRef(onRegionReveal);\n\tonRevealRef.current = onRegionReveal;\n\n\t// Surface mistakes early in dev when fractional coords escape [0, 1].\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) {\n\t\tconst offender = regions.find(\n\t\t\t(r) => r.x < 0 || r.y < 0 || r.x + r.width > 1.0001 || r.y + r.height > 1.0001,\n\t\t);\n\t\tif (offender) {\n\t\t\twarnedRef.current = true;\n\t\t\t// eslint-disable-next-line no-console\n\t\t\tconsole.warn(\n\t\t\t\t`[hex-core/ImageOcclusion] Region \"${offender.id}\" coords escape [0, 1]. Pass fractions, not pixels — e.g. x: 0.42 (not 168).`,\n\t\t\t);\n\t\t}\n\t}\n\n\tconst toggle = React.useCallback((id: string) => {\n\t\tsetRevealed((prev) => {\n\t\t\tconst next = new Set(prev);\n\t\t\tif (next.has(id)) {\n\t\t\t\tnext.delete(id);\n\t\t\t} else {\n\t\t\t\tnext.add(id);\n\t\t\t\tonRevealRef.current?.(id);\n\t\t\t}\n\t\t\treturn next;\n\t\t});\n\t}, []);\n\n\treturn (\n\t\t<div\n\t\t\t{...rest}\n\t\t\tdata-hex-image-occlusion\n\t\t\tclassName={cn(\"relative inline-block\", className)}\n\t\t>\n\t\t\t<img\n\t\t\t\tsrc={src}\n\t\t\t\talt={alt}\n\t\t\t\tdata-hex-image-occlusion-img\n\t\t\t\tclassName=\"block h-auto w-full select-none\"\n\t\t\t\tdraggable={false}\n\t\t\t/>\n\t\t\t<div\n\t\t\t\tdata-hex-image-occlusion-overlay\n\t\t\t\tclassName=\"pointer-events-none absolute inset-0\"\n\t\t\t>\n\t\t\t\t{regions.map((r, i) => {\n\t\t\t\t\tconst isRevealed = revealed.has(r.id);\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\tkey={r.id}\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tdata-hex-image-occlusion-region\n\t\t\t\t\t\t\tdata-region-id={r.id}\n\t\t\t\t\t\t\tdata-revealed={isRevealed}\n\t\t\t\t\t\t\taria-pressed={isRevealed}\n\t\t\t\t\t\t\taria-label={\n\t\t\t\t\t\t\t\tr.label\n\t\t\t\t\t\t\t\t\t? isRevealed\n\t\t\t\t\t\t\t\t\t\t? `Region ${i + 1} revealed: ${r.label}. Activate to hide.`\n\t\t\t\t\t\t\t\t\t\t: `Region ${i + 1} of ${regions.length}, hidden. Activate to reveal: ${r.label}.`\n\t\t\t\t\t\t\t\t\t: isRevealed\n\t\t\t\t\t\t\t\t\t\t? `Region ${i + 1} revealed. Activate to hide.`\n\t\t\t\t\t\t\t\t\t\t: `Region ${i + 1} of ${regions.length}, hidden. Activate to reveal.`\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tonClick={() => toggle(r.id)}\n\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\"pointer-events-auto absolute rounded-sm border-2 border-primary/60 transition-opacity focus:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n\t\t\t\t\t\t\t\tisRevealed\n\t\t\t\t\t\t\t\t\t? \"bg-transparent opacity-30 hover:opacity-60\"\n\t\t\t\t\t\t\t\t\t: \"bg-primary opacity-95 hover:bg-primary/90\",\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\tleft: `${r.x * 100}%`,\n\t\t\t\t\t\t\t\ttop: `${r.y * 100}%`,\n\t\t\t\t\t\t\t\twidth: `${r.width * 100}%`,\n\t\t\t\t\t\t\t\theight: `${r.height * 100}%`,\n\t\t\t\t\t\t\t\t// Explicit z-index = array index makes stacking deterministic:\n\t\t\t\t\t\t\t\t// later array entries always render (and click-catch) on top.\n\t\t\t\t\t\t\t\tzIndex: i,\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t/>\n\t\t\t\t\t);\n\t\t\t\t})}\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n\nexport { ImageOcclusion };\n"]}
|