@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.
- package/LICENSE +21 -0
- package/README.md +123 -0
- package/bin.js +77 -0
- package/commands/add.js +42 -0
- package/commands/create.js +238 -0
- package/commands/init.js +199 -0
- package/files/AGENTS.md +63 -0
- package/files/CLAUDE.md +63 -0
- package/files/LICENSE +21 -0
- package/files/rules/accessibility.md +219 -0
- package/files/rules/charts.md +352 -0
- package/files/rules/components.md +267 -0
- package/files/rules/design-principles.md +56 -0
- package/files/rules/linting.md +31 -0
- package/files/rules/state.md +405 -0
- package/files/rules/styling.md +245 -0
- package/files/skills/apply-ds/SKILL.md +117 -0
- package/files/skills/apply-ds/scripts/setup.sh +271 -0
- package/files/skills/build-pattern/SKILL.md +202 -0
- package/files/skills/create-dashboard/SKILL.md +189 -0
- package/files/skills/deploy-worker/SKILL.md +231 -0
- package/files/skills/deploy-worker/references/wrangler-commands.md +327 -0
- package/files/skills/fix-accessibility/SKILL.md +184 -0
- package/files/skills/fix-metadata/SKILL.md +118 -0
- package/files/skills/fix-metadata/assets/favicon.ico +0 -0
- package/files/skills/setup-chart/SKILL.md +225 -0
- package/files/skills/setup-chart/data/embedding.csv +42 -0
- package/files/skills/setup-chart/data/timeseries.csv +173 -0
- package/files/skills/setup-chart/references/scatter-chart.md +229 -0
- package/files/skills/setup-chart/references/sensor-chart.md +156 -0
- package/lib/add-ds-config-codeagent.js +154 -0
- package/lib/add-ds-ui-svelte.js +93 -0
- package/lib/scaffold-ds-svelte-project.js +272 -0
- package/lib/use-package-manager.js +65 -0
- package/lib/use-shadcn-svelte-registry.js +26 -0
- package/lib/validate-url.js +31 -0
- package/package.json +34 -0
package/files/AGENTS.md
ADDED
|
@@ -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/CLAUDE.md
ADDED
|
@@ -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
|
+
```
|