@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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +228 -2
  3. package/dist/ui-kit.css +2 -0
  4. package/dist/ui-kit.js +14955 -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/KIcon.vue +53 -0
  22. package/src/components/KInput.vue +108 -0
  23. package/src/components/KMenuItem.vue +56 -0
  24. package/src/components/KModal.vue +136 -0
  25. package/src/components/KMoney.vue +44 -0
  26. package/src/components/KPage.vue +93 -0
  27. package/src/components/KPagination.vue +113 -0
  28. package/src/components/KProgress.vue +63 -0
  29. package/src/components/KRadioGroup.vue +103 -0
  30. package/src/components/KSegmented.vue +88 -0
  31. package/src/components/KSelect.vue +231 -0
  32. package/src/components/KShell.vue +176 -0
  33. package/src/components/KStat.vue +74 -0
  34. package/src/components/KSteps.vue +119 -0
  35. package/src/components/KSwitch.vue +82 -0
  36. package/src/components/KTable.vue +161 -0
  37. package/src/components/KTabs.vue +102 -0
  38. package/src/components/KTextarea.vue +79 -0
  39. package/src/components/KTimeline.vue +72 -0
  40. package/src/components/KToast.vue +34 -0
  41. package/src/components/KToastItem.vue +86 -0
  42. package/src/components/KTooltip.vue +67 -0
  43. package/src/components/icon-registry.js +143 -0
  44. package/src/composables/useToast.js +52 -0
  45. package/src/format.js +137 -0
  46. package/src/index.js +84 -0
  47. package/src/theme.js +417 -0
  48. package/src/tokens.css +109 -0
