@muten/shadcn 0.1.15 → 0.2.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 (183) hide show
  1. package/AGENTS.md +88 -1
  2. package/globals.css +1 -0
  3. package/kit.css +348 -126
  4. package/motion.css +3 -0
  5. package/package.json +1 -1
  6. package/registry/accordion.muten +3 -3
  7. package/registry/agenda.js +20 -16
  8. package/registry/agenda.muten +14 -12
  9. package/registry/alert-dialog.muten +2 -2
  10. package/registry/alert.muten +1 -1
  11. package/registry/app-shell.muten +1 -1
  12. package/registry/attachment.muten +1 -1
  13. package/registry/avatar.muten +4 -4
  14. package/registry/badge.muten +14 -3
  15. package/registry/band.muten +2 -2
  16. package/registry/bar-chart.muten +2 -2
  17. package/registry/bio-play.muten +2 -2
  18. package/registry/booking-play.muten +5 -5
  19. package/registry/booking.muten +2 -2
  20. package/registry/breadcrumb.muten +4 -4
  21. package/registry/button.muten +9 -7
  22. package/registry/calendar.js +15 -10
  23. package/registry/calendar.muten +1 -1
  24. package/registry/card.muten +1 -1
  25. package/registry/carousel.muten +1 -1
  26. package/registry/chat-play.muten +8 -8
  27. package/registry/chatbot.muten +5 -4
  28. package/registry/checklist.muten +1 -1
  29. package/registry/choice-list.muten +3 -3
  30. package/registry/code.js +10 -4
  31. package/registry/collapsible.muten +2 -2
  32. package/registry/color-picker.js +37 -36
  33. package/registry/color-picker.muten +1 -1
  34. package/registry/combobox.js +1 -1
  35. package/registry/combobox.muten +2 -2
  36. package/registry/command-palette.muten +2 -2
  37. package/registry/command.js +1 -1
  38. package/registry/command.muten +7 -7
  39. package/registry/context-menu.muten +3 -3
  40. package/registry/count-up.js +33 -31
  41. package/registry/count-up.muten +6 -4
  42. package/registry/date-field.js +1 -1
  43. package/registry/date-field.muten +3 -3
  44. package/registry/day-plan.muten +5 -5
  45. package/registry/device-frame.muten +3 -3
  46. package/registry/dialog.muten +1 -1
  47. package/registry/dirty-bar.js +2 -2
  48. package/registry/dirty-bar.muten +3 -3
  49. package/registry/drawer.muten +1 -1
  50. package/registry/dropdown-menu.muten +4 -4
  51. package/registry/empty.muten +1 -1
  52. package/registry/feature-grid.muten +1 -1
  53. package/registry/feature-rows.muten +2 -2
  54. package/registry/field-behavior.js +13 -3
  55. package/registry/field-behavior.muten +3 -3
  56. package/registry/field.muten +5 -5
  57. package/registry/file-input.js +8 -4
  58. package/registry/file-input.muten +3 -3
  59. package/registry/fixed-card.muten +4 -4
  60. package/registry/folder-picker.js +6 -2
  61. package/registry/folder-picker.muten +2 -2
  62. package/registry/heatmap.muten +5 -5
  63. package/registry/image-upload.js +1 -1
  64. package/registry/image-upload.muten +2 -2
  65. package/registry/inbox-play.muten +5 -5
  66. package/registry/indicator.muten +1 -1
  67. package/registry/info-table.muten +1 -1
  68. package/registry/input-group.muten +11 -11
  69. package/registry/input-otp.muten +2 -2
  70. package/registry/input.muten +1 -1
  71. package/registry/inspector.muten +2 -2
  72. package/registry/item.muten +2 -2
  73. package/registry/kit/board.js +36 -34
  74. package/registry/kit/collections.js +24 -24
  75. package/registry/kit/core.js +231 -118
  76. package/registry/kit/dates.js +29 -27
  77. package/registry/kit/i18n.js +172 -0
  78. package/registry/kit/menus.js +14 -13
  79. package/registry/kit/overlays.js +7 -2
  80. package/registry/kit/structure.js +66 -61
  81. package/registry/kit/table.js +19 -17
  82. package/registry/kit/values.js +36 -28
  83. package/registry/kit-skeleton.muten +1 -1
  84. package/registry/kit-widget.js +9 -4
  85. package/registry/lang-scope.js +8 -0
  86. package/registry/lang-scope.muten +5 -0
  87. package/registry/legal-doc.muten +5 -5
  88. package/registry/link-btn.muten +1 -1
  89. package/registry/list-motion.js +46 -0
  90. package/registry/list-motion.muten +4 -0
  91. package/registry/live-stack.muten +2 -2
  92. package/registry/logo-cloud.muten +2 -2
  93. package/registry/marketing.muten +7 -7
  94. package/registry/member-list.muten +2 -2
  95. package/registry/menubar.muten +3 -3
  96. package/registry/message-scroller.muten +3 -3
  97. package/registry/message.muten +9 -9
  98. package/registry/messaging.muten +1 -1
  99. package/registry/modal-behavior.js +24 -0
  100. package/registry/modal.muten +4 -4
  101. package/registry/money-field.js +1 -1
  102. package/registry/money-field.muten +2 -2
  103. package/registry/nav-button.muten +4 -4
  104. package/registry/navigation-menu.muten +4 -4
  105. package/registry/navpill.muten +6 -6
  106. package/registry/new-appointment.js +1 -1
  107. package/registry/new-appointment.muten +4 -4
  108. package/registry/number-field.js +6 -4
  109. package/registry/number-field.muten +1 -1
  110. package/registry/order-board.js +1 -1
  111. package/registry/order-board.muten +2 -2
  112. package/registry/order-play.muten +4 -4
  113. package/registry/pagination-nav.js +1 -1
  114. package/registry/pagination-nav.muten +3 -3
  115. package/registry/pagination.muten +4 -4
  116. package/registry/password-field.muten +2 -2
  117. package/registry/permission-matrix.muten +4 -4
  118. package/registry/phone-bar.muten +6 -6
  119. package/registry/phone-field.js +1 -1
  120. package/registry/phone-field.muten +1 -1
  121. package/registry/phone-shell.js +1 -1
  122. package/registry/pill.muten +2 -2
  123. package/registry/plans.muten +1 -1
  124. package/registry/popover.muten +2 -2
  125. package/registry/pricing.muten +4 -4
  126. package/registry/product-play.muten +3 -3
  127. package/registry/progress.muten +3 -3
  128. package/registry/queue.muten +1 -1
  129. package/registry/radio-group.muten +1 -1
  130. package/registry/rank-item.muten +4 -4
  131. package/registry/resizable.muten +1 -1
  132. package/registry/row-menu.js +1 -1
  133. package/registry/row-menu.muten +2 -2
  134. package/registry/save-button.muten +5 -5
  135. package/registry/scale-slider.js +1 -1
  136. package/registry/scale-slider.muten +2 -2
  137. package/registry/screen-tour.muten +1 -1
  138. package/registry/share-bar.muten +2 -2
  139. package/registry/share-button.muten +4 -4
  140. package/registry/sheet.muten +1 -1
  141. package/registry/sidebar.muten +12 -6
  142. package/registry/site-footer.muten +6 -6
  143. package/registry/sortable-behavior.js +67 -20
  144. package/registry/sortable-list.js +3 -1
  145. package/registry/sortable-list.muten +2 -2
  146. package/registry/sortable.muten +5 -4
  147. package/registry/spinner.muten +2 -2
  148. package/registry/split-button.muten +2 -2
  149. package/registry/stacked-bars.js +3 -3
  150. package/registry/stacked-bars.muten +2 -2
  151. package/registry/stamp-card.muten +2 -2
  152. package/registry/stat-band.muten +3 -3
  153. package/registry/stat.muten +3 -3
  154. package/registry/status-bar.muten +2 -2
  155. package/registry/stepper.muten +4 -4
  156. package/registry/store-qr.js +1 -1
  157. package/registry/store-qr.muten +2 -2
  158. package/registry/swatch.muten +1 -1
  159. package/registry/switch.muten +8 -7
  160. package/registry/table-view.muten +3 -3
  161. package/registry/tabs-behavior.js +9 -1
  162. package/registry/tabs.muten +2 -2
  163. package/registry/tag-input.js +1 -1
  164. package/registry/tag-input.muten +2 -2
  165. package/registry/text.muten +2 -2
  166. package/registry/time-field.js +1 -1
  167. package/registry/time-field.muten +3 -3
  168. package/registry/timeline.muten +4 -4
  169. package/registry/toaster.muten +2 -2
  170. package/registry/toggle-group.muten +1 -1
  171. package/registry/toggle.muten +3 -3
  172. package/registry/toolbar.muten +4 -4
  173. package/registry/tooltip.muten +2 -2
  174. package/registry/tree-view.js +38 -25
  175. package/registry/tree-view.muten +1 -1
  176. package/registry/veil.muten +1 -1
  177. package/registry/virtual-list.js +1 -1
  178. package/registry/virtual-list.muten +2 -2
  179. package/registry/virtual-window.js +4 -4
  180. package/registry/virtual-window.muten +2 -2
  181. package/registry/wordline.muten +1 -1
  182. package/registry/zoom-control.muten +1 -1
  183. package/registry.json +22 -2
