@muten/shadcn 0.2.8 → 0.2.10

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.
package/AGENTS.md CHANGED
@@ -31,6 +31,7 @@ its own bugs, and is one more thing to fix. So before you write any UI piece, wa
31
31
  | a button / icon button / link that looks like one | `Btn(variant, size, label, onClick, disabled)`, `LinkBtn(to, variant)` | `Button … class("cn-button …")` |
32
32
  | buttons joined in a row, an action with a menu | `ButtonGroup`, `SplitButton` | two buttons with their own borders |
33
33
  | a text field, with an icon, a currency or a country | `Input`, `InputGroup` + `GroupInput`, `MoneyField`, `PhoneField` | a select drawn inside an input |
34
+ | a password to type in (sign-in), one to choose (sign-up) | `SecretInput(value)`, `PasswordField` (show/hide + strength) | a SearchField showing the password |
34
35
  | a label, help text and error around a control | `Field(label, description, error)` | spans with margins |
35
36
  | a person's face or initials, a name with a subtitle | `Avatar`, `AvatarImage`, `Who` | a circle div with a letter |
36
37
  | **a phone or browser mockup** (screenshot, live screen, store preview) | **`DeviceFrame`** (`island`, `width`, `href`, `label`, `shot`, `kind: "browser"`, `url`) | any frame of your own: it is the one phone, the iPhone 18 Pro to scale |
@@ -38,6 +39,7 @@ its own bugs, and is one more thing to fix. So before you write any UI piece, wa
38
39
  | a conversation column | `MessageScroller` + `Message` + `Marker` | a scrolling div of bubbles |
39
40
  | a dialog, a drawer, a side sheet | `Modal(mode: "auto" \| "drawer" \| …)` | a fixed div with a backdrop |
40
41
  | a short notice, an alert, a band | `Island`, `Alert`, `Band` | a toast of your own |
42
+ | the notifications bell in a top bar | `Bell(items, bare: "on")` | a bell icon with its own popover |
41
43
  | a loading state | `Loading(busy, kind) { … }`, `Skeletons(kind)`, `Skeleton` | grey boxes by hand |
42
44
  | an empty state | `Empty(title, description)` | a centred paragraph |
43
45
  | tabs, a segmented switch | `Tabs`, `SegmentedControl` | buttons with an active class |
package/kit.css CHANGED
@@ -743,7 +743,7 @@ input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webki
743
743
  .cx-dt-bulk[data-open] { grid-template-rows: 1fr; }
744
744
  .cx-dt-bulk > div { overflow: hidden; min-height: 0; display: flex; align-items: center; justify-content: space-between; gap: 12px; font-size: 13px; font-weight: 500; }
745
745
  .cx-dt-bulk[data-open] > div { padding: 0 4px 10px; }
746
- .cx-dt-scroll { max-height: 380px; overflow: auto; border-radius: calc(var(--radius) * 1.2); box-shadow: 0 0 0 1px var(--border); }
746
+ .cx-dt-scroll { max-height: var(--dt-h, 380px); overflow: auto; border-radius: calc(var(--radius) * 1.2); box-shadow: 0 0 0 1px var(--border); }
747
747
  .cx-dt { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 14px; }
748
748
  .cx-dt th { position: sticky; top: 0; z-index: 1; background: var(--muted); text-align: left; font-weight: 500; color: var(--muted-foreground); height: 40px; padding: 0 12px; border-bottom: 1px solid var(--border); white-space: nowrap; }
749
749
  .cx-dt td { height: 52px; padding: 0 12px; border-bottom: 1px solid var(--border); vertical-align: middle; }
@@ -771,12 +771,16 @@ input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webki
771
771
  .cx-dt td.cx-dt-check { grid-area: c; align-self: center; width: auto; }
772
772
  .cx-dt td.cx-dt-who { grid-area: who; }
773
773
  .cx-dt td.cx-dt-svc { grid-area: meta; color: var(--muted-foreground); font-size: 13px; }
774
+ /* the narrow row holds one text column in its place; any other text column gets a line of its own under it */
775
+ .cx-dt td.cx-dt-svc ~ td.cx-dt-svc { grid-area: auto; grid-column: 2 / -1; }
776
+ .cx-dt tbody tr { grid-auto-rows: min-content; }
774
777
  .cx-dt td.cx-dt-when { display: none; }
775
778
  .cx-dt td.cx-dt-st { grid-area: st; justify-self: end; }
776
779
  .cx-dt td.cx-dt-num { grid-area: amt; }
777
780
  .cx-dt td.cx-dt-act { display: none; }
778
781
  .cx-dt-who small { display: none; }
