@miozu/jera 0.0.2 → 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 (82) hide show
  1. package/CLAUDE.md +734 -0
  2. package/README.md +219 -1
  3. package/llms.txt +97 -0
  4. package/package.json +54 -14
  5. package/src/actions/index.js +375 -0
  6. package/src/components/docs/CodeBlock.svelte +203 -0
  7. package/src/components/docs/DocSection.svelte +120 -0
  8. package/src/components/docs/PropsTable.svelte +136 -0
  9. package/src/components/docs/SplitPane.svelte +98 -0
  10. package/src/components/docs/index.js +14 -0
  11. package/src/components/feedback/Alert.svelte +234 -0
  12. package/src/components/feedback/EmptyState.svelte +179 -0
  13. package/src/components/feedback/ProgressBar.svelte +116 -0
  14. package/src/components/feedback/Skeleton.svelte +107 -0
  15. package/src/components/feedback/Spinner.svelte +77 -0
  16. package/src/components/feedback/Toast.svelte +261 -0
  17. package/src/components/forms/Checkbox.svelte +147 -0
  18. package/src/components/forms/Dropzone.svelte +248 -0
  19. package/src/components/forms/FileUpload.svelte +266 -0
  20. package/src/components/forms/IconInput.svelte +184 -0
  21. package/src/components/forms/Input.svelte +121 -0
  22. package/src/components/forms/NumberInput.svelte +225 -0
  23. package/src/components/forms/PinInput.svelte +169 -0
  24. package/src/components/forms/Radio.svelte +143 -0
  25. package/src/components/forms/RadioGroup.svelte +62 -0
  26. package/src/components/forms/RangeSlider.svelte +212 -0
  27. package/src/components/forms/SearchInput.svelte +175 -0
  28. package/src/components/forms/Select.svelte +324 -0
  29. package/src/components/forms/Switch.svelte +159 -0
  30. package/src/components/forms/Textarea.svelte +122 -0
  31. package/src/components/navigation/Accordion.svelte +65 -0
  32. package/src/components/navigation/AccordionItem.svelte +146 -0
  33. package/src/components/navigation/NavigationContainer.svelte +344 -0
  34. package/src/components/navigation/Sidebar.svelte +334 -0
  35. package/src/components/navigation/SidebarAccountGroup.svelte +495 -0
  36. package/src/components/navigation/SidebarAccountItem.svelte +492 -0
  37. package/src/components/navigation/SidebarGroup.svelte +230 -0
  38. package/src/components/navigation/SidebarGroupSwitcher.svelte +262 -0
  39. package/src/components/navigation/SidebarItem.svelte +210 -0
  40. package/src/components/navigation/SidebarNavigationItem.svelte +470 -0
  41. package/src/components/navigation/SidebarPopover.svelte +145 -0
  42. package/src/components/navigation/SidebarSearch.svelte +236 -0
  43. package/src/components/navigation/SidebarSection.svelte +158 -0
  44. package/src/components/navigation/SidebarToggle.svelte +86 -0
  45. package/src/components/navigation/Tabs.svelte +239 -0
  46. package/src/components/navigation/WorkspaceMenu.svelte +416 -0
  47. package/src/components/navigation/blocks/NavigationAccountGroup.svelte +396 -0
  48. package/src/components/navigation/blocks/NavigationCustomBlock.svelte +74 -0
  49. package/src/components/navigation/blocks/NavigationGroupSwitcher.svelte +277 -0
  50. package/src/components/navigation/blocks/NavigationSearch.svelte +300 -0
  51. package/src/components/navigation/blocks/NavigationSection.svelte +230 -0
  52. package/src/components/navigation/index.js +22 -0
  53. package/src/components/overlays/ConfirmDialog.svelte +272 -0
  54. package/src/components/overlays/Dropdown.svelte +153 -0
  55. package/src/components/overlays/DropdownDivider.svelte +23 -0
  56. package/src/components/overlays/DropdownItem.svelte +97 -0
  57. package/src/components/overlays/Modal.svelte +232 -0
  58. package/src/components/overlays/Popover.svelte +206 -0
  59. package/src/components/primitives/Avatar.svelte +132 -0
  60. package/src/components/primitives/Badge.svelte +118 -0
  61. package/src/components/primitives/Button.svelte +214 -0
  62. package/src/components/primitives/Card.svelte +104 -0
  63. package/src/components/primitives/Divider.svelte +105 -0
  64. package/src/components/primitives/LazyImage.svelte +104 -0
  65. package/src/components/primitives/Link.svelte +122 -0
  66. package/src/components/primitives/Stat.svelte +197 -0
  67. package/src/components/primitives/StatusBadge.svelte +122 -0
  68. package/src/index.js +183 -0
  69. package/src/tokens/colors.css +157 -0
  70. package/src/tokens/effects.css +128 -0
  71. package/src/tokens/index.css +81 -0
  72. package/src/tokens/spacing.css +49 -0
  73. package/src/tokens/typography.css +79 -0
  74. package/src/utils/cn.svelte.js +175 -0
  75. package/src/utils/highlighter.js +124 -0
  76. package/src/utils/index.js +22 -0
  77. package/src/utils/navigation.svelte.js +423 -0
  78. package/src/utils/reactive.svelte.js +328 -0
  79. package/src/utils/sidebar.svelte.js +211 -0
  80. package/jera.js +0 -135
  81. package/www/components/jera/Input/Input.svelte +0 -63
  82. package/www/components/jera/Input/index.js +0 -1
