@gjsify/adwaita-web 0.51.1 → 0.53.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 (113) hide show
  1. package/README.md +34 -0
  2. package/dist/adwaita-web.css +259 -31
  3. package/dist/adwaita-web.css.map +1 -1
  4. package/lib/types/accent.d.ts +12 -2
  5. package/lib/types/adw-appearance.spec.d.ts +2 -0
  6. package/lib/types/appearance.d.ts +41 -0
  7. package/lib/types/blueprint-layout.spec.d.ts +1 -0
  8. package/lib/types/blueprint-markup.spec.d.ts +4 -0
  9. package/lib/types/blueprint-tree.spec.d.ts +1 -0
  10. package/lib/types/elements/adw-bottom-sheet.d.ts +31 -13
  11. package/lib/types/elements/adw-carousel.d.ts +5 -0
  12. package/lib/types/elements/adw-header-bar.d.ts +15 -0
  13. package/lib/types/elements/adw-inline-view-switcher.d.ts +1 -0
  14. package/lib/types/elements/adw-navigation-view.d.ts +1 -0
  15. package/lib/types/elements/adw-sidebar.d.ts +10 -0
  16. package/lib/types/elements/adw-tab-view.d.ts +1 -0
  17. package/lib/types/elements/adw-toggle-group.d.ts +11 -0
  18. package/lib/types/elements/gtk-action-bar.d.ts +19 -0
  19. package/lib/types/elements/gtk-box.d.ts +17 -0
  20. package/lib/types/elements/gtk-button.d.ts +2 -0
  21. package/lib/types/elements/gtk-label.d.ts +122 -0
  22. package/lib/types/font-stack.spec.d.ts +2 -0
  23. package/lib/types/gio-menu.spec.d.ts +1 -0
  24. package/lib/types/gtk-box.spec.d.ts +1 -0
  25. package/lib/types/gtk-label.spec.d.ts +1 -0
  26. package/lib/types/gtk-value-doors.spec.d.ts +1 -0
  27. package/lib/types/index.d.ts +6 -1
  28. package/lib/types/namespace/gio.d.ts +1 -0
  29. package/lib/types/namespace/gtk.d.ts +5 -0
  30. package/lib/types/shared-tree-builder.d.ts +64 -0
  31. package/lib/types/source-view/asm6502.d.ts +1 -20
  32. package/lib/types/source-view/index.d.ts +3 -2
  33. package/lib/types/source-view/theme.d.ts +1 -1
  34. package/lib/types/string-list-slot.d.ts +4 -0
  35. package/lib/types/styles.generated.d.ts +1 -1
  36. package/lib/types/tags.spec.d.ts +1 -0
  37. package/lib/types/value-lists.spec.d.ts +1 -0
  38. package/package.json +8 -6
  39. package/scss/_action_bar.scss +51 -0
  40. package/scss/_banner.scss +8 -1
  41. package/scss/_bottom_sheet.scss +57 -0
  42. package/scss/_box.scss +23 -0
  43. package/scss/_button.scss +29 -7
  44. package/scss/_button_content.scss +6 -1
  45. package/scss/_button_row.scss +5 -2
  46. package/scss/_carousel.scss +6 -0
  47. package/scss/_labels.scss +126 -12
  48. package/scss/_navigation_split_view.scss +18 -0
  49. package/scss/_overlay_split_view.scss +5 -0
  50. package/scss/_reset.scss +29 -4
  51. package/scss/_split_button.scss +5 -2
  52. package/scss/_status_page.scss +31 -17
  53. package/scss/_theme.scss +11 -3
  54. package/scss/_toolbar_view.scss +10 -0
  55. package/scss/_variables.scss +23 -3
  56. package/scss/_widget.scss +55 -0
  57. package/scss/_window_title.scss +9 -0
  58. package/scss/adwaita-skin.scss +8 -2
  59. package/src/accent.ts +28 -5
  60. package/src/adw-appearance.spec.ts +178 -0
  61. package/src/adw-banner.spec.ts +16 -0
  62. package/src/adw-button-row.spec.ts +8 -0
  63. package/src/adw-header-bar.spec.ts +17 -0
  64. package/src/appearance.ts +207 -0
  65. package/src/blueprint-layout.spec.ts +223 -0
  66. package/src/blueprint-markup.spec.ts +155 -0
  67. package/src/blueprint-tree.spec.ts +174 -0
  68. package/src/bottom-sheet.spec.ts +166 -1
  69. package/src/chrome.spec.ts +5 -2
  70. package/src/elements/adw-bottom-sheet.ts +166 -59
  71. package/src/elements/adw-carousel.ts +10 -5
  72. package/src/elements/adw-clamp.ts +12 -0
  73. package/src/elements/adw-combo-row.ts +13 -4
  74. package/src/elements/adw-header-bar.ts +46 -3
  75. package/src/elements/adw-inline-view-switcher.ts +11 -0
  76. package/src/elements/adw-navigation-view.ts +10 -0
  77. package/src/elements/adw-sidebar.ts +22 -0
  78. package/src/elements/adw-spin-row.ts +44 -5
  79. package/src/elements/adw-status-page.ts +12 -6
  80. package/src/elements/adw-tab-view.ts +20 -3
  81. package/src/elements/adw-toggle-group.ts +45 -4
  82. package/src/elements/adw-toolbar-view.ts +9 -0
  83. package/src/elements/gtk-action-bar.ts +90 -0
  84. package/src/elements/gtk-box.ts +87 -0
  85. package/src/elements/gtk-button.ts +60 -13
  86. package/src/elements/gtk-drop-down.ts +10 -1
  87. package/src/elements/gtk-label.ts +415 -0
  88. package/src/elements/swipe-drag.ts +1 -1
  89. package/src/font-stack.spec.ts +110 -0
  90. package/src/gio-menu.spec.ts +125 -0
  91. package/src/globals.d.ts +7 -0
  92. package/src/gtk-box.spec.ts +168 -0
  93. package/src/gtk-button.spec.ts +123 -1
  94. package/src/gtk-label.spec.ts +595 -0
  95. package/src/gtk-value-doors.spec.ts +104 -0
  96. package/src/index.ts +32 -1
  97. package/src/keyboard-operable.spec.ts +5 -4
  98. package/src/namespace/gio.ts +19 -0
  99. package/src/namespace/gtk.ts +14 -0
  100. package/src/shared-tree-builder.ts +268 -0
  101. package/src/shared-trees.spec.ts +296 -32
  102. package/src/source-view/adw-source-view.ts +3 -3
  103. package/src/source-view/asm6502.ts +11 -191
  104. package/src/source-view/index.ts +17 -11
  105. package/src/source-view/theme.ts +23 -17
  106. package/src/split-button.spec.ts +7 -0
  107. package/src/string-list-slot.ts +21 -0
  108. package/src/styles.generated.ts +1 -1
  109. package/src/tags.spec.ts +51 -0
  110. package/src/test.browser.mts +25 -0
  111. package/src/value-lists.spec.ts +93 -0
  112. package/lib/types/source-view/hex.d.ts +0 -21
  113. package/src/source-view/hex.ts +0 -37
