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