@muten/shadcn 0.0.5 → 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.
Files changed (213) hide show
  1. package/AGENTS.md +61 -6
  2. package/README.md +57 -4
  3. package/globals.css +30 -2
  4. package/kit.css +2763 -0
  5. package/motion.css +79 -0
  6. package/nova.css +1387 -0
  7. package/package.json +10 -2
  8. package/registry/accordion.muten +11 -10
  9. package/registry/agenda.js +35 -0
  10. package/registry/agenda.muten +10 -0
  11. package/registry/alert-dialog.muten +9 -12
  12. package/registry/alert.muten +8 -8
  13. package/registry/app-shell.js +9 -0
  14. package/registry/app-shell.muten +12 -0
  15. package/registry/attachment.muten +13 -9
  16. package/registry/avatar.muten +24 -5
  17. package/registry/badge.muten +23 -8
  18. package/registry/band.muten +16 -0
  19. package/registry/bar-chart.muten +7 -0
  20. package/registry/bell.muten +3 -0
  21. package/registry/bento.muten +6 -9
  22. package/registry/booking.js +21 -0
  23. package/registry/booking.muten +13 -0
  24. package/registry/breadcrumb.muten +9 -7
  25. package/registry/button-group.muten +13 -4
  26. package/registry/button.muten +21 -8
  27. package/registry/calendar.js +28 -93
  28. package/registry/calendar.muten +9 -8
  29. package/registry/canvas-viewport.js +12 -5
  30. package/registry/canvas-viewport.muten +9 -9
  31. package/registry/card-field.muten +4 -0
  32. package/registry/card.muten +12 -8
  33. package/registry/carousel.js +5 -36
  34. package/registry/carousel.muten +12 -5
  35. package/registry/chatbot.muten +9 -0
  36. package/registry/checklist.muten +6 -0
  37. package/registry/choice-list.muten +16 -0
  38. package/registry/code.js +14 -12
  39. package/registry/code.muten +6 -7
  40. package/registry/collapsible.muten +14 -10
  41. package/registry/color-picker.js +460 -41
  42. package/registry/color-picker.muten +7 -6
  43. package/registry/combobox.js +5 -0
  44. package/registry/combobox.muten +6 -23
  45. package/registry/command-palette.muten +8 -12
  46. package/registry/command.js +32 -0
  47. package/registry/command.muten +32 -11
  48. package/registry/context-menu.muten +9 -8
  49. package/registry/count-up.muten +5 -10
  50. package/registry/date-field.js +5 -0
  51. package/registry/date-field.muten +7 -0
  52. package/registry/device-frame.muten +8 -0
  53. package/registry/dialog.muten +6 -12
  54. package/registry/dirty-bar.js +19 -0
  55. package/registry/dirty-bar.muten +13 -0
  56. package/registry/drawer.muten +6 -11
  57. package/registry/dropdown-menu.muten +37 -13
  58. package/registry/empty.muten +11 -7
  59. package/registry/feature-grid.muten +10 -12
  60. package/registry/feature-rows.muten +12 -8
  61. package/registry/field-behavior.js +33 -0
  62. package/registry/field-behavior.muten +9 -0
  63. package/registry/field.muten +26 -7
  64. package/registry/file-input.js +36 -21
  65. package/registry/file-input.muten +8 -7
  66. package/registry/fixed-card.muten +25 -0
  67. package/registry/folder-picker.js +13 -15
  68. package/registry/folder-picker.muten +5 -8
  69. package/registry/gradient-blinds.muten +6 -13
  70. package/registry/hover-card.muten +7 -9
  71. package/registry/image-upload.js +5 -0
  72. package/registry/image-upload.muten +8 -0
  73. package/registry/indicator.muten +5 -0
  74. package/registry/input-group.muten +27 -4
  75. package/registry/input-otp.js +8 -35
  76. package/registry/input-otp.muten +6 -6
  77. package/registry/input.muten +8 -7
  78. package/registry/inspector.muten +13 -15
  79. package/registry/island.js +7 -0
  80. package/registry/island.muten +5 -0
  81. package/registry/item.muten +20 -7
  82. package/registry/kbd.muten +3 -3
  83. package/registry/kit/board.js +300 -0
  84. package/registry/kit/collections.js +148 -0
  85. package/registry/kit/core.js +989 -0
  86. package/registry/kit/dates.js +106 -0
  87. package/registry/kit/menus.js +90 -0
  88. package/registry/kit/overlays.js +114 -0
  89. package/registry/kit/paint.js +211 -0
  90. package/registry/kit/qrcode.js +2291 -0
  91. package/registry/kit/structure.js +393 -0
  92. package/registry/kit/table.js +198 -0
  93. package/registry/kit/values.js +179 -0
  94. package/registry/kit-layer.js +2 -0
  95. package/registry/kit-layer.muten +5 -0
  96. package/registry/kit-skeleton.js +24 -0
  97. package/registry/kit-skeleton.muten +16 -0
  98. package/registry/kit-widget.js +79 -0
  99. package/registry/kit-widget.muten +4 -0
  100. package/registry/link-btn.muten +9 -7
  101. package/registry/live-stack.js +29 -0
  102. package/registry/live-stack.muten +7 -0
  103. package/registry/logo-cloud.muten +6 -11
  104. package/registry/member-list.muten +6 -0
  105. package/registry/menubar.muten +10 -14
  106. package/registry/message-scroller.js +12 -0
  107. package/registry/message-scroller.muten +8 -4
  108. package/registry/message.muten +27 -10
  109. package/registry/messaging.muten +11 -0
  110. package/registry/modal-behavior.js +56 -0
  111. package/registry/modal-behavior.muten +4 -0
  112. package/registry/modal.muten +29 -0
  113. package/registry/money-field.js +5 -0
  114. package/registry/money-field.muten +7 -0
  115. package/registry/nav-button.muten +14 -0
  116. package/registry/navigation-menu.muten +16 -14
  117. package/registry/navpill.muten +15 -9
  118. package/registry/new-appointment.js +8 -0
  119. package/registry/new-appointment.muten +10 -0
  120. package/registry/number-field.js +34 -24
  121. package/registry/number-field.muten +8 -6
  122. package/registry/order-board.js +6 -0
  123. package/registry/order-board.muten +21 -0
  124. package/registry/overlay-behavior.js +13 -0
  125. package/registry/overlay-behavior.muten +4 -0
  126. package/registry/pagination-nav.js +6 -0
  127. package/registry/pagination-nav.muten +8 -0
  128. package/registry/pagination.muten +12 -9
  129. package/registry/password-field.muten +3 -0
  130. package/registry/permission-matrix.muten +6 -0
  131. package/registry/phone-field.js +5 -0
  132. package/registry/phone-field.muten +7 -0
  133. package/registry/phone-shell.js +7 -0
  134. package/registry/phone-shell.muten +9 -0
  135. package/registry/pill.muten +6 -5
  136. package/registry/plans.muten +4 -0
  137. package/registry/popover.muten +16 -9
  138. package/registry/pricing.muten +21 -21
  139. package/registry/progress.muten +5 -5
  140. package/registry/queue.muten +3 -0
  141. package/registry/radio-group.muten +16 -8
  142. package/registry/range-slider.js +9 -24
  143. package/registry/range-slider.muten +4 -5
  144. package/registry/resizable.js +34 -27
  145. package/registry/resizable.muten +16 -5
  146. package/registry/roving-behavior.js +16 -0
  147. package/registry/roving-behavior.muten +3 -0
  148. package/registry/row-menu.js +5 -0
  149. package/registry/row-menu.muten +5 -0
  150. package/registry/save-button.muten +11 -0
  151. package/registry/scale-slider.js +7 -0
  152. package/registry/scale-slider.muten +9 -0
  153. package/registry/scroll-area.muten +8 -4
  154. package/registry/segmented-control.muten +8 -8
  155. package/registry/separator.muten +3 -3
  156. package/registry/share-button.js +7 -0
  157. package/registry/share-button.muten +10 -0
  158. package/registry/sheet.muten +6 -10
  159. package/registry/sidebar.muten +23 -15
  160. package/registry/skeleton.muten +6 -4
  161. package/registry/slider.js +73 -27
  162. package/registry/slider.muten +6 -6
  163. package/registry/sortable-behavior.js +43 -0
  164. package/registry/sortable-behavior.muten +2 -0
  165. package/registry/sortable-list.js +10 -26
  166. package/registry/sortable-list.muten +6 -7
  167. package/registry/sortable.muten +15 -0
  168. package/registry/spark.js +14 -0
  169. package/registry/spark.muten +3 -0
  170. package/registry/spinner.muten +4 -3
  171. package/registry/split-button.muten +12 -10
  172. package/registry/stamp-card.muten +7 -0
  173. package/registry/stat-band.muten +6 -9
  174. package/registry/stat.muten +23 -0
  175. package/registry/status-bar.muten +7 -6
  176. package/registry/stepper.muten +17 -14
  177. package/registry/store-qr.js +5 -0
  178. package/registry/store-qr.muten +5 -0
  179. package/registry/swatch.muten +8 -8
  180. package/registry/switch.muten +14 -6
  181. package/registry/table-view.muten +10 -0
  182. package/registry/tabs-behavior.js +27 -0
  183. package/registry/tabs-behavior.muten +3 -0
  184. package/registry/tabs.muten +19 -10
  185. package/registry/tag-input.js +4 -27
  186. package/registry/tag-input.muten +6 -6
  187. package/registry/text.muten +7 -0
  188. package/registry/time-field.js +5 -0
  189. package/registry/time-field.muten +6 -0
  190. package/registry/timeline.muten +14 -14
  191. package/registry/toaster.js +34 -22
  192. package/registry/toaster.muten +10 -8
  193. package/registry/toggle-group.muten +11 -6
  194. package/registry/toggle.muten +8 -5
  195. package/registry/toolbar.muten +11 -6
  196. package/registry/tooltip.muten +6 -9
  197. package/registry/tree-view.js +27 -7
  198. package/registry/tree-view.muten +8 -13
  199. package/registry/vector-field.muten +9 -8
  200. package/registry/virtual-list.js +5 -0
  201. package/registry/virtual-list.muten +6 -0
  202. package/registry/weekly-hours.muten +4 -0
  203. package/registry/wordline.muten +6 -0
  204. package/registry/zoom-control.muten +9 -9
  205. package/registry.json +868 -424
  206. package/themes/bloom.css +53 -0
  207. package/themes/editorial.css +65 -0
  208. package/themes/noir.css +54 -0
  209. package/registry/chart.js +0 -34
  210. package/registry/chart.muten +0 -7
  211. package/registry/checkbox.muten +0 -8
  212. package/registry/context-menu.js +0 -34
  213. package/registry/select.muten +0 -21
