@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
@@ -1,13 +1,10 @@
1
1
  // DOM-level conformance tests for <adw-sidebar>, driven by the SAME vectors the
2
2
  // NativeScript renderer asserts against (`@gjsify/adwaita-core/conformance`).
3
3
  //
4
- // The two renderers used to carry independent selection logic, and it had
5
- // drifted apart AND away from libadwaita: `selected = 5` on a 3-item sidebar
6
- // gave "no selection" in GTK, `2` here (it clamped to the last row) and `0` on
7
- // NativeScript (it dropped the write). This element also keyed section headers
8
- // off the DECLARATION index, so an empty leading section drew a stray separator
9
- // and cost the next section its `.first` flush-to-top padding. Nothing failed,
10
- // because nothing compared them. This suite is that comparison.
4
+ // Two rules independent selection logic drifted from: an out-of-range `selected` means
5
+ // NO selection in GTK, not a clamp to the last row and not a dropped write; and section
6
+ // headers key off the RENDERED index, not the declaration index, or an empty leading
7
+ // section draws a stray separator and costs the next section its `.first` padding.
11
8
  import { describe, expect, it } from '@gjsify/unit';
12
9
 
13
10
  import type { AdwSidebarItemSpec, AdwSidebarSectionSpec } from '@gjsify/adwaita-core';
