partforge 0.88.0 → 0.90.0
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 +1 -1
- package/src/framework/app.css +60 -8
- package/src/framework/mount.js +37 -8
- package/src/framework/view-tabs.js +176 -12
package/package.json
CHANGED
package/src/framework/app.css
CHANGED
|
@@ -278,6 +278,7 @@ button.action:disabled { opacity: .5; cursor: default; }
|
|
|
278
278
|
/* keyboard focus ring shared across the panel's interactive controls */
|
|
279
279
|
.seg button:focus-visible, select.preset:focus-visible, .dl-row button:focus-visible,
|
|
280
280
|
button.action:focus-visible, .adv-toggle:focus-visible, .sec-title:focus-visible, #viewbar button:focus-visible,
|
|
281
|
+
.pf-view-select:focus-visible,
|
|
281
282
|
.pf-viewcube-toggle:focus-visible, .pf-float-rail-toggle:focus-visible, .pf-sketch-toolbar button:focus-visible {
|
|
282
283
|
outline: none;
|
|
283
284
|
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pf-accent) 35%, transparent);
|
|
@@ -299,14 +300,65 @@ button.action:focus-visible, .adv-toggle:focus-visible, .sec-title:focus-visible
|
|
|
299
300
|
border-radius: var(--pf-radius-pill);
|
|
300
301
|
box-shadow: var(--pf-shadow-float);
|
|
301
302
|
}
|
|
302
|
-
/*
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
303
|
+
/* flex: the panel's `.seg button` is `flex: 1 1 0%` — equal-width columns,
|
|
304
|
+
which is right for a radio group in the rail and wrong here. With a zero
|
|
305
|
+
basis every tab gets the SAME width, so the widest label does not get to
|
|
306
|
+
size its own column: import-demo's two tabs came out 106px each while
|
|
307
|
+
"Reference overlay" needs ~127, and the text simply drew outside the pill's
|
|
308
|
+
rounded background (measured in Chromium: scrollWidth 238 against
|
|
309
|
+
clientWidth 221). Sizing each tab to its own content is what the nowrap note
|
|
310
|
+
below always claimed happened, and is also what makes the pill's natural
|
|
311
|
+
width honest — view-tabs.js's collapse measures exactly this.
|
|
312
|
+
|
|
313
|
+
nowrap: with content-sized columns a long label would otherwise wrap onto a
|
|
314
|
+
second line, making this pill taller than the single fixed height
|
|
315
|
+
debug-overlay.js positions itself below (see its comment) — the overlay
|
|
316
|
+
would then overlap the now-two-row tabs. Letting the label force the column
|
|
317
|
+
wider instead keeps the pill single-row regardless of label length, and
|
|
318
|
+
hands the overflow to the collapse rather than to a second row.
|
|
319
|
+
|
|
320
|
+
view-tabs.js ALSO sets nowrap inline on every button it generates, and that is
|
|
321
|
+
the copy that actually protects the rule: a host restyling the pill from
|
|
322
|
+
scratch (partforge-cloud's `#viewer #part button`, with no `.seg` class)
|
|
323
|
+
never sees this selector, and long labels wrapped there for exactly that
|
|
324
|
+
reason. This one still covers hand-written markup on partforge's own pages. */
|
|
325
|
+
#topbar .seg button { flex: 0 0 auto; min-width: 70px; padding: 7px 10px; white-space: nowrap; }
|
|
326
|
+
|
|
327
|
+
/* The tabs' narrow-stage fallback. When view-tabs.js measures the segmented
|
|
328
|
+
pill as wider than the stage's top-centre slot it flips `data-pf-tabs` to
|
|
329
|
+
"menu", and this dropdown stands in for the buttons.
|
|
330
|
+
|
|
331
|
+
Ungated on `.seg` / `#topbar` for the same reason #viewbar's chrome below is:
|
|
332
|
+
partforge-cloud dresses the pill with its own `#viewer #part` rules and
|
|
333
|
+
neither of those selectors reaches it — and it is the host that most needs
|
|
334
|
+
this, since its stage is a column beside an editor rather than a full window.
|
|
335
|
+
|
|
336
|
+
27px tall, which is what the buttons it replaces measure (11px mono + 7px of
|
|
337
|
+
padding top and bottom). Matched rather than approximated so the pill's own
|
|
338
|
+
height does not change as it swaps — it sits at the stage's top centre, where
|
|
339
|
+
a 1px jump on every resize past the threshold would read as a twitch — and so
|
|
340
|
+
the collapsed pill stays under the fixed height debug-overlay.js parks itself
|
|
341
|
+
below. Capped in width so a long label ellipsizes rather than pushing the
|
|
342
|
+
pill back out to the width that collapsed it in the first place. */
|
|
343
|
+
.pf-view-select {
|
|
344
|
+
box-sizing: border-box;
|
|
345
|
+
height: 27px; max-width: min(220px, 100%);
|
|
346
|
+
padding: 0 24px 0 9px;
|
|
347
|
+
border: 0; border-radius: var(--pf-radius-control);
|
|
348
|
+
background: transparent; color: var(--pf-text-2);
|
|
349
|
+
font-family: var(--pf-mono); font-size: 11px; letter-spacing: 0.02em;
|
|
350
|
+
cursor: pointer; appearance: none; text-overflow: ellipsis;
|
|
351
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='7'%3E%3Cpolygon points='0,0 8,0 4,7' fill='%238b8b94'/%3E%3C/svg%3E");
|
|
352
|
+
background-repeat: no-repeat;
|
|
353
|
+
background-position: right 9px center;
|
|
354
|
+
}
|
|
355
|
+
/* `hidden` is the switch view-tabs.js flips between the two representations,
|
|
356
|
+
and a host that gives tab buttons a `display` of their own would beat the
|
|
357
|
+
UA's [hidden] rule — the trap `#viewbar button[hidden]` below already guards
|
|
358
|
+
against. Said again here against the layout state, which lives on the
|
|
359
|
+
element itself and so holds whatever a host does to the pill around it. */
|
|
360
|
+
[data-pf-tabs="menu"] > button[data-part] { display: none; }
|
|
361
|
+
[data-pf-tabs="segmented"] > .pf-view-select { display: none; }
|
|
310
362
|
|
|
311
363
|
/* viewer controls. APPEARANCE is ungated: partforge-cloud re-anchors #viewbar's
|
|
312
364
|
position in sandbox.css but inherits this pill chrome, so gating it on a class
|
package/src/framework/mount.js
CHANGED
|
@@ -444,15 +444,44 @@ export function mount(part, { createWorker, elements = {}, onBuild, onPick, onDo
|
|
|
444
444
|
stage: els.viewer, tooltip, send: annotateSend,
|
|
445
445
|
});
|
|
446
446
|
cleanup.defer(() => sketchToolbar.detach());
|
|
447
|
-
|
|
448
|
-
|
|
447
|
+
// Chrome the sketch toolbar stands in for while the mode is on. #viewbar
|
|
448
|
+
// because the toolbar carries its own tools; the view tabs because the
|
|
449
|
+
// toolbar floats in THEIR slot — both are the stage's top centre — and the
|
|
450
|
+
// two are not the same width, so a part with several views left its tabs
|
|
451
|
+
// peeking out either side (78px each side for a five-view part on a
|
|
452
|
+
// 1400px stage). The toolbar paints over them at z 20, so this was only
|
|
453
|
+
// ever about what shows.
|
|
454
|
+
//
|
|
455
|
+
// Each element remembers whatever hidden state the host had set and gets
|
|
456
|
+
// it back on exit, rather than being forced visible: a host with its own
|
|
457
|
+
// reason to hide either one must see that reason survive a round-trip.
|
|
458
|
+
//
|
|
459
|
+
// `hidden` is set for the semantics AND `display` inline for the effect.
|
|
460
|
+
// The property alone is not enough: app.css gives both the viewbar and the
|
|
461
|
+
// tab pill an author-origin `display`, which beats the UA's [hidden] rule
|
|
462
|
+
// — #viewbar[hidden] already carries an explicit rule for exactly that.
|
|
463
|
+
// A rule cannot fix the tabs the same way, because a host that restyles
|
|
464
|
+
// the pill under its own id (partforge-cloud's `#viewer #part`) outranks
|
|
465
|
+
// anything this framework could ship, so the hide rides on the element
|
|
466
|
+
// itself for the same reason view-tabs.js's nowrap does.
|
|
467
|
+
const sketchHides = [els.viewer.querySelector("#viewbar"), els.tabs]
|
|
468
|
+
.filter(Boolean)
|
|
469
|
+
.map((el) => ({ el, wasHidden: false, wasDisplay: "" }));
|
|
449
470
|
cleanup.defer(annotateMode.onModeChange(() => {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
471
|
+
// setEnabled early-returns when the mode is unchanged, so this only ever
|
|
472
|
+
// runs on a real transition and the saved state cannot be overwritten
|
|
473
|
+
// with the state we just imposed.
|
|
474
|
+
const on = annotateMode.isEnabled();
|
|
475
|
+
for (const entry of sketchHides) {
|
|
476
|
+
if (on) {
|
|
477
|
+
entry.wasHidden = entry.el.hidden;
|
|
478
|
+
entry.wasDisplay = entry.el.style.display;
|
|
479
|
+
entry.el.hidden = true;
|
|
480
|
+
entry.el.style.display = "none";
|
|
481
|
+
} else {
|
|
482
|
+
entry.el.hidden = entry.wasHidden;
|
|
483
|
+
entry.el.style.display = entry.wasDisplay;
|
|
484
|
+
}
|
|
456
485
|
}
|
|
457
486
|
}));
|
|
458
487
|
}
|
|
@@ -7,10 +7,89 @@ import { loadView, saveView } from "./view-state.js";
|
|
|
7
7
|
// hand-wrote. Which tab opens is resolveDefaultView's call, not key order. The
|
|
8
8
|
// active tab then persists per part for the rest of the browser session, so a
|
|
9
9
|
// Vite dev reload doesn't throw you back mid-edit.
|
|
10
|
-
|
|
10
|
+
//
|
|
11
|
+
// The bar has TWO representations of the same choice, and both exist from the
|
|
12
|
+
// start: the segmented buttons, and a dropdown that takes over when the pill no
|
|
13
|
+
// longer fits the stage's top-centre slot. Which one shows is `data-pf-tabs` on
|
|
14
|
+
// the element, kept current by the measurement below. They are built together
|
|
15
|
+
// and switched together rather than rebuilt on each swap, so a click and a
|
|
16
|
+
// dropdown change are the same commit path and neither can drift from the other.
|
|
17
|
+
|
|
18
|
+
// The slot must clear the pill by this much before the buttons come back. Purely
|
|
19
|
+
// an anti-flap margin: a pill whose natural width lands within a subpixel of the
|
|
20
|
+
// slot would otherwise swap layouts on every ResizeObserver notification.
|
|
21
|
+
const EXPAND_MARGIN = 2;
|
|
22
|
+
// chrome.css's `.pf-float-tabs { top: 12px; left: 50% }` inset — the margin the
|
|
23
|
+
// bar keeps from the stage edge when nothing else competes for the corner.
|
|
24
|
+
const STAGE_MARGIN = 12;
|
|
25
|
+
// Breathing room between the pill's edge and the rail toggle it must not reach.
|
|
26
|
+
const TOGGLE_GAP = 8;
|
|
27
|
+
|
|
28
|
+
// Which representation to show. Pure, and deliberately a function of the pill's
|
|
29
|
+
// NATURAL width rather than its current one — see `natural` below for why that
|
|
30
|
+
// distinction is what stops the swap oscillating.
|
|
31
|
+
//
|
|
32
|
+
// A non-positive measurement is the ABSENCE of a reading, not a claim that
|
|
33
|
+
// nothing fits: happy-dom reports zeros for every box, and so does a real
|
|
34
|
+
// browser before first layout. Segmented is the honest fallback — it is what
|
|
35
|
+
// the bar looked like before this measurement existed.
|
|
36
|
+
export function pickLayout({ natural, available, collapsed }) {
|
|
37
|
+
if (!(natural > 0) || !(available > 0)) return "segmented";
|
|
38
|
+
if (collapsed) return available >= natural + EXPAND_MARGIN ? "segmented" : "menu";
|
|
39
|
+
return natural > available ? "menu" : "segmented";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// How much of each stage edge is spoken for. Doubled by the caller because the
|
|
43
|
+
// pill is centre-anchored: the tighter side governs both halves. Derived from
|
|
44
|
+
// the rail toggle's measured box rather than from a second copy of its offsets,
|
|
45
|
+
// the same way animation-controls.js derives the view cube's claim from the
|
|
46
|
+
// viewbar's — retune chrome.css and this follows. The toggle is `[hidden]`
|
|
47
|
+
// below the narrow breakpoint, where the stage margin stands alone.
|
|
48
|
+
function sideClearance(stage, stageRect) {
|
|
49
|
+
const toggle = stage?.querySelector?.(".pf-float-rail-toggle");
|
|
50
|
+
if (!toggle || toggle.hidden || !stageRect) return STAGE_MARGIN;
|
|
51
|
+
const rect = toggle.getBoundingClientRect?.();
|
|
52
|
+
if (!(rect?.width > 0)) return STAGE_MARGIN;
|
|
53
|
+
return Math.max(STAGE_MARGIN, stageRect.right - rect.left + TOGGLE_GAP);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// `segWidth` is what the pill measures RIGHT NOW, which is near nothing once the
|
|
57
|
+
// buttons are hidden. Reporting it raw rather than smoothing it here keeps this
|
|
58
|
+
// a plain reading; remembering the last meaningful one is the orchestrator's job.
|
|
59
|
+
function domMeasure(el) {
|
|
60
|
+
return () => {
|
|
61
|
+
const stage = el.closest?.(".pf-stage") ?? el.offsetParent ?? el.ownerDocument?.documentElement;
|
|
62
|
+
const stageRect = stage?.getBoundingClientRect?.() ?? null;
|
|
63
|
+
const stageWidth = stageRect?.width || stage?.clientWidth || 0;
|
|
64
|
+
return {
|
|
65
|
+
segWidth: el.scrollWidth || 0,
|
|
66
|
+
available: stageWidth ? stageWidth - 2 * sideClearance(stage, stageRect) : 0,
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Built from the buttons rather than from part.views so a page with hand-written
|
|
72
|
+
// markup gets a working dropdown too — the buttons are the one description of
|
|
73
|
+
// the choice that both paths share. Option text goes through textContent for the
|
|
74
|
+
// same reason the buttons do: view labels are untrusted data.
|
|
75
|
+
function buildSelect(doc, tabs) {
|
|
76
|
+
const select = doc.createElement("select");
|
|
77
|
+
select.className = "pf-view-select";
|
|
78
|
+
select.setAttribute("aria-label", "View");
|
|
79
|
+
for (const btn of tabs) {
|
|
80
|
+
const option = doc.createElement("option");
|
|
81
|
+
option.value = btn.dataset.part;
|
|
82
|
+
option.textContent = btn.textContent;
|
|
83
|
+
select.append(option);
|
|
84
|
+
}
|
|
85
|
+
return select;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function createViewTabs(el, part, { onChange, measure } = {}) {
|
|
11
89
|
const generated = !!(el && part.views);
|
|
12
90
|
const partKey = part?.meta?.title ?? "";
|
|
13
91
|
const resolved = resolveDefaultView(part);
|
|
92
|
+
const doc = el.ownerDocument ?? document;
|
|
14
93
|
if (generated) {
|
|
15
94
|
// Built node-by-node with textContent/dataset rather than an innerHTML
|
|
16
95
|
// template — view keys and labels come from the part, which is untrusted
|
|
@@ -20,32 +99,113 @@ export function createViewTabs(el, part, { onChange }) {
|
|
|
20
99
|
const btn = document.createElement("button");
|
|
21
100
|
btn.dataset.part = key;
|
|
22
101
|
btn.textContent = v?.label ?? key;
|
|
102
|
+
// Inline, not a stylesheet rule, because this one is load-bearing rather
|
|
103
|
+
// than decorative and a host can drop a stylesheet rule by accident.
|
|
104
|
+
// partforge-cloud restyles the pill from scratch (`#viewer #part button`,
|
|
105
|
+
// no `.seg` class), which is exactly how app.css's `white-space: nowrap`
|
|
106
|
+
// stopped applying there and let long labels wrap onto a second line. A
|
|
107
|
+
// wrapping label makes the pill NARROWER, not wider, so the collapse
|
|
108
|
+
// below would never trip — the overflow it is meant to catch would show
|
|
109
|
+
// up as a two-row pill instead. On the element, it is out of reach.
|
|
110
|
+
btn.style.whiteSpace = "nowrap";
|
|
23
111
|
if (key === resolved) btn.classList.add("on");
|
|
24
112
|
return btn;
|
|
25
113
|
}));
|
|
26
114
|
}
|
|
27
115
|
|
|
28
|
-
const
|
|
116
|
+
const buttons = () => [...el.querySelectorAll("button[data-part]")];
|
|
117
|
+
const tabs = buttons();
|
|
118
|
+
// Below two tabs there is nothing to choose, and an empty pill must stay
|
|
119
|
+
// empty: partforge-cloud hides the bar with `#viewer #part:empty`, so a part
|
|
120
|
+
// with no views would otherwise gain a one-option dropdown and a visible pill.
|
|
121
|
+
const select = tabs.length >= 2 ? buildSelect(doc, tabs) : null;
|
|
122
|
+
if (select) el.append(select);
|
|
123
|
+
|
|
124
|
+
const setActive = (btn) => {
|
|
125
|
+
for (const b of buttons()) b.classList.toggle("on", b === btn);
|
|
126
|
+
if (select) select.value = btn.dataset.part;
|
|
127
|
+
};
|
|
29
128
|
|
|
30
129
|
// Initial view: the session-saved one if it still matches a tab, else the active
|
|
31
130
|
// button — the resolved default for a generated bar, or whatever the page's own
|
|
32
131
|
// markup marked `on` for a hand-written one.
|
|
33
132
|
const defaultView = el.querySelector("button.on")?.dataset.part ?? el.querySelector("button")?.dataset.part;
|
|
34
133
|
const saved = loadView(partKey);
|
|
35
|
-
const savedBtn = saved ?
|
|
134
|
+
const savedBtn = saved ? tabs.find((b) => b.dataset.part === saved) : null;
|
|
36
135
|
let view = savedBtn ? saved : defaultView;
|
|
37
|
-
|
|
136
|
+
// Unconditional now (it used to run only for a restored view): the dropdown
|
|
137
|
+
// has to open showing the same tab the buttons do, and for a generated bar
|
|
138
|
+
// the button half of this is the no-op it always was.
|
|
139
|
+
const activeBtn = savedBtn ?? tabs.find((b) => b.dataset.part === view);
|
|
140
|
+
if (activeBtn) setActive(activeBtn);
|
|
38
141
|
|
|
39
|
-
const
|
|
40
|
-
const btn = e.target.closest("button[data-part]");
|
|
41
|
-
if (!btn) return;
|
|
142
|
+
const commit = (btn) => {
|
|
42
143
|
view = btn.dataset.part;
|
|
43
144
|
saveView(partKey, view);
|
|
44
145
|
setActive(btn);
|
|
45
146
|
onChange(view);
|
|
46
147
|
};
|
|
148
|
+
|
|
149
|
+
const onClick = (e) => {
|
|
150
|
+
const btn = e.target.closest("button[data-part]");
|
|
151
|
+
if (!btn) return;
|
|
152
|
+
commit(btn);
|
|
153
|
+
};
|
|
47
154
|
el.addEventListener("click", onClick);
|
|
48
155
|
|
|
156
|
+
const onSelectChange = () => {
|
|
157
|
+
const btn = buttons().find((b) => b.dataset.part === select.value);
|
|
158
|
+
if (btn) commit(btn);
|
|
159
|
+
};
|
|
160
|
+
select?.addEventListener("change", onSelectChange);
|
|
161
|
+
|
|
162
|
+
// ---- layout: which representation is on screen --------------------------
|
|
163
|
+
let detached = false;
|
|
164
|
+
let mode = null;
|
|
165
|
+
// The pill's width AS IF THE BUTTONS WERE SHOWING — the fixed point the swap
|
|
166
|
+
// turns on, and the same trick animation-controls.js's nominalClusterRect
|
|
167
|
+
// plays for the view cube. Re-reading the live pill while collapsed would
|
|
168
|
+
// measure the dropdown instead, decide the buttons fit, expand, overflow, and
|
|
169
|
+
// collapse again: two frames per cycle, on screen as a flickering bar. This
|
|
170
|
+
// value cannot change while collapsed, so the collapsed state is stable.
|
|
171
|
+
// Caching is exact rather than approximate here because the buttons are
|
|
172
|
+
// content-sized (`min-width` + padding + nowrap, no percentages), so their
|
|
173
|
+
// natural width does not depend on the viewport at all.
|
|
174
|
+
let natural = 0;
|
|
175
|
+
const read = measure ?? domMeasure(el);
|
|
176
|
+
|
|
177
|
+
const setMode = (next) => {
|
|
178
|
+
if (next === mode) return;
|
|
179
|
+
mode = next;
|
|
180
|
+
el.dataset.pfTabs = next;
|
|
181
|
+
const menu = next === "menu";
|
|
182
|
+
for (const b of buttons()) b.hidden = menu;
|
|
183
|
+
if (select) select.hidden = !menu;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const relayout = () => {
|
|
187
|
+
if (detached) return;
|
|
188
|
+
if (!select) { setMode("segmented"); return; }
|
|
189
|
+
const { segWidth, available } = read();
|
|
190
|
+
if (mode !== "menu" && segWidth > 0) natural = segWidth;
|
|
191
|
+
setMode(pickLayout({ natural, available, collapsed: mode === "menu" }));
|
|
192
|
+
};
|
|
193
|
+
setMode("segmented"); // measure from the state the cache is only valid in
|
|
194
|
+
relayout();
|
|
195
|
+
|
|
196
|
+
// ResizeObserver is the precise trigger; the resize listener is the belt that
|
|
197
|
+
// also fires where it is absent, mirroring animation-controls.js's placement
|
|
198
|
+
// pair. Both land on the same idempotent relayout, so a doubled notification
|
|
199
|
+
// costs one measurement and no DOM write.
|
|
200
|
+
const win = doc.defaultView ?? globalThis;
|
|
201
|
+
win.addEventListener?.("resize", relayout);
|
|
202
|
+
const stage = el.closest?.(".pf-stage") ?? el.parentElement;
|
|
203
|
+
const observer = typeof win.ResizeObserver === "function" ? new win.ResizeObserver(relayout) : null;
|
|
204
|
+
if (observer && stage) observer.observe(stage);
|
|
205
|
+
// A webfont swap changes every label's width, and so the pill's natural width,
|
|
206
|
+
// after the first layout the measurement above ran against.
|
|
207
|
+
doc.fonts?.ready?.then(relayout).catch(() => {});
|
|
208
|
+
|
|
49
209
|
return {
|
|
50
210
|
current: () => view,
|
|
51
211
|
// Programmatic switch — the click path without the click. Used by an
|
|
@@ -53,17 +213,21 @@ export function createViewTabs(el, part, { onChange }) {
|
|
|
53
213
|
// Returns false for a name that isn't a tab so callers can validate.
|
|
54
214
|
select: (name) => {
|
|
55
215
|
if (name === view) return true; // already active — nothing to do
|
|
56
|
-
const btn =
|
|
216
|
+
const btn = buttons().find((b) => b.dataset.part === name);
|
|
57
217
|
if (!btn) return false;
|
|
58
|
-
|
|
59
|
-
saveView(partKey, view);
|
|
60
|
-
setActive(btn);
|
|
61
|
-
onChange(view);
|
|
218
|
+
commit(btn);
|
|
62
219
|
return true;
|
|
63
220
|
},
|
|
64
221
|
detach: () => {
|
|
222
|
+
detached = true;
|
|
65
223
|
el.removeEventListener("click", onClick);
|
|
224
|
+
select?.removeEventListener("change", onSelectChange);
|
|
225
|
+
win.removeEventListener?.("resize", relayout);
|
|
226
|
+
observer?.disconnect();
|
|
227
|
+
select?.remove(); // ours in both paths — hand-written markup never has one
|
|
228
|
+
delete el.dataset.pfTabs;
|
|
66
229
|
if (generated) el.innerHTML = ""; // we generated these buttons; hand-written markup stays
|
|
230
|
+
else for (const b of buttons()) b.hidden = false; // leave the page's own buttons as we found them
|
|
67
231
|
},
|
|
68
232
|
};
|
|
69
233
|
}
|