@muten/shadcn 0.1.15 → 0.2.1
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 +88 -1
- package/globals.css +1 -0
- package/kit.css +348 -126
- package/motion.css +3 -0
- package/package.json +1 -1
- package/registry/accordion.muten +3 -3
- package/registry/agenda.js +20 -16
- package/registry/agenda.muten +14 -12
- package/registry/alert-dialog.muten +2 -2
- package/registry/alert.muten +1 -1
- package/registry/app-shell.muten +1 -1
- package/registry/attachment.muten +1 -1
- package/registry/avatar.muten +4 -4
- package/registry/badge.muten +14 -3
- package/registry/band.muten +2 -2
- package/registry/bar-chart.muten +2 -2
- package/registry/bio-play.muten +2 -2
- package/registry/booking-play.muten +5 -5
- package/registry/booking.muten +2 -2
- package/registry/breadcrumb.muten +4 -4
- package/registry/button.muten +9 -7
- package/registry/calendar.js +15 -10
- package/registry/calendar.muten +1 -1
- package/registry/card.muten +1 -1
- package/registry/carousel.muten +1 -1
- package/registry/chat-play.muten +8 -8
- package/registry/chatbot.muten +5 -4
- package/registry/checklist.muten +1 -1
- package/registry/choice-list.muten +3 -3
- package/registry/code.js +10 -4
- package/registry/collapsible.muten +2 -2
- package/registry/color-picker.js +37 -36
- package/registry/color-picker.muten +1 -1
- package/registry/combobox.js +1 -1
- package/registry/combobox.muten +2 -2
- package/registry/command-palette.muten +2 -2
- package/registry/command.js +1 -1
- package/registry/command.muten +7 -7
- package/registry/context-menu.muten +3 -3
- package/registry/count-up.js +33 -31
- package/registry/count-up.muten +6 -4
- package/registry/date-field.js +1 -1
- package/registry/date-field.muten +3 -3
- package/registry/day-plan.muten +5 -5
- package/registry/device-frame.muten +3 -3
- package/registry/dialog.muten +1 -1
- package/registry/dirty-bar.js +2 -2
- package/registry/dirty-bar.muten +3 -3
- package/registry/drawer.muten +1 -1
- package/registry/dropdown-menu.muten +4 -4
- package/registry/empty.muten +1 -1
- package/registry/feature-grid.muten +1 -1
- package/registry/feature-rows.muten +2 -2
- package/registry/field-behavior.js +13 -3
- package/registry/field-behavior.muten +3 -3
- package/registry/field.muten +5 -5
- package/registry/file-input.js +8 -4
- package/registry/file-input.muten +3 -3
- package/registry/fixed-card.muten +4 -4
- package/registry/folder-picker.js +6 -2
- package/registry/folder-picker.muten +2 -2
- package/registry/heatmap.muten +5 -5
- package/registry/image-upload.js +1 -1
- package/registry/image-upload.muten +2 -2
- package/registry/inbox-play.muten +5 -5
- package/registry/indicator.muten +1 -1
- package/registry/info-table.muten +1 -1
- package/registry/input-group.muten +11 -11
- package/registry/input-otp.muten +2 -2
- package/registry/input.muten +1 -1
- package/registry/inspector.muten +2 -2
- package/registry/item.muten +2 -2
- package/registry/kit/board.js +36 -34
- package/registry/kit/collections.js +24 -24
- package/registry/kit/core.js +231 -118
- package/registry/kit/dates.js +29 -27
- package/registry/kit/i18n.js +172 -0
- package/registry/kit/menus.js +14 -13
- package/registry/kit/overlays.js +7 -2
- package/registry/kit/structure.js +66 -61
- package/registry/kit/table.js +19 -17
- package/registry/kit/values.js +36 -28
- package/registry/kit-skeleton.muten +1 -1
- package/registry/kit-widget.js +9 -4
- package/registry/lang-scope.js +8 -0
- package/registry/lang-scope.muten +5 -0
- package/registry/legal-doc.muten +5 -5
- package/registry/link-btn.muten +1 -1
- package/registry/list-motion.js +46 -0
- package/registry/list-motion.muten +4 -0
- package/registry/live-stack.muten +2 -2
- package/registry/logo-cloud.muten +2 -2
- package/registry/marketing.muten +7 -7
- package/registry/member-list.muten +2 -2
- package/registry/menubar.muten +3 -3
- package/registry/message-scroller.muten +3 -3
- package/registry/message.muten +9 -9
- package/registry/messaging.muten +1 -1
- package/registry/modal-behavior.js +24 -0
- package/registry/modal.muten +4 -4
- package/registry/money-field.js +1 -1
- package/registry/money-field.muten +2 -2
- package/registry/nav-button.muten +4 -4
- package/registry/navigation-menu.muten +4 -4
- package/registry/navpill.muten +6 -6
- package/registry/new-appointment.js +1 -1
- package/registry/new-appointment.muten +4 -4
- package/registry/number-field.js +6 -4
- package/registry/number-field.muten +1 -1
- package/registry/order-board.js +1 -1
- package/registry/order-board.muten +2 -2
- package/registry/order-play.muten +4 -4
- package/registry/pagination-nav.js +1 -1
- package/registry/pagination-nav.muten +3 -3
- package/registry/pagination.muten +4 -4
- package/registry/password-field.muten +2 -2
- package/registry/permission-matrix.muten +4 -4
- package/registry/phone-bar.muten +6 -6
- package/registry/phone-field.js +1 -1
- package/registry/phone-field.muten +1 -1
- package/registry/phone-shell.js +1 -1
- package/registry/pill.muten +2 -2
- package/registry/plans.muten +1 -1
- package/registry/popover.muten +2 -2
- package/registry/pricing.muten +4 -4
- package/registry/product-play.muten +3 -3
- package/registry/progress.muten +3 -3
- package/registry/queue.muten +1 -1
- package/registry/radio-group.muten +1 -1
- package/registry/rank-item.muten +4 -4
- package/registry/resizable.muten +1 -1
- package/registry/row-menu.js +1 -1
- package/registry/row-menu.muten +2 -2
- package/registry/save-button.muten +5 -5
- package/registry/scale-slider.js +1 -1
- package/registry/scale-slider.muten +2 -2
- package/registry/screen-tour.muten +1 -1
- package/registry/share-bar.muten +2 -2
- package/registry/share-button.muten +4 -4
- package/registry/sheet.muten +1 -1
- package/registry/sidebar.muten +12 -6
- package/registry/site-footer.muten +6 -6
- package/registry/sortable-behavior.js +67 -20
- package/registry/sortable-list.js +3 -1
- package/registry/sortable-list.muten +2 -2
- package/registry/sortable.muten +5 -4
- package/registry/spinner.muten +2 -2
- package/registry/split-button.muten +2 -2
- package/registry/stacked-bars.js +3 -3
- package/registry/stacked-bars.muten +2 -2
- package/registry/stamp-card.muten +2 -2
- package/registry/stat-band.muten +3 -3
- package/registry/stat.muten +3 -3
- package/registry/status-bar.muten +2 -2
- package/registry/stepper.muten +4 -4
- package/registry/store-qr.js +1 -1
- package/registry/store-qr.muten +2 -2
- package/registry/swatch.muten +1 -1
- package/registry/switch.muten +8 -7
- package/registry/table-view.muten +3 -3
- package/registry/tabs-behavior.js +9 -1
- package/registry/tabs.muten +2 -2
- package/registry/tag-input.js +1 -1
- package/registry/tag-input.muten +2 -2
- package/registry/text.muten +2 -2
- package/registry/time-field.js +1 -1
- package/registry/time-field.muten +3 -3
- package/registry/timeline.muten +4 -4
- package/registry/toaster.muten +2 -2
- package/registry/toggle-group.muten +1 -1
- package/registry/toggle.muten +3 -3
- package/registry/toolbar.muten +4 -4
- package/registry/tooltip.muten +2 -2
- package/registry/tree-view.js +38 -25
- package/registry/tree-view.muten +1 -1
- package/registry/veil.muten +1 -1
- package/registry/virtual-list.js +1 -1
- package/registry/virtual-list.muten +2 -2
- package/registry/virtual-window.js +4 -4
- package/registry/virtual-window.muten +2 -2
- package/registry/wordline.muten +1 -1
- package/registry/zoom-control.muten +1 -1
- package/registry.json +22 -2
package/motion.css
CHANGED
|
@@ -70,6 +70,9 @@
|
|
|
70
70
|
/* Hover affordances. */
|
|
71
71
|
.hover-lift { transition: transform var(--mu-motion, 200ms) var(--mu-ease, cubic-bezier(0.16,1,0.3,1)), box-shadow var(--mu-motion, 200ms) ease; }
|
|
72
72
|
.hover-lift:hover { transform: translateY(-4px); box-shadow: var(--shadow-soft, 0 12px 32px -12px rgb(0 0 0 / 0.25)); }
|
|
73
|
+
/* lifted, an invisible strip fills the 4 px it left below, so a pointer resting on the bottom edge stays «on» it:
|
|
74
|
+
without it the card rose off the pointer, dropped back under it and rose again, a hover that blinked forever */
|
|
75
|
+
.hover-lift:hover::after { content: ""; position: absolute; left: 0; right: 0; top: calc(100% - 2px); height: 10px; }
|
|
73
76
|
.hover-zoom img { transition: transform 0.5s var(--mu-ease, cubic-bezier(0.16,1,0.3,1)); }
|
|
74
77
|
.hover-zoom:hover img { transform: scale(1.05); }
|
|
75
78
|
|
package/package.json
CHANGED
package/registry/accordion.muten
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Accordion - stacked questions; each opens to its content's exact height (200 ms) and the arrow turns instead of
|
|
2
2
|
# swapping icons. The page owns a bool per item and the toggle.
|
|
3
|
-
# Accordion { AccordionItem(title: "
|
|
3
|
+
# Accordion { AccordionItem(title: "Can I cancel my appointment?", open: a1, onToggle: t1) { Text "Yes, up to 3 hours before…" } }
|
|
4
4
|
# No state to own (or a search engine must read the answers closed)? The same look on the native details:
|
|
5
|
-
# Accordion(size: "lg") { Details "
|
|
5
|
+
# Accordion(size: "lg") { Details "…?" group("faq") class("cn-accordion-item") { Text "…" class("cn-accordion-content-inner") } }
|
|
6
6
|
# size "lg": taller rows and bigger type, for the questions of a landing page.
|
|
7
7
|
# shadcn-for-muten · part · no deps · Nova (.cn-accordion) + kit.css.
|
|
8
8
|
part Accordion(size: text = "default") { Stack class("cn-accordion") data(slot: "accordion", size: $size) { slot } }
|
|
@@ -18,7 +18,7 @@ part AccordionItem(title: text, open: bool, onToggle: action) {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
# FaqItem - one question of an Accordion on the native <details>: no state to keep, and items sharing a `group` open one
|
|
21
|
-
# at a time (they share the group
|
|
21
|
+
# at a time (they share the group “faq”). The answer goes in the slot (text, or text with a button). FaqItemOpen starts open (the first of a list).
|
|
22
22
|
part FaqItem(question: text) {
|
|
23
23
|
Details "{$question}" group("faq") class("cn-accordion-item") { Stack class("cn-accordion-content-inner") { slot } }
|
|
24
24
|
}
|
package/registry/agenda.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// Agenda / AgendaLive (muten Custom) - the kit's agenda (kit/core.js): day columns per person, week, list; drag to move,
|
|
2
2
|
// edge to resize. Everything is data:
|
|
3
3
|
// resources [{id, name, photo, kind, capacity, group}] - each one is an AGENDA: kind "business" (the shop's own, shown
|
|
4
|
-
// first), "person" (a worker, the default) or any other (a room: say its `group`,
|
|
4
|
+
// first), "person" (a worker, the default) or any other (a room: say its `group`, “Rooms”). capacity = how many appointments
|
|
5
5
|
// it holds at once: 1 (default, never overlap) · 3 (a class of three) · 0 (no limit). overlap "never" | "allow"
|
|
6
|
-
// overrides every capacity. Citas that break one (the server sent them crossed) say
|
|
6
|
+
// overrides every capacity. Citas that break one (the server sent them crossed) say “Se cruza”.
|
|
7
7
|
// resources [{id, name, photo}] · hours [{id, days: [1..6], ranges: [[540, 780], [840, 1140]]}] (minutes from midnight);
|
|
8
8
|
// a person can have several entries, one per group of days, when the days do not share one schedule
|
|
9
9
|
// events [{id, resource, with: [ids], day, start "HH:MM", dur, title, subtitle, price, status, tint, ink, tone, steps, actions, at, payment, contact}]
|
|
10
10
|
// day = "yyyy-mm-dd", or a number of days from today (it lands on the next day that person works)
|
|
11
|
-
// with = the other agendas the
|
|
11
|
+
// with = the other agendas the appointment also takes (the room it uses, a second worker): checked and shown in each
|
|
12
12
|
// status = awaiting_payment | confirmed | started | done | noshow | cancelled · tint/ink = the service's colours
|
|
13
13
|
// blocks [{id, resource | "all", day, start, dur, reason}] · from / to (hours) · slot (minutes) · view: day | week | list
|
|
14
14
|
// free [{resource, day, start, end}] - the server's free hours; when sent they are the only free hours shown
|
|
@@ -22,19 +22,19 @@
|
|
|
22
22
|
// agenda runs a command when its value changes, so repeat one with a suffix after # ("new#1", "new#2"…)
|
|
23
23
|
// hour12 "yes" - times written 3:00 PM (the data stays "HH:MM") · timeZone "America/Bogota" - today and now are the shop's
|
|
24
24
|
// fit "yes" - the hours share the height the app gives the agenda (give it one: a flex parent), never under 56px
|
|
25
|
-
// phoneStrip "dots" - the phone's week strip marks each day with a dot per
|
|
25
|
+
// phoneStrip "dots" - the phone's week strip marks each day with a dot per appointment in its service's colour (default: the count)
|
|
26
26
|
// phoneView "list" - a phone opens on the list until a view is picked · blockScope "all" - blocks are for the whole
|
|
27
|
-
// business (no
|
|
28
|
-
// events also take tags ["
|
|
29
|
-
// resources also take role (read under the name), hideEmpty (the column shows only on days it has
|
|
30
|
-
// bookable false (an agenda that only holds
|
|
31
|
-
// resize "off" - a
|
|
32
|
-
// dragged
|
|
33
|
-
// placing [{resource, day, start, dur, title, tint}] - while the app's
|
|
34
|
-
// create "sheet" - a showcase:
|
|
27
|
+
// business (no “Para” picker: a backend with no per-person blocks)
|
|
28
|
+
// events also take tags ["At home", "Arrived 10:02"] (words next to the time) and photo (the client's face, in the list)
|
|
29
|
+
// resources also take role (read under the name), hideEmpty (the column shows only on days it has appointments) and
|
|
30
|
+
// bookable false (an agenda that only holds appointments, “Unassigned”: never offered as free)
|
|
31
|
+
// resize "off" - a appointment's length is not dragged (a backend that only moves day and time) · moveAcross "off" - a
|
|
32
|
+
// dragged appointment stays in its own agenda (only its time, and its day in the week view, change)
|
|
33
|
+
// placing [{resource, day, start, dur, title, tint}] - while the app's “new appointment” sheet is open, where it will land
|
|
34
|
+
// create "sheet" - a showcase: “new” and a tap on a free hour open a short built-in form and the appointment lands at once
|
|
35
35
|
// handlers: create(json {resource, day, start}) · open(json event) · block(json {resource, day, start, dur, reason})
|
|
36
36
|
// move(json {id, resource, day, start, dur}) · range(json {from, to, view}: view is day | week | list | range) · change(json [...]) · action(id)
|
|
37
|
-
// ranges "yes" - the month under the title also offers
|
|
37
|
+
// ranges "yes" - the month under the title also offers “Several days” (a range listed day by day); off by default
|
|
38
38
|
// The title opens a month (floating; a sheet on a phone) to jump to a day.
|
|
39
39
|
// LIVE: new inputs repaint the same agenda (same day, view and person): an app sends its data again and it shows;
|
|
40
40
|
// lang, currency, locale and cents change live too.
|
|
@@ -45,7 +45,7 @@ export function mount(el, inputs, on) {
|
|
|
45
45
|
const SKIP = new Set(['command', 'start']);
|
|
46
46
|
const sigOf = (i) => JSON.stringify(Object.keys(i).filter((k) => !SKIP.has(k)).sort().map((k) => [k, i[k]]));
|
|
47
47
|
let lastSig = sigOf(inputs);
|
|
48
|
-
import('@muten/shadcn/registry/kit/core.js').then(({ kit, today, addDays }) => {
|
|
48
|
+
import('@muten/shadcn/registry/kit/core.js').then(({ kit, today, addDays, langOf }) => {
|
|
49
49
|
const iso = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
50
50
|
const list = (v) => (Array.isArray(v) ? v : []);
|
|
51
51
|
const at = (s) => new Date(String(s) + 'T00:00:00');
|
|
@@ -65,7 +65,9 @@ export function mount(el, inputs, on) {
|
|
|
65
65
|
queue: list(i.queue).map((q) => ({ ...q, day: plain(q.day) })), closed: list(i.closed).map(plain), loading: yes(i.loading),
|
|
66
66
|
placing: (() => { const g = list(i.placing)[0]; return g && g.start ? { ...g, day: dayOf(g.resource, g.day) } : null; })(),
|
|
67
67
|
from: Number(i.from) || 9, to: Number(i.to) || 19,
|
|
68
|
-
|
|
68
|
+
// the language passed (Storio passes it), else the page's (the nearest lang), English by default
|
|
69
|
+
lang: i.lang === 'en' || i.lang === 'es' ? i.lang : (langOf(el) === 'es' ? 'es' : 'en'), overlap: ['never', 'allow'].includes(i.overlap) ? i.overlap : 'auto', currency: i.currency || '', locale: i.locale || '', cents: yes(i.cents),
|
|
70
|
+
agentMark: i.agentMark || '', agentLabel: i.agentLabel || '',
|
|
69
71
|
};
|
|
70
72
|
};
|
|
71
73
|
map = data;
|
|
@@ -88,7 +90,9 @@ export function mount(el, inputs, on) {
|
|
|
88
90
|
onChange: (evs) => send('change', evs.map((e) => ({ id: e.id, resource: e.resource, day: iso(e.day), start: e.start, dur: e.dur, status: e.status || '' }))),
|
|
89
91
|
});
|
|
90
92
|
if (pending) { const sig = sigOf(pending); if (sig !== lastSig) { lastSig = sig; api.update(data(pending)); } pending = null; }
|
|
93
|
+
// with no lang of its own it follows the page's: switched live when a lang changes
|
|
94
|
+
document.addEventListener('kit:lang', () => { if (el.isConnected && inputs.lang !== 'en' && inputs.lang !== 'es') api.update({ lang: langOf(el) === 'es' ? 'es' : 'en' }); });
|
|
91
95
|
});
|
|
92
96
|
const command = (next) => { const c = String(next.command || ''); if (c === lastCmd) return; lastCmd = c; if (c) api.run(c.split('#')[0]); };
|
|
93
|
-
return (next) => { if (!api || !map) { pending = next; return; } const sig = sigOf(next); if (sig !== lastSig) { lastSig = sig; api.update(map(next)); } command(next); };
|
|
97
|
+
return (next) => { inputs = next; if (!api || !map) { pending = next; return; } const sig = sigOf(next); if (sig !== lastSig) { lastSig = sig; api.update(map(next)); } command(next); };
|
|
94
98
|
}
|
package/registry/agenda.muten
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
# appointment to move it (the ghost turns red where it is not free), drag its edge to resize, tap it for its sheet:
|
|
3
3
|
# lifecycle, payment and receipt, write, change the time, cancel with a reason. Block time; create from a free slot.
|
|
4
4
|
# Everything is data (see agenda.js for the shapes); pass lists with @: Agenda(resources: @team, hours: @hours, events: @appointments, blocks: @blocks, onCreate: newAppointment, onChange: saveAgenda)
|
|
5
|
-
# actions: the toolbar's buttons as data, [{id, label, icon, primary}] (default: Bloquear and Nueva
|
|
5
|
+
# actions: the toolbar's buttons as data, [{id, label, icon, primary}] (default: Bloquear and Nueva appointment); an id other
|
|
6
6
|
# than block or new fires onAction with it: Agenda(…, actions: @agendaActions, onAction: agendaAction)
|
|
7
7
|
# view: how it opens, day (default) | week | list.
|
|
8
8
|
# shadcn-for-muten · Custom (kit core) · host JS is agenda.js.
|
|
9
9
|
part Agenda(resources: list, hours: list, events: list, blocks: list, actions: list, from: number = 9, to: number = 19, slot: number = 30, view: text = "day", onCreate: action, onChange: action, onAction: action) {
|
|
10
10
|
Custom Agenda inputs(resources: $resources, hours: $hours, events: $events, blocks: $blocks, actions: $actions, from: $from, to: $to, slot: $slot, view: $view) on(create: $onCreate, change: $onChange, action: $onAction) class("cx-agenda")
|
|
11
11
|
}
|
|
12
|
-
# AgendaDemo - the same agenda for a showcase with no app behind it:
|
|
12
|
+
# AgendaDemo - the same agenda for a showcase with no app behind it: “New appointment” and a tap on a free hour open a short
|
|
13
13
|
# built-in form (the service from `services` [{name, dur}], the client, the time) and the appointment lands on the
|
|
14
14
|
# agenda at once, with its notice. Block, move and the appointment sheet work as in Agenda. Nothing is saved anywhere.
|
|
15
15
|
part AgendaDemo(resources: list, hours: list, events: list, blocks: list, actions: list, services: list, from: number = 9, to: number = 19, slot: number = 30, view: text = "day", onChange: action, onAction: action) {
|
|
@@ -21,22 +21,24 @@ part AgendaDemo(resources: list, hours: list, events: list, blocks: list, action
|
|
|
21
21
|
# queue who waits for a free hour [{day, name, photo}] · closed days the shop is shut ["yyyy-mm-dd"]
|
|
22
22
|
# events carry status (awaiting_payment | confirmed | started | done | noshow | cancelled) and tint/ink (the service's colours)
|
|
23
23
|
# lang es | en · currency + locale (prices written with Intl) · cents "yes" · drag "off" · loading "yes" · start "yyyy-mm-dd"
|
|
24
|
-
# onOpen(json {id, …}) a
|
|
24
|
+
# onOpen(json {id, …}) a appointment tapped (open your sheet) · onCreate(json {resource, day, start}) a free hour tapped
|
|
25
25
|
# onBlock(json {resource, day, start, dur, reason}) · onMove(json {id, resource, day, start, dur}) dragged (answer with
|
|
26
26
|
# the data: it stays where it landed, or goes back if you send it unchanged) · onRange(json {from, to, view}) days shown
|
|
27
27
|
# fit "yes" the hours stretch to the height the app gives the agenda (a flex parent that grows), never under 56px
|
|
28
|
-
# ranges "yes" the date under the title also offers
|
|
29
|
-
# actions [{id, label, icon, primary}] the toolbar's buttons (default: Bloquear and Nueva
|
|
30
|
-
# leaves
|
|
31
|
-
# phone "nav": on a phone its actions (Bloquear, Nueva
|
|
28
|
+
# ranges "yes" the date under the title also offers “Several days” (a range, listed day by day); off by default
|
|
29
|
+
# actions [{id, label, icon, primary}] the toolbar's buttons (default: Bloquear and Nueva appointment; [{id: "block", …}] alone
|
|
30
|
+
# leaves “New appointment” to the app's own button) · an id other than block or new fires onAction(id)
|
|
31
|
+
# phone "nav": on a phone its actions (Bloquear, Nueva appointment, yours) and the arrows leave the toolbar for the app's
|
|
32
32
|
# phone bar (PhoneBar), which sends them back through `command` ("new", "block", "prev", "next", "today", "view:list",
|
|
33
33
|
# an action id; repeat one with a suffix after #: "new#2"). Nothing runs while loading.
|
|
34
34
|
# several agendas: resources carry kind (business | person | a room…) and capacity (1 never overlaps, 0 no limit);
|
|
35
|
-
# a
|
|
36
|
-
# resize "off" · moveAcross "off" (a backend that only moves day and time) · hour12 "yes" (3:00 PM) · timeZone (the shop's today and now) · phoneView "list" · phoneStrip "dots" (the phone's week: a dot per
|
|
37
|
-
# (the dashed
|
|
35
|
+
# a appointment takes its resource plus every id in `with`. overlap "never" | "allow" overrides all capacities.
|
|
36
|
+
# resize "off" · moveAcross "off" (a backend that only moves day and time) · hour12 "yes" (3:00 PM) · timeZone (the shop's today and now) · phoneView "list" · phoneStrip "dots" (the phone's week: a dot per appointment in its colour) · blockScope "all" · placing [{…}]
|
|
37
|
+
# (the dashed appointment while the app's new-cita sheet is open) · events take tags [words] and photo; resources role, hideEmpty
|
|
38
|
+
# events take by "agent" (made by the app's assistant): agentMark (an image url) goes before their name, and agentLabel
|
|
39
|
+
# ("Booked by your assistant") under it on the phone's list and in what a screen reader says
|
|
38
40
|
# AgendaLive(resources: @team, hours: @hours, events: @citas, blocks: @blocks, free: @free, queue: @queue, closed: @closed,
|
|
39
41
|
# lang: ui.lang, currency: "USD", onOpen: openCita, onCreate: newCita, onBlock: block, onMove: moveCita, onRange: loadDays)
|
|
40
|
-
part AgendaLive(resources: list, hours: list, events: list, blocks: list, actions: list, free: list, queue: list, closed: list, lang: text = "es", currency: text = "", locale: text = "", cents: text = "", from: number = 9, to: number = 19, slot: number = 30, view: text = "day", loading: text = "", start: text = "", drag: text = "", phone: text = "", command: text = "", overlap: text = "", hour12: text = "", timeZone: text = "", phoneView: text = "", phoneStrip: text = "", fit: text = "", ranges: text = "", blockScope: text = "", resize: text = "", moveAcross: text = "", placing: list, onOpen: action, onCreate: action, onBlock: action, onMove: action, onRange: action, onAction: action) {
|
|
41
|
-
Custom Agenda inputs(resources: $resources, hours: $hours, events: $events, blocks: $blocks, actions: $actions, free: $free, queue: $queue, closed: $closed, lang: $lang, currency: $currency, locale: $locale, cents: $cents, from: $from, to: $to, slot: $slot, view: $view, loading: $loading, start: $start, drag: $drag, phone: $phone, command: $command, overlap: $overlap, hour12: $hour12, timeZone: $timeZone, phoneView: $phoneView, phoneStrip: $phoneStrip, fit: $fit, ranges: $ranges, blockScope: $blockScope, resize: $resize, moveAcross: $moveAcross, placing: $placing, sheet: "app") on(open: $onOpen, create: $onCreate, block: $onBlock, move: $onMove, range: $onRange, action: $onAction) class("cx-agenda")
|
|
42
|
+
part AgendaLive(resources: list, hours: list, events: list, blocks: list, actions: list, free: list, queue: list, closed: list, lang: text = "es", currency: text = "", locale: text = "", cents: text = "", from: number = 9, to: number = 19, slot: number = 30, view: text = "day", loading: text = "", start: text = "", drag: text = "", phone: text = "", command: text = "", overlap: text = "", hour12: text = "", agentMark: text = "", agentLabel: text = "", timeZone: text = "", phoneView: text = "", phoneStrip: text = "", fit: text = "", ranges: text = "", blockScope: text = "", resize: text = "", moveAcross: text = "", placing: list, onOpen: action, onCreate: action, onBlock: action, onMove: action, onRange: action, onAction: action) {
|
|
43
|
+
Custom Agenda inputs(resources: $resources, hours: $hours, events: $events, blocks: $blocks, actions: $actions, free: $free, queue: $queue, closed: $closed, lang: $lang, currency: $currency, locale: $locale, cents: $cents, from: $from, to: $to, slot: $slot, view: $view, loading: $loading, start: $start, drag: $drag, phone: $phone, command: $command, overlap: $overlap, hour12: $hour12, agentMark: $agentMark, agentLabel: $agentLabel, timeZone: $timeZone, phoneView: $phoneView, phoneStrip: $phoneStrip, fit: $fit, ranges: $ranges, blockScope: $blockScope, resize: $resize, moveAcross: $moveAcross, placing: $placing, sheet: "app") on(open: $onOpen, create: $onCreate, block: $onBlock, move: $onMove, range: $onRange, action: $onAction) class("cx-agenda")
|
|
42
44
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# AlertDialog - asks before something that cannot be undone (delete, cancel and refund). It cannot be dismissed by
|
|
2
2
|
# tapping outside: the person answers with one of its two buttons (Esc counts as the safe one). Put them in a
|
|
3
3
|
# ModalFooter: the safe way out (cx-exit) and the action (cx-go, destructive when it destroys).
|
|
4
|
-
# AlertDialog(open: asking, onClose: keep, title: "
|
|
5
|
-
# ModalFooter { Btn(variant: "outline", onClick: keep) class("cx-exit") { Span "
|
|
4
|
+
# AlertDialog(open: asking, onClose: keep, title: "Delete the service?", description: "It disappears from your shop…") {
|
|
5
|
+
# ModalFooter { Btn(variant: "outline", onClick: keep) class("cx-exit") { Span "Cancel" } Btn(variant: "destructive", onClick: remove) class("cx-go") { Span "Delete" } }
|
|
6
6
|
# }
|
|
7
7
|
# shadcn-for-muten · part over Modal (role alertdialog, dismiss off).
|
|
8
8
|
part AlertDialog(open: bool, onClose: action, title: text, description: text = "") {
|
package/registry/alert.muten
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Alert - a callout: an icon, a title, a description, an optional action at the corner. The icon carries the tone.
|
|
2
|
-
# Alert(tone: "wait") { Icon "lucide:clock" AlertTitle(label: "
|
|
2
|
+
# Alert(tone: "wait") { Icon "lucide:clock" AlertTitle(label: "Your plan expires in 3 days") AlertDescription(label: "…") AlertAction { Btn(size: "xs", variant: "outline", onClick: renew) { Span "Renew" } } }
|
|
3
3
|
# variant: default | destructive · tone: "" | info | ok | wait
|
|
4
4
|
# shadcn-for-muten · part · no deps · Nova (.cn-alert) + kit.css (tone).
|
|
5
5
|
part Alert(variant: text = "default", tone: text = "") { Section class("cn-alert cn-alert-variant-{$variant} group/alert") data(slot: "alert", tone: $tone) aria(role: "status") { slot } }
|
package/registry/app-shell.muten
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# the person at the bottom. It folds to icons (the name shows on hover) and, in a narrow box, becomes a menu that
|
|
3
3
|
# slides from the left. The page header has breadcrumbs, title, description and actions. Tapping a row opens the
|
|
4
4
|
# detail Sheet on the right without leaving the list. Ctrl/⌘ K (or the search button) opens the Command.
|
|
5
|
-
# AppShell(brand: "
|
|
5
|
+
# AppShell(brand: "El Roble Barbershop", short: "El Roble", plan: "Plan Pro", user: "Julián Restrepo", email: "julian@elroble.co", page: "Orders", nav: @nav, pages: @pages, rows: @rows, commands: @commands, width: width, onNavigate: go, onOpen: openRow, onAction: act)
|
|
6
6
|
# nav {group, icon, label, count} · pages {name, title, description, actions [{label, variant, icon}]} · rows {page, id,
|
|
7
7
|
# title, description, badge, tone, amount, command, sheetTitle, sheetDescription, sheet [{icon, title, description,
|
|
8
8
|
# badge, tone, amount}], timeline [{label, when}], primary, secondary} · commands {icon, label, page}.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Attachment - a file in a message or a form: its kind as an icon, the name (cut with
|
|
1
|
+
# Attachment - a file in a message or a form: its kind as an icon, the name (cut with “…” when long) and the size;
|
|
2
2
|
# tapping it opens the file. kind: file | pdf | image | audio | sheet.
|
|
3
3
|
# Attachment(name: "comprobante-1042.pdf", size: "182 KB", kind: "pdf", to: "/…")
|
|
4
4
|
# shadcn-for-muten · part · no deps · kit.css (.cx-attach).
|
package/registry/avatar.muten
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# Avatar - three sizes (24, 32, 40). Without a photo the initials take a stable tone from the name (hue), mixed with
|
|
2
2
|
# the skin's background so they never clash with the brand. The name shows on hover and on focus. A status dot
|
|
3
|
-
# (online), a stacked group with
|
|
3
|
+
# (online), a stacked group with “+N”, and AvatarImage for a photo.
|
|
4
4
|
# Avatar(name: "Valentina Ruiz", size: "lg") Avatar(name: "Laura Méndez", size: "lg", status: "online")
|
|
5
|
-
# AvatarGroup { Avatar(name: "Laura Méndez") Avatar(name: "Diego Paredes") AvatarMore(count: "+3", label: "3
|
|
5
|
+
# AvatarGroup { Avatar(name: "Laura Méndez") Avatar(name: "Diego Paredes") AvatarMore(count: "+3", label: "3 more people") }
|
|
6
6
|
# Avatar(name: "El Roble", shape: "square") - a thing, not a person
|
|
7
7
|
# size: sm | default | lg · status: "" | online · shadcn-for-muten · part · no deps · Nova + kit.css (.cx-av).
|
|
8
8
|
part Avatar(name: text, size: text = "default", shape: text = "", status: text = "") {
|
|
9
9
|
Stack class("cx-av-wrap") data(name: $name) aria(label: $name) {
|
|
10
10
|
Stack class("cn-avatar group/avatar cx-av") data(slot: "avatar", size: $size, shape: $shape) style(h: "{hue($name)}") {
|
|
11
11
|
Span "{initials($name)}" class("cn-avatar-fallback") data(slot: "avatar-fallback")
|
|
12
|
-
when $status == "online" { Span "" class("cn-avatar-badge cx-online") data(slot: "avatar-badge") aria(label: "
|
|
12
|
+
when $status == "online" { Span "" class("cn-avatar-badge cx-online") data(slot: "avatar-badge", trl: "Online") aria(label: "Online") }
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
}
|
|
@@ -22,5 +22,5 @@ part AvatarImage(src: text, name: text, size: text = "default", shape: text = ""
|
|
|
22
22
|
}
|
|
23
23
|
part AvatarGroup() { Stack class("cn-avatar-group group/avatar-group") data(slot: "avatar-group") { slot } }
|
|
24
24
|
part AvatarMore(count: text, label: text = "") { Span "{$count}" class("cn-avatar-group-count") data(slot: "avatar-group-count", name: $label) }
|
|
25
|
-
# Who - a person with what they are doing now: Who(title: "Laura Méndez", subtitle: "
|
|
25
|
+
# Who - a person with what they are doing now: Who(title: "Laura Méndez", subtitle: "Working today · 4 appointments") { Avatar(…) }
|
|
26
26
|
part Who(title: text, subtitle: text = "") { Stack class("cx-who") { slot Stack class("cx-who-t") { Span "{$title}" class("cx-who-n") Span "{$subtitle}" class("cx-who-s") } } }
|
package/registry/badge.muten
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
# Badge - a short label. `StatusBadge` is the system's state: a dot and the word, never a tinted pill.
|
|
2
|
-
# Badge(label: "
|
|
3
|
-
# StatusBadge(label: "
|
|
2
|
+
# Badge(label: "New") Badge(label: "PRO", variant: "default")
|
|
3
|
+
# StatusBadge(label: "Confirmed", tone: "ok") StatusBadge(label: "To review", tone: "wait")
|
|
4
4
|
# Badge variant: default | secondary | destructive | outline | ghost | link (default: secondary)
|
|
5
5
|
# StatusBadge tone: ok | wait | info | bad | "" (neutral)
|
|
6
6
|
# shadcn-for-muten · part · no deps · Nova (.cn-badge) + kit.css (status).
|
|
7
7
|
part Badge(label: text, variant: text = "secondary") { Span "{$label}" class("cn-badge cn-badge-variant-{$variant} group/badge") data(slot: "badge") }
|
|
8
|
+
# IconBadge - a Badge with an Icon or a Spinner inside (its children); end: true puts it after the word.
|
|
9
|
+
# IconBadge(label: "Verified") { Icon "lucide:badge-check" } IconBadge(label: "Saving", variant: "outline") { Spinner }
|
|
10
|
+
part IconBadge(label: text, variant: text = "secondary", end: bool = false) {
|
|
11
|
+
Stack class("cn-badge cn-badge-variant-{$variant} group/badge cx-badge-icon") class("cx-badge-end" when $end) data(slot: "badge") { slot Span "{$label}" }
|
|
12
|
+
}
|
|
13
|
+
# LinkBadge - a badge that goes somewhere, with the arrow at its end.
|
|
14
|
+
# LinkBadge(label: "See plan", to: "/billing")
|
|
15
|
+
part LinkBadge(label: text, to: text, variant: text = "secondary") {
|
|
16
|
+
Link -> "{$to}" class("cn-badge cn-badge-variant-{$variant} group/badge cx-badge-icon cx-badge-end cx-badge-link") data(slot: "badge") { Icon "lucide:arrow-up-right" Span "{$label}" }
|
|
17
|
+
}
|
|
8
18
|
part StatusBadge(label: text, tone: text = "") { Span "{$label}" class("cn-badge cn-badge-variant-ghost group/badge") data(slot: "badge", variant: "status", tone: $tone) }
|
|
9
19
|
# ToneBadge - a state with its tone, tinted, with or without a dot: new | ok | wait | bad | pro (PRO uses --selected).
|
|
10
|
-
#
|
|
20
|
+
# The word is the tone mixed toward the ink, so it reads on its tint in light and dark.
|
|
21
|
+
# ToneBadge(label: "Confirmed", tone: "ok") ToneBadge(label: "Pending", tone: "wait", dot: "dot")
|
|
11
22
|
part ToneBadge(label: text, tone: text, dot: text = "") {
|
|
12
23
|
Stack class("cn-badge group/badge cx-tone-{$tone}") data(slot: "badge") {
|
|
13
24
|
when $dot != "" { Span "" class("cx-dot") }
|
package/registry/band.muten
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Band - the strip at the top for a state that lasts (no connection, the plan is about to end). One at a time: a new
|
|
2
2
|
# one replaces the old. It folds open and closed; the text fades first so nothing is clipped. The page owns open.
|
|
3
|
-
# Band(open: offline, tone: "muted", text: "
|
|
4
|
-
# Band(open: planEnding, tone: "warn", text: "
|
|
3
|
+
# Band(open: offline, tone: "muted", text: "Offline. We'll save your changes when you're back.") { Icon "lucide:info" }
|
|
4
|
+
# Band(open: planEnding, tone: "warn", text: "Your Pro plan expires in 3 days.", action: "Renew", onAction: renew) { Icon "lucide:clock" }
|
|
5
5
|
# tone: muted | warn. shadcn-for-muten · part · no deps · kit.css (.cx-band).
|
|
6
6
|
part Band(open: bool, tone: text = "muted", text: text, action: text = "", onAction: action) {
|
|
7
7
|
Stack class("cx-band-slot") data(open: $open) {
|
package/registry/bar-chart.muten
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# BarChart - bars that always fill their height, a period switch (14
|
|
1
|
+
# BarChart - bars that always fill their height, a period switch (14 days / 30 days). periods: {label, values} - the
|
|
2
2
|
# labels are the last N days up to today. money "on" formats as money.
|
|
3
|
-
# BarChart(title: "
|
|
3
|
+
# BarChart(title: "Sales", periods: @sales, money: "on")
|
|
4
4
|
# shadcn-for-muten · part over KitWidget (kit core).
|
|
5
5
|
part BarChart(title: text, periods: list, money: text = "") {
|
|
6
6
|
Custom KitWidget inputs(name: "barChart", title: $title, periods: $periods, money: $money) class("cx-chart-card")
|
package/registry/bio-play.muten
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# BioPlay - a social profile shown, not used: the photo, the handle, the followers, the bio with the store's link
|
|
2
2
|
# and the two buttons, as Instagram draws a profile. For a landing; a picture (aria-hidden), `label` says what.
|
|
3
|
-
# BioPlay(photo: "/owner.webp", handle: "brumabarberia", followers: "1
|
|
4
|
-
# link: "storio.ink/bruma-barberia", follow: "
|
|
3
|
+
# BioPlay(photo: "/owner.webp", handle: "brumabarberia", followers: "1,284 followers", bio: "Cortes y barba.",
|
|
4
|
+
# link: "storio.ink/bruma-barberia", follow: "Follow", message: "Message", label: "…")
|
|
5
5
|
# shadcn-for-muten · part · no deps · kit.css (.cx-bio).
|
|
6
6
|
part BioPlay(photo: text, handle: text, followers: text, bio: text, link: text, follow: text, message: text, label: text = "") {
|
|
7
7
|
Stack class("cx-bio cx-play") aria(role: "img", label: $label) {
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# BookingPlay - a booking shown, not used: one service, a few days and hours, and a booking that plays itself in a
|
|
2
|
-
# loop (a day is picked, then an hour,
|
|
2
|
+
# loop (a day is picked, then an hour, “Book” is pressed and the confirmation comes up with its pass). For a
|
|
3
3
|
# landing or a bento cell, where the real Booking would be a form nobody fills. Taken hours are struck through.
|
|
4
|
-
# BookingPlay(service: "
|
|
5
|
-
# doneTitle: "
|
|
6
|
-
# BookingPlayDays { BookingPlayDay(weekday: "
|
|
4
|
+
# BookingPlay(service: "Classic cut", place: "Bruma Barbershop · 30 min", price: "USD 15", cta: "Book",
|
|
5
|
+
# doneTitle: "Done, Carla!", doneText: "Classic cut · today · 4:30 PM", code: "STO-4F2K", label: "This is how your customer books") {
|
|
6
|
+
# BookingPlayDays { BookingPlayDay(weekday: "mon", day: "5", state: "off") … BookingPlayDay(weekday: "today", day: "9", state: "pick") }
|
|
7
7
|
# BookingPlayHours { BookingPlayHour(time: "10:00", state: "taken") … BookingPlayHour(time: "4:30", state: "pick", picked: "4:30 PM") }
|
|
8
8
|
# }
|
|
9
9
|
# photo: the owner's face in the circle (empty = the service's initial).
|
|
10
10
|
# state: "" | off (closed day) | taken (hour) | pick (the one the loop chooses; one day and one hour).
|
|
11
11
|
# Reduced motion: the pick is shown, the confirmation is not. shadcn-for-muten · part · no deps · kit.css (.cx-bkp).
|
|
12
|
-
part BookingPlay(service: text, photo: text = "", place: text = "", price: text = "", cta: text = "
|
|
12
|
+
part BookingPlay(service: text, photo: text = "", place: text = "", price: text = "", cta: text = "Book", doneTitle: text = "", doneText: text = "", code: text = "", label: text = "") {
|
|
13
13
|
Stack class("cx-bkp cx-play") aria(role: "img", label: $label) {
|
|
14
14
|
Stack class("cx-bkp-who") aria(hidden: true) {
|
|
15
15
|
when $photo != "" { Image "{$photo}" alt("") class("cx-bkp-av cx-play-face") }
|
package/registry/booking.muten
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
# Booking(week: @week, taken: @taken, wait: 380, onSelect: book)
|
|
5
5
|
# week {days [0..6], hours ["09:00", …]} = what the shop offers each weekday (a weekday in no row is closed);
|
|
6
6
|
# taken {day (days from today or "yyyy-mm-dd"), hours} = what is booked (no hours = the whole day). onSelect gets
|
|
7
|
-
# "yyyy-mm-dd HH:MM" on
|
|
7
|
+
# "yyyy-mm-dd HH:MM" on “Continue”.
|
|
8
8
|
# demo "on": it plays itself for a landing (a day, then an hour, again and again) until someone touches it; never
|
|
9
9
|
# confirms, and stays still under reduced motion.
|
|
10
10
|
# shadcn-for-muten · Custom (kit/structure.js) · kit.css (.cx-book, .cx-week, .cx-hours).
|
|
11
|
-
part Booking(week: list, taken: list, wait: number = 0, cta: text = "
|
|
11
|
+
part Booking(week: list, taken: list, wait: number = 0, cta: text = "Continue", demo: text = "", onSelect: action) {
|
|
12
12
|
Custom Booking inputs(week: $week, taken: $taken, wait: $wait, cta: $cta, demo: $demo) on(select: $onSelect) class("cx-book-host")
|
|
13
13
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Breadcrumb - where this page sits: the trail of pages above it, the current one last (not a link). A long trail
|
|
2
|
-
# folds its middle into
|
|
3
|
-
# Breadcrumb { BreadcrumbLink(label: "El Roble", to: "/") BreadcrumbSeparator() BreadcrumbLink(label: "
|
|
2
|
+
# folds its middle into “…” (BreadcrumbEllipsis).
|
|
3
|
+
# Breadcrumb { BreadcrumbLink(label: "El Roble", to: "/") BreadcrumbSeparator() BreadcrumbLink(label: "Orders", to: "/orders") BreadcrumbSeparator() BreadcrumbPage(label: "#1042") }
|
|
4
4
|
# shadcn-for-muten · parts · no deps · Nova (.cn-breadcrumb-*).
|
|
5
|
-
part Breadcrumb(label: text = "
|
|
5
|
+
part Breadcrumb(label: text = "Breadcrumb") { Nav class("cx-breadcrumb") data(slot: "breadcrumb", trl: $label) aria(label: $label) { Stack class("cn-breadcrumb-list cx-bc-list") data(slot: "breadcrumb-list") aria(role: "list") { slot } } }
|
|
6
6
|
part BreadcrumbLink(label: text, to: text) { Link "{$label}" -> "{$to}" class("cn-breadcrumb-link cx-bc-item") data(slot: "breadcrumb-link") }
|
|
7
7
|
part BreadcrumbPage(label: text) { Span "{$label}" class("cn-breadcrumb-page cx-bc-item") data(slot: "breadcrumb-page") aria(current: "page") }
|
|
8
8
|
part BreadcrumbSeparator() { Stack class("cn-breadcrumb-separator cx-bc-sep") data(slot: "breadcrumb-separator") aria(hidden: "true") { Icon "lucide:chevron-right" } }
|
|
9
|
-
part BreadcrumbEllipsis(label: text = "
|
|
9
|
+
part BreadcrumbEllipsis(label: text = "More pages") { Stack class("cn-breadcrumb-ellipsis cx-bc-more") data(slot: "breadcrumb-ellipsis", name: $label, trl: $label, trn: $label) aria(label: $label) { Icon "lucide:ellipsis" } }
|
package/registry/button.muten
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Button (part name Btn - `Button` is a muten primitive, so a part can't reuse that name) - shadcn's Nova button.
|
|
2
|
-
# Put the label (and an icon) in the slot
|
|
3
|
-
# Btn(onClick: save) { Span "
|
|
4
|
-
# Btn(variant: "outline", size: "sm", onClick: cancel) { Icon "lucide:x" Span "
|
|
2
|
+
# Put the label (and an icon) in the slot. Pressed, it drops 1px (as shadcn); icons stay still.
|
|
3
|
+
# Btn(onClick: save) { Span "Save" }
|
|
4
|
+
# Btn(variant: "outline", size: "sm", onClick: cancel) { Icon "lucide:x" Span "Cancel" }
|
|
5
5
|
# Btn(size: "icon-sm", variant: "ghost", tone: "wait", onClick: due) { Icon "lucide:clock" Indicator(tone: "wait") }
|
|
6
|
-
# Btn(onClick: go) class("cx-fill") { Span "
|
|
6
|
+
# Btn(onClick: go) class("cx-fill") { Span "Mark arrival" Icon "lucide:chevron-right" } - full width (a card's primary)
|
|
7
7
|
# variant: default | outline | secondary | ghost | destructive | link (default: default)
|
|
8
8
|
# size: xs (24) | sm (28) | default (32) | lg (36) | xl (44) | 2xl (52) | icon | icon-xs | icon-sm | icon-lg | icon-xl | icon-2xl
|
|
9
9
|
# xl and 2xl are for big surfaces (a landing, a sign-in) and match the fields' xl / 2xl.
|
|
@@ -11,13 +11,15 @@
|
|
|
11
11
|
# disabled: a bool (a state or a getter: a part argument can't be an expression) - the button can't be pressed.
|
|
12
12
|
# label: the name of an icon-only button (aria-label, and the kit's tooltip on hover). Leave it empty when there is text.
|
|
13
13
|
# shape: "" | pill - fully rounded ends, for the one call to action at the foot of a flow (Btn(size: "2xl", shape: "pill")).
|
|
14
|
+
# loading: a bool - a Spinner before the label, and the button can't be pressed meanwhile (aria-busy).
|
|
15
|
+
# Btn(loading: saving, onClick: save) { Span "Save" }
|
|
14
16
|
# shadcn-for-muten · part · no deps · Nova classes (nova.css) + kit.css.
|
|
15
|
-
part Btn(variant: text = "default", size: text = "default", tone: text = "", label: text = "", onClick: action, disabled: bool = false, shape: text = "") {
|
|
16
|
-
Button -> $onClick class("cn-button cn-button-variant-{$variant} cn-button-size-{$size} group/button", "cx-tonal cx-has-indicator" when $tone != "") data(slot: "button", tone: $tone, name: $label, shape: $shape) aria(label: $label) disabled when $disabled { slot }
|
|
17
|
+
part Btn(variant: text = "default", size: text = "default", tone: text = "", label: text = "", onClick: action, disabled: bool = false, shape: text = "", loading: bool = false) {
|
|
18
|
+
Button -> $onClick class("cn-button cn-button-variant-{$variant} cn-button-size-{$size} group/button", "cx-tonal cx-has-indicator" when $tone != "") data(slot: "button", tone: $tone, name: $label, shape: $shape, loading: $loading, trl: $label, trn: $label) aria(label: $label, busy: $loading) disabled when $disabled or $loading { slot when $loading { Spinner() } }
|
|
17
19
|
}
|
|
18
20
|
# TriggerBtn - a Btn that only opens what sits next to it (a Popover, a DropdownMenu, a SplitButton's menu): it has no
|
|
19
21
|
# action of its own, so opening never runs something else. Same variants and sizes as Btn.
|
|
20
|
-
# DropdownMenu { DropdownTrigger { TriggerBtn { Span "
|
|
22
|
+
# DropdownMenu { DropdownTrigger { TriggerBtn { Span "Options" Icon "lucide:chevron-down" } } DropdownContent { … } }
|
|
21
23
|
part TriggerBtn(variant: text = "outline", size: text = "default", label: text = "") {
|
|
22
24
|
Button class("cn-button cn-button-variant-{$variant} cn-button-size-{$size} group/button") data(slot: "button", name: $label) aria(label: $label) { slot }
|
|
23
25
|
}
|
package/registry/calendar.js
CHANGED
|
@@ -1,29 +1,34 @@
|
|
|
1
1
|
// Calendar (muten Custom) - the kit's month grid (one or two months, a day or a range), from kit/core.js.
|
|
2
2
|
// inputs: selected "yyyy-mm-dd" · "from/to" · or relative to today: "today", "0", "-6/0" (days from today)
|
|
3
3
|
// mode "single" | "range" · months 1 | 2 · min "today" | "yyyy-mm-dd"
|
|
4
|
-
// presets "reports" →
|
|
5
|
-
// caption "on" → the sentence under it (
|
|
6
|
-
// handlers: select("yyyy-mm-dd" | "from/to"). Same component as the reference artifact's Calendar.
|
|
4
|
+
// presets "reports" → Today · Last 7 days · This month · Last month beside a range
|
|
5
|
+
// caption "on" → the sentence under it (“You picked Thursday, October 8” / “From Oct 2 to Oct 8 · 7 days”)
|
|
6
|
+
// lang (optional) es | en, else the nearest lang. handlers: select("yyyy-mm-dd" | "from/to"). Same component as the reference artifact's Calendar.
|
|
7
7
|
export function mount(el, inputs, on) {
|
|
8
8
|
let cal = null;
|
|
9
|
-
import('@muten/shadcn/registry/kit/core.js').then((
|
|
9
|
+
import('@muten/shadcn/registry/kit/core.js').then((core) => core.relang(el, inputs, () => {
|
|
10
|
+
const { kit, ui, today, addDays, t, langOf } = core;
|
|
11
|
+
// the language: the one passed, else the nearest lang (the page's), English by default; repainted when it changes
|
|
12
|
+
const lang = inputs.lang || langOf(el), w = kit.words(lang), es = lang === 'es';
|
|
13
|
+
const fmtDay = (d) => (es ? `${d.getDate()} ${w.mon[d.getMonth()].slice(0, 3)}` : `${w.mon[d.getMonth()].slice(0, 3)} ${d.getDate()}`);
|
|
10
14
|
const iso = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
11
15
|
const one = (s) => { const t = String(s).trim(); if (t === 'today') return new Date(today); if (/^-?\d+$/.test(t)) return addDays(today, Number(t)); const d = new Date(t + 'T00:00:00'); return Number.isNaN(d.getTime()) ? null : d; };
|
|
12
16
|
const mode = inputs.mode === 'range' ? 'range' : 'single';
|
|
13
17
|
const decode = (v) => { const s = String(v ?? ''); if (!s) return null; if (mode === 'range') { const [a, b] = s.split('/').map(one); return a ? [a, b || a] : null; } return one(s); };
|
|
14
|
-
const sayOne = (d) => (d ?
|
|
15
|
-
const sayRange = (r) => (r?.[0] && r?.[1] ?
|
|
18
|
+
const sayOne = (d) => (d ? (es ? t(lang, 'You picked {0}, {1}', w.dayL[d.getDay()].toLowerCase(), `${d.getDate()} de ${w.mon[d.getMonth()]}`) : t(lang, 'You picked {0}, {1}', w.dayL[d.getDay()], `${w.mon[d.getMonth()]} ${d.getDate()}`)) : '');
|
|
19
|
+
const sayRange = (r) => (r?.[0] && r?.[1] ? t(lang, 'From {0} to {1} · {2} days', fmtDay(r[0]), fmtDay(r[1]), Math.round((r[1] - r[0]) / 864e5) + 1) : r?.[0] ? t(lang, 'From {0}: pick the last day', fmtDay(r[0])) : '');
|
|
16
20
|
const say = (v) => (mode === 'range' ? sayRange(v) : sayOne(v));
|
|
17
|
-
const PRESETS = [['
|
|
21
|
+
const PRESETS = [[t(lang, 'Today'), () => [today, today]], [t(lang, 'Last 7 days'), () => [addDays(today, -6), today]], [t(lang, 'This month'), () => [new Date(today.getFullYear(), today.getMonth(), 1), today]], [t(lang, 'Last month'), () => [new Date(today.getFullYear(), today.getMonth() - 1, 1), new Date(today.getFullYear(), today.getMonth(), 0)]]];
|
|
18
22
|
const withPresets = mode === 'range' && inputs.presets === 'reports';
|
|
19
23
|
el.innerHTML = `${withPresets ? `<div class="cx-range-wrap"><div class="cx-vstack cx-range-presets" style="width:auto;gap:2px">${PRESETS.map(([l], i) => ui.button({ label: l, variant: 'ghost', size: 'sm', cls: 'cx-left', attrs: `data-preset="${i}" aria-pressed="${i === 1}"` })).join('')}</div><div data-cal></div></div>` : '<div data-cal></div>'}${inputs.caption === 'on' ? '<span class="cx-text" data-variant="caption" data-out></span>' : ''}`;
|
|
20
24
|
const out = el.querySelector('[data-out]');
|
|
21
25
|
const value = decode(inputs.selected);
|
|
22
26
|
const emit = (v) => on.select?.(Array.isArray(v) ? (v[0] ? `${iso(v[0])}/${iso(v[1] || v[0])}` : '') : v ? iso(v) : '');
|
|
23
|
-
cal = kit.calendar(el.querySelector('[data-cal]'), { mode, months: Number(inputs.months) === 2 ? 2 : 1, value, min: inputs.min ? one(inputs.min) : null,
|
|
27
|
+
cal = kit.calendar(el.querySelector('[data-cal]'), { ...kit.calWords(lang), mode, months: Number(inputs.months) === 2 ? 2 : 1, value, min: inputs.min ? one(inputs.min) : null,
|
|
24
28
|
onChange: (v) => { if (out) out.textContent = say(v); el.querySelectorAll('[data-preset]').forEach((b) => b.setAttribute('aria-pressed', 'false')); emit(v); } });
|
|
25
29
|
if (out) out.textContent = say(value);
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
// on the presets' own box (made again on a repaint), so a repaint never stacks a second listener
|
|
31
|
+
el.querySelector('.cx-range-presets')?.addEventListener('click', (e) => { const b = e.target.closest('[data-preset]'); if (!b) return; const r = PRESETS[+b.dataset.preset][1](); cal.set(r); if (out) out.textContent = sayRange(r); el.querySelectorAll('[data-preset]').forEach((x) => x.setAttribute('aria-pressed', x === b)); emit(r); });
|
|
32
|
+
}));
|
|
28
33
|
return () => {};
|
|
29
34
|
}
|
package/registry/calendar.muten
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# onSelect gets "yyyy-mm-dd" (single) or "from/to" (range).
|
|
3
3
|
# Calendar(selected: "today", min: "today", caption: "on", onSelect: pickDay)
|
|
4
4
|
# Calendar(selected: "-6/0", mode: "range", months: 2, presets: "reports", caption: "on", onSelect: pickRange)
|
|
5
|
-
# selected/min take dates or days from today ("today", "-6/0"). presets "reports" adds
|
|
5
|
+
# selected/min take dates or days from today ("today", "-6/0"). presets "reports" adds Today · Last 7 days · This
|
|
6
6
|
# mes · Mes pasado beside a range; caption "on" writes what was chosen under it.
|
|
7
7
|
# shadcn-for-muten · Custom (kit core) · host JS is calendar.js.
|
|
8
8
|
part Calendar(selected: text = "", mode: text = "single", months: number = 1, min: text = "", presets: text = "", caption: text = "", onSelect: action) {
|
package/registry/card.muten
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Card - a surface that only CONVEYS: header (title, description, an action at the corner), content, footer.
|
|
2
2
|
# Decisions go in a modal, not on the card.
|
|
3
|
-
# Card(size: "sm") { CardHeader { CardTitle(label: "
|
|
3
|
+
# Card(size: "sm") { CardHeader { CardTitle(label: "Today") CardDescription(label: "3 appointments") CardAction { Btn(variant: "ghost", size: "icon-sm", onClick: more) { Icon "lucide:ellipsis" } } } CardContent { … } }
|
|
4
4
|
# size: default | sm (compact: items edge to edge)
|
|
5
5
|
# shadcn-for-muten · part · no deps · Nova (.cn-card) + kit.css (size sm).
|
|
6
6
|
part Card(size: text = "default") { Section class("cn-card group/card") data(slot: "card", size: $size) { slot } }
|
package/registry/carousel.muten
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# perView "4" or "2,4" (phone, wider) · center "on" (the active one centred, neighbours peeking) · loop "on" ·
|
|
3
3
|
# autoplay ms (pauses on hover, focus, touch, hidden tab; off with reduced motion; its own pause button) ·
|
|
4
4
|
# indicator dots | progress | count | thumbs · arrows bar | overlay
|
|
5
|
-
# Carousel(label: "
|
|
5
|
+
# Carousel(label: "Products", perView: "2,4") { each products as p { CarouselSlide { … } } }
|
|
6
6
|
# shadcn-for-muten · part + Custom (CarouselBehavior, kit collections) · kit.css (.cx-car).
|
|
7
7
|
part Carousel(label: text, perView: text = "1", center: text = "", loop: text = "", autoplay: number = 0, indicator: text = "dots", arrows: text = "bar") {
|
|
8
8
|
Section class("cx-car") aria(label: $label, roledescription: "carrusel") {
|
package/registry/chat-play.muten
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# ChatPlay - a conversation shown, not used: the messages come in one by one (in `step` order, 1-6) and the
|
|
2
2
|
# assistant closes with a confirmation card, then it starts over. Its height is the conversation's: no empty chat.
|
|
3
3
|
# For a landing; the real assistant is Chatbot. The suggestions and the composer are a picture (not focusable).
|
|
4
|
-
# ChatPlay(title: "Bruma
|
|
4
|
+
# ChatPlay(title: "Bruma Barbershop", status: "Assistant · online", placeholder: "Write a message", label: "…") {
|
|
5
5
|
# ChatPlayLog {
|
|
6
|
-
# ChatPlayMsg(text: "
|
|
6
|
+
# ChatPlayMsg(text: "Hi, do you have a slot today?", who: "in", step: 1)
|
|
7
7
|
# ChatPlayMsg(text: "Hoy quedan 4:30 PM y 6:00 PM.", step: 2)
|
|
8
|
-
# ChatPlayCard(title: "
|
|
8
|
+
# ChatPlayCard(title: "Classic cut", text: "Today · 4:30 PM", step: 5)
|
|
9
9
|
# }
|
|
10
|
-
# ChatPlayChips { ChatPlayChip(label: "
|
|
10
|
+
# ChatPlayChips { ChatPlayChip(label: "How much is it?") … }
|
|
11
11
|
# }
|
|
12
12
|
# who: out (the shop and its assistant: right, dark, the default) | in (the client: left). Seen from the shop, as
|
|
13
13
|
# its inbox shows it. The confirmation card is the shop's, so it sits right too. Reduced motion: the whole conversation, still.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
# (its wallpaper, bubbles with their tail, the hour and the ticks inside, its composer); theme: "" | dark (WhatsApp at night).
|
|
16
16
|
# With a skin, `photo` puts the person in the header instead of the assistant's face, ChatPlayDay sets a day chip,
|
|
17
17
|
# ChatPlayMsg takes a `time`, ChatPlayButtons + ChatPlayButton hang reply buttons under the message before them
|
|
18
|
-
# (Telegram draws them as its keyboard, two per row), and ChatPlayNote adds
|
|
18
|
+
# (Telegram draws them as its keyboard, two per row), and ChatPlayNote adds “Seen” or who answered. The header carries
|
|
19
19
|
# the app's back arrow and call buttons, and the composer is that app's own. With a skin it is a phone screen (300 × 600,
|
|
20
20
|
# a status bar with `clock`), and Instagram puts `photo` beside the client's last message of each turn.
|
|
21
21
|
# face: "" (the kit's two-dot face) | host - an empty face slot (.cx-cp-face-host) for the page's own face, a brand
|
|
@@ -47,7 +47,7 @@ part ChatPlay(title: text, status: text = "", placeholder: text = "", label: tex
|
|
|
47
47
|
Span "{$placeholder}" class("cx-cp-ph")
|
|
48
48
|
Stack class("cx-cp-tools") { Icon "lucide:paperclip" Icon "lucide:camera" }
|
|
49
49
|
Stack class("cx-cp-igtools") { Icon "lucide:mic" Icon "lucide:image" Icon "lucide:sticker" }
|
|
50
|
-
Stack class("cx-cp-menu") { Icon "lucide:menu" Span "
|
|
50
|
+
Stack class("cx-cp-menu") { Icon "lucide:menu" Span "Menu" data(tr: "Menu") }
|
|
51
51
|
Stack class("cx-cp-send") { Icon "lucide:send-horizontal" }
|
|
52
52
|
Stack class("cx-cp-mic") { Icon "lucide:mic" }
|
|
53
53
|
}
|
|
@@ -65,9 +65,9 @@ part ChatPlayMsg(text: text, who: text = "out", step: number = 1, time: text = "
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
part ChatPlayDay(label: text) { Span "{$label}" class("cx-cp-day") }
|
|
68
|
-
# The start of a conversation as Instagram shows it: the person's photo (ChatPlay's `photo`), name, handle and
|
|
68
|
+
# The start of a conversation as Instagram shows it: the person's photo (ChatPlay's `photo`), name, handle and “See profile”.
|
|
69
69
|
part ChatPlayProfile(name: text, sub: text = "", action: text = "") { Stack class("cx-cp-prof") { Span "" class("cx-cp-prof-ph") Span "{$name}" class("cx-cp-prof-n") Span "{$sub}" class("cx-cp-prof-s") Span "{$action}" class("cx-cp-prof-a") } }
|
|
70
|
-
# A small line under a message: Instagram's
|
|
70
|
+
# A small line under a message: Instagram's “Seen”, or who answered (“Bruma's assistant answered”).
|
|
71
71
|
part ChatPlayNote(label: text, step: number = 1) { Span "{$label}" class("cx-cp-note") data(step: $step) }
|
|
72
72
|
part ChatPlayButtons(step: number = 1) { Stack class("cx-cp-btns") data(step: $step) { slot } }
|
|
73
73
|
part ChatPlayButton(label: text) { Stack class("cx-cp-btn") { Icon "lucide:reply" Span "{$label}" } }
|
package/registry/chatbot.muten
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
# Chatbot - the assistant: greets, offers suggestions,
|
|
1
|
+
# Chatbot - the assistant: greets, offers suggestions, “thinks”, writes the answer live (with Stop), can carry an
|
|
2
2
|
# action button inside the answer (onAction gets its label), rated with 👍 / 👎. answers: {match (regex), text, action,
|
|
3
3
|
# primary}; the one without match is the fallback.
|
|
4
|
-
# Chatbot(title: "Rober", greeting: "
|
|
4
|
+
# Chatbot(title: "Rober", greeting: "Hi, I'm Rober…", suggestions: @ideas, answers: @answers, onAction: act)
|
|
5
|
+
# avatar: an image, a logo or a GIF url in place of the drawn face (“/logo.svg”, “/rober.gif”).
|
|
5
6
|
# demo "on": it taps its own suggestions in turn and answers them live, for a landing; the first touch stops it.
|
|
6
7
|
# shadcn-for-muten · part over KitWidget (kit core).
|
|
7
|
-
part Chatbot(title: text, greeting: text, suggestions: list, answers: list, demo: text = "", onAction: action) {
|
|
8
|
-
Custom KitWidget inputs(name: "chatbot", title: $title, greeting: $greeting, suggestions: $suggestions, answers: $answers, demo: $demo) on(action: $onAction) class("cx-bot")
|
|
8
|
+
part Chatbot(title: text, greeting: text, suggestions: list, answers: list, demo: text = "", avatar: text = "", onAction: action) {
|
|
9
|
+
Custom KitWidget inputs(name: "chatbot", title: $title, greeting: $greeting, suggestions: $suggestions, answers: $answers, demo: $demo, avatar: $avatar) on(action: $onAction) class("cx-bot")
|
|
9
10
|
}
|
package/registry/checklist.muten
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Checklist - first steps with progress: done steps checked, the next ones with their action (onAction gets its label).
|
|
2
|
-
# Checklist(title: "
|
|
2
|
+
# Checklist(title: "Your shop in 5 steps", steps: @steps, onAction: act) steps: {title, desc, done, action}
|
|
3
3
|
# shadcn-for-muten · part over KitWidget (kit core).
|
|
4
4
|
part Checklist(title: text, steps: list, onAction: action) {
|
|
5
5
|
Custom KitWidget inputs(name: "checklist", title: $title, steps: $steps) on(action: $onAction) class("cx-onboard")
|