@gjsify/adwaita-web 0.35.0 → 0.37.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 (146) hide show
  1. package/dist/adwaita-web.css +433 -33
  2. package/dist/adwaita-web.css.map +1 -1
  3. package/lib/types/adw-shortcut-label.spec.d.ts +2 -0
  4. package/lib/types/breakpoints.d.ts +10 -12
  5. package/lib/types/elements/adw-about-dialog.d.ts +19 -36
  6. package/lib/types/elements/adw-action-row.d.ts +9 -12
  7. package/lib/types/elements/adw-alert-dialog.d.ts +0 -12
  8. package/lib/types/elements/adw-avatar.d.ts +1 -0
  9. package/lib/types/elements/adw-bottom-sheet.d.ts +7 -12
  10. package/lib/types/elements/adw-button-content.d.ts +3 -4
  11. package/lib/types/elements/adw-button-row.d.ts +1 -1
  12. package/lib/types/elements/adw-carousel.d.ts +28 -49
  13. package/lib/types/elements/adw-checks.d.ts +2 -8
  14. package/lib/types/elements/adw-combo-row.d.ts +6 -6
  15. package/lib/types/elements/adw-data-grid.d.ts +5 -10
  16. package/lib/types/elements/adw-drop-down.d.ts +3 -11
  17. package/lib/types/elements/adw-entry-row.d.ts +20 -23
  18. package/lib/types/elements/adw-icon.d.ts +2 -4
  19. package/lib/types/elements/adw-inline-view-switcher.d.ts +18 -31
  20. package/lib/types/elements/adw-navigation-split-view.d.ts +7 -12
  21. package/lib/types/elements/adw-navigation-view.d.ts +2 -10
  22. package/lib/types/elements/adw-overlay-split-view.d.ts +28 -52
  23. package/lib/types/elements/adw-password-entry-row.d.ts +4 -5
  24. package/lib/types/elements/adw-popover.d.ts +2 -7
  25. package/lib/types/elements/adw-preferences-dialog.d.ts +21 -34
  26. package/lib/types/elements/adw-preferences-group.d.ts +12 -14
  27. package/lib/types/elements/adw-progress-bar.d.ts +3 -4
  28. package/lib/types/elements/adw-shortcut-label.d.ts +20 -0
  29. package/lib/types/elements/adw-sidebar.d.ts +5 -10
  30. package/lib/types/elements/adw-spinner.d.ts +3 -5
  31. package/lib/types/elements/adw-split-button.d.ts +6 -16
  32. package/lib/types/elements/adw-switch-row.d.ts +3 -3
  33. package/lib/types/elements/adw-tab-view.d.ts +16 -26
  34. package/lib/types/elements/adw-toast-overlay.d.ts +8 -13
  35. package/lib/types/elements/adw-view-stack.d.ts +7 -17
  36. package/lib/types/elements/adw-view-switcher-bar.d.ts +4 -8
  37. package/lib/types/elements/adw-view-switcher.d.ts +18 -38
  38. package/lib/types/elements/adw-wrap-box.d.ts +7 -17
  39. package/lib/types/elements/view-switcher-dom.d.ts +10 -13
  40. package/lib/types/index.d.ts +1 -0
  41. package/lib/types/source-view/hex.d.ts +7 -13
  42. package/lib/types/style-classes.spec.d.ts +11 -0
  43. package/lib/types/styles.generated.d.ts +1 -1
  44. package/package.json +6 -6
  45. package/scss/_about_dialog.scss +3 -2
  46. package/scss/_action_row.scss +33 -0
  47. package/scss/_avatar.scss +12 -0
  48. package/scss/_button.scss +44 -22
  49. package/scss/_card.scss +11 -8
  50. package/scss/_checks.scss +2 -4
  51. package/scss/_dialog.scss +3 -2
  52. package/scss/_headerbar.scss +23 -0
  53. package/scss/_popover.scss +8 -4
  54. package/scss/_preferences.scss +7 -7
  55. package/scss/_progress_bar.scss +9 -6
  56. package/scss/_reset.scss +1 -1
  57. package/scss/_shortcut_label.scss +80 -0
  58. package/scss/_sidebar.scss +57 -0
  59. package/scss/_status_page.scss +20 -0
  60. package/scss/_style_classes.scss +407 -0
  61. package/scss/_switch.scss +4 -3
  62. package/scss/_variables.scss +25 -0
  63. package/scss/adwaita-skin.scss +12 -0
  64. package/src/adw-about-dialog.spec.ts +34 -49
  65. package/src/adw-action-rows.spec.ts +25 -33
  66. package/src/adw-avatar.spec.ts +51 -20
  67. package/src/adw-banner.spec.ts +5 -8
  68. package/src/adw-button-content.spec.ts +9 -12
  69. package/src/adw-button-row.spec.ts +2 -4
  70. package/src/adw-carousel.spec.ts +40 -53
  71. package/src/adw-checks.spec.ts +36 -47
  72. package/src/adw-drop-down.spec.ts +25 -32
  73. package/src/adw-header-bar.spec.ts +5 -7
  74. package/src/adw-icon.spec.ts +23 -32
  75. package/src/adw-progress-bar.spec.ts +10 -12
  76. package/src/adw-row-state.spec.ts +9 -10
  77. package/src/adw-shortcut-label.spec.ts +112 -0
  78. package/src/adw-sidebar.spec.ts +17 -24
  79. package/src/adw-switch.spec.ts +11 -17
  80. package/src/adw-tab-view.spec.ts +7 -10
  81. package/src/adw-toast-overlay.spec.ts +7 -11
  82. package/src/adw-view-switcher-bar.spec.ts +10 -14
  83. package/src/adw-wrap-box.spec.ts +25 -36
  84. package/src/bottom-sheet.spec.ts +30 -36
  85. package/src/breakpoints.ts +15 -18
  86. package/src/chrome.spec.ts +13 -21
  87. package/src/elements/adw-about-dialog.ts +71 -114
  88. package/src/elements/adw-action-row.ts +22 -37
  89. package/src/elements/adw-alert-dialog.ts +25 -50
  90. package/src/elements/adw-avatar.ts +40 -15
  91. package/src/elements/adw-banner.ts +14 -18
  92. package/src/elements/adw-bottom-sheet.ts +31 -52
  93. package/src/elements/adw-button-content.ts +30 -37
  94. package/src/elements/adw-button-row.ts +14 -24
  95. package/src/elements/adw-carousel.ts +87 -148
  96. package/src/elements/adw-checks.ts +43 -56
  97. package/src/elements/adw-clamp.ts +8 -9
  98. package/src/elements/adw-combo-row.ts +17 -25
  99. package/src/elements/adw-data-grid.ts +26 -38
  100. package/src/elements/adw-dialog.ts +25 -56
  101. package/src/elements/adw-drop-down.ts +31 -51
  102. package/src/elements/adw-entry-row.ts +39 -46
  103. package/src/elements/adw-expander-row.ts +30 -53
  104. package/src/elements/adw-icon.ts +30 -53
  105. package/src/elements/adw-inline-view-switcher.ts +38 -67
  106. package/src/elements/adw-menu-button.ts +28 -58
  107. package/src/elements/adw-navigation-split-view.ts +35 -47
  108. package/src/elements/adw-navigation-view.ts +12 -28
  109. package/src/elements/adw-overlay-split-view.ts +100 -175
  110. package/src/elements/adw-password-entry-row.ts +18 -28
  111. package/src/elements/adw-popover.ts +28 -44
  112. package/src/elements/adw-preferences-dialog.ts +41 -66
  113. package/src/elements/adw-preferences-group.ts +33 -46
  114. package/src/elements/adw-progress-bar.ts +27 -47
  115. package/src/elements/adw-shortcut-label.ts +138 -0
  116. package/src/elements/adw-sidebar.ts +31 -39
  117. package/src/elements/adw-spin-row.ts +7 -14
  118. package/src/elements/adw-spinner.ts +31 -43
  119. package/src/elements/adw-split-button.ts +45 -75
  120. package/src/elements/adw-switch-row.ts +29 -46
  121. package/src/elements/adw-switch.ts +28 -47
  122. package/src/elements/adw-tab-view.ts +62 -103
  123. package/src/elements/adw-toast-overlay.ts +28 -35
  124. package/src/elements/adw-toggle-group.ts +9 -16
  125. package/src/elements/adw-view-stack.ts +32 -51
  126. package/src/elements/adw-view-switcher-bar.ts +26 -50
  127. package/src/elements/adw-view-switcher.ts +60 -110
  128. package/src/elements/adw-window-title.ts +4 -4
  129. package/src/elements/adw-wrap-box.ts +33 -59
  130. package/src/elements/view-switcher-dom.ts +15 -22
  131. package/src/entry-rows.spec.ts +18 -24
  132. package/src/index.ts +13 -15
  133. package/src/navigation-view.spec.ts +7 -10
  134. package/src/preferences-search.ts +11 -15
  135. package/src/preferences.spec.ts +13 -15
  136. package/src/source-view/adw-source-view.ts +6 -19
  137. package/src/source-view/hex.ts +7 -13
  138. package/src/split-button.spec.ts +32 -44
  139. package/src/split-views.spec.ts +83 -149
  140. package/src/style-classes.spec.ts +181 -0
  141. package/src/style-isolation.spec.ts +3 -6
  142. package/src/styles.generated.ts +1 -1
  143. package/src/tab-view.spec.ts +26 -35
  144. package/src/test.browser.mts +4 -0
  145. package/src/view-stack.spec.ts +23 -34
  146. package/src/view-switcher.spec.ts +56 -84
