@keenmate/pure-admin-core 2.9.0-rc01 → 2.9.0-rc04

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 (36) hide show
  1. package/README.md +61 -14
  2. package/dist/css/main.css +748 -2
  3. package/package.json +3 -1
  4. package/snippets/buttons.html +51 -0
  5. package/snippets/cards.html +34 -4
  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 +216 -0
  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 +10 -0
  30. package/src/scss/core-components/_buttons.scss +59 -0
  31. package/src/scss/core-components/_cards.scss +206 -0
  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/_splitter.scss +206 -0
  35. package/src/scss/core-components/_statistics.scss +163 -0
  36. package/src/scss/variables/_components.scss +56 -2
@@ -0,0 +1,216 @@
1
+ <!-- ================================
2
+ SPLITTER
3
+ Pure Admin Visual Framework
4
+
5
+ Resizable container with two or more panes. Drag a gutter to resize,
6
+ double-click to collapse / restore, arrow keys nudge size when the
7
+ gutter is focused.
8
+
9
+ CSS + structure here; drag, keyboard, persistence behaviour is wired up
10
+ by packages/core/src/js/splitter.js. The script auto-initializes on
11
+ [data-pa-splitter] at DOMContentLoaded.
12
+
13
+ Markup model: panes and gutters alternate (pane, gutter, pane, gutter,
14
+ …, pane). Each pane carries its own sizing attributes — there's no
15
+ "start" / "end" shorthand, every pane is generic. Works for any
16
+ N ≥ 2.
17
+
18
+ Orientation modifiers (match flexbox direction of the panes):
19
+ --horizontal panes side-by-side, vertical gutter (default)
20
+ --vertical panes stacked, horizontal gutter
21
+
22
+ The splitter takes whatever width/height its parent gives it — make
23
+ sure the parent has a sized cross axis (height for --horizontal,
24
+ width for --vertical) or the panes will collapse.
25
+ ================================ -->
26
+
27
+
28
+ <!-- ================================
29
+ HORIZONTAL SPLIT (2 panes)
30
+ Side-by-side panes; vertical gutter. The first pane has a 200px
31
+ floor and 60% ceiling, defaults to 280px; the second pane absorbs
32
+ the leftover. Persists under "demo-sidebar".
33
+ ================================ -->
34
+
35
+ <div class="pa-splitter pa-splitter--horizontal"
36
+ data-pa-splitter
37
+ data-pa-splitter-id="demo-sidebar"
38
+ style="height: 400px;">
39
+ <div class="pa-splitter__pane"
40
+ data-pa-splitter-size="280px"
41
+ data-pa-splitter-min="200px"
42
+ data-pa-splitter-max="60%">
43
+ <!-- Your sidebar / list / nav content -->
44
+ </div>
45
+ <div class="pa-splitter__gutter"
46
+ role="separator"
47
+ aria-orientation="vertical"
48
+ tabindex="0"></div>
49
+ <div class="pa-splitter__pane">
50
+ <!-- Your main / detail content (absorbs the leftover) -->
51
+ </div>
52
+ </div>
53
+
54
+
55
+ <!-- ================================
56
+ VERTICAL SPLIT (2 panes)
57
+ Stacked panes; horizontal gutter. Useful for editor + console / log
58
+ pane patterns.
59
+ ================================ -->
60
+
61
+ <div class="pa-splitter pa-splitter--vertical"
62
+ data-pa-splitter
63
+ data-pa-splitter-id="demo-console"
64
+ style="height: 400px;">
65
+ <div class="pa-splitter__pane"
66
+ data-pa-splitter-size="60%"
67
+ data-pa-splitter-min="80px"
68
+ data-pa-splitter-max="80%">
69
+ <!-- Editor / primary content -->
70
+ </div>
71
+ <div class="pa-splitter__gutter"
72
+ role="separator"
73
+ aria-orientation="horizontal"
74
+ tabindex="0"></div>
75
+ <div class="pa-splitter__pane">
76
+ <!-- Console / log / secondary -->
77
+ </div>
78
+ </div>
79
+
80
+
81
+ <!-- ================================
82
+ SPACED CARDS
83
+ Set `gap` on the splitter root to put breathing room between the
84
+ panes and the gutter — the JS subtracts it from the available
85
+ space so percent constraints still resolve correctly. Override
86
+ gutter thickness via the --pa-splitter-gutter-size custom property.
87
+ ================================ -->
88
+
89
+ <div class="pa-splitter pa-splitter--horizontal"
90
+ data-pa-splitter
91
+ style="height: 280px; gap: 1.6rem; --pa-splitter-gutter-size: 1rem;">
92
+ <div class="pa-splitter__pane"
93
+ data-pa-splitter-size="40%"
94
+ data-pa-splitter-min="25%"
95
+ data-pa-splitter-max="75%"
96
+ style="padding: 0;">
97
+ <div class="pa-card" style="height: 100%; margin: 0;">…</div>
98
+ </div>
99
+ <div class="pa-splitter__gutter"
100
+ role="separator"
101
+ aria-orientation="vertical"
102
+ tabindex="0"></div>
103
+ <div class="pa-splitter__pane" style="padding: 0;">
104
+ <div class="pa-card" style="height: 100%; margin: 0;">…</div>
105
+ </div>
106
+ </div>
107
+
108
+
109
+ <!-- ================================
110
+ N-PANE LAYOUT (3 panes — sidebar + content + inspector)
111
+ Per-pane attributes drive sizing. The first and last panes opt into
112
+ rail collapse via the minimize marker. Panes without a
113
+ data-pa-splitter-size share the leftover equally.
114
+ ================================ -->
115
+
116
+ <div class="pa-splitter pa-splitter--horizontal"
117
+ data-pa-splitter
118
+ data-pa-splitter-id="demo-three-pane"
119
+ style="height: 400px;">
120
+ <div class="pa-splitter__pane"
121
+ data-pa-splitter-size="240px"
122
+ data-pa-splitter-min="180px"
123
+ data-pa-splitter-max="360px"
124
+ data-pa-splitter-minimize>
125
+ <!-- File tree / nav (minimizable to a left rail) -->
126
+ </div>
127
+ <div class="pa-splitter__gutter"
128
+ role="separator"
129
+ aria-orientation="vertical"
130
+ tabindex="0"></div>
131
+ <div class="pa-splitter__pane"
132
+ data-pa-splitter-min="240px">
133
+ <!-- Editor / main content (fills the rest) -->
134
+ </div>
135
+ <div class="pa-splitter__gutter"
136
+ role="separator"
137
+ aria-orientation="vertical"
138
+ tabindex="0"></div>
139
+ <div class="pa-splitter__pane"
140
+ data-pa-splitter-size="280px"
141
+ data-pa-splitter-min="220px"
142
+ data-pa-splitter-max="420px"
143
+ data-pa-splitter-minimize>
144
+ <!-- Inspector / detail (minimizable to a right rail) -->
145
+ </div>
146
+ </div>
147
+
148
+
149
+ <!-- ================================
150
+ DATA ATTRIBUTES — root
151
+ data-pa-splitter marker; required
152
+ data-pa-splitter-id enables localStorage persistence
153
+ under "pa-splitter:<id>"
154
+ data-pa-splitter-step keyboard step in px (default: 10)
155
+ data-pa-splitter-rail-size rail width in px (default: 40)
156
+ data-pa-splitter-minimize-threshold drag-to-rail snap ratio (default: 0.40)
157
+
158
+ DATA ATTRIBUTES — per pane
159
+ data-pa-splitter-size "240px" or "30%" (default: shared leftover)
160
+ data-pa-splitter-min "150px" or "10%" (default: 0)
161
+ data-pa-splitter-max "400px" or "50%" (default: available)
162
+ data-pa-splitter-minimize marker — pane can collapse to a rail.
163
+ First / last panes dock against the
164
+ container edge; middle panes collapse
165
+ in place and split released slack
166
+ between both neighbours.
167
+
168
+ CHILD ATTRIBUTES
169
+ data-pa-splitter-toggle put on any element inside the splitter
170
+ (typically a button in the card header)
171
+ to trigger collapse / restore on click.
172
+ Toggles the enclosing pane if it's
173
+ minimizable.
174
+
175
+ RAIL-TITLE HOOK (non-card content)
176
+ Any element marked with `[data-pa-splitter-rail-title]` inside a
177
+ minimized pane rotates to vertical writing (sideways-rl). Use this
178
+ when you put plain content (not a `.pa-card`) in a minimizable pane:
179
+
180
+ <div class="pa-splitter__pane" data-pa-splitter-minimize>
181
+ <div data-pa-splitter-rail-title>📁 Files</div>
182
+ <!-- The rest of your pane content here -->
183
+ </div>
184
+
185
+ Cards adapt automatically — `_cards.scss` rotates the card header
186
+ inside any `.pa-splitter__pane--minimized` without needing the
187
+ attribute. No markup change needed for the card snippets above.
188
+
189
+ CSS CUSTOMIZATION
190
+ --pa-splitter-gutter-size gutter thickness (default: 6px)
191
+ --pa-splitter-rail-size rail width when a pane is minimized
192
+ (default: 40px / $splitter-rail-size).
193
+ JS picks this up via getComputedStyle, so
194
+ setting it on `:root` or per-instance via
195
+ inline style overrides the rail width
196
+ without also setting the data-attr.
197
+ gap space between panes and gutter
198
+ (native flexbox property)
199
+
200
+ MODIFIER CLASSES
201
+ pa-splitter--minimize-mirror flip the minimized title 180°
202
+ (transform: scale(-1,-1) on the heading
203
+ inside any `[data-pa-splitter-rail-title]`
204
+ or `.pa-card__header`)
205
+ pa-splitter--accordion (auto, JS-managed) added when the
206
+ container is narrower than the sum of
207
+ all pane mins + gutters/gaps/padding
208
+ AND there are 2+ minimizable panes.
209
+ While active, restoring one pane
210
+ auto-rails the others.
211
+
212
+ JAVASCRIPT API (window.PaSplitter)
213
+ PaSplitter.init(el) initialize a single element (idempotent)
214
+ PaSplitter.initAll(root?) initialize all uninitialized splitters
215
+ under root (default: document)
216
+ ================================ -->
@@ -140,6 +140,23 @@
140
140
  </div>
