@muten/shadcn 0.2.7 → 0.2.9
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 +2 -0
- package/kit.css +3 -2
- package/nova.css +3 -1
- package/package.json +1 -1
- package/registry/bell.muten +2 -1
- package/registry/input.muten +4 -0
- package/registry/kit/board.js +18 -2
- package/registry/kit/core.js +5 -3
- package/registry/kit/i18n.js +1 -1
- package/registry/kit-widget.js +1 -1
- package/registry/order-board.js +5 -2
- package/registry/table-view.muten +2 -0
- package/registry.json +1 -1
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; }
|
|
@@ -1722,7 +1722,8 @@ p.cx-text { margin: 0; }
|
|
|
1722
1722
|
|
|
1723
1723
|
/* ═════ Settings ═════ */
|
|
1724
1724
|
.cx-settings { width: 100%; max-width: 760px; }
|
|
1725
|
-
|
|
1725
|
+
/* the sections fold to one column under 600px of the FORM (not the window): it is their container, so it needs its width */
|
|
1726
|
+
.cx-set-form { position: relative; width: 100%; container-type: inline-size; display: flex; flex-direction: column; gap: 0; padding-bottom: 72px; }
|
|
1726
1727
|
.cx-set-sec { display: grid; grid-template-columns: 220px 1fr; gap: 16px 32px; padding: 20px 0; border-top: 1px solid var(--border); }
|
|
1727
1728
|
.cx-set-sec:first-child { border-top: 0; padding-top: 0; }
|
|
1728
1729
|
@container (max-width: 600px) { .cx-set-sec { grid-template-columns: 1fr; } }
|
package/nova.css
CHANGED
|
@@ -1006,7 +1006,9 @@
|
|
|
1006
1006
|
.style-nova .cn-card-title { @apply text-base leading-snug font-medium group-data-[size=sm]/card:text-sm; }
|
|
1007
1007
|
.style-nova .cn-card-description { @apply text-muted-foreground text-sm; }
|
|
1008
1008
|
.style-nova .cn-card-content { @apply px-(--card-spacing); }
|
|
1009
|
-
|
|
1009
|
+
/* the foot takes its card's corners (inherit), never a radius of its own: an app that rounds the card otherwise saw the
|
|
1010
|
+
foot's wider curve inside the card's edge */
|
|
1011
|
+
.style-nova .cn-card-footer { @apply bg-muted/50 rounded-b-[inherit] border-t p-(--card-spacing); }
|
|
1010
1012
|
.style-nova .cn-carousel-previous { @apply rounded-full; }
|
|
1011
1013
|
.style-nova .cn-carousel-next { @apply rounded-full; }
|
|
1012
1014
|
.style-nova .cn-chart-tooltip { @apply border-border/50 bg-background gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl; }
|
package/package.json
CHANGED
package/registry/bell.muten
CHANGED
|
@@ -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") }
|
package/registry/input.muten
CHANGED
|
@@ -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) }
|
package/registry/kit/board.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/registry/kit/core.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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;
|
package/registry/kit/i18n.js
CHANGED
|
@@ -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',
|
package/registry/kit-widget.js
CHANGED
|
@@ -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.
|
package/registry/order-board.js
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
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
|
|
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",
|