@muten/shadcn 0.1.7 → 0.1.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@muten/shadcn",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
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",
@@ -40,6 +40,11 @@
40
40
  // lang, currency, locale and cents change live too.
41
41
  export function mount(el, inputs, on) {
42
42
  let api = null, map = null, pending = null, lastCmd = String(inputs.command || ''); // the command it was born with never runs
43
+ // an app sends its inputs again on every change of its own, often the same data: repaint only when something drawn
44
+ // changed (`start` only places the first paint, `command` runs apart)
45
+ const SKIP = new Set(['command', 'start']);
46
+ const sigOf = (i) => JSON.stringify(Object.keys(i).filter((k) => !SKIP.has(k)).sort().map((k) => [k, i[k]]));
47
+ let lastSig = sigOf(inputs);
43
48
  import('@muten/shadcn/registry/kit/core.js').then(({ kit, today, addDays }) => {
44
49
  const iso = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
45
50
  const list = (v) => (Array.isArray(v) ? v : []);
@@ -82,8 +87,8 @@ export function mount(el, inputs, on) {
82
87
  onRange: (r) => send('range', { from: iso(r.from), to: iso(r.to), view: r.view }),
83
88
  onChange: (evs) => send('change', evs.map((e) => ({ id: e.id, resource: e.resource, day: iso(e.day), start: e.start, dur: e.dur, status: e.status || '' }))),
84
89
  });
85
- if (pending) { api.update(data(pending)); pending = null; }
90
+ if (pending) { const sig = sigOf(pending); if (sig !== lastSig) { lastSig = sig; api.update(data(pending)); } pending = null; }
86
91
  });
87
92
  const command = (next) => { const c = String(next.command || ''); if (c === lastCmd) return; lastCmd = c; if (c) api.run(c.split('#')[0]); };
88
- return (next) => { if (api && map) { api.update(map(next)); command(next); } else pending = next; };
93
+ return (next) => { if (!api || !map) { pending = next; return; } const sig = sigOf(next); if (sig !== lastSig) { lastSig = sig; api.update(map(next)); } command(next); };
89
94
  }
@@ -152,7 +152,8 @@ watchReveal();
152
152
  };
153
153
 
154
154
  // ── tabs with a sliding indicator (for panels a Custom builds) ──
155
- const initTabs = (list) => {
155
+ // redraws: the owner paints the tabs again on a click (the agenda), so a click does not animate tabs about to go
156
+ const initTabs = (list, { redraws = false } = {}) => {
156
157
  const tabs = $$('.cx-tab', list);
157
158
  const ind = document.createElement('span'); ind.className = 'cx-tab-ind'; list.prepend(ind);
158
159
  const place = (animate) => {
@@ -168,14 +169,15 @@ watchReveal();
168
169
  place(true);
169
170
  list.dispatchEvent(new CustomEvent('pick', { detail: tabs.indexOf(tab) }));
170
171
  };
171
- tabs.forEach((t) => { t.tabIndex = t.getAttribute('aria-selected') === 'true' ? 0 : -1; t.onclick = () => pick(t); });
172
+ tabs.forEach((t) => { t.tabIndex = t.getAttribute('aria-selected') === 'true' ? 0 : -1; if (!redraws) t.onclick = () => pick(t); });
172
173
  list.addEventListener('keydown', (e) => {
173
174
  const i = tabs.indexOf(document.activeElement); if (i < 0) return;
174
175
  const to = { ArrowRight: i + 1, ArrowLeft: i - 1, Home: 0, End: tabs.length - 1 }[e.key];
175
176
  if (to === undefined) return; e.preventDefault(); pick(tabs[(to + tabs.length) % tabs.length], true);
176
177
  });
178
+ // the first placing comes from the observer too: it runs once the browser has laid the tabs out, so it measures
179
+ // without forcing a layout of the whole page (a list painted again and again paid two each time)
177
180
  new ResizeObserver(() => place(false)).observe(list);
178
- place(false);
179
181
  return pick;
180
182
  };
181
183
 
@@ -717,7 +719,7 @@ let wheelsNow = [];
717
719
  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>` : ''}`; };
718
720
  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 }); };
719
721
  const paint = () => {
720
- refit();
722
+ if (fit) { const k = `${eff()}|${from}|${to}|${narrow}`; if (k !== fitKey && fitSeen.has(k)) { hourPx = fitSeen.get(k); fitKey = k; } }
721
723
  if (typeof drag !== 'undefined' && drag) { const d = drag; drag = null; clearTimeout(d.timer); cancelAnimationFrame(scrollRaf); d.ghost?.remove(); }
722
724
  const v = eff(); // the phone keeps the calendar: day grid or list, the week needs a wide screen
723
725
  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 }));
