synabun 1.3.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 (187) hide show
  1. package/CHANGELOG.md +665 -0
  2. package/LICENSE +191 -0
  3. package/README.md +853 -0
  4. package/hooks/claude-code/post-plan.mjs +391 -0
  5. package/hooks/claude-code/post-remember.mjs +239 -0
  6. package/hooks/claude-code/pre-compact.mjs +241 -0
  7. package/hooks/claude-code/pre-websearch.mjs +90 -0
  8. package/hooks/claude-code/prompt-submit.mjs +861 -0
  9. package/hooks/claude-code/session-start.mjs +494 -0
  10. package/hooks/claude-code/shared.mjs +368 -0
  11. package/hooks/claude-code/stop.mjs +646 -0
  12. package/hooks/claude-code/test-user-learning.mjs +531 -0
  13. package/mcp-server/package.json +38 -0
  14. package/mcp-server/run.mjs +10 -0
  15. package/mcp-server/src/config.ts +70 -0
  16. package/mcp-server/src/http.ts +93 -0
  17. package/mcp-server/src/index.ts +203 -0
  18. package/mcp-server/src/preload-http.ts +25 -0
  19. package/mcp-server/src/preload.ts +29 -0
  20. package/mcp-server/src/scripts/migrate-qdrant-to-sqlite.ts +506 -0
  21. package/mcp-server/src/services/categories.ts +429 -0
  22. package/mcp-server/src/services/discord.ts +702 -0
  23. package/mcp-server/src/services/file-checksums.ts +33 -0
  24. package/mcp-server/src/services/local-embeddings.ts +77 -0
  25. package/mcp-server/src/services/neural-interface.ts +344 -0
  26. package/mcp-server/src/services/sqlite.ts +823 -0
  27. package/mcp-server/src/tools/browser-advanced.ts +123 -0
  28. package/mcp-server/src/tools/browser-interact.ts +255 -0
  29. package/mcp-server/src/tools/browser-navigate.ts +77 -0
  30. package/mcp-server/src/tools/browser-observe.ts +1330 -0
  31. package/mcp-server/src/tools/browser.ts +107 -0
  32. package/mcp-server/src/tools/card-tools.ts +193 -0
  33. package/mcp-server/src/tools/card.ts +22 -0
  34. package/mcp-server/src/tools/category.ts +379 -0
  35. package/mcp-server/src/tools/discord-channel.ts +274 -0
  36. package/mcp-server/src/tools/discord-guild.ts +188 -0
  37. package/mcp-server/src/tools/discord-member.ts +158 -0
  38. package/mcp-server/src/tools/discord-message.ts +231 -0
  39. package/mcp-server/src/tools/discord-onboarding.ts +228 -0
  40. package/mcp-server/src/tools/discord-role.ts +209 -0
  41. package/mcp-server/src/tools/discord-thread.ts +110 -0
  42. package/mcp-server/src/tools/discord-webhook.ts +176 -0
  43. package/mcp-server/src/tools/discord.ts +40 -0
  44. package/mcp-server/src/tools/forget.ts +38 -0
  45. package/mcp-server/src/tools/git-tools.ts +141 -0
  46. package/mcp-server/src/tools/git.ts +13 -0
  47. package/mcp-server/src/tools/image-tools.ts +98 -0
  48. package/mcp-server/src/tools/image.ts +17 -0
  49. package/mcp-server/src/tools/leonardo-browser-tools.ts +281 -0
  50. package/mcp-server/src/tools/leonardo.ts +28 -0
  51. package/mcp-server/src/tools/loop.ts +348 -0
  52. package/mcp-server/src/tools/memories.ts +118 -0
  53. package/mcp-server/src/tools/recall.ts +323 -0
  54. package/mcp-server/src/tools/reflect.ts +145 -0
  55. package/mcp-server/src/tools/remember.ts +115 -0
  56. package/mcp-server/src/tools/response.ts +26 -0
  57. package/mcp-server/src/tools/restore.ts +34 -0
  58. package/mcp-server/src/tools/sync.ts +114 -0
  59. package/mcp-server/src/tools/tictactoe-tools.ts +98 -0
  60. package/mcp-server/src/tools/tictactoe.ts +13 -0
  61. package/mcp-server/src/tools/utils.ts +25 -0
  62. package/mcp-server/src/tools/whiteboard-tools.ts +260 -0
  63. package/mcp-server/src/tools/whiteboard.ts +22 -0
  64. package/mcp-server/src/tui.ts +940 -0
  65. package/mcp-server/src/types.ts +85 -0
  66. package/mcp-server/tsconfig.json +19 -0
  67. package/neural-interface/package.json +27 -0
  68. package/neural-interface/public/apple-touch-icon.png +0 -0
  69. package/neural-interface/public/category-logos/claude.svg +1 -0
  70. package/neural-interface/public/category-logos/gemini-color.svg +1 -0
  71. package/neural-interface/public/category-logos/openai.svg +1 -0
  72. package/neural-interface/public/category-logos/synabun.png +0 -0
  73. package/neural-interface/public/claude-chat.css +878 -0
  74. package/neural-interface/public/claude-chat.html +92 -0
  75. package/neural-interface/public/claude-chat.js +1168 -0
  76. package/neural-interface/public/favicon-16x16.png +0 -0
  77. package/neural-interface/public/favicon-32x32.png +0 -0
  78. package/neural-interface/public/gallery.svg +6 -0
  79. package/neural-interface/public/icon-192.png +0 -0
  80. package/neural-interface/public/icon-512.png +0 -0
  81. package/neural-interface/public/index-legacy.html +14677 -0
  82. package/neural-interface/public/index.html +212 -0
  83. package/neural-interface/public/index2d-legacy.html +8193 -0
  84. package/neural-interface/public/index2d.html +127 -0
  85. package/neural-interface/public/invite.html +351 -0
  86. package/neural-interface/public/leonardoai.svg +1 -0
  87. package/neural-interface/public/logo-synabun.png +0 -0
  88. package/neural-interface/public/logoHD.png +0 -0
  89. package/neural-interface/public/manifest.json +14 -0
  90. package/neural-interface/public/offline.html +299 -0
  91. package/neural-interface/public/onboarding.html +2335 -0
  92. package/neural-interface/public/openclaw-logo-text.png +0 -0
  93. package/neural-interface/public/pin-svgrepo-com.svg +25 -0
  94. package/neural-interface/public/sessionicon.svg +8 -0
  95. package/neural-interface/public/shared/ansi-parser.js +305 -0
  96. package/neural-interface/public/shared/api.js +1043 -0
  97. package/neural-interface/public/shared/colors.js +153 -0
  98. package/neural-interface/public/shared/constants.js +141 -0
  99. package/neural-interface/public/shared/html-shell.js +777 -0
  100. package/neural-interface/public/shared/html-term-renderer.js +1022 -0
  101. package/neural-interface/public/shared/i18n.js +147 -0
  102. package/neural-interface/public/shared/registry.js +82 -0
  103. package/neural-interface/public/shared/state.js +86 -0
  104. package/neural-interface/public/shared/storage.js +150 -0
  105. package/neural-interface/public/shared/styles.css +21212 -0
  106. package/neural-interface/public/shared/ui-automation-studio.js +3573 -0
  107. package/neural-interface/public/shared/ui-bookmarks.js +231 -0
  108. package/neural-interface/public/shared/ui-claude-panel.js +6333 -0
  109. package/neural-interface/public/shared/ui-command-runner.js +534 -0
  110. package/neural-interface/public/shared/ui-cost-widget.js +602 -0
  111. package/neural-interface/public/shared/ui-detail.js +1454 -0
  112. package/neural-interface/public/shared/ui-explorer.js +1356 -0
  113. package/neural-interface/public/shared/ui-file-explorer.js +2750 -0
  114. package/neural-interface/public/shared/ui-games.js +411 -0
  115. package/neural-interface/public/shared/ui-help.js +107 -0
  116. package/neural-interface/public/shared/ui-image-gallery.js +461 -0
  117. package/neural-interface/public/shared/ui-invite.js +519 -0
  118. package/neural-interface/public/shared/ui-keybinds.js +452 -0
  119. package/neural-interface/public/shared/ui-layouts.js +297 -0
  120. package/neural-interface/public/shared/ui-link.js +731 -0
  121. package/neural-interface/public/shared/ui-loading.js +255 -0
  122. package/neural-interface/public/shared/ui-menubar.js +499 -0
  123. package/neural-interface/public/shared/ui-multiselect.js +93 -0
  124. package/neural-interface/public/shared/ui-navbar.js +222 -0
  125. package/neural-interface/public/shared/ui-notifications.js +287 -0
  126. package/neural-interface/public/shared/ui-panels.js +309 -0
  127. package/neural-interface/public/shared/ui-resume.js +653 -0
  128. package/neural-interface/public/shared/ui-search.js +145 -0
  129. package/neural-interface/public/shared/ui-sessions.js +531 -0
  130. package/neural-interface/public/shared/ui-settings.js +5987 -0
  131. package/neural-interface/public/shared/ui-sidebar.js +1905 -0
  132. package/neural-interface/public/shared/ui-skills.js +2805 -0
  133. package/neural-interface/public/shared/ui-stats.js +38 -0
  134. package/neural-interface/public/shared/ui-sync.js +201 -0
  135. package/neural-interface/public/shared/ui-terminal.js +6385 -0
  136. package/neural-interface/public/shared/ui-tooltip.js +123 -0
  137. package/neural-interface/public/shared/ui-trash.js +439 -0
  138. package/neural-interface/public/shared/ui-tutorial-draw.js +875 -0
  139. package/neural-interface/public/shared/ui-tutorial-steps.js +2276 -0
  140. package/neural-interface/public/shared/ui-tutorial.js +1117 -0
  141. package/neural-interface/public/shared/ui-update.js +244 -0
  142. package/neural-interface/public/shared/ui-whiteboard.js +3262 -0
  143. package/neural-interface/public/shared/ui-workspaces.js +722 -0
  144. package/neural-interface/public/shared/utils.js +360 -0
  145. package/neural-interface/public/shared/vterm-buffer.js +1102 -0
  146. package/neural-interface/public/sw.js +52 -0
  147. package/neural-interface/public/synabun.png +0 -0
  148. package/neural-interface/public/toolbar-album.svg +8 -0
  149. package/neural-interface/public/toolbar-question.svg +6 -0
  150. package/neural-interface/public/toolbar-sessions.svg +6 -0
  151. package/neural-interface/public/variant/2d/background.js +113 -0
  152. package/neural-interface/public/variant/2d/context-menu.js +133 -0
  153. package/neural-interface/public/variant/2d/gfx.js +45 -0
  154. package/neural-interface/public/variant/2d/graph.js +1635 -0
  155. package/neural-interface/public/variant/2d/hulls.js +207 -0
  156. package/neural-interface/public/variant/2d/lasso.js +135 -0
  157. package/neural-interface/public/variant/2d/layout.js +594 -0
  158. package/neural-interface/public/variant/2d/main.js +521 -0
  159. package/neural-interface/public/variant/2d/minimap.js +312 -0
  160. package/neural-interface/public/variant/2d/settings-gfx.js +98 -0
  161. package/neural-interface/public/variant/3d/background.js +884 -0
  162. package/neural-interface/public/variant/3d/camera.js +661 -0
  163. package/neural-interface/public/variant/3d/gfx.js +116 -0
  164. package/neural-interface/public/variant/3d/graph.js +2399 -0
  165. package/neural-interface/public/variant/3d/main.js +500 -0
  166. package/neural-interface/public/variant/3d/settings-gfx.js +228 -0
  167. package/neural-interface/scripts/rebuild-pty.js +100 -0
  168. package/neural-interface/server.js +15000 -0
  169. package/neural-interface/templates/CLAUDE-template.md +211 -0
  170. package/package.json +53 -0
  171. package/postinstall.js +135 -0
  172. package/setup.js +260 -0
  173. package/skills/leonardo/SKILL.md +159 -0
  174. package/skills/leonardo/icon.svg +1 -0
  175. package/skills/leonardo/modules/browser-guide.md +378 -0
  176. package/skills/leonardo/modules/image-prompter.md +277 -0
  177. package/skills/leonardo/modules/model-advisor.md +100 -0
  178. package/skills/leonardo/modules/prompt-library.md +110 -0
  179. package/skills/leonardo/modules/style-guide.md +186 -0
  180. package/skills/leonardo/modules/video-prompter.md +311 -0
  181. package/skills/synabun/SKILL.md +185 -0
  182. package/skills/synabun/icon.png +0 -0
  183. package/skills/synabun/modules/audit.md +374 -0
  184. package/skills/synabun/modules/changelog.md +194 -0
  185. package/skills/synabun/modules/idea.md +144 -0
  186. package/skills/synabun/modules/memorize.md +188 -0
  187. package/skills/synabun/synabunicon.png +0 -0
