@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
@@ -0,0 +1,116 @@
1
+ <!--
2
+ @component ProgressBar
3
+
4
+ Visual progress indicator with optional label.
5
+
6
+ @example Basic usage
7
+ <ProgressBar value={65} />
8
+
9
+ @example With label
10
+ <ProgressBar value={65} showLabel />
11
+
12
+ @example Custom color
13
+ <ProgressBar value={80} variant="success" />
14
+ -->
15
+ <script>
16
+ let {
17
+ value = 0,
18
+ max = 100,
19
+ size = 'md',
20
+ variant = 'primary',
21
+ showLabel = false,
22
+ label = null,
23
+ indeterminate = false,
24
+ class: className = ''
25
+ } = $props();
26
+
27
+ const percentage = $derived(Math.min(100, Math.max(0, (value / max) * 100)));
28
+
29
+ const variantColors = {
30
+ primary: 'var(--color-primary)',
31
+ success: 'var(--color-success)',
32
+ warning: 'var(--color-warning)',
33
+ error: 'var(--color-error)',
34
+ info: 'var(--color-info)'
35
+ };
36
+
37
+ const barColor = $derived(variantColors[variant] || variantColors.primary);
38
+ </script>
39
+
40
+ <div class="progress-container {className}">
41
+ {#if showLabel || label}
42
+ <div class="progress-header">
43
+ {#if label}
44
+ <span class="progress-label">{label}</span>
45
+ {/if}
46
+ {#if showLabel && !indeterminate}
47
+ <span class="progress-value">{Math.round(percentage)}%</span>
48
+ {/if}
49
+ </div>
50
+ {/if}
51
+
52
+ <div
53
+ class="progress progress-{size}"
54
+ role="progressbar"
55
+ aria-valuenow={indeterminate ? undefined : value}
56
+ aria-valuemin="0"
57
+ aria-valuemax={max}
58
+ >
59
+ <div
60
+ class="progress-bar"
61
+ class:progress-indeterminate={indeterminate}
62
+ style="width: {indeterminate ? '100%' : percentage + '%'}; background: {barColor};"
63
+ ></div>
64
+ </div>
65
+ </div>
66
+
67
+ <style>
68
+ .progress-container {
69
+ width: 100%;
70
+ }
71
+
72
+ .progress-header {
73
+ display: flex;
74
+ justify-content: space-between;
75
+ align-items: center;
76
+ margin-bottom: 0.375rem;
77
+ }
78
+
79
+ .progress-label {
80
+ font-size: 0.875rem;
81
+ color: var(--color-text);
82
+ }
83
+
84
+ .progress-value {
85
+ font-size: 0.875rem;
86
+ font-weight: 500;
87
+ color: var(--color-text-strong);
88
+ }
89
+
90
+ .progress {
91
+ width: 100%;
92
+ background: var(--color-surface-alt);
93
+ border-radius: 9999px;
94
+ overflow: hidden;
95
+ }
96
+
97
+ .progress-sm { height: 0.25rem; }
98
+ .progress-md { height: 0.5rem; }
99
+ .progress-lg { height: 0.75rem; }
100
+
101
+ .progress-bar {
102
+ height: 100%;
103
+ border-radius: 9999px;
104
+ transition: width 0.3s ease-out;
105
+ }
106
+
107
+ .progress-indeterminate {
108
+ width: 30% !important;
109
+ animation: indeterminate 1.5s ease-in-out infinite;
110
+ }
111
+
112
+ @keyframes indeterminate {
113
+ 0% { transform: translateX(-100%); }
114
+ 100% { transform: translateX(400%); }
115
+ }
116
+ </style>
@@ -0,0 +1,107 @@
1
+ <!--
2
+ @component Skeleton
3
+
4
+ Loading placeholder with shimmer animation.
5
+
6
+ @example Text line
7
+ <Skeleton width="80%" />
8
+
9
+ @example Circle avatar
10
+ <Skeleton variant="circle" size="48px" />
11
+
12
+ @example Rectangle card
13
+ <Skeleton variant="rect" width="100%" height="200px" />
14
+ -->
15
+ <script>
16
+ let {
17
+ variant = 'text',
18
+ width = '100%',
19
+ height = null,
20
+ size = null,
21
+ lines = 1,
22
+ animate = true,
23
+ class: className = ''
24
+ } = $props();
25
+
26
+ // Compute dimensions based on variant
27
+ const computedHeight = $derived.by(() => {
28
+ if (height) return height;
29
+ if (size) return size;
30
+ switch (variant) {
31
+ case 'text': return '1rem';
32
+ case 'heading': return '1.5rem';
33
+ case 'circle': return size || '2.5rem';
34
+ case 'rect': return '4rem';
35
+ default: return '1rem';
36
+ }
37
+ });
38
+
39
+ const computedWidth = $derived.by(() => {
40
+ if (width) return width;
41
+ if (size && variant === 'circle') return size;
42
+ return '100%';
43
+ });
44
+ </script>
45
+
46
+ {#if lines > 1}
47
+ <div class="skeleton-lines {className}">
48
+ {#each Array(lines) as _, i}
49
+ <div
50
+ class="skeleton skeleton-{variant}"
51
+ class:skeleton-animate={animate}
52
+ style="width: {i === lines - 1 ? '60%' : computedWidth}; height: {computedHeight};"
53
+ ></div>
54
+ {/each}
55
+ </div>
56
+ {:else}
57
+ <div
58
+ class="skeleton skeleton-{variant} {className}"
59
+ class:skeleton-animate={animate}
60
+ style="width: {computedWidth}; height: {computedHeight};"
61
+ ></div>
62
+ {/if}
63
+
64
+ <style>
65
+ .skeleton {
66
+ background: var(--color-surface-alt);
67
+ border-radius: 0.25rem;
68
+ }
69
+
70
+ .skeleton-text {
71
+ border-radius: 0.25rem;
72
+ }
73
+
74
+ .skeleton-heading {
75
+ border-radius: 0.25rem;
76
+ }
77
+
78
+ .skeleton-circle {
79
+ border-radius: 50%;
80
+ }
81
+
82
+ .skeleton-rect {
83
+ border-radius: 0.5rem;
84
+ }
85
+
86
+ .skeleton-lines {
87
+ display: flex;
88
+ flex-direction: column;
89
+ gap: 0.5rem;
90
+ }
91
+
92
+ .skeleton-animate {
93
+ background: linear-gradient(
94
+ 90deg,
95
+ var(--color-surface-alt) 0%,
96
+ var(--color-border) 50%,
97
+ var(--color-surface-alt) 100%
98
+ );
99
+ background-size: 200% 100%;
100
+ animation: skeleton-shimmer 1.5s ease-in-out infinite;
101
+ }
102
+
103
+ @keyframes skeleton-shimmer {
104
+ 0% { background-position: 200% 0; }
105
+ 100% { background-position: -200% 0; }
106
+ }
107
+ </style>
@@ -0,0 +1,77 @@
1
+ <!--
2
+ @component Spinner
3
+
4
+ Loading spinner indicator.
5
+
6
+ @example Basic usage
7
+ <Spinner />
8
+
9
+ @example With size
10
+ <Spinner size="lg" />
11
+
12
+ @example Custom color
13
+ <Spinner color="var(--color-success)" />
14
+ -->
15
+ <script>
16
+ let {
17
+ size = 'md',
18
+ color = 'currentColor',
19
+ label = 'Loading...',
20
+ class: className = ''
21
+ } = $props();
22
+
23
+ const sizes = {
24
+ xs: 12,
25
+ sm: 16,
26
+ md: 24,
27
+ lg: 32,
28
+ xl: 48
29
+ };
30
+
31
+ const sizeValue = $derived(sizes[size] || sizes.md);
32
+ </script>
33
+
34
+ <svg
35
+ class="spinner {className}"
36
+ width={sizeValue}
37
+ height={sizeValue}
38
+ viewBox="0 0 24 24"
39
+ fill="none"
40
+ aria-label={label}
41
+ role="status"
42
+ >
43
+ <circle
44
+ class="spinner-track"
45
+ cx="12"
46
+ cy="12"
47
+ r="10"
48
+ stroke="currentColor"
49
+ stroke-width="3"
50
+ opacity="0.2"
51
+ />
52
+ <circle
53
+ class="spinner-indicator"
54
+ cx="12"
55
+ cy="12"
56
+ r="10"
57
+ stroke={color}
58
+ stroke-width="3"
59
+ stroke-linecap="round"
60
+ stroke-dasharray="60 200"
61
+ />
62
+ </svg>
63
+
64
+ <style>
65
+ .spinner {
66
+ animation: spin 1s linear infinite;
67
+ }
68
+
69
+ .spinner-track {
70
+ opacity: 0.2;
71
+ }
72
+
73
+ @keyframes spin {
74
+ from { transform: rotate(0deg); }
75
+ to { transform: rotate(360deg); }
76
+ }
77
+ </style>
@@ -0,0 +1,297 @@
1
+ <!--
2
+ @component Toast
3
+
4
+ A toast notification system with stacking, auto-dismiss, and animations.
5
+ Demonstrates advanced Svelte 5 patterns:
6
+
7
+ - Reactive class with ThemeState pattern
8
+ - $state.raw for non-proxied arrays (performance)
9
+ - $effect with cleanup for timers
10
+ - Context API for global toast access
11
+ - Portal action for DOM placement
12
+ - Compound variant styling
13
+
14
+ @example
15
+ // In your root layout
16
+ <ToastProvider>
17
+ <slot />
18
+ </ToastProvider>
19
+
20
+ // In any component
21
+ import { toast } from '@miozu/jera';
22
+ toast.success('Saved successfully!');
23
+ toast.error('Something went wrong');
24
+ toast.info('Did you know?');
25
+ toast.custom({ title: 'Custom', message: 'With title', duration: 5000 });
26
+ -->
27
+ <script module>
28
+ import { getContext, setContext } from 'svelte';
29
+ import { cv } from '../../utils/cn.svelte.js';
30
+
31
+ const TOAST_KEY = Symbol('jera-toast');
32
+
33
+ /**
34
+ * @typedef {'info' | 'success' | 'warning' | 'error'} ToastType
35
+ * @typedef {'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'} ToastPosition
36
+ *
37
+ * @typedef {{
38
+ * id: string,
39
+ * type: ToastType,
40
+ * title?: string,
41
+ * message: string,
42
+ * duration: number,
43
+ * createdAt: number,
44
+ * pausedAt?: number
45
+ * }} ToastItem
46
+ */
47
+
48
+ /**
49
+ * Toast Controller Class
50
+ *
51
+ * Manages toast state and provides methods for showing toasts.
52
+ * Uses Svelte 5 reactive class pattern.
53
+ */
54
+ export class ToastController {
55
+ /** @type {ToastItem[]} */
56
+ toasts = $state.raw([]);
57
+
58
+ /** @type {ToastPosition} */
59
+ position = $state('bottom-right');
60
+
61
+ /** @type {number} */
62
+ defaultDuration = 4000;
63
+
64
+ /** @type {number} */
65
+ maxToasts = 5;
66
+
67
+ #counter = 0;
68
+
69
+ /**
70
+ * Show a toast
71
+ * @param {Partial<ToastItem> & { message: string }} options
72
+ * @returns {string} Toast ID
73
+ */
74
+ show(options) {
75
+ const id = `toast-${++this.#counter}`;
76
+
77
+ const toast = {
78
+ id,
79
+ type: options.type ?? 'info',
80
+ title: options.title,
81
+ message: options.message,
82
+ duration: options.duration ?? this.defaultDuration,
83
+ createdAt: Date.now()
84
+ };
85
+
86
+ // Add to beginning (newest first for top positions)
87
+ this.toasts = [toast, ...this.toasts].slice(0, this.maxToasts);
88
+
89
+ return id;
90
+ }
91
+
92
+ /**
93
+ * Dismiss a toast by ID
94
+ * @param {string} id
95
+ */
96
+ dismiss(id) {
97
+ this.toasts = this.toasts.filter(t => t.id !== id);
98
+ }
99
+
100
+ /**
101
+ * Dismiss all toasts
102
+ */
103
+ dismissAll() {
104
+ this.toasts = [];
105
+ }
106
+
107
+ /**
108
+ * Pause a toast's auto-dismiss timer
109
+ * @param {string} id
110
+ */
111
+ pause(id) {
112
+ this.toasts = this.toasts.map(t =>
113
+ t.id === id ? { ...t, pausedAt: Date.now() } : t
114
+ );
115
+ }
116
+
117
+ /**
118
+ * Resume a toast's auto-dismiss timer
119
+ * @param {string} id
120
+ */
121
+ resume(id) {
122
+ this.toasts = this.toasts.map(t => {
123
+ if (t.id !== id || !t.pausedAt) return t;
124
+ const pausedDuration = Date.now() - t.pausedAt;
125
+ return {
126
+ ...t,
127
+ createdAt: t.createdAt + pausedDuration,
128
+ pausedAt: undefined
129
+ };
130
+ });
131
+ }
132
+
133
+ // Convenience methods
134
+ info = (message, options = {}) => this.show({ ...options, message, type: 'info' });
135
+ success = (message, options = {}) => this.show({ ...options, message, type: 'success' });
136
+ warning = (message, options = {}) => this.show({ ...options, message, type: 'warning' });
137
+ error = (message, options = {}) => this.show({ ...options, message, type: 'error' });
138
+ }
139
+
140
+ /**
141
+ * Create toast context (call in ToastProvider)
142
+ */
143
+ export function createToastContext() {
144
+ const controller = new ToastController();
145
+ setContext(TOAST_KEY, controller);
146
+ return controller;
147
+ }
148
+
149
+ /**
150
+ * Get toast controller from context
151
+ * @returns {ToastController}
152
+ */
153
+ export function getToastContext() {
154
+ return getContext(TOAST_KEY);
155
+ }
156
+
157
+ // Toast styles
158
+ export const toastStyles = cv({
159
+ base: [
160
+ 'relative flex items-start gap-3 w-full max-w-sm',
161
+ 'p-4 rounded-lg shadow-lg',
162
+ 'border',
163
+ 'animate-in fade-in slide-in-from-right-4 duration-200'
164
+ ].join(' '),
165
+
166
+ variants: {
167
+ type: {
168
+ info: 'bg-surface border-border text-text',
169
+ success: 'bg-success/10 border-success/30 text-success',
170
+ warning: 'bg-warning/10 border-warning/30 text-warning',
171
+ error: 'bg-error/10 border-error/30 text-error'
172
+ }
173
+ },
174
+
175
+ defaults: { type: 'info' }
176
+ });
177
+
178
+ export const positionStyles = {
179
+ 'top-left': 'top-4 left-4 flex-col',
180
+ 'top-center': 'top-4 left-1/2 -translate-x-1/2 flex-col',
181
+ 'top-right': 'top-4 right-4 flex-col',
182
+ 'bottom-left': 'bottom-4 left-4 flex-col-reverse',
183
+ 'bottom-center': 'bottom-4 left-1/2 -translate-x-1/2 flex-col-reverse',
184
+ 'bottom-right': 'bottom-4 right-4 flex-col-reverse'
185
+ };
186
+ </script>
187
+
188
+ <script>
189
+ import { cn } from '../../utils/cn.svelte.js';
190
+ import { portal } from '../../actions/index.js';
191
+
192
+ // Get toast controller from context
193
+ const toast = getToastContext();
194
+
195
+ // Icon components for each type
196
+ const icons = {
197
+ info: `<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>`,
198
+ success: `<circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/>`,
199
+ warning: `<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/>`,
200
+ error: `<circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/>`
201
+ };
202
+ </script>
203
+
204
+ {#if toast.toasts.length > 0}
205
+ <div
206
+ use:portal
207
+ class={cn(
208
+ 'fixed z-[var(--z-toast)] flex gap-2 pointer-events-none',
209
+ positionStyles[toast.position]
210
+ )}
211
+ role="region"
212
+ aria-label="Notifications"
213
+ >
214
+ {#each toast.toasts as item (item.id)}
215
+ {@const remaining = item.duration - (Date.now() - item.createdAt)}
216
+
217
+ <div
218
+ class={cn(toastStyles({ type: item.type }), 'pointer-events-auto')}
219
+ role="alert"
220
+ aria-live="polite"
221
+ onmouseenter={() => toast.pause(item.id)}
222
+ onmouseleave={() => toast.resume(item.id)}
223
+ >
224
+ <!-- Icon -->
225
+ <span class="shrink-0 w-5 h-5">
226
+ <svg
227
+ viewBox="0 0 24 24"
228
+ fill="none"
229
+ stroke="currentColor"
230
+ stroke-width="2"
231
+ stroke-linecap="round"
232
+ stroke-linejoin="round"
233
+ >
234
+ {@html icons[item.type]}
235
+ </svg>
236
+ </span>
237
+
238
+ <!-- Content -->
239
+ <div class="flex-1 min-w-0">
240
+ {#if item.title}
241
+ <p class="font-medium text-sm">{item.title}</p>
242
+ {/if}
243
+ <p class={cn('text-sm', item.title && 'mt-1 opacity-90')}>
244
+ {item.message}
245
+ </p>
246
+ </div>
247
+
248
+ <!-- Close Button -->
249
+ <button
250
+ type="button"
251
+ class={cn(
252
+ 'shrink-0 p-1 rounded-md',
253
+ 'opacity-60 hover:opacity-100',
254
+ 'transition-opacity duration-150',
255
+ 'focus:outline-none focus-visible:ring-2 focus-visible:ring-current/50'
256
+ )}
257
+ onclick={() => toast.dismiss(item.id)}
258
+ aria-label="Dismiss notification"
259
+ >
260
+ <svg
261
+ class="w-4 h-4"
262
+ viewBox="0 0 24 24"
263
+ fill="none"
264
+ stroke="currentColor"
265
+ stroke-width="2"
266
+ stroke-linecap="round"
267
+ stroke-linejoin="round"
268
+ >
269
+ <path d="M18 6 6 18" />
270
+ <path d="m6 6 12 12" />
271
+ </svg>
272
+ </button>
273
+
274
+ <!-- Auto-dismiss timer -->
275
+ {#if item.duration > 0 && !item.pausedAt}
276
+ {@const _ = setTimeout(() => toast.dismiss(item.id), remaining)}
277
+ {/if}
278
+ </div>
279
+ {/each}
280
+ </div>
281
+ {/if}
282
+
283
+ <style>
284
+ @keyframes fade-in {
285
+ from { opacity: 0; }
286
+ to { opacity: 1; }
287
+ }
288
+
289
+ @keyframes slide-in-from-right-4 {
290
+ from { transform: translateX(1rem); }
291
+ to { transform: translateX(0); }
292
+ }
293
+
294
+ .animate-in {
295
+ animation: fade-in 200ms ease-out, slide-in-from-right-4 200ms ease-out;
296
+ }
297
+ </style>