779
- .cx-dt td.cx-dt-svc::after { content: " · " attr(data-when); }
782
+ .cx-dt td.cx-dt-svc[data-when]:not([data-when=""])::after { content: " · " attr(data-when); }
783
+ .cx-dt td.cx-dt-svc ~ td.cx-dt-svc::after { content: none; }
780
784
  }
781
785
 
782
786
  /* ── Board ── */
@@ -1722,7 +1726,8 @@ p.cx-text { margin: 0; }
1722
1726
 
1723
1727
  /* ═════ Settings ═════ */
1724
1728
  .cx-settings { width: 100%; max-width: 760px; }
1725
- .cx-set-form { position: relative; display: flex; flex-direction: column; gap: 0; padding-bottom: 72px; }
1729
+ /* the sections fold to one column under 600px of the FORM (not the window): it is their container, so it needs its width */
1730
+ .cx-set-form { position: relative; width: 100%; container-type: inline-size; display: flex; flex-direction: column; gap: 0; padding-bottom: 72px; }
1726
1731
  .cx-set-sec { display: grid; grid-template-columns: 220px 1fr; gap: 16px 32px; padding: 20px 0; border-top: 1px solid var(--border); }
1727
1732
  .cx-set-sec:first-child { border-top: 0; padding-top: 0; }
1728
1733
  @container (max-width: 600px) { .cx-set-sec { grid-template-columns: 1fr; } }
@@ -5540,3 +5545,6 @@ a.cx-bc-item:hover { color: var(--foreground); }
5540
5545
  /* LangScope: a plain block; its host draws nothing */
5541
5546
  .cx-lang-scope { display: contents; }
5542
5547
  .cx-lang-host { display: none; }
