@gjsify/adwaita-web 0.56.0 → 0.57.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 (69) hide show
  1. package/dist/adwaita-web.css +8 -2
  2. package/dist/adwaita-web.css.map +1 -1
  3. package/lib/types/application.d.ts +24 -0
  4. package/lib/types/application.spec.d.ts +1 -0
  5. package/lib/types/elements/adw-application-window.d.ts +2 -0
  6. package/lib/types/elements/adw-view-stack.d.ts +3 -0
  7. package/lib/types/elements/checks.d.ts +6 -0
  8. package/lib/types/elements/gtk-application-window.d.ts +2 -0
  9. package/lib/types/elements/gtk-drop-down.d.ts +3 -0
  10. package/lib/types/elements/gtk-entry.d.ts +10 -0
  11. package/lib/types/elements/gtk-menu-button.d.ts +2 -0
  12. package/lib/types/elements/gtk-scrolled-window.d.ts +16 -0
  13. package/lib/types/elements/gtk-stack.d.ts +3 -0
  14. package/lib/types/elements/gtk-switch.d.ts +3 -0
  15. package/lib/types/glib.spec.d.ts +1 -0
  16. package/lib/types/gobject-door.d.ts +4 -0
  17. package/lib/types/gobject-door.spec.d.ts +1 -0
  18. package/lib/types/gobject-elements.d.ts +17 -0
  19. package/lib/types/gtksource/gtk-source-view.d.ts +77 -0
  20. package/lib/types/gtksource/gtk-source-view.spec.d.ts +2 -0
  21. package/lib/types/gtksource/web-editor-driver.d.ts +44 -0
  22. package/lib/types/index.d.ts +3 -0
  23. package/lib/types/menu-actions.spec.d.ts +1 -0
  24. package/lib/types/namespace/adw.d.ts +1 -0
  25. package/lib/types/namespace/gio.d.ts +2 -0
  26. package/lib/types/namespace/glib.d.ts +10 -0
  27. package/lib/types/namespace/gobject.d.ts +27 -0
  28. package/lib/types/namespace/gtk-enums.d.ts +22 -0
  29. package/lib/types/namespace/gtk.d.ts +2 -0
  30. package/lib/types/namespace/gtksource.d.ts +2 -0
  31. package/lib/types/shared-tree-builder.d.ts +20 -1
  32. package/lib/types/styles.generated.d.ts +1 -1
  33. package/package.json +11 -9
  34. package/scss/_reset.scss +1 -1
  35. package/src/application.spec.ts +75 -0
  36. package/src/application.ts +92 -0
  37. package/src/construct-vectors.spec.ts +155 -0
  38. package/src/elements/adw-application-window.ts +5 -0
  39. package/src/elements/adw-split-button.ts +5 -1
  40. package/src/elements/adw-view-stack.ts +14 -0
  41. package/src/elements/checks.ts +25 -1
  42. package/src/elements/gtk-application-window.ts +5 -0
  43. package/src/elements/gtk-drop-down.ts +16 -0
  44. package/src/elements/gtk-entry.ts +45 -7
  45. package/src/elements/gtk-menu-button.ts +12 -1
  46. package/src/elements/gtk-popover-menu.ts +7 -2
  47. package/src/elements/gtk-scrolled-window.ts +58 -0
  48. package/src/elements/gtk-stack.ts +8 -0
  49. package/src/elements/gtk-switch.ts +8 -0
  50. package/src/glib.spec.ts +34 -0
  51. package/src/gobject-door.spec.ts +119 -0
  52. package/src/gobject-door.ts +137 -0
  53. package/src/gobject-elements.ts +172 -0
  54. package/src/gtksource/gtk-source-view.spec.ts +69 -0
  55. package/src/gtksource/gtk-source-view.ts +379 -0
  56. package/src/gtksource/web-editor-driver.ts +260 -0
  57. package/src/index.ts +10 -0
  58. package/src/menu-actions.spec.ts +47 -0
  59. package/src/namespace/adw.ts +1 -0
  60. package/src/namespace/gio.ts +2 -0
  61. package/src/namespace/glib.ts +22 -0
  62. package/src/namespace/gobject.ts +37 -0
  63. package/src/namespace/gtk-enums.ts +15 -0
  64. package/src/namespace/gtk.ts +2 -0
  65. package/src/namespace/gtksource.ts +9 -0
  66. package/src/shared-tree-builder.ts +142 -7
  67. package/src/split-button.spec.ts +2 -0
  68. package/src/styles.generated.ts +1 -1
  69. package/src/test.browser.mts +10 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/adwaita-web",
