@muten/shadcn 0.2.9 → 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/kit.css CHANGED
@@ -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 ── */
@@ -5541,3 +5545,6 @@ a.cx-bc-item:hover { color: var(--foreground); }
5541
5545
  /* LangScope: a plain block; its host draws nothing */
5542
5546
  .cx-lang-scope { display: contents; }
5543
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.9",
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",
@@ -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") }
@@ -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
  }
@@ -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
  }