From 1b818a114554655e9541aad9f41822e903981dfa Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 20 Jul 2026 01:55:31 +0200 Subject: [PATCH 1/2] feat(onboarding): add first-run spotlight tour Walks new users through the new-tab button, terminal, file preview panel, and global search on first launch via a 4-step spotlight overlay. Persists via localStorage (tour-seen), replacing the old static help-card-on-first-run trigger; the help card itself stays reachable from the help button. Replayable anytime from Settings. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01JUpVoQXhYinHSuVCMQMj8o --- index.html | 4 ++ src/main.ts | 116 +++++++++++++++++++++++++++++++++++++++-- src/styles.css | 81 +++++++++++++++++++++++++++++ src/tour.ts | 137 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 334 insertions(+), 4 deletions(-) create mode 100644 src/tour.ts diff --git a/index.html b/index.html index 4c770da..ad3c384 100644 --- a/index.html +++ b/index.html @@ -196,6 +196,10 @@

+
+ + +
diff --git a/src/main.ts b/src/main.ts index efc8905..d409759 100644 --- a/src/main.ts +++ b/src/main.ts @@ -26,6 +26,7 @@ import { CommandBlocks } from "./blocks"; import { WebKitDeadKeyAddon } from "./xtermDeadKeyAddon"; import { MODEL_EXTS } from "./modelExts"; import type { ModelPreview } from "./modelPreview"; +import { startTour, type TourStep } from "./tour"; // ── Themes ──────────────────────────────────────────────── @@ -357,6 +358,20 @@ const I18N: Record> = { restoreAsk: "Preguntar", restoreNever: "Nunca", restoreNote: "Reabre tus pestañas (orden, nombres, colores y carpetas). Las pestañas de Claude ofrecen retomar la conversación anterior.", + tourLabel: "Recorrido guiado", + tourReplayBtn: "Ver el recorrido", + tourSkip: "Saltar", + tourNext: "Siguiente", + tourDone: "Listo", + tourStepOf: "Paso {i} de {n}", + tourStep1Title: "Abre una nueva pestaña", + tourStep1Body: "El botón + lanza Claude Code, OpenCode, Codex o PowerShell en la carpeta que elijas.", + tourStep2Title: "Tu terminal", + tourStep2Body: "ConPTY con render por GPU: colores completos y apps interactivas con impacto mínimo en tus FPS.", + tourStep3Title: "Vista previa de archivos", + tourStep3Body: "Al abrir un archivo se muestra aquí: código, imágenes e incluso modelos 3D (.glb, .gltf, .obj, .stl).", + tourStep4Title: "Búsqueda global", + tourStep4Body: "Pulsa Ctrl+K en cualquier momento para saltar a archivos, comandos o sesiones.", restoreBannerText: "¿Restaurar tu última sesión? ({n} pestañas)", restoreBtn: "Restaurar", resumeQuestion: "¿Retomar la conversación anterior?", @@ -484,6 +499,20 @@ const I18N: Record> = { restoreAsk: "Ask", restoreNever: "Never", restoreNote: "Reopens your tabs (order, names, colors and folders). Claude tabs offer to resume the previous conversation.", + tourLabel: "Guided tour", + tourReplayBtn: "Take the tour", + tourSkip: "Skip", + tourNext: "Next", + tourDone: "Done", + tourStepOf: "Step {i} of {n}", + tourStep1Title: "Open a new tab", + tourStep1Body: "The + button launches Claude Code, OpenCode, Codex, or PowerShell in a folder you pick.", + tourStep2Title: "Your terminal", + tourStep2Body: "ConPTY with GPU rendering: full colors and interactive apps with minimal impact on your FPS.", + tourStep3Title: "File preview", + tourStep3Body: "Open a file and it shows up here: code, images, and even 3D models (.glb, .gltf, .obj, .stl).", + tourStep4Title: "Global search", + tourStep4Body: "Press Ctrl+K anytime to jump to files, commands, or sessions.", restoreBannerText: "Restore your last session? ({n} tabs)", restoreBtn: "Restore", resumeQuestion: "Resume previous conversation?", @@ -611,6 +640,20 @@ const I18N: Record> = { restoreAsk: "Demander", restoreNever: "Jamais", restoreNote: "Rouvre vos onglets (ordre, noms, couleurs et dossiers). Les onglets Claude proposent de reprendre la conversation précédente.", + tourLabel: "Visite guidée", + tourReplayBtn: "Revoir la visite", + tourSkip: "Passer", + tourNext: "Suivant", + tourDone: "Terminé", + tourStepOf: "Étape {i} sur {n}", + tourStep1Title: "Ouvrez un nouvel onglet", + tourStep1Body: "Le bouton + lance Claude Code, OpenCode, Codex ou PowerShell dans le dossier de votre choix.", + tourStep2Title: "Votre terminal", + tourStep2Body: "ConPTY avec rendu GPU : couleurs complètes et applications interactives, impact minimal sur vos FPS.", + tourStep3Title: "Aperçu des fichiers", + tourStep3Body: "Ouvrez un fichier et il s'affiche ici : code, images et même modèles 3D (.glb, .gltf, .obj, .stl).", + tourStep4Title: "Recherche globale", + tourStep4Body: "Appuyez sur Ctrl+K à tout moment pour accéder aux fichiers, commandes ou sessions.", restoreBannerText: "Restaurer votre dernière session ? ({n} onglets)", restoreBtn: "Restaurer", resumeQuestion: "Reprendre la conversation précédente ?", @@ -738,6 +781,20 @@ const I18N: Record> = { restoreAsk: "Chiedi", restoreNever: "Mai", restoreNote: "Riapre le tue schede (ordine, nomi, colori e cartelle). Le schede Claude offrono di riprendere la conversazione precedente.", + tourLabel: "Visita guidata", + tourReplayBtn: "Rivedi la visita", + tourSkip: "Salta", + tourNext: "Avanti", + tourDone: "Fine", + tourStepOf: "Passo {i} di {n}", + tourStep1Title: "Apri una nuova scheda", + tourStep1Body: "Il pulsante + avvia Claude Code, OpenCode, Codex o PowerShell nella cartella che scegli.", + tourStep2Title: "Il tuo terminale", + tourStep2Body: "ConPTY con rendering GPU: colori completi e app interattive con impatto minimo sugli FPS.", + tourStep3Title: "Anteprima file", + tourStep3Body: "Apri un file e viene mostrato qui: codice, immagini e persino modelli 3D (.glb, .gltf, .obj, .stl).", + tourStep4Title: "Ricerca globale", + tourStep4Body: "Premi Ctrl+K in qualsiasi momento per saltare a file, comandi o sessioni.", restoreBannerText: "Ripristinare l'ultima sessione? ({n} schede)", restoreBtn: "Ripristina", resumeQuestion: "Riprendere la conversazione precedente?", @@ -2597,6 +2654,55 @@ wireSwitch("#chk-hooks", "hooks"); wireSwitch("#chk-matchmode", "matchMode"); wireSwitch("#chk-aisearch", "aiSearch"); +// ── Onboarding tour ─────────────────────────────────────── + +function buildTourSteps(): TourStep[] { + const previewModal = $("#file-preview-modal"); + const wasOpen = previewModal.classList.contains("open"); + return [ + { + selector: "#btn-new-tab", + title: t("tourStep1Title"), + body: t("tourStep1Body"), + }, + { + selector: "#terminals", + title: t("tourStep2Title"), + body: t("tourStep2Body"), + }, + { + selector: "#file-preview-modal", + title: t("tourStep3Title"), + body: t("tourStep3Body"), + onEnter: () => previewModal.classList.add("open"), + onLeave: () => { + if (!wasOpen) previewModal.classList.remove("open"); + }, + }, + { + selector: "#btn-global-search", + title: t("tourStep4Title"), + body: t("tourStep4Body"), + }, + ]; +} + +function tourLabels() { + return { + skip: t("tourSkip"), + next: t("tourNext"), + done: t("tourDone"), + stepOf: (i: number, n: number) => t("tourStepOf").replace("{i}", String(i)).replace("{n}", String(n)), + }; +} + +function replayTour() { + $("#settings-modal").classList.add("hidden"); + startTour(buildTourSteps(), tourLabels(), () => {}); +} + +$("#btn-replay-tour").addEventListener("click", replayTour); + const selRestore = $("#sel-restore"); selRestore.value = settings.restore; selRestore.addEventListener("change", () => { @@ -3594,8 +3700,10 @@ refreshCliButtons().then(() => { } }); -// First run: show the help card once so the user discovers the features. -if (!localStorage.getItem("help-seen")) { - helpModal.classList.remove("hidden"); - localStorage.setItem("help-seen", "1"); +// First run: walk the user through the core UI once. The static help +// card (still reachable any time via the help button) stays available +// as reference material afterward. +if (!localStorage.getItem("tour-seen")) { + localStorage.setItem("tour-seen", "1"); + startTour(buildTourSteps(), tourLabels(), () => {}); } diff --git a/src/styles.css b/src/styles.css index 7b746ac..1b6eb05 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1685,3 +1685,84 @@ kbd { .hidden { display: none !important; } + +/* ── Onboarding tour ───────────────────────────────────── */ + +.btn-replay-tour { + padding: 6px 12px; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 7px; + background: rgba(255, 255, 255, 0.05); + color: var(--text); + font-size: 12px; + cursor: pointer; +} +.btn-replay-tour:hover { + border-color: var(--accent); + color: var(--accent); +} + +.tour-overlay { + position: fixed; + inset: 0; + z-index: 300; + animation: fade-in 0.15s ease; +} + +.tour-spot { + position: fixed; + border-radius: 8px; + border: 2px solid var(--accent); + box-shadow: + 0 0 0 9999px rgba(10, 11, 15, 0.72), + 0 0 12px 1px var(--accent-glow); + transition: + left 0.25s ease, + top 0.25s ease, + width 0.25s ease, + height 0.25s ease; + pointer-events: none; +} + +.tour-tip { + position: fixed; + width: min(320px, 90vw); + padding: 14px 16px 12px; + background: rgb(var(--bar-rgb)); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45); + transition: + left 0.25s ease, + top 0.25s ease; +} + +.tour-tip-step { + font-size: 11px; + color: var(--text-dim); + margin-bottom: 2px; +} + +.tour-tip-title { + margin: 0 0 6px; + font-size: 14px; + color: var(--text-bright); +} + +.tour-tip-body { + margin: 0 0 12px; + font-size: 12.5px; + line-height: 1.5; + color: var(--text); +} + +.tour-tip-actions { + display: flex; + justify-content: flex-end; + gap: 8px; +} + +.tour-tip-actions .ask-btn.ok { + border-color: var(--accent); + color: var(--accent); +} diff --git a/src/tour.ts b/src/tour.ts new file mode 100644 index 0000000..fdc59d4 --- /dev/null +++ b/src/tour.ts @@ -0,0 +1,137 @@ +// First-run onboarding tour: a spotlight overlay that walks through a +// fixed list of real UI elements. Kept framework-free and DOM-driven to +// match the rest of the app. + +export interface TourStep { + selector: string; + title: string; + body: string; + onEnter?: () => void; + onLeave?: () => void; +} + +export interface TourLabels { + skip: string; + next: string; + done: string; + stepOf: (i: number, n: number) => string; +} + +let active = false; + +export function isTourActive(): boolean { + return active; +} + +export function startTour( + steps: TourStep[], + labels: TourLabels, + onEnd: (skipped: boolean) => void, +): void { + if (active || !steps.length) return; + active = true; + + const overlay = document.createElement("div"); + overlay.className = "tour-overlay"; + overlay.innerHTML = ` +
+
+
+

