@krometra/ui-kit 0.4.0 → 0.5.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.0",
3
+ "version": "0.5.0",
4
4
  "description": "UI-kit на Vue 3 со сменными темами, светлой и тёмной схемой; интерактивные компоненты на zag.js",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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>
@@ -11,6 +11,11 @@
11
11
  * tone — 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'neutral'
12
12
  * muted — завершённая или неактуальная задача, полоса приглушена
13
13
  * segments — несколько отрезков в одной строке (работа с перерывами)
14
+ * dayWidth — минимальная ширина дня в px; свободное место колонки дней делят поровну
15
+ * selected — key выбранной задачи, её строка подсвечивается
16
+ *
17
+ * Клик по полосе или по названию задачи — событие item-click (у полосы с href
18
+ * открывается ссылка).
14
19
  *
15
20
  * <KGantt :items="tasks" :groups="people" from="2026-10-19" to="2026-11-06"
16
21
  * @item-click="open">
@@ -33,6 +38,7 @@ const props = defineProps({
33
38
  dayWidth: { type: Number, default: 56 },
34
39
  labelWidth: { type: String, default: '18rem' },
35
40
  emptyText: { type: String, default: 'Нет задач в этом периоде' },
41
+ selected: { type: [String, Number], default: null },
36
42
  })
37
43
 
38
44
  const emit = defineEmits(['item-click'])
@@ -102,13 +108,16 @@ const rows = computed(() => {
102
108
  const gridStyle = computed(() => ({
103
109
  '--k-gantt-day-w': `${props.dayWidth}px`,
104
110
  '--k-gantt-label-w': props.labelWidth,
105
- gridTemplateColumns: `var(--k-gantt-label-w) repeat(${days.value.length}, var(--k-gantt-day-w))`,
111
+ gridTemplateColumns: `var(--k-gantt-label-w) repeat(${days.value.length}, minmax(var(--k-gantt-day-w), 1fr))`,
106
112
  }))
107
113
 
108
- const barStyle = (bar) => ({
109
- left: `calc(var(--k-gantt-day-w) * ${bar.first} + 3px)`,
110
- width: `calc(var(--k-gantt-day-w) * ${bar.last - bar.first + 1} - 6px)`,
111
- })
114
+ const barStyle = (bar) => {
115
+ const n = days.value.length
116
+ return {
117
+ left: `calc(${(bar.first / n) * 100}% + 3px)`,
118
+ width: `calc(${((bar.last - bar.first + 1) / n) * 100}% - 6px)`,
119
+ }
120
+ }
112
121
 
113
122
  const dayLines = computed(() => {
114
123
  const marks = []
@@ -157,13 +166,22 @@ function open(item) {
157
166
  </template>
158
167
 
159
168
  <template v-else>
160
- <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
+ >
161
179
  <slot name="label" :item="row.item">
162
180
  <span v-if="row.item.code" class="k-gantt__code">{{ row.item.code }}</span>
163
181
  <span class="k-gantt__title">{{ row.item.label }}</span>
164
182
  </slot>
165
183
  </div>
166
- <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 }">
167
185
  <div
168
186
  v-for="(bar, b) in row.bars"
169
187
  :key="b"
@@ -218,7 +236,7 @@ function open(item) {
218
236
  overflow: hidden;
219
237
  }
220
238
  .k-gantt__scroll { overflow: auto; max-height: inherit; }
221
- .k-gantt__grid { display: grid; grid-auto-rows: minmax(2.25rem, auto); position: relative; width: max-content; min-width: 100%; }
239
+ .k-gantt__grid { display: grid; grid-auto-rows: minmax(2.25rem, auto); position: relative; min-width: max-content; }
222
240
 
223
241
  .k-gantt__corner,
224
242
  .k-gantt__day {
@@ -284,6 +302,11 @@ function open(item) {
284
302
  color: var(--k-text);
285
303
  }
286
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); }
287
310
  .k-gantt__label--group {
288
311
  padding-left: 0.5rem;
289
312
  border: none;
@@ -320,6 +343,7 @@ function open(item) {
320
343
  border-bottom: 1px solid var(--k-border);
321
344
  }
322
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); }
323
347
  .k-gantt__summary {
324
348
  position: absolute;
325
349
  z-index: 1;
package/src/index.js CHANGED
@@ -25,6 +25,7 @@ import KCheckbox from './components/KCheckbox.vue'
25
25
  import KCombobox from './components/KCombobox.vue'
26
26
  import KCountdown from './components/KCountdown.vue'
27
27
  import KDescriptions from './components/KDescriptions.vue'
28
+ import KDrawer from './components/KDrawer.vue'
28
29
  import KDropdown from './components/KDropdown.vue'
29
30
  import KEmpty from './components/KEmpty.vue'
30
31
  import KFileUpload from './components/KFileUpload.vue'
@@ -54,7 +55,7 @@ import KTooltip from './components/KTooltip.vue'
54
55
 
55
56
  export {
56
57
  KAlert, KAvatar, KBadge, KButton, KCard, KCheckbox, KCombobox, KCountdown,
57
- KDescriptions, KDropdown, KEmpty, KFileUpload, KFilterBar, KGantt, KIcon,
58
+ KDescriptions, KDrawer, KDropdown, KEmpty, KFileUpload, KFilterBar, KGantt, KIcon,
58
59
  KInput, KMenuItem, KModal, KMoney, KPage, KPagination, KProgress,
59
60
  KRadioGroup, KSegmented, KSelect, KShell, KStat, KSteps, KSwitch, KTable,
60
61
  KTabs, KTextarea, KTimeline, KToast, KTooltip,
@@ -70,7 +71,7 @@ export * from './format.js'
70
71
 
71
72
  const components = {
72
73
  KAlert, KAvatar, KBadge, KButton, KCard, KCheckbox, KCombobox, KCountdown,
73
- KDescriptions, KDropdown, KEmpty, KFileUpload, KFilterBar, KGantt, KIcon,
74
+ KDescriptions, KDrawer, KDropdown, KEmpty, KFileUpload, KFilterBar, KGantt, KIcon,
74
75
  KInput, KMenuItem, KModal, KMoney, KPage, KPagination, KProgress,
75
76
  KRadioGroup, KSegmented, KSelect, KShell, KStat, KSteps, KSwitch, KTable,
76
77
  KTabs, KTextarea, KTimeline, KToast, KTooltip,