@streamscloud/kit 0.31.0 → 0.32.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.
@@ -6,7 +6,7 @@
6
6
  // Shared trigger shape for Singleselect / SingleselectAsync / (later) Multiselect.
7
7
  // Public CSS API lives under `--sc-kit--select--trigger--*` so consumers can override
8
8
  // once and have all select variants pick it up.
9
- @mixin select-trigger {
9
+ @mixin select-trigger($chevron: true) {
10
10
  --_sel--height: var(--sc-kit--select--trigger--height, var(--_sel--size-height, var(--sc-kit--field--height--md)));
11
11
  --_sel--padding-inline: var(--sc-kit--select--trigger--padding-inline, var(--_sel--size-padding-inline, var(--sc-kit--field--padding-inline--md)));
12
12
  --_sel--gap: var(--sc-kit--select--trigger--gap, var(--sc-kit--space--2));
@@ -164,12 +164,14 @@
164
164
  }
165
165
  }
166
166
 
167
- &__chevron {
168
- transition: transform var(--sc-kit--duration--fast) var(--sc-kit--ease--default);
169
- }
167
+ @if $chevron {
168
+ &__chevron {
169
+ transition: transform var(--sc-kit--duration--fast) var(--sc-kit--ease--default);
170
+ }
170
171
 
171
- &--open &__chevron {
172
- transform: rotate(180deg);
172
+ &--open &__chevron {
173
+ transform: rotate(180deg);
174
+ }
173
175
  }
174
176
 
175
177
  &__spinner {
@@ -193,12 +195,14 @@
193
195
  // reflow to additional rows below. (Spinner positioning lives in MultiselectAsync's
194
196
  // local style block — Svelte's per-component CSS-usage check otherwise flags this rule
195
197
  // as unused in Singleselect / sync Multiselect.)
196
- &--multi &__chevron {
197
- position: absolute;
198
- inset-block-start: calc(var(--_sel--height) / 2);
199
- inset-inline-end: var(--_sel--padding-inline);
200
- transform: translateY(-50%);
201
- margin: 0;
198
+ @if $chevron {
199
+ &--multi &__chevron {
200
+ position: absolute;
201
+ inset-block-start: calc(var(--_sel--height) / 2);
202
+ inset-inline-end: var(--_sel--padding-inline);
203
+ transform: translateY(-50%);
204
+ margin: 0;
205
+ }
202
206
  }
203
207
 
204
208
  // Inline-chip area visible inside the trigger when value is non-empty (inline mode).
@@ -0,0 +1,495 @@
1
+ <script lang="ts" module>export {};
2
+ </script>
3
+
4
+ <script lang="ts">import { InputClearButton } from '../_internal/input-clear-button';
5
+ import { IconSlot } from '../icon';
6
+ import { default as SelectListbox } from './select-listbox.svelte';
7
+ const { value, suggestions, size = 'md', placeholder = '', disabled = false, readonly = false, inert = false, error = false, borderless = false, clearable = false, icon, name, id, autocomplete = 'off', 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, on } = $props();
8
+ let inputEl = $state.raw(undefined);
9
+ let rootEl = $state.raw(undefined);
10
+ let isOpen = $state(false);
11
+ let isFiltering = $state(false);
12
+ let highlight = $state(-1);
13
+ let lastCommitted = '';
14
+ let isRestoringFocus = false;
15
+ const listboxId = `input-suggest-${Math.random().toString(36).slice(2, 9)}`;
16
+ const currentValue = $derived(value ?? '');
17
+ const isInteractive = $derived(!disabled && !readonly && !inert);
18
+ const matches = $derived.by(() => {
19
+ if (!isFiltering) {
20
+ return suggestions;
21
+ }
22
+ const needle = currentValue.trim().toLowerCase();
23
+ if (!needle) {
24
+ return suggestions;
25
+ }
26
+ return suggestions.filter((suggestion) => suggestion.value.toLowerCase().includes(needle));
27
+ });
28
+ const rows = $derived(matches.map((suggestion) => ({
29
+ kind: 'option',
30
+ option: { label: suggestion.value, value: suggestion },
31
+ indent: false,
32
+ selected: false
33
+ })));
34
+ const isPanelOpen = $derived(isOpen && rows.length > 0);
35
+ const activeIndex = $derived(highlight >= 0 && highlight < rows.length ? highlight : -1);
36
+ const showClear = $derived(clearable && isInteractive && !!currentValue);
37
+ const openSuggestions = () => {
38
+ isOpen = true;
39
+ isFiltering = false;
40
+ highlight = -1;
41
+ };
42
+ const closeSuggestions = () => {
43
+ isOpen = false;
44
+ isFiltering = false;
45
+ highlight = -1;
46
+ };
47
+ // clearing the flag synchronously drops the guard when focus() dispatches late — handleFocus then reopens the popover after a pick
48
+ const restoreFocus = () => {
49
+ isRestoringFocus = true;
50
+ inputEl?.focus();
51
+ queueMicrotask(() => {
52
+ isRestoringFocus = false;
53
+ });
54
+ };
55
+ export const focus = () => inputEl?.focus();
56
+ export const select = () => inputEl?.select();
57
+ export const open = () => {
58
+ if (isInteractive) {
59
+ inputEl?.focus();
60
+ openSuggestions();
61
+ }
62
+ };
63
+ export const close = () => closeSuggestions();
64
+ const commitValue = (next) => {
65
+ lastCommitted = next;
66
+ on?.change?.(next);
67
+ };
68
+ const flushCommit = () => {
69
+ if (inputEl && inputEl.value !== lastCommitted) {
70
+ commitValue(inputEl.value);
71
+ }
72
+ };
73
+ const emitValue = (next) => {
74
+ if (inputEl) {
75
+ inputEl.value = next;
76
+ }
77
+ on?.input?.(next);
78
+ commitValue(next);
79
+ };
80
+ const applySuggestion = (suggestion) => {
81
+ closeSuggestions();
82
+ emitValue(suggestion.value);
83
+ on?.select?.(suggestion);
84
+ restoreFocus();
85
+ };
86
+ export const clear = () => {
87
+ if (!isInteractive) {
88
+ return;
89
+ }
90
+ emitValue('');
91
+ restoreFocus();
92
+ openSuggestions();
93
+ };
94
+ const handleRootClick = () => {
95
+ if (!isInteractive) {
96
+ return;
97
+ }
98
+ inputEl?.focus();
99
+ if (!isOpen) {
100
+ openSuggestions();
101
+ }
102
+ };
103
+ // calling openSuggestions() here expands the popover on Tab-focus
104
+ const handleFocus = () => {
105
+ if (isRestoringFocus) {
106
+ return;
107
+ }
108
+ lastCommitted = inputEl?.value ?? currentValue;
109
+ on?.focus?.();
110
+ };
111
+ const handleBlur = (event) => {
112
+ const next = event.relatedTarget;
113
+ // Row clicks blur the input before the click lands — closing here would unmount the row first.
114
+ if (next instanceof Node && rootEl?.contains(next)) {
115
+ return;
116
+ }
117
+ closeSuggestions();
118
+ flushCommit();
119
+ if (inputEl) {
120
+ inputEl.value = currentValue;
121
+ }
122
+ on?.blur?.();
123
+ };
124
+ const handleRootMousedown = (event) => {
125
+ if (event.target !== inputEl) {
126
+ event.preventDefault();
127
+ }
128
+ };
129
+ const handleInput = () => {
130
+ if (!inputEl) {
131
+ return;
132
+ }
133
+ if (isInteractive) {
134
+ isOpen = true;
135
+ isFiltering = true;
136
+ highlight = -1;
137
+ }
138
+ on?.input?.(inputEl.value);
139
+ };
140
+ const handleKeydown = (event) => {
141
+ on?.keydown?.(event);
142
+ if (!isInteractive) {
143
+ return;
144
+ }
145
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
146
+ const step = event.key === 'ArrowDown' ? 1 : -1;
147
+ event.preventDefault();
148
+ // plain `isOpen` — openSuggestions() here would drop the active filter and offer unrelated rows to Enter
149
+ isOpen = true;
150
+ const count = rows.length;
151
+ if (count === 0) {
152
+ highlight = -1;
153
+ }
154
+ else if (activeIndex < 0) {
155
+ highlight = step === 1 ? 0 : count - 1;
156
+ }
157
+ else {
158
+ highlight = (activeIndex + step + count) % count;
159
+ }
160
+ return;
161
+ }
162
+ if (event.key === 'Enter') {
163
+ const row = isPanelOpen && activeIndex >= 0 ? rows[activeIndex] : undefined;
164
+ if (row?.kind === 'option') {
165
+ event.preventDefault();
166
+ applySuggestion(row.option.value);
167
+ }
168
+ else {
169
+ flushCommit();
170
+ }
171
+ return;
172
+ }
173
+ if (event.key === 'Escape' && isPanelOpen) {
174
+ event.preventDefault();
175
+ closeSuggestions();
176
+ return;
177
+ }
178
+ if (event.key === 'Tab') {
179
+ closeSuggestions();
180
+ }
181
+ };
182
+ const handlePickRow = (row) => {
183
+ if (row.kind === 'option') {
184
+ applySuggestion(row.option.value);
185
+ }
186
+ };
187
+ </script>
188
+
189
+ {#snippet suggestionRow({ option }: { option: SelectOption<InputSuggestion> })}
190
+ <span class="input-suggest__suggestion">
191
+ <span class="input-suggest__suggestion-value">{option.label}</span>
192
+ {#if option.value.description}
193
+ <span class="input-suggest__suggestion-description">{option.value.description}</span>
194
+ {/if}
195
+ </span>
196
+ {/snippet}
197
+
198
+ <div
199
+ bind:this={rootEl}
200
+ class="input-suggest input-suggest--{size}"
201
+ class:input-suggest--open={isPanelOpen}
202
+ class:input-suggest--error={error}
203
+ class:input-suggest--disabled={disabled}
204
+ class:input-suggest--readonly={readonly}
205
+ class:input-suggest--inert={inert}
206
+ class:input-suggest--borderless={borderless}
207
+ onclick={handleRootClick}
208
+ onmousedown={handleRootMousedown}
209
+ onkeydown={() => undefined}
210
+ role="none">
211
+ {#if icon}
212
+ <span class="input-suggest__icon input-suggest__icon--leading" aria-hidden="true"><IconSlot icon={icon} /></span>
213
+ {/if}
214
+
215
+ <input
216
+ bind:this={inputEl}
217
+ class="input-suggest__input"
218
+ type="text"
219
+ role="combobox"
220
+ aria-haspopup="listbox"
221
+ aria-expanded={isPanelOpen}
222
+ aria-controls={listboxId}
223
+ aria-activedescendant={isPanelOpen && activeIndex >= 0 ? `${listboxId}-row-${activeIndex}` : undefined}
224
+ aria-autocomplete="list"
225
+ aria-label={ariaLabel}
226
+ aria-describedby={ariaDescribedby}
227
+ aria-required={ariaRequired ? 'true' : undefined}
228
+ aria-invalid={error ? 'true' : undefined}
229
+ id={id}
230
+ name={name}
231
+ autocomplete={autocomplete}
232
+ placeholder={placeholder}
233
+ disabled={disabled}
234
+ readonly={readonly}
235
+ inert={inert}
236
+ value={currentValue}
237
+ oninput={handleInput}
238
+ onfocus={handleFocus}
239
+ onblur={handleBlur}
240
+ onkeydown={handleKeydown} />
241
+
242
+ {#if showClear}
243
+ <InputClearButton size={size} on={{ clear }} />
244
+ {/if}
245
+
246
+ <SelectListbox
247
+ triggerEl={rootEl}
248
+ isOpen={isPanelOpen}
249
+ rows={rows}
250
+ highlight={activeIndex}
251
+ listboxId={listboxId}
252
+ optionSnippet={suggestionRow}
253
+ on={{ pickRow: handlePickRow, hoverRow: (index) => (highlight = index), dismiss: closeSuggestions }} />
254
+ </div>
255
+
256
+ <!--
257
+ @component
258
+ InputSuggest — a plain text field that offers suggestions. **NOT a select:** the value is the
259
+ string the user typed, it changes on every keystroke, and it is never constrained to the
260
+ suggestion list — typed text that matches nothing stays valid, and there is no "Use …" /
261
+ "Create …" confirmation step. `on.input` fires per keystroke exactly like `Input`'s; `on.change`
262
+ commits once per edit — when focus leaves the whole control, on Enter, or on pick / clear —
263
+ so reaching into the popover never commits the half-typed query. `on.select` additionally fires
264
+ when a suggestion was picked. `value` is controlled and is what the filter reads.
265
+
266
+ A click opens the full list, typing filters it by case-insensitive substring, no match closes the
267
+ popover and the field keeps working as an ordinary input. Focus alone opens nothing — tabbing in
268
+ just places the caret, as with the selects. ArrowDown / ArrowUp move through the
269
+ suggestions, Enter substitutes the highlighted one (and is left alone when nothing is
270
+ highlighted), Escape closes without touching the text, Tab closes and keeps what was typed.
271
+ Trigger chrome, popover and rows are shared with the selects, minus the chevron.
272
+
273
+ ### CSS Custom Properties
274
+ | Property | Description | Default |
275
+ |---|---|---|
276
+ | `--sc-kit--input-suggest--description--color` | Suggestion description line color | `var(--sc-kit--color--text--muted)` |
277
+ | `--sc-kit--input-suggest--description--font-size` | Suggestion description line font size | `var(--sc-kit--font-size--sm)` |
278
+
279
+ **Trigger** (shared with the selects): `--sc-kit--select--trigger--{height, padding-inline, gap,
280
+ background, background--disabled, border-color, border-color--hover, border-color--error,
281
+ border-radius, underline-color, font-size, color, placeholder--color, icon--color, icon--size}`.
282
+
283
+ **Popover panel:** `--sc-kit--select--panel--{background, border-color, border-radius, box-shadow,
284
+ max-height, padding}`.
285
+
286
+ **Suggestion rows:** `--sc-kit--select--option--{color, color--selected, background--active,
287
+ padding-inline}`. `--sc-kit--select--option--height` is NOT overridable here — the component pins
288
+ it to `auto` so a row with a `description` can grow to two lines.
289
+
290
+ **Clear button** (rendered when `clearable` and the value is non-empty):
291
+ `--sc-kit--input-clear-button--{size, icon-size, border-radius, color, color--hover,
292
+ background--hover}`.
293
+ -->
294
+
295
+ <style>.input-suggest {
296
+ --_sel--height: var(--sc-kit--select--trigger--height, var(--_sel--size-height, var(--sc-kit--field--height--md)));
297
+ --_sel--padding-inline: var(--sc-kit--select--trigger--padding-inline, var(--_sel--size-padding-inline, var(--sc-kit--field--padding-inline--md)));
298
+ --_sel--gap: var(--sc-kit--select--trigger--gap, var(--sc-kit--space--2));
299
+ --_sel--background: var(--sc-kit--select--trigger--background, var(--sc-kit--color--bg--field));
300
+ --_sel--background-disabled: var(--sc-kit--select--trigger--background--disabled, var(--sc-kit--color--bg--field-alt));
301
+ --_sel--border-color: var(--sc-kit--select--trigger--border-color, var(--sc-kit--color--border--field));
302
+ --_sel--border-color-hover: var(--sc-kit--select--trigger--border-color--hover, var(--sc-kit--color--border--strong));
303
+ --_sel--border-color-error: var(--sc-kit--select--trigger--border-color--error, var(--sc-kit--color--danger));
304
+ --_sel--border-radius: var(--sc-kit--select--trigger--border-radius, var(--_sel--size-border-radius, var(--sc-kit--field--border-radius--md)));
305
+ --_sel--underline-color: var(--sc-kit--select--trigger--underline-color, transparent);
306
+ --_sel--font-size: var(--sc-kit--select--trigger--font-size, var(--_sel--size-font-size, var(--sc-kit--font-size--md)));
307
+ --_sel--color: var(--sc-kit--select--trigger--color, var(--sc-kit--color--text--primary));
308
+ --_sel--placeholder-color: var(--sc-kit--select--trigger--placeholder--color, var(--sc-kit--color--text--placeholder));
309
+ --_sel--icon-color: var(--sc-kit--select--trigger--icon--color, var(--sc-kit--color--text--muted));
310
+ --_sel--icon-size: var(--sc-kit--select--trigger--icon--size, var(--_sel--size-icon-size, var(--sc-kit--field--icon--size--md)));
311
+ --_--input-affordance--opacity: 0;
312
+ --_--input-affordance--scale: 0;
313
+ }
314
+ .input-suggest:where(:hover), .input-suggest:where(:focus-within) {
315
+ --_--input-affordance--opacity: 1;
316
+ --_--input-affordance--scale: 1;
317
+ }
318
+ .input-suggest {
319
+ box-sizing: border-box;
320
+ position: relative;
321
+ display: inline-flex;
322
+ align-items: center;
323
+ width: 100%;
324
+ max-width: 100%;
325
+ height: var(--_sel--height);
326
+ padding: 0 var(--_sel--padding-inline);
327
+ background: var(--_sel--background);
328
+ border: 1px solid var(--_sel--border-color);
329
+ border-radius: var(--_sel--border-radius);
330
+ color: var(--_sel--color);
331
+ font-size: var(--_sel--font-size);
332
+ line-height: var(--sc-kit--leading--tight);
333
+ cursor: pointer;
334
+ box-shadow: inset 0 calc(-1 * max(2px, 0.125em)) var(--_sel--underline-color);
335
+ transition: background-color var(--sc-kit--duration--base) var(--sc-kit--ease--default), border-color var(--sc-kit--duration--base) var(--sc-kit--ease--default), color var(--sc-kit--duration--base) var(--sc-kit--ease--default), box-shadow var(--sc-kit--duration--base) var(--sc-kit--ease--default);
336
+ }
337
+ .input-suggest--sm {
338
+ --_sel--size-height: var(--sc-kit--field--height--sm);
339
+ --_sel--size-padding-inline: var(--sc-kit--field--padding-inline--sm);
340
+ --_sel--size-font-size: var(--sc-kit--font-size--sm);
341
+ --_sel--size-icon-size: var(--sc-kit--field--icon--size--sm);
342
+ --_sel--size-border-radius: var(--sc-kit--field--border-radius--sm);
343
+ }
344
+ .input-suggest--md {
345
+ --_sel--size-height: var(--sc-kit--field--height--md);
346
+ --_sel--size-padding-inline: var(--sc-kit--field--padding-inline--md);
347
+ --_sel--size-font-size: var(--sc-kit--font-size--md);
348
+ --_sel--size-icon-size: var(--sc-kit--field--icon--size--md);
349
+ --_sel--size-border-radius: var(--sc-kit--field--border-radius--md);
350
+ }
351
+ .input-suggest--lg {
352
+ --_sel--size-height: var(--sc-kit--field--height--lg);
353
+ --_sel--size-padding-inline: var(--sc-kit--field--padding-inline--lg);
354
+ --_sel--size-font-size: var(--sc-kit--font-size--lg);
355
+ --_sel--size-icon-size: var(--sc-kit--field--icon--size--lg);
356
+ --_sel--size-border-radius: var(--sc-kit--field--border-radius--md);
357
+ }
358
+ .input-suggest:where(:hover:not(:focus-within)) {
359
+ --sc-kit--select--trigger--border-color: var(--_sel--border-color-hover);
360
+ }
361
+ .input-suggest:where(:focus-within), .input-suggest--open {
362
+ --sc-kit--select--trigger--underline-color: var(--sc-kit--color--accent);
363
+ }
364
+ .input-suggest--open {
365
+ --sc-kit--select--trigger--border-color: unset;
366
+ }
367
+ .input-suggest--error {
368
+ --sc-kit--select--trigger--border-color: var(--_sel--border-color-error);
369
+ --sc-kit--select--trigger--underline-color: var(--sc-kit--color--danger);
370
+ }
371
+ .input-suggest--disabled, .input-suggest--readonly, .input-suggest--inert {
372
+ --sc-kit--select--trigger--border-color: unset;
373
+ --sc-kit--select--trigger--underline-color: unset;
374
+ --_--input-affordance--opacity: 0;
375
+ --_--input-affordance--scale: 0;
376
+ cursor: default;
377
+ }
378
+ .input-suggest--disabled {
379
+ --sc-kit--select--trigger--background: var(--_sel--background-disabled);
380
+ --sc-kit--select--trigger--color: var(--sc-kit--color--text--muted);
381
+ }
382
+ .input-suggest--borderless {
383
+ --sc-kit--select--trigger--background: transparent;
384
+ --sc-kit--select--trigger--border-color: transparent;
385
+ --sc-kit--select--trigger--underline-color: transparent;
386
+ box-shadow: none;
387
+ }
388
+ .input-suggest__input {
389
+ flex: 1;
390
+ min-width: 0;
391
+ height: 100%;
392
+ padding: 0;
393
+ margin: 0;
394
+ border: none;
395
+ outline: none;
396
+ background: transparent;
397
+ color: inherit;
398
+ font-family: inherit;
399
+ font-size: inherit;
400
+ line-height: inherit;
401
+ cursor: inherit;
402
+ }
403
+ .input-suggest__input::placeholder {
404
+ color: var(--_sel--placeholder-color);
405
+ }
406
+ .input-suggest__input:not([readonly]) {
407
+ cursor: text;
408
+ }
409
+ .input-suggest__input--hidden {
410
+ opacity: 0;
411
+ width: 0;
412
+ flex: 0;
413
+ }
414
+ .input-suggest__input--no-search {
415
+ caret-color: transparent;
416
+ cursor: pointer;
417
+ }
418
+ .input-suggest__selection {
419
+ flex: 0 1 auto;
420
+ min-width: 0;
421
+ text-overflow: ellipsis;
422
+ max-width: 100%;
423
+ white-space: nowrap;
424
+ overflow: hidden;
425
+ }
426
+ .input-suggest__icon {
427
+ --sc-kit--icon--color: var(--_sel--icon-color);
428
+ --sc-kit--icon--size: var(--_sel--icon-size);
429
+ flex-shrink: 0;
430
+ display: inline-flex;
431
+ align-items: center;
432
+ justify-content: center;
433
+ line-height: 0;
434
+ }
435
+ .input-suggest__icon--leading {
436
+ margin-inline-end: var(--_sel--gap);
437
+ }
438
+ .input-suggest__icon--trailing {
439
+ margin-inline-start: var(--_sel--gap);
440
+ }
441
+ .input-suggest__spinner {
442
+ color: var(--_sel--icon-color);
443
+ }
444
+ .input-suggest--multi {
445
+ height: auto;
446
+ min-height: var(--_sel--height);
447
+ flex-wrap: wrap;
448
+ padding-block: 0.25rem;
449
+ padding-inline-end: calc(var(--_sel--padding-inline) + var(--_sel--icon-size) + var(--_sel--gap));
450
+ gap: var(--sc-kit--space--1);
451
+ }
452
+ .input-suggest__more {
453
+ display: inline-flex;
454
+ align-items: center;
455
+ padding: 0 var(--sc-kit--space--2);
456
+ font-size: var(--sc-kit--font-size--sm);
457
+ color: var(--sc-kit--color--text--muted);
458
+ user-select: none;
459
+ }
460
+ .input-suggest {
461
+ --_isg--description-color: var(--sc-kit--input-suggest--description--color, var(--sc-kit--color--text--muted));
462
+ --_isg--description-font-size: var(--sc-kit--input-suggest--description--font-size, var(--sc-kit--font-size--sm));
463
+ --sc-kit--select--option--height: auto;
464
+ cursor: text;
465
+ }
466
+ .input-suggest--disabled, .input-suggest--readonly, .input-suggest--inert {
467
+ cursor: default;
468
+ }
469
+ .input-suggest__suggestion {
470
+ box-sizing: border-box;
471
+ display: flex;
472
+ flex: 1;
473
+ flex-direction: column;
474
+ justify-content: center;
475
+ gap: 0.125rem;
476
+ min-width: 0;
477
+ min-block-size: var(--_sel--height);
478
+ padding-block: var(--sc-kit--space--1);
479
+ }
480
+ .input-suggest__suggestion-value {
481
+ text-overflow: ellipsis;
482
+ max-width: 100%;
483
+ white-space: nowrap;
484
+ overflow: hidden;
485
+ }
486
+ .input-suggest__suggestion-description {
487
+ color: var(--_isg--description-color);
488
+ font-size: var(--_isg--description-font-size);
489
+ white-space: pre-line;
490
+ word-break: break-word;
491
+ display: -webkit-box;
492
+ overflow: hidden;
493
+ -webkit-box-orient: vertical;
494
+ -webkit-line-clamp: 2;
495
+ }</style>
@@ -0,0 +1,97 @@
1
+ export type InputSuggestInstance = {
2
+ focus: () => void;
3
+ select: () => void;
4
+ clear: () => void;
5
+ open: () => void;
6
+ close: () => void;
7
+ };
8
+ import { type IconProp } from '../icon';
9
+ import type { InputSuggestion } from './types';
10
+ import type { HTMLInputAttributes } from 'svelte/elements';
11
+ type Props = {
12
+ /** Controlled — feed `on.input` back into it; the suggestion filter reads this prop, not the DOM. */
13
+ value: string | null | undefined;
14
+ /** Advisory shortcuts offered in the popover. Never a constraint — any typed text stays valid. */
15
+ suggestions: InputSuggestion[];
16
+ /** @default 'md' */
17
+ size?: 'sm' | 'md' | 'lg';
18
+ placeholder?: string;
19
+ disabled?: boolean;
20
+ /** Non-editable but selectable; the popover never opens. */
21
+ readonly?: boolean;
22
+ /** Visual display mode — non-focusable, no hover/focus animations. */
23
+ inert?: boolean;
24
+ /** Visual error state — also sets `aria-invalid="true"`. */
25
+ error?: boolean;
26
+ /** Strip border + background + focus underline. */
27
+ borderless?: boolean;
28
+ /** Show the × clear button when the value is non-empty. */
29
+ clearable?: boolean;
30
+ /** Leading icon — string SVG source, `{ src, color?, size? }` object, or custom snippet. */
31
+ icon?: IconProp;
32
+ name?: string;
33
+ id?: string;
34
+ /** @default 'off' */
35
+ autocomplete?: HTMLInputAttributes['autocomplete'];
36
+ 'aria-label'?: string;
37
+ 'aria-describedby'?: string;
38
+ 'aria-required'?: boolean;
39
+ on?: {
40
+ /** Fires on every keystroke, and on pick / clear — same signature and timing as `Input`. */
41
+ input?: (value: string) => void;
42
+ /** Fires when an edit is committed — focus leaving the control or Enter — and on pick / clear. */
43
+ change?: (value: string) => void;
44
+ /** Fires after `input` and `change` when the user picked a suggestion. */
45
+ select?: (suggestion: InputSuggestion) => void;
46
+ /** Fires when focus leaves the whole control — reaching into the popover does not count as leaving. */
47
+ blur?: () => void;
48
+ focus?: () => void;
49
+ keydown?: (event: KeyboardEvent) => void;
50
+ };
51
+ };
52
+ /**
53
+ * InputSuggest — a plain text field that offers suggestions. **NOT a select:** the value is the
54
+ * string the user typed, it changes on every keystroke, and it is never constrained to the
55
+ * suggestion list — typed text that matches nothing stays valid, and there is no "Use …" /
56
+ * "Create …" confirmation step. `on.input` fires per keystroke exactly like `Input`'s; `on.change`
57
+ * commits once per edit — when focus leaves the whole control, on Enter, or on pick / clear —
58
+ * so reaching into the popover never commits the half-typed query. `on.select` additionally fires
59
+ * when a suggestion was picked. `value` is controlled and is what the filter reads.
60
+ *
61
+ * A click opens the full list, typing filters it by case-insensitive substring, no match closes the
62
+ * popover and the field keeps working as an ordinary input. Focus alone opens nothing — tabbing in
63
+ * just places the caret, as with the selects. ArrowDown / ArrowUp move through the
64
+ * suggestions, Enter substitutes the highlighted one (and is left alone when nothing is
65
+ * highlighted), Escape closes without touching the text, Tab closes and keeps what was typed.
66
+ * Trigger chrome, popover and rows are shared with the selects, minus the chevron.
67
+ *
68
+ * ### CSS Custom Properties
69
+ * | Property | Description | Default |
70
+ * |---|---|---|
71
+ * | `--sc-kit--input-suggest--description--color` | Suggestion description line color | `var(--sc-kit--color--text--muted)` |
72
+ * | `--sc-kit--input-suggest--description--font-size` | Suggestion description line font size | `var(--sc-kit--font-size--sm)` |
73
+ *
74
+ * **Trigger** (shared with the selects): `--sc-kit--select--trigger--{height, padding-inline, gap,
75
+ * background, background--disabled, border-color, border-color--hover, border-color--error,
76
+ * border-radius, underline-color, font-size, color, placeholder--color, icon--color, icon--size}`.
77
+ *
78
+ * **Popover panel:** `--sc-kit--select--panel--{background, border-color, border-radius, box-shadow,
79
+ * max-height, padding}`.
80
+ *
81
+ * **Suggestion rows:** `--sc-kit--select--option--{color, color--selected, background--active,
82
+ * padding-inline}`. `--sc-kit--select--option--height` is NOT overridable here — the component pins
83
+ * it to `auto` so a row with a `description` can grow to two lines.
84
+ *
85
+ * **Clear button** (rendered when `clearable` and the value is non-empty):
86
+ * `--sc-kit--input-clear-button--{size, icon-size, border-radius, color, color--hover,
87
+ * background--hover}`.
88
+ */
89
+ declare const Cmp: import("svelte").Component<Props, {
90
+ focus: () => void;
91
+ select: () => void;
92
+ open: () => void;
93
+ close: () => void;
94
+ clear: () => void;
95
+ }, "">;
96
+ type Cmp = ReturnType<typeof Cmp>;
97
+ export default Cmp;
@@ -1,3 +1,5 @@
1
+ export { default as InputSuggest } from './cmp.input-suggest.svelte';
2
+ export type { InputSuggestInstance } from './cmp.input-suggest.svelte';
1
3
  export { default as Singleselect } from './cmp.singleselect.svelte';
2
4
  export type { SingleselectInstance } from './cmp.singleselect.svelte';
3
5
  export { default as SingleselectAsync } from './cmp.singleselect-async.svelte';
@@ -9,4 +11,4 @@ export type { MultiselectAsyncInstance } from './cmp.multiselect-async.svelte';
9
11
  export { default as MultiselectTree } from './cmp.multiselect-tree.svelte';
10
12
  export type { MultiselectTreeInstance } from './cmp.multiselect-tree.svelte';
11
13
  export { isSelectGroup } from './types';
12
- export type { SelectCreateOptions, SelectItem, SelectOption, SelectOptionGroup } from './types';
14
+ export type { InputSuggestion, SelectCreateOptions, SelectItem, SelectOption, SelectOptionGroup } from './types';
@@ -1,3 +1,4 @@
1
+ export { default as InputSuggest } from './cmp.input-suggest.svelte';
1
2
  export { default as Singleselect } from './cmp.singleselect.svelte';
2
3
  export { default as SingleselectAsync } from './cmp.singleselect-async.svelte';
3
4
  export { default as Multiselect } from './cmp.multiselect.svelte';
@@ -47,6 +47,16 @@ export type SelectRowCreate = {
47
47
  query: string;
48
48
  };
49
49
  export type SelectRow<T> = SelectRowOption<T> | SelectRowGroupHeader<T> | SelectRowCreate;
50
+ /**
51
+ * One advisory row of `InputSuggest`. Suggestions never constrain the typed value — they only
52
+ * offer a shortcut to a known string.
53
+ */
54
+ export type InputSuggestion = {
55
+ /** Text substituted into the field when the row is picked — also the row's label. */
56
+ value: string;
57
+ /** Secondary line rendered under the value in a muted tone. */
58
+ description?: string;
59
+ };
50
60
  /** Creatable mode — passing this object to a select enables the create UI. */
51
61
  export type SelectCreateOptions = {
52
62
  /** Validates each query — only when it returns true does the create UI appear (and the query isn't a duplicate of an existing label). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamscloud/kit",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "author": "StreamsCloud",
5
5
  "repository": {
6
6
  "type": "git",