viteboard 0.1.1 → 0.1.3

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 (83) hide show
  1. package/bin/content-api-plugin.mjs +722 -0
  2. package/bin/viteboard.mjs +3 -7
  3. package/content/docs/index.md +1 -1
  4. package/content/docs/product-roadmap.board.json +4 -4
  5. package/content/machine/board-v1.schema.json +138 -0
  6. package/content/machine/example.board.json +217 -0
  7. package/content/machine/guide.md +270 -0
  8. package/content/machine/llms.txt +31 -0
  9. package/content/machine/viteboard.json +62 -0
  10. package/dist/assets/index-D2W76uu9.js +96 -0
  11. package/dist/assets/index-rMkUjGXP.css +1 -0
  12. package/dist/index.html +4 -2
  13. package/package.json +10 -12
  14. package/vite.config.ts +619 -38
  15. package/dist/assets/index-C30zRPsW.js +0 -60
  16. package/dist/assets/index-e5aqtN8W.css +0 -1
  17. package/index.html +0 -13
  18. package/src/app/AppController.ts +0 -962
  19. package/src/app/BoardState.ts +0 -130
  20. package/src/app/ClipboardService.ts +0 -159
  21. package/src/app/CommandManager.ts +0 -66
  22. package/src/app/ElementActions.ts +0 -152
  23. package/src/app/EmbedRegionSelector.ts +0 -103
  24. package/src/app/ExportPng.ts +0 -107
  25. package/src/app/ImageInsertService.ts +0 -141
  26. package/src/app/KeyboardShortcuts.ts +0 -124
  27. package/src/app/main.ts +0 -6
  28. package/src/board/BoardPreview.ts +0 -189
  29. package/src/board/BoardService.ts +0 -222
  30. package/src/canvas/CanvasRenderer.ts +0 -253
  31. package/src/canvas/CanvasSurface.ts +0 -70
  32. package/src/canvas/HitTester.ts +0 -110
  33. package/src/canvas/ImageCache.ts +0 -123
  34. package/src/canvas/PerformanceMonitor.ts +0 -31
  35. package/src/canvas/RenderScheduler.ts +0 -26
  36. package/src/canvas/Viewport.ts +0 -77
  37. package/src/content/ContentApi.ts +0 -258
  38. package/src/content/Markdown.ts +0 -431
  39. package/src/content/MarkdownView.ts +0 -70
  40. package/src/editor/DocEditor.ts +0 -799
  41. package/src/editor/htmlToMarkdown.ts +0 -333
  42. package/src/elements/renderElement.ts +0 -598
  43. package/src/elements/types.ts +0 -119
  44. package/src/shell/Shell.ts +0 -2948
  45. package/src/shell/Sidebar.ts +0 -1651
  46. package/src/storage/AssetStore.ts +0 -86
  47. package/src/storage/BoardSerializer.ts +0 -114
  48. package/src/storage/ImportExportService.ts +0 -153
  49. package/src/storage/IndexedDbStore.ts +0 -92
  50. package/src/storage/LocalBoardStore.ts +0 -104
  51. package/src/styles.css +0 -3332
  52. package/src/templates/handoffTemplate.ts +0 -34
  53. package/src/templates/helpers.ts +0 -132
  54. package/src/templates/index.ts +0 -79
  55. package/src/templates/journeyMapTemplate.ts +0 -52
  56. package/src/templates/opportunityTreeTemplate.ts +0 -45
  57. package/src/templates/personaTemplate.ts +0 -41
  58. package/src/templates/prioritizationMatrixTemplate.ts +0 -44
  59. package/src/templates/requirementsFlowTemplate.ts +0 -45
  60. package/src/templates/screenshotReviewTemplate.ts +0 -52
  61. package/src/templates/systemDiagramTemplate.ts +0 -41
  62. package/src/templates/workflowTemplate.ts +0 -35
  63. package/src/testing/StressTestGenerator.ts +0 -134
  64. package/src/testing/StressTestPanel.ts +0 -64
  65. package/src/tools/ArrowTool.ts +0 -97
  66. package/src/tools/ImageTool.ts +0 -39
  67. package/src/tools/PanTool.ts +0 -34
  68. package/src/tools/SelectTool.ts +0 -377
  69. package/src/tools/ShapeTool.ts +0 -106
  70. package/src/tools/StickyTool.ts +0 -69
  71. package/src/tools/TaskTool.ts +0 -52
  72. package/src/tools/TextTool.ts +0 -45
  73. package/src/tools/ToolContext.ts +0 -51
  74. package/src/tools/ToolController.ts +0 -178
  75. package/src/ui/Modal.ts +0 -58
  76. package/src/ui/PerformanceOverlay.ts +0 -75
  77. package/src/ui/StorageManager.ts +0 -94
  78. package/src/ui/TemplatePicker.ts +0 -67
  79. package/src/ui/TextEditorOverlay.ts +0 -262
  80. package/src/ui/Toolbar.ts +0 -189
  81. package/src/ui/TopControls.ts +0 -137
  82. package/src/ui/icons.ts +0 -56
  83. package/tsconfig.json +0 -19
