@muten/shadcn 0.0.7 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +47 -0
- package/README.md +47 -0
- package/kit.css +2763 -0
- package/motion.css +31 -1
- package/nova.css +1387 -0
- package/package.json +6 -2
- package/registry/accordion.muten +11 -10
- package/registry/agenda.js +35 -0
- package/registry/agenda.muten +10 -0
- package/registry/alert-dialog.muten +9 -12
- package/registry/alert.muten +8 -8
- package/registry/app-shell.js +9 -0
- package/registry/app-shell.muten +12 -0
- package/registry/attachment.muten +13 -9
- package/registry/avatar.muten +24 -5
- package/registry/badge.muten +23 -8
- package/registry/band.muten +16 -0
- package/registry/bar-chart.muten +7 -0
- package/registry/bell.muten +3 -0
- package/registry/bento.muten +6 -9
- package/registry/booking.js +21 -0
- package/registry/booking.muten +13 -0
- package/registry/breadcrumb.muten +9 -7
- package/registry/button-group.muten +13 -4
- package/registry/button.muten +21 -8
- package/registry/calendar.js +28 -93
- package/registry/calendar.muten +9 -8
- package/registry/canvas-viewport.js +12 -5
- package/registry/canvas-viewport.muten +9 -9
- package/registry/card-field.muten +4 -0
- package/registry/card.muten +12 -8
- package/registry/carousel.js +5 -36
- package/registry/carousel.muten +12 -5
- package/registry/chatbot.muten +9 -0
- package/registry/checklist.muten +6 -0
- package/registry/choice-list.muten +16 -0
- package/registry/code.js +14 -12
- package/registry/code.muten +6 -7
- package/registry/collapsible.muten +14 -10
- package/registry/color-picker.js +460 -41
- package/registry/color-picker.muten +7 -6
- package/registry/combobox.js +5 -0
- package/registry/combobox.muten +6 -23
- package/registry/command-palette.muten +8 -12
- package/registry/command.js +32 -0
- package/registry/command.muten +32 -11
- package/registry/context-menu.muten +9 -8
- package/registry/count-up.muten +5 -10
- package/registry/date-field.js +5 -0
- package/registry/date-field.muten +7 -0
- package/registry/device-frame.muten +8 -0
- package/registry/dialog.muten +6 -12
- package/registry/dirty-bar.js +19 -0
- package/registry/dirty-bar.muten +13 -0
- package/registry/drawer.muten +6 -11
- package/registry/dropdown-menu.muten +37 -13
- package/registry/empty.muten +11 -7
- package/registry/feature-grid.muten +10 -12
- package/registry/feature-rows.muten +12 -8
- package/registry/field-behavior.js +33 -0
- package/registry/field-behavior.muten +9 -0
- package/registry/field.muten +26 -7
- package/registry/file-input.js +36 -21
- package/registry/file-input.muten +8 -7
- package/registry/fixed-card.muten +25 -0
- package/registry/folder-picker.js +13 -15
- package/registry/folder-picker.muten +5 -8
- package/registry/gradient-blinds.muten +6 -13
- package/registry/hover-card.muten +7 -9
- package/registry/image-upload.js +5 -0
- package/registry/image-upload.muten +8 -0
- package/registry/indicator.muten +5 -0
- package/registry/input-group.muten +27 -4
- package/registry/input-otp.js +8 -35
- package/registry/input-otp.muten +6 -6
- package/registry/input.muten +8 -7
- package/registry/inspector.muten +13 -15
- package/registry/island.js +7 -0
- package/registry/island.muten +5 -0
- package/registry/item.muten +20 -7
- package/registry/kbd.muten +3 -3
- package/registry/kit/board.js +300 -0
- package/registry/kit/collections.js +148 -0
- package/registry/kit/core.js +989 -0
- package/registry/kit/dates.js +106 -0
- package/registry/kit/menus.js +90 -0
- package/registry/kit/overlays.js +114 -0
- package/registry/kit/paint.js +211 -0
- package/registry/kit/qrcode.js +2291 -0
- package/registry/kit/structure.js +393 -0
- package/registry/kit/table.js +198 -0
- package/registry/kit/values.js +179 -0
- package/registry/kit-layer.js +2 -0
- package/registry/kit-layer.muten +5 -0
- package/registry/kit-skeleton.js +24 -0
- package/registry/kit-skeleton.muten +16 -0
- package/registry/kit-widget.js +79 -0
- package/registry/kit-widget.muten +4 -0
- package/registry/link-btn.muten +9 -7
- package/registry/live-stack.js +29 -0
- package/registry/live-stack.muten +7 -0
- package/registry/logo-cloud.muten +6 -11
- package/registry/member-list.muten +6 -0
- package/registry/menubar.muten +10 -14
- package/registry/message-scroller.js +12 -0
- package/registry/message-scroller.muten +8 -4
- package/registry/message.muten +27 -10
- package/registry/messaging.muten +11 -0
- package/registry/modal-behavior.js +56 -0
- package/registry/modal-behavior.muten +4 -0
- package/registry/modal.muten +29 -0
- package/registry/money-field.js +5 -0
- package/registry/money-field.muten +7 -0
- package/registry/nav-button.muten +14 -0
- package/registry/navigation-menu.muten +16 -14
- package/registry/navpill.muten +15 -9
- package/registry/new-appointment.js +8 -0
- package/registry/new-appointment.muten +10 -0
- package/registry/number-field.js +34 -24
- package/registry/number-field.muten +8 -6
- package/registry/order-board.js +6 -0
- package/registry/order-board.muten +21 -0
- package/registry/overlay-behavior.js +13 -0
- package/registry/overlay-behavior.muten +4 -0
- package/registry/pagination-nav.js +6 -0
- package/registry/pagination-nav.muten +8 -0
- package/registry/pagination.muten +12 -9
- package/registry/password-field.muten +3 -0
- package/registry/permission-matrix.muten +6 -0
- package/registry/phone-field.js +5 -0
- package/registry/phone-field.muten +7 -0
- package/registry/phone-shell.js +7 -0
- package/registry/phone-shell.muten +9 -0
- package/registry/pill.muten +6 -5
- package/registry/plans.muten +4 -0
- package/registry/popover.muten +16 -9
- package/registry/pricing.muten +21 -21
- package/registry/progress.muten +5 -5
- package/registry/queue.muten +3 -0
- package/registry/radio-group.muten +16 -8
- package/registry/range-slider.js +9 -24
- package/registry/range-slider.muten +4 -5
- package/registry/resizable.js +34 -27
- package/registry/resizable.muten +16 -5
- package/registry/roving-behavior.js +16 -0
- package/registry/roving-behavior.muten +3 -0
- package/registry/row-menu.js +5 -0
- package/registry/row-menu.muten +5 -0
- package/registry/save-button.muten +11 -0
- package/registry/scale-slider.js +7 -0
- package/registry/scale-slider.muten +9 -0
- package/registry/scroll-area.muten +8 -4
- package/registry/segmented-control.muten +8 -8
- package/registry/separator.muten +3 -3
- package/registry/share-button.js +7 -0
- package/registry/share-button.muten +10 -0
- package/registry/sheet.muten +6 -10
- package/registry/sidebar.muten +23 -15
- package/registry/skeleton.muten +6 -4
- package/registry/slider.js +73 -27
- package/registry/slider.muten +6 -6
- package/registry/sortable-behavior.js +43 -0
- package/registry/sortable-behavior.muten +2 -0
- package/registry/sortable-list.js +10 -26
- package/registry/sortable-list.muten +6 -7
- package/registry/sortable.muten +15 -0
- package/registry/spark.js +14 -0
- package/registry/spark.muten +3 -0
- package/registry/spinner.muten +4 -3
- package/registry/split-button.muten +12 -10
- package/registry/stamp-card.muten +7 -0
- package/registry/stat-band.muten +6 -9
- package/registry/stat.muten +23 -0
- package/registry/status-bar.muten +7 -6
- package/registry/stepper.muten +17 -14
- package/registry/store-qr.js +5 -0
- package/registry/store-qr.muten +5 -0
- package/registry/swatch.muten +8 -8
- package/registry/switch.muten +14 -6
- package/registry/table-view.muten +10 -0
- package/registry/tabs-behavior.js +27 -0
- package/registry/tabs-behavior.muten +3 -0
- package/registry/tabs.muten +19 -10
- package/registry/tag-input.js +4 -27
- package/registry/tag-input.muten +6 -6
- package/registry/text.muten +7 -0
- package/registry/time-field.js +5 -0
- package/registry/time-field.muten +6 -0
- package/registry/timeline.muten +14 -14
- package/registry/toaster.js +34 -22
- package/registry/toaster.muten +10 -8
- package/registry/toggle-group.muten +11 -6
- package/registry/toggle.muten +8 -5
- package/registry/toolbar.muten +11 -6
- package/registry/tooltip.muten +6 -9
- package/registry/tree-view.js +27 -7
- package/registry/tree-view.muten +8 -13
- package/registry/vector-field.muten +9 -8
- package/registry/virtual-list.js +5 -0
- package/registry/virtual-list.muten +6 -0
- package/registry/weekly-hours.muten +4 -0
- package/registry/wordline.muten +6 -0
- package/registry/zoom-control.muten +9 -9
- package/registry.json +872 -406
- package/registry/context-menu.js +0 -34
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// LiveStack (muten Custom) - a pile of news that keeps arriving (see live-stack.muten). Plain DOM, no kit needed:
|
|
2
|
+
// every `every` ms the next item drops in at the front; the older ones step back (--i) and the fourth leaves.
|
|
3
|
+
export function mount(el, inputs) {
|
|
4
|
+
let items = Array.isArray(inputs.items) ? inputs.items : [];
|
|
5
|
+
const every = Number(inputs.every) || 2800;
|
|
6
|
+
const still = matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
7
|
+
const esc = (s) => String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]);
|
|
8
|
+
el.setAttribute('role', 'log'); el.setAttribute('aria-live', 'polite'); if (inputs.label) el.setAttribute('aria-label', inputs.label);
|
|
9
|
+
const card = (it) => {
|
|
10
|
+
const n = document.createElement('div');
|
|
11
|
+
n.className = 'cx-live-card'; n.dataset.tone = it.tone || '';
|
|
12
|
+
n.innerHTML = `<span class="cx-live-ic">${it.icon ? `<svg><use href="#${esc(it.icon)}"></use></svg>` : ''}</span><span class="cx-live-tx"><b>${esc(it.title)}</b>${it.text ? `<span>${esc(it.text)}</span>` : ''}</span>${it.time ? `<small>${esc(it.time)}</small>` : ''}`;
|
|
13
|
+
return n;
|
|
14
|
+
};
|
|
15
|
+
const layout = () => [...el.children].forEach((c, i) => { c.style.setProperty('--i', i); if (i > 3) c.remove(); });
|
|
16
|
+
let k = 0, timer = 0, paused = false;
|
|
17
|
+
const push = () => {
|
|
18
|
+
if (!items.length) return;
|
|
19
|
+
const n = card(items[k % items.length]); k++;
|
|
20
|
+
n.dataset.enter = ''; el.prepend(n); layout();
|
|
21
|
+
requestAnimationFrame(() => requestAnimationFrame(() => delete n.dataset.enter));
|
|
22
|
+
};
|
|
23
|
+
const loop = () => { if (!paused) push(); timer = setTimeout(loop, every); };
|
|
24
|
+
if (still) { items.slice(0, 3).reverse().forEach((it) => { el.prepend(card(it)); }); layout(); }
|
|
25
|
+
else { push(); timer = setTimeout(loop, every); }
|
|
26
|
+
el.addEventListener('pointerenter', () => { paused = true; });
|
|
27
|
+
el.addEventListener('pointerleave', () => { paused = false; });
|
|
28
|
+
return (next) => { items = Array.isArray(next.items) ? next.items : items; };
|
|
29
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# LiveStack - news that keeps arriving, for a landing page: what the product does while nobody watches («Carla agendó
|
|
2
|
+
# un corte», «Pedido nuevo», «Tu asistente contestó»). One at a time a card drops in at the front and the older ones
|
|
3
|
+
# step back behind it, smaller, and leave after three. Pointer over it pauses; reduced motion shows the first three still.
|
|
4
|
+
# LiveStack(items: @news, every: 2800)
|
|
5
|
+
# items {icon (a kit sprite id), title, text, time, tone: "" | ok | wait | info}
|
|
6
|
+
# shadcn-for-muten · Custom · kit.css (.cx-live) · host JS live-stack.js.
|
|
7
|
+
part LiveStack(items: list, every: number = 2800, label: text = "") { Custom LiveStack inputs(items: $items, every: $every, label: $label) class("cx-live") }
|
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
# LogoCloud - the
|
|
2
|
-
# LogoCloudItem holds
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
# LogoCloudItem { Span "Globex" class("text-lg font-bold tracking-tight") } # text wordmark
|
|
8
|
-
# }
|
|
9
|
-
# shadcn-for-muten · block · no deps · styles via .logo-cloud / .logo-cloud-item in globals.css.
|
|
10
|
-
part LogoCloud() { Stack class("logo-cloud") { slot } }
|
|
11
|
-
part LogoCloudItem() { Stack class("logo-cloud-item") { slot } }
|
|
1
|
+
# LogoCloud - the «they use it» strip: a wrapping row of logos, muted until the pointer rests on one. A
|
|
2
|
+
# LogoCloudItem holds any logo: an Image, an Icon or a word in a Span; all take the same height.
|
|
3
|
+
# LogoCloud(label: "Lo usan") { LogoCloudItem { Image "~/logos/roble.svg" alt("El Roble") } LogoCloudItem { Span "Pelos & Co" } }
|
|
4
|
+
# shadcn-for-muten · parts · no deps · kit.css (.cx-logos).
|
|
5
|
+
part LogoCloud(label: text = "") { Stack class("cx-logos") aria(label: $label) { slot } }
|
|
6
|
+
part LogoCloudItem() { Stack class("cx-logo") { slot } }
|
|
@@ -0,0 +1,6 @@
|
|
|
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: "Propietario", one: "miembro", many: "miembros", cta: "Invitar", note: "Le llega un correo para unirse.")
|
|
3
|
+
# shadcn-for-muten · part over KitWidget (kit core).
|
|
4
|
+
part MemberList(members: list, roles: list, owner: text = "", one: text = "persona", many: text = "personas", cta: text = "Invitar", note: text = "") {
|
|
5
|
+
Custom KitWidget inputs(name: "memberList", members: $members, roles: $roles, owner: $owner, one: $one, many: $many, cta: $cta, note: $note) class("cx-team")
|
|
6
|
+
}
|
package/registry/menubar.muten
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
# Menubar - a
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
part MenubarItem(label: text, onSelect: action) { Button "{$label}" -> $onSelect class("dropdown-item") }
|
|
14
|
-
part MenubarSeparator() { Stack class("dropdown-separator") }
|
|
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
|
+
# opens that one, and the left and right arrows walk the bar. Each menu holds DropdownContent and its items.
|
|
3
|
+
# Menubar(label: "Editor") {
|
|
4
|
+
# MenubarMenu { MenubarTrigger(label: "Archivo") DropdownContent { DropdownItem(label: "Nuevo", shortcut: "⌘N", onSelect: create) … } }
|
|
5
|
+
# MenubarMenu { MenubarTrigger(label: "Editar") DropdownContent { … } }
|
|
6
|
+
# }
|
|
7
|
+
# shadcn-for-muten · parts + Custom (OverlayBehavior) · Nova (.cn-menubar*) + kit.css.
|
|
8
|
+
part Menubar(label: text = "Menú") { Stack class("cn-menubar cx-menubar") data(slot: "menubar") aria(role: "menubar", label: $label) { slot } }
|
|
9
|
+
part MenubarMenu() { Stack class("cx-ov") data(slot: "menubar-menu") { slot OverlayBehavior(kind: "menubar") } }
|
|
10
|
+
part MenubarTrigger(label: text) { Stack class("cx-ov-trigger") { Button "{$label}" class("cn-menubar-trigger") data(slot: "menubar-trigger") aria(role: "menuitem") } }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// MessageScrollerBehavior (muten Custom) - keeps a conversation at its latest message: it opens at the bottom, and a
|
|
2
|
+
// message that arrives keeps it there only when the reader was already at the bottom (scrolling back is respected).
|
|
3
|
+
export function mount(el) {
|
|
4
|
+
const box = el.parentElement;
|
|
5
|
+
let pinned = true;
|
|
6
|
+
const atBottom = () => box.scrollHeight - box.scrollTop - box.clientHeight < 24;
|
|
7
|
+
box.addEventListener('scroll', () => { pinned = atBottom(); }, { passive: true });
|
|
8
|
+
const stick = () => { if (pinned) box.scrollTop = box.scrollHeight; };
|
|
9
|
+
requestAnimationFrame(() => { box.scrollTop = box.scrollHeight; });
|
|
10
|
+
new MutationObserver(stick).observe(box, { childList: true, subtree: true });
|
|
11
|
+
return () => {};
|
|
12
|
+
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
#
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
part
|
|
1
|
+
# MessageScroller - the column of a conversation: it scrolls inside itself and opens at the latest message; a new
|
|
2
|
+
# message keeps it at the bottom only if you were already there (reading back is never interrupted).
|
|
3
|
+
# MessageScroller(label: "Chat con Valentina", height: "22rem") { Marker(label: "Hoy") Message(…) { … } }
|
|
4
|
+
# shadcn-for-muten · part + Custom (stick to the bottom) · kit.css (.cx-chat).
|
|
5
|
+
part MessageScroller(label: text = "Conversación", height: text = "22rem") {
|
|
6
|
+
Stack class("cx-chat cx-scroll cx-msg-scroller") data(slot: "message-scroller") style(h: "{$height}") aria(role: "log", label: $label) { slot MessageScrollerBehavior() }
|
|
7
|
+
}
|
|
8
|
+
part MessageScrollerBehavior() { Custom MessageScrollerBehavior class("cx-msg-behavior") }
|
package/registry/message.muten
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
|
-
# Message
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
|
|
6
|
-
|
|
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
|
+
# the primary colour, with the time and, on yours, whether it was sent, delivered or read. Text, a photo or an
|
|
3
|
+
# Attachment go inside. Marker is the day line between messages; MessageScroller holds them and scrolls.
|
|
4
|
+
# MessageScroller(label: "Chat con Valentina") {
|
|
5
|
+
# Marker(label: "Hoy")
|
|
6
|
+
# Message(variant: "received", time: "10:02") { Span "Hola, ¿tienen cita hoy?" }
|
|
7
|
+
# Message(variant: "sent", time: "10:03", status: "read") { Span "Sí: a las 15:00 con Camila." }
|
|
8
|
+
# }
|
|
9
|
+
# variant: received | sent · status (sent only): sending | sent | delivered | read · tag: a small label (Bot).
|
|
10
|
+
# shadcn-for-muten · parts · no deps · kit.css (.cx-chat-*).
|
|
11
|
+
part Message(variant: text = "received", time: text = "", status: text = "", tag: text = "") {
|
|
12
|
+
Stack class("cx-chat-msg") data(variant: $variant) {
|
|
13
|
+
Stack class("cx-chat-bubble cx-msg-bubble") {
|
|
14
|
+
Stack class("cx-msg-body") { slot }
|
|
15
|
+
Stack class("cx-chat-meta") {
|
|
16
|
+
Span "{$tag}" class("cx-chat-tag")
|
|
17
|
+
Span "{$time}" class("cx-text") data(variant: "caption")
|
|
18
|
+
when $status == "sending" { Icon "lucide:clock" class("cx-tick") data(status: "sending") aria(label: "Enviando") }
|
|
19
|
+
when $status == "sent" { Icon "lucide:check" class("cx-tick") data(status: "sent") aria(label: "Enviado") }
|
|
20
|
+
when $status == "delivered" { Icon "lucide:check-check" class("cx-tick") data(status: "delivered") aria(label: "Entregado") }
|
|
21
|
+
when $status == "read" { Icon "lucide:check-check" class("cx-tick") data(status: "read") aria(label: "Leído") }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
7
25
|
}
|
|
8
|
-
part Bubble(variant: text) {
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
part Marker(label: text) { Span "{$label}" class("marker") }
|
|
26
|
+
part Bubble(variant: text = "received") { Stack class("cx-chat-bubble cx-msg-bubble") data(variant: $variant) { slot } }
|
|
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 = "Foto") { Stack class("cx-chat-img") style(bg: "{$src}") aria(role: "img", label: $alt) }
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Messaging - one inbox for every channel: list (search, filters with counts, channel, state, unread), the chat (the
|
|
2
|
+
# bot's control, booking/order cards, system lines, quick actions that ask first, the InputGroup composer) and the
|
|
3
|
+
# person (numbers, what they have now, archive/delete). 3 panes wide, the person in a sheet medium, one pane narrow.
|
|
4
|
+
# All data (shapes in kit-widget.js / the bench page): channels, conversations (with messages + client), filters
|
|
5
|
+
# {id,label,state}, states {id,label,tone}, controls {id,icon,tone,title,description,action,next}, quick
|
|
6
|
+
# {id,icon,label,noun,title,subtitle}, templates {title,body}, replies {match (regex), text}.
|
|
7
|
+
# Messaging(channels: @channels, conversations: @inbox, filters: @filters, states: @states, controls: @controls, quick: @quick, templates: @templates, replies: @replies, onAction: act)
|
|
8
|
+
# shadcn-for-muten · part over KitWidget (kit core).
|
|
9
|
+
part Messaging(title: text = "Mensajes", channels: list, conversations: list, filters: list, states: list, controls: list, quick: list, templates: list, replies: list, onAction: action) {
|
|
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
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// ModalBehavior (muten Custom) - the behaviour of the adaptive modal; the markup, open/close and the animation are
|
|
2
|
+
// the Modal part's (muten state + kit.css). This only does what markup cannot: history (Back closes), Esc, the focus
|
|
3
|
+
// trap, the scroll lock, the auto mode (dialog on a wide screen, drawer on a phone) and the drawer's drag to close.
|
|
4
|
+
// Same rules as the reference artifact's modal.
|
|
5
|
+
export function mount(el, inputs, on) {
|
|
6
|
+
const mdl = el.closest('.cx-modal'), root = mdl.parentElement, ov = root.querySelector(':scope > .cx-overlay');
|
|
7
|
+
const body = mdl.querySelector(':scope > .cx-modal-body');
|
|
8
|
+
const phone = matchMedia('(max-width: 640px)');
|
|
9
|
+
const auto = mdl.dataset.mode === 'auto';
|
|
10
|
+
// the auto mode is resolved NOW (and again when the width crosses the phone line while closed), never at the moment
|
|
11
|
+
// of opening: a dialog that only learns it is a dialog as it opens animates its centring from the corner
|
|
12
|
+
const resolve = () => { if (auto && !isOpen) mdl.dataset.mode = phone.matches ? 'drawer' : 'dialog'; };
|
|
13
|
+
let isOpen = false, pushed = false, lastFocus = null;
|
|
14
|
+
queueMicrotask(() => resolve()); phone.addEventListener('change', resolve);
|
|
15
|
+
// the footer sticks under the scrolling body, not inside it
|
|
16
|
+
const placeFooter = () => { const f = body.querySelector('.cx-modal-foot'); if (f) mdl.appendChild(f); };
|
|
17
|
+
const focusables = () => [...mdl.querySelectorAll('button:not([disabled]), input, textarea, select, [tabindex]:not([tabindex="-1"])')].filter((x) => x.offsetParent !== null);
|
|
18
|
+
const close = () => on.close?.();
|
|
19
|
+
const open = () => {
|
|
20
|
+
if (isOpen) return; isOpen = true; placeFooter();
|
|
21
|
+
if (auto) mdl.dataset.mode = phone.matches ? 'drawer' : 'dialog';
|
|
22
|
+
lastFocus = document.activeElement;
|
|
23
|
+
document.documentElement.classList.add('cx-locked');
|
|
24
|
+
// on the phone, focusing a field would throw the keyboard over the sheet: focus the sheet itself
|
|
25
|
+
requestAnimationFrame(() => { ((mdl.dataset.mode === 'dialog' && focusables().find((x) => x.matches('input, textarea'))) || mdl).focus({ preventScroll: true }); });
|
|
26
|
+
history.pushState({ cxModal: 1 }, ''); pushed = true;
|
|
27
|
+
};
|
|
28
|
+
const shut = () => {
|
|
29
|
+
if (!isOpen) return; isOpen = false;
|
|
30
|
+
mdl.style.transform = ''; if (ov) ov.style.opacity = '';
|
|
31
|
+
document.documentElement.classList.remove('cx-locked');
|
|
32
|
+
lastFocus?.focus?.({ preventScroll: true });
|
|
33
|
+
if (pushed) { pushed = false; history.back(); }
|
|
34
|
+
};
|
|
35
|
+
mdl.tabIndex = -1;
|
|
36
|
+
addEventListener('popstate', () => { if (isOpen) { pushed = false; close(); } });
|
|
37
|
+
document.addEventListener('keydown', (e) => {
|
|
38
|
+
if (!isOpen) return;
|
|
39
|
+
if (e.key === 'Escape') { e.preventDefault(); close(); }
|
|
40
|
+
if (e.key === 'Tab') { const f = focusables(); if (!f.length) return; const first = f[0], last = f[f.length - 1]; if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } }
|
|
41
|
+
});
|
|
42
|
+
// drawer: drag the handle or the header down; closes past 30 % or on a flick, rubber-bands upward
|
|
43
|
+
let drag = null;
|
|
44
|
+
mdl.addEventListener('pointerdown', (e) => { if (mdl.dataset.mode !== 'drawer' || !e.target.closest('.cx-handle, [data-drag]')) return; drag = { y: e.clientY, t: performance.now(), dy: 0, live: false, id: e.pointerId }; });
|
|
45
|
+
addEventListener('pointermove', (e) => {
|
|
46
|
+
if (!drag || e.pointerId !== drag.id) return;
|
|
47
|
+
const dy = e.clientY - drag.y; if (!drag.live && Math.abs(dy) < 5) return;
|
|
48
|
+
if (!drag.live) { drag.live = true; mdl.classList.add('dragging'); ov?.classList.add('dragging'); }
|
|
49
|
+
drag.dy = dy; mdl.style.transform = `translateY(${dy > 0 ? dy : -Math.sqrt(-dy) * 2}px)`;
|
|
50
|
+
if (ov) ov.style.opacity = String(Math.max(0, 1 - Math.max(0, dy) / mdl.offsetHeight));
|
|
51
|
+
});
|
|
52
|
+
const endDrag = () => { if (!drag) return; const d = drag; drag = null; if (!d.live) return; mdl.classList.remove('dragging'); ov?.classList.remove('dragging'); const v = d.dy / (performance.now() - d.t); if (d.dy > mdl.offsetHeight * 0.3 || v > 0.5) close(); else { mdl.style.transform = ''; if (ov) ov.style.opacity = ''; } };
|
|
53
|
+
addEventListener('pointerup', endDrag); addEventListener('pointercancel', endDrag);
|
|
54
|
+
if (inputs.open === true || inputs.open === 'true') open();
|
|
55
|
+
return (next) => { (next.open === true || next.open === 'true') ? open() : shut(); };
|
|
56
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# ModalBehavior - what a modal needs that is not markup: the phone's Back closes it, Esc closes it, focus stays
|
|
2
|
+
# inside, the page behind does not scroll, and on a phone the drawer drags down to close. Used by Modal; you never
|
|
3
|
+
# call it yourself. shadcn-for-muten · Custom · host JS is modal-behavior.js.
|
|
4
|
+
part ModalBehavior(open: bool, onClose: action) { Custom ModalBehavior inputs(open: $open) on(close: $onClose) class("cx-modal-behavior") }
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Modal - THE adaptive modal: a Dialog on a wide screen, a Drawer from the bottom on a phone (mode "auto"), or forced
|
|
2
|
+
# with mode "dialog" | "drawer". The page owns the open bool; the overlay, the ✕, Esc, the phone's Back button and a
|
|
3
|
+
# drag down on the drawer all call onClose. Content goes in the slot; the buttons in a ModalFooter (it sticks under
|
|
4
|
+
# the scrolling body): the way out (cx-exit) on the left, the action (cx-go) filling the rest.
|
|
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: "Editar servicio", description: "Lo que cambies se ve en tu tienda al guardar.") {
|
|
7
|
+
# Field { … }
|
|
8
|
+
# ModalFooter { Btn(variant: "outline", onClick: done) class("cx-exit") { Span "Cancelar" } Btn(onClick: save) class("cx-go") { Span "Guardar cambios" } }
|
|
9
|
+
# }
|
|
10
|
+
# mode: auto | dialog | drawer | sheet (a panel from a side) · side: right | left | top | bottom (for sheet)
|
|
11
|
+
# role: dialog | alertdialog · dismiss: on | off (off: no ✕ and the backdrop does not close it; only its buttons do)
|
|
12
|
+
# size: "" | tall (a list that pages) · shadcn-for-muten · part + Custom (ModalBehavior) · kit.css (.cx-modal).
|
|
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
|
+
Stack class("cx-modal-root") data(open: $open) {
|
|
15
|
+
when $dismiss != "off" { Button "" -> $onClose class("cx-overlay") aria(label: "Cerrar", hidden: "true") data(closes: "true") }
|
|
16
|
+
when $dismiss == "off" { Stack class("cx-overlay") aria(hidden: "true") }
|
|
17
|
+
Stack class("cx-modal") data(mode: $mode, size: $size, side: $side) aria(role: $role, modal: "true", label: $title) {
|
|
18
|
+
Stack class("cx-handle") aria(hidden: "true")
|
|
19
|
+
Stack class("cx-modal-head") data(drag: "true") {
|
|
20
|
+
Span "{$title}" class("cx-modal-title")
|
|
21
|
+
Span "{$description}" class("cx-modal-desc")
|
|
22
|
+
}
|
|
23
|
+
when $dismiss != "off" { Button -> $onClose class("cn-button cn-button-variant-ghost cn-button-size-icon-sm group/button cx-modal-x") aria(label: "Cerrar") { Icon "lucide:x" } }
|
|
24
|
+
Stack class("cx-modal-body") { slot }
|
|
25
|
+
ModalBehavior(open: $open, onClose: $onClose)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
part ModalFooter() { Stack class("cx-modal-foot") data(slot: "modal-footer") { slot } }
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// MoneyField (muten Custom) - a price with its currency (kit/values.js moneyField).
|
|
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));
|
|
4
|
+
return () => {};
|
|
5
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# MoneyField - a price with its currency: the symbol, separators and decimals come from the currency (Intl); the
|
|
2
|
+
# currency is picked from a searchable list; under it, what is stored (minor units). onChange gets {amount, minor, currency}.
|
|
3
|
+
# MoneyField(label: "Precio del servicio", value: 25000, currency: "COP", onChange: setPrice)
|
|
4
|
+
# shadcn-for-muten · Custom (kit values) · host JS money-field.js.
|
|
5
|
+
part MoneyField(label: text = "Precio", value: number = 0, currency: text = "COP", onChange: action) {
|
|
6
|
+
Custom MoneyField inputs(label: $label, value: $value, currency: $currency) on(change: $onChange) class("cx-mf-host")
|
|
7
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# NavButton - a place in the app (a tab bar, a sidebar, a segmented header). Where you are is a STATE, not a
|
|
2
|
+
# variant: it rides aria-pressed and is marked by `marker`; everything else is a Btn (variant, size, any content in
|
|
3
|
+
# the slot: an icon, a count). Lay them in a NavBar, across or down.
|
|
4
|
+
# NavBar(label: "Secciones") { NavButton(label: "Mi tienda", current: atShop, onClick: goShop) … }
|
|
5
|
+
# NavBar(label: "Menú", orientation: "vertical") { NavButton(label: "Agenda", current: atAgenda, onClick: goAgenda, marker: "fill") { Icon "lucide:calendar" } }
|
|
6
|
+
# marker: dot (soft fill + a dot, the default) | fill (soft fill) | line (an underline) | none (only the text firms up)
|
|
7
|
+
# variant / size: as Btn. shadcn-for-muten · part · no deps · kit.css (.cx-nav, .cx-navbar).
|
|
8
|
+
part NavButton(label: text = "", current: bool, onClick: action, variant: text = "ghost", size: text = "default", marker: text = "dot") {
|
|
9
|
+
Button -> $onClick class("cn-button cn-button-variant-{$variant} cn-button-size-{$size} group/button cx-nav") data(slot: "button", marker: $marker) aria(pressed: $current) {
|
|
10
|
+
slot
|
|
11
|
+
Span "{$label}" class("cx-nav-label")
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
part NavBar(label: text = "Secciones", orientation: text = "horizontal") { Nav class("cx-navbar") data(orientation: $orientation) aria(label: $label) { slot } }
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
#
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
part
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
Span "{$
|
|
1
|
+
# NavigationMenu - a site's top navigation: words that open a panel of links on hover (or tap, or focus) and plain
|
|
2
|
+
# links. Once a panel is open, resting on the next word switches to its panel.
|
|
3
|
+
# NavigationMenu(label: "Principal") {
|
|
4
|
+
# NavMenuItem { NavMenuTrigger(label: "Producto") NavMenuContent { NavMenuLink(title: "Agenda", description: "…", to: "/agenda") … } }
|
|
5
|
+
# NavMenuItemLink(label: "Precios", to: "/precios")
|
|
6
|
+
# }
|
|
7
|
+
# shadcn-for-muten · parts + Custom (OverlayBehavior) · Nova (.cn-navigation-menu-*) + kit.css.
|
|
8
|
+
part NavigationMenu(label: text = "Principal") { Nav class("cn-navigation-menu cx-navmenu") data(slot: "navigation-menu") aria(label: $label) { slot } }
|
|
9
|
+
part NavMenuItem() { Stack class("cx-ov") data(slot: "navigation-menu-item") { slot OverlayBehavior(kind: "navmenu") } }
|
|
10
|
+
part NavMenuTrigger(label: text) { Stack class("cx-ov-trigger") { Button class("cn-navigation-menu-trigger cx-navmenu-trigger") data(slot: "navigation-menu-trigger") { Span "{$label}" Icon "lucide:chevron-down" class("cn-navigation-menu-trigger-icon") } } }
|
|
11
|
+
part NavMenuContent(width: text = "26rem", columns: number = 2) { Stack class("cn-navigation-menu-content cx-ov-content cx-navmenu-panel") data(slot: "navigation-menu-content", align: "start") style(w: "{$width}", cols: "{$columns}") { slot } }
|
|
12
|
+
part NavMenuLink(title: text, to: text, description: text = "") {
|
|
13
|
+
Link -> "{$to}" class("cn-navigation-menu-link cx-navmenu-link") data(slot: "navigation-menu-link") {
|
|
14
|
+
Span "{$title}" class("cx-navmenu-title")
|
|
15
|
+
Span "{$description}" class("cx-navmenu-desc")
|
|
15
16
|
}
|
|
16
17
|
}
|
|
18
|
+
part NavMenuItemLink(label: text, to: text) { Link "{$label}" -> "{$to}" class("cn-navigation-menu-trigger cx-navmenu-trigger") data(slot: "navigation-menu-link") }
|
package/registry/navpill.muten
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
# NavPill -
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
# Stack class("
|
|
6
|
-
# Stack class("flex flex-row items-center gap-6 text-sm text-muted-foreground max-md:hidden") { Link "Features" -> "/" class("link-underline") Link "Pricing" -> "/" class("link-underline") }
|
|
7
|
-
# Link "Get started" -> "/" class("btn btn-default btn-sm press")
|
|
1
|
+
# NavPill - a landing page's floating glass bar: it stays on top as you scroll, firming up (more opaque, a shadow)
|
|
2
|
+
# once the page moves. The logo on the left, links and the call to action on the right (they lay out apart).
|
|
3
|
+
# NavPill(label: "Principal") {
|
|
4
|
+
# Stack class("cx-hstack") { Icon "lucide:store" Span "Storio" class("font-semibold") }
|
|
5
|
+
# Stack class("cx-hstack") { Link "Precios" -> "/precios" LinkBtn(to: "/signup", size: "sm") { Span "Crear mi tienda" } }
|
|
8
6
|
# }
|
|
9
|
-
#
|
|
10
|
-
|
|
7
|
+
# look "tray": a tray in the muted fill at rest that turns to glass once the page moves; the logo and NavLinks sit
|
|
8
|
+
# together on the left, a Span class("cx-grow") pushes the rest right, and the call to action (class "cx-nav-cta")
|
|
9
|
+
# turns primary as you scroll. On a phone NavLinks and anything with class "cx-nav-hide-sm" fold away and a Btn with
|
|
10
|
+
# class "cx-nav-menu" shows instead.
|
|
11
|
+
# NavPill(look: "tray") { Image "~/logo.svg" NavLinks { Link "Precios" -> "/pricing" } Span class("cx-grow")
|
|
12
|
+
# Link "Entrar" -> "/login" class("cx-nav-hide-sm") LinkBtn(to: "/signup") class("cx-nav-cta") { Span "Crear mi tienda" }
|
|
13
|
+
# Btn(variant: "ghost", size: "icon", label: "Menú", onClick: openMenu) class("cx-nav-menu") { Icon "lucide:menu" } }
|
|
14
|
+
# shadcn-for-muten · part · no deps · kit.css (.cx-navpill, scroll-driven where the browser supports it).
|
|
15
|
+
part NavPill(label: text = "Principal", look: text = "") { Stack class("cx-navpill-wrap") data(look: $look) { Nav class("cx-navpill") data(look: $look) aria(label: $label) { slot } } }
|
|
16
|
+
part NavLinks() { Stack class("cx-nav-links") { slot } }
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// NewAppointment (muten Custom) - the kit's 4-step «Nueva cita» (kit/structure.js) in the adaptive modal. It opens
|
|
2
|
+
// when `open` turns true; closing it in any way fires close, creating fires create(json {client, service, day, hour}).
|
|
3
|
+
export function mount(el, inputs, on) {
|
|
4
|
+
let wiz = null, was = false;
|
|
5
|
+
const sync = (open) => { if (open && !was) wiz?.start(); was = !!open; };
|
|
6
|
+
Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/structure.js')]).then(([core, s]) => { wiz = s.wizard(core, inputs, on); was = false; sync(inputs.open); });
|
|
7
|
+
return (next) => sync(next.open);
|
|
8
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# NewAppointment - «Nueva cita» in 4 steps inside the adaptive modal, built only from atoms: a Stepper under the
|
|
2
|
+
# title, the client (search + choice list), the service (choice list with price), the day (tiles) and the hour, and
|
|
3
|
+
# the summary. «Siguiente» stays disabled until the step is answered; steps slide sideways and the height eases.
|
|
4
|
+
# The page owns `open` (true opens it); onClose fires however it closes, onCreate gets json {client, service, day, hour}.
|
|
5
|
+
# NewAppointment(open: creating, clients: @clients, services: @services, hours: @slots, closed: @closedDays, note: "Le llega la confirmación por WhatsApp.", onCreate: created, onClose: closeNew)
|
|
6
|
+
# clients {name, description} · services {name, duration, price, icon} · hours ["09:00", …] · closed [0] (weekdays).
|
|
7
|
+
# shadcn-for-muten · Custom (kit/structure.js) · the kit's adaptive modal.
|
|
8
|
+
part NewAppointment(open: bool, title: text = "Nueva cita", clients: list, services: list, hours: list, closed: list, note: text = "", onCreate: action, onClose: action) {
|
|
9
|
+
Custom NewAppointment inputs(open: $open, title: $title, clients: $clients, services: $services, hours: $hours, closed: $closed, note: $note) on(create: $onCreate, close: $onClose) class("cx-wizard-host")
|
|
10
|
+
}
|
package/registry/number-field.js
CHANGED
|
@@ -1,30 +1,40 @@
|
|
|
1
|
-
// NumberField
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
// NumberField (muten Custom) - a number with − and + inside one Nova InputGroup. inputs: value, min, max, step,
|
|
2
|
+
// prefix (a short label inside, like «X»), unit (after the number, like «min»), label (accessible name).
|
|
3
|
+
// handlers: input(value). Clamps to [min, max]; buttons, arrow keys and the wheel (while focused) step; holding a
|
|
4
|
+
// button repeats. The buttons switch off at the ends.
|
|
5
|
+
const ICON = { minus: '<path d="M5 12h14"/>', plus: '<path d="M5 12h14"/><path d="M12 5v14"/>' };
|
|
6
|
+
const svg = (k) => `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${ICON[k]}</svg>`;
|
|
7
|
+
export function mount(el, inputs, on) {
|
|
5
8
|
const num = (v, d) => { const n = Number(v); return Number.isFinite(n) ? n : d; };
|
|
6
9
|
let min = num(inputs.min, -Infinity), max = num(inputs.max, Infinity), step = num(inputs.step, 1) || 1;
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
const esc = (s) => String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]);
|
|
11
|
+
const btn = (k, name) => `<button type="button" data-slot="button" class="cn-button cn-button-variant-ghost cn-button-size-icon-xs group/button cx-nf-btn" data-step="${k}" aria-label="${name}">${svg(k)}</button>`;
|
|
12
|
+
el.innerHTML = `<div data-slot="input-group" role="group" class="cn-input-group group/input-group cx-nf">
|
|
13
|
+
${inputs.prefix ? `<div data-slot="input-group-addon" class="cn-input-group-addon cn-input-group-addon-align-inline-start cx-nf-prefix">${esc(inputs.prefix)}</div>` : ''}
|
|
14
|
+
<input data-slot="input-group-control" class="cn-input cn-input-group-input cx-num cx-nf-input" inputmode="decimal" aria-label="${esc(inputs.label || inputs.prefix || 'Número')}" role="spinbutton">
|
|
15
|
+
${inputs.unit ? `<div data-slot="input-group-addon" class="cn-input-group-addon cn-input-group-addon-align-inline-end cx-nf-unit">${esc(inputs.unit)}</div>` : ''}
|
|
16
|
+
<div data-slot="input-group-addon" class="cn-input-group-addon cn-input-group-addon-align-inline-end cx-nf-steps">${btn('minus', 'Menos')}${btn('plus', 'Más')}</div>
|
|
17
|
+
</div>`;
|
|
18
|
+
const input = el.querySelector('input'), dec = el.querySelector('[data-step="minus"]'), inc = el.querySelector('[data-step="plus"]');
|
|
16
19
|
const clamp = (v) => Math.min(max, Math.max(min, v));
|
|
20
|
+
const decimals = (String(step).split('.')[1] || '').length;
|
|
17
21
|
let value = clamp(num(inputs.value, 0));
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
input.addEventListener('change', () => emit(num(input.value, value)));
|
|
24
|
-
input.addEventListener('wheel', (e) => { if (document.activeElement === input) { e.preventDefault(); emit(value + (e.deltaY < 0 ? step : -step)); } }, { passive: false });
|
|
25
|
-
|
|
26
|
-
return (n) => {
|
|
27
|
-
min = num(n.min, min); max = num(n.max, max); step = num(n.step, step) || step;
|
|
28
|
-
value = clamp(num(n.value, value)); input.value = String(value);
|
|
22
|
+
const paint = () => {
|
|
23
|
+
input.value = decimals ? value.toFixed(decimals) : String(value);
|
|
24
|
+
input.setAttribute('aria-valuenow', value);
|
|
25
|
+
if (Number.isFinite(min)) input.setAttribute('aria-valuemin', min); if (Number.isFinite(max)) input.setAttribute('aria-valuemax', max);
|
|
26
|
+
dec.disabled = value <= min; inc.disabled = value >= max;
|
|
29
27
|
};
|
|
28
|
+
const emit = (v) => { const next = clamp(Math.round(v / step) * step); if (next === value) { paint(); return; } value = next; paint(); on.input?.(value); };
|
|
29
|
+
// holding a button repeats, slowly first and then faster
|
|
30
|
+
let hold = 0;
|
|
31
|
+
const press = (d) => { emit(value + d * step); let wait = 380; const again = () => { hold = setTimeout(() => { emit(value + d * step); wait = Math.max(50, wait * 0.7); again(); }, wait); }; again(); };
|
|
32
|
+
const release = () => clearTimeout(hold);
|
|
33
|
+
[[dec, -1], [inc, 1]].forEach(([b, d]) => { b.addEventListener('pointerdown', (e) => { if (e.button > 0) return; e.preventDefault(); press(d); }); b.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); emit(value + d * step); } }); });
|
|
34
|
+
addEventListener('pointerup', release); addEventListener('pointercancel', release);
|
|
35
|
+
input.addEventListener('change', () => emit(num(input.value.replace(',', '.'), value)));
|
|
36
|
+
input.addEventListener('keydown', (e) => { const d = { ArrowUp: 1, ArrowDown: -1 }[e.key]; if (d) { e.preventDefault(); emit(value + d * step * (e.shiftKey ? 10 : 1)); } });
|
|
37
|
+
input.addEventListener('wheel', (e) => { if (document.activeElement !== input) return; e.preventDefault(); emit(value + (e.deltaY < 0 ? step : -step)); }, { passive: false });
|
|
38
|
+
paint();
|
|
39
|
+
return (n) => { min = num(n.min, min); max = num(n.max, max); step = num(n.step, step) || step; const v = clamp(num(n.value, value)); if (v !== value) { value = v; paint(); } };
|
|
30
40
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
# NumberField - a
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
# NumberField - a number you nudge: − and + inside one field, arrows (Shift = ×10), the wheel while focused, and a
|
|
2
|
+
# held button that repeats. Clamped to [min, max]; the button at an end switches off. The page owns the value.
|
|
3
|
+
# state { minutes = 30 : number } action setMinutes(v: number) mutates minutes { minutes.set(v) }
|
|
4
|
+
# NumberField(value: minutes, min: 5, max: 240, step: 5, unit: "min", label: "Duración", onInput: setMinutes)
|
|
5
|
+
# NumberField(value: x, min: 0, max: 1920, prefix: "X", onInput: setX)
|
|
6
|
+
# shadcn-for-muten · Custom · Nova (.cn-input-group) + kit.css (.cx-nf) · host JS number-field.js.
|
|
7
|
+
part NumberField(value: number, min: number, max: number, step: number = 1, prefix: text = "", unit: text = "", label: text = "", onInput: action) {
|
|
8
|
+
Custom NumberField inputs(value: $value, min: $min, max: $max, step: $step, prefix: $prefix, unit: $unit, label: $label) on(input: $onInput) class("cx-nf-host")
|
|
7
9
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// OrderBoard / OrderCards (muten Custom) - the kit's order Board and card (kit/board.js). inputs: lanes, cards, sizes;
|
|
2
|
+
// handlers change(json [{id, lane}]) · action(json {id, action}).
|
|
3
|
+
export function mount(el, inputs, on) {
|
|
4
|
+
Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/board.js')]).then(([core, b]) => b.orderBoard(el, inputs, core, on));
|
|
5
|
+
return () => {};
|
|
6
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# OrderBoard - lanes and cards are data. Each card groups its information by what it is about and orders it by what
|
|
2
|
+
# needs you first: who and how much (the contact channel they asked for on the right), what waits for your decision
|
|
3
|
+
# (tonal icon Buttons with their Indicator; tapping opens it in the modal), the products, the payment (total, short
|
|
4
|
+
# state, and the button that fits: «Revisar comprobante», «Ver recibo de Stripe»), the delivery (time, progress,
|
|
5
|
+
# «Ver entrega»), the note, and the one next step. Tapping a button never starts a drag. Drag a card with the mouse;
|
|
6
|
+
# on a phone hold it a moment. Drag the board's background (or swipe) to see the side lanes; carrying a card to the
|
|
7
|
+
# edge scrolls it. Keyboard: Space lifts, arrows move, Space drops, Esc puts it back.
|
|
8
|
+
# OrderBoard(lanes: @lanes, cards: @orders, onChange: moved, onAction: act)
|
|
9
|
+
# OrderCards(cards: @orders, sizes: @sizes, onAction: act) the same card side by side (minimum · typical · maximum)
|
|
10
|
+
# lanes {id, title, next} (next = the step's label that moves a card INTO the following lane) · cards {id, lane, who,
|
|
11
|
+
# sub, total, contact, status [tone, label], request {tone, icon, title, when, text, photo, actions [[id, label]]} or a
|
|
12
|
+
# list of them, order {count, photos, extras}, pay {method, tone, label, detail}, delivery {mode, label, when, map
|
|
13
|
+
# {address, detail, route}, track {steps, at, courier, vehicle, eta}}, note, actions [["advance", label]]}
|
|
14
|
+
# · sizes {title, description, card}. Photos are CSS backgrounds (a gradient or url()).
|
|
15
|
+
# shadcn-for-muten · Custom (kit/board.js) · kit.css (.cx-board, .cx-bcard, .cx-card-sizes).
|
|
16
|
+
part OrderBoard(lanes: list, cards: list, onChange: action, onAction: action) {
|
|
17
|
+
Custom OrderBoard inputs(lanes: $lanes, cards: $cards) on(change: $onChange, action: $onAction) class("cx-board-host")
|
|
18
|
+
}
|
|
19
|
+
part OrderCards(cards: list, sizes: list, onAction: action) {
|
|
20
|
+
Custom OrderBoard inputs(cards: $cards, sizes: $sizes) on(action: $onAction) class("cx-board-host")
|
|
21
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// OverlayBehavior (muten Custom) - opens and closes the floating panel of the part it sits in (kit/overlays.js).
|
|
2
|
+
// inputs: kind popover | hover | menu | context | menubar | navmenu. Used by Popover, HoverCard, DropdownMenu,
|
|
3
|
+
// ContextMenu, Menubar and NavigationMenu; you never call it yourself.
|
|
4
|
+
export function mount(el, inputs) {
|
|
5
|
+
const wrap = el.parentElement, panel = wrap?.querySelector(':scope > .cx-ov-content');
|
|
6
|
+
if (panel) panel.hidden = true; // closed until the kit takes over (no flash of an open panel)
|
|
7
|
+
Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/overlays.js')]).then(([core, o]) => {
|
|
8
|
+
o.overlay(el, inputs.kind || 'popover', core);
|
|
9
|
+
// menu items are reached with the arrows, not the Tab key
|
|
10
|
+
wrap.querySelectorAll(':scope > .cx-ov-content [role^="menuitem"]').forEach((m) => { m.tabIndex = -1; });
|
|
11
|
+
});
|
|
12
|
+
return () => {};
|
|
13
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# OverlayBehavior - the opening, placing and closing of a floating panel; the floating parts (Popover, HoverCard,
|
|
2
|
+
# DropdownMenu, ContextMenu, Menubar, NavigationMenu) carry it. kind: popover | hover | menu | context | menubar | navmenu.
|
|
3
|
+
# shadcn-for-muten · Custom · host JS overlay-behavior.js (kit/overlays.js).
|
|
4
|
+
part OverlayBehavior(kind: text = "popover") { Custom OverlayBehavior inputs(kind: $kind) class("cx-ov-behavior") }
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// PaginationNav (muten Custom) - server pages' nav (kit/collections.js paginationNav): 7 fixed slots, range line.
|
|
2
|
+
export function mount(el, inputs, on) {
|
|
3
|
+
let update = null;
|
|
4
|
+
Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/collections.js')]).then(([core, c]) => { update = c.paginationNav(el, inputs, core, on); });
|
|
5
|
+
return (next) => update?.(next);
|
|
6
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# PaginationNav - pages that come from the server: always 7 slots once there are more than 7 pages (nothing slides),
|
|
2
|
+
# «Página n de N» on a phone, and «41–50 de 237 clientes». The page owns the page number and loads the rows; while it
|
|
3
|
+
# loads, give the list class("cx-pg-list") data(loading: busy) so the old page dims in place (nothing changes size).
|
|
4
|
+
# PaginationNav(total: 237, per: 10, page: page, noun: "clientes", onPage: loadPage)
|
|
5
|
+
# shadcn-for-muten · Custom (kit collections) · host JS pagination-nav.js.
|
|
6
|
+
part PaginationNav(total: number, per: number = 10, page: number, noun: text = "resultados", onPage: action) {
|
|
7
|
+
Custom PaginationNav inputs(total: $total, per: $per, page: $page, noun: $noun) on(page: $onPage) class("cx-pgnav-host")
|
|
8
|
+
}
|