frontier-os-app-builder 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -0
- package/agents/fos-executor.md +460 -0
- package/agents/fos-plan-checker.md +386 -0
- package/agents/fos-planner.md +416 -0
- package/agents/fos-researcher.md +358 -0
- package/agents/fos-verifier.md +491 -0
- package/bin/fos-tools.cjs +794 -0
- package/bin/install.js +234 -0
- package/commands/fos/add-feature.md +29 -0
- package/commands/fos/discuss.md +31 -0
- package/commands/fos/execute.md +35 -0
- package/commands/fos/new-app.md +39 -0
- package/commands/fos/new-milestone.md +28 -0
- package/commands/fos/next.md +29 -0
- package/commands/fos/plan.md +37 -0
- package/commands/fos/ship.md +29 -0
- package/commands/fos/status.md +22 -0
- package/package.json +30 -0
- package/references/app-patterns.md +501 -0
- package/references/deployment.md +395 -0
- package/references/module-inference.md +349 -0
- package/references/sdk-surface.md +1622 -0
- package/references/verification-rules.md +404 -0
- package/templates/app/gitignore +25 -0
- package/templates/app/index.css +111 -0
- package/templates/app/index.html +19 -0
- package/templates/app/layout.tsx +45 -0
- package/templates/app/main-router.tsx +17 -0
- package/templates/app/main-simple.tsx +19 -0
- package/templates/app/package.json +36 -0
- package/templates/app/postcss.config.js +5 -0
- package/templates/app/router.tsx +22 -0
- package/templates/app/sdk-context.tsx +33 -0
- package/templates/app/test-setup.ts +19 -0
- package/templates/app/tsconfig.json +22 -0
- package/templates/app/vercel.json +127 -0
- package/templates/app/vite.config.ts +15 -0
- package/templates/state/context.md +248 -0
- package/templates/state/manifest.json +11 -0
- package/templates/state/plan.md +187 -0
- package/templates/state/project.md +118 -0
- package/templates/state/requirements.md +133 -0
- package/templates/state/roadmap.md +129 -0
- package/templates/state/state.md +131 -0
- package/templates/state/summary.md +273 -0
- package/workflows/add-feature.md +234 -0
- package/workflows/discuss.md +310 -0
- package/workflows/execute-plan.md +222 -0
- package/workflows/execute.md +338 -0
- package/workflows/new-app.md +331 -0
- package/workflows/new-milestone.md +258 -0
- package/workflows/next.md +157 -0
- package/workflows/plan.md +310 -0
- package/workflows/ship.md +296 -0
- package/workflows/status.md +145 -0
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
# Frontier OS App Patterns
|
|
2
|
+
|
|
3
|
+
Reference for the standard structure, conventions, and tech stack used by all Frontier OS apps.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Tech Stack
|
|
8
|
+
|
|
9
|
+
| Layer | Package | Version |
|
|
10
|
+
| ------------ | ------------------------------- | --------- |
|
|
11
|
+
| Runtime | React | 19 |
|
|
12
|
+
| Bundler | Vite | 7 |
|
|
13
|
+
| Language | TypeScript | 5.9 |
|
|
14
|
+
| CSS | Tailwind CSS (via PostCSS) | 4 |
|
|
15
|
+
| Testing | Vitest + jsdom + Testing Library| 4 / 27 |
|
|
16
|
+
| SDK | @frontiertower/frontier-sdk | 0.15.0 |
|
|
17
|
+
| Routing | react-router-dom | 7 |
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Directory Layout
|
|
22
|
+
|
|
23
|
+
Every app follows this structure:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
app-<name>/
|
|
27
|
+
index.html # HTML shell (parameterized)
|
|
28
|
+
package.json # Project manifest (parameterized)
|
|
29
|
+
postcss.config.js # PostCSS with Tailwind (identical)
|
|
30
|
+
tsconfig.json # TypeScript config (identical)
|
|
31
|
+
vercel.json # Vercel deployment + CORS (identical)
|
|
32
|
+
vite.config.ts # Vite + Vitest config (parameterized)
|
|
33
|
+
src/
|
|
34
|
+
main.tsx # React root + RouterProvider (identical pattern)
|
|
35
|
+
router.tsx # Route definitions (parameterized)
|
|
36
|
+
lib/
|
|
37
|
+
sdk-context.tsx # SdkProvider + useSdk hook (identical)
|
|
38
|
+
views/
|
|
39
|
+
Layout.tsx # Shell layout component (parameterized)
|
|
40
|
+
<FeatureViews>.tsx # App-specific view components
|
|
41
|
+
components/ # Reusable UI components
|
|
42
|
+
hooks/ # Custom React hooks
|
|
43
|
+
styles/
|
|
44
|
+
index.css # Tailwind + dark theme variables (parameterized)
|
|
45
|
+
test/
|
|
46
|
+
setup.ts # Vitest setup (identical pattern)
|
|
47
|
+
lib/ # Unit tests for lib/
|
|
48
|
+
views/ # Tests for view components
|
|
49
|
+
hooks/ # Tests for hooks
|
|
50
|
+
components/ # Tests for components
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Naming Conventions
|
|
54
|
+
|
|
55
|
+
- Repository: `frontier-os-app-<name>` (kebab-case)
|
|
56
|
+
- Package name in `package.json`: `app-<name>` (kebab-case, no org scope)
|
|
57
|
+
- Views: PascalCase files in `src/views/`
|
|
58
|
+
- Components: PascalCase files in `src/components/`
|
|
59
|
+
- Hooks: camelCase `use<Name>` files in `src/hooks/`
|
|
60
|
+
- Tests: Mirror source structure under `src/test/`, with `.test.ts` or `.test.tsx` suffix
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Files Identical Across All Apps
|
|
65
|
+
|
|
66
|
+
These files are copied verbatim. They must not be modified per app.
|
|
67
|
+
|
|
68
|
+
### `src/lib/sdk-context.tsx`
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
import { createContext, useContext, useEffect, useRef, useState, type ReactNode } from 'react';
|
|
72
|
+
import { FrontierSDK } from '@frontiertower/frontier-sdk';
|
|
73
|
+
|
|
74
|
+
const SdkContext = createContext<FrontierSDK | null>(null);
|
|
75
|
+
|
|
76
|
+
export const useSdk = (): FrontierSDK => {
|
|
77
|
+
const sdk = useContext(SdkContext);
|
|
78
|
+
if (!sdk) throw new Error('useSdk must be used within SdkProvider');
|
|
79
|
+
return sdk;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const SdkProvider = ({ children }: { children: ReactNode }) => {
|
|
83
|
+
const sdkRef = useRef<FrontierSDK | null>(null);
|
|
84
|
+
const [ready, setReady] = useState(false);
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
const sdk = new FrontierSDK();
|
|
88
|
+
sdkRef.current = sdk;
|
|
89
|
+
setReady(true);
|
|
90
|
+
|
|
91
|
+
return () => {
|
|
92
|
+
sdk.destroy();
|
|
93
|
+
};
|
|
94
|
+
}, []);
|
|
95
|
+
|
|
96
|
+
if (!ready) return null;
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<SdkContext.Provider value={sdkRef.current}>
|
|
100
|
+
{children}
|
|
101
|
+
</SdkContext.Provider>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### `postcss.config.js`
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
import tailwindcss from '@tailwindcss/postcss';
|
|
110
|
+
|
|
111
|
+
export default {
|
|
112
|
+
plugins: [tailwindcss()],
|
|
113
|
+
};
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### `tsconfig.json`
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"compilerOptions": {
|
|
121
|
+
"target": "ES2020",
|
|
122
|
+
"useDefineForClassFields": true,
|
|
123
|
+
"module": "ESNext",
|
|
124
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
125
|
+
"skipLibCheck": true,
|
|
126
|
+
"jsx": "react-jsx",
|
|
127
|
+
"moduleResolution": "bundler",
|
|
128
|
+
"allowImportingTsExtensions": true,
|
|
129
|
+
"resolveJsonModule": true,
|
|
130
|
+
"isolatedModules": true,
|
|
131
|
+
"noEmit": true,
|
|
132
|
+
"strict": true,
|
|
133
|
+
"noUnusedLocals": true,
|
|
134
|
+
"noUnusedParameters": true,
|
|
135
|
+
"noFallthroughCasesInSwitch": true,
|
|
136
|
+
"types": ["vitest/globals", "@testing-library/jest-dom"]
|
|
137
|
+
},
|
|
138
|
+
"include": ["src"],
|
|
139
|
+
"exclude": ["src/test", "**/*.test.ts", "**/*.test.tsx"]
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### `vercel.json`
|
|
144
|
+
|
|
145
|
+
See [deployment.md](deployment.md) for the full file. All apps share the same CORS configuration with 5 origin blocks.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Files Parameterized Per App
|
|
150
|
+
|
|
151
|
+
These files follow a fixed template but contain app-specific values.
|
|
152
|
+
|
|
153
|
+
### `package.json`
|
|
154
|
+
|
|
155
|
+
Parameterized fields:
|
|
156
|
+
|
|
157
|
+
| Field | Example value |
|
|
158
|
+
| --------------- | ------------------------------------ |
|
|
159
|
+
| `name` | `"app-subscriptions"` |
|
|
160
|
+
| `dependencies` | App-specific packages added here |
|
|
161
|
+
|
|
162
|
+
Fixed fields (do not change):
|
|
163
|
+
|
|
164
|
+
- `"version": "1.0.0"`
|
|
165
|
+
- `"private": true`
|
|
166
|
+
- `"type": "module"`
|
|
167
|
+
- `scripts` block (see Package Scripts below)
|
|
168
|
+
- Core dependencies: `@frontiertower/frontier-sdk`, `react`, `react-dom`, `react-router-dom`
|
|
169
|
+
- Core devDependencies: `@tailwindcss/postcss`, `@types/react`, `@types/react-dom`, `@vitejs/plugin-react`, `postcss`, `tailwindcss`, `typescript`, `vite`
|
|
170
|
+
- Test devDependencies (when tests exist): `@testing-library/jest-dom`, `@testing-library/react`, `@testing-library/user-event`, `@vitest/coverage-v8`, `jsdom`, `vitest`
|
|
171
|
+
|
|
172
|
+
### `index.html`
|
|
173
|
+
|
|
174
|
+
Parameterized fields:
|
|
175
|
+
|
|
176
|
+
| Element | What changes |
|
|
177
|
+
| ----------------------- | ------------------------- |
|
|
178
|
+
| `<title>` | App display name |
|
|
179
|
+
| `<meta name="description">` | App description |
|
|
180
|
+
|
|
181
|
+
Fixed structure:
|
|
182
|
+
|
|
183
|
+
```html
|
|
184
|
+
<!doctype html>
|
|
185
|
+
<html lang="en">
|
|
186
|
+
<head>
|
|
187
|
+
<meta charset="utf-8" />
|
|
188
|
+
<title>{{APP_TITLE}}</title>
|
|
189
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
190
|
+
<meta name="description" content="{{APP_DESCRIPTION}}">
|
|
191
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
192
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
193
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
194
|
+
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
|
195
|
+
</head>
|
|
196
|
+
<body class="dark">
|
|
197
|
+
<div id="root"></div>
|
|
198
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
199
|
+
</body>
|
|
200
|
+
</html>
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Key points:
|
|
204
|
+
- `body class="dark"` is always set (dark-only theme)
|
|
205
|
+
- Plus Jakarta Sans font is loaded from Google Fonts with weights 400, 500, 600, 700
|
|
206
|
+
- Favicon is always `/favicon.svg`
|
|
207
|
+
|
|
208
|
+
### `vite.config.ts`
|
|
209
|
+
|
|
210
|
+
Parameterized fields:
|
|
211
|
+
|
|
212
|
+
| Field | What changes |
|
|
213
|
+
| --------------- | ----------------------------------- |
|
|
214
|
+
| `server.port` | Each app gets a unique dev port |
|
|
215
|
+
|
|
216
|
+
Template:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
import { defineConfig } from 'vitest/config';
|
|
220
|
+
import react from '@vitejs/plugin-react';
|
|
221
|
+
|
|
222
|
+
export default defineConfig({
|
|
223
|
+
plugins: [react()],
|
|
224
|
+
server: {
|
|
225
|
+
port: {{PORT}},
|
|
226
|
+
},
|
|
227
|
+
test: {
|
|
228
|
+
globals: true,
|
|
229
|
+
environment: 'jsdom',
|
|
230
|
+
setupFiles: ['./src/test/setup.ts'],
|
|
231
|
+
include: ['src/test/**/*.test.{ts,tsx}'],
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Note: Import `defineConfig` from `vitest/config` (not `vite`) so the `test` block is typed. If the app has no tests, the `test` block and vitest devDependencies may be omitted.
|
|
237
|
+
|
|
238
|
+
### `Layout.tsx`
|
|
239
|
+
|
|
240
|
+
Parameterized field: the app name passed to `createStandaloneHTML()`.
|
|
241
|
+
|
|
242
|
+
### `router.tsx`
|
|
243
|
+
|
|
244
|
+
Fully app-specific. Defines routes as children of the Layout element.
|
|
245
|
+
|
|
246
|
+
### `src/styles/index.css`
|
|
247
|
+
|
|
248
|
+
The `@theme` block and `@layer base` are standard. Apps may add extra `@layer components` rules for app-specific component styles.
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Layout Pattern
|
|
253
|
+
|
|
254
|
+
Every app's `Layout.tsx` follows this exact flow:
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
1. isInFrontierApp() -- Check if running inside Frontier Wallet iframe
|
|
258
|
+
2. If NOT in Frontier:
|
|
259
|
+
createStandaloneHTML('App Name') -- Render a standalone fallback page
|
|
260
|
+
return early
|
|
261
|
+
3. If loading:
|
|
262
|
+
Show spinner
|
|
263
|
+
4. If in Frontier and ready:
|
|
264
|
+
<SdkProvider>
|
|
265
|
+
<Outlet /> -- react-router child routes render here
|
|
266
|
+
</SdkProvider>
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Implementation:
|
|
270
|
+
|
|
271
|
+
```tsx
|
|
272
|
+
import { useEffect, useState } from 'react';
|
|
273
|
+
import { Outlet } from 'react-router-dom';
|
|
274
|
+
import { isInFrontierApp, createStandaloneHTML } from '@frontiertower/frontier-sdk/ui-utils';
|
|
275
|
+
import { SdkProvider } from '../lib/sdk-context';
|
|
276
|
+
|
|
277
|
+
export const Layout = () => {
|
|
278
|
+
const [loading, setLoading] = useState(true);
|
|
279
|
+
const [standaloneHtml, setStandaloneHtml] = useState('');
|
|
280
|
+
|
|
281
|
+
useEffect(() => {
|
|
282
|
+
const inFrontier = isInFrontierApp();
|
|
283
|
+
|
|
284
|
+
if (!inFrontier) {
|
|
285
|
+
setStandaloneHtml(createStandaloneHTML('{{APP_NAME}}'));
|
|
286
|
+
setLoading(false);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
setLoading(false);
|
|
291
|
+
}, []);
|
|
292
|
+
|
|
293
|
+
if (standaloneHtml) {
|
|
294
|
+
return (
|
|
295
|
+
<div
|
|
296
|
+
className="min-h-screen bg-background text-foreground"
|
|
297
|
+
dangerouslySetInnerHTML={{ __html: standaloneHtml }}
|
|
298
|
+
/>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (loading) {
|
|
303
|
+
return (
|
|
304
|
+
<div className="loading-screen">
|
|
305
|
+
<div className="spinner spinner-lg" />
|
|
306
|
+
<p className="text-sm text-muted-foreground">Loading...</p>
|
|
307
|
+
</div>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return (
|
|
312
|
+
<SdkProvider>
|
|
313
|
+
<Outlet />
|
|
314
|
+
</SdkProvider>
|
|
315
|
+
);
|
|
316
|
+
};
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
The `isInFrontierApp()` function checks `window.self !== window.top` -- it returns `true` when the app is loaded inside an iframe (the Frontier Wallet PWA embeds apps this way).
|
|
320
|
+
|
|
321
|
+
The `createStandaloneHTML()` fallback renders a branded page telling the user to open the app inside Frontier Wallet.
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## Router Variant (Standard)
|
|
326
|
+
|
|
327
|
+
All current apps use react-router-dom with `createBrowserRouter`. The router file defines a single root route with `Layout` as the element and feature views as children:
|
|
328
|
+
|
|
329
|
+
```tsx
|
|
330
|
+
import { createBrowserRouter } from 'react-router-dom';
|
|
331
|
+
import { Layout } from './views/Layout';
|
|
332
|
+
// ... import views
|
|
333
|
+
|
|
334
|
+
export const router = createBrowserRouter([
|
|
335
|
+
{
|
|
336
|
+
path: '/',
|
|
337
|
+
element: <Layout />,
|
|
338
|
+
children: [
|
|
339
|
+
{ index: true, element: <Home /> },
|
|
340
|
+
{ path: 'rooms', element: <Rooms /> },
|
|
341
|
+
{ path: 'book/:categoryId', element: <Book /> },
|
|
342
|
+
// ... more routes
|
|
343
|
+
],
|
|
344
|
+
},
|
|
345
|
+
]);
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Single-Component Variant
|
|
349
|
+
|
|
350
|
+
For very simple apps that need only one view, the router can be omitted. In this case:
|
|
351
|
+
- `main.tsx` renders the Layout directly instead of `<RouterProvider>`
|
|
352
|
+
- `Layout.tsx` renders the single view component instead of `<Outlet />`
|
|
353
|
+
- No `router.tsx` file needed
|
|
354
|
+
- `react-router-dom` can be removed from dependencies
|
|
355
|
+
|
|
356
|
+
### `main.tsx` (Identical Pattern)
|
|
357
|
+
|
|
358
|
+
```tsx
|
|
359
|
+
import { StrictMode } from 'react';
|
|
360
|
+
import { createRoot } from 'react-dom/client';
|
|
361
|
+
import { RouterProvider } from 'react-router-dom';
|
|
362
|
+
import { router } from './router';
|
|
363
|
+
import './styles/index.css';
|
|
364
|
+
|
|
365
|
+
const rootElement = document.getElementById('root');
|
|
366
|
+
|
|
367
|
+
if (!rootElement) {
|
|
368
|
+
throw new Error('Root element #root not found in document.');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
createRoot(rootElement).render(
|
|
372
|
+
<StrictMode>
|
|
373
|
+
<RouterProvider router={router} />
|
|
374
|
+
</StrictMode>
|
|
375
|
+
);
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
## Package Scripts
|
|
381
|
+
|
|
382
|
+
| Script | Command | Purpose |
|
|
383
|
+
| --------- | ----------------- | ------------------------------------- |
|
|
384
|
+
| `dev` | `vite` | Start dev server with HMR |
|
|
385
|
+
| `build` | `tsc && vite build` | Type-check then production build |
|
|
386
|
+
| `preview` | `vite preview` | Preview production build locally |
|
|
387
|
+
| `lint` | `tsc --noEmit` | Type-check without emitting |
|
|
388
|
+
| `test` | `vitest run` | Run tests once (CI mode) |
|
|
389
|
+
|
|
390
|
+
The `test` script is only included when the app has tests and vitest is in devDependencies.
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
## Dark Theme CSS Variables
|
|
395
|
+
|
|
396
|
+
All apps use a dark-only theme defined in `src/styles/index.css` via Tailwind 4's `@theme` directive. These CSS custom properties are available as Tailwind utilities (e.g., `bg-background`, `text-foreground`, `border-border`).
|
|
397
|
+
|
|
398
|
+
### Color Tokens
|
|
399
|
+
|
|
400
|
+
| Token | Value | Usage |
|
|
401
|
+
| ------------------------ | --------- | ------------------------------- |
|
|
402
|
+
| `--color-primary` | `#764AE2` | Primary actions, links |
|
|
403
|
+
| `--color-primary-foreground` | `#ffffff` | Text on primary backgrounds |
|
|
404
|
+
| `--color-accent` | `#E4DCF9` | Accent highlights |
|
|
405
|
+
| `--color-accent-foreground` | `#0A0A0A` | Text on accent backgrounds |
|
|
406
|
+
| `--color-alert` | `#EF4444` | Error states |
|
|
407
|
+
| `--color-alert-foreground` | `#F5F5F5` | Text on alert backgrounds |
|
|
408
|
+
| `--color-danger` | `#F87171` | Destructive actions |
|
|
409
|
+
| `--color-danger-foreground` | `#ffffff` | Text on danger backgrounds |
|
|
410
|
+
| `--color-success` | `#10b981` | Success states |
|
|
411
|
+
| `--color-background` | `#000000` | Page background |
|
|
412
|
+
| `--color-foreground` | `#FAFAFA` | Default text |
|
|
413
|
+
| `--color-muted` | `#1E293B` | Muted background |
|
|
414
|
+
| `--color-muted-foreground` | `#A3A3A3` | Secondary text |
|
|
415
|
+
| `--color-muted-background` | `#262626` | Alternative muted bg |
|
|
416
|
+
| `--color-card` | `#0A0A0A` | Card backgrounds |
|
|
417
|
+
| `--color-card-foreground` | `#F5F5F5` | Card text |
|
|
418
|
+
| `--color-border` | `#242424` | Borders |
|
|
419
|
+
| `--color-input` | `#242424` | Input borders/backgrounds |
|
|
420
|
+
| `--color-ring` | `#B6B1F6` | Focus rings |
|
|
421
|
+
| `--color-outline` | `#404040` | Outlines |
|
|
422
|
+
|
|
423
|
+
### Typography
|
|
424
|
+
|
|
425
|
+
| Token | Value |
|
|
426
|
+
| ------------- | -------------------------------------------------- |
|
|
427
|
+
| `--font-sans` | `"Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif` |
|
|
428
|
+
|
|
429
|
+
### Base Styles
|
|
430
|
+
|
|
431
|
+
The `@layer base` block sets:
|
|
432
|
+
- `box-sizing: border-box` on all elements
|
|
433
|
+
- Font smoothing (webkit + moz)
|
|
434
|
+
- `body` font-size `0.875rem` (14px), line-height `1.5`
|
|
435
|
+
- `-webkit-user-select: none` and `touch-action: manipulation` for mobile feel
|
|
436
|
+
- `#root` as a flex column filling viewport height
|
|
437
|
+
- Heading weights at `600`, line-height `1.25`
|
|
438
|
+
- Paragraph color set to `--color-muted-foreground`
|
|
439
|
+
- Link color set to `--color-primary`
|
|
440
|
+
|
|
441
|
+
### Component Styles
|
|
442
|
+
|
|
443
|
+
The `@layer components` block provides:
|
|
444
|
+
- `.loading-screen` -- centered full-height container
|
|
445
|
+
- `.spinner` / `.spinner-lg` -- CSS-only spinning loader using border-top trick
|
|
446
|
+
|
|
447
|
+
Apps may add additional component styles in this layer for app-specific needs (e.g., phone input styling).
|
|
448
|
+
|
|
449
|
+
---
|
|
450
|
+
|
|
451
|
+
## Test Setup Pattern
|
|
452
|
+
|
|
453
|
+
### `src/test/setup.ts`
|
|
454
|
+
|
|
455
|
+
```ts
|
|
456
|
+
import '@testing-library/jest-dom';
|
|
457
|
+
import { vi } from 'vitest';
|
|
458
|
+
|
|
459
|
+
// Mock window.open for external links
|
|
460
|
+
vi.stubGlobal('open', vi.fn());
|
|
461
|
+
|
|
462
|
+
// Mock ResizeObserver
|
|
463
|
+
vi.stubGlobal('ResizeObserver', vi.fn().mockImplementation(() => ({
|
|
464
|
+
observe: vi.fn(),
|
|
465
|
+
unobserve: vi.fn(),
|
|
466
|
+
disconnect: vi.fn(),
|
|
467
|
+
})));
|
|
468
|
+
|
|
469
|
+
// Mock IntersectionObserver
|
|
470
|
+
vi.stubGlobal('IntersectionObserver', vi.fn().mockImplementation(() => ({
|
|
471
|
+
observe: vi.fn(),
|
|
472
|
+
unobserve: vi.fn(),
|
|
473
|
+
disconnect: vi.fn(),
|
|
474
|
+
})));
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### Test Directory Structure
|
|
478
|
+
|
|
479
|
+
Tests mirror the source structure under `src/test/`:
|
|
480
|
+
|
|
481
|
+
```
|
|
482
|
+
src/test/
|
|
483
|
+
setup.ts # Global test setup
|
|
484
|
+
lib/ # Tests for src/lib/
|
|
485
|
+
views/ # Tests for src/views/
|
|
486
|
+
hooks/ # Tests for src/hooks/
|
|
487
|
+
components/ # Tests for src/components/
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### Vitest Configuration
|
|
491
|
+
|
|
492
|
+
Configured in `vite.config.ts`:
|
|
493
|
+
- `globals: true` -- no need to import `describe`, `it`, `expect`
|
|
494
|
+
- `environment: 'jsdom'` -- DOM simulation
|
|
495
|
+
- `setupFiles: ['./src/test/setup.ts']` -- runs before every test file
|
|
496
|
+
- `include: ['src/test/**/*.test.{ts,tsx}']` -- only files under `src/test/`
|
|
497
|
+
|
|
498
|
+
### tsconfig.json Test Handling
|
|
499
|
+
|
|
500
|
+
- `"types": ["vitest/globals", "@testing-library/jest-dom"]` -- global type augmentation
|
|
501
|
+
- `"exclude": ["src/test", "**/*.test.ts", "**/*.test.tsx"]` -- test files excluded from production type-check (`tsc --noEmit` / `tsc && vite build`)
|