@ziyarago/ui 0.1.1 → 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 (53) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -0
  3. package/dist/llms-full.txt +100 -8
  4. package/dist/llms.txt +1 -1
  5. package/dist/manifest.json +412 -20
  6. package/dist/style.css +73 -4
  7. package/docs/INTEGRATION.md +153 -0
  8. package/docs/MIGRATION.md +453 -0
  9. package/package.json +87 -7
  10. package/src/compat/index.js +468 -0
  11. package/src/compat/map.js +282 -0
  12. package/src/compat/resolver.js +22 -0
  13. package/src/compat/vite.js +29 -0
  14. package/src/core/bools.js +4 -0
  15. package/src/core/components.js +31 -23
  16. package/src/core/components2.js +20 -16
  17. package/src/core/components4.js +3 -3
  18. package/src/core/components6.js +25 -16
  19. package/src/core/index.js +2 -0
  20. package/src/i18n/components/CopyButton.js +3 -0
  21. package/src/i18n/components/index.js +5 -0
  22. package/src/i18n/index.js +48 -0
  23. package/src/meta/actions.js +10 -3
  24. package/src/meta/data.js +8 -3
  25. package/src/meta/feedback.js +2 -1
  26. package/src/meta/forms.js +44 -4
  27. package/src/meta/media.js +7 -2
  28. package/src/meta/navigation.js +3 -0
  29. package/src/meta/overlay.js +8 -2
  30. package/src/nuxt/module.mjs +2 -2
  31. package/src/nuxt/plugin.mjs +6 -5
  32. package/src/react/index.js +202 -8
  33. package/src/resolver.js +33 -0
  34. package/src/styles/components.css +7 -0
  35. package/src/styles/components6.css +2 -0
  36. package/src/styles/effects.css +6 -4
  37. package/src/styles/extras.css +55 -0
  38. package/src/styles/index.css +1 -0
  39. package/src/theme.js +16 -4
  40. package/src/vue/global.js +2 -0
  41. package/src/vue/index.js +13 -5
  42. package/types/compat-resolver.d.ts +12 -0
  43. package/types/compat-vite.d.ts +11 -0
  44. package/types/compat.d.ts +176 -0
  45. package/types/i18n.d.ts +12 -0
  46. package/types/meta.d.ts +4 -0
  47. package/types/nuxt.d.ts +1 -1
  48. package/types/react.d.ts +6282 -186
  49. package/types/resolver.d.ts +14 -0
  50. package/types/shared.d.ts +7 -7
  51. package/types/theme.d.ts +20 -1
  52. package/types/vue-global.d.ts +193 -0
  53. package/types/vue.d.ts +6178 -186