@@ -1,10 +1,9 @@
1
1
  // <adw-view-stack> — A standalone, named-page CONTENT container: the web
2
2
  // counterpart of Adw.ViewStack decoupled from any switcher. It holds a set of
3
3
  // named pages and shows exactly one at a time; a paired <adw-view-switcher-bar>
4
- // (or an adaptive header-bar switcher) binds to it and drives the selection.
5
- // This is the piece the web package lacked — it already had switchers that own
6
- // their own pages (<adw-view-switcher>, <adw-inline-view-switcher>) but no
7
- // standalone stack a separate switcher can drive.
4
+ // (or an adaptive header-bar switcher) binds to it and drives the selection. The
5
+ // alternative switchers (<adw-view-switcher>, <adw-inline-view-switcher>) own their
6
+ // own pages instead.
8
7
  //
9
8
  // The SELECTION itself — the name lookup, the guards on an index change, the
10
9
  // first-visible-page auto-pick and which paths notify — is HEADLESS and lives in
@@ -38,11 +37,10 @@
38
37
  // page falls back to the next visible one.
39
38
  // Events:
40
39
  // `notify::visible-child` (CustomEvent, bubbles,
41
- // detail = { index, name, title, interactive }) whenever the visible page
42
- // changes — including the auto-shown first page, exactly as
43
- // `set_visible_child` notifies there (adw-view-stack.c:1038-1039). It used to
44
- // stay silent on that path, which is why a bound switcher needed a manual
45
- // `refresh()`. `interactive` is false for an auto-pick.
40
+ // detail = { index, name, title, interactive }) whenever the visible page changes,
41
+ // INCLUDING the auto-shown first page — `set_visible_child` notifies there too, and
42
+ // a switcher that is not told needs a manual `refresh()`. `interactive` is false
43
+ // for an auto-pick.
46
44
  // Reference: refs/libadwaita/src/adw-view-stack.c (AdwViewStack behaviour)
47
45
  // Reference: packages/nativescript-bridge/adwaita/src/widgets/adw-view-stack.ts (NS twin)
48
46
  // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
@@ -51,7 +49,6 @@
51
49
  import { ViewStackState } from '@gjsify/adwaita-core';
52
50
  import type { AdwViewStackPageInfo as CorePageInfo, ViewStackStateChange } from '@gjsify/adwaita-core';
