@muten/shadcn 0.1.0 → 0.1.2
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 +64 -1
- package/README.md +7 -0
- package/kit.css +2289 -25
- package/motion.css +16 -1
- package/package.json +3 -2
- package/registry/accordion.muten +10 -0
- package/registry/agenda.js +72 -24
- package/registry/agenda.muten +30 -2
- package/registry/band.muten +1 -1
- package/registry/bio-play.muten +16 -0
- package/registry/booking-play.muten +43 -0
- package/registry/button.muten +3 -2
- package/registry/chat-play.muten +77 -0
- package/registry/day-plan.muten +40 -0
- package/registry/device-frame.muten +25 -7
- package/registry/drawer.muten +1 -1
- package/registry/gradient-blinds.js +88 -89
- package/registry/gradient-blinds.muten +11 -4
- package/registry/inbox-play.muten +56 -0
- package/registry/input-group.muten +3 -1
- package/registry/kit/board.js +1 -1
- package/registry/kit/collections.js +10 -2
- package/registry/kit/core.js +519 -99
- package/registry/kit/structure.js +7 -5
- package/registry/kit-widget.js +1 -1
- package/registry/legal-doc.muten +27 -0
- package/registry/modal-behavior.js +11 -0
- package/registry/modal.muten +2 -2
- package/registry/order-play.muten +33 -0
- package/registry/permission-matrix.muten +31 -5
- package/registry/phone-bar.muten +18 -0
- package/registry/phone-shell.muten +1 -1
- package/registry/product-play.muten +20 -0
- package/registry/screen-tour.js +59 -0
- package/registry/screen-tour.muten +10 -0
- package/registry/sidebar.muten +9 -0
- package/registry/site-footer.muten +37 -0
- package/registry/stamp-card.muten +5 -2
- package/registry/zoom-control.muten +3 -3
- package/registry.json +78 -0
- package/scripts/reuse-audit.mjs +37 -0
package/motion.css
CHANGED
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
|
|
61
61
|
/* Drift: a strip that slides by itself (put its items twice, so it loops without a seam). */
|
|
62
62
|
@keyframes mu-drift { to { transform: translateX(-50%); } }
|
|
63
|
-
.drift { display: flex; width: max-content; animation: mu-drift var(--drift, 60s) linear infinite; }
|
|
63
|
+
.drift { display: flex; flex-direction: row; width: max-content; animation: mu-drift var(--drift, 60s) linear infinite; }
|
|
64
64
|
.drift:hover { animation-play-state: paused; }
|
|
65
65
|
|
|
66
66
|
/* Photo-in: a picture settles from a soft zoom as it appears. */
|
|
@@ -77,3 +77,18 @@
|
|
|
77
77
|
.animate-fade-in, .animate-rise, .animate-scale-in, .animate-blur-in, .stagger > *, .words > *, .reveal, .reveal-stagger > *, .drift, .photo-in img { animation: none !important; }
|
|
78
78
|
.hover-lift, .hover-zoom img { transition: none !important; }
|
|
79
79
|
}
|
|
80
|
+
|
|
81
|
+
/* smooth-scroll - on any element of the page: a jump to an anchor of the same page glides instead of snapping. */
|
|
82
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
83
|
+
html:has(.smooth-scroll) { scroll-behavior: smooth; }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* reveal-blur - on a page's root: as you scroll down, each block of the marketing system (headings, rows, cards,
|
|
87
|
+
steps, tables, questions, the closer; never the hero) comes in from a blur, rising a little. Kit core marks each
|
|
88
|
+
block (data-rv) and lets it in as it enters the screen; what is on screen when the page opens is shown whole, so the
|
|
89
|
+
first paint, the LCP and a contrast checker never meet a half-faded block. No script or reduced motion: all shown. */
|
|
90
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
91
|
+
/* hiding is instant (it happens off screen, while the page loads); only the entrance moves */
|
|
92
|
+
[data-rv=""] { opacity: 0; filter: blur(14px); transform: translateY(28px); }
|
|
93
|
+
[data-rv="in"] { transition: opacity 700ms cubic-bezier(.22, 1, .36, 1), filter 700ms cubic-bezier(.22, 1, .36, 1), transform 700ms cubic-bezier(.22, 1, .36, 1); transition-delay: calc(var(--rv-i, 0) * 70ms); }
|
|
94
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@muten/shadcn",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "shadcn-style component registry for muten - own-the-source parts you add with `muten add`.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"motion.css",
|
|
21
21
|
"themes",
|
|
22
22
|
"README.md",
|
|
23
|
-
"AGENTS.md"
|
|
23
|
+
"AGENTS.md",
|
|
24
|
+
"scripts/reuse-audit.mjs"
|
|
24
25
|
],
|
|
25
26
|
"exports": {
|
|
26
27
|
"./globals.css": "./globals.css",
|
package/registry/accordion.muten
CHANGED
|
@@ -1,6 +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
3
|
# Accordion { AccordionItem(title: "¿Puedo cancelar mi cita?", open: a1, onToggle: t1) { Text "Sí, hasta 3 horas antes…" } }
|
|
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 "¿…?" group("faq") class("cn-accordion-item") { Text "…" class("cn-accordion-content-inner") } }
|
|
4
6
|
# size "lg": taller rows and bigger type, for the questions of a landing page.
|
|
5
7
|
# shadcn-for-muten · part · no deps · Nova (.cn-accordion) + kit.css.
|
|
6
8
|
part Accordion(size: text = "default") { Stack class("cn-accordion") data(slot: "accordion", size: $size) { slot } }
|
|
@@ -15,3 +17,11 @@ part AccordionItem(title: text, open: bool, onToggle: action) {
|
|
|
15
17
|
}
|
|
16
18
|
}
|
|
17
19
|
}
|
|
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 «faq»). The answer goes in the slot (text, or text with a button). FaqItemOpen starts open (the first of a list).
|
|
22
|
+
part FaqItem(question: text) {
|
|
23
|
+
Details "{$question}" group("faq") class("cn-accordion-item") { Stack class("cn-accordion-content-inner") { slot } }
|
|
24
|
+
}
|
|
25
|
+
part FaqItemOpen(question: text) {
|
|
26
|
+
Details "{$question}" group("faq") open class("cn-accordion-item") { Stack class("cn-accordion-content-inner") { slot } }
|
|
27
|
+
}
|
package/registry/agenda.js
CHANGED
|
@@ -1,35 +1,83 @@
|
|
|
1
|
-
// Agenda (muten Custom) - the kit's agenda (day columns per person, week, list; drag to move,
|
|
2
|
-
//
|
|
3
|
-
// resources [{id, name}]
|
|
4
|
-
//
|
|
1
|
+
// Agenda / AgendaLive (muten Custom) - the kit's agenda (kit/core.js): day columns per person, week, list; drag to move,
|
|
2
|
+
// edge to resize. Everything is data:
|
|
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`, «Salas»). capacity = how many citas
|
|
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 «Se cruza».
|
|
7
|
+
// resources [{id, name, photo}] · hours [{id, days: [1..6], ranges: [[540, 780], [840, 1140]]}] (minutes from midnight)
|
|
8
|
+
// events [{id, resource, with: [ids], day, start "HH:MM", dur, title, subtitle, price, status, tint, ink, tone, steps, actions, at, payment, contact}]
|
|
5
9
|
// day = "yyyy-mm-dd", or a number of days from today (it lands on the next day that person works)
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
+
// with = the other agendas the cita also takes (the room it uses, a second worker): checked and shown in each
|
|
11
|
+
// status = awaiting_payment | confirmed | started | done | noshow | cancelled · tint/ink = the service's colours
|
|
12
|
+
// blocks [{id, resource | "all", day, start, dur, reason}] · from / to (hours) · slot (minutes) · view: day | week | list
|
|
13
|
+
// free [{resource, day, start, end}] - the server's free hours; when sent they are the only free hours shown
|
|
14
|
+
// queue [{day, name, photo}] - who waits for a free hour that day · closed ["yyyy-mm-dd"] - days the shop is shut
|
|
15
|
+
// (blocks, queue and closed take a number of days from today too)
|
|
16
|
+
// lang es | en · currency (ISO, e.g. "USD") + locale · cents "yes" (prices in cents) · sheet "app" · drag "off"
|
|
17
|
+
// loading "yes" (the calendar's shape in grey until the data arrives) · start "yyyy-mm-dd" (the day it opens on)
|
|
18
|
+
// actions [{id, label, icon, primary}] - the toolbar's buttons (default: block and new); any other id fires `action`
|
|
19
|
+
// phone "nav" - on a phone the actions and the arrows leave the agenda's toolbar: the app's phone bar carries them and
|
|
20
|
+
// sends them back through `command`: "new", "block", "prev", "next", "today", "view:list", or an action id. The
|
|
21
|
+
// agenda runs a command when its value changes, so repeat one with a suffix after # ("new#1", "new#2"…)
|
|
22
|
+
// hour12 "yes" - times written 3:00 PM (the data stays "HH:MM") · timeZone "America/Bogota" - today and now are the shop's
|
|
23
|
+
// phoneView "list" - a phone opens on the list until a view is picked · blockScope "all" - blocks are for the whole
|
|
24
|
+
// business (no «Para» picker: a backend with no per-person blocks)
|
|
25
|
+
// events also take tags ["A domicilio", "Llegó 10:02"] (words next to the time) and photo (the client's face, in the list)
|
|
26
|
+
// resources also take role (read under the name), hideEmpty (the column shows only on days it has citas) and
|
|
27
|
+
// bookable false (an agenda that only holds citas, «Sin asignar»: never offered as free)
|
|
28
|
+
// resize "off" - a cita's length is not dragged (a backend that only moves day and time) · moveAcross "off" - a
|
|
29
|
+
// dragged cita stays in its own agenda (only its time, and its day in the week view, change)
|
|
30
|
+
// placing [{resource, day, start, dur, title, tint}] - while the app's «new cita» sheet is open, where it will land
|
|
31
|
+
// create "sheet" - a showcase: «new» and a tap on a free hour open a short built-in form and the cita lands at once
|
|
32
|
+
// handlers: create(json {resource, day, start}) · open(json event) · block(json {resource, day, start, dur, reason})
|
|
33
|
+
// move(json {id, resource, day, start, dur}) · range(json {from, to, view}: view is day | week | list | range) · change(json [...]) · action(id)
|
|
34
|
+
// The title opens a month (floating; a sheet on a phone): one day, or «Varios días», a range listed day by day.
|
|
35
|
+
// LIVE: new inputs repaint the same agenda (same day, view and person): an app sends its data again and it shows;
|
|
36
|
+
// lang, currency, locale and cents change live too.
|
|
10
37
|
export function mount(el, inputs, on) {
|
|
11
|
-
let
|
|
38
|
+
let api = null, map = null, pending = null, lastCmd = String(inputs.command || ''); // the command it was born with never runs
|
|
12
39
|
import('@muten/shadcn/registry/kit/core.js').then(({ kit, today, addDays }) => {
|
|
13
40
|
const iso = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
14
41
|
const list = (v) => (Array.isArray(v) ? v : []);
|
|
15
|
-
const
|
|
42
|
+
const at = (s) => new Date(String(s) + 'T00:00:00');
|
|
43
|
+
const yes = (v) => v === true || v === 'yes' || v === 'true';
|
|
44
|
+
const data = (i) => {
|
|
16
45
|
const hours = Object.fromEntries(list(i.hours).map((h) => [h.id, h]));
|
|
17
46
|
const work = (rid, d) => { const h = hours[rid]; return h && list(h.days).includes(d.getDay()) ? list(h.ranges) : []; };
|
|
18
47
|
const land = (rid, off) => { let o = Number(off); for (let k = 0; k < 14 && !work(rid, addDays(today, o)).length; k++) o++; return addDays(today, o); };
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
});
|
|
48
|
+
const plain = (d) => (typeof d === 'number' || /^-?\d+$/.test(String(d)) ? addDays(today, Number(d)) : at(d));
|
|
49
|
+
const dayOf = (rid, d) => (typeof d === 'number' || /^-?\d+$/.test(String(d)) ? land(rid, d) : at(d));
|
|
50
|
+
const free = Array.isArray(i.free) && i.free.length ? i.free.map((f) => ({ ...f, day: dayOf(f.resource, f.day) })) : null;
|
|
51
|
+
return {
|
|
52
|
+
resources: list(i.resources), availability: work, free,
|
|
53
|
+
events: list(i.events).map((e) => ({ tone: 'ok', at: 0, ...e, day: dayOf(e.resource, e.day) })),
|
|
54
|
+
blocks: list(i.blocks).map((b) => ({ ...b, day: plain(b.day) })),
|
|
55
|
+
queue: list(i.queue).map((q) => ({ ...q, day: plain(q.day) })), closed: list(i.closed).map(plain), loading: yes(i.loading),
|
|
56
|
+
placing: (() => { const g = list(i.placing)[0]; return g && g.start ? { ...g, day: dayOf(g.resource, g.day) } : null; })(),
|
|
57
|
+
lang: i.lang === 'en' ? 'en' : 'es', overlap: ['never', 'allow'].includes(i.overlap) ? i.overlap : 'auto', currency: i.currency || '', locale: i.locale || '', cents: yes(i.cents),
|
|
58
|
+
};
|
|
31
59
|
};
|
|
32
|
-
|
|
60
|
+
map = data;
|
|
61
|
+
const send = (name, value) => on[name]?.(typeof value === 'string' ? value : JSON.stringify(value));
|
|
62
|
+
const day1 = (x) => ({ ...x, day: x.day instanceof Date ? iso(x.day) : x.day });
|
|
63
|
+
el.innerHTML = '';
|
|
64
|
+
api = kit.agenda(el, {
|
|
65
|
+
...data(inputs),
|
|
66
|
+
actions: list(inputs.actions).length ? list(inputs.actions) : null,
|
|
67
|
+
from: Number(inputs.from) || 9, to: Number(inputs.to) || 19, slot: Number(inputs.slot) || 30, view: inputs.view || 'day',
|
|
68
|
+
createSheet: inputs.create === 'sheet', services: list(inputs.services),
|
|
69
|
+
sheet: inputs.sheet === 'app' ? 'app' : 'built-in', drag: inputs.drag !== 'off', start: inputs.start ? at(inputs.start) : null, phone: inputs.phone === 'nav' ? 'nav' : '',
|
|
70
|
+
resize: inputs.resize === 'off' ? 'off' : 'on', moveAcross: inputs.moveAcross === 'off' ? 'off' : 'on', hour12: yes(inputs.hour12), timeZone: inputs.timeZone || '', phoneView: ['list', 'day'].includes(inputs.phoneView) ? inputs.phoneView : '', blockScope: inputs.blockScope === 'all' ? 'all' : 'each',
|
|
71
|
+
onAction: (id) => send('action', id),
|
|
72
|
+
onCreate: (x) => send('create', day1(x)),
|
|
73
|
+
onOpen: on.open ? (e) => send('open', day1({ id: e.id, resource: e.resource, day: e.day, start: e.start, dur: e.dur, status: e.status || '' })) : null,
|
|
74
|
+
onBlock: on.block ? (b) => send('block', day1(b)) : null,
|
|
75
|
+
onMove: (m) => send('move', day1(m)),
|
|
76
|
+
onRange: (r) => send('range', { from: iso(r.from), to: iso(r.to), view: r.view }),
|
|
77
|
+
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 || '' }))),
|
|
78
|
+
});
|
|
79
|
+
if (pending) { api.update(data(pending)); pending = null; }
|
|
33
80
|
});
|
|
34
|
-
|
|
81
|
+
const command = (next) => { const c = String(next.command || ''); if (c === lastCmd) return; lastCmd = c; if (c) api.run(c.split('#')[0]); };
|
|
82
|
+
return (next) => { if (api && map) { api.update(map(next)); command(next); } else pending = next; };
|
|
35
83
|
}
|
package/registry/agenda.muten
CHANGED
|
@@ -4,7 +4,35 @@
|
|
|
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
5
|
# actions: the toolbar's buttons as data, [{id, label, icon, primary}] (default: Bloquear and Nueva cita); an id other
|
|
6
6
|
# than block or new fires onAction with it: Agenda(…, actions: @agendaActions, onAction: agendaAction)
|
|
7
|
+
# view: how it opens, day (default) | week | list.
|
|
7
8
|
# shadcn-for-muten · Custom (kit core) · host JS is agenda.js.
|
|
8
|
-
part Agenda(resources: list, hours: list, events: list, blocks: list, actions: list, from: number = 9, to: number = 19, slot: number = 30, onCreate: action, onChange: action, onAction: action) {
|
|
9
|
-
Custom Agenda inputs(resources: $resources, hours: $hours, events: $events, blocks: $blocks, actions: $actions, from: $from, to: $to, slot: $slot) on(create: $onCreate, change: $onChange, action: $onAction) class("cx-agenda")
|
|
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
|
+
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
|
+
}
|
|
12
|
+
# AgendaDemo - the same agenda for a showcase with no app behind it: «Nueva cita» and a tap on a free hour open a short
|
|
13
|
+
# built-in form (the service from `services` [{name, dur}], the client, the time) and the appointment lands on the
|
|
14
|
+
# agenda at once, with its notice. Block, move and the appointment sheet work as in Agenda. Nothing is saved anywhere.
|
|
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) {
|
|
16
|
+
Custom Agenda inputs(resources: $resources, hours: $hours, events: $events, blocks: $blocks, actions: $actions, from: $from, to: $to, slot: $slot, view: $view, create: "sheet", services: $services) on(change: $onChange, action: $onAction) class("cx-agenda")
|
|
17
|
+
}
|
|
18
|
+
# AgendaLive - the agenda for an APP: it paints what it is given and tells the app what happened; the app keeps the
|
|
19
|
+
# data and its own sheets. Send the lists again and it repaints on the same day, view and person.
|
|
20
|
+
# free the server's free hours [{resource, day, start, end}] (the only free hours shown; [] = from `hours`)
|
|
21
|
+
# queue who waits for a free hour [{day, name, photo}] · closed days the shop is shut ["yyyy-mm-dd"]
|
|
22
|
+
# events carry status (awaiting_payment | confirmed | started | done | noshow | cancelled) and tint/ink (the service's colours)
|
|
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 cita tapped (open your sheet) · onCreate(json {resource, day, start}) a free hour tapped
|
|
25
|
+
# onBlock(json {resource, day, start, dur, reason}) · onMove(json {id, resource, day, start, dur}) dragged (answer with
|
|
26
|
+
# the data: it stays where it landed, or goes back if you send it unchanged) · onRange(json {from, to, view}) days shown
|
|
27
|
+
# phone "nav": on a phone its actions (Bloquear, Nueva cita, yours) and the arrows leave the toolbar for the app's
|
|
28
|
+
# phone bar (PhoneBar), which sends them back through `command` ("new", "block", "prev", "next", "today", "view:list",
|
|
29
|
+
# an action id; repeat one with a suffix after #: "new#2"). Nothing runs while loading.
|
|
30
|
+
# several agendas: resources carry kind (business | person | a room…) and capacity (1 never overlaps, 0 no limit);
|
|
31
|
+
# a cita takes its resource plus every id in `with`. overlap "never" | "allow" overrides all capacities.
|
|
32
|
+
# 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" · blockScope "all" · placing [{…}]
|
|
33
|
+
# (the dashed cita while the app's new-cita sheet is open) · events take tags [words] and photo; resources role, hideEmpty
|
|
34
|
+
# AgendaLive(resources: @team, hours: @hours, events: @citas, blocks: @blocks, free: @free, queue: @queue, closed: @closed,
|
|
35
|
+
# lang: ui.lang, currency: "USD", onOpen: openCita, onCreate: newCita, onBlock: block, onMove: moveCita, onRange: loadDays)
|
|
36
|
+
part AgendaLive(resources: list, hours: list, events: list, blocks: 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 = "", blockScope: text = "", resize: text = "", moveAcross: text = "", placing: list, onOpen: action, onCreate: action, onBlock: action, onMove: action, onRange: action) {
|
|
37
|
+
Custom Agenda inputs(resources: $resources, hours: $hours, events: $events, blocks: $blocks, 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, blockScope: $blockScope, resize: $resize, moveAcross: $moveAcross, placing: $placing, sheet: "app") on(open: $onOpen, create: $onCreate, block: $onBlock, move: $onMove, range: $onRange) class("cx-agenda")
|
|
10
38
|
}
|
package/registry/band.muten
CHANGED
|
@@ -9,7 +9,7 @@ part Band(open: bool, tone: text = "muted", text: text, action: text = "", onAct
|
|
|
9
9
|
Stack class("cx-band") data(tone: $tone) aria(role: "status") {
|
|
10
10
|
slot
|
|
11
11
|
Span "{$text}" class("cx-band-text")
|
|
12
|
-
when $action != "" {
|
|
12
|
+
when $action != "" { Btn(variant: "outline", size: "xs", onClick: $onAction) { Span "{$action}" } }
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# BioPlay - a social profile shown, not used: the photo, the handle, the followers, the bio with the store's link
|
|
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.284 seguidores", bio: "Cortes y barba.",
|
|
4
|
+
# link: "storio.ink/bruma-barberia", follow: "Seguir", message: "Mensaje", label: "…")
|
|
5
|
+
# shadcn-for-muten · part · no deps · kit.css (.cx-bio).
|
|
6
|
+
part BioPlay(photo: text, handle: text, followers: text, bio: text, link: text, follow: text, message: text, label: text = "") {
|
|
7
|
+
Stack class("cx-bio cx-play") aria(role: "img", label: $label) {
|
|
8
|
+
Stack class("cx-bio-top") aria(hidden: true) {
|
|
9
|
+
Image "{$photo}" alt("") class("cx-bio-av")
|
|
10
|
+
Stack class("cx-bio-n") { Span "{$handle}" class("cx-bio-h") Span "{$followers}" }
|
|
11
|
+
}
|
|
12
|
+
Span "{$bio}" class("cx-bio-text") aria(hidden: true)
|
|
13
|
+
Stack class("cx-bio-link") aria(hidden: true) { Icon "lucide:link" Span "{$link}" }
|
|
14
|
+
Stack class("cx-bio-btns") aria(hidden: true) { Span "{$follow}" data(tone: "primary") Span "{$message}" }
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
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, «Reservar» is pressed and the confirmation comes up with its pass). For a
|
|
3
|
+
# landing or a bento cell, where the real Booking would be a form nobody fills. Taken hours are struck through.
|
|
4
|
+
# BookingPlay(service: "Corte clásico", place: "Bruma Barbería · 30 min", price: "USD 15", cta: "Reservar",
|
|
5
|
+
# doneTitle: "¡Listo, Carla!", doneText: "Corte clásico · hoy · 4:30 PM", code: "STO-4F2K", label: "Así reserva tu cliente") {
|
|
6
|
+
# BookingPlayDays { BookingPlayDay(weekday: "lun", day: "5", state: "off") … BookingPlayDay(weekday: "hoy", day: "9", state: "pick") }
|
|
7
|
+
# BookingPlayHours { BookingPlayHour(time: "10:00", state: "taken") … BookingPlayHour(time: "4:30", state: "pick", picked: "4:30 PM") }
|
|
8
|
+
# }
|
|
9
|
+
# photo: the owner's face in the circle (empty = the service's initial).
|
|
10
|
+
# state: "" | off (closed day) | taken (hour) | pick (the one the loop chooses; one day and one hour).
|
|
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 = "Reservar", doneTitle: text = "", doneText: text = "", code: text = "", label: text = "") {
|
|
13
|
+
Stack class("cx-bkp cx-play") aria(role: "img", label: $label) {
|
|
14
|
+
Stack class("cx-bkp-who") aria(hidden: true) {
|
|
15
|
+
when $photo != "" { Image "{$photo}" alt("") class("cx-bkp-av cx-play-face") }
|
|
16
|
+
when $photo == "" { Span "{initial($service)}" class("cx-bkp-av") }
|
|
17
|
+
Stack class("cx-bkp-name") { Span "{$service}" class("cx-bkp-t") Span "{$place}" class("cx-bkp-s") }
|
|
18
|
+
Span "{$price}" class("cx-bkp-price")
|
|
19
|
+
}
|
|
20
|
+
slot
|
|
21
|
+
Span "{$cta}" class("cx-bkp-go") aria(hidden: true)
|
|
22
|
+
Stack class("cx-bkp-done") aria(hidden: true) {
|
|
23
|
+
Stack class("cx-bkp-ok") { Icon "lucide:check" }
|
|
24
|
+
Span "{$doneTitle}" class("cx-bkp-dt")
|
|
25
|
+
Span "{$doneText}" class("cx-bkp-dd")
|
|
26
|
+
when $code != "" { Stack class("cx-bkp-pass") { Icon "lucide:qr-code" Span "{$code}" } }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
part BookingPlayDays() { Stack class("cx-bkp-days") aria(hidden: true) { slot } }
|
|
31
|
+
part BookingPlayDay(weekday: text, day: text, state: text = "") {
|
|
32
|
+
Stack class("cx-bkp-day") data(state: $state) {
|
|
33
|
+
Span "{$weekday}" class("cx-bkp-wd") Span "{$day}" class("cx-bkp-d")
|
|
34
|
+
when $state == "pick" { Stack class("cx-bkp-on") { Span "{$weekday}" class("cx-bkp-wd") Span "{$day}" class("cx-bkp-d") } }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
part BookingPlayHours() { Stack class("cx-bkp-hours") aria(hidden: true) { slot } }
|
|
38
|
+
part BookingPlayHour(time: text, state: text = "", picked: text = "") {
|
|
39
|
+
Stack class("cx-bkp-hour") data(state: $state) {
|
|
40
|
+
Span "{$time}"
|
|
41
|
+
when $state == "pick" { Span "{$picked}" class("cx-bkp-on") }
|
|
42
|
+
}
|
|
43
|
+
}
|
package/registry/button.muten
CHANGED
|
@@ -8,10 +8,11 @@
|
|
|
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.
|
|
10
10
|
# tone: "" | info | ok | wait | bad | note - a tinted icon button (a pending thing on a card)
|
|
11
|
+
# disabled: a bool (a state or a getter: a part argument can't be an expression) - the button can't be pressed.
|
|
11
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.
|
|
12
13
|
# shadcn-for-muten · part · no deps · Nova classes (nova.css) + kit.css.
|
|
13
|
-
part Btn(variant: text = "default", size: text = "default", tone: text = "", label: text = "", onClick: action) {
|
|
14
|
-
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) aria(label: $label) { slot }
|
|
14
|
+
part Btn(variant: text = "default", size: text = "default", tone: text = "", label: text = "", onClick: action, disabled: bool = false) {
|
|
15
|
+
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) aria(label: $label) disabled when $disabled { slot }
|
|
15
16
|
}
|
|
16
17
|
# TriggerBtn - a Btn that only opens what sits next to it (a Popover, a DropdownMenu, a SplitButton's menu): it has no
|
|
17
18
|
# action of its own, so opening never runs something else. Same variants and sizes as Btn.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# ChatPlay - a conversation shown, not used: the messages come in one by one (in `step` order, 1-6) and the
|
|
2
|
+
# assistant closes with a confirmation card, then it starts over. Its height is the conversation's: no empty chat.
|
|
3
|
+
# For a landing; the real assistant is Chatbot. The suggestions and the composer are a picture (not focusable).
|
|
4
|
+
# ChatPlay(title: "Bruma Barbería", status: "Asistente · en línea", placeholder: "Escribe un mensaje", label: "…") {
|
|
5
|
+
# ChatPlayLog {
|
|
6
|
+
# ChatPlayMsg(text: "Hola, ¿tienes hora hoy?", who: "in", step: 1)
|
|
7
|
+
# ChatPlayMsg(text: "Hoy quedan 4:30 PM y 6:00 PM.", step: 2)
|
|
8
|
+
# ChatPlayCard(title: "Corte clásico", text: "Hoy · 4:30 PM", step: 5)
|
|
9
|
+
# }
|
|
10
|
+
# ChatPlayChips { ChatPlayChip(label: "¿Cuánto cuesta?") … }
|
|
11
|
+
# }
|
|
12
|
+
# who: out (the shop and its assistant: right, dark, the default) | in (the client: left). Seen from the shop, as
|
|
13
|
+
# its inbox shows it. The confirmation card is the shop's, so it sits right too. Reduced motion: the whole conversation, still.
|
|
14
|
+
# skin: "" (the kit's own) | whatsapp | instagram | telegram - the conversation as that app shows it on the shop's phone
|
|
15
|
+
# (its wallpaper, bubbles with their tail, the hour and the ticks inside, its composer); theme: "" | dark (WhatsApp at night).
|
|
16
|
+
# With a skin, `photo` puts the person in the header instead of the assistant's face, ChatPlayDay sets a day chip,
|
|
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 «Visto» or who answered. The header carries
|
|
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
|
+
# a status bar with `clock`), and Instagram puts `photo` beside the client's last message of each turn.
|
|
21
|
+
# shadcn-for-muten · part · no deps · kit.css (.cx-cp).
|
|
22
|
+
part ChatPlay(title: text, status: text = "", placeholder: text = "", label: text = "", skin: text = "", theme: text = "", photo: text = "", clock: text = "9:41") {
|
|
23
|
+
# With a skin, the chat is a phone screen inside the library's one phone (the same iPhone as DeviceFrame), drawn at
|
|
24
|
+
# a real phone's 390 × 844 and scaled to the frame. Without one the wrappers do not count for layout.
|
|
25
|
+
Stack class("cx-cp-phone") data(skin: $skin) {
|
|
26
|
+
Stack class("cx-cp-frame") {
|
|
27
|
+
Stack class("cx-cp-screen") {
|
|
28
|
+
Stack class("cx-cp cx-play") data(skin: $skin, theme: $theme) style(face: "url('{$photo}')") aria(role: "img", label: $label) {
|
|
29
|
+
Stack class("cx-cp-sb") aria(hidden: true) { Span "{$clock}" Stack class("cx-cp-sbi") { Icon "lucide:signal" Icon "lucide:wifi" Icon "lucide:battery-full" } }
|
|
30
|
+
Stack class("cx-cp-head") aria(hidden: true) {
|
|
31
|
+
Stack class("cx-cp-back") { Icon "lucide:chevron-left" }
|
|
32
|
+
when $photo == "" { Stack class("cx-cp-face") { Span "" Span "" } }
|
|
33
|
+
when $photo != "" { Stack class("cx-cp-ring") { Image "{$photo}" alt("") class("cx-cp-photo") } }
|
|
34
|
+
Stack class("cx-cp-name") { Span "{$title}" class("cx-cp-t") Span "{$status}" class("cx-cp-s") }
|
|
35
|
+
Stack class("cx-cp-acts") { Icon "lucide:video" Icon "lucide:phone" Icon "lucide:ellipsis-vertical" }
|
|
36
|
+
}
|
|
37
|
+
slot
|
|
38
|
+
# The composer as each app draws it (the skin decides which of these show): Instagram's blue camera, WhatsApp's
|
|
39
|
+
# field with its emoji, clip and camera beside the green mic, Telegram's flat bar.
|
|
40
|
+
Stack class("cx-cp-comp") aria(hidden: true) {
|
|
41
|
+
Stack class("cx-cp-cam") { Icon "lucide:camera" }
|
|
42
|
+
Stack class("cx-cp-emo") { Icon "lucide:smile" }
|
|
43
|
+
Span "{$placeholder}" class("cx-cp-ph")
|
|
44
|
+
Stack class("cx-cp-tools") { Icon "lucide:paperclip" Icon "lucide:camera" }
|
|
45
|
+
Stack class("cx-cp-igtools") { Icon "lucide:mic" Icon "lucide:image" Icon "lucide:sticker" }
|
|
46
|
+
Stack class("cx-cp-menu") { Icon "lucide:menu" Span "Menú" }
|
|
47
|
+
Stack class("cx-cp-send") { Icon "lucide:send-horizontal" }
|
|
48
|
+
Stack class("cx-cp-mic") { Icon "lucide:mic" }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
part ChatPlayLog() { Stack class("cx-cp-log") aria(hidden: true) { slot } }
|
|
57
|
+
part ChatPlayMsg(text: text, who: text = "out", step: number = 1, time: text = "") {
|
|
58
|
+
Stack class("cx-cp-msg") data(who: $who, step: $step) {
|
|
59
|
+
Span "{$text}"
|
|
60
|
+
when $time != "" { Stack class("cx-cp-time") { Span "{$time}" Icon "lucide:check-check" } }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
part ChatPlayDay(label: text) { Span "{$label}" class("cx-cp-day") }
|
|
64
|
+
# The start of a conversation as Instagram shows it: the person's photo (ChatPlay's `photo`), name, handle and «Ver perfil».
|
|
65
|
+
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") } }
|
|
66
|
+
# A small line under a message: Instagram's «Visto», or who answered («Respondió el asistente de Bruma»).
|
|
67
|
+
part ChatPlayNote(label: text, step: number = 1) { Span "{$label}" class("cx-cp-note") data(step: $step) }
|
|
68
|
+
part ChatPlayButtons(step: number = 1) { Stack class("cx-cp-btns") data(step: $step) { slot } }
|
|
69
|
+
part ChatPlayButton(label: text) { Stack class("cx-cp-btn") { Icon "lucide:reply" Span "{$label}" } }
|
|
70
|
+
part ChatPlayCard(title: text, text: text = "", step: number = 1) {
|
|
71
|
+
Stack class("cx-cp-card") data(step: $step) {
|
|
72
|
+
Stack class("cx-cp-ok") { Icon "lucide:check" }
|
|
73
|
+
Stack class("cx-cp-name") { Span "{$title}" class("cx-cp-ct") Span "{$text}" class("cx-cp-cd") }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
part ChatPlayChips() { Stack class("cx-cp-chips") aria(hidden: true) { slot } }
|
|
77
|
+
part ChatPlayChip(label: text) { Span "{$label}" class("cx-cp-chip") }
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# DayPlan - the owner's day as a list in order: hour, a dot on a line, who and what, a tag on the right. The hours
|
|
2
|
+
# already gone are faded, a red line says what time it is now, and one free slot can fill itself in a loop (a new
|
|
3
|
+
# booking drops in with a green ring and the count steps up) - a picture of «your day keeps itself», not the Agenda.
|
|
4
|
+
# DayPlan(title: "Hoy", date: "viernes 9 de octubre", count: "5 citas", countNext: "6 citas") {
|
|
5
|
+
# DayPlanRow(time: "9:00 AM", title: "Pedro Ruiz", detail: "Corte clásico", tag: "Atendido", state: "done")
|
|
6
|
+
# DayPlanNow(time: "3:12 PM")
|
|
7
|
+
# DayPlanRow(time: "3:30 PM", title: "Laura Gómez", detail: "Corte y barba", tag: "Confirmada", tone: "ok")
|
|
8
|
+
# DayPlanRow(time: "4:30 PM", title: "Carla Méndez", detail: "Corte clásico", tag: "La agendó tu asistente", tone: "ai", state: "fill", free: "Libre")
|
|
9
|
+
# }
|
|
10
|
+
# state: "" | done (gone, faded) | fill (shows `free` dashed, then fills; one per plan). tone: "" | ok | ai.
|
|
11
|
+
# countNext: the count after the fill (empty = no count change). shadcn-for-muten · part · no deps · kit.css (.cx-dp).
|
|
12
|
+
part DayPlan(title: text, date: text = "", count: text = "", countNext: text = "", label: text = "") {
|
|
13
|
+
Stack class("cx-dp cx-play") aria(label: $label) {
|
|
14
|
+
Stack class("cx-dp-top") {
|
|
15
|
+
Span "{$title}" class("cx-dp-t")
|
|
16
|
+
Span "{$date}" class("cx-dp-date")
|
|
17
|
+
when $count != "" {
|
|
18
|
+
Stack class("cx-dp-count") data(next: $countNext != "") { Span "{$count}" class("cx-dp-c1") Span "{$countNext}" class("cx-dp-c2") aria(hidden: true) }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
Stack class("cx-dp-list") aria(role: "list") { slot }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
part DayPlanRow(time: text, title: text, detail: text = "", tag: text = "", tone: text = "", state: text = "", free: text = "") {
|
|
25
|
+
Stack class("cx-dp-row") data(state: $state) aria(role: "listitem") {
|
|
26
|
+
Span "{$time}" class("cx-dp-time")
|
|
27
|
+
Span "" class("cx-dp-dot")
|
|
28
|
+
Stack class("cx-dp-card") {
|
|
29
|
+
when $state == "fill" { Span "{$free}" class("cx-dp-free") aria(hidden: true) }
|
|
30
|
+
Stack class("cx-dp-body") {
|
|
31
|
+
Span "{$title}" class("cx-dp-who")
|
|
32
|
+
Span "{$detail}" class("cx-dp-what")
|
|
33
|
+
when $tag != "" { Span "{$tag}" class("cx-dp-tag") data(tone: $tone) }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
part DayPlanNow(time: text) {
|
|
39
|
+
Stack class("cx-dp-now") aria(hidden: true) { Span "{$time}" Span "" class("cx-dp-now-dot") Span "" class("cx-dp-now-line") }
|
|
40
|
+
}
|
|
@@ -1,8 +1,26 @@
|
|
|
1
|
-
# DeviceFrame - a device around a picture of a product: a phone (
|
|
2
|
-
#
|
|
3
|
-
#
|
|
1
|
+
# DeviceFrame - a device around a picture of a product, drawn like the real thing: a phone (the iPhone 18 Pro, to scale:
|
|
2
|
+
# aluminium band, black border, the side buttons, an optional Dynamic Island) or a browser window (the bar with its three
|
|
3
|
+
# dots and, when given, the address). Put an Image, or any content, in the slot; an Image fills the screen from the top.
|
|
4
|
+
# height crops it (the whole device fades out at the bottom), so a tall screenshot can sit beside a short text. Every
|
|
5
|
+
# measure of the phone follows its width, so it keeps the same proportions at any size. ChatPlay wears this same phone.
|
|
4
6
|
# DeviceFrame { Image "~/shots/store.png" alt("Tu tienda en un teléfono") }
|
|
5
|
-
# DeviceFrame(
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
|
|
7
|
+
# DeviceFrame(island: "on", width: "300px") { Image "~/shots/app.png" alt("La app") }
|
|
8
|
+
# DeviceFrame(kind: "browser", url: "storio.ink/elroble", height: "420px") { Image "~/shots/panel.png" alt("Tu panel") }
|
|
9
|
+
# DeviceFrame(href: "/aurora", label: "Estudio Aurora, una tienda de muestra", shot: "/stores/aurora.webp")
|
|
10
|
+
# width: empty (a phone is 280 px, a browser fills its column) or a width like «320px» or «100%». island: "on" draws the
|
|
11
|
+
# island (leave it off over a page that has its own top bar). kind: phone | browser.
|
|
12
|
+
# href: the whole device is a link there (it lifts on hover); label says where, for a screen reader.
|
|
13
|
+
# shot: a picture of the screen (390 × 844 at twice the pixels) laid as its background. Unlike an Image it is not
|
|
14
|
+
# downloaded until the device comes near the screen (content-visibility), so a long row of phones costs nothing.
|
|
15
|
+
# A name under the phone: wrap it with its spans in `Stack class("cx-mk-store")`.
|
|
16
|
+
# shadcn-for-muten · part · no deps · kit.css (.cx-device).
|
|
17
|
+
part DeviceFrame(kind: text = "phone", height: text = "", width: text = "", url: text = "", island: text = "", href: text = "", label: text = "", shot: text = "") {
|
|
18
|
+
Stack class("cx-device") data(kind: $kind, crop: $height != "", sized: $width != "", island: $island, link: $href != "", shot: $shot != "") style(h: "{$height}", w: "{$width}", shot: "url('{$shot}')") {
|
|
19
|
+
when $url != "" { Span "{$url}" class("cx-device-url") aria(hidden: true) }
|
|
20
|
+
when $href != "" { Link "" -> "{$href}" class("cx-device-link") aria(label: $label) }
|
|
21
|
+
Stack class("cx-device-screen") {
|
|
22
|
+
when $shot != "" { Stack class("cx-device-shot") }
|
|
23
|
+
slot
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
package/registry/drawer.muten
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
# It is the adaptive Modal forced to drawer. The page owns open.
|
|
3
3
|
# Drawer(open: picking, onClose: done, title: "Elige la hora") { … ModalFooter { … } }
|
|
4
4
|
# shadcn-for-muten · part over Modal.
|
|
5
|
-
part Drawer(open: bool, onClose: action, title: text, description: text = "", size: text = "") {
|
|
5
|
+
part Drawer(open: bool, onClose: action, title: text = "", description: text = "", size: text = "") {
|
|
6
6
|
Modal(open: $open, onClose: $onClose, title: $title, description: $description, mode: "drawer", size: $size) { slot }
|
|
7
7
|
}
|