@ibobbyts/svelte-ui-utils 0.4.4 → 0.4.6

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/README.md CHANGED
@@ -357,18 +357,23 @@ light and dark defaults and expose `--suu-sortable-table-row-*-light` and
357
357
  />
358
358
  ```
359
359
 
360
- Dialog components are controlled by the consuming app. `Dialog` calls `onClose`
361
- from the close button, backdrop, or Escape key when dismissible; it does not
362
- mutate `open` internally. Set `dimBackdrop={false}` to disable background
363
- darkening, `blurBackdrop={true}` to blur content behind the dialog, and
364
- `showCountdown={true}` with `countdownDurationMs` to show a toast-style top
360
+ Dialog components are controlled by the consuming app. `Dialog` renders a
361
+ native `<dialog>` element and opens it with the browser top layer, so dialogs
362
+ nest: with several dialogs open at once, Escape, Tab, and backdrop clicks only
363
+ affect the topmost dialog and parent dialogs stay open. `Dialog` calls
364
+ `onClose` from the close button, backdrop, or Escape key when dismissible; it
365
+ does not mutate `open` internally. Set `dimBackdrop={false}` to disable
366
+ background darkening, `blurBackdrop={true}` to blur content behind the dialog,
367
+ and `showCountdown={true}` with `countdownDurationMs` to show a toast-style top
365
368
  countdown bar. The optional `padding` prop accepts a CSS padding shorthand and
366
369
  applies it consistently to the dialog header, body, and footer. The default
367
370
  section-specific spacing is preserved when `padding` is omitted, and the same
368
371
  prop is available on all dialog wrapper components. Set `restoreFocusSelector`
369
- when the opening control may be replaced while the dialog is open; after closing,
370
- `Dialog` focuses the latest matching element and otherwise falls back to the
371
- original connected element. `ConfirmDialog` forwards this prop to `Dialog`.
372
+ when the opening control may be replaced while the dialog is open; after
373
+ closing, `Dialog` focuses the latest matching element and otherwise falls back
374
+ to the original connected element. `ConfirmDialog` renders its `message` once
375
+ in the dialog body and forwards this prop, along with the dismiss, backdrop,
376
+ countdown, and focus-restore lifecycle props, to `Dialog`.
372
377
 
373
378
  ## Dropdown
374
379
 
@@ -411,6 +416,14 @@ use `menuAlign="right"` to align their right edges instead. `fitContent` sizes
411
416
  the menu to its longest option while keeping the selected edge aligned.
412
417
  `DataTable` uses this same component for its page-size picker.
413
418
 
419
+ Set `search={true}` with `loadOptions` to load remote options as the user
420
+ types. The default `input_style="dropdown"` keeps the search field inside the
421
+ expanded menu. Set `input_style="input"` to render a directly editable input;
422
+ results appear while it is focused and hide on blur. In input style, the
423
+ optional `getItemLabel` callback controls the displayed label while `value`
424
+ and `onChange` continue to represent submitted option values. The input style
425
+ is ignored when `search` is disabled.
426
+
414
427
  For keyboard typeahead, an option may provide `searchText` separately from its
415
428
  display `label`. Typeahead matches the beginning of `searchText` or `label`,
416
429
  while the label remains the only text shown in the trigger and menu:
@@ -1,5 +1,14 @@
1
1
  <svelte:options runes={false} />
2
2
 
3
+ <script context="module" lang="ts">
4
+ // Mount order mirrors the native top layer: the last registered element is
5
+ // the topmost dialog, and only it reacts to Escape, Tab, and backdrop clicks.
6
+ const openDialogElements: HTMLDialogElement[] = [];
7
+
8
+ const focusableSelector =
9
+ 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
10
+ </script>
11
+
3
12
  <script lang="ts">
4
13
  import { tick } from 'svelte';
5
14
  import type { DialogSize } from './types.js';
@@ -26,29 +35,26 @@
26
35
  export let restoreFocusSelector: string | undefined = undefined;
27
36
  export let onClose: (() => void) | undefined = undefined;
28
37
 
29
- let dialogElement: HTMLElement | undefined;
38
+ let dialogElement: HTMLDialogElement | undefined;
30
39
  let titleId = `suu-dialog-title-${Math.random().toString(36).slice(2)}`;
31
40
  let descriptionId = `suu-dialog-description-${Math.random().toString(36).slice(2)}`;
32
41
  let wasOpen = false;
33
42
  let previouslyFocusedElement: HTMLElement | null = null;
34
43
  let restoreFocusSelectorAtOpen: string | undefined;
35
44
 
45
+ // The dialog element is the full-viewport top layer surface; the visual
46
+ // backdrop and panel stay on their existing inner elements.
47
+ const dialogResetStyle =
48
+ 'width: 100%; height: 100%; max-width: none; max-height: none; margin: 0; padding: 0; border: none; background: transparent;';
49
+
36
50
  $: resolvedLabelledBy = labelledBy ?? (title ? titleId : undefined);
37
51
  $: resolvedDescribedBy =
38
52
  describedBy ?? (description ? descriptionId : undefined);
39
53
  $: backdropStyle =
40
54
  `--suu-dialog-backdrop-opacity: ${backdropOpacity}; --suu-dialog-backdrop-blur: ${backdropBlur};`;
41
- $: dialogStyle = padding ? `--suu-dialog-padding: ${padding};` : '';
42
55
  $: countdownStyle = `--suu-dialog-countdown-duration: ${Math.max(0, countdownDurationMs)}ms;`;
43
56
  $: if (open && !wasOpen) {
44
57
  wasOpen = true;
45
- void focusDialog();
46
- } else if (!open && wasOpen) {
47
- wasOpen = false;
48
- restoreFocus();
49
- }
50
-
51
- async function focusDialog() {
52
58
  if (typeof document !== 'undefined') {
53
59
  previouslyFocusedElement =
54
60
  document.activeElement instanceof HTMLElement
@@ -56,7 +62,37 @@
56
62
  : null;
57
63
  restoreFocusSelectorAtOpen = restoreFocusSelector;
58
64
  }
65
+ void focusDialog();
66
+ } else if (!open && wasOpen) {
67
+ wasOpen = false;
68
+ void restoreFocus();
69
+ }
70
+
71
+ function presentDialog(element: HTMLDialogElement) {
72
+ if (!openDialogElements.includes(element)) {
73
+ openDialogElements.push(element);
74
+ }
75
+ if (typeof element.showModal === 'function') {
76
+ if (!element.open) {
77
+ element.showModal();
78
+ }
79
+ } else if (!element.hasAttribute('open')) {
80
+ // jsdom does not implement showModal; the attribute keeps the element
81
+ // rendered as an open dialog there.
82
+ element.setAttribute('open', '');
83
+ }
59
84
 
85
+ return {
86
+ destroy() {
87
+ const index = openDialogElements.indexOf(element);
88
+ if (index !== -1) {
89
+ openDialogElements.splice(index, 1);
90
+ }
91
+ },
92
+ };
93
+ }
94
+
95
+ async function focusDialog() {
60
96
  await tick();
61
97
  dialogElement?.focus();
62
98
  }
@@ -99,78 +135,115 @@
99
135
  requestClose();
100
136
  }
101
137
 
102
- function handleKeydown(event: KeyboardEvent) {
103
- if (open && event.key === 'Escape' && closeOnEscape) {
104
- event.preventDefault();
138
+ function handleCancel(event: Event) {
139
+ // Prevent the browser's own Escape close so the open prop stays the only
140
+ // source of truth; only the topmost dialog receives this event natively.
141
+ event.preventDefault();
142
+ if (closeOnEscape) {
105
143
  requestClose();
106
144
  }
107
145
  }
108
- </script>
109
146
 
110
- <svelte:window on:keydown={handleKeydown} />
147
+ function handleKeydown(event: KeyboardEvent) {
148
+ if (event.key !== 'Tab' || !dialogElement) {
149
+ return;
150
+ }
151
+ if (openDialogElements[openDialogElements.length - 1] !== dialogElement) {
152
+ return;
153
+ }
154
+
155
+ event.preventDefault();
156
+ const focusables = Array.from(
157
+ dialogElement.querySelectorAll<HTMLElement>(focusableSelector),
158
+ );
159
+ if (focusables.length === 0) {
160
+ dialogElement.focus();
161
+ return;
162
+ }
163
+
164
+ const activeElement =
165
+ document.activeElement instanceof HTMLElement
166
+ ? document.activeElement
167
+ : undefined;
168
+ const currentIndex = activeElement
169
+ ? focusables.indexOf(activeElement)
170
+ : -1;
171
+ const nextIndex = event.shiftKey
172
+ ? currentIndex <= 0
173
+ ? focusables.length - 1
174
+ : currentIndex - 1
175
+ : currentIndex === -1 || currentIndex === focusables.length - 1
176
+ ? 0
177
+ : currentIndex + 1;
178
+ focusables[nextIndex].focus();
179
+ }
180
+ </script>
111
181
 
112
182
  {#if open}
113
- <div
114
- class="suu-dialog-backdrop"
115
- data-dimmed={dimBackdrop ? 'true' : 'false'}
116
- data-blurred={blurBackdrop ? 'true' : 'false'}
117
- style={backdropStyle}
118
- role="presentation"
119
- on:mousedown={handleBackdropMouseDown}
183
+ <dialog
184
+ bind:this={dialogElement}
185
+ class="suu-dialog-root"
186
+ style={dialogResetStyle}
187
+ aria-labelledby={resolvedLabelledBy}
188
+ aria-describedby={resolvedDescribedBy}
189
+ tabindex="-1"
190
+ use:presentDialog
191
+ on:cancel={handleCancel}
192
+ on:keydown={handleKeydown}
120
193
  >
121
194
  <div
122
- bind:this={dialogElement}
123
- class={`suu-dialog suu-dialog--${size}`}
124
- style={dialogStyle}
125
- role="dialog"
126
- aria-modal="true"
127
- aria-labelledby={resolvedLabelledBy}
128
- aria-describedby={resolvedDescribedBy}
129
- tabindex="-1"
195
+ class="suu-dialog-backdrop"
196
+ data-dimmed={dimBackdrop ? 'true' : 'false'}
197
+ data-blurred={blurBackdrop ? 'true' : 'false'}
198
+ style={backdropStyle}
199
+ role="presentation"
200
+ on:mousedown={handleBackdropMouseDown}
130
201
  >
131
- {#if showCountdown && countdownDurationMs > 0}
132
- <div
133
- class="suu-dialog__countdown"
134
- style={countdownStyle}
135
- role="timer"
136
- aria-label={countdownLabel}
137
- ></div>
138
- {/if}
139
- <header class="suu-dialog__header">
140
- <div class="suu-dialog__heading">
141
- {#if title}
142
- <h2 class="suu-dialog__title" id={titleId}>{title}</h2>
143
- {/if}
144
- {#if description}
145
- <p class="suu-dialog__description" id={descriptionId}>
146
- {description}
147
- </p>
202
+ <div class={`suu-dialog suu-dialog--${size}`} style:--suu-dialog-padding={padding}>
203
+ {#if showCountdown && countdownDurationMs > 0}
204
+ <div
205
+ class="suu-dialog__countdown"
206
+ style={countdownStyle}
207
+ role="timer"
208
+ aria-label={countdownLabel}
209
+ ></div>
210
+ {/if}
211
+ <header class="suu-dialog__header">
212
+ <div class="suu-dialog__heading">
213
+ {#if title}
214
+ <h2 class="suu-dialog__title" id={titleId}>{title}</h2>
215
+ {/if}
216
+ {#if description}
217
+ <p class="suu-dialog__description" id={descriptionId}>
218
+ {description}
219
+ </p>
220
+ {/if}
221
+ </div>
222
+ {#if showCloseButton && dismissible}
223
+ <button
224
+ type="button"
225
+ class="suu-dialog__close"
226
+ aria-label={closeLabel}
227
+ title={closeLabel}
228
+ on:click={requestClose}
229
+ >
230
+ <svg viewBox="0 0 20 20" aria-hidden="true">
231
+ <path d="m6 6 8 8M14 6l-8 8"></path>
232
+ </svg>
233
+ </button>
148
234
  {/if}
235
+ </header>
236
+
237
+ <div class="suu-dialog__body">
238
+ <slot />
149
239
  </div>
150
- {#if showCloseButton && dismissible}
151
- <button
152
- type="button"
153
- class="suu-dialog__close"
154
- aria-label={closeLabel}
155
- title={closeLabel}
156
- on:click={requestClose}
157
- >
158
- <svg viewBox="0 0 20 20" aria-hidden="true">
159
- <path d="m6 6 8 8M14 6l-8 8"></path>
160
- </svg>
161
- </button>
162
- {/if}
163
- </header>
164
240
 
165
- <div class="suu-dialog__body">
166
- <slot />
241
+ {#if $$slots.footer}
242
+ <footer class="suu-dialog__footer">
243
+ <slot name="footer" />
244
+ </footer>
245
+ {/if}
167
246
  </div>
168
-
169
- {#if $$slots.footer}
170
- <footer class="suu-dialog__footer">
171
- <slot name="footer" />
172
- </footer>
173
- {/if}
174
247
  </div>
175
- </div>
248
+ </dialog>
176
249
  {/if}
@@ -1 +1 @@
1
- {"version":3,"file":"Dialog.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/dialog/Dialog.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AA0J7C,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,GAAG;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IAC9G,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AACD,KAAK,gCAAgC,CAAC,KAAK,EAAE,KAAK,IAAI,KAAK,GACvD,CAAC,KAAK,SAAS;IAAE,OAAO,EAAE,GAAG,CAAA;CAAE,GACzB,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,GACnC,GAAG,GACH;IAAE,QAAQ,CAAC,EAAE,GAAG,CAAA;CAAE,GAClB,EAAE,CAAC,CAAC;AAId,QAAA,MAAM,MAAM;;;;;WAfksB,UAAU;cAAY,MAAM,GAAG,SAAS;;;;;;;sBAAsP,MAAM,GAAG,MAAM;;;;;iBAAmL,MAAM,GAAG,SAAS;kBAAgB,MAAM,GAAG,SAAS;2BAAyB,MAAM,GAAG,SAAS;cAAY,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS;;;;;;;;;cAehtC,CAAC;AAChF,KAAK,MAAM,GAAG,YAAY,CAAC,OAAO,MAAM,CAAC,CAAC;AAC5C,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"Dialog.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/dialog/Dialog.svelte.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AA2N7C,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,GAAG;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IAC9G,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AACD,KAAK,gCAAgC,CAAC,KAAK,EAAE,KAAK,IAAI,KAAK,GACvD,CAAC,KAAK,SAAS;IAAE,OAAO,EAAE,GAAG,CAAA;CAAE,GACzB,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,GACnC,GAAG,GACH;IAAE,QAAQ,CAAC,EAAE,GAAG,CAAA;CAAE,GAClB,EAAE,CAAC,CAAC;AAId,QAAA,MAAM,MAAM;;;;;WAfksB,UAAU;cAAY,MAAM,GAAG,SAAS;;;;;;;sBAAsP,MAAM,GAAG,MAAM;;;;;iBAAmL,MAAM,GAAG,SAAS;kBAAgB,MAAM,GAAG,SAAS;2BAAyB,MAAM,GAAG,SAAS;cAAY,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS;;;;;;;;;cAehtC,CAAC;AAChF,KAAK,MAAM,GAAG,YAAY,CAAC,OAAO,MAAM,CAAC,CAAC;AAC5C,eAAe,MAAM,CAAC"}
@@ -9,6 +9,8 @@
9
9
  import { getUiMessages, type UiLanguage } from '../i18n.js';
10
10
  import type {
11
11
  DropdownChangeHandler,
12
+ DropdownInputStyle,
13
+ DropdownItemLabelGetter,
12
14
  DropdownLoadOptions,
13
15
  DropdownLoadOptionsResult,
14
16
  DropdownLoadStatus,
@@ -19,6 +21,7 @@
19
21
  DropdownOptionGroup,
20
22
  DropdownPlacement,
21
23
  DropdownSelectionChangeHandler,
24
+ DropdownSearchChangeHandler,
22
25
  DropdownSelection,
23
26
  DropdownTriggerClickHandler,
24
27
  DropdownValue
@@ -33,10 +36,13 @@
33
36
  export let selectedOptions: DropdownOption[] = [];
34
37
  export let groupsCollapsedByDefault: 'true' | 'false' | 'auto' = 'false';
35
38
  export let search = false;
39
+ export let input_style: DropdownInputStyle = 'dropdown';
40
+ export let getItemLabel: DropdownItemLabelGetter = (option) => option.label;
36
41
  export let loadOptions: DropdownLoadOptions | undefined = undefined;
37
42
  export let searchDebounceMs = 300;
38
43
  export let searchLimit = 10;
39
44
  export let searchPlaceholder: string | undefined = undefined;
45
+ export let placeholder: string | undefined = undefined;
40
46
  export let language: UiLanguage = 'en_us';
41
47
  export let loadingText: string | undefined = undefined;
42
48
  export let noResultsText: string | undefined = undefined;
@@ -55,6 +61,7 @@
55
61
  export let className: string | undefined = undefined;
56
62
  export let portal = false;
57
63
  export let onChange: DropdownSelectionChangeHandler | undefined = undefined;
64
+ export let onSearchChange: DropdownSearchChangeHandler | undefined = undefined;
58
65
  export let onTriggerClick: DropdownTriggerClickHandler | undefined = undefined;
59
66
 
60
67
  const viewportMargin = 20;
@@ -82,6 +89,10 @@
82
89
  // from a search response; once true, the user's manual toggles own the set.
83
90
  let searchGroupsInitialized = false;
84
91
  let searchQuery = '';
92
+ // 在受控 value 尚未响应 onChange 时保留用户当前输入,避免
93
+ // selectedText 的旧值在同一轮更新中覆盖输入框内容。
94
+ let inputDraft: string | undefined;
95
+ let preserveInputDraftOnClose = false;
85
96
  let searchStatus: DropdownLoadStatus = 'idle';
86
97
  let searchOptions: DropdownOption[] = [];
87
98
  let searchOptionGroups: DropdownOptionGroup[] | undefined = undefined;
@@ -115,7 +126,11 @@
115
126
  $: selectedOption = Array.isArray(value) ? undefined : resolvedOptions.find((option) => option.value === value);
116
127
  $: selectedText = multiselect
117
128
  ? selectedLabelTexts(selectedValues).join(', ')
118
- : labelForValue(Array.isArray(value) ? '' : value);
129
+ : displayLabelForValue(Array.isArray(value) ? '' : value);
130
+ $: inputMode = search && input_style === 'input';
131
+ $: inputDisplayValue = inputMode
132
+ ? (inputDraft ?? (searchQuery || (multiselect ? '' : selectedText)))
133
+ : '';
119
134
  $: if (!open) {
120
135
  activeValue = initialActiveValue();
121
136
  }
@@ -185,6 +200,12 @@
185
200
  return option?.label ?? knownLabels.get(nextValue) ?? String(nextValue);
186
201
  }
187
202
 
203
+ function displayLabelForValue(nextValue: DropdownValue): string {
204
+ const option = resolvedOptions.find((candidate) => candidate.value === nextValue) ??
205
+ selectedOptions.find((candidate) => candidate.value === nextValue);
206
+ return option ? getItemLabel(option) : labelForValue(nextValue);
207
+ }
208
+
188
209
  // Multiselect trigger text only lists values whose label is known; values
189
210
  // without one never render a raw fallback string into the trigger.
190
211
  function selectedLabelTexts(nextValues: DropdownMultiValue): string[] {
@@ -194,7 +215,9 @@
194
215
  resolvedOptions.find((candidate) => candidate.value === nextValue)?.label ??
195
216
  knownLabels.get(nextValue);
196
217
  if (label !== undefined) {
197
- texts.push(label);
218
+ const option = resolvedOptions.find((candidate) => candidate.value === nextValue) ??
219
+ selectedOptions.find((candidate) => candidate.value === nextValue);
220
+ texts.push(option ? getItemLabel(option) : label);
198
221
  }
199
222
  }
200
223
  return texts;
@@ -332,6 +355,10 @@
332
355
  return;
333
356
  }
334
357
 
358
+ if (inputMode) {
359
+ inputDraft = getItemLabel(option);
360
+ preserveInputDraftOnClose = true;
361
+ }
335
362
  open = false;
336
363
  clearTypeaheadBuffer();
337
364
  void (onChange as DropdownChangeHandler | undefined)?.(option.value);
@@ -452,6 +479,11 @@
452
479
  clearSearchTimer();
453
480
  abortSearchController();
454
481
  searchQuery = '';
482
+ if (preserveInputDraftOnClose) {
483
+ preserveInputDraftOnClose = false;
484
+ } else {
485
+ inputDraft = undefined;
486
+ }
455
487
  searchOptions = [];
456
488
  searchOptionGroups = undefined;
457
489
  searchStatus = 'idle';
@@ -459,6 +491,7 @@
459
491
 
460
492
  function beginSearchOnOpen() {
461
493
  searchQuery = '';
494
+ inputDraft = undefined;
462
495
  searchOptions = [];
463
496
  searchOptionGroups = undefined;
464
497
  collapsedGroupIndexes = new Set();
@@ -482,7 +515,7 @@
482
515
  }
483
516
  open = true;
484
517
  activeValue = initialActiveValue();
485
- if (search) {
518
+ if (search && !inputMode) {
486
519
  void tick().then(() => {
487
520
  searchInputElement?.focus();
488
521
  });
@@ -573,6 +606,7 @@
573
606
  openMenu();
574
607
  }
575
608
  searchQuery = wasOpen ? `${searchQuery}${normalizeTypeaheadKey(event.key)}` : normalizeTypeaheadKey(event.key);
609
+ void onSearchChange?.(searchQuery);
576
610
  scheduleSearch(searchQuery);
577
611
  return;
578
612
  }
@@ -591,10 +625,31 @@
591
625
  }
592
626
 
593
627
  function handleSearchInput(event: Event) {
594
- searchQuery = (event.currentTarget as HTMLInputElement).value;
628
+ const nextQuery = (event.currentTarget as HTMLInputElement).value;
629
+ inputDraft = nextQuery;
630
+ if (inputMode && !multiselect && value !== '' && nextQuery !== selectedText) {
631
+ void (onChange as DropdownChangeHandler | undefined)?.('');
632
+ }
633
+ searchQuery = nextQuery;
634
+ if (inputMode) {
635
+ if (nextQuery.trim()) {
636
+ if (!open) {
637
+ updateResolvedPlacement();
638
+ open = true;
639
+ activeValue = initialActiveValue();
640
+ }
641
+ } else {
642
+ open = false;
643
+ }
644
+ }
645
+ void onSearchChange?.(searchQuery);
595
646
  scheduleSearch(searchQuery);
596
647
  }
597
648
 
649
+ function handleInputFocus() {
650
+ // 输入模式下聚焦只准备输入;用户输入有效搜索词后才展开结果。
651
+ }
652
+
598
653
  function handleSearchKeydown(event: KeyboardEvent) {
599
654
  if (event.key === 'Escape') {
600
655
  event.preventDefault();
@@ -716,11 +771,19 @@
716
771
  }
717
772
 
718
773
  function portalMenu(node: HTMLDivElement, enabled: boolean) {
774
+ let resizeObserver: ResizeObserver | undefined;
719
775
  if (enabled && typeof document !== 'undefined') {
720
776
  document.body.appendChild(node);
777
+ if (typeof ResizeObserver !== 'undefined') {
778
+ resizeObserver = new ResizeObserver(() => {
779
+ if (open) void updateViewportPanelMaxHeight();
780
+ });
781
+ resizeObserver.observe(node);
782
+ }
721
783
  }
722
784
  return {
723
785
  destroy() {
786
+ resizeObserver?.disconnect();
724
787
  if (node.parentNode) {
725
788
  node.parentNode.removeChild(node);
726
789
  }
@@ -806,6 +869,47 @@
806
869
  style:max-width={maxWidth}
807
870
  on:focusout={handleFocusout}
808
871
  >
872
+ {#if inputMode}
873
+ {#if multiselect && selectedValues.length > 0}
874
+ <div class="suu-dropdown__selected-items">
875
+ {#each selectedValues as selectedValue}
876
+ {@const selectedOption = resolvedOptions.find((option) => option.value === selectedValue) ?? selectedOptions.find((option) => option.value === selectedValue)}
877
+ {#if selectedOption}
878
+ <span class="suu-dropdown__selected-item">
879
+ <span>{getItemLabel(selectedOption)}</span>
880
+ <button
881
+ type="button"
882
+ class="suu-dropdown__selected-remove"
883
+ aria-label={`Remove ${getItemLabel(selectedOption)}`}
884
+ on:mousedown|preventDefault
885
+ on:click={() => selectOption(selectedOption)}
886
+ >×</button>
887
+ </span>
888
+ {/if}
889
+ {/each}
890
+ </div>
891
+ {/if}
892
+ <div class="suu-dropdown__input-field">
893
+ <input
894
+ bind:this={searchInputElement}
895
+ {id}
896
+ class="suu-dropdown__input"
897
+ type="text"
898
+ value={inputDisplayValue}
899
+ placeholder={searchPlaceholder ?? placeholder ?? ''}
900
+ aria-label={ariaLabel}
901
+ autocomplete="off"
902
+ role="combobox"
903
+ aria-expanded={open}
904
+ aria-autocomplete="list"
905
+ aria-controls={resolvedListboxId}
906
+ {disabled}
907
+ on:focus={handleInputFocus}
908
+ on:input={handleSearchInput}
909
+ on:keydown={handleSearchKeydown}
910
+ />
911
+ </div>
912
+ {:else}
809
913
  <button
810
914
  bind:this={buttonElement}
811
915
  {id}
@@ -819,9 +923,10 @@
819
923
  on:click={handleTriggerClick}
820
924
  on:keydown={handleKeydown}
821
925
  >
822
- <span class="suu-dropdown__label">{selectedText}</span>
926
+ <span class="suu-dropdown__label">{selectedText || placeholder || ''}</span>
823
927
  <span class="suu-dropdown__chevron" aria-hidden="true"></span>
824
928
  </button>
929
+ {/if}
825
930
 
826
931
  {#if name !== undefined && !multiselect}
827
932
  <input
@@ -853,7 +958,7 @@
853
958
  style:--suu-dropdown-menu-width={portalMenuWidth}
854
959
  style:--suu-dropdown-panel-max-height={viewportPanelMaxHeight}
855
960
  >
856
- {#if search}
961
+ {#if search && !inputMode}
857
962
  <div class="suu-dropdown__search">
858
963
  <svg class="suu-dropdown__search-icon" viewBox="0 0 24 24" aria-hidden="true">
859
964
  <circle cx="11" cy="11" r="7"></circle>
@@ -1,5 +1,5 @@
1
1
  import { type UiLanguage } from '../i18n.js';
2
- import type { DropdownLoadOptions, DropdownMenuAlign, DropdownOption, DropdownOptionGroup, DropdownPlacement, DropdownSelectionChangeHandler, DropdownSelection, DropdownTriggerClickHandler } from './types.js';
2
+ import type { DropdownInputStyle, DropdownItemLabelGetter, DropdownLoadOptions, DropdownMenuAlign, DropdownOption, DropdownOptionGroup, DropdownPlacement, DropdownSelectionChangeHandler, DropdownSearchChangeHandler, DropdownSelection, DropdownTriggerClickHandler } from './types.js';
3
3
  interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
4
4
  new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
5
5
  $$bindings?: Bindings;
@@ -22,10 +22,13 @@ declare const Dropdown: $$__sveltets_2_IsomorphicComponent<{
22
22
  /** Labels for controlled values that are not present in the current search result. */ selectedOptions?: DropdownOption[];
23
23
  groupsCollapsedByDefault?: "true" | "false" | "auto";
24
24
  search?: boolean;
25
+ input_style?: DropdownInputStyle;
26
+ getItemLabel?: DropdownItemLabelGetter;
25
27
  loadOptions?: DropdownLoadOptions | undefined;
26
28
  searchDebounceMs?: number;
27
29
  searchLimit?: number;
28
30
  searchPlaceholder?: string | undefined;
31
+ placeholder?: string | undefined;
29
32
  language?: UiLanguage;
30
33
  loadingText?: string | undefined;
31
34
  noResultsText?: string | undefined;
@@ -44,8 +47,11 @@ declare const Dropdown: $$__sveltets_2_IsomorphicComponent<{
44
47
  className?: string | undefined;
45
48
  portal?: boolean;
46
49
  onChange?: DropdownSelectionChangeHandler | undefined;
50
+ onSearchChange?: DropdownSearchChangeHandler | undefined;
47
51
  onTriggerClick?: DropdownTriggerClickHandler | undefined;
48
52
  }, {
53
+ mousedown: MouseEvent;
54
+ } & {
49
55
  [evt: string]: CustomEvent<any>;
50
56
  }, {}, {}, string>;
51
57
  type Dropdown = InstanceType<typeof Dropdown>;
@@ -1 +1 @@
1
- {"version":3,"file":"Dropdown.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/dropdown/Dropdown.svelte.ts"],"names":[],"mappings":"AAOA,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAC5D,OAAO,KAAK,EAER,mBAAmB,EAGnB,iBAAiB,EAGjB,cAAc,EACd,mBAAmB,EACnB,iBAAiB,EACjB,8BAA8B,EAC9B,iBAAiB,EACjB,2BAA2B,EAE5B,MAAM,YAAY,CAAC;AA81BtB,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,GAAG;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IAC9G,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAKD,QAAA,MAAM,QAAQ;SAXswB,MAAM,GAAG,SAAS;YAAU,iBAAiB;;cAA8C,cAAc,EAAE;mBAAiB,mBAAmB,EAAE,GAAG,SAAS;IACj7B,sFAAsF,mBAAkB,cAAc,EAAE;+BAA6B,MAAM,GAAG,OAAO,GAAG,MAAM;;kBAAwC,mBAAmB,GAAG,SAAS;;;wBAAoG,MAAM,GAAG,SAAS;eAAa,UAAU;kBAAgB,MAAM,GAAG,SAAS;oBAAkB,MAAM,GAAG,SAAS;gBAAc,MAAM,GAAG,SAAS;gBAAc,MAAM,GAAG,SAAS;gBAAc,iBAAiB;gBAAc,iBAAiB;;;;WAAuG,MAAM,GAAG,SAAS;;YAAsC,MAAM,GAAG,SAAS;eAAa,MAAM,GAAG,SAAS;eAAa,MAAM,GAAG,SAAS;gBAAc,MAAM,GAAG,SAAS;;eAAqC,8BAA8B,GAAG,SAAS;qBAAmB,2BAA2B,GAAG,SAAS;;;kBAUj4B,CAAC;AAC5E,KAAK,QAAQ,GAAG,YAAY,CAAC,OAAO,QAAQ,CAAC,CAAC;AAChD,eAAe,QAAQ,CAAC"}
1
+ {"version":3,"file":"Dropdown.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/dropdown/Dropdown.svelte.ts"],"names":[],"mappings":"AAOA,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAC5D,OAAO,KAAK,EAER,kBAAkB,EAClB,uBAAuB,EACvB,mBAAmB,EAGnB,iBAAiB,EAGjB,cAAc,EACd,mBAAmB,EACnB,iBAAiB,EACjB,8BAA8B,EAC9B,2BAA2B,EAC3B,iBAAiB,EACjB,2BAA2B,EAE5B,MAAM,YAAY,CAAC;AA66BtB,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,GAAG;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IAC9G,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAKD,QAAA,MAAM,QAAQ;SAX03B,MAAM,GAAG,SAAS;YAAU,iBAAiB;;cAA8C,cAAc,EAAE;mBAAiB,mBAAmB,EAAE,GAAG,SAAS;IACriC,sFAAsF,mBAAkB,cAAc,EAAE;+BAA6B,MAAM,GAAG,OAAO,GAAG,MAAM;;kBAAwC,kBAAkB;mBAAiB,uBAAuB;kBAAgB,mBAAmB,GAAG,SAAS;;;wBAAoG,MAAM,GAAG,SAAS;kBAAgB,MAAM,GAAG,SAAS;eAAa,UAAU;kBAAgB,MAAM,GAAG,SAAS;oBAAkB,MAAM,GAAG,SAAS;gBAAc,MAAM,GAAG,SAAS;gBAAc,MAAM,GAAG,SAAS;gBAAc,iBAAiB;gBAAc,iBAAiB;;;;WAAuG,MAAM,GAAG,SAAS;;YAAsC,MAAM,GAAG,SAAS;eAAa,MAAM,GAAG,SAAS;eAAa,MAAM,GAAG,SAAS;gBAAc,MAAM,GAAG,SAAS;;eAAqC,8BAA8B,GAAG,SAAS;qBAAmB,2BAA2B,GAAG,SAAS;qBAAmB,2BAA2B,GAAG,SAAS;;;;;kBAUviC,CAAC;AAC5E,KAAK,QAAQ,GAAG,YAAY,CAAC,OAAO,QAAQ,CAAC,CAAC;AAChD,eAAe,QAAQ,CAAC"}
@@ -1,4 +1,4 @@
1
1
  export { default as Dropdown } from './Dropdown.svelte';
2
2
  export { default as DropdownMultiSelect } from './DropdownMultiSelect.svelte';
3
- export type { DropdownChangeHandler, DropdownLoadContext, DropdownLoadOptions, DropdownLoadOptionsResult, DropdownLoadStatus, DropdownMenuAlign, DropdownMultiChangeHandler, DropdownMultiValue, DropdownOption, DropdownOptionGroup, DropdownPlacement, DropdownSelectionChangeHandler, DropdownSelection, DropdownTriggerClickHandler, DropdownValue } from './types.js';
3
+ export type { DropdownChangeHandler, DropdownInputStyle, DropdownItemLabelGetter, DropdownLoadContext, DropdownLoadOptions, DropdownLoadOptionsResult, DropdownLoadStatus, DropdownMenuAlign, DropdownMultiChangeHandler, DropdownMultiValue, DropdownOption, DropdownOptionGroup, DropdownPlacement, DropdownSelectionChangeHandler, DropdownSearchChangeHandler, DropdownSelection, DropdownTriggerClickHandler, DropdownValue } from './types.js';
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/dropdown/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAC9E,YAAY,EACV,qBAAqB,EACrB,mBAAmB,EACnB,mBAAmB,EACnB,yBAAyB,EACzB,kBAAkB,EAClB,iBAAiB,EACjB,0BAA0B,EAC1B,kBAAkB,EAClB,cAAc,EACd,mBAAmB,EACnB,iBAAiB,EACjB,8BAA8B,EAC9B,iBAAiB,EACjB,2BAA2B,EAC3B,aAAa,EACd,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/dropdown/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAC9E,YAAY,EACV,qBAAqB,EACrB,kBAAkB,EAClB,uBAAuB,EACvB,mBAAmB,EACnB,mBAAmB,EACnB,yBAAyB,EACzB,kBAAkB,EAClB,iBAAiB,EACjB,0BAA0B,EAC1B,kBAAkB,EAClB,cAAc,EACd,mBAAmB,EACnB,iBAAiB,EACjB,8BAA8B,EAC9B,2BAA2B,EAC3B,iBAAiB,EACjB,2BAA2B,EAC3B,aAAa,EACd,MAAM,YAAY,CAAC"}
@@ -25,6 +25,9 @@ export type DropdownSelectionChangeHandler = {
25
25
  bivarianceHack(value: DropdownSelection): void | Promise<void>;
26
26
  }['bivarianceHack'];
27
27
  export type DropdownTriggerClickHandler = (event: MouseEvent) => void;
28
+ export type DropdownSearchChangeHandler = (query: string) => void | Promise<void>;
29
+ export type DropdownInputStyle = 'dropdown' | 'input';
30
+ export type DropdownItemLabelGetter = (option: DropdownOption) => string;
28
31
  export type DropdownLoadStatus = 'idle' | 'loading' | 'success' | 'error';
29
32
  export interface DropdownLoadContext {
30
33
  limit: number;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/dropdown/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC;AAEnC,MAAM,MAAM,kBAAkB,GAAG,MAAM,EAAE,CAAC;AAE1C,MAAM,MAAM,iBAAiB,GAAG,aAAa,GAAG,kBAAkB,CAAC;AAEnE,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,IAAI,GAAG,MAAM,CAAC;AAEvD,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,OAAO,CAAC;AAEjD,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,mGAAmG;IACnG,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,aAAa,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,cAAc,EAAE,CAAC;CAC3B;AAED,MAAM,MAAM,qBAAqB,GAAG,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAEnF,MAAM,MAAM,0BAA0B,GAAG,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE7F,MAAM,MAAM,8BAA8B,GAAG;IAC5C,cAAc,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/D,CAAC,gBAAgB,CAAC,CAAC;AAEpB,MAAM,MAAM,2BAA2B,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;AAEtE,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAE1E,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,yBAAyB;IACxC;;;OAGG;IACH,OAAO,CAAC,EAAE,cAAc,EAAE,CAAC;IAC3B,YAAY,CAAC,EAAE,mBAAmB,EAAE,CAAC;CACtC;AAED,MAAM,MAAM,mBAAmB,GAAG,CAChC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,mBAAmB,KACzB,OAAO,CAAC,yBAAyB,CAAC,GAAG,yBAAyB,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/dropdown/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC;AAEnC,MAAM,MAAM,kBAAkB,GAAG,MAAM,EAAE,CAAC;AAE1C,MAAM,MAAM,iBAAiB,GAAG,aAAa,GAAG,kBAAkB,CAAC;AAEnE,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,IAAI,GAAG,MAAM,CAAC;AAEvD,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,OAAO,CAAC;AAEjD,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,mGAAmG;IACnG,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,aAAa,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,cAAc,EAAE,CAAC;CAC3B;AAED,MAAM,MAAM,qBAAqB,GAAG,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAEnF,MAAM,MAAM,0BAA0B,GAAG,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE7F,MAAM,MAAM,8BAA8B,GAAG;IAC5C,cAAc,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/D,CAAC,gBAAgB,CAAC,CAAC;AAEpB,MAAM,MAAM,2BAA2B,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;AAEtE,MAAM,MAAM,2BAA2B,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAElF,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,OAAO,CAAC;AACtD,MAAM,MAAM,uBAAuB,GAAG,CAAC,MAAM,EAAE,cAAc,KAAK,MAAM,CAAC;AAEzE,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAE1E,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,yBAAyB;IACxC;;;OAGG;IACH,OAAO,CAAC,EAAE,cAAc,EAAE,CAAC;IAC3B,YAAY,CAAC,EAAE,mBAAmB,EAAE,CAAC;CACtC;AAED,MAAM,MAAM,mBAAmB,GAAG,CAChC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,mBAAmB,KACzB,OAAO,CAAC,yBAAyB,CAAC,GAAG,yBAAyB,CAAC"}
package/dist/style.css CHANGED
@@ -1209,6 +1209,59 @@
1209
1209
  opacity: 0.6;
1210
1210
  }
1211
1211
 
1212
+ .suu-dropdown__input-field {
1213
+ position: relative;
1214
+ width: 100%;
1215
+ }
1216
+
1217
+ .suu-dropdown__input {
1218
+ width: 100%;
1219
+ min-height: 42px;
1220
+ box-sizing: border-box;
1221
+ border: 1px solid var(--suu-color-border);
1222
+ border-radius: var(--suu-radius);
1223
+ background: var(--suu-color-bg);
1224
+ color: var(--suu-color-text);
1225
+ padding: 9px 14px;
1226
+ font: inherit;
1227
+ }
1228
+
1229
+ .suu-dropdown__input:focus {
1230
+ border-color: var(--suu-color-accent);
1231
+ outline: 2px solid color-mix(in srgb, var(--suu-color-accent) 22%, transparent);
1232
+ outline-offset: 1px;
1233
+ }
1234
+
1235
+ .suu-dropdown__selected-items {
1236
+ display: flex;
1237
+ flex-wrap: wrap;
1238
+ gap: 6px;
1239
+ margin-bottom: 6px;
1240
+ }
1241
+
1242
+ .suu-dropdown__selected-item {
1243
+ display: inline-flex;
1244
+ align-items: center;
1245
+ gap: 4px;
1246
+ max-width: 100%;
1247
+ padding: 3px 6px 3px 9px;
1248
+ border: 1px solid color-mix(in srgb, var(--suu-color-accent) 34%, var(--suu-color-border));
1249
+ border-radius: var(--suu-radius-pill);
1250
+ background: color-mix(in srgb, var(--suu-color-accent) 9%, var(--suu-color-bg));
1251
+ color: var(--suu-color-text);
1252
+ font-size: 0.875rem;
1253
+ }
1254
+
1255
+ .suu-dropdown__selected-remove {
1256
+ border: 0;
1257
+ padding: 0 2px;
1258
+ background: transparent;
1259
+ color: var(--suu-color-muted);
1260
+ cursor: pointer;
1261
+ font: inherit;
1262
+ line-height: 1;
1263
+ }
1264
+
1212
1265
  .suu-dropdown__chevron {
1213
1266
  flex: 0 0 auto;
1214
1267
  width: 0.58em;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ibobbyts/svelte-ui-utils",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "Reusable Svelte 5 UI utilities for dialogs, toast notifications, dropdown search, and data tables.",
5
5
  "type": "module",
6
6
  "license": "MIT",