@krometra/ui-kit 0.0.0-stage → 0.4.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.
- package/LICENSE +21 -0
- package/README.md +235 -2
- package/dist/ui-kit.css +2 -0
- package/dist/ui-kit.js +15222 -0
- package/dist/ui-kit.umd.cjs +3 -0
- package/package.json +74 -3
- package/src/base.css +32 -0
- package/src/components/KAlert.vue +65 -0
- package/src/components/KAvatar.vue +67 -0
- package/src/components/KBadge.vue +47 -0
- package/src/components/KButton.vue +98 -0
- package/src/components/KCard.vue +63 -0
- package/src/components/KCheckbox.vue +105 -0
- package/src/components/KCombobox.vue +232 -0
- package/src/components/KCountdown.vue +74 -0
- package/src/components/KDescriptions.vue +40 -0
- package/src/components/KDropdown.vue +111 -0
- package/src/components/KEmpty.vue +39 -0
- package/src/components/KFileUpload.vue +121 -0
- package/src/components/KFilterBar.vue +91 -0
- package/src/components/KGantt.vue +398 -0
- package/src/components/KIcon.vue +53 -0
- package/src/components/KInput.vue +108 -0
- package/src/components/KMenuItem.vue +56 -0
- package/src/components/KModal.vue +136 -0
- package/src/components/KMoney.vue +44 -0
- package/src/components/KPage.vue +93 -0
- package/src/components/KPagination.vue +113 -0
- package/src/components/KProgress.vue +63 -0
- package/src/components/KRadioGroup.vue +103 -0
- package/src/components/KSegmented.vue +88 -0
- package/src/components/KSelect.vue +231 -0
- package/src/components/KShell.vue +176 -0
- package/src/components/KStat.vue +74 -0
- package/src/components/KSteps.vue +119 -0
- package/src/components/KSwitch.vue +82 -0
- package/src/components/KTable.vue +161 -0
- package/src/components/KTabs.vue +102 -0
- package/src/components/KTextarea.vue +79 -0
- package/src/components/KTimeline.vue +72 -0
- package/src/components/KToast.vue +34 -0
- package/src/components/KToastItem.vue +86 -0
- package/src/components/KTooltip.vue +67 -0
- package/src/components/icon-registry.js +143 -0
- package/src/composables/useToast.js +52 -0
- package/src/format.js +137 -0
- package/src/index.js +85 -0
- package/src/theme.js +417 -0
- package/src/tokens.css +109 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* KMenuItem — пункт меню внутри KDropdown. Рендерится как <router-link>,
|
|
4
|
+
* если передан `to`, иначе как кнопка. Атрибут data-k-menu-item нужен
|
|
5
|
+
* дропдауну, чтобы закрыться после выбора.
|
|
6
|
+
*/
|
|
7
|
+
import KIcon from './KIcon.vue'
|
|
8
|
+
|
|
9
|
+
defineProps({
|
|
10
|
+
icon: { type: String, default: '' },
|
|
11
|
+
to: { type: [String, Object], default: null },
|
|
12
|
+
danger: { type: Boolean, default: false },
|
|
13
|
+
disabled: { type: Boolean, default: false },
|
|
14
|
+
})
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<component
|
|
19
|
+
:is="to ? 'router-link' : 'button'"
|
|
20
|
+
:to="to"
|
|
21
|
+
:type="to ? null : 'button'"
|
|
22
|
+
class="k-menu-item"
|
|
23
|
+
:class="{ 'k-menu-item--danger': danger }"
|
|
24
|
+
:disabled="to ? null : disabled"
|
|
25
|
+
role="menuitem"
|
|
26
|
+
data-k-menu-item
|
|
27
|
+
>
|
|
28
|
+
<KIcon v-if="icon" :name="icon" :size="15" />
|
|
29
|
+
<span class="k-menu-item__label"><slot /></span>
|
|
30
|
+
</component>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<style scoped>
|
|
34
|
+
.k-menu-item {
|
|
35
|
+
display: flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
gap: 0.5rem;
|
|
38
|
+
width: 100%;
|
|
39
|
+
padding: 0.45rem 0.55rem;
|
|
40
|
+
background: none;
|
|
41
|
+
border: none;
|
|
42
|
+
border-radius: var(--k-radius-sm);
|
|
43
|
+
font: inherit;
|
|
44
|
+
font-size: 0.875rem;
|
|
45
|
+
color: var(--k-text);
|
|
46
|
+
text-align: left;
|
|
47
|
+
text-decoration: none;
|
|
48
|
+
cursor: pointer;
|
|
49
|
+
}
|
|
50
|
+
.k-menu-item:hover { background: var(--k-surface-muted); text-decoration: none; }
|
|
51
|
+
.k-menu-item:focus-visible { outline: 2px solid var(--k-primary); outline-offset: -2px; }
|
|
52
|
+
.k-menu-item:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
53
|
+
.k-menu-item--danger { color: var(--k-danger-strong); }
|
|
54
|
+
.k-menu-item--danger:hover { background: var(--k-danger-soft); }
|
|
55
|
+
.k-menu-item__label { flex: 1; min-width: 0; }
|
|
56
|
+
</style>
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* KModal — модальное окно на @zag-js/dialog: фокус-трап, блокировка скролла,
|
|
4
|
+
* закрытие по Esc и возврат фокуса. Открытие управляется v-model.
|
|
5
|
+
*
|
|
6
|
+
* Клик по фону по умолчанию не закрывает окно: случайный клик мимо формы не
|
|
7
|
+
* должен терять введённое. Включается пропом `close-on-outside`.
|
|
8
|
+
*
|
|
9
|
+
* <KModal v-model="open" title="Новый документ" size="md">
|
|
10
|
+
* <form @submit.prevent="save">…</form>
|
|
11
|
+
* <template #footer><KButton variant="primary" @click="save">Сохранить</KButton></template>
|
|
12
|
+
* </KModal>
|
|
13
|
+
*/
|
|
14
|
+
import * as dialog from '@zag-js/dialog'
|
|
15
|
+
import { normalizeProps, useMachine } from '@zag-js/vue'
|
|
16
|
+
import { computed, useId } from 'vue'
|
|
17
|
+
import KIcon from './KIcon.vue'
|
|
18
|
+
|
|
19
|
+
const props = defineProps({
|
|
20
|
+
modelValue: { type: Boolean, default: false },
|
|
21
|
+
title: { type: String, default: '' },
|
|
22
|
+
subtitle: { type: String, default: '' },
|
|
23
|
+
size: { type: String, default: 'md' }, // sm | md | lg
|
|
24
|
+
closable: { type: Boolean, default: true },
|
|
25
|
+
closeOnOutside: { type: Boolean, default: false },
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const emit = defineEmits(['update:modelValue', 'close'])
|
|
29
|
+
|
|
30
|
+
const service = useMachine(dialog.machine, {
|
|
31
|
+
id: useId(),
|
|
32
|
+
get open() {
|
|
33
|
+
return props.modelValue
|
|
34
|
+
},
|
|
35
|
+
get closeOnInteractOutside() {
|
|
36
|
+
return props.closeOnOutside && props.closable
|
|
37
|
+
},
|
|
38
|
+
get closeOnEscape() {
|
|
39
|
+
return props.closable
|
|
40
|
+
},
|
|
41
|
+
onOpenChange({ open }) {
|
|
42
|
+
emit('update:modelValue', open)
|
|
43
|
+
if (!open) emit('close')
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const api = computed(() => dialog.connect(service, normalizeProps))
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<template>
|
|
51
|
+
<Teleport to="body">
|
|
52
|
+
<template v-if="api.open">
|
|
53
|
+
<div class="k-modal__backdrop" v-bind="api.getBackdropProps()" />
|
|
54
|
+
<div class="k-modal__positioner" v-bind="api.getPositionerProps()">
|
|
55
|
+
<div class="k-modal__panel" :class="`k-modal__panel--${size}`" v-bind="api.getContentProps()">
|
|
56
|
+
<header v-if="title || $slots.header" class="k-modal__header">
|
|
57
|
+
<div class="k-modal__heading">
|
|
58
|
+
<slot name="header">
|
|
59
|
+
<h2 class="k-modal__title" v-bind="api.getTitleProps()">{{ title }}</h2>
|
|
60
|
+
<p v-if="subtitle" class="k-modal__subtitle" v-bind="api.getDescriptionProps()">{{ subtitle }}</p>
|
|
61
|
+
</slot>
|
|
62
|
+
</div>
|
|
63
|
+
<button v-if="closable" class="k-modal__close" aria-label="Закрыть" v-bind="api.getCloseTriggerProps()">
|
|
64
|
+
<KIcon name="close" :size="18" />
|
|
65
|
+
</button>
|
|
66
|
+
</header>
|
|
67
|
+
<div class="k-modal__body"><slot /></div>
|
|
68
|
+
<footer v-if="$slots.footer" class="k-modal__footer"><slot name="footer" /></footer>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</template>
|
|
72
|
+
</Teleport>
|
|
73
|
+
</template>
|
|
74
|
+
|
|
75
|
+
<style scoped>
|
|
76
|
+
.k-modal__backdrop {
|
|
77
|
+
position: fixed;
|
|
78
|
+
inset: 0;
|
|
79
|
+
z-index: 1000;
|
|
80
|
+
background: var(--k-overlay);
|
|
81
|
+
backdrop-filter: blur(2px);
|
|
82
|
+
}
|
|
83
|
+
.k-modal__positioner {
|
|
84
|
+
position: fixed;
|
|
85
|
+
inset: 0;
|
|
86
|
+
z-index: 1000;
|
|
87
|
+
display: flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
justify-content: center;
|
|
90
|
+
padding: 1rem;
|
|
91
|
+
}
|
|
92
|
+
.k-modal__panel {
|
|
93
|
+
width: min(92vw, var(--k-modal-w, 34rem));
|
|
94
|
+
max-height: 86vh;
|
|
95
|
+
display: flex;
|
|
96
|
+
flex-direction: column;
|
|
97
|
+
background: var(--k-surface);
|
|
98
|
+
border-radius: var(--k-radius-lg);
|
|
99
|
+
box-shadow: var(--k-shadow-lg);
|
|
100
|
+
overflow: hidden;
|
|
101
|
+
outline: none;
|
|
102
|
+
}
|
|
103
|
+
.k-modal__panel--sm { --k-modal-w: 24rem; }
|
|
104
|
+
.k-modal__panel--md { --k-modal-w: 34rem; }
|
|
105
|
+
.k-modal__panel--lg { --k-modal-w: 52rem; }
|
|
106
|
+
.k-modal__header {
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: flex-start;
|
|
109
|
+
justify-content: space-between;
|
|
110
|
+
gap: 1rem;
|
|
111
|
+
padding: 1.125rem 1.25rem;
|
|
112
|
+
border-bottom: 1px solid var(--k-border);
|
|
113
|
+
}
|
|
114
|
+
.k-modal__title { font-size: 1.0625rem; font-weight: 600; }
|
|
115
|
+
.k-modal__subtitle { margin-top: 0.15rem; font-size: 0.8125rem; color: var(--k-text-muted); }
|
|
116
|
+
.k-modal__close {
|
|
117
|
+
background: none;
|
|
118
|
+
border: none;
|
|
119
|
+
color: var(--k-text-muted);
|
|
120
|
+
cursor: pointer;
|
|
121
|
+
padding: 0.15rem;
|
|
122
|
+
border-radius: var(--k-radius-sm);
|
|
123
|
+
flex: none;
|
|
124
|
+
}
|
|
125
|
+
.k-modal__close:hover { background: var(--k-surface-muted); color: var(--k-text); }
|
|
126
|
+
.k-modal__body { padding: 1.25rem; overflow-y: auto; }
|
|
127
|
+
.k-modal__footer {
|
|
128
|
+
display: flex;
|
|
129
|
+
align-items: center;
|
|
130
|
+
justify-content: flex-end;
|
|
131
|
+
gap: 0.5rem;
|
|
132
|
+
padding: 0.875rem 1.25rem;
|
|
133
|
+
border-top: 1px solid var(--k-border);
|
|
134
|
+
background: var(--k-surface-muted);
|
|
135
|
+
}
|
|
136
|
+
</style>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* KMoney — денежная сумма в русском формате.
|
|
4
|
+
*
|
|
5
|
+
* Вынесено в компонент, потому что суммы обычно встречаются на многих экранах
|
|
6
|
+
* и должны выглядеть одинаково: неразрывные разряды, символ валюты,
|
|
7
|
+
* опциональные копейки, табличные цифры для выравнивания в колонке.
|
|
8
|
+
*/
|
|
9
|
+
import { computed } from 'vue'
|
|
10
|
+
|
|
11
|
+
const props = defineProps({
|
|
12
|
+
value: { type: [Number, String], default: 0 },
|
|
13
|
+
currency: { type: String, default: 'RUB' },
|
|
14
|
+
fraction: { type: Boolean, default: false },
|
|
15
|
+
compact: { type: Boolean, default: false },
|
|
16
|
+
tone: { type: String, default: '' }, // '' | muted | success | danger
|
|
17
|
+
size: { type: String, default: 'md' }, // sm | md | lg
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const formatted = computed(() => {
|
|
21
|
+
const n = Number(props.value)
|
|
22
|
+
if (!Number.isFinite(n)) return '—'
|
|
23
|
+
return new Intl.NumberFormat('ru-RU', {
|
|
24
|
+
style: 'currency',
|
|
25
|
+
currency: props.currency,
|
|
26
|
+
minimumFractionDigits: props.fraction ? 2 : 0,
|
|
27
|
+
maximumFractionDigits: props.fraction ? 2 : 0,
|
|
28
|
+
notation: props.compact ? 'compact' : 'standard',
|
|
29
|
+
}).format(n)
|
|
30
|
+
})
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<span class="k-money" :class="[tone && `k-money--${tone}`, `k-money--${size}`]">{{ formatted }}</span>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<style scoped>
|
|
38
|
+
.k-money { font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
39
|
+
.k-money--sm { font-size: 0.8125rem; }
|
|
40
|
+
.k-money--lg { font-size: 1.25rem; font-weight: 650; letter-spacing: -0.01em; }
|
|
41
|
+
.k-money--muted { color: var(--k-text-muted); }
|
|
42
|
+
.k-money--success { color: var(--k-success); }
|
|
43
|
+
.k-money--danger { color: var(--k-danger); }
|
|
44
|
+
</style>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* KPage — стандартная обвязка экрана: хлебные крошки, заголовок, действия
|
|
4
|
+
* справа и контент. Используйте как корень каждого маршрута, чтобы отступы и
|
|
5
|
+
* расположение кнопок не разъезжались между страницами.
|
|
6
|
+
*/
|
|
7
|
+
import KIcon from './KIcon.vue'
|
|
8
|
+
|
|
9
|
+
defineProps({
|
|
10
|
+
title: { type: String, default: '' },
|
|
11
|
+
subtitle: { type: String, default: '' },
|
|
12
|
+
// [{ label, to? }] — последний элемент отрисуется как текущий, без ссылки
|
|
13
|
+
breadcrumbs: { type: Array, default: () => [] },
|
|
14
|
+
back: { type: [String, Object], default: null },
|
|
15
|
+
width: { type: String, default: 'wide' }, // wide | narrow | full
|
|
16
|
+
})
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<div class="k-page" :class="`k-page--${width}`">
|
|
21
|
+
<nav v-if="breadcrumbs.length" class="k-page__crumbs" aria-label="Хлебные крошки">
|
|
22
|
+
<template v-for="(crumb, i) in breadcrumbs" :key="i">
|
|
23
|
+
<component
|
|
24
|
+
:is="crumb.to && i < breadcrumbs.length - 1 ? 'router-link' : 'span'"
|
|
25
|
+
:to="crumb.to"
|
|
26
|
+
class="k-page__crumb"
|
|
27
|
+
:class="{ 'k-page__crumb--current': i === breadcrumbs.length - 1 }"
|
|
28
|
+
>{{ crumb.label }}</component>
|
|
29
|
+
<KIcon v-if="i < breadcrumbs.length - 1" name="chevron-right" :size="13" class="k-page__crumb-sep" />
|
|
30
|
+
</template>
|
|
31
|
+
</nav>
|
|
32
|
+
|
|
33
|
+
<header class="k-page__header">
|
|
34
|
+
<div class="k-page__heading">
|
|
35
|
+
<router-link v-if="back" :to="back" class="k-page__back" aria-label="Назад">
|
|
36
|
+
<KIcon name="back" :size="18" />
|
|
37
|
+
</router-link>
|
|
38
|
+
<div class="k-page__titles">
|
|
39
|
+
<div class="k-page__title-row">
|
|
40
|
+
<slot name="title"><h1>{{ title }}</h1></slot>
|
|
41
|
+
<slot name="title-extra" />
|
|
42
|
+
</div>
|
|
43
|
+
<p v-if="subtitle" class="k-page__subtitle">{{ subtitle }}</p>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<div v-if="$slots.actions" class="k-page__actions"><slot name="actions" /></div>
|
|
47
|
+
</header>
|
|
48
|
+
|
|
49
|
+
<slot />
|
|
50
|
+
</div>
|
|
51
|
+
</template>
|
|
52
|
+
|
|
53
|
+
<style scoped>
|
|
54
|
+
.k-page { padding: 1.75rem 2rem 3rem; margin: 0 auto; width: 100%; }
|
|
55
|
+
.k-page--wide { max-width: 1360px; }
|
|
56
|
+
.k-page--narrow { max-width: 880px; }
|
|
57
|
+
.k-page__crumbs { display: flex; align-items: center; gap: 0.3rem; margin-bottom: 0.6rem; font-size: 0.8125rem; }
|
|
58
|
+
.k-page__crumb { color: var(--k-text-muted); text-decoration: none; }
|
|
59
|
+
.k-page__crumb:hover { color: var(--k-primary); }
|
|
60
|
+
.k-page__crumb--current { color: var(--k-text); }
|
|
61
|
+
.k-page__crumb-sep { color: var(--k-text-faint); }
|
|
62
|
+
.k-page__header {
|
|
63
|
+
display: flex;
|
|
64
|
+
align-items: flex-start;
|
|
65
|
+
justify-content: space-between;
|
|
66
|
+
gap: 1rem;
|
|
67
|
+
margin-bottom: 1.5rem;
|
|
68
|
+
}
|
|
69
|
+
.k-page__heading { display: flex; align-items: flex-start; gap: 0.625rem; min-width: 0; }
|
|
70
|
+
.k-page__back {
|
|
71
|
+
display: inline-flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
width: 2rem;
|
|
75
|
+
height: 2rem;
|
|
76
|
+
margin-top: 0.15rem;
|
|
77
|
+
border: 1px solid var(--k-border);
|
|
78
|
+
border-radius: var(--k-radius-sm);
|
|
79
|
+
background: var(--k-surface);
|
|
80
|
+
color: var(--k-text-muted);
|
|
81
|
+
flex: none;
|
|
82
|
+
}
|
|
83
|
+
.k-page__back:hover { background: var(--k-surface-muted); color: var(--k-text); }
|
|
84
|
+
.k-page__titles { min-width: 0; }
|
|
85
|
+
.k-page__title-row { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
|
|
86
|
+
.k-page__subtitle { margin-top: 0.25rem; font-size: 0.875rem; color: var(--k-text-muted); }
|
|
87
|
+
.k-page__actions { display: flex; align-items: center; gap: 0.5rem; flex: none; flex-wrap: wrap; justify-content: flex-end; }
|
|
88
|
+
|
|
89
|
+
@media (max-width: 720px) {
|
|
90
|
+
.k-page { padding: 1.25rem 1rem 2.5rem; }
|
|
91
|
+
.k-page__header { flex-direction: column; align-items: stretch; }
|
|
92
|
+
}
|
|
93
|
+
</style>
|
|
@@ -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>
|