@ziyarago/ui 0.12.0 → 0.14.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,15 @@
1
+ ## [0.14.0](https://github.com/ziyarago/ui/compare/v0.13.0...v0.14.0) (2026-10-21)
2
+
3
+ ### Features
4
+
5
+ * plugin API (registerComponents, defineUiComponents, registerMeta, resolver and Nuxt plugins option) ([ab8b844](https://github.com/ziyarago/ui/commit/ab8b8442a9968cfc0c978d9a5d4c3dc22b28b467))
6
+
7
+ ## [0.13.0](https://github.com/ziyarago/ui/compare/v0.12.0...v0.13.0) (2026-10-21)
8
+
9
+ ### Features
10
+
11
+ * accessibility audit with axe (0 violations), keyboard navigation for tabs, selects and menus, readable tooltips ([95a25e1](https://github.com/ziyarago/ui/commit/95a25e1d763f2c63ee788986943aeb3abbb669ae))
12
+
1
13
  ## [0.12.0](https://github.com/ziyarago/ui/compare/v0.11.0...v0.12.0) (2026-10-21)
2
14
 
3
15
  ### 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.14.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.14.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.14.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
  }
@@ -151,3 +151,14 @@ React types: `Ui<Name>Props` (props, callbacks, named slots as `ReactNode` props
151
151
  ## 7. Checking the types yourself
152
152
 
153
153
  `npm run test:types` in this repository runs vue-tsc and tsc over `tests/types/*`: valid usage must compile, invalid enum / missing required prop / wrong event payload / wrong model type / unknown slot / wrong slot type must fail with the expected message.
154
+
155
+ ## 8. Add-on packages (plugins)
156
+
157
+ An add-on package (for example `@ziyarago/ui-pro`) plugs components into the same registry through `registerComponents` / `defineUiComponents`, so its definitions can use `C.Button` and the core components can reference them. A plugin ships its own stylesheet: import it after the core one.
158
+
159
+ ```ts
160
+ import '@ziyarago/ui/style.css'
161
+ import '@ziyarago/ui-pro/style.css' // plugin css, always after style.css
162
+ ```
163
+
164
+ Plugin CSS must only use the public design tokens (`--zg-*`, see `@ziyarago/ui/tokens.css`) and its own prefixed classes. Auto-import: pass `plugins: ['@ziyarago/ui-pro']` to the Nuxt module options (`ziyarago: { plugins: [...] }`) or to `ZiyaragoUiResolver({ plugins: [...] })`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziyarago/ui",
3
- "version": "0.12.0",
3
+ "version": "0.14.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 }) },
package/src/core/index.js CHANGED
@@ -7,5 +7,11 @@ 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
+ /** Extension API: adds definitions to the shared registry (used by add-on packages such as @ziyarago/ui-pro). Registering the very same definition twice is a no-op (Vue and React adapters can both load one plugin). */
11
+ export function registerComponents(defs, { override = false } = {}) {
12
+ for (const name of Object.keys(defs)) if (!override && name in components && components[name] !== defs[name]) throw new Error(`@ziyarago/ui: component "${name}" is already registered (pass { override: true } to replace it)`);
13
+ Object.assign(components, defs);
14
+ return components;
15
+ }
10
16
  export { LOCALES, FREE_LOCALES, t, setUiLocale, getUiLocale, setUiMessages, isRtlLocale, subscribeUiLocale, getUiVersion, defineMessages } from '../i18n/index.js';
11
17
  import '../i18n/components/index.js';
@@ -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);
package/src/meta/index.js CHANGED
@@ -11,4 +11,17 @@ import domain from './domain.js';
11
11
  import pages from './pages.js';
12
12
 
13
13
  export const meta = { ...typography, ...layout, ...actions, ...forms, ...data, ...feedback, ...overlay, ...navigation, ...media, ...domain, ...pages };
14
+ import { PRO_NAMES } from './tiers.js';
15
+ import { GROUPS } from './groups.js';
16
+ /** Extension API: adds API metadata for plugin components. `tiers` marks names as Pro, `groups` ({ id, title, blurb, names }) lists them in the manifest. */
17
+ export function registerMeta(extra, { tiers, groups } = {}) {
18
+ Object.assign(meta, extra);
19
+ for (const n of tiers || []) PRO_NAMES.add(n);
20
+ for (const g of groups || []) {
21
+ const ex = GROUPS.find(x => x.id === g.id);
22
+ if (ex) ex.names.push(...g.names.filter(n => !ex.names.includes(n)));
23
+ else GROUPS.push({ blurb: '', title: g.id, ...g, names: [...g.names] });
24
+ }
25
+ return meta;
26
+ }
14
27
  export default meta;
@@ -1,11 +1,12 @@
1
1
  // Nuxt 3/4 module. nuxt.config: modules: ['@ziyarago/ui/nuxt'], ziyarago: { theme: true }
2
2
  import { defineNuxtModule, addComponent, addImports, addPlugin, createResolver } from '@nuxt/kit';
3
+ import { createRequire } from 'node:module';
3
4
  import { components } from '../core/index.js';
4
5
  import { themeInitScript } from '../theme.js';
5
6
 
6
7
  export default defineNuxtModule({
7
8
  meta: { name: '@ziyarago/ui', configKey: 'ziyarago', compatibility: { nuxt: '>=3.0.0' } },
8
- defaults: { css: true, prefix: 'Ui', theme: { theme: 'dark', palette: 'gold', dir: 'ltr', bg: 'none', locale: 'en' } },
9
+ defaults: { css: true, plugins: [], prefix: 'Ui', theme: { theme: 'dark', palette: 'gold', dir: 'ltr', bg: 'none', locale: 'en' } },
9
10
  setup(opts, nuxt) {
10
11
  const { resolve } = createResolver(import.meta.url);
11
12
  if (opts.css) nuxt.options.css.push('@ziyarago/ui/style.css');
@@ -13,6 +14,13 @@ export default defineNuxtModule({
13
14
  for (const name of Object.keys(components)) {
14
15
  addComponent({ name: opts.prefix + name, export: opts.prefix + name, filePath: '@ziyarago/ui/vue' });
15
16
  }
17
+ for (const pkg of opts.plugins) {
18
+ nuxt.options.build.transpile.push(pkg);
19
+ let names = [];
20
+ try { names = createRequire(import.meta.url)(pkg + '/names'); } catch { try { names = createRequire(nuxt.options.rootDir + '/')(pkg + '/names'); } catch { console.warn(`[@ziyarago/ui] plugin ${pkg}: cannot load ${pkg}/names`); } }
21
+ for (const name of names) addComponent({ name: opts.prefix + name, export: 'Ui' + name, filePath: pkg + '/vue' });
22
+ if (opts.css) nuxt.options.css.push(pkg + '/style.css');
23
+ }
16
24
  addImports([
17
25
  { name: 'useUiTheme', from: '@ziyarago/ui/theme' },
18
26
  { name: 'fmt', from: '@ziyarago/ui/vue' },
@@ -2,7 +2,7 @@
2
2
  // React 18+ adapter. Usage: import { UiButton, UiAppShell } from '@ziyarago/ui/react'
3
3
  import React, { useState, useRef, useEffect, useLayoutEffect, useSyncExternalStore, createElement, cloneElement, isValidElement } from 'react';
4
4
  import { createPortal } from 'react-dom';
5
- import { components, subscribeUiLocale, getUiVersion } from '../core/index.js';
5
+ import { components, registerComponents, subscribeUiLocale, getUiVersion } from '../core/index.js';
6
6
 
7
7
  const C = {};
8
8
  const useIsoLayoutEffect = typeof document === 'undefined' ? useEffect : useLayoutEffect;
@@ -37,6 +37,13 @@ const wrap = (name, def) => {
37
37
  return Comp;
38
38
  };
39
39
  for (const [name, def] of Object.entries(components)) C[name] = wrap(name, def);
40
+ /** Wraps definitions like the built-in ones, registers them in C (cross references work both ways) and returns { UiName: Component }. */
41
+ export function defineUiComponents(defs, { override = false } = {}) {
42
+ registerComponents(defs, { override });
43
+ const out = {};
44
+ for (const [name, def] of Object.entries(defs)) out['Ui' + name] = C[name] = wrap(name, def);
45
+ return out;
46
+ }
40
47
 
41
48
  // Explicit named exports (not destructuring): Next.js reads the export names of a 'use client' module statically.
42
49
  export const UiButton = C.Button;
package/src/resolver.js CHANGED
@@ -3,20 +3,31 @@
3
3
  // import { ZiyaragoUiResolver } from '@ziyarago/ui/resolver'
4
4
  // Components({ resolvers: [ZiyaragoUiResolver()] })
5
5
  // Zero dependencies: it only maps <UiButton> to { name: 'UiButton', from: '@ziyarago/ui/vue' }.
6
+ import { createRequire } from 'node:module';
6
7
  import { components } from './core/index.js';
7
8
 
8
9
  const NAMES = new Set(Object.keys(components));
9
10
 
11
+ // Plugin packages (see docs-private/PRO-SPLIT.md, "Plugin contract") export ./names (JSON array of component names without the Ui prefix) and ./vue.
12
+ // A plugin may be a package name or { from, names } for tests and custom setups.
13
+ const loadPlugin = p => {
14
+ if (typeof p !== 'string') return { from: p.from, names: new Set(p.names) };
15
+ const req = createRequire(process.cwd() + '/');
16
+ return { from: p + '/vue', names: new Set(req(p + '/names')) };
17
+ };
18
+
10
19
  /**
11
- * @param {{ prefix?: string, css?: boolean | string, exclude?: string[], from?: string }} [options]
20
+ * @param {{ prefix?: string, css?: boolean | string, exclude?: string[], from?: string, plugins?: Array<string | { from: string, names: string[] }> }} [options]
12
21
  * prefix component prefix, default 'Ui' (must match the registered names, which are always Ui*)
13
22
  * css true: import '@ziyarago/ui/style.css' as a side effect together with the first resolved component (once per module that uses one);
14
23
  * a string: import that path instead (for example your own entry that imports the css inside a Tailwind @layer). Default false.
15
24
  * exclude component names (without prefix) that must not be auto-imported, e.g. ['Button'] if you keep your own UiButton.
16
25
  * from module the components are imported from, default '@ziyarago/ui/vue'
26
+ * plugins add-on packages whose components auto-import too: ['@ziyarago/ui-pro'] (needs its ./names and ./vue exports) or [{ from, names }]
17
27
  */
18
28
  export function ZiyaragoUiResolver(options = {}) {
19
- const { prefix = 'Ui', css = false, exclude = [], from = '@ziyarago/ui/vue' } = options;
29
+ const { prefix = 'Ui', css = false, exclude = [], from = '@ziyarago/ui/vue', plugins = [] } = options;
30
+ const loaded = plugins.map(loadPlugin);
20
31
  const skip = new Set(exclude.map(n => n.replace(/^Ui/, '')));
21
32
  const sideEffects = css ? (css === true ? '@ziyarago/ui/style.css' : css) : undefined;
22
33
  return {
@@ -24,8 +35,10 @@ export function ZiyaragoUiResolver(options = {}) {
24
35
  resolve(name) {
25
36
  if (!name.startsWith(prefix)) return;
26
37
  const bare = name.slice(prefix.length);
27
- if (!NAMES.has(bare) || skip.has(bare)) return;
28
- return { name: 'Ui' + bare, from, ...(sideEffects ? { sideEffects } : {}) };
38
+ if (skip.has(bare)) return;
39
+ const pl = NAMES.has(bare) ? null : loaded.find(x => x.names.has(bare));
40
+ if (!pl && !NAMES.has(bare)) return;
41
+ return { name: 'Ui' + bare, from: pl ? pl.from : from, ...(sideEffects ? { sideEffects } : {}) };
29
42
  },
30
43
  };
31
44
  }
@@ -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
  }
package/src/vue/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Vue 3 adapter. Usage: import { UiButton, UiAppShell } from '@ziyarago/ui/vue'
2
2
  import { h, ref, reactive, defineComponent, onMounted, onBeforeUnmount, Teleport, cloneVNode, isVNode } from 'vue';
3
- import { components, subscribeUiLocale } from '../core/index.js';
3
+ import { components, registerComponents, subscribeUiLocale } from '../core/index.js';
4
4
  import { BOOL_PROPS } from '../core/bools.js';
5
5
 
6
6
  const C = {};
@@ -28,6 +28,13 @@ const wrap = (name, def) => defineComponent({
28
28
  },
29
29
  });
30
30
  for (const [name, def] of Object.entries(components)) C[name] = wrap(name, def);
31
+ /** Wraps definitions like the built-in ones, registers them in C (cross references work both ways) and returns { UiName: Component }. */
32
+ export function defineUiComponents(defs, { override = false } = {}) {
33
+ registerComponents(defs, { override });
34
+ const out = {};
35
+ for (const [name, def] of Object.entries(defs)) out['Ui' + name] = C[name] = wrap(name, def);
36
+ return out;
37
+ }
31
38
 
32
39
  export const { Button: UiButton, IconButton: UiIconButton, Badge: UiBadge, Avatar: UiAvatar, Switch: UiSwitch, Checkbox: UiCheckbox, SegmentedControl: UiSegmentedControl, Tabs: UiTabs, FilterChips: UiFilterChips, TextField: UiTextField, Select: UiSelect, CodeInput: UiCodeInput, Stepper: UiStepper, Card: UiCard, Modal: UiModal, Drawer: UiDrawer, Toast: UiToast, Breadcrumbs: UiBreadcrumbs, Pagination: UiPagination, Progress: UiProgress, Ring: UiRing, KpiTile: UiKpiTile, DataTable: UiDataTable, KanbanColumn: UiKanbanColumn, KanbanCard: UiKanbanCard, Timeline: UiTimeline, TopNav: UiTopNav, SideNav: UiSideNav, AppShell: UiAppShell, PageHeader: UiPageHeader, AuthLayout: UiAuthLayout, LeadCard: UiLeadCard, HotelCard: UiHotelCard, TourCard: UiTourCard, PrayerTimes: UiPrayerTimes, RamadanTracker: UiRamadanTracker, QiblaCompass: UiQiblaCompass, CallRow: UiCallRow, Alert: UiAlert, Banner: UiBanner, Accordion: UiAccordion, Skeleton: UiSkeleton, Spinner: UiSpinner, Divider: UiDivider, EmptyState: UiEmptyState, ErrorPage: UiErrorPage, DataState: UiDataState, LoadingOverlay: UiLoadingOverlay, ConfirmDialog: UiConfirmDialog, Popconfirm: UiPopconfirm, Carousel: UiCarousel, Gallery: UiGallery, Code: UiCode, PricingCard: UiPricingCard, IntegrationCard: UiIntegrationCard, IncomingCall: UiIncomingCall, CallCard: UiCallCard, Dropdown: UiDropdown, Menu: UiMenu, Tooltip: UiTooltip, HoverCard: UiHoverCard, Popover: UiPopover, CopyButton: UiCopyButton, CommandBar: UiCommandBar, CommandPalette: UiCommandPalette, NotificationBell: UiNotificationBell, ToggleGroup: UiToggleGroup, Fab: UiFab, Textarea: UiTextarea, NumberInput: UiNumberInput, PasswordInput: UiPasswordInput, SearchInput: UiSearchInput, RadioGroup: UiRadioGroup, Slider: UiSlider, Rating: UiRating, ChipInput: UiChipInput, InlineEdit: UiInlineEdit, ColorPicker: UiColorPicker, FileUpload: UiFileUpload, Combobox: UiCombobox, MultiSelect: UiMultiSelect, GroupedSelect: UiGroupedSelect, PhoneInput: UiPhoneInput, Calendar: UiCalendar, DatePicker: UiDatePicker, TimePicker: UiTimePicker, FormField: UiFormField, LoginForm: UiLoginForm, StepForm: UiStepForm, Tag: UiTag, Kbd: UiKbd, StatsCard: UiStatsCard, KpiInline: UiKpiInline, MiniChart: UiMiniChart, Descriptions: UiDescriptions, LazyImage: UiLazyImage, SystemStatus: UiSystemStatus, TreeView: UiTreeView, CalendarView: UiCalendarView, SubNav: UiSubNav, ActionRail: UiActionRail, SplitLayout: UiSplitLayout, SettingsSection: UiSettingsSection, ScrollArea: UiScrollArea, DealCard: UiDealCard, PipelineBoard: UiPipelineBoard, ContactPanel: UiContactPanel, SalesFunnel: UiSalesFunnel, BookingCard: UiBookingCard, GuestCard: UiGuestCard, RoomGrid: UiRoomGrid, RoomCalendar: UiRoomCalendar, OccupancyChart: UiOccupancyChart, Chip: UiChip, IconTile: UiIconTile, StatusIcon: UiStatusIcon, UserCell: UiUserCell, StatRow: UiStatRow, CountryRow: UiCountryRow, StatLine: UiStatLine, StatStrip: UiStatStrip, VersusBar: UiVersusBar, SegBar: UiSegBar, HeroBanner: UiHeroBanner, TabTiles: UiTabTiles, PillTabs: UiPillTabs, PillSelect: UiPillSelect, ChartCard: UiChartCard, ActionCard: UiActionCard, Snackbar: UiSnackbar, TourPopover: UiTourPopover, ProfileHeader: UiProfileHeader, AuthSplit: UiAuthSplit, AuthShowcase: UiAuthShowcase, SocialAuth: UiSocialAuth, StatusPage: UiStatusPage, ValidatedField: UiValidatedField, Footer: UiFooter, DataGroup: UiDataGroup, OptionCard: UiOptionCard, OptionCards: UiOptionCards, RichEditor: UiRichEditor, RangeSlider: UiRangeSlider, RangeCalendar: UiRangeCalendar, StepDialog: UiStepDialog, FullscreenDialog: UiFullscreenDialog, PageDashboard: UiPageDashboard, PagePatterns: UiPagePatterns, PageCrm: UiPageCrm, PageBookings: UiPageBookings, PageCatalog: UiPageCatalog, PageDetail: UiPageDetail, PageSettings: UiPageSettings, PageUserList: UiPageUserList, PageProfile: UiPageProfile, PageForm: UiPageForm, PageOrder: UiPageOrder, PageAccount: UiPageAccount, PagePricing: UiPagePricing, PageFaq: UiPageFaq, PageAnalytics: UiPageAnalytics, PageCalendar: UiPageCalendar, PageKanban: UiPageKanban, PageBilling: UiPageBilling, PageTeam: UiPageTeam, PageIntegrations: UiPageIntegrations, PageSearch: UiPageSearch, PageArticle: UiPageArticle, PageChangelog: UiPageChangelog, PageHelp: UiPageHelp, PageStates: UiPageStates, PageLegal: UiPageLegal, PageLocations: UiPageLocations, PageBookingFlow: UiPageBookingFlow, PageItinerary: UiPageItinerary, PagePrayer: UiPagePrayer, PageGroupManager: UiPageGroupManager, PageTourPublic: UiPageTourPublic, PageNotifications: UiPageNotifications, PageInbox: UiPageInbox, PageCheckout: UiPageCheckout, PageOnboarding: UiPageOnboarding, PageLanding: UiPageLanding, PageAuth: UiPageAuth, PageStatus: UiPageStatus, TreeSelect: UiTreeSelect, ContextMenu: UiContextMenu, Sortable: UiSortable, Map: UiMap, ImageUpload: UiImageUpload, Screensaver: UiScreensaver, Tour: UiTour, Heading: UiHeading, Text: UiText, Box: UiBox, Center: UiCenter, HStack: UiHStack, VStack: UiVStack, Grid: UiGrid, Link: UiLink, Pressable: UiPressable, Icon: UiIcon, Portal: UiPortal, LiquidGlass: UiLiquidGlass, BottomSheet: UiBottomSheet, Actionsheet: UiActionsheet, ImageViewer: UiImageViewer, ChatAi: UiChatAi } = C;
33
40
  export const install = app => { for (const [n, c] of Object.entries(C)) app.component('Ui' + n, c); };
@@ -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 },
package/types/core.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  // Core (framework-neutral) entry. Most apps import from '@ziyarago/ui/vue' or '@ziyarago/ui/react' instead.
2
2
  export { rules, setIconBase, fmt } from './shared';
3
3
  export type * from './shared';
4
+ export interface ComponentDefinition { state?: Record<string, unknown>; slots?: string[]; render: (ctx: any) => unknown }
5
+ /** Extension API for add-on packages: adds definitions to the shared registry. Throws on a name clash with a different definition unless { override: true }. */
6
+ export function registerComponents(defs: Record<string, ComponentDefinition>, options?: { override?: boolean }): Record<string, ComponentDefinition>;
4
7
  export const components: Record<string, { state?: Record<string, unknown>; slots?: string[]; render: (ctx: any) => unknown }>;
package/types/meta.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  export interface MetaProp { type: string; values?: string[]; default?: unknown; description?: string; required?: boolean; model?: boolean; forwarded?: boolean }
2
2
  export interface ComponentMeta { description: string; props?: Record<string, MetaProp>; slots?: Record<string, string>; events?: Record<string, string>; notes?: string[] }
3
+ export interface MetaGroup { id: string; title?: string; blurb?: string; names: string[] }
4
+ /** Extension API: adds metadata for plugin components. `tiers` are marked as pro, `groups` list them in the manifest. */
5
+ export function registerMeta(extra: Record<string, ComponentMeta>, options?: { tiers?: string[]; groups?: MetaGroup[] }): Record<string, ComponentMeta>;
3
6
  export const meta: Record<string, ComponentMeta>;
4
7
  export default meta;
package/types/nuxt.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export interface ModuleOptions {
2
2
  css?: boolean;
3
3
  prefix?: string;
4
+ /** Add-on packages (for example '@ziyarago/ui-pro'): their components auto-import and their style.css is added after the core css. */
5
+ plugins?: string[];
4
6
  theme?: false | { theme?: 'dark' | 'light'; palette?: 'gold' | 'tamam'; dir?: 'ltr' | 'rtl'; bg?: 'none' | 'aurora' | 'sunset' | 'ocean' | 'emerald' | 'rose' | 'mesh' };
5
7
  }
6
8
  declare const _default: import('@nuxt/schema').NuxtModule<ModuleOptions>;
package/types/react.d.ts CHANGED
@@ -7739,3 +7739,5 @@ export const UiVersusBar: ComponentType<UiVersusBarProps>;
7739
7739
  export const rules: typeof import('./shared').rules;
7740
7740
  export { useUiTheme } from './theme';
7741
7741
  export { fmt, setIconBase } from './shared';
7742
+ /** Extension API: wraps plugin component definitions like the built-in ones, registers them (C.Name cross references work) and returns { UiName: Component }. */
7743
+ export function defineUiComponents(defs: Record<string, import('./core').ComponentDefinition>, options?: { override?: boolean }): Record<string, ComponentType<any>>;
@@ -7,6 +7,8 @@ export interface ZiyaragoUiResolverOptions {
7
7
  exclude?: string[];
8
8
  /** Module the components come from. Default '@ziyarago/ui/vue'. */
9
9
  from?: string;
10
+ /** Add-on packages whose components auto-import too: a package name exporting ./names and ./vue, or an explicit { from, names }. */
11
+ plugins?: Array<string | { from: string; names: string[] }>;
10
12
  }
11
13
  export interface ZiyaragoUiResolverResult { name: string; from: string; sideEffects?: string }
12
14
  export interface ZiyaragoUiResolver { type: 'component'; resolve(name: string): ZiyaragoUiResolverResult | undefined }
package/types/vue.d.ts CHANGED
@@ -7217,6 +7217,8 @@ export const UiVersusBar: UiVueComponent<UiVersusBarProps, UiVersusBarSlots>;
7217
7217
 
7218
7218
  export const rules: typeof import('./shared').rules;
7219
7219
  export function install(app: import('vue').App): void;
7220
+ /** Extension API: wraps plugin component definitions like the built-in ones, registers them (C.Name cross references work) and returns { UiName: Component }. */
7221
+ export function defineUiComponents(defs: Record<string, import('./core').ComponentDefinition>, options?: { override?: boolean }): Record<string, import('vue').Component>;
7220
7222
  declare const _default: { install: typeof install };
7221
7223
  export default _default;
7222
7224
  export { useUiTheme } from './theme';