3
- "version": "0.56.0",
3
+ "version": "0.57.0",
4
4
  "description": "Adwaita/Libadwaita web components for browser targets",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -38,7 +38,8 @@
38
38
  ],
39
39
  "scripts": {
40
40
  "clear": "gjsify clear lib dist tsconfig.tsbuildinfo scss/_icons.generated.scss src/styles.generated.ts",
41
- "check": "gjsify run build:scss && gjsify tsc --noEmit",
41
+ "check": "gjsify run build:scss && gjsify tsc --noEmit && gjsify run check:consumer",
42
+ "check:consumer": "gjsify tsc -p tsconfig.consumer.json",
42
43
  "build": "gjsify run build:scss && gjsify run build:types",
43
44
  "build:scss": "node scripts/build-scss.mjs",
44
45
  "build:types": "gjsify tsc",
@@ -49,16 +50,17 @@
49
50
  "@codemirror/language": "^6.12.4",
50
51
  "@codemirror/state": "^6.7.0",
51
52
  "@codemirror/view": "^6.43.4",
52
- "@gjsify/adwaita-core": "^0.56.0",
53
- "@gjsify/adwaita-fonts": "^0.56.0",
54
- "@gjsify/adwaita-icons": "^0.56.0",
53
+ "@gjsify/adwaita-core": "^0.57.0",
54
+ "@gjsify/adwaita-fonts": "^0.57.0",
55
+ "@gjsify/adwaita-icons": "^0.57.0",
56
+ "@gjsify/gtksource-core": "^0.57.0",
55
57
  "@lezer/highlight": "^1.2.3"
56
58
  },
57
59
  "devDependencies": {
58
- "@gjsify/blueprint": "^0.56.0",
59
- "@gjsify/cli": "^0.56.0",
60
- "@gjsify/unit": "^0.56.0",
61
- "@gjsify/vite-plugin-blueprint": "^0.56.0",
60
+ "@gjsify/blueprint": "^0.57.0",
61
+ "@gjsify/cli": "^0.57.0",
62
+ "@gjsify/unit": "^0.57.0",
63
+ "@gjsify/vite-plugin-blueprint": "^0.57.0",
62
64
  "sass": "^1.101.0",
63
65
  "typescript": "^6.0.3"
64
66
  },
package/scss/_reset.scss CHANGED
@@ -47,7 +47,7 @@ $adw-components:
47
47
  gtk-gl-area, gtk-graphics-offload, gtk-grid, gtk-grid-view, gtk-header-bar, gtk-image, gtk-inscription, gtk-label, gtk-level-bar,
48
48
  gtk-link-button, gtk-list-box, gtk-list-box-row, gtk-list-view, gtk-media-controls, gtk-menu-button, gtk-overlay, gtk-page-setup-unix-dialog, gtk-paned,
49
49
  gtk-password-entry, gtk-picture, gtk-popover, gtk-popover-bin, gtk-popover-menu, gtk-popover-menu-bar, gtk-print-unix-dialog,
50
- gtk-progress-bar, gtk-revealer, gtk-scale, gtk-scale-button, gtk-scrollbar, gtk-scrolled-window, gtk-search-bar, gtk-search-entry, gtk-separator,
50
+ gtk-progress-bar, gtk-revealer, gtk-scale, gtk-scale-button, gtk-scrollbar, gtk-scrolled-window, gtk-search-bar, gtk-search-entry, gtk-separator, gtk-source-view,
51
51
  gtk-spin-button, gtk-spinner, gtk-stack, gtk-stack-sidebar, gtk-stack-switcher, gtk-switch, gtk-text, gtk-text-view, gtk-toggle-button, gtk-tree-expander, gtk-video, gtk-viewport,
