@krometra/ui-kit 0.4.1 → 0.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krometra/ui-kit",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "description": "UI-kit на Vue 3 со сменными темами, светлой и тёмной схемой; интерактивные компоненты на zag.js",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,102 @@
1
+ <script setup>
2
+ /**
3
+ * KChip — кнопка-признак в виде таблетки: выбор вариантов, фильтры, теги.
4
+ *
5
+ * v-model:pressed — нажата ли; без него чип — обычная кнопка (событие click)
6
+ * tone — цвет нажатого: 'neutral' | 'primary' | 'success' | 'warning' | 'danger' | 'info'
7
+ * soft — подкрашен тоном и без нажатия (кнопка меню, акцент)
8
+ * dashed — пунктирная рамка (вторичный вариант)
9
+ * expanded — true/false рисует стрелку и ставит aria-expanded (чип открывает панель)
10
+ * marked — точка перед подписью: внутри что-то выбрано
11
+ * size — 'md' (по умолчанию) | 'sm'
12
+ * icon — семантическое имя KIcon перед подписью
13
+ */
14
+ import { computed } from 'vue'
15
+ import KIcon from './KIcon.vue'
16
+
17
+ const props = defineProps({
18
+ pressed: { type: Boolean, default: undefined },
19
+ tone: { type: String, default: 'primary' },
20
+ soft: { type: Boolean, default: false },
21
+ dashed: { type: Boolean, default: false },
22
+ expanded: { type: Boolean, default: undefined },
23
+ marked: { type: Boolean, default: false },
24
+ size: { type: String, default: 'md' },
25
+ icon: { type: String, default: '' },
26
+ disabled: { type: Boolean, default: false },
27
+ })
28
+ const emit = defineEmits(['update:pressed', 'click'])
29
+
30
+ const toggle = computed(() => props.pressed !== undefined)
31
+ const menu = computed(() => props.expanded !== undefined)
32
+
33
+ function click(event) {
34
+ if (toggle.value) emit('update:pressed', !props.pressed)
35
+ emit('click', event)
36
+ }
37
+ </script>
38
+
39
+ <template>
40
+ <button
41
+ type="button"
42
+ class="k-chip"
43
+ :class="[
44
+ `k-chip--${tone}`,
45
+ `k-chip--${size}`,
46
+ { 'k-chip--on': pressed, 'k-chip--soft': soft, 'k-chip--dashed': dashed },
47
+ ]"
48
+ :aria-pressed="toggle ? String(!!pressed) : undefined"
49
+ :aria-expanded="menu ? String(!!expanded) : undefined"
50
+ :disabled="disabled"
51
+ @click="click"
52
+ >
53
+ <span v-if="marked" class="k-chip__mark" />
54
+ <KIcon v-if="icon" :name="icon" :size="size === 'sm' ? 13 : 15" />
55
+ <span class="k-chip__label"><slot /></span>
56
+ <KIcon v-if="menu" :name="expanded ? 'chevron-up' : 'chevron-down'" :size="size === 'sm' ? 13 : 14" />
57
+ </button>
58
+ </template>
59
+
60
+ <style scoped>
61
+ .k-chip {
62
+ --tone: var(--k-primary);
63
+ --tone-soft: var(--k-primary-soft);
64
+ --tone-strong: var(--k-primary);
65
+ display: inline-flex;
66
+ align-items: center;
67
+ gap: 0.35rem;
68
+ max-width: 100%;
69
+ padding: 0.25rem 0.75rem;
70
+ border: 1px solid var(--k-border);
71
+ border-radius: 999px;
72
+ background: transparent;
73
+ color: var(--k-text);
74
+ font: inherit;
75
+ font-size: 0.875rem;
76
+ line-height: 1.35;
77
+ text-align: left;
78
+ cursor: pointer;
79
+ transition: background-color 0.12s, border-color 0.12s, color 0.12s;
80
+ }
81
+ .k-chip--sm { padding: 0.15rem 0.6rem; font-size: 0.8125rem; }
82
+ .k-chip__label { min-width: 0; overflow-wrap: anywhere; }
83
+ .k-chip__mark { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }
84
+ .k-chip:hover:not(:disabled) { border-color: var(--k-border-strong); }
85
+ .k-chip:focus-visible { outline: 2px solid var(--k-primary); outline-offset: 2px; }
86
+ .k-chip:disabled { opacity: 0.5; cursor: not-allowed; }
87
+ .k-chip--dashed { border-style: dashed; }
88
+ .k-chip--on,
89
+ .k-chip--soft { background: var(--tone-soft); border-color: var(--tone); color: var(--tone-strong); font-weight: 500; }
90
+ .k-chip--on:hover:not(:disabled),
91
+ .k-chip--soft:hover:not(:disabled) { border-color: var(--tone-strong); }
92
+
93
+ .k-chip--neutral { --tone: var(--k-border-strong); --tone-soft: var(--k-surface-muted); --tone-strong: var(--k-text); }
94
+ .k-chip--success { --tone: var(--k-success); --tone-soft: var(--k-success-soft); --tone-strong: var(--k-success-strong); }
95
+ .k-chip--warning { --tone: var(--k-warning); --tone-soft: var(--k-warning-soft); --tone-strong: var(--k-warning-strong); }
96
+ .k-chip--danger { --tone: var(--k-danger); --tone-soft: var(--k-danger-soft); --tone-strong: var(--k-danger-strong); }
97
+ .k-chip--info { --tone: var(--k-info); --tone-soft: var(--k-info-soft); --tone-strong: var(--k-info-strong); }
98
+
99
+ @media (prefers-reduced-motion: reduce) {
100
+ .k-chip { transition: none; }
101
+ }
102
+ </style>
@@ -85,8 +85,8 @@ const service = useMachine(combobox.machine, {
85
85
  const match = itemFor(props.modelValue)
86
86
  return match ? [keyOf(match)] : []
87
87
  },
88
- onOpenChange() {
89
- filtered.value = items.value
88
+ onOpenChange({ open }) {
89
+ if (!open) filtered.value = items.value
90
90
  },
91
91
  onInputValueChange({ inputValue }) {
92
92
  applyFilter(inputValue)
@@ -0,0 +1,176 @@
1
+ <script setup>
2
+ /**
3
+ * KDrawer — боковая панель на @zag-js/dialog: карточка записи, подробности,
4
+ * фильтры. Выезжает справа или слева, закрывается по Esc и крестику.
5
+ * Открытие управляется v-model.
6
+ *
7
+ * side — 'right' (по умолчанию) | 'left'
8
+ * size — 'sm' | 'md' | 'lg'
9
+ * modal — с затемнением, фокус-трапом и блокировкой скролла (по умолчанию).
10
+ * `:modal="false"` оставляет страницу живой: можно кликать по списку
11
+ * и менять содержимое панели, не закрывая её.
12
+ *
13
+ * <KDrawer v-model="open" :title="issue.key" :subtitle="issue.summary" :modal="false">
14
+ * …
15
+ * <template #actions><KButton variant="ghost" icon="external" /></template>
16
+ * <template #footer><KButton variant="primary">Сохранить</KButton></template>
17
+ * </KDrawer>
18
+ */
19
+ import * as dialog from '@zag-js/dialog'
20
+ import { normalizeProps, useMachine } from '@zag-js/vue'
21
+ import { computed, useId } from 'vue'
22
+ import KIcon from './KIcon.vue'
23
+
24
+ const props = defineProps({
25
+ modelValue: { type: Boolean, default: false },
26
+ title: { type: String, default: '' },
27
+ subtitle: { type: String, default: '' },
28
+ side: { type: String, default: 'right' },
29
+ size: { type: String, default: 'md' },
30
+ modal: { type: Boolean, default: true },
31
+ closable: { type: Boolean, default: true },
32
+ closeOnOutside: { type: Boolean, default: false },
33
+ })
34
+
35
+ const emit = defineEmits(['update:modelValue', 'close'])
36
+
37
+ const service = useMachine(dialog.machine, {
38
+ id: useId(),
39
+ get open() {
40
+ return props.modelValue
41
+ },
42
+ get modal() {
43
+ return props.modal
44
+ },
45
+ get trapFocus() {
46
+ return props.modal
47
+ },
48
+ get preventScroll() {
49
+ return props.modal
50
+ },
51
+ get closeOnInteractOutside() {
52
+ return props.modal && props.closeOnOutside && props.closable
53
+ },
54
+ get closeOnEscape() {
55
+ return props.closable
56
+ },
57
+ onOpenChange({ open }) {
58
+ emit('update:modelValue', open)
59
+ if (!open) emit('close')
60
+ },
61
+ })
62
+
63
+ const api = computed(() => dialog.connect(service, normalizeProps))
64
+ </script>
65
+
66
+ <template>
67
+ <Teleport to="body">
68
+ <Transition name="k-drawer" :duration="220">
69
+ <div v-if="api.open" class="k-drawer" :class="[`k-drawer--${side}`, { 'k-drawer--modal': modal }]">
70
+ <div v-if="modal" class="k-drawer__backdrop" v-bind="api.getBackdropProps()" />
71
+ <div class="k-drawer__positioner" v-bind="api.getPositionerProps()">
72
+ <aside class="k-drawer__panel" :class="`k-drawer__panel--${size}`" v-bind="api.getContentProps()">
73
+ <header class="k-drawer__header">
74
+ <div class="k-drawer__heading">
75
+ <slot name="header">
76
+ <h2 class="k-drawer__title" v-bind="api.getTitleProps()">{{ title }}</h2>
77
+ <p v-if="subtitle" class="k-drawer__subtitle" v-bind="api.getDescriptionProps()">{{ subtitle }}</p>
78
+ </slot>
79
+ </div>
80
+ <div class="k-drawer__tools">
81
+ <slot name="actions" />
82
+ <button v-if="closable" class="k-drawer__close" aria-label="Закрыть" v-bind="api.getCloseTriggerProps()">
83
+ <KIcon name="close" :size="18" />
84
+ </button>
85
+ </div>
86
+ </header>
87
+ <div class="k-drawer__body"><slot /></div>
88
+ <footer v-if="$slots.footer" class="k-drawer__footer"><slot name="footer" /></footer>
89
+ </aside>
90
+ </div>
91
+ </div>
92
+ </Transition>
93
+ </Teleport>
94
+ </template>
95
+
96
+ <style scoped>
97
+ .k-drawer__backdrop {
98
+ position: fixed;
99
+ inset: 0;
100
+ z-index: 1000;
101
+ background: var(--k-overlay);
102
+ backdrop-filter: blur(2px);
103
+ }
104
+ .k-drawer__positioner {
105
+ position: fixed;
106
+ top: 0;
107
+ bottom: 0;
108
+ z-index: 1000;
109
+ display: flex;
110
+ pointer-events: none;
111
+ }
112
+ .k-drawer--right .k-drawer__positioner { right: 0; }
113
+ .k-drawer--left .k-drawer__positioner { left: 0; }
114
+ .k-drawer__panel {
115
+ pointer-events: auto;
116
+ width: min(100vw, var(--k-drawer-w, 32rem));
117
+ height: 100%;
118
+ display: flex;
119
+ flex-direction: column;
120
+ background: var(--k-surface);
121
+ box-shadow: var(--k-shadow-lg);
122
+ outline: none;
123
+ }
124
+ .k-drawer--right .k-drawer__panel { border-left: 1px solid var(--k-border); }
125
+ .k-drawer--left .k-drawer__panel { border-right: 1px solid var(--k-border); }
126
+ .k-drawer__panel--sm { --k-drawer-w: 24rem; }
127
+ .k-drawer__panel--md { --k-drawer-w: 32rem; }
128
+ .k-drawer__panel--lg { --k-drawer-w: 44rem; }
129
+ .k-drawer__header {
130
+ display: flex;
131
+ align-items: flex-start;
132
+ justify-content: space-between;
133
+ gap: 1rem;
134
+ padding: 1.125rem 1.25rem;
135
+ border-bottom: 1px solid var(--k-border);
136
+ }
137
+ .k-drawer__heading { min-width: 0; }
138
+ .k-drawer__title { font-size: 1.0625rem; font-weight: 600; }
139
+ .k-drawer__subtitle { margin-top: 0.15rem; font-size: 0.8125rem; color: var(--k-text-muted); }
140
+ .k-drawer__tools { display: flex; align-items: center; gap: 0.25rem; flex: none; }
141
+ .k-drawer__close {
142
+ background: none;
143
+ border: none;
144
+ color: var(--k-text-muted);
145
+ cursor: pointer;
146
+ padding: 0.15rem;
147
+ border-radius: var(--k-radius-sm);
148
+ }
149
+ .k-drawer__close:hover { background: var(--k-surface-muted); color: var(--k-text); }
150
+ .k-drawer__close:focus-visible { outline: 2px solid var(--k-primary); outline-offset: 1px; }
151
+ .k-drawer__body { flex: 1; padding: 1.25rem; overflow-y: auto; }
152
+ .k-drawer__footer {
153
+ display: flex;
154
+ align-items: center;
155
+ justify-content: flex-end;
156
+ gap: 0.5rem;
157
+ padding: 0.875rem 1.25rem;
158
+ border-top: 1px solid var(--k-border);
159
+ background: var(--k-surface-muted);
160
+ }
161
+
162
+ .k-drawer-enter-active .k-drawer__panel,
163
+ .k-drawer-leave-active .k-drawer__panel { transition: transform 0.22s ease; }
164
+ .k-drawer-enter-active .k-drawer__backdrop,
165
+ .k-drawer-leave-active .k-drawer__backdrop { transition: opacity 0.22s ease; }
166
+ .k-drawer-enter-from.k-drawer--right .k-drawer__panel,
167
+ .k-drawer-leave-to.k-drawer--right .k-drawer__panel { transform: translateX(100%); }
168
+ .k-drawer-enter-from.k-drawer--left .k-drawer__panel,
169
+ .k-drawer-leave-to.k-drawer--left .k-drawer__panel { transform: translateX(-100%); }
170
+ .k-drawer-enter-from .k-drawer__backdrop,
171
+ .k-drawer-leave-to .k-drawer__backdrop { opacity: 0; }
172
+ @media (prefers-reduced-motion: reduce) {
173
+ .k-drawer-enter-active .k-drawer__panel,
174
+ .k-drawer-leave-active .k-drawer__panel { transition: none; }
175
+ }
176
+ </style>
@@ -12,6 +12,10 @@
12
12
  * muted — завершённая или неактуальная задача, полоса приглушена
13
13
  * segments — несколько отрезков в одной строке (работа с перерывами)
14
14
  * dayWidth — минимальная ширина дня в px; свободное место колонки дней делят поровну
15
+ * selected — key выбранной задачи, её строка подсвечивается
16
+ *
17
+ * Клик по полосе или по названию задачи — событие item-click (у полосы с href
18
+ * открывается ссылка).
15
19
  *
16
20
  * <KGantt :items="tasks" :groups="people" from="2026-10-19" to="2026-11-06"
17
21
  * @item-click="open">
@@ -34,6 +38,7 @@ const props = defineProps({
34
38
  dayWidth: { type: Number, default: 56 },
35
39
  labelWidth: { type: String, default: '18rem' },
36
40
  emptyText: { type: String, default: 'Нет задач в этом периоде' },
41
+ selected: { type: [String, Number], default: null },
37
42
  })
38
43
 
39
44
  const emit = defineEmits(['item-click'])
@@ -161,13 +166,22 @@ function open(item) {
161
166
  </template>
162
167
 
163
168
  <template v-else>
164
- <div class="k-gantt__label" :class="{ 'k-gantt__label--muted': row.item.muted }" :style="{ gridRow: r + 2 }">
169
+ <div
170
+ class="k-gantt__label k-gantt__label--item"
171
+ :class="{ 'k-gantt__label--muted': row.item.muted, 'k-gantt__label--selected': row.item.key === selected }"
172
+ :style="{ gridRow: r + 2 }"
173
+ role="button"
174
+ tabindex="0"
175
+ @click="open(row.item)"
176
+ @keydown.enter.prevent="open(row.item)"
177
+ @keydown.space.prevent="open(row.item)"
178
+ >
165
179
  <slot name="label" :item="row.item">
166
180
  <span v-if="row.item.code" class="k-gantt__code">{{ row.item.code }}</span>
167
181
  <span class="k-gantt__title">{{ row.item.label }}</span>
168
182
  </slot>
169
183
  </div>
170
- <div class="k-gantt__track" :style="{ gridRow: r + 2 }">
184
+ <div class="k-gantt__track" :class="{ 'k-gantt__track--selected': row.item.key === selected }" :style="{ gridRow: r + 2 }">
171
185
  <div
172
186
  v-for="(bar, b) in row.bars"
173
187
  :key="b"
@@ -288,6 +302,11 @@ function open(item) {
288
302
  color: var(--k-text);
289
303
  }
290
304
  .k-gantt__label--muted { color: var(--k-text-faint); }
305
+ .k-gantt__label--item { cursor: pointer; }
306
+ .k-gantt__label--item:hover { background: var(--k-surface-muted); }
307
+ .k-gantt__label--item:focus-visible { outline: 2px solid var(--k-primary); outline-offset: -2px; }
308
+ .k-gantt__label--selected,
309
+ .k-gantt__label--selected:hover { background: var(--k-primary-soft); box-shadow: inset 3px 0 0 var(--k-primary); }
291
310
  .k-gantt__label--group {
292
311
  padding-left: 0.5rem;
293
312
  border: none;
@@ -324,6 +343,7 @@ function open(item) {
324
343
  border-bottom: 1px solid var(--k-border);
325
344
  }
326
345
  .k-gantt__track--group { background: var(--k-surface-muted); }
346
+ .k-gantt__track--selected { background: color-mix(in srgb, var(--k-primary-soft) 55%, transparent); }
327
347
  .k-gantt__summary {
328
348
  position: absolute;
329
349
  z-index: 1;
package/src/index.js CHANGED
@@ -22,9 +22,11 @@ import KBadge from './components/KBadge.vue'
22
22
  import KButton from './components/KButton.vue'
23
23
  import KCard from './components/KCard.vue'
24
24
  import KCheckbox from './components/KCheckbox.vue'
25
+ import KChip from './components/KChip.vue'
25
26
  import KCombobox from './components/KCombobox.vue'
26
27
  import KCountdown from './components/KCountdown.vue'
27
28
  import KDescriptions from './components/KDescriptions.vue'
29
+ import KDrawer from './components/KDrawer.vue'
28
30
  import KDropdown from './components/KDropdown.vue'
29
31
  import KEmpty from './components/KEmpty.vue'
30
32
  import KFileUpload from './components/KFileUpload.vue'
@@ -53,8 +55,8 @@ import KToast from './components/KToast.vue'
53
55
  import KTooltip from './components/KTooltip.vue'
54
56
 
55
57
  export {
56
- KAlert, KAvatar, KBadge, KButton, KCard, KCheckbox, KCombobox, KCountdown,
57
- KDescriptions, KDropdown, KEmpty, KFileUpload, KFilterBar, KGantt, KIcon,
58
+ KAlert, KAvatar, KBadge, KButton, KCard, KCheckbox, KChip, KCombobox, KCountdown,
59
+ KDescriptions, KDrawer, KDropdown, KEmpty, KFileUpload, KFilterBar, KGantt, KIcon,
58
60
  KInput, KMenuItem, KModal, KMoney, KPage, KPagination, KProgress,
59
61
  KRadioGroup, KSegmented, KSelect, KShell, KStat, KSteps, KSwitch, KTable,
60
62
  KTabs, KTextarea, KTimeline, KToast, KTooltip,
@@ -69,8 +71,8 @@ export { useToast, pushToast, dismissToast, toastStore } from './composables/use
69
71
  export * from './format.js'
70
72
 
71
73
  const components = {
72
- KAlert, KAvatar, KBadge, KButton, KCard, KCheckbox, KCombobox, KCountdown,
73
- KDescriptions, KDropdown, KEmpty, KFileUpload, KFilterBar, KGantt, KIcon,
74
+ KAlert, KAvatar, KBadge, KButton, KCard, KCheckbox, KChip, KCombobox, KCountdown,
75
+ KDescriptions, KDrawer, KDropdown, KEmpty, KFileUpload, KFilterBar, KGantt, KIcon,
74
76
  KInput, KMenuItem, KModal, KMoney, KPage, KPagination, KProgress,
75
77
  KRadioGroup, KSegmented, KSelect, KShell, KStat, KSteps, KSwitch, KTable,
76
78
  KTabs, KTextarea, KTimeline, KToast, KTooltip,