@gjsify/adwaita-web 0.31.0 → 0.33.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 (183) hide show
  1. package/dist/adwaita-web.css +835 -299
  2. package/dist/adwaita-web.css.map +1 -1
  3. package/lib/types/adw-about-dialog.spec.d.ts +1 -0
  4. package/lib/types/adw-action-rows.spec.d.ts +1 -0
  5. package/lib/types/adw-avatar.spec.d.ts +1 -0
  6. package/lib/types/adw-banner.spec.d.ts +1 -0
  7. package/lib/types/adw-button-content.spec.d.ts +1 -0
  8. package/lib/types/adw-carousel.spec.d.ts +1 -0
  9. package/lib/types/adw-checks.spec.d.ts +1 -0
  10. package/lib/types/adw-header-bar.spec.d.ts +1 -0
  11. package/lib/types/adw-icon.spec.d.ts +1 -0
  12. package/lib/types/adw-progress-bar.spec.d.ts +1 -0
  13. package/lib/types/adw-sidebar.spec.d.ts +1 -0
  14. package/lib/types/adw-switch.spec.d.ts +1 -0
  15. package/lib/types/adw-wrap-box.spec.d.ts +1 -0
  16. package/lib/types/bottom-sheet.spec.d.ts +1 -0
  17. package/lib/types/breakpoints.d.ts +22 -0
  18. package/lib/types/breakpoints.spec.d.ts +1 -0
  19. package/lib/types/chrome.spec.d.ts +1 -0
  20. package/lib/types/elements/adw-about-dialog.d.ts +47 -8
  21. package/lib/types/elements/adw-action-row.d.ts +35 -0
  22. package/lib/types/elements/adw-avatar.d.ts +11 -0
  23. package/lib/types/elements/adw-bottom-sheet.d.ts +33 -4
  24. package/lib/types/elements/adw-button-content.d.ts +12 -0
  25. package/lib/types/elements/adw-button-row.d.ts +10 -1
  26. package/lib/types/elements/adw-carousel.d.ts +111 -8
  27. package/lib/types/elements/adw-checks.d.ts +57 -0
  28. package/lib/types/elements/adw-clamp.d.ts +4 -1
  29. package/lib/types/elements/adw-combo-row.d.ts +10 -0
  30. package/lib/types/elements/adw-data-grid.d.ts +11 -31
  31. package/lib/types/elements/adw-drop-down.d.ts +17 -20
  32. package/lib/types/elements/adw-entry-row.d.ts +93 -3
  33. package/lib/types/elements/adw-expander-row.d.ts +7 -1
  34. package/lib/types/elements/adw-header-bar.d.ts +15 -0
  35. package/lib/types/elements/adw-icon.d.ts +25 -0
  36. package/lib/types/elements/adw-inline-view-switcher.d.ts +73 -11
  37. package/lib/types/elements/adw-menu-button.d.ts +6 -6
  38. package/lib/types/elements/adw-navigation-split-view.d.ts +48 -0
  39. package/lib/types/elements/adw-navigation-view.d.ts +63 -20
  40. package/lib/types/elements/adw-overlay-split-view.d.ts +94 -0
  41. package/lib/types/elements/adw-password-entry-row.d.ts +19 -10
  42. package/lib/types/elements/adw-popover.d.ts +58 -0
  43. package/lib/types/elements/adw-preferences-dialog.d.ts +57 -2
  44. package/lib/types/elements/adw-preferences-group.d.ts +31 -0
  45. package/lib/types/elements/adw-progress-bar.d.ts +38 -0
  46. package/lib/types/elements/adw-sidebar.d.ts +37 -5
  47. package/lib/types/elements/adw-spinner.d.ts +23 -0
  48. package/lib/types/elements/adw-split-button.d.ts +53 -7
  49. package/lib/types/elements/adw-switch-row.d.ts +11 -2
  50. package/lib/types/elements/adw-switch.d.ts +15 -0
  51. package/lib/types/elements/adw-tab-view.d.ts +129 -12
  52. package/lib/types/elements/adw-toolbar-view.d.ts +5 -0
  53. package/lib/types/elements/adw-view-stack.d.ts +39 -19
  54. package/lib/types/elements/adw-view-switcher-bar.d.ts +25 -7
  55. package/lib/types/elements/adw-view-switcher.d.ts +76 -13
  56. package/lib/types/elements/adw-window-title.d.ts +3 -1
  57. package/lib/types/elements/adw-wrap-box.d.ts +58 -3
  58. package/lib/types/elements/view-switcher-dom.d.ts +44 -0
  59. package/lib/types/entry-rows.spec.d.ts +1 -0
  60. package/lib/types/index.d.ts +7 -0
  61. package/lib/types/navigation-view.spec.d.ts +1 -0
  62. package/lib/types/preferences-search.d.ts +20 -0
  63. package/lib/types/preferences.spec.d.ts +1 -0
  64. package/lib/types/split-button.spec.d.ts +1 -0
  65. package/lib/types/split-views.spec.d.ts +1 -0
  66. package/lib/types/styles.generated.d.ts +1 -1
  67. package/lib/types/tab-view.spec.d.ts +1 -0
  68. package/lib/types/view-stack.spec.d.ts +1 -0
  69. package/lib/types/view-switcher.spec.d.ts +1 -0
  70. package/package.json +6 -6
  71. package/scss/_banner.scss +13 -0
  72. package/scss/_bottom_sheet.scss +5 -6
  73. package/scss/_button.scss +3 -26
  74. package/scss/_button_content.scss +28 -0
  75. package/scss/_checks.scss +228 -0
  76. package/scss/_clamp.scss +9 -3
  77. package/scss/_combo_row.scss +19 -0
  78. package/scss/_data_grid.scss +15 -7
  79. package/scss/_drop_down.scss +12 -55
  80. package/scss/_expander_row.scss +5 -50
  81. package/scss/_icon.scss +51 -0
  82. package/scss/_icons.generated.scss +6 -0
  83. package/scss/_inline_view_switcher.scss +34 -0
  84. package/scss/_labels.scss +135 -0
  85. package/scss/_menu_button.scss +10 -70
  86. package/scss/_navigation_split_view.scss +47 -8
  87. package/scss/_overlay_split_view.scss +117 -32
  88. package/scss/_popover.scss +186 -0
  89. package/scss/_preferences.scss +29 -0
  90. package/scss/_preferences_dialog.scss +17 -0
  91. package/scss/_progress_bar.scss +161 -0
  92. package/scss/_reset.scss +18 -7
  93. package/scss/_spinner.scss +37 -19
  94. package/scss/_split_button.scss +6 -50
  95. package/scss/_switch.scss +90 -0
  96. package/scss/_switch_row.scss +6 -52
  97. package/scss/_tab_view.scss +63 -0
  98. package/scss/_theme.scss +10 -0
  99. package/scss/_toolbar_view.scss +74 -0
  100. package/scss/_variables.scss +34 -0
  101. package/scss/_view_switcher.scss +40 -0
  102. package/scss/_view_switcher_bar.scss +37 -0
  103. package/scss/_wrap_box.scss +65 -6
  104. package/scss/adwaita-skin.scss +23 -0
  105. package/src/adw-about-dialog.spec.ts +350 -0
  106. package/src/adw-action-rows.spec.ts +342 -0
  107. package/src/adw-avatar.spec.ts +176 -0
  108. package/src/adw-banner.spec.ts +152 -0
  109. package/src/adw-button-content.spec.ts +207 -0
  110. package/src/adw-button-row.spec.ts +12 -6
  111. package/src/adw-carousel.spec.ts +389 -0
  112. package/src/adw-checks.spec.ts +368 -0
  113. package/src/adw-drop-down.spec.ts +123 -0
  114. package/src/adw-header-bar.spec.ts +83 -0
  115. package/src/adw-icon.spec.ts +167 -0
  116. package/src/adw-progress-bar.spec.ts +173 -0
  117. package/src/adw-row-state.spec.ts +49 -0
  118. package/src/adw-sidebar.spec.ts +332 -0
  119. package/src/adw-switch.spec.ts +242 -0
  120. package/src/adw-tab-view.spec.ts +12 -3
  121. package/src/adw-view-switcher-bar.spec.ts +61 -13
  122. package/src/adw-wrap-box.spec.ts +341 -0
  123. package/src/bottom-sheet.spec.ts +325 -0
  124. package/src/breakpoints.spec.ts +147 -0
  125. package/src/breakpoints.ts +67 -0
  126. package/src/chrome.spec.ts +400 -0
  127. package/src/elements/adw-about-dialog.ts +223 -163
  128. package/src/elements/adw-action-row.ts +131 -11
  129. package/src/elements/adw-avatar.ts +48 -54
  130. package/src/elements/adw-banner.ts +46 -10
  131. package/src/elements/adw-bottom-sheet.ts +145 -60
  132. package/src/elements/adw-button-content.ts +107 -33
  133. package/src/elements/adw-button-row.ts +61 -37
  134. package/src/elements/adw-button.ts +5 -7
  135. package/src/elements/adw-carousel.ts +467 -105
  136. package/src/elements/adw-checks.ts +299 -0
  137. package/src/elements/adw-clamp.ts +80 -10
  138. package/src/elements/adw-combo-row.ts +32 -7
  139. package/src/elements/adw-data-grid.ts +43 -91
  140. package/src/elements/adw-drop-down.ts +117 -128
  141. package/src/elements/adw-entry-row.ts +404 -36
  142. package/src/elements/adw-expander-row.ts +61 -29
  143. package/src/elements/adw-header-bar.ts +31 -6
  144. package/src/elements/adw-icon.ts +148 -0
  145. package/src/elements/adw-inline-view-switcher.ts +325 -131
  146. package/src/elements/adw-menu-button.ts +93 -70
  147. package/src/elements/adw-navigation-split-view.ts +220 -14
  148. package/src/elements/adw-navigation-view.ts +286 -148
  149. package/src/elements/adw-overlay-split-view.ts +536 -28
  150. package/src/elements/adw-password-entry-row.ts +111 -89
  151. package/src/elements/adw-popover.ts +283 -0
  152. package/src/elements/adw-preferences-dialog.ts +120 -21
  153. package/src/elements/adw-preferences-group.ts +128 -22
  154. package/src/elements/adw-progress-bar.ts +189 -0
  155. package/src/elements/adw-sidebar.ts +301 -142
  156. package/src/elements/adw-spin-row.ts +11 -5
  157. package/src/elements/adw-spinner.ts +168 -9
  158. package/src/elements/adw-split-button.ts +371 -92
  159. package/src/elements/adw-status-page.ts +9 -9
  160. package/src/elements/adw-switch-row.ts +100 -30
  161. package/src/elements/adw-switch.ts +129 -0
  162. package/src/elements/adw-tab-view.ts +700 -129
  163. package/src/elements/adw-toggle-group.ts +5 -7
  164. package/src/elements/adw-toolbar-view.ts +84 -21
  165. package/src/elements/adw-view-stack.ts +183 -85
  166. package/src/elements/adw-view-switcher-bar.ts +171 -81
  167. package/src/elements/adw-view-switcher.ts +328 -117
  168. package/src/elements/adw-window-title.ts +44 -11
  169. package/src/elements/adw-wrap-box.ts +329 -78
  170. package/src/elements/view-switcher-dom.ts +108 -0
  171. package/src/entry-rows.spec.ts +391 -0
  172. package/src/index.ts +23 -0
  173. package/src/navigation-view.spec.ts +435 -0
  174. package/src/preferences-search.ts +135 -0
  175. package/src/preferences.spec.ts +250 -0
  176. package/src/source-view/adw-source-view.ts +2 -5
  177. package/src/split-button.spec.ts +470 -0
  178. package/src/split-views.spec.ts +893 -0
  179. package/src/styles.generated.ts +1 -1
  180. package/src/tab-view.spec.ts +451 -0
  181. package/src/test.browser.mts +49 -0
  182. package/src/view-stack.spec.ts +272 -0
  183. package/src/view-switcher.spec.ts +665 -0
