@spectral_labs/ui 1.3.1 → 1.5.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.
@@ -55,7 +55,7 @@
55
55
  align-items: center;
56
56
  gap: 0.5rem;
57
57
  padding: 0.5rem 0.75rem;
58
- border-radius: var(--sp-sys-shape-corner-xs, 4px);
58
+ border-radius: var(--sp-sys-shape-corner-md, 12px);
59
59
  font-size: var(--sp-sys-typescale-body-medium-size, 0.875rem);
60
60
  color: var(--sp-sys-color-on-surface);
61
61
  cursor: pointer;
@@ -2,24 +2,49 @@
2
2
  import DropdownMenuContent from '../components/dropdown-menu-content.svelte'
3
3
  import DropdownMenuItem from '../components/dropdown-menu-item.svelte'
4
4
  import DropdownMenuRoot from '../components/dropdown-menu-root.svelte'
5
+ import DropdownMenuSubContent from '../components/dropdown-menu-sub-content.svelte'
6
+ import DropdownMenuSubTrigger from '../components/dropdown-menu-sub-trigger.svelte'
7
+ import DropdownMenuSub from '../components/dropdown-menu-sub.svelte'
5
8
  import DropdownMenuTrigger from '../components/dropdown-menu-trigger.svelte'
6
9
 
7
10
  let {
8
11
  open = $bindable(false),
9
12
  portalTo,
10
13
  onOpenChange,
14
+ withSub = false,
15
+ subFirst = false,
16
+ subDisabled = false,
11
17
  }: {
12
18
  open?: boolean
13
19
  portalTo?: HTMLElement | string
14
20
  onOpenChange?: (open: boolean) => void
21
+ withSub?: boolean
22
+ subFirst?: boolean
23
+ subDisabled?: boolean
15
24
  } = $props()
16
25
  </script>
17
26
 
27
+ {#snippet subBlock()}
28
+ <DropdownMenuSub>
29
+ <DropdownMenuSubTrigger disabled={subDisabled}>More options</DropdownMenuSubTrigger>
30
+ <DropdownMenuSubContent>
31
+ <DropdownMenuItem>Sub Item 1</DropdownMenuItem>
32
+ <DropdownMenuItem>Sub Item 2</DropdownMenuItem>
33
+ </DropdownMenuSubContent>
34
+ </DropdownMenuSub>
35
+ {/snippet}
36
+
18
37
  <DropdownMenuRoot bind:open {onOpenChange}>
19
38
  <DropdownMenuTrigger>Open Menu</DropdownMenuTrigger>
20
39
  <DropdownMenuContent {portalTo}>
40
+ {#if withSub && subFirst}
41
+ {@render subBlock()}
42
+ {/if}
21
43
  <DropdownMenuItem>Item 1</DropdownMenuItem>
22
44
  <DropdownMenuItem>Item 2</DropdownMenuItem>
23
45
  <DropdownMenuItem>Item 3</DropdownMenuItem>
46
+ {#if withSub && !subFirst}
47
+ {@render subBlock()}
48
+ {/if}
24
49
  </DropdownMenuContent>
25
50
  </DropdownMenuRoot>
@@ -60,4 +60,71 @@ describe('dropdown-menu', () => {
60
60
  flushSync();
61
61
  expect(calls).toEqual([true, false]);
62
62
  });
63
+ describe('subTrigger', () => {
64
+ function pressKey(key) {
65
+ const el = document.activeElement;
66
+ el.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }));
67
+ flushSync();
68
+ }
69
+ it('participates in roving focus (data-roving-focus-item + tabindex -1)', () => {
70
+ setup({ open: true, withSub: true });
71
+ const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
72
+ expect(subTrigger).toBeTruthy();
73
+ expect(subTrigger.hasAttribute('data-roving-focus-item')).toBe(true);
74
+ expect(subTrigger.getAttribute('tabindex')).toBe('-1');
75
+ });
76
+ it('carries both item and sub-trigger classes (visual parity hook)', () => {
77
+ setup({ open: true, withSub: true });
78
+ const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
79
+ expect(subTrigger.classList.contains('sp-dropdown-menu-item')).toBe(true);
80
+ expect(subTrigger.classList.contains('sp-dropdown-sub-trigger')).toBe(true);
81
+ });
82
+ it('is autofocused on open when it is the first row', () => {
83
+ setup({ open: true, withSub: true, subFirst: true });
84
+ const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
85
+ expect(document.activeElement).toBe(subTrigger);
86
+ });
87
+ it('is reached by ArrowDown/ArrowUp roving among plain items', () => {
88
+ setup({ open: true, withSub: true });
89
+ const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
90
+ // Autofocus lands on Item 1; sub-trigger is the 4th row.
91
+ expect(document.activeElement?.textContent?.trim()).toBe('Item 1');
92
+ pressKey('ArrowDown');
93
+ pressKey('ArrowDown');
94
+ pressKey('ArrowDown');
95
+ expect(document.activeElement).toBe(subTrigger);
96
+ pressKey('ArrowUp');
97
+ expect(document.activeElement?.textContent?.trim()).toBe('Item 3');
98
+ });
99
+ it('is skipped by autofocus and roving when disabled', () => {
100
+ setup({ open: true, withSub: true, subFirst: true, subDisabled: true });
101
+ const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
102
+ expect(subTrigger.hasAttribute('data-disabled')).toBe(true);
103
+ // Autofocus skips the disabled sub-trigger even though it is first.
104
+ expect(document.activeElement?.textContent?.trim()).toBe('Item 1');
105
+ // Roving loops over the three plain items without ever landing on it.
106
+ pressKey('ArrowDown');
107
+ pressKey('ArrowDown');
108
+ pressKey('ArrowDown');
109
+ expect(document.activeElement?.textContent?.trim()).toBe('Item 1');
110
+ pressKey('ArrowUp');
111
+ expect(document.activeElement?.textContent?.trim()).toBe('Item 3');
112
+ });
113
+ it('opens the sub content on ArrowRight and reflects aria-expanded', () => {
114
+ setup({ open: true, withSub: true, subFirst: true });
115
+ const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
116
+ expect(document.activeElement).toBe(subTrigger);
117
+ expect(subTrigger.getAttribute('aria-expanded')).toBe('false');
118
+ expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeNull();
119
+ pressKey('ArrowRight');
120
+ expect(subTrigger.getAttribute('aria-expanded')).toBe('true');
121
+ expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeTruthy();
122
+ });
123
+ it('opens the sub content on Enter', () => {
124
+ setup({ open: true, withSub: true, subFirst: true });
125
+ expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeNull();
126
+ pressKey('Enter');
127
+ expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeTruthy();
128
+ });
129
+ });
63
130
  });
