@gjsify/adwaita-web 0.31.0 → 0.32.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 (176) hide show
  1. package/dist/adwaita-web.css +786 -272
  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-data-grid.d.ts +11 -31
  30. package/lib/types/elements/adw-drop-down.d.ts +17 -20
  31. package/lib/types/elements/adw-entry-row.d.ts +93 -3
  32. package/lib/types/elements/adw-expander-row.d.ts +7 -1
  33. package/lib/types/elements/adw-header-bar.d.ts +15 -0
  34. package/lib/types/elements/adw-icon.d.ts +25 -0
  35. package/lib/types/elements/adw-inline-view-switcher.d.ts +37 -11
  36. package/lib/types/elements/adw-menu-button.d.ts +6 -6
  37. package/lib/types/elements/adw-navigation-split-view.d.ts +6 -0
  38. package/lib/types/elements/adw-navigation-view.d.ts +63 -20
  39. package/lib/types/elements/adw-overlay-split-view.d.ts +38 -0
  40. package/lib/types/elements/adw-password-entry-row.d.ts +19 -10
  41. package/lib/types/elements/adw-popover.d.ts +58 -0
  42. package/lib/types/elements/adw-preferences-dialog.d.ts +57 -2
  43. package/lib/types/elements/adw-preferences-group.d.ts +31 -0
  44. package/lib/types/elements/adw-progress-bar.d.ts +38 -0
  45. package/lib/types/elements/adw-sidebar.d.ts +37 -5
  46. package/lib/types/elements/adw-split-button.d.ts +53 -7
  47. package/lib/types/elements/adw-switch-row.d.ts +11 -2
  48. package/lib/types/elements/adw-switch.d.ts +15 -0
  49. package/lib/types/elements/adw-tab-view.d.ts +129 -12
  50. package/lib/types/elements/adw-toolbar-view.d.ts +5 -0
  51. package/lib/types/elements/adw-view-stack.d.ts +27 -19
  52. package/lib/types/elements/adw-view-switcher-bar.d.ts +23 -7
  53. package/lib/types/elements/adw-view-switcher.d.ts +22 -11
  54. package/lib/types/elements/adw-window-title.d.ts +3 -1
  55. package/lib/types/elements/view-switcher-dom.d.ts +44 -0
  56. package/lib/types/entry-rows.spec.d.ts +1 -0
  57. package/lib/types/index.d.ts +7 -0
  58. package/lib/types/navigation-view.spec.d.ts +1 -0
  59. package/lib/types/preferences-search.d.ts +20 -0
  60. package/lib/types/preferences.spec.d.ts +1 -0
  61. package/lib/types/split-button.spec.d.ts +1 -0
  62. package/lib/types/split-views.spec.d.ts +1 -0
  63. package/lib/types/styles.generated.d.ts +1 -1
  64. package/lib/types/tab-view.spec.d.ts +1 -0
  65. package/lib/types/view-stack.spec.d.ts +1 -0
  66. package/lib/types/view-switcher.spec.d.ts +1 -0
  67. package/package.json +6 -6
  68. package/scss/_banner.scss +13 -0
  69. package/scss/_bottom_sheet.scss +5 -6
  70. package/scss/_button.scss +3 -26
  71. package/scss/_button_content.scss +28 -0
  72. package/scss/_checks.scss +228 -0
  73. package/scss/_clamp.scss +9 -3
  74. package/scss/_data_grid.scss +15 -7
  75. package/scss/_drop_down.scss +12 -55
  76. package/scss/_expander_row.scss +5 -50
  77. package/scss/_icon.scss +51 -0
  78. package/scss/_icons.generated.scss +6 -0
  79. package/scss/_inline_view_switcher.scss +34 -0
  80. package/scss/_labels.scss +135 -0
  81. package/scss/_menu_button.scss +10 -70
  82. package/scss/_popover.scss +186 -0
  83. package/scss/_preferences.scss +29 -0
  84. package/scss/_preferences_dialog.scss +17 -0
  85. package/scss/_progress_bar.scss +161 -0
  86. package/scss/_reset.scss +18 -7
  87. package/scss/_spinner.scss +7 -3
  88. package/scss/_split_button.scss +6 -50
  89. package/scss/_switch.scss +90 -0
  90. package/scss/_switch_row.scss +6 -52
  91. package/scss/_tab_view.scss +64 -0
  92. package/scss/_theme.scss +10 -0
  93. package/scss/_toolbar_view.scss +74 -0
  94. package/scss/_variables.scss +34 -0
  95. package/scss/_view_switcher.scss +40 -0
  96. package/scss/_view_switcher_bar.scss +37 -0
  97. package/scss/_wrap_box.scss +56 -6
  98. package/scss/adwaita-skin.scss +23 -0
  99. package/src/adw-about-dialog.spec.ts +350 -0
  100. package/src/adw-action-rows.spec.ts +342 -0
  101. package/src/adw-avatar.spec.ts +139 -0
  102. package/src/adw-banner.spec.ts +152 -0
  103. package/src/adw-button-content.spec.ts +183 -0
  104. package/src/adw-button-row.spec.ts +12 -6
  105. package/src/adw-carousel.spec.ts +389 -0
  106. package/src/adw-checks.spec.ts +368 -0
  107. package/src/adw-drop-down.spec.ts +123 -0
  108. package/src/adw-header-bar.spec.ts +83 -0
  109. package/src/adw-icon.spec.ts +167 -0
  110. package/src/adw-progress-bar.spec.ts +173 -0
  111. package/src/adw-sidebar.spec.ts +332 -0
  112. package/src/adw-switch.spec.ts +242 -0
  113. package/src/adw-tab-view.spec.ts +12 -3
  114. package/src/adw-view-switcher-bar.spec.ts +61 -13
  115. package/src/adw-wrap-box.spec.ts +194 -0
  116. package/src/bottom-sheet.spec.ts +325 -0
  117. package/src/breakpoints.spec.ts +147 -0
  118. package/src/breakpoints.ts +67 -0
  119. package/src/chrome.spec.ts +301 -0
  120. package/src/elements/adw-about-dialog.ts +223 -163
  121. package/src/elements/adw-action-row.ts +120 -10
  122. package/src/elements/adw-avatar.ts +48 -54
  123. package/src/elements/adw-banner.ts +46 -10
  124. package/src/elements/adw-bottom-sheet.ts +145 -60
  125. package/src/elements/adw-button-content.ts +107 -33
  126. package/src/elements/adw-button-row.ts +61 -37
  127. package/src/elements/adw-button.ts +5 -7
  128. package/src/elements/adw-carousel.ts +467 -105
  129. package/src/elements/adw-checks.ts +299 -0
  130. package/src/elements/adw-clamp.ts +80 -10
  131. package/src/elements/adw-combo-row.ts +15 -7
  132. package/src/elements/adw-data-grid.ts +43 -91
  133. package/src/elements/adw-drop-down.ts +117 -128
  134. package/src/elements/adw-entry-row.ts +404 -36
  135. package/src/elements/adw-expander-row.ts +61 -29
  136. package/src/elements/adw-header-bar.ts +31 -6
  137. package/src/elements/adw-icon.ts +148 -0
  138. package/src/elements/adw-inline-view-switcher.ts +240 -129
  139. package/src/elements/adw-menu-button.ts +93 -70
  140. package/src/elements/adw-navigation-split-view.ts +48 -7
  141. package/src/elements/adw-navigation-view.ts +286 -148
  142. package/src/elements/adw-overlay-split-view.ts +164 -14
  143. package/src/elements/adw-password-entry-row.ts +111 -89
  144. package/src/elements/adw-popover.ts +283 -0
  145. package/src/elements/adw-preferences-dialog.ts +120 -21
  146. package/src/elements/adw-preferences-group.ts +128 -22
  147. package/src/elements/adw-progress-bar.ts +189 -0
  148. package/src/elements/adw-sidebar.ts +301 -142
  149. package/src/elements/adw-spin-row.ts +11 -5
  150. package/src/elements/adw-spinner.ts +23 -8
  151. package/src/elements/adw-split-button.ts +371 -92
  152. package/src/elements/adw-status-page.ts +9 -9
  153. package/src/elements/adw-switch-row.ts +100 -30
  154. package/src/elements/adw-switch.ts +129 -0
  155. package/src/elements/adw-tab-view.ts +682 -129
  156. package/src/elements/adw-toggle-group.ts +5 -7
  157. package/src/elements/adw-toolbar-view.ts +84 -21
  158. package/src/elements/adw-view-stack.ts +162 -85
  159. package/src/elements/adw-view-switcher-bar.ts +163 -81
  160. package/src/elements/adw-view-switcher.ts +231 -112
  161. package/src/elements/adw-window-title.ts +44 -11
  162. package/src/elements/adw-wrap-box.ts +131 -35
  163. package/src/elements/view-switcher-dom.ts +108 -0
  164. package/src/entry-rows.spec.ts +391 -0
  165. package/src/index.ts +23 -0
  166. package/src/navigation-view.spec.ts +435 -0
  167. package/src/preferences-search.ts +135 -0
  168. package/src/preferences.spec.ts +250 -0
  169. package/src/source-view/adw-source-view.ts +2 -5
  170. package/src/split-button.spec.ts +470 -0
  171. package/src/split-views.spec.ts +309 -0
  172. package/src/styles.generated.ts +1 -1
  173. package/src/tab-view.spec.ts +451 -0
  174. package/src/test.browser.mts +49 -0
  175. package/src/view-stack.spec.ts +272 -0
  176. package/src/view-switcher.spec.ts +446 -0
