@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@muten/shadcn",
3
- "version": "0.0.7",
3
+ "version": "0.1.1",
4
4
  "description": "shadcn-style component registry for muten - own-the-source parts you add with `muten add`.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -15,6 +15,8 @@
15
15
  "registry",
16
16
  "registry.json",
17
17
  "globals.css",
18
+ "nova.css",
19
+ "kit.css",
18
20
  "motion.css",
19
21
  "themes",
20
22
  "README.md",
@@ -25,7 +27,9 @@
25
27
  "./motion.css": "./motion.css",
26
28
  "./themes/*": "./themes/*",
27
29
  "./registry.json": "./registry.json",
28
- "./registry/*": "./registry/*"
30
+ "./registry/*": "./registry/*",
31
+ "./nova.css": "./nova.css",
32
+ "./kit.css": "./kit.css"
29
33
  },
30
34
  "keywords": [
31
35
  "muten",
@@ -1,16 +1,17 @@
1
- # Accordion - stacked expandable sections. The PAGE owns a bool per item + a toggle action per item:
2
- # state { a1 = true : bool a2 = false : bool } action t1 mutates a1 { a1.toggle() }
3
- # Accordion { AccordionItem(title: "…", open: a1, onToggle: t1) { … } AccordionItem(title: "…", open: a2, onToggle: t2) { … } }
4
- # shadcn-for-muten · part · no deps · the content always renders and animates open via the .accordion-panel grid trick.
5
- part Accordion() { Stack class("accordion") { slot } }
1
+ # Accordion - stacked questions; each opens to its content's exact height (200 ms) and the arrow turns instead of
2
+ # swapping icons. The page owns a bool per item and the toggle.
3
+ # Accordion { AccordionItem(title: "¿Puedo cancelar mi cita?", open: a1, onToggle: t1) { Text "Sí, hasta 3 horas antes…" } }
4
+ # size "lg": taller rows and bigger type, for the questions of a landing page.
5
+ # shadcn-for-muten · part · no deps · Nova (.cn-accordion) + kit.css.
6
+ part Accordion(size: text = "default") { Stack class("cn-accordion") data(slot: "accordion", size: $size) { slot } }
6
7
  part AccordionItem(title: text, open: bool, onToggle: action) {
7
- Stack class("accordion-item") {
8
- Button -> $onToggle aria(expanded: $open) class("accordion-trigger") {
8
+ Stack class("cn-accordion-item") data(slot: "accordion-item") {
9
+ Button -> $onToggle class("cn-accordion-trigger group/accordion-trigger") data(slot: "accordion-trigger") aria(expanded: $open) {
9
10
  Span "{$title}"
10
- Icon "lucide:chevron-down" class("accordion-chevron", "accordion-chevron-open" when $open)
11
+ Icon "lucide:chevron-down" class("cx-acc-chev")
11
12
  }
12
- Stack class("accordion-panel", "accordion-panel-open" when $open) {
13
- Stack class("accordion-content") { slot }
13
+ Stack class("cn-accordion-content cx-acc-fold") data(slot: "accordion-content", open: $open) aria(role: "region") {
14
+ Stack class("cn-accordion-content-inner") { slot }
14
15
  }
15
16
  }
16
17
  }
@@ -0,0 +1,35 @@
1
+ // Agenda (muten Custom) - the kit's agenda (day columns per person, week, list; drag to move, edge to resize, the
2
+ // appointment sheet with its lifecycle, payment, move and cancel), from kit/core.js. Everything is data:
3
+ // resources [{id, name}] · hours [{id, days: [1..6], ranges: [[540, 780], [840, 1140]]}] (minutes from midnight)
4
+ // events [{id, resource, day, start "HH:MM", dur, title, subtitle, price, tone, steps, actions, at, payment, contact}]
5
+ // day = "yyyy-mm-dd", or a number of days from today (it lands on the next day that person works)
6
+ // blocks [{id, resource | "all", day, start, dur, reason}] · from / to (hours) · slot (minutes)
7
+ // actions [{id, label, icon, primary}] - the toolbar's buttons (default: block and new); «block» and «new» keep their
8
+ // built-in behaviour, any other id fires the action handler with that id
9
+ // handlers: create(json {resource, day, start}) · change(json [{id, resource, day, start, dur, status}]) · action(id)
10
+ export function mount(el, inputs, on) {
11
+ let latest = inputs;
12
+ import('@muten/shadcn/registry/kit/core.js').then(({ kit, today, addDays }) => {
13
+ const iso = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
14
+ const list = (v) => (Array.isArray(v) ? v : []);
15
+ const build = (i) => {
16
+ const hours = Object.fromEntries(list(i.hours).map((h) => [h.id, h]));
17
+ const work = (rid, d) => { const h = hours[rid]; return h && list(h.days).includes(d.getDay()) ? list(h.ranges) : []; };
18
+ const land = (rid, off) => { let o = Number(off); for (let k = 0; k < 14 && !work(rid, addDays(today, o)).length; k++) o++; return addDays(today, o); };
19
+ const dayOf = (rid, d) => (typeof d === 'number' || /^-?\d+$/.test(String(d)) ? land(rid, d) : new Date(String(d) + 'T00:00:00'));
20
+ const events = list(i.events).map((e) => ({ tone: 'ok', at: 0, ...e, day: dayOf(e.resource, e.day) }));
21
+ const blocks = list(i.blocks).map((b) => ({ ...b, day: typeof b.day === 'number' || /^-?\d+$/.test(String(b.day)) ? addDays(today, Number(b.day)) : new Date(String(b.day) + 'T00:00:00') }));
22
+ el.innerHTML = '';
23
+ kit.agenda(el, {
24
+ resources: list(i.resources), events, blocks, availability: work,
25
+ actions: list(i.actions).length ? list(i.actions) : null,
26
+ onAction: (id) => on.action?.(id),
27
+ from: Number(i.from) || 9, to: Number(i.to) || 19, slot: Number(i.slot) || 30,
28
+ onCreate: (x) => on.create?.(JSON.stringify({ resource: x.resource, day: iso(x.day), start: x.start })),
29
+ onChange: (evs) => on.change?.(JSON.stringify(evs.map((e) => ({ id: e.id, resource: e.resource, day: iso(e.day), start: e.start, dur: e.dur, status: e.status || '' })))),
30
+ });
31
+ };
32
+ build(latest);
33
+ });
34
+ return (next) => { latest = next; };
35
+ }
@@ -0,0 +1,10 @@
1
+ # Agenda - day columns per person, the week, or a list (the phone keeps the day grid and the list). Drag an
2
+ # appointment to move it (the ghost turns red where it is not free), drag its edge to resize, tap it for its sheet:
3
+ # lifecycle, payment and receipt, write, change the time, cancel with a reason. Block time; create from a free slot.
4
+ # Everything is data (see agenda.js for the shapes); pass lists with @: Agenda(resources: @team, hours: @hours, events: @appointments, blocks: @blocks, onCreate: newAppointment, onChange: saveAgenda)
5
+ # actions: the toolbar's buttons as data, [{id, label, icon, primary}] (default: Bloquear and Nueva cita); an id other
6
+ # than block or new fires onAction with it: Agenda(…, actions: @agendaActions, onAction: agendaAction)
7
+ # shadcn-for-muten · Custom (kit core) · host JS is agenda.js.
8
+ part Agenda(resources: list, hours: list, events: list, blocks: list, actions: list, from: number = 9, to: number = 19, slot: number = 30, onCreate: action, onChange: action, onAction: action) {
9
+ Custom Agenda inputs(resources: $resources, hours: $hours, events: $events, blocks: $blocks, actions: $actions, from: $from, to: $to, slot: $slot) on(create: $onCreate, change: $onChange, action: $onAction) class("cx-agenda")
10
+ }
@@ -1,13 +1,10 @@
1
- # Alert Dialog - a modal that REQUIRES an explicit choice (no backdrop dismiss). The PAGE owns the bool:
2
- # state { open = false : bool } action confirm mutates open { open.set(false) … } action cancel mutates open { open.set(false) }
3
- # AlertDialog(open: open) { AlertDialogTitle(label) AlertDialogDescription(label) AlertDialogFooter { Button "Cancel" -> cancel … } }
4
- # shadcn-for-muten · part · no deps · reuses the .dialog* classes; the backdrop is a plain Stack (not clickable).
5
- part AlertDialog(open: bool) {
6
- Stack class("dialog-overlay", "dialog-overlay-open" when $open) {
7
- Stack class("dialog-backdrop")
8
- Stack class("dialog-content") { slot }
9
- }
1
+ # AlertDialog - asks before something that cannot be undone (delete, cancel and refund). It cannot be dismissed by
2
+ # tapping outside: the person answers with one of its two buttons (Esc counts as the safe one). Put them in a
3
+ # ModalFooter: the safe way out (cx-exit) and the action (cx-go, destructive when it destroys).
4
+ # AlertDialog(open: asking, onClose: keep, title: "¿Eliminar el servicio?", description: "Desaparece de tu tienda…") {
5
+ # ModalFooter { Btn(variant: "outline", onClick: keep) class("cx-exit") { Span "Cancelar" } Btn(variant: "destructive", onClick: remove) class("cx-go") { Span "Eliminar" } }
6
+ # }
7
+ # shadcn-for-muten · part over Modal (role alertdialog, dismiss off).
8
+ part AlertDialog(open: bool, onClose: action, title: text, description: text = "") {
9
+ Modal(open: $open, onClose: $onClose, title: $title, description: $description, mode: "dialog", role: "alertdialog", dismiss: "off") { slot }
10
10
  }
11
- part AlertDialogTitle(label: text) { Title "{$label}" h2 class("dialog-title") }
12
- part AlertDialogDescription(label: text) { Span "{$label}" class("dialog-description") }
13
- part AlertDialogFooter() { Stack class("dialog-footer") { slot } }
@@ -1,8 +1,8 @@
1
- # Alert + sub-components. variant: default | destructive.
2
- # Compose: Alert(variant) { Icon … ; Stack { AlertTitle(label) AlertDescription(label) } }
3
- # shadcn-for-muten · part · no deps · styles via the semantic .alert* classes in globals.css.
4
- part Alert(variant: text) {
5
- Section class("alert", "alert-default" when $variant == "default", "alert-destructive" when $variant == "destructive") { slot }
6
- }
7
- part AlertTitle(label: text) { Span "{$label}" class("alert-title") }
8
- part AlertDescription(label: text) { Span "{$label}" class("alert-description") }
1
+ # Alert - a callout: an icon, a title, a description, an optional action at the corner. The icon carries the tone.
2
+ # Alert(tone: "wait") { Icon "lucide:clock" AlertTitle(label: "Tu plan vence en 3 días") AlertDescription(label: "…") AlertAction { Btn(size: "xs", variant: "outline", onClick: renew) { Span "Renovar" } } }
3
+ # variant: default | destructive · tone: "" | info | ok | wait
4
+ # shadcn-for-muten · part · no deps · Nova (.cn-alert) + kit.css (tone).
5
+ part Alert(variant: text = "default", tone: text = "") { Section class("cn-alert cn-alert-variant-{$variant} group/alert") data(slot: "alert", tone: $tone) aria(role: "status") { slot } }
6
+ part AlertTitle(label: text) { Span "{$label}" class("cn-alert-title") data(slot: "alert-title") }
7
+ part AlertDescription(label: text) { Text "{$label}" class("cn-alert-description") data(slot: "alert-description") }
8
+ part AlertAction() { Stack class("cn-alert-action") data(slot: "alert-action") { slot } }
@@ -0,0 +1,9 @@
1
+ // AppShell (muten Custom) - the kit's app structure (kit/structure.js): Sidebar, page header, detail Sheet and the
2
+ // Command (⌘K). inputs: brand, plan, user, email, page, nav, pages, rows, commands, width ("narrow" = a narrow box);
3
+ // handlers navigate(page) · open(id) · action(label).
4
+ export function mount(el, inputs, on) {
5
+ let shell = null, narrow = inputs.width === "narrow";
6
+ const map = (i) => ({ ...i, brand: { name: i.brand, short: i.short, plan: i.plan }, user: { name: i.user, email: i.email } });
7
+ Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/structure.js')]).then(([core, s]) => { shell = s.appShell(el, map(inputs), core, on); shell.narrow(narrow); });
8
+ return (next) => { narrow = next.width === "narrow"; shell?.narrow(narrow); };
9
+ }
@@ -0,0 +1,12 @@
1
+ # AppShell - the system's structure on shadcn's Sidebar: store switcher on top, groups with their label and counts,
2
+ # the person at the bottom. It folds to icons (the name shows on hover) and, in a narrow box, becomes a menu that
3
+ # slides from the left. The page header has breadcrumbs, title, description and actions. Tapping a row opens the
4
+ # detail Sheet on the right without leaving the list. Ctrl/⌘ K (or the search button) opens the Command.
5
+ # AppShell(brand: "Barbería El Roble", short: "El Roble", plan: "Plan Pro", user: "Julián Restrepo", email: "julian@elroble.co", page: "Pedidos", nav: @nav, pages: @pages, rows: @rows, commands: @commands, width: width, onNavigate: go, onOpen: openRow, onAction: act)
6
+ # nav {group, icon, label, count} · pages {name, title, description, actions [{label, variant, icon}]} · rows {page, id,
7
+ # title, description, badge, tone, amount, command, sheetTitle, sheetDescription, sheet [{icon, title, description,
8
+ # badge, tone, amount}], timeline [{label, when}], primary, secondary} · commands {icon, label, page}.
9
+ # width "narrow" lays it out as in a narrow box. shadcn-for-muten · Custom (kit/structure.js) · Nova sidebar/sheet/command + kit.css (.cx-app-*).
10
+ part AppShell(brand: text, short: text = "", plan: text = "", user: text, email: text = "", page: text = "", nav: list, pages: list, rows: list, commands: list, empty: text = "", width: text = "wide", onNavigate: action, onOpen: action, onAction: action) {
11
+ Custom AppShell inputs(brand: $brand, short: $short, plan: $plan, user: $user, email: $email, page: $page, nav: $nav, pages: $pages, rows: $rows, commands: $commands, empty: $empty, width: $width) on(navigate: $onNavigate, open: $onOpen, action: $onAction) class("cx-app-host")
12
+ }
@@ -1,12 +1,16 @@
1
- # Attachment - a file attachment chip (icon + name + size).
2
- # Attachment(name: "report.pdf", size: "2.4 MB")
3
- # shadcn-for-muten · part · no deps · styles via the semantic .attachment* classes in globals.css.
4
- part Attachment(name: text, size: text) {
5
- Stack class("attachment") {
6
- Icon "lucide:paperclip" class("size-4 text-muted-foreground shrink-0")
7
- Stack class("flex flex-col") {
8
- Span "{$name}" class("attachment-name")
9
- Span "{$size}" class("attachment-size")
1
+ # Attachment - a file in a message or a form: its kind as an icon, the name (cut with «…» when long) and the size;
2
+ # tapping it opens the file. kind: file | pdf | image | audio | sheet.
3
+ # Attachment(name: "comprobante-1042.pdf", size: "182 KB", kind: "pdf", to: "/…")
4
+ # shadcn-for-muten · part · no deps · kit.css (.cx-attach).
5
+ part Attachment(name: text, size: text = "", kind: text = "file", to: text = "#") {
6
+ Link -> "{$to}" class("cx-attach") data(slot: "attachment", kind: $kind) {
7
+ Stack class("cx-attach-ic") {
8
+ when $kind == "pdf" { Icon "lucide:file-text" }
9
+ when $kind == "image" { Icon "lucide:image" }
10
+ when $kind == "audio" { Icon "lucide:file-audio" }
11
+ when $kind == "sheet" { Icon "lucide:file-spreadsheet" }
12
+ when $kind == "file" { Icon "lucide:file" }
10
13
  }
14
+ Stack class("cx-attach-text") { Span "{$name}" class("cx-attach-name") Span "{$size}" class("cx-attach-size") }
11
15
  }
12
16
  }
@@ -1,7 +1,26 @@
1
- # Avatar - a user's initials in a circle (AvatarFallback). Pass a name; built-in initial() takes the first letter.
2
- # shadcn-for-muten · part · no deps · styles via the semantic .avatar* classes. For a photo, swap the inner Span for `Image "{$src}" alt("{$name}")`.
3
- part Avatar(name: text) {
4
- Stack class("avatar") {
5
- Stack class("avatar-fallback") { Span "{initial($name)}" }
1
+ # Avatar - three sizes (24, 32, 40). Without a photo the initials take a stable tone from the name (hue), mixed with
2
+ # the skin's background so they never clash with the brand. The name shows on hover and on focus. A status dot
3
+ # (online), a stacked group with «+N», and AvatarImage for a photo.
4
+ # Avatar(name: "Valentina Ruiz", size: "lg") Avatar(name: "Laura Méndez", size: "lg", status: "online")
5
+ # AvatarGroup { Avatar(name: "Laura Méndez") Avatar(name: "Diego Paredes") AvatarMore(count: "+3", label: "3 personas más") }
6
+ # Avatar(name: "El Roble", shape: "square") - a thing, not a person
7
+ # size: sm | default | lg · status: "" | online · shadcn-for-muten · part · no deps · Nova + kit.css (.cx-av).
8
+ part Avatar(name: text, size: text = "default", shape: text = "", status: text = "") {
9
+ Stack class("cx-av-wrap") data(name: $name) aria(label: $name) {
10
+ Stack class("cn-avatar group/avatar cx-av") data(slot: "avatar", size: $size, shape: $shape) style(h: "{hue($name)}") {
11
+ Span "{initials($name)}" class("cn-avatar-fallback") data(slot: "avatar-fallback")
12
+ when $status == "online" { Span "" class("cn-avatar-badge cx-online") data(slot: "avatar-badge") aria(label: "En línea") }
13
+ }
6
14
  }
7
15
  }
16
+ part AvatarImage(src: text, name: text, size: text = "default", shape: text = "") {
17
+ Stack class("cx-av-wrap") data(name: $name) {
18
+ Stack class("cn-avatar group/avatar") data(slot: "avatar", size: $size, shape: $shape) {
19
+ Image "{$src}" alt("{$name}") class("cn-avatar-image") data(slot: "avatar-image")
20
+ }
21
+ }
22
+ }
23
+ part AvatarGroup() { Stack class("cn-avatar-group group/avatar-group") data(slot: "avatar-group") { slot } }
24
+ part AvatarMore(count: text, label: text = "") { Span "{$count}" class("cn-avatar-group-count") data(slot: "avatar-group-count", name: $label) }
25
+ # Who - a person with what they are doing now: Who(title: "Laura Méndez", subtitle: "Atiende hoy · 4 citas") { Avatar(…) }
26
+ part Who(title: text, subtitle: text = "") { Stack class("cx-who") { slot Stack class("cx-who-t") { Span "{$title}" class("cx-who-n") Span "{$subtitle}" class("cx-who-s") } } }
@@ -1,9 +1,24 @@
1
- # Badge - a small status descriptor. variant: default | secondary | destructive | outline.
2
- # shadcn-for-muten · part · no deps · styles via the semantic .badge* classes in globals.css.
3
- part Badge(label: text, variant: text) {
4
- Span "{$label}" class("badge",
5
- "badge-default" when $variant == "default",
6
- "badge-secondary" when $variant == "secondary",
7
- "badge-destructive" when $variant == "destructive",
8
- "badge-outline" when $variant == "outline")
1
+ # Badge - a short label. `StatusBadge` is the system's state: a dot and the word, never a tinted pill.
2
+ # Badge(label: "Nuevo") Badge(label: "PRO", variant: "default")
3
+ # StatusBadge(label: "Confirmada", tone: "ok") StatusBadge(label: "Por revisar", tone: "wait")
4
+ # Badge variant: default | secondary | destructive | outline | ghost | link (default: secondary)
5
+ # StatusBadge tone: ok | wait | info | bad | "" (neutral)
6
+ # shadcn-for-muten · part · no deps · Nova (.cn-badge) + kit.css (status).
7
+ part Badge(label: text, variant: text = "secondary") { Span "{$label}" class("cn-badge cn-badge-variant-{$variant} group/badge") data(slot: "badge") }
8
+ part StatusBadge(label: text, tone: text = "") { Span "{$label}" class("cn-badge cn-badge-variant-ghost group/badge") data(slot: "badge", variant: "status", tone: $tone) }
9
+ # ToneBadge - a state with its tone, tinted, with or without a dot: new | ok | wait | bad | pro (PRO uses --selected).
10
+ # ToneBadge(label: "Confirmado", tone: "ok") ToneBadge(label: "Pendiente", tone: "wait", dot: "dot")
11
+ part ToneBadge(label: text, tone: text, dot: text = "") {
12
+ Stack class("cn-badge group/badge cx-tone-{$tone}") data(slot: "badge") {
13
+ when $dot != "" { Span "" class("cx-dot") }
14
+ Span "{$label}"
15
+ }
16
+ }
17
+ # Chip - a value the person can take away with its ×; it fades out (no jump). The page owns the list.
18
+ # each tags as t { Chip(label: t.name, onRemove: removeTag) } with action removeTag(name: text) mutates tags { … }
19
+ part Chip(label: text, onRemove: action) {
20
+ Stack class("cn-badge cn-badge-variant-secondary cx-chip") data(slot: "badge") {
21
+ Span "{$label}"
22
+ Button -> $onRemove($label) class("cn-button cn-button-variant-ghost cn-button-size-icon-xs cx-chip-x") aria(label: "Quitar {$label}") { Icon "lucide:x" }
23
+ }
9
24
  }
@@ -0,0 +1,16 @@
1
+ # Band - the strip at the top for a state that lasts (no connection, the plan is about to end). One at a time: a new
2
+ # one replaces the old. It folds open and closed; the text fades first so nothing is clipped. The page owns open.
3
+ # Band(open: offline, tone: "muted", text: "Sin conexión. Guardamos tus cambios al volver.") { Icon "lucide:info" }
4
+ # Band(open: planEnding, tone: "warn", text: "Tu plan Pro vence en 3 días.", action: "Renovar", onAction: renew) { Icon "lucide:clock" }
5
+ # tone: muted | warn. shadcn-for-muten · part · no deps · kit.css (.cx-band).
6
+ part Band(open: bool, tone: text = "muted", text: text, action: text = "", onAction: action) {
7
+ Stack class("cx-band-slot") data(open: $open) {
8
+ Stack class("cx-band-fold") {
9
+ Stack class("cx-band") data(tone: $tone) aria(role: "status") {
10
+ slot
11
+ Span "{$text}" class("cx-band-text")
12
+ when $action != "" { Button "{$action}" -> $onAction class("cn-button cn-button-variant-outline cn-button-size-xs group/button") }
13
+ }
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,7 @@
1
+ # BarChart - bars that always fill their height, a period switch (14 días / 30 días). periods: {label, values} - the
2
+ # labels are the last N days up to today. money "on" formats as money.
3
+ # BarChart(title: "Ventas", periods: @sales, money: "on")
4
+ # shadcn-for-muten · part over KitWidget (kit core).
5
+ part BarChart(title: text, periods: list, money: text = "") {
6
+ Custom KitWidget inputs(name: "barChart", title: $title, periods: $periods, money: $money) class("cx-chart-card")
7
+ }
@@ -0,0 +1,3 @@
1
+ # Bell - the system's notifications: tap to see them, mark all read. Bell(items: @notices) items: {icon, tone, title, desc, time, unread}
2
+ # shadcn-for-muten · part over KitWidget (kit core).
3
+ part Bell(items: list) { Custom KitWidget inputs(name: "bell", items: $items) class("cx-bell-host") }
@@ -1,9 +1,6 @@
1
- # BentoGallery - a tiling gallery on a fixed row-height grid, so cells of different column widths always line up.
2
- # Each BentoCell picks its own col-span (and bg / content) via class:
3
- # BentoGallery {
4
- # BentoCell class("col-span-4 bg-muted") { Span "01" }
5
- # BentoCell class("col-span-2 bg-accent") { Span "02" }
6
- # }
7
- # shadcn-for-muten · block · no deps · uses .bento-gallery / .bento-cell from globals.css.
8
- part BentoGallery() { Stack class("bento-gallery") { slot } }
9
- part BentoCell() { Stack class("bento-cell") { slot } }
1
+ # BentoGallery - a gallery of tiles on a grid of fixed-height rows, so tiles of different widths always line up.
2
+ # Each BentoCell says how many of the six columns and how many rows it takes (on a phone they stack).
3
+ # BentoGallery { BentoCell(span: 4, rows: 2) { … } BentoCell(span: 2) { … } BentoCell(span: 2) { … } }
4
+ # shadcn-for-muten · parts · no deps · kit.css (.cx-bento).
5
+ part BentoGallery(rowHeight: text = "160px") { Stack class("cx-bento") style(row: "{$rowHeight}") { slot } }
6
+ part BentoCell(span: number = 2, rows: number = 1) { Stack class("cx-bento-cell") style(span: "{$span}", rows: "{$rows}") { slot } }
@@ -0,0 +1,21 @@
1
+ // Booking (muten Custom) - the kit's booking picker (kit/structure.js): week strip and hours in one piece.
2
+ // inputs: week [{days, hours}], taken [{day, hours}], wait, cta, demo; handler select("yyyy-mm-dd HH:MM").
3
+ // demo "on" plays it by itself (a day, then an hour, in turn) until the first touch; it never presses the button.
4
+ export function mount(el, inputs, on) {
5
+ let timer = 0, stopped = false;
6
+ Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/structure.js')]).then(([core, s]) => {
7
+ s.booking(el, inputs, core, on);
8
+ if (inputs.demo !== 'on' || matchMedia('(prefers-reduced-motion: reduce)').matches) return;
9
+ let turn = 0;
10
+ const open = (sel) => [...el.querySelectorAll(sel)].filter((b) => !b.disabled && !b.hasAttribute('data-full'));
11
+ const pick = (sel) => { const all = open(sel); return all.length ? all[turn % all.length] : null; };
12
+ const play = () => {
13
+ if (stopped) return;
14
+ pick('.cx-day')?.click();
15
+ timer = setTimeout(() => { if (stopped) return; pick('.cx-hour')?.click(); turn++; timer = setTimeout(play, 2600); }, 1500);
16
+ };
17
+ el.addEventListener('pointerdown', () => { stopped = true; clearTimeout(timer); }, { once: true, capture: true });
18
+ timer = setTimeout(play, 900);
19
+ });
20
+ return () => {};
21
+ }
@@ -0,0 +1,13 @@
1
+ # Booking - week and hours in one piece. The dot under a day says something is left; closed days are disabled; a
2
+ # full day shows its empty state with a jump to the next day with hours. Changing day shows a skeleton with the exact
3
+ # shape of the hours while they load (nothing jumps in height). What is chosen goes on --selected.
4
+ # Booking(week: @week, taken: @taken, wait: 380, onSelect: book)
5
+ # week {days [0..6], hours ["09:00", …]} = what the shop offers each weekday (a weekday in no row is closed);
6
+ # taken {day (days from today or "yyyy-mm-dd"), hours} = what is booked (no hours = the whole day). onSelect gets
7
+ # "yyyy-mm-dd HH:MM" on «Continuar».
8
+ # demo "on": it plays itself for a landing (a day, then an hour, again and again) until someone touches it; never
9
+ # confirms, and stays still under reduced motion.
10
+ # shadcn-for-muten · Custom (kit/structure.js) · kit.css (.cx-book, .cx-week, .cx-hours).
11
+ part Booking(week: list, taken: list, wait: number = 0, cta: text = "Continuar", demo: text = "", onSelect: action) {
12
+ Custom Booking inputs(week: $week, taken: $taken, wait: $wait, cta: $cta, demo: $demo) on(select: $onSelect) class("cx-book-host")
13
+ }
@@ -1,7 +1,9 @@
1
- # Breadcrumb + sub-components.
2
- # Compose: Breadcrumb { BreadcrumbLink(label,href) BreadcrumbSeparator BreadcrumbLink(...) BreadcrumbSeparator BreadcrumbPage(label) }
3
- # shadcn-for-muten · part · no deps · styles via the semantic .breadcrumb* classes in globals.css.
4
- part Breadcrumb() { Nav class("breadcrumb") aria(label: "breadcrumb") { slot } }
5
- part BreadcrumbLink(label: text, href: text) { Link "{$label}" -> "{$href}" class("breadcrumb-link") }
6
- part BreadcrumbPage(label: text) { Span "{$label}" class("breadcrumb-page") }
7
- part BreadcrumbSeparator() { Icon "lucide:chevron-right" class("breadcrumb-separator") }
1
+ # Breadcrumb - where this page sits: the trail of pages above it, the current one last (not a link). A long trail
2
+ # folds its middle into «…» (BreadcrumbEllipsis).
3
+ # Breadcrumb { BreadcrumbLink(label: "El Roble", to: "/") BreadcrumbSeparator() BreadcrumbLink(label: "Pedidos", to: "/orders") BreadcrumbSeparator() BreadcrumbPage(label: "#1042") }
4
+ # shadcn-for-muten · parts · no deps · Nova (.cn-breadcrumb-*).
5
+ part Breadcrumb(label: text = "Migas") { Nav class("cx-breadcrumb") data(slot: "breadcrumb") aria(label: $label) { Stack class("cn-breadcrumb-list cx-bc-list") data(slot: "breadcrumb-list") aria(role: "list") { slot } } }
6
+ part BreadcrumbLink(label: text, to: text) { Link "{$label}" -> "{$to}" class("cn-breadcrumb-link cx-bc-item") data(slot: "breadcrumb-link") }
7
+ part BreadcrumbPage(label: text) { Span "{$label}" class("cn-breadcrumb-page cx-bc-item") data(slot: "breadcrumb-page") aria(current: "page") }
8
+ part BreadcrumbSeparator() { Stack class("cn-breadcrumb-separator cx-bc-sep") data(slot: "breadcrumb-separator") aria(hidden: "true") { Icon "lucide:chevron-right" } }
9
+ part BreadcrumbEllipsis(label: text = "Más páginas") { Stack class("cn-breadcrumb-ellipsis cx-bc-more") data(slot: "breadcrumb-ellipsis", name: $label) aria(label: $label) { Icon "lucide:ellipsis" } }
@@ -1,4 +1,13 @@
1
- # ButtonGroup - segmented row of buttons that share borders. Put `btn btn-outline` buttons inside:
2
- # ButtonGroup { Button "Bold" -> b class("btn btn-outline") Button "Italic" -> i class("btn btn-outline") }
3
- # shadcn-for-muten · part · no deps · uses .btn-group from globals.css.
4
- part ButtonGroup() { Stack class("btn-group") { slot } }
1
+ # ButtonGroup - buttons that belong together, sharing their borders (shadcn Nova). Buttons, an Input or a
2
+ # ButtonGroupText go inside; ButtonGroupSeparator splits two filled buttons. Groups can nest (they keep a gap).
3
+ # ButtonGroup { Btn(variant: "outline", onClick: prev) { Icon "lucide:chevron-left" } Btn(variant: "outline", onClick: next) { Icon "lucide:chevron-right" } }
4
+ # ButtonGroup(orientation: "vertical") { … }
5
+ # ButtonGroup { ButtonGroupText(label: "https://") Input(value: site) }
6
+ # orientation: horizontal | vertical. shadcn-for-muten · part · no deps · Nova (.cn-button-group).
7
+ part ButtonGroup(orientation: text = "horizontal", label: text = "") {
8
+ Stack class("cn-button-group cn-button-group-orientation-{$orientation}") data(slot: "button-group", orientation: $orientation) aria(role: "group", label: $label) { slot }
9
+ }
10
+ part ButtonGroupText(label: text) { Span "{$label}" class("cn-button-group-text") data(slot: "button-group-text") }
11
+ part ButtonGroupSeparator(orientation: text = "vertical") {
12
+ Stack class("cn-separator cn-separator-{$orientation} cn-button-group-separator") data(slot: "button-group-separator", orientation: $orientation) aria(role: "separator")
13
+ }
@@ -1,8 +1,21 @@
1
- # Button (part name Btn - `Button` is a muten primitive, so a part can't reuse that name) - a plug-and-play
2
- # button. No class props: pass a `variant` + your action, put the label/icon in the slot.
3
- # Btn(variant: "default", onClick: save) { Span "Save" }
4
- # Btn(variant: "outline", onClick: cancel) { Icon "lucide:x" class("size-4") Span "Cancel" }
5
- # variant: default | secondary | outline | ghost | destructive | link. For a link that looks like a button use
6
- # `Link "..." -> "/path" class("btn btn-default")`; for sizes add btn-sm|btn-lg on the primitive.
7
- # shadcn-for-muten · part · no deps · wraps the Button primitive + the .btn recipe.
8
- part Btn(variant: text, onClick: action) { Button -> $onClick class("btn btn-{$variant}") { slot } }
1
+ # Button (part name Btn - `Button` is a muten primitive, so a part can't reuse that name) - shadcn's Nova button.
2
+ # Put the label (and an icon) in the slot; an Icon nudges on hover (trailing moves on, leading lifts, lone grows).
3
+ # Btn(onClick: save) { Span "Guardar" }
4
+ # Btn(variant: "outline", size: "sm", onClick: cancel) { Icon "lucide:x" Span "Cancelar" }
5
+ # Btn(size: "icon-sm", variant: "ghost", tone: "wait", onClick: due) { Icon "lucide:clock" Indicator(tone: "wait") }
6
+ # Btn(onClick: go) class("cx-fill") { Span "Marcar llegada" Icon "lucide:chevron-right" } - full width (a card's primary)
7
+ # variant: default | outline | secondary | ghost | destructive | link (default: default)
8
+ # size: xs (24) | sm (28) | default (32) | lg (36) | xl (44) | 2xl (52) | icon | icon-xs | icon-sm | icon-lg | icon-xl | icon-2xl
9
+ # xl and 2xl are for big surfaces (a landing, a sign-in) and match the fields' xl / 2xl.
10
+ # tone: "" | info | ok | wait | bad | note - a tinted icon button (a pending thing on a card)
11
+ # label: the name of an icon-only button (aria-label, and the kit's tooltip on hover). Leave it empty when there is text.
12
+ # shadcn-for-muten · part · no deps · Nova classes (nova.css) + kit.css.
13
+ part Btn(variant: text = "default", size: text = "default", tone: text = "", label: text = "", onClick: action) {
14
+ Button -> $onClick class("cn-button cn-button-variant-{$variant} cn-button-size-{$size} group/button", "cx-tonal cx-has-indicator" when $tone != "") data(slot: "button", tone: $tone, name: $label) aria(label: $label) { slot }
15
+ }
16
+ # TriggerBtn - a Btn that only opens what sits next to it (a Popover, a DropdownMenu, a SplitButton's menu): it has no
17
+ # action of its own, so opening never runs something else. Same variants and sizes as Btn.
18
+ # DropdownMenu { DropdownTrigger { TriggerBtn { Span "Opciones" Icon "lucide:chevron-down" } } DropdownContent { … } }
19
+ part TriggerBtn(variant: text = "outline", size: text = "default", label: text = "") {
20
+ Button class("cn-button cn-button-variant-{$variant} cn-button-size-{$size} group/button") data(slot: "button", name: $label) aria(label: $label) { slot }
21
+ }
@@ -1,94 +1,29 @@
1
- // Calendar host (muten Custom). Variants via inputs:
2
- // mode: "single" | "range" | "multiple" months: 1 | 2 caption: "label" | "dropdown"
3
- // `selected` encodes the value: single -> "yyyy-mm-dd"; range -> "from/to"; multiple -> "d1,d2,...".
4
- // handlers: { select(encoded) }.
5
- export function mount(el, inputs, handlers) {
6
- const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
7
- const DOW = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
8
- const pad = (n) => String(n).padStart(2, '0');
9
- const iso = (d) => d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate());
10
- const parse = (s) => { const d = new Date(s + 'T00:00:00'); return isNaN(d.getTime()) ? null : d; };
11
- const same = (a, b) => a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
12
- const num = (d) => d.getFullYear() * 10000 + d.getMonth() * 100 + d.getDate();
13
-
14
- const mode = inputs.mode || 'single';
15
- const months = Math.min(2, Math.max(1, Number(inputs.months) || 1));
16
- const dropdown = inputs.caption === 'dropdown';
17
- const today = new Date();
18
-
19
- let selected = [];
20
- const readSelected = (val) => {
21
- selected = [];
22
- const s = String(val || '');
23
- if (!s) return;
24
- const parts = mode === 'range' ? s.split('/') : mode === 'multiple' ? s.split(',') : [s];
25
- for (const p of parts) { const d = parse(p); if (d) selected.push(d); }
26
- };
27
- readSelected(inputs.selected);
28
- const anchor = selected[0] || today;
29
- let vy = anchor.getFullYear();
30
- let vm = anchor.getMonth();
31
-
32
- const emit = () => {
33
- const out = mode === 'range' ? selected.map(iso).join('/') : mode === 'multiple' ? selected.map(iso).join(',') : (selected[0] ? iso(selected[0]) : '');
34
- if (handlers.select) handlers.select(out);
35
- };
36
- const isSel = (d) => selected.some((s) => same(s, d));
37
- const inRange = (d) => { if (mode !== 'range' || selected.length < 2) return false; const x = num(d), a = num(selected[0]), b = num(selected[1]); return x > Math.min(a, b) && x < Math.max(a, b); };
38
- const pick = (d) => {
39
- if (mode === 'single') selected = [d];
40
- else if (mode === 'multiple') { const i = selected.findIndex((s) => same(s, d)); if (i >= 0) selected.splice(i, 1); else selected.push(d); }
41
- else { if (selected.length !== 1) selected = [d]; else { const a = selected[0]; selected = num(d) < num(a) ? [d, a] : [a, d]; } }
42
- render(); emit();
43
- };
44
-
45
- const monthGrid = (y, m) => {
46
- const wrap = document.createElement('div'); wrap.className = 'calendar-month';
47
- const cap = document.createElement('div'); cap.className = 'calendar-caption';
48
- if (dropdown) {
49
- const ms = document.createElement('select'); ms.className = 'calendar-select';
50
- MONTHS.forEach((name, i) => { const o = document.createElement('option'); o.value = String(i); o.textContent = name; if (i === m) o.selected = true; ms.appendChild(o); });
51
- ms.addEventListener('change', () => { vm = Number(ms.value); render(); });
52
- const ys = document.createElement('select'); ys.className = 'calendar-select';
53
- for (let yy = y - 10; yy <= y + 10; yy++) { const o = document.createElement('option'); o.value = String(yy); o.textContent = String(yy); if (yy === y) o.selected = true; ys.appendChild(o); }
54
- ys.addEventListener('change', () => { vy = Number(ys.value); render(); });
55
- cap.appendChild(ms); cap.appendChild(ys);
56
- } else {
57
- const l = document.createElement('div'); l.className = 'calendar-label'; l.textContent = MONTHS[m] + ' ' + y; cap.appendChild(l);
58
- }
59
- wrap.appendChild(cap);
60
-
61
- const grid = document.createElement('div'); grid.className = 'calendar-grid';
62
- for (const d of DOW) { const c = document.createElement('div'); c.className = 'calendar-dow'; c.textContent = d; grid.appendChild(c); }
63
- const first = new Date(y, m, 1).getDay();
64
- const days = new Date(y, m + 1, 0).getDate();
65
- for (let i = 0; i < first; i++) grid.appendChild(document.createElement('div'));
66
- for (let day = 1; day <= days; day++) {
67
- const d = new Date(y, m, day);
68
- const cell = document.createElement('button'); cell.type = 'button'; cell.className = 'calendar-day'; cell.textContent = String(day);
69
- if (isSel(d)) cell.classList.add('calendar-day-selected');
70
- else if (inRange(d)) cell.classList.add('calendar-day-range');
71
- else if (same(d, today)) cell.classList.add('calendar-day-today');
72
- cell.addEventListener('click', () => pick(d));
73
- grid.appendChild(cell);
74
- }
75
- wrap.appendChild(grid);
76
- return wrap;
77
- };
78
-
79
- function render() {
80
- el.innerHTML = '';
81
- const SVG = (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>';
82
- const prev = document.createElement('button'); prev.type = 'button'; prev.className = 'calendar-nav-prev'; prev.innerHTML = SVG('<path d="m15 18-6-6 6-6"/>'); // lucide chevron-left
83
- const next = document.createElement('button'); next.type = 'button'; next.className = 'calendar-nav-next'; next.innerHTML = SVG('<path d="m9 18 6-6-6-6"/>'); // lucide chevron-right
84
- prev.addEventListener('click', () => { vm--; if (vm < 0) { vm = 11; vy--; } render(); });
85
- next.addEventListener('click', () => { vm++; if (vm > 11) { vm = 0; vy++; } render(); });
86
- el.appendChild(prev); el.appendChild(next);
87
- const wrap = document.createElement('div'); wrap.className = 'calendar-months';
88
- for (let i = 0; i < months; i++) { let yy = vy, mm = vm + i; while (mm > 11) { mm -= 12; yy++; } wrap.appendChild(monthGrid(yy, mm)); }
89
- el.appendChild(wrap);
90
- }
91
-
92
- render();
93
- return (n) => { readSelected(n.selected); render(); };
1
+ // Calendar (muten Custom) - the kit's month grid (one or two months, a day or a range), from kit/core.js.
2
+ // inputs: selected "yyyy-mm-dd" · "from/to" · or relative to today: "today", "0", "-6/0" (days from today)
3
+ // mode "single" | "range" · months 1 | 2 · min "today" | "yyyy-mm-dd"
4
+ // presets "reports" → Hoy · Últimos 7 días · Este mes · Mes pasado beside a range
5
+ // caption "on" → the sentence under it («Elegiste el jueves 8 de octubre» / «Del 2 oct al 8 oct · 7 días»)
6
+ // handlers: select("yyyy-mm-dd" | "from/to"). Same component as the reference artifact's Calendar.
7
+ export function mount(el, inputs, on) {
8
+ let cal = null;
9
+ import('@muten/shadcn/registry/kit/core.js').then(({ kit, ui, today, addDays, DAYLONG, MON, fmtDay }) => {
10
+ const iso = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
11
+ const one = (s) => { const t = String(s).trim(); if (t === 'today') return new Date(today); if (/^-?\d+$/.test(t)) return addDays(today, Number(t)); const d = new Date(t + 'T00:00:00'); return Number.isNaN(d.getTime()) ? null : d; };
12
+ const mode = inputs.mode === 'range' ? 'range' : 'single';
13
+ const decode = (v) => { const s = String(v ?? ''); if (!s) return null; if (mode === 'range') { const [a, b] = s.split('/').map(one); return a ? [a, b || a] : null; } return one(s); };
14
+ const sayOne = (d) => (d ? `Elegiste el ${DAYLONG[d.getDay()].toLowerCase()} ${d.getDate()} de ${MON[d.getMonth()]}` : '');
15
+ const sayRange = (r) => (r?.[0] && r?.[1] ? `Del ${fmtDay(r[0])} al ${fmtDay(r[1])} · ${Math.round((r[1] - r[0]) / 864e5) + 1} días` : r?.[0] ? `Desde el ${fmtDay(r[0])}: elige el último día` : '');
16
+ const say = (v) => (mode === 'range' ? sayRange(v) : sayOne(v));
17
+ const PRESETS = [['Hoy', () => [today, today]], ['Últimos 7 días', () => [addDays(today, -6), today]], ['Este mes', () => [new Date(today.getFullYear(), today.getMonth(), 1), today]], ['Mes pasado', () => [new Date(today.getFullYear(), today.getMonth() - 1, 1), new Date(today.getFullYear(), today.getMonth(), 0)]]];
18
+ const withPresets = mode === 'range' && inputs.presets === 'reports';
19
+ el.innerHTML = `${withPresets ? `<div class="cx-range-wrap"><div class="cx-vstack cx-range-presets" style="width:auto;gap:2px">${PRESETS.map(([l], i) => ui.button({ label: l, variant: 'ghost', size: 'sm', cls: 'cx-left', attrs: `data-preset="${i}" aria-pressed="${i === 1}"` })).join('')}</div><div data-cal></div></div>` : '<div data-cal></div>'}${inputs.caption === 'on' ? '<span class="cx-text" data-variant="caption" data-out></span>' : ''}`;
20
+ const out = el.querySelector('[data-out]');
21
+ const value = decode(inputs.selected);
22
+ const emit = (v) => on.select?.(Array.isArray(v) ? (v[0] ? `${iso(v[0])}/${iso(v[1] || v[0])}` : '') : v ? iso(v) : '');
23
+ cal = kit.calendar(el.querySelector('[data-cal]'), { mode, months: Number(inputs.months) === 2 ? 2 : 1, value, min: inputs.min ? one(inputs.min) : null,
24
+ onChange: (v) => { if (out) out.textContent = say(v); el.querySelectorAll('[data-preset]').forEach((b) => b.setAttribute('aria-pressed', 'false')); emit(v); } });
25
+ if (out) out.textContent = say(value);
26
+ el.addEventListener('click', (e) => { const b = e.target.closest('[data-preset]'); if (!b) return; const r = PRESETS[+b.dataset.preset][1](); cal.set(r); if (out) out.textContent = sayRange(r); el.querySelectorAll('[data-preset]').forEach((x) => x.setAttribute('aria-pressed', x === b)); emit(r); });
27
+ });
28
+ return () => {};
94
29
  }