@gjsify/adwaita-web 0.51.1 → 0.52.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 (103) hide show
  1. package/dist/adwaita-web.css +255 -27
  2. package/dist/adwaita-web.css.map +1 -1
  3. package/lib/types/blueprint-layout.spec.d.ts +1 -0
  4. package/lib/types/blueprint-markup.spec.d.ts +4 -0
  5. package/lib/types/blueprint-tree.spec.d.ts +1 -0
  6. package/lib/types/elements/adw-bottom-sheet.d.ts +31 -13
  7. package/lib/types/elements/adw-carousel.d.ts +5 -0
  8. package/lib/types/elements/adw-header-bar.d.ts +15 -0
  9. package/lib/types/elements/adw-inline-view-switcher.d.ts +1 -0
  10. package/lib/types/elements/adw-navigation-view.d.ts +1 -0
  11. package/lib/types/elements/adw-sidebar.d.ts +10 -0
  12. package/lib/types/elements/adw-tab-view.d.ts +1 -0
  13. package/lib/types/elements/adw-toggle-group.d.ts +11 -0
  14. package/lib/types/elements/gtk-action-bar.d.ts +19 -0
  15. package/lib/types/elements/gtk-box.d.ts +17 -0
  16. package/lib/types/elements/gtk-button.d.ts +2 -0
  17. package/lib/types/elements/gtk-label.d.ts +122 -0
  18. package/lib/types/gio-menu.spec.d.ts +1 -0
  19. package/lib/types/gtk-box.spec.d.ts +1 -0
  20. package/lib/types/gtk-label.spec.d.ts +1 -0
  21. package/lib/types/gtk-value-doors.spec.d.ts +1 -0
  22. package/lib/types/index.d.ts +4 -1
  23. package/lib/types/namespace/gio.d.ts +1 -0
  24. package/lib/types/namespace/gtk.d.ts +5 -0
  25. package/lib/types/shared-tree-builder.d.ts +64 -0
  26. package/lib/types/source-view/asm6502.d.ts +1 -20
  27. package/lib/types/source-view/index.d.ts +3 -2
  28. package/lib/types/source-view/theme.d.ts +1 -1
  29. package/lib/types/string-list-slot.d.ts +4 -0
  30. package/lib/types/styles.generated.d.ts +1 -1
  31. package/lib/types/tags.spec.d.ts +1 -0
  32. package/lib/types/value-lists.spec.d.ts +1 -0
  33. package/package.json +8 -6
  34. package/scss/_action_bar.scss +51 -0
  35. package/scss/_banner.scss +8 -1
  36. package/scss/_bottom_sheet.scss +57 -0
  37. package/scss/_box.scss +23 -0
  38. package/scss/_button.scss +29 -7
  39. package/scss/_button_content.scss +6 -1
  40. package/scss/_button_row.scss +5 -2
  41. package/scss/_carousel.scss +6 -0
  42. package/scss/_labels.scss +126 -12
  43. package/scss/_navigation_split_view.scss +18 -0
  44. package/scss/_overlay_split_view.scss +5 -0
  45. package/scss/_reset.scss +29 -4
  46. package/scss/_split_button.scss +5 -2
  47. package/scss/_status_page.scss +31 -17
  48. package/scss/_theme.scss +11 -3
  49. package/scss/_toolbar_view.scss +10 -0
  50. package/scss/_variables.scss +1 -0
  51. package/scss/_widget.scss +55 -0
  52. package/scss/_window_title.scss +9 -0
  53. package/scss/adwaita-skin.scss +8 -2
  54. package/src/adw-banner.spec.ts +16 -0
  55. package/src/adw-button-row.spec.ts +8 -0
  56. package/src/adw-header-bar.spec.ts +17 -0
  57. package/src/blueprint-layout.spec.ts +223 -0
  58. package/src/blueprint-markup.spec.ts +155 -0
  59. package/src/blueprint-tree.spec.ts +174 -0
  60. package/src/bottom-sheet.spec.ts +166 -1
  61. package/src/chrome.spec.ts +5 -2
  62. package/src/elements/adw-bottom-sheet.ts +166 -59
  63. package/src/elements/adw-carousel.ts +10 -5
  64. package/src/elements/adw-clamp.ts +12 -0
  65. package/src/elements/adw-combo-row.ts +13 -4
  66. package/src/elements/adw-header-bar.ts +46 -3
  67. package/src/elements/adw-inline-view-switcher.ts +11 -0
  68. package/src/elements/adw-navigation-view.ts +10 -0
  69. package/src/elements/adw-sidebar.ts +22 -0
  70. package/src/elements/adw-spin-row.ts +44 -5
  71. package/src/elements/adw-status-page.ts +12 -6
  72. package/src/elements/adw-tab-view.ts +20 -3
  73. package/src/elements/adw-toggle-group.ts +45 -4
  74. package/src/elements/adw-toolbar-view.ts +9 -0
  75. package/src/elements/gtk-action-bar.ts +90 -0
  76. package/src/elements/gtk-box.ts +87 -0
  77. package/src/elements/gtk-button.ts +60 -13
  78. package/src/elements/gtk-drop-down.ts +10 -1
  79. package/src/elements/gtk-label.ts +415 -0
  80. package/src/gio-menu.spec.ts +125 -0
  81. package/src/globals.d.ts +7 -0
  82. package/src/gtk-box.spec.ts +168 -0
  83. package/src/gtk-button.spec.ts +123 -1
  84. package/src/gtk-label.spec.ts +595 -0
  85. package/src/gtk-value-doors.spec.ts +104 -0
  86. package/src/index.ts +14 -1
  87. package/src/keyboard-operable.spec.ts +2 -1
  88. package/src/namespace/gio.ts +19 -0
  89. package/src/namespace/gtk.ts +14 -0
  90. package/src/shared-tree-builder.ts +268 -0
  91. package/src/shared-trees.spec.ts +296 -32
  92. package/src/source-view/adw-source-view.ts +3 -3
  93. package/src/source-view/asm6502.ts +11 -191
  94. package/src/source-view/index.ts +17 -11
  95. package/src/source-view/theme.ts +16 -16
  96. package/src/split-button.spec.ts +7 -0
  97. package/src/string-list-slot.ts +21 -0
  98. package/src/styles.generated.ts +1 -1
  99. package/src/tags.spec.ts +51 -0
  100. package/src/test.browser.mts +18 -0
  101. package/src/value-lists.spec.ts +93 -0
  102. package/lib/types/source-view/hex.d.ts +0 -21
  103. package/src/source-view/hex.ts +0 -37
