@miozu/jera 0.3.0 → 0.4.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.
Files changed (72) hide show
  1. package/CLAUDE.md +350 -59
  2. package/README.md +30 -22
  3. package/llms.txt +37 -4
  4. package/package.json +12 -2
  5. package/src/components/docs/CodeBlock.svelte +203 -0
  6. package/src/components/docs/DocSection.svelte +120 -0
  7. package/src/components/docs/PropsTable.svelte +136 -0
  8. package/src/components/docs/SplitPane.svelte +98 -0
  9. package/src/components/docs/index.js +14 -0
  10. package/src/components/feedback/Alert.svelte +234 -0
  11. package/src/components/feedback/EmptyState.svelte +6 -6
  12. package/src/components/feedback/ProgressBar.svelte +8 -8
  13. package/src/components/feedback/Skeleton.svelte +4 -4
  14. package/src/components/feedback/Spinner.svelte +1 -1
  15. package/src/components/feedback/Toast.svelte +137 -173
  16. package/src/components/forms/Checkbox.svelte +10 -10
  17. package/src/components/forms/Dropzone.svelte +14 -14
  18. package/src/components/forms/FileUpload.svelte +16 -16
  19. package/src/components/forms/IconInput.svelte +4 -4
  20. package/src/components/forms/Input.svelte +14 -14
  21. package/src/components/forms/NumberInput.svelte +13 -13
  22. package/src/components/forms/PinInput.svelte +8 -8
  23. package/src/components/forms/Radio.svelte +8 -8
  24. package/src/components/forms/RangeSlider.svelte +12 -12
  25. package/src/components/forms/SearchInput.svelte +10 -10
  26. package/src/components/forms/Select.svelte +156 -158
  27. package/src/components/forms/Switch.svelte +4 -4
  28. package/src/components/forms/Textarea.svelte +9 -9
  29. package/src/components/navigation/Accordion.svelte +1 -1
  30. package/src/components/navigation/AccordionItem.svelte +6 -6
  31. package/src/components/navigation/NavigationContainer.svelte +344 -0
  32. package/src/components/navigation/Sidebar.svelte +334 -0
  33. package/src/components/navigation/SidebarAccountGroup.svelte +495 -0
  34. package/src/components/navigation/SidebarAccountItem.svelte +492 -0
  35. package/src/components/navigation/SidebarGroup.svelte +230 -0
  36. package/src/components/navigation/SidebarGroupSwitcher.svelte +262 -0
  37. package/src/components/navigation/SidebarItem.svelte +210 -0
  38. package/src/components/navigation/SidebarNavigationItem.svelte +470 -0
  39. package/src/components/navigation/SidebarPopover.svelte +145 -0
  40. package/src/components/navigation/SidebarSearch.svelte +236 -0
  41. package/src/components/navigation/SidebarSection.svelte +158 -0
  42. package/src/components/navigation/SidebarToggle.svelte +86 -0
  43. package/src/components/navigation/Tabs.svelte +18 -18
  44. package/src/components/navigation/WorkspaceMenu.svelte +416 -0
  45. package/src/components/navigation/blocks/NavigationAccountGroup.svelte +396 -0
  46. package/src/components/navigation/blocks/NavigationCustomBlock.svelte +74 -0
  47. package/src/components/navigation/blocks/NavigationGroupSwitcher.svelte +277 -0
  48. package/src/components/navigation/blocks/NavigationSearch.svelte +300 -0
  49. package/src/components/navigation/blocks/NavigationSection.svelte +230 -0
  50. package/src/components/navigation/index.js +22 -0
  51. package/src/components/overlays/ConfirmDialog.svelte +18 -18
  52. package/src/components/overlays/Dropdown.svelte +2 -2
  53. package/src/components/overlays/DropdownDivider.svelte +1 -1
  54. package/src/components/overlays/DropdownItem.svelte +5 -5
  55. package/src/components/overlays/Modal.svelte +13 -13
  56. package/src/components/overlays/Popover.svelte +3 -3
  57. package/src/components/primitives/Avatar.svelte +12 -12
  58. package/src/components/primitives/Badge.svelte +7 -7
  59. package/src/components/primitives/Button.svelte +126 -174
  60. package/src/components/primitives/Card.svelte +15 -15
  61. package/src/components/primitives/Divider.svelte +3 -3
  62. package/src/components/primitives/LazyImage.svelte +1 -1
  63. package/src/components/primitives/Link.svelte +2 -2
  64. package/src/components/primitives/Stat.svelte +197 -0
  65. package/src/components/primitives/StatusBadge.svelte +24 -24
  66. package/src/index.js +62 -7
  67. package/src/tokens/colors.css +96 -128
  68. package/src/utils/highlighter.js +124 -0
  69. package/src/utils/index.js +7 -2
  70. package/src/utils/navigation.svelte.js +423 -0
  71. package/src/utils/reactive.svelte.js +126 -37
  72. package/src/utils/sidebar.svelte.js +211 -0