@@ -1,42 +1,70 @@
1
1
  // <adw-view-switcher-bar> — The narrow BOTTOM bar of icon-over-label buttons
2
2
  // bound to an <adw-view-stack>, the web counterpart of Adw.ViewSwitcherBar. It
3
- // owns no pages of its own: it reads the bound stack's `pages`, renders one
4
- // homogeneous icon-over-label button per page, and is two-way synced to the
5
- // stack's visible child — tapping a button sets the stack's visible page, and a
6
- // change to the stack (from anywhere) re-highlights the matching button. The
7
- // active button's label is accent-coloured (via the `.active` CSS class).
3
+ // owns no pages: it reads the bound stack's `pages`, renders one homogeneous
4
+ // button per page, and is two-way synced to the stack's visible child.
8
5
  //
9
- // A `revealed` flag lets an adaptive layout flip the bar on/off: shown on narrow
10
- // screens, hidden on wide ones where a header-bar <adw-view-switcher> takes over
11
- // (mirrors Adw.ViewSwitcherBar `reveal` driven by an Adw.Breakpoint).
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.
13
+ //
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
19
  //
13
20
  // Attributes:
14
- // stack — the id of the <adw-view-stack> to drive (like <label for>).
15
- // revealed — boolean; when absent the bar is hidden (matches `reveal`).
16
- // Properties (mirroring Adw.ViewSwitcherBar):
17
- // stack — the bound AdwViewStack element (get/set; set also accepts an id).
18
- // revealed — whether the bar is shown (get/set).
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.
19
29
  // Methods:
