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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +136 -0
  3. package/bin/viteboard.mjs +59 -0
  4. package/content/docs/assets/image-mqjpqlyw.png +0 -0
  5. package/content/docs/assets/image-mqjsmenr.png +0 -0
  6. package/content/docs/boards/product-roadmap.board.json +2130 -0
  7. package/content/docs/capabilities.md +89 -0
  8. package/content/docs/docs.md +84 -0
  9. package/content/docs/guide/boards-in-markdown.md +48 -0
  10. package/content/docs/guide/boards.md +92 -0
  11. package/content/docs/guide/getting-started.md +71 -0
  12. package/content/docs/guide/test.md +6 -0
  13. package/content/docs/index.md +47 -0
  14. package/content/docs/product-roadmap.board.json +2130 -0
  15. package/content/docs/test.md +219 -0
  16. package/content/docs/tetst-dir/test-board.board.json +15 -0
  17. package/content/docs/tetst-dir/test-test.md +1 -0
  18. package/content/docs/workspace.md +73 -0
  19. package/dist/assets/index-CEpxLM2o.css +1 -0
  20. package/dist/assets/index-D4xvJdEQ.js +60 -0
  21. package/dist/index.html +13 -0
  22. package/index.html +13 -0
  23. package/package.json +52 -0
  24. package/src/app/AppController.ts +955 -0
  25. package/src/app/BoardState.ts +130 -0
  26. package/src/app/ClipboardService.ts +159 -0
  27. package/src/app/CommandManager.ts +66 -0
  28. package/src/app/ElementActions.ts +152 -0
  29. package/src/app/EmbedRegionSelector.ts +103 -0
  30. package/src/app/ExportPng.ts +107 -0
  31. package/src/app/ImageInsertService.ts +141 -0
  32. package/src/app/KeyboardShortcuts.ts +124 -0
  33. package/src/app/main.ts +6 -0
  34. package/src/board/BoardPreview.ts +189 -0
  35. package/src/board/BoardService.ts +222 -0
  36. package/src/canvas/CanvasRenderer.ts +253 -0
  37. package/src/canvas/CanvasSurface.ts +70 -0
  38. package/src/canvas/HitTester.ts +110 -0
  39. package/src/canvas/ImageCache.ts +123 -0
  40. package/src/canvas/PerformanceMonitor.ts +31 -0
  41. package/src/canvas/RenderScheduler.ts +26 -0
  42. package/src/canvas/Viewport.ts +77 -0
  43. package/src/content/ContentApi.ts +258 -0
  44. package/src/content/Markdown.ts +431 -0
  45. package/src/content/MarkdownView.ts +70 -0
  46. package/src/editor/DocEditor.ts +799 -0
  47. package/src/editor/htmlToMarkdown.ts +333 -0
  48. package/src/elements/renderElement.ts +509 -0
  49. package/src/elements/types.ts +118 -0
  50. package/src/shell/Shell.ts +2950 -0
  51. package/src/shell/Sidebar.ts +1352 -0
  52. package/src/storage/AssetStore.ts +86 -0
  53. package/src/storage/BoardSerializer.ts +114 -0
  54. package/src/storage/ImportExportService.ts +153 -0
  55. package/src/storage/IndexedDbStore.ts +92 -0
  56. package/src/storage/LocalBoardStore.ts +104 -0
  57. package/src/styles.css +3257 -0
  58. package/src/templates/helpers.ts +124 -0
  59. package/src/templates/index.ts +65 -0
  60. package/src/templates/journeyMapTemplate.ts +52 -0
  61. package/src/templates/opportunityTreeTemplate.ts +45 -0
  62. package/src/templates/personaTemplate.ts +41 -0
  63. package/src/templates/prioritizationMatrixTemplate.ts +44 -0
  64. package/src/templates/requirementsFlowTemplate.ts +45 -0
  65. package/src/templates/screenshotReviewTemplate.ts +52 -0
  66. package/src/templates/systemDiagramTemplate.ts +41 -0
  67. package/src/testing/StressTestGenerator.ts +134 -0
  68. package/src/testing/StressTestPanel.ts +64 -0
  69. package/src/tools/ArrowTool.ts +87 -0
  70. package/src/tools/ImageTool.ts +39 -0
  71. package/src/tools/PanTool.ts +34 -0
  72. package/src/tools/SelectTool.ts +377 -0
  73. package/src/tools/ShapeTool.ts +106 -0
  74. package/src/tools/StickyTool.ts +69 -0
  75. package/src/tools/TaskTool.ts +52 -0
  76. package/src/tools/TextTool.ts +45 -0
  77. package/src/tools/ToolContext.ts +44 -0
  78. package/src/tools/ToolController.ts +178 -0
  79. package/src/ui/Modal.ts +58 -0
  80. package/src/ui/PerformanceOverlay.ts +75 -0
  81. package/src/ui/StorageManager.ts +94 -0
  82. package/src/ui/TemplatePicker.ts +67 -0
  83. package/src/ui/TextEditorOverlay.ts +137 -0
  84. package/src/ui/Toolbar.ts +151 -0
  85. package/src/ui/TopControls.ts +137 -0
  86. package/src/ui/icons.ts +50 -0
  87. package/tsconfig.json +19 -0
  88. package/vite.config.ts +694 -0