package/motion.css CHANGED
@@ -70,6 +70,9 @@
70
70
  /* Hover affordances. */
71
71
  .hover-lift { transition: transform var(--mu-motion, 200ms) var(--mu-ease, cubic-bezier(0.16,1,0.3,1)), box-shadow var(--mu-motion, 200ms) ease; }
72
72
  .hover-lift:hover { transform: translateY(-4px); box-shadow: var(--shadow-soft, 0 12px 32px -12px rgb(0 0 0 / 0.25)); }
73
+ /* lifted, an invisible strip fills the 4 px it left below, so a pointer resting on the bottom edge stays «on» it:
74
+ without it the card rose off the pointer, dropped back under it and rose again, a hover that blinked forever */
75
+ .hover-lift:hover::after { content: ""; position: absolute; left: 0; right: 0; top: calc(100% - 2px); height: 10px; }
73
76
  .hover-zoom img { transition: transform 0.5s var(--mu-ease, cubic-bezier(0.16,1,0.3,1)); }
74
77
  .hover-zoom:hover img { transform: scale(1.05); }
75
78
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@muten/shadcn",
3
- "version": "0.1.15",
3
+ "version": "0.2.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",
@@ -1,8 +1,8 @@
1
1
  # Accordion - stacked questions; each opens to its content's exact height (200 ms) and the arrow turns instead of
2
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…" } }
3
+ # Accordion { AccordionItem(title: "Can I cancel my appointment?", open: a1, onToggle: t1) { Text "Yes, up to 3 hours before…" } }
4
4
  # No state to own (or a search engine must read the answers closed)? The same look on the native details:
5
- # Accordion(size: "lg") { Details "¿…?" group("faq") class("cn-accordion-item") { Text "…" class("cn-accordion-content-inner") } }
5
+ # Accordion(size: "lg") { Details "…?" group("faq") class("cn-accordion-item") { Text "…" class("cn-accordion-content-inner") } }
6
6
  # size "lg": taller rows and bigger type, for the questions of a landing page.
7
7
  # shadcn-for-muten · part · no deps · Nova (.cn-accordion) + kit.css.
8
8
  part Accordion(size: text = "default") { Stack class("cn-accordion") data(slot: "accordion", size: $size) { slot } }
@@ -18,7 +18,7 @@ part AccordionItem(title: text, open: bool, onToggle: action) {
18
18
  }
19
19
  }
20
20
  # FaqItem - one question of an Accordion on the native <details>: no state to keep, and items sharing a `group` open one
21
- # at a time (they share the group «faq»). The answer goes in the slot (text, or text with a button). FaqItemOpen starts open (the first of a list).
21
+ # at a time (they share the group “faq”). The answer goes in the slot (text, or text with a button). FaqItemOpen starts open (the first of a list).
22
22
  part FaqItem(question: text) {
23
23
  Details "{$question}" group("faq") class("cn-accordion-item") { Stack class("cn-accordion-content-inner") { slot } }
24
24
  }
@@ -1,14 +1,14 @@
1
1
  // Agenda / AgendaLive (muten Custom) - the kit's agenda (kit/core.js): day columns per person, week, list; drag to move,
2
2
  // edge to resize. Everything is data:
3
3
  // resources [{id, name, photo, kind, capacity, group}] - each one is an AGENDA: kind "business" (the shop's own, shown
4
- // first), "person" (a worker, the default) or any other (a room: say its `group`, «Salas»). capacity = how many citas
4
+ // first), "person" (a worker, the default) or any other (a room: say its `group`, “Rooms”). capacity = how many appointments
5
5
  // it holds at once: 1 (default, never overlap) · 3 (a class of three) · 0 (no limit). overlap "never" | "allow"
6
- // overrides every capacity. Citas that break one (the server sent them crossed) say «Se cruza».
6
+ // overrides every capacity. Citas that break one (the server sent them crossed) say “Se cruza”.
7
7
  // resources [{id, name, photo}] · hours [{id, days: [1..6], ranges: [[540, 780], [840, 1140]]}] (minutes from midnight);
8
8
  // a person can have several entries, one per group of days, when the days do not share one schedule
9
9
  // events [{id, resource, with: [ids], day, start "HH:MM", dur, title, subtitle, price, status, tint, ink, tone, steps, actions, at, payment, contact}]
10
10
  // day = "yyyy-mm-dd", or a number of days from today (it lands on the next day that person works)
11
- // with = the other agendas the cita also takes (the room it uses, a second worker): checked and shown in each
11
+ // with = the other agendas the appointment also takes (the room it uses, a second worker): checked and shown in each
12
12
  // status = awaiting_payment | confirmed | started | done | noshow | cancelled · tint/ink = the service's colours
13
13
  // blocks [{id, resource | "all", day, start, dur, reason}] · from / to (hours) · slot (minutes) · view: day | week | list
14
14
  // free [{resource, day, start, end}] - the server's free hours; when sent they are the only free hours shown
@@ -22,19 +22,19 @@
22
22
  // agenda runs a command when its value changes, so repeat one with a suffix after # ("new#1", "new#2"…)
23
23
  // hour12 "yes" - times written 3:00 PM (the data stays "HH:MM") · timeZone "America/Bogota" - today and now are the shop's
24
24
  // fit "yes" - the hours share the height the app gives the agenda (give it one: a flex parent), never under 56px
25
- // phoneStrip "dots" - the phone's week strip marks each day with a dot per cita in its service's colour (default: the count)
25
+ // phoneStrip "dots" - the phone's week strip marks each day with a dot per appointment in its service's colour (default: the count)
26
26
  // phoneView "list" - a phone opens on the list until a view is picked · blockScope "all" - blocks are for the whole