20
30
  // setStack(stack) — bind to a stack element (or null); rebuilds the buttons.
21
- // refresh() — rebuild the buttons after late page adds to the stack.
31
+ // refresh() — rebuild after pages were added to the bound stack without
32
+ // changing its selection (see the note in `_rebuild`).
22
33
  // Reference: refs/libadwaita/src/adw-view-switcher-bar.c (AdwViewSwitcherBar)
23
- // Reference: refs/libadwaita/src/stylesheet/widgets/_view-switcher.scss (.narrow policy)
34
+ // Reference: refs/libadwaita/src/adw-view-switcher.c (the switcher it embeds)
24
35
  // Reference: packages/nativescript-bridge/adwaita/src/widgets/adw-view-switcher-bar.ts (NS twin)
25
36
  // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
26
37
  // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
27
38
 
39
+ import { ViewSwitcherBarState, buildViewSwitcherButtons, viewSwitcherPagesFromStack } from '@gjsify/adwaita-core';
40
+
28
41
  import type { AdwViewStack } from './adw-view-stack.js';
42
+ import { applyDescription, applyIndicator, applySwitcherIcon } from './view-switcher-dom.js';
43
+ import { type AdwIcon, createAdwIcon } from './adw-icon.js';
44
+
45
+ /** The DOM nodes one bar button owns — the bar paints the derived model onto them. */
46
+ interface BarButtonNodes {
47
+ button: HTMLButtonElement;
48
+ icon: AdwIcon;
49
+ label: HTMLSpanElement;
50
+ indicator: HTMLSpanElement;
51
+ }
29
52
 