@@ -1,79 +1,159 @@
1
1
  // <adw-inline-view-switcher> — A compact, toolbar-friendly view switcher (the
2
- // web counterpart of Adw.InlineViewSwitcher). It pairs a toggle-group-style
3
- // switcher bar with a stack of pages: each declared <adw-view-stack-page> child
4
- // (with a `title` and/or `icon-name` attribute plus arbitrary content) becomes a
5
- // toggle in the bar and a panel in the stack, and exactly one page is visible at
6
- // a time. The toggles can show labels, icons, or both via `display-mode`, just
7
- // like the native widget's AdwInlineViewSwitcherDisplayMode.
2
+ // web counterpart of Adw.InlineViewSwitcher). Each declared <adw-view-stack-page>
3
+ // child becomes a toggle in a toggle-group-style bar and a panel in the stack,
4
+ // and exactly one page is visible at a time.
5
+ //
6
+ // Everything that decides WHAT to show is HEADLESS and lives in
7
+ // `@gjsify/adwaita-core` (ADR 0004) — including the TWO INDEX SPACES this widget
8
+ // is built on: a hidden page produces NO toggle, so the compacted toggle index
9
+ // and the page index diverge, and libadwaita reconciles them through the
10
+ // `child-index` it stashes on every toggle (adw-inline-view-switcher.c:346,
11
+ // :114-129, :434-453). `buildInlineToggles` carries both indices, so this file
12
+ // never has to reconcile them itself.
13
+ //
14
+ // What the lift fixed here, all of it C-derived:
15
+ // - hidden pages were not filtered at all, so the two index spaces collapsed
16
+ // into one and `AdwViewStackPage:visible` had no spelling (:370-378).
17
+ // - the display-mode default was 'both'; libadwaita's is 'labels' (:657, :715).
18
+ // - an unrecognised display-mode value REPLACED the current mode with 'both';
19
+ // `g_return_if_fail (mode <= …_BOTH)` rejects it and keeps the current one
20
+ // (:819).
21
+ // - the icons-mode tooltip used the RAW title, so `title="_Files"` showed the
22
+ // underscore; C strips the mnemonic when use-underline is set (:180-188).
23
+ // - a page with no icon rendered no icon; C substitutes `image-missing`
24
+ // (:137-142) in every mode that builds an image at all.
25
+ // - `active` was CLAMPED into range instead of refused, `notify::display-mode`
26
+ // was documented but never dispatched, the page query was unscoped (so a
27
+ // nested switcher lost its pages to the outer one), and the 500 ms
28
+ // drag-hover auto-switch did not exist (:80, :194-236).
29
+ //
8
30
  // Attributes:
