@keenmate/pure-admin-core 2.9.0-rc03 → 2.9.0-rc05

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.
Files changed (35) hide show
  1. package/README.md +55 -13
  2. package/dist/css/main.css +554 -7
  3. package/package.json +3 -1
  4. package/snippets/buttons.html +51 -0
  5. package/snippets/cards.html +132 -47
  6. package/snippets/comparison.html +26 -22
  7. package/snippets/manifest.json +180 -115
  8. package/snippets/range-group.html +125 -0
  9. package/snippets/splitter.html +44 -38
  10. package/snippets/statistics.html +31 -0
  11. package/src/js/btn-split-auto-absorb.js +327 -0
  12. package/src/js/command-palette.js +472 -0
  13. package/src/js/file-selector.js +1275 -0
  14. package/src/js/internal/logging.js +121 -0
  15. package/src/js/logic-tree-renderer.js +303 -0
  16. package/src/js/modal-dialogs.js +460 -0
  17. package/src/js/overflow.js +371 -0
  18. package/src/js/pa-stat-fit.js +184 -0
  19. package/src/js/range-group.js +663 -0
  20. package/src/js/search-autocomplete-v2.js +907 -0
  21. package/src/js/search-autocomplete.js +434 -0
  22. package/src/js/settings-panel.js +245 -0
  23. package/src/js/split-button.js +141 -0
  24. package/src/js/splitter.js +1323 -0
  25. package/src/js/toast-service.js +302 -0
  26. package/src/js/tooltips-popovers.js +275 -0
  27. package/src/js/virtual-scroll.js +143 -0
  28. package/src/js/virtual-textbox.js +803 -0
  29. package/src/scss/_core.scss +7 -0
  30. package/src/scss/core-components/_buttons.scss +44 -0
  31. package/src/scss/core-components/_cards.scss +95 -6
  32. package/src/scss/core-components/_overflow.scss +50 -0
  33. package/src/scss/core-components/_range-group.scss +474 -0
  34. package/src/scss/core-components/_statistics.scss +163 -0
  35. package/src/scss/variables/_components.scss +41 -2