141
141
  </div>
142
142
 
143
+ <!-- Fit + progressive-disclosure mode (--square[data-pa-stat-fit])
144
+ Opt in with the data-pa-stat-fit attribute and load pa-stat-fit.js.
145
+ The number is sized to fill the tile (never overflows, any char count);
146
+ lower-priority rows reveal as the tile earns BOTH width and height:
147
+ number → symbol → label → change → context.
148
+ Authored markup stays flat — the JS wraps __number + __symbol into a
149
+ __slot > __group at runtime. REQUIRES a height source on the tile
150
+ (grid cell with a set height / explicit height / aspect-ratio), because
151
+ container-type: size makes the box size independently of its content. -->
152
+ <div class="pa-stat pa-stat--square pa-stat--info" data-pa-stat-fit style="height: 16rem;">
153
+ <span class="pa-stat__symbol">$</span>
154
+ <span class="pa-stat__number">847K</span>
155
+ <div class="pa-stat__label">Monthly Revenue</div>
156
+ <div class="pa-stat__change pa-stat__change--positive">▲ 12.5% vs last month</div>
157
+ <div class="pa-stat__context">Updated 2 min ago</div>
158
+ </div>
159
+
143
160
  <!-- All six square colour variants -->
144
161
  <div class="pa-stat pa-stat--square pa-stat--primary"> <div class="pa-stat__number">…</div><div class="pa-stat__symbol">…</div><div class="pa-stat__label">Primary</div> </div>
