@miozu/jera 0.7.0 → 0.7.2
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/CLAUDE.md
CHANGED
|
@@ -1,858 +1,117 @@
|
|
|
1
|
-
# @miozu/jera -
|
|
1
|
+
# @miozu/jera - Component Library
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
**Purpose:** Zero-dependency, AI-first component library for Svelte 5
|
|
5
|
-
**Author:** Nicholas Glazer <glazer.nicholas@gmail.com>
|
|
6
|
-
**Last Updated:** February 2026
|
|
3
|
+
Zero-dependency, AI-first component library for Svelte 5.
|
|
7
4
|
|
|
8
|
-
|
|
5
|
+
## Architecture
|
|
9
6
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
jera follows a **5-layer architecture** designed for portability and AI-assisted development:
|
|
7
|
+
5-layer design for portability and AI-assisted development:
|
|
13
8
|
|
|
14
9
|
1. **W3C Design Tokens (DTCG)** - `tokens.json` as single source of truth
|
|
15
|
-
2. **CSS Custom Properties** - Generated from tokens
|
|
10
|
+
2. **CSS Custom Properties** - Generated from tokens
|
|
16
11
|
3. **Pure Modern CSS** - Framework-agnostic component styles
|
|
17
12
|
4. **Svelte 5 Wrappers** - Thin components using native runes
|
|
18
|
-
5. **AI Documentation** - llms.txt standard
|
|
19
|
-
|
|
20
|
-
**Full architecture documentation:** [ARCHITECTURE.md](./ARCHITECTURE.md)
|
|
21
|
-
|
|
22
|
-
### Key Design Principles
|
|
23
|
-
|
|
24
|
-
- **CSS is the library** - Pure CSS is 100% portable across frameworks
|
|
25
|
-
- **Zero dependencies** - No runtime external dependencies
|
|
26
|
-
- **Browser-native** - Modern CSS features over JS polyfills
|
|
27
|
-
- **Theme-agnostic** - Components work in both themes automatically
|
|
28
|
-
- **AI-first** - Documentation optimized for AI assistants
|
|
13
|
+
5. **AI Documentation** - llms.txt standard
|
|
29
14
|
|
|
30
|
-
|
|
15
|
+
**Key Principles:** CSS is the library | Zero dependencies | Theme-agnostic | AI-first
|
|
31
16
|
|
|
32
17
|
## Project Structure
|
|
33
18
|
|
|
34
19
|
```
|
|
35
20
|
jera/
|
|
36
21
|
├── src/
|
|
37
|
-
│ ├──
|
|
38
|
-
│ ├──
|
|
39
|
-
│
|
|
40
|
-
│
|
|
41
|
-
|
|
42
|
-
│ │ ├── typography.css # Font system
|
|
43
|
-
│ │ └── effects.css # Shadows, radius, transitions
|
|
44
|
-
│ ├── utils/
|
|
45
|
-
│ │ ├── cn.svelte.js # cn(), cv() class utilities
|
|
46
|
-
│ │ └── reactive.svelte.js # ThemeState, reactive helpers
|
|
47
|
-
│ ├── actions/
|
|
48
|
-
│ │ └── index.js # Svelte actions
|
|
49
|
-
│ └── components/
|
|
50
|
-
│ ├── primitives/ # Button, Badge, Divider, Avatar
|
|
51
|
-
│ ├── forms/ # Input, Select, Checkbox, Switch
|
|
52
|
-
│ ├── feedback/ # Toast, Skeleton, ProgressBar, Spinner
|
|
53
|
-
│ ├── overlays/ # Modal, Popover
|
|
54
|
-
│ ├── navigation/ # Tabs, Accordion, Sidebar
|
|
55
|
-
│ └── docs/ # CodeBlock, PropsTable, SplitPane, DocSection
|
|
56
|
-
├── llms.txt # AI documentation index
|
|
57
|
-
├── CLAUDE.md # This file
|
|
58
|
-
└── package.json
|
|
22
|
+
│ ├── tokens/ # colors.css, spacing.css, typography.css
|
|
23
|
+
│ ├── utils/ # cn.svelte.js, reactive.svelte.js (ThemeState)
|
|
24
|
+
│ ├── actions/ # clickOutside, focusTrap, portal, escapeKey
|
|
25
|
+
│ └── components/ # primitives/, forms/, feedback/, overlays/, navigation/
|
|
26
|
+
└── llms.txt
|
|
59
27
|
```
|
|
60
28
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
## Coding Standards
|
|
64
|
-
|
|
65
|
-
### Svelte 5 Patterns (REQUIRED)
|
|
66
|
-
- Use `$props()` for component props (single call only)
|
|
67
|
-
- Use `$state()` for reactive local state
|
|
68
|
-
- Use `$derived()` for computed values
|
|
69
|
-
- Use `$effect()` sparingly, only for side effects
|
|
70
|
-
- Use `$bindable()` for two-way binding props
|
|
71
|
-
|
|
72
|
-
### Component Template
|
|
73
|
-
```svelte
|
|
74
|
-
<script>
|
|
75
|
-
let {
|
|
76
|
-
variant = 'default',
|
|
77
|
-
size = 'md',
|
|
78
|
-
disabled = false,
|
|
79
|
-
class: className = '',
|
|
80
|
-
...rest
|
|
81
|
-
} = $props();
|
|
82
|
-
|
|
83
|
-
const computedClass = $derived(/* class logic */);
|
|
84
|
-
</script>
|
|
29
|
+
## Color System
|
|
85
30
|
|
|
86
|
-
|
|
87
|
-
{@render children?.()}
|
|
88
|
-
</element>
|
|
89
|
-
```
|
|
31
|
+
Uses Base16 naming: `base00`-`base0F` (hex digits).
|
|
90
32
|
|
|
91
|
-
|
|
92
|
-
```javascript
|
|
93
|
-
export const componentStyles = cv({
|
|
94
|
-
base: 'base-classes here',
|
|
95
|
-
variants: {
|
|
96
|
-
variantName: {
|
|
97
|
-
option1: 'classes-for-option1',
|
|
98
|
-
option2: 'classes-for-option2'
|
|
99
|
-
}
|
|
100
|
-
},
|
|
101
|
-
compounds: [
|
|
102
|
-
{ condition: { variant: 'x', size: 'y' }, class: 'compound-classes' }
|
|
103
|
-
],
|
|
104
|
-
defaults: { variantName: 'option1' }
|
|
105
|
-
});
|
|
106
|
-
```
|
|
33
|
+
**Full reference:** `docs/ai-context/base16-colors.md`
|
|
107
34
|
|
|
108
|
-
|
|
109
|
-
- Components: PascalCase (`Button.svelte`)
|
|
110
|
-
- Utilities: camelCase (`getTheme`)
|
|
111
|
-
- CSS tokens: kebab-case (`--color-primary`)
|
|
112
|
-
- Actions: camelCase (`clickOutside`)
|
|
113
|
-
|
|
114
|
-
---
|
|
115
|
-
|
|
116
|
-
## Miozu Base16 Color System
|
|
117
|
-
|
|
118
|
-
Uses standard Base16 naming: `base00`-`base0F` (hex digits, NOT decimal).
|
|
119
|
-
|
|
120
|
-
### Grayscale (base00-base07)
|
|
121
|
-
| Token | Dark Theme | Light Theme | Usage |
|
|
122
|
-
|-------|------------|-------------|-------|
|
|
123
|
-
| `--color-base00` | #0f1419 | #ffffff | Primary background |
|
|
124
|
-
| `--color-base01` | #1a1f26 | #f8f9fa | Surface/card |
|
|
125
|
-
| `--color-base02` | #242a33 | #f1f3f5 | Selection/hover |
|
|
126
|
-
| `--color-base03` | #4a5568 | #adb5bd | Muted/disabled |
|
|
127
|
-
| `--color-base04` | #a0aec0 | #6c757d | Secondary text |
|
|
128
|
-
| `--color-base05` | #e2e8f0 | #212529 | Primary text |
|
|
129
|
-
| `--color-base06` | #f7fafc | #1a1d20 | High emphasis |
|
|
130
|
-
| `--color-base07` | #ffffff | #0d0f10 | Maximum contrast |
|
|
131
|
-
|
|
132
|
-
### Accents (base08-base0F)
|
|
133
|
-
| Token | Color | Usage |
|
|
134
|
-
|-------|-------|-------|
|
|
135
|
-
| `--color-base08` | Red | Error |
|
|
136
|
-
| `--color-base09` | Orange | Warning |
|
|
137
|
-
| `--color-base0A` | Yellow | Highlight |
|
|
138
|
-
| `--color-base0B` | Green | Success |
|
|
139
|
-
| `--color-base0C` | Cyan | Info |
|
|
140
|
-
| `--color-base0D` | Blue | Primary |
|
|
141
|
-
| `--color-base0E` | Purple | Accent |
|
|
142
|
-
| `--color-base0F` | Brown/Orange | Secondary accent |
|
|
143
|
-
|
|
144
|
-
### Semantic Mappings
|
|
145
|
-
```css
|
|
146
|
-
--color-bg: var(--color-base00);
|
|
147
|
-
--color-surface: var(--color-base01);
|
|
148
|
-
--color-text: var(--color-base05);
|
|
149
|
-
--color-text-strong: var(--color-base07);
|
|
150
|
-
--color-primary: var(--color-base0D);
|
|
151
|
-
--color-success: var(--color-base0B);
|
|
152
|
-
--color-error: var(--color-base08);
|
|
153
|
-
```
|
|
35
|
+
## Component API Quick Reference
|
|
154
36
|
|
|
155
|
-
|
|
37
|
+
**Button:** `variant` (primary|secondary|ghost|outline|danger|success), `size` (xs-xl), `disabled`, `loading`, `href`
|
|
156
38
|
|
|
157
|
-
|
|
39
|
+
**Input:** `bind:value`, `type`, `placeholder`, `disabled`, `required`
|
|
158
40
|
|
|
159
|
-
|
|
160
|
-
```svelte
|
|
161
|
-
<Button
|
|
162
|
-
variant="primary|secondary|ghost|outline|danger|success"
|
|
163
|
-
size="xs|sm|md|lg|xl"
|
|
164
|
-
disabled={boolean}
|
|
165
|
-
loading={boolean}
|
|
166
|
-
fullWidth={boolean}
|
|
167
|
-
href={string} // Renders as <a> if provided
|
|
168
|
-
onclick={function}
|
|
169
|
-
>
|
|
170
|
-
Content
|
|
171
|
-
</Button>
|
|
172
|
-
```
|
|
41
|
+
**Select:** `options=[{value, label}]`, `bind:value`, `placeholder`
|
|
173
42
|
|
|
174
|
-
|
|
175
|
-
```svelte
|
|
176
|
-
<Input
|
|
177
|
-
bind:value={string}
|
|
178
|
-
type="text|email|password|number|..."
|
|
179
|
-
placeholder={string}
|
|
180
|
-
disabled={boolean}
|
|
181
|
-
required={boolean}
|
|
182
|
-
oninput={function}
|
|
183
|
-
onchange={function}
|
|
184
|
-
/>
|
|
185
|
-
```
|
|
43
|
+
**Badge:** `variant` (default|primary|success|warning|error), `size` (sm|md|lg)
|
|
186
44
|
|
|
187
|
-
|
|
188
|
-
```svelte
|
|
189
|
-
<Select
|
|
190
|
-
options={[{ value, label }]}
|
|
191
|
-
bind:value={any}
|
|
192
|
-
placeholder={string}
|
|
193
|
-
labelKey="label"
|
|
194
|
-
valueKey="value"
|
|
195
|
-
disabled={boolean}
|
|
196
|
-
onchange={function}
|
|
197
|
-
/>
|
|
198
|
-
```
|
|
45
|
+
**Modal:** `bind:open`, `title`, `size` (sm-xl), `variant`, `footer` (snippet)
|
|
199
46
|
|
|
200
|
-
|
|
201
|
-
```svelte
|
|
202
|
-
<Badge
|
|
203
|
-
variant="default|primary|secondary|success|warning|error"
|
|
204
|
-
size="sm|md|lg"
|
|
205
|
-
>
|
|
206
|
-
Text
|
|
207
|
-
</Badge>
|
|
208
|
-
```
|
|
47
|
+
**Toast:** Use `createToastContext()` in root, `getToastContext()` anywhere
|
|
209
48
|
|
|
210
|
-
|
|
211
|
-
```svelte
|
|
212
|
-
<!-- In root layout -->
|
|
213
|
-
<script>
|
|
214
|
-
import { Toast, createToastContext } from '@miozu/jera';
|
|
215
|
-
const toast = createToastContext();
|
|
216
|
-
</script>
|
|
217
|
-
<Toast />
|
|
49
|
+
**Tabs:** `tabs=[{id, label, badge?}]`, `bind:active`, `variant` (default|underline|pills)
|
|
218
50
|
|
|
219
|
-
|
|
220
|
-
<script>
|
|
221
|
-
import { getToastContext } from '@miozu/jera';
|
|
222
|
-
const toast = getToastContext();
|
|
223
|
-
toast.success('Message');
|
|
224
|
-
toast.error('Error message');
|
|
225
|
-
</script>
|
|
226
|
-
```
|
|
51
|
+
**LeftBar:** `bind:collapsed`, `persistKey`, snippets: `header`, `navigation`, `footer`
|
|
227
52
|
|
|
228
|
-
|
|
229
|
-
```svelte
|
|
230
|
-
<script>
|
|
231
|
-
import { Modal, Button } from '@miozu/jera';
|
|
232
|
-
let showModal = $state(false);
|
|
233
|
-
</script>
|
|
53
|
+
**LeftBarItem:** `href`, `label`, `icon`, `active`, `badge`, `expandable`, `subroutes`
|
|
234
54
|
|
|
235
|
-
|
|
55
|
+
## Theme Management
|
|
236
56
|
|
|
237
|
-
|
|
238
|
-
<p>Are you sure you want to proceed?</p>
|
|
239
|
-
{#snippet footer()}
|
|
240
|
-
<Button variant="ghost" onclick={() => showModal = false}>Cancel</Button>
|
|
241
|
-
<Button variant="danger" onclick={handleConfirm}>Confirm</Button>
|
|
242
|
-
{/snippet}
|
|
243
|
-
</Modal>
|
|
244
|
-
```
|
|
57
|
+
Singleton pattern with `miozu-theme` storage key.
|
|
245
58
|
|
|
246
|
-
|
|
59
|
+
**Full reference:** `docs/ai-context/theme-management.md`
|
|
247
60
|
|
|
248
|
-
### Popover
|
|
249
61
|
```svelte
|
|
62
|
+
<!-- +layout.svelte -->
|
|
250
63
|
<script>
|
|
251
|
-
import {
|
|
64
|
+
import { getTheme } from '@miozu/jera';
|
|
65
|
+
const themeState = getTheme();
|
|
66
|
+
onMount(() => { themeState.sync(); themeState.init(); });
|
|
252
67
|
</script>
|
|
253
|
-
|
|
254
|
-
<Popover content="Helpful tooltip text" position="top">
|
|
255
|
-
<Button>Hover me</Button>
|
|
256
|
-
</Popover>
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
Props: `content`, `position` (top/bottom/left/right), `delay` ({show, hide}), `offset`
|
|
260
|
-
|
|
261
|
-
### Divider
|
|
262
|
-
```svelte
|
|
263
|
-
<Divider />
|
|
264
|
-
<Divider orientation="vertical" />
|
|
265
|
-
<Divider>or continue with</Divider>
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
Props: `orientation` (horizontal/vertical), `thickness`, `spacing`, `children`
|
|
269
|
-
|
|
270
|
-
### Stat
|
|
271
|
-
```svelte
|
|
272
|
-
<Stat value="42" label="Users" />
|
|
273
|
-
<Stat value="95%" label="Uptime" status="success" />
|
|
274
|
-
<Stat value="75" max={100} label="Progress" showBar />
|
|
275
|
-
<Stat value="12" unit="/15" label="Tasks" status="warning" />
|
|
276
|
-
<Stat value="8" label="Errors" href="/errors" status="error" />
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
Props: `value`, `label`, `unit`, `secondary`, `status` (success/warning/error/info), `size` (sm/md/lg), `showBar`, `barValue`, `max`, `href`
|
|
280
|
-
|
|
281
|
-
### Alert
|
|
282
|
-
```svelte
|
|
283
|
-
<Alert variant="info">This is an informational message.</Alert>
|
|
284
|
-
<Alert variant="warning" title="Warning">Please review your settings.</Alert>
|
|
285
|
-
<Alert variant="error" dismissible onclose={() => showAlert = false}>
|
|
286
|
-
An error occurred.
|
|
287
|
-
</Alert>
|
|
288
|
-
<Alert variant="success">
|
|
289
|
-
{#snippet icon()}
|
|
290
|
-
<CheckIcon size={16} />
|
|
291
|
-
{/snippet}
|
|
292
|
-
Your changes have been saved.
|
|
293
|
-
</Alert>
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
Props: `variant` (info/success/warning/error), `title`, `dismissible`, `size` (sm/md/lg), `onclose`, `icon` (snippet), `actions` (snippet)
|
|
297
|
-
|
|
298
|
-
### Avatar
|
|
299
|
-
```svelte
|
|
300
|
-
<Avatar src="/user.jpg" alt="John Doe" />
|
|
301
|
-
<Avatar name="John Doe" />
|
|
302
|
-
<Avatar src="/user.jpg" status="online" size="lg" />
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
Props: `src`, `alt`, `name`, `size` (xs/sm/md/lg/xl/2xl), `status` (online/offline/busy/away)
|
|
306
|
-
|
|
307
|
-
### Skeleton
|
|
308
|
-
```svelte
|
|
309
|
-
<Skeleton width="80%" />
|
|
310
|
-
<Skeleton variant="circle" size="48px" />
|
|
311
|
-
<Skeleton variant="rect" width="100%" height="200px" />
|
|
312
|
-
<Skeleton lines={3} />
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
Props: `variant` (text/heading/circle/rect), `width`, `height`, `size`, `lines`, `animate`
|
|
316
|
-
|
|
317
|
-
### ProgressBar
|
|
318
|
-
```svelte
|
|
319
|
-
<ProgressBar value={65} />
|
|
320
|
-
<ProgressBar value={80} showLabel variant="success" />
|
|
321
|
-
<ProgressBar indeterminate />
|
|
322
68
|
```
|
|
323
69
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
### Spinner
|
|
327
|
-
```svelte
|
|
328
|
-
<Spinner />
|
|
329
|
-
<Spinner size="lg" color="var(--color-base11)" />
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
Props: `size` (xs/sm/md/lg/xl), `color`, `label`
|
|
333
|
-
|
|
334
|
-
### Tabs
|
|
335
|
-
```svelte
|
|
336
|
-
<script>
|
|
337
|
-
import { Tabs } from '@miozu/jera';
|
|
338
|
-
let activeTab = $state('tab1');
|
|
339
|
-
</script>
|
|
340
|
-
|
|
341
|
-
<Tabs
|
|
342
|
-
tabs={[
|
|
343
|
-
{ id: 'tab1', label: 'Overview' },
|
|
344
|
-
{ id: 'tab2', label: 'Settings', badge: 3 },
|
|
345
|
-
{ id: 'tab3', label: 'Analytics', disabled: true }
|
|
346
|
-
]}
|
|
347
|
-
bind:active={activeTab}
|
|
348
|
-
variant="underline"
|
|
349
|
-
/>
|
|
350
|
-
```
|
|
70
|
+
## Svelte 5 Patterns
|
|
351
71
|
|
|
352
|
-
|
|
72
|
+
**Full reference:** `docs/ai-context/svelte5-patterns.md`
|
|
353
73
|
|
|
354
|
-
### Accordion
|
|
355
74
|
```svelte
|
|
356
75
|
<script>
|
|
357
|
-
|
|
76
|
+
let { variant = 'default', class: className = '', ...rest } = $props();
|
|
77
|
+
const classes = $derived(/* ... */);
|
|
358
78
|
</script>
|
|
359
79
|
|
|
360
|
-
<
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
</AccordionItem>
|
|
364
|
-
<AccordionItem id="section2" title="Section 2">
|
|
365
|
-
Content for section 2
|
|
366
|
-
</AccordionItem>
|
|
367
|
-
</Accordion>
|
|
80
|
+
<div class={classes} {...rest}>
|
|
81
|
+
{@render children?.()}
|
|
82
|
+
</div>
|
|
368
83
|
```
|
|
369
84
|
|
|
370
|
-
|
|
371
|
-
AccordionItem props: `id`, `title`, `disabled`
|
|
372
|
-
|
|
373
|
-
### LeftBar (Sidebar Navigation)
|
|
374
|
-
|
|
375
|
-
A lightweight, collapsible sidebar for admin-style navigation.
|
|
376
|
-
|
|
377
|
-
```svelte
|
|
378
|
-
<script>
|
|
379
|
-
import { LeftBar, LeftBarSection, LeftBarItem, LeftBarGroup, LeftBarToggle, createActiveChecker } from '@miozu/jera';
|
|
380
|
-
import { page } from '$app/stores';
|
|
381
|
-
import { Home, Settings, Users, Building2 } from 'lucide-svelte';
|
|
382
|
-
|
|
383
|
-
let collapsed = $state(false);
|
|
384
|
-
const isActive = createActiveChecker(() => $page.url.pathname);
|
|
385
|
-
</script>
|
|
386
|
-
|
|
387
|
-
<LeftBar bind:collapsed persistKey="my-sidebar">
|
|
388
|
-
{#snippet header()}
|
|
389
|
-
<div class="logo">MyApp</div>
|
|
390
|
-
{/snippet}
|
|
391
|
-
|
|
392
|
-
{#snippet navigation()}
|
|
393
|
-
<LeftBarSection>
|
|
394
|
-
<LeftBarItem href="/" icon={Home} label="Dashboard" active={isActive('/', 'exact')} />
|
|
395
|
-
<LeftBarItem href="/users" icon={Users} label="Users" badge={5} active={isActive('/users')} />
|
|
396
|
-
</LeftBarSection>
|
|
397
|
-
|
|
398
|
-
<LeftBarSection title="Management">
|
|
399
|
-
<LeftBarItem
|
|
400
|
-
label="Organization"
|
|
401
|
-
icon={Building2}
|
|
402
|
-
expandable
|
|
403
|
-
subroutes={[
|
|
404
|
-
{ label: 'Members', href: '/org/members' },
|
|
405
|
-
{ label: 'Settings', href: '/org/settings' }
|
|
406
|
-
]}
|
|
407
|
-
/>
|
|
408
|
-
</LeftBarSection>
|
|
409
|
-
|
|
410
|
-
<!-- Generic group for accounts, projects, workspaces, etc. -->
|
|
411
|
-
<LeftBarGroup
|
|
412
|
-
title="Connected Accounts"
|
|
413
|
-
items={accounts}
|
|
414
|
-
bind:expanded={accountsExpanded}
|
|
415
|
-
onItemClick={(account) => goto(`/account/${account.id}`)}
|
|
416
|
-
onAddClick={() => showConnectModal = true}
|
|
417
|
-
addLabel="Connect account"
|
|
418
|
-
/>
|
|
419
|
-
{/snippet}
|
|
420
|
-
|
|
421
|
-
{#snippet footer()}
|
|
422
|
-
<LeftBarToggle />
|
|
423
|
-
{/snippet}
|
|
424
|
-
</LeftBar>
|
|
425
|
-
```
|
|
85
|
+
## Class Variants (cv)
|
|
426
86
|
|
|
427
|
-
**LeftBar** props: `collapsed` (bindable), `persistKey`, `header` (snippet), `navigation` (snippet), `footer` (snippet)
|
|
428
|
-
|
|
429
|
-
**LeftBarSection** props: `title`, `class`
|
|
430
|
-
|
|
431
|
-
**LeftBarItem** props:
|
|
432
|
-
- `href` - Link destination (renders as `<a>`)
|
|
433
|
-
- `label` - Display text
|
|
434
|
-
- `icon` - Lucide icon component
|
|
435
|
-
- `active` - Boolean for active state
|
|
436
|
-
- `badge` - Number/string for badge count
|
|
437
|
-
- `expandable` - Enable expand/collapse
|
|
438
|
-
- `expanded` - Bindable expansion state
|
|
439
|
-
- `subroutes` - Array of `{ label, href }` for expandable items
|
|
440
|
-
- `preload` - Boolean for SvelteKit preload-data (default: true)
|
|
441
|
-
- `leading` - Snippet for content before icon
|
|
442
|
-
- `trailing` - Snippet for content after label (custom badges, etc.)
|
|
443
|
-
- `isActiveRoute` - Function to check subroute active state
|
|
444
|
-
|
|
445
|
-
**LeftBarGroup** props (generic collection for accounts, projects, etc.):
|
|
446
|
-
- `title` - Section header
|
|
447
|
-
- `items` - Array of items to display
|
|
448
|
-
- `expanded` - Bindable expansion state
|
|
449
|
-
- `expandedItems` - Object tracking which items are expanded
|
|
450
|
-
- `expandable` - Enable item-level expansion
|
|
451
|
-
- `showAdd` - Show add button
|
|
452
|
-
- `addLabel` - Add button text
|
|
453
|
-
- `showCount` - Show item count badge
|
|
454
|
-
- `onItemClick` - Item click handler
|
|
455
|
-
- `onAddClick` - Add button handler
|
|
456
|
-
- `getItemId`, `getItemName`, `getItemAvatar`, `getItemPlatform` - Item accessors
|
|
457
|
-
- `getSubroutes` - Function returning subroutes for an item
|
|
458
|
-
- `isItemActive`, `isSubrouteActive` - Active state checkers
|
|
459
|
-
- `item` - Custom item rendering snippet
|
|
460
|
-
|
|
461
|
-
**createActiveChecker** utility:
|
|
462
87
|
```javascript
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
### CodeBlock
|
|
475
|
-
```svelte
|
|
476
|
-
<script>
|
|
477
|
-
import { CodeBlock } from '@miozu/jera';
|
|
478
|
-
</script>
|
|
479
|
-
|
|
480
|
-
<CodeBlock
|
|
481
|
-
code={`const greeting = "Hello";`}
|
|
482
|
-
lang="javascript"
|
|
483
|
-
filename="example.js"
|
|
484
|
-
showLineNumbers
|
|
485
|
-
/>
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
Props: `code`, `lang` (default: javascript), `filename`, `showLineNumbers`, `class`
|
|
489
|
-
|
|
490
|
-
**Requires:** `shiki` package for syntax highlighting.
|
|
491
|
-
|
|
492
|
-
### PropsTable
|
|
493
|
-
```svelte
|
|
494
|
-
<script>
|
|
495
|
-
import { PropsTable } from '@miozu/jera';
|
|
496
|
-
</script>
|
|
497
|
-
|
|
498
|
-
<PropsTable props={[
|
|
499
|
-
{ name: 'variant', type: 'string', default: '"default"', description: 'Visual style' },
|
|
500
|
-
{ name: 'onclick', type: 'function', required: true, description: 'Click handler' }
|
|
501
|
-
]} />
|
|
502
|
-
```
|
|
503
|
-
|
|
504
|
-
Props: `props` (array of PropDef), `class`
|
|
505
|
-
|
|
506
|
-
PropDef: `{ name, type, default?, description, required? }`
|
|
507
|
-
|
|
508
|
-
### SplitPane
|
|
509
|
-
```svelte
|
|
510
|
-
<script>
|
|
511
|
-
import { SplitPane, CodeBlock } from '@miozu/jera';
|
|
512
|
-
</script>
|
|
513
|
-
|
|
514
|
-
<SplitPane ratio="1:1" gap="2rem" stickyRight>
|
|
515
|
-
{#snippet left()}
|
|
516
|
-
<h2>Description</h2>
|
|
517
|
-
<p>Explanation of the feature...</p>
|
|
518
|
-
{/snippet}
|
|
519
|
-
{#snippet right()}
|
|
520
|
-
<CodeBlock code={exampleCode} lang="javascript" />
|
|
521
|
-
{/snippet}
|
|
522
|
-
</SplitPane>
|
|
523
|
-
```
|
|
524
|
-
|
|
525
|
-
Props: `left` (snippet), `right` (snippet), `ratio` (e.g., '1:1', '2:1'), `gap`, `minHeight`, `stickyRight`, `class`
|
|
526
|
-
|
|
527
|
-
### DocSection
|
|
528
|
-
```svelte
|
|
529
|
-
<script>
|
|
530
|
-
import { DocSection, CodeBlock } from '@miozu/jera';
|
|
531
|
-
</script>
|
|
532
|
-
|
|
533
|
-
<DocSection id="installation" title="Installation" description="How to install">
|
|
534
|
-
<CodeBlock code="npm install @miozu/jera" lang="bash" />
|
|
535
|
-
</DocSection>
|
|
88
|
+
export const buttonStyles = cv({
|
|
89
|
+
base: 'inline-flex items-center justify-center rounded-lg font-medium',
|
|
90
|
+
variants: {
|
|
91
|
+
variant: {
|
|
92
|
+
primary: 'bg-base0D text-white',
|
|
93
|
+
ghost: 'bg-transparent text-base05 hover:bg-base02'
|
|
94
|
+
},
|
|
95
|
+
size: { sm: 'h-8 px-3 text-sm', md: 'h-10 px-4' }
|
|
96
|
+
},
|
|
97
|
+
defaults: { variant: 'primary', size: 'md' }
|
|
98
|
+
});
|
|
536
99
|
```
|
|
537
100
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
---
|
|
101
|
+
## Actions
|
|
541
102
|
|
|
542
|
-
## Actions Reference
|
|
543
|
-
|
|
544
|
-
### clickOutside
|
|
545
103
|
```svelte
|
|
546
104
|
<div use:clickOutside={() => isOpen = false}>
|
|
547
|
-
```
|
|
548
|
-
|
|
549
|
-
### focusTrap
|
|
550
|
-
```svelte
|
|
551
105
|
<dialog use:focusTrap={{ enabled: isOpen }}>
|
|
552
|
-
```
|
|
553
|
-
|
|
554
|
-
### portal
|
|
555
|
-
```svelte
|
|
556
106
|
<div use:portal={'body'}>
|
|
557
|
-
Renders at body level
|
|
558
|
-
</div>
|
|
559
|
-
```
|
|
560
|
-
|
|
561
|
-
### escapeKey
|
|
562
|
-
```svelte
|
|
563
107
|
<div use:escapeKey={() => close()}>
|
|
564
108
|
```
|
|
565
109
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
## Common Tasks
|
|
569
|
-
|
|
570
|
-
### Add New Component
|
|
571
|
-
1. Create file in appropriate folder (`primitives/`, `forms/`, `feedback/`)
|
|
572
|
-
2. Use single `$props()` call
|
|
573
|
-
3. Export styles with `cv()` if variants needed
|
|
574
|
-
4. Add to `src/index.js` exports
|
|
575
|
-
5. Document in this file
|
|
576
|
-
|
|
577
|
-
### Add New Token
|
|
578
|
-
1. Add to appropriate token file in `src/tokens/`
|
|
579
|
-
2. Use semantic naming
|
|
580
|
-
3. Add light theme variant if applicable
|
|
581
|
-
|
|
582
|
-
### Theme Switching (Single Source of Truth Pattern)
|
|
583
|
-
|
|
584
|
-
jera uses a singleton pattern for global theme state. Storage key: `miozu-theme`.
|
|
585
|
-
|
|
586
|
-
#### SvelteKit Execution Order (from source code analysis)
|
|
587
|
-
|
|
588
|
-
Understanding when code runs helps choose the right pattern:
|
|
589
|
-
|
|
590
|
-
| Phase | File | Server | Client | Notes |
|
|
591
|
-
|-------|------|--------|--------|-------|
|
|
592
|
-
| 1 | +layout.server.js | ✓ | - | Server-only data |
|
|
593
|
-
| 2 | +layout.js | ✓ | ✓ | Universal load, runs on EVERY navigation |
|
|
594
|
-
| 3 | +layout.svelte `<script>` | ✓ | ✓ | Component script, runs once on mount |
|
|
595
|
-
| 4 | +layout.svelte `onMount()` | - | ✓ | Client-only, after hydration |
|
|
596
|
-
|
|
597
|
-
**Key insight from SvelteKit source (`client.js:684`):**
|
|
598
|
-
```javascript
|
|
599
|
-
data = { ...data, ...node.data }; // Parent data cascades to children
|
|
600
|
-
```
|
|
601
|
-
|
|
602
|
-
#### Recommended Pattern: +layout.svelte (for singletons like theme)
|
|
603
|
-
|
|
604
|
-
For global singleton state that doesn't need server context:
|
|
605
|
-
|
|
606
|
-
```svelte
|
|
607
|
-
<!-- +layout.svelte - Initialize ONCE, pass via props -->
|
|
608
|
-
<script>
|
|
609
|
-
import { getTheme } from '@miozu/jera';
|
|
610
|
-
import { onMount } from 'svelte';
|
|
611
|
-
import { Sidebar } from '$components';
|
|
612
|
-
|
|
613
|
-
// Call singleton once in root layout
|
|
614
|
-
const themeState = getTheme();
|
|
615
|
-
|
|
616
|
-
onMount(() => {
|
|
617
|
-
themeState.sync(); // Hydrate from DOM (app.html)
|
|
618
|
-
themeState.init(); // Setup media query listener
|
|
619
|
-
});
|
|
620
|
-
</script>
|
|
621
|
-
|
|
622
|
-
<!-- Pass themeState as prop to children -->
|
|
623
|
-
<Sidebar {themeState} />
|
|
624
|
-
```
|
|
625
|
-
|
|
626
|
-
```svelte
|
|
627
|
-
<!-- Child component - receives themeState as prop -->
|
|
628
|
-
<script>
|
|
629
|
-
// DON'T call getTheme() here - receive as prop instead
|
|
630
|
-
let { themeState } = $props();
|
|
631
|
-
|
|
632
|
-
function handleToggle() {
|
|
633
|
-
themeState.toggle();
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
let isDark = $derived(themeState.isDark);
|
|
637
|
-
</script>
|
|
638
|
-
```
|
|
639
|
-
|
|
640
|
-
#### Alternative Pattern: +layout.js (for data-heavy state)
|
|
641
|
-
|
|
642
|
-
For state that needs server context or async initialization:
|
|
643
|
-
|
|
644
|
-
```javascript
|
|
645
|
-
// +layout.js - Universal load
|
|
646
|
-
export const load = async ({ data, fetch }) => {
|
|
647
|
-
// Can fetch data, access parent(), etc.
|
|
648
|
-
const someData = await fetch('/api/data').then(r => r.json());
|
|
649
|
-
|
|
650
|
-
return {
|
|
651
|
-
...data,
|
|
652
|
-
someData
|
|
653
|
-
};
|
|
654
|
-
};
|
|
655
|
-
```
|
|
656
|
-
|
|
657
|
-
**When to use which:**
|
|
658
|
-
|
|
659
|
-
| Use +layout.svelte | Use +layout.js |
|
|
660
|
-
|-------------------|----------------|
|
|
661
|
-
| Singletons (theme, toast) | Fetched data |
|
|
662
|
-
| Client-only initialization | Async operations |
|
|
663
|
-
| No server context needed | Needs `parent()` data |
|
|
664
|
-
| Runs once per mount | Needs invalidation support |
|
|
665
|
-
|
|
666
|
-
**ThemeState API:**
|
|
667
|
-
```javascript
|
|
668
|
-
themeState.toggle(); // Toggle dark/light
|
|
669
|
-
themeState.set('dark'); // Set to dark
|
|
670
|
-
themeState.set('light'); // Set to light
|
|
671
|
-
themeState.set('system'); // Follow system preference
|
|
672
|
-
|
|
673
|
-
// Reactive properties
|
|
674
|
-
themeState.current; // 'light' | 'dark' | 'system'
|
|
675
|
-
themeState.resolved; // 'light' | 'dark' (actual resolved value)
|
|
676
|
-
themeState.dataTheme; // 'miozu-light' | 'miozu-dark'
|
|
677
|
-
themeState.isDark; // boolean
|
|
678
|
-
themeState.isLight; // boolean
|
|
679
|
-
```
|
|
680
|
-
|
|
681
|
-
**Why pass as props instead of calling `getTheme()` everywhere?**
|
|
682
|
-
1. **Explicit data flow** - You see what state each component depends on
|
|
683
|
-
2. **Better testing** - Can inject mock state easily
|
|
684
|
-
3. **Single initialization** - Clear where state originates
|
|
685
|
-
4. **Efficiency** - Singleton runs once, not on every navigation
|
|
686
|
-
|
|
687
|
-
### Theme Data Attributes
|
|
688
|
-
|
|
689
|
-
| Theme | data-theme value |
|
|
690
|
-
|-------|------------------|
|
|
691
|
-
| Dark | `miozu-dark` |
|
|
692
|
-
| Light | `miozu-light` |
|
|
693
|
-
|
|
694
|
-
CSS selectors should use:
|
|
695
|
-
```css
|
|
696
|
-
[data-theme='miozu-dark'] { /* dark styles */ }
|
|
697
|
-
[data-theme='miozu-light'] { /* light styles */ }
|
|
698
|
-
```
|
|
699
|
-
|
|
700
|
-
### Migration Guide: Custom Theme → Jera Singleton
|
|
701
|
-
|
|
702
|
-
If you have a custom `theme.svelte.js` or similar, follow these steps:
|
|
703
|
-
|
|
704
|
-
#### Step 1: Update app.html
|
|
705
|
-
Replace custom theme script with unified storage key:
|
|
706
|
-
```javascript
|
|
707
|
-
// CRITICAL: Prevent FOUC - runs before any CSS
|
|
708
|
-
(function () {
|
|
709
|
-
try {
|
|
710
|
-
let pref = localStorage.getItem('miozu-theme');
|
|
711
|
-
|
|
712
|
-
// Migrate from old keys if needed
|
|
713
|
-
if (!pref || !['light', 'dark', 'system'].includes(pref)) {
|
|
714
|
-
const oldTheme = localStorage.getItem('theme'); // or your old key
|
|
715
|
-
if (oldTheme === 'miozu-dark' || oldTheme === 'dark') pref = 'dark';
|
|
716
|
-
else if (oldTheme === 'miozu-light' || oldTheme === 'light') pref = 'light';
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
// Resolve theme
|
|
720
|
-
let theme;
|
|
721
|
-
if (pref === 'light') theme = 'miozu-light';
|
|
722
|
-
else if (pref === 'dark') theme = 'miozu-dark';
|
|
723
|
-
else {
|
|
724
|
-
theme = window.matchMedia?.('(prefers-color-scheme: dark)').matches
|
|
725
|
-
? 'miozu-dark' : 'miozu-light';
|
|
726
|
-
pref = 'system';
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
// Apply and persist
|
|
730
|
-
document.documentElement.setAttribute('data-theme', theme);
|
|
731
|
-
document.documentElement.style.colorScheme = theme === 'miozu-dark' ? 'dark' : 'light';
|
|
732
|
-
localStorage.setItem('miozu-theme', pref);
|
|
733
|
-
document.cookie = 'miozu-theme=' + pref + '; path=/; max-age=31536000; SameSite=Lax';
|
|
734
|
-
|
|
735
|
-
// Clean up old keys
|
|
736
|
-
localStorage.removeItem('theme');
|
|
737
|
-
} catch (e) {
|
|
738
|
-
document.documentElement.setAttribute('data-theme', 'miozu-dark');
|
|
739
|
-
}
|
|
740
|
-
})();
|
|
741
|
-
```
|
|
742
|
-
|
|
743
|
-
#### Step 2: Update +layout.svelte
|
|
744
|
-
```svelte
|
|
745
|
-
<script>
|
|
746
|
-
import { getTheme } from '@miozu/jera';
|
|
747
|
-
import { onMount } from 'svelte';
|
|
748
|
-
|
|
749
|
-
const themeState = getTheme();
|
|
750
|
-
|
|
751
|
-
onMount(() => {
|
|
752
|
-
themeState.sync();
|
|
753
|
-
themeState.init();
|
|
754
|
-
});
|
|
755
|
-
</script>
|
|
756
|
-
```
|
|
757
|
-
|
|
758
|
-
#### Step 3: Update +layout.js
|
|
759
|
-
Remove any theme initialization - it should NOT be in +layout.js:
|
|
760
|
-
```javascript
|
|
761
|
-
// REMOVE these lines:
|
|
762
|
-
// import { ThemeReactiveState } from '$lib/reactiveStates/theme.svelte.js';
|
|
763
|
-
// const themeState = new ThemeReactiveState();
|
|
764
|
-
// return { themeState, ... };
|
|
765
|
-
```
|
|
766
|
-
|
|
767
|
-
#### Step 4: Update components using theme
|
|
768
|
-
```svelte
|
|
769
|
-
<script>
|
|
770
|
-
// OLD (remove):
|
|
771
|
-
// import { getThemeState } from '$lib/reactiveStates/theme.svelte.js';
|
|
772
|
-
// const themeState = getThemeState();
|
|
773
|
-
|
|
774
|
-
// NEW:
|
|
775
|
-
import { getTheme } from '@miozu/jera';
|
|
776
|
-
const themeState = getTheme();
|
|
777
|
-
|
|
778
|
-
let isDark = $derived(themeState.isDark);
|
|
779
|
-
</script>
|
|
780
|
-
```
|
|
781
|
-
|
|
782
|
-
#### Step 5: Delete old theme file
|
|
783
|
-
```bash
|
|
784
|
-
rm src/lib/reactiveStates/theme.svelte.js
|
|
785
|
-
```
|
|
786
|
-
|
|
787
|
-
#### Step 6: Verify no old imports remain
|
|
788
|
-
```bash
|
|
789
|
-
grep -r "theme.svelte.js\|getThemeState\|ThemeReactiveState" src/
|
|
790
|
-
# Should return no matches
|
|
791
|
-
```
|
|
792
|
-
|
|
793
|
-
### Apps Using Jera Theme (Proof of Concept)
|
|
794
|
-
|
|
795
|
-
| App | Status | Storage Key |
|
|
796
|
-
|-----|--------|-------------|
|
|
797
|
-
| dash.selify.ai | ✓ Migrated | `miozu-theme` |
|
|
798
|
-
| admin.selify.ai | ✓ Migrated | `miozu-theme` |
|
|
799
|
-
| miozu.com | ✓ Migrated | `miozu-theme` |
|
|
800
|
-
|
|
801
|
-
All three apps share the same theme preference via unified `miozu-theme` localStorage key.
|
|
802
|
-
|
|
803
|
-
---
|
|
804
|
-
|
|
805
|
-
## Integration with dash.selify.ai
|
|
806
|
-
|
|
807
|
-
jera components work out-of-the-box with dash.selify.ai. The semantic tokens are already configured in both systems.
|
|
808
|
-
|
|
809
|
-
### Required: Import jera tokens
|
|
810
|
-
```css
|
|
811
|
-
/* In your app.css or layout */
|
|
812
|
-
@import '@miozu/jera/tokens/colors.css';
|
|
813
|
-
```
|
|
814
|
-
|
|
815
|
-
### Semantic Token Mapping
|
|
816
|
-
| jera Token | dash.selify.ai Equivalent |
|
|
817
|
-
|------------|---------------------------|
|
|
818
|
-
| `--color-bg` | `--color-base00` |
|
|
819
|
-
| `--color-surface` | `--color-base01` |
|
|
820
|
-
| `--color-surface-alt` | `--color-base02` |
|
|
821
|
-
| `--color-text` | `--color-base05` |
|
|
822
|
-
| `--color-text-strong` | `--color-base07` |
|
|
823
|
-
| `--color-text-muted` | `--color-base04` |
|
|
824
|
-
| `--color-primary` | `--color-base0D` |
|
|
825
|
-
| `--color-success` | `--color-base0B` |
|
|
826
|
-
| `--color-warning` | `--color-base0A` |
|
|
827
|
-
| `--color-error` | `--color-base08` |
|
|
828
|
-
| `--color-info` | `--color-base0C` |
|
|
829
|
-
|
|
830
|
-
### Using jera Components in dash.selify.ai
|
|
831
|
-
```svelte
|
|
832
|
-
<script>
|
|
833
|
-
import { Button, Modal, Input } from '@miozu/jera';
|
|
834
|
-
|
|
835
|
-
let showModal = $state(false);
|
|
836
|
-
</script>
|
|
837
|
-
|
|
838
|
-
<!-- Works with existing dash.selify.ai theme system -->
|
|
839
|
-
<Button variant="primary" onclick={() => showModal = true}>
|
|
840
|
-
Open Modal
|
|
841
|
-
</Button>
|
|
842
|
-
|
|
843
|
-
<Modal bind:open={showModal} title="Example">
|
|
844
|
-
<Input placeholder="Type here..." />
|
|
845
|
-
</Modal>
|
|
846
|
-
```
|
|
847
|
-
|
|
848
|
-
---
|
|
849
|
-
|
|
850
|
-
## Rules for AI Assistants
|
|
110
|
+
## Rules for AI
|
|
851
111
|
|
|
852
|
-
1.
|
|
853
|
-
2.
|
|
854
|
-
3.
|
|
855
|
-
4.
|
|
856
|
-
5.
|
|
857
|
-
6.
|
|
858
|
-
7. **Mobile-first** - Design for mobile, enhance for desktop
|
|
112
|
+
1. Svelte 5 runes only - no legacy syntax
|
|
113
|
+
2. Single `$props()` call per component
|
|
114
|
+
3. Use `cv()` for variants
|
|
115
|
+
4. Semantic colors from Base16
|
|
116
|
+
5. Accessibility first (ARIA, keyboard)
|
|
117
|
+
6. Pure JavaScript (no TypeScript)
|
package/package.json
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miozu/jera",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Zero-dependency, AI-first component library for Svelte 5",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"prepublishOnly": "echo 'Publishing @miozu/jera...' && test -f src/index.js"
|
|
8
|
-
},
|
|
9
6
|
"svelte": "./src/index.js",
|
|
10
7
|
"exports": {
|
|
11
8
|
".": {
|
|
@@ -58,5 +55,6 @@
|
|
|
58
55
|
"bugs": {
|
|
59
56
|
"url": "https://github.com/miozu-com/jera/issues"
|
|
60
57
|
},
|
|
61
|
-
"dependencies": {}
|
|
62
|
-
}
|
|
58
|
+
"dependencies": {},
|
|
59
|
+
"scripts": {}
|
|
60
|
+
}
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
subroutes = [],
|
|
35
35
|
badge = null,
|
|
36
36
|
preload = true,
|
|
37
|
+
variant = 'default',
|
|
37
38
|
onclick = null,
|
|
38
39
|
isActiveRoute = () => false,
|
|
39
40
|
class: className = '',
|
|
@@ -74,6 +75,7 @@
|
|
|
74
75
|
class="nav-item {className}"
|
|
75
76
|
class:active
|
|
76
77
|
class:collapsed={isCollapsed}
|
|
78
|
+
data-variant={variant}
|
|
77
79
|
title={isCollapsed ? label : null}
|
|
78
80
|
data-sveltekit-preload-data={preloadAttr}
|
|
79
81
|
>
|
|
@@ -94,16 +96,22 @@
|
|
|
94
96
|
<button
|
|
95
97
|
class="nav-item expandable {className}"
|
|
96
98
|
class:collapsed={isCollapsed}
|
|
99
|
+
data-variant={variant}
|
|
97
100
|
onclick={handleClick}
|
|
98
101
|
onmouseenter={handleMouseEnter}
|
|
99
102
|
onmouseleave={handleMouseLeave}
|
|
100
103
|
title={isCollapsed ? label : null}
|
|
101
104
|
>
|
|
105
|
+
{@render leading?.()}
|
|
102
106
|
{#if Icon}
|
|
103
107
|
<Icon size={18} class="nav-icon" />
|
|
104
108
|
{/if}
|
|
105
109
|
{#if !isCollapsed}
|
|
106
110
|
<span class="nav-label" transition:fade={{ duration: 150 }}>{label}</span>
|
|
111
|
+
{#if badge != null}
|
|
112
|
+
<span class="nav-badge" transition:fade={{ duration: 150 }}>{badge}</span>
|
|
113
|
+
{/if}
|
|
114
|
+
{@render trailing?.()}
|
|
107
115
|
<span transition:fade={{ duration: 150 }}>
|
|
108
116
|
<svg
|
|
109
117
|
xmlns="http://www.w3.org/2000/svg"
|
|
@@ -122,6 +130,7 @@
|
|
|
122
130
|
</svg>
|
|
123
131
|
</span>
|
|
124
132
|
{/if}
|
|
133
|
+
{@render children?.()}
|
|
125
134
|
</button>
|
|
126
135
|
{#if expanded && !isCollapsed && subroutes.length > 0}
|
|
127
136
|
<ul class="subnav-list" transition:slide={{ duration: 200, easing: cubicOut }}>
|
|
@@ -143,6 +152,7 @@
|
|
|
143
152
|
class="nav-item {className}"
|
|
144
153
|
class:active
|
|
145
154
|
class:collapsed={isCollapsed}
|
|
155
|
+
data-variant={variant}
|
|
146
156
|
onclick={handleClick}
|
|
147
157
|
title={isCollapsed ? label : null}
|
|
148
158
|
>
|
|
@@ -295,4 +305,44 @@
|
|
|
295
305
|
font-weight: 500;
|
|
296
306
|
background-color: color-mix(in srgb, var(--color-base0D) 15%, transparent);
|
|
297
307
|
}
|
|
308
|
+
|
|
309
|
+
/* Variants */
|
|
310
|
+
.nav-item[data-variant="warning"] {
|
|
311
|
+
color: var(--color-base0A);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.nav-item[data-variant="warning"]:hover {
|
|
315
|
+
color: var(--color-base0A);
|
|
316
|
+
background-color: color-mix(in srgb, var(--color-base0A) 10%, transparent);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.nav-item[data-variant="warning"] :global(svg) {
|
|
320
|
+
color: var(--color-base0A);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.nav-item[data-variant="danger"] {
|
|
324
|
+
color: var(--color-base08);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.nav-item[data-variant="danger"]:hover {
|
|
328
|
+
color: var(--color-base08);
|
|
329
|
+
background-color: color-mix(in srgb, var(--color-base08) 10%, transparent);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.nav-item[data-variant="danger"] :global(svg) {
|
|
333
|
+
color: var(--color-base08);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.nav-item[data-variant="success"] {
|
|
337
|
+
color: var(--color-base0B);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.nav-item[data-variant="success"]:hover {
|
|
341
|
+
color: var(--color-base0B);
|
|
342
|
+
background-color: color-mix(in srgb, var(--color-base0B) 10%, transparent);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.nav-item[data-variant="success"] :global(svg) {
|
|
346
|
+
color: var(--color-base0B);
|
|
347
|
+
}
|
|
298
348
|
</style>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component LinkCard
|
|
3
|
+
|
|
4
|
+
A clickable card for navigation, commonly used in dashboards for quick-nav sections.
|
|
5
|
+
|
|
6
|
+
@example Basic
|
|
7
|
+
<LinkCard href="/services" label="Ops Hub" />
|
|
8
|
+
|
|
9
|
+
@example With trailing icon
|
|
10
|
+
<LinkCard href="/pm" label="PM Board">
|
|
11
|
+
{#snippet trailing()}
|
|
12
|
+
<ArrowRight size={14} />
|
|
13
|
+
{/snippet}
|
|
14
|
+
</LinkCard>
|
|
15
|
+
|
|
16
|
+
@example Disabled
|
|
17
|
+
<LinkCard href="/admin" label="Admin Panel" disabled />
|
|
18
|
+
-->
|
|
19
|
+
<script>
|
|
20
|
+
let {
|
|
21
|
+
href,
|
|
22
|
+
label,
|
|
23
|
+
disabled = false,
|
|
24
|
+
class: className = '',
|
|
25
|
+
trailing,
|
|
26
|
+
...rest
|
|
27
|
+
} = $props();
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
{#if disabled}
|
|
31
|
+
<div class="link-card link-card-disabled {className}" {...rest}>
|
|
32
|
+
<span class="link-card-label">{label}</span>
|
|
33
|
+
{#if trailing}
|
|
34
|
+
<span class="link-card-trailing">
|
|
35
|
+
{@render trailing()}
|
|
36
|
+
</span>
|
|
37
|
+
{/if}
|
|
38
|
+
</div>
|
|
39
|
+
{:else}
|
|
40
|
+
<a {href} class="link-card {className}" {...rest}>
|
|
41
|
+
<span class="link-card-label">{label}</span>
|
|
42
|
+
{#if trailing}
|
|
43
|
+
<span class="link-card-trailing">
|
|
44
|
+
{@render trailing()}
|
|
45
|
+
</span>
|
|
46
|
+
{/if}
|
|
47
|
+
</a>
|
|
48
|
+
{/if}
|
|
49
|
+
|
|
50
|
+
<style>
|
|
51
|
+
.link-card {
|
|
52
|
+
display: flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
justify-content: space-between;
|
|
55
|
+
width: 100%;
|
|
56
|
+
box-sizing: border-box;
|
|
57
|
+
padding: var(--space-3) var(--space-4);
|
|
58
|
+
border-radius: var(--radius-lg);
|
|
59
|
+
background: var(--color-base01);
|
|
60
|
+
border: 1px solid var(--color-base02);
|
|
61
|
+
text-decoration: none;
|
|
62
|
+
transition: border-color 0.2s ease, background-color 0.2s ease;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.link-card:hover {
|
|
66
|
+
border-color: var(--color-base03);
|
|
67
|
+
background: var(--color-base02);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.link-card:hover .link-card-trailing {
|
|
71
|
+
color: var(--color-base06);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.link-card-disabled {
|
|
75
|
+
opacity: 0.5;
|
|
76
|
+
cursor: not-allowed;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.link-card-disabled:hover {
|
|
80
|
+
border-color: var(--color-base02);
|
|
81
|
+
background: var(--color-base01);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.link-card-label {
|
|
85
|
+
font-size: var(--text-sm);
|
|
86
|
+
font-weight: 500;
|
|
87
|
+
color: var(--color-base05);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.link-card-trailing {
|
|
91
|
+
color: var(--color-base04);
|
|
92
|
+
transition: color 0.2s ease;
|
|
93
|
+
}
|
|
94
|
+
</style>
|
package/src/index.js
CHANGED
|
@@ -33,6 +33,7 @@ export { default as Avatar } from './components/primitives/Avatar.svelte';
|
|
|
33
33
|
export { default as Card } from './components/primitives/Card.svelte';
|
|
34
34
|
export { default as Stat } from './components/primitives/Stat.svelte';
|
|
35
35
|
export { default as Link } from './components/primitives/Link.svelte';
|
|
36
|
+
export { default as LinkCard } from './components/primitives/LinkCard.svelte';
|
|
36
37
|
export { default as LazyImage } from './components/primitives/LazyImage.svelte';
|
|
37
38
|
|
|
38
39
|
// ============================================
|