@usetheo/ui 0.1.0-next.0 → 0.1.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +116 -9
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/plugin-Atb0VKtr.d.ts +172 -0
  6. package/dist/slide/index.d.ts +212 -0
  7. package/dist/slide/index.js +714 -0
  8. package/dist/slide/index.js.map +1 -0
  9. package/dist/slide/plugins/emoji/index.d.ts +29 -0
  10. package/dist/slide/plugins/emoji/index.js +157 -0
  11. package/dist/slide/plugins/emoji/index.js.map +1 -0
  12. package/dist/slide/plugins/math/index.d.ts +13 -0
  13. package/dist/slide/plugins/math/index.js +145 -0
  14. package/dist/slide/plugins/math/index.js.map +1 -0
  15. package/dist/slide/plugins/mermaid/index.d.ts +55 -0
  16. package/dist/slide/plugins/mermaid/index.js +218 -0
  17. package/dist/slide/plugins/mermaid/index.js.map +1 -0
  18. package/dist/slide/plugins/shiki/index.d.ts +18 -0
  19. package/dist/slide/plugins/shiki/index.js +87 -0
  20. package/dist/slide/plugins/shiki/index.js.map +1 -0
  21. package/dist/slide/themes/default.css +256 -0
  22. package/dist/slide/themes/layouts.css +143 -0
  23. package/dist/slide/themes/violet-forge.css +256 -0
  24. package/dist/slide-deck/index.css +52 -0
  25. package/dist/slide-deck/index.css.map +1 -0
  26. package/dist/slide-deck/index.d.ts +377 -0
  27. package/dist/slide-deck/index.js +1797 -0
  28. package/dist/slide-deck/index.js.map +1 -0
  29. package/dist/whiteboard/index.d.ts +258 -0
  30. package/dist/whiteboard/index.js +738 -0
  31. package/dist/whiteboard/index.js.map +1 -0
  32. package/package.json +141 -9
  33. package/registry/r/agent-composer.json +4 -4
  34. package/registry/r/agent-editor.json +9 -9
  35. package/registry/r/agent-error-card.json +2 -2
  36. package/registry/r/agent-event.json +4 -4
  37. package/registry/r/agent-handoff.json +2 -2
  38. package/registry/r/agent-profile.json +2 -2
  39. package/registry/r/agent-starting-state.json +2 -2
  40. package/registry/r/agent-stream.json +9 -9
  41. package/registry/r/agent-streaming.json +2 -2
  42. package/registry/r/agent-timeline.json +4 -4
  43. package/registry/r/approval-card.json +4 -4
  44. package/registry/r/artifact-preview.json +2 -2
  45. package/registry/r/attachment-chip.json +4 -4
  46. package/registry/r/audit-log-entry.json +3 -3
  47. package/registry/r/auto-compact-notice.json +2 -2
  48. package/registry/r/avatar.json +2 -2
  49. package/registry/r/badge.json +2 -2
  50. package/registry/r/browser-controls.json +2 -2
  51. package/registry/r/build-log-stream.json +2 -2
  52. package/registry/r/button.json +2 -2
  53. package/registry/r/capability-indicator.json +3 -3
  54. package/registry/r/card.json +2 -2
  55. package/registry/r/chat-composer.json +3 -3
  56. package/registry/r/chat-message.json +3 -3
  57. package/registry/r/chat-thread.json +2 -2
  58. package/registry/r/checkbox.json +2 -2
  59. package/registry/r/command-palette.json +4 -4
  60. package/registry/r/context-card.json +3 -3
  61. package/registry/r/context-window-bar.json +2 -2
  62. package/registry/r/cost-meter.json +2 -2
  63. package/registry/r/created-files-card.json +3 -3
  64. package/registry/r/cron-job-card.json +2 -2
  65. package/registry/r/cron-jobs-list.json +3 -3
  66. package/registry/r/deployment-row.json +3 -3
  67. package/registry/r/dialog.json +2 -2
  68. package/registry/r/diff-viewer.json +2 -2
  69. package/registry/r/domain-config.json +6 -6
  70. package/registry/r/empty-state.json +3 -3
  71. package/registry/r/env-var-editor.json +5 -5
  72. package/registry/r/folder-context-card.json +3 -3
  73. package/registry/r/folder-selector.json +2 -2
  74. package/registry/r/form-field.json +2 -2
  75. package/registry/r/hook-config.json +2 -2
  76. package/registry/r/hook-event-log.json +2 -2
  77. package/registry/r/input.json +2 -2
  78. package/registry/r/intent-selector.json +3 -3
  79. package/registry/r/label.json +2 -2
  80. package/registry/r/lane-board.json +2 -2
  81. package/registry/r/login-split.json +2 -2
  82. package/registry/r/mcp-server-card.json +2 -2
  83. package/registry/r/mcp-server-list.json +3 -3
  84. package/registry/r/memory-editor.json +3 -3
  85. package/registry/r/mention-menu.json +3 -3
  86. package/registry/r/metrics-panel.json +2 -2
  87. package/registry/r/model-card.json +3 -3
  88. package/registry/r/model-selector.json +2 -2
  89. package/registry/r/permission-matrix.json +2 -2
  90. package/registry/r/permission-modal.json +4 -4
  91. package/registry/r/preview-env-card.json +5 -5
  92. package/registry/r/preview-panel.json +3 -3
  93. package/registry/r/progress-checklist.json +3 -3
  94. package/registry/r/project-card.json +5 -5
  95. package/registry/r/project-switcher.json +2 -2
  96. package/registry/r/quick-action-chips.json +3 -3
  97. package/registry/r/radio-group.json +2 -2
  98. package/registry/r/recent-folders-list.json +2 -2
  99. package/registry/r/rollback-ui.json +4 -4
  100. package/registry/r/rule-card.json +3 -3
  101. package/registry/r/rule-editor.json +10 -10
  102. package/registry/r/rule-types.json +1 -1
  103. package/registry/r/run-stats.json +2 -2
  104. package/registry/r/running-tasks-panel.json +2 -2
  105. package/registry/r/scroll-area.json +2 -2
  106. package/registry/r/select.json +2 -2
  107. package/registry/r/session-list-item.json +2 -2
  108. package/registry/r/session-timeline.json +2 -2
  109. package/registry/r/sheet.json +2 -2
  110. package/registry/r/sidebar.json +2 -2
  111. package/registry/r/skeleton.json +2 -2
  112. package/registry/r/skill-card.json +4 -4
  113. package/registry/r/skill-editor.json +10 -10
  114. package/registry/r/skills-list.json +3 -3
  115. package/registry/r/social-auth-row.json +3 -3
  116. package/registry/r/steps-rail.json +2 -2
  117. package/registry/r/sub-agent-dispatch.json +2 -2
  118. package/registry/r/switch.json +2 -2
  119. package/registry/r/system-prompt-editor.json +2 -2
  120. package/registry/r/tabs.json +2 -2
  121. package/registry/r/task-header.json +4 -4
  122. package/registry/r/task-plan.json +2 -2
  123. package/registry/r/terminal-panel.json +2 -2
  124. package/registry/r/textarea.json +2 -2
  125. package/registry/r/theme-provider.json +2 -2
  126. package/registry/r/theme-script.json +1 -1
  127. package/registry/r/theo-ui-provider.json +2 -2
  128. package/registry/r/toast.json +2 -2
  129. package/registry/r/token-usage-chart.json +2 -2
  130. package/registry/r/tool-call-card.json +3 -3
  131. package/registry/r/tool-call.json +2 -2
  132. package/registry/r/tool-result.json +2 -2
  133. package/registry/r/tools-list.json +3 -3
  134. package/registry/r/tooltip.json +2 -2
  135. package/registry/r/topnav.json +2 -2
