@gjsify/adwaita-web 0.64.1-next.240 → 0.64.1-next.243

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 (56) hide show
  1. package/README.md +42 -4
  2. package/dist/adwaita-web.css +12 -1
  3. package/dist/adwaita-web.css.map +1 -1
  4. package/lib/types/canvas2d/canvas2d-bridge.d.ts +18 -0
  5. package/lib/types/canvas2d-bridge.spec.d.ts +1 -0
  6. package/lib/types/elements/adw-action-row.d.ts +3 -0
  7. package/lib/types/elements/adw-bottom-sheet.d.ts +3 -0
  8. package/lib/types/elements/adw-navigation-view.d.ts +7 -0
  9. package/lib/types/elements/adw-switch-row.d.ts +3 -0
  10. package/lib/types/elements/adw-window-features.d.ts +6 -1
  11. package/lib/types/elements/gtk-application-window.d.ts +2 -0
  12. package/lib/types/elements/gtk-image.d.ts +12 -3
  13. package/lib/types/elements/gtk-scrolled-window.d.ts +10 -0
  14. package/lib/types/elements/gtk-window.d.ts +29 -3
  15. package/lib/types/gobject-elements.d.ts +7 -0
  16. package/lib/types/icon-registry.d.ts +3 -1
  17. package/lib/types/icon-theme.d.ts +53 -0
  18. package/lib/types/icon-theme.spec.d.ts +1 -0
  19. package/lib/types/index.d.ts +1 -0
  20. package/lib/types/styles.generated.d.ts +1 -1
  21. package/package.json +12 -12
  22. package/scss/_about_dialog.scss +2 -2
  23. package/scss/_icon.scss +18 -0
  24. package/scss/_window.scss +7 -0
  25. package/src/adw-action-rows.spec.ts +20 -0
  26. package/src/adw-alert-dialog.spec.ts +15 -0
  27. package/src/adw-application-window.spec.ts +2 -1
  28. package/src/adw-window.spec.ts +22 -3
  29. package/src/bottom-sheet.spec.ts +11 -0
  30. package/src/canvas2d/canvas2d-bridge.ts +51 -1
  31. package/src/canvas2d-bridge.spec.ts +47 -0
  32. package/src/elements/adw-about-dialog.ts +4 -2
  33. package/src/elements/adw-action-row.ts +4 -0
  34. package/src/elements/adw-alert-dialog.ts +6 -0
  35. package/src/elements/adw-application-window.ts +9 -2
  36. package/src/elements/adw-bottom-sheet.ts +4 -0
  37. package/src/elements/adw-navigation-view.ts +19 -0
  38. package/src/elements/adw-switch-row.ts +4 -0
  39. package/src/elements/adw-window-features.ts +19 -4
  40. package/src/elements/adw-window.ts +2 -2
  41. package/src/elements/gtk-application-window.ts +6 -0
  42. package/src/elements/gtk-image.ts +55 -24
  43. package/src/elements/gtk-scrolled-window.ts +37 -1
  44. package/src/elements/gtk-window.ts +82 -3
  45. package/src/gobject-door.spec.ts +44 -0
  46. package/src/gobject-elements.ts +43 -2
  47. package/src/gtk-scrolled-window.spec.ts +74 -0
  48. package/src/gtk-window.spec.ts +97 -1
  49. package/src/icon-registry.ts +13 -7
  50. package/src/icon-theme.spec.ts +182 -0
  51. package/src/icon-theme.ts +178 -0
  52. package/src/index.ts +3 -0
  53. package/src/navigation-view.spec.ts +26 -0
  54. package/src/shared-tree-builder.ts +2 -1
  55. package/src/styles.generated.ts +1 -1
  56. package/src/test.browser.mts +5 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/adwaita-web",
