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