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
21 changes: 14 additions & 7 deletions website/src/components/card-grid.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,16 +90,21 @@ function renderCategorySection(category, allAnchors) {
</h2>

<div class="anchor-cards-grid">
${categoryAnchors.map((anchor) => renderAnchorCard(anchor, color)).join('')}
${categoryAnchors.map((anchor) => renderAnchorCard(anchor, color, category.id)).join('')}
</div>
</section>
`
}

/**
* Render a single anchor card
* Render a single anchor card.
*
* The optional `categoryId` argument is used to namespace the heading id
* used by `aria-labelledby`, since the same anchor may appear in multiple
* category sections (anchors can belong to more than one category) and
* the DOM must not contain duplicate ids.
*/
function renderAnchorCard(anchor, categoryColor) {
function renderAnchorCard(anchor, categoryColor, categoryId) {
const isUmbrella = anchor.subAnchors && anchor.subAnchors.length > 0
const umbrellaClass = isUmbrella ? ' anchor-card-umbrella' : ''
const rolesCount = anchor.roles ? anchor.roles.length : 0
Expand All @@ -110,19 +115,21 @@ function renderAnchorCard(anchor, categoryColor) {
const editTitle = i18n.t('card.edit')
const copyLinkTitle = i18n.t('card.copyLink')
const safeId = escapeHtml(anchor.id)
const safeCategoryId = escapeHtml(categoryId || 'uncat')
const cardTitleId = `anchor-card-title-${safeCategoryId}-${safeId}`

return `
<article
<div
class="anchor-card${umbrellaClass}"
data-anchor="${safeId}"
data-roles="${escapeHtml(anchor.roles ? anchor.roles.join(',') : '')}"
data-tags="${escapeHtml(anchor.tags ? anchor.tags.join(',') : '')}"
tabindex="0"
role="button"
aria-label="${escapeHtml(i18n.t('card.openDetails').replace('{title}', anchor.title))}"
aria-labelledby="${cardTitleId}"
>
<div class="anchor-card-header">
<h3 class="anchor-card-title">${escapeHtml(anchor.title)}</h3>
<h3 id="${cardTitleId}" class="anchor-card-title">${escapeHtml(anchor.title)}</h3>
<div class="flex gap-1">
<button
class="anchor-copy-link-btn"
Expand Down Expand Up @@ -215,7 +222,7 @@ function renderAnchorCard(anchor, categoryColor) {
</div>

<div class="anchor-card-indicator" style="background-color: ${categoryColor}"></div>
</article>
</div>
`
}

Expand Down
12 changes: 8 additions & 4 deletions website/src/components/header.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function renderHeader() {
<!-- Logo left, spanning both rows -->
<div class="flex items-center">
<a href="#/" class="no-underline flex flex-col items-center">
<img src="${import.meta.env.BASE_URL}logo.png" alt="Semantic Anchors" class="max-h-24" />
<img src="${import.meta.env.BASE_URL}logo.png" alt="Semantic Anchors" width="218" height="96" class="max-h-24 w-auto" />
<span class="text-xs text-[var(--color-text-secondary)] leading-tight" data-i18n="header.slogan">${i18n.t('header.slogan')}</span>
</a>
<button
Expand Down Expand Up @@ -54,7 +54,8 @@ export function renderHeader() {
<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"
aria-label="${i18n.t('header.langToggleAria')}"
data-i18n-aria="header.langToggleAria"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
>${langLabel}</button>
<button
id="theme-toggle"
Expand Down Expand Up @@ -82,6 +83,8 @@ export function renderHeader() {
/>
<select
id="header-role-filter"
aria-label="${i18n.t('filter.allRoles')}"
data-i18n-aria="filter.allRoles"
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>
Expand All @@ -97,7 +100,7 @@ export function renderHeader() {
<div class="sm:hidden">
<div class="flex flex-col items-center">
<a href="#/" class="no-underline flex flex-col items-center">
<img src="${import.meta.env.BASE_URL}logo.png" alt="Semantic Anchors" class="max-h-16" />
<img src="${import.meta.env.BASE_URL}logo.png" alt="Semantic Anchors" width="145" height="64" class="max-h-16 w-auto" />
<span class="text-xs text-[var(--color-text-secondary)] leading-tight text-center" data-i18n="header.slogan">${i18n.t('header.slogan')}</span>
</a>
<div class="flex items-center gap-3 mt-2">
Expand All @@ -117,7 +120,8 @@ export function renderHeader() {
<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"
aria-label="${i18n.t('header.langToggleAria')}"
data-i18n-aria="header.langToggleAria"
>${langLabel}</button>
<button
id="theme-toggle-mobile"
Expand Down
2 changes: 1 addition & 1 deletion website/src/components/onboarding-modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ function buildModalContent() {
</div>

<div class="text-center px-6 pb-4">
<img src="${baseUrl}logo.png" alt="Semantic Anchors" class="h-24 mx-auto mb-3" />
<img src="${baseUrl}logo.png" alt="Semantic Anchors" width="218" height="96" class="h-24 w-auto mx-auto mb-3" />
<h2 id="onboarding-title" class="text-lg font-medium text-[var(--color-text-secondary)]">
${slogan2}
</h2>
Expand Down
11 changes: 11 additions & 0 deletions website/src/styles/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ body {
font-family: Inter, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}

/* Reserve viewport height so the browser doesn't shift layout when the
card grid / doc content loads asynchronously. This is the single biggest
lever for Cumulative Layout Shift on the homepage (Lighthouse weight 25). */
#page-content {
min-height: calc(100vh - 12rem);
}

/* Force sans-serif on all card descendants to override AsciiDoc serif fonts */
.anchor-card,
.anchor-card *,
Expand Down Expand Up @@ -164,6 +171,10 @@ body {
@apply flex items-center gap-1;
@apply px-2 py-1 rounded;
@apply bg-gray-100 dark:bg-gray-700;
/* Explicit text color for WCAG AA contrast on the badge background.
Default .anchor-card-meta text (gray-500/400) only reaches ~3.9:1 / ~3.1:1,
below the 4.5:1 threshold for normal text. */
@apply text-gray-700 dark:text-gray-200;
}

.feedback-badge {
Expand Down
1 change: 1 addition & 0 deletions website/src/translations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"app.title": "Semantic Anchors",
"app.description": "Gemeinsames Vokabular für LLM-Kommunikation",
"header.langToggle": "EN",
"header.langToggleAria": "EN — zu Englisch wechseln",
"header.slogan": "Ein Wort, und die KI versteht den Rest.",
"header.themeToggle.dark": "Zum Dunkelmodus wechseln",
"header.themeToggle.light": "Zum Hellmodus wechseln",
Expand Down
1 change: 1 addition & 0 deletions website/src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"app.title": "Semantic Anchors",
"app.description": "Shared vocabulary for LLM communication",
"header.langToggle": "DE",
"header.langToggleAria": "DE — switch to German",
"header.slogan": "One word, and the AI gets the rest.",
"header.themeToggle.dark": "Switch to dark mode",
"header.themeToggle.light": "Switch to light mode",
Expand Down
Loading