@muten/shadcn 0.1.15 → 0.2.0

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 (182) hide show
  1. package/AGENTS.md +88 -1
  2. package/globals.css +1 -0
  3. package/kit.css +328 -123
  4. package/motion.css +3 -0
  5. package/package.json +1 -1
  6. package/registry/accordion.muten +3 -3
  7. package/registry/agenda.js +19 -16
  8. package/registry/agenda.muten +10 -10
  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 +197 -114
  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.muten +4 -4
  100. package/registry/money-field.js +1 -1
  101. package/registry/money-field.muten +2 -2
  102. package/registry/nav-button.muten +4 -4
  103. package/registry/navigation-menu.muten +4 -4
  104. package/registry/navpill.muten +6 -6
  105. package/registry/new-appointment.js +1 -1
  106. package/registry/new-appointment.muten +4 -4
  107. package/registry/number-field.js +6 -4
  108. package/registry/number-field.muten +1 -1
  109. package/registry/order-board.js +1 -1
  110. package/registry/order-board.muten +2 -2
  111. package/registry/order-play.muten +4 -4
  112. package/registry/pagination-nav.js +1 -1
  113. package/registry/pagination-nav.muten +3 -3
  114. package/registry/pagination.muten +4 -4
  115. package/registry/password-field.muten +2 -2
  116. package/registry/permission-matrix.muten +4 -4
  117. package/registry/phone-bar.muten +6 -6
  118. package/registry/phone-field.js +1 -1
  119. package/registry/phone-field.muten +1 -1
  120. package/registry/phone-shell.js +1 -1
  121. package/registry/pill.muten +2 -2
  122. package/registry/plans.muten +1 -1
  123. package/registry/popover.muten +2 -2
  124. package/registry/pricing.muten +4 -4
  125. package/registry/product-play.muten +3 -3
  126. package/registry/progress.muten +3 -3
  127. package/registry/queue.muten +1 -1
  128. package/registry/radio-group.muten +1 -1
  129. package/registry/rank-item.muten +4 -4
  130. package/registry/resizable.muten +1 -1
  131. package/registry/row-menu.js +1 -1
  132. package/registry/row-menu.muten +2 -2
  133. package/registry/save-button.muten +5 -5
  134. package/registry/scale-slider.js +1 -1
  135. package/registry/scale-slider.muten +2 -2
  136. package/registry/screen-tour.muten +1 -1
  137. package/registry/share-bar.muten +2 -2
  138. package/registry/share-button.muten +4 -4
  139. package/registry/sheet.muten +1 -1
  140. package/registry/sidebar.muten +12 -6
  141. package/registry/site-footer.muten +6 -6
  142. package/registry/sortable-behavior.js +67 -20
  143. package/registry/sortable-list.js +3 -1
  144. package/registry/sortable-list.muten +2 -2
  145. package/registry/sortable.muten +5 -4
  146. package/registry/spinner.muten +2 -2
  147. package/registry/split-button.muten +2 -2
  148. package/registry/stacked-bars.js +3 -3
  149. package/registry/stacked-bars.muten +2 -2
  150. package/registry/stamp-card.muten +2 -2
  151. package/registry/stat-band.muten +3 -3
  152. package/registry/stat.muten +3 -3
  153. package/registry/status-bar.muten +2 -2
  154. package/registry/stepper.muten +4 -4
  155. package/registry/store-qr.js +1 -1
  156. package/registry/store-qr.muten +2 -2
  157. package/registry/swatch.muten +1 -1
  158. package/registry/switch.muten +8 -7
  159. package/registry/table-view.muten +3 -3
  160. package/registry/tabs-behavior.js +9 -1
  161. package/registry/tabs.muten +2 -2
  162. package/registry/tag-input.js +1 -1
  163. package/registry/tag-input.muten +2 -2
  164. package/registry/text.muten +2 -2
  165. package/registry/time-field.js +1 -1
  166. package/registry/time-field.muten +3 -3
  167. package/registry/timeline.muten +4 -4
  168. package/registry/toaster.muten +2 -2
  169. package/registry/toggle-group.muten +1 -1
  170. package/registry/toggle.muten +3 -3
  171. package/registry/toolbar.muten +4 -4
  172. package/registry/tooltip.muten +2 -2
  173. package/registry/tree-view.js +38 -25
  174. package/registry/tree-view.muten +1 -1
  175. package/registry/veil.muten +1 -1
  176. package/registry/virtual-list.js +1 -1
  177. package/registry/virtual-list.muten +2 -2
  178. package/registry/virtual-window.js +4 -4
  179. package/registry/virtual-window.muten +2 -2
  180. package/registry/wordline.muten +1 -1
  181. package/registry/zoom-control.muten +1 -1
  182. package/registry.json +22 -2
@@ -1,4 +1,4 @@
1
- // kit/board.js - the order Board and its card (the reference artifact's «Tablero» and «Tarjeta: mínimo y máximo"),
1
+ // kit/board.js - the order Board and its card (the reference artifact's “Board” and “Card: minimum and maximum”),
2
2
  // composed only from atoms. Lanes and cards are data; the card groups its information by what it is about and
3
3
  // orders it by what needs the owner first. Drag with the mouse, hold on touch, Space/arrows with the keyboard.
4
4
  // lanes [{id, title, next}] next = the label of the step that moves a card INTO the following lane
@@ -9,6 +9,8 @@
9
9
  // on.change(json [{id, lane}]) after a card moves; on.action(json {id, action}) for every card button.
10
10
  export function orderBoard(root, i, core, on) {
11
11
  const { ui, $, $$, lib, openModal: open, closeModal, CONTENT, body, foot, easeHeight, money0, hue, ini } = core;
12
+ // its language: the nearest lang (the page's), English by default; the card data the page passes is said as is
13
+ const L = core.langOf(root), T = (k, ...a) => core.t(L, k, ...a);
12
14
  const list = (v) => (Array.isArray(v) ? v : []);
13
15
  const copy = (x) => JSON.parse(JSON.stringify(x));
14
16
  const CARDS = Object.fromEntries(list(i.cards).map((c) => [c.id, { actions: [], ...copy(c) }]));
@@ -43,7 +45,7 @@ export function orderBoard(root, i, core, on) {
43
45
  footer: `<div class="cx-vstack"><div class="cx-hstack">${o.photos.slice(0, 5).map((x) => ui.avatar(x, { shape: 'square', size: 'lg', bg: x })).join('')}${o.photos.length > 5 ? `<span data-slot="avatar" data-size="lg" data-shape="square" class="cn-avatar group/avatar"><span class="cn-avatar-fallback">+${o.photos.length - 5}</span></span>` : ''}</div>${o.extras?.length ? `<div class="cx-hstack">${o.extras.map((e) => ui.badge(e, { variant: 'outline' })).join('')}</div>` : ''}</div>`,
44
46
  }),