@@ -728,8 +730,9 @@ let wheelsNow = [];
728
730
  if (!loading) { hadNow = top.includes('cx-ag-nowcard'); hadQueue = top.includes('cx-ag-queue'); }
729
731
  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>`;
730
732
  root.toggleAttribute('data-loading', !!loading);
731
- $$('.cx-tabs', root).forEach(initTabs);
733
+ $$('.cx-tabs', root).forEach((tl) => initTabs(tl, { redraws: true }));
732
734
  const now = $('.cx-ag-now', root); if (now) $('.cx-ag-scroll', root).scrollTop = Math.max(0, parseFloat(now.style.top) - 120);
735
+ if (refit()) { paint(); return; } // the hours' height changed: once more at the right size
733
736
  tellRange();
734
737
  };
735
738
  // drag to move · drag the bottom edge to resize (15 min). A ghost shows where it lands and turns red where the
@@ -848,7 +851,7 @@ let wheelsNow = [];
848
851
  $$('.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)); }
849
852
  $$('.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'; });
850
853
  }, 30000);
851
- new ResizeObserver(() => requestAnimationFrame(() => { const n = root.clientWidth < 700; const h = fit ? root.querySelector('.cx-ag-scroll')?.clientHeight || 0 : 0; if (n !== narrow) { narrow = n; paint(); } else if (fit && h && h !== fitH) paint(); })).observe(root);
854
+ new ResizeObserver(() => requestAnimationFrame(() => { const n = root.clientWidth < 700; const h = fit ? root.querySelector('.cx-ag-scroll')?.clientHeight || 0 : 0; if (n !== narrow) { narrow = n; paint(); } else if (fit && h && h !== fitH) { fitKey = ''; fitSeen.clear(); paint(); } })).observe(root);
852
855
 
853
856
  // ── the event sheet (showcase): detail · lifecycle · payment · move (second step) · cancel (second step).
854
857
  // In app mode a tapped cita goes to onOpen and the app opens its own sheet. ──
@@ -971,16 +974,23 @@ let wheelsNow = [];
971
974
  body.innerHTML = '<div class="cx-ag-datesheet"></div>';
972
975
  dateUI($('.cx-ag-datesheet', body), (r) => { closeModal(); applyDate(r); });
973
976
  };
974
- // fit: the hours share the height the app gives the agenda (never under baseHour), as a day view on a monitor should
975
- let hourPx = baseHour, fitH = 0;
977
+ // fit: the hours share the height the app gives the agenda (never under baseHour), as a day view on a monitor should.
978
+ // Measured after a paint and only when what decides it changed (view, hours, width, the agenda's height): reading
979
+ // the layout on every paint forced the whole page to lay out each time
980
+ let hourPx = baseHour, fitH = 0, fitKey = '';
981
+ const fitSeen = new Map(); // each view's measured hour, so going back to a view paints once at its size
976
982
  const refit = () => {
977
- if (!fit) return;
983
+ if (!fit) return false;
984
+ const key = `${eff()}|${from}|${to}|${narrow}`; if (key === fitKey) return false;
978
985
  const sc = root.querySelector('.cx-ag-scroll'), ag = sc ? sc.querySelector('.cx-ag') : null, hours = sc ? sc.querySelector('.cx-ag-body') : null;
979
- if (!sc || !sc.clientHeight || !ag || !hours) return;
980
- fitH = sc.clientHeight;
986
+ if (!sc || !sc.clientHeight || !ag || !hours) return false;
987
+ fitKey = key; fitH = sc.clientHeight;
981
988
  // everything in the scroll that is not the hours (the day heads, gaps, padding) keeps its height
982
989
  const around = ag.offsetHeight - hours.offsetHeight;
983
- hourPx = Math.max(baseHour, Math.floor((sc.clientHeight - around) / Math.max(1, to - from)));
990
+ const next = Math.max(baseHour, Math.floor((sc.clientHeight - around) / Math.max(1, to - from)));
991
+ fitSeen.set(key, next);
992
+ if (next === hourPx) return false;
993
+ hourPx = next; return true;
984
994
  };
985
995
  let blockDay = null; // the one open day of the week, when the form had nothing to choose from
986
996
  const openBlock = () => { blockOpen = true; sheetOf('agBlock'); };