@gjsify/adwaita-web 0.51.0 → 0.52.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 (103) hide show
  1. package/dist/adwaita-web.css +255 -27
  2. package/dist/adwaita-web.css.map +1 -1
  3. package/lib/types/blueprint-layout.spec.d.ts +1 -0
  4. package/lib/types/blueprint-markup.spec.d.ts +4 -0
  5. package/lib/types/blueprint-tree.spec.d.ts +1 -0
  6. package/lib/types/elements/adw-bottom-sheet.d.ts +31 -13
  7. package/lib/types/elements/adw-carousel.d.ts +5 -0
  8. package/lib/types/elements/adw-header-bar.d.ts +15 -0
  9. package/lib/types/elements/adw-inline-view-switcher.d.ts +1 -0
  10. package/lib/types/elements/adw-navigation-view.d.ts +1 -0
  11. package/lib/types/elements/adw-sidebar.d.ts +10 -0
  12. package/lib/types/elements/adw-tab-view.d.ts +1 -0
  13. package/lib/types/elements/adw-toggle-group.d.ts +11 -0
  14. package/lib/types/elements/gtk-action-bar.d.ts +19 -0
  15. package/lib/types/elements/gtk-box.d.ts +17 -0
  16. package/lib/types/elements/gtk-button.d.ts +2 -0
  17. package/lib/types/elements/gtk-label.d.ts +122 -0
  18. package/lib/types/gio-menu.spec.d.ts +1 -0
  19. package/lib/types/gtk-box.spec.d.ts +1 -0
  20. package/lib/types/gtk-label.spec.d.ts +1 -0
  21. package/lib/types/gtk-value-doors.spec.d.ts +1 -0
  22. package/lib/types/index.d.ts +4 -1
  23. package/lib/types/namespace/gio.d.ts +1 -0
  24. package/lib/types/namespace/gtk.d.ts +5 -0
  25. package/lib/types/shared-tree-builder.d.ts +64 -0
  26. package/lib/types/source-view/asm6502.d.ts +1 -20
  27. package/lib/types/source-view/index.d.ts +3 -2
  28. package/lib/types/source-view/theme.d.ts +1 -1
  29. package/lib/types/string-list-slot.d.ts +4 -0
  30. package/lib/types/styles.generated.d.ts +1 -1
  31. package/lib/types/tags.spec.d.ts +1 -0
  32. package/lib/types/value-lists.spec.d.ts +1 -0
  33. package/package.json +8 -6
  34. package/scss/_action_bar.scss +51 -0
  35. package/scss/_banner.scss +8 -1
  36. package/scss/_bottom_sheet.scss +57 -0
  37. package/scss/_box.scss +23 -0
  38. package/scss/_button.scss +29 -7
  39. package/scss/_button_content.scss +6 -1
  40. package/scss/_button_row.scss +5 -2
  41. package/scss/_carousel.scss +6 -0
  42. package/scss/_labels.scss +126 -12
  43. package/scss/_navigation_split_view.scss +18 -0
  44. package/scss/_overlay_split_view.scss +5 -0
  45. package/scss/_reset.scss +29 -4
  46. package/scss/_split_button.scss +5 -2
  47. package/scss/_status_page.scss +31 -17
  48. package/scss/_theme.scss +11 -3
  49. package/scss/_toolbar_view.scss +10 -0
  50. package/scss/_variables.scss +1 -0
  51. package/scss/_widget.scss +55 -0
  52. package/scss/_window_title.scss +9 -0
  53. package/scss/adwaita-skin.scss +8 -2
  54. package/src/adw-banner.spec.ts +16 -0
  55. package/src/adw-button-row.spec.ts +8 -0
  56. package/src/adw-header-bar.spec.ts +17 -0
  57. package/src/blueprint-layout.spec.ts +223 -0
  58. package/src/blueprint-markup.spec.ts +155 -0
  59. package/src/blueprint-tree.spec.ts +174 -0
  60. package/src/bottom-sheet.spec.ts +166 -1
  61. package/src/chrome.spec.ts +5 -2
  62. package/src/elements/adw-bottom-sheet.ts +166 -59
  63. package/src/elements/adw-carousel.ts +10 -5
  64. package/src/elements/adw-clamp.ts +12 -0
  65. package/src/elements/adw-combo-row.ts +13 -4
  66. package/src/elements/adw-header-bar.ts +46 -3
  67. package/src/elements/adw-inline-view-switcher.ts +11 -0
  68. package/src/elements/adw-navigation-view.ts +10 -0
  69. package/src/elements/adw-sidebar.ts +22 -0
  70. package/src/elements/adw-spin-row.ts +44 -5
  71. package/src/elements/adw-status-page.ts +12 -6
  72. package/src/elements/adw-tab-view.ts +20 -3
  73. package/src/elements/adw-toggle-group.ts +45 -4
  74. package/src/elements/adw-toolbar-view.ts +9 -0
  75. package/src/elements/gtk-action-bar.ts +90 -0
  76. package/src/elements/gtk-box.ts +87 -0
  77. package/src/elements/gtk-button.ts +60 -13
  78. package/src/elements/gtk-drop-down.ts +10 -1
  79. package/src/elements/gtk-label.ts +415 -0
  80. package/src/gio-menu.spec.ts +125 -0
  81. package/src/globals.d.ts +7 -0
  82. package/src/gtk-box.spec.ts +168 -0
  83. package/src/gtk-button.spec.ts +123 -1
  84. package/src/gtk-label.spec.ts +595 -0
  85. package/src/gtk-value-doors.spec.ts +104 -0
  86. package/src/index.ts +14 -1
  87. package/src/keyboard-operable.spec.ts +2 -1
  88. package/src/namespace/gio.ts +19 -0
  89. package/src/namespace/gtk.ts +14 -0
  90. package/src/shared-tree-builder.ts +268 -0
  91. package/src/shared-trees.spec.ts +296 -32
  92. package/src/source-view/adw-source-view.ts +3 -3
  93. package/src/source-view/asm6502.ts +11 -191
  94. package/src/source-view/index.ts +17 -11
  95. package/src/source-view/theme.ts +16 -16
  96. package/src/split-button.spec.ts +7 -0
  97. package/src/string-list-slot.ts +21 -0
  98. package/src/styles.generated.ts +1 -1
  99. package/src/tags.spec.ts +51 -0
  100. package/src/test.browser.mts +18 -0
  101. package/src/value-lists.spec.ts +93 -0
  102. package/lib/types/source-view/hex.d.ts +0 -21
  103. package/src/source-view/hex.ts +0 -37
