feat: establish nightdrive visual foundation

This commit is contained in:
2026-09-02 13:55:01 +03:00
parent f1e156fba7
commit 24afda8710
8 changed files with 193 additions and 47 deletions

View File

@@ -65,51 +65,40 @@
**Files:**
- Modify: `package.json`
- Modify: `package-lock.json`
- Modify: `src/App.vue`
- Modify: `src/styles/tokens.css`
- Modify: `src/styles/base.css`
- Modify: `scripts/build-contract.mjs`
- Create: `docs/third-party-notices.md`
- Test: `tests/unit/toolchain.test.ts`
- Modify: `tests/unit/build-contract.test.ts`
- Modify: `tests/browser/semantic.spec.ts`
**Interfaces:**
- Consumes: existing Vite CSS import order and Vue application entry.
- Produces: `Geist Variable`, `Geist Mono`, Phosphor Vue components, Simple Icons exports, and stable CSS tokens used by every subsequent task.
- Consumes: existing Vite CSS pipeline, manifest closure validator, and generated preview.
- Produces: `Geist Variable`, `Geist Mono`, font-aware asset closure, and stable CSS tokens used by every subsequent task. Phosphor Vue and Simple Icons are installed only when their tested consumers are introduced in Tasks 7 and 10.
- [ ] **Step 1: Write dependency and typography assertions**
- [ ] **Step 1: Write the browser typography assertion**
Add assertions that exact direct dependencies exist and that the application imports the two font stylesheets before local CSS:
Load the generated preview, await `document.fonts.ready`, and assert the actual rendered heading and route-label families:
```ts
expect(packageJson.dependencies).toMatchObject({
'@fontsource-variable/geist': '5.3.0',
'@fontsource-variable/geist-mono': '5.3.0',
'@phosphor-icons/vue': '2.2.1',
'simple-icons': '16.29.0',
});
expect(appSource).toContain("@fontsource-variable/geist/wght.css");
expect(appSource).toContain("@fontsource-variable/geist-mono/wght.css");
expect(fonts.headingFamily).toContain('Geist Variable');
expect(fonts.routeFamily).toContain('Geist Mono Variable');
```
- [ ] **Step 2: Run the focused test and verify it fails**
Run: `npx vitest run tests/unit/toolchain.test.ts`
Run: `npm run build:preview && npx playwright test tests/browser/semantic.spec.ts --project=desktop-chromium --grep "loads the self-hosted Geist"`
Expected: FAIL because the dependencies and font imports are absent.
Expected: FAIL because rendered text still uses Inter and the system monospace stack.
- [ ] **Step 3: Install verified dependencies and import fonts**
- [ ] **Step 3: Install verified font dependencies and define only required subsets**
Run:
```bash
npm install --save-exact @fontsource-variable/geist@5.3.0 @fontsource-variable/geist-mono@5.3.0 @phosphor-icons/vue@2.2.1 simple-icons@16.29.0
npm install --save-exact @fontsource-variable/geist@5.3.0 @fontsource-variable/geist-mono@5.3.0
```
Add before local CSS in `src/App.vue`:
```ts
import '@fontsource-variable/geist/wght.css';
import '@fontsource-variable/geist-mono/wght.css';
```
Define Latin and Latin Extended Geist faces plus the Latin Geist Mono face in `src/styles/tokens.css`. Reference package-owned `.woff2` files directly so Vite emits only the three subsets used by visible content.
Define the stable visual tokens:
@@ -141,18 +130,18 @@ Define the stable visual tokens:
}
```
Record the package names, versions, licenses, copyright owners, and source URLs in `docs/third-party-notices.md`.
Teach `resolveViteAssetClosure` to accept a non-entry manifest record only when its emitted file is already named by the `assets` field of a reachable module. Keep unrelated records rejected. Record the font package names, versions, licenses, copyright owners, and source URLs in `docs/third-party-notices.md`.
- [ ] **Step 4: Run the focused test and static verification**
Run: `npx vitest run tests/unit/toolchain.test.ts && npm run typecheck`
Run: `npx vitest run tests/unit/build-contract.test.ts && npm run build:preview && npx playwright test tests/browser/semantic.spec.ts --project=desktop-chromium --grep "loads the self-hosted Geist"`
Expected: PASS.
Expected: PASS with `initialFonts` below the existing 122880-byte threshold.
- [ ] **Step 5: Commit the foundation**
```bash
git add package.json package-lock.json src/App.vue src/styles/tokens.css src/styles/base.css docs/third-party-notices.md tests/unit/toolchain.test.ts
git add package.json package-lock.json src/styles/tokens.css scripts/build-contract.mjs docs/third-party-notices.md tests/unit/build-contract.test.ts tests/browser/semantic.spec.ts docs/superpowers/plans/2026-09-02-portfolio-nightdrive-redesign-implementation.md
git commit -m "feat: establish nightdrive visual foundation"
```
@@ -538,6 +527,8 @@ git commit -m "feat: integrate the cinematic nightdrive scene"
### Task 7: Replace exposed controls with the compact vehicle HUD
**Files:**
- Modify: `package.json`
- Modify: `package-lock.json`
- Create: `src/components/IconGlyph.vue`
- Modify: `src/components/DriveControls.vue`
- Modify: `src/components/JourneyHud.vue`
@@ -573,6 +564,12 @@ Expected: FAIL because the current controls expose every setting and use text-on
- [ ] **Step 3: Implement the icon contract and control states**
Install the verified Vue interface-icon package:
```bash
npm install --save-exact @phosphor-icons/vue@2.2.1
```
Use tree-shaken Phosphor imports with a standard `20px`, weight `regular`, and visual stroke weight. Keep button labels in `aria-label` and tooltips, not duplicate visible strings.
```vue
@@ -603,7 +600,7 @@ Expected: PASS.
- [ ] **Step 6: Commit the HUD milestone**
```bash
git add src/components/IconGlyph.vue src/components/DriveControls.vue src/components/JourneyHud.vue src/components/AppShell.vue src/styles/world.css tests/unit/SceneCanvas.test.ts tests/browser/audio-controls.spec.ts tests/browser/scene-controls.spec.ts tests/browser/responsive-layout.spec.ts
git add package.json package-lock.json src/components/IconGlyph.vue src/components/DriveControls.vue src/components/JourneyHud.vue src/components/AppShell.vue src/styles/world.css tests/unit/SceneCanvas.test.ts tests/browser/audio-controls.spec.ts tests/browser/scene-controls.spec.ts tests/browser/responsive-layout.spec.ts docs/third-party-notices.md
git commit -m "feat: introduce the compact vehicle HUD"
```
@@ -770,6 +767,8 @@ git commit -m "feat: recompose the portfolio information layer"
### Task 10: Build the infrastructure flow and official icon registries
**Files:**
- Modify: `package.json`
- Modify: `package-lock.json`
- Create: `src/components/BrandIcon.vue`
- Create: `src/icons/brand-icons.ts`
- Modify: `src/components/InfrastructureDiagram.vue`
@@ -807,6 +806,12 @@ Expected: FAIL because the registry and real flow diagram do not exist.
- [ ] **Step 3: Implement brand and interface icon rendering**
Install the verified brand-data package:
```bash
npm install --save-exact simple-icons@16.29.0
```
Import named Simple Icons exports only in `brand-icons.ts`. Render package-provided paths through `BrandIcon.vue`; hide decorative marks from assistive technology and keep the visible technology or service name. Continue using Phosphor only for interface and diagram actions.
- [ ] **Step 4: Replace the four-box diagram with a flow**
@@ -828,7 +833,7 @@ Expected: PASS.
- [ ] **Step 6: Commit diagrams and iconography**
```bash
git add src/components/BrandIcon.vue src/icons/brand-icons.ts src/components/InfrastructureDiagram.vue src/components/ToolkitGrid.vue src/components/ContactDestination.vue src/styles/components.css tests/unit/brand-icons.test.ts tests/unit/components.test.ts tests/browser/accessibility-axe.spec.ts tests/browser/responsive-layout.spec.ts
git add package.json package-lock.json src/components/BrandIcon.vue src/icons/brand-icons.ts src/components/InfrastructureDiagram.vue src/components/ToolkitGrid.vue src/components/ContactDestination.vue src/styles/components.css tests/unit/brand-icons.test.ts tests/unit/components.test.ts tests/browser/accessibility-axe.spec.ts tests/browser/responsive-layout.spec.ts docs/third-party-notices.md
git commit -m "feat: add infrastructure flow and official iconography"
```