145
162
  <div class="pa-stat pa-stat--square pa-stat--success"> <div class="pa-stat__number">…</div><div class="pa-stat__symbol">…</div><div class="pa-stat__label">Success</div> </div>
@@ -169,6 +186,13 @@ SUB-ELEMENTS:
169
186
  (red), --neutral (muted).
170
187
  .pa-stat__symbol Square variant only — the decorative
171
188
  watermark symbol on the inline-end side.
189
+ .pa-stat__context Square fit-mode only — lowest-priority
190
+ (P4) caption row (e.g. "Updated 2m ago").
191
+ .pa-stat__slot Square fit-mode only — created by
192
+ pa-stat-fit.js; wraps the fit group.
193
+ .pa-stat__group Square fit-mode only — created by
194
+ pa-stat-fit.js; holds __number + __symbol
195
+ and carries the JS-computed font-size.
172
196
 
173
197
  VARIANTS (mutually exclusive):
174
198
  (default) Icon + content row. Pair with
@@ -180,6 +204,13 @@ VARIANTS (mutually exclusive):
180
204
  --square Coloured tile, big __number, decorative
181
205
  __symbol, bottom-pinned __label. Apply
182
206
  a colour modifier on the same element.
207
+ [data-pa-stat-fit] Attribute (not a class), combine WITH
208
+ --square. Fit-to-box number + priority
209
+ disclosure ladder (number → symbol →
210
+ label → change → context, revealed as the
211
+ tile gains width AND height). Needs
212
+ pa-stat-fit.js loaded and a height source
213
+ on the tile.
183
214
 