@@ -0,0 +1,104 @@
1
+ // `Gtk.StringList` and `Gtk.Adjustment` AT THE ELEMENT, held against the spelling they
2
+ // replace.
3
+ //
4
+ // THE CLAIM UNDER TEST is backward compatibility, and it is the one claim a core-side spec
5
+ // cannot make. `@gjsify/adwaita-core` proves the two classes reduce to the same value as
6
+ // the array and the object literal; what a reader needs to know is that the ELEMENT cannot
7
+ // tell them apart — that `el.model = ['Blue', 'Teal']` and
8
+ // `el.model = new Gtk.StringList({ strings: ['Blue', 'Teal'] })` leave `<adw-combo-row>` in
9
+ // the same state, down to the `<option>` nodes it renders, and likewise for the two
10
+ // adjustment spellings on `<adw-spin-row>`. Four spellings, each asserted against what the
11
+ // element shows.
12
+ //
13
+ // The NativeScript port asserts the same four at its own widget door
14
+ // (`gtk-value-doors.spec.ts` there), so the two renderers cannot part on this without one
15
+ // of them failing.
16
+ //
17
+ // Compared as JSON rather than with `toEqual`: `@gjsify/unit`'s `toEqual` is `==`, so two
18
+ // different objects pass it whatever they hold.
19
+
20
+ import { describe, it, expect } from '@gjsify/unit';
21
+
22
+ import { GtkAdjustment, GtkStringList } from '@gjsify/adwaita-core';
23
+
24
+ import type { AdwComboRow } from './elements/adw-combo-row.js';
25
+ import type { AdwSpinRow } from './elements/adw-spin-row.js';
26
+ import type { GtkDropDown } from './elements/gtk-drop-down.js';
27
+
28
+ const COLOURS = ['Blue', 'Teal', 'Green'];
29
+ const FONT_SIZE = { lower: 0, upper: 100, value: 16, stepIncrement: 1 };
30
+
31
+ function mount<T extends HTMLElement>(tag: string): T {
32
+ const el = document.createElement(tag) as T;
33
+ document.body.appendChild(el);
34
+ return el;
35
+ }
36
+
37
+ /** Everything a `model` write leaves behind, the rendered options included. */
38
+ const modelState = (el: AdwComboRow | GtkDropDown, options: string) =>
39
+ JSON.stringify({ model: el.model, selected: el.selected, value: el.selectedValue, options });
40
+
41
+ /** The option labels the row actually paints, which is what a reader sees. */
42
+ const comboOptions = (el: HTMLElement) =>
43
+ [...el.querySelectorAll('option, .adw-drop-down-item-label')].map((node) => node.textContent).join('|');
44
+
45
+ export const GtkValueDoorsTest = async () => {
46
+ await describe('<adw-combo-row> takes a plain array and a Gtk.StringList alike', async () => {
47
+ await it('leaves the row in the same state, rendered options included', () => {
48
+ const array = mount<AdwComboRow>('adw-combo-row');
49
+ const list = mount<AdwComboRow>('adw-combo-row');
50
+ array.model = COLOURS;
51
+ list.model = new GtkStringList({ strings: COLOURS });
52
+ array.selected = 1;
53
+ list.selected = 1;
54
+
55
+ expect(modelState(list, comboOptions(list))).toBe(modelState(array, comboOptions(array)));
56
+ expect(list.selectedValue).toBe('Teal');
57
+ });
58
+
59
+ await it('follows a list that was appended to before it was assigned', () => {
60
+ const strings = new GtkStringList({ strings: ['Blue'] });
61
+ strings.append('Teal');
62
+ const row = mount<AdwComboRow>('adw-combo-row');
63
+ row.model = strings;
64
+ row.selected = 1;
65
+
66
+ expect(row.selectedValue).toBe('Teal');
67
+ });
68
+ });
69
+
70
+ await describe('<gtk-drop-down> takes a plain array and a Gtk.StringList alike', async () => {
71
+ await it('leaves the drop-down in the same state', () => {
72
+ const array = mount<GtkDropDown>('gtk-drop-down');
73
+ const list = mount<GtkDropDown>('gtk-drop-down');
74
+ array.model = COLOURS;
75
+ list.model = new GtkStringList({ strings: COLOURS });
76
+ array.selected = 2;
77
+ list.selected = 2;
78
+
79
+ expect(modelState(list, comboOptions(list))).toBe(modelState(array, comboOptions(array)));
80
+ expect(list.selectedValue).toBe('Green');
81
+ });
82
+ });
83
+
84
+ await describe('<adw-spin-row> takes a plain object and a Gtk.Adjustment alike', async () => {
85
+ await it('leaves the row in the same state', () => {
86
+ const literal = mount<AdwSpinRow>('adw-spin-row');
87
+ const adjustment = mount<AdwSpinRow>('adw-spin-row');
88
+ literal.adjustment = FONT_SIZE;
89
+ adjustment.adjustment = new GtkAdjustment(FONT_SIZE);
90
+
91
+ expect(JSON.stringify(adjustment.adjustment)).toBe(JSON.stringify(literal.adjustment));
92
+ expect(adjustment.value).toBe(literal.value);
93
+ expect(adjustment.value).toBe(16);
94
+ });
95
+
96
+ await it('steps by the increment the adjustment carries, whichever spelling set it', () => {
97
+ const row = mount<AdwSpinRow>('adw-spin-row');
98
+ row.adjustment = new GtkAdjustment({ ...FONT_SIZE, stepIncrement: 4 });
99
+
100
+ expect(row.adjustment.stepIncrement).toBe(4);
101
+ expect(row.value).toBe(16);
102
+ });
103
+ });
104
+ };
package/src/index.ts CHANGED
@@ -57,6 +57,14 @@ export type { ApplyAccentOptions } from './accent.js';
57
57
  export { AdwScrollShading } from './scroll-shading.js';
