@gjsify/adwaita-web 0.62.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.
Files changed (43) hide show
  1. package/README.md +31 -0
  2. package/dist/adwaita-web.css +4 -0
  3. package/dist/adwaita-web.css.map +1 -1
  4. package/lib/types/canvas2d/canvas2d-bridge.d.ts +26 -5
  5. package/lib/types/elements/adw-application-window.d.ts +26 -7
  6. package/lib/types/elements/adw-bottom-sheet.d.ts +3 -0
  7. package/lib/types/elements/adw-data-grid.d.ts +4 -0
  8. package/lib/types/elements/adw-window-features.d.ts +39 -0
  9. package/lib/types/elements/adw-window.d.ts +11 -24
  10. package/lib/types/elements/gtk-application-window.d.ts +12 -4
  11. package/lib/types/elements/gtk-window.d.ts +44 -0
  12. package/lib/types/gtk-window.spec.d.ts +6 -0
  13. package/lib/types/pango-markup.d.ts +10 -0
  14. package/lib/types/pango-markup.spec.d.ts +1 -0
  15. package/lib/types/source-view/copy-glyph.d.ts +6 -0
  16. package/lib/types/source-view/copy-glyph.spec.d.ts +1 -0
  17. package/lib/types/styles.generated.d.ts +1 -1
  18. package/package.json +13 -12
  19. package/scss/_window.scss +7 -2
  20. package/src/adw-application-window.spec.ts +8 -0
  21. package/src/adw-data-grid.spec.ts +11 -0
  22. package/src/adw-window.spec.ts +8 -0
  23. package/src/application.spec.ts +34 -1
  24. package/src/application.ts +16 -0
  25. package/src/bottom-sheet.spec.ts +9 -0
  26. package/src/canvas2d/canvas2d-bridge.ts +24 -8
  27. package/src/elements/adw-alert-dialog.ts +8 -5
  28. package/src/elements/adw-application-window.ts +90 -20
  29. package/src/elements/adw-banner.ts +14 -9
  30. package/src/elements/adw-bottom-sheet.ts +9 -0
  31. package/src/elements/adw-data-grid.ts +18 -0
  32. package/src/elements/adw-window-features.ts +128 -0
  33. package/src/elements/adw-window.ts +45 -108
  34. package/src/elements/gtk-application-window.ts +17 -6
  35. package/src/elements/gtk-window.ts +170 -18
  36. package/src/gtk-window.spec.ts +133 -0
  37. package/src/pango-markup.spec.ts +257 -0
  38. package/src/pango-markup.ts +266 -0
  39. package/src/source-view/adw-source-view.ts +2 -8
  40. package/src/source-view/copy-glyph.spec.ts +46 -0
  41. package/src/source-view/copy-glyph.ts +52 -0
  42. package/src/styles.generated.ts +1 -1
  43. package/src/test.browser.mts +4 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/adwaita-web",
3
- "version": "0.62.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.62.0",
62
- "@gjsify/adwaita-fonts": "^0.62.0",
63
- "@gjsify/adwaita-icons": "^0.62.0",
64
- "@gjsify/gtksource-core": "^0.62.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.62.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.62.0",
77
- "@gjsify/blueprint": "^0.62.0",
78
- "@gjsify/cli": "^0.62.0",
79
- "@gjsify/unit": "^0.62.0",
80
- "@gjsify/vite-plugin-blueprint": "^0.62.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;
@@ -63,6 +63,39 @@ export const ApplicationTest = async () => {
63
63
  await it('Adw.Application is a Gtk.Application', () => {
64
64
  expect(Adw.Application.prototype instanceof Gtk.Application).toBe(true);
65
65
  });
66
+ await it('a keydown in the window runs the accelerator, with its target', () => {
67
+ const app = new Adw.Application({ application_id: 'org.gjsify.vec.WebAccel' });
68
+ const window = scene.window(app as never) as Element;
69
+ const hits: unknown[] = [];
70
+ for (const [owner, name] of [
71
+ [app, 'save'],
72
+ [window, 'open'],
73
+ [app, 'view'],
74
+ ] as const) {
75
+ const action = new Gio.SimpleAction({ name });
76
+ action.connect('activate', (_a: unknown, parameter: unknown) => hits.push([name, parameter ?? null]));
77
+ (owner as unknown as { add_action(a: object): void }).add_action(action);
78
+ }
79
+ app.set_accels_for_action('app.save', ['<Primary>s']);
80
+ app.set_accels_for_action('win.open', ['<Primary><Shift>o']);
81
+ app.set_accels_for_action('app.view::list', ['<Primary>l']);
82
+ const press = (init: KeyboardEventInit): boolean => {
83
+ const event = new KeyboardEvent('keydown', { bubbles: true, cancelable: true, ...init });
84
+ window.dispatchEvent(event);
85
+ return event.defaultPrevented;
86
+ };
87
+ expect(press({ key: 's', ctrlKey: true })).toBe(true);
88
+ expect(press({ key: 'O', ctrlKey: true, shiftKey: true })).toBe(true);
89
+ expect(press({ key: 'l', ctrlKey: true })).toBe(true);
90
+ expect(press({ key: 's' })).toBe(false);
91
+ expect(hits).toStrictEqual([
92
+ ['save', null],
93
+ ['open', null],
94
+ ['view', 'list'],
95
+ ]);
96
+ (window as unknown as { application: object | null }).application = null;
97
+ expect(press({ key: 's', ctrlKey: true })).toBe(false);
98
+ });
66
99
  await it('quit() takes the windows it added off the page', async () => {
67
100
  const app = new Adw.Application({ application_id: 'org.gjsify.vec.WebQuit' });
68
101
  const window = scene.window(app as never) as Element;
@@ -11,6 +11,7 @@ import {
11
11
  activateWidgetAction as activateIn,
12
12
  activateMenuItem,
13
13
  applicationOfWindow,
14
+ dispatchAccelerator,
14
15
  menuActionsFor,
15
16
  installActionMap,
16
17
  type AdwMenuItem,
@@ -18,6 +19,7 @@ import {
18
19
  type AdwMenuModel,
19
20
  type ApplicationHost,
20
21
  } from '@gjsify/adwaita-core';
22
+ import { webPlatformHost } from './platform-host.js';
21
23
 
22
24
  export const webHost: ApplicationHost = {
23
25
  name: 'adwaita-web',
@@ -70,6 +72,19 @@ export class AdwApplication extends GtkApplication {}
70
72
 
71
73
  type WindowApplication = { add_window(window: object): void; remove_window(window: object): void };
72
74
 
75
+ const ACCEL_LISTENERS = new WeakMap<object, () => void>();
76
+
77
+ /** Key presses in a window of an application run its accelerators; keydown of descendants bubbles here. */
78
+ function followAccelerators(window: object, application: WindowApplication | null): void {
79
+ ACCEL_LISTENERS.get(window)?.();
80
+ ACCEL_LISTENERS.delete(window);
81
+ if (application === null) return;
82
+ ACCEL_LISTENERS.set(
83
+ window,
84
+ webPlatformHost.listenKeys(window, (event) => dispatchAccelerator(window, event, parentOfElement)),
85
+ );
86
+ }
87
+
73
88
  /**
74
89
  * `Gtk.Window:application` and the window's `Gio.ActionMap` (`win.` actions) on an element class.
75
90
  * Setting the property is `add_window`, as on GTK; setting it to `null` is `remove_window`.
@@ -87,6 +102,7 @@ export function defineWindowApplication(proto: object): void {
87
102
  if (current === value) return;
88
103
  current?.remove_window(this);
89
104
  value?.add_window(this);
105
+ followAccelerators(this, value);
90
106
  },
91
107
  });
92
108
  }
@@ -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
 
@@ -25,9 +25,11 @@
25
25
  // `Adw.AlertDialog:heading-use-markup` and `:body-use-markup` both default to FALSE
26
26
  // (`Adw.Banner:use-markup` is the one that defaults to TRUE, which is why
27
27
  // `adw-banner.ts` documents a deliberate departure and this file does not need to).
28
- // Opting in renders through `innerHTML`, which is the author asserting the string is
29
- // trusted — Pango markup has a fixed tag set and no scripting, HTML has neither
30
- // property.
28
+ // Opting in renders PANGO MARKUP through `renderPangoMarkup`: the string is PARSED by
29
+ // `@gjsify/adwaita-core` and turned into nodes one at a time, never handed to
30
+ // `innerHTML`. So opting in is no longer an assertion that the string is trusted —
31
+ // `<b>` is drawn bold whether or not the author vouched for it, and a tag pango does
32
+ // not know comes back as text.
31
33
  //
32
34
  // Buttons are stacked vertically by default (the Adw.AlertDialog default at medium
33
35
  // sizes) and laid out horizontally when prefer-wide-layout is set and there are no more
@@ -56,6 +58,7 @@
56
58
  import { AdwAlertResponses } from '@gjsify/adwaita-core';
57
59
  import type { AdwResponseAppearance } from '@gjsify/adwaita-core';
58
60
 
61
+ import { renderPangoMarkup } from '../pango-markup.js';
59
62
  import { bindEmptySections } from '../empty-sections.js';
60
63
  import { bindSlottedChildren } from '../slotted-children.js';
61
64
  import { AdwModalSurface } from './modal-surface.js';
@@ -463,12 +466,12 @@ export class AdwAlertDialog extends HTMLElement {
463
466
  this.classList.toggle('open', this.open);
464
467
 
465
468
  const heading = this.heading;
466
- if (this.headingUseMarkup) this._headingEl.innerHTML = heading;
469
+ if (this.headingUseMarkup) renderPangoMarkup(this._headingEl, heading);
467
470
  else this._headingEl.textContent = heading;
468
471
  this._headingEl.hidden = heading.length === 0;
469
472
 
470
473
  const body = this.body;
471
- if (this.bodyUseMarkup) this._bodyEl.innerHTML = body;
474
+ if (this.bodyUseMarkup) renderPangoMarkup(this._bodyEl, body);
472
475
  else this._bodyEl.textContent = body;
473
476
  this._bodyEl.hidden = body.length === 0;
474
477
 
@@ -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
 
@@ -20,6 +20,8 @@ import {
20
20
  parseBannerButtonStyle,
21
21
  } from '@gjsify/adwaita-core';
22
22
 
23
+ import { renderPangoMarkup } from '../pango-markup.js';
24
+
23
25
  export class AdwBanner extends HTMLElement {
24
26
  private _titleEl!: HTMLSpanElement;
25
27
  private _button!: HTMLButtonElement;
@@ -52,17 +54,20 @@ export class AdwBanner extends HTMLElement {
52
54
 
53
55
  private _render() {
54
56
  const title = this.getAttribute('title') ?? '';
55
- // DELIBERATE DEPARTURE FROM THE C DEFAULT. `Adw.Banner:use-markup`
56
- // defaults to TRUE and `@gjsify/adwaita-core`
57
- // carries that as the spec value — but Pango markup is not HTML. Pango
58
- // has a fixed tag set and no scripting; `innerHTML` will run an
59
- // `onerror` handler. Adopting the C default here would turn every
60
- // `title` attribute into an injection sink for a widget that is not one
61
- // in GTK, so on this renderer markup is OPT-IN and the default is text.
62
- // The opt-in is the developer asserting the title is trusted.
57
+ // DELIBERATE DEPARTURE FROM THE C DEFAULT, and the departure is now about
58
+ // SAFETY rather than about how markup is spelt. `Adw.Banner:use-markup`
59
+ // defaults to TRUE and `@gjsify/adwaita-core` carries that as the spec
60
+ // value — but a `title` attribute is an ATTRIBUTE, and a renderer that
61
+ // parsed it as markup would be one an author (or an injected value) could
62
+ // drive. Adopting the C default here would make every banner title markup
63
+ // by default for a widget that is not one in GTK, so on this renderer
64
+ // markup is OPT-IN and the default is text.
65
+ // Opting in renders PANGO markup — parsed by `renderPangoMarkup`, never
66
+ // handed to `innerHTML`, so nothing the title carries can become a node
67
+ // beyond the eleven tags pango itself knows.
63
68
  // The title label carries `use-underline=False` in the template
64
69
  //, so its underscores are literal — no stripping here.
65
- if (this.hasAttribute('use-markup')) this._titleEl.innerHTML = title;
70
+ if (this.hasAttribute('use-markup')) renderPangoMarkup(this._titleEl, title);
66
71
  else this._titleEl.textContent = title;
67
72
 
68
73
  // `label && label[0]` — a first-character test, so a label of
@@ -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') ?? '';