@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
@@ -3,15 +3,21 @@
3
3
  # whether it is folded. (AppShell is the whole frame - sidebar, header, sheet, ⌘K - built from data.)
4
4
  # AppSidebar(collapsed: folded) {
5
5
  # SidebarHeader { … }
6
- # SidebarGroup(label: "Principal") { SidebarItem(label: "Agenda", value: "agenda", current: place, count: "14", onSelect: go) { Icon "lucide:calendar" } }
6
+ # SidebarGroup(label: "Primary") { SidebarItem(label: "Schedule", value: "agenda", current: place, count: "14", onSelect: go) { Icon "lucide:calendar" } }
7
7
  # SidebarFooter { … }
8
8
  # }
9
- # SidebarLink is the same item as a link to a route: the app marks where you are by itself (no `current` to keep).
10
- # SidebarLink(label: "Citas", to: "/schedule", count: "6") { Icon "lucide:calendar-days" }
9
+ # SidebarLink is the same item as a link to a route: the app marks where you are by itself (no `current` to keep), also
10
+ # below it (on /schedule/42 the “Appointments” link stays marked). `match` lists other pages it stands for ("/a /b").
11
+ # SidebarLink(label: "Appointments", to: "/schedule", count: "6") { Icon "lucide:calendar-days" }
11
12
  # The name AppSidebar avoids muten's own Sidebar primitive. shadcn-for-muten · parts · no deps · Nova (.cn-sidebar-*) + kit.css.