@@ -0,0 +1,738 @@
1
+ import { useMemo, useEffect, useRef, useState, useCallback } from 'react';
2
+ import rough from 'roughjs';
3
+ import { getStroke } from 'perfect-freehand';
4
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
+ import { z } from 'zod';
6
+
7
+ // src/components/primitives/whiteboard/whiteboard.tsx
8
+
9
+ // src/components/primitives/whiteboard/seed.ts
10
+ var FNV_OFFSET_BASIS_32 = 2166136261;
11
+ var FNV_PRIME_32 = 16777619;
12
+ function fnv1a32(input) {
13
+ let hash = FNV_OFFSET_BASIS_32;
14
+ for (let i = 0; i < input.length; i++) {
15
+ hash ^= input.charCodeAt(i);
16
+ hash = Math.imul(hash, FNV_PRIME_32);
17
+ }
18
+ return hash | 0;
19
+ }
20
+ function deriveSeed(el) {
21
+ if (typeof el.seed === "number" && Number.isFinite(el.seed)) {
22
+ return el.seed | 0;
23
+ }
24
+ const key = `${el.type}|${el.x}|${el.y}|${el.w ?? ""}|${el.h ?? ""}|${el.label ?? ""}`;
25
+ return fnv1a32(key);
26
+ }
27
+ var DEFAULT_STROKE_OPTIONS = {
28
+ size: 8,
29
+ thinning: 0.5,
30
+ smoothing: 0.5,
31
+ streamline: 0.5
32
+ };
33
+ function fmt(n) {
34
+ return typeof n === "number" && Number.isFinite(n) ? n.toFixed(2) : "0";
35
+ }
36
+ function svgPathFromStroke(points) {
37
+ if (points.length === 0) return "";
38
+ const first = points[0] ?? [];
39
+ let d = `M ${fmt(first[0])} ${fmt(first[1])}`;
40
+ for (let i = 1; i < points.length; i++) {
41
+ const p = points[i] ?? [];
42
+ d += ` L ${fmt(p[0])} ${fmt(p[1])}`;
43
+ }
44
+ d += " Z";
45
+ return d;
46
+ }
47
+ function renderFreedraw(el) {
48
+ const size = (el.strokeWidth ?? 1.5) * 5;
49
+ const inputPoints = el.points.map(([x, y, pressure]) => {
50
+ const tx = el.x + x;
51
+ const ty = el.y + y;
52
+ return pressure === void 0 ? [tx, ty] : [tx, ty, pressure];
53
+ });
54
+ const stroke = getStroke(inputPoints, {
55
+ ...DEFAULT_STROKE_OPTIONS,
56
+ size
57
+ });
58
+ const d = svgPathFromStroke(stroke);
59
+ return /* @__PURE__ */ jsx("g", { opacity: el.opacity ?? 1, children: /* @__PURE__ */ jsx("path", { d, fill: el.stroke ?? "currentColor", stroke: "none" }) });
60
+ }
61
+
62
+ // src/components/primitives/whiteboard/render/rough-paths.ts
63
+ function toRoughPaths(generator, drawable) {
64
+ const paths = generator.toPaths(drawable);
65
+ return paths.map((p) => ({
66
+ d: p.d,
67
+ stroke: p.stroke,
68
+ strokeWidth: p.strokeWidth,
69
+ fill: p.fill
70
+ }));
71
+ }
72
+
73
+ // src/components/primitives/whiteboard/render/style.ts
74
+ var DEFAULT_STROKE = "currentColor";
75
+ function buildOptions(src, seed) {
76
+ const o = {
77
+ seed,
78
+ stroke: src.stroke ?? DEFAULT_STROKE,
79
+ strokeWidth: src.strokeWidth ?? 1.5,
80
+ roughness: src.roughness ?? 1.2
81
+ };
82
+ if (src.strokeStyle === "dashed") o.strokeLineDash = [10, 6];
83
+ else if (src.strokeStyle === "dotted") o.strokeLineDash = [2, 4];
84
+ if (src.fill) o.fill = src.fill;
85
+ if (src.fillStyle) o.fillStyle = src.fillStyle;
86
+ return o;
87
+ }
88
+ function strokeDashArray(strokeStyle2, strokeWidth) {
89
+ if (strokeStyle2 === "dashed") {
90
+ const dash = Math.max(8, strokeWidth * 6);
91
+ const gap = Math.max(5, strokeWidth * 4);
92
+ return `${dash} ${gap}`;
93
+ }
94
+ if (strokeStyle2 === "dotted") {
95
+ return `${strokeWidth} ${strokeWidth * 2.5}`;
96
+ }
97
+ return void 0;
98
+ }
99
+ var HEAD_BASE_PX = 12;
100
+ var HEAD_ANGLE_RAD = Math.PI / 7;
101
+ function arrowHeadGeometry(fromX, fromY, toX, toY, strokeWidth) {
102
+ const dx = toX - fromX;
103
+ const dy = toY - fromY;
104
+ const dist = Math.sqrt(dx * dx + dy * dy);
105
+ if (dist === 0) return null;
106
+ const angle = Math.atan2(dy, dx);
107
+ const headLen = Math.min(HEAD_BASE_PX + strokeWidth * 2, dist * 0.4);
108
+ const leftX = toX - headLen * Math.cos(angle - HEAD_ANGLE_RAD);
109
+ const leftY = toY - headLen * Math.sin(angle - HEAD_ANGLE_RAD);
110
+ const rightX = toX - headLen * Math.cos(angle + HEAD_ANGLE_RAD);
111
+ const rightY = toY - headLen * Math.sin(angle + HEAD_ANGLE_RAD);
112
+ return { apexX: toX, apexY: toY, leftX, leftY, rightX, rightY };
113
+ }
114
+ function lineBody(gen, el, seed) {
115
+ const opts = buildOptions(el, seed);
116
+ const drawable = gen.line(el.x, el.y, el.to[0], el.to[1], opts);
117
+ const dash = strokeDashArray(el.strokeStyle, el.strokeWidth ?? 1.5);
118
+ return toRoughPaths(gen, drawable).map((p, i) => /* @__PURE__ */ jsx(
119
+ "path",
120
+ {
121
+ d: p.d,
122
+ stroke: p.stroke,
123
+ strokeWidth: p.strokeWidth,
124
+ fill: "none",
125
+ strokeLinecap: "round",
126
+ strokeLinejoin: "round",
127
+ strokeDasharray: dash
128
+ },
129
+ `body-${i}`
130
+ ));
131
+ }
132
+ function arrowHead(gen, fromX, fromY, toX, toY, seed, el, side) {
133
+ const geom = arrowHeadGeometry(fromX, fromY, toX, toY, el.strokeWidth ?? 1.5);
134
+ if (!geom) return [];
135
+ const opts = buildOptions(el, seed);
136
+ const leftDrawable = gen.line(geom.apexX, geom.apexY, geom.leftX, geom.leftY, opts);
137
+ const rightDrawable = gen.line(geom.apexX, geom.apexY, geom.rightX, geom.rightY, opts);
138
+ const leftPaths = toRoughPaths(gen, leftDrawable);
139
+ const rightPaths = toRoughPaths(gen, rightDrawable);
140
+ const nodes = [];
141
+ for (const [i, p] of leftPaths.entries()) {
142
+ nodes.push(
143
+ /* @__PURE__ */ jsx(
144
+ "path",
145
+ {
146
+ d: p.d,
147
+ stroke: p.stroke,
148
+ strokeWidth: p.strokeWidth,
149
+ fill: "none",
150
+ strokeLinecap: "round",
151
+ strokeLinejoin: "round",
152
+ "data-line-part": "arrowhead"
153
+ },
154
+ `${side}-l-${i}`
155
+ )
156
+ );
157
+ }
158
+ for (const [i, p] of rightPaths.entries()) {
159
+ nodes.push(
160
+ /* @__PURE__ */ jsx(
161
+ "path",
162
+ {
163
+ d: p.d,
164
+ stroke: p.stroke,
165
+ strokeWidth: p.strokeWidth,
166
+ fill: "none",
167
+ strokeLinecap: "round",
168
+ strokeLinejoin: "round",
169
+ "data-line-part": "arrowhead"
170
+ },
171
+ `${side}-r-${i}`
172
+ )
173
+ );
174
+ }
175
+ return nodes;
176
+ }
177
+ function renderLine(gen, el) {
178
+ return /* @__PURE__ */ jsx("g", { opacity: el.opacity ?? 1, children: lineBody(gen, el, el.seed ?? 0) });
179
+ }
180
+ function renderArrow(gen, el) {
181
+ const seed = el.seed ?? 0;
182
+ const nodes = [...lineBody(gen, el, seed)];
183
+ if (el.headEnd !== false) {
184
+ nodes.push(...arrowHead(gen, el.x, el.y, el.to[0], el.to[1], seed + 1, el, "end"));
185
+ }
186
+ if (el.headStart) {
187
+ nodes.push(...arrowHead(gen, el.to[0], el.to[1], el.x, el.y, seed + 2, el, "start"));
188
+ }
189
+ let label = null;
190
+ if (el.label) {
191
+ const midX = (el.x + el.to[0]) / 2;
192
+ const midY = (el.y + el.to[1]) / 2;
193
+ const dx = el.to[0] - el.x;
194
+ const dy = el.to[1] - el.y;
195
+ const dist = Math.sqrt(dx * dx + dy * dy) || 1;
196
+ const nx = -dy / dist;
197
+ const ny = dx / dist;
198
+ const offset = 12;
199
+ label = /* @__PURE__ */ jsx(
200
+ "text",
201
+ {
202
+ x: midX + nx * offset,
203
+ y: midY + ny * offset,
204
+ textAnchor: "middle",
205
+ dominantBaseline: "central",
206
+ fontSize: 14,
207
+ fontFamily: "ui-sans-serif, system-ui, sans-serif",
208
+ fill: el.stroke ?? "currentColor",
209
+ style: { pointerEvents: "none" },
210
+ children: el.label
211
+ }
212
+ );
213
+ }
214
+ return /* @__PURE__ */ jsxs("g", { opacity: el.opacity ?? 1, children: [
215
+ nodes,
216
+ label
217
+ ] });
218
+ }
219
+ function Label({ cx, cy, label, stroke }) {
220
+ return /* @__PURE__ */ jsx(
221
+ "text",
222
+ {
223
+ x: cx,
224
+ y: cy,
225
+ textAnchor: "middle",
226
+ dominantBaseline: "central",
227
+ fontSize: 16,
228
+ fontFamily: "ui-sans-serif, system-ui, sans-serif",
229
+ fill: stroke ?? "currentColor",
230
+ style: { pointerEvents: "none" },
231
+ children: label
232
+ }
233
+ );
234
+ }
235
+ function pathsToReactNodes(paths, keyPrefix, outlineStroke, dashArray) {
236
+ return paths.map((p, i) => {
237
+ const isOutline = p.stroke === outlineStroke;
238
+ return /* @__PURE__ */ jsx(
239
+ "path",
240
+ {
241
+ d: p.d,
242
+ stroke: p.stroke,
243
+ strokeWidth: p.strokeWidth,
244
+ fill: p.fill ?? "none",
245
+ strokeLinecap: "round",
246
+ strokeLinejoin: "round",
247
+ strokeDasharray: isOutline ? dashArray : void 0
248
+ },
249
+ `${keyPrefix}-${i}`
250
+ );
251
+ });
252
+ }
253
+ function renderRect(gen, el) {
254
+ const opts = buildOptions(el, el.seed ?? 0);
255
+ const drawable = gen.rectangle(el.x, el.y, el.w, el.h, opts);
256
+ const paths = toRoughPaths(gen, drawable);
257
+ const dash = strokeDashArray(el.strokeStyle, el.strokeWidth ?? 1.5);
258
+ return /* @__PURE__ */ jsxs("g", { opacity: el.opacity ?? 1, children: [
259
+ pathsToReactNodes(paths, "rect", opts.stroke, dash),
260
+ el.label ? /* @__PURE__ */ jsx(Label, { cx: el.x + el.w / 2, cy: el.y + el.h / 2, label: el.label, stroke: el.stroke }) : null
261
+ ] });
262
+ }
263
+ function renderEllipse(gen, el) {
264
+ const opts = buildOptions(el, el.seed ?? 0);
265
+ const drawable = gen.ellipse(el.x + el.w / 2, el.y + el.h / 2, el.w, el.h, opts);
266
+ const paths = toRoughPaths(gen, drawable);
267
+ const dash = strokeDashArray(el.strokeStyle, el.strokeWidth ?? 1.5);
268
+ return /* @__PURE__ */ jsxs("g", { opacity: el.opacity ?? 1, children: [
269
+ pathsToReactNodes(paths, "ellipse", opts.stroke, dash),
270
+ el.label ? /* @__PURE__ */ jsx(Label, { cx: el.x + el.w / 2, cy: el.y + el.h / 2, label: el.label, stroke: el.stroke }) : null
271
+ ] });
272
+ }
273
+ function renderDiamond(gen, el) {
274
+ const opts = buildOptions(el, el.seed ?? 0);
275
+ const cx = el.x + el.w / 2;
276
+ const cy = el.y + el.h / 2;
277
+ const points = [
278
+ [cx, el.y],
279
+ // top
280
+ [el.x + el.w, cy],
281
+ // right
282
+ [cx, el.y + el.h],
283
+ // bottom
284
+ [el.x, cy]
285
+ // left
286
+ ];
287
+ const drawable = gen.polygon(points, opts);
288
+ const paths = toRoughPaths(gen, drawable);
289
+ const dash = strokeDashArray(el.strokeStyle, el.strokeWidth ?? 1.5);
290
+ return /* @__PURE__ */ jsxs("g", { opacity: el.opacity ?? 1, children: [
291
+ pathsToReactNodes(paths, "diamond", opts.stroke, dash),
292
+ el.label ? /* @__PURE__ */ jsx(Label, { cx, cy, label: el.label, stroke: el.stroke }) : null
293
+ ] });
294
+ }
295
+ var FONT_STACKS = {
296
+ sans: "ui-sans-serif, system-ui, sans-serif",
297
+ serif: "ui-serif, Georgia, serif",
298
+ mono: "ui-monospace, SFMono-Regular, Menlo, monospace",
299
+ hand: '"Virgil", "Caveat", "Comic Sans MS", cursive'
300
+ };
301
+ function textAnchor(align2) {
302
+ if (align2 === "center") return "middle";
303
+ if (align2 === "right") return "end";
304
+ return "start";
305
+ }
306
+ function renderText(el) {
307
+ const fontSize = el.fontSize ?? 18;
308
+ const fontFamily2 = FONT_STACKS[el.fontFamily ?? "hand"];
309
+ const anchor = textAnchor(el.align);
310
+ const lines = el.text.split("\n");
311
+ return /* @__PURE__ */ jsx("g", { opacity: el.opacity ?? 1, children: /* @__PURE__ */ jsx(
312
+ "text",
313
+ {
314
+ x: el.x,
315
+ y: el.y,
316
+ textAnchor: anchor,
317
+ dominantBaseline: "hanging",
318
+ fontSize,
319
+ fontFamily: fontFamily2,
320
+ fill: el.stroke ?? "currentColor",
321
+ children: lines.map((line, i) => (
322
+ // biome-ignore lint/suspicious/noArrayIndexKey: text is split deterministically on '\n' so the line index is the most stable key.
323
+ /* @__PURE__ */ jsx("tspan", { x: el.x, dy: i === 0 ? 0 : fontSize * 1.2, children: line }, i)
324
+ ))
325
+ }
326
+ ) });
327
+ }
328
+ function elementWithSeed(el) {
329
+ if (typeof el.seed === "number") return el;
330
+ return { ...el, seed: deriveSeed(el) };
331
+ }
332
+ function renderElement(el, gen) {
333
+ switch (el.type) {
334
+ case "rect":
335
+ return renderRect(gen, elementWithSeed(el));
336
+ case "ellipse":
337
+ return renderEllipse(gen, elementWithSeed(el));
338
+ case "diamond":
339
+ return renderDiamond(gen, elementWithSeed(el));
340
+ case "line":
341
+ return renderLine(gen, elementWithSeed(el));
342
+ case "arrow":
343
+ return renderArrow(gen, elementWithSeed(el));
344
+ case "text":
345
+ return renderText(elementWithSeed(el));
346
+ case "freedraw":
347
+ return renderFreedraw(elementWithSeed(el));
348
+ }
349
+ }
350
+ function renderScene(scene, gen) {
351
+ return /* @__PURE__ */ jsx(Fragment, { children: scene.elements.map((el, i) => /* @__PURE__ */ jsx(
352
+ "g",
353
+ {
354
+ "data-element-id": el.id ?? String(i),
355
+ "data-element-type": el.type,
356
+ children: renderElement(el, gen)
357
+ },
358
+ el.id ?? `__idx-${i}`
359
+ )) });
360
+ }
361
+ var finiteNumber = z.number().finite();
362
+ var finitePositive = finiteNumber.positive();
363
+ var strokeStyle = z.enum(["solid", "dashed", "dotted"]);
364
+ var fillStyle = z.enum(["hachure", "solid", "cross-hatch", "zigzag"]);
365
+ var align = z.enum(["left", "center", "right"]);
366
+ var fontFamily = z.enum(["sans", "serif", "mono", "hand"]);
367
+ var roundness = z.enum(["sharp", "round"]);
368
+ var baseElement = z.object({
369
+ id: z.string().max(120).optional(),
370
+ x: finiteNumber,
371
+ y: finiteNumber,
372
+ stroke: z.string().max(64).optional(),
373
+ strokeWidth: finitePositive.max(50).optional(),
374
+ strokeStyle: strokeStyle.optional(),
375
+ fill: z.string().max(64).optional(),
376
+ fillStyle: fillStyle.optional(),
377
+ opacity: finiteNumber.min(0).max(1).optional(),
378
+ roughness: finiteNumber.min(0).max(3).optional(),
379
+ seed: z.number().int().finite().optional()
380
+ });
381
+ var dim = finitePositive.max(2e4);
382
+ var labelText = z.string().max(500);
383
+ var rectElement = baseElement.extend({
384
+ type: z.literal("rect"),
385
+ w: dim,
386
+ h: dim,
387
+ label: labelText.optional(),
388
+ roundness: roundness.optional()
389
+ });
390
+ var ellipseElement = baseElement.extend({
391
+ type: z.literal("ellipse"),
392
+ w: dim,
393
+ h: dim,
394
+ label: labelText.optional()
395
+ });
396
+ var diamondElement = baseElement.extend({
397
+ type: z.literal("diamond"),
398
+ w: dim,
399
+ h: dim,
400
+ label: labelText.optional()
401
+ });
402
+ var point2 = z.tuple([finiteNumber, finiteNumber]);
403
+ var lineElement = baseElement.extend({
404
+ type: z.literal("line"),
405
+ to: point2
406
+ });
407
+ var arrowElement = baseElement.extend({
408
+ type: z.literal("arrow"),
409
+ to: point2,
410
+ label: labelText.optional(),
411
+ headStart: z.boolean().optional(),
412
+ headEnd: z.boolean().default(true)
413
+ });
414
+ var textElement = baseElement.extend({
415
+ type: z.literal("text"),
416
+ text: z.string().max(5e3),
417
+ fontSize: finitePositive.max(500).optional(),
418
+ align: align.optional(),
419
+ fontFamily: fontFamily.optional()
420
+ });
421
+ var freedrawPoint = z.tuple([finiteNumber, finiteNumber, finiteNumber.optional()]);
422
+ var freedrawElement = baseElement.extend({
423
+ type: z.literal("freedraw"),
424
+ points: z.array(freedrawPoint).min(2).max(5e3)
425
+ });
426
+ var whiteboardElement = z.discriminatedUnion("type", [
427
+ rectElement,
428
+ ellipseElement,
429
+ diamondElement,
430
+ lineElement,
431
+ arrowElement,
432
+ textElement,
433
+ freedrawElement
434
+ ]);
435
+ var whiteboardScene = z.object({
436
+ version: z.literal(1),
437
+ width: dim,
438
+ // EC-4: clamped 1..20000
439
+ height: dim,
440
+ // EC-4: clamped 1..20000
441
+ background: z.string().max(64).optional(),
442
+ elements: z.array(whiteboardElement).max(5e3)
443
+ });
444
+
445
+ // src/components/primitives/whiteboard/validate.ts
446
+ function valueAtPath(input, path) {
447
+ let cursor = input;
448
+ for (const segment of path) {
449
+ if (cursor === null || typeof cursor !== "object") return void 0;
450
+ cursor = cursor[segment];
451
+ }
452
+ return cursor;
453
+ }
454
+ function formatIssue(issue, input) {
455
+ const error = {
456
+ path: issue.path.join("."),
457
+ message: issue.message,
458
+ code: issue.code
459
+ };
460
+ if ("received" in issue && issue.received !== void 0) {
461
+ error.got = issue.received;
462
+ } else if (issue.path.length > 0) {
463
+ const value = valueAtPath(input, issue.path);
464
+ if (value !== void 0) error.got = value;
465
+ }
466
+ return error;
467
+ }
468
+ function validateScene(input) {
469
+ const result = whiteboardScene.safeParse(input);
470
+ if (result.success) {
471
+ return { ok: true, scene: result.data };
472
+ }
473
+ const errors = result.error.issues.map(
474
+ (issue) => formatIssue(issue, input)
475
+ );
476
+ return { ok: false, errors };
477
+ }
478
+ function usePointerPan(ref, viewport, size) {
479
+ const dragRef = useRef(null);
480
+ const spaceHeldRef = useRef(false);
481
+ useEffect(() => {
482
+ const el = ref.current;
483
+ if (!el) return;
484
+ const handler = (e) => {
485
+ e.preventDefault();
486
+ const rect = el.getBoundingClientRect();
487
+ const localX = e.clientX - rect.left;
488
+ const localY = e.clientY - rect.top;
489
+ const scaleX = size.width / rect.width;
490
+ const scaleY = size.height / rect.height;
491
+ viewport.zoomAt(localX * scaleX, localY * scaleY, -e.deltaY * 1e-3, size);
492
+ };
493
+ el.addEventListener("wheel", handler, { passive: false });
494
+ return () => el.removeEventListener("wheel", handler);
495
+ }, [ref, viewport, size]);
496
+ useEffect(() => {
497
+ const down = (e) => {
498
+ if (e.code === "Space") spaceHeldRef.current = true;
499
+ };
500
+ const up = (e) => {
501
+ if (e.code === "Space") spaceHeldRef.current = false;
502
+ };
503
+ window.addEventListener("keydown", down);
504
+ window.addEventListener("keyup", up);
505
+ return () => {
506
+ window.removeEventListener("keydown", down);
507
+ window.removeEventListener("keyup", up);
508
+ };
509
+ }, []);
510
+ const onPointerDown = useCallback((e) => {
511
+ if (e.target instanceof Element && !e.currentTarget.contains(e.target)) return;
512
+ const isMouseButton = e.button === 0 || e.button === 1;
513
+ if (!isMouseButton && e.pointerType === "mouse") return;
514
+ if (e.pointerType === "mouse" && e.button !== 1 && !spaceHeldRef.current && e.button !== 0) {
515
+ return;
516
+ }
517
+ dragRef.current = { pointerId: e.pointerId, lastX: e.clientX, lastY: e.clientY };
518
+ e.currentTarget.setPointerCapture(e.pointerId);
519
+ }, []);
520
+ const onPointerMove = useCallback(
521
+ (e) => {
522
+ const drag = dragRef.current;
523
+ if (!drag || drag.pointerId !== e.pointerId) return;
524
+ const dx = e.clientX - drag.lastX;
525
+ const dy = e.clientY - drag.lastY;
526
+ drag.lastX = e.clientX;
527
+ drag.lastY = e.clientY;
528
+ const el = ref.current;
529
+ if (!el) return;
530
+ const rect = el.getBoundingClientRect();
531
+ const scaleX = size.width / rect.width;
532
+ const scaleY = size.height / rect.height;
533
+ viewport.pan(dx * scaleX, dy * scaleY);
534
+ },
535
+ [ref, viewport, size]
536
+ );
537
+ const releaseDrag = useCallback((e) => {
538
+ const drag = dragRef.current;
539
+ if (!drag || drag.pointerId !== e.pointerId) return;
540
+ try {
541
+ e.currentTarget.releasePointerCapture(e.pointerId);
542
+ } catch {
543
+ }
544
+ dragRef.current = null;
545
+ }, []);
546
+ return {
547
+ onPointerDown,
548
+ onPointerMove,
549
+ onPointerUp: releaseDrag,
550
+ onPointerCancel: releaseDrag
551
+ };
552
+ }
553
+ var MIN_ZOOM = 0.1;
554
+ var MAX_ZOOM = 8;
555
+ function clampZoom(z2) {
556
+ if (!Number.isFinite(z2)) return 1;
557
+ return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, z2));
558
+ }
559
+ function useViewport(opts) {
560
+ const initial = useMemo(() => {
561
+ const zoom = clampZoom(opts.initialZoom ?? 1);
562
+ if (opts.initialCenter) {
563
+ const [cx, cy] = opts.initialCenter;
564
+ return {
565
+ x: cx - opts.width / (2 * zoom),
566
+ y: cy - opts.height / (2 * zoom),
567
+ zoom
568
+ };
569
+ }
570
+ return { x: 0, y: 0, zoom };
571
+ }, [opts.initialCenter, opts.initialZoom, opts.width, opts.height]);
572
+ const [state, setState] = useState(initial);
573
+ const pan = useCallback((dx, dy) => {
574
+ setState((prev) => ({ ...prev, x: prev.x - dx / prev.zoom, y: prev.y - dy / prev.zoom }));
575
+ }, []);
576
+ const setZoom = useCallback((zoom) => {
577
+ setState((prev) => ({ ...prev, zoom: clampZoom(zoom) }));
578
+ }, []);
579
+ const zoomAt = useCallback(
580
+ (screenX, screenY, delta, _size) => {
581
+ setState((prev) => {
582
+ const oldZoom = prev.zoom;
583
+ const newZoom = clampZoom(prev.zoom * Math.exp(delta));
584
+ if (newZoom === oldZoom) return prev;
585
+ const worldX = prev.x + screenX / oldZoom;
586
+ const worldY = prev.y + screenY / oldZoom;
587
+ return {
588
+ x: worldX - screenX / newZoom,
589
+ y: worldY - screenY / newZoom,
590
+ zoom: newZoom
591
+ };
592
+ });
593
+ },
594
+ []
595
+ );
596
+ const reset = useCallback(() => setState(initial), [initial]);
597
+ const fitTo = useCallback((bounds, size) => {
598
+ const bboxWidth = Math.max(1, bounds.maxX - bounds.minX);
599
+ const bboxHeight = Math.max(1, bounds.maxY - bounds.minY);
600
+ const zoomX = size.width / bboxWidth;
601
+ const zoomY = size.height / bboxHeight;
602
+ const zoom = clampZoom(Math.min(zoomX, zoomY));
603
+ const cx = (bounds.minX + bounds.maxX) / 2;
604
+ const cy = (bounds.minY + bounds.maxY) / 2;
605
+ setState({
606
+ x: cx - size.width / (2 * zoom),
607
+ y: cy - size.height / (2 * zoom),
608
+ zoom
609
+ });
610
+ }, []);
611
+ const viewBox = useCallback(
612
+ (size) => `${state.x} ${state.y} ${size.width / state.zoom} ${size.height / state.zoom}`,
613
+ [state]
614
+ );
615
+ return { state, pan, setZoom, zoomAt, reset, fitTo, viewBox };
616
+ }
617
+ function computeBounds(scene) {
618
+ if (scene.elements.length === 0) return null;
619
+ let minX = Number.POSITIVE_INFINITY;
620
+ let minY = Number.POSITIVE_INFINITY;
621
+ let maxX = Number.NEGATIVE_INFINITY;
622
+ let maxY = Number.NEGATIVE_INFINITY;
623
+ for (const el of scene.elements) {
624
+ const x1 = el.x;
625
+ const y1 = el.y;
626
+ let x2 = el.x;
627
+ let y2 = el.y;
628
+ if (el.type === "rect" || el.type === "ellipse" || el.type === "diamond") {
629
+ x2 = el.x + el.w;
630
+ y2 = el.y + el.h;
631
+ } else if (el.type === "line" || el.type === "arrow") {
632
+ x2 = el.to[0];
633
+ y2 = el.to[1];
634
+ } else if (el.type === "freedraw") {
635
+ for (const [px, py] of el.points) {
636
+ const tx = el.x + px;
637
+ const ty = el.y + py;
638
+ if (tx < minX) minX = tx;
639
+ if (ty < minY) minY = ty;
640
+ if (tx > maxX) maxX = tx;
641
+ if (ty > maxY) maxY = ty;
642
+ }
643
+ continue;
644
+ }
645
+ if (x1 < minX) minX = x1;
646
+ if (y1 < minY) minY = y1;
647
+ if (x2 < minX) minX = x2;
648
+ if (y2 < minY) minY = y2;
649
+ if (x1 > maxX) maxX = x1;
650
+ if (y1 > maxY) maxY = y1;
651
+ if (x2 > maxX) maxX = x2;
652
+ if (y2 > maxY) maxY = y2;
653
+ }
654
+ if (!Number.isFinite(minX)) return null;
655
+ return { minX, minY, maxX, maxY };
656
+ }
657
+ function Whiteboard({
658
+ data,
659
+ className,
660
+ initialZoom,
661
+ initialCenter,
662
+ fitOnLoad,
663
+ onValidationError,
664
+ "aria-label": ariaLabel
665
+ }) {
666
+ const validation = useMemo(() => validateScene(data), [data]);
667
+ const scene = validation.ok ? validation.scene : null;
668
+ useEffect(() => {
669
+ if (!validation.ok && onValidationError) {
670
+ onValidationError(validation.errors);
671
+ }
672
+ }, [validation, onValidationError]);
673
+ const generator = useMemo(() => {
674
+ if (typeof globalThis === "undefined") return null;
675
+ try {
676
+ return rough.generator();
677
+ } catch {
678
+ return null;
679
+ }
680
+ }, []);
681
+ const sceneWidth = scene?.width ?? 400;
682
+ const sceneHeight = scene?.height ?? 300;
683
+ const sceneBackground = scene?.background;
684
+ const viewport = useViewport({
685
+ width: sceneWidth,
686
+ height: sceneHeight,
687
+ initialZoom,
688
+ initialCenter
689
+ });
690
+ const svgRef = useRef(null);
691
+ const fitTo = viewport.fitTo;
692
+ useEffect(() => {
693
+ if (!fitOnLoad || !scene) return;
694
+ const bounds = computeBounds(scene);
695
+ if (!bounds) return;
696
+ fitTo(bounds, { width: sceneWidth, height: sceneHeight });
697
+ }, [fitOnLoad, scene, sceneWidth, sceneHeight, fitTo]);
698
+ const handlers = usePointerPan(svgRef, viewport, {
699
+ width: sceneWidth,
700
+ height: sceneHeight
701
+ });
702
+ const label = ariaLabel ?? "Whiteboard diagram";
703
+ return /* @__PURE__ */ jsxs(
704
+ "svg",
705
+ {
706
+ ref: svgRef,
707
+ viewBox: viewport.viewBox({ width: sceneWidth, height: sceneHeight }),
708
+ width: sceneWidth,
709
+ height: sceneHeight,
710
+ className,
711
+ role: "img",
712
+ "aria-label": label,
713
+ xmlns: "http://www.w3.org/2000/svg",
714
+ "data-whiteboard-state": scene ? "ok" : "invalid",
715
+ style: { touchAction: "none", userSelect: "none" },
716
+ ...handlers,
717
+ children: [
718
+ /* @__PURE__ */ jsx("title", { children: label }),
719
+ sceneBackground ? /* @__PURE__ */ jsx(
720
+ "rect",
721
+ {
722
+ x: 0,
723
+ y: 0,
724
+ width: sceneWidth,
725
+ height: sceneHeight,
726
+ fill: sceneBackground,
727
+ "data-layer": "background"
728
+ }
729
+ ) : null,
730
+ scene && generator ? renderScene(scene, generator) : null
731
+ ]
732
+ }
733
+ );
734
+ }
735
+
736
+ export { Whiteboard, validateScene };
737
+ //# sourceMappingURL=index.js.map
738
+ //# sourceMappingURL=index.js.map