@muten/shadcn 0.2.9 → 0.2.11
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 +20 -1
- package/package.json +1 -1
- package/registry/field.muten +3 -3
- package/registry/kit/table.js +22 -9
- package/registry/kit-widget.js +5 -2
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[data-when]::after { content: none; }
|
|
780
784
|
}
|
|
781
785
|
|
|
782
786
|
/* ── Board ── */
|
|
@@ -5541,3 +5545,18 @@ 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); }
|
|
5551
|
+
|
|
5552
|
+
/* ── Alert with an action: the action takes its own column (Nova's absolute corner reserves 72 px, and a wider button
|
|
5553
|
+
covered the title); on a phone it goes under the text ── */
|
|
5554
|
+
.style-nova .cn-alert:has(> .cn-alert-action) { padding-right: 10px; grid-template-columns: 1fr auto; }
|
|
5555
|
+
.style-nova .cn-alert:has(> :is(svg, .mu-icon)):has(> .cn-alert-action) { grid-template-columns: auto 1fr auto; }
|
|
5556
|
+
.style-nova .cn-alert > .cn-alert-action { position: static; grid-column: -2; grid-row: 1 / span 9; align-self: center; margin-left: 8px; }
|
|
5557
|
+
@media (max-width: 560px) {
|
|
5558
|
+
.style-nova .cn-alert:has(> .cn-alert-action) { grid-template-columns: 1fr; }
|
|
5559
|
+
.style-nova .cn-alert:has(> :is(svg, .mu-icon)):has(> .cn-alert-action) { grid-template-columns: auto 1fr; }
|
|
5560
|
+
.style-nova .cn-alert > .cn-alert-action { grid-column: 1 / -1; grid-row: auto; justify-self: start; margin: 8px 0 0; }
|
|
5561
|
+
.style-nova .cn-alert:has(> :is(svg, .mu-icon)) > .cn-alert-action { grid-column: 2; }
|
|
5562
|
+
}
|
package/package.json
CHANGED
package/registry/field.muten
CHANGED
|
@@ -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") }
|
package/registry/kit/table.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// and bulk actions are data. Everything else is the artifact's: filters from their type (search · facet · date ·
|
|
3
3
|
// range) with counts, chips that leave with a fade, rows that fade out / slide / enter, the height easing, sort by
|
|
4
4
|
// header, selection with its own bulk bar, the phone's “search + Filters” sheet, and a narrow box becoming Items.
|
|
5
|
-
// columns [{key, label, type: person|text|date|status|money, sub (person: the key under the name), sortable}]
|
|
5
|
+
// columns [{key, label, type: person|text|date|status|money, sub (person: the key under the name), sortable, sort: "desc" (opens newest first)}]
|
|
6
6
|
// rows [{id, …keys}] (a date is "yyyy-mm-ddTHH:MM"; a row's date may be "+N" days from today too)
|
|
7
7
|
// filters [{id, type: search|facet|date|range, label, key | keys (search)}]
|
|
8
8
|
// statuses [{id, label, tone: ok|wait|info|bad}] · noun ["appointment", "appointments"] · bulk [{id, label, variant}]
|
|
@@ -37,10 +37,16 @@ export function dataTable(root, cfg, core, on) {
|
|
|
37
37
|
};
|
|
38
38
|
const SORTERS = Object.fromEntries(COLS.map((c) => [c.key, c.type === 'status' ? (r) => ST_ORDER.indexOf(r[c.key]) : c.type === 'date' ? (r) => +asDate(r[c.key]) : (r) => r[c.key]]));
|
|
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
|
+
// the table opens sorted by its date column (else the first); a column with sort: "desc" opens newest first (a log)
|
|
41
|
+
const opening = dateCol ?? COLS[0];
|
|
42
|
+
const firstSort = { key: opening?.key, dir: opening?.sort === 'desc' ? -1 : 1 };
|
|
41
43
|
const bulk = list(cfg.bulk);
|
|
44
|
+
// rows are selectable only when there is something to do with them: without bulk actions, no checkbox column
|
|
45
|
+
const picks = bulk.length > 0;
|
|
46
|
+
// the phone's «Filters» sheet only when there is a filter besides the search
|
|
47
|
+
const sheetFilters = () => FILTERS.some((f) => f.type !== 'search');
|
|
42
48
|
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
|
|
49
|
+
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 ? (firstSort.dir === 1 ? 'ascending' : 'descending') : '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 ? (firstSort.dir === 1 ? 'arrowup' : 'arrowdown') : 'sort'}"/></svg></button>`}</th>`).join('')}</tr></thead><tbody></tbody></table></div></div>`;
|
|
44
50
|
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
51
|
const dtSel = new Set(); let dtSort = firstSort;
|
|
46
52
|
const passes = (r, except) => FILTERS.every((f) => f.id === except || PASS[f.type](f, fState[f.id], r));
|
|
@@ -53,7 +59,7 @@ export function dataTable(root, cfg, core, on) {
|
|
|
53
59
|
const paintTools = () => {
|
|
54
60
|
if (dtMobile) {
|
|
55
61
|
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"' });
|
|
62
|
+
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
63
|
if (focused) { const i = dtTools.querySelector('[data-filter]'); i.focus(); i.setSelectionRange(i.value.length, i.value.length); }
|
|
58
64
|
return;
|
|
59
65
|
}
|
|
@@ -146,7 +152,7 @@ export function dataTable(root, cfg, core, on) {
|
|
|
146
152
|
return `<td class="cx-dt-svc" ${dateCol ? `data-when="${whenText(r[dateCol.key])}"` : ''}>${v ?? ''}</td>`;
|
|
147
153
|
};
|
|
148
154
|
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
|
|
155
|
+
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
156
|
const EASE = 'cubic-bezier(.23, 1, .32, 1)';
|
|
151
157
|
let dtAnim = 0;
|
|
152
158
|
const dtPaint = () => {
|
|
@@ -170,7 +176,7 @@ export function dataTable(root, cfg, core, on) {
|
|
|
170
176
|
};
|
|
171
177
|
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
178
|
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');
|
|
179
|
+
if (dtAll) setCheck(dtAll, dtSel.size === 0 ? false : dtSel.size === DT.length ? true : 'mixed');
|
|
174
180
|
dtBulk.toggleAttribute('data-open', dtSel.size > 0);
|
|
175
181
|
if (dtSel.size) $('[data-dt-count]', root).textContent = `${dtSel.size} ${dtSel.size === 1 ? 'seleccionada' : 'seleccionadas'}`;
|
|
176
182
|
syncMobileCount(rows.length);
|
|
@@ -181,10 +187,10 @@ export function dataTable(root, cfg, core, on) {
|
|
|
181
187
|
$$('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
188
|
dtPaint();
|
|
183
189
|
});
|
|
184
|
-
dtAll
|
|
190
|
+
dtAll?.addEventListener('click', () => { if (dtSel.size === DT.length) dtSel.clear(); else DT.forEach((r) => dtSel.add(r.id)); dtPaint(); });
|
|
185
191
|
dtBody.addEventListener('click', (e) => {
|
|
186
192
|
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;
|
|
193
|
+
const tr = e.target.closest('tr'); if (!picks || !tr || tr.dataset.id === 'empty') return;
|
|
188
194
|
if (!e.target.closest('.cn-checkbox') && !host.hasAttribute('data-narrow') && !matchMedia('(max-width: 640px)').matches) return;
|
|
189
195
|
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
196
|
});
|
|
@@ -196,5 +202,12 @@ export function dataTable(root, cfg, core, on) {
|
|
|
196
202
|
dtSel.clear(); dtPaint(); on?.action?.(JSON.stringify({ action: b.dataset.dtBulk, ids }));
|
|
197
203
|
});
|
|
198
204
|
paintTools(); dtPaint();
|
|
199
|
-
|
|
205
|
+
// new rows from the page (a live list): the same table takes them, keeping its sort, filters and selection
|
|
206
|
+
const setRows = (rows) => {
|
|
207
|
+
DT.splice(0, DT.length, ...list(rows).map((r, i) => ({ id: r.id ?? i, ...r })));
|
|
208
|
+
const ids = new Set(DT.map((r) => r.id));
|
|
209
|
+
[...dtSel].forEach((id) => { if (!ids.has(id)) dtSel.delete(id); });
|
|
210
|
+
dtPaint(); paintTools();
|
|
211
|
+
};
|
|
212
|
+
return { refresh: () => { dtPaint(); paintTools(); }, setRows };
|
|
200
213
|
}
|
package/registry/kit-widget.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|