@ziyarago/ui 0.13.0 → 0.15.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 (75) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +5 -5
  3. package/dist/llms-full.txt +3 -3
  4. package/dist/llms.txt +2 -2
  5. package/dist/manifest.json +316 -2
  6. package/docs/INTEGRATION.md +14 -0
  7. package/package.json +10 -25
  8. package/src/core/components.js +0 -11
  9. package/src/core/components2.js +1 -71
  10. package/src/core/components3.js +2 -44
  11. package/src/core/components4.js +0 -38
  12. package/src/core/components6.js +0 -50
  13. package/src/core/components7.js +0 -34
  14. package/src/core/index.js +6 -0
  15. package/src/core/pages.js +2 -36
  16. package/src/i18n/components/index.js +0 -72
  17. package/src/internal.js +10 -0
  18. package/src/meta/index.js +15 -0
  19. package/src/meta/pages.js +1 -1
  20. package/src/nuxt/module.mjs +9 -1
  21. package/src/react/index.js +8 -107
  22. package/src/resolver.js +17 -4
  23. package/src/vue/index.js +9 -2
  24. package/types/core.d.ts +3 -0
  25. package/types/internal.d.ts +30 -0
  26. package/types/meta.d.ts +8 -0
  27. package/types/nuxt.d.ts +2 -0
  28. package/types/react.d.ts +138 -4143
  29. package/types/resolver.d.ts +2 -0
  30. package/types/vue-global.d.ts +0 -106
  31. package/types/vue.d.ts +147 -3598
  32. package/docs/MIGRATION.md +0 -455
  33. package/src/compat/index.js +0 -469
  34. package/src/compat/map.js +0 -282
  35. package/src/compat/resolver.js +0 -22
  36. package/src/compat/vite.js +0 -29
  37. package/src/i18n/components/ActionCard.js +0 -5
  38. package/src/i18n/components/Actionsheet.js +0 -5
  39. package/src/i18n/components/CallCard.js +0 -5
  40. package/src/i18n/components/CallRow.js +0 -5
  41. package/src/i18n/components/Carousel.js +0 -5
  42. package/src/i18n/components/ChartCard.js +0 -5
  43. package/src/i18n/components/ChatAi.js +0 -5
  44. package/src/i18n/components/ChipInput.js +0 -5
  45. package/src/i18n/components/ContactPanel.js +0 -5
  46. package/src/i18n/components/DataGroup.js +0 -5
  47. package/src/i18n/components/DealCard.js +0 -5
  48. package/src/i18n/components/FullscreenDialog.js +0 -5
  49. package/src/i18n/components/Gallery.js +0 -5
  50. package/src/i18n/components/GuestCard.js +0 -5
  51. package/src/i18n/components/ImageUpload.js +0 -5
  52. package/src/i18n/components/ImageViewer.js +0 -5
  53. package/src/i18n/components/IncomingCall.js +0 -5
  54. package/src/i18n/components/InlineEdit.js +0 -5
  55. package/src/i18n/components/IntegrationCard.js +0 -5
  56. package/src/i18n/components/Map.js +0 -5
  57. package/src/i18n/components/NotificationBell.js +0 -5
  58. package/src/i18n/components/PricingCard.js +0 -5
  59. package/src/i18n/components/QiblaCompass.js +0 -5
  60. package/src/i18n/components/RamadanTracker.js +0 -5
  61. package/src/i18n/components/RangeCalendar.js +0 -5
  62. package/src/i18n/components/RangeSlider.js +0 -5
  63. package/src/i18n/components/RichEditor.js +0 -5
  64. package/src/i18n/components/Screensaver.js +0 -5
  65. package/src/i18n/components/Sortable.js +0 -5
  66. package/src/i18n/components/StepDialog.js +0 -5
  67. package/src/i18n/components/StepForm.js +0 -5
  68. package/src/i18n/components/TabTiles.js +0 -5
  69. package/src/i18n/components/Tour.js +0 -5
  70. package/src/i18n/components/TourCard.js +0 -5
  71. package/src/i18n/components/TourPopover.js +0 -5
  72. package/src/i18n/components/TreeSelect.js +0 -5
  73. package/types/compat-resolver.d.ts +0 -12
  74. package/types/compat-vite.d.ts +0 -11
  75. package/types/compat.d.ts +0 -176
@@ -151,3 +151,17 @@ React types: `Ui<Name>Props` (props, callbacks, named slots as `ReactNode` props
151
151
  ## 7. Checking the types yourself
152
152
 
153
153
  `npm run test:types` in this repository runs vue-tsc and tsc over `tests/types/*`: valid usage must compile, invalid enum / missing required prop / wrong event payload / wrong model type / unknown slot / wrong slot type must fail with the expected message.
154
+
155
+ ## 8. Add-on packages (plugins)
156
+
157
+ An add-on package (for example `@ziyarago/ui-pro`) plugs components into the same registry through `registerComponents` / `defineUiComponents`, so its definitions can use `C.Button` and the core components can reference them. A plugin ships its own stylesheet: import it after the core one.
158
+
159
+ ```ts
160
+ import '@ziyarago/ui/style.css'
161
+ import '@ziyarago/ui-pro/style.css' // plugin css, always after style.css
162
+ ```
163
+
164
+ Plugin CSS must only use the public design tokens (`--zg-*`, see `@ziyarago/ui/tokens.css`) and its own prefixed classes. Auto-import: pass `plugins: ['@ziyarago/ui-pro']` to the Nuxt module options (`ziyarago: { plugins: [...] }`) or to `ZiyaragoUiResolver({ plugins: [...] })`.
165
+
166
+ ## Migrating from local copies of glass-ui-kit
167
+ The migration tooling (compat layer with the old component names, resolver and Vite plugin, step by step guide) is part of `@ziyarago/ui-pro`.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ziyarago/ui",
3
- "version": "0.13.0",
4
- "description": "ZiyaraGo UI — glass design system for Umrah and Hajj products. OKLCH tokens, two palettes, dark and light, RTL, Thmanyah. One framework-neutral core with Vue 3, Nuxt and React adapters, 208 components and 40 page templates.",
3
+ "version": "0.15.0",
4
+ "description": "ZiyaraGo UI — glass design system for Umrah and Hajj products. OKLCH tokens, two palettes, dark and light, RTL, Thmanyah. One framework-neutral core with Vue 3, Nuxt and React adapters, 102 free components and 6 page templates (more in @ziyarago/ui-pro).",
5
5
  "keywords": [
6
6
  "ui",
7
7
  "design-system",
@@ -75,21 +75,13 @@
75
75
  "types": "./types/resolver.d.ts",
76
76
  "default": "./src/resolver.js"
77
77
  },
78
- "./compat": {
79
- "types": "./types/compat.d.ts",
80
- "default": "./src/compat/index.js"
81
- },
82
- "./compat/resolver": {
83
- "types": "./types/compat-resolver.d.ts",
84
- "default": "./src/compat/resolver.js"
85
- },
86
- "./compat/vite": {
87
- "types": "./types/compat-vite.d.ts",
88
- "default": "./src/compat/vite.js"
89
- },
90
78
  "./i18n": {
91
79
  "types": "./types/i18n.d.ts",
92
80
  "default": "./src/i18n/index.js"
81
+ },
82
+ "./internal": {
83
+ "types": "./types/internal.d.ts",
84
+ "default": "./src/internal.js"
93
85
  }
94
86
  },
