diff --git a/web/public/fonts/Geist-Bold.woff2 b/web/public/fonts/Geist-Bold.woff2 new file mode 100644 index 0000000..88f092d Binary files /dev/null and b/web/public/fonts/Geist-Bold.woff2 differ diff --git a/web/public/fonts/Geist-Medium.woff2 b/web/public/fonts/Geist-Medium.woff2 new file mode 100644 index 0000000..76d44da Binary files /dev/null and b/web/public/fonts/Geist-Medium.woff2 differ diff --git a/web/public/fonts/Geist-Regular.woff2 b/web/public/fonts/Geist-Regular.woff2 new file mode 100644 index 0000000..3eb6a71 Binary files /dev/null and b/web/public/fonts/Geist-Regular.woff2 differ diff --git a/web/public/fonts/GeistMono-Medium.woff2 b/web/public/fonts/GeistMono-Medium.woff2 new file mode 100644 index 0000000..853642f Binary files /dev/null and b/web/public/fonts/GeistMono-Medium.woff2 differ diff --git a/web/public/fonts/GeistMono-Regular.woff2 b/web/public/fonts/GeistMono-Regular.woff2 new file mode 100644 index 0000000..d9d3833 Binary files /dev/null and b/web/public/fonts/GeistMono-Regular.woff2 differ diff --git a/web/public/fonts/NOTICE.md b/web/public/fonts/NOTICE.md new file mode 100644 index 0000000..bf8cef3 --- /dev/null +++ b/web/public/fonts/NOTICE.md @@ -0,0 +1,7 @@ +# Vendored fonts + +Geist and Geist Mono are © Vercel, Inc., licensed under SIL Open Font License 1.1. + +Source: https://github.com/vercel/geist-font + +Vendored locally per the air-gap constraint (no CDN font loads in production). diff --git a/web/src/tokens/colors.ts b/web/src/tokens/colors.ts new file mode 100644 index 0000000..49fdb77 --- /dev/null +++ b/web/src/tokens/colors.ts @@ -0,0 +1,27 @@ +// web/src/tokens/colors.ts +export const colors = { + bgPage: '#FBFAF6', + bgElev: '#FFFFFF', + bgSubtle: '#F4F2EC', + bgDeep: '#ECE7DB', + bgTint: '#FAF6EA', + ink1: '#15110A', + ink2: '#4A4540', + ink3: '#918A80', + ink4: '#C8C2B6', + hair: '#E6E1D4', + hairStrong: '#D4CDB8', + acc: '#2A4365', + accDim: '#1F3147', + accSoft: 'rgba(42, 67, 101, 0.08)', + accMid: 'rgba(42, 67, 101, 0.18)', + warn: '#B4814A', + warnBg: 'rgba(180, 129, 74, 0.08)', + danger: '#B85A4F', + dangerBg: 'rgba(184, 90, 79, 0.08)', + good: '#5C8862', + goodBg: 'rgba(92, 136, 98, 0.08)', + info: '#4F6F8E', +} as const; + +export type ColorToken = keyof typeof colors; diff --git a/web/src/tokens/elevation.ts b/web/src/tokens/elevation.ts new file mode 100644 index 0000000..3abccd3 --- /dev/null +++ b/web/src/tokens/elevation.ts @@ -0,0 +1,8 @@ +// web/src/tokens/elevation.ts +export const elevation = { + e1: '0 1px 2px rgba(21,17,10,0.04), 0 0 0 1px #E6E1D4', + e2: '0 2px 4px rgba(21,17,10,0.05), 0 8px 16px rgba(21,17,10,0.04), 0 0 0 1px #E6E1D4', + e3: '0 4px 12px rgba(21,17,10,0.07), 0 16px 32px rgba(21,17,10,0.06), 0 0 0 1px #D4CDB8', +} as const; + +export type ElevationToken = keyof typeof elevation; diff --git a/web/src/tokens/index.ts b/web/src/tokens/index.ts new file mode 100644 index 0000000..7964864 --- /dev/null +++ b/web/src/tokens/index.ts @@ -0,0 +1,5 @@ +// web/src/tokens/index.ts +export { colors, type ColorToken } from './colors'; +export { typography, type TypographyToken } from './typography'; +export { spacing, type SpacingToken } from './spacing'; +export { elevation, type ElevationToken } from './elevation'; diff --git a/web/src/tokens/spacing.ts b/web/src/tokens/spacing.ts new file mode 100644 index 0000000..5bea9f9 --- /dev/null +++ b/web/src/tokens/spacing.ts @@ -0,0 +1,11 @@ +// web/src/tokens/spacing.ts +export const spacing = { + s1: 4, + s2: 8, + s3: 12, + s4: 16, + s5: 24, + s6: 32, +} as const; + +export type SpacingToken = keyof typeof spacing; diff --git a/web/src/tokens/typography.ts b/web/src/tokens/typography.ts new file mode 100644 index 0000000..4fa5f40 --- /dev/null +++ b/web/src/tokens/typography.ts @@ -0,0 +1,15 @@ +// web/src/tokens/typography.ts +export const typography = { + ffSans: '"Geist", "Inter", system-ui, sans-serif', + ffMono: '"Geist Mono", "JetBrains Mono", ui-monospace, monospace', + micro: 10, + meta: 11, + body: 13, + lead: 14, + h3: 15, + h2: 18, + h1: 24, + display: 30, +} as const; + +export type TypographyToken = keyof typeof typography; diff --git a/web/tests/_helpers/render.tsx b/web/tests/_helpers/render.tsx new file mode 100644 index 0000000..7a64a8e --- /dev/null +++ b/web/tests/_helpers/render.tsx @@ -0,0 +1,16 @@ +// web/tests/_helpers/render.tsx +import type { ReactElement } from 'react'; +import { render as rtlRender, type RenderOptions } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +export function render(ui: ReactElement, options?: RenderOptions) { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false, gcTime: 0 } }, + }); + return rtlRender( + {ui}, + options, + ); +} + +export * from '@testing-library/react'; diff --git a/web/tests/setup.ts b/web/tests/setup.ts new file mode 100644 index 0000000..420be0d --- /dev/null +++ b/web/tests/setup.ts @@ -0,0 +1,2 @@ +// web/tests/setup.ts +import '@testing-library/jest-dom/vitest'; diff --git a/web/tests/unit/smoke.test.ts b/web/tests/unit/smoke.test.ts new file mode 100644 index 0000000..75f2297 --- /dev/null +++ b/web/tests/unit/smoke.test.ts @@ -0,0 +1,8 @@ +// web/tests/unit/smoke.test.ts +import { describe, it, expect } from 'vitest'; + +describe('vitest smoke', () => { + it('arithmetic still works', () => { + expect(1 + 1).toBe(2); + }); +}); diff --git a/web/vitest.config.ts b/web/vitest.config.ts new file mode 100644 index 0000000..b1e5163 --- /dev/null +++ b/web/vitest.config.ts @@ -0,0 +1,25 @@ +// web/vitest.config.ts +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; +import { fileURLToPath } from 'node:url'; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, + test: { + environment: 'jsdom', + setupFiles: ['./tests/setup.ts'], + globals: true, + css: false, + coverage: { + provider: 'v8', + reporter: ['text', 'lcov'], + include: ['src/**/*.{ts,tsx}'], + exclude: ['src/main.tsx', 'src/**/*.d.ts'], + }, + }, +});