Skip to content

Commit 19bf9bf

Browse files
authored
Merge pull request #146 from raifdmueller/feature/onboarding-modal
feat: Add onboarding modal with explainer video (#145)
2 parents f29a0d3 + ba24c7c commit 19bf9bf

16 files changed

Lines changed: 779 additions & 93 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
node_modules/
22
*.png
3+
!website/public/logo.png
34
!website/public/icon.png
45
.playwright-mcp/

docs/anchors/chatham-house-rule.adoc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,10 @@ Historical Context:: Originally created to enable diplomats and government offic
6262
[discrete]
6363
== *Example Invocation*:
6464

65-
----
65+
[quote]
66+
____
6667
This retrospective will be conducted under the Chatham House Rule.
6768
You may share the insights we generate, but don't attribute comments
6869
to specific team members.
69-
----
70+
____
7071
====

docs/anchors/chatham-house-rule.de.adoc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,10 @@ Historischer Kontext:: Ursprünglich geschaffen, um Diplomaten und Regierungsbea
6262
[discrete]
6363
== *Beispielhafte Anwendung*:
6464

65-
----
65+
[quote]
66+
____
6667
Diese Retrospektive wird unter der Chatham House Rule durchgeführt.
6768
Sie dürfen die generierten Erkenntnisse teilen, aber ordnen Sie
6869
Kommentare nicht spezifischen Teammitgliedern zu.
69-
----
70+
____
7071
====
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Onboarding Modal Design
2+
3+
**Issue:** #145
4+
**Date:** 2026-03-08
5+
6+
## Overview
7+
8+
Modal dialog explaining Semantic Anchors to first-time visitors. Combines logo, explainer video (YouTube Shorts), and concise text.
9+
10+
## Components
11+
12+
### 1. `onboarding-modal.js`
13+
14+
New component (separate from anchor-modal.js):
15+
- `createOnboardingModal()`: Singleton DOM creation
16+
- `showOnboarding()`: Opens modal, sets `overflow: hidden`
17+
- `closeOnboarding()`: Closes, saves `localStorage.setItem('onboarding-seen', 'true')`
18+
- `shouldShowOnboarding()`: Checks `localStorage.getItem('onboarding-seen')`
19+
20+
### 2. Layout
21+
22+
**Desktop:** Logo top, slogan highlighted, video (YouTube embed) left + text right, CTA button bottom.
23+
24+
**Mobile:** Logo top, slogan highlighted, text, YouTube link (no embed), CTA button bottom.
25+
26+
### 3. Header Change
27+
28+
Info icon (i) button next to site title. Calls `showOnboarding()`.
29+
30+
### 4. i18n Keys
31+
32+
New keys in en.json/de.json: `onboarding.slogan1`, `onboarding.slogan2`, `onboarding.text1`-`text4`, `onboarding.cta`, `onboarding.watchVideo`, `onboarding.infoButton`
33+
34+
### 5. Videos
35+
36+
- EN: https://youtube.com/shorts/Fb7t45E8_HE
37+
- DE: https://youtube.com/shorts/cp-qqiHU-MA
38+
39+
### 6. Text Content
40+
41+
**Slogan:**
42+
- EN: "Semantic Anchors. One word, and the AI gets the rest."
43+
- DE: "Semantic Anchors. Ein Wort, und die KI versteht den Rest."
44+
45+
**Body (4 paragraphs, condensed from video scripts):**
46+
47+
EN:
48+
1. Imagine saying just one word - and your counterpart instantly understands an entire concept.
49+
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.
50+
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.
51+
4. Instead of writing long prompts, just use the right anchor - and the AI delivers.
52+
53+
DE:
54+
1. Stell dir vor, du sagst ein einziges Wort - und dein Gegenüber versteht sofort ein ganzes Konzept.
55+
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.
56+
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.
57+
4. Statt lange Prompts zu schreiben, sagst du einfach den richtigen Anker - und die KI liefert.
58+
59+
### 7. Accessibility
60+
61+
- Focus trap in modal
62+
- ESC to close
63+
- `role="dialog"`, `aria-modal="true"`
64+
65+
### 8. Not Included (YAGNI)
66+
67+
- No open/close animation
68+
- No "don't show again" checkbox
69+
- No analytics

scripts/sync-anchors.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Sync anchor .adoc files from docs/anchors/ to website/public/docs/anchors/
4+
*
5+
* Ensures the website always has the latest anchor files available for
6+
* client-side rendering in the anchor modal. Runs as a pre-step for
7+
* both dev and build.
8+
*
9+
* Usage: node scripts/sync-anchors.js
10+
*/
11+
12+
const fs = require('fs')
13+
const path = require('path')
14+
15+
const ROOT = path.join(__dirname, '..')
16+
const SRC = path.join(ROOT, 'docs', 'anchors')
17+
const DEST = path.join(ROOT, 'website', 'public', 'docs', 'anchors')
18+
19+
function sync() {
20+
if (!fs.existsSync(SRC)) {
21+
console.warn(`[sync-anchors] Source directory not found: ${SRC}`)
22+
return
23+
}
24+
25+
fs.mkdirSync(DEST, { recursive: true })
26+
27+
const srcFiles = fs.readdirSync(SRC).filter((f) => f.endsWith('.adoc'))
28+
let copied = 0
29+
let skipped = 0
30+
31+
for (const file of srcFiles) {
32+
const srcPath = path.join(SRC, file)
33+
const destPath = path.join(DEST, file)
34+
35+
const srcStat = fs.statSync(srcPath)
36+
37+
if (fs.existsSync(destPath)) {
38+
const destStat = fs.statSync(destPath)
39+
if (srcStat.mtimeMs <= destStat.mtimeMs) {
40+
skipped++
41+
continue
42+
}
43+
}
44+
45+
fs.copyFileSync(srcPath, destPath)
46+
copied++
47+
}
48+
49+
console.log(`[sync-anchors] ${copied} copied, ${skipped} up-to-date (${srcFiles.length} total)`)
50+
}
51+
52+
sync()

website/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
"version": "0.1.0",
55
"type": "module",
66
"scripts": {
7+
"sync-anchors": "node ../scripts/sync-anchors.js",
8+
"predev": "node ../scripts/sync-anchors.js",
79
"dev": "vite",
8-
"prebuild": "node ../scripts/render-docs.js",
10+
"prebuild": "node ../scripts/sync-anchors.js && node ../scripts/render-docs.js",
911
"build": "vite build",
1012
"preview": "vite preview",
1113
"test": "vitest run",

website/public/logo.png

111 KB
Loading

website/src/components/anchor-modal.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export function createModal() {
2727
const modal = document.createElement('div')
2828
modal.id = 'anchor-modal'
2929
modal.className =
30-
'fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50 p-4'
30+
'fixed inset-0 bg-black/30 backdrop-blur-sm hidden items-center justify-center z-50 p-4'
3131
modal.innerHTML = `
3232
<div class="bg-[var(--color-bg)] rounded-xl shadow-2xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col border border-[var(--color-border)]">
3333
<div class="flex items-center justify-between p-6 border-b border-[var(--color-border)]">

website/src/components/card-grid.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -360,9 +360,10 @@ export function applyCardFilters(roleId, searchQuery) {
360360
* Update the anchor counter display
361361
*/
362362
export function updateAnchorCount(visible, total) {
363-
const visibleCountEl = document.getElementById('visible-count')
364-
const totalCountEl = document.getElementById('total-count')
365-
366-
if (visibleCountEl) visibleCountEl.textContent = visible
367-
if (totalCountEl) totalCountEl.textContent = total
363+
document.querySelectorAll('#visible-count, #visible-count-mobile').forEach((el) => {
364+
el.textContent = visible
365+
})
366+
document.querySelectorAll('#total-count, #total-count-mobile').forEach((el) => {
367+
el.textContent = total
368+
})
368369
}

website/src/components/header.js

Lines changed: 121 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,51 +5,132 @@ export function renderHeader() {
55

66
return `
77
<header class="border-b border-[var(--color-border)] bg-[var(--color-bg)] transition-colors duration-300">
8-
<nav class="mx-auto max-w-7xl px-4 py-3 sm:px-6 lg:px-8">
9-
<div class="flex items-center justify-between">
10-
<div class="flex items-center gap-6">
11-
<h1 class="text-xl font-bold text-[var(--color-text)]">
12-
<a href="#/" class="no-underline text-inherit hover:text-[var(--color-primary)] transition-colors flex items-center gap-2" data-i18n="app.title">
13-
<img src="${import.meta.env.BASE_URL}icon.png" alt="" class="h-8 w-8" aria-hidden="true" />
14-
Semantic Anchors
15-
</a>
16-
</h1>
17-
<div class="hidden sm:flex items-center gap-4 text-sm">
18-
<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>
19-
<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>
20-
<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>
21-
</div>
22-
</div>
23-
<div class="flex items-center gap-3">
24-
<button
25-
id="lang-toggle"
26-
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"
27-
aria-label="Toggle language"
28-
>${langLabel}</button>
29-
<button
30-
id="theme-toggle"
31-
class="rounded-md p-2 text-[var(--color-text-secondary)] hover:text-[var(--color-text)] hover:bg-[var(--color-bg-secondary)] transition-colors"
32-
data-i18n-aria="header.themeToggle.dark"
33-
aria-label="Switch to dark mode"
34-
>
35-
<svg id="theme-icon-moon" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
36-
<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" />
37-
</svg>
38-
<svg id="theme-icon-sun" class="h-5 w-5 hidden" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
39-
<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" />
40-
</svg>
41-
</button>
8+
<nav class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-3">
9+
<!-- Desktop: Logo spanning two rows, right side has nav + search -->
10+
<div class="hidden sm:flex items-stretch gap-6">
11+
<!-- Logo left, spanning both rows -->
12+
<div class="flex items-center">
13+
<a href="#/" class="no-underline flex flex-col items-start">
14+
<img src="${import.meta.env.BASE_URL}logo.png" alt="Semantic Anchors" class="max-h-24" />
15+
<span class="text-xs text-[var(--color-text-secondary)] leading-tight" data-i18n="header.slogan">${i18n.t('header.slogan')}</span>
16+
</a>
4217
<button
43-
id="mobile-menu-toggle"
44-
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"
45-
aria-label="Toggle menu"
46-
aria-expanded="false"
18+
id="onboarding-info-btn"
19+
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"
20+
data-i18n-aria="onboarding.infoButton"
21+
data-i18n-title="onboarding.infoButton"
22+
aria-label="${i18n.t('onboarding.infoButton')}"
23+
title="${i18n.t('onboarding.infoButton')}"
4724
>
48-
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
49-
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
25+
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
26+
<path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
27+
<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" />
5028
</svg>
5129
</button>
5230
</div>
31+
32+
<!-- Right side: two rows -->
33+
<div class="flex-1 flex flex-col justify-center gap-2">
34+
<!-- Row 1: Navigation + Language/Theme -->
35+
<div class="flex items-center justify-between">
36+
<div class="flex items-center gap-6 text-2xl">
37+
<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>
38+
<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>
39+
<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>
40+
</div>
41+
<div class="flex items-center gap-3">
42+
<button
43+
id="lang-toggle"
44+
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"
45+
aria-label="Toggle language"
46+
>${langLabel}</button>
47+
<button
48+
id="theme-toggle"
49+
class="rounded-md p-2 text-[var(--color-text-secondary)] hover:text-[var(--color-text)] hover:bg-[var(--color-bg-secondary)] transition-colors"
50+
data-i18n-aria="header.themeToggle.dark"
51+
aria-label="Switch to dark mode"
52+
>
53+
<svg id="theme-icon-moon" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
54+
<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" />
55+
</svg>
56+
<svg id="theme-icon-sun" class="h-5 w-5 hidden" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
57+
<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" />
58+
</svg>
59+
</button>
60+
</div>
61+
</div>
62+
<!-- Row 2: Search + Role filter + Anchor count -->
63+
<div class="flex items-center gap-3">
64+
<input
65+
id="header-search-input"
66+
type="search"
67+
data-i18n-placeholder="search.placeholder"
68+
placeholder="${i18n.t('search.placeholder')}"
69+
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"
70+
/>
71+
<select
72+
id="header-role-filter"
73+
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"
74+
>
75+
<option value="" data-i18n="filter.allRoles">${i18n.t('filter.allRoles')}</option>
76+
</select>
77+
<span id="anchor-count" class="text-sm text-[var(--color-text-secondary)] ml-auto">
78+
<span id="visible-count">0</span> / <span id="total-count">0</span> <span data-i18n="filter.anchors">${i18n.t('filter.anchors')}</span>
79+
</span>
80+
</div>
81+
</div>
82+
</div>
83+
84+
<!-- Mobile: stacked layout -->
85+
<div class="sm:hidden">
86+
<div class="flex items-center justify-between">
87+
<a href="#/" class="no-underline flex flex-col items-start">
88+
<img src="${import.meta.env.BASE_URL}logo.png" alt="Semantic Anchors" class="max-h-16" />
89+
<span class="text-xs text-[var(--color-text-secondary)] leading-tight" data-i18n="header.slogan">${i18n.t('header.slogan')}</span>
90+
</a>
91+
<div class="flex items-center gap-3">
92+
<button
93+
id="onboarding-info-btn-mobile"
94+
class="rounded-full p-1 text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] hover:bg-[var(--color-bg-secondary)] transition-colors"
95+
data-i18n-aria="onboarding.infoButton"
96+
data-i18n-title="onboarding.infoButton"
97+
aria-label="${i18n.t('onboarding.infoButton')}"
98+
title="${i18n.t('onboarding.infoButton')}"
99+
>
100+
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
101+
<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" />
102+
</svg>
103+
</button>
104+
<button
105+
id="lang-toggle-mobile"
106+
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"
107+
aria-label="Toggle language"
108+
>${langLabel}</button>
109+
<button
110+
id="theme-toggle-mobile"
111+
class="rounded-md p-2 text-[var(--color-text-secondary)] hover:text-[var(--color-text)] hover:bg-[var(--color-bg-secondary)] transition-colors"
112+
data-i18n-aria="header.themeToggle.dark"
113+
aria-label="Switch to dark mode"
114+
>
115+
<svg id="theme-icon-moon-mobile" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
116+
<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" />
117+
</svg>
118+
<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">
119+
<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" />
120+
</svg>
121+
</button>
122+
<button
123+
id="mobile-menu-toggle"
124+
class="rounded-md p-2 text-[var(--color-text-secondary)] hover:text-[var(--color-text)] hover:bg-[var(--color-bg-secondary)] transition-colors"
125+
aria-label="Toggle menu"
126+
aria-expanded="false"
127+
>
128+
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
129+
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
130+
</svg>
131+
</button>
132+
</div>
133+
</div>
53134
</div>
54135
55136
<!-- Mobile menu -->

0 commit comments

Comments
 (0)