@@ -57,7 +57,7 @@
57
57
  align-items: center;
58
58
  gap: 0.5rem;
59
59
  padding: 0.5rem 0.75rem;
60
- border-radius: var(--sp-sys-shape-corner-xs, 4px);
60
+ border-radius: var(--sp-sys-shape-corner-md, 12px);
61
61
  font-family: var(--sp-sys-typescale-body-medium-font, inherit);
62
62
  font-size: var(--sp-sys-typescale-body-medium-size, 0.875rem);
63
63
  color: var(--sp-sys-color-on-surface);
@@ -45,7 +45,7 @@
45
45
  align-items: center;
46
46
  gap: 0.5rem;
47
47
  padding: 0.5rem 0.75rem;
48
- border-radius: var(--sp-sys-shape-corner-xs, 4px);
48
+ border-radius: var(--sp-sys-shape-corner-md, 12px);
49
49
  font-family: var(--sp-sys-typescale-body-medium-font, inherit);
50
50
  font-size: var(--sp-sys-typescale-body-medium-size, 0.875rem);
51
51
  color: var(--sp-sys-color-on-surface);
@@ -29,6 +29,12 @@
29
29
  if (disabled) return
30
30
  if (e.key === 'ArrowRight' || e.key === 'Enter' || e.key === ' ') {
31
31
  e.preventDefault()
32
+ // The key is fully handled here: stop it from bubbling to ancestor menu
33
+ // keydown handlers (menubar-content treats ArrowRight as "close menu +
34
+ // open the next menubar menu", which would immediately undo openSub;
35
+ // Space would also leak into content-level typeahead). Same pattern as
36
+ // dropdown-menu-sub-content.svelte for ArrowLeft/Escape.
37
+ e.stopPropagation()
32
38
  sub.openSub()
33
39
  }
34
40
  }
@@ -47,7 +53,8 @@
47
53
  aria-expanded={sub.open}
48
54
  aria-disabled={disabled || undefined}
49
55
  data-disabled={disabled || undefined}
50
- tabindex={disabled ? -1 : 0}
56
+ data-roving-focus-item
57
+ tabindex={-1}
51
58
  onpointerenter={onPointerEnter}
52
59
  onpointerleave={onPointerLeave}