5548
+
5549
+ /* ── Field: a field that must be filled says so at its label ── */
5550
+ .cn-label.cx-required::after { content: " *"; color: var(--destructive); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@muten/shadcn",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "shadcn-style component registry for muten - own-the-source parts you add with `muten add`.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,3 +1,4 @@
1
1
  # Bell - the system's notifications: tap to see them, mark all read. Bell(items: @notices) items: {icon, tone, title, desc, time, unread}
2
+ # bare: "on" draws only the bell button (no heading, no caption), for an app's top bar: Bell(items: @notices, bare: "on")
2
3
  # shadcn-for-muten · part over KitWidget (kit core).
3
- part Bell(items: list) { Custom KitWidget inputs(name: "bell", items: $items) class("cx-bell-host") }
4
+ part Bell(items: list, bare: text = "") { Custom KitWidget inputs(name: "bell", items: $items, bare: $bare) class("cx-bell-host") }
@@ -3,15 +3,15 @@
3
3
  # Field(label: "Description", description: "Shows in your shop.") { TextArea(value: desc) }
4
4
  # Two short fields side by side: FieldRow { Field(…) { … } Field(…) { … } } (one column on a phone).
5
5
  # error: a message that replaces the description in the same place (nothing jumps) and marks the field invalid.
6
- # orientation: vertical | horizontal.
6
+ # orientation: vertical | horizontal. required: true marks the label (a red asterisk) for a field that must be filled.
7
7
  # FieldSet - sets the size and the look of every field inside it, the kit's own too (PasswordField, PhoneField…):
8
8
  # FieldSet(size: "2xl", variant: "filled") { Field(label: "Email") { … } PasswordField(label: "Password") }
9
9
  # FieldSeparator - a line with a word in the middle, between two ways in: FieldSeparator(label: "or with your email")
10
10
  # FieldHead - a label with an action beside it: FieldHead(label: "Password") { LinkBtn(to: "/recover", variant: "link", size: "sm") { Span "Forgot it?" } }
11
11
  # shadcn-for-muten · part · no deps · Nova (.cn-field) + kit.css.
12
- part Field(label: text = "", description: text = "", error: text = "", orientation: text = "vertical") {
12
+ part Field(label: text = "", description: text = "", error: text = "", orientation: text = "vertical", required: bool = false) {
13
13
  Stack class("cn-field cn-field-orientation-{$orientation} group/field") data(slot: "field", orientation: $orientation, invalid: $error != "") {
14
- Span "{$label}" class("cn-label") data(slot: "label")
14
+ Span "{$label}" class("cn-label", "cx-required" when $required) data(slot: "label")
15
15
  slot
16
16
  when $error == "" { Text "{$description}" class("cn-field-description") data(slot: "field-description") }
17
17
  when $error != "" { Text "{$error}" class("cn-field-error") data(slot: "field-error") aria(role: "alert") }
@@ -1,8 +1,12 @@
1
1
  # Input - a text field bound to a page text state. TextArea - the same for several lines (`Textarea` is muten's
2
2
  # primitive, so the part is TextArea).
3
3
  # Input(value: name, placeholder: "Name or phone") TextArea(value: notes, placeholder: "Notes for the team")
4
+ # SecretInput - the same field for a password, bound to a page text state (masked). PasswordField is the kit's
5
+ # show/hide + strength widget for CHOOSING a password; this one is for typing it in a sign-in.
6
+ # SecretInput(value: password, placeholder: "Your password", size: "xl", variant: "filled")
4
7
  # size: default (32) | xl (44) | 2xl (52) · variant: default (a line) | filled (the soft field of a landing: muted fill, no line).
5
8
  # For a whole form, set them once on a FieldSet around it instead (field.muten).
6
9
  # shadcn-for-muten · part · no deps · Nova (.cn-input, .cn-textarea).
7
10
  part Input(value: text, placeholder: text = "", size: text = "default", variant: text = "default") { SearchField bind($value) "{$placeholder}" class("cn-input") data(slot: "input", size: $size, variant: $variant) }
8
11
  part TextArea(value: text, placeholder: text = "") { Textarea bind($value) "{$placeholder}" class("cn-textarea") data(slot: "textarea") }
12
+ part SecretInput(value: text, placeholder: text = "", size: text = "default", variant: text = "default") { Password bind($value) "{$placeholder}" class("cn-input") data(slot: "input", size: $size, variant: $variant) }
@@ -98,7 +98,8 @@ export function orderBoard(root, i, core, on) {
98
98
  };
99
99
 
100
100
  const brd = $('.cx-board', root) || document.createElement('div'), brdLive = $('.cx-board-live', root);
101
- brd.innerHTML = LANES_B.map((l) => `<section class="cx-blane" data-lane="${l.id}" aria-label="${l.title}"><div class="cx-lane-h"><span>${l.title}</span><span class="cx-count"></span></div><div class="cx-lane-list">${l.cards.map((id) => `<div data-slot="card" data-size="sm" class="cn-card group/card cx-bcard" tabindex="0" data-card="${id}" aria-roledescription="tarjeta que se puede mover" aria-label="Pedido ${id} de ${CARDS[id].who}">${cardInner(id)}</div>`).join('')}</div></section>`).join('');
101
+ const cardHtml = (id) => `<div data-slot="card" data-size="sm" class="cn-card group/card cx-bcard" tabindex="0" data-card="${id}" aria-roledescription="tarjeta que se puede mover" aria-label="Pedido ${id} de ${CARDS[id].who}">${cardInner(id)}</div>`;
102
+ brd.innerHTML = LANES_B.map((l) => `<section class="cx-blane" data-lane="${l.id}" aria-label="${l.title}"><div class="cx-lane-h"><span>${l.title}</span><span class="cx-count"></span></div><div class="cx-lane-list">${l.cards.map(cardHtml).join('')}</div></section>`).join('');
102
103
  const lanesEls = () => $$('.cx-blane', brd);
103
104
  const syncLanes = () => lanesEls().forEach((l) => {
104
105
  const list = $('.cx-lane-list', l), n = $$('.cx-bcard', list).length;
@@ -299,4 +300,19 @@ export function orderBoard(root, i, core, on) {
299
300
  redraw(replyFor); replyFor = null; closeModal();
300
301
  }
301
302
  });
302
- syncLanes();}
303
+ syncLanes();
304
+ // The page sends the cards again (a live list, a refetch, another device's change): the same board takes them in place,
305
+ // without rebuilding itself (its listeners stay one). Nothing moves under a hand that is dragging a card.
306
+ return {
307
+ update(next) {
308
+ if ($('[data-lifted]', brd)) return;
309
+ for (const id of Object.keys(CARDS)) delete CARDS[id];
310
+ for (const c of list(next.cards)) CARDS[c.id] = { actions: [], ...copy(c) };
311
+ lanesEls().forEach((laneEl) => {
312
+ const want = list(next.cards).filter((c) => c.lane === laneEl.dataset.lane).map((c) => c.id);
313
+ $('.cx-lane-list', laneEl).innerHTML = want.map(cardHtml).join('');
314
+ });
315
+ syncLanes();
316
+ },
317
+ };
318
+ }
@@ -1485,7 +1485,7 @@ let wheelsNow = [];
1485
1485
  // ── Checklist { title, steps [{title, desc, done, action}] } ──
1486
1486
  kit.checklist = (root, { title, steps }) => {
1487
1487
  const T = (k, ...a) => tx(root, k, ...a);
1488
- const paint = () => { const n = steps.filter((s) => s.done).length; root.innerHTML = `<div data-slot="card" data-size="sm" class="cn-card"><div class="cx-vstack" style="gap:8px;padding:14px 14px 4px">${ui.text(title, 'heading')}<div class="cx-hstack" style="flex-wrap:nowrap;gap:10px">${ui.progress((n / steps.length) * 100, `$%n de ${steps.length} pasos`)}${ui.text(`$%n/${steps.length}`, 'caption')}</div></div><div class="cn-card-content" style="padding:0 12px 8px"><div class="cn-item-group">${steps.map((s, i) => ui.item({ media: `<div class="cn-item-media"><span class="cx-check-dot" ${s.done ? 'data-done' : ''}>${s.done ? ui.icon('i-tick') : i + 1}</span></div>`, title: s.title, description: s.desc, actions: s.done ? '' : ui.button({ label: s.action, size: 'xs', attrs: `data-step="${i}"` }) })).join(ui.itemSeparator())}</div></div></div>`; };
1488
+ const paint = () => { const n = steps.filter((s) => s.done).length; root.innerHTML = `<div data-slot="card" data-size="sm" class="cn-card"><div class="cx-vstack" style="gap:8px;padding:14px 14px 4px">${ui.text(title, 'heading')}<div class="cx-hstack" style="flex-wrap:nowrap;gap:10px">${ui.progress((n / steps.length) * 100, T('{0} of {1} steps', n, steps.length))}${ui.text(`${n}/${steps.length}`, 'caption')}</div></div><div class="cn-card-content" style="padding:0 12px 8px"><div class="cn-item-group">${steps.map((s, i) => ui.item({ media: `<div class="cn-item-media"><span class="cx-check-dot" ${s.done ? 'data-done' : ''}>${s.done ? ui.icon('i-tick') : i + 1}</span></div>`, title: s.title, description: s.desc, actions: s.done ? '' : ui.button({ label: s.action, size: 'xs', attrs: `data-step="${i}"` }) })).join(ui.itemSeparator())}</div></div></div>`; };
1489
1489
  root.addEventListener('click', (e) => { const b = e.target.closest('[data-step]'); if (!b) return; steps[+b.dataset.step].done = true; easeHeight(root, paint); });
1490
1490
  paint();
1491
1491
  };
@@ -1591,9 +1591,11 @@ let wheelsNow = [];
1591
1591
  };
1592
1592
 
1593
1593
  // ── Notifications bell { items [{icon,tone,title,desc,time,unread}] } ──
1594
- kit.bell = (root, { items }) => {
1594
+ kit.bell = (root, { items, bare }) => {
1595
1595
  const T = (k, ...a) => tx(root, k, ...a);
1596
- root.innerHTML = `<div class="cx-hstack" style="gap:12px">${ui.text(T('Notices'), 'heading')}<span class="cx-grow"></span><span class="cx-bell-wrap">${ui.button({ icon: 'i-bell', variant: 'outline', size: 'icon', cls: 'cx-has-indicator', attrs: 'data-bell aria-haspopup="dialog" aria-label="Avisos"' })}</span></div>${ui.text(T('The system bell: tap to see the notices.'), 'caption', 'p')}`;
1596
+ const wrap = `<span class="cx-bell-wrap">${ui.button({ icon: 'i-bell', variant: 'outline', size: 'icon', cls: 'cx-has-indicator', attrs: 'data-bell aria-haspopup="dialog" aria-label="Avisos"' })}</span>`;
1597
+ // bare: only the bell, for a top bar (no heading, no caption)
1598
+ root.innerHTML = bare ? wrap : `<div class="cx-hstack" style="gap:12px">${ui.text(T('Notices'), 'heading')}<span class="cx-grow"></span>${wrap}</div>${ui.text(T('The system bell: tap to see the notices.'), 'caption', 'p')}`;
1597
1599
  const pop = document.createElement('div'); pop.className = 'cn-popover-content pg-pop cx-bellpop'; pop.setAttribute('role', 'dialog'); pop.setAttribute('aria-label', T('Notices')); pop.hidden = true; lib.appendChild(pop);
1598
1600
  const btnEl = $('[data-bell]', root);
1599
1601
  const unread = () => items.filter((i) => i.unread).length;
@@ -162,7 +162,7 @@ Object.assign(ES, {
162
162
  'Card number': 'Número de la tarjeta', 'Expires': 'Vence', 'Code': 'Código', 'Secure payment with Stripe. We do not keep the number.': 'Pago seguro con Stripe. No guardamos el número.', 'Save card': 'Guardar tarjeta',
163
163
  'Check the card number.': 'Revisa el número de la tarjeta.', 'The expiry date is not valid.': 'La fecha de vencimiento no es válida.', 'The 3 or 4 digit code is missing.': 'Falta el código de 3 o 4 dígitos.',
164
164
  '{0} card ending in {1} saved.': 'Tarjeta {0} terminada en {1} guardada.', 'Invitation sent': 'Invitación enviada', 'Role of {0}': 'Rol de {0}', 'Email': 'Correo', 'name@email.com': 'nombre@correo.com', 'Role': 'Rol',
165
- 'Send invitation': 'Enviar invitación', 'Notices': 'Avisos', 'The system bell: tap to see the notices.': 'La campana del sistema: toca para ver los avisos.', 'Notices, {0} unread': 'Avisos, {0} sin leer',
165
+ 'Send invitation': 'Enviar invitación', 'Notices': 'Avisos', '{0} of {1} steps': '{0} de {1} pasos', 'The system bell: tap to see the notices.': 'La campana del sistema: toca para ver los avisos.', 'Notices, {0} unread': 'Avisos, {0} sin leer',
166
166
  'Mark all as read': 'Marcar todo como leído', 'Unread': 'Sin leer', 'All caught up': 'Todo al día', '{0} waiting': '{0} esperando', 'Offered {0} · {1}': 'Ofrecido {0} · {1}', '{0} · waiting since {1}': '{0} · espera desde {1}',
167
167
  'Offer a time': 'Ofrecer horario', 'today 16:30': 'hoy 16:30', '8 characters or more': '8 caracteres o más', 'A number': 'Un número', 'A capital letter': 'Una mayúscula', 'A symbol': 'Un símbolo',
168
168
  'Very weak': 'Muy débil', 'Weak': 'Débil', 'Fair': 'Regular', 'Good': 'Buena', 'Strong': 'Fuerte', 'Strength': 'Fuerza', 'Type a password': 'Escribe una contraseña', 'Show password': 'Mostrar contraseña', 'Hide password': 'Ocultar contraseña', 'Show': 'Mostrar',
@@ -39,8 +39,12 @@ export function dataTable(root, cfg, core, on) {
39
39
  const cbHtml = (label, on) => `<button data-slot="checkbox" role="checkbox" data-state="${on ? 'checked' : 'unchecked'}" aria-checked="${on}" class="cn-checkbox" aria-label="${label}"><span data-slot="checkbox-indicator" class="cn-checkbox-indicator"><svg class="cb-mark" viewBox="0 0 24 24">${CB_CHECK}</svg></span></button>`;
40
40
  const firstSort = dateCol ? { key: dateCol.key, dir: 1 } : { key: COLS[0]?.key, dir: 1 };
41
41
  const bulk = list(cfg.bulk);
42
+ // rows are selectable only when there is something to do with them: without bulk actions, no checkbox column
43
+ const picks = bulk.length > 0;
44
+ // the phone's «Filters» sheet only when there is a filter besides the search
45
+ const sheetFilters = () => FILTERS.some((f) => f.type !== 'search');
42
46
  const CELL_CLASS = { person: 'cx-dt-who', text: 'cx-dt-svc', date: 'cx-dt-when', status: 'cx-dt-st', money: 'cx-dt-num' };
43
- root.innerHTML = `<div class="cx-dt-host"><div class="cx-dt-toolbar"><div class="cx-hstack cx-dt-tools" role="toolbar" aria-label="${T('Filters')}"></div><span class="cx-text" data-variant="caption" data-dt-shown aria-live="polite"></span></div><div class="cx-hstack cx-dt-chips" hidden></div><div class="cx-dt-bulk" aria-live="polite"><div><span data-dt-count></span><div class="cx-hstack" style="gap:6px">${bulk.map((b) => ui.button({ label: b.label, variant: b.variant || 'outline', size: 'xs', attrs: `data-dt-bulk="${b.id}"` })).join('')}${ui.button({ label: T('Clear selection'), variant: 'ghost', size: 'xs', attrs: 'data-dt-unselect' })}</div></div></div><div class="cx-dt-scroll cx-scroll"><table class="cx-dt"><thead><tr><th class="cx-dt-check">${cbHtml(T('Select all'), false).replace('class="cn-checkbox"', 'class="cn-checkbox" data-dt-all')}</th>${COLS.map((c) => `<th aria-sort="${c.key === firstSort.key ? 'ascending' : 'none'}" ${c.type === 'money' ? 'class="cx-dt-num"' : ''}>${c.sortable === false ? ui.text(c.label, 'label') : `<button class="cn-button cn-button-variant-ghost cn-button-size-xs cx-th" data-sort="${c.key}">${c.label}<svg><use href="#${c.key === firstSort.key ? 'arrowup' : 'sort'}"/></svg></button>`}</th>`).join('')}</tr></thead><tbody></tbody></table></div></div>`;
47
+ root.innerHTML = `<div class="cx-dt-host"><div class="cx-dt-toolbar"><div class="cx-hstack cx-dt-tools" role="toolbar" aria-label="${T('Filters')}"></div><span class="cx-text" data-variant="caption" data-dt-shown aria-live="polite"></span></div><div class="cx-hstack cx-dt-chips" hidden></div><div class="cx-dt-bulk" aria-live="polite"><div><span data-dt-count></span><div class="cx-hstack" style="gap:6px">${bulk.map((b) => ui.button({ label: b.label, variant: b.variant || 'outline', size: 'xs', attrs: `data-dt-bulk="${b.id}"` })).join('')}${ui.button({ label: T('Clear selection'), variant: 'ghost', size: 'xs', attrs: 'data-dt-unselect' })}</div></div></div><div class="cx-dt-scroll cx-scroll"><table class="cx-dt"><thead><tr>${picks ? `<th class="cx-dt-check">${cbHtml(T('Select all'), false).replace('class="cn-checkbox"', 'class="cn-checkbox" data-dt-all')}</th>` : ''}${COLS.map((c) => `<th aria-sort="${c.key === firstSort.key ? 'ascending' : 'none'}" ${c.type === 'money' ? 'class="cx-dt-num"' : ''}>${c.sortable === false ? ui.text(c.label, 'label') : `<button class="cn-button cn-button-variant-ghost cn-button-size-xs cx-th" data-sort="${c.key}">${c.label}<svg><use href="#${c.key === firstSort.key ? 'arrowup' : 'sort'}"/></svg></button>`}</th>`).join('')}</tr></thead><tbody></tbody></table></div></div>`;
44
48
  const host = $('.cx-dt-host', root), dtTools = $('.cx-dt-tools', root), dtChips = $('.cx-dt-chips', root), dtCountEl = $('[data-dt-shown]', root), dtBody = $('tbody', root), dtAll = $('[data-dt-all]', root), dtBulk = $('.cx-dt-bulk', root);
45
49
  const dtSel = new Set(); let dtSort = firstSort;
46
50
  const passes = (r, except) => FILTERS.every((f) => f.id === except || PASS[f.type](f, fState[f.id], r));
@@ -53,7 +57,7 @@ export function dataTable(root, cfg, core, on) {
53
57
  const paintTools = () => {
54
58
  if (dtMobile) {
55
59
  const n = activeCount(), focused = document.activeElement?.dataset?.filter === searchF?.id;
56
- dtTools.innerHTML = searchHtml() + ui.button({ label: `${ui.icon('filter')}${T('Filters')}${n ? ui.badge(String(n), { variant: 'default' }) : ''}`, size: 'sm', cls: 'cx-fbtn-m', attrs: 'data-fmobile aria-haspopup="dialog"' });
60
+ dtTools.innerHTML = searchHtml() + (sheetFilters() ? ui.button({ label: `${ui.icon('filter')}${T('Filters')}${n ? ui.badge(String(n), { variant: 'default' }) : ''}`, size: 'sm', cls: 'cx-fbtn-m', attrs: 'data-fmobile aria-haspopup="dialog"' }) : '');
57
61
  if (focused) { const i = dtTools.querySelector('[data-filter]'); i.focus(); i.setSelectionRange(i.value.length, i.value.length); }
58
62
  return;
59
63
  }
@@ -146,7 +150,7 @@ export function dataTable(root, cfg, core, on) {
146
150
  return `<td class="cx-dt-svc" ${dateCol ? `data-when="${whenText(r[dateCol.key])}"` : ''}>${v ?? ''}</td>`;
147
151
  };
148
152
  const who = (r) => r[colOf('person')?.key ?? COLS[0]?.key] ?? '';
149
- const rowCells = (r) => `<td class="cx-dt-check">${cbHtml(T('Select {0}', who(r)), dtSel.has(r.id))}</td>${COLS.map((c) => cell(c, r)).join('')}`;
153
+ const rowCells = (r) => `${picks ? `<td class="cx-dt-check">${cbHtml(T('Select {0}', who(r)), dtSel.has(r.id))}</td>` : ''}${COLS.map((c) => cell(c, r)).join('')}`;
150
154
  const EASE = 'cubic-bezier(.23, 1, .32, 1)';
151
155
  let dtAnim = 0;
152
156
  const dtPaint = () => {
@@ -170,7 +174,7 @@ export function dataTable(root, cfg, core, on) {
170
174
  };
171
175
  if (leaving.length && dtBody.children.length) { leaving.forEach((tr) => tr.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 130, easing: 'ease-out', fill: 'forwards' })); setTimeout(apply, 130); } else apply();
172
176
  dtCountEl.textContent = rows.length === DT.length ? `${DT.length} ${DT.length === 1 ? ONE : MANY}` : `${rows.length} de ${DT.length} ${MANY}`; paintChips();
173
- setCheck(dtAll, dtSel.size === 0 ? false : dtSel.size === DT.length ? true : 'mixed');
177
+ if (dtAll) setCheck(dtAll, dtSel.size === 0 ? false : dtSel.size === DT.length ? true : 'mixed');
174
178
  dtBulk.toggleAttribute('data-open', dtSel.size > 0);
175
179
  if (dtSel.size) $('[data-dt-count]', root).textContent = `${dtSel.size} ${dtSel.size === 1 ? 'seleccionada' : 'seleccionadas'}`;
176
180
  syncMobileCount(rows.length);
@@ -181,10 +185,10 @@ export function dataTable(root, cfg, core, on) {
181
185
  $$('thead [data-sort]', root).forEach((b) => { const on = b.dataset.sort === key; b.parentElement.setAttribute('aria-sort', on ? (dtSort.dir === 1 ? 'ascending' : 'descending') : 'none'); $('use', b).setAttribute('href', on ? (dtSort.dir === 1 ? '#arrowup' : '#arrowdown') : '#sort'); });
182
186
  dtPaint();
183
187
  });
184
- dtAll.addEventListener('click', () => { if (dtSel.size === DT.length) dtSel.clear(); else DT.forEach((r) => dtSel.add(r.id)); dtPaint(); });
188
+ dtAll?.addEventListener('click', () => { if (dtSel.size === DT.length) dtSel.clear(); else DT.forEach((r) => dtSel.add(r.id)); dtPaint(); });
185
189
  dtBody.addEventListener('click', (e) => {
186
190
  if (e.target.closest('[data-fclear-empty]')) { dtTools.querySelector('[data-fclear]')?.click(); return; }
187
- const tr = e.target.closest('tr'); if (!tr || tr.dataset.id === 'empty') return;
191
+ const tr = e.target.closest('tr'); if (!picks || !tr || tr.dataset.id === 'empty') return;
188
192
  if (!e.target.closest('.cn-checkbox') && !host.hasAttribute('data-narrow') && !matchMedia('(max-width: 640px)').matches) return;
189
193
  const raw = tr.dataset.id, id = DT.find((r) => String(r.id) === raw)?.id; dtSel.has(id) ? dtSel.delete(id) : dtSel.add(id); dtPaint();
190
194
  });
@@ -196,5 +200,12 @@ export function dataTable(root, cfg, core, on) {
196
200
  dtSel.clear(); dtPaint(); on?.action?.(JSON.stringify({ action: b.dataset.dtBulk, ids }));
197
201
  });
198
202
  paintTools(); dtPaint();
199
- return { refresh: () => { dtPaint(); paintTools(); } };
203
+ // new rows from the page (a live list): the same table takes them, keeping its sort, filters and selection
204
+ const setRows = (rows) => {
205
+ DT.splice(0, DT.length, ...list(rows).map((r, i) => ({ id: r.id ?? i, ...r })));
206
+ const ids = new Set(DT.map((r) => r.id));
207
+ [...dtSel].forEach((id) => { if (!ids.has(id)) dtSel.delete(id); });
208
+ dtPaint(); paintTools();
209
+ };
210
+ return { refresh: () => { dtPaint(); paintTools(); }, setRows };
200
211
  }
@@ -37,7 +37,7 @@ export function mount(el, inputs, on) {
37
37
  memberList: ({ kit }, i) => kit.memberList(el, { members: list(i.members), roles: list(i.roles), ownerRole: i.owner || '', noun: [i.one || 'persona', i.many || 'personas'], invite: { cta: i.cta || 'Invitar', title: i.cta || 'Invitar', description: i.note || '' } }),
38
38
  permissionMatrix: ({ kit }, i) => kit.permissionMatrix(el, { roles: list(i.roles).map((r) => r.name ?? r), rows: list(i.rows), lockedRole: i.locked || '', onChange: (rows) => on.action?.(JSON.stringify(rows)) }),
39
39
  stampCard: ({ kit }, i) => kit.stampCard(el, { store: i.store || '', owner: i.owner || '', total: Number(i.total) || 10, stamps: Number(i.stamps) || 0, reward: i.reward || '', ...Object.fromEntries(['title', 'progress', 'complete', 'count', 'redeem', 'stamp'].filter((k) => i[k]).map((k) => [k, i[k]])) }),
40
- bell: ({ kit }, i) => kit.bell(el, { items: list(i.items) }),
40
+ bell: ({ kit }, i) => kit.bell(el, { items: list(i.items), bare: i.bare === 'on' }),
41
41
  queue: ({ kit }, i) => kit.queue(el, { title: i.title || '', people: list(i.people), offerMinutes: Number(i.minutes) || 10 }),
42
42
  password: ({ kit, t, langOf }, i) => kit.password(el, { label: t(langOf(el), i.label || 'Password') }),
43
43
  // days: {name, open, ranges [["09:00","13:00"]]}. Inside a form with a DirtyBar, “Descartar” brings the hours back.
@@ -48,12 +48,14 @@ export function mount(el, inputs, on) {
48
48
  el.addEventListener('input', () => on.action?.(JSON.stringify(wh.value())));
49
49
  },
50
50
  };
51
- RENDER.table = (core, i) => import('@muten/shadcn/registry/kit/table.js').then(({ dataTable }) => dataTable(el, { columns: i.columns, rows: i.rows, filters: i.filters, statuses: i.statuses, noun: [i.one || 'fila', i.many || 'filas'], bulk: i.bulk }, core, on));
51
+ RENDER.table = (core, i) => import('@muten/shadcn/registry/kit/table.js').then(({ dataTable }) => (shown = dataTable(el, { columns: i.columns, rows: i.rows, filters: i.filters, statuses: i.statuses, noun: [i.one || 'fila', i.many || 'filas'], bulk: i.bulk }, core, on)));
52
52
  // until the kit arrives the host keeps its final size with a shimmer (kit.css, :empty), then the widget fades in
53
53
  el.classList.add('cx-kw-wait');
54
54
  // its language: inputs.lang, else the nearest lang (the page's); a lang that changes repaints it in the new one
55
55
  if (inputs.lang) el.setAttribute('lang', inputs.lang);
56
56
  let core0 = null;
57
+ // what a widget hands back to be fed later (the table's setRows)
58
+ let shown = null;
57
59
  import('@muten/shadcn/registry/kit/core.js').then((core) => {
58
60
  core0 = core;
59
61
  const render = RENDER[inputs.name]; if (!render) return;
@@ -80,5 +82,6 @@ export function mount(el, inputs, on) {
80
82
  el.addEventListener('pointerdown', () => { stopped = true; clearTimeout(timer); }, { once: true, capture: true });
81
83
  timer = setTimeout(tick, d.every);
82
84
  }
83
- return () => {};
85
+ // muten calls this when a bound state changes: a table takes its new rows in place, the rest keep their snapshot
86
+ return (next) => { if (inputs.name === 'table' && next && shown?.setRows) shown.setRows(next.rows); };
84
87
  }
@@ -1,6 +1,9 @@
1
1
  // OrderBoard / OrderCards (muten Custom) - the kit's order Board and card (kit/board.js). inputs: lanes, cards, sizes;
2
2
  // handlers change(json [{id, lane}]) · action(json {id, action}).
3
3
  export function mount(el, inputs, on) {
4
- Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/board.js')]).then(([core, b]) => core.relang(el, inputs, () => b.orderBoard(el, inputs, core, on)));
5
- return () => {};
4
+ // the board is painted once (and again when the language changes); new cards after that go through its update, so a
5
+ // live list or a refetch shows on the board in place instead of being ignored
6
+ let current = inputs, board = null, last = JSON.stringify(inputs.cards);
7
+ Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/board.js')]).then(([core, b]) => core.relang(el, inputs, () => { board = b.orderBoard(el, current, core, on); }));
8
+ return (next) => { const sig = JSON.stringify(next.cards); if (sig === last) return; last = sig; current = next; if (board && board.update) board.update(next); };
6
9
  }
@@ -4,6 +4,8 @@
4
4
  # TableView(columns: @cols, rows: @appointments, filters: @filters, statuses: @statuses, bulk: @bulk, one: "appointment", many: "appointments", onAction: act)
5
5
  # columns {key, label, type: person|text|date|status|money, sub} · filters {id, type, label, key | keys} ·
6
6
  # statuses {id, label, tone} · bulk {id, label, variant, set {…}} → onAction gets {"action", "ids"}.
7
+ # The rows scroll inside the table (the header stays) past 380 px; a page that IS the table gives it more: --dt-h.
8
+ # TableView(…) class("clients-table") + .clients-table { --dt-h: calc(100vh - 240px); }
7
9
  # shadcn-for-muten · part over KitWidget (kit core + kit/table.js).
8
10
  part TableView(columns: list, rows: list, filters: list, statuses: list, bulk: list, one: text = "row", many: text = "rows", onAction: action) {
9
11
  Custom KitWidget inputs(name: "table", columns: $columns, rows: $rows, filters: $filters, statuses: $statuses, bulk: $bulk, one: $one, many: $many) on(action: $onAction) class("cx-table-host")
package/registry.json CHANGED
@@ -464,7 +464,7 @@
464
464
  "deps": [],
465
465
  "part": "Input",
466
466
  "file": "registry/input.muten",
467
- "description": "Input(value, placeholder) and TextArea(value, placeholder): Nova fields bound to a page text state."
467
+ "description": "Input(value, placeholder), TextArea(value, placeholder) and SecretInput(value, placeholder): Nova fields bound to a page text state (SecretInput masked, for a sign-in)."
468
468
  },
469
469
  {
470
470
  "name": "inspector",