@@ -4,57 +4,71 @@
4
4
  // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
5
5
  // Modifications: Adapted for @gjsify/adwaita-web Web Components.
6
6
 
7
+ // THE METRICS ARE UPSTREAM'S, measured against a GTK render of the gallery's status page
8
+ // `.blp`. They were the vendored web port's — a 96px icon, an 18pt title, the description
9
+ // capped at 32ch and 12px gaps all round — and beside GTK the page was a different
10
+ // composition: the description wrapped at 290px where GTK sets it on one line, and the child
11
+ // sat 61px higher. `_misc.scss` (statuspage) has a 36px 12px margin, 12px between icon,
12
+ // title and description, 24px more under the icon, `.title-1` for the title and 36px
13
+ // between that column and the child; `adw-status-page.ui` clamps the column at AdwClamp's
14
+ // default 600px.
7
15
  adw-status-page {
8
16
  display: flex;
9
17
  flex-direction: column;
10
18
  align-items: center;
11
19
  justify-content: center;
12
20
  text-align: center;
13
- gap: var(--spacing-m);
14
- padding: var(--spacing-xl);
21
+ gap: 12px;
22
+ padding: 36px 12px;
15
23
  color: var(--window-fg-color);
16
24
 
17
25
  .adw-status-page-icon {
18
- width: 96px;
19
- height: 96px;
26
+ width: 128px;
27
+ height: 128px;
28
+ margin-bottom: 24px;
20
29
  background-color: currentColor;
21
30
  opacity: 0.45;
22
31
  -webkit-mask-size: contain;
23
32
  mask-size: contain;
24
33
  }
25
34
 
35
+ // `.title-1` (`_labels.scss`), restated: each partial compiles on its own, so an
36
+ // `@extend` across them finds no target.
26
37
  .adw-status-page-title {
27
- font-size: 18pt;
28
38
  font-weight: 800;
39
+ font-size: 181%;
40
+ max-width: 600px;
29
41
  }
30
42
 
31
43
  .adw-status-page-description {
32
44
  font-size: var(--font-size-base);
33
45
  opacity: var(--dim-opacity);
34
- max-width: 32ch;
46
+ max-width: 600px;
35
47
  }
36
48
 
49
+ // 36px from the column, of which the flex gap already gives 12.
37
50
  .adw-status-page-child {
38
- margin-top: var(--spacing-s);
51
+ margin-top: 24px;
39
52
  }
40
53
 
41
54
  // `.compact` — the documented smaller status page, for a sidebar or a popover
42
- // (style-classes.md "Compact Status Page"). Upstream drops the outer margins
43
- // 36px → 24px, the icon 128px → 96px and the title from `.title-1` to
44
- // `.title-2` (`_misc.scss:154-172`); this port's resting icon is already 96px
45
- // because it renders at the size the vendored web port used, so `.compact`
46
- // steps down from there in the same proportion.
55
+ // (style-classes.md "Compact Status Page"): margins 24px 12px, the icon 96px with 12px
56
+ // under it, `.title-2`, and 24px to the child (`_misc.scss`, statuspage.compact).
47
57
  &.compact {
48
- gap: var(--spacing-s);
49
- padding: var(--spacing-m);
58
+ padding: 24px 12px;
50
59
 
51
60
  .adw-status-page-icon {
52
- width: 72px;
53
- height: 72px;
61
+ width: 96px;
62
+ height: 96px;
63
+ margin-bottom: 12px;
54
64
  }
55
65
 
56
66
  .adw-status-page-title {
57
- font-size: 14pt;
67
+ font-size: 136%;
68
+ }
69
+
70
+ .adw-status-page-child {
71
+ margin-top: 12px;
58
72
  }
59
73
  }
60
74
  }