45
47
  pay: (p, c) => {
46
- const action = p.method === 'transfer' && p.tone === 'new' ? ['receipt', 'Revisar comprobante', 'image'] : p.method === 'card' ? ['stripe', 'Ver recibo de Stripe', 'card'] : null;
48
+ const action = p.method === 'transfer' && p.tone === 'new' ? ['receipt', T('Review receipt'), 'image'] : p.method === 'card' ? ['stripe', T('See Stripe receipt'), 'card'] : null;
47
49
  return ui.item({
48
50
  media: ui.itemMedia(ui.icon('card')),
49
51
  title: `<span>Pago · ${money0(c.total)}</span>`,
@@ -54,7 +56,7 @@ export function orderBoard(root, i, core, on) {
54
56
  },
55
57
  delivery: (d) => {
56
58
  const info = d.track ? `<b>${d.track.steps[d.track.at]}</b> · ${d.track.courier.split(' ')[0]}, ${d.track.eta}` : d.map ? d.map.address : '';
57
- const more = (d.track || d.map) ? ui.button({ label: d.track ? 'Ver entrega' : 'Ver mapa', size: 'xs', attrs: 'data-bact="delivery"' }) : '';
59
+ const more = (d.track || d.map) ? ui.button({ label: d.track ? T('See delivery') : T('See map'), size: 'xs', attrs: 'data-bact="delivery"' }) : '';
58
60
  return ui.item({
59
61
  media: ui.itemMedia(ui.icon(d.mode === 'pickup' ? 'store' : 'truck')),
60
62
  title: `${d.label}${d.when ? ` · ${d.when}` : ''}`,
@@ -69,18 +71,18 @@ export function orderBoard(root, i, core, on) {
69
71
  const signals = (c) => {
70
72
  const list = c.request ? (Array.isArray(c.request) ? c.request : [c.request]) : [];
71
73
  const items = list.map((r, i) => ui.button({ icon: r.icon, size: 'icon-sm', variant: 'ghost', tone: SIGNAL_TONE[r.tone] || 'info', cls: 'cx-has-indicator', attrs: `data-bact="request" data-i="${i}" data-name="${r.title}" aria-haspopup="dialog" aria-label="${r.title}${r.text ? ': ' + r.text : ''}"` }).replace('</button>', `${ui.indicator(SIGNAL_TONE[r.tone] || 'info')}</button>`));
72
- if (c.note) items.push(ui.button({ icon: 'quote', size: 'icon-sm', variant: 'ghost', tone: 'note', cls: 'cx-has-indicator', attrs: `data-bact="note" data-name="Nota del cliente" aria-haspopup="dialog" aria-label="Nota de ${c.who.split(' ')[0]}: ${c.note}"` }).replace('</button>', `${ui.indicator('note')}</button>`));
73
- return items.length ? `<div class="cx-hstack" role="group" aria-label="Pendientes">${items.join('')}</div>` : '';
74
+ if (c.note) items.push(ui.button({ icon: 'quote', size: 'icon-sm', variant: 'ghost', tone: 'note', cls: 'cx-has-indicator', attrs: `data-bact="note" data-name="${T('Client note')}" aria-haspopup="dialog" aria-label="${T('{0}\'s note: {1}', c.who.split(' ')[0], c.note)}"` }).replace('</button>', `${ui.indicator('note')}</button>`));
75
+ return items.length ? `<div class="cx-hstack" role="group" aria-label="${T('Pending')}">${items.join('')}</div>` : '';
74
76
  };
75
77
  const CONTACT = {
76
78
  whatsapp: { icon: 'b-whatsapp', color: '#25D366', name: 'WhatsApp' },
77
79
  telegram: { icon: 'b-telegram', color: '#26A5E4', name: 'Telegram' },
78
80
  instagram: { icon: 'b-instagram', color: '#E4405F', name: 'Instagram' },
79
- phone: { icon: 'call', color: 'var(--foreground)', name: 'una llamada' },
81
+ phone: { icon: 'call', color: 'var(--foreground)', name: T('a call') },
80
82
  };
81
83
  const cardInner = (id) => {
82
84
  const c = CARDS[id], via = CONTACT[c.contact];
83
- const contact = via ? ui.button({ icon: via.icon, size: 'icon-sm', variant: 'ghost', tone: 'brand', attrs: `style="--brand:${via.color}" data-bact="contact" aria-label="Contactar a ${c.who} por ${via.name}, como lo pidió" title="Prefiere ${via.name}"` }) : '';
85
+ const contact = via ? ui.button({ icon: via.icon, size: 'icon-sm', variant: 'ghost', tone: 'brand', attrs: `style="--brand:${via.color}" data-bact="contact" aria-label="${T('Contact {0} on {1}, as they asked', c.who, via.name)}" title="${T('Prefers {0}', via.name)}"` }) : '';
84
86
  const person = ui.item({
85
87
  media: ui.itemMedia(ui.avatar(c.who), 'default'),
86
88
  title: `<span class="cx-ellipsis" title="${c.who}">${c.who}</span>`,
@@ -102,13 +104,13 @@ export function orderBoard(root, i, core, on) {
102
104
  const list = $('.cx-lane-list', l), n = $$('.cx-bcard', list).length;
103
105
  $('.cx-count', l).textContent = n;
104
106
  const empty = $('.cx-lane-empty-b', list);
105
- if (!n && !$('.cx-bph', list) && !empty) list.insertAdjacentHTML('beforeend', '<div class="cx-lane-empty-b">Suelta aquí un pedido</div>');
107
+ if (!n && !$('.cx-bph', list) && !empty) list.insertAdjacentHTML('beforeend', `<div class="cx-lane-empty-b">${T('Drop an order here')}</div>`);
106
108
  if ((n || $('.cx-bph', list)) && empty) empty.remove();
107
109
  });
108
110
  const sayCard = (card) => { const lane = card.closest('.cx-blane'), list = $$('.cx-bcard', lane); brdLive.textContent = `Pedido ${card.dataset.card}: ${lane.getAttribute('aria-label')}, posición ${list.indexOf(card) + 1} de ${list.length}`; };
109
111
  const flip = (els, change) => { // animate siblings from their old place to the new one
110
112
  const before = new Map(els.map((x) => [x, x.getBoundingClientRect()])); change();
111
- before.forEach((r, x) => { const n = x.getBoundingClientRect(); const dx = r.left - n.left, dy = r.top - n.top; if (dx || dy) x.animate([{ transform: `translate(${dx}px, ${dy}px)` }, { transform: 'none' }], { duration: 200, easing: 'cubic-bezier(.22, 1, .36, 1)' }); });
113
+ before.forEach((r, x) => { const n = x.getBoundingClientRect(); const dx = r.left - n.left, dy = r.top - n.top; if (dx || dy) x.animate([{ transform: `translate(${dx}px, ${dy}px)` }, { transform: 'none' }], { duration: 200, easing: 'cubic-bezier(.23, 1, .32, 1)' }); });
112
114
  };
113
115
  let bDrag = null;
114
116
  brd.addEventListener('pointerdown', (e) => {
@@ -148,7 +150,7 @@ export function orderBoard(root, i, core, on) {
148
150
  const to = d.ph.getBoundingClientRect(), from = d.card.getBoundingClientRect();
149
151
  d.card.classList.remove('cx-dragging'); d.card.style.cssText = '';
150
152
  d.ph.replaceWith(d.card); syncLanes();
151
- d.card.animate([{ transform: `translate(${from.left - to.left}px, ${from.top - to.top}px) rotate(1.5deg)` }, { transform: 'none' }], { duration: 220, easing: 'cubic-bezier(.22, 1, .36, 1)' });
153
+ d.card.animate([{ transform: `translate(${from.left - to.left}px, ${from.top - to.top}px) rotate(1.5deg)` }, { transform: 'none' }], { duration: 220, easing: 'cubic-bezier(.23, 1, .32, 1)' });
152
154
  sayCard(d.card); emit();
153
155
  };
154
156
  addEventListener('pointerup', bEnd); addEventListener('pointercancel', bEnd);
@@ -201,10 +203,10 @@ export function orderBoard(root, i, core, on) {
201
203
  let receiptFor = null, replyFor = null, deliveryFor = null, requestFor = null, requestIdx = 0;
202
204
  KINDS.request = () => {
203
205
  const c = CARDS[requestFor], list = Array.isArray(c.request) ? c.request : [c.request], r = list[requestIdx] || list[0];
204
- $('#mdl-title').textContent = r.title; $('#mdl-desc').textContent = `${c.who} · pedido ${requestFor}${r.when ? ' · ' + r.when : ''}`;
205
- body.innerHTML = `${r.text ? `<blockquote class="cx-req-full" data-tone="${r.tone}">${r.text}</blockquote>` : '<p class="cx-note" style="text-align:left">Sin mensaje del cliente.</p>'}${r.photo ? `<span class="cx-bc-ph" style="width:96px;background:${r.photo}"></span>` : ''}${list.length > 1 ? `<div class="cx-req-next"><b>También por resolver</b>${list.filter((x) => x !== r).map((x) => `<span><svg><use href="#${x.icon}"/></svg>${x.title}</span>`).join('')}</div>` : ''}`;
206
+ $('#mdl-title').textContent = r.title; $('#mdl-desc').textContent = `${T('{0} · order {1}', c.who, requestFor)}${r.when ? ' · ' + r.when : ''}`;
207
+ body.innerHTML = `${r.text ? `<blockquote class="cx-req-full" data-tone="${r.tone}">${r.text}</blockquote>` : `<p class="cx-note" style="text-align:left">${T('No message from the client.')}</p>`}${r.photo ? `<span class="cx-bc-ph" style="width:96px;background:${r.photo}"></span>` : ''}${list.length > 1 ? `<div class="cx-req-next"><b>${T('Also to solve')}</b>${list.filter((x) => x !== r).map((x) => `<span><svg><use href="#${x.icon}"/></svg>${x.title}</span>`).join('')}</div>` : ''}`;
206
208
  const [yes, no] = r.actions;
207
- foot.innerHTML = (no ? btn('outline', no[1], `data-req-act="${no[0]}"`) : btn('outline', 'Cerrar', 'data-dismiss')) + (yes ? btn('default', yes[1], `data-req-act="${yes[0]}"`) : '');
209
+ foot.innerHTML = (no ? btn('outline', no[1], `data-req-act="${no[0]}"`) : btn('outline', T('Close'), 'data-dismiss')) + (yes ? btn('default', yes[1], `data-req-act="${yes[0]}"`) : '');
208
210
  foot.firstElementChild.classList.add('cx-exit'); foot.lastElementChild.classList.add('cx-go');
209
211
  };
210
212
  document.addEventListener('click', (e) => {
@@ -216,30 +218,30 @@ export function orderBoard(root, i, core, on) {
216
218
  let noteFor = null, stripeFor = null;
217
219
  KINDS.note = () => {
218
220
  const c = CARDS[noteFor];
219
- $('#mdl-title').textContent = `Nota de ${c.who.split(' ')[0]}`; $('#mdl-desc').textContent = `${c.who} · pedido ${noteFor}`;
221
+ $('#mdl-title').textContent = T('{0}\'s note', c.who.split(' ')[0]); $('#mdl-desc').textContent = T('{0} · order {1}', c.who, noteFor);
220
222
  body.innerHTML = `<blockquote class="cx-req-full" data-tone="note">${c.note}</blockquote>`;
221
- foot.innerHTML = btn('outline', 'Cerrar', 'data-dismiss') + btn('default', 'Copiar nota', 'data-note-copy');
223
+ foot.innerHTML = btn('outline', T('Close'), 'data-dismiss') + btn('default', T('Copy note'), 'data-note-copy');
222
224
  foot.firstElementChild.classList.add('cx-exit'); foot.lastElementChild.classList.add('cx-go');
223
225
  };
224
226
  KINDS.stripe = () => {
225
227
  const c = CARDS[stripeFor], p = c.pay;
226
- $('#mdl-title').textContent = 'Recibo de pago'; $('#mdl-desc').textContent = `${c.who} · pedido ${stripeFor}`;
227
- body.innerHTML = `<div class="cx-stripe"><div class="cx-stripe-top"><span class="cx-stripe-amt cx-num">${money0(c.total)}</span><span data-slot="badge" class="cn-badge group/badge cx-tone-ok"><span class="cx-dot"></span>Pagado</span></div><dl><dt>Medio</dt><dd>${p.detail.split(' · ')[0]}</dd><dt>Procesado por</dt><dd>Stripe</dd><dt>Fecha</dt><dd>8 oct 2026 · 09:14</dd><dt>ID del pago</dt><dd class="cx-num">pi_3Q${stripeFor.slice(1)}xK8aR2</dd><dt>Comisión</dt><dd class="cx-num">${money0(Math.round(c.total * 0.039))}</dd><dt>Recibes</dt><dd class="cx-num"><b>${money0(c.total - Math.round(c.total * 0.039))}</b></dd></dl></div>`;
228
- foot.innerHTML = btn('outline', 'Cerrar', 'data-dismiss') + `<a data-slot="button" class="cn-button cn-button-variant-default cn-button-size-default group/button cx-go" href="https://dashboard.stripe.com/test/payments" target="_blank" rel="noopener">Abrir en Stripe</a>`;
228
+ $('#mdl-title').textContent = T('Payment receipt'); $('#mdl-desc').textContent = T('{0} · order {1}', c.who, stripeFor);
229
+ body.innerHTML = `<div class="cx-stripe"><div class="cx-stripe-top"><span class="cx-stripe-amt cx-num">${money0(c.total)}</span><span data-slot="badge" class="cn-badge group/badge cx-tone-ok"><span class="cx-dot"></span>${T('Paid')}</span></div><dl><dt>${T('Method')}</dt><dd>${p.detail.split(' · ')[0]}</dd><dt>${T('Processed by')}</dt><dd>Stripe</dd><dt>${T('Date')}</dt><dd>${T('Oct 8, 2026')} · 09:14</dd><dt>${T('Payment ID')}</dt><dd class="cx-num">pi_3Q${stripeFor.slice(1)}xK8aR2</dd><dt>${T('Fee')}</dt><dd class="cx-num">${money0(Math.round(c.total * 0.039))}</dd><dt>${T('You get')}</dt><dd class="cx-num"><b>${money0(c.total - Math.round(c.total * 0.039))}</b></dd></dl></div>`;
230
+ foot.innerHTML = btn('outline', T('Close'), 'data-dismiss') + `<a data-slot="button" class="cn-button cn-button-variant-default cn-button-size-default group/button cx-go" href="https://dashboard.stripe.com/test/payments" target="_blank" rel="noopener">${T('Open in Stripe')}</a>`;
229
231
  foot.firstElementChild.classList.add('cx-exit');
230
232
  };
231
233
  document.addEventListener('click', (e) => {
232
234
  if (!e.target.closest('[data-note-copy]') || !noteFor) return;
233
235
  const t = e.target.closest('[data-note-copy]'), text = CARDS[noteFor].note;
234
- try { navigator.clipboard.writeText(text).then(() => { t.textContent = 'Copiada'; }, () => { getSelection().selectAllChildren($('.cx-req-full', body)); }); } catch { getSelection().selectAllChildren($('.cx-req-full', body)); }
236
+ try { navigator.clipboard.writeText(text).then(() => { t.innerHTML = `<span class="cx-swap-in">${T('Copied')}</span>`; }, () => { getSelection().selectAllChildren($('.cx-req-full', body)); }); } catch { getSelection().selectAllChildren($('.cx-req-full', body)); }
235
237
  });
236
238
  KINDS.delivery = () => {
237
239
  const c = CARDS[deliveryFor], d = c.delivery;
238
- $('#mdl-title').textContent = d.label; $('#mdl-desc').textContent = `${c.who} · pedido ${deliveryFor}${d.when ? ' · ' + d.when : ''}`;
240
+ $('#mdl-title').textContent = d.label; $('#mdl-desc').textContent = `${T('{0} · order {1}', c.who, deliveryFor)}${d.when ? ' · ' + d.when : ''}`;
239
241
  const steps = d.track ? `<div class="cx-dl-steps" style="--n:${d.track.steps.length}">${d.track.steps.map((st, i) => `<div ${i < d.track.at ? 'data-done' : i === d.track.at ? 'data-now aria-current="step"' : ''}><i></i><span>${st}</span></div>`).join('')}</div>` : '';
240
- const courier = d.track ? `<div class="cx-dl-courier"><span data-slot="avatar" data-size="default" class="cn-avatar cx-av" style="--h:${hue(d.track.courier)}"><span class="cn-avatar-fallback">${ini(d.track.courier)}</span></span><div><b>${d.track.courier}</b><small>Domiciliario · ${d.track.vehicle}</small></div><div class="cx-dl-eta"><small>Llega en</small><b>${d.track.eta}</b></div></div>` : '';
242
+ const courier = d.track ? `<div class="cx-dl-courier"><span data-slot="avatar" data-size="default" class="cn-avatar cx-av" style="--h:${hue(d.track.courier)}"><span class="cn-avatar-fallback">${ini(d.track.courier)}</span></span><div><b>${d.track.courier}</b><small>Domiciliario · ${d.track.vehicle}</small></div><div class="cx-dl-eta"><small>${T('Arrives in')}</small><b>${d.track.eta}</b></div></div>` : '';
241
243
  body.innerHTML = `<div class="cx-dl">${d.map ? `<div class="cx-dl-map">${miniMap(d.map)}</div><div class="cx-dl-addr"><svg><use href="#mappin"/></svg><div><b>${d.map.address}</b><small>${d.map.detail}</small></div></div>` : ''}${steps}${courier}</div>`;
242
- foot.innerHTML = btn('outline', 'Cerrar', 'data-dismiss') + (d.map ? `<a data-slot="button" class="cn-button cn-button-variant-default cn-button-size-default group/button cx-go" href="https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(d.map.address)}" target="_blank" rel="noopener"><svg><use href="#mappin"/></svg>Abrir en Maps</a>` : '');
244
+ foot.innerHTML = btn('outline', T('Close'), 'data-dismiss') + (d.map ? `<a data-slot="button" class="cn-button cn-button-variant-default cn-button-size-default group/button cx-go" href="https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(d.map.address)}" target="_blank" rel="noopener"><svg><use href="#mappin"/></svg>${T('Open in Maps')}</a>` : '');
243
245
  foot.firstElementChild.classList.add('cx-exit');
244
246
  };
245
247
  const redraw = (id) => { const card = $(`.cx-bcard[data-card="${id}"]`, root); easeHeight(card, () => { card.innerHTML = cardInner(id); }); card.animate([{ boxShadow: '0 0 0 2px var(--selected)' }, { boxShadow: '0 0 0 1px var(--border)' }], { duration: 700, easing: 'ease-out' }); };
@@ -251,7 +253,7 @@ export function orderBoard(root, i, core, on) {
251
253
  const ls = lanesEls().filter((l) => l.dataset.lane !== 'cancel'), li = ls.indexOf(card.closest('.cx-blane')); if (li < 0 || li >= ls.length - 1) return;
252
254
  const NEXT = Object.fromEntries(LANES_B.map((l) => [l.id, l.next ? [['advance', l.next, 'secondary']] : []]));
253
255
  const to = ls[li + 1]; c.actions = NEXT[to.dataset.lane] ?? c.actions;
254
- if (to.dataset.lane === 'done') { if (c.delivery) delete c.delivery.track; c.status = ['ok', 'Entregado']; }
256
+ if (to.dataset.lane === 'done') { if (c.delivery) delete c.delivery.track; c.status = ['ok', T('Delivered')]; }
255
257
  flip($$('.cx-bcard', brd), () => { $('.cx-lane-list', to).prepend(card); card.innerHTML = cardInner(id); syncLanes(); });
256
258
  card.focus({ preventScroll: true }); sayCard(card); emit();
257
259
  }
@@ -262,8 +264,8 @@ export function orderBoard(root, i, core, on) {
262
264
  if (act === 'stripe') { stripeFor = id; openModal('stripe', 'auto'); }
263
265
  if (act === 'contact') brdLive.textContent = `Abriendo ${CONTACT[c.contact].name} con ${c.who}`;
264
266
  if (act === 'reply') { replyFor = id; openModal('reply', 'auto'); }
265
- if (act === 'cancel-yes') { delete c.request; c.status = ['bad', 'Cancelado']; c.pay = { ...c.pay, tone: 'muted', label: `Devuelto ${money0(c.total)}` }; delete c.delivery; c.actions = []; if (!card.closest('.cx-board')) { redraw(id); return; } const to = $('[data-lane=cancel] .cx-lane-list', brd); flip($$('.cx-bcard', brd), () => { to.prepend(card); card.innerHTML = cardInner(id); syncLanes(); }); to.closest('.cx-blane').scrollIntoView({ block: 'nearest', inline: 'nearest', behavior: 'smooth' }); brdLive.textContent = `Pedido ${id} cancelado y devuelto`; emit(); }
266
- if (act === 'cancel-no') { const rest = Array.isArray(c.request) ? c.request.filter((_, i) => i !== +(b.dataset.i || 0)) : []; if (rest.length) c.request = rest.length === 1 ? rest[0] : rest; else delete c.request; c.actions = [['advance', 'Preparar', 'secondary']]; redraw(id); }
267
+ if (act === 'cancel-yes') { delete c.request; c.status = ['bad', T('Canceled')]; c.pay = { ...c.pay, tone: 'muted', label: T('Refunded {0}', money0(c.total)) }; delete c.delivery; c.actions = []; if (!card.closest('.cx-board')) { redraw(id); return; } const to = $('[data-lane=cancel] .cx-lane-list', brd); flip($$('.cx-bcard', brd), () => { to.prepend(card); card.innerHTML = cardInner(id); syncLanes(); }); to.closest('.cx-blane').scrollIntoView({ block: 'nearest', inline: 'nearest', behavior: 'smooth' }); brdLive.textContent = `Pedido ${id} cancelado y devuelto`; emit(); }
268
+ if (act === 'cancel-no') { const rest = Array.isArray(c.request) ? c.request.filter((_, i) => i !== +(b.dataset.i || 0)) : []; if (rest.length) c.request = rest.length === 1 ? rest[0] : rest; else delete c.request; c.actions = [['advance', T('Prepare'), 'secondary']]; redraw(id); }
267
269
  if (act === 'refund') { if (Array.isArray(c.request)) { c.request = c.request.filter((_, i) => i !== +(b.dataset.i || 0)); if (c.request.length === 1) c.request = c.request[0]; } else delete c.request; c.pay = { ...c.pay, tone: 'info', label: `Reembolsado ${money0(c.total)}` }; redraw(id); }
268
270
  };
269
271
  root.addEventListener('click', onCardButton);
@@ -271,29 +273,29 @@ export function orderBoard(root, i, core, on) {
271
273
  const sizes = $('.cx-card-sizes', root); if (sizes) sizes.innerHTML = SIZES.map(([t, d, id]) => `<div class="cx-cs-col"><div class="cx-cs-h"><b>${t}</b><span>${d}</span></div><div data-slot="card" data-size="sm" class="cn-card group/card cx-bcard cx-bcard-static" data-card="${id}" aria-label="${t}">${cardInner(id)}</div></div>`).join('');
272
274
  KINDS.receipt = () => {
273
275
  const c = CARDS[receiptFor];
274
- $('#mdl-title').textContent = 'Comprobante de pago'; $('#mdl-desc').textContent = `${c.who} · pedido ${receiptFor}`;
275
- body.innerHTML = `<div class="cx-rcpt"><div class="cx-rcpt-shot" role="img" aria-label="Captura de la transferencia"><span class="cx-rcpt-ok"><svg><use href="#check"/></svg></span><b>Transferencia exitosa</b><span class="cx-rcpt-amt">${money0(c.total)}</span><dl><dt>Para</dt><dd>Barbería El Roble</dd><dt>Desde</dt><dd>${c.who}</dd><dt>Referencia</dt><dd class="cx-num">M${receiptFor.slice(1)}7731</dd><dt>Fecha</dt><dd>8 oct 2026 · 10:42</dd></dl></div><div class="cx-rcpt-check"><span>Monto del pedido</span><b class="cx-num">${money0(c.total)}</b><span data-slot="badge" class="cn-badge group/badge cx-tone-ok"><span class="cx-dot"></span>Coincide</span></div></div>`;
276
- foot.innerHTML = btn('outline', 'Rechazar', 'data-rcpt="no"') + btn('default', 'Aprobar pago', 'data-rcpt="yes"');
276
+ $('#mdl-title').textContent = T('Payment proof'); $('#mdl-desc').textContent = T('{0} · order {1}', c.who, receiptFor);
277
+ body.innerHTML = `<div class="cx-rcpt"><div class="cx-rcpt-shot" role="img" aria-label="${T('Screenshot of the transfer')}"><span class="cx-rcpt-ok"><svg><use href="#check"/></svg></span><b>${T('Transfer successful')}</b><span class="cx-rcpt-amt">${money0(c.total)}</span><dl><dt>${T('To')}</dt><dd>Barbería El Roble</dd><dt>${T('From')}</dt><dd>${c.who}</dd><dt>${T('Reference')}</dt><dd class="cx-num">M${receiptFor.slice(1)}7731</dd><dt>${T('Date')}</dt><dd>${T('Oct 8, 2026')} · 10:42</dd></dl></div><div class="cx-rcpt-check"><span>${T('Order amount')}</span><b class="cx-num">${money0(c.total)}</b><span data-slot="badge" class="cn-badge group/badge cx-tone-ok"><span class="cx-dot"></span>${T('Matches')}</span></div></div>`;
278
+ foot.innerHTML = btn('outline', T('Reject'), 'data-rcpt="no"') + btn('default', T('Approve payment'), 'data-rcpt="yes"');
277
279
  foot.firstElementChild.classList.add('cx-exit'); foot.lastElementChild.classList.add('cx-go');
278
280
  };
279
281
  KINDS.reply = () => {
280
282
  const c = CARDS[replyFor];
281
- $('#mdl-title').textContent = `Responder a ${c.who}`; $('#mdl-desc').textContent = `Queja del pedido ${replyFor}`;
282
- body.innerHTML = `<div class="cx-bc-req" data-tone="bad" style="margin:0"><div class="cx-bc-req-h"><svg><use href="#info"/></svg><b>Queja</b><small>${(c.request.when ?? c.request[0]?.when)}</small></div><p>«${(c.request.text ?? c.request[0]?.text)}»</p></div><div data-slot="field" class="cn-field cn-field-orientation-vertical group/field"><label data-slot="label" class="cn-label" for="rp-t">Tu respuesta</label><textarea data-slot="textarea" class="cn-textarea" id="rp-t" rows="4">Hola ${c.who.split(' ')[0]}, lo siento mucho. Te enviamos una nueva hoy sin costo.</textarea><p data-slot="field-description" class="cn-field-description">Le llega por WhatsApp.</p></div>`;
283
- foot.innerHTML = btn('outline', 'Cancelar', 'data-dismiss') + btn('default', 'Enviar respuesta', 'data-reply-send');
283
+ $('#mdl-title').textContent = T('Answer {0}', c.who); $('#mdl-desc').textContent = T('Complaint about order {0}', replyFor);
284
+ body.innerHTML = `<div class="cx-bc-req" data-tone="bad" style="margin:0"><div class="cx-bc-req-h"><svg><use href="#info"/></svg><b>${T('Complaint')}</b><small>${(c.request.when ?? c.request[0]?.when)}</small></div><p>«${(c.request.text ?? c.request[0]?.text)}»</p></div><div data-slot="field" class="cn-field cn-field-orientation-vertical group/field"><label data-slot="label" class="cn-label" for="rp-t">${T('Your answer')}</label><textarea data-slot="textarea" class="cn-textarea" id="rp-t" rows="4">${T('Hi {0}, I am so sorry. We are sending you a new one today at no cost.', c.who.split(' ')[0])}</textarea><p data-slot="field-description" class="cn-field-description">${T('It reaches them on WhatsApp.')}</p></div>`;
285
+ foot.innerHTML = btn('outline', T('Cancel'), 'data-dismiss') + btn('default', T('Send answer'), 'data-reply-send');
284
286
  foot.firstElementChild.classList.add('cx-exit'); foot.lastElementChild.classList.add('cx-go');
285
287
  };
286
288
  document.addEventListener('click', (e) => {
287
289
  const r = e.target.closest('[data-rcpt]');
288
290
  if (r && receiptFor) {
289
291
  const c = CARDS[receiptFor], ok = r.dataset.rcpt === 'yes';
290
- c.pay = ok ? { method: 'transfer', tone: 'ok', label: 'Pagado', detail: c.pay?.detail } : { method: 'transfer', tone: 'bad', label: 'Rechazado', detail: c.pay?.detail };
291
- c.actions = ok ? [['advance', 'Preparar', 'secondary']] : [];
292
+ c.pay = ok ? { method: 'transfer', tone: 'ok', label: T('Paid'), detail: c.pay?.detail } : { method: 'transfer', tone: 'bad', label: T('Rejected'), detail: c.pay?.detail };
293
+ c.actions = ok ? [['advance', T('Prepare'), 'secondary']] : [];
292
294
  redraw(receiptFor); brdLive.textContent = ok ? `Pago de ${c.who} aprobado` : `Comprobante de ${c.who} rechazado`;
293
295
  receiptFor = null; closeModal();
294
296
  }
295
297
  if (e.target.closest('[data-reply-send]') && replyFor) {
296
- const c = CARDS[replyFor]; c.request = { ...(Array.isArray(c.request) ? c.request[0] : c.request), tone: 'info', icon: 'check', title: 'Queja respondida', when: 'ahora', text: c.request.text, actions: [['refund', 'Reembolsar', 'ghost']] };
298
+ const c = CARDS[replyFor]; c.request = { ...(Array.isArray(c.request) ? c.request[0] : c.request), tone: 'info', icon: 'check', title: T('Complaint answered'), when: T('now'), text: c.request.text, actions: [['refund', T('Refund'), 'ghost']] };
297
299
  redraw(replyFor); replyFor = null; closeModal();
298
300
  }
299
301
  });
@@ -2,19 +2,19 @@
2
2
  // Carousel's behaviour, generic. Data and labels come in; nothing about a product is inside.
3
3
 
4
4
  // Tags: chips animate in and out, the field's height eases; suggestions; a cap with its count. i {label, tags
5
- // "Barba,Corte clásico", suggestions "Fade,Navaja", max, hint} → on.change("Barba,Corte clásico,Fade")
5
+ // "Beard,Classic cut", suggestions "Fade,Razor", max, hint} → on.change("Beard,Classic cut,Fade")
6
6
  export function tagInput(root, i, core, on) {
7
- const { $, $$, fold, easeHeight } = core;
7
+ const { $, $$, fold, easeHeight } = core, T = (k, ...a) => core.t(core.langOf(root), k, ...a);
8
8
  const split = (s) => String(s || '').split(',').map((x) => x.trim()).filter(Boolean);
9
9
  const MAXT = Number(i.max) || 8, SUG = split(i.suggestions), id = 'tg' + Math.random().toString(36).slice(2, 7);
10
10
  let tags = split(i.tags);
11
- root.innerHTML = `<div data-slot="field" class="cn-field cn-field-orientation-vertical group/field"><label data-slot="label" class="cn-label" for="${id}">${i.label || 'Etiquetas'}</label><div class="cx-taginput"><input id="${id}" class="cx-tag-field" placeholder="${i.placeholder || 'Agrega y presiona Enter'}" autocomplete="off"></div><p data-slot="field-description" class="cn-field-description cx-count-line"><span>${i.hint || ''}</span><span data-tg-n></span></p></div>${SUG.length ? '<div class="cx-suggest"><span>Sugerencias</span><div class="cx-line" style="justify-content:flex-start" data-tg-sug></div></div>' : ''}`;
11
+ root.innerHTML = `<div data-slot="field" class="cn-field cn-field-orientation-vertical group/field"><label data-slot="label" class="cn-label" for="${id}">${T(i.label || 'Tags')}</label><div class="cx-taginput"><input id="${id}" class="cx-tag-field" placeholder="${i.placeholder || 'Agrega y presiona Enter'}" autocomplete="off"></div><p data-slot="field-description" class="cn-field-description cx-count-line"><span>${i.hint || ''}</span><span data-tg-n></span></p></div>${SUG.length ? `<div class="cx-suggest"><span>${T('Suggestions')}</span><div class="cx-line" style="justify-content:flex-start" data-tg-sug></div></div>` : ''}`;
12
12
  const tgBox = $('.cx-taginput', root), tgIn = $('input', root), tgN = $('[data-tg-n]', root), tgSug = $('[data-tg-sug]', root);
13
- const chipHtml = (t) => `<span data-slot="badge" class="cn-badge cn-badge-variant-secondary cx-chip" data-tag="${t}">${t}<button class="cn-button cn-button-variant-ghost cn-button-size-icon-xs cx-chip-x" aria-label="Quitar ${t}" title="Quitar" tabindex="-1"><svg style="width:12px;height:12px"><use href="#x"/></svg></button></span>`;
13
+ const chipHtml = (t) => `<span data-slot="badge" class="cn-badge cn-badge-variant-secondary cx-chip" data-tag="${t}">${t}<button class="cn-button cn-button-variant-ghost cn-button-size-icon-xs cx-chip-x" aria-label="${T('Remove {0}', t)}" title="Quitar" tabindex="-1"><svg style="width:12px;height:12px"><use href="#x"/></svg></button></span>`;
14
14
  const emit = () => on?.change?.(tags.join(','));
15
15
  const tgMeta = () => {
16
16
  tgN.textContent = `${tags.length}/${MAXT}`; tgN.parentElement.toggleAttribute('data-near', tags.length >= MAXT);
17
- tgIn.placeholder = tags.length >= MAXT ? `Llegaste al tope de ${MAXT}` : tags.length ? '' : (i.placeholder || 'Agrega y presiona Enter');
17
+ tgIn.placeholder = tags.length >= MAXT ? T('You reached the limit of {0}', MAXT) : tags.length ? '' : (i.placeholder || T('Add and press Enter'));
18
18
  if (tgSug) $$('.cx-sug', tgSug).forEach((b) => { b.disabled = tags.some((t) => fold(t) === fold(b.textContent)) || tags.length >= MAXT; });
19
19
  };
20
20
  const tgAdd = (raw) => {
@@ -40,19 +40,19 @@ export function tagInput(root, i, core, on) {
40
40
  }
41
41
 
42
42
  // Pagination nav for server pages: always 7 slots once there are more than 7 pages (the buttons never slide),
43
- // «Página n de N» on a phone, and the range line. i {total, per, page, noun} → on.page(n)
43
+ // “Page n of N” on a phone, and the range line. i {total, per, page, noun} → on.page(n)
44
44
  export function paginationNav(root, i, core, on) {
45
- const { $ } = core;
46
- root.innerHTML = '<div class="cx-pg-foot"><nav class="cn-pagination cx-pagination" aria-label="Páginas"><ul class="cn-pagination-content" style="list-style:none;margin:0;padding:0"></ul></nav><span class="cx-num" aria-live="polite" data-pg-range></span></div>';
45
+ const { $ } = core, L = core.langOf(root), T = (k, ...a) => core.t(L, k, ...a);
46
+ root.innerHTML = `<div class="cx-pg-foot"><nav class="cn-pagination cx-pagination" aria-label="${T('Pages')}"><ul class="cn-pagination-content" style="list-style:none;margin:0;padding:0"></ul></nav><span class="cx-num" aria-live="polite" data-pg-range></span></div>`;
47
47
  const pgNav = $('ul', root), pgRange = $('[data-pg-range]', root);
48
- let total = Number(i.total) || 0, per = Number(i.per) || 10, page = Number(i.page) || 1, noun = i.noun || 'resultados';
48
+ let total = Number(i.total) || 0, per = Number(i.per) || 10, page = Number(i.page) || 1, noun = T(i.noun || 'results');
49
49
  const pages = () => Math.max(1, Math.ceil(total / per));
50
50
  const nums = () => { const P = pages(); if (P <= 7) return Array.from({ length: P }, (_, k) => k + 1); if (page <= 4) return [1, 2, 3, 4, 5, '…', P]; if (page >= P - 3) return [1, '…', P - 4, P - 3, P - 2, P - 1, P]; return [1, '…', page - 1, page, page + 1, '…', P]; };
51
51
  const paint = (focus) => {
52
52
  const P = pages();
53
- const nav = (dir, p, off) => `<li><button data-slot="button" class="cn-button cn-button-variant-ghost cn-button-size-default cn-pagination-${dir === -1 ? 'previous' : 'next'} group/button" data-page="${p}" ${off ? 'disabled' : ''} aria-label="Página ${dir === -1 ? 'anterior' : 'siguiente'}">${dir === -1 ? '<svg><use href="#left"/></svg><span>Anterior</span>' : '<span>Siguiente</span><svg><use href="#right"/></svg>'}</button></li>`;
54
- pgNav.innerHTML = nav(-1, page - 1, page === 1) + nums().map((p) => (p === '…' ? '<li class="cx-pg-num"><span class="cn-pagination-ellipsis cx-pg-gap" aria-hidden="true"><svg><use href="#more"/></svg></span></li>' : `<li class="cx-pg-num"><button data-slot="button" class="cn-button ${p === page ? 'cn-button-variant-outline' : 'cn-button-variant-ghost'} cn-button-size-icon group/button cx-num" data-page="${p}" ${p === page ? 'aria-current="page"' : ''} aria-label="Página ${p}">${p}</button></li>`)).join('') + `<li class="cx-pg-compact">Página ${page} de ${P}</li>` + nav(1, page + 1, page === P);
55
- pgRange.textContent = `${(page - 1) * per + 1}–${Math.min(total, page * per)} de ${total} ${noun}`;
53
+ const nav = (dir, p, off) => `<li><button data-slot="button" class="cn-button cn-button-variant-ghost cn-button-size-default cn-pagination-${dir === -1 ? 'previous' : 'next'} group/button" data-page="${p}" ${off ? 'disabled' : ''} aria-label="${dir === -1 ? T('Previous page') : T('Next page')}">${dir === -1 ? `<svg><use href="#left"/></svg><span>${T('Previous')}</span>` : `<span>${T('Next')}</span><svg><use href="#right"/></svg>`}</button></li>`;
54
+ pgNav.innerHTML = nav(-1, page - 1, page === 1) + nums().map((p) => (p === '…' ? '<li class="cx-pg-num"><span class="cn-pagination-ellipsis cx-pg-gap" aria-hidden="true"><svg><use href="#more"/></svg></span></li>' : `<li class="cx-pg-num"><button data-slot="button" class="cn-button ${p === page ? 'cn-button-variant-outline' : 'cn-button-variant-ghost'} cn-button-size-icon group/button cx-num" data-page="${p}" ${p === page ? 'aria-current="page"' : ''} aria-label="${T('Page {0}', p)}">${p}</button></li>`)).join('') + `<li class="cx-pg-compact">${T('Page {0} of {1}', page, P)}</li>` + nav(1, page + 1, page === P);
55
+ pgRange.textContent = T('{0}–{1} of {2} {3}', (page - 1) * per + 1, Math.min(total, page * per), total, noun);
56
56
  if (focus) pgNav.querySelector('[aria-current]')?.focus({ preventScroll: true });
57
57
  };
58
58
  pgNav.addEventListener('click', (e) => { const b = e.target.closest('[data-page]'); if (!b || b.disabled) return; page = +b.dataset.page; paint(true); on?.page?.(page); });
@@ -61,12 +61,12 @@ export function paginationNav(root, i, core, on) {
61
61
  }
62
62
 
63
63
  // Virtual list: only the visible rows (+ a margin) exist in the DOM. i {rows [{title, subtitle, value}], total
64
- // (rows repeat up to it, for a long demo), rowHeight, label} → on.jump not needed: «Ir al n» via inputs.jump.
64
+ // (rows repeat up to it, for a long demo), rowHeight, label} → on.jump not needed: “Go to n” via inputs.jump.
65
65
  export function virtualList(root, i, core) {
66
- const { $ } = core;
66
+ const { $ } = core, L = core.langOf(root), T = (k, ...a) => core.t(L, k, ...a), loc = L === 'es' ? 'es-CO' : 'en-US';
67
67
  const rows = Array.isArray(i.rows) ? i.rows : [];
68
68
  const VTOTAL = Math.max(Number(i.total) || 0, rows.length), VROW = Number(i.rowHeight) || 52, VOVER = 6;
69
- root.innerHTML = `<div class="cx-pages-wrap"><div class="cx-vlist cx-scroll" tabindex="0" role="list" aria-label="${i.label || 'Lista'}"><div class="cx-vspace"><div class="cx-vwin"></div></div></div><div class="cx-pg-foot"><span class="cx-num" data-vl-info></span>${i.jump ? `<button data-slot="button" class="cn-button cn-button-variant-outline cn-button-size-sm group/button" data-vl-jump>Ir al ${Number(i.jump).toLocaleString('es-CO')}</button>` : ''}</div></div>`;
69
+ root.innerHTML = `<div class="cx-pages-wrap"><div class="cx-vlist cx-scroll" tabindex="0" role="list" aria-label="${T(i.label || 'List')}"><div class="cx-vspace"><div class="cx-vwin"></div></div></div><div class="cx-pg-foot"><span class="cx-num" data-vl-info></span>${i.jump ? `<button data-slot="button" class="cn-button cn-button-variant-outline cn-button-size-sm group/button" data-vl-jump>${T('Go to {0}', Number(i.jump).toLocaleString(loc))}</button>` : ''}</div></div>`;
70
70
  const vBox = $('.cx-vlist', root), vSpace = $('.cx-vspace', root), vWin = $('.cx-vwin', root), vInfo = $('[data-vl-info]', root);
71
71
  vSpace.style.height = VTOTAL * VROW + 'px';
72
72
  let vFirst = -1, vRaf = 0;
@@ -80,7 +80,7 @@ export function virtualList(root, i, core) {
80
80
  vWin.innerHTML = html;
81
81
  }
82
82
  const a = Math.floor(top / VROW) + 1, b = Math.min(VTOTAL, Math.ceil((top + h) / VROW));
83
- vInfo.textContent = `${a.toLocaleString('es-CO')}–${b.toLocaleString('es-CO')} de ${VTOTAL.toLocaleString('es-CO')} · ${vWin.childElementCount} filas en el DOM`;
83
+ vInfo.textContent = T('{0}–{1} of {2} · {3} rows in the DOM', a.toLocaleString(loc), b.toLocaleString(loc), VTOTAL.toLocaleString(loc), vWin.childElementCount);
84
84
  };
85
85
  vBox.addEventListener('scroll', () => { cancelAnimationFrame(vRaf); vRaf = requestAnimationFrame(vPaint); }, { passive: true });
86
86
  vBox.addEventListener('keydown', (e) => { const d = { ArrowDown: VROW, ArrowUp: -VROW, PageDown: vBox.clientHeight, PageUp: -vBox.clientHeight }[e.key]; if (d) { e.preventDefault(); vBox.scrollTop += d; } if (e.key === 'Home') { e.preventDefault(); vBox.scrollTop = 0; } if (e.key === 'End') { e.preventDefault(); vBox.scrollTop = vBox.scrollHeight; } });
@@ -91,16 +91,16 @@ export function virtualList(root, i, core) {
91
91
  // Carousel behaviour over the slides muten rendered inside a `.cx-car` (the Carousel part): pages, indicator
92
92
  // (dots · progress · count · thumbs), loop, autoplay with its own pause, arrows (bar | overlay), keys and mouse drag.
93
93
  export function carouselBehavior(root, i, core) {
94
- const { $, $$, ui } = core;
94
+ const { $, $$, ui } = core, T = (k, ...a) => core.t(core.langOf(root), k, ...a);
95
95
  const perView = String(i.perView || '1').split(',').map(Number), [perSm, perLg] = perView.length > 1 ? perView : [Math.min(perView[0], 2), perView[0]];
96
96
  const center = i.center === 'on', loop = i.loop === 'on', autoplay = Number(i.autoplay) || 0, indicator = i.indicator || 'dots', arrows = i.arrows || 'bar';
97
97
  root.style.setProperty('--per', perLg); root.style.setProperty('--per-sm', perSm);
98
98
  if (center) root.dataset.center = '';
99
99
  const view = $('.cx-car-view', root); view.dataset.arrows = arrows;
100
- const prevB = ui.button({ icon: 'left', size: 'icon-sm', attrs: 'data-car="prev" aria-label="Anterior" title="Anterior"', cls: 'cx-car-arrow' });
101
- const nextB = ui.button({ icon: 'right', size: 'icon-sm', attrs: 'data-car="next" aria-label="Siguiente" title="Siguiente"', cls: 'cx-car-arrow' });
100
+ const prevB = ui.button({ icon: 'left', size: 'icon-sm', attrs: `data-car="prev" aria-label="${T('Previous')}" title="${T('Previous')}"`, cls: 'cx-car-arrow' });
101
+ const nextB = ui.button({ icon: 'right', size: 'icon-sm', attrs: `data-car="next" aria-label="${T('Next')}" title="${T('Next')}"`, cls: 'cx-car-arrow' });
102
102
  if (arrows === 'overlay') view.insertAdjacentHTML('beforeend', prevB + nextB);
103
- root.insertAdjacentHTML('beforeend', `<div class="cx-car-bar"><div class="cx-car-ind" data-ind="${indicator}"></div><div class="cx-hstack" style="gap:6px">${autoplay ? ui.button({ icon: 'pause', size: 'icon-sm', variant: 'ghost', attrs: 'data-car="play" aria-label="Pausar" title="Pausar"' }) : ''}${arrows === 'bar' ? prevB + nextB : ''}</div></div>`);
103
+ root.insertAdjacentHTML('beforeend', `<div class="cx-car-bar"><div class="cx-car-ind" data-ind="${indicator}"></div><div class="cx-hstack" style="gap:6px">${autoplay ? ui.button({ icon: 'pause', size: 'icon-sm', variant: 'ghost', attrs: `data-car="play" aria-label="${T('Pause')}" title="${T('Pause')}"` }) : ''}${arrows === 'bar' ? prevB + nextB : ''}</div></div>`);
104
104
  const track = $('.cx-car-track', root), ind = $('.cx-car-ind', root), cards = $$('.cx-car-slide', track);
105
105
  cards.forEach((c, k) => { c.setAttribute('role', 'group'); c.setAttribute('aria-roledescription', 'diapositiva'); c.setAttribute('aria-label', `${k + 1} de ${cards.length}`); });
106
106
  const step = () => (cards[1] ? cards[1].offsetLeft - cards[0].offsetLeft : track.clientWidth);
@@ -116,10 +116,10 @@ export function carouselBehavior(root, i, core) {
116
116
  const paint = () => {
117
117
  const n = pages(), cur = current();
118
118
  // the dots are tabs, so their holder is the tablist (a tab without one fails the a11y check)
119
- if (indicator === 'dots') { ind.setAttribute('role', 'tablist'); ind.setAttribute('aria-label', 'Diapositivas'); if (ind.childElementCount !== n) ind.innerHTML = Array.from({ length: n }, (_, k) => ui.button({ variant: 'ghost', size: 'icon-xs', cls: 'cx-dotb', attrs: `role="tab" data-go="${k}" aria-label="Ir a ${k + 1} de ${n}"`, label: '<span class="cx-dot-mark"></span>' })).join(''); $$('[data-go]', ind).forEach((d, k) => d.setAttribute('aria-selected', k === cur)); }
119
+ if (indicator === 'dots') { ind.setAttribute('role', 'tablist'); ind.setAttribute('aria-label', T('Slides')); if (ind.childElementCount !== n) ind.innerHTML = Array.from({ length: n }, (_, k) => ui.button({ variant: 'ghost', size: 'icon-xs', cls: 'cx-dotb', attrs: `role="tab" data-go="${k}" aria-label="${T('Go to {0} of {1}', k + 1, n)}"`, label: '<span class="cx-dot-mark"></span>' })).join(''); $$('[data-go]', ind).forEach((d, k) => d.setAttribute('aria-selected', k === cur)); }
120
120
  if (indicator === 'progress') ind.innerHTML = `<div class="cx-hstack" style="gap:10px;flex-wrap:nowrap">${ui.progress(((cur + 1) / n) * 100, `Diapositiva ${cur + 1} de ${n}`)}${ui.text(`${cur + 1} / ${n}`, 'caption')}</div>`;
121
121
  if (indicator === 'count') ind.innerHTML = ui.text(`${cur + 1} de ${n}`, 'caption');
122
- if (indicator === 'thumbs') { if (!ind.childElementCount) ind.innerHTML = `<div class="cx-hstack" role="tablist" aria-label="Miniaturas">${cards.map((_, k) => ui.button({ variant: 'ghost', size: 'icon-lg', cls: 'cx-thumb', attrs: `role="tab" data-go="${k}" aria-label="Foto ${k + 1}"`, label: thumbOf(k) })).join('')}</div>`; $$('[data-go]', ind).forEach((d, k) => { d.setAttribute('aria-selected', k === cur); d.setAttribute('aria-pressed', k === cur); }); }
122
+ if (indicator === 'thumbs') { if (!ind.childElementCount) ind.innerHTML = `<div class="cx-hstack" role="tablist" aria-label="${T('Thumbnails')}">${cards.map((_, k) => ui.button({ variant: 'ghost', size: 'icon-lg', cls: 'cx-thumb', attrs: `role="tab" data-go="${k}" aria-label="${T('Photo {0}', k + 1)}"`, label: thumbOf(k) })).join('')}</div>`; $$('[data-go]', ind).forEach((d, k) => { d.setAttribute('aria-selected', k === cur); d.setAttribute('aria-pressed', k === cur); }); }
123
123
  if (center) cards.forEach((c, k) => c.toggleAttribute('data-active', k === cur));
124
124
  if (!loop) { $$('[data-car="prev"]', root).forEach((b) => { b.disabled = cur === 0; }); $$('[data-car="next"]', root).forEach((b) => { b.disabled = cur >= n - 1; }); }
125
125
  };
@@ -129,7 +129,7 @@ export function carouselBehavior(root, i, core) {
129
129
  const a = e.target.closest('[data-car]'); if (!a) return;
130
130
  if (a.dataset.car === 'prev') go(current() - 1);
131
131
  if (a.dataset.car === 'next') go(current() + 1);
132
- if (a.dataset.car === 'play') { playing = !playing; a.innerHTML = ui.icon(playing ? 'pause' : 'play'); a.setAttribute('aria-label', playing ? 'Pausar' : 'Reproducir'); a.title = a.getAttribute('aria-label'); }
132
+ if (a.dataset.car === 'play') { playing = !playing; a.innerHTML = ui.icon(playing ? 'pause' : 'play'); a.setAttribute('aria-label', playing ? T('Pause') : T('Play')); a.title = a.getAttribute('aria-label'); }
133
133
  });
134
134
  track.tabIndex = 0;
135
135
  track.addEventListener('keydown', (e) => { if (e.key === 'ArrowRight') { e.preventDefault(); go(current() + 1); } if (e.key === 'ArrowLeft') { e.preventDefault(); go(current() - 1); } });
@@ -146,7 +146,7 @@ export function carouselBehavior(root, i, core) {
146
146
  addEventListener('pointerup', () => { if (!drag) return; const moved = drag.moved; drag = null; track.classList.remove('dragging'); if (moved) { go(nearest()); track.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); }, { capture: true, once: true }); } });
147
147
  let playing = !!autoplay && !matchMedia('(prefers-reduced-motion: reduce)').matches, hold = false;
148
148
  if (autoplay) {
149
- if (!playing) { const b = $('[data-car="play"]', root); b.innerHTML = ui.icon('play'); b.setAttribute('aria-label', 'Reproducir'); }
149
+ if (!playing) { const b = $('[data-car="play"]', root); b.innerHTML = ui.icon('play'); b.setAttribute('aria-label', T('Play')); }
150
150
  ['pointerenter', 'focusin', 'touchstart'].forEach((t) => root.addEventListener(t, () => { hold = true; }, { passive: true }));
151
151
  ['pointerleave', 'focusout'].forEach((t) => root.addEventListener(t, () => { hold = false; }));
152
152
  setInterval(() => { if (playing && !hold && !document.hidden) go(loop || current() < pages() - 1 ? current() + 1 : 0); }, autoplay);