@muten/shadcn 0.0.7 → 0.1.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 +47 -0
- package/README.md +47 -0
- package/kit.css +2767 -0
- package/motion.css +31 -1
- package/nova.css +1387 -0
- package/package.json +6 -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 +395 -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 +872 -406
- package/registry/context-menu.js +0 -34
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// CommandBehavior (muten Custom) - the Command's list: as you type it keeps the items whose label (or keywords)
|
|
2
|
+
// holds what you wrote, accent-insensitive; a group with nothing left hides, and «empty» shows when nothing matches.
|
|
3
|
+
// The arrows move the highlight, Enter picks it, the pointer highlights what it rests on.
|
|
4
|
+
export function mount(el) {
|
|
5
|
+
const root = el.parentElement;
|
|
6
|
+
const input = root.querySelector('.cx-command-input input, input');
|
|
7
|
+
const fold = (s) => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
|
|
8
|
+
const items = () => [...root.querySelectorAll('.cx-cmd-item')];
|
|
9
|
+
const visible = () => items().filter((x) => !x.hidden);
|
|
10
|
+
let hl = 0;
|
|
11
|
+
const mark = () => { const v = visible(); hl = Math.max(0, Math.min(hl, v.length - 1)); items().forEach((x) => { const on = x === v[hl]; x.toggleAttribute('data-selected', on); x.setAttribute('aria-selected', on); }); v[hl]?.scrollIntoView({ block: 'nearest' }); if (input) input.setAttribute('aria-activedescendant', v[hl]?.id || ''); };
|
|
12
|
+
const filter = () => {
|
|
13
|
+
const q = fold(input?.value.trim());
|
|
14
|
+
items().forEach((x, i) => { if (!x.id) x.id = `cmd-${Math.random().toString(36).slice(2, 7)}-${i}`; x.hidden = !!q && !fold(`${x.textContent} ${x.dataset.keywords || ''}`).includes(q); });
|
|
15
|
+
root.querySelectorAll('.cx-cmd-group').forEach((g) => { g.hidden = !g.querySelector('.cx-cmd-item:not([hidden])'); });
|
|
16
|
+
root.querySelectorAll('.cx-cmd-sep').forEach((s) => { s.hidden = !!q; });
|
|
17
|
+
const empty = root.querySelector('.cx-command-empty'); if (empty) empty.hidden = visible().length > 0;
|
|
18
|
+
hl = 0; mark();
|
|
19
|
+
};
|
|
20
|
+
input?.setAttribute('role', 'combobox'); input?.setAttribute('aria-expanded', 'true'); input?.setAttribute('autocomplete', 'off');
|
|
21
|
+
input?.addEventListener('input', filter);
|
|
22
|
+
root.addEventListener('keydown', (e) => {
|
|
23
|
+
const v = visible(); if (!v.length) return;
|
|
24
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); hl = (hl + (e.key === 'ArrowDown' ? 1 : -1) + v.length) % v.length; mark(); }
|
|
25
|
+
if (e.key === 'Enter' && e.target === input) { e.preventDefault(); v[hl]?.click(); }
|
|
26
|
+
});
|
|
27
|
+
root.addEventListener('pointermove', (e) => { const it = e.target.closest('.cx-cmd-item'); if (!it) return; const k = visible().indexOf(it); if (k >= 0 && k !== hl) { hl = k; mark(); } });
|
|
28
|
+
items().forEach((x) => { x.tabIndex = -1; });
|
|
29
|
+
new MutationObserver(() => filter()).observe(root.querySelector('.cx-command-list') || root, { childList: true, subtree: true });
|
|
30
|
+
filter();
|
|
31
|
+
return () => {};
|
|
32
|
+
}
|
package/registry/command.muten
CHANGED
|
@@ -1,16 +1,37 @@
|
|
|
1
|
-
# Command - a
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
1
|
+
# Command - a list you search and pick from with the keyboard (shadcn's cmdk). Typing keeps what matches, accent-
|
|
2
|
+
# insensitive; arrows move, Enter picks; groups with nothing left hide; «empty» says when nothing matches. The page
|
|
3
|
+
# owns the query text (read it to search the server too); each item runs its own action.
|
|
4
|
+
# Command(query: q, placeholder: "Cliente, pedido o acción…") {
|
|
5
|
+
# CommandGroup(label: "Acciones") { CommandItem(label: "Nueva cita", shortcut: "N", onSelect: create) { Icon "lucide:plus" } }
|
|
6
|
+
# CommandSeparator()
|
|
7
|
+
# CommandGroup(label: "Clientes") { CommandItem(label: "Laura Méndez", keywords: "300 112 4410", onSelect: openLaura) { Icon "lucide:user" } }
|
|
5
8
|
# }
|
|
6
|
-
#
|
|
7
|
-
part Command(
|
|
8
|
-
|
|
9
|
+
# Inside a Modal it is the ⌘K palette: CommandPalette. shadcn-for-muten · parts + Custom (CommandBehavior) · Nova (.cn-command*).
|
|
10
|
+
part Command(query: text, placeholder: text = "Busca…", empty: text = "Nada coincide.") {
|
|
11
|
+
Stack class("cn-command cx-command") data(slot: "command") {
|
|
12
|
+
Stack class("cn-command-input-wrapper cx-command-input") data(slot: "command-input-wrapper") {
|
|
13
|
+
Icon "lucide:search" class("cn-command-input-icon")
|
|
14
|
+
SearchField bind($query) "{$placeholder}" class("cn-command-input") data(slot: "command-input")
|
|
15
|
+
}
|
|
16
|
+
Stack class("cn-command-list cx-command-list") data(slot: "command-list") aria(role: "listbox") {
|
|
17
|
+
slot
|
|
18
|
+
Span "{$empty}" class("cn-command-empty cx-command-empty") data(slot: "command-empty")
|
|
19
|
+
}
|
|
20
|
+
CommandBehavior()
|
|
21
|
+
}
|
|
22
|
+
}
|
|
9
23
|
part CommandGroup(label: text) {
|
|
10
|
-
Stack class("
|
|
11
|
-
Span "{$label}" class("
|
|
24
|
+
Stack class("cn-command-group cx-cmd-group") data(slot: "command-group") aria(role: "group", label: $label) {
|
|
25
|
+
Span "{$label}" class("cx-cmd-label")
|
|
26
|
+
slot
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
part CommandItem(label: text, onSelect: action, shortcut: text = "", keywords: text = "") {
|
|
30
|
+
Button -> $onSelect class("cn-command-item cx-cmd-item") data(slot: "command-item", keywords: $keywords) aria(role: "option") {
|
|
12
31
|
slot
|
|
32
|
+
Span "{$label}" class("cx-cmd-text")
|
|
33
|
+
Span "{$shortcut}" class("cn-command-shortcut") data(slot: "command-shortcut")
|
|
13
34
|
}
|
|
14
35
|
}
|
|
15
|
-
part
|
|
16
|
-
part
|
|
36
|
+
part CommandSeparator() { Stack class("cn-command-separator cx-cmd-sep") data(slot: "command-separator") aria(role: "separator") }
|
|
37
|
+
part CommandBehavior() { Custom CommandBehavior class("cx-cmd-behavior") }
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
#
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
1
|
+
# ContextMenu - the menu of a zone, opened with a right click (or a long press on a phone, or the keyboard's menu key)
|
|
2
|
+
# right where the pointer is. The zone and the menu go inside; the menu uses DropdownContent and its items.
|
|
3
|
+
# ContextMenu {
|
|
4
|
+
# ContextTrigger { Stack class("…") { Span "Clic derecho aquí" } }
|
|
5
|
+
# DropdownContent { DropdownItem(label: "Duplicar", onSelect: dup) { Icon "lucide:copy" } … }
|
|
6
|
+
# }
|
|
7
|
+
# shadcn-for-muten · parts + Custom (OverlayBehavior) · Nova (dropdown classes) + kit.css.
|
|
8
|
+
part ContextMenu() { Stack class("cx-ov cx-ov-zone") data(slot: "context-menu") { slot OverlayBehavior(kind: "context") } }
|
|
9
|
+
part ContextTrigger(label: text = "Menú de esta zona") { Stack class("cx-ov-trigger cx-ctx-zone") data(slot: "context-menu-trigger") aria(label: $label) { slot } }
|
package/registry/count-up.muten
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
# CountUp -
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
|
|
6
|
-
# Span "trees planted" class("text-sm text-muted-foreground")
|
|
7
|
-
# }
|
|
8
|
-
# The `CountUp(to: ...)` part below is a thin convenience for the unstyled/inherit case (wrap it in a styled Stack).
|
|
9
|
-
# shadcn-for-muten · Custom · no deps · host JS: src/components/CountUp.js.
|
|
10
|
-
part CountUp(to: text) { Custom CountUp inputs(to: $to) }
|
|
1
|
+
# CountUp - a number that counts up from 0 the first time it comes into view, keeping its exact format («2.4M»,
|
|
2
|
+
# «94 %», «12,400», «$1.2M»). It takes the size of what holds it. Off with reduced motion.
|
|
3
|
+
# Stat(label: "citas al mes") { CountUp(to: "12,400") }
|
|
4
|
+
# shadcn-for-muten · Custom · host JS count-up.js.
|
|
5
|
+
part CountUp(to: text) { Custom CountUp inputs(to: $to) class("cx-countup") }
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// DateField (muten Custom) - the kit's dateField (kit/dates.js).
|
|
2
|
+
export function mount(el, inputs, on) {
|
|
3
|
+
Promise.all([import('@muten/shadcn/registry/kit/core.js'), import('@muten/shadcn/registry/kit/dates.js')]).then(([core, m]) => m.dateField(el, inputs, core, on));
|
|
4
|
+
return () => {};
|
|
5
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# DateField - a field-like button with the long date; it opens the day / month / year wheels in the adaptive modal.
|
|
2
|
+
# min "today" turns the past off (the wheel greys those days and «Listo» waits).
|
|
3
|
+
# DateField(label: "Fecha", value: "today", min: "today", onChange: setDay) → "yyyy-mm-dd"
|
|
4
|
+
# shadcn-for-muten · Custom (kit dates) · host JS date-field.js.
|
|
5
|
+
part DateField(label: text = "Fecha", value: text = "today", min: text = "", years: number = 2, onChange: action) {
|
|
6
|
+
Custom DateField inputs(label: $label, value: $value, min: $min, years: $years) on(change: $onChange) class("cx-df-host")
|
|
7
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# DeviceFrame - a device around a picture of a product: a phone (rounded bezel) or a browser window (a bar with
|
|
2
|
+
# three dots). Put an Image, or any content, in the slot; an Image fills the screen from the top. height crops it
|
|
3
|
+
# (the rest fades out at the bottom), so a tall screenshot can sit beside a short text.
|
|
4
|
+
# DeviceFrame { Image "~/shots/store.png" alt("Tu tienda en un teléfono") }
|
|
5
|
+
# DeviceFrame(kind: "browser", height: "420px") { Image "~/shots/panel.png" alt("Tu panel") }
|
|
6
|
+
# width: empty (a phone is 280 px, a browser fills its column) or a width like «320px».
|
|
7
|
+
# kind: phone | browser. shadcn-for-muten · part · no deps · kit.css (.cx-device).
|
|
8
|
+
part DeviceFrame(kind: text = "phone", height: text = "", width: text = "") { Stack class("cx-device") data(kind: $kind, crop: $height != "", sized: $width != "") style(h: "{$height}", w: "{$width}") { slot } }
|
package/registry/dialog.muten
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
# Dialog - a
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
# shadcn-for-muten · part
|
|
5
|
-
part Dialog(open: bool, onClose: action) {
|
|
6
|
-
|
|
7
|
-
Button -> $onClose class("dialog-backdrop")
|
|
8
|
-
Stack class("dialog-content") { slot }
|
|
9
|
-
}
|
|
1
|
+
# Dialog - a window in the middle of the screen for one task (edit, confirm a detail), on every width. It is the
|
|
2
|
+
# adaptive Modal forced to dialog; on a phone prefer Modal (it becomes a Drawer). The page owns open.
|
|
3
|
+
# Dialog(open: editing, onClose: done, title: "Editar servicio", description: "…") { … ModalFooter { … } }
|
|
4
|
+
# shadcn-for-muten · part over Modal.
|
|
5
|
+
part Dialog(open: bool, onClose: action, title: text, description: text = "", size: text = "") {
|
|
6
|
+
Modal(open: $open, onClose: $onClose, title: $title, description: $description, mode: "dialog", size: $size) { slot }
|
|
10
7
|
}
|
|
11
|
-
part DialogTitle(label: text) { Title "{$label}" h2 class("dialog-title") }
|
|
12
|
-
part DialogDescription(label: text) { Span "{$label}" class("dialog-description") }
|
|
13
|
-
part DialogFooter() { Stack class("dialog-footer") { slot } }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// DirtyBar (muten Custom) - the kit's «unsaved changes» bar over the form it sits in (kit/core.js kit.dirtyBar).
|
|
2
|
+
// «Descartar» restores what muten owns: inputs get their saved value (and an input event, so muten's bind follows),
|
|
3
|
+
// switches that moved are toggled back by a click (so the page's action runs). The weekly hours restore themselves.
|
|
4
|
+
export function mount(el, inputs, on) {
|
|
5
|
+
import('@muten/shadcn/registry/kit/core.js').then(({ kit }) => {
|
|
6
|
+
const form = el.closest('.cx-set-form') || el.parentElement;
|
|
7
|
+
const fields = () => [...form.querySelectorAll('input, select, textarea, [role=switch], .cx-time-btn, .cx-sel-btn')];
|
|
8
|
+
kit.dirtyBar(form, { onSave: () => { form.dispatchEvent(new Event('kit-saved')); on.save?.(); } });
|
|
9
|
+
form.addEventListener('discard', (e) => {
|
|
10
|
+
const saved = e.detail || [];
|
|
11
|
+
fields().forEach((x, i) => {
|
|
12
|
+
if (x.closest('.cx-wh-host') || i >= saved.length) return;
|
|
13
|
+
if (x.getAttribute('role') === 'switch') { if (String(x.getAttribute('aria-checked')) !== String(saved[i])) x.click(); return; }
|
|
14
|
+
if (x.matches('input, textarea, select') && x.value !== saved[i]) { x.value = saved[i]; x.dispatchEvent(new Event('input', { bubbles: true })); }
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
return () => {};
|
|
19
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# DirtyBar - «Tienes cambios sin guardar»: a floating footer of the form it sits in. It appears when any field,
|
|
2
|
+
# switch or time differs from the last save, «Descartar» puts everything back, «Guardar» saves in three moments.
|
|
3
|
+
# Put it as the LAST child of a SettingsForm.
|
|
4
|
+
# SettingsForm { SettingsSection(title: "Tu negocio", description: "…") { Field(…) { Input(…) } } DirtyBar(onSave: save) }
|
|
5
|
+
# shadcn-for-muten · part + Custom (DirtyBar, kit core) · kit.css (.cx-dirty, .cx-set-form).
|
|
6
|
+
part SettingsForm() { Stack class("cx-set-form") { slot } }
|
|
7
|
+
part SettingsSection(title: text, description: text = "") {
|
|
8
|
+
Section class("cx-set-sec") {
|
|
9
|
+
Stack class("cx-vstack cx-gap-2") { Span "{$title}" class("cx-text") data(variant: "heading") Text "{$description}" class("cx-text") data(variant: "muted") }
|
|
10
|
+
Stack class("cx-vstack cx-gap-14") { slot }
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
part DirtyBar(onSave: action) { Custom DirtyBar on(save: $onSave) class("cx-dirty-host") }
|
package/registry/drawer.muten
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
# Drawer - a
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Stack class("drawer-content") {
|
|
8
|
-
Stack class("drawer-handle")
|
|
9
|
-
slot
|
|
10
|
-
}
|
|
11
|
-
}
|
|
1
|
+
# Drawer - a sheet that rises from the bottom, with a handle you drag down to close (the phone's way), on every width.
|
|
2
|
+
# It is the adaptive Modal forced to drawer. The page owns open.
|
|
3
|
+
# Drawer(open: picking, onClose: done, title: "Elige la hora") { … ModalFooter { … } }
|
|
4
|
+
# shadcn-for-muten · part over Modal.
|
|
5
|
+
part Drawer(open: bool, onClose: action, title: text = "", description: text = "", size: text = "") {
|
|
6
|
+
Modal(open: $open, onClose: $onClose, title: $title, description: $description, mode: "drawer", size: $size) { slot }
|
|
12
7
|
}
|
|
@@ -1,15 +1,39 @@
|
|
|
1
|
-
#
|
|
2
|
-
#
|
|
3
|
-
# DropdownMenu
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
# DropdownMenu - a list of actions that opens from a button (shadcn Nova). Arrows, Home/End and the first letter move;
|
|
2
|
+
# Enter picks; a picked action closes it, a check or radio item stays open. Trigger and content go inside:
|
|
3
|
+
# DropdownMenu {
|
|
4
|
+
# DropdownTrigger { TriggerBtn { Span "Opciones" Icon "lucide:chevron-down" } }
|
|
5
|
+
# DropdownContent(align: "start") {
|
|
6
|
+
# DropdownLabel(label: "Pedido")
|
|
7
|
+
# DropdownItem(label: "Editar", shortcut: "⌘E", onSelect: edit) { Icon "lucide:pencil" }
|
|
8
|
+
# DropdownSeparator()
|
|
9
|
+
# DropdownCheckItem(label: "Avisarme", on: notify, onToggle: toggleNotify)
|
|
10
|
+
# DropdownItem(label: "Eliminar", tone: "destructive", onSelect: remove) { Icon "lucide:trash-2" }
|
|
11
|
+
# }
|
|
12
|
+
# }
|
|
13
|
+
# The same items and content serve ContextMenu and Menubar. shadcn-for-muten · parts + Custom (OverlayBehavior) · Nova.
|
|
14
|
+
part DropdownMenu() { Stack class("cx-ov") data(slot: "dropdown-menu") { slot OverlayBehavior(kind: "menu") } }
|
|
15
|
+
part DropdownTrigger() { Stack class("cx-ov-trigger") data(slot: "dropdown-menu-trigger") { slot } }
|
|
16
|
+
part DropdownContent(align: text = "start", width: text = "12rem") { Stack class("cn-dropdown-menu-content cx-ov-content cx-menu") data(slot: "dropdown-menu-content", align: $align) style(w: "{$width}") { slot } }
|
|
17
|
+
part DropdownItem(label: text, onSelect: action, shortcut: text = "", tone: text = "") {
|
|
18
|
+
Button -> $onSelect class("cn-dropdown-menu-item group/dropdown-menu-item cx-menu-item") data(slot: "dropdown-menu-item", variant: $tone) aria(role: "menuitem") {
|
|
19
|
+
slot
|
|
20
|
+
Span "{$label}" class("cx-menu-label")
|
|
21
|
+
Span "{$shortcut}" class("cn-dropdown-menu-shortcut") data(slot: "dropdown-menu-shortcut")
|
|
11
22
|
}
|
|
12
23
|
}
|
|
13
|
-
part
|
|
14
|
-
|
|
15
|
-
|
|
24
|
+
part DropdownCheckItem(label: text, on: bool, onToggle: action, shortcut: text = "") {
|
|
25
|
+
Button -> $onToggle class("cn-dropdown-menu-checkbox-item cx-menu-item cx-menu-check") data(slot: "dropdown-menu-checkbox-item") aria(role: "menuitemcheckbox", checked: $on) {
|
|
26
|
+
Span "{$label}" class("cx-menu-label")
|
|
27
|
+
Span "{$shortcut}" class("cn-dropdown-menu-shortcut")
|
|
28
|
+
Stack class("cn-dropdown-menu-item-indicator cx-menu-mark") { Icon "lucide:check" }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
part DropdownRadioItem(value: text, current: text, onSelect: action, label: text) {
|
|
32
|
+
Button -> $onSelect($value) class("cn-dropdown-menu-radio-item cn-dropdown-menu-checkbox-item cx-menu-item cx-menu-check") data(slot: "dropdown-menu-radio-item") aria(role: "menuitemradio", checked: $value == $current) {
|
|
33
|
+
Span "{$label}" class("cx-menu-label")
|
|
34
|
+
Span "" class("cn-dropdown-menu-item-indicator cx-menu-dot")
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
part DropdownLabel(label: text) { Span "{$label}" class("cn-dropdown-menu-label") data(slot: "dropdown-menu-label") }
|
|
38
|
+
part DropdownSeparator() { Stack class("cn-dropdown-menu-separator") data(slot: "dropdown-menu-separator") aria(role: "separator") }
|
|
39
|
+
part DropdownGroup(label: text = "") { Stack class("cx-menu-group") aria(role: "group", label: $label) { slot } }
|
package/registry/empty.muten
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
# Empty -
|
|
2
|
-
#
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
# Empty - what a list says when it has nothing yet: the icon in the slot, a title, a description. Put the way out
|
|
2
|
+
# (a button) right after it.
|
|
3
|
+
# Empty(title: "Nada por aquí", description: "Cuando te escriban, aparece aquí.") { Icon "lucide:inbox" }
|
|
4
|
+
# shadcn-for-muten · part · no deps · Nova (.cn-empty).
|
|
5
|
+
part Empty(title: text, description: text = "") {
|
|
6
|
+
Stack class("cn-empty") data(slot: "empty") {
|
|
7
|
+
Stack class("cn-empty-header") data(slot: "empty-header") {
|
|
8
|
+
Stack class("cn-empty-media cn-empty-media-icon") data(slot: "empty-icon", variant: "icon") { slot }
|
|
9
|
+
Span "{$title}" class("cn-empty-title") data(slot: "empty-title")
|
|
10
|
+
Text "{$description}" class("cn-empty-description") data(slot: "empty-description")
|
|
11
|
+
}
|
|
8
12
|
}
|
|
9
13
|
}
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
# FeatureGrid - a
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Title "{$title}" h3 class("feature-card-title")
|
|
12
|
-
Text "{$desc}" class("feature-card-desc")
|
|
1
|
+
# FeatureGrid - what a product does, as cards in a grid that wraps (three, two or one per row by the width). Each
|
|
2
|
+
# FeatureCard has its icon in the slot, a title and a line.
|
|
3
|
+
# FeatureGrid { FeatureCard(title: "Agenda", description: "Citas por persona y recordatorios.") { Icon "lucide:calendar" } … }
|
|
4
|
+
# shadcn-for-muten · parts · no deps · kit.css (.cx-features).
|
|
5
|
+
part FeatureGrid(columns: number = 3) { Stack class("cx-features") style(cols: "{$columns}") { slot } }
|
|
6
|
+
part FeatureCard(title: text, description: text) {
|
|
7
|
+
Stack class("cx-feature") {
|
|
8
|
+
Stack class("cx-feature-ic") { slot }
|
|
9
|
+
Title "{$title}" h3 class("cx-feature-t")
|
|
10
|
+
Text "{$description}" class("cx-feature-d")
|
|
13
11
|
}
|
|
14
12
|
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
# FeatureRows - a
|
|
2
|
-
#
|
|
3
|
-
# FeatureRows {
|
|
4
|
-
#
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
# FeatureRows - a list of numbered rows split by hairlines: the number, a title and its explanation in three
|
|
2
|
+
# columns on a wide screen, one under the other on a phone. For steps, a schedule, a «how it works».
|
|
3
|
+
# FeatureRows { FeatureRow(index: "01", title: "Crea tu tienda", description: "Nombre, rubro y ciudad en un minuto.") … }
|
|
4
|
+
# shadcn-for-muten · parts · no deps · kit.css (.cx-frows).
|
|
5
|
+
part FeatureRows() { Stack class("cx-frows") aria(role: "list") { slot } }
|
|
6
|
+
part FeatureRow(index: text, title: text, description: text = "") {
|
|
7
|
+
Stack class("cx-frow") aria(role: "listitem") {
|
|
8
|
+
Span "{$index}" class("cx-frow-i")
|
|
9
|
+
Title "{$title}" h3 class("cx-frow-t")
|
|
10
|
+
Text "{$description}" class("cx-frow-d")
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// FieldBehavior (muten Custom) - the reference artifact's field rules, on the muten field it sits in. The value stays
|
|
2
|
+
// muten's (bind): every change here dispatches an input event so the page's state follows.
|
|
3
|
+
export function mount(el, inputs) {
|
|
4
|
+
const field = el.closest('.cn-field, .cn-input-group') || el.parentElement;
|
|
5
|
+
const input = field.querySelector('input, textarea');
|
|
6
|
+
if (!input) return () => {};
|
|
7
|
+
const set = (v) => { input.value = v; input.dispatchEvent(new Event('input', { bubbles: true })); };
|
|
8
|
+
const kind = inputs.kind;
|
|
9
|
+
if (kind === 'money') {
|
|
10
|
+
const group = (d) => d.replace(/^0+(?=\d)/, '').replace(/\B(?=(\d{3})+(?!\d))/g, '.');
|
|
11
|
+
input.addEventListener('focus', () => set(input.value.replace(/\D/g, '')));
|
|
12
|
+
input.addEventListener('blur', () => set(group(input.value.replace(/\D/g, ''))));
|
|
13
|
+
set(group(input.value.replace(/\D/g, '')));
|
|
14
|
+
}
|
|
15
|
+
if (kind === 'email') {
|
|
16
|
+
// the Field draws its description after its slot, so it is looked up when needed, not at mount
|
|
17
|
+
let help = null;
|
|
18
|
+
const ok = (v) => /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v);
|
|
19
|
+
const check = () => { const desc = field.querySelector('.cn-field-description'); if (desc && help === null) help = desc.textContent; const bad = input.value.trim() !== '' && !ok(input.value.trim()); input.setAttribute('aria-invalid', String(bad)); field.toggleAttribute('data-invalid', bad); if (desc) { desc.classList.add('cx-msg'); desc.textContent = bad ? inputs.message || 'Revisa el correo.' : help; } };
|
|
20
|
+
input.addEventListener('blur', check);
|
|
21
|
+
input.addEventListener('input', () => { if (field.hasAttribute('data-invalid') && ok(input.value.trim())) check(); });
|
|
22
|
+
}
|
|
23
|
+
if (kind === 'search') input.addEventListener('keydown', (e) => { if (e.key === 'Escape' && input.value) { e.preventDefault(); set(''); } });
|
|
24
|
+
if (kind === 'count') {
|
|
25
|
+
const max = Number(inputs.max) || 0; if (max) input.maxLength = max;
|
|
26
|
+
requestAnimationFrame(() => { const desc = field.querySelector('.cn-field-description');
|
|
27
|
+
if (desc) { desc.classList.add('cx-count-line'); const help = desc.textContent; desc.innerHTML = ''; const a = document.createElement('span'); a.textContent = help; const n = document.createElement('span'); desc.append(a, n);
|
|
28
|
+
const paint = () => { n.textContent = max ? `${input.value.length}/${max}` : String(input.value.length); desc.toggleAttribute('data-near', max && input.value.length >= max * 0.9); };
|
|
29
|
+
input.addEventListener('input', paint); paint(); }
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return () => {};
|
|
33
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# FieldBehavior - what a field does that is not markup, put INSIDE its Field (after the control):
|
|
2
|
+
# kind "money" → separates thousands when leaving the field (25000 → 25.000)
|
|
3
|
+
# kind "email" → checks the address when leaving the field (not while typing); the error replaces the help text
|
|
4
|
+
# in the same place (nothing jumps). message = the error.
|
|
5
|
+
# kind "search" → Esc clears it.
|
|
6
|
+
# kind "count" → «71/160» at the end of the description line (needs maxlength via `max`).
|
|
7
|
+
# Field(label: "Correo para avisos", description: "Te llegan las reservas nuevas.") { Input(value: mail) FieldBehavior(kind: "email", message: "Falta algo: revisa que sea como nombre@dominio.co") }
|
|
8
|
+
# shadcn-for-muten · Custom · host JS is field-behavior.js.
|
|
9
|
+
part FieldBehavior(kind: text, message: text = "", max: number = 0) { Custom FieldBehavior inputs(kind: $kind, message: $message, max: $max) class("cx-field-behavior") }
|
package/registry/field.muten
CHANGED
|
@@ -1,7 +1,26 @@
|
|
|
1
|
-
# Field
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
# Field - a labelled control: the label, the control (slot), a description under it, an error when there is one.
|
|
2
|
+
# Field(label: "Nombre") { Input(value: name, placeholder: "Corte clásico") }
|
|
3
|
+
# Field(label: "Descripción", description: "Se ve en tu tienda.") { TextArea(value: desc) }
|
|
4
|
+
# Two short fields side by side: FieldRow { Field(…) { … } Field(…) { … } } (one column on a phone).
|
|
5
|
+
# error: a message that replaces the description in the same place (nothing jumps) and marks the field invalid.
|
|
6
|
+
# orientation: vertical | horizontal.
|
|
7
|
+
# FieldSet - sets the size and the look of every field inside it, the kit's own too (PasswordField, PhoneField…):
|
|
8
|
+
# FieldSet(size: "2xl", variant: "filled") { Field(label: "Correo") { … } PasswordField(label: "Contraseña") }
|
|
9
|
+
# FieldSeparator - a line with a word in the middle, between two ways in: FieldSeparator(label: "o con tu correo")
|
|
10
|
+
# FieldHead - a label with an action beside it: FieldHead(label: "Contraseña") { LinkBtn(to: "/recover", variant: "link", size: "sm") { Span "¿La olvidaste?" } }
|
|
11
|
+
# shadcn-for-muten · part · no deps · Nova (.cn-field) + kit.css.
|
|
12
|
+
part Field(label: text = "", description: text = "", error: text = "", orientation: text = "vertical") {
|
|
13
|
+
Stack class("cn-field cn-field-orientation-{$orientation} group/field") data(slot: "field", orientation: $orientation, invalid: $error != "") {
|
|
14
|
+
Span "{$label}" class("cn-label") data(slot: "label")
|
|
15
|
+
slot
|
|
16
|
+
when $error == "" { Text "{$description}" class("cn-field-description") data(slot: "field-description") }
|
|
17
|
+
when $error != "" { Text "{$error}" class("cn-field-error") data(slot: "field-error") aria(role: "alert") }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
part FieldRow() { Stack class("cx-2col") { slot } }
|
|
21
|
+
part FieldLabel(label: text) { Span "{$label}" class("cn-label") data(slot: "label") }
|
|
22
|
+
part FieldDescription(label: text) { Text "{$label}" class("cn-field-description") data(slot: "field-description") }
|
|
23
|
+
part FieldError(label: text) { Text "{$label}" class("cn-field-error") data(slot: "field-error") aria(role: "alert") }
|
|
24
|
+
part FieldSet(size: text = "xl", variant: text = "filled") { Stack class("cx-fieldset") data(fields: $size, look: $variant) { slot } }
|
|
25
|
+
part FieldSeparator(label: text) { Stack class("cn-field-separator") data(slot: "field-separator") { Stack class("cn-separator cn-separator-horizontal") data(slot: "separator", orientation: "horizontal") aria(role: "separator") Span "{$label}" class("cn-field-separator-content") data(slot: "field-separator-content") } }
|
|
26
|
+
part FieldHead(label: text) { Stack class("cx-field-head") { Span "{$label}" class("cn-label") data(slot: "label") slot } }
|
package/registry/file-input.js
CHANGED
|
@@ -1,23 +1,38 @@
|
|
|
1
|
-
// FileInput
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
export function mount(el, inputs,
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
// FileInput (muten Custom) - a drop zone for files: tap to pick, drag and drop, or paste. inputs: label, hint,
|
|
2
|
+
// accept, multiple ("on"), maxMb. handlers: files(names joined by ", "). What was picked is listed under the zone
|
|
3
|
+
// with its size and a × to take it away; a file of the wrong type or too heavy is refused there, with the reason.
|
|
4
|
+
export function mount(el, inputs, on) {
|
|
5
|
+
const esc = (s) => String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]);
|
|
6
|
+
const multiple = inputs.multiple === 'on', maxMb = Number(inputs.maxMb) || 10, accept = String(inputs.accept || '');
|
|
7
|
+
el.innerHTML = `<div class="cx-drop cx-file-drop" tabindex="0" role="button" aria-label="${esc(inputs.label || 'Subir archivo')}">
|
|
8
|
+
<input type="file" hidden ${accept ? `accept="${esc(accept)}"` : ''} ${multiple ? 'multiple' : ''}>
|
|
9
|
+
<div class="cx-drop-empty"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg><b>${esc(inputs.label || 'Sube un archivo')}</b><span>${esc(inputs.hint || 'Toca, arrastra o pega aquí')}</span></div>
|
|
10
|
+
</div>
|
|
11
|
+
<p class="cx-note cx-file-msg" aria-live="polite"></p>
|
|
12
|
+
<div data-slot="item-group" class="cn-item-group cx-file-list" role="list"></div>`;
|
|
13
|
+
const zone = el.querySelector('.cx-drop'), input = el.querySelector('input'), list = el.querySelector('.cx-file-list'), msg = el.querySelector('.cx-file-msg');
|
|
14
|
+
let files = [];
|
|
15
|
+
const size = (b) => (b < 1024 ? `${b} B` : b < 1048576 ? `${Math.round(b / 1024)} KB` : `${(b / 1048576).toFixed(1)} MB`);
|
|
16
|
+
const fits = (f) => { if (!accept) return true; return accept.split(',').map((a) => a.trim()).some((a) => (a.endsWith('/*') ? f.type.startsWith(a.slice(0, -1)) : a.startsWith('.') ? f.name.toLowerCase().endsWith(a.toLowerCase()) : f.type === a)); };
|
|
17
|
+
const paint = () => {
|
|
18
|
+
list.innerHTML = files.map((f, i) => `<div data-slot="item" class="cn-item cn-item-variant-outline cn-item-size-sm group/item" role="listitem"><div class="cn-item-media cn-item-media-variant-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/></svg></div><div class="cn-item-content"><div class="cn-item-title cx-ellipsis">${esc(f.name)}</div><p class="cn-item-description">${size(f.size)}</p></div><div class="cn-item-actions"><button type="button" class="cn-button cn-button-variant-ghost cn-button-size-icon-xs" data-drop="${i}" aria-label="Quitar ${esc(f.name)}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button></div></div>`).join('');
|
|
19
|
+
on.files?.(files.map((f) => f.name).join(', '));
|
|
16
20
|
};
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const take = (fileList) => {
|
|
22
|
+
const incoming = Array.from(fileList || []); if (!incoming.length) return;
|
|
23
|
+
const bad = incoming.filter((f) => !fits(f)), heavy = incoming.filter((f) => fits(f) && f.size > maxMb * 1048576);
|
|
24
|
+
const good = incoming.filter((f) => fits(f) && f.size <= maxMb * 1048576);
|
|
25
|
+
msg.textContent = bad.length ? `${bad[0].name} no es de un tipo que se pueda subir aquí.` : heavy.length ? `${heavy[0].name} pesa más de ${maxMb} MB.` : '';
|
|
26
|
+
files = multiple ? files.concat(good) : good.slice(0, 1).length ? good.slice(0, 1) : files;
|
|
27
|
+
paint();
|
|
28
|
+
};
|
|
29
|
+
zone.addEventListener('click', () => input.click());
|
|
30
|
+
zone.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); input.click(); } });
|
|
31
|
+
input.addEventListener('change', () => { take(input.files); input.value = ''; });
|
|
32
|
+
zone.addEventListener('dragover', (e) => { e.preventDefault(); zone.dataset.over = ''; });
|
|
33
|
+
zone.addEventListener('dragleave', () => delete zone.dataset.over);
|
|
34
|
+
zone.addEventListener('drop', (e) => { e.preventDefault(); delete zone.dataset.over; take(e.dataTransfer?.files); });
|
|
35
|
+
zone.addEventListener('paste', (e) => take(e.clipboardData?.files));
|
|
36
|
+
list.addEventListener('click', (e) => { const b = e.target.closest('[data-drop]'); if (!b) return; files.splice(+b.dataset.drop, 1); paint(); });
|
|
37
|
+
return () => {};
|
|
23
38
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
#
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
# FileInput - a drop zone for any file: tap to pick, drag and drop, or paste. Under it, the picked files with their
|
|
2
|
+
# size and a × each; a file of the wrong type or too heavy is refused in the same place, with the reason. For a
|
|
3
|
+
# photo that has to be cropped and shrunk before uploading, use ImageUpload.
|
|
4
|
+
# FileInput(label: "Sube el comprobante", hint: "PDF o imagen · hasta 5 MB", accept: "image/*,.pdf", maxMb: 5, onFiles: setFiles)
|
|
5
|
+
# FileInput(label: "Documentos", multiple: "on", onFiles: setFiles)
|
|
6
|
+
# onFiles gets the names, joined by ", ". shadcn-for-muten · Custom · kit.css (.cx-drop) · host JS file-input.js.
|
|
7
|
+
part FileInput(label: text = "Sube un archivo", hint: text = "Toca, arrastra o pega aquí", accept: text = "", multiple: text = "", maxMb: number = 10, onFiles: action) {
|
|
8
|
+
Custom FileInput inputs(label: $label, hint: $hint, accept: $accept, multiple: $multiple, maxMb: $maxMb) on(files: $onFiles) class("cx-file-host")
|
|
8
9
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# FixedCard - a card that never grows with the data: at most three rows and a «Ver todas» that opens the full list
|
|
2
|
+
# (in the adaptive Modal). Empty, it keeps the same shape. Rows are Row3 (time · who and what · its state).
|
|
3
|
+
# FixedCard(title: "Citas de hoy", description: "Jueves · 14 en total") {
|
|
4
|
+
# Row3(time: "09:00", title: "Andrés Rojas", subtitle: "Corte clásico") { ToneBadge(label: "Confirmada", tone: "ok") }
|
|
5
|
+
# }
|
|
6
|
+
# …then its CardFooter { Btn(variant: "outline", size: "sm", onClick: openAll) class("cx-fill") { Span "Ver todas (14)" } }
|
|
7
|
+
# EmptyMini - the empty line inside it: EmptyMini(text: "Entregaste todo.") { Icon "lucide:circle-check" }
|
|
8
|
+
# shadcn-for-muten · part · no deps · kit.css (.cx-fixed-card, .cx-row3, .cx-empty-mini).
|
|
9
|
+
part FixedCard(title: text, description: text = "") {
|
|
10
|
+
Section class("cn-card group/card cx-fixed-card") data(slot: "card") {
|
|
11
|
+
Stack class("cn-card-header group/card-header") data(slot: "card-header") {
|
|
12
|
+
Span "{$title}" class("cn-card-title") data(slot: "card-title")
|
|
13
|
+
Text "{$description}" class("cn-card-description") data(slot: "card-description")
|
|
14
|
+
}
|
|
15
|
+
slot
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
part Row3(time: text, title: text, subtitle: text = "") {
|
|
19
|
+
Stack class("cx-row3") {
|
|
20
|
+
Span "{$time}" class("cx-time")
|
|
21
|
+
Stack class("cx-row3-who") { Span "{$title}" class("cx-row3-t") Span "{$subtitle}" class("cx-row3-s") }
|
|
22
|
+
slot
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
part EmptyMini(text: text) { Stack class("cx-empty-mini") { slot Span "{$text}" } }
|