package/scss/_theme.scss CHANGED
@@ -45,6 +45,7 @@
45
45
  --entry-border-hover-color: rgba(255, 255, 255, 0.26);
46
46
  --destructive-bg-color: #c01c28;
47
47
  --destructive-fg-color: #ffffff;
48
+ --destructive-color: oklab(from var(--destructive-bg-color) max(l, 0.85) a b);
48
49
  // Semantic states — libadwaita's dark overrides (_colors.scss:83-92): the
49
50
  // backgrounds darken (green_5 / a hand-picked amber / red_4) and the
50
51
  // standalone colour LIGHTENS instead of darkening, so it stays readable on a
@@ -57,9 +58,16 @@
57
58
  --error-color: oklab(from var(--error-bg-color) max(l, 0.85) a b);
58
59
  --switch-off-bg: rgba(255, 255, 255, 0.2);
59
60
  --switch-knob-bg: #deddda;
60
- // The active toggle is a raised near-white chip in both themes, so its text
61
- // must be dark in dark mode (the default token is the light window-fg).
62
- --active-toggle-fg-color: rgba(0, 0, 6, 0.8);
61
+ // The active toggle chip is NOT near-white in both themes — that premise was
62
+ // wrong, and it is what made the selected toggle a solid white pill on a dark
63
+ // window while GTK draws a grey one. libadwaita's own _colors.scss:247-248 says
64
+ // dark takes a TRANSLUCENT white over the dark background, with white text:
65
+ // --active-toggle-bg-color: rgb(255 255 255 / 20%);
66
+ // --active-toggle-fg-color: #ffffff;
67
+ // The foreground needs no entry at all — _toggle_group.scss defaults it to
68
+ // var(--window-fg-color), which is already #ffffff here. Only the background
69
+ // was ever missing; the old dark override forced dark text to compensate for it.
70
+ --active-toggle-bg-color: rgb(255 255 255 / 20%);
63
71
  }
64
72
 
65
73
  // Auto dark theme via prefers-color-scheme — opt out with .theme-light
@@ -60,6 +60,16 @@ adw-toolbar-view {
60
60
  flex: 1 1 auto;
61
61
  }
62
62
 
