@ziyarago/ui 0.12.0 → 0.13.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 CHANGED
@@ -1,3 +1,9 @@
1
+ ## [0.13.0](https://github.com/ziyarago/ui/compare/v0.12.0...v0.13.0) (2026-10-21)
2
+
3
+ ### Features
4
+
5
+ * accessibility audit with axe (0 violations), keyboard navigation for tabs, selects and menus, readable tooltips ([95a25e1](https://github.com/ziyarago/ui/commit/95a25e1d763f2c63ee788986943aeb3abbb669ae))
6
+
1
7
  ## [0.12.0](https://github.com/ziyarago/ui/compare/v0.11.0...v0.12.0) (2026-10-21)
2
8
 
3
9
  ### Features
@@ -2,7 +2,7 @@
2
2
 
3
3
  > ZiyaraGo UI — glass design system for Umrah and Hajj products. OKLCH tokens, two palettes, dark and light, RTL, Thmanyah. One framework-neutral core with Vue 3, Nuxt and React adapters, 208 components and 40 page templates.
4
4
 
5
- Version 0.12.0. Docs: https://ziyara.vip . Manifest (machine readable): https://ziyara.vip/manifest.json
5
+ Version 0.13.0. Docs: https://ziyara.vip . Manifest (machine readable): https://ziyara.vip/manifest.json
6
6
 
7
7
  ## Setup
8
8
  - import { UiButton } from '@ziyarago/ui/vue'
package/dist/llms.txt CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > ZiyaraGo UI — glass design system for Umrah and Hajj products. OKLCH tokens, two palettes, dark and light, RTL, Thmanyah. One framework-neutral core with Vue 3, Nuxt and React adapters, 208 components and 40 page templates.
4
4
 
5
- Version 0.12.0. Docs: https://ziyara.vip . Manifest (machine readable): https://ziyara.vip/manifest.json
5
+ Version 0.13.0. Docs: https://ziyara.vip . Manifest (machine readable): https://ziyara.vip/manifest.json
6
6
 
7
7
  ## Setup
8
8
  - import { UiButton } from '@ziyarago/ui/vue'
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziyarago/ui",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "homepage": "https://ziyara.vip",
5
5
  "imports": {
6
6
  "vue": "import { UiButton } from '@ziyarago/ui/vue'",
package/dist/style.css CHANGED
@@ -43,9 +43,9 @@
43
43
  --zg-s1:oklch(1 0 0 / .055);--zg-s2:oklch(1 0 0 / .085);--zg-s3:oklch(1 0 0 / .13);
44
44
  --zg-glass:oklch(0.235 0.006 40 / .92);
45
45
  --zg-stroke:oklch(1 0 0 / .08);
46
- --zg-text-1:oklch(0.97 0.004 70);--zg-text-2:oklch(0.82 0.008 70);--zg-text-3:oklch(0.66 0.01 70);
46
+ --zg-text-1:oklch(0.97 0.004 70);--zg-text-2:oklch(0.82 0.008 70);--zg-text-3:oklch(0.74 0.01 70);
47
47
  --zg-inv-bg:oklch(0.96 0.004 70);--zg-inv-fg:oklch(0.2 0.01 60);
48
- --zg-green:oklch(0.76 0.15 150);--zg-amber:oklch(0.8 0.14 80);--zg-red:oklch(0.7 0.18 25);
48
+ --zg-green:oklch(0.76 0.15 150);--zg-amber:oklch(0.8 0.14 80);--zg-red:oklch(0.7 0.18 25);--zg-red-text:oklch(0.86 0.1 25);
49
49
  --zg-rim:inset 0 1px 0 oklch(1 0 0 / .16),inset 0 0 0 1px oklch(1 0 0 / .07),inset 0 -1px 0 oklch(0 0 0 / .25);
50
50
  --zg-shadow-1:0 8px 24px oklch(0 0 0 / .28);--zg-shadow-2:0 24px 64px oklch(0 0 0 / .5);
51
51
  }
@@ -59,7 +59,7 @@
59
59
  --zg-stroke:oklch(0.3 0.02 60 / .15);
60
60
  --zg-text-1:oklch(0.2 0.01 60);--zg-text-2:oklch(0.36 0.012 60);--zg-text-3:oklch(0.47 0.012 60);
61
61
  --zg-inv-bg:oklch(0.22 0.01 60);--zg-inv-fg:oklch(0.97 0.004 70);
62
- --zg-green:oklch(0.5 0.14 150);--zg-amber:oklch(0.55 0.13 70);--zg-red:oklch(0.52 0.19 25);
62
+ --zg-green:oklch(0.5 0.14 150);--zg-amber:oklch(0.55 0.13 70);--zg-red:oklch(0.52 0.19 25);--zg-red-text:oklch(0.44 0.17 25);
63
63
  --zg-rim:inset 0 1px 0 oklch(1 0 0 / .95),inset 0 0 0 1px oklch(1 0 0 / .45),0 0 0 1px oklch(0.3 0.02 60 / .13);
64
64
  --zg-shadow-1:0 8px 24px oklch(0.4 0.03 60 / .17);--zg-shadow-2:0 24px 64px oklch(0.4 0.03 60 / .22);
65
65
  }
@@ -105,7 +105,7 @@
105
105
  .zg-btn--primary{background:var(--zg-brand);color:var(--zg-on-brand);box-shadow:inset 0 1px 0 oklch(1 0 0 / .3),0 8px 24px var(--zg-brand-glow)}.zg-btn--primary:hover{background:var(--zg-brand-hover)}
106
106
  .zg-btn--inverse{background:var(--zg-inv-bg);color:var(--zg-inv-fg)}.zg-btn--inverse:hover{background:var(--zg-inv-bg);filter:brightness(1.1)}
107
107
  .zg-btn--ghost{background:transparent;box-shadow:none}.zg-btn--ghost:hover{background:var(--zg-s2)}
108
- .zg-btn--danger{background:color-mix(in oklch,var(--zg-red) 18%,transparent);color:var(--zg-red)}
108
+ .zg-btn--danger{background:color-mix(in oklch,var(--zg-red) 18%,transparent);color:var(--zg-red-text)}
109
109
  .zg-btn--xs{--h:var(--zg-h-xs);font-size:12px}.zg-btn--sm{--h:var(--zg-h-sm);font-size:13px}.zg-btn--lg{--h:var(--zg-h-lg);font-size:15px}.zg-btn--xl{--h:var(--zg-h-xl);font-size:16px}
110
110
  .zg-btn--block{display:flex;width:100%}
111
111
  .zg-spin{width:1em;height:1em;border-radius:50%;border:2px solid currentColor;border-inline-end-color:transparent;animation:zg-spin .7s linear infinite}
@@ -319,7 +319,7 @@
319
319
  .zg-rc{display:grid;grid-template-columns:56px repeat(var(--d),minmax(0,1fr));gap:2px;align-items:center;direction:ltr;font-size:12px}.zg-rc__h{text-align:center;color:var(--zg-text-3)}.zg-rc__c{height:28px;border-radius:6px;background:var(--zg-s2)}.zg-rc__c[data-b]{background:var(--zg-brand-soft);border-radius:0}.zg-rc__c[data-b="start"]{background:var(--zg-brand);border-radius:999px 0 0 999px}
320
320
  .zg-occ{display:flex;align-items:flex-end;gap:8px;height:160px}.zg-occ__c{flex:1;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;gap:6px}.zg-occ__c i{width:100%;border-radius:10px 10px 4px 4px}
321
321
 
322
- .zg-kpi--brand .zg-delta{color:inherit;opacity:.85}.zg-kpi--brand .zg-delta::before{opacity:.9}
322
+ .zg-kpi--brand .zg-kpi__label,.zg-kpi--brand .zg-kpi__sub{opacity:.9}.zg-kpi--brand .zg-delta{color:inherit;opacity:1}.zg-kpi--brand .zg-delta::before{opacity:.9}
323
323
 
324
324
 
325
325
  /* ./components3.css */
@@ -567,6 +567,9 @@
567
567
  .zg-qb__txt b{font-size:calc(22px*var(--dz,1));font-weight:600;color:var(--zg-text-1)}.zg-qb__txt small{color:var(--zg-text-3)}
568
568
  /* Numbers: tabular figures with a plain zero (no slashed or dotted zero) in KPI values, tables and code cells */
569
569
  .zg-num,.zg-mono,.zg-kpi__value,.zg-table .num{font-feature-settings:"zero" 0,"tnum" 1}
570
+ .zg-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}
571
+ .zg-select__opt:focus-visible{outline:3px solid var(--zg-brand-soft);outline-offset:-3px;background:var(--zg-s2)}
572
+ .zg-seg button:focus-visible,.zg-tabs button:focus-visible,.zg-code-b pre:focus-visible{outline:3px solid var(--zg-brand-soft);outline-offset:2px}
570
573
 
