Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
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/
5 changes: 3 additions & 2 deletions docs/anchors/chatham-house-rule.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,10 @@ Historical Context:: Originally created to enable diplomats and government offic
[discrete]
== *Example Invocation*:

----
[quote]
____
This retrospective will be conducted under the Chatham House Rule.
You may share the insights we generate, but don't attribute comments
to specific team members.
----
____
====
5 changes: 3 additions & 2 deletions docs/anchors/chatham-house-rule.de.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,10 @@ Historischer Kontext:: Ursprünglich geschaffen, um Diplomaten und Regierungsbea
[discrete]
== *Beispielhafte Anwendung*:

----
[quote]
____
Diese Retrospektive wird unter der Chatham House Rule durchgeführt.
Sie dürfen die generierten Erkenntnisse teilen, aber ordnen Sie
Kommentare nicht spezifischen Teammitgliedern zu.
----
____
====
69 changes: 69 additions & 0 deletions docs/plans/2026-03-08-onboarding-modal-design.md
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`

Comment thread
coderabbitai[bot] marked this conversation as resolved.
### 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
52 changes: 52 additions & 0 deletions scripts/sync-anchors.js
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Das ist noch kein vollständiger Sync.

Wenn ein Anchor in docs/anchors gelöscht oder umbenannt wird, bleibt die alte .adoc in website/public/docs/anchors erhalten, weil hier nur kopiert und nie bereinigt wird. Dadurch kann die Website veraltete Dokumente weiter ausliefern.

🧹 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
Verify each finding against the current code and only fix it if needed.

In `@scripts/sync-anchors.js` around lines 27 - 47, Das Script kopiert nur
neue/aktualisierte .adoc aus SRC nach DEST (siehe srcFiles, SRC, DEST, srcPath,
destPath, fs.copyFileSync) und entfernt nicht mehr existierende Dateien; ergänze
nach dem Einlesen von srcFiles und vor/ oder nach der Kopierschleife eine
Bereinigung: liste alle .adoc in DEST (z.B. destFiles =
fs.readdirSync(DEST).filter(...)) und für jede Datei, die nicht in srcFiles
vorkommt, rufe fs.unlinkSync(path.join(DEST, file)) auf, sodass
entfernte/umbenannte Anchors aus DEST gelöscht werden; beibehalte die bestehende
mtime-Vergleichslogik und die Zähler (copied/skipped) unverändert.


console.log(`[sync-anchors] ${copied} copied, ${skipped} up-to-date (${srcFiles.length} total)`)
}

sync()
4 changes: 3 additions & 1 deletion website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
"version": "0.1.0",
"type": "module",
"scripts": {
"sync-anchors": "node ../scripts/sync-anchors.js",
"predev": "node ../scripts/sync-anchors.js",
"dev": "vite",
"prebuild": "node ../scripts/render-docs.js",
"prebuild": "node ../scripts/sync-anchors.js && node ../scripts/render-docs.js",
"build": "vite build",
"preview": "vite preview",
"test": "vitest run",
Expand Down
Binary file added website/public/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion website/src/components/anchor-modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function createModal() {
const modal = document.createElement('div')
modal.id = 'anchor-modal'
modal.className =
'fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50 p-4'
'fixed inset-0 bg-black/30 backdrop-blur-sm hidden items-center justify-center z-50 p-4'
modal.innerHTML = `
<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)]">
<div class="flex items-center justify-between p-6 border-b border-[var(--color-border)]">
Expand Down
11 changes: 6 additions & 5 deletions website/src/components/card-grid.js
Original file line number Diff line number Diff line change
Expand Up @@ -360,9 +360,10 @@ export function applyCardFilters(roleId, searchQuery) {
* Update the anchor counter display
*/
export function updateAnchorCount(visible, total) {
const visibleCountEl = document.getElementById('visible-count')
const totalCountEl = document.getElementById('total-count')

if (visibleCountEl) visibleCountEl.textContent = visible
if (totalCountEl) totalCountEl.textContent = total
document.querySelectorAll('#visible-count, #visible-count-mobile').forEach((el) => {
el.textContent = visible
})
document.querySelectorAll('#total-count, #total-count-mobile').forEach((el) => {
el.textContent = total
})
}
161 changes: 121 additions & 40 deletions website/src/components/header.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Desktop-Trigger weicht vom geforderten Info-Icon ab.

Das Control neben dem Logo zeigt hier ein Play-Symbol, obwohl #145 explizit ein Info-Icon im Header fordert. Zusätzlich ist das Verhalten damit zwischen Desktop und Mobile inkonsistent.

Also applies to: 92-103

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@website/src/components/header.js` around lines 17 - 29, Replace the incorrect
"play" SVG used in the header control with the required info icon for the
element with id="onboarding-info-btn" so desktop matches the requested Info-Icon
from issue `#145`; update the SVG inside the button (and the corresponding mobile
instance referenced around the other block at lines 92-103) to the consistent
info icon SVG (preserve classes, attributes like aria-label/title using
i18n.t('onboarding.infoButton'), and styling/hover behavior) so desktop and
mobile show the same icon and accessibility labels.

</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 -->
Expand Down
Loading
Loading