63
+ // …UNLESS IT ASKED FOR LESS. A content widget with a `valign` other than fill is
64
+ // allocated its natural height and placed by that alignment (`_widget.scss`'s auto
65
+ // margin), which a growing flex item cannot be: the grow claims the free space before
66
+ // an auto margin sees any. Measured on the gallery's navigation view, whose `.blp`
67
+ // centres a pill button in a toolbar view: GTK drew it 44px tall mid-pane, this port
68
+ // stretched it over the whole 293px content rect.
69
+ .adw-toolbar-view-content > :only-child:is([valign='start'], [valign='end'], [valign='center']) {
70
+ flex-grow: 0;
71
+ }
72
+
63
73
  // top-bar-style / bottom-bar-style: an opaque background plus a persistent
64
74
  // shadow, which `.border` swaps for a hairline (adw-toolbar-view.c:1011-1024,
65
75
  // _toolbars.scss `> .top-bar.raised`).
@@ -83,6 +83,7 @@
83
83
  // Destructive action
84
84
  --destructive-bg-color: #e01b24;
85
85
  --destructive-fg-color: #ffffff;
86
+ --destructive-color: oklab(from var(--destructive-bg-color) min(l, 0.5) a b);
86
87
 
87
88
  // Semantic states. `.error` / `.warning` / `.success` (see _labels.scss) do
88
89
  // not just recolour text — they REBIND the three accent tokens to these, so
@@ -0,0 +1,55 @@
1
+ // _widget.scss — the GtkWidget layout properties an element carries as attributes.
2
+ //
3
+ // Every GTK widget inherits `halign`, `valign`, `hexpand` and `vexpand` from GtkWidget, and a
4
+ // projected `.blp` hands them to `buildSharedTree` as attributes (`halign="center"`). Until
5
+ // this partial nothing read them, so the gallery's bottom sheet stretched the button its
6
+ // Blueprint centres. The margins are not here: an attribute cannot carry a length into CSS
7
+ // portably, so `shared-tree-builder.ts` writes them as inline style.
8
+ //
9
+ // ALIGNMENT IS AN AUTO MARGIN, because that is the one CSS placement that means the same on
10
+ // both axes of a flex container and of a grid: GTK allocates a non-FILL child its natural
11
+ // size and places it inside the space its parent gave it, and an auto margin shrinks a flex
12
+ // or grid item to its content and puts the leftover on the named side. It does NOT place a
13
+ // child of a BLOCK container on the vertical axis, where CSS resolves an auto block margin to
14
+ // zero — so a GTK bin that lays its child out by alignment is a grid or a flex box here
15
+ // (`.adw-bottom-sheet-content`, `gtk-box`). And it takes the side it pushes from: a centred
16
+ // child cannot also keep a `margin-start`, where GTK would centre it inside the remaining
17
+ // space.
18
+ //
19
+ // EXPAND IS FLEX-GROW ALONG THE BOX'S OWN AXIS, which is why it lives with the box rules
20
+ // below and not on the attribute alone: `vexpand` in a horizontal box asks for the cross
21
+ // axis, which `align-items: stretch` already fills, and a `flex-grow` there would widen the
22
+ // child instead.
23
+ //
24
+ // Reference: refs/gtk/gtk/gtkwidget.c (halign, valign, hexpand, vexpand)
25
+ // Reference: refs/gtk/gtk/gtkboxlayout.c (spare space shared among expanding children)
26
+ // Copyright (c) The GTK Team. LGPLv2.1+.
27
+
28
+ [halign='start'] {
29
+ margin-inline-end: auto;
30
+ }
31
+
32
+ [halign='end'] {
33
+ margin-inline-start: auto;
34
+ }
35
+
36
+ [halign='center'] {
37
+ margin-inline: auto;
38
+ }
39
+
40
+ [valign='start'] {
41
+ margin-block-end: auto;
42
+ }
43
+
44
+ [valign='end'] {
45
+ margin-block-start: auto;
46
+ }
47
+
48
+ [valign='center'] {
49
+ margin-block: auto;
50
+ }
51
+
52
+ gtk-box:not([orientation='vertical']) > [hexpand],
53
+ gtk-box[orientation='vertical'] > [vexpand] {
54
+ flex-grow: 1;
55
+ }
@@ -11,6 +11,15 @@ adw-window-title {
11
11
  line-height: 1.15;
12
12
  min-width: 0;
13
13
 
14
+ // Both labels carry 12px sides upstream (_header-bar.scss:214-227). Without them the
15
+ // gallery's window title measured 27px narrower than GTK's render of the same `.blp`,
16
+ // and a header bar's title could run up against the buttons beside it.
17
+ .adw-window-title-title,
18
+ .adw-window-title-subtitle {
19
+ padding-left: 12px;
20
+ padding-right: 12px;
21
+ }
22
+
14
23
  .adw-window-title-title {
15
24
  font-size: var(--font-size-base);
16
25
  font-weight: 700;
@@ -19,12 +19,14 @@
19
19
  // specificity.
20
20
  @use 'icon';
21
21
  // libadwaita's typography + semantic-colour UTILITY CLASSES (`.dimmed`,
22
- // `.numeric`, `.monospace`, `.title-1`…). Classes only — there is deliberately
23
- // no <adw-label>, see _labels.scss. Emitted BEFORE every widget so a widget's
22
+ // `.numeric`, `.monospace`, `.title-1`…), and <gtk-label>, the element they are
23
+ // written for — see _labels.scss. Emitted BEFORE every widget so a widget's
24
24
  // own rule wins on ordinary specificity, and because two partials @extend from
25
25
  // it (`_progress_bar.scss`'s text node, `_data_grid.scss`'s cells) exactly as
26
26
  // libadwaita's own `_progress-bar.scss` does.
27
27
  @use 'labels';
28
+ // <gtk-box>. Before every widget, which may place its own children in one.
29
+ @use 'box';
28
30
  // The REST of libadwaita's documented style-class surface — the widget-
29
31
  // independent half: the surfaces (`.background`, `.view`, `.frame`), `.card` and
30
32
  // the two boxed lists, `.toolbar`, `.spacer`, `.osd`, `.linked`, the app-icon
@@ -36,6 +38,7 @@
36
38
  @use 'style_classes';
37
39
  @use 'window';
38
40
  @use 'headerbar';
41
+ @use 'action_bar';
39
42
  @use 'window_title';
40
43
  @use 'button';
41
44
  @use 'button_content';
@@ -95,6 +98,9 @@
95
98
  @use 'alert_dialog';
96
99
  @use 'preferences_dialog';
97
100
  @use 'dialog';
101
+ // Last among the widgets: an authored `halign` / `valign` is placement the author asked
102
+ // for, and an element rule that sets a margin must not win it back on source order.
103
+ @use 'widget';
98
104
 
99
105
  // Ensure the `hidden` attribute always wins: components that toggle visibility
100
106
  // via `element.hidden = true` set the HTML attribute, but a custom `display`
@@ -146,4 +146,20 @@ export const AdwBannerTest = async () => {
146
146
  host.remove();
147
147
  });
148
148
  });