52
52
  gtk-window, gtk-window-controls, gtk-window-handle;
53
53
 
@@ -0,0 +1,75 @@
1
+ // ADR 0098: the web port of `Gtk.Application`, held to the shared vectors (real GJS is their oracle,
2
+ // in adwaita-core's `application.gjs.spec.ts`) through real custom elements.
3
+ import {
4
+ APPLICATION_VECTORS,
5
+ driveApplicationVectors,
6
+ unprovenVfuncs,
7
+ type ApplicationScene,
8
+ type ApplicationSubject,
9
+ } from '@gjsify/adwaita-core/conformance';
10
+ import { describe, expect, it } from '@gjsify/unit';
11
+
12
+ import { activateWidgetAction } from './application.js';
13
+ import { Adw, Gio, GObject, Gtk } from './index.js';
14
+ import type { GtkMenuButton } from './elements/gtk-menu-button.js';
15
+
16
+ const place = <T extends Element>(parent: Element, child: T): T => {
17
+ parent.append(child);
18
+ return child;
19
+ };
20
+
21
+ const made: Element[] = [];
22
+
23
+ const scene: ApplicationScene = {
24
+ window(app) {
25
+ const window = document.createElement('adw-application-window') as Adw.ApplicationWindow;
26
+ window.application = app;
27
+ document.body.append(window);
28
+ made.push(window);
29
+ return window;
30
+ },
31
+ buttonIn: (window) => place(window as Element, document.createElement('gtk-button')),
32
+ popoverButtonIn(window) {
33
+ const menuButton = place(window as Element, document.createElement('gtk-menu-button')) as GtkMenuButton;
34
+ const popover = menuButton.querySelector('gtk-popover') as Element;
35
+ return place(popover, document.createElement('gtk-button'));
36
+ },
37
+ loose: () => document.createElement('gtk-button'),
38
+ activate: (widget, name) => activateWidgetAction(widget as Element, name),
39
+ addWindowAction(window, action) {
40
+ (window as unknown as { add_action(a: object): void }).add_action(action);
41
+ },
42
+ };
43
+
44
+ const subject: ApplicationSubject = {
45
+ name: 'adwaita-web',
46
+ isOracle: false,
47
+ GObject: GObject as unknown as ApplicationSubject['GObject'],
48
+ Application: Gtk.Application as unknown as ApplicationSubject['Application'],
49
+ SimpleAction: Gio.SimpleAction as unknown as ApplicationSubject['SimpleAction'],
50
+ handlesOpen: Gio.ApplicationFlags.HANDLES_OPEN,
51
+ scene,
52
+ };
53
+
54
+ export const ApplicationTest = async () => {
55
+ await driveApplicationVectors(subject, { describe, it, expect });
56
+ // The page is shared with every other suite: a window left in it moves their layout.
57
+ for (const element of made.splice(0)) element.remove();
58
+
59
+ await describe('adwaita-web: Application (ADR 0098 § 3)', async () => {
60
+ await it('has a vector for every unlocked vfunc', () => {
61
+ expect(unprovenVfuncs(APPLICATION_VECTORS).length).toBe(0);
62
+ });
63
+ await it('Adw.Application is a Gtk.Application', () => {
64
+ expect(Adw.Application.prototype instanceof Gtk.Application).toBe(true);
65
+ });
66
+ await it('quit() takes the windows it added off the page', async () => {
67
+ const app = new Adw.Application({ application_id: 'org.gjsify.vec.WebQuit' });
68
+ const window = scene.window(app as never) as Element;
69
+ app.connect('activate', () => app.quit());
70
+ await app.runAsync([]);
71
+ expect(window.isConnected).toBe(false);
72
+ made.length = 0;
73
+ });
74
+ });
75
+ };
@@ -0,0 +1,92 @@
1
+ // `Gio.Application`, `Gtk.Application` and `Adw.Application` on the browser (ADR 0098 §§ 1, 3).
2
+ //
3
+ // The behaviour is the core's (`@gjsify/adwaita-core`); this file is the HOST: when the platform is
4
+ // ready (`DOMContentLoaded`), how a window leaves it, and the walk that resolves `app.` / `win.`
5
+ // from an element. A web page has no process, so `quit()` removes the windows the application
6
+ // added and the tab stays.
7
+
8
+ import {
9
+ GioApplicationBase,
10
+ withGtkApplication,
11
+ activateWidgetAction as activateIn,
12
+ activateMenuItem,
13
+ applicationOfWindow,
14
+ menuActionsFor,
15
+ installActionMap,
16
+ type AdwMenuItem,
17
+ type AdwMenuActions,
18
+ type AdwMenuModel,
19
+ type ApplicationHost,
20
+ } from '@gjsify/adwaita-core';
21
+
22
+ export const webHost: ApplicationHost = {
23
+ name: 'adwaita-web',
24
+ requiresWindow: false,
25
+ start(body) {
26
+ return new Promise<void>((resolve) => {
27
+ const go = (): void => {
28
+ body();
29
+ resolve();
30
+ };
31
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', go, { once: true });
32
+ else go();
33
+ });
34
+ },
35
+ detach(window) {
36
+ (window as Element).remove();
37
+ },
38
+ exit() {},
39
+ };
40
+
41
+ /** An element's parent for action resolution; a popover is rooted at the widget that owns it. */
42
+ export function parentOfElement(node: object): object | null {
43
+ if (!(node instanceof Element)) return null;
44
+ const anchor = (node as { anchor?: unknown }).anchor;
45
+ return anchor instanceof Element ? anchor : node.parentElement;
46
+ }
47
+
48
+ /** What a `Gtk.Actionable` does on activation: the action `prefix.name` found from `widget`. */
49
+ export function activateWidgetAction(widget: Element, fullName: string, parameter: unknown = null): boolean {
50
+ return activateIn(widget, fullName, parentOfElement, parameter);
51
+ }
52
+
53
+ /** What the items of `model` read from the actions registered above `el` (ADR 0097 § 3). */
54
+ export function menuActionsOfElement(el: Element, model: AdwMenuModel): AdwMenuActions {
55
+ return menuActionsFor(model, el, parentOfElement);
56
+ }
57
+
58
+ /** What choosing `item` in a menu owned by `el` does: the action it names fires. */
59
+ export function activateElementMenuItem(el: Element, item: AdwMenuItem): boolean {
60
+ return activateMenuItem(item, el, parentOfElement);
61
+ }
62
+
63
+ export class GioApplication extends GioApplicationBase {
64
+ static override host = webHost;
65
+ }
66
+
67
+ export class GtkApplication extends withGtkApplication(GioApplication) {}
68
+
69
+ export class AdwApplication extends GtkApplication {}
70
+
71
+ type WindowApplication = { add_window(window: object): void; remove_window(window: object): void };
72
+
73
+ /**
74
+ * `Gtk.Window:application` and the window's `Gio.ActionMap` (`win.` actions) on an element class.
75
+ * Setting the property is `add_window`, as on GTK; setting it to `null` is `remove_window`.
76
+ */
77
+ export function defineWindowApplication(proto: object): void {
78
+ installActionMap(proto);
79
+ Object.defineProperty(proto, 'application', {
80
+ configurable: true,
81
+ enumerable: true,
82
+ get(this: object) {
83
+ return applicationOfWindow(this);
84
+ },
85
+ set(this: object, value: WindowApplication | null) {
86
+ const current = applicationOfWindow(this) as unknown as WindowApplication | null;
87
+ if (current === value) return;
88
+ current?.remove_window(this);
89
+ value?.add_window(this);
90
+ },
91
+ });
92
+ }
@@ -138,6 +138,161 @@ export const AdwConstructVectorsTest = async () => {
138
138
  'plain handlers only',
139
139
  );
