@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,282 @@
1
+ // @ziyarago/ui/compat · declarative old-API to new-API translation tables. Pure data and small pure functions (no Vue, no DOM), so it is unit-testable in Node.
2
+ // Old API = glass-ui-kit (@ziyarago/glass-ui-kit, Vue 3 + Tailwind, Ui* components). New API = @ziyarago/ui.
3
+ // A spec describes ONE old component:
4
+ // to new component name (string) or (src) => name
5
+ // defaults old-API defaults applied before translation (old prop values, not new ones)
6
+ // size size profile ('control' | 'shift' | 'same'); maps the old size onto the xs..xl scale, see SIZES
7
+ // props per old prop: undefined = pass through unchanged, string = rename, null = unsupported (dropped, dev warning), false = consumed elsewhere (dropped silently),
8
+ // { to, map } = rename and map values, (value, src, cfg) => object = set any new props
9
+ // events new event -> list of [oldEvent, fixedValue?] to emit; without fixedValue the original arguments are forwarded, NONE emits without arguments
10
+ // eventsWhen new event -> (src) => boolean, the handler is attached only when true (the new kit shows UI such as a close button only when a handler exists)
11
+ // slots old slot -> { into: 'default', at: 'start' | 'end', unless?: oldProp } (merge into the default slot) or a new slot name (rename)
12
+ // after (newProps, src, cfg) => void, last-chance adjustments
13
+ import { BOOL_PROPS } from '../core/bools.js';
14
+
15
+ export const config = { sizes: 'height', warn: true, t: null, legacyIcons: true, primary: 'primary' };
16
+
17
+ /** Global compat options. sizes: 'height' (default) keeps old pixel heights, 'name' maps sm/md/lg/xl to the same names. t: (key, fallback) => string, i18n bridge. warn: false silences dev warnings. legacyIcons: false disables the lucide rename table (use it after setIconBase to a lucide-static >= 1 build). primary: 'inverse' renders the old primary button (black pill in qrbon) with the new inverse variant instead of the brand-colored primary. */
18
+ export function configureCompat(o = {}) { return Object.assign(config, o); }
19
+
20
+ const camel = k => /^(aria|data)-/.test(k) || !k.includes('-') ? k : k.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
21
+ export const camelizeKeys = o => { const r = {}; for (const k of Object.keys(o)) if (o[k] !== undefined) r[camel(k)] = o[k]; return r; };
22
+
23
+ // Old kit control heights: sm 36, md 48, lg 56, xl 64 px. New: xs 28, sm 32, md 40, lg 48, xl 56 px.
24
+ export const SIZES = {
25
+ control: { height: { sm: 'md', md: 'lg', lg: 'xl', xl: 'xl' }, name: { sm: 'sm', md: 'md', lg: 'lg', xl: 'xl' } },
26
+ shift: { height: { sm: 'md', md: 'lg', lg: 'xl' }, name: { sm: 'sm', md: 'md', lg: 'lg' } },
27
+ same: { height: {}, name: {} },
28
+ };
29
+ export const mapSize = (profile, v, cfg = config) => (SIZES[profile] && SIZES[profile][cfg.sizes === 'name' ? 'name' : 'height'][v]) || v;
30
+
31
+ // lucide-vue-next 1.x renamed a few icons that lucide-static@0.460 (the kit's default icon CDN version) still knows under the old name.
32
+ export const ICON_RENAMES = { 'circle-question-mark': 'circle-help', 'text-align-start': 'align-left', 'text-align-center': 'align-center', 'text-align-end': 'align-right', 'text-align-justify': 'align-justify', 'file-code-corner': 'file-code-2', 'file-check-corner': 'file-check-2', funnel: 'filter', 'message-circle-question-mark': 'message-circle-question' };
33
+ const old = n => (config.legacyIcons === false ? n : ICON_RENAMES[n] || n);
34
+
35
+ /** Lucide component (lucide-vue-next) or string -> kebab-case icon name used by the new kit. Unknown shapes give undefined. */
36
+ export function iconName(c) {
37
+ if (!c) return undefined;
38
+ if (typeof c === 'string') return c;
39
+ try {
40
+ if (typeof c === 'function') { const v = c({}, { slots: {}, attrs: {} }); if (v && v.props && typeof v.props.name === 'string') return old(v.props.name); }
41
+ const n = c.name || c.displayName || c.__name;
42
+ if (n) return old(String(n).replace(/^Lucide/, '').replace(/([a-z])([A-Z0-9])/g, '$1-$2').replace(/([0-9])([A-Z])/g, '$1-$2').toLowerCase());
43
+ } catch { /* not an icon component */ }
44
+ return undefined;
45
+ }
46
+
47
+ /** Marker for events entries: emit the old event without arguments (the new kit passes a DOM event the old one never had). */
48
+ export const NONE = Symbol('none');
49
+ const err = v => (v === true ? { error: ' ' } : v ? { error: v } : {});
50
+ const BADGE_TONE = { sunshine: 'brand', cobalt: 'brand', petrol: 'info', ink: 'inverse', glass: undefined, neutral: undefined };
51
+ const ALERT_TONE = { success: 'success', warning: 'warning', danger: 'danger', info: 'info', glass: 'info', neutral: 'info' };
52
+ const CARD_TONE = { cobalt: 'brand', sunshine: 'warning', petrol: 'sky', ink: 'neutral' };
53
+ const AVATAR_TONE = { cobalt: 'brand', sunshine: 'warning', petrol: 'sky', pink: 'danger', ink: 'neutral' };
54
+ const PROGRESS_TONE = { success: 'success', warning: 'warning', danger: 'danger', sunshine: 'warning' };
55
+ const MODAL_SIZE = { sm: { size: 'sm' }, md: { size: 'md' }, lg: { size: 'lg' }, xl: { size: 'xl' }, '2xl': { size: 'xl' }, '3xl': { size: 'xl', width: '768px' }, '4xl': { size: 'xl', width: '896px' }, '5xl': { size: 'xl', width: '1024px' } };
56
+ const CARD_PADDING = { height: { none: 'md', sm: 'md', md: 'lg', lg: 'lg', xl: 'xl' }, name: { none: 'md', sm: 'sm', md: 'md', lg: 'lg', xl: 'xl' } };
57
+ const num = v => (Number.isFinite(+v) ? +v : 0);
58
+
59
+ export const SPECS = {
60
+ Button: {
61
+ to: 'Button', size: 'control', defaults: { variant: 'primary', size: 'lg' },
62
+ props: { variant: (v, s, cfg) => ({ glass: { variant: 'secondary' }, gradient: { variant: 'tonal', tone: 'brand' }, ink: { variant: 'inverse' } }[v] || { variant: v === 'primary' && cfg.primary === 'inverse' ? 'inverse' : v }) },
63
+ slots: { 'icon-left': { into: 'default', at: 'start', unless: 'loading' }, 'icon-right': { into: 'default', at: 'end', unless: 'loading' } },
64
+ },
65
+ Input: {
66
+ to: 'TextField', size: 'control', defaults: { size: 'lg' },
67
+ props: { error: err, help: 'hint', clearable: null },
68
+ },
69
+ Textarea: {
70
+ to: 'Textarea', defaults: { size: 'lg', resize: false },
71
+ props: { error: err },
72
+ },
73
+ Select: {
74
+ to: 'Select', size: 'control', defaults: { size: 'lg' },
75
+ props: { error: err },
76
+ t: { placeholder: ['common.select', 'Select an option'] },
77
+ },
78
+ Combobox: {
79
+ to: 'Combobox', size: 'control', defaults: { size: 'lg' },
80
+ props: { error: err },
81
+ t: { emptyText: ['common.noMatches', 'No matches'] },
82
+ },
83
+ Checkbox: { to: 'Checkbox', props: { error: err } },
84
+ Switch: { to: 'Switch', props: { error: err } },
85
+ Badge: {
86
+ to: 'Badge', size: 'shift', defaults: { tone: 'sunshine', size: 'md' },
87
+ props: { tone: v => (v in BADGE_TONE ? (BADGE_TONE[v] ? { tone: BADGE_TONE[v] } : {}) : { tone: v }) },
88
+ slots: { 'icon-left': { into: 'default', at: 'start' }, 'icon-right': { into: 'default', at: 'end' } },
89
+ },
90
+ Alert: {
91
+ to: 'Alert', defaults: { tone: 'info' },
92
+ props: { tone: v => ({ tone: ALERT_TONE[v] || 'info' }), closable: false, dismissible: false },
93
+ events: { close: [['close', NONE]] }, eventsWhen: { close: s => !!s.closable },
94
+ },
95
+ Skeleton: {
96
+ to: 'Skeleton',
97
+ props: { variant: v => (v === 'line' ? { variant: 'rect', height: '1px' } : { variant: v }), lastLineWidth: null },
98
+ },
99
+ Modal: {
100
+ to: 'Modal', defaults: { size: 'md' },
101
+ props: { size: v => MODAL_SIZE[v] || { size: v } },
102
+ events: { close: [['update:open', false], ['close', NONE]] },
103
+ },
104
+ ConfirmModal: {
105
+ to: 'ConfirmDialog', defaults: { confirmVariant: 'danger', size: 'sm' },
106
+ props: { message: 'description', confirmVariant: v => ({ tone: v === 'danger' ? 'danger' : 'default' }) },
107
+ t: { title: ['common.areYouSure', 'Are you sure?'], description: ['common.irreversible', 'This action cannot be undone.'], confirmLabel: ['common.delete', 'Delete'], cancelLabel: ['common.cancel', 'Cancel'] },
108
+ },
109
+ Card: {
110
+ to: 'Card', defaults: { padding: 'lg', variant: 'glass' },
111
+ props: {
112
+ variant: (v, s) => ({ gradient: { variant: 'gradient' }, outline: { variant: 'outline' }, block: { variant: 'gradient', intensity: 'vivid', tone: CARD_TONE[s.tone] || 'brand' } }[v] || {}),
113
+ tone: false, as: null,
114
+ padding: (v, s, cfg) => ({ size: CARD_PADDING[cfg.sizes === 'name' ? 'name' : 'height'][v] || 'lg', ...(v === 'none' ? { padding: 'none' } : {}) }),
115
+ },
116
+ },
117
+ Progress: {
118
+ to: 'Progress', defaults: { tone: 'ink' },
119
+ props: {
120
+ value: (v, s) => (v === undefined ? {} : { value: Math.max(0, Math.min(100, (num(v) / (num(s.max) || 100)) * 100)) }),
121
+ max: false,
122
+ showValue: (v, s) => (v && s.value !== undefined ? { meta: Math.round(Math.max(0, Math.min(100, (num(s.value) / (num(s.max) || 100)) * 100))) + '%' } : {}),
123
+ tone: v => (PROGRESS_TONE[v] ? { tone: PROGRESS_TONE[v] } : {}),
124
+ },
125
+ },
126
+ Breadcrumbs: { to: 'Breadcrumbs' },
127
+ Pagination: {
128
+ to: 'Pagination', props: { current: 'page', total: 'pages' },
129
+ events: { change: [['update:current']] },
130
+ after: (p, s, cfg) => { if (cfg.t) p.labels = { prev: cfg.t('common.prevPage', 'Previous page'), next: cfg.t('common.nextPage', 'Next page') }; },
131
+ },
132
+ StatsCard: {
133
+ to: s => (s.variant === 'block' ? 'KpiTile' : 'StatsCard'),
134
+ props: {
135
+ icon: v => ({ icon: iconName(v) }),
136
+ variant: (v, s) => (v === 'block' ? { variant: s.tone === 'ink' ? 'inverse' : 'brand' } : {}),
137
+ tone: false,
138
+ delta: (v, s) => {
139
+ if (v == null) return {};
140
+ const d = typeof v === 'object' ? v : { value: v };
141
+ const down = d.trend === 'down', text = String(d.value);
142
+ return { delta: s.variant === 'block' && down && !text.startsWith('-') ? '-' + text : text, trend: down ? 'down' : 'up', ...(d.sublabel ? { sub: d.sublabel } : {}) };
143
+ },
144
+ },
145
+ },
146
+ SegmentedControl: {
147
+ to: 'SegmentedControl', size: 'shift',
148
+ props: { options: v => ({ items: (v || []).map(o => (o.icon ? { ...o, icon: iconName(o.icon) } : o)) }) },
149
+ },
150
+ Avatar: {
151
+ to: 'Avatar', defaults: { size: 'md' },
152
+ props: { alt: 'name', tone: v => ({ tone: AVATAR_TONE[v] || 'brand' }), shape: v => (v === 'squircle' ? { shape: 'rounded' } : {}) },
153
+ },
154
+ Calendar: { to: 'Calendar', props: { range: null, initialMonth: null } },
155
+ DatePicker: {
156
+ to: 'DatePicker', size: 'control', defaults: { size: 'lg', clearable: true },
157
+ props: { error: err, range: false },
158
+ },
159
+ Spinner: { to: 'Spinner', props: { tone: null } },
160
+ Divider: { to: 'Divider', props: { align: null, orientation: null, tone: null } },
161
+ Banner: {
162
+ to: 'Banner', props: { tone: v => ({ tone: ALERT_TONE[v] || 'info' }), closable: false, sticky: null },
163
+ events: { close: [['close', NONE]] }, eventsWhen: { close: s => !!s.closable },
164
+ },
165
+ EmptyState: { to: 'EmptyState', props: { icon: v => ({ icon: iconName(v) }), tone: null, size: null }, slots: { actions: { into: 'default', at: 'end' } } },
166
+ Drawer: {
167
+ to: 'Drawer', defaults: { placement: 'right' },
168
+ props: { placement: v => ({ side: v === 'left' ? 'start' : 'end' }), size: null, closable: null, closeOnBackdrop: null, closeOnEsc: null },
169
+ events: { close: [['update:open', false], ['close', NONE]] },
170
+ },
171
+ Tag: {
172
+ to: 'Tag', props: { tone: null, size: null, removable: false, disabled: null },
173
+ slots: { 'icon-left': { into: 'default', at: 'start' } },
174
+ events: { remove: [['remove', NONE]] }, eventsWhen: { remove: s => !!s.removable },
175
+ },
176
+ OtpInput: { to: 'CodeInput', props: { masked: null, disabled: null, error: null, hint: null } },
177
+ KpiCard: {
178
+ to: 'KpiTile',
179
+ props: {
180
+ secondary: 'sub', icon: null, tone: null, loading: null,
181
+ delta: v => (v == null ? {} : { delta: String(typeof v === 'object' ? v.value : v) }),
182
+ },
183
+ },
184
+ FormField: { to: 'FormField' },
185
+ };
186
+
187
+ /** Old components (and old-kit names) that exist only as hand-written compat components in index.js. */
188
+ export const CUSTOM = [
189
+ 'Tabs', 'TabsList', 'Tab', 'TabPanel', 'Radio', 'RadioGroup', 'Dropdown', 'Menu', 'MenuItem', 'MenuSeparator', 'Table', 'Sidebar', 'SidebarSection', 'SidebarItem',
190
+ 'Navbar', 'SubNav', 'ToastContainer', 'CommandPalette', 'FileUpload', 'DateTimePicker', 'AvatarGroup', 'MultilangInput', 'Form', 'Tooltip',
191
+ ];
192
+
193
+ /** Every compat export name without the Ui prefix (components only; composables are listed in COMPAT_EXPORTS). */
194
+ export const COMPAT_NAMES = [...new Set([...Object.keys(SPECS), ...CUSTOM])].sort();
195
+ export const COMPAT_COMPONENTS = COMPAT_NAMES.map(n => 'Ui' + n);
196
+
197
+ // Names of dev-time notes for props the new kit cannot honour. Used for warnings and documented in docs/MIGRATION.md.
198
+ const warned = new Set();
199
+ export function warnOnce(name, props) {
200
+ if (!config.warn || !props.length || (typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'production')) return;
201
+ for (const p of props) { const key = name + '.' + p; if (!warned.has(key)) { warned.add(key); console.warn(`[@ziyarago/ui/compat] <Ui${name}> prop "${p}" is not supported by the new kit and is ignored.`); } }
202
+ }
203
+
204
+ /** Translate old attrs for the old component `name`. Returns { component, props, dropped, src } or null for unknown names. */
205
+ export function translate(name, attrs = {}, cfg = config) {
206
+ const spec = SPECS[name];
207
+ if (!spec) return null;
208
+ const src = { ...(spec.defaults || {}), ...camelizeKeys(attrs) };
209
+ for (const k of Object.keys(src)) if (src[k] === '' && BOOL_PROPS.has(k)) src[k] = true;
210
+ const props = {}, dropped = [];
211
+ for (const k of Object.keys(src)) {
212
+ const v = src[k], rule = spec.props && Object.prototype.hasOwnProperty.call(spec.props, k) ? spec.props[k] : undefined;
213
+ if (rule === undefined) {
214
+ if (k === 'size' && spec.size) props.size = mapSize(spec.size, v, cfg);
215
+ else props[k] = v;
216
+ } else if (rule === false) continue;
217
+ else if (rule === null) dropped.push(k);
218
+ else if (typeof rule === 'string') props[rule] = v;
219
+ else if (typeof rule === 'function') Object.assign(props, rule(v, src, cfg) || {});
220
+ else { const nv = rule.map && v in rule.map ? rule.map[v] : v; if (nv !== undefined) props[rule.to || k] = nv; }
221
+ }
222
+ if (spec.t) for (const k of Object.keys(spec.t)) if (props[k] === undefined) props[k] = cfg.t ? cfg.t(spec.t[k][0], spec.t[k][1]) : spec.t[k][1];
223
+ if (spec.after) spec.after(props, src, cfg);
224
+ return { component: typeof spec.to === 'function' ? spec.to(src) : spec.to, props, dropped, src };
225
+ }
226
+
227
+ /** Old event names a compat component must declare in `emits` so Vue does not forward them as listeners. */
228
+ export function emitsOf(name) {
229
+ const spec = SPECS[name];
230
+ return spec && spec.events ? [...new Set(Object.values(spec.events).flatMap(t => t.map(e => e[0])))] : [];
231
+ }
232
+
233
+ /** Listener props for the new component: { onClose: fn } that re-emit old events. `emit` is the Vue emit function (or any (name, ...args) => void). */
234
+ export function buildHandlers(name, src, emit) {
235
+ const spec = SPECS[name], out = {};
236
+ if (!spec || !spec.events) return out;
237
+ for (const ev of Object.keys(spec.events)) {
238
+ if (spec.eventsWhen && spec.eventsWhen[ev] && !spec.eventsWhen[ev](src)) continue;
239
+ out['on' + ev[0].toUpperCase() + ev.slice(1)] = (...a) => { for (const [n, ...fixed] of spec.events[ev]) { if (fixed[0] === NONE) emit(n); else emit(n, ...(fixed.length ? fixed : a)); } };
240
+ }
241
+ return out;
242
+ }
243
+
244
+ /** Old slot functions -> new slot functions. `slots` is { name: fn }, `src` the translated old props (for `unless`). Slot functions may receive args. */
245
+ export function translateSlots(name, slots, src = {}) {
246
+ const spec = SPECS[name], rules = (spec && spec.slots) || {}, out = {}, pre = [], post = [];
247
+ for (const k of Object.keys(slots)) {
248
+ const r = rules[k];
249
+ if (r === undefined) out[k] = slots[k];
250
+ else if (typeof r === 'string') out[r] = slots[k];
251
+ else if (!(r.unless && src[r.unless])) (r.at === 'start' ? pre : post).push(slots[k]);
252
+ }
253
+ if (pre.length || post.length) {
254
+ const base = out.default;
255
+ out.default = (...a) => [...pre.map(f => f()), ...(base ? [].concat(base(...a)) : []), ...post.map(f => f())];
256
+ }
257
+ return out;
258
+ }
259
+
260
+ /** Tailwind width utility (w-32, w-[120px], w-1/2, w-full) -> CSS width. Anything else gives undefined. */
261
+ export function twWidth(c) {
262
+ if (!c || typeof c !== 'string') return undefined;
263
+ const m = /(?:^|\s)w-(?:\[([^\]]+)\]|(\d+(?:\.\d+)?)|(full)|(\d+)\/(\d+))(?=\s|$)/.exec(c);
264
+ if (!m) return undefined;
265
+ if (m[1]) return m[1];
266
+ if (m[2]) return +m[2] * 0.25 + 'rem';
267
+ if (m[3]) return '100%';
268
+ return +(100 * m[4] / m[5]).toFixed(4) + '%';
269
+ }
270
+
271
+ /** Client-side sort used by the compat UiTable (the new DataTable is controlled and never sorts). */
272
+ export function sortRows(rows, col, dir, getValue) {
273
+ const d = dir === 'desc' ? -1 : 1;
274
+ return [...rows].sort((a, b) => {
275
+ const av = getValue(a, col), bv = getValue(b, col);
276
+ if (av == null && bv == null) return 0;
277
+ if (av == null) return -1 * d;
278
+ if (bv == null) return 1 * d;
279
+ if (typeof av === 'number' && typeof bv === 'number') return (av - bv) * d;
280
+ return String(av).localeCompare(String(bv)) * d;
281
+ });
282
+ }
@@ -0,0 +1,22 @@
1
+ // unplugin-vue-components resolver for the compat layer: <UiInput>, <UiTable>, <UiSidebar> ... -> import from '@ziyarago/ui/compat'.
2
+ // Components({ dirs: [], resolvers: [ZiyaragoCompatResolver(), ZiyaragoUiResolver({ exclude: [...] })] })
3
+ // Put it BEFORE ZiyaragoUiResolver: names that exist in both kits (UiButton, UiCard, ...) then resolve to the compat wrapper.
4
+ import { COMPAT_COMPONENTS } from './map.js';
5
+
6
+ const NAMES = new Set(COMPAT_COMPONENTS);
7
+
8
+ /** @param {{ from?: string, exclude?: string[], css?: boolean | string }} [options] exclude: full component names to leave alone, e.g. ['UiTable']. */
9
+ export function ZiyaragoCompatResolver(options = {}) {
10
+ const { from = '@ziyarago/ui/compat', exclude = [], css = false } = options;
11
+ const skip = new Set(exclude);
12
+ const sideEffects = css ? (css === true ? '@ziyarago/ui/style.css' : css) : undefined;
13
+ return {
14
+ type: 'component',
15
+ resolve(name) {
16
+ if (!NAMES.has(name) || skip.has(name)) return;
17
+ return { name, from, ...(sideEffects ? { sideEffects } : {}) };
18
+ },
19
+ };
20
+ }
21
+
22
+ export default ZiyaragoCompatResolver;
@@ -0,0 +1,29 @@
1
+ // Vite plugin: redirects imports of the project's local old-kit files (".../components/ui/Toast", ".../components/ui/Select/UiSelect.vue", "@/components/ui/Table", ...)
2
+ // to '@ziyarago/ui/compat', so useToast, TableColumn, SelectOption, UiInput ... keep working without editing hundreds of import lines.
3
+ // import { ziyaragoCompat } from '@ziyarago/ui/compat/vite' plugins: [ziyaragoCompat(), vue(), ...]
4
+ const PREFIX = '\0ziyarago-compat:';
5
+
6
+ /** @param {{ from?: string, ui?: RegExp, exclude?: RegExp[] }} [options] ui: matches the local ui directory in an import path. exclude: import paths to keep local (default: MultilangInput). */
7
+ export function ziyaragoCompat(options = {}) {
8
+ const { from = '@ziyarago/ui/compat', ui = /(?:^|\/)components\/ui\/([A-Za-z]+)(?:\/([A-Za-z]+)(\.vue|\.ts)?)?$/, exclude = [/MultilangInput/] } = options;
9
+ return {
10
+ name: 'ziyarago-ui-compat',
11
+ enforce: 'pre',
12
+ resolveId(source, importer) {
13
+ const norm = source.replace(/\\/g, '/');
14
+ if (!importer || norm.startsWith('\0') || norm.startsWith(from)) return null;
15
+ if (importer.replace(/\\/g, '/').includes('/components/ui/')) return null;
16
+ if (exclude.some(r => r.test(norm))) return null;
17
+ const m = ui.exec(norm);
18
+ if (!m) return null;
19
+ return PREFIX + (m[2] && m[3] === '.vue' ? m[2] : m[1]);
20
+ },
21
+ load(id) {
22
+ if (!id.startsWith(PREFIX)) return null;
23
+ const name = id.slice(PREFIX.length), comp = name.startsWith('Ui') ? name : name === 'Toast' ? 'UiToastContainer' : 'Ui' + name;
24
+ return `export * from '${from}';\nimport * as C from '${from}';\nexport default C['${comp}'];\n`;
25
+ },
26
+ };
27
+ }
28
+
29
+ export default ziyaragoCompat;
@@ -0,0 +1,4 @@
1
+ // Names of props that are boolean in EVERY component that documents them. The Vue adapter turns the bare-attribute form (<UiButton block>, which Vue delivers as '') into true for these.
2
+ // Names that are boolean somewhere and text elsewhere (modelValue, image, brand) are not listed: '' is a legitimate value there.
3
+ // scripts/check-meta.mjs keeps this list in sync with src/meta.
4
+ export const BOOL_PROPS = new Set(['addable', 'allowEmpty', 'autoResize', 'autofocus', 'banner', 'block', 'bold', 'checked', 'clearable', 'closable', 'closeOnBackdrop', 'closeOnEsc', 'collapsed', 'collapsible', 'disabled', 'dot', 'external', 'featured', 'frame', 'ghost', 'glass', 'hideArrows', 'hideIcon', 'highlightLast', 'iconOnly', 'indeterminate', 'interactive', 'labelLeft', 'last', 'loading', 'multi', 'multiple', 'open', 'pill', 'plain', 'readonly', 'required', 'resize', 'ring', 'sanitize', 'selectGroups', 'showCount', 'showForgot', 'showValue', 'spellcheck', 'ticks', 'truncate', 'vertical', 'vip', 'wrap']);
@@ -3,6 +3,9 @@
3
3
  // h(tag, props, ...children) is supplied by the Vue / React adapter. No hooks inside.
