1/** 2 * tutorial.js â Interactive step-by-step tutorial delivered by the floating 3 * digital assistant. 4 * 5 * Usage: 6 * import { initTutorial, startTutorial } from "./tutorial.js"; 7 * initTutorial(); // call once on app load (auto-starts for new users) 8 * startTutorial(); // call to manually re-trigger 9 */ 10 11import { openStreamManagerHelp } from "./streams/helpPopup.js"; 12 13const TUTORIAL_KEY = "zimTutorialDone"; 14 15/** Z-index layers: overlay (99990) â spotlight (99992) â card (99995) */ 16const Z_SPOTLIGHT = 99992; 17const MAX_SPEECH_LEN = 90; 18const SPEECH_TRUNCATE_AT = 87; 19/** ms to wait after app load before auto-starting (lets the assistant greet first) */ 20const TUTORIAL_AUTO_START_DELAY = 5000; 21 22// âââ Tutorial steps ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 23const STEPS = [ 24 { 25 title: "Welcome to ZimxDesigner! ð", 26 text: "Hi! I'm your digital assistant. Let me give you a quick tour so you can start building amazing UIs right away!", 27 target: null, 28 topic: "intro", 29 }, 30 { 31 title: "The Canvas ð¼ï¸", 32 text: "This is your canvas â the main workspace. Drag and drop components here to build your design!", 33 target: "#canvas", 34 position: "center", 35 topic: "canvas", 36 }, 37 { 38 title: "App Tray ð ï¸", 39 text: "The App Tray at the bottom gives you quick access to all your tools: Designer, Whiteboard, Task Board, Mindmap, Calendar, and more!", 40 target: "#appTray", 41 position: "top", 42 topic: "apptray", 43 }, 44 { 45 title: "Designer Palette ð¨", 46 text: "Click the Designer button to open the component palette. Drag any component from the palette onto the canvas to add it to your design!", 47 target: '[data-app-id="designer"]', 48 position: "top", 49 topic: "designer", 50 }, 51 { 52 title: "Radial Menu ð", 53 text: "Right-click (or long-press) anywhere on the canvas to open the Radial Menu â a fast way to create containers, charts, controls and more!", 54 target: null, 55 position: "center", 56 topic: "radial", 57 }, 58 { 59 title: "App Viewer ð", 60 text: "Click App Viewer in the App Tray to preview your production build inside the studio! Set the Project & Layout names, then click â¶ Preview. Switch between Desktop, Tablet, and Mobile presets to test responsiveness.", 61 target: '[data-app-id="appviewer"]', 62 position: "top", 63 topic: "appviewer", 64 }, 65 { 66 title: "Save & Load Your Work ð¾", 67 text: "Click the Repo button to save or load your project. Your work is stored as an NDJSON file so you can come back to it anytime.", 68 target: ".app-tray-repo", 69 position: "top", 70 topic: "save", 71 }, 72 { 73 title: "Animation Player ð¬", 74 text: "Add an Animation Player from the Designer palette to bring motion to your designs. It plays storyboard animations right on the canvas!", 75 target: '[data-app-id="designer"]', 76 position: "top", 77 topic: "animation", 78 }, 79 { 80 title: "Create & Edit Animations âï¸", 81 text: "Click the â Edit button on an Animation Player to open the Storyboard Editor. Draw shapes, set keyframes, and build frame-by-frame animations with full timeline control.", 82 target: null, 83 position: "center", 84 topic: "animation", 85 }, 86 { 87 title: "Reload Your Animation âº", 88 text: "After editing, close the Storyboard Editor and the player reloads automatically. You can also hit the ⺠Reload button at any time to refresh the animation from its saved asset.", 89 target: null, 90 position: "center", 91 topic: "animation", 92 }, 93 { 94 title: "You're All Set! ð", 95 text: "That's the tour! Click on me anytime for tips and helpful hints. Happy designing â I'll be cheering you on! â¨", 96 target: null, 97 topic: "outro", 98 }, 99]; 100 101// âââ TutorialManager ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 102class TutorialManager { 103 constructor() { 104 this._step = 0;
105 this._overlay = null; 106 this._card = null; 107 this._spotEl = null; 108 this._active = false; 109 } 110 111 /** Start (or restart) the tutorial from step 0. */ 112 start() { 113 this._teardown(); 114 this._step = 0; 115 this._active = true; 116 this._buildDOM(); 117 this._showStep(0); 118 } 119 120 /** Start tutorial from a specific topic */ 121 startFromTopic(topic) { 122 if (topic === "all") { 123 this.start(); 124 return; 125 } 126 127 // Handle special documentation topics (not part of the tutorial flow) 128 if (topic === "database") { 129 // Open database tutorial documentation in a new window 130 window.open("/docs/zim/how-to-add-data-tutorial.md", "_blank"); 131 return; 132 } 133 134 if (topic === "streams") { 135 // Open Stream Manager help popup 136 openStreamManagerHelp(); 137 return; 138 } 139 140 // Find the step index for the topic 141 const stepIndex = STEPS.findIndex(step => step.topic === topic); 142 if (stepIndex === -1) { 143 this.start(); // fallback to full tutorial 144 return; 145 } 146 147 this._teardown(); 148 this._step = stepIndex; 149 this._active = true; 150 this._buildDOM(); 151 this._showStep(stepIndex); 152 } 153 154 /** Close and mark completed. */ 155 stop() { 156 this._teardown(); 157 this._active = false; 158 // Reset assistant tutorial mode 159 const assistant = window.zimAssistant; 160 if (assistant) { 161 assistant.setTutorialMode(false); 162 } 163 try { localStorage.setItem(TUTORIAL_KEY, "1"); } catch { /* silent */ } 164 } 165 166 // ââ DOM ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 167 168 _buildDOM() { 169 // Semi-transparent dim overlay 170 this._overlay = document.createElement("div"); 171 this._overlay.id = "tutorialOverlay"; 172 document.body.appendChild(this._overlay); 173 174 // Tutorial card 175 this._card = document.createElement("div"); 176 this._card.id = "tutorialCard"; 177 document.body.appendChild(this._card); 178 } 179 180 _teardown() { 181 this._clearSpotlight(); 182 this._overlay?.remove(); 183 this._card?.remove(); 184 this._overlay = null; 185 this._card = null; 186 } 187 188 // ââ Spotlight ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 189 190 _applySpotlight(el) { 191 this._clearSpotlight(); 192 if (!el) return; 193 this._spotEl = el; 194 el.classList.add("tutorial-spotlight"); 195 // Boost z-index so element appears above the dim overlay 196 el._prevZIndex = el.style.zIndex; 197 el.style.setProperty("z-index", String(Z_SPOTLIGHT), "important"); 198 } 199 200 _clearSpotlight() { 201 if (!this._spotEl) return; 202 this._spotEl.classList.remove("tutorial-spotlight"); 203 this._spotEl.style.zIndex = this._spotEl._prevZIndex || ""; 204 delete this._spotEl._prevZIndex; 205 this._spotEl = null; 206 } 207 208 // ââ Render step ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 209 210 _showStep(idx) { 211 const step = STEPS[idx]; 212 if (!step) return; 213 214 const total = STEPS.length; 215 const isFirst = idx === 0; 216 const isLast = idx === total - 1; 217 218 // Spotlight target 219 const targetEl = step.target ? document.querySelector(step.target) : null; 220 this._applySpotlight(targetEl); 221 222 // Progress dots 223 const dots = STEPS.map((_, i) => 224 `<span class="tutorial-dot${i === idx ? " active" : ""}"></span>` 225 ).join(""); 226 227 // Card markup 228 this._card.innerHTML = ` 229 <div class="tutorial-card-header">${step.title}</div> 230 <div class="tutorial-card-body">${step.text}</div> 231 <div class="tutorial-card-footer"> 232 <div class="tutorial-dots">${dots}</div> 233 <div class="tutorial-btns"> 234 ${!isFirst ? '<button class="tutorial-btn tutorial-btn-prev">â Back</button>' : ""} 235 <button class="tutorial-btn tutorial-btn-skip">Skip</button> 236 <button class="tutorial-btn tutorial-btn-next${isLast ? " primary" : ""}">
237 ${isLast ? "Finish ð" : "Next â"} 238 </button> 239 </div> 240 </div>`; 241 242 // Button listeners 243 this._card.querySelector(".tutorial-btn-next") 244 ?.addEventListener("click", () => isLast ? this._finish() : this._go(idx + 1)); 245 this._card.querySelector(".tutorial-btn-prev") 246 ?.addEventListener("click", () => this._go(idx - 1)); 247 this._card.querySelector(".tutorial-btn-skip") 248 ?.addEventListener("click", () => this.stop()); 249 250 // Have the assistant speak the step text and position with card 251 const assistant = window.zimAssistant; 252 if (assistant) { 253 // Strip emoji for cleaner speech-bubble display 254 const speech = step.text.replace(/[\u{1F300}-\u{1FFFF}]/gu, "").trim(); 255 assistant.say(speech.length > MAX_SPEECH_LEN ? speech.substring(0, SPEECH_TRUNCATE_AT) + "â¦" : speech, 5000); 256 257 // Mark assistant as in tutorial mode 258 assistant.setTutorialMode(true); 259 } 260 261 // Position the card and assistant after DOM update 262 requestAnimationFrame(() => { 263 this._positionCard(targetEl, step.position); 264 this._positionAssistant(targetEl, step.position); 265 }); 266 } 267 268 // ââ Card positioning âââââââââââââââââââââââââââââââââââââââââââââââââââââ 269 270 _positionCard(targetEl, hint = "center") { 271 const card = this._card; 272 const margin = 16; 273 274 if (!targetEl) { 275 card.style.top = "50%"; 276 card.style.left = "50%"; 277 card.style.transform = "translate(-50%, -50%)"; 278 return; 279 } 280 281 card.style.transform = ""; 282 const rect = targetEl.getBoundingClientRect(); 283 const cardW = card.offsetWidth || 340; 284 const cardH = card.offsetHeight || 200; 285 286 let top, left; 287 288 if (hint === "top") { 289 top = rect.top - cardH - margin; 290 if (top < margin) top = rect.bottom + margin; // flip below if not enough room 291 } else if (hint === "bottom") { 292 top = rect.bottom + margin; 293 } else { 294 top = rect.top + rect.height / 2 - cardH / 2; 295 } 296 297 // Horizontally centre over the target, clamped to viewport 298 left = rect.left + rect.width / 2 - cardW / 2; 299 left = Math.max(margin, Math.min(left, window.innerWidth - cardW - margin)); 300 top = Math.max(margin, Math.min(top, window.innerHeight - cardH - margin)); 301 302 card.style.top = `${Math.round(top)}px`; 303 card.style.left = `${Math.round(left)}px`; 304 } 305 306 // ââ Assistant positioning ââââââââââââââââââââââââââââââââââââââââââââââââ 307 308 _positionAssistant(targetEl, hint = "center") { 309 const assistant = window.zimAssistant; 310 if (!assistant || !assistant.el) return; 311 312 const margin = 20; 313 const assistantEl = assistant.el; 314 315 if (!targetEl) { 316 // Center of screen - position assistant to the side of the card 317 assistantEl.style.position = "fixed"; 318 assistantEl.style.bottom = "auto"; 319 assistantEl.style.right = "auto"; 320 assistantEl.style.top = "50%"; 321 assistantEl.style.left = "calc(50% + 200px)"; // To the right of center card 322 assistantEl.style.transform = "translateY(-50%)"; 323 return; 324 } 325 326 const rect = targetEl.getBoundingClientRect(); 327 const assistantW = 120; // approximate width including bubble 328 const assistantH = 120; // approximate height including bubble 329 330 let top, left; 331 332 // Position assistant near the target but not overlapping the card 333 if (hint === "top") { 334 // Target is at top, card is above or below it 335 // Place assistant to the side of the target 336 top = rect.top + rect.height / 2 - assistantH / 2; 337 left = rect.right + margin; 338 339 // If not enough room on right, try left 340 if (left + assistantW > window.innerWidth - margin) { 341 left = rect.left - assistantW - margin; 342 } 343 } else if (hint === "bottom") { 344 top = rect.top + rect.height / 2 - assistantH / 2; 345 left = rect.right + margin; 346 347 if (left + assistantW > window.innerWidth - margin) { 348 left = rect.left - assistantW - margin; 349 } 350 } else { 351 // Center positioning - place to the side 352 top = rect.top + rect.height / 2 - assistantH / 2; 353 left = rect.right + margin; 354 355 if (left + assistantW > window.innerWidth - margin) { 356 left = rect.left - assistantW - margin; 357 } 358 } 359 360 // Clamp to viewport - use larger bottom margin to prevent agent from dropping too low 361 left = Math.max(margin, Math.min(left, window.innerWidth - assistantW - margin)); 362 top = Math.max(margin, Math.min(top, window.innerHeight - assistantH - 100)); 363 364 assistantEl.style.position = "fixed"; 365 assistantEl.style.bottom = "auto"; 366 assistantEl.style.right = "auto"; 367 assistantEl.style.top = `${Math.round(top)}
367px`; 368 assistantEl.style.left = `${Math.round(left)}px`; 369 assistantEl.style.transform = ""; 370 assistantEl.style.transition = "top 0.5s cubic-bezier(0.4, 0, 0.2, 1), left 0.5s cubic-bezier(0.4, 0, 0.2, 1)"; 371 } 372 373 // ââ Navigation âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 374 375 _go(idx) { 376 this._step = idx; 377 this._showStep(idx); 378 } 379 380 _finish() { 381 const assistant = window.zimAssistant; 382 if (assistant) assistant.say("Tutorial complete! Happy building!", 4000); 383 this.stop(); 384 } 385} 386 387// âââ Singleton + exports ââââââââââââââââââââââââââââââââââââââââââââââââââââââ 388let _tutorial = null; 389 390/** 391 * Initialise the tutorial manager. 392 * Auto-starts for first-time visitors (after a short delay to let the 393 * assistant greeting finish first). 394 */ 395export function initTutorial() { 396 if (_tutorial) return _tutorial; 397 _tutorial = new TutorialManager(); 398 399 const done = (() => { try { return localStorage.getItem(TUTORIAL_KEY); } catch { return null; } })(); 400 if (!done) { 401 // Let the assistant's greeting finish before beginning the tour 402 setTimeout(() => _tutorial.start(), TUTORIAL_AUTO_START_DELAY); 403 } 404 405 return _tutorial; 406} 407 408/** Manually (re-)start the tutorial â e.g. from a help button. */ 409export function startTutorial() { 410 initTutorial(); // ensures _tutorial exists 411 _tutorial.start(); 412} 413 414/** Start tutorial from a specific topic */ 415export function startTutorialTopic(topic) { 416 initTutorial(); // ensures _tutorial exists 417 _tutorial.startFromTopic(topic); 418} 419 420// Listen for help topic requests from the assistant 421document.addEventListener("assistant:help-topic", (e) => { 422 const topic = e?.detail?.topic; 423 if (topic) { 424 startTutorialTopic(topic); 425 } 426});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.