@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
@@ -0,0 +1,121 @@
1
+ <!--
2
+ @component Input
3
+
4
+ A flexible text input component with full browser feature control.
5
+
6
+ @example
7
+ <Input bind:value={email} type="email" placeholder="Enter email" />
8
+
9
+ @example
10
+ // Disable browser autofill (for sensitive fields)
11
+ <Input bind:value={password} type="password" disableBrowserFeatures />
12
+ -->
13
+ <script>
14
+ import { cn } from '../../utils/cn.svelte.js';
15
+
16
+ let {
17
+ value = $bindable(''),
18
+ ref = $bindable(),
19
+ type = 'text',
20
+ placeholder = '',
21
+ disabled = false,
22
+ required = false,
23
+ name = '',
24
+ id = '',
25
+ autocomplete = 'on',
26
+ autocorrect = 'off',
27
+ autocapitalize = 'off',
28
+ spellcheck = 'false',
29
+ maxlength,
30
+ minlength,
31
+ inputmode,
32
+ class: className = '',
33
+ unstyled = false,
34
+ disableBrowserFeatures = false,
35
+ error = false,
36
+ oninput,
37
+ onchange,
38
+ onkeydown,
39
+ onfocus,
40
+ onblur,
41
+ ...rest
42
+ } = $props();
43
+
44
+ const finalAutocomplete = $derived(
45
+ disableBrowserFeatures ? 'new-password' : autocomplete
46
+ );
47
+
48
+ const inputClass = $derived(
49
+ unstyled ? className : cn(
50
+ 'input-base',
51
+ error && 'input-error',
52
+ className
53
+ )
54
+ );
55
+ </script>
56
+
57
+ <input
58
+ class={inputClass}
59
+ bind:this={ref}
60
+ bind:value
61
+ {id}
62
+ {name}
63
+ {type}
64
+ {placeholder}
65
+ {disabled}
66
+ {required}
67
+ {inputmode}
68
+ {maxlength}
69
+ {minlength}
70
+ autocomplete={finalAutocomplete}
71
+ autocorrect={disableBrowserFeatures ? 'off' : autocorrect}
72
+ autocapitalize={disableBrowserFeatures ? 'off' : autocapitalize}
73
+ spellcheck={disableBrowserFeatures ? 'false' : spellcheck}
74
+ data-form-type={disableBrowserFeatures ? 'other' : undefined}
75
+ data-lpignore={disableBrowserFeatures ? 'true' : undefined}
76
+ aria-invalid={error || undefined}
77
+ {oninput}
78
+ {onchange}
79
+ {onkeydown}
80
+ {onfocus}
81
+ {onblur}
82
+ {...rest}
83
+ />
84
+
85
+ <style>
86
+ .input-base {
87
+ width: 100%;
88
+ padding: var(--space-2, 0.5rem) var(--space-3, 0.75rem);
89
+ font-size: var(--text-sm, 0.875rem);
90
+ line-height: var(--leading-normal, 1.5);
91
+ color: var(--color-base07);
92
+ background-color: var(--color-base00);
93
+ border: 1px solid var(--color-base02);
94
+ border-radius: var(--radius-lg, 0.5rem);
95
+ transition: border-color 150ms, box-shadow 150ms;
96
+ }
97
+
98
+ .input-base::placeholder {
99
+ color: var(--color-base04);
100
+ }
101
+
102
+ .input-base:focus {
103
+ outline: none;
104
+ border-color: var(--color-base0D);
105
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-base0D) 20%, transparent);
106
+ }
107
+
108
+ .input-base:disabled {
109
+ opacity: 0.5;
110
+ cursor: not-allowed;
111
+ }
112
+
113
+ .input-error {
114
+ border-color: var(--color-base08);
115
+ }
116
+
117
+ .input-error:focus {
118
+ border-color: var(--color-base08);
119
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-base08) 20%, transparent);
120
+ }
121
+ </style>
@@ -0,0 +1,225 @@
1
+ <!--
2
+ @component NumberInput
3
+
4
+ A number input with increment/decrement buttons.
5
+
6
+ @example
7
+ <NumberInput bind:value={quantity} min={1} max={100} />
8
+
9
+ @example
10
+ // With step
11
+ <NumberInput bind:value={price} step={0.5} min={0} />
12
+ -->
13
+ <script>
14
+ import { cn } from '../../utils/cn.svelte.js';
15
+
16
+ let {
17
+ value = $bindable(0),
18
+ min = -Infinity,
19
+ max = Infinity,
20
+ step = 1,
21
+ placeholder = '',
22
+ disabled = false,
23
+ required = false,
24
+ name = '',
25
+ id = '',
26
+ size = 'md',
27
+ class: className = '',
28
+ error = false,
29
+ oninput,
30
+ onchange,
31
+ ...rest
32
+ } = $props();
33
+
34
+ function ensureNumber(val) {
35
+ const num = parseFloat(val);
36
+ return isNaN(num) ? min > -Infinity ? min : 0 : num;
37
+ }
38
+
39
+ function increment() {
40
+ if (disabled) return;
41
+ const newValue = Math.min(ensureNumber(value) + step, max);
42
+ if (newValue !== value) {
43
+ value = newValue;
44
+ triggerChange();
45
+ }
46
+ }
47
+
48
+ function decrement() {
49
+ if (disabled) return;
50
+ const newValue = Math.max(ensureNumber(value) - step, min);
51
+ if (newValue !== value) {
52
+ value = newValue;
53
+ triggerChange();
54
+ }
55
+ }
56
+
57
+ function handleInput(e) {
58
+ const inputValue = e.target.value;
59
+ if (inputValue === '' || inputValue === '-') {
60
+ value = inputValue;
61
+ oninput?.(e);
62
+ return;
63
+ }
64
+
65
+ const num = parseFloat(inputValue);
66
+ if (!isNaN(num)) {
67
+ value = Math.max(min, Math.min(num, max));
68
+ }
69
+ oninput?.(e);
70
+ }
71
+
72
+ function handleBlur(e) {
73
+ const num = ensureNumber(value);
74
+ value = Math.max(min, Math.min(num, max));
75
+ triggerChange();
76
+ }
77
+
78
+ function triggerChange() {
79
+ onchange?.({ target: { name, value } });
80
+ }
81
+
82
+ function handleKeydown(e) {
83
+ if (e.key === 'ArrowUp') {
84
+ e.preventDefault();
85
+ increment();
86
+ } else if (e.key === 'ArrowDown') {
87
+ e.preventDefault();
88
+ decrement();
89
+ }
90
+ }
91
+ </script>
92
+
93
+ <div class="number-input number-input-{size} {className}" class:number-input-error={error}>
94
+ <button
95
+ type="button"
96
+ class="number-btn number-btn-decrement"
97
+ onclick={decrement}
98
+ disabled={disabled || value <= min}
99
+ aria-label="Decrease value"
100
+ >
101
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
102
+ <line x1="5" y1="12" x2="19" y2="12"></line>
103
+ </svg>
104
+ </button>
105
+
106
+ <input
107
+ {id}
108
+ {name}
109
+ type="text"
110
+ inputmode="decimal"
111
+ class="number-field"
112
+ value={value}
113
+ {placeholder}
114
+ {disabled}
115
+ {required}
116
+ oninput={handleInput}
117
+ onblur={handleBlur}
118
+ onkeydown={handleKeydown}
119
+ aria-invalid={error || undefined}
120
+ {...rest}
121
+ />
122
+
123
+ <button
124
+ type="button"
125
+ class="number-btn number-btn-increment"
126
+ onclick={increment}
127
+ disabled={disabled || value >= max}
128
+ aria-label="Increase value"
129
+ >
130
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
131
+ <line x1="12" y1="5" x2="12" y2="19"></line>
132
+ <line x1="5" y1="12" x2="19" y2="12"></line>
133
+ </svg>
134
+ </button>
135
+ </div>
136
+
137
+ <style>
138
+ .number-input {
139
+ display: inline-flex;
140
+ align-items: stretch;
141
+ width: 100%;
142
+ }
143
+
144
+ .number-input-sm { height: 2rem; }
145
+ .number-input-md { height: 2.25rem; }
146
+ .number-input-lg { height: 2.75rem; }
147
+
148
+ .number-field {
149
+ flex: 1;
150
+ min-width: 0;
151
+ padding: 0 var(--space-3);
152
+ text-align: center;
153
+ font-size: var(--text-sm);
154
+ color: var(--color-base07);
155
+ background: var(--color-base00);
156
+ border-top: 1px solid var(--color-base02);
157
+ border-bottom: 1px solid var(--color-base02);
158
+ border-left: none;
159
+ border-right: none;
160
+ transition: var(--transition-colors);
161
+ font-family: inherit;
162
+ }
163
+
164
+ .number-field:focus {
165
+ outline: none;
166
+ border-color: var(--color-base0D);
167
+ }
168
+
169
+ .number-field:disabled {
170
+ opacity: 0.5;
171
+ cursor: not-allowed;
172
+ }
173
+
174
+ .number-field::placeholder {
175
+ color: var(--color-base04);
176
+ }
177
+
178
+ /* Hide native spinners */
179
+ .number-field::-webkit-outer-spin-button,
180
+ .number-field::-webkit-inner-spin-button {
181
+ -webkit-appearance: none;
182
+ margin: 0;
183
+ }
184
+ .number-field[type="number"] {
185
+ -moz-appearance: textfield;
186
+ }
187
+
188
+ .number-btn {
189
+ display: flex;
190
+ align-items: center;
191
+ justify-content: center;
192
+ padding: 0 var(--space-3);
193
+ color: var(--color-base04);
194
+ background: var(--color-base01);
195
+ border: 1px solid var(--color-base02);
196
+ cursor: pointer;
197
+ transition: var(--transition-colors);
198
+ }
199
+
200
+ .number-btn:hover:not(:disabled) {
201
+ color: var(--color-base07);
202
+ background: var(--color-base02);
203
+ }
204
+
205
+ .number-btn:disabled {
206
+ opacity: 0.5;
207
+ cursor: not-allowed;
208
+ }
209
+
210
+ .number-btn-decrement {
211
+ border-radius: var(--radius-lg) 0 0 var(--radius-lg);
212
+ }
213
+
214
+ .number-btn-increment {
215
+ border-radius: 0 var(--radius-lg) var(--radius-lg) 0;
216
+ }
217
+
218
+ .number-input-error .number-field {
219
+ border-color: var(--color-base08);
220
+ }
221
+
222
+ .number-input-error .number-btn {
223
+ border-color: var(--color-base08);
224
+ }
225
+ </style>
@@ -0,0 +1,169 @@
1
+ <!--
2
+ @component PinInput
3
+
4
+ A PIN/OTP code input with individual digit fields.
5
+
6
+ @example
7
+ <PinInput bind:value={code} length={6} />
8
+
9
+ @example Masked (password style)
10
+ <PinInput bind:value={pin} length={4} masked />
11
+ -->
12
+ <script>
13
+ let {
14
+ value = $bindable(''),
15
+ length = 4,
16
+ masked = false,
17
+ disabled = false,
18
+ size = 'md',
19
+ class: className = '',
20
+ oncomplete,
21
+ ...rest
22
+ } = $props();
23
+
24
+ let inputs = $state([]);
25
+
26
+ // Keep value in sync with inputs
27
+ $effect(() => {
28
+ const chars = value.split('').slice(0, length);
29
+ inputs = Array(length).fill('').map((_, i) => chars[i] || '');
30
+ });
31
+
32
+ function updateValue() {
33
+ value = inputs.join('');
34
+ if (value.length === length) {
35
+ oncomplete?.(value);
36
+ }
37
+ }
38
+
39
+ function handleInput(index, e) {
40
+ const char = e.target.value.slice(-1);
41
+
42
+ if (!/^\d*$/.test(char)) {
43
+ e.target.value = inputs[index];
44
+ return;
45
+ }
46
+
47
+ inputs[index] = char;
48
+ updateValue();
49
+
50
+ // Move to next input
51
+ if (char && index < length - 1) {
52
+ const nextInput = e.target.parentElement.querySelector(`input:nth-child(${index + 2})`);
53
+ nextInput?.focus();
54
+ }
55
+ }
56
+
57
+ function handleKeydown(index, e) {
58
+ if (e.key === 'Backspace' && !inputs[index] && index > 0) {
59
+ const prevInput = e.target.parentElement.querySelector(`input:nth-child(${index})`);
60
+ prevInput?.focus();
61
+ } else if (e.key === 'ArrowLeft' && index > 0) {
62
+ const prevInput = e.target.parentElement.querySelector(`input:nth-child(${index})`);
63
+ prevInput?.focus();
64
+ } else if (e.key === 'ArrowRight' && index < length - 1) {
65
+ const nextInput = e.target.parentElement.querySelector(`input:nth-child(${index + 2})`);
66
+ nextInput?.focus();
67
+ }
68
+ }
69
+
70
+ function handlePaste(e) {
71
+ e.preventDefault();
72
+ const pasted = e.clipboardData.getData('text').replace(/\D/g, '').slice(0, length);
73
+
74
+ for (let i = 0; i < length; i++) {
75
+ inputs[i] = pasted[i] || '';
76
+ }
77
+ updateValue();
78
+
79
+ // Focus appropriate input
80
+ const focusIndex = Math.min(pasted.length, length - 1);
81
+ const input = e.target.parentElement.querySelector(`input:nth-child(${focusIndex + 1})`);
82
+ input?.focus();
83
+ }
84
+
85
+ function handleFocus(e) {
86
+ e.target.select();
87
+ }
88
+ </script>
89
+
90
+ <div class="pin-input pin-input-{size} {className}" {...rest}>
91
+ {#each inputs as digit, index}
92
+ <input
93
+ type={masked ? 'password' : 'text'}
94
+ inputmode="numeric"
95
+ pattern="[0-9]*"
96
+ maxlength="1"
97
+ value={digit}
98
+ {disabled}
99
+ class="pin-field"
100
+ class:pin-field-filled={digit}
101
+ oninput={(e) => handleInput(index, e)}
102
+ onkeydown={(e) => handleKeydown(index, e)}
103
+ onpaste={handlePaste}
104
+ onfocus={handleFocus}
105
+ autocomplete="one-time-code"
106
+ />
107
+ {/each}
108
+ </div>
109
+
110
+ <style>
111
+ .pin-input {
112
+ display: inline-flex;
113
+ gap: var(--space-2);
114
+ }
115
+
116
+ .pin-field {
117
+ width: 2.75rem;
118
+ height: 3rem;
119
+ padding: 0;
120
+ font-size: 1.25rem;
121
+ font-weight: 600;
122
+ text-align: center;
123
+ color: var(--color-base07);
124
+ background: var(--color-base00);
125
+ border: 2px solid var(--color-base02);
126
+ border-radius: var(--radius-lg);
127
+ transition: var(--transition-colors);
128
+ font-family: inherit;
129
+ }
130
+
131
+ .pin-input-sm .pin-field {
132
+ width: 2.25rem;
133
+ height: 2.5rem;
134
+ font-size: 1rem;
135
+ }
136
+
137
+ .pin-input-lg .pin-field {
138
+ width: 3.25rem;
139
+ height: 3.5rem;
140
+ font-size: 1.5rem;
141
+ }
142
+
143
+ .pin-field:focus {
144
+ outline: none;
145
+ border-color: var(--color-base0D);
146
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-base0D) 20%, transparent);
147
+ }
148
+
149
+ .pin-field-filled {
150
+ border-color: var(--color-base0D);
151
+ background: color-mix(in srgb, var(--color-base0D) 5%, transparent);
152
+ }
153
+
154
+ .pin-field:disabled {
155
+ opacity: 0.5;
156
+ cursor: not-allowed;
157
+ }
158
+
159
+ .pin-field::placeholder {
160
+ color: var(--color-base04);
161
+ }
162
+
163
+ /* Hide spin buttons on number input */
164
+ .pin-field::-webkit-outer-spin-button,
165
+ .pin-field::-webkit-inner-spin-button {
166
+ -webkit-appearance: none;
167
+ margin: 0;
168
+ }
169
+ </style>
@@ -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-base03);
92
+ border-radius: 50%;
93
+ background: var(--color-base00);
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-base0D);
106
+ transform: scale(0);
107
+ transition: transform 0.15s ease-out;
108
+ }
109
+
110
+ .radio-input:checked + .radio-control {
111
+ border-color: var(--color-base0D);
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-base0D) 20%, transparent);
120
+ }
121
+
122
+ .radio-label:hover:not(.radio-disabled) .radio-control {
123
+ border-color: var(--color-base0D);
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-base07);
135
+ line-height: 1.4;
136
+ }
137
+
138
+ .radio-description {
139
+ font-size: var(--text-xs);
140
+ color: var(--color-base04);
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>