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,123 +0,0 @@
1
- import type { AssetStore } from "../storage/AssetStore";
2
-
3
- /**
4
- * In-memory cache of decoded ImageBitmaps keyed by asset ID.
5
- * Decoding happens once, asynchronously; a re-render is requested when done.
6
- */
7
- export class ImageCache {
8
- private cache = new Map<string, ImageBitmap>();
9
- private loading = new Set<string>();
10
- private missing = new Set<string>();
11
- private assetStore: AssetStore;
12
- private onLoaded: () => void;
13
- private resolveSrcUrl?: (src: string) => string;
14
-
15
- constructor(assetStore: AssetStore, onLoaded: () => void, resolveSrcUrl?: (src: string) => string) {
16
- this.assetStore = assetStore;
17
- this.onLoaded = onLoaded;
18
- this.resolveSrcUrl = resolveSrcUrl;
19
- }
20
-
21
- get(assetId: string): ImageBitmap | null {
22
- const bmp = this.cache.get(assetId);
23
- if (bmp) return bmp;
24
- if (!this.loading.has(assetId) && !this.missing.has(assetId)) {
25
- void this.load(assetId);
26
- }
27
- return null;
28
- }
29
-
30
- isMissing(assetId: string): boolean {
31
- return this.missing.has(assetId);
32
- }
33
-
34
- /** Resolve a board image `data.src` (relative content path or URL) to a URL. */
35
- private srcToUrl(src: string): string {
36
- if (this.resolveSrcUrl) return this.resolveSrcUrl(src);
37
- if (/^(https?:)?\/\//.test(src) || src.startsWith("/") || src.startsWith("data:")) {
38
- return src;
39
- }
40
- return `/__api/asset?path=${encodeURIComponent(src)}`;
41
- }
42
-
43
- keyForSrc(src: string): string {
44
- return this.srcToUrl(src);
45
- }
46
-
47
- /** Load a file-backed image by its `data.src` path (used by git-based boards). */
48
- getBySrc(src: string): ImageBitmap | null {
49
- const url = this.srcToUrl(src);
50
- const bmp = this.cache.get(url);
51
- if (bmp) return bmp;
52
- if (!this.loading.has(url) && !this.missing.has(url)) void this.loadUrl(url);
53
- return null;
54
- }
55
-
56
- isMissingSrc(src: string): boolean {
57
- return this.missing.has(this.srcToUrl(src));
58
- }
59
-
60
- private async loadUrl(url: string): Promise<void> {
61
- this.loading.add(url);
62
- try {
63
- const res = await fetch(url);
64
- if (!res.ok) {
65
- this.missing.add(url);
66
- return;
67
- }
68
- const blob = await res.blob();
69
- const bitmap = await createImageBitmap(blob);
70
- this.cache.set(url, bitmap);
71
- this.onLoaded();
72
- } catch {
73
- this.missing.add(url);
74
- } finally {
75
- this.loading.delete(url);
76
- }
77
- }
78
-
79
- /** Put a freshly created bitmap directly into the cache. */
80
- set(assetId: string, bitmap: ImageBitmap): void {
81
- this.cache.set(assetId, bitmap);
82
- this.missing.delete(assetId);
83
- }
84
-
85
- setBySrc(src: string, bitmap: ImageBitmap): void {
86
- this.set(this.srcToUrl(src), bitmap);
87
- }
88
-
89
- private async load(assetId: string): Promise<void> {
90
- this.loading.add(assetId);
91
- try {
92
- const asset = await this.assetStore.get(assetId);
93
- if (!asset) {
94
- this.missing.add(assetId);
95
- return;
96
- }
97
- const bitmap = await createImageBitmap(asset.blob);
98
- this.cache.set(assetId, bitmap);
99
- this.onLoaded();
100
- } catch {
101
- this.missing.add(assetId);
102
- } finally {
103
- this.loading.delete(assetId);
104
- }
105
- }
106
-
107
- evict(assetId: string): void {
108
- const bmp = this.cache.get(assetId);
109
- if (bmp) bmp.close();
110
- this.cache.delete(assetId);
111
- this.missing.delete(assetId);
112
- }
113
-
114
- clear(): void {
115
- for (const bmp of this.cache.values()) bmp.close();
116
- this.cache.clear();
117
- this.missing.clear();
118
- }
119
-
120
- get size(): number {
121
- return this.cache.size;
122
- }
123
- }
@@ -1,31 +0,0 @@
1
- /**
2
- * Tracks FPS via rAF sampling and aggregates render/hit-test/save timings.
3
- */
4
- export class PerformanceMonitor {
5
- fps = 0;
6
- private frameCount = 0;
7
- private lastSample = performance.now();
8
- private running = false;
9
-
10
- start(): void {
11
- if (this.running) return;
12
- this.running = true;
13
- const tick = () => {
14
- if (!this.running) return;
15
- this.frameCount++;
16
- const now = performance.now();
17
- const elapsed = now - this.lastSample;
18
- if (elapsed >= 1000) {
19
- this.fps = Math.round((this.frameCount * 1000) / elapsed);
20
- this.frameCount = 0;
21
- this.lastSample = now;
22
- }
23
- requestAnimationFrame(tick);
24
- };
25
- requestAnimationFrame(tick);
26
- }
27
-
28
- stop(): void {
29
- this.running = false;
30
- }
31
- }
@@ -1,26 +0,0 @@
1
- /**
2
- * Ensures rendering happens at most once per animation frame.
3
- */
4
- export class RenderScheduler {
5
- private rafId: number | null = null;
6
- private renderFn: () => void;
7
-
8
- constructor(renderFn: () => void) {
9
- this.renderFn = renderFn;
10
- }
11
-
12
- requestRender(): void {
13
- if (this.rafId !== null) return;
14
- this.rafId = requestAnimationFrame(() => {
15
- this.rafId = null;
16
- this.renderFn();
17
- });
18
- }
19
-
20
- cancel(): void {
21
- if (this.rafId !== null) {
22
- cancelAnimationFrame(this.rafId);
23
- this.rafId = null;
24
- }
25
- }
26
- }
@@ -1,77 +0,0 @@
1
- export type Viewport = {
2
- offsetX: number;
3
- offsetY: number;
4
- scale: number;
5
- };
6
-
7
- export type ViewportBounds = {
8
- left: number;
9
- top: number;
10
- right: number;
11
- bottom: number;
12
- };
13
-
14
- export const MIN_ZOOM = 0.05;
15
- export const MAX_ZOOM = 40;
16
- export const DEFAULT_ZOOM = 1;
17
-
18
- export function screenToWorld(
19
- screenX: number,
20
- screenY: number,
21
- viewport: Viewport
22
- ): { x: number; y: number } {
23
- return {
24
- x: (screenX - viewport.offsetX) / viewport.scale,
25
- y: (screenY - viewport.offsetY) / viewport.scale,
26
- };
27
- }
28
-
29
- export function worldToScreen(
30
- worldX: number,
31
- worldY: number,
32
- viewport: Viewport
33
- ): { x: number; y: number } {
34
- return {
35
- x: worldX * viewport.scale + viewport.offsetX,
36
- y: worldY * viewport.scale + viewport.offsetY,
37
- };
38
- }
39
-
40
- export function clampZoom(scale: number): number {
41
- return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, scale));
42
- }
43
-
44
- /**
45
- * Cursor-centered zoom: the world point under the cursor stays under the
46
- * cursor after the zoom is applied.
47
- */
48
- export function zoomAt(
49
- viewport: Viewport,
50
- cursorX: number,
51
- cursorY: number,
52
- newScale: number
53
- ): void {
54
- const clamped = clampZoom(newScale);
55
- const worldBeforeZoom = {
56
- x: (cursorX - viewport.offsetX) / viewport.scale,
57
- y: (cursorY - viewport.offsetY) / viewport.scale,
58
- };
59
- viewport.scale = clamped;
60
- viewport.offsetX = cursorX - worldBeforeZoom.x * viewport.scale;
61
- viewport.offsetY = cursorY - worldBeforeZoom.y * viewport.scale;
62
- }
63
-
64
- export function getViewportBounds(
65
- viewport: Viewport,
66
- screenWidth: number,
67
- screenHeight: number
68
- ): ViewportBounds {
69
- const topLeft = screenToWorld(0, 0, viewport);
70
- const bottomRight = screenToWorld(screenWidth, screenHeight, viewport);
71
- return {
72
- left: topLeft.x,
73
- top: topLeft.y,
74
- right: bottomRight.x,
75
- bottom: bottomRight.y,
76
- };
77
- }
@@ -1,258 +0,0 @@
1
- import type { BoardDocument } from "../elements/types";
2
-
3
- export type TreeNode =
4
- | { type: "file"; name: string; path: string }
5
- | { type: "dir"; name: string; path: string; children: TreeNode[] };
6
-
7
- export type DocsRootSummary = {
8
- id: string;
9
- projectId: string;
10
- projectLabel: string;
11
- relativePath: string;
12
- displayPath: string;
13
- };
14
-
15
- export type WorkspaceProject = {
16
- id: string;
17
- label: string;
18
- docsRoots: DocsRootSummary[];
19
- };
20
-
21
- export type WorkspaceState = {
22
- projects: WorkspaceProject[];
23
- };
24
-
25
- export type PickProjectResult = {
26
- cancelled: boolean;
27
- workspace: WorkspaceState;
28
- addedProjectId?: string;
29
- };
30
-
31
- const API = "/__api";
32
-
33
- async function json<T>(res: Response): Promise<T> {
34
- if (!res.ok) {
35
- let detail = "";
36
- try {
37
- detail = (await res.json())?.error ?? "";
38
- } catch {
39
- /* ignore */
40
- }
41
- throw new Error(`${res.status} ${res.statusText}${detail ? ` — ${detail}` : ""}`);
42
- }
43
- return res.json() as Promise<T>;
44
- }
45
-
46
- export const ContentApi = {
47
- async getWorkspace(): Promise<WorkspaceState> {
48
- return json<WorkspaceState>(await fetch(`${API}/workspace`));
49
- },
50
-
51
- async removeProject(projectId: string): Promise<void> {
52
- await json(
53
- await fetch(
54
- `${API}/workspace/project?id=${encodeURIComponent(projectId)}`,
55
- { method: "DELETE" }
56
- )
57
- );
58
- },
59
-
60
- async pickAndAddProject(): Promise<PickProjectResult> {
61
- return json<PickProjectResult>(
62
- await fetch(`${API}/workspace/pick-project`, {
63
- method: "POST",
64
- })
65
- );
66
- },
67
-
68
- async ensureProjectDocsRoot(projectId: string): Promise<DocsRootSummary> {
69
- const data = await json<{ root: DocsRootSummary }>(
70
- await fetch(`${API}/workspace/project/docs-root`, {
71
- method: "POST",
72
- headers: { "Content-Type": "application/json" },
73
- body: JSON.stringify({ projectId }),
74
- })
75
- );
76
- return data.root;
77
- },
78
-
79
- async getTree(rootId: string): Promise<TreeNode[]> {
80
- const data = await json<{ tree: TreeNode[] }>(
81
- await fetch(`${API}/tree?rootId=${encodeURIComponent(rootId)}`)
82
- );
83
- return data.tree;
84
- },
85
-
86
- async getProjectTree(projectId: string): Promise<TreeNode[]> {
87
- const data = await json<{ tree: TreeNode[] }>(
88
- await fetch(`${API}/project-tree?projectId=${encodeURIComponent(projectId)}`)
89
- );
90
- return data.tree;
91
- },
92
-
93
- async createDocsFolder(projectId: string, parentPath: string): Promise<void> {
94
- await json(
95
- await fetch(`${API}/workspace/project/create-docs-folder`, {
96
- method: "POST",
97
- headers: { "Content-Type": "application/json" },
98
- body: JSON.stringify({ projectId, parentPath }),
99
- })
100
- );
101
- },
102
-
103
- async readFile(rootId: string, path: string): Promise<string> {
104
- const data = await json<{ content: string }>(
105
- await fetch(
106
- `${API}/file?rootId=${encodeURIComponent(rootId)}&path=${encodeURIComponent(path)}`
107
- )
108
- );
109
- return data.content;
110
- },
111
-
112
- async writeFile(rootId: string, path: string, content: string): Promise<void> {
113
- await json(
114
- await fetch(`${API}/file`, {
115
- method: "PUT",
116
- headers: { "Content-Type": "application/json" },
117
- body: JSON.stringify({ rootId, path, content }),
118
- })
119
- );
120
- },
121
-
122
- async createDirectory(rootId: string, path: string): Promise<void> {
123
- await json(
124
- await fetch(`${API}/directory`, {
125
- method: "PUT",
126
- headers: { "Content-Type": "application/json" },
127
- body: JSON.stringify({ rootId, path }),
128
- })
129
- );
130
- },
131
-
132
- async getBoard(rootId: string, id: string): Promise<BoardDocument | null> {
133
- const res = await fetch(
134
- `${API}/board?rootId=${encodeURIComponent(rootId)}&id=${encodeURIComponent(id)}`
135
- );
136
- if (res.status === 404) return null;
137
- const data = await json<{ doc: BoardDocument }>(res);
138
- return data.doc;
139
- },
140
-
141
- async saveBoard(rootId: string, id: string, doc: BoardDocument): Promise<void> {
142
- await json(
143
- await fetch(`${API}/board`, {
144
- method: "PUT",
145
- headers: { "Content-Type": "application/json" },
146
- body: JSON.stringify({ rootId, id, doc }),
147
- })
148
- );
149
- },
150
-
151
- async deleteFile(rootId: string, filePath: string): Promise<void> {
152
- await json(
153
- await fetch(
154
- `${API}/file?rootId=${encodeURIComponent(rootId)}&path=${encodeURIComponent(filePath)}`,
155
- { method: "DELETE" }
156
- )
157
- );
158
- },
159
-
160
- async renameFile(rootId: string, oldPath: string, newPath: string, ignoreMissing = false): Promise<void> {
161
- await json(
162
- await fetch(`${API}/file`, {
163
- method: "PATCH",
164
- headers: { "Content-Type": "application/json" },
165
- body: JSON.stringify({ rootId, oldPath, newPath, ignoreMissing }),
166
- })
167
- );
168
- },
169
-
170
- async deleteBoard(rootId: string, id: string): Promise<void> {
171
- await json(
172
- await fetch(
173
- `${API}/board?rootId=${encodeURIComponent(rootId)}&id=${encodeURIComponent(id)}`,
174
- { method: "DELETE" }
175
- )
176
- );
177
- },
178
-
179
- assetUrl(rootId: string, path: string): string {
180
- return `${API}/asset?rootId=${encodeURIComponent(rootId)}&path=${encodeURIComponent(path)}`;
181
- },
182
-
183
- async uploadAsset(rootId: string, name: string, base64: string): Promise<string> {
184
- const data = await json<{ path: string }>(
185
- await fetch(`${API}/asset`, {
186
- method: "POST",
187
- headers: { "Content-Type": "application/json" },
188
- body: JSON.stringify({ rootId, name, base64 }),
189
- })
190
- );
191
- return data.path;
192
- },
193
- };
194
-
195
- export function filesUnder(tree: TreeNode[], folder: string): TreeNode[] {
196
- const root = tree.find((n) => n.type === "dir" && n.name === folder);
197
- if (!root || root.type !== "dir") return [];
198
- const out: TreeNode[] = [];
199
- const walk = (nodes: TreeNode[]) => {
200
- for (const n of nodes) {
201
- if (n.type === "file") out.push(n);
202
- else walk(n.children);
203
- }
204
- };
205
- walk(root.children);
206
- return out;
207
- }
208
-
209
- export function firstMarkdownPath(tree: TreeNode[]): string | null {
210
- for (const node of tree) {
211
- if (node.type === "file" && node.name.endsWith(".md")) return node.path;
212
- if (node.type === "dir") {
213
- const found = firstMarkdownPath(node.children);
214
- if (found) return found;
215
- }
216
- }
217
- return null;
218
- }
219
-
220
- export function normalizeRelativePath(input: string): string {
221
- const parts: string[] = [];
222
- for (const raw of input.replace(/\\/g, "/").split("/")) {
223
- const part = raw.trim();
224
- if (!part || part === ".") continue;
225
- if (part === "..") {
226
- if (parts.length > 0) parts.pop();
227
- continue;
228
- }
229
- parts.push(part);
230
- }
231
- return parts.join("/");
232
- }
233
-
234
- export function dirnamePosix(filePath: string): string {
235
- const normalized = normalizeRelativePath(filePath);
236
- const idx = normalized.lastIndexOf("/");
237
- return idx === -1 ? "" : normalized.slice(0, idx);
238
- }
239
-
240
- export function resolveDocPath(fromFile: string, target: string): string {
241
- if (/^(https?:|mailto:|tel:|#|data:)/.test(target)) return target;
242
- if (target.startsWith("/")) return normalizeRelativePath(target);
243
- const base = dirnamePosix(fromFile);
244
- return normalizeRelativePath(base ? `${base}/${target}` : target);
245
- }
246
-
247
- export function relativePathBetween(fromFile: string, toPath: string): string {
248
- const fromDir = dirnamePosix(fromFile);
249
- const fromParts = fromDir ? fromDir.split("/").filter(Boolean) : [];
250
- const toParts = normalizeRelativePath(toPath).split("/").filter(Boolean);
251
- let shared = 0;
252
- while (shared < fromParts.length && shared < toParts.length && fromParts[shared] === toParts[shared]) {
253
- shared++;
254
- }
255
- const up = fromParts.slice(shared).map(() => "..");
256
- const down = toParts.slice(shared);
257
- return [...up, ...down].join("/") || toParts[toParts.length - 1] || "";
258
- }