viteboard 0.1.1 → 0.1.2

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 (81) hide show
  1. package/content/docs/index.md +1 -1
  2. package/content/docs/product-roadmap.board.json +4 -4
  3. package/content/machine/board-v1.schema.json +138 -0
  4. package/content/machine/example.board.json +217 -0
  5. package/content/machine/guide.md +270 -0
  6. package/content/machine/llms.txt +31 -0
  7. package/content/machine/viteboard.json +62 -0
  8. package/dist/assets/index-D2W76uu9.js +96 -0
  9. package/dist/assets/index-rMkUjGXP.css +1 -0
  10. package/dist/index.html +4 -2
  11. package/package.json +10 -12
  12. package/vite.config.ts +619 -38
  13. package/dist/assets/index-C30zRPsW.js +0 -60
  14. package/dist/assets/index-e5aqtN8W.css +0 -1
  15. package/index.html +0 -13
  16. package/src/app/AppController.ts +0 -962
  17. package/src/app/BoardState.ts +0 -130
  18. package/src/app/ClipboardService.ts +0 -159
  19. package/src/app/CommandManager.ts +0 -66
  20. package/src/app/ElementActions.ts +0 -152
  21. package/src/app/EmbedRegionSelector.ts +0 -103
  22. package/src/app/ExportPng.ts +0 -107
  23. package/src/app/ImageInsertService.ts +0 -141
  24. package/src/app/KeyboardShortcuts.ts +0 -124
  25. package/src/app/main.ts +0 -6
  26. package/src/board/BoardPreview.ts +0 -189
  27. package/src/board/BoardService.ts +0 -222
  28. package/src/canvas/CanvasRenderer.ts +0 -253
  29. package/src/canvas/CanvasSurface.ts +0 -70
  30. package/src/canvas/HitTester.ts +0 -110
  31. package/src/canvas/ImageCache.ts +0 -123
  32. package/src/canvas/PerformanceMonitor.ts +0 -31
  33. package/src/canvas/RenderScheduler.ts +0 -26
  34. package/src/canvas/Viewport.ts +0 -77
  35. package/src/content/ContentApi.ts +0 -258
  36. package/src/content/Markdown.ts +0 -431
  37. package/src/content/MarkdownView.ts +0 -70
  38. package/src/editor/DocEditor.ts +0 -799
  39. package/src/editor/htmlToMarkdown.ts +0 -333
  40. package/src/elements/renderElement.ts +0 -598
  41. package/src/elements/types.ts +0 -119
  42. package/src/shell/Shell.ts +0 -2948
  43. package/src/shell/Sidebar.ts +0 -1651
  44. package/src/storage/AssetStore.ts +0 -86
  45. package/src/storage/BoardSerializer.ts +0 -114
  46. package/src/storage/ImportExportService.ts +0 -153
  47. package/src/storage/IndexedDbStore.ts +0 -92
  48. package/src/storage/LocalBoardStore.ts +0 -104
  49. package/src/styles.css +0 -3332
  50. package/src/templates/handoffTemplate.ts +0 -34
  51. package/src/templates/helpers.ts +0 -132
  52. package/src/templates/index.ts +0 -79
  53. package/src/templates/journeyMapTemplate.ts +0 -52
  54. package/src/templates/opportunityTreeTemplate.ts +0 -45
  55. package/src/templates/personaTemplate.ts +0 -41
  56. package/src/templates/prioritizationMatrixTemplate.ts +0 -44
  57. package/src/templates/requirementsFlowTemplate.ts +0 -45
  58. package/src/templates/screenshotReviewTemplate.ts +0 -52
  59. package/src/templates/systemDiagramTemplate.ts +0 -41
  60. package/src/templates/workflowTemplate.ts +0 -35
  61. package/src/testing/StressTestGenerator.ts +0 -134
  62. package/src/testing/StressTestPanel.ts +0 -64
  63. package/src/tools/ArrowTool.ts +0 -97
  64. package/src/tools/ImageTool.ts +0 -39
  65. package/src/tools/PanTool.ts +0 -34
  66. package/src/tools/SelectTool.ts +0 -377
  67. package/src/tools/ShapeTool.ts +0 -106
  68. package/src/tools/StickyTool.ts +0 -69
  69. package/src/tools/TaskTool.ts +0 -52
  70. package/src/tools/TextTool.ts +0 -45
  71. package/src/tools/ToolContext.ts +0 -51
  72. package/src/tools/ToolController.ts +0 -178
  73. package/src/ui/Modal.ts +0 -58
  74. package/src/ui/PerformanceOverlay.ts +0 -75
  75. package/src/ui/StorageManager.ts +0 -94
  76. package/src/ui/TemplatePicker.ts +0 -67
  77. package/src/ui/TextEditorOverlay.ts +0 -262
  78. package/src/ui/Toolbar.ts +0 -189
  79. package/src/ui/TopControls.ts +0 -137
  80. package/src/ui/icons.ts +0 -56
  81. package/tsconfig.json +0 -19
