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