@globalbrain/sefirot 4.56.0 → 4.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/lib/blocks/lens/components/LensFormFilterCondition.vue +2 -1
  2. package/lib/blocks/lens/components/LensFormFilterGroup.vue +1 -1
  3. package/lib/blocks/lens/components/{LensAvatarInput.vue → LensInputAvatar.vue} +5 -3
  4. package/lib/blocks/lens/components/LensInputDropdown.vue +102 -0
  5. package/lib/blocks/lens/components/LensSheetAvatarField.vue +2 -2
  6. package/lib/blocks/lens/components/LensTableEditableCell.vue +5 -4
  7. package/lib/blocks/lens/fields/AvatarField.ts +2 -2
  8. package/lib/blocks/lens/fields/Field.ts +10 -6
  9. package/lib/blocks/lens/filter-inputs/SelectFilterInput.ts +2 -1
  10. package/lib/components/SButton.vue +2 -10
  11. package/lib/components/SControlInputSearch.vue +6 -3
  12. package/lib/components/SDescPill.vue +3 -2
  13. package/lib/components/SDescState.vue +3 -2
  14. package/lib/components/SInputAddon.vue +4 -2
  15. package/lib/components/SInputAsyncDropdown.vue +761 -0
  16. package/lib/components/SInputBase.vue +1 -3
  17. package/lib/components/SInputCheckbox.vue +10 -18
  18. package/lib/components/SInputCheckboxes.vue +16 -33
  19. package/lib/components/SInputDate.vue +10 -20
  20. package/lib/components/SInputDropdown.vue +22 -43
  21. package/lib/components/SInputDropdownItem.vue +10 -27
  22. package/lib/components/SInputFile.vue +10 -18
  23. package/lib/components/SInputFileUpload.vue +12 -34
  24. package/lib/components/SInputFileUploadItem.vue +7 -21
  25. package/lib/components/SInputHMS.vue +6 -7
  26. package/lib/components/SInputImage.vue +11 -16
  27. package/lib/components/SInputNumber.vue +1 -3
  28. package/lib/components/SInputRadio.vue +9 -19
  29. package/lib/components/SInputRadios.vue +18 -40
  30. package/lib/components/SInputSegments.vue +15 -26
  31. package/lib/components/SInputSegmentsOption.vue +7 -7
  32. package/lib/components/SInputSelect.vue +14 -19
  33. package/lib/components/SInputSwitch.vue +10 -19
  34. package/lib/components/SInputSwitches.vue +16 -30
  35. package/lib/components/SInputText.vue +1 -4
  36. package/lib/components/SInputTextarea.vue +0 -2
  37. package/lib/components/SInputYMD.vue +6 -7
  38. package/lib/components/SPagination.vue +1 -1
  39. package/lib/components/SPill.vue +2 -2
  40. package/lib/components/SState.vue +2 -2
  41. package/lib/components/STableCellPill.vue +3 -2
  42. package/lib/components/STableCellState.vue +3 -2
  43. package/lib/composables/Table.ts +8 -15
  44. package/lib/support/Color.ts +4 -0
  45. package/lib/support/InputBase.ts +5 -0
  46. package/lib/support/InputDropdown.ts +15 -0
  47. package/lib/support/InputFileUpload.ts +22 -0
  48. package/lib/support/InputOption.ts +8 -0
  49. package/lib/support/InputText.ts +6 -0
  50. package/package.json +1 -1