4
4
  import { trap } from './dialog.js';
5
5
  export const cx = (...a) => a.filter(Boolean).join(' ');
6
+ // Copies the listed props that are set; used to forward native attributes and listeners to the real <input>/<textarea>.
7
+ export const pick = (p, keys) => { const o = {}; for (const k of keys) if (p[k] !== undefined) o[k] = p[k]; return o; };
8
+ export const INPUT_ATTRS = ['name', 'autocomplete', 'required', 'min', 'max', 'step', 'maxlength', 'minlength', 'inputmode', 'pattern', 'spellcheck', 'autofocus', 'autocapitalize', 'onBlur', 'onFocus', 'onKeydown', 'onKeyDown', 'onKeyup', 'onKeyUp', 'onPaste'];
6
9
  export const DZ = { xs: .82, sm: .91, md: 1, lg: 1.12, xl: 1.25 };
7
10
  // Shared outside-click / Escape close for popups. Put `ref: ddRef(close)` on the popup element; its parent is the component root
8
11
  // (trigger + popup). Listeners attach only in the browser, only while a popup is mounted, and detach when the last one unmounts.
@@ -23,7 +26,7 @@ const ddSync = () => {
23
26
  };
24
27
  export const ddRef = (close, o = {}) => el => {
25
28
  if (typeof document === 'undefined') return;
26
- if (el) { const r = ddReg.find(x => x.el === el); if (r) { r.close = close; r.self = !!o.self; } else ddReg.push({ el, close, self: !!o.self }); ddSync(); }
29
+ if (el) { const r = ddReg.find(x => x.el === el); if (r) { r.close = close; r.self = !!o.self; } else { ddReg.push({ el, close, self: !!o.self }); if (!o.self && el.scrollIntoView) el.scrollIntoView({ block: 'nearest', inline: 'nearest' }); } ddSync(); }
27
30
  else Promise.resolve().then(ddPrune);
28
31
  };
