@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,146 @@
1
+ <!--
2
+ @component AccordionItem
3
+
4
+ Individual accordion section. Must be used inside Accordion.
5
+
6
+ @example
7
+ <AccordionItem id="section-1" title="Click to expand">
8
+ Hidden content here
9
+ </AccordionItem>
10
+ -->
11
+ <script>
12
+ import { getContext } from 'svelte';
13
+ import { slide } from 'svelte/transition';
14
+
15
+ let {
16
+ id,
17
+ title = '',
18
+ disabled = false,
19
+ children,
20
+ class: className = ''
21
+ } = $props();
22
+
23
+ const accordion = getContext('accordion');
24
+
25
+ // Generate id if not provided
26
+ const itemId = id || `accordion-${Math.random().toString(36).slice(2, 9)}`;
27
+
28
+ const isOpen = $derived(accordion?.isExpanded(itemId) ?? false);
29
+
30
+ function handleClick() {
31
+ if (!disabled && accordion) {
32
+ accordion.toggle(itemId);
33
+ }
34
+ }
35
+
36
+ function handleKeydown(e) {
37
+ if (e.key === 'Enter' || e.key === ' ') {
38
+ e.preventDefault();
39
+ handleClick();
40
+ }
41
+ }
42
+ </script>
43
+
44
+ <div class="accordion-item {className}" class:accordion-item-disabled={disabled}>
45
+ <button
46
+ type="button"
47
+ class="accordion-trigger"
48
+ class:accordion-trigger-open={isOpen}
49
+ aria-expanded={isOpen}
50
+ aria-controls="content-{itemId}"
51
+ aria-disabled={disabled}
52
+ onclick={handleClick}
53
+ onkeydown={handleKeydown}
54
+ >
55
+ <span class="accordion-title">{title}</span>
56
+ <svg
57
+ class="accordion-icon"
58
+ class:accordion-icon-open={isOpen}
59
+ width="16"
60
+ height="16"
61
+ viewBox="0 0 24 24"
62
+ fill="none"
63
+ stroke="currentColor"
64
+ stroke-width="2"
65
+ stroke-linecap="round"
66
+ stroke-linejoin="round"
67
+ >
68
+ <polyline points="6 9 12 15 18 9"></polyline>
69
+ </svg>
70
+ </button>
71
+
72
+ {#if isOpen}
73
+ <div
74
+ id="content-{itemId}"
75
+ class="accordion-content"
76
+ transition:slide={{ duration: 200 }}
77
+ >
78
+ <div class="accordion-body">
79
+ {@render children?.()}
80
+ </div>
81
+ </div>
82
+ {/if}
83
+ </div>
84
+
85
+ <style>
86
+ .accordion-item {
87
+ border-bottom: 1px solid var(--color-border);
88
+ }
89
+
90
+ .accordion-item:last-child {
91
+ border-bottom: none;
92
+ }
93
+
94
+ .accordion-item-disabled {
95
+ opacity: 0.5;
96
+ }
97
+
98
+ .accordion-trigger {
99
+ display: flex;
100
+ align-items: center;
101
+ justify-content: space-between;
102
+ width: 100%;
103
+ padding: 1rem;
104
+ background: transparent;
105
+ border: none;
106
+ font-size: 0.9375rem;
107
+ font-weight: 500;
108
+ color: var(--color-text-strong);
109
+ cursor: pointer;
110
+ text-align: left;
111
+ transition: background 0.15s ease;
112
+ }
113
+
114
+ .accordion-trigger:hover:not([aria-disabled="true"]) {
115
+ background: var(--color-surface);
116
+ }
117
+
118
+ .accordion-trigger-open {
119
+ background: var(--color-surface);
120
+ }
121
+
122
+ .accordion-title {
123
+ flex: 1;
124
+ }
125
+
126
+ .accordion-icon {
127
+ flex-shrink: 0;
128
+ color: var(--color-text);
129
+ transition: transform 0.2s ease;
130
+ }
131
+
132
+ .accordion-icon-open {
133
+ transform: rotate(180deg);
134
+ }
135
+
136
+ .accordion-content {
137
+ overflow: hidden;
138
+ }
139
+
140
+ .accordion-body {
141
+ padding: 0 1rem 1rem 1rem;
142
+ font-size: 0.875rem;
143
+ color: var(--color-text);
144
+ line-height: 1.6;
145
+ }
146
+ </style>
@@ -0,0 +1,239 @@
1
+ <!--
2
+ @component Tabs
3
+
4
+ Tabbed navigation component.
5
+
6
+ @example Basic usage
7
+ <Tabs
8
+ tabs={[
9
+ { id: 'tab1', label: 'Overview' },
10
+ { id: 'tab2', label: 'Settings' },
11
+ { id: 'tab3', label: 'Analytics' }
12
+ ]}
13
+ bind:active={activeTab}
14
+ />
15
+
16
+ @example With icons
17
+ <Tabs
18
+ tabs={[
19
+ { id: 'home', label: 'Home', icon: HomeIcon },
20
+ { id: 'settings', label: 'Settings', icon: SettingsIcon }
21
+ ]}
22
+ />
23
+ -->
24
+ <script>
25
+ let {
26
+ tabs = [],
27
+ active = $bindable(null),
28
+ variant = 'default',
29
+ size = 'md',
30
+ fullWidth = false,
31
+ onchange = () => {},
32
+ class: className = ''
33
+ } = $props();
34
+
35
+ // Initialize active to first tab if not set
36
+ $effect(() => {
37
+ if (active === null && tabs.length > 0) {
38
+ active = tabs[0].id;
39
+ }
40
+ });
41
+
42
+ function selectTab(tab) {
43
+ if (tab.disabled) return;
44
+ active = tab.id;
45
+ onchange(tab);
46
+ }
47
+
48
+ function handleKeydown(e, tab, index) {
49
+ let nextIndex = index;
50
+
51
+ if (e.key === 'ArrowRight') {
52
+ nextIndex = (index + 1) % tabs.length;
53
+ } else if (e.key === 'ArrowLeft') {
54
+ nextIndex = (index - 1 + tabs.length) % tabs.length;
55
+ } else if (e.key === 'Home') {
56
+ nextIndex = 0;
57
+ } else if (e.key === 'End') {
58
+ nextIndex = tabs.length - 1;
59
+ } else {
60
+ return;
61
+ }
62
+
63
+ e.preventDefault();
64
+ const nextTab = tabs[nextIndex];
65
+ if (!nextTab.disabled) {
66
+ selectTab(nextTab);
67
+ // Focus the next tab button
68
+ const buttons = e.currentTarget.parentElement.querySelectorAll('[role="tab"]');
69
+ buttons[nextIndex]?.focus();
70
+ }
71
+ }
72
+ </script>
73
+
74
+ <div
75
+ class="tabs tabs-{variant} tabs-{size} {className}"
76
+ class:tabs-full-width={fullWidth}
77
+ role="tablist"
78
+ >
79
+ {#each tabs as tab, index}
80
+ <button
81
+ type="button"
82
+ role="tab"
83
+ class="tab"
84
+ class:tab-active={active === tab.id}
85
+ class:tab-disabled={tab.disabled}
86
+ aria-selected={active === tab.id}
87
+ aria-disabled={tab.disabled}
88
+ tabindex={active === tab.id ? 0 : -1}
89
+ onclick={() => selectTab(tab)}
90
+ onkeydown={(e) => handleKeydown(e, tab, index)}
91
+ >
92
+ {#if tab.icon}
93
+ <span class="tab-icon">
94
+ <svelte:component this={tab.icon} size={16} />
95
+ </span>
96
+ {/if}
97
+ {#if tab.label}
98
+ <span class="tab-label">{tab.label}</span>
99
+ {/if}
100
+ {#if tab.badge !== undefined}
101
+ <span class="tab-badge">{tab.badge}</span>
102
+ {/if}
103
+ </button>
104
+ {/each}
105
+ </div>
106
+
107
+ <style>
108
+ .tabs {
109
+ display: inline-flex;
110
+ gap: 0.25rem;
111
+ background: var(--color-surface);
112
+ border-radius: 0.5rem;
113
+ padding: 0.25rem;
114
+ }
115
+
116
+ .tabs-full-width {
117
+ display: flex;
118
+ width: 100%;
119
+ }
120
+
121
+ .tabs-full-width .tab {
122
+ flex: 1;
123
+ justify-content: center;
124
+ }
125
+
126
+ .tab {
127
+ display: inline-flex;
128
+ align-items: center;
129
+ gap: 0.5rem;
130
+ padding: 0.5rem 1rem;
131
+ background: transparent;
132
+ border: none;
133
+ border-radius: 0.375rem;
134
+ font-size: 0.875rem;
135
+ font-weight: 500;
136
+ color: var(--color-text);
137
+ cursor: pointer;
138
+ transition: all 0.15s ease;
139
+ white-space: nowrap;
140
+ }
141
+
142
+ /* Size variants */
143
+ .tabs-sm .tab {
144
+ padding: 0.375rem 0.75rem;
145
+ font-size: 0.75rem;
146
+ }
147
+
148
+ .tabs-lg .tab {
149
+ padding: 0.625rem 1.25rem;
150
+ font-size: 1rem;
151
+ }
152
+
153
+ .tab:hover:not(.tab-disabled) {
154
+ color: var(--color-text-strong);
155
+ background: var(--color-surface-alt);
156
+ }
157
+
158
+ .tab:focus-visible {
159
+ outline: none;
160
+ box-shadow: 0 0 0 2px var(--color-primary);
161
+ }
162
+
163
+ .tab-active {
164
+ background: var(--color-surface-alt);
165
+ color: var(--color-text-strong);
166
+ box-shadow: var(--shadow-sm);
167
+ }
168
+
169
+ .tab-disabled {
170
+ opacity: 0.5;
171
+ cursor: not-allowed;
172
+ }
173
+
174
+ .tab-icon {
175
+ display: flex;
176
+ align-items: center;
177
+ }
178
+
179
+ .tab-badge {
180
+ display: inline-flex;
181
+ align-items: center;
182
+ justify-content: center;
183
+ min-width: 1.25rem;
184
+ height: 1.25rem;
185
+ padding: 0 0.375rem;
186
+ background: var(--color-border);
187
+ border-radius: 9999px;
188
+ font-size: 0.75rem;
189
+ font-weight: 600;
190
+ }
191
+
192
+ .tab-active .tab-badge {
193
+ background: var(--color-primary);
194
+ color: var(--color-text-strong);
195
+ }
196
+
197
+ /* Underline variant */
198
+ .tabs-underline {
199
+ background: transparent;
200
+ padding: 0;
201
+ gap: 0;
202
+ border-bottom: 1px solid var(--color-border);
203
+ }
204
+
205
+ .tabs-underline .tab {
206
+ border-radius: 0;
207
+ margin-bottom: -1px;
208
+ border-bottom: 2px solid transparent;
209
+ }
210
+
211
+ .tabs-underline .tab-active {
212
+ background: transparent;
213
+ box-shadow: none;
214
+ border-bottom-color: var(--color-primary);
215
+ color: var(--color-primary);
216
+ }
217
+
218
+ .tabs-underline .tab:hover:not(.tab-disabled) {
219
+ background: transparent;
220
+ border-bottom-color: var(--color-border);
221
+ }
222
+
223
+ /* Pills variant */
224
+ .tabs-pills {
225
+ background: transparent;
226
+ padding: 0;
227
+ gap: 0.5rem;
228
+ }
229
+
230
+ .tabs-pills .tab {
231
+ border: 1px solid var(--color-border);
232
+ }
233
+
234
+ .tabs-pills .tab-active {
235
+ background: var(--color-primary);
236
+ border-color: var(--color-primary);
237
+ color: var(--color-text-strong);
238
+ }
239
+ </style>
@@ -0,0 +1,272 @@
1
+ <!--
2
+ @component ConfirmDialog
3
+
4
+ A confirmation dialog with semantic variants.
5
+
6
+ @example Danger confirmation
7
+ <ConfirmDialog
8
+ bind:open={showDelete}
9
+ title="Delete Item"
10
+ message="Are you sure you want to delete this item? This action cannot be undone."
11
+ variant="danger"
12
+ confirmText="Delete"
13
+ onconfirm={handleDelete}
14
+ />
15
+
16
+ @example Warning confirmation
17
+ <ConfirmDialog
18
+ bind:open={showWarning}
19
+ title="Unsaved Changes"
20
+ message="You have unsaved changes. Are you sure you want to leave?"
21
+ variant="warning"
22
+ confirmText="Leave"
23
+ cancelText="Stay"
24
+ />
25
+
26
+ @example With custom icon
27
+ <ConfirmDialog bind:open={show} title="Confirm">
28
+ {#snippet icon()}
29
+ <CustomIcon size={24} />
30
+ {/snippet}
31
+ </ConfirmDialog>
32
+ -->
33
+ <script>
34
+ import Button from '../primitives/Button.svelte';
35
+ import { escapeKey } from '../../actions/index.js';
36
+
37
+ let {
38
+ open = $bindable(false),
39
+ title = 'Confirm Action',
40
+ message = 'Are you sure you want to proceed?',
41
+ confirmText = 'Confirm',
42
+ cancelText = 'Cancel',
43
+ variant = 'danger',
44
+ class: className = '',
45
+ icon,
46
+ onconfirm,
47
+ oncancel
48
+ } = $props();
49
+
50
+ const variantConfig = $derived({
51
+ danger: {
52
+ iconColor: 'var(--color-error)',
53
+ iconBg: 'color-mix(in srgb, var(--color-error) 10%, transparent)',
54
+ buttonVariant: 'danger'
55
+ },
56
+ warning: {
57
+ iconColor: 'var(--color-warning)',
58
+ iconBg: 'color-mix(in srgb, var(--color-warning) 10%, transparent)',
59
+ buttonVariant: 'primary'
60
+ },
61
+ success: {
62
+ iconColor: 'var(--color-success)',
63
+ iconBg: 'color-mix(in srgb, var(--color-success) 10%, transparent)',
64
+ buttonVariant: 'success'
65
+ },
66
+ info: {
67
+ iconColor: 'var(--color-primary)',
68
+ iconBg: 'color-mix(in srgb, var(--color-primary) 10%, transparent)',
69
+ buttonVariant: 'primary'
70
+ }
71
+ }[variant] || {
72
+ iconColor: 'var(--color-primary)',
73
+ iconBg: 'color-mix(in srgb, var(--color-primary) 10%, transparent)',
74
+ buttonVariant: 'primary'
75
+ });
76
+
77
+ function handleConfirm() {
78
+ onconfirm?.();
79
+ open = false;
80
+ }
81
+
82
+ function handleCancel() {
83
+ oncancel?.();
84
+ open = false;
85
+ }
86
+
87
+ function handleBackdropClick(e) {
88
+ if (e.target === e.currentTarget) {
89
+ handleCancel();
90
+ }
91
+ }
92
+ </script>
93
+
94
+ {#if open}
95
+ <div
96
+ class="confirm-backdrop {className}"
97
+ onclick={handleBackdropClick}
98
+ use:escapeKey={handleCancel}
99
+ role="dialog"
100
+ aria-modal="true"
101
+ aria-labelledby="confirm-title"
102
+ >
103
+ <div class="confirm-dialog">
104
+ <button
105
+ type="button"
106
+ class="confirm-close"
107
+ onclick={handleCancel}
108
+ aria-label="Close"
109
+ >
110
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
111
+ <line x1="18" y1="6" x2="6" y2="18"></line>
112
+ <line x1="6" y1="6" x2="18" y2="18"></line>
113
+ </svg>
114
+ </button>
115
+
116
+ <div class="confirm-content">
117
+ <div class="confirm-header">
118
+ <div
119
+ class="confirm-icon"
120
+ style="background: {variantConfig.iconBg}; color: {variantConfig.iconColor};"
121
+ >
122
+ {#if icon}
123
+ {@render icon()}
124
+ {:else if variant === 'danger'}
125
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
126
+ <polyline points="3 6 5 6 21 6"></polyline>
127
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
128
+ </svg>
129
+ {:else if variant === 'warning'}
130
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
131
+ <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
132
+ <line x1="12" y1="9" x2="12" y2="13"></line>
133
+ <line x1="12" y1="17" x2="12.01" y2="17"></line>
134
+ </svg>
135
+ {:else if variant === 'success'}
136
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
137
+ <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
138
+ <polyline points="22 4 12 14.01 9 11.01"></polyline>
139
+ </svg>
140
+ {:else}
141
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
142
+ <circle cx="12" cy="12" r="10"></circle>
143
+ <line x1="12" y1="16" x2="12" y2="12"></line>
144
+ <line x1="12" y1="8" x2="12.01" y2="8"></line>
145
+ </svg>
146
+ {/if}
147
+ </div>
148
+
149
+ <div class="confirm-text">
150
+ <h3 id="confirm-title" class="confirm-title">{title}</h3>
151
+ <p class="confirm-message">{message}</p>
152
+ </div>
153
+ </div>
154
+
155
+ <div class="confirm-actions">
156
+ <Button variant="ghost" size="sm" onclick={handleCancel}>
157
+ {cancelText}
158
+ </Button>
159
+ <Button variant={variantConfig.buttonVariant} size="sm" onclick={handleConfirm}>
160
+ {confirmText}
161
+ </Button>
162
+ </div>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ {/if}
167
+
168
+ <style>
169
+ .confirm-backdrop {
170
+ position: fixed;
171
+ inset: 0;
172
+ z-index: 100;
173
+ display: flex;
174
+ align-items: center;
175
+ justify-content: center;
176
+ }
177
+
178
+ .confirm-backdrop::before {
179
+ content: '';
180
+ position: absolute;
181
+ inset: 0;
182
+ background: color-mix(in srgb, var(--color-bg) 80%, transparent);
183
+ backdrop-filter: blur(8px);
184
+ }
185
+
186
+ .confirm-dialog {
187
+ position: relative;
188
+ background: var(--color-surface);
189
+ border: 1px solid var(--color-border-muted);
190
+ border-radius: var(--radius-xl);
191
+ box-shadow: var(--shadow-2xl);
192
+ width: 100%;
193
+ max-width: 28rem;
194
+ margin: var(--space-4);
195
+ animation: dialog-enter 0.2s ease-out;
196
+ }
197
+
198
+ @keyframes dialog-enter {
199
+ from {
200
+ opacity: 0;
201
+ transform: scale(0.95) translateY(10px);
202
+ }
203
+ to {
204
+ opacity: 1;
205
+ transform: scale(1) translateY(0);
206
+ }
207
+ }
208
+
209
+ .confirm-close {
210
+ position: absolute;
211
+ top: var(--space-4);
212
+ right: var(--space-4);
213
+ padding: var(--space-1);
214
+ background: transparent;
215
+ border: none;
216
+ border-radius: var(--radius-lg);
217
+ color: var(--color-text-muted);
218
+ cursor: pointer;
219
+ transition: background 0.15s ease, color 0.15s ease;
220
+ }
221
+
222
+ .confirm-close:hover {
223
+ background: var(--color-surface-hover);
224
+ color: var(--color-text);
225
+ }
226
+
227
+ .confirm-content {
228
+ padding: var(--space-6);
229
+ }
230
+
231
+ .confirm-header {
232
+ display: flex;
233
+ align-items: flex-start;
234
+ gap: var(--space-4);
235
+ }
236
+
237
+ .confirm-icon {
238
+ flex-shrink: 0;
239
+ display: flex;
240
+ align-items: center;
241
+ justify-content: center;
242
+ width: 3rem;
243
+ height: 3rem;
244
+ border-radius: var(--radius-lg);
245
+ }
246
+
247
+ .confirm-text {
248
+ flex: 1;
249
+ padding-top: var(--space-1);
250
+ }
251
+
252
+ .confirm-title {
253
+ margin: 0 0 var(--space-2) 0;
254
+ font-size: var(--text-lg);
255
+ font-weight: 600;
256
+ color: var(--color-text-strong);
257
+ }
258
+
259
+ .confirm-message {
260
+ margin: 0;
261
+ font-size: var(--text-sm);
262
+ color: var(--color-text-muted);
263
+ line-height: 1.5;
264
+ }
265
+
266
+ .confirm-actions {
267
+ display: flex;
268
+ gap: var(--space-3);
269
+ justify-content: flex-end;
270
+ margin-top: var(--space-6);
271
+ }
272
+ </style>