@miozu/jera 0.0.2 → 0.3.0

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 (53) hide show
  1. package/CLAUDE.md +443 -0
  2. package/README.md +211 -1
  3. package/llms.txt +64 -0
  4. package/package.json +44 -14
  5. package/src/actions/index.js +375 -0
  6. package/src/components/feedback/EmptyState.svelte +179 -0
  7. package/src/components/feedback/ProgressBar.svelte +116 -0
  8. package/src/components/feedback/Skeleton.svelte +107 -0
  9. package/src/components/feedback/Spinner.svelte +77 -0
  10. package/src/components/feedback/Toast.svelte +297 -0
  11. package/src/components/forms/Checkbox.svelte +147 -0
  12. package/src/components/forms/Dropzone.svelte +248 -0
  13. package/src/components/forms/FileUpload.svelte +266 -0
  14. package/src/components/forms/IconInput.svelte +184 -0
  15. package/src/components/forms/Input.svelte +121 -0
  16. package/src/components/forms/NumberInput.svelte +225 -0
  17. package/src/components/forms/PinInput.svelte +169 -0
  18. package/src/components/forms/Radio.svelte +143 -0
  19. package/src/components/forms/RadioGroup.svelte +62 -0
  20. package/src/components/forms/RangeSlider.svelte +212 -0
  21. package/src/components/forms/SearchInput.svelte +175 -0
  22. package/src/components/forms/Select.svelte +326 -0
  23. package/src/components/forms/Switch.svelte +159 -0
  24. package/src/components/forms/Textarea.svelte +122 -0
  25. package/src/components/navigation/Accordion.svelte +65 -0
  26. package/src/components/navigation/AccordionItem.svelte +146 -0
  27. package/src/components/navigation/Tabs.svelte +239 -0
  28. package/src/components/overlays/ConfirmDialog.svelte +272 -0
  29. package/src/components/overlays/Dropdown.svelte +153 -0
  30. package/src/components/overlays/DropdownDivider.svelte +23 -0
  31. package/src/components/overlays/DropdownItem.svelte +97 -0
  32. package/src/components/overlays/Modal.svelte +232 -0
  33. package/src/components/overlays/Popover.svelte +206 -0
  34. package/src/components/primitives/Avatar.svelte +132 -0
  35. package/src/components/primitives/Badge.svelte +118 -0
  36. package/src/components/primitives/Button.svelte +262 -0
  37. package/src/components/primitives/Card.svelte +104 -0
  38. package/src/components/primitives/Divider.svelte +105 -0
  39. package/src/components/primitives/LazyImage.svelte +104 -0
  40. package/src/components/primitives/Link.svelte +122 -0
  41. package/src/components/primitives/StatusBadge.svelte +122 -0
  42. package/src/index.js +128 -0
  43. package/src/tokens/colors.css +189 -0
  44. package/src/tokens/effects.css +128 -0
  45. package/src/tokens/index.css +81 -0
  46. package/src/tokens/spacing.css +49 -0
  47. package/src/tokens/typography.css +79 -0
  48. package/src/utils/cn.svelte.js +175 -0
  49. package/src/utils/index.js +17 -0
  50. package/src/utils/reactive.svelte.js +239 -0
  51. package/jera.js +0 -135
  52. package/www/components/jera/Input/Input.svelte +0 -63
  53. package/www/components/jera/Input/index.js +0 -1