@@ -1,377 +0,0 @@
1
- import type { Tool, ToolContext } from "./ToolContext";
2
- import type { BoardElement } from "../elements/types";
3
- import { screenToWorld } from "../canvas/Viewport";
4
- import { handlePoints, type HandleId } from "../canvas/CanvasRenderer";
5
- import { invalidateTextCache } from "../elements/renderElement";
6
-
7
- type DragMode = "none" | "move" | "marquee" | "resize" | "endpoint";
8
-
9
- type Snapshot = { id: string; x: number; y: number; width: number; height: number };
10
-
11
- const HANDLE_HIT_RADIUS = 7;
12
-
13
- const HANDLE_CURSORS: Record<HandleId, string> = {
14
- nw: "nwse-resize",
15
- n: "ns-resize",
16
- ne: "nesw-resize",
17
- e: "ew-resize",
18
- se: "nwse-resize",
19
- s: "ns-resize",
20
- sw: "nesw-resize",
21
- w: "ew-resize",
22
- };
23
-
24
- export class SelectTool implements Tool {
25
- name = "select" as const;
26
- cursor = "default";
27
-
28
- private mode: DragMode = "none";
29
- private startScreen = { x: 0, y: 0 };
30
- private startWorld = { x: 0, y: 0 };
31
- private snapshots: Snapshot[] = [];
32
- private activeHandle: HandleId | null = null;
33
- private endpointIsStart = false;
34
- private moved = false;
35
- private pointerDownHitId: string | null = null;
36
-
37
- onPointerDown(e: PointerEvent, ctx: ToolContext): void {
38
- const { state, hitTester, renderer } = ctx;
39
- this.startScreen = { x: e.clientX, y: e.clientY };
40
- this.startWorld = screenToWorld(e.clientX, e.clientY, state.viewport);
41
- this.moved = false;
42
-
43
- // 1. resize handle on current selection?
44
- const handle = this.hitHandle(e, ctx);
45
- if (handle) {
46
- this.mode = "resize";
47
- this.activeHandle = handle;
48
- this.takeSnapshots(state.getSelected());
49
- return;
50
- }
51
-
52
- // 2. arrow/line endpoint?
53
- const selected = state.getSelected();
54
- if (selected.length === 1 && this.isLinear(selected[0])) {
55
- const ep = this.hitEndpoint(e, selected[0], ctx);
56
- if (ep !== null) {
57
- this.mode = "endpoint";
58
- this.endpointIsStart = ep;
59
- this.takeSnapshots(selected);
60
- return;
61
- }
62
- }
63
-
64
- // 3. element under cursor?
65
- const hit = hitTester.hitTest(this.startWorld.x, this.startWorld.y, state.viewport.scale);
66
- if (hit) {
67
- this.pointerDownHitId = hit.id;
68
- if (e.shiftKey) {
69
- if (state.selectedIds.has(hit.id)) state.selectedIds.delete(hit.id);
70
- else state.selectedIds.add(hit.id);
71
- } else if (!state.selectedIds.has(hit.id)) {
72
- state.clearSelection();
73
- state.selectedIds.add(hit.id);
74
- }
75
- this.mode = "move";
76
- this.takeSnapshots(state.getSelected());
77
- state.requestRender();
78
- return;
79
- }
80
-
81
- // 4. empty canvas: marquee
82
- if (!e.shiftKey) state.clearSelection();
83
- this.mode = "marquee";
84
- renderer.overlay.marquee = { x: e.clientX, y: e.clientY, w: 0, h: 0 };
85
- state.requestRender();
86
- }
87
-
88
- onPointerMove(e: PointerEvent, ctx: ToolContext): void {
89
- const { state, renderer, hitTester } = ctx;
90
-
91
- if (this.mode === "none") {
92
- // hover feedback + handle cursors
93
- const handle = this.hitHandle(e, ctx);
94
- if (handle) {
95
- ctx.setCursor(HANDLE_CURSORS[handle]);
96
- return;
97
- }
98
- const world = screenToWorld(e.clientX, e.clientY, state.viewport);
99
- const hit = hitTester.hitTest(world.x, world.y, state.viewport.scale);
100
- const newHover = hit ? hit.id : null;
101
- ctx.setCursor(hit ? "move" : "default");
102
- if (newHover !== state.hoveredId) {
103
- state.hoveredId = newHover;
104
- state.requestRender();
105
- }
106
- return;
107
- }
108
-
109
- const dxScreen = e.clientX - this.startScreen.x;
110
- const dyScreen = e.clientY - this.startScreen.y;
111
- if (Math.abs(dxScreen) + Math.abs(dyScreen) > 2) this.moved = true;
112
-
113
- const dx = dxScreen / state.viewport.scale;
114
- const dy = dyScreen / state.viewport.scale;
115
-
116
- if (this.mode === "move") {
117
- for (const snap of this.snapshots) {
118
- const el = state.elementsById.get(snap.id);
119
- if (!el || el.locked) continue;
120
- el.x = snap.x + dx;
121
- el.y = snap.y + dy;
122
- }
123
- state.requestRender();
124
- } else if (this.mode === "marquee") {
125
- renderer.overlay.marquee = {
126
- x: this.startScreen.x,
127
- y: this.startScreen.y,
128
- w: dxScreen,
129
- h: dyScreen,
130
- };
131
- state.requestRender();
132
- } else if (this.mode === "resize" && this.activeHandle) {
133
- this.applyResize(dx, dy, e.shiftKey, ctx);
134
- state.requestRender();
135
- } else if (this.mode === "endpoint") {
136
- const snap = this.snapshots[0];
137
- const el = state.elementsById.get(snap.id);
138
- if (el) {
139
- if (this.endpointIsStart) {
140
- el.x = snap.x + dx;
141
- el.y = snap.y + dy;
142
- el.width = snap.width - dx;
143
- el.height = snap.height - dy;
144
- } else {
145
- el.width = snap.width + dx;
146
- el.height = snap.height + dy;
147
- }
148
- }
149
- state.requestRender();
150
- }
151
- }
152
-
153
- onPointerUp(e: PointerEvent, ctx: ToolContext): void {
154
- const { state, renderer, hitTester, commands } = ctx;
155
-
156
- if (this.mode === "marquee") {
157
- const m = renderer.overlay.marquee;
158
- renderer.overlay.marquee = null;
159
- if (m && (Math.abs(m.w) > 3 || Math.abs(m.h) > 3)) {
160
- const a = screenToWorld(m.x, m.y, state.viewport);
161
- const b = screenToWorld(m.x + m.w, m.y + m.h, state.viewport);
162
- const hits = hitTester.elementsInRect(a.x, a.y, b.x - a.x, b.y - a.y);
163
- for (const el of hits) state.selectedIds.add(el.id);
164
- }
165
- state.requestRender();
166
- } else if (
167
- (this.mode === "move" || this.mode === "resize" || this.mode === "endpoint") &&
168
- this.moved
169
- ) {
170
- // push undo command with before/after snapshots
171
- const before = this.snapshots;
172
- const after: Snapshot[] = before.map((s) => {
173
- const el = state.elementsById.get(s.id)!;
174
- return { id: s.id, x: el.x, y: el.y, width: el.width, height: el.height };
175
- });
176
- const apply = (snaps: Snapshot[]) => {
177
- for (const s of snaps) {
178
- const el = state.elementsById.get(s.id);
179
- if (!el) continue;
180
- el.x = s.x;
181
- el.y = s.y;
182
- el.width = s.width;
183
- el.height = s.height;
184
- el.updatedAt = Date.now();
185
- invalidateTextCache(el.id);
186
- }
187
- state.emitChange();
188
- };
189
- for (const s of after) {
190
- const el = state.elementsById.get(s.id);
191
- if (el) {
192
- el.updatedAt = Date.now();
193
- invalidateTextCache(el.id);
194
- }
195
- }
196
- commands.push(
197
- this.mode,
198
- () => apply(after),
199
- () => apply(before)
200
- );
201
- state.emitChange();
202
- } else if (this.mode === "move" && !this.moved) {
203
- this.toggleTaskIfCheckboxClick(e, ctx);
204
- }
205
-
206
- this.mode = "none";
207
- this.activeHandle = null;
208
- this.snapshots = [];
209
- this.pointerDownHitId = null;
210
- }
211
-
212
- onDoubleClick(e: PointerEvent, ctx: ToolContext): void {
213
- const { state, hitTester } = ctx;
214
- const world = screenToWorld(e.clientX, e.clientY, state.viewport);
215
- const hit = hitTester.hitTest(world.x, world.y, state.viewport.scale);
216
- if (hit && (hit.type === "sticky" || hit.type === "task" || hit.type === "text" || hit.type === "rectangle" || hit.type === "ellipse" || hit.type === "frame" || hit.type === "arrow" || hit.type === "line" || hit.type === "connector")) {
217
- ctx.openTextEditor(hit);
218
- }
219
- }
220
-
221
- onCancel(ctx: ToolContext): void {
222
- // restore snapshots if mid-drag
223
- if (this.mode !== "none" && this.snapshots.length > 0) {
224
- for (const s of this.snapshots) {
225
- const el = ctx.state.elementsById.get(s.id);
226
- if (!el) continue;
227
- el.x = s.x;
228
- el.y = s.y;
229
- el.width = s.width;
230
- el.height = s.height;
231
- }
232
- }
233
- ctx.renderer.overlay.marquee = null;
234
- this.mode = "none";
235
- ctx.state.clearSelection();
236
- ctx.state.requestRender();
237
- }
238
-
239
- // ---------- helpers ----------
240
-
241
- private isLinear(el: BoardElement): boolean {
242
- return el.type === "line" || el.type === "arrow" || el.type === "connector";
243
- }
244
-
245
- private takeSnapshots(elements: BoardElement[]): void {
246
- this.snapshots = elements.map((el) => ({
247
- id: el.id,
248
- x: el.x,
249
- y: el.y,
250
- width: el.width,
251
- height: el.height,
252
- }));
253
- }
254
-
255
- private toggleTaskIfCheckboxClick(e: PointerEvent, ctx: ToolContext): void {
256
- const { state, hitTester, commands } = ctx;
257
- const world = screenToWorld(e.clientX, e.clientY, state.viewport);
258
- const hit = hitTester.hitTest(world.x, world.y, state.viewport.scale);
259
- if (!hit || hit.id !== this.pointerDownHitId || hit.type !== "task") return;
260
- if (!this.hitTaskCheckbox(world.x, world.y, hit)) return;
261
- const before = hit.data.checked === true;
262
- const after = !before;
263
- const id = hit.id;
264
- const apply = (checked: boolean) => {
265
- const el = state.elementsById.get(id);
266
- if (!el) return;
267
- el.data.checked = checked;
268
- el.updatedAt = Date.now();
269
- state.emitChange();
270
- };
271
- commands.execute("toggle-task", () => apply(after), () => apply(before));
272
- }
273
-
274
- private hitTaskCheckbox(wx: number, wy: number, el: BoardElement): boolean {
275
- const boxSize = Math.min(22, Math.max(16, el.height * 0.38));
276
- const boxX = el.x + 16;
277
- const boxY = el.y + el.height / 2 - boxSize / 2;
278
- const pad = 8;
279
- return wx >= boxX - pad && wx <= boxX + boxSize + pad && wy >= boxY - pad && wy <= boxY + boxSize + pad;
280
- }
281
-
282
- private hitHandle(e: PointerEvent, ctx: ToolContext): HandleId | null {
283
- const { state, renderer } = ctx;
284
- const selected = state.getSelected();
285
- if (selected.length === 0) return null;
286
- if (selected.length === 1 && this.isLinear(selected[0])) return null;
287
-
288
- let bbox;
289
- if (selected.length === 1) {
290
- bbox = renderer.elementScreenBBox(selected[0]);
291
- } else {
292
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
293
- for (const el of selected) {
294
- const b = renderer.elementScreenBBox(el);
295
- minX = Math.min(minX, b.x);
296
- minY = Math.min(minY, b.y);
297
- maxX = Math.max(maxX, b.x + b.w);
298
- maxY = Math.max(maxY, b.y + b.h);
299
- }
300
- bbox = { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
301
- }
302
-
303
- for (const p of handlePoints(bbox)) {
304
- if (
305
- Math.abs(e.clientX - p.x) <= HANDLE_HIT_RADIUS &&
306
- Math.abs(e.clientY - p.y) <= HANDLE_HIT_RADIUS
307
- ) {
308
- return p.id;
309
- }
310
- }
311
- return null;
312
- }
313
-
314
- private hitEndpoint(e: PointerEvent, el: BoardElement, ctx: ToolContext): boolean | null {
315
- const vp = ctx.state.viewport;
316
- const sx1 = el.x * vp.scale + vp.offsetX;
317
- const sy1 = el.y * vp.scale + vp.offsetY;
318
- const sx2 = (el.x + el.width) * vp.scale + vp.offsetX;
319
- const sy2 = (el.y + el.height) * vp.scale + vp.offsetY;
320
- if (Math.hypot(e.clientX - sx1, e.clientY - sy1) <= HANDLE_HIT_RADIUS + 2) return true;
321
- if (Math.hypot(e.clientX - sx2, e.clientY - sy2) <= HANDLE_HIT_RADIUS + 2) return false;
322
- return null;
323
- }
324
-
325
- private applyResize(dx: number, dy: number, preserveAspect: boolean, ctx: ToolContext): void {
326
- const { state } = ctx;
327
- const h = this.activeHandle!;
328
-
329
- // group bbox in world coords from snapshots
330
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
331
- for (const s of this.snapshots) {
332
- minX = Math.min(minX, Math.min(s.x, s.x + s.width));
333
- minY = Math.min(minY, Math.min(s.y, s.y + s.height));
334
- maxX = Math.max(maxX, Math.max(s.x, s.x + s.width));
335
- maxY = Math.max(maxY, Math.max(s.y, s.y + s.height));
336
- }
337
- const ow = maxX - minX;
338
- const oh = maxY - minY;
339
- if (ow === 0 || oh === 0) return;
340
-
341
- let nx = minX, ny = minY, nw = ow, nh = oh;
342
- if (h.includes("e")) nw = ow + dx;
343
- if (h.includes("w")) { nx = minX + dx; nw = ow - dx; }
344
- if (h.includes("s")) nh = oh + dy;
345
- if (h.includes("n")) { ny = minY + dy; nh = oh - dy; }
346
-
347
- if (preserveAspect && (h === "nw" || h === "ne" || h === "se" || h === "sw")) {
348
- const ratio = ow / oh;
349
- if (Math.abs(nw / ow) > Math.abs(nh / oh)) {
350
- const newH = nw / ratio;
351
- if (h.includes("n")) ny = ny + (nh - newH);
352
- nh = newH;
353
- } else {
354
- const newW = nh * ratio;
355
- if (h.includes("w")) nx = nx + (nw - newW);
356
- nw = newW;
357
- }
358
- }
359
-
360
- const MIN = 4;
361
- if (Math.abs(nw) < MIN || Math.abs(nh) < MIN) return;
362
-
363
- const scaleX = nw / ow;
364
- const scaleY = nh / oh;
365
-
366
- for (const s of this.snapshots) {
367
- const el = state.elementsById.get(s.id);
368
- if (!el || el.locked) continue;
369
- el.x = nx + (s.x - minX) * scaleX;
370
- el.y = ny + (s.y - minY) * scaleY;
371
- el.width = s.width * scaleX;
372
- el.height = s.height * scaleY;
373
- el.updatedAt = Date.now();
374
- invalidateTextCache(el.id);
375
- }
376
- }
377
- }
@@ -1,106 +0,0 @@
1
- import type { Tool, ToolContext } from "./ToolContext";
2
- import type { ToolName } from "./ToolContext";
3
- import { createElement, type BoardElement, type ElementType } from "../elements/types";
4
- import { screenToWorld } from "../canvas/Viewport";
5
-
6
- type ShapeToolName = Extract<ToolName, "rectangle" | "ellipse" | "diamond" | "triangle" | "pill" | "chevron" | "parallelogram" | "hexagon" | "cylinder">;
7
-
8
- export class ShapeTool implements Tool {
9
- name: ShapeToolName;
10
- cursor = "crosshair";
11
-
12
- private shapeType: ElementType;
13
- private shapeKind: ShapeToolName;
14
- private drawing = false;
15
- private startWorld = { x: 0, y: 0 };
16
- private preview: BoardElement | null = null;
17
-
18
- constructor(name: ShapeToolName) {
19
- this.name = name;
20
- this.shapeKind = name;
21
- this.shapeType = name === "ellipse" ? "ellipse" : "rectangle";
22
- }
23
-
24
- onPointerDown(e: PointerEvent, ctx: ToolContext): void {
25
- const { state } = ctx;
26
- this.drawing = true;
27
- this.startWorld = screenToWorld(e.clientX, e.clientY, state.viewport);
28
- const isDark = state.theme === "dark";
29
- const isGraphicShape = !["rectangle", "ellipse", "pill", "cylinder"].includes(this.shapeKind);
30
- this.preview = createElement(this.shapeType, {
31
- x: this.startWorld.x,
32
- y: this.startWorld.y,
33
- width: 0,
34
- height: 0,
35
- zIndex: state.maxZIndex() + 1,
36
- style: {
37
- fill: isGraphicShape ? "#0078d4" : isDark ? "#2a2d33" : "#ffffff",
38
- stroke: isGraphicShape ? "#495057" : isDark ? "#9ca3af" : "#374151",
39
- strokeWidth: isGraphicShape ? 0 : 1.5,
40
- borderRadius: this.shapeKind === "pill" ? 999 : this.shapeKind === "rectangle" ? 8 : 0,
41
- fontSize: 14,
42
- textColor: isGraphicShape ? "#ffffff" : isDark ? "#f2f2f2" : "#1f2933",
43
- },
44
- data: { text: "", shapeKind: this.shapeKind },
45
- });
46
- ctx.renderer.overlay.preview = this.preview;
47
- }
48
-
49
- onPointerMove(e: PointerEvent, ctx: ToolContext): void {
50
- if (!this.drawing || !this.preview) return;
51
- const world = screenToWorld(e.clientX, e.clientY, ctx.state.viewport);
52
- let w = world.x - this.startWorld.x;
53
- let h = world.y - this.startWorld.y;
54
- if (e.shiftKey) {
55
- const size = Math.max(Math.abs(w), Math.abs(h));
56
- w = Math.sign(w || 1) * size;
57
- h = Math.sign(h || 1) * size;
58
- }
59
- this.preview.x = Math.min(this.startWorld.x, this.startWorld.x + w);
60
- this.preview.y = Math.min(this.startWorld.y, this.startWorld.y + h);
61
- this.preview.width = Math.abs(w);
62
- this.preview.height = Math.abs(h);
63
- ctx.state.requestRender();
64
- }
65
-
66
- onPointerUp(_e: PointerEvent, ctx: ToolContext): void {
67
- const { state, commands } = ctx;
68
- if (!this.drawing || !this.preview) return;
69
- this.drawing = false;
70
- ctx.renderer.overlay.preview = null;
71
-
72
- const el = this.preview;
73
- this.preview = null;
74
-
75
- if (el.width < 4 || el.height < 4) {
76
- // tiny click: create a default-size shape
77
- el.width = this.shapeKind === "chevron" ? 210 : 160;
78
- el.height = this.shapeKind === "chevron" ? 76 : 100;
79
- el.x -= el.width / 2;
80
- el.y -= el.height / 2;
81
- }
82
-
83
- commands.execute(
84
- "create-shape",
85
- () => {
86
- if (!state.elementsById.has(el.id)) state.addElement(el);
87
- state.emitChange();
88
- },
89
- () => {
90
- state.removeElement(el.id);
91
- state.emitChange();
92
- }
93
- );
94
-
95
- state.clearSelection();
96
- state.selectedIds.add(el.id);
97
- ctx.setTool("select");
98
- }
99
-
100
- onCancel(ctx: ToolContext): void {
101
- this.drawing = false;
102
- this.preview = null;
103
- ctx.renderer.overlay.preview = null;
104
- ctx.state.requestRender();
105
- }
106
- }
@@ -1,69 +0,0 @@
1
- import type { Tool, ToolContext } from "./ToolContext";
2
- import { createElement } from "../elements/types";
3
- import { screenToWorld } from "../canvas/Viewport";
4
-
5
- export const STICKY_COLORS = [
6
- "#fef08a",
7
- "#fdba74",
8
- "#fca5a5",
9
- "#f9a8d4",
10
- "#d8b4fe",
11
- "#a5b4fc",
12
- "#93c5fd",
13
- "#86efac",
14
- ];
15
-
16
- let lastStickyColor = STICKY_COLORS[0];
17
-
18
- export function getLastStickyColor(): string {
19
- return lastStickyColor;
20
- }
21
-
22
- export function setLastStickyColor(color: string): void {
23
- lastStickyColor = color;
24
- }
25
-
26
- export class StickyTool implements Tool {
27
- name = "sticky" as const;
28
- cursor = "crosshair";
29
-
30
- onPointerDown(e: PointerEvent, ctx: ToolContext): void {
31
- const { state, commands } = ctx;
32
- const world = screenToWorld(e.clientX, e.clientY, state.viewport);
33
- const el = createElement("sticky", {
34
- x: world.x - 110,
35
- y: world.y - 80,
36
- width: 220,
37
- height: 160,
38
- zIndex: state.maxZIndex() + 1,
39
- style: {
40
- fill: getLastStickyColor(),
41
- borderRadius: 12,
42
- shadow: true,
43
- fontSize: 15,
44
- textColor: "#1f2933",
45
- },
46
- data: { text: "" },
47
- });
48
-
49
- commands.execute(
50
- "create-sticky",
51
- () => {
52
- if (!state.elementsById.has(el.id)) state.addElement(el);
53
- state.emitChange();
54
- },
55
- () => {
56
- state.removeElement(el.id);
57
- state.emitChange();
58
- }
59
- );
60
-
61
- state.clearSelection();
62
- state.selectedIds.add(el.id);
63
- ctx.setTool("select");
64
- ctx.openTextEditor(el);
65
- }
66
-
67
- onPointerMove(_e: PointerEvent, _ctx: ToolContext): void {}
68
- onPointerUp(_e: PointerEvent, _ctx: ToolContext): void {}
69
- }
@@ -1,52 +0,0 @@
1
- import type { Tool, ToolContext } from "./ToolContext";
2
- import { createElement } from "../elements/types";
3
- import { screenToWorld } from "../canvas/Viewport";
4
-
5
- export class TaskTool implements Tool {
6
- name = "task" as const;
7
- cursor = "crosshair";
8
-
9
- onPointerDown(e: PointerEvent, ctx: ToolContext): void {
10
- const { state, commands } = ctx;
11
- const world = screenToWorld(e.clientX, e.clientY, state.viewport);
12
- const isDark = state.theme === "dark";
13
- const el = createElement("task", {
14
- x: world.x - 140,
15
- y: world.y - 28,
16
- width: 280,
17
- height: 56,
18
- zIndex: state.maxZIndex() + 1,
19
- style: {
20
- fill: isDark ? "#17191b" : "#ffffff",
21
- stroke: isDark ? "rgba(255,255,255,0.12)" : "rgba(0,0,0,0.09)",
22
- strokeWidth: 1,
23
- borderRadius: 14,
24
- shadow: true,
25
- fontSize: 15,
26
- fontWeight: "650",
27
- textColor: isDark ? "#f6f6f3" : "#252829",
28
- },
29
- data: { text: "Task title", checked: false },
30
- });
31
-
32
- commands.execute(
33
- "create-task",
34
- () => {
35
- if (!state.elementsById.has(el.id)) state.addElement(el);
36
- state.emitChange();
37
- },
38
- () => {
39
- state.removeElement(el.id);
40
- state.emitChange();
41
- }
42
- );
43
-
44
- state.clearSelection();
45
- state.selectedIds.add(el.id);
46
- ctx.setTool("select");
47
- ctx.openTextEditor(el);
48
- }
49
-
50
- onPointerMove(_e: PointerEvent, _ctx: ToolContext): void {}
51
- onPointerUp(_e: PointerEvent, _ctx: ToolContext): void {}
52
- }
@@ -1,45 +0,0 @@
1
- import type { Tool, ToolContext } from "./ToolContext";
2
- import { createElement } from "../elements/types";
3
- import { screenToWorld } from "../canvas/Viewport";
4
-
5
- export class TextTool implements Tool {
6
- name = "text" as const;
7
- cursor = "text";
8
-
9
- onPointerDown(e: PointerEvent, ctx: ToolContext): void {
10
- const { state, commands } = ctx;
11
- const world = screenToWorld(e.clientX, e.clientY, state.viewport);
12
- const el = createElement("text", {
13
- x: world.x,
14
- y: world.y - 12,
15
- width: 240,
16
- height: 28,
17
- zIndex: state.maxZIndex() + 1,
18
- style: {
19
- fontSize: 16,
20
- textColor: state.theme === "dark" ? "#f2f2f2" : "#1f2933",
21
- },
22
- data: { text: "" },
23
- });
24
-
25
- commands.execute(
26
- "create-text",
27
- () => {
28
- if (!state.elementsById.has(el.id)) state.addElement(el);
29
- state.emitChange();
30
- },
31
- () => {
32
- state.removeElement(el.id);
33
- state.emitChange();
34
- }
35
- );
36
-
37
- state.clearSelection();
38
- state.selectedIds.add(el.id);
39
- ctx.setTool("select");
40
- ctx.openTextEditor(el);
41
- }
42
-
43
- onPointerMove(_e: PointerEvent, _ctx: ToolContext): void {}
44
- onPointerUp(_e: PointerEvent, _ctx: ToolContext): void {}
45
- }
@@ -1,51 +0,0 @@
1
- import type { BoardState } from "../app/BoardState";
2
- import type { CommandManager } from "../app/CommandManager";
3
- import type { CanvasRenderer } from "../canvas/CanvasRenderer";
4
- import type { CanvasSurface } from "../canvas/CanvasSurface";
5
- import type { HitTester } from "../canvas/HitTester";
6
- import type { BoardElement } from "../elements/types";
7
-
8
- export type ToolName =
9
- | "select"
10
- | "hand"
11
- | "sticky"
12
- | "task"
13
- | "rectangle"
14
- | "ellipse"
15
- | "diamond"
16
- | "triangle"
17
- | "pill"
18
- | "chevron"
19
- | "parallelogram"
20
- | "hexagon"
21
- | "cylinder"
22
- | "text"
23
- | "arrow"
24
- | "dashedArrow"
25
- | "twoWayArrow"
26
- | "labeledArrow"
27
- | "sidedLabelArrow"
28
- | "image";
29
-
30
- export type ToolContext = {
31
- state: BoardState;
32
- renderer: CanvasRenderer;
33
- surface: CanvasSurface;
34
- hitTester: HitTester;
35
- commands: CommandManager;
36
- setTool: (tool: ToolName) => void;
37
- setCursor: (cursor: string) => void;
38
- openTextEditor: (el: BoardElement) => void;
39
- };
40
-
41
- export interface Tool {
42
- name: ToolName;
43
- cursor: string;
44
- onPointerDown(e: PointerEvent, ctx: ToolContext): void;
45
- onPointerMove(e: PointerEvent, ctx: ToolContext): void;
46
- onPointerUp(e: PointerEvent, ctx: ToolContext): void;
47
- onDoubleClick?(e: PointerEvent, ctx: ToolContext): void;
48
- onCancel?(ctx: ToolContext): void;
49
- onActivate?(ctx: ToolContext): void;
50
- onDeactivate?(ctx: ToolContext): void;
51
- }