571
574
 
572
575
  /* ./sizes/typography.css */
package/dist/tokens.css CHANGED
@@ -12,9 +12,9 @@
12
12
  --zg-s1:oklch(1 0 0 / .055);--zg-s2:oklch(1 0 0 / .085);--zg-s3:oklch(1 0 0 / .13);
13
13
  --zg-glass:oklch(0.235 0.006 40 / .92);
14
14
  --zg-stroke:oklch(1 0 0 / .08);
15
- --zg-text-1:oklch(0.97 0.004 70);--zg-text-2:oklch(0.82 0.008 70);--zg-text-3:oklch(0.66 0.01 70);
15
+ --zg-text-1:oklch(0.97 0.004 70);--zg-text-2:oklch(0.82 0.008 70);--zg-text-3:oklch(0.74 0.01 70);
16
16
  --zg-inv-bg:oklch(0.96 0.004 70);--zg-inv-fg:oklch(0.2 0.01 60);
17
- --zg-green:oklch(0.76 0.15 150);--zg-amber:oklch(0.8 0.14 80);--zg-red:oklch(0.7 0.18 25);
17
+ --zg-green:oklch(0.76 0.15 150);--zg-amber:oklch(0.8 0.14 80);--zg-red:oklch(0.7 0.18 25);--zg-red-text:oklch(0.86 0.1 25);
18
18
  --zg-rim:inset 0 1px 0 oklch(1 0 0 / .16),inset 0 0 0 1px oklch(1 0 0 / .07),inset 0 -1px 0 oklch(0 0 0 / .25);
19
19
  --zg-shadow-1:0 8px 24px oklch(0 0 0 / .28);--zg-shadow-2:0 24px 64px oklch(0 0 0 / .5);
20
20
  }
@@ -28,7 +28,7 @@
28
28
  --zg-stroke:oklch(0.3 0.02 60 / .15);
29
29
  --zg-text-1:oklch(0.2 0.01 60);--zg-text-2:oklch(0.36 0.012 60);--zg-text-3:oklch(0.47 0.012 60);
30
30
  --zg-inv-bg:oklch(0.22 0.01 60);--zg-inv-fg:oklch(0.97 0.004 70);
31
- --zg-green:oklch(0.5 0.14 150);--zg-amber:oklch(0.55 0.13 70);--zg-red:oklch(0.52 0.19 25);
31
+ --zg-green:oklch(0.5 0.14 150);--zg-amber:oklch(0.55 0.13 70);--zg-red:oklch(0.52 0.19 25);--zg-red-text:oklch(0.44 0.17 25);
32
32
  --zg-rim:inset 0 1px 0 oklch(1 0 0 / .95),inset 0 0 0 1px oklch(1 0 0 / .45),0 0 0 1px oklch(0.3 0.02 60 / .13);
33
33
  --zg-shadow-1:0 8px 24px oklch(0.4 0.03 60 / .17);--zg-shadow-2:0 24px 64px oklch(0.4 0.03 60 / .22);
34
34
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziyarago/ui",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "ZiyaraGo UI — glass design system for Umrah and Hajj products. OKLCH tokens, two palettes, dark and light, RTL, Thmanyah. One framework-neutral core with Vue 3, Nuxt and React adapters, 208 components and 40 page templates.",
5
5
  "keywords": [
6
6
  "ui",
@@ -115,7 +115,8 @@
115
115
  "test:browser": "node tests/browser/run.mjs && node tests/browser/interactions.mjs",
116
116
  "prepack": "npm run build && npm run check",
117
117
  "pack:dry": "npm pack --dry-run",
118
- "test:types": "node scripts/typecheck-fixtures.mjs"
118
+ "test:types": "node scripts/typecheck-fixtures.mjs",
119
+ "a11y": "node scripts/a11y.mjs"
119
120
  },
120
121
  "engines": {
121
122
  "node": ">=18"
@@ -123,8 +124,8 @@
123
124
  "peerDependencies": {
124
125
  "@nuxt/kit": ">=3.0.0",
125
126
  "react": ">=18",
126
- "vue": ">=3.5",
127
- "react-dom": ">=18"
127
+ "react-dom": ">=18",
128
+ "vue": ">=3.5"
128
129
  },
129
130
  "peerDependenciesMeta": {
130
131
  "vue": {
@@ -152,6 +153,7 @@
152
153
  "@types/react": "^19.3.0",
153
154
  "@vitejs/plugin-react": "^4.7.0",
154
155
  "@vitejs/plugin-vue": "^5.2.4",
156
+ "axe-core": "^4.14.0",
155
157
  "conventional-changelog-conventionalcommits": "^9.3.1",
156
158
  "esbuild": "^0.28.2",
157
159
  "puppeteer-core": "^23.11.1",
@@ -3,6 +3,7 @@
3
3
  // h(tag, props, ...children) is supplied by the Vue / React adapter. No hooks inside.
4
4
  import { trap } from './dialog.js';
5
5
  import { t } from '../i18n/index.js';
6
+ import { rove, tabIdx, listKeys, openKeys, focusIn, toTrigger, trigger } from './keys.js';
6
7
  export const cx = (...a) => a.filter(Boolean).join(' ');
7
8
  // Copies the listed props that are set; used to forward native attributes and listeners to the real <input>/<textarea>.
8
9
  export const pick = (p, keys) => { const o = {}; for (const k of keys) if (p[k] !== undefined) o[k] = p[k]; return o; };
@@ -27,7 +28,7 @@ const ddSync = () => {
27
28
  };
28
29
  export const ddRef = (close, o = {}) => el => {
29
30
  if (typeof document === 'undefined') return;
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(); }
31
+ 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' }); if (o.focus) focusIn(el); } ddSync(); }
31
32
  else Promise.resolve().then(ddPrune);
32
33
  };
33
34
  export const dzp = (p, b) => Math.round(b * (DZ[p.size] || 1));
@@ -66,16 +67,16 @@ export const components = {
66
67
  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
68
  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
69
  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))) },
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))) },
70
+ 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, onKeydown: rove('button') }, ...(p.items || []).map((it, i, all) => h('button', { key: it.value, type: 'button', tabIndex: tabIdx(all, x => x.value === p.modelValue)(i), 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))) },
71
+ Tabs: { render: ({ h, props: p }) => h('div', { class: 'zg-tabs', role: 'tablist', onKeydown: rove('[role=tab]') }, ...(p.items || []).map((it, i, all) => h('button', { key: it.value, role: 'tab', type: 'button', tabIndex: tabIdx(all, x => x.value === p.modelValue)(i), disabled: it.disabled, 'aria-selected': String(it.value === p.modelValue), onClick: () => p.onChange && p.onChange(it.value) }, it.label))) },
71
72
  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))); } },
72
73
  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
74
  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),
74
75
  p.error ? h('span', { class: 'zg-hint zg-hint--error', role: 'alert' }, p.error) : p.hint ? h('span', { class: 'zg-hint' }, p.hint) : null) },
75
76
  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;
76
77
  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),
78
+ 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 }), onKeydown: openKeys(open, () => set({ open: true })) }, h('span', null, cur ? cur.label : p.placeholder || '—'), icon(h, open ? 'chevron-up' : 'chevron-down', 16)),
79
+ open ? h('div', { class: 'zg-select__list', role: 'listbox', 'aria-label': p.label || p.placeholder, 'data-zg-dd': '', ref: ddRef(() => set({ open: false }), { focus: true }), onKeydown: listKeys('[role=option]', { onTab: e => { const t = trigger(e.currentTarget); t && t.focus(); set({ open: false }); } }) }, ...(p.options || []).map((o, i, all) => h('button', { key: o.value, type: 'button', role: 'option', tabIndex: tabIdx(all, x => x.value === p.modelValue)(i), class: 'zg-select__opt', disabled: o.disabled, 'aria-disabled': o.disabled ? 'true' : undefined, 'aria-selected': String(o.value === p.modelValue), onClick: e => { toTrigger(e); set({ open: false }); p.onChange && p.onChange(o.value); } }, o.label, o.value === p.modelValue ? icon(h, 'check', 16) : null))) : null),
79
80
  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); } },
80
81
  CodeInput: { render: ({ h, props: p }) => { const n = p.length || 6, v = p.modelValue || '';
81
82
  return h('div', { class: 'zg-code', style: { '--n': n } }, ...Array.from({ length: n }, (_, i) => h('span', { key: i, class: cx('zg-code__cell', i === v.length && 'zg-code__cell--on') }, v[i] || '')),
@@ -95,7 +96,7 @@ export const components = {
95
96
  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
97
  ...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
98
  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))); } },
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)) + '%' } }))) },
99
+ 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-label': p.label || t('Progress', 'label'), '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)) + '%' } }))) },
99
100
  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)';