30
53
  export class AdwViewSwitcherBar extends HTMLElement {
54
+ private readonly _barState = new ViewSwitcherBarState();
31
55
  private _barEl!: HTMLDivElement;
32
- private _buttons: HTMLButtonElement[] = [];
56
+ private _nodes: BarButtonNodes[] = [];
33
57
  private _stack: AdwViewStack | null = null;
34
- private _revealed = false;
35
58
  private _initialized = false;
36
- private _onStackNotify = (): void => this._syncActive();
59
+ private readonly _onStackNotify = (): void => this._rebuild();
37
60
 
38
61
  static get observedAttributes() {
39
- return ['stack', 'revealed'];
62
+ return ['stack', 'reveal', 'revealed'];
63
+ }
64
+
65
+ constructor() {
66
+ super();
67
+ this._barState.subscribe(() => this._applyRevealed());
40
68
  }
41
69
 
42
70
  /** The bound stack element, or null when unbound. */
@@ -48,14 +76,26 @@ export class AdwViewSwitcherBar extends HTMLElement {
48
76
  this.setStack(this._resolveStack(value));
49
77
  }
50
78
 
51
- /** Whether the bar is revealed (shown). Hidden when false. */
52
- get revealed(): boolean {
53
- return this._revealed;
79
+ /** Whether the layout asked for the bar — `Adw.ViewSwitcherBar:reveal`. */
80
+ get reveal(): boolean {
81
+ return this._barState.reveal;
54
82
  }
55
83
 
56
- set revealed(value: boolean) {
57
- if (value) this.setAttribute('revealed', '');
58
- else this.removeAttribute('revealed');
84
+ set reveal(value: boolean) {
85
+ if (value) this.setAttribute('reveal', '');
86
+ else {
87
+ this.removeAttribute('reveal');
88
+ this.removeAttribute('revealed');
89
+ }
90
+ }
91
+
92
+ /**
93
+ * 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).
95
+ * Write {@link reveal} to make the request.
96
+ */
97
+ get revealed(): boolean {
98
+ return this._barState.revealed;
59
99
  }
60
100
 
61
101
  connectedCallback() {
@@ -66,25 +106,31 @@ export class AdwViewSwitcherBar extends HTMLElement {
66
106
  this._barEl.setAttribute('role', 'tablist');
67
107
  this.replaceChildren(this._barEl);
68
108
  }
69
- this._revealed = this.hasAttribute('revealed');
70
- this._applyRevealed();
71
- // Resolve the `stack` id ref now that the element is in the document — the
72
- // referenced stack is normally earlier in the DOM (content above a bottom
73
- // bar), so its pages are ready.
109
+ this._barState.setReveal(this._readReveal());
110
+
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.
74
114
  const ref = this.getAttribute('stack');
75
- if (ref && !this._stack) this.setStack(this._resolveStack(ref));
76
- else this._syncActive();
115
+ if (ref && !this._stack) {
116
+ this.setStack(this._resolveStack(ref));
117
+ return;
118
+ }
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.
122
+ this._bindStack();
123
+ this._rebuild();
77
124
  }
78
125
 
79
126
  disconnectedCallback() {
80
- if (this._stack) this._stack.removeEventListener('notify::visible-child', this._onStackNotify);
127
+ this._unbindStack();
81
128
  }
82
129
 
83
130
  attributeChangedCallback(name: string, _old: string | null, value: string | null) {
84
131
  if (!this._initialized) return;
85
- if (name === 'revealed') {
86
- this._revealed = this.hasAttribute('revealed');
87
- this._applyRevealed();
132
+ if (name === 'reveal' || name === 'revealed') {
133
+ this._barState.setReveal(this._readReveal());
88
134
  return;
89
135
  }
90
136
  if (name === 'stack') {
@@ -94,74 +140,118 @@ export class AdwViewSwitcherBar extends HTMLElement {
94
140
 
95
141
  /** Bind to a stack element (or null). Unbinds the previous one and rebuilds. */
96
142
  setStack(stack: AdwViewStack | null): void {
97
- if (this._stack) this._stack.removeEventListener('notify::visible-child', this._onStackNotify);
143
+ this._unbindStack();
98
144
  this._stack = stack;
99
- if (stack) stack.addEventListener('notify::visible-child', this._onStackNotify);
145
+ this._bindStack();
100
146
  this._rebuild();
101
147
  }
102
148
 
103
- /** Rebuild the buttons — call after adding pages to the bound stack. */
149
+ /**
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).
157
+ */
104
158
  refresh(): void {
105
159
  this._rebuild();
106
160
  }
107
161
 
162
+ private _readReveal(): boolean {
163
+ return this.hasAttribute('reveal') || this.hasAttribute('revealed');
164
+ }
165
+
166
+ private _bindStack(): void {
167
+ 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.
172
+ this._stack?.addEventListener('items-changed', this._onStackNotify);
173
+ }
174
+
175
+ private _unbindStack(): void {
176
+ this._stack?.removeEventListener('notify::visible-child', this._onStackNotify);
177
+ this._stack?.removeEventListener('items-changed', this._onStackNotify);
178
+ }
179
+
108
180
  private _resolveStack(value: AdwViewStack | string | null): AdwViewStack | null {
109
181
  if (value === null) return null;
110
- if (typeof value === 'string') {
111
- return (document.getElementById(value) as AdwViewStack | null) ?? null;
112
- }
182
+ if (typeof value === 'string') return (document.getElementById(value) as AdwViewStack | null) ?? null;
113
183
  return value;
114
184
  }
115
185
 
116
186
  private _rebuild(): void {
117
187
  if (!this._initialized) return;
118
- this._barEl.replaceChildren();
119
- this._buttons = [];
120
- const pages = this._stack?.pages ?? [];
121
-
122
- pages.forEach((page, index) => {
123
- const button = document.createElement('button');
124
- button.type = 'button';
125
- button.className = 'adw-view-switcher-bar-button';
126
- button.setAttribute('role', 'tab');
127
-
128
- const iconEl = document.createElement('span');
129
- iconEl.className = `adw-icon adw-view-switcher-bar-icon${page.icon ? ` adw-icon--${page.icon}` : ''}`;
130
- iconEl.setAttribute('aria-hidden', 'true'); // decorative (mask-image only)
131
- iconEl.hidden = page.icon.length === 0;
132
-
133
- const labelEl = document.createElement('span');
134
- labelEl.className = 'adw-view-switcher-bar-label';
135
- labelEl.textContent = page.title;
136
- labelEl.hidden = page.title.length === 0;
137
-
138
- button.append(iconEl, labelEl);
139
- // An icon-only button (no title) needs an accessible name.
140
- if (page.icon && !page.title) button.setAttribute('aria-label', page.icon);
141
-
142
- button.addEventListener('click', () => {
143
- if (this._stack) this._stack.visibleChildIndex = index;
144
- });
145
-
146
- this._barEl.appendChild(button);
147
- this._buttons.push(button);
188
+
189
+ 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).
193
+ this._barState.setPages(stackPages);
194
+
195
+ const pages = viewSwitcherPagesFromStack(stackPages);
196
+ const models = buildViewSwitcherButtons(pages, this._stack?.visibleChildIndex ?? -1, 'narrow');
197
+
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.
202
+ if (models.length !== this._nodes.length) {
203
+ this._barEl.replaceChildren();
204
+ this._nodes = models.map((model) => this._buildButton(model.pageIndex));
205
+ this._barEl.append(...this._nodes.map((nodes) => nodes.button));
206
+ }
207
+
208
+ models.forEach((model, index) => {
209
+ const nodes = this._nodes[index];
210
+ if (!nodes) return;
211
+
212
+ nodes.button.hidden = !model.visible;
213
+ nodes.button.classList.toggle('active', model.selected);
214
+ nodes.button.setAttribute('aria-selected', String(model.selected));
215
+ nodes.button.tabIndex = model.selected ? 0 : -1;
216
+
217
+ applySwitcherIcon(nodes.icon, model.iconName);
218
+ nodes.label.textContent = model.label;
219
+ nodes.label.hidden = model.label.length === 0;
220
+ applyIndicator(nodes.indicator, model.badgeLabel, model.needsAttention);
221
+ applyDescription(nodes.button, model.description);
222
+
223
+ if (model.label) nodes.button.removeAttribute('aria-label');
224
+ else nodes.button.setAttribute('aria-label', model.iconName);
148
225
  });
149
226
 
150
- this._syncActive();
227
+ this._applyRevealed();
151
228
  }
152
229
 
153
- private _syncActive(): void {
154
- const selected = this._stack?.visibleChildIndex ?? -1;
155
- this._buttons.forEach((button, index) => {
156
- const isActive = index === selected;
157
- button.classList.toggle('active', isActive);
158
- button.setAttribute('aria-selected', String(isActive));
159
- button.tabIndex = isActive ? 0 : -1;
230
+ private _buildButton(pageIndex: number): BarButtonNodes {
231
+ const button = document.createElement('button');
232
+ button.type = 'button';
233
+ button.className = 'adw-view-switcher-bar-button';
234
+ button.setAttribute('role', 'tab');
235
+
236
+ const icon = createAdwIcon(null, 'adw-view-switcher-bar-icon');
237
+
238
+ const label = document.createElement('span');
239
+ label.className = 'adw-view-switcher-bar-label';
240
+
241
+ const indicator = document.createElement('span');
242
+ indicator.className = 'adw-view-switcher-bar-indicator';
243
+ indicator.setAttribute('aria-hidden', 'true'); // announced via aria-description
244
+
245
+ button.append(icon, label, indicator);
246
+ button.addEventListener('click', () => {
247
+ if (this._stack) this._stack.visibleChildIndex = pageIndex;
160
248
  });
249
+
250
+ return { button, icon, label, indicator };
161
251
  }
162
252
 
163
253
  private _applyRevealed(): void {
164
- this.hidden = !this._revealed;
254
+ this.hidden = !this._barState.revealed;
165
255
  }
166
256
  }
167
257