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.
- package/CHANGELOG.md +665 -0
- package/LICENSE +191 -0
- package/README.md +853 -0
- package/hooks/claude-code/post-plan.mjs +391 -0
- package/hooks/claude-code/post-remember.mjs +239 -0
- package/hooks/claude-code/pre-compact.mjs +241 -0
- package/hooks/claude-code/pre-websearch.mjs +90 -0
- package/hooks/claude-code/prompt-submit.mjs +861 -0
- package/hooks/claude-code/session-start.mjs +494 -0
- package/hooks/claude-code/shared.mjs +368 -0
- package/hooks/claude-code/stop.mjs +646 -0
- package/hooks/claude-code/test-user-learning.mjs +531 -0
- package/mcp-server/package.json +38 -0
- package/mcp-server/run.mjs +10 -0
- package/mcp-server/src/config.ts +70 -0
- package/mcp-server/src/http.ts +93 -0
- package/mcp-server/src/index.ts +203 -0
- package/mcp-server/src/preload-http.ts +25 -0
- package/mcp-server/src/preload.ts +29 -0
- package/mcp-server/src/scripts/migrate-qdrant-to-sqlite.ts +506 -0
- package/mcp-server/src/services/categories.ts +429 -0
- package/mcp-server/src/services/discord.ts +702 -0
- package/mcp-server/src/services/file-checksums.ts +33 -0
- package/mcp-server/src/services/local-embeddings.ts +77 -0
- package/mcp-server/src/services/neural-interface.ts +344 -0
- package/mcp-server/src/services/sqlite.ts +823 -0
- package/mcp-server/src/tools/browser-advanced.ts +123 -0
- package/mcp-server/src/tools/browser-interact.ts +255 -0
- package/mcp-server/src/tools/browser-navigate.ts +77 -0
- package/mcp-server/src/tools/browser-observe.ts +1330 -0
- package/mcp-server/src/tools/browser.ts +107 -0
- package/mcp-server/src/tools/card-tools.ts +193 -0
- package/mcp-server/src/tools/card.ts +22 -0
- package/mcp-server/src/tools/category.ts +379 -0
- package/mcp-server/src/tools/discord-channel.ts +274 -0
- package/mcp-server/src/tools/discord-guild.ts +188 -0
- package/mcp-server/src/tools/discord-member.ts +158 -0
- package/mcp-server/src/tools/discord-message.ts +231 -0
- package/mcp-server/src/tools/discord-onboarding.ts +228 -0
- package/mcp-server/src/tools/discord-role.ts +209 -0
- package/mcp-server/src/tools/discord-thread.ts +110 -0
- package/mcp-server/src/tools/discord-webhook.ts +176 -0
- package/mcp-server/src/tools/discord.ts +40 -0
- package/mcp-server/src/tools/forget.ts +38 -0
- package/mcp-server/src/tools/git-tools.ts +141 -0
- package/mcp-server/src/tools/git.ts +13 -0
- package/mcp-server/src/tools/image-tools.ts +98 -0
- package/mcp-server/src/tools/image.ts +17 -0
- package/mcp-server/src/tools/leonardo-browser-tools.ts +281 -0
- package/mcp-server/src/tools/leonardo.ts +28 -0
- package/mcp-server/src/tools/loop.ts +348 -0
- package/mcp-server/src/tools/memories.ts +118 -0
- package/mcp-server/src/tools/recall.ts +323 -0
- package/mcp-server/src/tools/reflect.ts +145 -0
- package/mcp-server/src/tools/remember.ts +115 -0
- package/mcp-server/src/tools/response.ts +26 -0
- package/mcp-server/src/tools/restore.ts +34 -0
- package/mcp-server/src/tools/sync.ts +114 -0
- package/mcp-server/src/tools/tictactoe-tools.ts +98 -0
- package/mcp-server/src/tools/tictactoe.ts +13 -0
- package/mcp-server/src/tools/utils.ts +25 -0
- package/mcp-server/src/tools/whiteboard-tools.ts +260 -0
- package/mcp-server/src/tools/whiteboard.ts +22 -0
- package/mcp-server/src/tui.ts +940 -0
- package/mcp-server/src/types.ts +85 -0
- package/mcp-server/tsconfig.json +19 -0
- package/neural-interface/package.json +27 -0
- package/neural-interface/public/apple-touch-icon.png +0 -0
- package/neural-interface/public/category-logos/claude.svg +1 -0
- package/neural-interface/public/category-logos/gemini-color.svg +1 -0
- package/neural-interface/public/category-logos/openai.svg +1 -0
- package/neural-interface/public/category-logos/synabun.png +0 -0
- package/neural-interface/public/claude-chat.css +878 -0
- package/neural-interface/public/claude-chat.html +92 -0
- package/neural-interface/public/claude-chat.js +1168 -0
- package/neural-interface/public/favicon-16x16.png +0 -0
- package/neural-interface/public/favicon-32x32.png +0 -0
- package/neural-interface/public/gallery.svg +6 -0
- package/neural-interface/public/icon-192.png +0 -0
- package/neural-interface/public/icon-512.png +0 -0
- package/neural-interface/public/index-legacy.html +14677 -0
- package/neural-interface/public/index.html +212 -0
- package/neural-interface/public/index2d-legacy.html +8193 -0
- package/neural-interface/public/index2d.html +127 -0
- package/neural-interface/public/invite.html +351 -0
- package/neural-interface/public/leonardoai.svg +1 -0
- package/neural-interface/public/logo-synabun.png +0 -0
- package/neural-interface/public/logoHD.png +0 -0
- package/neural-interface/public/manifest.json +14 -0
- package/neural-interface/public/offline.html +299 -0
- package/neural-interface/public/onboarding.html +2335 -0
- package/neural-interface/public/openclaw-logo-text.png +0 -0
- package/neural-interface/public/pin-svgrepo-com.svg +25 -0
- package/neural-interface/public/sessionicon.svg +8 -0
- package/neural-interface/public/shared/ansi-parser.js +305 -0
- package/neural-interface/public/shared/api.js +1043 -0
- package/neural-interface/public/shared/colors.js +153 -0
- package/neural-interface/public/shared/constants.js +141 -0
- package/neural-interface/public/shared/html-shell.js +777 -0
- package/neural-interface/public/shared/html-term-renderer.js +1022 -0
- package/neural-interface/public/shared/i18n.js +147 -0
- package/neural-interface/public/shared/registry.js +82 -0
- package/neural-interface/public/shared/state.js +86 -0
- package/neural-interface/public/shared/storage.js +150 -0
- package/neural-interface/public/shared/styles.css +21212 -0
- package/neural-interface/public/shared/ui-automation-studio.js +3573 -0
- package/neural-interface/public/shared/ui-bookmarks.js +231 -0
- package/neural-interface/public/shared/ui-claude-panel.js +6333 -0
- package/neural-interface/public/shared/ui-command-runner.js +534 -0
- package/neural-interface/public/shared/ui-cost-widget.js +602 -0
- package/neural-interface/public/shared/ui-detail.js +1454 -0
- package/neural-interface/public/shared/ui-explorer.js +1356 -0
- package/neural-interface/public/shared/ui-file-explorer.js +2750 -0
- package/neural-interface/public/shared/ui-games.js +411 -0
- package/neural-interface/public/shared/ui-help.js +107 -0
- package/neural-interface/public/shared/ui-image-gallery.js +461 -0
- package/neural-interface/public/shared/ui-invite.js +519 -0
- package/neural-interface/public/shared/ui-keybinds.js +452 -0
- package/neural-interface/public/shared/ui-layouts.js +297 -0
- package/neural-interface/public/shared/ui-link.js +731 -0
- package/neural-interface/public/shared/ui-loading.js +255 -0
- package/neural-interface/public/shared/ui-menubar.js +499 -0
- package/neural-interface/public/shared/ui-multiselect.js +93 -0
- package/neural-interface/public/shared/ui-navbar.js +222 -0
- package/neural-interface/public/shared/ui-notifications.js +287 -0
- package/neural-interface/public/shared/ui-panels.js +309 -0
- package/neural-interface/public/shared/ui-resume.js +653 -0
- package/neural-interface/public/shared/ui-search.js +145 -0
- package/neural-interface/public/shared/ui-sessions.js +531 -0
- package/neural-interface/public/shared/ui-settings.js +5987 -0
- package/neural-interface/public/shared/ui-sidebar.js +1905 -0
- package/neural-interface/public/shared/ui-skills.js +2805 -0
- package/neural-interface/public/shared/ui-stats.js +38 -0
- package/neural-interface/public/shared/ui-sync.js +201 -0
- package/neural-interface/public/shared/ui-terminal.js +6385 -0
- package/neural-interface/public/shared/ui-tooltip.js +123 -0
- package/neural-interface/public/shared/ui-trash.js +439 -0
- package/neural-interface/public/shared/ui-tutorial-draw.js +875 -0
- package/neural-interface/public/shared/ui-tutorial-steps.js +2276 -0
- package/neural-interface/public/shared/ui-tutorial.js +1117 -0
- package/neural-interface/public/shared/ui-update.js +244 -0
- package/neural-interface/public/shared/ui-whiteboard.js +3262 -0
- package/neural-interface/public/shared/ui-workspaces.js +722 -0
- package/neural-interface/public/shared/utils.js +360 -0
- package/neural-interface/public/shared/vterm-buffer.js +1102 -0
- package/neural-interface/public/sw.js +52 -0
- package/neural-interface/public/synabun.png +0 -0
- package/neural-interface/public/toolbar-album.svg +8 -0
- package/neural-interface/public/toolbar-question.svg +6 -0
- package/neural-interface/public/toolbar-sessions.svg +6 -0
- package/neural-interface/public/variant/2d/background.js +113 -0
- package/neural-interface/public/variant/2d/context-menu.js +133 -0
- package/neural-interface/public/variant/2d/gfx.js +45 -0
- package/neural-interface/public/variant/2d/graph.js +1635 -0
- package/neural-interface/public/variant/2d/hulls.js +207 -0
- package/neural-interface/public/variant/2d/lasso.js +135 -0
- package/neural-interface/public/variant/2d/layout.js +594 -0
- package/neural-interface/public/variant/2d/main.js +521 -0
- package/neural-interface/public/variant/2d/minimap.js +312 -0
- package/neural-interface/public/variant/2d/settings-gfx.js +98 -0
- package/neural-interface/public/variant/3d/background.js +884 -0
- package/neural-interface/public/variant/3d/camera.js +661 -0
- package/neural-interface/public/variant/3d/gfx.js +116 -0
- package/neural-interface/public/variant/3d/graph.js +2399 -0
- package/neural-interface/public/variant/3d/main.js +500 -0
- package/neural-interface/public/variant/3d/settings-gfx.js +228 -0
- package/neural-interface/scripts/rebuild-pty.js +100 -0
- package/neural-interface/server.js +15000 -0
- package/neural-interface/templates/CLAUDE-template.md +211 -0
- package/package.json +53 -0
- package/postinstall.js +135 -0
- package/setup.js +260 -0
- package/skills/leonardo/SKILL.md +159 -0
- package/skills/leonardo/icon.svg +1 -0
- package/skills/leonardo/modules/browser-guide.md +378 -0
- package/skills/leonardo/modules/image-prompter.md +277 -0
- package/skills/leonardo/modules/model-advisor.md +100 -0
- package/skills/leonardo/modules/prompt-library.md +110 -0
- package/skills/leonardo/modules/style-guide.md +186 -0
- package/skills/leonardo/modules/video-prompter.md +311 -0
- package/skills/synabun/SKILL.md +185 -0
- package/skills/synabun/icon.png +0 -0
- package/skills/synabun/modules/audit.md +374 -0
- package/skills/synabun/modules/changelog.md +194 -0
- package/skills/synabun/modules/idea.md +144 -0
- package/skills/synabun/modules/memorize.md +188 -0
- 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 → 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
|
+
}
|