@muten/shadcn 0.2.1 → 0.2.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/kit.css CHANGED
@@ -366,6 +366,13 @@ button.cn-item { width: 100%; font: inherit; text-align: left; cursor: pointer;
366
366
  /* entering is slower than leaving: it comes up from 6 px lower at 97 %, and goes in about 0.6 of the time */
367
367
  .cx-modal[data-mode="dialog"][data-state="open"], .cx-modal-root[data-open="true"] > .cx-modal[data-mode="dialog"] { transition: transform var(--t-modal) var(--ease-out), opacity calc(var(--t-modal) * .75) var(--ease-out); }
368
368
  .cx-modal[data-mode="dialog"][data-state="open"] { transform: translate(-50%, -50%); opacity: 1; }
369
+ /* anchor = a card beside what was tapped (ModalBehavior places it); the backdrop stays to close on a tap outside, but
370
+ shows nothing: the agenda behind is the context */
371
+ .cx-modal[data-mode="anchor"] { width: min(24rem, calc(100% - 2rem)); max-height: min(38rem, calc(100dvh - 2rem)); border-radius: calc(var(--radius) * 1.4); overflow: hidden;
372
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--foreground) 10%, transparent), 0 24px 60px -20px rgb(20 16 40 / .38);
373
+ transform: scale(.96); opacity: 0; transition: transform calc(var(--t-modal) * .5) ease, opacity calc(var(--t-modal) * .4) ease; }
374
+ .cx-modal-root[data-open="true"] > .cx-modal[data-mode="anchor"] { transform: none; opacity: 1; transition: transform var(--t-pop, 180ms) var(--ease-out), opacity var(--t-pop, 180ms) var(--ease-out); }
375
+ .cx-modal-root:has(> .cx-modal[data-mode="anchor"]) > .cx-overlay { background: transparent; backdrop-filter: none; }
369
376
  /* teléfono = Drawer desde abajo, arrastrable */
