@gjsify/adwaita-web 0.36.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
@@ -0,0 +1,138 @@
1
+ // <adw-shortcut-label> — a keyboard shortcut, drawn as keycaps.
2
+ // Attributes: accelerator (`<Control>C`, `<Shift>A Home`, `<Alt>1...9`,
3
+ // `Control_L&Control_R`, `<Control>C+<Control>X`), disabled-text (the
4
+ // placeholder shown when `accelerator` is empty).
5
+ // Events: `notify::accelerator`, `notify::disabled-text` (CustomEvent, bubbles),
6
+ // mirroring the AdwShortcutLabel GObject notify signals.
7
+ //
8
+ // The whole parse — four nesting levels and the fixed keycap order — is
9
+ // `@gjsify/adwaita-core`'s `parseShortcutLabel` (ADR 0004). This element only
10
+ // turns its nodes into DOM, which is the split that keeps a second renderer from
11
+ // re-deriving the grammar and drifting.
12
+ //
13
+ // TEXT, NOT MARKUP. The C builds pango markup: `&lt;` for a literal `<`, and
14
+ // `%s <small><b>%s</b></small>` for the `L`/`R` subscript of a sided modifier
15
+ // (adw-shortcut-label.c:195, :253-266). Core returns `{ label, sideMarker }`
16
+ // instead, so the escaping is `textContent`'s job here and the subscript is a
17
+ // real `<sub>` — nothing parses a markup string back apart.
18
+ //
19
+ // DIRECTION IS READ, NOT ASSUMED. `parse_sequence` picks its arrow from the
20
+ // widget's text direction (:439), so the element resolves its own computed
21
+ // direction at render time rather than defaulting to LTR.
22
+ //
23
+ // Reference: refs/libadwaita/src/adw-shortcut-label.c
24
+ // Reference: refs/libadwaita/src/stylesheet/widgets/_shortcuts-dialog.scss:33-58
25
+ // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
26
+ // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
27
+
28
+ import { parseShortcutLabel } from '@gjsify/adwaita-core';
29
+
30
+ export class AdwShortcutLabel extends HTMLElement {
31
+ private _initialized = false;
32
+
33
+ static get observedAttributes() {
34
+ return ['accelerator', 'disabled-text'];
35
+ }
36
+
37
+ connectedCallback() {
38
+ if (!this._initialized) {
39
+ this._initialized = true;
40
+ // `GTK_ACCESSIBLE_ROLE_LABEL` (:88). The accessible NAME is set from
41
+ // the parse below, because the keycaps read as a pile of single
42
+ // letters otherwise.
43
+ if (!this.hasAttribute('role')) this.setAttribute('role', 'img');
44
+ }
45
+ this._render();
46
+ }
47
+
48
+ attributeChangedCallback(name: string) {
49
+ if (!this._initialized) return;
50
+ this._render();
51
+ this.dispatchEvent(new CustomEvent(`notify::${name}`, { bubbles: true }));
52
+ }
53
+
54
+ get accelerator(): string {
55
+ return this.getAttribute('accelerator') ?? '';
56
+ }
57
+
58
+ set accelerator(value: string) {
59
+ this.setAttribute('accelerator', value);
60
+ }
61
+
62
+ get disabledText(): string {
63
+ return this.getAttribute('disabled-text') ?? '';
64
+ }
65
+
66
+ set disabledText(value: string) {
67
+ this.setAttribute('disabled-text', value);
68
+ }
69
+
70
+ private _render() {
71
+ const { nodes, accessibleLabel, error } = parseShortcutLabel(this.accelerator, {
72
+ disabledText: this.disabledText,
73
+ direction: this._direction(),
74
+ });
75
+
76
+ this.replaceChildren(...nodes.map((node) => this._nodeElement(node)));
77
+ this.setAttribute('aria-label', accessibleLabel);
78
+
79
+ // `g_warning ("Failed to parse %s, part of accelerator '%s'")` (:532).
80
+ // The partial render is upstream's too — it keeps what it built.
81
+ if (error) {
82
+ console.warn(`adw-shortcut-label: failed to parse ${error}, part of accelerator '${this.accelerator}'`);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * The rendered direction, which is what the sequence arrow follows.
88
+ *
89
+ * `getComputedStyle` is the only thing that sees an inherited `dir` — the
90
+ * attribute alone is set on an ancestor most of the time. A detached element
91
+ * has no computed style to read, so the attribute is the fallback.
92
+ */
93
+ private _direction(): 'ltr' | 'rtl' {
94
+ const computed = this.isConnected ? getComputedStyle(this).direction : '';
95
+ if (computed === 'rtl' || computed === 'ltr') return computed;
96
+ return this.closest('[dir="rtl"]') ? 'rtl' : 'ltr';
97
+ }
98
+
99
+ private _nodeElement(node: ReturnType<typeof parseShortcutLabel>['nodes'][number]): HTMLElement {
100
+ if (node.kind === 'separator' || node.kind === 'disabled') {
101
+ // `dim_label` (:359-368) — both the separators and the placeholder,
102
+ // which upstream tells apart only by position (a placeholder is the
103
+ // only child). The extra class on the placeholder is this port's:
104
+ // `.dimmed` still applies, and a consumer can now style the
105
+ // "no shortcut" state without matching on child count.
106
+ const label = document.createElement('span');
107
+ label.className = node.kind === 'disabled' ? 'dimmed adw-shortcut-label-disabled' : 'dimmed';
108
+ label.textContent = node.text;
109
+ return label;
110
+ }
111
+
112
+ // One combination is one BOX of keycaps, LTR even in an RTL context
113
+ // (:375-380): the modifier order is a property of the shortcut, not of
114
+ // the surrounding text.
115
+ const box = document.createElement('span');
116
+ box.className = 'adw-shortcut-label-keys';
117
+ box.setAttribute('dir', 'ltr');
118
+
119
+ for (const key of node.keys) {
120
+ const cap = document.createElement('span');
121
+ cap.className = 'keycap';
122
+ cap.textContent = key.label;
123
+
124
+ if (key.sideMarker) {
125
+ const marker = document.createElement('sub');
126
+ marker.className = 'adw-shortcut-label-side';
127
+ marker.textContent = key.sideMarker;
128
+ cap.append(marker);
129
+ }
130
+
131
+ box.append(cap);
132
+ }
133
+
134
+ return box;
135
+ }
136
+ }
137
+
138
+ customElements.define('adw-shortcut-label', AdwShortcutLabel);
@@ -7,16 +7,15 @@
7
7
  // chevron arrow on each row (the AdwPreferencesPage look the native widget
8
8
  // switches to when collapsed).
9
9
  //
10
- // The BEHAVIOUR — the flat index space, the out-of-range rule, which section
11
- // headers render, the selection-vs-activation split, the filter and the empty
12
- // state — is HEADLESS and lives in `@gjsify/adwaita-core` (ADR 0004) as
13
- // {@link SidebarState}; this element only builds DOM for it and re-emits its
14
- // changes as events. It used to carry its own copy, which clamped an
15
- // out-of-range `selected` to the LAST row where libadwaita drops the selection
16
- // entirely, keyed section headers off the DECLARATION index instead of the rows
17
- // that actually render, and froze the row list after the first connect. The
18
- // shared vectors in `@gjsify/adwaita-core/conformance` now pin this element to
19
- // the C source — see `src/adw-sidebar.spec.ts`.
10
+ // The BEHAVIOUR — the flat index space, the out-of-range rule, which section headers
11
+ // render, the selection-vs-activation split, the filter and the empty state — is
12
+ // HEADLESS and lives in `@gjsify/adwaita-core` (ADR 0004) as {@link SidebarState}; this
13
+ // element only builds DOM for it and re-emits its changes as events, pinned to the C
14
+ // source by the shared vectors in `@gjsify/adwaita-core/conformance` (see
15
+ // `src/adw-sidebar.spec.ts`). Three rules a hand-written copy gets wrong: an
16
+ // out-of-range `selected` DROPS the selection rather than clamping to the last row,
17
+ // section headers are keyed off the rows that actually RENDER and not the declaration
18
+ // index, and the row list stays live after the first connect.
20
19
  //
21
20
  // Sub-elements (declared as children, consumed at connect time — mirrors
22
21
  // adw-tab-page in adw-tab-view):
@@ -59,12 +58,10 @@ import {
59
58
 
60
59
  import { createAdwIcon } from './adw-icon.js';
61
60
 
62
- // The sidebar consumes its declared children and drops them from the tree, so a
63
- // later `setAttribute` on one cannot find its sidebar with `closest()`. These
64
- // keep the link — the web stand-in for the `g_object_bind_property` /
65
- // `notify::icon-name` bindings that keep a live AdwSidebarItem's labels in sync
66
- // (adw-sidebar.c:1393-1421). Before this, `observedAttributes` was declared on
67
- // both child elements and no `attributeChangedCallback` existed to answer it.
61
+ // The sidebar consumes its declared children and drops them from the tree, so a later
62
+ // `setAttribute` on one cannot find its sidebar with `closest()`. These keep the link —
63
+ // the web stand-in for the `g_object_bind_property` / `notify::icon-name` bindings that
64
+ // keep a live AdwSidebarItem's labels in sync.
68
65
  const itemBindings = new WeakMap<AdwSidebarItem, { sidebar: AdwSidebar; spec: AdwSidebarItemSpec }>();
69
66
  const sectionBindings = new WeakMap<AdwSidebarSection, { sidebar: AdwSidebar; spec: AdwSidebarSectionSpec }>();
70
67
 
@@ -116,7 +113,6 @@ export class AdwSidebar extends HTMLElement {
116
113
  return ['mode', 'selected'];
117
114
  }
118
115
 
119
- /** Zero-based flat index of the selected item, or -1 for no selection. */
120
116
  get selected(): number {
121
117
  return this._state.selected;
122
118
  }
@@ -130,7 +126,6 @@ export class AdwSidebar extends HTMLElement {
130
126
  return this._state.selectedItem;
131
127
  }
132
128
 
133
- /** 'sidebar' (flat navigation list) or 'page' (boxed lists). */
134
129
  get mode(): 'sidebar' | 'page' {
135
130
  return this._state.mode;
136
131
  }
@@ -171,10 +166,10 @@ export class AdwSidebar extends HTMLElement {
171
166
  // asked to hear about, and `setSections` runs the 0 → n auto-select.
172
167
  this._state.setSections(this._readDeclaredSections());
173
168
  this._state.setMode(this._readModeAttr());
174
- // A declared `selected` wins over that auto-select. libadwaita would let
175
- // the auto-select clobber a builder-set property (items arrive after
176
- // properties, and items_changed_cb then selects 0); for a declarative
177
- // element the authored attribute has to survive its own children.
169
+ // A declared `selected` wins over that auto-select. libadwaita lets the
170
+ // auto-select clobber a builder-set property (items arrive after properties, and
171
+ // `items_changed_cb` then selects 0); for a declarative element the authored
172
+ // attribute has to survive its own children.
178
173
  if (this.hasAttribute('selected')) this._state.setSelected(this._readSelectedAttr());
179
174
 
180
175
  this._state.subscribe((change) => this._onSelectionChanged(change));
@@ -190,8 +185,8 @@ export class AdwSidebar extends HTMLElement {
190
185
 
191
186
  if (name === 'selected') {
192
187
  this._state.setSelected(this._readSelectedAttr());
193
- // Normalise even when nothing moved, so `selected="5"` on a 3-item
194
- // sidebar does not keep claiming 5 while the property reads -1.
188
+ // Normalise even when nothing moved, so `selected="5"` on a 3-item sidebar
189
+ // does not keep claiming 5 while the property reads -1.
195
190
  this._reflectSelected();
196
191
  return;
197
192
  }
@@ -202,9 +197,9 @@ export class AdwSidebar extends HTMLElement {
202
197
  }
203
198
 
204
199
  /**
205
- * Re-derive after a declared `<adw-sidebar-item>` / `<adw-sidebar-section>`
206
- * attribute changed. The item model keeps its shape, so the selection cannot
207
- * move — exactly like the GObject property bindings this stands in for.
200
+ * Re-derive after a declared `<adw-sidebar-item>` / `<adw-sidebar-section>` attribute
201
+ * changed. The item model keeps its shape, so the selection cannot move — exactly
202
+ * like the GObject property bindings this stands in for.
208
203
  */
209
204
  refresh(): void {
210
205
  if (!this._initialized) return;
@@ -212,7 +207,6 @@ export class AdwSidebar extends HTMLElement {
212
207
  this._rebuild();
213
208
  }
214
209
 
215
- /** Snapshot the declared children into specs, and bind them for live updates. */
216
210
  private _readDeclaredSections(): AdwSidebarSectionSpec[] {
217
211
  const sections: AdwSidebarSectionSpec[] = [];
218
212
 
@@ -245,8 +239,8 @@ export class AdwSidebar extends HTMLElement {
245
239
 
246
240
  /**
247
241
  * `parseFloat`, not `parseInt`: the core rejects a fractional position, and
248
- * truncating `"1.5"` to row 1 here would hide that from it. An unparseable
249
- * value is NaN, which the core turns into "no selection".
242
+ * truncating `"1.5"` to row 1 here would hide that from it. An unparseable value is
243
+ * NaN, which the core turns into "no selection".
250
244
  */
251
245
  private _readSelectedAttr(): number {
252
246
  return Number.parseFloat(this.getAttribute('selected') ?? '');
@@ -269,13 +263,11 @@ export class AdwSidebar extends HTMLElement {
269
263
  );
270
264
  }
271
265
 
272
- /** Click handler — the core applies the selection, then `activated` fires. */
273
266
  private _activate(index: number): void {
274
267
  if (!this._state.activate(index).activated) return;
275
268
  this.dispatchEvent(new CustomEvent('activated', { bubbles: true, detail: { index } }));
276
269
  }
277
270
 
278
- /** Rebuild the rendered rows + headers from the core's derived model. */
279
271
  private _rebuild(): void {
280
272
  const headers = new Map<number, SidebarHeaderSpec>();
281
273
  for (const header of this._state.headers) headers.set(header.sectionIndex, header);
@@ -335,9 +327,9 @@ export class AdwSidebar extends HTMLElement {
335
327
  row.type = 'button';
336
328
  row.className = 'adw-sidebar-item';
337
329
  row.setAttribute('role', 'option');
338
- // `visible` / `enabled` are bound to the row's `visible` / `sensitive`
339
- // in create_row (adw-sidebar.c:1382-1383): the row still EXISTS and still
340
- // owns its flat index, it just does not show or respond.
330
+ // `visible` / `enabled` are bound to the row's `visible` / `sensitive` in
331
+ // `create_row`: the row still EXISTS and still owns its flat index, it just does
332
+ // not show or respond.
341
333
  row.hidden = item.visible === false;
342
334
  row.disabled = item.enabled === false;
343
335
 
@@ -362,8 +354,8 @@ export class AdwSidebar extends HTMLElement {
362
354
 
363
355
  row.appendChild(textEl);
364
356
 
365
- // Page mode adds a trailing chevron on every row, the way the
366
- // boxed-list AdwActionRow carries a `go-next-symbolic` arrow.
357
+ // Page mode adds a trailing chevron on every row, the way the boxed-list
358
+ // AdwActionRow carries a `go-next-symbolic` arrow.
367
359
  row.appendChild(createAdwIcon('go-next', 'adw-sidebar-item-arrow'));
368
360
 
369
361
  row.addEventListener('click', () => this._activate(flat.index));
@@ -379,8 +371,8 @@ export class AdwSidebar extends HTMLElement {
379
371
 
380
372
  private _applySelection(): void {
381
373
  const selected = this._state.selected;
382
- // Page mode tracks the selection but never paints it (adw-sidebar.c:2948-2951):
383
- // its rows are plain boxed-list AdwActionRows with no selected state.
374
+ // Page mode tracks the selection but never paints it: its rows are plain
375
+ // boxed-list AdwActionRows with no selected state.
384
376
  const paint = this._state.selectionVisible;
385
377
 
386
378
  this._rows.forEach((row, position) => {
@@ -1,15 +1,10 @@
1
- // <adw-spin-row> — Row with a title/subtitle and a numeric spin control (+/− buttons).
2
- // Attributes: title, subtitle, min, max, step, value
3
- // Events: notify::value (CustomEvent, mirrors GJS GObject signal naming)
1
+ // <adw-spin-row> — row with a title/subtitle and a numeric spin control (+/− buttons).
4
2
  //
5
- // The ADJUSTMENT state machine (value/min/max/step plus the clamp-on-every-
6
- // mutation and the change detection) is HEADLESS and lives in
7
- // `@gjsify/adwaita-core` (ADR 0004) as {@link SpinState}; this element composes it
8
- // and keeps only the DOM render half — the −/input/+ control, the step-precision
9
- // display (the `Adw.SpinRow:digits` concern, which belongs to the widget, not to
10
- // the adjustment) and the `notify::value` event.
11
- // `@gjsify/adwaita-nativescript` composes the same state machine, so both ports
12
- // share one behaviour.
3
+ // The ADJUSTMENT state machine (value/min/max/step plus the clamp-on-every-mutation and
4
+ // the change detection) is HEADLESS and lives in `@gjsify/adwaita-core` (ADR 0004) as
5
+ // {@link SpinState}; this element keeps only the DOM half — the −/input/+ control, the
6
+ // step-precision display (the `Adw.SpinRow:digits` concern, which belongs to the widget
7
+ // rather than to the adjustment) and the `notify::value` event.
13
8
  //
14
9
  // Adapted from Adwaita Web UI Framework (https://github.com/mclellac/adwaita-web).
15
10
  // Copyright (c) 2025 csm. MIT License.
@@ -61,7 +56,6 @@ export class AdwSpinRow extends HTMLElement {
61
56
  this._subtitleEl.className = 'adw-row-subtitle';
62
57
  text.append(this._titleEl, this._subtitleEl);
63
58
 
64
- // Spin control container
65
59
  const control = document.createElement('div');
66
60
  control.className = 'adw-spin-control';
67
61
 
@@ -123,8 +117,7 @@ export class AdwSpinRow extends HTMLElement {
123
117
  }
124
118
 
125
119
  private _renderText() {
126
- // The `string_is_not_empty` label rule, from core — this block was one of
127
- // six hand-rolled copies, all of which omitted the TITLE half.
120
+ // The `string_is_not_empty` label rule, from core — it hides the TITLE too.
128
121
  const labels = deriveRowLabels({
129
122
  title: this.getAttribute('title'),
130
123
  subtitle: this.getAttribute('subtitle'),
@@ -1,27 +1,21 @@
1
1
  // <adw-spinner> — A continuously-spinning loading indicator.
2
2
  // Attributes: size (px; the BOX, floored at the measured minimum 16).
3
3
  //
4
- // THE BOX AND THE RING ARE DIFFERENT THINGS. `adw_spinner_measure`
5
- // (adw-spinner.c:78-81) reports MIN_SIZE as both minimum and natural and has no
6
- // upper bound — `MAX_SIZE` at :15 is defined and never referenced — while
7
- // `adw_spinner_snapshot` (:95-99) hands the widget's real width and height to
8
- // the paintable, which caps only `radius` (adw-spinner-paintable.c:343) and
9
- // still centres on the box (:349-351). So `size="200"` occupies 200px of layout
10
- // and draws a 64px ring in the middle. This element used to BE the ring, which
11
- // is why its suite asserted the opposite of the core's `SPINNER_SIZE_VECTORS`.
4
+ // THE BOX AND THE RING ARE DIFFERENT THINGS. `adw_spinner_measure` reports MIN_SIZE as
5
+ // both minimum and natural and has no upper bound, while `adw_spinner_snapshot` hands
6
+ // the widget's real size to the paintable, which caps only `radius` and still centres on
7
+ // the box. So `size="200"` occupies 200px of layout and draws a 64px ring in the middle.
12
8
  //
13
- // THE ARC IS NOT A ROTATING QUARTER-CIRCLE. It extends, overlaps, contracts and
14
- // idles on an ease-in-out-sine while the figure turns; the arithmetic is
15
- // `@gjsify/adwaita-core`'s `spinnerArc`, shared with the vectors that judge it.
16
- // Expressing that needs a per-frame value, so this element draws an SVG arc and
17
- // advances it from one module-level ticker — not one timer per spinner. CSS
18
- // keyframes cannot express it: a `border-top-color` chase is a fixed 90 degrees,
19
- // which is what this element drew for its whole life.
9
+ // THE ARC IS NOT A ROTATING QUARTER-CIRCLE. It extends, overlaps, contracts and idles on
10
+ // an ease-in-out-sine while the figure turns; the arithmetic is `@gjsify/adwaita-core`'s
11
+ // `spinnerArc`, shared with the vectors that judge it. That needs a per-frame value, so
12
+ // this element draws an SVG arc and advances it from one module-level ticker. CSS
13
+ // keyframes cannot express it — a `border-top-color` chase is a fixed 90 degrees.
20
14
  //
21
- // IT KEEPS SPINNING UNDER REDUCED MOTION. `adw_spinner_paintable_set_widget`
22
- // calls `adw_animation_set_follow_enable_animations_setting (…, FALSE)` (:537)
23
- // — deliberately, because a frozen busy indicator reads as a hang. The old
24
- // `@media (prefers-reduced-motion: reduce) { animation: none }` did exactly that.
15
+ // IT KEEPS SPINNING UNDER REDUCED MOTION, deliberately: a frozen busy indicator reads as
16
+ // a hang, which is why `adw_spinner_paintable_set_widget` calls
17
+ // `adw_animation_set_follow_enable_animations_setting (…, FALSE)`. Do not reintroduce a
18
+ // `@media (prefers-reduced-motion: reduce) { animation: none }` rule for it.
25
19
  //
26
20
  // Reference: refs/libadwaita/src/adw-spinner.c (MIN_SIZE, adw_spinner_measure, the a11y role)
27
21
  // Reference: refs/libadwaita/src/adw-spinner-paintable.c (the animation + geometry)
@@ -42,10 +36,9 @@ const SVG_NS = 'http://www.w3.org/2000/svg';
42
36
  /**
43
37
  * Every mapped spinner, driven by ONE `requestAnimationFrame` loop.
44
38
  *
45
- * The C animates per widget off the frame clock, which costs nothing extra
46
- * because the clock is already ticking. A browser has no such clock, so the
47
- * equivalent is one shared rAF: N spinners on a page must not mean N loops, and
48
- * the loop must not exist at all while none are mapped.
39
+ * The C animates per widget off the frame clock, which costs nothing extra because the
40
+ * clock is already ticking. A browser has no such clock, so the equivalent is one shared
41
+ * rAF: N spinners must not mean N loops, and the loop must not exist while none are mapped.
49
42
  */
50
43
  const mapped = new Set<AdwSpinner>();
51
44
  let frameHandle = 0;
@@ -76,8 +69,8 @@ export class AdwSpinner extends HTMLElement {
76
69
 
77
70
  connectedCallback() {
78
71
  this._build();
79
- // `widget_map_cb` (adw-spinner-paintable.c:181-185) plays the animation
80
- // on MAP, not on construction: an off-screen spinner burns nothing.
72
+ // `widget_map_cb` plays the animation on MAP, not on construction: an off-screen
73
+ // spinner burns nothing.
81
74
  mapped.add(this);
82
75
  this._origin = null;
83
76
  startTicking();
@@ -92,11 +85,9 @@ export class AdwSpinner extends HTMLElement {
92
85
  }
93
86
 
94
87
  /**
95
- * Draw the frame for `now`, in `requestAnimationFrame` timebase.
96
- *
97
- * Public because the shared ticker is a module-level function rather than a
98
- * method — and because a test needs to place the spinner at a known moment
99
- * without waiting for real frames.
88
+ * Draw the frame for `now`, in `requestAnimationFrame` timebase. Public because the
89
+ * shared ticker is a module-level function, and because a test needs to place the
90
+ * spinner at a known moment without waiting for real frames.
100
91
  */
101
92
  drawAt(now: number): void {
102
93
  if (this._origin === null) this._origin = now;
@@ -107,19 +98,18 @@ export class AdwSpinner extends HTMLElement {
107
98
  if (this._svg) return;
108
99
  const svg = document.createElementNS(SVG_NS, 'svg');
109
100
  svg.setAttribute('fill', 'none');
110
- // The ring is decorative; the ROLE is on the host, per adw-spinner.c:124.
101
+ // The ring is decorative; the ROLE is on the host.
111
102
  svg.setAttribute('aria-hidden', 'true');
112
103
 
113
104
  const track = document.createElementNS(SVG_NS, 'circle');
114
105
  track.setAttribute('stroke', 'currentColor');
115
- // `CIRCLE_OPACITY` of the WIDGET's colour (adw-spinner-paintable.c:367-369),
116
- // not a hardcoded grey — so the track follows the text colour everywhere.
106
+ // C's `CIRCLE_OPACITY` of the WIDGET's colour, not a hardcoded grey, so the track
107
+ // follows the text colour everywhere.
117
108
  track.setAttribute('stroke-opacity', String(ADW_SPINNER_TRACK_OPACITY));
118
109
 
119
110
  const arc = document.createElementNS(SVG_NS, 'circle');
120
111
  arc.setAttribute('stroke', 'currentColor');
121
- // `GSK_LINE_CAP_ROUND` (adw-spinner-paintable.c:354). The port's
122
- // `border-top-color` ends were square-cut, visible from 48px up.
112
+ // C's `GSK_LINE_CAP_ROUND`; square-cut ends are visible from 48px up.
123
113
  arc.setAttribute('stroke-linecap', 'round');
124
114
 
125
115
  svg.append(track, arc);
@@ -128,9 +118,8 @@ export class AdwSpinner extends HTMLElement {
128
118
  this._arc = arc;
129
119
  this.replaceChildren(svg);
130
120
 
131
- // `gtk_widget_class_set_accessible_role (…, PROGRESS_BAR)` (adw-spinner.c:124)
132
- // and `GTK_ACCESSIBLE_STATE_BUSY, TRUE` (:139-141). A repo-wide grep for
133
- // either found ZERO hits before this: screen readers announced nothing.
121
+ // C's `gtk_widget_class_set_accessible_role (…, PROGRESS_BAR)` plus
122
+ // `GTK_ACCESSIBLE_STATE_BUSY, TRUE`; without both, screen readers announce nothing.
134
123
  this.setAttribute('role', 'progressbar');
135
124
  this.setAttribute('aria-busy', 'true');
136
125
 
@@ -154,8 +143,7 @@ export class AdwSpinner extends HTMLElement {
154
143
  svg.setAttribute('height', String(diameter));
155
144
  svg.setAttribute('viewBox', `0 0 ${diameter} ${diameter}`);
156
145
 
157
- // The stroke is centred on the path, so the circle sits half a stroke in
158
- // — `radius - line_width / 2` (adw-spinner-paintable.c:364).
146
+ // The stroke is centred on the path, so the circle sits half a stroke in.
159
147
  const radius = (diameter - lineWidth) / 2;
160
148
  this._radius = radius;
161
149
  this._circumference = 2 * Math.PI * radius;
@@ -180,9 +168,9 @@ export class AdwSpinner extends HTMLElement {
180
168
  const { end, length } = spinnerArc(progress);
181
169
  const drawn = length * this._radius;
182
170
  arc.setAttribute('stroke-dasharray', `${drawn} ${Math.max(this._circumference - drawn, 0)}`);
183
- // An SVG circle's path starts at 3 o'clock and runs clockwise, which is
184
- // the same origin and direction the C's angles use — so a radian is a
185
- // path distance and the dash only has to be shifted to `end`.
171
+ // An SVG circle's path starts at 3 o'clock and runs clockwise, the same origin and
172
+ // direction as the C's angles, so a radian is a path distance and the dash only
173
+ // has to be shifted to `end`.
186
174
  arc.setAttribute('stroke-dashoffset', String(-end * this._radius));
187
175
  }
188
176
  }