@archetypeai/ds-cli 0.3.16 → 0.3.18

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 CHANGED
@@ -26,13 +26,19 @@ Sets up a SvelteKit + Tailwind v4 + shadcn-svelte project with design tokens, co
26
26
  |------|--------|---------|
27
27
  | `--framework` | `svelte` | prompt |
28
28
  | `--pm` | `npm`, `pnpm`, `bun`, `yarn` | prompt |
29
+ | `--fonts <path>` | path to PP Neue Montreal fonts folder | prompt |
30
+ | `--no-fonts` | skip font installation (use system fallback) | — |
29
31
  | `--no-components` | skip component install | install all |
30
32
  | `--codeagent` | `cursor`, `claude`, `none` | prompt |
33
+ | `--defaults` | force non-interactive mode | — |
31
34
 
32
35
  ```bash
33
- npx @archetypeai/ds-cli create my-app --framework svelte --pm pnpm --codeagent none
36
+ npx @archetypeai/ds-cli create my-app --pm pnpm --no-fonts --codeagent claude
37
+ npx @archetypeai/ds-cli create my-app --pm npm --fonts /path/to/fonts --codeagent claude
34
38
  ```
35
39
 
40
+ **Non-interactive mode** (no TTY or `--defaults`): all required flags must be specified. The CLI lists missing options and exits so agents can ask the user before proceeding.
41
+
36
42
  ---
37
43
 
38
44
  ### `init` → Add DS to an existing project
@@ -47,11 +53,15 @@ Run from a SvelteKit project root. Auto-detects your package manager, installs t
47
53
  | Flag | Values | Default |
48
54
  |------|--------|---------|
49
55
  | `--pm` | `npm`, `pnpm`, `bun`, `yarn` | auto-detect |
56
+ | `--fonts <path>` | path to PP Neue Montreal fonts folder | prompt |
57
+ | `--no-fonts` | skip font installation (use system fallback) | — |
50
58
  | `--no-components` | skip component install | install all |
51
59
  | `--codeagent` | `cursor`, `claude`, `none` | prompt |
60
+ | `--defaults` | force non-interactive mode | — |
52
61
 
53
62
  ```bash
54
- npx @archetypeai/ds-cli init --pm npm --codeagent none
63
+ npx @archetypeai/ds-cli init --pm npm --no-fonts --codeagent claude
64
+ npx @archetypeai/ds-cli init --pm npm --fonts /path/to/fonts --codeagent claude
55
65
  ```
56
66
 
57
67
  ---
package/bin.js CHANGED
@@ -28,19 +28,22 @@ Usage:
28
28
  Create flags:
29
29
  --framework <svelte> Framework to use (default: prompt)
30
30
  --pm <npm|pnpm|bun|yarn> Package manager (default: prompt)
31
- --fonts / --no-fonts Install internal fonts (default: prompt)
31
+ --fonts <path> Install fonts from local folder
32
+ --no-fonts Skip font installation
32
33
  --no-components Skip component installation (default: install all)
33
34
  --codeagent <cursor|claude|none> Agent configuration (default: prompt)
34
35
  --defaults Skip prompts, use sensible defaults
35
36
 
36
37
  Init flags:
37
38
  --pm <npm|pnpm|bun|yarn> Package manager (default: auto-detect)
38
- --fonts / --no-fonts Install internal fonts (default: prompt)
39
+ --fonts <path> Install fonts from local folder
40
+ --no-fonts Skip font installation
39
41
  --no-components Skip component installation (default: install all)
40
42
  --codeagent <cursor|claude|none> Agent configuration (default: prompt)
41
- --defaults Skip prompts, use sensible defaults
43
+ --defaults Force non-interactive mode
42
44
 
43
- Non-interactive shells (no TTY) automatically use defaults.
45
+ Non-interactive mode (no TTY or --defaults):
46
+ All required options must be specified via flags. Missing options are listed and the CLI exits.
44
47
 
45
48
  Add targets:
46
49
  ds-ui-svelte Install all design system components
@@ -14,7 +14,8 @@ import {
14
14
  createDemoPage,
15
15
  installAgentConfig
16
16
  } from '../lib/scaffold-ds-svelte-project.js';
