@gjsify/adwaita-web 0.51.1 → 0.53.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 (113) hide show
  1. package/README.md +34 -0
  2. package/dist/adwaita-web.css +259 -31
  3. package/dist/adwaita-web.css.map +1 -1
  4. package/lib/types/accent.d.ts +12 -2
  5. package/lib/types/adw-appearance.spec.d.ts +2 -0
  6. package/lib/types/appearance.d.ts +41 -0
  7. package/lib/types/blueprint-layout.spec.d.ts +1 -0
  8. package/lib/types/blueprint-markup.spec.d.ts +4 -0
  9. package/lib/types/blueprint-tree.spec.d.ts +1 -0
  10. package/lib/types/elements/adw-bottom-sheet.d.ts +31 -13
  11. package/lib/types/elements/adw-carousel.d.ts +5 -0
  12. package/lib/types/elements/adw-header-bar.d.ts +15 -0
  13. package/lib/types/elements/adw-inline-view-switcher.d.ts +1 -0
  14. package/lib/types/elements/adw-navigation-view.d.ts +1 -0
  15. package/lib/types/elements/adw-sidebar.d.ts +10 -0
  16. package/lib/types/elements/adw-tab-view.d.ts +1 -0
  17. package/lib/types/elements/adw-toggle-group.d.ts +11 -0
  18. package/lib/types/elements/gtk-action-bar.d.ts +19 -0
  19. package/lib/types/elements/gtk-box.d.ts +17 -0
  20. package/lib/types/elements/gtk-button.d.ts +2 -0
  21. package/lib/types/elements/gtk-label.d.ts +122 -0
  22. package/lib/types/font-stack.spec.d.ts +2 -0
  23. package/lib/types/gio-menu.spec.d.ts +1 -0
  24. package/lib/types/gtk-box.spec.d.ts +1 -0
  25. package/lib/types/gtk-label.spec.d.ts +1 -0
  26. package/lib/types/gtk-value-doors.spec.d.ts +1 -0
  27. package/lib/types/index.d.ts +6 -1
  28. package/lib/types/namespace/gio.d.ts +1 -0
  29. package/lib/types/namespace/gtk.d.ts +5 -0
  30. package/lib/types/shared-tree-builder.d.ts +64 -0
  31. package/lib/types/source-view/asm6502.d.ts +1 -20
  32. package/lib/types/source-view/index.d.ts +3 -2
  33. package/lib/types/source-view/theme.d.ts +1 -1
  34. package/lib/types/string-list-slot.d.ts +4 -0
  35. package/lib/types/styles.generated.d.ts +1 -1
  36. package/lib/types/tags.spec.d.ts +1 -0
  37. package/lib/types/value-lists.spec.d.ts +1 -0
  38. package/package.json +8 -6
  39. package/scss/_action_bar.scss +51 -0
  40. package/scss/_banner.scss +8 -1
  41. package/scss/_bottom_sheet.scss +57 -0
  42. package/scss/_box.scss +23 -0
  43. package/scss/_button.scss +29 -7
  44. package/scss/_button_content.scss +6 -1
  45. package/scss/_button_row.scss +5 -2
  46. package/scss/_carousel.scss +6 -0
  47. package/scss/_labels.scss +126 -12
  48. package/scss/_navigation_split_view.scss +18 -0
  49. package/scss/_overlay_split_view.scss +5 -0
  50. package/scss/_reset.scss +29 -4
  51. package/scss/_split_button.scss +5 -2
  52. package/scss/_status_page.scss +31 -17
  53. package/scss/_theme.scss +11 -3
  54. package/scss/_toolbar_view.scss +10 -0
  55. package/scss/_variables.scss +23 -3
  56. package/scss/_widget.scss +55 -0
  57. package/scss/_window_title.scss +9 -0
  58. package/scss/adwaita-skin.scss +8 -2
  59. package/src/accent.ts +28 -5
  60. package/src/adw-appearance.spec.ts +178 -0
  61. package/src/adw-banner.spec.ts +16 -0
  62. package/src/adw-button-row.spec.ts +8 -0
  63. package/src/adw-header-bar.spec.ts +17 -0
  64. package/src/appearance.ts +207 -0
  65. package/src/blueprint-layout.spec.ts +223 -0
  66. package/src/blueprint-markup.spec.ts +155 -0
  67. package/src/blueprint-tree.spec.ts +174 -0
  68. package/src/bottom-sheet.spec.ts +166 -1
  69. package/src/chrome.spec.ts +5 -2
  70. package/src/elements/adw-bottom-sheet.ts +166 -59
  71. package/src/elements/adw-carousel.ts +10 -5
  72. package/src/elements/adw-clamp.ts +12 -0
  73. package/src/elements/adw-combo-row.ts +13 -4
  74. package/src/elements/adw-header-bar.ts +46 -3
  75. package/src/elements/adw-inline-view-switcher.ts +11 -0
  76. package/src/elements/adw-navigation-view.ts +10 -0
  77. package/src/elements/adw-sidebar.ts +22 -0
  78. package/src/elements/adw-spin-row.ts +44 -5
  79. package/src/elements/adw-status-page.ts +12 -6
  80. package/src/elements/adw-tab-view.ts +20 -3
  81. package/src/elements/adw-toggle-group.ts +45 -4
  82. package/src/elements/adw-toolbar-view.ts +9 -0
  83. package/src/elements/gtk-action-bar.ts +90 -0
  84. package/src/elements/gtk-box.ts +87 -0
  85. package/src/elements/gtk-button.ts +60 -13
  86. package/src/elements/gtk-drop-down.ts +10 -1
  87. package/src/elements/gtk-label.ts +415 -0
  88. package/src/elements/swipe-drag.ts +1 -1
  89. package/src/font-stack.spec.ts +110 -0
  90. package/src/gio-menu.spec.ts +125 -0
  91. package/src/globals.d.ts +7 -0
  92. package/src/gtk-box.spec.ts +168 -0
  93. package/src/gtk-button.spec.ts +123 -1
  94. package/src/gtk-label.spec.ts +595 -0
  95. package/src/gtk-value-doors.spec.ts +104 -0
  96. package/src/index.ts +32 -1
  97. package/src/keyboard-operable.spec.ts +5 -4
  98. package/src/namespace/gio.ts +19 -0
  99. package/src/namespace/gtk.ts +14 -0
  100. package/src/shared-tree-builder.ts +268 -0
  101. package/src/shared-trees.spec.ts +296 -32
  102. package/src/source-view/adw-source-view.ts +3 -3
  103. package/src/source-view/asm6502.ts +11 -191
  104. package/src/source-view/index.ts +17 -11
  105. package/src/source-view/theme.ts +23 -17
  106. package/src/split-button.spec.ts +7 -0
  107. package/src/string-list-slot.ts +21 -0
  108. package/src/styles.generated.ts +1 -1
  109. package/src/tags.spec.ts +51 -0
  110. package/src/test.browser.mts +25 -0
  111. package/src/value-lists.spec.ts +93 -0
  112. package/lib/types/source-view/hex.d.ts +0 -21
  113. 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
