@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,184 @@
1
+ <!--
2
+ @component IconInput
3
+
4
+ An input wrapper that supports left/right icons, loading states, and clearable functionality.
5
+
6
+ @example Basic with icon
7
+ <IconInput bind:value={email} placeholder="Email">
8
+ {#snippet leftIcon()}
9
+ <MailIcon size={16} />
10
+ {/snippet}
11
+ </IconInput>
12
+
13
+ @example With loading and clearable
14
+ <IconInput
15
+ bind:value={search}
16
+ placeholder="Search..."
17
+ loading
18
+ clearable
19
+ onclear={() => search = ''}
20
+ >
21
+ {#snippet leftIcon()}
22
+ <SearchIcon size={16} />
23
+ {/snippet}
24
+ </IconInput>
25
+ -->
26
+ <script>
27
+ import Input from './Input.svelte';
28
+
29
+ let {
30
+ value = $bindable(''),
31
+ type = 'text',
32
+ placeholder = '',
33
+ disabled = false,
34
+ size = 'md',
35
+ loading = false,
36
+ clearable = false,
37
+ class: className = '',
38
+ leftIcon,
39
+ rightIcon,
40
+ oninput,
41
+ onchange,
42
+ onclear,
43
+ ...rest
44
+ } = $props();
45
+
46
+ function handleClear() {
47
+ value = '';
48
+ onclear?.();
49
+ }
50
+
51
+ const showClearButton = $derived(clearable && value && !loading);
52
+ const hasLeftIcon = $derived(!!leftIcon);
53
+ const hasRightIcon = $derived(!!rightIcon || loading || showClearButton);
54
+ </script>
55
+
56
+ <div class="icon-input icon-input-{size} {className}">
57
+ {#if leftIcon}
58
+ <span class="icon-left">
59
+ {#if loading}
60
+ <svg class="spinner" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
61
+ <circle cx="12" cy="12" r="10" opacity="0.2" />
62
+ <path d="M12 2a10 10 0 0 1 10 10" stroke-linecap="round" />
63
+ </svg>
64
+ {:else}
65
+ {@render leftIcon()}
66
+ {/if}
67
+ </span>
68
+ {/if}
69
+
70
+ <Input
71
+ {type}
72
+ bind:value
73
+ {placeholder}
74
+ {disabled}
75
+ {size}
76
+ class="icon-input-field"
77
+ style="padding-left: {hasLeftIcon ? '2.5rem' : ''}; padding-right: {hasRightIcon ? '2.5rem' : ''};"
78
+ {oninput}
79
+ {onchange}
80
+ {...rest}
81
+ />
82
+
83
+ {#if showClearButton}
84
+ <button
85
+ type="button"
86
+ class="icon-clear"
87
+ onclick={handleClear}
88
+ aria-label="Clear input"
89
+ >
90
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
91
+ <line x1="18" y1="6" x2="6" y2="18"></line>
92
+ <line x1="6" y1="6" x2="18" y2="18"></line>
93
+ </svg>
94
+ </button>
95
+ {:else if rightIcon && !loading}
96
+ <span class="icon-right">
97
+ {@render rightIcon()}
98
+ </span>
99
+ {:else if loading && !leftIcon}
100
+ <span class="icon-right">
101
+ <svg class="spinner" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
102
+ <circle cx="12" cy="12" r="10" opacity="0.2" />
103
+ <path d="M12 2a10 10 0 0 1 10 10" stroke-linecap="round" />
104
+ </svg>
105
+ </span>
106
+ {/if}
107
+ </div>
108
+
109
+ <style>
110
+ .icon-input {
111
+ position: relative;
112
+ display: inline-flex;
113
+ align-items: center;
114
+ width: 100%;
115
+ }
116
+
117
+ .icon-input :global(.icon-input-field) {
118
+ width: 100%;
119
+ }
120
+
121
+ .icon-left,
122
+ .icon-right {
123
+ position: absolute;
124
+ display: flex;
125
+ align-items: center;
126
+ justify-content: center;
127
+ color: var(--color-text-muted);
128
+ pointer-events: none;
129
+ }
130
+
131
+ .icon-left {
132
+ left: 0.75rem;
133
+ }
134
+
135
+ .icon-right {
136
+ right: 0.75rem;
137
+ }
138
+
139
+ .icon-input-sm .icon-left,
140
+ .icon-input-sm .icon-right {
141
+ left: 0.625rem;
142
+ }
143
+
144
+ .icon-input-sm .icon-right {
145
+ right: 0.625rem;
146
+ }
147
+
148
+ .icon-input-lg .icon-left {
149
+ left: 1rem;
150
+ }
151
+
152
+ .icon-input-lg .icon-right {
153
+ right: 1rem;
154
+ }
155
+
156
+ .spinner {
157
+ animation: spin 1s linear infinite;
158
+ }
159
+
160
+ @keyframes spin {
161
+ from { transform: rotate(0deg); }
162
+ to { transform: rotate(360deg); }
163
+ }
164
+
165
+ .icon-clear {
166
+ position: absolute;
167
+ right: 0.5rem;
168
+ display: flex;
169
+ align-items: center;
170
+ justify-content: center;
171
+ padding: 0.25rem;
172
+ background: transparent;
173
+ border: none;
174
+ border-radius: var(--radius-sm);
175
+ color: var(--color-text-muted);
176
+ cursor: pointer;
177
+ transition: var(--transition-colors);
178
+ }
179
+
180
+ .icon-clear:hover {
181
+ color: var(--color-text);
182
+ background: var(--color-surface-hover);
183
+ }
184
+ </style>
@@ -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) var(--space-3);
89
+ font-size: var(--text-sm);
90
+ line-height: var(--leading-normal);
91
+ color: var(--color-text-strong);
92
+ background-color: var(--color-bg);
93
+ border: 1px solid var(--color-border-muted);
94
+ border-radius: var(--radius-lg);
95
+ transition: var(--transition-colors);
96
+ }
97
+
98
+ .input-base::placeholder {
99
+ color: var(--color-text-muted);
100
+ }
101
+
102
+ .input-base:focus {
103
+ outline: none;
104
+ border-color: var(--color-primary);
105
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 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-error);
115
+ }
116
+
117
+ .input-error:focus {
118
+ border-color: var(--color-error);
119
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-error) 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-text-strong);
155
+ background: var(--color-bg);
156
+ border-top: 1px solid var(--color-border-muted);
157
+ border-bottom: 1px solid var(--color-border-muted);
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-primary);
167
+ }
168
+
169
+ .number-field:disabled {
170
+ opacity: 0.5;
171
+ cursor: not-allowed;
172
+ }
173
+
174
+ .number-field::placeholder {
175
+ color: var(--color-text-muted);
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-text-muted);
194
+ background: var(--color-surface);
195
+ border: 1px solid var(--color-border-muted);
196
+ cursor: pointer;
197
+ transition: var(--transition-colors);
198
+ }
199
+
200
+ .number-btn:hover:not(:disabled) {
201
+ color: var(--color-text-strong);
202
+ background: var(--color-surface-hover);
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-error);
220
+ }
221
+
222
+ .number-input-error .number-btn {
223
+ border-color: var(--color-error);
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-text-strong);
124
+ background: var(--color-bg);
125
+ border: 2px solid var(--color-border-muted);
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-primary);
146
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 20%, transparent);
147
+ }
148
+
149
+ .pin-field-filled {
150
+ border-color: var(--color-primary);
151
+ background: color-mix(in srgb, var(--color-primary) 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-text-muted);
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>