@gjsify/adwaita-web 0.63.0 → 0.63.1-next.235

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/adwaita-web",
3
- "version": "0.63.0",
3
+ "version": "0.63.1-next.235",
4
4
  "description": "Adwaita/Libadwaita web components for browser targets",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -58,14 +58,14 @@
58
58
  "@codemirror/language": "^6.12.4",
59
59
  "@codemirror/state": "^6.7.0",
60
60
  "@codemirror/view": "^6.43.4",
61
- "@gjsify/adwaita-core": "^0.63.0",
62
- "@gjsify/adwaita-fonts": "^0.63.0",
63
- "@gjsify/adwaita-icons": "^0.63.0",
64
- "@gjsify/gtksource-core": "^0.63.0",
61
+ "@gjsify/adwaita-core": "0.63.1-next.235",
62
+ "@gjsify/adwaita-fonts": "0.63.1-next.235",
63
+ "@gjsify/adwaita-icons": "0.63.1-next.235",
64
+ "@gjsify/gtksource-core": "0.63.1-next.235",
65
65
  "@lezer/highlight": "^1.2.3"
66
66
  },
67
67
  "peerDependencies": {
68
- "@gjsify/app-settings": "^0.63.0"
68
+ "@gjsify/app-settings": "0.63.1-next.235"
69
69
  },
70
70
  "peerDependenciesMeta": {
71
71
  "@gjsify/app-settings": {
@@ -73,11 +73,11 @@
73
73
  }
74
74
  },
75
75
  "devDependencies": {
76
- "@gjsify/app-settings": "^0.63.0",
77
- "@gjsify/blueprint": "^0.63.0",
78
- "@gjsify/cli": "^0.63.0",
79
- "@gjsify/unit": "^0.63.0",
80
- "@gjsify/vite-plugin-blueprint": "^0.63.0",
76
+ "@gjsify/app-settings": "0.63.1-next.235",
77
+ "@gjsify/blueprint": "0.63.1-next.235",
78
+ "@gjsify/cli": "0.63.1-next.235",
79
+ "@gjsify/unit": "0.63.1-next.235",
80
+ "@gjsify/vite-plugin-blueprint": "0.63.1-next.235",
81
81
  "sass": "^1.101.0",
82
82
  "typescript": "^6.0.3"
83
83
  },
@@ -114,5 +114,6 @@
114
114
  "bugs": {
115
115
  "url": "https://github.com/gjsify/gjsify/issues"
116
116
  },
117
- "homepage": "https://github.com/gjsify/gjsify/tree/main/packages/web/adwaita-web#readme"
117
+ "homepage": "https://github.com/gjsify/gjsify/tree/main/packages/web/adwaita-web#readme",
118
+ "gitHead": "9d2cca56170a0215b3e0792e98421ed1f83e7f72"
118
119
  }
package/scss/_window.scss CHANGED
@@ -35,6 +35,11 @@ adw-application-window {
35
35
  outline: none;
36
36
  box-shadow: none;
37
37
  }
38
+
39
+ // `present()` and `hide-on-close` toggle `hidden`, which `display: flex` would override.
40
+ &[hidden] {
41
+ display: none;
42
+ }
38
43
  }
39
44
 
40
45
  // The content area — the one child slot `Adw.Window:content` fills. It takes the