@@ -15,19 +15,31 @@
15
15
  // attribute to another truthy spelling notifies nobody.
16
16
  // - the drag handle is decorative, not a close button — see below.
17
17
  //
18
- // Children are declared as <adw-bottom-sheet-content> (the persistent content)
19
- // and <adw-bottom-sheet-sheet> (the slide-up sheet); both are consumed at
20
- // connect time. Unslotted children fall back to the content, mirroring the
21
- // AdwBottomSheet GtkBuildable default (omitting the child type sets content).
18
+ // Children are declared as <adw-bottom-sheet-content> (the persistent content),
19
+ // <adw-bottom-sheet-sheet> (the slide-up sheet) and <adw-bottom-sheet-bottom-bar>
20
+ // (the collapsed bar shown in its place); all three are consumed at connect time.
21
+ // Unslotted children fall back to the content, mirroring the AdwBottomSheet
22
+ // GtkBuildable default (omitting the child type sets content).
23
+ //
24
+ // THE BOTTOM BAR IS THE AFFORDANCE. It is the only thing libadwaita lets a user
25
+ // click to OPEN a sheet, so an element without one can be opened by its owner and
26
+ // by nobody else. It is a real <button>, because the bin upstream is a GtkButton
27
+ // (adw-bottom-sheet.c:1203) — that is where the keyboard activation comes from.
22
28
  //
23
29
  // Attributes:
24
- // open (boolean — whether the sheet is revealed; default closed)
25
- // modal (boolean — dim + block the content while open; default on)
26
- // can-close (boolean — user can dismiss via the dimming / Esc / the
27
- // sheet.close action; default on. When off, a dismissal
28
- // raises `close-attempt` instead of closing — mirrors
29
- // Adw.BottomSheet:can-close)
30
- // show-drag-handle (boolean — overlay the drag handle on the sheet; default on)
30
+ // open (boolean — whether the sheet is revealed; default closed)
31
+ // modal (boolean — dim + block the content while open; default on)
32
+ // can-close (boolean — user can dismiss via the dimming / Esc / the
33
+ // sheet.close action; default on. When off, a dismissal
34
+ // raises `close-attempt` instead of closing — mirrors
35
+ // Adw.BottomSheet:can-close)
36
+ // can-open (boolean — user can open the sheet from the bottom bar;
37
+ // default on. When off the bar stays on screen and gains
38
+ // the `inert` class, because libadwaita refuses the click
39
+ // without disabling the button — Adw.BottomSheet:can-open)
40
+ // reveal-bottom-bar (boolean — whether the bottom bar is shown at all while the
41
+ // sheet is closed; default on)
42
+ // show-drag-handle (boolean — overlay the drag handle on the sheet; default on)
31
43
  //
32
44
  // Events:
33
45
  // `notify::open` (CustomEvent, bubbles, `detail = { open }`) when the revealed
@@ -44,7 +56,14 @@
44
56
  // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
45
57
 
46
58
  import { BottomSheetPresentation } from '@gjsify/adwaita-core';
