@muten/shadcn 0.0.7 → 0.1.1

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 (205) hide show
  1. package/AGENTS.md +47 -0
  2. package/README.md +47 -0
  3. package/kit.css +2767 -0
  4. package/motion.css +31 -1
  5. package/nova.css +1387 -0
  6. package/package.json +6 -2
  7. package/registry/accordion.muten +11 -10
  8. package/registry/agenda.js +35 -0
  9. package/registry/agenda.muten +10 -0
  10. package/registry/alert-dialog.muten +9 -12
  11. package/registry/alert.muten +8 -8
  12. package/registry/app-shell.js +9 -0
  13. package/registry/app-shell.muten +12 -0
  14. package/registry/attachment.muten +13 -9
  15. package/registry/avatar.muten +24 -5
  16. package/registry/badge.muten +23 -8
  17. package/registry/band.muten +16 -0
  18. package/registry/bar-chart.muten +7 -0
  19. package/registry/bell.muten +3 -0
  20. package/registry/bento.muten +6 -9
  21. package/registry/booking.js +21 -0
  22. package/registry/booking.muten +13 -0
  23. package/registry/breadcrumb.muten +9 -7
  24. package/registry/button-group.muten +13 -4
  25. package/registry/button.muten +21 -8
  26. package/registry/calendar.js +28 -93
  27. package/registry/calendar.muten +9 -8
  28. package/registry/canvas-viewport.js +12 -5
  29. package/registry/canvas-viewport.muten +9 -9
  30. package/registry/card-field.muten +4 -0
  31. package/registry/card.muten +12 -8
  32. package/registry/carousel.js +5 -36
  33. package/registry/carousel.muten +12 -5
  34. package/registry/chatbot.muten +9 -0
  35. package/registry/checklist.muten +6 -0
  36. package/registry/choice-list.muten +16 -0
  37. package/registry/code.js +14 -12
  38. package/registry/code.muten +6 -7
  39. package/registry/collapsible.muten +14 -10
  40. package/registry/color-picker.js +460 -41
  41. package/registry/color-picker.muten +7 -6
  42. package/registry/combobox.js +5 -0
  43. package/registry/combobox.muten +6 -23
  44. package/registry/command-palette.muten +8 -12
  45. package/registry/command.js +32 -0
  46. package/registry/command.muten +32 -11
  47. package/registry/context-menu.muten +9 -8
  48. package/registry/count-up.muten +5 -10
  49. package/registry/date-field.js +5 -0
  50. package/registry/date-field.muten +7 -0
  51. package/registry/device-frame.muten +8 -0
  52. package/registry/dialog.muten +6 -12
  53. package/registry/dirty-bar.js +19 -0
  54. package/registry/dirty-bar.muten +13 -0
  55. package/registry/drawer.muten +6 -11
  56. package/registry/dropdown-menu.muten +37 -13
  57. package/registry/empty.muten +11 -7
  58. package/registry/feature-grid.muten +10 -12
  59. package/registry/feature-rows.muten +12 -8
  60. package/registry/field-behavior.js +33 -0
  61. package/registry/field-behavior.muten +9 -0
  62. package/registry/field.muten +26 -7
  63. package/registry/file-input.js +36 -21
  64. package/registry/file-input.muten +8 -7
  65. package/registry/fixed-card.muten +25 -0
  66. package/registry/folder-picker.js +13 -15
  67. package/registry/folder-picker.muten +5 -8
  68. package/registry/gradient-blinds.muten +6 -13
  69. package/registry/hover-card.muten +7 -9
  70. package/registry/image-upload.js +5 -0
  71. package/registry/image-upload.muten +8 -0
  72. package/registry/indicator.muten +5 -0
  73. package/registry/input-group.muten +27 -4
  74. package/registry/input-otp.js +8 -35
  75. package/registry/input-otp.muten +6 -6
  76. package/registry/input.muten +8 -7
  77. package/registry/inspector.muten +13 -15
  78. package/registry/island.js +7 -0
  79. package/registry/island.muten +5 -0
  80. package/registry/item.muten +20 -7
  81. package/registry/kbd.muten +3 -3
  82. package/registry/kit/board.js +300 -0
  83. package/registry/kit/collections.js +148 -0
  84. package/registry/kit/core.js +989 -0
  85. package/registry/kit/dates.js +106 -0
  86. package/registry/kit/menus.js +90 -0
  87. package/registry/kit/overlays.js +114 -0
  88. package/registry/kit/paint.js +211 -0
  89. package/registry/kit/qrcode.js +2291 -0
  90. package/registry/kit/structure.js +395 -0
  91. package/registry/kit/table.js +198 -0
  92. package/registry/kit/values.js +179 -0
  93. package/registry/kit-layer.js +2 -0
  94. package/registry/kit-layer.muten +5 -0
  95. package/registry/kit-skeleton.js +24 -0
  96. package/registry/kit-skeleton.muten +16 -0
  97. package/registry/kit-widget.js +79 -0
  98. package/registry/kit-widget.muten +4 -0
  99. package/registry/link-btn.muten +9 -7
  100. package/registry/live-stack.js +29 -0
  101. package/registry/live-stack.muten +7 -0
  102. package/registry/logo-cloud.muten +6 -11
  103. package/registry/member-list.muten +6 -0
  104. package/registry/menubar.muten +10 -14
  105. package/registry/message-scroller.js +12 -0
  106. package/registry/message-scroller.muten +8 -4
  107. package/registry/message.muten +27 -10
  108. package/registry/messaging.muten +11 -0
  109. package/registry/modal-behavior.js +56 -0
  110. package/registry/modal-behavior.muten +4 -0
  111. package/registry/modal.muten +29 -0
  112. package/registry/money-field.js +5 -0
  113. package/registry/money-field.muten +7 -0
  114. package/registry/nav-button.muten +14 -0
  115. package/registry/navigation-menu.muten +16 -14
  116. package/registry/navpill.muten +15 -9
  117. package/registry/new-appointment.js +8 -0
  118. package/registry/new-appointment.muten +10 -0
  119. package/registry/number-field.js +34 -24
  120. package/registry/number-field.muten +8 -6
  121. package/registry/order-board.js +6 -0
  122. package/registry/order-board.muten +21 -0
  123. package/registry/overlay-behavior.js +13 -0
  124. package/registry/overlay-behavior.muten +4 -0
  125. package/registry/pagination-nav.js +6 -0
  126. package/registry/pagination-nav.muten +8 -0
  127. package/registry/pagination.muten +12 -9
  128. package/registry/password-field.muten +3 -0
  129. package/registry/permission-matrix.muten +6 -0
  130. package/registry/phone-field.js +5 -0
  131. package/registry/phone-field.muten +7 -0
  132. package/registry/phone-shell.js +7 -0
  133. package/registry/phone-shell.muten +9 -0
  134. package/registry/pill.muten +6 -5
  135. package/registry/plans.muten +4 -0
  136. package/registry/popover.muten +16 -9
  137. package/registry/pricing.muten +21 -21
  138. package/registry/progress.muten +5 -5
  139. package/registry/queue.muten +3 -0
  140. package/registry/radio-group.muten +16 -8
  141. package/registry/range-slider.js +9 -24
  142. package/registry/range-slider.muten +4 -5
  143. package/registry/resizable.js +34 -27
  144. package/registry/resizable.muten +16 -5
  145. package/registry/roving-behavior.js +16 -0
  146. package/registry/roving-behavior.muten +3 -0
  147. package/registry/row-menu.js +5 -0
  148. package/registry/row-menu.muten +5 -0
  149. package/registry/save-button.muten +11 -0
  150. package/registry/scale-slider.js +7 -0
  151. package/registry/scale-slider.muten +9 -0
  152. package/registry/scroll-area.muten +8 -4
  153. package/registry/segmented-control.muten +8 -8
  154. package/registry/separator.muten +3 -3
  155. package/registry/share-button.js +7 -0
  156. package/registry/share-button.muten +10 -0
  157. package/registry/sheet.muten +6 -10
  158. package/registry/sidebar.muten +23 -15
  159. package/registry/skeleton.muten +6 -4
  160. package/registry/slider.js +73 -27
  161. package/registry/slider.muten +6 -6
  162. package/registry/sortable-behavior.js +43 -0
  163. package/registry/sortable-behavior.muten +2 -0
  164. package/registry/sortable-list.js +10 -26
  165. package/registry/sortable-list.muten +6 -7
  166. package/registry/sortable.muten +15 -0
  167. package/registry/spark.js +14 -0
  168. package/registry/spark.muten +3 -0
  169. package/registry/spinner.muten +4 -3
  170. package/registry/split-button.muten +12 -10
  171. package/registry/stamp-card.muten +7 -0
  172. package/registry/stat-band.muten +6 -9
  173. package/registry/stat.muten +23 -0
  174. package/registry/status-bar.muten +7 -6
  175. package/registry/stepper.muten +17 -14
  176. package/registry/store-qr.js +5 -0
  177. package/registry/store-qr.muten +5 -0
  178. package/registry/swatch.muten +8 -8
  179. package/registry/switch.muten +14 -6
  180. package/registry/table-view.muten +10 -0
  181. package/registry/tabs-behavior.js +27 -0
  182. package/registry/tabs-behavior.muten +3 -0
  183. package/registry/tabs.muten +19 -10
  184. package/registry/tag-input.js +4 -27
  185. package/registry/tag-input.muten +6 -6
  186. package/registry/text.muten +7 -0
  187. package/registry/time-field.js +5 -0
  188. package/registry/time-field.muten +6 -0
  189. package/registry/timeline.muten +14 -14
  190. package/registry/toaster.js +34 -22
  191. package/registry/toaster.muten +10 -8
  192. package/registry/toggle-group.muten +11 -6
  193. package/registry/toggle.muten +8 -5
  194. package/registry/toolbar.muten +11 -6
  195. package/registry/tooltip.muten +6 -9
  196. package/registry/tree-view.js +27 -7
  197. package/registry/tree-view.muten +8 -13
  198. package/registry/vector-field.muten +9 -8
  199. package/registry/virtual-list.js +5 -0
  200. package/registry/virtual-list.muten +6 -0
  201. package/registry/weekly-hours.muten +4 -0
  202. package/registry/wordline.muten +6 -0
  203. package/registry/zoom-control.muten +9 -9
  204. package/registry.json +872 -406
  205. package/registry/context-menu.js +0 -34
