@gjsify/adwaita-web 0.36.0 → 0.38.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 (151) hide show
  1. package/dist/adwaita-web.css +504 -34
  2. package/dist/adwaita-web.css.map +1 -1
  3. package/lib/types/accent.d.ts +28 -0
  4. package/lib/types/adw-accent.spec.d.ts +2 -0
  5. package/lib/types/adw-shortcut-label.spec.d.ts +2 -0
  6. package/lib/types/breakpoints.d.ts +10 -12
  7. package/lib/types/elements/adw-about-dialog.d.ts +19 -36
  8. package/lib/types/elements/adw-action-row.d.ts +9 -12
  9. package/lib/types/elements/adw-alert-dialog.d.ts +0 -12
  10. package/lib/types/elements/adw-avatar.d.ts +1 -0
  11. package/lib/types/elements/adw-bottom-sheet.d.ts +7 -12
  12. package/lib/types/elements/adw-button-content.d.ts +3 -4
  13. package/lib/types/elements/adw-button-row.d.ts +1 -1
  14. package/lib/types/elements/adw-carousel.d.ts +28 -49
  15. package/lib/types/elements/adw-checks.d.ts +2 -8
  16. package/lib/types/elements/adw-combo-row.d.ts +6 -6
  17. package/lib/types/elements/adw-data-grid.d.ts +5 -10
  18. package/lib/types/elements/adw-drop-down.d.ts +3 -11
  19. package/lib/types/elements/adw-entry-row.d.ts +20 -23
  20. package/lib/types/elements/adw-icon.d.ts +2 -4
  21. package/lib/types/elements/adw-inline-view-switcher.d.ts +18 -31
  22. package/lib/types/elements/adw-navigation-split-view.d.ts +7 -12
  23. package/lib/types/elements/adw-navigation-view.d.ts +2 -10
  24. package/lib/types/elements/adw-overlay-split-view.d.ts +28 -52
  25. package/lib/types/elements/adw-password-entry-row.d.ts +4 -5
  26. package/lib/types/elements/adw-popover.d.ts +2 -7
  27. package/lib/types/elements/adw-preferences-dialog.d.ts +21 -34
  28. package/lib/types/elements/adw-preferences-group.d.ts +12 -14
  29. package/lib/types/elements/adw-progress-bar.d.ts +3 -4
  30. package/lib/types/elements/adw-shortcut-label.d.ts +20 -0
  31. package/lib/types/elements/adw-sidebar.d.ts +5 -10
  32. package/lib/types/elements/adw-spinner.d.ts +3 -5
  33. package/lib/types/elements/adw-split-button.d.ts +6 -16
  34. package/lib/types/elements/adw-switch-row.d.ts +3 -3
  35. package/lib/types/elements/adw-tab-view.d.ts +16 -26
  36. package/lib/types/elements/adw-toast-overlay.d.ts +8 -13
  37. package/lib/types/elements/adw-view-stack.d.ts +7 -17
  38. package/lib/types/elements/adw-view-switcher-bar.d.ts +4 -8
  39. package/lib/types/elements/adw-view-switcher.d.ts +18 -38
  40. package/lib/types/elements/adw-wrap-box.d.ts +7 -17
  41. package/lib/types/elements/view-switcher-dom.d.ts +10 -13
  42. package/lib/types/index.d.ts +3 -0
  43. package/lib/types/source-view/hex.d.ts +7 -13
  44. package/lib/types/style-classes.spec.d.ts +11 -0
  45. package/lib/types/styles.generated.d.ts +1 -1
  46. package/package.json +18 -7
  47. package/scss/_about_dialog.scss +3 -2
  48. package/scss/_action_row.scss +33 -0
  49. package/scss/_avatar.scss +12 -0
  50. package/scss/_button.scss +44 -22
  51. package/scss/_card.scss +11 -8
  52. package/scss/_checks.scss +2 -4
  53. package/scss/_dialog.scss +3 -2
  54. package/scss/_headerbar.scss +23 -0
  55. package/scss/_popover.scss +8 -4
  56. package/scss/_preferences.scss +7 -7
  57. package/scss/_progress_bar.scss +9 -6
  58. package/scss/_reset.scss +1 -1
  59. package/scss/_shortcut_label.scss +80 -0
  60. package/scss/_sidebar.scss +57 -0
  61. package/scss/_status_page.scss +20 -0
  62. package/scss/_style_classes.scss +407 -0
  63. package/scss/_switch.scss +4 -3
  64. package/scss/_theme.scss +20 -2
  65. package/scss/_variables.scss +34 -1
  66. package/scss/adwaita-skin.scss +12 -0
  67. package/src/accent.ts +74 -0
  68. package/src/adw-about-dialog.spec.ts +34 -49
  69. package/src/adw-accent.spec.ts +105 -0
  70. package/src/adw-action-rows.spec.ts +25 -33
  71. package/src/adw-avatar.spec.ts +51 -20
  72. package/src/adw-banner.spec.ts +5 -8
  73. package/src/adw-button-content.spec.ts +9 -12
  74. package/src/adw-button-row.spec.ts +2 -4
  75. package/src/adw-carousel.spec.ts +40 -53
  76. package/src/adw-checks.spec.ts +36 -47
  77. package/src/adw-drop-down.spec.ts +25 -32
  78. package/src/adw-header-bar.spec.ts +5 -7
  79. package/src/adw-icon.spec.ts +23 -32
  80. package/src/adw-progress-bar.spec.ts +10 -12
  81. package/src/adw-row-state.spec.ts +9 -10
  82. package/src/adw-shortcut-label.spec.ts +112 -0
  83. package/src/adw-sidebar.spec.ts +17 -24
  84. package/src/adw-switch.spec.ts +11 -17
  85. package/src/adw-tab-view.spec.ts +7 -10
  86. package/src/adw-toast-overlay.spec.ts +7 -11
  87. package/src/adw-view-switcher-bar.spec.ts +10 -14
  88. package/src/adw-wrap-box.spec.ts +25 -36
  89. package/src/bottom-sheet.spec.ts +30 -36
  90. package/src/breakpoints.ts +15 -18
  91. package/src/chrome.spec.ts +13 -21
  92. package/src/elements/adw-about-dialog.ts +71 -114
  93. package/src/elements/adw-action-row.ts +22 -37
  94. package/src/elements/adw-alert-dialog.ts +25 -50
  95. package/src/elements/adw-avatar.ts +40 -15
  96. package/src/elements/adw-banner.ts +14 -18
  97. package/src/elements/adw-bottom-sheet.ts +31 -52
  98. package/src/elements/adw-button-content.ts +30 -37
  99. package/src/elements/adw-button-row.ts +14 -24
  100. package/src/elements/adw-carousel.ts +87 -148
  101. package/src/elements/adw-checks.ts +43 -56
  102. package/src/elements/adw-clamp.ts +8 -9
  103. package/src/elements/adw-combo-row.ts +17 -25
  104. package/src/elements/adw-data-grid.ts +26 -38
  105. package/src/elements/adw-dialog.ts +25 -56
  106. package/src/elements/adw-drop-down.ts +31 -51
  107. package/src/elements/adw-entry-row.ts +39 -46
  108. package/src/elements/adw-expander-row.ts +30 -53
  109. package/src/elements/adw-icon.ts +30 -53
  110. package/src/elements/adw-inline-view-switcher.ts +38 -67
  111. package/src/elements/adw-menu-button.ts +28 -58
  112. package/src/elements/adw-navigation-split-view.ts +35 -47
  113. package/src/elements/adw-navigation-view.ts +12 -28
  114. package/src/elements/adw-overlay-split-view.ts +100 -175
  115. package/src/elements/adw-password-entry-row.ts +18 -28
  116. package/src/elements/adw-popover.ts +28 -44
  117. package/src/elements/adw-preferences-dialog.ts +41 -66
  118. package/src/elements/adw-preferences-group.ts +33 -46
  119. package/src/elements/adw-progress-bar.ts +27 -47
  120. package/src/elements/adw-shortcut-label.ts +138 -0
  121. package/src/elements/adw-sidebar.ts +31 -39
  122. package/src/elements/adw-spin-row.ts +7 -14
  123. package/src/elements/adw-spinner.ts +31 -43
  124. package/src/elements/adw-split-button.ts +45 -75
  125. package/src/elements/adw-switch-row.ts +29 -46
  126. package/src/elements/adw-switch.ts +28 -47
  127. package/src/elements/adw-tab-view.ts +62 -103
  128. package/src/elements/adw-toast-overlay.ts +28 -35
  129. package/src/elements/adw-toggle-group.ts +9 -16
  130. package/src/elements/adw-view-stack.ts +32 -51
  131. package/src/elements/adw-view-switcher-bar.ts +26 -50
  132. package/src/elements/adw-view-switcher.ts +60 -110
  133. package/src/elements/adw-window-title.ts +4 -4
  134. package/src/elements/adw-wrap-box.ts +33 -59
  135. package/src/elements/view-switcher-dom.ts +15 -22
  136. package/src/entry-rows.spec.ts +18 -24
  137. package/src/index.ts +24 -15
  138. package/src/navigation-view.spec.ts +7 -10
  139. package/src/preferences-search.ts +11 -15
  140. package/src/preferences.spec.ts +13 -15
  141. package/src/source-view/adw-source-view.ts +6 -19
  142. package/src/source-view/hex.ts +7 -13
  143. package/src/split-button.spec.ts +32 -44
  144. package/src/split-views.spec.ts +83 -149
  145. package/src/style-classes.spec.ts +181 -0
  146. package/src/style-isolation.spec.ts +3 -6
  147. package/src/styles.generated.ts +1 -1
  148. package/src/tab-view.spec.ts +26 -35
  149. package/src/test.browser.mts +6 -0
  150. package/src/view-stack.spec.ts +23 -34
  151. package/src/view-switcher.spec.ts +56 -84