@@ -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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[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 "trusted by" strip: a wrapping row of logos, muted + grayscale until hover. GENERIC - a
2
- # LogoCloudItem holds ANY logo type: an Image, an Icon, or a plain text wordmark (Span). All normalize to one
3
- # height. Give it a heading above via a plain Text.
4
- # LogoCloud {
5
- # LogoCloudItem { Image "~/logos/acme.svg" alt("Acme") } # image logo
6
- # LogoCloudItem { Icon "simple-icons:vercel" } # icon logo
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
+ }
@@ -1,14 +1,10 @@
1
- # Menubar - a bar of menus (File / Edit / View). The PAGE owns which is open via a text state:
2
- # state { bar = "" : text } action open(v: text) mutates bar { if bar == v { bar.set("") } else { bar.set(v) } }
3
- # action close mutates bar { bar.set("") }
4
- # Menubar { MenubarMenu(label: "File", name: "file", current: bar, onOpen: open) { MenubarItem(label: "New", onSelect: close) … } }
5
- # shadcn-for-muten · part · no deps · styles via the semantic .menubar* classes (items reuse .dropdown-item).
6
- part Menubar() { Stack class("menubar") { slot } }
7
- part MenubarMenu(label: text, name: text, current: text, onOpen: action) {
8
- Stack class("menubar-item") {
9
- Button "{$label}" -> $onOpen($name) aria(expanded: $name == $current) class("menubar-trigger", "menubar-trigger-active" when $name == $current)
10
- Stack class("menubar-content", "menubar-content-open" when $name == $current) { slot }
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
- # Message Scroller - a scrollable column of chat messages (max-h-96 by default; eject to change).
2
- # MessageScroller { Marker(label: "Today") Message(variant: "received") { … } Message(variant: "sent") { … } }
3
- # shadcn-for-muten · part · no deps · styles via the semantic .message-scroller class in globals.css.
4
- part MessageScroller() { Stack class("message-scroller") { slot } }
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") }
@@ -1,11 +1,28 @@
1
- # Message + Bubble + Marker - chat primitives. variant: sent | received.
2
- # Compose: Message(variant: "received") { Avatar(name) Bubble(variant: "received") { Span "Hi!" } }
3
- # Marker(label: "Today")
4
- # shadcn-for-muten · part · no deps · styles via the semantic .message* / .bubble / .marker classes.
5
- part Message(variant: text) {
6
- Stack class("message", "message-sent" when $variant == "sent") { slot }
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
- Stack class("bubble", "bubble-sent" when $variant == "sent", "bubble-received" when $variant == "received") { slot }
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
- # Navigation Menu - a nav bar whose items reveal a panel on hover (pure CSS, no state).
2
- # NavigationMenu { NavigationMenuItem(label: "Getting started") { NavigationMenuLink(title: "Intro", desc: "…", href: "/intro") … } }
3
- # shadcn-for-muten · part · no deps · styles via the semantic .nav-menu* classes in globals.css.
4
- part NavigationMenu() { Stack class("nav-menu") { slot } }
5
- part NavigationMenuItem(label: text) {
6
- Stack class("nav-menu-item") {
7
- Span "{$label}" class("nav-menu-trigger")
8
- Stack class("nav-menu-content") { slot }
9
- }
10
- }
11
- part NavigationMenuLink(title: text, desc: text, href: text) {
12
- Link "" -> "{$href}" class("nav-menu-link") {
13
- Span "{$title}" class("nav-menu-link-title")
14
- Span "{$desc}" class("nav-menu-link-text")
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") }
@@ -1,10 +1,16 @@
1
- # NavPill - the floating glass navbar, batteries included: a sticky + padded wrapper, the glass pill, AND the
2
- # scroll animation (settles + returns to rest at the top). Just drop your logo / links / CTA inside; it lays them
3
- # out with space-between (logo left, the rest right).
4
- # NavPill {
5
- # Stack class("flex flex-row items-center gap-2") { Icon "lucide:hexagon" class("size-5 text-primary") Span "Acme" class("font-bold") }
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
- # shadcn-for-muten · part · no deps · uses .nav-pill + nav-scroll from globals.css.
10
- part NavPill() { Stack class("sticky top-0 z-50 px-4 pt-4") { Header class("nav-pill nav-scroll mx-auto w-full max-w-5xl") { slot } } }
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
+ }
@@ -1,30 +1,40 @@
1
- // NumberField host (muten Custom). inputs: { value, min, max, step }. handlers: { input(value) }.
2
- // [-] <input type=number> [+] - clamps to [min,max], steps on buttons / arrows / wheel, emits on change.
3
- const ico = (p) => '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + p + '</svg>';
4
- export function mount(el, inputs, handlers) {
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 dec = document.createElement('button'); dec.type = 'button'; dec.className = 'number-field-btn'; dec.innerHTML = ico('<path d="M5 12h14"/>'); // lucide minus
9
- const input = document.createElement('input'); input.type = 'number'; input.className = 'number-field-input';
10
- const inc = document.createElement('button'); inc.type = 'button'; inc.className = 'number-field-btn'; inc.innerHTML = ico('<path d="M5 12h14"/><path d="M12 5v14"/>'); // lucide plus
11
- el.appendChild(dec); el.appendChild(input); el.appendChild(inc);
12
- if (Number.isFinite(min)) input.min = String(min);
13
- if (Number.isFinite(max)) input.max = String(max);
14
- input.step = String(step);
15
-
10
+ const esc = (s) => String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[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
- input.value = String(value);
19
-
20
- const emit = (v) => { value = clamp(v); input.value = String(value); if (handlers.input) handlers.input(value); };
21
- dec.addEventListener('click', () => emit(value - step));
22
- inc.addEventListener('click', () => emit(value + step));
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 numeric input with -/+ steppers. The PAGE owns the value (number) + the action:
2
- # state { size = 16 : number } action setSize(v: number) mutates size { size.set(v) }
3
- # NumberField(value: size, min: 1, max: 512, step: 1, onInput: setSize)
4
- # shadcn-for-muten · Custom · host JS is src/components/NumberField.js (copied by `muten add number-field`).
5
- part NumberField(value: number, min: number, max: number, step: number, onInput: action) {
6
- Custom NumberField inputs(value: $value, min: $min, max: $max, step: $step) on(input: $onInput) class("number-field")
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
+ }