@muten/shadcn 0.1.4 → 0.1.6
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 +19 -2
- package/package.json +1 -1
- package/registry/agenda.js +8 -4
- package/registry/agenda.muten +5 -3
- package/registry/kit/core.js +25 -12
- package/registry/legal-doc.muten +3 -1
- package/registry/marketing.muten +23 -0
- package/registry/stacked-bars.js +1 -1
package/kit.css
CHANGED
|
@@ -5057,7 +5057,8 @@ a.cx-bc-item:hover { color: var(--foreground); }
|
|
|
5057
5057
|
.cx-heat-c[data-level="4"] { background: color-mix(in oklab, var(--series) 72%, var(--muted)); }
|
|
5058
5058
|
.cx-heat-c[data-level="5"] { background: var(--series); }
|
|
5059
5059
|
.cx-heat-c[data-level="6"] { background: color-mix(in oklab, var(--series) 78%, black); }
|
|
5060
|
-
|
|
5060
|
+
/* only a cell with something to say reacts: never while loading or under the empty note */
|
|
5061
|
+
.cx-heat:not([data-busy="true"]) .cx-heat-grid:not(:has(> .cx-veil-note)) > .cx-heat-c[data-name]:not([data-name=""]):hover { outline: 2px solid var(--foreground); outline-offset: 1px; }
|
|
5061
5062
|
.cx-heat-scale { display: flex; flex-direction: row; align-items: center; gap: 5px; font-size: 12.5px; color: var(--muted-foreground); }
|
|
5062
5063
|
.cx-heat-scale > .cx-heat-c { width: 16px; aspect-ratio: auto; height: 12px; border-radius: 3px; }
|
|
5063
5064
|
.cx-heat-scale > span:first-child { margin-right: 3px; } .cx-heat-scale > span:last-child { margin-left: 3px; }
|
|
@@ -5078,7 +5079,8 @@ a.cx-bc-item:hover { color: var(--foreground); }
|
|
|
5078
5079
|
.cx-stb-bar:disabled { cursor: default; }
|
|
5079
5080
|
.cx-stb-bar:focus-visible { outline: 2px solid var(--ring); outline-offset: 3px; }
|
|
5080
5081
|
.cx-stb-stack { display: flex; flex-direction: column-reverse; gap: 2px; border-radius: 6px 6px 3px 3px; overflow: hidden; transform-origin: bottom; opacity: .55; transition: height .5s cubic-bezier(.22, 1, .36, 1), opacity .2s; }
|
|
5081
|
-
.cx-stb-bars:not(:has([aria-pressed="true"])) .cx-stb-stack, .cx-stb-bar[aria-pressed="true"] .cx-stb-stack, .cx-stb-bar:hover .cx-stb-stack { opacity: 1; }
|
|
5082
|
+
.cx-stb-bars:not(:has([aria-pressed="true"])) .cx-stb-stack, .cx-stb-bar[aria-pressed="true"] .cx-stb-stack, .cx-stb-bar:not(:disabled):hover .cx-stb-stack { opacity: 1; }
|
|
5083
|
+
.cx-stb-bar:disabled { pointer-events: none; }
|
|
5082
5084
|
.cx-stb-seg { display: block; flex: 1 1 0; min-height: 2px; background: var(--series); }
|
|
5083
5085
|
.cx-stb-sk { display: block; width: 100%; border-radius: 6px 6px 3px 3px; }
|
|
5084
5086
|
.cx-stb-x { display: grid; grid-template-columns: repeat(var(--n, 12), minmax(0, 1fr)); gap: clamp(4px, 1.2vw, 14px); font-size: 12px; color: var(--muted-foreground); text-align: center; }
|
|
@@ -5189,3 +5191,18 @@ a.cx-bc-item:hover { color: var(--foreground); }
|
|
|
5189
5191
|
.style-nova .cx-pr-td .cx-pr-note { display: block; margin-top: 3px; font-size: 12.5px; line-height: 1.45; color: var(--muted-foreground); font-weight: 400; }
|
|
5190
5192
|
.style-nova .cx-pr-table > .cx-pr-tr > .cx-pr-td:has(> .cx-pr-note), .style-nova .cx-pr-table > .cx-pr-tr > .cx-pr-rh:has(> .cx-pr-note) { flex-direction: column; align-items: flex-start; justify-content: center; gap: 2px; text-align: left; }
|
|
5191
5193
|
.style-nova .cx-pr-rh > .cx-pr-note { display: block; font-size: 12.5px; line-height: 1.45; color: var(--muted-foreground); font-weight: 400; max-width: 30ch; }
|
|
5194
|
+
|
|
5195
|
+
/* Agenda: a week column says how many citas it holds and who waits; a cita card carries its price; the phone's week
|
|
5196
|
+
strip can mark each day with a dot per cita in its service's colour (phoneStrip "dots") */
|
|
5197
|
+
.cx-ag-colhead { display: flex; flex-direction: column; align-items: center; gap: 1px; }
|
|
5198
|
+
.cx-ag-colday { font-weight: 600; }
|
|
5199
|
+
.cx-ag-colsub { font-size: 11px; font-weight: 500; color: var(--muted-foreground); white-space: nowrap; }
|
|
5200
|
+
.cx-ag-colsub[data-queue] { color: var(--warning-foreground, var(--foreground)); }
|
|
5201
|
+
.cx-ag-ev { position: absolute; }
|
|
5202
|
+
.cx-ag-evprice { position: absolute; top: 5px; right: 7px; font-size: 11.5px; font-weight: 650; font-variant-numeric: tabular-nums; opacity: .85; }
|
|
5203
|
+
.cx-ag-dots { display: flex; flex-direction: row; justify-content: center; gap: 3px; min-height: 6px; }
|
|
5204
|
+
.cx-ag-dots > i { width: 6px; height: 6px; border-radius: 99px; flex: none; }
|
|
5205
|
+
.cx-ag-ev:has(> .cx-ag-evprice) { padding-right: 62px; }
|
|
5206
|
+
/* a narrow column (the week on a laptop) keeps the client's name: the price shows only where there is room for both */
|
|
5207
|
+
.cx-ag-col { container-type: inline-size; }
|
|
5208
|
+
@container (max-width: 190px) { .cx-ag-evprice { display: none; } .cx-ag-ev:has(> .cx-ag-evprice) { padding-right: 8px; } }
|
package/package.json
CHANGED
package/registry/agenda.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// first), "person" (a worker, the default) or any other (a room: say its `group`, «Salas»). capacity = how many citas
|
|
5
5
|
// it holds at once: 1 (default, never overlap) · 3 (a class of three) · 0 (no limit). overlap "never" | "allow"
|
|
6
6
|
// overrides every capacity. Citas that break one (the server sent them crossed) say «Se cruza».
|
|
7
|
-
// resources [{id, name, photo}] · hours [{id, days: [1..6], ranges: [[540, 780], [840, 1140]]}] (minutes from midnight)
|
|
7
|
+
// resources [{id, name, photo}] · hours [{id, days: [1..6], ranges: [[540, 780], [840, 1140]]}] (minutes from midnight);
|
|
8
|
+
// a person can have several entries, one per group of days, when the days do not share one schedule
|
|
8
9
|
// events [{id, resource, with: [ids], day, start "HH:MM", dur, title, subtitle, price, status, tint, ink, tone, steps, actions, at, payment, contact}]
|
|
9
10
|
// day = "yyyy-mm-dd", or a number of days from today (it lands on the next day that person works)
|
|
10
11
|
// with = the other agendas the cita also takes (the room it uses, a second worker): checked and shown in each
|
|
@@ -20,6 +21,7 @@
|
|
|
20
21
|
// sends them back through `command`: "new", "block", "prev", "next", "today", "view:list", or an action id. The
|
|
21
22
|
// agenda runs a command when its value changes, so repeat one with a suffix after # ("new#1", "new#2"…)
|
|
22
23
|
// hour12 "yes" - times written 3:00 PM (the data stays "HH:MM") · timeZone "America/Bogota" - today and now are the shop's
|
|
24
|
+
// phoneStrip "dots" - the phone's week strip marks each day with a dot per cita in its service's colour (default: the count)
|
|
23
25
|
// phoneView "list" - a phone opens on the list until a view is picked · blockScope "all" - blocks are for the whole
|
|
24
26
|
// business (no «Para» picker: a backend with no per-person blocks)
|
|
25
27
|
// events also take tags ["A domicilio", "Llegó 10:02"] (words next to the time) and photo (the client's face, in the list)
|
|
@@ -42,8 +44,9 @@ export function mount(el, inputs, on) {
|
|
|
42
44
|
const at = (s) => new Date(String(s) + 'T00:00:00');
|
|
43
45
|
const yes = (v) => v === true || v === 'yes' || v === 'true';
|
|
44
46
|
const data = (i) => {
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
+
// a person can have several entries, one per group of days (Monday 9 to 18, Saturday 9 to 13): the first that names the day wins
|
|
48
|
+
const hours = list(i.hours);
|
|
49
|
+
const work = (rid, d) => { const h = hours.find((x) => x.id === rid && list(x.days).includes(d.getDay())); return h ? list(h.ranges) : []; };
|
|
47
50
|
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); };
|
|
48
51
|
const plain = (d) => (typeof d === 'number' || /^-?\d+$/.test(String(d)) ? addDays(today, Number(d)) : at(d));
|
|
49
52
|
const dayOf = (rid, d) => (typeof d === 'number' || /^-?\d+$/.test(String(d)) ? land(rid, d) : at(d));
|
|
@@ -54,6 +57,7 @@ export function mount(el, inputs, on) {
|
|
|
54
57
|
blocks: list(i.blocks).map((b) => ({ ...b, day: plain(b.day) })),
|
|
55
58
|
queue: list(i.queue).map((q) => ({ ...q, day: plain(q.day) })), closed: list(i.closed).map(plain), loading: yes(i.loading),
|
|
56
59
|
placing: (() => { const g = list(i.placing)[0]; return g && g.start ? { ...g, day: dayOf(g.resource, g.day) } : null; })(),
|
|
60
|
+
from: Number(i.from) || 9, to: Number(i.to) || 19,
|
|
57
61
|
lang: i.lang === 'en' ? 'en' : 'es', overlap: ['never', 'allow'].includes(i.overlap) ? i.overlap : 'auto', currency: i.currency || '', locale: i.locale || '', cents: yes(i.cents),
|
|
58
62
|
};
|
|
59
63
|
};
|
|
@@ -67,7 +71,7 @@ export function mount(el, inputs, on) {
|
|
|
67
71
|
from: Number(inputs.from) || 9, to: Number(inputs.to) || 19, slot: Number(inputs.slot) || 30, view: inputs.view || 'day',
|
|
68
72
|
createSheet: inputs.create === 'sheet', services: list(inputs.services),
|
|
69
73
|
sheet: inputs.sheet === 'app' ? 'app' : 'built-in', drag: inputs.drag !== 'off', start: inputs.start ? at(inputs.start) : null, phone: inputs.phone === 'nav' ? 'nav' : '',
|
|
70
|
-
resize: inputs.resize === 'off' ? 'off' : 'on', moveAcross: inputs.moveAcross === 'off' ? 'off' : 'on', hour12: yes(inputs.hour12), timeZone: inputs.timeZone || '', phoneView: ['list', 'day'].includes(inputs.phoneView) ? inputs.phoneView : '', blockScope: inputs.blockScope === 'all' ? 'all' : 'each',
|
|
74
|
+
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',
|
|
71
75
|
onAction: (id) => send('action', id),
|
|
72
76
|
onCreate: (x) => send('create', day1(x)),
|
|
73
77
|
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,
|
package/registry/agenda.muten
CHANGED
|
@@ -24,15 +24,17 @@ part AgendaDemo(resources: list, hours: list, events: list, blocks: list, action
|
|
|
24
24
|
# onOpen(json {id, …}) a cita tapped (open your sheet) · onCreate(json {resource, day, start}) a free hour tapped
|
|
25
25
|
# onBlock(json {resource, day, start, dur, reason}) · onMove(json {id, resource, day, start, dur}) dragged (answer with
|
|
26
26
|
# the data: it stays where it landed, or goes back if you send it unchanged) · onRange(json {from, to, view}) days shown
|
|
27
|
+
# actions [{id, label, icon, primary}] the toolbar's buttons (default: Bloquear and Nueva cita; [{id: "block", …}] alone
|
|
28
|
+
# leaves «Nueva cita» to the app's own button) · an id other than block or new fires onAction(id)
|
|
27
29
|
# phone "nav": on a phone its actions (Bloquear, Nueva cita, yours) and the arrows leave the toolbar for the app's
|
|
28
30
|
# phone bar (PhoneBar), which sends them back through `command` ("new", "block", "prev", "next", "today", "view:list",
|
|
29
31
|
# an action id; repeat one with a suffix after #: "new#2"). Nothing runs while loading.
|
|
30
32
|
# several agendas: resources carry kind (business | person | a room…) and capacity (1 never overlaps, 0 no limit);
|
|
31
33
|
# a cita takes its resource plus every id in `with`. overlap "never" | "allow" overrides all capacities.
|
|
32
|
-
# resize "off" · moveAcross "off" (a backend that only moves day and time) · hour12 "yes" (3:00 PM) · timeZone (the shop's today and now) · phoneView "list" · blockScope "all" · placing [{…}]
|
|
34
|
+
# 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 cita in its colour) · blockScope "all" · placing [{…}]
|
|
33
35
|
# (the dashed cita while the app's new-cita sheet is open) · events take tags [words] and photo; resources role, hideEmpty
|
|
34
36
|
# AgendaLive(resources: @team, hours: @hours, events: @citas, blocks: @blocks, free: @free, queue: @queue, closed: @closed,
|
|
35
37
|
# lang: ui.lang, currency: "USD", onOpen: openCita, onCreate: newCita, onBlock: block, onMove: moveCita, onRange: loadDays)
|
|
36
|
-
part AgendaLive(resources: list, hours: list, events: list, blocks: list, free: list, queue: list, closed: list, lang: text = "es", currency: text = "", locale: text = "", cents: text = "", from: number = 9, to: number = 19, slot: number = 30, view: text = "day", loading: text = "", start: text = "", drag: text = "", phone: text = "", command: text = "", overlap: text = "", hour12: text = "", timeZone: text = "", phoneView: text = "", blockScope: text = "", resize: text = "", moveAcross: text = "", placing: list, onOpen: action, onCreate: action, onBlock: action, onMove: action, onRange: action) {
|
|
37
|
-
Custom Agenda inputs(resources: $resources, hours: $hours, events: $events, blocks: $blocks, free: $free, queue: $queue, closed: $closed, lang: $lang, currency: $currency, locale: $locale, cents: $cents, from: $from, to: $to, slot: $slot, view: $view, loading: $loading, start: $start, drag: $drag, phone: $phone, command: $command, overlap: $overlap, hour12: $hour12, timeZone: $timeZone, phoneView: $phoneView, blockScope: $blockScope, resize: $resize, moveAcross: $moveAcross, placing: $placing, sheet: "app") on(open: $onOpen, create: $onCreate, block: $onBlock, move: $onMove, range: $onRange) class("cx-agenda")
|
|
38
|
+
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 = "", blockScope: text = "", resize: text = "", moveAcross: text = "", placing: list, onOpen: action, onCreate: action, onBlock: action, onMove: action, onRange: action, onAction: action) {
|
|
39
|
+
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, 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")
|
|
38
40
|
}
|
package/registry/kit/core.js
CHANGED
|
@@ -181,7 +181,7 @@ watchReveal();
|
|
|
181
181
|
|
|
182
182
|
// ── tooltip on any [data-name] ──
|
|
183
183
|
const tip = document.createElement('div'); tip.className = 'cx-tip'; tip.setAttribute('role', 'tooltip'); tip.hidden = true; lib.appendChild(tip);
|
|
184
|
-
const tipOn = (e) => { const w = e.target.closest?.('[data-name]'); if (!w) return; tip.textContent = w.dataset.name; tip.hidden = false; const r = w.getBoundingClientRect(); if (w.closest('[data-slot="sidebar"]')) { tip.style.left = (r.right + 8) + 'px'; tip.style.top = (r.top + r.height / 2 - tip.offsetHeight / 2) + 'px'; } else { place(tip, w, 'center', 6); tip.style.top = (r.top - tip.offsetHeight - 6) + 'px'; } requestAnimationFrame(() => tip.dataset.state = 'open'); };
|
|
184
|
+
const tipOn = (e) => { const w = e.target.closest?.('[data-name]'); if (!w || !w.dataset.name) return; /* an empty name (no data yet) says nothing */ tip.textContent = w.dataset.name; tip.hidden = false; const r = w.getBoundingClientRect(); if (w.closest('[data-slot="sidebar"]')) { tip.style.left = (r.right + 8) + 'px'; tip.style.top = (r.top + r.height / 2 - tip.offsetHeight / 2) + 'px'; } else { place(tip, w, 'center', 6); tip.style.top = (r.top - tip.offsetHeight - 6) + 'px'; } requestAnimationFrame(() => tip.dataset.state = 'open'); };
|
|
185
185
|
const tipOff = (e) => { if (e.target.closest?.('[data-name]')) { tip.dataset.state = 'closed'; tip.hidden = true; } };
|
|
186
186
|
['pointerover', 'focusin'].forEach((t) => document.addEventListener(t, tipOn)); ['pointerout', 'focusout'].forEach((t) => document.addEventListener(t, tipOff));
|
|
187
187
|
|
|
@@ -491,7 +491,7 @@ let wheelsNow = [];
|
|
|
491
491
|
weekOf: (d) => `Semana del ${d}`, dayTitle: (wd, n, m) => `${wd} ${n} de ${m}`,
|
|
492
492
|
off: 'No trabaja este día', blocked: 'Bloqueado', minutes: 'minutos', with: 'con', notFree: 'no está libre', notFreeLong: 'Ese horario no está libre',
|
|
493
493
|
nowLasts: (m) => `Ahora dura ${m} min`, movedTo: (t, who) => `Movida a las ${t}${who ? ` con ${who}` : ''}`,
|
|
494
|
-
free: 'Libre', freeFromTo: (a, b) => `Libre de ${a} a ${b}, agendar`, until: 'a', noHours: 'No hay horario este día', freeSlot: 'Hora libre',
|
|
494
|
+
free: 'Libre', freeFromTo: (a, b) => `Libre de ${a} a ${b}, agendar`, until: 'a', noHours: 'No hay horario este día', dayOver: 'Este día ya pasó', todayOver: 'Ya no quedan horas libres hoy', freeSlot: 'Hora libre',
|
|
495
495
|
now: 'Ahora', waiting: (n) => (n === 1 ? '1 en espera' : `${n} en espera`), closed: 'Cerrado', countOf: (n) => (n === 1 ? '1 cita' : `${n} citas`),
|
|
496
496
|
move: 'Cambiar la hora', day: 'Día', current: 'Es su hora actual', noneFree: 'No queda una hora libre ese día.', back: 'Volver', saveMove: 'Guardar nueva hora',
|
|
497
497
|
cancelOf: 'Cancelar la cita', why: '¿Por qué?', reasons: ['El cliente pidió cancelar', 'No vino', 'Me surgió algo', 'Otro'], reasonLabel: 'Motivo', tellClient: 'Le avisamos al cliente con este motivo.',
|
|
@@ -514,7 +514,7 @@ let wheelsNow = [];
|
|
|
514
514
|
weekOf: (d) => `Week of ${d}`, dayTitle: (wd, n, m) => `${wd}, ${m} ${n}`,
|
|
515
515
|
off: 'Not working this day', blocked: 'Blocked', minutes: 'minutes', with: 'with', notFree: 'not free', notFreeLong: 'That time is not free',
|
|
516
516
|
nowLasts: (m) => `Now ${m} min long`, movedTo: (t, who) => `Moved to ${t}${who ? ` with ${who}` : ''}`,
|
|
517
|
-
free: 'Free', freeFromTo: (a, b) => `Free from ${a} to ${b}, book`, until: 'to', noHours: 'No hours this day', freeSlot: 'Free time',
|
|
517
|
+
free: 'Free', freeFromTo: (a, b) => `Free from ${a} to ${b}, book`, until: 'to', noHours: 'No hours this day', dayOver: 'This day is over', todayOver: 'No free hours left today', freeSlot: 'Free time',
|
|
518
518
|
now: 'Now', waiting: (n) => `${n} waiting`, closed: 'Closed', countOf: (n) => (n === 1 ? '1 appointment' : `${n} appointments`),
|
|
519
519
|
move: 'Change the time', day: 'Day', current: 'Its current time', noneFree: 'No free time left that day.', back: 'Back', saveMove: 'Save new time',
|
|
520
520
|
cancelOf: 'Cancel the appointment', why: 'Why?', reasons: ['The client asked to cancel', 'No-show', 'Something came up', 'Other'], reasonLabel: 'Reason', tellClient: 'We tell the client with this reason.',
|
|
@@ -540,10 +540,12 @@ let wheelsNow = [];
|
|
|
540
540
|
const sheetOf = (name) => { agOwner = me; openModal(K(name), 'auto'); };
|
|
541
541
|
let { resources, events, blocks = [], availability = () => [[9 * 60, 19 * 60]], free: freeList = null, queue = [], closed = [], loading = false,
|
|
542
542
|
lang = 'es', words = {}, currency = '', locale = '', cents = false, overlap = 'auto', placing = null } = opts; // all of these can change later through update()
|
|
543
|
-
|
|
543
|
+
// the first and last hour can change with the data (a shop's hours arrive after the first paint): update() takes them
|
|
544
|
+
let { from = 9, to = 19 } = opts;
|
|
545
|
+
const { slot = 30, hourPx = 56, onCreate = () => {}, onChange = () => {}, onAction = () => {}, onOpen = null, onBlock = null, onMove = null, onRange = () => {},
|
|
544
546
|
actions = null, createSheet = false, services = [], view: startView = 'day', sheet: sheetMode = 'built-in', drag: dragOn = true,
|
|
545
547
|
resize: resizeMode = 'on', moveAcross = 'on',
|
|
546
|
-
start: startDay = null, phone: phoneMode = '', hour12 = false, timeZone = '', phoneView = '', blockScope = 'each' } = opts;
|
|
548
|
+
start: startDay = null, phone: phoneMode = '', hour12 = false, timeZone = '', phoneView = '', blockScope = 'each', phoneStrip = '' } = opts;
|
|
547
549
|
// THE SHOP'S CLOCK: with a timeZone, «today», «now» and «the past» are the shop's, not the browser's (an owner
|
|
548
550
|
// travelling, or a phone set to another zone, still sees the shop's day)
|
|
549
551
|
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(); } };
|
|
@@ -606,7 +608,7 @@ let wheelsNow = [];
|
|
|
606
608
|
const tagsOf = (e) => (Array.isArray(e.tags) ? e.tags.filter(Boolean).join(' · ') : '');
|
|
607
609
|
const SAY = ['started', 'awaiting_payment', 'noshow']; // the statuses a cita SAYS in words (no stripes, no glow)
|
|
608
610
|
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);
|
|
609
|
-
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, 'label')}${ui.text(`${said ? said + ' · ' : ''}${tg ? tg + ' · ' : ''}${clk(e.start)} · ${e.subtitle || ''}${showWho ? ' · ' + resName(e.resource) : ''}${others ? ' · ' + others : ''}`, 'caption')}`}${dragOn && resizeMode !== 'off' ? '<span class="cx-ag-grip" aria-hidden="true"></span>' : ''}</button>`; };
|
|
611
|
+
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, '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>`; };
|
|
610
612
|
// the hours a person does not work: hatched. With server free hours, everything that is neither free nor booked.
|
|
611
613
|
const offHtml = (d, rid) => {
|
|
612
614
|
const av = freeList ? null : availability(rid, d);
|
|
@@ -641,7 +643,7 @@ let wheelsNow = [];
|
|
|
641
643
|
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>`;
|
|
642
644
|
};
|
|
643
645
|
// the week strip: each day with how many citas it has (a plain number), closed days dimmed
|
|
644
|
-
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 ? ' disabled aria-disabled="true"' : ''}><small>${sameDay(d, today) ? w.todayShort : w.dayS[d.getDay()]}</small><b>${d.getDate()}</b
|
|
646
|
+
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 ? ' 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 ? '' : evs.slice(0, 4).map((e) => `<i style="background:${e.tint || 'var(--selected, var(--primary))'}"></i>`).join('')}</span>` : `<span class="cx-ag-count" aria-hidden="true">${loading ? '<s class="skel"></s>' : n || ''}</span>`}</button>`; }).join('')}</div>`;
|
|
645
647
|
// «Ahora»: on today, the cita in progress (started, or whose time is now), first thing, one tap to open it
|
|
646
648
|
const nowCard = () => {
|
|
647
649
|
if (!sameDay(day, today)) return '';
|
|
@@ -679,7 +681,7 @@ let wheelsNow = [];
|
|
|
679
681
|
}
|
|
680
682
|
}
|
|
681
683
|
const html = rows.map((r) => (typeof r === 'string' ? r : `<button class="cx-ag-row" data-kind="free" data-free="${r.start}" data-rid="${r.free[0]}" aria-label="${w.freeFromTo(clk(toHM(r.start)), clk(toHM(r.end)))}"><span class="cx-ag-row-t">${ui.text(clk(toHM(r.start)), 'strong')}${r.end - r.start > slot ? ui.text(`${w.until} ${clk(toHM(r.end))}`, 'caption') : ''}</span>${ui.text(`${w.free}${rids.length > 1 ? ` · ${r.free.map(resName).join(', ')}` : ''}`, 'muted')}<span class="cx-grow"></span>${ui.icon('plus')}</button>`)).join('');
|
|
682
|
-
return `<div class="cx-ag-list" role="list">${html || `<div class="cn-empty" style="padding:32px 12px"><div class="cn-empty-header"><div class="cn-empty-title">${isClosed(day) ? w.closed : w.noHours}</div></div></div>`}</div>`;
|
|
684
|
+
return `<div class="cx-ag-list" role="list">${html || `<div class="cn-empty" style="padding:32px 12px"><div class="cn-empty-header"><div class="cn-empty-title">${isClosed(day) ? w.closed : !rids.some((rid) => availability(rid, day).length) ? w.noHours : +day < +today && !sameDay(day, today) ? w.dayOver : sameDay(day, today) ? w.todayOver : w.noHours}</div></div></div>`}</div>`;
|
|
683
685
|
};
|
|
684
686
|
// LOADING keeps the very layout it is about to show: the same toolbar, strip, column heads, hours and height,
|
|
685
687
|
// with grey citas where the real ones will land. Nothing jumps when the data arrives, nothing shrinks while it loads.
|
|
@@ -704,17 +706,19 @@ let wheelsNow = [];
|
|
|
704
706
|
else return onAction(k);
|
|
705
707
|
paint();
|
|
706
708
|
}
|
|
709
|
+
// a week column says its day and, under it, how many citas it holds and who waits for a free hour (closed: «Cerrado»)
|
|
710
|
+
const weekHead = (d, rid) => { const n = evOn(d, rid).length, q = queue.filter((x) => sameDay(x.day, d)).length, shut = isClosed(d); const sub = [shut && !n ? w.closed : n ? w.countOf(n) : '', q ? w.waiting(q) : ''].filter(Boolean).join(' · '); return `<span class="cx-ag-colday">${w.dayS[d.getDay()]} ${d.getDate()}</span>${sub ? `<small class="cx-ag-colsub"${q ? ' data-queue' : ''}>${sub}</small>` : ''}`; };
|
|
707
711
|
const tellRange = () => { const v = eff(); const a = v === 'range' ? rangeSel[0] : v === 'week' ? weekDays()[0] : day, b = v === 'range' ? rangeSel[1] : v === 'week' ? weekDays()[6] : day; const key = `${v}|${+a}|${+b}`; if (key === lastRange) return; lastRange = key; onRange({ from: a, to: b, view: v }); };
|
|
708
712
|
const paint = () => {
|
|
709
713
|
if (typeof drag !== 'undefined' && drag) { const d = drag; drag = null; clearTimeout(d.timer); cancelAnimationFrame(scrollRaf); d.ghost?.remove(); }
|
|
710
714
|
const v = eff(); // the phone keeps the calendar: day grid or list, the week needs a wide screen
|
|
711
|
-
const cols = v === 'week' ? weekDays().map((d) => ({ day: d, rid: who === 'all' || who.startsWith('kind:') ? 'all' : who, head:
|
|
715
|
+
const cols = v === 'week' ? weekDays().map((d) => ({ day: d, rid: who === 'all' || who.startsWith('kind:') ? 'all' : who, head: weekHead(d, who === 'all' || who.startsWith('kind:') ? null : who), label: `${w.dayL[d.getDay()]} ${d.getDate()}`, today: sameDay(d, today), closed: isClosed(d) })) : dayCols().map((r) => ({ day, rid: r.id, head: `${r.photo ? `<span class="cn-avatar cx-ag-face" style="background-image:url('${r.photo}')"></span>` : ui.avatar(r.name, { size: 'sm' })}<span class="cx-ag-colname"><b>${r.name}</b>${roleOf(r) ? `<small>${roleOf(r)}</small>` : ''}</span>`, label: r.name }));
|
|
712
716
|
clash = loading ? new Set() : findClashes();
|
|
713
717
|
const crossed = loading ? 0 : evOn(day).filter((e) => clash.has(e.id)).length;
|
|
714
718
|
const top = loading ? skelTop() : nowCard() + queueLine();
|
|
715
719
|
const alert = crossed ? ui.alert({ icon: 'info', title: w.clash(crossed), description: w.clashHow, action: ui.button({ label: w.clashSee, size: 'sm', variant: 'outline', attrs: 'data-ag-clash' }), tone: 'bad', attrs: 'data-ag-alert' }) : '';
|
|
716
720
|
if (!loading) { hadNow = top.includes('cx-ag-nowcard'); hadQueue = top.includes('cx-ag-queue'); }
|
|
717
|
-
root.innerHTML = toolbar() + `<div class="cx-ag-frame"${loading ? ` aria-busy="true" aria-label="${w.loading}"` : ''}><div class="cx-ag-top">${people()}${strip()}${top}${alert}</div>${v === 'range' ? `<div class="cx-ag-listwrap">${loading ? skelList() : rangeList()}</div>` : v === 'list' ? `<div class="cx-ag-listwrap">${loading ? skelList() : list()}</div>` : grid(cols)}</div>`;
|
|
721
|
+
root.innerHTML = toolbar() + `<div class="cx-ag-frame"${loading ? ` aria-busy="true" aria-label="${w.loading}"` : ''}><div class="cx-ag-top">${people()}${v === 'week' ? '' : strip()}${top}${alert}</div>${v === 'range' ? `<div class="cx-ag-listwrap">${loading ? skelList() : rangeList()}</div>` : v === 'list' ? `<div class="cx-ag-listwrap">${loading ? skelList() : list()}</div>` : grid(cols)}</div>`;
|
|
718
722
|
root.toggleAttribute('data-loading', !!loading);
|
|
719
723
|
$$('.cx-tabs', root).forEach(initTabs);
|
|
720
724
|
const now = $('.cx-ag-now', root); if (now) $('.cx-ag-scroll', root).scrollTop = Math.max(0, parseFloat(now.style.top) - 120);
|
|
@@ -959,19 +963,27 @@ let wheelsNow = [];
|
|
|
959
963
|
body.innerHTML = '<div class="cx-ag-datesheet"></div>';
|
|
960
964
|
dateUI($('.cx-ag-datesheet', body), (r) => { closeModal(); applyDate(r); });
|
|
961
965
|
};
|
|
966
|
+
let blockDay = null; // the one open day of the week, when the form had nothing to choose from
|
|
962
967
|
const openBlock = () => { blockOpen = true; sheetOf('agBlock'); };
|
|
963
968
|
CONTENT[K('agBlock')] = () => {
|
|
964
969
|
$('#mdl-title').textContent = w.blockTitle; $('#mdl-desc').textContent = w.blockDesc;
|
|
965
970
|
const sel = (id, label, v) => ui.field({ id, label, control: kit.timeButton(v, `id="${id}" data-blt="${label}" style="width:100%;justify-content:flex-start"`) });
|
|
966
|
-
|
|
971
|
+
// the week shows seven days, so the form says which one it blocks: only days that open, from today on, the shown day first
|
|
972
|
+
const open = eff() === 'week' ? weekDays().filter((d) => !isClosed(d)) : [];
|
|
973
|
+
const ahead = open.filter((d) => +d >= +today), days = ahead.length ? ahead : open;
|
|
974
|
+
const pickDay = days.find((d) => sameDay(d, day)) || days[0];
|
|
975
|
+
const dayField = days.length > 1 ? ui.field({ id: 'bl-d', label: w.day, control: `<button type="button" class="cn-button cn-button-variant-outline cn-button-size-default cx-sel-btn cx-fill" id="bl-d" data-v="${+pickDay}"></button>` }) : '';
|
|
976
|
+
body.innerHTML = `${dayField}${resources.length > 1 && blockScope !== 'all' ? ui.field({ id: 'bl-r', label: w.for, control: `<button type="button" class="cn-button cn-button-variant-outline cn-button-size-default cx-sel-btn cx-fill" id="bl-r" data-v="${who === 'all' ? 'all' : who}"></button>` }) : ''}<div class="cx-frange">${sel('bl-f', w.from, '13:00')}${sel('bl-t', w.to, '14:00')}</div>${ui.field({ id: 'bl-why', label: w.reasonLabel, control: ui.input({ id: 'bl-why', value: w.reasonDefault }), description: w.onlyYou })}`;
|
|
967
977
|
foot.innerHTML = ui.button({ label: w.cancel, size: 'default', attrs: 'data-dismiss', cls: 'cx-exit' }) + ui.button({ label: `${ui.icon('i-lock')}${w.block}`, variant: 'default', size: 'default', attrs: 'data-bl-go', cls: 'cx-go' });
|
|
978
|
+
const bd = $('#bl-d', body); if (bd) kit.selectMenu(bd, { label: w.day, value: bd.dataset.v, options: days.map((d) => ({ value: String(+d), label: `${w.dayL[d.getDay()]} ${d.getDate()}` })), onChange: (v) => { bd.dataset.v = v; } });
|
|
979
|
+
blockDay = days.length === 1 ? days[0] : null;
|
|
968
980
|
const br = $('#bl-r', body); if (br) kit.selectMenu(br, { label: w.for, value: br.dataset.v, options: [{ value: 'all', label: w.wholeShop }, ...resources.map((r) => ({ value: r.id, label: r.name }))], onChange: (v) => { br.dataset.v = v; } });
|
|
969
981
|
};
|
|
970
982
|
body.addEventListener('click', async (e) => { const b = e.target.closest('[data-blt]'); if (agOwner !== me || !b || !blockOpen) return; const v = await kit.pickTime({ value: b.querySelector('span').textContent, label: b.dataset.blt, from, to, step: 15 }); if (!v) return; b.querySelector('span').textContent = v; b.removeAttribute('aria-invalid'); const fb = $('#bl-f span', body), tb = $('#bl-t span', body); if (toMin(tb.textContent) <= toMin(fb.textContent)) tb.textContent = toHM(Math.min(toMin(fb.textContent) + 60, to * 60)); });
|
|
971
983
|
document.addEventListener('click', (e) => {
|
|
972
984
|
if (agOwner !== me || !e.target.closest('[data-bl-go]') || !blockOpen) return;
|
|
973
985
|
const f = toMin($('#bl-f span', body).textContent), t2 = toMin($('#bl-t span', body).textContent); if (t2 <= f) { $('#bl-t', body).setAttribute('aria-invalid', 'true'); return; }
|
|
974
|
-
const b = { id: 'b' + Date.now(), resource: $('#bl-r', body)?.dataset.v || resources[0].id, day: new Date(day), start: toHM(f), dur: t2 - f, reason: $('#bl-why', body).value.trim() || w.blocked };
|
|
986
|
+
const b = { id: 'b' + Date.now(), resource: $('#bl-r', body)?.dataset.v || resources[0].id, day: $('#bl-d', body) ? new Date(+$('#bl-d', body).dataset.v) : new Date(blockDay || day), start: toHM(f), dur: t2 - f, reason: $('#bl-why', body).value.trim() || w.blocked };
|
|
975
987
|
blockOpen = false; closeModal();
|
|
976
988
|
if (onBlock) { onBlock(b); return; }
|
|
977
989
|
blocks.push(b); paint(); onChange(events); island('done', w.blockedOk, 1800);
|
|
@@ -983,6 +995,7 @@ let wheelsNow = [];
|
|
|
983
995
|
if ('resources' in next) resources = next.resources; if ('events' in next) events = next.events; if ('blocks' in next) blocks = next.blocks;
|
|
984
996
|
if ('availability' in next) availability = next.availability; if ('free' in next) freeList = next.free; if ('queue' in next) queue = next.queue;
|
|
985
997
|
if ('closed' in next) closed = next.closed; if ('loading' in next) loading = next.loading;
|
|
998
|
+
if (Number(next.from) >= 0 && Number(next.to) > Number(next.from)) { from = Number(next.from); to = Number(next.to); }
|
|
986
999
|
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;
|
|
987
1000
|
if ('lang' in next || 'words' in next) { lang = next.lang ?? lang; words = next.words ?? words; w = { ...(AG_WORDS[lang] || AG_WORDS.es), ...words }; }
|
|
988
1001
|
if (who !== 'all' && !(who.startsWith('kind:') ? resources.some((r) => kindOf(r) === who.slice(5)) : resources.some((r) => r.id === who))) who = 'all';
|
package/registry/legal-doc.muten
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# DocHead(title: "Términos y condiciones") { Text "Última actualización: 7 de octubre de 2026" class("cx-doc-line") }
|
|
5
5
|
# DocBody {
|
|
6
6
|
# DocToc(label: "Contenido") { DocTocLink(label: "1. Quiénes somos", to: "#s1") DocTocLink(label: "2. Tus datos", to: "#s2") }
|
|
7
|
-
# DocArticle { DocSection(anchor: "s1", title: "1. Quiénes somos") {
|
|
7
|
+
# DocArticle { DocSection(anchor: "s1", title: "1. Quiénes somos") { DocText(text: "Storio es un producto de Candhrly LLC.") } }
|
|
8
8
|
# }
|
|
9
9
|
# shadcn-for-muten · parts · no deps · kit.css (.cx-doc-*).
|
|
10
10
|
part DocHead(title: text) {
|
|
@@ -19,6 +19,8 @@ part DocTocLink(label: text, to: text) {
|
|
|
19
19
|
Link "{$label}" -> "{$to}" class("cn-button cn-button-variant-ghost cn-button-size-sm group/button cx-nav cx-doc-toc-item") data(slot: "button", marker: "fill")
|
|
20
20
|
}
|
|
21
21
|
part DocArticle() { Stack class("cx-doc-article") { slot } }
|
|
22
|
+
# DocText - one paragraph of a section, at reading width.
|
|
23
|
+
part DocText(text: text) { Text "{$text}" class("cx-doc-p") }
|
|
22
24
|
part DocSection(anchor: text, title: text) {
|
|
23
25
|
Stack class("cx-doc-sec") id("{$anchor}") {
|
|
24
26
|
Title "{$title}" h2 class("cx-doc-h2")
|
package/registry/marketing.muten
CHANGED
|
@@ -142,3 +142,26 @@ part VsRow(what: text, us: text, them: text, usName: text, themName: text) {
|
|
|
142
142
|
Span "{$them}" class("cn-table-cell cx-mk-vs-them") data(slot: "table-cell", label: $themName) aria(role: "cell")
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
|
+
|
|
146
|
+
# ── rows, features, stores ──
|
|
147
|
+
# MkRows / MkRow - zigzag rows: the words on one side and a picture on the other (MkStage); reverse puts the words on
|
|
148
|
+
# the right. MkRowCopy is the words: its title (an h2, or h3 under a section that already has one), a line and, in the
|
|
149
|
+
# slot, anything after it (MkChecks).
|
|
150
|
+
# MkRows { MkRow { MkRowCopy(title: "Tu agenda", text: "Se llena sola.") { MkChecks { … } } MkStage { … } } }
|
|
151
|
+
part MkRows() { Stack class("cx-mk-rows") { slot } }
|
|
152
|
+
part MkRow(reverse: bool = false) { Stack class("cx-mk-row", "cx-mk-rev" when $reverse) { slot } }
|
|
153
|
+
part MkRowCopy(title: text, text: text, level: text = "h2") {
|
|
154
|
+
Stack class("cx-mk-row-copy") {
|
|
155
|
+
when $level == "h2" { Title "{$title}" h2 class("cx-mk-h3") }
|
|
156
|
+
when $level != "h2" { Title "{$title}" h3 class("cx-mk-h3") }
|
|
157
|
+
Text "{$text}" class("cx-mk-p")
|
|
158
|
+
slot
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
# MkStage - the soft panel a row's picture sits on; tone "dark" for a dark one.
|
|
162
|
+
part MkStage(tone: text = "") { Stack class("cx-mk-stage", "cx-mk-stage-dark" when $tone == "dark") { slot } }
|
|
163
|
+
# MkFeats / MkFeat - a grid of short features, each with its icon (in the slot) and a label.
|
|
164
|
+
part MkFeats() { Stack class("cx-mk-feats") aria(role: "list") { slot } }
|
|
165
|
+
part MkFeat(label: text) { Stack class("cx-mk-feat") aria(role: "listitem") { Stack class("cx-mk-ic") { slot } Span "{$label}" } }
|
|
166
|
+
# MkStore - a store in a carousel: its phone (a DeviceFrame in the slot), its name and what it is.
|
|
167
|
+
part MkStore(name: text, caption: text = "") { Stack class("cx-mk-store") { slot Span "{$name}" Span "{$caption}" } }
|
package/registry/stacked-bars.js
CHANGED
|
@@ -40,7 +40,7 @@ export function mount(el, inputs, on) {
|
|
|
40
40
|
: rows.map((r, i) => {
|
|
41
41
|
const tip = `${r.label}: ${series.map((s) => `${s.label} ${fmt(Number(r[s.key]) || 0, unit, locale)}`).join(' · ')}`;
|
|
42
42
|
const segs = series.map((s) => { const v = Number(r[s.key]) || 0; return v > 0 ? `<span class="${NS}-seg" data-tone="${esc(s.tone || 'brand')}" style="flex-grow:${v}"></span>` : ''; }).join('');
|
|
43
|
-
return `<button type="button" class="${NS}-bar" data-id="${esc(r.id)}" aria-pressed="${String(r.id) === String(sel)}" aria-label="${esc(tip)}"
|
|
43
|
+
return `<button type="button" class="${NS}-bar" data-id="${esc(r.id)}" aria-pressed="${String(r.id) === String(sel)}" aria-label="${esc(tip)}"${totals[i] && !state.readonly ? ` data-name="${esc(tip)}"` : ' disabled'}><span class="${NS}-stack" style="height:${(totals[i] / top) * 100}%">${segs}</span></button>`;
|
|
44
44
|
}).join('');
|
|
45
45
|
const xs = (busy ? SKELETON.slice(0, Math.max(rows.length, 7)).map(() => ({ label: '' })) : rows).map((r) => `<span data-id="${esc(r.id ?? '')}"${String(r.id) === String(sel) ? ' data-on' : ''}>${esc(r.label)}</span>`).join('');
|
|
46
46
|
const n = busy ? Math.max(rows.length, 7) : rows.length;
|