@@ -0,0 +1,761 @@
1
+ <script setup lang="ts" generic="T, Multiple extends boolean = false">
2
+ import IconCaretDown from '~icons/ph/caret-down'
3
+ import IconCaretUp from '~icons/ph/caret-up'
4
+ import IconCheck from '~icons/ph/check'
5
+ import { type Ref, computed, nextTick, onUnmounted, ref, watch } from 'vue'
6
+ import { useManualDropdownPosition } from '../composables/Dropdown'
7
+ import { useFlyout } from '../composables/Flyout'
8
+ import { useTrans } from '../composables/Lang'
9
+ import { type Option } from '../support/InputDropdown'
10
+ import SDropdownSectionFilterItem from './SDropdownSectionFilterItem.vue'
11
+ import SInputBase, { type Props as BaseProps } from './SInputBase.vue'
12
+ import SInputDropdownItem from './SInputDropdownItem.vue'
13
+ import SSpinner from './SSpinner.vue'
14
+
15
+ export interface Props<T = any, Multiple extends boolean = false> extends BaseProps {
16
+ placeholder?: string
17
+ // Fetch the items matching `query` from the server. Called when the dropdown
18
+ // opens (with the current query, usually empty for the initial list) and again,
19
+ // debounced, on each keystroke. Out-of-order responses are discarded, so it
20
+ // does not need to guard against races itself.
21
+ fetch: (query: string) => Promise<T[]>
22
+ // Render an item as its dropdown option and, once selected, as its chip. The
23
+ // option's `value` is the item's identity — selection, de-duplication and
24
+ // removal all key off it. Applied to fetched items and to the bound model
25
+ // alike, so a seeded selection renders without appearing in any fetch.
26
+ toOption: (item: T) => Option
27
+ // Decide whether a `fetch` rejection should propagate (be re-thrown) instead of
28
+ // being swallowed into an empty list — e.g. to let auth/session failures reach
29
+ // the app's re-auth flow. By default every rejection is swallowed.
30
+ rethrow?: (error: unknown) => boolean
31
+ // Whether multiple items can be selected. The model is then an array of items;
32
+ // otherwise it is a single item or `null`. Typed `boolean & Multiple` (equal to
33
+ // `Multiple`, since it extends `boolean`) rather than a bare `Multiple`: the
34
+ // intersection keeps Vue's runtime Boolean casting (a bare `multiple` attribute
35
+ // would otherwise arrive as `''` and read as false), while `Multiple` still
36
+ // drives the arity-conditional model type.
37
+ multiple?: boolean & Multiple
38
+ // Whether the (single) value can be cleared, or the (multiple) selection emptied.
39
+ nullable?: boolean
40
+ disabled?: boolean
41
+ position?: 'top' | 'bottom'
42
+ // Whether to keep the dropdown open after a selection. Defaults to closing in
43
+ // single mode and staying open in multiple mode.
44
+ closeOnSelect?: boolean
45
+ // Debounce window (ms) between a keystroke and the refetch.
46
+ debounce?: number
47
+ }
48
+
49
+ // `closeOnSelect` defaults to `undefined` (not Vue's usual absent-Boolean → false)
50
+ // so the `?? !multiple` fallback in onSelect can tell "omitted" from an explicit
51
+ // `false`, keeping the single-select-closes / multiple-stays-open default.
52
+ const props = withDefaults(defineProps<Props<T, Multiple>>(), { closeOnSelect: undefined })
53
+
54
+ // The selection: an array of items when `multiple`, otherwise a single item or
55
+ // `null`. Items are the consumer's own model type; the option rendered for each
56
+ // is derived on demand via `toOption`, so nothing extra is stored on the model.
57
+ const model = defineModel<Multiple extends true ? T[] : T | null>({ required: true })
58
+
59
+ // Write the model regardless of arity. The public type is arity-conditional, so
60
+ // the concrete array / single value is cast at this single boundary.
61
+ function commit(value: T[] | T | null): void {
62
+ model.value = value as typeof model.value
63
+ }
64
+
65
+ const { t } = useTrans({
66
+ en: {
67
+ ph: 'Search',
68
+ searching: 'Searching…',
69
+ not_found: 'No options found.'
70
+ },
71
+ ja: {
72
+ ph: '検索',
73
+ searching: '検索中…',
74
+ not_found: 'オプションが見つかりません。'
75
+ }
76
+ })
77
+
78
+ const container = ref<HTMLDivElement>()
79
+ const input = ref<HTMLInputElement | null>(null)
80
+ const list = ref<HTMLUListElement | null>(null)
81
+
82
+ const { isOpen, open, close } = useFlyout(container)
83
+ const { inset, update: updatePosition } = useManualDropdownPosition(
84
+ container,
85
+ () => props.position
86
+ )
87
+
88
+ const query = ref('')
89
+ const items = ref([]) as Ref<T[]>
90
+ const loading = ref(false)
91
+
92
+ // Monotonic token so only the latest fetch may assign `items` / clear `loading`.
93
+ // With a debounced, user-driven search several requests can overlap; an older one
94
+ // resolving late must not clobber the newer result.
95
+ let fetchSeq = 0
96
+
97
+ const classes = computed(() => [
98
+ props.size ?? 'small',
99
+ { disabled: props.disabled }
100
+ ])
101
+
102
+ // The selected items as a flat array, regardless of arity, for internal handling.
103
+ const selectedItems = computed<T[]>(() => {
104
+ if (props.multiple) {
105
+ return Array.isArray(model.value) ? (model.value as T[]) : []
106
+ }
107
+ return model.value != null ? [model.value as T] : []
108
+ })
109
+
110
+ // The identities currently selected, for O(1) active / de-dup checks.
111
+ const selectedValues = computed(
112
+ () => new Set(selectedItems.value.map((item) => props.toOption(item).value))
113
+ )
114
+
115
+ // The option(s) rendered for the current selection: an array of chips when
116
+ // multiple, a single chip otherwise (matching SInputDropdownItem's `item`).
117
+ const selectedOptions = computed<Option | Option[] | null>(() => {
118
+ if (props.multiple) {
119
+ return selectedItems.value.map((item) => props.toOption(item))
120
+ }
121
+ return model.value != null ? props.toOption(model.value as T) : null
122
+ })
123
+
124
+ // The fetched items paired with their rendered option, for the dropdown list.
125
+ const optionItems = computed(() =>
126
+ items.value.map((item) => ({ item, option: props.toOption(item) }))
127
+ )
128
+
129
+ const hasSelected = computed(() => selectedItems.value.length > 0)
130
+
131
+ const removable = computed(() => {
132
+ if (props.multiple) {
133
+ return !!props.nullable || selectedItems.value.length > 1
134
+ }
135
+ return !!props.nullable
136
+ })
137
+
138
+ async function runFetch(q: string): Promise<void> {
139
+ const seq = ++fetchSeq
140
+ loading.value = true
141
+ try {
142
+ const res = await props.fetch(q)
143
+ if (seq === fetchSeq) {
144
+ items.value = res
145
+ }
146
+ } catch (e) {
147
+ // A failed search yields no options (the empty state shows)…
148
+ if (seq === fetchSeq) {
149
+ items.value = []
150
+ }
151
+ // …unless the consumer wants this failure propagated (e.g. an auth/session
152
+ // error that must reach the app's re-auth flow rather than look like "no
153
+ // results"). Re-throw regardless of `seq`: the failure is real even if a
154
+ // newer search has since superseded this one.
155
+ if (props.rethrow?.(e)) {
156
+ throw e
157
+ }
158
+ } finally {
159
+ if (seq === fetchSeq) {
160
+ loading.value = false
161
+ }
162
+ }
163
+ }
164
+
165
+ // Debounce keystroke-driven refetches with a cancelable timer. The pending timer
166
+ // is cancelled on close and before each immediate open-fetch, so a keystroke from
167
+ // a previous open session can't fire (as the latest fetch) after a reopen and
168
+ // clobber the fresh initial list.
169
+ let debounceTimer: ReturnType<typeof setTimeout> | undefined
170
+
171
+ function cancelPendingFetch(): void {
172
+ if (debounceTimer !== undefined) {
173
+ clearTimeout(debounceTimer)
174
+ debounceTimer = undefined
175
+ }
176
+ }
177
+
178
+ // Refetch as the user types, but only while open (the reset-on-close below also
179
+ // writes `query`, and that shouldn't fire a fetch against a closed dropdown).
180
+ watch(query, (q) => {
181
+ if (!isOpen.value) {
182
+ return
183
+ }
184
+ cancelPendingFetch()
185
+ // Supersede any in-flight fetch right away — not only once the timer fires. A
186
+ // previous query's request that resolves during the debounce window would
187
+ // otherwise still pass runFetch's `seq === fetchSeq` check and replace the
188
+ // options (and clear loading) for a query the input has already moved past.
189
+ // Bumping the token invalidates it; keep `loading` on so the pending search
190
+ // stays indicated until the new fetch resolves.
191
+ fetchSeq++
192
+ loading.value = true
193
+ debounceTimer = setTimeout(() => {
194
+ debounceTimer = undefined
195
+ runFetch(q)
196
+ }, props.debounce ?? 250)
197
+ })
198
+
199
+ function onOpen() {
200
+ if (props.disabled) {
201
+ return
202
+ }
203
+ updatePosition()
204
+ open()
205
+ // Fetch the current (initial) list immediately — dropping any keystroke timer
206
+ // left armed from a previous open — and focus the search input.
207
+ cancelPendingFetch()
208
+ runFetch(query.value)
209
+ nextTick(() => input.value?.focus())
210
+ }
211
+
212
+ watch(isOpen, (value) => {
213
+ // On close: drop any pending refetch, invalidate any in-flight fetch (so a late
214
+ // response can't repopulate the list while closed), and clear the query /
215
+ // items / loading. Reopening then starts fresh instead of briefly rendering
216
+ // the previous query's results (which would be selectable). The selected chips
217
+ // are unaffected — they render from the model, not from the fetched `items`.
218
+ if (!value) {
219
+ cancelPendingFetch()
220
+ fetchSeq++
221
+ query.value = ''
222
+ items.value = []
223
+ loading.value = false
224
+ }
225
+ })
226
+
227
+ onUnmounted(cancelPendingFetch)
228
+
229
+ // Close the dropdown if the control becomes disabled while open (e.g. a form
230
+ // submission starts), so its options can't be selected until it re-enables.
231
+ // `onSelect` also re-checks `props.disabled` as a belt-and-braces guard.
232
+ watch(() => props.disabled, (disabled) => {
233
+ if (disabled) {
234
+ close()
235
+ }
236
+ })
237
+
238
+ function isActive(value: any): boolean {
239
+ return selectedValues.value.has(value)
240
+ }
241
+
242
+ function onSelect(item: T): void {
243
+ const option = props.toOption(item)
244
+ if (props.disabled || option.disabled) {
245
+ return
246
+ }
247
+
248
+ props.validation?.$touch()
249
+
250
+ const value = option.value
251
+
252
+ if (props.multiple) {
253
+ const current = selectedItems.value
254
+ const next = selectedValues.value.has(value)
255
+ ? current.filter((i) => props.toOption(i).value !== value)
256
+ : [...current, item]
257
+ // Keep at least one unless clearing is allowed (mirrors SInputDropdown).
258
+ if (next.length !== 0 || props.nullable) {
259
+ commit(next)
260
+ }
261
+ } else if (!selectedValues.value.has(value)) {
262
+ commit(item)
263
+ } else if (props.nullable) {
264
+ commit(null)
265
+ }
266
+
267
+ if (props.closeOnSelect ?? !props.multiple) {
268
+ close()
269
+ }
270
+ }
271
+
272
+ // Remove a selected chip from the box (the X on SInputDropdownItem).
273
+ function onRemove(value: any): void {
274
+ props.validation?.$touch()
275
+
276
+ if (props.multiple) {
277
+ commit(selectedItems.value.filter((item) => props.toOption(item).value !== value))
278
+ } else {
279
+ commit(null)
280
+ }
281
+ }
282
+
283
+ // Move focus from the search input into the first option (ArrowDown from the box).
284
+ function focusFirstOption(): void {
285
+ list.value?.querySelector<HTMLElement>('.button:not([disabled])')?.focus()
286
+ }
287
+
288
+ // The next/previous enabled option button, skipping disabled ones (consistent
289
+ // with focusFirstOption and the click guard).
290
+ function enabledSibling(el: HTMLElement, forward: boolean): HTMLElement | null {
291
+ let li: Element | null | undefined = el.parentElement
292
+ for (;;) {
293
+ li = forward ? li?.nextElementSibling : li?.previousElementSibling
294
+ if (!li) {
295
+ return null
296
+ }
297
+ const button = li.querySelector<HTMLElement>('.button:not([disabled])')
298
+ if (button) {
299
+ return button
300
+ }
301
+ }
302
+ }
303
+
304
+ function focusPrev(event: any): void {
305
+ // ArrowUp from the first option returns focus to the search input.
306
+ const prev = enabledSibling(event.target as HTMLElement, false)
307
+ prev ? prev.focus() : input.value?.focus()
308
+ }
309
+
310
+ function focusNext(event: any): void {
311
+ enabledSibling(event.target as HTMLElement, true)?.focus()
312
+ }
313
+ </script>
314
+
315
+ <template>
316
+ <SInputBase
317
+ class="SInputAsyncDropdown"
318
+ :class="classes"
319
+ :size
320
+ :name
321
+ :label
322
+ :note
323
+ :info
324
+ :help
325
+ :check-icon
326
+ :check-text
327
+ :check-color
328
+ :validation
329
+ :warning
330
+ :hide-error
331
+ :hide-warning
332
+ >
333
+ <div ref="container" class="container">
334
+ <div
335
+ class="box"
336
+ role="button"
337
+ tabindex="0"
338
+ @click="onOpen"
339
+ @keydown.down.prevent
340
+ @keyup.enter="onOpen"
341
+ @keyup.down="onOpen"
342
+ >
343
+ <div class="box-content">
344
+ <SInputDropdownItem
345
+ v-if="hasSelected"
346
+ :item="selectedOptions!"
347
+ :size="size ?? 'small'"
348
+ :removable
349
+ :disabled="disabled ?? false"
350
+ @remove="onRemove"
351
+ />
352
+
353
+ <div v-else class="box-placeholder">{{ placeholder ?? t.ph }}</div>
354
+ </div>
355
+
356
+ <div class="box-icon">
357
+ <IconCaretUp class="box-icon-svg up" />
358
+ <IconCaretDown class="box-icon-svg down" />
359
+ </div>
360
+ </div>
361
+
362
+ <div v-if="isOpen" class="dropdown" :style="inset">
363
+ <div class="dropdown-content">
364
+ <div class="search">
365
+ <!-- Keep Enter inside the search field: it drives the dropdown and
366
+ must neither bubble to an enclosing form (`.stop`) nor trigger the
367
+ browser's implicit form submission (`.prevent`). ArrowDown moves
368
+ focus into the option list. -->
369
+ <input
370
+ ref="input"
371
+ v-model="query"
372
+ class="search-input"
373
+ :placeholder="t.ph"
374
+ @keydown.enter.stop.prevent
375
+ @keydown.down.prevent
376
+ @keyup.down.prevent="focusFirstOption"
377
+ >
378
+ <!-- A refetch while options are already shown keeps the (stale) list
379
+ visible; this spinner signals that newer results are loading. -->
380
+ <SSpinner v-if="loading && items.length" class="search-spinner" />
381
+ </div>
382
+
383
+ <ul v-if="items.length" ref="list" class="list">
384
+ <li v-for="{ item, option } in optionItems" :key="option.value" class="item">
385
+ <button
386
+ class="button"
387
+ :class="{ active: isActive(option.value) }"
388
+ type="button"
389
+ :disabled="option.disabled"
390
+ tabindex="0"
391
+ @keyup.up.prevent="focusPrev"
392
+ @keyup.down.prevent="focusNext"
393
+ @click="onSelect(item)"
394
+ >
395
+ <span v-if="multiple" class="checkbox">
396
+ <span class="checkbox-box">
397
+ <IconCheck class="checkbox-icon" />
398
+ </span>
399
+ </span>
400
+ <span v-else class="radio">
401
+ <span class="radio-dot" />
402
+ </span>
403
+ <span class="option-item">
404
+ <SDropdownSectionFilterItem :option />
405
+ </span>
406
+ </button>
407
+ </li>
408
+ </ul>
409
+
410
+ <div v-else-if="loading" class="state">
411
+ <SSpinner class="state-spinner" />
412
+ <span class="state-text">{{ t.searching }}</span>
413
+ </div>
414
+
415
+ <p v-else class="empty">{{ t.not_found }}</p>
416
+ </div>
417
+ </div>
418
+ </div>
419
+ <template v-if="$slots.info" #info><slot name="info" /></template>
420
+ </SInputBase>
421
+ </template>
422
+
423
+ <style scoped lang="postcss">
424
+ .container {
425
+ position: relative;
426
+ width: 100%;
427
+ }
428
+
429
+ .box {
430
+ position: relative;
431
+ display: flex;
432
+ align-items: center;
433
+ border: 1px solid var(--input-border-color);
434
+ border-radius: 8px;
435
+ width: 100%;
436
+ color: var(--input-text);
437
+ background-color: var(--input-bg-color);
438
+ box-shadow: var(--input-box-shadow);
439
+ cursor: pointer;
440
+ transition: border-color 0.25s, background-color 0.25s;
441
+
442
+ &:hover {
443
+ border-color: var(--input-hover-border-color);
444
+ }
445
+ }
446
+
447
+ .box-content {
448
+ display: flex;
449
+ align-items: center;
450
+ flex-grow: 1;
451
+ max-width: 100%;
452
+ }
453
+
454
+ .box-placeholder {
455
+ line-height: 24px;
456
+ color: var(--input-placeholder-color);
457
+ overflow: hidden;
458
+ white-space: nowrap;
459
+ }
460
+
461
+ .box-icon {
462
+ position: absolute;
463
+ z-index: 10;
464
+ cursor: pointer;
465
+ }
466
+
467
+ .box-icon-svg {
468
+ display: block;
469
+ width: 14px;
470
+ height: 14px;
471
+ color: var(--c-text-2);
472
+ }
473
+
474
+ .box-icon-svg.up {
475
+ margin-bottom: -4px;
476
+ }
477
+
478
+ .dropdown {
479
+ position: fixed;
480
+ z-index: var(--z-index-dropdown);
481
+ }
482
+
483
+ .dropdown-content {
484
+ border: 1px solid var(--c-border-mute-1);
485
+ border-radius: 8px;
486
+ background-color: var(--c-bg-elv-3);
487
+ box-shadow: var(--shadow-depth-3);
488
+ overflow: hidden;
489
+ }
490
+
491
+ .search {
492
+ position: sticky;
493
+ top: 0;
494
+ z-index: 10;
495
+ border-bottom: 1px solid var(--c-gutter);
496
+ padding: 8px;
497
+ background-color: var(--c-bg-elv-3);
498
+ }
499
+
500
+ .search-spinner {
501
+ position: absolute;
502
+ top: 50%;
503
+ right: 16px;
504
+ width: 16px;
505
+ height: 16px;
506
+ transform: translateY(-50%);
507
+ color: var(--c-text-2);
508
+ }
509
+
510
+ .search-input {
511
+ border: 1px solid var(--c-divider);
512
+ border-radius: 6px;
513
+ padding: 0 8px;
514
+ width: 100%;
515
+ font-size: 14px;
516
+ font-family: var(--input-value-font-family);
517
+ line-height: 32px;
518
+ background-color: var(--input-bg-color);
519
+ transition: border-color 0.25s;
520
+
521
+ &::placeholder {
522
+ color: var(--input-placeholder-color);
523
+ }
524
+
525
+ &:hover { border-color: var(--input-hover-border-color); }
526
+ &:focus { border-color: var(--input-focus-border-color); }
527
+ }
528
+
529
+ .list {
530
+ max-height: 280px;
531
+ padding: 8px;
532
+ overflow-y: auto;
533
+ }
534
+
535
+ .button {
536
+ display: flex;
537
+ border-radius: 6px;
538
+ padding: 0 8px;
539
+ width: 100%;
540
+ text-align: left;
541
+ transition: color 0.25s, background-color 0.25s;
542
+
543
+ &:hover:not(:disabled) {
544
+ background-color: var(--c-bg-mute-1);
545
+ }
546
+
547
+ &:disabled {
548
+ cursor: not-allowed;
549
+ opacity: 0.5;
550
+ }
551
+ }
552
+
553
+ .checkbox {
554
+ display: block;
555
+ padding-top: 8px;
556
+ padding-bottom: 8px;
557
+ }
558
+
559
+ .checkbox-box {
560
+ display: flex;
561
+ justify-content: center;
562
+ align-items: center;
563
+ border: 1px solid var(--c-border-mute-1);
564
+ border-radius: 4px;
565
+ width: 16px;
566
+ height: 16px;
567
+ background-color: var(--input-bg-color);
568
+ transition: border-color 0.1s, background-color 0.1s;
569
+
570
+ .button.active & {
571
+ border-color: var(--c-fg-info-1);
572
+ background-color: var(--c-fg-info-1);
573
+ }
574
+ }
575
+
576
+ .checkbox-icon {
577
+ display: block;
578
+ width: 10px;
579
+ height: 10px;
580
+ color: var(--c-white);
581
+ opacity: 0;
582
+ transition: opacity 0.25s;
583
+
584
+ .button.active & {
585
+ opacity: 1;
586
+ }
587
+ }
588
+
589
+ .radio {
590
+ position: relative;
591
+ flex-shrink: 0;
592
+ border: 1px solid var(--c-border-mute-1);
593
+ border-radius: 50%;
594
+ margin-top: 8px;
595
+ margin-bottom: 8px;
596
+ width: 16px;
597
+ height: 16px;
598
+ background-color: var(--input-bg-color);
599
+ transition: border-color 0.25s;
600
+
601
+ .button.active & {
602
+ border-color: var(--c-border-info-1);
603
+ }
604
+ }
605
+
606
+ .radio-dot {
607
+ position: absolute;
608
+ top: 0;
609
+ right: 0;
610
+ bottom: 0;
611
+ left: 0;
612
+ display: flex;
613
+ justify-content: center;
614
+ align-items: center;
615
+ border-radius: 50%;
616
+ width: 100%;
617
+ background-color: var(--c-fg-info-1);
618
+ opacity: 0;
619
+ transform: scale(0);
620
+ transition: opacity 0.25s, transform 0.1s;
621
+
622
+ .button.active & {
623
+ opacity: 1;
624
+ transform: scale(0.6);
625
+ }
626
+ }
627
+
628
+ .option-item {
629
+ padding-left: 12px;
630
+ width: 100%;
631
+ }
632
+
633
+ .state {
634
+ display: flex;
635
+ align-items: center;
636
+ gap: 8px;
637
+ padding: 14px 16px;
638
+ }
639
+
640
+ .state-spinner {
641
+ width: 16px;
642
+ height: 16px;
643
+ color: var(--c-text-2);
644
+ }
645
+
646
+ .state-text {
647
+ font-size: 12px;
648
+ color: var(--c-text-2);
649
+ }
650
+
651
+ .empty {
652
+ padding: 14px 16px;
653
+ font-size: 12px;
654
+ color: var(--c-text-2);
655
+ }
656
+
657
+ .SInputAsyncDropdown.sm,
658
+ .SInputAsyncDropdown.mini {
659
+ .box {
660
+ min-height: 32px;
661
+ }
662
+
663
+ .box-content {
664
+ padding: 0 30px 0 0;
665
+ line-height: 24px;
666
+ font-size: var(--input-font-size, var(--input-mini-font-size));
667
+ }
668
+
669
+ .box-placeholder {
670
+ padding-left: 10px;
671
+ }
672
+
673
+ .box-icon {
674
+ top: 3px;
675
+ right: 8px;
676
+ }
677
+ }
678
+
679
+ .SInputAsyncDropdown.md {
680
+ .box {
681
+ min-height: 36px;
682
+ }
683
+
684
+ .box-content {
685
+ padding: 0 30px 0 0;
686
+ line-height: 24px;
687
+ font-size: var(--input-font-size, 14px);
688
+ }
689
+
690
+ .box-placeholder {
691
+ padding-left: 10px;
692
+ }
693
+
694
+ .box-icon {
695
+ top: 5px;
696
+ right: 8px;
697
+ }
698
+ }
699
+
700
+ .SInputAsyncDropdown.small {
701
+ .box {
702
+ min-height: 40px;
703
+ }
704
+
705
+ .box-content {
706
+ padding: 0 30px 0 0;
707
+ line-height: 24px;
708
+ font-size: var(--input-font-size, var(--input-small-font-size));
709
+ }
710
+
711
+ .box-placeholder {
712
+ padding-left: 12px;
713
+ }
714
+
715
+ .box-icon {
716
+ top: 7px;
717
+ right: 8px;
718
+ }
719
+ }
720
+
721
+ .SInputAsyncDropdown.medium {
722
+ .box {
723
+ min-height: 48px;
724
+ }
725
+
726
+ .box-content {
727
+ padding: 0 40px 0 0;
728
+ line-height: 24px;
729
+ font-size: var(--input-font-size, var(--input-medium-font-size));
730
+ }
731
+
732
+ .box-placeholder {
733
+ padding-left: 16px;
734
+ }
735
+
736
+ .box-icon {
737
+ top: 11px;
738
+ right: 12px;
739
+ }
740
+ }
741
+
742
+ .SInputAsyncDropdown.disabled {
743
+ .box {
744
+ border-color: var(--input-disabled-border-color);
745
+ background-color: var(--input-disabled-bg-color);
746
+ cursor: not-allowed;
747
+
748
+ &:hover { border-color: var(--input-disabled-border-color); }
749
+ }
750
+
751
+ .box-icon {
752
+ cursor: not-allowed;
753
+ }
754
+ }
755
+
756
+ .SInputAsyncDropdown.has-error {
757
+ .box {
758
+ border-color: var(--input-error-border-color);
759
+ }
760
+ }
761
+ </style>