53
60
  onkeydown={onKeyDown}
@@ -71,11 +78,40 @@
71
78
  </div>
72
79
 
73
80
  <style>
81
+ /* Svelte scoped styles don't cross components: sharing the `sp-dropdown-menu-item`
82
+ class name inherits nothing from dropdown-menu-item.svelte. The item base styles
83
+ are duplicated here (same pattern as dropdown-menu-checkbox-item.svelte) and must
84
+ stay in sync with dropdown-menu-item.svelte. */
74
85
  .sp-dropdown-sub-trigger {
75
86
  display: flex;
76
87
  align-items: center;
77
88
  justify-content: space-between;
78
89
  gap: 0.5rem;
90
+ padding: 0.5rem 0.75rem;
91
+ border-radius: var(--sp-sys-shape-corner-md, 12px);
92
+ font-family: var(--sp-sys-typescale-body-medium-font, inherit);
93
+ font-size: var(--sp-sys-typescale-body-medium-size, 0.875rem);
94
+ color: var(--sp-sys-color-on-surface);
95
+ cursor: pointer;
96
+ user-select: none;
97
+ outline: none;
98
+ transition: background-color var(--sp-sys-motion-duration-short2, 100ms)
99
+ var(--sp-sys-motion-easing-effects, ease);
100
+ }
101
+
102
+ .sp-dropdown-sub-trigger:focus,
103
+ .sp-dropdown-sub-trigger:hover:not([data-disabled]),
104
+ .sp-dropdown-sub-trigger[aria-expanded='true'] {
105
+ background-color: color-mix(
106
+ in oklch,
107
+ var(--sp-sys-color-on-surface) calc(var(--sp-sys-state-hover-opacity, 0.08) * 100%),
108
+ transparent
109
+ );
110
+ }
111
+
112
+ .sp-dropdown-sub-trigger[data-disabled] {
113
+ opacity: 0.38;
114
+ cursor: not-allowed;
79
115
  }
80
116
 
81
117
  .sp-dropdown-sub-trigger-label {
@@ -1 +1 @@
1
- {"version":3,"file":"dropdown-menu-sub-trigger.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/dropdown-menu/components/dropdown-menu-sub-trigger.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAA;AAyD9D,QAAA,MAAM,sBAAsB,iEAAwC,CAAC;AACrE,KAAK,sBAAsB,GAAG,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAAC;AACxE,eAAe,sBAAsB,CAAC"}
1
+ {"version":3,"file":"dropdown-menu-sub-trigger.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/dropdown-menu/components/dropdown-menu-sub-trigger.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAA;AA+D9D,QAAA,MAAM,sBAAsB,iEAAwC,CAAC;AACrE,KAAK,sBAAsB,GAAG,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAAC;AACxE,eAAe,sBAAsB,CAAC"}
@@ -1,12 +1,19 @@
1
1
  <script lang="ts">
2
2
  import DropdownMenuItem from '../../dropdown-menu/components/dropdown-menu-item.svelte'
3
3
  import DropdownMenuSeparator from '../../dropdown-menu/components/dropdown-menu-separator.svelte'
4
+ import DropdownMenuSubContent from '../../dropdown-menu/components/dropdown-menu-sub-content.svelte'
5
+ import DropdownMenuSubTrigger from '../../dropdown-menu/components/dropdown-menu-sub-trigger.svelte'
6
+ import DropdownMenuSub from '../../dropdown-menu/components/dropdown-menu-sub.svelte'
4
7
  import MenubarContent from '../components/menubar-content.svelte'
5
8
  import MenubarMenu from '../components/menubar-menu.svelte'
6
9
  import MenubarRoot from '../components/menubar-root.svelte'
7
10
  import MenubarTrigger from '../components/menubar-trigger.svelte'
8
11
 
9
- const { loop = true, portalTo }: { loop?: boolean; portalTo?: HTMLElement | string } = $props()
12
+ const {
13
+ loop = true,
14
+ portalTo,
15
+ withSub = false,
16
+ }: { loop?: boolean; portalTo?: HTMLElement | string; withSub?: boolean } = $props()
10
17
  </script>
11
18
 
12
19
  <MenubarRoot {loop}>
@@ -17,6 +24,15 @@
17
24
  <DropdownMenuItem>Open</DropdownMenuItem>
18
25
  <DropdownMenuSeparator />
19
26
  <DropdownMenuItem>Save</DropdownMenuItem>
27
+ {#if withSub}
28
+ <DropdownMenuSub>
29
+ <DropdownMenuSubTrigger>Share</DropdownMenuSubTrigger>
30
+ <DropdownMenuSubContent>
31
+ <DropdownMenuItem>Email</DropdownMenuItem>
32
+ <DropdownMenuItem>Copy Link</DropdownMenuItem>
33
+ </DropdownMenuSubContent>
34
+ </DropdownMenuSub>
35
+ {/if}
20
36
  </MenubarContent>
21
37
  </MenubarMenu>
22
38
  <MenubarMenu value="edit">
@@ -195,4 +195,39 @@ describe('menubar', () => {
195
195
  // No menu should open from hover alone
196
196
  expect(target.querySelector('[role="menu"]')).toBeNull();
197
197
  });
198
+ // --- SubTrigger inside a menubar menu ---
199
+ describe('subTrigger', () => {
200
+ function pressKey(key) {
201
+ const el = document.activeElement;
202
+ el.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true }));
203
+ flushSync();
204
+ }
205
+ it('arrowRight on a plain item moves to the next menubar menu', () => {
206
+ setup({ withSub: true });
207
+ const triggers = target.querySelectorAll('[data-menubar-trigger]');
208
+ triggers[0].click();
209
+ flushSync();
210
+ // Autofocus lands on the first item of the opened menu
211
+ expect(document.activeElement?.textContent?.trim()).toBe('New');
212
+ pressKey('ArrowRight');
213
+ expect(triggers[0].getAttribute('data-state')).toBe('closed');
214
+ expect(triggers[1].getAttribute('data-state')).toBe('open');
215
+ });
216
+ it('arrowRight on the sub-trigger opens the submenu without switching menubar menus', () => {
217
+ setup({ withSub: true });
218
+ const triggers = target.querySelectorAll('[data-menubar-trigger]');
219
+ triggers[0].click();
220
+ flushSync();
221
+ const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
222
+ expect(subTrigger).toBeTruthy();
223
+ subTrigger.focus();
224
+ pressKey('ArrowRight');
225
+ // Submenu opened…
226
+ expect(subTrigger.getAttribute('aria-expanded')).toBe('true');
227
+ expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeTruthy();
228
+ // …and the menubar neither closed the current menu nor advanced to the next one
229
+ expect(triggers[0].getAttribute('data-state')).toBe('open');
230
+ expect(triggers[1].getAttribute('data-state')).toBe('closed');
231
+ });
232
+ });
198
233
  });
