@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.
- package/CHANGELOG.md +12 -0
- package/README.md +24 -2
- package/dist/llms-full.txt +101 -9
- package/dist/llms.txt +2 -2
- package/dist/manifest.json +412 -20
- package/dist/style.css +73 -4
- package/docs/INTEGRATION.md +153 -0
- package/docs/MIGRATION.md +453 -0
- package/package.json +88 -8
- package/src/compat/index.js +468 -0
- package/src/compat/map.js +282 -0
- package/src/compat/resolver.js +22 -0
- package/src/compat/vite.js +29 -0
- package/src/core/bools.js +4 -0
- package/src/core/components.js +40 -31
- package/src/core/components2.js +52 -47
- package/src/core/components3.js +8 -7
- package/src/core/components4.js +25 -24
- package/src/core/components6.js +30 -20
- package/src/core/components7.js +10 -9
- package/src/core/index.js +2 -0
- package/src/i18n/components/ActionCard.js +5 -0
- package/src/i18n/components/Actionsheet.js +5 -0
- package/src/i18n/components/Alert.js +5 -0
- package/src/i18n/components/Breadcrumbs.js +5 -0
- package/src/i18n/components/Calendar.js +5 -0
- package/src/i18n/components/CallCard.js +5 -0
- package/src/i18n/components/CallRow.js +5 -0
- package/src/i18n/components/Carousel.js +5 -0
- package/src/i18n/components/ChartCard.js +5 -0
- package/src/i18n/components/ChatAi.js +5 -0
- package/src/i18n/components/Chip.js +5 -0
- package/src/i18n/components/ChipInput.js +5 -0
- package/src/i18n/components/CodeInput.js +5 -0
- package/src/i18n/components/Combobox.js +5 -0
- package/src/i18n/components/CommandPalette.js +5 -0
- package/src/i18n/components/ConfirmDialog.js +5 -0
- package/src/i18n/components/ContactPanel.js +5 -0
- package/src/i18n/components/CopyButton.js +5 -0
- package/src/i18n/components/DataGroup.js +5 -0
- package/src/i18n/components/DataState.js +5 -0
- package/src/i18n/components/DataTable.js +5 -0
- package/src/i18n/components/DatePicker.js +5 -0
- package/src/i18n/components/DealCard.js +5 -0
- package/src/i18n/components/Drawer.js +5 -0
- package/src/i18n/components/ErrorPage.js +5 -0
- package/src/i18n/components/FileUpload.js +5 -0
- package/src/i18n/components/FullscreenDialog.js +5 -0
- package/src/i18n/components/Gallery.js +5 -0
- package/src/i18n/components/GuestCard.js +5 -0
- package/src/i18n/components/ImageUpload.js +5 -0
- package/src/i18n/components/ImageViewer.js +5 -0
- package/src/i18n/components/IncomingCall.js +5 -0
- package/src/i18n/components/InlineEdit.js +5 -0
- package/src/i18n/components/IntegrationCard.js +5 -0
- package/src/i18n/components/LoginForm.js +5 -0
- package/src/i18n/components/Map.js +5 -0
- package/src/i18n/components/Modal.js +5 -0
- package/src/i18n/components/MultiSelect.js +5 -0
- package/src/i18n/components/NotificationBell.js +5 -0
- package/src/i18n/components/NumberInput.js +5 -0
- package/src/i18n/components/Pagination.js +5 -0
- package/src/i18n/components/PasswordInput.js +5 -0
- package/src/i18n/components/Popconfirm.js +5 -0
- package/src/i18n/components/PricingCard.js +5 -0
- package/src/i18n/components/RamadanTracker.js +5 -0
- package/src/i18n/components/RangeCalendar.js +5 -0
- package/src/i18n/components/RangeSlider.js +5 -0
- package/src/i18n/components/Rating.js +5 -0
- package/src/i18n/components/RichEditor.js +5 -0
- package/src/i18n/components/Screensaver.js +5 -0
- package/src/i18n/components/SearchInput.js +5 -0
- package/src/i18n/components/SideNav.js +5 -0
- package/src/i18n/components/Snackbar.js +5 -0
- package/src/i18n/components/Sortable.js +5 -0
- package/src/i18n/components/Spinner.js +5 -0
- package/src/i18n/components/StatusPage.js +5 -0
- package/src/i18n/components/StepDialog.js +5 -0
- package/src/i18n/components/StepForm.js +5 -0
- package/src/i18n/components/TabTiles.js +5 -0
- package/src/i18n/components/Tag.js +5 -0
- package/src/i18n/components/TopNav.js +5 -0
- package/src/i18n/components/Tour.js +5 -0
- package/src/i18n/components/TourCard.js +5 -0
- package/src/i18n/components/TourPopover.js +5 -0
- package/src/i18n/components/TreeSelect.js +5 -0
- package/src/i18n/components/Validation.js +5 -0
- package/src/i18n/components/index.js +135 -0
- package/src/i18n/index.js +50 -0
- package/src/meta/actions.js +10 -3
- package/src/meta/data.js +8 -3
- package/src/meta/feedback.js +2 -1
- package/src/meta/forms.js +44 -4
- package/src/meta/media.js +7 -2
- package/src/meta/navigation.js +3 -0
- package/src/meta/overlay.js +8 -2
- package/src/nuxt/module.mjs +2 -2
- package/src/nuxt/plugin.mjs +6 -5
- package/src/react/index.js +202 -8
- package/src/resolver.js +33 -0
- package/src/styles/components.css +7 -0
- package/src/styles/components6.css +2 -0
- package/src/styles/effects.css +6 -4
- package/src/styles/extras.css +55 -0
- package/src/styles/index.css +1 -0
- package/src/theme.js +16 -4
- package/src/vue/global.js +2 -0
- package/src/vue/index.js +13 -5
- package/types/compat-resolver.d.ts +12 -0
- package/types/compat-vite.d.ts +11 -0
- package/types/compat.d.ts +176 -0
- package/types/i18n.d.ts +14 -0
- package/types/meta.d.ts +4 -0
- package/types/nuxt.d.ts +1 -1
- package/types/react.d.ts +6282 -186
- package/types/resolver.d.ts +14 -0
- package/types/shared.d.ts +7 -7
- package/types/theme.d.ts +20 -1
- package/types/vue-global.d.ts +193 -0
- 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']);
|
package/src/core/components.js
CHANGED
|
@@ -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 },
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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 || '
|
|
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,
|
|
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': '
|
|
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': '
|
|
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': '
|
|
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': '
|
|
88
|
-
h('button', { key: 'next', type: 'button', class: 'zg-pager__nav', 'aria-label': '
|
|
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) || '
|
|
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) || '
|
|
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': '
|
|
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': '
|
|
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: '
|
|
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 ?
|
|
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) || '
|
|
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 };
|