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