package/CLAUDE.md CHANGED
@@ -28,7 +28,8 @@ jera/
28
28
  │ ├── forms/ # Input, Select, Checkbox, Switch
29
29
  │ ├── feedback/ # Toast, Skeleton, ProgressBar, Spinner
30
30
  │ ├── overlays/ # Modal, Popover
31
- └── navigation/ # Tabs, Accordion, AccordionItem
31
+ ├── navigation/ # Tabs, Accordion, Sidebar
32
+ │ └── docs/ # CodeBlock, PropsTable, SplitPane, DocSection
32
33
  ├── llms.txt # AI documentation index
33
34
  ├── CLAUDE.md # This file
34
35
  └── package.json
@@ -83,47 +84,49 @@ export const componentStyles = cv({
83
84
 
84
85
  ### Naming Conventions
85
86
  - Components: PascalCase (`Button.svelte`)
86
- - Utilities: camelCase (`createThemeContext`)
87
+ - Utilities: camelCase (`getTheme`)
87
88
  - CSS tokens: kebab-case (`--color-primary`)
88
89
  - Actions: camelCase (`clickOutside`)
89
90
 
90
91
  ---
91
92
 
92
- ## Miozu Color System
93
-
94
- ### Base Colors (Grayscale)
95
- | Token | Hex | Usage |
96
- |-------|-----|-------|
97
- | `--base0` | #232733 | Darkest background |
98
- | `--base1` | #2C3040 | Default dark bg |
99
- | `--base2` | #3E4359 | Selection, hover |
100
- | `--base3` | #565E78 | Comments, subtle |
101
- | `--base4` | #737E99 | Muted text |
102
- | `--base5` | #D0D2DB | Default text |
103
- | `--base6` | #F3F4F7 | Light text |
104
- | `--base7` | #FAFDFB | Lightest/white |
105
-
106
- ### Accent Colors
107
- | Token | Hex | Usage |
108
- |-------|-----|-------|
109
- | `--magenta` | #C974E6 | Primary brand |
110
- | `--blue` | #83D2FC | Info, links |
111
- | `--green` | #6DD672 | Success |
112
- | `--yellow` | #E8D176 | Warning |
113
- | `--red` | #EB3137 | Error |
114
- | `--cyan` | #40FFE2 | Accent |
115
- | `--orange` | #FF9837 | Attention |
116
- | `--peach` | #FF9982 | Warm accent |
93
+ ## Miozu Base16 Color System
94
+
95
+ Uses standard Base16 naming: `base00`-`base0F` (hex digits, NOT decimal).
96
+
97
+ ### Grayscale (base00-base07)
98
+ | Token | Dark Theme | Light Theme | Usage |
99
+ |-------|------------|-------------|-------|
100
+ | `--color-base00` | #0f1419 | #ffffff | Primary background |
101
+ | `--color-base01` | #1a1f26 | #f8f9fa | Surface/card |
102
+ | `--color-base02` | #242a33 | #f1f3f5 | Selection/hover |
103
+ | `--color-base03` | #4a5568 | #adb5bd | Muted/disabled |
104
+ | `--color-base04` | #a0aec0 | #6c757d | Secondary text |
105
+ | `--color-base05` | #e2e8f0 | #212529 | Primary text |
106
+ | `--color-base06` | #f7fafc | #1a1d20 | High emphasis |
107
+ | `--color-base07` | #ffffff | #0d0f10 | Maximum contrast |
108
+
109
+ ### Accents (base08-base0F)
110
+ | Token | Color | Usage |
111
+ |-------|-------|-------|
112
+ | `--color-base08` | Red | Error |
113
+ | `--color-base09` | Orange | Warning |
114
+ | `--color-base0A` | Yellow | Highlight |
115
+ | `--color-base0B` | Green | Success |
116
+ | `--color-base0C` | Cyan | Info |
117
+ | `--color-base0D` | Blue | Primary |
118
+ | `--color-base0E` | Purple | Accent |
119
+ | `--color-base0F` | Brown/Orange | Secondary accent |
117
120
 
118
121
  ### Semantic Mappings
119
122
  ```css
120
- --color-bg: var(--base0);
121
- --color-surface: var(--base1);
122
- --color-text: var(--base5);
123
- --color-text-strong: var(--base7);
124
- --color-primary: var(--magenta);
125
- --color-success: var(--green);
126
- --color-error: var(--red);
123
+ --color-bg: var(--color-base00);
124
+ --color-surface: var(--color-base01);
125
+ --color-text: var(--color-base05);
126
+ --color-text-strong: var(--color-base07);
127
+ --color-primary: var(--color-base0D);
128
+ --color-success: var(--color-base0B);
129
+ --color-error: var(--color-base08);
127
130
  ```
128
131
 
129
132
  ---
@@ -241,6 +244,34 @@ Props: `content`, `position` (top/bottom/left/right), `delay` ({show, hide}), `o
241
244
 
242
245
  Props: `orientation` (horizontal/vertical), `thickness`, `spacing`, `children`
243
246
 
247
+ ### Stat
248
+ ```svelte
249
+ <Stat value="42" label="Users" />
250
+ <Stat value="95%" label="Uptime" status="success" />
251
+ <Stat value="75" max={100} label="Progress" showBar />
252
+ <Stat value="12" unit="/15" label="Tasks" status="warning" />
253
+ <Stat value="8" label="Errors" href="/errors" status="error" />
254
+ ```
255
+
256
+ Props: `value`, `label`, `unit`, `secondary`, `status` (success/warning/error/info), `size` (sm/md/lg), `showBar`, `barValue`, `max`, `href`
257
+
258
+ ### Alert
259
+ ```svelte
260
+ <Alert variant="info">This is an informational message.</Alert>
261
+ <Alert variant="warning" title="Warning">Please review your settings.</Alert>
262
+ <Alert variant="error" dismissible onclose={() => showAlert = false}>
263
+ An error occurred.
264
+ </Alert>
265
+ <Alert variant="success">
266
+ {#snippet icon()}
267
+ <CheckIcon size={16} />
268
+ {/snippet}
269
+ Your changes have been saved.
270
+ </Alert>
271
+ ```
272
+
273
+ Props: `variant` (info/success/warning/error), `title`, `dismissible`, `size` (sm/md/lg), `onclose`, `icon` (snippet), `actions` (snippet)
274
+
244
275
  ### Avatar
245
276
  ```svelte
246
277
  <Avatar src="/user.jpg" alt="John Doe" />
@@ -316,6 +347,72 @@ Props: `tabs` (array), `active`, `variant` (default/underline/pills), `size` (sm
316
347
  Accordion props: `expanded` (array of ids), `multiple`
317
348
  AccordionItem props: `id`, `title`, `disabled`
318
349
 
350
+ ### CodeBlock
351
+ ```svelte
352
+ <script>
353
+ import { CodeBlock } from '@miozu/jera';
354
+ </script>
355
+
356
+ <CodeBlock
357
+ code={`const greeting = "Hello";`}
358
+ lang="javascript"
359
+ filename="example.js"
360
+ showLineNumbers
361
+ />
362
+ ```
363
+
364
+ Props: `code`, `lang` (default: javascript), `filename`, `showLineNumbers`, `class`
365
+
366
+ **Requires:** `shiki` package for syntax highlighting.
367
+
368
+ ### PropsTable
369
+ ```svelte
370
+ <script>
371
+ import { PropsTable } from '@miozu/jera';
372
+ </script>
373
+
374
+ <PropsTable props={[
375
+ { name: 'variant', type: 'string', default: '"default"', description: 'Visual style' },
376
+ { name: 'onclick', type: 'function', required: true, description: 'Click handler' }
377
+ ]} />
378
+ ```
379
+
380
+ Props: `props` (array of PropDef), `class`
381
+
382
+ PropDef: `{ name, type, default?, description, required? }`
383
+
384
+ ### SplitPane
385
+ ```svelte
386
+ <script>
387
+ import { SplitPane, CodeBlock } from '@miozu/jera';
388
+ </script>
389
+
390
+ <SplitPane ratio="1:1" gap="2rem" stickyRight>
391
+ {#snippet left()}
392
+ <h2>Description</h2>
393
+ <p>Explanation of the feature...</p>
394
+ {/snippet}
395
+ {#snippet right()}
396
+ <CodeBlock code={exampleCode} lang="javascript" />
397
+ {/snippet}
398
+ </SplitPane>
399
+ ```
400
+
401
+ Props: `left` (snippet), `right` (snippet), `ratio` (e.g., '1:1', '2:1'), `gap`, `minHeight`, `stickyRight`, `class`
402
+
403
+ ### DocSection
404
+ ```svelte
405
+ <script>
406
+ import { DocSection, CodeBlock } from '@miozu/jera';
407
+ </script>
408
+
409
+ <DocSection id="installation" title="Installation" description="How to install">
410
+ <CodeBlock code="npm install @miozu/jera" lang="bash" />
411
+ </DocSection>
412
+ ```
413
+
414
+ Props: `id`, `title`, `description`, `level` (2-6), `showAnchor`, `children`, `class`
415
+
319
416
  ---
320
417
 
321
418
  ## Actions Reference
@@ -358,32 +455,226 @@ AccordionItem props: `id`, `title`, `disabled`
358
455
  2. Use semantic naming
359
456
  3. Add light theme variant if applicable
360
457
 
361
- ### Theme Switching
458
+ ### Theme Switching (Single Source of Truth Pattern)
459
+
460
+ jera uses a singleton pattern for global theme state. Storage key: `miozu-theme`.
461
+
462
+ #### SvelteKit Execution Order (from source code analysis)
463
+
464
+ Understanding when code runs helps choose the right pattern:
465
+
466
+ | Phase | File | Server | Client | Notes |
467
+ |-------|------|--------|--------|-------|
468
+ | 1 | +layout.server.js | ✓ | - | Server-only data |
469
+ | 2 | +layout.js | ✓ | ✓ | Universal load, runs on EVERY navigation |
470
+ | 3 | +layout.svelte `<script>` | ✓ | ✓ | Component script, runs once on mount |
471
+ | 4 | +layout.svelte `onMount()` | - | ✓ | Client-only, after hydration |
472
+
473
+ **Key insight from SvelteKit source (`client.js:684`):**
474
+ ```javascript
475
+ data = { ...data, ...node.data }; // Parent data cascades to children
476
+ ```
477
+
478
+ #### Recommended Pattern: +layout.svelte (for singletons like theme)
479
+
480
+ For global singleton state that doesn't need server context:
481
+
482
+ ```svelte
483
+ <!-- +layout.svelte - Initialize ONCE, pass via props -->
484
+ <script>
485
+ import { getTheme } from '@miozu/jera';
486
+ import { onMount } from 'svelte';
487
+ import { Sidebar } from '$components';
488
+
489
+ // Call singleton once in root layout
490
+ const themeState = getTheme();
491
+
492
+ onMount(() => {
493
+ themeState.sync(); // Hydrate from DOM (app.html)
494
+ themeState.init(); // Setup media query listener
495
+ });
496
+ </script>
497
+
498
+ <!-- Pass themeState as prop to children -->
499
+ <Sidebar {themeState} />
500
+ ```
501
+
502
+ ```svelte
503
+ <!-- Child component - receives themeState as prop -->
504
+ <script>
505
+ // DON'T call getTheme() here - receive as prop instead
506
+ let { themeState } = $props();
507
+
508
+ function handleToggle() {
509
+ themeState.toggle();
510
+ }
511
+
512
+ let isDark = $derived(themeState.isDark);
513
+ </script>
514
+ ```
515
+
516
+ #### Alternative Pattern: +layout.js (for data-heavy state)
517
+
518
+ For state that needs server context or async initialization:
519
+
362
520
  ```javascript
363
- // System preference detection
364
- const theme = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
365
- document.documentElement.setAttribute('data-theme', theme);
366
-
367
- // Or use ThemeState class
368
- import { createThemeContext } from '@miozu/jera';
369
- const theme = createThemeContext();
370
- theme.init(); // Reads from localStorage/system
371
- theme.toggle();
372
- ```
373
-
374
- ### Supported Theme Selectors
375
- jera supports multiple theme attribute values for flexibility:
376
-
377
- | Theme | Selectors |
378
- |-------|-----------|
379
- | Dark | `[data-theme="dark"]`, `[data-theme="miozu-dark"]`, `.dark` |
380
- | Light | `[data-theme="light"]`, `[data-theme="miozu-light"]`, `.light` |
381
- | High Contrast | `[data-theme="high-contrast"]` |
382
-
383
- This allows jera to integrate with any theming system. For example:
384
- - Selify apps use `miozu-dark` / `miozu-light`
385
- - Generic apps can use `dark` / `light`
386
- - Class-based theming via `.dark` / `.light`
521
+ // +layout.js - Universal load
522
+ export const load = async ({ data, fetch }) => {
523
+ // Can fetch data, access parent(), etc.
524
+ const someData = await fetch('/api/data').then(r => r.json());
525
+
526
+ return {
527
+ ...data,
528
+ someData
529
+ };
530
+ };
531
+ ```
532
+
533
+ **When to use which:**
534
+
535
+ | Use +layout.svelte | Use +layout.js |
536
+ |-------------------|----------------|
537
+ | Singletons (theme, toast) | Fetched data |
538
+ | Client-only initialization | Async operations |
539
+ | No server context needed | Needs `parent()` data |
540
+ | Runs once per mount | Needs invalidation support |
541
+
542
+ **ThemeState API:**
543
+ ```javascript
544
+ themeState.toggle(); // Toggle dark/light
545
+ themeState.set('dark'); // Set to dark
546
+ themeState.set('light'); // Set to light
547
+ themeState.set('system'); // Follow system preference
548
+
549
+ // Reactive properties
550
+ themeState.current; // 'light' | 'dark' | 'system'
551
+ themeState.resolved; // 'light' | 'dark' (actual resolved value)
552
+ themeState.dataTheme; // 'miozu-light' | 'miozu-dark'
553
+ themeState.isDark; // boolean
554
+ themeState.isLight; // boolean
555
+ ```
556
+
557
+ **Why pass as props instead of calling `getTheme()` everywhere?**
558
+ 1. **Explicit data flow** - You see what state each component depends on
559
+ 2. **Better testing** - Can inject mock state easily
560
+ 3. **Single initialization** - Clear where state originates
561
+ 4. **Efficiency** - Singleton runs once, not on every navigation
562
+
563
+ ### Theme Data Attributes
564
+
565
+ | Theme | data-theme value |
566
+ |-------|------------------|
567
+ | Dark | `miozu-dark` |
568
+ | Light | `miozu-light` |
569
+
570
+ CSS selectors should use:
571
+ ```css
572
+ [data-theme='miozu-dark'] { /* dark styles */ }
573
+ [data-theme='miozu-light'] { /* light styles */ }
574
+ ```
575
+
576
+ ### Migration Guide: Custom Theme → Jera Singleton
577
+
578
+ If you have a custom `theme.svelte.js` or similar, follow these steps:
579
+
580
+ #### Step 1: Update app.html
581
+ Replace custom theme script with unified storage key:
582
+ ```javascript
583
+ // CRITICAL: Prevent FOUC - runs before any CSS
584
+ (function () {
585
+ try {
586
+ let pref = localStorage.getItem('miozu-theme');
587
+
588
+ // Migrate from old keys if needed
589
+ if (!pref || !['light', 'dark', 'system'].includes(pref)) {
590
+ const oldTheme = localStorage.getItem('theme'); // or your old key
591
+ if (oldTheme === 'miozu-dark' || oldTheme === 'dark') pref = 'dark';
592
+ else if (oldTheme === 'miozu-light' || oldTheme === 'light') pref = 'light';
593
+ }
594
+
595
+ // Resolve theme
596
+ let theme;
597
+ if (pref === 'light') theme = 'miozu-light';
598
+ else if (pref === 'dark') theme = 'miozu-dark';
599
+ else {
600
+ theme = window.matchMedia?.('(prefers-color-scheme: dark)').matches
601
+ ? 'miozu-dark' : 'miozu-light';
602
+ pref = 'system';
603
+ }
604
+
605
+ // Apply and persist
606
+ document.documentElement.setAttribute('data-theme', theme);
607
+ document.documentElement.style.colorScheme = theme === 'miozu-dark' ? 'dark' : 'light';
608
+ localStorage.setItem('miozu-theme', pref);
609
+ document.cookie = 'miozu-theme=' + pref + '; path=/; max-age=31536000; SameSite=Lax';
610
+
611
+ // Clean up old keys
612
+ localStorage.removeItem('theme');
613
+ } catch (e) {
614
+ document.documentElement.setAttribute('data-theme', 'miozu-dark');
615
+ }
616
+ })();
617
+ ```
618
+
619
+ #### Step 2: Update +layout.svelte
620
+ ```svelte
621
+ <script>
622
+ import { getTheme } from '@miozu/jera';
623
+ import { onMount } from 'svelte';
624
+
625
+ const themeState = getTheme();
626
+
627
+ onMount(() => {
628
+ themeState.sync();
629
+ themeState.init();
630
+ });
631
+ </script>
632
+ ```
633
+
634
+ #### Step 3: Update +layout.js
635
+ Remove any theme initialization - it should NOT be in +layout.js:
636
+ ```javascript
637
+ // REMOVE these lines:
638
+ // import { ThemeReactiveState } from '$lib/reactiveStates/theme.svelte.js';
639
+ // const themeState = new ThemeReactiveState();
640
+ // return { themeState, ... };
641
+ ```
642
+
643
+ #### Step 4: Update components using theme
644
+ ```svelte
645
+ <script>
646
+ // OLD (remove):
647
+ // import { getThemeState } from '$lib/reactiveStates/theme.svelte.js';
648
+ // const themeState = getThemeState();
649
+
650
+ // NEW:
651
+ import { getTheme } from '@miozu/jera';
652
+ const themeState = getTheme();
653
+
654
+ let isDark = $derived(themeState.isDark);
655
+ </script>
656
+ ```
657
+
658
+ #### Step 5: Delete old theme file
659
+ ```bash
660
+ rm src/lib/reactiveStates/theme.svelte.js
661
+ ```
662
+
663
+ #### Step 6: Verify no old imports remain
664
+ ```bash
665
+ grep -r "theme.svelte.js\|getThemeState\|ThemeReactiveState" src/
666
+ # Should return no matches
667
+ ```
668
+
669
+ ### Apps Using Jera Theme (Proof of Concept)
670
+
671
+ | App | Status | Storage Key |
672
+ |-----|--------|-------------|
673
+ | dash.selify.ai | ✓ Migrated | `miozu-theme` |
674
+ | admin.selify.ai | ✓ Migrated | `miozu-theme` |
675
+ | miozu.com | ✓ Migrated | `miozu-theme` |
676
+
677
+ All three apps share the same theme preference via unified `miozu-theme` localStorage key.
387
678
 
388
679
  ---
389
680
 
package/README.md CHANGED
@@ -48,18 +48,20 @@ Import tokens for consistent styling:
48
48
  @import '@miozu/jera/tokens/effects';
49
49
  ```
50
50
 
51
- ### Miozu Color Palette
52
-
53
- | Base | Hex | Accent | Hex |
54
- |------|-----|--------|-----|
55
- | base0 | `#232733` | magenta | `#C974E6` |
56
- | base1 | `#2C3040` | blue | `#83D2FC` |
57
- | base2 | `#3E4359` | green | `#6DD672` |
58
- | base3 | `#565E78` | yellow | `#E8D176` |
59
- | base4 | `#737E99` | red | `#EB3137` |
60
- | base5 | `#D0D2DB` | cyan | `#40FFE2` |
61
- | base6 | `#F3F4F7` | orange | `#FF9837` |
62
- | base7 | `#FAFDFB` | peach | `#FF9982` |
51
+ ### Miozu Base16 Color Palette
52
+
53
+ Standard Base16 naming: `base00`-`base0F` (hex digits).
54
+
55
+ | Grayscale | Usage | Accents | Usage |
56
+ |-----------|-------|---------|-------|
57
+ | `base00` | Background | `base08` | Error (Red) |
58
+ | `base01` | Surface | `base09` | Warning (Orange) |
59
+ | `base02` | Selection | `base0A` | Highlight (Yellow) |
60
+ | `base03` | Muted | `base0B` | Success (Green) |
61
+ | `base04` | Secondary text | `base0C` | Info (Cyan) |
62
+ | `base05` | Primary text | `base0D` | Primary (Blue) |
63
+ | `base06` | High emphasis | `base0E` | Accent (Purple) |
64
+ | `base07` | Maximum contrast | `base0F` | Secondary accent |
63
65
 
64
66
  ## Components
65
67
 
@@ -172,23 +174,29 @@ button({ variant: 'secondary' }); // => "inline-flex items-center bg-surface h-1
172
174
 
173
175
  ## Theming
174
176
 
175
- Dark theme is default. Switch themes with `data-theme` attribute:
177
+ Dark theme is default. Uses singleton pattern with `miozu-theme` storage key.
176
178
 
177
- ```html
178
- <html data-theme="dark"> <!-- Dark (default) -->
179
- <html data-theme="light"> <!-- Light -->
180
- ```
179
+ ```javascript
180
+ // In root +layout.svelte
181
+ import { getTheme } from '@miozu/jera';
182
+ import { onMount } from 'svelte';
181
183
 
182
- Or use ThemeState:
184
+ const theme = getTheme();
185
+ onMount(() => theme.init());
186
+ ```
183
187
 
184
188
  ```javascript
185
- import { createThemeContext } from '@miozu/jera';
189
+ // Anywhere in your app
190
+ import { getTheme } from '@miozu/jera';
186
191
 
187
- const theme = createThemeContext();
188
- theme.init(); // Reads from localStorage/system preference
189
- theme.toggle(); // Switch between light/dark
192
+ const theme = getTheme();
193
+ theme.toggle(); // Switch between light/dark
194
+ theme.set('system'); // Follow system preference
195
+ theme.isDark; // boolean reactive property
190
196
  ```
191
197
 
198
+ Data-theme values: `miozu-dark` (default) or `miozu-light`.
199
+
192
200
  ## AI-First Design
193
201
 
194
202
  This library includes AI-optimized documentation:
package/llms.txt CHANGED
@@ -56,9 +56,42 @@ const button = cv({
56
56
  // Usage: button({ variant: 'secondary', size: 'sm' })
57
57
  ```
58
58
 
59
- ### Theme Usage Pattern
59
+ ### Theme Usage Pattern (Single Source of Truth)
60
+
61
+ **SvelteKit Execution Order:**
62
+ 1. +layout.server.js (server only)
63
+ 2. +layout.js (server + client, runs on EVERY navigation)
64
+ 3. +layout.svelte script (runs once on mount)
65
+ 4. onMount() (client only, after hydration)
66
+
67
+ **Recommended: Initialize in +layout.svelte** (for singletons):
68
+ ```svelte
69
+ <!-- +layout.svelte -->
70
+ <script>
71
+ import { getTheme } from '@miozu/jera';
72
+ import { onMount } from 'svelte';
73
+
74
+ const themeState = getTheme(); // Call ONCE
75
+ onMount(() => {
76
+ themeState.sync(); // Hydrate from DOM
77
+ themeState.init(); // Setup media query listener
78
+ });
79
+ </script>
80
+
81
+ <!-- Pass to children as props -->
82
+ <Sidebar {themeState} />
83
+ ```
84
+
85
+ **Child components receive as prop:**
60
86
  ```javascript
61
- import { ThemeState, createThemeContext } from '@miozu/jera';
62
- const theme = createThemeContext(); // In root layout
63
- theme.toggle(); // Switch between light/dark
87
+ let { themeState } = $props(); // DON'T call getTheme()
88
+ themeState.toggle(); // Switch light/dark
89
+ themeState.isDark; // Reactive boolean
64
90
  ```
91
+
92
+ **Why +layout.svelte over +layout.js for singletons?**
93
+ - Runs once per mount (not on every navigation)
94
+ - onMount clearly separates SSR from client code
95
+ - More efficient for global state that doesn't change
96
+
97
+ Storage key: `miozu-theme`. Data-theme values: `miozu-dark` | `miozu-light`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miozu/jera",
3
- "version": "0.3.0",
3
+ "version": "0.4.2",
4
4
  "description": "Minimal, reactive component library for Svelte 5",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -26,7 +26,17 @@
26
26
  "CLAUDE.md"
27
27
  ],
28
28
  "peerDependencies": {
29
- "svelte": "^5.0.0"
29
+ "svelte": "^5.0.0",
30
+ "shiki": "^3.0.0",
31
+ "@lucide/svelte": "^0.500.0"
32
+ },
33
+ "peerDependenciesMeta": {
34
+ "shiki": {
35
+ "optional": true
36
+ },
37
+ "@lucide/svelte": {
38
+ "optional": true
39
+ }
30
40
  },
31
41
  "devDependencies": {
32
42
  "svelte": "^5.41.0"