@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
@@ -6,19 +6,42 @@
6
6
  // Header-suffix: any child carrying `slot="header-suffix"` is hoisted into the
7
7
  // trailing edge of the header instead of the boxed list — mirroring
8
8
  // Adw.PreferencesGroup's `header-suffix` property / buildable type.
9
+ //
10
+ // The five visibility states (title, description, header, `single-line`,
11
+ // listbox) are NOT derived here: they come from
12
+ // `derivePreferencesGroupHeader` in `@gjsify/adwaita-core`, which both this
13
+ // element and the NativeScript group delegate to (ADR 0004). This element used
14
+ // to compute two of the five by hand and was missing the other three — no
15
+ // `single-line` class, and a `.boxed-list` card that stayed painted (with its
16
+ // full-width `box-shadow` hairline) over an empty group.
17
+ //
18
+ // Reference: refs/libadwaita/src/adw-preferences-group.c
19
+ // (update_title_visibility, update_listbox_visibility, is_single_line,
20
+ // update_header_visibility, adw_preferences_group_add)
9
21
  // Reference: refs/libadwaita/src/adw-preferences-group.ui (header_box layout)
10
22
  // Reference: refs/adwaita-web/adwaita-web/scss/_preferences.scss
11
23
  // Adapted from Adwaita Web UI Framework (https://github.com/mclellac/adwaita-web).
12
24
  // Copyright (c) 2025 csm. MIT License.
13
25
  // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
14
26
  // Modifications: Reimplemented as Web Component for @gjsify/adwaita-web;
15
- // description line + header-suffix slot added to match Adw.PreferencesGroup.
27
+ // description line + header-suffix slot added to match Adw.PreferencesGroup;
28
+ // header/listbox state delegated to @gjsify/adwaita-core.
29
+
30
+ import { derivePreferencesGroupHeader } from '@gjsify/adwaita-core';
31
+
32
+ /** A child that asked to sit in the header rather than in the boxed list. */
33
+ function isHeaderSuffix(node: Node): boolean {
34
+ return node instanceof Element && node.getAttribute('slot') === 'header-suffix';
35
+ }
16
36
 