12
- part AppSidebar(collapsed: bool, label: text = "Menú") {
13
+ part AppSidebar(collapsed: bool, label: text = "Menu") {
13
14
  Nav class("cx-sidebar") data(slot: "sidebar", collapsed: $collapsed) aria(label: $label) { Stack class("cn-sidebar-inner cx-sb-inner") data(slot: "sidebar-inner") { slot } }
14
15
  }
16
+ # SidebarTrigger - the fold toggle (shadcn's SidebarTrigger): an icon button that lives in the icon column, so it is
17
+ # there folded or open. The page owns the state. SidebarHeader { SidebarTrigger(collapsed: folded, onToggle: toggleFold) }
18
+ part SidebarTrigger(collapsed: bool, onToggle: action) {
19
+ Button -> $onToggle class("cn-button cn-button-variant-ghost cn-button-size-icon-sm group/button cx-sb-trigger") data(slot: "sidebar-trigger", name: "Show or hide the menu", trl: "Show or hide the menu", trn: "Show or hide the menu") aria(label: "Show or hide the menu", expanded: not $collapsed) { Icon "lucide:panel-left" }
20
+ }
15
21
  part SidebarHeader() { Stack class("cn-sidebar-header cx-sb-head") data(slot: "sidebar-header") { slot } }
16
22
  part SidebarFooter() { Stack class("cn-sidebar-footer cx-sb-foot") data(slot: "sidebar-footer") { slot } }
17
23
  part SidebarGroup(label: text = "") {
@@ -27,8 +33,8 @@ part SidebarItem(label: text, value: text, current: text, onSelect: action, coun
27
33
  Span "{$count}" class("cn-sidebar-menu-badge cx-sb-count") data(slot: "sidebar-menu-badge")
28
34
  }
29
35
  }
30
- part SidebarLink(label: text, to: text, count: text = "") {
31
- Link -> "{$to}" class("cn-sidebar-menu-button cn-sidebar-menu-button-variant-default cn-sidebar-menu-button-size-default cx-sb-item") data(slot: "sidebar-menu-button", name: $label) {
36
+ part SidebarLink(label: text, to: text, count: text = "", match: text = "") {
37
+ Link -> "{$to}" class("cn-sidebar-menu-button cn-sidebar-menu-button-variant-default cn-sidebar-menu-button-size-default cx-sb-item") data(slot: "sidebar-menu-button", name: $label, match: $match) {
32
38
  slot
33
39
  Span "{$label}" class("cx-sb-label")
34
40
  Span "{$count}" class("cn-sidebar-menu-badge cx-sb-count") data(slot: "sidebar-menu-badge")
@@ -4,20 +4,20 @@
4
4
  # theme, so it sits on any skin, light or dark.
5
5
  # SiteFooter {
6
6
  # FooterHead {
7
- # FooterBrand(tag: "Vende y agenda por ti.") { Link "" -> "/" aria(label: "Inicio") { Image "/logo.svg" alt("Storio") } }
8
- # FooterActions { LinkBtn(to: "/login", variant: "outline") { Span "Entrar" } LinkBtn(to: "/signup") { Span "Crear mi tienda" } }
7
+ # FooterBrand(tag: "Sells and books for you.") { Link "" -> "/" aria(label: "Home") { Image "/logo.svg" alt("Storio") } }
8
+ # FooterActions { LinkBtn(to: "/login", variant: "outline") { Span "Sign in" } LinkBtn(to: "/signup") { Span "Create my shop" } }
9
9
  # }
10
10
  # FooterLinks {
11
- # FooterCol(title: "Producto") { Link "Qué es" -> "/product" Link "Precios" -> "/pricing" }
12
- # FooterCol(title: "Legal") { Link "Términos" -> "/terms" Link "Privacidad" -> "/privacy" }
11
+ # FooterCol(title: "Product") { Link "What it is" -> "/product" Link "Prices" -> "/pricing" }
12
+ # FooterCol(title: "Legal") { Link "Terms" -> "/terms" Link "Privacy" -> "/privacy" }
13
13
  # }
14
14
  # FooterBar { Span "© 2026 Candhrly LLC" Link "hello@storio.ink" -> "mailto:hello@storio.ink" }
15
15
  # }
16
16
  # FooterCol(wide: "on") takes two columns of links on a phone and one long column on a desk (a long list, like trades).
17
17
  # The bar puts its first child on the left and the rest on the right.
18
18
  # shadcn-for-muten · part · no deps · kit.css (.cx-foot).
19
- part SiteFooter(label: text = "Pie de página") {
20
- Footer class("cx-foot") aria(label: $label) { Stack class("cx-foot-in") { slot } }
19
+ part SiteFooter(label: text = "Footer") {
20
+ Footer class("cx-foot") data(trl: $label) aria(label: $label) { Stack class("cx-foot-in") { slot } }
21
21
  }
22
22
  part FooterHead() { Stack class("cx-foot-head") { slot } }
23
23
  part FooterBrand(tag: text = "") {
@@ -1,37 +1,84 @@
1
- // SortableBehavior (muten Custom) - reorder the Items of a Sortable: pointer (mouse + touch) on the grip, the others
2
- // slide to make room; or focus the grip and use Up/Down. Announces «<title>: posición n de m» and emits the keys in
3
- // their new order. Same feel as the reference artifact's sortable list.
1
+ // SortableBehavior (muten Custom) - reorder the Items of a Sortable, with a GHOST of the row already where it will
2
+ // land. Pointer (mouse + touch) on the grip, or focus the grip and use Up/Down. Announces “<title>: position n of m”
3
+ // and emits the keys in their new order. Motion on the kit's tokens (kit.css “Sortable”):
4
+ // · the row taken lifts (2 % bigger, a deeper shadow) and follows the pointer, kept within the list
5
+ // · the others slide apart (--t-move) and the ghost (the row's own copy, dashed, its content faded) glides to the
6
+ // slot the row will take, in step with them; rows of different heights are measured, not assumed
7
+ // · on release the row settles exactly onto the ghost (200 ms), the ghost fades, THEN the order is committed with
8
+ // nothing moving, so there is no jump; Escape puts it back where it was
9
+ // · the keyboard moves it with no drag animation (keyboard actions never animate)
4
10
  export function mount(el, inputs, on) {
5
11
  const sort = el.parentElement;
12
+ let I = null; const T = (k, ...a) => (I ? I.t(I.langOf(sort), k, ...a) : a.reduce((x, v, n) => x.replace(`{${n}}`, v), k));
13
+ // the grips' names: “Move <title>” in the list's language (muten cannot interpolate inside aria(label: "…")), said
14
+ // again when a lang changes
15
+ const name = () => sort.querySelectorAll('.cx-grip').forEach((g) => { const it = g.closest('.cx-item'); const title = g.dataset.title || it?.querySelector('.cn-item-title')?.textContent || ''; g.setAttribute('aria-label', T('Move {0}', title)); g.title = T('Drag to reorder'); });
16
+ import('@muten/shadcn/registry/kit/i18n.js').then((m) => { I = m; name(); });
17
+ document.addEventListener('kit:lang', () => { if (sort.isConnected) name(); });
6
18
  const live = document.createElement('span'); live.className = 'cx-sr'; live.setAttribute('aria-live', 'polite'); sort.after(live);
7
19
  const items = () => [...sort.querySelectorAll(':scope > .cx-item')];
8
- sort.querySelectorAll('.cx-grip').forEach((g) => { g.tabIndex = 0; g.title = 'Arrastra para ordenar'; });
20
+ const settleMs = () => (parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--t-move')) > 1 ? 200 : 1);
21
+ sort.querySelectorAll('.cx-grip').forEach((g) => { g.tabIndex = 0; });
22
+ name();
9
23
  const done = (item) => {
10
- const all = items(); live.textContent = `${item.querySelector('.cn-item-title')?.textContent || ''}: posición ${all.indexOf(item) + 1} de ${all.length}`;
24
+ const all = items(); live.textContent = T('{0}: position {1} of {2}', item.querySelector('.cn-item-title')?.textContent || '', all.indexOf(item) + 1, all.length);
11
25
  on.reorder?.(all.map((x) => x.dataset.key).join(','));
12
26
  };
27
+
13
28
  sort.addEventListener('pointerdown', (e) => {
14
- const grip = e.target.closest('.cx-grip'); if (!grip) return;
29
+ const grip = e.target.closest('.cx-grip'); if (!grip || e.button > 0) return;
15
30
  e.preventDefault();
16
- const item = grip.closest('.cx-item'), all = items();
17
- const rects = all.map((x) => x.getBoundingClientRect()), start = e.clientY, from = all.indexOf(item);
18
- const step = rects[1] ? rects[1].top - rects[0].top : rects[0].height + 8;
31
+ const item = grip.closest('.cx-item'), all = items(), from = all.indexOf(item);
32
+ const tops = all.map((x) => x.offsetTop), heights = all.map((x) => x.offsetHeight);
33
+ const gap = parseFloat(getComputedStyle(sort).rowGap) || 0, h = heights[from], room = h + gap;
34
+ const others = all.filter((x) => x !== item);
35
+ const mids = others.map((x) => x.offsetTop + x.offsetHeight / 2);
36
+ // where the row's top lands when it takes index k among the others
37
+ const topAt = (k) => tops[0] + others.slice(0, k).reduce((sum, x) => sum + x.offsetHeight + gap, 0);
38
+ const startY = e.clientY, minDy = tops[0] - tops[from] - room / 2, maxDy = topAt(others.length) - tops[from] + room / 2;
19
39
  let to = from;
20
- item.classList.add('lifting'); grip.setPointerCapture(e.pointerId);
40
+
41
+ // the ghost: the row's own copy, the same box in the same place, dashed and faded; inert and silent
42
+ const ghost = item.cloneNode(true);
43
+ ghost.classList.add('cx-sort-ghost'); ghost.removeAttribute('data-key'); ghost.setAttribute('aria-hidden', 'true'); ghost.inert = true;
44
+ ghost.style.left = item.offsetLeft + 'px'; ghost.style.width = item.offsetWidth + 'px'; ghost.style.translate = `0 ${tops[from]}px`;
45
+ sort.appendChild(ghost);
46
+ requestAnimationFrame(() => ghost.classList.add('on'));
47
+
48
+ item.classList.add('lifting'); document.documentElement.classList.add('cx-sorting');
49
+ grip.setPointerCapture(e.pointerId);
50
+ const place = () => {
51
+ others.forEach((x, k) => { const i = all.indexOf(x); let shift = 0; if (from < to && i > from && i <= to) shift = -room; if (from > to && i < from && i >= to) shift = room; x.style.translate = shift ? `0 ${shift}px` : ''; });
52
+ ghost.style.translate = `0 ${topAt(to)}px`;
53
+ };
21
54
  const move = (ev) => {
22
- const dy = ev.clientY - start; item.style.transform = `translateY(${dy}px)`;
23
- to = Math.max(0, Math.min(all.length - 1, from + Math.round(dy / step)));
24
- all.forEach((x, i) => { if (x === item) return; let shift = 0; if (from < to && i > from && i <= to) shift = -step; if (from > to && i < from && i >= to) shift = step; x.style.transform = shift ? `translateY(${shift}px)` : ''; });
55
+ const dy = Math.max(minDy, Math.min(maxDy, ev.clientY - startY));
56
+ item.style.translate = `0 ${dy}px`;
57
+ const center = tops[from] + h / 2 + dy;
58
+ const next = mids.filter((m) => m < center).length;
59
+ if (next !== to) { to = next; place(); }
25
60
  };
26
- const up = () => {
27
- grip.removeEventListener('pointermove', move); grip.removeEventListener('pointerup', up); grip.removeEventListener('pointercancel', up);
28
- all.forEach((x) => { x.style.transition = 'none'; x.style.transform = ''; });
29
- const ref = all[to]; if (to !== from) sort.insertBefore(item, from < to ? ref.nextSibling : ref);
30
- item.classList.remove('lifting'); void sort.offsetWidth; all.forEach((x) => { x.style.transition = ''; });
31
- if (to !== from) done(item);
61
+ const finish = (commit) => {
62
+ grip.removeEventListener('pointermove', move); grip.removeEventListener('pointerup', up); grip.removeEventListener('pointercancel', cancel); removeEventListener('keydown', esc, true);
63
+ if (!commit) { to = from; place(); }
64
+ item.classList.remove('lifting'); item.classList.add('settling');
65
+ item.style.translate = `0 ${topAt(to) - tops[from]}px`;
66
+ ghost.classList.remove('on');
67
+ setTimeout(() => {
68
+ all.forEach((x) => { x.style.transition = 'none'; x.style.translate = ''; });
69
+ item.classList.remove('settling'); ghost.remove(); document.documentElement.classList.remove('cx-sorting');
70
+ // right before the row it now precedes, or right after the last one (never past the list's own markers)
71
+ if (to !== from) { if (others[to]) sort.insertBefore(item, others[to]); else others[others.length - 1].after(item); }
72
+ void sort.offsetWidth; all.forEach((x) => { x.style.transition = ''; });
73
+ if (to !== from) done(item);
74
+ }, settleMs());
32
75
  };
33
- grip.addEventListener('pointermove', move); grip.addEventListener('pointerup', up); grip.addEventListener('pointercancel', up);
76
+ const up = () => finish(true);
77
+ const cancel = () => finish(false);
78
+ const esc = (ev) => { if (ev.key === 'Escape') { ev.preventDefault(); ev.stopPropagation(); finish(false); } };
79
+ grip.addEventListener('pointermove', move); grip.addEventListener('pointerup', up); grip.addEventListener('pointercancel', cancel); addEventListener('keydown', esc, true);
34
80
  });
81
+
35
82
  sort.addEventListener('keydown', (e) => {
36
83
  const grip = e.target.closest('.cx-grip'); if (!grip || !['ArrowUp', 'ArrowDown'].includes(e.key)) return;
37
84
  e.preventDefault(); const item = grip.closest('.cx-item');
@@ -5,7 +5,9 @@ export function mount(el, inputs, on) {
5
5
  const esc = (s) => String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c]);
6
6
  const GRIP = '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="9" cy="6" r="1.5"/><circle cx="9" cy="12" r="1.5"/><circle cx="9" cy="18" r="1.5"/><circle cx="15" cy="6" r="1.5"/><circle cx="15" cy="12" r="1.5"/><circle cx="15" cy="18" r="1.5"/></svg>';
7
7
  const names = String(inputs.items || '').split(',').map((s) => s.trim()).filter(Boolean);
8
- el.innerHTML = `<div class="cx-sortable" aria-label="${esc(inputs.label || 'Orden')}">${names.map((n) => `<div class="cn-item cn-item-variant-outline cn-item-size-sm group/item cx-item" data-slot="item" data-key="${esc(n)}"><span class="cn-button cn-button-variant-ghost cn-button-size-icon-xs cx-grip" role="button" aria-label="Mover ${esc(n)}" data-grip="true">${GRIP}</span><div class="cn-item-content"><span class="cn-item-title">${esc(n)}</span></div></div>`).join('')}<span class="cx-sl-anchor"></span></div>`;
8
+ el.innerHTML = `<div class="cx-sortable" aria-label="${esc(inputs.label || 'Order')}" data-trl="${esc(inputs.label || 'Order')}">${names.map((n) => `<div class="cn-item cn-item-variant-outline cn-item-size-sm group/item cx-item" data-slot="item" data-key="${esc(n)}"><span class="cn-button cn-button-variant-ghost cn-button-size-icon-xs cx-grip" role="button" aria-label="Move ${esc(n)}" data-trl="Move {0}" data-tra="${esc(n)}" data-grip="true">${GRIP}</span><div class="cn-item-content"><span class="cn-item-title">${esc(n)}</span></div></div>`).join('')}<span class="cx-sl-anchor"></span></div>`;
9
+ if (inputs.lang) el.setAttribute('lang', inputs.lang);
10
+ import('@muten/shadcn/registry/kit/i18n.js').then((m) => m.localizeTree(el));
9
11
  import('@muten/shadcn/registry/sortable-behavior.js').then((b) => b.mount(el.querySelector('.cx-sl-anchor'), {}, { reorder: (v) => on.reorder?.(v) }));
10
12
  return () => {};
11
13
  }
@@ -1,6 +1,6 @@
1
1
  # SortableList - plain names you put in order (the layers of a design, the steps of a flow) from comma-joined text:
2
2
  # the same rows and feel as Sortable (the grip, the others making room, Up/Down on the focused grip).
3
3
  # state { layers = "Fondo,Jugador,Interfaz" : text } action reorder(v: text) mutates layers { layers.set(v) }
4
- # SortableList(items: layers, label: "Capas", onReorder: reorder)
4
+ # SortableList(items: layers, label: "Layers", onReorder: reorder)
5
5
  # For rows with a price or details use Sortable with SortItem. shadcn-for-muten · Custom · sortable-list.js.
6
- part SortableList(items: text, label: text = "Orden", onReorder: action) { Custom SortableList inputs(items: $items, label: $label) on(reorder: $onReorder) class("cx-slist-host") }
6
+ part SortableList(items: text, label: text = "Order", onReorder: action) { Custom SortableList inputs(items: $items, label: $label) on(reorder: $onReorder) class("cx-slist-host") }
@@ -1,14 +1,15 @@
1
1
  # Sortable - Items you reorder: take the grip with a mouse or a finger, or focus it and use the arrows. The others
2
- # make room as you drag; a screen reader hears the new position. onReorder gets the keys in their new order
2
+ # slide apart as you drag and a ghost of the row already sits where it will land (it settles onto it on release;
3
+ # Escape puts it back); a screen reader hears the new position. onReorder gets the keys in their new order
3
4
  # ("cut,beard,trim") so the page can save it.
4
- # Sortable(label: "Orden de los servicios en tu tienda", onReorder: saveOrder) {
5
- # SortItem(key: "cut", title: "Corte clásico", price: "$25.000")
5
+ # Sortable(label: "Order of the services in your shop", onReorder: saveOrder) {
6
+ # SortItem(key: "cut", title: "Classic cut", price: "$25.000")
6
7
  # }
7
8
  # shadcn-for-muten · part + Custom (SortableBehavior) · kit.css (.cx-sortable).
8
9
  part Sortable(label: text, onReorder: action) { Stack class("cx-sortable") aria(label: $label) { slot SortableBehavior(onReorder: $onReorder) } }
9
10
  part SortItem(key: text, title: text, price: text = "") {
10
11
  Stack class("cn-item cn-item-variant-outline cn-item-size-sm group/item cx-item") data(slot: "item", key: $key) {
11
- Stack class("cn-button cn-button-variant-ghost cn-button-size-icon-xs cx-grip") aria(role: "button", label: "Mover {$title}") data(grip: "true") { Icon "lucide:grip-vertical" }
12
+ Stack class("cn-button cn-button-variant-ghost cn-button-size-icon-xs cx-grip") aria(role: "button", label: $title) data(grip: "true", title: $title) { Icon "lucide:grip-vertical" }
12
13
  Stack class("cn-item-content") data(slot: "item-content") { Span "{$title}" class("cn-item-title") data(slot: "item-title") }
13
14
  Span "{$price}" class("cx-price")
14
15
  }
@@ -1,4 +1,4 @@
1
1
  # Spinner - a loading mark for a button or a line, never for a whole screen (that is a Skeleton).
2
- # Btn(onClick: save) disabled… { Spinner() Span "Guardando…" }
2
+ # Btn(onClick: save) disabled… { Spinner() Span "Saving…" }
3
3
  # shadcn-for-muten · part · no deps · Nova (.cn-spinner).
4
- part Spinner() { Icon "lucide:loader-circle" class("cn-spinner animate-spin") aria(label: "Cargando", role: "status") }
4
+ part Spinner() { Icon "lucide:loader-circle" class("cn-spinner animate-spin") data(trl: "Loading") aria(label: "Loading", role: "status") }
@@ -1,8 +1,8 @@
1
1
  # SplitButton - one main action and, joined to it, a chevron with the other ways to do it (save / save as draft).
2
2
  # The main action runs on its own; the menu takes DropdownItem parts.
3
- # SplitButton(label: "Publicar", onClick: publish) { DropdownItem(label: "Guardar como borrador", onSelect: draft) DropdownItem(label: "Programar…", onSelect: schedule) }
3
+ # SplitButton(label: "Publish", onClick: publish) { DropdownItem(label: "Save as draft", onSelect: draft) DropdownItem(label: "Schedule…", onSelect: schedule) }
4
4
  # variant: as Btn. shadcn-for-muten · part over ButtonGroup + DropdownMenu.
5
- part SplitButton(label: text, onClick: action, variant: text = "default", size: text = "default", menu: text = "Más opciones") {
5
+ part SplitButton(label: text, onClick: action, variant: text = "default", size: text = "default", menu: text = "More options") {
6
6
  ButtonGroup(label: $label) {
7
7
  Btn(variant: $variant, size: $size, onClick: $onClick) { Span "{$label}" }
8
8
  ButtonGroupSeparator()
@@ -1,4 +1,4 @@
1
- // StackedBars (muten Custom) - bars over time with one or more series stacked in each (citas + pedidos by month), an
1
+ // StackedBars (muten Custom) - bars over time with one or more series stacked in each (appointments + pedidos by month), an
2
2
  // axis in the page's unit (USD 8K), a bar you can pick (it says so with aria-pressed and sends its id), a note over the
3
3
  // plot while there is nothing to show, and a skeleton while it loads. The data changes in place: the bars slide to their
4
4
  // new height, nothing blinks. inputs: rows [{id, label, <key>: number, …}], series [{key, label, tone}], selected,
@@ -29,7 +29,7 @@ export function mount(el, inputs, on) {
29
29
  };
30
30
 
31
31
  const paint = () => {
32
- const rows = list(state.rows), series = list(state.series), unit = state.unit || '', locale = state.locale || 'es';
32
+ const rows = list(state.rows), series = list(state.series), unit = state.unit || '', locale = state.locale || (el.closest('[lang]')?.getAttribute('lang')) || 'en';
33
33
  const totals = rows.map((r) => series.reduce((sum, s) => sum + (Number(r[s.key]) || 0), 0));
34
34
  const top = niceTop(Math.max(0, ...totals)), busy = !!state.busy, none = !busy && totals.every((t) => t === 0);
35
35
  const legend = series.length > 1 ? `<ul class="${NS}-legend">${series.map((s) => `<li data-tone="${esc(s.tone || 'brand')}">${esc(s.label)}</li>`).join('')}</ul>` : '';
@@ -46,7 +46,7 @@ export function mount(el, inputs, on) {
46
46
  const n = busy ? Math.max(rows.length, 7) : rows.length;
47
47
  el.innerHTML = `${legend}<div class="${NS}-body"><div class="${NS}-y" aria-hidden="true">${ticks}</div><div class="${NS}-plot"><div class="${NS}-grid" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i></div><div class="${NS}-bars" role="group" aria-label="${esc(state.label || '')}" style="--n:${n}"${busy ? ' aria-busy="true"' : ''}>${bars}</div>${none && state.empty ? `<div class="cx-veil-note"><div class="cx-veil-box">${state.emptyTitle ? `<span class="cx-veil-t">${esc(state.emptyTitle)}</span>` : ''}<span class="cx-veil-s">${esc(state.empty)}</span></div></div>` : ''}</div><span></span><div class="${NS}-x" aria-hidden="true" style="--n:${n}">${xs}</div></div>`;
48
48
  if (!drawn && !busy && rows.length && !matchMedia('(prefers-reduced-motion: reduce)').matches) {
49
- el.querySelectorAll(`.${NS}-stack`).forEach((s, i) => s.animate([{ transform: 'scaleY(0)' }, { transform: 'scaleY(1)' }], { duration: 480, delay: i * 24, easing: 'cubic-bezier(.22,1,.36,1)', fill: 'backwards' }));
49
+ el.querySelectorAll(`.${NS}-stack`).forEach((s, i) => s.animate([{ transform: 'scaleY(0)' }, { transform: 'scaleY(1)' }], { duration: 480, delay: i * 24, easing: 'cubic-bezier(.23, 1, .32, 1)', fill: 'backwards' }));
50
50
  drawn = true;
51
51
  }
52
52
  };
@@ -1,8 +1,8 @@
1
- # StackedBars - bars over time with one or more series stacked in each: the year by month, citas and pedidos together.
1
+ # StackedBars - bars over time with one or more series stacked in each: the year by month, appointments and pedidos together.
2
2
  # The axis is in the page's unit (USD 8K), a bar can be picked (it is pressed, and onPick gets its id), while nothing
3
3
  # is there yet a calm note sits over the plot (`empty`, with `emptyTitle` on top), and while it loads (`busy`) a skeleton keeps the shape. When the
4
4
  # data changes the bars slide to their new height. Each bar says its values to a screen reader and on hover.
5
- # StackedBars(rows: @months, series: @kinds, selected: monthId, unit: "USD", onPick: pickMonth, label: "Ventas por mes")
5
+ # StackedBars(rows: @months, series: @kinds, selected: monthId, unit: "USD", onPick: pickMonth, label: "Sales by month")
6
6
  # rows {id, label, <key>: number…} · series {key, label, tone}: tone brand | coral | teal | amber | blue | pink, the
7
7
  # series palette (the first series sits at the bottom). One series is a plain bar chart. A legend shows from two.
8
8
  # readonly: true shows the pick but lets no bar be pressed (a plan that cannot change month).
@@ -1,10 +1,10 @@
1
1
  # StampCard - a loyalty card: stamps toward a reward (good for any shop).
2
- # StampCard(store: "Barbería El Roble", owner: "Laura", total: 10, stamps: 7, reward: "un corte gratis")
2
+ # StampCard(store: "El Roble Barbershop", owner: "Laura", total: 10, stamps: 7, reward: "a free haircut")
3
3
  # The words are templates (%owner %n %total %left %reward); the defaults are Spanish:
4
4
  # StampCard(…, title: "%owner's card", progress: "%n of %total · %left to go for %reward", complete: "Complete! %reward earned.",
5
5
  # count: "%n of %total stamps", redeem: "Redeem reward", stamp: "Stamp a visit")
6
6
  # demo "on": it stamps itself one visit at a time and starts over, for a landing; the first touch stops it.
7
7
  # shadcn-for-muten · part over KitWidget (kit core).
8
- part StampCard(store: text, owner: text, total: number = 10, stamps: number, reward: text, demo: text = "", title: text = "Cartilla de %owner", progress: text = "%n de %total · faltan %left para %reward", complete: text = "¡Completa! Ganó %reward.", count: text = "%n de %total sellos", redeem: text = "Canjear premio", stamp: text = "Sellar visita") {
8
+ part StampCard(store: text, owner: text, total: number = 10, stamps: number, reward: text, demo: text = "", title: text = "%owner's card", progress: text = "%n of %total · %left more for %reward", complete: text = "Complete! Earned %reward.", count: text = "%n of %total stamps", redeem: text = "Redeem reward", stamp: text = "Stamp a visit") {
9
9
  Custom KitWidget inputs(name: "stampCard", store: $store, owner: $owner, total: $total, stamps: $stamps, reward: $reward, demo: $demo, title: $title, progress: $progress, complete: $complete, count: $count, redeem: $redeem, stamp: $stamp) class("cx-stamp-host")
10
10
  }
@@ -1,9 +1,9 @@
1
1
  # StatBand - a row of big numbers split by hairlines (two per row on a phone). Each Stat holds its value in the slot
2
2
  # (a Span, or a CountUp that counts up when it comes into view) and its label under it.
3
- # StatBand { Stat(label: "citas al mes") { CountUp(to: "12,400") } Stat(label: "tiendas") { Span "850" } }
3
+ # StatBand { Stat(label: "appointments a month") { CountUp(to: "12,400") } Stat(label: "shops") { Span "850" } }
4
4
  # look: "tiles" puts each number on its own soft tile, left-aligned, for a card inside an app (what the bot did this
5
- # month: «0 reservas que cerró solo»); the default is the landing's band, centred between hairlines.
6
- # StatBand(look: "tiles") { Stat(label: "reservas que cerró solo") { Span "12" } Stat(label: "pedidos que tomó solo") { Span "5" } }
5
+ # month: “0 bookings it closed by itself”); the default is the landing's band, centred between hairlines.
6
+ # StatBand(look: "tiles") { Stat(label: "bookings it closed by itself") { Span "12" } Stat(label: "orders it took by itself") { Span "5" } }
7
7
  # While the numbers load, put a Skeleton in each Stat's slot.
8
8
  # shadcn-for-muten · parts · no deps · kit.css (.cx-statband).
9
9
  part StatBand(look: text = "") { Stack class("cx-statband") data(look: $look) { slot } }
@@ -1,10 +1,10 @@
1
1
  # StatCard - one number for any use (sales, bookings, wait time…): label, the value, the change against a period and
2
2
  # its line. The icon goes in the slot (or leave it empty). `tone` says if the change is good (ok) or bad (bad) -
3
3
  # for a cost or a wait, going down is the good one.
4
- # StatCard(label: "Ventas", value: "$1.284.000", delta: "▲ 12 %", tone: "ok", compare: "vs. semana pasada", series: "3,5,4,6,5,8,9") { Icon "lucide:credit-card" }
4
+ # StatCard(label: "Sales", value: "$1.284.000", delta: "▲ 12 %", tone: "ok", compare: "vs. last week", series: "3,5,4,6,5,8,9") { Icon "lucide:credit-card" }
5
5
  # A card that asks for something (what is owed, orders to confirm): `line` says what the number means and `to` + `cta`
6
6
  # is the one thing to do about it, written at the foot of the card.
7
- # StatCard(label: "Por cobrar", value: "USD 305", line: "4 citas sin marcar como pagadas", to: "/close-day", cta: "Cerrar el día") { Icon "lucide:clock" }
7
+ # StatCard(label: "To collect", value: "USD 305", line: "4 appointments not marked as paid", to: "/close-day", cta: "Close the day") { Icon "lucide:clock" }
8
8
  # busy: true keeps the card's shape with a skeleton while the number loads (the label stays).
9
9
  # Lay several in a StatGrid. Spark draws the series alone: Spark(series: "3,5,4", tone: "success").
10
10
  # shadcn-for-muten · part + Custom (Spark.js) · kit.css (.cx-stat).
@@ -20,7 +20,7 @@ part StatCard(label: text, value: text = "", delta: text = "", tone: text = "ok"
20
20
  Skeleton() class("cx-stat-sk-l")
21
21
  }
22
22
  when not $busy {
23
- Span "{$value}" class("cx-text") data(slot: "text", variant: "title")
23
+ Stack class("cx-text cx-stat-v") data(slot: "text", variant: "title") { CountUp(to: $value, roll: "change") }
24
24
  Span "{$line}" class("cx-text cx-stat-line") data(slot: "text", variant: "caption")
25
25
  Stack class("cx-stat-row cx-stat-delta") {
26
26
  Span "{$delta}" class("cn-badge cn-badge-variant-ghost group/badge") data(slot: "badge", variant: "status", tone: $tone)
@@ -1,7 +1,7 @@
1
1
  # StatusBar - the thin line under an editor that says how things stand: the tool, the position, the size, the zoom,
2
2
  # whether it is saved. StatusSpacer pushes what follows to the right; an item can carry an icon.
3
- # StatusBar { StatusItem(label: "Listo") { Icon "lucide:circle-check" } StatusSpacer() StatusItem(label: "1080 × 1350") StatusItem(label: "100 %") }
3
+ # StatusBar { StatusItem(label: "Done") { Icon "lucide:circle-check" } StatusSpacer() StatusItem(label: "1080 × 1350") StatusItem(label: "100 %") }
4
4
  # shadcn-for-muten · parts · no deps · kit.css (.cx-status).
5
- part StatusBar(label: text = "Estado") { Stack class("cx-status") data(slot: "status-bar") aria(role: "status", label: $label) { slot } }
5
+ part StatusBar(label: text = "Status") { Stack class("cx-status") data(slot: "status-bar", trl: $label) aria(role: "status", label: $label) { slot } }
6
6
  part StatusItem(label: text) { Stack class("cx-status-item") { slot Span "{$label}" class("cx-status-text") } }
7
7
  part StatusSpacer() { Stack class("cx-status-spacer") }
@@ -1,9 +1,9 @@
1
1
  # Stepper - where you are in a flow of a few steps. Horizontal: one short bar per step under its name, as the «Nueva
2
- # cita» modal shows it. Vertical: numbered circles joined by a line, each with what it asks. A done step fills its
2
+ # appointment» modal shows it. Vertical: numbered circles joined by a line, each with what it asks. A done step fills its
3
3
  # bar (or shows a check); the current one is marked and read out as the current step.
4
- # Stepper(count: 4, label: "Paso 2 de 4: Servicio") { Step(num: "1", label: "Cliente", state: "done") Step(num: "2", label: "Servicio", state: "now") … }
5
- # Stepper(count: 3, orientation: "vertical") { Step(num: "1", label: "Tu negocio", description: "Nombre y rubro", state: "done") … }
6
- # look "list" (vertical): a numbered list for «how it works», ruled rows and a round number, the current one filled.
4
+ # Stepper(count: 4, label: "Step 2 of 4: Service") { Step(num: "1", label: "Customer", state: "done") Step(num: "2", label: "Service", state: "now") … }
5
+ # Stepper(count: 3, orientation: "vertical") { Step(num: "1", label: "Your business", description: "Nombre y rubro", state: "done") … }
6
+ # look "list" (vertical): a numbered list for “how it works”, ruled rows and a round number, the current one filled.
7
7
  # state: done | now | "" (to come). shadcn-for-muten · part · no deps · kit.css (.cx-stepper).
8
8
  part Stepper(count: number, orientation: text = "horizontal", label: text = "", look: text = "") {
9
9
  Stack class("cx-stepper cx-stp") data(slot: "stepper", orientation: $orientation, look: $look) style(n: "{$count}") aria(role: "list", label: $label) { slot }
@@ -1,5 +1,5 @@
1
1
  // StoreQr (muten Custom) - the kit's storeQr (kit/menus.js).
2
2
  export function mount(el, inputs, on) {
3
- Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/menus.js')]).then(([core, m]) => m.storeQr(el, inputs, core, on));
3
+ Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/menus.js')]).then(([core, m]) => core.relang(el, inputs, () => m.storeQr(el, inputs, core, on)));
4
4
  return () => {};
5
5
  }
@@ -1,5 +1,5 @@
1
- # StoreQr - the shop's QR with its mark in the middle, the name, the link and «Copiar enlace» (falls back to
1
+ # StoreQr - the shop's QR with its mark in the middle, the name, the link and “Copiar enlace” (falls back to
2
2
  # selecting the link when the clipboard says no).
3
- # StoreQr(url: "https://storio.ink/el-roble", name: "Barbería El Roble", mark: "ER")
3
+ # StoreQr(url: "https://storio.ink/el-roble", name: "El Roble Barbershop", mark: "ER")
4
4
  # shadcn-for-muten · Custom (kit menus + vendored qrcode-generator, MIT) · host JS store-qr.js.
5
5
  part StoreQr(url: text, name: text = "", mark: text = "") { Custom StoreQr inputs(url: $url, name: $name, mark: $mark) class("cx-qr-host") }
@@ -4,7 +4,7 @@
4
4
  # action pick(c: text) mutates active { active.set(c) }
5
5
  # SwatchPalette(label: "Colores recientes") { each colors as c { Swatch(color: c, current: active, onSelect: pick) } }
6
6
  # size: sm | default | lg. shadcn-for-muten · part · no deps · kit.css (.cx-swatch).
7
- part SwatchPalette(label: text = "Colores") { Stack class("cx-swatches-row") aria(role: "group", label: $label) { slot } }
7
+ part SwatchPalette(label: text = "Colors") { Stack class("cx-swatches-row") data(trl: $label) aria(role: "group", label: $label) { slot } }
8
8
  part Swatch(color: text, current: text, onSelect: action, size: text = "default") {
9
9
  Button -> $onSelect($color) class("cx-swatch") data(size: $size, name: $color) aria(pressed: $color == $current, label: $color) style(bg: "{$color}")
10
10
  }
@@ -1,16 +1,17 @@
1
1
  # Switch - on/off; the page owns the bool and the action. SwitchRow is the settings line: label, switch, the why.
2
- # Switch(on: dark, onToggle: toggleDark, label: "Modo oscuro")
3
- # SwitchRow(on: home, onToggle: toggleHome, label: "También a domicilio", description: "Pide la verificación de identidad…")
2
+ # Switch(on: dark, onToggle: toggleDark, label: "Dark mode")
3
+ # SwitchRow(on: home, onToggle: toggleHome, label: "Also at home", description: "Ask for identity verification…")
4
+ # disabled: a bool (a state or a getter) - it shows its state and cannot be flipped (a locked choice, a prerequisite missing).
4
5
  # size: default | sm. shadcn-for-muten · part · no deps · Nova (.cn-switch reads data-checked / data-unchecked).
5
- part Switch(on: bool, onToggle: action, label: text = "", size: text = "default") {
6
- Button -> $onToggle class("cn-switch group/switch") data(slot: "switch", size: $size, checked: $on, unchecked: not $on) aria(role: "switch", checked: $on, label: $label) {
6
+ part Switch(on: bool, onToggle: action, label: text = "", size: text = "default", disabled: bool = false) {
7
+ Button -> $onToggle class("cn-switch group/switch") data(slot: "switch", size: $size, checked: $on, unchecked: not $on) aria(role: "switch", checked: $on, label: $label) disabled when $disabled {
7
8
  Span "" class("cn-switch-thumb") data(slot: "switch-thumb", checked: $on, unchecked: not $on)
8
9
  }
9
10
  }
10
- part SwitchRow(on: bool, onToggle: action, label: text, description: text = "") {
11
- Stack class("cx-switch-row") {
11
+ part SwitchRow(on: bool, onToggle: action, label: text, description: text = "", disabled: bool = false) {
12
+ Stack class("cx-switch-row") data(disabled: $disabled) {
12
13
  Span "{$label}" class("cn-label") data(slot: "label")
13
- Switch(on: $on, onToggle: $onToggle, label: $label)
14
+ Switch(on: $on, onToggle: $onToggle, label: $label, disabled: $disabled)
14
15
  Text "{$description}" class("cx-switch-why")
15
16
  }
16
17
  }
@@ -1,10 +1,10 @@
1
1
  # TableView - the DataTable: filters as data (search · facet · date · range) with counts and chips, rows that fade /
2
2
  # slide / enter, the height easing, sort by header, selection with a bulk bar in its own space, the phone's
3
- # «buscar + Filtros» sheet, and Items when its box is narrow. (`DataTable` is muten's raw table primitive.)
4
- # TableView(columns: @cols, rows: @appointments, filters: @filters, statuses: @statuses, bulk: @bulk, one: "cita", many: "citas", onAction: act)
3
+ # “buscar + Filtros” sheet, and Items when its box is narrow. (`DataTable` is muten's raw table primitive.)
4
+ # TableView(columns: @cols, rows: @appointments, filters: @filters, statuses: @statuses, bulk: @bulk, one: "appointment", many: "appointments", onAction: act)
5
5
  # columns {key, label, type: person|text|date|status|money, sub} · filters {id, type, label, key | keys} ·
6
6
  # statuses {id, label, tone} · bulk {id, label, variant, set {…}} → onAction gets {"action", "ids"}.
7
7
  # shadcn-for-muten · part over KitWidget (kit core + kit/table.js).
8
- part TableView(columns: list, rows: list, filters: list, statuses: list, bulk: list, one: text = "fila", many: text = "filas", onAction: action) {
8
+ part TableView(columns: list, rows: list, filters: list, statuses: list, bulk: list, one: text = "row", many: text = "rows", onAction: action) {
9
9
  Custom KitWidget inputs(name: "table", columns: $columns, rows: $rows, filters: $filters, statuses: $statuses, bulk: $bulk, one: $one, many: $many) on(action: $onAction) class("cx-table-host")
10
10
  }
@@ -6,9 +6,17 @@ export function mount(el) {
6
6
  const ind = document.createElement('span'); ind.className = 'cx-tab-ind'; ind.setAttribute('aria-hidden', 'true'); list.prepend(ind);
7
7
  const tabs = () => [...list.querySelectorAll(':scope > .cx-tab')];
8
8
  const chosen = () => tabs().find((t) => t.getAttribute('aria-selected') === 'true' || t.getAttribute('aria-pressed') === 'true');
9
- let first = true;
9
+ let first = true, lastIdx = -1;
10
+ // once the person switches (never on the first paint), the panels' container learns it and which way they went, so
11
+ // the new panel comes in from that side (kit.css “Tab panels”)
12
+ const mark = (t) => {
13
+ const k = tabs().indexOf(t); const box = list.parentElement;
14
+ if (lastIdx >= 0 && k !== lastIdx && box) { box.setAttribute('data-tabs-moved', ''); box.setAttribute('data-tabs-dir', k < lastIdx ? 'back' : 'forward'); }
15
+ lastIdx = k;
16
+ };
10
17
  const place = () => {
11
18
  const t = chosen(); if (!t) { ind.style.opacity = '0'; return; }
19
+ mark(t);
12
20
  ind.style.opacity = '';
13
21
  if (first) ind.style.transition = 'none';
14
22
  ind.style.left = t.offsetLeft + 'px'; ind.style.width = t.offsetWidth + 'px';
@@ -1,8 +1,8 @@
1
1
  # Tabs - the indicator slides from one tab to the next (260 ms). variant "" = segmented (a pill on a muted track),
2
2
  # "line" = an underline with a count per tab that scrolls sideways on a phone. Arrows, Home and End move between tabs.
3
3
  # The page owns the current value; TabsContent shows the panel of the current one.
4
- # Tabs(label: "Periodo") { TabsTrigger(value: "day", current: period, label: "Hoy", onSelect: setPeriod) TabsTrigger(value: "week", current: period, label: "Semana", onSelect: setPeriod) }
5
- # Tabs(label: "Pedidos", variant: "line") { TabsTrigger(value: "new", current: lane, label: "Nuevos", count: "4", onSelect: setLane) … }
4
+ # Tabs(label: "Period") { TabsTrigger(value: "day", current: period, label: "Today", onSelect: setPeriod) TabsTrigger(value: "week", current: period, label: "Week", onSelect: setPeriod) }
5
+ # Tabs(label: "Orders", variant: "line") { TabsTrigger(value: "new", current: lane, label: "New", count: "4", onSelect: setLane) … }
6
6
  # TabsContent(value: "new", current: lane) { … }
7
7
  # shadcn-for-muten · part + Custom (TabsBehavior: the indicator + keys) · kit.css (.cx-tabs).
8
8
  part Tabs(label: text, variant: text = "") {
@@ -1,5 +1,5 @@
1
1
  // TagInput (muten Custom) - the kit's tags field (kit/collections.js tagInput): chips in/out, height eases, a cap.
2
2
  export function mount(el, inputs, on) {
3
- Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/collections.js')]).then(([core, c]) => c.tagInput(el, { ...inputs, tags: inputs.value ?? inputs.tags }, core, on));
3
+ Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/collections.js')]).then(([core, c]) => core.relang(el, inputs, () => c.tagInput(el, { ...inputs, tags: inputs.value ?? inputs.tags }, core, on)));
4
4
  return () => {};
5
5
  }
@@ -1,7 +1,7 @@
1
1
  # TagInput - tags as chips: Enter or a comma adds, Backspace marks then removes the last, a repeated tag flashes the
2
2
  # one already there, suggestions to tap, a cap with its count. The value is comma-joined text.
3
- # TagInput(value: tags, label: "Etiquetas del servicio", suggestions: "Fade,Navaja,Niños", max: 8, hint: "Ayudan a que te encuentren.", onChange: setTags)
3
+ # TagInput(value: tags, label: "Service tags", suggestions: "Fade,Razor,Kids", max: 8, hint: "They help people find you.", onChange: setTags)
4
4
  # shadcn-for-muten · Custom (kit collections) · host JS tag-input.js.
5
- part TagInput(value: text, placeholder: text = "", label: text = "Etiquetas", suggestions: text = "", max: number = 8, hint: text = "", onChange: action) {
5
+ part TagInput(value: text, placeholder: text = "", label: text = "Tags", suggestions: text = "", max: number = 8, hint: text = "", onChange: action) {
6
6
  Custom TagInput inputs(value: $value, placeholder: $placeholder, label: $label, suggestions: $suggestions, max: $max, hint: $hint) on(change: $onChange) class("cx-tags-host")
7
7
  }
@@ -1,7 +1,7 @@
1
1
  # Text (part name Txt - `Text` is a muten primitive) - the one text atom, in a few roles. Everything that shows words
2
2
  # in the kit is built from it, so a role changes in one place.
3
- # Txt(value: "Corte clásico", variant: "heading")
4
- # Txt(value: "{count} citas", variant: "muted")
3
+ # Txt(value: "Classic cut", variant: "heading")
4
+ # Txt(value: "{count} appointments", variant: "muted")
5
5
  # variant: body | title | heading | strong | label | muted | caption (default: body)
6
6
  # shadcn-for-muten · part · no deps · styled by kit.css (.cx-text).
7
7
  part Txt(value: text, variant: text = "body") { Span "{$value}" class("cx-text") data(slot: "text", variant: $variant) }
@@ -1,5 +1,5 @@
1
1
  // TimeField (muten Custom) - the kit's timeField (kit/dates.js).
2
2
  export function mount(el, inputs, on) {
3
- Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/dates.js')]).then(([core, m]) => m.timeField(el, inputs, core, on));
3
+ Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/dates.js')]).then(([core, m]) => core.relang(el, inputs, () => m.timeField(el, inputs, core, on)));
4
4
  return () => {};
5
5
  }
@@ -1,6 +1,6 @@
1
- # TimeField - a field-like button with the time (and mañana/tarde); it opens the hour / minute wheels.
2
- # TimeField(label: "Hora", value: "10:30", from: 9, to: 19, step: 15, onChange: setTime) → "HH:MM"
1
+ # TimeField - a field-like button with the time (and AM/PM); it opens the hour / minute wheels.
2
+ # TimeField(label: "Time", value: "10:30", from: 9, to: 19, step: 15, onChange: setTime) → "HH:MM"
3
3
  # shadcn-for-muten · Custom (kit dates) · host JS time-field.js.
4
- part TimeField(label: text = "Hora", value: text = "", from: number = 9, to: number = 19, step: number = 15, onChange: action) {
4
+ part TimeField(label: text = "Time", value: text = "", from: number = 9, to: number = 19, step: number = 15, onChange: action) {
5
5
  Custom TimeField inputs(label: $label, value: $value, from: $from, to: $to, step: $step) on(change: $onChange) class("cx-tf-host")
6
6
  }
@@ -1,12 +1,12 @@
1
1
  # Timeline - what happened, in order: a dot and a line join the events; the last one (where things stand now) is
2
2
  # marked. Each event has what happened, when, and an optional detail.
3
- # Timeline(label: "Historial del pedido") {
4
- # TimelineItem(title: "Pedido recibido", time: "hoy · 10:38")
5
- # TimelineItem(title: "Esperando tu revisión", time: "ahora", state: "now")
3
+ # Timeline(label: "Order history") {
4
+ # TimelineItem(title: "Order received", time: "today · 10:38")
5
+ # TimelineItem(title: "Waiting for your review", time: "now", state: "now")
6
6
  # }
7
7
  # state: "" (past) | now (where it stands) | next (still to come, faded).
8
8
  # shadcn-for-muten · part · no deps · kit.css (.cx-timeline).
9
- part Timeline(label: text = "") { Stack class("cx-timeline cx-tl") aria(role: "list", label: $label) { slot } }
9
+ part Timeline(label: text = "") { Stack class("cx-timeline cx-tl") aria(role: "list", label: $label) { slot ListMotion() } }
10
10
  part TimelineItem(title: text, time: text = "", description: text = "", state: text = "") {
11
11
  Stack class("cx-tl-item") data(state: $state) aria(role: "listitem") {
12
12
  Span "{$title}" class("cx-text cx-tl-title") data(slot: "text", variant: "body")
@@ -2,8 +2,8 @@
2
2
  # hovering the pile fans them out and pauses them; each one leaves on its own (duration) or with its ×, and can carry
3
3
  # one action (Deshacer). The page bumps a counter to show one, with the title it has at that moment:
4
4
  # state { toasts = 0 : number toastTitle = "" : text }
5
- # action saved() mutates toasts, toastTitle { toastTitle.set("Cita creada") toasts.set(toasts + 1) }
6
- # Toaster(trigger: toasts, title: toastTitle, description: "Jueves 9 a las 10:30", action: "Deshacer", onAction: undo)
5
+ # action saved() mutates toasts, toastTitle { toastTitle.set("Appointment created") toasts.set(toasts + 1) }
6
+ # Toaster(trigger: toasts, title: toastTitle, description: "Thursday 9 at 10:30", action: "Undo", onAction: undo)
7
7
  # For what the person just did on this screen (saving, saved) the Island is lighter; a toast is for news that can wait.
8
8
  # shadcn-for-muten · Custom · kit.css (.pg-toasts, .pg-toast) · host JS toaster.js.
9
9
  part Toaster(trigger: number, title: text, description: text = "", action: text = "", duration: number = 5000, onAction: action) {
@@ -1,7 +1,7 @@
1
1
  # ToggleGroup - toggles in a row (shadcn Nova). One choice: ToggleGroupItem with the page's text state. Several
2
2
  # choices: put Toggle parts inside, each with its own bool. spacing "0" joins them into one control; "2" separates.
3
3
  # state { align = "left" : text } action setAlign(v: text) mutates align { align.set(v) }
4
- # ToggleGroup(label: "Alineación") { ToggleGroupItem(value: "left", current: align, onSelect: setAlign, label: "Izquierda") { Icon "lucide:align-left" } … }
4
+ # ToggleGroup(label: "Alignment") { ToggleGroupItem(value: "left", current: align, onSelect: setAlign, label: "Left") { Icon "lucide:align-left" } … }
5
5
  # ToggleGroup(variant: "outline", spacing: "2") { Toggle(on: bold, onToggle: b, variant: "outline") { Icon "lucide:bold" } … }
6
6
  # variant: default | outline · size: sm | default | lg · spacing: 0 | 1 | 2 · orientation: horizontal | vertical
7
7
  # shadcn-for-muten · part · no deps · Nova (.cn-toggle-group) + kit.css (joined/vertical).