package/package.json CHANGED
@@ -1,24 +1,54 @@
1
1
  {
2
2
  "name": "@miozu/jera",
3
- "version": "0.0.2",
4
- "description": "Svelte 5 component library",
5
- "main": "jera.js",
6
- "bin": {
7
- "jera": "./jera.js"
8
- },
3
+ "version": "0.3.0",
4
+ "description": "Minimal, reactive component library for Svelte 5",
9
5
  "type": "module",
10
6
  "scripts": {
11
- "test": "echo \"Error: no test specified\" && exit 1"
7
+ "prepublishOnly": "echo 'Publishing @miozu/jera...' && test -f src/index.js"
8
+ },
9
+ "svelte": "./src/index.js",
10
+ "exports": {
11
+ ".": {
12
+ "svelte": "./src/index.js",
13
+ "default": "./src/index.js"
14
+ },
15
+ "./tokens": "./src/tokens/index.css",
16
+ "./tokens/colors": "./src/tokens/colors.css",
17
+ "./tokens/spacing": "./src/tokens/spacing.css",
18
+ "./tokens/typography": "./src/tokens/typography.css",
19
+ "./tokens/effects": "./src/tokens/effects.css",
20
+ "./utils": "./src/utils/index.js",
21
+ "./actions": "./src/actions/index.js"
22
+ },
23
+ "files": [
24
+ "src",
25
+ "llms.txt",
26
+ "CLAUDE.md"
27
+ ],
28
+ "peerDependencies": {
29
+ "svelte": "^5.0.0"
12
30
  },
31
+ "devDependencies": {
32
+ "svelte": "^5.41.0"
33
+ },
34
+ "keywords": [
35
+ "svelte",
36
+ "svelte5",
37
+ "components",
38
+ "design-system",
39
+ "ui",
40
+ "miozu",
41
+ "base16",
42
+ "dark-theme"
43
+ ],
44
+ "author": "Nicholas Glazer <glazer.nicholas@gmail.com>",
45
+ "license": "MIT",
13
46
  "repository": {
14
47
  "type": "git",
15
- "url": "https://codeberg.org/miozu/jera"
48
+ "url": "https://github.com/miozu-com/jera"
16
49
  },
17
- "keywords": ["component library", "svelte 5", "sveltekit"],
18
- "author": "Nicholas Glazer <info@nicgl.com>",
19
- "license": "MIT",
20
- "packageManager": "pnpm@10.6.5",
21
- "dependencies": {
22
- "commander": "^13.1.0"
50
+ "homepage": "https://miozu.com",
51
+ "bugs": {
52
+ "url": "https://github.com/miozu-com/jera/issues"
23
53
  }
24
54
  }
@@ -0,0 +1,375 @@
1
+ /**
2
+ * Svelte Actions Library
3
+ *
4
+ * Reusable element behaviors that can be composed onto any element.
5
+ * Actions are the Svelte way to add imperative logic to elements.
6
+ *
7
+ * @example
8
+ * <div use:clickOutside={handleClose}>...</div>
9
+ * <input use:focus />
10
+ * <button use:longPress={{ duration: 500, onLongPress: handler }}>...</button>
11
+ */
12
+
13
+ /**
14
+ * Click Outside Action
15
+ *
16
+ * Fires callback when clicking outside the element.
17
+ * Useful for dropdowns, modals, popovers.
18
+ *
19
+ * @param {HTMLElement} node
20
+ * @param {() => void} callback
21
+ * @returns {{ destroy: () => void, update: (cb: () => void) => void }}
22
+ *
23
+ * @example
24
+ * <div use:clickOutside={() => isOpen = false}>Dropdown</div>
25
+ */
26
+ export function clickOutside(node, callback) {
27
+ let handler = callback;
28
+
29
+ function handleClick(event) {
30
+ if (!node.contains(event.target) && !event.defaultPrevented) {
31
+ handler?.();
32
+ }
33
+ }
34
+
35
+ // Use mousedown for better UX (fires before focus changes)
36
+ document.addEventListener('mousedown', handleClick, true);
37
+
38
+ return {
39
+ update(newCallback) {
40
+ handler = newCallback;
41
+ },
42
+ destroy() {
43
+ document.removeEventListener('mousedown', handleClick, true);
44
+ }
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Focus Trap Action
50
+ *
51
+ * Traps focus within an element. Essential for accessible modals.
52
+ *
53
+ * @param {HTMLElement} node
54
+ * @param {{ enabled?: boolean, initialFocus?: string }} [options]
55
+ * @returns {{ destroy: () => void, update: (opts: { enabled?: boolean }) => void }}
56
+ *
57
+ * @example
58
+ * <dialog use:focusTrap={{ enabled: isOpen }}>...</dialog>
59
+ */
60
+ export function focusTrap(node, options = {}) {
61
+ let { enabled = true, initialFocus } = options;
62
+
63
+ const focusableSelectors = [
64
+ 'a[href]',
65
+ 'button:not([disabled])',
66
+ 'input:not([disabled])',
67
+ 'select:not([disabled])',
68
+ 'textarea:not([disabled])',
69
+ '[tabindex]:not([tabindex="-1"])'
70
+ ].join(',');
71
+
72
+ function getFocusable() {
73
+ return Array.from(node.querySelectorAll(focusableSelectors));
74
+ }
75
+
76
+ function handleKeydown(event) {
77
+ if (!enabled || event.key !== 'Tab') return;
78
+
79
+ const focusable = getFocusable();
80
+ if (focusable.length === 0) return;
81
+
82
+ const first = focusable[0];
83
+ const last = focusable[focusable.length - 1];
84
+
85
+ if (event.shiftKey && document.activeElement === first) {
86
+ event.preventDefault();
87
+ last.focus();
88
+ } else if (!event.shiftKey && document.activeElement === last) {
89
+ event.preventDefault();
90
+ first.focus();
91
+ }
92
+ }
93
+
94
+ function setInitialFocus() {
95
+ if (!enabled) return;
96
+
97
+ const target = initialFocus
98
+ ? node.querySelector(initialFocus)
99
+ : getFocusable()[0];
100
+
101
+ target?.focus();
102
+ }
103
+
104
+ node.addEventListener('keydown', handleKeydown);
105
+ // Delay initial focus to allow transitions
106
+ requestAnimationFrame(setInitialFocus);
107
+
108
+ return {
109
+ update(newOptions) {
110
+ enabled = newOptions.enabled ?? true;
111
+ initialFocus = newOptions.initialFocus;
112
+ if (enabled) setInitialFocus();
113
+ },
114
+ destroy() {
115
+ node.removeEventListener('keydown', handleKeydown);
116
+ }
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Auto-focus Action
122
+ *
123
+ * Focuses element on mount.
124
+ *
125
+ * @param {HTMLElement} node
126
+ * @param {{ delay?: number, select?: boolean }} [options]
127
+ *
128
+ * @example
129
+ * <input use:autoFocus />
130
+ * <input use:autoFocus={{ delay: 100, select: true }} />
131
+ */
132
+ export function autoFocus(node, options = {}) {
133
+ const { delay = 0, select = false } = options;
134
+
135
+ const timeout = setTimeout(() => {
136
+ node.focus();
137
+ if (select && 'select' in node) {
138
+ node.select();
139
+ }
140
+ }, delay);
141
+
142
+ return {
143
+ destroy() {
144
+ clearTimeout(timeout);
145
+ }
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Long Press Action
151
+ *
152
+ * Detects long press/hold gesture.
153
+ *
154
+ * @param {HTMLElement} node
155
+ * @param {{ duration?: number, onLongPress: () => void }} options
156
+ *
157
+ * @example
158
+ * <button use:longPress={{ duration: 500, onLongPress: handleLongPress }}>
159
+ * Hold me
160
+ * </button>
161
+ */
162
+ export function longPress(node, options) {
163
+ let { duration = 500, onLongPress } = options;
164
+ let timeout = null;
165
+
166
+ function handleStart() {
167
+ timeout = setTimeout(() => {
168
+ onLongPress?.();
169
+ }, duration);
170
+ }
171
+
172
+ function handleEnd() {
173
+ if (timeout) {
174
+ clearTimeout(timeout);
175
+ timeout = null;
176
+ }
177
+ }
178
+
179
+ node.addEventListener('mousedown', handleStart);
180
+ node.addEventListener('touchstart', handleStart, { passive: true });
181
+ node.addEventListener('mouseup', handleEnd);
182
+ node.addEventListener('mouseleave', handleEnd);
183
+ node.addEventListener('touchend', handleEnd);
184
+ node.addEventListener('touchcancel', handleEnd);
185
+
186
+ return {
187
+ update(newOptions) {
188
+ duration = newOptions.duration ?? 500;
189
+ onLongPress = newOptions.onLongPress;
190
+ },
191
+ destroy() {
192
+ handleEnd();
193
+ node.removeEventListener('mousedown', handleStart);
194
+ node.removeEventListener('touchstart', handleStart);
195
+ node.removeEventListener('mouseup', handleEnd);
196
+ node.removeEventListener('mouseleave', handleEnd);
197
+ node.removeEventListener('touchend', handleEnd);
198
+ node.removeEventListener('touchcancel', handleEnd);
199
+ }
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Escape Key Action
205
+ *
206
+ * Fires callback when Escape is pressed while element or children have focus.
207
+ *
208
+ * @param {HTMLElement} node
209
+ * @param {() => void} callback
210
+ *
211
+ * @example
212
+ * <dialog use:escapeKey={handleClose}>...</dialog>
213
+ */
214
+ export function escapeKey(node, callback) {
215
+ let handler = callback;
216
+
217
+ function handleKeydown(event) {
218
+ if (event.key === 'Escape') {
219
+ event.preventDefault();
220
+ handler?.();
221
+ }
222
+ }
223
+
224
+ // Listen on document for global escape handling
225
+ document.addEventListener('keydown', handleKeydown);
226
+
227
+ return {
228
+ update(newCallback) {
229
+ handler = newCallback;
230
+ },
231
+ destroy() {
232
+ document.removeEventListener('keydown', handleKeydown);
233
+ }
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Portal Action
239
+ *
240
+ * Moves element to a different location in the DOM.
241
+ * Useful for modals, toasts, tooltips.
242
+ *
243
+ * @param {HTMLElement} node
244
+ * @param {string | HTMLElement} [target] - CSS selector or element (default: document.body)
245
+ *
246
+ * @example
247
+ * <div use:portal>Portaled to body</div>
248
+ * <div use:portal="#modal-root">Portaled to #modal-root</div>
249
+ */
250
+ export function portal(node, target = 'body') {
251
+ let targetEl;
252
+
253
+ function update(newTarget) {
254
+ targetEl = typeof newTarget === 'string'
255
+ ? document.querySelector(newTarget)
256
+ : newTarget;
257
+
258
+ if (targetEl) {
259
+ targetEl.appendChild(node);
260
+ }
261
+ }
262
+
263
+ update(target);
264
+
265
+ return {
266
+ update,
267
+ destroy() {
268
+ node.remove();
269
+ }
270
+ };
271
+ }
272
+
273
+ /**
274
+ * Intersection Observer Action
275
+ *
276
+ * Observes element visibility in viewport.
277
+ *
278
+ * @param {HTMLElement} node
279
+ * @param {{ onIntersect: (entry: IntersectionObserverEntry) => void, options?: IntersectionObserverInit }} config
280
+ *
281
+ * @example
282
+ * <div use:intersect={{ onIntersect: (e) => isVisible = e.isIntersecting }}>
283
+ * Lazy loaded content
284
+ * </div>
285
+ */
286
+ export function intersect(node, config) {
287
+ let { onIntersect, options = {} } = config;
288
+
289
+ const observer = new IntersectionObserver((entries) => {
290
+ entries.forEach(entry => onIntersect?.(entry));
291
+ }, options);
292
+
293
+ observer.observe(node);
294
+
295
+ return {
296
+ update(newConfig) {
297
+ onIntersect = newConfig.onIntersect;
298
+ },
299
+ destroy() {
300
+ observer.disconnect();
301
+ }
302
+ };
303
+ }
304
+
305
+ /**
306
+ * Resize Observer Action
307
+ *
308
+ * Observes element size changes.
309
+ *
310
+ * @param {HTMLElement} node
311
+ * @param {(entry: ResizeObserverEntry) => void} callback
312
+ *
313
+ * @example
314
+ * <div use:resize={(entry) => width = entry.contentRect.width}>
315
+ * Responsive content
316
+ * </div>
317
+ */
318
+ export function resize(node, callback) {
319
+ let handler = callback;
320
+
321
+ const observer = new ResizeObserver((entries) => {
322
+ entries.forEach(entry => handler?.(entry));
323
+ });
324
+
325
+ observer.observe(node);
326
+
327
+ return {
328
+ update(newCallback) {
329
+ handler = newCallback;
330
+ },
331
+ destroy() {
332
+ observer.disconnect();
333
+ }
334
+ };
335
+ }
336
+
337
+ /**
338
+ * Copy to Clipboard Action
339
+ *
340
+ * Copies element's text content or provided value on click.
341
+ *
342
+ * @param {HTMLElement} node
343
+ * @param {{ value?: string, onCopy?: () => void, onError?: (err: Error) => void }} [options]
344
+ *
345
+ * @example
346
+ * <button use:copy={{ value: 'Text to copy', onCopy: () => showToast('Copied!') }}>
347
+ * Copy
348
+ * </button>
349
+ */
350
+ export function copy(node, options = {}) {
351
+ let { value, onCopy, onError } = options;
352
+
353
+ async function handleClick() {
354
+ try {
355
+ const text = value ?? node.textContent ?? '';
356
+ await navigator.clipboard.writeText(text);
357
+ onCopy?.();
358
+ } catch (err) {
359
+ onError?.(err);
360
+ }
361
+ }
362
+
363
+ node.addEventListener('click', handleClick);
364
+
365
+ return {
366
+ update(newOptions) {
367
+ value = newOptions.value;
368
+ onCopy = newOptions.onCopy;
369
+ onError = newOptions.onError;
370
+ },
371
+ destroy() {
372
+ node.removeEventListener('click', handleClick);
373
+ }
374
+ };
375
+ }
@@ -0,0 +1,179 @@
1
+ <!--
2
+ @component EmptyState
3
+
4
+ A complete empty state UI with icon, title, description, and action buttons.
5
+
6
+ @example Basic
7
+ <EmptyState
8
+ title="No items found"
9
+ description="Try adjusting your search or filters"
10
+ />
11
+
12
+ @example With icon and action
13
+ <EmptyState
14
+ title="No messages"
15
+ description="Start a conversation to see messages here"
16
+ >
17
+ {#snippet icon()}
18
+ <MessageIcon size={32} />
19
+ {/snippet}
20
+ {#snippet actions()}
21
+ <Button variant="primary" onclick={startChat}>New Message</Button>
22
+ {/snippet}
23
+ </EmptyState>
24
+
25
+ @example Compact size
26
+ <EmptyState size="compact" title="No results" />
27
+ -->
28
+ <script>
29
+ let {
30
+ title = 'No data found',
31
+ description = '',
32
+ size = 'default',
33
+ class: className = '',
34
+ icon,
35
+ actions
36
+ } = $props();
37
+ </script>
38
+
39
+ <div class="empty-state empty-state-{size} {className}">
40
+ <div class="empty-state-content">
41
+ {#if icon}
42
+ <div class="empty-state-icon">
43
+ {@render icon()}
44
+ </div>
45
+ {/if}
46
+
47
+ <div class="empty-state-text">
48
+ <h3 class="empty-state-title">{title}</h3>
49
+ {#if description}
50
+ <p class="empty-state-description">{description}</p>
51
+ {/if}
52
+ </div>
53
+
54
+ {#if actions}
55
+ <div class="empty-state-actions">
56
+ {@render actions()}
57
+ </div>
58
+ {/if}
59
+ </div>
60
+ </div>
61
+
62
+ <style>
63
+ .empty-state {
64
+ display: flex;
65
+ align-items: center;
66
+ justify-content: center;
67
+ width: 100%;
68
+ padding: var(--space-16) var(--space-6);
69
+ }
70
+
71
+ .empty-state-compact {
72
+ padding: var(--space-8) var(--space-4);
73
+ }
74
+
75
+ .empty-state-large {
76
+ padding: var(--space-24) var(--space-6);
77
+ }
78
+
79
+ .empty-state-content {
80
+ display: flex;
81
+ flex-direction: column;
82
+ align-items: center;
83
+ text-align: center;
84
+ max-width: 24rem;
85
+ }
86
+
87
+ .empty-state-icon {
88
+ display: flex;
89
+ align-items: center;
90
+ justify-content: center;
91
+ width: 4rem;
92
+ height: 4rem;
93
+ margin-bottom: var(--space-6);
94
+ border-radius: 9999px;
95
+ background: var(--color-surface);
96
+ color: var(--color-text-muted);
97
+ transition: background 0.3s ease, color 0.3s ease, transform 0.3s ease;
98
+ }
99
+
100
+ .empty-state-compact .empty-state-icon {
101
+ width: 3rem;
102
+ height: 3rem;
103
+ margin-bottom: var(--space-4);
104
+ }
105
+
106
+ .empty-state-large .empty-state-icon {
107
+ width: 5rem;
108
+ height: 5rem;
109
+ margin-bottom: var(--space-8);
110
+ }
111
+
112
+ .empty-state:hover .empty-state-icon {
113
+ background: var(--color-surface-hover);
114
+ color: var(--color-text);
115
+ transform: scale(1.05);
116
+ }
117
+
118
+ .empty-state-text {
119
+ margin-bottom: var(--space-6);
120
+ }
121
+
122
+ .empty-state-compact .empty-state-text {
123
+ margin-bottom: var(--space-4);
124
+ }
125
+
126
+ .empty-state-large .empty-state-text {
127
+ margin-bottom: var(--space-8);
128
+ }
129
+
130
+ .empty-state-title {
131
+ margin: 0 0 var(--space-2) 0;
132
+ font-size: var(--text-lg);
133
+ font-weight: 600;
134
+ color: var(--color-text-strong);
135
+ line-height: 1.3;
136
+ }
137
+
138
+ .empty-state-compact .empty-state-title {
139
+ font-size: var(--text-base);
140
+ font-weight: 500;
141
+ }
142
+
143
+ .empty-state-large .empty-state-title {
144
+ font-size: var(--text-xl);
145
+ }
146
+
147
+ .empty-state-description {
148
+ margin: 0;
149
+ font-size: var(--text-sm);
150
+ color: var(--color-text-muted);
151
+ line-height: 1.5;
152
+ max-width: 20rem;
153
+ }
154
+
155
+ .empty-state-compact .empty-state-description {
156
+ font-size: var(--text-xs);
157
+ }
158
+
159
+ .empty-state-large .empty-state-description {
160
+ font-size: var(--text-base);
161
+ max-width: 24rem;
162
+ }
163
+
164
+ .empty-state-actions {
165
+ display: flex;
166
+ align-items: center;
167
+ gap: var(--space-3);
168
+ flex-wrap: wrap;
169
+ justify-content: center;
170
+ }
171
+
172
+ .empty-state-compact .empty-state-actions {
173
+ gap: var(--space-2);
174
+ }
175
+
176
+ .empty-state-large .empty-state-actions {
177
+ gap: var(--space-4);
178
+ }
179
+ </style>