feat: establish nightdrive visual foundation
This commit is contained in:
@@ -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"
|
||||
```
|
||||
|
||||
|
||||
23
docs/third-party-notices.md
Normal file
23
docs/third-party-notices.md
Normal 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
20
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user