@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
@@ -0,0 +1,91 @@
1
+ <script setup>
2
+ /**
3
+ * KFilterBar — панель поиска и фильтров над списком.
4
+ *
5
+ * Слоты: #search (обычно KInput), по умолчанию — сами фильтры, #actions —
6
+ * кнопки справа. Ниже показываются активные фильтры чипами со сбросом: без
7
+ * этого пользователь теряет из виду, почему список пуст.
8
+ *
9
+ * active: [{ key, label }]
10
+ */
11
+ import KIcon from './KIcon.vue'
12
+
13
+ defineProps({
14
+ active: { type: Array, default: () => [] },
15
+ })
16
+
17
+ defineEmits(['remove', 'reset'])
18
+ </script>
19
+
20
+ <template>
21
+ <div class="k-filterbar">
22
+ <div class="k-filterbar__row">
23
+ <div v-if="$slots.search" class="k-filterbar__search"><slot name="search" /></div>
24
+ <div class="k-filterbar__fields"><slot /></div>
25
+ <div v-if="$slots.actions" class="k-filterbar__actions"><slot name="actions" /></div>
26
+ </div>
27
+ <div v-if="active.length" class="k-filterbar__chips">
28
+ <span v-for="chip in active" :key="chip.key" class="k-filterbar__chip">
29
+ {{ chip.label }}
30
+ <button class="k-filterbar__chip-x" aria-label="Убрать фильтр" @click="$emit('remove', chip)">
31
+ <KIcon name="close" :size="12" />
32
+ </button>
33
+ </span>
34
+ <button class="k-filterbar__reset" @click="$emit('reset')">Сбросить всё</button>
35
+ </div>
36
+ </div>
37
+ </template>
38
+
39
+ <style scoped>
40
+ .k-filterbar {
41
+ background: var(--k-surface);
42
+ border: 1px solid var(--k-border);
43
+ border-radius: var(--k-radius);
44
+ box-shadow: var(--k-shadow-sm);
45
+ padding: 0.875rem 1rem;
46
+ }
47
+ .k-filterbar__row { display: flex; align-items: flex-end; gap: 0.75rem; flex-wrap: wrap; }
48
+ .k-filterbar__search { flex: 1 1 20rem; min-width: 14rem; }
49
+ .k-filterbar__fields { display: flex; align-items: flex-end; gap: 0.75rem; flex-wrap: wrap; }
50
+ .k-filterbar__actions { display: flex; align-items: center; gap: 0.5rem; margin-left: auto; }
51
+ .k-filterbar__chips {
52
+ display: flex;
53
+ align-items: center;
54
+ gap: 0.4rem;
55
+ flex-wrap: wrap;
56
+ margin-top: 0.75rem;
57
+ padding-top: 0.75rem;
58
+ border-top: 1px dashed var(--k-border);
59
+ }
60
+ .k-filterbar__chip {
61
+ display: inline-flex;
62
+ align-items: center;
63
+ gap: 0.3rem;
64
+ padding: 0.15rem 0.3rem 0.15rem 0.55rem;
65
+ background: var(--k-primary-soft);
66
+ color: var(--k-primary);
67
+ border-radius: 999px;
68
+ font-size: 0.75rem;
69
+ font-weight: 500;
70
+ }
71
+ .k-filterbar__chip-x {
72
+ display: inline-flex;
73
+ background: none;
74
+ border: none;
75
+ color: inherit;
76
+ opacity: 0.65;
77
+ cursor: pointer;
78
+ padding: 0.1rem;
79
+ }
80
+ .k-filterbar__chip-x:hover { opacity: 1; }
81
+ .k-filterbar__reset {
82
+ background: none;
83
+ border: none;
84
+ font: inherit;
85
+ font-size: 0.75rem;
86
+ color: var(--k-text-muted);
87
+ cursor: pointer;
88
+ text-decoration: underline;
89
+ }
90
+ .k-filterbar__reset:hover { color: var(--k-text); }
91
+ </style>
@@ -0,0 +1,398 @@
1
+ <script setup>
2
+ /**
3
+ * KGantt — диаграмма Ганта по дням: слева список задач, справа полосы сроков.
4
+ * Задачи группируются (по исполнителю, команде, этапу); у группы сводная
5
+ * полоса от самого раннего начала до самого позднего конца, группы
6
+ * сворачиваются. Даты — строки 'YYYY-MM-DD'.
7
+ *
8
+ * items: [{ key, label, start, end?, group?, code?, tone?, hint?, href?, muted?,
9
+ * segments?: [{ start, end }] }]
10
+ * groups: [{ key, label, hint? }] — порядок групп; без него задачи идут одним списком
11
+ * tone — 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'neutral'
12
+ * muted — завершённая или неактуальная задача, полоса приглушена
13
+ * segments — несколько отрезков в одной строке (работа с перерывами)
14
+ *
15
+ * <KGantt :items="tasks" :groups="people" from="2026-10-19" to="2026-11-06"
16
+ * @item-click="open">
17
+ * <template #group="{ group }"><KAvatar :name="group.label" size="sm" />{{ group.label }}</template>
18
+ * </KGantt>
19
+ *
20
+ * Слоты: #group="{ group, items, collapsed }", #label="{ item }", #bar="{ item }".
21
+ */
22
+ import { computed, ref } from 'vue'
23
+ import KIcon from './KIcon.vue'
24
+ import KTooltip from './KTooltip.vue'
25
+
26
+ const props = defineProps({
27
+ items: { type: Array, default: () => [] },
28
+ groups: { type: Array, default: () => [] },
29
+ from: { type: String, required: true },
30
+ to: { type: String, required: true },
31
+ today: { type: String, default: () => new Date(Date.now() - new Date().getTimezoneOffset() * 60000).toISOString().slice(0, 10) },
32
+ weekends: { type: Boolean, default: false },
33
+ dayWidth: { type: Number, default: 56 },
34
+ labelWidth: { type: String, default: '18rem' },
35
+ emptyText: { type: String, default: 'Нет задач в этом периоде' },
36
+ })
37
+
38
+ const emit = defineEmits(['item-click'])
39
+
40
+ const WEEKDAYS = ['вс', 'пн', 'вт', 'ср', 'чт', 'пт', 'сб']
41
+ const MONTHS = ['янв', 'фев', 'мар', 'апр', 'май', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек']
42
+
43
+ const utc = (iso) => new Date(`${iso}T00:00:00Z`)
44
+ const addDays = (iso, n) => new Date(utc(iso).getTime() + n * 86400000).toISOString().slice(0, 10)
45
+
46
+ const days = computed(() => {
47
+ const list = []
48
+ for (let iso = props.from; iso <= props.to; iso = addDays(iso, 1)) {
49
+ const wd = utc(iso).getUTCDay()
50
+ const weekend = wd === 0 || wd === 6
51
+ if (weekend && !props.weekends) continue
52
+ list.push({ iso, wd, weekend, date: utc(iso).getUTCDate(), month: utc(iso).getUTCMonth() })
53
+ }
54
+ return list.map((d, i) => ({ ...d, monthStart: i === 0 || d.month !== list[i - 1].month }))
55
+ })
56
+
57
+ const todayIndex = computed(() => days.value.findIndex(d => d.iso === props.today))
58
+
59
+ function span(start, end) {
60
+ const s = start || end
61
+ const e = end || start
62
+ if (!s || e < props.from || s > props.to) return null
63
+ const list = days.value
64
+ const first = list.findIndex(d => d.iso >= s)
65
+ let last = -1
66
+ for (let i = list.length - 1; i >= 0; i--) if (list[i].iso <= e) { last = i; break }
67
+ if (first < 0 || last < first) return null
68
+ return { first, last, cutStart: s < props.from, cutEnd: e > props.to }
69
+ }
70
+
71
+ function barsOf(item) {
72
+ const parts = item.segments?.length ? item.segments : [{ start: item.start, end: item.end }]
73
+ return parts.map(p => span(p.start, p.end)).filter(Boolean)
74
+ }
75
+
76
+ const collapsed = ref(new Set())
77
+ function toggle(key) {
78
+ const next = new Set(collapsed.value)
79
+ next.has(key) ? next.delete(key) : next.add(key)
80
+ collapsed.value = next
81
+ }
82
+
83
+ const rows = computed(() => {
84
+ const out = []
85
+ if (!props.groups.length) {
86
+ for (const item of props.items) out.push({ type: 'item', key: item.key, item, bars: barsOf(item) })
87
+ return out
88
+ }
89
+ for (const group of props.groups) {
90
+ const items = props.items.filter(i => i.group === group.key)
91
+ const bars = items.flatMap(barsOf)
92
+ const summary = bars.length
93
+ ? { first: Math.min(...bars.map(b => b.first)), last: Math.max(...bars.map(b => b.last)) }
94
+ : null
95
+ const isCollapsed = collapsed.value.has(group.key)
96
+ out.push({ type: 'group', key: `g:${group.key}`, group, items, summary, collapsed: isCollapsed })
97
+ if (!isCollapsed) for (const item of items) out.push({ type: 'item', key: `${group.key}:${item.key}`, item, bars: barsOf(item) })
98
+ }
99
+ return out
100
+ })
101
+
102
+ const gridStyle = computed(() => ({
103
+ '--k-gantt-day-w': `${props.dayWidth}px`,
104
+ '--k-gantt-label-w': props.labelWidth,
105
+ gridTemplateColumns: `var(--k-gantt-label-w) repeat(${days.value.length}, var(--k-gantt-day-w))`,
106
+ }))
107
+
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
+ })
112
+
113
+ const dayLines = computed(() => {
114
+ const marks = []
115
+ days.value.forEach((d, i) => {
116
+ if (d.weekend) marks.push({ key: `w${i}`, kind: 'weekend', i })
117
+ if (i > 0 && d.wd === 1 && !props.weekends) marks.push({ key: `m${i}`, kind: 'week', i })
118
+ })
119
+ return marks
120
+ })
121
+
122
+ function open(item) {
123
+ emit('item-click', item)
124
+ }
125
+ </script>
126
+
127
+ <template>
128
+ <div class="k-gantt">
129
+ <div class="k-gantt__scroll">
130
+ <div class="k-gantt__grid" :style="gridStyle">
131
+ <div class="k-gantt__corner"><slot name="corner" /></div>
132
+ <div
133
+ v-for="(d, i) in days"
134
+ :key="d.iso"
135
+ class="k-gantt__day"
136
+ :class="{ 'k-gantt__day--today': i === todayIndex, 'k-gantt__day--weekend': d.weekend, 'k-gantt__day--week': i > 0 && d.wd === 1 && !weekends }"
137
+ >
138
+ <span class="k-gantt__month">{{ d.monthStart ? MONTHS[d.month] : '' }}</span>
139
+ <span class="k-gantt__date">{{ d.date }}</span>
140
+ <span class="k-gantt__wd">{{ WEEKDAYS[d.wd] }}</span>
141
+ </div>
142
+
143
+ <div v-if="!rows.length" class="k-gantt__empty">{{ emptyText }}</div>
144
+
145
+ <template v-for="(row, r) in rows" :key="row.key">
146
+ <template v-if="row.type === 'group'">
147
+ <button type="button" class="k-gantt__label k-gantt__label--group" :style="{ gridRow: r + 2 }" @click="toggle(row.group.key)">
148
+ <KIcon :name="row.collapsed ? 'chevron-right' : 'chevron-down'" :size="14" class="k-gantt__chevron" />
149
+ <span class="k-gantt__group-name">
150
+ <slot name="group" :group="row.group" :items="row.items" :collapsed="row.collapsed">{{ row.group.label }}</slot>
151
+ </span>
152
+ <span class="k-gantt__count">{{ row.items.length }}</span>
153
+ </button>
154
+ <div class="k-gantt__track k-gantt__track--group" :style="{ gridRow: r + 2 }">
155
+ <div v-if="row.summary" class="k-gantt__summary" :style="barStyle(row.summary)" />
156
+ </div>
157
+ </template>
158
+
159
+ <template v-else>
160
+ <div class="k-gantt__label" :class="{ 'k-gantt__label--muted': row.item.muted }" :style="{ gridRow: r + 2 }">
161
+ <slot name="label" :item="row.item">
162
+ <span v-if="row.item.code" class="k-gantt__code">{{ row.item.code }}</span>
163
+ <span class="k-gantt__title">{{ row.item.label }}</span>
164
+ </slot>
165
+ </div>
166
+ <div class="k-gantt__track" :style="{ gridRow: r + 2 }">
167
+ <div
168
+ v-for="(bar, b) in row.bars"
169
+ :key="b"
170
+ class="k-gantt__bar-slot"
171
+ :style="barStyle(bar)"
172
+ >
173
+ <KTooltip :disabled="!row.item.hint" placement="top">
174
+ <component
175
+ :is="row.item.href ? 'a' : 'button'"
176
+ :href="row.item.href || undefined"
177
+ :target="row.item.href ? '_blank' : undefined"
178
+ :rel="row.item.href ? 'noopener' : undefined"
179
+ :type="row.item.href ? undefined : 'button'"
180
+ class="k-gantt__bar"
181
+ :class="[
182
+ `k-gantt__bar--${row.item.tone || 'primary'}`,
183
+ { 'k-gantt__bar--muted': row.item.muted, 'k-gantt__bar--cut-start': bar.cutStart, 'k-gantt__bar--cut-end': bar.cutEnd },
184
+ ]"
185
+ @click="open(row.item)"
186
+ >
187
+ <slot v-if="b === 0" name="bar" :item="row.item">{{ row.item.code || row.item.label }}</slot>
188
+ </component>
189
+ <template #content><span class="k-gantt__hint">{{ row.item.hint }}</span></template>
190
+ </KTooltip>
191
+ </div>
192
+ </div>
193
+ </template>
194
+ </template>
195
+
196
+ <div
197
+ v-for="m in dayLines"
198
+ :key="m.key"
199
+ class="k-gantt__mark"
200
+ :class="`k-gantt__mark--${m.kind}`"
201
+ :style="{ gridColumn: m.i + 2, gridRow: `2 / ${rows.length + 2}` }"
202
+ />
203
+ <div
204
+ v-if="todayIndex >= 0 && rows.length"
205
+ class="k-gantt__today"
206
+ :style="{ gridColumn: todayIndex + 2, gridRow: `2 / ${rows.length + 2}` }"
207
+ />
208
+ </div>
209
+ </div>
210
+ </div>
211
+ </template>
212
+
213
+ <style scoped>
214
+ .k-gantt {
215
+ border: 1px solid var(--k-border);
216
+ border-radius: var(--k-radius);
217
+ background: var(--k-surface);
218
+ overflow: hidden;
219
+ }
220
+ .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%; }
222
+
223
+ .k-gantt__corner,
224
+ .k-gantt__day {
225
+ position: sticky;
226
+ top: 0;
227
+ z-index: 3;
228
+ background: var(--k-surface-muted);
229
+ border-bottom: 1px solid var(--k-border);
230
+ }
231
+ .k-gantt__corner {
232
+ grid-row: 1;
233
+ grid-column: 1;
234
+ left: 0;
235
+ z-index: 4;
236
+ display: flex;
237
+ align-items: flex-end;
238
+ padding: 0.5rem 0.75rem;
239
+ border-right: 1px solid var(--k-border);
240
+ font-size: 0.6875rem;
241
+ font-weight: 600;
242
+ letter-spacing: 0.05em;
243
+ text-transform: uppercase;
244
+ color: var(--k-text-muted);
245
+ }
246
+ .k-gantt__day {
247
+ grid-row: 1;
248
+ display: grid;
249
+ grid-template-rows: 0.9rem auto auto;
250
+ justify-items: center;
251
+ padding: 0.35rem 0 0.4rem;
252
+ font-variant-numeric: tabular-nums;
253
+ }
254
+ .k-gantt__day--week { box-shadow: inset 1px 0 0 var(--k-border); }
255
+ .k-gantt__month { font-size: 0.625rem; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; color: var(--k-primary); }
256
+ .k-gantt__date {
257
+ min-width: 1.75rem;
258
+ padding: 0.05rem 0.35rem;
259
+ border-radius: 999px;
260
+ font-size: 0.875rem;
261
+ font-weight: 600;
262
+ text-align: center;
263
+ color: var(--k-text);
264
+ }
265
+ .k-gantt__wd { font-size: 0.6875rem; color: var(--k-text-faint); }
266
+ .k-gantt__day--weekend .k-gantt__date { color: var(--k-text-faint); }
267
+ .k-gantt__day--today .k-gantt__date { background: var(--k-primary); color: var(--k-on-primary); }
268
+ .k-gantt__day--today .k-gantt__wd { color: var(--k-primary); font-weight: 600; }
269
+
270
+ .k-gantt__label {
271
+ grid-column: 1;
272
+ position: sticky;
273
+ left: 0;
274
+ z-index: 2;
275
+ display: flex;
276
+ align-items: center;
277
+ gap: 0.5rem;
278
+ min-width: 0;
279
+ padding: 0.35rem 0.75rem 0.35rem 2rem;
280
+ background: var(--k-surface);
281
+ border-right: 1px solid var(--k-border);
282
+ border-bottom: 1px solid var(--k-border);
283
+ font-size: 0.8125rem;
284
+ color: var(--k-text);
285
+ }
286
+ .k-gantt__label--muted { color: var(--k-text-faint); }
287
+ .k-gantt__label--group {
288
+ padding-left: 0.5rem;
289
+ border: none;
290
+ border-right: 1px solid var(--k-border);
291
+ border-bottom: 1px solid var(--k-border);
292
+ background: var(--k-surface-muted);
293
+ font: inherit;
294
+ font-weight: 600;
295
+ font-size: 0.875rem;
296
+ text-align: left;
297
+ cursor: pointer;
298
+ }
299
+ .k-gantt__label--group:focus-visible { outline: 2px solid var(--k-primary); outline-offset: -2px; }
300
+ .k-gantt__chevron { color: var(--k-text-muted); flex: none; }
301
+ .k-gantt__group-name { display: inline-flex; align-items: center; gap: 0.5rem; min-width: 0; flex: 1; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
302
+ .k-gantt__count {
303
+ flex: none;
304
+ min-width: 1.4rem;
305
+ padding: 0 0.4rem;
306
+ border-radius: 999px;
307
+ background: var(--k-surface);
308
+ color: var(--k-text-muted);
309
+ font-size: 0.6875rem;
310
+ font-weight: 600;
311
+ text-align: center;
312
+ line-height: 1.4rem;
313
+ }
314
+ .k-gantt__code { flex: none; font-variant-numeric: tabular-nums; font-weight: 600; color: var(--k-text-muted); }
315
+ .k-gantt__title { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
316
+
317
+ .k-gantt__track {
318
+ grid-column: 2 / -1;
319
+ position: relative;
320
+ border-bottom: 1px solid var(--k-border);
321
+ }
322
+ .k-gantt__track--group { background: var(--k-surface-muted); }
323
+ .k-gantt__summary {
324
+ position: absolute;
325
+ z-index: 1;
326
+ top: 50%;
327
+ height: 0.5rem;
328
+ margin-top: -0.25rem;
329
+ border-radius: 2px;
330
+ background: var(--k-text-muted);
331
+ opacity: 0.55;
332
+ }
333
+ .k-gantt__summary::before,
334
+ .k-gantt__summary::after {
335
+ content: '';
336
+ position: absolute;
337
+ bottom: -0.3rem;
338
+ border: 0.3rem solid transparent;
339
+ border-top-color: var(--k-text-muted);
340
+ }
341
+ .k-gantt__summary::before { left: 0; border-left-color: var(--k-text-muted); }
342
+ .k-gantt__summary::after { right: 0; border-right-color: var(--k-text-muted); }
343
+
344
+ .k-gantt__bar-slot { position: absolute; top: 0.3rem; bottom: 0.3rem; z-index: 1; display: flex; }
345
+ .k-gantt__bar-slot :deep(.k-tooltip) { display: flex; flex: 1; min-width: 0; }
346
+ .k-gantt__bar {
347
+ flex: 1;
348
+ min-width: 0;
349
+ display: flex;
350
+ align-items: center;
351
+ padding: 0 0.5rem;
352
+ border: none;
353
+ border-left: 3px solid var(--k-gantt-accent);
354
+ border-radius: var(--k-radius-sm);
355
+ background: var(--k-gantt-fill);
356
+ color: var(--k-gantt-ink);
357
+ font: inherit;
358
+ font-size: 0.75rem;
359
+ font-weight: 600;
360
+ font-variant-numeric: tabular-nums;
361
+ white-space: nowrap;
362
+ overflow: hidden;
363
+ text-overflow: ellipsis;
364
+ text-decoration: none;
365
+ cursor: pointer;
366
+ transition: box-shadow 0.15s, transform 0.15s;
367
+ }
368
+ .k-gantt__bar:hover { box-shadow: var(--k-shadow-sm); transform: translateY(-1px); }
369
+ .k-gantt__bar:focus-visible { outline: 2px solid var(--k-primary); outline-offset: 1px; }
370
+ .k-gantt__bar--cut-start { border-top-left-radius: 0; border-bottom-left-radius: 0; border-left-style: dashed; }
371
+ .k-gantt__bar--cut-end { border-top-right-radius: 0; border-bottom-right-radius: 0; }
372
+ .k-gantt__bar--muted { opacity: 0.5; text-decoration: line-through; }
373
+ .k-gantt__bar--primary { --k-gantt-accent: var(--k-primary); --k-gantt-fill: var(--k-primary-soft); --k-gantt-ink: var(--k-primary); }
374
+ .k-gantt__bar--success { --k-gantt-accent: var(--k-success); --k-gantt-fill: var(--k-success-soft); --k-gantt-ink: var(--k-success-strong); }
375
+ .k-gantt__bar--warning { --k-gantt-accent: var(--k-warning); --k-gantt-fill: var(--k-warning-soft); --k-gantt-ink: var(--k-warning-strong); }
376
+ .k-gantt__bar--danger { --k-gantt-accent: var(--k-danger); --k-gantt-fill: var(--k-danger-soft); --k-gantt-ink: var(--k-danger-strong); }
377
+ .k-gantt__bar--info { --k-gantt-accent: var(--k-info); --k-gantt-fill: var(--k-info-soft); --k-gantt-ink: var(--k-info-strong); }
378
+ .k-gantt__bar--neutral { --k-gantt-accent: var(--k-border-strong); --k-gantt-fill: var(--k-surface-muted); --k-gantt-ink: var(--k-text-muted); }
379
+ .k-gantt__hint { white-space: pre-line; }
380
+
381
+ .k-gantt__mark { pointer-events: none; z-index: 0; }
382
+ .k-gantt__mark--weekend { background: var(--k-surface-muted); }
383
+ .k-gantt__mark--week { box-shadow: inset 1px 0 0 var(--k-border); }
384
+ .k-gantt__today {
385
+ pointer-events: none;
386
+ z-index: 0;
387
+ background: var(--k-primary-soft);
388
+ opacity: 0.6;
389
+ box-shadow: inset 2px 0 0 var(--k-primary);
390
+ }
391
+
392
+ .k-gantt__empty {
393
+ grid-column: 1 / -1;
394
+ padding: 2rem;
395
+ text-align: center;
396
+ color: var(--k-text-muted);
397
+ }
398
+ </style>
@@ -0,0 +1,53 @@
1
+ <script setup>
2
+ /**
3
+ * KIcon — единственная точка входа для иконок кита.
4
+ *
5
+ * Оборачивает Lucide (https://lucide.dev, ISC). Компоненты и приложения
6
+ * ссылаются на иконку по *семантическому* имени ("delete", "settings"), а не
7
+ * по форме, поэтому набор можно заменить в одном месте — см. icon-registry.js.
8
+ *
9
+ * <KIcon name="settings" :size="16" />
10
+ *
11
+ * Иконку, которой нет в реестре, можно передать напрямую компонентом —
12
+ * так приложение расширяет набор, не форкая кит:
13
+ *
14
+ * <KIcon :component="MyBrandIcon" />
15
+ */
16
+ import { computed } from 'vue'
17
+ import { iconRegistry } from './icon-registry.js'
18
+
19
+ const props = defineProps({
20
+ name: { type: String, default: '' },
21
+ component: { type: [Object, Function], default: null },
22
+ size: { type: [Number, String], default: 18 },
23
+ strokeWidth: { type: [Number, String], default: 2 },
24
+ spin: { type: Boolean, default: false },
25
+ })
26
+
27
+ const resolved = computed(() => {
28
+ if (props.component) return props.component
29
+ const c = iconRegistry[props.name]
30
+ if (!c && props.name && import.meta.env?.DEV) {
31
+ console.warn(`[KIcon] неизвестная иконка "${props.name}" — добавьте её в icon-registry.js или передайте :component`)
32
+ }
33
+ return c || null
34
+ })
35
+ </script>
36
+
37
+ <template>
38
+ <component
39
+ :is="resolved"
40
+ v-if="resolved"
41
+ class="k-icon"
42
+ :class="{ 'k-icon--spin': spin }"
43
+ :size="Number(size)"
44
+ :stroke-width="Number(strokeWidth)"
45
+ aria-hidden="true"
46
+ />
47
+ </template>
48
+
49
+ <style scoped>
50
+ .k-icon { display: inline-block; vertical-align: -0.15em; flex-shrink: 0; }
51
+ .k-icon--spin { animation: k-icon-spin 1s linear infinite; }
52
+ @keyframes k-icon-spin { to { transform: rotate(360deg); } }
53
+ </style>
@@ -0,0 +1,108 @@
1
+ <script setup>
2
+ /**
3
+ * KInput — однострочное поле ввода с подписью, подсказкой и ошибкой.
4
+ *
5
+ * Метрики совпадают с KSelect и KButton, поэтому строка фильтров
6
+ * выравнивается без правок в приложении. Нативные атрибуты (autocomplete,
7
+ * inputmode, min, @blur, …) прокидываются на <input>.
8
+ *
9
+ * <KInput v-model="query" label="Поиск" prefix-icon="search" :error="err" />
10
+ */
11
+ import { computed, useAttrs } from 'vue'
12
+ import KIcon from './KIcon.vue'
13
+
14
+ defineOptions({ inheritAttrs: false })
15
+
16
+ /*
17
+ * Атрибуты разделяются: class и style остаются на корне (иначе layout-классы
18
+ * из родителя не могут задать ширину поля в строке фильтров), остальное —
19
+ * autocomplete, inputmode, min, обработчики — уходит на сам контрол.
20
+ */
21
+ const attrs = useAttrs()
22
+ const rootAttrs = computed(() => ({ class: attrs.class, style: attrs.style }))
23
+ const controlAttrs = computed(() => {
24
+ const { class: _c, style: _s, ...rest } = attrs
25
+ return rest
26
+ })
27
+
28
+ defineProps({
29
+ modelValue: { type: [String, Number], default: '' },
30
+ label: { type: String, default: '' },
31
+ placeholder: { type: String, default: '' },
32
+ type: { type: String, default: 'text' },
33
+ hint: { type: String, default: '' },
34
+ error: { type: String, default: '' },
35
+ prefixIcon: { type: String, default: '' },
36
+ suffix: { type: String, default: '' },
37
+ required: { type: Boolean, default: false },
38
+ disabled: { type: Boolean, default: false },
39
+ readonly: { type: Boolean, default: false },
40
+ })
41
+
42
+ defineEmits(['update:modelValue'])
43
+ </script>
44
+
45
+ <template>
46
+ <label class="k-input" v-bind="rootAttrs">
47
+ <span v-if="label" class="k-input__label">
48
+ {{ label }}<span v-if="required" class="k-input__req">*</span>
49
+ </span>
50
+ <span
51
+ class="k-input__box"
52
+ :class="{ 'k-input__box--error': error, 'k-input__box--disabled': disabled }"
53
+ >
54
+ <KIcon v-if="prefixIcon" :name="prefixIcon" :size="16" class="k-input__icon" />
55
+ <input
56
+ class="k-input__field"
57
+ :type="type"
58
+ :value="modelValue"
59
+ :placeholder="placeholder"
60
+ :required="required"
61
+ :disabled="disabled"
62
+ :readonly="readonly"
63
+ :aria-invalid="error ? 'true' : null"
64
+ v-bind="controlAttrs"
65
+ @input="$emit('update:modelValue', $event.target.value)"
66
+ />
67
+ <span v-if="suffix" class="k-input__suffix">{{ suffix }}</span>
68
+ </span>
69
+ <span v-if="error" class="k-input__error">{{ error }}</span>
70
+ <span v-else-if="hint" class="k-input__hint">{{ hint }}</span>
71
+ </label>
72
+ </template>
73
+
74
+ <style scoped>
75
+ .k-input { display: flex; flex-direction: column; gap: 0.25rem; margin: 0; min-width: 0; }
76
+ .k-input__label { font-size: 0.8125rem; color: var(--k-text-muted); }
77
+ .k-input__req { color: var(--k-danger); margin-left: 2px; }
78
+ .k-input__box {
79
+ display: flex;
80
+ align-items: center;
81
+ gap: 0.5rem;
82
+ height: var(--k-control-h);
83
+ padding: 0 0.625rem;
84
+ background: var(--k-surface);
85
+ border: 1px solid var(--k-border-strong);
86
+ border-radius: var(--k-radius-sm);
87
+ transition: border-color 0.15s, box-shadow 0.15s;
88
+ }
89
+ .k-input__box:focus-within { border-color: var(--k-primary); box-shadow: 0 0 0 3px var(--k-primary-soft); }
90
+ .k-input__box--error { border-color: var(--k-danger); }
91
+ .k-input__box--error:focus-within { box-shadow: 0 0 0 3px var(--k-danger-soft); }
92
+ .k-input__box--disabled { background: var(--k-surface-muted); }
93
+ .k-input__icon { color: var(--k-text-faint); }
94
+ .k-input__field {
95
+ flex: 1;
96
+ min-width: 0;
97
+ border: none;
98
+ background: none;
99
+ outline: none;
100
+ font: inherit;
101
+ font-size: 0.9375rem;
102
+ color: inherit;
103
+ }
104
+ .k-input__field:disabled { cursor: not-allowed; color: var(--k-text-muted); }
105
+ .k-input__suffix { font-size: 0.875rem; color: var(--k-text-muted); white-space: nowrap; }
106
+ .k-input__hint { font-size: 0.75rem; color: var(--k-text-muted); }
107
+ .k-input__error { font-size: 0.75rem; color: var(--k-danger); }
108
+ </style>