@@ -3,9 +3,7 @@
3
3
  // `flex-wrap: wrap` with a gap). Mirrors Adw.WrapBox.
4
4
  //
5
5
  // Attributes — all fourteen of the widget's properties, each with a
6
- // `notify::<property>` (adw-wrap-box.c:284-497). The element used to carry nine
7
- // attributes and notify for two, so a consumer binding to any of the other
8
- // twelve was listening to a signal that could not fire.
6
+ // `notify::<property>`:
9
7
  //
10
8
  // child-spacing / line-spacing (int >= 0, default 0) -> column-gap / row-gap
11
9
  // child-spacing-unit / line-spacing-unit ("px" | "pt" | "sp", default "px")
@@ -19,10 +17,9 @@
19
17
  // wrap-policy ("minimum" | "natural", default "natural")
20
18
  // natural-line-length (int >= -1, default -1 = unset) + -unit
21
19
  //
22
- // The property CONTRACT — the normalisers and the justify/align/last-line
23
- // decision — is `@gjsify/adwaita-core`'s `wrap-box.ts`, held to
24
- // `WRAP_BOX_LINE_VECTORS` and friends, which the NativeScript suite drives too.
25
- // This element used to carry its own copy of all of it.
20
+ // The property CONTRACT — the normalisers and the justify/align/last-line decision —
21
+ // is `@gjsify/adwaita-core`'s `wrap-box.ts`, held to `WRAP_BOX_LINE_VECTORS` and
22
+ // friends, which the NativeScript suite drives too.
26
23
  //
27
24
  // Reference: refs/libadwaita/src/adw-wrap-box.c / adw-wrap-layout.c (Adw.WrapBox)
28
25
  // Reference: refs/adwaita-web/adwaita-web/scss/_wrap_box.scss
