@gjsify/adwaita-web 0.35.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/dist/adwaita-web.css +433 -33
  2. package/dist/adwaita-web.css.map +1 -1
  3. package/lib/types/adw-shortcut-label.spec.d.ts +2 -0
  4. package/lib/types/breakpoints.d.ts +10 -12
  5. package/lib/types/elements/adw-about-dialog.d.ts +19 -36
  6. package/lib/types/elements/adw-action-row.d.ts +9 -12
  7. package/lib/types/elements/adw-alert-dialog.d.ts +0 -12
  8. package/lib/types/elements/adw-avatar.d.ts +1 -0
  9. package/lib/types/elements/adw-bottom-sheet.d.ts +7 -12
  10. package/lib/types/elements/adw-button-content.d.ts +3 -4
  11. package/lib/types/elements/adw-button-row.d.ts +1 -1
  12. package/lib/types/elements/adw-carousel.d.ts +28 -49
  13. package/lib/types/elements/adw-checks.d.ts +2 -8
  14. package/lib/types/elements/adw-combo-row.d.ts +6 -6
  15. package/lib/types/elements/adw-data-grid.d.ts +5 -10
  16. package/lib/types/elements/adw-drop-down.d.ts +3 -11
  17. package/lib/types/elements/adw-entry-row.d.ts +20 -23
  18. package/lib/types/elements/adw-icon.d.ts +2 -4
  19. package/lib/types/elements/adw-inline-view-switcher.d.ts +18 -31
  20. package/lib/types/elements/adw-navigation-split-view.d.ts +7 -12
  21. package/lib/types/elements/adw-navigation-view.d.ts +2 -10
  22. package/lib/types/elements/adw-overlay-split-view.d.ts +28 -52
  23. package/lib/types/elements/adw-password-entry-row.d.ts +4 -5
  24. package/lib/types/elements/adw-popover.d.ts +2 -7
  25. package/lib/types/elements/adw-preferences-dialog.d.ts +21 -34
  26. package/lib/types/elements/adw-preferences-group.d.ts +12 -14
  27. package/lib/types/elements/adw-progress-bar.d.ts +3 -4
  28. package/lib/types/elements/adw-shortcut-label.d.ts +20 -0
  29. package/lib/types/elements/adw-sidebar.d.ts +5 -10
  30. package/lib/types/elements/adw-spinner.d.ts +3 -5
  31. package/lib/types/elements/adw-split-button.d.ts +6 -16
  32. package/lib/types/elements/adw-switch-row.d.ts +3 -3
  33. package/lib/types/elements/adw-tab-view.d.ts +16 -26
  34. package/lib/types/elements/adw-toast-overlay.d.ts +8 -13
  35. package/lib/types/elements/adw-view-stack.d.ts +7 -17
  36. package/lib/types/elements/adw-view-switcher-bar.d.ts +4 -8
  37. package/lib/types/elements/adw-view-switcher.d.ts +18 -38
  38. package/lib/types/elements/adw-wrap-box.d.ts +7 -17
  39. package/lib/types/elements/view-switcher-dom.d.ts +10 -13
  40. package/lib/types/index.d.ts +1 -0
  41. package/lib/types/source-view/hex.d.ts +7 -13
  42. package/lib/types/style-classes.spec.d.ts +11 -0
  43. package/lib/types/styles.generated.d.ts +1 -1
  44. package/package.json +6 -6
  45. package/scss/_about_dialog.scss +3 -2
  46. package/scss/_action_row.scss +33 -0
  47. package/scss/_avatar.scss +12 -0
  48. package/scss/_button.scss +44 -22
  49. package/scss/_card.scss +11 -8
  50. package/scss/_checks.scss +2 -4
  51. package/scss/_dialog.scss +3 -2
  52. package/scss/_headerbar.scss +23 -0
  53. package/scss/_popover.scss +8 -4
  54. package/scss/_preferences.scss +7 -7
  55. package/scss/_progress_bar.scss +9 -6
  56. package/scss/_reset.scss +1 -1
  57. package/scss/_shortcut_label.scss +80 -0
  58. package/scss/_sidebar.scss +57 -0
  59. package/scss/_status_page.scss +20 -0
  60. package/scss/_style_classes.scss +407 -0
  61. package/scss/_switch.scss +4 -3
  62. package/scss/_variables.scss +25 -0
  63. package/scss/adwaita-skin.scss +12 -0
  64. package/src/adw-about-dialog.spec.ts +34 -49
  65. package/src/adw-action-rows.spec.ts +25 -33
  66. package/src/adw-avatar.spec.ts +51 -20
  67. package/src/adw-banner.spec.ts +5 -8
  68. package/src/adw-button-content.spec.ts +9 -12
  69. package/src/adw-button-row.spec.ts +2 -4
  70. package/src/adw-carousel.spec.ts +40 -53
  71. package/src/adw-checks.spec.ts +36 -47
  72. package/src/adw-drop-down.spec.ts +25 -32
  73. package/src/adw-header-bar.spec.ts +5 -7
  74. package/src/adw-icon.spec.ts +23 -32
  75. package/src/adw-progress-bar.spec.ts +10 -12
  76. package/src/adw-row-state.spec.ts +9 -10
  77. package/src/adw-shortcut-label.spec.ts +112 -0
  78. package/src/adw-sidebar.spec.ts +17 -24
  79. package/src/adw-switch.spec.ts +11 -17
  80. package/src/adw-tab-view.spec.ts +7 -10
  81. package/src/adw-toast-overlay.spec.ts +7 -11
  82. package/src/adw-view-switcher-bar.spec.ts +10 -14
  83. package/src/adw-wrap-box.spec.ts +25 -36
  84. package/src/bottom-sheet.spec.ts +30 -36
  85. package/src/breakpoints.ts +15 -18
  86. package/src/chrome.spec.ts +13 -21
  87. package/src/elements/adw-about-dialog.ts +71 -114
  88. package/src/elements/adw-action-row.ts +22 -37
  89. package/src/elements/adw-alert-dialog.ts +25 -50
  90. package/src/elements/adw-avatar.ts +40 -15
  91. package/src/elements/adw-banner.ts +14 -18
  92. package/src/elements/adw-bottom-sheet.ts +31 -52
  93. package/src/elements/adw-button-content.ts +30 -37
  94. package/src/elements/adw-button-row.ts +14 -24
  95. package/src/elements/adw-carousel.ts +87 -148
  96. package/src/elements/adw-checks.ts +43 -56
  97. package/src/elements/adw-clamp.ts +8 -9
  98. package/src/elements/adw-combo-row.ts +17 -25
  99. package/src/elements/adw-data-grid.ts +26 -38
  100. package/src/elements/adw-dialog.ts +25 -56
  101. package/src/elements/adw-drop-down.ts +31 -51
  102. package/src/elements/adw-entry-row.ts +39 -46
  103. package/src/elements/adw-expander-row.ts +30 -53
  104. package/src/elements/adw-icon.ts +30 -53
  105. package/src/elements/adw-inline-view-switcher.ts +38 -67
  106. package/src/elements/adw-menu-button.ts +28 -58
  107. package/src/elements/adw-navigation-split-view.ts +35 -47
  108. package/src/elements/adw-navigation-view.ts +12 -28
  109. package/src/elements/adw-overlay-split-view.ts +100 -175
  110. package/src/elements/adw-password-entry-row.ts +18 -28
  111. package/src/elements/adw-popover.ts +28 -44
  112. package/src/elements/adw-preferences-dialog.ts +41 -66
  113. package/src/elements/adw-preferences-group.ts +33 -46
  114. package/src/elements/adw-progress-bar.ts +27 -47
  115. package/src/elements/adw-shortcut-label.ts +138 -0
  116. package/src/elements/adw-sidebar.ts +31 -39
  117. package/src/elements/adw-spin-row.ts +7 -14
  118. package/src/elements/adw-spinner.ts +31 -43
  119. package/src/elements/adw-split-button.ts +45 -75
  120. package/src/elements/adw-switch-row.ts +29 -46
  121. package/src/elements/adw-switch.ts +28 -47
  122. package/src/elements/adw-tab-view.ts +62 -103
  123. package/src/elements/adw-toast-overlay.ts +28 -35
  124. package/src/elements/adw-toggle-group.ts +9 -16
  125. package/src/elements/adw-view-stack.ts +32 -51
  126. package/src/elements/adw-view-switcher-bar.ts +26 -50
  127. package/src/elements/adw-view-switcher.ts +60 -110
  128. package/src/elements/adw-window-title.ts +4 -4
  129. package/src/elements/adw-wrap-box.ts +33 -59
  130. package/src/elements/view-switcher-dom.ts +15 -22
  131. package/src/entry-rows.spec.ts +18 -24
  132. package/src/index.ts +13 -15
  133. package/src/navigation-view.spec.ts +7 -10
  134. package/src/preferences-search.ts +11 -15
  135. package/src/preferences.spec.ts +13 -15
  136. package/src/source-view/adw-source-view.ts +6 -19
  137. package/src/source-view/hex.ts +7 -13
  138. package/src/split-button.spec.ts +32 -44
  139. package/src/split-views.spec.ts +83 -149
  140. package/src/style-classes.spec.ts +181 -0
  141. package/src/style-isolation.spec.ts +3 -6
  142. package/src/styles.generated.ts +1 -1
  143. package/src/tab-view.spec.ts +26 -35
  144. package/src/test.browser.mts +4 -0
  145. package/src/view-stack.spec.ts +23 -34
  146. package/src/view-switcher.spec.ts +56 -84