95
87
  "files": [
@@ -102,8 +94,7 @@
102
94
  "CHANGELOG.md",
103
95
  "THIRD_PARTY_NOTICES.md",
104
96
  "src",
105
- "docs/INTEGRATION.md",
106
- "docs/MIGRATION.md"
97
+ "docs/INTEGRATION.md"
107
98
  ],
108
99
  "scripts": {
109
100
  "types": "node scripts/types.mjs",
@@ -185,17 +176,11 @@
185
176
  "resolver": [
186
177
  "types/resolver.d.ts"
187
178
  ],
188
- "compat": [
189
- "types/compat.d.ts"
190
- ],
191
- "compat/resolver": [
192
- "types/compat-resolver.d.ts"
193
- ],
194
- "compat/vite": [
195
- "types/compat-vite.d.ts"
196
- ],
197
179
  "meta": [
198
180
  "types/meta.d.ts"
181
+ ],
182
+ "internal": [
183
+ "types/internal.d.ts"
199
184
  ]
200
185
  }
201
186
  }
@@ -81,7 +81,6 @@ export const components = {
81
81
  CodeInput: { render: ({ h, props: p }) => { const n = p.length || 6, v = p.modelValue || '';
82
82
  return h('div', { class: 'zg-code', style: { '--n': n } }, ...Array.from({ length: n }, (_, i) => h('span', { key: i, class: cx('zg-code__cell', i === v.length && 'zg-code__cell--on') }, v[i] || '')),
83
83
  h('input', { value: v, inputMode: 'numeric', autoComplete: 'one-time-code', maxLength: n, 'aria-label': p.label || t('CodeInput', 'label'), onInput: e => { const x = e.target.value.replace(/\D/g, '').slice(0, n); p.onChange && p.onChange(x); if (x.length === n && v.length !== n) p.onComplete && p.onComplete(x); } })); } },
84
- Stepper: { render: ({ h, props: p }) => h('div', { class: 'zg-stepper' }, ...(p.steps || []).flatMap((s, i) => [i ? h('span', { key: 'b' + i, class: 'zg-step__bar' }) : null, h('span', { key: i, class: cx('zg-step', i < p.current && 'zg-step--done', i === p.current && 'zg-step--cur') }, h('span', { class: 'zg-step__n' }, i < p.current ? icon(h, 'check', 14) : i + 1), s)])) },
85
84
  Card: { slots: ['header', 'footer'], render: ({ h, props: p, children, slots }) => h('section', { class: cx('zg-card', 'zg-glass', p.variant === 'inverse' && 'zg-card--inverse', p.variant === 'gradient' && 'zg-card--grad', p.variant === 'gradient' && 'zg-card--t-' + (p.tone || 'brand'), p.variant === 'gradient' && p.intensity === 'vivid' && 'zg-card--vivid', p.variant === 'outline' && 'zg-card--outline', p.padding === 'none' && 'zg-card--flush', p.interactive && 'zg-card--click'), 'data-theme': p.variant === 'inverse' ? 'dark' : (p.variant === 'gradient' && p.intensity === 'vivid' ? (['brand', 'warning', undefined].includes(p.tone) ? 'light' : 'dark') : undefined), onClick: p.onClick, tabIndex: p.interactive && p.onClick ? 0 : undefined, onKeydown: p.interactive && p.onClick ? e => { if (e.key === 'Enter' && e.target === e.currentTarget) p.onClick(e); } : undefined },
86
85
  slots.header ? h('div', { class: 'zg-card__head' }, slots.header) : (p.title || slots.actions) ? h('div', { class: 'zg-card__head' }, h('div', { style: { flex: 1, minWidth: 0 } }, h('h3', { class: 'zg-card__title' }, p.title), p.subtitle ? h('div', { class: 'zg-card__sub' }, p.subtitle) : null), slots.actions || null) : null, ...children, slots.footer ? h('div', { class: 'zg-card__foot' }, slots.footer) : null) },
87
86
  Modal: { slots: ['title', 'subtitle'], render: ({ h, props: p, children, slots }) => { const foot = slots.footer || p.footer, sub = slots.subtitle || p.subtitle, ttl = slots.title || p.title; return !p.open ? null : h('div', { class: 'zg-modal-scrim', onClick: p.closeOnBackdrop === false ? undefined : p.onClose }, h('div', { class: 'zg-modal', role: 'dialog', 'aria-modal': 'true', 'aria-label': p.title, style: p.width ? { maxWidth: p.width } : undefined, ref: trap({ onClose: p.closeOnEsc === false ? undefined : p.onClose }), onClick: e => e.stopPropagation() },
@@ -109,8 +108,6 @@ export const components = {
109
108
  const th = c => h('th', { key: c.key, 'aria-sort': p.sortKey === c.key ? (p.sortDir === 'desc' ? 'descending' : 'ascending') : undefined, class: c.num ? 'num' : undefined, style: cs(c) }, c.sortable ? h('button', { type: 'button', onClick: () => p.onSort && p.onSort(c.key, p.sortKey === c.key && p.sortDir !== 'desc' ? 'desc' : 'asc') }, c.label, p.sortKey === c.key ? icon(h, p.sortDir === 'desc' ? 'arrow-down' : 'arrow-up', 12) : null) : c.label);
110
109
  return h('div', { class: 'zg-table-wrap', tabIndex: 0 }, h('table', { class: 'zg-table' }, h('thead', null, h('tr', null, p.onSelect ? h('th', { style: { width: '44px' } }, h('button', { type: 'button', role: 'checkbox', class: 'zg-check', 'aria-checked': String(!!all), 'aria-label': (p.labels && p.labels.selectAll) || t('DataTable', 'selectAll'), onClick: () => p.onSelect(all ? [] : rows.map(r => r.id)) }, h('span', { class: 'zg-check__box' }, all ? icon(h, 'check', 14) : null))) : null, ...cols.map(th))),
111
110
  h('tbody', null, ...(rows.length ? rows.map((r, ri) => h('tr', { key: r.id, 'aria-selected': sel.includes(r.id) ? 'true' : undefined, class: p.onRowClick ? 'zg-tr--click' : undefined, onClick: p.onRowClick ? () => p.onRowClick(r, ri) : undefined }, p.onSelect ? h('td', null, h('button', { type: 'button', role: 'checkbox', class: 'zg-check', 'aria-checked': String(sel.includes(r.id)), 'aria-label': (p.labels && p.labels.selectRow) || t('DataTable', 'selectRow'), onClick: () => p.onSelect(sel.includes(r.id) ? sel.filter(i => i !== r.id) : [...sel, r.id]) }, h('span', { class: 'zg-check__box' }, sel.includes(r.id) ? icon(h, 'check', 14) : null))) : null, ...cols.map(c => h('td', { key: c.key, class: c.num ? 'num' : undefined, style: cs(c) }, c.render ? c.render(r[c.key], r, h) : (r[c.key] ?? '—'))))) : [h('tr', { key: 'e' }, h('td', { colSpan: cols.length + 1 }, h('div', { class: 'zg-empty' }, slots.empty || p.empty || '—')))])))); } },
112
- KanbanColumn: { render: ({ h, props: p, children }) => h('div', { class: 'zg-kcol zg-dm' }, h('div', { class: 'zg-kcol__head' }, p.title, h('span', { class: 'zg-badge' }, p.count ?? children.length)), ...children) },
113
- KanbanCard: { render: ({ h, props: p, children }) => h('div', { class: 'zg-kcard zg-dm' }, h('b', { style: { fontSize: dzf(14) } }, p.title), p.sub ? h('span', { class: 'zg-hint' }, p.sub) : null, ...children, p.tag ? h('span', { class: cx('zg-badge', p.tone && 'zg-badge--' + p.tone), style: { alignSelf: 'flex-start' } }, p.tag) : null) },
114
111
  Timeline: { render: ({ h, props: p }) => h('div', { class: 'zg-timeline' }, ...(p.items || []).map((it, i, a) => h('div', { key: i, class: 'zg-tl' }, h('div', { class: 'zg-tl__rail' }, h('span', { class: 'zg-tl__dot' }), i < a.length - 1 ? h('span', { class: 'zg-tl__line' }) : null), h('div', { class: 'zg-tl__body' }, h('b', null, it.title), it.text ? h('span', null, it.text) : null, h('span', { class: 'zg-tl__time' }, it.time))))) },
115
112
  TopNav: { render: ({ h, props: p, slots }) => h('header', { class: 'zg-topnav' }, h('div', { class: 'zg-topnav__brand' }, p.brand), h('nav', { class: 'zg-topnav__items', 'aria-label': t('TopNav', 'label') }, ...(p.items || []).map(it => nav(h, it, p.value, p.onChange))), h('div', { class: 'zg-topnav__actions' }, slots.actions || p.actions || null)) },
116
113
  SideNav: { render: ({ h, props: p, slots }) => h('aside', { class: cx('zg-sidenav', p.collapsed && 'zg-sidenav--collapsed') }, h('div', { class: 'zg-sidenav__brand' }, p.brand), ...(p.items || []).map(it => it.group ? h('div', { key: it.group, class: 'zg-sidenav__group', style: p.collapsed ? { display: 'none' } : undefined }, it.group) : nav(h, it, p.value, p.onChange, p.collapsed)), h('div', { class: 'zg-sidenav__sp' }), slots.footer || p.footer || null,
@@ -122,13 +119,5 @@ export const components = {
122
119
  h('main', { class: 'zg-shell__main' }, ...children)); } },
123
120
  PageHeader: { render: ({ C, h, props: p, slots }) => h('div', { class: 'zg-page-head' }, h('div', { style: { flex: 1, minWidth: 0 } }, p.crumbs ? h(C.Breadcrumbs, { items: p.crumbs }) : null, h('h1', null, p.title), p.subtitle ? h('p', null, p.subtitle) : null), slots.actions || null) },
124
121
  AuthLayout: { render: ({ h, props: p, children }) => h('div', { class: 'zg-root zg-auth', style: p.style }, h('div', { class: 'zg-auth__card' }, p.logo || null, h('div', null, h('h1', { style: { margin: 0, font: '300 30px 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)) },
125
- LeadCard: { render: ({ C, h, props: p }) => h('div', { class: 'zg-lead zg-dm' }, h('div', { class: 'zg-lead__row' }, h(C.Avatar, { name: p.name, size: dzp(p, 32) }), h('div', { style: { flex: 1, minWidth: 0 } }, h('b', { style: { fontSize: dzf(14) } }, p.name), h('div', { class: 'zg-hint zg-mono' }, p.code)), p.tag ? h('span', { class: cx('zg-badge', p.tone && 'zg-badge--' + p.tone) }, p.tag) : null), h('div', { class: 'zg-lead__row', style: { fontSize: dzf(13), color: 'var(--zg-text-2)' } }, icon(h, 'phone', dzp(p, 14)), p.phone, h('span', { class: 'zg-num', style: { marginInlineStart: 'auto', fontSize: dzf(15), fontWeight: 600, color: 'var(--zg-text-1)' } }, p.amount))) },
126
- HotelCard: { render: ({ h, props: p }) => h('article', { class: 'zg-hotel zg-dm' }, h('div', { class: 'zg-hotel__img', style: { backgroundImage: p.image ? `url(${p.image})` : undefined } }, h('div', { class: 'zg-hotel__tags' }, ...(p.tags || []).map(t => h('span', { key: t, class: 'zg-badge zg-badge--brand' }, t)))), h('div', { class: 'zg-hotel__body' },
127
- h('div', { class: 'zg-hotel__row' }, h('div', { style: { minWidth: 0 } }, h('b', { style: { fontSize: dzf(17) } }, p.name), h('div', { class: 'zg-hint' }, '★'.repeat(p.stars || 0), ' · ', p.distance)), h('span', { class: 'zg-badge zg-badge--success', style: { marginInlineStart: 'auto' } }, p.rating)),
128
- h('div', { class: 'zg-hotel__row', style: { alignItems: 'center' } }, h('span', { class: 'zg-hint' }, 'SAR / ', p.night || 'night'), h('div', { class: 'zg-hotel__price' }, h('div', { class: 'zg-num', style: { fontSize: dzf(26), fontWeight: 300 } }, fmt.num(p.price))))) ) },
129
- TourCard: { render: ({ C, h, props: p, slots }) => { const seats = Number(p.seats) || 0, booked = Number(p.booked) || 0; return h('article', { class: 'zg-card zg-glass zg-dm' }, h('div', { class: 'zg-card__head' }, h('div', { style: { flex: 1 } }, h('h3', { class: 'zg-card__title' }, p.name), h('div', { class: 'zg-card__sub' }, [p.city, p.sessions != null && p.sessions !== '' ? p.sessions + ' sessions' : null].filter(Boolean).join(' · '))), h('span', { class: 'zg-badge zg-badge--info' }, '360°')), h(C.Progress, { value: seats > 0 ? Math.min(100, booked / seats * 100) : 0, label: t('TourCard', 'seats'), meta: `${booked}/${seats}` }), slots.actions || null); } },
130
- PrayerTimes: { render: ({ h, props: p }) => h('div', { class: 'zg-pt zg-dm' }, ...(p.items || []).map((it, i) => { it = it || {}; return h('div', { key: i, class: 'zg-pt__i', 'aria-current': i === p.next ? 'true' : undefined }, it.name, h('b', null, it.time)); })) },
131
- RamadanTracker: { render: ({ h, props: p }) => { const day = Number(p.day) || 0; return h('div', { class: 'zg-rt zg-dm', role: 'img', 'aria-label': day ? t('RamadanTracker', 'day', { day }) : t('RamadanTracker', 'label') }, ...Array.from({ length: 30 }, (_, i) => { const n = i + 1; return h('i', { key: n, 'data-s': n === day ? 'cur' : n < day ? 'past' : n > 20 ? 'last' : undefined, 'data-odd': n > 20 && n % 2 ? '' : undefined, title: String(n) }); })); } },
132
- CallRow: { render: ({ C, h, props: p }) => h('div', { class: 'zg-call zg-dm' }, h(C.Avatar, { name: p.name, size: dzp(p, 36) }), h('div', { style: { minWidth: 0 } }, h('b', { style: { fontSize: dzf(14) } }, p.name), h('div', { class: 'zg-hint' }, [p.status, fmt.dur(p.duration, p.durUnits)].filter(Boolean).join(' · '))), h(C.IconButton, { icon: 'play', label: (p.labels && p.labels.play) || t('CallRow', 'play'), size: dzb(p), onClick: p.onPlay })) },
133
122
  };
134
123
  export { fmt };
@@ -1,5 +1,5 @@
1
1
  // @ziyarago/ui · components, part 2 — Forms, Surfaces, Actions, Data, Navigation, Layout, CRM, Hotels.
2
- import { icon, cx, fmt, dzp, dzf, dzb, ddRef, pick, INPUT_ATTRS } from './components.js';
2
+ import { icon, cx, ddRef, pick, INPUT_ATTRS } from './components.js';
3
3
  import { trap } from './dialog.js';
4
4
  import { listKeys, openKeys, tabIdx, toTrigger, trigger } from './keys.js';
5
5
  const tipId = s => { let x = 5381; for (const c of String(s)) x = (x * 33 ^ c.charCodeAt(0)) >>> 0; return 'zg-tip-' + x.toString(36); };
@@ -11,26 +11,6 @@ const dloc = p => p.locale || (getUiLocale() === 'en' ? 'en-GB' : intlLocale());
11
11
  const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
12
12
  const num = v => Number.isFinite(+v) && v !== null && v !== '' ? +v : 0;
13
13
  const arr = v => Array.isArray(v) ? v : [];
14
- 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));
15
- // 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.
16
- const autoplay = (el, ms, step) => {
17
- if (!el || typeof window === 'undefined') return;
18
- const a = el.__zgAuto || (el.__zgAuto = { t: 0, ms: 0 });
19
- a.step = step;
20
- if (a.ms === ms) return;
21
- clearInterval(a.t); a.t = 0; a.ms = ms;
22
- if (!ms) return;
23
- a.t = setInterval(() => {
24
- if (!el.isConnected) { clearInterval(a.t); a.t = 0; return; }
25
- const m = el.closest('[data-motion]');
26
- if ((m && m.getAttribute('data-motion') === 'off') || document.hidden || (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) || el.matches(':hover, :focus-within')) return;
27
- a.step();
28
- }, ms);
29
- };
30
- let dragging = null;
31
- const ROOM_TONES = { occupied: 'brand', clean: 'success', dirty: 'warning', blocked: 'danger' };
32
- const TONE_OK = ['brand', 'sky', 'success', 'warning', 'danger', 'neutral'];
33
- 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' };
34
14
  export 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; };
35
15
  const lab =(h, p, body) => h('label', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label, p.required ? h('span', { class: 'zg-req', 'aria-hidden': 'true' }, ' *') : null) : null, 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);
36
16
  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);
@@ -51,39 +31,21 @@ export const more = {
51
31
  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) },
52
32
  ConfirmDialog: { render: ({ C, h, props: p, children }) => 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 || t('ConfirmDialog', 'cancel')), h(C.Button, { key: 'k', variant: p.tone === 'danger' ? 'danger' : 'primary', loading: p.loading, onClick: p.onConfirm }, p.confirmLabel || t('ConfirmDialog', 'confirm'))] }, h('span', { style: { color: 'var(--zg-text-2)', lineHeight: 1.6 } }, p.description, ...children)) },
