@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 - AI Context File
1
+ # @miozu/jera - Component Library
2
2
 
3
- **Package:** @miozu/jera
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
- ## Architecture Overview
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, used by all styles
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 for AI assistants
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
- │ ├── index.js # Main exports
38
- │ ├── tokens/ # Design tokens (CSS custom properties)
39
- ├── index.css # Bundle all tokens
40
- │ ├── colors.css # Miozu Base16 palette
41
- │ │ ├── spacing.css # 4px-based scale
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
- <element class={computedClass} {disabled} {...rest}>
87
- {@render children?.()}
88
- </element>
89
- ```
31
+ Uses Base16 naming: `base00`-`base0F` (hex digits).
90
32
 
91
- ### Class Variants (cv) Pattern
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
- ### Naming Conventions
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
- ## Component API Reference
39
+ **Input:** `bind:value`, `type`, `placeholder`, `disabled`, `required`
158
40
 
159
- ### Button
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
- ### Input
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
- ### Select
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
- ### Badge
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
- ### Toast
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
- <!-- Usage anywhere -->
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
- ### Modal
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
- <Button onclick={() => showModal = true}>Open Modal</Button>
55
+ ## Theme Management
236
56
 
237
- <Modal bind:open={showModal} title="Confirm Action" variant="danger">
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
- Props: `open`, `title`, `size` (sm/md/lg/xl), `variant` (default/danger/warning/success/info), `closeOnBackdrop`, `closeOnEscape`, `showClose`, `children`, `footer`, `icon`, `onclose`
59
+ **Full reference:** `docs/ai-context/theme-management.md`
247
60
 
248
- ### Popover
249
61
  ```svelte
62
+ <!-- +layout.svelte -->
250
63
  <script>
251
- import { Popover, Button } from '@miozu/jera';
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
- Props: `value`, `max`, `size` (sm/md/lg), `variant` (primary/success/warning/error/info), `showLabel`, `label`, `indeterminate`
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
- Props: `tabs` (array), `active`, `variant` (default/underline/pills), `size` (sm/md/lg), `fullWidth`, `onchange`
72
+ **Full reference:** `docs/ai-context/svelte5-patterns.md`
353
73
 
354
- ### Accordion
355
74
  ```svelte
356
75
  <script>
357
- import { Accordion, AccordionItem } from '@miozu/jera';
76
+ let { variant = 'default', class: className = '', ...rest } = $props();
77
+ const classes = $derived(/* ... */);
358
78
  </script>
359
79
 
360
- <Accordion multiple>
361
- <AccordionItem id="section1" title="Section 1">
362
- Content for section 1
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
- Accordion props: `expanded` (array of ids), `multiple`
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
- import { createActiveChecker } from '@miozu/jera';
464
- import { page } from '$app/stores';
465
-
466
- // Create checker with current pathname
467
- const isActive = createActiveChecker(() => $page.url.pathname);
468
-
469
- // Use in components
470
- <LeftBarItem active={isActive('/dashboard')} /> // prefix match
471
- <LeftBarItem active={isActive('/dashboard', 'exact')} /> // exact match
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
- Props: `id`, `title`, `description`, `level` (2-6), `showAnchor`, `children`, `class`
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. **Always use Svelte 5 runes** - No legacy `$:`, `export let`, stores
853
- 2. **Single $props() call** - Destructure all props in one call
854
- 3. **Use cv() for variants** - Don't hardcode conditional classes
855
- 4. **Semantic colors** - Use `--color-*` tokens, not raw `--base*`
856
- 5. **Accessibility first** - Include ARIA attributes, keyboard support
857
- 6. **No TypeScript** - Pure JavaScript with JSDoc for documentation
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.0",
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>
@@ -73,7 +73,7 @@
73
73
  background: transparent;
74
74
  border: none;
75
75
  cursor: pointer;
76
- font: inherit;
76
+ font-family: inherit;
77
77
  }
78
78
 
79
79
  .collapse-toggle:hover {
@@ -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
  // ============================================