58
58
  export type { AdwUndershootEdges } from './scroll-shading.js';
59
59
 
60
+ // The ADR 0051 tree instantiation half — turns an `@gjsify/adwaita-core/conformance`
61
+ // `SharedTreeNode` into real custom elements. Shipped (not test-only) so a consumer
62
+ // replaying an authored tree — a storybook fixture, a devtools probe, the shared-trees
63
+ // spec itself — has a route to it that is not a `.spec.ts` import; see that file's header
64
+ // for why `mountSharedTree` and not `buildSharedTree` alone is the instantiation half.
65
+ export { buildSharedTree, mountSharedTree } from './shared-tree-builder.js';
66
+ export type { MountedSharedTree } from './shared-tree-builder.js';
67
+
60
68
  // The stylesheet compiles a chosen SUBSET of `@gjsify/adwaita-icons` (the whole set is
61
69
  // ~1.07 MB of data-URI), so a name outside it draws the `image-missing` fallback. This is
62
70
  // the way in for an app that needs a glyph this package does not ship — see
@@ -87,6 +95,11 @@ export { isIconAvailable, registerIcon } from './icon-registry.js';
87
95
  export * as Adw from './namespace/adw.js';
88
96
  export * as Gtk from './namespace/gtk.js';
89
97
 
98
+ // The THIRD namespace, and the one that defines no element: the GObject types an author
99
+ // CONSTRUCTS while building a tree. `Gio.Menu` is what makes `menuModel: menu` the same
100
+ // program here as on GJS — `./namespace/gio.ts` says what does not go in it.
101
+ export * as Gio from './namespace/gio.js';
102
+
90
103
  // WHAT DID NOT MOVE INTO THE NAMESPACE, and the rule that decides it. A member exists