3
- "version": "0.64.1-next.240",
3
+ "version": "0.64.1-next.243",
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.64.1-next.240",
62
- "@gjsify/adwaita-fonts": "0.64.1-next.240",
63
- "@gjsify/adwaita-icons": "0.64.1-next.240",
64
- "@gjsify/gtksource-core": "0.64.1-next.240",
61
+ "@gjsify/adwaita-core": "0.64.1-next.243",
62
+ "@gjsify/adwaita-fonts": "0.64.1-next.243",
63
+ "@gjsify/adwaita-icons": "0.64.1-next.243",
64
+ "@gjsify/gtksource-core": "0.64.1-next.243",
65
65
  "@lezer/highlight": "^1.2.3"
66
66
  },
67
67
  "peerDependencies": {
68
- "@gjsify/app-settings": "0.64.1-next.240"
68
+ "@gjsify/app-settings": "0.64.1-next.243"
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.64.1-next.240",
77
- "@gjsify/blueprint": "0.64.1-next.240",
78
- "@gjsify/cli": "0.64.1-next.240",
79
- "@gjsify/unit": "0.64.1-next.240",
80
- "@gjsify/vite-plugin-blueprint": "0.64.1-next.240",
76
+ "@gjsify/app-settings": "0.64.1-next.243",
77
+ "@gjsify/blueprint": "0.64.1-next.243",
78
+ "@gjsify/cli": "0.64.1-next.243",
79
+ "@gjsify/unit": "0.64.1-next.243",
80
+ "@gjsify/vite-plugin-blueprint": "0.64.1-next.243",
81
81
  "sass": "^1.101.0",
82
82
  "typescript": "^6.0.3"
83
83
  },
@@ -115,5 +115,5 @@
115
115
  "url": "https://github.com/gjsify/gjsify/issues"
116
116
  },
117
117
  "homepage": "https://github.com/gjsify/gjsify/tree/main/packages/web/adwaita-web#readme",
118
- "gitHead": "9cc65385593e615e921d7c3b2ceb31fd03e98e4b"
118
+ "gitHead": "f88c7b1074d454e211094963a9f4dc7d0982bf32"
119
119
  }
@@ -149,10 +149,10 @@ adw-about-dialog {
149
149
  height: 96px;
150
150
  margin-top: var(--spacing-s);
151
151
  margin-bottom: var(--spacing-m);
152
- // Mask-driven .adw-icon skin scaled up to the about-dialog app-icon size.
152
+ // A symbolic glyph scales with the box; the fill stays `.adw-icon`'s `currentColor`,
153
+ // which `.adw-icon-color` turns off for a registered colour application icon.
153
154
  -webkit-mask-size: contain;
154
155
  mask-size: contain;
155
- background-color: currentColor;
156
156
  }
157
157
 
158
158
  // --- App name (title-1) ---
package/scss/_icon.scss CHANGED
@@ -96,3 +96,21 @@
96
96
  .adw-icon:not([class*='adw-icon--']) {
97
97
  background-color: transparent;
98
98
  }
99
+
100
+ // A FULL-COLOUR icon — what `registerColorIcon()` registers and `<gtk-image>` resolves a
101
+ // name to before (or, for `-symbolic` names, after) the symbolic glyph, in the order GTK's
102
+ // theme tries them. It is drawn as-is, the way GTK draws `hicolor/…/apps/<app-id>.svg`: no
103
+ // mask and no `currentColor` fill, only the image the element puts in `background-image`.
104
+ //
105
+ // The modifier is NOT `adw-icon--…`, so the rule above never sees it as a mask class.
106
+ // Two classes beat both `:where(.adw-icon)` and the one-class sizing rules a context sets
107
+ // (`.adw-about-dialog-icon`'s mask-size), so an icon box drawn in colour never picks up a
108
+ // leftover mask from them.
109
+ .adw-icon.adw-icon-color {
110
+ mask-image: none;
111
+ -webkit-mask-image: none;
112
+ background-color: transparent;
113
+ background-position: center;
114
+ background-repeat: no-repeat;
115
+ background-size: contain;
116
+ }
package/scss/_window.scss CHANGED
@@ -26,6 +26,13 @@ adw-application-window {
26
26
  outline: 1px solid rgb(255 255 255 / 7%);
27
27
  outline-offset: -1px;
28
28
 
29
+ // No authored size (the element sets an inline one from `width`/`default-width`): a page
30
+ // has no window manager to size a toplevel, so it fills the viewport. The 360×200
31
+ // minimum (Adw.Window's size request) is the inline min-size the element sets.
32
+ width: 100%;
33
+ height: 100dvh;
34
+ box-sizing: border-box;
35
+
29
36
  // `window.maximized` / `window.fullscreen` zero the radius and drop the shadow
30
37
  // (_window.scss:66-72). A page-sized window IS the maximised one here: the frame
31
38
  // is in-page, so there is nothing outside it to cast a shadow on.
@@ -674,4 +674,24 @@ export const AdwActionRowsTest = async () => {
674
674
  host.remove();
675
675
  });
