@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
@@ -1,9 +1,9 @@
1
1
  # ChoiceList - Items you choose one of: a real radio group (arrows move and pick). The page owns the current value.
2
- # ChoiceList(label: "Servicio") {
3
- # ChoiceItem(value: "cut", current: service, onPick: pick, title: "Corte clásico", description: "30 min", price: "$25.000")
2
+ # ChoiceList(label: "Service") {
3
+ # ChoiceItem(value: "cut", current: service, onPick: pick, title: "Classic cut", description: "30 min", price: "$25.000")
4
4
  # }
5
5
  # A picture before the text: put an ItemMedia in its slot and the radio moves to the end of the row.
6
- # ChoiceItem(value: "shop", current: role, onPick: pickRole, title: "Tengo un local", description: "La gente viene a ti") {
6
+ # ChoiceItem(value: "shop", current: role, onPick: pickRole, title: "I have a venue", description: "People come to you") {
7
7
  # ItemMedia(variant: "icon") { Icon "lucide:store" }
8
8
  # }
9
9
  # shadcn-for-muten · part + Custom (RovingBehavior) · kit.css (.cx-radiogroup, .cx-radio).
package/registry/code.js CHANGED
@@ -1,16 +1,22 @@
1
- // Code (muten Custom) - a code block: a bar with the language (or a title) and Copiar, then the code highlighted with
1
+ // Code (muten Custom) - a code block: a bar with the language (or a title) and Copy, then the code highlighted with
2
2
  // the muten grammar (any other lang shows plain). inputs: code, lang, title. The highlighter is the shared
3
3
  // highlight.js, loaded lazily so the token rules live in one place.
4
4
  export function mount(el, inputs) {
5
5
  const code = String(inputs.code ?? ''), lang = String(inputs.lang || 'muten');
6
- el.innerHTML = '<div class="cx-code-bar"><span></span><button type="button" class="cn-button cn-button-variant-outline cn-button-size-xs">Copiar</button></div><pre class="cx-code-pre"><code></code></pre>';
6
+ el.innerHTML = '<div class="cx-code-bar"><span></span><button type="button" class="cn-button cn-button-variant-outline cn-button-size-xs" data-tr="Copy">Copy</button></div><pre class="cx-code-pre"><code></code></pre>';
7
7
  el.querySelector('.cx-code-bar span').textContent = inputs.title || lang;
8
8
  const box = el.querySelector('code'), copy = el.querySelector('button');
9
+ // its words: English, said in the language of the nearest lang (or inputs.lang) once kit/i18n.js is in
10
+ let I = null; const T = (k, ...a) => (I ? I.t(I.langOf(el), k, ...a) : a.reduce((s, v, n) => s.replace(`{${n}}`, v), k));
11
+ // (inputs.lang here is the code's language, “muten”, not a human one: the words follow the nearest lang)
12
+ import('@muten/shadcn/registry/kit/i18n.js').then((m) => { I = m; m.localizeTree(el); });
9
13
  box.textContent = code;
10
14
  if (lang === 'muten') import('@muten/shadcn/registry/highlight.js').then((m) => { box.innerHTML = m.highlight(code, lang); }).catch(() => {});
11
15
  copy.onclick = () => {
12
- const done = (t) => { copy.textContent = t; setTimeout(() => { copy.textContent = 'Copiar'; }, 1400); };
13
- try { navigator.clipboard.writeText(code).then(() => done('Copiado'), () => { getSelection().selectAllChildren(box); done('Selecciónalo'); }); } catch { getSelection().selectAllChildren(box); done('Selecciónalo'); }
16
+ // the new word comes in with the kit's swap (kit.css .cx-swap-in), the old one does not just blink away
17
+ const say = (k) => { copy.innerHTML = '<span class="cx-swap-in"></span>'; copy.firstChild.textContent = T(k); };
18
+ const done = (t) => { say(t); setTimeout(() => say('Copy'), 1400); };
19
+ try { navigator.clipboard.writeText(code).then(() => done('Copied'), () => { getSelection().selectAllChildren(box); done('Select it'); }); } catch { getSelection().selectAllChildren(box); done('Select it'); }
14
20
  };
15
21
  return (next) => { const c = String(next.code ?? ''); if (c === box.textContent) return; box.textContent = c; if (lang === 'muten') import('@muten/shadcn/registry/highlight.js').then((m) => { box.innerHTML = m.highlight(c, lang); }); };
16
22
  }
@@ -1,8 +1,8 @@
1
1
  # Collapsible - one block that opens and closes: a trigger line and the content under it, folding to its exact
2
2
  # height (200 ms) while the chevron turns. The page owns the bool. For several stacked questions use Accordion.
3
3
  # state { more = false : bool } action toggleMore() mutates more { more.toggle() }
4
- # Collapsible(label: "Más opciones", open: more, onToggle: toggleMore) { … }
5
- # Collapsible(label: "3 pedidos entregados", hint: "Ver", open: done, onToggle: toggleDone) { … }
4
+ # Collapsible(label: "More options", open: more, onToggle: toggleMore) { … }
5
+ # Collapsible(label: "3 orders delivered", hint: "View", open: done, onToggle: toggleDone) { … }
6
6
  # shadcn-for-muten · part · no deps · kit.css (.cx-coll, the same fold as Accordion).