149
+
150
+ // adw-banner.ui: the title is a `.heading` label and the button a plain text button.
151
+ // Beside a GTK render of the gallery's banner `.blp` this banner was 8px shorter,
152
+ // with a regular-weight title.
153
+ await describe('adw-banner geometry (adw-banner.ui)', async () => {
154
+ await it('draws a bold title and a 34px text button, so the banner is 46px tall', () => {
155
+ const { el, host } = mount({ revealed: '', title: 'Updates paused', 'button-label': 'Resume' });
156
+ host.style.width = '460px';
157
+ expect(getComputedStyle(titleEl(el)).fontWeight).toBe('700');
158
+ const button = getComputedStyle(buttonEl(el));
159
+ expect([button.paddingLeft, button.paddingRight]).toStrictEqual(['17px', '17px']);
160
+ expect(Math.round(buttonEl(el).getBoundingClientRect().height)).toBe(34);
161
+ expect(Math.round(el.getBoundingClientRect().height)).toBe(46);
162
+ host.remove();
163
+ });
164
+ });
149
165
  };
@@ -53,6 +53,14 @@ export const AdwButtonRowTest = async () => {
53
53
  // adw-action-rows.spec.ts, which asserts that every spelling of the
54
54
  // attribute leaves the row activatable.
55
55
 
56
+ // `row.button > box { min-height: 40px }` (_lists.scss:458-462): a GTK render of the
57
+ // gallery's button-row `.blp` measures the row at 44, where this one stood at 50.
58
+ await it('stands 44px tall, as a GTK button row does', () => {
59
+ const { row, host } = parseRow('<adw-button-row title="Add account"></adw-button-row>');
60
+ expect(Math.round(row.getBoundingClientRect().height)).toBe(44);
61
+ host.remove();
62
+ });
63
+
56
64
  await it('emits `activated` on click', async () => {
57
65
  const { row, host } = parseRow('<adw-button-row title="Add account"></adw-button-row>');
58
66
  let activated = false;
@@ -212,4 +212,21 @@ export const AdwHeaderBarTest = async () => {
212
212
  unmountAll();
213
213
  });
214
214
  });