package/CLAUDE.md ADDED
@@ -0,0 +1,734 @@
1
+ # @miozu/jera - AI Context File
2
+
3
+ **Package:** @miozu/jera
4
+ **Purpose:** Minimal, reactive component library for Svelte 5
5
+ **Author:** Nicholas Glazer <glazer.nicholas@gmail.com>
6
+
7
+ ---
8
+
9
+ ## Project Structure
10
+
11
+ ```
12
+ jera/
13
+ ├── src/
14
+ │ ├── index.js # Main exports
15
+ │ ├── tokens/ # Design tokens (CSS custom properties)
16
+ │ │ ├── index.css # Bundle all tokens
17
+ │ │ ├── colors.css # Miozu Base16 palette
18
+ │ │ ├── spacing.css # 4px-based scale
19
+ │ │ ├── typography.css # Font system
20
+ │ │ └── effects.css # Shadows, radius, transitions
21
+ │ ├── utils/
22
+ │ │ ├── cn.svelte.js # cn(), cv() class utilities
23
+ │ │ └── reactive.svelte.js # ThemeState, reactive helpers
24
+ │ ├── actions/
25
+ │ │ └── index.js # Svelte actions
26
+ │ └── components/
27
+ │ ├── primitives/ # Button, Badge, Divider, Avatar
28
+ │ ├── forms/ # Input, Select, Checkbox, Switch
29
+ │ ├── feedback/ # Toast, Skeleton, ProgressBar, Spinner
30
+ │ ├── overlays/ # Modal, Popover
31
+ │ ├── navigation/ # Tabs, Accordion, Sidebar
32
+ │ └── docs/ # CodeBlock, PropsTable, SplitPane, DocSection
33
+ ├── llms.txt # AI documentation index
34
+ ├── CLAUDE.md # This file
35
+ └── package.json
36
+ ```
37
+
38
+ ---
39
+
40
+ ## Coding Standards
41
+
42
+ ### Svelte 5 Patterns (REQUIRED)
43
+ - Use `$props()` for component props (single call only)
44
+ - Use `$state()` for reactive local state
45
+ - Use `$derived()` for computed values
46
+ - Use `$effect()` sparingly, only for side effects
47
+ - Use `$bindable()` for two-way binding props
48
+
49
+ ### Component Template
50
+ ```svelte
51
+ <script>
52
+ let {
53
+ variant = 'default',
54
+ size = 'md',
55
+ disabled = false,
56
+ class: className = '',
57
+ ...rest
58
+ } = $props();
59
+
60
+ const computedClass = $derived(/* class logic */);
61
+ </script>
62
+
63
+ <element class={computedClass} {disabled} {...rest}>
64
+ {@render children?.()}
65
+ </element>
66
+ ```
67
+
68
+ ### Class Variants (cv) Pattern
69
+ ```javascript
70
+ export const componentStyles = cv({
71
+ base: 'base-classes here',
72
+ variants: {
73
+ variantName: {
74
+ option1: 'classes-for-option1',
75
+ option2: 'classes-for-option2'
76
+ }
77
+ },
78
+ compounds: [
79
+ { condition: { variant: 'x', size: 'y' }, class: 'compound-classes' }
80
+ ],
81
+ defaults: { variantName: 'option1' }
82
+ });
83
+ ```
84
+
85
+ ### Naming Conventions
86
+ - Components: PascalCase (`Button.svelte`)
87
+ - Utilities: camelCase (`getTheme`)
88
+ - CSS tokens: kebab-case (`--color-primary`)
89
+ - Actions: camelCase (`clickOutside`)
90
+
91
+ ---
92
+
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 |
120
+
121
+ ### Semantic Mappings
122
+ ```css
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);
130
+ ```
131
+
132
+ ---
133
+
134
+ ## Component API Reference
135
+
136
+ ### Button
137
+ ```svelte
138
+ <Button
139
+ variant="primary|secondary|ghost|outline|danger|success"
140
+ size="xs|sm|md|lg|xl"
141
+ disabled={boolean}
142
+ loading={boolean}
143
+ fullWidth={boolean}
144
+ href={string} // Renders as <a> if provided
145
+ onclick={function}
146
+ >
147
+ Content
148
+ </Button>
149
+ ```
150
+
151
+ ### Input
152
+ ```svelte
153
+ <Input
154
+ bind:value={string}
155
+ type="text|email|password|number|..."
156
+ placeholder={string}
157
+ disabled={boolean}
158
+ required={boolean}
159
+ oninput={function}
160
+ onchange={function}
161
+ />
162
+ ```
163
+
164
+ ### Select
165
+ ```svelte
166
+ <Select
167
+ options={[{ value, label }]}
168
+ bind:value={any}
169
+ placeholder={string}
170
+ labelKey="label"
171
+ valueKey="value"
172
+ disabled={boolean}
173
+ onchange={function}
174
+ />
175
+ ```
176
+
177
+ ### Badge
178
+ ```svelte
179
+ <Badge
180
+ variant="default|primary|secondary|success|warning|error"
181
+ size="sm|md|lg"
182
+ >
183
+ Text
184
+ </Badge>
185
+ ```
186
+
187
+ ### Toast
188
+ ```svelte
189
+ <!-- In root layout -->
190
+ <script>
191
+ import { Toast, createToastContext } from '@miozu/jera';
192
+ const toast = createToastContext();
193
+ </script>
194
+ <Toast />
195
+
196
+ <!-- Usage anywhere -->
197
+ <script>
198
+ import { getToastContext } from '@miozu/jera';
199
+ const toast = getToastContext();
200
+ toast.success('Message');
201
+ toast.error('Error message');
202
+ </script>
203
+ ```
204
+
205
+ ### Modal
206
+ ```svelte
207
+ <script>
208
+ import { Modal, Button } from '@miozu/jera';
209
+ let showModal = $state(false);
210
+ </script>
211
+
212
+ <Button onclick={() => showModal = true}>Open Modal</Button>
213
+
214
+ <Modal bind:open={showModal} title="Confirm Action" variant="danger">
215
+ <p>Are you sure you want to proceed?</p>
216
+ {#snippet footer()}
217
+ <Button variant="ghost" onclick={() => showModal = false}>Cancel</Button>
218
+ <Button variant="danger" onclick={handleConfirm}>Confirm</Button>
219
+ {/snippet}
220
+ </Modal>
221
+ ```
222
+
223
+ Props: `open`, `title`, `size` (sm/md/lg/xl), `variant` (default/danger/warning/success/info), `closeOnBackdrop`, `closeOnEscape`, `showClose`, `children`, `footer`, `icon`, `onclose`
224
+
225
+ ### Popover
226
+ ```svelte
227
+ <script>
228
+ import { Popover, Button } from '@miozu/jera';
229
+ </script>
230
+
231
+ <Popover content="Helpful tooltip text" position="top">
232
+ <Button>Hover me</Button>
233
+ </Popover>
234
+ ```
235
+
236
+ Props: `content`, `position` (top/bottom/left/right), `delay` ({show, hide}), `offset`
237
+
238
+ ### Divider
239
+ ```svelte
240
+ <Divider />
241
+ <Divider orientation="vertical" />
242
+ <Divider>or continue with</Divider>
243
+ ```
244
+
245
+ Props: `orientation` (horizontal/vertical), `thickness`, `spacing`, `children`
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
+
275
+ ### Avatar
276
+ ```svelte
277
+ <Avatar src="/user.jpg" alt="John Doe" />
278
+ <Avatar name="John Doe" />
279
+ <Avatar src="/user.jpg" status="online" size="lg" />
280
+ ```
281
+
282
+ Props: `src`, `alt`, `name`, `size` (xs/sm/md/lg/xl/2xl), `status` (online/offline/busy/away)
283
+
284
+ ### Skeleton
285
+ ```svelte
286
+ <Skeleton width="80%" />
287
+ <Skeleton variant="circle" size="48px" />
288
+ <Skeleton variant="rect" width="100%" height="200px" />
289
+ <Skeleton lines={3} />
290
+ ```
291
+
292
+ Props: `variant` (text/heading/circle/rect), `width`, `height`, `size`, `lines`, `animate`
293
+
294
+ ### ProgressBar
295
+ ```svelte
296
+ <ProgressBar value={65} />
297
+ <ProgressBar value={80} showLabel variant="success" />
298
+ <ProgressBar indeterminate />
299
+ ```
300
+
301
+ Props: `value`, `max`, `size` (sm/md/lg), `variant` (primary/success/warning/error/info), `showLabel`, `label`, `indeterminate`
302
+
303
+ ### Spinner
304
+ ```svelte
305
+ <Spinner />
306
+ <Spinner size="lg" color="var(--color-base11)" />
307
+ ```
308
+
309
+ Props: `size` (xs/sm/md/lg/xl), `color`, `label`
310
+
311
+ ### Tabs
312
+ ```svelte
313
+ <script>
314
+ import { Tabs } from '@miozu/jera';
315
+ let activeTab = $state('tab1');
316
+ </script>
317
+
318
+ <Tabs
319
+ tabs={[
320
+ { id: 'tab1', label: 'Overview' },
321
+ { id: 'tab2', label: 'Settings', badge: 3 },
322
+ { id: 'tab3', label: 'Analytics', disabled: true }
323
+ ]}
324
+ bind:active={activeTab}
325
+ variant="underline"
326
+ />
327
+ ```
328
+
329
+ Props: `tabs` (array), `active`, `variant` (default/underline/pills), `size` (sm/md/lg), `fullWidth`, `onchange`
330
+
331
+ ### Accordion
332
+ ```svelte
333
+ <script>
334
+ import { Accordion, AccordionItem } from '@miozu/jera';
335
+ </script>
336
+
337
+ <Accordion multiple>
338
+ <AccordionItem id="section1" title="Section 1">
339
+ Content for section 1
340
+ </AccordionItem>
341
+ <AccordionItem id="section2" title="Section 2">
342
+ Content for section 2
343
+ </AccordionItem>
344
+ </Accordion>
345
+ ```
346
+
347
+ Accordion props: `expanded` (array of ids), `multiple`
348
+ AccordionItem props: `id`, `title`, `disabled`
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
+
416
+ ---
417
+
418
+ ## Actions Reference
419
+
420
+ ### clickOutside
421
+ ```svelte
422
+ <div use:clickOutside={() => isOpen = false}>
423
+ ```
424
+
425
+ ### focusTrap
426
+ ```svelte
427
+ <dialog use:focusTrap={{ enabled: isOpen }}>
428
+ ```
429
+
430
+ ### portal
431
+ ```svelte
432
+ <div use:portal={'body'}>
433
+ Renders at body level
434
+ </div>
435
+ ```
436
+
437
+ ### escapeKey
438
+ ```svelte
439
+ <div use:escapeKey={() => close()}>
440
+ ```
441
+
442
+ ---
443
+
444
+ ## Common Tasks
445
+
446
+ ### Add New Component
447
+ 1. Create file in appropriate folder (`primitives/`, `forms/`, `feedback/`)
448
+ 2. Use single `$props()` call
449
+ 3. Export styles with `cv()` if variants needed
450
+ 4. Add to `src/index.js` exports
451
+ 5. Document in this file
452
+
453
+ ### Add New Token
454
+ 1. Add to appropriate token file in `src/tokens/`
455
+ 2. Use semantic naming
456
+ 3. Add light theme variant if applicable
457
+
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
+
520
+ ```javascript
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.
678
+
679
+ ---
680
+
681
+ ## Integration with dash.selify.ai
682
+
683
+ jera components work out-of-the-box with dash.selify.ai. The semantic tokens are already configured in both systems.
684
+
685
+ ### Required: Import jera tokens
686
+ ```css
687
+ /* In your app.css or layout */
688
+ @import '@miozu/jera/tokens/colors.css';
689
+ ```
690
+
691
+ ### Semantic Token Mapping
692
+ | jera Token | dash.selify.ai Equivalent |
693
+ |------------|---------------------------|
694
+ | `--color-bg` | `--color-base00` |
695
+ | `--color-surface` | `--color-base01` |
696
+ | `--color-surface-alt` | `--color-base02` |
697
+ | `--color-text` | `--color-base05` |
698
+ | `--color-text-strong` | `--color-base07` |
699
+ | `--color-text-muted` | `--color-base04` |
700
+ | `--color-primary` | `--color-base0D` |
701
+ | `--color-success` | `--color-base0B` |
702
+ | `--color-warning` | `--color-base0A` |
703
+ | `--color-error` | `--color-base08` |
704
+ | `--color-info` | `--color-base0C` |
705
+
706
+ ### Using jera Components in dash.selify.ai
707
+ ```svelte
708
+ <script>
709
+ import { Button, Modal, Input } from '@miozu/jera';
710
+
711
+ let showModal = $state(false);
712
+ </script>
713
+
714
+ <!-- Works with existing dash.selify.ai theme system -->
715
+ <Button variant="primary" onclick={() => showModal = true}>
716
+ Open Modal
717
+ </Button>
718
+
719
+ <Modal bind:open={showModal} title="Example">
720
+ <Input placeholder="Type here..." />
721
+ </Modal>
722
+ ```
723
+
724
+ ---
725
+
726
+ ## Rules for AI Assistants
727
+
728
+ 1. **Always use Svelte 5 runes** - No legacy `$:`, `export let`, stores
729
+ 2. **Single $props() call** - Destructure all props in one call
730
+ 3. **Use cv() for variants** - Don't hardcode conditional classes
731
+ 4. **Semantic colors** - Use `--color-*` tokens, not raw `--base*`
732
+ 5. **Accessibility first** - Include ARIA attributes, keyboard support
733
+ 6. **No TypeScript** - Pure JavaScript with JSDoc for documentation
734
+ 7. **Mobile-first** - Design for mobile, enhance for desktop