7
7
  part Collapsible(label: text, open: bool, onToggle: action, hint: text = "") {
8
8
  Stack class("cx-coll") data(slot: "collapsible", open: $open) {
@@ -1,22 +1,22 @@
1
- // ColorPicker (muten Custom) - Storio's one colour picker (solid; gradients and animations when allowed), as the
1
+ // ColorPicker (muten Custom) - Storio's one color picker (solid; gradients and animations when allowed), as the
2
2
  // reference artifact mounts it. The value is paint text (a hex, or e.g. "linear 150 | #1a0f0a 0 | #c2410c 100 ~flow 14").
3
- // THE APP'S ONE COLOUR PICKER. Every place where somebody picks a colour uses this component (through the
3
+ // THE APP'S ONE COLOUR PICKER. Every place where somebody picks a color uses this component (through the
4
4
  // ColorPick / PaintPick parts), so it looks and behaves the same everywhere and a fix lands everywhere.
5
5
  //
6
- // Closed it is a small swatch showing the real colour or gradient (plus a dot when it is animated). Open it
6
+ // Closed it is a small swatch showing the real color or gradient (plus a dot when it is animated). Open it
7
7
  // is a popover (a bottom sheet on a phone) with:
8
- // · a hue × lightness map where every colour is a draggable point, and a SATURATION slider for the
8
+ // · a hue × lightness map where every color is a draggable point, and a SATURATION slider for the
9
9
  // selected point only (it is not opacity: the slider says which point it acts on);
10
10
  // · the hex code, an eyedropper where the browser has one, and a trash can to delete a gradient stop;
11
11
  // · OPTIONALLY, gradients: a bar of up to 4 stops (tap the bar to add one, drag a stop along it, drag it
12
12
  // off the bar / press Delete / use the trash can to remove it), linear · radial · conic with an angle
13
13
  // dial, and animations (flow, breathe, spin, hue) with a speed.
14
14
  //
15
- // Gradients are OPT-IN: not every colour in the app can be a gradient. `modes: "solid"` (the default) is a
16
- // plain colour picker and only ever emits a hex; `modes: "solid gradient"` adds the gradient controls, and
15
+ // Gradients are OPT-IN: not every color in the app can be a gradient. `modes: "solid"` (the default) is a
16
+ // plain color picker and only ever emits a hex; `modes: "solid gradient"` adds the gradient controls, and
17
17
  // `anims: "flow breathe spin hue"` lists which animations that place allows (none by default).
18
18
  //
19
- // The value is text, read and written by lib/paint.ts: a plain hex is a solid colour, a gradient is e.g.
19
+ // The value is text, read and written by lib/paint.ts: a plain hex is a solid color, a gradient is e.g.
20
20
  // "linear 150 | #1a0f0a 0 | #c2410c 100 ~flow 14". Hosts paint with paintCss() from that same lib.
21
21
  //
22
22
  // DRAG IS LOCAL, RELEASE COMMITS. While a point or stop is dragged only this popover repaints (and an
@@ -27,7 +27,8 @@
27
27
  // The popover lives on <body> with fixed coordinates, so a scrolling or clipping panel never hides it, and
28
28
  // it opens from anywhere in the swatch's row (the swatch and the hex label next to it).
29
29
  export function mount(el, inputs, handlers) {
30
- const en = String((inputs && inputs.lang) || document.documentElement.lang || 'es') === 'en';
30
+ // the language passed, else the nearest lang (the page's); English unless it is Spanish
31
+ const en = String((inputs && inputs.lang) || (el.closest && el.closest('[lang]')?.getAttribute('lang')) || document.documentElement.lang || 'en').slice(0, 2) !== 'es';
31
32
  const say = (es, english) => (en ? english : es);
32
33
  const modes = String((inputs && inputs.modes) || 'solid').split(/\s+/);
33
34
  const allowGradient = modes.includes('gradient');
@@ -53,7 +54,7 @@ export function mount(el, inputs, handlers) {
53
54
  const well = document.createElement('button');
54
55
  well.type = 'button';
55
56
  well.className = 'cp-well pk-well';
56
- well.setAttribute('aria-label', (inputs && inputs.label) || say('Elegir un color', 'Pick a colour'));
57
+ well.setAttribute('aria-label', (inputs && inputs.label) || say('Elegir un color', 'Pick a color'));
57
58
  el.appendChild(well);
58
59
 
59
60
  let lib = null; // lib/paint.ts, loaded on mount (a Custom cannot import at the top)
@@ -119,7 +120,7 @@ export function mount(el, inputs, handlers) {
119
120
  pop = document.createElement('div');
120
121
  pop.className = 'pk-pop';
121
122
  pop.setAttribute('role', 'dialog');
122
- pop.setAttribute('aria-label', (inputs && inputs.label) || say('Color', 'Colour'));
123
+ pop.setAttribute('aria-label', (inputs && inputs.label) || say('Color', 'Color'));
123
124
  pop.addEventListener('click', onClick);
124
125
  pop.addEventListener('keydown', (event) => { if (event.key === 'Escape') close(); });
125
126
  (document.getElementById('lib') || document.body).append(scrim, pop); // inside the skinned area, so it wears the skin's tokens
@@ -144,7 +145,7 @@ export function mount(el, inputs, handlers) {
144
145
  // ── the popover ──
145
146
  // Lightness 95 (top) to 5 (bottom); pure black or white sit on the edge instead of past it.
146
147
  const Y = (light, height) => Math.max(0, Math.min(height, ((95 - light) / 90) * height));
147
- // The INTENDED saturation of the selected colour lives on the slider, not in the hex: black at saturation 75
148
+ // The INTENDED saturation of the selected color lives on the slider, not in the hex: black at saturation 75
148
149
  // is still #000000, so reading it back from the hex would drop it to 0 and the map would stay grey.
149
150
  const intensity = () => {
150
151
  const slider = pop && pop.querySelector('[data-sat]');
@@ -161,15 +162,15 @@ export function mount(el, inputs, handlers) {
161
162
  html.push(`<div class="pk-seg" role="tablist"><button type="button" data-mode="solid" class="${gradient ? '' : 'on'}">${say('Sólido', 'Solid')}</button><button type="button" data-mode="gradient" class="${gradient ? 'on' : ''}">${say('Degradado', 'Gradient')}</button></div>`);
162
163
  }
163
164
  if (gradient) {
164
- html.push(`<div class="pk-bar" style="background:${lib.barCss(v)}" aria-label="${say('Colores del degradado', 'Gradient colours')}">${v.stops.map((stop, i) => `<span class="pk-stop ${i === sel ? 'on' : ''}" data-stop="${i}" style="left:${stop.p}%;background:${stop.c}" tabindex="0" role="slider" aria-label="${say('Color', 'Colour')} ${i + 1}" aria-valuenow="${Math.round(stop.p)}"></span>`).join('')}</div>`);
165
+ html.push(`<div class="pk-bar" style="background:${lib.barCss(v)}" aria-label="${say('Colores del degradado', 'Gradient colors')}">${v.stops.map((stop, i) => `<span class="pk-stop ${i === sel ? 'on' : ''}" data-stop="${i}" style="left:${stop.p}%;background:${stop.c}" tabindex="0" role="slider" aria-label="${say('Color', 'Color')} ${i + 1}" aria-valuenow="${Math.round(stop.p)}"></span>`).join('')}</div>`);
165
166
  }
166
- html.push(`<div class="pk-map" aria-label="${say('Mapa de tonos', 'Colour map')}"><canvas></canvas><svg>${gradient ? '<polyline class="pk-ln" fill="none" stroke="#fff" stroke-width="1.6" stroke-dasharray="3 4" opacity=".9"/>' : ''}</svg>${points.map((_, i) => `<span class="pk-pt ${i === sel ? 'on' : ''}">${gradient ? i + 1 : ''}</span>`).join('')}</div>`);
167
+ html.push(`<div class="pk-map" aria-label="${say('Mapa de tonos', 'Color map')}"><canvas></canvas><svg>${gradient ? '<polyline class="pk-ln" fill="none" stroke="#fff" stroke-width="1.6" stroke-dasharray="3 4" opacity=".9"/>' : ''}</svg>${points.map((_, i) => `<span class="pk-pt ${i === sel ? 'on' : ''}">${gradient ? i + 1 : ''}</span>`).join('')}</div>`);
167
168
  // Saturation of the SELECTED point, and it says so: in a gradient the label carries that point's number and
168
- // colour, so nobody takes it for opacity or for the whole gradient.
169
+ // color, so nobody takes it for opacity or for the whole gradient.
169
170
  const satName = say('Saturación', 'Saturation');
170
171
  const satOf = gradient ? `${satName} ${sel + 1}` : satName;
171
172
  html.push(`<div class="pk-row"><span class="pk-lbl pk-satlbl">${gradient ? `<i class="pk-satdot" style="background:${current.c}">${sel + 1}</i>` : ''}${satName}</span><input class="pk-range" type="range" min="0" max="100" value="${hsl.s}" data-sat aria-label="${satOf}"></div>`);
172
- html.push(`<div class="pk-row"><label class="pk-hex"><i style="background:${current.c}"></i><input value="${current.c}" maxlength="7" spellcheck="false" data-hex data-clear="on" aria-label="${say('Código del color', 'Colour code')}"></label>${'EyeDropper' in window ? `<button type="button" class="pk-ic" data-drop aria-label="${say('Tomar un color de la pantalla', 'Pick a colour from the screen')}">${ICON.drop}</button>` : ''}${gradient ? `<button type="button" class="pk-ic" data-del aria-label="${say('Quitar este color', 'Remove this colour')}" ${v.stops.length <= 2 ? 'disabled' : ''}>${ICON.trash}</button>` : ''}</div>`);
173
+ html.push(`<div class="pk-row"><label class="pk-hex"><i style="background:${current.c}"></i><input value="${current.c}" maxlength="7" spellcheck="false" data-hex data-clear="on" aria-label="${say('Código del color', 'Color code')}"></label>${'EyeDropper' in window ? `<button type="button" class="pk-ic" data-drop aria-label="${say('Tomar un color de la pantalla', 'Pick a color from the screen')}">${ICON.drop}</button>` : ''}${gradient ? `<button type="button" class="pk-ic" data-del aria-label="${say('Quitar este color', 'Remove this color')}" ${v.stops.length <= 2 ? 'disabled' : ''}>${ICON.trash}</button>` : ''}</div>`);
173
174
  if (gradient) {
174
175
  html.push(`<div class="pk-row">${['linear', 'radial', 'conic'].map((type) => `<button type="button" class="pk-ic ${v.type === type ? 'on' : ''}" data-type="${type}" aria-label="${TYPE_LABEL[type]}" aria-pressed="${v.type === type}">${ICON[type]}</button>`).join('')}<span class="pk-grow"></span>${v.type !== 'radial' ? `<span class="pk-dial" data-dial tabindex="0" role="slider" aria-label="${say('Ángulo', 'Angle')}" aria-valuenow="${Math.round(v.angle)}"><i style="transform:rotate(${v.angle + 180}deg)"></i></span><span class="pk-deg">${Math.round(v.angle)}°</span>` : ''}</div>`);
175
176
  if (allowedAnims.length > 1) {
@@ -179,12 +180,12 @@ export function mount(el, inputs, handlers) {
179
180
  }
180
181
  }
181
182
  }
182
- // Ready-made gradients in a grid (all visible, nothing to scroll), and the recent colours in a row of their own.
183
+ // Ready-made gradients in a grid (all visible, nothing to scroll), and the recent colors in a row of their own.
183
184
  if (gradient) {
184
- html.push(`<div class="pk-sep"></div><div class="pk-sw">${lib.PAIRS.map((pair) => { const name = en ? pair.name.en : pair.name.es; return `<button type="button" data-pair="${pair.colours.join(',')}" style="background:linear-gradient(135deg,${pair.colours.join(',')})" title="${name}" aria-label="${name}"></button>`; }).join('')}</div>`);
185
+ html.push(`<div class="pk-sep"></div><div class="pk-sw">${lib.PAIRS.map((pair) => { const name = en ? pair.name.en : pair.name.es; return `<button type="button" data-pair="${pair.colors.join(',')}" style="background:linear-gradient(135deg,${pair.colors.join(',')})" title="${name}" aria-label="${name}"></button>`; }).join('')}</div>`);
185
186
  }
186
187
  if (recent.length > 0) {
187
- html.push(`${gradient ? '' : '<div class="pk-sep"></div>'}<div class="pk-recent"><span class="pk-lbl">${say('Recientes', 'Recent')}</span>${recent.map((colour) => `<button type="button" data-swatch="${colour}" style="background:${colour}" title="${colour}" aria-label="${colour}"></button>`).join('')}</div>`);
188
+ html.push(`${gradient ? '' : '<div class="pk-sep"></div>'}<div class="pk-recent"><span class="pk-lbl">${say('Recientes', 'Recent')}</span>${recent.map((color) => `<button type="button" data-swatch="${color}" style="background:${color}" title="${color}" aria-label="${color}"></button>`).join('')}</div>`);
188
189
  }
189
190
  pop.innerHTML = html.join('');
190
191
  pop.querySelector('[data-sat]').style.background = `linear-gradient(90deg, hsl(${hsl.h} 0% ${hsl.l}%), hsl(${hsl.h} 100% ${hsl.l}%))`;
@@ -212,7 +213,7 @@ export function mount(el, inputs, handlers) {
212
213
  canvas.height = height * dpr;
213
214
  const ctx = canvas.getContext('2d');
214
215
  ctx.scale(dpr, dpr);
215
- // A grey shows the map in colour anyway: that is what touching it will give (see the pointerdown).
216
+ // A grey shows the map in color anyway: that is what touching it will give (see the pointerdown).
216
217
  const sat = intensity() < GREY ? COLOURFUL : intensity();
217
218
  for (let x = 0; x < width; x += 1) {
218
219
  const hue = (x / width) * 360;
@@ -264,9 +265,9 @@ export function mount(el, inputs, handlers) {
264
265
  paintPreviews();
265
266
  emit('input');
266
267
  }
267
- function setColour(i, colour) {
268
- v.stops[i].c = colour;
269
- if (v.type === 'solid') v.stops.forEach((stop) => { stop.c = colour; });
268
+ function setColor(i, color) {
269
+ v.stops[i].c = color;
270
+ if (v.type === 'solid') v.stops.forEach((stop) => { stop.c = color; });
270
271
  }
271
272
  function selectStop(i) {
272
273
  sel = i;
@@ -277,7 +278,7 @@ export function mount(el, inputs, handlers) {
277
278
  live();
278
279
  }
279
280
  let recent = [];
280
- function remember(colour) { recent = [colour, ...recent.filter((each) => each !== colour)].slice(0, 8); }
281
+ function remember(color) { recent = [color, ...recent.filter((each) => each !== color)].slice(0, 8); }
281
282
  function commit() { remember(v.stops[sel].c); emit('pick'); }
282
283
 
283
284
  function wire() {
@@ -291,23 +292,23 @@ export function mount(el, inputs, handlers) {
291
292
  if (dragging === null) return;
292
293
  const p = at(event);
293
294
  const sat = intensity();
294
- setColour(dragging, lib.hslToHex(Math.round((p.x / p.w) * 360) % 360, sat, Math.round(95 - (p.y / p.h) * 90)));
295
+ setColor(dragging, lib.hslToHex(Math.round((p.x / p.w) * 360) % 360, sat, Math.round(95 - (p.y / p.h) * 90)));
295
296
  live();
296
297
  };
297
298
  map.addEventListener('pointerdown', (event) => {
298
299
  const p = at(event);
299
300
  const stops = v.type !== 'solid' ? v.stops : [v.stops[0]];
300
- // Grab the point under the finger; on a tie (two stops of the same colour sit on the same spot) the
301
- // one already selected wins, so a drag never jumps to a colour you did not choose.
301
+ // Grab the point under the finger; on a tie (two stops of the same color sit on the same spot) the
302
+ // one already selected wins, so a drag never jumps to a color you did not choose.
302
303
  const far = (i) => { const hsl = lib.hexToHsl(stops[i].c); return Math.hypot((hsl.h / 360) * p.w - p.x, Y(hsl.l, p.h) - p.y); };
303
304
  let nearest = sel;
304
305
  stops.forEach((_, i) => { if (far(i) < far(nearest) - 2) nearest = i; });
305
306
  if (far(nearest) < GRAB && nearest !== sel) selectStop(nearest);
306
307
  // A grey (black, white) has no saturation, so the map is drawn grey and dragging would only give greys.
307
- // Touching the map means «I want a colour»: bring the saturation up first. The slider can take it back.
308
+ // Touching the map means “I want a color”: bring the saturation up first. The slider can take it back.
308
309
  const hsl = lib.hexToHsl(v.stops[sel].c);
309
310
  if (hsl.s < GREY) {
310
- setColour(sel, lib.hslToHex(hsl.h, COLOURFUL, hsl.l));
311
+ setColor(sel, lib.hslToHex(hsl.h, COLOURFUL, hsl.l));
311
312
  pop.querySelector('[data-sat]').value = COLOURFUL;
312
313
  drawMap();
313
314
  }
@@ -334,7 +335,7 @@ export function mount(el, inputs, handlers) {
334
335
  moving = stop ? Number(stop.dataset.stop) : near;
335
336
  if (moving !== sel) selectStop(moving);
336
337
  } else if (v.stops.length < lib.MAX_STOPS) {
337
- // The new stop takes the colour the gradient already has at that spot, so adding one never
338
+ // The new stop takes the color the gradient already has at that spot, so adding one never
338
339
  // changes how it looks until you move it, and its point does not sit on top of another one.
339
340
  const p = Math.round(((event.clientX - r.left) / r.width) * 100);
340
341
  const sorted = [...v.stops].sort((a, b) => a.p - b.p);
@@ -409,7 +410,7 @@ export function mount(el, inputs, handlers) {
409
410
  const sat = pop.querySelector('[data-sat]');
410
411
  sat.addEventListener('input', () => {
411
412
  const hsl = lib.hexToHsl(v.stops[sel].c);
412
- setColour(sel, lib.hslToHex(hsl.h, Number(sat.value), hsl.l));
413
+ setColor(sel, lib.hslToHex(hsl.h, Number(sat.value), hsl.l));
413
414
  drawMap();
414
415
  live();
415
416
  });
@@ -418,7 +419,7 @@ export function mount(el, inputs, handlers) {
418
419
  const typed = event.target.value.trim();
419
420
  const hex = typed.startsWith('#') ? typed : `#${typed}`;
420
421
  if (!lib.isHex(hex)) return;
421
- setColour(sel, hex.toLowerCase());
422
+ setColor(sel, hex.toLowerCase());
422
423
  drawMap();
423
424
  live();
424
425
  commit();
@@ -431,8 +432,8 @@ export function mount(el, inputs, handlers) {
431
432
  const data = button.dataset;
432
433
  if (data.mode) {
433
434
  if (data.mode === 'solid') {
434
- const colour = v.stops[sel].c;
435
- v = lib.parsePaint(colour);
435
+ const color = v.stops[sel].c;
436
+ v = lib.parsePaint(color);
436
437
  } else if (v.type === 'solid') {
437
438
  const hsl = lib.hexToHsl(v.stops[0].c);
438
439
  v = { ...v, type: 'linear', stops: [{ c: lib.hslToHex(hsl.h, hsl.s, Math.max(6, hsl.l - 28)), p: 0 }, { c: v.stops[0].c, p: 100 }] };
@@ -441,11 +442,11 @@ export function mount(el, inputs, handlers) {
441
442
  } else if (data.type) v.type = data.type;
442
443
  else if (data.anim) v.anim = data.anim;
443
444
  else if (data.speed) v.speed = Number(data.speed);
444
- else if (data.pair) { const colours = data.pair.split(','); v.stops = colours.map((c, i) => ({ c, p: Math.round((i / (colours.length - 1)) * 100) })); sel = 0; }
445
- else if (data.swatch) setColour(sel, data.swatch);
445
+ else if (data.pair) { const colors = data.pair.split(','); v.stops = colors.map((c, i) => ({ c, p: Math.round((i / (colors.length - 1)) * 100) })); sel = 0; }
446
+ else if (data.swatch) setColor(sel, data.swatch);
446
447
  else if ('del' in data) { if (v.stops.length <= 2) return; v.stops.splice(sel, 1); sel = 0; }
447
448
  else if ('drop' in data) {
448
- try { const picked = await new window.EyeDropper().open(); setColour(sel, picked.sRGBHex.toLowerCase()); } catch { return; }
449
+ try { const picked = await new window.EyeDropper().open(); setColor(sel, picked.sRGBHex.toLowerCase()); } catch { return; }
449
450
  } else return;
450
451
  render();
451
452
  place();
@@ -1,7 +1,7 @@
1
1
  # ColorPicker - Storio's one colour picker. Closed: a swatch with the real colour (or gradient). Open: a popover (a
2
2
  # sheet on a phone) to pick. modes "solid" (default) or "solid gradient"; anims lists the allowed animations.
3
3
  # ColorPicker(value: brand, onInput: setBrand)
4
- # ColorPicker(value: wall, modes: "solid gradient", anims: "flow breathe spin hue", label: "Fondo de la tienda", onInput: setWall)
4
+ # ColorPicker(value: wall, modes: "solid gradient", anims: "flow breathe spin hue", label: "Shop background", onInput: setWall)
5
5
  # The value is paint text (a hex, or "linear 150 | #1a0f0a 0 | #c2410c 100 ~flow 14"). shadcn-for-muten · Custom.
6
6
  part ColorPicker(value: text, modes: text = "solid", anims: text = "", label: text = "Color", onInput: action) {
7
7
  Custom ColorPicker inputs(value: $value, modes: $modes, anims: $anims, label: $label, lang: "es") on(input: $onInput, pick: $onInput) class("cx-cp-host")
@@ -1,5 +1,5 @@
1
1
  // Combobox (muten Custom) - the kit's combobox (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.combobox(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.combobox(el, inputs, core, on)));
4
4
  return () => {};
5
5
  }
@@ -1,6 +1,6 @@
1
- # Combobox - type and the list filters (accent-insensitive) and marks the match; arrows, Enter, Esc; «Usar «…»» to
1
+ # Combobox - type and the list filters (accent-insensitive) and marks the match; arrows, Enter, Esc; “Usar «…”» to
2
2
  # keep something that is not in the list (allowNew "off" to forbid it). options is comma-joined text.
3
- # Combobox(label: "Ciudad", options: "Bogotá,Medellín,Cali", placeholder: "Escribe tu ciudad", onChange: setCity)
3
+ # Combobox(label: "City", options: "Bogotá,Medellín,Cali", placeholder: "Type your city", onChange: setCity)
4
4
  # shadcn-for-muten · Custom (kit menus) · host JS combobox.js.
5
5
  part Combobox(label: text = "", options: text, value: text = "", placeholder: text = "", allowNew: text = "on", hint: text = "on", onChange: action) {
6
6
  Custom Combobox inputs(label: $label, options: $options, value: $value, placeholder: $placeholder, allowNew: $allowNew, hint: $hint) on(change: $onChange) class("cx-cb-host")
@@ -1,9 +1,9 @@
1
1
  # CommandPalette - the ⌘K search: a Command inside the adaptive modal. Open it with a Hotkey and a search button; an
2
2
  # item's action does its thing and closes the palette (the page sets open back to false).
3
3
  # Hotkey(keys: "k", onPress: openPalette)
4
- # CommandPalette(open: palette, onClose: closePalette, query: q) { CommandGroup(label: "Ir a") { CommandItem(label: "Agenda", onSelect: goAgenda) { Icon "lucide:calendar" } } }
4
+ # CommandPalette(open: palette, onClose: closePalette, query: q) { CommandGroup(label: "Go to") { CommandItem(label: "Schedule", onSelect: goAgenda) { Icon "lucide:calendar" } } }
5
5
  # shadcn-for-muten · part over Modal + Command.
6
- part CommandPalette(open: bool, onClose: action, query: text, placeholder: text = "Busca o salta a…", title: text = "Buscar") {
6
+ part CommandPalette(open: bool, onClose: action, query: text, placeholder: text = "Search or jump to…", title: text = "Search") {
7
7
  Modal(open: $open, onClose: $onClose, title: $title, mode: "dialog") class("cx-palette") {
8
8
  Command(query: $query, placeholder: $placeholder) { slot }
9
9
  }
@@ -1,5 +1,5 @@
1
1
  // CommandBehavior (muten Custom) - the Command's list: as you type it keeps the items whose label (or keywords)
2
- // holds what you wrote, accent-insensitive; a group with nothing left hides, and «empty» shows when nothing matches.
2
+ // holds what you wrote, accent-insensitive; a group with nothing left hides, and “empty” shows when nothing matches.
3
3
  // The arrows move the highlight, Enter picks it, the pointer highlights what it rests on.
4
4
  export function mount(el) {
5
5
  const root = el.parentElement;
@@ -1,21 +1,21 @@
1
1
  # Command - a list you search and pick from with the keyboard (shadcn's cmdk). Typing keeps what matches, accent-
2
- # insensitive; arrows move, Enter picks; groups with nothing left hide; «empty» says when nothing matches. The page
2
+ # insensitive; arrows move, Enter picks; groups with nothing left hide; “empty” says when nothing matches. The page
3
3
  # owns the query text (read it to search the server too); each item runs its own action.
4
- # Command(query: q, placeholder: "Cliente, pedido o acción…") {
5
- # CommandGroup(label: "Acciones") { CommandItem(label: "Nueva cita", shortcut: "N", onSelect: create) { Icon "lucide:plus" } }
4
+ # Command(query: q, placeholder: "Customer, order or action…") {
5
+ # CommandGroup(label: "Actions") { CommandItem(label: "New appointment", shortcut: "N", onSelect: create) { Icon "lucide:plus" } }
6
6
  # CommandSeparator()
7
- # CommandGroup(label: "Clientes") { CommandItem(label: "Laura Méndez", keywords: "300 112 4410", onSelect: openLaura) { Icon "lucide:user" } }
7
+ # CommandGroup(label: "Customers") { CommandItem(label: "Laura Méndez", keywords: "300 112 4410", onSelect: openLaura) { Icon "lucide:user" } }
8
8
  # }
9
9
  # Inside a Modal it is the ⌘K palette: CommandPalette. shadcn-for-muten · parts + Custom (CommandBehavior) · Nova (.cn-command*).
10
- part Command(query: text, placeholder: text = "Busca…", empty: text = "Nada coincide.") {
10
+ part Command(query: text, placeholder: text = "Search…", empty: text = "Nothing matches.") {
11
11
  Stack class("cn-command cx-command") data(slot: "command") {
12
12
  Stack class("cn-command-input-wrapper cx-command-input") data(slot: "command-input-wrapper") {
13
13
  Icon "lucide:search" class("cn-command-input-icon")
14
- SearchField bind($query) "{$placeholder}" class("cn-command-input") data(slot: "command-input")
14
+ SearchField bind($query) "{$placeholder}" class("cn-command-input") data(slot: "command-input", trp: $placeholder)
15
15
  }
16
16
  Stack class("cn-command-list cx-command-list") data(slot: "command-list") aria(role: "listbox") {
17
17
  slot
18
- Span "{$empty}" class("cn-command-empty cx-command-empty") data(slot: "command-empty")
18
+ Span "{$empty}" class("cn-command-empty cx-command-empty") data(slot: "command-empty", tr: $empty)
19
19
  }
20
20
  CommandBehavior()
21
21
  }
@@ -1,9 +1,9 @@
1
1
  # ContextMenu - the menu of a zone, opened with a right click (or a long press on a phone, or the keyboard's menu key)
2
2
  # right where the pointer is. The zone and the menu go inside; the menu uses DropdownContent and its items.
3
3
  # ContextMenu {
4
- # ContextTrigger { Stack class("…") { Span "Clic derecho aquí" } }
5
- # DropdownContent { DropdownItem(label: "Duplicar", onSelect: dup) { Icon "lucide:copy" } … }
4
+ # ContextTrigger { Stack class("…") { Span "Right click here" } }
5
+ # DropdownContent { DropdownItem(label: "Duplicate", onSelect: dup) { Icon "lucide:copy" } … }
6
6
  # }
7
7
  # shadcn-for-muten · parts + Custom (OverlayBehavior) · Nova (dropdown classes) + kit.css.
8
8
  part ContextMenu() { Stack class("cx-ov cx-ov-zone") data(slot: "context-menu") { slot OverlayBehavior(kind: "context") } }
9
- part ContextTrigger(label: text = "Menú de esta zona") { Stack class("cx-ov-trigger cx-ctx-zone") data(slot: "context-menu-trigger") aria(label: $label) { slot } }
9
+ part ContextTrigger(label: text = "Menu for this area") { Stack class("cx-ov-trigger cx-ctx-zone") data(slot: "context-menu-trigger", trl: $label) aria(label: $label) { slot } }
@@ -1,36 +1,38 @@
1
- // CountUp - a muten Custom that animates a formatted stat from 0 to its target the first time it scrolls into
2
- // view. inputs: { to } - the final display string (e.g. "2.4M", "94%", "120K", "58", "99.99%", "40ms", "12B").
3
- // It splits an optional prefix + number (decimals/commas ok) + suffix, animates the number, then restores the
4
- // EXACT target string so formatting like "2.4M" is preserved. Pure vanilla JS - this is muten's 20% escape for
5
- // something CSS can't do (animating a formatted number). Respects prefers-reduced-motion.
1
+ // CountUp - a muten Custom: a formatted number whose digits ROLL to their value (each digit is a 0-9 column that
2
+ // slides, the kit's --t-move rhythm; only the digits that change move). inputs: { to, roll }
3
+ // to the display string, kept EXACTLY ("$4.906.000", "2.4M", "94%", "12,400", "40ms")
4
+ // roll "view" (default) rolls up from zeros the first time it scrolls into view - for a landing
5
+ // "change" stands still on the first paint and rolls each time `to` changes - for a stat on a dashboard
6
+ // When `to` changes later (in either mode) it rolls from the old value to the new one. Screen readers get the plain
7
+ // text; the columns are aria-hidden. Under prefers-reduced-motion the kit's tokens make the roll instant.
6
8
  export function mount(el, inputs) {
7
- const target = String(inputs && inputs.to != null ? inputs.to : '0');
8
- const m = target.match(/^([^\d]*)([\d.,]+)(.*)$/);
9
- if (!m) { el.textContent = target; return; }
10
- const prefix = m[1], numStr = m[2], suffix = m[3];
11
- const end = parseFloat(numStr.replace(/,/g, ''));
12
- const decimals = (numStr.split('.')[1] || '').length;
13
- const grouped = numStr.indexOf(',') !== -1;
14
- const fmt = (v) => prefix + (decimals ? v.toFixed(decimals) : (grouped ? Math.round(v).toLocaleString() : String(Math.round(v)))) + suffix;
9
+ const isDigit = (ch) => ch >= '0' && ch <= '9';
10
+ const reader = document.createElement('span'); reader.className = 'cx-roll-sr';
11
+ const face = document.createElement('span'); face.className = 'cx-roll'; face.setAttribute('aria-hidden', 'true');
12
+ el.replaceChildren(reader, face);
13
+ let shown = '';
15
14
 
16
- const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
17
- if (reduce || !('IntersectionObserver' in window) || !isFinite(end)) { el.textContent = target; return; }
15
+ const column = () => { const d = document.createElement('span'); d.className = 'cx-roll-d'; const s = document.createElement('span'); s.innerHTML = '<em>0</em><em>1</em><em>2</em><em>3</em><em>4</em><em>5</em><em>6</em><em>7</em><em>8</em><em>9</em>'; d.appendChild(s); return d; };
16
+ const glyph = (ch) => { const g = document.createElement('span'); g.className = 'cx-roll-s'; g.textContent = ch; return g; };
17
+ const setDigit = (d, n, animate) => { const s = d.firstChild; if (!animate) { s.style.transition = 'none'; s.style.translate = `0 -${n}em`; void s.offsetHeight; s.style.transition = ''; } else s.style.translate = `0 -${n}em`; };
18
18
 
19
- el.textContent = fmt(0);
20
- let started = false;
21
- const run = () => {
22
- if (started) return; started = true;
23
- const dur = 1400, t0 = performance.now();
24
- const tick = (now) => {
25
- const p = Math.min(1, (now - t0) / dur);
26
- const eased = 1 - Math.pow(1 - p, 3); // ease-out cubic
27
- if (p < 1) { el.textContent = fmt(end * eased); requestAnimationFrame(tick); }
28
- else el.textContent = target; // land on the exact target (keeps "2.4M"/"99.99%" formatting)
29
- };
30
- requestAnimationFrame(tick);
19
+ // same shape (same length, digits and signs in the same places): reuse the columns so each digit rolls from where
20
+ // it is; another shape: rebuild, the digits starting from 0 when animating (or from their value when not)
21
+ const paint = (text, animate) => {
22
+ reader.textContent = text;
23
+ const same = shown.length === text.length && [...text].every((ch, k) => isDigit(ch) === isDigit(shown[k]) && (isDigit(ch) || ch === shown[k]));
24
+ if (!same) { face.replaceChildren(...[...text].map((ch) => (isDigit(ch) ? column() : glyph(ch)))); [...face.children].forEach((c, k) => { if (isDigit(text[k])) setDigit(c, animate ? 0 : +text[k], false); }); }
25
+ if (animate) requestAnimationFrame(() => requestAnimationFrame(() => [...face.children].forEach((c, k) => { if (isDigit(text[k])) setDigit(c, +text[k], true); })));
26
+ else [...face.children].forEach((c, k) => { if (isDigit(text[k])) setDigit(c, +text[k], false); });
27
+ shown = text;
31
28
  };
32
- const io = new IntersectionObserver((entries) => {
33
- for (const e of entries) if (e.isIntersecting) { run(); io.disconnect(); }
34
- }, { threshold: 0.5 });
35
- io.observe(el);
29
+
30
+ const first = String(inputs?.to ?? '0');
31
+ if (inputs?.roll === 'change' || !('IntersectionObserver' in window)) paint(first, false);
32
+ else {
33
+ paint(first.replace(/\d/g, '0'), false); reader.textContent = first;
34
+ const io = new IntersectionObserver((entries) => { if (entries.some((e) => e.isIntersecting)) { io.disconnect(); paint(first, true); } }, { threshold: 0.5 });
35
+ io.observe(el);
36
+ }
37
+ return (next) => { const t = String(next?.to ?? '0'); if (t !== shown) paint(t, true); };
36
38
  }
@@ -1,5 +1,7 @@
1
- # CountUp - a number that counts up from 0 the first time it comes into view, keeping its exact format («2.4M»,
2
- # «94 %», «12,400», «$1.2M»). It takes the size of what holds it. Off with reduced motion.
3
- # Stat(label: "citas al mes") { CountUp(to: "12,400") }
1
+ # CountUp - a number whose digits roll to their value, keeping its exact format (“2.4M”, “94 %”, “12,400”, “$1.2M”).
2
+ # roll "view" (default): rolls up from zeros the first time it comes into view (a landing). roll "change": still on the
3
+ # first paint, rolls whenever `to` changes (a dashboard; StatCard uses it for its value). It takes the size of what
4
+ # holds it; instant with reduced motion.
5
+ # Stat(label: "appointments a month") { CountUp(to: "12,400") } StatCard(…, value: total) → rolls when total changes
4
6
  # shadcn-for-muten · Custom · host JS count-up.js.
5
- part CountUp(to: text) { Custom CountUp inputs(to: $to) class("cx-countup") }
7
+ part CountUp(to: text, roll: text = "view") { Custom CountUp inputs(to: $to, roll: $roll) class("cx-countup") }
@@ -1,5 +1,5 @@
1
1
  // DateField (muten Custom) - the kit's dateField (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.dateField(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.dateField(el, inputs, core, on)));
4
4
  return () => {};
5
5
  }
@@ -1,7 +1,7 @@
1
1
  # DateField - a field-like button with the long date; it opens the day / month / year wheels in the adaptive modal.
2
- # min "today" turns the past off (the wheel greys those days and «Listo» waits).
3
- # DateField(label: "Fecha", value: "today", min: "today", onChange: setDay) → "yyyy-mm-dd"
2
+ # min "today" turns the past off (the wheel greys those days and “Done” waits).
3
+ # DateField(label: "Date", value: "today", min: "today", onChange: setDay) → "yyyy-mm-dd"
4
4
  # shadcn-for-muten · Custom (kit dates) · host JS date-field.js.
5
- part DateField(label: text = "Fecha", value: text = "today", min: text = "", years: number = 2, onChange: action) {
5
+ part DateField(label: text = "Date", value: text = "today", min: text = "", years: number = 2, onChange: action) {
6
6
  Custom DateField inputs(label: $label, value: $value, min: $min, years: $years) on(change: $onChange) class("cx-df-host")
7
7
  }
@@ -1,11 +1,11 @@
1
1
  # DayPlan - the owner's day as a list in order: hour, a dot on a line, who and what, a tag on the right. The hours
2
2
  # already gone are faded, a red line says what time it is now, and one free slot can fill itself in a loop (a new
3
- # booking drops in with a green ring and the count steps up) - a picture of «your day keeps itself», not the Agenda.
4
- # DayPlan(title: "Hoy", date: "viernes 9 de octubre", count: "5 citas", countNext: "6 citas") {
5
- # DayPlanRow(time: "9:00 AM", title: "Pedro Ruiz", detail: "Corte clásico", tag: "Atendido", state: "done")
3
+ # booking drops in with a green ring and the count steps up) - a picture of “your day keeps itself”, not the Agenda.
4
+ # DayPlan(title: "Today", date: "Friday, October 9", count: "5 appointments", countNext: "6 appointments") {
5
+ # DayPlanRow(time: "9:00 AM", title: "Pedro Ruiz", detail: "Classic cut", tag: "Served", state: "done")
6
6
  # DayPlanNow(time: "3:12 PM")
7
- # DayPlanRow(time: "3:30 PM", title: "Laura Gómez", detail: "Corte y barba", tag: "Confirmada", tone: "ok")
8
- # DayPlanRow(time: "4:30 PM", title: "Carla Méndez", detail: "Corte clásico", tag: "La agendó tu asistente", tone: "ai", state: "fill", free: "Libre")
7
+ # DayPlanRow(time: "3:30 PM", title: "Laura Gómez", detail: "Cut and beard", tag: "Confirmed", tone: "ok")
8
+ # DayPlanRow(time: "4:30 PM", title: "Carla Méndez", detail: "Classic cut", tag: "Your assistant booked it", tone: "ai", state: "fill", free: "Free")
9
9
  # }
10
10
  # state: "" | done (gone, faded) | fill (shows `free` dashed, then fills; one per plan). tone: "" | ok | ai.
11
11
  # countNext: the count after the fill (empty = no count change). shadcn-for-muten · part · no deps · kit.css (.cx-dp).
@@ -3,11 +3,11 @@
3
3
  # dots and, when given, the address). Put an Image, or any content, in the slot; an Image fills the screen from the top.
4
4
  # height crops it (the whole device fades out at the bottom), so a tall screenshot can sit beside a short text. Every
5
5
  # measure of the phone follows its width, so it keeps the same proportions at any size. ChatPlay wears this same phone.
6
- # DeviceFrame { Image "~/shots/store.png" alt("Tu tienda en un teléfono") }
6
+ # DeviceFrame { Image "~/shots/store.png" alt("Your shop on a phone") }
7
7
  # DeviceFrame(island: "on", width: "300px") { Image "~/shots/app.png" alt("La app") }
8
8
  # DeviceFrame(kind: "browser", url: "storio.ink/elroble", height: "420px") { Image "~/shots/panel.png" alt("Tu panel") }
9
- # DeviceFrame(href: "/aurora", label: "Estudio Aurora, una tienda de muestra", shot: "/stores/aurora.webp")
10
- # width: empty (a phone is 280 px, a browser fills its column) or a width like «320px» or «100%». island: "on" draws the
9
+ # DeviceFrame(href: "/aurora", label: "Estudio Aurora, a sample shop", shot: "/stores/aurora.webp")
10
+ # width: empty (a phone is 280 px, a browser fills its column) or a width like “320px” or “100%”. island: "on" draws the
11
11
  # island (leave it off over a page that has its own top bar). kind: phone | browser.
12
12
  # href: the whole device is a link there (it lifts on hover); label says where, for a screen reader.
13
13
  # shot: a picture of the screen (390 × 844 at twice the pixels) laid as its background. Unlike an Image it is not
@@ -1,6 +1,6 @@
1
1
  # Dialog - a window in the middle of the screen for one task (edit, confirm a detail), on every width. It is the
2
2
  # adaptive Modal forced to dialog; on a phone prefer Modal (it becomes a Drawer). The page owns open.
3
- # Dialog(open: editing, onClose: done, title: "Editar servicio", description: "…") { … ModalFooter { … } }
3
+ # Dialog(open: editing, onClose: done, title: "Edit service", description: "…") { … ModalFooter { … } }
4
4
  # shadcn-for-muten · part over Modal.
5
5
  part Dialog(open: bool, onClose: action, title: text, description: text = "", size: text = "") {
6
6
  Modal(open: $open, onClose: $onClose, title: $title, description: $description, mode: "dialog", size: $size) { slot }
@@ -1,5 +1,5 @@
1
- // DirtyBar (muten Custom) - the kit's «unsaved changes» bar over the form it sits in (kit/core.js kit.dirtyBar).
2
- // «Descartar» restores what muten owns: inputs get their saved value (and an input event, so muten's bind follows),
1
+ // DirtyBar (muten Custom) - the kit's “unsaved changes” bar over the form it sits in (kit/core.js kit.dirtyBar).
2
+ // “Descartar” restores what muten owns: inputs get their saved value (and an input event, so muten's bind follows),
3
3
  // switches that moved are toggled back by a click (so the page's action runs). The weekly hours restore themselves.
4
4
  export function mount(el, inputs, on) {
5
5
  import('@muten/shadcn/registry/kit/core.js').then(({ kit }) => {
@@ -1,7 +1,7 @@
1
- # DirtyBar - «Tienes cambios sin guardar»: a floating footer of the form it sits in. It appears when any field,
2
- # switch or time differs from the last save, «Descartar» puts everything back, «Guardar» saves in three moments.
1
+ # DirtyBar - “You have unsaved changes”: a floating footer of the form it sits in. It appears when any field,
2
+ # switch or time differs from the last save, “Descartar” puts everything back, “Save” saves in three moments.
3
3
  # Put it as the LAST child of a SettingsForm.
4
- # SettingsForm { SettingsSection(title: "Tu negocio", description: "…") { Field(…) { Input(…) } } DirtyBar(onSave: save) }
4
+ # SettingsForm { SettingsSection(title: "Your business", description: "…") { Field(…) { Input(…) } } DirtyBar(onSave: save) }
5
5
  # shadcn-for-muten · part + Custom (DirtyBar, kit core) · kit.css (.cx-dirty, .cx-set-form).
6
6
  part SettingsForm() { Stack class("cx-set-form") { slot } }
7
7
  part SettingsSection(title: text, description: text = "") {
@@ -1,6 +1,6 @@
1
1
  # Drawer - a sheet that rises from the bottom, with a handle you drag down to close (the phone's way), on every width.
2
2
  # It is the adaptive Modal forced to drawer. The page owns open.
3
- # Drawer(open: picking, onClose: done, title: "Elige la hora") { … ModalFooter { … } }
3
+ # Drawer(open: picking, onClose: done, title: "Pick the time") { … ModalFooter { … } }
4
4
  # shadcn-for-muten · part over Modal.
5
5
  part Drawer(open: bool, onClose: action, title: text = "", description: text = "", size: text = "") {
6
6
  Modal(open: $open, onClose: $onClose, title: $title, description: $description, mode: "drawer", size: $size) { slot }
@@ -1,13 +1,13 @@
1
1
  # DropdownMenu - a list of actions that opens from a button (shadcn Nova). Arrows, Home/End and the first letter move;
2
2
  # Enter picks; a picked action closes it, a check or radio item stays open. Trigger and content go inside:
3
3
  # DropdownMenu {
4
- # DropdownTrigger { TriggerBtn { Span "Opciones" Icon "lucide:chevron-down" } }
4
+ # DropdownTrigger { TriggerBtn { Span "Options" Icon "lucide:chevron-down" } }
5
5
  # DropdownContent(align: "start") {
6
- # DropdownLabel(label: "Pedido")
7
- # DropdownItem(label: "Editar", shortcut: "⌘E", onSelect: edit) { Icon "lucide:pencil" }
6
+ # DropdownLabel(label: "Order")
7
+ # DropdownItem(label: "Edit", shortcut: "⌘E", onSelect: edit) { Icon "lucide:pencil" }
8
8
  # DropdownSeparator()
9
9
  # DropdownCheckItem(label: "Avisarme", on: notify, onToggle: toggleNotify)
10
- # DropdownItem(label: "Eliminar", tone: "destructive", onSelect: remove) { Icon "lucide:trash-2" }
10
+ # DropdownItem(label: "Delete", tone: "destructive", onSelect: remove) { Icon "lucide:trash-2" }
11
11
  # }
12
12
  # }
13
13
  # The same items and content serve ContextMenu and Menubar. shadcn-for-muten · parts + Custom (OverlayBehavior) · Nova.