regular-layout 0.0.2 → 0.2.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.
Files changed (42) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +20 -6
  3. package/dist/extensions.d.ts +10 -11
  4. package/dist/index.d.ts +3 -3
  5. package/dist/index.js +15 -10
  6. package/dist/index.js.map +4 -4
  7. package/dist/{common/calculate_split.d.ts → layout/calculate_edge.d.ts} +2 -2
  8. package/dist/{common → layout}/calculate_intersect.d.ts +7 -9
  9. package/dist/layout/constants.d.ts +43 -0
  10. package/dist/{common → layout}/flatten.d.ts +1 -1
  11. package/dist/{common → layout}/generate_grid.d.ts +3 -3
  12. package/dist/layout/generate_overlay.d.ts +2 -0
  13. package/dist/{common → layout}/insert_child.d.ts +3 -2
  14. package/dist/{common → layout}/redistribute_panel_sizes.d.ts +1 -1
  15. package/dist/{common → layout}/remove_child.d.ts +1 -1
  16. package/dist/{common/layout_config.d.ts → layout/types.d.ts} +9 -31
  17. package/dist/regular-layout-frame.d.ts +2 -2
  18. package/dist/regular-layout-tab.d.ts +26 -0
  19. package/dist/regular-layout.d.ts +61 -27
  20. package/package.json +9 -6
  21. package/src/extensions.ts +25 -15
  22. package/src/index.ts +3 -7
  23. package/src/layout/calculate_edge.ts +209 -0
  24. package/src/{common → layout}/calculate_intersect.ts +61 -100
  25. package/src/layout/constants.ts +63 -0
  26. package/src/{common → layout}/flatten.ts +2 -1
  27. package/src/{common → layout}/generate_grid.ts +77 -106
  28. package/src/{common → layout}/generate_overlay.ts +27 -12
  29. package/src/{common → layout}/insert_child.ts +105 -50
  30. package/src/{common → layout}/redistribute_panel_sizes.ts +2 -4
  31. package/src/{common → layout}/remove_child.ts +3 -2
  32. package/src/{common/layout_config.ts → layout/types.ts} +9 -44
  33. package/src/regular-layout-frame.ts +83 -68
  34. package/src/regular-layout-tab.ts +103 -0
  35. package/src/regular-layout.ts +257 -175
  36. package/themes/chicago.css +89 -0
  37. package/themes/fluxbox.css +110 -0
  38. package/themes/gibson.css +264 -0
  39. package/themes/hotdog.css +88 -0
  40. package/themes/lorax.css +129 -0
  41. package/dist/common/generate_overlay.d.ts +0 -2
  42. package/src/common/calculate_split.ts +0 -185
@@ -16,25 +16,28 @@
16
16
  * @packageDocumentation
17
17
  */
18
18
 
19
- import {
20
- EMPTY_PANEL,
21
- iter_panel_children,
22
- OVERLAY_DEFAULT,
23
- } from "./common/layout_config.ts";
24
- import { create_css_grid_layout } from "./common/generate_grid.ts";
19
+ import { EMPTY_PANEL } from "./layout/types.ts";
20
+ import { create_css_grid_layout } from "./layout/generate_grid.ts";
25
21
  import type {
26
22
  LayoutPath,
27
23
  Layout,
28
24
  LayoutDivider,
29
25
  TabLayout,
30
- } from "./common/layout_config.ts";
31
- import { calculate_intersection } from "./common/calculate_intersect.ts";
32
- import { remove_child } from "./common/remove_child.ts";
33
- import { insert_child } from "./common/insert_child.ts";
34
- import { redistribute_panel_sizes } from "./common/redistribute_panel_sizes.ts";
35
- import { updateOverlaySheet } from "./common/generate_overlay.ts";
36
- import { calculate_split } from "./common/calculate_split.ts";
37
- import { flatten } from "./common/flatten.ts";
26
+ OverlayMode,
27
+ Orientation,
28
+ } from "./layout/types.ts";
29
+ import { calculate_intersection } from "./layout/calculate_intersect.ts";
30
+ import { remove_child } from "./layout/remove_child.ts";
31
+ import { insert_child } from "./layout/insert_child.ts";
32
+ import { redistribute_panel_sizes } from "./layout/redistribute_panel_sizes.ts";
33
+ import { updateOverlaySheet } from "./layout/generate_overlay.ts";
34
+ import { calculate_edge } from "./layout/calculate_edge.ts";
35
+ import { flatten } from "./layout/flatten.ts";
36
+ import {
37
+ CUSTOM_EVENT_NAME_PREFIX,
38
+ OVERLAY_CLASSNAME,
39
+ OVERLAY_DEFAULT,
40
+ } from "./layout/constants.ts";
38
41
 
39
42
  /**
40
43
  * A Web Component that provides a resizable panel layout system.
@@ -45,8 +48,8 @@ import { flatten } from "./common/flatten.ts";
45
48
  * @example
46
49
  * ```html
47
50
  * <regular-layout>
48
- * <div slot="sidebar">Sidebar content</div>
49
- * <div slot="main">Main content</div>
51
+ * <div name="sidebar">Sidebar content</div>
52
+ * <div name="main">Main content</div>
50
53
  * </regular-layout>
51
54
  * ```
52
55
  *
@@ -73,22 +76,34 @@ export class RegularLayout extends HTMLElement {
73
76
  private _shadowRoot: ShadowRoot;
74
77
  private _panel: Layout;
75
78
  private _stylesheet: CSSStyleSheet;
76
- private _dragPath?: [LayoutDivider, number, number];
77
- private _slots: Map<string, HTMLSlotElement>;
78
- private _unslotted_slot: HTMLSlotElement;
79
+ private _cursor_stylesheet: CSSStyleSheet;
80
+ private _drag_target?: [LayoutDivider, number, number];
81
+ private _cursor_override: boolean;
82
+ private _dimensions?: { box: DOMRect; style: CSSStyleDeclaration };
79
83
 
80
84
  constructor() {
81
85
  super();
82
86
  this._panel = structuredClone(EMPTY_PANEL);
83
- this._stylesheet = new CSSStyleSheet();
84
- this._unslotted_slot = document.createElement("slot");
87
+
88
+ // Why does this implementation use a `<slot>` at all? We must use
89
+ // `<slot>` and the Shadow DOM to scope the grid CSS rules to each
90
+ // instance of `<regular-layout>` (without e.g. giving them unique
91
+ // `"id"` and injecting into `document,head`), and we can only select
92
+ // `::slotted` light DOM children from `adoptedStyleSheets` on the
93
+ // `ShadowRoot`.
94
+
95
+ // In addition, this model uses a single un-named `<slot>` to host all
96
+ // light-DOM children, and the child's `"name"` attribute to identify
97
+ // its position in the `Layout`. Alternatively, using named
85
98
  this._shadowRoot = this.attachShadow({ mode: "open" });
86
- this._shadowRoot.adoptedStyleSheets = [this._stylesheet];
87
- this._shadowRoot.appendChild(this._unslotted_slot);
88
- this._slots = new Map();
89
- this.onPointerDown = this.onPointerDown.bind(this);
90
- this.onPointerMove = this.onPointerMove.bind(this);
91
- this.onPointerUp = this.onPointerUp.bind(this);
99
+ this._shadowRoot.innerHTML = `<slot></slot>`;
100
+ this._stylesheet = new CSSStyleSheet();
101
+ this._cursor_stylesheet = new CSSStyleSheet();
102
+ this._cursor_override = false;
103
+ this._shadowRoot.adoptedStyleSheets = [
104
+ this._stylesheet,
105
+ this._cursor_stylesheet,
106
+ ];
92
107
  }
93
108
 
94
109
  connectedCallback() {
@@ -103,124 +118,173 @@ export class RegularLayout extends HTMLElement {
103
118
  this.removeEventListener("pointermove", this.onPointerMove);
104
119
  }
105
120
 
121
+ /**
122
+ * Determines which panel is at a given screen coordinate.
123
+ *
124
+ * @param column - X coordinate in screen pixels.
125
+ * @param row - Y coordinate in screen pixels.
126
+ * @returns Panel information if a panel is at that position, null otherwise.
127
+ */
128
+ calculateIntersect = (
129
+ x: number,
130
+ y: number,
131
+ check_dividers: boolean = false,
132
+ ): LayoutPath<Layout> | null => {
133
+ const [col, row, box] = this.relativeCoordinates(x, y, false);
134
+ const panel = calculate_intersection(
135
+ col,
136
+ row,
137
+ this._panel,
138
+ check_dividers ? box : null,
139
+ );
140
+
141
+ if (panel?.type === "layout-path") {
142
+ return { ...panel, layout: this.save() };
143
+ }
144
+
145
+ return null;
146
+ };
147
+
106
148
  /**
107
149
  * Sets the visual overlay state during drag-and-drop operations.
108
150
  * Displays a preview of where a panel would be placed at the given coordinates.
109
151
  *
110
152
  * @param x - X coordinate in screen pixels.
111
153
  * @param y - Y coordinate in screen pixels.
112
- * @param layoutPath - Layout path containing the slot identifier.
113
- * @param mode - Overlay rendering mode: "grid" highlights the target,
114
- * "absolute" positions the panel absolutely, "interactive" updates the
115
- * actual layout in real-time. Defaults to "absolute".
154
+ * @param dragTarget - A `LayoutPath` (presumably from `calculateIntersect`)
155
+ * which points to the drag element in the current layout.
156
+ * @param className - The CSS class name to use for the overlay panel
157
+ * (defaults to "overlay").
158
+ * @param mode - Overlay rendering mode: "grid" uses CSS grid to position
159
+ * the target, "absolute" positions the panel absolutely. Defaults to
160
+ * "absolute".
116
161
  */
117
- setOverlayState<T>(
162
+ setOverlayState = (
118
163
  x: number,
119
164
  y: number,
120
- { slot }: LayoutPath<T>,
121
- mode: "grid" | "absolute" | "interactive" = OVERLAY_DEFAULT,
122
- ) {
123
- let panel = this._panel;
124
- if (mode === "absolute") {
125
- panel = remove_child(panel, slot);
126
- this._slots.get(slot)?.assignedElements()[0]?.removeAttribute("slot");
127
- }
165
+ { slot }: LayoutPath<unknown>,
166
+ className: string = OVERLAY_CLASSNAME,
167
+ mode: OverlayMode = OVERLAY_DEFAULT,
168
+ ) => {
169
+ const panel = remove_child(this._panel, slot);
170
+ Array.from(this.children)
171
+ .find((x) => x.getAttribute("name") === slot)
172
+ ?.classList.add(className);
128
173
 
129
- const [col, row, box] = this.relativeCoordinates(x, y);
130
- let drop_target = calculate_intersection(col, row, panel, false);
174
+ const [col, row, box, style] = this.relativeCoordinates(x, y, false);
175
+ let drop_target = calculate_intersection(col, row, panel);
131
176
  if (drop_target) {
132
- drop_target = calculate_split(col, row, panel, slot, drop_target);
133
- if (mode === "interactive") {
134
- let new_panel = remove_child(this._panel, slot);
135
- new_panel = flatten(insert_child(new_panel, slot, drop_target.path));
136
- const css = create_css_grid_layout(new_panel);
137
- this._stylesheet.replaceSync(css);
138
- } else if (mode === "grid") {
139
- const path: [string, string] = [slot, drop_target.slot];
140
- const css = create_css_grid_layout(this._panel, false, path);
141
- this._stylesheet.replaceSync(css);
142
- } else if (mode === "absolute") {
143
- const css = `${create_css_grid_layout(panel)}\n${updateOverlaySheet({ ...drop_target, box })}`;
144
- this._stylesheet.replaceSync(css);
145
- }
146
- } else {
147
- const css = `${create_css_grid_layout(panel)}}`;
177
+ drop_target = calculate_edge(col, row, panel, slot, drop_target, box);
178
+ }
179
+
180
+ if (mode === "grid" && drop_target) {
181
+ const path: [string, string] = [slot, drop_target?.slot];
182
+ const css = create_css_grid_layout(panel, false, path);
148
183
  this._stylesheet.replaceSync(css);
184
+ } else if (mode === "absolute") {
185
+ const grid_css = create_css_grid_layout(panel);
186
+ const overlay_css = updateOverlaySheet(slot, box, style, drop_target);
187
+ this._stylesheet.replaceSync([grid_css, overlay_css].join("\n"));
149
188
  }
150
189
 
151
- const event = new CustomEvent("regular-layout-update", { detail: panel });
190
+ const event_name = `${CUSTOM_EVENT_NAME_PREFIX}-before-update`;
191
+ const event = new CustomEvent<Layout>(event_name, { detail: panel });
152
192
  this.dispatchEvent(event);
153
- }
193
+ };
154
194
 
155
195
  /**
156
196
  * Clears the overlay state and commits the panel placement.
157
197
  *
158
198
  * @param x - X coordinate in screen pixels.
159
199
  * @param y - Y coordinate in screen pixels.
160
- * @param layout_path - Layout path containing the slot identifier.
200
+ * @param dragTarget - A `LayoutPath` (presumably from `calculateIntersect`)
201
+ * which points to the drag element in the current layout.
202
+ * @param className - The CSS class name to use for the overlay panel
203
+ * (defaults to "overlay").
161
204
  * @param mode - Overlay rendering mode that was used, must match the mode
162
- * passed to `setOverlayState`. Defaults to "absolute".
205
+ * passed to `setOverlayState`. Defaults to "absolute".
163
206
  */
164
- clearOverlayState<T>(
207
+ clearOverlayState = (
165
208
  x: number,
166
209
  y: number,
167
- drag_target: LayoutPath<T>,
168
- mode: "grid" | "absolute" | "interactive" = OVERLAY_DEFAULT,
169
- ) {
210
+ drag_target: LayoutPath<Layout>,
211
+ className: string = OVERLAY_CLASSNAME,
212
+ ) => {
170
213
  let panel = this._panel;
171
- if (mode === "absolute") {
172
- panel = remove_child(panel, drag_target.slot);
173
- this._unslotted_slot
174
- .assignedElements()[0]
175
- ?.setAttribute("slot", drag_target.slot);
176
- }
214
+ panel = remove_child(panel, drag_target.slot);
215
+ Array.from(this.children)
216
+ .find((x) => x.getAttribute("name") === drag_target.slot)
217
+ ?.classList.remove(className);
177
218
 
178
- const [col, row, _] = this.relativeCoordinates(x, y);
179
- let drop_target = calculate_intersection(col, row, panel, false);
219
+ const [col, row, box] = this.relativeCoordinates(x, y, false);
220
+ let drop_target = calculate_intersection(col, row, panel);
180
221
  if (drop_target) {
181
- // TODO I think I only need the new path here?
182
- drop_target = calculate_split(
222
+ drop_target = calculate_edge(
183
223
  col,
184
224
  row,
185
225
  panel,
186
226
  drag_target.slot,
187
227
  drop_target,
228
+ box,
188
229
  );
189
230
  }
190
231
 
191
232
  const { path, orientation } = drop_target ? drop_target : drag_target;
233
+ const new_layout = drop_target
234
+ ? insert_child(
235
+ panel,
236
+ drag_target.slot,
237
+ path,
238
+ drop_target?.is_edge ? orientation : undefined,
239
+ )
240
+ : drag_target.layout;
192
241
 
193
- this.restore(
194
- insert_child(
195
- panel,
196
- drag_target.slot,
197
- path,
198
- orientation,
199
- !drop_target?.is_edge,
200
- ),
201
- );
202
- }
242
+ this.restore(new_layout);
243
+ };
203
244
 
204
245
  /**
205
246
  * Inserts a new panel into the layout at a specified path.
206
247
  *
207
248
  * @param name - Unique identifier for the new panel.
208
249
  * @param path - Index path defining where to insert.
250
+ * @param split - Force a split in the layout at the end of `path`
251
+ * regardless if there is a leaf at this position or not. Optionally,
252
+ *. `split` may be your preferred `Orientation`, which will be used by
253
+ * the new `SplitPanel` _if_ there is an option of orientation (e.g. if
254
+ * the layout had no pre-existing `SplitPanel`)
209
255
  */
210
- insertPanel(name: string, path: number[] = []) {
211
- this.restore(insert_child(this._panel, name, path));
212
- }
256
+ insertPanel = (
257
+ name: string,
258
+ path: number[] = [],
259
+ split?: boolean | Orientation,
260
+ ) => {
261
+ let orientation: Orientation | undefined;
262
+ if (typeof split === "boolean" && split) {
263
+ orientation = "horizontal";
264
+ } else if (typeof split === "string") {
265
+ orientation = split;
266
+ }
267
+
268
+ this.restore(insert_child(this._panel, name, path, orientation));
269
+ };
213
270
 
214
271
  /**
215
272
  * Removes a panel from the layout by name.
216
273
  *
217
274
  * @param name - Name of the panel to remove
218
275
  */
219
- removePanel(name: string) {
276
+ removePanel = (name: string) => {
220
277
  this.restore(remove_child(this._panel, name));
221
- }
278
+ };
222
279
 
223
- getPanel(name: string, layout: Layout = this._panel): TabLayout | null {
280
+ /**
281
+ * Retrieves a panel by name from the layout tree.
282
+ *
283
+ * @param name - Name of the panel to find.
284
+ * @param layout - Optional layout tree to search in (defaults to current layout).
285
+ * @returns The TabLayout containing the panel if found, null otherwise.
286
+ */
287
+ getPanel = (name: string, layout: Layout = this._panel): TabLayout | null => {
224
288
  if (layout.type === "child-panel") {
225
289
  if (layout.child.includes(name)) {
226
290
  return layout;
@@ -236,33 +300,14 @@ export class RegularLayout extends HTMLElement {
236
300
  }
237
301
 
238
302
  return null;
239
- }
303
+ };
240
304
 
241
305
  /**
242
- * Determines which panel is at a given screen coordinate.
243
- * Useful for drag-and-drop operations or custom interactions.
244
- *
245
- * @param column - X coordinate in screen pixels.
246
- * @param row - Y coordinate in screen pixels.
247
- * @returns Panel information if a panel is at that position, null otherwise.
306
+ * Clears the entire layout, unslotting all panels.
248
307
  */
249
- calculateIntersect(
250
- x: number,
251
- y: number,
252
- check_dividers: boolean = false,
253
- ): LayoutPath<DOMRect> | null {
254
- const [col, row, box] = this.relativeCoordinates(x, y);
255
- const panel = calculate_intersection(col, row, this._panel, check_dividers);
256
- if (panel?.type === "layout-path") {
257
- return { ...panel, box };
258
- }
259
-
260
- return null;
261
- }
262
-
263
- clear() {
308
+ clear = () => {
264
309
  this.restore(EMPTY_PANEL);
265
- }
310
+ };
266
311
 
267
312
  /**
268
313
  * Restores the layout from a saved state.
@@ -276,32 +321,14 @@ export class RegularLayout extends HTMLElement {
276
321
  * layout.restore(savedState);
277
322
  * ```
278
323
  */
279
- restore(layout: Layout, _is_flattened: boolean = false) {
324
+ restore = (layout: Layout, _is_flattened: boolean = false) => {
280
325
  this._panel = !_is_flattened ? flatten(layout) : layout;
281
- const css = create_css_grid_layout(layout);
326
+ const css = create_css_grid_layout(this._panel);
282
327
  this._stylesheet.replaceSync(css);
283
- const old = new Set(this._slots.keys());
284
- for (const name of iter_panel_children(layout)) {
285
- old.delete(name);
286
- if (!this._slots.has(name)) {
287
- const slot = document.createElement("slot");
288
- slot.setAttribute("name", name);
289
- this._shadowRoot.appendChild(slot);
290
- this._slots.set(name, slot);
291
- }
292
- }
293
-
294
- for (const key of old) {
295
- const child = this._slots.get(key);
296
- if (child) {
297
- this._shadowRoot.removeChild(child);
298
- this._slots.delete(key);
299
- }
300
- }
301
-
302
- const event = new CustomEvent("regular-layout-update", { detail: layout });
328
+ const event_name = `${CUSTOM_EVENT_NAME_PREFIX}-update`;
329
+ const event = new CustomEvent<Layout>(event_name, { detail: this._panel });
303
330
  this.dispatchEvent(event);
304
- }
331
+ };
305
332
 
306
333
  /**
307
334
  * Serializes the current layout state, which can be restored via `restore`.
@@ -315,59 +342,114 @@ export class RegularLayout extends HTMLElement {
315
342
  * localStorage.setItem('layout', JSON.stringify(state));
316
343
  * ```
317
344
  */
318
- save(): Layout {
345
+ save = (): Layout => {
319
346
  return structuredClone(this._panel);
320
- }
347
+ };
321
348
 
322
- private relativeCoordinates(
349
+ /**
350
+ * Converts screen coordinates to relative layout coordinates.
351
+ *
352
+ * Transforms absolute pixel positions into normalized coordinates (0-1 range)
353
+ * relative to the layout's bounding box.
354
+ *
355
+ * @param clientX - X coordinate in screen pixels (client space).
356
+ * @param clientY - Y coordinate in screen pixels (client space).
357
+ * @returns A tuple containing:
358
+ * - col: Normalized X coordinate (0 = left edge, 1 = right edge)
359
+ * - row: Normalized Y coordinate (0 = top edge, 1 = bottom edge)
360
+ * - box: The layout element's bounding rectangle
361
+ */
362
+ relativeCoordinates = (
323
363
  clientX: number,
324
364
  clientY: number,
325
- ): [number, number, DOMRect] {
326
- const box = this.getBoundingClientRect();
327
- const col = (clientX - box.left) / (box.right - box.left);
328
- const row = (clientY - box.top) / (box.bottom - box.top);
329
- return [col, row, box];
330
- }
365
+ recalculate_bounds: boolean = true,
366
+ ): [number, number, DOMRect, CSSStyleDeclaration] => {
367
+ if (recalculate_bounds || !this._dimensions) {
368
+ this._dimensions = {
369
+ box: this.getBoundingClientRect(),
370
+ style: getComputedStyle(this),
371
+ };
372
+ }
373
+
374
+ const box = this._dimensions.box;
375
+ const style = this._dimensions.style;
376
+ const col =
377
+ (clientX - box.left - parseFloat(style.paddingLeft)) /
378
+ (box.width -
379
+ parseFloat(style.paddingLeft) -
380
+ parseFloat(style.paddingRight));
381
+ const row =
382
+ (clientY - box.top - parseFloat(style.paddingTop)) /
383
+ (box.height -
384
+ parseFloat(style.paddingTop) -
385
+ parseFloat(style.paddingBottom));
386
+
387
+ return [col, row, box, style];
388
+ };
331
389
 
332
- private onPointerDown(event: PointerEvent) {
390
+ private onPointerDown = (event: PointerEvent) => {
333
391
  if (event.target === this) {
334
- const [col, row] = this.relativeCoordinates(event.clientX, event.clientY);
335
- const hit = calculate_intersection(col, row, this._panel);
392
+ const [col, row, box] = this.relativeCoordinates(
393
+ event.clientX,
394
+ event.clientY,
395
+ );
396
+
397
+ const hit = calculate_intersection(col, row, this._panel, box);
336
398
  if (hit && hit.type !== "layout-path") {
337
- this._dragPath = [hit, col, row];
399
+ this._drag_target = [hit, col, row];
338
400
  this.setPointerCapture(event.pointerId);
339
- // event.preventDefault();
340
- // event.stopImmediatePropagation();
401
+ event.preventDefault();
341
402
  }
342
403
  }
343
- }
404
+ };
405
+
406
+ private onPointerMove = (event: PointerEvent) => {
407
+ if (this._drag_target) {
408
+ const [col, row] = this.relativeCoordinates(
409
+ event.clientX,
410
+ event.clientY,
411
+ false,
412
+ );
344
413
 
345
- private onPointerMove(event: PointerEvent) {
346
- if (this._dragPath) {
347
- const [col, row] = this.relativeCoordinates(event.clientX, event.clientY);
348
- const old_panel = this._panel;
349
- const [{ path, type }, old_col, old_row] = this._dragPath;
414
+ const [{ path, type }, old_col, old_row] = this._drag_target;
350
415
  const offset = type === "horizontal" ? old_col - col : old_row - row;
351
- const panel = redistribute_panel_sizes(old_panel, path, offset);
416
+ const panel = redistribute_panel_sizes(this._panel, path, offset);
352
417
  this._stylesheet.replaceSync(create_css_grid_layout(panel));
418
+ } else if (event.target === this) {
419
+ const [col, row, box] = this.relativeCoordinates(
420
+ event.clientX,
421
+ event.clientY,
422
+ false,
423
+ );
424
+
425
+ const divider = calculate_intersection(col, row, this._panel, box);
426
+ if (divider?.type === "vertical") {
427
+ this._cursor_stylesheet.replaceSync(":host{cursor:row-resize");
428
+ this._cursor_override = true;
429
+ } else if (divider?.type === "horizontal") {
430
+ this._cursor_stylesheet.replaceSync(":host{cursor:col-resize");
431
+ this._cursor_override = true;
432
+ }
433
+ } else if (this._cursor_override) {
434
+ this._cursor_override = false;
435
+ this._cursor_stylesheet.replaceSync("");
353
436
  }
354
- }
437
+ };
355
438
 
356
- private onPointerUp(event: PointerEvent) {
357
- if (this._dragPath) {
439
+ private onPointerUp = (event: PointerEvent) => {
440
+ if (this._drag_target) {
358
441
  this.releasePointerCapture(event.pointerId);
359
- const [col, row] = this.relativeCoordinates(event.clientX, event.clientY);
360
- const old_panel = this._panel;
361
- const [{ path }, old_col, old_row] = this._dragPath;
362
- if (this._dragPath[0].type === "horizontal") {
363
- const panel = redistribute_panel_sizes(old_panel, path, old_col - col);
364
- this.restore(panel, true);
365
- } else {
366
- const panel = redistribute_panel_sizes(old_panel, path, old_row - row);
367
- this.restore(panel, true);
368
- }
442
+ const [col, row] = this.relativeCoordinates(
443
+ event.clientX,
444
+ event.clientY,
445
+ false,
446
+ );
369
447
 
370
- this._dragPath = undefined;
448
+ const [{ path, type }, old_col, old_row] = this._drag_target;
449
+ const offset = type === "horizontal" ? old_col - col : old_row - row;
450
+ const panel = redistribute_panel_sizes(this._panel, path, offset);
451
+ this.restore(panel, true);
452
+ this._drag_target = undefined;
371
453
  }
372
- }
454
+ };
373
455
  }
@@ -0,0 +1,89 @@
1
+ /* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
2
+ * ░░░░░░░░▄▀░█▀▄░█▀▀░█▀▀░█░█░█░░░█▀█░█▀▄░░░░░█░░░█▀█░█░█░█▀█░█░█░▀█▀░▀▄░░░░░░░░
3
+ * ░░░░░░░▀▄░░█▀▄░█▀▀░█░█░█░█░█░░░█▀█░█▀▄░▀▀▀░█░░░█▀█░░█░░█░█░█░█░░█░░░▄▀░░░░░░░
4
+ * ░░░░░░░░░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░▀░▀░░░░░▀▀▀░▀░▀░░▀░░▀▀▀░▀▀▀░░▀░░▀░░░░░░░░░
5
+ * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
6
+ * ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
7
+ * ┃ * Copyright (c) 2026, the Regular Layout Authors. This file is part * ┃
8
+ * ┃ * of the Regular Layout library, distributed under the terms of the * ┃
9
+ * ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
10
+ * ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
11
+ */
12
+
13
+ regular-layout.chicago {
14
+ background-color: #008080;
15
+ font-family: "ui-monospace", "SFMono-Regular", "SF Mono", "Menlo", "Consolas", "Liberation Mono", monospace;
16
+ padding: 24px;
17
+ }
18
+
19
+ /* Frame */
20
+ regular-layout.chicago regular-layout-frame {
21
+ position: relative;
22
+ box-sizing: border-box;
23
+ margin: 12px;
24
+ background: #C0C0C0;
25
+ border-width: 2px;
26
+ border-color: #FFFFFF #808080 #808080 #FFFFFF;
27
+ border-style: solid;
28
+ }
29
+
30
+ regular-layout.chicago regular-layout-frame::part(close) {
31
+ border-width: 1px;
32
+ border-color: #FFFFFF #808080 #808080 #FFFFFF;
33
+ border-style: solid;
34
+ height: 16px;
35
+ background: #C0C0C0;
36
+ align-self: center;
37
+ display: flex;
38
+ align-items: center;
39
+ padding: 0px 4px;
40
+ }
41
+
42
+ regular-layout.chicago regular-layout-frame::part(close):before {
43
+ content: "X";
44
+ font-size: 10px;
45
+ font-weight: bold;
46
+ }
47
+
48
+ regular-layout.chicago regular-layout-frame::part(titlebar) {
49
+ display: flex;
50
+ align-items: stretch;
51
+ padding-right: 0px;
52
+ }
53
+
54
+ regular-layout.chicago regular-layout-frame::part(tab) {
55
+ display: flex;
56
+ flex: 1 1 150px;
57
+ align-items: center;
58
+ padding: 0 3px 0 8px;
59
+ cursor: pointer;
60
+ text-overflow: ellipsis;
61
+ background: #808080;
62
+ color: #FFF;
63
+ font-family: "Tahoma", "Arial", sans-serif;
64
+ font-weight: bold;
65
+ font-size: 11px;
66
+ }
67
+
68
+ regular-layout.chicago regular-layout-frame::part(active-tab) {
69
+ background: #000080;
70
+ opacity: 1;
71
+ }
72
+
73
+ regular-layout.chicago:has(.overlay)>* {
74
+ opacity: 0.8;
75
+ }
76
+
77
+ regular-layout.chicago:has(.overlay)>.overlay {
78
+ opacity: 1;
79
+ }
80
+
81
+ /* Frame in Overlay Mode */
82
+ regular-layout.chicago regular-layout-frame.overlay {
83
+ margin: 0;
84
+ transition:
85
+ top 0.1s ease-in-out,
86
+ height 0.1s ease-in-out,
87
+ width 0.1s ease-in-out,
88
+ left 0.1s ease-in-out;
89
+ }