@@ -1,4 +1,4 @@
1
- import { mount, unmount } from 'svelte';
1
+ import { flushSync, mount, unmount } from 'svelte';
2
2
  import { afterEach, describe, expect, it } from 'vitest';
3
3
  import PinInput from '../pin-input.svelte';
4
4
  describe('pin-input', () => {
@@ -23,4 +23,49 @@ describe('pin-input', () => {
23
23
  setup({ length: 6 });
24
24
  expect(target.querySelectorAll('input').length).toBe(6);
25
25
  });
26
+ it('uses the inputmode prop on slots', () => {
27
+ setup({ length: 4, inputmode: 'text' });
28
+ const input = target.querySelector('input');
29
+ expect(input.getAttribute('inputmode')).toBe('text');
30
+ });
31
+ it('defaults inputmode to numeric (backward compat)', () => {
32
+ setup({ length: 4 });
33
+ const input = target.querySelector('input');
34
+ expect(input.getAttribute('inputmode')).toBe('numeric');
35
+ });
36
+ it('uses ariaLabelPrefix in slot aria-labels', () => {
37
+ setup({ length: 4, ariaLabelPrefix: 'Character' });
38
+ const input = target.querySelector('input');
39
+ expect(input.getAttribute('aria-label')).toBe('Character 1 of 4');
40
+ });
41
+ it('pasteTransformer strips separators before distributing', () => {
42
+ setup({ length: 6, pasteTransformer: (raw) => raw.replace(/[^0-9A-Z]/gi, '').toUpperCase() });
43
+ const input = target.querySelector('input');
44
+ const ev = new Event('paste', { bubbles: true });
45
+ Object.defineProperty(ev, 'clipboardData', { value: { getData: () => 'ab-cd-ef' } });
46
+ input.dispatchEvent(ev);
47
+ flushSync();
48
+ const joined = Array.from(target.querySelectorAll('input')).map((i) => i.value).join('');
49
+ expect(joined).toBe('ABCDEF');
50
+ });
51
+ it('charTransformer uppercases typed input', () => {
52
+ setup({ length: 4, charTransformer: (c) => c.toUpperCase() });
53
+ const input = target.querySelector('input');
54
+ input.value = 'a';
55
+ input.dispatchEvent(new Event('input', { bubbles: true }));
56
+ flushSync();
57
+ expect(target.querySelectorAll('input')[0].value).toBe('A');
58
+ });
59
+ it('allowedPattern rejects disallowed typed characters', () => {
60
+ setup({ length: 4, allowedPattern: /[2-9A-HJKMNP-Z]/i });
61
+ const input = target.querySelector('input');
62
+ input.value = '0';
63
+ input.dispatchEvent(new Event('input', { bubbles: true }));
64
+ flushSync();
65
+ expect(target.querySelectorAll('input')[0].value).toBe('');
66
+ });
67
+ it('renders group separators when groupSize is set', () => {
68
+ setup({ length: 8, groupSize: 4 });
69
+ expect(target.querySelectorAll('.sp-pin-input-separator').length).toBe(1);
70
+ });
26
71
  });
