viteboard 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +136 -0
- package/bin/viteboard.mjs +59 -0
- package/content/docs/assets/image-mqjpqlyw.png +0 -0
- package/content/docs/assets/image-mqjsmenr.png +0 -0
- package/content/docs/boards/product-roadmap.board.json +2130 -0
- package/content/docs/capabilities.md +89 -0
- package/content/docs/docs.md +84 -0
- package/content/docs/guide/boards-in-markdown.md +48 -0
- package/content/docs/guide/boards.md +92 -0
- package/content/docs/guide/getting-started.md +71 -0
- package/content/docs/guide/test.md +6 -0
- package/content/docs/index.md +47 -0
- package/content/docs/product-roadmap.board.json +2130 -0
- package/content/docs/test.md +219 -0
- package/content/docs/tetst-dir/test-board.board.json +15 -0
- package/content/docs/tetst-dir/test-test.md +1 -0
- package/content/docs/workspace.md +73 -0
- package/dist/assets/index-CEpxLM2o.css +1 -0
- package/dist/assets/index-D4xvJdEQ.js +60 -0
- package/dist/index.html +13 -0
- package/index.html +13 -0
- package/package.json +52 -0
- package/src/app/AppController.ts +955 -0
- package/src/app/BoardState.ts +130 -0
- package/src/app/ClipboardService.ts +159 -0
- package/src/app/CommandManager.ts +66 -0
- package/src/app/ElementActions.ts +152 -0
- package/src/app/EmbedRegionSelector.ts +103 -0
- package/src/app/ExportPng.ts +107 -0
- package/src/app/ImageInsertService.ts +141 -0
- package/src/app/KeyboardShortcuts.ts +124 -0
- package/src/app/main.ts +6 -0
- package/src/board/BoardPreview.ts +189 -0
- package/src/board/BoardService.ts +222 -0
- package/src/canvas/CanvasRenderer.ts +253 -0
- package/src/canvas/CanvasSurface.ts +70 -0
- package/src/canvas/HitTester.ts +110 -0
- package/src/canvas/ImageCache.ts +123 -0
- package/src/canvas/PerformanceMonitor.ts +31 -0
- package/src/canvas/RenderScheduler.ts +26 -0
- package/src/canvas/Viewport.ts +77 -0
- package/src/content/ContentApi.ts +258 -0
- package/src/content/Markdown.ts +431 -0
- package/src/content/MarkdownView.ts +70 -0
- package/src/editor/DocEditor.ts +799 -0
- package/src/editor/htmlToMarkdown.ts +333 -0
- package/src/elements/renderElement.ts +509 -0
- package/src/elements/types.ts +118 -0
- package/src/shell/Shell.ts +2950 -0
- package/src/shell/Sidebar.ts +1352 -0
- package/src/storage/AssetStore.ts +86 -0
- package/src/storage/BoardSerializer.ts +114 -0
- package/src/storage/ImportExportService.ts +153 -0
- package/src/storage/IndexedDbStore.ts +92 -0
- package/src/storage/LocalBoardStore.ts +104 -0
- package/src/styles.css +3257 -0
- package/src/templates/helpers.ts +124 -0
- package/src/templates/index.ts +65 -0
- package/src/templates/journeyMapTemplate.ts +52 -0
- package/src/templates/opportunityTreeTemplate.ts +45 -0
- package/src/templates/personaTemplate.ts +41 -0
- package/src/templates/prioritizationMatrixTemplate.ts +44 -0
- package/src/templates/requirementsFlowTemplate.ts +45 -0
- package/src/templates/screenshotReviewTemplate.ts +52 -0
- package/src/templates/systemDiagramTemplate.ts +41 -0
- package/src/testing/StressTestGenerator.ts +134 -0
- package/src/testing/StressTestPanel.ts +64 -0
- package/src/tools/ArrowTool.ts +87 -0
- package/src/tools/ImageTool.ts +39 -0
- package/src/tools/PanTool.ts +34 -0
- package/src/tools/SelectTool.ts +377 -0
- package/src/tools/ShapeTool.ts +106 -0
- package/src/tools/StickyTool.ts +69 -0
- package/src/tools/TaskTool.ts +52 -0
- package/src/tools/TextTool.ts +45 -0
- package/src/tools/ToolContext.ts +44 -0
- package/src/tools/ToolController.ts +178 -0
- package/src/ui/Modal.ts +58 -0
- package/src/ui/PerformanceOverlay.ts +75 -0
- package/src/ui/StorageManager.ts +94 -0
- package/src/ui/TemplatePicker.ts +67 -0
- package/src/ui/TextEditorOverlay.ts +137 -0
- package/src/ui/Toolbar.ts +151 -0
- package/src/ui/TopControls.ts +137 -0
- package/src/ui/icons.ts +50 -0
- package/tsconfig.json +19 -0
- package/vite.config.ts +694 -0
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { BoardState } from "../app/BoardState";
|
|
2
|
+
import type { CommandManager } from "../app/CommandManager";
|
|
3
|
+
import type { BoardElement } from "../elements/types";
|
|
4
|
+
import { worldToScreen } from "../canvas/Viewport";
|
|
5
|
+
import { getRenderedTextFontSize, invalidateTextCache } from "../elements/renderElement";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Temporary HTML textarea positioned over a canvas element for editing.
|
|
9
|
+
* Canvas renders the final text; the textarea exists only during editing.
|
|
10
|
+
*/
|
|
11
|
+
export class TextEditorOverlay {
|
|
12
|
+
private state: BoardState;
|
|
13
|
+
private commands: CommandManager;
|
|
14
|
+
private textarea: HTMLTextAreaElement | null = null;
|
|
15
|
+
private editingId: string | null = null;
|
|
16
|
+
|
|
17
|
+
constructor(state: BoardState, commands: CommandManager) {
|
|
18
|
+
this.state = state;
|
|
19
|
+
this.commands = commands;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get isEditing(): boolean {
|
|
23
|
+
return this.editingId !== null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
open(el: BoardElement): void {
|
|
27
|
+
this.close();
|
|
28
|
+
this.editingId = el.id;
|
|
29
|
+
|
|
30
|
+
const ta = document.createElement("textarea");
|
|
31
|
+
ta.className = "text-editor";
|
|
32
|
+
ta.value = (el.data.text as string) ?? "";
|
|
33
|
+
this.position(ta, el);
|
|
34
|
+
|
|
35
|
+
document.body.appendChild(ta);
|
|
36
|
+
this.textarea = ta;
|
|
37
|
+
ta.focus();
|
|
38
|
+
ta.select();
|
|
39
|
+
|
|
40
|
+
// hide canvas text while editing by temporarily blanking it
|
|
41
|
+
const originalText = (el.data.text as string) ?? "";
|
|
42
|
+
el.data.text = "";
|
|
43
|
+
el.updatedAt = Date.now();
|
|
44
|
+
invalidateTextCache(el.id);
|
|
45
|
+
this.state.requestRender();
|
|
46
|
+
|
|
47
|
+
const commit = () => {
|
|
48
|
+
const newText = ta.value;
|
|
49
|
+
el.data.text = newText;
|
|
50
|
+
el.updatedAt = Date.now();
|
|
51
|
+
invalidateTextCache(el.id);
|
|
52
|
+
|
|
53
|
+
if (newText !== originalText) {
|
|
54
|
+
const id = el.id;
|
|
55
|
+
const state = this.state;
|
|
56
|
+
this.commands.push(
|
|
57
|
+
"edit-text",
|
|
58
|
+
() => {
|
|
59
|
+
const target = state.elementsById.get(id);
|
|
60
|
+
if (target) {
|
|
61
|
+
target.data.text = newText;
|
|
62
|
+
target.updatedAt = Date.now();
|
|
63
|
+
invalidateTextCache(id);
|
|
64
|
+
state.emitChange();
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
() => {
|
|
68
|
+
const target = state.elementsById.get(id);
|
|
69
|
+
if (target) {
|
|
70
|
+
target.data.text = originalText;
|
|
71
|
+
target.updatedAt = Date.now();
|
|
72
|
+
invalidateTextCache(id);
|
|
73
|
+
state.emitChange();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
this.state.emitChange();
|
|
79
|
+
this.close();
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
ta.addEventListener("blur", commit);
|
|
83
|
+
ta.addEventListener("keydown", (e) => {
|
|
84
|
+
e.stopPropagation();
|
|
85
|
+
if (e.key === "Escape") {
|
|
86
|
+
el.data.text = originalText;
|
|
87
|
+
invalidateTextCache(el.id);
|
|
88
|
+
this.close();
|
|
89
|
+
this.state.requestRender();
|
|
90
|
+
} else if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
91
|
+
ta.blur();
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Reposition while viewport changes (e.g. window resize). */
|
|
97
|
+
private position(ta: HTMLTextAreaElement, el: BoardElement): void {
|
|
98
|
+
const vp = this.state.viewport;
|
|
99
|
+
const tl = worldToScreen(el.x, el.y, vp);
|
|
100
|
+
const isChevron = el.data.shapeKind === "chevron";
|
|
101
|
+
const fontSize = getRenderedTextFontSize(el) * vp.scale;
|
|
102
|
+
const chevronPad =
|
|
103
|
+
isChevron
|
|
104
|
+
? Math.min(Math.abs(el.width) * 0.18, Math.abs(el.height) * 0.55) + 8
|
|
105
|
+
: 10;
|
|
106
|
+
const xPad = (el.type === "sticky" ? 14 : el.type === "task" ? 50 : el.type === "text" ? 0 : chevronPad) * vp.scale;
|
|
107
|
+
const editorHeight = isChevron
|
|
108
|
+
? Math.max(28, fontSize * 1.28)
|
|
109
|
+
: Math.max(24, el.height * vp.scale - (el.type === "sticky" ? 28 : el.type === "task" ? 0 : el.type === "text" ? 0 : chevronPad * 2) * vp.scale);
|
|
110
|
+
const yPad = isChevron
|
|
111
|
+
? Math.max(0, (el.height * vp.scale - editorHeight) / 2)
|
|
112
|
+
: (el.type === "sticky" ? 14 : el.type === "task" ? 0 : el.type === "text" ? 0 : chevronPad) * vp.scale;
|
|
113
|
+
const rightPad = (el.type === "task" ? 16 : el.type === "sticky" ? 14 : el.type === "text" ? 0 : chevronPad) * vp.scale;
|
|
114
|
+
|
|
115
|
+
ta.style.left = tl.x + xPad + "px";
|
|
116
|
+
ta.style.top = tl.y + yPad + "px";
|
|
117
|
+
ta.style.width = Math.max(40, el.width * vp.scale - xPad - rightPad) + "px";
|
|
118
|
+
ta.style.height = editorHeight + "px";
|
|
119
|
+
ta.style.fontSize = fontSize + "px";
|
|
120
|
+
ta.style.lineHeight = isChevron ? "1.18" : "1.35";
|
|
121
|
+
ta.style.fontFamily =
|
|
122
|
+
el.style.fontFamily ?? '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
|
|
123
|
+
ta.style.fontWeight = isChevron ? "750" : el.style.fontWeight ?? "400";
|
|
124
|
+
ta.style.color =
|
|
125
|
+
el.style.textColor ?? (this.state.theme === "dark" ? "#f2f2f2" : "#1f2933");
|
|
126
|
+
ta.style.textAlign = el.type === "text" || el.type === "task" ? "left" : "center";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
close(): void {
|
|
130
|
+
if (this.textarea) {
|
|
131
|
+
const ta = this.textarea;
|
|
132
|
+
this.textarea = null;
|
|
133
|
+
this.editingId = null;
|
|
134
|
+
ta.remove();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { ICONS } from "./icons";
|
|
2
|
+
import type { ToolName } from "../tools/ToolContext";
|
|
3
|
+
|
|
4
|
+
export type ToolbarActions = {
|
|
5
|
+
setTool: (tool: ToolName) => void;
|
|
6
|
+
openTemplates: () => void;
|
|
7
|
+
toggleTheme: () => void;
|
|
8
|
+
openExportMenu: () => void;
|
|
9
|
+
toggleStressPanel: () => void;
|
|
10
|
+
/** Optional: start the drag-to-select embed flow (file-backed boards only). */
|
|
11
|
+
copyEmbed?: () => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const TOOL_BUTTONS: { tool: ToolName; icon: string; label: string; key: string }[] = [
|
|
15
|
+
{ tool: "select", icon: "select", label: "Select", key: "V" },
|
|
16
|
+
{ tool: "hand", icon: "hand", label: "Hand", key: "H" },
|
|
17
|
+
{ tool: "sticky", icon: "sticky", label: "Sticky note", key: "S" },
|
|
18
|
+
{ tool: "task", icon: "task", label: "Task", key: "" },
|
|
19
|
+
{ tool: "text", icon: "text", label: "Text", key: "T" },
|
|
20
|
+
{ tool: "arrow", icon: "arrow", label: "Arrow", key: "A" },
|
|
21
|
+
{ tool: "image", icon: "image", label: "Image", key: "I" },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const SHAPE_BUTTONS: { tool: ToolName; icon: string; label: string; key: string }[] = [
|
|
25
|
+
{ tool: "rectangle", icon: "rectangle", label: "Rectangle", key: "R" },
|
|
26
|
+
{ tool: "ellipse", icon: "ellipse", label: "Ellipse", key: "O" },
|
|
27
|
+
{ tool: "diamond", icon: "diamond", label: "Diamond", key: "" },
|
|
28
|
+
{ tool: "triangle", icon: "triangle", label: "Triangle", key: "" },
|
|
29
|
+
{ tool: "pill", icon: "pill", label: "Pill", key: "" },
|
|
30
|
+
{ tool: "chevron", icon: "chevron", label: "Chevron", key: "" },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const SHAPE_TOOLS = new Set<ToolName>(SHAPE_BUTTONS.map((item) => item.tool));
|
|
34
|
+
|
|
35
|
+
export class Toolbar {
|
|
36
|
+
element: HTMLDivElement;
|
|
37
|
+
private toolButtons = new Map<ToolName, HTMLButtonElement>();
|
|
38
|
+
private shapeButtons = new Map<ToolName, HTMLButtonElement>();
|
|
39
|
+
private shapeBtn!: HTMLButtonElement;
|
|
40
|
+
private shapeGroup!: HTMLDivElement;
|
|
41
|
+
private themeBtn!: HTMLButtonElement;
|
|
42
|
+
|
|
43
|
+
constructor(parent: HTMLElement, actions: ToolbarActions) {
|
|
44
|
+
this.element = document.createElement("div");
|
|
45
|
+
this.element.className = "toolbar";
|
|
46
|
+
|
|
47
|
+
for (const def of TOOL_BUTTONS) {
|
|
48
|
+
const btn = this.makeButton(def.icon, def.label, def.key);
|
|
49
|
+
btn.addEventListener("click", () => actions.setTool(def.tool));
|
|
50
|
+
this.toolButtons.set(def.tool, btn);
|
|
51
|
+
this.element.appendChild(btn);
|
|
52
|
+
if (def.tool === "sticky") this.element.appendChild(this.makeShapePicker(actions));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.element.appendChild(this.divider());
|
|
56
|
+
|
|
57
|
+
const templatesBtn = this.makeButton("templates", "Templates", "");
|
|
58
|
+
templatesBtn.addEventListener("click", actions.openTemplates);
|
|
59
|
+
this.element.appendChild(templatesBtn);
|
|
60
|
+
|
|
61
|
+
this.element.appendChild(this.divider());
|
|
62
|
+
|
|
63
|
+
this.themeBtn = this.makeButton("moon", "Toggle theme", "");
|
|
64
|
+
this.themeBtn.addEventListener("click", actions.toggleTheme);
|
|
65
|
+
this.element.appendChild(this.themeBtn);
|
|
66
|
+
|
|
67
|
+
const exportBtn = this.makeButton("export", "Export / Import", "");
|
|
68
|
+
exportBtn.addEventListener("click", actions.openExportMenu);
|
|
69
|
+
this.element.appendChild(exportBtn);
|
|
70
|
+
|
|
71
|
+
const stressBtn = this.makeButton("flask", "Stress test", "");
|
|
72
|
+
stressBtn.addEventListener("click", actions.toggleStressPanel);
|
|
73
|
+
this.element.appendChild(stressBtn);
|
|
74
|
+
|
|
75
|
+
if (actions.copyEmbed) {
|
|
76
|
+
this.element.appendChild(this.divider());
|
|
77
|
+
const embedBtn = this.makeButton("embed", "Copy embed of a region", "");
|
|
78
|
+
embedBtn.addEventListener("click", actions.copyEmbed);
|
|
79
|
+
this.element.appendChild(embedBtn);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
parent.appendChild(this.element);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private makeButton(icon: string, label: string, key: string): HTMLButtonElement {
|
|
86
|
+
const btn = document.createElement("button");
|
|
87
|
+
btn.className = "tool-btn";
|
|
88
|
+
btn.setAttribute("aria-label", label);
|
|
89
|
+
btn.innerHTML = ICONS[icon] ?? "";
|
|
90
|
+
const tip = document.createElement("span");
|
|
91
|
+
tip.className = "tooltip";
|
|
92
|
+
tip.textContent = label;
|
|
93
|
+
if (key) {
|
|
94
|
+
const kbd = document.createElement("kbd");
|
|
95
|
+
kbd.textContent = key;
|
|
96
|
+
tip.appendChild(kbd);
|
|
97
|
+
}
|
|
98
|
+
btn.appendChild(tip);
|
|
99
|
+
return btn;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private makeShapePicker(actions: ToolbarActions): HTMLDivElement {
|
|
103
|
+
this.shapeGroup = document.createElement("div");
|
|
104
|
+
this.shapeGroup.className = "shape-tool-group";
|
|
105
|
+
this.shapeBtn = this.makeButton("shapes", "Shapes", "");
|
|
106
|
+
this.shapeBtn.classList.add("shape-tool-trigger");
|
|
107
|
+
this.shapeBtn.addEventListener("click", (e) => {
|
|
108
|
+
e.stopPropagation();
|
|
109
|
+
this.shapeGroup.classList.toggle("open");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const menu = document.createElement("div");
|
|
113
|
+
menu.className = "shape-tool-popover";
|
|
114
|
+
for (const def of SHAPE_BUTTONS) {
|
|
115
|
+
const btn = this.makeButton(def.icon, def.label, def.key);
|
|
116
|
+
btn.classList.add("shape-option-btn");
|
|
117
|
+
btn.addEventListener("click", (e) => {
|
|
118
|
+
e.stopPropagation();
|
|
119
|
+
actions.setTool(def.tool);
|
|
120
|
+
this.shapeGroup.classList.remove("open");
|
|
121
|
+
});
|
|
122
|
+
this.shapeButtons.set(def.tool, btn);
|
|
123
|
+
menu.appendChild(btn);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.shapeGroup.append(this.shapeBtn, menu);
|
|
127
|
+
return this.shapeGroup;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private divider(): HTMLDivElement {
|
|
131
|
+
const d = document.createElement("div");
|
|
132
|
+
d.className = "toolbar-divider";
|
|
133
|
+
return d;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
setActiveTool(tool: ToolName): void {
|
|
137
|
+
for (const [name, btn] of this.toolButtons) {
|
|
138
|
+
btn.classList.toggle("active", name === tool);
|
|
139
|
+
}
|
|
140
|
+
for (const [name, btn] of this.shapeButtons) {
|
|
141
|
+
btn.classList.toggle("active", name === tool);
|
|
142
|
+
}
|
|
143
|
+
this.shapeBtn?.classList.toggle("active", SHAPE_TOOLS.has(tool));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
setTheme(theme: "light" | "dark"): void {
|
|
147
|
+
const tip = this.themeBtn.querySelector(".tooltip");
|
|
148
|
+
this.themeBtn.innerHTML = ICONS[theme === "dark" ? "sun" : "moon"];
|
|
149
|
+
if (tip) this.themeBtn.appendChild(tip);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { ICONS } from "./icons";
|
|
2
|
+
|
|
3
|
+
export type TopControlActions = {
|
|
4
|
+
resetView: () => void;
|
|
5
|
+
zoomIn: () => void;
|
|
6
|
+
zoomOut: () => void;
|
|
7
|
+
openStorageManager: () => void;
|
|
8
|
+
togglePerfOverlay: () => void;
|
|
9
|
+
onTitleChange: (title: string) => void;
|
|
10
|
+
deleteBoard?: () => void;
|
|
11
|
+
/** Optional: shown as a "← ViteBoard" button to the left of the title. */
|
|
12
|
+
onExit?: () => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export class TopControls {
|
|
16
|
+
private zoomLabel!: HTMLButtonElement;
|
|
17
|
+
private titleInput!: HTMLInputElement;
|
|
18
|
+
private titleMeasure!: HTMLSpanElement;
|
|
19
|
+
|
|
20
|
+
constructor(parent: HTMLElement, actions: TopControlActions, initialTitle: string) {
|
|
21
|
+
// top-left: optional back button + board title
|
|
22
|
+
const topLeft = document.createElement("div");
|
|
23
|
+
topLeft.className = "top-left";
|
|
24
|
+
|
|
25
|
+
if (actions.onExit) {
|
|
26
|
+
const back = document.createElement("button");
|
|
27
|
+
back.className = "control-btn back-btn";
|
|
28
|
+
back.innerHTML = `${ICONS.arrowLeft ?? ""}<span>ViteBoard</span>`;
|
|
29
|
+
back.title = "Save and back to ViteBoard";
|
|
30
|
+
back.setAttribute("aria-label", "Back to ViteBoard");
|
|
31
|
+
back.addEventListener("click", actions.onExit);
|
|
32
|
+
topLeft.appendChild(back);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this.titleInput = document.createElement("input");
|
|
36
|
+
this.titleInput.className = "board-title";
|
|
37
|
+
this.titleInput.value = initialTitle;
|
|
38
|
+
this.titleInput.maxLength = 120;
|
|
39
|
+
this.titleInput.setAttribute("aria-label", "Board title");
|
|
40
|
+
this.titleInput.addEventListener("change", () =>
|
|
41
|
+
actions.onTitleChange(this.titleInput.value.trim() || "Untitled Board")
|
|
42
|
+
);
|
|
43
|
+
this.titleInput.addEventListener("input", () => this.resizeTitleInput());
|
|
44
|
+
this.titleInput.addEventListener("keydown", (e) => {
|
|
45
|
+
e.stopPropagation();
|
|
46
|
+
if (e.key === "Enter") this.titleInput.blur();
|
|
47
|
+
});
|
|
48
|
+
this.titleMeasure = document.createElement("span");
|
|
49
|
+
this.titleMeasure.className = "board-title-measure";
|
|
50
|
+
topLeft.appendChild(this.titleInput);
|
|
51
|
+
topLeft.appendChild(this.titleMeasure);
|
|
52
|
+
parent.appendChild(topLeft);
|
|
53
|
+
this.resizeTitleInput();
|
|
54
|
+
|
|
55
|
+
// top-right controls
|
|
56
|
+
const topRight = document.createElement("div");
|
|
57
|
+
topRight.className = "top-right";
|
|
58
|
+
|
|
59
|
+
const zoomGroup = document.createElement("div");
|
|
60
|
+
zoomGroup.className = "control-group";
|
|
61
|
+
|
|
62
|
+
const zoomOut = this.btn("−", "Zoom out (Ctrl/Cmd + wheel or Ctrl/Cmd + -)");
|
|
63
|
+
zoomOut.addEventListener("click", actions.zoomOut);
|
|
64
|
+
zoomGroup.appendChild(zoomOut);
|
|
65
|
+
|
|
66
|
+
this.zoomLabel = this.btn("100%", "Reset zoom to 100% (Ctrl/Cmd + 0)");
|
|
67
|
+
this.zoomLabel.classList.add("zoom-label");
|
|
68
|
+
this.zoomLabel.addEventListener("click", actions.resetView);
|
|
69
|
+
zoomGroup.appendChild(this.zoomLabel);
|
|
70
|
+
|
|
71
|
+
const zoomIn = this.btn("+", "Zoom in (Ctrl/Cmd + wheel or Ctrl/Cmd + +)");
|
|
72
|
+
zoomIn.addEventListener("click", actions.zoomIn);
|
|
73
|
+
zoomGroup.appendChild(zoomIn);
|
|
74
|
+
|
|
75
|
+
topRight.appendChild(zoomGroup);
|
|
76
|
+
|
|
77
|
+
const utilGroup = document.createElement("div");
|
|
78
|
+
utilGroup.className = "control-group";
|
|
79
|
+
|
|
80
|
+
const resetBtn = this.iconBtn("reset", "Reset view");
|
|
81
|
+
resetBtn.addEventListener("click", actions.resetView);
|
|
82
|
+
utilGroup.appendChild(resetBtn);
|
|
83
|
+
|
|
84
|
+
const storageBtn = this.iconBtn("storage", "Storage manager");
|
|
85
|
+
storageBtn.addEventListener("click", actions.openStorageManager);
|
|
86
|
+
utilGroup.appendChild(storageBtn);
|
|
87
|
+
|
|
88
|
+
const perfBtn = this.iconBtn("gauge", "Performance overlay");
|
|
89
|
+
perfBtn.addEventListener("click", actions.togglePerfOverlay);
|
|
90
|
+
utilGroup.appendChild(perfBtn);
|
|
91
|
+
|
|
92
|
+
if (actions.deleteBoard) {
|
|
93
|
+
const deleteBtn = this.iconBtn("trash", "Delete board");
|
|
94
|
+
deleteBtn.classList.add("danger-control");
|
|
95
|
+
deleteBtn.addEventListener("click", actions.deleteBoard);
|
|
96
|
+
utilGroup.appendChild(deleteBtn);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
topRight.appendChild(utilGroup);
|
|
100
|
+
parent.appendChild(topRight);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private btn(text: string, label: string): HTMLButtonElement {
|
|
104
|
+
const b = document.createElement("button");
|
|
105
|
+
b.className = "control-btn";
|
|
106
|
+
b.textContent = text;
|
|
107
|
+
b.title = label;
|
|
108
|
+
b.setAttribute("aria-label", label);
|
|
109
|
+
return b;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private iconBtn(icon: string, label: string): HTMLButtonElement {
|
|
113
|
+
const b = document.createElement("button");
|
|
114
|
+
b.className = "control-btn";
|
|
115
|
+
b.innerHTML = ICONS[icon] ?? "";
|
|
116
|
+
b.title = label;
|
|
117
|
+
b.setAttribute("aria-label", label);
|
|
118
|
+
return b;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
setZoom(scale: number): void {
|
|
122
|
+
this.zoomLabel.textContent = Math.round(scale * 100) + "%";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
setTitle(title: string): void {
|
|
126
|
+
this.titleInput.value = title;
|
|
127
|
+
this.resizeTitleInput();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private resizeTitleInput(): void {
|
|
131
|
+
if (!this.titleMeasure || !this.titleInput) return;
|
|
132
|
+
this.titleMeasure.textContent = this.titleInput.value || "Untitled Board";
|
|
133
|
+
const desired = Math.ceil(this.titleMeasure.getBoundingClientRect().width + 22);
|
|
134
|
+
const max = Math.max(180, window.innerWidth - 460);
|
|
135
|
+
this.titleInput.style.width = Math.min(Math.max(90, desired), max) + "px";
|
|
136
|
+
}
|
|
137
|
+
}
|
package/src/ui/icons.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline SVG icons (stroke-based, Lucide-style geometry, hand-written).
|
|
3
|
+
* Returned as innerHTML-safe static strings (no user content).
|
|
4
|
+
*/
|
|
5
|
+
export const ICONS: Record<string, string> = {
|
|
6
|
+
select: `<svg viewBox="0 0 24 24"><path d="M4 3l7 18 2.5-7.5L21 11z"/></svg>`,
|
|
7
|
+
hand: `<svg viewBox="0 0 24 24"><path d="M18 11V6.5a1.5 1.5 0 0 0-3 0V11m0-1V4.5a1.5 1.5 0 0 0-3 0V10m0 .5v-5a1.5 1.5 0 0 0-3 0V12m9-1v1.5a1.5 1.5 0 0 1 3 0V14c0 4-2.5 7-6.5 7S8 18.5 6 15l-1.8-3.2a1.4 1.4 0 0 1 2.3-1.5L8 12V7"/></svg>`,
|
|
8
|
+
sticky: `<svg viewBox="0 0 24 24"><path d="M5 4h14a1 1 0 0 1 1 1v9l-6 6H5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1z"/><path d="M14 20v-5a1 1 0 0 1 1-1h5"/></svg>`,
|
|
9
|
+
task: `<svg viewBox="0 0 24 24"><rect x="4" y="5" width="16" height="14" rx="3"/><path d="M8 12l2.3 2.3L16 8.8"/></svg>`,
|
|
10
|
+
rectangle: `<svg viewBox="0 0 24 24"><rect x="4" y="5" width="16" height="14" rx="2"/></svg>`,
|
|
11
|
+
ellipse: `<svg viewBox="0 0 24 24"><ellipse cx="12" cy="12" rx="8.5" ry="7"/></svg>`,
|
|
12
|
+
diamond: `<svg viewBox="0 0 24 24"><path d="M12 3l8 9-8 9-8-9z"/></svg>`,
|
|
13
|
+
triangle: `<svg viewBox="0 0 24 24"><path d="M12 4l9 16H3z"/></svg>`,
|
|
14
|
+
pill: `<svg viewBox="0 0 24 24"><rect x="3.5" y="7" width="17" height="10" rx="5"/></svg>`,
|
|
15
|
+
chevron: `<svg viewBox="0 0 24 24"><path d="M4 6h12l5 6-5 6H4l5-6z"/></svg>`,
|
|
16
|
+
text: `<svg viewBox="0 0 24 24"><path d="M5 6V4h14v2M12 4v16m-3 0h6"/></svg>`,
|
|
17
|
+
arrow: `<svg viewBox="0 0 24 24"><path d="M5 19L19 5m0 0h-7m7 0v7"/></svg>`,
|
|
18
|
+
image: `<svg viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="9" cy="10" r="1.6"/><path d="M21 16l-5-5-9 9"/></svg>`,
|
|
19
|
+
templates: `<svg viewBox="0 0 24 24"><rect x="3" y="3" width="8" height="8" rx="1.5"/><rect x="13" y="3" width="8" height="8" rx="1.5"/><rect x="3" y="13" width="8" height="8" rx="1.5"/><rect x="13" y="13" width="8" height="8" rx="1.5"/></svg>`,
|
|
20
|
+
shapes: `<svg viewBox="0 0 24 24"><rect x="4" y="5" width="7" height="7" rx="1.5"/><circle cx="16.5" cy="8.5" r="3.5"/><path d="M7.5 14l3.8 6H3.7z"/><path d="M15 14h4l2 3-2 3h-4l2-3z"/></svg>`,
|
|
21
|
+
sun: `<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M4.9 4.9l1.4 1.4m11.4 11.4l1.4 1.4M2 12h2m16 0h2M4.9 19.1l1.4-1.4m11.4-11.4l1.4-1.4"/></svg>`,
|
|
22
|
+
moon: `<svg viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3 7 7 0 0 0 21 12.8z"/></svg>`,
|
|
23
|
+
export: `<svg viewBox="0 0 24 24"><path d="M12 15V3m0 0L7 8m5-5l5 5M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/></svg>`,
|
|
24
|
+
import: `<svg viewBox="0 0 24 24"><path d="M12 3v12m0 0l-5-5m5 5l5-5M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/></svg>`,
|
|
25
|
+
gauge: `<svg viewBox="0 0 24 24"><path d="M12 14l4-4M3.3 14a9 9 0 1 1 17.4 0"/></svg>`,
|
|
26
|
+
storage: `<svg viewBox="0 0 24 24"><ellipse cx="12" cy="5.5" rx="8" ry="2.5"/><path d="M4 5.5V12c0 1.4 3.6 2.5 8 2.5s8-1.1 8-2.5V5.5M4 12v6.5C4 19.9 7.6 21 12 21s8-1.1 8-2.5V12"/></svg>`,
|
|
27
|
+
flask: `<svg viewBox="0 0 24 24"><path d="M9 3h6M10 3v6L4.5 18a2 2 0 0 0 1.8 3h11.4a2 2 0 0 0 1.8-3L14 9V3"/><path d="M7 15h10"/></svg>`,
|
|
28
|
+
reset: `<svg viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 2.6-6.3M3 4v5h5"/></svg>`,
|
|
29
|
+
camera: `<svg viewBox="0 0 24 24"><path d="M3 8a2 2 0 0 1 2-2h2l2-2h6l2 2h2a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><circle cx="12" cy="13" r="3.5"/></svg>`,
|
|
30
|
+
trash: `<svg viewBox="0 0 24 24"><path d="M4 7h16M10 11v6m4-6v6M6 7l1 13a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1l1-13M9 7V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3"/></svg>`,
|
|
31
|
+
embed: `<svg viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2" stroke-dasharray="3 3"/><path d="M9 10l-2 2 2 2m6-4l2 2-2 2"/></svg>`,
|
|
32
|
+
arrowLeft: `<svg viewBox="0 0 24 24"><path d="M19 12H5m0 0l6-6m-6 6l6 6"/></svg>`,
|
|
33
|
+
search: `<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="M20 20l-4-4"/></svg>`,
|
|
34
|
+
panelLeft: `<svg viewBox="0 0 24 24"><rect x="4" y="4" width="16" height="16" rx="2"/><path d="M10 4v16"/></svg>`,
|
|
35
|
+
edit: `<svg viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"/></svg>`,
|
|
36
|
+
copy: `<svg viewBox="0 0 24 24"><rect x="8" y="8" width="11" height="11" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1"/></svg>`,
|
|
37
|
+
folder: `<svg viewBox="0 0 24 24"><path d="M4 7a2 2 0 0 1 2-2h4l2 2h6a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z"/></svg>`,
|
|
38
|
+
folderOpen: `<svg viewBox="0 0 24 24"><path d="M4 7a2 2 0 0 1 2-2h4l2 2h6a2 2 0 0 1 2 2v1M4 8l1.5 9c.1.6.6 1 1.2 1h10.6c.6 0 1.1-.4 1.2-1L20 8z"/></svg>`,
|
|
39
|
+
file: `<svg viewBox="0 0 24 24"><path d="M13 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V10z"/><path d="M13 3v7h7"/></svg>`,
|
|
40
|
+
fileText: `<svg viewBox="0 0 24 24"><path d="M13 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V10z"/><path d="M13 3v7h7M10 13h4m-4 4h4"/></svg>`,
|
|
41
|
+
board: `<svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="12" y="3" width="9" height="7" rx="1"/><rect x="3" y="12" width="9" height="9" rx="1"/><rect x="14" y="12" width="7" height="9" rx="1"/></svg>`,
|
|
42
|
+
doc: `<svg viewBox="0 0 24 24"><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><path d="M14 3v6h6M10 13h4m-4 4h4m-4-8h1"/></svg>`,
|
|
43
|
+
skill: `<svg viewBox="0 0 24 24"><path d="M12 2l3.1 6.3 6.9 1-5 4.9 1.2 6.8-6.2-3.3-6.2 3.3 1.2-6.8-5-4.9 6.9-1z"/></svg>`,
|
|
44
|
+
plus: `<svg viewBox="0 0 24 24"><path d="M12 5v14m-7-7h14"/></svg>`,
|
|
45
|
+
moreVertical: `<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg>`,
|
|
46
|
+
chevronRight: `<svg viewBox="0 0 24 24"><path d="M9 18l6-6-6-6"/></svg>`,
|
|
47
|
+
chevronDown: `<svg viewBox="0 0 24 24"><path d="M6 9l6 6 6-6"/></svg>`,
|
|
48
|
+
wiki: `<svg viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6M8 13h2m-2 4h6m-6-8h1"/></svg>`,
|
|
49
|
+
list: `<svg viewBox="0 0 24 24"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><circle cx="4" cy="6" r="1"/><circle cx="4" cy="12" r="1"/><circle cx="4" cy="18" r="1"/></svg>`,
|
|
50
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"noUnusedLocals": false,
|
|
15
|
+
"noUnusedParameters": false,
|
|
16
|
+
"noFallthroughCasesInSwitch": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src"]
|
|
19
|
+
}
|