@muten/shadcn 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/kit.css +20 -3
- package/package.json +1 -1
- package/registry/agenda.js +1 -0
- package/registry/agenda.muten +4 -2
- package/registry/kit/core.js +35 -5
- package/registry/modal-behavior.js +24 -0
package/kit.css
CHANGED
|
@@ -390,7 +390,8 @@ button.cn-item { width: 100%; font: inherit; text-align: left; cursor: pointer;
|
|
|
390
390
|
.cx-sw i { width: 22px; height: 22px; border-radius: 6px; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--foreground) 15%, transparent); flex: none; }
|
|
391
391
|
|
|
392
392
|
/* ── 44 px touch target without changing the drawn size ── */
|
|
393
|
-
|
|
393
|
+
/* :where() keeps it at zero specificity: a button placed on purpose (the modal ✕, a pinned action) keeps its position */
|
|
394
|
+
@media (pointer: coarse) { :where(.cn-button) { position: relative; } .cn-button::before { content: ""; position: absolute; left: 50%; top: 50%; width: max(100%, 44px); height: max(100%, 44px); transform: translate(-50%, -50%); } }
|
|
394
395
|
|
|
395
396
|
/* ── page scaffolding for this preview (not part of the library) ── */
|
|
396
397
|
.cx-lib { display: flex; flex-direction: column; gap: 18px; }
|
|
@@ -1597,6 +1598,10 @@ p.cx-text { margin: 0; }
|
|
|
1597
1598
|
/* ═════ Agenda ═════ */
|
|
1598
1599
|
.cx-agenda { display: flex; flex-direction: column; gap: 12px; width: 100%; max-width: var(--agenda-max, none); margin: 0 auto; }
|
|
1599
1600
|
.cx-ag-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; }
|
|
1601
|
+
/* Today · the date · Day/List stay on ONE row: the date gives way (ellipsis), the view switch never drops below */
|
|
1602
|
+
.cx-ag-bar:not(:has(> .cx-text[data-variant="heading"])) { flex-wrap: nowrap; }
|
|
1603
|
+
.cx-ag-bar > .cx-ag-title { flex: 0 1 auto; }
|
|
1604
|
+
.cx-ag-bar > .cx-tabs, .cx-ag-bar > .cn-button { flex: none; }
|
|
1600
1605
|
.cx-ag-scroll { max-height: 520px; overflow: auto; }
|
|
1601
1606
|
.cx-ag { position: relative; display: grid; grid-template-rows: 36px 1fr; min-width: calc(56px + var(--cols) * 140px); }
|
|
1602
1607
|
.cx-ag-head { position: sticky; top: 0; z-index: 3; display: grid; grid-template-columns: 56px repeat(var(--cols), 1fr); background: var(--background); border-bottom: 1px solid var(--border); }
|
|
@@ -1616,6 +1621,11 @@ p.cx-text { margin: 0; }
|
|
|
1616
1621
|
.cx-ag-ev[data-dragging] { transition: none; cursor: grabbing; z-index: 2; box-shadow: 0 10px 24px -10px rgb(0 0 0 / .4); }
|
|
1617
1622
|
.cx-ag-ev:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; }
|
|
1618
1623
|
.cx-ag-ev .cx-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
|
|
1624
|
+
/* what the app's assistant did (event by "agent"): its mark before the name, ringed so it reads on any tone; on the
|
|
1625
|
+
phone's list, a line under the service. --cx-ag-by-ink colours that line (the app's assistant colour). */
|
|
1626
|
+
.cx-ag-by { display: inline-block; flex: none; width: 13px; height: 13px; margin-right: 4px; vertical-align: -2px; border-radius: 50%; box-shadow: 0 0 0 1.5px var(--background); }
|
|
1627
|
+
.cx-ag-byline { display: inline-flex; align-items: center; gap: 5px; margin-top: 2px; font-size: 12.5px; font-weight: 600; line-height: 1.3; color: var(--cx-ag-by-ink, var(--primary)); }
|
|
1628
|
+
.cx-ag-byline .cx-ag-by { width: 14px; height: 14px; margin: 0; box-shadow: none; }
|
|
1619
1629
|
.cx-ag-now { position: absolute; left: 56px; right: 0; height: 0; margin-top: -1px; border-top: 2px solid var(--destructive); z-index: 2; pointer-events: none; } /* the 2px border centred ON the hour, like the pill and its tail */
|
|
1620
1630
|
.cx-ag-now i { position: absolute; left: -5px; top: -6px; width: 10px; height: 10px; border-radius: 99px; background: var(--destructive); }
|
|
1621
1631
|
|
|
@@ -1816,7 +1826,10 @@ p.cx-text { margin: 0; }
|
|
|
1816
1826
|
.cx-ag-who .cn-button .cn-avatar { width: 20px; height: 20px; }
|
|
1817
1827
|
.cx-ag-who .cn-button .cn-avatar .cn-avatar-fallback { font-size: 9px; }
|
|
1818
1828
|
.cx-ag-who .cn-button[aria-checked="true"][aria-checked][aria-checked] { background: var(--selected-soft); color: var(--selected); border-color: transparent; }
|
|
1819
|
-
.cx-ag-strip { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
|
|
1829
|
+
.cx-ag-strip { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 4px; }
|
|
1830
|
+
/* a narrow phone: the seven days always fit (a long «TODAY» clips instead of pushing the week off the screen) */
|
|
1831
|
+
.cx-ag-strip .cx-ag-day { min-width: 0; padding-inline: 2px; }
|
|
1832
|
+
.cx-ag-strip .cx-ag-day small { max-width: 100%; overflow: hidden; text-overflow: clip; white-space: nowrap; }
|
|
1820
1833
|
.cx-ag-day small { font-size: 11px; color: var(--muted-foreground); text-transform: uppercase; letter-spacing: .04em; }
|
|
1821
1834
|
.cx-ag-day b { font-size: 16px; font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
1822
1835
|
/* a day with nothing booked has no count line: its name and number sit in the middle of the pill, not at its top */
|
|
@@ -4935,6 +4948,8 @@ a.cx-bc-item:hover { color: var(--foreground); }
|
|
|
4935
4948
|
/* the pill and the line are one: a short tail from the pill to the hour column's edge, where the first day's line starts */
|
|
4936
4949
|
.cx-ag-nowtime::after { content: ""; position: absolute; left: 100%; top: 50%; width: 6px; border-top: 2px solid var(--destructive); transform: translateY(-1px); }
|
|
4937
4950
|
.cx-ag-nowtime { position: absolute; right: 4px; z-index: 3; transform: translateY(-50%); padding: 1px 5px; border-radius: 6px; background: var(--destructive); color: var(--destructive-foreground, #fff); font-size: 10px; font-weight: 600; line-height: 1.4; white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
4951
|
+
/* «12:36 PM», «10:59 AM»: the longest hours are wider than the hour column; only they tighten, so they fit whole */
|
|
4952
|
+
.cx-ag-nowtime[data-long] { right: 2px; padding-inline: 3px; font-size: 9.5px; letter-spacing: -.02em; }
|
|
4938
4953
|
/* the crossed-citas alert sits under the strip, full width, with its button */
|
|
4939
4954
|
.cx-ag-top > [data-ag-alert] { margin: 0; }
|
|
4940
4955
|
/* no view picked (a range is on): the tabs show none, not the first */
|
|
@@ -5313,7 +5328,9 @@ a.cx-bc-item:hover { color: var(--foreground); }
|
|
|
5313
5328
|
.cx-ag-ev { position: absolute; }
|
|
5314
5329
|
.cx-ag-evprice { position: absolute; top: 5px; right: 7px; font-size: 11.5px; font-weight: 650; font-variant-numeric: tabular-nums; opacity: .85; }
|
|
5315
5330
|
.cx-ag-dots { display: flex; flex-direction: row; justify-content: center; gap: 3px; min-height: 6px; }
|
|
5316
|
-
|
|
5331
|
+
/* ONE dot: «this day has appointments»; how many is in the day's name for a screen reader, and on the day itself */
|
|
5332
|
+
.cx-ag-dots > i { width: 6px; height: 6px; border-radius: 99px; flex: none; background: var(--selected, var(--primary)); }
|
|
5333
|
+
.cx-ag-day[aria-pressed="true"] .cx-ag-dots > i { background: currentColor; }
|
|
5317
5334
|
.cx-ag-ev:has(> .cx-ag-evprice) { padding-right: 62px; }
|
|
5318
5335
|
/* a narrow column (the week on a laptop) keeps the client's name: the price shows only where there is room for both */
|
|
5319
5336
|
.cx-ag-col { container-type: inline-size; }
|
package/package.json
CHANGED
package/registry/agenda.js
CHANGED
|
@@ -67,6 +67,7 @@ export function mount(el, inputs, on) {
|
|
|
67
67
|
from: Number(i.from) || 9, to: Number(i.to) || 19,
|
|
68
68
|
// the language passed (Storio passes it), else the page's (the nearest lang), English by default
|
|
69
69
|
lang: i.lang === 'en' || i.lang === 'es' ? i.lang : (langOf(el) === 'es' ? 'es' : 'en'), overlap: ['never', 'allow'].includes(i.overlap) ? i.overlap : 'auto', currency: i.currency || '', locale: i.locale || '', cents: yes(i.cents),
|
|
70
|
+
agentMark: i.agentMark || '', agentLabel: i.agentLabel || '',
|
|
70
71
|
};
|
|
71
72
|
};
|
|
72
73
|
map = data;
|
package/registry/agenda.muten
CHANGED
|
@@ -35,8 +35,10 @@ 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
|
+
# events take by "agent" (made by the app's assistant): agentMark (an image url) goes before their name, and agentLabel
|
|
39
|
+
# ("Booked by your assistant") under it on the phone's list and in what a screen reader says
|
|
38
40
|
# AgendaLive(resources: @team, hours: @hours, events: @citas, blocks: @blocks, free: @free, queue: @queue, closed: @closed,
|
|
39
41
|
# lang: ui.lang, currency: "USD", onOpen: openCita, onCreate: newCita, onBlock: block, onMove: moveCita, onRange: loadDays)
|
|
40
|
-
part AgendaLive(resources: list, hours: list, events: list, blocks: list, actions: list, free: list, queue: list, closed: list, lang: text = "es", currency: text = "", locale: text = "", cents: text = "", from: number = 9, to: number = 19, slot: number = 30, view: text = "day", loading: text = "", start: text = "", drag: text = "", phone: text = "", command: text = "", overlap: text = "", hour12: text = "", timeZone: text = "", phoneView: text = "", phoneStrip: text = "", fit: text = "", ranges: text = "", blockScope: text = "", resize: text = "", moveAcross: text = "", placing: list, onOpen: action, onCreate: action, onBlock: action, onMove: action, onRange: action, onAction: action) {
|
|
41
|
-
Custom Agenda inputs(resources: $resources, hours: $hours, events: $events, blocks: $blocks, actions: $actions, free: $free, queue: $queue, closed: $closed, lang: $lang, currency: $currency, locale: $locale, cents: $cents, from: $from, to: $to, slot: $slot, view: $view, loading: $loading, start: $start, drag: $drag, phone: $phone, command: $command, overlap: $overlap, hour12: $hour12, timeZone: $timeZone, phoneView: $phoneView, phoneStrip: $phoneStrip, fit: $fit, ranges: $ranges, blockScope: $blockScope, resize: $resize, moveAcross: $moveAcross, placing: $placing, sheet: "app") on(open: $onOpen, create: $onCreate, block: $onBlock, move: $onMove, range: $onRange, action: $onAction) class("cx-agenda")
|
|
42
|
+
part AgendaLive(resources: list, hours: list, events: list, blocks: list, actions: list, free: list, queue: list, closed: list, lang: text = "es", currency: text = "", locale: text = "", cents: text = "", from: number = 9, to: number = 19, slot: number = 30, view: text = "day", loading: text = "", start: text = "", drag: text = "", phone: text = "", command: text = "", overlap: text = "", hour12: text = "", agentMark: text = "", agentLabel: text = "", timeZone: text = "", phoneView: text = "", phoneStrip: text = "", fit: text = "", ranges: text = "", blockScope: text = "", resize: text = "", moveAcross: text = "", placing: list, onOpen: action, onCreate: action, onBlock: action, onMove: action, onRange: action, onAction: action) {
|
|
43
|
+
Custom Agenda inputs(resources: $resources, hours: $hours, events: $events, blocks: $blocks, actions: $actions, free: $free, queue: $queue, closed: $closed, lang: $lang, currency: $currency, locale: $locale, cents: $cents, from: $from, to: $to, slot: $slot, view: $view, loading: $loading, start: $start, drag: $drag, phone: $phone, command: $command, overlap: $overlap, hour12: $hour12, agentMark: $agentMark, agentLabel: $agentLabel, timeZone: $timeZone, phoneView: $phoneView, phoneStrip: $phoneStrip, fit: $fit, ranges: $ranges, blockScope: $blockScope, resize: $resize, moveAcross: $moveAcross, placing: $placing, sheet: "app") on(open: $onOpen, create: $onCreate, block: $onBlock, move: $onMove, range: $onRange, action: $onAction) class("cx-agenda")
|
|
42
44
|
}
|
package/registry/kit/core.js
CHANGED
|
@@ -429,6 +429,30 @@ const focusables = () => $$('button:not([disabled]), input, textarea, select, [t
|
|
|
429
429
|
};
|
|
430
430
|
addEventListener('pointerup', endDrag); addEventListener('pointercancel', endDrag);
|
|
431
431
|
|
|
432
|
+
// a finger can also pull the sheet down from ANYWHERE in it while its content sits at the top, like the phone's own
|
|
433
|
+
// sheets: the body pans vertically (touch-action: pan-y), so this listens to touch events and takes the gesture
|
|
434
|
+
// (preventDefault) only once it is a pull down with nothing above to scroll back to
|
|
435
|
+
let pull = null;
|
|
436
|
+
const scrollerOf = (el) => { for (let x = el; x && x !== mdl; x = x.parentElement) { if (x.scrollHeight > x.clientHeight + 1 && /(auto|scroll)/.test(getComputedStyle(x).overflowY)) return x; } return null; };
|
|
437
|
+
mdl.addEventListener('touchstart', (e) => {
|
|
438
|
+
if (mdl.dataset.mode !== 'drawer' || e.touches.length !== 1 || drag) return;
|
|
439
|
+
if (e.target.closest('input, textarea, select, [contenteditable="true"], [role="slider"], .cx-wheel, .cx-slider')) return;
|
|
440
|
+
const t = e.touches[0]; pull = { y: t.clientY, x: t.clientX, t: performance.now(), dy: 0, live: false, sc: scrollerOf(e.target) };
|
|
441
|
+
}, { passive: true });
|
|
442
|
+
mdl.addEventListener('touchmove', (e) => {
|
|
443
|
+
if (!pull) return;
|
|
444
|
+
const t = e.touches[0], dy = t.clientY - pull.y, dx = t.clientX - pull.x;
|
|
445
|
+
if (!pull.live) {
|
|
446
|
+
if (Math.abs(dy) < 6 && Math.abs(dx) < 6) return;
|
|
447
|
+
if (dy < 0 || Math.abs(dx) > Math.abs(dy) || (pull.sc && pull.sc.scrollTop > 0)) { pull = null; return; }
|
|
448
|
+
pull.live = true; mdl.classList.add('dragging'); ov.classList.add('dragging');
|
|
449
|
+
}
|
|
450
|
+
e.preventDefault(); pull.dy = dy; mdl.style.transform = `translateY(${dy}px)`;
|
|
451
|
+
ov.style.opacity = String(Math.max(0, 1 - dy / mdl.offsetHeight));
|
|
452
|
+
}, { passive: false });
|
|
453
|
+
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) closeModal(); else { mdl.style.transform = ''; ov.style.opacity = ''; } };
|
|
454
|
+
mdl.addEventListener('touchend', endPull); mdl.addEventListener('touchcancel', endPull);
|
|
455
|
+
|
|
432
456
|
|
|
433
457
|
// ── wheels (date and time) ──
|
|
434
458
|
const MONTHS = MON.map((m) => m[0].toUpperCase() + m.slice(1));
|
|
@@ -663,6 +687,11 @@ let wheelsNow = [];
|
|
|
663
687
|
actions = null, createSheet = false, services = [], view: startView = 'day', sheet: sheetMode = 'built-in', drag: dragOn = true,
|
|
664
688
|
resize: resizeMode = 'on', moveAcross = 'on',
|
|
665
689
|
start: startDay = null, phone: phoneMode = '', hour12 = false, timeZone = '', phoneView = '', blockScope = 'each', phoneStrip = '' } = opts;
|
|
690
|
+
// WHAT THE APP'S ASSISTANT DID: an event with by "agent" carries the app's mark (an image url) before its name, and on
|
|
691
|
+
// the phone's list a line that says so (agentLabel). The library knows no assistant: the app names it and draws it.
|
|
692
|
+
let agentMark = opts.agentMark || '', agentLabel = opts.agentLabel || '';
|
|
693
|
+
const byAgent = (e) => e.by === 'agent' && !!agentMark;
|
|
694
|
+
const agentImg = () => `<img class="cx-ag-by" src="${String(agentMark).replace(/"/g, '"')}" alt="" aria-hidden="true">`;
|
|
666
695
|
// THE SHOP'S CLOCK: with a timeZone, “today”, “now” and “the past” are the shop's, not the browser's (an owner
|
|
667
696
|
// travelling, or a phone set to another zone, still sees the shop's day)
|
|
668
697
|
const clockNow = () => { if (!timeZone) return new Date(); try { const p = Object.fromEntries(new Intl.DateTimeFormat('en-US', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }).formatToParts(new Date()).map((x) => [x.type, x.value])); return new Date(+p.year, +p.month - 1, +p.day, +p.hour % 24, +p.minute); } catch { return new Date(); } };
|
|
@@ -725,7 +754,7 @@ let wheelsNow = [];
|
|
|
725
754
|
const tagsOf = (e) => (Array.isArray(e.tags) ? e.tags.filter(Boolean).join(' · ') : '');
|
|
726
755
|
const SAY = ['started', 'awaiting_payment', 'noshow']; // the statuses a cita SAYS in words (no stripes, no glow)
|
|
727
756
|
const evHtml = (e, ln, showWho, rid = null, gut = false) => { const [l, n] = ln.get(e.id) || [0, 1]; const short = e.dur < 45; const said = clash.has(e.id) ? w.clashOne : SAY.includes(e.status) && w.status[e.status] ? w.status[e.status] : ''; const others = rid ? occ(e).filter((x) => x !== rid).map(resName).filter(Boolean).join(', ') : ''; const tg = tagsOf(e);
|
|
728
|
-
const span = gut ? '(100% - 22px)' : '100%'; /* an agenda with no limit keeps a strip on the right to book beside */ return `<button class="cx-ag-ev" data-ev="${e.id}" data-tone="${toneOf(e)}" ${e.status ? `data-status="${e.status}"` : ''}${paint1(e)}top:${y(toMin(e.start))}px;height:${(e.dur / 60) * hourPx - 3}px;left:calc(${span} * ${l / n} + 3px);width:calc(${span} / ${n} - 6px)"${clash.has(e.id) ? ' data-conflict' : ''} aria-label="${e.title}, ${clk(e.start)}, ${e.dur} ${w.minutes}${tg ? ', ' + tg : ''}${showWho ? `, ${w.with} ${resName(e.resource)}` : ''}${others ? `, ${w.with} ${others}` : ''}${e.status && w.status[e.status] ? `, ${w.status[e.status]}` : ''}${clash.has(e.id) ? `, ${w.clashOne}` : ''}">${short ? ui.text(`${e.title} · ${said || tg || clk(e.start)}`, 'label') : `${ui.text(e.title
|
|
757
|
+
const span = gut ? '(100% - 22px)' : '100%'; /* an agenda with no limit keeps a strip on the right to book beside */ return `<button class="cx-ag-ev" data-ev="${e.id}" data-tone="${toneOf(e)}" ${e.status ? `data-status="${e.status}"` : ''}${paint1(e)}top:${y(toMin(e.start))}px;height:${(e.dur / 60) * hourPx - 3}px;left:calc(${span} * ${l / n} + 3px);width:calc(${span} / ${n} - 6px)"${clash.has(e.id) ? ' data-conflict' : ''} aria-label="${e.title}, ${clk(e.start)}, ${e.dur} ${w.minutes}${tg ? ', ' + tg : ''}${showWho ? `, ${w.with} ${resName(e.resource)}` : ''}${others ? `, ${w.with} ${others}` : ''}${e.status && w.status[e.status] ? `, ${w.status[e.status]}` : ''}${clash.has(e.id) ? `, ${w.clashOne}` : ''}${byAgent(e) && agentLabel ? `, ${agentLabel}` : ''}">${short ? ui.text(`${byAgent(e) ? agentImg() : ''}${e.title} · ${said || tg || clk(e.start)}`, 'label') : `${ui.text(`${byAgent(e) ? agentImg() : ''}${e.title}`, 'label')}${ui.text(`${said ? said + ' · ' : ''}${tg ? tg + ' · ' : ''}${clk(e.start)} · ${e.subtitle || ''}${showWho ? ' · ' + resName(e.resource) : ''}${others ? ' · ' + others : ''}`, 'caption')}`}${e.price && !short ? `<span class="cx-ag-evprice">${fmtMoney(e.price)}</span>` : ''}${dragOn && resizeMode !== 'off' ? '<span class="cx-ag-grip" aria-hidden="true"></span>' : ''}</button>`; };
|
|
729
758
|
// the hours a person does not work: hatched. With server free hours, everything that is neither free nor booked.
|
|
730
759
|
// a day the business does not open: the column hatched, “Closed” in the middle (grid) or as the list's only line
|
|
731
760
|
const shutPill = () => `<span class="cx-ag-shutpill">${ui.icon('caloff')}${ui.text(w.closed, 'label')}</span>`;
|
|
@@ -768,7 +797,7 @@ let wheelsNow = [];
|
|
|
768
797
|
return `<div class="cx-hstack cx-ag-who" role="radiogroup" aria-label="${w.whose}"><span class="cx-ag-wholabel" aria-hidden="true">${w.whose}</span>${all}${kinds.map((k) => { const rs = ordered().filter((r) => kindOf(r) === k); return `<span class="cx-ag-sep" aria-hidden="true"></span>${rs.length > 1 ? ui.button({ label: groupName(k), size: 'sm', variant: 'ghost', attrs: `role="radio" aria-checked="${who === 'kind:' + k}" data-who="kind:${k}"`, cls: 'cx-ag-group' }) : ''}${rs.map(personBtn).join('')}`; }).join('')}</div>`;
|
|
769
798
|
};
|
|
770
799
|
// the week strip: each day with how many appointments it has (a plain number), closed days dimmed
|
|
771
|
-
const strip = () => `<div class="cx-ag-strip" role="group" aria-label="${w.days}">${weekDays().map((d) => { const evs = loading ? [] : evOn(d); const n = evs.length; const shut = isClosed(d); return `<button class="cn-button cn-button-variant-ghost cx-size-tile cx-ag-day" data-ag-day="${+d}" aria-pressed="${sameDay(d, day)}" ${sameDay(d, today) ? 'data-today' : ''} ${shut ? 'data-closed' : ''} aria-label="${w.dayL[d.getDay()]} ${d.getDate()}, ${shut && !n ? w.closed : w.countOf(n)}"${loading && !shownOnce ? ' disabled aria-disabled="true"' : ''}><small>${sameDay(d, today) ? w.todayShort : w.dayS[d.getDay()]}</small><b>${d.getDate()}</b>${phoneStrip === 'dots' ? `<span class="cx-ag-dots" aria-hidden="true">${loading ? '' :
|
|
800
|
+
const strip = () => `<div class="cx-ag-strip" role="group" aria-label="${w.days}">${weekDays().map((d) => { const evs = loading ? [] : evOn(d); const n = evs.length; const shut = isClosed(d); return `<button class="cn-button cn-button-variant-ghost cx-size-tile cx-ag-day" data-ag-day="${+d}" aria-pressed="${sameDay(d, day)}" ${sameDay(d, today) ? 'data-today' : ''} ${shut ? 'data-closed' : ''} aria-label="${w.dayL[d.getDay()]} ${d.getDate()}, ${shut && !n ? w.closed : w.countOf(n)}"${loading && !shownOnce ? ' disabled aria-disabled="true"' : ''}><small>${sameDay(d, today) ? w.todayShort : w.dayS[d.getDay()]}</small><b>${d.getDate()}</b>${phoneStrip === 'dots' ? `<span class="cx-ag-dots" aria-hidden="true">${loading ? '' : (n ? '<i></i>' : '')}</span>` : `<span class="cx-ag-count" aria-hidden="true">${loading ? '<s class="skel"></s>' : n || ''}</span>`}</button>`; }).join('')}</div>`;
|
|
772
801
|
// “Ahora”: on today, the appointment in progress (started, or whose time is now), first thing, one tap to open it
|
|
773
802
|
const nowCard = () => {
|
|
774
803
|
if (!sameDay(day, today)) return '';
|
|
@@ -789,13 +818,13 @@ let wheelsNow = [];
|
|
|
789
818
|
// The dot marks where today's column starts, so day and week begin the same (pill, tail, line): in day view, or
|
|
790
819
|
// when today is the week's first column, the pill already stands there and no dot is drawn. Nothing while loading.
|
|
791
820
|
const nowLine = (c, ci, cols) => { const nowM = nowMin(); const at = cols.findIndex((k) => sameDay(k.day, today)); if (at < 0 || nowM <= from * 60 || nowM >= to * 60) return ''; const mine = sameDay(c.day, today); const gone = !mine && c.day < today; return `<div class="cx-ag-now${mine ? '' : gone ? ' cx-ag-now-past' : ' cx-ag-now-ahead'}" style="top:${y(nowM)}px" aria-hidden="true">${mine && at === ci && ci > 0 ? '<i></i>' : ''}</div>`; };
|
|
792
|
-
const grid = (cols) => { const hours = Array.from({ length: to - from }, (_, i) => from + i); const nowM = nowMin(); return `<div class="cx-ag-scroll cx-scroll"><div class="cx-ag" style="--cols:${cols.length};--h:${hourPx}px;grid-template-rows:auto auto"><div class="cx-ag-head"><span></span>${cols.map((c) => `<div class="cx-ag-colhead" ${c.today ? 'data-today' : ''} ${c.closed ? 'data-closed' : ''}>${loading ? skHead(c) : c.head}</div>`).join('')}</div><div class="cx-ag-body" style="height:${(to - from) * hourPx}px"><div class="cx-ag-times">${!loading && cols.some((k) => sameDay(k.day, today)) && nowM > from * 60 && nowM < to * 60 ? `<b class="cx-ag-nowtime" style="top:${y(nowM)}px">${clk(toHM(nowM))}</b>` : ''}${hours.map((h) => `<span style="top:${(h - from) * hourPx}px">${ui.text(hour12 ? `${((h + 11) % 12) + 1} ${h < 12 ? 'AM' : 'PM'}` : `${String(h).padStart(2, '0')}:00`, 'caption')}</span>`).join('')}</div>${cols.map((c, ci) => { const rid = c.rid === 'all' ? null : c.rid; const evs = evOn(c.day, rid); const cap = rid ? capOf(rid) : 1; const ln = lanes(evs); if (cap > 1) ln.forEach((v) => { v[1] = Math.max(v[1], Math.min(cap, 4)); }); const rids = rid ? [rid] : shown().map((r) => r.id); if (loading) return `<div class="cx-ag-col" data-day="${+c.day}" data-rid="${c.rid}" aria-hidden="true">${rid && !freeList ? offHtml(c.day, rid) : ''}${skelCol(ci)}</div>`;
|
|
821
|
+
const grid = (cols) => { const hours = Array.from({ length: to - from }, (_, i) => from + i); const nowM = nowMin(); return `<div class="cx-ag-scroll cx-scroll"><div class="cx-ag" style="--cols:${cols.length};--h:${hourPx}px;grid-template-rows:auto auto"><div class="cx-ag-head"><span></span>${cols.map((c) => `<div class="cx-ag-colhead" ${c.today ? 'data-today' : ''} ${c.closed ? 'data-closed' : ''}>${loading ? skHead(c) : c.head}</div>`).join('')}</div><div class="cx-ag-body" style="height:${(to - from) * hourPx}px"><div class="cx-ag-times">${!loading && cols.some((k) => sameDay(k.day, today)) && nowM > from * 60 && nowM < to * 60 ? `<b class="cx-ag-nowtime" style="top:${y(nowM)}px"${clk(toHM(nowM)).length > 7 ? ' data-long' : ''}>${clk(toHM(nowM))}</b>` : ''}${hours.map((h) => `<span style="top:${(h - from) * hourPx}px">${ui.text(hour12 ? `${((h + 11) % 12) + 1} ${h < 12 ? 'AM' : 'PM'}` : `${String(h).padStart(2, '0')}:00`, 'caption')}</span>`).join('')}</div>${cols.map((c, ci) => { const rid = c.rid === 'all' ? null : c.rid; const evs = evOn(c.day, rid); const cap = rid ? capOf(rid) : 1; const ln = lanes(evs); if (cap > 1) ln.forEach((v) => { v[1] = Math.max(v[1], Math.min(cap, 4)); }); const rids = rid ? [rid] : shown().map((r) => r.id); if (loading) return `<div class="cx-ag-col" data-day="${+c.day}" data-rid="${c.rid}" aria-hidden="true">${rid && !freeList ? offHtml(c.day, rid) : ''}${skelCol(ci)}</div>`;
|
|
793
822
|
const offRid = rid || (shown().length === 1 && !freeList ? shown()[0].id : null);
|
|
794
823
|
if (c.closed && !evs.length) return `<div class="cx-ag-col" data-day="${+c.day}" data-rid="${c.rid}" data-closed role="group" aria-label="${c.label}, ${w.closed}">${shutHtml()}${nowLine(c, ci, cols)}</div>`;
|
|
795
824
|
return `<div class="cx-ag-col" data-day="${+c.day}" data-rid="${c.rid}" role="group" aria-label="${c.label}">${offRid ? offHtml(c.day, offRid) : ''}${!rids.some(bookable) ? '' : freeList ? rids.filter(bookable).map((r) => freeHtml(c.day, r, c.label)).join('') : Array.from({ length: ((to - from) * 60) / slot }, (_, i) => gone(c.day, from * 60 + i * slot) ? '' : `<button class="cx-ag-slot" data-slot-time="${from * 60 + i * slot}" style="top:${y(from * 60 + i * slot)}px;height:${(slot / 60) * hourPx}px" aria-label="${w.newItem}, ${c.label}, ${toHM(from * 60 + i * slot)}" tabindex="-1"></button>`).join('')}${blOn(c.day, rid).map(blockHtml).join('')}${evs.map((e) => evHtml(e, ln, c.rid === 'all' && resources.length > 1, rid, cap === 0)).join('')}${ghostHtml(c.day, rid)}${goneHtml(c.day)}${nowLine(c, ci, cols)}</div>`; }).join('')}</div></div></div>`; };
|
|
796
825
|
// list = the phone's agenda: a solid row is sold, a dashed one is still free
|
|
797
826
|
// one appointment as a row of the list (the day's list and the range's)
|
|
798
|
-
const evRow = (e) => `<button class="cx-ag-row" data-ev="${e.id}"${clash.has(e.id) ? ' data-conflict' : ''} data-tone="${toneOf(e)}" ${e.status ? `data-status="${e.status}"` : ''}${paint1(e)}"><span class="cx-ag-row-t">${ui.text(clk(e.start), 'strong')}${ui.text(`${e.dur} min`, 'caption')}</span>${e.photo ? `<span class="cn-avatar cx-ag-face cx-ag-rowface" style="background-image:url('${e.photo}')" aria-hidden="true"></span>` : ''}<span class="cx-vstack" style="gap:0;min-width:0">${ui.text(e.title, 'heading')}${ui.text(`${e.subtitle || ''}${resources.length > 1 ? ' · ' + resName(e.resource) : ''}${tagsOf(e) ? ' · ' + tagsOf(e) : ''}`, 'muted')}</span>${e.status && w.status[e.status] ? ui.badge(w.status[e.status], { tone: { info: 'info', live: 'wait', done: 'ok', bad: 'bad', ok: 'ok' }[toneOf(e)] || 'ok' }) : e.steps ? ui.badge(e.steps[e.at], { tone: e.tone === 'wait' ? 'wait' : e.tone === 'info' ? 'info' : 'ok' }) : ''}${e.price ? ui.text(fmtMoney(e.price), 'strong') : ''}</button>`;
|
|
827
|
+
const evRow = (e) => `<button class="cx-ag-row" data-ev="${e.id}"${clash.has(e.id) ? ' data-conflict' : ''} data-tone="${toneOf(e)}" ${e.status ? `data-status="${e.status}"` : ''}${paint1(e)}"><span class="cx-ag-row-t">${ui.text(clk(e.start), 'strong')}${ui.text(`${e.dur} min`, 'caption')}</span>${e.photo ? `<span class="cn-avatar cx-ag-face cx-ag-rowface" style="background-image:url('${e.photo}')" aria-hidden="true"></span>` : ''}<span class="cx-vstack" style="gap:0;min-width:0">${ui.text(e.title, 'heading')}${ui.text(`${e.subtitle || ''}${resources.length > 1 ? ' · ' + resName(e.resource) : ''}${tagsOf(e) ? ' · ' + tagsOf(e) : ''}`, 'muted')}${byAgent(e) && agentLabel ? `<span class="cx-ag-byline">${agentImg()}${agentLabel}</span>` : ''}</span>${e.status && w.status[e.status] ? ui.badge(w.status[e.status], { tone: { info: 'info', live: 'wait', done: 'ok', bad: 'bad', ok: 'ok' }[toneOf(e)] || 'ok' }) : e.steps ? ui.badge(e.steps[e.at], { tone: e.tone === 'wait' ? 'wait' : e.tone === 'info' ? 'info' : 'ok' }) : ''}${e.price ? ui.text(fmtMoney(e.price), 'strong') : ''}</button>`;
|
|
799
828
|
// RANGE: the days picked in the calendar, one after another, each with its appointments (a day with none says so)
|
|
800
829
|
const rangeList = () => { const [a, b] = rangeSel; const n = Math.min(62, Math.round((b - a) / 864e5) + 1); return `<div class="cx-ag-list cx-ag-range" role="list">${Array.from({ length: n }, (_, k) => { const d = addDays(a, k); const evs = evOn(d).sort((x, z) => toMin(x.start) - toMin(z.start)); return `<button type="button" class="cx-ag-rangeday" data-ag-day="${+d}" ${sameDay(d, today) ? 'data-today' : ''}>${ui.text(`${w.dayL[d.getDay()]} ${d.getDate()} ${w.mon[d.getMonth()]}`, 'strong')}${ui.text(isClosed(d) && !evs.length ? w.closed : evs.length ? w.countOf(evs.length) : w.noItems, 'caption')}</button>${evs.map(evRow).join('')}`; }).join('')}</div>`; };
|
|
801
830
|
const list = () => {
|
|
@@ -975,7 +1004,7 @@ let wheelsNow = [];
|
|
|
975
1004
|
const tick = setInterval(() => {
|
|
976
1005
|
if (!root.isConnected) { clearInterval(tick); return; }
|
|
977
1006
|
if (drag || loading) return; const m = nowMin(); if (m <= from * 60 || m >= to * 60) return;
|
|
978
|
-
$$('.cx-ag-now', root).forEach((x) => { x.style.top = y(m) + 'px'; }); const t = $('.cx-ag-nowtime', root); if (t) { t.style.top = y(m) + 'px'; t.textContent = clk(toHM(m)); }
|
|
1007
|
+
$$('.cx-ag-now', root).forEach((x) => { x.style.top = y(m) + 'px'; }); const t = $('.cx-ag-nowtime', root); if (t) { t.style.top = y(m) + 'px'; t.textContent = clk(toHM(m)); t.toggleAttribute('data-long', t.textContent.length > 7); }
|
|
979
1008
|
$$('.cx-ag-col', root).forEach((c) => { if (!sameDay(new Date(+c.dataset.day), today)) return; const g = $('.cx-ag-gone', c); if (g) g.style.height = y(m) + 'px'; });
|
|
980
1009
|
}, 30000);
|
|
981
1010
|
// only the agenda's own box decides: crossing the phone width repaints, and with fit a new HEIGHT of the box measures
|
|
@@ -1161,6 +1190,7 @@ let wheelsNow = [];
|
|
|
1161
1190
|
if ('availability' in next) availability = next.availability; if ('free' in next) freeList = next.free; if ('queue' in next) queue = next.queue;
|
|
1162
1191
|
if ('closed' in next) closed = next.closed; if ('loading' in next) loading = next.loading;
|
|
1163
1192
|
if (Number(next.from) >= 0 && Number(next.to) > Number(next.from)) { from = Number(next.from); to = Number(next.to); }
|
|
1193
|
+
if ('agentMark' in next) agentMark = next.agentMark; if ('agentLabel' in next) agentLabel = next.agentLabel;
|
|
1164
1194
|
if ('overlap' in next) overlap = next.overlap; if ('placing' in next) placing = next.placing; if ('currency' in next) currency = next.currency; if ('locale' in next) locale = next.locale; if ('cents' in next) cents = next.cents;
|
|
1165
1195
|
if ('lang' in next || 'words' in next) { lang = next.lang ?? lang; words = next.words ?? words; w = { ...(AG_WORDS[lang] || AG_WORDS.es), ...words }; }
|
|
1166
1196
|
if (who !== 'all' && !(who.startsWith('kind:') ? resources.some((r) => kindOf(r) === who.slice(5)) : resources.some((r) => r.id === who))) who = 'all';
|
|
@@ -62,6 +62,30 @@ export function mount(el, inputs, on) {
|
|
|
62
62
|
});
|
|
63
63
|
const endDrag = () => { if (!drag) return; const d = drag; drag = 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 = ''; } };
|
|
64
64
|
addEventListener('pointerup', endDrag); addEventListener('pointercancel', endDrag);
|
|
65
|
+
|
|
66
|
+
// a finger can also pull the sheet down from ANYWHERE in it while its content sits at the top, like the phone's own
|
|
67
|
+
// sheets: the body pans vertically (touch-action: pan-y), so this listens to touch events and takes the gesture
|
|
68
|
+
// (preventDefault) only once it is a pull down with nothing above to scroll back to
|
|
69
|
+
let pull = null;
|
|
70
|
+
const scrollerOf = (el) => { for (let x = el; x && x !== mdl; x = x.parentElement) { if (x.scrollHeight > x.clientHeight + 1 && /(auto|scroll)/.test(getComputedStyle(x).overflowY)) return x; } return null; };
|
|
71
|
+
mdl.addEventListener('touchstart', (e) => {
|
|
72
|
+
if (mdl.dataset.mode !== 'drawer' || e.touches.length !== 1 || drag) return;
|
|
73
|
+
if (e.target.closest('input, textarea, select, [contenteditable="true"], [role="slider"], .cx-wheel, .cx-slider')) return;
|
|
74
|
+
const t = e.touches[0]; pull = { y: t.clientY, x: t.clientX, t: performance.now(), dy: 0, live: false, sc: scrollerOf(e.target) };
|
|
75
|
+
}, { passive: true });
|
|
76
|
+
mdl.addEventListener('touchmove', (e) => {
|
|
77
|
+
if (!pull) return;
|
|
78
|
+
const t = e.touches[0], dy = t.clientY - pull.y, dx = t.clientX - pull.x;
|
|
79
|
+
if (!pull.live) {
|
|
80
|
+
if (Math.abs(dy) < 6 && Math.abs(dx) < 6) return;
|
|
81
|
+
if (dy < 0 || Math.abs(dx) > Math.abs(dy) || (pull.sc && pull.sc.scrollTop > 0)) { pull = null; return; }
|
|
82
|
+
pull.live = true; mdl.classList.add('dragging'); ov?.classList.add('dragging');
|
|
83
|
+
}
|
|
84
|
+
e.preventDefault(); pull.dy = dy; mdl.style.transform = `translateY(${dy}px)`;
|
|
85
|
+
if (ov) ov.style.opacity = String(Math.max(0, 1 - dy / mdl.offsetHeight));
|
|
86
|
+
}, { passive: false });
|
|
87
|
+
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
|
+
mdl.addEventListener('touchend', endPull); mdl.addEventListener('touchcancel', endPull);
|
|
65
89
|
if (inputs.open === true || inputs.open === 'true') open();
|
|
66
90
|
return (next) => { (next.open === true || next.open === 'true') ? open() : shut(); };
|
|
67
91
|
}
|