@@ -20,26 +20,14 @@
20
20
  // in libadwaita's emission order.
21
21
  // `menu-activated` — a menu entry was chosen (detail.label / .action / .index).
22
22
  //
23
- // All of the behavior above lives in `@gjsify/adwaita-core` (ADR 0004) as
24
- // {@link SplitButtonState}; this element only paints it and translates DOM
25
- // events into state calls. It used to carry its own copy, and that copy rendered
26
- // the label AND the icon together (libadwaita clears whichever slot it is not
27
- // using), could not clear the label at all, hardcoded "Menu" as the dropdown's
28
- // accessible name, and opened an empty popover when there was no menu. The
29
- // shared vectors in `@gjsify/adwaita-core/conformance` now pin this element to
30
- // the C source — see `src/split-button.spec.ts`.
31
- //
32
- // The last piece of that copy to go was the direction→arrow table, which
33
- // outlived the lift and drew the `open-menu` hamburger for `direction="none"`.
34
- // Only the glyph→mask-class mapping is a renderer concern; see ARROW_MASK_CLASSES.
35
- //
36
- // THE POPOVER WENT THE SAME WAY, and it was the worst of the three copies: a
37
- // 9px (`--button-radius`) surface where libadwaita's is 15px, under a two-layer
38
- // shadow nothing in the vendored stylesheet has, with NO Escape dismissal and NO
39
- // arrow-key navigation at all — a menu you could open with the keyboard and then
40
- // only leave with the mouse. It is now `<adw-popover>`, so those two behaviours
41
- // arrive with the surface rather than having to be written a third time; the
42
- // spec cases for exactly them are in `src/split-button.spec.ts`.
23
+ // All of the behaviour above lives in `@gjsify/adwaita-core` (ADR 0004) as
24
+ // {@link SplitButtonState}; this element only paints it and translates DOM events
25
+ // into state calls, pinned to the C source by the shared vectors in
26
+ // `@gjsify/adwaita-core/conformance` (see `src/split-button.spec.ts`). Of the
27
+ // element's own former copies only the glyph→mask-class mapping is a renderer
28
+ // concern — see ARROW_MASK_CLASSES. The menu surface is `<adw-popover>`, which is
29
+ // what brings the 15px radius, Escape dismissal and arrow-key navigation; do not
30
+ // hand-roll a surface here instead.
43
31
  //
