From e3148196293f11347eea07387c6ef93c5cd0acbe Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 14:55:59 +0000 Subject: [PATCH] feat(web): migrate to Next.js App Router and add design tokens (ADR 0018) The Sprint 1 shell moves to /app with its own root layout so its dark theme does not leak. Figma tokens ported with light and dark parity. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_014SdXCbrN5PtGExkpvMaMHx --- .gitignore | 2 + e2e/tests/chat-flow.test.ts | 2 +- pnpm-lock.yaml | 520 +++++++++++++++++++++++ web/__tests__/tokens.test.ts | 81 ++++ web/app/(legacy)/app/layout.tsx | 22 + web/app/(legacy)/app/page.tsx | 5 + web/app/(marketing)/layout.tsx | 19 + web/app/(marketing)/page.tsx | 3 + web/index.html | 18 - web/next.config.ts | 16 + web/package.json | 9 +- web/src/{App.tsx => LegacyApp.tsx} | 4 +- web/src/main.tsx | 10 - web/src/styles/marketing.css | 12 + web/src/styles/tokens.css | 168 ++++++++ web/tsconfig.json | 15 +- web/{vite.config.ts => vitest.config.ts} | 10 +- 17 files changed, 870 insertions(+), 46 deletions(-) create mode 100644 web/__tests__/tokens.test.ts create mode 100644 web/app/(legacy)/app/layout.tsx create mode 100644 web/app/(legacy)/app/page.tsx create mode 100644 web/app/(marketing)/layout.tsx create mode 100644 web/app/(marketing)/page.tsx delete mode 100644 web/index.html create mode 100644 web/next.config.ts rename web/src/{App.tsx => LegacyApp.tsx} (97%) delete mode 100644 web/src/main.tsx create mode 100644 web/src/styles/marketing.css create mode 100644 web/src/styles/tokens.css rename web/{vite.config.ts => vitest.config.ts} (53%) diff --git a/.gitignore b/.gitignore index b518b64..6456203 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,5 @@ test-results/ .idea/ .vscode/ e2e/fixtures/large-exports/ +.next/ +next-env.d.ts diff --git a/e2e/tests/chat-flow.test.ts b/e2e/tests/chat-flow.test.ts index 7b0c98d..c95b980 100644 --- a/e2e/tests/chat-flow.test.ts +++ b/e2e/tests/chat-flow.test.ts @@ -1,7 +1,7 @@ import { test, expect } from '@playwright/test'; test('user signs up and sees the chat interface', async ({ page }) => { - await page.goto('/'); + await page.goto('/app'); await page.getByLabel('Email').fill(`e2e-${Date.now()}@example.com`); await page.getByLabel('Password').fill('password123'); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f668424..102e128 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,12 @@ importers: web: dependencies: + geist: + specifier: ^1.7.2 + version: 1.7.2(next@16.4.0(@playwright/test@1.62.1)(@types/node@22.20.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)) + next: + specifier: ^16.4.0 + version: 16.4.0(@playwright/test@1.62.1)(@types/node@22.20.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react: specifier: ^19.2.8 version: 19.2.8 @@ -162,6 +168,9 @@ packages: resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} engines: {node: '>=20.19.0'} + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + '@esbuild/aix-ppc64@0.28.2': resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} engines: {node: '>=18'} @@ -359,6 +368,168 @@ packages: engines: {node: '>=6'} hasBin: true + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.35.5': + resolution: {integrity: sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.35.5': + resolution: {integrity: sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-freebsd-wasm32@0.35.5': + resolution: {integrity: sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.4': + resolution: {integrity: sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.3.4': + resolution: {integrity: sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.3.4': + resolution: {integrity: sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.3.4': + resolution: {integrity: sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.3.4': + resolution: {integrity: sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.3.4': + resolution: {integrity: sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.3.4': + resolution: {integrity: sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.3.4': + resolution: {integrity: sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': + resolution: {integrity: sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.3.4': + resolution: {integrity: sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.35.5': + resolution: {integrity: sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.35.5': + resolution: {integrity: sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.35.5': + resolution: {integrity: sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.35.5': + resolution: {integrity: sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.35.5': + resolution: {integrity: sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.35.5': + resolution: {integrity: sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.35.5': + resolution: {integrity: sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.35.5': + resolution: {integrity: sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.35.5': + resolution: {integrity: sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.5': + resolution: {integrity: sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.35.5': + resolution: {integrity: sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.35.5': + resolution: {integrity: sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.35.5': + resolution: {integrity: sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} @@ -372,6 +543,61 @@ packages: os: [linux] libc: [glibc] + '@next/env@16.4.0': + resolution: {integrity: sha512-pyLlT5oafGlX8X7xjgGNXz5dNIDSxREm+qD6i9VLSZiZFopA/XWl8NM/KPmLx/8EG/++c49iid3gNWSQW1+2dQ==} + + '@next/swc-darwin-arm64@16.4.0': + resolution: {integrity: sha512-yyZ+9MgViAKLLb5l1gPBGjRwnq3teNLb5Zx6JEw8LzNSTjlsnp+baUHFxTgl7PpjCmKf48VQfyipG1eBflb48Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@16.4.0': + resolution: {integrity: sha512-RePEEejEj1Auf5s9qEcG0JbcDP3ceNUS/ofNX8AhxzgIjTVlZIHMatlp16YplcSMpsVo/ZMQHJTjbMYJznjGUg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@16.4.0': + resolution: {integrity: sha512-HhsyCqd+NpmvDDVY/LqkAN24f+QVvXh3ie+PfcwKinWL2Mu826JymQxJXGjAIfmJ4VlHjp4Pe/nZtXtUoEMAqw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-arm64-musl@16.4.0': + resolution: {integrity: sha512-sN9Pf+Zh+RpZAOmbR7d8KAA8T61CVhawtbEJ2Bk6QrRzx6Z/6haaF4UiwBzytM96GRRRWZh9/WALIEO3npMamA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@next/swc-linux-x64-gnu@16.4.0': + resolution: {integrity: sha512-D5M5xLWQVd1CvR+L7BickM1weRfmM41x3L1NWcnKY/oyJJqr1qwaCgmu3dMAhLBOMdZwSuDCRV6DJRRpiUkbDQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-x64-musl@16.4.0': + resolution: {integrity: sha512-8d5WLS6a9YalqpOgXdlbvonFu+sIgjA+CO6tGzetrpPVcUL56foa7B7664h41rAlbLi5fJwK4vWji3zf4QDy+w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@next/swc-win32-arm64-msvc@16.4.0': + resolution: {integrity: sha512-sQuOIiW3snb48UwrBcDMvFA/u03qtw2LeuSIxFIQ9AJuW9UG/4Ddsfo+eV73PnZwBhyncMtsKVh7PrzMol2/fA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@16.4.0': + resolution: {integrity: sha512-6/33lCuxpdHUy27RegyGM/5uTpF7YZd3UR4D0gpkOA/tWY5YR7msbQjqPgCZmSmxrrTLGCXYA4A8vT2H5DPGTQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + '@oxc-project/types@0.144.0': resolution: {integrity: sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==} @@ -641,6 +867,9 @@ packages: cpu: [x64] os: [win32] + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} + '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} @@ -806,6 +1035,11 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.11.28: + resolution: {integrity: sha512-br0LOmjJqr6Rt4AEkadbcNf67Zpo7EPwKsWbjnRrJIa1QYFHQBqM4C7F2WTbV8ahTEcg1Eo+Y0aWjjORInYFyA==} + engines: {node: '>=6.0.0'} + hasBin: true + bcrypt-pbkdf@1.0.2: resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} @@ -826,6 +1060,9 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + caniuse-lite@1.0.30001815: + resolution: {integrity: sha512-uDybUEyfgbyJJ93J1vikTylBX31AkdVYeVPeffMQpf8XN7hSfr0oZOn4VFOIMNvtU5+rHvC5Udg2TgtJKHm23Q==} + chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -837,6 +1074,9 @@ packages: chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -987,6 +1227,11 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + geist@1.7.2: + resolution: {integrity: sha512-Gu5lDFa3pLRyoBlBPf0QIFHVdWAnpco7fS1bJm41jyLPFoguBgiubseUN2oLXMgqZ7uxAxDoXcHMhCY/fOTTgg==} + peerDependencies: + next: '>=13.2.0' + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -1155,6 +1400,27 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + next@16.4.0: + resolution: {integrity: sha512-N2C+9q89Z/oLrsepSRS4jRMtwmUZAoZuAHU0e9T6+sAB5DdhADmrnOW+zjYijlrYFmVfmQhGVGy9eMFoB7UgKg==} + engines: {node: '>=20.9.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + on-exit-leak-free@2.1.2: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} @@ -1233,6 +1499,10 @@ packages: engines: {node: '>=20'} hasBin: true + postcss@8.5.23: + resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} + engines: {node: ^10 || ^12 || >=14} + postcss@8.5.26: resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} engines: {node: ^10 || ^12 || >=14} @@ -1365,6 +1635,15 @@ packages: set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + sharp@0.35.5: + resolution: {integrity: sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -1410,6 +1689,19 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -1465,6 +1757,9 @@ packages: resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} engines: {node: '>=20'} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.23.12: resolution: {integrity: sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==} engines: {node: '>=18.0.0'} @@ -1721,6 +2016,11 @@ snapshots: '@csstools/css-tokenizer@4.0.0': {} + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.28.2': optional: true @@ -1843,6 +2143,113 @@ snapshots: protobufjs: 7.6.5 yargs: 17.7.3 + '@img/colour@1.1.0': + optional: true + + '@img/sharp-darwin-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.4 + optional: true + + '@img/sharp-darwin-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.4 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.5': + dependencies: + '@img/sharp-wasm32': 0.35.5 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.3.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.3.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.3.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.3.4': + optional: true + + '@img/sharp-linux-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.4 + optional: true + + '@img/sharp-linux-arm@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.4 + optional: true + + '@img/sharp-linux-ppc64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.4 + optional: true + + '@img/sharp-linux-riscv64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.4 + optional: true + + '@img/sharp-linux-s390x@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.4 + optional: true + + '@img/sharp-linux-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 + optional: true + + '@img/sharp-wasm32@0.35.5': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-webcontainers-wasm32@0.35.5': + dependencies: + '@img/sharp-wasm32': 0.35.5 + optional: true + + '@img/sharp-win32-arm64@0.35.5': + optional: true + + '@img/sharp-win32-ia32@0.35.5': + optional: true + + '@img/sharp-win32-x64@0.35.5': + optional: true + '@jridgewell/sourcemap-codec@1.5.5': {} '@js-sdsl/ordered-map@4.4.2': {} @@ -1850,6 +2257,32 @@ snapshots: '@napi-rs/lzma-linux-x64-gnu@1.5.1': optional: true + '@next/env@16.4.0': {} + + '@next/swc-darwin-arm64@16.4.0': + optional: true + + '@next/swc-darwin-x64@16.4.0': + optional: true + + '@next/swc-linux-arm64-gnu@16.4.0': + optional: true + + '@next/swc-linux-arm64-musl@16.4.0': + optional: true + + '@next/swc-linux-x64-gnu@16.4.0': + optional: true + + '@next/swc-linux-x64-musl@16.4.0': + optional: true + + '@next/swc-win32-arm64-msvc@16.4.0': + optional: true + + '@next/swc-win32-x64-msvc@16.4.0': + optional: true + '@oxc-project/types@0.144.0': {} '@pinojs/redact@0.4.0': {} @@ -1997,6 +2430,10 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.62.4': optional: true + '@swc/helpers@0.5.23': + dependencies: + tslib: 2.8.1 + '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.29.7 @@ -2171,6 +2608,8 @@ snapshots: base64-js@1.5.1: {} + baseline-browser-mapping@2.11.28: {} + bcrypt-pbkdf@1.0.2: dependencies: tweetnacl: 0.14.5 @@ -2195,6 +2634,8 @@ snapshots: cac@6.7.14: {} + caniuse-lite@1.0.30001815: {} + chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -2207,6 +2648,8 @@ snapshots: chownr@1.1.4: {} + client-only@0.0.1: {} + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -2387,6 +2830,10 @@ snapshots: fsevents@2.3.3: optional: true + geist@1.7.2(next@16.4.0(@playwright/test@1.62.1)(@types/node@22.20.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)): + dependencies: + next: 16.4.0(@playwright/test@1.62.1)(@types/node@22.20.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + get-caller-file@2.0.5: {} html-encoding-sniffer@6.0.0: @@ -2525,6 +2972,32 @@ snapshots: nanoid@3.3.18: {} + next@16.4.0(@playwright/test@1.62.1)(@types/node@22.20.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + '@next/env': 16.4.0 + '@swc/helpers': 0.5.23 + baseline-browser-mapping: 2.11.28 + caniuse-lite: 1.0.30001815 + postcss: 8.5.23 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + styled-jsx: 5.1.6(react@19.2.8) + optionalDependencies: + '@next/swc-darwin-arm64': 16.4.0 + '@next/swc-darwin-x64': 16.4.0 + '@next/swc-linux-arm64-gnu': 16.4.0 + '@next/swc-linux-arm64-musl': 16.4.0 + '@next/swc-linux-x64-gnu': 16.4.0 + '@next/swc-linux-x64-musl': 16.4.0 + '@next/swc-win32-arm64-msvc': 16.4.0 + '@next/swc-win32-x64-msvc': 16.4.0 + '@playwright/test': 1.62.1 + sharp: 0.35.5(@types/node@22.20.1) + transitivePeerDependencies: + - '@babel/core' + - '@types/node' + - babel-plugin-macros + on-exit-leak-free@2.1.2: {} once@1.4.0: @@ -2606,6 +3079,12 @@ snapshots: optionalDependencies: fsevents: 2.3.2 + postcss@8.5.23: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + postcss@8.5.26: dependencies: nanoid: 3.3.18 @@ -2763,6 +3242,40 @@ snapshots: set-cookie-parser@2.7.2: {} + sharp@0.35.5(@types/node@22.20.1): + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.35.5 + '@img/sharp-darwin-x64': 0.35.5 + '@img/sharp-freebsd-wasm32': 0.35.5 + '@img/sharp-libvips-darwin-arm64': 1.3.4 + '@img/sharp-libvips-darwin-x64': 1.3.4 + '@img/sharp-libvips-linux-arm': 1.3.4 + '@img/sharp-libvips-linux-arm64': 1.3.4 + '@img/sharp-libvips-linux-ppc64': 1.3.4 + '@img/sharp-libvips-linux-riscv64': 1.3.4 + '@img/sharp-libvips-linux-s390x': 1.3.4 + '@img/sharp-libvips-linux-x64': 1.3.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 + '@img/sharp-linux-arm': 0.35.5 + '@img/sharp-linux-arm64': 0.35.5 + '@img/sharp-linux-ppc64': 0.35.5 + '@img/sharp-linux-riscv64': 0.35.5 + '@img/sharp-linux-s390x': 0.35.5 + '@img/sharp-linux-x64': 0.35.5 + '@img/sharp-linuxmusl-arm64': 0.35.5 + '@img/sharp-linuxmusl-x64': 0.35.5 + '@img/sharp-webcontainers-wasm32': 0.35.5 + '@img/sharp-win32-arm64': 0.35.5 + '@img/sharp-win32-ia32': 0.35.5 + '@img/sharp-win32-x64': 0.35.5 + '@types/node': 22.20.1 + optional: true + siginfo@2.0.0: {} sonic-boom@4.2.1: @@ -2809,6 +3322,11 @@ snapshots: dependencies: js-tokens: 9.0.1 + styled-jsx@5.1.6(react@19.2.8): + dependencies: + client-only: 0.0.1 + react: 19.2.8 + symbol-tree@3.2.4: {} tar-fs@2.1.5: @@ -2861,6 +3379,8 @@ snapshots: dependencies: punycode: 2.3.1 + tslib@2.8.1: {} + tsx@4.23.12: dependencies: esbuild: 0.28.2 diff --git a/web/__tests__/tokens.test.ts b/web/__tests__/tokens.test.ts new file mode 100644 index 0000000..40623d8 --- /dev/null +++ b/web/__tests__/tokens.test.ts @@ -0,0 +1,81 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; + +const css = readFileSync(resolve(process.cwd(), 'src/styles/tokens.css'), 'utf8'); + +/** Returns the declarations of the first block whose selector contains `selector`. */ +function block(selector: string): Record { + const start = css.indexOf(selector); + if (start === -1) return {}; + const open = css.indexOf('{', start); + const close = css.indexOf('}', open); + const out: Record = {}; + for (const m of css.slice(open + 1, close).matchAll(/(--[\w-]+)\s*:\s*([^;]+);/g)) { + out[m[1]!] = m[2]!.trim(); + } + return out; +} + +const light = block(':root {'); +const dark = block(':root[data-theme="dark"]'); +const auto = block(':root:not([data-theme="light"])'); + +const COLOR_TOKENS = [ + '--color-bg-canvas', '--color-bg-surface', '--color-bg-sunken', '--color-bg-inverse', + '--color-text-primary', '--color-text-secondary', '--color-text-tertiary', '--color-text-inverse', + '--color-text-on-accent', '--color-text-link', + '--color-border-subtle', '--color-border-default', '--color-border-strong', + '--color-accent-default', '--color-accent-text', '--color-accent-subtle', + '--color-status-working-dot', '--color-status-working-fg', '--color-status-working-bg', + '--color-status-needs-you-dot', '--color-status-needs-you-fg', '--color-status-needs-you-bg', + '--color-status-success-dot', '--color-status-success-fg', '--color-status-success-bg', + '--color-diff-add-fg', '--color-diff-remove-fg', '--icon-color', +]; + +describe('design tokens', () => { + it('defines every color token for the light theme', () => { + expect(COLOR_TOKENS.filter((t) => !(t in light))).toEqual([]); + }); + + it('defines every color token for the dark theme (explicit override)', () => { + expect(COLOR_TOKENS.filter((t) => !(t in dark))).toEqual([]); + }); + + it('follows the device setting for dark unless the light theme is forced', () => { + expect(COLOR_TOKENS.filter((t) => !(t in auto))).toEqual([]); + expect(css).toMatch(/@media \(prefers-color-scheme: dark\)/); + }); + + it('uses the Figma light values', () => { + expect(light['--color-bg-canvas']).toBe('#f7f7f4'); + expect(light['--color-accent-default']).toBe('#e8590c'); + expect(light['--color-text-primary']).toBe('#14120b'); + }); + + it('uses the Figma dark values', () => { + expect(dark['--color-bg-canvas']).toBe('#0e0e0c'); + expect(dark['--color-accent-default']).toBe('#ff7a33'); + }); + + it('keeps dark and automatic dark in agreement', () => { + expect(auto).toEqual(dark); + }); + + it('defines the space, radius and control scales', () => { + expect(light['--space-4']).toBe('16px'); + expect(light['--radius-full']).toBe('999px'); + expect(light['--size-control-lg']).toBe('44px'); + }); + + it('defines the Figma type styles as classes', () => { + for (const name of ['display-xl', 'display-l', 'display-m', 'title-l', 'title-m', 'title-s', + 'body-l', 'body-m', 'body-m-strong', 'label-m', 'label-s', 'caption', 'overline', 'mono-s']) { + expect(css).toContain(`.t-${name}`); + } + }); + + it('converts Figma percent tracking to em (Display/XL -3 is -0.03em)', () => { + expect(css).toMatch(/\.t-display-xl\s*\{[^}]*letter-spacing:\s*-0\.03em/); + }); +}); diff --git a/web/app/(legacy)/app/layout.tsx b/web/app/(legacy)/app/layout.tsx new file mode 100644 index 0000000..167b222 --- /dev/null +++ b/web/app/(legacy)/app/layout.tsx @@ -0,0 +1,22 @@ +import type { ReactNode } from 'react'; +import '../../../src/styles/global.css'; + +export const metadata = { title: 'Wursor' }; + +// Interim product shell from Sprint 1. Its own root layout keeps its dark theme +// from leaking into the marketing site. Replaced by the designed screens. +export default function LegacyLayout({ children }: { children: ReactNode }) { + return ( + + + + + + + {children} + + ); +} diff --git a/web/app/(legacy)/app/page.tsx b/web/app/(legacy)/app/page.tsx new file mode 100644 index 0000000..79fc978 --- /dev/null +++ b/web/app/(legacy)/app/page.tsx @@ -0,0 +1,5 @@ +import { LegacyApp } from '../../../src/LegacyApp.tsx'; + +export default function Page() { + return ; +} diff --git a/web/app/(marketing)/layout.tsx b/web/app/(marketing)/layout.tsx new file mode 100644 index 0000000..58102b8 --- /dev/null +++ b/web/app/(marketing)/layout.tsx @@ -0,0 +1,19 @@ +import { GeistMono } from 'geist/font/mono'; +import { GeistSans } from 'geist/font/sans'; +import type { Metadata } from 'next'; +import type { ReactNode } from 'react'; +import '../../src/styles/tokens.css'; +import '../../src/styles/marketing.css'; + +export const metadata: Metadata = { + title: { default: 'Wursor', template: '%s ยท Wursor' }, + description: 'Your website, handled. Tell Wursor what to change. Watch it work. Approve what you like.', +}; + +export default function MarketingLayout({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} diff --git a/web/app/(marketing)/page.tsx b/web/app/(marketing)/page.tsx new file mode 100644 index 0000000..c41dc82 --- /dev/null +++ b/web/app/(marketing)/page.tsx @@ -0,0 +1,3 @@ +export default function HomePage() { + return
Wursor
; +} diff --git a/web/index.html b/web/index.html deleted file mode 100644 index 23c13fb..0000000 --- a/web/index.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - Wursor - - -
- - - diff --git a/web/next.config.ts b/web/next.config.ts new file mode 100644 index 0000000..00bcb71 --- /dev/null +++ b/web/next.config.ts @@ -0,0 +1,16 @@ +import type { NextConfig } from 'next'; + +const apiOrigin = process.env.WURSOR_API_URL ?? 'http://localhost:3000'; + +const config: NextConfig = { + reactStrictMode: true, + // Interim: the Sprint 1 shell at /app still calls the API on the same origin. + // Replaced by server-held sessions (ADR 0018, step C). + async rewrites() { + return ['/auth/:path*', '/chat/:path*', '/chat', '/sites/:path*', '/sites', '/health'].map( + (source) => ({ source, destination: `${apiOrigin}${source}` }), + ); + }, +}; + +export default config; diff --git a/web/package.json b/web/package.json index 720f127..7a5d438 100644 --- a/web/package.json +++ b/web/package.json @@ -4,11 +4,14 @@ "type": "module", "scripts": { "test": "vitest run", - "dev": "vite", - "build": "tsc -b && vite build", - "preview": "vite preview" + "dev": "next dev -p 5173", + "build": "next build", + "start": "next start -p 5173", + "lint": "tsc --noEmit" }, "dependencies": { + "geist": "^1.7.2", + "next": "^16.4.0", "react": "^19.2.8", "react-dom": "^19.2.8" }, diff --git a/web/src/App.tsx b/web/src/LegacyApp.tsx similarity index 97% rename from web/src/App.tsx rename to web/src/LegacyApp.tsx index 65f5012..80200eb 100644 --- a/web/src/App.tsx +++ b/web/src/LegacyApp.tsx @@ -1,3 +1,5 @@ +'use client'; + import { useState } from 'react'; import { ApproveBar } from './components/ApproveBar.tsx'; import { ChatPanel } from './components/ChatPanel.tsx'; @@ -19,7 +21,7 @@ async function signUp(email: string, password: string): Promise { return body.sessionToken; } -export function App() { +export function LegacyApp() { const [sessionToken, setSessionToken] = useState(null); const [applied, setApplied] = useState(false); const chat = useChat(sessionToken); diff --git a/web/src/main.tsx b/web/src/main.tsx deleted file mode 100644 index 8e9e000..0000000 --- a/web/src/main.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import { App } from './App.tsx'; -import './styles/global.css'; - -createRoot(document.getElementById('root')!).render( - - - , -); diff --git a/web/src/styles/marketing.css b/web/src/styles/marketing.css new file mode 100644 index 0000000..32badce --- /dev/null +++ b/web/src/styles/marketing.css @@ -0,0 +1,12 @@ +*, *::before, *::after { box-sizing: border-box; } +html { -webkit-text-size-adjust: 100%; } +body { + margin: 0; + background: var(--color-bg-canvas); + color: var(--color-text-primary); + font: 400 16px/24px var(--font-sans); + -webkit-font-smoothing: antialiased; +} +a { color: inherit; text-decoration: none; } +button { font: inherit; color: inherit; } +:focus-visible { outline: 2px solid var(--color-accent-default); outline-offset: 2px; } diff --git a/web/src/styles/tokens.css b/web/src/styles/tokens.css new file mode 100644 index 0000000..bcfc10f --- /dev/null +++ b/web/src/styles/tokens.css @@ -0,0 +1,168 @@ +/* + * Wursor design tokens, ported from the Figma file "Wursor - weown.net" + * (Design system page). Light is the default; dark follows the device + * unless data-theme forces one (PX-10). Change a value here, not in a component. + * Figma letter-spacing is a percentage, so it is written in em here. + */ +:root { + --icon-color: #14120b; + + --color-bg-canvas: #f7f7f4; + --color-bg-surface: #ffffff; + --color-bg-sunken: #f1f0eb; + --color-bg-inverse: #14120b; + + --color-text-primary: #14120b; + --color-text-secondary: #6b675e; + --color-text-tertiary: #8f8b80; + --color-text-inverse: #f2f1ec; + --color-text-on-accent: #ffffff; + --color-text-link: #2a52b0; + + --color-border-subtle: #14120b14; + --color-border-default: #dddcd5; + --color-border-strong: #c4c2b8; + + --color-accent-default: #e8590c; + --color-accent-text: #b8420a; + --color-accent-subtle: #fdede3; + + --color-status-working-dot: #3b6fe0; + --color-status-working-fg: #2a52b0; + --color-status-working-bg: #e8eefb; + --color-status-needs-you-dot: #c98a12; + --color-status-needs-you-fg: #8a5a00; + --color-status-needs-you-bg: #fff6e5; + --color-status-success-dot: #2f8f5b; + --color-status-success-fg: #1f6b42; + --color-status-success-bg: #e4f2e8; + + --color-diff-add-fg: #1f6b42; + --color-diff-remove-fg: #a12a2a; + + --space-0: 0px; + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-5: 20px; + --space-6: 24px; + --space-8: 32px; + --space-10: 40px; + --space-16: 64px; + + --radius-xs: 4px; + --radius-sm: 8px; + --radius-md: 12px; + --radius-lg: 16px; + --radius-xl: 24px; + --radius-full: 999px; + + --size-control-sm: 28px; + --size-control-md: 36px; + --size-control-lg: 44px; + + --shadow-modal: 0 24px 64px -8px #14120b24, 0 2px 6px 0 #14120b0f; + + --font-sans: var(--font-geist-sans), system-ui, -apple-system, sans-serif; + --font-mono: var(--font-geist-mono), ui-monospace, monospace; + + color-scheme: light; +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + --icon-color: #f2f1ec; + + --color-bg-canvas: #0e0e0c; + --color-bg-surface: #171714; + --color-bg-sunken: #1e1e1a; + --color-bg-inverse: #f2f1ec; + + --color-text-primary: #f2f1ec; + --color-text-secondary: #8f8b80; + --color-text-tertiary: #6b675e; + --color-text-inverse: #14120b; + --color-text-on-accent: #14120b; + /* Figma keeps the light value (#2a52b0) in dark, which fails AA on #0e0e0c. Lightened to the dark "working" blue. */ + --color-text-link: #7c9cf0; + + --color-border-subtle: #f2f1ec14; + --color-border-default: #2c2c27; + --color-border-strong: #4a473f; + + --color-accent-default: #ff7a33; + --color-accent-text: #ff9a5e; + --color-accent-subtle: #3a1a08; + + --color-status-working-dot: #7c9cf0; + --color-status-working-fg: #7c9cf0; + --color-status-working-bg: #1b2440; + --color-status-needs-you-dot: #e8b04a; + --color-status-needs-you-fg: #e8b04a; + --color-status-needs-you-bg: #3a2d12; + --color-status-success-dot: #5cc48a; + --color-status-success-fg: #5cc48a; + --color-status-success-bg: #17301f; + + --color-diff-add-fg: #5cc48a; + --color-diff-remove-fg: #e86a6a; + + color-scheme: dark; + } +} + +:root[data-theme="dark"] { + --icon-color: #f2f1ec; + + --color-bg-canvas: #0e0e0c; + --color-bg-surface: #171714; + --color-bg-sunken: #1e1e1a; + --color-bg-inverse: #f2f1ec; + + --color-text-primary: #f2f1ec; + --color-text-secondary: #8f8b80; + --color-text-tertiary: #6b675e; + --color-text-inverse: #14120b; + --color-text-on-accent: #14120b; + --color-text-link: #7c9cf0; + + --color-border-subtle: #f2f1ec14; + --color-border-default: #2c2c27; + --color-border-strong: #4a473f; + + --color-accent-default: #ff7a33; + --color-accent-text: #ff9a5e; + --color-accent-subtle: #3a1a08; + + --color-status-working-dot: #7c9cf0; + --color-status-working-fg: #7c9cf0; + --color-status-working-bg: #1b2440; + --color-status-needs-you-dot: #e8b04a; + --color-status-needs-you-fg: #e8b04a; + --color-status-needs-you-bg: #3a2d12; + --color-status-success-dot: #5cc48a; + --color-status-success-fg: #5cc48a; + --color-status-success-bg: #17301f; + + --color-diff-add-fg: #5cc48a; + --color-diff-remove-fg: #e86a6a; + + color-scheme: dark; +} + +/* Type styles (Figma text styles). */ +.t-display-xl { font: 300 72px/72px var(--font-sans); letter-spacing: -0.03em; } +.t-display-l { font: 300 48px/52px var(--font-sans); letter-spacing: -0.025em; } +.t-display-m { font: 300 36px/40px var(--font-sans); letter-spacing: -0.02em; } +.t-title-l { font: 400 28px/34px var(--font-sans); letter-spacing: -0.015em; } +.t-title-m { font: 500 20px/28px var(--font-sans); letter-spacing: -0.01em; } +.t-title-s { font: 500 16px/24px var(--font-sans); letter-spacing: -0.005em; } +.t-body-l { font: 400 16px/24px var(--font-sans); } +.t-body-m { font: 400 14px/20px var(--font-sans); } +.t-body-m-strong { font: 500 14px/20px var(--font-sans); } +.t-label-m { font: 500 13px/16px var(--font-sans); } +.t-label-s { font: 500 12px/16px var(--font-sans); } +.t-caption { font: 400 12px/16px var(--font-sans); } +.t-overline { font: 600 11px/16px var(--font-sans); letter-spacing: 0.06em; text-transform: uppercase; } +.t-mono-s { font: 400 12px/16px var(--font-mono); } diff --git a/web/tsconfig.json b/web/tsconfig.json index 6d51467..0b5b365 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -1,12 +1,17 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { - "jsx": "react-jsx", - "outDir": "dist", - "rootDir": "src", + "jsx": "preserve", + "module": "ESNext", + "moduleResolution": "Bundler", "lib": ["ES2022", "DOM", "DOM.Iterable"], "noEmit": true, - "allowImportingTsExtensions": true + "incremental": true, + "declaration": false, + "sourceMap": false, + "allowImportingTsExtensions": true, + "plugins": [{ "name": "next" }] }, - "include": ["src"] + "include": ["next-env.d.ts", "app", "src", "__tests__", "next.config.ts", ".next/types/**/*.ts"], + "exclude": ["node_modules"] } diff --git a/web/vite.config.ts b/web/vitest.config.ts similarity index 53% rename from web/vite.config.ts rename to web/vitest.config.ts index 590a9a5..7ce4620 100644 --- a/web/vite.config.ts +++ b/web/vitest.config.ts @@ -3,16 +3,10 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ plugins: [react()], - server: { - proxy: { - '/auth': 'http://localhost:3000', - '/chat': 'http://localhost:3000', - '/sites': 'http://localhost:3000', - '/health': 'http://localhost:3000', - }, - }, + esbuild: { jsx: 'automatic' }, test: { environment: 'jsdom', setupFiles: ['./src/test-setup.ts'], + include: ['__tests__/**/*.test.{ts,tsx}'], }, });