17
- import { isInteractive, DEFAULTS, logDefaultsNotice } from '../lib/is-interactive.js';
17
+ import { validateFontsPath, installLocalFonts } from '../lib/install-fonts-local.js';
18
+ import { isInteractive, DEFAULTS } from '../lib/is-interactive.js';
18
19
 
19
20
  // parse flags
20
21
  export function parseFlags(args) {
@@ -22,6 +23,7 @@ export function parseFlags(args) {
22
23
  name: null,
23
24
  framework: null,
24
25
  pm: null,
26
+ fonts: null,
25
27
  components: true,
26
28
  agent: null,
27
29
  defaults: false
@@ -33,6 +35,10 @@ export function parseFlags(args) {
33
35
  flags.framework = args[++i];
34
36
  } else if (arg === '--pm' && args[i + 1]) {
35
37
  flags.pm = args[++i];
38
+ } else if (arg === '--fonts' && args[i + 1]) {
39
+ flags.fonts = args[++i];
40
+ } else if (arg === '--no-fonts') {
41
+ flags.fonts = false;
36
42
  } else if (arg === '--no-components') {
37
43
  flags.components = false;
38
44
  } else if (arg === '--codeagent' && args[i + 1]) {
@@ -52,6 +58,30 @@ export async function create(args) {
52
58
  const flags = parseFlags(args);
53
59
  const interactive = isInteractive(flags);
54
60
 
61
+ // preflight: in non-interactive mode, list all unspecified options and exit
62
+ if (!interactive) {
63
+ const missing = [];
64
+ if (!flags.name)
65
+ missing.push(' <project-name> Project name (positional argument)');
66
+ if (!flags.pm) missing.push(' --pm <npm (default)|pnpm|bun|yarn> Package manager');
67
+ if (flags.fonts === null)
68
+ missing.push(' --fonts <path> | --no-fonts Internal fonts (PP Neue Montreal)');
69
+ if (!flags.agent) missing.push(' --codeagent <cursor|claude|none> AI agent configuration');
70
+
71
+ if (missing.length > 0) {
72
+ console.log(
73
+ '[CONFIGURATION REQUIRED] Do not assume defaults. Ask the user to choose each option:\n\n' +
74
+ missing.join('\n') +
75
+ '\n\nOptional:\n' +
76
+ ' --no-components Skip component installation\n\n' +
77
+ 'Once the user has answered, re-run with their choices:\n' +
78
+ ' ds create my-app --pm <choice> --fonts <path> --codeagent <choice>\n' +
79
+ ' ds create my-app --pm <choice> --no-fonts --codeagent <choice>\n'
80
+ );
81
+ process.exit(1);
82
+ }
83
+ }
84
+
55
85
  p.intro('Create a new project using Archetype AI Design System.');
56
86
 
57
87
  // specify project name
@@ -95,44 +125,87 @@ export async function create(args) {
95
125
  process.exit(1);
96
126
  }
97
127
 
98
- // specify framework
99
- let framework = flags.framework;
100
- if (!framework) {
101
- if (!interactive) {
102
- framework = DEFAULTS.framework;
103
- } else {
104
- framework = await p.select({
105
- message: 'Which framework?',
106
- options: [{ value: 'svelte', label: 'SvelteKit', hint: 'Svelte 5 + Tailwind v4, default' }]
107
- });
108
- if (p.isCancel(framework)) {
109
- p.cancel('Setup cancelled.');
110
- process.exit(0);
111
- }
128
+ // specify framework (only svelte supported, default silently)
129
+ let framework = flags.framework || 'svelte';
130
+ if (!flags.framework && interactive) {
131
+ framework = await p.select({
132
+ message: 'Which framework?',
133
+ options: [{ value: 'svelte', label: 'SvelteKit', hint: 'Svelte 5 + Tailwind v4, default' }]
134
+ });
135
+ if (p.isCancel(framework)) {
136
+ p.cancel('Setup cancelled.');
137
+ process.exit(0);
112
138
  }
113
139
  }
114
140
 
115
141
  // specify package manager
116
142
  let pmName = flags.pm;
117
143
  if (!pmName) {
118
- if (!interactive) {
119
- pmName = DEFAULTS.pm;
144
+ pmName = await p.select({
145
+ message: 'Which package manager?',
146
+ options: pmNames.map((n) => ({
147
+ value: n,
148
+ label: n,
149
+ ...(n === DEFAULTS.pm && { hint: 'default' })
150
+ }))
151
+ });
152
+ if (p.isCancel(pmName)) {
153
+ p.cancel('Setup cancelled.');
154
+ process.exit(0);
155
+ }
156
+ }
157
+ const pm = getPm(pmName);
158
+
159
+ // specify fonts
160
+ let fontsPath = null;
161
+ let includeFonts = false;
162
+ if (flags.fonts === false) {
163
+ // --no-fonts: skip
164
+ } else if (typeof flags.fonts === 'string') {
165
+ fontsPath = flags.fonts;
166
+ const result = validateFontsPath(fontsPath);
167
+ if (!result.valid) {
168
+ if (!interactive) {
169
+ p.log.error(result.error);
170
+ process.exit(1);
171
+ }
172
+ p.log.warn(result.error);
173
+ p.log.warn('Continuing without fonts.');
174
+ fontsPath = null;
120
175
  } else {
121
- pmName = await p.select({
122
- message: 'Which package manager?',
123
- options: pmNames.map((n) => ({
124
- value: n,
125
- label: n,
126
- ...(n === DEFAULTS.pm && { hint: 'default' })
127
- }))
176
+ includeFonts = true;
177
+ }
178
+ } else {
179
+ const useFonts = await p.confirm({
180
+ message: 'Do you want to use internal fonts?',
181
+ initialValue: false
182
+ });
183
+ if (p.isCancel(useFonts)) {
184
+ p.cancel('Setup cancelled.');
185
+ process.exit(0);
186
+ }
187
+ if (useFonts) {
188
+ const fontInput = await p.text({
189
+ message: 'Drop your fonts folder here:',
190
+ placeholder: '/path/to/fonts'
128
191
  });
129
- if (p.isCancel(pmName)) {
192
+ if (p.isCancel(fontInput)) {
130
193
  p.cancel('Setup cancelled.');
131
194
  process.exit(0);
132
195
  }
196
+ if (fontInput && fontInput.trim()) {
197
+ fontsPath = fontInput.trim();
198
+ const result = validateFontsPath(fontsPath);
199
+ if (!result.valid) {
200
+ p.log.warn(result.error);
201
+ p.log.warn('Continuing without fonts.');
202
+ fontsPath = null;
203
+ } else {
204
+ includeFonts = true;
205
+ }
206
+ }
133
207
  }
134
208
  }
135
- const pm = getPm(pmName);
136
209
 
137
210
  // verify package manager is installed
138
211
  if (!isPmInstalled(pmName)) {
@@ -177,8 +250,22 @@ export async function create(args) {
177
250
  // init shadcn-svelte
178
251
  await initShadcn(pm, projectPath);
179
252
 
253
+ // install local fonts
254
+ if (includeFonts && fontsPath) {
255
+ const s = p.spinner();
256
+ s.start('Installing fonts');
257
+ const fontResult = installLocalFonts(fontsPath, projectPath);
258
+ if (fontResult.success) {
259
+ s.stop(`${fontResult.fileCount} font files installed`);
260
+ } else {
261
+ s.stop('Failed to install fonts');
262
+ p.log.warn(fontResult.error);
263
+ p.log.warn('Continuing without fonts.');
264
+ includeFonts = false;
265
+ }
266
+ }
267
+
180
268
  // configure CSS
181
- const includeFonts = false;
182
269
  configureCss(projectPath, includeFonts);
183
270
 
184
271
  // set up linting and formatting
@@ -202,33 +289,20 @@ export async function create(args) {
202
289
  // specify agent config
203
290
  let agent = flags.agent;
204
291
  if (!agent) {
205
- if (!interactive) {
206
- agent = DEFAULTS.agent;
207
- } else {
208
- agent = await p.select({
209
- message: 'Install AI agent configuration?',
210
- options: [
211
- { value: 'none', label: 'None' },
212
- { value: 'cursor', label: 'Cursor', hint: 'AGENTS.md + skills + rules' },
213
- { value: 'claude', label: 'Claude Code', hint: 'CLAUDE.md + skills + rules, default' }
214
- ]
215
- });
216
- if (p.isCancel(agent)) {
217
- p.cancel('Setup cancelled.');
218
- process.exit(0);
219
- }
292
+ agent = await p.select({
293
+ message: 'Install AI agent configuration?',
294
+ options: [
295
+ { value: 'none', label: 'None' },
296
+ { value: 'cursor', label: 'Cursor', hint: 'AGENTS.md + skills + rules' },
297
+ { value: 'claude', label: 'Claude Code', hint: 'CLAUDE.md + skills + rules, default' }
298
+ ]
299
+ });
300
+ if (p.isCancel(agent)) {
301
+ p.cancel('Setup cancelled.');
302
+ process.exit(0);
220
303
  }
221
304
  }
222
305
 
223
- // log defaults notice
224
- if (!interactive) {
225
- const applied = {};
226
- if (!flags.framework) applied.framework = framework;
227
- if (!flags.pm) applied.pm = pmName;
228
- if (!flags.agent) applied.codeagent = agent;
229
- logDefaultsNotice(applied);
230
- }
231
-
232
306
  const validAgents = ['cursor', 'claude', 'none'];
233
307
  if (!validAgents.includes(agent)) {
234
308
  p.log.error(`Invalid --codeagent value "${agent}". Must be one of: ${validAgents.join(', ')}`);
package/commands/init.js CHANGED
@@ -13,12 +13,14 @@ import {
13
13
  installComponents,
14
14
  installAgentConfig
15
15
  } from '../lib/scaffold-ds-svelte-project.js';
16
- import { isInteractive, DEFAULTS, logDefaultsNotice } from '../lib/is-interactive.js';
16
+ import { validateFontsPath, installLocalFonts } from '../lib/install-fonts-local.js';
17
+ import { isInteractive, DEFAULTS } from '../lib/is-interactive.js';
17
18
 
18
19
  // parse flags
19
20
  export function parseFlags(args) {
20
21
  const flags = {
21
22
  pm: null,
23
+ fonts: null,
22
24
  components: true,
23
25
  agent: null,
24
26
  defaults: false
@@ -28,6 +30,10 @@ export function parseFlags(args) {
28
30
  const arg = args[i];
29
31
  if (arg === '--pm' && args[i + 1]) {
30
32
  flags.pm = args[++i];
33
+ } else if (arg === '--fonts' && args[i + 1]) {
34
+ flags.fonts = args[++i];
35
+ } else if (arg === '--no-fonts') {
36
+ flags.fonts = false;
31
37
  } else if (arg === '--no-components') {
32
38
  flags.components = false;
33
39
  } else if (arg === '--codeagent' && args[i + 1]) {
@@ -57,6 +63,29 @@ export async function init(args) {
57
63
  const interactive = isInteractive(flags);
58
64
  const projectPath = process.cwd();
59
65
 
66
+ // preflight: in non-interactive mode, list all unspecified options and exit
67
+ if (!interactive) {
68
+ const missing = [];
69
+ if (!flags.pm && !detectPm(projectPath))
70
+ missing.push(' --pm <npm (default)|pnpm|bun|yarn> Package manager');
71
+ if (flags.fonts === null)
72
+ missing.push(' --fonts <path> | --no-fonts Internal fonts (PP Neue Montreal)');
73
+ if (!flags.agent) missing.push(' --codeagent <cursor|claude|none> AI agent configuration');
74
+
75
+ if (missing.length > 0) {
76
+ console.log(
77
+ '[CONFIGURATION REQUIRED] Do not assume defaults. Ask the user to choose each option:\n\n' +
78
+ missing.join('\n') +
79
+ '\n\nOptional:\n' +
80
+ ' --no-components Skip component installation\n\n' +
81
+ 'Once the user has answered, re-run with their choices:\n' +
82
+ ' ds init --pm <choice> --fonts <path> --codeagent <choice>\n' +
83
+ ' ds init --pm <choice> --no-fonts --codeagent <choice>\n'
84
+ );
85
+ process.exit(1);
86
+ }
87
+ }
88
+
60
89
  // validate --pm flag
61
90
  if (flags.pm && !pmNames.includes(flags.pm)) {
62
91
  p.log.error(`Invalid --pm value "${flags.pm}". Must be one of: ${pmNames.join(', ')}`);
@@ -91,32 +120,71 @@ export async function init(args) {
91
120
  }
92
121
  }
93
122
  if (!pmName) {
94
- if (!interactive) {
95
- pmName = DEFAULTS.pm;
123
+ pmName = await p.select({
124
+ message: 'Which package manager?',
125
+ options: pmNames.map((n) => ({
126
+ value: n,
127
+ label: n,
128
+ ...(n === DEFAULTS.pm && { hint: 'default' })
129
+ }))
130
+ });
131
+ if (p.isCancel(pmName)) {
132
+ p.cancel('Setup cancelled.');
133
+ process.exit(0);
134
+ }
135
+ }
136
+ const pm = getPm(pmName);
137
+
138
+ // specify fonts
139
+ let fontsPath = null;
140
+ let includeFonts = false;
141
+ if (flags.fonts === false) {
142
+ // --no-fonts: skip
143
+ } else if (typeof flags.fonts === 'string') {
144
+ fontsPath = flags.fonts;
145
+ const result = validateFontsPath(fontsPath);
146
+ if (!result.valid) {
147
+ if (!interactive) {
148
+ p.log.error(result.error);
149
+ process.exit(1);
150
+ }
151
+ p.log.warn(result.error);
152
+ p.log.warn('Continuing without fonts.');
153
+ fontsPath = null;
96
154
  } else {
97
- pmName = await p.select({
98
- message: 'Which package manager?',
99
- options: pmNames.map((n) => ({
100
- value: n,
101
- label: n,
102
- ...(n === DEFAULTS.pm && { hint: 'default' })
103
- }))
155
+ includeFonts = true;
156
+ }
157
+ } else {
158
+ const useFonts = await p.confirm({
159
+ message: 'Do you want to use internal fonts?',
160
+ initialValue: false
161
+ });
162
+ if (p.isCancel(useFonts)) {
163
+ p.cancel('Setup cancelled.');
164
+ process.exit(0);
165
+ }
166
+ if (useFonts) {
167
+ const fontInput = await p.text({
168
+ message: 'Drop your fonts folder here:',
169
+ placeholder: '/path/to/fonts'
104
170
  });
105
- if (p.isCancel(pmName)) {
171
+ if (p.isCancel(fontInput)) {
106
172
  p.cancel('Setup cancelled.');
107
173
  process.exit(0);
108
174
  }
175
+ if (fontInput && fontInput.trim()) {
176
+ fontsPath = fontInput.trim();
177
+ const result = validateFontsPath(fontsPath);
178
+ if (!result.valid) {
179
+ p.log.warn(result.error);
180
+ p.log.warn('Continuing without fonts.');
181
+ fontsPath = null;
182
+ } else {
183
+ includeFonts = true;
184
+ }
185
+ }
109
186
  }
110
187
  }
111
- const pm = getPm(pmName);
112
-
113
- // log defaults notice
114
- if (!interactive) {
115
- const applied = {};
116
- if (!flags.pm) applied.pm = pmName;
117
- if (!flags.agent) applied.codeagent = DEFAULTS.agent;
118
- logDefaultsNotice(applied);
119
- }
120
188
 
121
189
  // verify package manager is installed
122
190
  if (!isPmInstalled(pmName)) {
@@ -142,8 +210,22 @@ export async function init(args) {
142
210
  // init shadcn-svelte
143
211
  await initShadcn(pm, projectPath);
144
212
 
213
+ // install local fonts
214
+ if (includeFonts && fontsPath) {
215
+ const s = p.spinner();
216
+ s.start('Installing fonts');
217
+ const fontResult = installLocalFonts(fontsPath, projectPath);
218
+ if (fontResult.success) {
219
+ s.stop(`${fontResult.fileCount} font files installed`);
220
+ } else {
221
+ s.stop('Failed to install fonts');
222
+ p.log.warn(fontResult.error);
223
+ p.log.warn('Continuing without fonts.');
224
+ includeFonts = false;
225
+ }
226
+ }
227
+
145
228
  // configure CSS (prepend to existing)
146
- const includeFonts = false;
147
229
  prependCss(projectPath, includeFonts);
148
230
 
149
231
  // set up linting and formatting
@@ -167,21 +249,17 @@ export async function init(args) {
167
249
  // specify agent config
168
250
  let agent = flags.agent;
169
251
  if (!agent) {
170
- if (!interactive) {
171
- agent = DEFAULTS.agent;
172
- } else {
173
- agent = await p.select({
174
- message: 'Install AI agent configuration?',
175
- options: [
176
- { value: 'none', label: 'None' },
177
- { value: 'cursor', label: 'Cursor', hint: 'AGENTS.md + skills + rules' },
178
- { value: 'claude', label: 'Claude Code', hint: 'CLAUDE.md + skills + rules, default' }
179
- ]
180
- });
181
- if (p.isCancel(agent)) {
182
- p.cancel('Setup cancelled.');
183
- process.exit(0);
184
- }
252
+ agent = await p.select({
253
+ message: 'Install AI agent configuration?',
254
+ options: [
255
+ { value: 'none', label: 'None' },
256
+ { value: 'cursor', label: 'Cursor', hint: 'AGENTS.md + skills + rules' },
257
+ { value: 'claude', label: 'Claude Code', hint: 'CLAUDE.md + skills + rules, default' }
258
+ ]
259
+ });
260
+ if (p.isCancel(agent)) {
261
+ p.cancel('Setup cancelled.');
262
+ process.exit(0);
185
263
  }
186
264
  }
187
265
 
package/files/AGENTS.md CHANGED
@@ -44,8 +44,24 @@ Standard Tailwind is fine for:
44
44
  - **Variants**: `tailwind-variants` (tv) for component variants
45
45
  - **Slots**: `{@render children?.()}`
46
46
 
47
+ ## Pattern Registry
48
+
49
+ Before building a new component, fetch the pattern catalog from:
50
+ `https://design-system.archetypeai.workers.dev/r/patterns.json`
51
+
52
+ Each entry includes a `name`, `title`, `description`, and `registryDependencies`. Reuse or extend existing patterns instead of rebuilding from scratch.
53
+
54
+ Before installing, check if the pattern already exists locally in `$lib/components/ui/patterns/{name}/`. Only install if it is missing:
55
+ `npx shadcn-svelte@latest add https://design-system.archetypeai.workers.dev/r/{name}.json`
56
+
47
57
  ## Skills
48
58
 
59
+ **Skill composition:** When building a demo that uses a Newton or Embedding API skill:
60
+ 1. Use `@skills/create-dashboard` for the page layout (dashboard with Menubar)
61
+ 2. Fetch `https://design-system.archetypeai.workers.dev/r/patterns.json` and reuse existing patterns before creating new components
62
+ 3. Apply `@rules/design-principles` aesthetic conventions (BackgroundCard for single-purpose cards, mono font for headers/numbers)
63
+ 4. Only include chart components if the user's request involves time-series or explicitly mentions charts
64
+
49
65
  Read these when relevant to your task:
50
66
 
51
67
  - `@skills/apply-ds` - apply DS tokens, components, and patterns to an existing demo
package/files/CLAUDE.md CHANGED
@@ -44,9 +44,23 @@ Standard Tailwind is fine for:
44
44
  - **Variants**: `tailwind-variants` (tv) for component variants
45
45
  - **Slots**: `{@render children?.()}`
46
46
 
47
+ ## Pattern Registry
48
+
49
+ Before building a new component, fetch the pattern catalog from:
50
+ `https://design-system.archetypeai.workers.dev/r/patterns.json`
51
+
52
+ Each entry includes a `name`, `title`, `description`, and `registryDependencies`. Reuse or extend existing patterns instead of rebuilding from scratch.
53
+
54
+ Before installing, check if the pattern already exists locally in `$lib/components/ui/patterns/{name}/`. Only install if it is missing:
55
+ `npx shadcn-svelte@latest add https://design-system.archetypeai.workers.dev/r/{name}.json`
56
+
47
57
  ## Skills
48
58
 
49
- **Skill composition:** When building a demo that uses a Newton or Embedding API skill, also consult `@skills/create-dashboard` for dashboard layouts and `@skills/build-pattern` for extracting components. Only include chart components (SensorChart, ScatterChart) if the user's request involves time-series or explicitly mentions charts.
59
+ **Skill composition:** When building a demo that uses a Newton or Embedding API skill:
60
+ 1. Use `@skills/create-dashboard` for the page layout (dashboard with Menubar)
61
+ 2. Fetch `https://design-system.archetypeai.workers.dev/r/patterns.json` and reuse existing patterns before creating new components
62
+ 3. Apply `@rules/design-principles` aesthetic conventions (BackgroundCard for single-purpose cards, mono font for headers/numbers)
63
+ 4. Only include chart components if the user's request involves time-series or explicitly mentions charts
50
64
 
51
65
  Read these when relevant to your task:
52
66
 
@@ -253,100 +253,4 @@ Manual legend below chart:
253
253
  {/if}
254
254
  ```
255
255
 
256
- ## Complete SensorChart Example
257
-
258
- ```svelte
259
- <script>
260
- import { cn } from '$lib/utils.js';
261
- import { Card, CardHeader, CardTitle, CardContent } from '$lib/components/ui/primitives/card/index.js';
262
- import * as Chart from '$lib/components/ui/primitives/chart/index.js';
263
- import { LineChart } from 'layerchart';
264
- import { curveNatural } from 'd3-shape';
265
- import { scaleUtc, scaleLinear } from 'd3-scale';
266
-
267
- let {
268
- title = 'SENSOR',
269
- icon: Icon = undefined,
270
- data = [],
271
- signals = {},
272
- xKey = 'timestamp',
273
- maxPoints = undefined,
274
- yMin,
275
- yMax,
276
- yTicks,
277
- class: className,
278
- ...restProps
279
- } = $props();
280
-
281
- const chartColors = [
282
- 'var(--chart-1)',
283
- 'var(--chart-2)',
284
- 'var(--chart-3)',
285
- 'var(--chart-4)',
286
- 'var(--chart-5)'
287
- ];
288
-
289
- let displayData = $derived(maxPoints && data.length > maxPoints ? data.slice(-maxPoints) : data);
290
-
291
- let indexedData = $derived(displayData.map((d, i) => ({ ...d, _index: i })));
292
-
293
- let series = $derived(
294
- Object.entries(signals).map(([key, label], i) => ({
295
- key,
296
- label,
297
- color: chartColors[i % chartColors.length]
298
- }))
299
- );
300
-
301
- let chartConfig = $derived(
302
- Object.fromEntries(series.map((s) => [s.key, { label: s.label, color: s.color }]))
303
- );
304
-
305
- let useIndexX = $derived(maxPoints !== undefined);
306
- let xDomain = $derived(useIndexX ? [0, (maxPoints || displayData.length) - 1] : undefined);
307
- </script>
308
-
309
- <Card class={cn('p-4', className)} {...restProps}>
310
- <CardHeader class="flex flex-row items-center justify-between p-0">
311
- <CardTitle class="text-foreground font-mono text-base uppercase">
312
- {title}
313
- </CardTitle>
314
- {#if Icon}
315
- <Icon strokeWidth={1.25} class="text-muted-foreground size-6" />
316
- {/if}
317
- </CardHeader>
318
-
319
- <CardContent class="flex flex-col gap-6 p-0">
320
- <Chart.Container config={chartConfig} class="aspect-auto h-[220px] w-full">
321
- <LineChart
322
- data={indexedData}
323
- x={useIndexX ? '_index' : xKey}
324
- xScale={useIndexX ? scaleLinear() : scaleUtc()}
325
- {xDomain}
326
- yScale={scaleLinear()}
327
- yDomain={[yMin, yMax]}
328
- {series}
329
- tooltip={false}
330
- props={{
331
- spline: { curve: curveNatural, strokeWidth: 1.5 },
332
- yAxis: { ticks: yTicks }
333
- }}
334
- />
335
- </Chart.Container>
336
-
337
- {#if series.length > 0}
338
- <div class="flex items-center justify-center gap-10">
339
- {#each series as s (s.key)}
340
- <div class="flex items-center gap-2">
341
- <div
342
- class="size-2 rounded-full bg-(--legend-color)"
343
- style:--legend-color={s.color}
344
- ></div>
345
- <span class="text-foreground text-sm">{s.label}</span>
346
- </div>
347
- {/each}
348
- </div>
349
- {/if}
350
- </CardContent>
351
- </Card>
352
- ```
256
+ For a complete chart pattern example, see the `sensor-chart` pattern source in `$lib/components/ui/patterns/sensor-chart/`.