215
+
216
+ // `windowtitle .title, .subtitle { padding: 0 12px }` (_header-bar.scss:214-227).
217
+ // Measured beside a GTK render of the gallery's window-title `.blp`: 27px narrower here.
218
+ await describe('adw-window-title label sides', async () => {
219
+ await it('pads its title and subtitle 12px on each side', () => {
220
+ const title = document.createElement('adw-window-title');
221
+ title.setAttribute('title', 'Inbox');
222
+ title.setAttribute('subtitle', '3 unread messages');
223
+ document.body.appendChild(title);
224
+ const sides = ['.adw-window-title-title', '.adw-window-title-subtitle'].map((selector) => {
225
+ const style = getComputedStyle(title.querySelector(selector) as HTMLElement);
226
+ return `${style.paddingLeft} ${style.paddingRight}`;
227
+ });
228
+ title.remove();
229
+ expect(sides).toStrictEqual(['12px 12px', '12px 12px']);
230
+ });
231
+ });
215
232
  };
@@ -0,0 +1,223 @@
1
+ // A REAL `.blp` OF GtkWidget LAYOUT PROPERTIES, MOUNTED AND MEASURED.
2
+ //
3
+ // `bottom-sheet-layout.blp` (`@gjsify/adwaita-core/src/conformance/blueprints/`) is the shape
4
+ // the gallery's `Adw.BottomSheet` block writes: a button placed with `halign`/`valign`, a box
5
+ // inset by four `margin-*` lines, a title at the start edge, and one expanding child. The
6
+ // NativeScript port builds the same file (`adwaita-nativescript`'s `blueprint-trees.spec.ts`).
7
+ //
8
+ // Before `_widget.scss` and the builder's margin table, this renderer wrote every one of these
9
+ // as an attribute nothing read — no refusal, since the web builder refuses nothing it writes as
10
+ // an attribute, and no effect: the centred button stretched across the sheet's content. So the
11
+ // assertions here are about GEOMETRY, read off the laid-out page, never about the attribute
12
+ // being present.
13
+
14
+ import { describe, expect, it } from '@gjsify/unit';
15
+
16
+ import { mountSharedTree } from './shared-tree-builder.js';
17
+
18
+ import sheetTree from '../../adwaita-core/src/conformance/blueprints/bottom-sheet-layout.blp?shared-tree';
19
+ import carouselTree from '../../adwaita-core/src/conformance/blueprints/carousel-indicators.blp?shared-tree';
20
+
21
+ import { GALLERY_BLUEPRINTS } from './blueprint-markup.spec.js';
22
+
23
+ /** Mount the fixture at a size a layout can be measured in; the caller unmounts. */
24
+ function mountSheet() {
25
+ const mounted = mountSharedTree(sheetTree);
26
+ mounted.root.style.width = '400px';
27
+ mounted.root.style.height = '340px';
28
+ const byId = (id: string): HTMLElement => {
29
+ const el = mounted.root.querySelector<HTMLElement>(`#${id}`);
30
+ if (el === null) throw new Error(`the mounted tree has no element with the id '${id}'`);
31
+ return el;
32
+ };
33
+ return { ...mounted, byId };
34
+ }
35
+
36
+ export const AdwBlueprintLayoutTest = async () => {
37
+ await describe('adwaita-web: bottom-sheet-layout.blp, laid out', async () => {
38
+ await it('halign/valign center place the button in the middle of the content at its own size', async () => {
39
+ const { root, unmount, byId } = mountSheet();
40
+ try {
41
+ const content = root.querySelector('.adw-bottom-sheet-content') as HTMLElement;
42
+ const area = content.getBoundingClientRect();
43
+ const button = byId('toggle').getBoundingClientRect();
44
+
45
+ expect(button.width < area.width).toBe(true);
46
+ expect(button.height < area.height).toBe(true);
47
+ expect(Math.abs(button.left - area.left - (area.right - button.right)) <= 1).toBe(true);
48
+ expect(Math.abs(button.top - area.top - (area.bottom - button.bottom)) <= 1).toBe(true);
49
+ } finally {
50
+ unmount();
51
+ }
52
+ });
53
+
54
+ await it('the four margins inset the box, start and end on the logical edges', async () => {
55
+ const { unmount, byId } = mountSheet();
56
+ try {
57
+ const style = getComputedStyle(byId('box'));
58
+
59
+ expect(style.marginLeft).toBe('18px');
60
+ expect(style.marginRight).toBe('12px');
61
+ expect(style.marginTop).toBe('18px');
62
+ expect(style.marginBottom).toBe('24px');
63
+ } finally {
64
+ unmount();
65
+ }
66
+ });
67
+
68
+ await it('halign start keeps the title at the start edge of the box at its own width', async () => {
69
+ const { unmount, byId } = mountSheet();
70
+ try {
71
+ const box = byId('box').getBoundingClientRect();
72
+ const title = byId('title').getBoundingClientRect();
73
+
74
+ expect(Math.abs(title.left - box.left) <= 1).toBe(true);
75
+ expect(title.width < box.width).toBe(true);
76
+ } finally {
77
+ unmount();
78
+ }
79
+ });
80
+
81
+ await it('expand grows along the box axis only', async () => {
82
+ const { unmount, byId } = mountSheet();
83
+ try {
84
+ // The box is vertical: `vexpand` is its own axis and takes the spare height,
85
+ // `hexpand` is the cross axis, which `align-items: stretch` already fills.
86
+ expect(getComputedStyle(byId('body')).flexGrow).toBe('1');
87
+ expect(getComputedStyle(byId('caption')).flexGrow).toBe('0');
88
+ } finally {
89
+ unmount();
90
+ }
91
+ });
92
+
93
+ await it('the open modal sheet shows its dimming, and the non-modal one would not', async () => {
94
+ const { root, unmount } = mountSheet();
95
+ try {
96
+ const sheet = root as HTMLElement & { modal: boolean };
97
+ const dimming = root.querySelector('.adw-bottom-sheet-dimming') as HTMLElement;
98
+
99
+ expect(dimming.classList.contains('visible')).toBe(true);
100
+ sheet.modal = false;
101
+ expect(dimming.classList.contains('visible')).toBe(false);
102
+ } finally {
103
+ unmount();
104
+ }
105
+ });
106
+ });
107
+
108
+ // `carousel: carousel` projects as the attribute `carousel="carousel"`: GTK's property
109
+ // name, which the indicators now read beside the `for` they had first.
110
+ await describe('adwaita-web: carousel-indicators.blp, mounted', async () => {
111
+ await it('both indicators bind to the carousel by id and mark its first page', async () => {
112
+ const { root, unmount } = mountSharedTree(carouselTree);
113
+ try {
114
+ const dots = [...root.querySelectorAll('#dots .adw-carousel-dot')];
115
+ const lines = [...root.querySelectorAll('#lines .adw-carousel-line')];
116
+
117
+ expect(dots.length).toBe(3);
118
+ expect(lines.length).toBe(3);
119
+ expect(dots.map((dot) => dot.classList.contains('active'))).toStrictEqual([true, false, false]);
120
+ expect(lines[0]?.classList.contains('active')).toBe(true);
121
+ } finally {
122
+ unmount();
123
+ }
124
+ });
125
+ });
126
+
127
+ // The gallery's third batch of `.blp` files, mounted at the size of the gallery's stage
128
+ // and held to a GTK render of the same file (GJS, `Gtk.Builder`, an `Adw.ApplicationWindow`
129
+ // of that size, libadwaita 1.9). The GTK numbers are written beside each assertion; the
130
+ // tolerance is the 1-2px a header bar and text metrics differ by.
131
+ await describe('adwaita-web: the gallery Blueprints, laid out beside GTK', async () => {
132
+ /** The tree mounted in a grid of `width` x `height`, as the gallery stage holds it. */
133
+ const mountAt = (file: string, width: number, height: number) => {
134
+ const tree = GALLERY_BLUEPRINTS[file];
135
+ if (tree === undefined) throw new Error(`no gallery Blueprint ${file}`);
136
+ const mounted = mountSharedTree(tree);
137
+ const host = mounted.root.parentElement as HTMLElement;
138
+ host.style.cssText = `display: grid; width: ${width}px; height: ${height}px;`;
139
+ const box = (el: Element) => {
140
+ const r = el.getBoundingClientRect();
141
+ const h = host.getBoundingClientRect();
142
+ return { x: r.left - h.left, y: r.top - h.top, width: r.width, height: r.height };
143
+ };
144
+ const byId = (id: string): HTMLElement => {
145
+ const el = host.querySelector<HTMLElement>(`#${id}`);
146
+ if (el === null) throw new Error(`${file} mounted no element with the id '${id}'`);
147
+ return el;
148
+ };
149
+ return { ...mounted, host, box, byId };
150
+ };
151
+ const near = (actual: number, gtk: number, tolerance = 2) => Math.abs(actual - gtk) <= tolerance;
152
+
153
+ await it('a split view page fills its pane, and its header bar shows the page title', async () => {
154
+ const { unmount, box, byId, host } = mountAt('adwaita/navigation-split-view.blp', 652, 340);
155
+ try {
156
+ // GTK: sidebar 0,46 180x294 — the list fills the pane under the header bar.
157
+ expect(near(box(byId('sidebar')).height, 294)).toBe(true);
158
+ const titles = [...host.querySelectorAll('adw-header-bar')].map(
159
+ (bar) => bar.querySelector('adw-window-title')?.getAttribute('title') ?? null,
160
+ );
161
+ expect(titles).toStrictEqual(['Mailboxes', 'All Mail']);
162
+ } finally {
163
+ unmount();
164
+ }
165
+ });
166
+
167
+ await it('an overlay split view sidebar fills its pane', async () => {
168
+ const { unmount, box, byId } = mountAt('adwaita/overlay-split-view.blp', 652, 340);
169
+ try {
170
+ // GTK: sidebar 0,46 180x294.
171
+ expect(near(box(byId('sidebar')).height, 294)).toBe(true);
172
+ } finally {
173
+ unmount();
174
+ }
175
+ });
176
+
177
+ await it('a centred toolbar-view content widget keeps its own height', async () => {
178
+ const { unmount, box, byId } = mountAt('adwaita/navigation-view.blp', 652, 340);
179
+ try {
180
+ // GTK: open_button 245,171 161x44.
181
+ const button = box(byId('open_button'));
182
+ expect(near(button.height, 44)).toBe(true);
183
+ expect(near(button.y, 171)).toBe(true);
184
+ } finally {
185
+ unmount();
186
+ }
187
+ });
188
+
189
+ await it("a status page has upstream's metrics: a 128px icon, the child where GTK puts it", async () => {
190
+ const { unmount, box, byId, host } = mountAt('adwaita/status-page.blp', 652, 400);
191
+ try {
192
+ const icon = host.querySelector('.adw-status-page-icon') as HTMLElement;
193
+ expect(box(icon).width).toBe(128);
194
+ expect(box(icon).height).toBe(128);
195
+ // GTK: new_button 239,311 174x44.
196
+ expect(near(box(byId('new_button')).y, 311, 3)).toBe(true);
197
+ } finally {
198
+ unmount();
199
+ }
200
+ });
201
+
202
+ await it('a carousel does not widen the box it fills to the sum of its pages', async () => {
203
+ const { unmount, box, root } = mountAt('adwaita/carousel.blp', 652, 260);
204
+ try {
205
+ // GTK: the box is 652 wide, the window's width.
206
+ expect(box(root).width).toBe(652);
207
+ } finally {
208
+ unmount();
209
+ }
210
+ });
211
+
212
+ await it('tooltip-text becomes the icon button tooltip and accessible name', async () => {
213
+ const { unmount, byId } = mountAt('adwaita/toolbar-view.blp', 652, 300);
214
+ try {
215
+ const inner = byId('add_button').querySelector('button') as HTMLButtonElement;
216
+ expect(inner.title).toBe('Add');
217
+ expect(inner.getAttribute('aria-label')).toBe('Add');
218
+ } finally {
219
+ unmount();
220
+ }
221
+ });
222
+ });
223
+ };