17
37
  export class AdwPreferencesGroup extends HTMLElement {
38
+ private _headerEl!: HTMLDivElement;
18
39
  private _titleEl!: HTMLSpanElement;
19
40
  private _descriptionEl!: HTMLSpanElement;
20
41
  private _suffixEl!: HTMLDivElement;
42
+ private _listboxEl!: HTMLDivElement;
21
43
  private _initialized = false;
44
+ private _observer: MutationObserver | null = null;
22
45
 
23
46
  static get observedAttributes() {
24
47
  return ['title', 'description'];
@@ -33,22 +56,52 @@ export class AdwPreferencesGroup extends HTMLElement {
33
56
  else this.removeAttribute('description');
34
57
  }
35
58
 
59
+ /**
60
+ * `adw_preferences_group_add` (adw-preferences-group.c:396-411): a row goes
61
+ * into the boxed list, a `header-suffix` child into the header.
62
+ *
63
+ * Exposed as a method AND wired to a `MutationObserver` on the host, because
64
+ * both spellings occur: `group.addRow(row)` in code, `group.append(row)` in
65
+ * a framework's rendering. Before this, a child appended after connect
66
+ * stayed a bare child of the host — outside the card, and invisible to the
67
+ * row count that decides whether the card is painted at all.
68
+ */
69
+ addRow(child: Node): void {
70
+ if (isHeaderSuffix(child)) this._suffixEl.appendChild(child);
71
+ else this._listboxEl.appendChild(child);
72
+ this._renderHeader();
73
+ }
74
+
75
+ /**
76
+ * `adw_preferences_group_remove` (:421-440). C logs
77
+ * `ADW_CRITICAL_CANNOT_REMOVE_CHILD` and no-ops for a child it does not
78
+ * own; returning `false` is the same contract without a console write.
79
+ */
80
+ removeRow(child: Node): boolean {
81
+ const parent = child.parentNode;
82
+ if (parent !== this._listboxEl && parent !== this._suffixEl) return false;
83
+ parent.removeChild(child);
84
+ this._renderHeader();
85
+ return true;
86
+ }
87
+
36
88
  connectedCallback() {
37
- if (this._initialized) return;
89
+ if (this._initialized) {
90
+ this._observeHost();
91
+ return;
92
+ }
38
93
  this._initialized = true;
39
94
 
40
95
  const children = Array.from(this.childNodes);
41
96
  // Children opting into the header-suffix slot are hoisted to the
42
97
  // trailing edge of the header; everything else goes into the boxed list.
43
- const suffixChildren = children.filter(
44
- (child) => child instanceof Element && child.getAttribute('slot') === 'header-suffix',
45
- );
98
+ const suffixChildren = children.filter(isHeaderSuffix);
46
99
  const rowChildren = children.filter((child) => !suffixChildren.includes(child));
47
100
 
48
101
  // Header: a flex row with a title/description label column on the left
49
102
  // and the optional header-suffix on the trailing edge.
50
- const header = document.createElement('div');
51
- header.className = 'adw-preferences-group-header';
103
+ this._headerEl = document.createElement('div');
104
+ this._headerEl.className = 'adw-preferences-group-header';
52
105
 
53
106
  const labels = document.createElement('div');
54
107
  labels.className = 'adw-preferences-group-labels';
@@ -63,38 +116,91 @@ export class AdwPreferencesGroup extends HTMLElement {
63
116
 
64
117
  this._suffixEl = document.createElement('div');
65
118
  this._suffixEl.className = 'adw-preferences-group-suffix';
66
- suffixChildren.forEach((child) => this._suffixEl.appendChild(child));
119
+ for (const child of suffixChildren) this._suffixEl.appendChild(child);
67
120
 
68
- header.append(labels, this._suffixEl);
121
+ this._headerEl.append(labels, this._suffixEl);
69
122
 
70
123
  // Boxed list container — the remaining children move into it.
71
- const listbox = document.createElement('div');
72
- listbox.className = 'adw-preferences-group-listbox';
73
- rowChildren.forEach((child) => listbox.appendChild(child));
124
+ this._listboxEl = document.createElement('div');
125
+ this._listboxEl.className = 'adw-preferences-group-listbox';
126
+ for (const child of rowChildren) this._listboxEl.appendChild(child);
74
127
 
75
- this.replaceChildren(header, listbox);
128
+ this.replaceChildren(this._headerEl, this._listboxEl);
76
129
 
77
130
  this._renderHeader();
131
+ this._observeHost();
132
+ }
133
+
134
+ disconnectedCallback() {
135
+ // The observer holds this element alive through the host node; drop it
136
+ // with the connection that created it, not at some later "cleanup".
137
+ this._observer?.disconnect();
138
+ this._observer = null;
78
139
  }
79
140
 
80
141
  attributeChangedCallback() {
81
142
  if (this._initialized) this._renderHeader();
82
143
  }
83
144
 
145
+ /**
146
+ * Adopt children appended to the HOST after the subtree was built, so
147
+ * `group.append(row)` behaves like `adw_preferences_group_add`.
148
+ *
149
+ * C gets this for free — `gtk_widget_observe_children` on the listbox drives
150
+ * `update_listbox_visibility` through `items-changed`
151
+ * (adw-preferences-group.c:335-339) — and a DOM element needs the observer
152
+ * to have the same rule hold over time rather than only at connect.
153
+ */
154
+ private _observeHost(): void {
155
+ if (this._observer) return;
156
+ this._observer = new MutationObserver((records) => {
157
+ for (const record of records) {
158
+ // Only the HOST's own children need re-homing. Records from the
159
+ // listbox are the CONSEQUENCE of that move; re-adopting them
160
+ // would append each row a second time and reorder the list.
161
+ if (record.target !== this) continue;
162
+ for (const node of record.addedNodes) {
163
+ if (!(node instanceof Element)) continue;
164
+ if (node === this._headerEl || node === this._listboxEl) continue;
165
+ this.addRow(node);
166
+ }
167
+ }
168
+ // A removal straight out of the listbox still changes the row count.
169
+ this._renderHeader();
170
+ });
171
+ this._observer.observe(this, { childList: true });
172
+ this._observer.observe(this._listboxEl, { childList: true });
173
+ }
174
+
84
175
  private _renderHeader() {
85
176
  const title = this.getAttribute('title') ?? '';
86
- this._titleEl.textContent = title;
87
- this._titleEl.hidden = title.length === 0;
88
-
89
177
  const description = this.getAttribute('description') ?? '';
178
+
179
+ this._titleEl.textContent = title;
90
180
  this._descriptionEl.textContent = description;
91
- this._descriptionEl.hidden = description.length === 0;
92
181
 
93
- // The header only takes space when it carries a title, a description or
94
- // a suffix — an empty group has no header padding, like Adw's.
95
- const hasSuffix = this._suffixEl.childElementCount > 0;
96
- const headerEl = this._titleEl.parentElement?.parentElement;
97
- if (headerEl) headerEl.hidden = title.length === 0 && description.length === 0 && !hasSuffix;
182
+ const state = derivePreferencesGroupHeader({
183
+ title,
184
+ description,
185
+ hasHeaderSuffix: this._suffixEl.childElementCount > 0,
186
+ // The RAW child count, matching `gtk_widget_observe_children` — a
187
+ // row with an empty title still keeps the card painted.
188
+ rowCount: this._listboxEl.childElementCount,
189
+ // The labels are `textContent`, not markup: what is displayed IS the
190
+ // attribute value, so visibility must be judged on the raw string.
191
+ // libadwaita's labels are `use-markup=True`; rendering Pango markup
192
+ // in the browser is a separate, still-open gap, and half-closing it
193
+ // here would hide a title this element visibly renders.
194
+ useMarkup: false,
195
+ });
196
+
197
+ this._titleEl.hidden = !state.titleVisible;
198
+ this._descriptionEl.hidden = !state.descriptionVisible;
199
+ this._headerEl.hidden = !state.headerVisible;
200
+ // `single-line` is not cosmetic: the stylesheet keys the header's
201
+ // min-height / margin-bottom off it.
202
+ this._headerEl.classList.toggle('single-line', state.singleLine);
203
+ this._listboxEl.hidden = !state.listboxVisible;
98
204
  }
99
205
  }
100
206
 
@@ -0,0 +1,189 @@
1
+ // <adw-progress-bar> — the DETERMINATE progress indicator, and the partner to
2
+ // <adw-spinner>, which only ever covers "busy, no idea how long".
3
+ //
4
+ // NO CORE STATE MACHINE. The whole behaviour is `CLAMP(fraction, 0, 1)` plus a
5
+ // pulsing flag — ADR 0004 is explicit that a widget with genuinely trivial
6
+ // behaviour does not get a core class, and the clamp itself is already shared:
7
+ // `glibClamp` (GLib's CLAMP, high bound tested FIRST) comes from
8
+ // `@gjsify/adwaita-core`. Nothing was added there for this element.
9
+ //
10
+ // WHAT IS NOT VERIFIABLE IN THIS TREE. `GtkProgressBar` is a GTK widget:
11
+ // `refs/gtk` is EMPTY here and libadwaita vendors no `adw-progress-bar.c`, so
12
+ // `gtk_progress_bar_pulse()`'s step semantics — the `pulse-step` default, how
13
+ // the block reflects at the ends, whether it resets `fraction` — are NOT
14
+ // verifiable and are NOT reproduced. What ships instead is the web idiom the
15
+ // vendored port uses: a CSS keyframe animation that runs while `pulsing` is set
16
+ // (scss/_progress_bar.scss, which cites which half came from which source), so
17
+ // {@link AdwProgressBar.pulse} ENTERS that state and is idempotent afterwards
18
+ // rather than advancing an offset nothing here could check. The
19
+ // default `show-text` label — `NN%` — is likewise this port's, not a
20
+ // reproduction of GTK's format string.
21
+ //
22
+ // Attributes:
23
+ // fraction — 0…1, clamped. How much of the bar is filled.
24
+ // pulsing — boolean; indeterminate. The block bounces instead of filling.
25
+ // show-text — boolean; reveal the text node under the bar.
26
+ // text — the text to show. Defaults to the fraction as a percentage.
27
+ // inverted — boolean; fill from the far end.
28
+ // osd — boolean; libadwaita's `.osd` STYLE CLASS — a 2px troughless bar
29
+ // for under a header bar (_progress-bar.scss:59-83).
30
+ // disabled — boolean; dims the widget.
31
+ // Properties:
32
+ // fraction / pulsing / showText / text / inverted / osd / disabled (get/set).
33
+ // Methods:
34
+ // pulse() — switch to indeterminate. GtkProgressBar's own vocabulary; see
35
+ // the caveat above for how far the resemblance goes.
36
+ //
37
+ // A11Y: `role="progressbar"` with `aria-valuemin`/`aria-valuemax` fixed at 0/1
38
+ // and `aria-valuenow` carrying the clamped fraction. While pulsing,
39
+ // `aria-valuenow` is REMOVED — that is how ARIA spells "indeterminate", and a
40
+ // stale value there would be announced as real progress.
41
+ //
42
+ // Reference: refs/libadwaita/src/stylesheet/widgets/_progress-bar.scss
43
+ // Reference: refs/adwaita-web/adwaita-web/scss/_progressbar.scss (the indeterminate animation only)
44
+ // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
45
+ // Copyright (c) 2025 csm (adwaita-web). MIT License.
46
+ // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
47
+
48
+ import { glibClamp } from '@gjsify/adwaita-core';
49
+
50
+ export class AdwProgressBar extends HTMLElement {
51
+ private _trough!: HTMLSpanElement;
52
+ private _progress!: HTMLSpanElement;
53
+ private _text!: HTMLSpanElement;
54
+ private _initialized = false;
55
+
56
+ static get observedAttributes() {
57
+ return ['fraction', 'pulsing', 'show-text', 'text', 'inverted', 'osd', 'disabled'];
58
+ }
59
+
60
+ /** How much of the bar is filled, 0…1. Always the CLAMPED value. */
61
+ get fraction(): number {
62
+ const raw = Number.parseFloat(this.getAttribute('fraction') ?? '');
63
+ // A missing or unparseable attribute is 0, GtkProgressBar's default —
64
+ // NOT NaN, which would reach the width and the a11y value.
65
+ if (!Number.isFinite(raw)) return 0;
66
+ return glibClamp(raw, 0, 1);
67
+ }
68
+
69
+ set fraction(value: number) {
70
+ this.setAttribute('fraction', String(value));
71
+ }
72
+
73
+ /** Whether the bar is indeterminate. */
74
+ get pulsing(): boolean {
75
+ return this.hasAttribute('pulsing');
76
+ }
77
+
78
+ set pulsing(value: boolean) {
79
+ this.toggleAttribute('pulsing', !!value);
80
+ }
81
+
82
+ /** Whether the text node is shown. */
83
+ get showText(): boolean {
84
+ return this.hasAttribute('show-text');
85
+ }
86
+
87
+ set showText(value: boolean) {
88
+ this.toggleAttribute('show-text', !!value);
89
+ }
90
+
91
+ /** The shown text. Falls back to the fraction as a percentage. */
92
+ get text(): string {
93
+ const explicit = this.getAttribute('text');
94
+ if (explicit !== null) return explicit;
95
+ return `${Math.round(this.fraction * 100)}%`;
96
+ }
97
+
98
+ set text(value: string) {
99
+ this.setAttribute('text', value);
100
+ }
101
+
102
+ /** Whether the bar fills from the far end. */
103
+ get inverted(): boolean {
104
+ return this.hasAttribute('inverted');
105
+ }
106
+
107
+ set inverted(value: boolean) {
108
+ this.toggleAttribute('inverted', !!value);
109
+ }
110
+
111
+ /** Whether the `.osd` hairline variant is used. */
112
+ get osd(): boolean {
113
+ return this.hasAttribute('osd');
114
+ }
115
+
116
+ set osd(value: boolean) {
117
+ this.toggleAttribute('osd', !!value);
118
+ }
119
+
120
+ /** Whether the widget is dimmed. */
121
+ get disabled(): boolean {
122
+ return this.hasAttribute('disabled');
123
+ }
124
+
125
+ set disabled(value: boolean) {
126
+ this.toggleAttribute('disabled', !!value);
127
+ }
128
+
129
+ /**
130
+ * Switch the bar to indeterminate — GtkProgressBar's method name for the
131
+ * same intent. See the module header: the bouncing block is a CSS animation
132
+ * here, so this ENTERS the state and repeat calls are no-ops; GTK's
133
+ * `pulse-step` arithmetic is not verifiable in this tree and is not faked.
134
+ */
135
+ pulse(): void {
136
+ this.pulsing = true;
137
+ }
138
+
139
+ connectedCallback() {
140
+ if (this._initialized) return;
141
+ this._initialized = true;
142
+
143
+ this._trough = document.createElement('span');
144
+ this._trough.className = 'adw-progress-bar-trough';
145
+ this._progress = document.createElement('span');
146
+ this._progress.className = 'adw-progress-bar-progress';
147
+ this._trough.appendChild(this._progress);
148
+
149
+ this._text = document.createElement('span');
150
+ this._text.className = 'adw-progress-bar-text';
151
+
152
+ this.replaceChildren(this._trough, this._text);
153
+
154
+ this.setAttribute('role', 'progressbar');
155
+ this.setAttribute('aria-valuemin', '0');
156
+ this.setAttribute('aria-valuemax', '1');
157
+
158
+ this._render();
159
+ }
160
+
161
+ attributeChangedCallback() {
162
+ if (!this._initialized) return;
163
+ this._render();
164
+ }
165
+
166
+ private _render(): void {
167
+ const fraction = this.fraction;
168
+ const pulsing = this.pulsing;
169
+
170
+ // `> trough.empty > progress { all: unset }` (_progress-bar.scss:85) —
171
+ // the indicator has to VANISH at 0, not draw a rounded stub. A pulsing
172
+ // bar is never empty: its block is what says "working".
173
+ const empty = !pulsing && fraction === 0;
174
+ this._trough.classList.toggle('empty', empty);
175
+ // The inline width is dropped in both the pulsing and the empty case, so
176
+ // the rules that own the indicator there — the keyframes, and
177
+ // `all: unset` — are not fighting an inline declaration they cannot win
178
+ // against (inline styles beat any author rule, `all: unset` included).
179
+ this._progress.style.width = pulsing || empty ? '' : `${fraction * 100}%`;
180
+
181
+ this._text.textContent = this.text;
182
+ this._text.hidden = !this.showText;
183
+
184
+ if (pulsing) this.removeAttribute('aria-valuenow');
185
+ else this.setAttribute('aria-valuenow', String(fraction));
186
+ }
187
+ }
188
+
189
+ customElements.define('adw-progress-bar', AdwProgressBar);