View File

@@ -0,0 +1,23 @@
# Third-party notices
This project includes the following third-party software and typefaces.
## Geist Variable
- Package: `@fontsource-variable/geist` version 5.3.0
- Typeface copyright: 2024 The Geist Project Authors
- Package source: https://github.com/fontsource/font-files
- Typeface source: https://github.com/vercel/geist-font
- License: SIL Open Font License 1.1
The complete license text is distributed in `node_modules/@fontsource-variable/geist/LICENSE` during development and dependency review.
## Geist Mono Variable
- Package: `@fontsource-variable/geist-mono` version 5.3.0
- Typeface copyright: 2024 The Geist Project Authors
- Package source: https://github.com/fontsource/font-files
- Typeface source: https://github.com/vercel/geist-font
- License: SIL Open Font License 1.1
The complete license text is distributed in `node_modules/@fontsource-variable/geist-mono/LICENSE` during development and dependency review.

20
package-lock.json generated
View File

@@ -9,6 +9,8 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@fontsource-variable/geist": "5.3.0",
"@fontsource-variable/geist-mono": "5.3.0",
"canonicalize": "2.1.0",
"three": "^0.185.1",
"vue": "^3.5.42",
@@ -332,6 +334,24 @@
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/@fontsource-variable/geist": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource-variable/geist/-/geist-5.3.0.tgz",
"integrity": "sha512-j0m+vLQuG5XAYoHtGCVu0spvlGreR3EzpECUVzkFmI1mTVnAO38l/NEPDCFgZ177JxzYJCLSmTQibIiYPilGrA==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource-variable/geist-mono": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource-variable/geist-mono/-/geist-mono-5.3.0.tgz",
"integrity": "sha512-vBbuwDEo9AkrqADMXOrlAR3DFcJi4/JxeuU43FoiQERnNwsfXNnvxvReZG02cQKmyk4DZkZdBZX3oTDvy2zBAw==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@formatjs/ecma402-abstract": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz",

