Compare commits
No commits in common. "edca813c20f3a4e27ea077c8be4abf94fd43660e" and "1d4d832b6c22b4bb9d641507536fd49150b9bdfa" have entirely different histories.
edca813c20
...
1d4d832b6c
13 changed files with 71 additions and 765 deletions
|
|
@ -33,7 +33,6 @@ export default defineConfig({
|
|||
},
|
||||
favicon: '/favicon.ico',
|
||||
customCss: ['./src/styles/incognito.css'],
|
||||
routeMiddleware: ['./src/starlight-route-data.ts'],
|
||||
components: {
|
||||
Sidebar: './src/components/Sidebar.astro',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,327 +0,0 @@
|
|||
# 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"
|
||||
```
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
# Compact Programme Switch and Sidebar Filtering Design
|
||||
|
||||
## Goal
|
||||
|
||||
Replace the current two-column bachelor programme selector with a compact segmented control. Selecting a programme must also focus the sidebar on that programme's curriculum and a small set of shared destinations.
|
||||
|
||||
## Approved interaction
|
||||
|
||||
The switch appears above the Starlight navigation tree and contains two short labels:
|
||||
|
||||
- `DSAI` for Data Science & AI;
|
||||
- `CS` for Computer Science.
|
||||
|
||||
Both options remain ordinary links. The current programme is shown with a filled accent state and `aria-current="page"`. Switching programmes navigates to the existing route-aware destination: a paired course where one exists, otherwise the other programme's overview.
|
||||
|
||||
The control is navigation, not a client-side preference. Programme state continues to come from the current URL; it is not stored in cookies, local storage, or client JavaScript.
|
||||
|
||||
## Visual design
|
||||
|
||||
Use a small bordered container labelled `Bachelor programme`. Inside it, place `DSAI` and `CS` in an equal-width, two-segment row.
|
||||
|
||||
The inactive segment uses the sidebar's subdued text and background colors. The active segment uses a filled blue accent with high-contrast text. Do not repeat the active programme's full name above the segments because the selected state and curriculum heading already communicate it.
|
||||
|
||||
The control must fit the narrow mobile sidebar without either short label wrapping. It should use the existing Starlight and Incognito color variables so light and dark themes remain consistent.
|
||||
|
||||
## Accessible naming and keyboard behavior
|
||||
|
||||
Visible labels stay short, while each link exposes its full programme name to assistive technology. A tooltip may also expose the full name to pointer users, but it must not be the only source of the accessible name.
|
||||
|
||||
The current link retains `aria-current="page"`. Both links remain reachable and operable by keyboard without JavaScript. Keyboard focus receives a visible outline that is distinguishable from the selected state.
|
||||
|
||||
## Programme-specific sidebar contents
|
||||
|
||||
On a Computer Science route, render:
|
||||
|
||||
1. Home;
|
||||
2. Previous exams and documents;
|
||||
3. the complete Computer Science course tree;
|
||||
4. Useful Information and its existing child pages.
|
||||
|
||||
On a Data Science & AI route, render:
|
||||
|
||||
1. Home;
|
||||
2. Previous exams and documents;
|
||||
3. the complete Data Science & AI course tree;
|
||||
4. Useful Information and its existing child pages.
|
||||
|
||||
Hide the inactive bachelor programme, About Incognito, Master AI, and Master DSDM in both programme-specific views.
|
||||
|
||||
Global pages that are not associated with either bachelor programme retain the existing global sidebar. This keeps non-programme navigation usable and avoids arbitrarily choosing a programme when the URL supplies no programme context.
|
||||
|
||||
## Implementation boundaries
|
||||
|
||||
`ProgrammeSwitch.astro` owns only the accessible switch markup and presentation hooks. It consumes the current programme and switch destinations from the existing pure navigation helpers.
|
||||
|
||||
The route-aware sidebar configuration remains responsible for selecting and ordering navigation entries. It must produce focused programme navigation without duplicating course definitions or switch-destination logic in the component.
|
||||
|
||||
The Starlight `Sidebar` override continues to compose the programme switch with Starlight's default sidebar component. No client-side component or hydration directive is introduced.
|
||||
|
||||
## Data flow
|
||||
|
||||
1. The request pathname identifies the active programme.
|
||||
2. Existing navigation helpers calculate the two switch destinations.
|
||||
3. The switch renders ordinary links and marks the active programme.
|
||||
4. Middleware derives the resolved Starlight sidebar for the same pathname.
|
||||
5. Programme routes receive the selected curriculum plus the three approved shared destinations; global routes retain the global navigation.
|
||||
|
||||
Unknown or malformed paths must fall back to global navigation rather than showing an incorrect programme as selected.
|
||||
|
||||
## Verification
|
||||
|
||||
Automated tests should verify:
|
||||
|
||||
- the visible switch labels are `DSAI` and `CS`;
|
||||
- each link has a full accessible programme name;
|
||||
- the active link receives `aria-current="page"`;
|
||||
- the component contains no script or client hydration directive;
|
||||
- Computer Science routes include only the Computer Science course tree and the three shared destinations;
|
||||
- Data Science & AI routes include only the Data Science & AI course tree and the three shared destinations;
|
||||
- programme routes exclude About Incognito, Master AI, Master DSDM, and the inactive bachelor programme;
|
||||
- global routes retain the global navigation;
|
||||
- paired-course and fallback switch destinations continue to pass their existing route tests;
|
||||
- a production build renders the selector without horizontal overflow at narrow sidebar widths.
|
||||
|
||||
Manual review should check both programme states in desktop and mobile navigation, in light and dark themes, with keyboard focus visible.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Changing course content or curriculum structure;
|
||||
- changing the paired-course mapping;
|
||||
- storing programme selection as a user preference;
|
||||
- adding JavaScript to animate or hydrate the switch;
|
||||
- redesigning the rest of Starlight's navigation tree.
|
||||
|
|
@ -7,19 +7,9 @@ const withBase = (path: string) => `${import.meta.env.BASE_URL.replace(/\/$/, ''
|
|||
---
|
||||
|
||||
<nav class="programme-switch" aria-label="Bachelor programme">
|
||||
<span class="programme-switch__label">Bachelor programme</span>
|
||||
<span class="programme-switch__label">Course information</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>
|
||||
<a href={withBase(targets.dataScience)} aria-current={active === 'data-science-and-ai' ? 'page' : undefined}>Data Science & AI</a>
|
||||
<a href={withBase(targets.computerScience)} aria-current={active === 'computer-science' ? 'page' : undefined}>Computer Science</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ const computerScience = {
|
|||
};
|
||||
|
||||
const compact = { label: 'Bachelor programmes', items: [{ slug: 'computer-science' }, { slug: 'data-science-and-ai' }] };
|
||||
const programmeSharedLabels = new Set(['Home', 'Previous exams and documents', 'Useful Information']);
|
||||
|
||||
export function programmeForPathname(pathname) {
|
||||
if (pathname.startsWith('/computer-science')) return 'computer-science';
|
||||
|
|
@ -48,30 +47,20 @@ export function programmeSwitchTargets(pathname) {
|
|||
|
||||
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)];
|
||||
const dataScience = sidebar.find(({ label }) => label === 'Data Science & AI');
|
||||
const undergraduate = programme === 'computer-science' ? computerScience : programme === 'data-science-and-ai' ? dataScience : compact;
|
||||
return [...withoutProgrammes.slice(0, 3), undergraduate, ...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 entries.filter((entry) => {
|
||||
if (entry.label === 'Computer Science') return programme !== 'data-science-and-ai';
|
||||
if (entry.label === 'Data Science & AI') return programme !== 'computer-science';
|
||||
return true;
|
||||
}).map((entry) => {
|
||||
if (!programme && (entry.label === 'Computer Science' || entry.label === 'Data Science & AI') && 'entries' in entry) {
|
||||
return { ...entry, entries: entry.entries.slice(0, 1) };
|
||||
}
|
||||
return entry;
|
||||
|
|
|
|||
|
|
@ -3,204 +3,44 @@ title: Laptop Buying Advice
|
|||
description: Practical laptop-buying guidance for DACS students.
|
||||
---
|
||||
|
||||
A practical guide for students of the Department of Advanced Computing Sciences at Maastricht University, especially:
|
||||
MSV Incognito often receives questions from prospective students about the hardware needed for their studies. You do not need the most expensive laptop, but choosing a balanced machine will make computer labs, group projects, and longer programming tasks much more comfortable.
|
||||
|
||||
- BSc Computer Science
|
||||
- BSc Data Science and Artificial Intelligence
|
||||
- MSc Artificial Intelligence
|
||||
- MSc Data Science for Decision Making
|
||||
- MSc Responsible Data Science
|
||||
## Recommended baseline for a new laptop
|
||||
|
||||
> **Who is the AI hardware advice for?** The sections about CUDA, GPU memory, local model inference, and neural-network training are aimed mainly at BSc Data Science and Artificial Intelligence students, MSc Artificial Intelligence students, and anyone intentionally choosing AI- or deep-learning-heavy electives or research projects. They are not general hardware requirements for every DACS student. In particular, most Computer Science, Data Science for Decision Making, and Responsible Data Science coursework does not require an AI-focused laptop.
|
||||
- **Memory:** At least 16 GB RAM.
|
||||
- **Storage:** At least a 512 GB SSD.
|
||||
- **Processor:** A mid-range Core i5-class or equivalent processor, such as an Intel Core i5 or AMD Ryzen 5.
|
||||
- **Operating system:** Windows, Linux, and M-series Macs can all be suitable. Confirm course-specific software compatibility before purchasing.
|
||||
- **Data Science and AI workloads:** An Nvidia GPU is useful when coursework or personal projects use CUDA, but it is not required for every course.
|
||||
|
||||
## Short recommendation
|
||||
## Is a laptop required?
|
||||
|
||||
For a **new laptop**, we recommend at least:
|
||||
A laptop is required for computer labs in certain courses and is useful for group projects. You will use it to write, compile, and run software in languages such as Java, Python, and C. Lower-end hardware may still work, but compilation, data processing, simulations, and machine-learning workloads can take longer.
|
||||
|
||||
- **16 GB RAM** (32 GB is preferable for serious local AI, large datasets, virtual machines, or a laptop you intend to keep for several years)
|
||||
- **512 GB SSD storage** (1 TB is preferable for datasets, virtual machines, containers, and local AI models)
|
||||
- A recent **mid-range or better processor**, such as an Intel Core Ultra 5/Core i5, AMD Ryzen 5, Apple M-series chip, or equivalent
|
||||
- A recent version of **Windows, macOS, or Linux**
|
||||
- A good keyboard, screen, battery, and build quality: you will use this machine every day
|
||||
Chromebooks are generally not recommended because some required development tools and course applications may be difficult or impossible to install. A laptop that runs Windows, Linux, or macOS gives you more flexibility.
|
||||
|
||||
For most coursework, **16 GB RAM and 512 GB storage are sufficient**. You do not need an expensive gaming laptop merely to complete your degree. University servers and cloud resources may be used for workloads that are too large for a laptop.
|
||||
## Practical considerations
|
||||
|
||||
If you specifically want to train neural networks locally or work with software that expects CUDA, choose a laptop with an **NVIDIA RTX GPU**. Check its VRAM, not only the GPU model name.
|
||||
### Screen and portability
|
||||
|
||||
## Do you need a laptop?
|
||||
A 14- or 15-inch laptop is a common compromise between readable code, a comfortable keyboard, and portability. Smaller screens can feel cramped during programming work, while 17-inch laptops are heavier to carry. Screen quality and resolution matter when you spend many hours reading code and documents.
|
||||
|
||||
Yes. A laptop is used in computer labs, group projects, assignments, and presentations. It should be able to run development tools for languages such as Python, Java, C/C++, and R, as well as browsers, office software, containers, and possibly virtual machines.
|
||||
### Keyboard layout
|
||||
|
||||
The programmes do not all have the same emphasis. See [Suggested configurations by programme](#suggested-configurations-by-programme) for specific advice.
|
||||
Choose the keyboard layout you already use comfortably—for example, QWERTY, QWERTZ, or AZERTY. Changing layouts while learning to program adds unnecessary friction.
|
||||
|
||||
Course requirements can change. If a particular course publishes hardware or operating-system requirements, follow those over this general guide.
|
||||
### Battery life and build quality
|
||||
|
||||
## Memory and storage
|
||||
Battery life, weight, keyboard quality, cooling, and repairability can matter more in daily student life than a small difference in processor speed. Read independent reviews of the exact model you are considering, since laptops with similar specifications can differ substantially in these areas.
|
||||
|
||||
### RAM
|
||||
### Graphics hardware
|
||||
|
||||
**16 GB is the minimum we recommend for a new laptop in 2026.** Avoid buying an 8 GB machine unless it is an inexpensive temporary solution and its memory can be upgraded. Many thin laptops have soldered memory, so check before buying.
|
||||
A dedicated graphics card is not necessary for ordinary programming, writing, or most mathematics courses. It becomes more valuable for gaming, computer vision, deep learning, 3D work, or other GPU-accelerated tasks. For CUDA-based software, an Nvidia GPU offers the broadest compatibility.
|
||||
|
||||
Choose **32 GB** if you expect to use several of the following at once:
|
||||
## Choosing within your budget
|
||||
|
||||
- IDEs, notebooks, and many browser tabs
|
||||
- Docker containers or virtual machines
|
||||
- Large datasets or in-memory data processing
|
||||
- Local language, vision, or generative models
|
||||
- GPU workloads that can offload data to system memory
|
||||
Prioritize enough memory and storage before paying for a small processor upgrade. A balanced laptop with 16 GB RAM, a 512 GB SSD, a recent mid-range processor, a good keyboard, and dependable battery life is usually a better student machine than a faster processor paired with too little memory or storage.
|
||||
|
||||
For unusually large local workloads, 64 GB may be useful, but it is not a general degree requirement.
|
||||
If your budget is tight, consider a well-maintained refurbished business laptop with upgradeable storage or memory. Check the battery condition, warranty, charger, keyboard layout, and whether replacement parts are available before buying.
|
||||
|
||||
### SSD storage
|
||||
|
||||
**512 GB is the minimum we recommend.** Operating systems, development environments, Docker images, datasets, virtual machines, and model weights consume space quickly. Choose **1 TB** if the upgrade is affordable, especially when storage cannot be replaced later.
|
||||
|
||||
Prefer an SSD over a hard drive. External storage is useful for archives and backups, but it is less convenient for active projects and datasets.
|
||||
|
||||
## Understanding GPUs for AI and machine learning
|
||||
|
||||
This section is primarily relevant to DSAI and AI-focused students. Other students may safely treat it as optional buying advice unless their electives, thesis, research, or personal projects involve machine learning.
|
||||
|
||||
The GPU is the most confusing part of buying an AI laptop. A GPU can make some machine-learning workloads much faster, but many introductory algorithms and data-science tasks still run on the CPU. A fast GPU also cannot compensate for insufficient memory.
|
||||
|
||||
### NVIDIA GPUs: the safest option for local training
|
||||
|
||||
NVIDIA remains the most compatible choice for deep learning because CUDA is widely assumed by research code, libraries, tutorials, and course projects. If local training matters to you, look for a recent **GeForce RTX** laptop GPU.
|
||||
|
||||
Pay close attention to **VRAM**:
|
||||
|
||||
- **6 GB:** usable for learning and small models, but restrictive
|
||||
- **8 GB:** a reasonable entry point for smaller deep-learning experiments
|
||||
- **12 GB or more:** preferable for local training, larger models, and more flexibility
|
||||
- **16 GB or more:** excellent for a laptop, but expensive and still not enough for every workload
|
||||
|
||||
Laptop GPUs with the same product family name can have different power limits and performance. Read independent reviews of the exact laptop, not just the GPU specification.
|
||||
|
||||
### Apple GPUs: strong inference, different training ecosystem
|
||||
|
||||
Apple silicon MacBooks are fast, quiet, and power-efficient. Their CPU and GPU share a pool of **unified memory**, which can make a Mac with sufficient memory particularly good for running quantised local language models and other inference workloads. There is no separate VRAM capacity in the same sense as on a discrete NVIDIA laptop GPU.
|
||||
|
||||
Apple GPUs do **not support CUDA**. PyTorch can accelerate both inference and training through the MPS/Metal backend, and many popular local-inference applications support Apple silicon. However, some operations, libraries, research repositories, and course instructions are written specifically for CUDA. They may need workarounds, fall back to the CPU, or not work at all.
|
||||
|
||||
In short: a MacBook is a good general development laptop and can be excellent for local inference. It is less convenient for CUDA-based training and reproducibility. If buying one for AI work, prefer **at least 16 GB unified memory**, and seriously consider **24 GB or more**. Remember that macOS and applications also use this same memory pool.
|
||||
|
||||
### AMD GPUs
|
||||
|
||||
AMD GPUs can accelerate machine learning through ROCm and other backends. Support has improved substantially, including support for selected Radeon GPUs and Ryzen AI processors, but it remains more dependent on the exact GPU, operating system, framework, and software version than CUDA.
|
||||
|
||||
AMD can be a good option for an informed buyer who has checked the current compatibility matrix for the exact model. It is not the safest default when you need arbitrary research code to work without modification. Integrated Radeon graphics share system RAM and are mainly suited to light acceleration and inference, not heavy training.
|
||||
|
||||
### Intel GPUs
|
||||
|
||||
Recent Intel Arc discrete GPUs and Intel integrated GPUs can accelerate supported AI workloads through technologies such as the PyTorch XPU backend, oneAPI, OpenVINO, and platform-specific inference runtimes. They are increasingly useful for inference and experimentation.
|
||||
|
||||
As with AMD, compatibility depends on the exact hardware and software stack. Intel GPUs do not provide CUDA, and code written only for CUDA may require changes. An Intel Arc GPU can be a reasonable value choice for supported workloads, but NVIDIA remains the lower-friction option for general local deep-learning training.
|
||||
|
||||
### Integrated GPUs and NPUs
|
||||
|
||||
Integrated GPUs from Intel and AMD share system RAM with the CPU. They are useful for display work, media, light GPU computing, and some optimised inference workloads. Newer integrated GPUs are much more capable than older ones, but they usually have lower sustained performance and memory bandwidth than a good discrete GPU.
|
||||
|
||||
Many new processors also include an **NPU**. NPUs are efficient for supported on-device inference, such as transcription, image processing, or operating-system AI features. They are not a replacement for a CUDA GPU for general model training, and framework support varies.
|
||||
|
||||
An integrated GPU or NPU is perfectly adequate if you plan to do heavy computation on university servers or in the cloud. Do not pay a large premium for an "AI PC" label without checking which tools can actually use its NPU.
|
||||
|
||||
## VRAM, shared memory, and model size
|
||||
|
||||
For AI workloads, available memory often matters more than peak GPU speed. Model weights, activations, gradients, optimiser state, and data batches all consume memory. Training normally needs far more memory than inference.
|
||||
|
||||
On a laptop with a discrete GPU, **VRAM and system RAM are separate**. Some frameworks can offload model layers or data from VRAM into system RAM when the model does not fit. This is not guaranteed to happen automatically, and it is much slower because data must move between the CPU and GPU. If you intend to rely on offloading, buy enough system RAM as well: **32 GB or more is sensible**.
|
||||
|
||||
Apple silicon and some integrated-GPU systems use shared or unified memory. This can allow the GPU to access a larger memory pool, but the operating system and applications need part of that pool too. Large memory capacity makes a model possible to run; it does not necessarily make it fast.
|
||||
|
||||
Quantisation can greatly reduce the memory required for inference. Large-scale training, full fine-tuning, and high-throughput experiments generally belong on a server, desktop workstation, or cloud platform rather than a student laptop.
|
||||
|
||||
## Chromebooks, tablets, and unusual devices
|
||||
|
||||
We do **not recommend a Chromebook as your primary study laptop**. Some Chromebooks provide a Linux development environment and can run editors, terminals, and lightweight programming tools, but compatibility with virtualisation, containers, specialised drivers, required desktop software, and GPU computing is inconsistent. Storage and RAM are often limited as well.
|
||||
|
||||
An Android tablet or iPad is useful as a companion for notes and reading, but it should not be your only computer. Windows-on-ARM laptops may be excellent portable machines, but check that required development tools, virtualisation software, and drivers support ARM before buying.
|
||||
|
||||
If you already own one of these devices, it may cover part of the programme with remote access to another machine. That is different from recommending it as a safe new purchase.
|
||||
|
||||
## Operating system
|
||||
|
||||
- **Windows:** broadly compatible and supports NVIDIA CUDA. WSL 2 provides a convenient Linux environment, although virtualisation and storage require extra disk space.
|
||||
- **Linux:** excellent for development and closely matches many servers. Hardware support, battery life, fingerprint readers, sleep, and vendor utilities vary by laptop model.
|
||||
- **macOS:** excellent Unix-based development environment and strong Apple-silicon efficiency. It does not support CUDA, and some x86-only or Windows-only tools may require alternatives or virtualisation.
|
||||
|
||||
No operating system is universally best. Choose one you can work with, while considering the GPU software you expect to use.
|
||||
|
||||
## Other things worth checking
|
||||
|
||||
- **Screen:** 14 to 16 inches is a practical range. Prefer a readable resolution and enough brightness over an unnecessarily high refresh rate.
|
||||
- **Keyboard layout:** make sure it is one you are comfortable using, such as QWERTY, QWERTZ, or AZERTY.
|
||||
- **Battery and weight:** a powerful gaming laptop may spend much of the day near a power socket and can be tiring to carry.
|
||||
- **Cooling:** sustained CPU and GPU performance depends on cooling. Thin designs may slow down under long workloads.
|
||||
- **Ports and charging:** check for the displays, USB devices, and chargers you use.
|
||||
- **Repairability:** replaceable storage, memory, and battery can extend the useful life of a laptop.
|
||||
- **Warranty:** international students should check where warranty service is available.
|
||||
|
||||
## Suggested configurations by programme
|
||||
|
||||
These specifications are intended for a **new primary laptop**. "Minimum" means a practical floor for completing the programme, not the cheapest computer that could technically run an editor. "Recommended" provides more room for several years of coursework, development tools, datasets, containers, and virtual machines.
|
||||
|
||||
| Programme | Minimum | Recommended | GPU advice |
|
||||
| --- | --- | --- | --- |
|
||||
| **BSc Computer Science** | 16 GB RAM, 512 GB SSD | 32 GB RAM, 1 TB SSD | A dedicated GPU is recommended, especially for graphics, game development, computer vision, parallel computing, and AI electives. It is not required for most core programming work. |
|
||||
| **BSc Data Science and Artificial Intelligence** | 16 GB RAM, 512 GB SSD | 32 GB RAM, 1 TB SSD | For local deep learning, choose an NVIDIA GPU with at least 8 GB VRAM. 12 GB or more provides substantially more flexibility. |
|
||||
| **MSc Artificial Intelligence** | 16 GB RAM, 512 GB SSD | 32 GB RAM, 1 TB SSD | An NVIDIA GPU with 8 GB VRAM is the practical entry point for local AI work; 12 GB or more is strongly preferred. Large training jobs will still require university or cloud compute. |
|
||||
| **MSc Data Science for Decision Making** | 16 GB RAM, 512 GB SSD | 32 GB RAM, 1 TB SSD | Prioritise RAM and CPU performance for data processing, optimisation, notebooks, and containers. A dedicated GPU is optional unless electives, thesis work, or other projects involve deep learning. |
|
||||
| **MSc Responsible Data Science** | 16 GB RAM, 512 GB SSD | 32 GB RAM, 1 TB SSD if the budget allows | Prioritise RAM, CPU performance, portability, battery life, and software compatibility. A dedicated GPU depends on electives, thesis work, and research direction. |
|
||||
|
||||
### Choosing the CPU
|
||||
|
||||
Choose a recent mid-range or better processor, such as an Intel Core Ultra 5/Core i5, AMD Ryzen 5, Apple M-series chip, or equivalent. Avoid Intel N-series, Celeron, Pentium, and similarly low-end processors in a new primary laptop.
|
||||
|
||||
A Core Ultra 7/Core i7, Ryzen 7, or higher-tier Apple chip can help with compilation, data processing, and CPU-based modelling, but do not overpay for the CPU if it forces you to accept too little RAM, storage, or GPU memory. Cooling also matters: a high-end processor in a very thin laptop may not sustain its advertised performance.
|
||||
|
||||
### Choosing a dedicated GPU
|
||||
|
||||
For BSc Computer Science, "dedicated GPU recommended" does not mean that any dedicated GPU is worth buying. Do not sacrifice RAM, storage, battery life, or build quality merely to obtain a weak GPU. If AI or GPU computing is the reason for buying one, an NVIDIA RTX GPU is generally preferable because of CUDA support.
|
||||
|
||||
For local AI work, use the following VRAM guidance:
|
||||
|
||||
- **Less than 8 GB:** suitable for light acceleration and small experiments, but difficult to recommend for a new AI-focused laptop
|
||||
- **8 GB:** practical entry point for smaller training jobs, computer vision, and modest local inference
|
||||
- **12 GB:** preferred starting point for meaningful local AI experimentation
|
||||
- **16 GB or more:** desirable for larger models and workloads, although it still does not replace server hardware
|
||||
|
||||
A faster GPU with insufficient VRAM may be less useful for AI than a slower GPU with more VRAM. Always verify the VRAM and power limit of the exact laptop GPU; the product name alone is not enough.
|
||||
|
||||
### Apple configuration for local inference
|
||||
|
||||
For a MacBook, consider the unified-memory capacity separately from the minimum needed for coursework:
|
||||
|
||||
- **16 GB:** coursework, development, and light local inference
|
||||
- **24-32 GB:** a practical range for local inference and moderate models
|
||||
- **48-64 GB or more:** useful for larger quantised models that cannot fit in typical laptop VRAM
|
||||
|
||||
The operating system and applications share this memory with the GPU, so not all of it is available to the model. More unified memory can make a model possible to run, but does not make macOS compatible with CUDA-based software.
|
||||
|
||||
### Budget priority
|
||||
|
||||
If the budget cannot cover every upgrade, use this general order:
|
||||
|
||||
1. Meet the **16 GB RAM and 512 GB SSD minimum**.
|
||||
2. Increase RAM to **32 GB**, particularly for DSAI and MSc programmes.
|
||||
3. Increase storage to **1 TB**.
|
||||
4. Add an appropriate dedicated GPU if your intended work benefits from it.
|
||||
|
||||
For an AI-focused purchase, GPU memory can move ahead of SSD capacity once the minimum storage requirement is met. Avoid buying a powerful GPU in a system with inadequate RAM, storage, cooling, or battery life.
|
||||
|
||||
## Final advice
|
||||
|
||||
Do not choose a laptop from CPU or GPU branding alone. Check the exact amount of RAM, SSD storage, GPU memory, power limits, upgradeability, battery life, and independent reviews of the complete model.
|
||||
|
||||
For most students, **16 GB RAM, a 512 GB SSD, and a recent mid-range processor are the floor**. If your budget allows one major upgrade, 32 GB RAM or a 1 TB SSD will often improve everyday student life more than a low-end dedicated GPU. Choose NVIDIA when local CUDA work is a priority; choose Apple silicon when portability, battery life, and local inference matter more than CUDA compatibility.
|
||||
|
||||
## Further reading
|
||||
|
||||
- [PyTorch MPS backend for Apple GPUs](https://docs.pytorch.org/docs/stable/notes/mps.html)
|
||||
- [PyTorch XPU support for Intel GPUs](https://docs.pytorch.org/docs/stable/xpu.html)
|
||||
- [AMD ROCm compatibility for Radeon and Ryzen](https://rocm.docs.amd.com/projects/radeon/en/latest/docs/compatibility.html)
|
||||
- [NVIDIA CUDA GPU compatibility](https://developer.nvidia.com/cuda/gpus)
|
||||
Gaming laptops can provide powerful CPUs and GPUs, but they are often heavier, louder, and shorter-lived on battery. Only pay that portability cost if you expect to use the additional graphics performance.
|
||||
|
|
|
|||
12
src/middleware.ts
Normal file
12
src/middleware.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
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();
|
||||
});
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
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();
|
||||
});
|
||||
|
|
@ -134,7 +134,7 @@
|
|||
}
|
||||
|
||||
.programme-switch {
|
||||
padding: 0.625rem;
|
||||
padding: 0.75rem;
|
||||
margin: 0 0.5rem 0.75rem;
|
||||
border: 1px solid var(--sl-color-gray-5);
|
||||
border-radius: 0.6rem;
|
||||
|
|
@ -142,7 +142,7 @@
|
|||
|
||||
.programme-switch__label {
|
||||
display: block;
|
||||
margin-bottom: 0.4rem;
|
||||
margin-bottom: 0.45rem;
|
||||
color: var(--sl-color-gray-2);
|
||||
font-size: var(--sl-text-xs);
|
||||
font-weight: 600;
|
||||
|
|
@ -151,32 +151,25 @@
|
|||
.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);
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.programme-switch__links a {
|
||||
display: flex;
|
||||
min-height: 2.25rem;
|
||||
min-height: 2.5rem;
|
||||
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;
|
||||
padding: 0.35rem;
|
||||
border-radius: 0.4rem;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.programme-switch__links a[aria-current='page'] {
|
||||
color: var(--incognito-light);
|
||||
background: var(--incognito-primary);
|
||||
color: var(--sl-color-text-accent);
|
||||
background: var(--sl-color-accent-low);
|
||||
font-weight: 700;
|
||||
box-shadow: var(--sl-shadow-sm);
|
||||
box-shadow: inset 0 0 0 1px var(--sl-color-accent);
|
||||
}
|
||||
|
||||
.programme-switch__links a:focus-visible {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ async function collectCss(directory) {
|
|||
return chunks.join('\n');
|
||||
}
|
||||
|
||||
test('subpath production build includes local UI assets and focused programme navigation', async () => {
|
||||
test('subpath production build includes the local consent controller and its UI styles', async () => {
|
||||
const output = await mkdtemp(join(process.cwd(), '.matomo-build-'));
|
||||
try {
|
||||
await execFileAsync('node_modules/.bin/astro', ['build', '--outDir', output], {
|
||||
|
|
@ -40,32 +40,6 @@ test('subpath production build includes local UI assets and focused programme na
|
|||
assert.equal(scripts[0].hasAttribute('defer'), true);
|
||||
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);
|
||||
assert.match(css, /#incognito-analytics-consent/);
|
||||
assert.match(css, /\.incognito-consent-action/);
|
||||
|
|
|
|||
|
|
@ -1,64 +1,17 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { filterResolvedSidebar, programmeForPathname, programmeSwitchTargets, sidebarForPathname } from '../src/config/programme-navigation.mjs';
|
||||
import { programmeForPathname, programmeSwitchTargets, sidebarForPathname } from '../src/config/programme-navigation.mjs';
|
||||
|
||||
test('programme routes identify the active bachelor programme', () => {
|
||||
test('programme routes select the matching sidebar while retaining global groups', () => {
|
||||
assert.equal(programmeForPathname('/computer-science/year-1/'), 'computer-science');
|
||||
assert.equal(programmeForPathname('/data-science-and-ai/year-2/'), 'data-science-and-ai');
|
||||
assert.equal(programmeForPathname('/useful-information/'), null);
|
||||
});
|
||||
|
||||
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);
|
||||
for (const path of ['/computer-science/', '/data-science-and-ai/', '/useful-information/']) {
|
||||
const text = JSON.stringify(sidebarForPathname(path));
|
||||
assert.match(text, /Useful Information/);
|
||||
assert.match(text, /Master AI/);
|
||||
assert.match(text, /previous-exams-and-documents/);
|
||||
}
|
||||
});
|
||||
|
||||
test('shared course switches to its paired programme route', () => {
|
||||
|
|
|
|||
|
|
@ -10,15 +10,3 @@ test('Sidebar override renders an accessible no-JavaScript programme switch', as
|
|||
assert.doesNotMatch(component, /client:|<script/);
|
||||
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>/);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,16 +4,14 @@ import test from 'node:test';
|
|||
|
||||
const docsRoot = 'src/content/docs/useful-information';
|
||||
|
||||
test('Laptop Buying Advice publishes the 2026 hardware guidance without a dated warning', async () => {
|
||||
test('Laptop Buying Advice publishes the newer hardware baseline without a dated warning', async () => {
|
||||
const content = await readFile(`${docsRoot}/laptop-buying-advice.md`, 'utf8');
|
||||
|
||||
assert.match(content, /16 GB/);
|
||||
assert.match(content, /512 GB/);
|
||||
assert.match(content, /Core Ultra 5\/Core i5/);
|
||||
assert.match(content, /Apple M-series chip/);
|
||||
assert.match(content, /NVIDIA[\s\S]*CUDA/);
|
||||
assert.match(content, /Suggested configurations by programme/);
|
||||
assert.match(content, /MSc Responsible Data Science/);
|
||||
assert.match(content, /Core i5-class or equivalent/i);
|
||||
assert.match(content, /M-series Mac/i);
|
||||
assert.match(content, /Nvidia[\s\S]*CUDA/i);
|
||||
assert.doesNotMatch(content, /:::caution\[Historical information\]/);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue