regular-layout 0.4.0 → 0.5.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.
@@ -31,3 +31,12 @@ import type { Layout } from "../core/types.ts";
31
31
  * ```
32
32
  */
33
33
  export declare function create_css_grid_layout(layout: Layout, overlay?: [string, string], physics?: Physics): string;
34
+ /**
35
+ * Generates CSS Grid styles that render a single panel maximized to fill the
36
+ * entire layout, hiding all other slotted children.
37
+ *
38
+ * @param name - The name of the panel to maximize.
39
+ * @param physics - Instance constants (defaults to {@link DEFAULT_PHYSICS}).
40
+ * @returns CSS string showing only `name`, full-size.
41
+ */
42
+ export declare function create_css_maximize_layout(name: string, physics?: Physics): string;
@@ -14,8 +14,17 @@
14
14
  * [named `slot`s](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_templates_and_slots)
15
15
  * for wholesale replacement of the underlying Shadow DOM.
16
16
  *
17
+ * The `name` attribute identifies the panel within the layout. The tab label
18
+ * defaults to `name`, but can be overridden with pure CSS by setting the
19
+ * `--regular-layout-<name>--title` custom property to a CSS string - the label
20
+ * is rendered via the tab title's `::before` `content`, so the override
21
+ * applies reactively with no JavaScript.
22
+ *
17
23
  * @example
18
24
  * ```html
25
+ * <style>
26
+ * regular-layout { --regular-layout-panel-1--title: "Panel 1"; }
27
+ * </style>
19
28
  * <regular-layout>
20
29
  * <regular-layout-frame name="panel-1">
21
30
  * <!-- Panel content here -->