View File

@@ -34,6 +34,8 @@
"author": "",
"license": "ISC",
"dependencies": {
"@fontsource-variable/geist": "5.3.0",
"@fontsource-variable/geist-mono": "5.3.0",
"canonicalize": "2.1.0",
"three": "^0.185.1",
"vue": "^3.5.42",

View File

@@ -73,7 +73,25 @@ export const resolveViteAssetClosure = (manifest) => {
};
visit('index.html', true);
const unreachable = Object.keys(manifest).filter((key) => !allReachable.has(key));
const reachableAssetFiles = new Set(
[...allReachable].flatMap((key) => manifest[key].assets ?? []),
);
const emittedAssetRecords = new Set(
Object.entries(manifest).flatMap(([key, record]) => {
const isEmittedAssetRecord =
reachableAssetFiles.has(record.file) &&
record.isEntry === undefined &&
record.isDynamicEntry === undefined &&
(record.imports?.length ?? 0) === 0 &&
(record.dynamicImports?.length ?? 0) === 0 &&
(record.css?.length ?? 0) === 0 &&
(record.assets?.length ?? 0) === 0;
return isEmittedAssetRecord ? [key] : [];
}),
);
const unreachable = Object.keys(manifest).filter(
(key) => !allReachable.has(key) && !emittedAssetRecords.has(key),
);
if (unreachable.length > 0) throw new Error(`Unreachable manifest record: ${unreachable.join(', ')}`);
const executableModules = [];

View File

@@ -1,29 +1,71 @@
@font-face {
font-family: 'Geist Variable';
font-style: normal;
font-display: swap;
font-weight: 100 900;
src: url('@fontsource-variable/geist/files/geist-latin-ext-wght-normal.woff2') format('woff2-variations');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: 'Geist Variable';
font-style: normal;
font-display: swap;
font-weight: 100 900;
src: url('@fontsource-variable/geist/files/geist-latin-wght-normal.woff2') format('woff2-variations');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Geist Mono Variable';
font-style: normal;
font-display: swap;
font-weight: 100 900;
src: url('@fontsource-variable/geist-mono/files/geist-mono-latin-wght-normal.woff2') format('woff2-variations');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
:root {
color-scheme: dark;
--canvas: #080d16;
--surface: #101722;
--surface-raised: #141e2b;
--line: #314154;
--line-strong: #4a6275;
--text: #f2f4f5;
--text-muted: #b5c0c9;
--cyan: #80d9e6;
--cyan-deep: #163b48;
--amber: #f5b752;
--focus: #ffd17a;
--sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
--canvas: #040814;
--surface: rgb(7 14 25 / 95%);
--surface-strong: rgb(7 14 25 / 97%);
--surface-soft: rgb(15 28 43 / 92%);
--surface-raised: var(--surface-soft);
--line: rgb(126 200 214 / 22%);
--line-strong: rgb(126 220 232 / 44%);
--text: #f3f7f8;
--text-muted: #aab9c2;
--accent: #78d8e5;
--accent-deep: #103745;
--focus: #ffd693;
--world-magenta: #c74b91;
--world-amber: #f4ad67;
--cyan: var(--accent);
--cyan-deep: var(--accent-deep);
--amber: var(--world-amber);
--sans: 'Geist Variable', ui-sans-serif, system-ui, sans-serif;
--mono: 'Geist Mono Variable', ui-monospace, monospace;
--step--1: clamp(0.82rem, 0.78rem + 0.14vw, 0.92rem);
--step-0: clamp(1rem, 0.94rem + 0.24vw, 1.15rem);
--step-1: clamp(1.3rem, 1.14rem + 0.68vw, 1.75rem);
--step-2: clamp(1.9rem, 1.5rem + 1.35vw, 2.85rem);
--step-3: clamp(2.45rem, 1.82rem + 2.25vw, 4rem);
--step-4: clamp(3.5rem, 2.2rem + 5.8vw, 7.5rem);
--space-1: clamp(0.5rem, 0.45rem + 0.2vw, 0.68rem);
--space-2: clamp(0.85rem, 0.75rem + 0.38vw, 1.15rem);
--space-3: clamp(1.3rem, 1.1rem + 0.7vw, 1.9rem);
--space-4: clamp(2rem, 1.65rem + 1.1vw, 3rem);
--space-5: clamp(3.25rem, 2.5rem + 2.6vw, 6rem);
--radius-surface: 12px;
--radius-control: 10px;
--panel-border: 1px solid var(--line);
--panel-shadow: 0 14px 34px rgb(0 0 0 / 18%);
--transition: 160ms ease;
--panel-shadow: 0 1.5rem 4rem rgb(0 7 19 / 34%);
--transition: 220ms cubic-bezier(0.2, 0.75, 0.2, 1);
--z-world: 0;
--z-atmosphere: 1;
--z-content: 2;
--z-navigation: 3;
--z-hud: 4;
--z-popover: 5;
}

View File

@@ -111,6 +111,25 @@ test('exposes the complete semantic portfolio from the generated document', asyn
).toBe(true);
});
test('loads the self-hosted Geist families for information and route text', async ({
page,
distUrl,
}) => {
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
await page.evaluate(() => document.fonts.ready);
const fonts = await page.evaluate(() => ({
sansLoaded: document.fonts.check('16px "Geist Variable"'),
monoLoaded: document.fonts.check('16px "Geist Mono Variable"'),
headingFamily: getComputedStyle(document.querySelector('h1')!).fontFamily,
routeFamily: getComputedStyle(document.querySelector('.route-label')!).fontFamily,
}));
expect(fonts).toMatchObject({ sansLoaded: true, monoLoaded: true });
expect(fonts.headingFamily).toContain('Geist Variable');
expect(fonts.routeFamily).toContain('Geist Mono Variable');
});
test('keeps canonical native anchors and safe external destinations', async ({
page,
distUrl,

View File

@@ -53,6 +53,23 @@ describe('trusted Vite asset closure', () => {
});
});
it('accepts Vite emitted-asset records when a reachable module owns the emitted file', () => {
const manifest = recursiveManifest();
manifest['index.html']!.assets = [
'assets/entry-logo-a1.webp',
'assets/geist-latin-a1.woff2',
];
manifest['node_modules/font/files/geist-latin.woff2'] = {
file: 'assets/geist-latin-a1.woff2',
src: 'node_modules/font/files/geist-latin.woff2',
};
expect(resolveViteAssetClosure(manifest).viteAssets).toEqual([
'assets/entry-logo-a1.webp',
'assets/geist-latin-a1.woff2',
]);
});
it.each([
{
name: 'a missing exact index.html entry',