@muten/shadcn 0.1.14 → 0.2.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/AGENTS.md +88 -1
- package/globals.css +1 -0
- package/kit.css +329 -124
- package/motion.css +3 -0
- package/package.json +1 -1
- package/registry/accordion.muten +3 -3
- package/registry/agenda.js +19 -16
- package/registry/agenda.muten +10 -10
- package/registry/alert-dialog.muten +2 -2
- package/registry/alert.muten +1 -1
- package/registry/app-shell.muten +1 -1
- package/registry/attachment.muten +1 -1
- package/registry/avatar.muten +4 -4
- package/registry/badge.muten +14 -3
- package/registry/band.muten +2 -2
- package/registry/bar-chart.muten +2 -2
- package/registry/bio-play.muten +2 -2
- package/registry/booking-play.muten +5 -5
- package/registry/booking.muten +2 -2
- package/registry/breadcrumb.muten +4 -4
- package/registry/button.muten +9 -7
- package/registry/calendar.js +15 -10
- package/registry/calendar.muten +1 -1
- package/registry/card.muten +1 -1
- package/registry/carousel.muten +1 -1
- package/registry/chat-play.muten +8 -8
- package/registry/chatbot.muten +5 -4
- package/registry/checklist.muten +1 -1
- package/registry/choice-list.muten +3 -3
- package/registry/code.js +10 -4
- package/registry/collapsible.muten +2 -2
- package/registry/color-picker.js +37 -36
- package/registry/color-picker.muten +1 -1
- package/registry/combobox.js +1 -1
- package/registry/combobox.muten +2 -2
- package/registry/command-palette.muten +2 -2
- package/registry/command.js +1 -1
- package/registry/command.muten +7 -7
- package/registry/context-menu.muten +3 -3
- package/registry/count-up.js +33 -31
- package/registry/count-up.muten +6 -4
- package/registry/date-field.js +1 -1
- package/registry/date-field.muten +3 -3
- package/registry/day-plan.muten +5 -5
- package/registry/device-frame.muten +3 -3
- package/registry/dialog.muten +1 -1
- package/registry/dirty-bar.js +2 -2
- package/registry/dirty-bar.muten +3 -3
- package/registry/drawer.muten +1 -1
- package/registry/dropdown-menu.muten +4 -4
- package/registry/empty.muten +1 -1
- package/registry/feature-grid.muten +1 -1
- package/registry/feature-rows.muten +2 -2
- package/registry/field-behavior.js +13 -3
- package/registry/field-behavior.muten +3 -3
- package/registry/field.muten +5 -5
- package/registry/file-input.js +8 -4
- package/registry/file-input.muten +3 -3
- package/registry/fixed-card.muten +4 -4
- package/registry/folder-picker.js +6 -2
- package/registry/folder-picker.muten +2 -2
- package/registry/heatmap.muten +5 -5
- package/registry/image-upload.js +1 -1
- package/registry/image-upload.muten +2 -2
- package/registry/inbox-play.muten +5 -5
- package/registry/indicator.muten +1 -1
- package/registry/info-table.muten +1 -1
- package/registry/input-group.muten +11 -11
- package/registry/input-otp.muten +2 -2
- package/registry/input.muten +1 -1
- package/registry/inspector.muten +2 -2
- package/registry/item.muten +2 -2
- package/registry/kit/board.js +36 -34
- package/registry/kit/collections.js +24 -24
- package/registry/kit/core.js +197 -114
- package/registry/kit/dates.js +29 -27
- package/registry/kit/i18n.js +172 -0
- package/registry/kit/menus.js +14 -13
- package/registry/kit/overlays.js +7 -2
- package/registry/kit/structure.js +66 -61
- package/registry/kit/table.js +19 -17
- package/registry/kit/values.js +36 -28
- package/registry/kit-skeleton.muten +1 -1
- package/registry/kit-widget.js +9 -4
- package/registry/lang-scope.js +8 -0
- package/registry/lang-scope.muten +5 -0
- package/registry/legal-doc.muten +5 -5
- package/registry/link-btn.muten +1 -1
- package/registry/list-motion.js +46 -0
- package/registry/list-motion.muten +4 -0
- package/registry/live-stack.muten +2 -2
- package/registry/logo-cloud.muten +2 -2
- package/registry/marketing.muten +7 -7
- package/registry/member-list.muten +2 -2
- package/registry/menubar.muten +3 -3
- package/registry/message-scroller.muten +3 -3
- package/registry/message.muten +9 -9
- package/registry/messaging.muten +1 -1
- package/registry/modal.muten +4 -4
- package/registry/money-field.js +1 -1
- package/registry/money-field.muten +2 -2
- package/registry/nav-button.muten +4 -4
- package/registry/navigation-menu.muten +4 -4
- package/registry/navpill.muten +6 -6
- package/registry/new-appointment.js +1 -1
- package/registry/new-appointment.muten +4 -4
- package/registry/number-field.js +6 -4
- package/registry/number-field.muten +1 -1
- package/registry/order-board.js +1 -1
- package/registry/order-board.muten +2 -2
- package/registry/order-play.muten +4 -4
- package/registry/pagination-nav.js +1 -1
- package/registry/pagination-nav.muten +3 -3
- package/registry/pagination.muten +4 -4
- package/registry/password-field.muten +2 -2
- package/registry/permission-matrix.muten +4 -4
- package/registry/phone-bar.muten +6 -6
- package/registry/phone-field.js +1 -1
- package/registry/phone-field.muten +1 -1
- package/registry/phone-shell.js +1 -1
- package/registry/pill.muten +2 -2
- package/registry/plans.muten +1 -1
- package/registry/popover.muten +2 -2
- package/registry/pricing.muten +4 -4
- package/registry/product-play.muten +3 -3
- package/registry/progress.muten +3 -3
- package/registry/queue.muten +1 -1
- package/registry/radio-group.muten +1 -1
- package/registry/rank-item.muten +4 -4
- package/registry/resizable.muten +1 -1
- package/registry/row-menu.js +1 -1
- package/registry/row-menu.muten +2 -2
- package/registry/save-button.muten +5 -5
- package/registry/scale-slider.js +1 -1
- package/registry/scale-slider.muten +2 -2
- package/registry/screen-tour.muten +1 -1
- package/registry/share-bar.muten +2 -2
- package/registry/share-button.muten +4 -4
- package/registry/sheet.muten +1 -1
- package/registry/sidebar.muten +12 -6
- package/registry/site-footer.muten +6 -6
- package/registry/sortable-behavior.js +67 -20
- package/registry/sortable-list.js +3 -1
- package/registry/sortable-list.muten +2 -2
- package/registry/sortable.muten +5 -4
- package/registry/spinner.muten +2 -2
- package/registry/split-button.muten +2 -2
- package/registry/stacked-bars.js +3 -3
- package/registry/stacked-bars.muten +2 -2
- package/registry/stamp-card.muten +2 -2
- package/registry/stat-band.muten +3 -3
- package/registry/stat.muten +3 -3
- package/registry/status-bar.muten +2 -2
- package/registry/stepper.muten +4 -4
- package/registry/store-qr.js +1 -1
- package/registry/store-qr.muten +2 -2
- package/registry/swatch.muten +1 -1
- package/registry/switch.muten +8 -7
- package/registry/table-view.muten +3 -3
- package/registry/tabs-behavior.js +9 -1
- package/registry/tabs.muten +2 -2
- package/registry/tag-input.js +1 -1
- package/registry/tag-input.muten +2 -2
- package/registry/text.muten +2 -2
- package/registry/time-field.js +1 -1
- package/registry/time-field.muten +3 -3
- package/registry/timeline.muten +4 -4
- package/registry/toaster.muten +2 -2
- package/registry/toggle-group.muten +1 -1
- package/registry/toggle.muten +3 -3
- package/registry/toolbar.muten +4 -4
- package/registry/tooltip.muten +2 -2
- package/registry/tree-view.js +38 -25
- package/registry/tree-view.muten +1 -1
- package/registry/veil.muten +1 -1
- package/registry/virtual-list.js +1 -1
- package/registry/virtual-list.muten +2 -2
- package/registry/virtual-window.js +4 -4
- package/registry/virtual-window.muten +2 -2
- package/registry/wordline.muten +1 -1
- package/registry/zoom-control.muten +1 -1
- package/registry.json +22 -2
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// share Storio's share sheet inside the adaptive modal (link or product card, Copiar, brand targets, QR)
|
|
4
4
|
// appShell Sidebar (store switcher, groups, counts, person) + page header + detail Sheet + Command (⌘K)
|
|
5
5
|
// booking week strip + hours in one piece (dot = something left, closed days, a full day's empty state)
|
|
6
|
-
// wizard
|
|
6
|
+
// wizard “New appointment” in 4 steps inside the adaptive modal (Stepper, choice lists, day tiles, summary)
|
|
7
7
|
|
|
8
8
|
const list = (v) => (Array.isArray(v) ? v : []);
|
|
9
9
|
const esc = (s) => String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]);
|
|
@@ -11,17 +11,17 @@ const esc = (s) => String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<
|
|
|
11
11
|
// ── phone shell ──
|
|
12
12
|
// i: tabs [{label, icon, badge}], rows [{tab, section, title, subtitle, value}]; on.select(label)
|
|
13
13
|
export function phoneShell(root, i, core, on) {
|
|
14
|
-
const { $, $$ } = core;
|
|
14
|
+
const { $, $$ } = core, L = core.langOf(root), T = (k, ...a) => core.t(L, k, ...a);
|
|
15
15
|
const tabs = list(i.tabs), rows = list(i.rows);
|
|
16
16
|
root.innerHTML = `<div class="cx-device cx-ph-device" data-kind="phone"><div class="cx-device-screen"><div class="cx-ph-screen">
|
|
17
17
|
<div class="cx-ph-status"><span>9:41</span><span class="cx-ph-island"></span><span>100 %</span></div>
|
|
18
18
|
<header class="cx-ph-head"><div class="cx-ph-bar">
|
|
19
|
-
<button data-slot="button" class="cn-button cn-button-variant-ghost cn-button-size-icon group/button" aria-label="
|
|
19
|
+
<button data-slot="button" class="cn-button cn-button-variant-ghost cn-button-size-icon group/button" aria-label="${T('Back')}" title="${T('Back')}"><svg><use href="#left"/></svg></button>
|
|
20
20
|
<b class="cx-ph-small"></b>
|
|
21
|
-
<div class="cx-hstack" style="gap:2px;width:auto;flex-wrap:nowrap"><button data-slot="button" class="cn-button cn-button-variant-ghost cn-button-size-icon group/button" aria-label="
|
|
21
|
+
<div class="cx-hstack" style="gap:2px;width:auto;flex-wrap:nowrap"><button data-slot="button" class="cn-button cn-button-variant-ghost cn-button-size-icon group/button" aria-label="${T('Search')}" title="${T('Search')}"><svg><use href="#search"/></svg></button><button data-slot="button" class="cn-button cn-button-variant-ghost cn-button-size-icon group/button" aria-label="${T('More options')}" title="${T('More options')}"><svg><use href="#more"/></svg></button></div>
|
|
22
22
|
</div></header>
|
|
23
23
|
<div class="cx-ph-body cx-scroll"></div>
|
|
24
|
-
<nav class="cx-ph-nav" aria-label="
|
|
24
|
+
<nav class="cx-ph-nav" aria-label="${T('Main')}">${tabs.map((t, k) => `<button class="cn-button cn-button-variant-ghost cx-size-tile cx-ph-tab" data-tab="${k}" ${k === 0 ? 'aria-current="page"' : ''}>${t.badge ? `<span class="cx-ph-ico"><svg><use href="#${t.icon}"/></svg><i class="cx-ph-badge">${esc(t.badge)}</i></span>` : `<svg><use href="#${t.icon}"/></svg>`}<span>${esc(t.label)}</span></button>`).join('')}</nav>
|
|
25
25
|
</div></div></div>`;
|
|
26
26
|
const head = $('.cx-ph-head', root), phBody = $('.cx-ph-body', root);
|
|
27
27
|
const screen = (label) => {
|
|
@@ -53,8 +53,8 @@ const enc = encodeURIComponent;
|
|
|
53
53
|
const targetsFor = (url, text) => [
|
|
54
54
|
{ name: 'QR', icon: 'qrc', plain: true, qr: true },
|
|
55
55
|
{ name: 'WhatsApp', icon: 'b-whatsapp', color: '#25D366', href: `https://wa.me/?text=${enc(text + ' ' + url)}` },
|
|
56
|
-
{ name: 'Instagram', icon: 'b-instagram', color: '#E4405F', copy: '
|
|
57
|
-
{ name: 'TikTok', icon: 'b-tiktok', color: '#000000', copy: '
|
|
56
|
+
{ name: 'Instagram', icon: 'b-instagram', color: '#E4405F', copy: 'Link copied. Paste it in your bio or in an Instagram story.' },
|
|
57
|
+
{ name: 'TikTok', icon: 'b-tiktok', color: '#000000', copy: 'Link copied. Paste it in your TikTok bio.' },
|
|
58
58
|
{ name: 'Facebook', icon: 'b-facebook', color: '#0866FF', href: `https://www.facebook.com/sharer/sharer.php?u=${enc(url)}` },
|
|
59
59
|
{ name: 'X', icon: 'b-x', color: '#000000', href: `https://x.com/intent/post?text=${enc(text)}&url=${enc(url)}` },
|
|
60
60
|
{ name: 'Telegram', icon: 'b-telegram', color: '#26A5E4', href: `https://t.me/share/url?url=${enc(url)}&text=${enc(text)}` },
|
|
@@ -68,17 +68,17 @@ const dragScroll = (row) => {
|
|
|
68
68
|
addEventListener('pointerup', () => { if (!d) return; const moved = d.moved; d = null; row.classList.remove('dragging'); if (moved) row.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); }, { capture: true, once: true }); });
|
|
69
69
|
};
|
|
70
70
|
export function share(root, i, core) {
|
|
71
|
-
const { ui, $, openModal, CONTENT, body, foot, mdl, easeHeight, money0 } = core;
|
|
71
|
+
const { ui, $, openModal, CONTENT, body, foot, mdl, easeHeight, money0 } = core, L = core.langOf(root), T = (k, ...a) => core.t(L, k, ...a);
|
|
72
72
|
const product = i.kind === 'product' && i.product ? i.product : null;
|
|
73
73
|
const url = /^https?:\/\//.test(i.url || '') ? i.url : `https://${i.url || ''}`;
|
|
74
74
|
const shown = url.replace(/^https?:\/\//, '');
|
|
75
|
-
const text = i.text || (product ?
|
|
76
|
-
const title = product ? '
|
|
75
|
+
const text = i.text || (product ? T('{0} at {1}', product.name, i.name) : T('Book at {0}', i.name));
|
|
76
|
+
const title = product ? T('Share product') : T('Share link');
|
|
77
77
|
root.innerHTML = ui.button({ label: `${i.icon === 'none' ? '' : ui.icon('share')}${esc(i.label || 'Compartir')}`, variant: i.variant || 'default', size: 'default' });
|
|
78
78
|
const msg = (t) => { const m = $('.cx-sh-msg', body); if (m) m.textContent = t; };
|
|
79
79
|
const copy = (ok) => {
|
|
80
|
-
const fb = () => { const u = $('.cx-sh-url', body); if (u) getSelection().selectAllChildren(u); msg('
|
|
81
|
-
try { navigator.clipboard.writeText(url).then(() => { msg(ok); const b = $('.cx-sh-copybtn', body); if (b) { b.
|
|
80
|
+
const fb = () => { const u = $('.cx-sh-url', body); if (u) getSelection().selectAllChildren(u); msg(T('Select the link and copy it.')); };
|
|
81
|
+
try { navigator.clipboard.writeText(url).then(() => { msg(ok); const b = $('.cx-sh-copybtn', body); if (b) { b.innerHTML = `<span class="cx-swap-in">${T('Copied')}</span>`; setTimeout(() => { b.innerHTML = `<span class="cx-swap-in">${T('Copy')}</span>`; }, 1600); } }, fb); } catch { fb(); }
|
|
82
82
|
};
|
|
83
83
|
const main = () => {
|
|
84
84
|
$('#mdl-title').textContent = title;
|
|
@@ -86,18 +86,18 @@ export function share(root, i, core) {
|
|
|
86
86
|
? `<div class="cx-sh-prod" style="--g:${esc(product.image || 'var(--muted)')}"><span class="cx-sh-tag">${money0(Number(product.price) || 0)}</span><div class="cx-sh-pinfo"><b>${esc(product.name)}</b><span>${esc(product.meta)}</span></div></div>`
|
|
87
87
|
: `<div class="cx-sh-card"><span class="cx-sh-av">${esc(i.initials)}</span><b>${esc(i.name)}</b><span>${esc(shown)}</span></div>`;
|
|
88
88
|
body.innerHTML = `<div class="cx-sh">${card}
|
|
89
|
-
<div class="cx-sh-copy"><span class="cx-sh-url cx-num">${esc(shown)}</span><button class="cn-button cn-button-variant-default cn-button-size-sm cx-sh-copybtn" data-sh-copy
|
|
90
|
-
<span class="cx-sh-lbl"
|
|
89
|
+
<div class="cx-sh-copy"><span class="cx-sh-url cx-num">${esc(shown)}</span><button class="cn-button cn-button-variant-default cn-button-size-sm cx-sh-copybtn" data-sh-copy>${T('Copy')}</button></div>
|
|
90
|
+
<span class="cx-sh-lbl">${T('Share on')}</span>
|
|
91
91
|
<div class="cx-sh-targets">${targetsFor(url, text).map((t, k) => { const ico = `<span class="cx-sh-ic${t.plain ? ' plain' : ''}" style="${t.color ? `--brand:${t.color}` : ''}"><svg><use href="#${t.icon}"/></svg></span><span class="cx-sh-l">${t.name}</span>`; return t.href ? `<a class="cn-button cn-button-variant-ghost cx-size-tile-lg cx-sh-tg" href="${t.href}" target="_blank" rel="noopener" data-t="${k}">${ico}</a>` : `<button class="cn-button cn-button-variant-ghost cx-size-tile-lg cx-sh-tg" data-t="${k}">${ico}</button>`; }).join('')}</div>
|
|
92
92
|
<p class="cx-note cx-sh-msg" aria-live="polite" style="text-align:left"></p></div>`;
|
|
93
93
|
const row = $('.cx-sh-targets', body); dragScroll(row);
|
|
94
94
|
const edge = () => { row.toggleAttribute('data-end', row.scrollLeft >= row.scrollWidth - row.clientWidth - 2); row.toggleAttribute('data-scrolled', row.scrollLeft > 2); };
|
|
95
95
|
row.addEventListener('scroll', edge, { passive: true }); requestAnimationFrame(edge);
|
|
96
|
-
row.addEventListener('click', (e) => { const t = e.target.closest('[data-t]'); if (!t) return; const x = targetsFor(url, text)[+t.dataset.t]; if (x.copy) copy(x.copy); if (x.qr) easeHeight(mdl, qr); });
|
|
96
|
+
row.addEventListener('click', (e) => { const t = e.target.closest('[data-t]'); if (!t) return; const x = targetsFor(url, text)[+t.dataset.t]; if (x.copy) copy(T(x.copy)); if (x.qr) easeHeight(mdl, qr); });
|
|
97
97
|
};
|
|
98
98
|
const qr = () => {
|
|
99
|
-
$('#mdl-title').textContent = '
|
|
100
|
-
body.innerHTML = `<div class="cx-sh cx-sh-qrview"><div class="cx-qr" style="width:15rem;max-width:72%;aspect-ratio:1;height:auto" role="img" aria-label="
|
|
99
|
+
$('#mdl-title').textContent = T('QR code');
|
|
100
|
+
body.innerHTML = `<div class="cx-sh cx-sh-qrview"><div class="cx-qr" style="width:15rem;max-width:72%;aspect-ratio:1;height:auto" role="img" aria-label="${esc(T('QR code of {0}', shown))}"><div class="cx-qr-mark"><span>${esc(i.initials)}</span></div></div><span class="cx-sh-qrurl cx-num">${esc(shown)}</span><button data-slot="button" class="cn-button cn-button-variant-outline cn-button-size-sm group/button" data-sh-back><svg><use href="#left"/></svg>${T('Back')}</button></div>`;
|
|
101
101
|
import('@muten/shadcn/registry/kit/qrcode.js').then(({ default: qrcode }) => {
|
|
102
102
|
const q = qrcode(0, 'H'); q.addData(url); q.make(); const n = q.getModuleCount(); let d = '';
|
|
103
103
|
for (let r = 0; r < n; r++) for (let c = 0; c < n; c++) if (q.isDark(r, c)) d += `M${c} ${r}h1v1h-1z`;
|
|
@@ -108,13 +108,13 @@ export function share(root, i, core) {
|
|
|
108
108
|
const kind = `share-${Math.random().toString(36).slice(2, 8)}`;
|
|
109
109
|
CONTENT[kind] = () => {
|
|
110
110
|
$('#mdl-desc').textContent = ''; main();
|
|
111
|
-
foot.innerHTML = navigator.share ? ui.button({ label: `${ui.icon('share')}
|
|
111
|
+
foot.innerHTML = navigator.share ? ui.button({ label: `${ui.icon('share')}${T('Share in another app')}`, variant: 'default', size: 'default', cls: 'cx-go', attrs: 'data-sh-native' }) : '';
|
|
112
112
|
body.dataset.share = kind;
|
|
113
113
|
};
|
|
114
114
|
root.addEventListener('click', (e) => { if (e.target.closest('button')) openModal(kind, 'auto'); });
|
|
115
115
|
document.addEventListener('click', async (e) => {
|
|
116
116
|
if (body.dataset.share !== kind || !mdl.contains(e.target)) return;
|
|
117
|
-
if (e.target.closest('[data-sh-copy]')) copy('
|
|
117
|
+
if (e.target.closest('[data-sh-copy]')) copy(T('Link copied.'));
|
|
118
118
|
if (e.target.closest('[data-sh-back]')) easeHeight(mdl, main);
|
|
119
119
|
if (e.target.closest('[data-sh-native]')) { try { await navigator.share({ title: text, url }); } catch {} }
|
|
120
120
|
});
|
|
@@ -126,7 +126,7 @@ export function share(root, i, core) {
|
|
|
126
126
|
// badge, tone, amount, sheet [{icon, title, description, badge, tone, amount, photos}], timeline [{label, when}]}],
|
|
127
127
|
// commands [{icon, label, page}]; on.navigate(page) · on.open(id) · on.action(label)
|
|
128
128
|
export function appShell(root, i, core, on) {
|
|
129
|
-
const { ui, $, $$, openModal, closeModal, CONTENT, body, foot, mdl, money0, fold } = core;
|
|
129
|
+
const { ui, $, $$, openModal, closeModal, CONTENT, body, foot, mdl, money0, fold } = core, L = core.langOf(root), T = (k, ...a) => core.t(L, k, ...a);
|
|
130
130
|
const nav = list(i.nav), pages = list(i.pages), rows = list(i.rows), commands = list(i.commands);
|
|
131
131
|
const groups = [...new Set(nav.map((n) => n.group))].map((g) => [g, nav.filter((n) => n.group === g)]);
|
|
132
132
|
root.classList.add('cx-app'); const app = root;
|
|
@@ -136,7 +136,7 @@ export function appShell(root, i, core, on) {
|
|
|
136
136
|
const pageBody = () => {
|
|
137
137
|
const mine = rows.filter((r) => r.page === appPage);
|
|
138
138
|
if (mine.length) return `<div data-slot="item-group" class="cn-item-group cx-app-list" role="list">${mine.map((r) => `<button class="cn-item cn-item-variant-outline cn-item-size-sm cx-app-row" ${r.sheet ? `data-open-row="${esc(r.id)}"` : ''}>${ui.itemMedia(ui.avatar(r.title), 'default')}<div class="cn-item-content"><div class="cn-item-title">${esc(r.title)}</div><p class="cn-item-description">${esc(r.description)}</p></div><div class="cn-item-actions">${aside(r)}</div></button>`).join('')}</div>`;
|
|
139
|
-
return `<div data-slot="empty" class="cn-empty" style="padding:48px 12px"><div class="cn-empty-header"><div class="cn-empty-media cn-empty-media-icon">${ui.icon(nav.find((n) => n.label === appPage)?.icon || 'store')}</div><div class="cn-empty-title">${esc(appPage)}</div><div class="cn-empty-description">${esc(i.empty || '
|
|
139
|
+
return `<div data-slot="empty" class="cn-empty" style="padding:48px 12px"><div class="cn-empty-header"><div class="cn-empty-media cn-empty-media-icon">${ui.icon(nav.find((n) => n.label === appPage)?.icon || 'store')}</div><div class="cn-empty-title">${esc(appPage)}</div><div class="cn-empty-description">${esc(i.empty || T('Nothing here yet.'))}</div></div></div>`;
|
|
140
140
|
};
|
|
141
141
|
const twoLine = (a, b) => `<span class="cx-sb-two">${ui.text(esc(a), 'heading')}${ui.text(esc(b), 'caption')}</span>`;
|
|
142
142
|
const render = () => {
|
|
@@ -152,18 +152,18 @@ export function appShell(root, i, core, on) {
|
|
|
152
152
|
<div class="cn-sheet-overlay cx-app-scrim" data-app-scrim hidden></div>
|
|
153
153
|
<main data-slot="sidebar-inset" class="cn-sidebar-inset cx-app-main">
|
|
154
154
|
<header class="cx-app-top">
|
|
155
|
-
${ui.button({ icon: 'menu-lines', variant: 'ghost', size: 'icon-sm', attrs:
|
|
155
|
+
${ui.button({ icon: 'menu-lines', variant: 'ghost', size: 'icon-sm', attrs: `data-sb-toggle aria-label="${T('Show or hide the menu')}" title="${T('Menu')} (Ctrl B)"` })}
|
|
156
156
|
<div data-slot="separator" class="cn-separator cn-separator-vertical cx-app-vsep"></div>
|
|
157
|
-
<nav aria-label="
|
|
157
|
+
<nav aria-label="${T('Breadcrumb')}"><ol data-slot="breadcrumb-list" class="cn-breadcrumb-list"><li class="cn-breadcrumb-item"><a class="cn-breadcrumb-link" href="#" data-home>${esc(i.brand?.short || i.brand?.name)}</a></li><li class="cn-breadcrumb-separator" role="presentation">${ui.icon('right')}</li><li class="cn-breadcrumb-item"><span class="cn-breadcrumb-page" aria-current="page">${esc(meta.title)}</span></li></ol></nav>
|
|
158
158
|
<span class="cx-grow"></span>
|
|
159
|
-
${ui.button({ label: `${ui.icon('search')}<span class="cx-app-search-t"
|
|
159
|
+
${ui.button({ label: `${ui.icon('search')}<span class="cx-app-search-t">${T('Search…')}</span><kbd data-slot="kbd" class="cn-kbd">⌘K</kbd>`, size: 'sm', cls: 'cx-app-search', attrs: 'data-cmd-open aria-keyshortcuts="Control+K Meta+K"' })}
|
|
160
160
|
</header>
|
|
161
161
|
<div class="cx-app-page">
|
|
162
162
|
<div class="cx-app-head"><div class="cx-vstack" style="gap:2px">${ui.text(esc(meta.title), 'title', 'h1')}${meta.description ? ui.text(esc(meta.description), 'muted', 'p') : ''}</div><div class="cx-hstack">${list(meta.actions).map((a) => ui.button({ label: `${a.icon ? ui.icon(a.icon) : ''}${esc(a.label)}`, variant: a.variant || 'outline', size: 'sm', attrs: `data-app-act="${esc(a.label)}"` })).join('')}</div></div>
|
|
163
163
|
${pageBody()}
|
|
164
164
|
</div>
|
|
165
165
|
<div class="cn-sheet-overlay cx-app-scrim" data-sheet-scrim hidden></div>
|
|
166
|
-
<aside data-slot="sheet-content" data-side="right" class="cn-sheet-content cx-app-sheet" role="dialog" aria-modal="false" aria-label="
|
|
166
|
+
<aside data-slot="sheet-content" data-side="right" class="cn-sheet-content cx-app-sheet" role="dialog" aria-modal="false" aria-label="${T('Detail')}" hidden></aside>
|
|
167
167
|
</main>`;
|
|
168
168
|
syncTips();
|
|
169
169
|
};
|
|
@@ -174,8 +174,8 @@ export function appShell(root, i, core, on) {
|
|
|
174
174
|
const photos = (x) => (list(x.photos).length ? `<div class="cx-hstack">${list(x.photos).map((bg) => ui.avatar('', { shape: 'square', size: 'lg', bg })).join('')}</div>` : '');
|
|
175
175
|
const items = list(r.sheet).map((x) => ui.item({ media: ui.itemMedia(ui.icon(x.icon || 'info')), title: esc(x.title), description: esc(x.description || ''), actions: aside(x), footer: photos(x) }));
|
|
176
176
|
const tl = list(r.timeline);
|
|
177
|
-
if (tl.length) items.push(ui.item({ media: ui.itemMedia(ui.icon('clock')), title: '
|
|
178
|
-
s.innerHTML = `<div data-slot="sheet-header" class="cn-sheet-header"><div class="cx-hstack" style="justify-content:space-between;flex-wrap:nowrap"><div class="cx-vstack" style="gap:2px">${ui.text(esc(r.sheetTitle || r.title), 'title', 'h2')}${ui.text(esc(r.sheetDescription || r.description), 'muted', 'p')}</div>${ui.button({ icon: 'x', variant: 'ghost', size: 'icon-sm', attrs:
|
|
177
|
+
if (tl.length) items.push(ui.item({ media: ui.itemMedia(ui.icon('clock')), title: T('History'), footer: `<ol class="cx-timeline">${tl.map((t, k) => `<li ${k === tl.length - 1 ? 'data-now' : ''}>${ui.text(esc(t.label), k === tl.length - 1 ? 'label' : 'body')}${ui.text(esc(t.when), 'caption')}</li>`).join('')}</ol>` }));
|
|
178
|
+
s.innerHTML = `<div data-slot="sheet-header" class="cn-sheet-header"><div class="cx-hstack" style="justify-content:space-between;flex-wrap:nowrap"><div class="cx-vstack" style="gap:2px">${ui.text(esc(r.sheetTitle || r.title), 'title', 'h2')}${ui.text(esc(r.sheetDescription || r.description), 'muted', 'p')}</div>${ui.button({ icon: 'x', variant: 'ghost', size: 'icon-sm', attrs: `data-sheet-close aria-label="${T('Close')}" title="${T('Close')}"` })}</div></div>
|
|
179
179
|
<div class="cx-app-sheet-body"><div data-slot="item-group" class="cn-item-group">${items.join(ui.itemSeparator())}</div></div>
|
|
180
180
|
${r.primary || r.secondary ? `<div data-slot="sheet-footer" class="cn-sheet-footer">${r.secondary ? ui.button({ label: esc(r.secondary), size: 'default', cls: 'cx-exit', attrs: `data-app-act="${esc(r.secondary)}"` }) : ''}${r.primary ? ui.button({ label: `${esc(r.primary)}${ui.icon('right')}`, variant: 'default', size: 'default', cls: 'cx-go', attrs: `data-app-act="${esc(r.primary)}"` }) : ''}</div>` : ''}`;
|
|
181
181
|
s.hidden = false; sheetScrim().hidden = false;
|
|
@@ -190,7 +190,8 @@ export function appShell(root, i, core, on) {
|
|
|
190
190
|
if (app.dataset.collapsed) delete app.dataset.collapsed; else app.dataset.collapsed = '1';
|
|
191
191
|
const g = $('.cx-app-sb', app); g.dataset.state = app.dataset.collapsed ? 'collapsed' : 'expanded'; g.dataset.collapsible = app.dataset.collapsed ? 'icon' : ''; syncTips();
|
|
192
192
|
};
|
|
193
|
-
|
|
193
|
+
// a section opened from the menu comes in with a short fade (kit.css .cx-app-page[data-in]); the first one is just there
|
|
194
|
+
const go = (page) => { appPage = page; const col = app.dataset.collapsed; render(); if (col) app.dataset.collapsed = col; app.removeAttribute('data-sb-open'); app.querySelector('.cx-app-page')?.setAttribute('data-in', ''); on.navigate?.(page); };
|
|
194
195
|
app.addEventListener('click', (e) => {
|
|
195
196
|
if (e.target.closest('[data-sb-toggle]')) return toggleSidebar();
|
|
196
197
|
if (e.target.closest('[data-app-scrim]')) return toggleSidebar();
|
|
@@ -211,7 +212,7 @@ export function appShell(root, i, core, on) {
|
|
|
211
212
|
|
|
212
213
|
// Command (⌘K): groups of results filtered as you type, arrows + Enter
|
|
213
214
|
const CMD = () => [
|
|
214
|
-
['
|
|
215
|
+
[T('Actions'), commands.map((c) => [c.icon || 'right', c.label, () => (c.page ? go(c.page) : on.action?.(c.label))])],
|
|
215
216
|
...[...new Set(rows.map((r) => r.page))].map((p) => [p, rows.filter((r) => r.page === p).map((r) => [nav.find((n) => n.label === p)?.icon || 'user', r.command || r.title, () => { go(p); if (r.sheet) { app.scrollIntoView({ block: 'center', behavior: 'smooth' }); openRow(r.id); } }])]),
|
|
216
217
|
];
|
|
217
218
|
let cmdItems = [], cmdHl = 0;
|
|
@@ -219,15 +220,15 @@ export function appShell(root, i, core, on) {
|
|
|
219
220
|
const paint = (q = '') => {
|
|
220
221
|
const t = fold(q.trim()); cmdItems = [];
|
|
221
222
|
const html = CMD().map(([g, items]) => { const hits = items.filter(([, l]) => !t || fold(l).includes(t)); if (!hits.length) return ''; return `<div data-slot="command-group" class="cn-command-group" role="group" aria-label="${esc(g)}"><div class="cx-cmd-label">${ui.text(esc(g), 'caption')}</div>${hits.map((h) => { cmdItems.push(h); const k = cmdItems.length - 1; return `<div data-slot="command-item" class="cn-command-item" role="option" id="cmd-${k}" data-ci="${k}" ${k === cmdHl ? 'data-selected="true" aria-selected="true"' : ''}>${ui.icon(h[0])}${ui.text(esc(h[1]))}</div>`; }).join('')}</div>`; }).join('');
|
|
222
|
-
$('.cx-cmd-list', body).innerHTML = html || `<div data-slot="command-empty" class="cn-command-empty"
|
|
223
|
+
$('.cx-cmd-list', body).innerHTML = html || `<div data-slot="command-empty" class="cn-command-empty">${T('Nothing with “{0}”', esc(q))}</div>`;
|
|
223
224
|
$('.cx-cmd-in', body).setAttribute('aria-activedescendant', cmdItems.length ? `cmd-${cmdHl}` : '');
|
|
224
225
|
$(`#cmd-${cmdHl}`, body)?.scrollIntoView({ block: 'nearest' });
|
|
225
226
|
};
|
|
226
227
|
CONTENT[kind] = () => {
|
|
227
|
-
$('#mdl-title').textContent = '
|
|
228
|
+
$('#mdl-title').textContent = T('Search'); $('#mdl-desc').textContent = '';
|
|
228
229
|
body.dataset.cmd = kind;
|
|
229
|
-
body.innerHTML = `<div data-slot="command" class="cn-command cx-cmd"><div data-slot="command-input-wrapper" class="cn-command-input-wrapper"><div data-slot="input-group" class="cn-input-group cn-command-input-group"><input data-slot="input-group-control" class="cn-command-input cn-input-group-input cx-cmd-in" placeholder="${esc(i.placeholder || '
|
|
230
|
-
foot.innerHTML = `<span class="cx-hstack cx-cmd-help">${ui.text(
|
|
230
|
+
body.innerHTML = `<div data-slot="command" class="cn-command cx-cmd"><div data-slot="command-input-wrapper" class="cn-command-input-wrapper"><div data-slot="input-group" class="cn-input-group cn-command-input-group"><input data-slot="input-group-control" class="cn-command-input cn-input-group-input cx-cmd-in" placeholder="${esc(i.placeholder || T('Search or jump to…'))}" role="combobox" aria-expanded="true" aria-controls="cmd-list" autocomplete="off"><div class="cn-input-group-addon cn-input-group-addon-align-inline-start"><svg class="cn-command-input-icon"><use href="#search"/></svg></div></div></div><div data-slot="command-list" class="cn-command-list cx-cmd-list" id="cmd-list" role="listbox"></div></div>`;
|
|
231
|
+
foot.innerHTML = `<span class="cx-hstack cx-cmd-help">${ui.text(`<kbd class="cn-kbd">↑</kbd><kbd class="cn-kbd">↓</kbd> ${T('move')}`, 'caption')}${ui.text(`<kbd class="cn-kbd">Enter</kbd> ${T('open')}`, 'caption')}${ui.text(`<kbd class="cn-kbd">Esc</kbd> ${T('close')}`, 'caption')}</span>`;
|
|
231
232
|
cmdHl = 0; paint();
|
|
232
233
|
setTimeout(() => $('.cx-cmd-in', body)?.focus(), 30);
|
|
233
234
|
};
|
|
@@ -251,9 +252,11 @@ export function appShell(root, i, core, on) {
|
|
|
251
252
|
// ── booking picker ──
|
|
252
253
|
// i: week [{days [0..6], hours ["09:00", …]}] (what the shop offers each weekday), taken [{day, hours}] (day = days
|
|
253
254
|
// from today or "yyyy-mm-dd"; hours empty = the whole day is booked), wait (ms the server takes: shows the
|
|
254
|
-
// skeleton), cta; on.select("yyyy-mm-dd HH:MM") on
|
|
255
|
+
// skeleton), cta; on.select("yyyy-mm-dd HH:MM") on “Continue”
|
|
255
256
|
export function booking(root, i, core, on) {
|
|
256
|
-
const { $, $$, today, addDays,
|
|
257
|
+
const { $, $$, today, addDays } = core, L = core.langOf(root), T = (k, ...a) => core.t(L, k, ...a);
|
|
258
|
+
const W = core.kit.words(L), DAY = W.dayS, MON = W.mon, DAYLONG = W.dayL, es = L === 'es';
|
|
259
|
+
const longDay = (d) => (es ? `${DAYLONG[d.getDay()]} ${d.getDate()} de ${MON[d.getMonth()]}` : `${DAYLONG[d.getDay()]}, ${MON[d.getMonth()]} ${d.getDate()}`);
|
|
257
260
|
const key = (d) => `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
|
|
258
261
|
const iso = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
259
262
|
const dayOf = (v) => (/^-?\d+$/.test(String(v)) ? addDays(today, Number(v)) : new Date(String(v) + 'T00:00:00'));
|
|
@@ -269,13 +272,13 @@ export function booking(root, i, core, on) {
|
|
|
269
272
|
};
|
|
270
273
|
root.innerHTML = `<div class="cx-book">
|
|
271
274
|
<div class="cx-book-head">
|
|
272
|
-
<button data-slot="button" class="cn-button cn-button-variant-outline cn-button-size-icon-sm group/button" data-wk="-1" aria-label="
|
|
275
|
+
<button data-slot="button" class="cn-button cn-button-variant-outline cn-button-size-icon-sm group/button" data-wk="-1" aria-label="${T('Previous week')}" title="${T('Previous week')}"><svg><use href="#left"/></svg></button>
|
|
273
276
|
<b class="cx-wk-label" aria-live="polite"></b>
|
|
274
|
-
<button data-slot="button" class="cn-button cn-button-variant-outline cn-button-size-icon-sm group/button" data-wk="1" aria-label="
|
|
277
|
+
<button data-slot="button" class="cn-button cn-button-variant-outline cn-button-size-icon-sm group/button" data-wk="1" aria-label="${T('Next week')}" title="${T('Next week')}"><svg><use href="#right"/></svg></button>
|
|
275
278
|
</div>
|
|
276
|
-
<div class="cx-week" role="group" aria-label="
|
|
279
|
+
<div class="cx-week" role="group" aria-label="${T('Day')}"></div>
|
|
277
280
|
<div class="cx-hours" aria-live="polite"></div>
|
|
278
|
-
<div class="cx-book-sum"><span class="cx-book-say"
|
|
281
|
+
<div class="cx-book-sum"><span class="cx-book-say">${T('Pick a day and a time')}</span><button data-slot="button" class="cn-button cn-button-variant-default cn-button-size-default group/button cx-book-go" disabled>${esc(T(i.cta || 'Continue'))}</button></div>
|
|
279
282
|
</div>`;
|
|
280
283
|
let weekStart = addDays(today, -((today.getDay() + 6) % 7)); // monday
|
|
281
284
|
let picked = { day: null, hour: null }, loadTimer = 0;
|
|
@@ -283,12 +286,12 @@ export function booking(root, i, core, on) {
|
|
|
283
286
|
const firstOpen = (from) => { for (let k = 0; k < 60; k++) { const d = addDays(from, k); const s = slotsFor(d); if (s && s.length) return d; } return null; };
|
|
284
287
|
const sum = () => {
|
|
285
288
|
const d = picked.day;
|
|
286
|
-
$('.cx-book-say', root).innerHTML = d ? `<b>${
|
|
289
|
+
$('.cx-book-say', root).innerHTML = d ? `<b>${longDay(d)}</b>${picked.hour ? ` · <b>${picked.hour}</b>` : ` · ${T('pick a time')}`}` : T('Pick a day and a time');
|
|
287
290
|
go.disabled = !(d && picked.hour);
|
|
288
291
|
};
|
|
289
292
|
const paintWeek = () => {
|
|
290
293
|
const end = addDays(weekStart, 6);
|
|
291
|
-
$('.cx-wk-label', root).textContent = weekStart.getMonth() === end.getMonth() ? `${weekStart.getDate()} al ${end.getDate()} de ${MON[end.getMonth()]}` : `${weekStart.getDate()} de ${MON[weekStart.getMonth()]} al ${end.getDate()} de ${MON[end.getMonth()]}
|
|
294
|
+
$('.cx-wk-label', root).textContent = es ? (weekStart.getMonth() === end.getMonth() ? `${weekStart.getDate()} al ${end.getDate()} de ${MON[end.getMonth()]}` : `${weekStart.getDate()} de ${MON[weekStart.getMonth()]} al ${end.getDate()} de ${MON[end.getMonth()]}`) : (weekStart.getMonth() === end.getMonth() ? `${MON[end.getMonth()]} ${weekStart.getDate()}–${end.getDate()}` : `${MON[weekStart.getMonth()]} ${weekStart.getDate()} – ${MON[end.getMonth()]} ${end.getDate()}`);
|
|
292
295
|
$('[data-wk="-1"]', root).disabled = weekStart <= today;
|
|
293
296
|
weekEl.innerHTML = '';
|
|
294
297
|
for (let k = 0; k < 7; k++) {
|
|
@@ -297,9 +300,9 @@ export function booking(root, i, core, on) {
|
|
|
297
300
|
b.className = 'cn-button cn-button-variant-outline cx-size-tile cx-day'; b.disabled = s === null;
|
|
298
301
|
if (s && !s.length) b.dataset.full = '';
|
|
299
302
|
b.setAttribute('aria-pressed', !!picked.day && key(d) === key(picked.day));
|
|
300
|
-
// the name starts with what the tile shows (
|
|
301
|
-
b.setAttribute('aria-label', `${key(d) === key(today) ? '
|
|
302
|
-
b.innerHTML = `<small>${key(d) === key(today) ? '
|
|
303
|
+
// the name starts with what the tile shows (“vie 9”), so a voice user can say it; the long day and the state follow
|
|
304
|
+
b.setAttribute('aria-label', `${key(d) === key(today) ? T('today') : DAY[d.getDay()]} ${d.getDate()}, ${DAYLONG[d.getDay()]}${s === null ? `, ${T('closed')}` : s.length ? `, ${T('{0} times', s.length)}` : `, ${T('no times')}`}`);
|
|
305
|
+
b.innerHTML = `<small>${key(d) === key(today) ? T('today') : DAY[d.getDay()]}</small> <b>${d.getDate()}</b><i></i>`;
|
|
303
306
|
b.onclick = () => pickDay(d);
|
|
304
307
|
weekEl.appendChild(b);
|
|
305
308
|
}
|
|
@@ -308,17 +311,17 @@ export function booking(root, i, core, on) {
|
|
|
308
311
|
const s = slotsFor(d) || [];
|
|
309
312
|
if (!s.length) {
|
|
310
313
|
const next = firstOpen(addDays(d, 1));
|
|
311
|
-
hours.innerHTML = `<div class="cx-hours-empty"><svg><use href="#caloff"/></svg><span
|
|
314
|
+
hours.innerHTML = `<div class="cx-hours-empty"><svg><use href="#caloff"/></svg><span>${T('No times left this day.')}</span>${next ? `<button data-slot="button" class="cn-button cn-button-variant-outline cn-button-size-sm group/button" data-jump>${T('See {0} {1}', es ? `el ${DAYLONG[next.getDay()].toLowerCase()}` : DAYLONG[next.getDay()], next.getDate())}</button>` : ''}</div>`;
|
|
312
315
|
if (next) $('[data-jump]', hours).onclick = () => { if (next > addDays(weekStart, 6)) weekStart = addDays(next, -((next.getDay() + 6) % 7)); pickDay(next); };
|
|
313
316
|
return;
|
|
314
317
|
}
|
|
315
318
|
const am = s.filter((h) => h < '13:00'), pm = s.filter((h) => h >= '13:00');
|
|
316
319
|
// the part of the day is a label, not a heading: the picker sits inside pages whose outline it must not break
|
|
317
320
|
const grid = (label, hs) => hs.length ? `<p class="cx-hours-h">${label}</p><div class="cx-hour-grid">${hs.map((h) => `<button class="cn-button cn-button-variant-outline cn-button-size-sm cx-hour" aria-pressed="${h === picked.hour}">${h}</button>`).join('')}</div>` : '';
|
|
318
|
-
hours.innerHTML = `<div class="cx-panel" style="display:flex;flex-direction:column;gap:10px">${grid('
|
|
321
|
+
hours.innerHTML = `<div class="cx-panel" style="display:flex;flex-direction:column;gap:10px">${grid(T('Morning'), am)}${grid(T('Afternoon'), pm)}</div>`;
|
|
319
322
|
$$('.cx-hour', hours).forEach((b) => b.onclick = () => { picked.hour = b.textContent; $$('.cx-hour', hours).forEach((x) => x.setAttribute('aria-pressed', x === b)); sum(); });
|
|
320
323
|
};
|
|
321
|
-
const skeleton = () => { hours.innerHTML = `<h4 style="visibility:hidden"
|
|
324
|
+
const skeleton = () => { hours.innerHTML = `<h4 style="visibility:hidden">${T('Morning')}</h4><div class="cx-hour-grid">${'<div data-slot="skeleton" class="cn-skeleton cx-hour-skel"></div>'.repeat(8)}</div>`; };
|
|
322
325
|
function pickDay(d) {
|
|
323
326
|
if (!d) return;
|
|
324
327
|
picked = { day: d, hour: null }; paintWeek(); sum();
|
|
@@ -331,12 +334,14 @@ export function booking(root, i, core, on) {
|
|
|
331
334
|
paintWeek(); pickDay(firstOpen(today));
|
|
332
335
|
}
|
|
333
336
|
|
|
334
|
-
// ──
|
|
337
|
+
// ── “New appointment” in 4 steps ──
|
|
335
338
|
// i: clients [{name, description}], services [{name, duration, price}], hours ["09:00", …], closed [0] (weekdays),
|
|
336
339
|
// note (under the summary); opened by start(); on.create(json {client, service, day, hour}) · on.close()
|
|
337
340
|
export function wizard(core, i, on) {
|
|
338
|
-
const { ui, $, $$, openModal, closeModal, CONTENT, body, foot, mdl, easeHeight, today, addDays,
|
|
339
|
-
const
|
|
341
|
+
const { ui, $, $$, openModal, closeModal, CONTENT, body, foot, mdl, easeHeight, today, addDays, money0 } = core;
|
|
342
|
+
const L = i.lang || core.langOf(document.body), T = (k, ...a) => core.t(L, k, ...a), W = core.kit.words(L), DAY = W.dayS, MON = W.mon, DAYLONG = W.dayL;
|
|
343
|
+
const longDay = (d) => (L === 'es' ? `${DAYLONG[d.getDay()]} ${d.getDate()} de ${MON[d.getMonth()]}` : `${DAYLONG[d.getDay()]}, ${MON[d.getMonth()]} ${d.getDate()}`);
|
|
344
|
+
const STEPS = [T('Client'), T('Service'), T('Day and time'), T('Confirm')];
|
|
340
345
|
const clients = list(i.clients), services = list(i.services), hoursList = list(i.hours), closed = list(i.closed);
|
|
341
346
|
const iso = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
342
347
|
const days = () => Array.from({ length: 10 }, (_, k) => addDays(today, k + 1)).filter((d) => !closed.includes(d.getDay())).slice(0, 5);
|
|
@@ -344,29 +349,29 @@ export function wizard(core, i, on) {
|
|
|
344
349
|
const kind = `wizard-${Math.random().toString(36).slice(2, 8)}`;
|
|
345
350
|
const valid = () => [wiz.client != null, wiz.service != null, wiz.day != null && wiz.hour != null, true][wiz.at];
|
|
346
351
|
const clientRows = () => clients.map((c, k) => [c, k]).filter(([c]) => !q || core.fold(`${c.name} ${c.description}`).includes(core.fold(q)))
|
|
347
|
-
.map(([c, k]) => ui.choice({ media: ui.itemMedia(ui.avatar(c.name), 'default'), title: esc(c.name), description: esc(c.description), checked: wiz.client === k, attrs: `data-v="${k}"` })).join('') || ui.text(
|
|
352
|
+
.map(([c, k]) => ui.choice({ media: ui.itemMedia(ui.avatar(c.name), 'default'), title: esc(c.name), description: esc(c.description), checked: wiz.client === k, attrs: `data-v="${k}"` })).join('') || ui.text(T('Nobody with “{0}”', esc(q)), 'muted', 'p');
|
|
348
353
|
const stepBody = () => {
|
|
349
354
|
const at = wiz.at;
|
|
350
|
-
if (at === 0) return `<div class="cx-vstack" style="gap:12px">${ui.field({ id: 'wz-q', label: '
|
|
351
|
-
if (at === 1) return `<div class="cx-vstack" role="radiogroup" aria-label="
|
|
352
|
-
if (at === 2) return `<div class="cx-vstack" style="gap:14px">${ui.text('
|
|
355
|
+
if (at === 0) return `<div class="cx-vstack" style="gap:12px">${ui.field({ id: 'wz-q', label: T('Search or pick'), control: ui.input({ id: 'wz-q', placeholder: T('Name or phone'), value: esc(q) }) })}<div class="cx-vstack" role="radiogroup" aria-label="${T('Client')}" data-wz="client">${clientRows()}</div></div>`;
|
|
356
|
+
if (at === 1) return `<div class="cx-vstack" role="radiogroup" aria-label="${T('Service')}" data-wz="service">${services.map((s, k) => ui.choice({ media: ui.itemMedia(ui.icon(s.icon || 'scissors')), title: esc(s.name), description: esc(s.duration), aside: ui.text(money0(Number(s.price) || 0), 'strong'), checked: wiz.service === k, attrs: `data-v="${k}"` })).join('')}</div>`;
|
|
357
|
+
if (at === 2) return `<div class="cx-vstack" style="gap:14px">${ui.text(T('Day'), 'label')}<div class="cx-wz-days" role="group" aria-label="${T('Day')}">${days().map((d, k) => `<button class="cn-button cn-button-variant-outline cx-size-tile cx-day" aria-pressed="${wiz.day === k}" data-wz-day="${k}"><small>${DAY[d.getDay()]}</small><b>${d.getDate()}</b><i></i></button>`).join('')}</div>${ui.text('Hora', 'label')}<div class="cx-hour-grid" role="group" aria-label="${T('Hour')}">${hoursList.map((h) => `<button class="cn-button cn-button-variant-outline cn-button-size-sm cx-hour" aria-pressed="${wiz.hour === h}" data-wz-hour="${h}">${h}</button>`).join('')}</div></div>`;
|
|
353
358
|
const c = clients[wiz.client], s = services[wiz.service], d = days()[wiz.day];
|
|
354
359
|
return `<div data-slot="item-group" class="cn-item-group cx-wz-sum">${[
|
|
355
|
-
ui.item({ media: ui.itemMedia(ui.avatar(c.name), 'default'), title: esc(c.name), description: '
|
|
360
|
+
ui.item({ media: ui.itemMedia(ui.avatar(c.name), 'default'), title: esc(c.name), description: T('Client') }),
|
|
356
361
|
ui.item({ media: ui.itemMedia(ui.icon(s.icon || 'scissors')), title: esc(s.name), description: esc(s.duration), actions: ui.text(money0(Number(s.price) || 0), 'strong') }),
|
|
357
|
-
ui.item({ media: ui.itemMedia(ui.icon('cal')), title:
|
|
362
|
+
ui.item({ media: ui.itemMedia(ui.icon('cal')), title: longDay(d), description: wiz.hour }),
|
|
358
363
|
].join(ui.itemSeparator())}</div>${i.note ? ui.text(esc(i.note), 'caption', 'p') : ''}`;
|
|
359
364
|
};
|
|
360
365
|
const paintFoot = () => {
|
|
361
366
|
const last = wiz.at === STEPS.length - 1;
|
|
362
|
-
foot.innerHTML = ui.button({ label: wiz.at === 0 ? '
|
|
363
|
-
+ ui.button({ label: last ? '
|
|
367
|
+
foot.innerHTML = ui.button({ label: wiz.at === 0 ? T('Cancel') : T('Back'), size: 'default', attrs: wiz.at === 0 ? 'data-dismiss' : 'data-wz-back', cls: 'cx-exit' })
|
|
368
|
+
+ ui.button({ label: last ? T('Create appointment') : `${T('Next')}<svg><use href="#right"/></svg>`, variant: 'default', size: 'default', attrs: `data-wz-next ${valid() ? '' : 'disabled'}`, cls: 'cx-go' });
|
|
364
369
|
};
|
|
365
370
|
const render = (dir = 0) => {
|
|
366
|
-
$('#mdl-title').textContent = i.title || '
|
|
371
|
+
$('#mdl-title').textContent = T(i.title || 'New appointment');
|
|
367
372
|
$('#mdl-desc').innerHTML = ui.stepper(STEPS, wiz.at);
|
|
368
373
|
const paint = () => { body.innerHTML = stepBody(); };
|
|
369
|
-
if (dir) { easeHeight(mdl, paint); body.animate([{ opacity: 0, transform: `translateX(${dir * 14}px)` }, { opacity: 1, transform: 'none' }], { duration: 200, easing: 'cubic-bezier(.
|
|
374
|
+
if (dir) { easeHeight(mdl, paint); body.animate([{ opacity: 0, transform: `translateX(${dir * 14}px)` }, { opacity: 1, transform: 'none' }], { duration: 200, easing: 'cubic-bezier(.23, 1, .32, 1)' }); } else paint();
|
|
370
375
|
paintFoot();
|
|
371
376
|
};
|
|
372
377
|
CONTENT[kind] = () => { wiz = { at: 0, client: null, service: null, day: null, hour: null }; q = ''; body.dataset.wizard = kind; render(); };
|
|
@@ -389,7 +394,7 @@ export function wizard(core, i, on) {
|
|
|
389
394
|
setTimeout(() => { closeModal(); wiz = null; }, 700);
|
|
390
395
|
}
|
|
391
396
|
});
|
|
392
|
-
// closing by Esc, the overlay or
|
|
397
|
+
// closing by Esc, the overlay or “Cancel” tells the page, so its own “open” state can go back to false
|
|
393
398
|
new MutationObserver(() => { if (mdl.hidden && body.dataset.wizard === kind) { delete body.dataset.wizard; wiz = null; on.close?.(); } }).observe(mdl, { attributes: true, attributeFilter: ['hidden'] });
|
|
394
399
|
return { start: () => openModal(kind, 'auto') };
|
|
395
400
|
}
|
package/registry/kit/table.js
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
// @muten/shadcn · DataTable (kit). The reference artifact's DataTable, made generic: columns, rows, filters, statuses
|
|
2
2
|
// and bulk actions are data. Everything else is the artifact's: filters from their type (search · facet · date ·
|
|
3
3
|
// range) with counts, chips that leave with a fade, rows that fade out / slide / enter, the height easing, sort by
|
|
4
|
-
// header, selection with its own bulk bar, the phone's
|
|
4
|
+
// header, selection with its own bulk bar, the phone's “search + Filters” sheet, and a narrow box becoming Items.
|
|
5
5
|
// columns [{key, label, type: person|text|date|status|money, sub (person: the key under the name), sortable}]
|
|
6
6
|
// rows [{id, …keys}] (a date is "yyyy-mm-ddTHH:MM"; a row's date may be "+N" days from today too)
|
|
7
7
|
// filters [{id, type: search|facet|date|range, label, key | keys (search)}]
|
|
8
|
-
// statuses [{id, label, tone: ok|wait|info|bad}] · noun ["
|
|
8
|
+
// statuses [{id, label, tone: ok|wait|info|bad}] · noun ["appointment", "appointments"] · bulk [{id, label, variant}]
|
|
9
9
|
// on.action(json {action, ids})
|
|
10
10
|
export function dataTable(root, cfg, core, on) {
|
|
11
11
|
const { ui, $, $$, lib, place, show, hide, easeHeight, setCheck, CB_CHECK, money0, fold, reformat, groupThousands, today, addDays, DAY, openModal, CONTENT, body, foot } = core;
|
|
12
12
|
const list = (v) => (Array.isArray(v) ? v : []);
|
|
13
13
|
const COLS = list(cfg.columns), STATUS = Object.fromEntries(list(cfg.statuses).map((s) => [s.id, s]));
|
|
14
|
-
|
|
14
|
+
// its language: the nearest lang (the page's), English by default
|
|
15
|
+
const L = core.langOf(root), T = (k, ...a) => core.t(L, k, ...a), DAYS = core.kit.words(L).dayS;
|
|
16
|
+
const [ONE, MANY] = (list(cfg.noun).length === 2 ? cfg.noun : ['row', 'rows']).map((x) => T(x));
|
|
15
17
|
const ST_ORDER = list(cfg.statuses).map((s) => s.id);
|
|
16
18
|
const asDate = (v) => { const s = String(v ?? ''); const m = /^\+(\d+)(?:T(\d\d:\d\d))?$/.exec(s); if (m) { const d = addDays(today, +m[1]); if (m[2]) { const [h, mi] = m[2].split(':'); d.setHours(+h, +mi); } return d; } return new Date(s); };
|
|
17
19
|
const DT = list(cfg.rows).map((r, i) => ({ id: r.id ?? i, ...r }));
|
|
18
20
|
const colOf = (type) => COLS.find((c) => c.type === type);
|
|
19
21
|
const dateCol = colOf('date'), statusCol = colOf('status');
|
|
20
|
-
const whenText = (v) => { const d = asDate(v); return `${
|
|
21
|
-
const DATE_PRESETS = { today: ['
|
|
22
|
+
const whenText = (v) => { const d = asDate(v); return `${DAYS[d.getDay()]} ${d.getDate()} · ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; };
|
|
23
|
+
const DATE_PRESETS = { today: [T('Today'), (d) => +d === +today], tomorrow: [T('Tomorrow'), (d) => +d === +addDays(today, 1)], week: [T('Next 7 days'), (d) => d >= today && d < addDays(today, 7)], later: [T('Later'), (d) => d >= addDays(today, 7)] };
|
|
22
24
|
const dayOf = (v) => { const d = asDate(v); d.setHours(0, 0, 0, 0); return d; };
|
|
23
25
|
const FILTERS = list(cfg.filters).map((f) => ({
|
|
24
26
|
...f,
|
|
@@ -38,7 +40,7 @@ export function dataTable(root, cfg, core, on) {
|
|
|
38
40
|
const firstSort = dateCol ? { key: dateCol.key, dir: 1 } : { key: COLS[0]?.key, dir: 1 };
|
|
39
41
|
const bulk = list(cfg.bulk);
|
|
40
42
|
const CELL_CLASS = { person: 'cx-dt-who', text: 'cx-dt-svc', date: 'cx-dt-when', status: 'cx-dt-st', money: 'cx-dt-num' };
|
|
41
|
-
root.innerHTML = `<div class="cx-dt-host"><div class="cx-dt-toolbar"><div class="cx-hstack cx-dt-tools" role="toolbar" aria-label="
|
|
43
|
+
root.innerHTML = `<div class="cx-dt-host"><div class="cx-dt-toolbar"><div class="cx-hstack cx-dt-tools" role="toolbar" aria-label="${T('Filters')}"></div><span class="cx-text" data-variant="caption" data-dt-shown aria-live="polite"></span></div><div class="cx-hstack cx-dt-chips" hidden></div><div class="cx-dt-bulk" aria-live="polite"><div><span data-dt-count></span><div class="cx-hstack" style="gap:6px">${bulk.map((b) => ui.button({ label: b.label, variant: b.variant || 'outline', size: 'xs', attrs: `data-dt-bulk="${b.id}"` })).join('')}${ui.button({ label: T('Clear selection'), variant: 'ghost', size: 'xs', attrs: 'data-dt-unselect' })}</div></div></div><div class="cx-dt-scroll cx-scroll"><table class="cx-dt"><thead><tr><th class="cx-dt-check">${cbHtml(T('Select all'), false).replace('class="cn-checkbox"', 'class="cn-checkbox" data-dt-all')}</th>${COLS.map((c) => `<th aria-sort="${c.key === firstSort.key ? 'ascending' : 'none'}" ${c.type === 'money' ? 'class="cx-dt-num"' : ''}>${c.sortable === false ? ui.text(c.label, 'label') : `<button class="cn-button cn-button-variant-ghost cn-button-size-xs cx-th" data-sort="${c.key}">${c.label}<svg><use href="#${c.key === firstSort.key ? 'arrowup' : 'sort'}"/></svg></button>`}</th>`).join('')}</tr></thead><tbody></tbody></table></div></div>`;
|
|
42
44
|
const host = $('.cx-dt-host', root), dtTools = $('.cx-dt-tools', root), dtChips = $('.cx-dt-chips', root), dtCountEl = $('[data-dt-shown]', root), dtBody = $('tbody', root), dtAll = $('[data-dt-all]', root), dtBulk = $('.cx-dt-bulk', root);
|
|
43
45
|
const dtSel = new Set(); let dtSort = firstSort;
|
|
44
46
|
const passes = (r, except) => FILTERS.every((f) => f.id === except || PASS[f.type](f, fState[f.id], r));
|
|
@@ -51,37 +53,37 @@ export function dataTable(root, cfg, core, on) {
|
|
|
51
53
|
const paintTools = () => {
|
|
52
54
|
if (dtMobile) {
|
|
53
55
|
const n = activeCount(), focused = document.activeElement?.dataset?.filter === searchF?.id;
|
|
54
|
-
dtTools.innerHTML = searchHtml() + ui.button({ label: `${ui.icon('filter')}
|
|
56
|
+
dtTools.innerHTML = searchHtml() + ui.button({ label: `${ui.icon('filter')}${T('Filters')}${n ? ui.badge(String(n), { variant: 'default' }) : ''}`, size: 'sm', cls: 'cx-fbtn-m', attrs: 'data-fmobile aria-haspopup="dialog"' });
|
|
55
57
|
if (focused) { const i = dtTools.querySelector('[data-filter]'); i.focus(); i.setSelectionRange(i.value.length, i.value.length); }
|
|
56
58
|
return;
|
|
57
59
|
}
|
|
58
60
|
dtTools.innerHTML = FILTERS.map((f) => (f.type === 'search' ? searchHtml()
|
|
59
61
|
: ui.button({ label: `${isActive(f) ? '' : ui.icon('plus')}${f.label}${isActive(f) ? `<span data-slot="separator" class="cn-separator cn-separator-vertical cx-fsep"></span>${ui.badge(summary(f), { variant: 'secondary' })}` : ''}`, size: 'sm', cls: `cx-fbtn ${isActive(f) ? '' : 'cx-dashed'}`, attrs: `data-fopen="${f.id}" aria-haspopup="dialog" aria-expanded="false"` }))).join('')
|
|
60
|
-
+ (FILTERS.some(isActive) ? ui.button({ label: '
|
|
62
|
+
+ (FILTERS.some(isActive) ? ui.button({ label: T('Clear'), size: 'sm', variant: 'ghost', attrs: 'data-fclear' }) : '');
|
|
61
63
|
};
|
|
62
64
|
const fPop = document.createElement('div'); fPop.className = 'cn-popover-content pg-pop cx-fpop'; fPop.setAttribute('role', 'dialog'); fPop.hidden = true; lib.appendChild(fPop);
|
|
63
65
|
let fOpen = null, fBtn = null;
|
|
64
66
|
const fClose = (refocus) => { if (!fOpen) return; hide(fPop, 120); fBtn?.setAttribute('aria-expanded', 'false'); if (refocus) fBtn?.focus(); fOpen = null; };
|
|
65
|
-
const rangeInputs = (f, prefix) => { const v = fState[f.id] || [null, null]; return `<div class="cx-frange">${['
|
|
67
|
+
const rangeInputs = (f, prefix) => { const v = fState[f.id] || [null, null]; return `<div class="cx-frange">${[T('From'), T('To')].map((lab, i) => ui.field({ id: `${prefix}${f.id}${i}`, label: lab, control: `<div data-slot="input-group" class="cn-input-group"><div class="cn-input-group-addon cn-input-group-addon-align-inline-start">$</div><input class="cn-input cn-input-group-input cx-num" id="${prefix}${f.id}${i}" inputmode="numeric" data-frange="${i}" data-frid="${f.id}" value="${v[i] != null ? groupThousands(String(v[i]), '.') : ''}" placeholder="${i ? T('No limit') : '0'}"></div>` })).join('')}</div>`; };
|
|
66
68
|
const facetItems = (f, sheet) => { const counts = new Map(); DT.filter((r) => passes(r, f.id)).forEach((r) => counts.set(f.read(r), (counts.get(f.read(r)) || 0) + 1)); return [...new Set(DT.map(f.read))].map((v) => sheet
|
|
67
69
|
? `<button type="button" class="cn-command-item cx-fsheet-opt" role="checkbox" aria-checked="${fState[f.id].has(v)}" data-fs-facet="${f.id}" data-fval="${v}"><span data-slot="checkbox" class="cn-checkbox" data-state="${fState[f.id].has(v) ? 'checked' : 'unchecked'}"><span class="cn-checkbox-indicator"><svg class="cb-mark" viewBox="0 0 24 24">${CB_CHECK}</svg></span></span>${ui.text(f.name(v))}<span class="cx-fcount">${counts.get(v) || 0}</span></button>`
|
|
68
70
|
: `<div data-slot="command-item" class="cn-command-item group/command-item" role="option" aria-selected="${fState[f.id].has(v)}" data-fval="${v}" tabindex="-1"><span data-slot="checkbox" class="cn-checkbox" data-state="${fState[f.id].has(v) ? 'checked' : 'unchecked'}"><span class="cn-checkbox-indicator"><svg class="cb-mark" viewBox="0 0 24 24">${CB_CHECK}</svg></span></span>${ui.text(f.name(v))}<span class="cx-fcount">${counts.get(v) || 0}</span></div>`).join(''); };
|
|
69
71
|
const popBody = (f) => {
|
|
70
|
-
if (f.type === 'facet') return `<div data-slot="command" class="cn-command"><div data-slot="command-input-wrapper" class="cn-command-input-wrapper"><div data-slot="input-group" class="cn-input-group cn-command-input-group group/input-group"><input data-slot="input-group-control" class="cn-command-input cn-input-group-input" placeholder="${f.label}…" data-fsearch aria-label="
|
|
71
|
-
if (f.type === 'date') return `<div class="cx-vstack" style="gap:4px">${Object.entries(DATE_PRESETS).map(([k, [n]]) => ui.button({ label: n, variant: 'ghost', size: 'sm', cls: 'cx-fill cx-left', attrs: `aria-pressed="${fState[f.id] === k}" data-fdate="${k}"` })).join('')}${fState[f.id] ? ui.button({ label: '
|
|
72
|
+
if (f.type === 'facet') return `<div data-slot="command" class="cn-command"><div data-slot="command-input-wrapper" class="cn-command-input-wrapper"><div data-slot="input-group" class="cn-input-group cn-command-input-group group/input-group"><input data-slot="input-group-control" class="cn-command-input cn-input-group-input" placeholder="${f.label}…" data-fsearch aria-label="${T('Search in {0}', f.label)}"><div class="cn-input-group-addon cn-input-group-addon-align-inline-start" data-align="inline-start"><svg class="cn-command-input-icon"><use href="#search"/></svg></div></div></div><div data-slot="command-list" class="cn-command-list" role="listbox" aria-multiselectable="true">${facetItems(f)}</div>${fState[f.id].size ? `<div data-slot="separator" class="cn-separator cn-separator-horizontal"></div><button class="cn-command-item cx-fclear-one" data-fclear-one>${T('Remove filter')}</button>` : ''}</div>`;
|
|
73
|
+
if (f.type === 'date') return `<div class="cx-vstack" style="gap:4px">${Object.entries(DATE_PRESETS).map(([k, [n]]) => ui.button({ label: n, variant: 'ghost', size: 'sm', cls: 'cx-fill cx-left', attrs: `aria-pressed="${fState[f.id] === k}" data-fdate="${k}"` })).join('')}${fState[f.id] ? ui.button({ label: T('Any date'), variant: 'ghost', size: 'sm', cls: 'cx-fill cx-left', attrs: 'data-fdate=""' }) : ''}</div>`;
|
|
72
74
|
if (f.type === 'range') return `<div class="cx-vstack" style="gap:10px">${ui.text(f.label, 'label')}${rangeInputs(f, 'f-')}</div>`;
|
|
73
75
|
return '';
|
|
74
76
|
};
|
|
75
77
|
const fOpenPop = (id, btn) => { fClose(); fOpen = id; fBtn = btn; const f = FILTERS.find((x) => x.id === id); fPop.innerHTML = popBody(f); fPop.hidden = false; place(fPop, btn, 'start', 6); show(fPop); btn.setAttribute('aria-expanded', 'true'); (fPop.querySelector('[data-fsearch], input, [data-fdate], .cn-command-item') || fPop).focus({ preventScroll: true }); };
|
|
76
78
|
const refresh = (keepPop) => { dtPaint(); paintTools(); if (keepPop && fOpen) { const f = FILTERS.find((x) => x.id === fOpen); const scroll = fPop.querySelector('.cn-command-list')?.scrollTop; const focused = document.activeElement?.dataset?.frange; fPop.innerHTML = popBody(f); if (scroll) fPop.querySelector('.cn-command-list').scrollTop = scroll; fBtn = dtTools.querySelector(`[data-fopen="${fOpen}"]`); fBtn?.setAttribute('aria-expanded', 'true'); if (focused != null) { const i = fPop.querySelector(`[data-frange="${focused}"]`); i?.focus(); i?.setSelectionRange(i.value.length, i.value.length); } } };
|
|
77
79
|
let mobileCount = DT.length, sheetOpen = false;
|
|
78
|
-
const syncMobileCount = (n) => { mobileCount = n; const go = $('[data-fsheet-go]', foot); if (go && sheetOpen) go.innerHTML =
|
|
80
|
+
const syncMobileCount = (n) => { mobileCount = n; const go = $('[data-fsheet-go]', foot); if (go && sheetOpen) go.innerHTML = T('See {0} {1}', n, n === 1 ? ONE : MANY); };
|
|
79
81
|
const sheetBody = () => FILTERS.filter((f) => f.type !== 'search').map((f) => {
|
|
80
82
|
const ctrl = f.type === 'facet' ? `<div class="cx-vstack" style="gap:2px">${facetItems(f, true)}</div>` : f.type === 'date' ? `<div class="cx-hstack">${Object.entries(DATE_PRESETS).map(([k, [n]]) => ui.button({ label: n, size: 'sm', attrs: `aria-pressed="${fState[f.id] === k}" data-fs-date="${f.id}|${k}"` })).join('')}</div>` : f.type === 'range' ? rangeInputs(f, 'fs-') : '';
|
|
81
83
|
return `<section class="cx-vstack" style="gap:10px">${ui.text(f.label, 'label')}${ctrl}</section>`;
|
|
82
84
|
}).join('<div data-slot="separator" class="cn-separator cn-separator-horizontal"></div>');
|
|
83
85
|
const sheetKind = 'dtFilters' + Math.random().toString(36).slice(2, 7);
|
|
84
|
-
CONTENT[sheetKind] = () => { sheetOpen = true; document.getElementById('mdl-title').textContent = '
|
|
86
|
+
CONTENT[sheetKind] = () => { sheetOpen = true; document.getElementById('mdl-title').textContent = T('Filters'); document.getElementById('mdl-desc').textContent = T('They apply as you pick.'); body.innerHTML = sheetBody(); foot.innerHTML = ui.button({ label: T('Clear'), size: 'default', attrs: 'data-fsheet-clear', cls: 'cx-exit' }) + ui.button({ label: T('See {0} {1}', mobileCount, mobileCount === 1 ? ONE : MANY), variant: 'default', size: 'default', attrs: 'data-dismiss data-fsheet-go', cls: 'cx-go' }); };
|
|
85
87
|
const sheetRefresh = () => { if (!sheetOpen) return; const st = body.scrollTop, focused = document.activeElement?.dataset?.frange; body.innerHTML = sheetBody(); body.scrollTop = st; if (focused != null) { const i = body.querySelector(`[data-frange="${focused}"]`); i?.focus(); i?.setSelectionRange(i.value.length, i.value.length); } };
|
|
86
88
|
const clearAll = () => FILTERS.forEach((f) => { fState[f.id] = f.type === 'search' ? (f.id === searchF?.id ? fState[f.id] : '') : f.type === 'facet' ? new Set() : null; });
|
|
87
89
|
body.addEventListener('click', (e) => {
|
|
@@ -126,7 +128,7 @@ export function dataTable(root, cfg, core, on) {
|
|
|
126
128
|
have.forEach((c, k) => { if (!want.has(k) && !c.classList.contains('out')) { c.classList.add('out'); c.addEventListener('animationend', () => easeHeight(dtChips, () => { c.remove(); dtChips.hidden = !dtChips.children.length; }), { once: true }); } });
|
|
127
129
|
easeHeight(dtChips, () => {
|
|
128
130
|
chips.forEach(([t, k]) => {
|
|
129
|
-
const html = `${t}${ui.button({ icon: 'x', variant: 'ghost', size: 'icon-xs', cls: 'cx-chip-x', attrs: `data-fchip="${k}" aria-label="
|
|
131
|
+
const html = `${t}${ui.button({ icon: 'x', variant: 'ghost', size: 'icon-xs', cls: 'cx-chip-x', attrs: `data-fchip="${k}" aria-label="${T('Remove {0}', t)}" title="${T('Remove')}"` })}`;
|
|
130
132
|
const c = have.get(k);
|
|
131
133
|
if (c) { if (c.dataset.text !== t) { c.innerHTML = html; c.dataset.text = t; } dtChips.appendChild(c); return; }
|
|
132
134
|
const el = document.createElement('span'); el.dataset.slot = 'badge'; el.className = 'cn-badge cn-badge-variant-secondary cx-chip in'; el.dataset.key = k; el.dataset.text = t; el.innerHTML = html; dtChips.appendChild(el);
|
|
@@ -144,8 +146,8 @@ export function dataTable(root, cfg, core, on) {
|
|
|
144
146
|
return `<td class="cx-dt-svc" ${dateCol ? `data-when="${whenText(r[dateCol.key])}"` : ''}>${v ?? ''}</td>`;
|
|
145
147
|
};
|
|
146
148
|
const who = (r) => r[colOf('person')?.key ?? COLS[0]?.key] ?? '';
|
|
147
|
-
const rowCells = (r) => `<td class="cx-dt-check">${cbHtml('
|
|
148
|
-
const EASE = 'cubic-bezier(.
|
|
149
|
+
const rowCells = (r) => `<td class="cx-dt-check">${cbHtml(T('Select {0}', who(r)), dtSel.has(r.id))}</td>${COLS.map((c) => cell(c, r)).join('')}`;
|
|
150
|
+
const EASE = 'cubic-bezier(.23, 1, .32, 1)';
|
|
149
151
|
let dtAnim = 0;
|
|
150
152
|
const dtPaint = () => {
|
|
151
153
|
const f = SORTERS[dtSort.key] || (() => 0);
|
|
@@ -160,7 +162,7 @@ export function dataTable(root, cfg, core, on) {
|
|
|
160
162
|
const byId = new Map([...dtBody.children].map((tr) => [tr.dataset.id, tr]));
|
|
161
163
|
const frag = document.createDocumentFragment(), entering = [];
|
|
162
164
|
rows.forEach((r) => { let tr = byId.get(String(r.id)); if (!tr) { tr = document.createElement('tr'); tr.dataset.id = r.id; entering.push(tr); } tr.toggleAttribute('data-selected', dtSel.has(r.id)); tr.innerHTML = rowCells(r); frag.appendChild(tr); });
|
|
163
|
-
if (!rows.length) { let tr = byId.get('empty'); if (!tr) { tr = document.createElement('tr'); tr.dataset.id = 'empty'; tr.innerHTML = `<td colspan="${COLS.length + 1}"><div data-slot="empty" class="cn-empty cx-dt-empty"><div class="cn-empty-header"><div class="cn-empty-media cn-empty-media-icon">${ui.icon('search')}</div><div class="cn-empty-title"
|
|
165
|
+
if (!rows.length) { let tr = byId.get('empty'); if (!tr) { tr = document.createElement('tr'); tr.dataset.id = 'empty'; tr.innerHTML = `<td colspan="${COLS.length + 1}"><div data-slot="empty" class="cn-empty cx-dt-empty"><div class="cn-empty-header"><div class="cn-empty-media cn-empty-media-icon">${ui.icon('search')}</div><div class="cn-empty-title">${T('Nothing matches the filters')}</div><div class="cn-empty-description">${T('Remove one or clear them all.')}</div></div><div class="cn-empty-content">${ui.button({ label: T('Clear filters'), attrs: 'data-fclear-empty' })}</div></div></td>`; entering.push(tr); } frag.appendChild(tr); }
|
|
164
166
|
dtBody.replaceChildren(frag);
|
|
165
167
|
dtBody.querySelectorAll('tr').forEach((tr) => { const old = before.get(tr.dataset.id); if (old == null) return; const dy = old - tr.getBoundingClientRect().top; if (dy) tr.animate([{ transform: `translateY(${dy}px)` }, { transform: 'none' }], { duration: 240, easing: EASE }); });
|
|
166
168
|
entering.forEach((tr, i) => tr.animate([{ opacity: 0, transform: 'translateY(6px)' }, { opacity: 1, transform: 'none' }], { duration: 220, delay: Math.min(i, 6) * 18, easing: EASE, fill: 'backwards' }));
|