+

+
+ + +
+
+ `; + document.body.appendChild(overlay); + + const spot = overlay.querySelector(".tour-spot")!; + const tip = overlay.querySelector(".tour-tip")!; + const stepEl = overlay.querySelector(".tour-tip-step")!; + const titleEl = overlay.querySelector(".tour-tip-title")!; + const bodyEl = overlay.querySelector(".tour-tip-body")!; + const skipBtn = overlay.querySelector(".tour-skip")!; + const nextBtn = overlay.querySelector(".tour-next")!; + + skipBtn.textContent = labels.skip; + + let index = 0; + + function place() { + const step = steps[index]; + const target = document.querySelector(step.selector); + if (!target) { + advance(); + return; + } + const r = target.getBoundingClientRect(); + const pad = 6; + spot.style.left = `${r.left - pad}px`; + spot.style.top = `${r.top - pad}px`; + spot.style.width = `${r.width + pad * 2}px`; + spot.style.height = `${r.height + pad * 2}px`; + + stepEl.textContent = labels.stepOf(index + 1, steps.length); + titleEl.textContent = step.title; + bodyEl.textContent = step.body; + nextBtn.textContent = index === steps.length - 1 ? labels.done : labels.next; + + // Position the tip below the target, flipping above if it wouldn't + // fit, and clamping horizontally so it never spills off-screen. + const tipRect = tip.getBoundingClientRect(); + const margin = 12; + let top = r.bottom + margin; + if (top + tipRect.height > window.innerHeight) { + top = Math.max(margin, r.top - tipRect.height - margin); + } + let left = r.left; + left = Math.min(left, window.innerWidth - tipRect.width - margin); + left = Math.max(margin, left); + tip.style.top = `${top}px`; + tip.style.left = `${left}px`; + } + + function enterStep() { + steps[index].onEnter?.(); + // Let onEnter's DOM changes (e.g. opening a panel) settle before + // measuring positions off it. + requestAnimationFrame(place); + } + + function leaveStep() { + steps[index].onLeave?.(); + } + + function advance() { + leaveStep(); + index++; + if (index >= steps.length) { + finish(false); + return; + } + enterStep(); + } + + function finish(skipped: boolean) { + if (!active) return; + if (index < steps.length) leaveStep(); + active = false; + window.removeEventListener("resize", place); + document.removeEventListener("keydown", onKeydown); + overlay.remove(); + onEnd(skipped); + } + + function onKeydown(e: KeyboardEvent) { + if (e.key === "Escape") finish(true); + } + + nextBtn.addEventListener("click", advance); + skipBtn.addEventListener("click", () => finish(true)); + window.addEventListener("resize", place); + document.addEventListener("keydown", onKeydown); + + enterStep(); +} From 3064c142de5169360cc6077cb34bab6b6304d4cd Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 20 Jul 2026 01:59:29 +0200 Subject: [PATCH 2/2] chore: bump version to 0.8.14 Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01JUpVoQXhYinHSuVCMQMj8o --- package-lock.json | 4 ++-- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index b868807..f8e797b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "afkode", - "version": "0.8.12", + "version": "0.8.14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "afkode", - "version": "0.8.12", + "version": "0.8.14", "dependencies": { "@tauri-apps/api": "^2.11.1", "@tauri-apps/plugin-clipboard-manager": "^2.3.2", diff --git a/package.json b/package.json index 2696fe5..8a7ae50 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "afkode", "private": true, - "version": "0.8.13", + "version": "0.8.14", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index feb0f53..a321a40 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "afkode" -version = "0.8.13" +version = "0.8.14" dependencies = [ "base64 0.22.1", "core-foundation", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 707ff0c..d743253 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "afkode" -version = "0.8.13" +version = "0.8.14" description = "In-game overlay to supervise AI coding agents while you play" authors = ["Omar Hernandez"] license = "MIT" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 5b9888f..39f7fea 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "AFKode", - "version": "0.8.13", + "version": "0.8.14", "identifier": "app.afkode.overlay", "build": { "beforeDevCommand": "npm run dev",