9
- // active (zero-based index of the visible page, default 0)
10
- // display-mode (labels | icons | both, default both) — what the toggles show
11
- // flat / round (mirroring the Adw.InlineViewSwitcher `.flat` / `.round` style
12
- // classes, which it inherits from Adw.ToggleGroup)
31
+ // active — zero-based index of the visible PAGE (not of the toggle). Refused,
32
+ // never clamped, and reflected back to what actually holds.
33
+ // display-mode — labels | icons | both (default 'labels'). An unrecognised
34
+ // value is rejected; removing the attribute restores the default.
35
+ // flat / round — the Adw.ToggleGroup style classes.
36
+ // Page attributes: name, title, icon-name, hidden (= AdwViewStackPage:visible),
37
+ // use-underline, badge-number, needs-attention.
13
38
  // Events:
14
- // `notify::active` (CustomEvent, bubbles, `detail = { active }`) when the
15
- // visible page changes — mirrors selecting a page in the paired Adw.ViewStack.
16
- // `notify::display-mode` (CustomEvent, bubbles, `detail = { displayMode }`)
17
- // when the display mode changes — mirrors the Adw.InlineViewSwitcher
18
- // `display-mode` GObject property.
19
- // Reference: refs/libadwaita/src/adw-inline-view-switcher.c (AdwInlineViewSwitcher behaviour)
20
- // Reference: refs/libadwaita/src/stylesheet/widgets/_toggle-group.scss (inline-view-switcher > toggle-group)
21
- // Reference: refs/adwaita-web/adwaita-web/scss/_viewswitcher.scss
39
+ // `notify::active` (CustomEvent, bubbles,
40
+ // detail = { active, name, toggleIndex, interactive }) on every selection
41
+ // change, including the auto-pick.
42
+ // `notify::display-mode` (CustomEvent, bubbles, detail = { displayMode }).
43
+ // Reference: refs/libadwaita/src/adw-inline-view-switcher.c (AdwInlineViewSwitcher)
44
+ // Reference: refs/libadwaita/src/stylesheet/widgets/_toggle-group.scss
22
45
  // Copyright (c) 2023 Maximiliano Sandoval / 2024 GNOME Foundation Inc. (libadwaita). LGPLv2.1+.