@@ -64,8 +69,8 @@ adw-application-window {
64
69
  // through a selector list: `_reset.scss` re-roots the box model at each registered element,
65
70
  // and a shared `adw-window, gtk-window` block would put two component resets in one rule.
66
71
  //
67
- // NOT the same as `<adw-window>`, deliberately. `Adw.Window` is a plain styled container —
68
- // nothing about it is GTK's `GtkWindow` — while this one answers the FRAME properties:
72
+ // NOT the same as `<adw-window>`, deliberately. `Adw.Window` extends `GtkWindow` but has no
73
+ // titlebar area, while this one answers the FRAME properties:
69
74
  // `decorated` drops the titlebar strip, `maximized` and `fullscreened` square the corners
70
75
  // off and drop the shadow (`_window.scss:66-72`), and the strip itself is either the
71
76
  // application's `titlebar` or the two `<gtk-window-controls>` a server-side decoration would
@@ -7,6 +7,9 @@
7
7
  // `Adw.Window` plus `Gtk.ApplicationWindow:show-menubar`, so the window half is
8
8
  // <adw-window>'s spec and only the delta is asserted here.
9
9
  import { describe, it, expect } from '@gjsify/unit';
10
+ import { driveWindowVectors, WINDOW_VECTORS } from '@gjsify/adwaita-core/conformance';
11
+
12
+ import { webWindowSubject } from './gtk-window.spec.js';
10
13
 
11
14
  import type { AdwApplicationWindow } from './elements/adw-application-window.js';
12
15
 
@@ -30,6 +33,11 @@ function makeWindow(model: unknown = MENU_MODEL): AdwApplicationWindow {
30
33
  }
31
34
 
32
35
  export const AdwApplicationWindowTest = async () => {
36
+ // `Adw.Window` IS a `Gtk.Window` upstream: the same vectors that run against real GTK and
37
+ // <gtk-window> must hold here, or the state members have fallen off the subclass again.
38
+ const { subject, dispose } = webWindowSubject('adw-application-window');
39
+ await driveWindowVectors(subject, { describe, it, expect }, WINDOW_VECTORS);
40
+ dispose();
33
41
  await describe('adw-application-window is an adw-window', async () => {
34
42
  await it('places children in the inherited content area', async () => {
35
43
  const win = makeWindow();
@@ -33,6 +33,17 @@ export const AdwDataGridTest = async () => {
33
33
  grid.remove();
34
34
  });
35
35
 
36
+ await it('set_columns / set_rows are the same properties, the spelling the NativeScript port shares', async () => {
37
+ const grid = makeGrid();
38
+ grid.set_columns(BWA_COLUMNS);
39
+ grid.set_rows([{ position: 'Umsatz', total: '1' }]);
40
+ expect(grid.get_columns().map((column) => column.key)).toStrictEqual(['position', 'total']);
41
+ expect(grid.columns).toBe(grid.get_columns());
42
+ expect(grid.get_rows().length).toBe(1);
43
+ expect(grid.querySelectorAll('.adw-data-grid-row:not(.header)').length).toBe(1);
44
+ grid.remove();
45
+ });
46
+
36
47
  await it('falls back to the key when no label is given', async () => {
37
48
  const grid = makeGrid([{ key: 'amount' }], []);
38
49
  const cell = grid.querySelector('.adw-data-grid-row.header .adw-data-grid-cell');
@@ -4,6 +4,9 @@
4
4
  // these specs create elements via document.createElement and assert their DOM
5
5
  // behaviour (content area, breakpoints, dialog tracking).
6
6
  import { describe, it, expect } from '@gjsify/unit';
7
+ import { driveWindowVectors, WINDOW_VECTORS } from '@gjsify/adwaita-core/conformance';
8
+
9
+ import { webWindowSubject } from './gtk-window.spec.js';
7
10
 
8
11
  import type { AdwWindow } from './elements/adw-window.js';
9
12
 
@@ -22,6 +25,11 @@ function settle(): Promise<void> {
22
25
  }
23
26
 
24
27
  export const AdwWindowTest = async () => {
28
+ // `Adw.Window` IS a `Gtk.Window` upstream: the same vectors that run against real GTK and
29
+ // <gtk-window> must hold here, or the state members have fallen off the subclass again.
30
+ const { subject, dispose } = webWindowSubject('adw-window');
31
+ await driveWindowVectors(subject, { describe, it, expect }, WINDOW_VECTORS);
32
+ dispose();
25
33
  await describe('adw-window content', async () => {
26
34
  await it('places children in the content area', async () => {
27
35
  const win = makeWindow('<p id="hello">Hello</p>');
@@ -23,7 +23,7 @@ const made: Element[] = [];
23
23
  const scene: ApplicationScene = {
24
24
  window(app) {
25
25
  const window = document.createElement('adw-application-window') as Adw.ApplicationWindow;
26
- window.application = app;
26
+ window.application = app as unknown as Gtk.Application;
27
27
  document.body.append(window);
28
28
  made.push(window);
29
29
  return window;
@@ -558,6 +558,15 @@ export const AdwBottomSheetTest = async () => {
558
558
  sheet.remove();
559
559
  });
560
560
 
561
+ await it('get_modal / set_modal are the same property, the spelling the NativeScript port shares', () => {
562
+ const sheet = mountSheet();
563
+ sheet.set_modal(false);
564
+ expect(sheet.modal).toBe(false);
565
+ expect(sheet.get_modal()).toBe(false);
566
+ expect(sheet.classList.contains('modal')).toBe(false);
567
+ sheet.remove();
568
+ });
569
+
561
570
  await it('the dimming layer becomes interactive only while open AND modal', () => {
562
571
  const sheet = mountSheet();
563
572
  expect(dimming(sheet).classList.contains('visible')).toBe(false);
@@ -16,7 +16,23 @@
16
16
 
17
17
  import { registerTemplateClass } from '../template-classes.js';
18
18
 
19
- type ReadyCallback = (canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) => void;
19
+ /**
20
+ * The DOM types this element names, resolved through `globalThis` so the published `.d.ts`
21
+ * never names a `lib.dom` type directly: a consumer that loads `lib.dom` gets the real ones,
22
+ * one that does not still type-checks the declarations with `skipLibCheck: false`.
23
+ */
24
+ export type BridgeCanvas = typeof globalThis extends { HTMLCanvasElement: { prototype: infer T } } ? T : object;
25
+ export type BridgeContext2D = typeof globalThis extends { CanvasRenderingContext2D: { prototype: infer T } }
26
+ ? T
27
+ : object;
28
+ /** `FrameRequestCallback` without `lib.dom`. */
29
+ export type FrameCallback = (time: number) => void;
30
+ type ElementConstructor = typeof globalThis extends { HTMLElement: infer C } ? C : abstract new () => object;
31
+
32
+ // biome-ignore lint: the annotation, not the value, is what the declaration file keeps
33
+ const HostElement: ElementConstructor = (globalThis as { HTMLElement?: unknown }).HTMLElement as ElementConstructor;
34
+
35
+ type ReadyCallback = (canvas: BridgeCanvas, ctx: BridgeContext2D) => void;
20
36
  type ResizeCallback = (width: number, height: number) => void;
21
37
  type SignalHandler = (emitter: Canvas2DBridgeElement, ...args: unknown[]) => unknown;
22
38
 
@@ -25,18 +41,18 @@ const TAG = 'gjsify-canvas2d-bridge';
25
41
  /** `G_MAXINT`, the top of `g_param_spec_int`'s range. */
26
42
  const MAX_INT = 2147483647;
27
43
 
28
- function intAttribute(element: HTMLElement, name: string): number {
44
+ function intAttribute(element: Element, name: string): number {
29
45
  const raw = Number.parseInt(element.getAttribute(name) ?? '', 10);
30
46
  return Number.isFinite(raw) ? raw : 0;
31
47
  }
32
48
 
33
- export class Canvas2DBridgeElement extends HTMLElement {
49
+ export class Canvas2DBridgeElement extends HostElement {
34
50
  static readonly GTypeName = 'GjsifyCanvas2DBridge';
35
51
  /** The signals `connect()` accepts, as `Gtk.Widget` names them. */
36
52
  static readonly signals: Record<string, string> = { resize: 'resize' };
37
53
 
38
- private _canvas: HTMLCanvasElement | null = null;
39
- private _ctx: CanvasRenderingContext2D | null = null;
54
+ private _canvas: BridgeCanvas | null = null;
55
+ private _ctx: BridgeContext2D | null = null;
40
56
  private _readyCallbacks: ReadyCallback[] = [];
41
57
  private _resizeCallbacks: ResizeCallback[] = [];
42
58
  private _handlers = new Map<number, { signal: string; handler: SignalHandler }>();
@@ -89,12 +105,12 @@ export class Canvas2DBridgeElement extends HTMLElement {
89
105
  }
90
106
 
91
107
  /** The `<canvas>` backing this widget. Available after the first allocation. */
92
- get canvas(): HTMLCanvasElement | null {
108
+ get canvas(): BridgeCanvas | null {
93
109
  return this._canvas;
94
110
  }
95
111
 
96
112
  /** The browser's own 2D context; available after the first allocation. */
97
- getContext(_id: '2d'): CanvasRenderingContext2D | null {
113
+ getContext(_id: '2d'): BridgeContext2D | null {
98
114
  return this._ctx;
99
115
  }
100
116
 
@@ -113,7 +129,7 @@ export class Canvas2DBridgeElement extends HTMLElement {
113
129
  }
114
130
 
115
131
  /** The browser's own, here only so the call reads the same on every target. */
116
- requestAnimationFrame(cb: FrameRequestCallback): number {
132
+ requestAnimationFrame(cb: FrameCallback): number {
117
133
  return globalThis.requestAnimationFrame(cb);
118
134
  }
119
135
 
@@ -23,8 +23,9 @@
23
23
  // menu-model (the portable menu model, JSON — the stand-in for the model a
24
24
  // GtkApplication would own; the same value `<gtk-menu-button menu-model>` takes)
25
25
  //
26
- // Extends <adw-window> and adds nothing else: the content property, the size request,
27
- // the breakpoints and the dialog bookkeeping are all inherited.
26
+ // Extends <gtk-application-window>, as Adw.ApplicationWindow extends Gtk.ApplicationWindow,
27
+ // so the Gtk.Window state members come with it. The content property, the size request, the
28
+ // breakpoints and the dialog bookkeeping are the same AdwWindowFeatures <adw-window> uses.
28
29
  //
29
30
  // Reference: refs/libadwaita/src/adw-application-window.c (the class adds no behaviour)
30
31
  // Reference: Gtk-4.0.gir `Gtk.ApplicationWindow:show-menubar` ("If this property is true,
@@ -40,21 +41,56 @@ import {
40
41
  type AdwMenuModel,
41
42
  } from '@gjsify/adwaita-core';
42
43
 
43
- import { AdwWindow } from './adw-window.js';
44
+ import type { AdwBreakpoint } from '@gjsify/adwaita-core';
45
+ import type { AdwSlot } from '../slotted-children.js';
46
+ import { AdwWindowFeatures } from './adw-window-features.js';
47
+ import { GtkApplicationWindow } from './gtk-application-window.js';
44
48
  import { defineWindowApplication } from '../application.js';
45
49
 
46
- export class AdwApplicationWindow extends AdwWindow {
47
- /** `Gtk.Window:application`: setting it is `add_window` (ADR 0098), set by `defineWindowApplication`. */
48
- declare application: object | null;
49
- private _menubarEl: HTMLDivElement | null = null;
50
+ export class AdwApplicationWindow extends GtkApplicationWindow {
51
+ private readonly _features = new AdwWindowFeatures(this);
50
52
  private _model: AdwMenuModel = [];
53
+ private _menubarEl: HTMLDivElement | null = null;
51
54
 
52
55
  // The base's list BY NAME, not `super.observedAttributes`: `AdwPasswordEntryRow`
53
56
  // spells its spread this way and the attribute readers follow (`adwaita-elements.mjs`
54
57
  // resolves a `[...Base.observedAttributes]` spread across files, and reports a
55
58
  // `super` one as unreadable rather than as empty).
56
59
  static get observedAttributes(): string[] {
57
- return [...AdwWindow.observedAttributes, 'show-menubar', 'menu-model'];
60
+ return [...GtkApplicationWindow.observedAttributes, 'width', 'height', 'menu-model'];
61
+ }
62
+
63
+ /** The content area — the Adw.Window:content property. */
64
+ get contentArea(): HTMLDivElement {
65
+ return this._content;
66
+ }
67
+
68
+ get content(): HTMLElement | null {
69
+ return this._features.content;
70
+ }
71
+
72
+ set content(value: HTMLElement | null) {
73
+ this._features.content = value;
74
+ }
75
+
76
+ get breakpoints(): readonly AdwBreakpoint[] {
77
+ return this._features.breakpoints;
78
+ }
79
+
80
+ get currentBreakpoint(): AdwBreakpoint | null {
81
+ return this._features.currentBreakpoint;
82
+ }
83
+
84
+ get dialogs(): readonly HTMLElement[] {
85
+ return this._features.dialogs;
86
+ }
87
+
88
+ get visibleDialog(): HTMLElement | null {
89
+ return this._features.visibleDialog;
90
+ }
91
+
92
+ addBreakpoint(condition: string): AdwBreakpoint {
93
+ return this._features.addBreakpoint(condition);
58
94
  }
59
95
 
60
96
  /** Whether this window shows the application's menubar (Gtk.ApplicationWindow:show-menubar). */
@@ -84,17 +120,45 @@ export class AdwApplicationWindow extends AdwWindow {
84
120
 
85
121
  connectedCallback() {
86
122
  super.connectedCallback();
87
- this._buildMenubar();
123
+ this._features.adopt(this._content);
124
+ // EVERY connect, not only the first — see `AdwWindowFeatures.bind`.
125
+ this._features.bind();
126
+ }
127
+
128
+ disconnectedCallback() {
129
+ this._features.release();
130
+ super.disconnectedCallback();
88
131
  }
89
132
 
90
133
  attributeChangedCallback(name: string) {
134
+ if (name === 'width' || name === 'height') {
135
+ this.renderFrameSize();
136
+ return;
137
+ }
91
138
  super.attributeChangedCallback(name);
92
139
  // The attribute is the declarative form and it wins when it changes — the same
93
140
  // precedence `<gtk-menu-button>` gives `menu-model`, through the same parser.
94
- if (name === 'menu-model') this._model = parseMenuModel(this.getAttribute('menu-model'));
95
- // Before the first connect the menubar does not exist yet, and `_buildMenubar`
96
- // renders from the model this has just read — so nothing is built twice.
97
- if (name === 'show-menubar' || name === 'menu-model') this._renderMenubar();
141
+ if (name === 'menu-model') {
142
+ this._model = parseMenuModel(this.getAttribute('menu-model'));
143
+ this._renderMenubar();
144
+ }
145
+ }
146
+
147
+ /** `Adw.ApplicationWindow` has no titlebar of its own, like `Adw.Window`. */
148
+ protected override hasTitlebar(): boolean {
149
+ return false;
150
+ }
151
+
152
+ protected override strips(): Node[] {
153
+ return [this._content];
154
+ }
155
+
156
+ protected override extraSlots(): AdwSlot[] {
157
+ return [{ name: 'content', into: this._content }];
158
+ }
159
+
160
+ protected override renderFrameSize(): void {
161
+ this._features.applySizeRequest();
98
162
  }
99
163
 
100
164
  /**
@@ -104,17 +168,23 @@ export class AdwApplicationWindow extends AdwWindow {
104
168
  * into the content area, so a sibling would be re-homed into it one tick later. The
105
169
  * content area's own children are not watched, so prepending here is stable — and
106
170
  * it is also where the bar belongs: above the toolbar view, inside the window box.
171
+ * Replaces the base's observer: this bar is filled from the model, not from a slot,
172
+ * and observing it would re-trigger every render.
107
173
  */
108
- private _buildMenubar() {
109
- if (this._menubarEl) return;
110
- this._menubarEl = document.createElement('div');
111
- this._menubarEl.className = 'adw-application-window-menubar';
112
- this.contentArea.prepend(this._menubarEl);
113
-
174
+ protected override _followMenubar(): void {
175
+ if (this._menubarEl === null) {
176
+ this._menubarEl = document.createElement('div');
177
+ this._menubarEl.className = 'adw-application-window-menubar';
178
+ this._content.prepend(this._menubarEl);
179
+ }
114
180
  this._renderMenubar();
115
181
  }
116
182
 
117
- private _renderMenubar(): void {
183
+ protected override menubarSection(): HTMLDivElement {
184
+ return this._menubarEl ?? super.menubarSection();
185
+ }
186
+
187
+ protected override _renderMenubar(): void {
118
188
  const bar = this._menubarEl;
119
189
  if (bar === null) return;
120
190
 
@@ -110,6 +110,15 @@ export class AdwBottomSheet extends HTMLElement {
110
110
  this._setBoolAttr('modal', value);
111
111
  }
112
112
 
113
+ /** `adw_bottom_sheet_get_modal` — the NativeScript port's only door to it (ADR 0112). */
114
+ get_modal(): boolean {
115
+ return this.modal;
116
+ }
117
+
118
+ set_modal(modal: boolean): void {
119
+ this.modal = modal;
120
+ }
121
+
113
122
  /** Whether the user can dismiss the sheet (dimming / Escape / sheet.close). */
114
123
  get canClose(): boolean {
115
124
  return this._state.canClose;
@@ -111,6 +111,24 @@ export class AdwDataGrid extends HTMLElement {
111
111
  if (this._initialized) this._render();
112
112
  }
113
113
 
114
+ // The verbs are the spelling the NativeScript port has to use, where `GridLayout` owns both
115
+ // accessor names (ADR 0112); offered here too so one code-behind runs on both.
116
+ get_columns(): AdwDataGridColumn[] {
117
+ return this.columns;
118
+ }
119
+
120
+ set_columns(value: ReadonlyArray<AdwDataGridColumn>): void {
121
+ this.columns = value;
122
+ }
123
+
124
+ get_rows(): AdwDataGridRow[] {
125
+ return this.rows;
126
+ }
127
+
128
+ set_rows(value: ReadonlyArray<AdwDataGridRow>): void {
129
+ this.rows = value;
130
+ }
131
+
114
132
  /** The optional grid caption/title (reflected to the attribute). */
115
133
  get caption(): string {
116
134
  return this.getAttribute('caption') ?? '';
@@ -0,0 +1,128 @@
1
+ // What `Adw.Window` and `Adw.ApplicationWindow` add on top of their Gtk parent: the content
2
+ // area, the breakpoints and the open-dialog bookkeeping.
3
+ //
4
+ // A HELPER, NOT A BASE CLASS. Upstream `AdwWindow` extends `GtkWindow` and `AdwApplicationWindow`
5
+ // extends `GtkApplicationWindow` (Adw-1.gir), so the two cannot share an Adw base without one of
6
+ // them losing its Gtk parent. A custom element has one prototype chain, so the shared part is
7
+ // composed in: each class owns one of these and forwards to it.
8
+ //
9
+ // Reference: refs/libadwaita/src/adw-window.c
10
+ // Copyright (c) 2020-2023 Alice Mikhaylenko / Purism SPC (libadwaita). LGPLv2.1+
11
+ // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
12
+
13
+ import { AdwBreakpoint } from '@gjsify/adwaita-core';
14
+ import { addBreakpoints } from '../breakpoints.js';
15
+
16
+ /** The default minimum size of an AdwWindow, from adw_window_init (C:344). */
17
+ const DEFAULT_WIDTH = 360;
18
+ const DEFAULT_HEIGHT = 200;
19
+
20
+ export class AdwWindowFeatures {
21
+ private _contentEl: HTMLDivElement | null = null;
22
+ private _breakpoints: AdwBreakpoint[] = [];
23
+ private _disposeBreakpoints: (() => void) | null = null;
24
+ private _openDialogs: HTMLElement[] = [];
25
+
26
+ constructor(private readonly _host: HTMLElement) {}
27
+
28
+ /** The content area, or `null` before the window is first connected. */
29
+ get contentArea(): HTMLDivElement | null {
30
+ return this._contentEl;
31
+ }
32
+
33
+ /** The content widget — the first child of the content area. */
34
+ get content(): HTMLElement | null {
35
+ return (this._contentEl?.firstElementChild as HTMLElement | null) ?? null;
36
+ }
37
+
38
+ set content(value: HTMLElement | null) {
39
+ if (value) this._contentEl?.appendChild(value);
40
+ }
41
+
42
+ get breakpoints(): readonly AdwBreakpoint[] {
43
+ return this._breakpoints;
44
+ }
45
+
46
+ get currentBreakpoint(): AdwBreakpoint | null {
47
+ return this._breakpoints.find((bp) => bp.applied) ?? null;
48
+ }
49
+
50
+ get dialogs(): readonly HTMLElement[] {
51
+ // A dialog taken out of the document never raises `notify::open` on the way
52
+ // out, so a dialog that was REMOVED while open would otherwise stay listed
53
+ // for the rest of the window's life.
54
+ this._openDialogs = this._openDialogs.filter((dialog) => dialog.isConnected);
55
+ return this._openDialogs;
56
+ }
57
+
58
+ get visibleDialog(): HTMLElement | null {
59
+ const dialogs = this.dialogs;
60
+ return dialogs.length > 0 ? (dialogs[dialogs.length - 1] as HTMLElement) : null;
61
+ }
62
+
63
+ /**
64
+ * Take the content area the Gtk parent built, once. Tracks open dialogs presented inside
65
+ * the window: an AdwDialog raises `notify::open`, which bubbles to the host.
66
+ */
67
+ adopt(contentEl: HTMLDivElement): void {
68
+ if (this._contentEl !== null) return;
69
+ this._contentEl = contentEl;
70
+ contentEl.classList.add('adw-window-content');
71
+ this._host.addEventListener('notify::open', (e) => {
72
+ const dialog = e.target as HTMLElement;
73
+ if (dialog === this._host) return;
74
+ if (dialog.hasAttribute('open')) {
75
+ if (!this._openDialogs.includes(dialog)) this._openDialogs.push(dialog);
76
+ } else {
77
+ this._openDialogs = this._openDialogs.filter((d) => d !== dialog);
78
+ }
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Add a breakpoint (adw_window_add_breakpoint). The breakpoint evaluates against the
84
+ * window's own box and sets a `data-breakpoint` attribute when applied.
85
+ */
86
+ addBreakpoint(condition: string): AdwBreakpoint {
87
+ const host = this._host;
88
+ const bp = new AdwBreakpoint(condition, {
89
+ onApply: () => (host.dataset.breakpoint = condition),
90
+ onUnapply: () => {
91
+ if (host.dataset.breakpoint === condition) delete host.dataset.breakpoint;
92
+ },
93
+ });
94
+ this._breakpoints.push(bp);
95
+ // Re-evaluated against a box that may have changed since the last add.
96
+ this.bind();
97
+ return bp;
98
+ }
99
+
100
+ /**
101
+ * Bind the breakpoints to THIS box.
102
+ *
103
+ * NOT once per lifetime: `release` drops the disposer, so a window moved between
104
+ * parents would come back deaf — and a window is exactly the thing a client-side route
105
+ * change re-parents. Re-binding is cheap and idempotent, and it re-measures the new box.
106
+ */
107
+ bind(): void {
108
+ this._disposeBreakpoints?.();
109
+ this._disposeBreakpoints = null;
110
+ if (this._contentEl !== null && this._breakpoints.length > 0) {
111
+ this._disposeBreakpoints = addBreakpoints(this._host, this._breakpoints);
112
+ }
113
+ }
114
+
115
+ release(): void {
116
+ this._disposeBreakpoints?.();
117
+ this._disposeBreakpoints = null;
118
+ }
119
+
120
+ /** The window's size request — GTK's, not CSS's, so an absent attribute means the default. */
121
+ applySizeRequest(): void {
122
+ const host = this._host;
123
+ const width = host.getAttribute('default-width') ?? host.getAttribute('width');
124
+ const height = host.getAttribute('default-height') ?? host.getAttribute('height');
125
+ host.style.width = width ? `${width}px` : `${DEFAULT_WIDTH}px`;
126
+ host.style.height = height ? `${height}px` : `${DEFAULT_HEIGHT}px`;
127
+ }
128
+ }