@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,143 @@
1
+ <!--
2
+ @component Radio
3
+
4
+ A radio button input. Use within RadioGroup for proper grouping.
5
+
6
+ @example
7
+ <Radio value="option1" label="Option 1" />
8
+ -->
9
+ <script>
10
+ import { getContext } from 'svelte';
11
+ import { cn } from '../../utils/cn.svelte.js';
12
+
13
+ let {
14
+ value,
15
+ label = '',
16
+ description = '',
17
+ disabled = false,
18
+ id,
19
+ class: className = '',
20
+ ...rest
21
+ } = $props();
22
+
23
+ const group = getContext('radioGroup');
24
+
25
+ const radioId = id || `radio-${Math.random().toString(36).slice(2, 9)}`;
26
+ const isChecked = $derived(group?.value === value);
27
+ const isDisabled = $derived(disabled || group?.disabled);
28
+ const name = $derived(group?.name || '');
29
+
30
+ function handleChange() {
31
+ if (!isDisabled && group) {
32
+ group.setValue(value);
33
+ }
34
+ }
35
+ </script>
36
+
37
+ <label
38
+ class={cn('radio-label', isDisabled && 'radio-disabled', className)}
39
+ for={radioId}
40
+ >
41
+ <input
42
+ type="radio"
43
+ id={radioId}
44
+ {name}
45
+ {value}
46
+ checked={isChecked}
47
+ disabled={isDisabled}
48
+ onchange={handleChange}
49
+ class="radio-input"
50
+ {...rest}
51
+ />
52
+ <span class="radio-control"></span>
53
+ {#if label || description}
54
+ <span class="radio-content">
55
+ {#if label}
56
+ <span class="radio-text">{label}</span>
57
+ {/if}
58
+ {#if description}
59
+ <span class="radio-description">{description}</span>
60
+ {/if}
61
+ </span>
62
+ {/if}
63
+ </label>
64
+
65
+ <style>
66
+ .radio-label {
67
+ display: inline-flex;
68
+ align-items: flex-start;
69
+ gap: var(--space-2);
70
+ cursor: pointer;
71
+ user-select: none;
72
+ }
73
+
74
+ .radio-disabled {
75
+ opacity: 0.5;
76
+ cursor: not-allowed;
77
+ }
78
+
79
+ .radio-input {
80
+ position: absolute;
81
+ opacity: 0;
82
+ width: 0;
83
+ height: 0;
84
+ }
85
+
86
+ .radio-control {
87
+ flex-shrink: 0;
88
+ width: 1.125rem;
89
+ height: 1.125rem;
90
+ margin-top: 0.125rem;
91
+ border: 2px solid var(--color-border);
92
+ border-radius: 50%;
93
+ background: var(--color-bg);
94
+ transition: var(--transition-colors);
95
+ display: flex;
96
+ align-items: center;
97
+ justify-content: center;
98
+ }
99
+
100
+ .radio-control::after {
101
+ content: '';
102
+ width: 0.5rem;
103
+ height: 0.5rem;
104
+ border-radius: 50%;
105
+ background: var(--color-primary);
106
+ transform: scale(0);
107
+ transition: transform 0.15s ease-out;
108
+ }
109
+
110
+ .radio-input:checked + .radio-control {
111
+ border-color: var(--color-primary);
112
+ }
113
+
114
+ .radio-input:checked + .radio-control::after {
115
+ transform: scale(1);
116
+ }
117
+
118
+ .radio-input:focus-visible + .radio-control {
119
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 20%, transparent);
120
+ }
121
+
122
+ .radio-label:hover:not(.radio-disabled) .radio-control {
123
+ border-color: var(--color-primary);
124
+ }
125
+
126
+ .radio-content {
127
+ display: flex;
128
+ flex-direction: column;
129
+ gap: 0.125rem;
130
+ }
131
+
132
+ .radio-text {
133
+ font-size: var(--text-sm);
134
+ color: var(--color-text-strong);
135
+ line-height: 1.4;
136
+ }
137
+
138
+ .radio-description {
139
+ font-size: var(--text-xs);
140
+ color: var(--color-text-muted);
141
+ line-height: 1.4;
142
+ }
143
+ </style>
@@ -0,0 +1,62 @@
1
+ <!--
2
+ @component RadioGroup
3
+
4
+ A group container for Radio buttons.
5
+
6
+ @example
7
+ <RadioGroup bind:value={selected} name="options">
8
+ <Radio value="a" label="Option A" />
9
+ <Radio value="b" label="Option B" />
10
+ <Radio value="c" label="Option C" />
11
+ </RadioGroup>
12
+ -->
13
+ <script>
14
+ import { setContext } from 'svelte';
15
+
16
+ let {
17
+ value = $bindable(null),
18
+ name = '',
19
+ disabled = false,
20
+ orientation = 'vertical',
21
+ children,
22
+ onchange,
23
+ class: className = ''
24
+ } = $props();
25
+
26
+ function setValue(newValue) {
27
+ value = newValue;
28
+ onchange?.({ target: { name, value: newValue } });
29
+ }
30
+
31
+ setContext('radioGroup', {
32
+ get value() { return value; },
33
+ get name() { return name; },
34
+ get disabled() { return disabled; },
35
+ setValue
36
+ });
37
+ </script>
38
+
39
+ <div
40
+ class="radio-group radio-group-{orientation} {className}"
41
+ role="radiogroup"
42
+ aria-disabled={disabled}
43
+ >
44
+ {@render children?.()}
45
+ </div>
46
+
47
+ <style>
48
+ .radio-group {
49
+ display: flex;
50
+ }
51
+
52
+ .radio-group-vertical {
53
+ flex-direction: column;
54
+ gap: var(--space-2);
55
+ }
56
+
57
+ .radio-group-horizontal {
58
+ flex-direction: row;
59
+ flex-wrap: wrap;
60
+ gap: var(--space-4);
61
+ }
62
+ </style>
@@ -0,0 +1,212 @@
1
+ <!--
2
+ @component RangeSlider
3
+
4
+ A range slider input for selecting numeric values.
5
+
6
+ @example
7
+ <RangeSlider bind:value={volume} min={0} max={100} />
8
+
9
+ @example With label and formatting
10
+ <RangeSlider
11
+ bind:value={price}
12
+ min={0}
13
+ max={1000}
14
+ label="Price"
15
+ formatValue={(v) => `$${v}`}
16
+ />
17
+ -->
18
+ <script>
19
+ let {
20
+ value = $bindable(50),
21
+ min = 0,
22
+ max = 100,
23
+ step = 1,
24
+ label = '',
25
+ disabled = false,
26
+ showValues = true,
27
+ showCurrentValue = true,
28
+ formatValue = (val) => val.toString(),
29
+ size = 'md',
30
+ class: className = '',
31
+ id,
32
+ oninput,
33
+ onchange,
34
+ ...rest
35
+ } = $props();
36
+
37
+ const inputId = id || `slider-${Math.random().toString(36).slice(2, 9)}`;
38
+ const percentage = $derived(((value - min) / (max - min)) * 100);
39
+ </script>
40
+
41
+ <div class="slider-container slider-{size} {className}">
42
+ {#if label}
43
+ <label class="slider-label" for={inputId}>{label}</label>
44
+ {/if}
45
+
46
+ <div class="slider-wrapper">
47
+ <input
48
+ type="range"
49
+ id={inputId}
50
+ bind:value
51
+ {min}
52
+ {max}
53
+ {step}
54
+ {disabled}
55
+ class="slider"
56
+ style="--percentage: {percentage}%"
57
+ {oninput}
58
+ {onchange}
59
+ {...rest}
60
+ />
61
+ <div class="slider-track">
62
+ <div class="slider-fill" style="width: {percentage}%"></div>
63
+ </div>
64
+ </div>
65
+
66
+ {#if showValues}
67
+ <div class="slider-values">
68
+ <span class="slider-min">{formatValue(min)}</span>
69
+ {#if showCurrentValue}
70
+ <span class="slider-current">{formatValue(value)}</span>
71
+ {/if}
72
+ <span class="slider-max">{formatValue(max)}</span>
73
+ </div>
74
+ {/if}
75
+ </div>
76
+
77
+ <style>
78
+ .slider-container {
79
+ width: 100%;
80
+ }
81
+
82
+ .slider-label {
83
+ display: block;
84
+ font-size: var(--text-sm);
85
+ font-weight: 500;
86
+ color: var(--color-text);
87
+ margin-bottom: var(--space-2);
88
+ }
89
+
90
+ .slider-wrapper {
91
+ position: relative;
92
+ width: 100%;
93
+ height: 0.5rem;
94
+ }
95
+
96
+ .slider-sm .slider-wrapper { height: 0.375rem; }
97
+ .slider-lg .slider-wrapper { height: 0.625rem; }
98
+
99
+ .slider {
100
+ position: absolute;
101
+ inset: 0;
102
+ width: 100%;
103
+ height: 100%;
104
+ appearance: none;
105
+ background: transparent;
106
+ cursor: pointer;
107
+ z-index: 10;
108
+ outline: none;
109
+ }
110
+
111
+ .slider:disabled {
112
+ cursor: not-allowed;
113
+ opacity: 0.5;
114
+ }
115
+
116
+ .slider-track {
117
+ position: absolute;
118
+ inset: 0;
119
+ width: 100%;
120
+ height: 100%;
121
+ background: var(--color-surface-alt);
122
+ border-radius: 9999px;
123
+ pointer-events: none;
124
+ }
125
+
126
+ .slider-fill {
127
+ height: 100%;
128
+ background: var(--color-primary);
129
+ border-radius: 9999px;
130
+ }
131
+
132
+ /* Webkit (Chrome, Safari, Edge) */
133
+ .slider::-webkit-slider-thumb {
134
+ appearance: none;
135
+ width: 1.25rem;
136
+ height: 1.25rem;
137
+ border-radius: 0.375rem;
138
+ background: var(--color-bg);
139
+ border: 2px solid var(--color-primary);
140
+ cursor: pointer;
141
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
142
+ margin-top: -0.375rem;
143
+ }
144
+
145
+ .slider-sm .slider::-webkit-slider-thumb {
146
+ width: 1rem;
147
+ height: 1rem;
148
+ margin-top: -0.3125rem;
149
+ }
150
+
151
+ .slider-lg .slider::-webkit-slider-thumb {
152
+ width: 1.5rem;
153
+ height: 1.5rem;
154
+ margin-top: -0.4375rem;
155
+ }
156
+
157
+ .slider::-webkit-slider-thumb:hover {
158
+ transform: scale(1.1);
159
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-primary) 20%, transparent);
160
+ }
161
+
162
+ .slider:active::-webkit-slider-thumb {
163
+ transform: scale(0.95);
164
+ }
165
+
166
+ .slider:focus-visible::-webkit-slider-thumb {
167
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-primary) 30%, transparent);
168
+ }
169
+
170
+ /* Firefox */
171
+ .slider::-moz-range-thumb {
172
+ appearance: none;
173
+ width: 1.25rem;
174
+ height: 1.25rem;
175
+ border-radius: 0.375rem;
176
+ background: var(--color-bg);
177
+ border: 2px solid var(--color-primary);
178
+ cursor: pointer;
179
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
180
+ }
181
+
182
+ .slider::-moz-range-thumb:hover {
183
+ transform: scale(1.1);
184
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-primary) 20%, transparent);
185
+ }
186
+
187
+ .slider::-webkit-slider-runnable-track {
188
+ appearance: none;
189
+ background: transparent;
190
+ height: 100%;
191
+ }
192
+
193
+ .slider::-moz-range-track {
194
+ appearance: none;
195
+ background: transparent;
196
+ height: 100%;
197
+ }
198
+
199
+ .slider-values {
200
+ display: flex;
201
+ justify-content: space-between;
202
+ align-items: center;
203
+ margin-top: var(--space-2);
204
+ font-size: var(--text-xs);
205
+ color: var(--color-text-muted);
206
+ }
207
+
208
+ .slider-current {
209
+ font-weight: 500;
210
+ color: var(--color-text);
211
+ }
212
+ </style>
@@ -0,0 +1,175 @@
1
+ <!--
2
+ @component SearchInput
3
+
4
+ A search input with icon and clearable functionality.
5
+
6
+ @example
7
+ <SearchInput bind:value={query} placeholder="Search..." />
8
+
9
+ @example With loading state
10
+ <SearchInput bind:value={query} loading={isSearching} />
11
+ -->
12
+ <script>
13
+ import { cn } from '../../utils/cn.svelte.js';
14
+
15
+ let {
16
+ value = $bindable(''),
17
+ placeholder = 'Search...',
18
+ disabled = false,
19
+ loading = false,
20
+ size = 'md',
21
+ class: className = '',
22
+ oninput,
23
+ onchange,
24
+ onclear,
25
+ ...rest
26
+ } = $props();
27
+
28
+ function handleClear() {
29
+ value = '';
30
+ onclear?.();
31
+ }
32
+
33
+ function handleKeydown(e) {
34
+ if (e.key === 'Escape' && value) {
35
+ handleClear();
36
+ }
37
+ }
38
+ </script>
39
+
40
+ <div class={cn('search-input', `search-input-${size}`, className)}>
41
+ <span class="search-icon">
42
+ {#if loading}
43
+ <svg class="search-spinner" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
44
+ <circle cx="12" cy="12" r="10" opacity="0.2" />
45
+ <path d="M12 2a10 10 0 0 1 10 10" stroke-linecap="round" />
46
+ </svg>
47
+ {:else}
48
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
49
+ <circle cx="11" cy="11" r="8"></circle>
50
+ <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
51
+ </svg>
52
+ {/if}
53
+ </span>
54
+
55
+ <input
56
+ type="search"
57
+ bind:value
58
+ {placeholder}
59
+ {disabled}
60
+ class="search-field"
61
+ onkeydown={handleKeydown}
62
+ {oninput}
63
+ {onchange}
64
+ {...rest}
65
+ />
66
+
67
+ {#if value && !loading}
68
+ <button
69
+ type="button"
70
+ class="search-clear"
71
+ onclick={handleClear}
72
+ aria-label="Clear search"
73
+ >
74
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
75
+ <line x1="18" y1="6" x2="6" y2="18"></line>
76
+ <line x1="6" y1="6" x2="18" y2="18"></line>
77
+ </svg>
78
+ </button>
79
+ {/if}
80
+ </div>
81
+
82
+ <style>
83
+ .search-input {
84
+ position: relative;
85
+ display: flex;
86
+ align-items: center;
87
+ width: 100%;
88
+ }
89
+
90
+ .search-icon {
91
+ position: absolute;
92
+ left: 0.75rem;
93
+ display: flex;
94
+ align-items: center;
95
+ justify-content: center;
96
+ color: var(--color-text-muted);
97
+ pointer-events: none;
98
+ }
99
+
100
+ .search-spinner {
101
+ animation: spin 1s linear infinite;
102
+ }
103
+
104
+ @keyframes spin {
105
+ from { transform: rotate(0deg); }
106
+ to { transform: rotate(360deg); }
107
+ }
108
+
109
+ .search-field {
110
+ width: 100%;
111
+ padding: var(--space-2) var(--space-3);
112
+ padding-left: 2.5rem;
113
+ padding-right: 2.25rem;
114
+ font-size: var(--text-sm);
115
+ color: var(--color-text-strong);
116
+ background: var(--color-bg);
117
+ border: 1px solid var(--color-border-muted);
118
+ border-radius: var(--radius-lg);
119
+ transition: var(--transition-colors);
120
+ }
121
+
122
+ .search-input-sm .search-field {
123
+ padding: var(--space-1) var(--space-2);
124
+ padding-left: 2rem;
125
+ padding-right: 2rem;
126
+ font-size: var(--text-xs);
127
+ }
128
+
129
+ .search-input-lg .search-field {
130
+ padding: var(--space-3) var(--space-4);
131
+ padding-left: 3rem;
132
+ padding-right: 2.75rem;
133
+ font-size: var(--text-base);
134
+ }
135
+
136
+ .search-field::placeholder {
137
+ color: var(--color-text-muted);
138
+ }
139
+
140
+ .search-field:focus {
141
+ outline: none;
142
+ border-color: var(--color-primary);
143
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 20%, transparent);
144
+ }
145
+
146
+ .search-field:disabled {
147
+ opacity: 0.5;
148
+ cursor: not-allowed;
149
+ }
150
+
151
+ /* Hide native search clear button */
152
+ .search-field::-webkit-search-cancel-button {
153
+ display: none;
154
+ }
155
+
156
+ .search-clear {
157
+ position: absolute;
158
+ right: 0.5rem;
159
+ display: flex;
160
+ align-items: center;
161
+ justify-content: center;
162
+ padding: 0.25rem;
163
+ background: transparent;
164
+ border: none;
165
+ border-radius: var(--radius-sm);
166
+ color: var(--color-text-muted);
167
+ cursor: pointer;
168
+ transition: var(--transition-colors);
169
+ }
170
+
171
+ .search-clear:hover {
172
+ color: var(--color-text);
173
+ background: var(--color-surface-hover);
174
+ }
175
+ </style>