44
32
  // Reference: refs/libadwaita/src/adw-split-button.c (AdwSplitButton)
45
33
  // Reference: refs/adwaita-web/adwaita-web/scss/_split_button.scss
@@ -78,19 +66,17 @@ const VARIANT_CLASSES: Record<string, string> = {
78
66
 
79
67
  /**
80
68
  * libadwaita's arrow GLYPH NAMES mapped onto the mask classes
81
- * `_icons.generated.scss` actually ships. Renderer-specific by nature: the icon
82
- * set here is the `go-*` family, so no `pan-*` mask exists to map onto.
83
- *
84
- * WHICH glyph a direction gets is NOT decided here — {@link splitButtonArrowIcon}
85
- * owns that, and this table is keyed by its output rather than by direction. That
86
- * split is the point: keying by direction is how this element came to draw the
87
- * `open-menu` hamburger for `direction="none"`, where a split button draws the
88
- * down caret (`splitbutton > menubutton > button > arrow.none`,
89
- * _buttons.scss:621-623). The mask mapping is a fact about our asset set; the
90
- * glyph choice is a fact about libadwaita, and only one of them lives here.
69
+ * `_icons.generated.scss` actually ships. Renderer-specific by nature: the icon set
70
+ * here is the `go-*` family, so no `pan-*` mask exists to map onto, and
71
+ * `pan-up-symbolic` reuses `go-down` flipped by the caller rather than drawing a blank
72
+ * arrow.
91
73
  *
92
- * `pan-up-symbolic` has no mask at all, so it reuses `go-down` flipped by the
93
- * caller — the alternative is a blank arrow.
74
+ * WHICH glyph a direction gets is NOT decided here — {@link splitButtonArrowIcon} owns
75
+ * that, and this table is keyed by its OUTPUT rather than by direction. Keying by
76
+ * direction is how this element came to draw the `open-menu` hamburger for
77
+ * `direction="none"`, where a split button draws the down caret
78
+ * (`splitbutton > menubutton > button > arrow.none`). The mask mapping is a fact about
79
+ * our asset set, the glyph choice a fact about libadwaita.
94
80
  */
95
81
  const ARROW_MASK_CLASSES: Readonly<Record<AdwArrowIcon, string>> = {
96
82
  'open-menu-symbolic': 'open-menu',
@@ -100,11 +86,9 @@ const ARROW_MASK_CLASSES: Readonly<Record<AdwArrowIcon, string>> = {
100
86
  'pan-end-symbolic': 'go-next',
101
87
  };
102
88
 
103
- // The `-symbolic` strip AND the token guard that used to live here — as
104
- // `iconClass`, the ONLY copy of the guard in the package — are
105
- // `normalizeIconName`'s now, applied by <adw-icon>. Five other places built the
106
- // same class without the guard; keeping the correct one private is what let
107
- // them stay wrong.
89
+ // The `-symbolic` strip and the single-CSS-token guard belong to `normalizeIconName`,
90
+ // applied by <adw-icon>: never rebuild the mask class by hand, which is how five sites
91
+ // in this package shipped without the guard.
108
92
 
109
93
  /**
110
94
  * Direction → the CSS axis `<adw-popover>` places the surface on. WHICH
@@ -147,7 +131,6 @@ export class AdwSplitButton extends HTMLElement {
147
131
  ];
148
132
  }
149
133
 
150
- /** Whether the dropdown menu is currently open. */
151
134
  get active(): boolean {
152
135
  return this._state.open;
153
136
  }
@@ -164,8 +147,8 @@ export class AdwSplitButton extends HTMLElement {
164
147
 
165
148
  set label(value: string) {
166
149
  this.setAttribute('label', value ?? '');
167
- // Setting the label clears the icon (adw-split-button.c:658-659); mirror
168
- // that in the DOM so the attributes never contradict the state.
150
+ // Setting the label clears the icon; mirror that in the DOM so the
151
+ // attributes never contradict the state.
169
152
  this.removeAttribute('icon-name');
170
153
  }
171
154
 
@@ -179,7 +162,6 @@ export class AdwSplitButton extends HTMLElement {
179
162
  this.removeAttribute('label');
180
163
  }
181
164
 
182
- /** The dropdown menu entries. */
183
165
  get menuItems(): readonly AdwMenuEntry[] {
184
166
  return this._state.menuModel ?? [];
185
167
  }
@@ -198,7 +180,6 @@ export class AdwSplitButton extends HTMLElement {
198
180
  this.setAttribute('dropdown-tooltip', value ?? '');
199
181
  }
200
182
 
201
- /** The direction the popup opens in, and the arrow points. */
202
183
  get direction(): SplitButtonDirection {
203
184
  return this._state.direction;
204
185
  }
@@ -207,7 +188,6 @@ export class AdwSplitButton extends HTMLElement {
207
188
  this.setAttribute('direction', value);
208
189
  }
209
190
 
210
- /** Whether an underline in the label marks a mnemonic. */
211
191
  get useUnderline(): boolean {
212
192
  return this._state.useUnderline;
213
193
  }
@@ -252,8 +232,8 @@ export class AdwSplitButton extends HTMLElement {
252
232
  this._menuEl = document.createElement('adw-popover') as AdwPopover;
253
233
  this._menuEl.classList.add('adw-split-button-menu');
254
234
  this._menuEl.setAttribute('role', 'menu');
255
- // The dropdown half IS a GtkMenuButton (adw-split-button.c:997 passes
256
- // straight through to it), so its popover is a `popover.menu`.
235
+ // The dropdown half IS a GtkMenuButton (the C passes straight through to
236
+ // one), so its popover is a `popover.menu`.
257
237
  this._menuEl.setAttribute('menu', '');
258
238
  this._menuEl.setAttribute('align', 'end');
259
239
 
@@ -315,12 +295,11 @@ export class AdwSplitButton extends HTMLElement {
315
295
  this._render();
316
296
  }
317
297
 
318
- /** Seed label/icon/menu/direction/tooltip from the attributes present at connect time. */
319
298
  private _syncStateFromAttributes(): void {
320
299
  const label = this.getAttribute('label') ?? (this._inlineLabel.length > 0 ? this._inlineLabel : null);
321
300
  const iconName = this.getAttribute('icon-name');
322
301
  // With both present the icon wins — exactly where `set_label();
323
- // set_icon_name();` leaves the widget (adw-split-button.c:749-771).
302
+ // set_icon_name();` leaves the widget.
324
303
  if (label !== null) this._state.setLabel(label);
325
304
  if (iconName !== null) this._state.setIconName(iconName);
326
305
 
@@ -334,14 +313,12 @@ export class AdwSplitButton extends HTMLElement {
334
313
  }
335
314
 
336
315
  /**
337
- * Route a `label` / `icon-name` attribute mutation into the content machine.
338
- *
339
- * C has no independent "unset" for either slot — clearing one is a side
340
- * effect of setting another — so a REMOVED attribute falls back to whichever
341
- * sibling is still present, and to the empty child otherwise. That fallback
342
- * is what makes `removeAttribute('label')` actually drop the label; it used
343
- * to be a silent no-op, so the storybook's icon-only mode rendered the icon
344
- * next to a stale "Save".
316
+ * Route a `label` / `icon-name` attribute mutation into the content machine. C has no
317
+ * independent "unset" for either slot — clearing one is a side effect of setting
318
+ * another — so a REMOVED attribute falls back to whichever sibling is still present,
319
+ * and to the empty child otherwise. Without that fallback
320
+ * `removeAttribute('label')` is a silent no-op and an icon-only button keeps a stale
321
+ * label beside its icon.
345
322
  */
346
323
  private _applyContentAttribute(name: 'label' | 'icon-name', value: string | null): void {
347
324
  if (value !== null) {
@@ -366,8 +343,7 @@ export class AdwSplitButton extends HTMLElement {
366
343
  }
367
344
  this._render();
368
345
  // Focus lands in the menu on open, AFTER _render has built the rows —
369
- // without it the arrow keys the popover now provides would have nothing
370
- // to move from, which is half of why this element had none.
346
+ // otherwise the popover's arrow keys have nothing to move from.
371
347
  if (opened) this._menuEl.items[0]?.focus();
372
348
  }
373
349
 
@@ -375,9 +351,8 @@ export class AdwSplitButton extends HTMLElement {
375
351
  const disabled = this.hasAttribute('disabled');
376
352
  this.classList.toggle('disabled', disabled);
377
353
  this._actionEl.disabled = disabled;
378
- // With neither a menu model nor a popover the dropdown is INSENSITIVE
379
- // (adw-split-button.c:376-378, :394-396) — it must not open an empty
380
- // popover, which is what the old unguarded click handler did.
354
+ // With neither a menu model nor a popover the dropdown is INSENSITIVE: it must
355
+ // not open an empty popover.
381
356
  this._dropdownEl.disabled = disabled || !this._state.dropdownEnabled;
382
357
 
383
358
  this.classList.remove('flat', 'suggested-action', 'destructive-action');
@@ -390,12 +365,10 @@ export class AdwSplitButton extends HTMLElement {
390
365
  this._renderMenu();
391
366
  }
392
367
 
393
- /** The action half + the root style classes libadwaita derives from it. */
394
368
  private _renderContent(): void {
395
369
  const { mode, label, iconName, styleClasses } = this._state;
396
370
 
397
- // `splitbutton[.image-button][.text-button]` — the documented CSS node
398
- // contract (adw-split-button.c:32-50, :145-165).
371
+ // `splitbutton[.image-button][.text-button]` — the documented CSS node contract.
399
372
  this.classList.toggle('image-button', styleClasses.includes('image-button'));
400
373
  this.classList.toggle('text-button', styleClasses.includes('text-button'));
401
374
 
@@ -417,7 +390,6 @@ export class AdwSplitButton extends HTMLElement {
417
390
  else this._actionEl.removeAttribute('aria-label');
418
391
  }
419
392
 
420
- /** The dropdown half: arrow glyph, tooltip/accessible name, expanded state. */
421
393
  private _renderDropdown(): void {
422
394
  const { direction, open } = this._state;
423
395
  const glyph = splitButtonArrowIcon(direction);
@@ -425,18 +397,18 @@ export class AdwSplitButton extends HTMLElement {
425
397
  // No pan-up mask ships yet, so it is the down arrow turned over. Keyed off
426
398
  // the GLYPH, not the direction: the flip belongs to the mask substitution.
427
399
  this._arrowEl.style.transform = glyph === 'pan-up-symbolic' ? 'rotate(180deg)' : '';
428
- // Where the popup goes is core's call (`none` → `down`); only the
429
- // direction → CSS-axis mapping is ours.
400
+ // Where the popup goes is core's call (`none` → `down`); only the direction →
401
+ // CSS-axis mapping is ours.
430
402
  this._menuEl.position = POPOVER_POSITIONS[splitButtonPopupDirection(direction)];
431
403
 
432
- // An empty value RESTORES the translated default (adw-split-button.c:1044-1051)
433
- // instead of leaving the button without an accessible name.
404
+ // An empty value RESTORES the translated default instead of leaving the button
405
+ // without an accessible name.
434
406
  const { text } = resolveDropdownTooltip(this._state.dropdownTooltip);
435
407
  this._dropdownEl.title = text;
436
408
  this._dropdownEl.setAttribute('aria-label', text);
437
409
  this._dropdownEl.setAttribute('aria-expanded', String(open));
438
410
 
439
- // update_state folds both halves onto the root (adw-split-button.c:118-143).
411
+ // update_state folds both halves onto the root.
440
412
  // The browser gives the ACTIVE half of that fold for free — CSS `:active`
441
413
  // matches the ancestors of the pressed element — so only CHECKED, which
442
414
  // the arrow half carries while the menu is open, has to be applied here.
@@ -450,7 +422,6 @@ export class AdwSplitButton extends HTMLElement {
450
422
  this.classList.toggle('active', open);
451
423
  }
452
424
 
453
- /** The popover contents, one button per menu entry, dispatched BY POSITION. */
454
425
  private _renderMenu(): void {
455
426
  this._menuEl.replaceChildren();
456
427
  this._menuEl.open = this._state.open;
@@ -458,16 +429,15 @@ export class AdwSplitButton extends HTMLElement {
458
429
  for (const [index, entry] of (this._state.menuModel ?? []).entries()) {
459
430
  const item = document.createElement('button');
460
431
  item.type = 'button';
461
- // `.adw-popover-item` is what makes the row navigable: it is the
462
- // selector `<adw-popover>` walks for arrow/Home/End/Enter, which this
463
- // element never had.
432
+ // `.adw-popover-item` is what makes the row navigable: it is the selector
433
+ // `<adw-popover>` walks for arrow/Home/End/Enter.
464
434
  item.className = 'adw-popover-item adw-split-button-menu-item';
465
435
  item.setAttribute('role', 'menuitem');
466
436
  item.tabIndex = -1;
467
437
  item.textContent = entry.label;
468
438
  item.addEventListener('click', () => {
469
- // By position, never by label: two entries called "Copy" are
470
- // legal and must dispatch their own action (c:385-388).
439
+ // By position, never by label: two entries called "Copy" are legal
440
+ // and must each dispatch their own action.
471
441
  const activated = this._state.activateMenuEntry(index);
472
442
  if (activated === null) return;
473
443
  this.dispatchEvent(
@@ -1,26 +1,14 @@
1
- // <adw-switch-row> — Row with a title/subtitle and a trailing toggle switch.
2
- // Attributes: title, subtitle, active (boolean)
3
- // Events: notify::active (CustomEvent, mirrors GJS GObject signal naming)
1
+ // <adw-switch-row> — row with a title/subtitle and a trailing `<adw-switch>`.
4
2
  //
5
- // The ACTIVE flag, its notify rule and the toggle-on-row-activation are HEADLESS
6
- // and live in `@gjsify/adwaita-core` (ADR 0004) as {@link SwitchRowState}; this
7
- // element composes it and keeps only the DOM render half — the checkbox, the
8
- // label column and the `notify::active` event.
9
- // `@gjsify/adwaita-nativescript` composes the same state, so both ports share one
10
- // behaviour. Two things this fixes, both from adw-switch-row.c:
11
- // - a PROGRAMMATIC `row.active = true` now notifies. The DOM does not fire
12
- // `change` for `.checked =`, so keying the event off the checkbox meant the
13
- // row was silent — while the NativeScript port, whose `checkedChange` DOES
14
- // fire on a programmatic write, notified. libadwaita has exactly one notify
15
- // path (`slider_notify_active_cb`, C:66-77) and it cannot see the origin.
16
- // - clicking the ROW toggles. `adw_switch_row_init` points the
17
- // activatable-widget at the slider (C:160-162), so the title is part of the
18
- // control; here only the handle itself worked.
19
- //
20
- // The TOGGLE itself is `<adw-switch>`. It used to be built here by hand —
21
- // `label.adw-switch > input[type=checkbox] + span.adw-switch-slider` — and
22
- // `<adw-expander-row>` built the identical thing, over a stylesheet block that
23
- // was a character-for-character copy of this row's.
3
+ // The ACTIVE flag, its notify rule and the toggle-on-row-activation are HEADLESS and live
4
+ // in `@gjsify/adwaita-core` (ADR 0004) as {@link SwitchRowState}; this element keeps only
5
+ // the DOM half — the label column, the switch and the `notify::active` event. Two rules
6
+ // from adw-switch-row.c that a DOM-first implementation gets wrong:
7
+ // - a PROGRAMMATIC `row.active = true` MUST notify. The DOM fires no `change` for
8
+ // `.checked =`, so an event keyed off the checkbox leaves the row silent; libadwaita
9
+ // has exactly one notify path (`slider_notify_active_cb`) and it cannot see the origin.
10
+ // - clicking the ROW toggles: `adw_switch_row_init` points the activatable-widget at the
11
+ // slider, so the title is part of the control, not just the handle.
24
12
  //
25
13
  // Adapted from Adwaita Web UI Framework (https://github.com/mclellac/adwaita-web).
26
14
  // Copyright (c) 2025 csm. MIT License.
@@ -30,13 +18,12 @@
30
18
 
31
19
  import { SwitchRowState, deriveRowLabels } from '@gjsify/adwaita-core';
32
20
 
33
- // SIDE-EFFECT import, deliberately separate from the type import below: it is
34
- // what guarantees `adw-switch` is defined before this module's own
35
- // `customElements.define` can upgrade a server-rendered `<adw-switch-row>` and
36
- // build one. A combined `import { AdwSwitch }` would NOT do it — the binding is
37
- // only ever used in type position here, and this package compiles without
38
- // `verbatimModuleSyntax`, so TypeScript would elide the whole statement and take
39
- // the registration with it.
21
+ // SIDE-EFFECT import, deliberately separate from the type import below: it guarantees
22
+ // `adw-switch` is defined before this module's `customElements.define` can upgrade a
23
+ // server-rendered `<adw-switch-row>` and build one. A combined `import { AdwSwitch }`
24
+ // would NOT do it — the binding is only used in type position, and this package compiles
25
+ // without `verbatimModuleSyntax`, so TypeScript would elide the statement and take the
26
+ // registration with it.
40
27
  import './adw-switch.js';
41
28
  import type { AdwSwitch } from './adw-switch.js';
42
29
 
@@ -80,23 +67,20 @@ export class AdwSwitchRow extends HTMLElement {
80
67
 
81
68
  this._state.setActive(this.hasAttribute('active'));
82
69
  this._switchEl.active = this._state.active;
83
- // `gtk_widget_class_set_accessible_role (…, GTK_ACCESSIBLE_ROLE_SWITCH)`
84
- // plus the initial CHECKED state (adw-switch-row.c:147, :155-158).
70
+ // C's `GTK_ACCESSIBLE_ROLE_SWITCH` plus the initial CHECKED state.
85
71
  this.setAttribute('role', 'switch');
86
72
  this.setAttribute('aria-checked', String(this._state.active));
87
73
 
88
74
  this._switchEl.addEventListener('notify::active', (event) => {
89
- // The ROW is the public event surface — libadwaita has exactly one
90
- // notify path for this property (`slider_notify_active_cb`, C:66-77).
91
- // Letting the composed switch's own identically-named event escape
92
- // would make every listener on the row see each toggle twice.
75
+ // The ROW is the public event surface, and libadwaita has exactly one notify
76
+ // path for the property. Letting the composed switch's identically-named event
77
+ // escape would make every listener on the row see each toggle twice.
93
78
  event.stopPropagation();
94
79
  this._apply(this._state.setActive(this._switchEl.active));
95
80
  });
96
81
 
97
- // Activating the row inverts the state (adw-switch-row.c:23-27). A click
98
- // that landed on the switch itself is already handled by the switch —
99
- // toggling again here would immediately undo it.
82
+ // Activating the row inverts the state. A click that landed on the switch itself is
83
+ // already handled there; toggling again here would undo it.
100
84
  this.addEventListener('click', (event) => {
101
85
  if (event.target instanceof Node && this._switchEl.contains(event.target)) return;
102
86
  this._apply(this._state.activate());
@@ -112,17 +96,17 @@ export class AdwSwitchRow extends HTMLElement {
112
96
  }
113
97
 
114
98
  /**
115
- * Push a state transition out to the DOM and emit `notify::active` — but
116
- * only when something actually changed, which is the `g_object_notify`
117
- * gate libadwaita puts on the same transition (C:224-225 → :76).
99
+ * Push a state transition out to the DOM and emit `notify::active`, but only when
100
+ * something actually changed — the `g_object_notify` gate libadwaita puts on the same
101
+ * transition.
118
102
  */
119
103
  private _apply(changed: boolean) {
120
104
  if (!changed) return;
121
- // Idempotent on the switch's side too, so this write does not bounce
122
- // back through the `notify::active` listener as a second transition.
105
+ // Idempotent on the switch's side too, so this write does not bounce back through
106
+ // the `notify::active` listener as a second transition.
123
107
  this._switchEl.active = this._state.active;
124
108
  this.toggleAttribute('active', this._state.active);
125
- // The a11y state libadwaita updates alongside the notify (C:71-74).
109
+ // The a11y state libadwaita updates alongside the notify.
126
110
  this.setAttribute('aria-checked', String(this._state.active));
127
111
  this.dispatchEvent(
128
112
  new CustomEvent('notify::active', {
@@ -138,8 +122,7 @@ export class AdwSwitchRow extends HTMLElement {
138
122
  subtitle: this.getAttribute('subtitle'),
139
123
  });
140
124
  this._titleEl.textContent = title;
141
- // `string_is_not_empty` applies to the TITLE too (adw-action-row.ui:49-53,
142
- // inherited — AdwSwitchRow derives from AdwActionRow, adw-switch-row.c:50).
125
+ // `string_is_not_empty` applies to the TITLE too, inherited from AdwActionRow.
143
126
  this._titleEl.hidden = !titleVisible;
144
127
  this._subtitleEl.textContent = subtitle;
145
128
  this._subtitleEl.hidden = !subtitleVisible;
@@ -1,43 +1,27 @@
1
1
  // <adw-switch> — the Adwaita toggle, as an element. A 44×24 track with a hidden
2
2
  // checkbox behind it and a knob that slides on `active`.
3
3
  //
4
- // WHY IT EXISTS: `<adw-switch-row>` and `<adw-expander-row>` each built
5
- // `label.adw-switch > input[type=checkbox] + span.adw-switch-slider` by hand, and
6
- // `_expander_row.scss:73-121` was a character-for-character copy of
7
- // `_switch_row.scss:15-63`. The cause was structural — `_switch_row.scss:9`
8
- // opened `adw-switch-row {` and nested everything inside it, so `.adw-switch` did
9
- // not exist outside a switch row and the expander could not reuse it. The styles
10
- // are now the unscoped `scss/_switch.scss` and the markup is here.
4
+ // The styles are the UNSCOPED `scss/_switch.scss`, so `.adw-switch` exists outside a
5
+ // switch row and `<adw-expander-row>` can reuse it instead of copying the block.
11
6
  //
12
- // NO CORE STATE MACHINE. The state is one boolean with no derivation, no
13
- // ordering and no notify subtlety beyond "on change" — ADR 0004 is explicit that
14
- // a widget with genuinely trivial behaviour does not get a core class, and the
15
- // `active` ATTRIBUTE is the state (`toggleAttribute` is idempotent by
16
- // definition, so "notify only on a real change" needs no guard of its own).
17
- // `GtkSwitch` does carry a two-phase `active`/`state` pair for async toggles,
18
- // but `refs/gtk` is EMPTY in this tree so that is unverifiable here, and neither
19
- // this port nor `@gjsify/adwaita-nativescript` models it — see the ROW's state
20
- // (`SwitchRowState`), which is where the notify rule that IS derived from C
21
- // lives.
7
+ // NO CORE STATE MACHINE. The state is one boolean with no derivation, no ordering and no
8
+ // notify subtlety beyond "on change" — ADR 0004 is explicit that trivial behaviour gets
9
+ // no core class, and the `active` ATTRIBUTE is the state (`toggleAttribute` is idempotent,
10
+ // so "notify only on a real change" needs no guard). `GtkSwitch` does carry a two-phase
11
+ // `active`/`state` pair for async toggles, but `refs/gtk` is an uninitialized submodule
12
+ // here so that is unverifiable, and neither this port nor
13
+ // `@gjsify/adwaita-nativescript` models it. The notify rule that IS derived from C lives
14
+ // in the ROW's `SwitchRowState`.
22
15
  //
23
- // Attributes:
24
- // active — boolean; reflects (and drives) the toggled state.
25
- // disabled — boolean; the control does not respond and is dimmed.
26
- // Properties:
27
- // active — the toggled state (get/set).
28
- // disabled — whether the control is inert (get/set).
29
- // Events:
30
- // `notify::active` (CustomEvent, bubbles, detail = { active }) — mirrors the
31
- // GObject property-notify; fires on every change, programmatic included.
32
- // A widget that COMPOSES this one and publishes its own `notify::active`
33
- // (`<adw-switch-row>`) stops this one at the switch, so the row stays the
34
- // single public event surface.
16
+ // `notify::active` (CustomEvent, bubbles, detail `{ active }`) fires on every change,
17
+ // programmatic included. A widget that COMPOSES this one and publishes its own
18
+ // `notify::active` (`<adw-switch-row>`) stops this one at the switch, so the row stays
19
+ // the single public event surface.
35
20
  //
36
- // A11Y: the checkbox is left bare — no `role="switch"` — because the widgets
37
- // that host one claim that role themselves (`adw_switch_row_init` sets
38
- // `GTK_ACCESSIBLE_ROLE_SWITCH` on the ROW, adw-switch-row.c:147), and a nested
39
- // second switch role would be announced twice. This is exactly the markup that
40
- // shipped inside the two rows before the extraction.
21
+ // A11Y: the checkbox is left bare — no `role="switch"` — because the widgets that host
22
+ // one claim that role themselves (`adw_switch_row_init` sets
23
+ // `GTK_ACCESSIBLE_ROLE_SWITCH` on the ROW) and a nested second switch role would be
24
+ // announced twice.
41
25
  //
42
26
  // Reference: refs/adwaita-web/adwaita-web/scss/_switch.scss
43
27
  // Reference: refs/libadwaita/src/stylesheet/widgets/_switch.scss
@@ -60,11 +44,10 @@ export class AdwSwitch extends HTMLElement {
60
44
  }
61
45
 
62
46
  set active(value: boolean) {
63
- // `toggleAttribute` with a force flag is a no-op when the attribute is
64
- // already in the wanted state, so re-setting the current value mutates
65
- // nothing and `attributeChangedCallback` — and therefore the notify —
66
- // never runs. That idempotence is what a state class would otherwise be
67
- // holding.
47
+ // `toggleAttribute` with a force flag is a no-op when the attribute already holds
48
+ // the wanted state, so re-setting the current value never runs
49
+ // `attributeChangedCallback` and never notifies. That idempotence is what a state
50
+ // class would otherwise hold.
68
51
  this.toggleAttribute('active', !!value);
69
52
  }
70
53
 
@@ -89,18 +72,16 @@ export class AdwSwitch extends HTMLElement {
89
72
  this._slider.className = 'adw-switch-slider';
90
73
  this.replaceChildren(this._input, this._slider);
91
74
 
92
- // Keyboard (Space on the focused checkbox) and a direct programmatic
93
- // `input.click()` both arrive here; the attribute is the state, so this
94
- // just writes it and lets attributeChangedCallback do the rest.
75
+ // Keyboard (Space on the focused checkbox) and a programmatic `input.click()` both
76
+ // arrive here; the attribute is the state, so this writes it and lets
77
+ // attributeChangedCallback do the rest.
95
78
  this._input.addEventListener('change', () => {
96
79
  this.active = this._input.checked;
97
80
  });
98
81
 
99
- // The slider covers the whole track (`inset: 0`), so every pointer click
100
- // lands on it rather than on the 0×0 checkbox — this is the toggle the
101
- // wrapping `<label>` used to provide for free. A click that DID reach the
102
- // checkbox is already handled by the listener above; toggling again here
103
- // would immediately undo it.
82
+ // The slider covers the whole track (`inset: 0`), so every pointer click lands on
83
+ // it rather than on the 0×0 checkbox. A click that DID reach the checkbox is
84
+ // already handled by the listener above; toggling again here would undo it.
104
85
  this.addEventListener('click', (event) => {
105
86
  if (this.disabled) return;
106
87
  if (event.target === this._input) return;