@@ -0,0 +1,102 @@
1
+ <script setup>
2
+ /**
3
+ * KTabs — горизонтальные вкладки на @zag-js/tabs: навигация стрелками,
4
+ * roving tabindex и ARIA от машины.
5
+ *
6
+ * Только заголовки: содержимое рисует приложение по v-model. Так вкладки можно
7
+ * привязать к маршруту, не дублируя контент внутри компонента.
8
+ *
9
+ * items: [{ value, label, hint?, icon?, count?, disabled? }]
10
+ *
11
+ * hint — пояснение второй строкой под названием вкладки.
12
+ */
13
+ import * as tabs from '@zag-js/tabs'
14
+ import { normalizeProps, useMachine } from '@zag-js/vue'
15
+ import { computed, useId } from 'vue'
16
+ import KIcon from './KIcon.vue'
17
+
18
+ const props = defineProps({
19
+ modelValue: { type: [String, Number], default: '' },
20
+ items: { type: Array, default: () => [] },
21
+ variant: { type: String, default: 'line' }, // line | pill
22
+ })
23
+
24
+ const emit = defineEmits(['update:modelValue'])
25
+
26
+ const service = useMachine(tabs.machine, {
27
+ id: useId(),
28
+ get value() {
29
+ return props.modelValue === '' || props.modelValue == null ? null : String(props.modelValue)
30
+ },
31
+ onValueChange({ value }) {
32
+ const item = props.items.find((i) => String(i.value) === value)
33
+ emit('update:modelValue', item ? item.value : value)
34
+ },
35
+ })
36
+
37
+ const api = computed(() => tabs.connect(service, normalizeProps))
38
+ </script>
39
+
40
+ <template>
41
+ <div class="k-tabs" :class="`k-tabs--${variant}`" v-bind="api.getRootProps()">
42
+ <div class="k-tabs__list" v-bind="api.getListProps()">
43
+ <button
44
+ v-for="item in items"
45
+ :key="item.value"
46
+ class="k-tabs__tab"
47
+ :class="{ 'k-tabs__tab--hinted': item.hint }"
48
+ v-bind="api.getTriggerProps({ value: String(item.value), disabled: item.disabled })"
49
+ >
50
+ <span class="k-tabs__label">
51
+ <KIcon v-if="item.icon" :name="item.icon" :size="15" />
52
+ {{ item.label }}
53
+ <span v-if="item.count != null" class="k-tabs__count">{{ item.count }}</span>
54
+ </span>
55
+ <span v-if="item.hint" class="k-tabs__hint">{{ item.hint }}</span>
56
+ </button>
57
+ </div>
58
+ </div>
59
+ </template>
60
+
61
+ <style scoped>
62
+ .k-tabs { overflow-x: auto; }
63
+ .k-tabs__list { display: flex; align-items: stretch; gap: 0.25rem; }
64
+ .k-tabs--line .k-tabs__list { border-bottom: 1px solid var(--k-border); }
65
+ .k-tabs__tab {
66
+ display: inline-flex;
67
+ align-items: center;
68
+ gap: 0.4rem;
69
+ padding: 0.6rem 0.75rem;
70
+ background: none;
71
+ border: none;
72
+ font: inherit;
73
+ font-size: 0.9375rem;
74
+ font-weight: 500;
75
+ color: var(--k-text-muted);
76
+ cursor: pointer;
77
+ white-space: nowrap;
78
+ transition: color 0.15s, background 0.15s, border-color 0.15s;
79
+ }
80
+ .k-tabs__tab:focus { outline: none; }
81
+ .k-tabs__tab:focus-visible { outline: 2px solid var(--k-primary); outline-offset: -2px; border-radius: var(--k-radius-sm); }
82
+ .k-tabs__tab[data-disabled] { opacity: 0.5; cursor: not-allowed; }
83
+ .k-tabs--line .k-tabs__tab { border-bottom: 2px solid transparent; margin-bottom: -1px; }
84
+ .k-tabs--line .k-tabs__tab:hover:not([data-disabled]) { color: var(--k-text); }
85
+ .k-tabs--line .k-tabs__tab[data-selected] { color: var(--k-primary); border-bottom-color: var(--k-primary); }
86
+ .k-tabs--pill .k-tabs__tab { border-radius: 999px; padding: 0.4rem 0.85rem; }
87
+ .k-tabs--pill .k-tabs__tab:hover:not([data-disabled]) { background: var(--k-surface-muted); }
88
+ .k-tabs--pill .k-tabs__tab[data-selected] { background: var(--k-primary-soft); color: var(--k-primary); }
89
+ .k-tabs__label { display: inline-flex; align-items: center; gap: 0.4rem; }
90
+ .k-tabs__tab--hinted { flex-direction: column; align-items: flex-start; justify-content: center; gap: 0.1rem; }
91
+ .k-tabs__hint { font-size: 0.75rem; font-weight: 400; color: var(--k-text-muted); }
92
+ .k-tabs--pill .k-tabs__tab--hinted { border-radius: var(--k-radius-lg); }
93
+ .k-tabs__count {
94
+ padding: 0.05rem 0.35rem;
95
+ border-radius: 999px;
96
+ background: var(--k-surface-muted);
97
+ font-size: 0.6875rem;
98
+ font-weight: 600;
99
+ color: var(--k-text-muted);
100
+ }
101
+ .k-tabs__tab[data-selected] .k-tabs__count { background: var(--k-primary-soft); color: var(--k-primary); }
102
+ </style>
@@ -0,0 +1,79 @@
1
+ <script setup>
2
+ /**
3
+ * KTextarea — многострочное поле. Подпись, подсказка и ошибка ведут себя так
4
+ * же, как у KInput, чтобы формы выглядели однородно.
5
+ */
6
+ import { computed, useAttrs } from 'vue'
7
+
8
+ defineOptions({ inheritAttrs: false })
9
+
10
+ /*
11
+ * Атрибуты разделяются: class и style остаются на корне (иначе layout-классы
12
+ * из родителя не могут задать ширину поля в строке фильтров), остальное —
13
+ * autocomplete, inputmode, min, обработчики — уходит на сам контрол.
14
+ */
15
+ const attrs = useAttrs()
16
+ const rootAttrs = computed(() => ({ class: attrs.class, style: attrs.style }))
17
+ const controlAttrs = computed(() => {
18
+ const { class: _c, style: _s, ...rest } = attrs
19
+ return rest
20
+ })
21
+
22
+ defineProps({
23
+ modelValue: { type: String, default: '' },
24
+ label: { type: String, default: '' },
25
+ placeholder: { type: String, default: '' },
26
+ rows: { type: [Number, String], default: 4 },
27
+ hint: { type: String, default: '' },
28
+ error: { type: String, default: '' },
29
+ required: { type: Boolean, default: false },
30
+ disabled: { type: Boolean, default: false },
31
+ })
32
+
33
+ defineEmits(['update:modelValue'])
34
+ </script>
35
+
36
+ <template>
37
+ <label class="k-textarea" v-bind="rootAttrs">
38
+ <span v-if="label" class="k-textarea__label">
39
+ {{ label }}<span v-if="required" class="k-textarea__req">*</span>
40
+ </span>
41
+ <textarea
42
+ class="k-textarea__field"
43
+ :class="{ 'k-textarea__field--error': error }"
44
+ :value="modelValue"
45
+ :rows="rows"
46
+ :placeholder="placeholder"
47
+ :required="required"
48
+ :disabled="disabled"
49
+ v-bind="controlAttrs"
50
+ @input="$emit('update:modelValue', $event.target.value)"
51
+ />
52
+ <span v-if="error" class="k-textarea__error">{{ error }}</span>
53
+ <span v-else-if="hint" class="k-textarea__hint">{{ hint }}</span>
54
+ </label>
55
+ </template>
56
+
57
+ <style scoped>
58
+ .k-textarea { display: flex; flex-direction: column; gap: 0.25rem; margin: 0; min-width: 0; }
59
+ .k-textarea__label { font-size: 0.8125rem; color: var(--k-text-muted); }
60
+ .k-textarea__req { color: var(--k-danger); margin-left: 2px; }
61
+ .k-textarea__field {
62
+ width: 100%;
63
+ padding: 0.5rem 0.625rem;
64
+ background: var(--k-surface);
65
+ border: 1px solid var(--k-border-strong);
66
+ border-radius: var(--k-radius-sm);
67
+ font: inherit;
68
+ font-size: 0.9375rem;
69
+ color: inherit;
70
+ outline: none;
71
+ resize: vertical;
72
+ transition: border-color 0.15s, box-shadow 0.15s;
73
+ }
74
+ .k-textarea__field:focus { border-color: var(--k-primary); box-shadow: 0 0 0 3px var(--k-primary-soft); }
75
+ .k-textarea__field--error { border-color: var(--k-danger); }
76
+ .k-textarea__field:disabled { background: var(--k-surface-muted); cursor: not-allowed; }
77
+ .k-textarea__hint { font-size: 0.75rem; color: var(--k-text-muted); }
78
+ .k-textarea__error { font-size: 0.75rem; color: var(--k-danger); }
79
+ </style>
@@ -0,0 +1,72 @@
1
+ <script setup>
2
+ /**
3
+ * KTimeline — хронология событий: история статусов, журнал действий, лента
4
+ * активности.
5
+ *
6
+ * items: [{ time, title, text?, tone?, icon? }]
7
+ */
8
+ import KIcon from './KIcon.vue'
9
+
10
+ defineProps({
11
+ items: { type: Array, default: () => [] },
12
+ })
13
+ </script>
14
+
15
+ <template>
16
+ <ol class="k-timeline">
17
+ <li v-for="(item, i) in items" :key="i" class="k-timeline__item">
18
+ <span class="k-timeline__marker" :class="`k-timeline__marker--${item.tone || 'neutral'}`">
19
+ <KIcon v-if="item.icon" :name="item.icon" :size="12" />
20
+ </span>
21
+ <div class="k-timeline__body">
22
+ <div class="k-timeline__head">
23
+ <span class="k-timeline__title">
24
+ <slot name="title" :item="item">{{ item.title }}</slot>
25
+ </span>
26
+ <span class="k-timeline__time">{{ item.time }}</span>
27
+ </div>
28
+ <p v-if="item.text" class="k-timeline__text">{{ item.text }}</p>
29
+ <slot name="extra" :item="item" />
30
+ </div>
31
+ </li>
32
+ </ol>
33
+ </template>
34
+
35
+ <style scoped>
36
+ .k-timeline { margin: 0; padding: 0; list-style: none; }
37
+ .k-timeline__item { position: relative; display: flex; gap: 0.75rem; padding-bottom: 1rem; }
38
+ /* Вертикальная линия между маркерами; у последнего элемента не рисуем. */
39
+ .k-timeline__item:not(:last-child)::before {
40
+ content: '';
41
+ position: absolute;
42
+ left: 0.53rem;
43
+ top: 1.35rem;
44
+ bottom: 0.1rem;
45
+ width: 1px;
46
+ background: var(--k-border);
47
+ }
48
+ .k-timeline__item:last-child { padding-bottom: 0; }
49
+ .k-timeline__marker {
50
+ position: relative;
51
+ z-index: 1;
52
+ display: inline-flex;
53
+ align-items: center;
54
+ justify-content: center;
55
+ width: 1.125rem;
56
+ height: 1.125rem;
57
+ margin-top: 0.2rem;
58
+ border-radius: 50%;
59
+ flex: none;
60
+ border: 2px solid var(--k-surface);
61
+ }
62
+ .k-timeline__marker--neutral { background: var(--k-border-strong); color: var(--k-surface); }
63
+ .k-timeline__marker--primary { background: var(--k-primary); color: var(--k-on-primary); }
64
+ .k-timeline__marker--success { background: var(--k-success); color: var(--k-on-success); }
65
+ .k-timeline__marker--warning { background: var(--k-warning); color: var(--k-on-warning); }
66
+ .k-timeline__marker--danger { background: var(--k-danger); color: var(--k-on-danger); }
67
+ .k-timeline__body { flex: 1; min-width: 0; }
68
+ .k-timeline__head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem; }
69
+ .k-timeline__title { font-size: 0.9375rem; }
70
+ .k-timeline__time { font-size: 0.75rem; color: var(--k-text-faint); white-space: nowrap; font-variant-numeric: tabular-nums; }
71
+ .k-timeline__text { font-size: 0.8125rem; color: var(--k-text-muted); margin-top: 0.1rem; }
72
+ </style>
@@ -0,0 +1,34 @@
1
+ <script setup>
2
+ /**
3
+ * KToast — контейнер всплывающих уведомлений на @zag-js/toast.
4
+ *
5
+ * Ставится один раз в корне приложения; сообщения добавляются через
6
+ * useToast() — вызывающему коду доступ к компоненту не нужен.
7
+ */
8
+ import * as toast from '@zag-js/toast'
9
+ import { normalizeProps, useMachine } from '@zag-js/vue'
10
+ import { computed, useId } from 'vue'
11
+ import KToastItem from './KToastItem.vue'
12
+ import { toastStore } from '../composables/useToast.js'
13
+
14
+ const service = useMachine(toast.group.machine, { id: useId(), store: toastStore })
15
+ const api = computed(() => toast.group.connect(service, normalizeProps))
16
+ </script>
17
+
18
+ <template>
19
+ <Teleport to="body">
20
+ <div class="k-toasts" v-bind="api.getGroupProps({ label: 'Уведомления' })">
21
+ <KToastItem
22
+ v-for="(item, index) in api.getToasts()"
23
+ :key="item.id"
24
+ :actor="item"
25
+ :index="index"
26
+ :parent="service"
27
+ />
28
+ </div>
29
+ </Teleport>
30
+ </template>
31
+
32
+ <style scoped>
33
+ .k-toasts { z-index: 1300; }
34
+ </style>
@@ -0,0 +1,86 @@
1
+ <script setup>
2
+ /**
3
+ * KToastItem — одно уведомление внутри KToast. Внутренний компонент: машина
4
+ * тоста получает родительский сервис группы и свой индекс в стопке.
5
+ */
6
+ import * as toast from '@zag-js/toast'
7
+ import { normalizeProps, useMachine } from '@zag-js/vue'
8
+ import { computed } from 'vue'
9
+ import KIcon from './KIcon.vue'
10
+
11
+ const props = defineProps({
12
+ actor: { type: Object, required: true },
13
+ index: { type: Number, required: true },
14
+ parent: { type: Object, required: true },
15
+ })
16
+
17
+ const machineProps = computed(() => ({ ...props.actor, parent: props.parent, index: props.index }))
18
+
19
+ const service = useMachine(toast.machine, machineProps)
20
+ const api = computed(() => toast.connect(service, normalizeProps))
21
+
22
+ const icons = { info: 'info', success: 'check-badge', warning: 'alert', error: 'reject', loading: 'spinner' }
23
+ </script>
24
+
25
+ <template>
26
+ <div class="k-toast" v-bind="api.getRootProps()">
27
+ <span v-bind="api.getGhostBeforeProps()" />
28
+ <KIcon :name="icons[api.type] || 'info'" :size="18" class="k-toast__icon" :spin="api.type === 'loading'" />
29
+ <div class="k-toast__body">
30
+ <p v-if="api.title" class="k-toast__title" v-bind="api.getTitleProps()">{{ api.title }}</p>
31
+ <p v-if="api.description" class="k-toast__text" v-bind="api.getDescriptionProps()">{{ api.description }}</p>
32
+ </div>
33
+ <button v-if="api.closable" class="k-toast__close" aria-label="Закрыть" v-bind="api.getCloseTriggerProps()">
34
+ <KIcon name="close" :size="14" />
35
+ </button>
36
+ <span v-bind="api.getGhostAfterProps()" />
37
+ </div>
38
+ </template>
39
+
40
+ <style scoped>
41
+ .k-toast {
42
+ /* Координаты и прозрачность приходят от машины в CSS-переменных; здесь они
43
+ только отображаются на свойства, чтобы стопка анимировалась плавно. */
44
+ translate: var(--x) var(--y);
45
+ scale: var(--scale, 1);
46
+ opacity: var(--opacity);
47
+ height: var(--height, auto);
48
+ z-index: var(--z-index);
49
+ will-change: translate, opacity, scale;
50
+ transition:
51
+ translate 400ms cubic-bezier(0.21, 1.02, 0.73, 1),
52
+ scale 400ms,
53
+ opacity 400ms,
54
+ height 400ms;
55
+ display: flex;
56
+ align-items: flex-start;
57
+ gap: 0.6rem;
58
+ width: min(24rem, calc(100vw - 2rem));
59
+ padding: 0.75rem 0.875rem;
60
+ background: var(--k-surface);
61
+ border: 1px solid var(--k-border);
62
+ border-left: 3px solid var(--k-primary);
63
+ border-radius: var(--k-radius-sm);
64
+ box-shadow: var(--k-shadow-md);
65
+ color: var(--k-text);
66
+ }
67
+ .k-toast[data-type='success'] { border-left-color: var(--k-success); }
68
+ .k-toast[data-type='warning'] { border-left-color: var(--k-warning); }
69
+ .k-toast[data-type='error'] { border-left-color: var(--k-danger); }
70
+ .k-toast__icon { margin-top: 0.1rem; color: var(--k-text-muted); flex: none; }
71
+ .k-toast[data-type='success'] .k-toast__icon { color: var(--k-success); }
72
+ .k-toast[data-type='warning'] .k-toast__icon { color: var(--k-warning-strong); }
73
+ .k-toast[data-type='error'] .k-toast__icon { color: var(--k-danger); }
74
+ .k-toast__body { flex: 1; min-width: 0; }
75
+ .k-toast__title { font-size: 0.9375rem; font-weight: 600; }
76
+ .k-toast__text { font-size: 0.8125rem; color: var(--k-text-muted); white-space: pre-line; overflow-wrap: break-word; }
77
+ .k-toast__close {
78
+ background: none;
79
+ border: none;
80
+ color: var(--k-text-faint);
81
+ cursor: pointer;
82
+ padding: 0;
83
+ flex: none;
84
+ }
85
+ .k-toast__close:hover { color: var(--k-text); }
86
+ </style>
@@ -0,0 +1,67 @@
1
+ <script setup>
2
+ /**
3
+ * KTooltip — подсказка над обёрнутым элементом на @zag-js/tooltip: открывается
4
+ * по наведению и фокусу с задержкой, закрывается по Esc и уходу указателя.
5
+ * Пузырёк рендерится в <body>, поэтому не обрезается контейнерами с overflow.
6
+ *
7
+ * <KTooltip text="Расшифровка статуса"><KBadge>В работе</KBadge></KTooltip>
8
+ */
9
+ import * as tooltip from '@zag-js/tooltip'
10
+ import { normalizeProps, useMachine } from '@zag-js/vue'
11
+ import { computed, useId } from 'vue'
12
+
13
+ const props = defineProps({
14
+ text: { type: String, default: '' },
15
+ placement: { type: String, default: 'top' }, // top | bottom | left | right
16
+ openDelay: { type: Number, default: 300 },
17
+ closeDelay: { type: Number, default: 100 },
18
+ disabled: { type: Boolean, default: false },
19
+ })
20
+
21
+ const service = useMachine(tooltip.machine, {
22
+ id: useId(),
23
+ get positioning() {
24
+ return { placement: props.placement }
25
+ },
26
+ get openDelay() {
27
+ return props.openDelay
28
+ },
29
+ get closeDelay() {
30
+ return props.closeDelay
31
+ },
32
+ get disabled() {
33
+ return props.disabled
34
+ },
35
+ })
36
+
37
+ const api = computed(() => tooltip.connect(service, normalizeProps))
38
+ </script>
39
+
40
+ <template>
41
+ <span class="k-tooltip" tabindex="0" v-bind="api.getTriggerProps()">
42
+ <slot />
43
+ </span>
44
+ <Teleport to="body">
45
+ <div v-if="api.open" v-bind="api.getPositionerProps()">
46
+ <div class="k-tooltip__bubble" v-bind="api.getContentProps()">
47
+ <slot name="content">{{ text }}</slot>
48
+ </div>
49
+ </div>
50
+ </Teleport>
51
+ </template>
52
+
53
+ <style scoped>
54
+ .k-tooltip { display: inline-flex; outline: none; }
55
+ .k-tooltip:focus-visible { outline: 2px solid var(--k-primary); outline-offset: 2px; border-radius: var(--k-radius-sm); }
56
+ .k-tooltip__bubble {
57
+ z-index: 1200;
58
+ max-width: 18rem;
59
+ padding: 0.4rem 0.55rem;
60
+ background: var(--k-tooltip-bg);
61
+ color: var(--k-tooltip-text);
62
+ border-radius: var(--k-radius-sm);
63
+ font-size: 0.75rem;
64
+ line-height: 1.4;
65
+ box-shadow: var(--k-shadow-md);
66
+ }
67
+ </style>
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Реестр иконок кита: семантическое имя → компонент Lucide.
3
+ *
4
+ * Отдельный модуль, чтобы витрина и тесты могли перечислить имена, не
5
+ * инстанцируя компонент. Имена задаются по роли в интерфейсе ("delete",
6
+ * "settings"), а не по форме ("trash", "gear"): так набор можно заменить
7
+ * целиком, не переписывая вызовы.
8
+ *
9
+ * Набор общего назначения. Иконки, специфичные для конкретного продукта,
10
+ * добавляйте в приложении через проп `component` или собственный реестр.
11
+ */
12
+ import {
13
+ Activity, AlertTriangle, ArchiveRestore, ArrowDown, ArrowLeft, ArrowRight, ArrowUp,
14
+ BadgeCheck, Banknote, Bell, Bookmark, Building2, Calendar, Check,
15
+ ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Clipboard, Clock,
16
+ Copy, CreditCard, Database, Download, ExternalLink, Eye, EyeOff, File,
17
+ FileText, FileUp, Filter, Folder, FolderInput, FolderOpen, Globe, Heart, Home, Image, Info,
18
+ KeyRound, LayoutDashboard, LayoutGrid, LifeBuoy, Link, List, ListChecks, Loader2, Lock,
19
+ LogIn, LogOut, Mail, MapPin, Menu, MessageSquare, MinusCircle,
20
+ MoreHorizontal, MoreVertical, Paperclip, Pencil, Percent, Phone, Play,
21
+ Plus, RefreshCw, RotateCw, Save, Scissors, Search, Send, Settings, Share2, Shield,
22
+ ShieldCheck, SlidersHorizontal, Star, Sun, Moon, Tag, Trash2, TrendingDown,
23
+ TrendingUp, Upload, User, Users, Wallet, X, XCircle, Zap,
24
+ } from 'lucide-vue-next'
25
+
26
+ export const iconRegistry = {
27
+ // навигация
28
+ back: ArrowLeft,
29
+ forward: ArrowRight,
30
+ up: ArrowUp,
31
+ down: ArrowDown,
32
+ 'chevron-up': ChevronUp,
33
+ 'chevron-down': ChevronDown,
34
+ 'chevron-left': ChevronLeft,
35
+ 'chevron-right': ChevronRight,
36
+ menu: Menu,
37
+ more: MoreVertical,
38
+ 'more-horizontal': MoreHorizontal,
39
+ home: Home,
40
+ dashboard: LayoutDashboard,
41
+ external: ExternalLink,
42
+ link: Link,
43
+
44
+ // действия
45
+ add: Plus,
46
+ edit: Pencil,
47
+ delete: Trash2,
48
+ save: Save,
49
+ copy: Copy,
50
+ search: Search,
51
+ filter: Filter,
52
+ sliders: SlidersHorizontal,
53
+ refresh: RefreshCw,
54
+ rotate: RotateCw,
55
+ scissors: Scissors,
56
+ restore: ArchiveRestore,
57
+ 'file-up': FileUp,
58
+ move: FolderInput,
59
+ upload: Upload,
60
+ download: Download,
61
+ share: Share2,
62
+ send: Send,
63
+ play: Play,
64
+ close: X,
65
+ check: Check,
66
+ reject: XCircle,
67
+ remove: MinusCircle,
68
+
69
+ // состояния и статусы
70
+ info: Info,
71
+ alert: AlertTriangle,
72
+ 'check-badge': BadgeCheck,
73
+ spinner: Loader2,
74
+ activity: Activity,
75
+ trending: TrendingUp,
76
+ declining: TrendingDown,
77
+ zap: Zap,
78
+
79
+ // объекты
80
+ document: FileText,
81
+ file: File,
82
+ folder: Folder,
83
+ 'folder-open': FolderOpen,
84
+ attach: Paperclip,
85
+ image: Image,
86
+ clipboard: Clipboard,
87
+ checklist: ListChecks,
88
+ list: List,
89
+ grid: LayoutGrid,
90
+ database: Database,
91
+ tag: Tag,
92
+ bookmark: Bookmark,
93
+ star: Star,
94
+ heart: Heart,
95
+
96
+ // люди и организации
97
+ user: User,
98
+ users: Users,
99
+ organization: Building2,
100
+
101
+ // связь
102
+ mail: Mail,
103
+ phone: Phone,
104
+ message: MessageSquare,
105
+ bell: Bell,
106
+
107
+ // безопасность
108
+ lock: Lock,
109
+ key: KeyRound,
110
+ shield: Shield,
111
+ 'shield-check': ShieldCheck,
112
+ login: LogIn,
113
+ logout: LogOut,
114
+ eye: Eye,
115
+ 'eye-off': EyeOff,
116
+
117
+ // деньги
118
+ finance: Banknote,
119
+ wallet: Wallet,
120
+ card: CreditCard,
121
+ percent: Percent,
122
+
123
+ // прочее
124
+ calendar: Calendar,
125
+ clock: Clock,
126
+ location: MapPin,
127
+ globe: Globe,
128
+ settings: Settings,
129
+ help: LifeBuoy,
130
+ sun: Sun,
131
+ moon: Moon,
132
+ }
133
+
134
+ /**
135
+ * Расширяет реестр из приложения: registerIcons({ rescan: RotateCw }). Имена
136
+ * действуют для всех компонентов кита, включая проп `icon` у KButton.
137
+ */
138
+ export function registerIcons(icons) {
139
+ Object.assign(iconRegistry, icons)
140
+ iconNames.splice(0, iconNames.length, ...Object.keys(iconRegistry))
141
+ }
142
+
143
+ export const iconNames = Object.keys(iconRegistry)
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Всплывающие уведомления на @zag-js/toast.
3
+ *
4
+ * Очередь живёт в модуле, а не в компоненте: любому стору или обработчику
5
+ * достаточно импортировать useToast(), не пробрасывая ссылку на <KToast>.
6
+ * Сам контейнер монтируется один раз в корне приложения.
7
+ *
8
+ * const toast = useToast()
9
+ * toast.success('Сохранено', 'Изменения применены')
10
+ * toast.error('Ошибка', 'Не удалось связаться с сервером')
11
+ * pushToast({ title: 'Идёт загрузка', tone: 'loading', timeout: 0 }) // висит до dismiss
12
+ */
13
+ import { createStore } from '@zag-js/toast'
14
+
15
+ export const toastStore = createStore({
16
+ placement: 'top-end',
17
+ duration: 4000,
18
+ removeDelay: 400,
19
+ gap: 12,
20
+ })
21
+
22
+ const types = {
23
+ info: 'info',
24
+ success: 'success',
25
+ warning: 'warning',
26
+ danger: 'error',
27
+ error: 'error',
28
+ loading: 'loading',
29
+ }
30
+
31
+ export function pushToast({ title = '', text = '', tone = 'info', timeout } = {}) {
32
+ const data = { title, description: text, type: types[tone] ?? 'info' }
33
+ if (timeout === 0) data.duration = Infinity
34
+ else if (timeout != null) data.duration = timeout
35
+ return toastStore.create(data)
36
+ }
37
+
38
+ export function dismissToast(id) {
39
+ toastStore.dismiss(id)
40
+ }
41
+
42
+ export function useToast() {
43
+ return {
44
+ toast: pushToast,
45
+ success: (title, text) => pushToast({ title, text, tone: 'success' }),
46
+ info: (title, text) => pushToast({ title, text, tone: 'info' }),
47
+ warning: (title, text) => pushToast({ title, text, tone: 'warning' }),
48
+ error: (title, text) => pushToast({ title, text, tone: 'danger' }),
49
+ loading: (title, text) => pushToast({ title, text, tone: 'loading', timeout: 0 }),
50
+ dismiss: dismissToast,
51
+ }
52
+ }