regular-layout 0.5.0 → 0.6.1

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.
@@ -1,4 +1,12 @@
1
- import type { Layout } from "../core/types.ts";
1
+ import type { Layout, LayoutPath } from "../core/types.ts";
2
+ /**
3
+ * The dragged panel's preview geometry during a drag transition - see
4
+ * {@link PresizeDetail.overlay}.
5
+ */
6
+ export type PresizeOverlay = {
7
+ slot: string;
8
+ path: LayoutPath;
9
+ } | null;
2
10
  /**
3
11
  * Manages cancelable pre-resize gating for layout updates.
4
12
  *
@@ -11,7 +19,8 @@ export declare class PresizeQueue {
11
19
  #private;
12
20
  private _target;
13
21
  private _eventName;
14
- constructor(_target: EventTarget, _eventName: string);
15
- run(layout: Layout, fn: () => void): Promise<void>;
22
+ private _onDispatch?;
23
+ constructor(_target: EventTarget, _eventName: string, _onDispatch?: (() => void) | undefined);
24
+ run(layout: Layout, fn: () => void, overlay?: PresizeOverlay): Promise<void>;
16
25
  resume(): void;
17
26
  }
@@ -35,11 +35,11 @@
35
35
  export declare class RegularLayoutFrame extends HTMLElement {
36
36
  private _shadowRoot;
37
37
  private _container_sheet;
38
- private _title_sheet;
38
+ private _tab_sheet;
39
39
  private _layout;
40
40
  private _header;
41
+ private _default_tab;
41
42
  private _drag;
42
- private _tab_to_index_map;
43
43
  /**
44
44
  * Initializes this elements. Override this method and
45
45
  * `disconnectedCallback` to modify how this subclass renders the Shadow
@@ -57,14 +57,16 @@ export declare class RegularLayoutFrame extends HTMLElement {
57
57
  private onPointerLost;
58
58
  private drawTabs;
59
59
  /**
60
- * Regenerates this frame's title stylesheet, mapping each tab (by its slot
61
- * `name`) to its label. The label is rendered via the title's `::before`
62
- * `content`, reading the slot's `--regular-layout-<slot>--title` override
63
- * variable with the slot name as the fallback - so a consumer can relabel a
64
- * tab purely in CSS and the change applies reactively.
60
+ * Regenerates this frame's tab stylesheet: the shared titlebar grid template
61
+ * (so every frame in the stack aligns), the column this frame's tab occupies,
62
+ * and the tab label. The label renders via the title's `::before` `content`,
63
+ * reading the slot's `--regular-layout-<slot>--title` override variable with
64
+ * the slot name as the fallback - so a consumer can relabel a tab purely in
65
+ * CSS and the change applies reactively.
65
66
  *
66
- * @param attr - The child name attribute (`CHILD_ATTRIBUTE_NAME`).
67
- * @param tabs - The slot names rendered in this frame's titlebar.
67
+ * @param slot - This frame's panel name.
68
+ * @param index - This frame's column (zero-based) within the stack.
69
+ * @param count - The number of tabs in the stack.
68
70
  */
69
- private drawTitles;
71
+ private drawTab;
70
72
  }