100
101
  const r = (s - th) / 2 - (ticks ? 12 : 0), cir = 2 * Math.PI * r, tk = [];
101
102
  if (ticks) for (let i = 0; i < 60; i++) { const an = i / 60 * 2 * Math.PI, R = s / 2 - 2, L = i % 5 ? 4 : 8; tk.push(h('line', { key: i, x1: +(s / 2 + Math.cos(an) * R).toFixed(2), y1: +(s / 2 + Math.sin(an) * R).toFixed(2), x2: +(s / 2 + Math.cos(an) * (R - L)).toFixed(2), y2: +(s / 2 + Math.sin(an) * (R - L)).toFixed(2), stroke: i / 60 <= v ? col : 'var(--zg-s3)', 'stroke-width': i % 5 ? 1 : 1.6, 'stroke-linecap': 'round' })); }
@@ -106,7 +107,7 @@ export const components = {
106
107
  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) },
107
108
  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
109
  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
+ return h('div', { class: 'zg-table-wrap', tabIndex: 0 }, h('table', { class: 'zg-table' }, h('thead', null, h('tr', null, p.onSelect ? h('th', { style: { width: '44px' } }, h('button', { type: 'button', role: 'checkbox', class: 'zg-check', 'aria-checked': String(!!all), 'aria-label': (p.labels && p.labels.selectAll) || t('DataTable', 'selectAll'), onClick: () => p.onSelect(all ? [] : rows.map(r => r.id)) }, h('span', { class: 'zg-check__box' }, all ? icon(h, 'check', 14) : null))) : null, ...cols.map(th))),
110
111
  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 || '—')))])))); } },
111
112
  KanbanColumn: { render: ({ h, props: p, children }) => h('div', { class: 'zg-kcol zg-dm' }, h('div', { class: 'zg-kcol__head' }, p.title, h('span', { class: 'zg-badge' }, p.count ?? children.length)), ...children) },
112
113
  KanbanCard: { render: ({ h, props: p, children }) => h('div', { class: 'zg-kcard zg-dm' }, h('b', { style: { fontSize: dzf(14) } }, p.title), p.sub ? h('span', { class: 'zg-hint' }, p.sub) : null, ...children, p.tag ? h('span', { class: cx('zg-badge', p.tone && 'zg-badge--' + p.tone), style: { alignSelf: 'flex-start' } }, p.tag) : null) },
@@ -1,6 +1,11 @@
1
1
  // @ziyarago/ui · components, part 2 — Forms, Surfaces, Actions, Data, Navigation, Layout, CRM, Hotels.
2
2
  import { icon, cx, fmt, dzp, dzf, dzb, ddRef, pick, INPUT_ATTRS } from './components.js';
3
3
  import { trap } from './dialog.js';
4
+ import { listKeys, openKeys, tabIdx, toTrigger, trigger } from './keys.js';
5
+ const tipId = s => { let x = 5381; for (const c of String(s)) x = (x * 33 ^ c.charCodeAt(0)) >>> 0; return 'zg-tip-' + x.toString(36); };
6
+ let tipN = 0;
7
+ // Same text twice on one page would repeat the id: the later tooltip gets a unique one (browser only, after mount).
8
+ const tipRef = el => { const s = el && el.lastElementChild; if (!s || !s.id || typeof document === 'undefined') return; const same = document.querySelectorAll('[id="' + s.id + '"]'); if (same.length > 1 && same[0] !== s) { s.id += '-' + (++tipN); el.setAttribute('aria-describedby', s.id); } };
4
9
  import { t, getUiLocale, intlLocale } from '../i18n/index.js';
5
10
  const dloc = p => p.locale || (getUiLocale() === 'en' ? 'en-GB' : intlLocale());
6
11
  const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -53,16 +58,16 @@ export const more = {
53
58
  Gallery: { state: { i: 0 }, render: ({ h, props: p, state, set }) => { const im = arr(p.images).filter(Boolean), n = im.length, i = idx(p, state, n), L = p.labels || {};
54
59
  const to = k => { if (k === i) return; if (p.index === undefined || p.index === null) set({ i: k }); p.onChange && p.onChange(k); };
55
60
  return h('div', { class: 'zg-gal' }, h('div', { class: 'zg-gal__m', style: { backgroundImage: n ? `url(${im[i]})` : undefined }, role: 'img', 'aria-label': p.alt || '' }, n ? h('span', { class: 'zg-badge zg-gal__n' }, `${i + 1} / ${n}`) : null), n ? h('div', { class: 'zg-gal__t' }, ...im.map((u, k) => h('button', { key: k, type: 'button', 'aria-label': (L.photo || t('Gallery', 'photo')) + ' ' + (k + 1), 'aria-current': String(k === i), style: { backgroundImage: `url(${u})` }, onClick: () => to(k) }))) : null); } },
56
- Code: { render: ({ C, h, props: p }) => h('div', { class: 'zg-code-b' }, h('div', { class: 'zg-code-b__h' }, h('span', null, p.title || p.language || 'code'), h(C.CopyButton, { value: p.code, size: 'xs' })), h('pre', { class: 'zg-mono' }, p.code)) },
61
+ Code: { render: ({ C, h, props: p }) => h('div', { class: 'zg-code-b' }, h('div', { class: 'zg-code-b__h' }, h('span', null, p.title || p.language || 'code'), h(C.CopyButton, { value: p.code, size: 'xs' })), h('pre', { class: 'zg-mono', tabIndex: 0 }, p.code)) },
57
62
  PricingCard: { render: ({ C, h, props: p }) => h('div', { class: cx('zg-card', 'zg-glass', p.featured && 'zg-price--on') }, h('div', { class: 'zg-card__head' }, h('h3', { class: 'zg-card__title' }, p.name), p.badge ? h(C.Badge, { tone: 'brand' }, p.badge) : null), h('div', { style: { display: 'flex', alignItems: 'baseline', gap: '6px' } }, h('span', { class: 'zg-num', style: { fontSize: '44px' } }, p.price), h('span', { class: 'zg-hint' }, p.period || '')), h('ul', { class: 'zg-feat' }, ...(p.features || []).map((f, i) => h('li', { key: i }, icon(h, 'check', 16), f))), h(C.Button, { variant: p.featured ? 'primary' : 'secondary', block: true, onClick: p.onSelect }, p.cta || t('PricingCard', 'choose'))) },
58
63
  IntegrationCard: { render: ({ C, h, props: p }) => { const st = ['connected', 'error'].includes(p.status) ? p.status : 'off', tone = { connected: 'success', error: 'danger' }[st], L = { connected: t('IntegrationCard', 'connected'), error: t('IntegrationCard', 'error'), off: t('IntegrationCard', 'off'), manage: t('IntegrationCard', 'manage'), reconnect: t('IntegrationCard', 'reconnect'), link: t('IntegrationCard', 'link'), ...(p.labels || {}) };
59
64
  return h('div', { class: 'zg-card zg-glass' }, h('div', { class: 'zg-card__head' }, h('span', { class: 'zg-tl__dot', style: { margin: '8px 4px 0' } }), h('div', { style: { flex: 1 } }, h('h3', { class: 'zg-card__title' }, p.name), h('div', { class: 'zg-card__sub' }, p.description)), h(C.Badge, { tone, dot: true }, L[st])), h(C.Button, { variant: st === 'connected' ? 'secondary' : 'primary', size: 'sm', onClick: p.onToggle }, st === 'connected' ? L.manage : st === 'error' ? L.reconnect : L.link)); } },
60
65
  IncomingCall: { render: ({ C, h, props: p }) => h('div', { class: 'zg-inc zg-glass zg-dm' }, h(C.Avatar, { name: p.name, size: dzp(p, 48) }), h('div', { style: { flex: 1, minWidth: 0 } }, h('div', { class: 'zg-hint' }, p.label || t('IncomingCall', 'incoming')), h('b', { style: { fontSize: dzf(16) } }, p.name), h('div', { class: 'zg-mono zg-hint' }, p.phone)), h(C.IconButton, { icon: 'phone-off', label: t('IncomingCall', 'decline'), size: p.size ? p.size : undefined, onClick: p.onDecline }), h('button', { type: 'button', class: 'zg-ibtn zg-inc__ok', 'aria-label': t('IncomingCall', 'answer'), onClick: p.onAnswer }, icon(h, 'phone'))) },
61
66
  CallCard: { render: ({ C, h, props: p }) => h('div', { class: 'zg-card zg-glass zg-dm' }, h('div', { class: 'zg-lead__row' }, h(C.Avatar, { name: p.name, size: dzp(p, 36) }), h('div', { style: { flex: 1, minWidth: 0 } }, h('b', { style: { fontSize: dzf(14) } }, p.name), h('div', { class: 'zg-hint zg-mono' }, p.phone)), p.status ? h(C.Badge, { tone: { ...CALL_TONES, ...(p.statusTones || {}) }[String(p.status).toLowerCase()] || (p.statusTones || {})[p.status], dot: true }, p.status) : null), h('div', { class: 'zg-lead__row' }, h(C.IconButton, { icon: 'play', label: (p.labels && p.labels.play) || t('CallCard', 'play'), size: dzb(p), onClick: p.onPlay }), h(C.Progress, { value: num(p.progress) }), h('span', { class: 'zg-mono zg-hint' }, fmt.dur(p.duration, p.durUnits)))) },
62
67
  /* ── Actions ── */
63
- 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) },
64
- 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))) },
65
- 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) },
68
+ Dropdown: { state: { open: false }, render: ({ h, props: p, state, set }) => h('div', { class: 'zg-dd' }, h('button', { type: 'button', class: cx('zg-btn', p.size && 'zg-btn--' + p.size), 'aria-haspopup': 'menu', 'aria-expanded': String(state.open && !p.disabled), disabled: p.disabled, onClick: () => set({ open: !state.open }), onKeydown: openKeys(state.open && !p.disabled, () => set({ open: true })) }, p.icon ? icon(h, p.icon, 16) : null, p.label, icon(h, 'chevron-down', 14)), state.open && !p.disabled ? h('div', { class: 'zg-menu', role: 'menu', 'data-zg-dd': '', ref: ddRef(() => set({ open: false }), { focus: true }), onKeydown: listKeys('[role=menuitem]', { wrap: true, onTab: e => { const t = trigger(e.currentTarget); t && t.focus(); set({ open: false }); } }) }, ...(p.items || []).map((it, i, all) => it.separator ? h('div', { key: i, class: 'zg-menu__sep', role: 'separator' }) : h('button', { key: i, type: 'button', role: 'menuitem', tabIndex: tabIdx(all.map(x => ({ disabled: x.separator || x.disabled })), () => false)(i), disabled: it.disabled, 'aria-disabled': it.disabled ? 'true' : undefined, class: cx('zg-menu__i', it.tone === 'danger' && 'zg-menu__i--danger'), onClick: e => { toTrigger(e); set({ open: false }); it.onClick && it.onClick(it); p.onSelect && p.onSelect(it.value); } }, it.icon ? icon(h, it.icon, 16) : null, it.label, it.shortcut ? h('span', { class: 'zg-kbd' }, it.shortcut) : null))) : null) },
69
+ Menu: { render: ({ h, props: p }) => p.open === false ? null : h('div', { class: 'zg-menu zg-menu--static', role: 'menu', 'data-zg-dd': '', ref: p.open === true && p.onClose ? ddRef(p.onClose, { self: true, focus: true }) : undefined, onKeydown: listKeys('[role=menuitem]', { wrap: true, onTab: () => p.onClose && p.onClose() }) }, ...(p.items || []).map((it, i, all) => it.separator ? h('div', { key: i, class: 'zg-menu__sep', role: 'separator' }) : h('button', { key: i, type: 'button', role: 'menuitem', tabIndex: tabIdx(all.map(x => ({ disabled: x.separator || x.disabled })), () => false)(i), disabled: it.disabled, 'aria-disabled': it.disabled ? 'true' : undefined, class: cx('zg-menu__i', it.tone === 'danger' && 'zg-menu__i--danger'), onClick: () => { it.onClick && it.onClick(it); p.onSelect && p.onSelect(it.value); } }, it.icon ? icon(h, it.icon, 16) : null, it.label, it.shortcut ? h('span', { class: 'zg-kbd' }, it.shortcut) : null))) },
70
+ Tooltip: { render: ({ h, props: p, children }) => h('span', { class: cx('zg-tip', p.placement && p.placement !== 'top' && 'zg-tip--' + p.placement), 'data-tip': p.text, tabIndex: 0, 'aria-describedby': p.text ? tipId(p.text) : undefined, ref: p.text ? tipRef : undefined }, ...children, p.text ? h('span', { id: tipId(p.text), role: 'tooltip', class: 'zg-sr' }, p.text) : null) },
66
71
  HoverCard: { render: ({ h, props: p, children }) => h('span', { class: 'zg-hc', tabIndex: 0 }, h('span', null, p.trigger), h('div', { class: 'zg-hc__c zg-glass' }, ...children)) },