140
140
  });
141
+ await it('runs a CheckButton `toggled` handler on a click and on a programmatic change', () => {
142
+ let calls = 0;
143
+ const { root, unmount } = mountSharedTree(
144
+ {
145
+ tag: 'GtkBox',
146
+ children: [
147
+ { tag: 'GtkCheckButton', id: 'box', signals: [{ name: 'toggled', handler: 'onToggled' }] },
148
+ ],
149
+ },
150
+ { scope: { onToggled: () => calls++ } },
151
+ );
152
+ try {
153
+ const box = root.querySelector('#box') as unknown as HTMLElement & { active: boolean };
154
+ box.click();
155
+ expect(box.active).toBe(true);
156
+ box.active = false;
157
+ expect(calls).toBe(2);
158
+ } finally {
159
+ unmount();
160
+ }
161
+ });
162
+ await it('makes a bind follow a CheckButton `active`', () => {
163
+ const { root, unmount } = mountSharedTree({
164
+ tag: 'GtkBox',
165
+ children: [
166
+ { tag: 'GtkCheckButton', id: 'box' },
167
+ {
168
+ tag: 'GtkToggleButton',
169
+ id: 'target',
170
+ bindings: { active: { source: 'box', property: 'active' } },
171
+ },
172
+ ],
173
+ });
174
+ try {
175
+ const target = root.querySelector('#target') as unknown as { active: boolean };
176
+ expect(target.active).toBe(false);
177
+ (root.querySelector('#box') as HTMLElement).click();
178
+ expect(target.active).toBe(true);
179
+ } finally {
180
+ unmount();
181
+ }
182
+ });
183
+ await it('makes a bind follow a Switch `active`', () => {
184
+ const { root, unmount } = mountSharedTree({
185
+ tag: 'GtkBox',
186
+ children: [
187
+ { tag: 'GtkSwitch', id: 'source' },
188
+ { tag: 'GtkSwitch', id: 'target', bindings: { active: { source: 'source', property: 'active' } } },
189
+ ],
190
+ });
191
+ try {
192
+ const target = root.querySelector('#target') as unknown as { active: boolean };
193
+ (root.querySelector('#source') as unknown as { active: boolean }).active = true;
194
+ expect(target.active).toBe(true);
195
+ } finally {
196
+ unmount();
197
+ }
198
+ });
199
+ await it('runs an Entry `changed` and `notify::text` handler on typing and on a programmatic set', () => {
200
+ const seen: string[] = [];
201
+ const { root, unmount } = mountSharedTree(
202
+ {
203
+ tag: 'GtkBox',
204
+ children: [
205
+ {
206
+ tag: 'GtkEntry',
207
+ id: 'entry',
208
+ signals: [
209
+ { name: 'changed', handler: 'onChanged' },
210
+ { name: 'notify::text', handler: 'onText' },
211
+ ],
212
+ },
213
+ ],
214
+ },
215
+ { scope: { onChanged: () => seen.push('changed'), onText: () => seen.push('notify::text') } },
216
+ );
217
+ try {
218
+ const entry = root.querySelector('#entry') as unknown as HTMLElement & {
219
+ text: string;
220
+ input: HTMLInputElement;
221
+ };
222
+ entry.input.value = 'typed';
223
+ entry.input.dispatchEvent(new Event('input'));
224
+ entry.text = 'set';
225
+ entry.text = 'set';
226
+ expect(seen.join()).toBe('changed,notify::text,changed,notify::text');
227
+ } finally {
228
+ unmount();
229
+ }
230
+ });
231
+ await it('dispatches ViewStack `notify::visible-child-name` when the visible page changes', () => {
232
+ const stack = document.createElement('adw-view-stack') as HTMLElement & {
233
+ addTitled(c: HTMLElement, name: string, title: string): unknown;
234
+ visibleChildName: string;
235
+ };
236
+ const names: string[] = [];
237
+ stack.addEventListener('notify::visible-child-name', (e) =>
238
+ names.push((e as CustomEvent).detail['visible-child-name']),
239
+ );
240
+ document.body.append(stack);
241
+ try {
242
+ stack.addTitled(document.createElement('div'), 'a', 'A');
243
+ stack.addTitled(document.createElement('div'), 'b', 'B');
244
+ stack.visibleChildName = 'b';
245
+ expect(names.join()).toBe('a,b');
246
+ } finally {
247
+ stack.remove();
248
+ }
249
+ });
250
+ await it('dispatches DropDown `notify::selected-item` after `notify::selected`, on a real change only', () => {
251
+ const dropDown = document.createElement('gtk-drop-down') as HTMLElement & {
252
+ model: string[];
253
+ selected: number;
254
+ };
255
+ const seen: string[] = [];
256
+ for (const name of ['notify::selected', 'notify::selected-item'])
257
+ dropDown.addEventListener(name, () => seen.push(name));
258
+ document.body.append(dropDown);
259
+ try {
260
+ dropDown.model = ['a', 'b'];
261
+ seen.length = 0;
262
+ dropDown.selected = 1;
263
+ dropDown.selected = 1;
264
+ expect(seen.join()).toBe('notify::selected,notify::selected-item');
265
+ } finally {
266
+ dropDown.remove();
267
+ }
268
+ });
269
+ await it('dispatches Stack `notify::visible-child` before `notify::visible-child-name`', () => {
270
+ const stack = document.createElement('gtk-stack') as HTMLElement & { visibleChildName: string | null };
271
+ const a = document.createElement('div');
272
+ a.setAttribute('name', 'a');
273
+ const b = document.createElement('div');
274
+ b.setAttribute('name', 'b');
275
+ stack.append(a, b);
276
+ document.body.append(stack);
277
+ try {
278
+ const seen: string[] = [];
279
+ for (const name of ['notify::visible-child', 'notify::visible-child-name'])
280
+ stack.addEventListener(name, () => seen.push(name));
281
+ stack.visibleChildName = 'b';
282
+ expect(seen.join()).toBe('notify::visible-child,notify::visible-child-name');
283
+ } finally {
284
+ stack.remove();
285
+ }
286
+ });
287
+ await it('declares the notify each of DropDown, Stack and Switch dispatches', () => {
288
+ const declared = (tag: string) =>
289
+ (customElements.get(tag) as unknown as { signals: Record<string, string> }).signals;
290
+ expect(declared('gtk-drop-down')['notify::selected']).toBe('notify::selected');
291
+ expect(declared('gtk-drop-down')['notify::selected-item']).toBe('notify::selected-item');
292
+ expect(declared('gtk-stack')['notify::visible-child']).toBe('notify::visible-child');
293
+ expect(declared('gtk-stack')['notify::visible-child-name']).toBe('notify::visible-child-name');
294
+ expect(declared('gtk-switch')['notify::active']).toBe('notify::active');
295
+ });
141
296
  await it('refuses a bind to an id nothing has, to a source that dispatches no notify, and the flags', () => {
142
297
  const bound = (binding: NonNullable<SharedTreeNode['bindings']>[string], sourceTag = 'GtkToggleButton') =>
143
298
  ({
@@ -41,8 +41,11 @@ import {
41
41
  } from '@gjsify/adwaita-core';
42
42
 
43
43
  import { AdwWindow } from './adw-window.js';
44
+ import { defineWindowApplication } from '../application.js';
44
45
 
45
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;
46
49
  private _menubarEl: HTMLDivElement | null = null;
47
50
  private _model: AdwMenuModel = [];
48
51
 
@@ -152,3 +155,5 @@ export class AdwApplicationWindow extends AdwWindow {
152
155
  }
153
156
 
154
157
  customElements.define('adw-application-window', AdwApplicationWindow);
158
+
159
+ defineWindowApplication(AdwApplicationWindow.prototype);
@@ -57,6 +57,7 @@ import type {
57
57
  SplitButtonChange,
58
58
  SplitButtonDirection,
59
59
  } from '@gjsify/adwaita-core';
60
+ import { activateElementMenuItem, menuActionsOfElement } from '../application.js';
60
61
 
61
62
  import { PopoverMenuView } from './popover-menu.js';
62
63
 
@@ -336,6 +337,7 @@ export class AdwSplitButton extends HTMLElement {
336
337
  this._menuView = new PopoverMenuView(this._menuEl, 'adw-split-button-menu', (path) => {
337
338
  const activated = this._state.activateMenuItem(path);
338
339
  if (activated === null) return;
340
+ activateElementMenuItem(this, activated);
339
341
  this.dispatchEvent(
340
342
  new CustomEvent('menu-activated', {
341
343
  bubbles: true,
@@ -525,7 +527,9 @@ export class AdwSplitButton extends HTMLElement {
525
527
 
526
528
  private _renderMenu(): void {
527
529
  this._menuEl.open = this._state.open;
528
- this._menuView.setMenuModel(this._state.menuModel ?? []);
530
+ const model = this._state.menuModel ?? [];
531
+ this._menuView.setMenuModel(model);
532
+ this._menuView.setActions(this._actions ?? menuActionsOfElement(this, model));
529
533
  this._menuView.render();
530
534
  }
531
535
  }
@@ -50,9 +50,17 @@
50
50
  import { ViewStackState } from '@gjsify/adwaita-core';
51
51
  import type { AdwViewStackPageInfo as CorePageInfo, ViewStackStateChange } from '@gjsify/adwaita-core';
52
52
 
53
+ import type { DispatchedSignals } from '../signals.js';
54
+
53
55
  export type AdwViewStackPageInfo = CorePageInfo<HTMLElement>;
54
56
 
55
57
  export class AdwViewStack extends HTMLElement {
58
+ /** The GTK signals this element dispatches, each with the DOM event it arrives as (ADR 0093). */
59
+ static readonly signals: DispatchedSignals = {
60
+ 'notify::visible-child': 'notify::visible-child',
61
+ 'notify::visible-child-name': 'notify::visible-child-name',
62
+ };
63
+
56
64
  private readonly _state = new ViewStackState<HTMLElement>();
57
65
  private _initialized = false;
58
66
 
@@ -255,6 +263,12 @@ export class AdwViewStack extends HTMLElement {
255
263
  },
256
264
  }),
257
265
  );
266
+ this.dispatchEvent(
267
+ new CustomEvent('notify::visible-child-name', {
268
+ bubbles: true,
269
+ detail: { 'visible-child-name': change.name },
270
+ }),
271
+ );
258
272
  }
259
273
 
260
274
  private _applyVisibility(): void {
@@ -87,6 +87,8 @@
87
87
 
88
88
  import { RadioGroupState, resolveCheckState } from '@gjsify/adwaita-core';
89
89
 
90
+ import type { DispatchedSignals } from '../signals.js';
91
+
90
92
  /**
91
93
  * The document's radio groups. ONE registry, as global as a bare
92
94
  * `<input type="radio" name="x">` outside a form is — two independent groups therefore
@@ -124,6 +126,15 @@ abstract class AdwCheckBase extends HTMLElement {
124
126
  this.toggleAttribute('checked', !!value);
125
127
  }
126
128
 
129
+ /** `Gtk.CheckButton:active`, the GObject name for {@link checked}. */
130
+ get active(): boolean {
131
+ return this.checked;
132
+ }
133
+
134
+ set active(value: boolean) {
135
+ this.checked = value;
136
+ }
137
+
127
138
  /** Whether the control is in the third, inconsistent state. */
128
139
  get indeterminate(): boolean {
129
140
  return this.hasAttribute('indeterminate');
@@ -202,10 +213,17 @@ abstract class AdwCheckBase extends HTMLElement {
202
213
  this._label.textContent = this.getAttribute('label') ?? '';
203
214
  }
204
215
  this.render();
216
+ if (name === 'active') {
217
+ // The GObject spelling a projected `.blp` writes; `checked` stays the state.
218
+ this.checked = this.hasAttribute('active');
219
+ }
205
220
  if (name === 'checked') {
206
221
  this.dispatchEvent(
207
222
  new CustomEvent('notify::checked', { bubbles: true, detail: { checked: this.checked } }),
208
223
  );
224
+ // `toggled` fires on every change of `active`, programmatic included.
225
+ this.dispatchEvent(new CustomEvent('toggled', { bubbles: true }));
226
+ this.dispatchEvent(new CustomEvent('notify::active', { bubbles: true, detail: { active: this.checked } }));
209
227
  }
210
228
  }
211
229
 
@@ -227,8 +245,14 @@ abstract class AdwCheckBase extends HTMLElement {
227
245
 
228
246
  /** The Adwaita checkbox — `checkbutton > check` as a custom element. */
229
247
  export class GtkCheckButton extends AdwCheckBase {
248
+ /** The GTK signals this element dispatches, each with the DOM event it arrives as (ADR 0093). */
249
+ static readonly signals: DispatchedSignals = {
250
+ toggled: 'toggled',
251
+ 'notify::active': 'notify::active',
252
+ };
253
+
230
254
  static get observedAttributes() {
231
- return ['checked', 'indeterminate', 'disabled', 'label'];
255
+ return ['checked', 'active', 'indeterminate', 'disabled', 'label'];
232
256
  }
233
257
 
234
258
  protected get inputType(): 'checkbox' {
@@ -26,8 +26,11 @@
26
26
 
27
27
  import type { AdwSlot } from '../slotted-children.js';
28
28
  import { GtkWindow } from './gtk-window.js';
29
+ import { defineWindowApplication } from '../application.js';
29
30
 
30
31
  export class GtkApplicationWindow extends GtkWindow {
32
+ /** `Gtk.Window:application`: setting it is `add_window` (ADR 0098), set by `defineWindowApplication`. */
33
+ declare application: object | null;
31
34
  private _menubar: HTMLDivElement | null = null;
32
35
  private _menubarObserver: MutationObserver | null = null;
33
36
 
@@ -106,3 +109,5 @@ export class GtkApplicationWindow extends GtkWindow {
106
109
  }
107
110
 
108
111
  customElements.define('gtk-application-window', GtkApplicationWindow);
112
+
113
+ defineWindowApplication(GtkApplicationWindow.prototype);
@@ -47,6 +47,8 @@
47
47
  // Events:
48
48
  // `notify::selected` (CustomEvent, bubbles, detail = { selected }) — the index
49
49
  // changed (mirrors the Gtk.DropDown `notify::selected` GObject signal).
50
+ // `notify::selected-item` (CustomEvent, bubbles, detail = { 'selected-item' }) — fired
51
+ // right after `notify::selected`, on the same real change.
50
52
  // `change` (CustomEvent, bubbles, detail = { index, value, label }) —
51
53
  // a convenience event carrying the full selection. Fires ONLY on a user
52
54
  // pick — `ComboState`'s `interactive` flag is what tells the two apart.
@@ -69,6 +71,7 @@ import { ComboState, normalizeComboOptions, parseListModel } from '@gjsify/adwai
69
71
  import type { AdwComboOption, AdwListItemsChanged, AdwListModelInput } from '@gjsify/adwaita-core';
70
72
 
71
73
  import { bindSlottedChildren } from '../slotted-children.js';
74
+ import type { DispatchedSignals } from '../signals.js';
72
75
  import { stringListSlot } from '../string-list-slot.js';
73
76
 
74
77
  import { createGtkImage } from './gtk-image.js';
@@ -81,6 +84,12 @@ import { createGtkImage } from './gtk-image.js';
81
84
  export type GtkDropDownOption = AdwComboOption;
82
85
 
83
86
  export class GtkDropDown extends HTMLElement {
87
+ /** The GTK signals this element dispatches, each with the DOM event it arrives as (ADR 0093). */
88
+ static readonly signals: DispatchedSignals = {
89
+ 'notify::selected': 'notify::selected',
90
+ 'notify::selected-item': 'notify::selected-item',
91
+ };
92
+
84
93
  private _buttonEl!: HTMLButtonElement;
85
94
  private _labelEl!: HTMLSpanElement;
86
95
  private _popoverEl!: GtkPopover;
@@ -242,6 +251,13 @@ export class GtkDropDown extends HTMLElement {
242
251
  // `notify::selected` mirrors GObject property-notify: EVERY change,
243
252
  // programmatic included.
244
253
  this.dispatchEvent(new CustomEvent('notify::selected', { bubbles: true, detail: { selected: index } }));
254
+ // `gtk_drop_down_set_selected` notifies `selected-item` right after `selected`.
255
+ this.dispatchEvent(
256
+ new CustomEvent('notify::selected-item', {
257
+ bubbles: true,
258
+ detail: { 'selected-item': option ?? null },
259
+ }),
260
+ );
245
261
  // `change` mirrors the DOM <select> contract: ONLY a user-initiated change. A
246
262
  // programmatic assignment stays silent, just as native `select.value = x` fires
247
263
  // no `change`, so consumers can set the value without guarding against it.