47
- import type { BottomSheetCloseOutcome, BottomSheetCloseSource } from '@gjsify/adwaita-core';
59
+ import type {
60
+ BottomSheetCloseOutcome,
61
+ BottomSheetCloseSource,
62
+ BottomSheetOpenOutcome,
63
+ BottomSheetOpenSource,
64
+ } from '@gjsify/adwaita-core';
65
+
66
+ import { bindSlottedChildren, type AdwSlot } from '../slotted-children.js';
48
67
 
49
68
  /** The persistent content. Child of <adw-bottom-sheet>; consumed at connect time. */
50
69
  export class AdwBottomSheetContent extends HTMLElement {}
@@ -52,19 +71,26 @@ export class AdwBottomSheetContent extends HTMLElement {}
52
71
  /** The slide-up sheet. Child of <adw-bottom-sheet>; consumed at connect time. */
53
72
  export class AdwBottomSheetSheet extends HTMLElement {}
54
73
 
74
+ /** The collapsed bar the sheet morphs out of. Child of <adw-bottom-sheet>. */
75
+ export class AdwBottomSheetBottomBar extends HTMLElement {}
76
+
55
77
  export class AdwBottomSheet extends HTMLElement {
56
78
  private _initialized = false;
57
79
  private _contentEl!: HTMLDivElement;
58
80
  private _dimmingEl!: HTMLDivElement;
59
81
  private _sheetEl!: HTMLDivElement;
82
+ private _sheetBodyEl!: HTMLDivElement;
83
+ private _bottomBarEl!: HTMLButtonElement;
60
84
  private _dragHandleEl!: HTMLDivElement;
85
+ /** A bar set through {@link setBottomBar} before the element upgraded. */
86
+ private _pendingBottomBar: Node | null = null;
61
87
  /** The shared open/can-close model — the single source of truth for both. */
62
88
  private readonly _state = new BottomSheetPresentation();
63
89
  /** Set while the element writes `open` back, so the write does not re-enter. */
64
90
  private _reflecting = false;
65
91
 
66
92
  static get observedAttributes() {
67
- return ['open', 'modal', 'can-close', 'show-drag-handle'];
93
+ return ['open', 'modal', 'can-close', 'can-open', 'reveal-bottom-bar', 'show-drag-handle'];
68
94
  }
69
95
 
70
96
  get open(): boolean {
@@ -75,8 +101,9 @@ export class AdwBottomSheet extends HTMLElement {
75
101
  this._setOpen(!!value);
76
102
  }
77
103
 
104
+ /** Whether the content is dimmed and blocked while the sheet is open (`AdwBottomSheet:modal`). */
78
105
  get modal(): boolean {
79
- return this._boolAttr('modal', true);
106
+ return this._state.modal;
80
107
  }
81
108
 
82
109
  set modal(value: boolean) {
@@ -92,6 +119,24 @@ export class AdwBottomSheet extends HTMLElement {
92
119
  this._setBoolAttr('can-close', value);
93
120
  }
94
121
 
122
+ /** Whether the user can open the sheet from its bottom bar (`Adw.BottomSheet:can-open`). */
123
+ get canOpen(): boolean {
124
+ return this._state.canOpen;
125
+ }
126
+
127
+ set canOpen(value: boolean) {
128
+ this._setBoolAttr('can-open', value);
129
+ }
130
+
131
+ /** Whether the bottom bar is shown while the sheet is closed. */
132
+ get revealBottomBar(): boolean {
133
+ return this._state.revealBottomBar;
134
+ }
135
+
136
+ set revealBottomBar(value: boolean) {
137
+ this._setBoolAttr('reveal-bottom-bar', value);
138
+ }
139
+
95
140
  get showDragHandle(): boolean {
96
141
  return this._boolAttr('show-drag-handle', true);
97
142
  }
@@ -100,6 +145,38 @@ export class AdwBottomSheet extends HTMLElement {
100
145
  this._setBoolAttr('show-drag-handle', value);
101
146
  }
102
147
 
148
+ /** The bar's content, or `null` when the sheet has none — the read-back for {@link setBottomBar}. */
149
+ get bottomBar(): Node | null {
150
+ return this._initialized ? this._bottomBarEl.firstChild : this._pendingBottomBar;
151
+ }
152
+
153
+ /**
154
+ * Give the sheet a bottom bar, or take it away — `adw_bottom_sheet_set_bottom_bar`.
155
+ *
156
+ * Presence is an INPUT to the open gate, not decoration: removing the bar removes every
157
+ * way a user has of opening this sheet, which is why the state hears about it rather
158
+ * than only the DOM.
159
+ */
160
+ setBottomBar(child: Node | null): void {
161
+ this._state.setHasBottomBar(child !== null);
162
+ if (!this._initialized) {
163
+ this._pendingBottomBar = child;
164
+ return;
165
+ }
166
+ if (child) this._bottomBarEl.replaceChildren(child);
167
+ else this._bottomBarEl.replaceChildren();
168
+ this._render();
169
+ }
170
+
171
+ /**
172
+ * Route an open affordance through the shared gate. `'ignored'` is silent by design:
173
+ * libadwaita has no `open-attempt` counterpart to `close-attempt`, so a refused open
174
+ * emits nothing at all.
175
+ */
176
+ requestOpen(source: BottomSheetOpenSource): BottomSheetOpenOutcome {
177
+ return this._state.requestOpen(source);
178
+ }
179
+
103
180
  /**
104
181
  * Route a dismissal affordance through the shared gate and act on the verdict: close,
105
182
  * raise `close-attempt`, forward `sheet.close` to an ancestor, or do nothing. Called
@@ -123,17 +200,8 @@ export class AdwBottomSheet extends HTMLElement {
123
200
  if (this._initialized) return;
124
201
  this._initialized = true;
125
202
 
126
- // The sheet and content are placed by slot; anything unslotted falls back to the
127
- // content (the AdwBottomSheet GtkBuildable default).
128
- const claimed = new Set<Node>();
129
- const sheetChildren = this._collectSlot('adw-bottom-sheet-sheet', 'sheet', claimed);
130
- const contentChildren = this._collectSlot('adw-bottom-sheet-content', 'content', claimed);
131
- const unslotted = Array.from(this.childNodes).filter((n) => !claimed.has(n));
132
-
133
203
  this._contentEl = document.createElement('div');
134
204
  this._contentEl.className = 'adw-bottom-sheet-content';
135
- for (const child of contentChildren) this._contentEl.appendChild(child);
136
- for (const child of unslotted) this._contentEl.appendChild(child);
137
205
 
138
206
  this._dimmingEl = document.createElement('div');
139
207
  this._dimmingEl.className = 'adw-bottom-sheet-dimming';
@@ -151,13 +219,57 @@ export class AdwBottomSheet extends HTMLElement {
151
219
  this._dragHandleEl.className = 'adw-bottom-sheet-drag-handle';
152
220
  this._dragHandleEl.setAttribute('aria-hidden', 'true');
153
221
 
154
- const sheetBody = document.createElement('div');
155
- sheetBody.className = 'adw-bottom-sheet-sheet-body';
156
- for (const child of sheetChildren) sheetBody.appendChild(child);
157
-
158
- this._sheetEl.append(this._dragHandleEl, sheetBody);
159
-
160
- this.replaceChildren(this._contentEl, this._dimmingEl, this._sheetEl);
222
+ this._sheetBodyEl = document.createElement('div');
223
+ this._sheetBodyEl.className = 'adw-bottom-sheet-sheet-body';
224
+
225
+ // A real <button>: the bin upstream is `gtk_button_new ()`
226
+ // (adw-bottom-sheet.c:1203), which is where its keyboard activation comes from
227
+ // (`bottom_bar_clicked_cb`, :402-407) — a div with a click handler would have the
228
+ // pointer door and not that one. It is the sheet bin's OTHER stack child, so it
229
+ // lives inside the sheet element and the two swap rather than stacking.
230
+ this._bottomBarEl = document.createElement('button');
231
+ this._bottomBarEl.type = 'button';
232
+ this._bottomBarEl.className = 'adw-bottom-sheet-bottom-bar';
233
+ this._bottomBarEl.addEventListener('click', () => this.requestOpen('bottom-bar'));
234
+
235
+ this._sheetEl.append(this._bottomBarEl, this._dragHandleEl, this._sheetBodyEl);
236
+
237
+ // The three layers are SLOTS, bound live (`slotted-children.ts`), under the names
238
+ // GtkBuilder gives the properties — `content`, `sheet`, `bottom-bar` — so a projected
239
+ // `.blp` places its children here, and the mount's refusal of an unknown slot can
240
+ // ask this element which names it has. The wrapper elements are the markup spelling
241
+ // of the same three: each is consumed and its children land in the layer. Anything
242
+ // unslotted is content, the AdwBottomSheet GtkBuildable default.
243
+ const layer = (tag: string, into: HTMLElement): AdwSlot => ({
244
+ claims: (node) => node instanceof Element && node.localName === tag,
245
+ consume: (node) => {
246
+ for (const child of Array.from(node.childNodes)) into.appendChild(child);
247
+ },
248
+ });
249
+ bindSlottedChildren(
250
+ this,
251
+ [
252
+ { name: 'sheet', into: this._sheetBodyEl },
253
+ { name: 'bottom-bar', into: this._bottomBarEl },
254
+ { name: 'content', into: this._contentEl },
255
+ layer('adw-bottom-sheet-sheet', this._sheetBodyEl),
256
+ layer('adw-bottom-sheet-bottom-bar', this._bottomBarEl),
257
+ layer('adw-bottom-sheet-content', this._contentEl),
258
+ { into: this._contentEl },
259
+ ],
260
+ (_node, slot) => {
261
+ // A bar adopted after connect is presence, which the open gate reads.
262
+ if (!('into' in slot) || slot.into !== this._bottomBarEl) return;
263
+ this._state.setHasBottomBar(true);
264
+ this._render();
265
+ },
266
+ ).install(this._contentEl, this._dimmingEl, this._sheetEl);
267
+
268
+ // A bar handed to `setBottomBar` before the upgrade wins over a declared one, as the
269
+ // later write does in GTK. `hasBottomBar` is re-seeded from the bin below, so the two
270
+ // cannot start out disagreeing about whether there is a bar.
271
+ if (this._pendingBottomBar) this._bottomBarEl.replaceChildren(this._pendingBottomBar);
272
+ this._pendingBottomBar = null;
161
273
 
162
274
  // Escape → `maybe_close_cb`. In GTK the shortcut sits on the sheet itself, so it
163
275
  // fires exactly while the sheet has focus — INCLUDING while the sheet is closed,
@@ -174,6 +286,10 @@ export class AdwBottomSheet extends HTMLElement {
174
286
  // Seeding BEFORE the subscription keeps the initial value silent, which is what
175
287
  // an attribute set during upgrade must be.
176
288
  this._state.setCanClose(this._boolAttr('can-close', true));
289
+ this._state.setCanOpen(this._boolAttr('can-open', true));
290
+ this._state.setRevealBottomBar(this._boolAttr('reveal-bottom-bar', true));
291
+ // A declared bar counts as present even where nothing called `setBottomBar`.
292
+ this._state.setHasBottomBar(this._bottomBarEl.childNodes.length > 0);
177
293
  this._state.setOpen(this._boolAttr('open', false));
178
294
  this._state.subscribe((open) => {
179
295
  this._render();
@@ -196,7 +312,10 @@ export class AdwBottomSheet extends HTMLElement {
196
312
  this._state.setOpen(this._boolAttr('open', false));
197
313
  return;
198
314
  }
315
+ if (name === 'modal') this._state.setModal(this._boolAttr('modal', true));
199
316
  if (name === 'can-close') this._state.setCanClose(this._boolAttr('can-close', true));
317
+ if (name === 'can-open') this._state.setCanOpen(this._boolAttr('can-open', true));
318
+ if (name === 'reveal-bottom-bar') this._state.setRevealBottomBar(this._boolAttr('reveal-bottom-bar', true));
200
319
  if (this._initialized) this._render();
201
320
  }
202
321
 
@@ -211,13 +330,25 @@ export class AdwBottomSheet extends HTMLElement {
211
330
 
212
331
  private _render(): void {
213
332
  const open = this._state.open;
333
+ const chrome = this._state.chrome;
334
+ const showsBar = chrome.layer === 'bottom-bar';
214
335
  this._reflectOpen(open);
215
336
  this.classList.toggle('open', open);
216
- this.classList.toggle('modal', this.modal);
337
+ this.classList.toggle('modal', this._state.modal);
217
338
  this.classList.toggle('has-drag-handle', this.showDragHandle);
218
-
219
- this._dimmingEl.classList.toggle('visible', open && this.modal);
220
- this._dragHandleEl.hidden = !this.showDragHandle;
339
+ // `showing-bottom-bar` is what holds the sheet at translateY(0) with only the bar
340
+ // exposed; the CSS transform IS this port's `gtk_widget_set_child_visible`, so the
341
+ // bar being hidden and the sheet being off screen are two different states.
342
+ this.classList.toggle('showing-bottom-bar', showsBar && chrome.surfaceVisible);
343
+
344
+ this._dimmingEl.classList.toggle('visible', chrome.dimmed);
345
+ this._bottomBarEl.hidden = !showsBar;
346
+ // `inert` only paints. The button stays enabled and focusable exactly as upstream
347
+ // leaves it (adw-bottom-sheet.c:2033-2036); the gate is what refuses the click, and
348
+ // `disabled` here would take the focus stop away with it.
349
+ this._bottomBarEl.classList.toggle('inert', chrome.bottomBarInert);
350
+ this._sheetBodyEl.hidden = showsBar;
351
+ this._dragHandleEl.hidden = !this.showDragHandle || showsBar;
221
352
  }
222
353
 
223
354
  private _reflectOpen(open: boolean): void {
@@ -237,33 +368,9 @@ export class AdwBottomSheet extends HTMLElement {
237
368
  if (value) this.setAttribute(name, '');
238
369
  else this.setAttribute(name, 'false');
239
370
  }
240
-
241
- /**
242
- * Collect the nodes declared for one slot, in DOCUMENT order, marking every
243
- * consumed child in `claimed`.
244
- *
245
- * The `<adw-bottom-sheet-sheet>` / `<adw-bottom-sheet-content>` wrappers are
246
- * markup, not widgets — GtkBuilder's `<child type="sheet">` leaves nothing in
247
- * the tree — so the wrapper itself is claimed
248
- * too. It used to fall through to `unslotted` and be appended to the content
249
- * layer, leaving an empty custom element behind after its children were
250
- * pulled out.
251
- */
252
- private _collectSlot(tag: string, slot: string, claimed: Set<Node>): Node[] {
253
- const nodes: Node[] = [];
254
- for (const child of Array.from(this.children)) {
255
- if (child.tagName.toLowerCase() === tag) {
256
- claimed.add(child);
257
- nodes.push(...Array.from(child.childNodes));
258
- } else if (child.getAttribute('slot') === slot) {
259
- claimed.add(child);
260
- nodes.push(child);
261
- }
262
- }
263
- return nodes;
264
- }
265
371
  }
266
372
 
267
373
  customElements.define('adw-bottom-sheet-content', AdwBottomSheetContent);
268
374
  customElements.define('adw-bottom-sheet-sheet', AdwBottomSheetSheet);
375
+ customElements.define('adw-bottom-sheet-bottom-bar', AdwBottomSheetBottomBar);
269
376
  customElements.define('adw-bottom-sheet', AdwBottomSheet);
@@ -22,8 +22,8 @@
22
22
  // `page-changed` (`detail = { index }`) fires on every settle, including back
23
23
  // onto the same page, and reports -1 when empty.
24
24
  //
25
- // The indicators bind through their `carousel` property or a `for` attribute
26
- // naming the carousel's id.
25
+ // The indicators bind through their `carousel` property, or a `carousel` (GTK's
26
+ // name, what a projected `.blp` writes) or `for` attribute naming the carousel's id.
27
27
  //
28
28
  // Reference: refs/libadwaita/src/adw-carousel.c (AdwCarousel behaviour)
29
29
  // Reference: refs/libadwaita/src/adw-carousel-indicator-dots.c
@@ -678,7 +678,7 @@ abstract class AdwCarouselIndicator extends HTMLElement {
678
678
  protected _initialized = false;
679
679
 
680
680
  static get observedAttributes() {
681
- return ['for'];
681
+ return ['for', 'carousel'];
682
682
  }
683
683
 
684
684
  /** The bound carousel. Mirrors AdwCarouselIndicator*'s `carousel` property. */
@@ -705,7 +705,7 @@ abstract class AdwCarouselIndicator extends HTMLElement {
705
705
  }
706
706
 
707
707
  attributeChangedCallback(name: string) {
708
- if (name === 'for' && this._initialized) {
708
+ if ((name === 'for' || name === 'carousel') && this._initialized) {
709
709
  this._detach();
710
710
  this._carousel = null;
711
711
  this._resolveForAttr();
@@ -714,8 +714,13 @@ abstract class AdwCarouselIndicator extends HTMLElement {
714
714
  }
715
715
  }
716
716
 
717
+ /**
718
+ * The carousel named by id. `carousel` is the GTK property's own name, and what a
719
+ * projected `.blp` writes for `carousel: carousel`; `for` is the markup spelling this
720
+ * element had first, kept as the fallback.
721
+ */
717
722
  private _resolveForAttr(): void {
718
- const id = this.getAttribute('for');
723
+ const id = this.getAttribute('carousel') ?? this.getAttribute('for');
719
724
  if (!id) return;
720
725
  const root = this.getRootNode() as Document | ShadowRoot;
721
726
  const el = root.getElementById?.(id) ?? document.getElementById(id);
@@ -26,6 +26,8 @@
26
26
 
27
27
  import { ADW_CLAMP_DEFAULTS, ADW_CLAMP_SIZE_CLASSES, clampAllocate, normalizeClampSize } from '@gjsify/adwaita-core';
28
28
 
29
+ import { bindSlottedChildren } from '../slotted-children.js';
30
+
29
31
  export class AdwClamp extends HTMLElement {
30
32
  private _resize: ResizeObserver | null = null;
31
33
  private _mutations: MutationObserver | null = null;
@@ -35,6 +37,16 @@ export class AdwClamp extends HTMLElement {
35
37
  }
36
38
 
37
39
  connectedCallback() {
40
+ // `Adw.Clamp:child` is a widget PROPERTY, so an authored `child: …` names the
41
+ // slot by that name (`shared-tree-builder.ts`'s `refuseUnknownSlots`) — and without
42
+ // a declaration for it, the placement was refused sight unseen. There is nowhere to
43
+ // ROUTE it to: every child here is already the placement, clamped in place, so this
44
+ // only enrols the NAME — `.install()` is never called, and nothing is moved. Calling
45
+ // it would be actively wrong: `into: this` would make the routed append a same-
46
+ // parent move, which the DOM still fires as a childList mutation, re-triggering the
47
+ // very MutationObserver that routes it.
48
+ bindSlottedChildren(this, [{ name: 'child', into: this }]);
49
+
38
50
  // ResizeObserver delivers an initial observation on observe(), so the
39
51
  // first allocation lands before paint without a separate seeding pass.
40
52
  this._resize = new ResizeObserver(() => this._allocate());
@@ -58,6 +58,9 @@
58
58
  import { ComboState, deriveRowLabels, normalizeComboOptions, parseListModel } from '@gjsify/adwaita-core';
59
59
  import type { AdwComboOption, AdwListItemsChanged, AdwListModelInput } from '@gjsify/adwaita-core';
60
60
 
61
+ import { bindSlottedChildren } from '../slotted-children.js';
62
+ import { stringListSlot } from '../string-list-slot.js';
63
+
61
64
  export class AdwComboRow extends HTMLElement {
62
65
  private _select!: HTMLSelectElement;
63
66
  private _valueEl!: HTMLSpanElement;
@@ -136,9 +139,6 @@ export class AdwComboRow extends HTMLElement {
136
139
  // Only when the PROPERTY was not already set, the rule `<adw-data-grid>` and
137
140
  // `<gtk-drop-down>` both follow: a model assigned to a detached element must
138
141
  // survive being attached, and an absent attribute must not blank it.
139
- if (this._state.count === 0) this._state.setModel(parseListModel(this.getAttribute('model')));
140
- this._state.setSelectedIndex(parseInt(this.getAttribute('selected') || '0', 10));
141
-
142
142
  const text = document.createElement('div');
143
143
  text.className = 'adw-row-text';
144
144
  this._titleEl = document.createElement('span');
@@ -152,7 +152,16 @@ export class AdwComboRow extends HTMLElement {
152
152
 
153
153
  this._select = document.createElement('select');
154
154
 
155
- this.replaceChildren(text, this._valueEl, this._select);
155
+ // A `<gtk-string-list slot="model">` child is the list a `.blp` authored (ADR 0072),
156
+ // consumed here BEFORE the attribute seed below, so the seed sees a model and leaves
157
+ // it alone, and before the selection, so an authored index lands on the real list.
158
+ bindSlottedChildren(this, [stringListSlot((model) => this._state.setModel(model))]).install(
159
+ text,
160
+ this._valueEl,
161
+ this._select,
162
+ );
163
+ if (this._state.count === 0) this._state.setModel(parseListModel(this.getAttribute('model')));
164
+ this._state.setSelectedIndex(parseInt(this.getAttribute('selected') || '0', 10));
156
165
 
157
166
  // WHERE the model changed — applied before the selection change below, which is
158
167
  // the ordering that lets that subscriber's index write land on the new list.
@@ -47,6 +47,13 @@ export class AdwHeaderBar extends HTMLElement {
47
47
  * see the divergence at the top of this file.
48
48
  */
49
49
  private _titleEl: HTMLElement | null = null;
50
+ /**
51
+ * The `<adw-navigation-page>` this bar sits in, and the observer that follows its
52
+ * title. `update_title` (adw-header-bar.c:488) asks that page FIRST, which is how a
53
+ * bare `Adw.HeaderBar {}` in a `.blp` page shows "Mailboxes" with no title of its own.
54
+ */
55
+ private _page: Element | null = null;
56
+ private _pageObserver: MutationObserver | null = null;
50
57
 
51
58
  static get observedAttributes() {
52
59
  return ['title', 'subtitle'];
@@ -69,7 +76,11 @@ export class AdwHeaderBar extends HTMLElement {
69
76
  }
70
77
 
71
78
  connectedCallback() {
72
- if (this._initialized) return;
79
+ if (this._initialized) {
80
+ this._followPage();
81
+ this._renderTitle();
82
+ return;
83
+ }
73
84
  this._initialized = true;
74
85
 
75
86
  this._startEl = document.createElement('div');
@@ -92,10 +103,19 @@ export class AdwHeaderBar extends HTMLElement {
92
103
  [
93
104
  { name: 'start', into: this._startEl },
94
105
  { name: 'center', into: this._centerEl },
106
+ // The SAME destination under the name GTK writes it at. This section has
107
+ // always documented itself as "the equivalent of Adw.HeaderBar's
108
+ // title-widget"; a tree authored at the property position (a Blueprint
109
+ // `title-widget: …`) carried exactly that name, found no destination for it
110
+ // and stayed a stray sibling — after which this element derived its own
111
+ // title into the centre, so the authored one was gone from the document.
112
+ // `center` stays because it is what this package published and what the
113
+ // gallery's own fences are written in.
114
+ { name: 'title-widget', into: this._centerEl },
95
115
  { name: 'end', into: this._endEl },
96
116
  ],
97
117
  (_node, slot) => {
98
- if (slot.name === 'center') this._dropDerivedTitle();
118
+ if ('into' in slot && slot.into === this._centerEl) this._dropDerivedTitle();
99
119
  },
100
120
  ).install(this._startEl, this._centerEl, this._endEl);
101
121
 
@@ -108,9 +128,29 @@ export class AdwHeaderBar extends HTMLElement {
108
128
  this._titleEl.className = 'adw-header-bar-title';
109
129
  this._centerEl.appendChild(this._titleEl);
110
130
  }
131
+ this._followPage();
111
132
  this._renderTitle();
112
133
  }
113
134
 
135
+ disconnectedCallback() {
136
+ this._pageObserver?.disconnect();
137
+ this._pageObserver = null;
138
+ this._page = null;
139
+ }
140
+
141
+ /**
142
+ * Find the enclosing navigation page and re-render when its title moves, as GTK does:
143
+ * `root` looks the ancestor up (adw-header-bar.c:586) and a `notify::title` on it re-runs
144
+ * `update_title`. Looked up on every connect, since a bar moved into another page has a
145
+ * new ancestor, which is why C does it in `root` and drops it in `unroot`.
146
+ */
147
+ private _followPage() {
148
+ this._page = this.closest('adw-navigation-page');
149
+ if (this._page === null) return;
150
+ this._pageObserver ??= new MutationObserver(() => this._renderTitle());
151
+ this._pageObserver.observe(this._page, { attributes: true, attributeFilter: ['title'] });
152
+ }
153
+
114
154
  /**
115
155
  * `title` used to be read ONCE, in `connectedCallback`, so every later write
116
156
  * was a silent no-op — a header bar whose title tracked the open document
@@ -147,7 +187,10 @@ export class AdwHeaderBar extends HTMLElement {
147
187
  // empty-string collapse do the work. Removing rather than writing `''`
148
188
  // keeps "unset" distinguishable from "set to empty" on the child.
149
189
  for (const name of ['title', 'subtitle']) {
150
- const value = this.getAttribute(name);
190
+ // The bar's own `title` is this port's declarative divergence (see the header) and
191
+ // wins; without one, the page's title is the first answer `update_title` has.
192
+ const value =
193
+ this.getAttribute(name) ?? (name === 'title' ? (this._page?.getAttribute('title') ?? null) : null);
151
194
  if (value === null) this._titleEl.removeAttribute(name);
152
195
  else this._titleEl.setAttribute(name, value);
153
196
  }
@@ -55,6 +55,7 @@ import {
55
55
  domViewSwitcherScheduler,
56
56
  readSwitcherPage,
57
57
  } from './view-switcher-dom.js';
58
+ import { bindSlottedChildren } from '../slotted-children.js';
58
59
  import { attachRovingFocus } from './roving-focus.js';
59
60
 
60
61
  /** A single page. Children of <adw-inline-view-switcher>; consumed at connect time. */
@@ -62,6 +63,16 @@ export class AdwViewStackPage extends HTMLElement {
62
63
  static get observedAttributes() {
63
64
  return ['name', 'title', 'icon-name', 'badge-number', 'needs-attention', 'use-underline'];
64
65
  }
66
+
67
+ connectedCallback(): void {
68
+ // `AdwViewStackPage:child` is a PROPERTY, so a `.blp`'s `child: …` authors
69
+ // `slot="child"`, and the mount refused the name this element never declared. The
70
+ // page's children already ARE its content — the stack or the switcher moves them
71
+ // out when it adopts the page — so this only enrols the name, as `<adw-tab-page>`
72
+ // does. The owner adopts on ITS connect, which runs first, so by now this page may
73
+ // be detached; enrolling is all that is left to do either way.
74
+ bindSlottedChildren(this, [{ name: 'child', into: this }]);
75
+ }
65
76
  }
66
77
 
67
78
  export class AdwInlineViewSwitcher extends HTMLElement {
@@ -54,6 +54,8 @@
54
54
  import { BACK_BUTTON_FALLBACK_TOOLTIP, NavigationViewState, describeNavigationDiagnostic } from '@gjsify/adwaita-core';
55
55
  import type { AdwNavigationPageProps, NavigationDiagnostic, NavigationStackChange } from '@gjsify/adwaita-core';
56
56
 
57
+ import { bindSlottedChildren } from '../slotted-children.js';
58
+
57
59
  /** The page properties the core owns, read off the element's attributes. */
58
60
  function readPageProps(page: AdwNavigationPage): AdwNavigationPageProps {
59
61
  return { tag: page.tag, title: page.title, canPop: page.canPop };
@@ -68,6 +70,14 @@ export class AdwNavigationPage extends HTMLElement {
68
70
  return ['title', 'tag', 'can-pop', 'no-back-button'];
69
71
  }
70
72
 
73
+ connectedCallback(): void {
74
+ // `AdwNavigationPage:child` is a widget PROPERTY, so a `.blp`'s `child: …` authors
75
+ // `slot="child"`. The page's children already ARE its content, so this only enrols
76
+ // the name — nothing is routed, and `.install()` is never called, for the reason
77
+ // `adw-clamp.ts` gives: `into: this` would re-trigger the observer that routes it.
78
+ bindSlottedChildren(this, [{ name: 'child', into: this }]);
79
+ }
80
+
71
81
  get title(): string {
72
82
  return this.getAttribute('title') ?? '';
73
83
  }
@@ -72,6 +72,28 @@ export class AdwSidebarItem extends HTMLElement {
72
72
  return ['title', 'subtitle', 'icon-name', 'disabled', 'hidden'];
73
73
  }
74
74
 
75
+ /**
76
+ * `AdwSidebarItem:enabled`, under the GIR name — the `disabled` attribute inverted. The
77
+ * property is what lets an authored `enabled: false` reach this item through the
78
+ * shared-tree builder, which writes a `false` through the element's own property.
79
+ */
80
+ get enabled(): boolean {
81
+ return !this.hasAttribute('disabled');
82
+ }
83
+
84
+ set enabled(value: boolean) {
85
+ this.toggleAttribute('disabled', !value);
86
+ }
87
+
88
+ /** `AdwSidebarItem:visible` — the `hidden` attribute inverted, for the same reason. */
89
+ get visible(): boolean {
90
+ return !this.hasAttribute('hidden');
91
+ }
92
+
93
+ set visible(value: boolean) {
94
+ this.toggleAttribute('hidden', !value);
95
+ }
96
+
75
97
  attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
76
98
  const binding = itemBindings.get(this);
77
99
  if (!binding) return;