@@ -26,6 +35,7 @@
26
35
  export declare class RegularLayoutFrame extends HTMLElement {
27
36
  private _shadowRoot;
28
37
  private _container_sheet;
38
+ private _title_sheet;
29
39
  private _layout;
30
40
  private _header;
31
41
  private _drag;
@@ -46,4 +56,15 @@ export declare class RegularLayoutFrame extends HTMLElement {
46
56
  private onPointerCancel;
47
57
  private onPointerLost;
48
58
  private drawTabs;
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.
65
+ *
66
+ * @param attr - The child name attribute (`CHILD_ATTRIBUTE_NAME`).
67
+ * @param tabs - The slot names rendered in this frame's titlebar.
68
+ */
69
+ private drawTitles;
49
70
  }
@@ -57,6 +57,14 @@ export interface PointerEventCoordinates {
57
57
  * latter implementation would require synchronizing the light DOM
58
58
  * and shadow DOM slots/slotted children continuously.
59
59
  *
60
+ * Note that `::slotted()` only matches *directly* slotted nodes - it
61
+ * does not pierce a slotted `<slot>`. A consumer that wants to forward
62
+ * its own light-DOM content through `<regular-layout>` therefore cannot
63
+ * place a bare `<slot name="X">` as a layout child and expect it to be
64
+ * grid-positioned; instead, wrap the forwarding slot in a concrete
65
+ * `<regular-layout-frame name="X">` (which *is* directly slotted and so
66
+ * matched by the generated `::slotted([name="X"])` rules).
67
+ *
60
68
  */
61
69
  export declare class RegularLayout extends HTMLElement {
62
70
  private _shadowRoot;
@@ -69,6 +77,7 @@ export declare class RegularLayout extends HTMLElement {
69
77
  private _physics;
70
78
  private _presizeQueue;
71
79
  private _overlayController;
80
+ private _maximized?;
72
81
  constructor();
73
82
  connectedCallback(): void;
74
83
  disconnectedCallback(): void;
@@ -133,6 +142,20 @@ export declare class RegularLayout extends HTMLElement {
133
142
  * @param name - Name of the panel to remove
134
143
  */
135
144
  removePanel: (name: string) => Promise<void>;
145
+ /**
146
+ * Selects a panel by `name`, making it the front-most tab within its
147
+ * containing stack, then dispatches a `<prefix>-select` event with
148
+ * `detail: { name }`.
149
+ *
150
+ * The event fires whenever a tab is selected - including re-selecting the
151
+ * already-active tab, or selecting the sole tab of a single-element stack -
152
+ * so consumers can always map a tab interaction to an "active panel". When
153
+ * the selection actually changes, a `<prefix>-update` event is dispatched
154
+ * first (via {@link restore}).
155
+ *
156
+ * @param name - The name of the panel to select.
157
+ */
158
+ select: (name: string) => Promise<void>;
136
159
  /**
137
160
  * Retrieves a panel by name from the layout tree.
138
161
  *
@@ -145,6 +168,22 @@ export declare class RegularLayout extends HTMLElement {
145
168
  * Clears the entire layout, unslotting all panels.
146
169
  */
147
170
  clear: () => Promise<void>;
171
+ /**
172
+ * Maximizes a panel by `name`, rendering it full-size and hiding every
173
+ * other panel. This is transient display state - it is **not** persisted by
174
+ * {@link save}, and any subsequent {@link restore} resets the layout to the
175
+ * minimized (normal, multi-panel) view.
176
+ *
177
+ * Has no effect if `name` is not present in the current layout.
178
+ *
179
+ * @param name - The name of the panel to maximize.
180
+ */
181
+ maximize: (name: string) => void;
182
+ /**
183
+ * Restores the normal multi-panel view after a {@link maximize}, without
184
+ * altering the layout tree. No-op if no panel is currently maximized.
185
+ */
186
+ minimize: () => void;
148
187
  /**
149
188
  * Restores the layout from a saved state synchronously, without
150
189
  * 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.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "A regular CSS `grid` container",
5
5
  "keywords": [],
6
6
  "license": "Apache-2.0",
package/src/extensions.ts CHANGED
@@ -65,6 +65,12 @@ declare global {
65
65
  options?: { signal: AbortSignal },
66
66
  ): void;
67
67
 
68
+ addEventListener(
69
+ name: "regular-layout-select",
70
+ cb: (e: RegularLayoutSelectEvent) => void,
71
+ options?: { signal: AbortSignal },
72
+ ): void;
73
+
68
74
  removeEventListener(
69
75
  name: "regular-layout-update",
70
76
  cb: (e: RegularLayoutEvent) => void,
@@ -79,8 +85,14 @@ declare global {
79
85
  name: "regular-layout-before-resize",
80
86
  cb: (e: RegularLayoutPresizeEvent) => void,
81
87
  ): void;
88
+
89
+ removeEventListener(
90
+ name: "regular-layout-select",
91
+ cb: (e: RegularLayoutSelectEvent) => void,
92
+ ): void;
82
93
  }
83
94
  }
84
95
 
85
96
  export type RegularLayoutEvent = CustomEvent<Layout>;
86
97
  export type RegularLayoutPresizeEvent = CustomEvent<PresizeDetail>;
98
+ export type RegularLayoutSelectEvent = CustomEvent<{ name: string }>;
@@ -264,3 +264,21 @@ export function create_css_grid_layout(
264
264
 
265
265
  return css.join("\n");
266
266
  }
267
+
268
+ /**
269
+ * Generates CSS Grid styles that render a single panel maximized to fill the
270
+ * entire layout, hiding all other slotted children.
271
+ *
272
+ * @param name - The name of the panel to maximize.
273
+ * @param physics - Instance constants (defaults to {@link DEFAULT_PHYSICS}).
274
+ * @returns CSS string showing only `name`, full-size.
275
+ */
276
+ export function create_css_maximize_layout(
277
+ name: string,
278
+ physics: Physics = DEFAULT_PHYSICS,
279
+ ): string {
280
+ return [
281
+ host_template("100%", "100%"),
282
+ child_template(physics, name, "1", "1"),
283
+ ].join("\n");
284
+ }
@@ -58,7 +58,6 @@ export class OverlayController {
58
58
 
59
59
  const [col, row, box, style] = host.relativeCoordinates(event, true);
60
60
  let drop_target = calculate_intersection(col, row, panel);
61
- console.log(row, col, drop_target);
62
61
  if (drop_target) {
63
62
  drop_target = calculate_edge(
64
63
  col,
@@ -31,6 +31,20 @@ const HTML_TEMPLATE = `
31
31
 
32
32
  type DragState = { moved?: boolean; path: LayoutPath };
33
33
 
34
+ /**
35
+ * Escapes a string for safe use as a CSS `<string>` token (the `content`
36
+ * fallback), handling backslashes, double-quotes, and newlines.
37
+ */
38
+ const css_string = (value: string): string =>
39
+ `"${value.replace(/[\\"\n]/g, (c) => (c === "\n" ? "\\A " : `\\${c}`))}"`;
40
+
41
+ /**
42
+ * The per-slot CSS custom property a consumer sets to override a tab's label,
43
+ * e.g. `regular-layout { --regular-layout-my-panel--title: "My Panel"; }`.
44
+ */
45
+ const title_variable = (slot: string): string =>
46
+ `--regular-layout-${globalThis.CSS.escape(slot)}--title`;
47
+
34
48
  /**
35
49
  * A custom element that represents a draggable panel within a
36
50
  * `<regular-layout>`.
@@ -47,8 +61,17 @@ type DragState = { moved?: boolean; path: LayoutPath };
47
61
  * [named `slot`s](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_templates_and_slots)
48
62
  * for wholesale replacement of the underlying Shadow DOM.
49
63
  *
64
+ * The `name` attribute identifies the panel within the layout. The tab label
65
+ * defaults to `name`, but can be overridden with pure CSS by setting the
66
+ * `--regular-layout-<name>--title` custom property to a CSS string - the label
67
+ * is rendered via the tab title's `::before` `content`, so the override
68
+ * applies reactively with no JavaScript.
69
+ *
50
70
  * @example
51
71
  * ```html
72
+ * <style>
73
+ * regular-layout { --regular-layout-panel-1--title: "Panel 1"; }
74
+ * </style>
52
75
  * <regular-layout>
53
76
  * <regular-layout-frame name="panel-1">
54
77
  * <!-- Panel content here -->
@@ -59,6 +82,7 @@ type DragState = { moved?: boolean; path: LayoutPath };
59
82
  export class RegularLayoutFrame extends HTMLElement {
60
83
  private _shadowRoot!: ShadowRoot;
61
84
  private _container_sheet!: CSSStyleSheet;
85
+ private _title_sheet!: CSSStyleSheet;
62
86
  private _layout!: RegularLayout;
63
87
  private _header!: HTMLElement;
64
88
  private _drag: DragState | null = null;
@@ -72,8 +96,12 @@ export class RegularLayoutFrame extends HTMLElement {
72
96
  connectedCallback() {
73
97
  this._container_sheet ??= new CSSStyleSheet();
74
98
  this._container_sheet.replaceSync(CSS);
99
+ this._title_sheet ??= new CSSStyleSheet();
75
100
  this._shadowRoot ??= this.attachShadow({ mode: "open" });
76
- this._shadowRoot.adoptedStyleSheets = [this._container_sheet];
101
+ this._shadowRoot.adoptedStyleSheets = [
102
+ this._container_sheet,
103
+ this._title_sheet,
104
+ ];
77
105
  this._shadowRoot.innerHTML = HTML_TEMPLATE;
78
106
  this._layout = this.parentElement as RegularLayout;
79
107
  this._header = this._shadowRoot.children[0] as HTMLElement;
@@ -106,6 +134,10 @@ export class RegularLayoutFrame extends HTMLElement {
106
134
  }
107
135
 
108
136
  private onPointerDown = (event: PointerEvent): void => {
137
+ if (event.button !== 0) {
138
+ return;
139
+ }
140
+
109
141
  const elem = event.target as RegularLayoutTab;
110
142
  if (elem.part.contains("tab")) {
111
143
  const path = this._layout.calculateIntersect(event);
@@ -184,5 +216,28 @@ export class RegularLayoutFrame extends HTMLElement {
184
216
  for (let j = this._header.children.length - 1; j >= last_index; j--) {
185
217
  this._header.removeChild(this._header.children[j]);
186
218
  }
219
+
220
+ this.drawTitles(attr, new_tab_panel.tabs);
221
+ };
222
+
223
+ /**
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.
229
+ *
230
+ * @param attr - The child name attribute (`CHILD_ATTRIBUTE_NAME`).
231
+ * @param tabs - The slot names rendered in this frame's titlebar.
232
+ */
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);
187
242
  };
188
243
  }
@@ -34,6 +34,14 @@ export class RegularLayoutTab extends HTMLElement {
34
34
  * @param index - The index of this tab within the tab panel.
35
35
  */
36
36
  populate = (layout: RegularLayout, tab_panel: TabLayout, index: number) => {
37
+ // Stamp the slot onto the tab so its title can be styled by name (see
38
+ // `RegularLayoutFrame#drawTitles`). Tag-qualified selectors keep this
39
+ // distinct from the panel `name` on `<regular-layout-frame>`.
40
+ this.setAttribute(
41
+ layout.savePhysics().CHILD_ATTRIBUTE_NAME,
42
+ tab_panel.tabs[index],
43
+ );
44
+
37
45
  if (this._tab_panel) {
38
46
  const tab_changed =
39
47
  (index === tab_panel.selected) !==
@@ -44,9 +52,6 @@ export class RegularLayoutTab extends HTMLElement {
44
52
 
45
53
  if (index_changed) {
46
54
  const selected = tab_panel.selected === index;
47
- const slot = tab_panel.tabs[index];
48
- this.children[0].textContent = slot;
49
-
50
55
  if (selected) {
51
56
  this.children[1].part.add("active-close");
52
57
  this.part.add("active-tab");
@@ -56,7 +61,6 @@ export class RegularLayoutTab extends HTMLElement {
56
61
  }
57
62
  }
58
63
  } else {
59
- const slot = tab_panel.tabs[index];
60
64
  const selected = tab_panel.selected === index;
61
65
  const parts = selected ? "active-close close" : "close";
62
66
  this.innerHTML = `<div part="title"></div><button part="${parts}"></button>`;
@@ -67,7 +71,6 @@ export class RegularLayoutTab extends HTMLElement {
67
71
  }
68
72
 
69
73
  this.addEventListener("pointerdown", this.onTabClick);
70
- this.children[0].textContent = slot;
71
74
  this.children[1].addEventListener("pointerdown", this.onTabClose);
72
75
  }
73
76
 
@@ -76,28 +79,23 @@ export class RegularLayoutTab extends HTMLElement {
76
79
  this._index = index;
77
80
  };
78
81
 
79
- private onTabClose = (_: Event) => {
82
+ private onTabClose = (event: Event) => {
83
+ if (event instanceof PointerEvent && event?.button !== 0) {
84
+ return;
85
+ }
86
+
80
87
  if (this._tab_panel !== undefined && this._index !== undefined) {
81
88
  this._layout?.removePanel(this._tab_panel.tabs[this._index]);
82
89
  }
83
90
  };
84
91
 
85
- private onTabClick = (_: PointerEvent) => {
86
- if (
87
- this._tab_panel !== undefined &&
88
- this._index !== undefined &&
89
- this._index !== this._tab_panel.selected
90
- ) {
91
- const new_layout = this._layout?.save();
92
- const new_tab_panel = this._layout?.getPanel(
93
- this._tab_panel.tabs[this._index],
94
- new_layout,
95
- );
92
+ private onTabClick = (event: PointerEvent) => {
93
+ if (event.button !== 0) {
94
+ return;
95
+ }
96
96
 
97
- if (new_tab_panel && new_layout) {
98
- new_tab_panel.selected = this._index;
99
- this._layout?.restore(new_layout);
100
- }
97
+ if (this._tab_panel !== undefined && this._index !== undefined) {
98
+ this._layout?.select(this._tab_panel.tabs[this._index]);
101
99
  }
102
100
  };
103
101
  }
@@ -17,7 +17,10 @@
17
17
  */
18
18
 
19
19
  import { EMPTY_PANEL } from "./core/types.ts";
20
- import { create_css_grid_layout } from "./layout/generate_grid.ts";
20
+ import {
21
+ create_css_grid_layout,
22
+ create_css_maximize_layout,
23
+ } from "./layout/generate_grid.ts";
21
24
  import type {
22
25
  LayoutPath,
23
26
  Layout,
@@ -106,6 +109,14 @@ export interface PointerEventCoordinates {
106
109
  * latter implementation would require synchronizing the light DOM
107
110
  * and shadow DOM slots/slotted children continuously.
108
111
  *
112
+ * Note that `::slotted()` only matches *directly* slotted nodes - it
113
+ * does not pierce a slotted `<slot>`. A consumer that wants to forward
114
+ * its own light-DOM content through `<regular-layout>` therefore cannot
115
+ * place a bare `<slot name="X">` as a layout child and expect it to be
116
+ * grid-positioned; instead, wrap the forwarding slot in a concrete
117
+ * `<regular-layout-frame name="X">` (which *is* directly slotted and so
118
+ * matched by the generated `::slotted([name="X"])` rules).
119
+ *
109
120
  */
110
121
  export class RegularLayout extends HTMLElement {
111
122
  private _shadowRoot: ShadowRoot;
@@ -118,6 +129,7 @@ export class RegularLayout extends HTMLElement {
118
129
  private _physics: Physics;
119
130
  private _presizeQueue: PresizeQueue;
120
131
  private _overlayController: OverlayController;
132
+ private _maximized?: string;
121
133
 
122
134
  constructor() {
123
135
  super();
@@ -253,6 +265,36 @@ export class RegularLayout extends HTMLElement {
253
265
  await this.restore(remove_child(this._panel, name));
254
266
  };
255
267
 
268
+ /**
269
+ * Selects a panel by `name`, making it the front-most tab within its
270
+ * containing stack, then dispatches a `<prefix>-select` event with
271
+ * `detail: { name }`.
272
+ *
273
+ * The event fires whenever a tab is selected - including re-selecting the
274
+ * already-active tab, or selecting the sole tab of a single-element stack -
275
+ * so consumers can always map a tab interaction to an "active panel". When
276
+ * the selection actually changes, a `<prefix>-update` event is dispatched
277
+ * first (via {@link restore}).
278
+ *
279
+ * @param name - The name of the panel to select.
280
+ */
281
+ select = async (name: string) => {
282
+ const layout = this.save();
283
+ const tab_panel = this.getPanel(name, layout);
284
+ if (!tab_panel) {
285
+ return;
286
+ }
287
+
288
+ const index = tab_panel.tabs.indexOf(name);
289
+ if (index !== -1 && tab_panel.selected !== index) {
290
+ tab_panel.selected = index;
291
+ await this.restore(layout);
292
+ }
293
+
294
+ const event_name = `${this._physics.CUSTOM_EVENT_NAME_PREFIX}-select`;
295
+ this.dispatchEvent(new CustomEvent(event_name, { detail: { name } }));
296
+ };
297
+
256
298
  /**
257
299
  * Retrieves a panel by name from the layout tree.
258
300
  *
@@ -286,6 +328,42 @@ export class RegularLayout extends HTMLElement {
286
328
  await this.restore(EMPTY_PANEL);
287
329
  };
288
330
 
331
+ /**
332
+ * Maximizes a panel by `name`, rendering it full-size and hiding every
333
+ * other panel. This is transient display state - it is **not** persisted by
334
+ * {@link save}, and any subsequent {@link restore} resets the layout to the
335
+ * minimized (normal, multi-panel) view.
336
+ *
337
+ * Has no effect if `name` is not present in the current layout.
338
+ *
339
+ * @param name - The name of the panel to maximize.
340
+ */
341
+ maximize = (name: string) => {
342
+ if (!this.getPanel(name)) {
343
+ return;
344
+ }
345
+
346
+ this._maximized = name;
347
+ this._stylesheet.replaceSync(
348
+ create_css_maximize_layout(name, this._physics),
349
+ );
350
+ };
351
+
352
+ /**
353
+ * Restores the normal multi-panel view after a {@link maximize}, without
354
+ * altering the layout tree. No-op if no panel is currently maximized.
355
+ */
356
+ minimize = () => {
357
+ if (this._maximized === undefined) {
358
+ return;
359
+ }
360
+
361
+ this._maximized = undefined;
362
+ this._stylesheet.replaceSync(
363
+ create_css_grid_layout(this._panel, undefined, this._physics),
364
+ );
365
+ };
366
+
289
367
  /**
290
368
  * Restores the layout from a saved state synchronously, without
291
369
  * dispatching the `regular-layout-before-resize` event.
@@ -293,6 +371,7 @@ export class RegularLayout extends HTMLElement {
293
371
  * @param layout - The layout tree to restore
294
372
  */
295
373
  restoreSync = (layout: Layout, _is_flattened: boolean = false) => {
374
+ this._maximized = undefined;
296
375
  this._panel = !_is_flattened ? flatten(layout) : layout;
297
376
  const css = create_css_grid_layout(this._panel, undefined, this._physics);
298
377
  this._stylesheet.replaceSync(css);
@@ -320,6 +399,7 @@ export class RegularLayout extends HTMLElement {
320
399
  restore = async (layout: Layout, _is_flattened: boolean = false) => {
321
400
  const panel = !_is_flattened ? flatten(layout) : layout;
322
401
  await this._presizeQueue.run(panel, () => {
402
+ this._maximized = undefined;
323
403
  this._panel = panel;
324
404
  const css = create_css_grid_layout(this._panel, undefined, this._physics);
325
405
  this._stylesheet.replaceSync(css);
@@ -327,6 +407,7 @@ export class RegularLayout extends HTMLElement {
327
407
  const event = new CustomEvent<Layout>(event_name, {
328
408
  detail: this._panel,
329
409
  });
410
+
330
411
  this.dispatchEvent(event);
331
412
  });
332
413
  };
@@ -528,6 +609,10 @@ export class RegularLayout extends HTMLElement {
528
609
  };
529
610
 
530
611
  private onPointerDown = (event: PointerEvent) => {
612
+ if (event.button !== 0) {
613
+ return;
614
+ }
615
+
531
616
  if (!this._physics.GRID_DIVIDER_CHECK_TARGET || event.target === this) {
532
617
  const [col, row, rect] = this.relativeCoordinates(event);
533
618
  const size = this._physics.GRID_DIVIDER_SIZE;