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,178 +0,0 @@
1
- import type { Tool, ToolContext, ToolName } from "./ToolContext";
2
- import { zoomAt } from "../canvas/Viewport";
3
-
4
- const LINE_HEIGHT_PX = 16;
5
- const PAGE_HEIGHT_PX = 720;
6
- const MAX_WHEEL_DELTA = 120;
7
- const ZOOM_SENSITIVITY = 0.0032;
8
-
9
- export class ToolController {
10
- private tools = new Map<ToolName, Tool>();
11
- private active: Tool;
12
- private ctx: ToolContext;
13
- private target: HTMLElement;
14
-
15
- /** spacebar / middle-mouse temporary pan */
16
- private spaceDown = false;
17
- private tempPanning = false;
18
- private panLast = { x: 0, y: 0 };
19
-
20
- private toolChangeListeners: ((tool: ToolName) => void)[] = [];
21
- private signal?: AbortSignal;
22
-
23
- constructor(target: HTMLElement, ctx: ToolContext, tools: Tool[], signal?: AbortSignal) {
24
- this.target = target;
25
- this.ctx = ctx;
26
- this.signal = signal;
27
- for (const tool of tools) this.tools.set(tool.name, tool);
28
- this.active = this.tools.get("select")!;
29
- this.bindEvents();
30
- }
31
-
32
- get activeName(): ToolName {
33
- return this.active.name;
34
- }
35
-
36
- onToolChange(fn: (tool: ToolName) => void): void {
37
- this.toolChangeListeners.push(fn);
38
- }
39
-
40
- setTool(name: ToolName): void {
41
- if (this.active.name === name) {
42
- // re-trigger activate for tools like image picker
43
- this.active.onActivate?.(this.ctx);
44
- return;
45
- }
46
- this.active.onDeactivate?.(this.ctx);
47
- const tool = this.tools.get(name);
48
- if (!tool) return;
49
- this.active = tool;
50
- this.ctx.setCursor(tool.cursor);
51
- tool.onActivate?.(this.ctx);
52
- for (const fn of this.toolChangeListeners) fn(name);
53
- }
54
-
55
- cancel(): void {
56
- this.active.onCancel?.(this.ctx);
57
- }
58
-
59
- private bindEvents(): void {
60
- const t = this.target;
61
-
62
- t.addEventListener("pointerdown", (e) => {
63
- // middle mouse, right mouse, or spacebar temporarily pan.
64
- if (e.button === 1 || e.button === 2 || this.spaceDown) {
65
- e.preventDefault();
66
- this.tempPanning = true;
67
- this.panLast = { x: e.clientX, y: e.clientY };
68
- this.ctx.setCursor("grabbing");
69
- t.setPointerCapture(e.pointerId);
70
- return;
71
- }
72
- if (e.button !== 0) return;
73
- t.setPointerCapture(e.pointerId);
74
- this.active.onPointerDown(e, this.ctx);
75
- });
76
-
77
- t.addEventListener("pointermove", (e) => {
78
- if (this.tempPanning) {
79
- const vp = this.ctx.state.viewport;
80
- vp.offsetX += e.clientX - this.panLast.x;
81
- vp.offsetY += e.clientY - this.panLast.y;
82
- this.panLast = { x: e.clientX, y: e.clientY };
83
- this.ctx.state.requestRender();
84
- return;
85
- }
86
- this.active.onPointerMove(e, this.ctx);
87
- });
88
-
89
- t.addEventListener("pointerup", (e) => {
90
- if (this.tempPanning) {
91
- this.tempPanning = false;
92
- this.ctx.setCursor(this.spaceDown ? "grab" : this.active.cursor);
93
- return;
94
- }
95
- this.active.onPointerUp(e, this.ctx);
96
- });
97
-
98
- t.addEventListener("pointercancel", () => {
99
- if (!this.tempPanning) return;
100
- this.tempPanning = false;
101
- this.ctx.setCursor(this.spaceDown ? "grab" : this.active.cursor);
102
- });
103
-
104
- t.addEventListener("dblclick", (e) => {
105
- this.active.onDoubleClick?.(e as unknown as PointerEvent, this.ctx);
106
- });
107
-
108
- // wheel: zoom (ctrl/cmd or pinch) and trackpad pan
109
- t.addEventListener(
110
- "wheel",
111
- (e) => {
112
- e.preventDefault();
113
- const vp = this.ctx.state.viewport;
114
- const dx = normalizeWheelDelta(e.deltaX, e.deltaMode, this.ctx.state.viewport.scale);
115
- const dy = normalizeWheelDelta(e.deltaY, e.deltaMode, this.ctx.state.viewport.scale);
116
- if (e.ctrlKey || e.metaKey) {
117
- // pinch-zoom or ctrl+wheel: cursor-centered zoom with normalized deltas
118
- const factor = Math.exp(-clampWheelDelta(dy) * ZOOM_SENSITIVITY);
119
- zoomAt(vp, e.clientX, e.clientY, vp.scale * factor);
120
- } else {
121
- // trackpad two-finger pan / mouse wheel scroll.
122
- // Shift+wheel pans horizontally to make wide boards easier to navigate.
123
- if (e.shiftKey && Math.abs(dx) < Math.abs(dy)) {
124
- vp.offsetX -= dy;
125
- } else {
126
- vp.offsetX -= dx;
127
- vp.offsetY -= dy;
128
- }
129
- }
130
- this.ctx.state.requestRender();
131
- },
132
- { passive: false }
133
- );
134
-
135
- // spacebar temporary hand tool
136
- window.addEventListener("keydown", (e) => {
137
- if (e.code === "Space" && !this.isEditingText(e)) {
138
- if (!this.spaceDown) {
139
- this.spaceDown = true;
140
- if (!this.tempPanning) this.ctx.setCursor("grab");
141
- }
142
- e.preventDefault();
143
- }
144
- }, { signal: this.signal });
145
-
146
- window.addEventListener("keyup", (e) => {
147
- if (e.code === "Space") {
148
- this.spaceDown = false;
149
- if (!this.tempPanning) this.ctx.setCursor(this.active.cursor);
150
- }
151
- }, { signal: this.signal });
152
-
153
- // prevent context menu so right-drag can pan cleanly.
154
- t.addEventListener("contextmenu", (e) => e.preventDefault());
155
- }
156
-
157
- private isEditingText(e: KeyboardEvent): boolean {
158
- const target = e.target as HTMLElement | null;
159
- return (
160
- !!target &&
161
- (target.tagName === "TEXTAREA" ||
162
- target.tagName === "INPUT" ||
163
- target.isContentEditable)
164
- );
165
- }
166
- }
167
-
168
- function normalizeWheelDelta(delta: number, deltaMode: number, scale: number): number {
169
- const modeFactor = deltaMode === 1 ? LINE_HEIGHT_PX : deltaMode === 2 ? PAGE_HEIGHT_PX : 1;
170
- const pixels = delta * modeFactor;
171
- // When zoomed far out, slightly reduce pan velocity to avoid overshooting.
172
- const zoomFriction = scale < 0.5 ? 0.8 : scale > 2 ? 1.08 : 1;
173
- return pixels * zoomFriction;
174
- }
175
-
176
- function clampWheelDelta(delta: number): number {
177
- return Math.max(-MAX_WHEEL_DELTA, Math.min(MAX_WHEEL_DELTA, delta));
178
- }
package/src/ui/Modal.ts DELETED
@@ -1,58 +0,0 @@
1
- /**
2
- * Minimal modal helper. Content is built with DOM APIs (no user innerHTML).
3
- */
4
- export function openModal(title: string, build: (body: HTMLDivElement, close: () => void) => void): void {
5
- const backdrop = document.createElement("div");
6
- backdrop.className = "modal-backdrop";
7
-
8
- const modal = document.createElement("div");
9
- modal.className = "modal";
10
- modal.setAttribute("role", "dialog");
11
- modal.setAttribute("aria-label", title);
12
-
13
- const closeBtn = document.createElement("button");
14
- closeBtn.className = "modal-close";
15
- closeBtn.textContent = "✕";
16
- closeBtn.setAttribute("aria-label", "Close");
17
-
18
- const heading = document.createElement("h2");
19
- heading.textContent = title;
20
-
21
- const body = document.createElement("div");
22
-
23
- const close = () => backdrop.remove();
24
- closeBtn.addEventListener("click", close);
25
- backdrop.addEventListener("click", (e) => {
26
- if (e.target === backdrop) close();
27
- });
28
- backdrop.addEventListener("keydown", (e) => {
29
- e.stopPropagation();
30
- if (e.key === "Escape") close();
31
- });
32
-
33
- modal.appendChild(closeBtn);
34
- modal.appendChild(heading);
35
- modal.appendChild(body);
36
- backdrop.appendChild(modal);
37
- document.body.appendChild(backdrop);
38
-
39
- build(body, close);
40
- closeBtn.focus();
41
- }
42
-
43
- let toastEl: HTMLDivElement | null = null;
44
- let toastTimer: number | null = null;
45
-
46
- export function showToast(message: string): void {
47
- if (!toastEl) {
48
- toastEl = document.createElement("div");
49
- toastEl.className = "toast";
50
- document.body.appendChild(toastEl);
51
- }
52
- toastEl.textContent = message;
53
- toastEl.classList.add("visible");
54
- if (toastTimer !== null) clearTimeout(toastTimer);
55
- toastTimer = window.setTimeout(() => {
56
- toastEl?.classList.remove("visible");
57
- }, 2600);
58
- }
@@ -1,75 +0,0 @@
1
- import type { BoardState } from "../app/BoardState";
2
- import type { CanvasRenderer } from "../canvas/CanvasRenderer";
3
- import type { HitTester } from "../canvas/HitTester";
4
- import type { PerformanceMonitor } from "../canvas/PerformanceMonitor";
5
- import type { ImageCache } from "../canvas/ImageCache";
6
- import { boardStore } from "../storage/LocalBoardStore";
7
- import { assetStore } from "../storage/AssetStore";
8
-
9
- export class PerformanceOverlay {
10
- element: HTMLDivElement;
11
- visible = false;
12
-
13
- private state: BoardState;
14
- private renderer: CanvasRenderer;
15
- private hitTester: HitTester;
16
- private monitor: PerformanceMonitor;
17
- private imageCache: ImageCache;
18
- private timer: number | null = null;
19
- private storageEstimate = "–";
20
-
21
- constructor(
22
- parent: HTMLElement,
23
- state: BoardState,
24
- renderer: CanvasRenderer,
25
- hitTester: HitTester,
26
- monitor: PerformanceMonitor,
27
- imageCache: ImageCache
28
- ) {
29
- this.state = state;
30
- this.renderer = renderer;
31
- this.hitTester = hitTester;
32
- this.monitor = monitor;
33
- this.imageCache = imageCache;
34
- this.element = document.createElement("div");
35
- this.element.className = "perf-overlay";
36
- parent.appendChild(this.element);
37
- }
38
-
39
- toggle(): void {
40
- this.visible = !this.visible;
41
- this.element.classList.toggle("visible", this.visible);
42
- if (this.visible) {
43
- this.monitor.start();
44
- this.update();
45
- this.timer = window.setInterval(() => this.update(), 500);
46
- void this.refreshStorage();
47
- } else if (this.timer !== null) {
48
- clearInterval(this.timer);
49
- this.timer = null;
50
- }
51
- }
52
-
53
- private async refreshStorage(): Promise<void> {
54
- const bytes = await assetStore.totalSizeBytes();
55
- this.storageEstimate = (bytes / (1024 * 1024)).toFixed(1) + "MB";
56
- }
57
-
58
- private update(): void {
59
- const vp = this.state.viewport;
60
- const lines = [
61
- `FPS: ${this.monitor.fps}`,
62
- `Total elements: ${this.state.elements.length.toLocaleString()}`,
63
- `Visible elements: ${this.renderer.visibleCount.toLocaleString()}`,
64
- `Render time: ${this.renderer.lastRenderMs.toFixed(1)}ms`,
65
- `Hit-test time: ${this.hitTester.lastHitTestMs.toFixed(2)}ms`,
66
- `Save time: ${boardStore.lastSaveMs.toFixed(0)}ms`,
67
- `Image cache: ${this.imageCache.size}`,
68
- `Estimated storage: ${this.storageEstimate}`,
69
- `Zoom: ${Math.round(vp.scale * 100)}%`,
70
- `Viewport: x: ${Math.round(-vp.offsetX / vp.scale)}, y: ${Math.round(-vp.offsetY / vp.scale)}`,
71
- `Culling: ${this.renderer.cullingEnabled ? "on" : "off"} LOD: ${this.renderer.lodEnabled ? "on" : "off"}`,
72
- ];
73
- this.element.textContent = lines.join("\n");
74
- }
75
- }
@@ -1,94 +0,0 @@
1
- import { openModal, showToast } from "./Modal";
2
- import type { BoardState } from "../app/BoardState";
3
- import { assetStore } from "../storage/AssetStore";
4
- import { boardStore } from "../storage/LocalBoardStore";
5
-
6
- function formatBytes(bytes: number): string {
7
- if (bytes < 1024) return bytes + " B";
8
- if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
9
- return (bytes / (1024 * 1024)).toFixed(1) + " MB";
10
- }
11
-
12
- export function openStorageManager(
13
- state: BoardState,
14
- onClearBoard: () => void,
15
- onResetAll: () => void
16
- ): void {
17
- openModal("Local storage", async (body, close) => {
18
- const rows = document.createElement("div");
19
- body.appendChild(rows);
20
-
21
- const addRow = (name: string, value: string) => {
22
- const row = document.createElement("div");
23
- row.className = "storage-row";
24
- const a = document.createElement("span");
25
- a.textContent = name;
26
- const b = document.createElement("span");
27
- b.textContent = value;
28
- row.appendChild(a);
29
- row.appendChild(b);
30
- rows.appendChild(row);
31
- };
32
-
33
- addRow("Elements on board", String(state.elements.length));
34
-
35
- const boardJson = JSON.stringify(state.elements);
36
- addRow("Board data size", formatBytes(boardJson.length));
37
-
38
- const assetKeys = await assetStore.getAllKeys();
39
- addRow("Image assets", String(assetKeys.length));
40
-
41
- const assetBytes = await assetStore.totalSizeBytes();
42
- addRow("Image asset size", formatBytes(assetBytes));
43
-
44
- const est = await boardStore.estimateStorage();
45
- if (est.quota > 0) {
46
- addRow(
47
- "Browser storage",
48
- `${formatBytes(est.usage)} of ${formatBytes(est.quota)}`
49
- );
50
- }
51
-
52
- const actions = document.createElement("div");
53
- actions.className = "modal-actions";
54
-
55
- const clearUnusedBtn = document.createElement("button");
56
- clearUnusedBtn.className = "btn";
57
- clearUnusedBtn.textContent = "Clear unused assets";
58
- clearUnusedBtn.addEventListener("click", async () => {
59
- const referenced = new Set<string>();
60
- for (const el of state.elements) {
61
- const id = el.data.assetId;
62
- if (typeof id === "string") referenced.add(id);
63
- }
64
- const removed = await assetStore.clearUnused(referenced);
65
- showToast(`Removed ${removed} unused asset${removed === 1 ? "" : "s"}`);
66
- close();
67
- });
68
- actions.appendChild(clearUnusedBtn);
69
-
70
- const clearBoardBtn = document.createElement("button");
71
- clearBoardBtn.className = "btn danger";
72
- clearBoardBtn.textContent = "Clear board";
73
- clearBoardBtn.addEventListener("click", () => {
74
- if (confirm("Remove all elements from this board?")) {
75
- onClearBoard();
76
- close();
77
- }
78
- });
79
- actions.appendChild(clearBoardBtn);
80
-
81
- const resetBtn = document.createElement("button");
82
- resetBtn.className = "btn danger";
83
- resetBtn.textContent = "Reset all local data";
84
- resetBtn.addEventListener("click", () => {
85
- if (confirm("Delete ALL local boards, assets and preferences? This cannot be undone.")) {
86
- onResetAll();
87
- close();
88
- }
89
- });
90
- actions.appendChild(resetBtn);
91
-
92
- body.appendChild(actions);
93
- });
94
- }
@@ -1,67 +0,0 @@
1
- import { openModal } from "./Modal";
2
- import { TEMPLATES } from "../templates";
3
- import type { BoardState } from "../app/BoardState";
4
- import type { CommandManager } from "../app/CommandManager";
5
- import { screenToWorld } from "../canvas/Viewport";
6
- import type { BoardElement } from "../elements/types";
7
-
8
- export function openTemplatePicker(state: BoardState, commands: CommandManager): void {
9
- openModal("Insert a template", (body, close) => {
10
- const grid = document.createElement("div");
11
- grid.className = "template-grid";
12
-
13
- for (const def of TEMPLATES) {
14
- const card = document.createElement("button");
15
- card.className = "template-card";
16
-
17
- const name = document.createElement("span");
18
- name.className = "t-name";
19
- name.textContent = def.name;
20
-
21
- const desc = document.createElement("span");
22
- desc.className = "t-desc";
23
- desc.textContent = def.description;
24
-
25
- card.appendChild(name);
26
- card.appendChild(desc);
27
- card.addEventListener("click", () => {
28
- insertTemplate(state, commands, def.build);
29
- close();
30
- });
31
- grid.appendChild(card);
32
- }
33
-
34
- body.appendChild(grid);
35
- });
36
- }
37
-
38
- function insertTemplate(
39
- state: BoardState,
40
- commands: CommandManager,
41
- build: (ox: number, oy: number, theme: "light" | "dark", baseZ: number) => BoardElement[]
42
- ): void {
43
- const center = screenToWorld(
44
- window.innerWidth / 2,
45
- window.innerHeight / 2,
46
- state.viewport
47
- );
48
- const elements = build(center.x - 380, center.y - 280, state.theme, state.maxZIndex());
49
-
50
- commands.execute(
51
- "insert-template",
52
- () => {
53
- for (const el of elements) {
54
- if (!state.elementsById.has(el.id)) state.addElement(el);
55
- }
56
- state.emitChange();
57
- },
58
- () => {
59
- for (const el of elements) state.removeElement(el.id);
60
- state.emitChange();
61
- }
62
- );
63
-
64
- state.clearSelection();
65
- for (const el of elements) state.selectedIds.add(el.id);
66
- state.requestRender();
67
- }