@@ -0,0 +1,124 @@
1
+ import { createElement, type BoardElement, type ElementStyle } from "../elements/types";
2
+
3
+ export type TemplateBuild = {
4
+ name: string;
5
+ elements: BoardElement[];
6
+ };
7
+
8
+ export type Palette = {
9
+ card: string;
10
+ cardStroke: string;
11
+ heading: string;
12
+ text: string;
13
+ accent: string;
14
+ sticky: string;
15
+ zone: string;
16
+ };
17
+
18
+ export function palette(theme: "light" | "dark"): Palette {
19
+ if (theme === "dark") {
20
+ return {
21
+ card: "#1e2126",
22
+ cardStroke: "#3a3f46",
23
+ heading: "#f2f2f2",
24
+ text: "#c8cdd4",
25
+ accent: "#4dabf7",
26
+ sticky: "#fef08a",
27
+ zone: "#22262c",
28
+ };
29
+ }
30
+ return {
31
+ card: "#ffffff",
32
+ cardStroke: "#d8dade",
33
+ heading: "#1f2933",
34
+ text: "#4b5563",
35
+ accent: "#6e6e6e",
36
+ sticky: "#fef08a",
37
+ zone: "#f0f0ed",
38
+ };
39
+ }
40
+
41
+ let z = 0;
42
+
43
+ export function beginTemplate(baseZ: number): void {
44
+ z = baseZ;
45
+ }
46
+
47
+ export function rect(
48
+ x: number,
49
+ y: number,
50
+ w: number,
51
+ h: number,
52
+ style: ElementStyle,
53
+ text = ""
54
+ ): BoardElement {
55
+ return createElement("rectangle", {
56
+ x, y, width: w, height: h,
57
+ zIndex: ++z,
58
+ style: { borderRadius: 10, ...style },
59
+ data: { text },
60
+ });
61
+ }
62
+
63
+ export function label(
64
+ x: number,
65
+ y: number,
66
+ w: number,
67
+ text: string,
68
+ fontSize: number,
69
+ color: string,
70
+ fontWeight = "600"
71
+ ): BoardElement {
72
+ return createElement("text", {
73
+ x, y, width: w, height: fontSize * 1.6,
74
+ zIndex: ++z,
75
+ style: { fontSize, textColor: color, fontWeight },
76
+ data: { text },
77
+ });
78
+ }
79
+
80
+ export function sticky(
81
+ x: number,
82
+ y: number,
83
+ text: string,
84
+ fill: string,
85
+ w = 180,
86
+ h = 120
87
+ ): BoardElement {
88
+ return createElement("sticky", {
89
+ x, y, width: w, height: h,
90
+ zIndex: ++z,
91
+ style: { fill, borderRadius: 12, shadow: true, fontSize: 13, textColor: "#1f2933" },
92
+ data: { text },
93
+ });
94
+ }
95
+
96
+ export function arrow(
97
+ x1: number,
98
+ y1: number,
99
+ x2: number,
100
+ y2: number,
101
+ stroke: string
102
+ ): BoardElement {
103
+ return createElement("arrow", {
104
+ x: x1, y: y1, width: x2 - x1, height: y2 - y1,
105
+ zIndex: ++z,
106
+ style: { stroke, strokeWidth: 2, arrowEnd: true },
107
+ });
108
+ }
109
+
110
+ export function ellipseEl(
111
+ x: number,
112
+ y: number,
113
+ w: number,
114
+ h: number,
115
+ style: ElementStyle,
116
+ text = ""
117
+ ): BoardElement {
118
+ return createElement("ellipse", {
119
+ x, y, width: w, height: h,
120
+ zIndex: ++z,
121
+ style,
122
+ data: { text },
123
+ });
124
+ }
@@ -0,0 +1,65 @@
1
+ import type { BoardElement } from "../elements/types";
2
+ import { personaTemplate } from "./personaTemplate";
3
+ import { journeyMapTemplate } from "./journeyMapTemplate";
4
+ import { prioritizationMatrixTemplate } from "./prioritizationMatrixTemplate";
5
+ import { requirementsFlowTemplate } from "./requirementsFlowTemplate";
6
+ import { systemDiagramTemplate } from "./systemDiagramTemplate";
7
+ import { opportunityTreeTemplate } from "./opportunityTreeTemplate";
8
+ import { screenshotReviewTemplate } from "./screenshotReviewTemplate";
9
+
10
+ export type TemplateDef = {
11
+ id: string;
12
+ name: string;
13
+ description: string;
14
+ build: (
15
+ ox: number,
16
+ oy: number,
17
+ theme: "light" | "dark",
18
+ baseZ: number
19
+ ) => BoardElement[];
20
+ };
21
+
22
+ export const TEMPLATES: TemplateDef[] = [
23
+ {
24
+ id: "persona",
25
+ name: "User Persona Card",
26
+ description: "Name, role, goals, pain points, JTBD, success criteria.",
27
+ build: personaTemplate,
28
+ },
29
+ {
30
+ id: "journey",
31
+ name: "User Journey Map",
32
+ description: "Awareness → Retention stages with sticky note areas.",
33
+ build: journeyMapTemplate,
34
+ },
35
+ {
36
+ id: "matrix",
37
+ name: "Prioritization Matrix",
38
+ description: "Impact vs effort: quick wins, strategic bets, fill-ins, avoid.",
39
+ build: prioritizationMatrixTemplate,
40
+ },
41
+ {
42
+ id: "requirements",
43
+ name: "Requirements Flow",
44
+ description: "Problem, user, context, solution, edge cases, risks.",
45
+ build: requirementsFlowTemplate,
46
+ },
47
+ {
48
+ id: "system",
49
+ name: "System Diagram Starter",
50
+ description: "User, frontend, API, database, external services + arrows.",
51
+ build: systemDiagramTemplate,
52
+ },
53
+ {
54
+ id: "ost",
55
+ name: "Opportunity Solution Tree",
56
+ description: "Outcome, opportunities, solutions, experiments, signals.",
57
+ build: opportunityTreeTemplate,
58
+ },
59
+ {
60
+ id: "screenshot-review",
61
+ name: "Screenshot Review Board",
62
+ description: "Screenshot zone with observations, problems, UX notes.",
63
+ build: screenshotReviewTemplate,
64
+ },
65
+ ];
@@ -0,0 +1,52 @@
1
+ import type { BoardElement } from "../elements/types";
2
+ import { palette, beginTemplate, rect, label, sticky, arrow } from "./helpers";
3
+
4
+ const STAGES = [
5
+ "Awareness",
6
+ "Discovery",
7
+ "Evaluation",
8
+ "Onboarding",
9
+ "Activation",
10
+ "Retention",
11
+ ];
12
+
13
+ const STAGE_COLORS = ["#93c5fd", "#a5b4fc", "#d8b4fe", "#f9a8d4", "#fdba74", "#86efac"];
14
+
15
+ export function journeyMapTemplate(
16
+ ox: number,
17
+ oy: number,
18
+ theme: "light" | "dark",
19
+ baseZ: number
20
+ ): BoardElement[] {
21
+ const p = palette(theme);
22
+ beginTemplate(baseZ);
23
+ const els: BoardElement[] = [];
24
+ const colW = 230;
25
+ const totalW = STAGES.length * colW + 56;
26
+
27
+ els.push(rect(ox, oy, totalW, 640, { fill: p.card, stroke: p.cardStroke, strokeWidth: 1.5, borderRadius: 16, shadow: true }));
28
+ els.push(label(ox + 28, oy + 22, 500, "User Journey Map", 22, p.heading, "700"));
29
+
30
+ STAGES.forEach((stage, i) => {
31
+ const x = ox + 28 + i * colW;
32
+ const y = oy + 70;
33
+ els.push(rect(x, y, colW - 16, 48, { fill: STAGE_COLORS[i], borderRadius: 10 }, stage));
34
+ if (i < STAGES.length - 1) {
35
+ els.push(arrow(x + colW - 14, y + 24, x + colW - 2, y + 24, p.heading));
36
+ }
37
+ // sticky zones per stage
38
+ els.push(rect(x, y + 64, colW - 16, 150, { fill: p.zone, borderRadius: 10 }, ""));
39
+ els.push(label(x + 10, y + 70, colW - 36, "Actions", 12, p.accent));
40
+ els.push(sticky(x + 14, y + 96, "", "#fef08a", colW - 44, 100));
41
+
42
+ els.push(rect(x, y + 226, colW - 16, 150, { fill: p.zone, borderRadius: 10 }, ""));
43
+ els.push(label(x + 10, y + 232, colW - 36, "Pain points", 12, p.accent));
44
+ els.push(sticky(x + 14, y + 258, "", "#fca5a5", colW - 44, 100));
45
+
46
+ els.push(rect(x, y + 388, colW - 16, 150, { fill: p.zone, borderRadius: 10 }, ""));
47
+ els.push(label(x + 10, y + 394, colW - 36, "Opportunities", 12, p.accent));
48
+ els.push(sticky(x + 14, y + 420, "", "#86efac", colW - 44, 100));
49
+ });
50
+
51
+ return els;
52
+ }
@@ -0,0 +1,45 @@
1
+ import type { BoardElement } from "../elements/types";
2
+ import { palette, beginTemplate, rect, label, sticky, arrow } from "./helpers";
3
+
4
+ export function opportunityTreeTemplate(
5
+ ox: number,
6
+ oy: number,
7
+ theme: "light" | "dark",
8
+ baseZ: number
9
+ ): BoardElement[] {
10
+ const p = palette(theme);
11
+ beginTemplate(baseZ);
12
+ const els: BoardElement[] = [];
13
+
14
+ els.push(label(ox + 280, oy - 40, 500, "Opportunity Solution Tree", 22, p.heading, "700"));
15
+
16
+ // outcome
17
+ els.push(rect(ox + 300, oy, 280, 80, { fill: "#a5b4fc", borderRadius: 12, shadow: true, fontSize: 15, fontWeight: "600", textColor: "#1f2933" }, "Desired outcome"));
18
+
19
+ // opportunities
20
+ const oppY = oy + 160;
21
+ const oppXs = [ox, ox + 300, ox + 600];
22
+ for (const x of oppXs) {
23
+ els.push(sticky(x, oppY, "Opportunity", "#93c5fd", 220, 100));
24
+ els.push(arrow(ox + 440, oy + 85, x + 110, oppY - 5, p.heading));
25
+ }
26
+
27
+ // solutions
28
+ const solY = oppY + 180;
29
+ for (const x of oppXs) {
30
+ els.push(sticky(x, solY, "Solution", "#86efac", 220, 100));
31
+ els.push(arrow(x + 110, oppY + 105, x + 110, solY - 5, p.heading));
32
+ }
33
+
34
+ // experiments + signals
35
+ const expY = solY + 180;
36
+ for (const x of oppXs) {
37
+ els.push(sticky(x, expY, "Experiment", "#fdba74", 220, 90));
38
+ els.push(arrow(x + 110, solY + 105, x + 110, expY - 5, p.heading));
39
+ }
40
+
41
+ els.push(label(ox, expY + 120, 400, "Signals / metrics", 14, p.accent));
42
+ els.push(sticky(ox, expY + 150, "What signal tells us this is working?", "#fef08a", 380, 100));
43
+
44
+ return els;
45
+ }
@@ -0,0 +1,41 @@
1
+ import type { BoardElement } from "../elements/types";
2
+ import { palette, beginTemplate, rect, label, sticky } from "./helpers";
3
+
4
+ export function personaTemplate(
5
+ ox: number,
6
+ oy: number,
7
+ theme: "light" | "dark",
8
+ baseZ: number
9
+ ): BoardElement[] {
10
+ const p = palette(theme);
11
+ beginTemplate(baseZ);
12
+ const els: BoardElement[] = [];
13
+
14
+ els.push(rect(ox, oy, 760, 560, { fill: p.card, stroke: p.cardStroke, strokeWidth: 1.5, borderRadius: 16, shadow: true }));
15
+ els.push(label(ox + 28, oy + 22, 400, "User Persona", 22, p.heading, "700"));
16
+ els.push(label(ox + 28, oy + 60, 300, "Name", 13, p.accent));
17
+ els.push(sticky(ox + 28, oy + 86, "Jordan Lee", p.sticky, 200, 64));
18
+ els.push(label(ox + 260, oy + 60, 300, "Role", 13, p.accent));
19
+ els.push(sticky(ox + 260, oy + 86, "Senior Product Manager", "#a5b4fc", 220, 64));
20
+ els.push(label(ox + 510, oy + 60, 300, "Buying trigger", 13, p.accent));
21
+ els.push(sticky(ox + 510, oy + 86, "Team scaling pains", "#fdba74", 220, 64));
22
+
23
+ const fields: [string, string, string][] = [
24
+ ["Goals", "Ship the right features faster", "#86efac"],
25
+ ["Pain points", "Scattered docs, unclear priorities", "#fca5a5"],
26
+ ["Jobs to be done", "Align team on what to build next", "#93c5fd"],
27
+ ["Current workaround", "Spreadsheets + slide decks", "#d8b4fe"],
28
+ ["Success criteria", "Stakeholders aligned in 1 meeting", "#f9a8d4"],
29
+ ["Key quote", '"I need clarity, not more tools."', "#fef08a"],
30
+ ];
31
+ fields.forEach(([title, text, color], i) => {
32
+ const col = i % 3;
33
+ const row = Math.floor(i / 3);
34
+ const x = ox + 28 + col * 242;
35
+ const y = oy + 188 + row * 178;
36
+ els.push(label(x, y, 230, title, 13, p.accent));
37
+ els.push(sticky(x, y + 26, text, color, 220, 124));
38
+ });
39
+
40
+ return els;
41
+ }
@@ -0,0 +1,44 @@
1
+ import type { BoardElement } from "../elements/types";
2
+ import { palette, beginTemplate, rect, label, sticky, arrow } from "./helpers";
3
+
4
+ export function prioritizationMatrixTemplate(
5
+ ox: number,
6
+ oy: number,
7
+ theme: "light" | "dark",
8
+ baseZ: number
9
+ ): BoardElement[] {
10
+ const p = palette(theme);
11
+ beginTemplate(baseZ);
12
+ const els: BoardElement[] = [];
13
+ const size = 680;
14
+
15
+ els.push(rect(ox, oy, size + 120, size + 130, { fill: p.card, stroke: p.cardStroke, strokeWidth: 1.5, borderRadius: 16, shadow: true }));
16
+ els.push(label(ox + 28, oy + 22, 500, "Feature Prioritization Matrix", 22, p.heading, "700"));
17
+
18
+ const gx = ox + 80;
19
+ const gy = oy + 80;
20
+ const half = size / 2;
21
+
22
+ // quadrants
23
+ els.push(rect(gx, gy, half, half, { fill: "#86efac55", borderRadius: 8 }));
24
+ els.push(rect(gx + half, gy, half, half, { fill: "#93c5fd55", borderRadius: 8 }));
25
+ els.push(rect(gx, gy + half, half, half, { fill: "#fef08a55", borderRadius: 8 }));
26
+ els.push(rect(gx + half, gy + half, half, half, { fill: "#fca5a555", borderRadius: 8 }));
27
+
28
+ els.push(label(gx + 16, gy + 12, 300, "Quick wins", 16, p.heading, "700"));
29
+ els.push(label(gx + half + 16, gy + 12, 300, "Strategic bets", 16, p.heading, "700"));
30
+ els.push(label(gx + 16, gy + half + 12, 300, "Fill-ins", 16, p.heading, "700"));
31
+ els.push(label(gx + half + 16, gy + half + 12, 300, "Avoid", 16, p.heading, "700"));
32
+
33
+ // axes
34
+ els.push(arrow(gx - 24, gy + size, gx - 24, gy, p.heading));
35
+ els.push(label(gx - 64, gy + half - 12, 60, "Impact", 14, p.text, "600"));
36
+ els.push(arrow(gx, gy + size + 24, gx + size, gy + size + 24, p.heading));
37
+ els.push(label(gx + half - 30, gy + size + 34, 80, "Effort", 14, p.text, "600"));
38
+
39
+ // sample stickies
40
+ els.push(sticky(gx + 40, gy + 60, "Feature idea", "#fef08a", 150, 90));
41
+ els.push(sticky(gx + half + 40, gy + 60, "Big bet", "#a5b4fc", 150, 90));
42
+
43
+ return els;
44
+ }
@@ -0,0 +1,45 @@
1
+ import type { BoardElement } from "../elements/types";
2
+ import { palette, beginTemplate, rect, label, sticky } from "./helpers";
3
+
4
+ const SECTIONS: [string, string][] = [
5
+ ["Problem", "#fca5a5"],
6
+ ["User", "#93c5fd"],
7
+ ["Context", "#d8b4fe"],
8
+ ["Current workflow", "#fdba74"],
9
+ ["Proposed solution", "#86efac"],
10
+ ["Edge cases", "#f9a8d4"],
11
+ ["Acceptance criteria", "#a5b4fc"],
12
+ ["Dependencies", "#fef08a"],
13
+ ["Risks", "#fca5a5"],
14
+ ];
15
+
16
+ export function requirementsFlowTemplate(
17
+ ox: number,
18
+ oy: number,
19
+ theme: "light" | "dark",
20
+ baseZ: number
21
+ ): BoardElement[] {
22
+ const p = palette(theme);
23
+ beginTemplate(baseZ);
24
+ const els: BoardElement[] = [];
25
+ const cols = 3;
26
+ const cellW = 280;
27
+ const cellH = 210;
28
+ const w = cols * cellW + 56;
29
+ const rows = Math.ceil(SECTIONS.length / cols);
30
+ const h = rows * cellH + 100;
31
+
32
+ els.push(rect(ox, oy, w, h, { fill: p.card, stroke: p.cardStroke, strokeWidth: 1.5, borderRadius: 16, shadow: true }));
33
+ els.push(label(ox + 28, oy + 22, 500, "Product Requirements Flow", 22, p.heading, "700"));
34
+
35
+ SECTIONS.forEach(([title, color], i) => {
36
+ const col = i % cols;
37
+ const row = Math.floor(i / cols);
38
+ const x = ox + 28 + col * cellW;
39
+ const y = oy + 72 + row * cellH;
40
+ els.push(label(x, y, cellW - 30, `${i + 1}. ${title}`, 14, p.accent));
41
+ els.push(sticky(x, y + 30, "", color, cellW - 28, cellH - 60));
42
+ });
43
+
44
+ return els;
45
+ }
@@ -0,0 +1,52 @@
1
+ import type { BoardElement } from "../elements/types";
2
+ import { createElement } from "../elements/types";
3
+ import { palette, beginTemplate, rect, label, sticky } from "./helpers";
4
+
5
+ export function screenshotReviewTemplate(
6
+ ox: number,
7
+ oy: number,
8
+ theme: "light" | "dark",
9
+ baseZ: number
10
+ ): BoardElement[] {
11
+ const p = palette(theme);
12
+ beginTemplate(baseZ);
13
+ const els: BoardElement[] = [];
14
+
15
+ els.push(rect(ox, oy, 1060, 620, { fill: p.card, stroke: p.cardStroke, strokeWidth: 1.5, borderRadius: 16, shadow: true }));
16
+ els.push(label(ox + 28, oy + 22, 500, "Screenshot Review", 22, p.heading, "700"));
17
+
18
+ // screenshot drop zone (placeholder image element with no asset = drop target visual)
19
+ els.push(label(ox + 28, oy + 64, 300, "Screenshot", 13, p.accent));
20
+ const placeholder = createElement("frame", {
21
+ x: ox + 28,
22
+ y: oy + 92,
23
+ width: 480,
24
+ height: 360,
25
+ zIndex: baseZ + 100,
26
+ style: { fill: p.zone, stroke: p.cardStroke, strokeWidth: 1.5, borderRadius: 12, fontSize: 13, textColor: p.text },
27
+ data: { text: "Paste a screenshot here (Cmd/Ctrl+V)" },
28
+ });
29
+ els.push(placeholder);
30
+
31
+ const sections: [string, string][] = [
32
+ ["Observations", "#93c5fd"],
33
+ ["Problems", "#fca5a5"],
34
+ ["UX notes", "#d8b4fe"],
35
+ ["Requirements", "#86efac"],
36
+ ];
37
+ sections.forEach(([title, color], i) => {
38
+ const col = i % 2;
39
+ const row = Math.floor(i / 2);
40
+ const x = ox + 540 + col * 250;
41
+ const y = oy + 64 + row * 200;
42
+ els.push(label(x, y, 230, title, 13, p.accent));
43
+ els.push(sticky(x, y + 28, "", color, 230, 140));
44
+ });
45
+
46
+ els.push(label(ox + 28, oy + 470, 300, "Follow-up tasks", 13, p.accent));
47
+ els.push(sticky(ox + 28, oy + 498, "", "#fef08a", 480, 96));
48
+ els.push(label(ox + 540, oy + 470, 300, "Decision", 13, p.accent));
49
+ els.push(sticky(ox + 540, oy + 498, "", "#fdba74", 480, 96));
50
+
51
+ return els;
52
+ }
@@ -0,0 +1,41 @@
1
+ import type { BoardElement } from "../elements/types";
2
+ import { palette, beginTemplate, rect, label, arrow, ellipseEl } from "./helpers";
3
+
4
+ export function systemDiagramTemplate(
5
+ ox: number,
6
+ oy: number,
7
+ theme: "light" | "dark",
8
+ baseZ: number
9
+ ): BoardElement[] {
10
+ const p = palette(theme);
11
+ beginTemplate(baseZ);
12
+ const els: BoardElement[] = [];
13
+
14
+ els.push(label(ox, oy - 40, 400, "System Diagram", 22, p.heading, "700"));
15
+
16
+ const box = (x: number, y: number, w: number, h: number, fill: string, text: string) =>
17
+ rect(x, y, w, h, { fill, stroke: p.cardStroke, strokeWidth: 1.5, borderRadius: 12, shadow: true, fontSize: 15, fontWeight: "600", textColor: "#1f2933" }, text);
18
+
19
+ // user
20
+ els.push(ellipseEl(ox, oy + 130, 110, 110, { fill: "#f9a8d4", stroke: p.cardStroke, strokeWidth: 1.5, fontSize: 14, fontWeight: "600", textColor: "#1f2933" }, "User"));
21
+
22
+ // frontend
23
+ els.push(box(ox + 200, oy + 140, 180, 90, "#93c5fd", "Frontend"));
24
+ // api
25
+ els.push(box(ox + 470, oy + 140, 180, 90, "#a5b4fc", "API / Server"));
26
+ // database
27
+ els.push(box(ox + 740, oy + 60, 180, 90, "#86efac", "Database"));
28
+ // external service
29
+ els.push(box(ox + 740, oy + 200, 180, 90, "#fdba74", "External Service"));
30
+ // notifications
31
+ els.push(box(ox + 470, oy + 330, 180, 90, "#fef08a", "Notification Service"));
32
+
33
+ els.push(arrow(ox + 115, oy + 185, ox + 195, oy + 185, p.heading));
34
+ els.push(arrow(ox + 385, oy + 185, ox + 465, oy + 185, p.heading));
35
+ els.push(arrow(ox + 655, oy + 165, ox + 735, oy + 110, p.heading));
36
+ els.push(arrow(ox + 655, oy + 205, ox + 735, oy + 240, p.heading));
37
+ els.push(arrow(ox + 560, oy + 235, ox + 560, oy + 325, p.heading));
38
+ els.push(arrow(ox + 470, oy + 375, ox + 80, oy + 245, p.heading));
39
+
40
+ return els;
41
+ }
@@ -0,0 +1,134 @@
1
+ import type { BoardState } from "../app/BoardState";
2
+ import { createElement, type BoardElement, type ElementType } from "../elements/types";
3
+ import { STICKY_COLORS } from "../tools/StickyTool";
4
+
5
+ const SHAPE_FILLS = [
6
+ "#93c5fd", "#a5b4fc", "#86efac", "#fdba74", "#fca5a5",
7
+ "#d8b4fe", "#f9a8d4", "#fef08a", "#ffffff", "#e5e7eb",
8
+ ];
9
+
10
+ const WORDS = [
11
+ "Onboarding", "Checkout", "Retention", "Activation", "Churn risk",
12
+ "API gateway", "User insight", "Edge case", "Quick win", "Tech debt",
13
+ "Experiment", "Metric", "Persona", "Journey", "Feature flag",
14
+ ];
15
+
16
+ function rand(min: number, max: number): number {
17
+ return min + Math.random() * (max - min);
18
+ }
19
+
20
+ function pick<T>(arr: T[]): T {
21
+ return arr[Math.floor(Math.random() * arr.length)];
22
+ }
23
+
24
+ const STRESS_TAG = "stress";
25
+
26
+ export class StressTestGenerator {
27
+ private state: BoardState;
28
+ lastGenerateMs = 0;
29
+
30
+ constructor(state: BoardState) {
31
+ this.state = state;
32
+ }
33
+
34
+ generate(count: number): void {
35
+ const start = performance.now();
36
+ const spread = Math.sqrt(count) * 280;
37
+ const types: ElementType[] = ["sticky", "rectangle", "ellipse", "text", "arrow"];
38
+ let z = this.state.maxZIndex();
39
+
40
+ const batch: BoardElement[] = [];
41
+ for (let i = 0; i < count; i++) {
42
+ const type = pick(types);
43
+ const x = rand(-spread / 2, spread / 2);
44
+ const y = rand(-spread / 2, spread / 2);
45
+ let el: BoardElement;
46
+ if (type === "sticky") {
47
+ el = createElement("sticky", {
48
+ x, y, width: 220, height: 160, zIndex: ++z,
49
+ style: { fill: pick(STICKY_COLORS), borderRadius: 12, shadow: true, fontSize: 14, textColor: "#1f2933" },
50
+ data: { text: pick(WORDS), [STRESS_TAG]: true },
51
+ });
52
+ } else if (type === "rectangle" || type === "ellipse") {
53
+ el = createElement(type, {
54
+ x, y, width: rand(80, 300), height: rand(60, 200), zIndex: ++z,
55
+ style: { fill: pick(SHAPE_FILLS), stroke: "#37415188", strokeWidth: 1.5, borderRadius: type === "rectangle" ? 8 : 0 },
56
+ data: { [STRESS_TAG]: true },
57
+ });
58
+ } else if (type === "text") {
59
+ el = createElement("text", {
60
+ x, y, width: 200, height: 28, zIndex: ++z,
61
+ style: { fontSize: 16, textColor: this.state.theme === "dark" ? "#f2f2f2" : "#1f2933" },
62
+ data: { text: pick(WORDS), [STRESS_TAG]: true },
63
+ });
64
+ } else {
65
+ el = createElement("arrow", {
66
+ x, y, width: rand(-250, 250), height: rand(-250, 250), zIndex: ++z,
67
+ style: { stroke: pick(SHAPE_FILLS), strokeWidth: 2, arrowEnd: true },
68
+ data: { [STRESS_TAG]: true },
69
+ });
70
+ }
71
+ batch.push(el);
72
+ }
73
+
74
+ for (const el of batch) this.state.addElement(el);
75
+ this.lastGenerateMs = performance.now() - start;
76
+ this.state.emitChange();
77
+ }
78
+
79
+ generateImagePlaceholders(count: number): void {
80
+ const spread = Math.sqrt(count) * 420;
81
+ let z = this.state.maxZIndex();
82
+ for (let i = 0; i < count; i++) {
83
+ const el = createElement("image", {
84
+ x: rand(-spread / 2, spread / 2),
85
+ y: rand(-spread / 2, spread / 2),
86
+ width: 320,
87
+ height: 220,
88
+ zIndex: ++z,
89
+ data: { assetId: "stress-placeholder-" + i, [STRESS_TAG]: true },
90
+ });
91
+ this.state.addElement(el);
92
+ }
93
+ this.state.emitChange();
94
+ }
95
+
96
+ generateMixedPmBoard(): void {
97
+ this.generate(300);
98
+ }
99
+
100
+ randomizePositions(): void {
101
+ const spread = Math.sqrt(this.state.elements.length) * 280;
102
+ for (const el of this.state.elements) {
103
+ if (!el.data[STRESS_TAG]) continue;
104
+ el.x = rand(-spread / 2, spread / 2);
105
+ el.y = rand(-spread / 2, spread / 2);
106
+ }
107
+ this.state.emitChange();
108
+ }
109
+
110
+ randomizeColors(): void {
111
+ for (const el of this.state.elements) {
112
+ if (!el.data[STRESS_TAG]) continue;
113
+ if (el.type === "sticky") el.style.fill = pick(STICKY_COLORS);
114
+ else if (el.type === "rectangle" || el.type === "ellipse") el.style.fill = pick(SHAPE_FILLS);
115
+ else if (el.type === "arrow") el.style.stroke = pick(SHAPE_FILLS);
116
+ }
117
+ this.state.emitChange();
118
+ }
119
+
120
+ randomizeZIndex(): void {
121
+ for (const el of this.state.elements) {
122
+ if (!el.data[STRESS_TAG]) continue;
123
+ el.zIndex = Math.floor(rand(0, this.state.elements.length));
124
+ }
125
+ this.state.invalidateSort();
126
+ this.state.emitChange();
127
+ }
128
+
129
+ clearGenerated(): void {
130
+ const toRemove = this.state.elements.filter((el) => el.data[STRESS_TAG]);
131
+ for (const el of toRemove) this.state.removeElement(el.id);
132
+ this.state.emitChange();
133
+ }
134
+ }