184
215
  SQUARE COLOUR MODIFIERS (apply with --square):
185
216
  --primary, --success, --info, --warning, --danger, --secondary
@@ -0,0 +1,327 @@
1
+ /**
2
+ * Pure Admin — Split Button Auto-Absorb
3
+ *
4
+ * Extends the split button (.pa-btn-split) to automatically absorb adjacent
5
+ * sibling buttons sitting next to it inside a `.pa-btn-toolbar` row, when
6
+ * the row can't fit them all on one line. Absorbed items drop into the
7
+ * split button's own dropdown menu and return to the row as space comes
8
+ * back. Reuses the existing split-button toggle / menu / Floating UI
9
+ * positioning — no parallel chrome.
10
+ *
11
+ * Opt-in markup:
12
+ *
13
+ * <div class="pa-btn-toolbar">
14
+ * <button class="pa-btn pa-btn--primary">Save</button>
15
+ * <button class="pa-btn pa-btn--primary">Format</button>
16
+ * <button class="pa-btn pa-btn--primary"
17
+ * data-pa-absorb-priority="10">Refresh</button>
18
+ * <div class="pa-btn-split pa-btn-split--auto-absorb">
19
+ * <button class="pa-btn pa-btn--primary">Run</button>
20
+ * <button class="pa-btn pa-btn--primary pa-btn-split__toggle"
21
+ * onclick="toggleSplitMenu(event)">
22
+ * <i class="fas fa-chevron-down pa-btn-split__chevron"></i>
23
+ * </button>
24
+ * <div class="pa-btn-split__menu">
25
+ * <div class="pa-btn-split__menu-inner">
26
+ * <button class="pa-btn-split__item">Run with options…</button>
27
+ * </div>
28
+ * </div>
29
+ * </div>
30
+ * </div>
31
+ *
32
+ * Drop order: lowest `data-pa-absorb-priority` (default 0) drops first;
33
+ * ties broken by DOM order with the sibling closest to the split button
34
+ * dropping first (`data-pa-absorb-from="end"`, default). Flip with
35
+ * `="start"` to drop the leftmost sibling first instead.
36
+ *
37
+ * Absorbed items keep their original click handlers (the DOM node is
38
+ * preserved). On absorb, their classList is replaced with
39
+ * `pa-btn-split__item` so they pick up the standard menu-row styling; the
40
+ * original classList is stashed on the element and restored when the item
41
+ * returns to the bar.
42
+ *
43
+ * Existing static menu items (rendered into `__menu-inner` server-side)
44
+ * stay BELOW absorbed items: the first static child is captured at init
45
+ * and used as the insertBefore anchor.
46
+ *
47
+ * Layout contract: the bar needs `min-width: 0; overflow: hidden;
48
+ * flex-shrink: 1` so it actually shrinks below its content (otherwise
49
+ * `scrollWidth > clientWidth` never goes truthful). `_buttons.scss`
50
+ * supplies that for `.pa-btn-toolbar`; any flex row with equivalent
51
+ * styling works.
52
+ *
53
+ * Opt in to console diagnostics: `window.PA_BTN_SPLIT_AUTO_ABSORB_DEBUG = true`
54
+ * before page load.
55
+ *
56
+ * Public API (window.PaBtnSplitAutoAbsorb):
57
+ * init(splitContainer) — idempotent single-container init
58
+ * initAll(scope) — initialize all uninitialized auto-absorb split
59
+ * buttons under scope
60
+ */
61
+ (function () {
62
+ 'use strict';
63
+
64
+ var INIT_FLAG = '__paBtnSplitAutoAbsorbInit';
65
+ var SELECTOR = '.pa-btn-split.pa-btn-split--auto-absorb';
66
+ var STASH_KEY = '__paAbsorbOrigClass';
67
+
68
+ function init(splitContainer) {
69
+ if (!splitContainer || splitContainer[INIT_FLAG]) return;
70
+ if (!splitContainer.classList.contains('pa-btn-split')) return;
71
+ if (!splitContainer.classList.contains('pa-btn-split--auto-absorb')) return;
72
+
73
+ var bar = splitContainer.parentNode;
74
+ if (!bar || bar.nodeType !== 1) return;
75
+
76
+ var menu = splitContainer.querySelector('.pa-btn-split__menu');
77
+ var menuInner = menu && menu.querySelector('.pa-btn-split__menu-inner');
78
+ if (!menuInner) return;
79
+
80
+ splitContainer[INIT_FLAG] = true;
81
+
82
+ // First static menu child captured at init — absorbed items are
83
+ // inserted BEFORE it so static items stay at the bottom of the menu.
84
+ // If there are no static items this stays null and insertBefore
85
+ // degrades to appendChild, which is exactly what we want.
86
+ var firstStaticItem = menuInner.firstElementChild || null;
87
+
88
+ // The split button's OWN primary action (the non-toggle `.pa-btn`) and
89
+ // its toggle. When even an empty bar can't fit the whole split button,
90
+ // the primary folds into the menu as a last resort and only the toggle
91
+ // stays visible (see absorbPrimary / the final relayout step).
92
+ var toggleBtn = splitContainer.querySelector('.pa-btn-split__toggle');
93
+ var primaryBtn = null;
94
+ for (var pc = splitContainer.firstElementChild; pc; pc = pc.nextElementSibling) {
95
+ if (pc !== toggleBtn && pc.classList &&
96
+ pc.classList.contains('pa-btn') &&
97
+ !pc.classList.contains('pa-btn-split__toggle')) {
98
+ primaryBtn = pc;
99
+ break;
100
+ }
101
+ }
102
+ var canCollapsePrimary = !!(primaryBtn && toggleBtn);
103
+
104
+ // Collect siblings that come BEFORE the split container in the bar.
105
+ // We snapshot at init time — siblings added after init won't be
106
+ // tracked (call init again, or refactor to MutationObserver later).
107
+ var candidates = [];
108
+ var idx = 0;
109
+ for (var node = bar.firstElementChild; node && node !== splitContainer; node = node.nextElementSibling) {
110
+ var p = parseInt(node.getAttribute('data-pa-absorb-priority'), 10);
111
+ candidates.push({
112
+ el: node,
113
+ priority: isNaN(p) ? 0 : p,
114
+ domIndex: idx
115
+ });
116
+ idx++;
117
+ }
118
+ if (candidates.length === 0 && !canCollapsePrimary) return;
119
+
120
+ var dropOrder = [];
121
+ function buildDropOrder() {
122
+ var fromAttr = splitContainer.getAttribute('data-pa-absorb-from');
123
+ var dropFromStart = fromAttr === 'start';
124
+ dropOrder = candidates.slice().sort(function (a, b) {
125
+ if (a.priority !== b.priority) return a.priority - b.priority;
126
+ // Default ("end") drops the sibling nearest the split
127
+ // button first — feels right because that's the one
128
+ // visually adjacent to the menu it falls into.
129
+ return dropFromStart ? (a.domIndex - b.domIndex) : (b.domIndex - a.domIndex);
130
+ });
131
+ }
132
+ buildDropOrder();
133
+
134
+ var DEBUG = window.PA_BTN_SPLIT_AUTO_ABSORB_DEBUG === true;
135
+ var label = '[pa-btn-split-auto-absorb#' + (splitContainer.id || candidates.map(function (c) { return c.el.tagName; }).join('-')).slice(0, 40) + ']';
136
+ function log() {
137
+ if (!DEBUG) return;
138
+ var args = Array.prototype.slice.call(arguments);
139
+ console.log.apply(console, [label].concat(args));
140
+ }
141
+
142
+ // Rewrite any `<span class="pa-btn__icon">` inside an absorbed item
143
+ // to the canonical `pa-btn-split__item-icon`. The pa-btn icon column
144
+ // width is scoped to `.pa-btn:has(.pa-btn__icon) .pa-btn__icon` —
145
+ // once the outer button stops being a `.pa-btn`, that rule no
146
+ // longer matches and the icon span goes auto-width, throwing
147
+ // absorbed labels out of column with static items.
148
+ function rewriteIcons(el) {
149
+ var icons = el.querySelectorAll('.pa-btn__icon');
150
+ for (var i = 0; i < icons.length; i++) {
151
+ if (icons[i][STASH_KEY] == null) {
152
+ icons[i][STASH_KEY] = icons[i].className;
153
+ }
154
+ icons[i].className = 'pa-btn-split__item-icon';
155
+ }
156
+ }
157
+
158
+ function restoreIcons(el) {
159
+ // After a previous absorb cycle the icon spans now carry our
160
+ // rewritten class. Find them and put their original class back.
161
+ var icons = el.querySelectorAll('.pa-btn-split__item-icon');
162
+ for (var i = 0; i < icons.length; i++) {
163
+ if (icons[i][STASH_KEY] != null) {
164
+ icons[i].className = icons[i][STASH_KEY];
165
+ }
166
+ }
167
+ }
168
+
169
+ function moveToMenu(el) {
170
+ if (el[STASH_KEY] == null) {
171
+ el[STASH_KEY] = el.className;
172
+ }
173
+ el.className = 'pa-btn-split__item';
174
+ rewriteIcons(el);
175
+ menuInner.insertBefore(el, firstStaticItem);
176
+ }
177
+
178
+ function moveToBar(el) {
179
+ if (el[STASH_KEY] != null) {
180
+ el.className = el[STASH_KEY];
181
+ restoreIcons(el);
182
+ }
183
+ // insertBefore moves the node if already attached, so calling
184
+ // it unconditionally keeps siblings in their original DOM
185
+ // order even after a drop-from-start cycle reshuffled them.
186
+ bar.insertBefore(el, splitContainer);
187
+ }
188
+
189
+ // Last-resort collapse: fold the split button's OWN primary action
190
+ // into the menu (as the top item) so only the toggle chevron remains
191
+ // when even an otherwise-empty bar can't fit the full split button.
192
+ var primaryAbsorbed = false;
193
+ function absorbPrimary() {
194
+ if (primaryAbsorbed || !canCollapsePrimary) return;
195
+ if (primaryBtn[STASH_KEY] == null) primaryBtn[STASH_KEY] = primaryBtn.className;
196
+ primaryBtn.className = 'pa-btn-split__item';
197
+ rewriteIcons(primaryBtn);
198
+ // Above absorbed siblings + static items — the primary action is
199
+ // the most important row, so it sits at the top of the menu.
200
+ menuInner.insertBefore(primaryBtn, menuInner.firstChild);
201
+ splitContainer.classList.add('pa-btn-split--collapsed');
202
+ primaryAbsorbed = true;
203
+ }
204
+ function restorePrimary() {
205
+ if (!primaryAbsorbed || !canCollapsePrimary) return;
206
+ if (primaryBtn[STASH_KEY] != null) {
207
+ primaryBtn.className = primaryBtn[STASH_KEY];
208
+ restoreIcons(primaryBtn);
209
+ }
210
+ // Back to its slot: immediately before the toggle.
211
+ splitContainer.insertBefore(primaryBtn, toggleBtn);
212
+ splitContainer.classList.remove('pa-btn-split--collapsed');
213
+ primaryAbsorbed = false;
214
+ }
215
+
216
+ function relayout() {
217
+ log('relayout entered');
218
+
219
+ // Step 1 — fully expand before measuring: primary action back in
220
+ // the split button, all candidates back in the bar in original
221
+ // DOM order (immediately before the split container).
222
+ restorePrimary();
223
+ candidates.slice().sort(function (a, b) {
224
+ return a.domIndex - b.domIndex;
225
+ }).forEach(function (item) {
226
+ moveToBar(item.el);
227
+ });
228
+
229
+ var clientW = bar.clientWidth;
230
+ var scrollW = bar.scrollWidth;
231
+ log('step 2', {
232
+ clientW: clientW,
233
+ scrollW: scrollW,
234
+ fitsAll: scrollW <= clientW
235
+ });
236
+
237
+ if (scrollW <= clientW) {
238
+ log('all fits');
239
+ return;
240
+ }
241
+
242
+ // Step 3 — absorb siblings in drop order until it fits.
243
+ for (var i = 0; i < dropOrder.length; i++) {
244
+ if (bar.scrollWidth <= bar.clientWidth) break;
245
+ moveToMenu(dropOrder[i].el);
246
+ log('absorbed', dropOrder[i].el);
247
+ }
248
+
249
+ // Step 4 — last resort: every sibling is absorbed and it STILL
250
+ // overflows (a very narrow card header). Fold the split button's
251
+ // own primary action into the menu so only the toggle remains;
252
+ // otherwise the primary + toggle spill past the bar's
253
+ // overflow:hidden edge and the toggle becomes unclickable.
254
+ if (bar.scrollWidth > bar.clientWidth) {
255
+ absorbPrimary();
256
+ log('absorbed primary — collapsed to toggle');
257
+ }
258
+ log('done');
259
+ }
260
+
261
+ // A stable ancestor whose width tracks the card / window, NOT our button
262
+ // shuffling: the nearest `.pa-card__header` (its width is the card width
263
+ // no matter how the shrinkable `.pa-card__actions` collapses around the
264
+ // toolbar), else the toolbar's own parent for standalone use (a sized /
265
+ // resizable wrapper). It must NOT be the shrinkable actions wrapper —
266
+ // that resizes in response to OUR own absorb/restore, which fed a
267
+ // nondeterministic feedback storm (the reload-vs-resize hysteresis).
268
+ var widthHost = (bar.closest && bar.closest('.pa-card__header')) || bar.parentNode;
269
+
270
+ // Coalesce a burst of size changes (ours + real layout) into ONE
271
+ // relayout on the next frame. relayout is deterministic — it always
272
+ // measures from a full restore — so one pass per frame converges without
273
+ // the ResizeObserver loop-limit suppression that can freeze a transient
274
+ // state.
275
+ var relayoutScheduled = false;
276
+ function scheduleRelayout() {
277
+ if (relayoutScheduled) return;
278
+ relayoutScheduled = true;
279
+ requestAnimationFrame(function () {
280
+ relayoutScheduled = false;
281
+ relayout();
282
+ });
283
+ }
284
+
285
+ // First pass after layout settles.
286
+ requestAnimationFrame(relayout);
287
+
288
+ if (typeof ResizeObserver !== 'undefined') {
289
+ var ro = new ResizeObserver(scheduleRelayout);
290
+ // The toolbar itself — catches ANY change in the space available to
291
+ // it, including the header title being given a larger `min-width`
292
+ // floor (which widens the title and narrows the toolbar WITHOUT
293
+ // changing the header's outer width). A bar-only observer isn't
294
+ // enough on its own: once the toolbar collapses to a lone toggle its
295
+ // box stops shrinking, so…
296
+ ro.observe(bar);
297
+ // …also watch the stable width host, so widening the card re-fires
298
+ // and lets relayout re-expand from the collapsed state.
299
+ if (widthHost && widthHost.nodeType === 1 && widthHost !== bar) {
300
+ ro.observe(widthHost);
301
+ }
302
+ }
303
+
304
+ // React to live `data-pa-absorb-from` flips so consumers can change
305
+ // drop direction at runtime without re-initializing.
306
+ if (typeof MutationObserver !== 'undefined') {
307
+ var mo = new MutationObserver(function () {
308
+ buildDropOrder();
309
+ relayout();
310
+ });
311
+ mo.observe(splitContainer, { attributes: true, attributeFilter: ['data-pa-absorb-from'] });
312
+ }
313
+ }
314
+
315
+ function initAll(scope) {
316
+ var nodes = (scope || document).querySelectorAll(SELECTOR);
317
+ for (var i = 0; i < nodes.length; i++) init(nodes[i]);
318
+ }
319
+
320
+ window.PaBtnSplitAutoAbsorb = { init: init, initAll: initAll };
321
+
322
+ if (document.readyState === 'loading') {
323
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
324
+ } else {
325
+ initAll();
326
+ }
327
+ })();