91
104
  // for an element whose GIR tag names a real widget; `WEB_ELEMENT_ALIGNMENT` declares the
92
105
  // rest `webOnly`, meaning no widget in the reference vocabulary stands behind it, so
@@ -127,7 +140,7 @@ export { AdwSidebarItem, AdwSidebarSection } from './elements/adw-sidebar.js';
127
140
 
128
141
  // Slot wrappers and a declarative response: on GTK these are `set_content()`,
129
142
  // `set_sheet()` and `add_response()` — calls, not widgets.
130
- export { AdwBottomSheetContent, AdwBottomSheetSheet } from './elements/adw-bottom-sheet.js';
143
+ export { AdwBottomSheetBottomBar, AdwBottomSheetContent, AdwBottomSheetSheet } from './elements/adw-bottom-sheet.js';
131
144
  export { AdwAlertResponse } from './elements/adw-alert-dialog.js';
132
145
 
133
146
  // Supporting types — the option bags, enums and unions the widgets above take and
@@ -621,7 +621,8 @@ export const AdwKeyboardOperableTest = async () => {
621
621
  //
622
622
  // Prose cannot hold that: this line fails the commit that adds it.
623
623
  // `<adw-inline-view-switcher>` has the same gap (status/open-todos.md).
624
- expect([...AdwToggleGroup.observedAttributes]).toStrictEqual(['active', 'flat', 'round']);
624
+ // `active-name` joined the list without touching the axis: it picks a toggle.
625
+ expect([...AdwToggleGroup.observedAttributes]).toStrictEqual(['active', 'active-name', 'flat', 'round']);
625
626
  });
626
627
 