67
72
  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) },
68
73
  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(); };
@@ -102,20 +107,20 @@ export const more = {
102
107
  return h('div', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label) : null, h('div', { class: 'zg-fdrop', role: 'button', tabIndex: 0, 'aria-label': p.title || t('FileUpload', 'title'), 'data-drag': state.drag ? '' : undefined, onClick: pick, onKeydown: e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); pick(e); } }, onDragenter: e => { e.preventDefault(); if (!state.drag) set({ drag: true }); }, onDragover: e => { e.preventDefault(); if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'; if (!state.drag) set({ drag: true }); }, onDragleave: e => { if (!e.currentTarget.contains(e.relatedTarget)) set({ drag: false }); }, onDrop: e => { e.preventDefault(); set({ drag: false }); take([...(e.dataTransfer ? e.dataTransfer.files : [])]); } }, icon(h, p.image ? 'image-up' : 'upload', 24), h('b', null, p.title || t('FileUpload', 'title')), h('span', { class: 'zg-hint' }, p.hint || t('FileUpload', 'hint')), h('input', { type: 'file', multiple: p.multiple, accept: p.accept, tabIndex: -1, 'aria-hidden': 'true', style: { display: 'none' }, onClick: e => e.stopPropagation(), onChange: e => { take([...e.target.files]); e.target.value = ''; } })), p.error ? h('span', { class: 'zg-hint zg-hint--error', role: 'alert' }, p.error) : null); } },
103
108
  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())));
104
109
  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 || t('Combobox', 'placeholder'), 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': t('Combobox', '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)]),
105
- 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)); } },
110
+ state.open && !p.disabled ? h('div', { class: 'zg-select__list', role: 'listbox', 'aria-label': p.label || p.placeholder, 'data-zg-dd': '', ref: ddRef(() => set({ open: false, 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)); } },
106
111
  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]);
107
112
  return h('div', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label) : null, h('div', { class: 'zg-select' }, h('button', { type: 'button', class: 'zg-btn zg-select__trigger', 'aria-haspopup': 'listbox', 'aria-expanded': String(state.open && !p.disabled), disabled: p.disabled, style: { height: 'var(--zg-h-lg)' }, onClick: () => set({ open: !state.open }) }, h('span', null, v.length ? t('MultiSelect', 'selected', { n: v.length }) : (p.placeholder || '—')), icon(h, 'chevron-down', 16)),
108
- state.open && !p.disabled ? h('div', { class: 'zg-select__list', role: 'listbox', 'aria-multiselectable': 'true', 'data-zg-dd': '', ref: ddRef(() => set({ open: false })) }, ...(p.options || []).map(o => h('button', { key: o.value, type: 'button', role: 'option', class: 'zg-select__opt', disabled: o.disabled, 'aria-disabled': o.disabled ? 'true' : undefined, 'aria-selected': String(v.includes(o.value)), onClick: () => tog(o.value) }, h('span', { class: 'zg-check__box', style: { width: '18px', height: '18px', borderRadius: '6px', background: v.includes(o.value) ? 'var(--zg-brand)' : 'transparent' } }, v.includes(o.value) ? icon(h, 'check', 12) : null), h('span', { style: { flex: 1, marginInlineStart: '10px' } }, o.label)))) : null)); } },
113
+ state.open && !p.disabled ? h('div', { class: 'zg-select__list', role: 'listbox', 'aria-label': p.label || p.placeholder, 'aria-multiselectable': 'true', 'data-zg-dd': '', ref: ddRef(() => set({ open: false })) }, ...(p.options || []).map(o => h('button', { key: o.value, type: 'button', role: 'option', class: 'zg-select__opt', disabled: o.disabled, 'aria-disabled': o.disabled ? 'true' : undefined, 'aria-selected': String(v.includes(o.value)), onClick: () => tog(o.value) }, h('span', { class: 'zg-check__box', style: { width: '18px', height: '18px', borderRadius: '6px', background: v.includes(o.value) ? 'var(--zg-brand)' : 'transparent' } }, v.includes(o.value) ? icon(h, 'check', 12) : null), h('span', { style: { flex: 1, marginInlineStart: '10px' } }, o.label)))) : null)); } },
109
114
  GroupedSelect: { state: { open: false }, render: ({ h, props: p, state, set }) => { const all = (p.groups || []).flatMap(g => g.options), cur = all.find(o => o.value === p.modelValue);
110
115
  return h('div', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label) : null, h('div', { class: 'zg-select' }, h('button', { type: 'button', class: 'zg-btn zg-select__trigger', 'aria-haspopup': 'listbox', 'aria-expanded': String(state.open && !p.disabled), disabled: p.disabled, style: { height: 'var(--zg-h-lg)' }, onClick: () => set({ open: !state.open }) }, h('span', null, cur ? cur.label : (p.placeholder || '—')), icon(h, 'chevron-down', 16)),
111
- state.open && !p.disabled ? h('div', { class: 'zg-select__list', role: 'listbox', 'data-zg-dd': '', ref: ddRef(() => set({ open: false })) }, ...(p.groups || []).flatMap(g => [h('div', { key: g.label, class: 'zg-sidenav__group', style: { padding: '8px 14px 2px' } }, g.label), ...g.options.map(o => h('button', { key: o.value, type: 'button', role: 'option', class: 'zg-select__opt', disabled: o.disabled, 'aria-disabled': o.disabled ? 'true' : undefined, 'aria-selected': String(o.value === p.modelValue), onClick: () => { set({ open: false }); p.onChange && p.onChange(o.value); } }, o.label))])) : null)); } },
116
+ state.open && !p.disabled ? h('div', { class: 'zg-select__list', role: 'listbox', 'aria-label': p.label || p.placeholder, 'data-zg-dd': '', ref: ddRef(() => set({ open: false })) }, ...(p.groups || []).flatMap(g => [h('div', { key: g.label, class: 'zg-sidenav__group', style: { padding: '8px 14px 2px' } }, g.label), ...g.options.map(o => h('button', { key: o.value, type: 'button', role: 'option', class: 'zg-select__opt', disabled: o.disabled, 'aria-disabled': o.disabled ? 'true' : undefined, 'aria-selected': String(o.value === p.modelValue), onClick: () => { set({ open: false }); p.onChange && p.onChange(o.value); } }, o.label))])) : null)); } },
112
117
  PhoneInput: { render: ({ C, h, props: p }) => { const CC = p.countries || [{ value: '+966', label: 'SA +966' }, { value: '+998', label: 'UZ +998' }, { value: '+7', label: 'RU +7' }, { value: '+90', label: 'TR +90' }, { value: '+20', label: 'EG +20' }], v = p.modelValue || {};
113
118
  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); } },
