@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,7 +1,7 @@
1
- # Toggle - a button that stays pressed (bold, favourite, «solo hoy»). The page owns the bool and the action.
1
+ # Toggle - a button that stays pressed (bold, favourite, “today only”). The page owns the bool and the action.
2
2
  # state { bold = false : bool } action toggleBold() mutates bold { bold.toggle() }
3
- # Toggle(on: bold, onToggle: toggleBold, label: "Negrita") { Icon "lucide:bold" }
4
- # Toggle(on: today, onToggle: toggleToday, variant: "outline") { Icon "lucide:calendar" Span "Solo hoy" }
3
+ # Toggle(on: bold, onToggle: toggleBold, label: "Bold") { Icon "lucide:bold" }
4
+ # Toggle(on: today, onToggle: toggleToday, variant: "outline") { Icon "lucide:calendar" Span "Today only" }
5
5
  # variant: default | outline · size: sm | default | lg · label: the name of an icon-only toggle.
6
6
  # shadcn-for-muten · part · no deps · Nova (.cn-toggle, pressed = aria-pressed).
7
7
  part Toggle(on: bool, onToggle: action, variant: text = "default", size: text = "default", label: text = "") {
@@ -1,11 +1,11 @@
1
1
  # Toolbar - the tools of an editor in one bar: groups of icon buttons (Btn, Toggle, ToggleGroup) split by thin lines.
2
2
  # It floats over a canvas (CanvasViewport) or sits on top of a panel.
3
- # Toolbar(label: "Herramientas") {
4
- # ToolbarGroup { ToggleGroup(label: "Herramienta") { ToggleGroupItem(value: "select", current: tool, onSelect: setTool, label: "Seleccionar") { Icon "lucide:mouse-pointer-2" } … } }
3
+ # Toolbar(label: "Tools") {
4
+ # ToolbarGroup { ToggleGroup(label: "Tool") { ToggleGroupItem(value: "select", current: tool, onSelect: setTool, label: "Select") { Icon "lucide:mouse-pointer-2" } … } }
5
5
  # ToolbarSeparator()
6
- # ToolbarGroup { Btn(variant: "ghost", size: "icon-sm", label: "Deshacer", onClick: undo) { Icon "lucide:undo-2" } }
6
+ # ToolbarGroup { Btn(variant: "ghost", size: "icon-sm", label: "Undo", onClick: undo) { Icon "lucide:undo-2" } }
7
7
  # }
8
8
  # shadcn-for-muten · parts · no deps · kit.css (.cx-toolbar).
9
- part Toolbar(label: text = "Herramientas") { Stack class("cx-toolbar") data(slot: "toolbar") aria(role: "toolbar", label: $label) { slot } }
9
+ part Toolbar(label: text = "Tools") { Stack class("cx-toolbar") data(slot: "toolbar", trl: $label) aria(role: "toolbar", label: $label) { slot } }
10
10
  part ToolbarGroup() { Stack class("cx-toolbar-group") data(slot: "toolbar-group") { slot } }
11
11
  part ToolbarSeparator() { Stack class("cx-toolbar-sep") data(slot: "toolbar-separator") aria(role: "separator", orientation: "vertical") }
@@ -1,8 +1,8 @@
1
1
  # Tooltip - a short name that shows while the pointer rests on something (or it gets the focus): what an icon-only
2
2
  # button does, the full text of a cut-off line. Only a label: never a place for something the person must read.
3
3
  # The kit's single tooltip (KitLayer) does it for anything with a name; Tooltip just gives that name.
4
- # Tooltip(text: "Editar servicio") { Btn(variant: "ghost", size: "icon", onClick: edit) { Icon "lucide:pencil" } }
4
+ # Tooltip(text: "Edit service") { Btn(variant: "ghost", size: "icon", onClick: edit) { Icon "lucide:pencil" } }
5
5
  # A Btn with label already has one. shadcn-for-muten · part · the kit's tooltip (.cx-tip).
6
6
  # side: "right" puts it beside the trigger instead of above (a vertical rail, where above covers the item before it).
7
- # Tooltip(text: "Traer de Linktree o Shopify", side: "right") { Link … }
7
+ # Tooltip(text: "Bring from Linktree or Shopify", side: "right") { Link … }
8
8
  part Tooltip(text: text, side: text = "") { Stack class("cx-tip-wrap") data(slot: "tooltip-trigger", name: $text, side: $side) { slot } }
@@ -62,55 +62,68 @@ export function mount(el, inputs, handlers) {
62
62
  const seed = () => { for (const n of data) if (Array.isArray(n.children)) expanded.add('/' + n.name); if (selected) expandTo(data, ''); };
63
63
  seed();
64
64
 
65
+ // Built ONCE per data: a folder is its row plus a group that folds open and closed (grid rows 0fr ↔ 1fr, the
66
+ // accordion's 200 ms), its chevron turns, and picking a leaf only moves aria-selected. Nothing is rebuilt on a
67
+ // click, so the row under the pointer keeps its hover, the tree never jumps in height and keeps its width.
68
+ const CHEV = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>';
69
+ let root = null;
65
70
  const build = (nodes, container, depth, parent) => {
66
71
  for (const node of nodes) {
67
72
  const path = parent + '/' + node.name;
68
73
  const isFolder = Array.isArray(node.children);
69
74
  const id = idOf(node, path);
70
- const row = document.createElement('button'); row.type = 'button'; row.className = 'cx-tree-row'; row.setAttribute('role', 'treeitem'); row.tabIndex = -1; row.dataset.path = path; if (isFolder) row.setAttribute('aria-expanded', expanded.has(path)); row.setAttribute('aria-level', depth + 1);
75
+ const row = document.createElement('button'); row.type = 'button'; row.className = 'cx-tree-row'; row.setAttribute('role', 'treeitem'); row.tabIndex = -1; row.dataset.path = path; row.dataset.id = id; row.setAttribute('aria-level', depth + 1);
71
76
  row.style.paddingLeft = (depth * 16 + 8) + 'px';
72
- if (!isFolder && id === selected) { row.setAttribute('aria-selected', 'true'); row.tabIndex = 0; }
73
- const chev = document.createElement('span'); chev.className = 'cx-tree-chev' + (isFolder ? '' : ' cx-tree-chev-none');
74
- // lucide chevron-down (open) / chevron-right (closed)
75
- chev.innerHTML = isFolder ? '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="' + (expanded.has(path) ? 'm6 9 6 6 6-6' : 'm9 18 6-6-6-6') + '"/></svg>' : '';
77
+ if (!isFolder) row.dataset.leaf = '';
78
+ const chev = document.createElement('span'); chev.className = 'cx-tree-chev' + (isFolder ? '' : ' cx-tree-chev-none'); chev.innerHTML = isFolder ? CHEV : '';
76
79
  const ic = document.createElement('span'); ic.className = 'cx-tree-ic'; ic.innerHTML = isFolder ? FOLDER : FILE;
77
80
  const label = document.createElement('span'); label.className = 'cx-tree-label'; label.textContent = node.name;
78
- row.appendChild(chev); row.appendChild(ic); row.appendChild(label);
79
- row.addEventListener('click', () => {
80
- if (isFolder) { expanded.has(path) ? expanded.delete(path) : expanded.add(path); } // folders: expand only
81
- else { selected = id; if (handlers.select) handlers.select(node.value != null ? node.value : path); } // leaves emit
82
- render();
83
- });
81
+ row.append(chev, ic, label);
84
82
  container.appendChild(row);
85
- if (isFolder && expanded.has(path)) build(node.children, container, depth + 1, path);
83
+ if (isFolder) {
84
+ row.setAttribute('aria-expanded', String(expanded.has(path)));
85
+ const fold = document.createElement('div'); fold.className = 'cx-tree-fold'; fold.setAttribute('role', 'group');
86
+ const inner = document.createElement('div'); inner.className = 'cx-tree-inner';
87
+ fold.appendChild(inner); container.appendChild(fold);
88
+ build(node.children, inner, depth + 1, path);
89
+ }
90
+ if (!isFolder) row.addEventListener('click', () => { selected = id; mark(); if (handlers.select) handlers.select(node.value != null ? node.value : path); });
91
+ else row.addEventListener('click', () => { expanded.has(path) ? expanded.delete(path) : expanded.add(path); row.setAttribute('aria-expanded', String(expanded.has(path))); visible(); });
86
92
  }
87
93
  };
94
+ // the rows a keyboard can reach: not inside a closed folder
95
+ const visible = () => { const all = [...root.querySelectorAll('.cx-tree-row')]; const shown = all.filter((r) => !r.parentElement.closest('.cx-tree-fold') || ![...ancestorsOf(r)].some((f) => f.previousElementSibling?.getAttribute('aria-expanded') !== 'true')); all.forEach((r) => { r.inert = !shown.includes(r); }); return shown; };
96
+ const ancestorsOf = function* (r) { let f = r.parentElement.closest('.cx-tree-fold'); while (f) { yield f; f = f.parentElement.closest('.cx-tree-fold'); } };
97
+ const mark = () => { root.querySelectorAll('.cx-tree-row').forEach((r) => { const on = r.hasAttribute('data-leaf') && r.dataset.id === selected; if (on) r.setAttribute('aria-selected', 'true'); else r.removeAttribute('aria-selected'); }); roving(); };
98
+ const roving = () => { const rows = visible(); const cur = rows.find((r) => r.dataset.path === focusPath) || rows.find((r) => r.getAttribute('aria-selected') === 'true') || rows[0]; rows.forEach((r) => { r.tabIndex = r === cur ? 0 : -1; }); return cur; };
88
99
  let focusPath = null;
89
100
  const render = () => {
90
- el.innerHTML = ''; const root = document.createElement('div'); root.className = 'cx-tree'; root.setAttribute('role', 'tree'); root.setAttribute('aria-label', inputs.label || 'Archivos');
91
- build(data, root, 0, ''); el.appendChild(root);
92
- const rows = [...root.querySelectorAll('.cx-tree-row')];
93
- if (!rows.some((r) => r.tabIndex === 0) && rows[0]) rows[0].tabIndex = 0;
94
- if (focusPath) { const r = rows.find((x) => x.dataset.path === focusPath); if (r) { rows.forEach((x) => { x.tabIndex = -1; }); r.tabIndex = 0; r.focus({ preventScroll: true }); } }
101
+ el.innerHTML = ''; root = document.createElement('div'); root.className = 'cx-tree'; root.setAttribute('role', 'tree'); root.setAttribute('aria-label', inputs.label || 'Archivos');
102
+ build(data, root, 0, ''); el.appendChild(root); mark();
95
103
  };
96
104
  // keyboard (WAI tree): Up/Down move, Right opens a folder (or steps in), Left closes it (or goes to its parent), Enter/Space pick
97
105
  el.addEventListener('keydown', (e) => {
98
- const rows = [...el.querySelectorAll('.cx-tree-row')], k = rows.indexOf(document.activeElement); if (k < 0) return;
106
+ const rows = visible(), k = rows.indexOf(document.activeElement); if (k < 0) return;
99
107
  const row = rows[k], folder = row.hasAttribute('aria-expanded'), open = row.getAttribute('aria-expanded') === 'true';
100
- const go = (r) => { if (!r) return; rows.forEach((x) => { x.tabIndex = -1; }); r.tabIndex = 0; r.focus(); focusPath = r.dataset.path; };
108
+ const go = (r) => { if (!r) return; focusPath = r.dataset.path; roving(); r.focus(); };
101
109
  if (e.key === 'ArrowDown') { e.preventDefault(); go(rows[k + 1]); }
102
110
  else if (e.key === 'ArrowUp') { e.preventDefault(); go(rows[k - 1]); }
103
111
  else if (e.key === 'Home') { e.preventDefault(); go(rows[0]); }
104
112
  else if (e.key === 'End') { e.preventDefault(); go(rows[rows.length - 1]); }
105
- else if (e.key === 'ArrowRight' && folder) { e.preventDefault(); if (!open) { focusPath = row.dataset.path; row.click(); } else go(rows[k + 1]); }
106
- else if (e.key === 'ArrowLeft') { e.preventDefault(); if (folder && open) { focusPath = row.dataset.path; row.click(); } else { const parent = row.dataset.path.split('/').slice(0, -1).join('/'); go(rows.find((x) => x.dataset.path === parent)); } }
113
+ else if (e.key === 'ArrowRight' && folder) { e.preventDefault(); if (!open) row.click(); else go(rows[k + 1]); }
114
+ else if (e.key === 'ArrowLeft') { e.preventDefault(); if (folder && open) row.click(); else { const parent = row.dataset.path.split('/').slice(0, -1).join('/'); go(rows.find((x) => x.dataset.path === parent)); } }
107
115
  });
108
- el.addEventListener('click', (e) => { const r = e.target.closest('.cx-tree-row'); if (r) focusPath = r.dataset.path; }, true);
116
+ el.addEventListener('click', (e) => { const r = e.target.closest('.cx-tree-row'); if (r) { focusPath = r.dataset.path; roving(); } }, true);
109
117
 
110
118
  render();
119
+ // new data rebuilds (once); a new `selected` only moves the mark and opens the folders down to it
120
+ let lastData = inputs.data;
111
121
  return (n) => {
112
- data = toTree(n);
113
- if (n.selected != null) { selected = String(n.selected); if (selected) expandTo(data, ''); } // re-open to the controlled leaf
114
- render();
122
+ if (n.data !== lastData) { lastData = n.data; data = toTree(n); if (n.selected != null) selected = String(n.selected); if (selected) expandTo(data, ''); render(); return; }
123
+ if (n.selected != null && String(n.selected) !== selected) {
124
+ selected = String(n.selected); if (selected) expandTo(data, '');
125
+ root.querySelectorAll('.cx-tree-row[aria-expanded]').forEach((r) => r.setAttribute('aria-expanded', String(expanded.has(r.dataset.path))));
126
+ mark();
127
+ }
115
128
  };
116
129
  }
@@ -4,6 +4,6 @@
4
4
  # flat: a list of rows grouped by a key - groupKey makes the folders, labelKey the text, valueKey what a file emits.
5
5
  # TreeView(data: @tiles, labelKey: "name", valueKey: "url", groupKey: "folder", selected: tileUrl, onSelect: pick)
6
6
  # selected highlights that file and opens the folders above it. shadcn-for-muten · Custom · kit.css (.cx-tree) · tree-view.js.
7
- part TreeView(data: list, labelKey: text = "", valueKey: text = "", groupKey: text = "", selected: text = "", label: text = "Archivos", onSelect: action) {
7
+ part TreeView(data: list, labelKey: text = "", valueKey: text = "", groupKey: text = "", selected: text = "", label: text = "Files", onSelect: action) {
8
8
  Custom TreeView inputs(data: $data, labelKey: $labelKey, valueKey: $valueKey, groupKey: $groupKey, selected: $selected, label: $label) on(select: $onSelect) class("cx-tree-host")
9
9
  }
@@ -1,6 +1,6 @@
1
1
  # Veil - a calm note over a card's content while it has nothing to show yet: the content stays behind, faded (ghost
2
2
  # rows, an empty chart), and a title and a line say what will appear. With `on` false it is the content alone.
3
- # Veil(on: noSales, title: "Nada vendido todavía", text: "Lo que más te reserven y te compren aparece aquí.") { … }
3
+ # Veil(on: noSales, title: "Nothing sold yet", text: "What people book and buy from you most shows here.") { … }
4
4
  # StackedBars and Heatmap carry their own (empty / emptyTitle); use Veil for anything else.
5
5
  # shadcn-for-muten · part · no deps · kit.css (.cx-veil).
6
6
  part Veil(on: bool, title: text = "", text: text = "") {
@@ -1,5 +1,5 @@
1
1
  // VirtualList (muten Custom) - only the visible rows exist in the DOM (kit/collections.js virtualList).
2
2
  export function mount(el, inputs) {
3
- Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/collections.js')]).then(([core, c]) => c.virtualList(el, inputs, core));
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.virtualList(el, inputs, core)));
4
4
  return () => {};
5
5
  }
@@ -1,6 +1,6 @@
1
1
  # VirtualList - thousands of rows, only the visible ones (+ a margin) in the DOM; arrows, Page Up/Down, Home/End.
2
- # VirtualList(rows: @clients, label: "Clientes", jump: 5000) rows: {title, subtitle, value}
2
+ # VirtualList(rows: @clients, label: "Customers", jump: 5000) rows: {title, subtitle, value}
3
3
  # total > rows.length repeats the rows up to it (a long demo). shadcn-for-muten · Custom (kit collections).
4
- part VirtualList(rows: list, total: number = 0, rowHeight: number = 52, label: text = "Lista", jump: number = 0) {
4
+ part VirtualList(rows: list, total: number = 0, rowHeight: number = 52, label: text = "List", jump: number = 0) {
5
5
  Custom VirtualList inputs(rows: $rows, total: $total, rowHeight: $rowHeight, label: $label, jump: $jump) class("cx-vl-host")
6
6
  }
@@ -5,7 +5,7 @@
5
5
  // average from the rows on screen. Plain DOM, no kit.
6
6
  export function mount(el, inputs, on) {
7
7
  let count = Math.max(0, Number(inputs.count) || 0), guess = Number(inputs.item) || 72, over = Math.max(0, Number(inputs.over ?? 6));
8
- // -1: the first range is always said, even «0,0» (nothing on screen yet), or the page keeps its own default
8
+ // -1: the first range is always said, even “0,0” (nothing on screen yet), or the page keeps its own default
9
9
  let from = -1, to = -1, step = 0, raf = 0, list = null, scroller = null, basePad = 0, wired = false;
10
10
  // the real height (plus gap, screen px) of every row already drawn, by its place in the list. Rows of very different
11
11
  // heights (a group of 2 next to one of 50) no longer move the scrollbar as they come and go: a row seen once keeps its
@@ -44,7 +44,7 @@ export function mount(el, inputs, on) {
44
44
  if (to > from && parts.length === to - from) {
45
45
  for (let i = 0; i < parts.length; i++) {
46
46
  const shown = parts[i].filter((n) => n.getClientRects().length);
47
- // a row with nothing shown (an empty group) takes no room and no gap: it is 0, not «unknown»
47
+ // a row with nothing shown (an empty group) takes no room and no gap: it is 0, not “unknown”
48
48
  if (!shown.length) { sizes[from + i] = 0; continue; }
49
49
  const h = shown[shown.length - 1].getBoundingClientRect().bottom - shown[0].getBoundingClientRect().top;
50
50
  if (h > 0) sizes[from + i] = h + gap;
@@ -71,7 +71,7 @@ export function mount(el, inputs, on) {
71
71
  raf = 0; if (!list) return;
72
72
  gapNow = -1;
73
73
  // a list that is not on the page right now (a hidden panel, a closed tab: no box at all) keeps the rows it has. Its
74
- // scroller measures 0 × 0, which read as «scrolled to the end» and drew EVERY row into a panel nobody could see
74
+ // scroller measures 0 × 0, which read as “scrolled to the end” and drew EVERY row into a panel nobody could see
75
75
  if (!list.getClientRects().length || (scroller && !scroller.clientHeight)) return;
76
76
  learn();
77
77
  const [top, bottom] = view();
@@ -100,7 +100,7 @@ export function mount(el, inputs, on) {
100
100
  list = el.parentElement; if (!list || wired) return; wired = true;
101
101
  for (let n = list.parentElement; n && n !== document.body && n !== document.documentElement; n = n.parentElement) { const o = getComputedStyle(n).overflowY; if (o === 'auto' || o === 'scroll') { scroller = n; break; } }
102
102
  basePad = parseFloat(getComputedStyle(list).paddingBottom) || 0;
103
- // the browser must not «hold» a row in place while rows above it come and go: that fights the window
103
+ // the browser must not “hold” a row in place while rows above it come and go: that fights the window
104
104
  list.style.overflowAnchor = 'none';
105
105
  // and a flex parent must not shrink the list to fit: its length IS the scrollbar's (the space for the rows not drawn)
106
106
  list.style.flexShrink = '0';
@@ -11,8 +11,8 @@
11
11
  # The list stays the real typed list (rows keep their keys, fields and drag); only the `where` changes. In lib.ts:
12
12
  # export function windowIds(rows: { id: string }[], seen: string): string { const [a, b] = String(seen).split(',').map(Number); return ' ' + (rows || []).slice(Number.isFinite(a) ? a : 0, Number.isFinite(b) ? b : 20).map((r) => r.id).join(' ') + ' '; }
13
13
  # export function rowKey(id: string): string { return ` ${id} `; }
14
- # (A sorted or filtered list: give windowIds the same list, in the same order, that the `each` draws. And «0,0» is a
15
- # real answer, nothing on screen yet: never read a 0 as «missing».)
14
+ # (A sorted or filtered list: give windowIds the same list, in the same order, that the `each` draws. And “0,0” is a
15
+ # real answer, nothing on screen yet: never read a 0 as “missing”.)
16
16
  # It stands in for the rows above (its own height) and pads the list below, so the scrollbar keeps the whole length.
17
17
  # count = how many rows the list has (after any filter) · item = a first guess of one row's height in px (it learns the
18
18
  # real one) · over = rows kept beyond each edge (6). The list scrolls inside its nearest `overflow: auto` parent or the
@@ -1,6 +1,6 @@
1
1
  # WordLine - a line of words set as big type: who a product is for, what a shop does. The words sit in a soft grey,
2
2
  # joined by dots in ink; a Word with tone "strong" stands out in ink too. It wraps like text.
3
- # WordLine(label: "Para quién es") { Word(label: "Barberías") Word(label: "Clases", tone: "strong") Word(label: "Spa") }
3
+ # WordLine(label: "Who it is for") { Word(label: "Barbershops") Word(label: "Classes", tone: "strong") Word(label: "Spa") }
4
4
  # shadcn-for-muten · parts · no deps · kit.css (.cx-wordline).
5
5
  part WordLine(label: text = "") { Stack class("cx-wordline") aria(label: $label) { slot } }
6
6
  part Word(label: text, tone: text = "") { Span "{$label}" class("cx-word") data(tone: $tone) }
@@ -5,7 +5,7 @@
5
5
  part ZoomControl(value: number, onOut: action, onIn: action, onReset: action) {
6
6
  Stack class("cx-zoom") data(slot: "zoom-control") aria(role: "group", label: "Zoom") {
7
7
  Btn(variant: "ghost", size: "icon-sm", label: "Alejar", onClick: $onOut) { Icon "lucide:minus" }
8
- Button "{$value} %" -> $onReset class("cn-button cn-button-variant-ghost cn-button-size-sm group/button cx-zoom-value") data(name: "Volver a 100 %") aria(label: "Zoom {$value} %, volver a 100") # its words carry $value: a part arg cannot interpolate an outer param, so it stays a Button
8
+ Button "{$value} %" -> $onReset class("cn-button cn-button-variant-ghost cn-button-size-sm group/button cx-zoom-value") data(name: "Back to 100 %", trn: "Back to 100 %", trl: "Zoom {0} %, back to 100", tra: $value) aria(label: "Zoom {$value} %, back to 100") # its words carry $value: a part arg cannot interpolate an outer param, so it stays a Button
9
9
  Btn(variant: "ghost", size: "icon-sm", label: "Acercar", onClick: $onIn) { Icon "lucide:plus" }
10
10
  }
11
11
  }
package/registry.json CHANGED
@@ -483,7 +483,9 @@
483
483
  },
484
484
  {
485
485
  "name": "item",
486
- "deps": [],
486
+ "deps": [
487
+ "list-motion"
488
+ ],
487
489
  "part": "Item",
488
490
  "file": "registry/item.muten",
489
491
  "description": "Item(variant, size) row with ItemMedia / ItemContent / ItemTitle / ItemDescription / ItemActions / ItemFooter; lists = ItemGroup + ItemSeparator."
@@ -519,6 +521,14 @@
519
521
  "component": "KitWidget",
520
522
  "description": "Host of the system kit widgets (Custom, kit core). Use the named parts."
521
523
  },
524
+ {
525
+ "name": "lang-scope",
526
+ "deps": [],
527
+ "part": "LangScope",
528
+ "file": "registry/lang-scope.muten",
529
+ "component": "LangScope",
530
+ "description": "LangScope - everything inside speaks this language (es | en …): the library's words, its dates and its Customs follow"
531
+ },
522
532
  {
523
533
  "name": "legal-doc",
524
534
  "deps": [],
@@ -533,6 +543,14 @@
533
543
  "file": "registry/link-btn.muten",
534
544
  "description": "A navigation link styled as a button. `LinkBtn(to: \"/signup\", variant: \"default\") { Span \"Get started\" }`."
535
545
  },
546
+ {
547
+ "name": "list-motion",
548
+ "deps": [],
549
+ "part": "ListMotion",
550
+ "file": "registry/list-motion.muten",
551
+ "component": "ListMotion",
552
+ "description": "Rows that open and close their place (Custom); ItemGroup carries it."
553
+ },
536
554
  {
537
555
  "name": "live-stack",
538
556
  "deps": [],
@@ -1109,7 +1127,9 @@
1109
1127
  },
1110
1128
  {
1111
1129
  "name": "timeline",
1112
- "deps": [],
1130
+ "deps": [
1131
+ "list-motion"
1132
+ ],
1113
1133
  "part": "Timeline",
1114
1134
  "file": "registry/timeline.muten",
1115
1135
  "description": "Vertical event timeline: a dot on a connector line + title/time/description per item (parts: Timeline, TimelineItem)."