@@ -0,0 +1,468 @@
1
+ // @ziyarago/ui/compat · the OLD glass-ui-kit component names (UiInput, UiTable, UiSidebar, ...) as thin Vue wrappers over the new components.
2
+ // Switch the imports first, refactor to the new names later. See docs/MIGRATION.md. Vue 3 only (the old kit never had a React build).
3
+ import { h, defineComponent, Teleport, ref, readonly, computed, provide, inject, watch, getCurrentInstance, onMounted, onBeforeUnmount, Fragment, Comment } from 'vue';
4
+ import * as K from '../vue/index.js';
5
+ import { useUiTheme } from '../theme.js';
6
+ import { ddRef } from '../core/components.js';
7
+ import { config, configureCompat, SPECS, translate, emitsOf, buildHandlers, translateSlots, warnOnce, iconName, mapSize, twWidth, sortRows, camelizeKeys } from './map.js';
8
+
9
+ export { configureCompat, iconName };
10
+ export { COMPAT_NAMES, COMPAT_COMPONENTS, SPECS, translate } from './map.js';
11
+
12
+ const N = n => K['Ui' + n];
13
+ const cx = (...a) => a.filter(Boolean).join(' ');
14
+ const truthy = v => v === '' || !!v;
15
+
16
+ /** (key, fallback) => string. Order: configureCompat({ t }), then the host app's vue-i18n ($t, only when the key exists), then the English fallback. */
17
+ function useT() {
18
+ const g = (getCurrentInstance() || { appContext: { config: { globalProperties: {} } } }).appContext.config.globalProperties;
19
+ return (key, fb) => {
20
+ if (config.t) return config.t(key, fb);
21
+ if (g.$t && (!g.$te || g.$te(key))) return g.$t(key);
22
+ return fb;
23
+ };
24
+ }
25
+
26
+ /** () => BCP 47 locale of the host vue-i18n (or undefined). Date pickers use it when no locale prop is passed, like the old kit did. */
27
+ function useLocale() {
28
+ const g = (getCurrentInstance() || { appContext: { config: { globalProperties: {} } } }).appContext.config.globalProperties;
29
+ return () => (g.$i18n && typeof g.$i18n.locale === 'string' ? g.$i18n.locale : undefined);
30
+ }
31
+
32
+ const vtext = vnodes => [].concat(vnodes || []).map(v => (typeof v === 'string' || typeof v === 'number' ? String(v) : v && v.children ? vtext(typeof v.children === 'function' ? v.children() : v.children) : '')).join('');
33
+
34
+ // ───────────────────────── table-driven wrappers ─────────────────────────
35
+ // pre(attrs, slots) -> { attrs?, slots?, render? } lets a wrapper adjust input or bypass the new component entirely.
36
+ const PRE = {
37
+ Calendar: (attrs, _s, ctx) => (attrs.locale === undefined && ctx.locale() ? { attrs: { ...attrs, locale: ctx.locale() } } : undefined),
38
+ Skeleton: attrs => (/(^|\s)h-/.test(String(attrs.class || '')) && attrs.variant !== 'text' && attrs.variant !== 'circle' ? { attrs: { ...attrs, height: '100%' } } : undefined),
39
+ };
40
+
41
+ function makeCompat(name) {
42
+ return defineComponent({
43
+ name: 'Ui' + name, inheritAttrs: false, emits: emitsOf(name),
44
+ setup(_, { attrs, slots, emit }) {
45
+ const t = useT(), locale = useLocale();
46
+ return () => {
47
+ let a = attrs, s = slots;
48
+ const pre = PRE[name] && PRE[name](a, s, { t, locale });
49
+ if (pre && pre.render) return pre.render();
50
+ if (pre) { a = pre.attrs || a; s = pre.slots || s; }
51
+ const r = translate(name, a, { ...config, t });
52
+ warnOnce(name, r.dropped);
53
+ return h(N(r.component), { ...r.props, ...buildHandlers(name, r.src, emit) }, translateSlots(name, s, r.src));
54
+ };
55
+ },
56
+ });
57
+ }
58
+
59
+ const generated = {};
60
+ for (const name of Object.keys(SPECS)) generated[name] = makeCompat(name);
61
+
62
+ // ───────────────────────── Tabs (compound) ─────────────────────────
63
+ const TabsKey = Symbol('UiTabs');
64
+ export const UiTabs = defineComponent({
65
+ name: 'UiTabs', props: { modelValue: { type: [String, Number], default: undefined }, variant: { type: String, default: 'pill' } }, emits: ['update:modelValue'],
66
+ setup(props, { slots, emit }) {
67
+ provide(TabsKey, { modelValue: computed(() => props.modelValue), variant: computed(() => props.variant), setValue: v => emit('update:modelValue', v) });
68
+ return () => h('div', null, slots.default && slots.default());
69
+ },
70
+ });
71
+ export const UiTabsList = defineComponent({
72
+ name: 'UiTabsList',
73
+ setup(_, { slots }) { const ctx = inject(TabsKey); return () => h('div', { class: ctx && ctx.variant.value === 'underline' ? 'zg-tabs' : 'zg-pt', role: 'tablist' }, slots.default && slots.default()); },
74
+ });
75
+ export const UiTab = defineComponent({
76
+ name: 'UiTab', props: { value: { type: [String, Number], required: true }, disabled: Boolean },
77
+ setup(props, { slots }) {
78
+ const ctx = inject(TabsKey);
79
+ return () => h('button', { type: 'button', role: 'tab', 'aria-selected': String(ctx.modelValue.value === props.value), disabled: props.disabled, onClick: () => !props.disabled && ctx.setValue(props.value) }, slots.default && slots.default());
80
+ },
81
+ });
82
+ export const UiTabPanel = defineComponent({
83
+ name: 'UiTabPanel', props: { value: { type: [String, Number], required: true } },
84
+ setup(props, { slots }) { const ctx = inject(TabsKey); return () => (ctx.modelValue.value === props.value ? h('div', { role: 'tabpanel', tabindex: 0 }, slots.default && slots.default()) : null); },
85
+ });
86
+
87
+ // ───────────────────────── Radio / RadioGroup ─────────────────────────
88
+ const RadioItem = defineComponent({ name: 'UiRadio', inheritAttrs: false, props: { value: null, label: String, hint: String, disabled: Boolean, modelValue: null, size: String, name: String, id: String }, emits: ['update:modelValue'],
89
+ setup(props, { emit, attrs }) {
90
+ return () => h(N('RadioGroup'), { ...attrs, modelValue: props.modelValue, size: props.size, options: [{ value: props.value, label: props.label, hint: props.hint, disabled: props.disabled }], onChange: v => emit('update:modelValue', v) });
91
+ } });
92
+ export const UiRadio = RadioItem;
93
+ function collectRadios(vnodes, out = []) {
94
+ for (const v of vnodes || []) {
95
+ if (!v || v.type === Comment) continue;
96
+ if (v.type === Fragment) collectRadios(v.children, out);
97
+ else if (v.type === RadioItem) { const p = camelizeKeys(v.props || {}); out.push({ value: p.value, label: p.label, hint: p.hint, disabled: truthy(p.disabled) }); }
98
+ }
99
+ return out;
100
+ }
101
+ export const UiRadioGroup = defineComponent({
102
+ name: 'UiRadioGroup', inheritAttrs: false, emits: ['update:modelValue'],
103
+ setup(_, { attrs, slots, emit }) {
104
+ return () => {
105
+ const a = camelizeKeys(attrs), { modelValue, label, hint, error, size, disabled, orientation, name, ...rest } = a;
106
+ return h(N('RadioGroup'), { ...rest, modelValue, label, hint, error: error === true ? ' ' : error || undefined, size, disabled: truthy(disabled), orientation, options: collectRadios(slots.default && slots.default()), onChange: v => emit('update:modelValue', v) });
107
+ };
108
+ },
109
+ });
110
+
111
+ // ───────────────────────── Dropdown / Menu / MenuItem / MenuSeparator ─────────────────────────
112
+ const DropdownKey = Symbol('UiDropdown');
113
+ export const UiDropdown = defineComponent({
114
+ name: 'UiDropdown', inheritAttrs: false, props: { open: { type: Boolean, default: undefined }, placement: { type: String, default: 'bottom-start' }, offset: { type: Number, default: 8 }, closeOnOutsideClick: { type: Boolean, default: true }, closeOnEsc: { type: Boolean, default: true } }, emits: ['update:open'],
115
+ setup(props, { slots, emit }) {
116
+ const inner = ref(false), trig = ref(null), panel = ref(null), pos = ref({ position: 'fixed', top: '0px', left: '0px', visibility: 'hidden' });
117
+ const isOpen = computed(() => (props.open !== undefined ? props.open : inner.value));
118
+ const setOpen = v => { inner.value = v; emit('update:open', v); };
119
+ const close = () => { if (isOpen.value) setOpen(false); }, toggle = () => setOpen(!isOpen.value);
120
+ provide(DropdownKey, { close });
121
+ // Panel is teleported to <body> and positioned with fixed coordinates (flip + clamp to the viewport), so overflow:hidden parents (tables, cards) never clip it.
122
+ const place = () => {
123
+ const t = trig.value, f = panel.value; if (!t || !f) return;
124
+ const r = t.getBoundingClientRect(), fw = f.offsetWidth, fh = f.offsetHeight, pad = 8, off = props.offset;
125
+ let [side, align = 'start'] = props.placement.split('-'), x, y;
126
+ if (side === 'bottom' && r.bottom + off + fh > innerHeight - pad && r.top - off - fh >= pad) side = 'top'; else if (side === 'top' && r.top - off - fh < pad && r.bottom + off + fh <= innerHeight - pad) side = 'bottom';
127
+ if (side === 'top' || side === 'bottom') { y = side === 'bottom' ? r.bottom + off : r.top - off - fh; x = align === 'end' ? r.right - fw : align === 'center' ? r.left + (r.width - fw) / 2 : r.left; }
128
+ else { x = side === 'right' ? r.right + off : r.left - off - fw; y = align === 'end' ? r.bottom - fh : r.top; }
129
+ pos.value = { position: 'fixed', top: Math.max(pad, Math.min(y, innerHeight - fh - pad)) + 'px', left: Math.max(pad, Math.min(x, innerWidth - fw - pad)) + 'px', visibility: 'visible' };
130
+ };
131
+ const onDown = e => { if (isOpen.value && props.closeOnOutsideClick && !(trig.value && trig.value.contains(e.target)) && !(panel.value && panel.value.contains(e.target))) close(); };
132
+ const onKey = e => { if (e.key === 'Escape' && isOpen.value && props.closeOnEsc) { e.stopPropagation(); close(); const b = trig.value && trig.value.querySelector('button,[href],[tabindex]'); if (b) b.focus(); } };
133
+ onMounted(() => { document.addEventListener('mousedown', onDown); document.addEventListener('keydown', onKey); addEventListener('resize', place); addEventListener('scroll', place, true); });
134
+ onBeforeUnmount(() => { document.removeEventListener('mousedown', onDown); document.removeEventListener('keydown', onKey); removeEventListener('resize', place); removeEventListener('scroll', place, true); });
135
+ watch(isOpen, o => { if (o) { pos.value = { ...pos.value, visibility: 'hidden' }; Promise.resolve().then(() => requestAnimationFrame(place)); } });
136
+ return () => [
137
+ h('span', { ref: trig, class: 'zg-dd-trig', onClick: toggle }, slots.trigger ? slots.trigger({ isOpen: isOpen.value, toggle }) : null),
138
+ isOpen.value ? h(Teleport, { to: 'body' }, h('div', { ref: panel, class: 'zg-dd__panel', style: pos.value }, slots.default && slots.default())) : null,
139
+ ];
140
+ },
141
+ });
142
+ export const UiMenu = defineComponent({
143
+ name: 'UiMenu', props: { minWidth: { type: String, default: '12rem' }, autofocus: { type: Boolean, default: true } },
144
+ setup(props, { slots }) {
145
+ const el = ref(null);
146
+ const items = () => (el.value ? [...el.value.querySelectorAll("[role='menuitem']:not([aria-disabled='true'])")] : []);
147
+ onMounted(() => { if (props.autofocus) Promise.resolve().then(() => { const i = items()[0]; if (i) i.focus(); }); });
148
+ const onKeydown = e => {
149
+ const list = items(); if (!list.length) return;
150
+ const i = list.indexOf(document.activeElement), go = n => { e.preventDefault(); list[n].focus(); };
151
+ if (e.key === 'ArrowDown') go((i + 1) % list.length); else if (e.key === 'ArrowUp') go(i <= 0 ? list.length - 1 : i - 1); else if (e.key === 'Home') go(0); else if (e.key === 'End') go(list.length - 1);
152
+ };
153
+ return () => h('div', { ref: el, class: 'zg-menu zg-menu--static', role: 'menu', style: { minWidth: props.minWidth }, onKeydown }, slots.default && slots.default());
154
+ },
155
+ });
156
+ export const UiMenuItem = defineComponent({
157
+ name: 'UiMenuItem', props: { disabled: Boolean, tone: { type: String, default: 'default' }, closeOnClick: { type: Boolean, default: true }, loading: Boolean }, emits: ['click'],
158
+ setup(props, { slots, emit }) {
159
+ const ctx = inject(DropdownKey, null);
160
+ return () => h('button', {
161
+ type: 'button', role: 'menuitem', disabled: props.disabled, 'aria-disabled': props.disabled ? 'true' : undefined, class: cx('zg-menu__i', props.tone === 'danger' && 'zg-menu__i--danger'),
162
+ onClick: e => { if (props.disabled) return; emit('click', e); if (props.closeOnClick && ctx) ctx.close(); },
163
+ }, slots['icon-left'] && slots['icon-left'](), h('span', { style: { flex: 1, textAlign: 'start' } }, slots.default && slots.default()), slots.shortcut ? h('span', { class: 'zg-kbd' }, slots.shortcut()) : null, slots['icon-right'] && slots['icon-right']());
164
+ },
165
+ });
166
+ export const UiMenuSeparator = defineComponent({
167
+ name: 'UiMenuSeparator', props: { label: String },
168
+ setup(props) { return () => (props.label ? h('div', { class: 'zg-menu__lbl', role: 'separator' }, props.label) : h('div', { class: 'zg-menu__sep', role: 'separator' })); },
169
+ });
170
+
171
+ // ───────────────────────── Table ─────────────────────────
172
+ export const UiTable = defineComponent({
173
+ name: 'UiTable', inheritAttrs: false, emits: ['rowClick'],
174
+ setup(_, { attrs, slots, emit }) {
175
+ const t = useT();
176
+ const sortKey = ref(null), sortDir = ref('asc');
177
+ const value = (row, col) => (col.accessor ? col.accessor(row) : row[col.key]);
178
+ return () => {
179
+ const a = camelizeKeys(attrs), cols = a.columns || [], rows = a.rows || [];
180
+ const sorted = sortKey.value && cols.find(c => c.key === sortKey.value) ? sortRows(rows, cols.find(c => c.key === sortKey.value), sortDir.value, value) : rows;
181
+ const loading = truthy(a.loading), sk = a.skeletonRows || 5;
182
+ const newCols = cols.map(c => ({
183
+ key: c.key, sortable: c.sortable, align: c.align, width: twWidth(c.width),
184
+ label: slots['header-' + c.key] ? slots['header-' + c.key]({ column: c }) : c.label,
185
+ render: (_v, nr) => (nr.__sk ? h(N('Skeleton'), { lines: 1 }) : slots['cell-' + c.key] ? slots['cell-' + c.key]({ row: nr.__row, value: value(nr.__row, c), index: nr.__i }) : (value(nr.__row, c) ?? '')),
186
+ }));
187
+ const data = loading ? Array.from({ length: sk }, (_x, i) => ({ id: 'sk' + i, __sk: true })) : sorted.map((row, i) => ({ id: a.rowKey ? a.rowKey(row, i) : i, __row: row, __i: i }));
188
+ const root = {};
189
+ for (const k of Object.keys(attrs)) if (/^(aria|data)-|^(class|style|id)$/.test(k)) root[k] = attrs[k];
190
+ return h(N('DataTable'), {
191
+ ...root, columns: newCols, rows: data, size: a.density === 'compact' ? 'sm' : 'md', sortKey: sortKey.value, sortDir: sortDir.value,
192
+ empty: t('common.noData', 'No data to display'),
193
+ onSort: (key, dir) => { sortKey.value = key; sortDir.value = dir; },
194
+ onRowClick: (nr) => { if (!nr.__sk) emit('rowClick', nr.__row, nr.__i); },
195
+ }, slots.empty ? { empty: () => slots.empty() } : undefined);
196
+ };
197
+ },
198
+ });
199
+
200
+ // ───────────────────────── Sidebar / Navbar / SubNav ─────────────────────────
201
+ const SidebarKey = Symbol('UiSidebar');
202
+ export const UiSidebar = defineComponent({
203
+ name: 'UiSidebar', props: { collapsed: { type: Boolean, default: undefined }, defaultCollapsed: Boolean, open: { type: Boolean, default: undefined }, storageKey: String, widthExpanded: String, widthCollapsed: String }, emits: ['update:collapsed', 'update:open'],
204
+ setup(props, { slots, emit }) {
205
+ const innerCollapsed = ref(props.defaultCollapsed), innerOpen = ref(false), desktop = ref(true);
206
+ onMounted(() => {
207
+ if (props.storageKey) { try { const v = localStorage.getItem(props.storageKey); if ((v === 'true' || v === 'false') && props.collapsed === undefined) innerCollapsed.value = v === 'true'; } catch { /* storage blocked */ } }
208
+ if (typeof matchMedia === 'function') { const mq = matchMedia('(min-width:1024px)'); desktop.value = mq.matches; const f = () => { desktop.value = mq.matches; }; mq.addEventListener('change', f); onBeforeUnmount(() => mq.removeEventListener('change', f)); }
209
+ });
210
+ watch(() => props.collapsed, v => { if (v !== undefined && props.storageKey) { try { localStorage.setItem(props.storageKey, String(v)); } catch { /* storage blocked */ } } });
211
+ const collapsed = computed(() => (props.collapsed !== undefined ? props.collapsed : innerCollapsed.value));
212
+ const isOpen = computed(() => (props.open !== undefined ? props.open : innerOpen.value));
213
+ const eff = computed(() => collapsed.value && desktop.value);
214
+ const closeMobile = () => { innerOpen.value = false; emit('update:open', false); };
215
+ provide(SidebarKey, { collapsed: eff, closeMobile });
216
+ return () => [
217
+ isOpen.value ? h('div', { class: 'zg-sidenav-scrim', 'aria-hidden': 'true', onClick: closeMobile }) : null,
218
+ h('aside', { class: cx('zg-sidenav zg-sidenav--compat', eff.value && 'zg-sidenav--collapsed', isOpen.value && 'is-open'), 'data-theme': 'dark' },
219
+ h('button', { type: 'button', class: 'zg-sidenav__close zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': 'Close menu', onClick: closeMobile }, h(N('Icon'), { name: 'x', size: 16 })),
220
+ slots.header ? h('div', { class: 'zg-sidenav__brand' }, slots.header({ collapsed: eff.value })) : null,
221
+ h('nav', { class: 'zg-sidenav__nav' }, slots.default && slots.default()),
222
+ slots.footer ? h('div', { class: 'zg-sidenav__foot' }, slots.footer({ collapsed: eff.value })) : null),
223
+ ];
224
+ },
225
+ });
226
+ export const UiSidebarSection = defineComponent({
227
+ name: 'UiSidebarSection', props: { label: String },
228
+ setup(props, { slots }) { const ctx = inject(SidebarKey, null); return () => h('div', { class: 'zg-sidenav__sec' }, props.label && !(ctx && ctx.collapsed.value) ? h('div', { class: 'zg-sidenav__group' }, props.label) : null, slots.default && slots.default()); },
229
+ });
230
+ export const UiSidebarItem = defineComponent({
231
+ name: 'UiSidebarItem', props: { label: { type: String, required: true }, icon: null, active: Boolean, badge: [Number, String], href: String, disabled: Boolean }, emits: ['click'],
232
+ setup(props, { emit }) {
233
+ const ctx = inject(SidebarKey, null);
234
+ return () => h(props.href ? 'a' : 'button', {
235
+ class: 'zg-navitem', 'aria-current': props.active ? 'page' : undefined, title: ctx && ctx.collapsed.value ? props.label : undefined, href: props.href, type: props.href ? undefined : 'button', disabled: props.href ? undefined : props.disabled, 'aria-disabled': props.disabled ? 'true' : undefined,
236
+ onClick: e => { if (props.disabled) return; emit('click', e); if (ctx) ctx.closeMobile(); },
237
+ }, props.icon ? h(props.icon, { size: 18, class: 'zg-navitem__ic' }) : null, h('span', null, props.label), props.badge !== undefined && props.badge !== null && props.badge !== '' ? h('span', { class: 'zg-navitem__count' }, props.badge) : null);
238
+ },
239
+ });
240
+ export const UiNavbar = defineComponent({
241
+ name: 'UiNavbar', props: { variant: String, size: String, sticky: Boolean },
242
+ setup(props, { slots }) {
243
+ return () => h('header', { class: cx('zg-topnav zg-topnav--compat', props.size === 'md' && 'zg-topnav--md'), style: props.sticky ? undefined : { position: 'static' } },
244
+ h('div', { class: 'zg-topnav__brand' }, slots.left && slots.left()), h('div', { class: 'zg-topnav__mid' }, slots.center && slots.center()), h('div', { class: 'zg-topnav__actions' }, slots.right && slots.right()));
245
+ },
246
+ });
247
+ export const UiSubNav = defineComponent({
248
+ name: 'UiSubNav', props: { items: { type: Array, default: () => [] }, title: String, titleIcon: null, matchExact: { type: Boolean, default: true } },
249
+ setup(props) {
250
+ const g = getCurrentInstance().appContext.config.globalProperties;
251
+ const path = () => (g.$route ? g.$route.path : typeof location !== 'undefined' ? location.pathname : '');
252
+ const active = p => (props.matchExact ? path() === p : path() === p || path().startsWith(p + '/'));
253
+ return () => h('nav', { class: 'zg-subnav zg-subnav--compat', 'aria-label': props.title },
254
+ props.title ? h('div', { class: 'zg-sidenav__group' }, props.titleIcon ? h(props.titleIcon, { size: 16 }) : null, props.title) : null,
255
+ ...props.items.map(it => h('a', { key: it.path, href: it.path, class: 'zg-navitem', 'aria-current': active(it.path) ? 'page' : undefined, onClick: e => { if (g.$router && !(e.metaKey || e.ctrlKey || e.shiftKey || e.button)) { e.preventDefault(); g.$router.push(it.path); } } },
256
+ it.icon ? h(it.icon, { size: 16 }) : null, h('span', { style: { flex: 1, textAlign: 'start' } }, it.label), it.badge !== undefined && it.badge !== '' ? h('span', { class: 'zg-navitem__count' }, it.badge) : null)));
257
+ },
258
+ });
259
+
260
+ // ───────────────────────── Toast ─────────────────────────
261
+ const toasts = ref([]);
262
+ let toastId = 0;
263
+ export function useToast() {
264
+ const dismiss = id => { const i = toasts.value.findIndex(t => t.id === id); if (i !== -1) toasts.value.splice(i, 1); };
265
+ const push = input => {
266
+ const id = `toast-${Date.now()}-${++toastId}`;
267
+ const toast = { id, tone: input.tone || 'default', duration: input.duration ?? 5000, title: input.title, message: input.message, action: input.action };
268
+ toasts.value.push(toast);
269
+ if (toast.duration > 0 && typeof window !== 'undefined') window.setTimeout(() => dismiss(id), toast.duration);
270
+ return id;
271
+ };
272
+ const tone = t => (message, options) => push({ tone: t, message, ...options });
273
+ return { toasts, push, dismiss, clear: () => { toasts.value = []; }, success: tone('success'), info: tone('info'), warning: tone('warning'), danger: tone('danger') };
274
+ }
275
+ export const UiToastContainer = defineComponent({
276
+ name: 'UiToastContainer', props: { position: { type: String, default: 'top-right' } },
277
+ setup(props) {
278
+ const { toasts: list, dismiss } = useToast();
279
+ return () => h('div', { class: cx('zg-toasts', 'zg-toasts--' + props.position), 'aria-live': 'polite' }, list.value.map(t =>
280
+ h('div', { class: 'zg-toasts__i', key: t.id },
281
+ h(N('Toast'), { tone: t.tone === 'default' ? 'info' : t.tone, title: t.title }, () => [t.message, t.action ? h('div', { class: 'zg-toasts__act' }, h(N('Button'), { size: 'xs', variant: 'ghost', onClick: () => { t.action.onClick(); dismiss(t.id); } }, () => t.action.label)) : null]),
282
+ h('button', { type: 'button', class: 'zg-toasts__x zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': 'Dismiss', onClick: () => dismiss(t.id) }, h(N('Icon'), { name: 'x', size: 14 })))));
283
+ },
284
+ });
285
+
286
+ // ───────────────────────── Command palette ─────────────────────────
287
+ const commands = ref([]), paletteOpen = ref(false);
288
+ export function useCommandPalette() {
289
+ return {
290
+ commands, isOpen: paletteOpen,
291
+ register: c => { const i = commands.value.findIndex(x => x.id === c.id); if (i !== -1) commands.value[i] = c; else commands.value.push(c); },
292
+ unregister: id => { commands.value = commands.value.filter(c => c.id !== id); },
293
+ clear: () => { commands.value = []; },
294
+ open: () => { paletteOpen.value = true; }, close: () => { paletteOpen.value = false; }, toggle: () => { paletteOpen.value = !paletteOpen.value; },
295
+ };
296
+ }
297
+ export const UiCommandPalette = defineComponent({
298
+ name: 'UiCommandPalette', props: { shortcutKey: { type: String, default: 'k' }, disableShortcut: Boolean, emptyText: String, placeholder: String },
299
+ setup(props) {
300
+ const pal = useCommandPalette();
301
+ const onKey = e => { if (!props.disableShortcut && (e.metaKey || e.ctrlKey) && e.key.toLowerCase() === props.shortcutKey) { e.preventDefault(); pal.toggle(); } };
302
+ onMounted(() => document.addEventListener('keydown', onKey));
303
+ onBeforeUnmount(() => document.removeEventListener('keydown', onKey));
304
+ return () => h(N('CommandPalette'), {
305
+ open: pal.isOpen.value, placeholder: props.placeholder,
306
+ commands: pal.commands.value.map(c => ({ id: c.id, label: c.label, icon: iconName(c.icon), shortcut: c.shortcut, keywords: [c.description, c.group, ...(c.keywords || [])].filter(Boolean).join(' ') })),
307
+ onRun: id => { const c = pal.commands.value.find(x => x.id === id); pal.close(); if (c) c.action(); },
308
+ onClose: pal.close,
309
+ });
310
+ },
311
+ });
312
+
313
+ // ───────────────────────── FileUpload (adds the file list the old component had) ─────────────────────────
314
+ const fileSize = b => (b < 1024 ? b + ' B' : b < 1048576 ? (b / 1024).toFixed(1) + ' KB' : (b / 1048576).toFixed(1) + ' MB');
315
+ export const UiFileUpload = defineComponent({
316
+ name: 'UiFileUpload', inheritAttrs: false, emits: ['update:modelValue', 'reject'],
317
+ setup(_, { attrs, slots, emit }) {
318
+ const t = useT(), inner = ref([]);
319
+ return () => {
320
+ const a = camelizeKeys(attrs), files = a.modelValue || inner.value, multiple = truthy(a.multiple);
321
+ const set = list => { inner.value = list; emit('update:modelValue', list); };
322
+ const root = {}; for (const k of Object.keys(attrs)) if (/^(aria|data)-|^(class|style|id)$/.test(k)) root[k] = attrs[k];
323
+ return h('div', { ...root, style: [{ display: 'flex', flexDirection: 'column', gap: '10px' }, root.style] },
324
+ h(N('FileUpload'), {
325
+ label: a.label, hint: a.hint, error: a.error === true ? ' ' : a.error || undefined, accept: a.accept, maxSize: a.maxSize, multiple,
326
+ title: slots.title ? vtext(slots.title()) : t('common.dropFilesOrBrowse', 'Drop files here or browse'),
327
+ onChange: list => set(multiple ? [...files, ...list] : list.slice(0, 1)),
328
+ onReject: (file, reason) => emit('reject', { file, cause: reason }),
329
+ }),
330
+ files.length ? h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '8px' } }, files.map((f, i) => h(N('Chip'), { key: f.name + i, size: 'sm', icon: 'file', onClose: () => set(files.filter((_x, j) => j !== i)) }, () => `${f.name} · ${fileSize(f.size)}`))) : null);
331
+ };
332
+ },
333
+ });
334
+
335
+ // ───────────────────────── DatePicker (adds range) and DateTimePicker ─────────────────────────
336
+ const pad = n => String(n).padStart(2, '0');
337
+ const RangePicker = defineComponent({
338
+ name: 'UiRangePicker', props: { value: Object, label: String, hint: String, error: String, placeholder: String, disabled: Boolean, locale: String, weekStartsOn: Number, size: String }, emits: ['pick'],
339
+ setup(props, { emit }) {
340
+ const open = ref(false);
341
+ const fmt = d => new Intl.DateTimeFormat(props.locale || 'en-GB', { dateStyle: 'medium' }).format(d);
342
+ return () => {
343
+ const v = props.value || {}, text = v.start ? fmt(v.start) + (v.end ? ' – ' + fmt(v.end) : ' – …') : props.placeholder || 'Select dates';
344
+ return h('div', { class: 'zg-field', 'data-size': props.size }, props.label ? h('span', { class: 'zg-label' }, props.label) : null,
345
+ h('div', { class: 'zg-select' }, h('button', { type: 'button', class: cx('zg-btn zg-select__trigger', props.error && 'zg-select__trigger--error'), disabled: props.disabled, 'aria-expanded': String(open.value), onClick: () => { open.value = !open.value; } }, h('span', null, text), h(N('Icon'), { name: 'calendar', size: 16 })),
346
+ open.value ? h('div', { class: 'zg-select__list', 'data-zg-dd': '', style: { padding: '12px' }, ref: ddRef(() => { open.value = false; }) },
347
+ h(N('RangeCalendar'), { modelValue: v.start ? [v.start, v.end || null] : [], locale: props.locale, weekStartsOn: props.weekStartsOn, onChange: r => { emit('pick', { start: r[0] || null, end: r[1] || null }); if (r[0] && r[1]) open.value = false; } })) : null),
348
+ props.error ? h('span', { class: 'zg-hint zg-hint--error', role: 'alert' }, props.error) : props.hint ? h('span', { class: 'zg-hint' }, props.hint) : null);
349
+ };
350
+ },
351
+ });
352
+ const BaseDatePicker = generated.DatePicker;
353
+ export const UiDatePicker = defineComponent({
354
+ name: 'UiDatePicker', inheritAttrs: false, emits: ['update:modelValue'],
355
+ setup(_, { attrs, emit }) {
356
+ const t = useT(), locale = useLocale();
357
+ return () => {
358
+ const a = camelizeKeys(attrs);
359
+ if (!truthy(a.range)) return h(BaseDatePicker, { ...attrs, locale: attrs.locale === undefined ? locale() : attrs.locale, 'onUpdate:modelValue': v => emit('update:modelValue', v) });
360
+ return h(RangePicker, { value: a.modelValue, label: a.label, hint: a.hint, error: typeof a.error === 'string' ? a.error : undefined, placeholder: a.placeholder || t('common.selectDates', 'Select dates'), disabled: truthy(a.disabled), locale: a.locale === undefined ? locale() : a.locale, weekStartsOn: a.weekStartsOn, size: mapSize('control', a.size || 'lg'), onPick: r => emit('update:modelValue', r) });
361
+ };
362
+ },
363
+ });
364
+ export const UiDateTimePicker = defineComponent({
365
+ name: 'UiDateTimePicker', inheritAttrs: false, emits: ['update:modelValue'],
366
+ setup(_, { attrs, emit }) {
367
+ const t = useT(), locale = useLocale(), buffer = ref('');
368
+ const combine = (d, time) => { const m = /^(\d{1,2}):(\d{1,2})$/.exec(time || ''); return new Date(d.getFullYear(), d.getMonth(), d.getDate(), m ? Math.min(23, +m[1]) : 0, m ? Math.min(59, +m[2]) : 0).toISOString(); };
369
+ return () => {
370
+ const a = camelizeKeys(attrs), d = a.modelValue ? new Date(a.modelValue) : null, valid = d && !Number.isNaN(d.getTime());
371
+ const date = valid ? d : null, time = valid ? `${pad(d.getHours())}:${pad(d.getMinutes())}` : buffer.value;
372
+ const defaultTime = a.defaultTime === undefined ? '12:00' : a.defaultTime;
373
+ const err = a.error === true ? ' ' : a.error || undefined;
374
+ const root = {}; for (const k of Object.keys(attrs)) if (/^(aria|data)-|^(class|style|id)$/.test(k)) root[k] = attrs[k];
375
+ return h('div', { ...root, class: cx('zg-field', root.class) },
376
+ a.label ? h('span', { class: 'zg-label' }, a.label) : null,
377
+ h('div', { style: { display: 'grid', gridTemplateColumns: '1fr 140px', gap: '8px' } },
378
+ h(N('DatePicker'), { modelValue: date, size: 'xl', clearable: true, locale: locale(), disabled: truthy(a.disabled), error: err ? ' ' : undefined, placeholder: a.placeholder || t('common.datePlaceholder', 'dd/mm/yyyy'), onChange: nd => emit('update:modelValue', nd ? combine(nd, time || defaultTime || '00:00') : null) }),
379
+ h(N('TextField'), {
380
+ modelValue: time, size: 'xl', icon: 'clock', placeholder: '23:59', maxlength: 5, inputmode: 'numeric', disabled: truthy(a.disabled), error: err ? ' ' : undefined,
381
+ onChange: v => { let val = v; if (/^\d{2}$/.test(val) && !val.includes(':')) val += ':'; buffer.value = val; if (date && /^\d{1,2}:\d{1,2}$/.test(val)) emit('update:modelValue', combine(date, val)); },
382
+ onBlur: () => { const dg = buffer.value.replace(/\D/g, '').slice(0, 4); if (dg.length >= 3) { buffer.value = `${dg.slice(0, 2)}:${dg.slice(2).padEnd(2, '0')}`; if (date) emit('update:modelValue', combine(date, buffer.value)); } },
383
+ })),
384
+ err && err.trim() ? h('span', { class: 'zg-hint zg-hint--error', role: 'alert' }, err) : a.hint ? h('span', { class: 'zg-hint' }, a.hint) : null);
385
+ };
386
+ },
387
+ });
388
+
389
+ // ───────────────────────── Tooltip (teleported, so overflow:hidden parents never clip it) ─────────────────────────
390
+ export const UiTooltip = defineComponent({
391
+ name: 'UiTooltip', props: { text: String, placement: { type: String, default: 'top' }, tone: { type: String, default: 'ink' }, delay: { type: Number, default: 0 }, disabled: Boolean, anchorClass: { type: String, default: 'inline-flex' } },
392
+ setup(props, { slots }) {
393
+ const anchor = ref(null), tip = ref(null), shown = ref(false), pos = ref({ top: '0px', left: '0px', visibility: 'hidden' });
394
+ let timer = 0;
395
+ const place = () => {
396
+ const a = anchor.value, f = tip.value; if (!a || !f) return;
397
+ const r = a.getBoundingClientRect(), w = f.offsetWidth, hh = f.offsetHeight, gap = 8, pad = 4;
398
+ const fit = { top: r.top - gap - hh >= pad, bottom: r.bottom + gap + hh <= innerHeight - pad, left: r.left - gap - w >= pad, right: r.right + gap + w <= innerWidth - pad };
399
+ let side = props.placement; if (!fit[side]) { const o = { top: 'bottom', bottom: 'top', left: 'right', right: 'left' }[side]; if (o && fit[o]) side = o; }
400
+ const cx0 = r.left + r.width / 2 - w / 2, cy0 = r.top + r.height / 2 - hh / 2;
401
+ const [x, y] = side === 'top' ? [cx0, r.top - gap - hh] : side === 'bottom' ? [cx0, r.bottom + gap] : side === 'left' ? [r.left - gap - w, cy0] : [r.right + gap, cy0];
402
+ pos.value = { top: Math.max(pad, Math.min(y, innerHeight - hh - pad)) + 'px', left: Math.max(pad, Math.min(x, innerWidth - w - pad)) + 'px', visibility: 'visible' };
403
+ };
404
+ const show = () => { if (props.disabled) return; clearTimeout(timer); timer = setTimeout(() => { shown.value = true; Promise.resolve().then(() => requestAnimationFrame(place)); }, props.delay); };
405
+ const hide = () => { clearTimeout(timer); shown.value = false; pos.value = { ...pos.value, visibility: 'hidden' }; };
406
+ onBeforeUnmount(() => clearTimeout(timer));
407
+ return () => [
408
+ h('span', { ref: anchor, class: props.anchorClass, onMouseenter: show, onMouseleave: hide, onFocusin: show, onFocusout: hide }, slots.default && slots.default()),
409
+ shown.value && !props.disabled && (slots.content || props.text) ? h(Teleport, { to: 'body' }, h('div', { ref: tip, role: 'tooltip', class: cx('zg-tipbox', props.tone === 'glass' && 'zg-tipbox--glass'), style: pos.value }, slots.content ? slots.content() : props.text)) : null,
410
+ ];
411
+ },
412
+ });
413
+
414
+ // ───────────────────────── small ones ─────────────────────────
415
+ export const UiAvatarGroup = defineComponent({
416
+ name: 'UiAvatarGroup', props: { spacing: { type: String, default: 'normal' }, reverse: Boolean },
417
+ setup(props, { slots }) { return () => h('div', { class: cx('zg-avgroup', 'zg-avgroup--' + props.spacing, props.reverse && 'zg-avgroup--rev') }, slots.default && slots.default()); },
418
+ });
419
+ export const UiForm = defineComponent({
420
+ name: 'UiForm', props: { novalidate: Boolean }, emits: ['submit'],
421
+ setup(props, { slots, emit }) { return () => h('form', { novalidate: props.novalidate, onSubmit: e => { e.preventDefault(); emit('submit', e); } }, slots.default && slots.default()); },
422
+ });
423
+ const LANGS = [['ru', 'RU'], ['uz', 'UZ'], ['en', 'EN']];
424
+ export const UiMultilangInput = defineComponent({
425
+ name: 'UiMultilangInput', props: { modelValue: { type: Object, default: () => ({}) }, label: String, placeholder: String, required: Boolean, error: [String, Boolean], disabled: Boolean }, emits: ['update:modelValue'],
426
+ setup(props, { emit }) {
427
+ const active = ref('ru');
428
+ return () => {
429
+ const v = props.modelValue || {}, invalid = props.required && !LANGS.some(([c]) => (v[c] || '').trim());
430
+ return h('div', { class: 'zg-field' },
431
+ h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px' } }, props.label ? h('span', { class: 'zg-label' }, props.label, props.required ? h('span', { class: 'zg-req' }, ' *') : null) : h('span'),
432
+ h(N('SegmentedControl'), { size: 'sm', modelValue: active.value, items: LANGS.map(([c, l]) => ({ value: c, label: (v[c] || '').trim() && c !== active.value ? l + ' •' : l })), onChange: c => { if (!props.disabled) active.value = c; } })),
433
+ h(N('TextField'), { modelValue: v[active.value] || '', placeholder: props.placeholder, disabled: props.disabled, size: 'lg', error: props.error === true || invalid ? ' ' : props.error || undefined, onChange: s => emit('update:modelValue', { ...v, [active.value]: s }) }),
434
+ typeof props.error === 'string' && props.error ? h('span', { class: 'zg-hint zg-hint--error', role: 'alert' }, props.error) : null);
435
+ };
436
+ },
437
+ });
438
+
439
+ // ───────────────────────── theme ─────────────────────────
440
+ const LEGACY_KEY = 'glass-ui-theme', themeRef = ref('light');
441
+ let themeInit = false;
442
+ /** Drop-in for the old useTheme(): { theme, setTheme, toggleTheme }. Also toggles the .dark class (for Tailwind dark:) next to the new data-theme attribute. Reads the old localStorage key once. */
443
+ export function useTheme() {
444
+ const ui = useUiTheme();
445
+ if (!themeInit && typeof window !== 'undefined') {
446
+ themeInit = true;
447
+ const sync = tm => { themeRef.value = tm; document.documentElement.classList.toggle('dark', tm === 'dark'); try { localStorage.setItem(LEGACY_KEY, tm); } catch { /* storage blocked */ } };
448
+ let legacy = null, hasNew = false;
449
+ try { legacy = localStorage.getItem(LEGACY_KEY); hasNew = !!JSON.parse(localStorage.getItem('zg-ui') || '{}').theme; } catch { /* storage blocked */ }
450
+ if (!hasNew) ui.setTheme(legacy === 'light' || legacy === 'dark' ? legacy : (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'));
451
+ sync(ui.theme);
452
+ ui.subscribe(s => sync(s.theme));
453
+ }
454
+ return { theme: readonly(themeRef), setTheme: tm => ui.setTheme(tm), toggleTheme: () => ui.toggleTheme() };
455
+ }
456
+
457
+ // ───────────────────────── exports ─────────────────────────
458
+ const all = {
459
+ ...Object.fromEntries(Object.entries(generated).map(([k, v]) => ['Ui' + k, v])),
460
+ UiDatePicker, UiTabs, UiTabsList, UiTab, UiTabPanel, UiRadio, UiRadioGroup, UiDropdown, UiMenu, UiMenuItem, UiMenuSeparator, UiTable, UiSidebar, UiSidebarSection, UiSidebarItem,
461
+ UiNavbar, UiSubNav, UiToastContainer, UiCommandPalette, UiFileUpload, UiDateTimePicker, UiAvatarGroup, UiForm, UiMultilangInput, UiTooltip,
462
+ };
463
+ export const { UiButton, UiInput, UiTextarea, UiSelect, UiCombobox, UiCheckbox, UiSwitch, UiBadge, UiAlert, UiSkeleton, UiModal, UiConfirmModal, UiCard, UiProgress, UiBreadcrumbs, UiPagination, UiStatsCard, UiSegmentedControl, UiAvatar, UiCalendar, UiSpinner, UiDivider, UiBanner, UiEmptyState, UiDrawer, UiTag, UiOtpInput, UiKpiCard, UiFormField } = all;
464
+ export const MultilangInput = UiMultilangInput;
465
+ export const compatComponents = all;
466
+ export const install = app => { for (const [n, c] of Object.entries(all)) app.component(n, c); };
467
+ export default { install };
468
+ export { mapSize };