@@ -12,19 +12,27 @@
12
12
  name,
13
13
  required = false,
14
14
  class: className,
15
+ inputmode = 'numeric',
16
+ ariaLabelPrefix = 'Digit',
17
+ pasteTransformer,
18
+ charTransformer,
19
+ allowedPattern,
15
20
  }: PinInputProps = $props()
16
21
 
17
22
  const inputs: HTMLInputElement[] = $state.raw([])
18
23
 
19
24
  function handleInput(index: number, event: Event) {
20
25
  const target = event.target as HTMLInputElement
21
- const char = target.value.slice(-1)
26
+ let char = target.value.slice(-1)
27
+ if (char && allowedPattern && !allowedPattern.test(char)) {
28
+ target.value = value[index] ?? '' // reject: value state unchanged ⇒ Svelte won't re-render this slot
29
+ return
30
+ }
31
+ if (char && charTransformer) char = charTransformer(char)
22
32
  const chars = value.split('')
23
33
  chars[index] = char
24
34
  value = chars.join('').slice(0, length)
25
-
26
35
  if (char && index < length - 1) inputs[index + 1]?.focus()
27
-
28
36
  if (value.length === length) onComplete?.(value)
29
37
  }
30
38
 
@@ -45,38 +53,31 @@
45
53
 
46
54
  function handlePaste(event: ClipboardEvent) {
47
55
  event.preventDefault()
48
- const pasted = event.clipboardData?.getData('text')?.slice(0, length) ?? ''
49
- value = pasted
50
- const nextIndex = Math.min(pasted.length, length - 1)
56
+ const raw = event.clipboardData?.getData('text') ?? ''
57
+ const cleaned = pasteTransformer ? pasteTransformer(raw) : raw
58
+ value = cleaned.slice(0, length)
59
+ const nextIndex = Math.min(value.length, length - 1)
51
60
  inputs[nextIndex]?.focus()
52
- if (pasted.length === length) onComplete?.(pasted)
61
+ if (value.length === length) onComplete?.(value)
53
62
  }
54
63
  </script>
55
64
 
56
65
  <div class="sp-pin-input {className ?? ''}" role="group">
