@urbicon-ui/blocks 6.36.0 → 6.37.1
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.
- package/dist/components/PinInput/PinInput.svelte +325 -0
- package/dist/components/PinInput/PinInput.svelte.d.ts +4 -0
- package/dist/components/PinInput/index.d.ts +98 -0
- package/dist/components/PinInput/index.js +2 -0
- package/dist/components/PinInput/pin-input.variants.d.ts +96 -0
- package/dist/components/PinInput/pin-input.variants.js +111 -0
- package/dist/components/QRCode/QRCode.svelte +107 -0
- package/dist/components/QRCode/QRCode.svelte.d.ts +4 -0
- package/dist/components/QRCode/index.d.ts +74 -0
- package/dist/components/QRCode/index.js +3 -0
- package/dist/components/QRCode/qr-code.variants.d.ts +25 -0
- package/dist/components/QRCode/qr-code.variants.js +30 -0
- package/dist/components/QRCode/qr-encode.d.ts +22 -0
- package/dist/components/QRCode/qr-encode.js +528 -0
- package/dist/components/TimeInput/TimeInput.svelte +536 -0
- package/dist/components/TimeInput/TimeInput.svelte.d.ts +4 -0
- package/dist/components/TimeInput/index.d.ts +93 -0
- package/dist/components/TimeInput/index.js +2 -0
- package/dist/components/TimeInput/time-input.variants.d.ts +131 -0
- package/dist/components/TimeInput/time-input.variants.js +121 -0
- package/dist/components/index.d.ts +6 -0
- package/dist/components/index.js +3 -0
- package/dist/i18n/index.d.ts +18 -0
- package/dist/translations/de.d.ts +6 -0
- package/dist/translations/de.js +6 -0
- package/dist/translations/en.d.ts +6 -0
- package/dist/translations/en.js +6 -0
- package/package.json +3 -3
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
3
|
+
import { useBlocksI18n } from '../..';
|
|
4
|
+
import { getBlocksConfig, resolveSlotClasses } from '../../provider';
|
|
5
|
+
import { getTierContext } from '../../utils';
|
|
6
|
+
import type { PinInputProps } from './index';
|
|
7
|
+
import { pinInputVariants, type PinInputVariants } from './pin-input.variants';
|
|
8
|
+
|
|
9
|
+
const bt = useBlocksI18n();
|
|
10
|
+
|
|
11
|
+
let {
|
|
12
|
+
value = $bindable(''),
|
|
13
|
+
length = 6,
|
|
14
|
+
type = 'numeric',
|
|
15
|
+
mask = false,
|
|
16
|
+
placeholder = '',
|
|
17
|
+
uppercase = false,
|
|
18
|
+
autoFocus = false,
|
|
19
|
+
separator,
|
|
20
|
+
groupSize = 3,
|
|
21
|
+
tier,
|
|
22
|
+
variant = 'outlined',
|
|
23
|
+
size = 'md',
|
|
24
|
+
intent = 'default',
|
|
25
|
+
disabled = false,
|
|
26
|
+
readonly = false,
|
|
27
|
+
required = false,
|
|
28
|
+
label,
|
|
29
|
+
helper,
|
|
30
|
+
error,
|
|
31
|
+
onValueChange,
|
|
32
|
+
onComplete,
|
|
33
|
+
name,
|
|
34
|
+
class: className = '',
|
|
35
|
+
unstyled: unstyledProp = false,
|
|
36
|
+
slotClasses: slotClassesProp = {},
|
|
37
|
+
preset,
|
|
38
|
+
id: idProp,
|
|
39
|
+
'aria-label': ariaLabel
|
|
40
|
+
}: PinInputProps = $props();
|
|
41
|
+
|
|
42
|
+
const tierCtx = getTierContext();
|
|
43
|
+
const effectiveTier = $derived(tier ?? tierCtx?.tier ?? 'modify');
|
|
44
|
+
|
|
45
|
+
const blocksConfig = getBlocksConfig();
|
|
46
|
+
const unstyled = $derived(unstyledProp || blocksConfig?.unstyled || false);
|
|
47
|
+
|
|
48
|
+
const propsId = $props.id();
|
|
49
|
+
const fieldId = $derived(idProp ?? `pininput-${propsId}`);
|
|
50
|
+
const labelId = $derived(`${fieldId}-label`);
|
|
51
|
+
const messageId = $derived(`${fieldId}-message`);
|
|
52
|
+
const describedBy = $derived(error || helper ? messageId : undefined);
|
|
53
|
+
|
|
54
|
+
// Allowed-character filter. `numeric` keeps digits; `alphanumeric` also keeps
|
|
55
|
+
// ASCII letters and (optionally) uppercases them. Applied to typed input,
|
|
56
|
+
// pasted text, and externally-set values alike, so the model never holds a
|
|
57
|
+
// character a cell could not have produced.
|
|
58
|
+
function sanitize(raw: string): string {
|
|
59
|
+
const re = type === 'numeric' ? /[^0-9]/g : /[^0-9a-zA-Z]/g;
|
|
60
|
+
const cleaned = raw.replace(re, '');
|
|
61
|
+
return uppercase && type !== 'numeric' ? cleaned.toUpperCase() : cleaned;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function toCells(raw: string): string[] {
|
|
65
|
+
const chars = sanitize(raw).slice(0, length).split('');
|
|
66
|
+
return Array.from({ length }, (_, i) => chars[i] ?? '');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// `cells` is the DOM source of truth; `value` (bindable) mirrors its join.
|
|
70
|
+
// Empty cells collapse in the join — fine for OTP, where the value is the
|
|
71
|
+
// sequence of entered characters, not a positional map.
|
|
72
|
+
let cells = $state<string[]>(toCells(value ?? ''));
|
|
73
|
+
let inputs = $state<(HTMLInputElement | undefined)[]>([]);
|
|
74
|
+
let wasComplete = $state(false);
|
|
75
|
+
|
|
76
|
+
// External value / length / filter changes re-seed the buffer. Writing back a
|
|
77
|
+
// normalized `value` keeps an out-of-range or unsanitized prop honest without
|
|
78
|
+
// firing onValueChange (the parent set it deliberately).
|
|
79
|
+
$effect(() => {
|
|
80
|
+
const normalized = toCells(value ?? '').join('');
|
|
81
|
+
if (normalized !== untrack(() => cells.join(''))) {
|
|
82
|
+
cells = toCells(normalized);
|
|
83
|
+
}
|
|
84
|
+
if (normalized !== (value ?? '')) {
|
|
85
|
+
value = normalized;
|
|
86
|
+
}
|
|
87
|
+
// Completeness must track the (possibly externally reset) value — a stale
|
|
88
|
+
// `true` after a parent-side clear would swallow the onComplete of the next
|
|
89
|
+
// single-emit fill (paste/autofill), the standard wrong-code retry flow.
|
|
90
|
+
wasComplete = normalized.length === length;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Stable per-position keys — the index IS the cell identity, but a derived id
|
|
94
|
+
// keeps `{#each}` off a bare index key.
|
|
95
|
+
const cellKeys = $derived(Array.from({ length }, (_, i) => `${fieldId}-cell-${i}`));
|
|
96
|
+
|
|
97
|
+
const variantProps: PinInputVariants = $derived({
|
|
98
|
+
tier: effectiveTier,
|
|
99
|
+
variant,
|
|
100
|
+
size,
|
|
101
|
+
intent,
|
|
102
|
+
disabled: disabled || undefined,
|
|
103
|
+
readonly: readonly || undefined,
|
|
104
|
+
error: !!error || undefined,
|
|
105
|
+
required: required || undefined,
|
|
106
|
+
messageType: error ? 'error' : 'helper'
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const styles = $derived(pinInputVariants(variantProps));
|
|
110
|
+
|
|
111
|
+
const slotClasses = $derived(
|
|
112
|
+
resolveSlotClasses(blocksConfig, 'PinInput', preset, variantProps, slotClassesProp)
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
function cellClass() {
|
|
116
|
+
return unstyled ? (slotClasses?.cell ?? '') : styles.cell({ class: slotClasses?.cell });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function focusCell(index: number) {
|
|
120
|
+
const el = inputs[index];
|
|
121
|
+
if (el) {
|
|
122
|
+
el.focus();
|
|
123
|
+
el.select();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function emit() {
|
|
128
|
+
const joined = cells.join('');
|
|
129
|
+
if (joined !== value) {
|
|
130
|
+
value = joined;
|
|
131
|
+
onValueChange?.(joined);
|
|
132
|
+
}
|
|
133
|
+
const complete = cells.every((c) => c !== '');
|
|
134
|
+
if (complete && !wasComplete) onComplete?.(joined);
|
|
135
|
+
wasComplete = complete;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Distribute a run of characters across cells starting at `start` (typing more
|
|
139
|
+
// than one char, autofill, or paste). Focus lands on the cell after the last
|
|
140
|
+
// one written, or the final cell when the run fills to the end.
|
|
141
|
+
function fill(start: number, raw: string) {
|
|
142
|
+
const chars = sanitize(raw).split('');
|
|
143
|
+
if (chars.length === 0) return;
|
|
144
|
+
let idx = start;
|
|
145
|
+
for (const ch of chars) {
|
|
146
|
+
if (idx >= length) break;
|
|
147
|
+
cells[idx] = ch;
|
|
148
|
+
idx++;
|
|
149
|
+
}
|
|
150
|
+
emit();
|
|
151
|
+
focusCell(Math.min(idx, length - 1));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function handleInput(index: number, event: Event) {
|
|
155
|
+
const el = event.currentTarget as HTMLInputElement;
|
|
156
|
+
const sanitized = sanitize(el.value);
|
|
157
|
+
if (sanitized.length <= 1) {
|
|
158
|
+
const ch = sanitized;
|
|
159
|
+
// A rejected character (a letter in numeric mode) sanitizes to '' while the
|
|
160
|
+
// raw field is non-empty. Restore the existing digit instead of erasing it —
|
|
161
|
+
// a bad keystroke must not destroy a filled cell.
|
|
162
|
+
if (ch === '' && el.value !== '') {
|
|
163
|
+
el.value = cells[index];
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// Force the DOM to the sanitized single char so a rejected glyph can never
|
|
167
|
+
// linger in an otherwise-empty cell (the model may not have changed).
|
|
168
|
+
el.value = ch;
|
|
169
|
+
if (cells[index] !== ch) {
|
|
170
|
+
cells[index] = ch;
|
|
171
|
+
emit();
|
|
172
|
+
}
|
|
173
|
+
if (ch && index < length - 1) focusCell(index + 1);
|
|
174
|
+
} else {
|
|
175
|
+
fill(index, sanitized);
|
|
176
|
+
el.value = cells[index];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function handleKeydown(index: number, event: KeyboardEvent) {
|
|
181
|
+
if (disabled) return;
|
|
182
|
+
switch (event.key) {
|
|
183
|
+
case 'Backspace':
|
|
184
|
+
if (readonly) break;
|
|
185
|
+
event.preventDefault();
|
|
186
|
+
if (cells[index]) {
|
|
187
|
+
cells[index] = '';
|
|
188
|
+
if (inputs[index]) inputs[index]!.value = '';
|
|
189
|
+
emit();
|
|
190
|
+
} else if (index > 0) {
|
|
191
|
+
cells[index - 1] = '';
|
|
192
|
+
if (inputs[index - 1]) inputs[index - 1]!.value = '';
|
|
193
|
+
emit();
|
|
194
|
+
focusCell(index - 1);
|
|
195
|
+
}
|
|
196
|
+
break;
|
|
197
|
+
case 'Delete':
|
|
198
|
+
if (readonly) break;
|
|
199
|
+
event.preventDefault();
|
|
200
|
+
if (cells[index]) {
|
|
201
|
+
cells[index] = '';
|
|
202
|
+
if (inputs[index]) inputs[index]!.value = '';
|
|
203
|
+
emit();
|
|
204
|
+
}
|
|
205
|
+
break;
|
|
206
|
+
case 'ArrowLeft':
|
|
207
|
+
event.preventDefault();
|
|
208
|
+
if (index > 0) focusCell(index - 1);
|
|
209
|
+
break;
|
|
210
|
+
case 'ArrowRight':
|
|
211
|
+
event.preventDefault();
|
|
212
|
+
if (index < length - 1) focusCell(index + 1);
|
|
213
|
+
break;
|
|
214
|
+
case 'Home':
|
|
215
|
+
event.preventDefault();
|
|
216
|
+
focusCell(0);
|
|
217
|
+
break;
|
|
218
|
+
case 'End':
|
|
219
|
+
event.preventDefault();
|
|
220
|
+
focusCell(length - 1);
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function handlePaste(index: number, event: ClipboardEvent) {
|
|
226
|
+
event.preventDefault();
|
|
227
|
+
if (disabled || readonly) return;
|
|
228
|
+
fill(index, event.clipboardData?.getData('text') ?? '');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function handleFocus(event: FocusEvent) {
|
|
232
|
+
(event.currentTarget as HTMLInputElement).select();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Focus the first empty cell on mount when requested. Runs once — autoFocus is
|
|
236
|
+
// stable — and untracks `cells` so a later edit does not re-steal focus.
|
|
237
|
+
$effect(() => {
|
|
238
|
+
if (!autoFocus) return;
|
|
239
|
+
untrack(() => {
|
|
240
|
+
const firstEmpty = cells.findIndex((c) => !c);
|
|
241
|
+
focusCell(firstEmpty === -1 ? length - 1 : firstEmpty);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
</script>
|
|
245
|
+
|
|
246
|
+
<div
|
|
247
|
+
class={unstyled
|
|
248
|
+
? [slotClasses?.root, className].filter(Boolean).join(' ')
|
|
249
|
+
: styles.root({ class: [slotClasses?.root, className] })}
|
|
250
|
+
>
|
|
251
|
+
{#if label}
|
|
252
|
+
<span
|
|
253
|
+
id={labelId}
|
|
254
|
+
class={unstyled ? (slotClasses?.label ?? '') : styles.label({ class: slotClasses?.label })}
|
|
255
|
+
>
|
|
256
|
+
{label}
|
|
257
|
+
</span>
|
|
258
|
+
{/if}
|
|
259
|
+
|
|
260
|
+
<div
|
|
261
|
+
role="group"
|
|
262
|
+
aria-labelledby={label ? labelId : undefined}
|
|
263
|
+
aria-label={label ? undefined : ariaLabel}
|
|
264
|
+
class={unstyled ? (slotClasses?.group ?? '') : styles.group({ class: slotClasses?.group })}
|
|
265
|
+
>
|
|
266
|
+
{#each cellKeys as key, i (key)}
|
|
267
|
+
{#if separator && i > 0 && i % groupSize === 0}
|
|
268
|
+
<span
|
|
269
|
+
aria-hidden="true"
|
|
270
|
+
class={unstyled
|
|
271
|
+
? (slotClasses?.separator ?? '')
|
|
272
|
+
: styles.separator({ class: slotClasses?.separator })}
|
|
273
|
+
>
|
|
274
|
+
{separator}
|
|
275
|
+
</span>
|
|
276
|
+
{/if}
|
|
277
|
+
<input
|
|
278
|
+
bind:this={inputs[i]}
|
|
279
|
+
id={i === 0 ? fieldId : undefined}
|
|
280
|
+
value={cells[i]}
|
|
281
|
+
type={mask ? 'password' : 'text'}
|
|
282
|
+
inputmode={type === 'numeric' ? 'numeric' : 'text'}
|
|
283
|
+
autocomplete={i === 0 ? 'one-time-code' : 'off'}
|
|
284
|
+
pattern={type === 'numeric' ? '[0-9]*' : undefined}
|
|
285
|
+
maxlength="1"
|
|
286
|
+
placeholder={placeholder || undefined}
|
|
287
|
+
{disabled}
|
|
288
|
+
{readonly}
|
|
289
|
+
aria-label={bt('accessibility.pinInputCell', { index: i + 1, total: length })}
|
|
290
|
+
aria-invalid={error ? 'true' : undefined}
|
|
291
|
+
aria-describedby={describedBy}
|
|
292
|
+
class={cellClass()}
|
|
293
|
+
oninput={(e) => handleInput(i, e)}
|
|
294
|
+
onkeydown={(e) => handleKeydown(i, e)}
|
|
295
|
+
onpaste={(e) => handlePaste(i, e)}
|
|
296
|
+
onfocus={handleFocus}
|
|
297
|
+
/>
|
|
298
|
+
{/each}
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
{#if error}
|
|
302
|
+
<div
|
|
303
|
+
id={messageId}
|
|
304
|
+
role="alert"
|
|
305
|
+
class={unstyled
|
|
306
|
+
? (slotClasses?.message ?? '')
|
|
307
|
+
: styles.message({ class: slotClasses?.message })}
|
|
308
|
+
>
|
|
309
|
+
{error}
|
|
310
|
+
</div>
|
|
311
|
+
{:else if helper}
|
|
312
|
+
<div
|
|
313
|
+
id={messageId}
|
|
314
|
+
class={unstyled
|
|
315
|
+
? (slotClasses?.message ?? '')
|
|
316
|
+
: styles.message({ class: slotClasses?.message })}
|
|
317
|
+
>
|
|
318
|
+
{helper}
|
|
319
|
+
</div>
|
|
320
|
+
{/if}
|
|
321
|
+
|
|
322
|
+
{#if name}
|
|
323
|
+
<input type="hidden" {name} {value} />
|
|
324
|
+
{/if}
|
|
325
|
+
</div>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { PinInputSlots, PinInputVariants } from './pin-input.variants.js';
|
|
2
|
+
/**
|
|
3
|
+
* @description Segmented one-time-code / PIN entry — a row of single-character
|
|
4
|
+
* cells with auto-advance, backspace-to-previous, paste-to-fill, and optional
|
|
5
|
+
* masking. Purpose-built for the 2FA/OTP flow the auth package's
|
|
6
|
+
* `TwoFactorManager` drives (pair it with `autoComplete="one-time-code"` for
|
|
7
|
+
* iOS SMS autofill). The value is the concatenated string; `onComplete` fires
|
|
8
|
+
* once every cell is filled.
|
|
9
|
+
*
|
|
10
|
+
* @tag form
|
|
11
|
+
* @related Input
|
|
12
|
+
* @related NumberInput
|
|
13
|
+
* @stability beta
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```svelte
|
|
17
|
+
* <script>
|
|
18
|
+
* let code = $state('');
|
|
19
|
+
* </script>
|
|
20
|
+
* <PinInput bind:value={code} length={6} onComplete={(v) => verify(v)} />
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @example Masked, alphanumeric, grouped with a separator
|
|
24
|
+
* ```svelte
|
|
25
|
+
* <PinInput length={8} type="alphanumeric" mask separator="-" groupSize={4} />
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export interface PinInputProps extends Omit<PinInputVariants, 'error'> {
|
|
29
|
+
/** Current value — the concatenated cell characters. Supports `bind:value`. */
|
|
30
|
+
value?: string;
|
|
31
|
+
/** Number of cells. @default 6 */
|
|
32
|
+
length?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Allowed characters and keyboard hint. `numeric` accepts `0-9` and sets a
|
|
35
|
+
* numeric inputmode; `alphanumeric` also accepts `A-Z`/`a-z`. @default 'numeric'
|
|
36
|
+
*/
|
|
37
|
+
type?: 'numeric' | 'alphanumeric';
|
|
38
|
+
/** Render each filled cell as a masked dot (password style). @default false */
|
|
39
|
+
mask?: boolean;
|
|
40
|
+
/** Placeholder character shown in every empty cell. @default '' */
|
|
41
|
+
placeholder?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Uppercase alphanumeric input as it is entered — keeps a code like `ABCD`
|
|
44
|
+
* visually consistent regardless of caps lock. Ignored for `numeric`.
|
|
45
|
+
* @default false
|
|
46
|
+
*/
|
|
47
|
+
uppercase?: boolean;
|
|
48
|
+
/** Focus the first empty cell on mount. @default false */
|
|
49
|
+
autoFocus?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Render a separator between groups of `groupSize` cells (e.g. `123-456`).
|
|
52
|
+
* A string is shown verbatim; omit for no separator.
|
|
53
|
+
*/
|
|
54
|
+
separator?: string;
|
|
55
|
+
/** Cells per group when `separator` is set. @default 3 */
|
|
56
|
+
groupSize?: number;
|
|
57
|
+
/** @default false */
|
|
58
|
+
disabled?: boolean;
|
|
59
|
+
/** @default false */
|
|
60
|
+
readonly?: boolean;
|
|
61
|
+
/** Adds a required asterisk to the label. @default false */
|
|
62
|
+
required?: boolean;
|
|
63
|
+
/** Group label rendered above the cells and linked via `aria-labelledby`. */
|
|
64
|
+
label?: string;
|
|
65
|
+
/** Helper text below the cells — hidden when `error` is present. */
|
|
66
|
+
helper?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Error message below the cells. When set it overrides `helper`, colours the
|
|
69
|
+
* cells danger, and sets `aria-invalid` on every cell.
|
|
70
|
+
*/
|
|
71
|
+
error?: string;
|
|
72
|
+
/** Fires after any change (typing, paste, backspace) with the full value. */
|
|
73
|
+
onValueChange?: (value: string) => void;
|
|
74
|
+
/** Fires once when the last empty cell is filled, with the complete value. */
|
|
75
|
+
onComplete?: (value: string) => void;
|
|
76
|
+
/** Shared `name` for a hidden input, for native form submission. */
|
|
77
|
+
name?: string;
|
|
78
|
+
/** Extra classes merged onto the root element. */
|
|
79
|
+
class?: string;
|
|
80
|
+
/** Remove all default tv() classes — only user-provided classes apply. */
|
|
81
|
+
unstyled?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Per-slot class overrides merged with tv() styles. Slots: root (what `class`
|
|
84
|
+
* also targets) | label | group | cell | separator | message.
|
|
85
|
+
*/
|
|
86
|
+
slotClasses?: Partial<Record<PinInputSlots, string>>;
|
|
87
|
+
/** Apply a named preset registered via `<BlocksProvider presets={{ PinInput: {...} }}>`. */
|
|
88
|
+
preset?: string;
|
|
89
|
+
/**
|
|
90
|
+
* Accessible name for the cell group when no visible `label` is set. Each
|
|
91
|
+
* cell additionally announces its position ("digit 2 of 6").
|
|
92
|
+
*/
|
|
93
|
+
'aria-label'?: string;
|
|
94
|
+
/** Root id; the cells derive their ids and ARIA wiring from it. */
|
|
95
|
+
id?: string;
|
|
96
|
+
}
|
|
97
|
+
export { default as PinInput } from './PinInput.svelte';
|
|
98
|
+
export { type PinInputVariants, pinInputVariants } from './pin-input.variants.js';
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { type SlotNames, type VariantProps } from '../../utils/variants.js';
|
|
2
|
+
export declare const pinInputVariants: ((props?: {
|
|
3
|
+
tier?: "commit" | "modify" | undefined;
|
|
4
|
+
variant?: "ghost" | "filled" | "outlined" | undefined;
|
|
5
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
6
|
+
intent?: "default" | "success" | "warning" | "danger" | undefined;
|
|
7
|
+
disabled?: boolean | undefined;
|
|
8
|
+
readonly?: boolean | undefined;
|
|
9
|
+
messageType?: "error" | "helper" | undefined;
|
|
10
|
+
error?: boolean | undefined;
|
|
11
|
+
required?: boolean | undefined;
|
|
12
|
+
} | undefined) => {
|
|
13
|
+
root: (props?: ({
|
|
14
|
+
tier?: "commit" | "modify" | undefined;
|
|
15
|
+
variant?: "ghost" | "filled" | "outlined" | undefined;
|
|
16
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
17
|
+
intent?: "default" | "success" | "warning" | "danger" | undefined;
|
|
18
|
+
disabled?: boolean | undefined;
|
|
19
|
+
readonly?: boolean | undefined;
|
|
20
|
+
messageType?: "error" | "helper" | undefined;
|
|
21
|
+
error?: boolean | undefined;
|
|
22
|
+
required?: boolean | undefined;
|
|
23
|
+
} & {
|
|
24
|
+
class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
|
|
25
|
+
}) | undefined) => string;
|
|
26
|
+
label: (props?: ({
|
|
27
|
+
tier?: "commit" | "modify" | undefined;
|
|
28
|
+
variant?: "ghost" | "filled" | "outlined" | undefined;
|
|
29
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
30
|
+
intent?: "default" | "success" | "warning" | "danger" | undefined;
|
|
31
|
+
disabled?: boolean | undefined;
|
|
32
|
+
readonly?: boolean | undefined;
|
|
33
|
+
messageType?: "error" | "helper" | undefined;
|
|
34
|
+
error?: boolean | undefined;
|
|
35
|
+
required?: boolean | undefined;
|
|
36
|
+
} & {
|
|
37
|
+
class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
|
|
38
|
+
}) | undefined) => string;
|
|
39
|
+
group: (props?: ({
|
|
40
|
+
tier?: "commit" | "modify" | undefined;
|
|
41
|
+
variant?: "ghost" | "filled" | "outlined" | undefined;
|
|
42
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
43
|
+
intent?: "default" | "success" | "warning" | "danger" | undefined;
|
|
44
|
+
disabled?: boolean | undefined;
|
|
45
|
+
readonly?: boolean | undefined;
|
|
46
|
+
messageType?: "error" | "helper" | undefined;
|
|
47
|
+
error?: boolean | undefined;
|
|
48
|
+
required?: boolean | undefined;
|
|
49
|
+
} & {
|
|
50
|
+
class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
|
|
51
|
+
}) | undefined) => string;
|
|
52
|
+
cell: (props?: ({
|
|
53
|
+
tier?: "commit" | "modify" | undefined;
|
|
54
|
+
variant?: "ghost" | "filled" | "outlined" | undefined;
|
|
55
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
56
|
+
intent?: "default" | "success" | "warning" | "danger" | undefined;
|
|
57
|
+
disabled?: boolean | undefined;
|
|
58
|
+
readonly?: boolean | undefined;
|
|
59
|
+
messageType?: "error" | "helper" | undefined;
|
|
60
|
+
error?: boolean | undefined;
|
|
61
|
+
required?: boolean | undefined;
|
|
62
|
+
} & {
|
|
63
|
+
class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
|
|
64
|
+
}) | undefined) => string;
|
|
65
|
+
separator: (props?: ({
|
|
66
|
+
tier?: "commit" | "modify" | undefined;
|
|
67
|
+
variant?: "ghost" | "filled" | "outlined" | undefined;
|
|
68
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
69
|
+
intent?: "default" | "success" | "warning" | "danger" | undefined;
|
|
70
|
+
disabled?: boolean | undefined;
|
|
71
|
+
readonly?: boolean | undefined;
|
|
72
|
+
messageType?: "error" | "helper" | undefined;
|
|
73
|
+
error?: boolean | undefined;
|
|
74
|
+
required?: boolean | undefined;
|
|
75
|
+
} & {
|
|
76
|
+
class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
|
|
77
|
+
}) | undefined) => string;
|
|
78
|
+
message: (props?: ({
|
|
79
|
+
tier?: "commit" | "modify" | undefined;
|
|
80
|
+
variant?: "ghost" | "filled" | "outlined" | undefined;
|
|
81
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
82
|
+
intent?: "default" | "success" | "warning" | "danger" | undefined;
|
|
83
|
+
disabled?: boolean | undefined;
|
|
84
|
+
readonly?: boolean | undefined;
|
|
85
|
+
messageType?: "error" | "helper" | undefined;
|
|
86
|
+
error?: boolean | undefined;
|
|
87
|
+
required?: boolean | undefined;
|
|
88
|
+
} & {
|
|
89
|
+
class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
|
|
90
|
+
}) | undefined) => string;
|
|
91
|
+
}) & {
|
|
92
|
+
readonly config: import("../../utils/variants.js").TVConfig;
|
|
93
|
+
};
|
|
94
|
+
export type PinInputVariants = VariantProps<typeof pinInputVariants>;
|
|
95
|
+
/** Slot names derived from the `tv()` config above — single source of truth for `slotClasses`. */
|
|
96
|
+
export type PinInputSlots = SlotNames<typeof pinInputVariants>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { tv } from '../../utils/variants.js';
|
|
2
|
+
export const pinInputVariants = tv({
|
|
3
|
+
slots: {
|
|
4
|
+
root: ['flex flex-col gap-1.5'],
|
|
5
|
+
label: ['block font-medium text-text-secondary text-sm'],
|
|
6
|
+
group: ['flex items-center'],
|
|
7
|
+
cell: [
|
|
8
|
+
'box-border text-center font-medium tabular-nums caret-primary',
|
|
9
|
+
'border text-text-primary bg-surface-base',
|
|
10
|
+
'transition-[color,background-color,border-color,box-shadow] duration-[var(--blocks-duration-fast)] ease-out',
|
|
11
|
+
'focus-visible:outline-none focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-primary/20 focus-visible:z-10',
|
|
12
|
+
'hover:border-border-default',
|
|
13
|
+
'disabled:opacity-50 disabled:cursor-not-allowed disabled:bg-surface-subtle',
|
|
14
|
+
'read-only:bg-surface-subtle read-only:cursor-default'
|
|
15
|
+
],
|
|
16
|
+
separator: ['text-text-tertiary select-none'],
|
|
17
|
+
message: ['text-xs']
|
|
18
|
+
},
|
|
19
|
+
variants: {
|
|
20
|
+
// 3-tier semantic radius. Default `modify` (soft) — pin cells read as fields.
|
|
21
|
+
// Inherited from a wrapping <Toolbar tier="commit"> via TierContext.
|
|
22
|
+
tier: {
|
|
23
|
+
modify: { cell: 'rounded-modify' },
|
|
24
|
+
commit: { cell: 'rounded-commit' }
|
|
25
|
+
},
|
|
26
|
+
variant: {
|
|
27
|
+
outlined: { cell: 'border-border-subtle' },
|
|
28
|
+
filled: {
|
|
29
|
+
cell: 'bg-surface-interactive border-transparent hover:bg-surface-hover focus-visible:bg-surface-base'
|
|
30
|
+
},
|
|
31
|
+
ghost: {
|
|
32
|
+
cell: 'bg-transparent hover:bg-surface-subtle focus-visible:bg-surface-base focus-visible:border-border-subtle'
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
size: {
|
|
36
|
+
sm: {
|
|
37
|
+
// `pointer-coarse:text-base` floors the font to 16px on touch-primary
|
|
38
|
+
// devices — below 16px iOS Safari auto-zooms the field on focus.
|
|
39
|
+
cell: 'h-9 w-9 text-sm pointer-coarse:text-base',
|
|
40
|
+
group: 'gap-1.5',
|
|
41
|
+
separator: 'mx-0.5 text-sm'
|
|
42
|
+
},
|
|
43
|
+
md: {
|
|
44
|
+
cell: 'h-11 w-11 text-lg',
|
|
45
|
+
group: 'gap-2',
|
|
46
|
+
separator: 'mx-1 text-lg'
|
|
47
|
+
},
|
|
48
|
+
lg: {
|
|
49
|
+
cell: 'h-14 w-14 text-2xl',
|
|
50
|
+
group: 'gap-2.5',
|
|
51
|
+
separator: 'mx-1.5 text-2xl'
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
intent: {
|
|
55
|
+
default: {},
|
|
56
|
+
success: {
|
|
57
|
+
cell: 'border-success focus-visible:border-success focus-visible:ring-success/20'
|
|
58
|
+
},
|
|
59
|
+
warning: {
|
|
60
|
+
cell: 'border-warning focus-visible:border-warning focus-visible:ring-warning/20'
|
|
61
|
+
},
|
|
62
|
+
danger: {
|
|
63
|
+
cell: 'border-danger focus-visible:border-danger focus-visible:ring-danger/20'
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
disabled: {
|
|
67
|
+
true: {
|
|
68
|
+
cell: 'opacity-50 cursor-not-allowed bg-surface-disabled pointer-events-none',
|
|
69
|
+
label: 'text-text-disabled'
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
readonly: {
|
|
73
|
+
true: { cell: 'bg-surface-subtle cursor-default' }
|
|
74
|
+
},
|
|
75
|
+
// Declared BEFORE `error` so the error tone wins the message-color bucket
|
|
76
|
+
// in every call shape — `{ error: true }` alone must read red.
|
|
77
|
+
messageType: {
|
|
78
|
+
error: { message: 'text-danger' },
|
|
79
|
+
helper: { message: 'text-text-tertiary' }
|
|
80
|
+
},
|
|
81
|
+
error: {
|
|
82
|
+
true: {
|
|
83
|
+
cell: 'border-danger focus-visible:border-danger focus-visible:ring-danger/20',
|
|
84
|
+
message: 'text-danger'
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
required: {
|
|
88
|
+
true: { label: "after:content-['*'] after:ml-1 after:text-danger" }
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
compoundVariants: [
|
|
92
|
+
// Ghost keeps a transparent border at rest — even under an intent. The error
|
|
93
|
+
// state drops this so validation feedback (border-danger) stays visible.
|
|
94
|
+
{
|
|
95
|
+
variant: 'ghost',
|
|
96
|
+
error: false,
|
|
97
|
+
class: { cell: 'border-transparent' }
|
|
98
|
+
}
|
|
99
|
+
],
|
|
100
|
+
defaultVariants: {
|
|
101
|
+
tier: 'modify',
|
|
102
|
+
variant: 'outlined',
|
|
103
|
+
size: 'md',
|
|
104
|
+
intent: 'default',
|
|
105
|
+
disabled: false,
|
|
106
|
+
readonly: false,
|
|
107
|
+
error: false,
|
|
108
|
+
required: false,
|
|
109
|
+
messageType: 'helper'
|
|
110
|
+
}
|
|
111
|
+
});
|