@krometra/ui-kit 0.0.0-stage → 0.3.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 (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +228 -2
  3. package/dist/ui-kit.css +2 -0
  4. package/dist/ui-kit.js +14955 -0
  5. package/dist/ui-kit.umd.cjs +3 -0
  6. package/package.json +74 -3
  7. package/src/base.css +32 -0
  8. package/src/components/KAlert.vue +65 -0
  9. package/src/components/KAvatar.vue +67 -0
  10. package/src/components/KBadge.vue +47 -0
  11. package/src/components/KButton.vue +98 -0
  12. package/src/components/KCard.vue +63 -0
  13. package/src/components/KCheckbox.vue +105 -0
  14. package/src/components/KCombobox.vue +232 -0
  15. package/src/components/KCountdown.vue +74 -0
  16. package/src/components/KDescriptions.vue +40 -0
  17. package/src/components/KDropdown.vue +111 -0
  18. package/src/components/KEmpty.vue +39 -0
  19. package/src/components/KFileUpload.vue +121 -0
  20. package/src/components/KFilterBar.vue +91 -0
  21. package/src/components/KIcon.vue +53 -0
  22. package/src/components/KInput.vue +108 -0
  23. package/src/components/KMenuItem.vue +56 -0
  24. package/src/components/KModal.vue +136 -0
  25. package/src/components/KMoney.vue +44 -0
  26. package/src/components/KPage.vue +93 -0
  27. package/src/components/KPagination.vue +113 -0
  28. package/src/components/KProgress.vue +63 -0
  29. package/src/components/KRadioGroup.vue +103 -0
  30. package/src/components/KSegmented.vue +88 -0
  31. package/src/components/KSelect.vue +231 -0
  32. package/src/components/KShell.vue +176 -0
  33. package/src/components/KStat.vue +74 -0
  34. package/src/components/KSteps.vue +119 -0
  35. package/src/components/KSwitch.vue +82 -0
  36. package/src/components/KTable.vue +161 -0
  37. package/src/components/KTabs.vue +102 -0
  38. package/src/components/KTextarea.vue +79 -0
  39. package/src/components/KTimeline.vue +72 -0
  40. package/src/components/KToast.vue +34 -0
  41. package/src/components/KToastItem.vue +86 -0
  42. package/src/components/KTooltip.vue +67 -0
  43. package/src/components/icon-registry.js +143 -0
  44. package/src/composables/useToast.js +52 -0
  45. package/src/format.js +137 -0
  46. package/src/index.js +84 -0
  47. package/src/theme.js +417 -0
  48. package/src/tokens.css +109 -0
@@ -0,0 +1,113 @@
1
+ <script setup>
2
+ /**
3
+ * KPagination — постраничная навигация на @zag-js/pagination.
4
+ *
5
+ * Контролируемая: компонент не хранит состояние, а сообщает нужную страницу
6
+ * через update:page. Диапазон страниц сворачивается многоточиями, чтобы ширина
7
+ * не зависела от объёма выборки.
8
+ *
9
+ * <KPagination v-model:page="page" :page-size="20" :total="total" />
10
+ */
11
+ import * as pagination from '@zag-js/pagination'
12
+ import { normalizeProps, useMachine } from '@zag-js/vue'
13
+ import { computed, useId } from 'vue'
14
+ import KIcon from './KIcon.vue'
15
+
16
+ const props = defineProps({
17
+ page: { type: Number, default: 1 },
18
+ pageSize: { type: Number, default: 20 },
19
+ total: { type: Number, default: 0 },
20
+ siblings: { type: Number, default: 1 },
21
+ showInfo: { type: Boolean, default: true },
22
+ })
23
+
24
+ const emit = defineEmits(['update:page'])
25
+
26
+ const service = useMachine(pagination.machine, {
27
+ id: useId(),
28
+ translations: {
29
+ rootLabel: 'Постраничная навигация',
30
+ prevTriggerLabel: 'Назад',
31
+ nextTriggerLabel: 'Вперёд',
32
+ itemLabel: ({ page }) => `Страница ${page}`,
33
+ },
34
+ get count() {
35
+ return props.total
36
+ },
37
+ get pageSize() {
38
+ return props.pageSize || 1
39
+ },
40
+ get siblingCount() {
41
+ return props.siblings
42
+ },
43
+ get page() {
44
+ return props.page
45
+ },
46
+ onPageChange({ page }) {
47
+ if (page !== props.page) emit('update:page', page)
48
+ },
49
+ })
50
+
51
+ const api = computed(() => pagination.connect(service, normalizeProps))
52
+
53
+ const rangeFrom = computed(() => (props.total ? api.value.pageRange.start + 1 : 0))
54
+ const rangeTo = computed(() => api.value.pageRange.end)
55
+ </script>
56
+
57
+ <template>
58
+ <nav class="k-pagination" v-bind="api.getRootProps()">
59
+ <span v-if="showInfo" class="k-pagination__info">{{ rangeFrom }}–{{ rangeTo }} из {{ total }}</span>
60
+ <div class="k-pagination__pages">
61
+ <button class="k-pagination__nav" v-bind="api.getPrevTriggerProps()">
62
+ <KIcon name="chevron-left" :size="16" />
63
+ </button>
64
+ <template v-for="(item, i) in api.pages" :key="item.type === 'page' ? item.value : `ellipsis-${i}`">
65
+ <span v-if="item.type === 'ellipsis'" class="k-pagination__gap" v-bind="api.getEllipsisProps({ index: i })">…</span>
66
+ <button v-else class="k-pagination__page" v-bind="api.getItemProps(item)">{{ item.value }}</button>
67
+ </template>
68
+ <button class="k-pagination__nav" v-bind="api.getNextTriggerProps()">
69
+ <KIcon name="chevron-right" :size="16" />
70
+ </button>
71
+ </div>
72
+ </nav>
73
+ </template>
74
+
75
+ <style scoped>
76
+ .k-pagination {
77
+ display: flex;
78
+ align-items: center;
79
+ justify-content: space-between;
80
+ gap: 1rem;
81
+ flex-wrap: wrap;
82
+ padding: 0.75rem 0.875rem;
83
+ border-top: 1px solid var(--k-border);
84
+ }
85
+ .k-pagination__info { font-size: 0.8125rem; color: var(--k-text-muted); }
86
+ .k-pagination__pages { display: flex; align-items: center; gap: 0.25rem; margin-left: auto; }
87
+ .k-pagination__page,
88
+ .k-pagination__nav {
89
+ min-width: var(--k-control-h-sm);
90
+ height: var(--k-control-h-sm);
91
+ padding: 0 0.4rem;
92
+ display: inline-flex;
93
+ align-items: center;
94
+ justify-content: center;
95
+ background: none;
96
+ border: 1px solid transparent;
97
+ border-radius: var(--k-radius-sm);
98
+ font: inherit;
99
+ font-size: 0.875rem;
100
+ color: var(--k-text-muted);
101
+ cursor: pointer;
102
+ }
103
+ .k-pagination__page:hover,
104
+ .k-pagination__nav:hover:not(:disabled) { background: var(--k-surface-muted); color: var(--k-text); }
105
+ .k-pagination__page[data-selected] {
106
+ background: var(--k-primary-soft);
107
+ border-color: currentColor;
108
+ color: var(--k-primary);
109
+ font-weight: 600;
110
+ }
111
+ .k-pagination__nav:disabled { opacity: 0.4; cursor: not-allowed; }
112
+ .k-pagination__gap { padding: 0 0.25rem; color: var(--k-text-faint); }
113
+ </style>
@@ -0,0 +1,63 @@
1
+ <script setup>
2
+ /**
3
+ * KProgress — линейный индикатор на @zag-js/progress: заполненность, ход
4
+ * операции, доля от нормы. `value: null` — неопределённое состояние.
5
+ */
6
+ import * as progress from '@zag-js/progress'
7
+ import { normalizeProps, useMachine } from '@zag-js/vue'
8
+ import { computed, useId } from 'vue'
9
+
10
+ const props = defineProps({
11
+ value: { type: Number, default: 0 },
12
+ max: { type: Number, default: 100 },
13
+ label: { type: String, default: '' },
14
+ tone: { type: String, default: 'primary' }, // primary | success | warning | danger
15
+ showValue: { type: Boolean, default: false },
16
+ })
17
+
18
+ const service = useMachine(progress.machine, {
19
+ id: useId(),
20
+ min: 0,
21
+ locale: 'ru-RU',
22
+ get value() {
23
+ return props.value
24
+ },
25
+ get max() {
26
+ return props.max
27
+ },
28
+ })
29
+
30
+ const api = computed(() => progress.connect(service, normalizeProps))
31
+ </script>
32
+
33
+ <template>
34
+ <div class="k-progress" v-bind="api.getRootProps()">
35
+ <div v-if="label || showValue" class="k-progress__head">
36
+ <span class="k-progress__label" v-bind="api.getLabelProps()">{{ label }}</span>
37
+ <span v-if="showValue" class="k-progress__value" v-bind="api.getValueTextProps()">{{ api.percentAsString }}</span>
38
+ </div>
39
+ <div class="k-progress__track" v-bind="api.getTrackProps()">
40
+ <div class="k-progress__bar" :class="`k-progress__bar--${tone}`" v-bind="api.getRangeProps()" />
41
+ </div>
42
+ </div>
43
+ </template>
44
+
45
+ <style scoped>
46
+ .k-progress__head { display: flex; justify-content: space-between; gap: 0.5rem; margin-bottom: 0.3rem; font-size: 0.8125rem; }
47
+ .k-progress__label { color: var(--k-text-muted); }
48
+ .k-progress__value { font-weight: 600; font-variant-numeric: tabular-nums; }
49
+ .k-progress__track { height: 0.5rem; background: var(--k-surface-muted); border-radius: 999px; overflow: hidden; }
50
+ .k-progress__bar { height: 100%; border-radius: 999px; transition: width 0.3s ease; }
51
+ .k-progress__bar[data-state="indeterminate"] {
52
+ width: 40%;
53
+ animation: k-progress-slide 1.2s ease-in-out infinite;
54
+ }
55
+ .k-progress__bar--primary { background: var(--k-primary); }
56
+ .k-progress__bar--success { background: var(--k-success); }
57
+ .k-progress__bar--warning { background: var(--k-warning); }
58
+ .k-progress__bar--danger { background: var(--k-danger); }
59
+ @keyframes k-progress-slide {
60
+ from { transform: translateX(-100%); }
61
+ to { transform: translateX(250%); }
62
+ }
63
+ </style>
@@ -0,0 +1,103 @@
1
+ <script setup>
2
+ /**
3
+ * KRadioGroup — группа радио-кнопок на @zag-js/radio-group: навигация
4
+ * стрелками и roving tabindex от машины. Опции — строки или
5
+ * { value, label, hint?, disabled? }. Направление — колонка или строка.
6
+ */
7
+ import * as radio from '@zag-js/radio-group'
8
+ import { normalizeProps, useMachine } from '@zag-js/vue'
9
+ import { computed, useId } from 'vue'
10
+
11
+ const props = defineProps({
12
+ modelValue: { type: [String, Number, Boolean, null], default: null },
13
+ options: { type: Array, default: () => [] },
14
+ label: { type: String, default: '' },
15
+ name: { type: String, default: '' },
16
+ inline: { type: Boolean, default: false },
17
+ disabled: { type: Boolean, default: false },
18
+ })
19
+
20
+ const emit = defineEmits(['update:modelValue'])
21
+
22
+ const items = computed(() =>
23
+ props.options.map((o) => (typeof o === 'object' && o !== null ? o : { value: o, label: String(o) })),
24
+ )
25
+
26
+ const service = useMachine(radio.machine, {
27
+ id: useId(),
28
+ get value() {
29
+ return props.modelValue == null ? null : String(props.modelValue)
30
+ },
31
+ get disabled() {
32
+ return props.disabled
33
+ },
34
+ get name() {
35
+ return props.name || undefined
36
+ },
37
+ get orientation() {
38
+ return props.inline ? 'horizontal' : 'vertical'
39
+ },
40
+ onValueChange({ value }) {
41
+ const item = items.value.find((i) => String(i.value) === value)
42
+ emit('update:modelValue', item ? item.value : value)
43
+ },
44
+ })
45
+
46
+ const api = computed(() => radio.connect(service, normalizeProps))
47
+
48
+ const itemProps = (item) => ({ value: String(item.value), disabled: item.disabled })
49
+ </script>
50
+
51
+ <template>
52
+ <div class="k-radio" v-bind="api.getRootProps()">
53
+ <span v-if="label" class="k-radio__legend" v-bind="api.getLabelProps()">{{ label }}</span>
54
+ <div class="k-radio__items" :class="{ 'k-radio__items--inline': inline }">
55
+ <label
56
+ v-for="item in items"
57
+ :key="item.value"
58
+ class="k-radio__item"
59
+ v-bind="api.getItemProps(itemProps(item))"
60
+ >
61
+ <input v-bind="api.getItemHiddenInputProps(itemProps(item))" />
62
+ <span class="k-radio__control" v-bind="api.getItemControlProps(itemProps(item))" />
63
+ <span class="k-radio__body">
64
+ <span class="k-radio__label" v-bind="api.getItemTextProps(itemProps(item))">{{ item.label }}</span>
65
+ <span v-if="item.hint" class="k-radio__hint">{{ item.hint }}</span>
66
+ </span>
67
+ </label>
68
+ </div>
69
+ </div>
70
+ </template>
71
+
72
+ <style scoped>
73
+ .k-radio { min-width: 0; }
74
+ .k-radio__legend { display: block; padding-bottom: 0.375rem; font-size: 0.8125rem; color: var(--k-text-muted); }
75
+ .k-radio__items { display: flex; flex-direction: column; gap: 0.5rem; }
76
+ .k-radio__items--inline { flex-direction: row; flex-wrap: wrap; gap: 1rem; }
77
+ .k-radio__item { display: inline-flex; align-items: flex-start; gap: 0.5rem; cursor: pointer; }
78
+ .k-radio__item[data-disabled] { cursor: not-allowed; opacity: 0.6; }
79
+ .k-radio__control {
80
+ position: relative;
81
+ width: 1.05rem;
82
+ height: 1.05rem;
83
+ margin-top: 0.15rem;
84
+ flex: none;
85
+ background: var(--k-surface);
86
+ border: 1px solid var(--k-border-strong);
87
+ border-radius: 50%;
88
+ transition: border-color 0.12s, box-shadow 0.12s;
89
+ }
90
+ .k-radio__control[data-hover]:not([data-disabled]) { border-color: var(--k-primary); }
91
+ .k-radio__control[data-focus-visible] { box-shadow: 0 0 0 3px var(--k-primary-soft); border-color: var(--k-primary); }
92
+ .k-radio__control[data-state="checked"] { border-color: var(--k-primary); }
93
+ .k-radio__control[data-state="checked"]::after {
94
+ content: '';
95
+ position: absolute;
96
+ inset: 3px;
97
+ border-radius: 50%;
98
+ background: var(--k-primary);
99
+ }
100
+ .k-radio__body { display: flex; flex-direction: column; gap: 0.1rem; }
101
+ .k-radio__label { font-size: 0.9375rem; }
102
+ .k-radio__hint { font-size: 0.75rem; color: var(--k-text-muted); }
103
+ </style>
@@ -0,0 +1,88 @@
1
+ <script setup>
2
+ /**
3
+ * KSegmented — компактный переключатель режимов (карточки/таблица, день/месяц)
4
+ * на @zag-js/radio-group: семантически это радио-группа, поэтому стрелки и
5
+ * фокус работают как у неё. В отличие от KTabs не претендует на навигацию.
6
+ *
7
+ * options: [{ value, label?, icon?, title? }]
8
+ */
9
+ import * as radio from '@zag-js/radio-group'
10
+ import { normalizeProps, useMachine } from '@zag-js/vue'
11
+ import { computed, useId } from 'vue'
12
+ import KIcon from './KIcon.vue'
13
+
14
+ const props = defineProps({
15
+ modelValue: { type: [String, Number], default: '' },
16
+ options: { type: Array, default: () => [] },
17
+ size: { type: String, default: 'md' }, // sm | md
18
+ disabled: { type: Boolean, default: false },
19
+ })
20
+
21
+ const emit = defineEmits(['update:modelValue'])
22
+
23
+ const service = useMachine(radio.machine, {
24
+ id: useId(),
25
+ orientation: 'horizontal',
26
+ get value() {
27
+ return props.modelValue === '' || props.modelValue == null ? null : String(props.modelValue)
28
+ },
29
+ get disabled() {
30
+ return props.disabled
31
+ },
32
+ onValueChange({ value }) {
33
+ const item = props.options.find((o) => String(o.value) === value)
34
+ emit('update:modelValue', item ? item.value : value)
35
+ },
36
+ })
37
+
38
+ const api = computed(() => radio.connect(service, normalizeProps))
39
+ </script>
40
+
41
+ <template>
42
+ <div class="k-segmented" :class="`k-segmented--${size}`" v-bind="api.getRootProps()">
43
+ <label
44
+ v-for="opt in options"
45
+ :key="opt.value"
46
+ class="k-segmented__item"
47
+ :title="opt.title"
48
+ v-bind="api.getItemProps({ value: String(opt.value) })"
49
+ >
50
+ <input v-bind="api.getItemHiddenInputProps({ value: String(opt.value) })" />
51
+ <KIcon v-if="opt.icon" :name="opt.icon" :size="size === 'sm' ? 14 : 16" />
52
+ <span v-if="opt.label" v-bind="api.getItemTextProps({ value: String(opt.value) })">{{ opt.label }}</span>
53
+ </label>
54
+ </div>
55
+ </template>
56
+
57
+ <style scoped>
58
+ .k-segmented {
59
+ display: inline-flex;
60
+ padding: 0.15rem;
61
+ background: var(--k-surface-muted);
62
+ border: 1px solid var(--k-border);
63
+ border-radius: var(--k-radius-sm);
64
+ gap: 0.15rem;
65
+ }
66
+ .k-segmented__item {
67
+ display: inline-flex;
68
+ align-items: center;
69
+ gap: 0.35rem;
70
+ padding: 0.3rem 0.65rem;
71
+ border-radius: calc(var(--k-radius-sm) - 2px);
72
+ font-size: 0.875rem;
73
+ color: var(--k-text-muted);
74
+ cursor: pointer;
75
+ white-space: nowrap;
76
+ transition: background 0.12s, color 0.12s, box-shadow 0.12s;
77
+ }
78
+ .k-segmented--sm .k-segmented__item { padding: 0.2rem 0.5rem; font-size: 0.8125rem; }
79
+ .k-segmented__item[data-hover]:not([data-disabled]) { color: var(--k-text); }
80
+ .k-segmented__item[data-focus-visible] { box-shadow: 0 0 0 2px var(--k-primary); }
81
+ .k-segmented__item[data-disabled] { cursor: not-allowed; opacity: 0.5; }
82
+ .k-segmented__item[data-state="checked"] {
83
+ background: var(--k-surface);
84
+ color: var(--k-text);
85
+ font-weight: 550;
86
+ box-shadow: var(--k-shadow-sm);
87
+ }
88
+ </style>
@@ -0,0 +1,231 @@
1
+ <script setup>
2
+ /**
3
+ * KSelect — выпадающий список с одиночным выбором на @zag-js/select.
4
+ *
5
+ * Опции — массив строк или любых объектов; ключ и подпись извлекаются через
6
+ * `getValue`/`getLabel`, поэтому в v-model живёт исходное значение (число,
7
+ * строка, id), а не его строковое представление. Список рендерится в <body>,
8
+ * чтобы не обрезаться внутри модалок и карточек с overflow.
9
+ *
10
+ * <KSelect v-model="role" label="Роль" :options="[{ value: 'admin', label: 'Администратор' }]" />
11
+ * <KSelect v-model="volumeId" :options="volumes" :get-value="v => v.id" :get-label="v => v.name" clearable />
12
+ */
13
+ import * as select from '@zag-js/select'
14
+ import { normalizeProps, useMachine } from '@zag-js/vue'
15
+ import { computed, ref, useAttrs, useId } from 'vue'
16
+ import KIcon from './KIcon.vue'
17
+
18
+ defineOptions({ inheritAttrs: false })
19
+
20
+ const attrs = useAttrs()
21
+ const rootAttrs = computed(() => ({ class: attrs.class, style: attrs.style }))
22
+
23
+ const props = defineProps({
24
+ modelValue: { type: [String, Number, Boolean, null], default: null },
25
+ options: { type: Array, default: () => [] },
26
+ getValue: { type: Function, default: (o) => o.value },
27
+ getLabel: { type: Function, default: (o) => o.label },
28
+ label: { type: String, default: '' },
29
+ placeholder: { type: String, default: 'Выберите…' },
30
+ hint: { type: String, default: '' },
31
+ error: { type: String, default: '' },
32
+ name: { type: String, default: '' },
33
+ required: { type: Boolean, default: false },
34
+ disabled: { type: Boolean, default: false },
35
+ clearable: { type: Boolean, default: false },
36
+ })
37
+
38
+ const emit = defineEmits(['update:modelValue'])
39
+
40
+ const box = ref(null)
41
+
42
+ const items = computed(() =>
43
+ props.options.map((o) => (typeof o === 'object' && o !== null ? o : { value: o, label: String(o) })),
44
+ )
45
+
46
+ const keyOf = (item) => String(props.getValue(item))
47
+ const labelOf = (item) => String(props.getLabel(item))
48
+
49
+ const collection = computed(() =>
50
+ select.collection({ items: items.value, itemToValue: keyOf, itemToString: labelOf }),
51
+ )
52
+
53
+ function itemFor(raw) {
54
+ if (raw == null || raw === '') return undefined
55
+ return items.value.find((i) => keyOf(i) === String(raw))
56
+ }
57
+
58
+ const service = useMachine(select.machine, {
59
+ id: useId(),
60
+ positioning: { sameWidth: true, getAnchorElement: () => box.value },
61
+ get collection() {
62
+ return collection.value
63
+ },
64
+ get disabled() {
65
+ return props.disabled
66
+ },
67
+ get invalid() {
68
+ return !!props.error
69
+ },
70
+ get required() {
71
+ return props.required
72
+ },
73
+ get name() {
74
+ return props.name || undefined
75
+ },
76
+ get value() {
77
+ const match = itemFor(props.modelValue)
78
+ return match ? [keyOf(match)] : []
79
+ },
80
+ onValueChange({ value }) {
81
+ const item = itemFor(value[0])
82
+ emit('update:modelValue', item !== undefined ? props.getValue(item) : null)
83
+ },
84
+ })
85
+
86
+ const api = computed(() => select.connect(service, normalizeProps))
87
+ </script>
88
+
89
+ <template>
90
+ <div class="k-select" v-bind="{ ...rootAttrs, ...api.getRootProps() }">
91
+ <label v-if="label" class="k-select__label" v-bind="api.getLabelProps()">
92
+ {{ label }}<span v-if="required" class="k-select__req">*</span>
93
+ </label>
94
+ <div
95
+ ref="box"
96
+ class="k-select__box"
97
+ :class="{ 'k-select__box--error': error, 'k-select__box--disabled': disabled }"
98
+ v-bind="api.getControlProps()"
99
+ >
100
+ <button class="k-select__trigger" v-bind="api.getTriggerProps()">
101
+ <span
102
+ class="k-select__value"
103
+ :class="{ 'k-select__value--placeholder': !api.hasSelectedItems }"
104
+ v-bind="api.getValueTextProps()"
105
+ >{{ api.hasSelectedItems ? api.valueAsString : placeholder }}</span>
106
+ </button>
107
+ <button
108
+ v-if="clearable && api.hasSelectedItems"
109
+ class="k-select__clear"
110
+ aria-label="Очистить"
111
+ v-bind="api.getClearTriggerProps()"
112
+ >
113
+ <KIcon name="close" :size="14" />
114
+ </button>
115
+ <span class="k-select__chevron" v-bind="api.getIndicatorProps()">
116
+ <KIcon name="chevron-down" :size="16" />
117
+ </span>
118
+ </div>
119
+ <select v-bind="api.getHiddenSelectProps()">
120
+ <option v-for="item in items" :key="keyOf(item)" :value="keyOf(item)">{{ labelOf(item) }}</option>
121
+ </select>
122
+ <span v-if="error" class="k-select__error">{{ error }}</span>
123
+ <span v-else-if="hint" class="k-select__hint">{{ hint }}</span>
124
+
125
+ <Teleport to="body">
126
+ <div v-bind="api.getPositionerProps()">
127
+ <ul class="k-select__content" v-bind="api.getContentProps()">
128
+ <li
129
+ v-for="item in items"
130
+ :key="keyOf(item)"
131
+ class="k-select__item"
132
+ v-bind="api.getItemProps({ item })"
133
+ >
134
+ <span class="k-select__item-text" v-bind="api.getItemTextProps({ item })">{{ labelOf(item) }}</span>
135
+ <span class="k-select__item-check" v-bind="api.getItemIndicatorProps({ item })">
136
+ <KIcon name="check" :size="15" />
137
+ </span>
138
+ </li>
139
+ <li v-if="!items.length" class="k-select__empty">Нет вариантов</li>
140
+ </ul>
141
+ </div>
142
+ </Teleport>
143
+ </div>
144
+ </template>
145
+
146
+ <style scoped>
147
+ .k-select { display: flex; flex-direction: column; gap: 0.25rem; margin: 0; min-width: 0; }
148
+ .k-select__label { font-size: 0.8125rem; color: var(--k-text-muted); }
149
+ .k-select__req { color: var(--k-danger); margin-left: 2px; }
150
+ .k-select__box {
151
+ display: flex;
152
+ align-items: center;
153
+ height: var(--k-control-h);
154
+ padding-right: 0.5rem;
155
+ background: var(--k-surface);
156
+ border: 1px solid var(--k-border-strong);
157
+ border-radius: var(--k-radius-sm);
158
+ transition: border-color 0.15s, box-shadow 0.15s;
159
+ }
160
+ .k-select__box:focus-within { border-color: var(--k-primary); box-shadow: 0 0 0 3px var(--k-primary-soft); }
161
+ .k-select__box--error { border-color: var(--k-danger); }
162
+ .k-select__box--error:focus-within { box-shadow: 0 0 0 3px var(--k-danger-soft); }
163
+ .k-select__box--disabled { background: var(--k-surface-muted); }
164
+ .k-select__trigger {
165
+ flex: 1;
166
+ min-width: 0;
167
+ height: 100%;
168
+ padding: 0 0.625rem;
169
+ border: none;
170
+ background: none;
171
+ outline: none;
172
+ font: inherit;
173
+ font-size: 0.9375rem;
174
+ color: inherit;
175
+ text-align: left;
176
+ cursor: pointer;
177
+ }
178
+ .k-select__trigger:disabled { cursor: not-allowed; color: var(--k-text-muted); }
179
+ .k-select__value { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
180
+ .k-select__value--placeholder { color: var(--k-text-faint); }
181
+ .k-select__clear {
182
+ display: inline-flex;
183
+ padding: 0.15rem;
184
+ border: none;
185
+ border-radius: var(--k-radius-sm);
186
+ background: none;
187
+ color: var(--k-text-faint);
188
+ cursor: pointer;
189
+ }
190
+ .k-select__clear:hover { color: var(--k-text); background: var(--k-surface-muted); }
191
+ .k-select__chevron { display: inline-flex; color: var(--k-text-faint); pointer-events: none; transition: transform 0.15s; }
192
+ .k-select__chevron[data-state="open"] { transform: rotate(180deg); }
193
+ .k-select__hint { font-size: 0.75rem; color: var(--k-text-muted); }
194
+ .k-select__error { font-size: 0.75rem; color: var(--k-danger); }
195
+
196
+ /* zag читает вычисленный z-index контента и переносит его на positioner —
197
+ задаём здесь, выше модалки (1000), чтобы список не прятался под ней. */
198
+ .k-select__content {
199
+ z-index: 1100;
200
+ list-style: none;
201
+ margin: 0;
202
+ padding: 0.25rem;
203
+ max-height: 260px;
204
+ overflow-y: auto;
205
+ min-width: var(--reference-width);
206
+ background: var(--k-surface);
207
+ border: 1px solid var(--k-border);
208
+ border-radius: var(--k-radius);
209
+ box-shadow: var(--k-shadow-md);
210
+ outline: none;
211
+ }
212
+ .k-select__content[hidden] { display: none; }
213
+ .k-select__item {
214
+ display: flex;
215
+ align-items: center;
216
+ justify-content: space-between;
217
+ gap: 0.5rem;
218
+ padding: 0.45rem 0.6rem;
219
+ border-radius: var(--k-radius-sm);
220
+ font-size: 0.9rem;
221
+ color: var(--k-text);
222
+ cursor: pointer;
223
+ }
224
+ .k-select__item[data-highlighted] { background: var(--k-primary-soft); color: var(--k-primary); }
225
+ .k-select__item[data-state="checked"] { font-weight: 600; }
226
+ .k-select__item[data-disabled] { opacity: 0.5; cursor: not-allowed; }
227
+ .k-select__item-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
228
+ .k-select__item-check { display: inline-flex; flex: none; }
229
+ .k-select__item-check[hidden] { display: none; }
230
+ .k-select__empty { padding: 0.5rem 0.6rem; font-size: 0.875rem; color: var(--k-text-muted); }
231
+ </style>