53
51
 
54
- /** A page descriptor exposed via `pages` (consumed by a bound switcher bar). */
55
52
  export type AdwViewStackPageInfo = CorePageInfo<HTMLElement>;
56
53
 
57
54
  export class AdwViewStack extends HTMLElement {
@@ -69,7 +66,6 @@ export class AdwViewStack extends HTMLElement {
69
66
  this._state.subscribe((change) => this._onStateChange(change));
70
67
  }
71
68
 
72
- /** All pages in declaration/add order. A bound switcher reads this. */
73
69
  get pages(): readonly AdwViewStackPageInfo[] {
74
70
  return this._state.pages;
75
71
  }
@@ -83,7 +79,6 @@ export class AdwViewStack extends HTMLElement {
83
79
  this._state.setVisibleIndex(value);
84
80
  }
85
81
 
86
- /** Name of the visible page, or '' when nothing is selected. */
87
82
  get visibleChildName(): string {
88
83
  return this._state.visibleName;
89
84
  }
@@ -92,7 +87,6 @@ export class AdwViewStack extends HTMLElement {
92
87
  this._state.setVisibleName(name);
93
88
  }
94
89
 
95
- /** The visible page's content element, or null when nothing is selected. */
96
90
  get visibleChild(): HTMLElement | null {
97
91
  return this._state.visiblePage?.content ?? null;
98
92
  }
@@ -106,21 +100,17 @@ export class AdwViewStack extends HTMLElement {
106
100
  // otherwise overwrite the author's value with page 0's name.
107
101
  const initialName = this.getAttribute('visible-child-name');
108
102
 
109
- // Adopt the declared <adw-view-stack-page> children by APPENDING them —
110
- // never by replacing the page list. The old code assigned the parsed
111
- // wrappers wholesale and then `replaceChildren()`d the subtree away, so a
112
- // stack built imperatively before it entered the document
113
- // (`createElement` → `add()` → `body.append()`) lost every page it had
114
- // been given. libadwaita has no parse-once phase that can discard
115
- // children: the buildable adds them one at a time
116
- // (adw_view_stack_buildable_add_child, adw-view-stack.c:1666-1680) and
117
- // `adw_view_stack_add` works at any point in the widget's life.
103
+ // Adopt the declared <adw-view-stack-page> children by APPENDING them, never by
104
+ // replacing the page list: a stack built imperatively before it entered the
105
+ // document (`createElement` → `add()` → `body.append()`) would lose every page
106
+ // it had been given. libadwaita has no parse-once phase that can discard
107
+ // children — the buildable adds them one at a time and `adw_view_stack_add`
108
+ // works at any point in the widget's life.
118
109
  for (const pageEl of Array.from(this.querySelectorAll(':scope > adw-view-stack-page')) as HTMLElement[]) {
119
- // A declared page with no `name` is the page named '' — C stores the
120
- // name verbatim (adw-view-stack.c:1170), and an invented `page-<index>`
121
- // is addressable by a name the author never wrote.
110
+ // A declared page with no `name` is the page named '': C stores the name
111
+ // verbatim, and an invented `page-<index>` would be addressable by a name the
112
+ // author never wrote.
122
113
  const name = pageEl.getAttribute('name') ?? '';
123
- // Wrapping each page's content lets us toggle exactly one.
124
114
  const element = document.createElement('div');
125
115
  element.className = 'adw-view-stack-child';
126
116
  element.dataset.name = name;
@@ -132,16 +122,15 @@ export class AdwViewStack extends HTMLElement {
132
122
  icon: pageEl.getAttribute('icon-name'),
133
123
  content: element,
134
124
  // `hidden` is the DOM spelling of AdwViewStackPage:visible, which
135
- // gates the auto-pick and refuses selection (adw-view-stack.c:1149-1151, :2415).
125
+ // gates the auto-pick and refuses selection.
136
126
  visible: !pageEl.hasAttribute('hidden'),
137
127
  });
138
128
  this.replaceChild(element, pageEl);
139
129
  }
140
130
 
141
- // An explicit `visible-child-name` selects the initial page. Only a
142
- // MISSING attribute falls through to the auto-pick — an empty value is a
143
- // legal lookup for a page named '' (g_strcmp0, adw-view-stack.c:901),
144
- // which the old `initialName ? … : -1` falsy check silently dropped.
131
+ // An explicit `visible-child-name` selects the initial page; only a MISSING
132
+ // attribute falls through to the auto-pick. An empty value is a legal lookup for
133
+ // a page named '' (`g_strcmp0`), so a falsy check here is wrong.
145
134
  if (initialName !== null) this._state.setVisibleName(initialName, false);
146
135
  this._applyVisibility();
147
136
  this._reflectName();
@@ -159,7 +148,7 @@ export class AdwViewStack extends HTMLElement {
159
148
 
160
149
  /**
161
150
  * Append a page. It becomes visible when nothing is selected yet, matching
162
- * `Adw.ViewStack.add()` — and, unlike before, that auto-pick NOTIFIES.
151
+ * `Adw.ViewStack.add()`, and that auto-pick NOTIFIES.
163
152
  */
164
153
  add(content: HTMLElement, name: string, title?: string, icon?: string): AdwViewStackPageInfo {
165
154
  content.classList.add('adw-view-stack-child');
@@ -172,7 +161,6 @@ export class AdwViewStack extends HTMLElement {
172
161
  return page;
173
162
  }
174
163
 
175
- /** Convenience alias mirroring Adw.ViewStack.add_titled() (no icon). */
176
164
  addTitled(content: HTMLElement, name: string, title: string): AdwViewStackPageInfo {
177
165
  return this.add(content, name, title);
178
166
  }
@@ -181,7 +169,7 @@ export class AdwViewStack extends HTMLElement {
181
169
  * Remove the first page named `name` and detach its content. Returns whether
182
170
  * anything was removed. When the removed page was the visible one the stack
183
171
  * ends up showing NOTHING and emits no event — `stack_remove` clears
184
- * `visible_child` without re-picking (adw-view-stack.c:1202-1203).
172
+ * `visible_child` without re-picking.
185
173
  */
186
174
  removePage(name: string): boolean {
187
175
  const content = this._state.pages[this._state.indexOfName(name)]?.content;
@@ -197,28 +185,23 @@ export class AdwViewStack extends HTMLElement {
197
185
  * Show or hide a page (`AdwViewStackPage:visible`). Returns whether the
198
186
  * SELECTION moved: hiding the visible page falls back to the next visible
199
187
  * one, and making a page visible while nothing is selected selects it
200
- * (`update_child_visible`, adw-view-stack.c:1061-1082).
188
+ * (`update_child_visible`).
201
189
  */
202
190
  setPageVisible(name: string, visible: boolean): boolean {
203
191
  const moved = this._state.setPageVisible(name, visible);
204
192
  this._applyVisibility();
205
193
  // A visibility flip changes what the pages MODEL reports, which is what
206
- // `update_bar_revealed` counts (adw-view-switcher-bar.c:340) — hiding
207
- // the second-to-last visible page must retract the bar.
194
+ // `update_bar_revealed` counts — hiding the second-to-last visible page must
195
+ // retract the bar.
208
196
  this._emitItemsChanged();
209
197
  return moved;
210
198
  }
211
199
 
212
200
  /**
213
- * The pages model's `items-changed`, which the DOM stack had no counterpart
214
- * for at all.
215
- *
216
- * `AdwViewSwitcherBar` re-runs `update_bar_revealed` on it
217
- * (adw-view-switcher-bar.c:340) and `AdwViewSwitcher` rebinds its buttons
218
- * (adw-view-switcher.c:258-263). Both ports listened only on
219
- * `notify::visible-child`, so adding a page or hiding a non-selected one
220
- * left `revealed` — and the button row — stale, with a manual `refresh()`
221
- * as the documented workaround.
201
+ * The pages model's `items-changed`: `AdwViewSwitcherBar` re-runs
202
+ * `update_bar_revealed` on it and `AdwViewSwitcher` rebinds its buttons. A listener
203
+ * on `notify::visible-child` alone leaves `revealed` and the button row stale when a
204
+ * page is added or a non-selected one hidden.
222
205
  */
223
206
  private _emitItemsChanged(): void {
224
207
  this.dispatchEvent(new CustomEvent('items-changed', { bubbles: true, detail: { count: this.pages.length } }));
@@ -253,11 +236,9 @@ export class AdwViewStack extends HTMLElement {
253
236
 
254
237
  private _reflectName(): void {
255
238
  // With nothing selected there is no name to mirror — C's
256
- // `adw_view_stack_get_visible_child_name` returns NULL there
257
- // (adw-view-stack.c:2384), and a missing attribute is the DOM spelling of
258
- // that. An EMPTY name is still a name and IS reflected: the old
259
- // `if (current && …)` guard meant a stack of nameless pages never
260
- // reflected its selection at all.
239
+ // `adw_view_stack_get_visible_child_name` returns NULL, and a missing attribute
240
+ // is the DOM spelling of that. An EMPTY name is still a name and IS reflected: a
241
+ // truthiness guard here stops a stack of nameless pages reflecting at all.
261
242
  if (this._state.visibleIndex < 0) {
262
243
  this.removeAttribute('visible-child-name');
263
244
  return;
@@ -3,33 +3,16 @@
3
3
  // owns no pages: it reads the bound stack's `pages`, renders one homogeneous
4
4
  // button per page, and is two-way synced to the stack's visible child.
5
5
  //
6
- // The derivations are HEADLESS and live in `@gjsify/adwaita-core` (ADR 0004):
7
- // the per-button model (`buildViewSwitcherButtons`, so this bar and the header
8
- // switcher cannot disagree about what a page looks like) and — the rule this
9
- // port was missing entirely — `ViewSwitcherBarState`, which is `reveal` AND
10
- // MORE THAN ONE VISIBLE PAGE (`update_bar_revealed`,
11
- // adw-view-switcher-bar.c:104-126). A bar bound to a one-page stack used to show
12
- // an empty strip that libadwaita keeps collapsed.
6
+ // The derivations are HEADLESS and live in `@gjsify/adwaita-core` (ADR 0004): the
7
+ // per-button model (`buildViewSwitcherButtons`, so this bar and the header switcher
8
+ // cannot disagree about what a page looks like) and `ViewSwitcherBarState`, which is
9
+ // `reveal` AND MORE THAN ONE VISIBLE PAGE (`update_bar_revealed`) — a bar bound to a
10
+ // one-page stack stays collapsed however loudly the layout asks.
13
11
  //
14
- // Also fixed here, from the same C: `disconnectedCallback` removed the stack
15
- // listener and `connectedCallback` only re-added it when `ref && !this._stack`,
16
- // so a bar that was detached and re-inserted never tracked its stack again. In
17
- // C the handlers are bound in `set_stack` and released only in `dispose`
18
- // (:330-341, :218-221) — realize/unrealize never touch them.
12
+ // `stack` names the <adw-view-stack> to drive by id, like `<label for>`. `revealed` is
13
+ // the LEGACY spelling of the `reveal` request and is still read as one; as a property
14
+ // it is read-only and reports whether the bar is actually shown.
19
15
  //
20
- // Attributes:
21
- // stack — the id of the <adw-view-stack> to drive (like <label for>).
22
- // reveal — boolean; the REQUEST (`Adw.ViewSwitcherBar:reveal`, default off).
23
- // revealed — the legacy spelling of `reveal`, still accepted as the request.
24
- // Properties:
25
- // stack — the bound AdwViewStack element (get/set; set also accepts an id).
26
- // reveal — the request (get/set).
27
- // revealed — READ-ONLY: whether the bar is actually shown, i.e. the request
28
- // AND more than one visible page.
29
- // Methods:
30
- // setStack(stack) — bind to a stack element (or null); rebuilds the buttons.
31
- // refresh() — rebuild after pages were added to the bound stack without
32
- // changing its selection (see the note in `_rebuild`).
33
16
  // Reference: refs/libadwaita/src/adw-view-switcher-bar.c (AdwViewSwitcherBar)
34
17
  // Reference: refs/libadwaita/src/adw-view-switcher.c (the switcher it embeds)
35
18
  // Reference: packages/nativescript-bridge/adwaita/src/widgets/adw-view-switcher-bar.ts (NS twin)
@@ -91,7 +74,7 @@ export class AdwViewSwitcherBar extends HTMLElement {
91
74
 
92
75
  /**
93
76
  * Whether the bar is actually shown. READ-ONLY: a one-page stack keeps it
94
- * collapsed however loudly the layout asks (adw-view-switcher-bar.c:125).
77
+ * collapsed however loudly the layout asks.
95
78
  * Write {@link reveal} to make the request.
96
79
  */
97
80
  get revealed(): boolean {
@@ -108,17 +91,17 @@ export class AdwViewSwitcherBar extends HTMLElement {
108
91
  }
109
92
  this._barState.setReveal(this._readReveal());
110
93
 
111
- // Resolve the `stack` id ref now that the element is in the document —
112
- // the referenced stack is normally earlier in the DOM (content above a
113
- // bottom bar), so its pages are ready.
94
+ // Resolve the `stack` id ref now the element is in the document: the referenced
95
+ // stack is normally earlier in the DOM (content above a bottom bar), so its pages
96
+ // are ready.
114
97
  const ref = this.getAttribute('stack');
115
98
  if (ref && !this._stack) {
116
99
  this.setStack(this._resolveStack(ref));
117
100
  return;
118
101
  }
119
- // Re-attaching must re-bind: `disconnectedCallback` released the
120
- // listener, and the old `ref && !this._stack` guard could never take it
121
- // back. In C the binding survives unrealize entirely.
102
+ // Re-attaching must re-bind, because `disconnectedCallback` released the listener.
103
+ // In C the binding is made in `set_stack`, released in `dispose`, and survives
104
+ // unrealize entirely.
122
105
  this._bindStack();
123
106
  this._rebuild();
124
107
  }
@@ -147,13 +130,9 @@ export class AdwViewSwitcherBar extends HTMLElement {
147
130
  }
148
131
 
149
132
  /**
150
- * Rebuild the buttons by hand.
151
- *
152
- * Kept as public API — a consumer that mutates a page in place still has a
153
- * way to say so — but no longer the ONLY way: the stack emits
154
- * `items-changed` now and this bar binds it, which is the mechanism
155
- * libadwaita uses (adw-view-switcher-bar.c:340,
156
- * adw-view-switcher.c:258-263).
133
+ * Rebuild the buttons by hand — public API for a consumer that mutated a page in
134
+ * place. Not the only path: the stack emits `items-changed` and this bar binds it,
135
+ * which is the mechanism libadwaita uses.
157
136
  */
158
137
  refresh(): void {
159
138
  this._rebuild();
@@ -165,10 +144,9 @@ export class AdwViewSwitcherBar extends HTMLElement {
165
144
 
166
145
  private _bindStack(): void {
167
146
  this._stack?.addEventListener('notify::visible-child', this._onStackNotify);
168
- // `update_bar_revealed` runs on the pages model's `items-changed` too
169
- // (adw-view-switcher-bar.c:340), which is the half that was missing: a
170
- // page added without moving the selection, or a non-selected page
171
- // hidden, changes the COUNT the reveal is decided from.
147
+ // `update_bar_revealed` runs on the pages model's `items-changed` too: a page
148
+ // added without moving the selection, or a non-selected page hidden, changes the
149
+ // COUNT the reveal is decided from.
172
150
  this._stack?.addEventListener('items-changed', this._onStackNotify);
173
151
  }
174
152
 
@@ -187,18 +165,16 @@ export class AdwViewSwitcherBar extends HTMLElement {
187
165
  if (!this._initialized) return;
188
166
 
189
167
  const stackPages = this._stack?.pages ?? [];
190
- // The page count is half the reveal derivation, so it is fed on every
191
- // rebuild — `update_bar_revealed` is re-run from set_stack and from
192
- // items-changed for exactly this reason (:340-343, :277).
168
+ // The page count is half the reveal derivation, so it is fed on every rebuild —
169
+ // which is why C re-runs `update_bar_revealed` from set_stack and items-changed.
193
170
  this._barState.setPages(stackPages);
194
171
 
195
172
  const pages = viewSwitcherPagesFromStack(stackPages);
196
173
  const models = buildViewSwitcherButtons(pages, this._stack?.visibleChildIndex ?? -1, 'narrow');
197
174
 
198
- // Nodes are recreated only when the page COUNT moves. A selection change
199
- // must not replace the button the user is standing on — `selection_changed_cb`
200
- // only re-marks the existing buttons (adw-view-switcher.c:265-295), and a
201
- // wholesale replace would drop keyboard focus mid-interaction.
175
+ // Nodes are recreated only when the page COUNT moves: a selection change must not
176
+ // replace the button the user is standing on. `selection_changed_cb` only re-marks
177
+ // the existing buttons, and a wholesale replace drops keyboard focus mid-interaction.
202
178
  if (models.length !== this._nodes.length) {
203
179
  this._barEl.replaceChildren();
204
180
  this._nodes = models.map((model) => this._buildButton(model.pageIndex));
@@ -3,46 +3,28 @@
3
3
  // libadwaita keeps separate. Pages are declared as <adw-view-switcher-page>
4
4
  // children; their own children become the page body.
5
5
  //
6
- // Everything that decides WHAT to show is HEADLESS and lives in
7
- // `@gjsify/adwaita-core` (ADR 0004): the button-visibility rule, the
8
- // `image-missing` icon fallback, the mnemonic-stripped label, the badge label
9
- // and its screen-reader description, and the SELECTION — which is
10
- // `ViewSwitcherState` over the wave-1 `ViewStackState`, so this element and the
11
- // NativeScript twin share one set of guards and one set of conformance vectors
12
- // (`@gjsify/adwaita-core/conformance`). This file is the DOM half only.
6
+ // Everything that decides WHAT to show is HEADLESS, in `@gjsify/adwaita-core`
7
+ // (ADR 0004): `ViewSwitcherState` over the wave-1 `ViewStackState`, so this
8
+ // element and the NativeScript twin share one set of guards and one set of
9
+ // conformance vectors. This file is the DOM half only.
13
10
  //
14
- // What the lift fixed here, all of it C-derived:
15
- // - a page with neither title nor icon rendered an empty, clickable,
16
- // space-consuming tab; libadwaita hides the button outright
17
- // (adw-view-switcher.c:178). An EMPTY title is NOT that case — it keeps its
18
- // button, which is why `getAttribute` is no longer flattened with `?? ''`.
19
- // - a page with no icon rendered no icon at all; libadwaita substitutes
20
- // `image-missing` (adw-view-switcher-button.c:399-405).
21
- // - `active="-1"` / `active="99"` were CLAMPED into range, so an out-of-range
22
- // request silently switched pages; `adw_view_stack_pages_select_item`
23
- // refuses (adw-view-stack.c:682-684).
24
- // - `notify::visible-child` fired only for a click, never for a property or
25
- // attribute set; C notifies on every path (adw-view-stack.c:1038-1039).
26
- // - `notify::policy` was documented in this header and never dispatched.
27
- // - the declared pages were harvested with an UNSCOPED querySelectorAll, so a
28
- // nested switcher inside a page had its pages stolen by the outer one.
29
- // - the 500 ms drag-hover auto-switch did not exist
30
- // (adw-view-switcher-button.c:14, :58-96).
31
- //
32
- // Attributes:
33
- // policy — 'wide' | 'narrow' (default 'narrow'). An unrecognised value is
34
- // REJECTED and leaves the current policy alone, as `g_return_if_fail` does;
35
- // removing the attribute restores the default.
36
- // active — zero-based index of the selected PAGE. Out-of-range, negative and
37
- // fractional values are refused and the attribute is reflected back to the
11
+ // The C rules that are not visible in the DOM code:
12
+ // - a page with NEITHER title NOR icon has no button at all. An EMPTY title is not
13
+ // that case — it keeps its button, hence no `?? ''` flattening of `getAttribute`.
14
+ // - a page with no icon gets `image-missing`, not nothing.
15
+ // - an out-of-range, negative or fractional `active` is REFUSED, not clamped
16
+ // (`adw_view_stack_pages_select_item`), and the attribute is reflected back to the
38
17
  // selection that actually holds.
39
- // Page attributes: name, title, icon-name, hidden (= AdwViewStackPage:visible),
40
- // use-underline, badge-number, needs-attention.
41
- // Events:
42
- // `notify::visible-child` (CustomEvent, bubbles,
43
- // detail = { index, name, title, interactive }) on every selection change,
44
- // including the auto-pick; `interactive` is false for a model-driven one.
45
- // `notify::policy` (CustomEvent, bubbles, detail = { policy }).
18
+ // - `notify::visible-child` fires on EVERY path, property and attribute sets
19
+ // included, not only on a click.
20
+ // - a drag hovering a button auto-switches after 500 ms.
21
+ // - `policy` defaults 'narrow'; an unrecognised value is refused and leaves the
22
+ // current policy alone, removing the attribute restores the default.
23
+ //
24
+ // A page's `hidden` attribute is `AdwViewStackPage:visible` inverted. Both events
25
+ // bubble; `notify::visible-child` carries `{ index, name, title, interactive }`, where
26
+ // `interactive` is false for a model-driven change such as the auto-pick.
27
+ //
46
28
  // Reference: refs/libadwaita/src/adw-view-switcher.c (AdwViewSwitcher behaviour)
47
29
  // Reference: refs/libadwaita/src/adw-view-switcher-button.c (AdwViewSwitcherButton)
48
30
  // Reference: refs/libadwaita/src/stylesheet/widgets/_view-switcher.scss
@@ -86,29 +68,19 @@ interface PageNodes {
86
68
  /**
87
69
  * A single page. Children of `<adw-view-switcher>`; consumed at connect time.
88
70
  *
89
- * Deliberately carries NO `observedAttributes`. It used to declare six of them
90
- * and no `attributeChangedCallback`, which is a dead declaration — the browser
91
- * computes the list and has nowhere to deliver to — so changing `badge-number`,
92
- * `title`, `icon-name` or `needs-attention` after connect did nothing at all,
93
- * where C rebinds on every page `notify` (adw-view-switcher.c:184-193, :223).
94
- *
95
- * A callback could not fix it either: the switcher consumes these elements at
96
- * connect and `replaceChildren`s them away, so a detached page has no `closest`
97
- * to reach its owner through. The switcher watches them with a `MutationObserver`
98
- * instead, which keeps working after the detach and needs nothing from this class.
71
+ * Deliberately carries NO `observedAttributes` and no `attributeChangedCallback`:
72
+ * the switcher consumes these elements at connect and `replaceChildren`s them
73
+ * away, so a detached page has no `closest` to reach its owner through. The
74
+ * switcher watches them with a `MutationObserver` instead — the DOM stand-in for
75
+ * the per-page `notify` C rebinds on.
99
76
  */
100
77
  export class AdwViewSwitcherPage extends HTMLElement {}
101
78
 
102
79
  export class AdwViewSwitcher extends HTMLElement {
103
80
  /**
104
- * The dwell timer, injectable.
105
- *
106
- * It used to be `new ViewSwitcherState({ scheduler: domViewSwitcherScheduler })`
107
- * inline, which left NOWHERE to hand a clock in — so
108
- * `VIEW_SWITCHER_DRAG_VECTORS` was driven by the core suite alone, and the
109
- * conformance header claimed all three suites drove it. `scheduler` is a
110
- * property so a test can replace it before the element connects; the DOM one
111
- * is the default, exactly as before.
81
+ * The dwell timer, injectable so a test can hand in a clock and drive
82
+ * `VIEW_SWITCHER_DRAG_VECTORS` against the real element. Replace it before the
83
+ * element connects.
112
84
  */
113
85
  scheduler: ViewSwitcherScheduler = domViewSwitcherScheduler;
114
86
  private _stateInstance: ViewSwitcherState | null = null;
@@ -136,11 +108,8 @@ export class AdwViewSwitcher extends HTMLElement {
136
108
 
137
109
  constructor() {
138
110
  super();
139
- // Subscribing touches no attributes and no children, so it is legal in a
140
- // custom element constructor — unlike anything DOM-shaped.
141
111
  }
142
112
 
143
- /** The policy controlling the button layout ('wide' | 'narrow'). */
144
113
  get policy(): AdwViewSwitcherPolicy {
145
114
  return this._policy;
146
115
  }
@@ -159,7 +128,6 @@ export class AdwViewSwitcher extends HTMLElement {
159
128
  this._reflectActive();
160
129
  }
161
130
 
162
- /** Name of the selected page, `''` when nothing is selected. */
163
131
  get visibleChildName(): string {
164
132
  return this._state.selectedName;
165
133
  }
@@ -168,7 +136,6 @@ export class AdwViewSwitcher extends HTMLElement {
168
136
  this._state.selectName(name);
169
137
  }
170
138
 
171
- /** The resolved page records a bound layout can read. */
172
139
  get pages(): readonly AdwViewSwitcherPageInfo[] {
173
140
  return this._state.pages;
174
141
  }
@@ -176,7 +143,7 @@ export class AdwViewSwitcher extends HTMLElement {
176
143
  /**
177
144
  * Show or hide a page (`AdwViewStackPage:visible`). Returns whether the
178
145
  * SELECTION moved — hiding the selected page falls back to the first still
179
- * visible one (adw-view-stack.c:1061-1082).
146
+ * visible one.
180
147
  */
181
148
  setPageVisible(name: string, visible: boolean): boolean {
182
149
  const moved = this._state.setPageVisible(name, visible);
@@ -186,25 +153,21 @@ export class AdwViewSwitcher extends HTMLElement {
186
153
 
187
154
  connectedCallback() {
188
155
  if (this._initialized) {
189
- // Re-entering a document. `disconnectedCallback` drops the page
190
- // observer on the way out, and this early return used to skip
191
- // rebuilding it — so a switcher that had merely been MOVED (a
192
- // slideshow slide, a client-side route change) stopped noticing
193
- // page attribute changes for good, and did it silently.
156
+ // Re-entering a document: `disconnectedCallback` drops the page observer on
157
+ // the way out, so a switcher that was merely MOVED (a slideshow slide, a
158
+ // route change) has to get it back here or it stops noticing page changes.
194
159
  this._watchPages();
195
160
  return;
196
161
  }
197
162
 
198
- // `:scope >` rather than a subtree scan: an unscoped query let an outer
199
- // switcher adopt the pages of a nested one, since both use the same tag.
200
- // Pages come from the stack's OWN children in C (adw-view-switcher.c:203).
163
+ // `:scope >`, not a subtree scan: an unscoped query lets an outer switcher
164
+ // adopt a nested one's pages, since both use the same tag. Pages come from
165
+ // the stack's OWN children in C.
201
166
  const declared = Array.from(this.querySelectorAll(':scope > adw-view-switcher-page')) as HTMLElement[];
202
- // Read the declared selection BEFORE adopting any page: the auto-pick
203
- // reflects its own index back into this very attribute.
167
+ // Read BEFORE adopting any page: the auto-pick reflects its own index back
168
+ // into this very attribute.
204
169
  const declaredActive = this.getAttribute('active');
205
170
 
206
- // The single `viewswitcher` node holds the segmented button bar; a
207
- // sibling content area shows the selected page.
208
171
  this._barEl = document.createElement('div');
209
172
  this._barEl.className = 'adw-view-switcher-bar';
210
173
  this._barEl.setAttribute('role', 'tablist');
@@ -232,19 +195,18 @@ export class AdwViewSwitcher extends HTMLElement {
232
195
  }
233
196
 
234
197
  disconnectedCallback() {
235
- // A drag that leaves with the element would otherwise switch pages after
236
- // the widget is gone; C drops the source in dispose the same way.
198
+ // A drag that leaves with the element would otherwise switch pages after the
199
+ // widget is gone; C drops the source in dispose the same way.
237
200
  this._state.cancelDrag();
238
201
  this._pageObserver?.disconnect();
239
202
  this._pageObserver = undefined;
240
203
  }
241
204
 
242
205
  /**
243
- * Re-read every declared page and push it into the state.
244
- *
245
- * Called by `AdwViewSwitcherPage.attributeChangedCallback`, which is what
246
- * makes a runtime `badge-number` change reach the button. `setPages` keeps
247
- * the selection where it can, so this is a repaint and not a reset.
206
+ * Re-read every declared page and push it into the state. Driven by the
207
+ * `MutationObserver` in {@link _watchPages}, which is what makes a runtime
208
+ * `badge-number` change reach the button. `setPages` keeps the selection where it
209
+ * can, so this is a repaint and not a reset.
248
210
  */
249
211
  refreshPages(): void {
250
212
  if (!this._initialized) return;
@@ -253,18 +215,12 @@ export class AdwViewSwitcher extends HTMLElement {
253
215
  }
254
216
 
255
217
  /**
256
- * Watch the declared pages for attribute changes.
257
- *
258
- * A `MutationObserver` rather than an `attributeChangedCallback` on the page:
259
- * the switcher consumes those elements at connect and `replaceChildren`s them
260
- * out of the tree, so a detached page cannot find its owner — but an observer
261
- * bound to it keeps firing. This is the DOM's `notify::` on
262
- * `AdwViewStackPage`, which the C's `update_button` handler binds per page
263
- * (adw-view-switcher.c:184-193).
218
+ * Watch the declared pages for attribute changes — the DOM's `notify::` on
219
+ * `AdwViewStackPage`, which C's `update_button` binds per page. See
220
+ * {@link AdwViewSwitcherPage} for why an observer and not a callback.
264
221
  *
265
- * It delivers on a MICROTASK where GTK's notify is synchronous. That is the
266
- * DOM's contract for watching a node, it is invisible to a user, and nothing
267
- * in this element reads the button back inside the setter.
222
+ * MODIFICATION: delivers on a MICROTASK where GTK's notify is synchronous —
223
+ * invisible to a user, and nothing here reads the button back inside a setter.
268
224
  */
269
225
  private _watchPages(): void {
270
226
  this._pageObserver?.disconnect();
@@ -299,9 +255,8 @@ export class AdwViewSwitcher extends HTMLElement {
299
255
  button.className = 'adw-view-switcher-button';
300
256
  button.setAttribute('role', 'tab');
301
257
 
302
- // Both children always exist, exactly as AdwViewSwitcherButton's template
303
- // does — the icon carries the `image-missing` fallback rather than
304
- // disappearing (adw-view-switcher-button.ui).
258
+ // Both children always exist, as in AdwViewSwitcherButton's template — the icon
259
+ // carries `image-missing` rather than disappearing.
305
260
  const icon = createAdwIcon(null, 'adw-view-switcher-icon');
306
261
 
307
262
  const label = document.createElement('span');
@@ -315,16 +270,12 @@ export class AdwViewSwitcher extends HTMLElement {
315
270
  button.addEventListener('click', () => {
316
271
  this._state.setSelected(index);
317
272
  });
318
- // GtkDropControllerMotion's enter/leave, which drive TIMEOUT_EXPAND.
319
- //
320
- // The `relatedTarget` test is the whole point: this button contains an
321
- // icon and a label, and the DOM fires `dragleave` + `dragenter` when the
322
- // pointer crosses BETWEEN them. `ViewSwitcherDragSwitch.leave` clears the
323
- // dwell timer and `enter` re-arms a fresh 500ms, so a drag that merely
324
- // slid from the icon to the label restarted the countdown and the page
325
- // never switched. C has one `GtkDropControllerMotion` per BUTTON
326
- // (adw-view-switcher-button.c:79-96), which has no such internal edges —
327
- // `relatedTarget` containment is that same "did we really leave" test.
273
+ // GtkDropControllerMotion's enter/leave, which drive TIMEOUT_EXPAND. The
274
+ // `relatedTarget` containment test is load-bearing: the button contains an icon
275
+ // and a label, and the DOM fires `dragleave` + `dragenter` when the pointer
276
+ // crosses BETWEEN them. Since `leave` clears the dwell timer and `enter` re-arms
277
+ // a fresh 500 ms, an untested crossing restarts the countdown forever. C's one
278
+ // `GtkDropControllerMotion` per BUTTON has no such internal edges.
328
279
  button.addEventListener('dragenter', (event) => {
329
280
  if (button.contains((event as DragEvent).relatedTarget as Node | null)) return;
330
281
  this._state.dragEnter(index);
@@ -344,9 +295,9 @@ export class AdwViewSwitcher extends HTMLElement {
344
295
 
345
296
  private _readPolicyAttr(): AdwViewSwitcherPolicy {
346
297
  const raw = this.getAttribute('policy');
347
- // A removed attribute falls back to the C default; an unrecognised value
348
- // is refused and keeps the current policy (adw_view_switcher_set_policy
349
- // is reached only through the enum, which rejects garbage).
298
+ // A removed attribute falls back to the C default; an unrecognised value is
299
+ // refused and keeps the current policy, since `adw_view_switcher_set_policy` is
300
+ // reached only through the enum, which rejects garbage.
350
301
  if (raw === null) return 'narrow';
351
302
  return isViewSwitcherPolicy(raw) ? raw : this._policy;
352
303
  }
@@ -369,8 +320,7 @@ export class AdwViewSwitcher extends HTMLElement {
369
320
  private _render(): void {
370
321
  if (!this._initialized) return;
371
322
 
372
- // The single `viewswitcher` node carries the policy style class
373
- // (adw-view-switcher.c:539-545).
323
+ // The single `viewswitcher` node carries the policy style class.
374
324
  for (const policy of VIEW_SWITCHER_POLICIES) this.classList.toggle(policy, policy === this._policy);
375
325
 
376
326
  const models = buildViewSwitcherButtons(this._state.pages, this._state.selected, this._policy);
@@ -9,11 +9,11 @@
9
9
  // `@gjsify/adwaita-nativescript` composes the same state, so both ports share one
10
10
  // behaviour. Three rules from adw-window-title.c that neither renderer had:
11
11
  // - the TITLE label hides when the title is empty (C:207-208; the template even
12
- // starts it `visible=False`, adw-window-title.ui:15). Only the subtitle was
12
+ // starts it `visible=False`). Only the subtitle was
13
13
  // ever hidden here, so a header bar with a subtitle and no title reserved a
14
14
  // blank title line above it.
15
- // - setting the value it already has returns early (C:203-204, :244-245).
16
- // - a real change notifies (C:210, :251).
15
+ // - setting the value it already has returns early (C:203-204).
16
+ // - a real change notifies (C:210).
17
17
  //
18
18
  // Reference: refs/adwaita-web/adwaita-web/docs/widgets/windowtitle.md
19
19
  // Reference: refs/libadwaita/src/adw-window-title.c, adw-window-title.ui
@@ -56,7 +56,7 @@ export class AdwWindowTitle extends HTMLElement {
56
56
  if (!this._initialized) return;
57
57
  const changed = name === 'title' ? this._state.setTitle(value) : this._state.setSubtitle(value);
58
58
  // The early-return is the whole point: re-setting the same value paints
59
- // nothing and notifies nothing (C:203-204, :244-245).
59
+ // nothing and notifies nothing (C:203-204).
60
60
  if (!changed) return;
61
61
  this._render();
62
62
  this.dispatchEvent(