diff --git a/docs/superpowers/plans/2026-08-16-compact-programme-switch-and-sidebar-filtering.md b/docs/superpowers/plans/2026-08-16-compact-programme-switch-and-sidebar-filtering.md new file mode 100644 index 0000000..97309cd --- /dev/null +++ b/docs/superpowers/plans/2026-08-16-compact-programme-switch-and-sidebar-filtering.md @@ -0,0 +1,327 @@ +# Compact Programme Switch and Sidebar Filtering Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the wide bachelor programme selector with a compact DSAI/CS control and focus programme sidebars on the selected curriculum plus Home, Previous exams and documents, and Useful Information. + +**Architecture:** Keep the current URL-derived, no-JavaScript programme state and route-aware switch targets. Concentrate sidebar selection in the existing pure navigation helpers, while `ProgrammeSwitch.astro` remains a presentational link-based component and Starlight middleware consumes the filtered result. + +**Tech Stack:** Astro 7, Astro Starlight 0.41, TypeScript/Astro components, CSS custom properties, Node.js 22 built-in test runner. + +## Global Constraints + +- Visible programme labels are exactly `DSAI` and `CS`. +- Full programme names remain available to assistive technology. +- Programme selection is derived from the URL and is not stored in cookies or local storage. +- The switch uses ordinary links and no client-side JavaScript or hydration directive. +- Computer Science and Data Science & AI routes each show only Home, Previous exams and documents, the active curriculum, and Useful Information. +- About Incognito, Master AI, Master DSDM, and the inactive bachelor curriculum are hidden on programme routes. +- Non-programme routes retain the existing global sidebar. +- Existing paired-course and fallback programme destinations remain unchanged. +- Do not change course content, curriculum structure, or the paired-course mapping. + +--- + +### Task 1: Focus programme-specific sidebar navigation + +**Files:** +- Modify: `tests/programme-navigation.test.mjs` +- Modify: `src/config/programme-navigation.mjs:48-68` + +**Interfaces:** +- Consumes: `programmeForPathname(pathname: string): 'computer-science' | 'data-science-and-ai' | null` and the existing exported `sidebar` configuration. +- Produces: `sidebarForPathname(pathname: string): Array` and `filterResolvedSidebar(entries: Array, pathname: string): Array` with identical programme-specific inclusion rules. + +- [ ] **Step 1: Replace the broad sidebar-retention test with focused failing tests** + +Update `tests/programme-navigation.test.mjs` so programme and global behavior are asserted independently: + +```js +test('programme routes retain only the selected curriculum and shared destinations', () => { + const computerScience = JSON.stringify(sidebarForPathname('/computer-science/year-1/')); + assert.match(computerScience, /"label":"Home"/); + assert.match(computerScience, /previous-exams-and-documents/); + assert.match(computerScience, /"label":"Computer Science"/); + assert.match(computerScience, /"label":"Useful Information"/); + assert.doesNotMatch(computerScience, /"label":"Data Science & AI"/); + assert.doesNotMatch(computerScience, /"label":"About Incognito"/); + assert.doesNotMatch(computerScience, /"label":"Master AI"/); + assert.doesNotMatch(computerScience, /"label":"Master DSDM"/); + + const dataScience = JSON.stringify(sidebarForPathname('/data-science-and-ai/year-2/')); + assert.match(dataScience, /"label":"Home"/); + assert.match(dataScience, /previous-exams-and-documents/); + assert.match(dataScience, /"label":"Data Science & AI"/); + assert.match(dataScience, /"label":"Useful Information"/); + assert.doesNotMatch(dataScience, /"label":"Computer Science"/); + assert.doesNotMatch(dataScience, /"label":"About Incognito"/); + assert.doesNotMatch(dataScience, /"label":"Master AI"/); + assert.doesNotMatch(dataScience, /"label":"Master DSDM"/); +}); + +test('global routes retain the existing global navigation', () => { + const global = JSON.stringify(sidebarForPathname('/useful-information/')); + assert.match(global, /"label":"About Incognito"/); + assert.match(global, /"label":"Master AI"/); + assert.match(global, /"label":"Master DSDM"/); + assert.match(global, /"label":"Bachelor programmes"/); +}); + +test('resolved programme sidebars use the same focused groups', () => { + const entries = [ + { label: 'Home' }, + { label: 'About Incognito' }, + { label: 'Previous exams and documents' }, + { label: 'Computer Science' }, + { label: 'Data Science & AI' }, + { label: 'Master AI' }, + { label: 'Master DSDM' }, + { label: 'Useful Information' }, + ]; + + assert.deepEqual( + filterResolvedSidebar(entries, '/computer-science/').map(({ label }) => label), + ['Home', 'Previous exams and documents', 'Computer Science', 'Useful Information'], + ); + assert.deepEqual( + filterResolvedSidebar(entries, '/data-science-and-ai/').map(({ label }) => label), + ['Home', 'Previous exams and documents', 'Data Science & AI', 'Useful Information'], + ); + assert.deepEqual(filterResolvedSidebar(entries, '/useful-information/'), entries); +}); +``` + +Also add `filterResolvedSidebar` to the named import at the top of the test file. + +- [ ] **Step 2: Run the navigation tests and verify the new assertions fail** + +Run: + +```bash +node --test tests/programme-navigation.test.mjs +``` + +Expected: FAIL because programme routes still include About Incognito, Master AI, and Master DSDM, and the resolved filter preserves those groups. + +- [ ] **Step 3: Implement one shared inclusion policy in the navigation helpers** + +In `src/config/programme-navigation.mjs`, define the resolved labels shared across programme views and update both exported functions: + +```js +const programmeSharedLabels = new Set(['Home', 'Previous exams and documents', 'Useful Information']); + +export function sidebarForPathname(pathname) { + const programme = programmeForPathname(pathname); + const dataScience = sidebar.find(({ label }) => label === 'Data Science & AI'); + + if (programme) { + const undergraduate = programme === 'computer-science' ? computerScience : dataScience; + const home = sidebar.find(({ label }) => label === 'Home'); + const previousExams = sidebar.find(({ slug }) => slug === 'previous-exams-and-documents'); + const usefulInformation = sidebar.find(({ label }) => label === 'Useful Information'); + return [home, previousExams, undergraduate, usefulInformation]; + } + + const withoutProgrammes = sidebar.filter(({ label }) => label !== 'Computer Science' && label !== 'Data Science & AI'); + return [...withoutProgrammes.slice(0, 3), compact, ...withoutProgrammes.slice(3)]; +} + +export function filterResolvedSidebar(entries, pathname) { + const programme = programmeForPathname(pathname); + if (programme) { + const activeLabel = programme === 'computer-science' ? 'Computer Science' : 'Data Science & AI'; + return entries.filter(({ label }) => programmeSharedLabels.has(label) || label === activeLabel); + } + + return entries.map((entry) => { + if ((entry.label === 'Computer Science' || entry.label === 'Data Science & AI') && 'entries' in entry) { + return { ...entry, entries: entry.entries.slice(0, 1) }; + } + return entry; + }); +} +``` + +Keep the helper ordering exactly as Home, Previous exams and documents, active curriculum, and Useful Information. Do not modify `shared`, `programmeForPathname`, or `programmeSwitchTargets`. + +- [ ] **Step 4: Run the focused and related tests** + +Run: + +```bash +node --test tests/programme-navigation.test.mjs tests/programme-switch.test.mjs tests/programme-redirects.test.mjs +``` + +Expected: all tests PASS, including the unchanged paired-course assertion. + +- [ ] **Step 5: Commit the focused navigation behavior** + +```bash +git add tests/programme-navigation.test.mjs src/config/programme-navigation.mjs +git commit -m "feat: focus programme sidebar navigation" +``` + +--- + +### Task 2: Render and style the compact accessible programme switch + +**Files:** +- Modify: `tests/programme-switch.test.mjs` +- Modify: `src/components/ProgrammeSwitch.astro:9-14` +- Modify: `src/styles/incognito.css:136-178` + +**Interfaces:** +- Consumes: `programmeForPathname(pathname)` and `programmeSwitchTargets(pathname)` from `src/config/programme-navigation.mjs`. +- Produces: a `