@archetypeai/ds-cli 0.9.0 → 0.10.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.
Files changed (32) hide show
  1. package/README.md +3 -3
  2. package/commands/create.js +2 -2
  3. package/commands/init.js +2 -2
  4. package/files/AGENTS.md +128 -70
  5. package/files/CLAUDE.md +128 -70
  6. package/files/ds-manifest.json +998 -1023
  7. package/lib/add-ds-config-codeagent.js +3 -57
  8. package/package.json +2 -2
  9. package/files/LICENSE +0 -21
  10. package/files/rules/accessibility.md +0 -268
  11. package/files/rules/charts.md +0 -256
  12. package/files/rules/components.md +0 -251
  13. package/files/rules/design-principles.md +0 -71
  14. package/files/rules/frontend-architecture.md +0 -86
  15. package/files/rules/linting.md +0 -31
  16. package/files/rules/state.md +0 -373
  17. package/files/rules/styling.md +0 -142
  18. package/files/skills/apply-ds/SKILL.md +0 -121
  19. package/files/skills/apply-ds/scripts/audit.sh +0 -169
  20. package/files/skills/apply-ds/scripts/setup.sh +0 -153
  21. package/files/skills/build-component/SKILL.md +0 -153
  22. package/files/skills/create-dashboard/SKILL.md +0 -220
  23. package/files/skills/deploy-worker/SKILL.md +0 -231
  24. package/files/skills/deploy-worker/references/wrangler-commands.md +0 -327
  25. package/files/skills/fix-accessibility/SKILL.md +0 -232
  26. package/files/skills/fix-metadata/SKILL.md +0 -118
  27. package/files/skills/fix-metadata/assets/favicon.ico +0 -0
  28. package/files/skills/setup-chart/SKILL.md +0 -223
  29. package/files/skills/setup-chart/data/embedding.csv +0 -42
  30. package/files/skills/setup-chart/data/timeseries.csv +0 -173
  31. package/files/skills/setup-chart/references/scatter-chart.md +0 -229
  32. package/files/skills/setup-chart/references/sensor-chart.md +0 -156
