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,1117 @@
1
+ // ═══════════════════════════════════════════
2
+ // TUTORIAL RUNTIME — Animated onboarding system
3
+ // ═══════════════════════════════════════════
4
+ //
5
+ // Orchestrates the tutorial experience. Two rendering modes:
6
+ //
7
+ // 1. WHITEBOARD MODE (whiteboardMode: true) — renders hand-drawn animated
8
+ // elements directly on the whiteboard in focus mode. No modals, no overlays.
9
+ // Steps use a render() function for full control.
10
+ //
11
+ // 2. OVERLAY MODE (legacy) — renders an SVG overlay + card panel on top of
12
+ // the app. Used for non-whiteboard steps or when restarting from ? button.
13
+ //
14
+ // Drawing is delegated to ui-tutorial-draw.js.
15
+ // Step content comes from ui-tutorial-steps.js.
16
+
17
+ import { storage, flushStorage } from './storage.js';
18
+ import { emit, on } from './state.js';
19
+ import { KEYS } from './constants.js';
20
+ import { registerMenuItem, registerHelpSection } from './registry.js';
21
+ import { loadIfaceConfig, saveIfaceConfig, applyIfaceConfig } from './ui-settings.js';
22
+ import { TUTORIAL_STEPS } from './ui-tutorial-steps.js';
23
+ import {
24
+ createAnimatedArrow,
25
+ createWobblyCircle,
26
+ createHandDrawnUnderline,
27
+ typewriterText,
28
+ handwrittenLabel,
29
+ createDoodle,
30
+ createSpotlight,
31
+ resolveTargetRect,
32
+ resolveAnchorPoint,
33
+ resolveRelativePoint,
34
+ AnimationSequencer,
35
+ } from './ui-tutorial-draw.js';
36
+
37
+ const SVG_NS = 'http://www.w3.org/2000/svg';
38
+
39
+ // ── Internal state ─────────────────────────
40
+ let _steps = [];
41
+ let _currentStep = -1;
42
+ let _active = false;
43
+ let _sequencer = null;
44
+ let _resizeTimer = null;
45
+ let _keyHandler = null;
46
+
47
+ // Overlay mode state (legacy)
48
+ let _spotlight = null;
49
+ let _overlayEl = null;
50
+ let _cardEl = null;
51
+ let _pulseEl = null;
52
+ let _annotationGroup = null;
53
+ let _advanceCleanup = null;
54
+ let _domAnnotationLayer = null;
55
+
56
+ // Whiteboard mode state
57
+ let _wbTutorialSvg = null;
58
+ let _wbTutorialDom = null;
59
+ let _isFocusModeForced = false;
60
+ let _originalVizEnabled = true;
61
+ let _isSkipAnimating = false;
62
+ let _clickInterceptor = null;
63
+ let _adhdMessageTimer = null;
64
+
65
+
66
+ // ═══════════════════════════════════════════
67
+ // INITIALIZATION
68
+ // ═══════════════════════════════════════════
69
+
70
+ export function initTutorial() {
71
+ _steps = TUTORIAL_STEPS;
72
+ if (!_steps || _steps.length === 0) return;
73
+
74
+ // Register menu item to restart
75
+ registerMenuItem({
76
+ menu: 'Help',
77
+ order: 50,
78
+ type: 'item',
79
+ id: 'restart-tutorial',
80
+ label: 'Restart Tutorial',
81
+ init(el) { el.addEventListener('click', () => startTutorial(true)); },
82
+ });
83
+
84
+ // Register help section
85
+ registerHelpSection({
86
+ order: 95,
87
+ html: `<div class="help-section">
88
+ <div class="help-section-title">Tutorial</div>
89
+ <div class="help-row"><span class="help-desc">Restart the guided tour from Help &rarr; Restart Tutorial</span></div>
90
+ </div>`,
91
+ });
92
+
93
+ // Auto-start only on true first visit (never seen tutorial at all)
94
+ const completed = storage.getItem(KEYS.TUTORIAL_COMPLETED);
95
+ const skipped = storage.getItem(KEYS.TUTORIAL_SKIPPED);
96
+ const started = storage.getItem(KEYS.TUTORIAL_STARTED);
97
+
98
+ if (!completed && !skipped && !started) {
99
+ // Force focus mode BEFORE boot() reads the config
100
+ _forceFocusForTutorial();
101
+
102
+ on('data-loaded', () => {
103
+ setTimeout(() => {
104
+ if (!_active) startTutorial(false);
105
+ }, 500);
106
+ });
107
+ }
108
+ }
109
+
110
+
111
+ // ═══════════════════════════════════════════
112
+ // FOCUS MODE FORCING
113
+ // ═══════════════════════════════════════════
114
+
115
+ function _forceFocusForTutorial() {
116
+ const cfg = loadIfaceConfig();
117
+ _originalVizEnabled = cfg.visualizationEnabled !== false;
118
+ if (cfg.visualizationEnabled !== false) {
119
+ cfg.visualizationEnabled = false;
120
+ saveIfaceConfig(cfg);
121
+ _isFocusModeForced = true;
122
+ }
123
+ }
124
+
125
+ function _ensureFocusMode() {
126
+ const staticBg = document.getElementById('static-bg');
127
+ if (!staticBg?.classList.contains('visible')) {
128
+ document.getElementById('titlebar-viz-toggle')?.click();
129
+ }
130
+ }
131
+
132
+ function _restoreFocusMode() {
133
+ if (_isFocusModeForced) {
134
+ const cfg = loadIfaceConfig();
135
+ cfg.visualizationEnabled = _originalVizEnabled;
136
+ saveIfaceConfig(cfg);
137
+ applyIfaceConfig(cfg);
138
+ _isFocusModeForced = false;
139
+ }
140
+ }
141
+
142
+
143
+ // ═══════════════════════════════════════════
144
+ // WHITEBOARD LAYER MANAGEMENT
145
+ // ═══════════════════════════════════════════
146
+
147
+ function _createWhiteboardLayers() {
148
+ if (_wbTutorialSvg) return;
149
+
150
+ // Append to document.body with z-index above the titlebar (50000)
151
+ // so tutorial annotations render above dropdowns and navbar elements.
152
+ // Coordinates are viewport-based, so placement is identical.
153
+
154
+ // SVG layer for hand-drawn elements
155
+ _wbTutorialSvg = document.createElementNS(SVG_NS, 'svg');
156
+ _wbTutorialSvg.id = 'wb-tutorial';
157
+ _wbTutorialSvg.style.cssText = 'position:fixed;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible;z-index:60000;';
158
+ document.body.appendChild(_wbTutorialSvg);
159
+
160
+ // DOM layer for clickable interactive elements
161
+ _wbTutorialDom = document.createElement('div');
162
+ _wbTutorialDom.id = 'wb-tutorial-dom';
163
+ _wbTutorialDom.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:60001;';
164
+ document.body.appendChild(_wbTutorialDom);
165
+ }
166
+
167
+ function _destroyWhiteboardLayers() {
168
+ if (_wbTutorialSvg) { _wbTutorialSvg.remove(); _wbTutorialSvg = null; }
169
+ if (_wbTutorialDom) { _wbTutorialDom.remove(); _wbTutorialDom = null; }
170
+ }
171
+
172
+
173
+ // ═══════════════════════════════════════════
174
+ // UI HELPERS
175
+ // ═══════════════════════════════════════════
176
+
177
+ function _hideStaticBgLogo(hide) {
178
+ const logo = document.querySelector('#static-bg .static-bg-logo');
179
+ const breathe = document.querySelector('#static-bg .focus-breathe');
180
+ if (logo) logo.classList.toggle('tutorial-hidden', hide);
181
+ if (breathe) breathe.classList.toggle('tutorial-hidden', hide);
182
+ }
183
+
184
+ function _hideWhiteboardToolbar(hide) {
185
+ const toolbar = document.getElementById('wb-toolbar');
186
+ if (toolbar) toolbar.classList.toggle('tutorial-hidden', hide);
187
+ }
188
+
189
+ function _hideTopControls(hide) {
190
+ const top = document.getElementById('topright-controls');
191
+ if (top) top.classList.toggle('tutorial-hidden', hide);
192
+ }
193
+
194
+
195
+ // ═══════════════════════════════════════════
196
+ // HAND-DRAWN BUTTON (DOM, Caveat font, clickable)
197
+ // ═══════════════════════════════════════════
198
+
199
+ function _createHandDrawnButton(container, text, { x, y, color = 'rgba(255,255,255,0.5)', fontSize = 26, onClick }) {
200
+ const el = document.createElement('div');
201
+ el.className = 'wb-tutorial-btn';
202
+ el.textContent = text;
203
+ el.style.cssText = `
204
+ position: absolute;
205
+ left: ${x}px;
206
+ top: ${y}px;
207
+ transform: translate(-50%, 0);
208
+ font-family: 'Caveat', cursive;
209
+ font-size: ${fontSize}px;
210
+ color: ${color};
211
+ pointer-events: auto;
212
+ opacity: 0;
213
+ `;
214
+ container.appendChild(el);
215
+
216
+ // Animate in
217
+ requestAnimationFrame(() => { el.style.opacity = '1'; });
218
+
219
+ // Hover effect
220
+ el.addEventListener('mouseenter', () => {
221
+ el.style.color = 'rgba(255, 255, 255, 0.85)';
222
+ el.style.transform = 'translate(-50%, 0) scale(1.08)';
223
+ });
224
+ el.addEventListener('mouseleave', () => {
225
+ el.style.color = color;
226
+ el.style.transform = 'translate(-50%, 0) scale(1)';
227
+ });
228
+
229
+ el.addEventListener('click', onClick);
230
+ return el;
231
+ }
232
+
233
+
234
+ // ═══════════════════════════════════════════
235
+ // START / STOP
236
+ // ═══════════════════════════════════════════
237
+
238
+ export function startTutorial(fromBeginning = false) {
239
+ if (_active) destroyTutorial();
240
+
241
+ _active = true;
242
+ _currentStep = 0;
243
+
244
+ // Always start from step 0 — no mid-tutorial resume on refresh
245
+ if (fromBeginning) {
246
+ storage.removeItem(KEYS.TUTORIAL_COMPLETED);
247
+ storage.removeItem(KEYS.TUTORIAL_SKIPPED);
248
+ storage.removeItem(KEYS.TUTORIAL_STARTED);
249
+ storage.removeItem(KEYS.ONBOARDING_EXPLORE);
250
+ storage.removeItem(KEYS.ONBOARDING_CLI);
251
+ storage.removeItem(KEYS.ONBOARDING_MODEL);
252
+ storage.removeItem(KEYS.ONBOARDING_PROJECT);
253
+ }
254
+ storage.removeItem(KEYS.TUTORIAL_STEP);
255
+
256
+ // Mark that tutorial has been seen — refresh mid-tutorial won't re-trigger
257
+ // Flush immediately (bypass 500ms debounce) so the flag survives a quick refresh
258
+ storage.setItem(KEYS.TUTORIAL_STARTED, 'true');
259
+ flushStorage();
260
+
261
+ const step = _steps[_currentStep];
262
+
263
+ if (step?.whiteboardMode) {
264
+ // Whiteboard-based rendering
265
+ _ensureFocusMode();
266
+ _hideStaticBgLogo(true);
267
+ _hideWhiteboardToolbar(true);
268
+ _hideTopControls(true);
269
+ _createWhiteboardLayers();
270
+ } else {
271
+ // Legacy overlay + card rendering
272
+ _createOverlay();
273
+ _createCard();
274
+ }
275
+
276
+ _installKeyHandler();
277
+ _installClickInterceptor();
278
+ _installResizeHandler();
279
+
280
+ emit('tutorial:start');
281
+ _renderStep(_currentStep);
282
+ }
283
+
284
+ function skipTutorial() {
285
+ storage.setItem(KEYS.TUTORIAL_SKIPPED, 'true');
286
+ storage.removeItem(KEYS.TUTORIAL_STEP);
287
+ _exitStep(() => {
288
+ _restoreFocusMode();
289
+ destroyTutorial();
290
+ emit('tutorial:skip');
291
+ });
292
+ }
293
+
294
+ function completeTutorial() {
295
+ storage.setItem(KEYS.TUTORIAL_COMPLETED, 'true');
296
+ storage.removeItem(KEYS.TUTORIAL_STEP);
297
+ _exitStep(() => _animateCompletion());
298
+ }
299
+
300
+ async function _animateCompletion() {
301
+ // ── 1. Fade out tutorial layers ──
302
+ if (_wbTutorialSvg) {
303
+ _wbTutorialSvg.style.transition = 'opacity 0.6s ease';
304
+ _wbTutorialSvg.style.opacity = '0';
305
+ }
306
+ if (_wbTutorialDom) {
307
+ _wbTutorialDom.style.transition = 'opacity 0.6s ease';
308
+ _wbTutorialDom.style.opacity = '0';
309
+ }
310
+
311
+ await new Promise(r => setTimeout(r, 400));
312
+
313
+ // ── 2. Reveal logo with scale + fade entrance ──
314
+ const logo = document.querySelector('#static-bg .static-bg-logo');
315
+ const breathe = document.querySelector('#static-bg .focus-breathe');
316
+
317
+ if (logo) {
318
+ logo.style.transition = 'none';
319
+ logo.style.transform = 'translate(-50%, -50%) scale(0.7)';
320
+ logo.style.opacity = '0';
321
+ logo.classList.remove('tutorial-hidden');
322
+ logo.offsetHeight; // force reflow
323
+ logo.style.transition = 'opacity 0.8s cubic-bezier(0.22, 1, 0.36, 1), transform 0.8s cubic-bezier(0.22, 1, 0.36, 1)';
324
+ logo.style.opacity = ''; // back to CSS default (0.18)
325
+ logo.style.transform = '';
326
+ }
327
+ if (breathe) {
328
+ breathe.style.transition = 'none';
329
+ breathe.style.opacity = '0';
330
+ breathe.classList.remove('tutorial-hidden');
331
+ breathe.offsetHeight;
332
+ breathe.style.transition = 'opacity 1.2s ease 0.3s';
333
+ breathe.style.opacity = '';
334
+ }
335
+
336
+ // ── 3. Slide toolbar in from left (staggered) ──
337
+ const toolbar = document.getElementById('wb-toolbar');
338
+ if (toolbar) {
339
+ toolbar.style.transition = 'none';
340
+ toolbar.style.transform = 'translateX(-30px)';
341
+ toolbar.style.opacity = '0';
342
+ toolbar.style.pointerEvents = '';
343
+ toolbar.classList.remove('tutorial-hidden');
344
+ toolbar.offsetHeight;
345
+ toolbar.style.transition = 'opacity 0.5s ease 0.2s, transform 0.5s cubic-bezier(0.22, 1, 0.36, 1) 0.2s';
346
+ toolbar.style.opacity = '';
347
+ toolbar.style.transform = '';
348
+ }
349
+
350
+ // ── 4. Slide top controls in from top (staggered) ──
351
+ const topControls = document.getElementById('topright-controls');
352
+ if (topControls) {
353
+ topControls.style.transition = 'none';
354
+ topControls.style.transform = 'translateY(-20px)';
355
+ topControls.style.opacity = '0';
356
+ topControls.classList.remove('tutorial-hidden');
357
+ topControls.offsetHeight;
358
+ topControls.style.transition = 'opacity 0.5s ease 0.35s, transform 0.5s cubic-bezier(0.22, 1, 0.36, 1) 0.35s';
359
+ topControls.style.opacity = '';
360
+ topControls.style.transform = '';
361
+ }
362
+
363
+ // ── 5. Wait for all transitions, then clean up ──
364
+ await new Promise(r => setTimeout(r, 1000));
365
+
366
+ // Clean inline styles
367
+ if (logo) { logo.style.transition = ''; logo.style.transform = ''; logo.style.opacity = ''; }
368
+ if (breathe) { breathe.style.transition = ''; breathe.style.opacity = ''; }
369
+ if (toolbar) { toolbar.style.transition = ''; toolbar.style.transform = ''; toolbar.style.opacity = ''; }
370
+ if (topControls) { topControls.style.transition = ''; topControls.style.transform = ''; topControls.style.opacity = ''; }
371
+
372
+ _restoreFocusMode();
373
+ destroyTutorial();
374
+ emit('tutorial:complete');
375
+ }
376
+
377
+ function destroyTutorial() {
378
+ _active = false;
379
+ _currentStep = -1;
380
+ _isSkipAnimating = false;
381
+ if (_sequencer) { _sequencer.cancel(); _sequencer = null; }
382
+
383
+ // Overlay mode cleanup
384
+ if (_spotlight) { _spotlight.destroy(); _spotlight = null; }
385
+ if (_overlayEl) { _overlayEl.remove(); _overlayEl = null; }
386
+ if (_cardEl) { _cardEl.remove(); _cardEl = null; }
387
+ if (_pulseEl) { _pulseEl.remove(); _pulseEl = null; }
388
+ if (_advanceCleanup) { _advanceCleanup(); _advanceCleanup = null; }
389
+ if (_domAnnotationLayer) { _domAnnotationLayer.remove(); _domAnnotationLayer = null; }
390
+
391
+ // Whiteboard mode cleanup
392
+ _destroyWhiteboardLayers();
393
+ _hideStaticBgLogo(false);
394
+ _hideWhiteboardToolbar(false);
395
+ _hideTopControls(false);
396
+
397
+ // Close any open menubar dropdowns opened by tutorial
398
+ const openMenu = document.querySelector('.menubar-item.open');
399
+ if (openMenu) openMenu.classList.remove('open');
400
+
401
+ // Handlers
402
+ if (_keyHandler) { document.removeEventListener('keydown', _keyHandler, true); _keyHandler = null; }
403
+ _removeClickInterceptor();
404
+ if (_resizeTimer) { clearTimeout(_resizeTimer); _resizeTimer = null; }
405
+ window.removeEventListener('resize', _onResize);
406
+ }
407
+
408
+
409
+ // ═══════════════════════════════════════════
410
+ // STEP NAVIGATION
411
+ // ═══════════════════════════════════════════
412
+
413
+ function nextStep() {
414
+ if (_isSkipAnimating) return;
415
+ if (_currentStep >= _steps.length - 1) {
416
+ completeTutorial();
417
+ return;
418
+ }
419
+ _exitStep(() => {
420
+ _currentStep++;
421
+ emit('tutorial:step', { index: _currentStep, id: _steps[_currentStep].id });
422
+ _renderStep(_currentStep);
423
+ });
424
+ }
425
+
426
+ function prevStep() {
427
+ if (_isSkipAnimating) return;
428
+ if (_currentStep <= 0) return;
429
+ _exitStep(() => {
430
+ _currentStep--;
431
+ emit('tutorial:step', { index: _currentStep, id: _steps[_currentStep].id });
432
+ _renderStep(_currentStep);
433
+ });
434
+ }
435
+
436
+ function goToStep(targetIndex) {
437
+ if (_isSkipAnimating) return;
438
+ if (targetIndex < 0 || targetIndex >= _steps.length) return;
439
+ _exitStep(() => {
440
+ _currentStep = targetIndex;
441
+ emit('tutorial:step', { index: _currentStep, id: _steps[_currentStep].id });
442
+ _renderStep(_currentStep);
443
+ });
444
+ }
445
+
446
+
447
+ // ═══════════════════════════════════════════
448
+ // STEP RENDERING (ROUTER)
449
+ // ═══════════════════════════════════════════
450
+
451
+ function _renderStep(index) {
452
+ const step = _steps[index];
453
+ if (!step) return;
454
+
455
+ step.onEnter?.({ nextStep, prevStep, skipTutorial, emit });
456
+
457
+ // If step has onEnter, delay render to let CSS transitions settle (panels slide 0.3s)
458
+ const renderDelay = step.onEnter ? 380 : 0;
459
+
460
+ if (step.whiteboardMode) {
461
+ if (renderDelay > 0) {
462
+ setTimeout(() => _renderWhiteboardStep(step, index), renderDelay);
463
+ } else {
464
+ _renderWhiteboardStep(step, index);
465
+ }
466
+ } else {
467
+ // Legacy overlay mode
468
+ if (step.focusModeRequired) {
469
+ const staticBg = document.getElementById('static-bg');
470
+ if (!staticBg?.classList.contains('visible')) {
471
+ document.getElementById('titlebar-viz-toggle')?.click();
472
+ setTimeout(() => _renderOverlayStep(step, index), 600);
473
+ return;
474
+ }
475
+ }
476
+ _renderOverlayStep(step, index);
477
+ }
478
+ }
479
+
480
+
481
+ // ═══════════════════════════════════════════
482
+ // WHITEBOARD MODE RENDERING
483
+ // ═══════════════════════════════════════════
484
+
485
+ function _renderWhiteboardStep(step, index) {
486
+ // Clean previous step content
487
+ if (_sequencer) { _sequencer.cancel(); _sequencer = null; }
488
+ if (_wbTutorialSvg) _wbTutorialSvg.innerHTML = '';
489
+ if (_wbTutorialDom) _wbTutorialDom.innerHTML = '';
490
+
491
+ if (!step.render) return;
492
+
493
+ _sequencer = new AnimationSequencer();
494
+ step.render({
495
+ svg: _wbTutorialSvg,
496
+ dom: _wbTutorialDom,
497
+ seq: _sequencer,
498
+ vw: window.innerWidth,
499
+ vh: window.innerHeight,
500
+ nextStep,
501
+ prevStep,
502
+ goToStep,
503
+ skipTutorial,
504
+ onSkipWithArrow: _animateSkipArrow,
505
+ createButton: _createHandDrawnButton,
506
+ });
507
+ _sequencer.play();
508
+ }
509
+
510
+
511
+ // ═══════════════════════════════════════════
512
+ // SKIP ARROW ANIMATION ("I'll be here")
513
+ // ═══════════════════════════════════════════
514
+
515
+ async function _animateSkipArrow() {
516
+ if (_isSkipAnimating) return;
517
+ _isSkipAnimating = true;
518
+
519
+ // Clear current content
520
+ if (_sequencer) { _sequencer.cancel(); _sequencer = null; }
521
+ if (_wbTutorialSvg) _wbTutorialSvg.innerHTML = '';
522
+ if (_wbTutorialDom) _wbTutorialDom.innerHTML = '';
523
+
524
+ // Find the ? button
525
+ const tutBtn = document.getElementById('titlebar-tutorial-btn');
526
+ const btnRect = tutBtn?.getBoundingClientRect();
527
+ if (!btnRect || !_wbTutorialSvg) { _finishSkip(); return; }
528
+
529
+ // Arrow tip: below the ? button, clear of the toolbar
530
+ const toX = btnRect.left + btnRect.width / 2;
531
+ const toY = btnRect.bottom + 16;
532
+
533
+ // Arrow start: well below the toolbar so nothing overlaps
534
+ const fromX = toX - 50;
535
+ const fromY = toY + 120;
536
+
537
+ const seq = new AnimationSequencer();
538
+ _sequencer = seq;
539
+
540
+ // "I'll be here" handwritten text — appears first, then arrow points up from it
541
+ seq.add(() => handwrittenLabel(_wbTutorialSvg, "I'll be here", {
542
+ x: fromX - 30,
543
+ y: fromY + 28,
544
+ color: 'rgba(255, 255, 255, 0.45)',
545
+ fontSize: 20,
546
+ duration: 500,
547
+ }), 200);
548
+
549
+ // Small curvy arrow from text area up to the ? button
550
+ seq.add(() => createAnimatedArrow(_wbTutorialSvg, {
551
+ from: { x: fromX, y: fromY },
552
+ to: { x: toX, y: toY },
553
+ color: 'rgba(255, 255, 255, 0.35)',
554
+ wobbleSeed: 'skip-arrow',
555
+ duration: 600,
556
+ delay: 0,
557
+ }), 300);
558
+
559
+ // Tiny sparkle near the button
560
+ seq.add(() => createDoodle(_wbTutorialSvg, 'sparkle', {
561
+ x: toX + 18,
562
+ y: toY + 6,
563
+ scale: 0.6,
564
+ color: 'rgba(255, 255, 255, 0.3)',
565
+ duration: 400,
566
+ }), 400);
567
+
568
+ await seq.play();
569
+
570
+ // Hold for 2 seconds so the user can read it
571
+ await new Promise(r => setTimeout(r, 2000));
572
+ if (!_active) return;
573
+
574
+ // ── Graceful exit: fade arrow, fade in logo, slide in toolbar ──
575
+
576
+ // 1. Fade out the tutorial drawings
577
+ if (_wbTutorialSvg) {
578
+ _wbTutorialSvg.style.transition = 'opacity 0.5s ease';
579
+ _wbTutorialSvg.style.opacity = '0';
580
+ }
581
+ if (_wbTutorialDom) {
582
+ _wbTutorialDom.style.transition = 'opacity 0.5s ease';
583
+ _wbTutorialDom.style.opacity = '0';
584
+ }
585
+
586
+ await new Promise(r => setTimeout(r, 300));
587
+ if (!_active) return;
588
+
589
+ // 2. Fade in the center logo (remove tutorial-hidden — CSS transition handles fade)
590
+ _hideStaticBgLogo(false);
591
+
592
+ // 3. Slide toolbar in from left
593
+ const toolbar = document.getElementById('wb-toolbar');
594
+ if (toolbar) {
595
+ toolbar.style.transition = 'none';
596
+ toolbar.style.transform = 'translateX(-30px)';
597
+ toolbar.style.opacity = '0';
598
+ toolbar.style.pointerEvents = '';
599
+ toolbar.classList.remove('tutorial-hidden');
600
+ toolbar.offsetHeight; // eslint-disable-line no-unused-expressions
601
+ toolbar.style.transition = 'opacity 0.5s ease, transform 0.5s cubic-bezier(0.22, 1, 0.36, 1)';
602
+ toolbar.style.opacity = '';
603
+ toolbar.style.transform = '';
604
+ }
605
+
606
+ // 4. Slide top controls in from top
607
+ const topControls = document.getElementById('topright-controls');
608
+ if (topControls) {
609
+ topControls.style.transition = 'none';
610
+ topControls.style.transform = 'translateY(-20px)';
611
+ topControls.style.opacity = '0';
612
+ topControls.classList.remove('tutorial-hidden');
613
+ topControls.offsetHeight; // eslint-disable-line no-unused-expressions
614
+ topControls.style.transition = 'opacity 0.5s ease 0.1s, transform 0.5s cubic-bezier(0.22, 1, 0.36, 1) 0.1s';
615
+ topControls.style.opacity = '';
616
+ topControls.style.transform = '';
617
+ }
618
+
619
+ // Wait for transitions to finish
620
+ await new Promise(r => setTimeout(r, 700));
621
+ if (!_active) return;
622
+
623
+ // Clean up inline styles
624
+ if (toolbar) {
625
+ toolbar.style.transition = '';
626
+ toolbar.style.transform = '';
627
+ toolbar.style.opacity = '';
628
+ }
629
+ if (topControls) {
630
+ topControls.style.transition = '';
631
+ topControls.style.transform = '';
632
+ topControls.style.opacity = '';
633
+ }
634
+
635
+ _finishSkip();
636
+ }
637
+
638
+ function _finishSkip() {
639
+ storage.setItem(KEYS.TUTORIAL_SKIPPED, 'true');
640
+ storage.removeItem(KEYS.TUTORIAL_STEP);
641
+ _hideStaticBgLogo(false);
642
+ _hideWhiteboardToolbar(false);
643
+ _hideTopControls(false);
644
+ _restoreFocusMode();
645
+ destroyTutorial();
646
+ emit('tutorial:skip');
647
+ }
648
+
649
+
650
+ // ═══════════════════════════════════════════
651
+ // OVERLAY MODE RENDERING (LEGACY)
652
+ // ═══════════════════════════════════════════
653
+
654
+ function _createOverlay() {
655
+ if (_overlayEl) return;
656
+ _overlayEl = document.createElementNS(SVG_NS, 'svg');
657
+ _overlayEl.id = 'tutorial-overlay';
658
+ _overlayEl.setAttribute('viewBox', `0 0 ${window.innerWidth} ${window.innerHeight}`);
659
+ document.body.appendChild(_overlayEl);
660
+ }
661
+
662
+ function _createCard() {
663
+ if (_cardEl) return;
664
+ _cardEl = document.createElement('div');
665
+ _cardEl.className = 'tutorial-card';
666
+ _cardEl.innerHTML = `
667
+ <div class="tutorial-title"></div>
668
+ <div class="tutorial-body"></div>
669
+ <div class="tutorial-nav">
670
+ <button class="tutorial-nav-btn tutorial-btn-back">Back</button>
671
+ <span class="tutorial-step-counter"></span>
672
+ <button class="tutorial-nav-btn primary tutorial-btn-next">Next</button>
673
+ </div>
674
+ <span class="tutorial-skip">Skip tutorial</span>
675
+ `;
676
+ document.body.appendChild(_cardEl);
677
+ _cardEl.querySelector('.tutorial-btn-next').addEventListener('click', nextStep);
678
+ _cardEl.querySelector('.tutorial-btn-back').addEventListener('click', prevStep);
679
+ _cardEl.querySelector('.tutorial-skip').addEventListener('click', skipTutorial);
680
+ }
681
+
682
+ function _renderOverlayStep(step, index) {
683
+ const targetRect = resolveTargetRect(step.target);
684
+
685
+ // Spotlight
686
+ if (_spotlight) { _spotlight.destroy(); _spotlight = null; }
687
+ if (step.spotlight !== false) {
688
+ _spotlight = createSpotlight(_overlayEl, targetRect, {
689
+ padding: step.spotlightPadding || 16,
690
+ wobbleSeed: step.id + '-spot',
691
+ });
692
+ }
693
+
694
+ // Card
695
+ _updateCard(step, index, targetRect);
696
+
697
+ // Annotations
698
+ if (_annotationGroup) _annotationGroup.remove();
699
+ _annotationGroup = document.createElementNS(SVG_NS, 'g');
700
+ _annotationGroup.classList.add('tutorial-annotations');
701
+ _overlayEl.appendChild(_annotationGroup);
702
+
703
+ if (_sequencer) _sequencer.cancel();
704
+ _sequencer = new AnimationSequencer();
705
+ for (const ann of (step.annotations || [])) {
706
+ _sequencer.add(() => _renderAnnotation(ann, step, targetRect), ann.delay || 0);
707
+ }
708
+ _sequencer.play();
709
+
710
+ _setupAdvanceTrigger(step, targetRect);
711
+ _updatePulseRing(step, targetRect);
712
+ }
713
+
714
+ function _updateCard(step, index, targetRect) {
715
+ _cardEl.querySelector('.tutorial-title').textContent = step.title || '';
716
+ _cardEl.querySelector('.tutorial-body').textContent = step.body || '';
717
+ _cardEl.querySelector('.tutorial-step-counter').textContent = `${index + 1} of ${_steps.length}`;
718
+ _cardEl.querySelector('.tutorial-btn-back').style.display = index === 0 ? 'none' : '';
719
+ _cardEl.querySelector('.tutorial-btn-next').textContent = index === _steps.length - 1 ? 'Finish' : 'Next';
720
+
721
+ _positionCard(step.cardPosition || 'auto', targetRect);
722
+ _cardEl.classList.remove('exit');
723
+ requestAnimationFrame(() => { _cardEl.classList.add('visible'); });
724
+ }
725
+
726
+ function _positionCard(position, targetRect) {
727
+ const cardW = 380;
728
+ const cardH = _cardEl.offsetHeight || 200;
729
+ const margin = 24;
730
+ const vw = window.innerWidth;
731
+ const vh = window.innerHeight;
732
+ let left, top;
733
+
734
+ if (position === 'center' || !targetRect) {
735
+ left = (vw - cardW) / 2;
736
+ top = (vh - cardH) / 2;
737
+ } else if (position === 'auto') {
738
+ const cx = targetRect.left + targetRect.width / 2;
739
+ const cy = targetRect.top + targetRect.height / 2;
740
+ left = cx < vw / 2
741
+ ? Math.min(targetRect.right + margin + 20, vw - cardW - margin)
742
+ : Math.max(targetRect.left - cardW - margin - 20, margin);
743
+ top = cy < vh / 2
744
+ ? Math.max(targetRect.bottom + margin, margin)
745
+ : Math.max(targetRect.top - cardH - margin, margin);
746
+ left = Math.max(margin, Math.min(left, vw - cardW - margin));
747
+ top = Math.max(margin, Math.min(top, vh - cardH - margin));
748
+ } else {
749
+ const positions = {
750
+ 'top-left': { left: margin, top: margin },
751
+ 'top-right': { left: vw - cardW - margin, top: margin },
752
+ 'bottom-left': { left: margin, top: vh - cardH - margin },
753
+ 'bottom-right': { left: vw - cardW - margin, top: vh - cardH - margin },
754
+ 'top-center': { left: (vw - cardW) / 2, top: margin },
755
+ 'bottom-center': { left: (vw - cardW) / 2, top: vh - cardH - margin },
756
+ };
757
+ const pos = positions[position] || positions['top-right'];
758
+ left = pos.left;
759
+ top = pos.top;
760
+ }
761
+ _cardEl.style.left = `${left}px`;
762
+ _cardEl.style.top = `${top}px`;
763
+ }
764
+
765
+ function _renderAnnotation(ann, step, targetRect) {
766
+ const svg = _annotationGroup;
767
+
768
+ switch (ann.type) {
769
+ case 'arrow': {
770
+ const from = _resolveAnnotationPoint(ann.from, targetRect);
771
+ const to = _resolveAnnotationPoint(ann.to, targetRect, step.targetAnchor);
772
+ if (!from || !to) return null;
773
+ return createAnimatedArrow(svg, {
774
+ from, to,
775
+ color: ann.color || 'rgba(255,255,255,0.35)',
776
+ wobbleSeed: step.id + '-arrow',
777
+ duration: ann.duration || 800,
778
+ arrowheadSize: ann.arrowheadSize || 14,
779
+ });
780
+ }
781
+ case 'circle': {
782
+ const rect = ann.target === 'inherit' ? targetRect : resolveTargetRect(ann.target);
783
+ if (!rect) return null;
784
+ return createWobblyCircle(svg, rect, {
785
+ color: ann.color || 'rgba(255,255,255,0.3)',
786
+ padding: ann.padding || 12,
787
+ wobbleSeed: step.id + '-circle',
788
+ duration: ann.duration || 700,
789
+ });
790
+ }
791
+ case 'underline': {
792
+ const rect = ann.target === 'inherit' ? targetRect : resolveTargetRect(ann.target);
793
+ if (!rect) return null;
794
+ return createHandDrawnUnderline(svg, rect, {
795
+ color: ann.color || 'rgba(255,255,255,0.3)',
796
+ wobbleSeed: step.id + '-underline',
797
+ duration: ann.duration || 500,
798
+ });
799
+ }
800
+ case 'text': {
801
+ if (ann.style === 'handwritten') {
802
+ const pos = _resolveAnnotationPoint(ann.position, targetRect);
803
+ if (!pos) return null;
804
+ return handwrittenLabel(svg, ann.text, {
805
+ x: pos.x, y: pos.y,
806
+ color: ann.color || 'rgba(255,255,255,0.4)',
807
+ fontSize: ann.fontSize || 18,
808
+ textAnchor: ann.textAnchor,
809
+ duration: ann.duration || 400,
810
+ });
811
+ }
812
+ const container = _getOrCreateDomAnnotationLayer();
813
+ return typewriterText(container, ann.text, {
814
+ charDelay: ann.charDelay || 35,
815
+ font: ann.font || 'sans',
816
+ color: ann.color || 'rgba(255,255,255,0.6)',
817
+ });
818
+ }
819
+ case 'doodle': {
820
+ const pos = _resolveAnnotationPoint(ann.position, targetRect);
821
+ if (!pos) return null;
822
+ return createDoodle(svg, ann.doodleId, {
823
+ x: pos.x, y: pos.y,
824
+ scale: ann.scale || 1,
825
+ color: ann.color || 'rgba(255,255,255,0.3)',
826
+ duration: ann.duration || 600,
827
+ });
828
+ }
829
+ default:
830
+ return null;
831
+ }
832
+ }
833
+
834
+ function _resolveAnnotationPoint(value, targetRect, defaultAnchor) {
835
+ if (!value) return null;
836
+ if (value === 'target') {
837
+ if (!targetRect) return null;
838
+ return resolveAnchorPoint(targetRect, defaultAnchor || 'center');
839
+ }
840
+ if (typeof value === 'object' && 'x' in value && 'y' in value) {
841
+ if (value.x <= 1 && value.y <= 1 && value.x >= 0 && value.y >= 0) {
842
+ return resolveRelativePoint(value);
843
+ }
844
+ return value;
845
+ }
846
+ return null;
847
+ }
848
+
849
+ function _getOrCreateDomAnnotationLayer() {
850
+ if (_domAnnotationLayer && _domAnnotationLayer.parentNode) return _domAnnotationLayer;
851
+ _domAnnotationLayer = document.createElement('div');
852
+ _domAnnotationLayer.style.cssText = 'position:fixed;inset:0;z-index:9998;pointer-events:none;display:flex;align-items:center;justify-content:center;padding:20px;';
853
+ document.body.appendChild(_domAnnotationLayer);
854
+ return _domAnnotationLayer;
855
+ }
856
+
857
+ function _setupAdvanceTrigger(step, targetRect) {
858
+ if (_advanceCleanup) { _advanceCleanup(); _advanceCleanup = null; }
859
+ const advanceOn = step.advanceOn || 'click';
860
+
861
+ if (advanceOn === 'click') return;
862
+
863
+ if (advanceOn === 'target-click' && step.target) {
864
+ const targetEl = document.querySelector(step.target);
865
+ if (targetEl) {
866
+ const handler = () => nextStep();
867
+ targetEl.addEventListener('click', handler, { once: true });
868
+ _advanceCleanup = () => targetEl.removeEventListener('click', handler);
869
+ }
870
+ return;
871
+ }
872
+ if (advanceOn.startsWith('key:')) {
873
+ const key = advanceOn.slice(4).toLowerCase();
874
+ const handler = (e) => {
875
+ if (e.key.toLowerCase() === key) { nextStep(); document.removeEventListener('keydown', handler); }
876
+ };
877
+ document.addEventListener('keydown', handler);
878
+ _advanceCleanup = () => document.removeEventListener('keydown', handler);
879
+ return;
880
+ }
881
+ if (advanceOn.startsWith('auto:')) {
882
+ const ms = parseInt(advanceOn.slice(5), 10) || 3000;
883
+ const timer = setTimeout(nextStep, ms);
884
+ _advanceCleanup = () => clearTimeout(timer);
885
+ }
886
+ }
887
+
888
+ function _updatePulseRing(step, targetRect) {
889
+ if (_pulseEl) { _pulseEl.remove(); _pulseEl = null; }
890
+ if (step.advanceOn === 'target-click' && targetRect) {
891
+ _pulseEl = document.createElement('div');
892
+ _pulseEl.className = 'tutorial-pulse-ring';
893
+ _pulseEl.style.left = `${targetRect.left - 4}px`;
894
+ _pulseEl.style.top = `${targetRect.top - 4}px`;
895
+ _pulseEl.style.width = `${targetRect.width + 8}px`;
896
+ _pulseEl.style.height = `${targetRect.height + 8}px`;
897
+ document.body.appendChild(_pulseEl);
898
+ }
899
+ }
900
+
901
+
902
+ // ═══════════════════════════════════════════
903
+ // STEP EXIT
904
+ // ═══════════════════════════════════════════
905
+
906
+ function _exitStep(callback) {
907
+ const step = _steps[_currentStep];
908
+ if (step) step.onExit?.({ nextStep, prevStep, skipTutorial, emit });
909
+
910
+ if (_sequencer) { _sequencer.cancel(); _sequencer = null; }
911
+ if (_advanceCleanup) { _advanceCleanup(); _advanceCleanup = null; }
912
+ if (_pulseEl) { _pulseEl.remove(); _pulseEl = null; }
913
+
914
+ if (step?.whiteboardMode) {
915
+ // Whiteboard mode: just clear the layers
916
+ if (_wbTutorialSvg) _wbTutorialSvg.innerHTML = '';
917
+ if (_wbTutorialDom) _wbTutorialDom.innerHTML = '';
918
+ callback?.();
919
+ } else {
920
+ // Overlay mode: fade out annotations + card
921
+ if (_annotationGroup) {
922
+ _annotationGroup.style.transition = 'opacity 0.2s ease';
923
+ _annotationGroup.style.opacity = '0';
924
+ }
925
+ if (_cardEl) {
926
+ _cardEl.classList.remove('visible');
927
+ _cardEl.classList.add('exit');
928
+ }
929
+ if (_spotlight) {
930
+ _spotlight.fadeOut(() => {
931
+ if (_spotlight) { _spotlight.destroy(); _spotlight = null; }
932
+ });
933
+ }
934
+ if (_domAnnotationLayer) {
935
+ _domAnnotationLayer.remove();
936
+ _domAnnotationLayer = null;
937
+ }
938
+ setTimeout(() => {
939
+ if (_annotationGroup) { _annotationGroup.remove(); _annotationGroup = null; }
940
+ callback?.();
941
+ }, 300);
942
+ }
943
+ }
944
+
945
+
946
+ // ═══════════════════════════════════════════
947
+ // KEYBOARD HANDLER
948
+ // ═══════════════════════════════════════════
949
+
950
+ function _installKeyHandler() {
951
+ if (_keyHandler) return;
952
+ _keyHandler = (e) => {
953
+ if (!_active) return;
954
+
955
+ const step = _steps[_currentStep];
956
+
957
+ // Block V key during whiteboard steps to prevent exiting focus mode
958
+ if ((e.key === 'v' || e.key === 'V') && step?.whiteboardMode) {
959
+ e.stopPropagation();
960
+ e.preventDefault();
961
+ return;
962
+ }
963
+
964
+ if (e.key === 'Escape') {
965
+ e.stopPropagation();
966
+ if (_isSkipAnimating) return;
967
+ if (step?.id === 'welcome' && step?.whiteboardMode) {
968
+ _animateSkipArrow();
969
+ } else if (_currentStep <= 5) {
970
+ skipTutorial();
971
+ } else {
972
+ prevStep();
973
+ }
974
+ return;
975
+ }
976
+
977
+ if (e.key === 'ArrowRight' || e.key === 'Enter' || e.key === ' ') {
978
+ e.stopPropagation();
979
+ e.preventDefault();
980
+ nextStep();
981
+ return;
982
+ }
983
+
984
+ if (e.key === 'ArrowLeft') {
985
+ e.stopPropagation();
986
+ prevStep();
987
+ }
988
+ };
989
+ document.addEventListener('keydown', _keyHandler, true);
990
+ }
991
+
992
+
993
+ // ═══════════════════════════════════════════
994
+ // TOOLBAR CLICK INTERCEPTOR (ADHD protection)
995
+ // ═══════════════════════════════════════════
996
+
997
+ const _interceptedBtnIds = [
998
+ 'titlebar-viz-toggle',
999
+ 'titlebar-fullscreen-btn',
1000
+ ];
1001
+
1002
+ function _installClickInterceptor() {
1003
+ if (_clickInterceptor) return;
1004
+ _clickInterceptor = (e) => {
1005
+ if (!_active || _isSkipAnimating) return;
1006
+ const step = _steps[_currentStep];
1007
+ if (!step?.whiteboardMode) return;
1008
+
1009
+ // Check if the click target is one of the intercepted buttons or menu items
1010
+ const btn = e.target.closest('#titlebar-viz-toggle, #titlebar-fullscreen-btn, #topright-keybinds-btn, .menubar-dropdown .menu-item, .menubar-label');
1011
+ if (!btn) return;
1012
+
1013
+ e.stopPropagation();
1014
+ e.preventDefault();
1015
+ _showAdhdMessage();
1016
+ };
1017
+ document.addEventListener('click', _clickInterceptor, true);
1018
+ }
1019
+
1020
+ function _removeClickInterceptor() {
1021
+ if (_clickInterceptor) {
1022
+ document.removeEventListener('click', _clickInterceptor, true);
1023
+ _clickInterceptor = null;
1024
+ }
1025
+ if (_adhdMessageTimer) {
1026
+ clearTimeout(_adhdMessageTimer);
1027
+ _adhdMessageTimer = null;
1028
+ }
1029
+ }
1030
+
1031
+ function _showAdhdMessage() {
1032
+ if (!_wbTutorialSvg || !_wbTutorialDom) return;
1033
+
1034
+ // Cancel the step's sequencer so pending tasks don't re-add elements
1035
+ if (_sequencer) { _sequencer.cancel(); _sequencer = null; }
1036
+
1037
+ // Save current step content and replace with ADHD message
1038
+ const savedSvg = _wbTutorialSvg.innerHTML;
1039
+ const savedDom = _wbTutorialDom.innerHTML;
1040
+ _wbTutorialSvg.innerHTML = '';
1041
+ _wbTutorialDom.innerHTML = '';
1042
+
1043
+ if (_adhdMessageTimer) clearTimeout(_adhdMessageTimer);
1044
+
1045
+ const seq = new AnimationSequencer();
1046
+ const cx = window.innerWidth / 2;
1047
+ const cy = window.innerHeight / 2 - 20;
1048
+
1049
+ seq.add(() => handwrittenLabel(_wbTutorialSvg, 'Wait bro... gee', {
1050
+ x: cx, y: cy - 10,
1051
+ textAnchor: 'middle',
1052
+ color: 'rgba(255, 255, 255, 0.55)',
1053
+ fontSize: 28, duration: 400,
1054
+ }), 100);
1055
+
1056
+ seq.add(() => handwrittenLabel(_wbTutorialSvg, 'You can click shit later.', {
1057
+ x: cx, y: cy + 30,
1058
+ textAnchor: 'middle',
1059
+ color: 'rgba(255, 255, 255, 0.35)',
1060
+ fontSize: 20, duration: 400,
1061
+ }), 400);
1062
+
1063
+ // OK button to dismiss and return to the step
1064
+ seq.add(() => {
1065
+ const okEl = _createHandDrawnButton(_wbTutorialDom, 'Ok', {
1066
+ x: cx, y: cy + 100, fontSize: 24,
1067
+ color: 'rgba(255, 255, 255, 0.45)',
1068
+ onClick: () => {
1069
+ if (!_wbTutorialSvg || !_wbTutorialDom || !_active) return;
1070
+ _wbTutorialSvg.innerHTML = '';
1071
+ _wbTutorialDom.innerHTML = '';
1072
+ const step = _steps[_currentStep];
1073
+ if (step) _renderWhiteboardStep(step, _currentStep);
1074
+ },
1075
+ });
1076
+ return { destroy() { okEl.remove(); } };
1077
+ }, 700);
1078
+
1079
+ seq.play();
1080
+ }
1081
+
1082
+
1083
+ // ═══════════════════════════════════════════
1084
+ // RESIZE HANDLER
1085
+ // ═══════════════════════════════════════════
1086
+
1087
+ function _onResize() {
1088
+ if (_resizeTimer) clearTimeout(_resizeTimer);
1089
+ _resizeTimer = setTimeout(() => {
1090
+ if (!_active || _currentStep < 0) return;
1091
+
1092
+ const step = _steps[_currentStep];
1093
+ if (!step) return;
1094
+
1095
+ if (step.whiteboardMode && !_isSkipAnimating) {
1096
+ // Re-render the whiteboard step to re-center
1097
+ _renderWhiteboardStep(step, _currentStep);
1098
+ } else if (!step.whiteboardMode) {
1099
+ if (_overlayEl) {
1100
+ _overlayEl.setAttribute('viewBox', `0 0 ${window.innerWidth} ${window.innerHeight}`);
1101
+ }
1102
+ const targetRect = resolveTargetRect(step.target);
1103
+ if (_spotlight) _spotlight.update(targetRect);
1104
+ if (_cardEl) _positionCard(step.cardPosition || 'auto', targetRect);
1105
+ if (_pulseEl && targetRect) {
1106
+ _pulseEl.style.left = `${targetRect.left - 4}px`;
1107
+ _pulseEl.style.top = `${targetRect.top - 4}px`;
1108
+ _pulseEl.style.width = `${targetRect.width + 8}px`;
1109
+ _pulseEl.style.height = `${targetRect.height + 8}px`;
1110
+ }
1111
+ }
1112
+ }, 300);
1113
+ }
1114
+
1115
+ function _installResizeHandler() {
1116
+ window.addEventListener('resize', _onResize);
1117
+ }