@ziyarago/ui 0.0.0-stage → 0.1.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 (119) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/LICENSE +25 -0
  3. package/README.md +121 -2
  4. package/THIRD_PARTY_NOTICES.md +12 -0
  5. package/assets/3d/_sheet.png +0 -0
  6. package/assets/3d/archive.png +0 -0
  7. package/assets/3d/bag.png +0 -0
  8. package/assets/3d/cameraoff.png +0 -0
  9. package/assets/3d/comment.png +0 -0
  10. package/assets/3d/crowd.png +0 -0
  11. package/assets/3d/firstaid.png +0 -0
  12. package/assets/3d/flag.png +0 -0
  13. package/assets/3d/grain.png +0 -0
  14. package/assets/3d/guide.png +0 -0
  15. package/assets/3d/hotel.png +0 -0
  16. package/assets/3d/kaaba.png +0 -0
  17. package/assets/3d/logo.png +0 -0
  18. package/assets/3d/luggage.png +0 -0
  19. package/assets/3d/map.png +0 -0
  20. package/assets/3d/mosque.png +0 -0
  21. package/assets/3d/plane.png +0 -0
  22. package/assets/3d/profile.png +0 -0
  23. package/assets/3d/restaurant.png +0 -0
  24. package/assets/3d/ring360.png +0 -0
  25. package/assets/3d/shoes.png +0 -0
  26. package/assets/3d/star.png +0 -0
  27. package/assets/3d/store.png +0 -0
  28. package/assets/3d/taxi.png +0 -0
  29. package/assets/3d/threads.png +0 -0
  30. package/assets/3d/tour360.png +0 -0
  31. package/assets/3d/trash.png +0 -0
  32. package/assets/3d/umbrella.png +0 -0
  33. package/assets/3d/video.png +0 -0
  34. package/assets/3d/water.png +0 -0
  35. package/assets/brand/mark.png +0 -0
  36. package/assets/brand/ziyarago-logo-light.svg +21 -0
  37. package/assets/brand/ziyarago-logo.svg +21 -0
  38. package/assets/brand/ziyarago-mark-light.svg +13 -0
  39. package/assets/brand/ziyarago-mark.svg +13 -0
  40. package/assets/fonts/thmanyah/thmanyahsans-Black.otf +0 -0
  41. package/assets/fonts/thmanyah/thmanyahsans-Bold.otf +0 -0
  42. package/assets/fonts/thmanyah/thmanyahsans-Light.otf +0 -0
  43. package/assets/fonts/thmanyah/thmanyahsans-Medium.otf +0 -0
  44. package/assets/fonts/thmanyah/thmanyahsans-Regular.otf +0 -0
  45. package/assets/fonts/thmanyah/thmanyahserifdisplay-Black.otf +0 -0
  46. package/assets/fonts/thmanyah/thmanyahserifdisplay-Bold.otf +0 -0
  47. package/assets/fonts/thmanyah/thmanyahserifdisplay-Light.otf +0 -0
  48. package/assets/fonts/thmanyah/thmanyahserifdisplay-Medium.otf +0 -0
  49. package/assets/fonts/thmanyah/thmanyahserifdisplay-Regular.otf +0 -0
  50. package/assets/fonts/thmanyah/thmanyahseriftext-Black.otf +0 -0
  51. package/assets/fonts/thmanyah/thmanyahseriftext-Bold.otf +0 -0
  52. package/assets/fonts/thmanyah/thmanyahseriftext-Light.otf +0 -0
  53. package/assets/fonts/thmanyah/thmanyahseriftext-Medium.otf +0 -0
  54. package/assets/fonts/thmanyah/thmanyahseriftext-Regular.otf +0 -0
  55. package/dist/llms-full.txt +2293 -0
  56. package/dist/llms.txt +239 -0
  57. package/dist/manifest.json +10254 -0
  58. package/dist/style.css +1357 -0
  59. package/dist/tokens.css +48 -0
  60. package/package.json +118 -4
  61. package/src/catalog.js +4 -0
  62. package/src/core/components.js +124 -0
  63. package/src/core/components2.js +163 -0
  64. package/src/core/components3.js +35 -0
  65. package/src/core/components4.js +58 -0
  66. package/src/core/components6.js +48 -0
  67. package/src/core/components7.js +42 -0
  68. package/src/core/dialog.js +65 -0
  69. package/src/core/index.js +9 -0
  70. package/src/core/pages.js +51 -0
  71. package/src/meta/README.md +29 -0
  72. package/src/meta/actions.js +161 -0
  73. package/src/meta/data.js +327 -0
  74. package/src/meta/domain.js +212 -0
  75. package/src/meta/feedback.js +153 -0
  76. package/src/meta/forms.js +453 -0
  77. package/src/meta/groups.js +16 -0
  78. package/src/meta/index.js +14 -0
  79. package/src/meta/layout.js +159 -0
  80. package/src/meta/media.js +104 -0
  81. package/src/meta/navigation.js +99 -0
  82. package/src/meta/overlay.js +264 -0
  83. package/src/meta/pages.js +229 -0
  84. package/src/meta/templates.json +2422 -0
  85. package/src/meta/typography.js +58 -0
  86. package/src/nuxt/module.mjs +29 -0
  87. package/src/nuxt/plugin.mjs +9 -0
  88. package/src/react/index.js +31 -0
  89. package/src/styles/components.css +162 -0
  90. package/src/styles/components2.css +68 -0
  91. package/src/styles/components3.css +29 -0
  92. package/src/styles/components4.css +18 -0
  93. package/src/styles/components5.css +5 -0
  94. package/src/styles/components6.css +19 -0
  95. package/src/styles/components7.css +44 -0
  96. package/src/styles/components8.css +47 -0
  97. package/src/styles/effects.css +43 -0
  98. package/src/styles/fonts.css +17 -0
  99. package/src/styles/index.css +21 -0
  100. package/src/styles/sizes/actions.css +126 -0
  101. package/src/styles/sizes/data.css +161 -0
  102. package/src/styles/sizes/domain.css +35 -0
  103. package/src/styles/sizes/feedback.css +69 -0
  104. package/src/styles/sizes/forms.css +122 -0
  105. package/src/styles/sizes/layout.css +15 -0
  106. package/src/styles/sizes/media.css +28 -0
  107. package/src/styles/sizes/navigation.css +146 -0
  108. package/src/styles/sizes/overlay.css +79 -0
  109. package/src/styles/sizes/typography.css +12 -0
  110. package/src/styles/tokens.css +47 -0
  111. package/src/theme.js +52 -0
  112. package/src/vue/index.js +29 -0
  113. package/tokens/tokens.json +9 -0
  114. package/types/core.d.ts +4 -0
  115. package/types/nuxt.d.ts +11 -0
  116. package/types/react.d.ts +189 -0
  117. package/types/shared.d.ts +18 -0
  118. package/types/theme.d.ts +6 -0
  119. package/types/vue.d.ts +192 -0
