Skip to content

Commit c23caf9

Browse files
raifdmuellerclaude
andcommitted
feat: Redesign header with two-row layout and integrated search
Split header into two rows for better layout: - Row 1: Logo (full size), info button, language/theme toggles - Row 2: Navigation links (larger text), search input and role filter with visible borders Search and role filter in header sync bidirectionally with main content equivalents. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent cdab9e6 commit c23caf9

2 files changed

Lines changed: 128 additions & 38 deletions

File tree

website/src/components/header.js

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,26 @@ 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-
<div class="flex items-center gap-2">
12-
<a href="#/" class="no-underline flex flex-col items-start">
13-
<img src="${import.meta.env.BASE_URL}logo.png" alt="Semantic Anchors" class="h-8" />
14-
<span class="hidden sm:block text-xs text-[var(--color-text-secondary)] leading-tight" data-i18n="header.slogan">${i18n.t('header.slogan')}</span>
15-
</a>
16-
<button
17-
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-
data-i18n-aria="onboarding.infoButton"
20-
data-i18n-title="onboarding.infoButton"
21-
aria-label="${i18n.t('onboarding.infoButton')}"
22-
title="${i18n.t('onboarding.infoButton')}"
23-
>
24-
<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-
</svg>
27-
</button>
28-
</div>
29-
<div class="hidden sm:flex items-center gap-4 text-sm">
30-
<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>
31-
<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>
32-
<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>
33-
</div>
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">
12+
<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>
15+
</a>
16+
<button
17+
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+
data-i18n-aria="onboarding.infoButton"
20+
data-i18n-title="onboarding.infoButton"
21+
aria-label="${i18n.t('onboarding.infoButton')}"
22+
title="${i18n.t('onboarding.infoButton')}"
23+
>
24+
<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+
</svg>
27+
</button>
3428
</div>
3529
<div class="flex items-center gap-3">
3630
<button
@@ -64,6 +58,30 @@ export function renderHeader() {
6458
</div>
6559
</div>
6660
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>
82+
</div>
83+
</div>
84+
6785
<!-- Mobile menu -->
6886
<div id="mobile-menu" class="hidden sm:hidden mt-3 pb-2 border-t border-[var(--color-border)] pt-3">
6987
<div class="flex flex-col gap-2">

website/src/main.js

Lines changed: 84 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,10 @@ function triggerSearchIndexBuild() {
8383
searchIndexTriggered = true
8484
buildSearchIndex(appData.anchors)
8585
.then(() => {
86-
const searchInput = document.getElementById('search-input')
87-
if (searchInput) {
88-
searchInput.placeholder = `${i18n.t('search.placeholder')} (full-text)`
89-
}
86+
;['search-input', 'header-search-input'].forEach((id) => {
87+
const el = document.getElementById(id)
88+
if (el) el.placeholder = `${i18n.t('search.placeholder')} (full-text)`
89+
})
9090
})
9191
.catch((err) => {
9292
console.warn('Search index build failed:', err)
@@ -128,9 +128,14 @@ function initApp() {
128128
showOnboarding()
129129
}
130130

131-
ensureDataLoaded().catch((err) => {
132-
console.error('Failed to load app data:', err)
133-
})
131+
ensureDataLoaded()
132+
.then(() => {
133+
populateHeaderRoleFilter()
134+
bindHeaderSearchInput()
135+
})
136+
.catch((err) => {
137+
console.error('Failed to load app data:', err)
138+
})
134139
}
135140

136141
function renderHomePage() {
@@ -222,7 +227,66 @@ function initCardGridVisualization() {
222227
}
223228

224229
function bindRoleFilter() {
225-
const roleFilter = document.getElementById('role-filter')
230+
const roleFilterIds = ['role-filter', 'header-role-filter']
231+
232+
roleFilterIds.forEach((id) => {
233+
const roleFilter = document.getElementById(id)
234+
if (!roleFilter || !appData?.roles) return
235+
236+
while (roleFilter.options.length > 1) {
237+
roleFilter.remove(1)
238+
}
239+
240+
appData.roles.forEach((role) => {
241+
const option = document.createElement('option')
242+
option.value = role.id
243+
option.textContent = role.name
244+
roleFilter.appendChild(option)
245+
})
246+
247+
roleFilter.onchange = (e) => {
248+
// Sync the other dropdown
249+
roleFilterIds.forEach((otherId) => {
250+
if (otherId !== id) {
251+
const other = document.getElementById(otherId)
252+
if (other) other.value = e.target.value
253+
}
254+
})
255+
const searchQuery = document.getElementById('header-search-input')?.value
256+
|| document.getElementById('search-input')?.value || ''
257+
applyCardFilters(e.target.value, searchQuery)
258+
}
259+
})
260+
}
261+
262+
function bindSearchInput() {
263+
const searchInputIds = ['search-input', 'header-search-input']
264+
265+
searchInputIds.forEach((id) => {
266+
const searchInput = document.getElementById(id)
267+
if (!searchInput) return
268+
269+
searchInput.oninput = (e) => {
270+
const query = e.target.value
271+
// Sync the other search input
272+
searchInputIds.forEach((otherId) => {
273+
if (otherId !== id) {
274+
const other = document.getElementById(otherId)
275+
if (other) other.value = query
276+
}
277+
})
278+
if (query.trim()) {
279+
triggerSearchIndexBuild()
280+
}
281+
const roleId = document.getElementById('header-role-filter')?.value
282+
|| document.getElementById('role-filter')?.value || ''
283+
applyCardFilters(roleId, query)
284+
}
285+
})
286+
}
287+
288+
function populateHeaderRoleFilter() {
289+
const roleFilter = document.getElementById('header-role-filter')
226290
if (!roleFilter || !appData?.roles) return
227291

228292
while (roleFilter.options.length > 1) {
@@ -237,22 +301,30 @@ function bindRoleFilter() {
237301
})
238302

239303
roleFilter.onchange = (e) => {
240-
const searchQuery = document.getElementById('search-input')?.value || ''
304+
// Sync the main content dropdown if it exists
305+
const mainFilter = document.getElementById('role-filter')
306+
if (mainFilter) mainFilter.value = e.target.value
307+
308+
const searchQuery = document.getElementById('header-search-input')?.value || ''
241309
applyCardFilters(e.target.value, searchQuery)
242310
}
243311
}
244312

245-
function bindSearchInput() {
246-
const searchInput = document.getElementById('search-input')
313+
function bindHeaderSearchInput() {
314+
const searchInput = document.getElementById('header-search-input')
247315
if (!searchInput) return
248316

249317
searchInput.oninput = (e) => {
250318
const query = e.target.value
319+
// Sync the main content search input if it exists
320+
const mainSearch = document.getElementById('search-input')
321+
if (mainSearch) mainSearch.value = query
322+
251323
if (query.trim()) {
252324
triggerSearchIndexBuild()
253325
}
254326

255-
const roleId = document.getElementById('role-filter')?.value || ''
327+
const roleId = document.getElementById('header-role-filter')?.value || ''
256328
applyCardFilters(roleId, query)
257329
}
258330
}

0 commit comments

Comments
 (0)