@@ -22,9 +19,8 @@ import {
22
19
  import type { AdwSidebar } from './elements/adw-sidebar.js';
23
20
 
24
21
  /**
25
- * Mount a sidebar built from DECLARED children, the way an author writes it —
26
- * the element consumes them at connect time, so they all have to exist before
27
- * the host is appended.
22
+ * Mount a sidebar from DECLARED children, the way an author writes it: the element
23
+ * consumes them at connect time, so they must all exist before the host is appended.
28
24
  */
29
25
  function mountSidebar(
30
26
  sections: ReadonlyArray<AdwSidebarSectionSpec>,
@@ -144,8 +140,8 @@ export const AdwSidebarTest = async () => {
144
140
  }
145
141
 
146
142
  await it('draws no section container for a section that renders no rows', () => {
147
- // The old element appended a header AND an empty `.adw-sidebar-section`
148
- // per DECLARED section, which in page mode is a visible empty card.
143
+ // A header plus an empty `.adw-sidebar-section` per DECLARED section is a
144
+ // visible empty card in page mode.
149
145
  const { sidebar, host } = mountSidebar([{ title: 'Empty', items: [] }, { items: [{ title: 'A' }] }]);
150
146
  expect(sidebar.querySelectorAll('.adw-sidebar-section')).toHaveLength(1);
151
147
  expect(headersOf(sidebar)).toHaveLength(0);
@@ -303,10 +299,9 @@ export const AdwSidebarTest = async () => {
303
299
  });
304
300
 
305
301
  await it('follows a declared item attribute change without touching the selection', () => {
306
- // <adw-sidebar-item> declared `observedAttributes` with no
307
- // attributeChangedCallback behind it, so every post-construction
308
- // change was silently lost. The declared element is detached once
309
- // consumed, so the link back to its spec cannot be a DOM lookup.
302
+ // `observedAttributes` with no attributeChangedCallback behind it loses every
303
+ // post-construction change. The declared element is detached once consumed, so
304
+ // the link back to its spec cannot be a DOM lookup.
310
305
  const declared = createItem({ title: 'A' });
311
306
  const sectionEl = document.createElement('adw-sidebar-section');
312
307
  sectionEl.append(declared, createItem({ title: 'B' }));
@@ -330,15 +325,13 @@ export const AdwSidebarTest = async () => {
330
325
  });
331
326
  });
332
327
 
333
- // A row's own box, which nothing had ever measured.
328
+ // A row's own BOX, which asking what a row contains never measures.
334
329
  //
335
- // `.adw-sidebar-item` carried `width: 100%` ON TOP of `margin: 0 6px 2px`.
336
- // The list is a column flex container, so a row already stretches to it and
337
- // stretching subtracts margins; `width: 100%` resolves against the
338
- // containing block instead, which does not, so every row came out 12px too
339
- // wide — inset on the left, hanging past the right edge with its rounded
340
- // corners clipped away. Every earlier test asked what a row CONTAINED, so
341
- // all of them passed.
330
+ // `width: 100%` on top of `margin: 0 6px 2px` makes a row 12px too wide: the list is a
331
+ // column flex container, so the row already stretches to it and stretching subtracts
332
+ // margins, while `width: 100%` resolves against the containing block, which does not —
333
+ // leaving the row inset on the left and hanging past the right edge with its rounded
334
+ // corners clipped.
342
335
  await describe('a sidebar row sits inside its list, not over its edge', async () => {
343
336
  await it('leaves the same margin on both sides', async () => {
344
337
  const { sidebar, host } = mountSidebar([{ items: [{ title: 'Inbox' }, { title: 'Starred' }] }], {
@@ -1,20 +1,14 @@
1
- // DOM-level tests for <adw-switch>, plus the REGRESSION PROOF that lifting it
2
- // out of <adw-switch-row> and <adw-expander-row> left both rows working.
1
+ // DOM-level tests for <adw-switch>, plus the proof that both <adw-switch-row> and
2
+ // <adw-expander-row> still work now that the switch is lifted out of them. Two
3
+ // invariants:
3
4
  //
4
- // What the extraction removed: `_expander_row.scss:73-121` was a
5
- // character-for-character copy of `_switch_row.scss:15-63` (same 44×24 box, same
6
- // hidden input, same 20px knob, same `translateX(20px)`, same focus ring),
7
- // because `_switch_row.scss:9` opened `adw-switch-row {` and nested `.adw-switch`
8
- // inside it — so the class did not exist anywhere else and the expander row could
9
- // not reuse it. Two consequences this suite pins down:
10
- //
11
- // - the GEOMETRY now comes from ONE unscoped partial. `measure()` compares the
12
- // computed box of the row's switch against the expander's; before the lift
13
- // they were two independently-maintained blocks that happened to agree, and
14
- // deleting either one silently unstyled its widget.
15
- // - the EVENTS are unchanged. Both rows compose an element that emits its own
16
- // `notify::active`, so each row stops it at the switch and publishes its own
17
- // name — a row listener must still see exactly one event per toggle.
5
+ // - the GEOMETRY comes from ONE unscoped partial, so `measure()` compares the
6
+ // computed box of the row's switch against the expander's. Scoping `.adw-switch`
7
+ // inside `adw-switch-row {` is what forced the expander row to keep a duplicate
8
+ // block, where deleting either copy silently unstyled its widget.
9
+ // - the EVENTS are unchanged: both rows compose an element emitting its own
10
+ // `notify::active`, so each row stops it at the switch and publishes under its own
11
+ // name — a row listener must see exactly one event per toggle.
18
12
  import { describe, expect, it } from '@gjsify/unit';
19
13
 
20
14
  import type { AdwExpanderRow } from './elements/adw-expander-row.js';
@@ -59,7 +53,7 @@ export const AdwSwitchTest = async () => {
59
53
  expect(el.active).toBe(false);
60
54
  expect(input).toBeTruthy();
61
55
  expect(input.checked).toBe(false);
62
- // The focus ring is `input:focus-visible + .adw-switch-slider`, so the
56
+ // The focus ring is `input:focus-visible +.adw-switch-slider`, so the
63
57
  // slider MUST be the input's immediate next sibling.
64
58
  expect(input.nextElementSibling).toBe(slider);
65
59
  expect(el.classList.contains('adw-switch')).toBe(true);
@@ -1,12 +1,9 @@
1
- // DOM-level behaviour tests for <adw-tab-view>. Runs in a real browser via the
2
- // @gjsify/adwaita-web browser test axis (the entry self-applies the compiled
3
- // stylesheet, so computed-style assertions are valid). Asserts page building,
4
- // selected↔click sync, notify/close events, and the expand-tabs / no-close
5
- // attributes — i.e. the DOM/CSS surface.
1
+ // The DOM/CSS surface of <adw-tab-view>, in a real browser via the @gjsify/adwaita-web
2
+ // test axis — the entry self-applies the compiled stylesheet, so computed-style
3
+ // assertions are valid.
6
4
  //
7
- // The MODEL is asserted in `tab-view.spec.ts`, against the shared libadwaita
8
- // conformance vectors. Keep the split: this file is about markup and computed
9
- // style, that one is about behaviour both renderers must agree on.
5
+ // Keep the split: the MODEL is asserted in `tab-view.spec.ts` against the shared
6
+ // libadwaita conformance vectors, this file is markup and computed style only.
10
7
  import { describe, expect, it } from '@gjsify/unit';
11
8
 
12
9
  import type { AdwTabView } from './elements/adw-tab-view.js';
@@ -65,10 +62,10 @@ export const AdwTabViewTest = async () => {
65
62
  expect(closed).toBe(1);
66
63
  // The element used to emit and remove NOTHING, so every close was
67
64
  // permanently denied; the default handler now confirms a non-pinned
68
- // page (close_page_cb, adw-tab-view.c:1990-1991).
65
+ // page (close_page_cb).
69
66
  expect(view.querySelectorAll('.adw-tab').length).toBe(2);
70
67
  // Closing a page BEFORE the selection must not also select the tab,
71
- // and must leave the selected PAGE selected (adw-tab-view.c:1864-1865).
68
+ // and must leave the selected PAGE selected.
72
69
  expect(view.selected).toBe(0);
73
70
  view.parentElement?.remove();
74
71
  });
@@ -1,16 +1,12 @@
1
- // DOM-level behaviour tests for <adw-toast-overlay>. Runs in a real browser via
2
- // the @gjsify/adwaita-web browser test axis.
1
+ // DOM-level behaviour tests for <adw-toast-overlay>, in a real browser via the
2
+ // @gjsify/adwaita-web browser test axis.
3
3
  //
4
- // The regression these guard: the overlay used to append EVERY toast as its own
5
- // strip the moment `addToast()` was called, so three calls stacked three visible
6
- // toasts and each ran its own timer. `Adw.ToastOverlay` shows exactly ONE toast
7
- // at a time and queues the rest FIFO, advancing only when the visible one is
8
- // dismissed (manually or by its timeout) — the policy `AdwToastQueue` in
9
- // `@gjsify/adwaita-core` owns and this element now composes.
4
+ // `Adw.ToastOverlay` shows exactly ONE toast at a time and queues the rest FIFO,
5
+ // advancing only when the visible one is dismissed (manually or by its timeout) — the
6
+ // policy `AdwToastQueue` owns in `@gjsify/adwaita-core` and this element composes.
10
7
  //
11
- // Timing is driven through core's injected `ToastScheduler` seam (a virtual
12
- // clock), so the auto-dismiss lifecycle is asserted deterministically with no
13
- // real timers.
8
+ // Timing runs through core's injected `ToastScheduler` seam (a virtual clock), so the
9
+ // auto-dismiss lifecycle is deterministic with no real timers.
14
10
  import { describe, expect, it } from '@gjsify/unit';
15
11
 
16
12
  import type { AdwToastOverlay, ToastScheduler, ToastTimerHandle } from './elements/adw-toast-overlay.js';
@@ -1,17 +1,13 @@
1
- // adw-view-switcher-bar visibility regression (browser axis). Guards that the
2
- // derived reveal state actually shows/hides the bar. The bar's base scss sets
3
- // `display: block`, an AUTHOR rule that would defeat the UA `[hidden]{display:none}`
4
- // the component relies on (cascade origin beats specificity) — so an unrevealed
5
- // bar rendered visible until scss/_view_switcher_bar.scss re-asserted
6
- // `&[hidden] { display: none }`. This test pins that fix.
1
+ // That the derived reveal state actually shows and hides the bar. The bar's base scss
2
+ // sets `display: block`, an AUTHOR rule that defeats the UA `[hidden]{display:none}` the
3
+ // component relies on (cascade origin beats specificity), so
4
+ // `scss/_view_switcher_bar.scss` has to re-assert `&[hidden] { display: none }`.
7
5
  //
8
- // It now stages the bar against a real two-page <adw-view-stack>, because the
9
- // request alone is no longer enough: `update_bar_revealed` reveals the bar only
10
- // when `reveal` is set AND MORE THAN ONE page is visible
11
- // (refs/libadwaita/src/adw-view-switcher-bar.c:104-126), a rule the port used to
12
- // skip entirely. The one-page case is asserted here too — it is the state the old
13
- // bar rendered as an empty strip. The full derivation is driven from the shared
14
- // conformance vectors in `view-switcher.spec.ts`.
6
+ // Staged against a real two-page <adw-view-stack>, because the request alone is not
7
+ // enough: `update_bar_revealed` (refs/libadwaita/src/adw-view-switcher-bar.c) reveals
8
+ // the bar only when `reveal` is set AND MORE THAN ONE page is visible. The one-page case
9
+ // is asserted here too. The full derivation is driven from the shared conformance
10
+ // vectors in `view-switcher.spec.ts`.
15
11
  import { describe, expect, it } from '@gjsify/unit';
16
12
 
17
13
  import type { AdwViewStack } from './elements/adw-view-stack.js';
@@ -66,7 +62,7 @@ export const AdwViewSwitcherBarTest = async () => {
66
62
  });
67
63
 
68
64
  await it('stays collapsed for a ONE-page stack, however loudly the layout asks', () => {
69
- // `count > 1` (adw-view-switcher-bar.c:125). The old bar showed an
65
+ // `count > 1`. The old bar showed an
70
66
  // empty strip here because it never consulted the page count.
71
67
  const { bar, host } = mountBar(1);
72
68
  bar.reveal = true;
@@ -1,24 +1,17 @@
1
1
  // DOM-level conformance tests for <adw-wrap-box>, driven by the SAME tables the
2
2
  // NativeScript renderer asserts against (`@gjsify/adwaita-core/conformance`).
3
3
  //
4
- // There were no wrap-box specs at all, which is how sixteen divergences from
5
- // `Adw.WrapBox` sat untouched (issue #1048). Three of them were wrong answers to
6
- // the one question that IS portable — where a line's leftover space goes:
4
+ // The portable question is where a line's leftover space goes, and three `Adw.WrapBox`
5
+ // rules answer it: `align` is a MAIN-axis offset of the whole line block, not
6
+ // `align-items` on the cross axis (writing it there pushes children DOWN); `fill` grows
7
+ // the CHILDREN where `spread` grows the GAPS, so they are not both `space-between`; and
8
+ // `justify-last-line` governs the final line, whose C default is NOT justified.
7
9
  //
8
- // - `align` was written to `align-items`, the CROSS axis, where libadwaita
9
- // applies it as a MAIN-axis offset of the whole line block
10
- // (adw-wrap-layout.c:717-725), so `align="1"` pushed children DOWN;
11
- // - `JUSTIFY_MAP` mapped `fill` and `spread` to the same `space-between`,
12
- // where C grows the CHILDREN for one and the GAPS for the other
13
- // (adw-wrap-layout.c:317-341);
14
- // - `justify-last-line` was observed and never read, so the final line was
15
- // always justified — the opposite of the C default (adw-wrap-box.c:379-381).
16
- //
17
- // The element publishes its resolved decision as `data-line-justify` (what
18
- // governs a complete line) and `data-last-line-justify` (what governs the final
19
- // one), which is both what the stylesheet keys off and what these vectors read:
20
- // the rendered `justify-content` / `flex-grow` are asserted alongside, so a
21
- // correct data attribute with a stylesheet that ignores it still fails.
10
+ // The element publishes its resolved decision as `data-line-justify` (complete lines)
11
+ // and `data-last-line-justify` (the final one) — what the stylesheet keys off and what
12
+ // these vectors read. The rendered `justify-content` / `flex-grow` are asserted
13
+ // alongside, so a correct data attribute with a stylesheet that ignores it still
14
+ // fails.
22
15
 
23
16
  import { describe, expect, it } from '@gjsify/unit';
24
17
 
@@ -61,10 +54,9 @@ function label(value: unknown): string {
61
54
  /**
62
55
  * `justify-content` for an un-justified line at `align`.
63
56
  *
64
- * C offsets the line block by `roundf (length_delta * align)`, a continuum;
65
- * flexbox has three positions, so the port takes the nearest one. That
66
- * approximation lives here rather than in the vectors because it is the browser
67
- * renderer's, not libadwaita's.
57
+ * C offsets the line block by `roundf (length_delta * align)`, a continuum, where
58
+ * flexbox has three positions — so the port takes the nearest. The approximation lives
59
+ * here rather than in the vectors because it is the browser renderer's, not libadwaita's.
68
60
  */
69
61
  function expectedJustifyContent(align: number): string {
70
62
  if (align < 0.25) return 'flex-start';
@@ -82,10 +74,9 @@ export const AdwWrapBoxTest = async () => {
82
74
  await it(`${title}: ${vector.rule}`, () => {
83
75
  const attrs: Record<string, string> = { justify: vector.justify, align: String(vector.align) };
84
76
  if (vector.justifyLastLine) attrs['justify-last-line'] = '';
85
- // A box with ONE child has exactly one line, and that line is the
86
- // final one — the only line membership CSS can know without a
87
- // layout pass, and the reason the stylesheet has an `:only-child`
88
- // rule at all.
77
+ // A box with ONE child has exactly one line and it is the final one — the
78
+ // only line membership CSS can know without a layout pass, and why the
79
+ // stylesheet has an `:only-child` rule.
89
80
  const childCount = vector.childrenInLine === 1 ? 1 : 3;
90
81
  const { box, children } = mountWrapBox(attrs, childCount);
91
82
 
@@ -93,9 +84,8 @@ export const AdwWrapBoxTest = async () => {
93
84
  expect(resolved).toBe(vector.layout.justify);
94
85
 
95
86
  if (!vector.lastLine && vector.childrenInLine > 1) {
96
- // `justify-content` is the container knob, and a flex
97
- // container applies it to every line — so it carries the
98
- // COMPLETE-line rule.
87
+ // `justify-content` is the container knob and a flex container applies
88
+ // it to every line, so it carries the COMPLETE-line rule.
99
89
  expect(getComputedStyle(box).justifyContent).toBe(
100
90
  vector.layout.growGaps ? 'space-between' : expectedJustifyContent(vector.layout.align),
101
91
  );
@@ -114,7 +104,7 @@ export const AdwWrapBoxTest = async () => {
114
104
  const style = getComputedStyle(box);
115
105
  expect(style.justifyContent).toBe('flex-end');
116
106
  // The cross axis is libadwaita's `h = line_size` — every child takes
117
- // the full line extent (adw-wrap-layout.c:746-751).
107
+ // the full line extent.
118
108
  expect(style.alignItems).toBe('stretch');
119
109
  unmountAll();
120
110
  });
@@ -134,9 +124,8 @@ export const AdwWrapBoxTest = async () => {
134
124
 
135
125
  await it('a spread box whose last line is not justified gets the filler that packs it', () => {
136
126
  const { box } = mountWrapBox({ justify: 'spread' });
137
- // The only per-line hook flexbox offers: a generated flex item on the
138
- // final line that absorbs its leftover, so the line packs at the start
139
- // the way an un-justified line does.
127
+ // The only per-line hook flexbox offers: a generated flex item on the final
128
+ // line absorbs its leftover, so the line packs at the start.
140
129
  expect(getComputedStyle(box, '::after').flexGrow).toBe('1');
141
130
  unmountAll();
142
131
 
@@ -216,8 +205,8 @@ export const AdwWrapBoxTest = async () => {
216
205
  });
217
206
 
218
207
  for (const { value, unit, dpi, px, rule } of WRAP_BOX_LENGTH_VECTORS) {
219
- // The element resolves at the default dpi; the scaled rows are the
220
- // core suite's, since a browser has no `gtk-xft-dpi` to report.
208
+ // The element resolves at the default dpi; the scaled rows are the core
209
+ // suite's, since a browser has no `gtk-xft-dpi` to report.
221
210
  if (dpi !== 96 || value < 0) continue;
222
211
  await it(`child-spacing ${value}${String(unit)} → ${px}px: ${rule}`, () => {
223
212
  const attrs: Record<string, string> = { 'child-spacing': String(value) };
@@ -314,8 +303,8 @@ export const AdwWrapBoxTest = async () => {
314
303
  return view;
315
304
  };
316
305
  for (const id of children) box.appendChild(make(id));
317
- // The child being placed may be one of the mounted ones (a
318
- // reorder, or the "already parented" refusal) or a fresh one.
306
+ // The child placed may be one of the mounted ones (a reorder, or the
307
+ // "already parented" refusal) or a fresh one.
319
308
  const target = byId.get(child) ?? make(child);
320
309
  // A sibling the box does not have still has to be a real element.
321
310
  const after = sibling === null ? null : (byId.get(sibling) ?? make(sibling));
@@ -2,18 +2,15 @@
2
2
  // the core suite and the NativeScript renderer assert against
3
3
  // (`@gjsify/adwaita-core/conformance`).
4
4
  //
5
- // `Adw.BottomSheet` has four dismissal paths and they are four different gates.
6
- // This element used to route all of them through one `_attemptClose()`, which
7
- // meant: the drag handle closed the sheet (in libadwaita it is `can_target =
8
- // FALSE` and closes nothing), Escape on a closed-but-focused sheet did nothing
9
- // (libadwaita still signals `close-attempt`), and the `sheet.close` action on an
10
- // already-closed sheet did nothing (libadwaita forwards it to the parent).
11
- // Nothing failed, because nothing compared the element to the C source.
5
+ // `Adw.BottomSheet` has four dismissal paths and they are four DIFFERENT gates, so one
6
+ // shared `_attemptClose()` cannot express them: the drag handle is `can_target = FALSE`
7
+ // and closes nothing; Escape on a closed-but-focused sheet still signals
8
+ // `close-attempt`; and the `sheet.close` action on an already-closed sheet forwards to
9
+ // the parent.
12
10
  //
13
- // Two element-local bugs are pinned here too, both from the same root cause —
14
- // the open STATE used to be read off the attribute rather than held:
15
- // `open="false"` rendered OPEN, and re-spelling the attribute fired a
16
- // `notify::open` carrying an unchanged payload.
11
+ // The open STATE must be HELD, not read back off the attribute — reading it made
12
+ // `open="false"` render OPEN and let a re-spelt attribute fire a `notify::open` with an
13
+ // unchanged payload.
17
14
  import { describe, expect, it } from '@gjsify/unit';
18
15
 
19
16
  import type { AdwBottomSheet } from './elements/adw-bottom-sheet.js';
@@ -75,8 +72,8 @@ export const AdwBottomSheetTest = async () => {
75
72
  });
76
73
 
77
74
  expect(returned).toBe(outcome);
78
- // The verdict must be VISIBLE, not just returned: a renderer that
79
- // returns the right word and closes anyway is still wrong.
75
+ // The verdict must be VISIBLE, not just returned: returning the right word
76
+ // and closing anyway is still wrong.
80
77
  expect(sheet.open).toBe(outcome === 'close' ? false : open);
81
78
  expect(attempts).toBe(outcome === 'close-attempt' ? 1 : 0);
82
79
  expect(delegated).toBe(outcome === 'delegate' ? 1 : 0);
@@ -120,9 +117,9 @@ export const AdwBottomSheetTest = async () => {
120
117
 
121
118
  await describe('adw-bottom-sheet open attribute semantics', async () => {
122
119
  await it('open="false" renders CLOSED', () => {
123
- // The getter used to be `hasAttribute('open')` while every other
124
- // boolean used `!== 'false'`, so one element carried two conventions.
125
- // adw-bottom-sheet.c:1670 — `open = !!open` on a strict gboolean.
120
+ // `hasAttribute('open')` for the getter while every other boolean uses
121
+ // `!== 'false'` gives one element two conventions.
122
+ // adw-bottom-sheet.c — `open = !!open` on a strict gboolean.
126
123
  const sheet = document.createElement('adw-bottom-sheet') as AdwBottomSheet;
127
124
  sheet.setAttribute('open', 'false');
128
125
  document.body.appendChild(sheet);
@@ -141,10 +138,9 @@ export const AdwBottomSheetTest = async () => {
141
138
  });
142
139
 
143
140
  await it('re-spelling the attribute fires no spurious notify::open', () => {
144
- // The old callback guarded only `oldValue === newValue` and then
145
- // dispatched unconditionally, so `open=""` → `open="open"` fired a
146
- // second notification carrying an UNCHANGED payload.
147
- // adw-bottom-sheet.c:1672-1682 returns before notifying.
141
+ // Guarding only on `oldValue === newValue` lets `open=""` → `open="open"` fire
142
+ // a second notification carrying an UNCHANGED payload.
143
+ // adw-bottom-sheet.c returns before notifying.
148
144
  const sheet = mountSheet();
149
145
  const payloads: boolean[] = [];
150
146
  sheet.addEventListener('notify::open', (event) => {
@@ -170,9 +166,8 @@ export const AdwBottomSheetTest = async () => {
170
166
 
171
167
  await describe('adw-bottom-sheet affordances', async () => {
172
168
  await it('the drag handle is decorative — not focusable, not a button, closes nothing', () => {
173
- // gtk_widget_set_can_focus/can_target (self->drag_handle, FALSE)
174
- // — adw-bottom-sheet.c:1197-1198. This element used to give it
175
- // role="button", tabindex=0 and a click-to-close handler.
169
+ // `gtk_widget_set_can_focus/can_target (self->drag_handle, FALSE)` — so no
170
+ // role="button", no tabindex=0 and no click-to-close handler.
176
171
  const sheet = mountSheet();
177
172
  sheet.open = true;
178
173
  const handle = dragHandle(sheet);
@@ -207,8 +202,8 @@ export const AdwBottomSheetTest = async () => {
207
202
  });
208
203
 
209
204
  await it('a dimming click on a CLOSED sheet does nothing and signals nothing', () => {
210
- // The scrim is `can_target = open` (adw-bottom-sheet.c:1692), so it
211
- // is not reachable at all — unlike Escape, it does not signal.
205
+ // The scrim is `can_target = open`, so it is not reachable at all — unlike
206
+ // Escape, it does not signal.
212
207
  const sheet = mountSheet();
213
208
  sheet.canClose = false;
214
209
  const { attempts } = recordSignals(sheet, () => {
@@ -228,10 +223,9 @@ export const AdwBottomSheetTest = async () => {
228
223
  });
229
224
 
230
225
  await it('Escape on a CLOSED but focused sheet still raises close-attempt', () => {
231
- // maybe_close_cb's emit is the fallthrough for every case that is not
232
- // (can_close && open) — adw-bottom-sheet.c:393-399. This element
233
- // guarded it away twice (once in the key handler, once in
234
- // `_attemptClose`), so the corner was unreachable.
226
+ // `maybe_close_cb`'s emit is the fallthrough for every case that is NOT
227
+ // (can_close && open); guarding it away in the key handler and in
228
+ // `_attemptClose` makes the corner unreachable.
235
229
  const sheet = mountSheet();
236
230
  const { attempts } = recordSignals(sheet, () => {
237
231
  insideSheet(sheet).dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
@@ -254,9 +248,9 @@ export const AdwBottomSheetTest = async () => {
254
248
  });
255
249
 
256
250
  await it('sheet.close on an already-closed sheet delegates upward', () => {
257
- // sheet_close_cb activates the PARENT's `sheet.close` action instead
258
- // of doing nothing — adw-bottom-sheet.c:382-385. A bubbling event is
259
- // the DOM equivalent, so an enclosing sheet/dialog can act on it.
251
+ // `sheet_close_cb` activates the PARENT's `sheet.close` action rather than
252
+ // doing nothing; a bubbling event is the DOM equivalent, so an enclosing
253
+ // sheet or dialog can act on it.
260
254
  const sheet = mountSheet();
261
255
  const host = sheet.parentElement as HTMLElement;
262
256
  let seenOnAncestor = 0;
@@ -278,10 +272,10 @@ export const AdwBottomSheetTest = async () => {
278
272
  });
279
273
 
280
274
  await it('consumes the slot wrappers instead of leaving them in the content', () => {
281
- // `<child type="sheet">` is GtkBuilder markup — nothing of it survives
282
- // into the widget tree (adw-bottom-sheet.c:1278-1293). The emptied
283
- // <adw-bottom-sheet-sheet> / <adw-bottom-sheet-content> wrappers used
284
- // to fall through to `unslotted` and land in the content layer.
275
+ // `<child type="sheet">` is GtkBuilder markup and nothing of it survives into
276
+ // the widget tree, so the emptied <adw-bottom-sheet-sheet> /
277
+ // <adw-bottom-sheet-content> wrappers must not fall through to `unslotted` and
278
+ // land in the content layer.
285
279
  const sheet = mountSheet();
286
280
  const content = sheet.querySelector('.adw-bottom-sheet-content') as HTMLElement;
287
281
  expect(Array.from(content.children).map((el) => el.tagName.toLowerCase())).toStrictEqual(['button']);
@@ -8,10 +8,9 @@
8
8
  // `ResizeObserver` — which this module supplies under the same name, so the two
9
9
  // ports read alike.
10
10
  //
11
- // Until this existed, `adwaita-web` was the only one of the three renderers with
12
- // no size source at all: `<adw-navigation-split-view collapsed>` was a manual
13
- // attribute the application had to flip itself, so the same markup that adapts
14
- // on GTK and on NativeScript stayed frozen in one layout in the browser.
11
+ // Without a size source `<adw-navigation-split-view collapsed>` is a manual attribute the
12
+ // application has to flip itself, and markup that adapts on GTK and on NativeScript stays
13
+ // frozen in one layout in the browser.
15
14
  //
16
15
  // Reference: refs/libadwaita/src/adw-breakpoint.c
17
16
  // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
@@ -21,19 +20,17 @@ import { AdwBreakpoint } from '@gjsify/adwaita-core';
21
20
  /**
22
21
  * Drive `breakpoints` from `element`'s own box, and return a dispose function.
23
22
  *
24
- * `ResizeObserver` delivers an initial observation on `observe()`, so the
25
- * breakpoints settle on the correct state before first paint — no flash of the
26
- * wrong layout, and no separate seeding pass.
27
- *
28
- * The size read is the BORDER box in CSS pixels, the browser's counterpart to
29
- * the `sp`/DIP units an Adwaita condition is written in.
23
+ * `ResizeObserver` delivers an initial observation on `observe()`, so the breakpoints
24
+ * settle on the correct state before first paint — no flash of the wrong layout, no
25
+ * separate seeding pass. The size read is the BORDER box in CSS pixels, the browser's
26
+ * counterpart to the `sp`/DIP units an Adwaita condition is written in.
30
27
  */
31
28
  export function addBreakpoints(element: Element, breakpoints: readonly AdwBreakpoint[]): () => void {
32
29
  const observer = new ResizeObserver((entries) => {
33
30
  const entry = entries[entries.length - 1];
34
31
  if (!entry) return;
35
- // borderBoxSize is the spec'd path; contentRect is the fallback for the
36
- // shape older engines report.
32
+ // borderBoxSize is the spec'd path; contentRect is the fallback for the shape older
33
+ // engines report.
37
34
  const box = entry.borderBoxSize?.[0];
38
35
  const size = box
39
36
  ? { width: box.inlineSize, height: box.blockSize }
@@ -45,13 +42,13 @@ export function addBreakpoints(element: Element, breakpoints: readonly AdwBreakp
45
42
  }
46
43
 
47
44
  /**
48
- * Wire a `breakpoint="<condition>"` attribute to a boolean setter, the browser
49
- * equivalent of an `Adw.Breakpoint` with one `add_setter()` call.
45
+ * Wire a `breakpoint="<condition>"` attribute to a boolean setter, the browser equivalent
46
+ * of an `Adw.Breakpoint` with one `add_setter()` call. Returns a dispose function; call it
47
+ * before re-binding and on disconnect.
50
48
  *
51
- * Returns a dispose function; call it before re-binding and on disconnect.
52
- * A missing or unparsable condition binds nothing and returns a no-op, leaving
53
- * whatever the attribute already says untouched — an element must not silently
54
- * flip state because its condition had a typo.
49
+ * A missing or unparsable condition binds nothing and returns a no-op, leaving whatever
50
+ * the attribute already says untouched — an element must not silently flip state because
51
+ * its condition had a typo.
55
52
  */
56
53
  export function bindBreakpointSetter(
57
54
  element: Element,