docs: plan compact programme switch
This commit is contained in:
parent
287ba66e70
commit
f9abeb4b3d
1 changed files with 327 additions and 0 deletions
|
|
@ -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<SidebarEntry>` and `filterResolvedSidebar(entries: Array<ResolvedSidebarEntry>, pathname: string): Array<ResolvedSidebarEntry>` 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 `<nav aria-label="Bachelor programme">` with visible `DSAI` and `CS` links, full `aria-label` and `title` values, and `aria-current="page"` on the active programme.
|
||||
|
||||
- [ ] **Step 1: Strengthen the component contract with failing markup tests**
|
||||
|
||||
Extend `tests/programme-switch.test.mjs` with exact compact-label and accessible-name assertions:
|
||||
|
||||
```js
|
||||
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>/);
|
||||
});
|
||||
```
|
||||
|
||||
Retain the existing no-JavaScript, `aria-current`, navigation-label, and default Starlight sidebar assertions.
|
||||
|
||||
- [ ] **Step 2: Run the component test and verify it fails**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test tests/programme-switch.test.mjs
|
||||
```
|
||||
|
||||
Expected: FAIL because the component still exposes long visible labels and the `Course information` caption.
|
||||
|
||||
- [ ] **Step 3: Replace the wide link copy with compact accessible links**
|
||||
|
||||
Update the rendered markup in `src/components/ProgrammeSwitch.astro`:
|
||||
|
||||
```astro
|
||||
<nav class="programme-switch" aria-label="Bachelor programme">
|
||||
<span class="programme-switch__label">Bachelor programme</span>
|
||||
<div class="programme-switch__links">
|
||||
<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>
|
||||
</nav>
|
||||
```
|
||||
|
||||
Do not change how `active`, `targets`, or deployment-base-aware URLs are calculated.
|
||||
|
||||
- [ ] **Step 4: Restyle the existing component as a compact segmented control**
|
||||
|
||||
Replace the programme-switch rules in `src/styles/incognito.css` with compact styles based on the existing theme variables:
|
||||
|
||||
```css
|
||||
.programme-switch {
|
||||
padding: 0.625rem;
|
||||
margin: 0 0.5rem 0.75rem;
|
||||
border: 1px solid var(--sl-color-gray-5);
|
||||
border-radius: 0.6rem;
|
||||
}
|
||||
|
||||
.programme-switch__label {
|
||||
display: block;
|
||||
margin-bottom: 0.4rem;
|
||||
color: var(--sl-color-gray-2);
|
||||
font-size: var(--sl-text-xs);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.programme-switch__links {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.2rem;
|
||||
padding: 0.2rem;
|
||||
border-radius: 0.45rem;
|
||||
background: var(--sl-color-gray-6);
|
||||
}
|
||||
|
||||
.programme-switch__links a {
|
||||
display: flex;
|
||||
min-height: 2.25rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.3rem 0.5rem;
|
||||
border-radius: 0.35rem;
|
||||
color: var(--sl-color-gray-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'] {
|
||||
color: var(--incognito-light);
|
||||
background: var(--incognito-primary);
|
||||
font-weight: 700;
|
||||
box-shadow: var(--sl-shadow-sm);
|
||||
}
|
||||
|
||||
.programme-switch__links a:focus-visible {
|
||||
outline: 2px solid var(--sl-color-accent-high);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
```
|
||||
|
||||
These variables already exist in `src/styles/incognito.css` and Starlight's public theme tokens. Keep the visible focus outline independent from the active fill.
|
||||
|
||||
- [ ] **Step 5: Run the component and navigation tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test tests/programme-switch.test.mjs tests/programme-navigation.test.mjs tests/base-aware-markdown.test.mjs
|
||||
```
|
||||
|
||||
Expected: all tests PASS.
|
||||
|
||||
- [ ] **Step 6: Build and run the complete verification suite**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npm run verify
|
||||
```
|
||||
|
||||
Expected: Astro checks, Node tests, content audit, production build, rendered-output checks, and internal-link checks all exit successfully.
|
||||
|
||||
- [ ] **Step 7: Check both responsive selector states manually**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Inspect a Computer Science route and a Data Science & AI route at desktop width and at a 285-pixel sidebar/mobile-navigation width. Confirm the short labels never wrap, only the selected curriculum and three shared destinations are present, the filled active state has sufficient contrast in light and dark themes, and keyboard focus is visible on both links. Stop the development server after the review.
|
||||
|
||||
- [ ] **Step 8: Commit the compact selector**
|
||||
|
||||
```bash
|
||||
git add tests/programme-switch.test.mjs src/components/ProgrammeSwitch.astro src/styles/incognito.css
|
||||
git commit -m "feat: compact programme selector"
|
||||
```
|
||||
Loading…
Add table
Reference in a new issue