114
119
  Calendar: { state: { ym: null }, render: ({ h, props: p, state, set }) => { const s = pd(p.modelValue), b = state.ym ? new Date(state.ym) : (s || new Date()), y = b.getFullYear(), m = b.getMonth(), loc = dloc(p), ws = p.weekStartsOn ?? 1, off = (new Date(y, m, 1).getDay() - ws + 7) % 7, dn = new Date(y, m + 1, 0).getDate(), today = new Date();
115
120
  const wd = Array.from({ length: 7 }, (_, i) => new Intl.DateTimeFormat(loc, { weekday: 'narrow' }).format(new Date(2023, 0, 1 + ((i + ws) % 7))));
116
121
  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))); };
117
122
  return h('div', { class: 'zg-cal' }, h('div', { class: 'zg-cal__h' }, h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': t('Calendar', 'prev'), onClick: () => set({ ym: new Date(y, m - 1, 1).getTime() }) }, icon(h, 'chevron-left', 16)), h('b', null, new Intl.DateTimeFormat(loc, { month: 'long', year: 'numeric' }).format(b)), h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': t('Calendar', 'next'), onClick: () => set({ ym: new Date(y, m + 1, 1).getTime() }) }, icon(h, 'chevron-right', 16))),
118
- 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); }))); } },
123
+ 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); }))); } },
119
124
  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(dloc(p), { dateStyle: 'medium' }).format(pd(p.modelValue) || new Date()) : (p.placeholder || t('DatePicker', 'placeholder'))), 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': t('DatePicker', '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))),
120
125
  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)) },
121
126
  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 }));
@@ -1,6 +1,7 @@
1
1
  // @ziyarago/ui · components, part 3 — card patterns, tonal variants, shells for auth/profile/status pages.
2
2
  import { icon, cx, fmt, ddRef } from './components.js';
3
3
  import { t } from '../i18n/index.js';
4
+ import { rove, tabIdx, listKeys, openKeys, toTrigger, trigger } from './keys.js';
4
5
  const TONES = ['brand', 'sky', 'success', 'warning', 'danger', 'neutral'];
5
6
  const tc = t => 'zg-t-' + (TONES.includes(t) ? t : 'brand');
6
7
  const delta = (h, d) => d ? h('span', { class: cx('zg-delta', String(d).trim().startsWith('-') ? 'zg-delta--down' : 'zg-delta--up') }, (String(d).trim().startsWith('-') ? '▼ ' : '▲ ') + String(d).replace(/^[+-]/, '')) : null;
@@ -21,8 +22,8 @@ export const more3 = {
21
22
  return h('div', { class: 'zg-field', style: { gap: '12px' } }, h('div', { class: 'zg-segbar' }, ...s.map((x, i) => h('i', { key: i, title: x.label, style: { width: (x.value / t * 100) + '%', background: x.color || col[i % 5] } }))), h('div', { class: 'zg-chips' }, ...s.map((x, i) => h('span', { key: i, class: 'zg-hint', style: { display: 'inline-flex', alignItems: 'center', gap: '6px' } }, h('i', { style: { width: '8px', height: '8px', borderRadius: '50%', background: x.color || col[i % 5] } }), x.label + ' · ' + x.value)))); } },
22
23
  HeroBanner: { render: ({ C, h, props: p, children }) => h('section', { class: 'zg-hero zg-glass' }, h('div', { class: 'zg-hero__t' }, h('b', { style: { fontSize: '20px', fontWeight: 500 } }, p.title), p.text ? h('span', { style: { color: 'var(--zg-text-2)', lineHeight: 1.5 } }, p.text) : null, p.value ? h('span', { class: 'zg-num', style: { fontSize: '36px', fontWeight: 300 } }, p.value) : null, ...children, p.cta ? h(C.Button, { variant: 'primary', size: 'sm', onClick: p.onCta }, p.cta) : null), p.image ? h('span', { class: 'zg-hero__img', style: { backgroundImage: `url(${p.image})` } }) : null) },
23
24
  TabTiles: { render: ({ h, props: p }) => h('div', { class: 'zg-tt' }, ...(p.items || []).map(it => h('button', { key: it.value, type: 'button', class: 'zg-tt__i', 'aria-pressed': String(it.value === p.value), onClick: () => p.onChange && p.onChange(it.value) }, icon(h, it.icon || 'circle', 20), h('span', null, it.label))), p.addable ? h('button', { type: 'button', class: 'zg-tt__i zg-tt__add', 'aria-label': t('TabTiles', 'add'), onClick: p.onAdd }, icon(h, 'plus', 20)) : null) },
