viteboard 0.1.0 → 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 (90) hide show
  1. package/README.md +110 -61
  2. package/content/docs/guide/boards-in-markdown.md +28 -26
  3. package/content/docs/guide/boards.md +44 -82
  4. package/content/docs/guide/getting-started.md +36 -48
  5. package/content/docs/index.md +21 -32
  6. package/content/docs/product-roadmap.board.json +4 -4
  7. package/content/machine/board-v1.schema.json +138 -0
  8. package/content/machine/example.board.json +217 -0
  9. package/content/machine/guide.md +270 -0
  10. package/content/machine/llms.txt +31 -0
  11. package/content/machine/viteboard.json +62 -0
  12. package/dist/assets/index-D2W76uu9.js +96 -0
  13. package/dist/assets/index-rMkUjGXP.css +1 -0
  14. package/dist/index.html +4 -2
  15. package/package.json +10 -12
  16. package/vite.config.ts +635 -48
  17. package/content/docs/capabilities.md +0 -89
  18. package/content/docs/docs.md +0 -84
  19. package/content/docs/guide/test.md +0 -6
  20. package/content/docs/test.md +0 -219
  21. package/content/docs/tetst-dir/test-board.board.json +0 -15
  22. package/content/docs/tetst-dir/test-test.md +0 -1
  23. package/content/docs/workspace.md +0 -73
  24. package/dist/assets/index-CEpxLM2o.css +0 -1
  25. package/dist/assets/index-D4xvJdEQ.js +0 -60
  26. package/index.html +0 -13
  27. package/src/app/AppController.ts +0 -955
  28. package/src/app/BoardState.ts +0 -130
  29. package/src/app/ClipboardService.ts +0 -159
  30. package/src/app/CommandManager.ts +0 -66
  31. package/src/app/ElementActions.ts +0 -152
  32. package/src/app/EmbedRegionSelector.ts +0 -103
  33. package/src/app/ExportPng.ts +0 -107
  34. package/src/app/ImageInsertService.ts +0 -141
  35. package/src/app/KeyboardShortcuts.ts +0 -124
  36. package/src/app/main.ts +0 -6
  37. package/src/board/BoardPreview.ts +0 -189
  38. package/src/board/BoardService.ts +0 -222
  39. package/src/canvas/CanvasRenderer.ts +0 -253
  40. package/src/canvas/CanvasSurface.ts +0 -70
  41. package/src/canvas/HitTester.ts +0 -110
  42. package/src/canvas/ImageCache.ts +0 -123
  43. package/src/canvas/PerformanceMonitor.ts +0 -31
  44. package/src/canvas/RenderScheduler.ts +0 -26
  45. package/src/canvas/Viewport.ts +0 -77
  46. package/src/content/ContentApi.ts +0 -258
  47. package/src/content/Markdown.ts +0 -431
  48. package/src/content/MarkdownView.ts +0 -70
  49. package/src/editor/DocEditor.ts +0 -799
  50. package/src/editor/htmlToMarkdown.ts +0 -333
  51. package/src/elements/renderElement.ts +0 -509
  52. package/src/elements/types.ts +0 -118
  53. package/src/shell/Shell.ts +0 -2950
  54. package/src/shell/Sidebar.ts +0 -1352
  55. package/src/storage/AssetStore.ts +0 -86
  56. package/src/storage/BoardSerializer.ts +0 -114
  57. package/src/storage/ImportExportService.ts +0 -153
  58. package/src/storage/IndexedDbStore.ts +0 -92
  59. package/src/storage/LocalBoardStore.ts +0 -104
  60. package/src/styles.css +0 -3257
  61. package/src/templates/helpers.ts +0 -124
  62. package/src/templates/index.ts +0 -65
  63. package/src/templates/journeyMapTemplate.ts +0 -52
  64. package/src/templates/opportunityTreeTemplate.ts +0 -45
  65. package/src/templates/personaTemplate.ts +0 -41
  66. package/src/templates/prioritizationMatrixTemplate.ts +0 -44
  67. package/src/templates/requirementsFlowTemplate.ts +0 -45
  68. package/src/templates/screenshotReviewTemplate.ts +0 -52
  69. package/src/templates/systemDiagramTemplate.ts +0 -41
  70. package/src/testing/StressTestGenerator.ts +0 -134
  71. package/src/testing/StressTestPanel.ts +0 -64
  72. package/src/tools/ArrowTool.ts +0 -87
  73. package/src/tools/ImageTool.ts +0 -39
  74. package/src/tools/PanTool.ts +0 -34
  75. package/src/tools/SelectTool.ts +0 -377
  76. package/src/tools/ShapeTool.ts +0 -106
  77. package/src/tools/StickyTool.ts +0 -69
  78. package/src/tools/TaskTool.ts +0 -52
  79. package/src/tools/TextTool.ts +0 -45
  80. package/src/tools/ToolContext.ts +0 -44
  81. package/src/tools/ToolController.ts +0 -178
  82. package/src/ui/Modal.ts +0 -58
  83. package/src/ui/PerformanceOverlay.ts +0 -75
  84. package/src/ui/StorageManager.ts +0 -94
  85. package/src/ui/TemplatePicker.ts +0 -67
  86. package/src/ui/TextEditorOverlay.ts +0 -137
  87. package/src/ui/Toolbar.ts +0 -151
  88. package/src/ui/TopControls.ts +0 -137
  89. package/src/ui/icons.ts +0 -50
  90. package/tsconfig.json +0 -19
@@ -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">;
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"].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,44 +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
- | "text"
20
- | "arrow"
21
- | "image";
22
-
23
- export type ToolContext = {
24
- state: BoardState;
25
- renderer: CanvasRenderer;
26
- surface: CanvasSurface;
27
- hitTester: HitTester;
28
- commands: CommandManager;
29
- setTool: (tool: ToolName) => void;
30
- setCursor: (cursor: string) => void;
31
- openTextEditor: (el: BoardElement) => void;
32
- };
33
-
34
- export interface Tool {
35
- name: ToolName;
36
- cursor: string;
37
- onPointerDown(e: PointerEvent, ctx: ToolContext): void;
38
- onPointerMove(e: PointerEvent, ctx: ToolContext): void;
39
- onPointerUp(e: PointerEvent, ctx: ToolContext): void;
40
- onDoubleClick?(e: PointerEvent, ctx: ToolContext): void;
41
- onCancel?(ctx: ToolContext): void;
42
- onActivate?(ctx: ToolContext): void;
43
- onDeactivate?(ctx: ToolContext): void;
44
- }
@@ -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
- }