@ziyarago/ui 0.0.0-stage → 0.1.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 +46 -0
- package/LICENSE +25 -0
- package/README.md +121 -2
- package/THIRD_PARTY_NOTICES.md +12 -0
- package/assets/3d/_sheet.png +0 -0
- package/assets/3d/archive.png +0 -0
- package/assets/3d/bag.png +0 -0
- package/assets/3d/cameraoff.png +0 -0
- package/assets/3d/comment.png +0 -0
- package/assets/3d/crowd.png +0 -0
- package/assets/3d/firstaid.png +0 -0
- package/assets/3d/flag.png +0 -0
- package/assets/3d/grain.png +0 -0
- package/assets/3d/guide.png +0 -0
- package/assets/3d/hotel.png +0 -0
- package/assets/3d/kaaba.png +0 -0
- package/assets/3d/logo.png +0 -0
- package/assets/3d/luggage.png +0 -0
- package/assets/3d/map.png +0 -0
- package/assets/3d/mosque.png +0 -0
- package/assets/3d/plane.png +0 -0
- package/assets/3d/profile.png +0 -0
- package/assets/3d/restaurant.png +0 -0
- package/assets/3d/ring360.png +0 -0
- package/assets/3d/shoes.png +0 -0
- package/assets/3d/star.png +0 -0
- package/assets/3d/store.png +0 -0
- package/assets/3d/taxi.png +0 -0
- package/assets/3d/threads.png +0 -0
- package/assets/3d/tour360.png +0 -0
- package/assets/3d/trash.png +0 -0
- package/assets/3d/umbrella.png +0 -0
- package/assets/3d/video.png +0 -0
- package/assets/3d/water.png +0 -0
- package/assets/brand/mark.png +0 -0
- package/assets/brand/ziyarago-logo-light.svg +21 -0
- package/assets/brand/ziyarago-logo.svg +21 -0
- package/assets/brand/ziyarago-mark-light.svg +13 -0
- package/assets/brand/ziyarago-mark.svg +13 -0
- package/assets/fonts/thmanyah/thmanyahsans-Black.otf +0 -0
- package/assets/fonts/thmanyah/thmanyahsans-Bold.otf +0 -0
- package/assets/fonts/thmanyah/thmanyahsans-Light.otf +0 -0
- package/assets/fonts/thmanyah/thmanyahsans-Medium.otf +0 -0
- package/assets/fonts/thmanyah/thmanyahsans-Regular.otf +0 -0
- package/assets/fonts/thmanyah/thmanyahserifdisplay-Black.otf +0 -0
- package/assets/fonts/thmanyah/thmanyahserifdisplay-Bold.otf +0 -0
- package/assets/fonts/thmanyah/thmanyahserifdisplay-Light.otf +0 -0
- package/assets/fonts/thmanyah/thmanyahserifdisplay-Medium.otf +0 -0
- package/assets/fonts/thmanyah/thmanyahserifdisplay-Regular.otf +0 -0
- package/assets/fonts/thmanyah/thmanyahseriftext-Black.otf +0 -0
- package/assets/fonts/thmanyah/thmanyahseriftext-Bold.otf +0 -0
- package/assets/fonts/thmanyah/thmanyahseriftext-Light.otf +0 -0
- package/assets/fonts/thmanyah/thmanyahseriftext-Medium.otf +0 -0
- package/assets/fonts/thmanyah/thmanyahseriftext-Regular.otf +0 -0
- package/dist/llms-full.txt +2293 -0
- package/dist/llms.txt +239 -0
- package/dist/manifest.json +10254 -0
- package/dist/style.css +1357 -0
- package/dist/tokens.css +48 -0
- package/package.json +118 -4
- package/src/catalog.js +4 -0
- package/src/core/components.js +124 -0
- package/src/core/components2.js +163 -0
- package/src/core/components3.js +35 -0
- package/src/core/components4.js +58 -0
- package/src/core/components6.js +48 -0
- package/src/core/components7.js +42 -0
- package/src/core/dialog.js +65 -0
- package/src/core/index.js +9 -0
- package/src/core/pages.js +51 -0
- package/src/meta/README.md +29 -0
- package/src/meta/actions.js +161 -0
- package/src/meta/data.js +327 -0
- package/src/meta/domain.js +212 -0
- package/src/meta/feedback.js +153 -0
- package/src/meta/forms.js +453 -0
- package/src/meta/groups.js +16 -0
- package/src/meta/index.js +14 -0
- package/src/meta/layout.js +159 -0
- package/src/meta/media.js +104 -0
- package/src/meta/navigation.js +99 -0
- package/src/meta/overlay.js +264 -0
- package/src/meta/pages.js +229 -0
- package/src/meta/templates.json +2422 -0
- package/src/meta/typography.js +58 -0
- package/src/nuxt/module.mjs +29 -0
- package/src/nuxt/plugin.mjs +9 -0
- package/src/react/index.js +31 -0
- package/src/styles/components.css +162 -0
- package/src/styles/components2.css +68 -0
- package/src/styles/components3.css +29 -0
- package/src/styles/components4.css +18 -0
- package/src/styles/components5.css +5 -0
- package/src/styles/components6.css +19 -0
- package/src/styles/components7.css +44 -0
- package/src/styles/components8.css +47 -0
- package/src/styles/effects.css +43 -0
- package/src/styles/fonts.css +17 -0
- package/src/styles/index.css +21 -0
- package/src/styles/sizes/actions.css +126 -0
- package/src/styles/sizes/data.css +161 -0
- package/src/styles/sizes/domain.css +35 -0
- package/src/styles/sizes/feedback.css +69 -0
- package/src/styles/sizes/forms.css +122 -0
- package/src/styles/sizes/layout.css +15 -0
- package/src/styles/sizes/media.css +28 -0
- package/src/styles/sizes/navigation.css +146 -0
- package/src/styles/sizes/overlay.css +79 -0
- package/src/styles/sizes/typography.css +12 -0
- package/src/styles/tokens.css +47 -0
- package/src/theme.js +52 -0
- package/src/vue/index.js +29 -0
- package/tokens/tokens.json +9 -0
- package/types/core.d.ts +4 -0
- package/types/nuxt.d.ts +11 -0
- package/types/react.d.ts +189 -0
- package/types/shared.d.ts +18 -0
- package/types/theme.d.ts +6 -0
- package/types/vue.d.ts +192 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Shared dialog behaviour: Escape, focus move/trap/restore. Used as a function ref on the dialog element, so
|
|
2
|
+
// it runs only in a browser (SSR ignores refs) and listeners exist only while a dialog is mounted.
|
|
3
|
+
const reg = new WeakMap(), stack = [], pend = new WeakMap();
|
|
4
|
+
// Adapters may call a ref before the element is attached (Transition, detached parents): wait until it is in the document.
|
|
5
|
+
const whenConnected = (el, opts, run) => {
|
|
6
|
+
if (el.isConnected) return run(el, opts);
|
|
7
|
+
const w = pend.get(el); if (w) { w.opts = opts; return; }
|
|
8
|
+
const q = { opts }; pend.set(el, q);
|
|
9
|
+
const step = n => { if (el.isConnected) { pend.delete(el); run(el, q.opts); } else if (n < 60) requestAnimationFrame(() => step(n + 1)); else pend.delete(el); };
|
|
10
|
+
queueMicrotask(() => step(0));
|
|
11
|
+
};
|
|
12
|
+
const SEL = 'a[href],button:not([disabled]),input:not([disabled]):not([type=hidden]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"]),[contenteditable="true"]';
|
|
13
|
+
const focusables = el => [...el.querySelectorAll(SEL)].filter(n => n.tabIndex >= 0 && n.getClientRects().length > 0 && !n.closest('[inert]'));
|
|
14
|
+
const onKey = e => {
|
|
15
|
+
const r = stack[stack.length - 1]; if (!r) return;
|
|
16
|
+
const o = r.opts;
|
|
17
|
+
if (o.onKey && o.onKey(e) === true) return;
|
|
18
|
+
if (e.key === 'Escape' && !e.defaultPrevented && o.onClose) { e.preventDefault(); o.onClose(e); return; }
|
|
19
|
+
if (e.key !== 'Tab' || o.focus === false) return;
|
|
20
|
+
const list = focusables(r.el), a = document.activeElement;
|
|
21
|
+
if (!list.length) { e.preventDefault(); r.el.focus(); return; }
|
|
22
|
+
const first = list[0], last = list[list.length - 1], inside = r.el.contains(a) && a !== r.el;
|
|
23
|
+
if (e.shiftKey && (!inside || a === first)) { e.preventDefault(); last.focus(); }
|
|
24
|
+
else if (!e.shiftKey && (!inside || a === last)) { e.preventDefault(); first.focus(); }
|
|
25
|
+
};
|
|
26
|
+
const mount = (el, opts) => {
|
|
27
|
+
let prev = document.activeElement;
|
|
28
|
+
for (const o of [...stack]) if (!o.el.isConnected) { if ((!prev || prev === document.body) && o.prev) prev = o.prev; drop(o, true); }
|
|
29
|
+
const r = { el, opts, prev };
|
|
30
|
+
reg.set(el, r);
|
|
31
|
+
if (opts.mount) r.cleanup = opts.mount(el, () => r.opts);
|
|
32
|
+
if (!stack.length) document.addEventListener('keydown', onKey);
|
|
33
|
+
stack.push(r);
|
|
34
|
+
if (opts.focus !== false) {
|
|
35
|
+
if (!el.hasAttribute('tabindex')) el.setAttribute('tabindex', '-1');
|
|
36
|
+
(el.querySelector('[data-autofocus],[autofocus]') || focusables(el)[0] || el).focus({ preventScroll: true });
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const drop = (r, silent) => {
|
|
40
|
+
const i = stack.indexOf(r); if (i >= 0) stack.splice(i, 1);
|
|
41
|
+
reg.delete(r.el);
|
|
42
|
+
if (r.cleanup) r.cleanup();
|
|
43
|
+
if (!stack.length) document.removeEventListener('keydown', onKey);
|
|
44
|
+
const p = r.prev;
|
|
45
|
+
const a = document.activeElement;
|
|
46
|
+
if (!silent && r.opts.focus !== false && p && p.isConnected && typeof p.focus === 'function' && (!a || a === document.body || r.el.contains(a))) p.focus({ preventScroll: true });
|
|
47
|
+
};
|
|
48
|
+
const sweep = () => { for (const r of [...stack]) if (!r.el.isConnected) drop(r); };
|
|
49
|
+
// opts: { mount(el, getOpts) -> cleanup fn (runs once per open), onClose, onKey(e) (return true to skip default handling), focus: false to disable focus handling }
|
|
50
|
+
export const trap = (opts = {}) => el => {
|
|
51
|
+
if (typeof document === 'undefined') return;
|
|
52
|
+
if (!el) return queueMicrotask(sweep);
|
|
53
|
+
const r = reg.get(el);
|
|
54
|
+
if (r) r.opts = opts; else whenConnected(el, opts, mount);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Lifecycle-only ref (no keys/focus): mount(el, getOpts) runs once when the element appears and returns a cleanup run when it goes away.
|
|
58
|
+
const lreg = new Map();
|
|
59
|
+
const lsweep = () => { for (const [el, r] of [...lreg]) if (!el.isConnected) { lreg.delete(el); if (r.cleanup) r.cleanup(); } };
|
|
60
|
+
export const life = opts => el => {
|
|
61
|
+
if (typeof document === 'undefined') return;
|
|
62
|
+
if (!el) return queueMicrotask(lsweep);
|
|
63
|
+
const r = lreg.get(el);
|
|
64
|
+
if (r) r.opts = opts; else whenConnected(el, opts, (e, o) => { const n = { opts: o }; lreg.set(e, n); if (o.mount) n.cleanup = o.mount(e, () => n.opts); });
|
|
65
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { components as base, fmt, setIconBase } from './components.js';
|
|
2
|
+
import { more } from './components2.js';
|
|
3
|
+
import { more3 } from './components3.js';
|
|
4
|
+
import { more4, rules } from './components4.js';
|
|
5
|
+
import { pages } from './pages.js';
|
|
6
|
+
import { more6 } from './components6.js';
|
|
7
|
+
import { more7 } from './components7.js';
|
|
8
|
+
export const components = { ...base, ...more, ...more3, ...more4, ...more6, ...more7, ...pages };
|
|
9
|
+
export { fmt, setIconBase, rules };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// @ziyarago/ui · page templates — real slot contracts. Put inside <UiAppShell>, fill only the slots you need.
|
|
2
|
+
// Vue: <UiPageDashboard title="Dashboard"><template #kpis>…</template></UiPageDashboard>
|
|
3
|
+
// React: <UiPageDashboard title="Dashboard" kpis={<…/>} />
|
|
4
|
+
import { cx } from './components.js';
|
|
5
|
+
|
|
6
|
+
const region = (h, id, kids) => (kids == null || (Array.isArray(kids) && !kids.length)) ? null : h('div', { key: id, class: 'zg-pg__r', 'data-slot': id }, kids);
|
|
7
|
+
|
|
8
|
+
const draw = (h, node, ctx) => {
|
|
9
|
+
if (typeof node === 'string') return node === 'header' ? ctx.header() : region(h, node, ctx.slots[node]);
|
|
10
|
+
const [kind, ...a] = node, go = n => draw(h, n, ctx);
|
|
11
|
+
if (kind === 'col') return h('div', { class: 'zg-pg' }, ...a.map(go));
|
|
12
|
+
if (kind === 'auto') { const min = a.shift(); return h('div', { class: 'zg-pg__auto', style: { '--min': min + 'px' } }, ...a.map(go)); }
|
|
13
|
+
if (kind === 'two') return h('div', { class: 'zg-pg__two' }, h('div', { class: 'zg-pg__main' }, ...a[0].map(go)), h('div', { class: 'zg-pg__side' }, ...a[1].map(go)));
|
|
14
|
+
if (kind === 'wrap') return h('div', { class: 'zg-pg__two' }, h('div', { class: 'zg-pg__aside', style: { flexBasis: (a[2] || 300) + 'px' } }, ...a[0].map(go)), h('div', { class: 'zg-pg__main' }, ...a[1].map(go)));
|
|
15
|
+
return null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const page = (name, slots, tree) => [name, {
|
|
19
|
+
slots,
|
|
20
|
+
render: ({ C, h, props: p, slots: s }) => {
|
|
21
|
+
const header = () => s.header ? region(h, 'header', s.header) : (p.title ? h('div', { class: 'zg-pg__r', 'data-slot': 'header' }, h('div', { class: 'zg-page-head' }, h('div', { style: { flex: 1, minWidth: 0 } }, p.crumbs ? h(C.Breadcrumbs, { items: p.crumbs }) : null, h('h1', null, p.title), p.subtitle ? h('p', null, p.subtitle) : null), s.actions || null)) : (s.actions ? h('div', { class: 'zg-pg__r', 'data-slot': 'header' }, h('div', { class: 'zg-page-head', style: { justifyContent: 'flex-end' } }, s.actions)) : null));
|
|
22
|
+
return h('div', { class: 'zg-pgroot' }, draw(h, tree, { slots: s, header }));
|
|
23
|
+
}
|
|
24
|
+
}];
|
|
25
|
+
|
|
26
|
+
const T = Object.fromEntries([
|
|
27
|
+
page('PageDashboard', ['header', 'hero', 'kpis', 'occupancy', 'funnel', 'top', 'activity'], ['col', 'header', ['auto', 340, 'hero', 'kpis'], ['auto', 360, 'occupancy', 'funnel'], ['auto', 360, 'top', 'activity']]),
|
|
28
|
+
page('PagePatterns', ['header', 'hero', 'strip', 'statlines', 'chartcard', 'tiles', 'listrows', 'countries', 'versus', 'segbar', 'activity', 'actioncard'], ['col', 'header', ['auto', 340, 'hero', 'strip'], 'statlines', ['auto', 360, 'chartcard', 'tiles'], ['auto', 320, 'listrows', 'countries', 'versus'], 'segbar', ['auto', 360, 'activity', 'actioncard']]),
|
|
29
|
+
page('PageCrm', ['header', 'summary', 'filters', 'board', 'calls'], ['col', 'header', 'summary', 'filters', 'board', 'calls']),
|
|
30
|
+
page('PageBookings', ['header', 'stats', 'toolbar', 'bulk', 'table', 'pager'], ['col', 'header', 'stats', 'toolbar', 'bulk', 'table', 'pager']),
|
|
31
|
+
page('PageCatalog', ['header', 'filters', 'toolbar', 'grid', 'pager'], ['col', 'header', ['wrap', ['filters'], ['toolbar', 'grid', 'pager'], 260]]),
|
|
32
|
+
page('PageDetail', ['header', 'gallery', 'summary', 'tabs', 'history', 'booking'], ['col', 'header', ['two', ['gallery', 'summary', 'tabs', 'history'], ['booking']]]),
|
|
33
|
+
page('PageSettings', ['header', 'subnav', 'profile', 'notifications', 'integrations', 'danger'], ['col', 'header', ['wrap', ['subnav'], ['profile', 'notifications', 'integrations', 'danger'], 220]]),
|
|
34
|
+
page('PageUserList', ['header', 'stats', 'filters', 'toolbar', 'table', 'drawer'], ['col', 'header', 'stats', ['two', ['filters', 'toolbar', 'table'], ['drawer']]]),
|
|
35
|
+
page('PageProfile', ['header', 'profile', 'tabs', 'about', 'activity', 'plan'], ['col', 'header', 'profile', 'tabs', ['wrap', ['about'], ['activity', 'plan'], 320]]),
|
|
36
|
+
page('PageForm', ['header', 'main', 'media', 'side', 'actions'], ['col', 'header', ['two', ['main', 'media'], ['side']]]),
|
|
37
|
+
page('PageOrder', ['header', 'status', 'items', 'shipping', 'side'], ['col', 'header', 'status', ['two', ['items', 'shipping'], ['side']]]),
|
|
38
|
+
page('PageAccount', ['header', 'tabs', 'panel'], ['col', 'header', 'tabs', 'panel']),
|
|
39
|
+
page('PagePricing', ['header', 'toggle', 'plans', 'faq'], ['col', 'header', 'toggle', 'plans', 'faq']),
|
|
40
|
+
page('PageFaq', ['header', 'search', 'cats', 'answers', 'contact'], ['col', 'header', 'search', ['wrap', ['cats'], ['answers'], 200], 'contact'])
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
T.PageAuth = { slots: ['background', 'logo', 'aside', 'form', 'footer'], render: ({ C, h, props: p, slots: s }) => {
|
|
44
|
+
const body = [s.form || null, s.footer ? h('div', { class: 'zg-pg__r', 'data-slot': 'footer' }, s.footer) : null];
|
|
45
|
+
return p.layout === 'split'
|
|
46
|
+
? h(C.AuthSplit, { title: p.title, subtitle: p.subtitle, logo: s.logo, aside: s.aside }, ...body)
|
|
47
|
+
: h('div', { class: 'zg-pgauth' }, s.background ? h('div', { class: 'zg-pg__r zg-pgauth__bg', 'data-slot': 'background' }, s.background) : null, h('div', { class: 'zg-pgauth__c' }, h(C.AuthLayout, { title: p.title, subtitle: p.subtitle, logo: s.logo }, ...body)));
|
|
48
|
+
} };
|
|
49
|
+
T.PageStatus = { slots: ['code', 'message', 'action'], render: ({ C, h, props: p, slots: s }) => h(C.StatusPage, { kind: p.kind, title: p.title, description: p.description, onAction: p.onAction, actionLabel: p.actionLabel, notifyLabel: p.notifyLabel, emailLabel: p.emailLabel, onNotify: p.onNotify, codeNode: s.code || undefined, messageNode: s.message || undefined, actionNode: s.action || undefined }) };
|
|
50
|
+
|
|
51
|
+
export const pages = T;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Component API metadata
|
|
2
|
+
|
|
3
|
+
One file per group (`typography`, `layout`, `actions`, `forms`, `data`, `feedback`, `overlay`, `navigation`, `media`, `domain`, `pages`). They are the single source of truth for the docs props tables, `dist/manifest.json`, `llms.txt` and the MCP server.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
export default {
|
|
7
|
+
Button: {
|
|
8
|
+
description: 'One sentence: what it is and when to use it.',
|
|
9
|
+
props: {
|
|
10
|
+
variant: { type: "'primary' | 'secondary' | 'ghost' | 'danger' | 'inverse' | 'tonal' | 'outlined' | 'text' | 'elevated'", values: ['primary', 'secondary', ...], default: 'secondary', description: 'Visual emphasis.' },
|
|
11
|
+
disabled: { type: 'boolean', default: false, description: 'Blocks interaction and dims the button.' },
|
|
12
|
+
modelValue: { type: 'string', model: true, description: 'Controlled value. Vue v-model, React modelValue + onChange.' },
|
|
13
|
+
},
|
|
14
|
+
slots: { default: 'Button label.', actions: 'Right side of the header.' }, // Vue slot name = React prop / children
|
|
15
|
+
events: { change: 'Called with the new value.', click: 'Native click.' }, // React: onChange / onClick
|
|
16
|
+
notes: ['Optional: gotchas, accessibility, composition tips, 1 sentence each.'],
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Rules
|
|
22
|
+
- Document every prop the render code reads (`node scripts/props-extract.mjs Name` lists them; `onXxx` are events, put them in `events`). Do not invent props the code does not read.
|
|
23
|
+
- Enums MUST list every allowed value in `values` and show them in `type`; give the real `default` (look at the `||`/`??` fallback in the code).
|
|
24
|
+
- `type` is TypeScript style: `string`, `number`, `boolean`, `string[]`, `{ value: string; label: string }[]`, `'a' | 'b'`, `VNode`.
|
|
25
|
+
- Props shared by many components (`size`) are documented on each component that supports it: `size: { type: "'xs' | 'sm' | 'md' | 'lg' | 'xl'", values: [...], default: 'md' }`.
|
|
26
|
+
- Page templates: document each slot with what it expects (see `site/docs-data/templates.ts`).
|
|
27
|
+
- English, short, no filler.
|
|
28
|
+
|
|
29
|
+
Check with `node scripts/check-meta.mjs`.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// API metadata for the actions group. Format: see src/meta/README.md
|
|
2
|
+
const SIZES = ['xs', 'sm', 'md', 'lg', 'xl'];
|
|
3
|
+
const sizeProp = (def = 'md', desc = 'Component size.') => ({ type: "'xs' | 'sm' | 'md' | 'lg' | 'xl'", values: SIZES, default: def, description: desc });
|
|
4
|
+
const TONES = ['brand', 'sky', 'success', 'warning', 'danger', 'neutral'];
|
|
5
|
+
const toneType = "'brand' | 'sky' | 'success' | 'warning' | 'danger' | 'neutral'";
|
|
6
|
+
const icon = (desc = 'Icon name.') => ({ type: 'string', description: desc });
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
Button: {
|
|
10
|
+
description: 'Primary action control with visual variants, tones, sizes, icons and a loading state.',
|
|
11
|
+
props: {
|
|
12
|
+
variant: { type: "'primary' | 'secondary' | 'ghost' | 'danger' | 'inverse' | 'tonal' | 'outlined' | 'text' | 'elevated'", values: ['primary', 'secondary', 'ghost', 'danger', 'inverse', 'tonal', 'outlined', 'text', 'elevated'], default: 'secondary', description: 'Visual emphasis. primary is the main call to action, danger is destructive, inverse is for dark surfaces.' },
|
|
13
|
+
tone: { type: toneType, values: TONES, description: 'Accent color applied to tonal, outlined, text and elevated variants.' },
|
|
14
|
+
size: sizeProp('md', 'Button height: xs 28px, sm 32px, md 40px, lg 48px, xl 56px.'),
|
|
15
|
+
icon: icon('Icon shown before the label.'),
|
|
16
|
+
iconRight: icon('Icon shown after the label.'),
|
|
17
|
+
loading: { type: 'boolean', default: false, description: 'Shows a spinner instead of the icon and disables the button.' },
|
|
18
|
+
disabled: { type: 'boolean', default: false, description: 'Blocks interaction and dims the button.' },
|
|
19
|
+
block: { type: 'boolean', default: false, description: 'Stretches the button to the full width of its container.' },
|
|
20
|
+
},
|
|
21
|
+
events: { click: 'Native click event. Not fired while disabled or loading.' },
|
|
22
|
+
slots: { default: 'Button label.' },
|
|
23
|
+
notes: ['Always renders type="button", so it never submits a form by itself.'],
|
|
24
|
+
},
|
|
25
|
+
IconButton: {
|
|
26
|
+
description: 'Square button with only an icon; use for toolbars and compact actions.',
|
|
27
|
+
props: {
|
|
28
|
+
icon: { type: 'string', required: true, description: 'Icon name.' },
|
|
29
|
+
label: { type: 'string', required: true, description: 'Accessible name (aria-label). Required because there is no visible text.' },
|
|
30
|
+
size: sizeProp('md', 'Button size: xs 28px, sm 32px, md 40px, lg 48px, xl 56px.'),
|
|
31
|
+
ghost: { type: 'boolean', default: false, description: 'Transparent background until hover.' },
|
|
32
|
+
},
|
|
33
|
+
events: { click: 'Native click event.' },
|
|
34
|
+
},
|
|
35
|
+
Pressable: {
|
|
36
|
+
description: 'Unstyled pressable wrapper that makes any content a button with press feedback.',
|
|
37
|
+
props: {
|
|
38
|
+
as: { type: 'string', default: 'button', description: 'HTML tag. type="button" is set only when this is button.' },
|
|
39
|
+
label: { type: 'string', description: 'Accessible name (aria-label).' },
|
|
40
|
+
disabled: { type: 'boolean', default: false, description: 'Blocks interaction.' },
|
|
41
|
+
},
|
|
42
|
+
events: { click: 'Native click event.' },
|
|
43
|
+
slots: { default: 'Pressable content.' },
|
|
44
|
+
notes: ['For a non-button tag add role and keyboard handling yourself.'],
|
|
45
|
+
},
|
|
46
|
+
Fab: {
|
|
47
|
+
description: 'Floating action button for the main action of a screen; extended when a label is given.',
|
|
48
|
+
props: {
|
|
49
|
+
icon: { type: 'string', default: 'plus', description: 'Icon name.' },
|
|
50
|
+
label: { type: 'string', description: 'Text next to the icon (extended FAB) and the accessible name.' },
|
|
51
|
+
size: sizeProp('md', 'Button height: xs 36px, sm 44px, md 56px, lg 64px, xl 72px.'),
|
|
52
|
+
},
|
|
53
|
+
events: { click: 'Native click event.' },
|
|
54
|
+
notes: ['Always pass label: it is the accessible name. Without it the button falls back to the icon name as aria-label (for example "plus"), which is rarely a good description.'],
|
|
55
|
+
},
|
|
56
|
+
CopyButton: {
|
|
57
|
+
description: 'Button that copies a value to the clipboard and briefly confirms with a check icon, or reports a failure.',
|
|
58
|
+
props: {
|
|
59
|
+
value: { type: 'string', required: true, description: 'Text copied to the clipboard.' },
|
|
60
|
+
label: { type: 'string', description: 'Button text. Defaults to "Copy", then "Copied" for 1.5s after a click.' },
|
|
61
|
+
variant: { type: "'primary' | 'secondary' | 'ghost' | 'danger' | 'inverse' | 'tonal' | 'outlined' | 'text' | 'elevated'", values: ['primary', 'secondary', 'ghost', 'danger', 'inverse', 'tonal', 'outlined', 'text', 'elevated'], default: 'secondary', description: 'Button variant, same values as Button.' },
|
|
62
|
+
size: sizeProp('sm', 'Button size.'),
|
|
63
|
+
},
|
|
64
|
+
events: { copy: 'Called with the value after it was written to the clipboard.', error: 'Called with the error when the clipboard is unavailable or rejects the write. The button turns red with "Copy failed" for 1.5s.' },
|
|
65
|
+
notes: ['Needs a secure context (https or localhost) for navigator.clipboard. The result ("Copied" or "Copy failed") is announced through an aria-live region.'],
|
|
66
|
+
},
|
|
67
|
+
ToggleGroup: {
|
|
68
|
+
description: 'Group of toggle buttons with single or multiple selection, optionally icon-only.',
|
|
69
|
+
props: {
|
|
70
|
+
options: { type: '{ value: string; label: string; icon?: string }[]', default: '[]', required: true, description: 'Buttons to render. label is also used as the tooltip.' },
|
|
71
|
+
modelValue: { type: 'string | string[]', model: true, description: 'Selected value, or array of values when multiple.' },
|
|
72
|
+
multiple: { type: 'boolean', default: false, description: 'Allows several pressed buttons; modelValue becomes an array.' },
|
|
73
|
+
iconOnly: { type: 'boolean', default: false, description: 'Hides labels and shows only icons.' },
|
|
74
|
+
allowEmpty: { type: 'boolean', default: false, description: 'Single mode only: clicking the pressed button deselects it and emits null.' },
|
|
75
|
+
size: sizeProp('md', 'Control size.'),
|
|
76
|
+
},
|
|
77
|
+
events: { change: 'Called with the new value (string), null when deselected with allowEmpty, or the new array when multiple.' },
|
|
78
|
+
notes: ['Each button exposes aria-pressed. In single mode clicking the active option keeps it selected.'],
|
|
79
|
+
},
|
|
80
|
+
SegmentedControl: {
|
|
81
|
+
description: 'Segmented switch for choosing exactly one of a few options.',
|
|
82
|
+
props: {
|
|
83
|
+
items: { type: '{ value: string; label: string }[]', default: '[]', required: true, description: 'Segments to render.' },
|
|
84
|
+
modelValue: { type: 'string', model: true, description: 'Value of the selected segment.' },
|
|
85
|
+
size: sizeProp('md', 'Control size. sm and md also have a dedicated compact style.'),
|
|
86
|
+
},
|
|
87
|
+
events: { change: 'Called with the value of the clicked segment.' },
|
|
88
|
+
},
|
|
89
|
+
FilterChips: {
|
|
90
|
+
description: 'Row of selectable filter chips with single or multiple selection.',
|
|
91
|
+
props: {
|
|
92
|
+
items: { type: '{ value: string; label: string }[]', default: '[]', required: true, description: 'Chips to render.' },
|
|
93
|
+
modelValue: { type: 'string | string[]', model: true, description: 'Selected value, or array of values when multi.' },
|
|
94
|
+
multi: { type: 'boolean', default: false, description: 'Allows several selected chips; modelValue becomes an array.' },
|
|
95
|
+
size: sizeProp('md', 'Chip size.'),
|
|
96
|
+
},
|
|
97
|
+
events: { change: 'Called with the new value, or the new array when multi. Toggles chips off in multi mode only.' },
|
|
98
|
+
},
|
|
99
|
+
Chip: {
|
|
100
|
+
description: 'Compact label with optional icon and remove button; use for statuses, filters and attributes.',
|
|
101
|
+
props: {
|
|
102
|
+
variant: { type: "'tonal' | 'outlined' | 'solid'", values: ['tonal', 'outlined', 'solid'], default: 'tonal', description: 'Visual style.' },
|
|
103
|
+
tone: { type: toneType, values: TONES, default: 'brand', description: 'Accent color. Unknown values fall back to brand.' },
|
|
104
|
+
size: sizeProp('md', 'Chip height: xs 22px, sm 26px, md 32px, lg 38px, xl 46px.'),
|
|
105
|
+
pill: { type: 'boolean', default: false, description: 'Fully rounded corners.' },
|
|
106
|
+
icon: icon('Icon shown before the label.'),
|
|
107
|
+
},
|
|
108
|
+
events: { close: 'Canonical. Called when the remove button is clicked. The button is shown only when close or remove is provided.', remove: 'Alias of close, matching Tag.' },
|
|
109
|
+
slots: { default: 'Chip label.' },
|
|
110
|
+
},
|
|
111
|
+
Tag: {
|
|
112
|
+
description: 'Small pill label, optionally removable; use for keywords and selected values.',
|
|
113
|
+
props: {
|
|
114
|
+
size: sizeProp('md', 'Tag height: xs 22px, sm 24px, md 28px, lg 34px, xl 40px.'),
|
|
115
|
+
},
|
|
116
|
+
events: { remove: 'Called when the x button is clicked. The button is shown only when remove or close is provided.', close: 'Alias of remove, matching Chip. Use one name across both components in your app (close is the canonical one).' },
|
|
117
|
+
slots: { default: 'Tag label.' },
|
|
118
|
+
},
|
|
119
|
+
Badge: {
|
|
120
|
+
description: 'Status badge with a colored tone and optional dot.',
|
|
121
|
+
props: {
|
|
122
|
+
tone: { type: "'brand' | 'success' | 'warning' | 'danger' | 'info'", values: ['brand', 'success', 'warning', 'danger', 'info'], description: 'Color. Omit for the neutral style.' },
|
|
123
|
+
dot: { type: 'boolean', default: false, description: 'Shows a status dot before the content.' },
|
|
124
|
+
size: sizeProp('md', 'Badge height: xs 18px, sm 20px, md 24px, lg 28px, xl 32px.'),
|
|
125
|
+
},
|
|
126
|
+
slots: { default: 'Badge content.' },
|
|
127
|
+
notes: ['Badge uses info for blue; Chip and Button use the tone name sky instead.'],
|
|
128
|
+
},
|
|
129
|
+
ActionRail: {
|
|
130
|
+
description: 'Floating vertical or horizontal rail of icon buttons with a highlighted active item.',
|
|
131
|
+
props: {
|
|
132
|
+
items: { type: '{ value?: string; icon?: string; label?: string; separator?: boolean }[]', default: '[]', required: true, description: 'Rail entries. An item with separator: true renders a divider instead of a button.' },
|
|
133
|
+
value: { type: 'string', description: 'Value of the active item. Changed via the change event.' },
|
|
134
|
+
size: sizeProp('md', 'Button size inside the rail.'),
|
|
135
|
+
},
|
|
136
|
+
events: { change: 'Called with the value of the clicked item.' },
|
|
137
|
+
notes: ['Items other than the active one are shown as ghost buttons; label is the accessible name.'],
|
|
138
|
+
},
|
|
139
|
+
ActionCard: {
|
|
140
|
+
description: 'Card with a title and built-in collapse, refresh and remove controls.',
|
|
141
|
+
props: {
|
|
142
|
+
title: { type: 'string', required: true, description: 'Card title.' },
|
|
143
|
+
subtitle: { type: 'string', description: 'Text under the title.' },
|
|
144
|
+
collapsed: { type: 'boolean', description: 'Controlled collapsed state. When omitted the card keeps it internally.' },
|
|
145
|
+
loading: { type: 'boolean', description: 'Controlled refresh spinner. When omitted, Refresh shows the spinner for 1.8s by itself.' },
|
|
146
|
+
size: sizeProp('md', 'Card padding and font size.'),
|
|
147
|
+
},
|
|
148
|
+
events: { collapse: 'Called with the new collapsed boolean when the collapse button is clicked.', refresh: 'Refresh button clicked. Pass loading to show your own spinner.', remove: 'Remove button clicked; the card hides itself afterwards.' },
|
|
149
|
+
slots: { default: 'Card body. Hidden while collapsed.' },
|
|
150
|
+
notes: ['With collapsed set, update it from the collapse event, otherwise the card will not toggle.', 'Removing hides the card until the component is remounted; also remove it from your data on the remove event.'],
|
|
151
|
+
},
|
|
152
|
+
NotificationBell: {
|
|
153
|
+
description: 'Bell icon button with an unread counter and a dropdown list of notifications.',
|
|
154
|
+
props: {
|
|
155
|
+
items: { type: '{ id: string | number; title: string; message?: string; time?: string; read?: boolean }[]', default: '[]', description: 'Notifications. Items without read: true count as unread and show a dot.' },
|
|
156
|
+
size: sizeProp('md', 'Bell button size.'),
|
|
157
|
+
},
|
|
158
|
+
events: { read: 'Called with the item id when an unread item is clicked.', markAllRead: 'Called when "Mark all read" is clicked (shown while something is unread).', select: 'Called with the item when any item is clicked.' },
|
|
159
|
+
notes: ['If you handle read or markAllRead the component is controlled: it trusts only the read flags in items, so update them in your state. Without these handlers it marks items read internally.', 'The dropdown opens and closes internally, and closes on outside click or Escape.'],
|
|
160
|
+
},
|
|
161
|
+
};
|