676
676
  });
677
+
678
+ await describe('declared GTK signals reach connect()', async () => {
679
+ await it('<adw-action-row> connects `activated`', () => {
680
+ const { el, host } = mount<AdwActionRow>('adw-action-row');
681
+ let seen = 0;
682
+ (el as unknown as { connect(s: string, h: () => void): number }).connect('activated', () => seen++);
683
+ el.dispatchEvent(new CustomEvent('activated'));
684
+ expect(seen).toBe(1);
685
+ host.remove();
686
+ });
687
+
688
+ await it('<adw-switch-row> connects `notify::active`', () => {
689
+ const { el, host } = mount<AdwSwitchRow>('adw-switch-row');
690
+ let seen = 0;
691
+ (el as unknown as { connect(s: string, h: () => void): number }).connect('notify::active', () => seen++);
692
+ el.setAttribute('active', '');
693
+ expect(seen).toBe(1);
694
+ host.remove();
695
+ });
696
+ });
677
697
  };
@@ -143,4 +143,19 @@ export const AdwAlertDialogTest = async () => {
143
143
  host.remove();
144
144
  });
145
145
  });
146
+
147
+ await describe('adw-alert-dialog responses before the first connect', async () => {
148
+ await it('buttons added while detached appear once connected', () => {
149
+ const el = document.createElement('adw-alert-dialog') as AdwAlertDialog;
150
+ el.addResponse('cancel', 'Cancel');
151
+ el.addResponse('ok', 'OK');
152
+ expect(el.getResponseLabel('ok')).toBe('OK');
153
+ const host = document.createElement('div');
154
+ document.body.appendChild(host);
155
+ host.appendChild(el);
156
+ const labels = [...el.querySelectorAll('.adw-alert-dialog-response')].map((b) => b.textContent);
157
+ expect(labels.join(',')).toBe('Cancel,OK');
158
+ host.remove();
159
+ });
160
+ });
146
161
  };