@@ -72,12 +69,10 @@ const PROPERTY_ATTRIBUTES = [
72
69
  type PropertyValue = string | number | boolean;
73
70
 
74
71
  /**
75
- * `align` as a `justify-content` keyword.
76
- *
77
- * C offsets the whole line block by `roundf (length_delta * align)` — a
78
- * continuum. Flexbox has three main-axis positions, so the nearest one is taken;
79
- * that is the renderer's approximation, not libadwaita's rule, which is why it
80
- * lives here and not in the conformance table.
72
+ * `align` as a `justify-content` keyword. C offsets the whole line block by
73
+ * `roundf (length_delta * align)` — a continuum — while flexbox has three main-axis
74
+ * positions, so the nearest one is taken. That approximation is the renderer's, not
75
+ * libadwaita's rule, which is why it lives here and not in the conformance table.
81
76
  */
82
77
  function alignToJustifyContent(align: number): string {
83
78
  if (align < 0.25) return 'flex-start';
@@ -92,8 +87,6 @@ export class AdwWrapBox extends HTMLElement {
92
87
  return [...PROPERTY_ATTRIBUTES];
93
88
  }
94
89
 
95
- // --- properties ---
96
-
97
90
  /** Spacing between children on the same line, in `child-spacing-unit`. */
98
91
  get childSpacing(): number {
99
92
  return normalizeWrapBoxSpacing(this.getAttribute('child-spacing'));
@@ -103,7 +96,6 @@ export class AdwWrapBox extends HTMLElement {
103
96
  this.setAttribute('child-spacing', String(value));
104
97
  }
105
98
 
106
- /** The unit `child-spacing` is written in. Defaults to `px`, as in C. */
107
99
  get childSpacingUnit(): AdwLengthUnit {
108
100
  return normalizeWrapBoxLengthUnit(this.getAttribute('child-spacing-unit'));
109
101
  }
@@ -121,7 +113,6 @@ export class AdwWrapBox extends HTMLElement {
121
113
  this.setAttribute('line-spacing', String(value));
122
114
  }
123
115
 
124
- /** The unit `line-spacing` is written in. Defaults to `px`, as in C. */
125
116
  get lineSpacingUnit(): AdwLengthUnit {
126
117
  return normalizeWrapBoxLengthUnit(this.getAttribute('line-spacing-unit'));
127
118
  }
@@ -139,7 +130,6 @@ export class AdwWrapBox extends HTMLElement {
139
130
  this.setAttribute('align', String(value));
140
131
  }
141
132
 
142
- /** Whether and how each line is stretched to fill the widget. */
143
133
  get justify(): 'none' | 'fill' | 'spread' {
144
134
  return normalizeWrapBoxJustify(this.getAttribute('justify'));
145
135
  }
@@ -166,7 +156,6 @@ export class AdwWrapBox extends HTMLElement {
166
156
  this.setAttribute('wrap-policy', value);
167
157
  }
168
158
 
169
- /** The direction children are packed in each line. */
170
159
  get packDirection(): 'start-to-end' | 'end-to-start' {
171
160
  return normalizeWrapBoxPackDirection(this.getAttribute('pack-direction'));
172
161
  }
@@ -184,7 +173,6 @@ export class AdwWrapBox extends HTMLElement {
184
173
  this.toggleAttribute('wrap-reverse', !!value);
185
174
  }
186
175
 
187
- /** Whether every line takes the same amount of space. */
188
176
  get lineHomogeneous(): boolean {
189
177
  return this.hasAttribute('line-homogeneous');
190
178
  }
@@ -202,7 +190,6 @@ export class AdwWrapBox extends HTMLElement {
202
190
  this.setAttribute('natural-line-length', String(value));
203
191
  }
204
192
 
205
- /** The unit `natural-line-length` is written in. Defaults to `px`, as in C. */
206
193
  get naturalLineLengthUnit(): AdwLengthUnit {
207
194
  return normalizeWrapBoxLengthUnit(this.getAttribute('natural-line-length-unit'));
208
195
  }
@@ -211,7 +198,6 @@ export class AdwWrapBox extends HTMLElement {
211
198
  this.setAttribute('natural-line-length-unit', value);
212
199
  }
213
200
 
214
- /** The axis children are packed along. */
215
201
  get orientation(): 'horizontal' | 'vertical' {
216
202
  return this.getAttribute('orientation') === 'vertical' ? 'vertical' : 'horizontal';
217
203
  }
@@ -220,37 +206,31 @@ export class AdwWrapBox extends HTMLElement {
220
206
  this.setAttribute('orientation', value);
221
207
  }
222
208
 
223
- // --- child list (adw_wrap_box_insert_child_after / reorder / remove_all) ---
224
- //
225
- // `adw_wrap_box_append`, `_prepend` and `_remove` are deliberately NOT
226
- // redeclared: `ParentNode.append` / `.prepend` and `Node.removeChild` are
227
- // those three, with the same semantics, and shadowing them would give this
228
- // element two spellings of one operation. Only the three the DOM has no
229
- // counterpart for are added.
209
+ // `adw_wrap_box_append`, `_prepend` and `_remove` are deliberately NOT redeclared:
210
+ // `ParentNode.append` / `.prepend` and `Node.removeChild` ARE those three, with the
211
+ // same semantics, and shadowing them would give this element two spellings of one
212
+ // operation. Only the three the DOM has no counterpart for are added.
230
213
 
231
- /** The element children, which are what the layout actually lays out. */
232
214
  private get _children(): Element[] {
233
215
  return [...this.children];
234
216
  }
235
217
 
236
218
  /**
237
- * `adw_wrap_box_insert_child_after` (adw-wrap-box.c:1283-1300).
238
- *
239
- * A NULL/absent `sibling` inserts at the FIRST position, not the last —
240
- * `gtk_widget_insert_after`'s documented rule, pinned by
241
- * `WRAP_BOX_CHILD_ORDER_VECTORS`. Returns whether the insert happened; a
242
- * refusal is where C would have hit a `g_return_if_fail`.
219
+ * `adw_wrap_box_insert_child_after`. A NULL/absent `sibling` inserts at the FIRST
220
+ * position, not the last — `gtk_widget_insert_after`'s documented rule, pinned by
221
+ * `WRAP_BOX_CHILD_ORDER_VECTORS`. Returns whether the insert happened; a refusal is
222
+ * where C would have hit a `g_return_if_fail`.
243
223
  */
244
224
  insertChildAfter(child: Element, sibling: Element | null = null): boolean {
245
225
  return this._applyOrder('insert-after', child, sibling);
246
226
  }
247
227
 
248
- /** `adw_wrap_box_reorder_child_after` (adw-wrap-box.c:1315-1332). Same NULL rule. */
228
+ /** `adw_wrap_box_reorder_child_after`. Same NULL rule. */
249
229
  reorderChildAfter(child: Element, sibling: Element | null = null): boolean {
250
230
  return this._applyOrder('reorder-after', child, sibling);
251
231
  }
252
232
 
253
- /** `adw_wrap_box_remove_all` (adw-wrap-box.c:1406-1414). */
233
+ /** `adw_wrap_box_remove_all`. */
254
234
  removeAll(): void {
255
235
  for (const child of this._children) this.removeChild(child);
256
236
  }
@@ -267,8 +247,6 @@ export class AdwWrapBox extends HTMLElement {
267
247
  return true;
268
248
  }
269
249
 
270
- // --- lifecycle ---
271
-
272
250
  connectedCallback() {
273
251
  if (this._initialized) return;
274
252
  this._initialized = true;
@@ -278,10 +256,9 @@ export class AdwWrapBox extends HTMLElement {
278
256
  attributeChangedCallback(name: string, old: string | null, value: string | null) {
279
257
  if (!this._initialized) return;
280
258
  this._sync();
281
- // C compares AFTER normalising and returns early when nothing changed
282
- // (adw-wrap-box.c:592-593), so `child-spacing="-5"` on a box already at 0
283
- // notifies nobody. The same gate applies to every property, not just the
284
- // two spacings this element used to special-case.
259
+ // C compares AFTER normalising and returns early when nothing changed, so
260
+ // `child-spacing="-5"` on a box already at 0 notifies nobody. The gate applies to
261
+ // EVERY property, not just the two spacings.
285
262
  const next = this._normalized(name, value);
286
263
  if (next === this._normalized(name, old)) return;
287
264
  this.dispatchEvent(new CustomEvent(`notify::${name}`, { bubbles: true, detail: { [name]: next } }));
@@ -319,7 +296,6 @@ export class AdwWrapBox extends HTMLElement {
319
296
  const style = this.style;
320
297
  const vertical = this.orientation === 'vertical';
321
298
 
322
- // Main axis is the packing direction; lines wrap along the cross axis.
323
299
  const direction = vertical ? 'column' : 'row';
324
300
  style.flexDirection = this.packDirection === 'end-to-start' ? `${direction}-reverse` : direction;
325
301
  style.flexWrap = this.wrapReverse ? 'wrap-reverse' : 'wrap';
@@ -345,14 +321,13 @@ export class AdwWrapBox extends HTMLElement {
345
321
  const justifyLastLine = this.justifyLastLine;
346
322
  const align = this.align;
347
323
 
348
- // A flex container has ONE `justify-content` and applies it to every
349
- // line, so it carries the COMPLETE-line rule; the final-line rule is
350
- // published beside it for the stylesheet, which reaches that line through
351
- // the only two selectors CSS offers — `:only-child` (a box with one child
352
- // has one line, and it is the last) and the generated `::after` filler.
353
- // The pair is deliberately never collapsed into one attribute: flexbox
354
- // cannot express "every line but the last", and pretending otherwise is
355
- // what made `justify-last-line` a no-op in the first place.
324
+ // A flex container has ONE `justify-content` and applies it to every line, so it
325
+ // carries the COMPLETE-line rule; the final-line rule is published beside it for
326
+ // the stylesheet, which reaches that line through the only two selectors CSS
327
+ // offers — `:only-child` (a box with one child has one line, and it is the last)
328
+ // and the generated `::after` filler. Never collapse the pair into one attribute:
329
+ // flexbox cannot express "every line but the last", and pretending it can is what
330
+ // makes `justify-last-line` a no-op.
356
331
  const line = resolveWrapBoxLine({ justify, justifyLastLine, align, lastLine: false, childrenInLine: 2 });
357
332
  const lastLine = resolveWrapBoxLine({ justify, justifyLastLine, align, lastLine: true, childrenInLine: 2 });
358
333
  this.dataset.lineJustify = line.justify;
@@ -367,12 +342,11 @@ export class AdwWrapBox extends HTMLElement {
367
342
  // flexbox approximates by stretching the lines across the cross axis.
368
343
  style.alignContent = this.lineHomogeneous ? 'stretch' : 'flex-start';
369
344
 
370
- // `wrap-policy` decides whether an overflowing line squeezes its children
371
- // or lets them spill. CSS defaults `flex-shrink` to 1, i.e. to
372
- // ADW_WRAP_MINIMUM — the value libadwaita does NOT default to, so an
373
- // unset policy has been drawing the wrong one on every wrap box. What
374
- // flexbox cannot do either way is pack MORE children onto a line, since
375
- // line breaking runs on hypothetical sizes; see `wrapPolicyFlexShrink`.
345
+ // `wrap-policy` decides whether an overflowing line squeezes its children or lets
346
+ // them spill. CSS defaults `flex-shrink` to 1, i.e. to ADW_WRAP_MINIMUM — which is
347
+ // NOT libadwaita's default, so the policy has to be written out explicitly. What
348
+ // flexbox cannot do either way is pack MORE children onto a line, since line
349
+ // breaking runs on hypothetical sizes; see `wrapPolicyFlexShrink`.
376
350
  style.setProperty('--adw-wrap-box-child-shrink', String(wrapPolicyFlexShrink(this.wrapPolicy)));
377
351
 
378
352
  // DELIBERATE DEVIATION: libadwaita's `natural-line-length` caps the box's
@@ -9,11 +9,8 @@
9
9
  // NAME turned into a CSS mask class, an indicator span, and a `setTimeout`
10
10
  // wrapped in the scheduler seam the core takes for the drag-hover dwell.
11
11
  //
12
- // Shared by all three switcher elements because it was three copies before:
13
- // `<adw-view-switcher>`, `<adw-inline-view-switcher>` and
14
- // `<adw-view-switcher-bar>` each built their own icon span and their own page
15
- // parsing, and the copies had already drifted (a titleless page rendered `''` in
16
- // one and `page-<index>` in another).
12
+ // Shared by `<adw-view-switcher>`, `<adw-inline-view-switcher>` and
13
+ // `<adw-view-switcher-bar>`, so their icon nodes and page parsing cannot drift apart.
17
14
  //
18
15
  // Reference: refs/libadwaita/src/adw-view-switcher.c
19
16
  // Reference: refs/libadwaita/src/adw-indicator-bin.c
@@ -26,11 +23,9 @@ import type { AdwViewSwitcherPageInit, ViewSwitcherScheduler, ViewSwitcherTimerH
26
23
  import { type AdwIcon, createAdwIcon } from './adw-icon.js';
27
24
 
28
25
  /**
29
- * The browser's timer behind the core's scheduler seam.
30
- *
31
- * One shared instance rather than one per element: it holds no state, and the
32
- * seam exists so the CORE stays free of a global timer, not so each element
33
- * invents its own.
26
+ * The browser's timer behind the core's scheduler seam. One shared instance rather than
27
+ * one per element: it holds no state, and the seam exists so the CORE stays free of a
28
+ * global timer, not so each element invents its own.
34
29
  */
35
30
  export const domViewSwitcherScheduler: ViewSwitcherScheduler = {
36
31
  schedule(callback: () => void, ms: number): ViewSwitcherTimerHandle {
@@ -62,19 +57,17 @@ export function applySwitcherIcon(icon: AdwIcon, iconName: string): void {
62
57
  /**
63
58
  * Read a declared page element into the core's page init.
64
59
  *
65
- * `getAttribute` returns `null` for an absent attribute, which is EXACTLY C's
66
- * NULL — so the values are passed through unchanged rather than defaulted to
67
- * `''`. That `?? ''` is what made the button-visibility rule unimplementable in
68
- * the old code: once an absent title has become the empty string, "the page has
69
- * no title" is no longer expressible.
60
+ * `getAttribute` returns `null` for an absent attribute, which is EXACTLY C's NULL, so
61
+ * values are passed through unchanged rather than defaulted to `''`. Do not add a
62
+ * `?? ''`: once an absent title has become the empty string, "the page has no title" is
63
+ * no longer expressible and the button-visibility rule cannot be implemented.
70
64
  */
71
65
  export function readSwitcherPage(element: Element): AdwViewSwitcherPageInit {
72
66
  const badge = Number.parseInt(element.getAttribute('badge-number') ?? '', 10);
73
67
  return {
74
- // A page with no `name` is the page named '' — the same choice
75
- // `<adw-view-stack>` made, and for the same reason: C stores the name
76
- // verbatim (adw-view-stack.c:1170) and an invented `page-<index>` is
77
- // addressable by a name the author never wrote.
68
+ // A page with no `name` is the page named '', as in `<adw-view-stack>`: C stores
69
+ // the name verbatim, and an invented `page-<index>` would be addressable by a
70
+ // name the author never wrote.
78
71
  name: element.getAttribute('name') ?? '',
79
72
  title: element.getAttribute('title'),
80
73
  iconName: element.getAttribute('icon-name'),
@@ -88,8 +81,8 @@ export function readSwitcherPage(element: Element): AdwViewSwitcherPageInit {
88
81
 
89
82
  /**
90
83
  * Paint `AdwIndicatorBin`'s badge/dot onto a span: the badge text when there is
91
- * one, a bare dot when only `needs-attention` is set, nothing otherwise
92
- * (adw-indicator-bin.c:58-68 + the `.needs-attention` style class).
84
+ * one, a bare dot when only `needs-attention` is set, nothing otherwise — C's
85
+ * `AdwIndicatorBin` plus the `.needs-attention` style class.
93
86
  */
94
87
  export function applyIndicator(indicator: HTMLElement, badgeLabel: string, needsAttention: boolean): void {
95
88
  indicator.textContent = badgeLabel;
@@ -100,7 +93,7 @@ export function applyIndicator(indicator: HTMLElement, badgeLabel: string, needs
100
93
  /**
101
94
  * Mirror the derived screen-reader description onto the element, REMOVING the
102
95
  * attribute when there is none — `update_description_cb` resets the accessible
103
- * property rather than setting it to `""` (adw-view-switcher-button.c:104-113).
96
+ * property rather than setting it to `""`.
104
97
  */
105
98
  export function applyDescription(element: HTMLElement, description: string): void {
106
99
  if (description) element.setAttribute('aria-description', description);
@@ -2,12 +2,10 @@
2
2
  // driven by the SAME vectors the NativeScript renderer asserts against
3
3
  // (`@gjsify/adwaita-core/conformance`).
4
4
  //
5
- // Everything these vectors describe used to be MISSING here, not merely
6
- // different: `update_empty`'s truth table, the apply button (declared `true` in
7
- // the shared story metadata all along), the Enter key, max-length, the caps-lock
8
- // warning and its two suppression rules. Nothing failed, because nothing
9
- // compared this renderer to the C. This suite is that comparison — it drives the
10
- // real elements through each scenario and reads back what a user would see.
5
+ // The vectors cover `update_empty`'s truth table, the apply button, the Enter key,
6
+ // max-length, and the caps-lock warning with its two suppression rules — all of which
7
+ // were missing from this renderer, not merely different. Each scenario drives the real
8
+ // elements and reads back what a user would see.
11
9
  import { describe, expect, it } from '@gjsify/unit';
12
10
 
13
11
  import {
@@ -141,9 +139,8 @@ export const AdwEntryRowsTest = async () => {
141
139
  parts.row.maxLength = 0;
142
140
  parts.row.text = '';
143
141
  parts.row.maxLength = maxLength;
144
- // Type it: the `input` event path must clamp exactly like the
145
- // property path, which is why `input.maxLength` (UTF-16 units)
146
- // is deliberately left unset.
142
+ // The `input` event path must clamp exactly like the property path, which
143
+ // is why `input.maxLength` (UTF-16 units) is deliberately left unset.
147
144
  parts.input.value = text;
148
145
  parts.input.dispatchEvent(new Event('input', { bubbles: true }));
149
146
  expect(parts.input.value).toBe(clamped);
@@ -166,8 +163,8 @@ export const AdwEntryRowsTest = async () => {
166
163
  }
167
164
 
168
165
  await it('activates the default BEFORE emitting entry-activated', () => {
169
- // C:253-256 — gtk_widget_activate_default is line 254, the signal is
170
- // line 256. Implicit form submission is the web's default activation.
166
+ // `gtk_widget_activate_default` runs before the `entry-activated` signal;
167
+ // implicit form submission is the web's spelling of that default activation.
171
168
  const host = document.createElement('div');
172
169
  document.body.appendChild(host);
173
170
  host.innerHTML = '<form><adw-entry-row title="T" activates-default></adw-entry-row></form>';
@@ -210,9 +207,8 @@ export const AdwEntryRowsTest = async () => {
210
207
 
211
208
  await describe('adw-entry-row change signals', async () => {
212
209
  await it('emits both GtkEditable spellings on every buffer change', () => {
213
- // libadwaita implements GtkEditable through a delegate (C:87-93,
214
- // :821-834), so an AdwEntryRow emits `changed` AND `notify::text`.
215
- // The two ports had each picked one and drifted apart.
210
+ // libadwaita implements GtkEditable through a DELEGATE, so an AdwEntryRow emits
211
+ // `changed` AND `notify::text` — a port must not pick just one.
216
212
  const parts = mount('adw-entry-row', 'adw-entry-row');
217
213
  const recorded = recordEvents(parts.row, ['changed', 'notify::text', 'notify::text-length']);
218
214
  parts.row.text = 'Ada';
@@ -255,8 +251,8 @@ export const AdwEntryRowsTest = async () => {
255
251
 
256
252
  await describe('adw-entry-row light-DOM children (GtkBuildable)', async () => {
257
253
  await it('keeps author children instead of discarding them', () => {
258
- // `replaceChildren(text, edit)` used to throw every authored child
259
- // away silently. add_suffix (C:885-899) appends into a BOX.
254
+ // `replaceChildren(text, edit)` throws every authored child away silently,
255
+ // where `add_suffix` appends into a BOX.
260
256
  const parts = mount('adw-entry-row', 'adw-entry-row', '<button id="go">Go</button>');
261
257
  const go = parts.row.querySelector('#go') as HTMLElement;
262
258
  expect(go).not.toBe(null);
@@ -281,9 +277,8 @@ export const AdwEntryRowsTest = async () => {
281
277
 
282
278
  await describe('adw-entry-row click-to-focus (pressed_cb)', async () => {
283
279
  await it('focuses the entry when the row body is clicked', () => {
284
- // C:201-227 — the click lands on the row, the header, the editable
285
- // area, the indicator or an affordance box, and grabs focus. The port
286
- // had this inverted: only the pencil focused, the body was inert.
280
+ // The click lands on the row, the header, the editable area, the indicator or
281
+ // an affordance box, and grabs focus — not only on the pencil.
287
282
  const parts = mount('adw-entry-row', 'adw-entry-row');
288
283
  parts.row.click();
289
284
  expect(document.activeElement).toBe(parts.input);
@@ -365,20 +360,19 @@ export const AdwEntryRowsTest = async () => {
365
360
  });
366
361
 
367
362
  await it('keeps the peek toggle when a consumer adds a suffix', () => {
368
- // add_suffix appends into a box (C:885-899) and the toggle is
369
- // installed through it (adw-password-entry-row.c:152), so both stay.
363
+ // `add_suffix` appends into a box and the toggle is installed through it, so
364
+ // both stay.
370
365
  const parts = mount('adw-password-entry-row', 'adw-password-entry-row', '<button id="go">Go</button>');
371
366
  const suffixes = parts.row.querySelector('.adw-password-entry-row-suffixes') as HTMLElement;
372
367
  expect(suffixes.querySelector('.adw-password-entry-row-toggle')).not.toBe(null);
373
368
  expect(suffixes.querySelector('#go')).not.toBe(null);
374
- // C:152 — the row's own toggle is the FIRST suffix.
369
+ // The row's own toggle is the FIRST suffix.
375
370
  expect(suffixes.children[0]?.classList.contains('adw-password-entry-row-toggle')).toBe(true);
376
371
  parts.host.remove();
377
372
  });
378
373
 
379
374
  await it('inherits the entry row rather than copying it', () => {
380
- // The two element files used to be a normalized-diff-identical
381
- // copy-paste; the password row now IS an entry row.
375
+ // The password row IS an entry row, not a copy-paste of one.
382
376
  const parts = mount('adw-password-entry-row', 'adw-password-entry-row');
383
377
  parts.row.showApplyButton = true;
384
378
  parts.input.focus();
package/src/index.ts CHANGED
@@ -8,11 +8,10 @@
8
8
  import '@gjsify/adwaita-fonts'; // Registers @font-face (fontsource pattern)
9
9
  import { ADWAITA_WEB_CSS } from './styles.generated.js';
10
10
 
11
- // Self-apply the compiled stylesheet on import. This makes `import
12
- // '@gjsify/adwaita-web'` enough to style the components under ANY bundler —
11
+ // Self-applied so `import '@gjsify/adwaita-web'` styles the components under ANY bundler:
13
12
  // a separate `import '@gjsify/adwaita-web/style.css'` is a no-op under a gjsify
14
- // `--app browser` build (css-as-string yields a string a side-effect import
15
- // discards), which used to leave consumers unstyled. Idempotent + browser-only.
13
+ // `--app browser` build, where css-as-string yields a string a side-effect import
14
+ // discards and the consumer ends up unstyled. Idempotent + browser-only.
16
15
  if (typeof document !== 'undefined' && !document.getElementById('adwaita-web-style')) {
17
16
  const style = document.createElement('style');
18
17
  style.id = 'adwaita-web-style';
@@ -24,7 +23,17 @@ if (typeof document !== 'undefined' && !document.getElementById('adwaita-web-sty
24
23
  // `AdwBreakpoint`, and the `breakpoint="…"` attribute wiring the split views use.
25
24
  export { addBreakpoints, bindBreakpointSetter } from './breakpoints.js';
26
25
 
27
- // Register custom elements (side-effect imports)
26
+ // The accent palette and its OkLab standalone derivation are
27
+ // `@gjsify/adwaita-core`'s; this is the browser half that puts them in the cascade.
28
+ export {
29
+ ACCENT_BG_PROPERTY,
30
+ ACCENT_PROPERTY,
31
+ applyAdwaitaAccent,
32
+ clearAdwaitaAccent,
33
+ isAdwaitaDark,
34
+ } from './accent.js';
35
+ export type { ApplyAccentOptions } from './accent.js';
36
+
28
37
  export { AdwAvatar } from './elements/adw-avatar.js';
29
38
  export { AdwBanner } from './elements/adw-banner.js';
30
39
  export { AdwCard } from './elements/adw-card.js';
@@ -43,23 +52,23 @@ export { AdwHeaderBar } from './elements/adw-header-bar.js';
43
52
  export { AdwWindowTitle } from './elements/adw-window-title.js';
44
53
  export { AdwButton } from './elements/adw-button.js';
45
54
  export { AdwButtonContent } from './elements/adw-button-content.js';
46
- // The ONE symbolic-icon node and the ONE toggle. Exported before the widgets
47
- // that build them so the barrel's definition order matches theirs; each host
48
- // ALSO imports them directly, which is what actually guarantees the tags are
49
- // defined before a server-rendered host upgrades.
55
+ // The ONE symbolic-icon node and the ONE toggle, exported before the widgets that build
56
+ // them so the barrel's definition order matches theirs. Each host ALSO imports them
57
+ // directly, which is what actually guarantees the tags are defined before a
58
+ // server-rendered host upgrades.
50
59
  export { AdwIcon, createAdwIcon } from './elements/adw-icon.js';
51
60
  export { AdwSwitch } from './elements/adw-switch.js';
52
61
  // The two form-control primitives. ONE module, because upstream merges their
53
62
  // stylesheet (`_checks.scss`; there is no `_radio.scss` in libadwaita) and
54
63
  // everything but the corner radius, the glyph and the group is shared.
55
64
  export { AdwCheckbox, AdwRadio } from './elements/adw-checks.js';
56
- // The determinate progress indicator — <adw-spinner> covers only the
57
- // indeterminate case.
65
+ // The determinate progress indicator — <adw-spinner> covers only the indeterminate case.
58
66
  export { AdwProgressBar } from './elements/adw-progress-bar.js';
59
- // The ONE popover surface. Exported before its three hosts so the barrel's own
60
- // definition order matches theirs; each host ALSO imports it directly, which is
61
- // what actually guarantees `adw-popover` is defined before a server-rendered
62
- // host upgrades and builds one.
67
+ // A keyboard shortcut, drawn as keycaps. The accelerator GRAMMAR lives in
68
+ // @gjsify/adwaita-core; this element only builds the nodes.
69
+ export { AdwShortcutLabel } from './elements/adw-shortcut-label.js';
70
+ // The ONE popover surface, exported before its three hosts for the same reason as
71
+ // <adw-icon> above.
63
72
  export { AdwPopover } from './elements/adw-popover.js';
64
73
  export type { AdwPopoverAlign, AdwPopoverPosition, AdwPopoverRole } from './elements/adw-popover.js';
65
74
  export { AdwSplitButton } from './elements/adw-split-button.js';
@@ -2,14 +2,11 @@
2
2
  // vectors the NativeScript renderer and the headless core assert against
3
3
  // (`@gjsify/adwaita-core/conformance`).
4
4
  //
5
- // The two renderers used to carry independent copies of the whole stack machine,
6
- // and both had drifted away from libadwaita in different directions. Four rows in
7
- // the shared table fail against the pre-lift code here: `pop()` refused a
8
- // `can-pop="false"` page (the C documents the opposite), `replace(['some-tag'])`
9
- // blanked the view when the tag belonged to a dynamically-pushed page,
10
- // `popToTag()` stopped early at a `can-pop="false"` page and emitted N separate
11
- // transitions instead of one, and a duplicate `tag` was accepted silently.
12
- // Nothing failed, because nothing compared them. This suite is that comparison.
5
+ // Four rows of the shared table are the ones independent copies of the stack machine got
6
+ // wrong: a programmatic `pop()` IGNORES `can-pop` (which gates only the back button and
7
+ // the shortcut pops), `replace(['some-tag'])` resolves a tag belonging to a
8
+ // dynamically-pushed page, `popToTag()` pops in ONE atomic splice emitting one
9
+ // transition rather than N, and a duplicate `tag` is refused, not silently accepted.
13
10
  import { describe, expect, it } from '@gjsify/unit';
14
11
 
15
12
  import {
@@ -269,7 +266,7 @@ export const AdwNavigationViewTest = async () => {
269
266
  expect(backButton(view)).toBe(null);
270
267
  view.pushByTag('detail');
271
268
  // The old element hardcoded "Back" here, where the C uses the title of
272
- // the page the button reveals (adw-back-button.c query_tooltip:411-417).
269
+ // the page the button reveals (adw-back-button.c query_tooltip).
273
270
  expect(backButton(view)?.getAttribute('tooltip')).toBe('Home');
274
271
 
275
272
  view.pop();
@@ -406,7 +403,7 @@ export const AdwNavigationViewTest = async () => {
406
403
 
407
404
  await it('stops Alt+Left WITHOUT popping a can-pop="false" page', () => {
408
405
  // GDK_EVENT_STOP so the key never reaches an enclosing navigation view
409
- // (pop_shortcut_cb:1150-1152).
406
+ // (pop_shortcut_cb).
410
407
  const { adapter, view, host, page } = mountView();
411
408
  adapter.add(page('root'));
412
409
  adapter.add(page('detail'));
@@ -7,7 +7,7 @@
7
7
  // the core reasons over.
8
8
  //
9
9
  // Reference: refs/libadwaita/src/adw-preferences-dialog.c
10
- // (adw_preferences_dialog_init's page/group/row model, :617-641)
10
+ // (adw_preferences_dialog_init's page/group/row model)
11
11
  // Reference: refs/libadwaita/src/adw-action-row.c, adw-entry-row.c,
12
12
  // adw-expander-row.c, adw-button-row.c (which rows ARE action rows)
13
13
  // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
@@ -22,16 +22,14 @@ import {
22
22
  } from '@gjsify/adwaita-core';
23
23
 
24
24
  /**
25
- * The rows that derive from `AdwActionRow` and therefore have their SUBTITLE
26
- * searched (`filter_search_results`, adw-preferences-dialog.c:140-146).
25
+ * The rows that derive from `AdwActionRow` and therefore have their SUBTITLE searched
26
+ * (`filter_search_results`).
27
27
  *
28
- * A tag-name set rather than an `instanceof` chain, because the browser
29
- * elements are all direct `HTMLElement` subclasses — the GObject hierarchy is
30
- * not reproduced in the DOM, so the distinction has to be stated. It is not
31
- * "has a subtitle": `adw-entry-row`, `adw-expander-row` and `adw-button-row`
32
- * all derive from `AdwPreferencesRow` in C (adw-entry-row.c:90,
33
- * adw-expander-row.c:72, adw-button-row.c:74), so the text typed into an entry
34
- * row is deliberately NOT searchable.
28
+ * A tag-name set rather than an `instanceof` chain, because the browser elements are all
29
+ * direct `HTMLElement` subclasses: the GObject hierarchy is not reproduced in the DOM, so
30
+ * the distinction has to be stated. The test is NOT "has a subtitle" —
31
+ * `adw-entry-row`, `adw-expander-row` and `adw-button-row` derive from
32
+ * `AdwPreferencesRow` in C, so text typed into an entry row is deliberately not searchable.
35
33
  */
36
34
  const ACTION_ROW_TAGS: ReadonlySet<string> = new Set([
37
35
  'adw-action-row',
@@ -42,11 +40,9 @@ const ACTION_ROW_TAGS: ReadonlySet<string> = new Set([
42
40
 
43
41
  /**
44
42
  * `ADW_IS_PREFERENCES_ROW` for the browser elements — the first clause of
45
- * `row_has_title` (adw-preferences-group.c:158-173).
46
- *
47
- * Spelled out rather than inferred from the `adw-` prefix, because a group's
48
- * boxed list legitimately holds other things: a bare `<adw-button>` dropped in
49
- * as a footer is not a preferences row and must not be searchable.
43
+ * `row_has_title`. Spelled out rather than inferred from the `adw-` prefix, because a
44
+ * group's boxed list legitimately holds other things: a bare `<adw-button>` dropped in as
45
+ * a footer is not a preferences row and must not be searchable.
50
46
  */
51
47
  const PREFERENCES_ROW_TAGS: ReadonlySet<string> = new Set([
52
48
  ...ACTION_ROW_TAGS,
@@ -2,14 +2,12 @@
2
2
  // <adw-preferences-dialog>, driven by the SAME vectors the NativeScript
3
3
  // renderer asserts against (`@gjsify/adwaita-core/conformance`).
4
4
  //
5
- // Two things are being held here. First, the group header: this element derived
6
- // two of the five states by hand and was missing the other three, so an empty
7
- // group kept painting a `.boxed-list` card (its `box-shadow` strokes a
8
- // full-width hairline over nothing) and no header ever carried the `single-line`
9
- // class the stylesheet keys its metrics off. Second, the search: it did not
10
- // exist in this port at all, and the dialog actively prevented it by hoisting
11
- // every page's children out of the page element and discarding the pages —
12
- // leaving nothing to derive `General → Appearance` from.
5
+ // Two things are held here. The GROUP HEADER has five states, all five derived: an empty
6
+ // group must not paint a `.boxed-list` card (its `box-shadow` strokes a full-width
7
+ // hairline over nothing) and a one-line header carries the `single-line` class the
8
+ // stylesheet keys its metrics off. The SEARCH needs the page elements to survive — a
9
+ // dialog that hoists every page's children out and discards the pages leaves nothing to
10
+ // derive `General → Appearance` from.
13
11
  import { describe, expect, it } from '@gjsify/unit';
14
12
 
15
13
  import {
@@ -49,8 +47,8 @@ function mountGroup(options: {
49
47
  }
50
48
 
51
49
  host.appendChild(group);
52
- // Set AFTER connect so the attribute path is exercised, and via
53
- // setAttribute so exact whitespace survives (` ` is a visible title).
50
+ // Set AFTER connect to exercise the attribute path, and via setAttribute so exact
51
+ // whitespace survives (` ` is a visible title).
54
52
  if (options.title !== undefined && options.title !== null) group.setAttribute('title', options.title);
55
53
  if (options.description !== undefined && options.description !== null) {
56
54
  group.setAttribute('description', options.description);
@@ -97,8 +95,8 @@ function mountSearchDialog(pages: readonly PreferencesSearchPage[]): {
97
95
  if (group.visible === false) groupEl.hidden = true;
98
96
 
99
97
  for (const row of group.rows) {
100
- // An action row has a searchable subtitle; an entry row does
101
- // not, and that difference is the point of the vector.
98
+ // An action row has a searchable subtitle and an entry row does not — the
99
+ // point of the vector.
102
100
  const rowEl = document.createElement(row.isActionRow ? 'adw-action-row' : 'adw-entry-row');
103
101
  rowEl.setAttribute('title', row.title);
104
102
  if (row.subtitle) rowEl.setAttribute('subtitle', row.subtitle);
@@ -107,8 +105,8 @@ function mountSearchDialog(pages: readonly PreferencesSearchPage[]): {
107
105
  }
108
106
  pageEl.appendChild(groupEl);
109
107
  }
110
- // Through the public add(), i.e. AFTER connect — the path that used to
111
- // leave a page rendering outside the dialog card.
108
+ // Through the public add(), i.e. AFTER connect: the path that can leave a page
109
+ // rendering outside the dialog card.
112
110
  dialog.add(pageEl);
113
111
  }
114
112
  return { dialog, host };
@@ -170,7 +168,7 @@ export const AdwPreferencesTest = async () => {
170
168
  const { group, host } = mountGroup({ title: 'Account', rowCount: 1 });
171
169
  const foreign = document.createElement('adw-action-row');
172
170
  document.body.appendChild(foreign);
173
- // ADW_CRITICAL_CANNOT_REMOVE_CHILD (adw-preferences-group.c:437).
171
+ // ADW_CRITICAL_CANNOT_REMOVE_CHILD.
174
172
  expect(group.removeRow(foreign)).toBe(false);
175
173
  foreign.remove();
176
174
  host.remove();