29
32
  export const dzp = (p, b) => Math.round(b * (DZ[p.size] || 1));
@@ -51,41 +54,46 @@ const nav = (h, it, cur, onChange, collapsed) => h('button', { key: it.value, cl
51
54
  it.icon ? icon(h, it.icon, 18) : null, h('span', null, it.label), it.count ? h('span', { class: 'zg-navitem__count' }, it.count) : null);
52
55
 
53
56
  export const components = {
54
- Button: { render: ({ h, props: p, children }) => h('button', { type: 'button', class: cx('zg-btn', 'zg-btn--' + (p.variant || 'secondary'), p.size && 'zg-btn--' + p.size, p.block && 'zg-btn--block', p.tone && 'zg-t-' + p.tone), disabled: p.disabled || p.loading, onClick: p.onClick },
57
+ Button: { render: ({ h, props: p, children }) => h('button', { type: p.type || 'button', title: p.title, class: cx('zg-btn', 'zg-btn--' + (p.variant || 'secondary'), p.size && 'zg-btn--' + p.size, p.block && 'zg-btn--block', p.iconOnly && 'zg-btn--sq', p.tone && 'zg-t-' + p.tone), disabled: p.disabled || p.loading, onClick: p.onClick },
55
58
  p.loading ? h('span', { class: 'zg-spin' }) : p.icon ? icon(h, p.icon, 16) : null, ...children, p.iconRight ? icon(h, p.iconRight, 16) : null) },
56
59
  IconButton: { render: ({ h, props: p }) => h('button', { type: 'button', 'aria-label': p.label, class: cx('zg-ibtn', p.size && 'zg-ibtn--' + p.size, p.ghost && 'zg-ibtn--ghost'), onClick: p.onClick }, icon(h, p.icon)) },
57
- Badge: { render: ({ h, props: p, children }) => h('span', { class: cx('zg-badge', p.tone && 'zg-badge--' + p.tone) }, p.dot ? h('span', { class: 'zg-badge__dot' }) : null, ...children) },
58
- Avatar: { render: ({ h, props: p }) => h('span', { class: 'zg-avatar', style: { '--s': ({ xs: 24, sm: 32, md: 40, lg: 56, xl: 72 }[p.size] || p.size || 40) + 'px' }, title: p.name }, p.src ? h('img', { src: p.src, alt: p.name || '' }) : (p.name || '?').split(/\s+/).slice(0, 2).map(w => w[0]).join('').toUpperCase()) },
59
- Switch: { render: ({ h, props: p }) => h('button', { type: 'button', role: 'switch', class: 'zg-switch', 'aria-checked': String(!!p.modelValue), disabled: p.disabled, 'aria-disabled': p.disabled ? 'true' : undefined, onClick: () => !p.disabled && p.onChange && p.onChange(!p.modelValue) }, h('span', { class: 'zg-switch__track' }, h('span', { class: 'zg-switch__thumb' })), p.label || null) },
60
- Checkbox: { render: ({ h, props: p }) => h('button', { type: 'button', role: 'checkbox', class: 'zg-check', 'aria-checked': String(!!p.modelValue), disabled: p.disabled, 'aria-disabled': p.disabled ? 'true' : undefined, onClick: () => !p.disabled && p.onChange && p.onChange(!p.modelValue) }, h('span', { class: 'zg-check__box' }, p.modelValue ? icon(h, 'check', 14) : null), p.label || null) },
61
- SegmentedControl: { render: ({ h, props: p }) => h('div', { class: cx('zg-seg', p.size === 'sm' && 'zg-seg--sm'), role: 'group' }, ...(p.items || []).map(it => h('button', { key: it.value, type: 'button', 'aria-pressed': String(it.value === p.modelValue), onClick: () => p.onChange && p.onChange(it.value) }, it.label))) },
60
+ Badge: { render: ({ h, props: p, children }) => h('span', { class: cx('zg-badge', p.tone && 'zg-badge--' + p.tone, p.shape === 'square' && 'zg-badge--sq') }, p.dot ? h('span', { class: 'zg-badge__dot' }) : null, ...children) },
61
+ Avatar: { render: ({ h, props: p }) => h('span', { class: cx('zg-avatar', p.shape === 'rounded' && 'zg-avatar--sq', p.ring && 'zg-avatar--ring', p.status && 'zg-avatar--st', p.tone && 'zg-avatar--' + p.tone), style: { '--s': ({ xs: 24, sm: 32, md: 40, lg: 56, xl: 72 }[p.size] || p.size || 40) + 'px' }, title: p.name },
62
+ p.src ? h('img', { src: p.src, alt: p.name || '' }) : p.initials ? String(p.initials).slice(0, 3).toUpperCase() : (p.name || '?').split(/\s+/).slice(0, 2).map(w => w[0]).join('').toUpperCase(),
63
+ p.status ? h('i', { class: 'zg-avatar__st zg-avatar__st--' + p.status, role: 'img', 'aria-label': p.status }) : null) },
64
+ Switch: { render: ({ h, props: p }) => { const txt = p.label || p.hint || p.error ? h('span', null, p.label || null, p.hint && !p.error ? h('span', { class: 'zg-hint', style: { display: 'block' } }, p.hint) : null, p.error ? h('span', { class: 'zg-hint zg-hint--error', role: 'alert', style: { display: 'block' } }, p.error) : null) : null, tr = h('span', { class: 'zg-switch__track' }, h('span', { class: 'zg-switch__thumb' }));
65
+ return h('button', { type: 'button', role: 'switch', class: 'zg-switch', 'aria-checked': String(!!p.modelValue), disabled: p.disabled, 'aria-disabled': p.disabled ? 'true' : undefined, onClick: () => !p.disabled && p.onChange && p.onChange(!p.modelValue) }, ...(p.labelLeft ? [txt, tr] : [tr, txt])); } },
66
+ Checkbox: { render: ({ h, props: p }) => h('button', { type: 'button', role: 'checkbox', class: cx('zg-check', p.error && 'zg-check--error'), 'aria-checked': p.indeterminate ? 'mixed' : String(!!p.modelValue), disabled: p.disabled, 'aria-disabled': p.disabled ? 'true' : undefined, onClick: () => !p.disabled && p.onChange && p.onChange(p.indeterminate ? true : !p.modelValue) }, h('span', { class: 'zg-check__box' }, p.indeterminate ? icon(h, 'minus', 14) : p.modelValue ? icon(h, 'check', 14) : null),
67
+ p.label || p.hint || p.error ? h('span', null, p.label || null, p.hint && !p.error ? h('span', { class: 'zg-hint', style: { display: 'block' } }, p.hint) : null, p.error && typeof p.error === 'string' ? h('span', { class: 'zg-hint zg-hint--error', role: 'alert', style: { display: 'block' } }, p.error) : null) : null) },
68
+ SegmentedControl: { render: ({ h, props: p }) => h('div', { class: cx('zg-seg', p.size === 'sm' && 'zg-seg--sm', p.block && 'zg-seg--block'), role: 'group', 'aria-label': p.ariaLabel }, ...(p.items || []).map(it => h('button', { key: it.value, type: 'button', disabled: it.disabled, title: p.iconOnly ? it.label : undefined, 'aria-label': p.iconOnly && it.icon ? it.label : undefined, 'aria-pressed': String(it.value === p.modelValue), onClick: () => !it.disabled && p.onChange && p.onChange(it.value) }, it.icon ? icon(h, it.icon, 16) : null, p.iconOnly && it.icon ? null : it.label))) },
62
69
  Tabs: { render: ({ h, props: p }) => h('div', { class: 'zg-tabs', role: 'tablist' }, ...(p.items || []).map(it => h('button', { key: it.value, role: 'tab', 'aria-selected': String(it.value === p.modelValue), onClick: () => p.onChange && p.onChange(it.value) }, it.label))) },
63
70
  FilterChips: { render: ({ h, props: p }) => { const sel = p.multi ? (p.modelValue || []) : [p.modelValue]; return h('div', { class: 'zg-chips' }, ...(p.items || []).map(it => h('button', { key: it.value, type: 'button', class: 'zg-chip', 'aria-pressed': String(sel.includes(it.value)), onClick: () => { if (!p.onChange) return; p.onChange(p.multi ? (sel.includes(it.value) ? sel.filter(v => v !== it.value) : [...sel, it.value]) : it.value); } }, it.label))); } },
64
- TextField: { render: ({ h, props: p }) => h('label', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label) : null,
65
- h('span', { class: cx('zg-input', p.error && 'zg-input--error', p.variant && 'zg-input--' + p.variant, p.density === 'compact' && 'zg-input--compact', p.disabled && 'zg-input--disabled') }, p.icon ? icon(h, p.icon, 18) : null, h('input', { type: p.type || 'text', value: p.modelValue ?? '', placeholder: p.placeholder, disabled: p.disabled, readOnly: p.readonly, onInput: e => p.onChange && p.onChange(e.target.value) }), p.suffix || null),
71
+ TextField: { slots: ['prefix', 'suffix'], render: ({ h, props: p, slots }) => h('label', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label, p.required ? h('span', { class: 'zg-req', 'aria-hidden': 'true' }, ' *') : null) : null,
72
+ h('span', { class: cx('zg-input', p.error && 'zg-input--error', p.variant && 'zg-input--' + p.variant, p.density === 'compact' && 'zg-input--compact', p.disabled && 'zg-input--disabled') }, slots.prefix || (p.icon ? icon(h, p.icon, 18) : null), h('input', { ...pick(p, INPUT_ATTRS), type: p.type || 'text', value: p.modelValue ?? '', placeholder: p.placeholder, disabled: p.disabled, readOnly: p.readonly, onInput: e => { p.onChange && p.onChange(e.target.value); p.onInput && p.onInput(e); } }), slots.suffix || p.suffix || null),
66
73
  p.error ? h('span', { class: 'zg-hint zg-hint--error', role: 'alert' }, p.error) : p.hint ? h('span', { class: 'zg-hint' }, p.hint) : null) },
67
74
  Select: { state: { open: false }, render: ({ h, props: p, state, set }) => { const cur = (p.options || []).find(o => o.value === p.modelValue), open = state.open && !p.disabled;
68
- return h('div', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label) : null,
69
- h('div', { class: 'zg-select' }, h('button', { type: 'button', class: 'zg-btn zg-select__trigger', 'aria-haspopup': 'listbox', 'aria-expanded': String(open), disabled: p.disabled, style: { height: 'var(--zg-h-lg)' }, onClick: () => set({ open: !state.open }) }, h('span', null, cur ? cur.label : p.placeholder || '—'), icon(h, open ? 'chevron-up' : 'chevron-down', 16)),
70
- open ? h('div', { class: 'zg-select__list', role: 'listbox', 'data-zg-dd': '', ref: ddRef(() => set({ open: false })) }, ...(p.options || []).map(o => h('button', { key: o.value, type: 'button', role: 'option', class: 'zg-select__opt', disabled: o.disabled, 'aria-disabled': o.disabled ? 'true' : undefined, 'aria-selected': String(o.value === p.modelValue), onClick: () => { set({ open: false }); p.onChange && p.onChange(o.value); } }, o.label, o.value === p.modelValue ? icon(h, 'check', 16) : null))) : null)); } },
75
+ return h('div', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label, p.required ? h('span', { class: 'zg-req', 'aria-hidden': 'true' }, ' *') : null) : null,
76
+ h('div', { class: 'zg-select' }, h('button', { type: 'button', class: cx('zg-btn', 'zg-select__trigger', p.error && 'zg-select__trigger--error'), 'aria-haspopup': 'listbox', 'aria-expanded': String(open), disabled: p.disabled, style: { height: 'var(--zg-h-lg)' }, onClick: () => set({ open: !state.open }) }, h('span', null, cur ? cur.label : p.placeholder || '—'), icon(h, open ? 'chevron-up' : 'chevron-down', 16)),
77
+ open ? h('div', { class: 'zg-select__list', role: 'listbox', 'data-zg-dd': '', ref: ddRef(() => set({ open: false })) }, ...(p.options || []).map(o => h('button', { key: o.value, type: 'button', role: 'option', class: 'zg-select__opt', disabled: o.disabled, 'aria-disabled': o.disabled ? 'true' : undefined, 'aria-selected': String(o.value === p.modelValue), onClick: () => { set({ open: false }); p.onChange && p.onChange(o.value); } }, o.label, o.value === p.modelValue ? icon(h, 'check', 16) : null))) : null),
78
+ p.name ? h('input', { type: 'hidden', name: p.name, value: p.modelValue ?? '' }) : null, p.error ? h('span', { class: 'zg-hint zg-hint--error', role: 'alert' }, p.error) : p.hint ? h('span', { class: 'zg-hint' }, p.hint) : null); } },
71
79
  CodeInput: { render: ({ h, props: p }) => { const n = p.length || 6, v = p.modelValue || '';
72
80
  return h('div', { class: 'zg-code', style: { '--n': n } }, ...Array.from({ length: n }, (_, i) => h('span', { key: i, class: cx('zg-code__cell', i === v.length && 'zg-code__cell--on') }, v[i] || '')),
73
81
  h('input', { value: v, inputMode: 'numeric', autoComplete: 'one-time-code', maxLength: n, 'aria-label': p.label || 'Code', onInput: e => { const x = e.target.value.replace(/\D/g, '').slice(0, n); p.onChange && p.onChange(x); if (x.length === n && v.length !== n) p.onComplete && p.onComplete(x); } })); } },
