@gjsify/adwaita-web 0.40.0 → 0.42.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.
- package/README.md +98 -3
- package/dist/adwaita-web.css +142 -63
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/adw-fonts.spec.d.ts +2 -0
- package/lib/types/breakpoints.d.ts +10 -1
- package/lib/types/connect-lifecycle.spec.d.ts +1 -0
- package/lib/types/elements/adw-about-dialog.d.ts +1 -0
- package/lib/types/elements/adw-action-row.d.ts +2 -0
- package/lib/types/elements/adw-alert-dialog.d.ts +14 -2
- package/lib/types/elements/adw-carousel.d.ts +48 -0
- package/lib/types/elements/adw-dialog.d.ts +2 -7
- package/lib/types/elements/adw-icon.d.ts +17 -1
- package/lib/types/elements/adw-navigation-split-view.d.ts +7 -0
- package/lib/types/elements/adw-overlay-split-view.d.ts +6 -0
- package/lib/types/elements/adw-popover.d.ts +1 -0
- package/lib/types/elements/adw-preferences-dialog.d.ts +1 -0
- package/lib/types/elements/adw-switch.d.ts +10 -0
- package/lib/types/elements/adw-tab-view.d.ts +23 -0
- package/lib/types/elements/adw-toggle-group.d.ts +2 -0
- package/lib/types/elements/adw-toolbar-view.d.ts +8 -0
- package/lib/types/elements/modal-surface.d.ts +59 -0
- package/lib/types/elements/roving-focus.d.ts +26 -0
- package/lib/types/elements/row-activation.d.ts +26 -0
- package/lib/types/empty-sections.d.ts +15 -0
- package/lib/types/empty-sections.spec.d.ts +1 -0
- package/lib/types/fonts.d.ts +1 -0
- package/lib/types/icon-registry.d.ts +35 -0
- package/lib/types/icon-registry.spec.d.ts +18 -0
- package/lib/types/index.d.ts +3 -1
- package/lib/types/keyboard-operable.spec.d.ts +1 -0
- package/lib/types/scroll-shading.d.ts +56 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/package.json +10 -6
- package/scss/_banner.scss +1 -1
- package/scss/_carousel.scss +15 -1
- package/scss/_checks.scss +8 -8
- package/scss/_combo_row.scss +1 -1
- package/scss/_drop_down.scss +1 -1
- package/scss/_expander_row.scss +1 -1
- package/scss/_headerbar.scss +35 -6
- package/scss/_icon.scss +47 -0
- package/scss/_icons.generated.scss +36 -6
- package/scss/_password_entry_row.scss +1 -1
- package/scss/_preferences_dialog.scss +3 -2
- package/scss/_progress_bar.scss +1 -1
- package/scss/_row.scss +1 -1
- package/scss/_scrolling.scss +107 -0
- package/scss/_status_page.scss +1 -1
- package/scss/_style_classes.scss +21 -8
- package/scss/_tab_view.scss +1 -1
- package/scss/_theme.scss +1 -0
- package/scss/_toast.scss +6 -24
- package/scss/_toolbar_view.scss +52 -22
- package/scss/_variables.scss +30 -3
- package/scss/_view_switcher.scss +80 -20
- package/scss/adwaita-skin.scss +1 -0
- package/src/adw-action-rows.spec.ts +21 -9
- package/src/adw-button-content.spec.ts +11 -6
- package/src/adw-carousel.spec.ts +141 -1
- package/src/adw-fonts.spec.ts +188 -0
- package/src/adw-header-bar.spec.ts +92 -0
- package/src/adw-icon.spec.ts +17 -8
- package/src/adw-row-state.spec.ts +11 -1
- package/src/adw-tab-view.spec.ts +151 -0
- package/src/bottom-sheet.spec.ts +87 -0
- package/src/breakpoints.ts +18 -4
- package/src/chrome.spec.ts +233 -0
- package/src/connect-lifecycle.spec.ts +351 -0
- package/src/elements/adw-about-dialog.ts +24 -18
- package/src/elements/adw-action-row.ts +14 -2
- package/src/elements/adw-alert-dialog.ts +39 -20
- package/src/elements/adw-button-row.ts +3 -0
- package/src/elements/adw-carousel.ts +121 -4
- package/src/elements/adw-clamp.ts +3 -2
- package/src/elements/adw-data-grid.ts +5 -2
- package/src/elements/adw-dialog.ts +16 -63
- package/src/elements/adw-expander-row.ts +18 -4
- package/src/elements/adw-header-bar.ts +2 -0
- package/src/elements/adw-icon.ts +32 -6
- package/src/elements/adw-inline-view-switcher.ts +12 -0
- package/src/elements/adw-menu-button.ts +17 -10
- package/src/elements/adw-navigation-split-view.ts +30 -7
- package/src/elements/adw-overlay-split-view.ts +15 -3
- package/src/elements/adw-popover.ts +11 -0
- package/src/elements/adw-preferences-dialog.ts +18 -16
- package/src/elements/adw-preferences-group.ts +7 -0
- package/src/elements/adw-sidebar.ts +15 -0
- package/src/elements/adw-status-page.ts +12 -4
- package/src/elements/adw-switch-row.ts +7 -0
- package/src/elements/adw-switch.ts +19 -1
- package/src/elements/adw-tab-view.ts +70 -7
- package/src/elements/adw-toast-overlay.ts +8 -1
- package/src/elements/adw-toggle-group.ts +94 -2
- package/src/elements/adw-toolbar-view.ts +28 -2
- package/src/elements/adw-view-switcher-bar.ts +18 -0
- package/src/elements/adw-view-switcher.ts +14 -0
- package/src/elements/modal-surface.ts +180 -0
- package/src/elements/roving-focus.ts +154 -0
- package/src/elements/row-activation.ts +92 -0
- package/src/empty-sections.spec.ts +81 -0
- package/src/empty-sections.ts +54 -0
- package/src/fonts.ts +25 -0
- package/src/icon-registry.spec.ts +240 -0
- package/src/icon-registry.ts +157 -0
- package/src/index.ts +30 -1
- package/src/keyboard-operable.spec.ts +755 -0
- package/src/scroll-shading.ts +255 -0
- package/src/source-view/theme.ts +15 -3
- package/src/split-button.spec.ts +6 -2
- package/src/style-classes.spec.ts +6 -0
- package/src/styles.generated.ts +1 -1
- package/src/test.browser.mts +14 -0
- package/src/view-switcher.spec.ts +149 -0
package/scss/_tab_view.scss
CHANGED
|
@@ -183,7 +183,7 @@ adw-tab-view {
|
|
|
183
183
|
background-color: var(--accent-bg-color);
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
// --- Close affordance (CSS-drawn "×"
|
|
186
|
+
// --- Close affordance (CSS-drawn "×"; `window-close` ships, see adw-tab-view.ts) ---
|
|
187
187
|
// Presence is decided in JS by `tabCloseVisible` — `(hovering && fullyVisible)
|
|
188
188
|
// || selected || dragging`, and never on a pinned tab (adw-tab.c:124, :645-650)
|
|
189
189
|
// — which is why the `[hidden]` rule below is load-bearing rather than cosmetic.
|
package/scss/_theme.scss
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
--headerbar-fg-color: #ffffff;
|
|
18
18
|
--headerbar-shade-color: rgba(0, 0, 6, 0.36);
|
|
19
19
|
--separator-color: rgba(255, 255, 255, 0.08);
|
|
20
|
+
--shade-color: rgba(0, 0, 6, 0.25);
|
|
20
21
|
--sidebar-bg-color: #2e2e32;
|
|
21
22
|
--sidebar-fg-color: #ffffff;
|
|
22
23
|
--sidebar-backdrop-color: #28282c;
|
package/scss/_toast.scss
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// up on show / down on dismiss (mirroring Adw.Toast).
|
|
6
6
|
// Reference: refs/adwaita-web/adwaita-web/scss/_toast.scss
|
|
7
7
|
// Reference: refs/adwaita-web/adwaita-web/scss/_toast_overlay.scss
|
|
8
|
-
// Reference: refs/libadwaita/src/stylesheet/widgets/
|
|
8
|
+
// Reference: refs/libadwaita/src/stylesheet/widgets/_misc.scss (Toasts)
|
|
9
9
|
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
10
10
|
// Copyright (c) 2025 csm (adwaita-web). MIT License.
|
|
11
11
|
// Modifications: Adapted for @gjsify/adwaita-web Web Components.
|
|
@@ -120,12 +120,14 @@ adw-toast-overlay {
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
// Close affordance — a
|
|
124
|
-
//
|
|
123
|
+
// Close affordance — `window-close-symbolic` in a 24px circle, as upstream builds it
|
|
124
|
+
// (adw-toast-widget.ui:64). The element supplies the <adw-icon>; this only places it.
|
|
125
125
|
.adw-toast-close-button {
|
|
126
126
|
box-sizing: border-box;
|
|
127
127
|
flex-shrink: 0;
|
|
128
|
-
|
|
128
|
+
display: flex;
|
|
129
|
+
align-items: center;
|
|
130
|
+
justify-content: center;
|
|
129
131
|
width: 24px;
|
|
130
132
|
height: 24px;
|
|
131
133
|
margin: 0;
|
|
@@ -149,25 +151,5 @@ adw-toast-overlay {
|
|
|
149
151
|
outline: 2px solid var(--accent-color);
|
|
150
152
|
outline-offset: 1px;
|
|
151
153
|
}
|
|
152
|
-
|
|
153
|
-
&::before,
|
|
154
|
-
&::after {
|
|
155
|
-
content: '';
|
|
156
|
-
position: absolute;
|
|
157
|
-
top: 50%;
|
|
158
|
-
left: 50%;
|
|
159
|
-
width: 11px;
|
|
160
|
-
height: 1.5px;
|
|
161
|
-
background-color: currentColor;
|
|
162
|
-
border-radius: 1px;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
&::before {
|
|
166
|
-
transform: translate(-50%, -50%) rotate(45deg);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
&::after {
|
|
170
|
-
transform: translate(-50%, -50%) rotate(-45deg);
|
|
171
|
-
}
|
|
172
154
|
}
|
|
173
155
|
}
|
package/scss/_toolbar_view.scss
CHANGED
|
@@ -31,6 +31,35 @@ adw-toolbar-view {
|
|
|
31
31
|
background-color: var(--window-bg-color);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// THE CONTENT WIDGET FILLS THE CONTENT RECT. `adw_toolbar_view_size_allocate`
|
|
35
|
+
// (adw-toolbar-view.c:390-397) hands `self->content` the whole rect between the
|
|
36
|
+
// bars, and a GTK widget that wants less of it says so through its own
|
|
37
|
+
// alignment. This port collects the content into a flex COLUMN, where an item
|
|
38
|
+
// declaring no `flex-grow` is content-sized instead, so the child stopped
|
|
39
|
+
// wherever its content did and the pane below it stayed empty. Measured on the
|
|
40
|
+
// docs Navigation Split View preview: a 293px sidebar pane holding a 176px
|
|
41
|
+
// `adw-sidebar`, i.e. 117px of window-coloured dead strip under the last row,
|
|
42
|
+
// and beside it a status page top-aligned in a pane it was supposed to centre in.
|
|
43
|
+
// The web storybook had been papering over it in its own sheet
|
|
44
|
+
// (`.sb-sidebar-scroll { flex: 1 1 auto }`), deleted now that the widget carries
|
|
45
|
+
// it: a per-consumer copy is what a missing widget rule looks like from the
|
|
46
|
+
// outside, and it keeps the widget green while the next consumer hits the same
|
|
47
|
+
// wall.
|
|
48
|
+
//
|
|
49
|
+
// `:only-child` because Adw.ToolbarView HAS one content widget. Several stacked
|
|
50
|
+
// children are this port's own extension, and GTK defines no distribution for
|
|
51
|
+
// them, so inventing one here would be a claim rather than a copy.
|
|
52
|
+
//
|
|
53
|
+
// NO `min-height: 0` alongside it, deliberately: the automatic minimum size is
|
|
54
|
+
// already the right answer per child. A scroller (`adw-sidebar`, any
|
|
55
|
+
// `overflow: auto` box) gets an automatic minimum of 0 and shrinks to scroll
|
|
56
|
+
// itself, as its GtkScrolledWindow would; a status page keeps its content-based
|
|
57
|
+
// minimum, so a pane too short to hold it scrolls THIS box instead of crushing a
|
|
58
|
+
// centred column into overflow its reader cannot scroll back up to.
|
|
59
|
+
.adw-toolbar-view-content > :only-child {
|
|
60
|
+
flex: 1 1 auto;
|
|
61
|
+
}
|
|
62
|
+
|
|
34
63
|
// top-bar-style / bottom-bar-style: an opaque background plus a persistent
|
|
35
64
|
// shadow, which `.border` swaps for a hairline (adw-toolbar-view.c:1011-1024,
|
|
36
65
|
// _toolbars.scss `> .top-bar.raised`).
|
|
@@ -60,30 +89,31 @@ adw-toolbar-view {
|
|
|
60
89
|
}
|
|
61
90
|
}
|
|
62
91
|
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
92
|
+
// A bar in a toolbar view is INLINE — `%headerbar-inline` → `%headerbar-flat`:
|
|
93
|
+
// no background of its own, no separator, the content's foreground
|
|
94
|
+
// (_toolbars.scss :313-317, _header-bar.scss :110-147). That is what makes a
|
|
95
|
+
// flat-style view read as ONE surface with the bar sitting in it rather than as
|
|
96
|
+
// a bar bolted on top of a pane, and it is why the shading below is the ONLY
|
|
97
|
+
// thing that ever draws a line between them.
|
|
98
|
+
//
|
|
99
|
+
// Not folded into `%headerbar` itself: a header bar used as a WINDOW titlebar
|
|
100
|
+
// keeps its own background and shade, which is what `adw-header-bar` outside a
|
|
101
|
+
// toolbar view still renders.
|
|
102
|
+
.adw-toolbar-view-top,
|
|
103
|
+
.adw-toolbar-view-bottom {
|
|
104
|
+
&:not(.raised) adw-header-bar {
|
|
105
|
+
background: none;
|
|
106
|
+
color: inherit;
|
|
107
|
+
box-shadow: none;
|
|
108
|
+
}
|
|
80
109
|
}
|
|
81
110
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
111
|
+
// The scroll shading under a FLAT bar belongs to the SCROLLER, not to the view:
|
|
112
|
+
// upstream's selector is `toolbarview.undershoot-top scrolledwindow`, and GTK
|
|
113
|
+
// draws that node only while the scroller is off the edge. Drawn from the view
|
|
114
|
+
// instead — which is what this did — the fade is unconditional, so every flat
|
|
115
|
+
// bar sat behind a permanent hairline that native shows only after scrolling.
|
|
116
|
+
// `_scrolling.scss` + `src/scroll-shading.ts` now own it, per scroller.
|
|
87
117
|
}
|
|
88
118
|
|
|
89
119
|
// extend-content-to-*-edge: the content spans the full height and the bar is
|
package/scss/_variables.scss
CHANGED
|
@@ -24,6 +24,13 @@
|
|
|
24
24
|
// draws separators, instead of a harsh near-black line on a dark surface.
|
|
25
25
|
--separator-color: rgba(0, 0, 6, 0.1);
|
|
26
26
|
|
|
27
|
+
// The GENERIC shade the whole `*-shade-color` family below specialises
|
|
28
|
+
// (`_colors.scss:79,141`). Scroll undershoots are drawn from it, and it belongs
|
|
29
|
+
// to the palette rather than to any one widget: a scroller can sit on a window,
|
|
30
|
+
// a view, a sidebar or a card, and reading its neighbour's shade would make the
|
|
31
|
+
// same indicator four different colours.
|
|
32
|
+
--shade-color: rgba(0, 0, 6, 0.07);
|
|
33
|
+
|
|
27
34
|
// Sidebar / split panes. libadwaita defines the whole family, not just the
|
|
28
35
|
// background (_colors.scss:47-58), and only the background was here — so
|
|
29
36
|
// `.sidebar-pane` had no foreground to name, and an app overriding
|
|
@@ -137,9 +144,29 @@
|
|
|
137
144
|
// by AdwStyleManager from the desktop's font settings
|
|
138
145
|
// (adw-style-manager.c:169-218), defaulting to `Sans 10` / `Monospace 10`
|
|
139
146
|
// (:50-56). A browser has no style manager, so they are ordinary tokens here
|
|
140
|
-
// — same names, so re-theming goes through the documented contract.
|
|
141
|
-
//
|
|
142
|
-
//
|
|
147
|
+
// — same names, so re-theming goes through the documented contract.
|
|
148
|
+
//
|
|
149
|
+
// The monospace stack has ONE home, and did not until recently: `_data_grid.scss`
|
|
150
|
+
// used to inline it, and `source-view/theme.ts` carried a SECOND, longer copy in
|
|
151
|
+
// a TS string literal (measured on one page, `.cm-scroller` resolved to
|
|
152
|
+
// 'Adwaita Mono', 'Cascadia Code', 'JetBrains Mono', … while a `.monospace`
|
|
153
|
+
// label beside it resolved to this token — two typefaces, same document).
|
|
154
|
+
// A literal in TS is also outside the `stylesheet-font-families` rule, which
|
|
155
|
+
// reads `.css`/`.scss`. Both now read this token; its consumers are
|
|
156
|
+
// `.monospace` (_labels.scss:128), `_action_row.scss:94`, the data-grid mono
|
|
157
|
+
// cell (via `@extend .monospace`) and `<adw-source-view>`.
|
|
158
|
+
//
|
|
159
|
+
// Adwaita Mono heads the stack as a PROGRESSIVE ENHANCEMENT, not because
|
|
160
|
+
// anything here ships it: `@gjsify/adwaita-fonts` vendors two Sans TTFs and no
|
|
161
|
+
// mono face at all (the upstream mono faces are 1.4-1.5 MB each). This comment
|
|
162
|
+
// used to claim the package ships it — a claim the NativeScript theme, which
|
|
163
|
+
// carries the same stack, already contradicted in prose
|
|
164
|
+
// (`nativescript-bridge/adwaita/src/theme/adwaita.css:769`). So on a GNOME
|
|
165
|
+
// desktop the system `adwaita-mono-fonts` resolves it; anywhere else the
|
|
166
|
+
// generic tail is what renders, which is the correct outcome and the reason the
|
|
167
|
+
// tail exists. Held honest by the `stylesheet-font-families` conformance rule,
|
|
168
|
+
// whose ledger carries this reason — never delete one without the other.
|
|
169
|
+
//
|
|
143
170
|
// `'Cantarell'` is dropped from that stack on the way here: Cantarell is
|
|
144
171
|
// GNOME's PROPORTIONAL UI font, so a monospace fallback chain that reaches it
|
|
145
172
|
// second lands on a font whose figures do not line up — the exact property
|
package/scss/_view_switcher.scss
CHANGED
|
@@ -22,12 +22,19 @@ adw-view-switcher {
|
|
|
22
22
|
min-height: 34px;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
// Each tab is a flat, transparent toggle that lights up with a subtle
|
|
26
|
-
// when hovered and
|
|
25
|
+
// Each tab is a flat, transparent toggle that lights up with a subtle
|
|
26
|
+
// neutral fill when hovered and a slightly stronger one while selected.
|
|
27
27
|
.adw-view-switcher-button {
|
|
28
28
|
box-sizing: border-box;
|
|
29
29
|
display: inline-flex;
|
|
30
30
|
flex: 1 1 0; // homogeneous — every tab gets equal width
|
|
31
|
+
// `min-width: auto` is the flex-item default, and it floors every tab at
|
|
32
|
+
// its own min-content width, so a long title made ITS tab wider than the
|
|
33
|
+
// others and homogeneity silently stopped holding at exactly the widths
|
|
34
|
+
// where it matters. GTK's homogeneous box has no such floor
|
|
35
|
+
// (adw-view-switcher.c:475) and the labels ellipsize instead
|
|
36
|
+
// (adw-view-switcher-button.ui:42), which is what the 0 restores.
|
|
37
|
+
min-width: 0;
|
|
31
38
|
align-items: center;
|
|
32
39
|
justify-content: center;
|
|
33
40
|
margin: 0;
|
|
@@ -63,6 +70,10 @@ adw-view-switcher {
|
|
|
63
70
|
}
|
|
64
71
|
|
|
65
72
|
.adw-view-switcher-label {
|
|
73
|
+
// Same min-content floor as the button, one level down: without it the
|
|
74
|
+
// `overflow: hidden` box never shrinks, so the ellipsis never appears
|
|
75
|
+
// and the label pushes its tab wide instead.
|
|
76
|
+
min-width: 0;
|
|
66
77
|
white-space: nowrap;
|
|
67
78
|
overflow: hidden;
|
|
68
79
|
text-overflow: ellipsis;
|
|
@@ -99,12 +110,27 @@ adw-view-switcher {
|
|
|
99
110
|
}
|
|
100
111
|
}
|
|
101
112
|
|
|
113
|
+
// AdwViewSwitcherButton is a `.flat` GtkToggleButton
|
|
114
|
+
// (adw-view-switcher-button.ui:5-7), so its five states are
|
|
115
|
+
// `%button_basic_flat`'s currentColor alpha ladder, NOT the filled-button
|
|
116
|
+
// tokens: hover 7% / pressed 16%, and checked 10% / 13% / 19%
|
|
117
|
+
// (_buttons.scss:250-269 over _colors.scss:268-272). The same ladder the
|
|
118
|
+
// tab bar and the sidebar rows already spell out.
|
|
119
|
+
//
|
|
120
|
+
// It used to spend the FILLED-button tokens here: `--button-active-color`
|
|
121
|
+
// (.19 light / .22 dark) for the selected tab AND for its hover, and
|
|
122
|
+
// `--button-hover-color` (.13 / .15) for an unselected hover. Two defects
|
|
123
|
+
// in one rule. The selected tab sat at the shade upstream keeps for HELD
|
|
124
|
+
// DOWN, so it read as permanently pressed; and pinning its hover to that
|
|
125
|
+
// same value left the selected tab answering the pointer with nothing.
|
|
126
|
+
// Those tokens are also a FIXED rgba, so unlike the ladder they do not
|
|
127
|
+
// follow the foreground of whatever bar the switcher was dropped into.
|
|
102
128
|
&:hover {
|
|
103
|
-
background-color:
|
|
129
|
+
background-color: color-mix(in srgb, currentColor 7%, transparent);
|
|
104
130
|
}
|
|
105
131
|
|
|
106
132
|
&:active {
|
|
107
|
-
background-color:
|
|
133
|
+
background-color: color-mix(in srgb, currentColor 16%, transparent);
|
|
108
134
|
}
|
|
109
135
|
|
|
110
136
|
&:focus-visible {
|
|
@@ -112,15 +138,17 @@ adw-view-switcher {
|
|
|
112
138
|
outline-offset: -2px;
|
|
113
139
|
}
|
|
114
140
|
|
|
115
|
-
// The selected view
|
|
116
|
-
//
|
|
117
|
-
// rather than turning accent).
|
|
141
|
+
// The selected view: a neutral fill over the whole tab. libadwaita leaves
|
|
142
|
+
// the foreground alone here, so no accent recolouring of icon or label.
|
|
118
143
|
&.active {
|
|
119
|
-
background-color:
|
|
120
|
-
color: var(--window-fg-color);
|
|
144
|
+
background-color: color-mix(in srgb, currentColor 10%, transparent);
|
|
121
145
|
|
|
122
146
|
&:hover {
|
|
123
|
-
background-color:
|
|
147
|
+
background-color: color-mix(in srgb, currentColor 13%, transparent);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
&:active {
|
|
151
|
+
background-color: color-mix(in srgb, currentColor 19%, transparent);
|
|
124
152
|
}
|
|
125
153
|
}
|
|
126
154
|
}
|
|
@@ -141,8 +169,15 @@ adw-view-switcher {
|
|
|
141
169
|
.adw-view-switcher-button {
|
|
142
170
|
flex-direction: column;
|
|
143
171
|
gap: 1px;
|
|
144
|
-
padding
|
|
145
|
-
padding-
|
|
172
|
+
// The narrow box carries NO horizontal padding upstream: only
|
|
173
|
+
// `padding-top: 4px`, and the 3px per side plus the 2px below sit on
|
|
174
|
+
// the LABEL (_view-switcher.scss:15-24). Aggregated onto the button
|
|
175
|
+
// here because this port has no separate box node between button and
|
|
176
|
+
// label, which is the same geometry for every label at least as wide
|
|
177
|
+
// as the 16px icon. Overriding only the vertical halves left the wide
|
|
178
|
+
// policy's `2px 12px` standing, so a narrow tab spent 24px on side
|
|
179
|
+
// padding where libadwaita spends 6px.
|
|
180
|
+
padding: 4px 3px 2px;
|
|
146
181
|
font-size: 0.75rem;
|
|
147
182
|
|
|
148
183
|
.adw-view-switcher-label {
|
|
@@ -152,22 +187,47 @@ adw-view-switcher {
|
|
|
152
187
|
}
|
|
153
188
|
}
|
|
154
189
|
|
|
155
|
-
// The paired content area showing the active page
|
|
156
|
-
//
|
|
190
|
+
// The paired content area showing the active page: the Adw.ViewStack half of
|
|
191
|
+
// this composite, and it draws NOTHING. libadwaita's recommended arrangement
|
|
192
|
+
// is an AdwToolbarView whose top bar holds the switcher and whose `content`
|
|
193
|
+
// is the bare AdwViewStack (adw-view-switcher.c:41-73); no rule in
|
|
194
|
+
// `_view-switcher.scss`, `_toolbars.scss` or `_views.scss` frames that stack,
|
|
195
|
+
// and the standalone `adw-view-stack` element here says the same in its own
|
|
196
|
+
// words.
|
|
197
|
+
//
|
|
198
|
+
// It used to be a card: a 1px separator outline, a 12px radius, a 9px gap
|
|
199
|
+
// under the row and 12px of padding inside. On a page that gives the switcher
|
|
200
|
+
// its full width, that outline runs down the container's own left and right
|
|
201
|
+
// edges with its corners curling away from them, which reads as a broken
|
|
202
|
+
// frame rather than as a view. libadwaita has no line between the row and the
|
|
203
|
+
// content at all: a FLAT top bar (the default, adw-toolbar-view.c:731) has no
|
|
204
|
+
// background and no shade, and the only shading that ever appears is the
|
|
205
|
+
// scroller's undershoot, which `_scrolling.scss` owns per scroller for the
|
|
206
|
+
// reason `_toolbar_view.scss` records.
|
|
207
|
+
//
|
|
208
|
+
// `display: flex` is load-bearing, not decoration: this box is the stack, and
|
|
209
|
+
// `adw-view-stack` is a flex COLUMN for the same reason. Without it the box
|
|
210
|
+
// stays block-level, its child's `flex: 1 1 auto` below has no flex container
|
|
211
|
+
// to answer and is silently inert, and the visible page keeps stopping at its
|
|
212
|
+
// own text in a pane many times its height.
|
|
157
213
|
.adw-view-switcher-content {
|
|
214
|
+
display: flex;
|
|
215
|
+
flex-direction: column;
|
|
158
216
|
flex: 1 1 auto;
|
|
159
|
-
|
|
160
|
-
padding: var(--spacing-m);
|
|
161
|
-
background-color: var(--view-bg-color);
|
|
162
|
-
border: 1px solid var(--separator-color);
|
|
163
|
-
border-radius: var(--card-radius);
|
|
217
|
+
min-height: 0;
|
|
164
218
|
}
|
|
165
219
|
|
|
220
|
+
// Mirrors `.adw-view-stack-child`: the visible page is handed the whole
|
|
221
|
+
// content area, which is what lets a status page centre itself in it the way
|
|
222
|
+
// AdwViewStack allocates to its visible child.
|
|
166
223
|
.adw-view-switcher-page {
|
|
167
224
|
display: none;
|
|
168
225
|
|
|
169
226
|
&.active-view {
|
|
170
|
-
display:
|
|
227
|
+
display: flex;
|
|
228
|
+
flex-direction: column;
|
|
229
|
+
flex: 1 1 auto;
|
|
230
|
+
min-height: 0;
|
|
171
231
|
}
|
|
172
232
|
}
|
|
173
233
|
}
|
package/scss/adwaita-skin.scss
CHANGED
|
@@ -26,6 +26,9 @@ import {
|
|
|
26
26
|
|
|
27
27
|
import { normalizeIconName } from '@gjsify/adwaita-core';
|
|
28
28
|
|
|
29
|
+
import { isIconAvailable } from './icon-registry.js';
|
|
30
|
+
import { fallbackMask, maskOf } from './icon-registry.spec.js';
|
|
31
|
+
|
|
29
32
|
import type { AdwActionRow } from './elements/adw-action-row.js';
|
|
30
33
|
import type { AdwSwitchRow } from './elements/adw-switch-row.js';
|
|
31
34
|
import type { AdwWindowTitle } from './elements/adw-window-title.js';
|
|
@@ -259,15 +262,24 @@ export const AdwActionRowsTest = async () => {
|
|
|
259
262
|
// The mask class is the NORMALIZED name — the generated classes never
|
|
260
263
|
// carry `-symbolic`, so interpolating the raw one asks for a class that has
|
|
261
264
|
// never existed and draws an empty box.
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
265
|
+
//
|
|
266
|
+
// And the class is only HALF the answer. `external-link-symbolic` in these
|
|
267
|
+
// vectors is well formed, normalizes cleanly and gets its class — while no
|
|
268
|
+
// mask class of that name exists (it is in neither the Adwaita icon theme
|
|
269
|
+
// nor `@gjsify/adwaita-icons`), so it paints the image-missing glyph. This
|
|
270
|
+
// suite was green on that: the class-string assertion the icon work exists
|
|
271
|
+
// to end. `isIconAvailable` reads the live cascade, so it decides which of
|
|
272
|
+
// the two answers is right for THIS name instead of the test guessing.
|
|
273
|
+
for (const [index, declared, visible] of [
|
|
274
|
+
[0, vector.startIconName, vector.startIconVisible],
|
|
275
|
+
[1, vector.endIconName, vector.endIconVisible],
|
|
276
|
+
] as const) {
|
|
277
|
+
if (!visible) continue;
|
|
278
|
+
const icon = icons[index]!;
|
|
279
|
+
const resolved = normalizeIconName(declared);
|
|
280
|
+
expect(icon.classList.contains(`adw-icon--${resolved}`)).toBe(true);
|
|
281
|
+
expect(maskOf(icon)).not.toBe('none');
|
|
282
|
+
expect(maskOf(icon) === fallbackMask()).toBe(!isIconAvailable(resolved));
|
|
271
283
|
}
|
|
272
284
|
host.remove();
|
|
273
285
|
});
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
BUTTON_CONTENT_TEXT_VECTORS,
|
|
21
21
|
} from '@gjsify/adwaita-core/conformance';
|
|
22
22
|
import type { AdwButtonContent } from './elements/adw-button-content.js';
|
|
23
|
+
import { fallbackMask, maskOf } from './icon-registry.spec.js';
|
|
23
24
|
|
|
24
25
|
/** Mount a button content inside a plain Adwaita button, the way the stories do. */
|
|
25
26
|
function mount(attributes: Record<string, string> = {}): {
|
|
@@ -123,13 +124,17 @@ export const AdwButtonContentTest = async () => {
|
|
|
123
124
|
// asked for `.adw-icon--folder-download-symbolic`, a class that has
|
|
124
125
|
// never existed, and drew an empty 16px box. `<adw-icon>` applies
|
|
125
126
|
// `normalizeIconName`, which also makes the `' '` row assertable
|
|
126
|
-
// instead of skipped: a space is not one CSS token, so it
|
|
127
|
-
//
|
|
127
|
+
// instead of skipped: a space is not one CSS token, so it lands on
|
|
128
|
+
// `image-missing` — the missing icon that row's own rule records GTK
|
|
129
|
+
// drawing there, by lookup failure rather than by this branch.
|
|
128
130
|
const maskName = normalizeIconName(resolved);
|
|
129
|
-
|
|
130
|
-
expect(
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
const drawn = maskName === '' ? 'image-missing' : maskName;
|
|
132
|
+
expect(icon.classList.contains(`adw-icon--${drawn}`)).toBe(true);
|
|
133
|
+
expect([...icon.classList].filter((c) => c.startsWith('adw-icon--')).length).toBe(1);
|
|
134
|
+
// …and it DRAWS that. The class alone is what the original bug survived:
|
|
135
|
+
// applied correctly, to a name with no glyph behind it.
|
|
136
|
+
expect(maskOf(icon)).not.toBe('none');
|
|
137
|
+
expect(maskOf(icon) === fallbackMask()).toBe(drawn === 'image-missing');
|
|
133
138
|
// The image node is never hidden — `gtk_widget_set_visible` is
|
|
134
139
|
// called on the LABEL only.
|
|
135
140
|
expect(icon.hidden).toBe(false);
|
package/src/adw-carousel.spec.ts
CHANGED
|
@@ -9,7 +9,12 @@
|
|
|
9
9
|
// element sets it as an inline style precisely so it stays overridable.
|
|
10
10
|
import { describe, expect, it } from '@gjsify/unit';
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import type { CarouselPageMeasurement } from '@gjsify/adwaita-core';
|
|
13
|
+
import {
|
|
14
|
+
CAROUSEL_PAGE_ALLOCATION_VECTORS,
|
|
15
|
+
CAROUSEL_PAGE_AT_POSITION_VECTORS,
|
|
16
|
+
CAROUSEL_PAGE_LIST_VECTORS,
|
|
17
|
+
} from '@gjsify/adwaita-core/conformance';
|
|
13
18
|
import type { CarouselPageOp } from '@gjsify/adwaita-core/conformance';
|
|
14
19
|
|
|
15
20
|
/** The carousel surface this suite drives — the element's own public API. */
|
|
@@ -62,6 +67,45 @@ function mount(labels: readonly string[], attributes: Record<string, string> = {
|
|
|
62
67
|
return { carousel, host, track };
|
|
63
68
|
}
|
|
64
69
|
|
|
70
|
+
/**
|
|
71
|
+
* One page that MEASURES what a conformance row says it does. `expand` is spelled
|
|
72
|
+
* `width: 100%`, the CSS of hexpand; `flex: none` keeps the slot's own flex layout from
|
|
73
|
+
* shrinking a natural size that is wider than the carousel before it can be measured.
|
|
74
|
+
*/
|
|
75
|
+
function sizedCard(page: CarouselPageMeasurement, index: number): HTMLElement {
|
|
76
|
+
const element = document.createElement('div');
|
|
77
|
+
element.dataset.label = `p${index}`;
|
|
78
|
+
const width = page.expand ? '100%' : `${page.natural}px`;
|
|
79
|
+
element.style.cssText = `flex:none;height:100%;width:${width};`;
|
|
80
|
+
return element;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Mount a carousel `available` px wide whose pages measure what `pages` says. */
|
|
84
|
+
function mountSized(available: number, pages: readonly CarouselPageMeasurement[]) {
|
|
85
|
+
const host = document.createElement('div');
|
|
86
|
+
host.style.cssText = `width:${available}px;height:80px;`;
|
|
87
|
+
document.body.appendChild(host);
|
|
88
|
+
|
|
89
|
+
const carousel = document.createElement('adw-carousel') as CarouselElement;
|
|
90
|
+
carousel.style.cssText = `display:block;width:${available}px;height:80px;`;
|
|
91
|
+
pages.forEach((page, index) => carousel.appendChild(sizedCard(page, index)));
|
|
92
|
+
host.appendChild(carousel);
|
|
93
|
+
|
|
94
|
+
const track = carousel.querySelector('.adw-carousel-track') as HTMLElement;
|
|
95
|
+
// Instant scrolls; snapping stays ON here, because where a page SNAPS is half of
|
|
96
|
+
// what these tests are about.
|
|
97
|
+
track.style.setProperty('scroll-behavior', 'auto');
|
|
98
|
+
return { carousel, host, track };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Each page's left edge relative to the carousel, rounded off the subpixel noise. */
|
|
102
|
+
function pageEdges(carousel: HTMLElement): number[] {
|
|
103
|
+
const origin = carousel.getBoundingClientRect().left;
|
|
104
|
+
return Array.from(carousel.querySelectorAll('.adw-carousel-page')).map((slot) =>
|
|
105
|
+
Math.round(slot.getBoundingClientRect().left - origin),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
65
109
|
/** The page labels in DOM order — what the user actually sees, not what the model says. */
|
|
66
110
|
function domOrder(carousel: HTMLElement): string[] {
|
|
67
111
|
return Array.from(carousel.querySelectorAll('.adw-carousel-page')).map(
|
|
@@ -159,6 +203,102 @@ export const AdwCarouselTest = async () => {
|
|
|
159
203
|
}
|
|
160
204
|
});
|
|
161
205
|
|
|
206
|
+
await describe('adw-carousel page allocation (libadwaita conformance vectors)', async () => {
|
|
207
|
+
// The rows the DOM can express. The element measures a RENDERED width, so a page
|
|
208
|
+
// whose MINIMUM exceeds the carousel has already been resolved by CSS before the
|
|
209
|
+
// element sees it, and a carousel with no pages has nothing to measure at all.
|
|
210
|
+
const measurable = CAROUSEL_PAGE_ALLOCATION_VECTORS.filter(
|
|
211
|
+
(vector) => vector.pages.length > 0 && vector.pages.every((page) => page.minimum === undefined),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
for (const vector of measurable) {
|
|
215
|
+
await it(vector.rule, async () => {
|
|
216
|
+
const { carousel, host, track } = mountSized(vector.available, vector.pages);
|
|
217
|
+
await flush();
|
|
218
|
+
|
|
219
|
+
const widths = Array.from(carousel.querySelectorAll('.adw-carousel-page')).map((slot) =>
|
|
220
|
+
Math.round(slot.getBoundingClientRect().width),
|
|
221
|
+
);
|
|
222
|
+
expect(widths).toStrictEqual(vector.pages.map(() => vector.pageSize));
|
|
223
|
+
// The half-gap is the track's own padding, which is what lets the page
|
|
224
|
+
// strip start inside the carousel instead of flush against its edge.
|
|
225
|
+
expect(Math.round(Number.parseFloat(getComputedStyle(track).paddingInlineStart))).toBe(
|
|
226
|
+
vector.leadingInset,
|
|
227
|
+
);
|
|
228
|
+
host.remove();
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
await it('shows the previous and next page at the edges', async () => {
|
|
233
|
+
// THE REPORTED DEFECT. With `flex: 0 0 100%` the pages of the Carousel story
|
|
234
|
+
// (440px cards in a 480px carousel) filled the width and both neighbours sat
|
|
235
|
+
// entirely off screen, where GTK leaves 20px of each showing.
|
|
236
|
+
const pages: CarouselPageMeasurement[] = [{ natural: 440 }, { natural: 440 }, { natural: 440 }];
|
|
237
|
+
const { carousel, host } = mountSized(480, pages);
|
|
238
|
+
await flush();
|
|
239
|
+
|
|
240
|
+
// At rest on page 0: centred, with page 1 starting 20px before the right edge.
|
|
241
|
+
expect(pageEdges(carousel)).toStrictEqual([20, 460, 900]);
|
|
242
|
+
|
|
243
|
+
carousel.scrollToPage(1);
|
|
244
|
+
await flush();
|
|
245
|
+
// Page 1 centred, 20px of page 0 showing at the left and 20px of page 2 at the right.
|
|
246
|
+
expect(pageEdges(carousel)).toStrictEqual([-420, 20, 460]);
|
|
247
|
+
host.remove();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
await it('leaves the LAST page room to centre as well', async () => {
|
|
251
|
+
// The trailing half-gap is track padding too. A scroll container that dropped
|
|
252
|
+
// its end padding would stop `leadingInset` short of the last snap point, and
|
|
253
|
+
// the last page would never reach the middle.
|
|
254
|
+
const pages: CarouselPageMeasurement[] = [{ natural: 440 }, { natural: 440 }, { natural: 440 }];
|
|
255
|
+
const { carousel, host, track } = mountSized(480, pages);
|
|
256
|
+
await flush();
|
|
257
|
+
|
|
258
|
+
carousel.scrollToPage(2);
|
|
259
|
+
await flush();
|
|
260
|
+
expect(Math.round(track.scrollLeft)).toBe(880);
|
|
261
|
+
expect(carousel.currentPage).toBe(2);
|
|
262
|
+
expect(pageEdges(carousel)).toStrictEqual([-860, -420, 20]);
|
|
263
|
+
host.remove();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
await it('re-aims the scroll in flight instead of rewinding it', async () => {
|
|
267
|
+
// Mounted inside a HIDDEN ancestor, which is how real markup reaches this:
|
|
268
|
+
// `connectedCallback` measures 0, so the declared position waits for a
|
|
269
|
+
// layout, and revealing the host delivers the pending-position observer and
|
|
270
|
+
// the layout observer in ONE resize pass. The first fires a scroll to page
|
|
271
|
+
// 2 at the fallback pitch; the second narrows the pages and has to put the
|
|
272
|
+
// offset back, and `state.position` is still 0 there, because the `scroll`
|
|
273
|
+
// event that will teach it 2 fires a frame later. Restoring THAT parks the
|
|
274
|
+
// carousel on page 0 and swallows `position="2"`. GTK re-aims instead: it
|
|
275
|
+
// pushes the recomputed snap point into the running animation (:786-788).
|
|
276
|
+
//
|
|
277
|
+
// A carousel already laid out when it is mounted does NOT reach this: it
|
|
278
|
+
// allocates during `connectedCallback` and the pending observer never
|
|
279
|
+
// arms, which is why the mounted-visible spelling of this test passes
|
|
280
|
+
// either way and is not the one kept.
|
|
281
|
+
const host = document.createElement('div');
|
|
282
|
+
host.style.cssText = 'width:480px;height:80px;display:none;';
|
|
283
|
+
document.body.appendChild(host);
|
|
284
|
+
|
|
285
|
+
const carousel = document.createElement('adw-carousel') as CarouselElement;
|
|
286
|
+
carousel.style.cssText = 'display:block;width:480px;height:80px;';
|
|
287
|
+
carousel.setAttribute('position', '2');
|
|
288
|
+
for (let index = 0; index < 3; index++) carousel.appendChild(sizedCard({ natural: 440 }, index));
|
|
289
|
+
host.appendChild(carousel);
|
|
290
|
+
await flush();
|
|
291
|
+
|
|
292
|
+
host.style.display = 'block';
|
|
293
|
+
await flush();
|
|
294
|
+
|
|
295
|
+
const track = carousel.querySelector('.adw-carousel-track') as HTMLElement;
|
|
296
|
+
expect(Math.round(track.scrollLeft)).toBe(880);
|
|
297
|
+
expect(carousel.currentPage).toBe(2);
|
|
298
|
+
host.remove();
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
162
302
|
await describe('adw-carousel page lookup (libadwaita conformance vectors)', async () => {
|
|
163
303
|
for (const vector of CAROUSEL_PAGE_AT_POSITION_VECTORS) {
|
|
164
304
|
// Only rows whose snap points are a settled page list are reachable through
|