23
46
  // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
24
47
 
25
- /** The three display modes, mirroring AdwInlineViewSwitcherDisplayMode. */
26
- const DISPLAY_MODES = ['labels', 'icons', 'both'] as const;
27
- type DisplayMode = (typeof DISPLAY_MODES)[number];
48
+ import {
49
+ INLINE_VIEW_SWITCHER_DISPLAY_MODES,
50
+ VIEW_SWITCHER_NO_SELECTION,
51
+ ViewSwitcherState,
52
+ buildInlineToggles,
53
+ isInlineViewSwitcherDisplayMode,
54
+ toggleIndexForPage,
55
+ } from '@gjsify/adwaita-core';
56
+ import type {
57
+ AdwInlineViewSwitcherDisplayMode,
58
+ AdwViewSwitcherPage,
59
+ ViewSwitcherStateChange,
60
+ } from '@gjsify/adwaita-core';
61
+
62
+ import {
63
+ applyDescription,
64
+ applyIndicator,
65
+ createSwitcherIcon,
66
+ domViewSwitcherScheduler,
67
+ readSwitcherPage,
68
+ } from './view-switcher-dom.js';
28
69
 
29
70
  /** A single page. Children of <adw-inline-view-switcher>; consumed at connect time. */
30
71
  export class AdwViewStackPage extends HTMLElement {
31
72
  static get observedAttributes() {
32
- return ['title', 'icon-name'];
73
+ return ['name', 'title', 'icon-name', 'badge-number', 'needs-attention', 'use-underline'];
33
74
  }
34
75
  }
35
76
 
