@muten/shadcn 0.1.15 → 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 +328 -123
- 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
package/registry/kit/values.js
CHANGED
|
@@ -2,19 +2,23 @@
|
|
|
2
2
|
// CodePicker (a searchable list under a trigger), MoneyField (a currency = code + locale, Intl does the rest),
|
|
3
3
|
// PhoneField (a country = dial code + digits + mask) and the 6-slot verification code. Generic: data and labels in.
|
|
4
4
|
export const CURRENCIES = [
|
|
5
|
-
{ id: 'COP', label: '
|
|
6
|
-
{ id: 'MXN', label: '
|
|
7
|
-
{ id: 'CLP', label: '
|
|
8
|
-
{ id: 'EUR', label: 'Euro', locale: 'es-ES' }, { id: 'CAD', label: '
|
|
5
|
+
{ id: 'COP', label: 'Colombian peso', locale: 'es-CO', minor: 2, shown: 0 }, { id: 'USD', label: 'US dollar', locale: 'en-US' },
|
|
6
|
+
{ id: 'MXN', label: 'Mexican peso', locale: 'es-MX' }, { id: 'ARS', label: 'Argentine peso', locale: 'es-AR' }, { id: 'VES', label: 'Bolívar', locale: 'es-VE' },
|
|
7
|
+
{ id: 'CLP', label: 'Chilean peso', locale: 'es-CL' }, { id: 'PEN', label: 'Peruvian sol', locale: 'es-PE' }, { id: 'BRL', label: 'Brazilian real', locale: 'pt-BR' },
|
|
8
|
+
{ id: 'EUR', label: 'Euro', locale: 'es-ES' }, { id: 'CAD', label: 'Canadian dollar', locale: 'en-CA' }, { id: 'AUD', label: 'Australian dollar', locale: 'en-AU' }, { id: 'JPY', label: 'Yen', locale: 'ja-JP' },
|
|
9
9
|
];
|
|
10
10
|
export const COUNTRIES = [
|
|
11
11
|
{ id: 'AR', label: 'Argentina', dial: '+54 9', e164: '549', len: 10, mask: '## ####-####' }, { id: 'CO', label: 'Colombia', dial: '+57', e164: '57', len: 10, mask: '### ### ####' },
|
|
12
|
-
{ id: 'MX', label: '
|
|
13
|
-
{ id: 'VE', label: 'Venezuela', dial: '+58', e164: '58', len: 10, mask: '### ### ####' }, { id: 'BR', label: '
|
|
14
|
-
{ id: 'CL', label: 'Chile', dial: '+56', e164: '56', len: 9, mask: '# #### ####' }, { id: 'PE', label: '
|
|
15
|
-
{ id: 'ES', label: '
|
|
12
|
+
{ id: 'MX', label: 'Mexico', dial: '+52', e164: '52', len: 10, mask: '## #### ####' }, { id: 'US', label: 'United States', dial: '+1', e164: '1', len: 10, mask: '(###) ###-####' },
|
|
13
|
+
{ id: 'VE', label: 'Venezuela', dial: '+58', e164: '58', len: 10, mask: '### ### ####' }, { id: 'BR', label: 'Brazil', dial: '+55', e164: '55', len: 11, mask: '(##) #####-####' },
|
|
14
|
+
{ id: 'CL', label: 'Chile', dial: '+56', e164: '56', len: 9, mask: '# #### ####' }, { id: 'PE', label: 'Peru', dial: '+51', e164: '51', len: 9, mask: '### ### ###' },
|
|
15
|
+
{ id: 'ES', label: 'Spain', dial: '+34', e164: '34', len: 9, mask: '### ## ## ##' }, { id: 'CA', label: 'Canada', dial: '+1', e164: '1', len: 10, mask: '(###) ###-####' },
|
|
16
16
|
];
|
|
17
17
|
|
|
18
|
+
// a currency's or a country's name in a language (Intl knows them all); the table's English label when it cannot
|
|
19
|
+
const nameIn = (lang, type, id, fallback) => { try { return new Intl.DisplayNames([lang], { type }).of(id) || fallback; } catch { return fallback; } };
|
|
20
|
+
const cap = (x) => x.charAt(0).toUpperCase() + x.slice(1);
|
|
21
|
+
|
|
18
22
|
export function codePicker(core, { trigger, anchor, items, current, onPick, placeholder }) {
|
|
19
23
|
const { $, $$, lib, place, show, hide, fold } = core;
|
|
20
24
|
const pop = document.createElement('div');
|
|
@@ -52,13 +56,13 @@ export function codePicker(core, { trigger, anchor, items, current, onPick, plac
|
|
|
52
56
|
// ScaleSlider: one or two thumbs, the value rides on each thumb, two bubbles that would touch merge into one.
|
|
53
57
|
// i: {label, min, max, step, values "24" | "20000,80000", gap, format: hours|money|percent|"" , suffix}
|
|
54
58
|
export function scaleSlider(root, i, core, on) {
|
|
55
|
-
const { $, $$, money0 } = core;
|
|
59
|
+
const { $, $$, money0, t, langOf } = core, T = (k, ...a) => t(langOf(root), k, ...a);
|
|
56
60
|
const min = Number(i.min) || 0, max = Number(i.max) || 100, step = Number(i.step) || 1, gap = Number(i.gap) || 0;
|
|
57
61
|
const FMT = { hours: (v) => `${v} h`, money: money0, percent: (v) => `${v} %` };
|
|
58
62
|
const fmt = FMT[i.format] || ((v) => `${v}${i.suffix || ''}`);
|
|
59
63
|
const vals = String(i.values ?? min).split(',').map(Number), two = vals.length > 1, TW = 21;
|
|
60
64
|
const id = 'sl' + Math.random().toString(36).slice(2, 7);
|
|
61
|
-
root.innerHTML = `<div class="cx-sl-field"><div class="cx-sl-head"><span class="cn-label" id="${id}-l">${i.label || ''}</span></div><div data-slot="slider" class="cn-slider cx-slider" data-orientation="horizontal" data-horizontal><div data-slot="slider-track" class="cn-slider-track" data-orientation="horizontal"><div data-slot="slider-range" class="cn-slider-range" data-orientation="horizontal"></div></div>${vals.map((_, k) => `<span data-slot="slider-thumb" class="cn-slider-thumb" tabindex="0" role="slider" ${two ? `aria-label="${i.label || ''} ${k ? '
|
|
65
|
+
root.innerHTML = `<div class="cx-sl-field"><div class="cx-sl-head"><span class="cn-label" id="${id}-l">${i.label || ''}</span></div><div data-slot="slider" class="cn-slider cx-slider" data-orientation="horizontal" data-horizontal><div data-slot="slider-track" class="cn-slider-track" data-orientation="horizontal"><div data-slot="slider-range" class="cn-slider-range" data-orientation="horizontal"></div></div>${vals.map((_, k) => `<span data-slot="slider-thumb" class="cn-slider-thumb" tabindex="0" role="slider" ${two ? `aria-label="${i.label || ''} ${k ? T('maximum') : T('minimum')}"` : `aria-labelledby="${id}-l"`}></span>`).join('')}</div><div class="cx-sl-scale"><span>${fmt(min)}</span><span>${fmt(max)}</span></div></div>`;
|
|
62
66
|
const sl = $('.cx-slider', root), thumbs = $$('.cn-slider-thumb', sl), range = $('.cn-slider-range', sl);
|
|
63
67
|
const bubbles = thumbs.map(() => { const b = document.createElement('span'); b.className = 'cx-sl-bubble'; b.setAttribute('aria-hidden', 'true'); sl.appendChild(b); return b; });
|
|
64
68
|
const bounds = (k) => [two && k === 1 ? vals[0] + gap : min, two && k === 0 ? vals[1] - gap : max];
|
|
@@ -98,9 +102,9 @@ export function scaleSlider(root, i, core, on) {
|
|
|
98
102
|
|
|
99
103
|
// MoneyField: i {label, value (a number), currency "COP", hint "on"} → on.change(json {amount, minor, currency})
|
|
100
104
|
export function moneyField(root, i, core, on) {
|
|
101
|
-
const { $, ui, reformat, groupThousands } = core;
|
|
105
|
+
const { $, ui, reformat, groupThousands, t, langOf } = core, L = langOf(root), T = (k, ...a) => t(L, k, ...a);
|
|
102
106
|
const id = 'mf' + Math.random().toString(36).slice(2, 7);
|
|
103
|
-
root.innerHTML = `<div data-slot="field" class="cn-field cn-field-orientation-vertical group/field"><label data-slot="label" class="cn-label" for="${id}">${i.label || '
|
|
107
|
+
root.innerHTML = `<div data-slot="field" class="cn-field cn-field-orientation-vertical group/field"><label data-slot="label" class="cn-label" for="${id}">${T(i.label || 'Price')}</label><div data-slot="input-group" class="cn-input-group group/input-group" data-mf-group><div data-slot="input-group-addon" class="cn-input-group-addon cn-input-group-addon-align-inline-start cx-num" data-mf-sym>$</div><input data-slot="input-group-control" class="cn-input cn-input-group-input cx-num" id="${id}" inputmode="decimal" autocomplete="off"><div data-slot="input-group-addon" class="cn-input-group-addon cn-input-group-addon-align-inline-end" style="padding-right:2px"><button data-slot="button" class="cn-button cn-button-variant-ghost cn-button-size-xs group/button cx-cc" data-mf-cur aria-haspopup="dialog" aria-expanded="false" aria-label="${T('Currency')}"><span class="cx-cc-code" data-mf-code>COP</span><svg><use href="#chev"/></svg></button></div></div><p data-slot="field-description" class="cn-field-description cx-num" data-mf-out></p></div>`;
|
|
104
108
|
const currencySpec = (c) => {
|
|
105
109
|
const nf = new Intl.NumberFormat(c.locale, { style: 'currency', currency: c.id, currencyDisplay: 'narrowSymbol' });
|
|
106
110
|
const parts = nf.formatToParts(1234567.89), part = (t, d) => parts.find((p) => p.type === t)?.value ?? d;
|
|
@@ -114,42 +118,46 @@ export function moneyField(root, i, core, on) {
|
|
|
114
118
|
const sigFor = () => new RegExp(`[\\d${escRe(cur.dec)}]`);
|
|
115
119
|
const fmtMoney = (v) => { const k = cur.places ? v.indexOf(cur.dec) : -1; const int = (k < 0 ? v : v.slice(0, k)).replace(/\D/g, '').slice(0, 12); const frac = k < 0 ? '' : v.slice(k + 1).replace(/\D/g, '').slice(0, cur.places); return (groupThousands(int, cur.group) || (k >= 0 ? '0' : '')) + (k >= 0 ? cur.dec + frac : ''); };
|
|
116
120
|
const amount = () => { if (!mf.value) return 0; const [a, f = ''] = mf.value.split(cur.dec); return Number(a.split(cur.group).join('') + '.' + (f || '0')); };
|
|
117
|
-
const unitName = () => (cur.minor === 0 ?
|
|
118
|
-
const showMinor = () => { mfOut.textContent = mf.value ?
|
|
121
|
+
const unitName = () => (cur.minor === 0 ? T('{0} (it has no minor unit)', curRow.id) : T('cents of {0}', curRow.id));
|
|
122
|
+
const showMinor = () => { mfOut.textContent = mf.value ? T('Saved as: {0} {1}', Math.round(amount() * 10 ** cur.minor), unitName()) : T('Type a price'); };
|
|
119
123
|
const toText = (n) => { const [a, f] = n.toFixed(cur.places).split('.'); return groupThousands(a, cur.group) + (cur.places && /[1-9]/.test(f) ? cur.dec + f : ''); };
|
|
120
124
|
const emit = () => on?.change?.(JSON.stringify({ amount: amount(), minor: Math.round(amount() * 10 ** cur.minor), currency: curRow.id }));
|
|
121
125
|
mf.addEventListener('input', () => { reformat(mf, fmtMoney, sigFor()); showMinor(); emit(); });
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
126
|
+
// a decimal key: in a currency with no decimals it is not written (it used to drop the comma and glue the digits after
|
|
127
|
+
// it to the whole part, ten times the amount); in one with decimals either key writes that currency's separator
|
|
128
|
+
mf.addEventListener('keydown', (e) => { if (e.key !== '.' && e.key !== ',') return; if (!cur.places) { e.preventDefault(); return; } if (e.key === cur.dec) return; e.preventDefault(); if (!mf.value.includes(cur.dec)) { mf.setRangeText(cur.dec, mf.selectionStart, mf.selectionEnd, 'end'); mf.dispatchEvent(new Event('input')); } });
|
|
129
|
+
// the currency's own shape as the empty field's hint: “0” or “0,00” / “0.00”
|
|
130
|
+
const setCurrency = (row) => { const n = amount(); curRow = row; cur = currencySpec(row); $('[data-mf-sym]', root).textContent = cur.sym; $('[data-mf-code]', root).textContent = row.id; mf.placeholder = cur.places ? `0${cur.dec}${'0'.repeat(cur.places)}` : '0'; mf.value = n ? toText(n) : ''; showMinor(); };
|
|
131
|
+
codePicker(core, { trigger: $('[data-mf-cur]', root), anchor: $('[data-mf-group]', root), placeholder: T('Search currency'), current: () => curRow.id, items: () => CURRENCIES.map((c) => ({ id: c.id, label: cap(nameIn(L, 'currency', c.id, c.label)), hint: currencySpec(c).sym })), onPick: (it) => { setCurrency(CURRENCIES.find((c) => c.id === it.id)); mf.focus({ preventScroll: true }); emit(); } });
|
|
125
132
|
mf.value = fmtMoney(String(i.value ?? '')); setCurrency(curRow);
|
|
126
133
|
void ui;
|
|
127
134
|
}
|
|
128
135
|
|
|
129
136
|
// PhoneField: i {label, value (digits), country "CO"} → on.change(json {e164, valid, country})
|
|
130
137
|
export function phoneField(root, i, core, on) {
|
|
138
|
+
const L = core.langOf(root), T = (k, ...a) => core.t(L, k, ...a);
|
|
131
139
|
const { $, reformat } = core;
|
|
132
140
|
const id = 'ph' + Math.random().toString(36).slice(2, 7);
|
|
133
|
-
root.innerHTML = `<div data-slot="field" class="cn-field cn-field-orientation-vertical group/field" data-ph-f><label data-slot="label" class="cn-label" for="${id}">${i.label || '
|
|
141
|
+
root.innerHTML = `<div data-slot="field" class="cn-field cn-field-orientation-vertical group/field" data-ph-f><label data-slot="label" class="cn-label" for="${id}">${T(i.label || 'Phone')}</label><div data-slot="input-group" class="cn-input-group group/input-group cx-phone" data-ph-group><div data-slot="input-group-addon" class="cn-input-group-addon cn-input-group-addon-align-inline-start" style="padding-left:2px"><button data-slot="button" class="cn-button cn-button-variant-ghost cn-button-size-xs group/button cx-cc" data-ph-cc aria-haspopup="dialog" aria-expanded="false" aria-label="${T('Country')}"><span class="cx-cc-code" data-ph-flag></span><span class="cx-num cx-cc-dial" data-ph-dial></span><svg><use href="#chev"/></svg></button></div><input data-slot="input-group-control" class="cn-input cn-input-group-input cx-num" id="${id}" type="tel" inputmode="tel" autocomplete="tel-national" aria-describedby="${id}-msg"></div><p data-slot="field-description" class="cn-field-description cx-msg cx-num" id="${id}-msg"></p></div>`;
|
|
134
142
|
const ph = $('input', root), phMsg = $(`#${id}-msg`, root), phF = $('[data-ph-f]', root);
|
|
135
143
|
let country = COUNTRIES.find((c) => c.id === i.country) || COUNTRIES[1];
|
|
136
144
|
const phDigits = (v) => { let d = v.replace(/\D/g, ''); if (d.length > country.len && d.startsWith(country.e164)) d = d.slice(country.e164.length); return d.slice(0, country.len); };
|
|
137
145
|
const fmtPhone = (v) => { const d = phDigits(v); let out = '', k = 0; for (const ch of country.mask) { if (k >= d.length) break; out += ch === '#' ? d[k++] : ch; } return out; };
|
|
138
146
|
const emit = () => { const d = phDigits(ph.value); on?.change?.(JSON.stringify({ e164: `+${country.e164}${d}`, valid: d.length === country.len, country: country.id })); };
|
|
139
|
-
const phCheck = (final) => { const d = phDigits(ph.value), ok = d.length === country.len, bad = final && d.length > 0 && !ok; phF.toggleAttribute('data-invalid', bad); ph.setAttribute('aria-invalid', bad); phMsg.textContent = ok ?
|
|
147
|
+
const phCheck = (final) => { const d = phDigits(ph.value), ok = d.length === country.len, bad = final && d.length > 0 && !ok; phF.toggleAttribute('data-invalid', bad); ph.setAttribute('aria-invalid', bad); phMsg.textContent = ok ? T('Saved as +{0}', `${country.e164}${d}`) : bad ? T('{0} digits short for a number from {1}', country.len - d.length, nameIn(L, 'region', country.id, country.label)) : T('{0} digits, without the {1}', country.len, country.dial); };
|
|
140
148
|
const setCountry = (c) => { country = c; $('[data-ph-flag]', root).textContent = c.id; $('[data-ph-dial]', root).textContent = c.dial; ph.placeholder = c.mask.replace(/#/g, '0'); ph.value = fmtPhone(ph.value); phCheck(false); };
|
|
141
149
|
ph.addEventListener('input', () => { reformat(ph, fmtPhone, /\d/); phCheck(phF.hasAttribute('data-invalid')); emit(); });
|
|
142
150
|
ph.addEventListener('blur', () => phCheck(true));
|
|
143
|
-
codePicker(core, { trigger: $('[data-ph-cc]', root), anchor: $('[data-ph-group]', root), placeholder: '
|
|
151
|
+
codePicker(core, { trigger: $('[data-ph-cc]', root), anchor: $('[data-ph-group]', root), placeholder: T('Search country'), current: () => country.id, items: () => COUNTRIES.map((c) => ({ id: c.id, label: nameIn(L, 'region', c.id, c.label), hint: c.dial })), onPick: (it) => { setCountry(COUNTRIES.find((c) => c.id === it.id)); ph.focus({ preventScroll: true }); emit(); } });
|
|
144
152
|
setCountry(country); ph.value = fmtPhone(String(i.value ?? '')); phCheck(false);
|
|
145
153
|
}
|
|
146
154
|
|
|
147
155
|
// OtpField: 6 slots in two groups of 3. i {length, hint, state: ""|checking|ok|bad, message, resendIn} →
|
|
148
156
|
// on.complete(code) when full, on.resend(). The page checks the code and answers with state + message.
|
|
149
157
|
export function otpField(root, i, core, on) {
|
|
150
|
-
const { $ } = core;
|
|
158
|
+
const { $ } = core, T = (k, ...a) => core.t(core.langOf(root), k, ...a);
|
|
151
159
|
const len = Number(i.length) || 6, half = Math.ceil(len / 2);
|
|
152
|
-
root.innerHTML = `<div class="cx-vline cx-otp-stack"><div class="cx-otp"><input class="cx-otp-input" inputmode="numeric" autocomplete="one-time-code" maxlength="${len}" aria-label="
|
|
160
|
+
root.innerHTML = `<div class="cx-vline cx-otp-stack"><div class="cx-otp"><input class="cx-otp-input" inputmode="numeric" autocomplete="one-time-code" maxlength="${len}" aria-label="${T('{0}-digit code', len)}"><div data-slot="input-otp" class="cn-input-otp" aria-hidden="true"><div data-slot="input-otp-group" class="cn-input-otp-group"></div><div data-slot="input-otp-separator" class="cn-input-otp-separator cx-otp-sep"><span></span></div><div data-slot="input-otp-group" class="cn-input-otp-group"></div></div></div><p class="cx-note" data-otp-msg></p><button data-slot="button" class="cn-button cn-button-variant-link cn-button-size-sm group/button" data-otp-resend disabled></button></div>`;
|
|
153
161
|
const otpIn = $('.cx-otp-input', root), otpBox = $('.cx-otp', root), otpMsg = $('[data-otp-msg]', root), resend = $('[data-otp-resend]', root);
|
|
154
162
|
const slots = [];
|
|
155
163
|
root.querySelectorAll('.cn-input-otp-group').forEach((g, gi) => { const n = gi ? len - half : half; for (let k = 0; k < n; k++) { const s = document.createElement('div'); s.className = 'cn-input-otp-slot'; s.dataset.slot = 'input-otp-slot'; g.appendChild(s); slots.push(s); } });
|
|
@@ -159,20 +167,20 @@ export function otpField(root, i, core, on) {
|
|
|
159
167
|
const toEnd = () => { const n = otpIn.value.length; otpIn.setSelectionRange(n, n); };
|
|
160
168
|
otpIn.addEventListener('input', () => {
|
|
161
169
|
otpIn.value = otpIn.value.replace(/\D/g, '').slice(0, len); delete otpBox.dataset.state; otpMsg.removeAttribute('data-tone'); otpMsg.textContent = hint; paint();
|
|
162
|
-
if (otpIn.value.length === len) { busy = true; otpIn.readOnly = true; otpMsg.textContent = '
|
|
170
|
+
if (otpIn.value.length === len) { busy = true; otpIn.readOnly = true; otpMsg.textContent = T('Checking…'); paint(); on?.complete?.(otpIn.value); }
|
|
163
171
|
});
|
|
164
172
|
['focus', 'blur', 'click', 'keyup'].forEach((t) => otpIn.addEventListener(t, () => { toEnd(); paint(); }));
|
|
165
173
|
otpIn.addEventListener('select', toEnd);
|
|
166
174
|
let left = Number(i.resendIn ?? 30), rt = 0;
|
|
167
|
-
const tick = () => { left--; resend.disabled = left > 0; resend.textContent = left > 0 ?
|
|
168
|
-
resend.textContent =
|
|
169
|
-
resend.onclick = () => { left = Number(i.resendIn ?? 30) + 1; clearTimeout(rt); tick(); otpMsg.removeAttribute('data-tone'); otpMsg.textContent = '
|
|
175
|
+
const tick = () => { left--; resend.disabled = left > 0; resend.textContent = left > 0 ? T('Resend in 0:{0}', String(left).padStart(2, '0')) : T('Resend code'); if (left > 0) rt = setTimeout(tick, 1000); };
|
|
176
|
+
resend.textContent = T('Resend in 0:{0}', String(left).padStart(2, '0'));
|
|
177
|
+
resend.onclick = () => { left = Number(i.resendIn ?? 30) + 1; clearTimeout(rt); tick(); otpMsg.removeAttribute('data-tone'); otpMsg.textContent = T('We sent you a new code'); otpIn.value = ''; delete otpBox.dataset.state; otpIn.focus(); on?.resend?.(); };
|
|
170
178
|
rt = setTimeout(tick, 1000); paint();
|
|
171
179
|
const answer = (st, msg) => {
|
|
172
180
|
if (!st || st === 'checking') return;
|
|
173
181
|
busy = false; otpIn.readOnly = false;
|
|
174
|
-
if (st === 'ok') { otpBox.dataset.state = 'ok'; otpMsg.dataset.tone = 'ok'; otpMsg.textContent = msg || '
|
|
175
|
-
if (st === 'bad') { otpBox.dataset.state = 'bad'; otpMsg.dataset.tone = 'bad'; otpMsg.textContent = msg || '
|
|
182
|
+
if (st === 'ok') { otpBox.dataset.state = 'ok'; otpMsg.dataset.tone = 'ok'; otpMsg.textContent = msg || T('Done, it is verified'); otpIn.blur(); }
|
|
183
|
+
if (st === 'bad') { otpBox.dataset.state = 'bad'; otpMsg.dataset.tone = 'bad'; otpMsg.textContent = msg || T('That is not the code'); const wrong = otpIn.value; setTimeout(() => { if (otpIn.value === wrong) { otpIn.value = ''; paint(); } }, 450); }
|
|
176
184
|
paint();
|
|
177
185
|
};
|
|
178
186
|
return (next) => answer(next.state, next.message);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Skeletons / Loading - while there is NOTHING to show, the shape of the content: built from the same atoms (Item,
|
|
2
2
|
# Card, Stat, bubbles) with Skeleton in place of the text, so when the data lands only the pixels change, with a fade
|
|
3
|
-
# and no jump in height. A refresh keeps the content: gate on
|
|
3
|
+
# and no jump in height. A refresh keeps the content: gate on “nothing yet”, never on “fetching again”.
|
|
4
4
|
# Loading(busy: orders.loading, kind: "list", count: 4) { each orders as o { … } }
|
|
5
5
|
# Skeletons(kind: "messaging")
|
|
6
6
|
# kind: list · item · stat · stats · card · products · bubbles · table · messaging · agenda. media: avatar · icon · none.
|
package/registry/kit-widget.js
CHANGED
|
@@ -11,7 +11,7 @@ export function mount(el, inputs, on) {
|
|
|
11
11
|
const conversations = list(i.conversations).map((c) => ({ ...c, messages: list(c.messages).map((m) => (m.event ? { ...m, event: { ...m.event, actions: Array.isArray(m.event.actions) ? buttons(m.event.actions) : m.event.actions || '' } } : m)) }));
|
|
12
12
|
const replies = list(i.replies);
|
|
13
13
|
kit.messaging(el, {
|
|
14
|
-
title: i.title || '
|
|
14
|
+
title: i.title || core0.t(core0.langOf(el), 'Messages'), channels: map(i.channels), conversations,
|
|
15
15
|
filters: list(i.filters).map((f) => ({ id: f.id, label: f.label, match: f.state ? (c) => c.state === f.state : () => true })),
|
|
16
16
|
states: map(i.states), control: map(i.controls),
|
|
17
17
|
quick: list(i.quick).map((q) => ({ ...q, card: () => `${ui.text(q.title || q.label, 'label')}${q.subtitle ? ui.text(q.subtitle, 'caption') : ''}` })),
|
|
@@ -22,7 +22,7 @@ export function mount(el, inputs, on) {
|
|
|
22
22
|
},
|
|
23
23
|
chatbot: ({ kit, ui }, i) => {
|
|
24
24
|
const answers = list(i.answers);
|
|
25
|
-
kit.chatbot(el, { name: i.title || '
|
|
25
|
+
kit.chatbot(el, { name: i.title || core0.t(core0.langOf(el), 'Assistant'), greeting: i.greeting || '', avatar: i.avatar || '', suggestions: list(i.suggestions).map((s) => s.text ?? s),
|
|
26
26
|
respond: (t) => { const a = answers.find((x) => x.match && rx(x.match).test(t)) || answers.find((x) => !x.match) || { text: '' }; return { text: a.text, actions: a.action ? ui.button({ label: a.action, variant: a.primary ? 'default' : 'outline', size: 'xs', attrs: `data-kw-act="${a.action}"` }) : '' }; } });
|
|
27
27
|
if (!el.__kwBound) { el.__kwBound = true; el.addEventListener('click', (e) => { const b = e.target.closest('[data-kw-act]'); if (b) on.action?.(b.dataset.kwAct); }); }
|
|
28
28
|
},
|
|
@@ -39,8 +39,8 @@ export function mount(el, inputs, on) {
|
|
|
39
39
|
stampCard: ({ kit }, i) => kit.stampCard(el, { store: i.store || '', owner: i.owner || '', total: Number(i.total) || 10, stamps: Number(i.stamps) || 0, reward: i.reward || '', ...Object.fromEntries(['title', 'progress', 'complete', 'count', 'redeem', 'stamp'].filter((k) => i[k]).map((k) => [k, i[k]])) }),
|
|
40
40
|
bell: ({ kit }, i) => kit.bell(el, { items: list(i.items) }),
|
|
41
41
|
queue: ({ kit }, i) => kit.queue(el, { title: i.title || '', people: list(i.people), offerMinutes: Number(i.minutes) || 10 }),
|
|
42
|
-
password: ({ kit }, i) => kit.password(el, { label: i.label || '
|
|
43
|
-
// days: {name, open, ranges [["09:00","13:00"]]}. Inside a form with a DirtyBar,
|
|
42
|
+
password: ({ kit, t, langOf }, i) => kit.password(el, { label: t(langOf(el), i.label || 'Password') }),
|
|
43
|
+
// days: {name, open, ranges [["09:00","13:00"]]}. Inside a form with a DirtyBar, “Descartar” brings the hours back.
|
|
44
44
|
weeklyHours: ({ kit }, i) => {
|
|
45
45
|
const wh = kit.weeklyHours(el, { days: list(i.days).map((d) => ({ name: d.name, open: !!d.open, ranges: list(d.ranges).map((r) => [...r]) })) });
|
|
46
46
|
let saved = wh.value(); const form = el.closest('.cx-set-form');
|
|
@@ -51,9 +51,14 @@ export function mount(el, inputs, on) {
|
|
|
51
51
|
RENDER.table = (core, i) => import('@muten/shadcn/registry/kit/table.js').then(({ dataTable }) => dataTable(el, { columns: i.columns, rows: i.rows, filters: i.filters, statuses: i.statuses, noun: [i.one || 'fila', i.many || 'filas'], bulk: i.bulk }, core, on));
|
|
52
52
|
// until the kit arrives the host keeps its final size with a shimmer (kit.css, :empty), then the widget fades in
|
|
53
53
|
el.classList.add('cx-kw-wait');
|
|
54
|
+
// its language: inputs.lang, else the nearest lang (the page's); a lang that changes repaints it in the new one
|
|
55
|
+
if (inputs.lang) el.setAttribute('lang', inputs.lang);
|
|
56
|
+
let core0 = null;
|
|
54
57
|
import('@muten/shadcn/registry/kit/core.js').then((core) => {
|
|
58
|
+
core0 = core;
|
|
55
59
|
const render = RENDER[inputs.name]; if (!render) return;
|
|
56
60
|
render(core, inputs); el.classList.remove('cx-kw-wait');
|
|
61
|
+
document.addEventListener('kit:lang', () => { if (el.isConnected && !inputs.lang) { el.innerHTML = ''; render(core, inputs); } });
|
|
57
62
|
el.animate([{ opacity: 0.001 }, { opacity: 1 }], { duration: 180, easing: 'ease-out' });
|
|
58
63
|
play(core, render);
|
|
59
64
|
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// LangScope (muten Custom) - puts `lang` on the scope it sits first in, so every piece inside (kit/i18n.js langOf)
|
|
2
|
+
// speaks it; a new lang later changes it in place and the kit repaints in it (kit/core.js listens for lang changes).
|
|
3
|
+
export function mount(el, inputs) {
|
|
4
|
+
const scope = el.parentElement;
|
|
5
|
+
const set = (lang) => { const v = String(lang || ''); if (!scope) return; if (v) { if (scope.getAttribute('lang') !== v) scope.setAttribute('lang', v); } else scope.removeAttribute('lang'); };
|
|
6
|
+
set(inputs.lang);
|
|
7
|
+
return (next) => set(next.lang);
|
|
8
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# LangScope - everything inside speaks this language (es | en …): the library's words, its dates and its Customs follow
|
|
2
|
+
# it. Without it they follow the page's <html lang>, English when there is none. A store in the shop's language:
|
|
3
|
+
# LangScope(lang: shopLang) { … the store's parts … }
|
|
4
|
+
# shadcn-for-muten · Custom · host JS is lang-scope.js.
|
|
5
|
+
part LangScope(lang: text) { Stack class("cx-lang-scope") { Custom LangScope inputs(lang: $lang) class("cx-lang-host") slot } }
|
package/registry/legal-doc.muten
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Legal document - the pages a business must have (terms, privacy, data deletion), the way Storio's site writes them:
|
|
2
2
|
# the title and its line, then two columns, the contents (they follow the reading: the section on screen is marked)
|
|
3
|
-
# and the text at reading width, each section titled
|
|
4
|
-
# DocHead(title: "
|
|
3
|
+
# and the text at reading width, each section titled “N. Title”. On a phone the contents become a row that slides.
|
|
4
|
+
# DocHead(title: "Terms and conditions") { Text "Last updated: October 7, 2026" class("cx-doc-line") }
|
|
5
5
|
# DocBody {
|
|
6
|
-
# DocToc(label: "
|
|
7
|
-
# DocArticle { DocSection(anchor: "s1", title: "1.
|
|
6
|
+
# DocToc(label: "Content") { DocTocLink(label: "1. Who we are", to: "#s1") DocTocLink(label: "2. Tus datos", to: "#s2") }
|
|
7
|
+
# DocArticle { DocSection(anchor: "s1", title: "1. Who we are") { DocText(text: "Storio is a product of Candhrly LLC.") } }
|
|
8
8
|
# }
|
|
9
9
|
# shadcn-for-muten · parts · no deps · kit.css (.cx-doc-*).
|
|
10
10
|
part DocHead(title: text) {
|
|
@@ -14,7 +14,7 @@ part DocHead(title: text) {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
part DocBody() { Section class("cx-mk-col cx-doc-body") { slot } }
|
|
17
|
-
part DocToc(label: text = "
|
|
17
|
+
part DocToc(label: text = "Contents") { Stack class("cx-navbar cx-doc-toc") data(orientation: "vertical", spy: "on", trl: $label) aria(role: "navigation", label: $label) { slot } }
|
|
18
18
|
part DocTocLink(label: text, to: text) {
|
|
19
19
|
Link "{$label}" -> "{$to}" class("cn-button cn-button-variant-ghost cn-button-size-sm group/button cx-nav cx-doc-toc-item") data(slot: "button", marker: "fill")
|
|
20
20
|
}
|
package/registry/link-btn.muten
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# LinkBtn - a link to a route that looks like a Button: same variants and sizes as Btn, but it navigates (a real
|
|
2
2
|
# <a>, so it opens in a new tab with a middle click and reads as a link). Label and icon go in the slot.
|
|
3
3
|
# LinkBtn(to: "/signup") { Span "Crear cuenta" }
|
|
4
|
-
# LinkBtn(to: "/docs", variant: "outline", size: "sm") { Span "
|
|
4
|
+
# LinkBtn(to: "/docs", variant: "outline", size: "sm") { Span "See the guide" Icon "lucide:arrow-right" }
|
|
5
5
|
# variant: default | outline | secondary | ghost | destructive | link · size: xs | sm | default | lg | xl (44) | 2xl (52)
|
|
6
6
|
# shape: "" | pill - fully rounded ends, like Btn(shape: "pill").
|
|
7
7
|
# shadcn-for-muten · part · no deps · Nova classes (nova.css).
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// ListMotion (muten Custom) - the rows of the list it sits in open and close their place (kit.css “List rows”).
|
|
2
|
+
// · a row added after the first paint opens its place and fades in (cx-row-enter)
|
|
3
|
+
// · a row muten removes stays a moment, closing its gap (cx-row-leave), then goes; it is already disposed, so it
|
|
4
|
+
// is only a picture of the row and takes no clicks
|
|
5
|
+
// · a row muten only MOVES (it leaves and comes back in the same update) is not an entrance
|
|
6
|
+
// Nothing moves on the first paint, and with reduced motion the kit's tokens make it instant.
|
|
7
|
+
export function mount(el) {
|
|
8
|
+
const box = el.parentElement;
|
|
9
|
+
if (!box) return;
|
|
10
|
+
let ready = false;
|
|
11
|
+
requestAnimationFrame(() => requestAnimationFrame(() => { ready = true; }));
|
|
12
|
+
const isRow = (n) => n.nodeType === 1 && n !== el && !n.classList.contains('cx-row-leave');
|
|
13
|
+
const settle = (n, cls) => {
|
|
14
|
+
const done = () => { clearTimeout(t); n.removeEventListener('animationend', end); if (cls === 'cx-row-leave') n.remove(); else n.classList.remove(cls); };
|
|
15
|
+
const end = (e) => { if (e.target === n) done(); };
|
|
16
|
+
n.addEventListener('animationend', end);
|
|
17
|
+
const t = setTimeout(done, 900); // a safety net if the animation never reports back
|
|
18
|
+
};
|
|
19
|
+
const mo = new MutationObserver((records) => {
|
|
20
|
+
if (!ready) return;
|
|
21
|
+
const removed = new Set(), added = new Set();
|
|
22
|
+
for (const r of records) r.removedNodes.forEach((n) => { if (isRow(n)) removed.add(n); });
|
|
23
|
+
for (const r of records) r.addedNodes.forEach((n) => { if (isRow(n) && !removed.has(n)) added.add(n); });
|
|
24
|
+
const gone = [...removed].filter((n) => n.parentNode !== box);
|
|
25
|
+
// A WHOLE PAGE SWAPPED (a page turned, a filter changed most rows): closing ten rows while ten open would make
|
|
26
|
+
// the list jump in height, so the old ones go at once and the new ones only fade in where they land
|
|
27
|
+
const swap = gone.length > 1 && added.size > 1;
|
|
28
|
+
if (swap) { added.forEach((n) => { n.classList.add('cx-row-swap'); settle(n, 'cx-row-swap'); }); return; }
|
|
29
|
+
for (const r of records) {
|
|
30
|
+
r.addedNodes.forEach((n) => {
|
|
31
|
+
if (!added.has(n)) return; // a move, not an entrance
|
|
32
|
+
n.classList.add('cx-row-enter'); settle(n, 'cx-row-enter');
|
|
33
|
+
});
|
|
34
|
+
r.removedNodes.forEach((n) => {
|
|
35
|
+
if (!isRow(n) || n.parentNode === box) return; // moved back in: not leaving
|
|
36
|
+
const ref = r.nextSibling && r.nextSibling.parentNode === box ? r.nextSibling : (r.previousSibling && r.previousSibling.parentNode === box ? r.previousSibling.nextSibling : null);
|
|
37
|
+
n.classList.remove('cx-row-enter'); n.classList.add('cx-row-leave'); n.inert = true;
|
|
38
|
+
box.insertBefore(n, ref);
|
|
39
|
+
n.style.setProperty('--mu-h', n.offsetHeight + 'px');
|
|
40
|
+
settle(n, 'cx-row-leave');
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
mo.observe(box, { childList: true });
|
|
45
|
+
// nothing returned: in muten a Custom's returned function is its UPDATER (called with new inputs), not a cleanup
|
|
46
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# ListMotion - rows that open and close their place. Put inside a list container, after its rows; ItemGroup already
|
|
2
|
+
# carries it, so any `each` inside an ItemGroup moves without the page doing anything.
|
|
3
|
+
# shadcn-for-muten · Custom · host JS is list-motion.js.
|
|
4
|
+
part ListMotion() { Custom ListMotion class("cx-list-motion") }
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
# LiveStack - news that keeps arriving, for a landing page: what the product does while nobody watches (
|
|
2
|
-
#
|
|
1
|
+
# LiveStack - news that keeps arriving, for a landing page: what the product does while nobody watches (“Carla booked
|
|
2
|
+
# a haircut”, “New order”, “Your assistant answered”). One at a time a card drops in at the front and the older ones
|
|
3
3
|
# step back behind it, smaller, and leave after three. Pointer over it pauses; reduced motion shows the first three still.
|
|
4
4
|
# LiveStack(items: @news, every: 2800)
|
|
5
5
|
# items {icon (a kit sprite id), title, text, time, tone: "" | ok | wait | info}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# LogoCloud - the
|
|
1
|
+
# LogoCloud - the “they use it” strip: a wrapping row of logos, muted until the pointer rests on one. A
|
|
2
2
|
# LogoCloudItem holds any logo: an Image, an Icon or a word in a Span; all take the same height.
|
|
3
|
-
# LogoCloud(label: "
|
|
3
|
+
# LogoCloud(label: "Used by") { LogoCloudItem { Image "~/logos/roble.svg" alt("El Roble") } LogoCloudItem { Span "Pelos & Co" } }
|
|
4
4
|
# shadcn-for-muten · parts · no deps · kit.css (.cx-logos).
|
|
5
5
|
part LogoCloud(label: text = "") { Stack class("cx-logos") aria(label: $label) { slot } }
|
|
6
6
|
part LogoCloudItem() { Stack class("cx-logo") { slot } }
|
package/registry/marketing.muten
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
# hero's pill and two-line headline, the address field with its button, a list of checks, numbered steps, cards,
|
|
3
3
|
# columns of facts, a path of steps, a checked list with explanations and the closing call. Every argument is text,
|
|
4
4
|
# so the words can come from a translation function: MkCheck(label: "{t(lang, "x")}").
|
|
5
|
-
# MkSection(anchor: "how", title: "
|
|
6
|
-
# MkChecks { MkCheck(label: "
|
|
5
|
+
# MkSection(anchor: "how", title: "How it works") {
|
|
6
|
+
# MkChecks { MkCheck(label: "No app for your customer") MkCheck(label: "Confirma solo") }
|
|
7
7
|
# }
|
|
8
|
-
# MkHero { MkHeroCopy { MkPill(label: "
|
|
9
|
-
# MkClaim(value: handle, to: joinUrl, cta: "
|
|
10
|
-
# MkCta(title: "
|
|
8
|
+
# MkHero { MkHeroCopy { MkPill(label: "For musicians") { Icon "lucide:music" } MkHeadline(first: "Your music,", second: "in one link.")
|
|
9
|
+
# MkClaim(value: handle, to: joinUrl, cta: "Create my shop") class("animate-rise") } … }
|
|
10
|
+
# MkCta(title: "Your business,", soft: "sells and books by itself.") { MkClaim(value: handle, to: joinUrl, cta: "Create my shop") }
|
|
11
11
|
# shadcn-for-muten · parts · no deps · kit.css (.cx-mk-*), uses InputGroup and LinkBtn.
|
|
12
12
|
|
|
13
13
|
# MkSection - a page section at reading width with its h2. anchor is its id (a link can go to #anchor).
|
|
@@ -50,7 +50,7 @@ part MkHeadline(first: text, second: text, fit: bool = true) {
|
|
|
50
50
|
part MkTitle(text: text, size: text = "") {
|
|
51
51
|
Title "{$text}" h1 class("cx-mk-h1 words", "cx-mk-h1-fit" when $size == "fit", "cx-mk-h1-wrap" when $size == "wrap")
|
|
52
52
|
}
|
|
53
|
-
# MkClaim - the address field and its button:
|
|
53
|
+
# MkClaim - the address field and its button: “storio.ink/ [your-name] [Create]”. value is the page's text state.
|
|
54
54
|
part MkClaim(value: text, to: text, cta: text, placeholder: text = "", prefix: text = "storio.ink/") {
|
|
55
55
|
Stack class("cx-mk-claim") {
|
|
56
56
|
InputGroup(size: "2xl", variant: "filled") {
|
|
@@ -147,7 +147,7 @@ part VsRow(what: text, us: text, them: text, usName: text, themName: text) {
|
|
|
147
147
|
# MkRows / MkRow - zigzag rows: the words on one side and a picture on the other (MkStage); reverse puts the words on
|
|
148
148
|
# the right. MkRowCopy is the words: its title (an h2, or h3 under a section that already has one), a line and, in the
|
|
149
149
|
# slot, anything after it (MkChecks).
|
|
150
|
-
# MkRows { MkRow { MkRowCopy(title: "
|
|
150
|
+
# MkRows { MkRow { MkRowCopy(title: "Your schedule", text: "Se llena sola.") { MkChecks { … } } MkStage { … } } }
|
|
151
151
|
part MkRows() { Stack class("cx-mk-rows") { slot } }
|
|
152
152
|
part MkRow(reverse: bool = false) { Stack class("cx-mk-row", "cx-mk-rev" when $reverse) { slot } }
|
|
153
153
|
part MkRowCopy(title: text, text: text, level: text = "h2") {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# MemberList - any group (team, workspace, family plan): who is in, their role in the library's menu, invite.
|
|
2
|
-
# MemberList(members: @members, roles: @roles, owner: "
|
|
2
|
+
# MemberList(members: @members, roles: @roles, owner: "Owner", one: "member", many: "members", cta: "Invite", note: "They get an email to join.")
|
|
3
3
|
# shadcn-for-muten · part over KitWidget (kit core).
|
|
4
|
-
part MemberList(members: list, roles: list, owner: text = "", one: text = "
|
|
4
|
+
part MemberList(members: list, roles: list, owner: text = "", one: text = "person", many: text = "people", cta: text = "Invite", note: text = "") {
|
|
5
5
|
Custom KitWidget inputs(name: "memberList", members: $members, roles: $roles, owner: $owner, one: $one, many: $many, cta: $cta, note: $note) class("cx-team")
|
|
6
6
|
}
|
package/registry/menubar.muten
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Menubar - a desktop app's menu bar: each word opens its menu; once one is open, moving the pointer to the next word
|
|
2
2
|
# opens that one, and the left and right arrows walk the bar. Each menu holds DropdownContent and its items.
|
|
3
3
|
# Menubar(label: "Editor") {
|
|
4
|
-
# MenubarMenu { MenubarTrigger(label: "
|
|
5
|
-
# MenubarMenu { MenubarTrigger(label: "
|
|
4
|
+
# MenubarMenu { MenubarTrigger(label: "File") DropdownContent { DropdownItem(label: "New", shortcut: "⌘N", onSelect: create) … } }
|
|
5
|
+
# MenubarMenu { MenubarTrigger(label: "Edit") DropdownContent { … } }
|
|
6
6
|
# }
|
|
7
7
|
# shadcn-for-muten · parts + Custom (OverlayBehavior) · Nova (.cn-menubar*) + kit.css.
|
|
8
|
-
part Menubar(label: text = "
|
|
8
|
+
part Menubar(label: text = "Menu") { Stack class("cn-menubar cx-menubar") data(slot: "menubar", trl: $label) aria(role: "menubar", label: $label) { slot } }
|
|
9
9
|
part MenubarMenu() { Stack class("cx-ov") data(slot: "menubar-menu") { slot OverlayBehavior(kind: "menubar") } }
|
|
10
10
|
part MenubarTrigger(label: text) { Stack class("cx-ov-trigger") { Button "{$label}" class("cn-menubar-trigger") data(slot: "menubar-trigger") aria(role: "menuitem") } }
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# MessageScroller - the column of a conversation: it scrolls inside itself and opens at the latest message; a new
|
|
2
2
|
# message keeps it at the bottom only if you were already there (reading back is never interrupted).
|
|
3
|
-
# MessageScroller(label: "Chat
|
|
3
|
+
# MessageScroller(label: "Chat with Valentina", height: "22rem") { Marker(label: "Today") Message(…) { … } }
|
|
4
4
|
# shadcn-for-muten · part + Custom (stick to the bottom) · kit.css (.cx-chat).
|
|
5
|
-
part MessageScroller(label: text = "
|
|
6
|
-
Stack class("cx-chat cx-scroll cx-msg-scroller") data(slot: "message-scroller") style(h: "{$height}") aria(role: "log", label: $label) { slot MessageScrollerBehavior() }
|
|
5
|
+
part MessageScroller(label: text = "Conversation", height: text = "22rem") {
|
|
6
|
+
Stack class("cx-chat cx-scroll cx-msg-scroller") data(slot: "message-scroller") style(h: "{$height}") data(trl: $label) aria(role: "log", label: $label) { slot MessageScrollerBehavior() }
|
|
7
7
|
}
|
|
8
8
|
part MessageScrollerBehavior() { Custom MessageScrollerBehavior class("cx-msg-behavior") }
|
package/registry/message.muten
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Message - a chat bubble, the same the kit's inbox draws: theirs on the left on the surface, yours on the right in
|
|
2
2
|
# the primary colour, with the time and, on yours, whether it was sent, delivered or read. Text, a photo or an
|
|
3
3
|
# Attachment go inside. Marker is the day line between messages; MessageScroller holds them and scrolls.
|
|
4
|
-
# MessageScroller(label: "Chat
|
|
5
|
-
# Marker(label: "
|
|
6
|
-
# Message(variant: "received", time: "10:02") { Span "
|
|
7
|
-
# Message(variant: "sent", time: "10:03", status: "read") { Span "
|
|
4
|
+
# MessageScroller(label: "Chat with Valentina") {
|
|
5
|
+
# Marker(label: "Today")
|
|
6
|
+
# Message(variant: "received", time: "10:02") { Span "Hi, do you have an appointment today?" }
|
|
7
|
+
# Message(variant: "sent", time: "10:03", status: "read") { Span "Yes: at 15:00 with Camila." }
|
|
8
8
|
# }
|
|
9
9
|
# variant: received | sent · status (sent only): sending | sent | delivered | read · tag: a small label (Bot).
|
|
10
10
|
# shadcn-for-muten · parts · no deps · kit.css (.cx-chat-*).
|
|
@@ -15,14 +15,14 @@ part Message(variant: text = "received", time: text = "", status: text = "", tag
|
|
|
15
15
|
Stack class("cx-chat-meta") {
|
|
16
16
|
Span "{$tag}" class("cx-chat-tag")
|
|
17
17
|
Span "{$time}" class("cx-text") data(variant: "caption")
|
|
18
|
-
when $status == "sending" { Icon "lucide:clock" class("cx-tick") data(status: "sending") aria(label: "
|
|
19
|
-
when $status == "sent" { Icon "lucide:check" class("cx-tick") data(status: "sent") aria(label: "
|
|
20
|
-
when $status == "delivered" { Icon "lucide:check-check" class("cx-tick") data(status: "delivered") aria(label: "
|
|
21
|
-
when $status == "read" { Icon "lucide:check-check" class("cx-tick") data(status: "read") aria(label: "
|
|
18
|
+
when $status == "sending" { Icon "lucide:clock" class("cx-tick") data(status: "sending", trl: "Sending") aria(label: "Sending") }
|
|
19
|
+
when $status == "sent" { Icon "lucide:check" class("cx-tick") data(status: "sent", trl: "Sent") aria(label: "Sent") }
|
|
20
|
+
when $status == "delivered" { Icon "lucide:check-check" class("cx-tick") data(status: "delivered", trl: "Delivered") aria(label: "Delivered") }
|
|
21
|
+
when $status == "read" { Icon "lucide:check-check" class("cx-tick") data(status: "read", trl: "Read") aria(label: "Read") }
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
part Bubble(variant: text = "received") { Stack class("cx-chat-bubble cx-msg-bubble") data(variant: $variant) { slot } }
|
|
27
27
|
part Marker(label: text) { Stack class("cx-chat-day") aria(role: "separator") { Span "{$label}" class("cx-text") data(variant: "caption") } }
|
|
28
|
-
part MessageImage(src: text, alt: text = "
|
|
28
|
+
part MessageImage(src: text, alt: text = "Photo") { Stack class("cx-chat-img") style(bg: "{$src}") data(trl: $alt) aria(role: "img", label: $alt) }
|
package/registry/messaging.muten
CHANGED
|
@@ -6,6 +6,6 @@
|
|
|
6
6
|
# {id,icon,label,noun,title,subtitle}, templates {title,body}, replies {match (regex), text}.
|
|
7
7
|
# Messaging(channels: @channels, conversations: @inbox, filters: @filters, states: @states, controls: @controls, quick: @quick, templates: @templates, replies: @replies, onAction: act)
|
|
8
8
|
# shadcn-for-muten · part over KitWidget (kit core).
|
|
9
|
-
part Messaging(title: text = "
|
|
9
|
+
part Messaging(title: text = "Messages", channels: list, conversations: list, filters: list, states: list, controls: list, quick: list, templates: list, replies: list, onAction: action) {
|
|
10
10
|
Custom KitWidget inputs(name: "messaging", title: $title, channels: $channels, conversations: $conversations, filters: $filters, states: $states, controls: $controls, quick: $quick, templates: $templates, replies: $replies) on(action: $onAction) class("cx-mx-host")
|
|
11
11
|
}
|
package/registry/modal.muten
CHANGED
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
# drag down on the drawer all call onClose. Content goes in the slot; the buttons in a ModalFooter (it sticks under
|
|
4
4
|
# the scrolling body): the way out (cx-exit) on the left, the action (cx-go) filling the rest.
|
|
5
5
|
# state { editing = false : bool } action edit() mutates editing { editing.set(true) } action done() mutates editing { editing.set(false) }
|
|
6
|
-
# Modal(open: editing, onClose: done, title: "
|
|
6
|
+
# Modal(open: editing, onClose: done, title: "Edit service", description: "What you change shows in your shop when you save.") {
|
|
7
7
|
# Field { … }
|
|
8
|
-
# ModalFooter { Btn(variant: "outline", onClick: done) class("cx-exit") { Span "
|
|
8
|
+
# ModalFooter { Btn(variant: "outline", onClick: done) class("cx-exit") { Span "Cancel" } Btn(onClick: save) class("cx-go") { Span "Save changes" } }
|
|
9
9
|
# }
|
|
10
10
|
# mode: auto | dialog | drawer | sheet (a panel from a side) · side: right | left | top | bottom (for sheet)
|
|
11
11
|
# role: dialog | alertdialog · dismiss: on | off (off: no ✕ and the backdrop does not close it; only its buttons do)
|
|
12
12
|
# size: "" | tall (a list that pages) · shadcn-for-muten · part + Custom (ModalBehavior) · kit.css (.cx-modal).
|
|
13
13
|
part Modal(open: bool, onClose: action, title: text = "", description: text = "", mode: text = "auto", size: text = "", side: text = "right", role: text = "dialog", dismiss: text = "on") {
|
|
14
14
|
Stack class("cx-modal-root") data(open: $open) {
|
|
15
|
-
when $dismiss != "off" { Button "" -> $onClose class("cx-overlay") aria(label: "
|
|
15
|
+
when $dismiss != "off" { Button "" -> $onClose class("cx-overlay") aria(label: "Close", hidden: "true") data(closes: "true", trl: "Close") }
|
|
16
16
|
when $dismiss == "off" { Stack class("cx-overlay") aria(hidden: "true") }
|
|
17
17
|
Stack class("cx-modal") data(mode: $mode, size: $size, side: $side) aria(role: $role, modal: "true", label: $title) {
|
|
18
18
|
Stack class("cx-handle") aria(hidden: "true")
|
|
@@ -20,7 +20,7 @@ part Modal(open: bool, onClose: action, title: text = "", description: text = ""
|
|
|
20
20
|
Span "{$title}" class("cx-modal-title")
|
|
21
21
|
Span "{$description}" class("cx-modal-desc")
|
|
22
22
|
}
|
|
23
|
-
when $dismiss != "off" { Btn(variant: "ghost", size: "icon-sm", label: "
|
|
23
|
+
when $dismiss != "off" { Btn(variant: "ghost", size: "icon-sm", label: "Close", onClick: $onClose) class("cx-modal-x") { Icon "lucide:x" } }
|
|
24
24
|
Stack class("cx-modal-body") { slot }
|
|
25
25
|
ModalBehavior(open: $open, onClose: $onClose)
|
|
26
26
|
}
|
package/registry/money-field.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// MoneyField (muten Custom) - a price with its currency (kit/values.js moneyField).
|
|
2
2
|
export function mount(el, inputs, on) {
|
|
3
|
-
Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/values.js')]).then(([core, v]) => v.moneyField(el, inputs, core, on));
|
|
3
|
+
Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/values.js')]).then(([core, v]) => core.relang(el, inputs, () => v.moneyField(el, inputs, core, on)));
|
|
4
4
|
return () => {};
|
|
5
5
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# MoneyField - a price with its currency: the symbol, separators and decimals come from the currency (Intl); the
|
|
2
2
|
# currency is picked from a searchable list; under it, what is stored (minor units). onChange gets {amount, minor, currency}.
|
|
3
|
-
# MoneyField(label: "
|
|
3
|
+
# MoneyField(label: "Service price", value: 25000, currency: "COP", onChange: setPrice)
|
|
4
4
|
# shadcn-for-muten · Custom (kit values) · host JS money-field.js.
|
|
5
|
-
part MoneyField(label: text = "
|
|
5
|
+
part MoneyField(label: text = "Price", value: number = 0, currency: text = "COP", onChange: action) {
|
|
6
6
|
Custom MoneyField inputs(label: $label, value: $value, currency: $currency) on(change: $onChange) class("cx-mf-host")
|
|
7
7
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# NavButton - a place in the app (a tab bar, a sidebar, a segmented header). Where you are is a STATE, not a
|
|
2
2
|
# variant: it rides aria-pressed and is marked by `marker`; everything else is a Btn (variant, size, any content in
|
|
3
3
|
# the slot: an icon, a count). Lay them in a NavBar, across or down.
|
|
4
|
-
# NavBar(label: "
|
|
5
|
-
# NavBar(label: "
|
|
4
|
+
# NavBar(label: "Sections") { NavButton(label: "My shop", current: atShop, onClick: goShop) … }
|
|
5
|
+
# NavBar(label: "Menu", orientation: "vertical") { NavButton(label: "Schedule", current: atAgenda, onClick: goAgenda, marker: "fill") { Icon "lucide:calendar" } }
|
|
6
6
|
# marker: dot (soft fill + a dot, the default) | fill (soft fill) | line (an underline) | none (only the text firms up)
|
|
7
7
|
# variant / size: as Btn. shadcn-for-muten · part · no deps · kit.css (.cx-nav, .cx-navbar).
|
|
8
8
|
part NavButton(label: text = "", current: bool, onClick: action, variant: text = "ghost", size: text = "default", marker: text = "dot") {
|
|
@@ -11,11 +11,11 @@ part NavButton(label: text = "", current: bool, onClick: action, variant: text =
|
|
|
11
11
|
Span "{$label}" class("cx-nav-label")
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
-
part NavBar(label: text = "
|
|
14
|
+
part NavBar(label: text = "Sections", orientation: text = "horizontal") { Nav class("cx-navbar") data(orientation: $orientation, trl: $label) aria(label: $label) { slot } }
|
|
15
15
|
|
|
16
16
|
# NavLink - a NavButton that goes to another page (a Link): the tabs between sibling pages (Terms · Privacy · Delete).
|
|
17
17
|
# NavHere - the tab of the page you are on, which goes nowhere (aria-current="page", marked like a current NavButton).
|
|
18
|
-
# NavBar(label: "
|
|
18
|
+
# NavBar(label: "Documents") { NavHere(label: "Terms", marker: "fill") NavLink(label: "Privacy", to: "/privacy", marker: "fill") }
|
|
19
19
|
part NavLink(label: text, to: text, variant: text = "ghost", size: text = "default", marker: text = "dot") {
|
|
20
20
|
Link "" -> "{$to}" class("cn-button cn-button-variant-{$variant} cn-button-size-{$size} group/button cx-nav") data(slot: "button", marker: $marker) {
|
|
21
21
|
slot
|