24
- PillTabs: { render: ({ h, props: p }) => h('div', { class: cx('zg-pt', p.vertical && 'zg-pt--v'), role: 'tablist' }, ...(p.items || []).map(it => h('button', { key: it.value, type: 'button', role: 'tab', 'aria-selected': String(it.value === p.value), onClick: () => p.onChange && p.onChange(it.value) }, it.icon ? icon(h, it.icon, 16) : null, it.label))) },
25
- PillSelect: { state: { open: false }, render: ({ h, props: p, state, set }) => { const opts = p.options || [], v = p.modelValue !== undefined ? p.modelValue : p.value, open = state.open && !p.disabled; return h('div', { class: 'zg-dd' }, h('button', { type: 'button', class: 'zg-btn zg-btn--tonal zg-btn--sm', 'aria-haspopup': 'listbox', 'aria-expanded': String(open), disabled: p.disabled, onClick: () => set({ open: !state.open }) }, (opts.find(o => o.value === v) || {}).label || p.placeholder, icon(h, 'chevron-down', 14)), open ? h('div', { class: 'zg-menu', role: 'listbox', 'data-zg-dd': '', ref: ddRef(() => set({ open: false })) }, ...opts.map(o => h('button', { key: o.value, type: 'button', role: 'option', class: 'zg-menu__i', disabled: o.disabled, 'aria-disabled': o.disabled ? 'true' : undefined, 'aria-selected': String(o.value === v), onClick: () => { set({ open: false }); p.onChange && p.onChange(o.value); } }, o.label))) : null); } },
25
+ PillTabs: { render: ({ h, props: p }) => h('div', { class: cx('zg-pt', p.vertical && 'zg-pt--v'), role: 'tablist', 'aria-orientation': p.vertical ? 'vertical' : undefined, onKeydown: rove('[role=tab]', { vertical: !!p.vertical }) }, ...(p.items || []).map((it, i, all) => h('button', { key: it.value, type: 'button', role: 'tab', tabIndex: tabIdx(all, x => x.value === p.value)(i), disabled: it.disabled, 'aria-selected': String(it.value === p.value), onClick: () => p.onChange && p.onChange(it.value) }, it.icon ? icon(h, it.icon, 16) : null, it.label))) },
26
+ PillSelect: { state: { open: false }, render: ({ h, props: p, state, set }) => { const opts = p.options || [], v = p.modelValue !== undefined ? p.modelValue : p.value, open = state.open && !p.disabled; return h('div', { class: 'zg-dd' }, h('button', { type: 'button', class: 'zg-btn zg-btn--tonal zg-btn--sm', 'aria-haspopup': 'listbox', 'aria-expanded': String(open), disabled: p.disabled, onClick: () => set({ open: !state.open }), onKeydown: openKeys(open, () => set({ open: true })) }, (opts.find(o => o.value === v) || {}).label || p.placeholder, icon(h, 'chevron-down', 14)), open ? h('div', { class: 'zg-menu', role: 'listbox', 'aria-label': p.placeholder || (opts.find(o => o.value === v) || {}).label, 'data-zg-dd': '', ref: ddRef(() => set({ open: false }), { focus: true }), onKeydown: listKeys('[role=option]', { onTab: e => { const t = trigger(e.currentTarget); t && t.focus(); set({ open: false }); } }) }, ...opts.map((o, i) => h('button', { key: o.value, type: 'button', role: 'option', tabIndex: tabIdx(opts, x => x.value === v)(i), class: 'zg-menu__i', disabled: o.disabled, 'aria-disabled': o.disabled ? 'true' : undefined, 'aria-selected': String(o.value === v), onClick: e => { toTrigger(e); set({ open: false }); p.onChange && p.onChange(o.value); } }, o.label))) : null); } },
26
27
  ChartCard: { state: { open: false }, render: ({ h, props: p, state, set, children }) => h('section', { class: 'zg-card zg-glass' }, h('div', { class: 'zg-card__head' }, h('div', { style: { flex: 1, minWidth: 0 } }, h('h3', { class: 'zg-card__title' }, p.title), p.subtitle ? h('div', { class: 'zg-card__sub' }, p.subtitle) : null),
27
28
  p.menu ? h('div', { class: 'zg-dd' }, h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': t('ChartCard', 'more'), 'aria-haspopup': 'menu', onClick: () => set({ open: !state.open }) }, icon(h, 'ellipsis-vertical')), state.open ? h('div', { class: 'zg-menu', role: 'menu', style: { insetInlineStart: 'auto', insetInlineEnd: 0 } }, ...p.menu.map(m => h('button', { key: m, type: 'button', role: 'menuitem', class: cx('zg-menu__i', m === 'Delete' && 'zg-menu__i--danger'), onClick: () => { set({ open: false }); p.onMenu && p.onMenu(m); } }, m))) : null) : null), ...children) },
28
29
  ActionCard: { state: { collapsed: false, loading: false, removed: false }, render: ({ C, h, props: p, state, set, children }) => { const col = p.collapsed !== undefined ? !!p.collapsed && p.collapsed !== 'false' : state.collapsed, busy = p.loading !== undefined ? !!p.loading && p.loading !== 'false' : state.loading; return state.removed ? null : h('section', { class: 'zg-card zg-glass zg-lo' }, h('div', { class: 'zg-card__head' }, h('div', { style: { flex: 1 } }, h('h3', { class: 'zg-card__title' }, p.title), p.subtitle ? h('div', { class: 'zg-card__sub' }, p.subtitle) : null), h('div', { style: { display: 'flex', gap: '2px' } }, h(C.IconButton, { icon: col ? 'chevron-down' : 'chevron-up', label: col ? t('ActionCard', 'expand') : t('ActionCard', 'collapse'), size: 'sm', ghost: true, onClick: () => { set({ collapsed: !col }); p.onCollapse && p.onCollapse(!col); } }), h(C.IconButton, { icon: 'refresh-cw', label: t('ActionCard', 'refresh'), size: 'sm', ghost: true, onClick: () => { if (p.loading === undefined) { set({ loading: true }); setTimeout(() => set({ loading: false }), 1800); } p.onRefresh && p.onRefresh(); } }), h(C.IconButton, { icon: 'x', label: t('ActionCard', 'remove'), size: 'sm', ghost: true, onClick: () => { set({ removed: true }); p.onRemove && p.onRemove(); } }))),
@@ -35,7 +35,7 @@ export const more4 = {
35
35
  Footer: { render: ({ h, props: p, children }) => h('footer', { class: 'zg-foot' }, h('span', { style: { color: 'var(--zg-text-3)', fontSize: '13px' } }, p.copyright || '© 2026 ZiyaraGo'), h('nav', { style: { display: 'flex', gap: '16px', flexWrap: 'wrap' } }, ...(p.links || []).map(l => h('a', { key: l.label, href: l.href || '#', style: { color: 'var(--zg-brand-text)', fontSize: '13px', fontWeight: 600 } }, l.label))), ...children) },
36
36
  DataGroup: { state: { open: {} , grp: {} }, render: ({ C, h, props: p, state, set }) => { const cols = p.columns || [];
37
37
  const row = r => { const o = !!state.open[r.id]; return [h('tr', { key: r.id, 'aria-selected': o ? 'true' : undefined }, p.expand ? h('td', { style: { width: '44px' } }, h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-expanded': String(o), 'aria-label': t('DataGroup', 'expand'), onClick: () => set({ open: { ...state.open, [r.id]: !o } }) }, icon(h, o ? 'chevron-down' : 'chevron-right', 16))) : null, ...cols.map(c => h('td', { key: c.key, class: c.num ? 'num' : undefined }, c.render ? c.render(r[c.key], r) : r[c.key]))), o && p.expand ? h('tr', { key: r.id + 'x' }, h('td', { colSpan: cols.length + 1, style: { background: 'var(--zg-s1)', padding: '16px 20px' } }, p.expand(r))) : null]; };
38
- return h('div', { class: 'zg-table-wrap' }, h('table', { class: 'zg-table' }, h('thead', null, h('tr', null, p.expand ? h('th') : null, ...cols.map(c => h('th', { key: c.key, class: c.num ? 'num' : undefined }, c.label)))),
38
+ return h('div', { class: 'zg-table-wrap', tabIndex: 0 }, h('table', { class: 'zg-table' }, h('thead', null, h('tr', null, p.expand ? h('th') : null, ...cols.map(c => h('th', { key: c.key, class: c.num ? 'num' : undefined }, c.label)))),
39
39
  h('tbody', null, ...(p.groups || []).flatMap(g => { const c = !!state.grp[g.label]; return [h('tr', { key: 'g' + g.label }, h('td', { colSpan: cols.length + 1, style: { padding: 0 } }, h('button', { type: 'button', class: 'zg-dg__g', onClick: () => set({ grp: { ...state.grp, [g.label]: !c } }), style: { all: 'unset', boxSizing: 'border-box', display: 'flex', alignItems: 'center', gap: '8px', width: '100%', padding: '10px 16px', cursor: 'pointer', fontWeight: 700, fontSize: '13px', background: 'var(--zg-s1)' } }, icon(h, c ? 'chevron-right' : 'chevron-down', 14), g.label + ' (' + g.rows.length + ')'))), ...(c ? [] : g.rows.flatMap(row))]; })))); } },
40
40
  OptionCard: { render: ({ h, props: p }) => h('button', { type: 'button', role: p.multiple ? 'checkbox' : 'radio', 'aria-checked': String(!!p.checked), class: 'zg-opt', onClick: p.onClick }, p.image ? h('span', { class: 'zg-opt__img', style: { backgroundImage: `url(${p.image})` } }) : p.icon ? h('span', { class: 'zg-tile zg-t-brand', style: { '--s': '40px' } }, icon(h, p.icon, 20)) : null, h('span', { style: { display: 'flex', flexDirection: 'column', gap: '2px', flex: 1, textAlign: 'start' } }, h('b', { style: { fontSize: '15px' } }, p.title), p.desc ? h('span', { class: 'zg-hint' }, p.desc) : null), p.price ? h('b', { class: 'zg-num', style: { fontSize: '16px' } }, p.price) : null, h('span', { class: cx('zg-check__box', !p.multiple && 'zg-radio'), style: { background: p.checked ? 'var(--zg-brand)' : 'transparent' } }, p.checked ? (p.multiple ? icon(h, 'check', 12) : h('i')) : null)) },
41
41
  OptionCards: { render: ({ C, h, props: p }) => { const cur = p.multiple ? (p.modelValue || []) : p.modelValue;
@@ -2,6 +2,7 @@
2
2
  import { t } from '../i18n/index.js';
3
3
  import { icon, cx, ddRef } from './components.js';
4
4
  import { trap, life } from './dialog.js';
5
+ import { listKeys, tabIdx } from './keys.js';
5
6
  const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
6
7
 
7
8
  export const more6 = {
@@ -15,7 +16,7 @@ export const more6 = {
15
16
  ContextMenu: { state: { x: 0, y: 0, open: false }, render: ({ h, props: p, state, set, children }) => {
16
17
  const close = () => set({ open: false });
17
18
  return h('div', { class: 'zg-ctx', onContextmenu: e => { e.preventDefault(); set({ open: true, x: e.clientX, y: e.clientY }); } }, ...children,
18
- state.open ? h('div', { class: 'zg-ctx__scrim', onClick: close, onContextmenu: e => { e.preventDefault(); close(); } }, h('div', { class: 'zg-menu', role: 'menu', 'data-zg-dd': '', ref: ddRef(close, { self: true }), style: { position: 'fixed', left: Math.min(state.x, innerWidth - 240) + 'px', top: Math.min(state.y, innerHeight - (p.items || []).length * 42 - 20) + 'px', insetInlineStart: 'auto' }, onClick: e => e.stopPropagation() }, ...(p.items || []).map((it, i) => it.separator ? h('div', { key: i, class: 'zg-menu__sep' }) : h('button', { key: i, type: 'button', role: 'menuitem', class: cx('zg-menu__i', it.tone === 'danger' && 'zg-menu__i--danger'), onClick: () => { close(); p.onSelect && p.onSelect(it.value); } }, it.icon ? icon(h, it.icon, 16) : null, it.label, it.shortcut ? h('span', { class: 'zg-kbd' }, it.shortcut) : null)))) : null); } },
19
+ state.open ? h('div', { class: 'zg-ctx__scrim', onClick: close, onContextmenu: e => { e.preventDefault(); close(); } }, h('div', { class: 'zg-menu', role: 'menu', 'data-zg-dd': '', ref: ddRef(close, { self: true, focus: true }), onKeydown: listKeys('[role=menuitem]', { wrap: true, onTab: close }), style: { position: 'fixed', left: Math.min(state.x, innerWidth - 240) + 'px', top: Math.min(state.y, innerHeight - (p.items || []).length * 42 - 20) + 'px', insetInlineStart: 'auto' }, onClick: e => e.stopPropagation() }, ...(p.items || []).map((it, i, all) => it.separator ? h('div', { key: i, class: 'zg-menu__sep', role: 'separator' }) : h('button', { key: i, type: 'button', role: 'menuitem', tabIndex: tabIdx(all.map(x => ({ disabled: x.separator || x.disabled })), () => false)(i), class: cx('zg-menu__i', it.tone === 'danger' && 'zg-menu__i--danger'), onClick: () => { close(); p.onSelect && p.onSelect(it.value); } }, it.icon ? icon(h, it.icon, 16) : null, it.label, it.shortcut ? h('span', { class: 'zg-kbd' }, it.shortcut) : null)))) : null); } },
19
20
  Sortable: { state: { drag: null, over: null }, render: ({ h, props: p, state, set }) => { const items = p.items || [];
20
21
  const drop = (to, f) => { const from = f === undefined ? state.drag : f; set({ drag: null, over: null }); if (from == null || from === to) return; const a = items.slice(); const [m] = a.splice(from, 1); a.splice(to, 0, m); p.onChange && p.onChange(a); };
21
22
  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 }) },
@@ -0,0 +1,55 @@
1
+ // Keyboard helpers shared by the core components. Handlers run in the browser only (SSR renders no events).
2
+ const isRtl = el => typeof getComputedStyle === 'function' && getComputedStyle(el).direction === 'rtl';
3
+ const live = (root, sel) => [...root.querySelectorAll(sel)].filter(n => !n.disabled && n.getAttribute('aria-disabled') !== 'true');
4
+ const mod = e => e.altKey || e.ctrlKey || e.metaKey;
5
+
6
+ /** Roving tabindex value per item: the active one (or the first enabled when none) is 0, the rest -1. Returns i => 0 | -1. */
7
+ export const tabIdx = (items, isOn) => {
8
+ let on = items.findIndex(it => isOn(it) && !it.disabled);
9
+ if (on < 0) on = items.findIndex(it => !it.disabled);
10
+ return i => (i === on ? 0 : -1);
11
+ };
12
+
13
+ const park = (items, n) => { for (const i of items) i.tabIndex = i === n ? 0 : -1; n.focus(); };
14
+
15
+ /** Tablist / button group: Left/Right (mirrored in RTL, Up/Down when vertical), Home, End. Focus moves, Enter/Space activates. */
16
+ export const rove = (sel, o = {}) => e => {
17
+ if (mod(e)) return;
18
+ const root = e.currentTarget, items = live(root, sel), cur = items.indexOf(e.target.closest && e.target.closest(sel));
19
+ if (cur < 0 || !items.length) return;
20
+ const rtl = isRtl(root), fwd = o.vertical ? 'ArrowDown' : rtl ? 'ArrowLeft' : 'ArrowRight', back = o.vertical ? 'ArrowUp' : rtl ? 'ArrowRight' : 'ArrowLeft';
21
+ const n = e.key === fwd ? (cur + 1) % items.length : e.key === back ? (cur - 1 + items.length) % items.length : e.key === 'Home' ? 0 : e.key === 'End' ? items.length - 1 : -1;
22
+ if (n < 0) return;
23
+ e.preventDefault(); park(items, items[n]);
24
+ };
25
+
26
+ /** The element that opened a popup list: the first button or input of the component root that is not inside the list. */
27
+ export const trigger = list => { const root = list && list.parentElement; return root && [...root.querySelectorAll('button,input')].find(x => !list.contains(x)); };
28
+ /** Call from an item's onClick so focus lands on the trigger instead of being lost when the list unmounts. */
29
+ export const toTrigger = e => { const l = e.currentTarget && e.currentTarget.closest && e.currentTarget.closest('[data-zg-dd]'), t = l && trigger(l); if (t) t.focus({ preventScroll: true }); };
30
+
31
+ /** Popup listbox / menu: Up/Down (menus wrap, listboxes stop at the ends), Home, End, type-ahead, Tab closes. */
32
+ export const listKeys = (sel, o = {}) => e => {
33
+ if (mod(e)) return;
34
+ const root = e.currentTarget, items = live(root, sel), cur = items.indexOf(e.target.closest && e.target.closest(sel)), len = items.length;
35
+ if (!len) return;
36
+ if (e.key === 'Tab') { if (o.onTab) o.onTab(e); return; }
37
+ let n = -1;
38
+ if (e.key === 'ArrowDown') n = cur < 0 ? 0 : o.wrap ? (cur + 1) % len : Math.min(cur + 1, len - 1);
39
+ else if (e.key === 'ArrowUp') n = cur < 0 ? len - 1 : o.wrap ? (cur - 1 + len) % len : Math.max(cur - 1, 0);
40
+ else if (e.key === 'Home') n = 0;
41
+ else if (e.key === 'End') n = len - 1;
42
+ else if (e.key.length === 1 && e.key !== ' ') {
43
+ const c = e.key.toLowerCase(), order = [...items.slice(cur + 1), ...items.slice(0, cur + 1)];
44
+ const hit = order.find(i => (i.textContent || '').trim().toLowerCase().startsWith(c));
45
+ if (hit) n = items.indexOf(hit);
46
+ }
47
+ if (n < 0) return;
48
+ e.preventDefault(); park(items, items[n]);
49
+ };
50
+
51
+ /** onKeydown for a popup trigger: ArrowDown / ArrowUp open the closed list. */
52
+ export const openKeys = (isOpen, open) => e => { if (!isOpen && !mod(e) && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) { e.preventDefault(); open(); } };
53
+
54
+ /** Focus the entry point of a freshly opened popup: the item with tabindex 0, else the first one. */
55
+ export const focusIn = el => { const t = el.querySelector('[tabindex="0"]') || el.querySelector('button:not([disabled])'); if (t && t.focus) t.focus({ preventScroll: true }); };
@@ -0,0 +1,5 @@
1
+ export default {
2
+ en: { label: 'Progress' },
3
+ ar: { label: 'التقدم' },
4
+ ru: { label: 'Прогресс' },
5
+ };
@@ -44,6 +44,7 @@ import Pagination from './Pagination.js';
44
44
  import PasswordInput from './PasswordInput.js';
45
45
  import Popconfirm from './Popconfirm.js';
46
46
  import PricingCard from './PricingCard.js';
47
+ import Progress from './Progress.js';
47
48
  import QiblaCompass from './QiblaCompass.js';
48
49
  import RamadanTracker from './RamadanTracker.js';
49
50
  import RangeCalendar from './RangeCalendar.js';
@@ -113,6 +114,7 @@ defineMessages('Pagination', Pagination);
113
114
  defineMessages('PasswordInput', PasswordInput);
114
115
  defineMessages('Popconfirm', Popconfirm);
115
116
  defineMessages('PricingCard', PricingCard);
117
+ defineMessages('Progress', Progress);
116
118
  defineMessages('QiblaCompass', QiblaCompass);
117
119
  defineMessages('RamadanTracker', RamadanTracker);
118
120
  defineMessages('RangeCalendar', RangeCalendar);
@@ -23,7 +23,7 @@
23
23
  .zg-btn--primary{background:var(--zg-brand);color:var(--zg-on-brand);box-shadow:inset 0 1px 0 oklch(1 0 0 / .3),0 8px 24px var(--zg-brand-glow)}.zg-btn--primary:hover{background:var(--zg-brand-hover)}
24
24
  .zg-btn--inverse{background:var(--zg-inv-bg);color:var(--zg-inv-fg)}.zg-btn--inverse:hover{background:var(--zg-inv-bg);filter:brightness(1.1)}
25
25
  .zg-btn--ghost{background:transparent;box-shadow:none}.zg-btn--ghost:hover{background:var(--zg-s2)}
26
- .zg-btn--danger{background:color-mix(in oklch,var(--zg-red) 18%,transparent);color:var(--zg-red)}
26
+ .zg-btn--danger{background:color-mix(in oklch,var(--zg-red) 18%,transparent);color:var(--zg-red-text)}
27
27
  .zg-btn--xs{--h:var(--zg-h-xs);font-size:12px}.zg-btn--sm{--h:var(--zg-h-sm);font-size:13px}.zg-btn--lg{--h:var(--zg-h-lg);font-size:15px}.zg-btn--xl{--h:var(--zg-h-xl);font-size:16px}
28
28
  .zg-btn--block{display:flex;width:100%}
29
29
  .zg-spin{width:1em;height:1em;border-radius:50%;border:2px solid currentColor;border-inline-end-color:transparent;animation:zg-spin .7s linear infinite}
@@ -65,4 +65,4 @@
65
65
  .zg-rc{display:grid;grid-template-columns:56px repeat(var(--d),minmax(0,1fr));gap:2px;align-items:center;direction:ltr;font-size:12px}.zg-rc__h{text-align:center;color:var(--zg-text-3)}.zg-rc__c{height:28px;border-radius:6px;background:var(--zg-s2)}.zg-rc__c[data-b]{background:var(--zg-brand-soft);border-radius:0}.zg-rc__c[data-b="start"]{background:var(--zg-brand);border-radius:999px 0 0 999px}
66
66
  .zg-occ{display:flex;align-items:flex-end;gap:8px;height:160px}.zg-occ__c{flex:1;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;gap:6px}.zg-occ__c i{width:100%;border-radius:10px 10px 4px 4px}
67
67
 
68
- .zg-kpi--brand .zg-delta{color:inherit;opacity:.85}.zg-kpi--brand .zg-delta::before{opacity:.9}
68
+ .zg-kpi--brand .zg-kpi__label,.zg-kpi--brand .zg-kpi__sub{opacity:.9}.zg-kpi--brand .zg-delta{color:inherit;opacity:1}.zg-kpi--brand .zg-delta::before{opacity:.9}
@@ -110,3 +110,6 @@
110
110
  .zg-qb__txt b{font-size:calc(22px*var(--dz,1));font-weight:600;color:var(--zg-text-1)}.zg-qb__txt small{color:var(--zg-text-3)}
111
111
  /* Numbers: tabular figures with a plain zero (no slashed or dotted zero) in KPI values, tables and code cells */
112
112
  .zg-num,.zg-mono,.zg-kpi__value,.zg-table .num{font-feature-settings:"zero" 0,"tnum" 1}
113
+ .zg-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}
114
+ .zg-select__opt:focus-visible{outline:3px solid var(--zg-brand-soft);outline-offset:-3px;background:var(--zg-s2)}
115
+ .zg-seg button:focus-visible,.zg-tabs button:focus-visible,.zg-code-b pre:focus-visible{outline:3px solid var(--zg-brand-soft);outline-offset:2px}
@@ -11,9 +11,9 @@
11
11
  --zg-s1:oklch(1 0 0 / .055);--zg-s2:oklch(1 0 0 / .085);--zg-s3:oklch(1 0 0 / .13);
12
12
  --zg-glass:oklch(0.235 0.006 40 / .92);
13
13
  --zg-stroke:oklch(1 0 0 / .08);
14
- --zg-text-1:oklch(0.97 0.004 70);--zg-text-2:oklch(0.82 0.008 70);--zg-text-3:oklch(0.66 0.01 70);
14
+ --zg-text-1:oklch(0.97 0.004 70);--zg-text-2:oklch(0.82 0.008 70);--zg-text-3:oklch(0.74 0.01 70);
15
15
  --zg-inv-bg:oklch(0.96 0.004 70);--zg-inv-fg:oklch(0.2 0.01 60);
16
- --zg-green:oklch(0.76 0.15 150);--zg-amber:oklch(0.8 0.14 80);--zg-red:oklch(0.7 0.18 25);
16
+ --zg-green:oklch(0.76 0.15 150);--zg-amber:oklch(0.8 0.14 80);--zg-red:oklch(0.7 0.18 25);--zg-red-text:oklch(0.86 0.1 25);
17
17
  --zg-rim:inset 0 1px 0 oklch(1 0 0 / .16),inset 0 0 0 1px oklch(1 0 0 / .07),inset 0 -1px 0 oklch(0 0 0 / .25);
18
18
  --zg-shadow-1:0 8px 24px oklch(0 0 0 / .28);--zg-shadow-2:0 24px 64px oklch(0 0 0 / .5);
19
19
  }
@@ -27,7 +27,7 @@
27
27
  --zg-stroke:oklch(0.3 0.02 60 / .15);
28
28
  --zg-text-1:oklch(0.2 0.01 60);--zg-text-2:oklch(0.36 0.012 60);--zg-text-3:oklch(0.47 0.012 60);
29
29
  --zg-inv-bg:oklch(0.22 0.01 60);--zg-inv-fg:oklch(0.97 0.004 70);
30
- --zg-green:oklch(0.5 0.14 150);--zg-amber:oklch(0.55 0.13 70);--zg-red:oklch(0.52 0.19 25);
30
+ --zg-green:oklch(0.5 0.14 150);--zg-amber:oklch(0.55 0.13 70);--zg-red:oklch(0.52 0.19 25);--zg-red-text:oklch(0.44 0.17 25);
31
31
  --zg-rim:inset 0 1px 0 oklch(1 0 0 / .95),inset 0 0 0 1px oklch(1 0 0 / .45),0 0 0 1px oklch(0.3 0.02 60 / .13);
32
32
  --zg-shadow-1:0 8px 24px oklch(0.4 0.03 60 / .17);--zg-shadow-2:0 24px 64px oklch(0.4 0.03 60 / .22);
33
33
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$description": "Design tokens for Flutter / native. Colors are sRGB hex approximations of the OKLCH source in src/styles/tokens.css.",
3
3
  "palettes": { "gold": { "brand": "#BC926A", "sky": "#5FAACC" }, "tamam": { "brand": "#3FD13F", "sky": "#4FB5C4" } },
4
- "dark": { "bg": "#1B1919", "text1": "#F1EDE8", "text2": "#D0CAC3", "text3": "#A19A93", "onBrand": "#1F1812", "green": "#6ED08F", "amber": "#E8B964", "red": "#E26E5F", "surface1": "#FFFFFF0E", "surface2": "#FFFFFF16", "surface3": "#FFFFFF21", "stroke": "#FFFFFF14" },
4
+ "dark": { "bg": "#1B1919", "text1": "#F1EDE8", "text2": "#D0CAC3", "text3": "#AAA49E", "onBrand": "#1F1812", "green": "#6ED08F", "amber": "#E8B964", "red": "#E26E5F", "surface1": "#FFFFFF0E", "surface2": "#FFFFFF16", "surface3": "#FFFFFF21", "stroke": "#FFFFFF14" },
5
5
  "light": { "bg": "#E7E3DF", "text1": "#241E18", "text2": "#4E4640", "text3": "#6C645D", "onBrand": "#1F1812", "green": "#2F7D4D", "amber": "#8A5F12", "red": "#B33A2C", "surface1": "#FFFFFF66", "surface2": "#FFFFFF8C", "surface3": "#FFFFFFBF", "stroke": "#4A3B2A1A" },
6
6
  "radius": { "tile": 20, "card": 32, "pill": 999 },
7
7
  "control": { "xs": 28, "sm": 32, "md": 40, "lg": 48, "xl": 56 },