@@ -117,8 +118,21 @@
117
118
  --spacing-l: 18px;
118
119
  --spacing-xl: 24px;
119
120
 
120
- // Typography — GNOME default: Adwaita Sans 11
121
- --font-family: 'Adwaita Sans', 'Cantarell', 'Inter', 'Segoe UI', sans-serif;
121
+ // Typography — GNOME default: Adwaita Sans 11. The tail is a PLATFORM-NATIVE
122
+ // ladder, and it is there because the families above it resolve on a GNOME desktop
123
+ // and NOWHERE ELSE: 'Adwaita Sans' is NAMED, not shipped (`status/stylesheet-font-
124
+ // families.json`), and 'Cantarell' is its predecessor. A macOS host has neither,
125
+ // and the `sans-serif` generic is Helvetica there — #1817, a widget set that looks
126
+ // right on the machine it was authored on and lands on Helvetica in every screenshot
127
+ // a consumer takes. `system-ui` asks the host for its own UI face (San Francisco,
128
+ // Segoe UI); `-apple-system` and 'Segoe UI' are the pre-`system-ui` spellings of
129
+ // those same two answers, kept for the engines that need them.
130
+ //
131
+ // The caption is this block's FIRST SENTENCE on purpose: `generate-adwaita-tokens.mjs`
132
+ // titles a group from the block touching its declaration, cut at the first `. `, so a
133
+ // caption written as a trailing line is unreachable and an uncapped one becomes a
134
+ // section header on the website's theming page.
135
+ --font-family: 'Adwaita Sans', 'Cantarell', 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
122
136
  --font-size-base: 11pt;
