@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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +235 -2
  3. package/dist/ui-kit.css +2 -0
  4. package/dist/ui-kit.js +15222 -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/KGantt.vue +398 -0
  22. package/src/components/KIcon.vue +53 -0
  23. package/src/components/KInput.vue +108 -0
  24. package/src/components/KMenuItem.vue +56 -0
  25. package/src/components/KModal.vue +136 -0
  26. package/src/components/KMoney.vue +44 -0
  27. package/src/components/KPage.vue +93 -0
  28. package/src/components/KPagination.vue +113 -0
  29. package/src/components/KProgress.vue +63 -0
  30. package/src/components/KRadioGroup.vue +103 -0
  31. package/src/components/KSegmented.vue +88 -0
  32. package/src/components/KSelect.vue +231 -0
  33. package/src/components/KShell.vue +176 -0
  34. package/src/components/KStat.vue +74 -0
  35. package/src/components/KSteps.vue +119 -0
  36. package/src/components/KSwitch.vue +82 -0
  37. package/src/components/KTable.vue +161 -0
  38. package/src/components/KTabs.vue +102 -0
  39. package/src/components/KTextarea.vue +79 -0
  40. package/src/components/KTimeline.vue +72 -0
  41. package/src/components/KToast.vue +34 -0
  42. package/src/components/KToastItem.vue +86 -0
  43. package/src/components/KTooltip.vue +67 -0
  44. package/src/components/icon-registry.js +143 -0
  45. package/src/composables/useToast.js +52 -0
  46. package/src/format.js +137 -0
  47. package/src/index.js +85 -0
  48. package/src/theme.js +417 -0
  49. 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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.4.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>