627
628
  await it('adw-toggle has no state a roving walk would have to skip', async () => {
@@ -0,0 +1,19 @@
1
+ // The GIO half of `@gjsify/adwaita-web`'s vocabulary — `Gio.Menu`, `Gio.MenuItem`.
2
+ // `export * as Gio from './namespace/gio.js'` in `src/index.ts` makes it the namespace,
3
+ // the same module shape `./adw.ts` and `./gtk.ts` take and for the same reason (one list
4
+ // carrying the value AND the type meaning — see the header of `./adw.ts`).
5
+ //
6
+ // RE-EXPORTED, NOT DEFINED. This package used to carry its own copy of the classes and
7
+ // said so; they hold no DOM, so they now live in `@gjsify/adwaita-core` beside the
8
+ // portable model they build. This barrel is the port's DOOR onto them, and
9
+ // `gio-menu.spec.ts` drives the whole suite through it.
10
+ //
11
+ // WHY THIS BARREL IS SHORT, AND WILL STAY SHORT. The other two hold ELEMENTS, and
12
+ // `check-vocabulary-alignment.mjs` derives their members from the elements this package
13
+ // defines. GIO defines none: what lands here is the handful of GObject types an Adwaita
14
+ // author CONSTRUCTS while building a tree — today the menu a `menuModel` property takes.
15
+ // `Gio.File`, `Gio.Settings`, `Gio.SimpleAction` and the rest of the library are NOT this
16
+ // package's business; a namespace that grows past what the elements consume is a second,
17
+ // unheld vocabulary.
18
+
19
+ export { GioMenu as Menu, GioMenuItem as MenuItem } from '@gjsify/adwaita-core';
@@ -10,13 +10,27 @@
10
10
  // declare. GTK4 has no radio type: a radio is a GtkCheckButton with its `group` set,
11
11
  // which is what `<adw-radio>`'s own `why` in the ledger says. So the plain form takes the
12
12
  // GIR name and the grouped one keeps its flat export in `src/index.ts`.
13
+ //
14
+ // TWO MEMBERS HERE ARE NOT WIDGETS — `Gtk.Adjustment` and `Gtk.StringList`, the values an
15
+ // `adjustment` and a `model` property take. ADR 0034 § Amendment 19 is what lets a
16
+ // clause-2 namespace carry them, `CONSTRUCTIBLE_VALUES` in `scripts/value-types.mjs` is
17
+ // where each one is declared, and the GIR answers for both in gtk-host's committed
18
+ // `generated/value-types.mts`. They are re-exported from `@gjsify/adwaita-core`, which is
19
+ // where the portable list and adjustment already live: the class IS that value wearing the
20
+ // GIR spelling, so `model: ['a','b']` and `model: new Gtk.StringList({ strings: ['a','b'] })`
21
+ // are the same write and nothing in this package had to learn a second input shape.
13
22
 
23
+ export { GtkAdjustment as Adjustment } from '@gjsify/adwaita-core';
24
+ export { GtkActionBar as ActionBar } from '../elements/gtk-action-bar.js';
25
+ export { GtkBox as Box } from '../elements/gtk-box.js';
14
26
  export { GtkButton as Button } from '../elements/gtk-button.js';
15
27
  export { GtkCheckButton as CheckButton } from '../elements/checks.js';
16
28
  export { GtkDropDown as DropDown } from '../elements/gtk-drop-down.js';
17
29
  export { GtkEntry as Entry } from '../elements/gtk-entry.js';
18
30
  export { GtkImage as Image } from '../elements/gtk-image.js';
31
+ export { GtkLabel as Label } from '../elements/gtk-label.js';
19
32
  export { GtkMenuButton as MenuButton } from '../elements/gtk-menu-button.js';
20
33
  export { GtkPopover as Popover } from '../elements/gtk-popover.js';
21
34
  export { GtkProgressBar as ProgressBar } from '../elements/gtk-progress-bar.js';
35
+ export { GtkStringList as StringList } from '@gjsify/adwaita-core';
22
36
  export { GtkSwitch as Switch } from '../elements/gtk-switch.js';
@@ -0,0 +1,268 @@
1
+ // THE INSTANTIATION HALF OF ADR 0051, SHIPPED — the `adwaita-web` third of what #1726 did
2
+ // for `gtk-host` and #1729 did for `adwaita-nativescript`. Turning an
3
+ // `@gjsify/adwaita-core/conformance` `SharedTreeNode` into real `<adw-*>`/`<gtk-*>` custom
4
+ // elements is this renderer's OWN translation, not test code, and it lived only inside
5
+ // `shared-trees.spec.ts` — unreachable by a storybook fixture, a devtools probe replaying a
6
+ // gallery block, or a second suite, all of which would have had to import a `.spec.ts` file
7
+ // to reach it. `hostTagOf`/`attributeOf` moved to `@gjsify/adwaita-core/tags` for exactly
8
+ // this: a builder shipping FROM a package, not a dev-only driver reading `scripts/`, can
9
+ // depend on the two case rules without depending on `scripts/`, which cannot ship inside an
10
+ // npm package at all.
11
+ //
12
+ // TWO FUNCTIONS, NOT ONE WITH A FLAG — the split neither sibling builder needed.
13
+ // `gtk-host`'s `materialize`/`insert` and NativeScript's `_addChildFromBuilder` each fully
14
+ // REALISE a widget at construction; nothing later changes what it is. A Custom Element is
15
+ // not: most elements this package defines build their internals in `connectedCallback`
16
+ // (`shared-trees.spec.ts`'s own note: "these elements build on connect"), which runs only
17
+ // once the element is CONNECTED to a document. So `buildSharedTree` alone — `createElement`
18
+ // + `setAttribute`, recursing into children — is precisely the builder that ships dead
19
+ // nodes: it stays exported for a caller supplying its own attachment point (its result
20
+ // becomes live the moment ANYTHING connects it, same as a bare `document.createElement`
21
+ // always has), but the complete path most callers want is `mountSharedTree`: build, attach
22
+ // under a fresh host `<div>` in `document.body`, hand back the realised root.
23
+ //
24
+ // `unmount` IS PART OF THE SAME LIFECYCLE, NOT A TEST HOOK. `shared-trees.spec.ts`'s own
25
+ // `finally` block is test POLICY (never leave a red test's tree mounted for the next one to
26
+ // trip over); the ability to detach what was attached is a plain fact about anything with a
27
+ // `connectedCallback`, so `mountSharedTree` hands it back rather than making a caller reach
28
+ // into a host `<div>` it was never given.
29
+ //
30
+ // Placed flat under `src/`, at parity with this package's other single-purpose modules
31
+ // (`accent.ts`, `breakpoints.ts`, `icon-registry.ts`) rather than a new `conformance/` or
32
+ // `builder/` directory for one file: unlike `gtk-host`, this package has no existing
33
+ // `conformance/` home to add to. Exported through the existing barrel, `src/index.ts` — the
34
+ // package's `exports` map ships only `.`, and a new subpath would buy nothing for a module
35
+ // this small, most of which (the `SharedTreeNode` type) is erased at build anyway.
36
+
37
+ import type { SharedTreeNode } from '@gjsify/adwaita-core/conformance';
38
+ import { GTK_WIDGET_MARGIN_CSS, attributeOf, hostTagOf, propertyOf } from '@gjsify/adwaita-core/tags';
39
+
40
+ import { slottedChildrenOf } from './slotted-children.js';
41
+
42
+ /** One authored placement, kept so {@link mountSharedTree} can hold the renderer to it. */
43
+ interface PlacedChild {
44
+ parent: HTMLElement;
45
+ child: HTMLElement;
46
+ slot: string;
47
+ }
48
+
49
+ /** One node that authored `extensions`, kept so {@link mountSharedTree} can hold it too. */
50
+ interface ExtendedNode {
51
+ el: HTMLElement;
52
+ node: SharedTreeNode;
53
+ }
54
+
55
+ /** What one build collects for the checks that can only run once the tree is connected. */
56
+ interface BuildRecord {
57
+ placed: PlacedChild[];
58
+ extended: ExtendedNode[];
59
+ }
60
+
61
+ /**
62
+ * Whether `member` can be assigned on `el`: the nearest descriptor up the prototype chain is
63
+ * a writable data property or an accessor WITH a setter. `in` alone answers true for a
64
+ * getter-only accessor, whose assignment throws in strict code.
65
+ */
66
+ function isWritable(el: object, member: string): boolean {
67
+ for (let at: object | null = el; at !== null; at = Object.getPrototypeOf(at) as object | null) {
68
+ const descriptor = Object.getOwnPropertyDescriptor(at, member);
69
+ if (descriptor === undefined) continue;
70
+ return descriptor.set !== undefined || descriptor.writable === true;
71
+ }
72
+ return false;
73
+ }
74
+
75
+ /**
76
+ * A `SharedTreeNode`, realised as a DETACHED element tree: a tag, its authored properties as
77
+ * attributes, its style classes as classes, its extensions (ADR 0072) as the markup the element
78
+ * reads, its placement as `slot=`, its children, in that order — recursive and total,
79
+ * no tag list, no per-block case. A boolean authored property is the ATTRIBUTE'S PRESENCE
80
+ * (`toggleAttribute`), which is what every element in the corpus reads
81
+ * (`hasAttribute('revealed')`, `hasAttribute('expanded')`); spelling `"true"` would set a
82
+ * present attribute for `false` as well.
83
+ *
84
+ * EXCEPT AN AUTHORED `false` ON A PROPERTY THE ELEMENT DECLARES. Absence cannot say `false`
85
+ * where the GTK default is TRUE — `AdwNavigationPage:can-pop`, `GtkActionBar:revealed` —
86
+ * because those elements read an absent attribute as that default, so `can-pop: false`
87
+ * reached the page as `can-pop` unset and the page stayed poppable. The element's own
88
+ * property setter knows its attribute convention, so an authored `false` is written
89
+ * through it when the element (already upgraded: `createElement` of a defined tag
90
+ * constructs it) declares one; everything else keeps the presence rule. "Declares" means a
91
+ * member it can WRITE ({@link isWritable}): a getter-only accessor of the same name — the
92
+ * split button's and the menu button's read-only `active` — would throw a bare `TypeError`
93
+ * out of the assignment, so such a property falls back to the presence rule too.
94
+ *
95
+ * THE SLOT IS WRITTEN AS THE ATTRIBUTE THIS RENDERER ALREADY ROUTES ON, not translated:
96
+ * `src/slotted-children.ts` reads `slot=` off every light-DOM child and keeps the routing
97
+ * live. This builder read `tag`, `props` and `children` and dropped `slot` silently until a
98
+ * real `.blp` authored one — the `[top]` header bar landed in `adw-toolbar-view-content` and
99
+ * the window title was then discarded by `<adw-header-bar>`'s own build, at exit 0.
100
+ *
101
+ * NOT YET LIVE — see the file header. Nothing here has run `connectedCallback` until
102
+ * something connects it: {@link mountSharedTree} for the common case, or a caller's own
103
+ * container. A DETACHED build therefore cannot check a slot either: an element that has not
104
+ * upgraded has declared no slots yet, so the refusal below belongs to the mount.
105
+ */
106
+ export function buildSharedTree(node: SharedTreeNode, record: BuildRecord = { placed: [], extended: [] }): HTMLElement {
107
+ const el = document.createElement(hostTagOf(node.tag));
108
+ // The id is how the TypeScript beside a `.blp` reaches this element
109
+ // (`root.querySelector('#…')`), the counterpart of `InternalChildren` on GTK.
110
+ if (node.id !== undefined) el.id = node.id;
111
+ for (const [prop, value] of Object.entries(node.props ?? {})) {
112
+ const member = propertyOf(prop);
113
+ if (value === false && isWritable(el, member)) (el as unknown as Record<string, unknown>)[member] = false;
114
+ else if (typeof value === 'boolean') el.toggleAttribute(attributeOf(prop), value);
115
+ else el.setAttribute(attributeOf(prop), String(value));
116
+ // A margin is also inline style (`GTK_WIDGET_MARGIN_CSS` says why); the attribute
117
+ // stays, since it is what the tree authored and what a reader of the DOM looks for.
118
+ const margin = GTK_WIDGET_MARGIN_CSS[attributeOf(prop)];
119
+ if (margin !== undefined) el.style.setProperty(margin, `${Number(value)}px`);
120
+ }
121
+ // `styleClasses` is `GtkWidget:css-classes`, and this renderer's door for it is the
122
+ // `class` attribute — what `.title-1`, `.dimmed` and `.card` select on. Unread, a
123
+ // `.blp`'s `styles ["title-1"]` reached the tree and never the page.
124
+ if (node.styleClasses !== undefined && node.styleClasses.length > 0) el.classList.add(...node.styleClasses);
125
+ writeExtensions(el, node);
126
+ if (node.extensions !== undefined) record.extended.push({ el, node });
127
+ for (const child of node.children ?? []) {
128
+ const childEl = buildSharedTree(child, record);
129
+ if (child.slot !== undefined) {
130
+ childEl.setAttribute('slot', child.slot);
131
+ record.placed.push({ parent: el, child: childEl, slot: child.slot });
132
+ }
133
+ el.append(childEl);
134
+ }
135
+ return el;
136
+ }
137
+
138
+ /**
139
+ * ADR 0072's `extensions`, written in the markup this package already reads for each.
140
+ *
141
+ * `strings` is the `strings` attribute, a JSON array — `Gtk.StringList:strings` is a real
142
+ * property, and JSON is how `model` is written on the two elements that take the list (see
143
+ * `string-list-slot.ts`). `responses` are `<adw-alert-response>` children, the markup
144
+ * spelling of GtkBuilder's `<response>` that `<adw-alert-dialog>` consumes at connect. A
145
+ * translatable string is written as its source text: no renderer here translates, and the
146
+ * marking stays on the tree for whoever extracts it.
147
+ */
148
+ function writeExtensions(el: HTMLElement, node: SharedTreeNode): void {
149
+ const strings = node.extensions?.strings;
150
+ if (strings !== undefined) el.setAttribute('strings', JSON.stringify(strings.map((string) => string.value)));
151
+ for (const response of node.extensions?.responses ?? []) {
152
+ const responseEl = document.createElement('adw-alert-response');
153
+ responseEl.id = response.id;
154
+ if (response.appearance !== undefined) responseEl.setAttribute('appearance', response.appearance);
155
+ if (response.enabled === false) responseEl.setAttribute('enabled', 'false');
156
+ responseEl.textContent = response.label;
157
+ el.append(responseEl);
158
+ }
159
+ }
160
+
161
+ /** The response API an element answers to — `<adw-alert-dialog>`'s, named after libadwaita's. */
162
+ interface ResponseReader {
163
+ getResponseLabel(id: string): string | null;
164
+ getResponseAppearance(id: string): string | null;
165
+ getResponseEnabled(id: string): boolean;
166
+ }
167
+
168
+ /**
169
+ * An extension the realised element did not take is refused, after connect, like a slot.
170
+ *
171
+ * Both doors are markup the ELEMENT consumes, so writing them proves nothing: an element that
172
+ * has no `model` slot, or is not a dialog, leaves the list or the responses where the builder
173
+ * put them, and the widget renders empty at exit 0. So each is read back off the element. A
174
+ * string list must have been consumed (it is data and leaves the tree when taken); every
175
+ * response must be registered with the label, appearance and enabled state the tree authored.
176
+ */
177
+ function refuseUnheldExtensions(extended: readonly ExtendedNode[]): void {
178
+ for (const { el, node } of extended) {
179
+ if (node.extensions?.strings !== undefined && el.isConnected) {
180
+ const parent = el.parentElement?.localName ?? 'nothing';
181
+ throw new Error(
182
+ `<${parent}> did not take the <${el.localName}> authored at "${el.getAttribute('slot') ?? ''}", so ` +
183
+ `its ${node.extensions.strings.length} string(s) reach no list.`,
184
+ );
185
+ }
186
+ const reader = el as unknown as Partial<ResponseReader>;
187
+ for (const response of node.extensions?.responses ?? []) {
188
+ const held =
189
+ typeof reader.getResponseLabel === 'function' &&
190
+ reader.getResponseLabel(response.id) === response.label &&
191
+ reader.getResponseAppearance?.(response.id) === (response.appearance ?? 'default') &&
192
+ reader.getResponseEnabled?.(response.id) === (response.enabled ?? true);
193
+ if (!held) {
194
+ throw new Error(
195
+ `<${el.localName}> did not register the response "${response.id}" as authored, so the ` +
196
+ 'dialog would show without it.',
197
+ );
198
+ }
199
+ }
200
+ }
201
+ }
202
+
203
+ /**
204
+ * A placement the element has no destination for is refused BY NAME, after connect.
205
+ *
206
+ * `bindSlottedChildren` copies the NATIVE assignment algorithm — an unmatched `slot=` name
207
+ * is assigned nowhere and the child visibly stays put — which is right for hand-written
208
+ * markup and is not a report. An authored tree is a claim about where a widget goes, so the
209
+ * builder that realises one has to say when the renderer could not honour it; a widget
210
+ * silently left beside its destination is the defect this whole path was measured on.
211
+ *
212
+ * The element's own `slots` declaration is the answer, never a list kept here: an element
213
+ * that binds a slot enrols itself in this refusal, and one that stops binding drops out of
214
+ * it visibly.
215
+ *
216
+ * AN ELEMENT THIS PACKAGE DOES NOT DEFINE IS NOT REFUSED, and that exemption is narrow on
217
+ * purpose. An undefined element has exactly ONE destination — itself — so a placement
218
+ * cannot land anywhere but where the tree authored it; what such a tree is really missing
219
+ * is the WIDGET, which is a wider gap than a slot and not this refusal's claim to make.
220
+ * (`AdwApplicationWindow` is the live case: a real `.blp` roots at one and this package has
221
+ * no element for it.) A DEFINED element that routes no named slot is refused like any
222
+ * other: it built a structure and chose not to route into it, so a name it does not have
223
+ * would leave the child beside that structure.
224
+ */
225
+ function refuseUnknownSlots(placed: readonly PlacedChild[]): void {
226
+ for (const { parent, child, slot } of placed) {
227
+ if (customElements.get(parent.localName) === undefined) continue;
228
+ const binding = slottedChildrenOf(parent);
229
+ const known = (binding?.slots ?? []).map((declared) => declared.name).filter((name) => name !== undefined);
230
+ if (known.includes(slot)) continue;
231
+ throw new Error(
232
+ `<${parent.localName}> has no slot "${slot}", so the authored <${child.localName}> has nowhere ` +
233
+ `to go. Known slots: ${known.length > 0 ? known.join(', ') : 'none — it routes no named slot'}.`,
234
+ );
235
+ }
236
+ }
237
+
238
+ /** A tree {@link mountSharedTree} built and connected. */
239
+ export interface MountedSharedTree {
240
+ /** The authored root — connected, so every custom element under it has upgraded and run. */
241
+ root: HTMLElement;
242
+ /** Disconnects and discards the mount point. */
243
+ unmount: () => void;
244
+ }
245
+
246
+ /**
247
+ * {@link buildSharedTree}, attached under a fresh host `<div>` in `document.body` so the tree
248
+ * — and every custom element in it — is REAL rather than merely constructed. This is the
249
+ * instantiation half a caller reading the corpus's elements normally wants; a bare
250
+ * `buildSharedTree` is for a caller that already has somewhere of its own to attach it.
251
+ */
252
+ export function mountSharedTree(node: SharedTreeNode): MountedSharedTree {
253
+ const host = document.createElement('div');
254
+ const record: BuildRecord = { placed: [], extended: [] };
255
+ host.append(buildSharedTree(node, record));
256
+ document.body.append(host);
257
+ // After the append, because that is what upgrades the elements and runs the binds the
258
+ // refusal reads; before the return, because a caller handed a tree back has no way left
259
+ // to tell a placement that was honoured from one that was dropped.
260
+ try {
261
+ refuseUnknownSlots(record.placed);
262
+ refuseUnheldExtensions(record.extended);
263
+ } catch (error) {
264
+ host.remove();
265
+ throw error;
266
+ }
267
+ return { root: host.firstElementChild as HTMLElement, unmount: () => host.remove() };
268
+ }