27
- // business (no «Para» picker: a backend with no per-person blocks)
28
- // events also take tags ["A domicilio", "Llegó 10:02"] (words next to the time) and photo (the client's face, in the list)
29
- // resources also take role (read under the name), hideEmpty (the column shows only on days it has citas) and
30
- // bookable false (an agenda that only holds citas, «Sin asignar»: never offered as free)
31
- // resize "off" - a cita's length is not dragged (a backend that only moves day and time) · moveAcross "off" - a
32
- // dragged cita stays in its own agenda (only its time, and its day in the week view, change)
33
- // placing [{resource, day, start, dur, title, tint}] - while the app's «new cita» sheet is open, where it will land
34
- // create "sheet" - a showcase: «new» and a tap on a free hour open a short built-in form and the cita lands at once
27
+ // business (no “Para” picker: a backend with no per-person blocks)
28
+ // events also take tags ["At home", "Arrived 10:02"] (words next to the time) and photo (the client's face, in the list)
29
+ // resources also take role (read under the name), hideEmpty (the column shows only on days it has appointments) and
30
+ // bookable false (an agenda that only holds appointments, “Unassigned”: never offered as free)
31
+ // resize "off" - a appointment's length is not dragged (a backend that only moves day and time) · moveAcross "off" - a
32
+ // dragged appointment stays in its own agenda (only its time, and its day in the week view, change)
33
+ // placing [{resource, day, start, dur, title, tint}] - while the app's “new appointment” sheet is open, where it will land
34
+ // create "sheet" - a showcase: “new” and a tap on a free hour open a short built-in form and the appointment lands at once
35
35
  // handlers: create(json {resource, day, start}) · open(json event) · block(json {resource, day, start, dur, reason})
36
36
  // move(json {id, resource, day, start, dur}) · range(json {from, to, view}: view is day | week | list | range) · change(json [...]) · action(id)
37
- // ranges "yes" - the month under the title also offers «Varios días» (a range listed day by day); off by default
37
+ // ranges "yes" - the month under the title also offers “Several days” (a range listed day by day); off by default
38
38
  // The title opens a month (floating; a sheet on a phone) to jump to a day.
39
39
  // LIVE: new inputs repaint the same agenda (same day, view and person): an app sends its data again and it shows;
40
40
  // lang, currency, locale and cents change live too.
@@ -45,7 +45,7 @@ export function mount(el, inputs, on) {
45
45
  const SKIP = new Set(['command', 'start']);
46
46
  const sigOf = (i) => JSON.stringify(Object.keys(i).filter((k) => !SKIP.has(k)).sort().map((k) => [k, i[k]]));
47
47
  let lastSig = sigOf(inputs);
48
- import('@muten/shadcn/registry/kit/core.js').then(({ kit, today, addDays }) => {
48
+ import('@muten/shadcn/registry/kit/core.js').then(({ kit, today, addDays, langOf }) => {
49
49
  const iso = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
50
50
  const list = (v) => (Array.isArray(v) ? v : []);
51
51
  const at = (s) => new Date(String(s) + 'T00:00:00');
@@ -65,7 +65,9 @@ export function mount(el, inputs, on) {
65
65
  queue: list(i.queue).map((q) => ({ ...q, day: plain(q.day) })), closed: list(i.closed).map(plain), loading: yes(i.loading),
66
66
  placing: (() => { const g = list(i.placing)[0]; return g && g.start ? { ...g, day: dayOf(g.resource, g.day) } : null; })(),
67
67
  from: Number(i.from) || 9, to: Number(i.to) || 19,
68
- lang: i.lang === 'en' ? 'en' : 'es', overlap: ['never', 'allow'].includes(i.overlap) ? i.overlap : 'auto', currency: i.currency || '', locale: i.locale || '', cents: yes(i.cents),
68
+ // the language passed (Storio passes it), else the page's (the nearest lang), English by default
69
+ lang: i.lang === 'en' || i.lang === 'es' ? i.lang : (langOf(el) === 'es' ? 'es' : 'en'), overlap: ['never', 'allow'].includes(i.overlap) ? i.overlap : 'auto', currency: i.currency || '', locale: i.locale || '', cents: yes(i.cents),
70
+ agentMark: i.agentMark || '', agentLabel: i.agentLabel || '',
69
71
  };
70
72
  };
71
73
  map = data;
@@ -88,7 +90,9 @@ export function mount(el, inputs, on) {
88
90
  onChange: (evs) => send('change', evs.map((e) => ({ id: e.id, resource: e.resource, day: iso(e.day), start: e.start, dur: e.dur, status: e.status || '' }))),
89
91
  });
90
92
  if (pending) { const sig = sigOf(pending); if (sig !== lastSig) { lastSig = sig; api.update(data(pending)); } pending = null; }
93
+ // with no lang of its own it follows the page's: switched live when a lang changes
94
+ document.addEventListener('kit:lang', () => { if (el.isConnected && inputs.lang !== 'en' && inputs.lang !== 'es') api.update({ lang: langOf(el) === 'es' ? 'es' : 'en' }); });
91
95
  });
92
96
  const command = (next) => { const c = String(next.command || ''); if (c === lastCmd) return; lastCmd = c; if (c) api.run(c.split('#')[0]); };
93
- return (next) => { if (!api || !map) { pending = next; return; } const sig = sigOf(next); if (sig !== lastSig) { lastSig = sig; api.update(map(next)); } command(next); };
97
+ return (next) => { inputs = next; if (!api || !map) { pending = next; return; } const sig = sigOf(next); if (sig !== lastSig) { lastSig = sig; api.update(map(next)); } command(next); };
94
98
  }
@@ -2,14 +2,14 @@
2
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
3
  # lifecycle, payment and receipt, write, change the time, cancel with a reason. Block time; create from a free slot.
4
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
5
+ # actions: the toolbar's buttons as data, [{id, label, icon, primary}] (default: Bloquear and Nueva appointment); an id other
6
6
  # than block or new fires onAction with it: Agenda(…, actions: @agendaActions, onAction: agendaAction)
7
7
  # view: how it opens, day (default) | week | list.
8
8
  # shadcn-for-muten · Custom (kit core) · host JS is agenda.js.
9
9
  part Agenda(resources: list, hours: list, events: list, blocks: list, actions: list, from: number = 9, to: number = 19, slot: number = 30, view: text = "day", onCreate: action, onChange: action, onAction: action) {
10
10
  Custom Agenda inputs(resources: $resources, hours: $hours, events: $events, blocks: $blocks, actions: $actions, from: $from, to: $to, slot: $slot, view: $view) on(create: $onCreate, change: $onChange, action: $onAction) class("cx-agenda")
11
11
  }
12
- # AgendaDemo - the same agenda for a showcase with no app behind it: «Nueva cita» and a tap on a free hour open a short
12
+ # AgendaDemo - the same agenda for a showcase with no app behind it: “New appointment” and a tap on a free hour open a short
13
13
  # built-in form (the service from `services` [{name, dur}], the client, the time) and the appointment lands on the
14
14
  # agenda at once, with its notice. Block, move and the appointment sheet work as in Agenda. Nothing is saved anywhere.