@@ -0,0 +1,2276 @@
1
+ // ═══════════════════════════════════════════
2
+ // TUTORIAL STEP DEFINITIONS
3
+ // ═══════════════════════════════════════════
4
+ //
5
+ // Each step describes what to show and how.
6
+ // Steps with whiteboardMode: true render directly on the whiteboard
7
+ // using a render() function. Others use the legacy overlay+card mode.
8
+
9
+ import {
10
+ handwrittenLabel,
11
+ createDoodle,
12
+ createHandDrawnUnderline,
13
+ createAnimatedArrow,
14
+ createWobblyCircle,
15
+ createHighlightCircle,
16
+ } from './ui-tutorial-draw.js';
17
+ import { emit } from './state.js';
18
+ import { storage, flushStorage } from './storage.js';
19
+ import { KEYS } from './constants.js';
20
+ import { createTerminalSession } from './api.js';
21
+ import { toggleExplorer } from './ui-explorer.js';
22
+ import { toggleFileExplorer } from './ui-file-explorer.js';
23
+ import { toggleClaudePanel, isClaudePanelOpen } from './ui-claude-panel.js';
24
+
25
+
26
+ // ── Settings hint (shown when user declines explore) ──────────
27
+ function _showSettingsHint(svg, dom, cx, cy, goToStep) {
28
+ svg.innerHTML = '';
29
+ dom.innerHTML = '';
30
+
31
+ handwrittenLabel(svg, 'You can do this at any time from Project Settings', {
32
+ x: cx,
33
+ y: cy,
34
+ color: 'rgba(255, 255, 255, 0.5)',
35
+ fontSize: 28,
36
+ textAnchor: 'middle',
37
+ duration: 400,
38
+ });
39
+
40
+ createDoodle(svg, 'star', {
41
+ x: cx + 280,
42
+ y: cy - 30,
43
+ scale: 0.9,
44
+ color: 'rgba(255, 255, 255, 0.25)',
45
+ duration: 400,
46
+ });
47
+
48
+ createDoodle(svg, 'sparkle', {
49
+ x: cx - 290,
50
+ y: cy - 20,
51
+ scale: 0.8,
52
+ color: 'rgba(255, 255, 255, 0.2)',
53
+ duration: 350,
54
+ });
55
+
56
+ // Continue button — user clicks to advance (no auto-timer)
57
+ const contEl = document.createElement('div');
58
+ contEl.className = 'wb-tutorial-btn';
59
+ contEl.textContent = 'Continue';
60
+ contEl.style.cssText = `
61
+ position: absolute; left: ${cx}px; top: ${cy + 60}px;
62
+ transform: translate(-50%, 0);
63
+ font-family: 'Caveat', cursive; font-size: 22px;
64
+ color: rgba(255, 255, 255, 0.35);
65
+ pointer-events: auto; opacity: 0;
66
+ transition: opacity 0.4s ease 0.8s;
67
+ `;
68
+ dom.appendChild(contEl);
69
+ requestAnimationFrame(() => { contEl.style.opacity = '1'; });
70
+ contEl.addEventListener('mouseenter', () => {
71
+ contEl.style.color = 'rgba(255, 255, 255, 0.7)';
72
+ contEl.style.transform = 'translate(-50%, 0) scale(1.08)';
73
+ });
74
+ contEl.addEventListener('mouseleave', () => {
75
+ contEl.style.color = 'rgba(255, 255, 255, 0.35)';
76
+ contEl.style.transform = 'translate(-50%, 0) scale(1)';
77
+ });
78
+ contEl.addEventListener('click', () => {
79
+ goToStep(6);
80
+ });
81
+ }
82
+
83
+
84
+ // ── Exploration prompt (injected into CLI on launch) ────────
85
+ // This prompt MUST be forceful and explicit — weaker models (Haiku, Flash, o4-mini)
86
+ // will only follow instructions that are direct and structured as mandatory steps.
87
+ export function buildExplorePrompt(projectSlug) {
88
+ const cat = projectSlug || 'project';
89
+ const catProject = `${cat}-project`;
90
+ const catArch = `${cat}-architecture`;
91
+ const catConfig = `${cat}-config`;
92
+
93
+ // PTY sends raw bytes — newlines would trigger early submission in CLI prompts.
94
+ // Join as single line with ' | ' separating phases for readability by the model.
95
+ return [
96
+ `EXPLORATION MODE — FULL CODEBASE ANALYSIS.`,
97
+ `You are a codebase analyst. Your ONLY job is to deeply explore this project and create detailed persistent memories using SynaBun MCP tools. Do NOT ask for confirmation. Do NOT summarize what you will do. Execute each phase immediately.`,
98
+
99
+ `PHASE 0 — SETUP CATEGORIES (do this FIRST):`,
100
+ `Call the \`category\` tool 4 times with action "create":`,
101
+ `(1) name: "${cat}", description: "Knowledge and context for the ${cat} project", is_parent: true.`,
102
+ `(2) name: "${catProject}", description: "General project knowledge, decisions, and milestones", parent: "${cat}".`,
103
+ `(3) name: "${catArch}", description: "System design, tech stack, data flow, and component architecture", parent: "${cat}".`,
104
+ `(4) name: "${catConfig}", description: "Configuration, deployment, environment, and infrastructure", parent: "${cat}".`,
105
+ `If a category already exists, skip it and continue.`,
106
+
107
+ `PHASE 1 — PROJECT IDENTITY (create 2-3 memories in "${catProject}"):`,
108
+ `Read README.md, package.json (or Cargo.toml/go.mod/pyproject.toml), and any CONTRIBUTING or ARCHITECTURE docs. Create memories covering: project purpose, what it does, who it's for, main scripts/commands, key dependencies and their roles.`,
109
+
110
+ `PHASE 2 — DIRECTORY MAP (create 1-2 memories in "${catArch}"):`,
111
+ `List the top-level directory structure, then list contents of each major source directory (src/, lib/, app/, components/, etc.). Create a memory with the full directory tree and a brief note on what each directory contains.`,
112
+
113
+ `PHASE 3 — ARCHITECTURE DEEP DIVE (create 4-8 memories in "${catArch}"):`,
114
+ `Read and analyze these areas ONE BY ONE. For EACH area, read the actual source files, then create a SEPARATE memory:`,
115
+ `(a) Entry points and routing — how the app starts, route definitions, middleware.`,
116
+ `(b) Core modules — main business logic files, what each does, how they interact.`,
117
+ `(c) Data layer — database schemas, ORMs, migrations, API clients, data models and types.`,
118
+ `(d) State management — stores, contexts, reducers, reactive patterns.`,
119
+ `(e) Component architecture — UI component hierarchy, shared components, layouts.`,
120
+ `(f) API surface — endpoints, handlers, request/response shapes, authentication flow.`,
121
+ `(g) Key patterns — singletons, dependency injection, event systems, error handling conventions.`,
122
+ `(h) External integrations — third-party APIs, SDKs, webhooks.`,
123
+ `Skip areas that do not apply. But for each that DOES exist, you MUST read the files and create a memory.`,
124
+
125
+ `PHASE 4 — CONFIGURATION (create 2-4 memories in "${catConfig}"):`,
126
+ `(a) Build config — bundler, compiler settings, output targets.`,
127
+ `(b) Environment — .env structure, required env vars, feature flags.`,
128
+ `(c) CI/CD — deployment scripts, Docker configs, hosting setup.`,
129
+ `(d) Dev tooling — linting, formatting, testing framework, pre-commit hooks.`,
130
+
131
+ `PHASE 5 — CODE STYLE (create 1 memory in "${catProject}"):`,
132
+ `Read 3-4 representative source files. Note: file naming, variable naming, import style, module pattern, indentation, comment style.`,
133
+
134
+ `PHASE 6 — FINAL SUMMARY (create 1 memory in "${catProject}", importance: 8):`,
135
+ `Create one comprehensive summary a developer needs to understand this project from scratch: what it is, how it is built, how to work on it, what to watch out for.`,
136
+
137
+ `RULES:`,
138
+ `Every \`remember\` call MUST include: related_files (array of file paths you read), importance (6-7 for details, 8 for summaries), project: "${cat}", and 3-5 tags.`,
139
+ `You MUST create at minimum 12 memories total. If you finish with fewer, go back and explore deeper.`,
140
+ `Read actual file contents — do NOT guess from file names alone.`,
141
+ `Do NOT batch everything into one giant memory — each memory covers ONE specific topic.`,
142
+ `Do NOT ask the user anything. Do NOT explain what you are about to do. Just execute.`,
143
+ `After all phases, say "Exploration complete" and list memories created per category.`,
144
+ `Begin PHASE 0 now.`,
145
+ ].join(' ');
146
+ }
147
+
148
+ // ── Launch exploration CLI session ──────────────────────────
149
+ async function _launchExploration(goToStep) {
150
+ console.log('[SynaBun] _launchExploration: starting');
151
+ const cli = storage.getItem(KEYS.ONBOARDING_CLI) || 'claude-code';
152
+ const model = storage.getItem(KEYS.ONBOARDING_MODEL) || null;
153
+
154
+ // Use the project chosen in the onboarding project picker (step 4)
155
+ let cwd = null;
156
+ let projectSlug = null;
157
+ try {
158
+ const stored = JSON.parse(storage.getItem(KEYS.ONBOARDING_PROJECT) || 'null');
159
+ if (stored?.path) {
160
+ cwd = stored.path;
161
+ const folderName = stored.label || cwd.split(/[\\/]/).filter(Boolean).pop() || 'project';
162
+ projectSlug = folderName.toLowerCase().replace(/[^a-z0-9]+/g, '');
163
+ }
164
+ } catch { /* fallback to null (home dir) */ }
165
+
166
+ const prompt = buildExplorePrompt(projectSlug);
167
+
168
+ try {
169
+ const result = await createTerminalSession(cli, 120, 30, cwd, model ? { model } : {});
170
+ console.log('[SynaBun] _launchExploration: session created, id =', result?.sessionId);
171
+ if (result?.sessionId) {
172
+ emit('terminal:attach-floating', {
173
+ terminalSessionId: result.sessionId,
174
+ profile: cli,
175
+ initialMessage: prompt,
176
+ autoSubmit: true,
177
+ });
178
+ }
179
+ } catch (err) {
180
+ console.error('[SynaBun] Exploration launch failed:', err);
181
+ }
182
+
183
+ // Advance to tutorial welcome regardless of launch success
184
+ goToStep(6);
185
+ }
186
+
187
+
188
+ export const TUTORIAL_STEPS = [
189
+ // ── Step 0: Explore Your Project? (onboarding pre-step) ───
190
+ {
191
+ id: 'onboarding-explore',
192
+ whiteboardMode: true,
193
+
194
+ render({ svg, dom, seq, vw, vh, nextStep, goToStep, createButton }) {
195
+ const cx = vw / 2;
196
+ const cy = vh / 2 - 40;
197
+
198
+ // 1. Hand-drawn question text
199
+ seq.add(() => handwrittenLabel(svg, 'Explore your project and create memories?', {
200
+ x: cx,
201
+ y: cy,
202
+ color: 'rgba(255, 255, 255, 0.55)',
203
+ fontSize: 34,
204
+ textAnchor: 'middle',
205
+ duration: 400,
206
+ }), 200);
207
+
208
+ // 2. Decorative sparkle doodles
209
+ seq.add(() => createDoodle(svg, 'sparkle', {
210
+ x: cx - 260,
211
+ y: cy - 55,
212
+ scale: 1.1,
213
+ color: 'rgba(255, 255, 255, 0.3)',
214
+ duration: 350,
215
+ }), 200);
216
+
217
+ seq.add(() => createDoodle(svg, 'sparkle', {
218
+ x: cx + 230,
219
+ y: cy + 15,
220
+ scale: 0.85,
221
+ color: 'rgba(255, 255, 255, 0.3)',
222
+ duration: 350,
223
+ }), 200);
224
+
225
+ // 3. Yes / No buttons
226
+ seq.add(() => {
227
+ const btnY = cy + 50;
228
+ const gap = 80;
229
+
230
+ const yesEl = createButton(dom, 'Yes', {
231
+ x: cx - gap,
232
+ y: btnY,
233
+ color: 'rgba(255, 255, 255, 0.5)',
234
+ fontSize: 28,
235
+ onClick: () => {
236
+ storage.setItem(KEYS.ONBOARDING_EXPLORE, 'yes');
237
+ flushStorage();
238
+ nextStep();
239
+ },
240
+ });
241
+
242
+ const noEl = createButton(dom, 'No', {
243
+ x: cx + gap,
244
+ y: btnY,
245
+ color: 'rgba(255, 255, 255, 0.5)',
246
+ fontSize: 28,
247
+ onClick: () => {
248
+ storage.setItem(KEYS.ONBOARDING_EXPLORE, 'no');
249
+ flushStorage();
250
+ _showSettingsHint(svg, dom, cx, cy, goToStep);
251
+ },
252
+ });
253
+
254
+ // Hand-drawn underlines beneath buttons
255
+ setTimeout(() => {
256
+ const yesRect = yesEl.getBoundingClientRect();
257
+ const noRect = noEl.getBoundingClientRect();
258
+ createHandDrawnUnderline(svg, yesRect, {
259
+ color: 'rgba(255, 255, 255, 0.22)',
260
+ wobbleSeed: 'explore-yes-ul', duration: 250, offset: 2,
261
+ });
262
+ createHandDrawnUnderline(svg, noRect, {
263
+ color: 'rgba(255, 255, 255, 0.22)',
264
+ wobbleSeed: 'explore-no-ul', duration: 250, offset: 2,
265
+ });
266
+ }, 100);
267
+
268
+ return {
269
+ destroy() { yesEl.remove(); noEl.remove(); },
270
+ };
271
+ }, 200);
272
+ },
273
+ },
274
+
275
+ // ── Step 1: Token Warning (Red Alert) ─────────────────────
276
+ {
277
+ id: 'onboarding-token-warning',
278
+ whiteboardMode: true,
279
+
280
+ render({ svg, dom, seq, vw, vh, nextStep, prevStep, goToStep, createButton }) {
281
+ const cx = vw / 2;
282
+ const cy = vh / 2 - 40;
283
+
284
+ // 1. Red-tinted alert heading
285
+ seq.add(() => handwrittenLabel(svg, 'Heads up!', {
286
+ x: cx,
287
+ y: cy - 30,
288
+ color: 'rgba(239, 83, 80, 0.65)',
289
+ fontSize: 38,
290
+ textAnchor: 'middle',
291
+ duration: 400,
292
+ }), 200);
293
+
294
+ // 2. Warning message body (white, not red)
295
+ seq.add(() => handwrittenLabel(svg, 'Exploring your project can burn tokens.', {
296
+ x: cx,
297
+ y: cy + 20,
298
+ color: 'rgba(255, 255, 255, 0.45)',
299
+ fontSize: 26,
300
+ textAnchor: 'middle',
301
+ duration: 350,
302
+ }), 300);
303
+
304
+ seq.add(() => handwrittenLabel(svg, 'Choose your model and provider with caution.', {
305
+ x: cx,
306
+ y: cy + 55,
307
+ color: 'rgba(255, 255, 255, 0.35)',
308
+ fontSize: 24,
309
+ textAnchor: 'middle',
310
+ duration: 350,
311
+ }), 200);
312
+
313
+ // 3. Alert triangle doodles
314
+ seq.add(() => createDoodle(svg, 'alert', {
315
+ x: cx - 250,
316
+ y: cy - 50,
317
+ scale: 1.0,
318
+ color: 'rgba(239, 83, 80, 0.3)',
319
+ duration: 350,
320
+ }), 200);
321
+
322
+ seq.add(() => createDoodle(svg, 'alert', {
323
+ x: cx + 230,
324
+ y: cy + 10,
325
+ scale: 0.8,
326
+ color: 'rgba(239, 83, 80, 0.25)',
327
+ duration: 350,
328
+ }), 200);
329
+
330
+ // 4. Continue / Back buttons
331
+ seq.add(() => {
332
+ const btnY = cy + 110;
333
+ const gap = 90;
334
+
335
+ const backEl = createButton(dom, 'Back', {
336
+ x: cx - gap,
337
+ y: btnY,
338
+ color: 'rgba(255, 255, 255, 0.35)',
339
+ fontSize: 24,
340
+ onClick: () => prevStep(),
341
+ });
342
+
343
+ const contEl = createButton(dom, 'Continue', {
344
+ x: cx + gap,
345
+ y: btnY,
346
+ color: 'rgba(255, 255, 255, 0.45)',
347
+ fontSize: 24,
348
+ onClick: () => nextStep(),
349
+ });
350
+
351
+ // Subtle underlines
352
+ setTimeout(() => {
353
+ const backRect = backEl.getBoundingClientRect();
354
+ const contRect = contEl.getBoundingClientRect();
355
+ createHandDrawnUnderline(svg, backRect, {
356
+ color: 'rgba(255, 255, 255, 0.15)',
357
+ wobbleSeed: 'warn-back-ul', duration: 250, offset: 2,
358
+ });
359
+ createHandDrawnUnderline(svg, contRect, {
360
+ color: 'rgba(255, 255, 255, 0.15)',
361
+ wobbleSeed: 'warn-cont-ul', duration: 250, offset: 2,
362
+ });
363
+ }, 100);
364
+
365
+ return {
366
+ destroy() { backEl.remove(); contEl.remove(); },
367
+ };
368
+ }, 200);
369
+ },
370
+ },
371
+
372
+ // ── Step 2: CLI Picker ─────────────────────────────────────
373
+ {
374
+ id: 'onboarding-cli-picker',
375
+ whiteboardMode: true,
376
+
377
+ render({ svg, dom, seq, vw, vh, nextStep, prevStep, goToStep, createButton }) {
378
+ const cx = vw / 2;
379
+ const cy = vh / 2 - 50;
380
+
381
+ // SVG logo paths (same as tic-tac-toe opponent picker)
382
+ const ICON_CLAUDE = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z"/></svg>';
383
+ const ICON_OPENAI = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M9.205 8.658v-2.26c0-.19.072-.333.238-.428l4.543-2.616c.619-.357 1.356-.523 2.117-.523 2.854 0 4.662 2.212 4.662 4.566 0 .167 0 .357-.024.547l-4.71-2.759a.797.797 0 00-.856 0l-5.97 3.473zm10.609 8.8V12.06c0-.333-.143-.57-.429-.737l-5.97-3.473 1.95-1.118a.433.433 0 01.476 0l4.543 2.617c1.309.76 2.189 2.378 2.189 3.948 0 1.808-1.07 3.473-2.76 4.163zM7.802 12.703l-1.95-1.142c-.167-.095-.239-.238-.239-.428V5.899c0-2.545 1.95-4.472 4.591-4.472 1 0 1.927.333 2.712.928L8.23 5.067c-.285.166-.428.404-.428.737v6.898zM12 15.128l-2.795-1.57v-3.33L12 8.658l2.795 1.57v3.33L12 15.128zm1.796 7.23c-1 0-1.927-.332-2.712-.927l4.686-2.712c.285-.166.428-.404.428-.737v-6.898l1.974 1.142c.167.095.238.238.238.428v5.233c0 2.545-1.974 4.472-4.614 4.472zm-5.637-5.303l-4.544-2.617c-1.308-.761-2.188-2.378-2.188-3.948A4.482 4.482 0 014.21 6.327v5.423c0 .333.143.571.428.738l5.947 3.449-1.95 1.118a.432.432 0 01-.476 0zm-.262 3.9c-2.688 0-4.662-2.021-4.662-4.519 0-.19.024-.38.047-.57l4.686 2.71c.286.167.571.167.856 0l5.97-3.448v2.26c0 .19-.07.333-.237.428l-4.543 2.616c-.619.357-1.356.523-2.117.523zm5.899 2.83a5.947 5.947 0 005.827-4.756C22.287 18.339 24 15.84 24 13.296c0-1.665-.713-3.282-1.998-4.448.119-.5.19-.999.19-1.498 0-3.401-2.759-5.947-5.946-5.947-.642 0-1.26.095-1.88.31A5.962 5.962 0 0010.205 0a5.947 5.947 0 00-5.827 4.757C1.713 5.447 0 7.945 0 10.49c0 1.666.713 3.283 1.998 4.448-.119.5-.19 1-.19 1.499 0 3.401 2.759 5.946 5.946 5.946.642 0 1.26-.095 1.88-.309a5.96 5.96 0 004.162 1.713z"/></svg>';
384
+ const ICON_GEMINI = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z"/></svg>';
385
+
386
+ const CLIS = [
387
+ { id: 'claude-code', icon: ICON_CLAUDE, label: 'Claude Code', desc: 'Anthropic' },
388
+ { id: 'codex', icon: ICON_OPENAI, label: 'Codex CLI', desc: 'OpenAI' },
389
+ { id: 'gemini', icon: ICON_GEMINI, label: 'Gemini CLI', desc: 'Google' },
390
+ ];
391
+
392
+ // 1. Title
393
+ seq.add(() => handwrittenLabel(svg, 'Pick your CLI', {
394
+ x: cx, y: cy - 20,
395
+ color: 'rgba(255, 255, 255, 0.55)',
396
+ fontSize: 34, textAnchor: 'middle', duration: 400,
397
+ }), 200);
398
+
399
+ // 2. CLI cards (DOM elements, same style as ttt-picker)
400
+ seq.add(() => {
401
+ const cardY = cy + 40;
402
+ const gap = 130;
403
+ const els = [];
404
+
405
+ CLIS.forEach((cli, i) => {
406
+ const x = cx + (i - 1) * gap;
407
+ const card = document.createElement('div');
408
+ card.style.cssText = `
409
+ position: absolute; left: ${x}px; top: ${cardY}px;
410
+ transform: translate(-50%, 0);
411
+ display: flex; flex-direction: column; align-items: center; gap: 8px;
412
+ padding: 16px 22px; border: 1px solid rgba(255,255,255,0.06);
413
+ border-radius: 12px; background: rgba(255,255,255,0.02);
414
+ cursor: pointer; pointer-events: auto;
415
+ transition: all 0.2s ease; opacity: 0;
416
+ `;
417
+ card.innerHTML = `
418
+ <div style="width:36px;height:36px;color:rgba(255,255,255,0.4);transition:color 0.2s">${cli.icon}</div>
419
+ <div style="font-family:'Caveat',cursive;font-size:18px;color:rgba(255,255,255,0.45);user-select:none;transition:color 0.2s">${cli.label}</div>
420
+ <div style="font-size:11px;color:rgba(255,255,255,0.25);user-select:none">${cli.desc}</div>
421
+ `;
422
+ dom.appendChild(card);
423
+ requestAnimationFrame(() => { card.style.opacity = '1'; });
424
+
425
+ card.addEventListener('mouseenter', () => {
426
+ card.style.background = 'rgba(255,255,255,0.06)';
427
+ card.style.borderColor = 'rgba(255,255,255,0.15)';
428
+ card.style.transform = 'translate(-50%, 0) translateY(-2px)';
429
+ card.querySelector('div').style.color = 'rgba(255,255,255,0.7)';
430
+ });
431
+ card.addEventListener('mouseleave', () => {
432
+ card.style.background = 'rgba(255,255,255,0.02)';
433
+ card.style.borderColor = 'rgba(255,255,255,0.06)';
434
+ card.style.transform = 'translate(-50%, 0)';
435
+ card.querySelector('div').style.color = 'rgba(255,255,255,0.4)';
436
+ });
437
+ card.addEventListener('click', () => {
438
+ storage.setItem(KEYS.ONBOARDING_CLI, cli.id);
439
+ flushStorage();
440
+ nextStep();
441
+ });
442
+ els.push(card);
443
+ });
444
+
445
+ return { destroy() { els.forEach(e => e.remove()); } };
446
+ }, 300);
447
+
448
+ // 3. Sparkle doodles
449
+ seq.add(() => createDoodle(svg, 'sparkle', {
450
+ x: cx - 240, y: cy - 50, scale: 0.9,
451
+ color: 'rgba(255, 255, 255, 0.25)', duration: 350,
452
+ }), 200);
453
+ seq.add(() => createDoodle(svg, 'sparkle', {
454
+ x: cx + 230, y: cy - 30, scale: 0.7,
455
+ color: 'rgba(255, 255, 255, 0.2)', duration: 350,
456
+ }), 200);
457
+
458
+ // 4. Back button
459
+ seq.add(() => {
460
+ const backEl = createButton(dom, 'Back', {
461
+ x: cx, y: cy + 180, fontSize: 22,
462
+ color: 'rgba(255, 255, 255, 0.35)',
463
+ onClick: () => prevStep(),
464
+ });
465
+ return { destroy() { backEl.remove(); } };
466
+ }, 200);
467
+ },
468
+ },
469
+
470
+ // ── Step 3: Model Picker ──────────────────────────────────
471
+ {
472
+ id: 'onboarding-model-picker',
473
+ whiteboardMode: true,
474
+
475
+ render({ svg, dom, seq, vw, vh, nextStep, prevStep, createButton }) {
476
+ const cx = vw / 2;
477
+ const cy = vh / 2 - 50;
478
+
479
+ const CLI_MODELS = {
480
+ 'claude-code': [
481
+ { id: 'claude-opus-4-6', label: 'Opus 4.6', desc: 'Most capable', tier: 'top' },
482
+ { id: 'claude-sonnet-4-6', label: 'Sonnet 4.6', desc: 'Balanced', tier: 'default' },
483
+ { id: 'claude-haiku-4-5', label: 'Haiku 4.5', desc: 'Fastest', tier: 'light' },
484
+ ],
485
+ 'codex': [
486
+ { id: 'o3', label: 'o3', desc: 'Deep reasoning', tier: 'top' },
487
+ { id: 'o4-mini', label: 'o4-mini', desc: 'Fast reasoning', tier: 'default' },
488
+ ],
489
+ 'gemini': [
490
+ { id: 'gemini-2.5-pro', label: '2.5 Pro', desc: 'Most capable', tier: 'default' },
491
+ { id: 'gemini-2.5-flash', label: '2.5 Flash', desc: 'Lightweight', tier: 'light' },
492
+ ],
493
+ };
494
+
495
+ const CLI_LABELS = {
496
+ 'claude-code': 'Claude Code',
497
+ 'codex': 'Codex CLI',
498
+ 'gemini': 'Gemini CLI',
499
+ };
500
+
501
+ const chosenCli = storage.getItem(KEYS.ONBOARDING_CLI) || 'claude-code';
502
+ const models = CLI_MODELS[chosenCli] || CLI_MODELS['claude-code'];
503
+ const cliLabel = CLI_LABELS[chosenCli] || 'Claude Code';
504
+
505
+ // 1. Title with CLI name
506
+ seq.add(() => handwrittenLabel(svg, `Choose a model for ${cliLabel}`, {
507
+ x: cx, y: cy - 20,
508
+ color: 'rgba(255, 255, 255, 0.55)',
509
+ fontSize: 32, textAnchor: 'middle', duration: 400,
510
+ }), 200);
511
+
512
+ // 2. Model cards
513
+ seq.add(() => {
514
+ const cardY = cy + 40;
515
+ const gap = 150;
516
+ const startX = cx - ((models.length - 1) / 2) * gap;
517
+ const els = [];
518
+
519
+ models.forEach((model, i) => {
520
+ const x = startX + i * gap;
521
+ const tierColor = model.tier === 'top'
522
+ ? 'rgba(255, 180, 80, 0.5)'
523
+ : model.tier === 'light'
524
+ ? 'rgba(130, 200, 255, 0.5)'
525
+ : 'rgba(255, 255, 255, 0.45)';
526
+
527
+ const card = document.createElement('div');
528
+ card.style.cssText = `
529
+ position: absolute; left: ${x}px; top: ${cardY}px;
530
+ transform: translate(-50%, 0);
531
+ display: flex; flex-direction: column; align-items: center; gap: 6px;
532
+ padding: 18px 26px; border: 1px solid rgba(255,255,255,0.06);
533
+ border-radius: 12px; background: rgba(255,255,255,0.02);
534
+ cursor: pointer; pointer-events: auto;
535
+ transition: all 0.2s ease; opacity: 0;
536
+ `;
537
+ card.innerHTML = `
538
+ <div style="font-family:'Caveat',cursive;font-size:22px;color:${tierColor};user-select:none;transition:color 0.2s">${model.label}</div>
539
+ <div style="font-size:11px;color:rgba(255,255,255,0.3);user-select:none">${model.desc}</div>
540
+ `;
541
+ dom.appendChild(card);
542
+ requestAnimationFrame(() => { card.style.opacity = '1'; });
543
+
544
+ card.addEventListener('mouseenter', () => {
545
+ card.style.background = 'rgba(255,255,255,0.06)';
546
+ card.style.borderColor = 'rgba(255,255,255,0.15)';
547
+ card.style.transform = 'translate(-50%, 0) translateY(-2px)';
548
+ });
549
+ card.addEventListener('mouseleave', () => {
550
+ card.style.background = 'rgba(255,255,255,0.02)';
551
+ card.style.borderColor = 'rgba(255,255,255,0.06)';
552
+ card.style.transform = 'translate(-50%, 0)';
553
+ });
554
+ card.addEventListener('click', () => {
555
+ storage.setItem(KEYS.ONBOARDING_MODEL, model.id);
556
+ flushStorage();
557
+ nextStep();
558
+ });
559
+ els.push(card);
560
+ });
561
+
562
+ return { destroy() { els.forEach(e => e.remove()); } };
563
+ }, 300);
564
+
565
+ // 3. Back button
566
+ seq.add(() => {
567
+ const backEl = createButton(dom, 'Back', {
568
+ x: cx, y: cy + 180, fontSize: 22,
569
+ color: 'rgba(255, 255, 255, 0.35)',
570
+ onClick: () => prevStep(),
571
+ });
572
+ return { destroy() { backEl.remove(); } };
573
+ }, 200);
574
+ },
575
+ },
576
+
577
+ // ── Step 4: Project Picker ───────────────────────────────────
578
+ {
579
+ id: 'onboarding-project-picker',
580
+ whiteboardMode: true,
581
+
582
+ render({ svg, dom, seq, vw, vh, nextStep, prevStep, goToStep, createButton }) {
583
+ const cx = vw / 2;
584
+ const cy = vh / 2 - 60;
585
+
586
+ // 1. Title
587
+ seq.add(() => handwrittenLabel(svg, 'Which project should we explore?', {
588
+ x: cx, y: cy - 30,
589
+ color: 'rgba(255, 255, 255, 0.55)',
590
+ fontSize: 32, textAnchor: 'middle', duration: 400,
591
+ }), 200);
592
+
593
+ // 2. Fetch projects and build cards
594
+ seq.add(async () => {
595
+ let projects = [];
596
+ try {
597
+ const res = await fetch('/api/terminal/profiles');
598
+ const data = await res.json();
599
+ projects = data.projects || [];
600
+ } catch { /* empty list fallback */ }
601
+
602
+ const els = [];
603
+ const cardY = cy + 30;
604
+ const maxVisible = 4;
605
+ const visible = projects.slice(0, maxVisible);
606
+ // +1 for the "Add Project" card
607
+ const totalCards = visible.length + 1;
608
+ const gap = Math.min(170, (vw - 160) / totalCards);
609
+ const startX = cx - ((totalCards - 1) * gap) / 2;
610
+
611
+ // Folder icon for project cards
612
+ const ICON_FOLDER = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="width:28px;height:28px"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
613
+ const ICON_PLUS = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="width:28px;height:28px"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>';
614
+
615
+ function makeCard(x, icon, label, sublabel, color, onClick) {
616
+ const card = document.createElement('div');
617
+ card.style.cssText = `
618
+ position: absolute; left: ${x}px; top: ${cardY}px;
619
+ transform: translate(-50%, 0);
620
+ display: flex; flex-direction: column; align-items: center; gap: 8px;
621
+ padding: 18px 22px; border: 1px solid rgba(255,255,255,0.06);
622
+ border-radius: 12px; background: rgba(255,255,255,0.02);
623
+ cursor: pointer; pointer-events: auto;
624
+ transition: all 0.2s ease; opacity: 0;
625
+ min-width: 120px; max-width: 150px;
626
+ `;
627
+ card.innerHTML = `
628
+ <div style="color:${color};opacity:0.7;transition:opacity 0.2s">${icon}</div>
629
+ <div style="font-family:'Caveat',cursive;font-size:20px;color:${color};user-select:none;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:130px" title="${label}">${label}</div>
630
+ ${sublabel ? `<div style="font-size:10px;color:rgba(255,255,255,0.25);user-select:none;max-width:130px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:'JetBrains Mono',monospace" title="${sublabel}">${sublabel}</div>` : ''}
631
+ `;
632
+ dom.appendChild(card);
633
+ requestAnimationFrame(() => { card.style.opacity = '1'; });
634
+
635
+ card.addEventListener('mouseenter', () => {
636
+ card.style.background = 'rgba(255,255,255,0.06)';
637
+ card.style.borderColor = 'rgba(255,255,255,0.15)';
638
+ card.style.transform = 'translate(-50%, 0) translateY(-2px)';
639
+ });
640
+ card.addEventListener('mouseleave', () => {
641
+ card.style.background = 'rgba(255,255,255,0.02)';
642
+ card.style.borderColor = 'rgba(255,255,255,0.06)';
643
+ card.style.transform = 'translate(-50%, 0)';
644
+ });
645
+ card.addEventListener('click', onClick);
646
+ els.push(card);
647
+ }
648
+
649
+ // Project cards
650
+ visible.forEach((proj, i) => {
651
+ const x = startX + i * gap;
652
+ const folder = proj.label || proj.path.split(/[\\/]/).filter(Boolean).pop();
653
+ makeCard(x, ICON_FOLDER, folder, proj.path.replace(/\\/g, '/'), 'rgba(255, 255, 255, 0.45)', () => {
654
+ storage.setItem(KEYS.ONBOARDING_PROJECT, JSON.stringify({ path: proj.path, label: proj.label || folder }));
655
+ flushStorage();
656
+ nextStep();
657
+ });
658
+ });
659
+
660
+ // "Add Project" card (always last)
661
+ const addX = startX + visible.length * gap;
662
+ makeCard(addX, ICON_PLUS, 'Add Project', null, 'rgba(79, 195, 247, 0.5)', async () => {
663
+ // Open Settings on the Projects tab
664
+ const { openSettingsModal } = await import('./ui-settings.js');
665
+ await openSettingsModal();
666
+ const panel = document.getElementById('settings-panel');
667
+ if (panel) {
668
+ panel.querySelectorAll('.settings-nav-item').forEach(n => n.classList.remove('active'));
669
+ panel.querySelectorAll('.settings-tab-body').forEach(b => b.classList.remove('active'));
670
+ const nav = panel.querySelector('.settings-nav-item[data-tab="projects"]');
671
+ const tab = panel.querySelector('.settings-tab-body[data-tab="projects"]');
672
+ if (nav) nav.classList.add('active');
673
+ if (tab) tab.classList.add('active');
674
+ }
675
+ });
676
+
677
+ // If no projects exist, show hint
678
+ if (projects.length === 0) {
679
+ handwrittenLabel(svg, 'No projects registered yet — add one to get started', {
680
+ x: cx, y: cardY + 140,
681
+ color: 'rgba(255, 255, 255, 0.3)',
682
+ fontSize: 19, textAnchor: 'middle', duration: 300,
683
+ });
684
+ }
685
+
686
+ return { destroy() { els.forEach(e => e.remove()); } };
687
+ }, 300);
688
+
689
+ // 3. Back button
690
+ seq.add(() => {
691
+ const backEl = createButton(dom, 'Back', {
692
+ x: cx, y: cy + 210, fontSize: 22,
693
+ color: 'rgba(255, 255, 255, 0.35)',
694
+ onClick: () => prevStep(),
695
+ });
696
+ return { destroy() { backEl.remove(); } };
697
+ }, 200);
698
+ },
699
+ },
700
+
701
+ // ── Step 5: Memory Explanation ─────────────────────────────
702
+ {
703
+ id: 'onboarding-memory-explain',
704
+ whiteboardMode: true,
705
+
706
+ render({ svg, dom, seq, vw, vh, nextStep, prevStep, goToStep, createButton }) {
707
+ const cx = vw / 2;
708
+ const cy = vh / 2 - 70;
709
+
710
+ // 1. Title
711
+ seq.add(() => handwrittenLabel(svg, 'Memories are regenerative', {
712
+ x: cx, y: cy,
713
+ color: 'rgba(255, 255, 255, 0.55)',
714
+ fontSize: 34, textAnchor: 'middle', duration: 400,
715
+ }), 200);
716
+
717
+ // 2. Explanation lines
718
+ seq.add(() => handwrittenLabel(svg, 'A cheaper model can lay the groundwork.', {
719
+ x: cx, y: cy + 50,
720
+ color: 'rgba(255, 255, 255, 0.4)',
721
+ fontSize: 24, textAnchor: 'middle', duration: 350,
722
+ }), 300);
723
+
724
+ seq.add(() => handwrittenLabel(svg, 'When a stronger model uses those memories later,', {
725
+ x: cx, y: cy + 85,
726
+ color: 'rgba(255, 255, 255, 0.4)',
727
+ fontSize: 24, textAnchor: 'middle', duration: 350,
728
+ }), 200);
729
+
730
+ seq.add(() => handwrittenLabel(svg, 'it will refine and update them automatically.', {
731
+ x: cx, y: cy + 120,
732
+ color: 'rgba(255, 255, 255, 0.4)',
733
+ fontSize: 24, textAnchor: 'middle', duration: 350,
734
+ }), 200);
735
+
736
+ // 3. Key takeaway
737
+ seq.add(() => handwrittenLabel(svg, 'Starting small is not a compromise -- it is a strategy.', {
738
+ x: cx, y: cy + 175,
739
+ color: 'rgba(255, 255, 255, 0.5)',
740
+ fontSize: 26, textAnchor: 'middle', duration: 400,
741
+ }), 400);
742
+
743
+ // 4. Sparkle doodles
744
+ seq.add(() => createDoodle(svg, 'sparkle', {
745
+ x: cx - 300, y: cy + 40, scale: 0.9,
746
+ color: 'rgba(255, 255, 255, 0.2)', duration: 350,
747
+ }), 200);
748
+ seq.add(() => createDoodle(svg, 'star', {
749
+ x: cx + 280, y: cy + 100, scale: 0.8,
750
+ color: 'rgba(255, 255, 255, 0.2)', duration: 350,
751
+ }), 200);
752
+
753
+ // 5. Navigation
754
+ seq.add(() => {
755
+ const navY = cy + 235;
756
+ const backEl = createButton(dom, 'Back', {
757
+ x: cx - 80, y: navY, fontSize: 22,
758
+ color: 'rgba(255, 255, 255, 0.35)',
759
+ onClick: () => prevStep(),
760
+ });
761
+ const contEl = createButton(dom, 'Begin Exploration', {
762
+ x: cx + 100, y: navY, fontSize: 22,
763
+ color: 'rgba(255, 255, 255, 0.45)',
764
+ onClick: () => _launchExploration(goToStep),
765
+ });
766
+ return { destroy() { backEl.remove(); contEl.remove(); } };
767
+ }, 200);
768
+ },
769
+ },
770
+
771
+ // ── Step 6: Welcome (whiteboard mode) ─────────────────────
772
+ {
773
+ id: 'welcome',
774
+ whiteboardMode: true,
775
+
776
+ render({ svg, dom, seq, vw, vh, nextStep, onSkipWithArrow, createButton }) {
777
+ const cx = vw / 2;
778
+ const cy = vh / 2 - 40;
779
+
780
+ // 1. Hand-drawn question text (centered, Caveat, large)
781
+ seq.add(() => handwrittenLabel(svg, 'Do you want to see the tutorial?', {
782
+ x: cx,
783
+ y: cy,
784
+ color: 'rgba(255, 255, 255, 0.55)',
785
+ fontSize: 36,
786
+ textAnchor: 'middle',
787
+ duration: 400,
788
+ }), 200);
789
+
790
+ // 2. Decorative sparkle doodles
791
+ seq.add(() => createDoodle(svg, 'sparkle', {
792
+ x: cx - 240,
793
+ y: cy - 60,
794
+ scale: 1.2,
795
+ color: 'rgba(255, 255, 255, 0.3)',
796
+ duration: 350,
797
+ }), 200);
798
+
799
+ seq.add(() => createDoodle(svg, 'sparkle', {
800
+ x: cx + 210,
801
+ y: cy + 20,
802
+ scale: 0.9,
803
+ color: 'rgba(255, 255, 255, 0.3)',
804
+ duration: 350,
805
+ }), 200);
806
+
807
+ // 3. Interactive Yes / No buttons
808
+ seq.add(() => {
809
+ const btnY = cy + 50;
810
+ const gap = 80;
811
+
812
+ const yesEl = createButton(dom, 'Yes', {
813
+ x: cx - gap,
814
+ y: btnY,
815
+ color: 'rgba(255, 255, 255, 0.5)',
816
+ fontSize: 28,
817
+ onClick: () => nextStep(),
818
+ });
819
+
820
+ const noEl = createButton(dom, 'No', {
821
+ x: cx + gap,
822
+ y: btnY,
823
+ color: 'rgba(255, 255, 255, 0.5)',
824
+ fontSize: 28,
825
+ onClick: () => onSkipWithArrow(),
826
+ });
827
+
828
+ // Draw hand-drawn underlines after buttons paint
829
+ setTimeout(() => {
830
+ const yesRect = yesEl.getBoundingClientRect();
831
+ const noRect = noEl.getBoundingClientRect();
832
+
833
+ createHandDrawnUnderline(svg, yesRect, {
834
+ color: 'rgba(255, 255, 255, 0.22)',
835
+ wobbleSeed: 'yes-underline',
836
+ duration: 250,
837
+ offset: 2,
838
+ });
839
+ createHandDrawnUnderline(svg, noRect, {
840
+ color: 'rgba(255, 255, 255, 0.22)',
841
+ wobbleSeed: 'no-underline',
842
+ duration: 250,
843
+ offset: 2,
844
+ });
845
+ }, 100);
846
+
847
+ return {
848
+ destroy() { yesEl.remove(); noEl.remove(); },
849
+ };
850
+ }, 200);
851
+ },
852
+ },
853
+
854
+ // ── Step 7: Session Monitor ────────────────────────────
855
+ {
856
+ id: 'explain-sessions',
857
+ whiteboardMode: true,
858
+
859
+ render({ svg, dom, seq, vw, vh, nextStep, prevStep, skipTutorial, createButton }) {
860
+ const btn = document.getElementById('titlebar-sessions-btn');
861
+ const rect = btn?.getBoundingClientRect();
862
+ if (!rect) return;
863
+
864
+ const btnCx = rect.left + rect.width / 2;
865
+ const textX = Math.max(220, Math.min(btnCx, vw - 220));
866
+ const textY = rect.bottom + 90;
867
+
868
+ seq.add(() => createHighlightCircle(svg, rect, {
869
+ color: 'rgba(255, 255, 255, 0.25)',
870
+ padding: 10, duration: 500, strokeWidth: 1.2,
871
+ }), 100);
872
+
873
+ seq.add(() => createAnimatedArrow(svg, {
874
+ from: { x: textX, y: textY - 18 },
875
+ to: { x: btnCx, y: rect.bottom + 24 },
876
+ color: 'rgba(255, 255, 255, 0.22)',
877
+ wobbleSeed: 'sessions-arrow', duration: 400,
878
+ arrowheadSize: 14, strokeWidth: 1.5,
879
+ }), 250);
880
+
881
+ seq.add(() => handwrittenLabel(svg, 'Session Monitor', {
882
+ x: textX, y: textY,
883
+ textAnchor: 'middle',
884
+ color: 'rgba(255, 255, 255, 0.6)',
885
+ fontSize: 28, duration: 350,
886
+ }), 400);
887
+
888
+ seq.add(() => handwrittenLabel(svg, 'Track active CLI sessions across your terminals', {
889
+ x: textX, y: textY + 36,
890
+ textAnchor: 'middle',
891
+ color: 'rgba(255, 255, 255, 0.32)',
892
+ fontSize: 19, duration: 300,
893
+ }), 100);
894
+
895
+ seq.add(() => {
896
+ const navY = textY + 105;
897
+ const backEl = createButton(dom, 'Back', {
898
+ x: textX - 80, y: navY, fontSize: 22,
899
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => prevStep(),
900
+ });
901
+ const endEl = createButton(dom, 'End', {
902
+ x: textX, y: navY, fontSize: 17,
903
+ color: 'rgba(255, 255, 255, 0.25)', onClick: () => skipTutorial(),
904
+ });
905
+ const nextEl = createButton(dom, 'Next', {
906
+ x: textX + 80, y: navY, fontSize: 22,
907
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => nextStep(),
908
+ });
909
+ return { destroy() { backEl.remove(); endEl.remove(); nextEl.remove(); } };
910
+ }, 200);
911
+ },
912
+ },
913
+
914
+ // ── Step 8: Image Gallery ────────────────────────────
915
+ {
916
+ id: 'explain-gallery',
917
+ whiteboardMode: true,
918
+
919
+ render({ svg, dom, seq, vw, vh, nextStep, prevStep, skipTutorial, createButton }) {
920
+ const btn = document.getElementById('titlebar-gallery-btn');
921
+ const rect = btn?.getBoundingClientRect();
922
+ if (!rect) return;
923
+
924
+ const btnCx = rect.left + rect.width / 2;
925
+ const textX = Math.max(220, Math.min(btnCx, vw - 220));
926
+ const textY = rect.bottom + 90;
927
+
928
+ seq.add(() => createHighlightCircle(svg, rect, {
929
+ color: 'rgba(255, 255, 255, 0.25)',
930
+ padding: 10, duration: 500, strokeWidth: 1.2,
931
+ }), 100);
932
+
933
+ seq.add(() => createAnimatedArrow(svg, {
934
+ from: { x: textX, y: textY - 18 },
935
+ to: { x: btnCx, y: rect.bottom + 24 },
936
+ color: 'rgba(255, 255, 255, 0.22)',
937
+ wobbleSeed: 'gallery-arrow', duration: 400,
938
+ arrowheadSize: 14, strokeWidth: 1.5,
939
+ }), 250);
940
+
941
+ seq.add(() => handwrittenLabel(svg, 'Image Gallery', {
942
+ x: textX, y: textY,
943
+ textAnchor: 'middle',
944
+ color: 'rgba(255, 255, 255, 0.6)',
945
+ fontSize: 28, duration: 350,
946
+ }), 400);
947
+
948
+ seq.add(() => handwrittenLabel(svg, 'Browse and manage images from your sessions', {
949
+ x: textX, y: textY + 36,
950
+ textAnchor: 'middle',
951
+ color: 'rgba(255, 255, 255, 0.32)',
952
+ fontSize: 19, duration: 300,
953
+ }), 100);
954
+
955
+ seq.add(() => {
956
+ const navY = textY + 105;
957
+ const backEl = createButton(dom, 'Back', {
958
+ x: textX - 80, y: navY, fontSize: 22,
959
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => prevStep(),
960
+ });
961
+ const endEl = createButton(dom, 'End', {
962
+ x: textX, y: navY, fontSize: 17,
963
+ color: 'rgba(255, 255, 255, 0.25)', onClick: () => skipTutorial(),
964
+ });
965
+ const nextEl = createButton(dom, 'Next', {
966
+ x: textX + 80, y: navY, fontSize: 22,
967
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => nextStep(),
968
+ });
969
+ return { destroy() { backEl.remove(); endEl.remove(); nextEl.remove(); } };
970
+ }, 200);
971
+ },
972
+ },
973
+
974
+ // ── Step 9: Cost Tracker ─────────────────────────────
975
+ {
976
+ id: 'explain-cost',
977
+ whiteboardMode: true,
978
+
979
+ render({ svg, dom, seq, vw, vh, nextStep, prevStep, skipTutorial, createButton }) {
980
+ const btn = document.getElementById('titlebar-cost-btn');
981
+ const rect = btn?.getBoundingClientRect();
982
+ if (!rect) return;
983
+
984
+ const btnCx = rect.left + rect.width / 2;
985
+ const textX = Math.max(220, Math.min(btnCx, vw - 220));
986
+ const textY = rect.bottom + 90;
987
+
988
+ seq.add(() => createHighlightCircle(svg, rect, {
989
+ color: 'rgba(255, 255, 255, 0.25)',
990
+ padding: 10, duration: 500, strokeWidth: 1.2,
991
+ }), 100);
992
+
993
+ seq.add(() => createAnimatedArrow(svg, {
994
+ from: { x: textX, y: textY - 18 },
995
+ to: { x: btnCx, y: rect.bottom + 24 },
996
+ color: 'rgba(255, 255, 255, 0.22)',
997
+ wobbleSeed: 'cost-arrow', duration: 400,
998
+ arrowheadSize: 14, strokeWidth: 1.5,
999
+ }), 250);
1000
+
1001
+ seq.add(() => handwrittenLabel(svg, 'Cost Tracker', {
1002
+ x: textX, y: textY,
1003
+ textAnchor: 'middle',
1004
+ color: 'rgba(255, 255, 255, 0.6)',
1005
+ fontSize: 28, duration: 350,
1006
+ }), 400);
1007
+
1008
+ seq.add(() => handwrittenLabel(svg, 'Watch your token spend in real-time', {
1009
+ x: textX, y: textY + 36,
1010
+ textAnchor: 'middle',
1011
+ color: 'rgba(255, 255, 255, 0.32)',
1012
+ fontSize: 19, duration: 300,
1013
+ }), 100);
1014
+
1015
+ seq.add(() => {
1016
+ const navY = textY + 105;
1017
+ const backEl = createButton(dom, 'Back', {
1018
+ x: textX - 80, y: navY, fontSize: 22,
1019
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => prevStep(),
1020
+ });
1021
+ const endEl = createButton(dom, 'End', {
1022
+ x: textX, y: navY, fontSize: 17,
1023
+ color: 'rgba(255, 255, 255, 0.25)', onClick: () => skipTutorial(),
1024
+ });
1025
+ const nextEl = createButton(dom, 'Next', {
1026
+ x: textX + 80, y: navY, fontSize: 22,
1027
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => nextStep(),
1028
+ });
1029
+ return { destroy() { backEl.remove(); endEl.remove(); nextEl.remove(); } };
1030
+ }, 200);
1031
+ },
1032
+ },
1033
+
1034
+ // ── Step 10: Help & Tutorial ─────────────────────────
1035
+ {
1036
+ id: 'explain-help',
1037
+ whiteboardMode: true,
1038
+
1039
+ render({ svg, dom, seq, vw, vh, nextStep, prevStep, skipTutorial, createButton }) {
1040
+ const btn = document.getElementById('titlebar-tutorial-btn');
1041
+ const rect = btn?.getBoundingClientRect();
1042
+ if (!rect) return;
1043
+
1044
+ const btnCx = rect.left + rect.width / 2;
1045
+ const textX = Math.max(220, Math.min(btnCx, vw - 220));
1046
+ const textY = rect.bottom + 90;
1047
+
1048
+ seq.add(() => createHighlightCircle(svg, rect, {
1049
+ color: 'rgba(255, 255, 255, 0.25)',
1050
+ padding: 10, duration: 500, strokeWidth: 1.2,
1051
+ }), 100);
1052
+
1053
+ seq.add(() => createAnimatedArrow(svg, {
1054
+ from: { x: textX, y: textY - 18 },
1055
+ to: { x: btnCx, y: rect.bottom + 24 },
1056
+ color: 'rgba(255, 255, 255, 0.22)',
1057
+ wobbleSeed: 'help-arrow', duration: 400,
1058
+ arrowheadSize: 14, strokeWidth: 1.5,
1059
+ }), 250);
1060
+
1061
+ seq.add(() => handwrittenLabel(svg, 'Help & Tutorial', {
1062
+ x: textX, y: textY,
1063
+ textAnchor: 'middle',
1064
+ color: 'rgba(255, 255, 255, 0.6)',
1065
+ fontSize: 28, duration: 350,
1066
+ }), 400);
1067
+
1068
+ seq.add(() => handwrittenLabel(svg, 'Restart this tour anytime from here', {
1069
+ x: textX, y: textY + 36,
1070
+ textAnchor: 'middle',
1071
+ color: 'rgba(255, 255, 255, 0.32)',
1072
+ fontSize: 19, duration: 300,
1073
+ }), 100);
1074
+
1075
+ seq.add(() => {
1076
+ const navY = textY + 105;
1077
+ const backEl = createButton(dom, 'Back', {
1078
+ x: textX - 80, y: navY, fontSize: 22,
1079
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => prevStep(),
1080
+ });
1081
+ const endEl = createButton(dom, 'End', {
1082
+ x: textX, y: navY, fontSize: 17,
1083
+ color: 'rgba(255, 255, 255, 0.25)', onClick: () => skipTutorial(),
1084
+ });
1085
+ const nextEl = createButton(dom, 'Next', {
1086
+ x: textX + 80, y: navY, fontSize: 22,
1087
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => nextStep(),
1088
+ });
1089
+ return { destroy() { backEl.remove(); endEl.remove(); nextEl.remove(); } };
1090
+ }, 200);
1091
+ },
1092
+ },
1093
+
1094
+ // ── Step 11: Graph View ──────────────────────────────
1095
+ {
1096
+ id: 'explain-focus',
1097
+ whiteboardMode: true,
1098
+
1099
+ render({ svg, dom, seq, vw, vh, nextStep, prevStep, skipTutorial, createButton }) {
1100
+ const btn = document.getElementById('titlebar-viz-toggle');
1101
+ const rect = btn?.getBoundingClientRect();
1102
+ if (!rect) return;
1103
+
1104
+ const btnCx = rect.left + rect.width / 2;
1105
+ const textX = Math.max(220, Math.min(btnCx, vw - 220));
1106
+ const textY = rect.bottom + 90;
1107
+
1108
+ seq.add(() => createHighlightCircle(svg, rect, {
1109
+ color: 'rgba(255, 255, 255, 0.25)',
1110
+ padding: 10, duration: 500, strokeWidth: 1.2,
1111
+ }), 100);
1112
+
1113
+ seq.add(() => createAnimatedArrow(svg, {
1114
+ from: { x: textX, y: textY - 18 },
1115
+ to: { x: btnCx, y: rect.bottom + 24 },
1116
+ color: 'rgba(255, 255, 255, 0.22)',
1117
+ wobbleSeed: 'focus-arrow', duration: 400,
1118
+ arrowheadSize: 14, strokeWidth: 1.5,
1119
+ }), 250);
1120
+
1121
+ seq.add(() => handwrittenLabel(svg, 'Graph View', {
1122
+ x: textX, y: textY,
1123
+ textAnchor: 'middle',
1124
+ color: 'rgba(255, 255, 255, 0.6)',
1125
+ fontSize: 28, duration: 350,
1126
+ }), 400);
1127
+
1128
+ seq.add(() => handwrittenLabel(svg, 'Switch between the memory graph and this whiteboard', {
1129
+ x: textX, y: textY + 36,
1130
+ textAnchor: 'middle',
1131
+ color: 'rgba(255, 255, 255, 0.32)',
1132
+ fontSize: 19, duration: 300,
1133
+ }), 100);
1134
+
1135
+ seq.add(() => {
1136
+ const navY = textY + 105;
1137
+ const backEl = createButton(dom, 'Back', {
1138
+ x: textX - 80, y: navY, fontSize: 22,
1139
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => prevStep(),
1140
+ });
1141
+ const endEl = createButton(dom, 'End', {
1142
+ x: textX, y: navY, fontSize: 17,
1143
+ color: 'rgba(255, 255, 255, 0.25)', onClick: () => skipTutorial(),
1144
+ });
1145
+ const nextEl = createButton(dom, 'Next', {
1146
+ x: textX + 80, y: navY, fontSize: 22,
1147
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => nextStep(),
1148
+ });
1149
+ return { destroy() { backEl.remove(); endEl.remove(); nextEl.remove(); } };
1150
+ }, 200);
1151
+ },
1152
+ },
1153
+
1154
+ // ── Step 12: Fullscreen ──────────────────────────────
1155
+ {
1156
+ id: 'explain-fullscreen',
1157
+ whiteboardMode: true,
1158
+
1159
+ render({ svg, dom, seq, vw, vh, nextStep, prevStep, skipTutorial, createButton }) {
1160
+ const btn = document.getElementById('titlebar-fullscreen-btn');
1161
+ const rect = btn?.getBoundingClientRect();
1162
+ if (!rect) return;
1163
+
1164
+ const btnCx = rect.left + rect.width / 2;
1165
+ const textX = Math.max(220, Math.min(btnCx, vw - 220));
1166
+ const textY = rect.bottom + 90;
1167
+
1168
+ seq.add(() => createHighlightCircle(svg, rect, {
1169
+ color: 'rgba(255, 255, 255, 0.25)',
1170
+ padding: 10, duration: 500, strokeWidth: 1.2,
1171
+ }), 100);
1172
+
1173
+ seq.add(() => createAnimatedArrow(svg, {
1174
+ from: { x: textX, y: textY - 18 },
1175
+ to: { x: btnCx, y: rect.bottom + 24 },
1176
+ color: 'rgba(255, 255, 255, 0.22)',
1177
+ wobbleSeed: 'fs-arrow', duration: 400,
1178
+ arrowheadSize: 14, strokeWidth: 1.5,
1179
+ }), 250);
1180
+
1181
+ seq.add(() => handwrittenLabel(svg, 'Fullscreen', {
1182
+ x: textX, y: textY,
1183
+ textAnchor: 'middle',
1184
+ color: 'rgba(255, 255, 255, 0.6)',
1185
+ fontSize: 28, duration: 350,
1186
+ }), 400);
1187
+
1188
+ seq.add(() => handwrittenLabel(svg, 'A clean, distraction-free workspace', {
1189
+ x: textX, y: textY + 36,
1190
+ textAnchor: 'middle',
1191
+ color: 'rgba(255, 255, 255, 0.32)',
1192
+ fontSize: 19, duration: 300,
1193
+ }), 100);
1194
+
1195
+ seq.add(() => {
1196
+ const navY = textY + 105;
1197
+ const backEl = createButton(dom, 'Back', {
1198
+ x: textX - 80, y: navY, fontSize: 22,
1199
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => prevStep(),
1200
+ });
1201
+ const endEl = createButton(dom, 'End', {
1202
+ x: textX, y: navY, fontSize: 17,
1203
+ color: 'rgba(255, 255, 255, 0.25)', onClick: () => skipTutorial(),
1204
+ });
1205
+ const nextEl = createButton(dom, 'Next', {
1206
+ x: textX + 80, y: navY, fontSize: 22,
1207
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => nextStep(),
1208
+ });
1209
+ return { destroy() { backEl.remove(); endEl.remove(); nextEl.remove(); } };
1210
+ }, 200);
1211
+ },
1212
+ },
1213
+
1214
+ // ── Step 13: Clock ───────────────────────────────────
1215
+ {
1216
+ id: 'explain-clock',
1217
+ whiteboardMode: true,
1218
+
1219
+ render({ svg, dom, seq, vw, vh, nextStep, prevStep, skipTutorial, createButton }) {
1220
+ // Hide workspace toolbar for the next section reveal
1221
+ const topControls = document.getElementById('topright-controls');
1222
+ if (topControls) topControls.classList.add('tutorial-hidden');
1223
+
1224
+ const clock = document.getElementById('titlebar-clock');
1225
+ const rect = clock?.getBoundingClientRect();
1226
+ if (!rect) return;
1227
+
1228
+ const clockCx = rect.left + rect.width / 2;
1229
+ const textX = Math.max(220, Math.min(clockCx, vw - 220));
1230
+ const textY = rect.bottom + 90;
1231
+
1232
+ seq.add(() => createHighlightCircle(svg, rect, {
1233
+ color: 'rgba(255, 255, 255, 0.25)',
1234
+ padding: 10, duration: 500, strokeWidth: 1.2,
1235
+ }), 100);
1236
+
1237
+ seq.add(() => createAnimatedArrow(svg, {
1238
+ from: { x: textX, y: textY - 18 },
1239
+ to: { x: clockCx, y: rect.bottom + 24 },
1240
+ color: 'rgba(255, 255, 255, 0.22)',
1241
+ wobbleSeed: 'clock-arrow', duration: 400,
1242
+ arrowheadSize: 14, strokeWidth: 1.5,
1243
+ }), 250);
1244
+
1245
+ seq.add(() => handwrittenLabel(svg, 'Whoa... a clock', {
1246
+ x: textX, y: textY,
1247
+ textAnchor: 'middle',
1248
+ color: 'rgba(255, 255, 255, 0.6)',
1249
+ fontSize: 28, duration: 350,
1250
+ }), 400);
1251
+
1252
+ seq.add(() => {
1253
+ const navY = textY + 70;
1254
+ const backEl = createButton(dom, 'Back', {
1255
+ x: textX - 80, y: navY, fontSize: 22,
1256
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => prevStep(),
1257
+ });
1258
+ const endEl = createButton(dom, 'End', {
1259
+ x: textX, y: navY, fontSize: 17,
1260
+ color: 'rgba(255, 255, 255, 0.25)', onClick: () => skipTutorial(),
1261
+ });
1262
+ const nextEl = createButton(dom, 'Next', {
1263
+ x: textX + 80, y: navY, fontSize: 22,
1264
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => nextStep(),
1265
+ });
1266
+ return { destroy() { backEl.remove(); endEl.remove(); nextEl.remove(); } };
1267
+ }, 200);
1268
+ },
1269
+ },
1270
+
1271
+
1272
+ // ── Step 14: Trash (+ reveal workspace toolbar) ──────
1273
+ {
1274
+ id: 'explain-trash',
1275
+ whiteboardMode: true,
1276
+
1277
+ render({ svg, dom, seq, vw, vh, nextStep, prevStep, skipTutorial, createButton }) {
1278
+ // Slide in the top-right workspace toolbar
1279
+ const topControls = document.getElementById('topright-controls');
1280
+ if (topControls) {
1281
+ topControls.classList.remove('tutorial-hidden');
1282
+ topControls.style.transform = 'translateY(-20px)';
1283
+ topControls.style.opacity = '0';
1284
+ topControls.style.transition = 'transform 0.35s ease, opacity 0.35s ease';
1285
+ requestAnimationFrame(() => {
1286
+ topControls.style.transform = 'translateY(0)';
1287
+ topControls.style.opacity = '1';
1288
+ });
1289
+ }
1290
+
1291
+ const btn = document.getElementById('topright-trash-btn');
1292
+ const rect = btn?.getBoundingClientRect();
1293
+ if (!rect) return;
1294
+
1295
+ const btnCx = rect.left + rect.width / 2;
1296
+ const textX = Math.max(220, Math.min(btnCx, vw - 220));
1297
+ const textY = rect.bottom + 90;
1298
+
1299
+ seq.add(() => createHighlightCircle(svg, rect, {
1300
+ color: 'rgba(255, 255, 255, 0.25)',
1301
+ padding: 10, duration: 500, strokeWidth: 1.2,
1302
+ }), 400);
1303
+
1304
+ seq.add(() => createAnimatedArrow(svg, {
1305
+ from: { x: textX, y: textY - 18 },
1306
+ to: { x: btnCx, y: rect.bottom + 24 },
1307
+ color: 'rgba(255, 255, 255, 0.22)',
1308
+ wobbleSeed: 'trash-arrow', duration: 400,
1309
+ arrowheadSize: 14, strokeWidth: 1.5,
1310
+ }), 250);
1311
+
1312
+ seq.add(() => handwrittenLabel(svg, 'Trash', {
1313
+ x: textX, y: textY,
1314
+ textAnchor: 'middle',
1315
+ color: 'rgba(255, 255, 255, 0.6)',
1316
+ fontSize: 28, duration: 350,
1317
+ }), 400);
1318
+
1319
+ seq.add(() => handwrittenLabel(svg, 'Recover deleted cards and terminals', {
1320
+ x: textX, y: textY + 36,
1321
+ textAnchor: 'middle',
1322
+ color: 'rgba(255, 255, 255, 0.32)',
1323
+ fontSize: 19, duration: 300,
1324
+ }), 100);
1325
+
1326
+ seq.add(() => {
1327
+ const navY = textY + 105;
1328
+ const backEl = createButton(dom, 'Back', {
1329
+ x: textX - 80, y: navY, fontSize: 22,
1330
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => prevStep(),
1331
+ });
1332
+ const endEl = createButton(dom, 'End', {
1333
+ x: textX, y: navY, fontSize: 17,
1334
+ color: 'rgba(255, 255, 255, 0.25)', onClick: () => skipTutorial(),
1335
+ });
1336
+ const nextEl = createButton(dom, 'Next', {
1337
+ x: textX + 80, y: navY, fontSize: 22,
1338
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => nextStep(),
1339
+ });
1340
+ return { destroy() { backEl.remove(); endEl.remove(); nextEl.remove(); } };
1341
+ }, 200);
1342
+ },
1343
+ },
1344
+
1345
+ // ── Step 15: Grid Snap ────────────────────────────────
1346
+ {
1347
+ id: 'explain-grid',
1348
+ whiteboardMode: true,
1349
+
1350
+ render({ svg, dom, seq, vw, vh, nextStep, prevStep, skipTutorial, createButton }) {
1351
+ const btn = document.getElementById('ws-grid-toggle');
1352
+ const rect = btn?.getBoundingClientRect();
1353
+ if (!rect) return;
1354
+
1355
+ const btnCx = rect.left + rect.width / 2;
1356
+ const textX = Math.max(220, Math.min(btnCx, vw - 220));
1357
+ const textY = rect.bottom + 90;
1358
+
1359
+ seq.add(() => createHighlightCircle(svg, rect, {
1360
+ color: 'rgba(255, 255, 255, 0.25)',
1361
+ padding: 10, duration: 500, strokeWidth: 1.2,
1362
+ }), 100);
1363
+
1364
+ seq.add(() => createAnimatedArrow(svg, {
1365
+ from: { x: textX, y: textY - 18 },
1366
+ to: { x: btnCx, y: rect.bottom + 24 },
1367
+ color: 'rgba(255, 255, 255, 0.22)',
1368
+ wobbleSeed: 'grid-arrow', duration: 400,
1369
+ arrowheadSize: 14, strokeWidth: 1.5,
1370
+ }), 250);
1371
+
1372
+ seq.add(() => handwrittenLabel(svg, 'Grid Snap', {
1373
+ x: textX, y: textY,
1374
+ textAnchor: 'middle',
1375
+ color: 'rgba(255, 255, 255, 0.6)',
1376
+ fontSize: 28, duration: 350,
1377
+ }), 400);
1378
+
1379
+ seq.add(() => handwrittenLabel(svg, 'Align cards to a grid for tidy layouts', {
1380
+ x: textX, y: textY + 36,
1381
+ textAnchor: 'middle',
1382
+ color: 'rgba(255, 255, 255, 0.32)',
1383
+ fontSize: 19, duration: 300,
1384
+ }), 100);
1385
+
1386
+ seq.add(() => {
1387
+ const navY = textY + 105;
1388
+ const backEl = createButton(dom, 'Back', {
1389
+ x: textX - 80, y: navY, fontSize: 22,
1390
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => prevStep(),
1391
+ });
1392
+ const endEl = createButton(dom, 'End', {
1393
+ x: textX, y: navY, fontSize: 17,
1394
+ color: 'rgba(255, 255, 255, 0.25)', onClick: () => skipTutorial(),
1395
+ });
1396
+ const nextEl = createButton(dom, 'Next', {
1397
+ x: textX + 80, y: navY, fontSize: 22,
1398
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => nextStep(),
1399
+ });
1400
+ return { destroy() { backEl.remove(); endEl.remove(); nextEl.remove(); } };
1401
+ }, 200);
1402
+ },
1403
+ },
1404
+
1405
+ // ── Step 16: Tile Terminals ───────────────────────────
1406
+ {
1407
+ id: 'explain-tile',
1408
+ whiteboardMode: true,
1409
+
1410
+ render({ svg, dom, seq, vw, vh, nextStep, prevStep, skipTutorial, createButton }) {
1411
+ const btn = document.getElementById('ws-tile-btn');
1412
+ const rect = btn?.getBoundingClientRect();
1413
+ if (!rect) return;
1414
+
1415
+ const btnCx = rect.left + rect.width / 2;
1416
+ const textX = Math.max(220, Math.min(btnCx, vw - 220));
1417
+ const textY = rect.bottom + 90;
1418
+
1419
+ seq.add(() => createHighlightCircle(svg, rect, {
1420
+ color: 'rgba(255, 255, 255, 0.25)',
1421
+ padding: 10, duration: 500, strokeWidth: 1.2,
1422
+ }), 100);
1423
+
1424
+ seq.add(() => createAnimatedArrow(svg, {
1425
+ from: { x: textX, y: textY - 18 },
1426
+ to: { x: btnCx, y: rect.bottom + 24 },
1427
+ color: 'rgba(255, 255, 255, 0.22)',
1428
+ wobbleSeed: 'tile-arrow', duration: 400,
1429
+ arrowheadSize: 14, strokeWidth: 1.5,
1430
+ }), 250);
1431
+
1432
+ seq.add(() => handwrittenLabel(svg, 'Tile Terminals', {
1433
+ x: textX, y: textY,
1434
+ textAnchor: 'middle',
1435
+ color: 'rgba(255, 255, 255, 0.6)',
1436
+ fontSize: 28, duration: 350,
1437
+ }), 400);
1438
+
1439
+ seq.add(() => handwrittenLabel(svg, 'Auto-arrange all terminals into a neat grid', {
1440
+ x: textX, y: textY + 36,
1441
+ textAnchor: 'middle',
1442
+ color: 'rgba(255, 255, 255, 0.32)',
1443
+ fontSize: 19, duration: 300,
1444
+ }), 100);
1445
+
1446
+ seq.add(() => {
1447
+ const navY = textY + 105;
1448
+ const backEl = createButton(dom, 'Back', {
1449
+ x: textX - 80, y: navY, fontSize: 22,
1450
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => prevStep(),
1451
+ });
1452
+ const endEl = createButton(dom, 'End', {
1453
+ x: textX, y: navY, fontSize: 17,
1454
+ color: 'rgba(255, 255, 255, 0.25)', onClick: () => skipTutorial(),
1455
+ });
1456
+ const nextEl = createButton(dom, 'Next', {
1457
+ x: textX + 80, y: navY, fontSize: 22,
1458
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => nextStep(),
1459
+ });
1460
+ return { destroy() { backEl.remove(); endEl.remove(); nextEl.remove(); } };
1461
+ }, 200);
1462
+ },
1463
+ },
1464
+
1465
+ // ── Step 17: Keybinds ─────────────────────────────────
1466
+ {
1467
+ id: 'explain-keybinds',
1468
+ whiteboardMode: true,
1469
+
1470
+ render({ svg, dom, seq, vw, vh, nextStep, prevStep, skipTutorial, createButton }) {
1471
+ const btn = document.getElementById('topright-keybinds-btn');
1472
+ const rect = btn?.getBoundingClientRect();
1473
+ if (!rect) return;
1474
+
1475
+ const btnCx = rect.left + rect.width / 2;
1476
+ const textX = Math.max(220, Math.min(btnCx, vw - 220));
1477
+ const textY = rect.bottom + 90;
1478
+
1479
+ seq.add(() => createHighlightCircle(svg, rect, {
1480
+ color: 'rgba(255, 255, 255, 0.25)',
1481
+ padding: 10, duration: 500, strokeWidth: 1.2,
1482
+ }), 100);
1483
+
1484
+ seq.add(() => createAnimatedArrow(svg, {
1485
+ from: { x: textX, y: textY - 18 },
1486
+ to: { x: btnCx, y: rect.bottom + 24 },
1487
+ color: 'rgba(255, 255, 255, 0.22)',
1488
+ wobbleSeed: 'keys-arrow', duration: 400,
1489
+ arrowheadSize: 14, strokeWidth: 1.5,
1490
+ }), 250);
1491
+
1492
+ seq.add(() => handwrittenLabel(svg, 'Keybinds', {
1493
+ x: textX, y: textY,
1494
+ textAnchor: 'middle',
1495
+ color: 'rgba(255, 255, 255, 0.6)',
1496
+ fontSize: 28, duration: 350,
1497
+ }), 400);
1498
+
1499
+ seq.add(() => handwrittenLabel(svg, 'View and customize keyboard shortcuts', {
1500
+ x: textX, y: textY + 36,
1501
+ textAnchor: 'middle',
1502
+ color: 'rgba(255, 255, 255, 0.32)',
1503
+ fontSize: 19, duration: 300,
1504
+ }), 100);
1505
+
1506
+ seq.add(() => {
1507
+ const navY = textY + 105;
1508
+ const backEl = createButton(dom, 'Back', {
1509
+ x: textX - 80, y: navY, fontSize: 22,
1510
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => prevStep(),
1511
+ });
1512
+ const endEl = createButton(dom, 'End', {
1513
+ x: textX, y: navY, fontSize: 17,
1514
+ color: 'rgba(255, 255, 255, 0.25)', onClick: () => skipTutorial(),
1515
+ });
1516
+ const nextEl = createButton(dom, 'Next', {
1517
+ x: textX + 80, y: navY, fontSize: 22,
1518
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => nextStep(),
1519
+ });
1520
+ return { destroy() { backEl.remove(); endEl.remove(); nextEl.remove(); } };
1521
+ }, 200);
1522
+ },
1523
+ },
1524
+
1525
+ // ── Step 18: Memory Explorer ──────────────────────────
1526
+ {
1527
+ id: 'explain-memory-explorer',
1528
+ whiteboardMode: true,
1529
+ _openedExplorer: false,
1530
+
1531
+ onEnter() {
1532
+ // Slide the memory explorer open so the user sees it
1533
+ if (!document.body.classList.contains('explorer-open')) {
1534
+ this._openedExplorer = true;
1535
+ toggleExplorer();
1536
+ }
1537
+ },
1538
+
1539
+ onExit() {
1540
+ // Close it back if we opened it
1541
+ if (this._openedExplorer && document.body.classList.contains('explorer-open')) {
1542
+ toggleExplorer();
1543
+ }
1544
+ this._openedExplorer = false;
1545
+ },
1546
+
1547
+ render({ svg, dom, seq, vw, vh, nextStep, prevStep, skipTutorial, createButton }) {
1548
+ const btn = document.getElementById('topright-memory-explorer-btn');
1549
+ const rect = btn?.getBoundingClientRect();
1550
+ if (!rect) return;
1551
+
1552
+ const btnCx = rect.left + rect.width / 2;
1553
+ const textX = Math.max(220, Math.min(btnCx, vw - 220));
1554
+ const textY = rect.bottom + 90;
1555
+
1556
+ seq.add(() => createHighlightCircle(svg, rect, {
1557
+ color: 'rgba(255, 255, 255, 0.25)',
1558
+ padding: 10, duration: 500, strokeWidth: 1.2,
1559
+ }), 100);
1560
+
1561
+ seq.add(() => createAnimatedArrow(svg, {
1562
+ from: { x: textX, y: textY - 18 },
1563
+ to: { x: btnCx, y: rect.bottom + 24 },
1564
+ color: 'rgba(255, 255, 255, 0.22)',
1565
+ wobbleSeed: 'mem-arrow', duration: 400,
1566
+ arrowheadSize: 14, strokeWidth: 1.5,
1567
+ }), 250);
1568
+
1569
+ seq.add(() => handwrittenLabel(svg, 'Memory Explorer', {
1570
+ x: textX, y: textY,
1571
+ textAnchor: 'middle',
1572
+ color: 'rgba(255, 255, 255, 0.6)',
1573
+ fontSize: 28, duration: 350,
1574
+ }), 400);
1575
+
1576
+ seq.add(() => handwrittenLabel(svg, 'Browse, search, and manage stored memories', {
1577
+ x: textX, y: textY + 36,
1578
+ textAnchor: 'middle',
1579
+ color: 'rgba(255, 255, 255, 0.32)',
1580
+ fontSize: 19, duration: 300,
1581
+ }), 100);
1582
+
1583
+ seq.add(() => {
1584
+ const navY = textY + 105;
1585
+ const backEl = createButton(dom, 'Back', {
1586
+ x: textX - 80, y: navY, fontSize: 22,
1587
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => prevStep(),
1588
+ });
1589
+ const endEl = createButton(dom, 'End', {
1590
+ x: textX, y: navY, fontSize: 17,
1591
+ color: 'rgba(255, 255, 255, 0.25)', onClick: () => skipTutorial(),
1592
+ });
1593
+ const nextEl = createButton(dom, 'Next', {
1594
+ x: textX + 80, y: navY, fontSize: 22,
1595
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => nextStep(),
1596
+ });
1597
+ return { destroy() { backEl.remove(); endEl.remove(); nextEl.remove(); } };
1598
+ }, 200);
1599
+ },
1600
+ },
1601
+
1602
+ // ── Step 19: File Explorer ────────────────────────────
1603
+ {
1604
+ id: 'explain-file-explorer',
1605
+ whiteboardMode: true,
1606
+ _openedFileExplorer: false,
1607
+
1608
+ onEnter() {
1609
+ if (!document.body.classList.contains('file-explorer-open')) {
1610
+ this._openedFileExplorer = true;
1611
+ toggleFileExplorer();
1612
+ }
1613
+ },
1614
+
1615
+ onExit() {
1616
+ if (this._openedFileExplorer && document.body.classList.contains('file-explorer-open')) {
1617
+ toggleFileExplorer();
1618
+ }
1619
+ this._openedFileExplorer = false;
1620
+ },
1621
+
1622
+ render({ svg, dom, seq, vw, vh, nextStep, prevStep, skipTutorial, createButton }) {
1623
+ const btn = document.getElementById('topright-file-explorer-btn');
1624
+ const rect = btn?.getBoundingClientRect();
1625
+ if (!rect) return;
1626
+
1627
+ const btnCx = rect.left + rect.width / 2;
1628
+ const textX = Math.max(220, Math.min(btnCx, vw - 220));
1629
+ const textY = rect.bottom + 90;
1630
+
1631
+ seq.add(() => createHighlightCircle(svg, rect, {
1632
+ color: 'rgba(255, 255, 255, 0.25)',
1633
+ padding: 10, duration: 500, strokeWidth: 1.2,
1634
+ }), 100);
1635
+
1636
+ seq.add(() => createAnimatedArrow(svg, {
1637
+ from: { x: textX, y: textY - 18 },
1638
+ to: { x: btnCx, y: rect.bottom + 24 },
1639
+ color: 'rgba(255, 255, 255, 0.22)',
1640
+ wobbleSeed: 'files-arrow', duration: 400,
1641
+ arrowheadSize: 14, strokeWidth: 1.5,
1642
+ }), 250);
1643
+
1644
+ seq.add(() => handwrittenLabel(svg, 'File Explorer', {
1645
+ x: textX, y: textY,
1646
+ textAnchor: 'middle',
1647
+ color: 'rgba(255, 255, 255, 0.6)',
1648
+ fontSize: 28, duration: 350,
1649
+ }), 400);
1650
+
1651
+ seq.add(() => handwrittenLabel(svg, 'Browse project files right from the whiteboard', {
1652
+ x: textX, y: textY + 36,
1653
+ textAnchor: 'middle',
1654
+ color: 'rgba(255, 255, 255, 0.32)',
1655
+ fontSize: 19, duration: 300,
1656
+ }), 100);
1657
+
1658
+ seq.add(() => {
1659
+ const navY = textY + 105;
1660
+ const backEl = createButton(dom, 'Back', {
1661
+ x: textX - 80, y: navY, fontSize: 22,
1662
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => prevStep(),
1663
+ });
1664
+ const endEl = createButton(dom, 'End', {
1665
+ x: textX, y: navY, fontSize: 17,
1666
+ color: 'rgba(255, 255, 255, 0.25)', onClick: () => skipTutorial(),
1667
+ });
1668
+ const nextEl = createButton(dom, 'Next', {
1669
+ x: textX + 80, y: navY, fontSize: 22,
1670
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => nextStep(),
1671
+ });
1672
+ return { destroy() { backEl.remove(); endEl.remove(); nextEl.remove(); } };
1673
+ }, 200);
1674
+ },
1675
+ },
1676
+
1677
+ // ── Step 20: Bookmarks ────────────────────────────────
1678
+ {
1679
+ id: 'explain-bookmarks',
1680
+ whiteboardMode: true,
1681
+
1682
+ render({ svg, dom, seq, vw, vh, nextStep, prevStep, skipTutorial, createButton }) {
1683
+ const btn = document.getElementById('topright-bookmarks-btn');
1684
+ const rect = btn?.getBoundingClientRect();
1685
+ if (!rect) return;
1686
+
1687
+ const btnCx = rect.left + rect.width / 2;
1688
+ const textX = Math.max(220, Math.min(btnCx, vw - 220));
1689
+ const textY = rect.bottom + 90;
1690
+
1691
+ seq.add(() => createHighlightCircle(svg, rect, {
1692
+ color: 'rgba(255, 255, 255, 0.25)',
1693
+ padding: 10, duration: 500, strokeWidth: 1.2,
1694
+ }), 100);
1695
+
1696
+ seq.add(() => createAnimatedArrow(svg, {
1697
+ from: { x: textX, y: textY - 18 },
1698
+ to: { x: btnCx, y: rect.bottom + 24 },
1699
+ color: 'rgba(255, 255, 255, 0.22)',
1700
+ wobbleSeed: 'bm-arrow', duration: 400,
1701
+ arrowheadSize: 14, strokeWidth: 1.5,
1702
+ }), 250);
1703
+
1704
+ seq.add(() => handwrittenLabel(svg, 'Bookmarks', {
1705
+ x: textX, y: textY,
1706
+ textAnchor: 'middle',
1707
+ color: 'rgba(255, 255, 255, 0.6)',
1708
+ fontSize: 28, duration: 350,
1709
+ }), 400);
1710
+
1711
+ seq.add(() => handwrittenLabel(svg, 'Pin important memories for quick access', {
1712
+ x: textX, y: textY + 36,
1713
+ textAnchor: 'middle',
1714
+ color: 'rgba(255, 255, 255, 0.32)',
1715
+ fontSize: 19, duration: 300,
1716
+ }), 100);
1717
+
1718
+ seq.add(() => {
1719
+ const navY = textY + 105;
1720
+ const backEl = createButton(dom, 'Back', {
1721
+ x: textX - 80, y: navY, fontSize: 22,
1722
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => prevStep(),
1723
+ });
1724
+ const endEl = createButton(dom, 'End', {
1725
+ x: textX, y: navY, fontSize: 17,
1726
+ color: 'rgba(255, 255, 255, 0.25)', onClick: () => skipTutorial(),
1727
+ });
1728
+ const nextEl = createButton(dom, 'Next', {
1729
+ x: textX + 80, y: navY, fontSize: 22,
1730
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => nextStep(),
1731
+ });
1732
+ return { destroy() { backEl.remove(); endEl.remove(); nextEl.remove(); } };
1733
+ }, 200);
1734
+ },
1735
+ },
1736
+
1737
+ // ── Step 21: Share / Invite ───────────────────────────
1738
+ {
1739
+ id: 'explain-share',
1740
+ whiteboardMode: true,
1741
+
1742
+ render({ svg, dom, seq, vw, vh, nextStep, prevStep, skipTutorial, createButton }) {
1743
+ const btn = document.getElementById('invite-btn');
1744
+ const rect = btn?.getBoundingClientRect();
1745
+ if (!rect) return;
1746
+
1747
+ const btnCx = rect.left + rect.width / 2;
1748
+ const textX = Math.max(220, Math.min(btnCx, vw - 220));
1749
+ const textY = rect.bottom + 90;
1750
+
1751
+ seq.add(() => createHighlightCircle(svg, rect, {
1752
+ color: 'rgba(255, 255, 255, 0.25)',
1753
+ padding: 10, duration: 500, strokeWidth: 1.2,
1754
+ }), 100);
1755
+
1756
+ seq.add(() => createAnimatedArrow(svg, {
1757
+ from: { x: textX, y: textY - 18 },
1758
+ to: { x: btnCx, y: rect.bottom + 24 },
1759
+ color: 'rgba(255, 255, 255, 0.22)',
1760
+ wobbleSeed: 'share-arrow', duration: 400,
1761
+ arrowheadSize: 14, strokeWidth: 1.5,
1762
+ }), 250);
1763
+
1764
+ seq.add(() => handwrittenLabel(svg, 'Share / Invite', {
1765
+ x: textX, y: textY,
1766
+ textAnchor: 'middle',
1767
+ color: 'rgba(255, 255, 255, 0.6)',
1768
+ fontSize: 28, duration: 350,
1769
+ }), 400);
1770
+
1771
+ seq.add(() => handwrittenLabel(svg, 'Invite others to view your whiteboard live', {
1772
+ x: textX, y: textY + 36,
1773
+ textAnchor: 'middle',
1774
+ color: 'rgba(255, 255, 255, 0.32)',
1775
+ fontSize: 19, duration: 300,
1776
+ }), 100);
1777
+
1778
+ seq.add(() => {
1779
+ const navY = textY + 105;
1780
+ const backEl = createButton(dom, 'Back', {
1781
+ x: textX - 80, y: navY, fontSize: 22,
1782
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => prevStep(),
1783
+ });
1784
+ const endEl = createButton(dom, 'End', {
1785
+ x: textX, y: navY, fontSize: 17,
1786
+ color: 'rgba(255, 255, 255, 0.25)', onClick: () => skipTutorial(),
1787
+ });
1788
+ const nextEl = createButton(dom, 'Next', {
1789
+ x: textX + 80, y: navY, fontSize: 22,
1790
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => nextStep(),
1791
+ });
1792
+ return { destroy() { backEl.remove(); endEl.remove(); nextEl.remove(); } };
1793
+ }, 200);
1794
+ },
1795
+ },
1796
+
1797
+ // ── Step 22: Workspaces ───────────────────────────────
1798
+ {
1799
+ id: 'explain-workspaces',
1800
+ whiteboardMode: true,
1801
+
1802
+ render({ svg, dom, seq, vw, vh, nextStep, prevStep, skipTutorial, createButton }) {
1803
+ const wsEl = document.getElementById('ws-indicator');
1804
+ const rect = wsEl?.getBoundingClientRect();
1805
+ if (!rect) return;
1806
+
1807
+ const wsCx = rect.left + rect.width / 2;
1808
+ const textX = Math.max(220, Math.min(wsCx, vw - 220));
1809
+ const textY = rect.bottom + 90;
1810
+
1811
+ seq.add(() => createHighlightCircle(svg, rect, {
1812
+ color: 'rgba(255, 255, 255, 0.25)',
1813
+ padding: 10, duration: 500, strokeWidth: 1.2,
1814
+ }), 100);
1815
+
1816
+ seq.add(() => createAnimatedArrow(svg, {
1817
+ from: { x: textX, y: textY - 18 },
1818
+ to: { x: wsCx, y: rect.bottom + 24 },
1819
+ color: 'rgba(255, 255, 255, 0.22)',
1820
+ wobbleSeed: 'ws-arrow', duration: 400,
1821
+ arrowheadSize: 14, strokeWidth: 1.5,
1822
+ }), 250);
1823
+
1824
+ seq.add(() => handwrittenLabel(svg, 'Workspaces', {
1825
+ x: textX, y: textY,
1826
+ textAnchor: 'middle',
1827
+ color: 'rgba(255, 255, 255, 0.6)',
1828
+ fontSize: 28, duration: 350,
1829
+ }), 400);
1830
+
1831
+ seq.add(() => handwrittenLabel(svg, 'Save and load your whiteboard layouts', {
1832
+ x: textX, y: textY + 36,
1833
+ textAnchor: 'middle',
1834
+ color: 'rgba(255, 255, 255, 0.32)',
1835
+ fontSize: 19, duration: 300,
1836
+ }), 100);
1837
+
1838
+ seq.add(() => {
1839
+ const navY = textY + 105;
1840
+ const backEl = createButton(dom, 'Back', {
1841
+ x: textX - 80, y: navY, fontSize: 22,
1842
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => prevStep(),
1843
+ });
1844
+ const endEl = createButton(dom, 'End', {
1845
+ x: textX, y: navY, fontSize: 17,
1846
+ color: 'rgba(255, 255, 255, 0.25)', onClick: () => skipTutorial(),
1847
+ });
1848
+ const nextEl = createButton(dom, 'Next', {
1849
+ x: textX + 80, y: navY, fontSize: 22,
1850
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => nextStep(),
1851
+ });
1852
+ return { destroy() { backEl.remove(); endEl.remove(); nextEl.remove(); } };
1853
+ }, 200);
1854
+ },
1855
+ },
1856
+
1857
+ // ── Step 23: Side Panel ───────────────────────────────
1858
+ {
1859
+ id: 'explain-side-panel',
1860
+ whiteboardMode: true,
1861
+ _openedClaudePanel: false,
1862
+
1863
+ onEnter() {
1864
+ if (!isClaudePanelOpen()) {
1865
+ this._openedClaudePanel = true;
1866
+ toggleClaudePanel();
1867
+ }
1868
+ },
1869
+
1870
+ onExit() {
1871
+ if (this._openedClaudePanel && isClaudePanelOpen()) {
1872
+ toggleClaudePanel();
1873
+ }
1874
+ this._openedClaudePanel = false;
1875
+ },
1876
+
1877
+ render({ svg, dom, seq, vw, vh, nextStep, prevStep, skipTutorial, createButton }) {
1878
+ const btn = document.getElementById('topright-claude-panel-btn');
1879
+ const rect = btn?.getBoundingClientRect();
1880
+ if (!rect) return;
1881
+
1882
+ const btnCx = rect.left + rect.width / 2;
1883
+ const textX = Math.max(220, Math.min(btnCx, vw - 220));
1884
+ const textY = rect.bottom + 90;
1885
+
1886
+ seq.add(() => createHighlightCircle(svg, rect, {
1887
+ color: 'rgba(255, 255, 255, 0.25)',
1888
+ padding: 10, duration: 500, strokeWidth: 1.2,
1889
+ }), 100);
1890
+
1891
+ seq.add(() => createAnimatedArrow(svg, {
1892
+ from: { x: textX, y: textY - 18 },
1893
+ to: { x: btnCx, y: rect.bottom + 24 },
1894
+ color: 'rgba(255, 255, 255, 0.22)',
1895
+ wobbleSeed: 'panel-arrow', duration: 400,
1896
+ arrowheadSize: 14, strokeWidth: 1.5,
1897
+ }), 250);
1898
+
1899
+ seq.add(() => handwrittenLabel(svg, 'Side Panel', {
1900
+ x: textX, y: textY,
1901
+ textAnchor: 'middle',
1902
+ color: 'rgba(255, 255, 255, 0.6)',
1903
+ fontSize: 28, duration: 350,
1904
+ }), 400);
1905
+
1906
+ seq.add(() => handwrittenLabel(svg, 'Chat with Claude Code without leaving the board', {
1907
+ x: textX, y: textY + 36,
1908
+ textAnchor: 'middle',
1909
+ color: 'rgba(255, 255, 255, 0.32)',
1910
+ fontSize: 19, duration: 300,
1911
+ }), 100);
1912
+
1913
+ seq.add(() => {
1914
+ const navY = textY + 105;
1915
+ const backEl = createButton(dom, 'Back', {
1916
+ x: textX - 80, y: navY, fontSize: 22,
1917
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => prevStep(),
1918
+ });
1919
+ const endEl = createButton(dom, 'End', {
1920
+ x: textX, y: navY, fontSize: 17,
1921
+ color: 'rgba(255, 255, 255, 0.25)', onClick: () => skipTutorial(),
1922
+ });
1923
+ const nextEl = createButton(dom, 'Next', {
1924
+ x: textX + 80, y: navY, fontSize: 22,
1925
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => nextStep(),
1926
+ });
1927
+ return { destroy() { backEl.remove(); endEl.remove(); nextEl.remove(); } };
1928
+ }, 200);
1929
+ },
1930
+ },
1931
+
1932
+ // ── Step 24: Menubar Tour (animated sweep) ───────────
1933
+ {
1934
+ id: 'explain-menubar',
1935
+ whiteboardMode: true,
1936
+
1937
+ onExit() {
1938
+ document.querySelectorAll('.menubar-item.open').forEach(m => m.classList.remove('open'));
1939
+ },
1940
+
1941
+ render({ svg, dom, seq, vw, vh, nextStep, prevStep, skipTutorial, createButton }) {
1942
+ document.querySelectorAll('.menubar-item.open').forEach(m => m.classList.remove('open'));
1943
+
1944
+ const menubar = document.querySelector('.menubar');
1945
+ if (!menubar) return;
1946
+ const mbRect = menubar.getBoundingClientRect();
1947
+ const cx = vw / 2;
1948
+
1949
+ // Menus to sweep — resume stays closed (no dropdown content to show)
1950
+ const menus = [
1951
+ { menu: 'apps', desc: 'Your AI tools live here', open: true },
1952
+ { menu: 'resume', desc: 'Continue where you left off', open: false },
1953
+ { menu: 'view', desc: 'Show or hide panels', open: true },
1954
+ { menu: 'graph', desc: 'Tune the memory graph', open: true },
1955
+ { menu: 'skills', desc: 'Reusable AI skills', open: true },
1956
+ { menu: 'automations', desc: 'Workflows on autopilot', open: true },
1957
+ { menu: 'games', desc: 'You earned a break', open: true },
1958
+ ];
1959
+
1960
+ // Sweep state
1961
+ let sweepG = null;
1962
+ let currentOpenItem = null;
1963
+
1964
+ const cleanupSweep = () => {
1965
+ if (sweepG) { sweepG.remove(); sweepG = null; }
1966
+ if (currentOpenItem) { currentOpenItem.classList.remove('open'); currentOpenItem = null; }
1967
+ };
1968
+
1969
+ // Sweep through each menu
1970
+ menus.forEach((m, i) => {
1971
+ seq.add(() => {
1972
+ cleanupSweep();
1973
+
1974
+ const menuItem = document.querySelector(`.menubar-item[data-menu="${m.menu}"]`);
1975
+ if (!menuItem) return;
1976
+
1977
+ const label = menuItem.querySelector('.menubar-label');
1978
+ const labelRect = label?.getBoundingClientRect();
1979
+ if (!labelRect) return;
1980
+
1981
+ // SVG group for this menu's annotations
1982
+ sweepG = document.createElementNS('http://www.w3.org/2000/svg', 'g');
1983
+ svg.appendChild(sweepG);
1984
+
1985
+ // Highlight circle around the label
1986
+ createHighlightCircle(sweepG, labelRect, {
1987
+ color: 'rgba(255, 255, 255, 0.2)',
1988
+ padding: 6, duration: 350, strokeWidth: 1,
1989
+ });
1990
+
1991
+ const labelCx = labelRect.left + labelRect.width / 2;
1992
+
1993
+ if (m.open) {
1994
+ // Open the dropdown
1995
+ menuItem.classList.add('open');
1996
+ currentOpenItem = menuItem;
1997
+
1998
+ const dropdown = menuItem.querySelector('.menubar-dropdown');
1999
+ if (dropdown) dropdown.offsetHeight;
2000
+ const ddRect = dropdown?.getBoundingClientRect();
2001
+
2002
+ if (ddRect && ddRect.width > 0) {
2003
+ const ddCx = ddRect.left + ddRect.width / 2;
2004
+ const textX = Math.max(200, Math.min(ddCx, vw - 200));
2005
+ const textY = ddRect.bottom + 40;
2006
+
2007
+ createAnimatedArrow(sweepG, {
2008
+ from: { x: textX, y: textY - 10 },
2009
+ to: { x: ddCx, y: ddRect.bottom + 8 },
2010
+ color: 'rgba(255, 255, 255, 0.22)',
2011
+ wobbleSeed: `${m.menu}-sweep`, duration: 200,
2012
+ strokeWidth: 1.2, arrowheadSize: 10,
2013
+ });
2014
+
2015
+ handwrittenLabel(sweepG, m.desc, {
2016
+ x: textX, y: textY,
2017
+ textAnchor: 'middle', maxWidth: 400,
2018
+ color: 'rgba(255, 255, 255, 0.45)',
2019
+ fontSize: 22, duration: 200,
2020
+ });
2021
+ }
2022
+ } else {
2023
+ // Closed menu — show description below the label
2024
+ const textX = Math.max(200, Math.min(labelCx, vw - 200));
2025
+ const textY = labelRect.bottom + 55;
2026
+
2027
+ createAnimatedArrow(sweepG, {
2028
+ from: { x: textX, y: textY - 10 },
2029
+ to: { x: labelCx, y: labelRect.bottom + 10 },
2030
+ color: 'rgba(255, 255, 255, 0.22)',
2031
+ wobbleSeed: `${m.menu}-sweep`, duration: 200,
2032
+ strokeWidth: 1.2, arrowheadSize: 10,
2033
+ });
2034
+
2035
+ handwrittenLabel(sweepG, m.desc, {
2036
+ x: textX, y: textY,
2037
+ textAnchor: 'middle', maxWidth: 400,
2038
+ color: 'rgba(255, 255, 255, 0.45)',
2039
+ fontSize: 22, duration: 200,
2040
+ });
2041
+ }
2042
+
2043
+ return { destroy: cleanupSweep };
2044
+ }, i === 0 ? 600 : 3000);
2045
+ });
2046
+
2047
+ // After sweep — close last menu, show summary + nav
2048
+ seq.add(() => {
2049
+ cleanupSweep();
2050
+
2051
+ handwrittenLabel(svg, 'Explore each menu at your own pace', {
2052
+ x: cx, y: mbRect.bottom + 80,
2053
+ textAnchor: 'middle',
2054
+ color: 'rgba(255, 255, 255, 0.38)',
2055
+ fontSize: 22, duration: 300,
2056
+ });
2057
+
2058
+ createDoodle(svg, 'sparkle', {
2059
+ x: cx - 200, y: mbRect.bottom + 60,
2060
+ scale: 0.7, color: 'rgba(255, 255, 255, 0.18)', duration: 300,
2061
+ });
2062
+ createDoodle(svg, 'sparkle', {
2063
+ x: cx + 190, y: mbRect.bottom + 70,
2064
+ scale: 0.6, color: 'rgba(255, 255, 255, 0.18)', duration: 300,
2065
+ });
2066
+
2067
+ const navY = mbRect.bottom + 145;
2068
+ const backEl = createButton(dom, 'Back', {
2069
+ x: cx - 80, y: navY, fontSize: 22,
2070
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => prevStep(),
2071
+ });
2072
+ const endEl = createButton(dom, 'End', {
2073
+ x: cx, y: navY, fontSize: 17,
2074
+ color: 'rgba(255, 255, 255, 0.25)', onClick: () => skipTutorial(),
2075
+ });
2076
+ const nextEl = createButton(dom, 'Next', {
2077
+ x: cx + 80, y: navY, fontSize: 22,
2078
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => nextStep(),
2079
+ });
2080
+ return { destroy() { backEl.remove(); endEl.remove(); nextEl.remove(); } };
2081
+ }, 1500);
2082
+ },
2083
+ },
2084
+
2085
+ // ── Step 25: Whiteboard Toolbar ─────────────────────
2086
+ {
2087
+ id: 'explain-whiteboard-toolbar',
2088
+ whiteboardMode: true,
2089
+
2090
+ onEnter() {
2091
+ const tb = document.getElementById('wb-toolbar');
2092
+ if (tb) tb.classList.remove('tutorial-hidden');
2093
+ },
2094
+
2095
+ onExit() {
2096
+ const tb = document.getElementById('wb-toolbar');
2097
+ if (tb) tb.classList.add('tutorial-hidden');
2098
+ },
2099
+
2100
+ render({ svg, dom, seq, vw, vh, nextStep, prevStep, skipTutorial, createButton }) {
2101
+ const toolbar = document.getElementById('wb-toolbar');
2102
+ if (!toolbar) return;
2103
+ toolbar.offsetHeight; // force reflow after unhide
2104
+ const tbRect = toolbar.getBoundingClientRect();
2105
+ const cx = vw / 2;
2106
+
2107
+ // 1. Highlight the toolbar
2108
+ seq.add(() => createHighlightCircle(svg, tbRect, {
2109
+ color: 'rgba(255, 255, 255, 0.18)',
2110
+ padding: 12, duration: 500, strokeWidth: 1,
2111
+ }), 100);
2112
+
2113
+ // 2. Arrow from description up to toolbar
2114
+ const textY = tbRect.bottom + 70;
2115
+ seq.add(() => createAnimatedArrow(svg, {
2116
+ from: { x: cx, y: textY - 12 },
2117
+ to: { x: tbRect.left + tbRect.width / 2, y: tbRect.bottom + 12 },
2118
+ color: 'rgba(255, 255, 255, 0.22)',
2119
+ wobbleSeed: 'wb-toolbar', duration: 300,
2120
+ strokeWidth: 1.2, arrowheadSize: 10,
2121
+ }), 200);
2122
+
2123
+ // 3. Title
2124
+ seq.add(() => handwrittenLabel(svg, 'The Whiteboard', {
2125
+ x: cx, y: textY,
2126
+ textAnchor: 'middle',
2127
+ color: 'rgba(255, 255, 255, 0.55)',
2128
+ fontSize: 28, duration: 300,
2129
+ }), 200);
2130
+
2131
+ // 4. Description lines
2132
+ seq.add(() => handwrittenLabel(svg, 'Draw, write, sketch — Claude can see everything here.', {
2133
+ x: cx, y: textY + 38,
2134
+ textAnchor: 'middle',
2135
+ color: 'rgba(255, 255, 255, 0.4)',
2136
+ fontSize: 20, duration: 250,
2137
+ }), 200);
2138
+
2139
+ seq.add(() => handwrittenLabel(svg, 'Just say "look at the whiteboard" and it will.', {
2140
+ x: cx, y: textY + 68,
2141
+ textAnchor: 'middle',
2142
+ color: 'rgba(255, 255, 255, 0.4)',
2143
+ fontSize: 20, duration: 250,
2144
+ }), 200);
2145
+
2146
+ // 5. Nav
2147
+ seq.add(() => {
2148
+ const navY = textY + 130;
2149
+ const backEl = createButton(dom, 'Back', {
2150
+ x: cx - 80, y: navY, fontSize: 22,
2151
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => prevStep(),
2152
+ });
2153
+ const endEl = createButton(dom, 'End', {
2154
+ x: cx, y: navY, fontSize: 17,
2155
+ color: 'rgba(255, 255, 255, 0.25)', onClick: () => skipTutorial(),
2156
+ });
2157
+ const nextEl = createButton(dom, 'Next', {
2158
+ x: cx + 80, y: navY, fontSize: 22,
2159
+ color: 'rgba(255, 255, 255, 0.35)', onClick: () => nextStep(),
2160
+ });
2161
+ return { destroy() { backEl.remove(); endEl.remove(); nextEl.remove(); } };
2162
+ }, 200);
2163
+ },
2164
+ },
2165
+
2166
+ // ── Step 26: Feedback / Social links ──────────────────
2167
+ {
2168
+ id: 'feedback',
2169
+ whiteboardMode: true,
2170
+
2171
+ render({ svg, dom, seq, vw, vh, nextStep, prevStep, skipTutorial, createButton }) {
2172
+ // Close any previously open dropdowns
2173
+ document.querySelectorAll('.menubar-item.open').forEach(m => m.classList.remove('open'));
2174
+
2175
+ const cx = vw / 2;
2176
+ const cy = vh / 2 - 60;
2177
+
2178
+ // 1. "Feedback is always welcome" handwritten text
2179
+ seq.add(() => handwrittenLabel(svg, 'Feedback is always welcome', {
2180
+ x: cx,
2181
+ y: cy,
2182
+ color: 'rgba(255, 255, 255, 0.55)',
2183
+ fontSize: 34,
2184
+ textAnchor: 'middle',
2185
+ duration: 400,
2186
+ }), 200);
2187
+
2188
+ // 2. Decorative sparkles
2189
+ seq.add(() => createDoodle(svg, 'sparkle', {
2190
+ x: cx - 220, y: cy - 50,
2191
+ scale: 1, color: 'rgba(255, 255, 255, 0.25)', duration: 300,
2192
+ }), 300);
2193
+ seq.add(() => createDoodle(svg, 'sparkle', {
2194
+ x: cx + 200, y: cy + 10,
2195
+ scale: 0.8, color: 'rgba(255, 255, 255, 0.25)', duration: 300,
2196
+ }), 400);
2197
+
2198
+ // 3. Social icons row
2199
+ seq.add(() => {
2200
+ const iconY = cy + 60;
2201
+ const gap = 90;
2202
+
2203
+ const socials = [
2204
+ {
2205
+ label: 'Discord',
2206
+ url: 'https://discord.gg/x6yWqE9GZP',
2207
+ svg: '<svg width="28" height="28" viewBox="0 0 24 24" fill="currentColor"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>',
2208
+ },
2209
+ {
2210
+ label: 'GitHub',
2211
+ url: 'https://github.com/danilokhury/Synabun',
2212
+ svg: '<svg width="28" height="28" viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>',
2213
+ },
2214
+ {
2215
+ label: 'X',
2216
+ url: 'https://x.com/SynabunAI',
2217
+ svg: '<svg width="28" height="28" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>',
2218
+ },
2219
+ ];
2220
+
2221
+ const els = [];
2222
+ socials.forEach((s, i) => {
2223
+ const x = cx + (i - 1) * gap;
2224
+ const el = document.createElement('a');
2225
+ el.href = s.url;
2226
+ el.target = '_blank';
2227
+ el.rel = 'noopener';
2228
+ el.title = s.label;
2229
+ el.style.cssText = `
2230
+ position: absolute;
2231
+ left: ${x}px; top: ${iconY}px;
2232
+ transform: translate(-50%, 0);
2233
+ display: flex; flex-direction: column; align-items: center; gap: 8px;
2234
+ color: rgba(255, 255, 255, 0.4);
2235
+ text-decoration: none;
2236
+ pointer-events: auto;
2237
+ transition: color 0.2s, transform 0.2s;
2238
+ opacity: 0;
2239
+ font-family: 'Caveat', cursive;
2240
+ font-size: 18px;
2241
+ cursor: pointer;
2242
+ `;
2243
+ el.innerHTML = `${s.svg}<span>${s.label}</span>`;
2244
+ dom.appendChild(el);
2245
+ requestAnimationFrame(() => { el.style.opacity = '1'; });
2246
+
2247
+ el.addEventListener('mouseenter', () => {
2248
+ el.style.color = 'rgba(255, 255, 255, 0.8)';
2249
+ el.style.transform = 'translate(-50%, 0) scale(1.1)';
2250
+ });
2251
+ el.addEventListener('mouseleave', () => {
2252
+ el.style.color = 'rgba(255, 255, 255, 0.4)';
2253
+ el.style.transform = 'translate(-50%, 0) scale(1)';
2254
+ });
2255
+ els.push(el);
2256
+ });
2257
+
2258
+ return { destroy() { els.forEach(e => e.remove()); } };
2259
+ }, 300);
2260
+
2261
+ // 4. Navigation buttons
2262
+ seq.add(() => {
2263
+ const navY = cy + 170;
2264
+ const backEl = createButton(dom, 'Back', {
2265
+ x: cx - 70, y: navY, fontSize: 22,
2266
+ color: 'rgba(255, 255, 255, 0.4)', onClick: () => prevStep(),
2267
+ });
2268
+ const doneEl = createButton(dom, 'Done', {
2269
+ x: cx + 70, y: navY, fontSize: 22,
2270
+ color: 'rgba(255, 255, 255, 0.4)', onClick: () => nextStep(),
2271
+ });
2272
+ return { destroy() { backEl.remove(); doneEl.remove(); } };
2273
+ }, 200);
2274
+ },
2275
+ },
2276
+ ];