74
82
  Stepper: { render: ({ h, props: p }) => h('div', { class: 'zg-stepper' }, ...(p.steps || []).flatMap((s, i) => [i ? h('span', { key: 'b' + i, class: 'zg-step__bar' }) : null, h('span', { key: i, class: cx('zg-step', i < p.current && 'zg-step--done', i === p.current && 'zg-step--cur') }, h('span', { class: 'zg-step__n' }, i < p.current ? icon(h, 'check', 14) : i + 1), s)])) },
75
- Card: { render: ({ h, props: p, children, slots }) => h('section', { class: cx('zg-card', 'zg-glass', p.variant === 'inverse' && 'zg-card--inverse', p.variant === 'gradient' && 'zg-card--grad', p.variant === 'gradient' && 'zg-card--t-' + (p.tone || 'brand'), p.variant === 'gradient' && p.intensity === 'vivid' && 'zg-card--vivid'), 'data-theme': p.variant === 'inverse' ? 'dark' : (p.variant === 'gradient' && p.intensity === 'vivid' ? (['brand', 'warning', undefined].includes(p.tone) ? 'light' : 'dark') : undefined) },
76
- (p.title || slots.actions) ? h('div', { class: 'zg-card__head' }, h('div', { style: { flex: 1, minWidth: 0 } }, h('h3', { class: 'zg-card__title' }, p.title), p.subtitle ? h('div', { class: 'zg-card__sub' }, p.subtitle) : null), slots.actions || null) : null, ...children) },
77
- Modal: { render: ({ h, props: p, children, slots }) => { const foot = slots.footer || p.footer; return !p.open ? null : h('div', { class: 'zg-modal-scrim', onClick: p.onClose }, h('div', { class: 'zg-modal', role: 'dialog', 'aria-modal': 'true', 'aria-label': p.title, ref: trap({ onClose: p.onClose }), onClick: e => e.stopPropagation() },
78
- h('div', { class: 'zg-modal__head' }, h('h3', null, p.title), h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': 'Close', onClick: p.onClose }, icon(h, 'x'))), h('div', { class: 'zg-modal__body' }, ...children), foot ? h('div', { class: 'zg-modal__foot' }, foot) : null)); } },
83
+ Card: { slots: ['header', 'footer'], render: ({ h, props: p, children, slots }) => h('section', { class: cx('zg-card', 'zg-glass', p.variant === 'inverse' && 'zg-card--inverse', p.variant === 'gradient' && 'zg-card--grad', p.variant === 'gradient' && 'zg-card--t-' + (p.tone || 'brand'), p.variant === 'gradient' && p.intensity === 'vivid' && 'zg-card--vivid', p.variant === 'outline' && 'zg-card--outline', p.padding === 'none' && 'zg-card--flush', p.interactive && 'zg-card--click'), 'data-theme': p.variant === 'inverse' ? 'dark' : (p.variant === 'gradient' && p.intensity === 'vivid' ? (['brand', 'warning', undefined].includes(p.tone) ? 'light' : 'dark') : undefined), onClick: p.onClick, tabIndex: p.interactive && p.onClick ? 0 : undefined, onKeydown: p.interactive && p.onClick ? e => { if (e.key === 'Enter' && e.target === e.currentTarget) p.onClick(e); } : undefined },
84
+ slots.header ? h('div', { class: 'zg-card__head' }, slots.header) : (p.title || slots.actions) ? h('div', { class: 'zg-card__head' }, h('div', { style: { flex: 1, minWidth: 0 } }, h('h3', { class: 'zg-card__title' }, p.title), p.subtitle ? h('div', { class: 'zg-card__sub' }, p.subtitle) : null), slots.actions || null) : null, ...children, slots.footer ? h('div', { class: 'zg-card__foot' }, slots.footer) : null) },
85
+ Modal: { slots: ['title', 'subtitle'], render: ({ h, props: p, children, slots }) => { const foot = slots.footer || p.footer, sub = slots.subtitle || p.subtitle, ttl = slots.title || p.title; return !p.open ? null : h('div', { class: 'zg-modal-scrim', onClick: p.closeOnBackdrop === false ? undefined : p.onClose }, h('div', { class: 'zg-modal', role: 'dialog', 'aria-modal': 'true', 'aria-label': p.title, style: p.width ? { maxWidth: p.width } : undefined, ref: trap({ onClose: p.closeOnEsc === false ? undefined : p.onClose }), onClick: e => e.stopPropagation() },
86
+ h('div', { class: 'zg-modal__head' }, sub ? h('div', { style: { flex: 1, minWidth: 0 } }, h('h3', null, ttl), h('div', { class: 'zg-modal__sub' }, sub)) : h('h3', null, ttl), p.closable === false ? null : h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': 'Close', onClick: p.onClose }, icon(h, 'x'))), h('div', { class: 'zg-modal__body' }, ...children), foot ? h('div', { class: 'zg-modal__foot' }, foot) : null)); } },
79
87
  Drawer: { render: ({ h, props: p, children, slots }) => { const foot = slots.footer || p.footer; return !p.open ? null : h('div', { class: cx('zg-modal-scrim', 'zg-drawer-scrim', 'zg-drawer-scrim--' + (p.side || 'end')), onClick: p.onClose }, h('div', { class: 'zg-modal zg-drawer', role: 'dialog', 'aria-modal': 'true', 'aria-label': p.title, ref: trap({ onClose: p.onClose }), onClick: e => e.stopPropagation() },
80
88
  h('div', { class: 'zg-modal__head' }, h('h3', null, p.title), h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': 'Close', onClick: p.onClose }, icon(h, 'x'))), h('div', { class: 'zg-modal__body' }, ...children), foot ? h('div', { class: 'zg-modal__foot' }, foot) : null)); } },
81
89
  Toast: { render: ({ h, props: p, children }) => h('div', { class: cx('zg-toast', 'zg-toast--' + (p.tone || 'info')), role: 'status' }, h('span', { class: 'zg-toast__icon' }, icon(h, { success: 'check', danger: 'circle-alert', warning: 'triangle-alert' }[p.tone] || 'info', 18)), h('div', null, p.title ? h('b', null, p.title) : null, h('span', null, ...children))) },
82
- Breadcrumbs: { render: ({ h, props: p }) => h('nav', { class: 'zg-crumbs', 'aria-label': 'Breadcrumb' }, ...(p.items || []).flatMap((it, i, a) => [i ? icon(h, 'chevron-right', 14) : null, i === a.length - 1 ? h('b', { key: i, 'aria-current': 'page' }, it.label) : h('a', { key: i, href: it.href || '#' }, it.label)])) },
90
+ Breadcrumbs: { render: ({ h, props: p }) => h('nav', { class: 'zg-crumbs', 'aria-label': 'Breadcrumb' }, ...(p.items || []).flatMap((it, i, a) => [i ? (p.separator === 'slash' || p.separator === 'dot' ? h('span', { key: 's' + i, class: 'zg-crumbs__sep', 'aria-hidden': 'true' }, p.separator === 'slash' ? '/' : '\u00b7') : icon(h, 'chevron-right', 14)) : null, i === a.length - 1 ? h('b', { key: i, 'aria-current': 'page' }, it.label) : h('a', { key: i, href: it.href || '#' }, it.label)])) },
83
91
  Pagination: { render: ({ h, props: p }) => { const total = Math.max(1, Math.floor(+p.pages) || 1), cur = Math.min(total, Math.max(1, +p.page || 1)), s = Math.max(0, p.siblings == null ? 1 : Math.floor(+p.siblings) || 0), go = n => p.onChange && n >= 1 && n <= total && n !== p.page && p.onChange(n);
84
92
  const set = new Set([1, total]); for (let i = cur - s; i <= cur + s; i++) if (i >= 1 && i <= total) set.add(i);
85
93
  const nums = [...set].sort((a, b) => a - b), items = []; nums.forEach((n, i) => { const prev = nums[i - 1]; if (prev && n - prev === 2) items.push(prev + 1); else if (prev && n - prev > 2) items.push('…'); items.push(n); });
86
- return h('nav', { class: 'zg-pager', 'aria-label': p.label || 'Pagination' }, h('button', { key: 'prev', type: 'button', class: 'zg-pager__nav', 'aria-label': 'Previous page', disabled: cur <= 1, onClick: () => go(cur - 1) }, icon(h, 'chevron-left', 16)),
87
- ...items.map((n, i) => n === '…' ? h('span', { key: 'e' + i, class: 'zg-pager__gap', 'aria-hidden': 'true' }, '…') : h('button', { key: n, type: 'button', 'aria-label': 'Page ' + n, 'aria-current': n === p.page ? 'page' : undefined, onClick: () => go(n) }, n)),
88
- h('button', { key: 'next', type: 'button', class: 'zg-pager__nav', 'aria-label': 'Next page', disabled: cur >= total, onClick: () => go(cur + 1) }, icon(h, 'chevron-right', 16))); } },
94
+ return h('nav', { class: 'zg-pager', 'aria-label': p.label || 'Pagination' }, p.hideArrows ? null : h('button', { key: 'prev', type: 'button', class: 'zg-pager__nav', 'aria-label': (p.labels && p.labels.prev) || 'Previous page', disabled: cur <= 1, onClick: () => go(cur - 1) }, icon(h, 'chevron-left', 16)),
95
+ ...items.map((n, i) => n === '…' ? h('span', { key: 'e' + i, class: 'zg-pager__gap', 'aria-hidden': 'true' }, '…') : h('button', { key: n, type: 'button', 'aria-label': ((p.labels && p.labels.page) || 'Page') + ' ' + n, 'aria-current': n === p.page ? 'page' : undefined, onClick: () => go(n) }, n)),
96
+ p.hideArrows ? null : h('button', { key: 'next', type: 'button', class: 'zg-pager__nav', 'aria-label': (p.labels && p.labels.next) || 'Next page', disabled: cur >= total, onClick: () => go(cur + 1) }, icon(h, 'chevron-right', 16))); } },
89
97
  Progress: { render: ({ h, props: p }) => h('div', { class: cx('zg-progress', p.tone && 'zg-progress--' + p.tone) }, (p.label || p.meta) ? h('div', { class: 'zg-progress__row' }, h('span', null, p.label), h('span', { class: 'zg-mono' }, p.meta)) : null, h('div', { class: 'zg-progress__track', role: 'progressbar', 'aria-valuenow': p.value, 'aria-valuemin': 0, 'aria-valuemax': 100 }, h('div', { class: 'zg-progress__bar', style: { width: Math.max(0, Math.min(100, p.value || 0)) + '%' } }))) },
90
98
  Ring: { render: ({ h, props: p }) => { const s = { xs: 72, sm: 120, md: 180, lg: 240, xl: 300 }[p.size] || (Number(p.size) > 0 ? Number(p.size) : 180), th = p.thickness || Math.max(8, Math.round(s * .078)), ticks = p.ticks !== false, v = Math.max(0, Math.min(100, p.value || 0)) / 100, col = p.color || 'var(--zg-brand)';
91
99
  const r = (s - th) / 2 - (ticks ? 12 : 0), cir = 2 * Math.PI * r, tk = [];
@@ -95,10 +103,10 @@ export const components = {
95
103
  h('circle', { cx: s / 2, cy: s / 2, r, fill: 'none', stroke: col, 'stroke-width': th, 'stroke-linecap': 'round', 'stroke-dasharray': cir, 'stroke-dashoffset': cir * (1 - v), style: { filter: 'drop-shadow(0 0 10px color-mix(in oklch, ' + col + ' 45%, transparent))', transition: 'stroke-dashoffset 1.4s cubic-bezier(.2,.8,.2,1)' } })),
96
104
  h('div', { class: 'zg-ring__c' }, h('span', { class: 'zg-ring__v', style: { fontSize: (p.valueSize || s * .22) + 'px' } }, Math.round(v * 100), h('span', { style: { fontSize: '.45em', color: 'var(--zg-text-3)' } }, '%')), p.label ? h('span', { class: 'zg-ring__l' }, p.label) : null)); } },
97
105
  KpiTile: { render: ({ h, props: p }) => h('div', { class: cx('zg-kpi', p.variant && 'zg-kpi--' + p.variant) }, h('span', { class: 'zg-kpi__label' }, p.label), h('span', { class: 'zg-kpi__value zg-num' }, p.value), (p.sub || p.delta) ? h('span', { class: 'zg-kpi__sub' }, p.delta ? h('span', { class: cx('zg-delta', String(p.delta).startsWith('-') ? 'zg-delta--down' : 'zg-delta--up') }, p.delta + ' ') : null, p.sub) : null) },
98
- DataTable: { render: ({ h, props: p }) => { const cols = p.columns || [], rows = p.rows || [], sel = p.selected || [], all = rows.length && rows.every(r => sel.includes(r.id));
99
- const th = c => h('th', { key: c.key, 'aria-sort': p.sortKey === c.key ? (p.sortDir === 'desc' ? 'descending' : 'ascending') : undefined, class: c.num ? 'num' : undefined }, c.sortable ? h('button', { type: 'button', onClick: () => p.onSort && p.onSort(c.key, p.sortKey === c.key && p.sortDir !== 'desc' ? 'desc' : 'asc') }, c.label, p.sortKey === c.key ? icon(h, p.sortDir === 'desc' ? 'arrow-down' : 'arrow-up', 12) : null) : c.label);
106
+ DataTable: { render: ({ h, props: p, slots }) => { const cs = c => (c.align || c.width) ? { textAlign: c.align, width: c.width } : undefined, cols = p.columns || [], rows = p.rows || [], sel = p.selected || [], all = rows.length && rows.every(r => sel.includes(r.id));
107
+ const th = c => h('th', { key: c.key, 'aria-sort': p.sortKey === c.key ? (p.sortDir === 'desc' ? 'descending' : 'ascending') : undefined, class: c.num ? 'num' : undefined, style: cs(c) }, c.sortable ? h('button', { type: 'button', onClick: () => p.onSort && p.onSort(c.key, p.sortKey === c.key && p.sortDir !== 'desc' ? 'desc' : 'asc') }, c.label, p.sortKey === c.key ? icon(h, p.sortDir === 'desc' ? 'arrow-down' : 'arrow-up', 12) : null) : c.label);
100
108
  return h('div', { class: 'zg-table-wrap' }, h('table', { class: 'zg-table' }, h('thead', null, h('tr', null, p.onSelect ? h('th', { style: { width: '44px' } }, h('button', { type: 'button', role: 'checkbox', class: 'zg-check', 'aria-checked': String(!!all), 'aria-label': (p.labels && p.labels.selectAll) || 'Select all', onClick: () => p.onSelect(all ? [] : rows.map(r => r.id)) }, h('span', { class: 'zg-check__box' }, all ? icon(h, 'check', 14) : null))) : null, ...cols.map(th))),
101
- h('tbody', null, ...(rows.length ? rows.map(r => h('tr', { key: r.id, 'aria-selected': sel.includes(r.id) ? 'true' : undefined }, p.onSelect ? h('td', null, h('button', { type: 'button', role: 'checkbox', class: 'zg-check', 'aria-checked': String(sel.includes(r.id)), 'aria-label': (p.labels && p.labels.selectRow) || 'Select row', onClick: () => p.onSelect(sel.includes(r.id) ? sel.filter(i => i !== r.id) : [...sel, r.id]) }, h('span', { class: 'zg-check__box' }, sel.includes(r.id) ? icon(h, 'check', 14) : null))) : null, ...cols.map(c => h('td', { key: c.key, class: c.num ? 'num' : undefined }, c.render ? c.render(r[c.key], r, h) : (r[c.key] ?? '—'))))) : [h('tr', { key: 'e' }, h('td', { colSpan: cols.length + 1 }, h('div', { class: 'zg-empty' }, p.empty || '—')))])))); } },
109
+ h('tbody', null, ...(rows.length ? rows.map((r, ri) => h('tr', { key: r.id, 'aria-selected': sel.includes(r.id) ? 'true' : undefined, class: p.onRowClick ? 'zg-tr--click' : undefined, onClick: p.onRowClick ? () => p.onRowClick(r, ri) : undefined }, p.onSelect ? h('td', null, h('button', { type: 'button', role: 'checkbox', class: 'zg-check', 'aria-checked': String(sel.includes(r.id)), 'aria-label': (p.labels && p.labels.selectRow) || 'Select row', onClick: () => p.onSelect(sel.includes(r.id) ? sel.filter(i => i !== r.id) : [...sel, r.id]) }, h('span', { class: 'zg-check__box' }, sel.includes(r.id) ? icon(h, 'check', 14) : null))) : null, ...cols.map(c => h('td', { key: c.key, class: c.num ? 'num' : undefined, style: cs(c) }, c.render ? c.render(r[c.key], r, h) : (r[c.key] ?? '—'))))) : [h('tr', { key: 'e' }, h('td', { colSpan: cols.length + 1 }, h('div', { class: 'zg-empty' }, slots.empty || p.empty || '—')))])))); } },
102
110
  KanbanColumn: { render: ({ h, props: p, children }) => h('div', { class: 'zg-kcol zg-dm' }, h('div', { class: 'zg-kcol__head' }, p.title, h('span', { class: 'zg-badge' }, p.count ?? children.length)), ...children) },
103
111
  KanbanCard: { render: ({ h, props: p, children }) => h('div', { class: 'zg-kcard zg-dm' }, h('b', { style: { fontSize: dzf(14) } }, p.title), p.sub ? h('span', { class: 'zg-hint' }, p.sub) : null, ...children, p.tag ? h('span', { class: cx('zg-badge', p.tone && 'zg-badge--' + p.tone), style: { alignSelf: 'flex-start' } }, p.tag) : null) },
104
112
  Timeline: { render: ({ h, props: p }) => h('div', { class: 'zg-timeline' }, ...(p.items || []).map((it, i, a) => h('div', { key: i, class: 'zg-tl' }, h('div', { class: 'zg-tl__rail' }, h('span', { class: 'zg-tl__dot' }), i < a.length - 1 ? h('span', { class: 'zg-tl__line' }) : null), h('div', { class: 'zg-tl__body' }, h('b', null, it.title), it.text ? h('span', null, it.text) : null, h('span', { class: 'zg-tl__time' }, it.time))))) },