15
15
  part AgendaDemo(resources: list, hours: list, events: list, blocks: list, actions: list, services: list, from: number = 9, to: number = 19, slot: number = 30, view: text = "day", onChange: action, onAction: action) {
@@ -21,22 +21,24 @@ part AgendaDemo(resources: list, hours: list, events: list, blocks: list, action
21
21
  # queue who waits for a free hour [{day, name, photo}] · closed days the shop is shut ["yyyy-mm-dd"]
22
22
  # events carry status (awaiting_payment | confirmed | started | done | noshow | cancelled) and tint/ink (the service's colours)
23
23
  # lang es | en · currency + locale (prices written with Intl) · cents "yes" · drag "off" · loading "yes" · start "yyyy-mm-dd"
24
- # onOpen(json {id, …}) a cita tapped (open your sheet) · onCreate(json {resource, day, start}) a free hour tapped
24
+ # onOpen(json {id, …}) a appointment tapped (open your sheet) · onCreate(json {resource, day, start}) a free hour tapped
25
25
  # onBlock(json {resource, day, start, dur, reason}) · onMove(json {id, resource, day, start, dur}) dragged (answer with
26
26
  # the data: it stays where it landed, or goes back if you send it unchanged) · onRange(json {from, to, view}) days shown
27
27
  # fit "yes" the hours stretch to the height the app gives the agenda (a flex parent that grows), never under 56px
28
- # ranges "yes" the date under the title also offers «Varios días» (a range, listed day by day); off by default
29
- # actions [{id, label, icon, primary}] the toolbar's buttons (default: Bloquear and Nueva cita; [{id: "block", …}] alone
30
- # leaves «Nueva cita» to the app's own button) · an id other than block or new fires onAction(id)
31
- # phone "nav": on a phone its actions (Bloquear, Nueva cita, yours) and the arrows leave the toolbar for the app's
28
+ # ranges "yes" the date under the title also offers “Several days” (a range, listed day by day); off by default
29
+ # actions [{id, label, icon, primary}] the toolbar's buttons (default: Bloquear and Nueva appointment; [{id: "block", …}] alone
30
+ # leaves “New appointment” to the app's own button) · an id other than block or new fires onAction(id)
31
+ # phone "nav": on a phone its actions (Bloquear, Nueva appointment, yours) and the arrows leave the toolbar for the app's
32
32
  # phone bar (PhoneBar), which sends them back through `command` ("new", "block", "prev", "next", "today", "view:list",
33
33
  # an action id; repeat one with a suffix after #: "new#2"). Nothing runs while loading.
34
34
  # several agendas: resources carry kind (business | person | a room…) and capacity (1 never overlaps, 0 no limit);
35
- # a cita takes its resource plus every id in `with`. overlap "never" | "allow" overrides all capacities.
36
- # resize "off" · moveAcross "off" (a backend that only moves day and time) · hour12 "yes" (3:00 PM) · timeZone (the shop's today and now) · phoneView "list" · phoneStrip "dots" (the phone's week: a dot per cita in its colour) · blockScope "all" · placing [{…}]
37
- # (the dashed cita while the app's new-cita sheet is open) · events take tags [words] and photo; resources role, hideEmpty
35
+ # a appointment takes its resource plus every id in `with`. overlap "never" | "allow" overrides all capacities.
36
+ # resize "off" · moveAcross "off" (a backend that only moves day and time) · hour12 "yes" (3:00 PM) · timeZone (the shop's today and now) · phoneView "list" · phoneStrip "dots" (the phone's week: a dot per appointment in its colour) · blockScope "all" · placing [{…}]
37
+ # (the dashed appointment while the app's new-cita sheet is open) · events take tags [words] and photo; resources role, hideEmpty
38
+ # events take by "agent" (made by the app's assistant): agentMark (an image url) goes before their name, and agentLabel
39
+ # ("Booked by your assistant") under it on the phone's list and in what a screen reader says
38
40
  # AgendaLive(resources: @team, hours: @hours, events: @citas, blocks: @blocks, free: @free, queue: @queue, closed: @closed,
39
41
  # lang: ui.lang, currency: "USD", onOpen: openCita, onCreate: newCita, onBlock: block, onMove: moveCita, onRange: loadDays)
40
- part AgendaLive(resources: list, hours: list, events: list, blocks: list, actions: list, free: list, queue: list, closed: list, lang: text = "es", currency: text = "", locale: text = "", cents: text = "", from: number = 9, to: number = 19, slot: number = 30, view: text = "day", loading: text = "", start: text = "", drag: text = "", phone: text = "", command: text = "", overlap: text = "", hour12: text = "", timeZone: text = "", phoneView: text = "", phoneStrip: text = "", fit: text = "", ranges: text = "", blockScope: text = "", resize: text = "", moveAcross: text = "", placing: list, onOpen: action, onCreate: action, onBlock: action, onMove: action, onRange: action, onAction: action) {
41
- Custom Agenda inputs(resources: $resources, hours: $hours, events: $events, blocks: $blocks, actions: $actions, free: $free, queue: $queue, closed: $closed, lang: $lang, currency: $currency, locale: $locale, cents: $cents, from: $from, to: $to, slot: $slot, view: $view, loading: $loading, start: $start, drag: $drag, phone: $phone, command: $command, overlap: $overlap, hour12: $hour12, timeZone: $timeZone, phoneView: $phoneView, phoneStrip: $phoneStrip, fit: $fit, ranges: $ranges, blockScope: $blockScope, resize: $resize, moveAcross: $moveAcross, placing: $placing, sheet: "app") on(open: $onOpen, create: $onCreate, block: $onBlock, move: $onMove, range: $onRange, action: $onAction) class("cx-agenda")
42
+ part AgendaLive(resources: list, hours: list, events: list, blocks: list, actions: list, free: list, queue: list, closed: list, lang: text = "es", currency: text = "", locale: text = "", cents: text = "", from: number = 9, to: number = 19, slot: number = 30, view: text = "day", loading: text = "", start: text = "", drag: text = "", phone: text = "", command: text = "", overlap: text = "", hour12: text = "", agentMark: text = "", agentLabel: text = "", timeZone: text = "", phoneView: text = "", phoneStrip: text = "", fit: text = "", ranges: text = "", blockScope: text = "", resize: text = "", moveAcross: text = "", placing: list, onOpen: action, onCreate: action, onBlock: action, onMove: action, onRange: action, onAction: action) {
43
+ Custom Agenda inputs(resources: $resources, hours: $hours, events: $events, blocks: $blocks, actions: $actions, free: $free, queue: $queue, closed: $closed, lang: $lang, currency: $currency, locale: $locale, cents: $cents, from: $from, to: $to, slot: $slot, view: $view, loading: $loading, start: $start, drag: $drag, phone: $phone, command: $command, overlap: $overlap, hour12: $hour12, agentMark: $agentMark, agentLabel: $agentLabel, timeZone: $timeZone, phoneView: $phoneView, phoneStrip: $phoneStrip, fit: $fit, ranges: $ranges, blockScope: $blockScope, resize: $resize, moveAcross: $moveAcross, placing: $placing, sheet: "app") on(open: $onOpen, create: $onCreate, block: $onBlock, move: $onMove, range: $onRange, action: $onAction) class("cx-agenda")
42
44
  }
@@ -1,8 +1,8 @@
1
1
  # AlertDialog - asks before something that cannot be undone (delete, cancel and refund). It cannot be dismissed by
2
2
  # tapping outside: the person answers with one of its two buttons (Esc counts as the safe one). Put them in a
3
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" } }
4
+ # AlertDialog(open: asking, onClose: keep, title: "Delete the service?", description: "It disappears from your shop…") {
5
+ # ModalFooter { Btn(variant: "outline", onClick: keep) class("cx-exit") { Span "Cancel" } Btn(variant: "destructive", onClick: remove) class("cx-go") { Span "Delete" } }
6
6
  # }
7
7
  # shadcn-for-muten · part over Modal (role alertdialog, dismiss off).
8
8
  part AlertDialog(open: bool, onClose: action, title: text, description: text = "") {
@@ -1,5 +1,5 @@
1
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" } } }
2
+ # Alert(tone: "wait") { Icon "lucide:clock" AlertTitle(label: "Your plan expires in 3 days") AlertDescription(label: "…") AlertAction { Btn(size: "xs", variant: "outline", onClick: renew) { Span "Renew" } } }
3
3
  # variant: default | destructive · tone: "" | info | ok | wait
4
4
  # shadcn-for-muten · part · no deps · Nova (.cn-alert) + kit.css (tone).
5
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 } }
@@ -2,7 +2,7 @@
2
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
3
  # slides from the left. The page header has breadcrumbs, title, description and actions. Tapping a row opens the
4
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)
5
+ # AppShell(brand: "El Roble Barbershop", short: "El Roble", plan: "Plan Pro", user: "Julián Restrepo", email: "julian@elroble.co", page: "Orders", nav: @nav, pages: @pages, rows: @rows, commands: @commands, width: width, onNavigate: go, onOpen: openRow, onAction: act)
6
6
  # nav {group, icon, label, count} · pages {name, title, description, actions [{label, variant, icon}]} · rows {page, id,
7
7
  # title, description, badge, tone, amount, command, sheetTitle, sheetDescription, sheet [{icon, title, description,
8
8
  # badge, tone, amount}], timeline [{label, when}], primary, secondary} · commands {icon, label, page}.
@@ -1,4 +1,4 @@
1
- # Attachment - a file in a message or a form: its kind as an icon, the name (cut with «…» when long) and the 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
2
  # tapping it opens the file. kind: file | pdf | image | audio | sheet.
3
3
  # Attachment(name: "comprobante-1042.pdf", size: "182 KB", kind: "pdf", to: "/…")
4
4
  # shadcn-for-muten · part · no deps · kit.css (.cx-attach).
@@ -1,15 +1,15 @@
1
1
  # Avatar - three sizes (24, 32, 40). Without a photo the initials take a stable tone from the name (hue), mixed with
2
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.
3
+ # (online), a stacked group with “+N”, and AvatarImage for a photo.
4
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") }
5
+ # AvatarGroup { Avatar(name: "Laura Méndez") Avatar(name: "Diego Paredes") AvatarMore(count: "+3", label: "3 more people") }
6
6
  # Avatar(name: "El Roble", shape: "square") - a thing, not a person
7
7
  # size: sm | default | lg · status: "" | online · shadcn-for-muten · part · no deps · Nova + kit.css (.cx-av).
8
8
  part Avatar(name: text, size: text = "default", shape: text = "", status: text = "") {
9
9
  Stack class("cx-av-wrap") data(name: $name) aria(label: $name) {
10
10
  Stack class("cn-avatar group/avatar cx-av") data(slot: "avatar", size: $size, shape: $shape) style(h: "{hue($name)}") {
11
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") }
12
+ when $status == "online" { Span "" class("cn-avatar-badge cx-online") data(slot: "avatar-badge", trl: "Online") aria(label: "Online") }
13
13
  }
14
14
  }
15
15
  }
@@ -22,5 +22,5 @@ part AvatarImage(src: text, name: text, size: text = "default", shape: text = ""
22
22
  }
23
23
  part AvatarGroup() { Stack class("cn-avatar-group group/avatar-group") data(slot: "avatar-group") { slot } }
24
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(…) }
25
+ # Who - a person with what they are doing now: Who(title: "Laura Méndez", subtitle: "Working today · 4 appointments") { Avatar(…) }
26
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,13 +1,24 @@
1
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")
2
+ # Badge(label: "New") Badge(label: "PRO", variant: "default")
3
+ # StatusBadge(label: "Confirmed", tone: "ok") StatusBadge(label: "To review", tone: "wait")
4
4
  # Badge variant: default | secondary | destructive | outline | ghost | link (default: secondary)
