-
Notifications
You must be signed in to change notification settings - Fork 38
feat: Add onboarding modal with explainer video (#145) #146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
df17007
b3de4bc
47b37db
cdab9e6
c23caf9
44f807a
3e396ca
fd1de79
7b1d885
23a804f
ba24c7c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| node_modules/ | ||
| *.png | ||
| !website/public/logo.png | ||
| !website/public/icon.png | ||
| .playwright-mcp/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| # Onboarding Modal Design | ||
|
|
||
| **Issue:** #145 | ||
| **Date:** 2026-03-08 | ||
|
|
||
| ## Overview | ||
|
|
||
| Modal dialog explaining Semantic Anchors to first-time visitors. Combines logo, explainer video (YouTube Shorts), and concise text. | ||
|
|
||
| ## Components | ||
|
|
||
| ### 1. `onboarding-modal.js` | ||
|
|
||
| New component (separate from anchor-modal.js): | ||
| - `createOnboardingModal()`: Singleton DOM creation | ||
| - `showOnboarding()`: Opens modal, sets `overflow: hidden` | ||
| - `closeOnboarding()`: Closes, saves `localStorage.setItem('onboarding-seen', 'true')` | ||
| - `shouldShowOnboarding()`: Checks `localStorage.getItem('onboarding-seen')` | ||
|
|
||
| ### 2. Layout | ||
|
|
||
| **Desktop:** Logo top, slogan highlighted, video (YouTube embed) left + text right, CTA button bottom. | ||
|
|
||
| **Mobile:** Logo top, slogan highlighted, text, YouTube link (no embed), CTA button bottom. | ||
|
|
||
| ### 3. Header Change | ||
|
|
||
| Info icon (i) button next to site title. Calls `showOnboarding()`. | ||
|
|
||
| ### 4. i18n Keys | ||
|
|
||
| New keys in en.json/de.json: `onboarding.slogan1`, `onboarding.slogan2`, `onboarding.text1`-`text4`, `onboarding.cta`, `onboarding.watchVideo`, `onboarding.infoButton` | ||
|
|
||
| ### 5. Videos | ||
|
|
||
| - EN: https://youtube.com/shorts/Fb7t45E8_HE | ||
| - DE: https://youtube.com/shorts/cp-qqiHU-MA | ||
|
|
||
| ### 6. Text Content | ||
|
|
||
| **Slogan:** | ||
| - EN: "Semantic Anchors. One word, and the AI gets the rest." | ||
| - DE: "Semantic Anchors. Ein Wort, und die KI versteht den Rest." | ||
|
|
||
| **Body (4 paragraphs, condensed from video scripts):** | ||
|
|
||
| EN: | ||
| 1. Imagine saying just one word - and your counterpart instantly understands an entire concept. | ||
| 2. A Semantic Anchor is an established term that activates an entire body of knowledge. Like an anchor holding a ship in place - a Semantic Anchor pins your conversation to a precise concept. | ||
| 3. This works because AI models were trained on millions of texts. Terms like MECE, Clean Architecture, or the Feynman Technique instantly trigger deep contextual knowledge. | ||
| 4. Instead of writing long prompts, just use the right anchor - and the AI delivers. | ||
|
|
||
| DE: | ||
| 1. Stell dir vor, du sagst ein einziges Wort - und dein Gegenüber versteht sofort ein ganzes Konzept. | ||
| 2. Ein Semantic Anchor ist ein etablierter Begriff, der ein ganzes Wissensgebiet aktiviert. Wie ein Anker, der ein Schiff an einem festen Punkt hält - so verankert ein Semantic Anchor dein Gespräch an einem präzisen Konzept. | ||
| 3. Das funktioniert, weil KI-Modelle auf Millionen von Texten trainiert wurden. Begriffe wie MECE, Clean Architecture oder Feynman-Technik lösen sofort tiefes Kontextwissen aus. | ||
| 4. Statt lange Prompts zu schreiben, sagst du einfach den richtigen Anker - und die KI liefert. | ||
|
|
||
| ### 7. Accessibility | ||
|
|
||
| - Focus trap in modal | ||
| - ESC to close | ||
| - `role="dialog"`, `aria-modal="true"` | ||
|
|
||
| ### 8. Not Included (YAGNI) | ||
|
|
||
| - No open/close animation | ||
| - No "don't show again" checkbox | ||
| - No analytics | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| #!/usr/bin/env node | ||
| /** | ||
| * Sync anchor .adoc files from docs/anchors/ to website/public/docs/anchors/ | ||
| * | ||
| * Ensures the website always has the latest anchor files available for | ||
| * client-side rendering in the anchor modal. Runs as a pre-step for | ||
| * both dev and build. | ||
| * | ||
| * Usage: node scripts/sync-anchors.js | ||
| */ | ||
|
|
||
| const fs = require('fs') | ||
| const path = require('path') | ||
|
|
||
| const ROOT = path.join(__dirname, '..') | ||
| const SRC = path.join(ROOT, 'docs', 'anchors') | ||
| const DEST = path.join(ROOT, 'website', 'public', 'docs', 'anchors') | ||
|
|
||
| function sync() { | ||
| if (!fs.existsSync(SRC)) { | ||
| console.warn(`[sync-anchors] Source directory not found: ${SRC}`) | ||
| return | ||
| } | ||
|
|
||
| fs.mkdirSync(DEST, { recursive: true }) | ||
|
|
||
| const srcFiles = fs.readdirSync(SRC).filter((f) => f.endsWith('.adoc')) | ||
| let copied = 0 | ||
| let skipped = 0 | ||
|
|
||
| for (const file of srcFiles) { | ||
| const srcPath = path.join(SRC, file) | ||
| const destPath = path.join(DEST, file) | ||
|
|
||
| const srcStat = fs.statSync(srcPath) | ||
|
|
||
| if (fs.existsSync(destPath)) { | ||
| const destStat = fs.statSync(destPath) | ||
| if (srcStat.mtimeMs <= destStat.mtimeMs) { | ||
| skipped++ | ||
| continue | ||
| } | ||
| } | ||
|
|
||
| fs.copyFileSync(srcPath, destPath) | ||
| copied++ | ||
| } | ||
|
Comment on lines
+27
to
+47
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Das ist noch kein vollständiger Sync. Wenn ein Anchor in 🧹 Mögliche Ergänzung const srcFiles = fs.readdirSync(SRC).filter((f) => f.endsWith('.adoc'))
+ const destFiles = fs.readdirSync(DEST).filter((f) => f.endsWith('.adoc'))
let copied = 0
let skipped = 0
+
+ for (const file of destFiles) {
+ if (!srcFiles.includes(file)) {
+ fs.unlinkSync(path.join(DEST, file))
+ }
+ }
for (const file of srcFiles) {🤖 Prompt for AI Agents |
||
|
|
||
| console.log(`[sync-anchors] ${copied} copied, ${skipped} up-to-date (${srcFiles.length} total)`) | ||
| } | ||
|
|
||
| sync() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,51 +5,132 @@ export function renderHeader() { | |
|
|
||
| return ` | ||
| <header class="border-b border-[var(--color-border)] bg-[var(--color-bg)] transition-colors duration-300"> | ||
| <nav class="mx-auto max-w-7xl px-4 py-3 sm:px-6 lg:px-8"> | ||
| <div class="flex items-center justify-between"> | ||
| <div class="flex items-center gap-6"> | ||
| <h1 class="text-xl font-bold text-[var(--color-text)]"> | ||
| <a href="#/" class="no-underline text-inherit hover:text-[var(--color-primary)] transition-colors flex items-center gap-2" data-i18n="app.title"> | ||
| <img src="${import.meta.env.BASE_URL}icon.png" alt="" class="h-8 w-8" aria-hidden="true" /> | ||
| Semantic Anchors | ||
| </a> | ||
| </h1> | ||
| <div class="hidden sm:flex items-center gap-4 text-sm"> | ||
| <a href="#/" class="nav-link text-[var(--color-text-secondary)] hover:text-[var(--color-text)] transition-colors" data-route="/" data-i18n="nav.catalog">${i18n.t('nav.catalog')}</a> | ||
| <a href="#/about" class="nav-link text-[var(--color-text-secondary)] hover:text-[var(--color-text)] transition-colors" data-route="/about" data-i18n="nav.about">${i18n.t('nav.about')}</a> | ||
| <a href="#/contributing" class="nav-link text-[var(--color-text-secondary)] hover:text-[var(--color-text)] transition-colors" data-route="/contributing" data-i18n="nav.contributing">${i18n.t('nav.contributing')}</a> | ||
| </div> | ||
| </div> | ||
| <div class="flex items-center gap-3"> | ||
| <button | ||
| id="lang-toggle" | ||
| class="rounded-md px-2 py-1 text-sm font-medium text-[var(--color-text-secondary)] hover:text-[var(--color-text)] hover:bg-[var(--color-bg-secondary)] transition-colors" | ||
| aria-label="Toggle language" | ||
| >${langLabel}</button> | ||
| <button | ||
| id="theme-toggle" | ||
| class="rounded-md p-2 text-[var(--color-text-secondary)] hover:text-[var(--color-text)] hover:bg-[var(--color-bg-secondary)] transition-colors" | ||
| data-i18n-aria="header.themeToggle.dark" | ||
| aria-label="Switch to dark mode" | ||
| > | ||
| <svg id="theme-icon-moon" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" /> | ||
| </svg> | ||
| <svg id="theme-icon-sun" class="h-5 w-5 hidden" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" /> | ||
| </svg> | ||
| </button> | ||
| <nav class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-3"> | ||
| <!-- Desktop: Logo spanning two rows, right side has nav + search --> | ||
| <div class="hidden sm:flex items-stretch gap-6"> | ||
| <!-- Logo left, spanning both rows --> | ||
| <div class="flex items-center"> | ||
| <a href="#/" class="no-underline flex flex-col items-start"> | ||
| <img src="${import.meta.env.BASE_URL}logo.png" alt="Semantic Anchors" class="max-h-24" /> | ||
| <span class="text-xs text-[var(--color-text-secondary)] leading-tight" data-i18n="header.slogan">${i18n.t('header.slogan')}</span> | ||
| </a> | ||
| <button | ||
| id="mobile-menu-toggle" | ||
| class="sm:hidden rounded-md p-2 text-[var(--color-text-secondary)] hover:text-[var(--color-text)] hover:bg-[var(--color-bg-secondary)] transition-colors" | ||
| aria-label="Toggle menu" | ||
| aria-expanded="false" | ||
| id="onboarding-info-btn" | ||
| class="self-start mt-1 rounded-full p-1 text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] hover:bg-[var(--color-bg-secondary)] transition-colors" | ||
| data-i18n-aria="onboarding.infoButton" | ||
| data-i18n-title="onboarding.infoButton" | ||
| aria-label="${i18n.t('onboarding.infoButton')}" | ||
| title="${i18n.t('onboarding.infoButton')}" | ||
| > | ||
| <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" /> | ||
| <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> | ||
| <path stroke-linecap="round" stroke-linejoin="round" d="M15.91 11.672a.375.375 0 010 .656l-5.603 3.113a.375.375 0 01-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112z" /> | ||
| </svg> | ||
| </button> | ||
|
Comment on lines
17
to
29
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Desktop-Trigger weicht vom geforderten Info-Icon ab. Das Control neben dem Logo zeigt hier ein Play-Symbol, obwohl Also applies to: 92-103 🤖 Prompt for AI Agents |
||
| </div> | ||
|
|
||
| <!-- Right side: two rows --> | ||
| <div class="flex-1 flex flex-col justify-center gap-2"> | ||
| <!-- Row 1: Navigation + Language/Theme --> | ||
| <div class="flex items-center justify-between"> | ||
| <div class="flex items-center gap-6 text-2xl"> | ||
| <a href="#/" class="nav-link text-[var(--color-text-secondary)] hover:text-[var(--color-text)] transition-colors" data-route="/" data-i18n="nav.catalog">${i18n.t('nav.catalog')}</a> | ||
| <a href="#/about" class="nav-link text-[var(--color-text-secondary)] hover:text-[var(--color-text)] transition-colors" data-route="/about" data-i18n="nav.about">${i18n.t('nav.about')}</a> | ||
| <a href="#/contributing" class="nav-link text-[var(--color-text-secondary)] hover:text-[var(--color-text)] transition-colors" data-route="/contributing" data-i18n="nav.contributing">${i18n.t('nav.contributing')}</a> | ||
| </div> | ||
| <div class="flex items-center gap-3"> | ||
| <button | ||
| id="lang-toggle" | ||
| class="rounded-md px-2 py-1 text-sm font-medium text-[var(--color-text-secondary)] hover:text-[var(--color-text)] hover:bg-[var(--color-bg-secondary)] transition-colors" | ||
| aria-label="Toggle language" | ||
| >${langLabel}</button> | ||
| <button | ||
| id="theme-toggle" | ||
| class="rounded-md p-2 text-[var(--color-text-secondary)] hover:text-[var(--color-text)] hover:bg-[var(--color-bg-secondary)] transition-colors" | ||
| data-i18n-aria="header.themeToggle.dark" | ||
| aria-label="Switch to dark mode" | ||
| > | ||
| <svg id="theme-icon-moon" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" /> | ||
| </svg> | ||
| <svg id="theme-icon-sun" class="h-5 w-5 hidden" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" /> | ||
| </svg> | ||
| </button> | ||
| </div> | ||
| </div> | ||
| <!-- Row 2: Search + Role filter + Anchor count --> | ||
| <div class="flex items-center gap-3"> | ||
| <input | ||
| id="header-search-input" | ||
| type="search" | ||
| data-i18n-placeholder="search.placeholder" | ||
| placeholder="${i18n.t('search.placeholder')}" | ||
| class="w-48 lg:w-64 rounded-lg border-2 border-[var(--color-border)] bg-[var(--color-bg)] px-4 py-2 text-base text-[var(--color-text)] placeholder-[var(--color-text-secondary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)] transition-colors duration-300" | ||
| /> | ||
| <select | ||
| id="header-role-filter" | ||
| class="rounded-lg border-2 border-[var(--color-border)] bg-[var(--color-bg)] px-4 py-2 text-base text-[var(--color-text)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)] transition-colors duration-300" | ||
| > | ||
| <option value="" data-i18n="filter.allRoles">${i18n.t('filter.allRoles')}</option> | ||
| </select> | ||
| <span id="anchor-count" class="text-sm text-[var(--color-text-secondary)] ml-auto"> | ||
| <span id="visible-count">0</span> / <span id="total-count">0</span> <span data-i18n="filter.anchors">${i18n.t('filter.anchors')}</span> | ||
| </span> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <!-- Mobile: stacked layout --> | ||
| <div class="sm:hidden"> | ||
| <div class="flex items-center justify-between"> | ||
| <a href="#/" class="no-underline flex flex-col items-start"> | ||
| <img src="${import.meta.env.BASE_URL}logo.png" alt="Semantic Anchors" class="max-h-16" /> | ||
| <span class="text-xs text-[var(--color-text-secondary)] leading-tight" data-i18n="header.slogan">${i18n.t('header.slogan')}</span> | ||
| </a> | ||
| <div class="flex items-center gap-3"> | ||
| <button | ||
| id="onboarding-info-btn-mobile" | ||
| class="rounded-full p-1 text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] hover:bg-[var(--color-bg-secondary)] transition-colors" | ||
| data-i18n-aria="onboarding.infoButton" | ||
| data-i18n-title="onboarding.infoButton" | ||
| aria-label="${i18n.t('onboarding.infoButton')}" | ||
| title="${i18n.t('onboarding.infoButton')}" | ||
| > | ||
| <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" /> | ||
| </svg> | ||
| </button> | ||
| <button | ||
| id="lang-toggle-mobile" | ||
| class="rounded-md px-2 py-1 text-sm font-medium text-[var(--color-text-secondary)] hover:text-[var(--color-text)] hover:bg-[var(--color-bg-secondary)] transition-colors" | ||
| aria-label="Toggle language" | ||
| >${langLabel}</button> | ||
| <button | ||
| id="theme-toggle-mobile" | ||
| class="rounded-md p-2 text-[var(--color-text-secondary)] hover:text-[var(--color-text)] hover:bg-[var(--color-bg-secondary)] transition-colors" | ||
| data-i18n-aria="header.themeToggle.dark" | ||
| aria-label="Switch to dark mode" | ||
| > | ||
| <svg id="theme-icon-moon-mobile" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" /> | ||
| </svg> | ||
| <svg id="theme-icon-sun-mobile" class="h-5 w-5 hidden" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" /> | ||
| </svg> | ||
| </button> | ||
| <button | ||
| id="mobile-menu-toggle" | ||
| class="rounded-md p-2 text-[var(--color-text-secondary)] hover:text-[var(--color-text)] hover:bg-[var(--color-bg-secondary)] transition-colors" | ||
| aria-label="Toggle menu" | ||
| aria-expanded="false" | ||
| > | ||
| <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" /> | ||
| </svg> | ||
| </button> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <!-- Mobile menu --> | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.