@@ -0,0 +1,371 @@
1
+ /**
2
+ * Pure Admin — Overflow (progressive-collapse row)
3
+ *
4
+ * Generic progressive-collapse primitive: a horizontal row of children that
5
+ * walks the lowest-priority items into a "more" menu when the row can't fit,
6
+ * and restores them as space comes back. Originally lived as the
7
+ * `pa-card__actions--overflow` variant in `_cards.scss`; promoted to a
8
+ * standalone block (`pa-overflow`) so toolbars, button bars, breadcrumbs,
9
+ * tab rows, chip rows — anything with a flex row of inline-block children —
10
+ * can reuse it without the card scaffolding.
11
+ *
12
+ * Auto-inits on:
13
+ * .pa-overflow — the generic primitive (preferred)
14
+ * .pa-card__actions--overflow — the card-flavoured alias (back-compat)
15
+ *
16
+ * Drop order: lowest `data-pa-actions-priority` (default 0) drops first.
17
+ * Ties broken by DOM order, with the tiebreak direction set on the wrapper:
18
+ *
19
+ * data-pa-actions-overflow-from="end" (default) — rightmost drops first
20
+ * data-pa-actions-overflow-from="start" — leftmost drops first
21
+ *
22
+ * Pin a button so it never collapses by giving it a high priority (e.g. 10).
23
+ *
24
+ * <div class="pa-overflow" data-pa-actions-overflow-from="end">
25
+ * <button class="pa-btn pa-btn--xs">Save</button>
26
+ * <button class="pa-btn pa-btn--xs">Format</button>
27
+ * <button class="pa-btn pa-btn--xs"
28
+ * data-pa-actions-priority="10">Run</button>
29
+ * </div>
30
+ *
31
+ * Algorithm: restore everything to the wrapper, check `scrollWidth >
32
+ * clientWidth`, move items by ascending priority until it fits. Avoids
33
+ * the cached-width math entirely — what the browser thinks fits is the
34
+ * source of truth.
35
+ *
36
+ * Layout contract: the wrapper needs `min-width: 0; overflow: hidden;
37
+ * flex-shrink: 1` so the row actually shrinks below its content (otherwise
38
+ * `scrollWidth > clientWidth` never becomes truthful). `_overflow.scss`
39
+ * supplies that for `.pa-overflow`; the card variant carries equivalent
40
+ * rules in `_cards.scss` under `.pa-card__header`.
41
+ *
42
+ * Opt in to console diagnostics: `window.PA_OVERFLOW_DEBUG = true`
43
+ * before page load. (`window.PA_CARD_ACTIONS_OVERFLOW_DEBUG` still works.)
44
+ *
45
+ * Public API (window.PaOverflow):
46
+ * init(el) — idempotent single-element init
47
+ * initAll(scope) — initialize all uninitialized overflow wrappers under scope
48
+ *
49
+ * `window.PaCardActionsOverflow` is preserved as a back-compat alias.
50
+ */
51
+ (function () {
52
+ 'use strict';
53
+
54
+ var INIT_FLAG = '__paOverflowInit';
55
+
56
+ // Selectors we auto-init on. The card-actions modifier stays in the
57
+ // list so existing markup keeps working without having to add the
58
+ // generic class manually.
59
+ var SELECTOR = '.pa-overflow, .pa-card__actions--overflow';
60
+
61
+ function matchesContract(el) {
62
+ return el && el.classList && (
63
+ el.classList.contains('pa-overflow') ||
64
+ el.classList.contains('pa-card__actions--overflow')
65
+ );
66
+ }
67
+
68
+ // Floating UI (optional) — used to position the overflow menu. Mirrors
69
+ // the helpers `split-button.js` pulls from `window.FloatingUIDOM`. We
70
+ // capture them once at module scope so each instance doesn't repeat
71
+ // the lookup; if the library isn't loaded we transparently fall back
72
+ // to a hand-rolled `getBoundingClientRect` positioner below.
73
+ var FUI = window.FloatingUIDOM || null;
74
+
75
+ function init(root) {
76
+ if (!root || root[INIT_FLAG]) return;
77
+ if (!matchesContract(root)) return;
78
+ root[INIT_FLAG] = true;
79
+
80
+ var items = Array.prototype.slice.call(root.children).filter(function (el) {
81
+ return el.nodeType === 1;
82
+ });
83
+ if (items.length === 0) return;
84
+
85
+ var ordered = items.map(function (el, idx) {
86
+ var p = parseInt(el.getAttribute('data-pa-actions-priority'), 10);
87
+ return { el: el, priority: isNaN(p) ? 0 : p, domIndex: idx };
88
+ });
89
+
90
+ // Drop order recomputed on every relayout via `buildDropOrder()` —
91
+ // cheap (one sort over N items) and means the wrapper's
92
+ // `data-pa-actions-overflow-from` attribute can be flipped at
93
+ // runtime (a MutationObserver below also forces a re-layout when
94
+ // that attribute changes).
95
+ var dropOrder = [];
96
+ function buildDropOrder() {
97
+ var fromAttr = root.getAttribute('data-pa-actions-overflow-from');
98
+ var dropFromStart = fromAttr === 'start';
99
+ dropOrder = ordered.slice().sort(function (a, b) {
100
+ if (a.priority !== b.priority) return a.priority - b.priority;
101
+ return dropFromStart ? (a.domIndex - b.domIndex) : (b.domIndex - a.domIndex);
102
+ });
103
+ }
104
+ buildDropOrder();
105
+
106
+ // Opt-in diagnostics — set `window.PA_OVERFLOW_DEBUG = true` (or the
107
+ // pre-rename `PA_CARD_ACTIONS_OVERFLOW_DEBUG`) before page load to
108
+ // see init / relayout / move-to-menu traces.
109
+ var DEBUG = window.PA_OVERFLOW_DEBUG === true || window.PA_CARD_ACTIONS_OVERFLOW_DEBUG === true;
110
+ var instanceLabel = '[pa-overflow#' + (root.id || ordered.map(function (i) { return i.el.tagName; }).join('-')).slice(0, 40) + ']';
111
+ function log() {
112
+ if (!DEBUG) return;
113
+ var args = Array.prototype.slice.call(arguments);
114
+ console.log.apply(console, [instanceLabel].concat(args));
115
+ }
116
+
117
+ // Trigger button — the "..." affordance. Carries both the generic
118
+ // and the card-flavoured class so any existing SCSS targeting the
119
+ // old `.pa-card__actions-overflow-trigger` selector keeps working
120
+ // (themes / consumer overrides) without forcing a coordinated update.
121
+ var trigger = document.createElement('button');
122
+ trigger.type = 'button';
123
+ trigger.className = 'pa-btn pa-btn--xs pa-btn--ghost pa-overflow__trigger pa-card__actions-overflow-trigger';
124
+ trigger.setAttribute('aria-label', 'More actions');
125
+ trigger.setAttribute('aria-haspopup', 'menu');
126
+ trigger.setAttribute('aria-expanded', 'false');
127
+ trigger.innerHTML = '<i class="fa-solid fa-ellipsis-vertical" aria-hidden="true"></i>';
128
+ trigger.style.display = 'none';
129
+ root.appendChild(trigger);
130
+
131
+ // Menu reuses pa-btn-split__menu — same container styling and
132
+ // `--open` toggle behaviour as the standard split button, no
133
+ // parallel styling to maintain. Lives on <body> so card / wrapper
134
+ // overflow:hidden can't clip it; positioned via Floating UI.
135
+ var menu = document.createElement('div');
136
+ menu.className = 'pa-btn-split__menu';
137
+ menu.setAttribute('role', 'menu');
138
+ document.body.appendChild(menu);
139
+ var menuInner = document.createElement('div');
140
+ menuInner.className = 'pa-btn-split__menu-inner';
141
+ menu.appendChild(menuInner);
142
+
143
+ log('init', { itemCount: ordered.length, priorities: ordered.map(function (i) { return i.priority; }) });
144
+
145
+ // When we move an item into the menu we replace its classList with
146
+ // `pa-btn-split__item` so it adopts the standard split-button menu
147
+ // row styling; the original classList is stashed on the element
148
+ // itself and restored on the way back.
149
+ function moveToMenu(el) {
150
+ if (el.__paOverflowOrigClass == null) {
151
+ el.__paOverflowOrigClass = el.className;
152
+ }
153
+ el.className = 'pa-btn-split__item';
154
+ menuInner.appendChild(el);
155
+ }
156
+
157
+ function moveToRoot(el) {
158
+ if (el.__paOverflowOrigClass != null) {
159
+ el.className = el.__paOverflowOrigClass;
160
+ }
161
+ // insertBefore moves the node if it's already attached, so we
162
+ // always call it — that keeps the items in original DOM order
163
+ // even after a drop-from-start cycle reshuffled them.
164
+ root.insertBefore(el, trigger);
165
+ }
166
+
167
+ function relayout() {
168
+ log('relayout entered');
169
+
170
+ // Step 1 — bring everything back to root in DOM order, hide trigger.
171
+ ordered.slice().sort(function (a, b) {
172
+ return a.domIndex - b.domIndex;
173
+ }).forEach(function (item) {
174
+ moveToRoot(item.el);
175
+ });
176
+ trigger.style.display = 'none';
177
+
178
+ // Step 2 — does everything fit? `scrollWidth > clientWidth` is the
179
+ // truthful "browser thinks this overflows" signal; requires the
180
+ // wrapper to have `min-width: 0; overflow: hidden` (set in SCSS)
181
+ // so the wrapper actually shrinks below its content when there's
182
+ // not enough room.
183
+ var clientW = root.clientWidth;
184
+ var scrollW = root.scrollWidth;
185
+ var parent = root.parentNode;
186
+ var parentW = parent ? parent.clientWidth : null;
187
+ var rootComputedStyle = getComputedStyle(root);
188
+ log('relayout step 2', {
189
+ clientW: clientW,
190
+ scrollW: scrollW,
191
+ fitsAll: scrollW <= clientW,
192
+ parentClientW: parentW,
193
+ rootMinWidth: rootComputedStyle.minWidth,
194
+ rootOverflow: rootComputedStyle.overflow,
195
+ rootFlex: rootComputedStyle.flex,
196
+ rootDisplay: rootComputedStyle.display
197
+ });
198
+
199
+ if (scrollW <= clientW) {
200
+ log('all fits, hiding trigger');
201
+ if (menu.classList.contains('pa-btn-split__menu--open')) closeMenu();
202
+ return;
203
+ }
204
+
205
+ // Step 3 — needs to overflow. Show trigger; walk drop order and
206
+ // move items into the menu one at a time, re-checking after each
207
+ // move. Stop as soon as `scrollWidth <= clientWidth`.
208
+ trigger.style.display = '';
209
+
210
+ for (var i = 0; i < dropOrder.length; i++) {
211
+ if (root.scrollWidth <= root.clientWidth) break;
212
+ var el = dropOrder[i].el;
213
+ moveToMenu(el);
214
+ log('moved to menu', el, { remainingScroll: root.scrollWidth, clientW: root.clientWidth });
215
+ }
216
+ log('relayout done', { inMenu: menuInner.children.length, inRoot: ordered.length - menuInner.children.length });
217
+ }
218
+
219
+ // Floating UI's autoUpdate returns a cleanup fn — we hold it while
220
+ // open so closeMenu() can stop watching scroll/resize/IntersectionObserver
221
+ // for free, instead of unhooking listeners by hand.
222
+ var stopAutoUpdate = null;
223
+
224
+ function openMenu() {
225
+ if (menuInner.children.length === 0) return; // nothing to show
226
+ menu.classList.add('pa-btn-split__menu--open');
227
+ trigger.setAttribute('aria-expanded', 'true');
228
+
229
+ if (FUI && FUI.computePosition && FUI.autoUpdate) {
230
+ // autoUpdate fires the callback on init AND whenever scroll
231
+ // ancestors, the viewport, or the reference itself move —
232
+ // handles flip / shift / off-screen retraction without our
233
+ // own scroll/resize listeners.
234
+ stopAutoUpdate = FUI.autoUpdate(trigger, menu, function () {
235
+ // Trigger gone (display:none on it or any ancestor —
236
+ // e.g. the splitter rail-minimize hides all buttons in
237
+ // a minimized pane) → close the menu so it doesn't
238
+ // hang in the middle of the page detached from any
239
+ // anchor.
240
+ if (trigger.offsetParent === null) {
241
+ closeMenu();
242
+ return;
243
+ }
244
+ var rect = trigger.getBoundingClientRect();
245
+ if (rect.width === 0 && rect.height === 0) {
246
+ closeMenu();
247
+ return;
248
+ }
249
+ FUI.computePosition(trigger, menu, {
250
+ placement: 'bottom-end',
251
+ strategy: 'fixed',
252
+ middleware: [
253
+ FUI.offset(4),
254
+ FUI.flip(),
255
+ FUI.shift({ padding: 8 })
256
+ ]
257
+ }).then(function (pos) {
258
+ Object.assign(menu.style, {
259
+ position: 'fixed',
260
+ left: pos.x + 'px',
261
+ top: pos.y + 'px',
262
+ right: 'auto'
263
+ });
264
+ });
265
+ });
266
+ } else {
267
+ positionMenuFallback();
268
+ window.addEventListener('scroll', positionMenuFallback, true);
269
+ window.addEventListener('resize', positionMenuFallback);
270
+ }
271
+
272
+ setTimeout(function () {
273
+ document.addEventListener('mousedown', onDocClick);
274
+ }, 0);
275
+ }
276
+
277
+ function closeMenu() {
278
+ menu.classList.remove('pa-btn-split__menu--open');
279
+ trigger.setAttribute('aria-expanded', 'false');
280
+ if (stopAutoUpdate) {
281
+ stopAutoUpdate();
282
+ stopAutoUpdate = null;
283
+ }
284
+ window.removeEventListener('scroll', positionMenuFallback, true);
285
+ window.removeEventListener('resize', positionMenuFallback);
286
+ document.removeEventListener('mousedown', onDocClick);
287
+ }
288
+
289
+ function positionMenuFallback() {
290
+ // Used only if Floating UI isn't loaded — no flip, no shift,
291
+ // no scroll-into-corner handling.
292
+ var rect = trigger.getBoundingClientRect();
293
+ menu.style.position = 'fixed';
294
+ menu.style.top = (rect.bottom + 4) + 'px';
295
+ menu.style.right = (window.innerWidth - rect.right) + 'px';
296
+ menu.style.left = 'auto';
297
+ }
298
+
299
+ function onDocClick(e) {
300
+ if (menu.contains(e.target) || trigger.contains(e.target)) return;
301
+ closeMenu();
302
+ }
303
+
304
+ trigger.addEventListener('click', function (e) {
305
+ e.stopPropagation();
306
+ if (menu.classList.contains('pa-btn-split__menu--open')) closeMenu();
307
+ else openMenu();
308
+ });
309
+
310
+ menu.addEventListener('click', function (e) {
311
+ // Any click on a menu row defers a close — the row's original
312
+ // click handler fires first because we don't preventDefault.
313
+ if (!e.target.closest('.pa-btn-split__item')) return;
314
+ setTimeout(closeMenu, 0);
315
+ });
316
+
317
+ // First paint — wait for layout, then run relayout.
318
+ requestAnimationFrame(relayout);
319
+
320
+ if (typeof ResizeObserver !== 'undefined') {
321
+ var ro = new ResizeObserver(function (entries) {
322
+ log('ResizeObserver', entries.map(function (e) {
323
+ return { target: e.target === root ? 'root' : 'parent', width: e.contentRect.width };
324
+ }));
325
+ // Any card resize closes the menu. UX is cleaner than
326
+ // dragging the menu around mid-resize; the user can reopen
327
+ // once they've settled the layout.
328
+ if (menu.classList.contains('pa-btn-split__menu--open')) {
329
+ closeMenu();
330
+ }
331
+ relayout();
332
+ });
333
+ ro.observe(root);
334
+ // Also observe the parent header — when items are in the menu, the
335
+ // wrapper sizes to its (smaller) content and doesn't fire its own
336
+ // size change when the parent grows. Watching the parent gives us
337
+ // a signal to attempt restoring items.
338
+ if (root.parentNode) ro.observe(root.parentNode);
339
+ }
340
+
341
+ // React to `data-pa-actions-overflow-from` flips so consumers can
342
+ // change drop direction at runtime (eg. a settings toggle) without
343
+ // re-initializing the wrapper.
344
+ if (typeof MutationObserver !== 'undefined') {
345
+ var mo = new MutationObserver(function () {
346
+ buildDropOrder();
347
+ relayout();
348
+ });
349
+ mo.observe(root, { attributes: true, attributeFilter: ['data-pa-actions-overflow-from'] });
350
+ }
351
+ }
352
+
353
+ function initAll(scope) {
354
+ var nodes = (scope || document).querySelectorAll(SELECTOR);
355
+ for (var i = 0; i < nodes.length; i++) init(nodes[i]);
356
+ }
357
+
358
+ var api = { init: init, initAll: initAll };
359
+ window.PaOverflow = api;
360
+ // Back-compat: `card-actions-overflow.js` exported this global; keep
361
+ // pointing at the same { init, initAll } pair so existing consumers
362
+ // calling `window.PaCardActionsOverflow.initAll(scope)` (after
363
+ // injecting dynamic markup) keep working.
364
+ window.PaCardActionsOverflow = api;
365
+
366
+ if (document.readyState === 'loading') {
367
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
368
+ } else {
369
+ initAll();
370
+ }
371
+ })();
@@ -0,0 +1,184 @@
1
+ /* ============================================================================
2
+ pa-stat-fit.js — fit-to-box for pa-stat--square[data-pa-stat-fit]
3
+
4
+ Opt-in companion to the SCSS "fit + progressive disclosure" mode. For every
5
+ .pa-stat--square[data-pa-stat-fit] it:
6
+
7
+ 1. Wraps the authored .pa-stat__number (+ optional .pa-stat__symbol, in
8
+ their authored order so prefix "$847K" and suffix "87%" both work) into
9
+ a .pa-stat__slot > .pa-stat__group structure. Idempotent.
10
+ 2. Sizes .pa-stat__group so it fills .pa-stat__slot at the largest font
11
+ that still fits — never overflowing, independent of character count
12
+ (which a pure-CSS `cqi` clamp can't guarantee). The symbol scales with
13
+ the number because the SCSS sizes it in `em`.
14
+ 3. Re-fits whenever the slot changes size — including when the CSS
15
+ @container ladder reveals/hides the label / change / context rows
16
+ (which grows or shrinks the slot).
17
+
18
+ The progressive disclosure itself (which rows show at which size) is pure
19
+ CSS — see core-components/_statistics.scss. This file only does the one
20
+ thing CSS can't: fit the primary number to the box.
21
+
22
+ Public API (window.PaStatFit):
23
+ init(root) — wrap + observe every fit tile under `root` (default document)
24
+ refresh(el) — re-run fit on one tile (or all, if omitted); call after you
25
+ change the number's text content programmatically
26
+ ========================================================================== */
27
+ (function () {
28
+ 'use strict';
29
+
30
+ var BASE = 200; // measure the group at a known font-size, then scale linearly
31
+ var SAFETY = 0.92; // leave a hair of breathing room so glyphs never kiss the edge
32
+ // The number is the HERO; the metadata is subordinate. Its column font-size is set
33
+ // to this fraction of the TILE HEIGHT, so the label / trend / context stay small and
34
+ // — crucially — CONSISTENT across a row of same-height tiles. (Basing it on the fitted
35
+ // number size looked right on one tile but drifted between tiles: a wide "847K" fits
36
+ // at a different size than "87", so the meta came out bigger on some tiles than others.
37
+ // Tile height is uniform across the row, so the meta matches while still scaling if the
38
+ // tile itself grows/shrinks.) This is THE hierarchy knob — lower = tinier meta. The
39
+ // rows are em-relative in the SCSS, so this one value drives all three.
40
+ var META_RATIO = 0.10; // ~14px label on a 140px tile (was 0.07 ≈ 10px, too small to read)
41
+ var META_MIN_PX = 12; // …but never below this: on a short tile the proportional size
42
+ // would go illegible. Floors the __label; __change/__context are
43
+ // 0.9em/0.8em of it, so the smallest row bottoms out ~9.6px.
44
+ var HORIZONTAL_MIN_REM = 32; // keep in sync with $stat-square-fit-horizontal-min-w
45
+ var observed = []; // [{ tile, slot }] for refresh()
46
+
47
+ function rootFontPx() {
48
+ return parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
49
+ }
50
+
51
+ // Switch a tile to the horizontal "banner" layout once it's wide enough.
52
+ // Done in JS (not a @container query) because the tile is its own size
53
+ // container, and a container query can't restyle the container element itself.
54
+ function applyMode(tile) {
55
+ var wide = tile.clientWidth >= HORIZONTAL_MIN_REM * rootFontPx();
56
+ tile.classList.toggle('pa-stat--fit-wide', wide);
57
+ }
58
+
59
+ // Wrap the authored-flat children:
60
+ // __number + __symbol (in DOM order) → __slot > __group (the fit target)
61
+ // __label / __change / __context → __meta (the metadata column)
62
+ // so the metadata can move beside the number in the horizontal banner layout.
63
+ // Returns the slot element, or null if there's no number to fit.
64
+ function ensureStructure(tile) {
65
+ var existing = tile.querySelector(':scope > .pa-stat__slot');
66
+ if (existing) return existing;
67
+
68
+ var numberParts = [];
69
+ var metaParts = [];
70
+ Array.prototype.forEach.call(tile.children, function (child) {
71
+ if (child.classList.contains('pa-stat__number') ||
72
+ child.classList.contains('pa-stat__symbol')) {
73
+ numberParts.push(child);
74
+ } else if (child.classList.contains('pa-stat__label') ||
75
+ child.classList.contains('pa-stat__change') ||
76
+ child.classList.contains('pa-stat__context')) {
77
+ metaParts.push(child);
78
+ }
79
+ });
80
+ if (!numberParts.length) return null;
81
+
82
+ var group = document.createElement('span');
83
+ group.className = 'pa-stat__group';
84
+ var slot = document.createElement('div');
85
+ slot.className = 'pa-stat__slot';
86
+
87
+ // Drop the slot where the first number part lived, then move the parts into
88
+ // the group (appendChild relocates them, preserving authored order).
89
+ tile.insertBefore(slot, numberParts[0]);
90
+ numberParts.forEach(function (part) { group.appendChild(part); });
91
+ slot.appendChild(group);
92
+
93
+ // Wrap the metadata rows into a column after the slot.
94
+ if (metaParts.length) {
95
+ var meta = document.createElement('div');
96
+ meta.className = 'pa-stat__meta';
97
+ tile.insertBefore(meta, metaParts[0]);
98
+ metaParts.forEach(function (part) { meta.appendChild(part); });
99
+ }
100
+
101
+ return slot;
102
+ }
103
+
104
+ // Scale .pa-stat__group to fill its slot without overflowing either axis.
105
+ function fitSlot(slot) {
106
+ if (!slot) return;
107
+ var group = slot.querySelector('.pa-stat__group');
108
+ if (!group) return;
109
+
110
+ group.style.fontSize = BASE + 'px';
111
+ var gw = group.offsetWidth, gh = group.offsetHeight;
112
+ var sw = slot.clientWidth, sh = slot.clientHeight;
113
+ if (!gw || !gh || !sw || !sh) return;
114
+
115
+ var scale = Math.min(sw / gw, sh / gh) * SAFETY;
116
+ group.style.fontSize = (BASE * scale) + 'px';
117
+
118
+ // Size the metadata column as a small fraction of the TILE HEIGHT so it stays
119
+ // subordinate AND consistent across same-height tiles (not coupled to the per-tile
120
+ // number size). The slot's parent is the tile; the meta column is its sibling.
121
+ // label/change/context are em-relative, so this one set cascades to all three.
122
+ var tile = slot.parentNode;
123
+ var meta = tile && tile.querySelector(':scope > .pa-stat__meta');
124
+ if (meta && tile.clientHeight) {
125
+ meta.style.fontSize = Math.max(META_MIN_PX, tile.clientHeight * META_RATIO) + 'px';
126
+ }
127
+ }
128
+
129
+ // ResizeObserver fires when a watched box changes. We watch:
130
+ // • the TILE → toggle the horizontal/vertical layout class by its width
131
+ // • the SLOT → refit the number (also catches the @container disclosure
132
+ // ladder growing/shrinking the slot, which doesn't resize the tile)
133
+ var ro = typeof ResizeObserver !== 'undefined'
134
+ ? new ResizeObserver(function (entries) {
135
+ entries.forEach(function (entry) {
136
+ var el = entry.target;
137
+ if (el.classList.contains('pa-stat__slot')) {
138
+ fitSlot(el);
139
+ } else {
140
+ applyMode(el);
141
+ fitSlot(el.querySelector(':scope > .pa-stat__slot'));
142
+ }
143
+ });
144
+ })
145
+ : null;
146
+
147
+ function setupTile(tile) {
148
+ if (tile.__paStatFit) return;
149
+ var slot = ensureStructure(tile);
150
+ if (!slot) return;
151
+ tile.__paStatFit = true;
152
+ observed.push({ tile: tile, slot: slot });
153
+ applyMode(tile); // set initial layout before first fit
154
+ if (ro) { ro.observe(tile); ro.observe(slot); }
155
+ fitSlot(slot); // initial pass (also covers no-ResizeObserver fallback)
156
+ }
157
+
158
+ function init(root) {
159
+ (root || document)
160
+ .querySelectorAll('.pa-stat--square[data-pa-stat-fit]')
161
+ .forEach(setupTile);
162
+ }
163
+
164
+ // Re-fit after the number text changes (font-size needs recomputing even
165
+ // though the slot box didn't move, so ResizeObserver wouldn't fire).
166
+ function refresh(el) {
167
+ if (el) {
168
+ var slot = el.classList && el.classList.contains('pa-stat__slot')
169
+ ? el
170
+ : el.querySelector('.pa-stat__slot');
171
+ fitSlot(slot);
172
+ return;
173
+ }
174
+ observed.forEach(function (rec) { fitSlot(rec.slot); });
175
+ }
176
+
177
+ if (document.readyState === 'loading') {
178
+ document.addEventListener('DOMContentLoaded', function () { init(); });
179
+ } else {
180
+ init();
181
+ }
182
+
183
+ window.PaStatFit = { init: init, refresh: refresh };
184
+ })();