@@ -47,7 +47,8 @@ export const AdwApplicationWindowTest = async () => {
47
47
 
48
48
  await it('keeps the inherited size request', async () => {
49
49
  const win = makeWindow();
50
- expect(win.style.width).toBe('360px');
50
+ expect(win.style.minWidth).toBe('360px');
51
+ expect(win.style.width).toBe('');
51
52
  win.setAttribute('width', '520');
52
53
  expect(win.style.width).toBe('520px');
53
54
  win.remove();
@@ -53,13 +53,32 @@ export const AdwWindowTest = async () => {
53
53
  win.remove();
54
54
  });
55
55
 
56
- await it('applies the default 360×200 size', async () => {
56
+ await it('keeps 360×200 as a minimum and fills the viewport without an authored size', async () => {
57
57
  const win = makeWindow();
58
- expect(win.style.width).toBe('360px');
59
- expect(win.style.height).toBe('200px');
58
+ expect(win.style.minWidth).toBe('360px');
59
+ expect(win.style.minHeight).toBe('200px');
60
+ expect(win.style.width).toBe('');
61
+ expect(win.style.height).toBe('');
62
+ expect(Math.round(win.getBoundingClientRect().width)).toBe(document.documentElement.clientWidth);
63
+ expect(Math.round(win.getBoundingClientRect().height)).toBe(window.innerHeight);
60
64
  win.remove();
61
65
  });
62
66
 
67
+ await it('width-request/height-request override the minimum; gtk-window has none', async () => {
68
+ const win = document.createElement('adw-window') as AdwWindow;
69
+ win.setAttribute('width-request', '100');
70
+ win.setAttribute('height-request', '50');
71
+ document.body.appendChild(win);
72
+ expect(win.style.minWidth).toBe('100px');
73
+ expect(win.style.minHeight).toBe('50px');
74
+ win.remove();
75
+ const plain = document.createElement('gtk-window');
76
+ document.body.appendChild(plain);
77
+ expect(plain.style.minWidth).toBe('');
78
+ expect(plain.style.minHeight).toBe('');
79
+ plain.remove();
80
+ });
81
+
63
82
  await it('width/height attributes override the default size', async () => {
64
83
  const win = document.createElement('adw-window') as AdwWindow;
65
84
  win.setAttribute('width', '500');
@@ -577,4 +577,15 @@ export const AdwBottomSheetTest = async () => {
577
577
  sheet.remove();
578
578
  });
579
579
  });
580
+
581
+ await describe('<adw-bottom-sheet> declared signals', async () => {
582
+ await it('connects `notify::open`', () => {
583
+ const sheet = mountSheet();
584
+ let seen = 0;
585
+ (sheet as unknown as { connect(s: string, h: () => void): number }).connect('notify::open', () => seen++);
586
+ sheet.open = true;
587
+ expect(seen).toBe(1);
588
+ sheet.remove();
589
+ });
590
+ });
580
591
  };
@@ -63,7 +63,55 @@ export class Canvas2DBridgeElement extends HostElement {
63
63
  private _drawFrame: number | null = null;
64
64
 
65
65
  static get observedAttributes() {
66
- return ['content-width', 'content-height'];
66
+ return ['content-width', 'content-height', 'focusable', 'can-focus'];
67
+ }
68
+
69
+ /**
70
+ * `Gtk.Widget:focusable`, whether the widget can hold the keyboard focus. `Gtk.DrawingArea`
71
+ * leaves it FALSE, so it is read by PRESENCE; a blueprint's `focusable: true` writes it.
72
+ */
73
+ get focusable(): boolean {
74
+ return this.hasAttribute('focusable');
75
+ }
76
+
77
+ set focusable(v: boolean) {
78
+ this.toggleAttribute('focusable', !!v);
79
+ }
80
+
81
+ /** `Gtk.Widget:can-focus`, TRUE by default, so it is read by VALUE. */
82
+ get canFocus(): boolean {
83
+ return this.getAttribute('can-focus') !== 'false';
84
+ }
85
+
86
+ set canFocus(v: boolean) {
87
+ this.setAttribute('can-focus', v ? 'true' : 'false');
88
+ }
89
+
90
+ get can_focus(): boolean {
91
+ return this.canFocus;
92
+ }
93
+
94
+ set can_focus(v: boolean) {
95
+ this.canFocus = v;
96
+ }
97
+
98
+ /**
99
+ * `gtk_widget_grab_focus`: moves the keyboard focus here and says whether it landed.
100
+ * FALSE when the widget is not focusable or cannot focus, as in GTK.
101
+ */
102
+ grab_focus(): boolean {
103
+ if (!this.focusable || !this.canFocus) return false;
104
+ this.focus();
105
+ return this.ownerDocument.activeElement === this;
106
+ }
107
+
108
+ grabFocus(): boolean {
109
+ return this.grab_focus();
110
+ }
111
+
112
+ private _syncFocus(): void {
113
+ if (this.focusable && this.canFocus) this.tabIndex = 0;
114
+ else this.removeAttribute('tabindex');
67
115
  }
68
116
 
69
117
  /** `Gtk.DrawingArea:content-width`, the minimum and natural width. Defaults to 0. */
@@ -161,6 +209,7 @@ export class Canvas2DBridgeElement extends HostElement {
161
209
  this.style.overflow = 'hidden';
162
210
  this.style.position = 'relative';
163
211
  this._publishSize();
212
+ this._syncFocus();
164
213
  this._observe();
165
214
  }
166
215
 
@@ -171,6 +220,7 @@ export class Canvas2DBridgeElement extends HostElement {
171
220
 
172
221
  attributeChangedCallback(name: string) {
173
222
  if (name === 'content-width' || name === 'content-height') this._publishSize();
223
+ else this._syncFocus();
174
224
  }
175
225
 
176
226
  private _publishSize(): void {
@@ -0,0 +1,47 @@
1
+ // DOM-level tests for <gjsify-canvas2d-bridge>: the keyboard focus a game console needs for its keys.
2
+ import { describe, expect, it } from '@gjsify/unit';
3
+
4
+ import type { Canvas2DBridgeElement } from './canvas2d/canvas2d-bridge.js';
5
+
6
+ function mount(attrs: Record<string, string> = {}): { el: Canvas2DBridgeElement; host: HTMLElement } {
7
+ const host = document.createElement('div');
8
+ document.body.appendChild(host);
9
+ const el = document.createElement('gjsify-canvas2d-bridge') as Canvas2DBridgeElement;
10
+ for (const [name, value] of Object.entries(attrs)) el.setAttribute(name, value);
11
+ host.appendChild(el);
12
+ return { el, host };
13
+ }
14
+
15
+ export const Canvas2DBridgeTest = async () => {
16
+ await describe('<gjsify-canvas2d-bridge> focus', async () => {
17
+ await it('is not focusable by default, so grab_focus returns false', () => {
18
+ const { el, host } = mount();
19
+ expect(el.focusable).toBe(false);
20
+ expect(el.grab_focus()).toBe(false);
21
+ expect(document.activeElement === el).toBe(false);
22
+ host.remove();
23
+ });
24
+
25
+ await it('grab_focus takes the keyboard focus when focusable', () => {
26
+ const { el, host } = mount({ focusable: '' });
27
+ expect(el.grab_focus()).toBe(true);
28
+ expect(document.activeElement === el).toBe(true);
29
+ host.remove();
30
+ });
31
+
32
+ await it('a focusable set after connect makes the element focusable', () => {
33
+ const { el, host } = mount();
34
+ el.focusable = true;
35
+ expect(el.grab_focus()).toBe(true);
36
+ host.remove();
37
+ });
38
+
39
+ await it('can-focus false refuses the focus', () => {
40
+ const { el, host } = mount({ focusable: '' });
41
+ el.canFocus = false;
42
+ expect(el.can_focus).toBe(false);
43
+ expect(el.grab_focus()).toBe(false);
44
+ host.remove();
45
+ });
46
+ });
47
+ };
@@ -19,8 +19,10 @@
19
19
  // Properties / attributes (all reflected, mirroring the AdwAboutDialog GObject
20
20
  // properties of the same name, hyphenated):
21
21
  // application-name the application's display name (shown as a title-1)
22
- // application-icon a symbolic icon name (with or without -symbolic); absent
23
- // one, no icon is drawn
22
+ // application-icon an icon name, by convention the application ID; drawn in
23
+ // colour once `registerColorIcon(id, …)` registered it, else
24
+ // as a symbolic glyph or `image-missing` (`<gtk-image>`'s
25
+ // lookup); absent one, no icon is drawn
24
26
  // developer-name the developer / vendor line below the name
25
27
  // version the version, shown as a clickable pill
26
28
  // comments a longer description (Details page)
@@ -27,6 +27,7 @@ import { ActionRowState } from '@gjsify/adwaita-core';
27
27
  import { bindEmptySections } from '../empty-sections.js';
28
28
  import { applyRowTooltip } from '../row-tooltip.js';
29
29
  import { bindSlottedChildren } from '../slotted-children.js';
30
+ import type { DispatchedSignals } from '../signals.js';
30
31
  import { type AdwRowActivation, attachRowActivation } from './row-activation.js';
31
32
  import { applyRowLineClamp, parseRowLinesAttribute } from '../row-line-clamp.js';
32
33
 
@@ -44,6 +45,9 @@ function isSensitive(widget: HTMLElement): boolean {
44
45
  }
45
46
 
46
47
  export class AdwActionRow extends HTMLElement {
48
+ /** The GTK signals this element dispatches, each with the DOM event it arrives as (ADR 0093). */
49
+ static readonly signals: DispatchedSignals = { activated: 'activated' };
50
+
47
51
  private _titleEl!: HTMLSpanElement;
48
52
  private _subtitleEl!: HTMLSpanElement;
49
53
  private _suffixEl!: HTMLDivElement;
@@ -222,6 +222,10 @@ export class AdwAlertDialog extends HTMLElement {
222
222
  this._responseAreaEl = document.createElement('div');
223
223
  this._responseAreaEl.className = 'adw-alert-dialog-responses';
224
224
 
225
+ // Responses added before the first connect already own a button (`addResponse` is
226
+ // callable at any point in the dialog's life); only the area to hang them in was missing.
227
+ this._responseAreaEl.append(...this._buttons.values());
228
+
225
229
  this._dialogEl.append(messageArea, this._responseAreaEl);
226
230
 
227
231
  // Two slots, both LIVE. An `<adw-alert-response>` is DATA — read for its id, label
@@ -463,6 +467,8 @@ export class AdwAlertDialog extends HTMLElement {
463
467
  }
464
468
 
465
469
  private _render(): void {
470
+ // Before the first connect there is no DOM to render into; `connectedCallback` renders.
471
+ if (!this._initialized) return;
466
472
  this.classList.toggle('open', this.open);
467
473
 
468
474
  const heading = this.heading;
@@ -57,7 +57,14 @@ export class AdwApplicationWindow extends GtkApplicationWindow {
57
57
  // resolves a `[...Base.observedAttributes]` spread across files, and reports a
58
58
  // `super` one as unreadable rather than as empty).
59
59
  static get observedAttributes(): string[] {
60
- return [...GtkApplicationWindow.observedAttributes, 'width', 'height', 'menu-model'];
60
+ return [
61
+ ...GtkApplicationWindow.observedAttributes,
62
+ 'width',
63
+ 'height',
64
+ 'width-request',
65
+ 'height-request',
66
+ 'menu-model',
67
+ ];
61
68
  }
62
69
 
63
70
  /** The content area — the Adw.Window:content property. */
@@ -131,7 +138,7 @@ export class AdwApplicationWindow extends GtkApplicationWindow {
131
138
  }
132
139
 
133
140
  attributeChangedCallback(name: string) {
134
- if (name === 'width' || name === 'height') {
141
+ if (name === 'width' || name === 'height' || name === 'width-request' || name === 'height-request') {
135
142
  this.renderFrameSize();
136
143
  return;
137
144
  }
@@ -64,6 +64,7 @@ import type {
64
64
  } from '@gjsify/adwaita-core';
65
65
 
66
66
  import { bindSlottedChildren, type AdwSlot } from '../slotted-children.js';
67
+ import type { DispatchedSignals } from '../signals.js';
67
68
 
68
69
  /** The persistent content. Child of <adw-bottom-sheet>; consumed at connect time. */
69
70
  export class AdwBottomSheetContent extends HTMLElement {}
@@ -75,6 +76,9 @@ export class AdwBottomSheetSheet extends HTMLElement {}
75
76
  export class AdwBottomSheetBottomBar extends HTMLElement {}
76
77
 
77
78
  export class AdwBottomSheet extends HTMLElement {
79
+ /** The GTK signals this element dispatches, each with the DOM event it arrives as (ADR 0093). */
80
+ static readonly signals: DispatchedSignals = { 'notify::open': 'notify::open' };
81
+
78
82
  private _initialized = false;
79
83
  private _contentEl!: HTMLDivElement;
80
84
  private _dimmingEl!: HTMLDivElement;
@@ -55,6 +55,7 @@ import { BACK_BUTTON_FALLBACK_TOOLTIP, NavigationViewState, describeNavigationDi
55
55
  import type { AdwNavigationPageProps, NavigationDiagnostic, NavigationStackChange } from '@gjsify/adwaita-core';
56
56
 
57
57
  import { bindSlottedChildren } from '../slotted-children.js';
58
+ import type { DispatchedSignals } from '../signals.js';
58
59
 
59
60
  /** The page properties the core owns, read off the element's attributes. */
60
61
  function readPageProps(page: AdwNavigationPage): AdwNavigationPageProps {
@@ -135,6 +136,14 @@ export class AdwNavigationPage extends HTMLElement {
135
136
  }
136
137
 
137
138
  export class AdwNavigationView extends HTMLElement {
139
+ /** The GTK signals this element dispatches, each with the DOM event it arrives as (ADR 0093). */
140
+ static readonly signals: DispatchedSignals = {
141
+ pushed: 'pushed',
142
+ popped: 'popped',
143
+ replaced: 'replaced',
144
+ 'notify::visible-page': 'notify::visible-page',
145
+ };
146
+
138
147
  private _pagesEl!: HTMLDivElement;
139
148
  // The back button injected into each visible page (so we can remove it).
140
149
  private readonly _backButtons = new WeakMap<AdwNavigationPage, HTMLElement>();
@@ -178,6 +187,11 @@ export class AdwNavigationView extends HTMLElement {
178
187
  return this._state.visiblePageTag;
179
188
  }
180
189
 
190
+ /** `adw_navigation_view_get_visible_page_tag`. */
191
+ get_visible_page_tag(): string | null {
192
+ return this.visiblePageTag;
193
+ }
194
+
181
195
  get depth(): number {
182
196
  return this._state.depth;
183
197
  }
@@ -303,6 +317,11 @@ export class AdwNavigationView extends HTMLElement {
303
317
  return pushed;
304
318
  }
305
319
 
320
+ /** `adw_navigation_view_push_by_tag`, under the name a GJS caller uses. */
321
+ push_by_tag(tag: string): boolean {
322
+ return this.pushByTag(tag);
323
+ }
324
+
306
325
  /** Push the page carrying `tag` (mirrors AdwNavigationView.push_by_tag). */
307
326
  pushByTag(tag: string): boolean {
308
327
  const pushed = this._state.pushByTag(tag);
@@ -29,6 +29,7 @@ import { SwitchRowState, deriveRowLabels } from '@gjsify/adwaita-core';
29
29
  import { bindEmptySections } from '../empty-sections.js';
30
30
  import { applyRowTooltip } from '../row-tooltip.js';
31
31
  import { bindSlottedChildren } from '../slotted-children.js';
32
+ import type { DispatchedSignals } from '../signals.js';
32
33
  import { applyRowLineClamp, parseRowLinesAttribute } from '../row-line-clamp.js';
33
34
 
34
35
  // SIDE-EFFECT import, deliberately separate from the type import below: it guarantees
@@ -42,6 +43,9 @@ import { attachRowActivation } from './row-activation.js';
42
43
  import type { GtkSwitch } from './gtk-switch.js';
43
44
 
44
45
  export class AdwSwitchRow extends HTMLElement {
46
+ /** The GTK signals this element dispatches, each with the DOM event it arrives as (ADR 0093). */
47
+ static readonly signals: DispatchedSignals = { 'notify::active': 'notify::active' };
48
+
45
49
  private _switchEl!: GtkSwitch;
46
50
  private _prefixEl!: HTMLDivElement;
47
51
  /** The label column — kept for `tooltip-text`, which the browser reads off a PART. */
@@ -13,10 +13,16 @@
13
13
  import { AdwBreakpoint } from '@gjsify/adwaita-core';
14
14
  import { addBreakpoints } from '../breakpoints.js';
15
15
 
16
- /** The default minimum size of an AdwWindow, from adw_window_init (C:344). */
16
+ /** `adw_window_init`'s size request: a MINIMUM, overridable by `width-request`/`height-request`. */
17
17
  const DEFAULT_WIDTH = 360;
18
18
  const DEFAULT_HEIGHT = 200;
19
19
 
20
+ /** A `width-request`/`height-request` attribute as px; absent, unparsable or -1 means unset. */
21
+ function requested(host: HTMLElement, name: string, fallback: number): number {
22
+ const value = Number.parseFloat(host.getAttribute(name) ?? '');
23
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
24
+ }
25
+
20
26
  export class AdwWindowFeatures {
21
27
  private _contentEl: HTMLDivElement | null = null;
22
28
  private _breakpoints: AdwBreakpoint[] = [];
@@ -117,12 +123,21 @@ export class AdwWindowFeatures {
117
123
  this._disposeBreakpoints = null;
118
124
  }
119
125
 
120
- /** The window's size request — GTK's, not CSS's, so an absent attribute means the default. */
126
+ /**
127
+ * The window's size. `Adw.Window` defaults to a 360×200 MINIMUM (a size request, not the
128
+ * size; `width-request`/`height-request` override it). `default-width/height` is only
129
+ * a default: an authored size is the box, and an absent one leaves the box to the
130
+ * stylesheet, which fills the viewport — a page has no window manager to pick a size.
131
+ */
121
132
  applySizeRequest(): void {
122
133
  const host = this._host;
134
+ host.style.minWidth = `${requested(host, 'width-request', DEFAULT_WIDTH)}px`;
135
+ host.style.minHeight = `${requested(host, 'height-request', DEFAULT_HEIGHT)}px`;
123
136
  const width = host.getAttribute('default-width') ?? host.getAttribute('width');
124
137
  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`;
138
+ if (width) host.style.width = `${width}px`;
139
+ else host.style.removeProperty('width');
140
+ if (height) host.style.height = `${height}px`;
141
+ else host.style.removeProperty('height');
127
142
  }
128
143
  }
@@ -48,7 +48,7 @@ export class AdwWindow extends GtkWindow {
48
48
  private readonly _features = new AdwWindowFeatures(this);
49
49
 
50
50
  static get observedAttributes() {
51
- return [...GtkWindow.observedAttributes, 'width', 'height'];
51
+ return [...GtkWindow.observedAttributes, 'width', 'height', 'width-request', 'height-request'];
52
52
  }
53
53
 
54
54
  /** The content area — the Adw.Window:content property. */
@@ -105,7 +105,7 @@ export class AdwWindow extends GtkWindow {
105
105
  attributeChangedCallback(name: string) {
106
106
  // The size request is read from the ATTRIBUTE, so it is applied on every change —
107
107
  // including one that arrived before the element was connected.
108
- if (name === 'width' || name === 'height') {
108
+ if (name === 'width' || name === 'height' || name === 'width-request' || name === 'height-request') {
109
109
  this.renderFrameSize();
110
110
  return;
111
111
  }
@@ -28,6 +28,7 @@
28
28
 
29
29
  import type { ActionLike } from '@gjsify/adwaita-core';
30
30
  import type { GtkApplication } from '../application.js';
31
+ import type { DispatchedSignals } from '../signals.js';
31
32
  import type { AdwSlot } from '../slotted-children.js';
32
33
  import { GtkWindow } from './gtk-window.js';
33
34
  import { defineWindowApplication } from '../application.js';
@@ -45,6 +46,11 @@ export class GtkApplicationWindow extends GtkWindow {
45
46
  private _menubar: HTMLDivElement | null = null;
46
47
  private _menubarObserver: MutationObserver | null = null;
47
48
 
49
+ static override readonly signals: DispatchedSignals = {
50
+ ...GtkWindow.signals,
51
+ 'notify::show-menubar': 'notify::show-menubar',
52
+ };
53
+
48
54
  static get observedAttributes(): string[] {
49
55
  return [...GtkWindow.observedAttributes, 'show-menubar'];
50
56
  }