370
377
  .cx-modal[data-mode="drawer"] { left: 0; right: 0; bottom: 0; margin-inline: auto; width: min(100%, 34rem); max-height: calc(100dvh - 3rem); border-radius: calc(var(--radius) * 1.8) calc(var(--radius) * 1.8) 0 0; overflow: hidden;
371
378
  transform: translateY(100%); transition: transform 450ms var(--ease-drawer); touch-action: none; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@muten/shadcn",
3
- "version": "0.2.1",
3
+ "version": "0.2.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",
@@ -72,6 +72,9 @@ export function mount(el, inputs, on) {
72
72
  };
73
73
  map = data;
74
74
  const send = (name, value) => on[name]?.(typeof value === 'string' ? value : JSON.stringify(value));
75
+ // WHERE A CITA IS ON SCREEN ("x,y,w,h"), so the app can open it beside itself (Modal mode "anchor"): the shown chip
76
+ // or row that carries its id ('' when none is visible, e.g. opened from a command)
77
+ const whereIs = (id) => { const box = [...el.querySelectorAll(`[data-ev="${CSS.escape(String(id))}"]`)].map((x) => x.getBoundingClientRect()).find((r) => r.width > 0 && r.height > 0); return box ? [box.left, box.top, box.width, box.height].map(Math.round).join(',') : ''; };
75
78
  const day1 = (x) => ({ ...x, day: x.day instanceof Date ? iso(x.day) : x.day });
76
79
  el.innerHTML = '';
77
80
  api = kit.agenda(el, {
@@ -83,7 +86,7 @@ export function mount(el, inputs, on) {
83
86
  resize: inputs.resize === 'off' ? 'off' : 'on', moveAcross: inputs.moveAcross === 'off' ? 'off' : 'on', hour12: yes(inputs.hour12), timeZone: inputs.timeZone || '', phoneStrip: inputs.phoneStrip === 'dots' ? 'dots' : '', phoneView: ['list', 'day'].includes(inputs.phoneView) ? inputs.phoneView : '', blockScope: inputs.blockScope === 'all' ? 'all' : 'each',
84
87
  onAction: (id) => send('action', id),
85
88
  onCreate: (x) => send('create', day1(x)),
86
- 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,
89
+ 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 || '', at: whereIs(e.id) })) : null,
87
90
  onBlock: on.block ? (b) => send('block', day1(b)) : null,
88
91
  onMove: (m) => send('move', day1(m)),
89
92
  onRange: (r) => send('range', { from: iso(r.from), to: iso(r.to), view: r.view }),
@@ -35,6 +35,7 @@ part AgendaDemo(resources: list, hours: list, events: list, blocks: list, action
35
35
  # a appointment takes its resource plus every id in `with`. overlap "never" | "allow" overrides all capacities.
36
36
  # resize "off" · moveAcross "off" (a backend that only moves day and time) · hour12 "yes" (3:00 PM) · timeZone (the shop's today and now) · phoneView "list" · phoneStrip "dots" (the phone's week: a dot per appointment in its colour) · blockScope "all" · placing [{…}]
37
37
  # (the dashed appointment while the app's new-cita sheet is open) · events take tags [words] and photo; resources role, hideEmpty
38
+ # onOpen's json also carries `at` ("x,y,w,h", where the cita is on screen): give it to a Modal's `anchor` (mode "anchor")
38
39
  # events take by "agent" (made by the app's assistant): agentMark (an image url) goes before their name, and agentLabel
39
40
  # ("Booked by your assistant") under it on the phone's list and in what a screen reader says
40
41
  # AgendaLive(resources: @team, hours: @hours, events: @citas, blocks: @blocks, free: @free, queue: @queue, closed: @closed,
@@ -16,9 +16,27 @@ export function mount(el, inputs, on) {
16
16
  const placeFooter = () => { const f = body.querySelector('.cx-modal-foot'); if (f) mdl.appendChild(f); };
17
17
  const focusables = () => [...mdl.querySelectorAll('button:not([disabled]), input, textarea, select, [tabindex]:not([tabindex="-1"])')].filter((x) => x.offsetParent !== null);
18
18
  const close = () => on.close?.();
19
+ // ANCHORED: a card beside what was tapped ("x,y,w,h" in the viewport): on its right when it fits, else its left, and
20
+ // kept on screen; it grows from that side. No anchor (or a phone) makes it the dialog.
21
+ let anchor = String(inputs.anchor || '');
22
+ // `want` is the mode the page asked for; `mode` is what it is now (an anchor with nothing to hold on to is a dialog)
23
+ const wantsAnchor = () => mdl.dataset.want === 'anchor';
24
+ const place = () => {
25
+ const at = anchor.split(',').map(Number);
26
+ if (!wantsAnchor() || phone.matches || at.length !== 4 || at.some((n) => !Number.isFinite(n))) { if (wantsAnchor()) mdl.dataset.mode = phone.matches ? 'drawer' : 'dialog'; return; }
27
+ mdl.dataset.mode = 'anchor';
28
+ const [x, y, w] = at, gap = 12, edge = 16;
29
+ const width = mdl.offsetWidth, height = Math.min(mdl.scrollHeight, innerHeight - edge * 2);
30
+ const right = x + w + gap + width <= innerWidth - edge;
31
+ const left = right ? x + w + gap : Math.max(edge, x - gap - width);
32
+ const top = Math.min(Math.max(edge, y - 24), innerHeight - height - edge);
33
+ mdl.style.left = `${left}px`; mdl.style.top = `${top}px`;
34
+ mdl.style.transformOrigin = `${right ? 'left' : 'right'} ${Math.max(0, y - top + 12)}px`;
35
+ };
19
36
  const open = () => {
20
37
  if (isOpen) return; isOpen = true; placeFooter();
21
38
  if (auto) mdl.dataset.mode = phone.matches ? 'drawer' : 'dialog';
39
+ if (wantsAnchor()) place();
22
40
  lastFocus = document.activeElement;
23
41
  document.documentElement.classList.add('cx-locked');
24
42
  // on the phone, focusing a field would throw the keyboard over the sheet: focus the sheet itself
@@ -87,5 +105,9 @@ export function mount(el, inputs, on) {
87
105
  const endPull = () => { if (!pull) return; const d = pull; pull = null; if (!d.live) return; mdl.classList.remove('dragging'); ov?.classList.remove('dragging'); const v = d.dy / (performance.now() - d.t); if (d.dy > mdl.offsetHeight * 0.3 || v > 0.5) close(); else { mdl.style.transform = ''; if (ov) ov.style.opacity = ''; } };
88
106
  mdl.addEventListener('touchend', endPull); mdl.addEventListener('touchcancel', endPull);
89
107
  if (inputs.open === true || inputs.open === 'true') open();
90
- return (next) => { (next.open === true || next.open === 'true') ? open() : shut(); };
108
+ // the card follows the window, and its own height when a step inside changes it (it only moves when it would leave
109
+ // the screen: the top stays by the cita while there is room below)
110
+ addEventListener('resize', () => { if (isOpen && wantsAnchor()) place(); });
111
+ new ResizeObserver(() => { if (isOpen && mdl.dataset.mode === 'anchor') place(); }).observe(body);
112
+ return (next) => { anchor = String(next.anchor || ''); (next.open === true || next.open === 'true') ? open() : shut(); };
91
113
  }
@@ -1,4 +1,4 @@
1
1
  # ModalBehavior - what a modal needs that is not markup: the phone's Back closes it, Esc closes it, focus stays
2
2
  # inside, the page behind does not scroll, and on a phone the drawer drags down to close. Used by Modal; you never
3
3
  # call it yourself. shadcn-for-muten · Custom · host JS is modal-behavior.js.
4
- part ModalBehavior(open: bool, onClose: action) { Custom ModalBehavior inputs(open: $open) on(close: $onClose) class("cx-modal-behavior") }
4
+ part ModalBehavior(open: bool, onClose: action, anchor: text = "") { Custom ModalBehavior inputs(open: $open, anchor: $anchor) on(close: $onClose) class("cx-modal-behavior") }
@@ -7,14 +7,16 @@
7
7
  # Field { … }
8
8
  # ModalFooter { Btn(variant: "outline", onClick: done) class("cx-exit") { Span "Cancel" } Btn(onClick: save) class("cx-go") { Span "Save changes" } }
9
9
  # }
10
- # mode: auto | dialog | drawer | sheet (a panel from a side) · side: right | left | top | bottom (for sheet)
10
+ # mode: auto | dialog | drawer | sheet (a panel from a side) | anchor (a card beside what was tapped)
11
+ # side: right | left | top | bottom (for sheet) · anchor: "x,y,w,h" of what was tapped, in the viewport (for anchor;
12
+ # with none it opens as a dialog). AgendaLive's onOpen sends it as `at`, so a cita opens beside itself.
11
13
  # role: dialog | alertdialog · dismiss: on | off (off: no ✕ and the backdrop does not close it; only its buttons do)
12
14
  # size: "" | tall (a list that pages) · shadcn-for-muten · part + Custom (ModalBehavior) · kit.css (.cx-modal).
13
- part Modal(open: bool, onClose: action, title: text = "", description: text = "", mode: text = "auto", size: text = "", side: text = "right", role: text = "dialog", dismiss: text = "on") {
15
+ part Modal(open: bool, onClose: action, title: text = "", description: text = "", mode: text = "auto", size: text = "", side: text = "right", role: text = "dialog", dismiss: text = "on", anchor: text = "") {
14
16
  Stack class("cx-modal-root") data(open: $open) {
15
17
  when $dismiss != "off" { Button "" -> $onClose class("cx-overlay") aria(label: "Close", hidden: "true") data(closes: "true", trl: "Close") }
16
18
  when $dismiss == "off" { Stack class("cx-overlay") aria(hidden: "true") }
17
- Stack class("cx-modal") data(mode: $mode, size: $size, side: $side) aria(role: $role, modal: "true", label: $title) {
19
+ Stack class("cx-modal") data(mode: $mode, want: $mode, size: $size, side: $side) aria(role: $role, modal: "true", label: $title) {
18
20
  Stack class("cx-handle") aria(hidden: "true")
19
21
  Stack class("cx-modal-head") data(drag: "true") {
20
22
  Span "{$title}" class("cx-modal-title")
@@ -22,7 +24,7 @@ part Modal(open: bool, onClose: action, title: text = "", description: text = ""
22
24
  }
23
25
  when $dismiss != "off" { Btn(variant: "ghost", size: "icon-sm", label: "Close", onClick: $onClose) class("cx-modal-x") { Icon "lucide:x" } }
24
26
  Stack class("cx-modal-body") { slot }
25
- ModalBehavior(open: $open, onClose: $onClose)
27
+ ModalBehavior(open: $open, onClose: $onClose, anchor: $anchor)
26
28
  }
27
29
  }
28
30
  }