@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
|
@@ -4,20 +4,20 @@
|
|
|
4
4
|
# theme, so it sits on any skin, light or dark.
|
|
5
5
|
# SiteFooter {
|
|
6
6
|
# FooterHead {
|
|
7
|
-
# FooterBrand(tag: "
|
|
8
|
-
# FooterActions { LinkBtn(to: "/login", variant: "outline") { Span "
|
|
7
|
+
# FooterBrand(tag: "Sells and books for you.") { Link "" -> "/" aria(label: "Home") { Image "/logo.svg" alt("Storio") } }
|
|
8
|
+
# FooterActions { LinkBtn(to: "/login", variant: "outline") { Span "Sign in" } LinkBtn(to: "/signup") { Span "Create my shop" } }
|
|
9
9
|
# }
|
|
10
10
|
# FooterLinks {
|
|
11
|
-
# FooterCol(title: "
|
|
12
|
-
# FooterCol(title: "Legal") { Link "
|
|
11
|
+
# FooterCol(title: "Product") { Link "What it is" -> "/product" Link "Prices" -> "/pricing" }
|
|
12
|
+
# FooterCol(title: "Legal") { Link "Terms" -> "/terms" Link "Privacy" -> "/privacy" }
|
|
13
13
|
# }
|
|
14
14
|
# FooterBar { Span "© 2026 Candhrly LLC" Link "hello@storio.ink" -> "mailto:hello@storio.ink" }
|
|
15
15
|
# }
|
|
16
16
|
# FooterCol(wide: "on") takes two columns of links on a phone and one long column on a desk (a long list, like trades).
|
|
17
17
|
# The bar puts its first child on the left and the rest on the right.
|
|
18
18
|
# shadcn-for-muten · part · no deps · kit.css (.cx-foot).
|
|
19
|
-
part SiteFooter(label: text = "
|
|
20
|
-
Footer class("cx-foot") aria(label: $label) { Stack class("cx-foot-in") { slot } }
|
|
19
|
+
part SiteFooter(label: text = "Footer") {
|
|
20
|
+
Footer class("cx-foot") data(trl: $label) aria(label: $label) { Stack class("cx-foot-in") { slot } }
|
|
21
21
|
}
|
|
22
22
|
part FooterHead() { Stack class("cx-foot-head") { slot } }
|
|
23
23
|
part FooterBrand(tag: text = "") {
|
|
@@ -1,37 +1,84 @@
|
|
|
1
|
-
// SortableBehavior (muten Custom) - reorder the Items of a Sortable
|
|
2
|
-
//
|
|
3
|
-
// their new order.
|
|
1
|
+
// SortableBehavior (muten Custom) - reorder the Items of a Sortable, with a GHOST of the row already where it will
|
|
2
|
+
// land. Pointer (mouse + touch) on the grip, or focus the grip and use Up/Down. Announces “<title>: position n of m”
|
|
3
|
+
// and emits the keys in their new order. Motion on the kit's tokens (kit.css “Sortable”):
|
|
4
|
+
// · the row taken lifts (2 % bigger, a deeper shadow) and follows the pointer, kept within the list
|
|
5
|
+
// · the others slide apart (--t-move) and the ghost (the row's own copy, dashed, its content faded) glides to the
|
|
6
|
+
// slot the row will take, in step with them; rows of different heights are measured, not assumed
|
|
7
|
+
// · on release the row settles exactly onto the ghost (200 ms), the ghost fades, THEN the order is committed with
|
|
8
|
+
// nothing moving, so there is no jump; Escape puts it back where it was
|
|
9
|
+
// · the keyboard moves it with no drag animation (keyboard actions never animate)
|
|
4
10
|
export function mount(el, inputs, on) {
|
|
5
11
|
const sort = el.parentElement;
|
|
12
|
+
let I = null; const T = (k, ...a) => (I ? I.t(I.langOf(sort), k, ...a) : a.reduce((x, v, n) => x.replace(`{${n}}`, v), k));
|
|
13
|
+
// the grips' names: “Move <title>” in the list's language (muten cannot interpolate inside aria(label: "…")), said
|
|
14
|
+
// again when a lang changes
|
|
15
|
+
const name = () => sort.querySelectorAll('.cx-grip').forEach((g) => { const it = g.closest('.cx-item'); const title = g.dataset.title || it?.querySelector('.cn-item-title')?.textContent || ''; g.setAttribute('aria-label', T('Move {0}', title)); g.title = T('Drag to reorder'); });
|
|
16
|
+
import('@muten/shadcn/registry/kit/i18n.js').then((m) => { I = m; name(); });
|
|
17
|
+
document.addEventListener('kit:lang', () => { if (sort.isConnected) name(); });
|
|
6
18
|
const live = document.createElement('span'); live.className = 'cx-sr'; live.setAttribute('aria-live', 'polite'); sort.after(live);
|
|
7
19
|
const items = () => [...sort.querySelectorAll(':scope > .cx-item')];
|
|
8
|
-
|
|
20
|
+
const settleMs = () => (parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--t-move')) > 1 ? 200 : 1);
|
|
21
|
+
sort.querySelectorAll('.cx-grip').forEach((g) => { g.tabIndex = 0; });
|
|
22
|
+
name();
|
|
9
23
|
const done = (item) => {
|
|
10
|
-
const all = items(); live.textContent =
|
|
24
|
+
const all = items(); live.textContent = T('{0}: position {1} of {2}', item.querySelector('.cn-item-title')?.textContent || '', all.indexOf(item) + 1, all.length);
|
|
11
25
|
on.reorder?.(all.map((x) => x.dataset.key).join(','));
|
|
12
26
|
};
|
|
27
|
+
|
|
13
28
|
sort.addEventListener('pointerdown', (e) => {
|
|
14
|
-
const grip = e.target.closest('.cx-grip'); if (!grip) return;
|
|
29
|
+
const grip = e.target.closest('.cx-grip'); if (!grip || e.button > 0) return;
|
|
15
30
|
e.preventDefault();
|
|
16
|
-
const item = grip.closest('.cx-item'), all = items();
|
|
17
|
-
const
|
|
18
|
-
const
|
|
31
|
+
const item = grip.closest('.cx-item'), all = items(), from = all.indexOf(item);
|
|
32
|
+
const tops = all.map((x) => x.offsetTop), heights = all.map((x) => x.offsetHeight);
|
|
33
|
+
const gap = parseFloat(getComputedStyle(sort).rowGap) || 0, h = heights[from], room = h + gap;
|
|
34
|
+
const others = all.filter((x) => x !== item);
|
|
35
|
+
const mids = others.map((x) => x.offsetTop + x.offsetHeight / 2);
|
|
36
|
+
// where the row's top lands when it takes index k among the others
|
|
37
|
+
const topAt = (k) => tops[0] + others.slice(0, k).reduce((sum, x) => sum + x.offsetHeight + gap, 0);
|
|
38
|
+
const startY = e.clientY, minDy = tops[0] - tops[from] - room / 2, maxDy = topAt(others.length) - tops[from] + room / 2;
|
|
19
39
|
let to = from;
|
|
20
|
-
|
|
40
|
+
|
|
41
|
+
// the ghost: the row's own copy, the same box in the same place, dashed and faded; inert and silent
|
|
42
|
+
const ghost = item.cloneNode(true);
|
|
43
|
+
ghost.classList.add('cx-sort-ghost'); ghost.removeAttribute('data-key'); ghost.setAttribute('aria-hidden', 'true'); ghost.inert = true;
|
|
44
|
+
ghost.style.left = item.offsetLeft + 'px'; ghost.style.width = item.offsetWidth + 'px'; ghost.style.translate = `0 ${tops[from]}px`;
|
|
45
|
+
sort.appendChild(ghost);
|
|
46
|
+
requestAnimationFrame(() => ghost.classList.add('on'));
|
|
47
|
+
|
|
48
|
+
item.classList.add('lifting'); document.documentElement.classList.add('cx-sorting');
|
|
49
|
+
grip.setPointerCapture(e.pointerId);
|
|
50
|
+
const place = () => {
|
|
51
|
+
others.forEach((x, k) => { const i = all.indexOf(x); let shift = 0; if (from < to && i > from && i <= to) shift = -room; if (from > to && i < from && i >= to) shift = room; x.style.translate = shift ? `0 ${shift}px` : ''; });
|
|
52
|
+
ghost.style.translate = `0 ${topAt(to)}px`;
|
|
53
|
+
};
|
|
21
54
|
const move = (ev) => {
|
|
22
|
-
const dy = ev.clientY -
|
|
23
|
-
|
|
24
|
-
|
|
55
|
+
const dy = Math.max(minDy, Math.min(maxDy, ev.clientY - startY));
|
|
56
|
+
item.style.translate = `0 ${dy}px`;
|
|
57
|
+
const center = tops[from] + h / 2 + dy;
|
|
58
|
+
const next = mids.filter((m) => m < center).length;
|
|
59
|
+
if (next !== to) { to = next; place(); }
|
|
25
60
|
};
|
|
26
|
-
const
|
|
27
|
-
grip.removeEventListener('pointermove', move); grip.removeEventListener('pointerup', up); grip.removeEventListener('pointercancel',
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
item.
|
|
31
|
-
|
|
61
|
+
const finish = (commit) => {
|
|
62
|
+
grip.removeEventListener('pointermove', move); grip.removeEventListener('pointerup', up); grip.removeEventListener('pointercancel', cancel); removeEventListener('keydown', esc, true);
|
|
63
|
+
if (!commit) { to = from; place(); }
|
|
64
|
+
item.classList.remove('lifting'); item.classList.add('settling');
|
|
65
|
+
item.style.translate = `0 ${topAt(to) - tops[from]}px`;
|
|
66
|
+
ghost.classList.remove('on');
|
|
67
|
+
setTimeout(() => {
|
|
68
|
+
all.forEach((x) => { x.style.transition = 'none'; x.style.translate = ''; });
|
|
69
|
+
item.classList.remove('settling'); ghost.remove(); document.documentElement.classList.remove('cx-sorting');
|
|
70
|
+
// right before the row it now precedes, or right after the last one (never past the list's own markers)
|
|
71
|
+
if (to !== from) { if (others[to]) sort.insertBefore(item, others[to]); else others[others.length - 1].after(item); }
|
|
72
|
+
void sort.offsetWidth; all.forEach((x) => { x.style.transition = ''; });
|
|
73
|
+
if (to !== from) done(item);
|
|
74
|
+
}, settleMs());
|
|
32
75
|
};
|
|
33
|
-
|
|
76
|
+
const up = () => finish(true);
|
|
77
|
+
const cancel = () => finish(false);
|
|
78
|
+
const esc = (ev) => { if (ev.key === 'Escape') { ev.preventDefault(); ev.stopPropagation(); finish(false); } };
|
|
79
|
+
grip.addEventListener('pointermove', move); grip.addEventListener('pointerup', up); grip.addEventListener('pointercancel', cancel); addEventListener('keydown', esc, true);
|
|
34
80
|
});
|
|
81
|
+
|
|
35
82
|
sort.addEventListener('keydown', (e) => {
|
|
36
83
|
const grip = e.target.closest('.cx-grip'); if (!grip || !['ArrowUp', 'ArrowDown'].includes(e.key)) return;
|
|
37
84
|
e.preventDefault(); const item = grip.closest('.cx-item');
|
|
@@ -5,7 +5,9 @@ export function mount(el, inputs, on) {
|
|
|
5
5
|
const esc = (s) => String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]);
|
|
6
6
|
const GRIP = '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="9" cy="6" r="1.5"/><circle cx="9" cy="12" r="1.5"/><circle cx="9" cy="18" r="1.5"/><circle cx="15" cy="6" r="1.5"/><circle cx="15" cy="12" r="1.5"/><circle cx="15" cy="18" r="1.5"/></svg>';
|
|
7
7
|
const names = String(inputs.items || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
8
|
-
el.innerHTML = `<div class="cx-sortable" aria-label="${esc(inputs.label || '
|
|
8
|
+
el.innerHTML = `<div class="cx-sortable" aria-label="${esc(inputs.label || 'Order')}" data-trl="${esc(inputs.label || 'Order')}">${names.map((n) => `<div class="cn-item cn-item-variant-outline cn-item-size-sm group/item cx-item" data-slot="item" data-key="${esc(n)}"><span class="cn-button cn-button-variant-ghost cn-button-size-icon-xs cx-grip" role="button" aria-label="Move ${esc(n)}" data-trl="Move {0}" data-tra="${esc(n)}" data-grip="true">${GRIP}</span><div class="cn-item-content"><span class="cn-item-title">${esc(n)}</span></div></div>`).join('')}<span class="cx-sl-anchor"></span></div>`;
|
|
9
|
+
if (inputs.lang) el.setAttribute('lang', inputs.lang);
|
|
10
|
+
import('@muten/shadcn/registry/kit/i18n.js').then((m) => m.localizeTree(el));
|
|
9
11
|
import('@muten/shadcn/registry/sortable-behavior.js').then((b) => b.mount(el.querySelector('.cx-sl-anchor'), {}, { reorder: (v) => on.reorder?.(v) }));
|
|
10
12
|
return () => {};
|
|
11
13
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# SortableList - plain names you put in order (the layers of a design, the steps of a flow) from comma-joined text:
|
|
2
2
|
# the same rows and feel as Sortable (the grip, the others making room, Up/Down on the focused grip).
|
|
3
3
|
# state { layers = "Fondo,Jugador,Interfaz" : text } action reorder(v: text) mutates layers { layers.set(v) }
|
|
4
|
-
# SortableList(items: layers, label: "
|
|
4
|
+
# SortableList(items: layers, label: "Layers", onReorder: reorder)
|
|
5
5
|
# For rows with a price or details use Sortable with SortItem. shadcn-for-muten · Custom · sortable-list.js.
|
|
6
|
-
part SortableList(items: text, label: text = "
|
|
6
|
+
part SortableList(items: text, label: text = "Order", onReorder: action) { Custom SortableList inputs(items: $items, label: $label) on(reorder: $onReorder) class("cx-slist-host") }
|
package/registry/sortable.muten
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# Sortable - Items you reorder: take the grip with a mouse or a finger, or focus it and use the arrows. The others
|
|
2
|
-
#
|
|
2
|
+
# slide apart as you drag and a ghost of the row already sits where it will land (it settles onto it on release;
|
|
3
|
+
# Escape puts it back); a screen reader hears the new position. onReorder gets the keys in their new order
|
|
3
4
|
# ("cut,beard,trim") so the page can save it.
|
|
4
|
-
# Sortable(label: "
|
|
5
|
-
# SortItem(key: "cut", title: "
|
|
5
|
+
# Sortable(label: "Order of the services in your shop", onReorder: saveOrder) {
|
|
6
|
+
# SortItem(key: "cut", title: "Classic cut", price: "$25.000")
|
|
6
7
|
# }
|
|
7
8
|
# shadcn-for-muten · part + Custom (SortableBehavior) · kit.css (.cx-sortable).
|
|
8
9
|
part Sortable(label: text, onReorder: action) { Stack class("cx-sortable") aria(label: $label) { slot SortableBehavior(onReorder: $onReorder) } }
|
|
9
10
|
part SortItem(key: text, title: text, price: text = "") {
|
|
10
11
|
Stack class("cn-item cn-item-variant-outline cn-item-size-sm group/item cx-item") data(slot: "item", key: $key) {
|
|
11
|
-
Stack class("cn-button cn-button-variant-ghost cn-button-size-icon-xs cx-grip") aria(role: "button", label:
|
|
12
|
+
Stack class("cn-button cn-button-variant-ghost cn-button-size-icon-xs cx-grip") aria(role: "button", label: $title) data(grip: "true", title: $title) { Icon "lucide:grip-vertical" }
|
|
12
13
|
Stack class("cn-item-content") data(slot: "item-content") { Span "{$title}" class("cn-item-title") data(slot: "item-title") }
|
|
13
14
|
Span "{$price}" class("cx-price")
|
|
14
15
|
}
|
package/registry/spinner.muten
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
# Spinner - a loading mark for a button or a line, never for a whole screen (that is a Skeleton).
|
|
2
|
-
# Btn(onClick: save) disabled… { Spinner() Span "
|
|
2
|
+
# Btn(onClick: save) disabled… { Spinner() Span "Saving…" }
|
|
3
3
|
# shadcn-for-muten · part · no deps · Nova (.cn-spinner).
|
|
4
|
-
part Spinner() { Icon "lucide:loader-circle" class("cn-spinner animate-spin") aria(label: "
|
|
4
|
+
part Spinner() { Icon "lucide:loader-circle" class("cn-spinner animate-spin") data(trl: "Loading") aria(label: "Loading", role: "status") }
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# SplitButton - one main action and, joined to it, a chevron with the other ways to do it (save / save as draft).
|
|
2
2
|
# The main action runs on its own; the menu takes DropdownItem parts.
|
|
3
|
-
# SplitButton(label: "
|
|
3
|
+
# SplitButton(label: "Publish", onClick: publish) { DropdownItem(label: "Save as draft", onSelect: draft) DropdownItem(label: "Schedule…", onSelect: schedule) }
|
|
4
4
|
# variant: as Btn. shadcn-for-muten · part over ButtonGroup + DropdownMenu.
|
|
5
|
-
part SplitButton(label: text, onClick: action, variant: text = "default", size: text = "default", menu: text = "
|
|
5
|
+
part SplitButton(label: text, onClick: action, variant: text = "default", size: text = "default", menu: text = "More options") {
|
|
6
6
|
ButtonGroup(label: $label) {
|
|
7
7
|
Btn(variant: $variant, size: $size, onClick: $onClick) { Span "{$label}" }
|
|
8
8
|
ButtonGroupSeparator()
|
package/registry/stacked-bars.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// StackedBars (muten Custom) - bars over time with one or more series stacked in each (
|
|
1
|
+
// StackedBars (muten Custom) - bars over time with one or more series stacked in each (appointments + pedidos by month), an
|
|
2
2
|
// axis in the page's unit (USD 8K), a bar you can pick (it says so with aria-pressed and sends its id), a note over the
|
|
3
3
|
// plot while there is nothing to show, and a skeleton while it loads. The data changes in place: the bars slide to their
|
|
4
4
|
// new height, nothing blinks. inputs: rows [{id, label, <key>: number, …}], series [{key, label, tone}], selected,
|
|
@@ -29,7 +29,7 @@ export function mount(el, inputs, on) {
|
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
const paint = () => {
|
|
32
|
-
const rows = list(state.rows), series = list(state.series), unit = state.unit || '', locale = state.locale || '
|
|
32
|
+
const rows = list(state.rows), series = list(state.series), unit = state.unit || '', locale = state.locale || (el.closest('[lang]')?.getAttribute('lang')) || 'en';
|
|
33
33
|
const totals = rows.map((r) => series.reduce((sum, s) => sum + (Number(r[s.key]) || 0), 0));
|
|
34
34
|
const top = niceTop(Math.max(0, ...totals)), busy = !!state.busy, none = !busy && totals.every((t) => t === 0);
|
|
35
35
|
const legend = series.length > 1 ? `<ul class="${NS}-legend">${series.map((s) => `<li data-tone="${esc(s.tone || 'brand')}">${esc(s.label)}</li>`).join('')}</ul>` : '';
|
|
@@ -46,7 +46,7 @@ export function mount(el, inputs, on) {
|
|
|
46
46
|
const n = busy ? Math.max(rows.length, 7) : rows.length;
|
|
47
47
|
el.innerHTML = `${legend}<div class="${NS}-body"><div class="${NS}-y" aria-hidden="true">${ticks}</div><div class="${NS}-plot"><div class="${NS}-grid" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i></div><div class="${NS}-bars" role="group" aria-label="${esc(state.label || '')}" style="--n:${n}"${busy ? ' aria-busy="true"' : ''}>${bars}</div>${none && state.empty ? `<div class="cx-veil-note"><div class="cx-veil-box">${state.emptyTitle ? `<span class="cx-veil-t">${esc(state.emptyTitle)}</span>` : ''}<span class="cx-veil-s">${esc(state.empty)}</span></div></div>` : ''}</div><span></span><div class="${NS}-x" aria-hidden="true" style="--n:${n}">${xs}</div></div>`;
|
|
48
48
|
if (!drawn && !busy && rows.length && !matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
49
|
-
el.querySelectorAll(`.${NS}-stack`).forEach((s, i) => s.animate([{ transform: 'scaleY(0)' }, { transform: 'scaleY(1)' }], { duration: 480, delay: i * 24, easing: 'cubic-bezier(.
|
|
49
|
+
el.querySelectorAll(`.${NS}-stack`).forEach((s, i) => s.animate([{ transform: 'scaleY(0)' }, { transform: 'scaleY(1)' }], { duration: 480, delay: i * 24, easing: 'cubic-bezier(.23, 1, .32, 1)', fill: 'backwards' }));
|
|
50
50
|
drawn = true;
|
|
51
51
|
}
|
|
52
52
|
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# StackedBars - bars over time with one or more series stacked in each: the year by month,
|
|
1
|
+
# StackedBars - bars over time with one or more series stacked in each: the year by month, appointments and pedidos together.
|
|
2
2
|
# The axis is in the page's unit (USD 8K), a bar can be picked (it is pressed, and onPick gets its id), while nothing
|
|
3
3
|
# is there yet a calm note sits over the plot (`empty`, with `emptyTitle` on top), and while it loads (`busy`) a skeleton keeps the shape. When the
|
|
4
4
|
# data changes the bars slide to their new height. Each bar says its values to a screen reader and on hover.
|
|
5
|
-
# StackedBars(rows: @months, series: @kinds, selected: monthId, unit: "USD", onPick: pickMonth, label: "
|
|
5
|
+
# StackedBars(rows: @months, series: @kinds, selected: monthId, unit: "USD", onPick: pickMonth, label: "Sales by month")
|
|
6
6
|
# rows {id, label, <key>: number…} · series {key, label, tone}: tone brand | coral | teal | amber | blue | pink, the
|
|
7
7
|
# series palette (the first series sits at the bottom). One series is a plain bar chart. A legend shows from two.
|
|
8
8
|
# readonly: true shows the pick but lets no bar be pressed (a plan that cannot change month).
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# StampCard - a loyalty card: stamps toward a reward (good for any shop).
|
|
2
|
-
# StampCard(store: "
|
|
2
|
+
# StampCard(store: "El Roble Barbershop", owner: "Laura", total: 10, stamps: 7, reward: "a free haircut")
|
|
3
3
|
# The words are templates (%owner %n %total %left %reward); the defaults are Spanish:
|
|
4
4
|
# StampCard(…, title: "%owner's card", progress: "%n of %total · %left to go for %reward", complete: "Complete! %reward earned.",
|
|
5
5
|
# count: "%n of %total stamps", redeem: "Redeem reward", stamp: "Stamp a visit")
|
|
6
6
|
# demo "on": it stamps itself one visit at a time and starts over, for a landing; the first touch stops it.
|
|
7
7
|
# shadcn-for-muten · part over KitWidget (kit core).
|
|
8
|
-
part StampCard(store: text, owner: text, total: number = 10, stamps: number, reward: text, demo: text = "", title: text = "
|
|
8
|
+
part StampCard(store: text, owner: text, total: number = 10, stamps: number, reward: text, demo: text = "", title: text = "%owner's card", progress: text = "%n of %total · %left more for %reward", complete: text = "Complete! Earned %reward.", count: text = "%n of %total stamps", redeem: text = "Redeem reward", stamp: text = "Stamp a visit") {
|
|
9
9
|
Custom KitWidget inputs(name: "stampCard", store: $store, owner: $owner, total: $total, stamps: $stamps, reward: $reward, demo: $demo, title: $title, progress: $progress, complete: $complete, count: $count, redeem: $redeem, stamp: $stamp) class("cx-stamp-host")
|
|
10
10
|
}
|
package/registry/stat-band.muten
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# StatBand - a row of big numbers split by hairlines (two per row on a phone). Each Stat holds its value in the slot
|
|
2
2
|
# (a Span, or a CountUp that counts up when it comes into view) and its label under it.
|
|
3
|
-
# StatBand { Stat(label: "
|
|
3
|
+
# StatBand { Stat(label: "appointments a month") { CountUp(to: "12,400") } Stat(label: "shops") { Span "850" } }
|
|
4
4
|
# look: "tiles" puts each number on its own soft tile, left-aligned, for a card inside an app (what the bot did this
|
|
5
|
-
# month:
|
|
6
|
-
# StatBand(look: "tiles") { Stat(label: "
|
|
5
|
+
# month: “0 bookings it closed by itself”); the default is the landing's band, centred between hairlines.
|
|
6
|
+
# StatBand(look: "tiles") { Stat(label: "bookings it closed by itself") { Span "12" } Stat(label: "orders it took by itself") { Span "5" } }
|
|
7
7
|
# While the numbers load, put a Skeleton in each Stat's slot.
|
|
8
8
|
# shadcn-for-muten · parts · no deps · kit.css (.cx-statband).
|
|
9
9
|
part StatBand(look: text = "") { Stack class("cx-statband") data(look: $look) { slot } }
|
package/registry/stat.muten
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# StatCard - one number for any use (sales, bookings, wait time…): label, the value, the change against a period and
|
|
2
2
|
# its line. The icon goes in the slot (or leave it empty). `tone` says if the change is good (ok) or bad (bad) -
|
|
3
3
|
# for a cost or a wait, going down is the good one.
|
|
4
|
-
# StatCard(label: "
|
|
4
|
+
# StatCard(label: "Sales", value: "$1.284.000", delta: "▲ 12 %", tone: "ok", compare: "vs. last week", series: "3,5,4,6,5,8,9") { Icon "lucide:credit-card" }
|
|
5
5
|
# A card that asks for something (what is owed, orders to confirm): `line` says what the number means and `to` + `cta`
|
|
6
6
|
# is the one thing to do about it, written at the foot of the card.
|
|
7
|
-
# StatCard(label: "
|
|
7
|
+
# StatCard(label: "To collect", value: "USD 305", line: "4 appointments not marked as paid", to: "/close-day", cta: "Close the day") { Icon "lucide:clock" }
|
|
8
8
|
# busy: true keeps the card's shape with a skeleton while the number loads (the label stays).
|
|
9
9
|
# Lay several in a StatGrid. Spark draws the series alone: Spark(series: "3,5,4", tone: "success").
|
|
10
10
|
# shadcn-for-muten · part + Custom (Spark.js) · kit.css (.cx-stat).
|
|
@@ -20,7 +20,7 @@ part StatCard(label: text, value: text = "", delta: text = "", tone: text = "ok"
|
|
|
20
20
|
Skeleton() class("cx-stat-sk-l")
|
|
21
21
|
}
|
|
22
22
|
when not $busy {
|
|
23
|
-
|
|
23
|
+
Stack class("cx-text cx-stat-v") data(slot: "text", variant: "title") { CountUp(to: $value, roll: "change") }
|
|
24
24
|
Span "{$line}" class("cx-text cx-stat-line") data(slot: "text", variant: "caption")
|
|
25
25
|
Stack class("cx-stat-row cx-stat-delta") {
|
|
26
26
|
Span "{$delta}" class("cn-badge cn-badge-variant-ghost group/badge") data(slot: "badge", variant: "status", tone: $tone)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# StatusBar - the thin line under an editor that says how things stand: the tool, the position, the size, the zoom,
|
|
2
2
|
# whether it is saved. StatusSpacer pushes what follows to the right; an item can carry an icon.
|
|
3
|
-
# StatusBar { StatusItem(label: "
|
|
3
|
+
# StatusBar { StatusItem(label: "Done") { Icon "lucide:circle-check" } StatusSpacer() StatusItem(label: "1080 × 1350") StatusItem(label: "100 %") }
|
|
4
4
|
# shadcn-for-muten · parts · no deps · kit.css (.cx-status).
|
|
5
|
-
part StatusBar(label: text = "
|
|
5
|
+
part StatusBar(label: text = "Status") { Stack class("cx-status") data(slot: "status-bar", trl: $label) aria(role: "status", label: $label) { slot } }
|
|
6
6
|
part StatusItem(label: text) { Stack class("cx-status-item") { slot Span "{$label}" class("cx-status-text") } }
|
|
7
7
|
part StatusSpacer() { Stack class("cx-status-spacer") }
|
package/registry/stepper.muten
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Stepper - where you are in a flow of a few steps. Horizontal: one short bar per step under its name, as the «Nueva
|
|
2
|
-
#
|
|
2
|
+
# appointment» modal shows it. Vertical: numbered circles joined by a line, each with what it asks. A done step fills its
|
|
3
3
|
# bar (or shows a check); the current one is marked and read out as the current step.
|
|
4
|
-
# Stepper(count: 4, label: "
|
|
5
|
-
# Stepper(count: 3, orientation: "vertical") { Step(num: "1", label: "
|
|
6
|
-
# look "list" (vertical): a numbered list for
|
|
4
|
+
# Stepper(count: 4, label: "Step 2 of 4: Service") { Step(num: "1", label: "Customer", state: "done") Step(num: "2", label: "Service", state: "now") … }
|
|
5
|
+
# Stepper(count: 3, orientation: "vertical") { Step(num: "1", label: "Your business", description: "Nombre y rubro", state: "done") … }
|
|
6
|
+
# look "list" (vertical): a numbered list for “how it works”, ruled rows and a round number, the current one filled.
|
|
7
7
|
# state: done | now | "" (to come). shadcn-for-muten · part · no deps · kit.css (.cx-stepper).
|
|
8
8
|
part Stepper(count: number, orientation: text = "horizontal", label: text = "", look: text = "") {
|
|
9
9
|
Stack class("cx-stepper cx-stp") data(slot: "stepper", orientation: $orientation, look: $look) style(n: "{$count}") aria(role: "list", label: $label) { slot }
|
package/registry/store-qr.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// StoreQr (muten Custom) - the kit's storeQr (kit/menus.js).
|
|
2
2
|
export function mount(el, inputs, on) {
|
|
3
|
-
Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/menus.js')]).then(([core, m]) => m.storeQr(el, inputs, core, on));
|
|
3
|
+
Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/menus.js')]).then(([core, m]) => core.relang(el, inputs, () => m.storeQr(el, inputs, core, on)));
|
|
4
4
|
return () => {};
|
|
5
5
|
}
|
package/registry/store-qr.muten
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
# StoreQr - the shop's QR with its mark in the middle, the name, the link and
|
|
1
|
+
# StoreQr - the shop's QR with its mark in the middle, the name, the link and “Copiar enlace” (falls back to
|
|
2
2
|
# selecting the link when the clipboard says no).
|
|
3
|
-
# StoreQr(url: "https://storio.ink/el-roble", name: "
|
|
3
|
+
# StoreQr(url: "https://storio.ink/el-roble", name: "El Roble Barbershop", mark: "ER")
|
|
4
4
|
# shadcn-for-muten · Custom (kit menus + vendored qrcode-generator, MIT) · host JS store-qr.js.
|
|
5
5
|
part StoreQr(url: text, name: text = "", mark: text = "") { Custom StoreQr inputs(url: $url, name: $name, mark: $mark) class("cx-qr-host") }
|
package/registry/swatch.muten
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# action pick(c: text) mutates active { active.set(c) }
|
|
5
5
|
# SwatchPalette(label: "Colores recientes") { each colors as c { Swatch(color: c, current: active, onSelect: pick) } }
|
|
6
6
|
# size: sm | default | lg. shadcn-for-muten · part · no deps · kit.css (.cx-swatch).
|
|
7
|
-
part SwatchPalette(label: text = "
|
|
7
|
+
part SwatchPalette(label: text = "Colors") { Stack class("cx-swatches-row") data(trl: $label) aria(role: "group", label: $label) { slot } }
|
|
8
8
|
part Swatch(color: text, current: text, onSelect: action, size: text = "default") {
|
|
9
9
|
Button -> $onSelect($color) class("cx-swatch") data(size: $size, name: $color) aria(pressed: $color == $current, label: $color) style(bg: "{$color}")
|
|
10
10
|
}
|
package/registry/switch.muten
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
# Switch - on/off; the page owns the bool and the action. SwitchRow is the settings line: label, switch, the why.
|
|
2
|
-
# Switch(on: dark, onToggle: toggleDark, label: "
|
|
3
|
-
# SwitchRow(on: home, onToggle: toggleHome, label: "
|
|
2
|
+
# Switch(on: dark, onToggle: toggleDark, label: "Dark mode")
|
|
3
|
+
# SwitchRow(on: home, onToggle: toggleHome, label: "Also at home", description: "Ask for identity verification…")
|
|
4
|
+
# disabled: a bool (a state or a getter) - it shows its state and cannot be flipped (a locked choice, a prerequisite missing).
|
|
4
5
|
# size: default | sm. shadcn-for-muten · part · no deps · Nova (.cn-switch reads data-checked / data-unchecked).
|
|
5
|
-
part Switch(on: bool, onToggle: action, label: text = "", size: text = "default") {
|
|
6
|
-
Button -> $onToggle class("cn-switch group/switch") data(slot: "switch", size: $size, checked: $on, unchecked: not $on) aria(role: "switch", checked: $on, label: $label) {
|
|
6
|
+
part Switch(on: bool, onToggle: action, label: text = "", size: text = "default", disabled: bool = false) {
|
|
7
|
+
Button -> $onToggle class("cn-switch group/switch") data(slot: "switch", size: $size, checked: $on, unchecked: not $on) aria(role: "switch", checked: $on, label: $label) disabled when $disabled {
|
|
7
8
|
Span "" class("cn-switch-thumb") data(slot: "switch-thumb", checked: $on, unchecked: not $on)
|
|
8
9
|
}
|
|
9
10
|
}
|
|
10
|
-
part SwitchRow(on: bool, onToggle: action, label: text, description: text = "") {
|
|
11
|
-
Stack class("cx-switch-row") {
|
|
11
|
+
part SwitchRow(on: bool, onToggle: action, label: text, description: text = "", disabled: bool = false) {
|
|
12
|
+
Stack class("cx-switch-row") data(disabled: $disabled) {
|
|
12
13
|
Span "{$label}" class("cn-label") data(slot: "label")
|
|
13
|
-
Switch(on: $on, onToggle: $onToggle, label: $label)
|
|
14
|
+
Switch(on: $on, onToggle: $onToggle, label: $label, disabled: $disabled)
|
|
14
15
|
Text "{$description}" class("cx-switch-why")
|
|
15
16
|
}
|
|
16
17
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# TableView - the DataTable: filters as data (search · facet · date · range) with counts and chips, rows that fade /
|
|
2
2
|
# slide / enter, the height easing, sort by header, selection with a bulk bar in its own space, the phone's
|
|
3
|
-
#
|
|
4
|
-
# TableView(columns: @cols, rows: @appointments, filters: @filters, statuses: @statuses, bulk: @bulk, one: "
|
|
3
|
+
# “buscar + Filtros” sheet, and Items when its box is narrow. (`DataTable` is muten's raw table primitive.)
|
|
4
|
+
# TableView(columns: @cols, rows: @appointments, filters: @filters, statuses: @statuses, bulk: @bulk, one: "appointment", many: "appointments", onAction: act)
|
|
5
5
|
# columns {key, label, type: person|text|date|status|money, sub} · filters {id, type, label, key | keys} ·
|
|
6
6
|
# statuses {id, label, tone} · bulk {id, label, variant, set {…}} → onAction gets {"action", "ids"}.
|
|
7
7
|
# shadcn-for-muten · part over KitWidget (kit core + kit/table.js).
|
|
8
|
-
part TableView(columns: list, rows: list, filters: list, statuses: list, bulk: list, one: text = "
|
|
8
|
+
part TableView(columns: list, rows: list, filters: list, statuses: list, bulk: list, one: text = "row", many: text = "rows", onAction: action) {
|
|
9
9
|
Custom KitWidget inputs(name: "table", columns: $columns, rows: $rows, filters: $filters, statuses: $statuses, bulk: $bulk, one: $one, many: $many) on(action: $onAction) class("cx-table-host")
|
|
10
10
|
}
|
|
@@ -6,9 +6,17 @@ export function mount(el) {
|
|
|
6
6
|
const ind = document.createElement('span'); ind.className = 'cx-tab-ind'; ind.setAttribute('aria-hidden', 'true'); list.prepend(ind);
|
|
7
7
|
const tabs = () => [...list.querySelectorAll(':scope > .cx-tab')];
|
|
8
8
|
const chosen = () => tabs().find((t) => t.getAttribute('aria-selected') === 'true' || t.getAttribute('aria-pressed') === 'true');
|
|
9
|
-
let first = true;
|
|
9
|
+
let first = true, lastIdx = -1;
|
|
10
|
+
// once the person switches (never on the first paint), the panels' container learns it and which way they went, so
|
|
11
|
+
// the new panel comes in from that side (kit.css “Tab panels”)
|
|
12
|
+
const mark = (t) => {
|
|
13
|
+
const k = tabs().indexOf(t); const box = list.parentElement;
|
|
14
|
+
if (lastIdx >= 0 && k !== lastIdx && box) { box.setAttribute('data-tabs-moved', ''); box.setAttribute('data-tabs-dir', k < lastIdx ? 'back' : 'forward'); }
|
|
15
|
+
lastIdx = k;
|
|
16
|
+
};
|
|
10
17
|
const place = () => {
|
|
11
18
|
const t = chosen(); if (!t) { ind.style.opacity = '0'; return; }
|
|
19
|
+
mark(t);
|
|
12
20
|
ind.style.opacity = '';
|
|
13
21
|
if (first) ind.style.transition = 'none';
|
|
14
22
|
ind.style.left = t.offsetLeft + 'px'; ind.style.width = t.offsetWidth + 'px';
|
package/registry/tabs.muten
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Tabs - the indicator slides from one tab to the next (260 ms). variant "" = segmented (a pill on a muted track),
|
|
2
2
|
# "line" = an underline with a count per tab that scrolls sideways on a phone. Arrows, Home and End move between tabs.
|
|
3
3
|
# The page owns the current value; TabsContent shows the panel of the current one.
|
|
4
|
-
# Tabs(label: "
|
|
5
|
-
# Tabs(label: "
|
|
4
|
+
# Tabs(label: "Period") { TabsTrigger(value: "day", current: period, label: "Today", onSelect: setPeriod) TabsTrigger(value: "week", current: period, label: "Week", onSelect: setPeriod) }
|
|
5
|
+
# Tabs(label: "Orders", variant: "line") { TabsTrigger(value: "new", current: lane, label: "New", count: "4", onSelect: setLane) … }
|
|
6
6
|
# TabsContent(value: "new", current: lane) { … }
|
|
7
7
|
# shadcn-for-muten · part + Custom (TabsBehavior: the indicator + keys) · kit.css (.cx-tabs).
|
|
8
8
|
part Tabs(label: text, variant: text = "") {
|
package/registry/tag-input.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// TagInput (muten Custom) - the kit's tags field (kit/collections.js tagInput): chips in/out, height eases, a cap.
|
|
2
2
|
export function mount(el, inputs, on) {
|
|
3
|
-
Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/collections.js')]).then(([core, c]) => c.tagInput(el, { ...inputs, tags: inputs.value ?? inputs.tags }, core, on));
|
|
3
|
+
Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/collections.js')]).then(([core, c]) => core.relang(el, inputs, () => c.tagInput(el, { ...inputs, tags: inputs.value ?? inputs.tags }, core, on)));
|
|
4
4
|
return () => {};
|
|
5
5
|
}
|
package/registry/tag-input.muten
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# TagInput - tags as chips: Enter or a comma adds, Backspace marks then removes the last, a repeated tag flashes the
|
|
2
2
|
# one already there, suggestions to tap, a cap with its count. The value is comma-joined text.
|
|
3
|
-
# TagInput(value: tags, label: "
|
|
3
|
+
# TagInput(value: tags, label: "Service tags", suggestions: "Fade,Razor,Kids", max: 8, hint: "They help people find you.", onChange: setTags)
|
|
4
4
|
# shadcn-for-muten · Custom (kit collections) · host JS tag-input.js.
|
|
5
|
-
part TagInput(value: text, placeholder: text = "", label: text = "
|
|
5
|
+
part TagInput(value: text, placeholder: text = "", label: text = "Tags", suggestions: text = "", max: number = 8, hint: text = "", onChange: action) {
|
|
6
6
|
Custom TagInput inputs(value: $value, placeholder: $placeholder, label: $label, suggestions: $suggestions, max: $max, hint: $hint) on(change: $onChange) class("cx-tags-host")
|
|
7
7
|
}
|
package/registry/text.muten
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Text (part name Txt - `Text` is a muten primitive) - the one text atom, in a few roles. Everything that shows words
|
|
2
2
|
# in the kit is built from it, so a role changes in one place.
|
|
3
|
-
# Txt(value: "
|
|
4
|
-
# Txt(value: "{count}
|
|
3
|
+
# Txt(value: "Classic cut", variant: "heading")
|
|
4
|
+
# Txt(value: "{count} appointments", variant: "muted")
|
|
5
5
|
# variant: body | title | heading | strong | label | muted | caption (default: body)
|
|
6
6
|
# shadcn-for-muten · part · no deps · styled by kit.css (.cx-text).
|
|
7
7
|
part Txt(value: text, variant: text = "body") { Span "{$value}" class("cx-text") data(slot: "text", variant: $variant) }
|
package/registry/time-field.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// TimeField (muten Custom) - the kit's timeField (kit/dates.js).
|
|
2
2
|
export function mount(el, inputs, on) {
|
|
3
|
-
Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/dates.js')]).then(([core, m]) => m.timeField(el, inputs, core, on));
|
|
3
|
+
Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/dates.js')]).then(([core, m]) => core.relang(el, inputs, () => m.timeField(el, inputs, core, on)));
|
|
4
4
|
return () => {};
|
|
5
5
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# TimeField - a field-like button with the time (and
|
|
2
|
-
# TimeField(label: "
|
|
1
|
+
# TimeField - a field-like button with the time (and AM/PM); it opens the hour / minute wheels.
|
|
2
|
+
# TimeField(label: "Time", value: "10:30", from: 9, to: 19, step: 15, onChange: setTime) → "HH:MM"
|
|
3
3
|
# shadcn-for-muten · Custom (kit dates) · host JS time-field.js.
|
|
4
|
-
part TimeField(label: text = "
|
|
4
|
+
part TimeField(label: text = "Time", value: text = "", from: number = 9, to: number = 19, step: number = 15, onChange: action) {
|
|
5
5
|
Custom TimeField inputs(label: $label, value: $value, from: $from, to: $to, step: $step) on(change: $onChange) class("cx-tf-host")
|
|
6
6
|
}
|
package/registry/timeline.muten
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Timeline - what happened, in order: a dot and a line join the events; the last one (where things stand now) is
|
|
2
2
|
# marked. Each event has what happened, when, and an optional detail.
|
|
3
|
-
# Timeline(label: "
|
|
4
|
-
# TimelineItem(title: "
|
|
5
|
-
# TimelineItem(title: "
|
|
3
|
+
# Timeline(label: "Order history") {
|
|
4
|
+
# TimelineItem(title: "Order received", time: "today · 10:38")
|
|
5
|
+
# TimelineItem(title: "Waiting for your review", time: "now", state: "now")
|
|
6
6
|
# }
|
|
7
7
|
# state: "" (past) | now (where it stands) | next (still to come, faded).
|
|
8
8
|
# shadcn-for-muten · part · no deps · kit.css (.cx-timeline).
|
|
9
|
-
part Timeline(label: text = "") { Stack class("cx-timeline cx-tl") aria(role: "list", label: $label) { slot } }
|
|
9
|
+
part Timeline(label: text = "") { Stack class("cx-timeline cx-tl") aria(role: "list", label: $label) { slot ListMotion() } }
|
|
10
10
|
part TimelineItem(title: text, time: text = "", description: text = "", state: text = "") {
|
|
11
11
|
Stack class("cx-tl-item") data(state: $state) aria(role: "listitem") {
|
|
12
12
|
Span "{$title}" class("cx-text cx-tl-title") data(slot: "text", variant: "body")
|
package/registry/toaster.muten
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
# hovering the pile fans them out and pauses them; each one leaves on its own (duration) or with its ×, and can carry
|
|
3
3
|
# one action (Deshacer). The page bumps a counter to show one, with the title it has at that moment:
|
|
4
4
|
# state { toasts = 0 : number toastTitle = "" : text }
|
|
5
|
-
# action saved() mutates toasts, toastTitle { toastTitle.set("
|
|
6
|
-
# Toaster(trigger: toasts, title: toastTitle, description: "
|
|
5
|
+
# action saved() mutates toasts, toastTitle { toastTitle.set("Appointment created") toasts.set(toasts + 1) }
|
|
6
|
+
# Toaster(trigger: toasts, title: toastTitle, description: "Thursday 9 at 10:30", action: "Undo", onAction: undo)
|
|
7
7
|
# For what the person just did on this screen (saving, saved) the Island is lighter; a toast is for news that can wait.
|
|
8
8
|
# shadcn-for-muten · Custom · kit.css (.pg-toasts, .pg-toast) · host JS toaster.js.
|
|
9
9
|
part Toaster(trigger: number, title: text, description: text = "", action: text = "", duration: number = 5000, onAction: action) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# ToggleGroup - toggles in a row (shadcn Nova). One choice: ToggleGroupItem with the page's text state. Several
|
|
2
2
|
# choices: put Toggle parts inside, each with its own bool. spacing "0" joins them into one control; "2" separates.
|
|
3
3
|
# state { align = "left" : text } action setAlign(v: text) mutates align { align.set(v) }
|
|
4
|
-
# ToggleGroup(label: "
|
|
4
|
+
# ToggleGroup(label: "Alignment") { ToggleGroupItem(value: "left", current: align, onSelect: setAlign, label: "Left") { Icon "lucide:align-left" } … }
|
|
5
5
|
# ToggleGroup(variant: "outline", spacing: "2") { Toggle(on: bold, onToggle: b, variant: "outline") { Icon "lucide:bold" } … }
|
|
6
6
|
# variant: default | outline · size: sm | default | lg · spacing: 0 | 1 | 2 · orientation: horizontal | vertical
|
|
7
7
|
# shadcn-for-muten · part · no deps · Nova (.cn-toggle-group) + kit.css (joined/vertical).
|
package/registry/toggle.muten
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# Toggle - a button that stays pressed (bold, favourite,
|
|
1
|
+
# Toggle - a button that stays pressed (bold, favourite, “today only”). The page owns the bool and the action.
|
|
2
2
|
# state { bold = false : bool } action toggleBold() mutates bold { bold.toggle() }
|
|
3
|
-
# Toggle(on: bold, onToggle: toggleBold, label: "
|
|
4
|
-
# Toggle(on: today, onToggle: toggleToday, variant: "outline") { Icon "lucide:calendar" Span "
|
|
3
|
+
# Toggle(on: bold, onToggle: toggleBold, label: "Bold") { Icon "lucide:bold" }
|
|
4
|
+
# Toggle(on: today, onToggle: toggleToday, variant: "outline") { Icon "lucide:calendar" Span "Today only" }
|
|
5
5
|
# variant: default | outline · size: sm | default | lg · label: the name of an icon-only toggle.
|
|
6
6
|
# shadcn-for-muten · part · no deps · Nova (.cn-toggle, pressed = aria-pressed).
|
|
7
7
|
part Toggle(on: bool, onToggle: action, variant: text = "default", size: text = "default", label: text = "") {
|
package/registry/toolbar.muten
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Toolbar - the tools of an editor in one bar: groups of icon buttons (Btn, Toggle, ToggleGroup) split by thin lines.
|
|
2
2
|
# It floats over a canvas (CanvasViewport) or sits on top of a panel.
|
|
3
|
-
# Toolbar(label: "
|
|
4
|
-
# ToolbarGroup { ToggleGroup(label: "
|
|
3
|
+
# Toolbar(label: "Tools") {
|
|
4
|
+
# ToolbarGroup { ToggleGroup(label: "Tool") { ToggleGroupItem(value: "select", current: tool, onSelect: setTool, label: "Select") { Icon "lucide:mouse-pointer-2" } … } }
|
|
5
5
|
# ToolbarSeparator()
|
|
6
|
-
# ToolbarGroup { Btn(variant: "ghost", size: "icon-sm", label: "
|
|
6
|
+
# ToolbarGroup { Btn(variant: "ghost", size: "icon-sm", label: "Undo", onClick: undo) { Icon "lucide:undo-2" } }
|
|
7
7
|
# }
|
|
8
8
|
# shadcn-for-muten · parts · no deps · kit.css (.cx-toolbar).
|
|
9
|
-
part Toolbar(label: text = "
|
|
9
|
+
part Toolbar(label: text = "Tools") { Stack class("cx-toolbar") data(slot: "toolbar", trl: $label) aria(role: "toolbar", label: $label) { slot } }
|
|
10
10
|
part ToolbarGroup() { Stack class("cx-toolbar-group") data(slot: "toolbar-group") { slot } }
|
|
11
11
|
part ToolbarSeparator() { Stack class("cx-toolbar-sep") data(slot: "toolbar-separator") aria(role: "separator", orientation: "vertical") }
|
package/registry/tooltip.muten
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Tooltip - a short name that shows while the pointer rests on something (or it gets the focus): what an icon-only
|
|
2
2
|
# button does, the full text of a cut-off line. Only a label: never a place for something the person must read.
|
|
3
3
|
# The kit's single tooltip (KitLayer) does it for anything with a name; Tooltip just gives that name.
|
|
4
|
-
# Tooltip(text: "
|
|
4
|
+
# Tooltip(text: "Edit service") { Btn(variant: "ghost", size: "icon", onClick: edit) { Icon "lucide:pencil" } }
|
|
5
5
|
# A Btn with label already has one. shadcn-for-muten · part · the kit's tooltip (.cx-tip).
|
|
6
6
|
# side: "right" puts it beside the trigger instead of above (a vertical rail, where above covers the item before it).
|
|
7
|
-
# Tooltip(text: "
|
|
7
|
+
# Tooltip(text: "Bring from Linktree or Shopify", side: "right") { Link … }
|
|
8
8
|
part Tooltip(text: text, side: text = "") { Stack class("cx-tip-wrap") data(slot: "tooltip-trigger", name: $text, side: $side) { slot } }
|