53
33
  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 || t('Popconfirm', 'no')), h(C.Button, { size: 'xs', variant: 'primary', onClick: () => { set({ open: false }); p.onConfirm && p.onConfirm(); } }, p.confirmLabel || t('Popconfirm', 'yes')))) : null) },
54
- 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 || {};
55
- 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); };
56
- const ms = p.autoplay === true ? 5000 : n > 1 && num(p.autoplay) > 0 ? num(p.autoplay) : 0;
57
- 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 || t('Carousel', 'prev'), 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 || t('Carousel', '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 || t('Carousel', 'slide')) + ' ' + (k + 1), 'aria-current': String(k === i), onClick: () => to(k) }))) : null); } },
58
- 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 || {};
59
- const to = k => { if (k === i) return; if (p.index === undefined || p.index === null) set({ i: k }); p.onChange && p.onChange(k); };
60
- 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 || t('Gallery', 'photo')) + ' ' + (k + 1), 'aria-current': String(k === i), style: { backgroundImage: `url(${u})` }, onClick: () => to(k) }))) : null); } },
61
34
  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', tabIndex: 0 }, p.code)) },
62
- 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 || t('PricingCard', 'choose'))) },
63
- IntegrationCard: { render: ({ C, h, props: p }) => { const st = ['connected', 'error'].includes(p.status) ? p.status : 'off', tone = { connected: 'success', error: 'danger' }[st], L = { connected: t('IntegrationCard', 'connected'), error: t('IntegrationCard', 'error'), off: t('IntegrationCard', 'off'), manage: t('IntegrationCard', 'manage'), reconnect: t('IntegrationCard', 'reconnect'), link: t('IntegrationCard', 'link'), ...(p.labels || {}) };
64
- 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)); } },
65
- 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 || t('IncomingCall', 'incoming')), h('b', { style: { fontSize: dzf(16) } }, p.name), h('div', { class: 'zg-mono zg-hint' }, p.phone)), h(C.IconButton, { icon: 'phone-off', label: t('IncomingCall', 'decline'), size: p.size ? p.size : undefined, onClick: p.onDecline }), h('button', { type: 'button', class: 'zg-ibtn zg-inc__ok', 'aria-label': t('IncomingCall', 'answer'), onClick: p.onAnswer }, icon(h, 'phone'))) },
66
- 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) || t('CallCard', '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)))) },
67
35
  /* ── Actions ── */
68
36
  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 }), onKeydown: openKeys(state.open && !p.disabled, () => set({ open: true })) }, 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 }), { focus: true }), onKeydown: listKeys('[role=menuitem]', { wrap: true, onTab: e => { const t = trigger(e.currentTarget); t && t.focus(); set({ open: false }); } }) }, ...(p.items || []).map((it, i, all) => it.separator ? h('div', { key: i, class: 'zg-menu__sep', role: 'separator' }) : h('button', { key: i, type: 'button', role: 'menuitem', tabIndex: tabIdx(all.map(x => ({ disabled: x.separator || x.disabled })), () => false)(i), disabled: it.disabled, 'aria-disabled': it.disabled ? 'true' : undefined, class: cx('zg-menu__i', it.tone === 'danger' && 'zg-menu__i--danger'), onClick: e => { toTrigger(e); 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) },
69
37
  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, focus: true }) : undefined, onKeydown: listKeys('[role=menuitem]', { wrap: true, onTab: () => p.onClose && p.onClose() }) }, ...(p.items || []).map((it, i, all) => it.separator ? h('div', { key: i, class: 'zg-menu__sep', role: 'separator' }) : h('button', { key: i, type: 'button', role: 'menuitem', tabIndex: tabIdx(all.map(x => ({ disabled: x.separator || x.disabled })), () => false)(i), 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))) },