@@ -1,9 +1,12 @@
1
- # Pagination + sub-components (links reuse the .btn classes).
2
- # Compose: Pagination { PaginationPrevious(href) PaginationLink(label,href,page,current) PaginationEllipsis PaginationNext(href) }
3
- # shadcn-for-muten · part · no deps · styles via the semantic .pagination* + .btn classes in globals.css.
4
- part Pagination() { Nav class("pagination") aria(label: "pagination") { slot } }
5
- # `page` is this link's number, `current` the active-page state - the part highlights itself when they match.
6
- part PaginationLink(label: text, href: text, page: number, current: number) { Link "{$label}" -> "{$href}" class("btn btn-icon", "btn-outline" when $page == $current, "btn-ghost" when $page != $current) }
7
- part PaginationPrevious(href: text) { Link "Previous" -> "{$href}" class("btn btn-ghost btn-sm gap-1") }
8
- part PaginationNext(href: text) { Link "Next" -> "{$href}" class("btn btn-ghost btn-sm gap-1") }
9
- part PaginationEllipsis() { Span "…" class("pagination-ellipsis") }
1
+ # Pagination (links) - pages that are routes of their own (a blog, a catalog that can be shared by URL): previous,
2
+ # the numbers with the current one outlined, «…», next. For pages loaded from the server inside one screen use
3
+ # PaginationNav.
4
+ # Pagination { PaginationPrevious(to: "/blog/1") PaginationLink(label: "1", to: "/blog/1", page: 1, current: 2) PaginationLink(label: "2", to: "/blog/2", page: 2, current: 2) PaginationEllipsis() PaginationNext(to: "/blog/3") }
5
+ # shadcn-for-muten · parts · no deps · Nova (.cn-pagination-*).
6
+ part Pagination(label: text = "Páginas") { Nav class("cn-pagination cx-pagination-links") data(slot: "pagination") aria(label: $label) { Stack class("cn-pagination-content cx-pl-row") data(slot: "pagination-content") { slot } } }
7
+ part PaginationLink(label: text, to: text, page: number, current: number) {
8
+ Link "{$label}" -> "{$to}" class("cn-button cn-button-size-icon group/button cx-pl-num", "cn-button-variant-outline" when $page == $current, "cn-button-variant-ghost" when $page != $current) data(slot: "pagination-link") aria(current: $page == $current)
9
+ }
10
+ part PaginationPrevious(to: text, label: text = "Anterior") { Link -> "{$to}" class("cn-button cn-button-variant-ghost cn-button-size-default group/button cn-pagination-previous") data(slot: "pagination-previous") { Icon "lucide:chevron-left" Span "{$label}" class("cn-pagination-previous-text") } }
11
+ part PaginationNext(to: text, label: text = "Siguiente") { Link -> "{$to}" class("cn-button cn-button-variant-ghost cn-button-size-default group/button cn-pagination-next") data(slot: "pagination-next") { Span "{$label}" class("cn-pagination-next-text") Icon "lucide:chevron-right" } }
12
+ part PaginationEllipsis() { Stack class("cn-pagination-ellipsis cx-pl-more") data(slot: "pagination-ellipsis") aria(hidden: "true") { Icon "lucide:ellipsis" } }
@@ -0,0 +1,3 @@
1
+ # PasswordField - a password with show/hide and its strength. PasswordField(label: "Contraseña nueva")
2
+ # shadcn-for-muten · part over KitWidget (kit core).
3
+ part PasswordField(label: text = "Contraseña") { Custom KitWidget inputs(name: "password", label: $label) class("cx-pwd-host") }
@@ -0,0 +1,6 @@
1
+ # PermissionMatrix - any rows × any roles; one role can be locked (always allowed).
2
+ # PermissionMatrix(roles: @roleNames, rows: @permissions, locked: "Propietario", onChange: savePerms) rows: {name, description, allow [1,0,…]}
3
+ # shadcn-for-muten · part over KitWidget (kit core).
4
+ part PermissionMatrix(roles: list, rows: list, locked: text = "", onChange: action) {
5
+ Custom KitWidget inputs(name: "permissionMatrix", roles: $roles, rows: $rows, locked: $locked) on(action: $onChange) class("cx-perms")
6
+ }
@@ -0,0 +1,5 @@
1
+ // PhoneField (muten Custom) - a phone with its country (kit/values.js phoneField).
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.phoneField(el, inputs, core, on));
4
+ return () => {};
5
+ }
@@ -0,0 +1,7 @@
1
+ # PhoneField - a phone with its country: dial code from a searchable list, the country's mask while typing, checked on
2
+ # leaving the field, and what is stored (E.164). onChange gets {e164, valid, country}.
3
+ # PhoneField(label: "WhatsApp", value: "3001234567", country: "CO", onChange: setPhone)
4
+ # shadcn-for-muten · Custom (kit values) · host JS phone-field.js.
5
+ part PhoneField(label: text = "Teléfono", value: text = "", country: text = "CO", onChange: action) {
6
+ Custom PhoneField inputs(label: $label, value: $value, country: $country) on(change: $onChange) class("cx-ph-host")
7
+ }
@@ -0,0 +1,7 @@
1
+ // PhoneShell (muten Custom) - the kit's phone frame (kit/structure.js): big title that shrinks on scroll, back and two
2
+ // actions, three fixed bottom tabs with a count. tabs [{label, icon, badge}], rows [{tab, section, title, subtitle,
3
+ // value}]; handler select(label).
4
+ export function mount(el, inputs, on) {
5
+ Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/structure.js')]).then(([core, s]) => s.phoneShell(el, inputs, core, on));
6
+ return () => {};
7
+ }
@@ -0,0 +1,9 @@
1
+ # PhoneShell - the app's frame on a phone: a big title that shrinks into the bar as you scroll (the header never
2
+ # jumps in height), back on the left and at most two actions, and a bottom bar of three fixed tabs (the chosen one
3
+ # on --selected with its dot; a count never changes the width). Respects the phone's safe area.
4
+ # PhoneShell(tabs: @tabs, rows: @rows, onSelect: pickTab)
5
+ # tabs {label, icon, badge} · rows {tab, section, title, subtitle, value}: each tab's screen is its rows, grouped by section.
6
+ # shadcn-for-muten · Custom (kit/structure.js) · kit.css (.cx-phone-frame, .cx-ph-*).
7
+ part PhoneShell(tabs: list, rows: list, onSelect: action) {
8
+ Custom PhoneShell inputs(tabs: $tabs, rows: $rows) on(select: $onSelect) class("cx-ph-host")
9
+ }
@@ -1,5 +1,6 @@
1
- # Pill - a hero eyebrow chip (bigger than a Badge, rounded-full). Slot-based so you drop an icon + text:
2
- # Pill { Icon "lucide:sparkles" class("size-4") Span "For freelancers, studios and agencies" }
3
- # Tint it per brand with extra classes: Pill class("bg-blue-50 border-blue-100 text-blue-700").
4
- # shadcn-for-muten · part · no deps · styles via the semantic .pill class in globals.css.
5
- part Pill() { Stack class("pill") { slot } }
1
+ # Pill - the chip over a hero title (what is new, who it is for): rounder and bigger than a Badge, with an icon and a
2
+ # line of text inside. tone "brand" tints it with the skin's --selected.
3
+ # Pill { Icon "lucide:sparkles" Span "Nuevo: cobra con tarjeta" }
4
+ # Pill(tone: "brand") { Span "Para barberías y salones" }
5
+ # shadcn-for-muten · part · no deps · kit.css (.cx-pill).
6
+ part Pill(tone: text = "") { Stack class("cx-pill") data(slot: "pill", tone: $tone) { slot } }
@@ -0,0 +1,4 @@
1
+ # Plans - plan cards with a monthly/yearly switch; the current plan is marked; going up is one tap, going down asks.
2
+ # Plans(plans: @plans, note: "2 meses gratis") plans: {id, name, price {month, year}, desc, features, current}
3
+ # shadcn-for-muten · part over KitWidget (kit core).
4
+ part Plans(plans: list, note: text = "") { Custom KitWidget inputs(name: "plans", plans: $plans, note: $note) class("cx-plans-host") }
@@ -1,11 +1,18 @@
1
- # Popover - a button that opens a floating panel below it. The PAGE owns the bool + the toggle action:
2
- # state { open = false : bool } action t mutates open { open.toggle() }
3
- # Popover(label: "Open", open: open, onToggle: t) { … any content … }
4
- # shadcn-for-muten · part · no deps · the backdrop catches outside clicks; styles via the semantic .popover* classes.
5
- part Popover(label: text, open: bool, onToggle: action) {
6
- Stack class("popover") {
7
- Button "{$label}" -> $onToggle aria(expanded: $open) class("btn btn-outline")
8
- when $open { Button -> $onToggle class("popover-backdrop") }
9
- Stack class("popover-content", "popover-content-open" when $open) { slot }
1
+ # Popover - a panel that opens next to what you tapped and stays until you tap outside, press Esc or close it. Its
2
+ # content is anything: a small form, a filter, a detail. Trigger and content go inside, as in shadcn:
3
+ # Popover {
4
+ # PopoverTrigger { TriggerBtn { Span "Filtrar" } }
5
+ # PopoverContent(align: "start") { PopoverHeader(title: "Filtrar", description: "Lo que ves en la lista.") … }
6
+ # }
7
+ # PopoverClose is a button inside the content that closes it. align: start | center | end.
8
+ # shadcn-for-muten · parts + Custom (OverlayBehavior) · Nova (.cn-popover-*) + kit.css (.cx-ov).
9
+ part Popover() { Stack class("cx-ov") data(slot: "popover") { slot OverlayBehavior(kind: "popover") } }
10
+ part PopoverTrigger() { Stack class("cx-ov-trigger") data(slot: "popover-trigger") { slot } }
11
+ part PopoverContent(align: text = "center", width: text = "18rem") { Stack class("cn-popover-content cx-ov-content") data(slot: "popover-content", align: $align) style(w: "{$width}") { slot } }
12
+ part PopoverHeader(title: text, description: text = "") {
13
+ Stack class("cn-popover-header") data(slot: "popover-header") {
14
+ Span "{$title}" class("cn-popover-title") data(slot: "popover-title")
15
+ Text "{$description}" class("cn-popover-description") data(slot: "popover-description")
10
16
  }
11
17
  }
18
+ part PopoverClose(label: text = "Listo", variant: text = "default") { Button "{$label}" class("cn-button cn-button-variant-{$variant} cn-button-size-sm group/button") data(ovclose: "true") }
@@ -1,25 +1,25 @@
1
- # Pricing - a row of tiers. PricingTier / PricingTierFeatured take name/price/period/description; the slot holds
2
- # the feature list (PricingFeature items) + the CTA. Use PricingTierFeatured to highlight the recommended plan.
1
+ # Pricing - the plans of a landing page side by side: name, the line under it, the price and its period, what it
2
+ # includes (PricingFeature) and its button. featured "on" raises the recommended one, with its badge.
3
3
  # Pricing {
4
- # PricingTierFeatured(name: "Pro", price: "$20", period: "/mo", description: "for pro sites") {
5
- # Stack class("pricing-features") { PricingFeature { Span "Image optimization" } PricingFeature { Span "Bot protection" } }
6
- # Button "See Pro plan" -> choose class("btn btn-default w-full mt-auto")
7
- # }
4
+ # PricingTier(name: "Gratis", price: "$0", period: "/mes", description: "Para empezar") { PricingFeature(label: "Tu tienda") LinkBtn(to: "/signup", variant: "outline") { Span "Empezar" } }
5
+ # PricingTier(name: "Pro", price: "$12", period: "/mes", description: "Para crecer", featured: "on") { … }
8
6
  # }
9
- # shadcn-for-muten · block · no deps · styles via the semantic .pricing-* classes in globals.css.
10
- part Pricing() { Stack class("pricing-grid") { slot } }
11
- part PricingTier(name: text, price: text, period: text, description: text) {
12
- Stack class("pricing-tier") {
13
- Stack class("gap-1") { Span "{$name}" class("pricing-name") Span "{$description}" class("pricing-desc") }
14
- Stack class("flex flex-row items-baseline gap-1.5") { Span "{$price}" class("pricing-price") Span "{$period}" class("pricing-period") }
15
- slot
7
+ # was: the old price, struck (a yearly discount) · note: how it is billed · lead: the line over the features.
8
+ # For the plans inside the app (monthly/yearly, current plan) use Plans. shadcn-for-muten · parts · no deps · kit.css (.cx-pricing).
9
+ part Pricing() { Stack class("cx-pricing") { slot } }
10
+ part PricingTier(name: text, price: text, period: text = "", description: text = "", featured: text = "", badge: text = "Recomendado", was: text = "", note: text = "", lead: text = "") {
11
+ Stack class("cx-tier") data(featured: $featured) {
12
+ when $featured != "" { Span "{$badge}" class("cx-tier-badge") }
13
+ Stack class("cx-tier-head") { Span "{$name}" class("cx-tier-name") Text "{$description}" class("cx-tier-desc") }
14
+ Stack class("cx-tier-price") {
15
+ Span "{$price}" class("cx-tier-amount") Span "{$period}" class("cx-tier-period")
16
+ when $was != "" { Span "{$was}" class("cx-tier-was") }
17
+ }
18
+ when $note != "" { Text "{$note}" class("cx-tier-note") }
19
+ Stack class("cx-tier-body") {
20
+ when $lead != "" { Span "{$lead}" class("cx-tier-lead") }
21
+ slot
22
+ }
16
23
  }
17
24
  }
18
- part PricingTierFeatured(name: text, price: text, period: text, description: text) {
19
- Stack class("pricing-tier pricing-tier-featured") {
20
- Stack class("gap-1") { Span "{$name}" class("pricing-name") Span "{$description}" class("pricing-desc") }
21
- Stack class("flex flex-row items-baseline gap-1.5") { Span "{$price}" class("pricing-price") Span "{$period}" class("pricing-period") }
22
- slot
23
- }
24
- }
25
- part PricingFeature() { Stack class("pricing-feature") { Icon "lucide:check" class("size-4 text-primary shrink-0") slot } }
25
+ part PricingFeature(label: text) { Stack class("cx-tier-feature") { Icon "lucide:check" Span "{$label}" } }
@@ -1,7 +1,7 @@
1
- # Progress - a determinate progress bar. value is 0-100; the fill width is the one runtime value, set via style().
2
- # shadcn-for-muten · part · no deps · styles via the semantic .progress* classes in globals.css.
3
- part Progress(value: number) {
4
- Stack class("progress") {
5
- Stack class("progress-indicator") style(w: "{$value}%")
1
+ # Progress - a determinate bar, value 0-100; it eases between values. Progress(value: done, label: "Pasos hechos")
2
+ # shadcn-for-muten · part · no deps · Nova (.cn-progress) + kit.css (the value rides --w).
3
+ part Progress(value: number, label: text = "Progreso") {
4
+ Stack class("cn-progress") data(slot: "progress") aria(role: "progressbar", label: $label, valuemin: 0, valuemax: 100, valuenow: $value) {
5
+ Stack class("cn-progress-indicator") data(slot: "progress-indicator") style(w: "{$value}%")
6
6
  }
7
7
  }
@@ -0,0 +1,3 @@
1
+ # Queue - a waitlist: offer a freed slot to the first in line, with a countdown. Queue(title: "Lista de espera · sábado", people: @waiting)
2
+ # shadcn-for-muten · part over KitWidget (kit core).
3
+ part Queue(title: text, people: list, minutes: number = 10) { Custom KitWidget inputs(name: "queue", title: $title, people: $people, minutes: $minutes) class("cx-queue-host") }
@@ -1,11 +1,19 @@
1
- # Radio Group - single choice from a list. The PAGE owns a `text` state + an action that sets it:
1
+ # RadioGroup - one choice from a short list (shadcn Nova): a circle and its label, and a description when the
2
+ # choice needs one. A real radio group: arrows move and pick, only the chosen one is in the Tab order. The page
3
+ # owns the value:
2
4
  # state { plan = "pro" : text } action setPlan(v: text) mutates plan { plan.set(v) }
3
- # RadioGroup { RadioGroupItem(value: "pro", current: plan, label: "Pro", onSelect: setPlan) … }
4
- # shadcn-for-muten · part · no deps · the dot always renders and scales in via .radio-dot-on.
5
- part RadioGroup() { Stack class("radio-group") { slot } }
6
- part RadioGroupItem(value: text, current: text, label: text, onSelect: action) {
7
- Button -> $onSelect($value) aria(checked: $value == $current) class("radio-row") {
8
- Stack class("radio") { Stack class("radio-dot", "radio-dot-on" when $value == $current) }
9
- Span "{$label}" class("radio-label")
5
+ # RadioGroup(label: "Plan") { RadioGroupItem(value: "free", current: plan, label: "Gratis", onSelect: setPlan) … }
6
+ # For choices that carry more (a price, a duration) use ChoiceList. orientation: vertical | horizontal.
7
+ # shadcn-for-muten · part + Custom (RovingBehavior) · Nova (.cn-radio-group) + kit.css (checked state).
8
+ part RadioGroup(label: text = "", orientation: text = "vertical") {
9
+ Stack class("cn-radio-group cx-rg") data(slot: "radio-group", orientation: $orientation) aria(role: "radiogroup", label: $label) { slot RovingBehavior() }
10
+ }
11
+ part RadioGroupItem(value: text, current: text, label: text, onSelect: action, description: text = "") {
12
+ Button -> $onSelect($value) class("cx-rg-row") aria(role: "radio", checked: $value == $current) {
13
+ Span "" class("cn-radio-group-item cx-rg-dot") data(slot: "radio-group-item", state: $value == $current)
14
+ Stack class("cx-rg-text") {
15
+ Span "{$label}" class("cn-label") data(slot: "label")
16
+ Text "{$description}" class("cn-field-description") data(slot: "field-description")
17
+ }
10
18
  }
11
19
  }
@@ -1,25 +1,10 @@
1
- // RangeSlider host (muten Custom). inputs: { lo, hi, min, max, step }. handlers: { lo(v), hi(v) }.
2
- // Two thumbs; drag the nearer one. The page owns two numbers (the low + high bounds).
3
- export function mount(el, inputs, handlers) {
4
- const num = (v, d) => { const n = Number(v); return Number.isFinite(n) ? n : d; };
5
- const min = num(inputs.min, 0), max = num(inputs.max, 100), step = num(inputs.step, 1) || 1;
6
- let lo = num(inputs.lo, min), hi = num(inputs.hi, max);
7
-
8
- const fill = document.createElement('div'); fill.className = 'range-slider-fill';
9
- const tlo = document.createElement('div'); tlo.className = 'range-slider-thumb';
10
- const thi = document.createElement('div'); thi.className = 'range-slider-thumb';
11
- el.appendChild(fill); el.appendChild(tlo); el.appendChild(thi);
12
-
13
- const pct = (v) => (max === min ? 0 : (v - min) / (max - min) * 100);
14
- const paint = () => { fill.style.left = pct(lo) + '%'; fill.style.width = (pct(hi) - pct(lo)) + '%'; tlo.style.left = pct(lo) + '%'; thi.style.left = pct(hi) + '%'; };
15
- paint();
16
-
17
- const valAt = (clientX) => { const r = el.getBoundingClientRect(); if (!r.width) return lo; const ratio = Math.min(1, Math.max(0, (clientX - r.left) / r.width)); return Math.round((min + ratio * (max - min)) / step) * step; };
18
- let dragging = null;
19
- const move = (e) => { const v = valAt(e.clientX); if (dragging === 'lo') { lo = Math.min(v, hi); if (handlers.lo) handlers.lo(lo); } else { hi = Math.max(v, lo); if (handlers.hi) handlers.hi(hi); } paint(); };
20
- el.addEventListener('pointerdown', (e) => { const v = valAt(e.clientX); dragging = Math.abs(v - lo) <= Math.abs(v - hi) ? 'lo' : 'hi'; el.setPointerCapture(e.pointerId); move(e); });
21
- el.addEventListener('pointermove', (e) => { if (dragging) move(e); });
22
- el.addEventListener('pointerup', () => dragging = null);
23
-
24
- return (n) => { lo = num(n.lo, lo); hi = num(n.hi, hi); paint(); };
1
+ // RangeSlider (muten Custom) - two thumbs (lo / hi) on the kit's ScaleSlider; keeps the plugin's lo/hi handlers.
2
+ export function mount(el, inputs, on) {
3
+ el.dataset.bubble = inputs.bubble || 'always'; el.dataset.ends = inputs.ends || 'on'; // optional value bubble and end labels (kit.css)
4
+ let update = null;
5
+ const values = (i) => `${i.lo},${i.hi}`;
6
+ Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/values.js')]).then(([core, v]) => {
7
+ update = v.scaleSlider(el, { ...inputs, values: values(inputs) }, core, { change: (s) => { const [lo, hi] = s.split(',').map(Number); on.lo?.(lo); on.hi?.(hi); } });
8
+ });
9
+ return (next) => update?.({ ...next, values: values(next) });
25
10
  }
@@ -1,7 +1,6 @@
1
- # Range Slider - two thumbs (a low + high bound). The PAGE owns two numbers + two actions:
2
- # state { lo = 20 : number hi = 80 : number } action setLo(v: number) mutates lo { lo.set(v) } action setHi(v: number) mutates hi { hi.set(v) }
1
+ # RangeSlider - two thumbs (a range) on the kit's ScaleSlider. The page owns lo and hi.
3
2
  # RangeSlider(lo: lo, hi: hi, min: 0, max: 100, step: 1, onLo: setLo, onHi: setHi)
4
- # shadcn-for-muten · Custom · host JS is src/components/RangeSlider.js (copied by `muten add range-slider`).
5
- part RangeSlider(lo: number, hi: number, min: number, max: number, step: number, onLo: action, onHi: action) {
6
- Custom RangeSlider inputs(lo: $lo, hi: $hi, min: $min, max: $max, step: $step) on(lo: $onLo, hi: $onHi) class("range-slider")
3
+ # shadcn-for-muten · Custom (kit values) · host JS range-slider.js.
4
+ part RangeSlider(lo: number, hi: number, min: number, max: number, step: number = 1, gap: number = 0, label: text = "", format: text = "", bubble: text = "always", ends: text = "on", onLo: action, onHi: action) {
5
+ Custom RangeSlider inputs(lo: $lo, hi: $hi, min: $min, max: $max, step: $step, gap: $gap, label: $label, format: $format, bubble: $bubble, ends: $ends) on(lo: $onLo, hi: $onHi) class("cx-sl-host")
7
6
  }
@@ -1,29 +1,36 @@
1
- // Resizable host component (muten Custom). inputs: { left, right } (panel labels). A draggable splitter
2
- // resizes the two panels. Edit this file to render real content per panel instead of the label text.
3
- export function mount(el, inputs, handlers) {
4
- const a = document.createElement('div'); a.className = 'resizable-panel';
5
- const handle = document.createElement('div'); handle.className = 'resizable-handle';
6
- const b = document.createElement('div'); b.className = 'resizable-panel';
7
- el.appendChild(a); el.appendChild(handle); el.appendChild(b);
8
-
9
- let pct = 50;
10
- const paint = () => { a.style.flexBasis = pct + '%'; b.style.flexBasis = (100 - pct) + '%'; };
11
- const setText = (left, right) => { a.textContent = left == null ? 'One' : String(left); b.textContent = right == null ? 'Two' : String(right); };
12
- setText(inputs.left, inputs.right);
13
- paint();
14
-
15
- let drag = false;
16
- handle.addEventListener('pointerdown', (e) => { drag = true; handle.setPointerCapture(e.pointerId); });
17
- el.addEventListener('pointermove', (e) => {
18
- if (!drag) return;
19
- const r = el.getBoundingClientRect();
20
- if (!r.width) return;
21
- pct = Math.min(85, Math.max(15, ((e.clientX - r.left) / r.width) * 100));
22
- paint();
1
+ // ResizableBehavior (muten Custom) - drags the handles of a ResizablePanelGroup: each handle resizes the two panels
2
+ // beside it (sizes are percentages of the group), never under a panel's min. Keyboard on a focused handle: arrows
3
+ // move 5 %, Home / End push to an end. Double-click puts the two panels back to their starting sizes.
4
+ export function mount(el) {
5
+ const group = el.parentElement, vertical = () => group.dataset.orientation === 'vertical';
6
+ const panels = () => [...group.children].filter((c) => c.classList.contains('cx-rz-panel'));
7
+ const handles = () => [...group.children].filter((c) => c.classList.contains('cx-rz-handle'));
8
+ const start = new Map();
9
+ const set = (p, v) => { p.style.flex = `${v} 1 0px`; p.dataset.now = String(Math.round(v)); };
10
+ const size = (p) => Number(p.dataset.now ?? p.dataset.size) || 50;
11
+ const min = (p) => Number(p.dataset.min) || 15;
12
+ const init = () => { panels().forEach((p) => { if (!start.has(p)) start.set(p, Number(p.dataset.size) || 50); set(p, size(p)); }); handles().forEach((h) => { h.tabIndex = 0; h.setAttribute('aria-orientation', vertical() ? 'horizontal' : 'vertical'); }); };
13
+ const pair = (h) => [h.previousElementSibling, h.nextElementSibling].map((x) => (x && x.classList.contains('cx-rz-panel') ? x : null));
14
+ const move = (h, delta) => { const [a, b] = pair(h); if (!a || !b) return; const total = size(a) + size(b); const na = Math.min(total - min(b), Math.max(min(a), size(a) + delta)); set(a, na); set(b, total - na); h.setAttribute('aria-valuenow', Math.round(na)); };
15
+ let drag = null;
16
+ group.addEventListener('pointerdown', (e) => {
17
+ const h = e.target.closest('.cx-rz-handle'); if (!h || h.parentElement !== group || e.button > 0) return;
18
+ e.preventDefault(); h.setPointerCapture(e.pointerId); h.dataset.drag = '';
19
+ drag = { h, at: vertical() ? e.clientY : e.clientX, span: vertical() ? group.clientHeight : group.clientWidth, sum: panels().reduce((s, p) => s + size(p), 0) };
23
20
  });
24
- const stop = () => { drag = false; };
25
- handle.addEventListener('pointerup', stop);
26
- handle.addEventListener('pointercancel', stop);
27
-
28
- return (n) => setText(n.left, n.right);
21
+ group.addEventListener('pointermove', (e) => {
22
+ if (!drag) return; const at = vertical() ? e.clientY : e.clientX;
23
+ move(drag.h, ((at - drag.at) / drag.span) * drag.sum); drag.at = at;
24
+ });
25
+ const end = () => { if (!drag) return; delete drag.h.dataset.drag; drag = null; };
26
+ group.addEventListener('pointerup', end); group.addEventListener('pointercancel', end);
27
+ group.addEventListener('keydown', (e) => {
28
+ const h = e.target.closest('.cx-rz-handle'); if (!h) return;
29
+ const d = { ArrowLeft: -5, ArrowUp: -5, ArrowRight: 5, ArrowDown: 5, Home: -100, End: 100 }[e.key]; if (d === undefined) return;
30
+ e.preventDefault(); move(h, d);
31
+ });
32
+ group.addEventListener('dblclick', (e) => { const h = e.target.closest('.cx-rz-handle'); if (!h) return; pair(h).forEach((p) => p && set(p, start.get(p))); });
33
+ init();
34
+ new MutationObserver(init).observe(group, { childList: true });
35
+ return () => {};
29
36
  }
@@ -1,6 +1,17 @@
1
- # Resizable - two panels with a draggable splitter. Pass the panel labels; drag the handle to resize.
2
- # Resizable(left: "Sidebar", right: "Content")
3
- # shadcn-for-muten · Custom · host JS is src/components/Resizable.js (edit it to render real content per panel).
4
- part Resizable(left: text, right: text) {
5
- Custom Resizable inputs(left: $left, right: $right) class("resizable")
1
+ # Resizable - panels you size by dragging the line between them (shadcn Nova). Each panel starts at its size (a
2
+ # percentage) and never goes under min; the handle also moves with the arrow keys (Home/End to the ends).
3
+ # ResizablePanelGroup(orientation: "horizontal") {
4
+ # ResizablePanel(size: 30) { … } ResizableHandle(grip: "on") ResizablePanel(size: 70) { … }
5
+ # }
6
+ # orientation: horizontal (side by side) | vertical (one above the other).
7
+ # shadcn-for-muten · parts + Custom (ResizableBehavior) · Nova (.cn-resizable-*) + kit.css.
8
+ part ResizablePanelGroup(orientation: text = "horizontal", label: text = "") {
9
+ Stack class("cn-resizable-panel-group cx-rz") data(slot: "resizable-panel-group", orientation: $orientation) aria(label: $label) { slot ResizableBehavior() }
6
10
  }
11
+ part ResizablePanel(size: number = 50, min: number = 15) { Stack class("cx-rz-panel") data(slot: "resizable-panel", size: $size, min: $min) { slot } }
12
+ part ResizableHandle(grip: text = "") {
13
+ Stack class("cn-resizable-handle cx-rz-handle") data(slot: "resizable-handle", grip: $grip) aria(role: "separator", label: "Cambiar el tamaño") {
14
+ when $grip != "" { Span "" class("cn-resizable-handle-icon") }
15
+ }
16
+ }
17
+ part ResizableBehavior() { Custom ResizableBehavior class("cx-rz-behavior") }
@@ -0,0 +1,16 @@
1
+ // RovingBehavior (muten Custom) - a radio group's keyboard: arrows move to the next/previous option and pick it,
2
+ // Space picks the focused one, and only the chosen option sits in the Tab order. The choice itself is the page's
3
+ // state (aria-checked); picking = clicking, so the page's action runs. Same rules as the reference artifact.
4
+ export function mount(el) {
5
+ const group = el.parentElement;
6
+ const options = () => [...group.querySelectorAll(':scope > [role="radio"]')];
7
+ const sync = () => { const all = options(); const on = all.find((o) => o.getAttribute('aria-checked') === 'true') || all[0]; all.forEach((o) => { o.tabIndex = o === on ? 0 : -1; }); };
8
+ new MutationObserver(sync).observe(group, { subtree: true, attributes: true, attributeFilter: ['aria-checked'], childList: true });
9
+ group.addEventListener('keydown', (e) => {
10
+ const all = options(), i = all.indexOf(document.activeElement); if (i < 0) return;
11
+ const d = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 }[e.key];
12
+ if (e.key === ' ') { e.preventDefault(); all[i].click(); }
13
+ if (d) { e.preventDefault(); const next = all[(i + d + all.length) % all.length]; next.focus(); next.click(); }
14
+ });
15
+ sync();
16
+ }
@@ -0,0 +1,3 @@
1
+ # RovingBehavior - arrow keys walk a group of radios (or options) and pick as they go; only the chosen one is in the
2
+ # Tab order. Used by ChoiceList. shadcn-for-muten · Custom · host JS is roving-behavior.js.
3
+ part RovingBehavior() { Custom RovingBehavior class("cx-roving-behavior") }
@@ -0,0 +1,5 @@
1
+ // RowMenu (muten Custom) - the kit's rowMenu (kit/menus.js).
2
+ export function mount(el, inputs, on) {
3
+ Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/menus.js')]).then(([core, m]) => m.rowMenu(el, inputs, core, on));
4
+ return () => {};
5
+ }
@@ -0,0 +1,5 @@
1
+ # RowMenu - the ⋯ of a row: a menu with arrows, Home/End, type-ahead, Esc back to the button. Put it in ItemActions.
2
+ # RowMenu(label: "Opciones de Corte clásico", items: @rowActions, onPick: act)
3
+ # items {id, label, icon (a kit icon id), danger, separator (a line before it)}
4
+ # shadcn-for-muten · Custom (kit menus) · host JS row-menu.js.
5
+ part RowMenu(label: text = "Más opciones", items: list, onPick: action) { Custom RowMenu inputs(label: $label, items: $items) on(pick: $onPick) class("cx-rowmenu-host") }
@@ -0,0 +1,11 @@
1
+ # SaveButton - saving takes three moments and the button keeps its width through them, so nothing jumps:
2
+ # idle («Guardar cambios») → saving (spinner, «Guardando…») → saved (check, «Guardado», green). The page owns status.
3
+ # SaveButton(status: saveState, onClick: save) status: idle | saving | saved
4
+ # shadcn-for-muten · part · no deps · kit.css (.cx-save).
5
+ part SaveButton(status: text, onClick: action, label: text = "Guardar cambios") {
6
+ Button -> $onClick class("cn-button cn-button-variant-default cn-button-size-default group/button cx-save") data(slot: "button", status: $status) {
7
+ when $status == "saving" { Stack class("cx-btn-label") { Icon "lucide:loader-circle" class("animate-spin") Span "Guardando…" } }
8
+ when $status == "saved" { Stack class("cx-btn-label") { Icon "lucide:check" Span "Guardado" } }
9
+ when $status != "saving" and $status != "saved" { Stack class("cx-btn-label") { Span "{$label}" } }
10
+ }
11
+ }
@@ -0,0 +1,7 @@
1
+ // ScaleSlider (muten Custom) - the kit's slider with the value riding on the thumb (kit/values.js).
2
+ export function mount(el, inputs, on) {
3
+ el.dataset.bubble = inputs.bubble || 'always'; el.dataset.ends = inputs.ends || 'on'; // optional value bubble and end labels (kit.css)
4
+ let update = null;
5
+ Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/values.js')]).then(([core, v]) => { update = v.scaleSlider(el, inputs, core, on); });
6
+ return (next) => update?.(next);
7
+ }
@@ -0,0 +1,9 @@
1
+ # ScaleSlider - one or two thumbs; the value rides on each thumb and two bubbles that would touch merge into one;
2
+ # the ends are written under it. Keys: arrows, Page Up/Down, Home/End. onChange gets "24" or "20000,80000".
3
+ # ScaleSlider(label: "Recordatorio antes de la cita", min: 1, max: 72, values: "24", format: "hours", onChange: setHours)
4
+ # ScaleSlider(label: "Precio", min: 0, max: 150000, step: 5000, values: "20000,80000", gap: 5000, format: "money", onChange: setPrice)
5
+ # bubble: always | drag (only while moving or focused) | off · ends: on | off (the min and max written under it).
6
+ # format: hours | money | percent | "" (+ suffix). shadcn-for-muten · Custom (kit values) · host JS scale-slider.js.
7
+ part ScaleSlider(label: text, min: number = 0, max: number = 100, step: number = 1, values: text, gap: number = 0, format: text = "", suffix: text = "", bubble: text = "always", ends: text = "on", onChange: action) {
8
+ Custom ScaleSlider inputs(label: $label, min: $min, max: $max, step: $step, values: $values, gap: $gap, format: $format, suffix: $suffix, bubble: $bubble, ends: $ends) on(change: $onChange) class("cx-sl-host")
9
+ }
@@ -1,4 +1,8 @@
1
- # Scroll Area - a scrollable region with styled scrollbars (max-height 18rem by default; eject to change).
2
- # ScrollArea { … long content … }
3
- # shadcn-for-muten · part · no deps · styles via the semantic .scroll-area class in globals.css.
4
- part ScrollArea() { Stack class("scroll-area") { slot } }
1
+ # ScrollArea - a region that scrolls inside itself with the kit's thin scrollbar, so a long list never makes the page
2
+ # longer. Give it a height; horizontal scrolls sideways (a row of cards) and hides the bar until you scroll.
3
+ # ScrollArea(height: "18rem") { each tags as t { … } }
4
+ # ScrollArea(orientation: "horizontal") { Stack class("cx-hstack") { … } }
5
+ # shadcn-for-muten · part · no deps · Nova (.cn-scroll-area) + kit.css (.cx-scroll-area).
6
+ part ScrollArea(height: text = "18rem", orientation: text = "vertical", label: text = "") {
7
+ Stack class("cn-scroll-area cx-scroll-area cx-scroll") data(slot: "scroll-area", orientation: $orientation) style(h: "{$height}") aria(label: $label) { slot }
8
+ }
@@ -1,14 +1,14 @@
1
- # Segmented Control - a single-select toolbar (tool picker, view switch). The PAGE owns a text state:
1
+ # Segmented Control - a single-select toolbar (tool picker, view switch) on the kit's tabs track: the chosen one
2
+ # sits on a sliding pill (TabsBehavior), arrows move between items. The PAGE owns a text state:
2
3
  # state { tool = "select" : text } action setTool(v: text) mutates tool { tool.set(v) }
3
4
  # SegmentedControl { SegmentedItem(value: "select", current: tool, label: "Select", onSelect: setTool) … }
4
- # shadcn-for-muten · part · no deps · styles via the semantic .segmented* classes in globals.css.
5
- part SegmentedControl() { Stack class("segmented") { slot } }
5
+ # SegmentedTabs fills its width; IconTab holds an Icon instead of a label.
6
+ # shadcn-for-muten · part + Custom (TabsBehavior) · kit.css (.cx-tabs).
7
+ part SegmentedControl() { Stack class("cx-tabs") data(slot: "segmented") aria(role: "group") { slot TabsBehavior() } }
6
8
  part SegmentedItem(value: text, current: text, label: text, onSelect: action) {
7
- Button "{$label}" -> $onSelect($value) aria(pressed: $value == $current) class("segmented-item", "segmented-item-active" when $value == $current)
9
+ Button "{$label}" -> $onSelect($value) class("cn-button cn-button-variant-ghost cn-button-size-sm cx-tab") aria(pressed: $value == $current)
8
10
  }
9
- # Full-width tab bar with icon + label (Forms · Scheduling · PDF …). IconTab is slot-based so you drop an
10
- # Icon + Span inside: SegmentedTabs { IconTab(value: "forms", current: tab, onSelect: setTab) { Icon "lucide:file-text" Span "Forms" } … }
11
- part SegmentedTabs() { Stack class("segmented segmented-fill") { slot } }
11
+ part SegmentedTabs() { Stack class("cx-tabs cx-tabs-fill") data(slot: "segmented") { slot TabsBehavior() } }
12
12
  part IconTab(value: text, current: text, onSelect: action) {
13
- Button -> $onSelect($value) aria(pressed: $value == $current) class("segmented-item", "segmented-item-active" when $value == $current) { slot }
13
+ Button -> $onSelect($value) class("cn-button cn-button-variant-ghost cn-button-size-icon-sm cx-tab") aria(pressed: $value == $current) { slot }
14
14
  }
@@ -1,3 +1,3 @@
1
- # Separator - a thin divider line (horizontal). For a vertical one: class("separator separator-vertical").
2
- # shadcn-for-muten · part · no deps · styles via the semantic .separator class in globals.css.
3
- part Separator() { Stack class("separator") }
1
+ # Separator - a hairline between groups. Separator() · Separator(orientation: "vertical")
2
+ # shadcn-for-muten · part · no deps · Nova (.cn-separator).
3
+ part Separator(orientation: text = "horizontal") { Stack class("cn-separator cn-separator-{$orientation}") data(slot: "separator", orientation: $orientation) aria(role: "separator", orientation: $orientation) }
@@ -0,0 +1,7 @@
1
+ // ShareButton (muten Custom) - a Button that opens the kit's share sheet (kit/structure.js) in the adaptive modal.
2
+ // inputs: label, variant, kind "link" | "product", name, initials, url, text, productName/Meta/Price/Image.
3
+ export function mount(el, inputs) {
4
+ const i = { ...inputs, product: inputs.kind === 'product' ? { name: inputs.productName, meta: inputs.productMeta, price: inputs.productPrice, image: inputs.productImage } : null };
5
+ Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/structure.js')]).then(([core, s]) => s.share(el, i, core));
6
+ return () => {};
7
+ }
@@ -0,0 +1,10 @@
1
+ # ShareButton - Storio's share sheet made generic, inside the adaptive modal (Dialog wide, Drawer on a phone). On top
2
+ # what is shared: the link's card (initials, name, address) or the product's (image, price in the corner, name over
3
+ # it); then the link with Copiar and a row of brand targets you can drag sideways. QR opens the code in the same
4
+ # sheet; if the phone has its own share menu, «Compartir en otra app» appears.
5
+ # ShareButton(label: "Compartir mi tienda", name: "Barbería El Roble", initials: "ER", url: "storio.ink/el-roble")
6
+ # ShareButton(label: "Compartir un producto", variant: "outline", kind: "product", name: "Barbería El Roble", url: "storio.ink/el-roble/p/pomada-mate", productName: "Pomada mate", productMeta: "Fijación media · 100 g", productPrice: 32000, productImage: "linear-gradient(135deg,#c79a6b,#6b4a2e)")
7
+ # shadcn-for-muten · Custom (kit/structure.js) · kit.css (.cx-sh-*).
8
+ part ShareButton(label: text = "Compartir", variant: text = "default", kind: text = "link", name: text, initials: text = "", url: text, text: text = "", productName: text = "", productMeta: text = "", productPrice: number = 0, productImage: text = "") {
9
+ Custom ShareButton inputs(label: $label, variant: $variant, kind: $kind, name: $name, initials: $initials, url: $url, text: $text, productName: $productName, productMeta: $productMeta, productPrice: $productPrice, productImage: $productImage) class("cx-share-host")
10
+ }
@@ -1,11 +1,7 @@
1
- # Sheet - a panel that slides in from the right edge. The PAGE owns the bool + open/close actions:
2
- # state { open = false : bool } action close mutates open { open.set(false) } Sheet(open: open, onClose: close) { … }
3
- # shadcn-for-muten · part · no deps · backdrop click closes; styles via the semantic .sheet* classes.
4
- part Sheet(open: bool, onClose: action) {
5
- Stack class("sheet-overlay", "sheet-overlay-open" when $open) {
6
- Button -> $onClose class("sheet-backdrop")
7
- Stack class("sheet-content") { slot }
8
- }
1
+ # Sheet - a panel that slides from a side and keeps the page behind it in view: a detail, filters, a cart. Full
2
+ # height from the right (default) or left, full width from the top or bottom. The page owns open.
3
+ # Sheet(open: showing, onClose: done, title: "Pedido #1042", side: "right") { … ModalFooter { … } }
4
+ # shadcn-for-muten · part over Modal (mode sheet).
5
+ part Sheet(open: bool, onClose: action, title: text, description: text = "", side: text = "right") {
6
+ Modal(open: $open, onClose: $onClose, title: $title, description: $description, mode: "sheet", side: $side) { slot }
9
7
  }
10
- part SheetTitle(label: text) { Title "{$label}" h2 class("sheet-title") }
11
- part SheetDescription(label: text) { Span "{$label}" class("sheet-description") }