57
66
  {#each { length } as _, i (i)}
58
67
  {#if groupSize > 0 && i > 0 && i % groupSize === 0}
59
- <span class="sp-pin-input-separator" aria-hidden="true">
60
- <svg
61
- viewBox="0 0 24 24"
62
- fill="none"
63
- stroke="currentColor"
64
- stroke-width="2"
65
- stroke-linecap="round"><line x1="5" y1="12" x2="19" y2="12" /></svg
66
- >
67
- </span>
68
+ <span class="sp-pin-input-separator" aria-hidden="true"></span>
68
69
  {/if}
69
70
  <input
70
71
  bind:this={inputs[i]}
71
72
  class="sp-pin-input-slot"
72
73
  type={mask ? 'password' : 'text'}
73
- inputmode="numeric"
74
+ inputmode={inputmode}
74
75
  maxlength={1}
75
76
  autocomplete="off"
76
77
  {placeholder}
77
78
  {disabled}
78
79
  value={value[i] ?? ''}
79
- aria-label={`Digit ${i + 1} of ${length}`}
80
+ aria-label={`${ariaLabelPrefix} ${i + 1} of ${length}`}
80
81
  oninput={(e) => handleInput(i, e)}
81
82
  onkeydown={(e) => handleKeydown(i, e)}
82
83
  onpaste={handlePaste}
@@ -96,14 +97,14 @@
96
97
  }
97
98
 
98
99
  .sp-pin-input-separator {
99
- display: flex;
100
- align-items: center;
101
- color: var(--sp-sys-color-outline);
102
- }
103
-
104
- .sp-pin-input-separator svg {
105
- width: 16px;
106
- height: 16px;
100
+ /* A reliable CSS dash between groups. The previous inline <svg viewBox> rendered at 0 width
101
+ (no intrinsic size), so groupSize separators were invisible. */
102
+ flex: 0 0 auto;
103
+ align-self: center;
104
+ width: 0.75rem;
105
+ height: 2px;
106
+ border-radius: 999px;
107
+ background-color: var(--sp-sys-color-outline);
107
108
  }
108
109
 
109
110
  .sp-pin-input-slot {
@@ -132,6 +133,12 @@
132
133
  opacity: 0.5;
133
134
  }
134
135
 
136
+ /* Empty focused slot: hide the placeholder so the caret never overlaps the ○.
137
+ Stable class hook (.sp-pin-input-slot), no generated s-... classes. */
138
+ .sp-pin-input-slot:focus::placeholder {
139
+ color: transparent;
140
+ }
141
+
135
142
  .sp-pin-input-slot:disabled {
136
143
  opacity: 0.38;
137
144
  cursor: not-allowed;
@@ -1 +1 @@
1
- {"version":3,"file":"pin-input.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/components/pin-input/pin-input.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AA4EhD,QAAA,MAAM,QAAQ,wDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
1
+ {"version":3,"file":"pin-input.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/components/pin-input/pin-input.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAmFhD,QAAA,MAAM,QAAQ,wDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
@@ -19,5 +19,15 @@ export interface PinInputProps {
19
19
  required?: boolean;
20
20
  /** Additional CSS class. */
21
21
  class?: string;
22
+ /** Virtual-keyboard hint applied to every slot. Default 'numeric' (PIN). Use 'text' for alphanumeric codes. */
23
+ inputmode?: 'numeric' | 'text' | 'tel' | 'email' | 'url' | 'decimal' | 'search' | 'none';
24
+ /** Noun used in each slot's aria-label: `${ariaLabelPrefix} N of M`. Default 'Digit'. */
25
+ ariaLabelPrefix?: string;
26
+ /** Transform pasted clipboard text BEFORE distributing into slots (e.g. strip separators, uppercase). */
27
+ pasteTransformer?: (raw: string) => string;
28
+ /** Transform each typed character before storing (e.g. uppercase). Applied only to characters accepted by `allowedPattern`. */
29
+ charTransformer?: (char: string) => string;
30
+ /** When set, a typed character is accepted only if it matches this pattern (paste is filtered by pasteTransformer instead). */
31
+ allowedPattern?: RegExp;
22
32
  }
23
33
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/lib/components/pin-input/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,4BAA4B;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,mCAAmC;IACnC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,gGAAgG;IAChG,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mCAAmC;IACnC,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,sBAAsB;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,sCAAsC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,+CAA+C;IAC/C,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACpC,uBAAuB;IACvB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,4BAA4B;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;CACf"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/lib/components/pin-input/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,4BAA4B;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,mCAAmC;IACnC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,gGAAgG;IAChG,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mCAAmC;IACnC,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,sBAAsB;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,sCAAsC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,+CAA+C;IAC/C,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACpC,uBAAuB;IACvB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,4BAA4B;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,+GAA+G;IAC/G,SAAS,CAAC,EAAE,SAAS,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,KAAK,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAA;IACxF,yFAAyF;IACzF,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,yGAAyG;IACzG,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAA;IAC1C,+HAA+H;IAC/H,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IAC1C,+HAA+H;IAC/H,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB"}
@@ -65,7 +65,7 @@
65
65
  align-items: center;
66
66
  gap: 0.5rem;
67
67
  padding: 0.5rem 0.75rem;
68
- border-radius: var(--sp-sys-shape-corner-xs, 4px);
68
+ border-radius: var(--sp-sys-shape-corner-md, 12px);
69
69
  font-family: var(--sp-sys-typescale-body-medium-font, inherit);
70
70
  font-size: var(--sp-sys-typescale-body-medium-size, 0.875rem);
71
71
  color: var(--sp-sys-color-on-surface);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@spectral_labs/ui",
3
3
  "type": "module",
4
- "version": "1.3.1",
4
+ "version": "1.5.0",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org",
7
7
  "access": "public"