@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.
- package/LICENSE +21 -0
- package/README.md +228 -2
- package/dist/ui-kit.css +2 -0
- package/dist/ui-kit.js +14955 -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/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 +84 -0
- package/src/theme.js +417 -0
- package/src/tokens.css +109 -0
package/package.json
CHANGED
|
@@ -1,6 +1,77 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krometra/ui-kit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "UI-kit на Vue 3 со сменными темами, светлой и тёмной схемой; интерактивные компоненты на zag.js",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"packageManager": "yarn@4.13.0",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
12
|
+
"main": "./dist/ui-kit.umd.cjs",
|
|
13
|
+
"module": "./dist/ui-kit.js",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"development": "./src/index.js",
|
|
17
|
+
"import": "./dist/ui-kit.js",
|
|
18
|
+
"require": "./dist/ui-kit.umd.cjs",
|
|
19
|
+
"default": "./src/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./style.css": "./dist/ui-kit.css",
|
|
22
|
+
"./src/*": "./src/*",
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"dev": "vite",
|
|
27
|
+
"build": "vite build",
|
|
28
|
+
"build:demo": "vite build --config vite.demo.config.js",
|
|
29
|
+
"preview": "vite preview",
|
|
30
|
+
"prepack": "vite build >&2",
|
|
31
|
+
"prepare": "vite build >&2",
|
|
32
|
+
"tokens": "node scripts/build-tokens.mjs"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"vue": "^3.4.0"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@zag-js/checkbox": "^1.43.3",
|
|
39
|
+
"@zag-js/combobox": "^1.43.3",
|
|
40
|
+
"@zag-js/dialog": "^1.43.3",
|
|
41
|
+
"@zag-js/file-upload": "^1.43.3",
|
|
42
|
+
"@zag-js/pagination": "^1.43.3",
|
|
43
|
+
"@zag-js/popover": "^1.43.3",
|
|
44
|
+
"@zag-js/progress": "^1.43.3",
|
|
45
|
+
"@zag-js/radio-group": "^1.43.3",
|
|
46
|
+
"@zag-js/select": "^1.43.3",
|
|
47
|
+
"@zag-js/steps": "^1.43.3",
|
|
48
|
+
"@zag-js/switch": "^1.43.3",
|
|
49
|
+
"@zag-js/tabs": "^1.43.3",
|
|
50
|
+
"@zag-js/toast": "^1.43.3",
|
|
51
|
+
"@zag-js/tooltip": "^1.43.3",
|
|
52
|
+
"@zag-js/vue": "^1.43.3",
|
|
53
|
+
"lucide-vue-next": "^1.0.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@vitejs/plugin-vue": "^6.0.5",
|
|
57
|
+
"vite": "^8.0.1",
|
|
58
|
+
"vue": "^3.5.30"
|
|
59
|
+
},
|
|
60
|
+
"repository": {
|
|
61
|
+
"type": "git",
|
|
62
|
+
"url": "git+https://github.com/krometra/ui-kit.git"
|
|
63
|
+
},
|
|
64
|
+
"keywords": [
|
|
65
|
+
"vue",
|
|
66
|
+
"vue3",
|
|
67
|
+
"ui-kit",
|
|
68
|
+
"design-system",
|
|
69
|
+
"components",
|
|
70
|
+
"theme",
|
|
71
|
+
"theming",
|
|
72
|
+
"light-theme",
|
|
73
|
+
"dark-theme",
|
|
74
|
+
"zag-js",
|
|
75
|
+
"accessible"
|
|
76
|
+
]
|
|
6
77
|
}
|
package/src/base.css
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Базовые стили страницы. Импортируются отдельно от токенов, чтобы приложение
|
|
3
|
+
* могло взять только примитивы, не трогая глобальный reset.
|
|
4
|
+
*/
|
|
5
|
+
@import './tokens.css';
|
|
6
|
+
|
|
7
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
8
|
+
|
|
9
|
+
html { -webkit-text-size-adjust: 100%; }
|
|
10
|
+
|
|
11
|
+
body {
|
|
12
|
+
margin: 0;
|
|
13
|
+
font-family: var(--k-font);
|
|
14
|
+
font-size: 15px;
|
|
15
|
+
line-height: 1.5;
|
|
16
|
+
color: var(--k-text);
|
|
17
|
+
background: var(--k-bg);
|
|
18
|
+
-webkit-font-smoothing: antialiased;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
h1, h2, h3, h4, h5, h6, p, figure { margin: 0; }
|
|
22
|
+
h1 { font-size: 1.5rem; font-weight: 650; letter-spacing: -0.01em; }
|
|
23
|
+
h2 { font-size: 1.125rem; font-weight: 600; }
|
|
24
|
+
h3 { font-size: 1rem; font-weight: 600; }
|
|
25
|
+
|
|
26
|
+
a { color: var(--k-primary); text-decoration: none; }
|
|
27
|
+
a:hover { text-decoration: underline; }
|
|
28
|
+
|
|
29
|
+
::selection { background: var(--k-selection); }
|
|
30
|
+
|
|
31
|
+
/* Единый вид фокуса для клавиатуры во всём ките */
|
|
32
|
+
:focus-visible { outline: 2px solid var(--k-primary); outline-offset: 2px; }
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* KAlert — информационная плашка: подсказка, предупреждение, сообщение об
|
|
4
|
+
* ошибке.
|
|
5
|
+
*
|
|
6
|
+
* tone — 'info' | 'success' | 'warning' | 'danger'
|
|
7
|
+
*/
|
|
8
|
+
import { computed } from 'vue'
|
|
9
|
+
import KIcon from './KIcon.vue'
|
|
10
|
+
|
|
11
|
+
const props = defineProps({
|
|
12
|
+
tone: { type: String, default: 'info' },
|
|
13
|
+
title: { type: String, default: '' },
|
|
14
|
+
icon: { type: String, default: '' },
|
|
15
|
+
closable: { type: Boolean, default: false },
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
defineEmits(['close'])
|
|
19
|
+
|
|
20
|
+
const defaultIcons = { info: 'info', success: 'check-badge', warning: 'alert', danger: 'reject' }
|
|
21
|
+
const iconName = computed(() => props.icon || defaultIcons[props.tone] || 'info')
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<div class="k-alert" :class="`k-alert--${tone}`" role="status">
|
|
26
|
+
<KIcon :name="iconName" :size="18" class="k-alert__icon" />
|
|
27
|
+
<div class="k-alert__body">
|
|
28
|
+
<p v-if="title" class="k-alert__title">{{ title }}</p>
|
|
29
|
+
<div v-if="$slots.default" class="k-alert__text"><slot /></div>
|
|
30
|
+
</div>
|
|
31
|
+
<button v-if="closable" class="k-alert__close" aria-label="Закрыть" @click="$emit('close')">
|
|
32
|
+
<KIcon name="close" :size="15" />
|
|
33
|
+
</button>
|
|
34
|
+
</div>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<style scoped>
|
|
38
|
+
.k-alert {
|
|
39
|
+
display: flex;
|
|
40
|
+
align-items: flex-start;
|
|
41
|
+
gap: 0.625rem;
|
|
42
|
+
padding: 0.75rem 0.875rem;
|
|
43
|
+
border: 1px solid transparent;
|
|
44
|
+
border-radius: var(--k-radius-sm);
|
|
45
|
+
font-size: 0.875rem;
|
|
46
|
+
}
|
|
47
|
+
.k-alert__icon { margin-top: 0.1rem; flex: none; }
|
|
48
|
+
.k-alert__body { flex: 1; min-width: 0; }
|
|
49
|
+
.k-alert__title { font-weight: 600; }
|
|
50
|
+
.k-alert__title + .k-alert__text { margin-top: 0.15rem; }
|
|
51
|
+
.k-alert__close {
|
|
52
|
+
background: none;
|
|
53
|
+
border: none;
|
|
54
|
+
color: inherit;
|
|
55
|
+
opacity: 0.6;
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
padding: 0;
|
|
58
|
+
flex: none;
|
|
59
|
+
}
|
|
60
|
+
.k-alert__close:hover { opacity: 1; }
|
|
61
|
+
.k-alert--info { background: var(--k-primary-soft); border-color: color-mix(in srgb, var(--k-primary) 25%, transparent); color: var(--k-primary-hover); }
|
|
62
|
+
.k-alert--success { background: var(--k-success-soft); border-color: color-mix(in srgb, var(--k-success) 30%, transparent); color: var(--k-success-strong); }
|
|
63
|
+
.k-alert--warning { background: var(--k-warning-soft); border-color: color-mix(in srgb, var(--k-warning) 35%, transparent); color: var(--k-warning-strong); }
|
|
64
|
+
.k-alert--danger { background: var(--k-danger-soft); border-color: color-mix(in srgb, var(--k-danger) 30%, transparent); color: var(--k-danger-strong); }
|
|
65
|
+
</style>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* KAvatar — инициалы пользователя или сущности в кружке.
|
|
4
|
+
* Оттенок детерминированно выводится из имени, чтобы не хранить его в данных;
|
|
5
|
+
* сами цвета берутся из семантических токенов темы, поэтому аватары
|
|
6
|
+
* перекрашиваются вместе с ней.
|
|
7
|
+
*/
|
|
8
|
+
import { computed } from 'vue'
|
|
9
|
+
import KIcon from './KIcon.vue'
|
|
10
|
+
|
|
11
|
+
const props = defineProps({
|
|
12
|
+
name: { type: String, default: '' },
|
|
13
|
+
icon: { type: String, default: '' },
|
|
14
|
+
size: { type: String, default: 'md' }, // sm | md | lg
|
|
15
|
+
square: { type: Boolean, default: false },
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const initials = computed(() =>
|
|
19
|
+
props.name
|
|
20
|
+
.replace(/["«»]/g, '')
|
|
21
|
+
.split(/[\s-]+/)
|
|
22
|
+
.filter(Boolean)
|
|
23
|
+
.slice(0, 2)
|
|
24
|
+
.map((w) => w[0].toUpperCase())
|
|
25
|
+
.join(''),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
const tones = ['primary', 'success', 'warning', 'info', 'danger']
|
|
29
|
+
|
|
30
|
+
const tone = computed(() => {
|
|
31
|
+
let hash = 0
|
|
32
|
+
for (const ch of props.name) hash = (hash * 31 + ch.charCodeAt(0)) >>> 0
|
|
33
|
+
return tones[hash % tones.length]
|
|
34
|
+
})
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<template>
|
|
38
|
+
<span
|
|
39
|
+
class="k-avatar"
|
|
40
|
+
:class="[`k-avatar--${size}`, `k-avatar--${tone}`, { 'k-avatar--square': square }]"
|
|
41
|
+
:title="name"
|
|
42
|
+
>
|
|
43
|
+
<KIcon v-if="icon" :name="icon" :size="size === 'sm' ? 14 : size === 'lg' ? 22 : 17" />
|
|
44
|
+
<template v-else>{{ initials }}</template>
|
|
45
|
+
</span>
|
|
46
|
+
</template>
|
|
47
|
+
|
|
48
|
+
<style scoped>
|
|
49
|
+
.k-avatar {
|
|
50
|
+
display: inline-flex;
|
|
51
|
+
align-items: center;
|
|
52
|
+
justify-content: center;
|
|
53
|
+
border-radius: 50%;
|
|
54
|
+
font-weight: 600;
|
|
55
|
+
flex: none;
|
|
56
|
+
user-select: none;
|
|
57
|
+
}
|
|
58
|
+
.k-avatar--square { border-radius: var(--k-radius-sm); }
|
|
59
|
+
.k-avatar--sm { width: 1.75rem; height: 1.75rem; font-size: 0.6875rem; }
|
|
60
|
+
.k-avatar--md { width: 2.25rem; height: 2.25rem; font-size: 0.8125rem; }
|
|
61
|
+
.k-avatar--lg { width: 3rem; height: 3rem; font-size: 1rem; }
|
|
62
|
+
.k-avatar--primary { background: var(--k-primary-soft); color: var(--k-primary); }
|
|
63
|
+
.k-avatar--success { background: var(--k-success-soft); color: var(--k-success-strong); }
|
|
64
|
+
.k-avatar--warning { background: var(--k-warning-soft); color: var(--k-warning-strong); }
|
|
65
|
+
.k-avatar--info { background: var(--k-info-soft); color: var(--k-info-strong); }
|
|
66
|
+
.k-avatar--danger { background: var(--k-danger-soft); color: var(--k-danger-strong); }
|
|
67
|
+
</style>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* KBadge — статусная метка: состояние записи, тег, признак.
|
|
4
|
+
*
|
|
5
|
+
* tone — 'neutral' | 'primary' | 'success' | 'warning' | 'danger' | 'info'
|
|
6
|
+
* dot — точка-индикатор перед текстом (для состояний «идёт сейчас»)
|
|
7
|
+
* icon — семантическое имя KIcon перед текстом
|
|
8
|
+
*/
|
|
9
|
+
import KIcon from './KIcon.vue'
|
|
10
|
+
|
|
11
|
+
defineProps({
|
|
12
|
+
tone: { type: String, default: 'neutral' },
|
|
13
|
+
size: { type: String, default: 'md' }, // md | sm
|
|
14
|
+
dot: { type: Boolean, default: false },
|
|
15
|
+
icon: { type: String, default: '' },
|
|
16
|
+
})
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<span class="k-badge" :class="[`k-badge--${tone}`, `k-badge--${size}`]">
|
|
21
|
+
<span v-if="dot" class="k-badge__dot" />
|
|
22
|
+
<KIcon v-if="icon" :name="icon" :size="size === 'sm' ? 11 : 13" />
|
|
23
|
+
<slot />
|
|
24
|
+
</span>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<style scoped>
|
|
28
|
+
.k-badge {
|
|
29
|
+
display: inline-flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
gap: 0.35rem;
|
|
32
|
+
padding: 0.15rem 0.5rem;
|
|
33
|
+
border-radius: 999px;
|
|
34
|
+
font-size: 0.75rem;
|
|
35
|
+
font-weight: 500;
|
|
36
|
+
line-height: 1.4;
|
|
37
|
+
white-space: nowrap;
|
|
38
|
+
}
|
|
39
|
+
.k-badge--sm { font-size: 0.6875rem; padding: 0.1rem 0.4rem; }
|
|
40
|
+
.k-badge__dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }
|
|
41
|
+
.k-badge--neutral { background: var(--k-surface-muted); color: var(--k-text-muted); }
|
|
42
|
+
.k-badge--primary { background: var(--k-primary-soft); color: var(--k-primary); }
|
|
43
|
+
.k-badge--success { background: var(--k-success-soft); color: var(--k-success-strong); }
|
|
44
|
+
.k-badge--warning { background: var(--k-warning-soft); color: var(--k-warning-strong); }
|
|
45
|
+
.k-badge--danger { background: var(--k-danger-soft); color: var(--k-danger-strong); }
|
|
46
|
+
.k-badge--info { background: var(--k-info-soft); color: var(--k-info-strong); }
|
|
47
|
+
</style>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* KButton — единственный кнопочный примитив кита.
|
|
4
|
+
*
|
|
5
|
+
* variant — 'primary' | 'secondary' | 'ghost' | 'danger' | 'success' | 'link'
|
|
6
|
+
* size — 'md' (по умолчанию) | 'sm' | 'lg'
|
|
7
|
+
* icon — семантическое имя KIcon перед подписью
|
|
8
|
+
* iconEnd — иконка после подписи
|
|
9
|
+
* loading — блокирует кнопку и показывает спиннер вместо иконки
|
|
10
|
+
*
|
|
11
|
+
* Подпись — слот по умолчанию и необязательна: с одним `icon` кнопка
|
|
12
|
+
* рендерится квадратной иконочной.
|
|
13
|
+
*/
|
|
14
|
+
import { computed, useSlots } from 'vue'
|
|
15
|
+
import KIcon from './KIcon.vue'
|
|
16
|
+
|
|
17
|
+
const props = defineProps({
|
|
18
|
+
variant: { type: String, default: 'secondary' },
|
|
19
|
+
size: { type: String, default: 'md' },
|
|
20
|
+
icon: { type: String, default: '' },
|
|
21
|
+
iconEnd: { type: String, default: '' },
|
|
22
|
+
htmlType: { type: String, default: 'button' },
|
|
23
|
+
disabled: { type: Boolean, default: false },
|
|
24
|
+
loading: { type: Boolean, default: false },
|
|
25
|
+
block: { type: Boolean, default: false },
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const slots = useSlots()
|
|
29
|
+
const hasLabel = computed(() => !!slots.default)
|
|
30
|
+
const iconPx = computed(() => ({ sm: 15, md: 17, lg: 19 })[props.size] ?? 17)
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<button
|
|
35
|
+
class="k-btn"
|
|
36
|
+
:class="[
|
|
37
|
+
`k-btn--${variant}`,
|
|
38
|
+
`k-btn--${size}`,
|
|
39
|
+
{ 'k-btn--icon-only': (icon || loading) && !hasLabel, 'k-btn--block': block },
|
|
40
|
+
]"
|
|
41
|
+
:type="htmlType"
|
|
42
|
+
:disabled="disabled || loading"
|
|
43
|
+
>
|
|
44
|
+
<KIcon v-if="loading" name="spinner" :size="iconPx" spin />
|
|
45
|
+
<KIcon v-else-if="icon" :name="icon" :size="iconPx" />
|
|
46
|
+
<span v-if="hasLabel" class="k-btn__label"><slot /></span>
|
|
47
|
+
<KIcon v-if="iconEnd && !loading" :name="iconEnd" :size="iconPx" />
|
|
48
|
+
</button>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<style scoped>
|
|
52
|
+
.k-btn {
|
|
53
|
+
display: inline-flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
justify-content: center;
|
|
56
|
+
gap: 0.4rem;
|
|
57
|
+
min-height: var(--k-control-h);
|
|
58
|
+
padding: 0 0.875rem;
|
|
59
|
+
border: 1px solid transparent;
|
|
60
|
+
border-radius: var(--k-radius-sm);
|
|
61
|
+
font: inherit;
|
|
62
|
+
font-size: 0.9375rem;
|
|
63
|
+
font-weight: 500;
|
|
64
|
+
line-height: 1;
|
|
65
|
+
white-space: nowrap;
|
|
66
|
+
cursor: pointer;
|
|
67
|
+
user-select: none;
|
|
68
|
+
transition: background 0.15s, border-color 0.15s, color 0.15s, box-shadow 0.15s;
|
|
69
|
+
}
|
|
70
|
+
.k-btn:focus { outline: none; }
|
|
71
|
+
.k-btn:focus-visible { outline: 2px solid var(--k-primary); outline-offset: 2px; }
|
|
72
|
+
.k-btn:disabled { opacity: 0.55; cursor: not-allowed; }
|
|
73
|
+
.k-btn--block { width: 100%; }
|
|
74
|
+
|
|
75
|
+
.k-btn--sm { min-height: var(--k-control-h-sm); padding: 0 0.625rem; font-size: 0.8125rem; }
|
|
76
|
+
.k-btn--lg { min-height: 2.875rem; padding: 0 1.25rem; font-size: 1rem; }
|
|
77
|
+
.k-btn--icon-only { width: var(--k-control-h); padding: 0; }
|
|
78
|
+
.k-btn--icon-only.k-btn--sm { width: var(--k-control-h-sm); }
|
|
79
|
+
.k-btn--icon-only.k-btn--lg { width: 2.875rem; }
|
|
80
|
+
|
|
81
|
+
.k-btn--primary { background: var(--k-primary); color: var(--k-on-primary); box-shadow: var(--k-shadow-sm); }
|
|
82
|
+
.k-btn--primary:hover:not(:disabled) { background: var(--k-primary-hover); }
|
|
83
|
+
|
|
84
|
+
.k-btn--secondary { background: var(--k-surface); border-color: var(--k-border-strong); color: var(--k-text); }
|
|
85
|
+
.k-btn--secondary:hover:not(:disabled) { background: var(--k-surface-muted); }
|
|
86
|
+
|
|
87
|
+
.k-btn--ghost { background: transparent; color: var(--k-text-muted); }
|
|
88
|
+
.k-btn--ghost:hover:not(:disabled) { background: var(--k-surface-muted); color: var(--k-text); }
|
|
89
|
+
|
|
90
|
+
.k-btn--danger { background: var(--k-danger); color: var(--k-on-danger); }
|
|
91
|
+
.k-btn--danger:hover:not(:disabled) { background: var(--k-danger-hover); }
|
|
92
|
+
|
|
93
|
+
.k-btn--success { background: var(--k-success); color: var(--k-on-success); }
|
|
94
|
+
.k-btn--success:hover:not(:disabled) { background: var(--k-success-hover); }
|
|
95
|
+
|
|
96
|
+
.k-btn--link { background: none; color: var(--k-primary); padding: 0; min-height: 0; }
|
|
97
|
+
.k-btn--link:hover:not(:disabled) { text-decoration: underline; }
|
|
98
|
+
</style>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* KCard — базовая поверхность: белая карточка со светлой рамкой.
|
|
4
|
+
*
|
|
5
|
+
* Заголовок задаётся пропом `title` (+ `subtitle`) либо слотом #header;
|
|
6
|
+
* действия шапки — #actions, низ — #footer. `padding="none"` нужен, когда
|
|
7
|
+
* внутрь кладут таблицу во всю ширину.
|
|
8
|
+
*/
|
|
9
|
+
defineProps({
|
|
10
|
+
title: { type: String, default: '' },
|
|
11
|
+
subtitle: { type: String, default: '' },
|
|
12
|
+
padding: { type: String, default: 'md' }, // md | sm | none
|
|
13
|
+
hoverable: { type: Boolean, default: false },
|
|
14
|
+
})
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<section class="k-card" :class="[`k-card--p-${padding}`, { 'k-card--hoverable': hoverable }]">
|
|
19
|
+
<header v-if="title || subtitle || $slots.header || $slots.actions" class="k-card__header">
|
|
20
|
+
<div class="k-card__heading">
|
|
21
|
+
<slot name="header">
|
|
22
|
+
<h2 v-if="title" class="k-card__title">{{ title }}</h2>
|
|
23
|
+
<p v-if="subtitle" class="k-card__subtitle">{{ subtitle }}</p>
|
|
24
|
+
</slot>
|
|
25
|
+
</div>
|
|
26
|
+
<div v-if="$slots.actions" class="k-card__actions"><slot name="actions" /></div>
|
|
27
|
+
</header>
|
|
28
|
+
<div class="k-card__body"><slot /></div>
|
|
29
|
+
<footer v-if="$slots.footer" class="k-card__footer"><slot name="footer" /></footer>
|
|
30
|
+
</section>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<style scoped>
|
|
34
|
+
.k-card {
|
|
35
|
+
background: var(--k-surface);
|
|
36
|
+
border: 1px solid var(--k-border);
|
|
37
|
+
border-radius: var(--k-radius);
|
|
38
|
+
box-shadow: var(--k-shadow-sm);
|
|
39
|
+
overflow: hidden;
|
|
40
|
+
}
|
|
41
|
+
.k-card--hoverable { transition: box-shadow 0.15s, border-color 0.15s; }
|
|
42
|
+
.k-card--hoverable:hover { box-shadow: var(--k-shadow-md); border-color: var(--k-border-strong); }
|
|
43
|
+
.k-card__header {
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: flex-start;
|
|
46
|
+
justify-content: space-between;
|
|
47
|
+
gap: 1rem;
|
|
48
|
+
padding: 1rem 1.25rem;
|
|
49
|
+
border-bottom: 1px solid var(--k-border);
|
|
50
|
+
}
|
|
51
|
+
.k-card__heading { min-width: 0; }
|
|
52
|
+
.k-card__title { font-size: 1rem; font-weight: 600; }
|
|
53
|
+
.k-card__subtitle { margin-top: 0.15rem; font-size: 0.8125rem; color: var(--k-text-muted); }
|
|
54
|
+
.k-card__actions { display: flex; align-items: center; gap: 0.5rem; flex: none; }
|
|
55
|
+
.k-card--p-md .k-card__body { padding: 1.25rem; }
|
|
56
|
+
.k-card--p-sm .k-card__body { padding: 0.75rem; }
|
|
57
|
+
.k-card--p-none .k-card__body { padding: 0; }
|
|
58
|
+
.k-card__footer {
|
|
59
|
+
padding: 0.875rem 1.25rem;
|
|
60
|
+
border-top: 1px solid var(--k-border);
|
|
61
|
+
background: var(--k-surface-muted);
|
|
62
|
+
}
|
|
63
|
+
</style>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* KCheckbox — чекбокс на @zag-js/checkbox. Скрытый нативный input остаётся в
|
|
4
|
+
* потоке фокуса и участвует в формах, а видимый квадрат рисуется из токенов
|
|
5
|
+
* темы. v-model — булево или массив (тогда нужен `value`).
|
|
6
|
+
*
|
|
7
|
+
* <KCheckbox v-model="agree" label="Принимаю условия" />
|
|
8
|
+
* <KCheckbox v-model="selected" value="pdf" label="PDF" />
|
|
9
|
+
*/
|
|
10
|
+
import * as checkbox from '@zag-js/checkbox'
|
|
11
|
+
import { normalizeProps, useMachine } from '@zag-js/vue'
|
|
12
|
+
import { computed, useId } from 'vue'
|
|
13
|
+
import KIcon from './KIcon.vue'
|
|
14
|
+
|
|
15
|
+
const props = defineProps({
|
|
16
|
+
modelValue: { type: [Boolean, Array], default: false },
|
|
17
|
+
value: { type: [String, Number, Boolean], default: undefined },
|
|
18
|
+
label: { type: String, default: '' },
|
|
19
|
+
hint: { type: String, default: '' },
|
|
20
|
+
name: { type: String, default: '' },
|
|
21
|
+
indeterminate: { type: Boolean, default: false },
|
|
22
|
+
disabled: { type: Boolean, default: false },
|
|
23
|
+
required: { type: Boolean, default: false },
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const emit = defineEmits(['update:modelValue'])
|
|
27
|
+
|
|
28
|
+
const isArray = computed(() => Array.isArray(props.modelValue))
|
|
29
|
+
const checked = computed(() => (isArray.value ? props.modelValue.includes(props.value) : props.modelValue))
|
|
30
|
+
|
|
31
|
+
const service = useMachine(checkbox.machine, {
|
|
32
|
+
id: useId(),
|
|
33
|
+
get checked() {
|
|
34
|
+
return props.indeterminate ? 'indeterminate' : checked.value
|
|
35
|
+
},
|
|
36
|
+
get disabled() {
|
|
37
|
+
return props.disabled
|
|
38
|
+
},
|
|
39
|
+
get required() {
|
|
40
|
+
return props.required
|
|
41
|
+
},
|
|
42
|
+
get name() {
|
|
43
|
+
return props.name || undefined
|
|
44
|
+
},
|
|
45
|
+
get value() {
|
|
46
|
+
return props.value != null ? String(props.value) : undefined
|
|
47
|
+
},
|
|
48
|
+
onCheckedChange({ checked: next }) {
|
|
49
|
+
const on = next === true
|
|
50
|
+
if (!isArray.value) {
|
|
51
|
+
emit('update:modelValue', on)
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
emit(
|
|
55
|
+
'update:modelValue',
|
|
56
|
+
on ? [...props.modelValue, props.value] : props.modelValue.filter((v) => v !== props.value),
|
|
57
|
+
)
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const api = computed(() => checkbox.connect(service, normalizeProps))
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<template>
|
|
65
|
+
<label class="k-check" v-bind="api.getRootProps()">
|
|
66
|
+
<input v-bind="api.getHiddenInputProps()" />
|
|
67
|
+
<span class="k-check__control" v-bind="api.getControlProps()">
|
|
68
|
+
<span class="k-check__indicator" v-bind="api.getIndicatorProps()">
|
|
69
|
+
<KIcon :name="api.indeterminate ? 'remove' : 'check'" :size="12" :stroke-width="3" />
|
|
70
|
+
</span>
|
|
71
|
+
</span>
|
|
72
|
+
<span v-if="label || hint || $slots.default" class="k-check__body">
|
|
73
|
+
<span class="k-check__label" v-bind="api.getLabelProps()"><slot>{{ label }}</slot></span>
|
|
74
|
+
<span v-if="hint" class="k-check__hint">{{ hint }}</span>
|
|
75
|
+
</span>
|
|
76
|
+
</label>
|
|
77
|
+
</template>
|
|
78
|
+
|
|
79
|
+
<style scoped>
|
|
80
|
+
.k-check { display: inline-flex; align-items: flex-start; gap: 0.5rem; margin: 0; cursor: pointer; font-weight: 400; }
|
|
81
|
+
.k-check[data-disabled] { cursor: not-allowed; opacity: 0.6; }
|
|
82
|
+
.k-check__control {
|
|
83
|
+
display: inline-flex;
|
|
84
|
+
align-items: center;
|
|
85
|
+
justify-content: center;
|
|
86
|
+
width: 1.05rem;
|
|
87
|
+
height: 1.05rem;
|
|
88
|
+
margin-top: 0.15rem;
|
|
89
|
+
flex: none;
|
|
90
|
+
background: var(--k-surface);
|
|
91
|
+
border: 1px solid var(--k-border-strong);
|
|
92
|
+
border-radius: calc(var(--k-radius-sm) - 2px);
|
|
93
|
+
color: var(--k-on-primary);
|
|
94
|
+
transition: background 0.12s, border-color 0.12s, box-shadow 0.12s;
|
|
95
|
+
}
|
|
96
|
+
.k-check__control[data-hover]:not([data-disabled]) { border-color: var(--k-primary); }
|
|
97
|
+
.k-check__control[data-focus-visible] { box-shadow: 0 0 0 3px var(--k-primary-soft); border-color: var(--k-primary); }
|
|
98
|
+
.k-check__control[data-state="checked"],
|
|
99
|
+
.k-check__control[data-state="indeterminate"] { background: var(--k-primary); border-color: var(--k-primary); }
|
|
100
|
+
.k-check__indicator { display: inline-flex; }
|
|
101
|
+
.k-check__indicator[hidden] { display: none; }
|
|
102
|
+
.k-check__body { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
|
|
103
|
+
.k-check__label { font-size: 0.9375rem; color: var(--k-text); }
|
|
104
|
+
.k-check__hint { font-size: 0.75rem; color: var(--k-text-muted); }
|
|
105
|
+
</style>
|