feat: compact programme selector

This commit is contained in:
msa46 2026-08-16 12:03:59 +02:00
parent a0e9d2c042
commit c723d46ebf
7 changed files with 81 additions and 27 deletions

View file

@ -33,6 +33,7 @@ export default defineConfig({
}, },
favicon: '/favicon.ico', favicon: '/favicon.ico',
customCss: ['./src/styles/incognito.css'], customCss: ['./src/styles/incognito.css'],
routeMiddleware: ['./src/starlight-route-data.ts'],
components: { components: {
Sidebar: './src/components/Sidebar.astro', Sidebar: './src/components/Sidebar.astro',
}, },

View file

@ -7,9 +7,19 @@ const withBase = (path: string) => `${import.meta.env.BASE_URL.replace(/\/$/, ''
--- ---
<nav class="programme-switch" aria-label="Bachelor programme"> <nav class="programme-switch" aria-label="Bachelor programme">
<span class="programme-switch__label">Course information</span> <span class="programme-switch__label">Bachelor programme</span>
<div class="programme-switch__links"> <div class="programme-switch__links">
<a href={withBase(targets.dataScience)} aria-current={active === 'data-science-and-ai' ? 'page' : undefined}>Data Science &amp; AI</a> <a
<a href={withBase(targets.computerScience)} aria-current={active === 'computer-science' ? 'page' : undefined}>Computer Science</a> href={withBase(targets.dataScience)}
aria-label="Data Science & AI"
title="Data Science & AI"
aria-current={active === 'data-science-and-ai' ? 'page' : undefined}
>DSAI</a>
<a
href={withBase(targets.computerScience)}
aria-label="Computer Science"
title="Computer Science"
aria-current={active === 'computer-science' ? 'page' : undefined}
>CS</a>
</div> </div>
</nav> </nav>

View file

@ -1,12 +0,0 @@
import { defineMiddleware } from 'astro:middleware';
import { filterResolvedSidebar } from './config/programme-navigation.mjs';
export const onRequest = defineMiddleware((context, next) => {
try {
const route = context.locals.starlightRoute;
route.sidebar = filterResolvedSidebar(route.sidebar, context.url.pathname);
} catch {
// Non-Starlight routes such as the generated 404 page have no route data.
}
return next();
});

View file

@ -0,0 +1,10 @@
import { defineRouteMiddleware } from '@astrojs/starlight/route-data';
import { filterResolvedSidebar } from './config/programme-navigation.mjs';
export const onRequest = defineRouteMiddleware((context, next) => {
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
const pathname = context.url.pathname.replace(base, '');
const route = context.locals.starlightRoute;
route.sidebar = filterResolvedSidebar(route.sidebar, pathname);
return next();
});

View file

@ -134,7 +134,7 @@
} }
.programme-switch { .programme-switch {
padding: 0.75rem; padding: 0.625rem;
margin: 0 0.5rem 0.75rem; margin: 0 0.5rem 0.75rem;
border: 1px solid var(--sl-color-gray-5); border: 1px solid var(--sl-color-gray-5);
border-radius: 0.6rem; border-radius: 0.6rem;
@ -142,7 +142,7 @@
.programme-switch__label { .programme-switch__label {
display: block; display: block;
margin-bottom: 0.45rem; margin-bottom: 0.4rem;
color: var(--sl-color-gray-2); color: var(--sl-color-gray-2);
font-size: var(--sl-text-xs); font-size: var(--sl-text-xs);
font-weight: 600; font-weight: 600;
@ -151,25 +151,32 @@
.programme-switch__links { .programme-switch__links {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: 0.35rem; gap: 0.2rem;
padding: 0.2rem;
border-radius: 0.45rem;
background: var(--sl-color-gray-6);
} }
.programme-switch__links a { .programme-switch__links a {
display: flex; display: flex;
min-height: 2.5rem; min-height: 2.25rem;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 0.35rem; padding: 0.3rem 0.5rem;
border-radius: 0.4rem; border-radius: 0.35rem;
text-align: center; color: var(--sl-color-gray-2);
line-height: 1.2; font-size: var(--sl-text-xs);
font-weight: 600;
line-height: 1;
text-decoration: none;
white-space: nowrap;
} }
.programme-switch__links a[aria-current='page'] { .programme-switch__links a[aria-current='page'] {
color: var(--sl-color-text-accent); color: var(--incognito-light);
background: var(--sl-color-accent-low); background: var(--incognito-primary);
font-weight: 700; font-weight: 700;
box-shadow: inset 0 0 0 1px var(--sl-color-accent); box-shadow: var(--sl-shadow-sm);
} }
.programme-switch__links a:focus-visible { .programme-switch__links a:focus-visible {

View file

@ -19,7 +19,7 @@ async function collectCss(directory) {
return chunks.join('\n'); return chunks.join('\n');
} }
test('subpath production build includes the local consent controller and its UI styles', async () => { test('subpath production build includes local UI assets and focused programme navigation', async () => {
const output = await mkdtemp(join(process.cwd(), '.matomo-build-')); const output = await mkdtemp(join(process.cwd(), '.matomo-build-'));
try { try {
await execFileAsync('node_modules/.bin/astro', ['build', '--outDir', output], { await execFileAsync('node_modules/.bin/astro', ['build', '--outDir', output], {
@ -40,6 +40,32 @@ test('subpath production build includes the local consent controller and its UI
assert.equal(scripts[0].hasAttribute('defer'), true); assert.equal(scripts[0].hasAttribute('defer'), true);
await access(join(output, 'matomo-consent.js')); await access(join(output, 'matomo-consent.js'));
const computerScienceHtml = await readFile(join(output, 'computer-science/index.html'), 'utf8');
const { document: computerScienceDocument } = parseHTML(computerScienceHtml);
const computerScienceSidebar = computerScienceDocument.querySelector('#starlight__sidebar');
assert.ok(computerScienceSidebar);
assert.match(computerScienceSidebar.textContent, /Home/);
assert.match(computerScienceSidebar.textContent, /Previous exams and documents/);
assert.match(computerScienceSidebar.textContent, /Computer Science/);
assert.match(computerScienceSidebar.textContent, /Useful Information/);
assert.doesNotMatch(computerScienceSidebar.textContent, /About Incognito/);
assert.doesNotMatch(computerScienceSidebar.textContent, /Data Science & AI/);
assert.doesNotMatch(computerScienceSidebar.textContent, /Master AI/);
assert.doesNotMatch(computerScienceSidebar.textContent, /Master DSDM/);
const dataScienceHtml = await readFile(join(output, 'data-science-and-ai/index.html'), 'utf8');
const { document: dataScienceDocument } = parseHTML(dataScienceHtml);
const dataScienceSidebar = dataScienceDocument.querySelector('#starlight__sidebar');
assert.ok(dataScienceSidebar);
assert.match(dataScienceSidebar.textContent, /Home/);
assert.match(dataScienceSidebar.textContent, /Previous exams and documents/);
assert.match(dataScienceSidebar.textContent, /Data Science & AI/);
assert.match(dataScienceSidebar.textContent, /Useful Information/);
assert.doesNotMatch(dataScienceSidebar.textContent, /About Incognito/);
assert.doesNotMatch(dataScienceSidebar.textContent, /Computer Science/);
assert.doesNotMatch(dataScienceSidebar.textContent, /Master AI/);
assert.doesNotMatch(dataScienceSidebar.textContent, /Master DSDM/);
const css = await collectCss(output); const css = await collectCss(output);
assert.match(css, /#incognito-analytics-consent/); assert.match(css, /#incognito-analytics-consent/);
assert.match(css, /\.incognito-consent-action/); assert.match(css, /\.incognito-consent-action/);

View file

@ -10,3 +10,15 @@ test('Sidebar override renders an accessible no-JavaScript programme switch', as
assert.doesNotMatch(component, /client:|<script/); assert.doesNotMatch(component, /client:|<script/);
assert.match(wrapper, /@astrojs\/starlight\/components\/Sidebar\.astro/); assert.match(wrapper, /@astrojs\/starlight\/components\/Sidebar\.astro/);
}); });
test('programme switch uses compact visible labels with full accessible names', async () => {
const component = await readFile('src/components/ProgrammeSwitch.astro', 'utf8');
assert.match(component, />DSAI<\/a>/);
assert.match(component, />CS<\/a>/);
assert.match(component, /aria-label="Data Science & AI"/);
assert.match(component, /aria-label="Computer Science"/);
assert.match(component, /title="Data Science & AI"/);
assert.match(component, /title="Computer Science"/);
assert.match(component, />Bachelor programme<\/span>/);
assert.doesNotMatch(component, />Course information<\/span>/);
});