70
38
  Tooltip: { render: ({ h, props: p, children }) => h('span', { class: cx('zg-tip', p.placement && p.placement !== 'top' && 'zg-tip--' + p.placement), 'data-tip': p.text, tabIndex: 0, 'aria-describedby': p.text ? tipId(p.text) : undefined, ref: p.text ? tipRef : undefined }, ...children, p.text ? h('span', { id: tipId(p.text), role: 'tooltip', class: 'zg-sr' }, p.text) : null) },
71
- 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)) },
72
39
  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) },
73
40
  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(); };
74
41
  const msg = state.err ? t('CopyButton', 'failed') : state.done ? t('CopyButton', 'copied') : '';
75
42
  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 || t('CopyButton', 'copy')),
76
43
  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)); } },
77
- 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')))) },
78
44
  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(); };
79
45
  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': t('CommandPalette', 'label'), ref: trap({ onClose: p.onClose }), onClick: e => e.stopPropagation() },
80
46
  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 || t('CommandPalette', 'placeholder'), placeholder: p.placeholder || t('CommandPalette', 'placeholder'), 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')),
81
47
  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' }, t('CommandPalette', 'empty'))])))); } },
82
- 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;
83
- return h('div', { class: 'zg-dd' }, h('button', { type: 'button', class: 'zg-ibtn', 'aria-label': t('NotificationBell', 'label'), 'aria-expanded': String(state.open), onClick: () => set({ open: !state.open }) }, icon(h, 'bell'), un ? h('span', { class: 'zg-bell__n' }, un) : null),
84
- 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(); } }, t('NotificationBell', 'markAllRead')) : 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); } },
85
48
  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))); } },
86
- 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) },
87
49
  /* ── Forms ── */
88
50
  Textarea: { render: ({ h, props: p }) => lab(h, p, [h('textarea', { ...pick(p, ['name', 'required', 'autocomplete', 'spellcheck', 'autofocus', 'minlength', 'onBlur', 'onFocus', 'onKeydown', 'onKeyDown', 'onKeyup', 'onKeyUp']), class: cx('zg-ta', p.error && 'zg-input--error', p.resize === false && 'zg-ta--fixed', p.autoResize && 'zg-ta--auto'), rows: p.rows || 4, value: p.modelValue ?? '', placeholder: p.placeholder, maxLength: p.maxlength, disabled: p.disabled, readOnly: p.readonly, onInput: e => { p.onChange && p.onChange(e.target.value); p.onInput && p.onInput(e); } }),
89
51
  p.showCount ? h('span', { class: 'zg-hint zg-ta__count' }, String((p.modelValue ?? '').length) + (p.maxlength ? ' / ' + p.maxlength : '')) : null]) },
@@ -97,11 +59,6 @@ export const more = {
97
59
  Slider: { render: ({ h, props: p }) => { const mn = p.min ?? 0, mx = p.max ?? 100, v = p.modelValue ?? mn, pct = (v - mn) / (mx - mn) * 100;
98
60
  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)); } },
99
61
  Rating: { render: ({ h, props: p }) => h('div', { class: 'zg-rate', role: 'radiogroup', 'aria-label': p.label || t('Rating', 'label') }, ...Array.from({ length: p.max || 5 }, (_, i) => h('button', { key: i, type: 'button', disabled: p.readonly, 'aria-label': t('Rating', 'stars', { n: i + 1 }), '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)))) },
100
- ChipInput: { state: { q: '' }, render: ({ h, props: p, state, set }) => { const v = p.modelValue || [];
101
- 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: '' }); };
102
- return lab(h, p, h('span', { class: 'zg-input zg-chipin' }, ...v.map(tg => h('span', { key: tg, class: 'zg-tag' }, tg, h('button', { type: 'button', 'aria-label': t('ChipInput', 'remove', { name: tg }), onClick: () => p.onChange && p.onChange(v.filter(x => x !== tg)) }, 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)); } }))); } },
103
- 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: t('InlineEdit', '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 || t('InlineEdit', 'edit')), icon(h, 'pencil', 14)) },
104
- 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) })))) },
105
62
  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),
106
63
  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(); };
107
64
  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 || t('FileUpload', 'title'), '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 || t('FileUpload', 'title')), h('span', { class: 'zg-hint' }, p.hint || t('FileUpload', 'hint')), 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); } },
@@ -111,9 +68,6 @@ export const more = {
111
68
  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]);
112
69
  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 ? t('MultiSelect', 'selected', { n: v.length }) : (p.placeholder || '—')), icon(h, 'chevron-down', 16)),
113
70
  state.open && !p.disabled ? h('div', { class: 'zg-select__list', role: 'listbox', 'aria-label': p.label || p.placeholder, '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)); } },
114
- 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);
115
- 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)),
116
- state.open && !p.disabled ? h('div', { class: 'zg-select__list', role: 'listbox', 'aria-label': p.label || p.placeholder, '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)); } },
117
71
  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 || {};
118
72
  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); } },
119
73
  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 = dloc(p), 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();
@@ -130,44 +84,20 @@ export const more = {
130
84
  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 }) }),
131
85
  h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } }, p.showRemember !== false ? h(C.Checkbox, { label: L.remember, modelValue: state.rm, onChange: v => set({ rm: v }) }) : h('span', null), 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),
132
86
  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)); } },
133
- StepForm: { state: { i: 0 }, render: ({ C, h, props: p, state, set }) => { const st = p.steps || [], i = clamp(state.i, 0, st.length - 1);
134
- 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 }) }, t('StepForm', '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 || t('StepForm', 'finish')) : t('StepForm', 'next')))); } },
135
87
  /* ── Data ── */
136
88
  Tag: { render: ({ h, props: p, children }) => h('span', { class: 'zg-tag' }, ...children, (p.onRemove || p.onClose) ? h('button', { type: 'button', 'aria-label': t('Tag', 'remove'), onClick: p.onRemove || p.onClose }, icon(h, 'x', 12)) : null) },
137
89
  Kbd: { render: ({ h, children }) => h('kbd', { class: 'zg-kbd' }, ...children) },
138
90
  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) },
139
- 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) },
140
91
  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;
141
92
  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' }))
142
93
  : 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)' } }))); } },
143
94
  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 ?? '—')])) },
144
95
  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' } }) },
145
- 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)))) },
146
- 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;
147
- 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); };
148
- return h('div', { class: 'zg-tree' }, ...(p.nodes || []).map(n => node(n, 0))); } },
149
- CalendarView: { render: ({ C, h, props: p }) => h(C.Calendar, { ...p, events: Object.fromEntries((p.events || []).map(e => [e.date, e.label])) }) },
150
96
  /* ── Navigation / Layout ── */
151
97
  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))) },
152
- 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) }))) },
153
98
  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)) },
154
99
  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)) },
155
100
  ScrollArea: { render: ({ h, props: p, children }) => h('div', { class: 'zg-scroll', style: { maxHeight: p.maxHeight || '240px' } }, ...children) },
156
101
  /* ── CRM ── */
157
- 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: t('DealCard', '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))) },
158
- PipelineBoard: { state: { over: null }, render: ({ C, h, props: p, state, set }) => { const mv = p.onMove, stages = arr(p.stages).filter(Boolean);
159
- 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 }); };
160
- 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;
161
- 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 });
162
- 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; }));
163
- 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; })); } },
164
- 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: t('ContactPanel', 'phone'), value: p.phone }, { label: t('ContactPanel', 'telegram'), value: p.telegram }, { label: t('ContactPanel', 'email'), value: p.email }, { label: t('ContactPanel', '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) || t('ContactPanel', 'call')), h(C.Button, { size: 'sm', icon: 'send', onClick: () => p.onMessage && p.onMessage() }, (p.labels && p.labels.message) || t('ContactPanel', 'message')))) },
165
- SalesFunnel: { render: ({ h, props: p }) => { const st = arr(p.steps).map(s => s || {}), mx = Math.max(...st.map(s => num(s.value)), 1);
166
- 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))))); } },
167
102
  /* ── Hotels ── */
168
- 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)))) },
169
- 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, ' · ', t('GuestCard', 'nights', { n: p.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)))) },
170
- 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)); })); } },
171
- 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 }); })])); } },
172
- 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] || '')); })) },
173
103
  };
@@ -1,36 +1,15 @@
1
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';
2
+ import { icon, cx } from './components.js';
3
3
  import { t } from '../i18n/index.js';
4
- import { rove, tabIdx, listKeys, openKeys, toTrigger, trigger } from './keys.js';
4
+ import { rove, tabIdx } from './keys.js';
5
5
  const TONES = ['brand', 'sky', 'success', 'warning', 'danger', 'neutral'];
6
6
  const tc = t => 'zg-t-' + (TONES.includes(t) ? t : 'brand');
7
- 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;
8
-
9
7
  export const more3 = {
10
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': t('Chip', 'remove'), onClick: p.onClose || p.onRemove }, icon(h, 'x', 12)) : null) },
11
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))) },
12
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))) },
13
- 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)) },
14
- 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))) },
15
- 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)) },
16
- 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' })) },
17
- 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))))) },
18
- 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;
19
- 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)))),
20
- 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)' } }))); } },
21
- 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)'];
22
- 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)))); } },
23
- 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) },
24
- 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': t('TabTiles', 'add'), onClick: p.onAdd }, icon(h, 'plus', 20)) : null) },
25
11
  PillTabs: { render: ({ h, props: p }) => h('div', { class: cx('zg-pt', p.vertical && 'zg-pt--v'), role: 'tablist', 'aria-orientation': p.vertical ? 'vertical' : undefined, onKeydown: rove('[role=tab]', { vertical: !!p.vertical }) }, ...(p.items || []).map((it, i, all) => h('button', { key: it.value, type: 'button', role: 'tab', tabIndex: tabIdx(all, x => x.value === p.value)(i), disabled: it.disabled, 'aria-selected': String(it.value === p.value), onClick: () => p.onChange && p.onChange(it.value) }, it.icon ? icon(h, it.icon, 16) : null, it.label))) },
26
- 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 }), onKeydown: openKeys(open, () => set({ open: true })) }, (opts.find(o => o.value === v) || {}).label || p.placeholder, icon(h, 'chevron-down', 14)), open ? h('div', { class: 'zg-menu', role: 'listbox', 'aria-label': p.placeholder || (opts.find(o => o.value === v) || {}).label, 'data-zg-dd': '', ref: ddRef(() => set({ open: false }), { focus: true }), onKeydown: listKeys('[role=option]', { onTab: e => { const t = trigger(e.currentTarget); t && t.focus(); set({ open: false }); } }) }, ...opts.map((o, i) => h('button', { key: o.value, type: 'button', role: 'option', tabIndex: tabIdx(opts, x => x.value === v)(i), class: 'zg-menu__i', disabled: o.disabled, 'aria-disabled': o.disabled ? 'true' : undefined, 'aria-selected': String(o.value === v), onClick: e => { toTrigger(e); set({ open: false }); p.onChange && p.onChange(o.value); } }, o.label))) : null); } },
27
- 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),
28
- p.menu ? h('div', { class: 'zg-dd' }, h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': t('ChartCard', '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) },
29
- 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 ? t('ActionCard', 'expand') : t('ActionCard', 'collapse'), size: 'sm', ghost: true, onClick: () => { set({ collapsed: !col }); p.onCollapse && p.onCollapse(!col); } }), h(C.IconButton, { icon: 'refresh-cw', label: t('ActionCard', '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: t('ActionCard', 'remove'), size: 'sm', ghost: true, onClick: () => { set({ removed: true }); p.onRemove && p.onRemove(); } }))),
30
- col ? null : h('div', null, ...children), busy ? h('div', { class: 'zg-lo__o' }, h(C.Spinner, { size: 28 })) : null); } },
31
12
  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': t('Snackbar', 'close') }, icon(h, 'x', 14)) : null) },
32
- 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': t('TourPopover', '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 }, t('TourPopover', 'back')) : null, h(C.Button, { size: 'sm', variant: 'primary', onClick: p.onNext }, p.last ? t('TourPopover', 'finish') : t('TourPopover', 'next'))))) },
33
- 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)) },
34
13
  SocialAuth: { render: ({ C, h, props: p }) => {
35
14
  const list = (p.providers || ['google', 'apple']).filter(k => k === 'google' || k === 'apple');
36
15
  const icon = k => k === 'apple'
@@ -46,27 +25,6 @@ export const more3 = {
46
25
  p.divider === false ? null : h('div', { class: 'zg-social__or', role: 'separator' }, h('span', null, t('SocialAuth', 'or'))),
47
26
  h('div', { class: 'zg-social__row' }, ...list.map(k => h(C.Button, { key: k, variant: 'secondary', size: 'lg', block: stack, iconOnly: !stack, title: t('SocialAuth', 'continueWith', { provider: name(k) }), 'aria-label': t('SocialAuth', 'continueWith', { provider: name(k) }), onClick: () => p.onSelect && p.onSelect(k) }, icon(k), stack ? t('SocialAuth', 'continueWith', { provider: name(k) }) : null))));
48
27
  } },
49
- AuthShowcase: { render: ({ C, h, props: p, children }) => {
50
- const v = p.variant === 'light' || p.variant === 'tint' ? p.variant : 'photo';
51
- const people = p.people || [];
52
- const stack = size => h('span', { class: 'zg-ash__stack' }, ...people.slice(0, 4).map((u, i) => h(C.Avatar, { key: i, name: u.name, src: u.src, size })));
53
- const head = h('div', { class: 'zg-ash__head' }, p.logo || null, h('h1', { class: 'zg-ash__title' }, p.title), p.subtitle ? h('p', { class: 'zg-ash__sub' }, p.subtitle) : null);
54
- const form = h('div', { class: 'zg-ash__form' }, head, h('div', { class: 'zg-ash__fields' }, ...children), p.footer ? h('div', { class: 'zg-ash__foot' }, p.footer) : null);
55
- const art = { backgroundImage: p.image ? 'url(' + p.image + ')' : undefined };
56
- if (v === 'photo') {
57
- const spots = ['a', 'b', 'c'];
58
- return h('div', { class: 'zg-root zg-ash zg-ash--photo' }, h('div', { class: 'zg-ash__frame' },
59
- h('section', { class: 'zg-ash__media', style: art }, h('div', { class: 'zg-ash__copy' }, p.headline ? h('h2', null, p.headline) : null, p.caption ? h('p', null, p.caption) : null),
60
- ...people.slice(0, 3).map((u, i) => h('span', { key: i, class: 'zg-ash__float zg-ash__float--' + spots[i] }, h(C.Avatar, { name: u.name, src: u.src, size: i === 1 ? 'lg' : 'md' }))),
61
- p.quote ? h('small', { class: 'zg-ash__quote' }, p.quote) : null),
62
- h('main', { class: 'zg-ash__side' }, form)));
63
- }
64
- return h('div', { class: 'zg-root zg-ash zg-ash--' + v }, h('div', { class: 'zg-ash__grid' },
65
- h('div', { class: 'zg-ash__col' }, h('main', { class: 'zg-ash__card' }, form),
66
- p.proof ? h('div', { class: 'zg-ash__proof' }, stack('sm'), h('div', { class: 'zg-ash__proofT' }, h('b', null, p.proof), p.proofHint ? h('span', null, p.proofHint) : null), h(C.IconButton, { icon: 'arrow-up-right', label: p.proof, ghost: true })) : null),
67
- h('section', { class: 'zg-ash__media', style: art }, v === 'tint' && p.headline ? h('h2', { class: 'zg-ash__mh' }, p.headline) : null,
68
- h('div', { class: 'zg-ash__glass' }, (p.chips || []).length ? h('div', { class: 'zg-ash__chips' }, ...p.chips.map((c, i) => h('span', { key: i, class: 'zg-ash__chip' }, c))) : null, p.caption ? h('p', null, p.caption) : null))));
69
- } },
70
28
  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))) },
