@archetypeai/ds-cli 0.3.7

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 (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +123 -0
  3. package/bin.js +77 -0
  4. package/commands/add.js +42 -0
  5. package/commands/create.js +238 -0
  6. package/commands/init.js +199 -0
  7. package/files/AGENTS.md +63 -0
  8. package/files/CLAUDE.md +63 -0
  9. package/files/LICENSE +21 -0
  10. package/files/rules/accessibility.md +219 -0
  11. package/files/rules/charts.md +352 -0
  12. package/files/rules/components.md +267 -0
  13. package/files/rules/design-principles.md +56 -0
  14. package/files/rules/linting.md +31 -0
  15. package/files/rules/state.md +405 -0
  16. package/files/rules/styling.md +245 -0
  17. package/files/skills/apply-ds/SKILL.md +117 -0
  18. package/files/skills/apply-ds/scripts/setup.sh +271 -0
  19. package/files/skills/build-pattern/SKILL.md +202 -0
  20. package/files/skills/create-dashboard/SKILL.md +189 -0
  21. package/files/skills/deploy-worker/SKILL.md +231 -0
  22. package/files/skills/deploy-worker/references/wrangler-commands.md +327 -0
  23. package/files/skills/fix-accessibility/SKILL.md +184 -0
  24. package/files/skills/fix-metadata/SKILL.md +118 -0
  25. package/files/skills/fix-metadata/assets/favicon.ico +0 -0
  26. package/files/skills/setup-chart/SKILL.md +225 -0
  27. package/files/skills/setup-chart/data/embedding.csv +42 -0
  28. package/files/skills/setup-chart/data/timeseries.csv +173 -0
  29. package/files/skills/setup-chart/references/scatter-chart.md +229 -0
  30. package/files/skills/setup-chart/references/sensor-chart.md +156 -0
  31. package/lib/add-ds-config-codeagent.js +154 -0
  32. package/lib/add-ds-ui-svelte.js +93 -0
  33. package/lib/scaffold-ds-svelte-project.js +272 -0
  34. package/lib/use-package-manager.js +65 -0
  35. package/lib/use-shadcn-svelte-registry.js +26 -0
  36. package/lib/validate-url.js +31 -0
  37. package/package.json +34 -0
@@ -0,0 +1,63 @@
1
+ # Archetype AI Design System
2
+
3
+ ## Stack
4
+
5
+ - Svelte 5 with runes (`$props`, `$state`, `$derived`, `$bindable`)
6
+ - Tailwind v4 with semantic tokens (`@archetypeai/ds-lib-tokens`)
7
+ - shadcn-svelte registry pattern (via `@archetypeai/ds-cli`)
8
+ - bits-ui for headless primitives
9
+ - layerchart for data visualization
10
+
11
+ ## Commands
12
+
13
+ - `npx @archetypeai/ds-cli add ds-ui-svelte` - install all components
14
+ - `npx shadcn-svelte@latest add <url>` - install individual component
15
+
16
+ ## Tokens
17
+
18
+ Prefer semantic tokens for themed colors:
19
+
20
+ - `bg-background`, `text-foreground`, `border-border`
21
+ - `bg-primary`, `text-primary-foreground`
22
+ - `bg-muted`, `text-muted-foreground`
23
+ - `bg-card`, `bg-popover`, `bg-accent`, `bg-destructive`
24
+ - `bg-atai-neutral`, `text-atai-good`, `text-atai-warning`, `text-atai-critical`
25
+ - etc.
26
+
27
+ Standard Tailwind is fine for:
28
+
29
+ - Spacing/sizing: `p-4`, `w-full`, `gap-2`, `h-screen`
30
+ - Layout: `flex`, `grid`, `absolute`, `relative`
31
+ - One-off colors: gradients, illustrations, custom accents
32
+
33
+ ## CSS Import Order
34
+
35
+ ```css
36
+ @import '@archetypeai/ds-lib-fonts-internal';
37
+ @import '@archetypeai/ds-lib-tokens/theme.css';
38
+ @import 'tailwindcss';
39
+ ```
40
+
41
+ ## Component Patterns
42
+
43
+ - **Props**: `let { class: className, ref = $bindable(null), children, ...restProps } = $props();`
44
+ - **Classes**: `cn()` from `$lib/utils.js` - never raw concatenation
45
+ - **Variants**: `tailwind-variants` (tv) for component variants
46
+ - **Slots**: `{@render children?.()}`
47
+
48
+ ## Skills
49
+
50
+ Read these when relevant to your task:
51
+
52
+ - `@skills/apply-ds` - setup tokens in new project
53
+ - `@skills/build-pattern` - create composite patterns from primitives
54
+ - `@skills/setup-chart` - set up charts with layerchart
55
+ - `@skills/create-dashboard` - scaffold a full-viewport dashboard with menubar and panels
56
+ - `@skills/fix-accessibility` - audit and fix a11y issues
57
+ - `@skills/fix-metadata` - update page titles, favicons, and OG tags
58
+ - `@skills/deploy-worker` - deploy SvelteKit projects to Cloudflare Workers
59
+ - `@skills/explain-code` - explain code with structure and traced execution
60
+
61
+ ## Rules
62
+
63
+ See `@rules/` for comprehensive guidance on design principles, components, styling, charts, and linting.
@@ -0,0 +1,63 @@
1
+ # Archetype AI Design System
2
+
3
+ ## Stack
4
+
5
+ - Svelte 5 with runes (`$props`, `$state`, `$derived`, `$bindable`)
6
+ - Tailwind v4 with semantic tokens (`@archetypeai/ds-lib-tokens`)
7
+ - shadcn-svelte registry pattern (via `@archetypeai/ds-cli`)
8
+ - bits-ui for headless primitives
9
+ - layerchart for data visualization
10
+
11
+ ## Commands
12
+
13
+ - `npx @archetypeai/ds-cli add ds-ui-svelte` - install all components
14
+ - `npx shadcn-svelte@latest add <url>` - install individual component
15
+
16
+ ## Tokens
17
+
18
+ Prefer semantic tokens for themed colors:
19
+
20
+ - `bg-background`, `text-foreground`, `border-border`
21
+ - `bg-primary`, `text-primary-foreground`
22
+ - `bg-muted`, `text-muted-foreground`
23
+ - `bg-card`, `bg-popover`, `bg-accent`, `bg-destructive`
24
+ - `bg-atai-neutral`, `text-atai-good`, `text-atai-warning`, `text-atai-critical`
25
+ - etc.
26
+
27
+ Standard Tailwind is fine for:
28
+
29
+ - Spacing/sizing: `p-4`, `w-full`, `gap-2`, `h-screen`
30
+ - Layout: `flex`, `grid`, `absolute`, `relative`
31
+ - One-off colors: gradients, illustrations, custom accents
32
+
33
+ ## CSS Import Order
34
+
35
+ ```css
36
+ @import '@archetypeai/ds-lib-fonts-internal';
37
+ @import '@archetypeai/ds-lib-tokens/theme.css';
38
+ @import 'tailwindcss';
39
+ ```
40
+
41
+ ## Component Patterns
42
+
43
+ - **Props**: `let { class: className, ref = $bindable(null), children, ...restProps } = $props();`
44
+ - **Classes**: `cn()` from `$lib/utils.js` - never raw concatenation
45
+ - **Variants**: `tailwind-variants` (tv) for component variants
46
+ - **Slots**: `{@render children?.()}`
47
+
48
+ ## Skills
49
+
50
+ Read these when relevant to your task:
51
+
52
+ - `@skills/apply-ds` - setup tokens in new project
53
+ - `@skills/build-pattern` - create composite patterns from primitives
54
+ - `@skills/setup-chart` - set up charts with layerchart
55
+ - `@skills/create-dashboard` - scaffold a full-viewport dashboard with menubar and panels
56
+ - `@skills/fix-accessibility` - audit and fix a11y issues
57
+ - `@skills/fix-metadata` - update page titles, favicons, and OG tags
58
+ - `@skills/deploy-worker` - deploy SvelteKit projects to Cloudflare Workers
59
+ - `@skills/explain-code` - explain code with structure and traced execution
60
+
61
+ ## Rules
62
+
63
+ See `@rules/` for comprehensive guidance on design principles, components, styling, charts, and linting.
package/files/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Archetype AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,219 @@
1
+ # Accessibility Rules
2
+
3
+ ## Screen Reader Support
4
+
5
+ Use `sr-only` for text that should be accessible but visually hidden:
6
+
7
+ ```svelte
8
+ <button>
9
+ <XIcon />
10
+ <span class="sr-only">Close</span>
11
+ </button>
12
+ ```
13
+
14
+ The `sr-only` class hides content visually while keeping it accessible to screen readers.
15
+
16
+ ## aria-label
17
+
18
+ ### Icon-Only Buttons
19
+
20
+ Always provide `aria-label` for buttons that only contain an icon:
21
+
22
+ ```svelte
23
+ <!-- Correct -->
24
+ <Button size="icon" aria-label="Search">
25
+ <SearchIcon />
26
+ </Button>
27
+
28
+ <!-- Wrong - no accessible name -->
29
+ <Button size="icon">
30
+ <SearchIcon />
31
+ </Button>
32
+ ```
33
+
34
+ ### Interactive Groups
35
+
36
+ Provide `aria-label` for grouped interactive elements:
37
+
38
+ ```svelte
39
+ <ButtonGroup.Root aria-label="Pagination">
40
+ <Button variant="outline" size="icon-sm" aria-label="Previous page">
41
+ <ChevronLeftIcon />
42
+ </Button>
43
+ <ButtonGroup.Text>Page 1 of 10</ButtonGroup.Text>
44
+ <Button variant="outline" size="icon-sm" aria-label="Next page">
45
+ <ChevronRightIcon />
46
+ </Button>
47
+ </ButtonGroup.Root>
48
+ ```
49
+
50
+ ### Toggles
51
+
52
+ ```svelte
53
+ <Toggle variant="outline" aria-label="Toggle bookmark">
54
+ <BookmarkIcon />
55
+ </Toggle>
56
+ ```
57
+
58
+ ## aria-hidden
59
+
60
+ Use `aria-hidden="true"` for decorative elements that shouldn't be announced:
61
+
62
+ ```svelte
63
+ <!-- Decorative icons next to text -->
64
+ <CardTitle>Sensor Status</CardTitle>
65
+ <Icon strokeWidth={1.25} class="text-muted-foreground size-6" aria-hidden="true" />
66
+
67
+ <!-- Icons in buttons WITH text labels -->
68
+ <Button>
69
+ <PlusIcon aria-hidden="true" />
70
+ Add Item
71
+ </Button>
72
+ ```
73
+
74
+ Do NOT use `aria-hidden` on icon-only buttons - use `aria-label` instead.
75
+
76
+ ## role Attributes
77
+
78
+ ### Lists
79
+
80
+ ```svelte
81
+ <div role="list" data-slot="item-group">
82
+ {#each items as item}
83
+ <div role="listitem">{item}</div>
84
+ {/each}
85
+ </div>
86
+ ```
87
+
88
+ ### Groups
89
+
90
+ ```svelte
91
+ <!-- Input groups -->
92
+ <div role="group" data-slot="input-group">
93
+ <Input />
94
+ <Button>Submit</Button>
95
+ </div>
96
+
97
+ <!-- Button groups -->
98
+ <div role="group" aria-label="Text formatting" data-slot="button-group">
99
+ <Button>Bold</Button>
100
+ <Button>Italic</Button>
101
+ </div>
102
+ ```
103
+
104
+ ### Status
105
+
106
+ ```svelte
107
+ <!-- Spinners/loading indicators -->
108
+ <svg role="status" aria-label="Loading" class="animate-spin"> ... </svg>
109
+ ```
110
+
111
+ ## aria-invalid Styling
112
+
113
+ Components use `aria-invalid` for validation states. The styling pattern:
114
+
115
+ ```svelte
116
+ <input
117
+ class={cn(
118
+ 'border-input focus-visible:ring-ring/50',
119
+ 'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40',
120
+ 'aria-invalid:border-destructive'
121
+ )}
122
+ aria-invalid={hasError}
123
+ />
124
+ ```
125
+
126
+ This applies to: inputs, textareas, selects, checkboxes, buttons, badges.
127
+
128
+ To trigger invalid styling, set `aria-invalid="true"`:
129
+
130
+ ```svelte
131
+ <Input aria-invalid={errors.email ? true : undefined} />
132
+ ```
133
+
134
+ ## Focus Management
135
+
136
+ ### Focus Ring Pattern
137
+
138
+ All interactive elements use this focus pattern:
139
+
140
+ ```svelte
141
+ <button
142
+ class={cn(
143
+ 'outline-none',
144
+ 'focus-visible:border-ring',
145
+ 'focus-visible:ring-ring/50',
146
+ 'focus-visible:ring-[3px]'
147
+ )}
148
+ >
149
+ ```
150
+
151
+ Key points:
152
+
153
+ - `outline-none` removes default browser outline
154
+ - `focus-visible` only shows on keyboard focus (not mouse clicks)
155
+ - `ring-[3px]` provides visible focus indicator
156
+ - `ring-ring/50` uses semantic ring color at 50% opacity
157
+
158
+ ### Disabled States
159
+
160
+ ```svelte
161
+ <button
162
+ class="disabled:pointer-events-none disabled:opacity-50"
163
+ disabled={isDisabled}
164
+ >
165
+ ```
166
+
167
+ For links styled as buttons (can't use `disabled` attribute):
168
+
169
+ ```svelte
170
+ <a
171
+ href={disabled ? undefined : href}
172
+ aria-disabled={disabled}
173
+ role={disabled ? 'button' : undefined}
174
+ tabindex={disabled ? -1 : undefined}
175
+ class="aria-disabled:pointer-events-none aria-disabled:opacity-50"
176
+ >
177
+ ```
178
+
179
+ ## Keyboard Navigation
180
+
181
+ ### bits-ui Primitives
182
+
183
+ Most keyboard navigation is handled automatically by bits-ui:
184
+
185
+ - Dialog: Escape to close, focus trap
186
+ - Select: Arrow keys to navigate, Enter to select
187
+ - Command: Arrow keys, Enter, type to filter
188
+ - Popover: Escape to close
189
+
190
+ ### Custom Components
191
+
192
+ For custom interactive elements, ensure:
193
+
194
+ ```svelte
195
+ <div
196
+ role="button"
197
+ tabindex="0"
198
+ onkeydown={(e) => {
199
+ if (e.key === 'Enter' || e.key === ' ') {
200
+ e.preventDefault();
201
+ handleClick();
202
+ }
203
+ }}
204
+ onclick={handleClick}
205
+ >
206
+ ```
207
+
208
+ ## Checklist
209
+
210
+ When building components, verify:
211
+
212
+ - [ ] Icon-only buttons have `aria-label`
213
+ - [ ] Decorative icons have `aria-hidden="true"`
214
+ - [ ] Interactive groups have `aria-label`
215
+ - [ ] Form inputs support `aria-invalid` styling
216
+ - [ ] Focus states are visible (`focus-visible:ring-*`)
217
+ - [ ] Disabled states prevent interaction and reduce opacity
218
+ - [ ] Loading states use `role="status"`
219
+ - [ ] Lists use `role="list"` and `role="listitem"`
@@ -0,0 +1,352 @@
1
+ ---
2
+ paths:
3
+ - '**/chart*/**'
4
+ - '**/*Chart*.svelte'
5
+ - '**/*chart*.svelte'
6
+ - '**/dashboard*/**'
7
+ ---
8
+
9
+ # Chart Rules
10
+
11
+ ## Stack
12
+
13
+ - **layerchart** - Svelte-native charting library
14
+ - **d3-scale** - Scale functions (scaleUtc, scaleLinear, scaleOrdinal)
15
+ - **d3-shape** - Curve functions (curveNatural, curveMonotoneX)
16
+ - **Chart primitives** - Chart.Container, Chart.Tooltip from the design system
17
+
18
+ ## Chart.Container
19
+
20
+ Wrap all charts in Chart.Container with a config prop:
21
+
22
+ ```svelte
23
+ <script>
24
+ import * as Chart from '$lib/components/ui/chart/index.js';
25
+
26
+ const chartConfig = {
27
+ temperature: { label: 'Temperature', color: 'var(--chart-1)' },
28
+ humidity: { label: 'Humidity', color: 'var(--chart-2)' }
29
+ };
30
+ </script>
31
+
32
+ <Chart.Container config={chartConfig} class="aspect-auto h-[220px] w-full">
33
+ <LineChart ... />
34
+ </Chart.Container>
35
+ ```
36
+
37
+ The config maps data keys to labels and colors for tooltips and legends.
38
+
39
+ ## Chart Token Colors
40
+
41
+ Use semantic chart colors from the theme:
42
+
43
+ ```javascript
44
+ const chartColors = [
45
+ 'var(--chart-1)', // purple
46
+ 'var(--chart-2)', // red-orange
47
+ 'var(--chart-3)', // green
48
+ 'var(--chart-4)', // yellow
49
+ 'var(--chart-5)' // coral
50
+ ];
51
+ ```
52
+
53
+ In series config:
54
+
55
+ ```javascript
56
+ let series = $derived(
57
+ Object.entries(signals).map(([key, label], i) => ({
58
+ key,
59
+ label,
60
+ color: chartColors[i % chartColors.length]
61
+ }))
62
+ );
63
+ ```
64
+
65
+ ## LineChart Pattern
66
+
67
+ Basic line chart with layerchart:
68
+
69
+ ```svelte
70
+ <script>
71
+ import { LineChart } from 'layerchart';
72
+ import { scaleUtc, scaleLinear } from 'd3-scale';
73
+ import { curveNatural } from 'd3-shape';
74
+
75
+ let { data, xKey = 'timestamp', yMin, yMax } = $props();
76
+
77
+ const series = [{ key: 'value', label: 'Value', color: 'var(--chart-1)' }];
78
+ </script>
79
+
80
+ <LineChart
81
+ {data}
82
+ x={xKey}
83
+ xScale={scaleUtc()}
84
+ yScale={scaleLinear()}
85
+ yDomain={[yMin, yMax]}
86
+ {series}
87
+ props={{
88
+ spline: {
89
+ curve: curveNatural,
90
+ strokeWidth: 1.5
91
+ }
92
+ }}
93
+ />
94
+ ```
95
+
96
+ ## D3 Scale Usage
97
+
98
+ ### Time Scale (x-axis with dates)
99
+
100
+ ```javascript
101
+ import { scaleUtc } from 'd3-scale';
102
+
103
+ xScale={scaleUtc()}
104
+ ```
105
+
106
+ ### Linear Scale (numeric values)
107
+
108
+ ```javascript
109
+ import { scaleLinear } from 'd3-scale';
110
+
111
+ yScale={scaleLinear()}
112
+ yDomain={[0, 100]} // fixed domain
113
+ ```
114
+
115
+ ### Dynamic Domain
116
+
117
+ ```javascript
118
+ // Let layerchart calculate domain from data
119
+ yDomain = { undefined };
120
+
121
+ // Or calculate manually
122
+ let yDomain = $derived([
123
+ Math.min(...data.map((d) => d.value)),
124
+ Math.max(...data.map((d) => d.value))
125
+ ]);
126
+ ```
127
+
128
+ ## Streaming Data Pattern
129
+
130
+ For real-time charts with a sliding window:
131
+
132
+ ```svelte
133
+ <script>
134
+ let {
135
+ data = [],
136
+ maxPoints = 100, // sliding window size
137
+ ...
138
+ } = $props();
139
+
140
+ // Slice to maxPoints for display
141
+ let displayData = $derived(
142
+ maxPoints && data.length > maxPoints
143
+ ? data.slice(-maxPoints)
144
+ : data
145
+ );
146
+
147
+ // Add index for stable x positioning
148
+ let indexedData = $derived(
149
+ displayData.map((d, i) => ({ ...d, _index: i }))
150
+ );
151
+
152
+ // Use index-based x scale for streaming
153
+ let useIndexX = $derived(maxPoints !== undefined);
154
+ let xDomain = $derived(
155
+ useIndexX ? [0, (maxPoints || displayData.length) - 1] : undefined
156
+ );
157
+ </script>
158
+
159
+ <LineChart
160
+ data={indexedData}
161
+ x={useIndexX ? '_index' : xKey}
162
+ xScale={useIndexX ? scaleLinear() : scaleUtc()}
163
+ {xDomain}
164
+ ...
165
+ />
166
+ ```
167
+
168
+ ## Axis Configuration
169
+
170
+ ```svelte
171
+ <LineChart
172
+ axis="both" // "both" | "x" | "y" | "none"
173
+ props={{
174
+ xAxis: {
175
+ format: (date) => formatTime(date) // custom formatter
176
+ },
177
+ yAxis: {
178
+ ticks: [0, 25, 50, 75, 100], // explicit tick values
179
+ format: (v) => `${v}%`
180
+ },
181
+ grid: {
182
+ y: true,
183
+ x: false,
184
+ yTicks: [0, 25, 50, 75, 100]
185
+ }
186
+ }}
187
+ />
188
+ ```
189
+
190
+ ## Time Formatting
191
+
192
+ Relative time (MM:SS from start):
193
+
194
+ ```javascript
195
+ let baseTimestamp = $derived(
196
+ displayData.length > 0 && displayData[0][xKey] ? new Date(displayData[0][xKey]).getTime() : 0
197
+ );
198
+
199
+ function formatTime(date) {
200
+ if (!baseTimestamp) return '00:00';
201
+ const diffMs = date.getTime() - baseTimestamp;
202
+ const totalSeconds = Math.floor(diffMs / 1000);
203
+ const minutes = Math.floor(totalSeconds / 60);
204
+ const seconds = totalSeconds % 60;
205
+ return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
206
+ }
207
+ ```
208
+
209
+ ## Curve Types
210
+
211
+ ```javascript
212
+ import { curveNatural, curveMonotoneX, curveStep, curveLinear } from 'd3-shape';
213
+
214
+ props={{
215
+ spline: {
216
+ curve: curveNatural, // smooth natural curve
217
+ // curve: curveMonotoneX, // monotonic (no overshooting)
218
+ // curve: curveStep, // stepped line
219
+ // curve: curveLinear, // straight lines
220
+ strokeWidth: 1.5
221
+ }
222
+ }}
223
+ ```
224
+
225
+ ## Tooltip Integration
226
+
227
+ ```svelte
228
+ <LineChart
229
+ tooltip={false} // disable default tooltip
230
+ ...
231
+ />
232
+
233
+ <!-- Or use custom Chart.Tooltip -->
234
+ <Chart.Container config={chartConfig}>
235
+ <LineChart tooltip={{ ... }} />
236
+ </Chart.Container>
237
+ ```
238
+
239
+ ## Legend Pattern
240
+
241
+ Manual legend below chart:
242
+
243
+ ```svelte
244
+ {#if series.length > 0}
245
+ <div class="flex items-center justify-center gap-10">
246
+ {#each series as s (s.key)}
247
+ <div class="flex items-center gap-2">
248
+ <div class="size-2 rounded-full bg-(--legend-color)" style:--legend-color={s.color}></div>
249
+ <span class="text-foreground text-sm">{s.label}</span>
250
+ </div>
251
+ {/each}
252
+ </div>
253
+ {/if}
254
+ ```
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/card/index.js';
262
+ import * as Chart from '$lib/components/ui/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
+ ```