5
5
  # StatusBadge tone: ok | wait | info | bad | "" (neutral)
6
6
  # shadcn-for-muten · part · no deps · Nova (.cn-badge) + kit.css (status).
7
7
  part Badge(label: text, variant: text = "secondary") { Span "{$label}" class("cn-badge cn-badge-variant-{$variant} group/badge") data(slot: "badge") }
8
+ # IconBadge - a Badge with an Icon or a Spinner inside (its children); end: true puts it after the word.
9
+ # IconBadge(label: "Verified") { Icon "lucide:badge-check" } IconBadge(label: "Saving", variant: "outline") { Spinner }
10
+ part IconBadge(label: text, variant: text = "secondary", end: bool = false) {
11
+ Stack class("cn-badge cn-badge-variant-{$variant} group/badge cx-badge-icon") class("cx-badge-end" when $end) data(slot: "badge") { slot Span "{$label}" }
12
+ }
13
+ # LinkBadge - a badge that goes somewhere, with the arrow at its end.
14
+ # LinkBadge(label: "See plan", to: "/billing")
15
+ part LinkBadge(label: text, to: text, variant: text = "secondary") {
16
+ Link -> "{$to}" class("cn-badge cn-badge-variant-{$variant} group/badge cx-badge-icon cx-badge-end cx-badge-link") data(slot: "badge") { Icon "lucide:arrow-up-right" Span "{$label}" }
17
+ }
8
18
  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
19
  # 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")