@@ -0,0 +1,163 @@
1
+ // @ziyarago/ui · components, part 2 — Forms, Surfaces, Actions, Data, Navigation, Layout, CRM, Hotels.
2
+ import { icon, cx, fmt, dzp, dzf, dzb, ddRef } from './components.js';
3
+ import { trap } from './dialog.js';
4
+ const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
5
+ const num = v => Number.isFinite(+v) && v !== null && v !== '' ? +v : 0;
6
+ const arr = v => Array.isArray(v) ? v : [];
7
+ const idx = (p, st, n) => clamp(Math.trunc(num(p.index !== undefined && p.index !== null ? p.index : st.i)), 0, Math.max(0, n - 1));
8
+ // Client-only autoplay: a ref callback re-run on each render; the interval lives on the element and ends itself once the element is detached.
9
+ const autoplay = (el, ms, step) => {
10
+ if (!el || typeof window === 'undefined') return;
11
+ const a = el.__zgAuto || (el.__zgAuto = { t: 0, ms: 0 });
12
+ a.step = step;
13
+ if (a.ms === ms) return;
14
+ clearInterval(a.t); a.t = 0; a.ms = ms;
15
+ if (!ms) return;
16
+ a.t = setInterval(() => {
17
+ if (!el.isConnected) { clearInterval(a.t); a.t = 0; return; }
18
+ const m = el.closest('[data-motion]');
19
+ if ((m && m.getAttribute('data-motion') === 'off') || document.hidden || (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) || el.matches(':hover, :focus-within')) return;
20
+ a.step();
21
+ }, ms);
22
+ };
23
+ let dragging = null;
24
+ const ROOM_TONES = { occupied: 'brand', clean: 'success', dirty: 'warning', blocked: 'danger' };
25
+ const TONE_OK = ['brand', 'sky', 'success', 'warning', 'danger', 'neutral'];
26
+ const CALL_TONES = { answered: 'success', completed: 'success', missed: 'danger', declined: 'danger', rejected: 'danger', failed: 'danger', voicemail: 'warning', busy: 'warning', 'no answer': 'warning', ringing: 'info', 'in progress': 'info' };
27
+ const pd = v => { if (v == null || v === '') return null; if (v instanceof Date) return isNaN(v) ? null : v; if (typeof v === 'string') { const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(v.trim()); if (m) return new Date(+m[1], +m[2] - 1, +m[3]); } const d = new Date(v); return isNaN(d) ? null : d; };
28
+ const lab =(h, p, body) => h('label', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label) : null, body, p.error ? h('span', { class: 'zg-hint zg-hint--error', role: 'alert' }, p.error) : p.hint ? h('span', { class: 'zg-hint' }, p.hint) : null);
29
+ const field = (h, p, pre, input, suf) => h('span', { class: cx('zg-input', p.error && 'zg-input--error', p.disabled && 'zg-input--disabled') }, pre || null, input, suf || null);
30
+ const TONE = { success: 'check', danger: 'circle-alert', warning: 'triangle-alert' };
31
+ const sel = (h, o, on) => ({ 'aria-selected': String(on), role: 'option' });
32
+
33
+ export const more = {
34
+ /* ── Surfaces ── */
35
+ Alert: { render: ({ h, props: p, children }) => h('div', { class: cx('zg-alert', 'zg-alert--' + (p.tone || 'info'), p.banner && 'zg-banner'), role: 'alert' }, h('span', { class: 'zg-alert__i' }, icon(h, TONE[p.tone] || 'info', 18)), h('div', { class: 'zg-alert__b' }, p.title ? h('b', null, p.title) : null, h('span', null, ...children)), p.onClose ? h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': 'Close', onClick: p.onClose }, icon(h, 'x')) : null) },
36
+ Banner: { render: ({ C, h, props: p, children }) => h(C.Alert, { ...p, banner: true }, ...children) },
37
+ Accordion: { state: { open: null }, render: ({ h, props: p, state, set }) => h('div', { class: 'zg-acc' }, ...(p.items || []).map(it => { const o = state.open === it.id; return h('div', { key: it.id, class: 'zg-acc__i', 'data-open': String(o) }, h('button', { type: 'button', 'aria-expanded': String(o), class: 'zg-acc__h', onClick: () => set({ open: o ? null : it.id }) }, it.title, icon(h, 'chevron-down', 16)), o ? h('div', { class: 'zg-acc__b' }, it.body) : null); })) },
38
+ Skeleton: { render: ({ h, props: p }) => h('div', { class: 'zg-skelw' }, ...Array.from({ length: p.lines || 1 }, (_, i) => h('span', { key: i, class: 'zg-skel zg-skel--' + (p.variant || 'text'), style: { width: p.variant === 'circle' ? ({ xs: 24, sm: 32, md: 40, lg: 56, xl: 72 }[p.size] || p.size || 40) + 'px' : (i === (p.lines || 1) - 1 && (p.lines || 1) > 1 ? '60%' : p.width || '100%'), height: p.height || (p.variant === 'circle' ? ({ xs: 24, sm: 32, md: 40, lg: 56, xl: 72 }[p.size] || p.size || 40) + 'px' : undefined) } }))) },
39
+ Spinner: { render: ({ h, props: p }) => h('span', { class: 'zg-spin', role: 'status', 'aria-label': 'Loading', style: { fontSize: ({ xs: 14, sm: 16, md: 20, lg: 28, xl: 36 }[p.size] || p.size || 20) + 'px' } }) },
40
+ Divider: { render: ({ h, props: p }) => h('div', { class: 'zg-div', role: 'separator' }, p.label ? h('span', null, p.label) : null) },
41
+ EmptyState: { render: ({ h, props: p, children }) => h('div', { class: 'zg-es' }, p.image ? h('span', { class: 'zg-es__img', style: { backgroundImage: `url(${p.image})` } }) : h('span', { class: 'zg-es__ico' }, icon(h, p.icon || 'inbox', 28)), h('b', null, p.title), p.description ? h('span', null, p.description) : null, children.length ? h('div', { class: 'zg-es__a' }, ...children) : null) },
42
+ ErrorPage: { render: ({ C, h, props: p, children }) => h('div', { class: 'zg-es zg-es--page' }, h('span', { class: 'zg-num', style: { fontSize: 'clamp(80px,14vw,160px)', lineHeight: 1, color: 'var(--zg-brand-text)' } }, p.code || '404'), h('b', { style: { fontSize: '24px' } }, p.title || 'Page not found'), h('span', null, p.description || 'The page may have moved. Check the address or go back.'), h('div', { class: 'zg-es__a' }, ...children)) },
43
+ DataState: { render: ({ C, h, props: p, children }) => ({ ready: () => h('div', null, ...children), loading: () => h('div', { class: 'zg-es' }, h(C.Spinner, { size: 28 }), h('b', null, p.title || 'Loading…')), empty: () => h(C.EmptyState, { title: p.title || 'Nothing here yet', description: p.description, icon: 'inbox' }), error: () => h(C.EmptyState, { title: p.title || 'Something went wrong', description: p.description, icon: 'circle-alert' }, h(C.Button, { onClick: p.onRetry }, p.retryLabel || 'Retry')), offline: () => h(C.EmptyState, { title: p.title || 'No internet', description: p.description || 'Saved guides still work.', icon: 'wifi-off' }, h(C.Button, { onClick: p.onRetry }, p.retryLabel || 'Try again')) }[p.state || 'ready'])() },
44
+ LoadingOverlay: { render: ({ C, h, props: p, children }) => h('div', { class: 'zg-lo' }, ...children, p.loading ? h('div', { class: 'zg-lo__o' }, h(C.Spinner, { size: 28 }), p.label ? h('span', null, p.label) : null) : null) },
45
+ ConfirmDialog: { render: ({ C, h, props: p }) => h(C.Modal, { open: p.open, size: p.size, title: p.title, onClose: p.onCancel, footer: [h(C.Button, { key: 'c', variant: 'ghost', onClick: p.onCancel }, p.cancelLabel || 'Cancel'), h(C.Button, { key: 'k', variant: p.tone === 'danger' ? 'danger' : 'primary', loading: p.loading, onClick: p.onConfirm }, p.confirmLabel || 'Confirm')] }, h('span', { style: { color: 'var(--zg-text-2)', lineHeight: 1.6 } }, p.description)) },
46
+ Popconfirm: { state: { open: false }, render: ({ C, h, props: p, state, set }) => h('span', { class: 'zg-dd' }, h(C.Button, { size: p.size || 'sm', variant: p.variant || 'danger', onClick: () => set({ open: !state.open }) }, p.label), state.open ? h('div', { class: 'zg-pop zg-glass', 'data-zg-dd': '', ref: ddRef(() => set({ open: false })) }, h('b', { style: { fontSize: '14px' } }, p.title), h('div', { style: { display: 'flex', gap: '8px', justifyContent: 'flex-end' } }, h(C.Button, { size: 'xs', variant: 'ghost', onClick: () => set({ open: false }) }, p.cancelLabel || 'No'), h(C.Button, { size: 'xs', variant: 'primary', onClick: () => { set({ open: false }); p.onConfirm && p.onConfirm(); } }, p.confirmLabel || 'Yes'))) : null) },
47
+ Carousel: { state: { i: 0 }, render: ({ h, props: p, state, set }) => { const items = arr(p.items).map(x => x || {}), n = items.length, i = idx(p, state, n), it = items[i] || {}, L = p.labels || {};
48
+ const to = k => { if (!n) return; k = ((k % n) + n) % n; if (k === i) return; if (p.index === undefined || p.index === null) set({ i: k }); p.onChange && p.onChange(k); };
49
+ const ms = p.autoplay === true ? 5000 : n > 1 && num(p.autoplay) > 0 ? num(p.autoplay) : 0;
50
+ return h('div', { class: 'zg-car', role: 'region', 'aria-roledescription': 'carousel', 'aria-label': p.label || undefined, ref: el => autoplay(el, n > 1 ? ms : 0, () => to(i + 1)), onKeyDown: e => { if (e.key === 'ArrowLeft') to(i - 1); else if (e.key === 'ArrowRight') to(i + 1); } }, h('div', { class: 'zg-car__s', 'aria-live': ms ? 'off' : 'polite', style: { backgroundImage: it.image ? `url(${it.image})` : undefined } }, h('div', { class: 'zg-car__c' }, it.title ? h('b', null, it.title) : null, it.sub ? h('span', null, it.sub) : null)), n > 1 ? h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-car__p', 'aria-label': L.prev || 'Previous', onClick: () => to(i - 1) }, icon(h, 'chevron-left')) : null, n > 1 ? h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-car__n', 'aria-label': L.next || 'Next', onClick: () => to(i + 1) }, icon(h, 'chevron-right')) : null, n > 1 ? h('div', { class: 'zg-car__d' }, ...items.map((_, k) => h('button', { key: k, type: 'button', 'aria-label': (L.slide || 'Slide') + ' ' + (k + 1), 'aria-current': String(k === i), onClick: () => to(k) }))) : null); } },
51
+ Gallery: { state: { i: 0 }, render: ({ h, props: p, state, set }) => { const im = arr(p.images).filter(Boolean), n = im.length, i = idx(p, state, n), L = p.labels || {};
52
+ const to = k => { if (k === i) return; if (p.index === undefined || p.index === null) set({ i: k }); p.onChange && p.onChange(k); };
53
+ return h('div', { class: 'zg-gal' }, h('div', { class: 'zg-gal__m', style: { backgroundImage: n ? `url(${im[i]})` : undefined }, role: 'img', 'aria-label': p.alt || '' }, n ? h('span', { class: 'zg-badge zg-gal__n' }, `${i + 1} / ${n}`) : null), n ? h('div', { class: 'zg-gal__t' }, ...im.map((u, k) => h('button', { key: k, type: 'button', 'aria-label': (L.photo || 'Photo') + ' ' + (k + 1), 'aria-current': String(k === i), style: { backgroundImage: `url(${u})` }, onClick: () => to(k) }))) : null); } },
54
+ Code: { render: ({ C, h, props: p }) => h('div', { class: 'zg-code-b' }, h('div', { class: 'zg-code-b__h' }, h('span', null, p.title || p.language || 'code'), h(C.CopyButton, { value: p.code, size: 'xs' })), h('pre', { class: 'zg-mono' }, p.code)) },
55
+ PricingCard: { render: ({ C, h, props: p }) => h('div', { class: cx('zg-card', 'zg-glass', p.featured && 'zg-price--on') }, h('div', { class: 'zg-card__head' }, h('h3', { class: 'zg-card__title' }, p.name), p.badge ? h(C.Badge, { tone: 'brand' }, p.badge) : null), h('div', { style: { display: 'flex', alignItems: 'baseline', gap: '6px' } }, h('span', { class: 'zg-num', style: { fontSize: '44px' } }, p.price), h('span', { class: 'zg-hint' }, p.period || '')), h('ul', { class: 'zg-feat' }, ...(p.features || []).map((f, i) => h('li', { key: i }, icon(h, 'check', 16), f))), h(C.Button, { variant: p.featured ? 'primary' : 'secondary', block: true, onClick: p.onSelect }, p.cta || 'Choose')) },
56
+ IntegrationCard: { render: ({ C, h, props: p }) => { const st = ['connected', 'error'].includes(p.status) ? p.status : 'off', tone = { connected: 'success', error: 'danger' }[st], L = { connected: 'Connected', error: 'Error', off: 'Not linked', manage: 'Manage', reconnect: 'Reconnect', link: 'Link', ...(p.labels || {}) };
57
+ return h('div', { class: 'zg-card zg-glass' }, h('div', { class: 'zg-card__head' }, h('span', { class: 'zg-tl__dot', style: { margin: '8px 4px 0' } }), h('div', { style: { flex: 1 } }, h('h3', { class: 'zg-card__title' }, p.name), h('div', { class: 'zg-card__sub' }, p.description)), h(C.Badge, { tone, dot: true }, L[st])), h(C.Button, { variant: st === 'connected' ? 'secondary' : 'primary', size: 'sm', onClick: p.onToggle }, st === 'connected' ? L.manage : st === 'error' ? L.reconnect : L.link)); } },
58
+ IncomingCall: { render: ({ C, h, props: p }) => h('div', { class: 'zg-inc zg-glass zg-dm' }, h(C.Avatar, { name: p.name, size: dzp(p, 48) }), h('div', { style: { flex: 1, minWidth: 0 } }, h('div', { class: 'zg-hint' }, p.label || 'Incoming call'), h('b', { style: { fontSize: dzf(16) } }, p.name), h('div', { class: 'zg-mono zg-hint' }, p.phone)), h(C.IconButton, { icon: 'phone-off', label: 'Decline', size: p.size ? p.size : undefined, onClick: p.onDecline }), h('button', { type: 'button', class: 'zg-ibtn zg-inc__ok', 'aria-label': 'Answer', onClick: p.onAnswer }, icon(h, 'phone'))) },
59
+ CallCard: { render: ({ C, h, props: p }) => h('div', { class: 'zg-card zg-glass zg-dm' }, h('div', { class: 'zg-lead__row' }, h(C.Avatar, { name: p.name, size: dzp(p, 36) }), h('div', { style: { flex: 1, minWidth: 0 } }, h('b', { style: { fontSize: dzf(14) } }, p.name), h('div', { class: 'zg-hint zg-mono' }, p.phone)), p.status ? h(C.Badge, { tone: { ...CALL_TONES, ...(p.statusTones || {}) }[String(p.status).toLowerCase()] || (p.statusTones || {})[p.status], dot: true }, p.status) : null), h('div', { class: 'zg-lead__row' }, h(C.IconButton, { icon: 'play', label: (p.labels && p.labels.play) || 'Play', size: dzb(p), onClick: p.onPlay }), h(C.Progress, { value: num(p.progress) }), h('span', { class: 'zg-mono zg-hint' }, fmt.dur(p.duration, p.durUnits)))) },
60
+ /* ── Actions ── */
61
+ Dropdown: { state: { open: false }, render: ({ h, props: p, state, set }) => h('div', { class: 'zg-dd' }, h('button', { type: 'button', class: cx('zg-btn', p.size && 'zg-btn--' + p.size), 'aria-haspopup': 'menu', 'aria-expanded': String(state.open && !p.disabled), disabled: p.disabled, onClick: () => set({ open: !state.open }) }, p.icon ? icon(h, p.icon, 16) : null, p.label, icon(h, 'chevron-down', 14)), state.open && !p.disabled ? h('div', { class: 'zg-menu', role: 'menu', 'data-zg-dd': '', ref: ddRef(() => set({ open: false })) }, ...(p.items || []).map((it, i) => it.separator ? h('div', { key: i, class: 'zg-menu__sep' }) : h('button', { key: i, type: 'button', role: 'menuitem', disabled: it.disabled, 'aria-disabled': it.disabled ? 'true' : undefined, class: cx('zg-menu__i', it.tone === 'danger' && 'zg-menu__i--danger'), onClick: () => { set({ open: false }); it.onClick && it.onClick(it); p.onSelect && p.onSelect(it.value); } }, it.icon ? icon(h, it.icon, 16) : null, it.label, it.shortcut ? h('span', { class: 'zg-kbd' }, it.shortcut) : null))) : null) },
62
+ Menu: { render: ({ h, props: p }) => p.open === false ? null : h('div', { class: 'zg-menu zg-menu--static', role: 'menu', 'data-zg-dd': '', ref: p.open === true && p.onClose ? ddRef(p.onClose, { self: true }) : undefined }, ...(p.items || []).map((it, i) => it.separator ? h('div', { key: i, class: 'zg-menu__sep' }) : h('button', { key: i, type: 'button', role: 'menuitem', disabled: it.disabled, 'aria-disabled': it.disabled ? 'true' : undefined, class: cx('zg-menu__i', it.tone === 'danger' && 'zg-menu__i--danger'), onClick: () => { it.onClick && it.onClick(it); p.onSelect && p.onSelect(it.value); } }, it.icon ? icon(h, it.icon, 16) : null, it.label, it.shortcut ? h('span', { class: 'zg-kbd' }, it.shortcut) : null))) },
63
+ Tooltip: { render: ({ h, props: p, children }) => h('span', { class: 'zg-tip', 'data-tip': p.text, tabIndex: 0 }, ...children) },
64
+ HoverCard: { render: ({ h, props: p, children }) => h('span', { class: 'zg-hc', tabIndex: 0 }, h('span', null, p.trigger), h('div', { class: 'zg-hc__c zg-glass' }, ...children)) },
65
+ Popover: { state: { open: false }, render: ({ C, h, props: p, state, set, children }) => h('span', { class: 'zg-dd' }, h(C.Button, { size: p.size, onClick: () => set({ open: !state.open }) }, p.label), state.open ? h('div', { class: 'zg-pop zg-glass', 'data-zg-dd': '', ref: ddRef(() => set({ open: false })) }, ...children) : null) },
66
+ CopyButton: { state: { done: false, err: false }, render: ({ C, h, props: p, state, set }) => { const reset = () => setTimeout(() => set({ done: false, err: false }), 1500), fail = e => { set({ done: false, err: true }); p.onError && p.onError(e); reset(); }, ok = () => { set({ done: true, err: false }); p.onCopy && p.onCopy(p.value); reset(); };
67
+ const msg = state.err ? 'Copy failed' : state.done ? 'Copied' : '';
68
+ return h('span', { style: { display: 'inline-flex' } }, h(C.Button, { size: p.size || 'sm', variant: state.err ? 'danger' : p.variant, icon: state.err ? 'triangle-alert' : state.done ? 'check' : 'copy', onClick: () => { try { const cb = typeof navigator !== 'undefined' && navigator.clipboard; if (!cb || !cb.writeText) return fail(new Error('Clipboard unavailable')); Promise.resolve(cb.writeText(p.value)).then(ok, fail); } catch (e) { fail(e); } } }, p.label || msg || 'Copy'),
69
+ h('span', { role: 'status', 'aria-live': 'polite', style: { position: 'absolute', width: '1px', height: '1px', overflow: 'hidden', clip: 'rect(0 0 0 0)', whiteSpace: 'nowrap' } }, msg)); } },
70
+ CommandBar: { render: ({ h, props: p }) => h('div', { class: cx('zg-cb', 'zg-glass') }, ...(p.items || []).map((it, i) => it.separator ? h('span', { key: i, class: 'zg-cb__s' }) : h('button', { key: i, type: 'button', class: 'zg-ibtn zg-ibtn--ghost zg-ibtn--sm', title: it.label, 'aria-label': it.label, disabled: it.disabled, onClick: () => p.onAction && p.onAction(it.id) }, icon(h, it.icon || 'circle')))) },
71
+ CommandPalette: { state: { q: '', a: 0 }, render: ({ h, props: p, state, set }) => { if (!p.open) return null; const f = (p.commands || []).filter(c => !state.q || (c.label + ' ' + (c.keywords || '')).toLowerCase().includes(state.q.toLowerCase())); const ai = f.length ? clamp(state.a, 0, f.length - 1) : -1; const run = c => { if (!c) return; p.onRun && p.onRun(c.id); p.onClose && p.onClose(); };
72
+ return h('div', { class: 'zg-modal-scrim', style: { alignItems: 'flex-start', paddingTop: '12vh' }, onClick: p.onClose }, h('div', { class: 'zg-modal', style: { maxWidth: '560px' }, role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Command palette', ref: trap({ onClose: p.onClose }), onClick: e => e.stopPropagation() },
73
+ h('div', { class: 'zg-modal__head' }, icon(h, 'search', 18), h('input', { value: state.q, autoFocus: true, 'data-autofocus': '', role: 'combobox', 'aria-expanded': 'true', 'aria-controls': 'zg-cp-list', 'aria-activedescendant': ai >= 0 ? 'zg-cp-opt-' + ai : undefined, 'aria-label': p.placeholder || 'Type a command…', placeholder: p.placeholder || 'Type a command…', onInput: e => set({ q: e.target.value, a: 0 }), onKeyDown: e => { if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); if (f.length) set({ a: (ai + (e.key === 'ArrowDown' ? 1 : -1) + f.length) % f.length }); } else if (e.key === 'Enter') { e.preventDefault(); run(f[ai]); } }, style: { flex: 1, border: 0, outline: 0, background: 'none', color: 'var(--zg-text-1)', font: '500 17px var(--zg-font)' } }), h('span', { class: 'zg-kbd' }, 'Esc')),
74
+ h('div', { class: 'zg-modal__body', id: 'zg-cp-list', role: 'listbox', style: { padding: '8px', gap: '2px' } }, ...(f.length ? f.map((c, i) => h('button', { key: c.id, type: 'button', id: 'zg-cp-opt-' + i, role: 'option', tabIndex: -1, 'aria-selected': String(i === ai), class: cx('zg-menu__i', i === ai && 'is-active'), ref: i === ai ? (el => { if (el && el.scrollIntoView) el.scrollIntoView({ block: 'nearest' }); }) : undefined, onMouseMove: () => { if (i !== ai) set({ a: i }); }, onClick: () => run(c) }, c.icon ? icon(h, c.icon, 16) : null, c.label, c.shortcut ? h('span', { class: 'zg-kbd' }, c.shortcut) : null)) : [h('div', { class: 'zg-empty' }, 'No commands')])))); } },
75
+ NotificationBell: { state: { open: false, seen: {}, all: false }, render: ({ C, h, props: p, state, set }) => { const ctl = !!(p.onRead || p.onMarkAllRead), isRead = i => !!i.read || (!ctl && (state.all || !!state.seen[i.id])), un = (p.items || []).filter(i => !isRead(i)).length;
76
+ return h('div', { class: 'zg-dd' }, h('button', { type: 'button', class: 'zg-ibtn', 'aria-label': 'Notifications', 'aria-expanded': String(state.open), onClick: () => set({ open: !state.open }) }, icon(h, 'bell'), un ? h('span', { class: 'zg-bell__n' }, un) : null),
77
+ state.open ? h('div', { class: 'zg-pop zg-glass', 'data-zg-dd': '', ref: ddRef(() => set({ open: false })), style: { width: '320px', padding: '8px' } }, un ? h('button', { key: '_all', type: 'button', class: 'zg-menu__i', style: { justifyContent: 'flex-end', minHeight: '32px', color: 'var(--zg-brand-text)' }, onClick: () => { if (!ctl) set({ all: true }); p.onMarkAllRead && p.onMarkAllRead(); } }, 'Mark all read') : null, ...(p.items || []).map(it => h('div', { key: it.id, class: 'zg-menu__i', role: 'button', tabIndex: 0, style: { alignItems: 'flex-start', opacity: isRead(it) ? .6 : 1 }, onClick: () => { if (!isRead(it)) { if (!ctl) set({ seen: { ...state.seen, [it.id]: true } }); p.onRead && p.onRead(it.id); } p.onSelect && p.onSelect(it); }, onKeyDown: e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.currentTarget.click(); } } }, h('span', { style: { display: 'flex', flexDirection: 'column', gap: '2px', flex: 1 } }, h('b', { style: { fontSize: '14px' } }, it.title), h('span', { class: 'zg-hint' }, it.message), h('span', { class: 'zg-hint' }, it.time)), !isRead(it) ? h('span', { class: 'zg-tl__dot', style: { margin: '6px 4px' } }) : null))) : null); } },
78
+ ToggleGroup: { render: ({ h, props: p }) => { const cur = p.multiple ? (p.modelValue || []) : [p.modelValue]; return h('div', { class: 'zg-seg', role: 'group' }, ...(p.options || []).map(o => h('button', { key: o.value, type: 'button', 'aria-pressed': String(cur.includes(o.value)), title: o.label, onClick: () => p.onChange && p.onChange(p.multiple ? (cur.includes(o.value) ? cur.filter(v => v !== o.value) : [...cur, o.value]) : (p.allowEmpty && cur.includes(o.value) ? null : o.value)) }, o.icon ? icon(h, o.icon, 16) : null, p.iconOnly ? null : o.label))); } },
79
+ Fab: { render: ({ h, props: p }) => h('button', { type: 'button', class: 'zg-fab', 'aria-label': p.label || p.icon || 'plus', onClick: p.onClick }, icon(h, p.icon || 'plus', 20), p.label ? h('span', null, p.label) : null) },
80
+ /* ── Forms ── */
81
+ Textarea: { render: ({ h, props: p }) => lab(h, p, h('textarea', { class: cx('zg-ta', p.error && 'zg-input--error'), rows: p.rows || 4, value: p.modelValue ?? '', placeholder: p.placeholder, maxLength: p.maxlength, disabled: p.disabled, onInput: e => p.onChange && p.onChange(e.target.value) })) },
82
+ NumberInput: { state: { draft: null }, render: ({ C, h, props: p, state, set: put }) => { const v = p.modelValue ?? null, st = p.step || 1, lo = p.min ?? -Infinity, hi = p.max ?? Infinity, parse = s => { const t = String(s).trim().replace(',', '.'); if (t === '') return null; const n = Number(t); return Number.isFinite(n) ? n : NaN; }, emit = n => p.onChange && p.onChange(n), bump = d => { put({ draft: null }); emit(clamp(Math.round(((v ?? 0) + d) * 1e10) / 1e10, lo, hi)); };
83
+ return lab(h, p, h('span', { class: 'zg-input zg-num-in' }, h(C.IconButton, { icon: 'minus', label: 'Decrease', size: 'sm', onClick: () => bump(-st) }), h('input', { value: state.draft ?? (v ?? ''), inputMode: 'decimal', placeholder: p.placeholder, style: { textAlign: 'center' }, onInput: e => { const raw = e.target.value, n = parse(raw); put({ draft: raw }); if (n === null || !Number.isNaN(n)) emit(n); }, onBlur: e => { const n = parse(e.target.value); put({ draft: null }); if (n === null) { if (v !== null) emit(null); } else if (!Number.isNaN(n)) { const c = clamp(n, lo, hi); if (c !== v) emit(c); } } }), h(C.IconButton, { icon: 'plus', label: 'Increase', size: 'sm', onClick: () => bump(st) }))); } },
84
+ PasswordInput: { state: { show: false }, render: ({ C, h, props: p, state, set }) => lab(h, p, field(h, p, icon(h, 'lock-keyhole', 18), h('input', { type: state.show ? 'text' : 'password', value: p.modelValue ?? '', placeholder: p.placeholder, autoComplete: p.autocomplete || 'current-password', onInput: e => p.onChange && p.onChange(e.target.value) }), h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': state.show ? 'Hide password' : 'Show password', onClick: () => set({ show: !state.show }) }, icon(h, state.show ? 'eye-off' : 'eye', 16)))) },
85
+ SearchInput: { render: ({ h, props: p }) => h('span', { class: 'zg-input', style: { '--h': 'var(--zg-h-md)' } }, icon(h, 'search', 16), h('input', { type: 'search', value: p.modelValue ?? '', placeholder: p.placeholder || 'Search', onInput: e => p.onChange && p.onChange(e.target.value), onKeyDown: e => e.key === 'Enter' && p.onSearch && p.onSearch(p.modelValue) }), p.modelValue ? h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': 'Clear', onClick: () => p.onChange && p.onChange('') }, icon(h, 'x', 14)) : null) },
86
+ RadioGroup: { render: ({ h, props: p }) => h('div', { class: 'zg-field', role: 'radiogroup', 'aria-disabled': p.disabled ? 'true' : undefined, style: { gap: '10px' } }, p.label ? h('span', { class: 'zg-label' }, p.label) : null, ...(p.options || []).map(o => h('button', { key: o.value, type: 'button', role: 'radio', class: 'zg-check', 'aria-checked': String(o.value === p.modelValue), disabled: p.disabled || o.disabled, 'aria-disabled': p.disabled || o.disabled ? 'true' : undefined, onClick: () => !(p.disabled || o.disabled) && p.onChange && p.onChange(o.value) }, h('span', { class: 'zg-check__box zg-radio' }, o.value === p.modelValue ? h('i') : null), h('span', null, o.label, o.hint ? h('span', { class: 'zg-hint', style: { display: 'block' } }, o.hint) : null)))) },
87
+ Slider: { render: ({ h, props: p }) => { const mn = p.min ?? 0, mx = p.max ?? 100, v = p.modelValue ?? mn, pct = (v - mn) / (mx - mn) * 100;
88
+ return lab(h, p, h('span', { style: { display: 'flex', alignItems: 'center', gap: '12px' } }, h('input', { type: 'range', class: 'zg-range', min: mn, max: mx, step: p.step || 1, value: v, style: { '--p': pct + '%' }, onInput: e => p.onChange && p.onChange(Number(e.target.value)) }), p.showValue !== false ? h('span', { class: 'zg-mono', style: { minWidth: '48px', textAlign: 'end' } }, p.formatValue ? p.formatValue(v) : v) : null)); } },
89
+ Rating: { render: ({ h, props: p }) => h('div', { class: 'zg-rate', role: 'radiogroup', 'aria-label': p.label || 'Rating' }, ...Array.from({ length: p.max || 5 }, (_, i) => h('button', { key: i, type: 'button', disabled: p.readonly, 'aria-label': (i + 1) + ' stars', 'data-on': String(i < (p.modelValue || 0)), onClick: () => p.onChange && p.onChange(i + 1) }, icon(h, 'star', ({ xs: 14, sm: 18, md: 20, lg: 26, xl: 32 })[p.size] || p.size || 20)))) },
90
+ ChipInput: { state: { q: '' }, render: ({ h, props: p, state, set }) => { const v = p.modelValue || [];
91
+ const add = t => { t = t.trim(); if (t && !v.includes(t) && (!p.maxItems || v.length < p.maxItems)) p.onChange && p.onChange([...v, t]); set({ q: '' }); };
92
+ return lab(h, p, h('span', { class: 'zg-input zg-chipin' }, ...v.map(t => h('span', { key: t, class: 'zg-tag' }, t, h('button', { type: 'button', 'aria-label': 'Remove ' + t, onClick: () => p.onChange && p.onChange(v.filter(x => x !== t)) }, icon(h, 'x', 12)))), h('input', { value: state.q, placeholder: v.length ? '' : p.placeholder, onInput: e => set({ q: e.target.value }), onKeyDown: e => { if (e.key === 'Enter' || e.key === ',') { e.preventDefault(); add(state.q); } else if (e.key === 'Backspace' && !state.q && v.length) p.onChange && p.onChange(v.slice(0, -1)); } }))); } },
93
+ InlineEdit: { state: { edit: false, d: '' }, render: ({ C, h, props: p, state, set }) => state.edit ? h('span', { class: 'zg-input', style: { '--h': 'var(--zg-h-md)' } }, h('input', { value: state.d, autoFocus: true, onInput: e => set({ d: e.target.value }), onKeyDown: e => { if (e.key === 'Enter') { p.onChange && p.onChange(state.d); set({ edit: false }); } if (e.key === 'Escape') set({ edit: false }); } }), h(C.IconButton, { icon: 'check', label: 'Save', size: 'sm', onClick: () => { p.onChange && p.onChange(state.d); set({ edit: false }); } })) : h('button', { type: 'button', class: 'zg-inline', onClick: () => set({ edit: true, d: p.modelValue || '' }) }, p.modelValue || h('span', { class: 'zg-hint' }, p.emptyText || 'Click to edit'), icon(h, 'pencil', 14)) },
94
+ ColorPicker: { render: ({ h, props: p }) => h('div', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label) : null, h('div', { class: 'zg-chips' }, ...(p.presets || ['#BC926A', '#5FAACC', '#3FD13F', '#E26E5F', '#E8B964', '#A98BD6', '#1B1919']).map(c => h('button', { key: c, type: 'button', 'aria-label': c, 'aria-pressed': String(c === p.modelValue), class: 'zg-swatch', style: { background: c }, onClick: () => p.onChange && p.onChange(c) })))) },
95
+ FileUpload: { state: { drag: false }, render: ({ h, props: p, state, set }) => { const ac = String(p.accept || '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean), okType = f => !ac.length || ac.some(a => a[0] === '.' ? f.name.toLowerCase().endsWith(a) : a.endsWith('/*') ? (f.type || '').toLowerCase().startsWith(a.slice(0, -1)) : (f.type || '').toLowerCase() === a), rej = (f, r) => p.onReject && p.onReject(f, r),
96
+ take = list => { const ok = []; for (const f of list) { if (!okType(f)) rej(f, 'type'); else if (p.maxSize && f.size > p.maxSize) rej(f, 'size'); else ok.push(f); } if (!p.multiple) ok.slice(1).forEach(f => rej(f, 'count')); const out = p.multiple ? ok : ok.slice(0, 1); if (out.length && p.onChange) p.onChange(out); }, pick = e => { const i = e.currentTarget.querySelector('input'); if (i) i.click(); };
97
+ return h('div', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label) : null, h('div', { class: 'zg-fdrop', role: 'button', tabIndex: 0, 'aria-label': p.title || 'Drop files here or browse', 'data-drag': state.drag ? '' : undefined, onClick: pick, onKeydown: e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); pick(e); } }, onDragenter: e => { e.preventDefault(); if (!state.drag) set({ drag: true }); }, onDragover: e => { e.preventDefault(); if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'; if (!state.drag) set({ drag: true }); }, onDragleave: e => { if (!e.currentTarget.contains(e.relatedTarget)) set({ drag: false }); }, onDrop: e => { e.preventDefault(); set({ drag: false }); take([...(e.dataTransfer ? e.dataTransfer.files : [])]); } }, icon(h, p.image ? 'image-up' : 'upload', 24), h('b', null, p.title || 'Drop files here or browse'), h('span', { class: 'zg-hint' }, p.hint || 'PDF, JPG up to 10 MB'), h('input', { type: 'file', multiple: p.multiple, accept: p.accept, tabIndex: -1, 'aria-hidden': 'true', style: { display: 'none' }, onClick: e => e.stopPropagation(), onChange: e => { take([...e.target.files]); e.target.value = ''; } })), p.error ? h('span', { class: 'zg-hint zg-hint--error', role: 'alert' }, p.error) : null); } },
98
+ Combobox: { state: { open: false, q: '' }, render: ({ h, props: p, state, set }) => { const cur = (p.options || []).find(o => o.value === p.modelValue), f = (p.options || []).filter(o => !state.q || o.label.toLowerCase().includes(state.q.toLowerCase()));
99
+ return lab(h, p, h('div', { class: 'zg-select' }, field(h, p, icon(h, 'search', 16), h('input', { value: state.open && !p.disabled ? state.q : (cur ? cur.label : ''), disabled: p.disabled, placeholder: p.placeholder || 'Search…', onFocus: () => set({ open: true, q: '' }), onInput: e => set({ q: e.target.value, open: true }) }), icon(h, 'chevron-down', 16)),
100
+ state.open && !p.disabled ? h('div', { class: 'zg-select__list', role: 'listbox', 'data-zg-dd': '', ref: ddRef(() => set({ open: false, q: '' })) }, ...(f.length ? f.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), onMouseDown: e => e.preventDefault(), onClick: () => { set({ open: false, q: '' }); p.onChange && p.onChange(o.value); } }, o.label)) : [h('div', { key: 'e', class: 'zg-empty' }, p.emptyText || '—')])) : null)); } },
101
+ MultiSelect: { state: { open: false }, render: ({ h, props: p, state, set }) => { const v = p.modelValue || [], tog = x => p.onChange && p.onChange(v.includes(x) ? v.filter(i => i !== x) : [...v, x]);
102
+ return h('div', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label) : null, h('div', { class: 'zg-select' }, h('button', { type: 'button', class: 'zg-btn zg-select__trigger', 'aria-haspopup': 'listbox', 'aria-expanded': String(state.open && !p.disabled), disabled: p.disabled, style: { height: 'var(--zg-h-lg)' }, onClick: () => set({ open: !state.open }) }, h('span', null, v.length ? v.length + ' selected' : (p.placeholder || '—')), icon(h, 'chevron-down', 16)),
103
+ state.open && !p.disabled ? h('div', { class: 'zg-select__list', role: 'listbox', 'aria-multiselectable': 'true', '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(v.includes(o.value)), onClick: () => tog(o.value) }, h('span', { class: 'zg-check__box', style: { width: '18px', height: '18px', borderRadius: '6px', background: v.includes(o.value) ? 'var(--zg-brand)' : 'transparent' } }, v.includes(o.value) ? icon(h, 'check', 12) : null), h('span', { style: { flex: 1, marginInlineStart: '10px' } }, o.label)))) : null)); } },
104
+ GroupedSelect: { state: { open: false }, render: ({ h, props: p, state, set }) => { const all = (p.groups || []).flatMap(g => g.options), cur = all.find(o => o.value === p.modelValue);
105
+ return h('div', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label) : null, h('div', { class: 'zg-select' }, h('button', { type: 'button', class: 'zg-btn zg-select__trigger', 'aria-haspopup': 'listbox', 'aria-expanded': String(state.open && !p.disabled), disabled: p.disabled, style: { height: 'var(--zg-h-lg)' }, onClick: () => set({ open: !state.open }) }, h('span', null, cur ? cur.label : (p.placeholder || '—')), icon(h, 'chevron-down', 16)),
106
+ state.open && !p.disabled ? h('div', { class: 'zg-select__list', role: 'listbox', 'data-zg-dd': '', ref: ddRef(() => set({ open: false })) }, ...(p.groups || []).flatMap(g => [h('div', { key: g.label, class: 'zg-sidenav__group', style: { padding: '8px 14px 2px' } }, g.label), ...g.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))])) : null)); } },
107
+ PhoneInput: { render: ({ C, h, props: p }) => { const CC = p.countries || [{ value: '+966', label: 'SA +966' }, { value: '+998', label: 'UZ +998' }, { value: '+7', label: 'RU +7' }, { value: '+90', label: 'TR +90' }, { value: '+20', label: 'EG +20' }], v = p.modelValue || {};
108
+ return h('div', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label) : null, h('div', { style: { display: 'grid', gridTemplateColumns: '130px minmax(0,1fr)', gap: '8px', alignItems: 'start' } }, h(C.Select, { options: CC, modelValue: v.code || CC[0].value, onChange: c => p.onChange && p.onChange({ ...v, code: c }) }), field(h, p, icon(h, 'phone', 16), h('input', { type: 'tel', inputMode: 'tel', value: v.number || '', placeholder: p.placeholder ?? '90 123 45 67', onInput: e => p.onChange && p.onChange({ ...v, number: e.target.value }) }))), typeof p.error === 'string' && p.error ? h('span', { class: 'zg-hint zg-hint--error', role: 'alert' }, p.error) : p.hint ? h('span', { class: 'zg-hint' }, p.hint) : null); } },
109
+ Calendar: { state: { ym: null }, render: ({ h, props: p, state, set }) => { const s = pd(p.modelValue), b = state.ym ? new Date(state.ym) : (s || new Date()), y = b.getFullYear(), m = b.getMonth(), loc = p.locale || 'en-GB', ws = p.weekStartsOn ?? 1, off = (new Date(y, m, 1).getDay() - ws + 7) % 7, dn = new Date(y, m + 1, 0).getDate(), today = new Date();
110
+ const wd = Array.from({ length: 7 }, (_, i) => new Intl.DateTimeFormat(loc, { weekday: 'narrow' }).format(new Date(2023, 0, 1 + ((i + ws) % 7))));
111
+ const same = (a, d) => a && a.getFullYear() === y && a.getMonth() === m && a.getDate() === d, ev = p.events || {};
112
+ return h('div', { class: 'zg-cal' }, h('div', { class: 'zg-cal__h' }, h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': 'Previous month', onClick: () => set({ ym: new Date(y, m - 1, 1).getTime() }) }, icon(h, 'chevron-left', 16)), h('b', null, new Intl.DateTimeFormat(loc, { month: 'long', year: 'numeric' }).format(b)), h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': 'Next month', onClick: () => set({ ym: new Date(y, m + 1, 1).getTime() }) }, icon(h, 'chevron-right', 16))),
113
+ h('div', { class: 'zg-cal__g' }, ...wd.map((d, i) => h('span', { key: 'w' + i, class: 'zg-cal__w' }, d)), ...Array.from({ length: off }, (_, i) => h('span', { key: 'o' + i })), ...Array.from({ length: dn }, (_, i) => { const d = i + 1, key = `${y}-${String(m + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`; return h('button', { key: d, type: 'button', class: 'zg-cal__d', 'aria-pressed': String(same(s, d)), 'data-today': String(same(today, d)), 'data-ev': ev[key] ? '' : undefined, title: ev[key] || undefined, onClick: () => p.onChange && p.onChange(new Date(y, m, d)) }, d); }))); } },
114
+ DatePicker: { state: { open: false }, render: ({ C, h, props: p, state, set }) => lab(h, p, h('div', { class: 'zg-select' }, h('button', { type: 'button', class: 'zg-btn zg-select__trigger', 'aria-haspopup': 'dialog', 'aria-expanded': String(state.open && !p.disabled), disabled: p.disabled, style: { height: 'var(--zg-h-lg)' }, onClick: () => set({ open: !state.open }) }, h('span', { style: { display: 'flex', gap: '10px', alignItems: 'center' } }, icon(h, 'calendar', 16), p.modelValue ? new Intl.DateTimeFormat(p.locale || 'en-GB', { dateStyle: 'medium' }).format(pd(p.modelValue) || new Date()) : (p.placeholder || 'Select date')), icon(h, 'chevron-down', 16)),
115
+ state.open && !p.disabled ? h('div', { class: 'zg-select__list', 'data-zg-dd': '', ref: ddRef(() => set({ open: false })), style: { padding: '12px' } }, h(C.Calendar, { modelValue: p.modelValue, locale: p.locale, weekStartsOn: p.weekStartsOn, onChange: d => { set({ open: false }); p.onChange && p.onChange(d); } })) : null)) },
116
+ TimePicker: { render: ({ C, h, props: p }) => { const [H0, M0] = (p.modelValue || '09:00').split(':'), H = String(H0 ?? '00').padStart(2, '0'), M = String(M0 ?? '00').padStart(2, '0'); const hs = Array.from({ length: 24 }, (_, i) => { const v = String(i).padStart(2, '0'); return { value: v, label: v }; }), st = Math.max(1, Math.min(60, Math.floor(+p.minuteStep) || 15)), mv = Array.from({ length: Math.ceil(60 / st) }, (_, i) => String(i * st).padStart(2, '0')); if (!mv.includes(M)) { mv.push(M); mv.sort(); } const ms = mv.map(v => ({ value: v, label: v }));
117
+ return h('div', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label) : null, h('div', { style: { display: 'flex', gap: '8px', alignItems: 'center' } }, h('div', { style: { flex: 1 } }, h(C.Select, { options: hs, disabled: p.disabled, modelValue: H, onChange: v => p.onChange && p.onChange(v + ':' + M) })), h('b', null, ':'), h('div', { style: { flex: 1 } }, h(C.Select, { options: ms, disabled: p.disabled, modelValue: M, onChange: v => p.onChange && p.onChange(H + ':' + v) })))); } },
118
+ FormField: { render: ({ h, props: p, children }) => h('div', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label, p.required ? h('span', { style: { color: 'var(--zg-red)' } }, ' *') : null) : null, ...children, p.error ? h('span', { class: 'zg-hint zg-hint--error' }, p.error) : p.hint ? h('span', { class: 'zg-hint' }, p.hint) : null) },
119
+ LoginForm: { state: { e: '', pw: '', rm: true }, render: ({ C, h, props: p, state, set }) => { const L = { email: 'Email or phone', password: 'Password', remember: 'Remember device', forgot: 'Forgot password?', submit: 'Sign in', ...(p.labels || {}) }; return h('form', { class: 'zg-field', style: { gap: '14px' }, onSubmit: ev => { ev.preventDefault(); p.onSubmit && p.onSubmit({ email: state.e, password: state.pw, remember: state.rm }); } },
120
+ p.error ? h(C.Alert, { tone: 'danger' }, p.error) : null, h(C.TextField, { label: L.email, icon: 'user-round', modelValue: state.e, onChange: v => set({ e: v }) }), h(C.PasswordInput, { label: L.password, modelValue: state.pw, onChange: v => set({ pw: v }) }),
121
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } }, h(C.Checkbox, { label: L.remember, modelValue: state.rm, onChange: v => set({ rm: v }) }), p.showForgot !== false ? h('a', { href: '#', onClick: e => { e.preventDefault(); p.onForgot && p.onForgot(); }, style: { color: 'var(--zg-brand-text)', fontWeight: 600, fontSize: '14px' } }, L.forgot) : null),
122
+ h('button', { type: 'submit', class: 'zg-btn zg-btn--primary zg-btn--lg zg-btn--block', disabled: p.loading }, p.loading ? h('span', { class: 'zg-spin' }) : null, p.submitLabel || L.submit)); } },
123
+ StepForm: { state: { i: 0 }, render: ({ C, h, props: p, state, set }) => { const st = p.steps || [], i = clamp(state.i, 0, st.length - 1);
124
+ return h('div', { class: 'zg-field', style: { gap: '18px' } }, h(C.Stepper, { steps: st.map(s => s.label), current: i }), h('div', null, st[i] && st[i].content), h('div', { style: { display: 'flex', gap: '8px', justifyContent: 'flex-end' } }, h(C.Button, { size: p.size, variant: 'ghost', disabled: i === 0, onClick: () => set({ i: i - 1 }) }, 'Back'), h(C.Button, { size: p.size, variant: 'primary', onClick: () => i === st.length - 1 ? p.onFinish && p.onFinish() : set({ i: i + 1 }) }, i === st.length - 1 ? (p.finishLabel || 'Finish') : 'Next'))); } },
125
+ /* ── Data ── */
126
+ Tag: { render: ({ h, props: p, children }) => h('span', { class: 'zg-tag' }, ...children, (p.onRemove || p.onClose) ? h('button', { type: 'button', 'aria-label': 'Remove', onClick: p.onRemove || p.onClose }, icon(h, 'x', 12)) : null) },
127
+ Kbd: { render: ({ h, children }) => h('kbd', { class: 'zg-kbd' }, ...children) },
128
+ StatsCard: { render: ({ h, props: p }) => h('div', { class: 'zg-kpi' }, h('div', { style: { display: 'flex', alignItems: 'center', gap: '10px' } }, p.icon ? h('span', { class: 'zg-toast__icon' }, icon(h, p.icon, 18)) : null, h('span', { class: 'zg-kpi__label' }, p.label)), h('span', { class: 'zg-kpi__value zg-num' }, p.value), p.delta ? h('span', { class: cx('zg-delta', p.trend === 'down' ? 'zg-delta--down' : 'zg-delta--up') }, (p.trend === 'down' ? '▼ ' : '▲ ') + p.delta) : null) },
129
+ KpiInline: { render: ({ h, props: p }) => h('span', { class: 'zg-kpii' }, h('span', { class: 'zg-hint' }, p.label), h('b', { class: 'zg-num zg-kpii__v', style: { fontSize: '20px', fontWeight: 300 } }, p.value), p.delta ? h('span', { class: cx('zg-delta', String(p.delta).startsWith('-') ? 'zg-delta--down' : 'zg-delta--up') }, p.delta) : null) },
130
+ MiniChart: { render: ({ h, props: p }) => { const v = arr(p.values).map(x => Math.max(0, num(x))), mx = Math.max(...v, 1), H = p.height || { xs: 28, sm: 36, md: 48, lg: 64, xl: 80 }[p.size] || 48, W = v.length * 14;
131
+ return p.type === 'line' ? h('svg', { width: '100%', height: H, viewBox: `0 0 ${Math.max(W, 10)} ${H}`, preserveAspectRatio: 'none', role: 'img' }, h('path', { d: v.map((x, i) => `${i ? 'L' : 'M'}${i * 14 + 7} ${H - 4 - (x / mx) * (H - 8)}`).join(' '), fill: 'none', stroke: 'var(--zg-brand)', 'stroke-width': 2, 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'vector-effect': 'non-scaling-stroke' }))
132
+ : h('div', { class: 'zg-mini', style: { height: H + 'px' } }, ...v.map((x, i) => h('i', { key: i, style: { height: Math.max(6, x / mx * 100) + '%', background: p.highlightLast && i === v.length - 1 ? 'var(--zg-brand)' : 'var(--zg-s3)' } }))); } },
133
+ Descriptions: { render: ({ h, props: p }) => h('dl', { class: 'zg-desc' }, ...(p.items || []).flatMap((it, i) => [h('dt', { key: 'k' + i }, it.label), h('dd', { key: 'v' + i }, it.value ?? '—')])) },
134
+ LazyImage: { render: ({ h, props: p }) => h('span', { role: 'img', 'aria-label': p.alt || '', class: 'zg-lazy', style: { aspectRatio: p.aspectRatio || '16/9', backgroundImage: p.src ? `url(${p.src})` : undefined, borderRadius: (p.rounded ?? 20) + 'px' } }) },
135
+ SystemStatus: { render: ({ h, props: p }) => h('div', { class: 'zg-sys' }, ...(p.items || []).map((it, i) => h('div', { key: i, class: 'zg-sys__r' }, h('span', { class: 'zg-badge__dot', style: { color: { ok: 'var(--zg-green)', warn: 'var(--zg-amber)', down: 'var(--zg-red)' }[it.state || 'ok'] } }), h('span', { style: { flex: 1 } }, it.name), h('span', { class: 'zg-mono', style: { color: { ok: 'var(--zg-green)', warn: 'var(--zg-amber)', down: 'var(--zg-red)' }[it.state || 'ok'] } }, it.value)))) },
136
+ TreeView: { state: { open: {} }, render: ({ h, props: p, state, set }) => { const node = (n, d) => { const has = n.children && n.children.length, o = state.open[n.id] ?? d < 1;
137
+ return h('div', { key: n.id }, h('button', { type: 'button', class: cx('zg-tree__n', p.selected === n.id && 'zg-tree__n--on'), style: { paddingInlineStart: 8 + d * 18 + 'px' }, 'aria-expanded': has ? String(o) : undefined, onClick: () => { has && set({ open: { ...state.open, [n.id]: !o } }); p.onSelect && p.onSelect(n.id); } }, has ? icon(h, o ? 'chevron-down' : 'chevron-right', 14) : h('span', { style: { width: '14px' } }), n.icon ? icon(h, n.icon, 16) : null, n.label), has && o ? n.children.map(c => node(c, d + 1)) : null); };
138
+ return h('div', { class: 'zg-tree' }, ...(p.nodes || []).map(n => node(n, 0))); } },
139
+ CalendarView: { render: ({ C, h, props: p }) => h(C.Calendar, { ...p, events: Object.fromEntries((p.events || []).map(e => [e.date, e.label])) }) },
140
+ /* ── Navigation / Layout ── */
141
+ SubNav: { render: ({ h, props: p }) => h('nav', { class: 'zg-subnav', 'aria-label': p.title }, p.title ? h('div', { class: 'zg-sidenav__group' }, p.title) : null, ...(p.items || []).map(it => h('button', { key: it.value, type: 'button', class: 'zg-navitem', 'aria-current': it.value === p.value ? 'page' : undefined, onClick: () => p.onChange && p.onChange(it.value) }, it.icon ? icon(h, it.icon, 16) : null, h('span', { style: { flex: 1, textAlign: 'start' } }, it.label), it.badge ? h('span', { class: 'zg-navitem__count' }, it.badge) : null))) },
142
+ ActionRail: { render: ({ C, h, props: p }) => h('div', { class: 'zg-rail zg-glass' }, ...(p.items || []).map((it, i) => it.separator ? h('span', { key: i, class: 'zg-cb__s' }) : h(C.IconButton, { key: i, icon: it.icon, label: it.label, ghost: it.value !== p.value, onClick: () => p.onChange && p.onChange(it.value) }))) },
143
+ SplitLayout: { render: ({ h, props: p, slots, children }) => h('div', { class: 'zg-split', style: { gridTemplateColumns: p.columns || '1fr 1fr' } }, h('div', null, slots.left || children[0] || null), h('div', null, slots.right || children[1] || null)) },
144
+ SettingsSection: { render: ({ h, props: p, children }) => h('section', { class: 'zg-setsec' }, h('div', null, h('b', { style: { fontSize: '16px' } }, p.title), p.description ? h('p', { class: 'zg-hint', style: { margin: '4px 0 0', fontSize: '13px' } }, p.description) : null), h('div', { class: 'zg-field', style: { gap: '14px' } }, ...children)) },
145
+ ScrollArea: { render: ({ h, props: p, children }) => h('div', { class: 'zg-scroll', style: { maxHeight: p.maxHeight || '240px' } }, ...children) },
146
+ /* ── CRM ── */
147
+ DealCard: { render: ({ C, h, props: p }) => h('div', { class: 'zg-lead zg-dm' }, h('div', { class: 'zg-lead__row' }, h('b', { style: { fontSize: dzf(14), flex: 1 } }, p.title), p.stage ? h(C.Badge, { tone: p.tone || 'info' }, p.stage) : null), h('div', { class: 'zg-num', style: { fontSize: dzf(26), fontWeight: 300 } }, p.amount), h(C.Progress, { value: p.probability ?? 0, label: 'Probability', meta: (p.probability ?? 0) + '%' }), h('div', { class: 'zg-lead__row' }, h(C.Avatar, { name: p.owner, size: dzp(p, 24) }), h('span', { class: 'zg-hint' }, p.owner), h('span', { class: 'zg-hint', style: { marginInlineStart: 'auto' } }, p.due))) },
148
+ PipelineBoard: { state: { over: null }, render: ({ C, h, props: p, state, set }) => { const mv = p.onMove, stages = arr(p.stages).filter(Boolean);
149
+ const drop = (e, to) => { e.preventDefault(); const d = dragging; dragging = null; if (state.over !== null) set({ over: null }); if (d && d.from !== to) mv({ id: d.id, from: d.from, to }); };
150
+ return h('div', { style: { display: 'flex', gap: '14px', overflowX: 'auto', paddingBottom: '4px' } }, ...stages.map((s, si) => { const cards = arr(s.cards).filter(Boolean), sid = s.id ?? si;
151
+ const col = h(C.KanbanColumn, { key: sid, title: s.title, count: cards.length, size: p.size }, ...cards.map((c, i) => { const deal = h(C.DealCard, { key: c.id ?? i, size: p.size, ...c });
152
+ return mv ? h('div', { key: c.id ?? i, class: 'zg-drag', draggable: 'true', onDragStart: e => { dragging = { id: c.id ?? i, from: sid }; if (e.dataTransfer) { e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', String(c.id ?? i)); } }, onDragEnd: () => { dragging = null; if (state.over !== null) set({ over: null }); } }, deal) : deal; }));
153
+ return mv ? h('div', { key: sid, class: 'zg-drop', 'data-over': String(state.over === sid && !!dragging && dragging.from !== sid), onDragOver: e => { if (!dragging) return; e.preventDefault(); if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'; if (state.over !== sid) set({ over: sid }); }, onDragLeave: e => { if (!e.currentTarget.contains(e.relatedTarget) && state.over === sid) set({ over: null }); }, onDrop: e => drop(e, sid) }, col) : col; })); } },
154
+ ContactPanel: { render: ({ C, h, props: p }) => h('div', { class: 'zg-card zg-glass zg-dm' }, h('div', { class: 'zg-lead__row' }, h(C.Avatar, { name: p.name, size: dzp(p, 56) }), h('div', null, h('b', { style: { fontSize: dzf(18) } }, p.name), h('div', { class: 'zg-hint' }, p.role))), h(C.Descriptions, { items: [{ label: 'Phone', value: p.phone }, { label: 'Telegram', value: p.telegram }, { label: 'Email', value: p.email }, { label: 'City', value: p.city }] }), h('div', { style: { display: 'flex', gap: '8px' } }, h(C.Button, { variant: 'primary', size: 'sm', icon: 'phone', onClick: () => p.onCall && p.onCall() }, (p.labels && p.labels.call) || 'Call'), h(C.Button, { size: 'sm', icon: 'send', onClick: () => p.onMessage && p.onMessage() }, (p.labels && p.labels.message) || 'Message'))) },
155
+ SalesFunnel: { render: ({ h, props: p }) => { const st = arr(p.steps).map(s => s || {}), mx = Math.max(...st.map(s => num(s.value)), 1);
156
+ return h('div', { class: 'zg-field', style: { gap: '8px' } }, ...st.map((s, i) => h('div', { key: i, style: { display: 'grid', gridTemplateColumns: '120px minmax(0,1fr) 56px', gap: '12px', alignItems: 'center', fontSize: '14px' } }, h('span', null, s.label), h('span', { style: { display: 'flex', height: '32px', borderRadius: '12px', background: 'var(--zg-s2)' } }, h('span', { style: { width: (Math.max(0, num(s.value)) / mx * 100) + '%', borderRadius: '12px', background: i === st.length - 1 ? 'var(--zg-brand)' : 'color-mix(in oklch,var(--zg-sky) 40%,var(--zg-s2))' } })), h('b', { class: 'zg-mono', style: { textAlign: 'end' } }, fmt.num(s.value))))); } },
157
+ /* ── Hotels ── */
158
+ BookingCard: { render: ({ C, h, props: p }) => h('div', { class: 'zg-lead zg-dm' }, h('div', { class: 'zg-lead__row' }, h('b', { class: 'zg-mono', style: { fontSize: dzf(14), flex: 1 } }, p.code), h(C.Badge, { tone: { Confirmed: 'success', Pending: 'warning', Cancelled: 'danger' }[p.status], dot: true }, p.status)), h('div', { class: 'zg-lead__row' }, h(C.Avatar, { name: p.guest, size: dzp(p, 32) }), h('div', null, h('b', { style: { fontSize: dzf(14) } }, p.guest), h('div', { class: 'zg-hint' }, p.hotel))), h('div', { class: 'zg-lead__row zg-hint' }, icon(h, 'calendar', dzp(p, 14)), p.dates, h('span', { class: 'zg-num', style: { marginInlineStart: 'auto', fontSize: dzf(16), fontWeight: 600, color: 'var(--zg-text-1)' } }, fmt.sar(p.amount)))) },
159
+ GuestCard: { render: ({ C, h, props: p }) => h('div', { class: 'zg-card zg-glass zg-dm' }, h('div', { class: 'zg-lead__row' }, h(C.Avatar, { name: p.name, size: dzp(p, 48) }), h('div', { style: { flex: 1 } }, h('b', { style: { fontSize: dzf(16) } }, p.name), h('div', { class: 'zg-hint' }, p.country, ' · ', p.nights, ' nights')), p.vip ? h(C.Badge, { tone: 'brand' }, 'VIP') : null), h('div', { class: 'zg-chips' }, ...(p.tags || []).map(t => h('span', { key: t, class: 'zg-tag' }, t)))) },
160
+ RoomGrid: { render: ({ h, props: p }) => { const T = { ...ROOM_TONES, ...(p.statusTones || {}) }; return h('div', { class: 'zg-rooms zg-dm' }, ...arr(p.rooms).filter(Boolean).map((r, i) => { const tone = TONE_OK.includes(T[r.status]) ? T[r.status] : undefined; return h('button', { key: r.no ?? i, type: 'button', class: cx('zg-room', tone && 'zg-t-' + tone), 'data-s': r.status, 'data-tone': tone, onClick: () => p.onSelect && p.onSelect(r.no) }, h('b', { class: 'zg-mono' }, r.no), h('span', null, r.type)); })); } },
161
+ RoomCalendar: { render: ({ h, props: p }) => { const D = p.days || 14; return h('div', { class: 'zg-rc zg-dm', style: { '--d': D } }, h('span'), ...Array.from({ length: D }, (_, i) => h('span', { key: 'd' + i, class: 'zg-rc__h' }, i + 1)), ...arr(p.rooms).filter(Boolean).flatMap(r => [h('b', { key: r.no, class: 'zg-mono' }, r.no), ...Array.from({ length: D }, (_, i) => { const b = arr(r.bookings).find(x => i >= x.from && i < x.from + x.nights); return h('i', { key: r.no + '_' + i, class: 'zg-rc__c', 'data-b': b ? (i === b.from ? 'start' : 'mid') : undefined, title: b ? b.guest : undefined }); })])); } },
162
+ OccupancyChart: { render: ({ h, props: p }) => h('div', { class: 'zg-occ' }, ...arr(p.values).map((v, i) => { v = clamp(num(v), 0, 100); return h('div', { key: i, class: 'zg-occ__c' }, h('span', { class: 'zg-hint zg-mono' }, v + '%'), h('i', { style: { height: v + '%', background: v > 90 ? 'var(--zg-brand)' : 'color-mix(in oklch,var(--zg-sky) 55%,var(--zg-s3))' } }), h('span', { class: 'zg-hint' }, arr(p.labels)[i] || '')); })) },
163
+ };
@@ -0,0 +1,35 @@
1
+ // @ziyarago/ui · components, part 3 — card patterns, tonal variants, shells for auth/profile/status pages.
2
+ import { icon, cx, fmt, ddRef } from './components.js';
3
+ const TONES = ['brand', 'sky', 'success', 'warning', 'danger', 'neutral'];
4
+ const tc = t => 'zg-t-' + (TONES.includes(t) ? t : 'brand');
5
+ const delta = (h, d) => d ? h('span', { class: cx('zg-delta', String(d).trim().startsWith('-') ? 'zg-delta--down' : 'zg-delta--up') }, (String(d).trim().startsWith('-') ? '▼ ' : '▲ ') + String(d).replace(/^[+-]/, '')) : null;
6
+
7
+ export const more3 = {
8
+ Chip: { render: ({ h, props: p, children }) => h('span', { class: cx('zg-chip2', tc(p.tone), 'zg-chip2--' + (p.variant || 'tonal'), p.size && 'zg-chip2--' + p.size, p.pill && 'zg-chip2--pill') }, p.icon ? icon(h, p.icon, 14) : null, ...children, (p.onClose || p.onRemove) ? h('button', { type: 'button', 'aria-label': 'Remove', onClick: p.onClose || p.onRemove }, icon(h, 'x', 12)) : null) },
9
+ IconTile: { render: ({ h, props: p }) => h('span', { class: cx('zg-tile', tc(p.tone), p.shape === 'circle' && 'zg-tile--circle'), style: { '--s': ({ xs: 28, sm: 32, md: 40, lg: 48, xl: 56 }[p.size] || p.size || 40) + 'px' } }, icon(h, p.icon || 'circle', Math.round(({ xs: 28, sm: 32, md: 40, lg: 48, xl: 56 }[p.size] || p.size || 40) * .5))) },
10
+ StatusIcon: { render: ({ h, props: p }) => h('span', { class: cx('zg-tile zg-tile--circle zg-tip', tc(p.tone)), 'data-tip': p.tip, tabIndex: 0, style: { '--s': ({ xs: 24, sm: 30, md: 36, lg: 44, xl: 52 }[p.size] || p.size || 36) + 'px' } }, icon(h, p.icon || 'circle', Math.round(({ xs: 24, sm: 30, md: 36, lg: 44, xl: 52 }[p.size] || p.size || 36) * .44))) },
11
+ UserCell: { render: ({ C, h, props: p }) => h('span', { class: 'zg-usercell' }, h(C.Avatar, { name: p.name, src: p.src, size: { xs: 24, sm: 30, md: 36, lg: 44, xl: 56 }[p.size] || p.size || 36 }), h('span', null, h('b', null, p.name), p.sub ? h('span', { class: 'zg-hint' }, p.sub) : null)) },
12
+ StatRow: { render: ({ C, h, props: p }) => h('div', { class: 'zg-srow' }, h(C.IconTile, { icon: p.icon, tone: p.tone, size: { xs: 28, sm: 34, md: 40, lg: 48, xl: 56 }[p.size] || p.size || 40 }), h('div', { style: { flex: 1, minWidth: 0 } }, h('b', { class: 'zg-srow__t', style: { fontSize: '15px' } }, p.title), p.sub ? h('div', { class: 'zg-hint' }, p.sub) : null), h('div', { style: { textAlign: 'end' } }, h('div', { class: 'zg-num zg-srow__v', style: { fontSize: '17px', fontWeight: 600 } }, p.value), delta(h, p.delta))) },
13
+ CountryRow: { render: ({ h, props: p }) => h('div', { class: 'zg-srow' }, h('span', { class: 'zg-tile zg-tile--circle zg-t-neutral', style: { '--s': '38px', font: '700 12px var(--zg-font)' } }, p.code), h('div', { style: { flex: 1, minWidth: 0 } }, h('b', { class: 'zg-num', style: { fontSize: '16px' } }, p.amount), h('div', { class: 'zg-hint' }, p.name)), delta(h, p.pct)) },
14
+ StatLine: { render: ({ C, h, props: p }) => h('div', { class: cx('zg-card zg-glass zg-statline', tc(p.tone)) }, h('div', { style: { display: 'flex', alignItems: 'center', gap: '12px' } }, h(C.IconTile, { icon: p.icon, tone: p.tone, size: { xs: 28, sm: 34, md: 40, lg: 48, xl: 56 }[p.size] || p.size || 40 }), h('span', { class: 'zg-num zg-statline__v', style: { fontSize: '26px', fontWeight: 400 } }, p.value)), h('span', { class: 'zg-hint' }, p.label), p.delta ? h('span', null, delta(h, p.delta)) : null, h('i', { class: 'zg-statline__bar' })) },
15
+ StatStrip: { render: ({ C, h, props: p }) => h('div', { class: 'zg-card zg-glass zg-strip' }, ...(p.items || []).map((it, i) => h('div', { key: i, class: 'zg-strip__i' }, h(C.IconTile, { icon: it.icon, tone: it.tone, size: { xs: 30, sm: 36, md: 44, lg: 52, xl: 60 }[p.size] || p.size || 44 }), h('div', null, h('div', { class: 'zg-num zg-strip__v', style: { fontSize: '22px', fontWeight: 400 } }, it.value), h('div', { class: 'zg-hint' }, it.label))))) },
16
+ VersusBar: { render: ({ h, props: p }) => { const a = { ...p.left }, b = { ...p.right }; a.value = Math.max(0, Number(a.value) || 0); b.value = Math.max(0, Number(b.value) || 0); const t = (a.value + b.value) || 1;
17
+ return h('div', { class: 'zg-vs' }, h('div', { class: 'zg-vs__r' }, h('div', null, h('div', { class: 'zg-hint' }, a.label), h('b', { class: 'zg-num', style: { fontSize: '20px' } }, fmt.num(a.value))), h('span', { class: 'zg-tile zg-tile--circle zg-t-neutral', style: { '--s': '32px', font: '700 11px var(--zg-font)' } }, 'VS'), h('div', { style: { textAlign: 'end' } }, h('div', { class: 'zg-hint' }, b.label), h('b', { class: 'zg-num', style: { fontSize: '20px' } }, fmt.num(b.value)))),
18
+ h('div', { class: 'zg-vs__bar' }, h('i', { style: { width: (a.value / t * 100) + '%', background: 'var(--zg-brand)' } }), h('i', { style: { flex: 1, background: 'var(--zg-sky)' } }))); } },
19
+ SegBar: { render: ({ h, props: p }) => { const s = (Array.isArray(p.segments) ? p.segments : []).filter(Boolean).map(x => ({ ...x, value: Math.max(0, Number(x.value) || 0) })), t = s.reduce((a, x) => a + x.value, 0) || 1, col = ['var(--zg-brand)', 'var(--zg-sky)', 'var(--zg-green)', 'var(--zg-amber)', 'var(--zg-red)'];
20
+ return h('div', { class: 'zg-field', style: { gap: '12px' } }, h('div', { class: 'zg-segbar' }, ...s.map((x, i) => h('i', { key: i, title: x.label, style: { width: (x.value / t * 100) + '%', background: x.color || col[i % 5] } }))), h('div', { class: 'zg-chips' }, ...s.map((x, i) => h('span', { key: i, class: 'zg-hint', style: { display: 'inline-flex', alignItems: 'center', gap: '6px' } }, h('i', { style: { width: '8px', height: '8px', borderRadius: '50%', background: x.color || col[i % 5] } }), x.label + ' · ' + x.value)))); } },
21
+ HeroBanner: { render: ({ C, h, props: p, children }) => h('section', { class: 'zg-hero zg-glass' }, h('div', { class: 'zg-hero__t' }, h('b', { style: { fontSize: '20px', fontWeight: 500 } }, p.title), p.text ? h('span', { style: { color: 'var(--zg-text-2)', lineHeight: 1.5 } }, p.text) : null, p.value ? h('span', { class: 'zg-num', style: { fontSize: '36px', fontWeight: 300 } }, p.value) : null, ...children, p.cta ? h(C.Button, { variant: 'primary', size: 'sm', onClick: p.onCta }, p.cta) : null), p.image ? h('span', { class: 'zg-hero__img', style: { backgroundImage: `url(${p.image})` } }) : null) },
22
+ TabTiles: { render: ({ h, props: p }) => h('div', { class: 'zg-tt' }, ...(p.items || []).map(it => h('button', { key: it.value, type: 'button', class: 'zg-tt__i', 'aria-pressed': String(it.value === p.value), onClick: () => p.onChange && p.onChange(it.value) }, icon(h, it.icon || 'circle', 20), h('span', null, it.label))), p.addable ? h('button', { type: 'button', class: 'zg-tt__i zg-tt__add', 'aria-label': 'Add', onClick: p.onAdd }, icon(h, 'plus', 20)) : null) },
23
+ PillTabs: { render: ({ h, props: p }) => h('div', { class: cx('zg-pt', p.vertical && 'zg-pt--v'), role: 'tablist' }, ...(p.items || []).map(it => h('button', { key: it.value, type: 'button', role: 'tab', 'aria-selected': String(it.value === p.value), onClick: () => p.onChange && p.onChange(it.value) }, it.icon ? icon(h, it.icon, 16) : null, it.label))) },
24
+ PillSelect: { state: { open: false }, render: ({ h, props: p, state, set }) => { const opts = p.options || [], v = p.modelValue !== undefined ? p.modelValue : p.value, open = state.open && !p.disabled; return h('div', { class: 'zg-dd' }, h('button', { type: 'button', class: 'zg-btn zg-btn--tonal zg-btn--sm', 'aria-haspopup': 'listbox', 'aria-expanded': String(open), disabled: p.disabled, onClick: () => set({ open: !state.open }) }, (opts.find(o => o.value === v) || {}).label || p.placeholder, icon(h, 'chevron-down', 14)), open ? h('div', { class: 'zg-menu', role: 'listbox', 'data-zg-dd': '', ref: ddRef(() => set({ open: false })) }, ...opts.map(o => h('button', { key: o.value, type: 'button', role: 'option', class: 'zg-menu__i', disabled: o.disabled, 'aria-disabled': o.disabled ? 'true' : undefined, 'aria-selected': String(o.value === v), onClick: () => { set({ open: false }); p.onChange && p.onChange(o.value); } }, o.label))) : null); } },
25
+ ChartCard: { state: { open: false }, render: ({ h, props: p, state, set, children }) => h('section', { class: 'zg-card zg-glass' }, 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),
26
+ p.menu ? h('div', { class: 'zg-dd' }, h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': 'More', 'aria-haspopup': 'menu', onClick: () => set({ open: !state.open }) }, icon(h, 'ellipsis-vertical')), state.open ? h('div', { class: 'zg-menu', role: 'menu', style: { insetInlineStart: 'auto', insetInlineEnd: 0 } }, ...p.menu.map(m => h('button', { key: m, type: 'button', role: 'menuitem', class: cx('zg-menu__i', m === 'Delete' && 'zg-menu__i--danger'), onClick: () => { set({ open: false }); p.onMenu && p.onMenu(m); } }, m))) : null) : null), ...children) },
27
+ ActionCard: { state: { collapsed: false, loading: false, removed: false }, render: ({ C, h, props: p, state, set, children }) => { const col = p.collapsed !== undefined ? !!p.collapsed && p.collapsed !== 'false' : state.collapsed, busy = p.loading !== undefined ? !!p.loading && p.loading !== 'false' : state.loading; return state.removed ? null : h('section', { class: 'zg-card zg-glass zg-lo' }, h('div', { class: 'zg-card__head' }, h('div', { style: { flex: 1 } }, h('h3', { class: 'zg-card__title' }, p.title), p.subtitle ? h('div', { class: 'zg-card__sub' }, p.subtitle) : null), h('div', { style: { display: 'flex', gap: '2px' } }, h(C.IconButton, { icon: col ? 'chevron-down' : 'chevron-up', label: col ? 'Expand' : 'Collapse', size: 'sm', ghost: true, onClick: () => { set({ collapsed: !col }); p.onCollapse && p.onCollapse(!col); } }), h(C.IconButton, { icon: 'refresh-cw', label: 'Refresh', size: 'sm', ghost: true, onClick: () => { if (p.loading === undefined) { set({ loading: true }); setTimeout(() => set({ loading: false }), 1800); } p.onRefresh && p.onRefresh(); } }), h(C.IconButton, { icon: 'x', label: 'Remove', size: 'sm', ghost: true, onClick: () => { set({ removed: true }); p.onRemove && p.onRemove(); } }))),
28
+ col ? null : h('div', null, ...children), busy ? h('div', { class: 'zg-lo__o' }, h(C.Spinner, { size: 28 })) : null); } },
29
+ Snackbar: { render: ({ h, props: p, children }) => h('div', { class: cx('zg-snack', p.variant === 'tonal' && 'zg-snack--tonal', tc(p.tone)), role: 'status' }, h('span', { style: { flex: 1 } }, ...children), p.action ? h('button', { type: 'button', onClick: p.onAction, style: { color: p.tone === 'success' ? 'var(--zg-green)' : 'var(--zg-red)' } }, p.action) : null, p.onClose ? h('button', { type: 'button', onClick: p.onClose, 'aria-label': 'Close' }, icon(h, 'x', 14)) : null) },
30
+ TourPopover: { render: ({ C, h, props: p }) => h('div', { class: 'zg-tour' }, h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } }, h('b', { style: { flex: 1, fontSize: '16px' } }, p.title), h('button', { type: 'button', 'aria-label': 'Close', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', onClick: p.onClose }, icon(h, 'x', 14))), h('span', { style: { fontSize: '14px', lineHeight: 1.5 } }, p.text), h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } }, h('span', { class: 'zg-hint zg-mono' }, p.step || ''), h('div', { style: { display: 'flex', gap: '8px' } }, p.onBack ? h(C.Button, { size: 'sm', onClick: p.onBack }, 'Back') : null, h(C.Button, { size: 'sm', variant: 'primary', onClick: p.onNext }, p.last ? 'Finish' : 'Next')))) },
31
+ ProfileHeader: { render: ({ C, h, props: p, children }) => h('section', { class: 'zg-card zg-glass zg-prof' }, h('div', { class: 'zg-prof__cover', style: { backgroundImage: p.cover ? `url(${p.cover})` : undefined } }), h('div', { class: 'zg-prof__row' }, h('span', { class: 'zg-prof__av' }, h(C.Avatar, { name: p.name, src: p.src, size: 104 })), h('div', { style: { flex: 1, minWidth: '200px', display: 'flex', flexDirection: 'column', gap: '8px' } }, h('b', { style: { fontSize: '22px', fontWeight: 500 } }, p.name), h('div', { class: 'zg-chips', style: { color: 'var(--zg-text-3)', fontSize: '13px' } }, ...(p.meta || []).map(m => h('span', { key: m.label, style: { display: 'inline-flex', gap: '6px', alignItems: 'center' } }, icon(h, m.icon, 14), m.label)))), ...children)) },
32
+ AuthSplit: { render: ({ h, props: p, children }) => h('div', { class: 'zg-root zg-split2' }, h('aside', { class: 'zg-split2__a' }, p.aside), h('main', { class: 'zg-split2__m' }, h('div', { style: { width: '100%', maxWidth: '420px', display: 'flex', flexDirection: 'column', gap: '20px' } }, p.logo || null, h('div', null, h('h1', { style: { margin: 0, font: '300 28px var(--zg-font)', letterSpacing: '-.03em' } }, p.title), p.subtitle ? h('p', { style: { margin: '6px 0 0', color: 'var(--zg-text-2)', lineHeight: 1.5 } }, p.subtitle) : null), ...children))) },
33
+ StatusPage: { state: { email: '' }, render: ({ C, h, props: p, children, state, set }) => { const K = { 404: ['404', 'Page not found', 'The page may have moved. Check the address or go back.', 'Back to home'], 401: ['401', 'Not authorized', 'You need permission to open this page.', 'Back to home'], maintenance: ['', 'Under maintenance', 'We are updating the service. It will be back shortly.', 'Back to home'], soon: ['', 'Coming soon', 'We are working on it. Leave your email and we will tell you first.', 'Notify me'] }[p.kind || '404'];
34
+ return h('div', { class: 'zg-es zg-es--page' }, p.codeNode || (K[0] ? h('span', { class: 'zg-num', style: { fontSize: 'clamp(80px,14vw,150px)', lineHeight: 1, color: 'var(--zg-brand-text)' } }, K[0]) : h('span', { class: 'zg-es__ico' }, icon(h, p.kind === 'soon' ? 'rocket' : 'wrench', 28))), p.messageNode || [h('b', { key: 't', style: { fontSize: '24px' } }, p.title || K[1]), h('span', { key: 'd' }, p.description || K[2])], p.actionNode || (p.kind === 'soon' ? h('div', { style: { display: 'flex', gap: '8px', flexWrap: 'wrap', justifyContent: 'center', width: '100%', maxWidth: '420px' } }, h('div', { style: { flex: '1 1 200px' } }, h(C.TextField, { placeholder: p.emailLabel || 'name@email.com', icon: 'mail', modelValue: state.email, onChange: v => set({ email: v }) })), h(C.Button, { variant: 'primary', onClick: () => p.onNotify && p.onNotify(state.email) }, p.notifyLabel || K[3])) : h(C.Button, { variant: 'primary', onClick: p.onAction }, p.actionLabel || K[3])), ...children); } }
35
+ };
@@ -0,0 +1,58 @@
1
+ // @ziyarago/ui · components, part 4 — gaps closed after the Vuexy audit.
2
+ import { icon, cx, fmt } from './components.js';
3
+ import { trap } from './dialog.js';
4
+ const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
5
+ const pd = v => { if (v == null || v === '') return null; if (v instanceof Date) return isNaN(v) ? null : v; if (typeof v === 'string') { const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(v.trim()); if (m) return new Date(+m[1], +m[2] - 1, +m[3]); } const d = new Date(v); return isNaN(d) ? null : d; };
6
+ const SAN_TAGS = new Set('a b i u s em strong strike del ins sub sup p div span br hr ul ol li blockquote pre code h1 h2 h3 h4 h5 h6 img table thead tbody tr td th font small mark'.split(' ')), SAN_DROP = new Set('script style iframe object embed link meta base form input button textarea select svg math noscript template frame frameset applet'.split(' ')), SAN_ATTR = new Set('href src alt title target rel class style colspan rowspan align color size width height'.split(' '));
7
+ const sanitizeHtml = html => { if (typeof document === 'undefined') return html; const t = document.createElement('template'); t.innerHTML = html;
8
+ const walk = node => { for (const c of [...node.childNodes]) { if (c.nodeType !== 1) { if (c.nodeType !== 3) c.remove(); continue; } const tag = c.tagName.toLowerCase(); if (SAN_DROP.has(tag)) { c.remove(); continue; } walk(c); if (!SAN_TAGS.has(tag)) { c.replaceWith(...c.childNodes); continue; }
9
+ for (const a of [...c.attributes]) { const n = a.name.toLowerCase(), v = a.value.replace(/[\s\u0000-\u001f]/g, '').toLowerCase(); if (!SAN_ATTR.has(n) || n.startsWith('on') || (/^(javascript|vbscript|data):/.test(v) && !(n === 'src' && /^data:image\//.test(v))) || (n === 'style' && /expression\(|javascript:|url\(|@import/.test(v))) c.removeAttribute(a.name); }
10
+ if (tag === 'a' && c.getAttribute('target') === '_blank') c.setAttribute('rel', 'noopener noreferrer'); } };
11
+ walk(t.content); return t.innerHTML; };
12
+
13
+ export const rules = {
14
+ required: () => v => (v == null || String(v).trim() === '') ? 'This field is required' : true,
15
+ email: () => v => !v || /^\S+@\S+\.\S+$/.test(v) || 'Enter a valid email',
16
+ url: () => v => !v || /^https?:\/\/\S+\.\S+/.test(v) || 'Enter a valid URL',
17
+ digits: n => v => !v || new RegExp(`^\\d{${n}}$`).test(v) || `Exactly ${n} digits`,
18
+ alpha: () => v => !v || /^[\p{L}\s]+$/u.test(v) || 'Letters only',
19
+ alphaDash: () => v => !v || /^[\p{L}\d_-]+$/u.test(v) || 'Letters, digits, dash and underscore',
20
+ minLength: n => v => !v || String(v).length >= n || `At least ${n} characters`,
21
+ maxLength: n => v => !v || String(v).length <= n || `At most ${n} characters`,
22
+ range: (a, b) => v => !v || (Number(v) >= a && Number(v) <= b) || `Between ${a} and ${b}`,
23
+ regex: (re, msg) => v => !v || re.test(v) || msg || 'Invalid format',
24
+ confirmed: get => v => v === get() || 'Values do not match',
25
+ password: () => v => !v || (/\d/.test(v) && /[A-ZА-Я]/.test(v) && String(v).length >= 8) || '8+ characters, a digit and a capital letter'
26
+ };
27
+
28
+ export const more4 = {
29
+ ValidatedField: { state: { v: undefined, touched: false }, render: ({ C, h, props: p, state, set }) => {
30
+ const val = state.v ?? p.modelValue ?? '', rs = p.rules || [];
31
+ let err = null; if (state.touched) for (const r of rs) { const x = r(val); if (x !== true) { err = x; break; } }
32
+ const Comp = p.type === 'password' ? C.PasswordInput : C.TextField;
33
+ return h(Comp, { ...p, modelValue: val, error: p.error || err, onChange: v => { set({ v, touched: true }); p.onChange && p.onChange(v); } }); } },
34
+ Footer: { render: ({ h, props: p, children }) => h('footer', { class: 'zg-foot' }, h('span', { style: { color: 'var(--zg-text-3)', fontSize: '13px' } }, p.copyright || '© 2026 ZiyaraGo'), h('nav', { style: { display: 'flex', gap: '16px', flexWrap: 'wrap' } }, ...(p.links || []).map(l => h('a', { key: l.label, href: l.href || '#', style: { color: 'var(--zg-brand-text)', fontSize: '13px', fontWeight: 600 } }, l.label))), ...children) },
35
+ DataGroup: { state: { open: {} , grp: {} }, render: ({ C, h, props: p, state, set }) => { const cols = p.columns || [];
36
+ const row = r => { const o = !!state.open[r.id]; return [h('tr', { key: r.id, 'aria-selected': o ? 'true' : undefined }, p.expand ? h('td', { style: { width: '44px' } }, h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-expanded': String(o), 'aria-label': 'Expand', onClick: () => set({ open: { ...state.open, [r.id]: !o } }) }, icon(h, o ? 'chevron-down' : 'chevron-right', 16))) : null, ...cols.map(c => h('td', { key: c.key, class: c.num ? 'num' : undefined }, c.render ? c.render(r[c.key], r) : r[c.key]))), o && p.expand ? h('tr', { key: r.id + 'x' }, h('td', { colSpan: cols.length + 1, style: { background: 'var(--zg-s1)', padding: '16px 20px' } }, p.expand(r))) : null]; };
37
+ return h('div', { class: 'zg-table-wrap' }, h('table', { class: 'zg-table' }, h('thead', null, h('tr', null, p.expand ? h('th') : null, ...cols.map(c => h('th', { key: c.key, class: c.num ? 'num' : undefined }, c.label)))),
38
+ h('tbody', null, ...(p.groups || []).flatMap(g => { const c = !!state.grp[g.label]; return [h('tr', { key: 'g' + g.label }, h('td', { colSpan: cols.length + 1, style: { padding: 0 } }, h('button', { type: 'button', class: 'zg-dg__g', onClick: () => set({ grp: { ...state.grp, [g.label]: !c } }), style: { all: 'unset', boxSizing: 'border-box', display: 'flex', alignItems: 'center', gap: '8px', width: '100%', padding: '10px 16px', cursor: 'pointer', fontWeight: 700, fontSize: '13px', background: 'var(--zg-s1)' } }, icon(h, c ? 'chevron-right' : 'chevron-down', 14), g.label + ' (' + g.rows.length + ')'))), ...(c ? [] : g.rows.flatMap(row))]; })))); } },
39
+ OptionCard: { render: ({ h, props: p }) => h('button', { type: 'button', role: p.multiple ? 'checkbox' : 'radio', 'aria-checked': String(!!p.checked), class: 'zg-opt', onClick: p.onClick }, p.image ? h('span', { class: 'zg-opt__img', style: { backgroundImage: `url(${p.image})` } }) : p.icon ? h('span', { class: 'zg-tile zg-t-brand', style: { '--s': '40px' } }, icon(h, p.icon, 20)) : null, h('span', { style: { display: 'flex', flexDirection: 'column', gap: '2px', flex: 1, textAlign: 'start' } }, h('b', { style: { fontSize: '15px' } }, p.title), p.desc ? h('span', { class: 'zg-hint' }, p.desc) : null), p.price ? h('b', { class: 'zg-num', style: { fontSize: '16px' } }, p.price) : null, h('span', { class: cx('zg-check__box', !p.multiple && 'zg-radio'), style: { background: p.checked ? 'var(--zg-brand)' : 'transparent' } }, p.checked ? (p.multiple ? icon(h, 'check', 12) : h('i')) : null)) },
40
+ OptionCards: { render: ({ C, h, props: p }) => { const cur = p.multiple ? (p.modelValue || []) : p.modelValue;
41
+ return h('div', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label) : null, h('div', { style: { display: 'grid', gridTemplateColumns: `repeat(auto-fit,minmax(min(100%,${p.min || 220}px),1fr))`, gap: '10px' } }, ...(p.options || []).map(o => h(C.OptionCard, { ...o, multiple: p.multiple, checked: p.multiple ? cur.includes(o.value) : cur === o.value, onClick: () => p.onChange && p.onChange(p.multiple ? (cur.includes(o.value) ? cur.filter(x => x !== o.value) : [...cur, o.value]) : o.value) })))); } },
42
+ RichEditor: { render: ({ h, props: p }) => { let ed; const san = x => p.sanitize ? sanitizeHtml(x) : x, emit = () => { if (!ed) return; const x = san(ed.innerHTML); ed.__zg = x; p.onChange && p.onChange(x); }; const cmd = (c, v) => () => { if (!ed) return; ed.focus(); try { document.execCommand(c, false, v); } catch (e) {} emit(); };
43
+ const B = [['bold', 'bold', 'Bold'], ['italic', 'italic', 'Italic'], ['underline', 'underline', 'Underline'], ['strikeThrough', 'strikethrough', 'Strike'], null, ['justifyLeft', 'align-left', 'Left'], ['justifyCenter', 'align-center', 'Center'], ['justifyRight', 'align-right', 'Right'], null, ['insertUnorderedList', 'list', 'Bullets'], ['insertOrderedList', 'list-ordered', 'Numbers'], null, ['formatBlock', 'quote', 'Quote', 'blockquote'], ['formatBlock', 'code', 'Code block', 'pre']];
44
+ const area = h('div', { class: 'zg-rte__c', contentEditable: 'true', role: 'textbox', 'aria-multiline': 'true', 'aria-label': p.label || 'Editor', 'data-ph': p.placeholder || 'Write something…', ref: el => { if (!el) return; ed = el; const want = p.modelValue ?? ''; if (el.__zg !== want && el.innerHTML !== want) el.innerHTML = san(want); el.__zg = want; }, onFocus: e => { ed = e.currentTarget; }, onInput: e => { ed = e.currentTarget; emit(); } });
45
+ return h('div', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label) : null, h('div', { class: 'zg-rte' }, h('div', { class: 'zg-rte__t' }, ...B.map((b, i) => b ? h('button', { key: b[0], type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', title: b[2], 'aria-label': b[2], onMouseDown: e => e.preventDefault(), onClick: cmd(b[0], b[3]) }, icon(h, b[1], 16)) : h('span', { key: 's' + i, class: 'zg-cb__s' }))), area)); } },
46
+ RangeSlider: { render: ({ h, props: p }) => { const mn = p.min ?? 0, mx = p.max ?? 100, [a, b] = p.modelValue || [mn, mx], pa = (a - mn) / (mx - mn) * 100, pb = (b - mn) / (mx - mn) * 100, f = p.formatValue || (v => v);
47
+ const inp = (v, k) => h('input', { type: 'range', class: 'zg-range2', min: mn, max: mx, step: p.step || 1, value: v, 'aria-label': k ? 'Max' : 'Min', onInput: e => { const x = Number(e.target.value); p.onChange && p.onChange(k ? [a, Math.max(x, a)] : [Math.min(x, b), b]); } });
48
+ return h('div', { class: 'zg-field' }, h('div', { style: { display: 'flex', justifyContent: 'space-between' } }, p.label ? h('span', { class: 'zg-label' }, p.label) : h('span'), h('span', { class: 'zg-mono', style: { fontSize: '13px' } }, f(a) + ' – ' + f(b))), h('div', { class: 'zg-range2w', style: { '--a': pa + '%', '--b': pb + '%' } }, h('i'), inp(a, 0), inp(b, 1))); } },
49
+ RangeCalendar: { state: { from: null, to: null, hover: null, ym: null }, render: ({ h, props: p, state, set }) => { const loc = p.locale || 'en-GB', ctl = p.modelValue !== undefined, mv = ctl ? (p.modelValue || []) : [], day = v => { const d = pd(v); return d ? new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime() : null; }, f = ctl ? day(mv[0]) : state.from, tt = ctl ? day(mv[1]) : state.to, b = state.ym ? new Date(state.ym) : (f != null ? new Date(f) : (pd(p.start) || new Date())), y = b.getFullYear(), m = b.getMonth(), ws = p.weekStartsOn ?? 1, off = (new Date(y, m, 1).getDay() - ws + 7) % 7, dn = new Date(y, m + 1, 0).getDate();
50
+ const wd = Array.from({ length: 7 }, (_, i) => new Intl.DateTimeFormat(loc, { weekday: 'narrow' }).format(new Date(2023, 0, 1 + ((i + ws) % 7)))), t = d => new Date(y, m, d).getTime(), e = tt ?? (f && state.hover && state.hover > f ? state.hover : null);
51
+ const pick = d => { const x = t(d); if (f == null || tt != null || x < f) { if (ctl) { set({ hover: null }); p.onChange && p.onChange([new Date(x), null]); } else set({ from: x, to: null, hover: null }); } else { if (!ctl) set({ to: x }); p.onChange && p.onChange([new Date(f), new Date(x)]); } };
52
+ return h('div', { class: 'zg-cal' }, h('div', { class: 'zg-cal__h' }, h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': 'Previous', onClick: () => set({ ym: new Date(y, m - 1, 1).getTime() }) }, icon(h, 'chevron-left', 16)), h('b', null, new Intl.DateTimeFormat(loc, { month: 'long', year: 'numeric' }).format(b)), h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': 'Next', onClick: () => set({ ym: new Date(y, m + 1, 1).getTime() }) }, icon(h, 'chevron-right', 16))),
53
+ h('div', { class: 'zg-cal__g' }, ...wd.map((d, i) => h('span', { key: 'w' + i, class: 'zg-cal__w' }, d)), ...Array.from({ length: off }, (_, i) => h('span', { key: 'o' + i })), ...Array.from({ length: dn }, (_, i) => { const d = i + 1, x = t(d), edge = x === f || x === e, mid = f && e && x > f && x < e; return h('button', { key: d, type: 'button', class: 'zg-cal__d', 'aria-pressed': String(!!edge), 'data-mid': mid ? '' : undefined, onMouseEnter: () => f != null && tt == null && set({ hover: x }), onClick: () => pick(d) }, d); }))); } },
54
+ StepDialog: { render: ({ C, h, props: p, children, slots }) => !p.open ? null : h('div', { class: 'zg-modal-scrim', onClick: p.onClose }, h('div', { class: 'zg-modal zg-stepdlg', role: 'dialog', 'aria-modal': 'true', 'aria-label': p.title, ref: trap({ onClose: p.onClose }), onClick: e => e.stopPropagation() },
55
+ h('aside', { class: 'zg-stepdlg__s' }, ...(p.steps || []).map((s, i) => h('div', { key: i, class: cx('zg-stepdlg__i', i < p.current && 'is-done', i === p.current && 'is-cur') }, h('span', { class: 'zg-step__n' }, i < p.current ? icon(h, 'check', 14) : i + 1), h('span', { style: { display: 'flex', flexDirection: 'column' } }, h('b', { style: { fontSize: '14px' } }, s.title), s.sub ? h('span', { class: 'zg-hint' }, s.sub) : null)))),
56
+ h('div', { style: { flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column' } }, 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', style: { flex: 1 } }, ...children), h('div', { class: 'zg-modal__foot', style: { justifyContent: 'space-between' } }, h(C.Button, { disabled: p.current === 0, onClick: p.onBack }, 'Previous'), h(C.Button, { variant: 'primary', onClick: p.onNext }, p.current === (p.steps || []).length - 1 ? 'Submit' : 'Next'))))) },
57
+ FullscreenDialog: { render: ({ C, h, props: p, children }) => !p.open ? null : h('div', { class: 'zg-fsd', role: 'dialog', 'aria-modal': 'true', 'aria-label': p.title, ref: trap({ onClose: p.onClose }) }, h('div', { class: 'zg-fsd__h' }, h(C.IconButton, { icon: 'x', label: 'Close', onClick: p.onClose }), h('b', { style: { flex: 1, fontSize: '18px' } }, p.title), h(C.Button, { variant: 'inverse', onClick: p.onSave }, p.saveLabel || 'Save')), h('div', { class: 'zg-fsd__b' }, ...children)) }
58
+ };
@@ -0,0 +1,48 @@
1
+ // @ziyarago/ui · components, part 6 — last catalog gaps: TreeSelect, ContextMenu, Sortable, Map, Screensaver, ImageUpload, Tour.
2
+ import { icon, cx, ddRef } from './components.js';
3
+ import { trap, life } from './dialog.js';
4
+ const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
5
+
6
+ export const more6 = {
7
+ TreeSelect: { state: { open: false, q: '', ex: {} }, render: ({ h, props: p, state, set }) => {
8
+ const flat = (ns, d = 0) => ns.flatMap(n => [{ ...n, d }, ...(n.children ? flat(n.children, d + 1) : [])]), all = flat(p.nodes || []), cur = all.find(n => n.id === p.modelValue), q = state.q.toLowerCase();
9
+ const node = (n, d) => { const has = n.children && n.children.length, o = q ? true : (state.ex[n.id] ?? d < 1);
10
+ if (q && !all.filter(x => x.label.toLowerCase().includes(q)).some(x => x.id === n.id || (n.children || []).some(function f(c) { return c.id === x.id || (c.children || []).some(f); }))) return null;
11
+ return h('div', { key: n.id }, h('button', { type: 'button', role: 'treeitem', disabled: n.disabled, 'aria-disabled': n.disabled ? 'true' : undefined, class: cx('zg-tree__n', n.id === p.modelValue && 'zg-tree__n--on'), style: { paddingInlineStart: 8 + d * 18 + 'px', width: '100%' }, onClick: () => { if (has && !q) set({ ex: { ...state.ex, [n.id]: !o } }); if (!has || p.selectGroups) { set({ open: false, q: '' }); p.onChange && p.onChange(n.id); } } }, has ? icon(h, o ? 'chevron-down' : 'chevron-right', 14) : h('span', { style: { width: '14px' } }), n.label), has && o ? n.children.map(c => node(c, d + 1)) : null); };
12
+ return h('div', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label) : null, h('div', { class: 'zg-select' }, h('button', { type: 'button', class: 'zg-btn zg-select__trigger', 'aria-haspopup': 'tree', 'aria-expanded': String(state.open && !p.disabled), disabled: p.disabled, style: { height: 'var(--zg-h-lg)' }, onClick: () => set({ open: !state.open }) }, h('span', null, cur ? cur.label : (p.placeholder || '—')), icon(h, 'chevron-down', 16)),
13
+ state.open && !p.disabled ? h('div', { class: 'zg-select__list', role: 'tree', 'data-zg-dd': '', ref: ddRef(() => set({ open: false, q: '' })), style: { padding: '8px' } }, h('span', { class: 'zg-input', style: { '--h': '36px', marginBottom: '6px' } }, icon(h, 'search', 14), h('input', { value: state.q, placeholder: 'Search', onInput: e => set({ q: e.target.value }) })), ...(p.nodes || []).map(n => node(n, 0))) : null)); } },
14
+ ContextMenu: { state: { x: 0, y: 0, open: false }, render: ({ h, props: p, state, set, children }) => {
15
+ const close = () => set({ open: false });
16
+ return h('div', { class: 'zg-ctx', onContextmenu: e => { e.preventDefault(); set({ open: true, x: e.clientX, y: e.clientY }); } }, ...children,
17
+ state.open ? h('div', { class: 'zg-ctx__scrim', onClick: close, onContextmenu: e => { e.preventDefault(); close(); } }, h('div', { class: 'zg-menu', role: 'menu', 'data-zg-dd': '', ref: ddRef(close, { self: true }), style: { position: 'fixed', left: Math.min(state.x, innerWidth - 240) + 'px', top: Math.min(state.y, innerHeight - (p.items || []).length * 42 - 20) + 'px', insetInlineStart: 'auto' }, onClick: e => e.stopPropagation() }, ...(p.items || []).map((it, i) => it.separator ? h('div', { key: i, class: 'zg-menu__sep' }) : h('button', { key: i, type: 'button', role: 'menuitem', class: cx('zg-menu__i', it.tone === 'danger' && 'zg-menu__i--danger'), onClick: () => { close(); p.onSelect && p.onSelect(it.value); } }, it.icon ? icon(h, it.icon, 16) : null, it.label, it.shortcut ? h('span', { class: 'zg-kbd' }, it.shortcut) : null)))) : null); } },
18
+ Sortable: { state: { drag: null, over: null }, render: ({ h, props: p, state, set }) => { const items = p.items || [];
19
+ const drop = (to, f) => { const from = f === undefined ? state.drag : f; set({ drag: null, over: null }); if (from == null || from === to) return; const a = items.slice(); const [m] = a.splice(from, 1); a.splice(to, 0, m); p.onChange && p.onChange(a); };
20
+ return h('div', { class: 'zg-sort', role: 'list' }, ...items.map((it, i) => h('div', { key: it.id ?? i, role: 'listitem', class: cx('zg-sort__i', state.over === i && state.drag !== i && 'is-over', state.drag === i && 'is-drag'), draggable: 'true', onDragstart: e => { e.dataTransfer.effectAllowed = 'move'; try { e.dataTransfer.setData('text/plain', String(i)); } catch (x) {} set({ drag: i }); }, onDragover: e => { e.preventDefault(); if (state.over !== i) set({ over: i }); }, onDrop: e => { e.preventDefault(); drop(i); }, onDragend: () => set({ drag: null, over: null }) },
21
+ h('span', { class: 'zg-sort__h', 'aria-hidden': 'true' }, icon(h, 'grip-vertical', 16)), h('div', { style: { flex: 1, minWidth: 0 } }, p.render ? p.render(it, i) : h('b', null, it.label)),
22
+ h('span', { style: { display: 'flex', gap: '2px' } }, h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': 'Move up', disabled: i === 0, onClick: () => drop(i - 1, i) }, icon(h, 'chevron-up', 14)), h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': 'Move down', disabled: i === items.length - 1, onClick: () => drop(i + 1, i) }, icon(h, 'chevron-down', 14)))))); } },
23
+ Map: { state: { lon: null, lat: null, z: null, dx: 0, dy: 0 }, render: ({ h, props: p, state, set }) => {
24
+ const W = p.width || 640, H = p.height || 360, z = state.z ?? p.zoom ?? 14, lat0 = state.lat ?? p.lat ?? 21.4225, lon0 = state.lon ?? p.lon ?? 39.8262, n = 2 ** z;
25
+ const px = (lat, lon) => [(lon + 180) / 360 * n * 256, (1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * n * 256];
26
+ const [cx0, cy0] = px(lat0, lon0), x0 = cx0 - W / 2, y0 = cy0 - H / 2, dark = p.theme === 'dark' || (p.theme == null && typeof document !== 'undefined' && document.documentElement.closest('[data-theme="dark"]') != null);
27
+ const tiles = []; for (let tx = Math.floor(x0 / 256); tx <= Math.floor((x0 + W) / 256); tx++) for (let ty = Math.floor(y0 / 256); ty <= Math.floor((y0 + H) / 256); ty++) if (ty >= 0 && ty < n) tiles.push(h('img', { key: tx + '_' + ty, alt: '', draggable: 'false', src: `https://${'abc'[(tx + ty) & 1]}.basemaps.cartocdn.com/${p.style || (dark ? 'dark_all' : 'light_all')}/${z}/${((tx % n) + n) % n}/${ty}.png`, style: { position: 'absolute', left: (tx * 256 - x0) + 'px', top: (ty * 256 - y0) + 'px', width: '256px', height: '256px' } }));
28
+ const unpx = (x, y) => { const lon = x / (n * 256) * 360 - 180, a = Math.PI * (1 - 2 * y / (n * 256)); return [Math.atan(Math.sinh(a)) * 180 / Math.PI, lon]; };
29
+ let el; const pan = e => { const sx = e.clientX, sy = e.clientY; el.setPointerCapture(e.pointerId); const mv = ev => { const [la, lo] = unpx(cx0 - (ev.clientX - sx), cy0 - (ev.clientY - sy)); set({ lat: la, lon: lo }); }; const up = () => { el.removeEventListener('pointermove', mv); el.removeEventListener('pointerup', up); }; el.addEventListener('pointermove', mv); el.addEventListener('pointerup', up); };
30
+ const mk = (p.markers || []).map((m, i) => { const [mx, my] = px(m.lat, m.lon); return h('button', { key: 'm' + i, type: 'button', class: cx('zg-pin', m.tone && 'zg-t-' + m.tone), title: m.label, style: { left: (mx - x0) + 'px', top: (my - y0) + 'px' }, onClick: () => p.onSelect && p.onSelect(m) }, icon(h, m.icon || 'map-pin', 16)); });
31
+ el = h('div', { class: 'zg-map', role: 'application', 'aria-label': p.label || 'Map', style: { height: H + 'px' }, onPointerdown: pan }, ...tiles, ...mk,
32
+ h('div', { class: 'zg-map__z' }, h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm', 'aria-label': (p.labels && p.labels.zoomIn) || 'Zoom in', onPointerdown: e => e.stopPropagation(), onClick: () => set({ z: clamp(z + 1, 3, 18), lat: lat0, lon: lon0 }) }, icon(h, 'plus', 16)), h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm', 'aria-label': (p.labels && p.labels.zoomOut) || 'Zoom out', onPointerdown: e => e.stopPropagation(), onClick: () => set({ z: clamp(z - 1, 3, 18), lat: lat0, lon: lon0 }) }, icon(h, 'minus', 16))),
33
+ h('span', { class: 'zg-map__c' }, '© OpenStreetMap · CARTO'));
34
+ return el; } },
35
+ ImageUpload: { state: { src: null }, render: ({ h, props: p, state, set }) => { const src = state.src || p.modelValue, round = p.shape === 'circle';
36
+ return h('div', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label) : null, h('label', { class: 'zg-imgup', style: { aspectRatio: p.aspect || '1/1', maxWidth: (({ xs: 112, sm: 128, md: 160, lg: 192, xl: 224 })[p.size] || p.size || 160) + 'px', borderRadius: round ? '50%' : '24px', backgroundImage: src ? `url(${src})` : undefined } }, src ? null : h('span', { class: 'zg-imgup__e' }, icon(h, 'image-up', 24), h('span', { class: 'zg-hint' }, p.hint || 'Upload')), h('span', { class: 'zg-imgup__o' }, icon(h, 'camera', 18)),
37
+ h('input', { type: 'file', accept: 'image/*', style: { display: 'none' }, onChange: e => { const f = e.target.files[0]; if (!f) return; const r = new FileReader(); r.onload = () => { set({ src: r.result }); p.onChange && p.onChange(r.result, f); }; r.readAsDataURL(f); } }))); } },
38
+ Screensaver: { state: { t: 0 }, render: ({ h, props: p, state, set }) => { if (!p.open) return p.idleMs > 0 ? h('span', { hidden: true, 'aria-hidden': 'true', ref: life({ idleMs: p.idleMs, onIdle: p.onIdle, mount: (el, get) => { let tm; const arm = () => { clearTimeout(tm); tm = setTimeout(() => { const o = get(); o.onIdle && o.onIdle(); }, get().idleMs); }, evs = ['pointermove', 'pointerdown', 'keydown', 'wheel', 'touchstart']; evs.forEach(e => document.addEventListener(e, arm, { passive: true })); arm(); return () => { clearTimeout(tm); evs.forEach(e => document.removeEventListener(e, arm)); }; } }) }) : null;
39
+ const d = new Date(state.t || (typeof window === 'undefined' ? 0 : Date.now())), pad = n => String(n).padStart(2, '0'), ph = (d.getSeconds() % 40) / 40;
40
+ return h('div', { class: 'zg-saver', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Screensaver', tabIndex: 0, ref: trap({ onClose: p.onClose, onKey: e => { if (e.key === 'Tab') return false; p.onClose && p.onClose(); return true; }, mount: el => { const id = setInterval(() => set({ t: Date.now() }), 1000); set({ t: Date.now() }); return () => clearInterval(id); } }), onClick: p.onClose }, h('span', { class: 'zg-saver__g', style: { left: (10 + ph * 60) + '%', top: (20 + Math.sin(ph * 6.28) * 18) + '%' } }),
41
+ h('div', { class: 'zg-saver__c' }, h('span', { class: 'zg-saver__m', style: { backgroundImage: 'url(assets/brand/ziyarago-mark.svg)' } }), h('b', { class: 'zg-num', style: { fontSize: 'clamp(64px,12vw,160px)', fontWeight: 200, letterSpacing: '-.04em', lineHeight: 1 } }, pad(d.getHours()) + ':' + pad(d.getMinutes())), h('span', { style: { color: 'var(--zg-text-2)', fontSize: '18px' } }, new Intl.DateTimeFormat(p.locale || 'en-GB', { weekday: 'long', day: 'numeric', month: 'long' }).format(d)), p.hint ? h('span', { class: 'zg-hint' }, p.hint) : null)); } },
42
+ Tour: { state: { i: 0, r: null }, render: ({ C, h, props: p, state, set }) => { if (!p.open) return null; const st = p.steps || [], i = clamp(p.step != null ? p.step : state.i, 0, Math.max(0, st.length - 1)), s = st[i] || {}, el = s.target && typeof document !== 'undefined' && document.querySelector(s.target), el0 = el;
43
+ if (el && !state.r && !state.busy) { state.busy = true; setTimeout(() => { const el = document.querySelector(s.target) || el0, q = el.getBoundingClientRect(); if (q.top < 80 || q.bottom > innerHeight - 220) { let a = el.parentElement; while (a && !(/(auto|scroll)/.test(getComputedStyle(a).overflowY) && a.scrollHeight > a.clientHeight + 4)) a = a.parentElement; const dy = q.top - innerHeight / 3; if (a) a.scrollTop += dy; else window.scrollTo(0, window.scrollY + dy); } setTimeout(() => { state.busy = false; set({ r: (document.querySelector(s.target) || el).getBoundingClientRect() }); }, 160); }, 0); } if (!el && state.r) setTimeout(() => set({ r: null }), 0);
44
+ const r = el ? el.getBoundingClientRect() : null, pad = 8, below = r ? r.bottom + 12 + 190 < innerHeight : true;
45
+ const go = d => { const n = clamp(i + d, 0, st.length - 1); if (n === i) return; set({ i: n, r: null }); p.onStep && p.onStep(n); }, done = () => { set({ i: 0, r: null }); p.onClose && p.onClose(); };
46
+ return h('div', { class: 'zg-tourov', role: 'dialog', 'aria-label': 'Tour', ref: trap({ focus: false, onClose: done, mount: () => () => set({ i: 0, r: null }), onKey: e => { const rtl = document.documentElement.dir === 'rtl', k = e.key === 'ArrowRight' ? (rtl ? -1 : 1) : e.key === 'ArrowLeft' ? (rtl ? 1 : -1) : 0; if (k) { e.preventDefault(); go(k); return true; } } }) }, r ? h('span', { class: 'zg-tourov__cut', style: { left: (r.left - pad) + 'px', top: (r.top - pad) + 'px', width: (r.width + pad * 2) + 'px', height: (r.height + pad * 2) + 'px' } }) : h('span', { class: 'zg-tourov__full' }),
47
+ h('div', { style: { position: 'fixed', zIndex: 2, left: r ? clamp(r.left, 12, innerWidth - 392) + 'px' : '50%', top: r ? (below ? r.bottom + 12 : Math.max(12, r.top - 12 - 190)) + 'px' : '40%', transform: r ? 'none' : 'translateX(-50%)' } }, h(C.TourPopover, { title: s.title, text: s.text, step: (i + 1) + ' / ' + st.length, last: i === st.length - 1, onBack: i ? () => go(-1) : null, onNext: () => i === st.length - 1 ? done() : go(1), onClose: done }))); } }
48
+ };
@@ -0,0 +1,42 @@
1
+ // @ziyarago/ui · components, part 7 — gluestack parity: typography, layout primitives, Link, Pressable, Icon, Portal, sheets, ImageViewer, ChatAi, LiquidGlass.
2
+ import { icon, cx } from './components.js';
3
+ import { trap } from './dialog.js';
4
+ const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
5
+ const sp = v => (typeof v === 'number' ? v + 'px' : v);
6
+ const stack = (dir) => ({ render: ({ h, props: p, children }) => h(p.as || 'div', { class: 'zg-stack', style: { flexDirection: dir, gap: sp(p.gap ?? 12), alignItems: p.align, justifyContent: p.justify, flexWrap: p.wrap ? 'wrap' : undefined, ...(p.style || {}) } }, ...children) });
7
+
8
+ export const more7 = {
9
+ Heading: { render: ({ h, props: p, children }) => h('h' + clamp(p.level || 2, 1, 6), { class: cx('zg-heading', 'zg-heading--' + (p.size || 'lg'), p.brand && 'zg-heading--brand') }, ...children) },
10
+ Text: { render: ({ h, props: p, children }) => h(p.as || 'p', { class: cx('zg-text', 'zg-text--' + (p.size || 'md'), p.tone && 'zg-text--' + p.tone, p.bold && 'zg-text--bold', p.truncate && 'zg-text--truncate') }, ...children) },
11
+ Box: { render: ({ h, props: p, children }) => h(p.as || 'div', { class: cx('zg-box', p.glass && 'zg-glass'), style: { padding: sp(p.p), ...(p.style || {}) } }, ...children) },
12
+ Center: { render: ({ h, props: p, children }) => h(p.as || 'div', { class: 'zg-center', style: p.style }, ...children) },
13
+ HStack: stack('row'),
14
+ VStack: stack('column'),
15
+ Grid: { render: ({ h, props: p, children }) => h(p.as || 'div', { class: 'zg-grid', style: { gap: sp(p.gap ?? 16), gridTemplateColumns: p.cols ? `repeat(${p.cols}, minmax(0, 1fr))` : `repeat(auto-fit, minmax(${sp(p.min || 240)}, 1fr))`, ...(p.style || {}) } }, ...children) },
16
+ Link: { render: ({ h, props: p, children }) => h('a', { class: cx('zg-link', p.plain && 'zg-link--plain'), href: p.href || '#', target: p.external ? '_blank' : undefined, rel: p.external ? 'noopener noreferrer' : undefined, onClick: p.onClick }, ...children, p.external ? icon(h, 'external-link', 12) : null) },
17
+ Pressable: { render: ({ h, props: p, children }) => h(p.as || 'button', { type: (p.as || 'button') === 'button' ? 'button' : undefined, class: 'zg-press', disabled: p.disabled, 'aria-label': p.label, onClick: p.onClick }, ...children) },
18
+ Icon: { render: ({ h, props: p }) => icon(h, p.name || 'circle', { xs: 14, sm: 16, md: 20, lg: 24, xl: 32 }[p.size] || p.size || 20) },
19
+ Portal: { render: ({ props: p, children, portal }) => (portal ? portal(children, p.to) : null) },
20
+ LiquidGlass: { render: ({ h, props: p, children }) => h('div', { class: 'zg-liquid', style: { '--zg-liquid-r': sp(p.radius ?? 24), '--zg-liquid-b': sp(p.blur ?? 18) } }, ...children) },
21
+ BottomSheet: { render: ({ h, props: p, children, slots }) => !p.open ? null : h('div', { class: 'zg-modal-scrim zg-sheet-scrim', onClick: p.onClose }, h('div', { class: 'zg-sheet', role: 'dialog', 'aria-modal': 'true', 'aria-label': p.title, ref: trap({ onClose: p.onClose }), style: { maxHeight: sp(p.maxHeight || '85vh') }, onClick: e => e.stopPropagation() },
22
+ h('span', { class: 'zg-sheet__grab', 'aria-hidden': 'true' }), p.title ? h('h3', { class: 'zg-sheet__t' }, p.title) : null, h('div', { class: 'zg-sheet__b' }, ...children), (slots.footer || p.footer) ? h('div', { class: 'zg-sheet__f' }, slots.footer || p.footer) : null)) },
23
+ Actionsheet: { render: ({ C, h, props: p }) => h(C.BottomSheet, { open: p.open, size: p.size, onClose: p.onClose, title: p.title },
24
+ h('div', { class: 'zg-as' }, ...(p.items || []).map((it, i) => h('button', { key: i, type: 'button', class: cx('zg-as__i', it.tone && 'zg-as__i--' + it.tone), disabled: it.disabled, onClick: () => { it.onClick && it.onClick(); p.onSelect && p.onSelect(it); p.onClose && p.onClose(); } }, it.icon ? icon(h, it.icon, 18) : null, h('span', null, it.label)))),
25
+ h('button', { type: 'button', class: 'zg-as__i zg-as__i--cancel', onClick: p.onClose }, p.cancelLabel || 'Cancel')) },
26
+ ImageViewer: { state: { i: null }, render: ({ h, props: p, state, set }) => {
27
+ if (!p.open) return null;
28
+ const im = p.images || [], i = clamp(state.i ?? p.index ?? 0, 0, Math.max(0, im.length - 1)), go = d => set({ i: (i + d + im.length) % im.length });
29
+ return h('div', { class: 'zg-iv', role: 'dialog', 'aria-modal': 'true', 'aria-label': p.alt || 'Image viewer', onClick: p.onClose },
30
+ h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--ghost zg-iv__x', 'aria-label': 'Close', onClick: p.onClose }, icon(h, 'x', 20)),
31
+ im.length > 1 ? h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--ghost zg-iv__p', 'aria-label': 'Previous', onClick: e => { e.stopPropagation(); go(-1); } }, icon(h, 'chevron-left', 22)) : null,
32
+ h('img', { class: 'zg-iv__img', src: im[i], alt: p.alt || '', onClick: e => e.stopPropagation() }),
33
+ im.length > 1 ? h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--ghost zg-iv__n', 'aria-label': 'Next', onClick: e => { e.stopPropagation(); go(1); } }, icon(h, 'chevron-right', 22)) : null,
34
+ h('span', { class: 'zg-badge zg-iv__c' }, `${i + 1} / ${im.length}`)); } },
35
+ ChatAi: { state: { q: '' }, render: ({ h, props: p, state, set }) => {
36
+ const send = () => { const t = state.q.trim(); if (!t || p.loading) return; set({ q: '' }); p.onSend && p.onSend(t); };
37
+ return h('section', { class: 'zg-chat zg-glass', 'aria-label': p.title || 'Assistant' },
38
+ p.title ? h('div', { class: 'zg-chat__h' }, h('b', null, p.title), p.subtitle ? h('span', { class: 'zg-hint' }, p.subtitle) : null) : null,
39
+ h('div', { class: 'zg-chat__m', role: 'log', 'aria-live': 'polite' }, ...(p.messages || []).map((m, i) => h('div', { key: i, class: cx('zg-chat__b', 'zg-chat__b--' + (m.role === 'user' ? 'user' : 'ai')) }, m.text)), p.loading ? h('div', { class: 'zg-chat__b zg-chat__b--ai zg-chat__dots', 'aria-label': 'Typing' }, h('i'), h('i'), h('i')) : null),
40
+ h('div', { class: 'zg-chat__f' }, h('input', { class: 'zg-chat__in', value: state.q, placeholder: p.placeholder || 'Ask anything…', 'aria-label': 'Message', onInput: e => set({ q: e.target.value }), onKeydown: e => { if (e.key === 'Enter') { e.preventDefault(); send(); } } }),
41
+ h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--brand', 'aria-label': 'Send', disabled: p.loading || !state.q.trim(), onClick: send }, icon(h, 'send', 18)))); } },
42
+ };