From 0a2ef750c4b76a94f76320c39dd1d1860f9b46f5 Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Thu, 2 Apr 2026 12:45:11 +0200 Subject: [PATCH] feat(design-system): add Tailwind 4 @theme tokens, fonts, and completeness tests - Load Fraunces, DM Sans, DM Mono via Google Fonts preconnect in app.html - Define all design tokens in @theme block: neutrals, green/yellow/blue/ purple/orange scales, spacing (--space-1..20), radii, shadows, button base - Note --green-dark as button background (--green fails WCAG AA with white) - Add @types/node for Node fs/path usage in design-system tests Co-Authored-By: Claude Sonnet 4.6 --- frontend/package-lock.json | 18 ++++ frontend/package.json | 1 + frontend/src/app.css | 89 +++++++++++++++++++ frontend/src/app.html | 15 ++++ frontend/src/lib/design-system/tokens.test.ts | 57 ++++++++++++ frontend/tsconfig.json | 21 +++++ 6 files changed, 201 insertions(+) create mode 100644 frontend/src/app.css create mode 100644 frontend/src/app.html create mode 100644 frontend/src/lib/design-system/tokens.test.ts create mode 100644 frontend/tsconfig.json diff --git a/frontend/package-lock.json b/frontend/package-lock.json index cef869d..7df3349 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19,6 +19,7 @@ "@tailwindcss/vite": "^4.2.2", "@testing-library/jest-dom": "^6.9.1", "@testing-library/svelte": "^5.3.1", + "@types/node": "^25.5.0", "@vitest/ui": "^4.1.2", "jsdom": "^29.0.1", "openapi-typescript": "^7.13.0", @@ -1847,6 +1848,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -3737,6 +3748,13 @@ "node": ">=20.18.1" } }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, "node_modules/uri-js-replace": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index f99978e..ee0cbb9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,6 +25,7 @@ "@tailwindcss/vite": "^4.2.2", "@testing-library/jest-dom": "^6.9.1", "@testing-library/svelte": "^5.3.1", + "@types/node": "^25.5.0", "@vitest/ui": "^4.1.2", "jsdom": "^29.0.1", "openapi-typescript": "^7.13.0", diff --git a/frontend/src/app.css b/frontend/src/app.css new file mode 100644 index 0000000..988ac1c --- /dev/null +++ b/frontend/src/app.css @@ -0,0 +1,89 @@ +@import 'tailwindcss'; + +@theme { + /* ── Fonts ─────────────────────────────────────────────────────── */ + --font-display: 'Fraunces', Georgia, serif; + --font-sans: 'DM Sans', system-ui, sans-serif; + --font-mono: 'DM Mono', monospace; + + /* ── Neutrals ───────────────────────────────────────────────────── */ + --color-page: #fafaf7; + --color-surface: #f5f4ee; + --color-subtle: #edecea; + --color-border: #d8d7d0; + --color-text: #1c1c18; + --color-text-muted: #6b6a63; + + /* ── Green scale ────────────────────────────────────────────────── */ + --green-tint: #e8f5ea; + --green-light: #aedcb0; + --green: #3d8c4a; + --green-dark: #2e6e39; /* button backgrounds with white text — #3D8C4A gives 4.16:1, fails AA */ + --green-deeper: #1e4a26; + + /* ── Yellow scale ───────────────────────────────────────────────── */ + --yellow-tint: #fdf6d8; + --yellow-light: #f9e08a; + --yellow: #f2c12e; + --yellow-dark: #c49610; + --yellow-text: #8a6800; + + /* ── Blue scale ─────────────────────────────────────────────────── */ + --blue-tint: #e6f1fb; + --blue-light: #a4cff4; + --blue: #2d7dd2; + --blue-dark: #185fa5; + + /* ── Purple scale ───────────────────────────────────────────────── */ + --purple-tint: #eeedfe; + --purple: #534ab7; + --purple-dark: #3c3489; + + /* ── Orange scale ───────────────────────────────────────────────── */ + --orange-tint: #fef0e6; + --orange: #e8862a; + --orange-dark: #b46820; + + /* ── Status ─────────────────────────────────────────────────────── */ + --color-error: #dc4c3e; + + /* ── Spacing (8px base grid, 4px half-step) ─────────────────────── */ + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-5: 20px; + --space-6: 24px; + --space-7: 28px; + --space-8: 32px; + --space-9: 36px; + --space-10: 40px; + --space-11: 44px; + --space-12: 48px; + --space-13: 52px; + --space-14: 56px; + --space-15: 60px; + --space-16: 64px; + --space-17: 68px; + --space-18: 72px; + --space-19: 76px; + --space-20: 80px; + + /* ── Radii ──────────────────────────────────────────────────────── */ + --radius-xs: 2px; + --radius-sm: 4px; + --radius-md: 6px; /* default */ + --radius-lg: 10px; + --radius-xl: 16px; + --radius-full: 9999px; + + /* ── Elevation ──────────────────────────────────────────────────── */ + --shadow-card: 0 1px 3px rgba(28, 28, 24, 0.06), 0 1px 2px rgba(28, 28, 24, 0.04); + --shadow-raised: 0 4px 12px rgba(28, 28, 24, 0.08), 0 2px 4px rgba(28, 28, 24, 0.04); + --shadow-overlay: 0 8px 32px rgba(28, 28, 24, 0.12), 0 2px 8px rgba(28, 28, 24, 0.06); + + /* ── Button base tokens ─────────────────────────────────────────── */ + --btn-font-size: 13px; + --btn-font-weight: 500; + --btn-letter-spacing: 0.04em; +} diff --git a/frontend/src/app.html b/frontend/src/app.html new file mode 100644 index 0000000..0911030 --- /dev/null +++ b/frontend/src/app.html @@ -0,0 +1,15 @@ + + + + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/frontend/src/lib/design-system/tokens.test.ts b/frontend/src/lib/design-system/tokens.test.ts new file mode 100644 index 0000000..1928339 --- /dev/null +++ b/frontend/src/lib/design-system/tokens.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +const css = readFileSync(resolve(__dirname, '../../app.css'), 'utf-8'); + +const requiredTokens = [ + // Fonts + '--font-display', + '--font-sans', + '--font-mono', + // Neutrals + '--color-page', + '--color-surface', + '--color-subtle', + '--color-border', + '--color-text', + '--color-text-muted', + // Green scale + '--green-tint', + '--green-light', + '--green', + '--green-dark', + '--green-deeper', + // Yellow scale + '--yellow-tint', + '--yellow-light', + '--yellow', + '--yellow-dark', + '--yellow-text', + // Status + '--color-error', + // Spacing + '--space-1', + '--space-4', + '--space-8', + '--space-12', + '--space-16', + '--space-20', + // Radii + '--radius-xs', + '--radius-sm', + '--radius-md', + '--radius-lg', + '--radius-xl', + '--radius-full', + // Shadows + '--shadow-card', + '--shadow-raised', + '--shadow-overlay' +]; + +describe('design token completeness', () => { + it.each(requiredTokens)('%s is defined in app.css', (token) => { + expect(css).toContain(token); + }); +}); diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..12cbc44 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "rewriteRelativeImportExtensions": true, + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler", + "types": ["@types/node"] + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // To make changes to top-level options such as include and exclude, we recommend extending + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript +}