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