123
137
  --font-size-small: 9pt;
124
138
  --font-size-heading: 12pt;
@@ -171,9 +185,15 @@
171
185
  // GNOME's PROPORTIONAL UI font, so a monospace fallback chain that reaches it
172
186
  // second lands on a font whose figures do not line up — the exact property
173
187
  // `.monospace` exists for.
188
+ //
189
+ // 'Consolas' is the same repair as `system-ui` beside it in `--font-family` (#1817):
190
+ // `ui-monospace` is the right way to ask and is not universally supported, and the
191
+ // engines without it fell to the `monospace` generic — Courier New on Windows. The
192
+ // `'SF Mono'` / `'Menlo'` pair answers on macOS and `ui-monospace` on Linux, so this
193
+ // is the Windows half, not a second opinion about the other two.
174
194
  --document-font-family: var(--font-family);
175
195
  --document-font-size: var(--font-size-base);
176
- --monospace-font-family: 'Adwaita Mono', ui-monospace, 'SF Mono', 'Menlo', monospace;
196
+ --monospace-font-family: 'Adwaita Mono', ui-monospace, 'SF Mono', 'Menlo', 'Consolas', monospace;
177
197
  --monospace-font-size: var(--font-size-base);
178
198
  }
179
199
 
@@ -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`
package/src/accent.ts CHANGED
@@ -58,17 +58,40 @@ export function isAdwaitaDark(element: HTMLElement): boolean {
58
58
  return globalThis.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
59
59
  }
60
60
 
61
- /** Set the accent custom properties for `name`. */
62
- export function applyAdwaitaAccent(name: AdwAccentColorName, options: ApplyAccentOptions = {}): void {
63
- const target = options.target ?? document.documentElement;
64
- const dark = options.dark ?? isAdwaitaDark(target);
61
+ /**
62
+ * Elements whose accent the APP chose. The desktop-appearance follower
63
+ * (`appearance.ts`) never writes over one: an explicit choice outranks every
64
+ * source it reads (ADR 0078). A WeakSet, so a removed element is not kept alive.
65
+ */
66
+ const appChosen = new WeakSet<HTMLElement>();
65
67
 
68
+ /** Whether the app has chosen `target`'s accent with {@link applyAdwaitaAccent}. */
69
+ export function hasAppChosenAccent(target: HTMLElement): boolean {
70
+ return appChosen.has(target);
71
+ }
72
+
73
+ /** Write the two properties — the one place they are set, for the app's choice and the follower's alike. */
74
+ export function writeAdwaitaAccent(name: AdwAccentColorName, target: HTMLElement, dark: boolean): void {
66
75
  target.style.setProperty(ACCENT_BG_PROPERTY, adwaitaAccentBgColor(name));
67
76
  target.style.setProperty(ACCENT_PROPERTY, adwaitaAccentColor(name, dark));
68
77
  }
69
78
 
70
- /** Drop the properties, so the stylesheet's own values apply again. */
79
+ /**
80
+ * Set the accent custom properties for `name` — the app's own choice, which
81
+ * from now on wins over the desktop's appearance on this element.
82
+ */
83
+ export function applyAdwaitaAccent(name: AdwAccentColorName, options: ApplyAccentOptions = {}): void {
84
+ const target = options.target ?? document.documentElement;
85
+ appChosen.add(target);
86
+ writeAdwaitaAccent(name, target, options.dark ?? isAdwaitaDark(target));
87
+ }
88
+
89
+ /**
90
+ * Drop the properties, so the stylesheet's own values apply again, and hand
91
+ * the element back to the desktop's appearance.
92
+ */
71
93
  export function clearAdwaitaAccent(target: HTMLElement = document.documentElement): void {
94
+ appChosen.delete(target);
72
95
  target.style.removeProperty(ACCENT_BG_PROPERTY);
73
96
  target.style.removeProperty(ACCENT_PROPERTY);
74
97
  }
@@ -0,0 +1,178 @@
1
+ // Following the desktop's appearance in a real browser (ADR 0078): the handoff
2
+ // tags, live JSON, the CSS system colour, and the precedence between them — all
3
+ // read back through the root's COMPUTED style, since the point is what reaches
4
+ // the cascade.
5
+ //
6
+ // Every test restores the root: the follower acts on `document.documentElement`,
7
+ // which every other suite in this bundle renders under.
8
+
9
+ import { describe, expect, it } from '@gjsify/unit';
10
+ import { ADW_ACCENT_BG_COLORS, renderAppearanceMeta } from '@gjsify/adwaita-core';
11
+ import { APPEARANCE_HANDOFF_VECTORS, NEAREST_ACCENT_VECTORS } from '@gjsify/adwaita-core/conformance';
12
+
13
+ import '@gjsify/adwaita-web';
14
+ import { ACCENT_BG_PROPERTY, applyAdwaitaAccent, clearAdwaitaAccent } from './accent.js';
15
+ import { adwaitaAccentSource, applyDesktopAppearance, applySystemAccent, readSystemAccent } from './appearance.js';
16
+
17
+ const root = () => document.documentElement;
18
+ const rootAccent = () => getComputedStyle(root()).getPropertyValue(ACCENT_BG_PROPERTY).trim();
19
+
20
+ /** MutationObserver callbacks are microtasks; one macrotask later they have run. */
21
+ const settle = () => new Promise<void>((resolve) => setTimeout(resolve, 0));
22
+
23
+ function addMeta(name: string, content: string): HTMLMetaElement {
24
+ const meta = document.createElement('meta');
25
+ meta.name = name;
26
+ meta.content = content;
27
+ document.head.append(meta);
28
+ return meta;
29
+ }
30
+
31
+ async function restore(...metas: HTMLMetaElement[]): Promise<void> {
32
+ for (const meta of metas) meta.remove();
33
+ await settle();
34
+ applySystemAccent(false);
35
+ applyDesktopAppearance({});
36
+ clearAdwaitaAccent(root());
37
+ }
38
+
39
+ export const AdwAppearanceTest = async () => {
40
+ await describe('desktop appearance: the <meta> handoff', async () => {
41
+ await it('applies a handed-over accent without any code in the page', async () => {
42
+ const defaultAccent = rootAccent();
43
+ const meta = addMeta('adw-accent', 'purple');
44
+ await settle();
45
+ expect(rootAccent()).toBe(ADW_ACCENT_BG_COLORS.purple);
46
+ expect(adwaitaAccentSource()).toBe('handoff');
47
+
48
+ // Removing the tag hands the root back to the stylesheet.
49
+ meta.remove();
50
+ await settle();
51
+ expect(rootAccent()).toBe(defaultAccent);
52
+ expect(adwaitaAccentSource()).toBe('default');
53
+ await restore();
54
+ });
55
+
56
+ await it('follows a changed content attribute', async () => {
57
+ const meta = addMeta('adw-accent', 'teal');
58
+ await settle();
59
+ meta.content = 'orange';
60
+ await settle();
61
+ expect(rootAccent()).toBe(ADW_ACCENT_BG_COLORS.orange);
62
+ await restore(meta);
63
+ });
64
+
65
+ await it('hands over the colour scheme as a theme class, and takes it back', async () => {
66
+ const meta = addMeta('adw-color-scheme', 'dark');
67
+ await settle();
68
+ expect(root().classList.contains('theme-dark')).toBe(true);
69
+ meta.content = 'no-preference';
70
+ await settle();
71
+ expect(root().classList.contains('theme-dark')).toBe(false);
72
+ await restore(meta);
73
+ });
74
+
75
+ await it('does not re-read the tags for an unrelated <head> change', async () => {
76
+ // A live JSON handoff must survive a framework injecting a <style>.
77
+ applyDesktopAppearance({ accent: 'green' });
78
+ document.head.append(document.createElement('style'));
79
+ await settle();
80
+ expect(rootAccent()).toBe(ADW_ACCENT_BG_COLORS.green);
81
+ document.head.lastElementChild?.remove();
82
+ await restore();
83
+ });
84
+ });
85
+
86
+ await describe('desktop appearance: APPEARANCE_HANDOFF_VECTORS through a real <head>', async () => {
87
+ for (const vector of APPEARANCE_HANDOFF_VECTORS) {
88
+ await it(`${JSON.stringify(vector.meta)} paints ${vector.paints ?? 'the default'}`, async () => {
89
+ const defaultAccent = rootAccent();
90
+ const holder = document.createElement('template');
91
+ holder.innerHTML = renderAppearanceMeta(vector.appearance);
92
+ const metas = [...holder.content.children] as HTMLMetaElement[];
93
+ document.head.append(...metas);
94
+ await settle();
95
+ expect(rootAccent()).toBe(vector.paints ? ADW_ACCENT_BG_COLORS[vector.paints] : defaultAccent);
96
+ const themeClass = root().classList.contains('theme-dark')
97
+ ? 'theme-dark'
98
+ : root().classList.contains('theme-light')
99
+ ? 'theme-light'
100
+ : null;
101
+ expect(themeClass).toBe(vector.themeClass);
102
+ await restore(...metas);
103
+ });
104
+ }
105
+ });
106
+
107
+ await describe('desktop appearance: a colour-only handoff is snapped (NEAREST_ACCENT_VECTORS)', async () => {
108
+ // A server that sends only `accentRgb` gets libadwaita's snapping in the page, read
109
+ // back from the cascade: the table is held by what the page paints, not by the function.
110
+ for (const vector of NEAREST_ACCENT_VECTORS) {
111
+ await it(`${vector.source}: ${vector.color} paints ${vector.expected}`, async () => {
112
+ applyDesktopAppearance({ accentRgb: vector.color });
113
+ expect(rootAccent()).toBe(ADW_ACCENT_BG_COLORS[vector.expected]);
114
+ await restore();
115
+ });
116
+ }
117
+ });
118
+
119
+ await describe('desktop appearance: live JSON and precedence', async () => {
120
+ await it('applies validated JSON and ignores what is not an accent', async () => {
121
+ applyDesktopAppearance({ accent: 'red', colorScheme: 'light' });
122
+ expect(rootAccent()).toBe(ADW_ACCENT_BG_COLORS.red);
123
+ applyDesktopAppearance({ accent: '"><script>' });
124
+ expect(adwaitaAccentSource()).toBe('default');
125
+ await restore();
126
+ });
127
+
128
+ await it("never overrides the app's own choice", async () => {
129
+ applyAdwaitaAccent('yellow');
130
+ applyDesktopAppearance({ accent: 'pink' });
131
+ const meta = addMeta('adw-accent', 'slate');
132
+ await settle();
133
+ expect(rootAccent()).toBe(ADW_ACCENT_BG_COLORS.yellow);
134
+ expect(adwaitaAccentSource()).toBe('app');
135
+
136
+ // Clearing the app's choice hands the root back to the desktop.
137
+ clearAdwaitaAccent(root());
138
+ applyDesktopAppearance({ accent: 'pink' });
139
+ expect(rootAccent()).toBe(ADW_ACCENT_BG_COLORS.pink);
140
+ await restore(meta);
141
+ });
142
+
143
+ await it('ranks the handoff above the CSS system colour', async () => {
144
+ applySystemAccent();
145
+ applyDesktopAppearance({ accent: 'teal' });
146
+ expect(adwaitaAccentSource()).toBe('handoff');
147
+ expect(rootAccent()).toBe(ADW_ACCENT_BG_COLORS.teal);
148
+ await restore();
149
+ });
150
+ });
151
+
152
+ await describe('desktop appearance: the CSS system colour (#1821)', async () => {
153
+ await it('snaps AccentColor to one of the nine where the engine resolves it, else keeps blue', async () => {
154
+ // Both outcomes are asserted: support for `AccentColor` differs per engine and
155
+ // per OS, and "unsupported" must leave the stylesheet's blue, not a guess.
156
+ const defaultAccent = rootAccent();
157
+ const system = readSystemAccent();
158
+ const applied = applySystemAccent();
159
+ expect(applied).toBe(system?.accent ?? null);
160
+ if (system) {
161
+ expect(adwaitaAccentSource()).toBe('system');
162
+ expect(rootAccent()).toBe(ADW_ACCENT_BG_COLORS[system.accent]);
163
+ } else {
164
+ expect(adwaitaAccentSource()).toBe('default');
165
+ expect(rootAccent()).toBe(defaultAccent);
166
+ }
167
+ await restore();
168
+ expect(rootAccent()).toBe(defaultAccent);
169
+ });
170
+
171
+ await it('the meta keyword "system" asks for the same thing', async () => {
172
+ const meta = addMeta('adw-accent', 'system');
173
+ await settle();
174
+ expect(adwaitaAccentSource()).toBe(readSystemAccent() ? 'system' : 'default');
175
+ await restore(meta);
176
+ });
177
+ });
178
+ };
@@ -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
  };