Skip to content

Commit 44f807a

Browse files
raifdmuellerclaude
andcommitted
feat: Redesign header layout and add sync-anchors build step
- Logo spans both rows on desktop, nav in row 1, search/filter in row 2 - Move anchor count (56/56) into header row 2 - Hide duplicate search/filter in main content on desktop - Replace info (i) icon with play/video icon for onboarding button - Add sync-anchors.js script to reliably copy .adoc files from docs/anchors/ to website/public/docs/anchors/ before dev and build - Support mobile-specific IDs for theme, lang, and onboarding buttons Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c23caf9 commit 44f807a

6 files changed

Lines changed: 205 additions & 95 deletions

File tree

scripts/sync-anchors.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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(
50+
`[sync-anchors] ${copied} copied, ${skipped} up-to-date (${srcFiles.length} total)`
51+
)
52+
}
53+
54+
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/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: 109 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -5,80 +5,131 @@ 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 sm:px-6 lg:px-8">
9-
<!-- Row 1: Logo + Language/Theme -->
10-
<div class="flex items-center justify-between py-3">
11-
<div class="flex items-center gap-2">
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">
1213
<a href="#/" class="no-underline flex flex-col items-start">
13-
<img src="${import.meta.env.BASE_URL}logo.png" alt="Semantic Anchors" class="max-h-16 sm:max-h-20" />
14-
<span class="hidden sm:block text-xs text-[var(--color-text-secondary)] leading-tight" data-i18n="header.slogan">${i18n.t('header.slogan')}</span>
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>
1516
</a>
1617
<button
1718
id="onboarding-info-btn"
18-
class="rounded-full p-1 text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] hover:bg-[var(--color-bg-secondary)] transition-colors"
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"
1920
data-i18n-aria="onboarding.infoButton"
2021
data-i18n-title="onboarding.infoButton"
2122
aria-label="${i18n.t('onboarding.infoButton')}"
2223
title="${i18n.t('onboarding.infoButton')}"
2324
>
2425
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
25-
<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" />
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" />
2628
</svg>
2729
</button>
2830
</div>
29-
<div class="flex items-center gap-3">
30-
<button
31-
id="lang-toggle"
32-
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"
33-
aria-label="Toggle language"
34-
>${langLabel}</button>
35-
<button
36-
id="theme-toggle"
37-
class="rounded-md p-2 text-[var(--color-text-secondary)] hover:text-[var(--color-text)] hover:bg-[var(--color-bg-secondary)] transition-colors"
38-
data-i18n-aria="header.themeToggle.dark"
39-
aria-label="Switch to dark mode"
40-
>
41-
<svg id="theme-icon-moon" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
42-
<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" />
43-
</svg>
44-
<svg id="theme-icon-sun" class="h-5 w-5 hidden" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
45-
<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" />
46-
</svg>
47-
</button>
48-
<button
49-
id="mobile-menu-toggle"
50-
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"
51-
aria-label="Toggle menu"
52-
aria-expanded="false"
53-
>
54-
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
55-
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
56-
</svg>
57-
</button>
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>
5881
</div>
5982
</div>
6083
61-
<!-- Row 2: Navigation + Search/Filter (desktop) -->
62-
<div class="hidden sm:flex items-center justify-between pb-3">
63-
<div class="flex items-center gap-6 text-2xl">
64-
<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>
65-
<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>
66-
<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>
67-
</div>
68-
<div class="flex items-center gap-3">
69-
<input
70-
id="header-search-input"
71-
type="search"
72-
data-i18n-placeholder="search.placeholder"
73-
placeholder="${i18n.t('search.placeholder')}"
74-
class="w-48 lg:w-72 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"
75-
/>
76-
<select
77-
id="header-role-filter"
78-
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"
79-
>
80-
<option value="" data-i18n="filter.allRoles">${i18n.t('filter.allRoles')}</option>
81-
</select>
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>
82133
</div>
83134
</div>
84135

website/src/components/main-content.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,16 @@ export function renderMain() {
4444
type="search"
4545
data-i18n-placeholder="search.placeholder"
4646
placeholder="${i18n.t('search.placeholder')}"
47-
class="rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] px-4 py-2 text-sm text-[var(--color-text)] placeholder-[var(--color-text-secondary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] transition-colors duration-300"
47+
class="sm:hidden rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] px-4 py-2 text-sm text-[var(--color-text)] placeholder-[var(--color-text-secondary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] transition-colors duration-300"
4848
/>
4949
<select
5050
id="role-filter"
51-
class="rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] px-4 py-2 text-sm text-[var(--color-text)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] transition-colors duration-300"
51+
class="sm:hidden rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] px-4 py-2 text-sm text-[var(--color-text)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] transition-colors duration-300"
5252
>
5353
<option value="" data-i18n="filter.allRoles">${i18n.t('filter.allRoles')}</option>
5454
</select>
55-
<span id="anchor-count" class="text-sm text-[var(--color-text-secondary)] ml-auto">
56-
<span id="visible-count">0</span> / <span id="total-count">0</span> <span data-i18n="filter.anchors">${i18n.t('filter.anchors')}</span>
55+
<span id="anchor-count-mobile" class="sm:hidden text-sm text-[var(--color-text-secondary)] ml-auto">
56+
<span id="visible-count-mobile">0</span> / <span id="total-count-mobile">0</span> <span data-i18n="filter.anchors">${i18n.t('filter.anchors')}</span>
5757
</span>
5858
</section>
5959

0 commit comments

Comments
 (0)