71
29
  StatusPage: { state: { email: '' }, render: ({ C, h, props: p, children, state, set }) => { const K = { 404: ['404', t('StatusPage', 'nfTitle'), t('StatusPage', 'nfDesc'), t('StatusPage', 'home')], 401: ['401', t('StatusPage', 'authTitle'), t('StatusPage', 'authDesc'), t('StatusPage', 'home')], maintenance: ['', t('StatusPage', 'mtTitle'), t('StatusPage', 'mtDesc'), t('StatusPage', 'home')], soon: ['', t('StatusPage', 'soonTitle'), t('StatusPage', 'soonDesc'), t('StatusPage', 'notify')] }[p.kind || '404'];
72
30
  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); } }
@@ -1,16 +1,6 @@
1
1
  // @ziyarago/ui · components, part 4 — gaps closed after the Vuexy audit.
2
2
  import { t } from '../i18n/index.js';
3
- import { icon, cx, fmt } from './components.js';
4
- import { trap } from './dialog.js';
5
3
  const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
6
- export 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; };
7
- 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(' '));
8
- const sanitizeHtml = html => { if (typeof document === 'undefined') return html; const t = document.createElement('template'); t.innerHTML = html;
9
- 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; }
10
- 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); }
11
- if (tag === 'a' && c.getAttribute('target') === '_blank') c.setAttribute('rel', 'noopener noreferrer'); } };
12
- walk(t.content); return t.innerHTML; };
13
-
14
4
  export const rules = {
15
5
  required: () => v => (v == null || String(v).trim() === '') ? t('Validation', 'required') : true,
16
6
  email: () => v => !v || /^\S+@\S+\.\S+$/.test(v) || t('Validation', 'email'),
@@ -27,33 +17,5 @@ export const rules = {
27
17
  };
28
18
 
29
19
  export const more4 = {
30
- ValidatedField: { state: { v: undefined, touched: false }, render: ({ C, h, props: p, state, set }) => {
31
- const val = state.v ?? p.modelValue ?? '', rs = p.rules || [];
32
- let err = null; if (state.touched) for (const r of rs) { const x = r(val); if (x !== true) { err = x; break; } }
33
- const Comp = p.type === 'password' ? C.PasswordInput : C.TextField;
34
- return h(Comp, { ...p, modelValue: val, error: p.error || err, onChange: v => { set({ v, touched: true }); p.onChange && p.onChange(v); } }); } },
35
20
  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) },
36
- DataGroup: { state: { open: {} , grp: {} }, render: ({ C, h, props: p, state, set }) => { const cols = p.columns || [];
37
- 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': t('DataGroup', '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]; };
38
- return h('div', { class: 'zg-table-wrap', tabIndex: 0 }, 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)))),
39
- 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))]; })))); } },
40
- 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)) },
41
- OptionCards: { render: ({ C, h, props: p }) => { const cur = p.multiple ? (p.modelValue || []) : p.modelValue;
42
- 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) })))); } },
43
- 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(); };
44
- const B = [['bold', 'bold', t('RichEditor', 'bold')], ['italic', 'italic', t('RichEditor', 'italic')], ['underline', 'underline', t('RichEditor', 'underline')], ['strikeThrough', 'strikethrough', t('RichEditor', 'strike')], null, ['justifyLeft', 'align-left', t('RichEditor', 'left')], ['justifyCenter', 'align-center', t('RichEditor', 'center')], ['justifyRight', 'align-right', t('RichEditor', 'right')], null, ['insertUnorderedList', 'list', t('RichEditor', 'bullets')], ['insertOrderedList', 'list-ordered', t('RichEditor', 'numbers')], null, ['formatBlock', 'quote', t('RichEditor', 'quote'), 'blockquote'], ['formatBlock', 'code', t('RichEditor', 'codeBlock'), 'pre']];
45
- const area = h('div', { class: 'zg-rte__c', contentEditable: 'true', role: 'textbox', 'aria-multiline': 'true', 'aria-label': p.label || t('RichEditor', 'editor'), 'data-ph': p.placeholder || t('RichEditor', 'placeholder'), 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(); } });
46
- 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] + i, 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)); } },
47
- 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);
48
- const inp = (v, k) => h('input', { type: 'range', class: 'zg-range2', min: mn, max: mx, step: p.step || 1, value: v, 'aria-label': k ? t('RangeSlider', 'max') : t('RangeSlider', 'min'), onInput: e => { const x = Number(e.target.value); p.onChange && p.onChange(k ? [a, Math.max(x, a)] : [Math.min(x, b), b]); } });
49
- 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))); } },
50
- 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();
51
- const wd = Array.from({ length: 7 }, (_, i) => new Intl.DateTimeFormat(loc, { weekday: 'narrow' }).format(new Date(2023, 0, 1 + ((i + ws) % 7)))), td = d => new Date(y, m, d).getTime(), e = tt ?? (f && state.hover && state.hover > f ? state.hover : null);
52
- const pick = d => { const x = td(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)]); } };
53
- 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': t('RangeCalendar', 'prev'), 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': t('RangeCalendar', 'next'), onClick: () => set({ ym: new Date(y, m + 1, 1).getTime() }) }, icon(h, 'chevron-right', 16))),
54
- 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 = td(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); }))); } },
55
- 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() },
56
- 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)))),
57
- 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': t('StepDialog', '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 }, t('StepDialog', 'previous')), h(C.Button, { variant: 'primary', onClick: p.onNext }, p.current === (p.steps || []).length - 1 ? t('StepDialog', 'submit') : t('StepDialog', 'next')))))) },
58
- 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: t('FullscreenDialog', 'close'), onClick: p.onClose }), h('b', { style: { flex: 1, fontSize: '18px' } }, p.title), h(C.Button, { variant: 'inverse', onClick: p.onSave }, p.saveLabel || t('FullscreenDialog', 'save'))), h('div', { class: 'zg-fsd__b' }, ...children)) }
59
21
  };