20
+ # The word is the tone mixed toward the ink, so it reads on its tint in light and dark.
21
+ # ToneBadge(label: "Confirmed", tone: "ok") ToneBadge(label: "Pending", tone: "wait", dot: "dot")
11
22
  part ToneBadge(label: text, tone: text, dot: text = "") {
12
23
  Stack class("cn-badge group/badge cx-tone-{$tone}") data(slot: "badge") {
13
24
  when $dot != "" { Span "" class("cx-dot") }
@@ -1,7 +1,7 @@
1
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
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" }
3
+ # Band(open: offline, tone: "muted", text: "Offline. We'll save your changes when you're back.") { Icon "lucide:info" }
4
+ # Band(open: planEnding, tone: "warn", text: "Your Pro plan expires in 3 days.", action: "Renew", onAction: renew) { Icon "lucide:clock" }
5
5
  # tone: muted | warn. shadcn-for-muten · part · no deps · kit.css (.cx-band).
6
6
  part Band(open: bool, tone: text = "muted", text: text, action: text = "", onAction: action) {
7
7
  Stack class("cx-band-slot") data(open: $open) {
@@ -1,6 +1,6 @@
1
- # BarChart - bars that always fill their height, a period switch (14 días / 30 días). periods: {label, values} - the
1
+ # BarChart - bars that always fill their height, a period switch (14 days / 30 days). periods: {label, values} - the
2
2
  # labels are the last N days up to today. money "on" formats as money.
3
- # BarChart(title: "Ventas", periods: @sales, money: "on")
3
+ # BarChart(title: "Sales", periods: @sales, money: "on")
4
4
  # shadcn-for-muten · part over KitWidget (kit core).
5
5
  part BarChart(title: text, periods: list, money: text = "") {
6
6
  Custom KitWidget inputs(name: "barChart", title: $title, periods: $periods, money: $money) class("cx-chart-card")
@@ -1,7 +1,7 @@
1
1
  # BioPlay - a social profile shown, not used: the photo, the handle, the followers, the bio with the store's link
2
2
  # and the two buttons, as Instagram draws a profile. For a landing; a picture (aria-hidden), `label` says what.
3
- # BioPlay(photo: "/owner.webp", handle: "brumabarberia", followers: "1.284 seguidores", bio: "Cortes y barba.",
4
- # link: "storio.ink/bruma-barberia", follow: "Seguir", message: "Mensaje", label: "…")
3
+ # BioPlay(photo: "/owner.webp", handle: "brumabarberia", followers: "1,284 followers", bio: "Cortes y barba.",
4
+ # link: "storio.ink/bruma-barberia", follow: "Follow", message: "Message", label: "…")
5
5
  # shadcn-for-muten · part · no deps · kit.css (.cx-bio).
6
6
  part BioPlay(photo: text, handle: text, followers: text, bio: text, link: text, follow: text, message: text, label: text = "") {
7
7
  Stack class("cx-bio cx-play") aria(role: "img", label: $label) {
@@ -1,15 +1,15 @@
1
1
  # BookingPlay - a booking shown, not used: one service, a few days and hours, and a booking that plays itself in a
2
- # loop (a day is picked, then an hour, «Reservar» is pressed and the confirmation comes up with its pass). For a
2
+ # loop (a day is picked, then an hour, “Book” is pressed and the confirmation comes up with its pass). For a
3
3
  # landing or a bento cell, where the real Booking would be a form nobody fills. Taken hours are struck through.
4
- # BookingPlay(service: "Corte clásico", place: "Bruma Barbería · 30 min", price: "USD 15", cta: "Reservar",
5
- # doneTitle: "¡Listo, Carla!", doneText: "Corte clásico · hoy · 4:30 PM", code: "STO-4F2K", label: "Así reserva tu cliente") {
6
- # BookingPlayDays { BookingPlayDay(weekday: "lun", day: "5", state: "off") … BookingPlayDay(weekday: "hoy", day: "9", state: "pick") }
4
+ # BookingPlay(service: "Classic cut", place: "Bruma Barbershop · 30 min", price: "USD 15", cta: "Book",
5
+ # doneTitle: "Done, Carla!", doneText: "Classic cut · today · 4:30 PM", code: "STO-4F2K", label: "This is how your customer books") {
6
+ # BookingPlayDays { BookingPlayDay(weekday: "mon", day: "5", state: "off") … BookingPlayDay(weekday: "today", day: "9", state: "pick") }
7
7
  # BookingPlayHours { BookingPlayHour(time: "10:00", state: "taken") … BookingPlayHour(time: "4:30", state: "pick", picked: "4:30 PM") }
8
8
  # }
9
9
  # photo: the owner's face in the circle (empty = the service's initial).
10
10
  # state: "" | off (closed day) | taken (hour) | pick (the one the loop chooses; one day and one hour).
11
11
  # Reduced motion: the pick is shown, the confirmation is not. shadcn-for-muten · part · no deps · kit.css (.cx-bkp).
12
- part BookingPlay(service: text, photo: text = "", place: text = "", price: text = "", cta: text = "Reservar", doneTitle: text = "", doneText: text = "", code: text = "", label: text = "") {
12
+ part BookingPlay(service: text, photo: text = "", place: text = "", price: text = "", cta: text = "Book", doneTitle: text = "", doneText: text = "", code: text = "", label: text = "") {
13
13
  Stack class("cx-bkp cx-play") aria(role: "img", label: $label) {
14
14
  Stack class("cx-bkp-who") aria(hidden: true) {
15
15
  when $photo != "" { Image "{$photo}" alt("") class("cx-bkp-av cx-play-face") }
@@ -4,10 +4,10 @@
4
4
  # Booking(week: @week, taken: @taken, wait: 380, onSelect: book)
5
5
  # week {days [0..6], hours ["09:00", …]} = what the shop offers each weekday (a weekday in no row is closed);
6
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».
7
+ # "yyyy-mm-dd HH:MM" on “Continue”.
8
8
  # demo "on": it plays itself for a landing (a day, then an hour, again and again) until someone touches it; never
9
9
  # confirms, and stays still under reduced motion.
10
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) {
11
+ part Booking(week: list, taken: list, wait: number = 0, cta: text = "Continue", demo: text = "", onSelect: action) {
12
12
  Custom Booking inputs(week: $week, taken: $taken, wait: $wait, cta: $cta, demo: $demo) on(select: $onSelect) class("cx-book-host")
13
13
  }
@@ -1,9 +1,9 @@
1
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") }
2
+ # folds its middle into “…” (BreadcrumbEllipsis).
3
+ # Breadcrumb { BreadcrumbLink(label: "El Roble", to: "/") BreadcrumbSeparator() BreadcrumbLink(label: "Orders", to: "/orders") BreadcrumbSeparator() BreadcrumbPage(label: "#1042") }
4
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 } } }
5
+ part Breadcrumb(label: text = "Breadcrumb") { Nav class("cx-breadcrumb") data(slot: "breadcrumb", trl: $label) aria(label: $label) { Stack class("cn-breadcrumb-list cx-bc-list") data(slot: "breadcrumb-list") aria(role: "list") { slot } } }
6
6
  part BreadcrumbLink(label: text, to: text) { Link "{$label}" -> "{$to}" class("cn-breadcrumb-link cx-bc-item") data(slot: "breadcrumb-link") }
7
7
  part BreadcrumbPage(label: text) { Span "{$label}" class("cn-breadcrumb-page cx-bc-item") data(slot: "breadcrumb-page") aria(current: "page") }
8
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" } }
9
+ part BreadcrumbEllipsis(label: text = "More pages") { Stack class("cn-breadcrumb-ellipsis cx-bc-more") data(slot: "breadcrumb-ellipsis", name: $label, trl: $label, trn: $label) aria(label: $label) { Icon "lucide:ellipsis" } }
@@ -1,9 +1,9 @@
1
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" }
2
+ # Put the label (and an icon) in the slot. Pressed, it drops 1px (as shadcn); icons stay still.
3
+ # Btn(onClick: save) { Span "Save" }
4
+ # Btn(variant: "outline", size: "sm", onClick: cancel) { Icon "lucide:x" Span "Cancel" }
5
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)
6
+ # Btn(onClick: go) class("cx-fill") { Span "Mark arrival" Icon "lucide:chevron-right" } - full width (a card's primary)
7
7
  # variant: default | outline | secondary | ghost | destructive | link (default: default)
8
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
9
  # xl and 2xl are for big surfaces (a landing, a sign-in) and match the fields' xl / 2xl.
@@ -11,13 +11,15 @@
11
11
  # disabled: a bool (a state or a getter: a part argument can't be an expression) - the button can't be pressed.
12
12
  # label: the name of an icon-only button (aria-label, and the kit's tooltip on hover). Leave it empty when there is text.
13
13
  # shape: "" | pill - fully rounded ends, for the one call to action at the foot of a flow (Btn(size: "2xl", shape: "pill")).
14
+ # loading: a bool - a Spinner before the label, and the button can't be pressed meanwhile (aria-busy).
15
+ # Btn(loading: saving, onClick: save) { Span "Save" }
14
16
  # shadcn-for-muten · part · no deps · Nova classes (nova.css) + kit.css.
15
- part Btn(variant: text = "default", size: text = "default", tone: text = "", label: text = "", onClick: action, disabled: bool = false, shape: text = "") {
16
- 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, shape: $shape) aria(label: $label) disabled when $disabled { slot }
17
+ part Btn(variant: text = "default", size: text = "default", tone: text = "", label: text = "", onClick: action, disabled: bool = false, shape: text = "", loading: bool = false) {
18
+ 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, shape: $shape, loading: $loading, trl: $label, trn: $label) aria(label: $label, busy: $loading) disabled when $disabled or $loading { slot when $loading { Spinner() } }
17
19
  }
18
20
  # TriggerBtn - a Btn that only opens what sits next to it (a Popover, a DropdownMenu, a SplitButton's menu): it has no
19
21
  # action of its own, so opening never runs something else. Same variants and sizes as Btn.
20
- # DropdownMenu { DropdownTrigger { TriggerBtn { Span "Opciones" Icon "lucide:chevron-down" } } DropdownContent { … } }
22
+ # DropdownMenu { DropdownTrigger { TriggerBtn { Span "Options" Icon "lucide:chevron-down" } } DropdownContent { … } }
21
23
  part TriggerBtn(variant: text = "outline", size: text = "default", label: text = "") {
22
24
  Button class("cn-button cn-button-variant-{$variant} cn-button-size-{$size} group/button") data(slot: "button", name: $label) aria(label: $label) { slot }
23
25
  }
@@ -1,29 +1,34 @@
1
1
  // Calendar (muten Custom) - the kit's month grid (one or two months, a day or a range), from kit/core.js.
2
2
  // inputs: selected "yyyy-mm-dd" · "from/to" · or relative to today: "today", "0", "-6/0" (days from today)
3
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.
4
+ // presets "reports" → Today · Last 7 days · This month · Last month beside a range
5
+ // caption "on" → the sentence under it (“You picked Thursday, October 8” / “From Oct 2 to Oct 8 · 7 days”)
6
+ // lang (optional) es | en, else the nearest lang. handlers: select("yyyy-mm-dd" | "from/to"). Same component as the reference artifact's Calendar.
7
7
  export function mount(el, inputs, on) {
8
8
  let cal = null;
9
- import('@muten/shadcn/registry/kit/core.js').then(({ kit, ui, today, addDays, DAYLONG, MON, fmtDay }) => {
9
+ import('@muten/shadcn/registry/kit/core.js').then((core) => core.relang(el, inputs, () => {
10
+ const { kit, ui, today, addDays, t, langOf } = core;
11
+ // the language: the one passed, else the nearest lang (the page's), English by default; repainted when it changes
12
+ const lang = inputs.lang || langOf(el), w = kit.words(lang), es = lang === 'es';
13
+ const fmtDay = (d) => (es ? `${d.getDate()} ${w.mon[d.getMonth()].slice(0, 3)}` : `${w.mon[d.getMonth()].slice(0, 3)} ${d.getDate()}`);
10
14
  const iso = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
11
15
  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
16
  const mode = inputs.mode === 'range' ? 'range' : 'single';
13
17
  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` : '');
18
+ const sayOne = (d) => (d ? (es ? t(lang, 'You picked {0}, {1}', w.dayL[d.getDay()].toLowerCase(), `${d.getDate()} de ${w.mon[d.getMonth()]}`) : t(lang, 'You picked {0}, {1}', w.dayL[d.getDay()], `${w.mon[d.getMonth()]} ${d.getDate()}`)) : '');
19
+ const sayRange = (r) => (r?.[0] && r?.[1] ? t(lang, 'From {0} to {1} · {2} days', fmtDay(r[0]), fmtDay(r[1]), Math.round((r[1] - r[0]) / 864e5) + 1) : r?.[0] ? t(lang, 'From {0}: pick the last day', fmtDay(r[0])) : '');
16
20
  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)]]];
21
+ const PRESETS = [[t(lang, 'Today'), () => [today, today]], [t(lang, 'Last 7 days'), () => [addDays(today, -6), today]], [t(lang, 'This month'), () => [new Date(today.getFullYear(), today.getMonth(), 1), today]], [t(lang, 'Last month'), () => [new Date(today.getFullYear(), today.getMonth() - 1, 1), new Date(today.getFullYear(), today.getMonth(), 0)]]];
18
22
  const withPresets = mode === 'range' && inputs.presets === 'reports';
19
23
  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
24
  const out = el.querySelector('[data-out]');
21
25
  const value = decode(inputs.selected);
22
26
  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,
27
+ cal = kit.calendar(el.querySelector('[data-cal]'), { ...kit.calWords(lang), mode, months: Number(inputs.months) === 2 ? 2 : 1, value, min: inputs.min ? one(inputs.min) : null,
24
28
  onChange: (v) => { if (out) out.textContent = say(v); el.querySelectorAll('[data-preset]').forEach((b) => b.setAttribute('aria-pressed', 'false')); emit(v); } });
25
29
  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
- });
30
+ // on the presets' own box (made again on a repaint), so a repaint never stacks a second listener
31
+ el.querySelector('.cx-range-presets')?.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); });
32
+ }));
28
33
  return () => {};
29
34
  }
@@ -2,7 +2,7 @@
2
2
  # onSelect gets "yyyy-mm-dd" (single) or "from/to" (range).
3
3
  # Calendar(selected: "today", min: "today", caption: "on", onSelect: pickDay)
4
4
  # Calendar(selected: "-6/0", mode: "range", months: 2, presets: "reports", caption: "on", onSelect: pickRange)
5
- # selected/min take dates or days from today ("today", "-6/0"). presets "reports" adds Hoy · Últimos 7 días · Este
5
+ # selected/min take dates or days from today ("today", "-6/0"). presets "reports" adds Today · Last 7 days · This
6
6
  # mes · Mes pasado beside a range; caption "on" writes what was chosen under it.
7
7
  # shadcn-for-muten · Custom (kit core) · host JS is calendar.js.
8
8
  part Calendar(selected: text = "", mode: text = "single", months: number = 1, min: text = "", presets: text = "", caption: text = "", onSelect: action) {
@@ -1,6 +1,6 @@
1
1
  # Card - a surface that only CONVEYS: header (title, description, an action at the corner), content, footer.
2
2
  # Decisions go in a modal, not on the card.
3
- # Card(size: "sm") { CardHeader { CardTitle(label: "Hoy") CardDescription(label: "3 citas") CardAction { Btn(variant: "ghost", size: "icon-sm", onClick: more) { Icon "lucide:ellipsis" } } } CardContent { … } }
3
+ # Card(size: "sm") { CardHeader { CardTitle(label: "Today") CardDescription(label: "3 appointments") CardAction { Btn(variant: "ghost", size: "icon-sm", onClick: more) { Icon "lucide:ellipsis" } } } CardContent { … } }
4
4
  # size: default | sm (compact: items edge to edge)
5
5
  # shadcn-for-muten · part · no deps · Nova (.cn-card) + kit.css (size sm).
6
6
  part Card(size: text = "default") { Section class("cn-card group/card") data(slot: "card", size: $size) { slot } }
@@ -2,7 +2,7 @@
2
2
  # perView "4" or "2,4" (phone, wider) · center "on" (the active one centred, neighbours peeking) · loop "on" ·
3
3
  # autoplay ms (pauses on hover, focus, touch, hidden tab; off with reduced motion; its own pause button) ·
4
4
  # indicator dots | progress | count | thumbs · arrows bar | overlay
5
- # Carousel(label: "Productos", perView: "2,4") { each products as p { CarouselSlide { … } } }
5
+ # Carousel(label: "Products", perView: "2,4") { each products as p { CarouselSlide { … } } }
6
6
  # shadcn-for-muten · part + Custom (CarouselBehavior, kit collections) · kit.css (.cx-car).
7
7
  part Carousel(label: text, perView: text = "1", center: text = "", loop: text = "", autoplay: number = 0, indicator: text = "dots", arrows: text = "bar") {
8
8
  Section class("cx-car") aria(label: $label, roledescription: "carrusel") {
@@ -1,13 +1,13 @@
1
1
  # ChatPlay - a conversation shown, not used: the messages come in one by one (in `step` order, 1-6) and the
2
2
  # assistant closes with a confirmation card, then it starts over. Its height is the conversation's: no empty chat.
3
3
  # For a landing; the real assistant is Chatbot. The suggestions and the composer are a picture (not focusable).
4
- # ChatPlay(title: "Bruma Barbería", status: "Asistente · en línea", placeholder: "Escribe un mensaje", label: "…") {
4
+ # ChatPlay(title: "Bruma Barbershop", status: "Assistant · online", placeholder: "Write a message", label: "…") {
5
5
  # ChatPlayLog {
6
- # ChatPlayMsg(text: "Hola, ¿tienes hora hoy?", who: "in", step: 1)
6
+ # ChatPlayMsg(text: "Hi, do you have a slot today?", who: "in", step: 1)
7
7
  # ChatPlayMsg(text: "Hoy quedan 4:30 PM y 6:00 PM.", step: 2)
8
- # ChatPlayCard(title: "Corte clásico", text: "Hoy · 4:30 PM", step: 5)
8
+ # ChatPlayCard(title: "Classic cut", text: "Today · 4:30 PM", step: 5)
9
9
  # }
10
- # ChatPlayChips { ChatPlayChip(label: "¿Cuánto cuesta?") … }
10
+ # ChatPlayChips { ChatPlayChip(label: "How much is it?") … }
11
11
  # }
12
12
  # who: out (the shop and its assistant: right, dark, the default) | in (the client: left). Seen from the shop, as
13
13
  # its inbox shows it. The confirmation card is the shop's, so it sits right too. Reduced motion: the whole conversation, still.
@@ -15,7 +15,7 @@
15
15
  # (its wallpaper, bubbles with their tail, the hour and the ticks inside, its composer); theme: "" | dark (WhatsApp at night).
16
16
  # With a skin, `photo` puts the person in the header instead of the assistant's face, ChatPlayDay sets a day chip,
17
17
  # ChatPlayMsg takes a `time`, ChatPlayButtons + ChatPlayButton hang reply buttons under the message before them
18
- # (Telegram draws them as its keyboard, two per row), and ChatPlayNote adds «Visto» or who answered. The header carries
18
+ # (Telegram draws them as its keyboard, two per row), and ChatPlayNote adds “Seen” or who answered. The header carries
19
19
  # the app's back arrow and call buttons, and the composer is that app's own. With a skin it is a phone screen (300 × 600,
20
20
  # a status bar with `clock`), and Instagram puts `photo` beside the client's last message of each turn.
21
21
  # face: "" (the kit's two-dot face) | host - an empty face slot (.cx-cp-face-host) for the page's own face, a brand
@@ -47,7 +47,7 @@ part ChatPlay(title: text, status: text = "", placeholder: text = "", label: tex
47
47
  Span "{$placeholder}" class("cx-cp-ph")
48
48
  Stack class("cx-cp-tools") { Icon "lucide:paperclip" Icon "lucide:camera" }
49
49
  Stack class("cx-cp-igtools") { Icon "lucide:mic" Icon "lucide:image" Icon "lucide:sticker" }
50
- Stack class("cx-cp-menu") { Icon "lucide:menu" Span "Menú" }
50
+ Stack class("cx-cp-menu") { Icon "lucide:menu" Span "Menu" data(tr: "Menu") }
51
51
  Stack class("cx-cp-send") { Icon "lucide:send-horizontal" }
52
52
  Stack class("cx-cp-mic") { Icon "lucide:mic" }
53
53
  }
@@ -65,9 +65,9 @@ part ChatPlayMsg(text: text, who: text = "out", step: number = 1, time: text = "
65
65
  }
66
66
  }
67
67
  part ChatPlayDay(label: text) { Span "{$label}" class("cx-cp-day") }
68
- # The start of a conversation as Instagram shows it: the person's photo (ChatPlay's `photo`), name, handle and «Ver perfil».
68
+ # The start of a conversation as Instagram shows it: the person's photo (ChatPlay's `photo`), name, handle and “See profile”.
69
69
  part ChatPlayProfile(name: text, sub: text = "", action: text = "") { Stack class("cx-cp-prof") { Span "" class("cx-cp-prof-ph") Span "{$name}" class("cx-cp-prof-n") Span "{$sub}" class("cx-cp-prof-s") Span "{$action}" class("cx-cp-prof-a") } }
70
- # A small line under a message: Instagram's «Visto», or who answered («Respondió el asistente de Bruma»).
70
+ # A small line under a message: Instagram's “Seen”, or who answered (“Bruma's assistant answered”).
71
71
  part ChatPlayNote(label: text, step: number = 1) { Span "{$label}" class("cx-cp-note") data(step: $step) }
72
72
  part ChatPlayButtons(step: number = 1) { Stack class("cx-cp-btns") data(step: $step) { slot } }
73
73
  part ChatPlayButton(label: text) { Stack class("cx-cp-btn") { Icon "lucide:reply" Span "{$label}" } }
@@ -1,9 +1,10 @@
1
- # Chatbot - the assistant: greets, offers suggestions, «thinks», writes the answer live (with Stop), can carry an
1
+ # Chatbot - the assistant: greets, offers suggestions, “thinks”, writes the answer live (with Stop), can carry an
2
2
  # action button inside the answer (onAction gets its label), rated with 👍 / 👎. answers: {match (regex), text, action,
3
3
  # primary}; the one without match is the fallback.
4
- # Chatbot(title: "Rober", greeting: "Hola, soy Rober…", suggestions: @ideas, answers: @answers, onAction: act)
4
+ # Chatbot(title: "Rober", greeting: "Hi, I'm Rober…", suggestions: @ideas, answers: @answers, onAction: act)
5
+ # avatar: an image, a logo or a GIF url in place of the drawn face (“/logo.svg”, “/rober.gif”).
5
6
  # demo "on": it taps its own suggestions in turn and answers them live, for a landing; the first touch stops it.
6
7
  # shadcn-for-muten · part over KitWidget (kit core).
7
- part Chatbot(title: text, greeting: text, suggestions: list, answers: list, demo: text = "", onAction: action) {
8
- Custom KitWidget inputs(name: "chatbot", title: $title, greeting: $greeting, suggestions: $suggestions, answers: $answers, demo: $demo) on(action: $onAction) class("cx-bot")
8
+ part Chatbot(title: text, greeting: text, suggestions: list, answers: list, demo: text = "", avatar: text = "", onAction: action) {
9
+ Custom KitWidget inputs(name: "chatbot", title: $title, greeting: $greeting, suggestions: $suggestions, answers: $answers, demo: $demo, avatar: $avatar) on(action: $onAction) class("cx-bot")
9
10
  }
@@ -1,5 +1,5 @@
1
1
  # Checklist - first steps with progress: done steps checked, the next ones with their action (onAction gets its label).
2
- # Checklist(title: "Tu tienda en 5 pasos", steps: @steps, onAction: act) steps: {title, desc, done, action}
2
+ # Checklist(title: "Your shop in 5 steps", steps: @steps, onAction: act) steps: {title, desc, done, action}
3
3
  # shadcn-for-muten · part over KitWidget (kit core).
4
4
  part Checklist(title: text, steps: list, onAction: action) {
5
5
  Custom KitWidget inputs(name: "checklist", title: $title, steps: $steps) on(action: $onAction) class("cx-onboard")