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,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
- }