@@ -78,6 +78,7 @@ export declare class RegularLayout extends HTMLElement {
78
78
  private _presizeQueue;
79
79
  private _overlayController;
80
80
  private _maximized?;
81
+ private _resize_observer?;
81
82
  constructor();
82
83
  connectedCallback(): void;
83
84
  disconnectedCallback(): void;
@@ -174,16 +175,27 @@ export declare class RegularLayout extends HTMLElement {
174
175
  * {@link save}, and any subsequent {@link restore} resets the layout to the
175
176
  * minimized (normal, multi-panel) view.
176
177
  *
178
+ * Before applying, dispatches a cancelable `regular-layout-before-resize`
179
+ * event (as {@link restore} does) whose paths describe the post-maximize
180
+ * geometry - a single full-window entry for `name`. If the event is
181
+ * cancelled via `preventDefault()`, the update is suspended until
182
+ * {@link resumeResize} is called.
183
+ *
177
184
  * Has no effect if `name` is not present in the current layout.
178
185
  *
179
186
  * @param name - The name of the panel to maximize.
180
187
  */
181
- maximize: (name: string) => void;
188
+ maximize: (name: string) => Promise<void>;
182
189
  /**
183
190
  * Restores the normal multi-panel view after a {@link maximize}, without
184
191
  * altering the layout tree. No-op if no panel is currently maximized.
192
+ *
193
+ * Before applying, dispatches a cancelable `regular-layout-before-resize`
194
+ * event (as {@link restore} does) with paths for the restored multi-panel
195
+ * geometry. If the event is cancelled via `preventDefault()`, the update
196
+ * is suspended until {@link resumeResize} is called.
185
197
  */
186
- minimize: () => void;
198
+ minimize: () => Promise<void>;
187
199
  /**
188
200
  * Restores the layout from a saved state synchronously, without
189
201
  * dispatching the `regular-layout-before-resize` event.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "regular-layout",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "A regular CSS `grid` container",
5
5
  "keywords": [],
6
6
  "license": "Apache-2.0",
package/src/core/types.ts CHANGED
@@ -100,6 +100,18 @@ export interface LayoutPath {
100
100
  */
101
101
  export interface PresizeDetail {
102
102
  calculatePresizePaths(): Record<string, LayoutPath>;
103
+
104
+ /**
105
+ * When the transition is a drag preview, the dragged panel's name and the
106
+ * {@link LayoutPath} of its preview box. The dragged panel is removed from
107
+ * the layout tree during a drag (so it is absent from
108
+ * {@link PresizeDetail.calculatePresizePaths}), but `path.view_window` here
109
+ * is exactly where the overlay renders it, letting consumers presize the
110
+ * dragged panel like any other. `null` when the pointer is outside every
111
+ * drop target (the dragged panel is hidden), `undefined` for non-drag
112
+ * transitions.
113
+ */
114
+ overlay?: { slot: string; path: LayoutPath } | null;
103
115
  }
104
116
 
105
117
  /**
@@ -18,6 +18,7 @@ interface GridCell {
18
18
  colEnd: number;
19
19
  rowStart: number;
20
20
  rowEnd: number;
21
+ zIndex?: number;
21
22
  }
22
23
 
23
24
  function dedupe_sort(physics: Physics, result: number[], pos: number) {
@@ -102,15 +103,26 @@ function build_cells(
102
103
  ): GridCell[] {
103
104
  if (panel.type === "tab-layout") {
104
105
  const selected = panel.selected ?? 0;
105
- return [
106
- {
107
- child: panel.tabs[selected],
108
- colStart: find_track_index(physics, colPositions, colStart),
109
- colEnd: find_track_index(physics, colPositions, colEnd),
110
- rowStart: find_track_index(physics, rowPositions, rowStart),
111
- rowEnd: find_track_index(physics, rowPositions, rowEnd),
112
- },
113
- ];
106
+ const col_start = find_track_index(physics, colPositions, colStart);
107
+ const col_end = find_track_index(physics, colPositions, colEnd);
108
+ const row_start = find_track_index(physics, rowPositions, rowStart);
109
+ const row_end = find_track_index(physics, rowPositions, rowEnd);
110
+
111
+ // Stacked tabs all occupy the same cell, overlapping. Only the selected
112
+ // frame is lifted (`z-index:1`) so its content sits on top; the rest
113
+ // self-hide their content and just contribute their tab. Keeping the
114
+ // max frame `z-index` at 1 lets the drag overlay sit above any stack
115
+ // with a small constant (2). A single-tab stack
116
+ // emits one placement with no `z-index`, so non-stacked output is
117
+ // unchanged.
118
+ return panel.tabs.map((child, i) => ({
119
+ child,
120
+ colStart: col_start,
121
+ colEnd: col_end,
122
+ rowStart: row_start,
123
+ rowEnd: row_end,
124
+ zIndex: panel.tabs.length > 1 && i === selected ? 1 : undefined,
125
+ }));
114
126
  }
115
127
 
116
128
  const { children, sizes, orientation } = panel;
@@ -162,8 +174,11 @@ const child_template = (
162
174
  slot: string,
163
175
  rowPart: string,
164
176
  colPart: string,
177
+ zIndex?: number,
165
178
  ) =>
166
- `:host ::slotted([${physics.CHILD_ATTRIBUTE_NAME}="${slot}"]){display:flex;grid-column:${colPart};grid-row:${rowPart}}`;
179
+ `:host ::slotted([${physics.CHILD_ATTRIBUTE_NAME}="${slot}"]){display:flex;grid-column:${colPart};grid-row:${rowPart}${
180
+ zIndex === undefined ? "" : `;z-index:${zIndex}`
181
+ }}`;
167
182
 
168
183
  /**
169
184
  * Generates CSS Grid styles to render a layout tree.
@@ -202,9 +217,18 @@ export function create_css_grid_layout(
202
217
  ): string {
203
218
  if (layout.type === "tab-layout") {
204
219
  const selected = layout.selected ?? 0;
220
+ const stacked = layout.tabs.length > 1;
205
221
  return [
206
222
  host_template("100%", "100%"),
207
- child_template(physics, layout.tabs[selected], "1", "1"),
223
+ ...layout.tabs.map((tab, i) =>
224
+ child_template(
225
+ physics,
226
+ tab,
227
+ "1",
228
+ "1",
229
+ stacked && i === selected ? 1 : undefined,
230
+ ),
231
+ ),
208
232
  ].join("\n");
209
233
  }
210
234
 
@@ -253,11 +277,13 @@ export function create_css_grid_layout(
253
277
  for (const cell of cells) {
254
278
  const colPart = formatGridLine(cell.colStart, cell.colEnd);
255
279
  const rowPart = formatGridLine(cell.rowStart, cell.rowEnd);
256
- css.push(child_template(physics, cell.child, rowPart, colPart));
280
+ css.push(
281
+ child_template(physics, cell.child, rowPart, colPart, cell.zIndex),
282
+ );
257
283
  if (cell.child === overlay?.[1]) {
258
284
  css.push(child_template(physics, overlay[0], rowPart, colPart));
259
285
  css.push(
260
- `:host ::slotted([${physics.CHILD_ATTRIBUTE_NAME}=${overlay[0]}]){z-index:1}`,
286
+ `:host ::slotted([${physics.CHILD_ATTRIBUTE_NAME}=${overlay[0]}]){z-index:2}`,
261
287
  );
262
288
  }
263
289
  }
@@ -69,6 +69,6 @@ export function updateOverlaySheet(
69
69
  margin,
70
70
  );
71
71
 
72
- const css = `display:flex;position:absolute!important;z-index:1;top:${local.y}px;left:${local.x}px;height:${local.height}px;width:${local.width}px;`;
72
+ const css = `display:flex;position:absolute!important;z-index:2;top:${local.y}px;left:${local.x}px;height:${local.height}px;width:${local.width}px;`;
73
73
  return `::slotted([${physics.CHILD_ATTRIBUTE_NAME}="${slot}"]){${css}}`;
74
74
  }
@@ -70,36 +70,49 @@ export class OverlayController {
70
70
  );
71
71
  }
72
72
 
73
- await host.presizeQueue.run(panel, () => {
74
- if (mode === "grid" && drop_target) {
75
- const path: [string, string] = [slot, drop_target?.slot];
76
- const css = create_css_grid_layout(panel, path, host.physics);
77
- host.stylesheet.replaceSync(css);
78
- } else if (mode === "absolute") {
79
- const grid_css = create_css_grid_layout(panel, undefined, host.physics);
80
-
81
- while (drag_element?.tagName === "SLOT" && drag_element) {
82
- drag_element = (
83
- drag_element as HTMLSlotElement
84
- ).assignedElements()[0];
73
+ // The dragged panel is removed from `panel` above, so it is absent from
74
+ // the presize paths - `drop_target` carries its preview box instead
75
+ // (null when the pointer is outside every drop target and the dragged
76
+ // panel is hidden).
77
+ const overlay = drop_target ? { slot, path: drop_target } : null;
78
+ await host.presizeQueue.run(
79
+ panel,
80
+ () => {
81
+ if (mode === "grid" && drop_target) {
82
+ const path: [string, string] = [slot, drop_target?.slot];
83
+ const css = create_css_grid_layout(panel, path, host.physics);
84
+ host.stylesheet.replaceSync(css);
85
+ } else if (mode === "absolute") {
86
+ const grid_css = create_css_grid_layout(
87
+ panel,
88
+ undefined,
89
+ host.physics,
90
+ );
91
+
92
+ while (drag_element?.tagName === "SLOT" && drag_element) {
93
+ drag_element = (
94
+ drag_element as HTMLSlotElement
95
+ ).assignedElements()[0];
96
+ }
97
+
98
+ const margin = drag_element
99
+ ? getComputedStyle(drag_element)
100
+ : undefined;
101
+
102
+ const overlay_css = updateOverlaySheet(
103
+ slot,
104
+ box,
105
+ style,
106
+ drop_target,
107
+ host.physics,
108
+ margin,
109
+ );
110
+
111
+ host.stylesheet.replaceSync([grid_css, overlay_css].join("\n"));
85
112
  }
86
-
87
- const margin = drag_element
88
- ? getComputedStyle(drag_element)
89
- : undefined;
90
-
91
- const overlay_css = updateOverlaySheet(
92
- slot,
93
- box,
94
- style,
95
- drop_target,
96
- host.physics,
97
- margin,
98
- );
99
-
100
- host.stylesheet.replaceSync([grid_css, overlay_css].join("\n"));
101
- }
102
- });
113
+ },
114
+ overlay,
115
+ );
103
116
 
104
117
  const event_name = `${host.physics.CUSTOM_EVENT_NAME_PREFIX}-before-update`;
105
118
  const custom_event = new CustomEvent<Layout>(event_name, {
@@ -9,9 +9,15 @@
9
9
  // ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
10
10
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
11
11
 
12
- import type { Layout, PresizeDetail } from "../core/types.ts";
12
+ import type { Layout, LayoutPath, PresizeDetail } from "../core/types.ts";
13
13
  import { calculate_presize_paths } from "../layout/calculate_presize_paths.ts";
14
14
 
15
+ /**
16
+ * The dragged panel's preview geometry during a drag transition - see
17
+ * {@link PresizeDetail.overlay}.
18
+ */
19
+ export type PresizeOverlay = { slot: string; path: LayoutPath } | null;
20
+
15
21
  /**
16
22
  * Manages cancelable pre-resize gating for layout updates.
17
23
  *
@@ -22,27 +28,40 @@ import { calculate_presize_paths } from "../layout/calculate_presize_paths.ts";
22
28
  */
23
29
  export class PresizeQueue {
24
30
  #resizing = false;
25
- #queued: { layout: Layout; fn: () => void } | null = null;
31
+ #queued: {
32
+ layout: Layout;
33
+ fn: () => void;
34
+ overlay?: PresizeOverlay;
35
+ } | null = null;
26
36
  #pending: (() => void) | null = null;
27
37
 
28
38
  constructor(
29
39
  private _target: EventTarget,
30
40
  private _eventName: string,
41
+ private _onDispatch?: () => void,
31
42
  ) {}
32
43
 
33
- async run(layout: Layout, fn: () => void): Promise<void> {
44
+ async run(
45
+ layout: Layout,
46
+ fn: () => void,
47
+ overlay?: PresizeOverlay,
48
+ ): Promise<void> {
34
49
  if (this.#resizing) {
35
- this.#queued = { layout, fn };
50
+ this.#queued = { layout, fn, overlay };
36
51
  return;
37
52
  }
38
53
 
39
54
  this.#resizing = true;
40
55
  try {
41
- await this.#dispatchAndMaybeWait(layout, fn);
56
+ await this.#dispatchAndMaybeWait(layout, fn, overlay);
42
57
  while (this.#queued) {
43
- const { layout: nextLayout, fn: nextFn } = this.#queued;
58
+ const {
59
+ layout: nextLayout,
60
+ fn: nextFn,
61
+ overlay: nextOverlay,
62
+ } = this.#queued;
44
63
  this.#queued = null;
45
- await this.#dispatchAndMaybeWait(nextLayout, nextFn);
64
+ await this.#dispatchAndMaybeWait(nextLayout, nextFn, nextOverlay);
46
65
  }
47
66
  } finally {
48
67
  this.#resizing = false;
@@ -57,9 +76,15 @@ export class PresizeQueue {
57
76
  }
58
77
  }
59
78
 
60
- async #dispatchAndMaybeWait(layout: Layout, fn: () => void): Promise<void> {
79
+ async #dispatchAndMaybeWait(
80
+ layout: Layout,
81
+ fn: () => void,
82
+ overlay?: PresizeOverlay,
83
+ ): Promise<void> {
84
+ this._onDispatch?.();
61
85
  const detail: PresizeDetail = {
62
86
  calculatePresizePaths: () => calculate_presize_paths(layout),
87
+ overlay,
63
88
  };
64
89
 
65
90
  const event = new CustomEvent(this._eventName, {
@@ -15,17 +15,19 @@ import type { RegularLayout } from "./regular-layout.ts";
15
15
  import type { RegularLayoutTab } from "./regular-layout-tab.ts";
16
16
 
17
17
  const CSS = `
18
- :host{box-sizing:border-box;flex-direction:column}
19
- :host::part(titlebar){display:flex;height:24px;user-select:none;overflow:hidden}
20
- :host::part(container){flex:1 1 auto}
21
- :host::part(title){flex:1 1 auto;pointer-events:none}
22
- :host::part(close){align-self:stretch}
23
- :host::slotted{flex:1 1 auto;}
24
- :host regular-layout-tab{width:0px;}
18
+ :host{box-sizing:border-box;flex-direction:column;pointer-events:none}
19
+ [part~="titlebar"]{height:24px;user-select:none;overflow:hidden}
20
+ [part~="container"]{flex:1 1 auto;pointer-events:auto}
21
+ [part~="title"]{flex:1 1 auto;pointer-events:none}
22
+ [part~="close"]{align-self:stretch}
23
+ :host([inactive]) [part~="container"]{display:none}
24
+ .tabs{display:grid;width:100%;height:100%}
25
+ .tabs slot[name="tab"]{display:grid;grid-row:1;pointer-events:auto}
26
+ .tabs regular-layout-tab{display:flex;overflow:hidden}
25
27
  `;
26
28
 
27
29
  const HTML_TEMPLATE = `
28
- <div part="titlebar"></div>
30
+ <div part="titlebar"><div class="tabs" part="titlebar-track"><slot name="tab"><regular-layout-tab></regular-layout-tab></slot></div></div>
29
31
  <div part="container"><slot></slot></div>
30
32
  `;
31
33
 
@@ -82,11 +84,11 @@ const title_variable = (slot: string): string =>
82
84
  export class RegularLayoutFrame extends HTMLElement {
83
85
  private _shadowRoot!: ShadowRoot;
84
86
  private _container_sheet!: CSSStyleSheet;
85
- private _title_sheet!: CSSStyleSheet;
87
+ private _tab_sheet!: CSSStyleSheet;
86
88
  private _layout!: RegularLayout;
87
89
  private _header!: HTMLElement;
90
+ private _default_tab!: RegularLayoutTab;
88
91
  private _drag: DragState | null = null;
89
- private _tab_to_index_map: WeakMap<RegularLayoutTab, number> = new WeakMap();
90
92
 
91
93
  /**
92
94
  * Initializes this elements. Override this method and
@@ -96,15 +98,20 @@ export class RegularLayoutFrame extends HTMLElement {
96
98
  connectedCallback() {
97
99
  this._container_sheet ??= new CSSStyleSheet();
98
100
  this._container_sheet.replaceSync(CSS);
99
- this._title_sheet ??= new CSSStyleSheet();
101
+ this._tab_sheet ??= new CSSStyleSheet();
100
102
  this._shadowRoot ??= this.attachShadow({ mode: "open" });
101
103
  this._shadowRoot.adoptedStyleSheets = [
102
104
  this._container_sheet,
103
- this._title_sheet,
105
+ this._tab_sheet,
104
106
  ];
105
107
  this._shadowRoot.innerHTML = HTML_TEMPLATE;
106
108
  this._layout = this.parentElement as RegularLayout;
107
109
  this._header = this._shadowRoot.children[0] as HTMLElement;
110
+ // The built-in tab is the `slot="tab"` fallback; a consumer-supplied
111
+ // `slot="tab"` child replaces it.
112
+ this._default_tab = this._header.querySelector(
113
+ "regular-layout-tab",
114
+ ) as RegularLayoutTab;
108
115
  this._header.addEventListener("pointerdown", this.onPointerDown);
109
116
  this.addEventListener("pointermove", this.onPointerMove);
110
117
  this.addEventListener("pointerup", this.onPointerUp);
@@ -140,9 +147,17 @@ export class RegularLayoutFrame extends HTMLElement {
140
147
 
141
148
  const elem = event.target as RegularLayoutTab;
142
149
  if (elem.part.contains("tab")) {
150
+ const slot = this.getAttribute(
151
+ this._layout.savePhysics().CHILD_ATTRIBUTE_NAME,
152
+ );
153
+
143
154
  const path = this._layout.calculateIntersect(event);
144
- if (path) {
145
- this._drag = { path };
155
+ if (path && slot) {
156
+ // Drag *this frame's* panel, not whichever panel is front-most at
157
+ // the pointer (which is what `calculateIntersect` resolves to in a
158
+ // stack). The overlapping stack shares geometry, so only the slot
159
+ // needs overriding.
160
+ this._drag = { path: { ...path, slot } };
146
161
  this.setPointerCapture(event.pointerId);
147
162
  event.preventDefault();
148
163
  } else {
@@ -190,54 +205,41 @@ export class RegularLayoutFrame extends HTMLElement {
190
205
  return;
191
206
  }
192
207
 
193
- const new_panel = event.detail;
194
- let new_tab_panel = this._layout.getPanel(slot, new_panel);
195
- if (!new_tab_panel) {
196
- new_tab_panel = {
197
- type: "tab-layout",
198
- tabs: [slot],
199
- selected: 0,
200
- };
201
- }
202
-
203
- for (let i = 0; i < new_tab_panel.tabs.length; i++) {
204
- if (i >= this._header.children.length) {
205
- const new_tab = document.createElement("regular-layout-tab");
206
- new_tab.populate(this._layout, new_tab_panel, i);
207
- this._header.appendChild(new_tab);
208
- this._tab_to_index_map.set(new_tab, i);
209
- } else {
210
- const tab = this._header.children[i] as RegularLayoutTab;
211
- tab.populate(this._layout, new_tab_panel, i);
212
- }
213
- }
214
-
215
- const last_index = new_tab_panel.tabs.length;
216
- for (let j = this._header.children.length - 1; j >= last_index; j--) {
217
- this._header.removeChild(this._header.children[j]);
208
+ let tab_panel = this._layout.getPanel(slot, event.detail);
209
+ if (!tab_panel) {
210
+ tab_panel = { type: "tab-layout", tabs: [slot], selected: 0 };
218
211
  }
219
212
 
220
- this.drawTitles(attr, new_tab_panel.tabs);
213
+ // Each frame renders only its *own* tab. Frames in a stack overlap (see
214
+ // `create_css_grid_layout`); the tabs tile because every frame derives
215
+ // the same column template and places its tab in its own column. Only
216
+ // the selected frame shows its content; the rest keep just their tab.
217
+ const index = tab_panel.tabs.indexOf(slot);
218
+ const selected = (tab_panel.selected ?? 0) === index;
219
+ this.toggleAttribute("inactive", !selected);
220
+ this._default_tab.populate(this._layout, tab_panel, index);
221
+ this.drawTab(slot, index, tab_panel.tabs.length);
221
222
  };
222
223
 
223
224
  /**
224
- * Regenerates this frame's title stylesheet, mapping each tab (by its slot
225
- * `name`) to its label. The label is rendered via the title's `::before`
226
- * `content`, reading the slot's `--regular-layout-<slot>--title` override
227
- * variable with the slot name as the fallback - so a consumer can relabel a
228
- * tab purely in CSS and the change applies reactively.
225
+ * Regenerates this frame's tab stylesheet: the shared titlebar grid template
226
+ * (so every frame in the stack aligns), the column this frame's tab occupies,
227
+ * and the tab label. The label renders via the title's `::before` `content`,
228
+ * reading the slot's `--regular-layout-<slot>--title` override variable with
229
+ * the slot name as the fallback - so a consumer can relabel a tab purely in
230
+ * CSS and the change applies reactively.
229
231
  *
230
- * @param attr - The child name attribute (`CHILD_ATTRIBUTE_NAME`).
231
- * @param tabs - The slot names rendered in this frame's titlebar.
232
+ * @param slot - This frame's panel name.
233
+ * @param index - This frame's column (zero-based) within the stack.
234
+ * @param count - The number of tabs in the stack.
232
235
  */
233
- private drawTitles = (attr: string, tabs: string[]) => {
234
- const css = tabs
235
- .map(
236
- (slot) =>
237
- `regular-layout-tab[${attr}="${slot}"] [part~="title"]::before{content:var(${title_variable(slot)}, ${css_string(slot)})}`,
238
- )
239
- .join("\n");
240
-
241
- this._title_sheet.replaceSync(css);
236
+ private drawTab = (slot: string, index: number, count: number) => {
237
+ this._tab_sheet.replaceSync(
238
+ [
239
+ `.tabs{grid-template-columns:repeat(${count},var(--rl-tab-width,1fr))}`,
240
+ `.tabs slot[name="tab"]{grid-column:${index + 1}}`,
241
+ `[part~="title"]::before{content:var(${title_variable(slot)}, ${css_string(slot)})}`,
242
+ ].join("\n"),
243
+ );
242
244
  };
243
245
  }