@muten/shadcn 0.0.5 → 0.1.0
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 +61 -6
- package/README.md +57 -4
- package/globals.css +30 -2
- package/kit.css +2763 -0
- package/motion.css +79 -0
- package/nova.css +1387 -0
- package/package.json +10 -2
- package/registry/accordion.muten +11 -10
- package/registry/agenda.js +35 -0
- package/registry/agenda.muten +10 -0
- package/registry/alert-dialog.muten +9 -12
- package/registry/alert.muten +8 -8
- package/registry/app-shell.js +9 -0
- package/registry/app-shell.muten +12 -0
- package/registry/attachment.muten +13 -9
- package/registry/avatar.muten +24 -5
- package/registry/badge.muten +23 -8
- package/registry/band.muten +16 -0
- package/registry/bar-chart.muten +7 -0
- package/registry/bell.muten +3 -0
- package/registry/bento.muten +6 -9
- package/registry/booking.js +21 -0
- package/registry/booking.muten +13 -0
- package/registry/breadcrumb.muten +9 -7
- package/registry/button-group.muten +13 -4
- package/registry/button.muten +21 -8
- package/registry/calendar.js +28 -93
- package/registry/calendar.muten +9 -8
- package/registry/canvas-viewport.js +12 -5
- package/registry/canvas-viewport.muten +9 -9
- package/registry/card-field.muten +4 -0
- package/registry/card.muten +12 -8
- package/registry/carousel.js +5 -36
- package/registry/carousel.muten +12 -5
- package/registry/chatbot.muten +9 -0
- package/registry/checklist.muten +6 -0
- package/registry/choice-list.muten +16 -0
- package/registry/code.js +14 -12
- package/registry/code.muten +6 -7
- package/registry/collapsible.muten +14 -10
- package/registry/color-picker.js +460 -41
- package/registry/color-picker.muten +7 -6
- package/registry/combobox.js +5 -0
- package/registry/combobox.muten +6 -23
- package/registry/command-palette.muten +8 -12
- package/registry/command.js +32 -0
- package/registry/command.muten +32 -11
- package/registry/context-menu.muten +9 -8
- package/registry/count-up.muten +5 -10
- package/registry/date-field.js +5 -0
- package/registry/date-field.muten +7 -0
- package/registry/device-frame.muten +8 -0
- package/registry/dialog.muten +6 -12
- package/registry/dirty-bar.js +19 -0
- package/registry/dirty-bar.muten +13 -0
- package/registry/drawer.muten +6 -11
- package/registry/dropdown-menu.muten +37 -13
- package/registry/empty.muten +11 -7
- package/registry/feature-grid.muten +10 -12
- package/registry/feature-rows.muten +12 -8
- package/registry/field-behavior.js +33 -0
- package/registry/field-behavior.muten +9 -0
- package/registry/field.muten +26 -7
- package/registry/file-input.js +36 -21
- package/registry/file-input.muten +8 -7
- package/registry/fixed-card.muten +25 -0
- package/registry/folder-picker.js +13 -15
- package/registry/folder-picker.muten +5 -8
- package/registry/gradient-blinds.muten +6 -13
- package/registry/hover-card.muten +7 -9
- package/registry/image-upload.js +5 -0
- package/registry/image-upload.muten +8 -0
- package/registry/indicator.muten +5 -0
- package/registry/input-group.muten +27 -4
- package/registry/input-otp.js +8 -35
- package/registry/input-otp.muten +6 -6
- package/registry/input.muten +8 -7
- package/registry/inspector.muten +13 -15
- package/registry/island.js +7 -0
- package/registry/island.muten +5 -0
- package/registry/item.muten +20 -7
- package/registry/kbd.muten +3 -3
- package/registry/kit/board.js +300 -0
- package/registry/kit/collections.js +148 -0
- package/registry/kit/core.js +989 -0
- package/registry/kit/dates.js +106 -0
- package/registry/kit/menus.js +90 -0
- package/registry/kit/overlays.js +114 -0
- package/registry/kit/paint.js +211 -0
- package/registry/kit/qrcode.js +2291 -0
- package/registry/kit/structure.js +393 -0
- package/registry/kit/table.js +198 -0
- package/registry/kit/values.js +179 -0
- package/registry/kit-layer.js +2 -0
- package/registry/kit-layer.muten +5 -0
- package/registry/kit-skeleton.js +24 -0
- package/registry/kit-skeleton.muten +16 -0
- package/registry/kit-widget.js +79 -0
- package/registry/kit-widget.muten +4 -0
- package/registry/link-btn.muten +9 -7
- package/registry/live-stack.js +29 -0
- package/registry/live-stack.muten +7 -0
- package/registry/logo-cloud.muten +6 -11
- package/registry/member-list.muten +6 -0
- package/registry/menubar.muten +10 -14
- package/registry/message-scroller.js +12 -0
- package/registry/message-scroller.muten +8 -4
- package/registry/message.muten +27 -10
- package/registry/messaging.muten +11 -0
- package/registry/modal-behavior.js +56 -0
- package/registry/modal-behavior.muten +4 -0
- package/registry/modal.muten +29 -0
- package/registry/money-field.js +5 -0
- package/registry/money-field.muten +7 -0
- package/registry/nav-button.muten +14 -0
- package/registry/navigation-menu.muten +16 -14
- package/registry/navpill.muten +15 -9
- package/registry/new-appointment.js +8 -0
- package/registry/new-appointment.muten +10 -0
- package/registry/number-field.js +34 -24
- package/registry/number-field.muten +8 -6
- package/registry/order-board.js +6 -0
- package/registry/order-board.muten +21 -0
- package/registry/overlay-behavior.js +13 -0
- package/registry/overlay-behavior.muten +4 -0
- package/registry/pagination-nav.js +6 -0
- package/registry/pagination-nav.muten +8 -0
- package/registry/pagination.muten +12 -9
- package/registry/password-field.muten +3 -0
- package/registry/permission-matrix.muten +6 -0
- package/registry/phone-field.js +5 -0
- package/registry/phone-field.muten +7 -0
- package/registry/phone-shell.js +7 -0
- package/registry/phone-shell.muten +9 -0
- package/registry/pill.muten +6 -5
- package/registry/plans.muten +4 -0
- package/registry/popover.muten +16 -9
- package/registry/pricing.muten +21 -21
- package/registry/progress.muten +5 -5
- package/registry/queue.muten +3 -0
- package/registry/radio-group.muten +16 -8
- package/registry/range-slider.js +9 -24
- package/registry/range-slider.muten +4 -5
- package/registry/resizable.js +34 -27
- package/registry/resizable.muten +16 -5
- package/registry/roving-behavior.js +16 -0
- package/registry/roving-behavior.muten +3 -0
- package/registry/row-menu.js +5 -0
- package/registry/row-menu.muten +5 -0
- package/registry/save-button.muten +11 -0
- package/registry/scale-slider.js +7 -0
- package/registry/scale-slider.muten +9 -0
- package/registry/scroll-area.muten +8 -4
- package/registry/segmented-control.muten +8 -8
- package/registry/separator.muten +3 -3
- package/registry/share-button.js +7 -0
- package/registry/share-button.muten +10 -0
- package/registry/sheet.muten +6 -10
- package/registry/sidebar.muten +23 -15
- package/registry/skeleton.muten +6 -4
- package/registry/slider.js +73 -27
- package/registry/slider.muten +6 -6
- package/registry/sortable-behavior.js +43 -0
- package/registry/sortable-behavior.muten +2 -0
- package/registry/sortable-list.js +10 -26
- package/registry/sortable-list.muten +6 -7
- package/registry/sortable.muten +15 -0
- package/registry/spark.js +14 -0
- package/registry/spark.muten +3 -0
- package/registry/spinner.muten +4 -3
- package/registry/split-button.muten +12 -10
- package/registry/stamp-card.muten +7 -0
- package/registry/stat-band.muten +6 -9
- package/registry/stat.muten +23 -0
- package/registry/status-bar.muten +7 -6
- package/registry/stepper.muten +17 -14
- package/registry/store-qr.js +5 -0
- package/registry/store-qr.muten +5 -0
- package/registry/swatch.muten +8 -8
- package/registry/switch.muten +14 -6
- package/registry/table-view.muten +10 -0
- package/registry/tabs-behavior.js +27 -0
- package/registry/tabs-behavior.muten +3 -0
- package/registry/tabs.muten +19 -10
- package/registry/tag-input.js +4 -27
- package/registry/tag-input.muten +6 -6
- package/registry/text.muten +7 -0
- package/registry/time-field.js +5 -0
- package/registry/time-field.muten +6 -0
- package/registry/timeline.muten +14 -14
- package/registry/toaster.js +34 -22
- package/registry/toaster.muten +10 -8
- package/registry/toggle-group.muten +11 -6
- package/registry/toggle.muten +8 -5
- package/registry/toolbar.muten +11 -6
- package/registry/tooltip.muten +6 -9
- package/registry/tree-view.js +27 -7
- package/registry/tree-view.muten +8 -13
- package/registry/vector-field.muten +9 -8
- package/registry/virtual-list.js +5 -0
- package/registry/virtual-list.muten +6 -0
- package/registry/weekly-hours.muten +4 -0
- package/registry/wordline.muten +6 -0
- package/registry/zoom-control.muten +9 -9
- package/registry.json +868 -424
- package/themes/bloom.css +53 -0
- package/themes/editorial.css +65 -0
- package/themes/noir.css +54 -0
- package/registry/chart.js +0 -34
- package/registry/chart.muten +0 -7
- package/registry/checkbox.muten +0 -8
- package/registry/context-menu.js +0 -34
- package/registry/select.muten +0 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@muten/shadcn",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
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",
|
|
@@ -15,13 +15,21 @@
|
|
|
15
15
|
"registry",
|
|
16
16
|
"registry.json",
|
|
17
17
|
"globals.css",
|
|
18
|
+
"nova.css",
|
|
19
|
+
"kit.css",
|
|
20
|
+
"motion.css",
|
|
21
|
+
"themes",
|
|
18
22
|
"README.md",
|
|
19
23
|
"AGENTS.md"
|
|
20
24
|
],
|
|
21
25
|
"exports": {
|
|
22
26
|
"./globals.css": "./globals.css",
|
|
27
|
+
"./motion.css": "./motion.css",
|
|
28
|
+
"./themes/*": "./themes/*",
|
|
23
29
|
"./registry.json": "./registry.json",
|
|
24
|
-
"./registry/*": "./registry/*"
|
|
30
|
+
"./registry/*": "./registry/*",
|
|
31
|
+
"./nova.css": "./nova.css",
|
|
32
|
+
"./kit.css": "./kit.css"
|
|
25
33
|
},
|
|
26
34
|
"keywords": [
|
|
27
35
|
"muten",
|
package/registry/accordion.muten
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
# Accordion - stacked
|
|
2
|
-
#
|
|
3
|
-
# Accordion { AccordionItem(title: "
|
|
4
|
-
#
|
|
5
|
-
part
|
|
1
|
+
# Accordion - stacked questions; each opens to its content's exact height (200 ms) and the arrow turns instead of
|
|
2
|
+
# swapping icons. The page owns a bool per item and the toggle.
|
|
3
|
+
# Accordion { AccordionItem(title: "¿Puedo cancelar mi cita?", open: a1, onToggle: t1) { Text "Sí, hasta 3 horas antes…" } }
|
|
4
|
+
# size "lg": taller rows and bigger type, for the questions of a landing page.
|
|
5
|
+
# shadcn-for-muten · part · no deps · Nova (.cn-accordion) + kit.css.
|
|
6
|
+
part Accordion(size: text = "default") { Stack class("cn-accordion") data(slot: "accordion", size: $size) { slot } }
|
|
6
7
|
part AccordionItem(title: text, open: bool, onToggle: action) {
|
|
7
|
-
Stack class("accordion-item") {
|
|
8
|
-
Button -> $onToggle
|
|
8
|
+
Stack class("cn-accordion-item") data(slot: "accordion-item") {
|
|
9
|
+
Button -> $onToggle class("cn-accordion-trigger group/accordion-trigger") data(slot: "accordion-trigger") aria(expanded: $open) {
|
|
9
10
|
Span "{$title}"
|
|
10
|
-
Icon "lucide:chevron-down" class("
|
|
11
|
+
Icon "lucide:chevron-down" class("cx-acc-chev")
|
|
11
12
|
}
|
|
12
|
-
Stack class("accordion-
|
|
13
|
-
Stack class("accordion-content") { slot }
|
|
13
|
+
Stack class("cn-accordion-content cx-acc-fold") data(slot: "accordion-content", open: $open) aria(role: "region") {
|
|
14
|
+
Stack class("cn-accordion-content-inner") { slot }
|
|
14
15
|
}
|
|
15
16
|
}
|
|
16
17
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Agenda (muten Custom) - the kit's agenda (day columns per person, week, list; drag to move, edge to resize, the
|
|
2
|
+
// appointment sheet with its lifecycle, payment, move and cancel), from kit/core.js. Everything is data:
|
|
3
|
+
// resources [{id, name}] · hours [{id, days: [1..6], ranges: [[540, 780], [840, 1140]]}] (minutes from midnight)
|
|
4
|
+
// events [{id, resource, day, start "HH:MM", dur, title, subtitle, price, tone, steps, actions, at, payment, contact}]
|
|
5
|
+
// day = "yyyy-mm-dd", or a number of days from today (it lands on the next day that person works)
|
|
6
|
+
// blocks [{id, resource | "all", day, start, dur, reason}] · from / to (hours) · slot (minutes)
|
|
7
|
+
// actions [{id, label, icon, primary}] - the toolbar's buttons (default: block and new); «block» and «new» keep their
|
|
8
|
+
// built-in behaviour, any other id fires the action handler with that id
|
|
9
|
+
// handlers: create(json {resource, day, start}) · change(json [{id, resource, day, start, dur, status}]) · action(id)
|
|
10
|
+
export function mount(el, inputs, on) {
|
|
11
|
+
let latest = inputs;
|
|
12
|
+
import('@muten/shadcn/registry/kit/core.js').then(({ kit, today, addDays }) => {
|
|
13
|
+
const iso = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
14
|
+
const list = (v) => (Array.isArray(v) ? v : []);
|
|
15
|
+
const build = (i) => {
|
|
16
|
+
const hours = Object.fromEntries(list(i.hours).map((h) => [h.id, h]));
|
|
17
|
+
const work = (rid, d) => { const h = hours[rid]; return h && list(h.days).includes(d.getDay()) ? list(h.ranges) : []; };
|
|
18
|
+
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 dayOf = (rid, d) => (typeof d === 'number' || /^-?\d+$/.test(String(d)) ? land(rid, d) : new Date(String(d) + 'T00:00:00'));
|
|
20
|
+
const events = list(i.events).map((e) => ({ tone: 'ok', at: 0, ...e, day: dayOf(e.resource, e.day) }));
|
|
21
|
+
const blocks = list(i.blocks).map((b) => ({ ...b, day: typeof b.day === 'number' || /^-?\d+$/.test(String(b.day)) ? addDays(today, Number(b.day)) : new Date(String(b.day) + 'T00:00:00') }));
|
|
22
|
+
el.innerHTML = '';
|
|
23
|
+
kit.agenda(el, {
|
|
24
|
+
resources: list(i.resources), events, blocks, availability: work,
|
|
25
|
+
actions: list(i.actions).length ? list(i.actions) : null,
|
|
26
|
+
onAction: (id) => on.action?.(id),
|
|
27
|
+
from: Number(i.from) || 9, to: Number(i.to) || 19, slot: Number(i.slot) || 30,
|
|
28
|
+
onCreate: (x) => on.create?.(JSON.stringify({ resource: x.resource, day: iso(x.day), start: x.start })),
|
|
29
|
+
onChange: (evs) => on.change?.(JSON.stringify(evs.map((e) => ({ id: e.id, resource: e.resource, day: iso(e.day), start: e.start, dur: e.dur, status: e.status || '' })))),
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
build(latest);
|
|
33
|
+
});
|
|
34
|
+
return (next) => { latest = next; };
|
|
35
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Agenda - day columns per person, the week, or a list (the phone keeps the day grid and the list). Drag an
|
|
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
|
+
# lifecycle, payment and receipt, write, change the time, cancel with a reason. Block time; create from a free slot.
|
|
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 cita); an id other
|
|
6
|
+
# than block or new fires onAction with it: Agenda(…, actions: @agendaActions, onAction: agendaAction)
|
|
7
|
+
# 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")
|
|
10
|
+
}
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
#
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
1
|
+
# AlertDialog - asks before something that cannot be undone (delete, cancel and refund). It cannot be dismissed by
|
|
2
|
+
# tapping outside: the person answers with one of its two buttons (Esc counts as the safe one). Put them in a
|
|
3
|
+
# ModalFooter: the safe way out (cx-exit) and the action (cx-go, destructive when it destroys).
|
|
4
|
+
# AlertDialog(open: asking, onClose: keep, title: "¿Eliminar el servicio?", description: "Desaparece de tu tienda…") {
|
|
5
|
+
# ModalFooter { Btn(variant: "outline", onClick: keep) class("cx-exit") { Span "Cancelar" } Btn(variant: "destructive", onClick: remove) class("cx-go") { Span "Eliminar" } }
|
|
6
|
+
# }
|
|
7
|
+
# shadcn-for-muten · part over Modal (role alertdialog, dismiss off).
|
|
8
|
+
part AlertDialog(open: bool, onClose: action, title: text, description: text = "") {
|
|
9
|
+
Modal(open: $open, onClose: $onClose, title: $title, description: $description, mode: "dialog", role: "alertdialog", dismiss: "off") { slot }
|
|
10
10
|
}
|
|
11
|
-
part AlertDialogTitle(label: text) { Title "{$label}" h2 class("dialog-title") }
|
|
12
|
-
part AlertDialogDescription(label: text) { Span "{$label}" class("dialog-description") }
|
|
13
|
-
part AlertDialogFooter() { Stack class("dialog-footer") { slot } }
|
package/registry/alert.muten
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# Alert
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
part
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
part
|
|
8
|
-
part
|
|
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: "Tu plan vence en 3 días") AlertDescription(label: "…") AlertAction { Btn(size: "xs", variant: "outline", onClick: renew) { Span "Renovar" } } }
|
|
3
|
+
# variant: default | destructive · tone: "" | info | ok | wait
|
|
4
|
+
# shadcn-for-muten · part · no deps · Nova (.cn-alert) + kit.css (tone).
|
|
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 } }
|
|
6
|
+
part AlertTitle(label: text) { Span "{$label}" class("cn-alert-title") data(slot: "alert-title") }
|
|
7
|
+
part AlertDescription(label: text) { Text "{$label}" class("cn-alert-description") data(slot: "alert-description") }
|
|
8
|
+
part AlertAction() { Stack class("cn-alert-action") data(slot: "alert-action") { slot } }
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// AppShell (muten Custom) - the kit's app structure (kit/structure.js): Sidebar, page header, detail Sheet and the
|
|
2
|
+
// Command (⌘K). inputs: brand, plan, user, email, page, nav, pages, rows, commands, width ("narrow" = a narrow box);
|
|
3
|
+
// handlers navigate(page) · open(id) · action(label).
|
|
4
|
+
export function mount(el, inputs, on) {
|
|
5
|
+
let shell = null, narrow = inputs.width === "narrow";
|
|
6
|
+
const map = (i) => ({ ...i, brand: { name: i.brand, short: i.short, plan: i.plan }, user: { name: i.user, email: i.email } });
|
|
7
|
+
Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/structure.js')]).then(([core, s]) => { shell = s.appShell(el, map(inputs), core, on); shell.narrow(narrow); });
|
|
8
|
+
return (next) => { narrow = next.width === "narrow"; shell?.narrow(narrow); };
|
|
9
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# AppShell - the system's structure on shadcn's Sidebar: store switcher on top, groups with their label and counts,
|
|
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
|
+
# slides from the left. The page header has breadcrumbs, title, description and actions. Tapping a row opens the
|
|
4
|
+
# detail Sheet on the right without leaving the list. Ctrl/⌘ K (or the search button) opens the Command.
|
|
5
|
+
# AppShell(brand: "Barbería El Roble", short: "El Roble", plan: "Plan Pro", user: "Julián Restrepo", email: "julian@elroble.co", page: "Pedidos", nav: @nav, pages: @pages, rows: @rows, commands: @commands, width: width, onNavigate: go, onOpen: openRow, onAction: act)
|
|
6
|
+
# nav {group, icon, label, count} · pages {name, title, description, actions [{label, variant, icon}]} · rows {page, id,
|
|
7
|
+
# title, description, badge, tone, amount, command, sheetTitle, sheetDescription, sheet [{icon, title, description,
|
|
8
|
+
# badge, tone, amount}], timeline [{label, when}], primary, secondary} · commands {icon, label, page}.
|
|
9
|
+
# width "narrow" lays it out as in a narrow box. shadcn-for-muten · Custom (kit/structure.js) · Nova sidebar/sheet/command + kit.css (.cx-app-*).
|
|
10
|
+
part AppShell(brand: text, short: text = "", plan: text = "", user: text, email: text = "", page: text = "", nav: list, pages: list, rows: list, commands: list, empty: text = "", width: text = "wide", onNavigate: action, onOpen: action, onAction: action) {
|
|
11
|
+
Custom AppShell inputs(brand: $brand, short: $short, plan: $plan, user: $user, email: $email, page: $page, nav: $nav, pages: $pages, rows: $rows, commands: $commands, empty: $empty, width: $width) on(navigate: $onNavigate, open: $onOpen, action: $onAction) class("cx-app-host")
|
|
12
|
+
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
# Attachment - a file
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
part
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Stack class("
|
|
8
|
-
|
|
9
|
-
|
|
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
|
+
# tapping it opens the file. kind: file | pdf | image | audio | sheet.
|
|
3
|
+
# Attachment(name: "comprobante-1042.pdf", size: "182 KB", kind: "pdf", to: "/…")
|
|
4
|
+
# shadcn-for-muten · part · no deps · kit.css (.cx-attach).
|
|
5
|
+
part Attachment(name: text, size: text = "", kind: text = "file", to: text = "#") {
|
|
6
|
+
Link -> "{$to}" class("cx-attach") data(slot: "attachment", kind: $kind) {
|
|
7
|
+
Stack class("cx-attach-ic") {
|
|
8
|
+
when $kind == "pdf" { Icon "lucide:file-text" }
|
|
9
|
+
when $kind == "image" { Icon "lucide:image" }
|
|
10
|
+
when $kind == "audio" { Icon "lucide:file-audio" }
|
|
11
|
+
when $kind == "sheet" { Icon "lucide:file-spreadsheet" }
|
|
12
|
+
when $kind == "file" { Icon "lucide:file" }
|
|
10
13
|
}
|
|
14
|
+
Stack class("cx-attach-text") { Span "{$name}" class("cx-attach-name") Span "{$size}" class("cx-attach-size") }
|
|
11
15
|
}
|
|
12
16
|
}
|
package/registry/avatar.muten
CHANGED
|
@@ -1,7 +1,26 @@
|
|
|
1
|
-
# Avatar - a
|
|
2
|
-
#
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
# Avatar - three sizes (24, 32, 40). Without a photo the initials take a stable tone from the name (hue), mixed with
|
|
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 «+N», and AvatarImage for a photo.
|
|
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 personas más") }
|
|
6
|
+
# Avatar(name: "El Roble", shape: "square") - a thing, not a person
|
|
7
|
+
# size: sm | default | lg · status: "" | online · shadcn-for-muten · part · no deps · Nova + kit.css (.cx-av).
|
|
8
|
+
part Avatar(name: text, size: text = "default", shape: text = "", status: text = "") {
|
|
9
|
+
Stack class("cx-av-wrap") data(name: $name) aria(label: $name) {
|
|
10
|
+
Stack class("cn-avatar group/avatar cx-av") data(slot: "avatar", size: $size, shape: $shape) style(h: "{hue($name)}") {
|
|
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: "En línea") }
|
|
13
|
+
}
|
|
6
14
|
}
|
|
7
15
|
}
|
|
16
|
+
part AvatarImage(src: text, name: text, size: text = "default", shape: text = "") {
|
|
17
|
+
Stack class("cx-av-wrap") data(name: $name) {
|
|
18
|
+
Stack class("cn-avatar group/avatar") data(slot: "avatar", size: $size, shape: $shape) {
|
|
19
|
+
Image "{$src}" alt("{$name}") class("cn-avatar-image") data(slot: "avatar-image")
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
part AvatarGroup() { Stack class("cn-avatar-group group/avatar-group") data(slot: "avatar-group") { slot } }
|
|
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: "Atiende hoy · 4 citas") { Avatar(…) }
|
|
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,9 +1,24 @@
|
|
|
1
|
-
# Badge - a
|
|
2
|
-
#
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
# Badge - a short label. `StatusBadge` is the system's state: a dot and the word, never a tinted pill.
|
|
2
|
+
# Badge(label: "Nuevo") Badge(label: "PRO", variant: "default")
|
|
3
|
+
# StatusBadge(label: "Confirmada", tone: "ok") StatusBadge(label: "Por revisar", tone: "wait")
|
|
4
|
+
# Badge variant: default | secondary | destructive | outline | ghost | link (default: secondary)
|
|
5
|
+
# StatusBadge tone: ok | wait | info | bad | "" (neutral)
|
|
6
|
+
# shadcn-for-muten · part · no deps · Nova (.cn-badge) + kit.css (status).
|
|
7
|
+
part Badge(label: text, variant: text = "secondary") { Span "{$label}" class("cn-badge cn-badge-variant-{$variant} group/badge") data(slot: "badge") }
|
|
8
|
+
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
|
+
# ToneBadge - a state with its tone, tinted, with or without a dot: new | ok | wait | bad | pro (PRO uses --selected).
|
|
10
|
+
# ToneBadge(label: "Confirmado", tone: "ok") ToneBadge(label: "Pendiente", tone: "wait", dot: "dot")
|
|
11
|
+
part ToneBadge(label: text, tone: text, dot: text = "") {
|
|
12
|
+
Stack class("cn-badge group/badge cx-tone-{$tone}") data(slot: "badge") {
|
|
13
|
+
when $dot != "" { Span "" class("cx-dot") }
|
|
14
|
+
Span "{$label}"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
# Chip - a value the person can take away with its ×; it fades out (no jump). The page owns the list.
|
|
18
|
+
# each tags as t { Chip(label: t.name, onRemove: removeTag) } with action removeTag(name: text) mutates tags { … }
|
|
19
|
+
part Chip(label: text, onRemove: action) {
|
|
20
|
+
Stack class("cn-badge cn-badge-variant-secondary cx-chip") data(slot: "badge") {
|
|
21
|
+
Span "{$label}"
|
|
22
|
+
Button -> $onRemove($label) class("cn-button cn-button-variant-ghost cn-button-size-icon-xs cx-chip-x") aria(label: "Quitar {$label}") { Icon "lucide:x" }
|
|
23
|
+
}
|
|
9
24
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
# 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: "Sin conexión. Guardamos tus cambios al volver.") { Icon "lucide:info" }
|
|
4
|
+
# Band(open: planEnding, tone: "warn", text: "Tu plan Pro vence en 3 días.", action: "Renovar", onAction: renew) { Icon "lucide:clock" }
|
|
5
|
+
# tone: muted | warn. shadcn-for-muten · part · no deps · kit.css (.cx-band).
|
|
6
|
+
part Band(open: bool, tone: text = "muted", text: text, action: text = "", onAction: action) {
|
|
7
|
+
Stack class("cx-band-slot") data(open: $open) {
|
|
8
|
+
Stack class("cx-band-fold") {
|
|
9
|
+
Stack class("cx-band") data(tone: $tone) aria(role: "status") {
|
|
10
|
+
slot
|
|
11
|
+
Span "{$text}" class("cx-band-text")
|
|
12
|
+
when $action != "" { Button "{$action}" -> $onAction class("cn-button cn-button-variant-outline cn-button-size-xs group/button") }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# BarChart - bars that always fill their height, a period switch (14 días / 30 días). periods: {label, values} - the
|
|
2
|
+
# labels are the last N days up to today. money "on" formats as money.
|
|
3
|
+
# BarChart(title: "Ventas", periods: @sales, money: "on")
|
|
4
|
+
# shadcn-for-muten · part over KitWidget (kit core).
|
|
5
|
+
part BarChart(title: text, periods: list, money: text = "") {
|
|
6
|
+
Custom KitWidget inputs(name: "barChart", title: $title, periods: $periods, money: $money) class("cx-chart-card")
|
|
7
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# Bell - the system's notifications: tap to see them, mark all read. Bell(items: @notices) items: {icon, tone, title, desc, time, unread}
|
|
2
|
+
# shadcn-for-muten · part over KitWidget (kit core).
|
|
3
|
+
part Bell(items: list) { Custom KitWidget inputs(name: "bell", items: $items) class("cx-bell-host") }
|
package/registry/bento.muten
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
# BentoGallery - a
|
|
2
|
-
# Each BentoCell
|
|
3
|
-
# BentoGallery {
|
|
4
|
-
#
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
# shadcn-for-muten · block · no deps · uses .bento-gallery / .bento-cell from globals.css.
|
|
8
|
-
part BentoGallery() { Stack class("bento-gallery") { slot } }
|
|
9
|
-
part BentoCell() { Stack class("bento-cell") { slot } }
|
|
1
|
+
# BentoGallery - a gallery of tiles on a grid of fixed-height rows, so tiles of different widths always line up.
|
|
2
|
+
# Each BentoCell says how many of the six columns and how many rows it takes (on a phone they stack).
|
|
3
|
+
# BentoGallery { BentoCell(span: 4, rows: 2) { … } BentoCell(span: 2) { … } BentoCell(span: 2) { … } }
|
|
4
|
+
# shadcn-for-muten · parts · no deps · kit.css (.cx-bento).
|
|
5
|
+
part BentoGallery(rowHeight: text = "160px") { Stack class("cx-bento") style(row: "{$rowHeight}") { slot } }
|
|
6
|
+
part BentoCell(span: number = 2, rows: number = 1) { Stack class("cx-bento-cell") style(span: "{$span}", rows: "{$rows}") { slot } }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Booking (muten Custom) - the kit's booking picker (kit/structure.js): week strip and hours in one piece.
|
|
2
|
+
// inputs: week [{days, hours}], taken [{day, hours}], wait, cta, demo; handler select("yyyy-mm-dd HH:MM").
|
|
3
|
+
// demo "on" plays it by itself (a day, then an hour, in turn) until the first touch; it never presses the button.
|
|
4
|
+
export function mount(el, inputs, on) {
|
|
5
|
+
let timer = 0, stopped = false;
|
|
6
|
+
Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/structure.js')]).then(([core, s]) => {
|
|
7
|
+
s.booking(el, inputs, core, on);
|
|
8
|
+
if (inputs.demo !== 'on' || matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
|
9
|
+
let turn = 0;
|
|
10
|
+
const open = (sel) => [...el.querySelectorAll(sel)].filter((b) => !b.disabled && !b.hasAttribute('data-full'));
|
|
11
|
+
const pick = (sel) => { const all = open(sel); return all.length ? all[turn % all.length] : null; };
|
|
12
|
+
const play = () => {
|
|
13
|
+
if (stopped) return;
|
|
14
|
+
pick('.cx-day')?.click();
|
|
15
|
+
timer = setTimeout(() => { if (stopped) return; pick('.cx-hour')?.click(); turn++; timer = setTimeout(play, 2600); }, 1500);
|
|
16
|
+
};
|
|
17
|
+
el.addEventListener('pointerdown', () => { stopped = true; clearTimeout(timer); }, { once: true, capture: true });
|
|
18
|
+
timer = setTimeout(play, 900);
|
|
19
|
+
});
|
|
20
|
+
return () => {};
|
|
21
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Booking - week and hours in one piece. The dot under a day says something is left; closed days are disabled; a
|
|
2
|
+
# full day shows its empty state with a jump to the next day with hours. Changing day shows a skeleton with the exact
|
|
3
|
+
# shape of the hours while they load (nothing jumps in height). What is chosen goes on --selected.
|
|
4
|
+
# Booking(week: @week, taken: @taken, wait: 380, onSelect: book)
|
|
5
|
+
# week {days [0..6], hours ["09:00", …]} = what the shop offers each weekday (a weekday in no row is closed);
|
|
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 «Continuar».
|
|
8
|
+
# demo "on": it plays itself for a landing (a day, then an hour, again and again) until someone touches it; never
|
|
9
|
+
# confirms, and stays still under reduced motion.
|
|
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 = "Continuar", demo: text = "", onSelect: action) {
|
|
12
|
+
Custom Booking inputs(week: $week, taken: $taken, wait: $wait, cta: $cta, demo: $demo) on(select: $onSelect) class("cx-book-host")
|
|
13
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
# Breadcrumb
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
|
|
5
|
-
part
|
|
6
|
-
part
|
|
7
|
-
part
|
|
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 «…» (BreadcrumbEllipsis).
|
|
3
|
+
# Breadcrumb { BreadcrumbLink(label: "El Roble", to: "/") BreadcrumbSeparator() BreadcrumbLink(label: "Pedidos", to: "/orders") BreadcrumbSeparator() BreadcrumbPage(label: "#1042") }
|
|
4
|
+
# shadcn-for-muten · parts · no deps · Nova (.cn-breadcrumb-*).
|
|
5
|
+
part Breadcrumb(label: text = "Migas") { Nav class("cx-breadcrumb") data(slot: "breadcrumb") aria(label: $label) { Stack class("cn-breadcrumb-list cx-bc-list") data(slot: "breadcrumb-list") aria(role: "list") { slot } } }
|
|
6
|
+
part BreadcrumbLink(label: text, to: text) { Link "{$label}" -> "{$to}" class("cn-breadcrumb-link cx-bc-item") data(slot: "breadcrumb-link") }
|
|
7
|
+
part BreadcrumbPage(label: text) { Span "{$label}" class("cn-breadcrumb-page cx-bc-item") data(slot: "breadcrumb-page") aria(current: "page") }
|
|
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 = "Más páginas") { Stack class("cn-breadcrumb-ellipsis cx-bc-more") data(slot: "breadcrumb-ellipsis", name: $label) aria(label: $label) { Icon "lucide:ellipsis" } }
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
# ButtonGroup -
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
|
|
1
|
+
# ButtonGroup - buttons that belong together, sharing their borders (shadcn Nova). Buttons, an Input or a
|
|
2
|
+
# ButtonGroupText go inside; ButtonGroupSeparator splits two filled buttons. Groups can nest (they keep a gap).
|
|
3
|
+
# ButtonGroup { Btn(variant: "outline", onClick: prev) { Icon "lucide:chevron-left" } Btn(variant: "outline", onClick: next) { Icon "lucide:chevron-right" } }
|
|
4
|
+
# ButtonGroup(orientation: "vertical") { … }
|
|
5
|
+
# ButtonGroup { ButtonGroupText(label: "https://") Input(value: site) }
|
|
6
|
+
# orientation: horizontal | vertical. shadcn-for-muten · part · no deps · Nova (.cn-button-group).
|
|
7
|
+
part ButtonGroup(orientation: text = "horizontal", label: text = "") {
|
|
8
|
+
Stack class("cn-button-group cn-button-group-orientation-{$orientation}") data(slot: "button-group", orientation: $orientation) aria(role: "group", label: $label) { slot }
|
|
9
|
+
}
|
|
10
|
+
part ButtonGroupText(label: text) { Span "{$label}" class("cn-button-group-text") data(slot: "button-group-text") }
|
|
11
|
+
part ButtonGroupSeparator(orientation: text = "vertical") {
|
|
12
|
+
Stack class("cn-separator cn-separator-{$orientation} cn-button-group-separator") data(slot: "button-group-separator", orientation: $orientation) aria(role: "separator")
|
|
13
|
+
}
|
package/registry/button.muten
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
|
-
# Button (part name Btn - `Button` is a muten primitive, so a part can't reuse that name) -
|
|
2
|
-
#
|
|
3
|
-
# Btn(
|
|
4
|
-
# Btn(variant: "outline", onClick: cancel) { Icon "lucide:x"
|
|
5
|
-
# variant:
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
|
|
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; an Icon nudges on hover (trailing moves on, leading lifts, lone grows).
|
|
3
|
+
# Btn(onClick: save) { Span "Guardar" }
|
|
4
|
+
# Btn(variant: "outline", size: "sm", onClick: cancel) { Icon "lucide:x" Span "Cancelar" }
|
|
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 "Marcar llegada" Icon "lucide:chevron-right" } - full width (a card's primary)
|
|
7
|
+
# variant: default | outline | secondary | ghost | destructive | link (default: default)
|
|
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
|
+
# xl and 2xl are for big surfaces (a landing, a sign-in) and match the fields' xl / 2xl.
|
|
10
|
+
# tone: "" | info | ok | wait | bad | note - a tinted icon button (a pending thing on a card)
|
|
11
|
+
# 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
|
+
# 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 }
|
|
15
|
+
}
|
|
16
|
+
# TriggerBtn - a Btn that only opens what sits next to it (a Popover, a DropdownMenu, a SplitButton's menu): it has no
|
|
17
|
+
# action of its own, so opening never runs something else. Same variants and sizes as Btn.
|
|
18
|
+
# DropdownMenu { DropdownTrigger { TriggerBtn { Span "Opciones" Icon "lucide:chevron-down" } } DropdownContent { … } }
|
|
19
|
+
part TriggerBtn(variant: text = "outline", size: text = "default", label: text = "") {
|
|
20
|
+
Button class("cn-button cn-button-variant-{$variant} cn-button-size-{$size} group/button") data(slot: "button", name: $label) aria(label: $label) { slot }
|
|
21
|
+
}
|
package/registry/calendar.js
CHANGED
|
@@ -1,94 +1,29 @@
|
|
|
1
|
-
// Calendar
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
let vy = anchor.getFullYear();
|
|
30
|
-
let vm = anchor.getMonth();
|
|
31
|
-
|
|
32
|
-
const emit = () => {
|
|
33
|
-
const out = mode === 'range' ? selected.map(iso).join('/') : mode === 'multiple' ? selected.map(iso).join(',') : (selected[0] ? iso(selected[0]) : '');
|
|
34
|
-
if (handlers.select) handlers.select(out);
|
|
35
|
-
};
|
|
36
|
-
const isSel = (d) => selected.some((s) => same(s, d));
|
|
37
|
-
const inRange = (d) => { if (mode !== 'range' || selected.length < 2) return false; const x = num(d), a = num(selected[0]), b = num(selected[1]); return x > Math.min(a, b) && x < Math.max(a, b); };
|
|
38
|
-
const pick = (d) => {
|
|
39
|
-
if (mode === 'single') selected = [d];
|
|
40
|
-
else if (mode === 'multiple') { const i = selected.findIndex((s) => same(s, d)); if (i >= 0) selected.splice(i, 1); else selected.push(d); }
|
|
41
|
-
else { if (selected.length !== 1) selected = [d]; else { const a = selected[0]; selected = num(d) < num(a) ? [d, a] : [a, d]; } }
|
|
42
|
-
render(); emit();
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const monthGrid = (y, m) => {
|
|
46
|
-
const wrap = document.createElement('div'); wrap.className = 'calendar-month';
|
|
47
|
-
const cap = document.createElement('div'); cap.className = 'calendar-caption';
|
|
48
|
-
if (dropdown) {
|
|
49
|
-
const ms = document.createElement('select'); ms.className = 'calendar-select';
|
|
50
|
-
MONTHS.forEach((name, i) => { const o = document.createElement('option'); o.value = String(i); o.textContent = name; if (i === m) o.selected = true; ms.appendChild(o); });
|
|
51
|
-
ms.addEventListener('change', () => { vm = Number(ms.value); render(); });
|
|
52
|
-
const ys = document.createElement('select'); ys.className = 'calendar-select';
|
|
53
|
-
for (let yy = y - 10; yy <= y + 10; yy++) { const o = document.createElement('option'); o.value = String(yy); o.textContent = String(yy); if (yy === y) o.selected = true; ys.appendChild(o); }
|
|
54
|
-
ys.addEventListener('change', () => { vy = Number(ys.value); render(); });
|
|
55
|
-
cap.appendChild(ms); cap.appendChild(ys);
|
|
56
|
-
} else {
|
|
57
|
-
const l = document.createElement('div'); l.className = 'calendar-label'; l.textContent = MONTHS[m] + ' ' + y; cap.appendChild(l);
|
|
58
|
-
}
|
|
59
|
-
wrap.appendChild(cap);
|
|
60
|
-
|
|
61
|
-
const grid = document.createElement('div'); grid.className = 'calendar-grid';
|
|
62
|
-
for (const d of DOW) { const c = document.createElement('div'); c.className = 'calendar-dow'; c.textContent = d; grid.appendChild(c); }
|
|
63
|
-
const first = new Date(y, m, 1).getDay();
|
|
64
|
-
const days = new Date(y, m + 1, 0).getDate();
|
|
65
|
-
for (let i = 0; i < first; i++) grid.appendChild(document.createElement('div'));
|
|
66
|
-
for (let day = 1; day <= days; day++) {
|
|
67
|
-
const d = new Date(y, m, day);
|
|
68
|
-
const cell = document.createElement('button'); cell.type = 'button'; cell.className = 'calendar-day'; cell.textContent = String(day);
|
|
69
|
-
if (isSel(d)) cell.classList.add('calendar-day-selected');
|
|
70
|
-
else if (inRange(d)) cell.classList.add('calendar-day-range');
|
|
71
|
-
else if (same(d, today)) cell.classList.add('calendar-day-today');
|
|
72
|
-
cell.addEventListener('click', () => pick(d));
|
|
73
|
-
grid.appendChild(cell);
|
|
74
|
-
}
|
|
75
|
-
wrap.appendChild(grid);
|
|
76
|
-
return wrap;
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
function render() {
|
|
80
|
-
el.innerHTML = '';
|
|
81
|
-
const SVG = (p) => '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + p + '</svg>';
|
|
82
|
-
const prev = document.createElement('button'); prev.type = 'button'; prev.className = 'calendar-nav-prev'; prev.innerHTML = SVG('<path d="m15 18-6-6 6-6"/>'); // lucide chevron-left
|
|
83
|
-
const next = document.createElement('button'); next.type = 'button'; next.className = 'calendar-nav-next'; next.innerHTML = SVG('<path d="m9 18 6-6-6-6"/>'); // lucide chevron-right
|
|
84
|
-
prev.addEventListener('click', () => { vm--; if (vm < 0) { vm = 11; vy--; } render(); });
|
|
85
|
-
next.addEventListener('click', () => { vm++; if (vm > 11) { vm = 0; vy++; } render(); });
|
|
86
|
-
el.appendChild(prev); el.appendChild(next);
|
|
87
|
-
const wrap = document.createElement('div'); wrap.className = 'calendar-months';
|
|
88
|
-
for (let i = 0; i < months; i++) { let yy = vy, mm = vm + i; while (mm > 11) { mm -= 12; yy++; } wrap.appendChild(monthGrid(yy, mm)); }
|
|
89
|
-
el.appendChild(wrap);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
render();
|
|
93
|
-
return (n) => { readSelected(n.selected); render(); };
|
|
1
|
+
// Calendar (muten Custom) - the kit's month grid (one or two months, a day or a range), from kit/core.js.
|
|
2
|
+
// inputs: selected "yyyy-mm-dd" · "from/to" · or relative to today: "today", "0", "-6/0" (days from today)
|
|
3
|
+
// mode "single" | "range" · months 1 | 2 · min "today" | "yyyy-mm-dd"
|
|
4
|
+
// presets "reports" → Hoy · Últimos 7 días · Este mes · Mes pasado beside a range
|
|
5
|
+
// caption "on" → the sentence under it («Elegiste el jueves 8 de octubre» / «Del 2 oct al 8 oct · 7 días»)
|
|
6
|
+
// handlers: select("yyyy-mm-dd" | "from/to"). Same component as the reference artifact's Calendar.
|
|
7
|
+
export function mount(el, inputs, on) {
|
|
8
|
+
let cal = null;
|
|
9
|
+
import('@muten/shadcn/registry/kit/core.js').then(({ kit, ui, today, addDays, DAYLONG, MON, fmtDay }) => {
|
|
10
|
+
const iso = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
11
|
+
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
|
+
const mode = inputs.mode === 'range' ? 'range' : 'single';
|
|
13
|
+
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 ? `Elegiste el ${DAYLONG[d.getDay()].toLowerCase()} ${d.getDate()} de ${MON[d.getMonth()]}` : '');
|
|
15
|
+
const sayRange = (r) => (r?.[0] && r?.[1] ? `Del ${fmtDay(r[0])} al ${fmtDay(r[1])} · ${Math.round((r[1] - r[0]) / 864e5) + 1} días` : r?.[0] ? `Desde el ${fmtDay(r[0])}: elige el último día` : '');
|
|
16
|
+
const say = (v) => (mode === 'range' ? sayRange(v) : sayOne(v));
|
|
17
|
+
const PRESETS = [['Hoy', () => [today, today]], ['Últimos 7 días', () => [addDays(today, -6), today]], ['Este mes', () => [new Date(today.getFullYear(), today.getMonth(), 1), today]], ['Mes pasado', () => [new Date(today.getFullYear(), today.getMonth() - 1, 1), new Date(today.getFullYear(), today.getMonth(), 0)]]];
|
|
18
|
+
const withPresets = mode === 'range' && inputs.presets === 'reports';
|
|
19
|
+
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
|
+
const out = el.querySelector('[data-out]');
|
|
21
|
+
const value = decode(inputs.selected);
|
|
22
|
+
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,
|
|
24
|
+
onChange: (v) => { if (out) out.textContent = say(v); el.querySelectorAll('[data-preset]').forEach((b) => b.setAttribute('aria-pressed', 'false')); emit(v); } });
|
|
25
|
+
if (out) out.textContent = say(value);
|
|
26
|
+
el.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); });
|
|
27
|
+
});
|
|
28
|
+
return () => {};
|
|
94
29
|
}
|