@ziyarago/ui 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +1 -0
- package/dist/llms-full.txt +100 -8
- package/dist/llms.txt +1 -1
- 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 +87 -7
- 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 +31 -23
- package/src/core/components2.js +20 -16
- package/src/core/components4.js +3 -3
- package/src/core/components6.js +25 -16
- package/src/core/index.js +2 -0
- package/src/i18n/components/CopyButton.js +3 -0
- package/src/i18n/components/index.js +5 -0
- package/src/i18n/index.js +48 -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 +12 -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
package/src/core/components2.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @ziyarago/ui · components, part 2 — Forms, Surfaces, Actions, Data, Navigation, Layout, CRM, Hotels.
|
|
2
|
-
import { icon, cx, fmt, dzp, dzf, dzb, ddRef } from './components.js';
|
|
2
|
+
import { icon, cx, fmt, dzp, dzf, dzb, ddRef, pick } from './components.js';
|
|
3
3
|
import { trap } from './dialog.js';
|
|
4
|
+
import { t } from '../i18n/index.js';
|
|
4
5
|
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
|
|
5
6
|
const num = v => Number.isFinite(+v) && v !== null && v !== '' ? +v : 0;
|
|
6
7
|
const arr = v => Array.isArray(v) ? v : [];
|
|
@@ -24,15 +25,15 @@ let dragging = null;
|
|
|
24
25
|
const ROOM_TONES = { occupied: 'brand', clean: 'success', dirty: 'warning', blocked: 'danger' };
|
|
25
26
|
const TONE_OK = ['brand', 'sky', 'success', 'warning', 'danger', 'neutral'];
|
|
26
27
|
const CALL_TONES = { answered: 'success', completed: 'success', missed: 'danger', declined: 'danger', rejected: 'danger', failed: 'danger', voicemail: 'warning', busy: 'warning', 'no answer': 'warning', ringing: 'info', 'in progress': 'info' };
|
|
27
|
-
const pd = v => { if (v == null || v === '') return null; if (v instanceof Date) return isNaN(v) ? null : v; if (typeof v === 'string') { const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(v.trim()); if (m) return new Date(+m[1], +m[2] - 1, +m[3]); } const d = new Date(v); return isNaN(d) ? null : d; };
|
|
28
|
-
const lab =(h, p, body) => h('label', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label) : null, body, p.error ? h('span', { class: 'zg-hint zg-hint--error', role: 'alert' }, p.error) : p.hint ? h('span', { class: 'zg-hint' }, p.hint) : null);
|
|
28
|
+
export const pd = v => { if (v == null || v === '') return null; if (v instanceof Date) return isNaN(v) ? null : v; if (typeof v === 'string') { const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(v.trim()); if (m) return new Date(+m[1], +m[2] - 1, +m[3]); } const d = new Date(v); return isNaN(d) ? null : d; };
|
|
29
|
+
const lab =(h, p, body) => h('label', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label, p.required ? h('span', { class: 'zg-req', 'aria-hidden': 'true' }, ' *') : null) : null, body, p.error ? h('span', { class: 'zg-hint zg-hint--error', role: 'alert' }, p.error) : p.hint ? h('span', { class: 'zg-hint' }, p.hint) : null);
|
|
29
30
|
const field = (h, p, pre, input, suf) => h('span', { class: cx('zg-input', p.error && 'zg-input--error', p.disabled && 'zg-input--disabled') }, pre || null, input, suf || null);
|
|
30
31
|
const TONE = { success: 'check', danger: 'circle-alert', warning: 'triangle-alert' };
|
|
31
32
|
const sel = (h, o, on) => ({ 'aria-selected': String(on), role: 'option' });
|
|
32
33
|
|
|
33
34
|
export const more = {
|
|
34
35
|
/* ── Surfaces ── */
|
|
35
|
-
Alert: { render: ({ h, props: p, children }) => h('div', { class: cx('zg-alert', 'zg-alert--' + (p.tone || 'info'), p.banner && 'zg-banner'), role: 'alert' }, h('span', { class: 'zg-alert__i' }, icon(h, TONE[p.tone] || 'info', 18)), h('div', { class: 'zg-alert__b' }, p.title ? h('b', null, p.title) : null, h('span', null, ...children)), p.onClose ? h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': 'Close', onClick: p.onClose }, icon(h, 'x')) : null) },
|
|
36
|
+
Alert: { render: ({ h, props: p, children, slots }) => h('div', { class: cx('zg-alert', 'zg-alert--' + (p.tone || 'info'), p.banner && 'zg-banner'), role: 'alert' }, p.hideIcon ? null : h('span', { class: 'zg-alert__i' }, slots.icon || icon(h, TONE[p.tone] || 'info', 18)), h('div', { class: 'zg-alert__b' }, (slots.title || p.title) ? h('b', null, slots.title || p.title) : null, h('span', null, ...children), slots.actions ? h('div', { class: 'zg-alert__act' }, slots.actions) : null), p.onClose ? h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': 'Close', onClick: p.onClose }, icon(h, 'x')) : null) },
|
|
36
37
|
Banner: { render: ({ C, h, props: p, children }) => h(C.Alert, { ...p, banner: true }, ...children) },
|
|
37
38
|
Accordion: { state: { open: null }, render: ({ h, props: p, state, set }) => h('div', { class: 'zg-acc' }, ...(p.items || []).map(it => { const o = state.open === it.id; return h('div', { key: it.id, class: 'zg-acc__i', 'data-open': String(o) }, h('button', { type: 'button', 'aria-expanded': String(o), class: 'zg-acc__h', onClick: () => set({ open: o ? null : it.id }) }, it.title, icon(h, 'chevron-down', 16)), o ? h('div', { class: 'zg-acc__b' }, it.body) : null); })) },
|
|
38
39
|
Skeleton: { render: ({ h, props: p }) => h('div', { class: 'zg-skelw' }, ...Array.from({ length: p.lines || 1 }, (_, i) => h('span', { key: i, class: 'zg-skel zg-skel--' + (p.variant || 'text'), style: { width: p.variant === 'circle' ? ({ xs: 24, sm: 32, md: 40, lg: 56, xl: 72 }[p.size] || p.size || 40) + 'px' : (i === (p.lines || 1) - 1 && (p.lines || 1) > 1 ? '60%' : p.width || '100%'), height: p.height || (p.variant === 'circle' ? ({ xs: 24, sm: 32, md: 40, lg: 56, xl: 72 }[p.size] || p.size || 40) + 'px' : undefined) } }))) },
|
|
@@ -42,7 +43,7 @@ export const more = {
|
|
|
42
43
|
ErrorPage: { render: ({ C, h, props: p, children }) => h('div', { class: 'zg-es zg-es--page' }, h('span', { class: 'zg-num', style: { fontSize: 'clamp(80px,14vw,160px)', lineHeight: 1, color: 'var(--zg-brand-text)' } }, p.code || '404'), h('b', { style: { fontSize: '24px' } }, p.title || 'Page not found'), h('span', null, p.description || 'The page may have moved. Check the address or go back.'), h('div', { class: 'zg-es__a' }, ...children)) },
|
|
43
44
|
DataState: { render: ({ C, h, props: p, children }) => ({ ready: () => h('div', null, ...children), loading: () => h('div', { class: 'zg-es' }, h(C.Spinner, { size: 28 }), h('b', null, p.title || 'Loading…')), empty: () => h(C.EmptyState, { title: p.title || 'Nothing here yet', description: p.description, icon: 'inbox' }), error: () => h(C.EmptyState, { title: p.title || 'Something went wrong', description: p.description, icon: 'circle-alert' }, h(C.Button, { onClick: p.onRetry }, p.retryLabel || 'Retry')), offline: () => h(C.EmptyState, { title: p.title || 'No internet', description: p.description || 'Saved guides still work.', icon: 'wifi-off' }, h(C.Button, { onClick: p.onRetry }, p.retryLabel || 'Try again')) }[p.state || 'ready'])() },
|
|
44
45
|
LoadingOverlay: { render: ({ C, h, props: p, children }) => h('div', { class: 'zg-lo' }, ...children, p.loading ? h('div', { class: 'zg-lo__o' }, h(C.Spinner, { size: 28 }), p.label ? h('span', null, p.label) : null) : null) },
|
|
45
|
-
ConfirmDialog: { render: ({ C, h, props: p }) => h(C.Modal, { open: p.open, size: p.size, title: p.title, onClose: p.onCancel, footer: [h(C.Button, { key: 'c', variant: 'ghost', onClick: p.onCancel }, p.cancelLabel || 'Cancel'), h(C.Button, { key: 'k', variant: p.tone === 'danger' ? 'danger' : 'primary', loading: p.loading, onClick: p.onConfirm }, p.confirmLabel || 'Confirm')] }, h('span', { style: { color: 'var(--zg-text-2)', lineHeight: 1.6 } }, p.description)) },
|
|
46
|
+
ConfirmDialog: { render: ({ C, h, props: p, children }) => h(C.Modal, { open: p.open, size: p.size, title: p.title, onClose: p.onCancel, footer: [h(C.Button, { key: 'c', variant: 'ghost', onClick: p.onCancel }, p.cancelLabel || 'Cancel'), h(C.Button, { key: 'k', variant: p.tone === 'danger' ? 'danger' : 'primary', loading: p.loading, onClick: p.onConfirm }, p.confirmLabel || 'Confirm')] }, h('span', { style: { color: 'var(--zg-text-2)', lineHeight: 1.6 } }, p.description, ...children)) },
|
|
46
47
|
Popconfirm: { state: { open: false }, render: ({ C, h, props: p, state, set }) => h('span', { class: 'zg-dd' }, h(C.Button, { size: p.size || 'sm', variant: p.variant || 'danger', onClick: () => set({ open: !state.open }) }, p.label), state.open ? h('div', { class: 'zg-pop zg-glass', 'data-zg-dd': '', ref: ddRef(() => set({ open: false })) }, h('b', { style: { fontSize: '14px' } }, p.title), h('div', { style: { display: 'flex', gap: '8px', justifyContent: 'flex-end' } }, h(C.Button, { size: 'xs', variant: 'ghost', onClick: () => set({ open: false }) }, p.cancelLabel || 'No'), h(C.Button, { size: 'xs', variant: 'primary', onClick: () => { set({ open: false }); p.onConfirm && p.onConfirm(); } }, p.confirmLabel || 'Yes'))) : null) },
|
|
47
48
|
Carousel: { state: { i: 0 }, render: ({ h, props: p, state, set }) => { const items = arr(p.items).map(x => x || {}), n = items.length, i = idx(p, state, n), it = items[i] || {}, L = p.labels || {};
|
|
48
49
|
const to = k => { if (!n) return; k = ((k % n) + n) % n; if (k === i) return; if (p.index === undefined || p.index === null) set({ i: k }); p.onChange && p.onChange(k); };
|
|
@@ -60,12 +61,12 @@ export const more = {
|
|
|
60
61
|
/* ── Actions ── */
|
|
61
62
|
Dropdown: { state: { open: false }, render: ({ h, props: p, state, set }) => h('div', { class: 'zg-dd' }, h('button', { type: 'button', class: cx('zg-btn', p.size && 'zg-btn--' + p.size), 'aria-haspopup': 'menu', 'aria-expanded': String(state.open && !p.disabled), disabled: p.disabled, onClick: () => set({ open: !state.open }) }, p.icon ? icon(h, p.icon, 16) : null, p.label, icon(h, 'chevron-down', 14)), state.open && !p.disabled ? h('div', { class: 'zg-menu', role: 'menu', 'data-zg-dd': '', ref: ddRef(() => set({ open: false })) }, ...(p.items || []).map((it, i) => it.separator ? h('div', { key: i, class: 'zg-menu__sep' }) : h('button', { key: i, type: 'button', role: 'menuitem', disabled: it.disabled, 'aria-disabled': it.disabled ? 'true' : undefined, class: cx('zg-menu__i', it.tone === 'danger' && 'zg-menu__i--danger'), onClick: () => { set({ open: false }); it.onClick && it.onClick(it); p.onSelect && p.onSelect(it.value); } }, it.icon ? icon(h, it.icon, 16) : null, it.label, it.shortcut ? h('span', { class: 'zg-kbd' }, it.shortcut) : null))) : null) },
|
|
62
63
|
Menu: { render: ({ h, props: p }) => p.open === false ? null : h('div', { class: 'zg-menu zg-menu--static', role: 'menu', 'data-zg-dd': '', ref: p.open === true && p.onClose ? ddRef(p.onClose, { self: true }) : undefined }, ...(p.items || []).map((it, i) => it.separator ? h('div', { key: i, class: 'zg-menu__sep' }) : h('button', { key: i, type: 'button', role: 'menuitem', disabled: it.disabled, 'aria-disabled': it.disabled ? 'true' : undefined, class: cx('zg-menu__i', it.tone === 'danger' && 'zg-menu__i--danger'), onClick: () => { it.onClick && it.onClick(it); p.onSelect && p.onSelect(it.value); } }, it.icon ? icon(h, it.icon, 16) : null, it.label, it.shortcut ? h('span', { class: 'zg-kbd' }, it.shortcut) : null))) },
|
|
63
|
-
Tooltip: { render: ({ h, props: p, children }) => h('span', { class: 'zg-tip', 'data-tip': p.text, tabIndex: 0 }, ...children) },
|
|
64
|
+
Tooltip: { render: ({ h, props: p, children }) => h('span', { class: cx('zg-tip', p.placement && p.placement !== 'top' && 'zg-tip--' + p.placement), 'data-tip': p.text, tabIndex: 0 }, ...children) },
|
|
64
65
|
HoverCard: { render: ({ h, props: p, children }) => h('span', { class: 'zg-hc', tabIndex: 0 }, h('span', null, p.trigger), h('div', { class: 'zg-hc__c zg-glass' }, ...children)) },
|
|
65
66
|
Popover: { state: { open: false }, render: ({ C, h, props: p, state, set, children }) => h('span', { class: 'zg-dd' }, h(C.Button, { size: p.size, onClick: () => set({ open: !state.open }) }, p.label), state.open ? h('div', { class: 'zg-pop zg-glass', 'data-zg-dd': '', ref: ddRef(() => set({ open: false })) }, ...children) : null) },
|
|
66
67
|
CopyButton: { state: { done: false, err: false }, render: ({ C, h, props: p, state, set }) => { const reset = () => setTimeout(() => set({ done: false, err: false }), 1500), fail = e => { set({ done: false, err: true }); p.onError && p.onError(e); reset(); }, ok = () => { set({ done: true, err: false }); p.onCopy && p.onCopy(p.value); reset(); };
|
|
67
|
-
const msg = state.err ? '
|
|
68
|
-
return h('span', { style: { display: 'inline-flex' } }, h(C.Button, { size: p.size || 'sm', variant: state.err ? 'danger' : p.variant, icon: state.err ? 'triangle-alert' : state.done ? 'check' : 'copy', onClick: () => { try { const cb = typeof navigator !== 'undefined' && navigator.clipboard; if (!cb || !cb.writeText) return fail(new Error('Clipboard unavailable')); Promise.resolve(cb.writeText(p.value)).then(ok, fail); } catch (e) { fail(e); } } }, p.label || msg || '
|
|
68
|
+
const msg = state.err ? t('CopyButton', 'failed') : state.done ? t('CopyButton', 'copied') : '';
|
|
69
|
+
return h('span', { style: { display: 'inline-flex' } }, h(C.Button, { size: p.size || 'sm', variant: state.err ? 'danger' : p.variant, icon: state.err ? 'triangle-alert' : state.done ? 'check' : 'copy', onClick: () => { try { const cb = typeof navigator !== 'undefined' && navigator.clipboard; if (!cb || !cb.writeText) return fail(new Error('Clipboard unavailable')); Promise.resolve(cb.writeText(p.value)).then(ok, fail); } catch (e) { fail(e); } } }, p.label || msg || t('CopyButton', 'copy')),
|
|
69
70
|
h('span', { role: 'status', 'aria-live': 'polite', style: { position: 'absolute', width: '1px', height: '1px', overflow: 'hidden', clip: 'rect(0 0 0 0)', whiteSpace: 'nowrap' } }, msg)); } },
|
|
70
71
|
CommandBar: { render: ({ h, props: p }) => h('div', { class: cx('zg-cb', 'zg-glass') }, ...(p.items || []).map((it, i) => it.separator ? h('span', { key: i, class: 'zg-cb__s' }) : h('button', { key: i, type: 'button', class: 'zg-ibtn zg-ibtn--ghost zg-ibtn--sm', title: it.label, 'aria-label': it.label, disabled: it.disabled, onClick: () => p.onAction && p.onAction(it.id) }, icon(h, it.icon || 'circle')))) },
|
|
71
72
|
CommandPalette: { state: { q: '', a: 0 }, render: ({ h, props: p, state, set }) => { if (!p.open) return null; const f = (p.commands || []).filter(c => !state.q || (c.label + ' ' + (c.keywords || '')).toLowerCase().includes(state.q.toLowerCase())); const ai = f.length ? clamp(state.a, 0, f.length - 1) : -1; const run = c => { if (!c) return; p.onRun && p.onRun(c.id); p.onClose && p.onClose(); };
|
|
@@ -78,12 +79,15 @@ export const more = {
|
|
|
78
79
|
ToggleGroup: { render: ({ h, props: p }) => { const cur = p.multiple ? (p.modelValue || []) : [p.modelValue]; return h('div', { class: 'zg-seg', role: 'group' }, ...(p.options || []).map(o => h('button', { key: o.value, type: 'button', 'aria-pressed': String(cur.includes(o.value)), title: o.label, onClick: () => p.onChange && p.onChange(p.multiple ? (cur.includes(o.value) ? cur.filter(v => v !== o.value) : [...cur, o.value]) : (p.allowEmpty && cur.includes(o.value) ? null : o.value)) }, o.icon ? icon(h, o.icon, 16) : null, p.iconOnly ? null : o.label))); } },
|
|
79
80
|
Fab: { render: ({ h, props: p }) => h('button', { type: 'button', class: 'zg-fab', 'aria-label': p.label || p.icon || 'plus', onClick: p.onClick }, icon(h, p.icon || 'plus', 20), p.label ? h('span', null, p.label) : null) },
|
|
80
81
|
/* ── Forms ── */
|
|
81
|
-
Textarea: { render: ({ h, props: p }) => lab(h, p, h('textarea', { class: cx('zg-ta', p.error && 'zg-input--error'), rows: p.rows || 4, value: p.modelValue ?? '', placeholder: p.placeholder, maxLength: p.maxlength, disabled: p.disabled, onInput: e => p.onChange && p.onChange(e.target.value)
|
|
82
|
+
Textarea: { render: ({ h, props: p }) => lab(h, p, [h('textarea', { ...pick(p, ['name', 'required', 'autocomplete', 'spellcheck', 'autofocus', 'minlength', 'onBlur', 'onFocus', 'onKeydown', 'onKeyDown', 'onKeyup', 'onKeyUp']), class: cx('zg-ta', p.error && 'zg-input--error', p.resize === false && 'zg-ta--fixed', p.autoResize && 'zg-ta--auto'), rows: p.rows || 4, value: p.modelValue ?? '', placeholder: p.placeholder, maxLength: p.maxlength, disabled: p.disabled, readOnly: p.readonly, onInput: e => { p.onChange && p.onChange(e.target.value); p.onInput && p.onInput(e); } }),
|
|
83
|
+
p.showCount ? h('span', { class: 'zg-hint zg-ta__count' }, String((p.modelValue ?? '').length) + (p.maxlength ? ' / ' + p.maxlength : '')) : null]) },
|
|
82
84
|
NumberInput: { state: { draft: null }, render: ({ C, h, props: p, state, set: put }) => { const v = p.modelValue ?? null, st = p.step || 1, lo = p.min ?? -Infinity, hi = p.max ?? Infinity, parse = s => { const t = String(s).trim().replace(',', '.'); if (t === '') return null; const n = Number(t); return Number.isFinite(n) ? n : NaN; }, emit = n => p.onChange && p.onChange(n), bump = d => { put({ draft: null }); emit(clamp(Math.round(((v ?? 0) + d) * 1e10) / 1e10, lo, hi)); };
|
|
83
85
|
return lab(h, p, h('span', { class: 'zg-input zg-num-in' }, h(C.IconButton, { icon: 'minus', label: 'Decrease', size: 'sm', onClick: () => bump(-st) }), h('input', { value: state.draft ?? (v ?? ''), inputMode: 'decimal', placeholder: p.placeholder, style: { textAlign: 'center' }, onInput: e => { const raw = e.target.value, n = parse(raw); put({ draft: raw }); if (n === null || !Number.isNaN(n)) emit(n); }, onBlur: e => { const n = parse(e.target.value); put({ draft: null }); if (n === null) { if (v !== null) emit(null); } else if (!Number.isNaN(n)) { const c = clamp(n, lo, hi); if (c !== v) emit(c); } } }), h(C.IconButton, { icon: 'plus', label: 'Increase', size: 'sm', onClick: () => bump(st) }))); } },
|
|
84
86
|
PasswordInput: { state: { show: false }, render: ({ C, h, props: p, state, set }) => lab(h, p, field(h, p, icon(h, 'lock-keyhole', 18), h('input', { type: state.show ? 'text' : 'password', value: p.modelValue ?? '', placeholder: p.placeholder, autoComplete: p.autocomplete || 'current-password', onInput: e => p.onChange && p.onChange(e.target.value) }), h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': state.show ? 'Hide password' : 'Show password', onClick: () => set({ show: !state.show }) }, icon(h, state.show ? 'eye-off' : 'eye', 16)))) },
|
|
85
87
|
SearchInput: { render: ({ h, props: p }) => h('span', { class: 'zg-input', style: { '--h': 'var(--zg-h-md)' } }, icon(h, 'search', 16), h('input', { type: 'search', value: p.modelValue ?? '', placeholder: p.placeholder || 'Search', onInput: e => p.onChange && p.onChange(e.target.value), onKeyDown: e => e.key === 'Enter' && p.onSearch && p.onSearch(p.modelValue) }), p.modelValue ? h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': 'Clear', onClick: () => p.onChange && p.onChange('') }, icon(h, 'x', 14)) : null) },
|
|
86
|
-
RadioGroup: { render: ({ h, props: p }) => h('div', { class: 'zg-field', role: 'radiogroup', 'aria-disabled': p.disabled ? 'true' : undefined, style: { gap: '10px' } }, p.label ? h('span', { class: 'zg-label' }, p.label) : null,
|
|
88
|
+
RadioGroup: { render: ({ h, props: p }) => h('div', { class: 'zg-field', role: 'radiogroup', 'aria-disabled': p.disabled ? 'true' : undefined, style: { gap: '10px' } }, p.label ? h('span', { class: 'zg-label' }, p.label) : null,
|
|
89
|
+
h('div', { class: cx('zg-rg', p.orientation === 'horizontal' && 'zg-rg--row') }, ...(p.options || []).map(o => h('button', { key: o.value, type: 'button', role: 'radio', class: 'zg-check', 'aria-checked': String(o.value === p.modelValue), disabled: p.disabled || o.disabled, 'aria-disabled': p.disabled || o.disabled ? 'true' : undefined, onClick: () => !(p.disabled || o.disabled) && p.onChange && p.onChange(o.value) }, h('span', { class: 'zg-check__box zg-radio' }, o.value === p.modelValue ? h('i') : null), h('span', null, o.label, o.hint ? h('span', { class: 'zg-hint', style: { display: 'block' } }, o.hint) : null)))),
|
|
90
|
+
p.error ? h('span', { class: 'zg-hint zg-hint--error', role: 'alert' }, p.error) : p.hint ? h('span', { class: 'zg-hint' }, p.hint) : null) },
|
|
87
91
|
Slider: { render: ({ h, props: p }) => { const mn = p.min ?? 0, mx = p.max ?? 100, v = p.modelValue ?? mn, pct = (v - mn) / (mx - mn) * 100;
|
|
88
92
|
return lab(h, p, h('span', { style: { display: 'flex', alignItems: 'center', gap: '12px' } }, h('input', { type: 'range', class: 'zg-range', min: mn, max: mx, step: p.step || 1, value: v, style: { '--p': pct + '%' }, onInput: e => p.onChange && p.onChange(Number(e.target.value)) }), p.showValue !== false ? h('span', { class: 'zg-mono', style: { minWidth: '48px', textAlign: 'end' } }, p.formatValue ? p.formatValue(v) : v) : null)); } },
|
|
89
93
|
Rating: { render: ({ h, props: p }) => h('div', { class: 'zg-rate', role: 'radiogroup', 'aria-label': p.label || 'Rating' }, ...Array.from({ length: p.max || 5 }, (_, i) => h('button', { key: i, type: 'button', disabled: p.readonly, 'aria-label': (i + 1) + ' stars', 'data-on': String(i < (p.modelValue || 0)), onClick: () => p.onChange && p.onChange(i + 1) }, icon(h, 'star', ({ xs: 14, sm: 18, md: 20, lg: 26, xl: 32 })[p.size] || p.size || 20)))) },
|
|
@@ -95,8 +99,8 @@ export const more = {
|
|
|
95
99
|
FileUpload: { state: { drag: false }, render: ({ h, props: p, state, set }) => { const ac = String(p.accept || '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean), okType = f => !ac.length || ac.some(a => a[0] === '.' ? f.name.toLowerCase().endsWith(a) : a.endsWith('/*') ? (f.type || '').toLowerCase().startsWith(a.slice(0, -1)) : (f.type || '').toLowerCase() === a), rej = (f, r) => p.onReject && p.onReject(f, r),
|
|
96
100
|
take = list => { const ok = []; for (const f of list) { if (!okType(f)) rej(f, 'type'); else if (p.maxSize && f.size > p.maxSize) rej(f, 'size'); else ok.push(f); } if (!p.multiple) ok.slice(1).forEach(f => rej(f, 'count')); const out = p.multiple ? ok : ok.slice(0, 1); if (out.length && p.onChange) p.onChange(out); }, pick = e => { const i = e.currentTarget.querySelector('input'); if (i) i.click(); };
|
|
97
101
|
return h('div', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label) : null, h('div', { class: 'zg-fdrop', role: 'button', tabIndex: 0, 'aria-label': p.title || 'Drop files here or browse', 'data-drag': state.drag ? '' : undefined, onClick: pick, onKeydown: e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); pick(e); } }, onDragenter: e => { e.preventDefault(); if (!state.drag) set({ drag: true }); }, onDragover: e => { e.preventDefault(); if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'; if (!state.drag) set({ drag: true }); }, onDragleave: e => { if (!e.currentTarget.contains(e.relatedTarget)) set({ drag: false }); }, onDrop: e => { e.preventDefault(); set({ drag: false }); take([...(e.dataTransfer ? e.dataTransfer.files : [])]); } }, icon(h, p.image ? 'image-up' : 'upload', 24), h('b', null, p.title || 'Drop files here or browse'), h('span', { class: 'zg-hint' }, p.hint || 'PDF, JPG up to 10 MB'), h('input', { type: 'file', multiple: p.multiple, accept: p.accept, tabIndex: -1, 'aria-hidden': 'true', style: { display: 'none' }, onClick: e => e.stopPropagation(), onChange: e => { take([...e.target.files]); e.target.value = ''; } })), p.error ? h('span', { class: 'zg-hint zg-hint--error', role: 'alert' }, p.error) : null); } },
|
|
98
|
-
Combobox: { state: { open: false, q: '' }, render: ({ h, props: p, state, set }) => { const cur = (p.options || []).find(o => o.value === p.modelValue), f = (p.options || []).filter(o => !state.q || o.label.toLowerCase().includes(state.q.toLowerCase()));
|
|
99
|
-
return lab(h, p, h('div', { class: 'zg-select' }, field(h, p, icon(h, 'search', 16), h('input', { value: state.open && !p.disabled ? state.q : (cur ? cur.label : ''), disabled: p.disabled, placeholder: p.placeholder || 'Search…', onFocus: () => set({ open: true, q: '' }), onInput: e => set({ q: e.target.value, open: true }) }), icon(h, 'chevron-down', 16)),
|
|
102
|
+
Combobox: { state: { open: false, q: '' }, render: ({ h, props: p, state, set }) => { const cur = (p.options || []).find(o => o.value === p.modelValue), f = (p.options || []).filter(o => !state.q || (p.filter ? p.filter(o, state.q) : o.label.toLowerCase().includes(state.q.toLowerCase())));
|
|
103
|
+
return lab(h, p, h('div', { class: 'zg-select' }, field(h, p, icon(h, 'search', 16), h('input', { value: state.open && !p.disabled ? state.q : (cur ? cur.label : ''), disabled: p.disabled, placeholder: p.placeholder || 'Search…', onFocus: () => set({ open: true, q: '' }), onInput: e => set({ q: e.target.value, open: true }) }), [p.clearable && cur && !p.disabled ? h('button', { key: 'x', type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': 'Clear', onMouseDown: e => e.preventDefault(), onClick: () => { set({ open: false, q: '' }); p.onChange && p.onChange(null); } }, icon(h, 'x', 14)) : null, icon(h, 'chevron-down', 16)]),
|
|
100
104
|
state.open && !p.disabled ? h('div', { class: 'zg-select__list', role: 'listbox', 'data-zg-dd': '', ref: ddRef(() => set({ open: false, q: '' })) }, ...(f.length ? f.map(o => h('button', { key: o.value, type: 'button', role: 'option', class: 'zg-select__opt', disabled: o.disabled, 'aria-disabled': o.disabled ? 'true' : undefined, 'aria-selected': String(o.value === p.modelValue), onMouseDown: e => e.preventDefault(), onClick: () => { set({ open: false, q: '' }); p.onChange && p.onChange(o.value); } }, o.label)) : [h('div', { key: 'e', class: 'zg-empty' }, p.emptyText || '—')])) : null)); } },
|
|
101
105
|
MultiSelect: { state: { open: false }, render: ({ h, props: p, state, set }) => { const v = p.modelValue || [], tog = x => p.onChange && p.onChange(v.includes(x) ? v.filter(i => i !== x) : [...v, x]);
|
|
102
106
|
return h('div', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label) : null, h('div', { class: 'zg-select' }, h('button', { type: 'button', class: 'zg-btn zg-select__trigger', 'aria-haspopup': 'listbox', 'aria-expanded': String(state.open && !p.disabled), disabled: p.disabled, style: { height: 'var(--zg-h-lg)' }, onClick: () => set({ open: !state.open }) }, h('span', null, v.length ? v.length + ' selected' : (p.placeholder || '—')), icon(h, 'chevron-down', 16)),
|
|
@@ -108,11 +112,11 @@ export const more = {
|
|
|
108
112
|
return h('div', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label) : null, h('div', { style: { display: 'grid', gridTemplateColumns: '130px minmax(0,1fr)', gap: '8px', alignItems: 'start' } }, h(C.Select, { options: CC, modelValue: v.code || CC[0].value, onChange: c => p.onChange && p.onChange({ ...v, code: c }) }), field(h, p, icon(h, 'phone', 16), h('input', { type: 'tel', inputMode: 'tel', value: v.number || '', placeholder: p.placeholder ?? '90 123 45 67', onInput: e => p.onChange && p.onChange({ ...v, number: e.target.value }) }))), typeof p.error === 'string' && p.error ? h('span', { class: 'zg-hint zg-hint--error', role: 'alert' }, p.error) : p.hint ? h('span', { class: 'zg-hint' }, p.hint) : null); } },
|
|
109
113
|
Calendar: { state: { ym: null }, render: ({ h, props: p, state, set }) => { const s = pd(p.modelValue), b = state.ym ? new Date(state.ym) : (s || new Date()), y = b.getFullYear(), m = b.getMonth(), loc = p.locale || 'en-GB', ws = p.weekStartsOn ?? 1, off = (new Date(y, m, 1).getDay() - ws + 7) % 7, dn = new Date(y, m + 1, 0).getDate(), today = new Date();
|
|
110
114
|
const wd = Array.from({ length: 7 }, (_, i) => new Intl.DateTimeFormat(loc, { weekday: 'narrow' }).format(new Date(2023, 0, 1 + ((i + ws) % 7))));
|
|
111
|
-
const same = (a, d) => a && a.getFullYear() === y && a.getMonth() === m && a.getDate() === d, ev = p.events || {};
|
|
115
|
+
const same = (a, d) => a && a.getFullYear() === y && a.getMonth() === m && a.getDate() === d, ev = p.events || {}, lo = pd(p.min), hi = pd(p.max), day0 = x => new Date(x.getFullYear(), x.getMonth(), x.getDate()), blocked = d => { const t = new Date(y, m, d); return !!((lo && t < day0(lo)) || (hi && t > day0(hi)) || (p.disabledDate && p.disabledDate(t))); };
|
|
112
116
|
return h('div', { class: 'zg-cal' }, h('div', { class: 'zg-cal__h' }, h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': 'Previous month', onClick: () => set({ ym: new Date(y, m - 1, 1).getTime() }) }, icon(h, 'chevron-left', 16)), h('b', null, new Intl.DateTimeFormat(loc, { month: 'long', year: 'numeric' }).format(b)), h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': 'Next month', onClick: () => set({ ym: new Date(y, m + 1, 1).getTime() }) }, icon(h, 'chevron-right', 16))),
|
|
113
|
-
h('div', { class: 'zg-cal__g' }, ...wd.map((d, i) => h('span', { key: 'w' + i, class: 'zg-cal__w' }, d)), ...Array.from({ length: off }, (_, i) => h('span', { key: 'o' + i })), ...Array.from({ length: dn }, (_, i) => { const d = i + 1, key = `${y}-${String(m + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`; return h('button', { key: d, type: 'button', class: 'zg-cal__d', 'aria-pressed': String(same(s, d)), 'data-today': String(same(today, d)), 'data-ev': ev[key] ? '' : undefined, title: ev[key] || undefined, onClick: () => p.onChange && p.onChange(new Date(y, m, d)) }, d); }))); } },
|
|
114
|
-
DatePicker: { state: { open: false }, render: ({ C, h, props: p, state, set }) => lab(h, p, h('div', { class: 'zg-select' }, h('button', { type: 'button', class: 'zg-btn zg-select__trigger', 'aria-haspopup': 'dialog', 'aria-expanded': String(state.open && !p.disabled), disabled: p.disabled, style: { height: 'var(--zg-h-lg)' }, onClick: () => set({ open: !state.open }) }, h('span', { style: { display: 'flex', gap: '10px', alignItems: 'center' } }, icon(h, 'calendar', 16), p.modelValue ? new Intl.DateTimeFormat(p.locale || 'en-GB', { dateStyle: 'medium' }).format(pd(p.modelValue) || new Date()) : (p.placeholder || 'Select date')), icon(h, 'chevron-down', 16)),
|
|
115
|
-
state.open && !p.disabled ? h('div', { class: 'zg-select__list', 'data-zg-dd': '', ref: ddRef(() => set({ open: false })), style: { padding: '12px' } }, h(C.Calendar, { modelValue: p.modelValue, locale: p.locale, weekStartsOn: p.weekStartsOn, onChange: d => { set({ open: false }); p.onChange && p.onChange(d); } })) : null)) },
|
|
117
|
+
h('div', { class: 'zg-cal__g' }, ...wd.map((d, i) => h('span', { key: 'w' + i, class: 'zg-cal__w' }, d)), ...Array.from({ length: off }, (_, i) => h('span', { key: 'o' + i })), ...Array.from({ length: dn }, (_, i) => { const d = i + 1, key = `${y}-${String(m + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`; return h('button', { key: d, type: 'button', class: 'zg-cal__d', 'aria-pressed': String(same(s, d)), 'data-today': String(same(today, d)), 'data-ev': ev[key] ? '' : undefined, title: ev[key] || undefined, disabled: blocked(d) || undefined, onClick: () => !blocked(d) && p.onChange && p.onChange(new Date(y, m, d)) }, d); }))); } },
|
|
118
|
+
DatePicker: { state: { open: false }, render: ({ C, h, props: p, state, set }) => lab(h, p, h('div', { class: 'zg-select' }, h('button', { type: 'button', class: 'zg-btn zg-select__trigger', 'aria-haspopup': 'dialog', 'aria-expanded': String(state.open && !p.disabled), disabled: p.disabled, style: { height: 'var(--zg-h-lg)' }, onClick: () => set({ open: !state.open }) }, h('span', { style: { display: 'flex', gap: '10px', alignItems: 'center' } }, icon(h, 'calendar', 16), p.modelValue ? new Intl.DateTimeFormat(p.locale || 'en-GB', { dateStyle: 'medium' }).format(pd(p.modelValue) || new Date()) : (p.placeholder || 'Select date')), h('span', { style: { display: 'flex', alignItems: 'center', gap: '6px' } }, p.clearable && p.modelValue && !p.disabled ? h('span', { role: 'button', tabIndex: 0, class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': 'Clear', onClick: e => { e.stopPropagation(); p.onChange && p.onChange(null); }, onKeydown: e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); p.onChange && p.onChange(null); } } }, icon(h, 'x', 14)) : null, icon(h, 'chevron-down', 16))),
|
|
119
|
+
state.open && !p.disabled ? h('div', { class: 'zg-select__list', 'data-zg-dd': '', ref: ddRef(() => set({ open: false })), style: { padding: '12px' } }, h(C.Calendar, { modelValue: p.modelValue, locale: p.locale, weekStartsOn: p.weekStartsOn, min: p.min, max: p.max, disabledDate: p.disabledDate, onChange: d => { set({ open: false }); p.onChange && p.onChange(d); } })) : null)) },
|
|
116
120
|
TimePicker: { render: ({ C, h, props: p }) => { const [H0, M0] = (p.modelValue || '09:00').split(':'), H = String(H0 ?? '00').padStart(2, '0'), M = String(M0 ?? '00').padStart(2, '0'); const hs = Array.from({ length: 24 }, (_, i) => { const v = String(i).padStart(2, '0'); return { value: v, label: v }; }), st = Math.max(1, Math.min(60, Math.floor(+p.minuteStep) || 15)), mv = Array.from({ length: Math.ceil(60 / st) }, (_, i) => String(i * st).padStart(2, '0')); if (!mv.includes(M)) { mv.push(M); mv.sort(); } const ms = mv.map(v => ({ value: v, label: v }));
|
|
117
121
|
return h('div', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label) : null, h('div', { style: { display: 'flex', gap: '8px', alignItems: 'center' } }, h('div', { style: { flex: 1 } }, h(C.Select, { options: hs, disabled: p.disabled, modelValue: H, onChange: v => p.onChange && p.onChange(v + ':' + M) })), h('b', null, ':'), h('div', { style: { flex: 1 } }, h(C.Select, { options: ms, disabled: p.disabled, modelValue: M, onChange: v => p.onChange && p.onChange(H + ':' + v) })))); } },
|
|
118
122
|
FormField: { render: ({ h, props: p, children }) => h('div', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label, p.required ? h('span', { style: { color: 'var(--zg-red)' } }, ' *') : null) : null, ...children, p.error ? h('span', { class: 'zg-hint zg-hint--error' }, p.error) : p.hint ? h('span', { class: 'zg-hint' }, p.hint) : null) },
|
package/src/core/components4.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { icon, cx, fmt } from './components.js';
|
|
3
3
|
import { trap } from './dialog.js';
|
|
4
4
|
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
|
|
5
|
-
const pd = v => { if (v == null || v === '') return null; if (v instanceof Date) return isNaN(v) ? null : v; if (typeof v === 'string') { const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(v.trim()); if (m) return new Date(+m[1], +m[2] - 1, +m[3]); } const d = new Date(v); return isNaN(d) ? null : d; };
|
|
5
|
+
export const pd = v => { if (v == null || v === '') return null; if (v instanceof Date) return isNaN(v) ? null : v; if (typeof v === 'string') { const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(v.trim()); if (m) return new Date(+m[1], +m[2] - 1, +m[3]); } const d = new Date(v); return isNaN(d) ? null : d; };
|
|
6
6
|
const SAN_TAGS = new Set('a b i u s em strong strike del ins sub sup p div span br hr ul ol li blockquote pre code h1 h2 h3 h4 h5 h6 img table thead tbody tr td th font small mark'.split(' ')), SAN_DROP = new Set('script style iframe object embed link meta base form input button textarea select svg math noscript template frame frameset applet'.split(' ')), SAN_ATTR = new Set('href src alt title target rel class style colspan rowspan align color size width height'.split(' '));
|
|
7
7
|
const sanitizeHtml = html => { if (typeof document === 'undefined') return html; const t = document.createElement('template'); t.innerHTML = html;
|
|
8
8
|
const walk = node => { for (const c of [...node.childNodes]) { if (c.nodeType !== 1) { if (c.nodeType !== 3) c.remove(); continue; } const tag = c.tagName.toLowerCase(); if (SAN_DROP.has(tag)) { c.remove(); continue; } walk(c); if (!SAN_TAGS.has(tag)) { c.replaceWith(...c.childNodes); continue; }
|
|
@@ -19,7 +19,7 @@ export const rules = {
|
|
|
19
19
|
alphaDash: () => v => !v || /^[\p{L}\d_-]+$/u.test(v) || 'Letters, digits, dash and underscore',
|
|
20
20
|
minLength: n => v => !v || String(v).length >= n || `At least ${n} characters`,
|
|
21
21
|
maxLength: n => v => !v || String(v).length <= n || `At most ${n} characters`,
|
|
22
|
-
range: (a, b) => v =>
|
|
22
|
+
range: (a, b) => v => v == null || v === '' || (Number(v) >= a && Number(v) <= b) || `Between ${a} and ${b}`,
|
|
23
23
|
regex: (re, msg) => v => !v || re.test(v) || msg || 'Invalid format',
|
|
24
24
|
confirmed: get => v => v === get() || 'Values do not match',
|
|
25
25
|
password: () => v => !v || (/\d/.test(v) && /[A-ZА-Я]/.test(v) && String(v).length >= 8) || '8+ characters, a digit and a capital letter'
|
|
@@ -42,7 +42,7 @@ export const more4 = {
|
|
|
42
42
|
RichEditor: { render: ({ h, props: p }) => { let ed; const san = x => p.sanitize ? sanitizeHtml(x) : x, emit = () => { if (!ed) return; const x = san(ed.innerHTML); ed.__zg = x; p.onChange && p.onChange(x); }; const cmd = (c, v) => () => { if (!ed) return; ed.focus(); try { document.execCommand(c, false, v); } catch (e) {} emit(); };
|
|
43
43
|
const B = [['bold', 'bold', 'Bold'], ['italic', 'italic', 'Italic'], ['underline', 'underline', 'Underline'], ['strikeThrough', 'strikethrough', 'Strike'], null, ['justifyLeft', 'align-left', 'Left'], ['justifyCenter', 'align-center', 'Center'], ['justifyRight', 'align-right', 'Right'], null, ['insertUnorderedList', 'list', 'Bullets'], ['insertOrderedList', 'list-ordered', 'Numbers'], null, ['formatBlock', 'quote', 'Quote', 'blockquote'], ['formatBlock', 'code', 'Code block', 'pre']];
|
|
44
44
|
const area = h('div', { class: 'zg-rte__c', contentEditable: 'true', role: 'textbox', 'aria-multiline': 'true', 'aria-label': p.label || 'Editor', 'data-ph': p.placeholder || 'Write something…', ref: el => { if (!el) return; ed = el; const want = p.modelValue ?? ''; if (el.__zg !== want && el.innerHTML !== want) el.innerHTML = san(want); el.__zg = want; }, onFocus: e => { ed = e.currentTarget; }, onInput: e => { ed = e.currentTarget; emit(); } });
|
|
45
|
-
return h('div', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label) : null, h('div', { class: 'zg-rte' }, h('div', { class: 'zg-rte__t' }, ...B.map((b, i) => b ? h('button', { key: b[0], type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', title: b[2], 'aria-label': b[2], onMouseDown: e => e.preventDefault(), onClick: cmd(b[0], b[3]) }, icon(h, b[1], 16)) : h('span', { key: 's' + i, class: 'zg-cb__s' }))), area)); } },
|
|
45
|
+
return h('div', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label) : null, h('div', { class: 'zg-rte' }, h('div', { class: 'zg-rte__t' }, ...B.map((b, i) => b ? h('button', { key: b[0] + i, type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', title: b[2], 'aria-label': b[2], onMouseDown: e => e.preventDefault(), onClick: cmd(b[0], b[3]) }, icon(h, b[1], 16)) : h('span', { key: 's' + i, class: 'zg-cb__s' }))), area)); } },
|
|
46
46
|
RangeSlider: { render: ({ h, props: p }) => { const mn = p.min ?? 0, mx = p.max ?? 100, [a, b] = p.modelValue || [mn, mx], pa = (a - mn) / (mx - mn) * 100, pb = (b - mn) / (mx - mn) * 100, f = p.formatValue || (v => v);
|
|
47
47
|
const inp = (v, k) => h('input', { type: 'range', class: 'zg-range2', min: mn, max: mx, step: p.step || 1, value: v, 'aria-label': k ? 'Max' : 'Min', onInput: e => { const x = Number(e.target.value); p.onChange && p.onChange(k ? [a, Math.max(x, a)] : [Math.min(x, b), b]); } });
|
|
48
48
|
return h('div', { class: 'zg-field' }, h('div', { style: { display: 'flex', justifyContent: 'space-between' } }, p.label ? h('span', { class: 'zg-label' }, p.label) : h('span'), h('span', { class: 'zg-mono', style: { fontSize: '13px' } }, f(a) + ' – ' + f(b))), h('div', { class: 'zg-range2w', style: { '--a': pa + '%', '--b': pb + '%' } }, h('i'), inp(a, 0), inp(b, 1))); } },
|
package/src/core/components6.js
CHANGED
|
@@ -20,29 +20,38 @@ export const more6 = {
|
|
|
20
20
|
return h('div', { class: 'zg-sort', role: 'list' }, ...items.map((it, i) => h('div', { key: it.id ?? i, role: 'listitem', class: cx('zg-sort__i', state.over === i && state.drag !== i && 'is-over', state.drag === i && 'is-drag'), draggable: 'true', onDragstart: e => { e.dataTransfer.effectAllowed = 'move'; try { e.dataTransfer.setData('text/plain', String(i)); } catch (x) {} set({ drag: i }); }, onDragover: e => { e.preventDefault(); if (state.over !== i) set({ over: i }); }, onDrop: e => { e.preventDefault(); drop(i); }, onDragend: () => set({ drag: null, over: null }) },
|
|
21
21
|
h('span', { class: 'zg-sort__h', 'aria-hidden': 'true' }, icon(h, 'grip-vertical', 16)), h('div', { style: { flex: 1, minWidth: 0 } }, p.render ? p.render(it, i) : h('b', null, it.label)),
|
|
22
22
|
h('span', { style: { display: 'flex', gap: '2px' } }, h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': 'Move up', disabled: i === 0, onClick: () => drop(i - 1, i) }, icon(h, 'chevron-up', 14)), h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': 'Move down', disabled: i === items.length - 1, onClick: () => drop(i + 1, i) }, icon(h, 'chevron-down', 14)))))); } },
|
|
23
|
-
Map: { state: { lon: null, lat: null, z: null,
|
|
24
|
-
const W = p.width || 640, H = p.height || 360, z = state.z ?? p.zoom ?? 14, lat0 = state.lat ?? p.lat ?? 21.4225, lon0 = state.lon ?? p.lon ?? 39.8262, n = 2 ** z;
|
|
25
|
-
const
|
|
26
|
-
const [cx0, cy0] =
|
|
23
|
+
Map: { state: { lon: null, lat: null, z: null, dk: null, w: null }, render: ({ h, props: p, state, set }) => {
|
|
24
|
+
const W = state.w || p.width || 640, H = p.height || 360, z = state.z ?? p.zoom ?? 14, lat0 = state.lat ?? p.lat ?? 21.4225, lon0 = state.lon ?? p.lon ?? 39.8262, n = 2 ** z;
|
|
25
|
+
const mpx = (lat, lon, nn) => [(lon + 180) / 360 * nn * 256, (1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * nn * 256], px = (lat, lon) => mpx(lat, lon, n);
|
|
26
|
+
const [cx0, cy0] = mpx(lat0, lon0, n), x0 = cx0 - W / 2, y0 = cy0 - H / 2, dark = p.theme ? p.theme === 'dark' : state.dk === true;
|
|
27
27
|
const tiles = []; for (let tx = Math.floor(x0 / 256); tx <= Math.floor((x0 + W) / 256); tx++) for (let ty = Math.floor(y0 / 256); ty <= Math.floor((y0 + H) / 256); ty++) if (ty >= 0 && ty < n) tiles.push(h('img', { key: tx + '_' + ty, alt: '', draggable: 'false', src: `https://${'abc'[(tx + ty) & 1]}.basemaps.cartocdn.com/${p.style || (dark ? 'dark_all' : 'light_all')}/${z}/${((tx % n) + n) % n}/${ty}.png`, style: { position: 'absolute', left: (tx * 256 - x0) + 'px', top: (ty * 256 - y0) + 'px', width: '256px', height: '256px' } }));
|
|
28
|
-
const unpx = (x, y) =>
|
|
29
|
-
|
|
28
|
+
const unpx = (x, y, nn) => [Math.atan(Math.sinh(Math.PI * (1 - 2 * y / (nn * 256)))) * 180 / Math.PI, x / (nn * 256) * 360 - 180];
|
|
29
|
+
const emit = (la, lo, zz) => { p.onMove && p.onMove({ lat: la, lon: lo, zoom: zz }); if (zz !== z) p.onZoom && p.onZoom(zz); };
|
|
30
|
+
const zoomAt = (dz, ox, oy) => { const nz = clamp(z + dz, 3, 18); if (nz === z) return; const s = 2 ** (nz - z), nn = 2 ** nz, [la, lo] = unpx((cx0 + ox) * s - ox, (cy0 + oy) * s - oy, nn); set({ z: nz, lat: la, lon: lo }); emit(la, lo, nz); };
|
|
31
|
+
const rel = (e, el) => { const q = el.getBoundingClientRect(); return [e.clientX - q.left - q.width / 2, e.clientY - q.top - q.height / 2]; };
|
|
32
|
+
const pan = e => { if (e.button > 0 || !e.isPrimary) return; const el = e.currentTarget, sx = e.clientX, sy = e.clientY, pid = e.pointerId; let cap = false;
|
|
33
|
+
const mv = ev => { if (ev.pointerId !== pid) return; const dx = ev.clientX - sx, dy = ev.clientY - sy; if (!cap) { if (Math.abs(dx) + Math.abs(dy) < 4) return; cap = true; try { el.setPointerCapture(pid); } catch (x) {} } const [la, lo] = unpx(cx0 - dx, cy0 - dy, n); set({ lat: la, lon: lo }); emit(la, lo, z); };
|
|
34
|
+
const up = ev => { if (ev.pointerId !== pid) return; el.removeEventListener('pointermove', mv); el.removeEventListener('pointerup', up); el.removeEventListener('pointercancel', up); };
|
|
35
|
+
el.addEventListener('pointermove', mv); el.addEventListener('pointerup', up); el.addEventListener('pointercancel', up); };
|
|
30
36
|
const mk = (p.markers || []).map((m, i) => { const [mx, my] = px(m.lat, m.lon); return h('button', { key: 'm' + i, type: 'button', class: cx('zg-pin', m.tone && 'zg-t-' + m.tone), title: m.label, style: { left: (mx - x0) + 'px', top: (my - y0) + 'px' }, onClick: () => p.onSelect && p.onSelect(m) }, icon(h, m.icon || 'map-pin', 16)); });
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
const stop = e => e.stopPropagation(), zb = (dz, k, d, ic) => h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm', 'aria-label': (p.labels && p.labels[k]) || d, onPointerdown: stop, onDblclick: stop, onClick: () => zoomAt(dz, 0, 0) }, icon(h, ic, 16));
|
|
38
|
+
return h('div', { class: 'zg-map', role: 'application', 'aria-label': p.label || 'Map', style: { height: H + 'px' }, onPointerdown: pan, onDblclick: e => { const [ox, oy] = rel(e, e.currentTarget); zoomAt(1, ox, oy); },
|
|
39
|
+
ref: el => { if (!el) return; el.__zgw = e => { e.preventDefault(); const t = Date.now(); if (t - (el.__zgt || 0) < 180) return; el.__zgt = t; const [ox, oy] = rel(e, el); zoomAt(e.deltaY < 0 ? 1 : -1, ox, oy); };
|
|
40
|
+
if (!el.__zgi) { el.__zgi = 1; el.addEventListener('wheel', e => el.__zgw(e), { passive: false }); if (typeof ResizeObserver !== 'undefined') new ResizeObserver(() => { const w = Math.round(el.clientWidth); if (w && w !== el.__zgl) { el.__zgl = w; set({ w }); } }).observe(el); }
|
|
41
|
+
if (p.theme == null && state.dk == null) set({ dk: !!el.closest('[data-theme="dark"]') }); } }, ...tiles, ...mk,
|
|
42
|
+
h('div', { class: 'zg-map__z' }, zb(1, 'zoomIn', 'Zoom in', 'plus'), zb(-1, 'zoomOut', 'Zoom out', 'minus')),
|
|
43
|
+
h('span', { class: 'zg-map__c' }, '© OpenStreetMap · CARTO')); } },
|
|
35
44
|
ImageUpload: { state: { src: null }, render: ({ h, props: p, state, set }) => { const src = state.src || p.modelValue, round = p.shape === 'circle';
|
|
36
45
|
return h('div', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label) : null, h('label', { class: 'zg-imgup', style: { aspectRatio: p.aspect || '1/1', maxWidth: (({ xs: 112, sm: 128, md: 160, lg: 192, xl: 224 })[p.size] || p.size || 160) + 'px', borderRadius: round ? '50%' : '24px', backgroundImage: src ? `url(${src})` : undefined } }, src ? null : h('span', { class: 'zg-imgup__e' }, icon(h, 'image-up', 24), h('span', { class: 'zg-hint' }, p.hint || 'Upload')), h('span', { class: 'zg-imgup__o' }, icon(h, 'camera', 18)),
|
|
37
46
|
h('input', { type: 'file', accept: 'image/*', style: { display: 'none' }, onChange: e => { const f = e.target.files[0]; if (!f) return; const r = new FileReader(); r.onload = () => { set({ src: r.result }); p.onChange && p.onChange(r.result, f); }; r.readAsDataURL(f); } }))); } },
|
|
38
47
|
Screensaver: { state: { t: 0 }, render: ({ h, props: p, state, set }) => { if (!p.open) return p.idleMs > 0 ? h('span', { hidden: true, 'aria-hidden': 'true', ref: life({ idleMs: p.idleMs, onIdle: p.onIdle, mount: (el, get) => { let tm; const arm = () => { clearTimeout(tm); tm = setTimeout(() => { const o = get(); o.onIdle && o.onIdle(); }, get().idleMs); }, evs = ['pointermove', 'pointerdown', 'keydown', 'wheel', 'touchstart']; evs.forEach(e => document.addEventListener(e, arm, { passive: true })); arm(); return () => { clearTimeout(tm); evs.forEach(e => document.removeEventListener(e, arm)); }; } }) }) : null;
|
|
39
|
-
const d = new Date(state.t ||
|
|
48
|
+
const d = new Date(state.t || 0), pad = n => String(n).padStart(2, '0'), ph = (d.getSeconds() % 40) / 40;
|
|
40
49
|
return h('div', { class: 'zg-saver', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Screensaver', tabIndex: 0, ref: trap({ onClose: p.onClose, onKey: e => { if (e.key === 'Tab') return false; p.onClose && p.onClose(); return true; }, mount: el => { const id = setInterval(() => set({ t: Date.now() }), 1000); set({ t: Date.now() }); return () => clearInterval(id); } }), onClick: p.onClose }, h('span', { class: 'zg-saver__g', style: { left: (10 + ph * 60) + '%', top: (20 + Math.sin(ph * 6.28) * 18) + '%' } }),
|
|
41
|
-
h('div', { class: 'zg-saver__c' }, h('span', { class: 'zg-saver__m', style: { backgroundImage: 'url(assets/brand/ziyarago-mark.svg)' } }), h('b', { class: 'zg-num', style: { fontSize: 'clamp(64px,12vw,160px)', fontWeight: 200, letterSpacing: '-.04em', lineHeight: 1 } }, pad(d.getHours()) + ':' + pad(d.getMinutes())), h('span', { style: { color: 'var(--zg-text-2)', fontSize: '18px' } }, new Intl.DateTimeFormat(p.locale || 'en-GB', { weekday: 'long', day: 'numeric', month: 'long' }).format(d)), p.hint ? h('span', { class: 'zg-hint' }, p.hint) : null)); } },
|
|
42
|
-
Tour: { state: { i: 0, r: null }, render: ({ C, h, props: p, state, set }) => { if (!p.open) return null; const st = p.steps || [], i = clamp(p.step != null ? p.step : state.i, 0, Math.max(0, st.length - 1)), s = st[i] || {}
|
|
43
|
-
|
|
44
|
-
const r =
|
|
50
|
+
h('div', { class: 'zg-saver__c' }, h('span', { class: 'zg-saver__m', style: { backgroundImage: 'url(assets/brand/ziyarago-mark.svg)' } }), h('b', { class: 'zg-num', style: { fontSize: 'clamp(64px,12vw,160px)', fontWeight: 200, letterSpacing: '-.04em', lineHeight: 1 } }, state.t ? pad(d.getHours()) + ':' + pad(d.getMinutes()) : '--:--'), h('span', { style: { color: 'var(--zg-text-2)', fontSize: '18px' } }, state.t ? new Intl.DateTimeFormat(p.locale || 'en-GB', { weekday: 'long', day: 'numeric', month: 'long' }).format(d) : '\u00a0'), p.hint ? h('span', { class: 'zg-hint' }, p.hint) : null)); } },
|
|
51
|
+
Tour: { state: { i: 0, r: null }, render: ({ C, h, props: p, state, set }) => { if (!p.open) return null; const st = p.steps || [], i = clamp(p.step != null ? p.step : state.i, 0, Math.max(0, st.length - 1)), s = st[i] || {};
|
|
52
|
+
const measure = () => { if (state.r || state.busy || !s.target) return; const el0 = document.querySelector(s.target); if (!el0) return; state.busy = true; setTimeout(() => { const el = document.querySelector(s.target) || el0, q = el.getBoundingClientRect(); if (q.top < 80 || q.bottom > innerHeight - 220) { let a = el.parentElement; while (a && !(/(auto|scroll)/.test(getComputedStyle(a).overflowY) && a.scrollHeight > a.clientHeight + 4)) a = a.parentElement; const dy = q.top - innerHeight / 3; if (a) a.scrollTop += dy; else window.scrollTo(0, window.scrollY + dy); } setTimeout(() => { state.busy = false; set({ r: (document.querySelector(s.target) || el).getBoundingClientRect() }); }, 160); }, 0); };
|
|
53
|
+
const r = state.r, pad = 8, below = r ? r.bottom + 12 + 190 < innerHeight : true;
|
|
45
54
|
const go = d => { const n = clamp(i + d, 0, st.length - 1); if (n === i) return; set({ i: n, r: null }); p.onStep && p.onStep(n); }, done = () => { set({ i: 0, r: null }); p.onClose && p.onClose(); };
|
|
46
|
-
return h('div', { class: 'zg-tourov', role: 'dialog', 'aria-label': 'Tour', ref: trap({ focus: false, onClose: done, mount: () => () => set({ i: 0, r: null }), onKey: e => { const rtl = document.documentElement.dir === 'rtl', k = e.key === 'ArrowRight' ? (rtl ? -1 : 1) : e.key === 'ArrowLeft' ? (rtl ? 1 : -1) : 0; if (k) { e.preventDefault(); go(k); return true; } } }) }, r ? h('span', { class: 'zg-tourov__cut', style: { left: (r.left - pad) + 'px', top: (r.top - pad) + 'px', width: (r.width + pad * 2) + 'px', height: (r.height + pad * 2) + 'px' } }) : h('span', { class: 'zg-tourov__full' }),
|
|
55
|
+
return h('div', { class: 'zg-tourov', role: 'dialog', 'aria-label': 'Tour', ref: el => { if (el) measure(); trap({ focus: false, onClose: done, mount: () => () => set({ i: 0, r: null }), onKey: e => { const rtl = document.documentElement.dir === 'rtl', k = e.key === 'ArrowRight' ? (rtl ? -1 : 1) : e.key === 'ArrowLeft' ? (rtl ? 1 : -1) : 0; if (k) { e.preventDefault(); go(k); return true; } } })(el); } }, r ? h('span', { class: 'zg-tourov__cut', style: { left: (r.left - pad) + 'px', top: (r.top - pad) + 'px', width: (r.width + pad * 2) + 'px', height: (r.height + pad * 2) + 'px' } }) : h('span', { class: 'zg-tourov__full' }),
|
|
47
56
|
h('div', { style: { position: 'fixed', zIndex: 2, left: r ? clamp(r.left, 12, innerWidth - 392) + 'px' : '50%', top: r ? (below ? r.bottom + 12 : Math.max(12, r.top - 12 - 190)) + 'px' : '40%', transform: r ? 'none' : 'translateX(-50%)' } }, h(C.TourPopover, { title: s.title, text: s.text, step: (i + 1) + ' / ' + st.length, last: i === st.length - 1, onBack: i ? () => go(-1) : null, onNext: () => i === st.length - 1 ? done() : go(1), onClose: done }))); } }
|
|
48
57
|
};
|
package/src/core/index.js
CHANGED
|
@@ -7,3 +7,5 @@ import { more6 } from './components6.js';
|
|
|
7
7
|
import { more7 } from './components7.js';
|
|
8
8
|
export const components = { ...base, ...more, ...more3, ...more4, ...more6, ...more7, ...pages };
|
|
9
9
|
export { fmt, setIconBase, rules };
|
|
10
|
+
export { LOCALES, t, setUiLocale, getUiLocale, setUiMessages, isRtlLocale, subscribeUiLocale, getUiVersion, defineMessages } from '../i18n/index.js';
|
|
11
|
+
import '../i18n/components/index.js';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Built-in component strings. One file per component in ./components/<Name>.js with all locales:
|
|
2
|
+
// export default { en: { copy: 'Copy' } } (the open source package ships English; other languages come from a language pack
|
|
3
|
+
// that calls defineMessages(component, { ru: {...} }) or from the app via setUiMessages)
|
|
4
|
+
// The kit has its own translator: it never depends on the host app's i18n library. Sync it with yours:
|
|
5
|
+
// watch(() => i18n.locale.value, l => setUiLocale(l)) // vue-i18n
|
|
6
|
+
// setUiMessages('ru', { CopyButton: { copy: 'Скопировать' } }) // override single strings from the app
|
|
7
|
+
export const LOCALES = ['en', 'uz', 'ru', 'tr', 'zh', 'ar'];
|
|
8
|
+
export const RTL_LOCALES = ['ar'];
|
|
9
|
+
|
|
10
|
+
const registry = {};
|
|
11
|
+
const overrides = {};
|
|
12
|
+
const subs = new Set();
|
|
13
|
+
let locale = 'en';
|
|
14
|
+
let version = 0;
|
|
15
|
+
|
|
16
|
+
const norm = l => { const k = String(l || 'en').toLowerCase().split(/[-_]/)[0]; return LOCALES.includes(k) ? k : 'en'; };
|
|
17
|
+
const bump = () => { version++; subs.forEach(f => f(locale)); };
|
|
18
|
+
|
|
19
|
+
/** Register strings of a component. Calling it again for the same component MERGES locales: an extra pack can add uz, ru, tr, zh, ar on top of the built-in English. */
|
|
20
|
+
export function defineMessages(component, messages) {
|
|
21
|
+
const cur = registry[component] || (registry[component] = {});
|
|
22
|
+
for (const [l, msgs] of Object.entries(messages)) cur[l] = { ...(cur[l] || {}), ...msgs };
|
|
23
|
+
return registry[component];
|
|
24
|
+
}
|
|
25
|
+
export function getUiLocale() { return locale; }
|
|
26
|
+
export function isRtlLocale(l = locale) { return RTL_LOCALES.includes(norm(l)); }
|
|
27
|
+
export function setUiLocale(l) { const n = norm(l); if (n === locale) return; locale = n; bump(); }
|
|
28
|
+
/** Override single strings: setUiMessages('ru', { Pagination: { prev: 'Назад' } }). Call again to change; pass null to clear a locale. */
|
|
29
|
+
export function setUiMessages(l, partial) {
|
|
30
|
+
const k = norm(l);
|
|
31
|
+
if (partial === null) delete overrides[k];
|
|
32
|
+
else for (const [c, msgs] of Object.entries(partial || {})) overrides[k] = { ...(overrides[k] || {}), [c]: { ...(overrides[k]?.[c] || {}), ...msgs } };
|
|
33
|
+
bump();
|
|
34
|
+
}
|
|
35
|
+
export function subscribeUiLocale(fn) { subs.add(fn); return () => subs.delete(fn); }
|
|
36
|
+
export function getUiVersion() { return version; }
|
|
37
|
+
|
|
38
|
+
const fill = (s, p) => (p ? s.replace(/\{(\w+)\}/g, (m, k) => (k in p ? String(p[k]) : m)) : s);
|
|
39
|
+
|
|
40
|
+
/** Translate a component string. Order: app overrides, component file in the current locale, app overrides in English, component file in English, the key. */
|
|
41
|
+
export function t(component, key, params) {
|
|
42
|
+
const hit = overrides[locale]?.[component]?.[key] ?? registry[component]?.[locale]?.[key]
|
|
43
|
+
?? overrides.en?.[component]?.[key] ?? registry[component]?.en?.[key] ?? key;
|
|
44
|
+
return fill(hit, params);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Locale-aware helper for numbers and dates in components (uses the browser/Node Intl). */
|
|
48
|
+
export const intlLocale = () => ({ en: 'en-US', uz: 'uz-UZ', ru: 'ru-RU', tr: 'tr-TR', zh: 'zh-CN', ar: 'ar' })[locale];
|
package/src/meta/actions.js
CHANGED
|
@@ -17,10 +17,13 @@ export default {
|
|
|
17
17
|
loading: { type: 'boolean', default: false, description: 'Shows a spinner instead of the icon and disables the button.' },
|
|
18
18
|
disabled: { type: 'boolean', default: false, description: 'Blocks interaction and dims the button.' },
|
|
19
19
|
block: { type: 'boolean', default: false, description: 'Stretches the button to the full width of its container.' },
|
|
20
|
+
type: { type: "'button' | 'submit' | 'reset'", values: ['button', 'submit', 'reset'], default: 'button', description: 'Native button type. Use submit inside a form.' },
|
|
21
|
+
iconOnly: { type: 'boolean', default: false, description: 'Square button (width equals height) with no horizontal padding, for a lone icon. Pass an aria-label.' },
|
|
22
|
+
title: { type: 'string', description: 'Native tooltip text.' },
|
|
20
23
|
},
|
|
21
24
|
events: { click: 'Native click event. Not fired while disabled or loading.' },
|
|
22
25
|
slots: { default: 'Button label.' },
|
|
23
|
-
notes: ['
|
|
26
|
+
notes: ['Renders type="button" unless type is set, so it never submits a form by itself.'],
|
|
24
27
|
},
|
|
25
28
|
IconButton: {
|
|
26
29
|
description: 'Square button with only an icon; use for toolbars and compact actions.',
|
|
@@ -80,9 +83,12 @@ export default {
|
|
|
80
83
|
SegmentedControl: {
|
|
81
84
|
description: 'Segmented switch for choosing exactly one of a few options.',
|
|
82
85
|
props: {
|
|
83
|
-
items: { type: '{ value: string; label: string }[]', default: '[]', required: true, description: 'Segments to render.' },
|
|
86
|
+
items: { type: '{ value: string; label: string; icon?: string; disabled?: boolean }[]', default: '[]', required: true, description: 'Segments to render. icon is an icon name; disabled: true dims one segment.' },
|
|
84
87
|
modelValue: { type: 'string', model: true, description: 'Value of the selected segment.' },
|
|
85
88
|
size: sizeProp('md', 'Control size. sm and md also have a dedicated compact style.'),
|
|
89
|
+
block: { type: 'boolean', default: false, description: 'Stretches the control to the container width with equal-width segments.' },
|
|
90
|
+
iconOnly: { type: 'boolean', default: false, description: 'Hides labels of segments that have an icon; the label stays as tooltip and aria-label.' },
|
|
91
|
+
ariaLabel: { type: 'string', description: 'Accessible name of the group.' },
|
|
86
92
|
},
|
|
87
93
|
events: { change: 'Called with the value of the clicked segment.' },
|
|
88
94
|
},
|
|
@@ -119,8 +125,9 @@ export default {
|
|
|
119
125
|
Badge: {
|
|
120
126
|
description: 'Status badge with a colored tone and optional dot.',
|
|
121
127
|
props: {
|
|
122
|
-
tone: { type: "'brand' | 'success' | 'warning' | 'danger' | 'info'", values: ['brand', 'success', 'warning', 'danger', 'info'], description: 'Color. Omit for the neutral style.' },
|
|
128
|
+
tone: { type: "'brand' | 'success' | 'warning' | 'danger' | 'info' | 'inverse'", values: ['brand', 'success', 'warning', 'danger', 'info', 'inverse'], description: 'Color. inverse is a solid high-contrast badge (dark on light themes, light on dark). Omit for the neutral style.' },
|
|
123
129
|
dot: { type: 'boolean', default: false, description: 'Shows a status dot before the content.' },
|
|
130
|
+
shape: { type: "'pill' | 'square'", values: ['pill', 'square'], default: 'pill', description: 'square uses small rounded corners instead of a full pill.' },
|
|
124
131
|
size: sizeProp('md', 'Badge height: xs 18px, sm 20px, md 24px, lg 28px, xl 32px.'),
|
|
125
132
|
},
|
|
126
133
|
slots: { default: 'Badge content.' },
|
package/src/meta/data.js
CHANGED
|
@@ -12,19 +12,22 @@ export default {
|
|
|
12
12
|
props: {
|
|
13
13
|
title: { type: 'string', description: 'Header title. The header renders only when title or the actions slot is set.' },
|
|
14
14
|
subtitle: { type: 'string', description: 'Muted line under the title.' },
|
|
15
|
-
variant: { type: "'inverse' | 'gradient'", values: ['inverse', 'gradient'], description: 'inverse is a dark card regardless of the page theme; gradient is a tinted gradient card colored by tone. Omit for the default glass card.' },
|
|
15
|
+
variant: { type: "'inverse' | 'gradient' | 'outline'", values: ['inverse', 'gradient', 'outline'], description: 'inverse is a dark card regardless of the page theme; gradient is a tinted gradient card colored by tone; outline is transparent with a border. Omit for the default glass card.' },
|
|
16
16
|
tone: { type: "'brand' | 'sky' | 'success' | 'warning' | 'danger' | 'neutral' | 'rose' | 'violet'", values: ['brand', 'sky', 'success', 'warning', 'danger', 'neutral', 'rose', 'violet'], default: 'brand', description: 'Gradient color. Used only with variant="gradient".' },
|
|
17
17
|
intensity: { type: "'vivid'", values: ['vivid'], description: 'With variant="gradient", fills the card with a saturated color instead of a soft tint.' },
|
|
18
|
+
padding: { type: "'none'", values: ['none'], description: 'none removes the inner padding (for full-bleed tables or images). Other paddings come from size.' },
|
|
19
|
+
interactive: { type: 'boolean', default: false, description: 'Adds a pointer cursor and hover lift. With a click handler the card also becomes keyboard focusable (Enter activates).' },
|
|
18
20
|
size: size('md', 'Padding, gap and title size.'),
|
|
19
21
|
},
|
|
20
|
-
slots: { default: 'Card body.', actions: 'Right side of the header, for buttons or menus.' },
|
|
22
|
+
slots: { default: 'Card body.', actions: 'Right side of the header, for buttons or menus.', header: 'Replaces the whole title/actions header with custom content.', footer: 'Row under the body, for buttons or meta.' },
|
|
23
|
+
events: { click: 'Native click on the card. Combine with interactive.' },
|
|
21
24
|
notes: ['Vue slot names are React props (actions) and children (default).'],
|
|
22
25
|
},
|
|
23
26
|
|
|
24
27
|
DataTable: {
|
|
25
28
|
description: 'Table with sortable columns, custom cell rendering and optional row selection checkboxes.',
|
|
26
29
|
props: {
|
|
27
|
-
columns: { type: '{ key: string; label: string; sortable?: boolean; num?: boolean; render?: (value, row, h) => VNode | string }[]', required: true, default: [], description: 'Column definitions. key reads row[key]; num right-aligns numbers; render customizes the cell; sortable shows a sort button in the header.' },
|
|
30
|
+
columns: { type: '{ key: string; label: string; sortable?: boolean; num?: boolean; align?: "left" | "center" | "right"; width?: string; render?: (value, row, h) => VNode | string }[]', required: true, default: [], description: 'Column definitions. key reads row[key]; num right-aligns numbers; render customizes the cell; sortable shows a sort button in the header.' },
|
|
28
31
|
rows: { type: '({ id: string | number; [key: string]: any })[]', required: true, default: [], description: 'Row objects. Every row needs a unique id; missing cell values render as an em dash.' },
|
|
29
32
|
selected: { type: '(string | number)[]', default: [], description: 'Ids of selected rows. Checkboxes show only when the select event is handled.' },
|
|
30
33
|
sortKey: { type: 'string', description: 'Key of the column currently sorted. The component does not sort rows itself.' },
|
|
@@ -33,7 +36,9 @@ export default {
|
|
|
33
36
|
labels: { type: '{ selectAll?: string; selectRow?: string }', default: "{ selectAll: 'Select all', selectRow: 'Select row' }", description: 'Accessible names of the selection checkboxes.' },
|
|
34
37
|
size: size('md', 'Row density and font size.'),
|
|
35
38
|
},
|
|
39
|
+
slots: { empty: 'Replaces the empty text with custom content when rows is empty.' },
|
|
36
40
|
events: {
|
|
41
|
+
rowClick: 'Called with (row, index) when a body row is clicked. Rows get a pointer cursor.',
|
|
37
42
|
select: 'Called with the new array of selected row ids. Handling it enables the checkbox column and select-all.',
|
|
38
43
|
sort: 'Called with (key, dir) where dir is "asc" or "desc". Sort the rows yourself and pass sortKey/sortDir back.',
|
|
39
44
|
},
|
package/src/meta/feedback.js
CHANGED
|
@@ -16,8 +16,9 @@ export default {
|
|
|
16
16
|
props: {
|
|
17
17
|
...alertProps,
|
|
18
18
|
banner: { type: 'boolean', default: false, description: 'Removes the border radius so the alert spans the full width like a Banner.' },
|
|
19
|
+
hideIcon: { type: 'boolean', default: false, description: 'Hides the leading tone icon (the icon slot is hidden too).' },
|
|
19
20
|
},
|
|
20
|
-
slots: { default: 'Message text.' },
|
|
21
|
+
slots: { default: 'Message text.', title: 'Replaces the title prop with custom content.', icon: 'Replaces the tone icon inside the icon tile.', actions: 'Row under the message for buttons or links.' },
|
|
21
22
|
events: { close: 'Called when the close button is clicked. The button is rendered only when this handler is set.' },
|
|
22
23
|
notes: ['Has role="alert", so screen readers announce it when it appears.', 'Not self-dismissing: hide it yourself in the close handler.'],
|
|
23
24
|
},
|
package/src/meta/forms.js
CHANGED
|
@@ -23,10 +23,24 @@ export default {
|
|
|
23
23
|
density: { type: "'compact' | 'default'", values: ['compact', 'default'], default: 'default', description: 'compact uses the small control height.' },
|
|
24
24
|
disabled,
|
|
25
25
|
readonly: { type: 'boolean', default: false, description: 'Makes the input read-only.' },
|
|
26
|
+
required: { type: 'boolean', default: false, description: 'Adds a red asterisk to the label and the native required attribute (browser validation inside a form).' },
|
|
27
|
+
name: { type: 'string', forwarded: true, description: 'Native name attribute of the input.' },
|
|
28
|
+
autocomplete: { type: 'string', forwarded: true, description: 'Native autocomplete hint, for example "email" or "new-password".' },
|
|
29
|
+
min: { type: 'number | string', forwarded: true, description: 'Native min for number, date and time types.' },
|
|
30
|
+
max: { type: 'number | string', forwarded: true, description: 'Native max for number, date and time types.' },
|
|
31
|
+
step: { type: 'number | string', forwarded: true, description: 'Native step for number, date and time types.' },
|
|
32
|
+
maxlength: { type: 'number', forwarded: true, description: 'Maximum number of characters.' },
|
|
33
|
+
minlength: { type: 'number', forwarded: true, description: 'Minimum number of characters (native validation).' },
|
|
34
|
+
inputmode: { type: 'string', forwarded: true, description: 'Virtual keyboard hint: numeric, decimal, tel, email, url, search or none.' },
|
|
35
|
+
pattern: { type: 'string', forwarded: true, description: 'Native validation pattern.' },
|
|
36
|
+
spellcheck: { type: 'boolean', forwarded: true, description: 'Enables or disables spell checking.' },
|
|
37
|
+
autofocus: { type: 'boolean', forwarded: true, description: 'Focuses the input on mount.' },
|
|
38
|
+
autocapitalize: { type: 'string', forwarded: true, description: 'Native autocapitalize hint.' },
|
|
26
39
|
size: size(),
|
|
27
40
|
},
|
|
28
|
-
events: change('Called with the new string on every keystroke.'),
|
|
29
|
-
|
|
41
|
+
events: { ...change('Called with the new string on every keystroke.'), blur: 'Native blur of the input (Vue @blur, React onBlur).', focus: 'Native focus of the input.', keydown: 'Native keydown of the input; modifiers like @keydown.enter work in Vue.', input: 'Native input event (after change), receives the Event.' },
|
|
42
|
+
slots: { prefix: 'Content before the input (icon or text). Replaces the icon prop. React: prefix prop.', suffix: 'Content after the input (button, unit). Same as the suffix prop. React: suffix prop.' },
|
|
43
|
+
notes: ['Wrapped in a label, so clicking the label focuses the input.', 'Native attributes (name, autocomplete, min, max, step, maxlength, inputmode, pattern ...) and the blur, focus, keydown, keyup and paste listeners are forwarded to the inner input. id, class, style, aria-* and data-* land on the root label.'],
|
|
30
44
|
},
|
|
31
45
|
Textarea: {
|
|
32
46
|
description: 'Multi-line text input. Use for comments, descriptions and other long text.',
|
|
@@ -35,9 +49,15 @@ export default {
|
|
|
35
49
|
label, placeholder: placeholder(), hint, error, disabled,
|
|
36
50
|
rows: { type: 'number', default: 4, description: 'Visible text rows.' },
|
|
37
51
|
maxlength: { type: 'number', description: 'Maximum number of characters.' },
|
|
52
|
+
readonly: { type: 'boolean', default: false, description: 'Makes the textarea read-only.' },
|
|
53
|
+
required: { type: 'boolean', default: false, forwarded: true, description: 'Adds a red asterisk to the label and the native required attribute.' },
|
|
54
|
+
autoResize: { type: 'boolean', default: false, description: 'Grows with the content (CSS field-sizing: content, Chromium and Safari).' },
|
|
55
|
+
resize: { type: 'boolean', default: true, description: 'false removes the manual resize handle.' },
|
|
56
|
+
showCount: { type: 'boolean', default: false, description: 'Shows the character count under the field, as "n / maxlength" when maxlength is set.' },
|
|
57
|
+
name: { type: 'string', forwarded: true, description: 'Native name attribute.' },
|
|
38
58
|
size: size(),
|
|
39
59
|
},
|
|
40
|
-
events: change('Called with the new string on every keystroke.'),
|
|
60
|
+
events: { ...change('Called with the new string on every keystroke.'), input: 'Native input event (after change), receives the Event.', blur: 'Native blur.', focus: 'Native focus.' },
|
|
41
61
|
},
|
|
42
62
|
NumberInput: {
|
|
43
63
|
description: 'Numeric stepper with minus and plus buttons and a typed value. Use for quantities, guest counts and small integers.',
|
|
@@ -137,6 +157,9 @@ export default {
|
|
|
137
157
|
modelValue: { type: 'string | number', model: true, description: 'Value of the selected option.' },
|
|
138
158
|
options: { type: optShape, required: true, default: '[]', description: 'Available options. An option with disabled: true is dimmed and cannot be chosen.' },
|
|
139
159
|
label, placeholder: placeholder('Shown when nothing is selected. Defaults to "—".'),
|
|
160
|
+
hint, error,
|
|
161
|
+
required: { type: 'boolean', default: false, description: 'Adds a red asterisk to the label. Does not validate.' },
|
|
162
|
+
name: { type: 'string', description: 'Renders a hidden input with this name and the selected value, so the control posts with a native form.' },
|
|
140
163
|
disabled,
|
|
141
164
|
size: size(),
|
|
142
165
|
},
|
|
@@ -175,6 +198,8 @@ export default {
|
|
|
175
198
|
label, hint, error,
|
|
176
199
|
placeholder: placeholder('Defaults to "Search…".'),
|
|
177
200
|
emptyText: { type: 'string', default: '—', description: 'Shown when no option matches the query.' },
|
|
201
|
+
clearable: { type: 'boolean', default: false, description: 'Shows a clear (x) button while a value is selected; it calls change with null.' },
|
|
202
|
+
filter: { type: '(option, query) => boolean', description: 'Custom matcher. Default is a case-insensitive substring match on label.' },
|
|
178
203
|
disabled,
|
|
179
204
|
size: size(),
|
|
180
205
|
},
|
|
@@ -212,6 +237,9 @@ export default {
|
|
|
212
237
|
props: {
|
|
213
238
|
modelValue: { type: 'boolean', model: true, default: false, description: 'Checked state.' },
|
|
214
239
|
label: { type: 'string', description: 'Text next to the box.' },
|
|
240
|
+
hint: { type: 'string', description: 'Second line under the label.' },
|
|
241
|
+
error: { type: 'string', description: 'Error message under the label (announced with role="alert"); a boolean or empty string shows nothing.' },
|
|
242
|
+
indeterminate: { type: 'boolean', default: false, description: 'Shows a dash instead of a check (aria-checked="mixed"). Clicking calls change with true.' },
|
|
215
243
|
disabled,
|
|
216
244
|
size: size('Box and label size.'),
|
|
217
245
|
},
|
|
@@ -223,6 +251,9 @@ export default {
|
|
|
223
251
|
props: {
|
|
224
252
|
modelValue: { type: 'boolean', model: true, default: false, description: 'On state.' },
|
|
225
253
|
label: { type: 'string', description: 'Text next to the toggle.' },
|
|
254
|
+
hint: { type: 'string', description: 'Second line under the label.' },
|
|
255
|
+
error: { type: 'string', description: 'Error message under the label (announced with role="alert").' },
|
|
256
|
+
labelLeft: { type: 'boolean', default: false, description: 'Places the label before the toggle instead of after it.' },
|
|
226
257
|
disabled,
|
|
227
258
|
size: size('Track and label size.'),
|
|
228
259
|
},
|
|
@@ -234,7 +265,8 @@ export default {
|
|
|
234
265
|
props: {
|
|
235
266
|
modelValue: { type: 'string | number', model: true, description: 'Value of the selected option.' },
|
|
236
267
|
options: { type: '{ value: string | number; label: string; hint?: string; disabled?: boolean }[]', default: '[]', description: 'Radio options. hint is a second line under the label; disabled: true disables that option only.' },
|
|
237
|
-
label,
|
|
268
|
+
label, hint, error,
|
|
269
|
+
orientation: { type: "'vertical' | 'horizontal'", values: ['vertical', 'horizontal'], default: 'vertical', description: 'horizontal lays the options out in a wrapping row under the label.' },
|
|
238
270
|
disabled,
|
|
239
271
|
size: size(),
|
|
240
272
|
},
|
|
@@ -321,6 +353,9 @@ export default {
|
|
|
321
353
|
modelValue: { type: 'Date | string | number', model: true, description: 'Selected date: a Date, ISO string or timestamp. Also sets the visible month. A date-only string like "2026-10-21" is read as a local date, so the day does not shift with the time zone.' },
|
|
322
354
|
locale: { type: 'string', default: 'en-GB', description: 'BCP 47 locale for month and weekday names.' },
|
|
323
355
|
weekStartsOn: { type: 'number', default: 1, description: 'First day of the week: 0 = Sunday, 1 = Monday.' },
|
|
356
|
+
min: { type: 'Date | string | number', description: 'Earliest selectable day; earlier days are disabled.' },
|
|
357
|
+
max: { type: 'Date | string | number', description: 'Latest selectable day; later days are disabled.' },
|
|
358
|
+
disabledDate: { type: '(date: Date) => boolean', description: 'Return true to disable a specific day.' },
|
|
324
359
|
events: { type: 'Record<string, string>', default: '{}', description: 'Marked days: keys are YYYY-MM-DD, values are the tooltip text.' },
|
|
325
360
|
size: size('Day cell and header size.'),
|
|
326
361
|
},
|
|
@@ -345,6 +380,11 @@ export default {
|
|
|
345
380
|
modelValue: { type: 'string | Date | number', model: true, description: 'Selected date (ISO string recommended, e.g. 2026-10-21).' },
|
|
346
381
|
locale: { type: 'string', default: 'en-GB', description: 'BCP 47 locale used for the trigger text and the calendar.' },
|
|
347
382
|
weekStartsOn: { type: 'number', default: 1, description: 'First day of the week: 0 = Sunday, 1 = Monday.' },
|
|
383
|
+
clearable: { type: 'boolean', default: false, description: 'Shows a clear (x) button while a date is set; it calls change with null.' },
|
|
384
|
+
min: { type: 'Date | string | number', forwarded: true, description: 'Earliest selectable day, forwarded to the calendar.' },
|
|
385
|
+
max: { type: 'Date | string | number', forwarded: true, description: 'Latest selectable day, forwarded to the calendar.' },
|
|
386
|
+
disabledDate: { type: '(date: Date) => boolean', forwarded: true, description: 'Return true to disable a specific day, forwarded to the calendar.' },
|
|
387
|
+
required: { type: 'boolean', default: false, forwarded: true, description: 'Adds a red asterisk to the label. Does not validate.' },
|
|
348
388
|
label, hint, error,
|
|
349
389
|
placeholder: placeholder('Shown when no date is set. Defaults to "Select date".'),
|
|
350
390
|
disabled,
|