@@ -1,598 +0,0 @@
1
- import type { BoardElement } from "./types";
2
- import type { ImageCache } from "../canvas/ImageCache";
3
-
4
- export type RenderContext = {
5
- ctx: CanvasRenderingContext2D;
6
- scale: number;
7
- /** level of detail: 2 = full, 1 = simplified (<25%), 0 = blocks (<10%) */
8
- lod: 0 | 1 | 2;
9
- theme: "light" | "dark";
10
- imageCache: ImageCache;
11
- };
12
-
13
- const textMeasureCache = new Map<string, string[]>();
14
- const TEXT_CACHE_LIMIT = 2000;
15
-
16
- export function invalidateTextCache(elementId: string): void {
17
- for (const key of textMeasureCache.keys()) {
18
- if (key.startsWith(elementId + "|")) textMeasureCache.delete(key);
19
- }
20
- }
21
-
22
- export function getRenderedTextFontSize(el: BoardElement): number {
23
- if (el.data.shapeKind === "chevron") {
24
- const byHeight = Math.abs(el.height) * 0.34;
25
- const byWidth = Math.abs(el.width) * 0.18;
26
- return Math.round(Math.max(13, Math.min(48, byHeight, byWidth)));
27
- }
28
- return el.style.fontSize ?? 14;
29
- }
30
-
31
- function wrapText(
32
- ctx: CanvasRenderingContext2D,
33
- cacheKey: string,
34
- text: string,
35
- maxWidth: number
36
- ): string[] {
37
- const cached = textMeasureCache.get(cacheKey);
38
- if (cached) return cached;
39
-
40
- const lines: string[] = [];
41
- for (const paragraph of text.split("\n")) {
42
- if (paragraph === "") {
43
- lines.push("");
44
- continue;
45
- }
46
- const words = paragraph.split(" ");
47
- let line = "";
48
- for (const word of words) {
49
- const test = line === "" ? word : line + " " + word;
50
- if (ctx.measureText(test).width > maxWidth && line !== "") {
51
- lines.push(line);
52
- line = word;
53
- } else {
54
- line = test;
55
- }
56
- }
57
- lines.push(line);
58
- }
59
-
60
- if (textMeasureCache.size > TEXT_CACHE_LIMIT) textMeasureCache.clear();
61
- textMeasureCache.set(cacheKey, lines);
62
- return lines;
63
- }
64
-
65
- function roundRectPath(
66
- ctx: CanvasRenderingContext2D,
67
- x: number,
68
- y: number,
69
- w: number,
70
- h: number,
71
- r: number
72
- ): void {
73
- const radius = Math.min(r, w / 2, h / 2);
74
- ctx.beginPath();
75
- ctx.moveTo(x + radius, y);
76
- ctx.arcTo(x + w, y, x + w, y + h, radius);
77
- ctx.arcTo(x + w, y + h, x, y + h, radius);
78
- ctx.arcTo(x, y + h, x, y, radius);
79
- ctx.arcTo(x, y, x + w, y, radius);
80
- ctx.closePath();
81
- }
82
-
83
- export function renderElement(el: BoardElement, rc: RenderContext): void {
84
- const { ctx, lod } = rc;
85
- ctx.save();
86
- ctx.globalAlpha = el.style.opacity ?? 1;
87
-
88
- if (lod === 0) {
89
- renderAsBlock(el, rc);
90
- ctx.restore();
91
- return;
92
- }
93
-
94
- switch (el.type) {
95
- case "sticky":
96
- renderSticky(el, rc);
97
- break;
98
- case "task":
99
- renderTask(el, rc);
100
- break;
101
- case "rectangle":
102
- case "frame":
103
- renderRectangle(el, rc);
104
- break;
105
- case "ellipse":
106
- renderEllipse(el, rc);
107
- break;
108
- case "text":
109
- renderText(el, rc);
110
- break;
111
- case "line":
112
- case "arrow":
113
- case "connector":
114
- renderLineArrow(el, rc);
115
- break;
116
- case "image":
117
- case "screenshot":
118
- renderImage(el, rc);
119
- break;
120
- default:
121
- renderRectangle(el, rc);
122
- }
123
- ctx.restore();
124
- }
125
-
126
- function renderAsBlock(el: BoardElement, rc: RenderContext): void {
127
- const { ctx } = rc;
128
- if (el.type === "line" || el.type === "arrow" || el.type === "connector") {
129
- ctx.strokeStyle = el.style.stroke ?? (rc.theme === "dark" ? "#888" : "#999");
130
- ctx.lineWidth = Math.max(1 / rc.scale, el.style.strokeWidth ?? 2);
131
- ctx.beginPath();
132
- ctx.moveTo(el.x, el.y);
133
- ctx.lineTo(el.x + el.width, el.y + el.height);
134
- ctx.stroke();
135
- return;
136
- }
137
- ctx.fillStyle =
138
- el.style.fill ?? (rc.theme === "dark" ? "#3a3d42" : "#d8d8d4");
139
- ctx.fillRect(el.x, el.y, el.width, el.height);
140
- }
141
-
142
- function applyShadow(el: BoardElement, rc: RenderContext): void {
143
- if (el.style.shadow && rc.lod === 2) {
144
- rc.ctx.shadowColor =
145
- rc.theme === "dark" ? "rgba(0,0,0,0.5)" : "rgba(0,0,0,0.14)";
146
- rc.ctx.shadowBlur = 8 * rc.scale;
147
- rc.ctx.shadowOffsetY = 2 * rc.scale;
148
- }
149
- }
150
-
151
- function clearShadow(rc: RenderContext): void {
152
- rc.ctx.shadowColor = "transparent";
153
- rc.ctx.shadowBlur = 0;
154
- rc.ctx.shadowOffsetY = 0;
155
- }
156
-
157
- function renderSticky(el: BoardElement, rc: RenderContext): void {
158
- const { ctx, lod } = rc;
159
- const fill = el.style.fill ?? "#fef08a";
160
- applyShadow(el, rc);
161
- roundRectPath(ctx, el.x, el.y, el.width, el.height, el.style.borderRadius ?? 12);
162
- ctx.fillStyle = fill;
163
- ctx.fill();
164
- clearShadow(rc);
165
-
166
- if (lod === 2) {
167
- const text = (el.data.text as string) ?? "";
168
- if (text) {
169
- drawWrappedText(el, rc, text, 14);
170
- }
171
- }
172
- }
173
-
174
- function renderTask(el: BoardElement, rc: RenderContext): void {
175
- const { ctx, lod } = rc;
176
- const checked = el.data.checked === true;
177
- const isDark = rc.theme === "dark";
178
- const radius = el.style.borderRadius ?? 14;
179
- applyShadow(el, rc);
180
- roundRectPath(ctx, el.x, el.y, el.width, el.height, radius);
181
- ctx.fillStyle = el.style.fill ?? (isDark ? "#17191b" : "#ffffff");
182
- ctx.fill();
183
- clearShadow(rc);
184
-
185
- ctx.strokeStyle = el.style.stroke ?? (isDark ? "rgba(255,255,255,0.12)" : "rgba(0,0,0,0.09)");
186
- ctx.lineWidth = el.style.strokeWidth ?? 1;
187
- ctx.stroke();
188
-
189
- if (lod < 2) return;
190
-
191
- const boxSize = Math.min(22, Math.max(16, el.height * 0.38));
192
- const boxX = el.x + 16;
193
- const boxY = el.y + el.height / 2 - boxSize / 2;
194
- roundRectPath(ctx, boxX, boxY, boxSize, boxSize, 6);
195
- ctx.fillStyle = checked ? "#0078d4" : isDark ? "#111314" : "#f3f2f1";
196
- ctx.fill();
197
- ctx.strokeStyle = checked ? "#0078d4" : isDark ? "rgba(255,255,255,0.18)" : "rgba(0,0,0,0.13)";
198
- ctx.lineWidth = 1.4;
199
- ctx.stroke();
200
-
201
- if (checked) {
202
- ctx.strokeStyle = "#ffffff";
203
- ctx.lineWidth = 2.1;
204
- ctx.lineCap = "round";
205
- ctx.lineJoin = "round";
206
- ctx.beginPath();
207
- ctx.moveTo(boxX + boxSize * 0.26, boxY + boxSize * 0.52);
208
- ctx.lineTo(boxX + boxSize * 0.43, boxY + boxSize * 0.68);
209
- ctx.lineTo(boxX + boxSize * 0.75, boxY + boxSize * 0.32);
210
- ctx.stroke();
211
- }
212
-
213
- const text = (el.data.text as string) ?? "";
214
- if (!text) return;
215
- const leftPad = 50;
216
- const rightPad = 16;
217
- const fontSize = el.style.fontSize ?? 15;
218
- const fontFamily =
219
- el.style.fontFamily ??
220
- '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
221
- ctx.font = `${el.style.fontWeight ?? "650"} ${fontSize}px ${fontFamily}`;
222
- ctx.fillStyle = checked ? (isDark ? "#a7aaa8" : "#777777") : el.style.textColor ?? (isDark ? "#f6f6f3" : "#252829");
223
- ctx.textAlign = "left";
224
- ctx.textBaseline = "middle";
225
-
226
- ctx.save();
227
- ctx.beginPath();
228
- ctx.rect(el.x + leftPad, el.y, Math.max(10, el.width - leftPad - rightPad), el.height);
229
- ctx.clip();
230
- ctx.fillText(text, el.x + leftPad, el.y + el.height / 2);
231
- if (checked) {
232
- const metrics = ctx.measureText(text);
233
- const lineY = el.y + el.height / 2 + 1;
234
- ctx.strokeStyle = isDark ? "rgba(167,170,168,0.58)" : "rgba(119,119,119,0.48)";
235
- ctx.lineWidth = 1.3;
236
- ctx.beginPath();
237
- ctx.moveTo(el.x + leftPad, lineY);
238
- ctx.lineTo(el.x + leftPad + Math.min(metrics.width, el.width - leftPad - rightPad), lineY);
239
- ctx.stroke();
240
- }
241
- ctx.restore();
242
- }
243
-
244
- function drawText(
245
- ctx: CanvasRenderingContext2D,
246
- value: string,
247
- x: number,
248
- y: number,
249
- fontSize: number,
250
- color: string,
251
- fontWeight: string,
252
- fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
253
- ): void {
254
- ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
255
- ctx.fillStyle = color;
256
- ctx.textAlign = "left";
257
- ctx.textBaseline = "alphabetic";
258
- ctx.fillText(value, x, y);
259
- }
260
-
261
- function renderRectangle(el: BoardElement, rc: RenderContext): void {
262
- const { ctx, lod } = rc;
263
- const shapeKind = (el.data.shapeKind as string | undefined) ?? "rectangle";
264
- const r = el.style.borderRadius ?? 0;
265
- applyShadow(el, rc);
266
- if (shapeKind === "diamond") {
267
- pathDiamond(ctx, el);
268
- } else if (shapeKind === "triangle") {
269
- pathTriangle(ctx, el);
270
- } else if (shapeKind === "chevron") {
271
- pathChevron(ctx, el);
272
- } else if (shapeKind === "parallelogram") {
273
- pathParallelogram(ctx, el);
274
- } else if (shapeKind === "hexagon") {
275
- pathHexagon(ctx, el);
276
- } else if (shapeKind === "cylinder") {
277
- pathCylinder(ctx, el);
278
- } else if (shapeKind === "pill") {
279
- roundRectPath(ctx, el.x, el.y, el.width, el.height, Math.min(Math.abs(el.height) / 2, Math.abs(el.width) / 2));
280
- } else if (r > 0) {
281
- roundRectPath(ctx, el.x, el.y, el.width, el.height, r);
282
- } else {
283
- ctx.beginPath();
284
- ctx.rect(el.x, el.y, el.width, el.height);
285
- }
286
- if (el.style.fill && el.style.fill !== "transparent") {
287
- ctx.fillStyle = el.style.fill;
288
- ctx.fill();
289
- }
290
- clearShadow(rc);
291
- if (el.style.stroke && (el.style.strokeWidth ?? 0) > 0) {
292
- ctx.strokeStyle = el.style.stroke;
293
- ctx.lineWidth = el.style.strokeWidth ?? 1;
294
- ctx.stroke();
295
- if (shapeKind === "cylinder") {
296
- const rx = Math.abs(el.width) / 2;
297
- const ry = Math.min(Math.abs(el.height) * 0.18, 18);
298
- ctx.beginPath();
299
- ctx.ellipse(el.x + el.width / 2, el.y + ry, rx, ry, 0, 0, Math.PI * 2);
300
- ctx.stroke();
301
- }
302
- }
303
- if (lod === 2) {
304
- const text = (el.data.text as string) ?? "";
305
- if (text) {
306
- const textPadding =
307
- shapeKind === "chevron" ? Math.min(Math.abs(el.width) * 0.18, Math.abs(el.height) * 0.55) + 8 : 10;
308
- drawWrappedText(el, rc, text, textPadding);
309
- }
310
- }
311
- }
312
-
313
- function pathDiamond(ctx: CanvasRenderingContext2D, el: BoardElement): void {
314
- const x = el.x;
315
- const y = el.y;
316
- const w = el.width;
317
- const h = el.height;
318
- ctx.beginPath();
319
- ctx.moveTo(x + w / 2, y);
320
- ctx.lineTo(x + w, y + h / 2);
321
- ctx.lineTo(x + w / 2, y + h);
322
- ctx.lineTo(x, y + h / 2);
323
- ctx.closePath();
324
- }
325
-
326
- function pathTriangle(ctx: CanvasRenderingContext2D, el: BoardElement): void {
327
- const x = el.x;
328
- const y = el.y;
329
- const w = el.width;
330
- const h = el.height;
331
- ctx.beginPath();
332
- ctx.moveTo(x + w / 2, y);
333
- ctx.lineTo(x + w, y + h);
334
- ctx.lineTo(x, y + h);
335
- ctx.closePath();
336
- }
337
-
338
- function pathChevron(ctx: CanvasRenderingContext2D, el: BoardElement): void {
339
- const x = el.x;
340
- const y = el.y;
341
- const w = el.width;
342
- const h = el.height;
343
- const notch = Math.min(Math.abs(w) * 0.22, Math.abs(h) * 0.62);
344
- ctx.beginPath();
345
- ctx.moveTo(x, y);
346
- ctx.lineTo(x + w - notch, y);
347
- ctx.lineTo(x + w, y + h / 2);
348
- ctx.lineTo(x + w - notch, y + h);
349
- ctx.lineTo(x, y + h);
350
- ctx.lineTo(x + notch, y + h / 2);
351
- ctx.closePath();
352
- }
353
-
354
- function pathParallelogram(ctx: CanvasRenderingContext2D, el: BoardElement): void {
355
- const skew = Math.min(Math.abs(el.width) * 0.2, Math.abs(el.height) * 0.55);
356
- ctx.beginPath();
357
- ctx.moveTo(el.x + skew, el.y);
358
- ctx.lineTo(el.x + el.width, el.y);
359
- ctx.lineTo(el.x + el.width - skew, el.y + el.height);
360
- ctx.lineTo(el.x, el.y + el.height);
361
- ctx.closePath();
362
- }
363
-
364
- function pathHexagon(ctx: CanvasRenderingContext2D, el: BoardElement): void {
365
- const inset = Math.min(Math.abs(el.width) * 0.22, Math.abs(el.height) * 0.5);
366
- ctx.beginPath();
367
- ctx.moveTo(el.x + inset, el.y);
368
- ctx.lineTo(el.x + el.width - inset, el.y);
369
- ctx.lineTo(el.x + el.width, el.y + el.height / 2);
370
- ctx.lineTo(el.x + el.width - inset, el.y + el.height);
371
- ctx.lineTo(el.x + inset, el.y + el.height);
372
- ctx.lineTo(el.x, el.y + el.height / 2);
373
- ctx.closePath();
374
- }
375
-
376
- function pathCylinder(ctx: CanvasRenderingContext2D, el: BoardElement): void {
377
- const rx = Math.abs(el.width) / 2;
378
- const ry = Math.min(Math.abs(el.height) * 0.18, 18);
379
- const cx = el.x + el.width / 2;
380
- ctx.beginPath();
381
- ctx.ellipse(cx, el.y + ry, rx, ry, 0, Math.PI, Math.PI * 2);
382
- ctx.lineTo(el.x + el.width, el.y + el.height - ry);
383
- ctx.ellipse(cx, el.y + el.height - ry, rx, ry, 0, 0, Math.PI);
384
- ctx.lineTo(el.x, el.y + ry);
385
- ctx.closePath();
386
- }
387
-
388
- function renderEllipse(el: BoardElement, rc: RenderContext): void {
389
- const { ctx, lod } = rc;
390
- applyShadow(el, rc);
391
- ctx.beginPath();
392
- ctx.ellipse(
393
- el.x + el.width / 2,
394
- el.y + el.height / 2,
395
- Math.abs(el.width / 2),
396
- Math.abs(el.height / 2),
397
- 0,
398
- 0,
399
- Math.PI * 2
400
- );
401
- if (el.style.fill && el.style.fill !== "transparent") {
402
- ctx.fillStyle = el.style.fill;
403
- ctx.fill();
404
- }
405
- clearShadow(rc);
406
- if (el.style.stroke && (el.style.strokeWidth ?? 0) > 0) {
407
- ctx.strokeStyle = el.style.stroke;
408
- ctx.lineWidth = el.style.strokeWidth ?? 1;
409
- ctx.stroke();
410
- }
411
- if (lod === 2) {
412
- const text = (el.data.text as string) ?? "";
413
- if (text) drawWrappedText(el, rc, text, 10);
414
- }
415
- }
416
-
417
- function renderText(el: BoardElement, rc: RenderContext): void {
418
- if (rc.lod < 2) {
419
- // tiny placeholder bar when far out
420
- rc.ctx.fillStyle = rc.theme === "dark" ? "#55585e" : "#c5c5c0";
421
- rc.ctx.fillRect(el.x, el.y, el.width, Math.min(el.height, 6 / rc.scale));
422
- return;
423
- }
424
- const text = (el.data.text as string) ?? "";
425
- drawWrappedText(el, rc, text, 0, "left", "top");
426
- }
427
-
428
- function drawWrappedText(
429
- el: BoardElement,
430
- rc: RenderContext,
431
- text: string,
432
- padding: number,
433
- align: "center" | "left" = "center",
434
- vAlign: "middle" | "top" = "middle"
435
- ): void {
436
- const { ctx } = rc;
437
- const isChevron = el.data.shapeKind === "chevron";
438
- const fontSize = getRenderedTextFontSize(el);
439
- const fontFamily =
440
- el.style.fontFamily ??
441
- '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
442
- const fontWeight = isChevron ? "750" : el.style.fontWeight ?? "400";
443
- ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
444
- ctx.fillStyle =
445
- el.style.textColor ?? (rc.theme === "dark" && el.type === "text" ? "#f2f2f2" : "#1f2933");
446
-
447
- const maxWidth = Math.max(10, el.width - padding * 2);
448
- const cacheKey = `${el.id}|${el.updatedAt}|${fontSize}|${maxWidth.toFixed(0)}`;
449
- const lines = wrapText(ctx, cacheKey, text, maxWidth);
450
- const lineHeight = fontSize * 1.35;
451
- const totalHeight = lines.length * lineHeight;
452
-
453
- let startY: number;
454
- if (vAlign === "middle") {
455
- startY = el.y + el.height / 2 - totalHeight / 2 + lineHeight / 2;
456
- } else {
457
- startY = el.y + padding + lineHeight / 2;
458
- }
459
-
460
- ctx.textBaseline = "middle";
461
- ctx.textAlign = align;
462
- const tx = align === "center" ? el.x + el.width / 2 : el.x + padding;
463
-
464
- // clip to element bounds
465
- ctx.save();
466
- ctx.beginPath();
467
- ctx.rect(el.x, el.y, el.width, el.height);
468
- ctx.clip();
469
- for (let i = 0; i < lines.length; i++) {
470
- ctx.fillText(lines[i], tx, startY + i * lineHeight);
471
- }
472
- ctx.restore();
473
- }
474
-
475
- function renderLineArrow(el: BoardElement, rc: RenderContext): void {
476
- const { ctx, lod } = rc;
477
- const x1 = el.x;
478
- const y1 = el.y;
479
- const x2 = el.x + el.width;
480
- const y2 = el.y + el.height;
481
- const stroke = el.style.stroke ?? (rc.theme === "dark" ? "#d1d5db" : "#374151");
482
- const strokeWidth = el.style.strokeWidth ?? 2;
483
-
484
- ctx.strokeStyle = stroke;
485
- ctx.fillStyle = stroke;
486
- ctx.lineWidth = strokeWidth;
487
- ctx.setLineDash(el.style.strokeDash ?? []);
488
- ctx.beginPath();
489
- ctx.moveTo(x1, y1);
490
- ctx.lineTo(x2, y2);
491
- ctx.stroke();
492
- ctx.setLineDash([]);
493
-
494
- if (lod >= 1) {
495
- const angle = Math.atan2(y2 - y1, x2 - x1);
496
- const headLen = Math.max(11, strokeWidth * 5.5);
497
- if (el.style.arrowEnd ?? el.type === "arrow") {
498
- drawArrowhead(ctx, x2, y2, angle, headLen);
499
- }
500
- if (el.style.arrowStart) {
501
- drawArrowhead(ctx, x1, y1, angle + Math.PI, headLen);
502
- }
503
- const label = typeof el.data.text === "string" ? el.data.text.trim() : "";
504
- if (label) {
505
- if (el.data.labelPosition === "sides") {
506
- const [left, right] = label.split("|").map((part) => part.trim());
507
- if (left) drawArrowLabel(ctx, left, x1, y1, x2, y2, rc, 0.24);
508
- if (right) drawArrowLabel(ctx, right, x1, y1, x2, y2, rc, 0.76);
509
- } else {
510
- drawArrowLabel(ctx, label, x1, y1, x2, y2, rc);
511
- }
512
- }
513
- }
514
- }
515
-
516
- function drawArrowLabel(
517
- ctx: CanvasRenderingContext2D,
518
- label: string,
519
- x1: number,
520
- y1: number,
521
- x2: number,
522
- y2: number,
523
- rc: RenderContext,
524
- position = 0.5
525
- ): void {
526
- const fontSize = 13;
527
- ctx.font = `600 ${fontSize}px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif`;
528
- const metrics = ctx.measureText(label);
529
- const padX = 7;
530
- const width = metrics.width + padX * 2;
531
- const height = fontSize + 8;
532
- const x = x1 + (x2 - x1) * position - width / 2;
533
- const y = y1 + (y2 - y1) * position - height / 2;
534
- ctx.fillStyle = rc.theme === "dark" ? "#202328" : "#ffffff";
535
- roundRectPath(ctx, x, y, width, height, 4);
536
- ctx.fill();
537
- ctx.strokeStyle = rc.theme === "dark" ? "#4a4d52" : "#d8dade";
538
- ctx.lineWidth = 1;
539
- ctx.stroke();
540
- ctx.fillStyle = rc.theme === "dark" ? "#f2f2f2" : "#1f2933";
541
- ctx.textAlign = "center";
542
- ctx.textBaseline = "middle";
543
- ctx.fillText(label, x + width / 2, y + height / 2);
544
- }
545
-
546
- function drawArrowhead(
547
- ctx: CanvasRenderingContext2D,
548
- x: number,
549
- y: number,
550
- angle: number,
551
- len: number
552
- ): void {
553
- ctx.beginPath();
554
- ctx.moveTo(x, y);
555
- ctx.lineTo(
556
- x - len * Math.cos(angle - Math.PI / 6),
557
- y - len * Math.sin(angle - Math.PI / 6)
558
- );
559
- ctx.lineTo(
560
- x - len * Math.cos(angle + Math.PI / 6),
561
- y - len * Math.sin(angle + Math.PI / 6)
562
- );
563
- ctx.closePath();
564
- ctx.fill();
565
- }
566
-
567
- function renderImage(el: BoardElement, rc: RenderContext): void {
568
- const { ctx, imageCache } = rc;
569
- const src = el.data.src as string | undefined;
570
- const assetId = el.data.assetId as string | undefined;
571
- // File-backed boards reference images by `src` path; legacy boards by assetId.
572
- const img = src ? imageCache.getBySrc(src) : assetId ? imageCache.get(assetId) : null;
573
-
574
- if (img) {
575
- if (rc.lod === 1 && rc.scale < 0.15) {
576
- ctx.fillStyle = rc.theme === "dark" ? "#3a3d42" : "#d8d8d4";
577
- ctx.fillRect(el.x, el.y, el.width, el.height);
578
- return;
579
- }
580
- ctx.drawImage(img, el.x, el.y, el.width, el.height);
581
- } else {
582
- // placeholder / broken state
583
- ctx.fillStyle = rc.theme === "dark" ? "#2a2d31" : "#ececea";
584
- ctx.fillRect(el.x, el.y, el.width, el.height);
585
- ctx.strokeStyle = rc.theme === "dark" ? "#4a4d52" : "#c9c9c5";
586
- ctx.lineWidth = 1 / rc.scale;
587
- ctx.strokeRect(el.x, el.y, el.width, el.height);
588
- if (rc.lod === 2) {
589
- const isMissing = src ? imageCache.isMissingSrc(src) : assetId ? imageCache.isMissing(assetId) : false;
590
- const status = isMissing ? "Missing image" : "Loading…";
591
- ctx.font = `12px -apple-system, sans-serif`;
592
- ctx.fillStyle = rc.theme === "dark" ? "#9ca3af" : "#6b7280";
593
- ctx.textAlign = "center";
594
- ctx.textBaseline = "middle";
595
- ctx.fillText(status, el.x + el.width / 2, el.y + el.height / 2);
596
- }
597
- }
598
- }
@@ -1,119 +0,0 @@
1
- export type ElementType =
2
- | "sticky"
3
- | "task"
4
- | "rectangle"
5
- | "ellipse"
6
- | "text"
7
- | "line"
8
- | "arrow"
9
- | "connector"
10
- | "image"
11
- | "screenshot"
12
- | "frame"
13
- | "templateGroup";
14
-
15
- export type ElementStyle = {
16
- fill?: string;
17
- stroke?: string;
18
- strokeWidth?: number;
19
- opacity?: number;
20
- borderRadius?: number;
21
- shadow?: boolean;
22
- fontSize?: number;
23
- fontFamily?: string;
24
- fontWeight?: string;
25
- textColor?: string;
26
- arrowStart?: boolean;
27
- arrowEnd?: boolean;
28
- strokeDash?: number[];
29
- };
30
-
31
- export type BoardElement = {
32
- id: string;
33
- type: ElementType;
34
- x: number;
35
- y: number;
36
- width: number;
37
- height: number;
38
- rotation: number;
39
- zIndex: number;
40
- locked: boolean;
41
- hidden: boolean;
42
- createdAt: number;
43
- updatedAt: number;
44
- style: ElementStyle;
45
- data: Record<string, unknown>;
46
- };
47
-
48
- export type ImageElementData = {
49
- assetId: string;
50
- originalWidth: number;
51
- originalHeight: number;
52
- alt?: string;
53
- };
54
-
55
- export type LocalAsset = {
56
- id: string;
57
- type: "image";
58
- mimeType: string;
59
- blob: Blob;
60
- width: number;
61
- height: number;
62
- sizeBytes: number;
63
- createdAt: number;
64
- updatedAt: number;
65
- name?: string;
66
- };
67
-
68
- export type BoardDocument = {
69
- version: 1;
70
- id: string;
71
- title: string;
72
- createdAt: number;
73
- updatedAt: number;
74
- viewport: {
75
- offsetX: number;
76
- offsetY: number;
77
- scale: number;
78
- };
79
- theme: "light" | "dark";
80
- elements: BoardElement[];
81
- assetIds: string[];
82
- };
83
-
84
- let idCounter = 0;
85
-
86
- export function generateId(): string {
87
- idCounter = (idCounter + 1) % 0xffff;
88
- return (
89
- Date.now().toString(36) +
90
- "-" +
91
- idCounter.toString(36) +
92
- "-" +
93
- Math.random().toString(36).slice(2, 8)
94
- );
95
- }
96
-
97
- export function createElement(
98
- type: ElementType,
99
- partial: Partial<BoardElement> = {}
100
- ): BoardElement {
101
- const now = Date.now();
102
- return {
103
- id: generateId(),
104
- type,
105
- x: 0,
106
- y: 0,
107
- width: 100,
108
- height: 100,
109
- rotation: 0,
110
- zIndex: 0,
111
- locked: false,
112
- hidden: false,
113
- createdAt: now,
114
- updatedAt: now,
115
- style: {},
116
- data: {},
117
- ...partial,
118
- };
119
- }