@@ -1,373 +0,0 @@
1
- ---
2
- paths:
3
- - '**/*.svelte'
4
- - '**/*.svelte.js'
5
- - '**/*.svelte.ts'
6
- ---
7
-
8
- # Svelte 5 State Management
9
-
10
- Svelte 5 uses runes - special `$` prefixed primitives that control reactivity.
11
-
12
- ## $state - Reactive State
13
-
14
- Declare reactive state that triggers UI updates when changed:
15
-
16
- ```svelte
17
- <script>
18
- let count = $state(0);
19
- let user = $state({ name: 'Alice', age: 30 });
20
- </script>
21
-
22
- <button onclick={() => count++}>
23
- Clicked {count} times
24
- </button>
25
- ```
26
-
27
- ### Deep Reactivity
28
-
29
- Objects and arrays become deeply reactive proxies:
30
-
31
- ```svelte
32
- <script>
33
- let todos = $state([{ done: false, text: 'Learn Svelte 5' }]);
34
-
35
- // This triggers updates - the array is a proxy
36
- todos.push({ done: false, text: 'Build app' });
37
-
38
- // This also triggers updates
39
- todos[0].done = true;
40
- </script>
41
- ```
42
-
43
- ### $state.raw - Non-Proxied State
44
-
45
- For large objects you don't plan to mutate, use `$state.raw` for better performance:
46
-
47
- ```svelte
48
- <script>
49
- // Must reassign entirely, can't mutate
50
- let data = $state.raw({ items: largeArray });
51
-
52
- // This won't trigger updates
53
- data.items.push(newItem);
54
-
55
- // This will - full reassignment
56
- data = { items: [...data.items, newItem] };
57
- </script>
58
- ```
59
-
60
- ### $state.snapshot - Get Plain Object
61
-
62
- Get a non-reactive copy of a state proxy (useful for APIs that don't expect proxies):
63
-
64
- ```svelte
65
- <script>
66
- let state = $state({ count: 0 });
67
-
68
- function save() {
69
- // Pass plain object to external API
70
- const snapshot = $state.snapshot(state);
71
- localStorage.setItem('state', JSON.stringify(snapshot));
72
- }
73
- </script>
74
- ```
75
-
76
- ### Class Fields
77
-
78
- Use $state in class fields:
79
-
80
- ```svelte
81
- <script>
82
- class Counter {
83
- count = $state(0);
84
-
85
- increment() {
86
- this.count++;
87
- }
88
- }
89
-
90
- let counter = new Counter();
91
- </script>
92
- ```
93
-
94
- ## $derived - Computed Values
95
-
96
- Declare values that automatically update when dependencies change:
97
-
98
- ```svelte
99
- <script>
100
- let count = $state(0);
101
- let doubled = $derived(count * 2);
102
- let quadrupled = $derived(doubled * 2);
103
- </script>
104
-
105
- <p>{count} × 2 = {doubled}</p><p>{count} × 4 = {quadrupled}</p>
106
- ```
107
-
108
- ### $derived.by - Complex Derivations
109
-
110
- For multi-line logic, use `$derived.by`:
111
-
112
- ```svelte
113
- <script>
114
- let items = $state([1, 2, 3, 4, 5]);
115
-
116
- let stats = $derived.by(() => {
117
- const sum = items.reduce((a, b) => a + b, 0);
118
- const avg = sum / items.length;
119
- const max = Math.max(...items);
120
- return { sum, avg, max };
121
- });
122
- </script>
123
-
124
- <p>Sum: {stats.sum}, Avg: {stats.avg}, Max: {stats.max}</p>
125
- ```
126
-
127
- ### Overriding Derived Values (Optimistic UI)
128
-
129
- Derived values can be temporarily overridden:
130
-
131
- ```svelte
132
- <script>
133
- let { post } = $props();
134
- let likes = $derived(post.likes);
135
-
136
- async function like() {
137
- likes++; // Optimistic update
138
- try {
139
- await api.likePost(post.id);
140
- } catch {
141
- likes--; // Rollback on failure
142
- }
143
- }
144
- </script>
145
- ```
146
-
147
- ## $effect - Side Effects
148
-
149
- Run code when reactive dependencies change:
150
-
151
- ```svelte
152
- <script>
153
- let count = $state(0);
154
-
155
- $effect(() => {
156
- console.log('Count changed to', count);
157
- document.title = `Count: ${count}`;
158
- });
159
- </script>
160
- ```
161
-
162
- ### Teardown / Cleanup
163
-
164
- Return a function to clean up before re-running or on destroy:
165
-
166
- ```svelte
167
- <script>
168
- let interval = $state(1000);
169
-
170
- $effect(() => {
171
- const id = setInterval(() => {
172
- console.log('tick');
173
- }, interval);
174
-
175
- // Cleanup: runs before re-run and on component destroy
176
- return () => clearInterval(id);
177
- });
178
- </script>
179
- ```
180
-
181
- ### $effect.pre - Run Before DOM Updates
182
-
183
- Rarely needed - runs before DOM updates:
184
-
185
- ```svelte
186
- <script>
187
- let messages = $state([]);
188
- let container;
189
-
190
- $effect.pre(() => {
191
- // Check scroll position before DOM updates
192
- messages.length; // Track this dependency
193
-
194
- if (container && shouldAutoScroll(container)) {
195
- // Will scroll after DOM updates
196
- tick().then(() => container.scrollTo(0, container.scrollHeight));
197
- }
198
- });
199
- </script>
200
- ```
201
-
202
- ### When NOT to Use $effect
203
-
204
- **Don't sync state - use $derived instead:**
205
-
206
- ```svelte
207
- <script>
208
- let count = $state(0);
209
-
210
- // BAD - don't do this
211
- let doubled = $state(0);
212
- $effect(() => {
213
- doubled = count * 2;
214
- });
215
-
216
- // GOOD - use $derived
217
- let doubled = $derived(count * 2);
218
- </script>
219
- ```
220
-
221
- **Don't create infinite loops:**
222
-
223
- ```svelte
224
- <script>
225
- let count = $state(0);
226
-
227
- // BAD - infinite loop! reads and writes count
228
- $effect(() => {
229
- count = count + 1;
230
- });
231
- </script>
232
- ```
233
-
234
- **Use callbacks for linked values instead of effects:**
235
-
236
- ```svelte
237
- <script>
238
- let spent = $state(0);
239
- let left = $derived(100 - spent);
240
-
241
- // Update spent via callback, not effect
242
- function updateLeft(newLeft) {
243
- spent = 100 - newLeft;
244
- }
245
- </script>
246
-
247
- <input type="range" bind:value={spent} max={100} />
248
- <input type="range" value={left} oninput={(e) => updateLeft(+e.target.value)} max={100} />
249
- ```
250
-
251
- ## $bindable - Two-Way Binding
252
-
253
- Enable parent components to bind to a prop:
254
-
255
- ```svelte
256
- <!-- FancyInput.svelte -->
257
- <script>
258
- let { value = $bindable(''), ...props } = $props();
259
- </script>
260
-
261
- <input bind:value {value} {...props} />
262
- ```
263
-
264
- ```svelte
265
- <!-- Parent.svelte -->
266
- <script>
267
- import FancyInput from './FancyInput.svelte';
268
- let name = $state('');
269
- </script>
270
-
271
- <FancyInput bind:value={name} /><p>Hello, {name}!</p>
272
- ```
273
-
274
- See [components.md](./components.md) for more on `$props()` and `$bindable()`.
275
-
276
- ## $inspect - Debugging (Dev Only)
277
-
278
- Log when values change (removed in production builds):
279
-
280
- ```svelte
281
- <script>
282
- let count = $state(0);
283
- let user = $state({ name: 'Alice' });
284
-
285
- // Logs whenever count or user changes
286
- $inspect(count, user);
287
- </script>
288
- ```
289
-
290
- ### Custom Inspection
291
-
292
- ```svelte
293
- <script>
294
- let count = $state(0);
295
-
296
- $inspect(count).with((type, value) => {
297
- if (type === 'update') {
298
- debugger; // Break on changes
299
- }
300
- });
301
- </script>
302
- ```
303
-
304
- ### Trace Effect Dependencies
305
-
306
- ```svelte
307
- <script>
308
- $effect(() => {
309
- $inspect.trace(); // Must be first statement
310
- doSomethingComplex();
311
- });
312
- </script>
313
- ```
314
-
315
- ## Common Gotchas
316
-
317
- ### Destructuring Breaks Reactivity
318
-
319
- ```svelte
320
- <script>
321
- let user = $state({ name: 'Alice', age: 30 });
322
-
323
- // BAD - name and age are not reactive
324
- let { name, age } = user;
325
-
326
- // GOOD - access via object
327
- // In template: {user.name}, {user.age}
328
- </script>
329
- ```
330
-
331
- ### Async Code Doesn't Track Dependencies
332
-
333
- Only synchronously-read state inside `$effect` is tracked. State read inside `setTimeout`, `await`, or callbacks is not tracked.
334
-
335
- ### Exporting Reassigned State
336
-
337
- Can't directly export state that gets reassigned. Wrap in an object instead:
338
-
339
- ```javascript
340
- // state.svelte.js
341
- export const counter = $state({ count: 0 });
342
- export function increment() {
343
- counter.count++;
344
- }
345
- ```
346
-
347
- ### Effects Only Run in Browser
348
-
349
- `$effect` doesn't run during SSR:
350
-
351
- ```svelte
352
- <script>
353
- $effect(() => {
354
- // Safe to use browser APIs here
355
- window.addEventListener('resize', handleResize);
356
- return () => window.removeEventListener('resize', handleResize);
357
- });
358
- </script>
359
- ```
360
-
361
- ## Quick Reference
362
-
363
- | Rune | Purpose | Example |
364
- | ------------------- | ----------------- | ---------------------------------------- |
365
- | `$state(value)` | Reactive state | `let count = $state(0)` |
366
- | `$state.raw(value)` | Non-proxied state | `let data = $state.raw(bigObject)` |
367
- | `$derived(expr)` | Computed value | `let doubled = $derived(count * 2)` |
368
- | `$derived.by(fn)` | Complex computed | `$derived.by(() => { ... })` |
369
- | `$effect(fn)` | Side effect | `$effect(() => { ... })` |
370
- | `$effect.pre(fn)` | Pre-DOM effect | `$effect.pre(() => { ... })` |
371
- | `$props()` | Component props | `let { foo } = $props()` |
372
- | `$bindable()` | Two-way prop | `let { value = $bindable() } = $props()` |
373
- | `$inspect(...)` | Debug logging | `$inspect(count)` |
@@ -1,142 +0,0 @@
1
- ---
2
- paths:
3
- - '**/*.css'
4
- - '**/*.svelte'
5
- ---
6
-
7
- # Styling Rules
8
-
9
- ## Semantic Tokens
10
-
11
- For the complete token reference including dark mode overrides, type scale (h1–h6, p, small), and spacing definitions, read `node_modules/@archetypeai/ds-lib-tokens/theme.css`.
12
-
13
- Token naming pattern: `bg-{token}`, `text-{token}-foreground`, `border-{token}`.
14
-
15
- Core tokens: `background`, `foreground`, `primary`, `secondary`, `muted`, `card`, `accent`, `destructive`, `border`, `input`, `ring`.
16
-
17
- ATAI brand tokens: `atai-neutral`, `atai-good`, `atai-warning`, `atai-critical`.
18
-
19
- Chart series: `--chart-1` through `--chart-5` (see `@rules/charts`).
20
-
21
- ## When to Use Semantic vs Standard Tailwind
22
-
23
- ### Use Semantic Tokens For:
24
-
25
- ```svelte
26
- <!-- Themed colors that should change with light/dark mode -->
27
- <div class="bg-background text-foreground">
28
- <div class="bg-card border-border">
29
- <button class="bg-primary text-primary-foreground">
30
- <span class="text-muted-foreground">
31
- ```
32
-
33
- ### Use Standard Tailwind For:
34
-
35
- ```svelte
36
- <!-- Spacing and sizing -->
37
- <div class="p-4 gap-2 w-full h-screen">
38
-
39
- <!-- Layout utilities -->
40
- <div class="flex items-center justify-between">
41
- <div class="grid grid-cols-2 gap-4">
42
- <div class="absolute top-0 left-0">
43
-
44
- <!-- One-off custom colors -->
45
- <div class="bg-linear-to-r from-purple-500 to-pink-500">
46
- <div class="text-amber-500"> <!-- specific non-themed color -->
47
-
48
- <!-- Responsive breakpoints -->
49
- <div class="sm:flex md:grid lg:hidden">
50
-
51
- <!-- Animations and transitions -->
52
- <div class="transition-all duration-200 ease-in-out">
53
- ```
54
-
55
- ## Dark Mode
56
-
57
- Dark mode activates via `.dark` class on the root element:
58
-
59
- ```html
60
- <html class="dark">
61
- ...
62
- </html>
63
- ```
64
-
65
- Semantic tokens automatically switch values in dark mode. No manual dark: prefixes needed for themed colors:
66
-
67
- ```svelte
68
- <!-- This works in both light and dark mode automatically -->
69
- <div class="bg-background text-foreground border-border">
70
- ```
71
-
72
- For custom dark mode overrides, use the `dark:` prefix:
73
-
74
- ```svelte
75
- <div class="bg-white dark:bg-slate-900"> <!-- custom dark override -->
76
- ```
77
-
78
- For class merging with `cn()`, see `@rules/components`.
79
-
80
- ## CSS Import Order
81
-
82
- In your global CSS file:
83
-
84
- ```css
85
- @import 'tailwindcss';
86
- @import 'tw-animate-css';
87
- @import '@archetypeai/ds-lib-tokens/fonts.css';
88
- @import '@archetypeai/ds-lib-tokens/theme.css';
89
- @source '../../node_modules/@archetypeai/ds-ui-svelte-console/dist';
90
- @source '../../node_modules/@archetypeai/ds-ui-svelte-labs/dist';
91
- ```
92
-
93
- Order matters — `tailwindcss` first, then the tokens theme, then the package `@source` directives (they make Tailwind emit classes used inside the package dists).
94
-
95
- ## Tailwind v4 Specifics
96
-
97
- ### @theme Directive
98
-
99
- Custom theme values are defined with `@theme`:
100
-
101
- ```css
102
- @theme {
103
- --font-sans: 'PP Neue Montreal', system-ui, sans-serif;
104
- --radius-md: var(--radius);
105
- --spacing-md: 0.75rem;
106
- --color-primary: var(--primary);
107
- }
108
- ```
109
-
110
- ### CSS Variables in Classes
111
-
112
- You can use CSS variables directly:
113
-
114
- ```svelte
115
- <div style:--legend-color={seriesColor}>
116
- <span class="bg-(--legend-color)"> </span>
117
- </div>
118
- ```
119
-
120
- ### No @apply in Components
121
-
122
- Prefer Tailwind classes directly in markup. Use @apply only in base layer CSS:
123
-
124
- ```css
125
- /* In theme.css - OK */
126
- @layer base {
127
- body {
128
- @apply bg-background text-foreground;
129
- }
130
- }
131
- ```
132
-
133
- ```svelte
134
- <!-- In components - use classes directly -->
135
- <div class="bg-background text-foreground">
136
- ```
137
-
138
- ## Common Patterns
139
-
140
- For focus ring, disabled state, and data-state animation CSS patterns, see `@rules/accessibility`.
141
-
142
- For icon sizing conventions, see `@rules/components`.
@@ -1,121 +0,0 @@
1
- ---
2
- name: apply-ds
3
- description: Apply DS tokens and components to an existing demo initialized with ds-cli init.
4
- allowed-tools: Read, Edit, Grep, Bash
5
- ---
6
-
7
- # Applying the Design System to a Demo
8
-
9
- This skill assumes `npx @archetypeai/ds-cli init` has already been run. The project has tokens, Tailwind v4, and both design system packages (`@archetypeai/ds-ui-svelte-console`, `@archetypeai/ds-ui-svelte-labs`) installed. The goal is to migrate the demo's existing UI to use DS components and semantic tokens. Read `ds-manifest.json` at the project root for the full component catalog.
10
-
11
- ## Steps
12
-
13
- 1. Audit for non-DS patterns
14
- 2. Replace raw colors with semantic tokens
15
- 3. Replace native elements with DS components
16
- 4. Apply layout patterns
17
- 5. Set up linting and formatting
18
- 6. Verify
19
-
20
- ## Step 1: Audit for Non-DS Patterns
21
-
22
- Run the `scripts/audit.sh` script located in this skill's directory from the project root. The script scans all `.svelte` files and reports:
23
-
24
- - **Raw Tailwind colors** — utilities like `bg-gray-100`, `text-blue-500`, `border-red-300` that should be semantic tokens
25
- - **Native HTML elements** — `<button>`, `<input>`, `<table>`, `<select>`, `<textarea>`, `<dialog>`, `<hr>` that have DS component equivalents
26
- - **Hardcoded colors** — inline `color:`, `background:`, `background-color:`, `border-color:` style values
27
- - **Class concatenation** — string templates or `+` concatenation on class attributes instead of `cn()`
28
-
29
- Review the output to prioritize what to migrate.
30
-
31
- ## Step 2: Replace Raw Colors with Semantic Tokens
32
-
33
- Replace raw Tailwind color utilities with semantic equivalents throughout `.svelte` files:
34
-
35
- | Raw Tailwind | Semantic Token |
36
- | --- | --- |
37
- | `bg-white` | `bg-background` |
38
- | `bg-black` | `bg-foreground` |
39
- | `text-black` | `text-foreground` |
40
- | `text-white` | `text-background` |
41
- | `bg-gray-50`, `bg-gray-100` | `bg-muted` |
42
- | `text-gray-500`, `text-gray-600` | `text-muted-foreground` |
43
- | `bg-gray-200`, `bg-gray-300` | `bg-accent` |
44
- | `border-gray-200`, `border-gray-300` | `border-border` |
45
- | `bg-slate-900`, `bg-gray-900` | `bg-primary` |
46
- | `text-slate-900`, `text-gray-900` | `text-primary` |
47
- | `bg-red-*` | `bg-destructive` |
48
- | `text-red-*` | `text-destructive` |
49
- | `bg-green-*` | `bg-atai-good` |
50
- | `text-green-*` | `text-atai-good` |
51
- | `bg-yellow-*` | `bg-atai-warning` |
52
- | `text-yellow-*` | `text-atai-warning` |
53
- | `bg-blue-*` | `bg-atai-neutral` |
54
- | `text-blue-*` | `text-atai-neutral` |
55
-
56
- Use judgement — not every raw color maps 1:1. Context matters:
57
-
58
- - A `bg-gray-100` card background → `bg-card` or `bg-muted`
59
- - A `bg-gray-100` hover state → `bg-accent`
60
- - A `text-gray-400` placeholder → `text-muted-foreground`
61
- - A `border-gray-200` divider → `border-border`
62
-
63
- Leave non-color utilities (`p-4`, `flex`, `rounded-lg`, etc.) unchanged.
64
-
65
- ## Step 3: Replace Native Elements with DS Components
66
-
67
- Swap native HTML elements for their DS component equivalents. Add imports as needed.
68
-
69
- | Native Element | DS Component | Import From |
70
- | --- | --- | --- |
71
- | `<button>` | `<Button>` | `@archetypeai/ds-ui-svelte-console/primitives/button` |
72
- | `<input>` | `<Input>` | `@archetypeai/ds-ui-svelte-console/primitives/input` |
73
- | `<textarea>` | `<Textarea>` | `@archetypeai/ds-ui-svelte-console/primitives/textarea` |
74
- | `<select>` | `<Select.Root>` + `<Select.Trigger>` + `<Select.Content>` + `<Select.Item>` | `@archetypeai/ds-ui-svelte-console/primitives/select` |
75
- | `<table>` | `<Table.Root>` + `<Table.Header>` + `<Table.Row>` + `<Table.Head>` + `<Table.Body>` + `<Table.Cell>` | `@archetypeai/ds-ui-svelte-console/primitives/table` |
76
- | `<dialog>` | `<Dialog.Root>` + `<Dialog.Content>` + `<Dialog.Header>` + `<Dialog.Title>` | `@archetypeai/ds-ui-svelte-console/primitives/dialog` |
77
- | `<hr>` | `<Separator>` | `@archetypeai/ds-ui-svelte-console/primitives/separator` |
78
- | `<label>` | `<Label>` | `@archetypeai/ds-ui-svelte-console/primitives/label` |
79
- | `<a>` (styled as button) | `<Button variant="link">` or `<Button>` with `href` | `@archetypeai/ds-ui-svelte-console/primitives/button` |
80
-
81
- For each replacement:
82
-
83
- 1. Add the import to the `<script>` block
84
- 2. Replace the element and map attributes (`class` → `class`, `onclick` → `onclick`, etc.)
85
- 3. Use `cn()` from `$lib/utils.js` for merging classes — never raw concatenation
86
- 4. Map native variants: e.g. a small styled button → `<Button size="sm">`, a ghost-styled button → `<Button variant="ghost">`
87
-
88
- ## Step 4: Apply Layout Patterns
89
-
90
- Identify structural opportunities to use DS composition patterns:
91
-
92
- - **Cards** — wrap content sections in `<Card.Root>` / `<Card.Header>` / `<Card.Content>` instead of raw `<div>` with border/shadow classes
93
- - **Separators** — replace `<hr>` or border-bottom hacks with `<Separator>`
94
- - **Badges** — replace styled `<span>` status indicators with `<Badge>`
95
- - **Spinners** — replace loading placeholders with `<Spinner>`
96
- - **Empty states** — replace ad-hoc "no data" markup with `<EmptyState>`
97
- - **Tooltips** — replace `title` attributes with `<Tooltip.Root>` / `<Tooltip.Trigger>` / `<Tooltip.Content>`
98
-
99
- Import console components from `@archetypeai/ds-ui-svelte-console/primitives/<name>` and labs components (menubar, charts, slider, switch, ...) from `@archetypeai/ds-ui-svelte-labs/primitives/<name>` — see `ds-manifest.json` for the catalog.
100
-
101
- ## Step 5: Lint and Format
102
-
103
- Run linting and formatting to clean up the migrated code:
104
-
105
- ```bash
106
- npm run lint:fix && npm run format
107
- ```
108
-
109
- See `@rules/linting.md` for details.
110
-
111
- ## Step 6: Verify
112
-
113
- Confirm the design system is properly applied:
114
-
115
- - [ ] No raw Tailwind color utilities in `.svelte` files (re-run the audit script)
116
- - [ ] Native elements replaced with DS components where appropriate
117
- - [ ] Component imports resolve to the design system packages (`@archetypeai/ds-ui-svelte-console`, `@archetypeai/ds-ui-svelte-labs`) — or `$lib/components/ui/` only for deliberately modified registry source
118
- - [ ] `cn()` used for class merging — no string concatenation
119
- - [ ] Linting passes: `npm run lint && npm run format:check`
120
- - [ ] App builds without errors: `npm run build`
121
- - [ ] App runs correctly: `npm run dev`