36
77
  export class AdwInlineViewSwitcher extends HTMLElement {
78
+ private readonly _state = new ViewSwitcherState({ scheduler: domViewSwitcherScheduler });
37
79
  private _groupEl!: HTMLDivElement;
38
80
  private _pagesEl!: HTMLDivElement;
81
+ private _panels: HTMLDivElement[] = [];
39
82
  private _toggles: HTMLButtonElement[] = [];
40
- private _pages: HTMLDivElement[] = [];
41
- private _active = 0;
42
- private _displayMode: DisplayMode = 'both';
83
+ private _displayMode: AdwInlineViewSwitcherDisplayMode = 'labels';
43
84
  private _initialized = false;
44
85
 
45
86
  static get observedAttributes() {
46
87
  return ['active', 'display-mode', 'flat', 'round'];
47
88
  }
48
89
 
49
- /** Zero-based index of the visible page. */
90
+ constructor() {
91
+ super();
92
+ this._state.subscribe((change) => this._onStateChange(change));
93
+ }
94
+
95
+ /** Zero-based index of the visible PAGE, `-1` when nothing is selected. */
50
96
  get active(): number {
51
- return this._active;
97
+ return this._state.selected;
52
98
  }
53
99
 
54
100
  set active(value: number) {
55
- this.setAttribute('active', String(value));
101
+ this._state.setSelected(value);
102
+ this._reflectActive();
103
+ }
104
+
105
+ /**
106
+ * Index of the ACTIVE TOGGLE, `-1` when the visible page has none because it
107
+ * is hidden — the port-safe `GTK_INVALID_LIST_POSITION`
108
+ * (adw-inline-view-switcher.c:441-446).
109
+ */
110
+ get activeToggle(): number {
111
+ return toggleIndexForPage(this._state.pages, this._state.selected);
56
112
  }
57
113
 
58
114
  /** What the toggles display: 'labels', 'icons' or 'both'. */
59
- get displayMode(): DisplayMode {
115
+ get displayMode(): AdwInlineViewSwitcherDisplayMode {
60
116
  return this._displayMode;
61
117
  }
62
118
 
63
- set displayMode(value: DisplayMode) {
119
+ set displayMode(value: AdwInlineViewSwitcherDisplayMode) {
64
120
  this.setAttribute('display-mode', value);
65
121
  }
66
122
 
123
+ /** Name of the visible page, `''` when nothing is selected. */
124
+ get visibleChildName(): string {
125
+ return this._state.selectedName;
126
+ }
127
+
128
+ set visibleChildName(name: string | null | undefined) {
129
+ this._state.selectName(name);
130
+ }
131
+
132
+ /** The resolved page records. */
133
+ get pages(): readonly AdwViewSwitcherPage[] {
134
+ return this._state.pages;
135
+ }
136
+
137
+ /**
138
+ * Show or hide a page. Returns whether the SELECTION moved. Rebuilds the
139
+ * toggle row, because a hidden page has no toggle at all — C reacts to a
140
+ * page's `notify::visible` with `recreate_toggles`
141
+ * (adw-inline-view-switcher.c:425, :471-477).
142
+ */
143
+ setPageVisible(name: string, visible: boolean): boolean {
144
+ const moved = this._state.setPageVisible(name, visible);
145
+ this._rebuildToggles();
146
+ return moved;
147
+ }
148
+
67
149
  connectedCallback() {
68
150
  if (this._initialized) return;
69
- this._initialized = true;
70
151
 
71
- // Snapshot the declared <adw-view-stack-page> children before we take
72
- // over the subtree — their title / icon-name + content become the
73
- // rendered toggles and panels.
74
- const pages = Array.from(this.querySelectorAll('adw-view-stack-page')) as AdwViewStackPage[];
152
+ // `:scope >` rather than a subtree scan — `<adw-view-stack>` uses the
153
+ // same tag for its pages, so an unscoped query adopted a nested stack's.
154
+ const declared = Array.from(this.querySelectorAll(':scope > adw-view-stack-page')) as HTMLElement[];
155
+ const declaredActive = this.getAttribute('active');
75
156
 
76
- // The switcher bar — a toggle-group of one toggle per page.
77
157
  this._groupEl = document.createElement('div');
78
158
  this._groupEl.className = 'adw-inline-view-switcher-group';
79
159
  this._groupEl.setAttribute('role', 'tablist');
@@ -81,140 +161,171 @@ export class AdwInlineViewSwitcher extends HTMLElement {
81
161
  this._pagesEl = document.createElement('div');
82
162
  this._pagesEl.className = 'adw-inline-view-switcher-pages';
83
163
 
84
- this._toggles = [];
85
- this._pages = [];
86
-
87
- pages.forEach((page, index) => {
88
- const title = page.getAttribute('title') ?? '';
89
- const icon = (page.getAttribute('icon-name') ?? '').replace(/-symbolic$/, '');
90
-
91
- const toggle = document.createElement('button');
92
- toggle.type = 'button';
93
- toggle.className = 'adw-inline-view-switcher-toggle';
94
- toggle.setAttribute('role', 'tab');
95
- // The toggle stores both title + icon; _render() shows the parts the
96
- // current display mode calls for (mirrors libadwaita rebuilding the
97
- // toggle child on a display-mode change).
98
- toggle.dataset.title = title;
99
- toggle.dataset.icon = icon;
100
-
101
- const iconEl = document.createElement('span');
102
- iconEl.className = `adw-icon${icon ? ` adw-icon--${icon}` : ''} adw-inline-view-switcher-icon`;
103
- iconEl.setAttribute('aria-hidden', 'true'); // decorative (mask-image only)
104
- toggle.appendChild(iconEl);
105
-
106
- const labelEl = document.createElement('span');
107
- labelEl.className = 'adw-inline-view-switcher-label';
108
- labelEl.textContent = title;
109
- toggle.appendChild(labelEl);
110
-
111
- toggle.addEventListener('click', () => this._selectIndex(index));
112
- this._groupEl.appendChild(toggle);
113
- this._toggles.push(toggle);
114
-
115
- // The page content area — the declared element's children are moved
116
- // into a panel that the switcher toggles visible.
164
+ this._panels = declared.map((pageEl) => {
117
165
  const panel = document.createElement('div');
118
166
  panel.className = 'adw-inline-view-switcher-page';
119
167
  panel.setAttribute('role', 'tabpanel');
120
- for (const child of Array.from(page.childNodes)) panel.appendChild(child);
168
+ panel.dataset.name = pageEl.getAttribute('name') ?? '';
169
+ for (const child of Array.from(pageEl.childNodes)) panel.appendChild(child);
121
170
  this._pagesEl.appendChild(panel);
122
- this._pages.push(panel);
171
+ return panel;
123
172
  });
124
173
 
125
174
  this.replaceChildren(this._groupEl, this._pagesEl);
126
175
 
127
- this._active = this._readActiveAttr();
128
176
  this._displayMode = this._readDisplayModeAttr();
129
- this._render();
177
+ this._initialized = true;
178
+
179
+ this._state.setPages(declared.map((pageEl) => readSwitcherPage(pageEl)));
180
+ if (declaredActive !== null) {
181
+ const index = Number.parseInt(declaredActive, 10);
182
+ if (!Number.isNaN(index)) this._state.setSelected(index, false);
183
+ }
184
+ this._rebuildToggles();
185
+ }
186
+
187
+ disconnectedCallback() {
188
+ this._state.cancelDrag();
130
189
  }
131
190
 
132
191
  attributeChangedCallback(name: string) {
133
192
  if (!this._initialized) return;
134
193
  if (name === 'active') {
135
- const next = this._readActiveAttr();
136
- if (next !== this._active) {
137
- this._active = next;
138
- this._render();
139
- }
194
+ const raw = this.getAttribute('active');
195
+ const index = raw === null ? Number.NaN : Number.parseInt(raw, 10);
196
+ if (!Number.isNaN(index)) this._state.setSelected(index);
197
+ this._reflectActive();
140
198
  return;
141
199
  }
142
200
  if (name === 'display-mode') {
143
201
  const next = this._readDisplayModeAttr();
144
- if (next !== this._displayMode) {
145
- this._displayMode = next;
146
- this._render();
147
- }
202
+ if (next === this._displayMode) return;
203
+ this._displayMode = next;
204
+ // C rebuilds every toggle's child on a mode change rather than
205
+ // toggling visibility on the existing ones (:852-854).
206
+ this._rebuildToggles();
207
+ this.dispatchEvent(
208
+ new CustomEvent('notify::display-mode', { bubbles: true, detail: { displayMode: next } }),
209
+ );
148
210
  return;
149
211
  }
150
212
  // flat / round are styling-only.
151
- this._render();
213
+ this._applyStyleClasses();
152
214
  }
153
215
 
154
- private _readActiveAttr(): number {
155
- const raw = Number.parseInt(this.getAttribute('active') ?? '0', 10);
156
- if (Number.isNaN(raw)) return 0;
157
- const max = Math.max(0, this._pages.length - 1);
158
- return Math.min(Math.max(raw, 0), max);
216
+ private _readDisplayModeAttr(): AdwInlineViewSwitcherDisplayMode {
217
+ const raw = this.getAttribute('display-mode');
218
+ if (raw === null) return 'labels';
219
+ return isInlineViewSwitcherDisplayMode(raw) ? raw : this._displayMode;
159
220
  }
160
221
 
161
- private _readDisplayModeAttr(): DisplayMode {
162
- const raw = this.getAttribute('display-mode') ?? 'both';
163
- return (DISPLAY_MODES as readonly string[]).includes(raw) ? (raw as DisplayMode) : 'both';
222
+ /**
223
+ * Rebuild the toggle row from the derived models. One node per VISIBLE page,
224
+ * in compacted order — the structural half of `recreate_toggles`. Selection
225
+ * alone never lands here: `selection_changed_cb` only sets the active toggle
226
+ * (adw-inline-view-switcher.c:434-453), and rebuilding mid-drag would drop
227
+ * the dwell the drag has already armed.
228
+ */
229
+ private _rebuildToggles(): void {
230
+ if (!this._initialized) return;
231
+
232
+ const models = buildInlineToggles(this._state.pages, this._displayMode);
233
+ this._groupEl.replaceChildren();
234
+ this._toggles = models.map((model) => {
235
+ const toggle = document.createElement('button');
236
+ toggle.type = 'button';
237
+ toggle.className = 'adw-inline-view-switcher-toggle';
238
+ toggle.setAttribute('role', 'tab');
239
+ // Both index spaces, so a consumer never has to recompute either.
240
+ toggle.dataset.pageIndex = String(model.pageIndex);
241
+ toggle.dataset.toggleIndex = String(model.toggleIndex);
242
+
243
+ if (model.showIcon) {
244
+ toggle.appendChild(createSwitcherIcon(model.iconName, 'adw-inline-view-switcher-icon'));
245
+ }
246
+ if (model.showLabel) {
247
+ const label = document.createElement('span');
248
+ label.className = 'adw-inline-view-switcher-label';
249
+ label.textContent = model.label;
250
+ toggle.appendChild(label);
251
+ }
252
+
253
+ const indicator = document.createElement('span');
254
+ indicator.className = 'adw-inline-view-switcher-indicator';
255
+ indicator.setAttribute('aria-hidden', 'true');
256
+ applyIndicator(indicator, model.badgeLabel, model.needsAttention);
257
+ toggle.appendChild(indicator);
258
+
259
+ if (model.tooltip) toggle.title = model.tooltip;
260
+ // An icon-only toggle has no visible text; the tooltip is also its
261
+ // accessible name, which is exactly what C's toggle does.
262
+ if (!model.showLabel) toggle.setAttribute('aria-label', model.tooltip || model.iconName);
263
+ applyDescription(toggle, model.description);
264
+
265
+ toggle.addEventListener('click', () => {
266
+ this._state.setSelected(model.pageIndex);
267
+ });
268
+ toggle.addEventListener('dragenter', () => this._state.dragEnter(model.pageIndex));
269
+ toggle.addEventListener('dragleave', () => this._state.dragLeave(model.pageIndex));
270
+
271
+ this._groupEl.appendChild(toggle);
272
+ return toggle;
273
+ });
274
+
275
+ this._render();
164
276
  }
165
277
 
166
- private _selectIndex(index: number): void {
167
- if (index === this._active) return;
168
- this._active = index;
169
- // Keep the attribute in sync without re-entering via the guarded
170
- // attributeChangedCallback (value already matches, so it no-ops).
171
- this.setAttribute('active', String(index));
278
+ private _onStateChange(change: ViewSwitcherStateChange): void {
172
279
  this._render();
173
- this.dispatchEvent(new CustomEvent('notify::active', { bubbles: true, detail: { active: index } }));
280
+ this.dispatchEvent(
281
+ new CustomEvent('notify::active', {
282
+ bubbles: true,
283
+ detail: {
284
+ active: change.selected,
285
+ name: change.name,
286
+ toggleIndex: this.activeToggle,
287
+ interactive: change.interactive,
288
+ },
289
+ }),
290
+ );
174
291
  }
175
292
 
176
293
  private _render(): void {
177
- this.classList.toggle('flat', this.hasAttribute('flat'));
178
- this.classList.toggle('round', this.hasAttribute('round'));
179
-
180
- // The bar carries the active display mode as a class — mirrors
181
- // libadwaita's inline-view-switcher > toggle-group.{labels,icons,both}.
182
- for (const mode of DISPLAY_MODES) this._groupEl.classList.toggle(mode, mode === this._displayMode);
294
+ if (!this._initialized) return;
183
295
 
184
- const showIcon = this._displayMode !== 'labels';
185
- const showLabel = this._displayMode !== 'icons';
296
+ this._applyStyleClasses();
186
297
 
187
- this._toggles.forEach((toggle, index) => {
188
- const isActive = index === this._active;
298
+ const activeToggle = this.activeToggle;
299
+ this._toggles.forEach((toggle, toggleIndex) => {
300
+ const isActive = toggleIndex === activeToggle && activeToggle !== VIEW_SWITCHER_NO_SELECTION;
189
301
  toggle.classList.toggle('active', isActive);
190
302
  toggle.setAttribute('aria-selected', String(isActive));
191
303
  toggle.tabIndex = isActive ? 0 : -1;
192
-
193
- const iconEl = toggle.querySelector('.adw-inline-view-switcher-icon') as HTMLElement | null;
194
- const labelEl = toggle.querySelector('.adw-inline-view-switcher-label') as HTMLElement | null;
195
- const hasIcon = (toggle.dataset.icon ?? '').length > 0;
196
- const title = toggle.dataset.title ?? '';
197
-
198
- if (iconEl) iconEl.hidden = !showIcon || !hasIcon;
199
- if (labelEl) labelEl.hidden = !showLabel || title.length === 0;
200
-
201
- // In icons-only mode the toggle has no visible text — surface the
202
- // title as an accessible name + a hover tooltip (the native widget
203
- // sets the toggle tooltip to the page title in ICONS mode).
204
- if (!showLabel && title) {
205
- toggle.setAttribute('aria-label', title);
206
- toggle.title = title;
207
- } else {
208
- toggle.removeAttribute('aria-label');
209
- toggle.removeAttribute('title');
210
- }
211
304
  });
212
305
 
213
- this._pages.forEach((panel, index) => {
214
- const isActive = index === this._active;
306
+ const selected = this._state.selected;
307
+ this._panels.forEach((panel, index) => {
308
+ const isActive = index === selected;
215
309
  panel.classList.toggle('active-view', isActive);
216
310
  panel.hidden = !isActive;
217
311
  });
312
+
313
+ this._reflectActive();
314
+ }
315
+
316
+ private _applyStyleClasses(): void {
317
+ this.classList.toggle('flat', this.hasAttribute('flat'));
318
+ this.classList.toggle('round', this.hasAttribute('round'));
319
+ // The group carries the display-mode class, mirroring libadwaita's
320
+ // inline-view-switcher > toggle-group.{labels,icons,both} (:826-850).
321
+ for (const mode of INLINE_VIEW_SWITCHER_DISPLAY_MODES) {
322
+ this._groupEl.classList.toggle(mode, mode === this._displayMode);
323
+ }
324
+ }
325
+
326
+ private _reflectActive(): void {
327
+ const current = String(this._state.selected);
328
+ if (this.getAttribute('active') !== current) this.setAttribute('active', current);
218
329
  }
219
330
  }
220
331