@gjsify/adwaita-web 0.32.0 → 0.33.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 (38) hide show
  1. package/dist/adwaita-web.css +72 -50
  2. package/dist/adwaita-web.css.map +1 -1
  3. package/lib/types/elements/adw-combo-row.d.ts +10 -0
  4. package/lib/types/elements/adw-inline-view-switcher.d.ts +37 -1
  5. package/lib/types/elements/adw-navigation-split-view.d.ts +42 -0
  6. package/lib/types/elements/adw-overlay-split-view.d.ts +56 -0
  7. package/lib/types/elements/adw-spinner.d.ts +23 -0
  8. package/lib/types/elements/adw-view-stack.d.ts +12 -0
  9. package/lib/types/elements/adw-view-switcher-bar.d.ts +7 -5
  10. package/lib/types/elements/adw-view-switcher.d.ts +55 -3
  11. package/lib/types/elements/adw-wrap-box.d.ts +58 -3
  12. package/lib/types/styles.generated.d.ts +1 -1
  13. package/package.json +6 -6
  14. package/scss/_combo_row.scss +19 -0
  15. package/scss/_navigation_split_view.scss +47 -8
  16. package/scss/_overlay_split_view.scss +117 -32
  17. package/scss/_spinner.scss +35 -21
  18. package/scss/_tab_view.scss +12 -13
  19. package/scss/_wrap_box.scss +9 -0
  20. package/src/adw-avatar.spec.ts +37 -0
  21. package/src/adw-button-content.spec.ts +24 -0
  22. package/src/adw-row-state.spec.ts +49 -0
  23. package/src/adw-wrap-box.spec.ts +148 -1
  24. package/src/chrome.spec.ts +111 -12
  25. package/src/elements/adw-action-row.ts +11 -1
  26. package/src/elements/adw-combo-row.ts +17 -0
  27. package/src/elements/adw-inline-view-switcher.ts +91 -8
  28. package/src/elements/adw-navigation-split-view.ts +179 -14
  29. package/src/elements/adw-overlay-split-view.ts +378 -20
  30. package/src/elements/adw-spinner.ts +165 -21
  31. package/src/elements/adw-tab-view.ts +20 -2
  32. package/src/elements/adw-view-stack.ts +21 -0
  33. package/src/elements/adw-view-switcher-bar.ts +13 -5
  34. package/src/elements/adw-view-switcher.ts +103 -11
  35. package/src/elements/adw-wrap-box.ts +290 -135
  36. package/src/split-views.spec.ts +586 -2
  37. package/src/styles.generated.ts +1 -1
  38. package/src/view-switcher.spec.ts +219 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/adwaita-web",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "description": "Adwaita/Libadwaita web components for browser targets",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -41,14 +41,14 @@
41
41
  "@codemirror/language": "^6.12.4",
42
42
  "@codemirror/state": "^6.7.0",
43
43
  "@codemirror/view": "^6.43.4",
44
- "@gjsify/adwaita-core": "^0.32.0",
45
- "@gjsify/adwaita-fonts": "^0.32.0",
46
- "@gjsify/adwaita-icons": "^0.32.0",
44
+ "@gjsify/adwaita-core": "^0.33.0",
45
+ "@gjsify/adwaita-fonts": "^0.33.0",
46
+ "@gjsify/adwaita-icons": "^0.33.0",
47
47
  "@lezer/highlight": "^1.2.3"
48
48
  },
49
49
  "devDependencies": {
50
- "@gjsify/cli": "^0.32.0",
51
- "@gjsify/unit": "^0.32.0",
50
+ "@gjsify/cli": "^0.33.0",
51
+ "@gjsify/unit": "^0.33.0",
52
52
  "sass": "^1.101.0",
53
53
  "typescript": "^6.0.3"
54
54
  },
@@ -59,6 +59,11 @@ adw-combo-row {
59
59
  // fill, making the icon theme-aware (matches libadwaita's dark theme).
60
60
  // Using background-image with a data-URI SVG would NOT resolve
61
61
  // `currentColor` in most browsers.
62
+ //
63
+ // `model_changed` (adw-combo-row.c:187-195) hides the arrow box entirely on
64
+ // a model of one item or none, so the row stops presenting itself as a
65
+ // chooser. `[data-presents-chooser='false']` below is that rule; the element
66
+ // publishes the attribute and disables the overlaid <select> alongside.
62
67
  &::after {
63
68
  content: "";
64
69
  display: inline-block;
@@ -76,3 +81,17 @@ adw-combo-row {
76
81
  }
77
82
  }
78
83
  }
84
+
85
+ // A model with one item or none: no arrow, and the row is not activatable
86
+ // (`model_changed`, adw-combo-row.c:187-195). Written as its own block rather
87
+ // than a modifier inside `.adw-row-value` so the two selectors that draw the
88
+ // arrow and the one that removes it read at the same specificity depth.
89
+ adw-combo-row[data-presents-chooser='false'] {
90
+ .adw-row-value::after {
91
+ display: none;
92
+ }
93
+
94
+ select {
95
+ cursor: default;
96
+ }
97
+ }
@@ -1,5 +1,16 @@
1
1
  // _navigation_split_view.scss — adw-navigation-split-view styling (docked sidebar).
2
+ //
3
+ // Visibility is `data-pane-visible`, written by the element from
4
+ // `update_navigation_stack` (adw-navigation-split-view.c:342-405), not from the
5
+ // two flags. Two of that table's rules are not expressible as
6
+ // `.collapsed.show-content`: a LONE child stays visible whatever `show-content`
7
+ // says (:389-401), and with `sidebar-position: end` the CONTENT is the root page
8
+ // so the sidebar is pushed ON TOP of it rather than replacing it. This file used
9
+ // to key off the flags and got both wrong — a collapsed view with only a sidebar
10
+ // rendered blank.
11
+ //
2
12
  // Reference: refs/adwaita-web/adwaita-web/docs/widgets/navigationsplitview.md
13
+ // Reference: refs/libadwaita/src/adw-navigation-split-view.c
3
14
  // Reference: refs/libadwaita/src/stylesheet/widgets/_sidebars.scss
4
15
  // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
5
16
  // Modifications: Adapted for @gjsify/adwaita-web Web Components.
@@ -16,33 +27,61 @@ adw-navigation-split-view {
16
27
  flex-direction: column;
17
28
  min-height: 0;
18
29
  flex-shrink: 0;
19
- border-right: 1px solid var(--separator-color);
20
30
  background-color: var(--sidebar-bg-color);
21
31
  }
22
32
 
33
+ // The container MINIMUM, as CSS spells it: `measure_uncollapsed`
34
+ // (adw-navigation-split-view.c:264-267) is `sidebar_min + content_min`, which
35
+ // a flex row reaches by itself once the content may claim its own minimum.
36
+ // `min-width: 0` — the flex idiom for "let this be crushed" — was here, which
37
+ // is how the content pane could be squeezed to nothing while the sidebar,
38
+ // being `flex-shrink: 0`, kept its whole width.
23
39
  .adw-nsv-content {
24
40
  display: flex;
25
41
  flex-direction: column;
26
- min-width: 0;
42
+ min-width: min-content;
27
43
  min-height: 0;
28
44
  flex: 1 1 auto;
29
45
  background-color: var(--window-bg-color);
30
46
  }
31
47
 
32
- // Collapsed (narrow): one pane at a time.
33
- &.collapsed.show-content .adw-nsv-sidebar {
34
- display: none;
48
+ // `sidebar-position: end` docks the sidebar past the content. flex `order`
49
+ // flips the visual order without changing the slotting DOM order.
50
+ &.sidebar-end {
51
+ .adw-nsv-content {
52
+ order: 1;
53
+ }
54
+
55
+ .adw-nsv-sidebar {
56
+ order: 2;
57
+ }
35
58
  }
36
59
 
37
- &.collapsed:not(.show-content) {
38
- .adw-nsv-content {
60
+ // THE DIVIDER FOLLOWS THE VISUAL SIDE. `_sidebars.scss:1293-1302` makes it
61
+ // `:dir()` × `.end`, which is the product `.sidebar-at-visual-start` carries:
62
+ // under RTL a `start` sidebar is drawn on the right and its divider belongs on
63
+ // its left edge. A fixed `border-right` drew it on the wrong side for half the
64
+ // four combinations.
65
+ &.sidebar-at-visual-start .adw-nsv-sidebar {
66
+ border-right: 1px solid var(--separator-color);
67
+ }
68
+
69
+ &:not(.sidebar-at-visual-start) .adw-nsv-sidebar {
70
+ border-left: 1px solid var(--separator-color);
71
+ }
72
+
73
+ // Collapsed: whichever pane the STACK puts on top fills the view, and the
74
+ // other is not rendered at all.
75
+ &.collapsed {
76
+ [data-pane-visible='false'] {
39
77
  display: none;
40
78
  }
41
79
 
42
- .adw-nsv-sidebar {
80
+ .adw-nsv-sidebar[data-pane-visible='true'] {
43
81
  flex: 1 1 auto;
44
82
  max-width: none;
45
83
  border-right: none;
84
+ border-left: none;
46
85
  }
47
86
  }
48
87
  }
@@ -1,5 +1,21 @@
1
1
  // _overlay_split_view.scss — adw-overlay-split-view styling.
2
+ //
3
+ // The GEOMETRY is no longer here. It used to be four `transform: translateX(±100%)`
4
+ // rules keyed on `.collapsed` / `.show-sidebar` / `.sidebar-end`, which can only
5
+ // express the two END STATES of a reveal — so `show-progress` was 0 or 1, the
6
+ // five `OVERLAY_SWIPE_*` conformance tables shipped unexercised, and the docked
7
+ // hide slid the wrong way for an `end` sidebar because the JS wrote `marginRight`
8
+ // whichever edge the pane was on.
9
+ //
10
+ // The element now writes the pane rect per frame from
11
+ // `layoutOverlaySplitView`, which is continuous and knows the edge (including
12
+ // under RTL, where a `start` sidebar is drawn on the RIGHT —
13
+ // `get_start_or_end`, adw-overlay-split-view.c:227-234). What is left here is
14
+ // what CSS genuinely owns: the box model, the stacking, and the transition that
15
+ // smooths a programmatic toggle.
16
+ //
2
17
  // Reference: refs/libadwaita/src/adw-overlay-split-view.c
18
+ // Reference: refs/libadwaita/src/stylesheet/widgets/_sidebars.scss
3
19
  // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
4
20
  // Modifications: Implemented as Web Component for @gjsify/adwaita-web.
5
21
 
@@ -15,34 +31,44 @@ adw-overlay-split-view {
15
31
  background-color: var(--sidebar-bg-color);
16
32
  overflow-y: auto;
17
33
  flex-shrink: 0;
18
- transition: transform 0.2s ease, opacity 0.2s ease, margin 0.2s ease;
19
34
  z-index: 10;
20
35
  align-self: stretch;
36
+ // Only `opacity` transitions. The offset is driven per frame, so animating
37
+ // it here would fight the swipe: a gesture would lag its own finger.
38
+ transition: opacity 0.2s ease;
21
39
  }
22
40
 
41
+ // THE CONTAINER MINIMUM, as CSS spells it. `measure_uncollapsed`
42
+ // (adw-overlay-split-view.c:558-563) is `trunc(sidebar_min * progress) +
43
+ // content_min`, and a flex row reaches exactly that on its own PROVIDED the
44
+ // content is allowed to claim its own minimum: the sidebar is `flex-shrink: 0`
45
+ // with a `min-width` of `sidebar_min`, so the sum is the container's floor.
46
+ //
47
+ // `min-width: 0` was here instead — the flex idiom for "let this be crushed so
48
+ // an ellipsis can happen" — which is precisely how the content pane could be
49
+ // squeezed to nothing while the sidebar kept its whole width. A child that
50
+ // needs to truncate can still ask for `min-width: 0` itself; the PANE may not.
51
+ //
52
+ // Not computed in JS: a measured number would need the content's min-content
53
+ // size, which the DOM does not expose without a reflow, and the first attempt
54
+ // used `scrollWidth` — the pane's CURRENT width — which pinned the view at
55
+ // whatever it happened to be and stopped it responding to its breakpoint.
23
56
  .adw-osv-content {
24
57
  flex: 1;
25
- min-width: 0;
58
+ min-width: min-content;
26
59
  min-height: 0;
27
60
  display: flex;
28
61
  flex-direction: column;
29
62
  align-self: stretch;
30
63
  }
31
64
 
32
- // Docked mode (not collapsed) — sidebar beside content
65
+ // Docked — the sidebar sits in the flex row and the hidden part is taken off
66
+ // its own edge by a negative margin the element computes.
33
67
  &:not(.collapsed) .adw-osv-sidebar {
34
68
  position: relative;
35
69
  }
36
70
 
37
- // Docked hide: slide left and collapse via opacity. Sidebar keeps its
38
- // intrinsic width so internal elements don't reflow.
39
- &:not(.collapsed):not(.show-sidebar) .adw-osv-sidebar {
40
- transform: translateX(-100%);
41
- opacity: 0;
42
- pointer-events: none;
43
- }
44
-
45
- // Docked + sidebar-end — dock the sidebar to the RIGHT of the content
71
+ // Docked + sidebar-end — dock the sidebar to the far side of the content
46
72
  // (the Adw.OverlaySplitView { sidebar-position: end } layout). flex `order`
47
73
  // flips the visual order without changing the slotting DOM order.
48
74
  &:not(.collapsed).sidebar-end {
@@ -52,49 +78,108 @@ adw-overlay-split-view {
52
78
 
53
79
  .adw-osv-sidebar {
54
80
  order: 2;
55
- border-inline-start: 1px solid var(--separator-color);
56
81
  }
57
82
  }
58
83
 
59
- // Docked hide when the sidebar is on the end edge: slide RIGHT, not left.
60
- &:not(.collapsed):not(.show-sidebar).sidebar-end .adw-osv-sidebar {
61
- transform: translateX(100%);
84
+ // THE DIVIDER FOLLOWS THE VISUAL SIDE, not the logical one. `_sidebars.scss`
85
+ // makes it `:dir()` × `.end` (:21-39), which is exactly the product
86
+ // `.sidebar-at-visual-start` already carries: under RTL a `start` sidebar is
87
+ // drawn on the right and its divider belongs on its left edge.
88
+ &:not(.collapsed) {
89
+ .adw-osv-sidebar {
90
+ border-inline-end: none;
91
+ border-inline-start: none;
92
+ }
93
+
94
+ &.sidebar-at-visual-start .adw-osv-sidebar {
95
+ border-right: 1px solid var(--separator-color);
96
+ }
97
+
98
+ &:not(.sidebar-at-visual-start) .adw-osv-sidebar {
99
+ border-left: 1px solid var(--separator-color);
100
+ }
62
101
  }
63
102
 
64
- // Overlay mode (collapsed) — sidebar on top of content
103
+ // Overlay (collapsed) — the sidebar floats above the content at the absolute
104
+ // offset the element writes into `left`, which is the only property that can
105
+ // carry a mid-swipe position.
65
106
  &.collapsed .adw-osv-sidebar {
66
107
  position: absolute;
67
108
  top: 0;
68
109
  bottom: 0;
69
- left: 0;
70
- transform: translateX(-100%);
71
- opacity: 0;
72
- pointer-events: none;
110
+ box-shadow: 0 0 12px rgb(0 0 0 / 25%);
73
111
  }
74
112
 
75
- &.collapsed.sidebar-end .adw-osv-sidebar {
76
- left: auto;
77
- right: 0;
78
- transform: translateX(100%);
113
+ // THE TWO RESTING STATES ARE CSS. Everything BETWEEN them is JS.
114
+ //
115
+ // Dropping these is what shipped the docs-site regression. The offset is
116
+ // driven per frame from `layoutOverlaySplitView` — but a view that has never
117
+ // been measured drives nothing, and an absolutely-positioned pane with no
118
+ // `left` resolves to `left: 0` at full opacity. So a HIDDEN sidebar was
119
+ // painted over the content at the wrong edge, on a page whose slideshow
120
+ // builds each slide inside `display: none` and moves it in, so the first
121
+ // measurement never arrived and this fallback was the whole rendering.
122
+ //
123
+ // Not a second copy of the arithmetic: 0% and 100% ARE the two ends the JS
124
+ // interpolates between, and neither of them needs a pixel to say. The element
125
+ // clears its inline placement whenever it has no measurement, which is what
126
+ // hands control back to these rules instead of stranding a stale offset.
127
+ &.collapsed {
128
+ // Revealed: flush against the edge the pane is drawn on.
129
+ &.sidebar-at-visual-start .adw-osv-sidebar {
130
+ left: 0;
131
+ right: auto;
132
+ }
133
+
134
+ &:not(.sidebar-at-visual-start) .adw-osv-sidebar {
135
+ left: auto;
136
+ right: 0;
137
+ }
138
+
139
+ // Hidden: entirely off that same edge, and untouchable while it is there.
140
+ &:not(.show-sidebar) {
141
+ .adw-osv-sidebar {
142
+ opacity: 0;
143
+ pointer-events: none;
144
+ }
145
+
146
+ &.sidebar-at-visual-start .adw-osv-sidebar {
147
+ left: auto;
148
+ right: 100%;
149
+ }
150
+
151
+ &:not(.sidebar-at-visual-start) .adw-osv-sidebar {
152
+ left: 100%;
153
+ right: auto;
154
+ }
155
+ }
79
156
  }
80
157
 
81
- &.collapsed.show-sidebar .adw-osv-sidebar {
82
- transform: translateX(0);
83
- opacity: 1;
84
- pointer-events: auto;
158
+ // Docked and hidden — the pane takes no room in the row. The JS expresses
159
+ // this as a negative margin so it can animate; unmeasured, the end state is
160
+ // simply a pane of no width.
161
+ &:not(.collapsed):not(.show-sidebar) .adw-osv-sidebar {
162
+ width: 0;
163
+ min-width: 0;
164
+ overflow: hidden;
165
+ opacity: 0;
166
+ pointer-events: none;
85
167
  }
86
168
 
87
- // Backdrop — visible only in overlay mode when sidebar is open
169
+ // Backdrop — the click-outside shield. `hidden` is the element's, off
170
+ // `shieldVisible`; the OPACITY is the shadow-helper progress, so it fades with
171
+ // the reveal instead of appearing whole.
88
172
  .adw-osv-backdrop {
89
173
  display: none;
90
174
  }
91
175
 
92
- &.collapsed.show-sidebar .adw-osv-backdrop {
176
+ &.collapsed .adw-osv-backdrop:not([hidden]) {
93
177
  display: block;
94
178
  position: absolute;
95
179
  inset: 0;
96
- background: rgba(0, 0, 0, 0.3);
180
+ background: rgb(0 0 0 / 30%);
97
181
  z-index: 9;
182
+ transition: opacity 0.2s ease;
98
183
  }
99
184
  }
100
185
 
@@ -1,32 +1,46 @@
1
- // _spinner.scss — adw-spinner styling (ring with a spinning arc).
1
+ // _spinner.scss — adw-spinner styling.
2
+ //
3
+ // Almost nothing is left here on purpose. The ring is an SVG the element draws
4
+ // per frame from `@gjsify/adwaita-core`'s `spinnerArc`, because the arc BREATHES
5
+ // (2.7 to 102.8 degrees) and CSS cannot express that: the `border-top-color`
6
+ // chase this file used to carry was a fixed 90-degree quarter-circle at 0.8s
7
+ // against libadwaita's 1.2s, with a hardcoded grey track and square-cut ends.
8
+ //
9
+ // What CSS still owns is the box: the element is `size × size` and the ring is
10
+ // centred inside it, which is how a 200px spinner shows a 64px ring
11
+ // (adw-spinner.c:95-99 hands the widget's real size to the paintable, which caps
12
+ // only the radius).
13
+ //
14
+ // There is deliberately NO `prefers-reduced-motion` rule. The C opts out of the
15
+ // animation setting explicitly (`adw_animation_set_follow_enable_animations_setting
16
+ // (…, FALSE)`, adw-spinner-paintable.c:537) — a frozen busy indicator reads as a
17
+ // hang. This file used to stop the animation there.
18
+ //
19
+ // Reference: refs/libadwaita/src/adw-spinner.c
20
+ // Reference: refs/libadwaita/src/adw-spinner-paintable.c
2
21
  // Reference: refs/adwaita-web/adwaita-web/scss/_spinner.scss
3
- // Reference: refs/libadwaita (Adw.Spinner)
4
22
  // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
5
23
  // Modifications: Adapted for @gjsify/adwaita-web Web Components.
6
24
 
7
25
  adw-spinner {
8
- display: inline-block;
26
+ // The element writes the measured box onto `style` when it upgrades; these are
27
+ // the values in force until then, which are Adwaita's own minimum
28
+ // (adw-spinner.c:14, reported as both minimum and natural at :78-81).
29
+ display: inline-flex;
30
+ align-items: center;
31
+ justify-content: center;
9
32
  box-sizing: border-box;
10
- // Adw.Spinner measures 16 as BOTH its minimum and its natural size
11
- // (adw-spinner.c:78-81), stroked at the documented "2px for 16px". The element
12
- // overwrites both from `spinnerGeometry` when it upgrades; these are the values
13
- // in force until then — the 24px/3px they replace were not from any source.
14
33
  width: 16px;
15
34
  height: 16px;
16
- // Neutral arc (currentColor, theme-aware) over a faint track — Adw.Spinner
17
- // is a neutral indicator, NOT accent-coloured.
18
- border: 2px solid rgba(127, 127, 127, 0.25);
19
- border-top-color: currentColor;
20
- border-radius: 50%;
21
- animation: adw-spinner-spin 0.8s linear infinite;
35
+ // The arc strokes `currentColor` — `gtk_widget_get_color` (:432), the widget's
36
+ // TEXT colour, never the accent. The track is the same colour at 15%, which
37
+ // the SVG applies as `stroke-opacity`.
38
+ color: inherit;
22
39
 
23
- @media (prefers-reduced-motion: reduce) {
24
- animation: none;
25
- }
26
- }
27
-
28
- @keyframes adw-spinner-spin {
29
- to {
30
- transform: rotate(360deg);
40
+ svg {
41
+ display: block;
42
+ // The ring never grows past its own diameter, so an over-large box centres
43
+ // it rather than stretching it.
44
+ flex: none;
31
45
  }
32
46
  }
@@ -143,19 +143,18 @@ adw-tab-view {
143
143
  }
144
144
  }
145
145
 
146
- // `AdwTabPage:loading` swaps the icon image for a spinner in the same slot
147
- // (adw-tab.c:178-183) — the arc geometry and the keyframes are adw-spinner's,
148
- // not a second copy of them.
149
- .adw-tab-icon.loading {
150
- background: none;
151
- mask-image: none;
152
- border: 2px solid rgba(127, 127, 127, 0.25);
153
- border-top-color: currentColor;
154
- border-radius: 50%;
155
- animation: adw-spinner-spin 0.8s linear infinite;
156
-
157
- @media (prefers-reduced-motion: reduce) {
158
- animation: none;
146
+ // `AdwTabPage:loading` swaps the icon image for an `AdwSpinnerPaintable`
147
+ // (`update_icons`, adw-tab.c:172-190) — the SAME paintable `Adw.Spinner` uses.
148
+ // So the tab mounts a real `<adw-spinner>` in the icon's slot and this block
149
+ // only places it. It used to restate `border: 2px`,
150
+ // `rgba(127, 127, 127, 0.25)` and `0.8s` under a comment claiming the geometry
151
+ // and keyframes were adw-spinner's and "not a second copy of them" — which is
152
+ // how it inherited every spinner defect independently.
153
+ .adw-tab-spinner {
154
+ flex: none;
155
+
156
+ &[hidden] {
157
+ display: none;
159
158
  }
160
159
  }
161
160
 
@@ -23,6 +23,15 @@ adw-wrap-box {
23
23
  align-content: flex-start;
24
24
  }
25
25
 
26
+ // `wrap-policy`, published by the component as a custom property. The FALLBACK
27
+ // is the load-bearing part: it is `ADW_WRAP_NATURAL`, libadwaita's default
28
+ // (adw-wrap-box.c:491-495), where CSS defaults `flex-shrink` to 1 — so a wrap
29
+ // box that has not upgraded yet, or one whose policy is unset, draws Adwaita's
30
+ // choice rather than the cascade's.
31
+ adw-wrap-box > * {
32
+ flex-shrink: var(--adw-wrap-box-child-shrink, 0);
33
+ }
34
+
26
35
  // FILL grows the children and keeps the spacing constant
27
36
  // (adw-wrap-layout.c:317-319, :326-341); SPREAD deliberately does NOT, so the
28
37
  // gaps take the leftover instead. `data-line-justify` is the mode the component
@@ -9,6 +9,8 @@
9
9
  // them. This suite is that comparison.
10
10
  import { describe, expect, it } from '@gjsify/unit';
11
11
 
12
+ import { AVATAR_MODE_VECTORS } from '@gjsify/adwaita-core/conformance';
13
+
12
14
  import { AVATAR_COLORS, avatarMaxFontSize } from '@gjsify/adwaita-core';
13
15
  import {
14
16
  AVATAR_COLOR_VECTORS,
@@ -136,4 +138,39 @@ export const AdwAvatarTest = async () => {
136
138
  host.remove();
137
139
  });
138
140
  });
141
+
142
+ await describe('<adw-avatar> mode (update_visibility, adw-avatar.c:117)', async () => {
143
+ // The `hasCustomImage` rows are not reachable through this element: it
144
+ // passes `hasCustomImage: false` unconditionally, because it has no
145
+ // custom-image support at all — `Adw.Avatar:custom-image` is unported.
146
+ // Named below rather than silently filtered.
147
+ for (const { hasCustomImage, showInitials, text, mode, rule } of AVATAR_MODE_VECTORS) {
148
+ if (hasCustomImage) continue;
149
+ await it(`initials=${showInitials} "${text}" → ${mode} — ${rule}`, () => {
150
+ const host = document.createElement('div');
151
+ document.body.appendChild(host);
152
+ const avatar = document.createElement('adw-avatar');
153
+ if (showInitials) avatar.setAttribute('show-initials', '');
154
+ if (text) avatar.setAttribute('text', text);
155
+ host.appendChild(avatar);
156
+
157
+ // Exactly ONE of the two reachable modes is drawn — the point of
158
+ // the table is the precedence, and "both visible at once" is the
159
+ // failure it guards against.
160
+ const initials = (avatar.querySelector('.adw-avatar-text') as HTMLElement).hidden === false;
161
+ const icon = (avatar.querySelector('.adw-avatar-icon') as HTMLElement).hidden === false;
162
+ expect(initials).toBe(mode === 'initials');
163
+ expect(icon).toBe(mode === 'icon');
164
+ host.remove();
165
+ });
166
+ }
167
+
168
+ await it('leaves out only the custom-image rows, which this element cannot reach', () => {
169
+ const unreachable = AVATAR_MODE_VECTORS.filter((vector) => vector.hasCustomImage);
170
+ expect(unreachable.length).toBeGreaterThan(0);
171
+ // Every one of them is an `image` outcome, i.e. the whole mode is
172
+ // missing rather than a corner of it.
173
+ for (const vector of unreachable) expect(vector.mode).toBe('image');
174
+ });
175
+ });
139
176
  };
@@ -13,6 +13,8 @@
13
13
  // `image-missing` (:355-356).
14
14
  import { describe, expect, it } from '@gjsify/unit';
15
15
 
16
+ import { BUTTON_CONTENT_DEFAULT_VECTORS } from '@gjsify/adwaita-core/conformance';
17
+
16
18
  import { BUTTON_CONTENT_STYLE_CLASS, normalizeIconName } from '@gjsify/adwaita-core';
17
19
  import {
18
20
  BUTTON_CONTENT_ELLIPSIZE_VECTORS,
@@ -180,4 +182,26 @@ export const AdwButtonContentTest = async () => {
180
182
  });
181
183
  }
182
184
  });
185
+
186
+ await describe('<adw-button-content> the four GParamSpec defaults', async () => {
187
+ for (const { property, value, rule } of BUTTON_CONTENT_DEFAULT_VECTORS) {
188
+ await it(`${property} defaults to ${JSON.stringify(value)} — ${rule}`, () => {
189
+ const host = document.createElement('div');
190
+ document.body.appendChild(host);
191
+ const content = document.createElement('adw-button-content');
192
+ host.appendChild(content);
193
+
194
+ // A bare element must READ BACK the C's defaults. The table was
195
+ // driven by nobody, so an element that invented its own defaults
196
+ // — `use-underline` true, say, which is the banner button's — would
197
+ // have looked correct.
198
+ const actual =
199
+ typeof value === 'boolean'
200
+ ? content.hasAttribute(property)
201
+ : (content.getAttribute(property) ?? '');
202
+ expect(actual).toStrictEqual(value);
203
+ host.remove();
204
+ });
205
+ }
206
+ });
183
207
  };
@@ -9,6 +9,8 @@
9
9
  // display silently.
10
10
  import { describe, expect, it } from '@gjsify/unit';
11
11
 
12
+ import { COMBO_CHOOSER_VECTORS } from '@gjsify/adwaita-core/conformance';
13
+
12
14
  import type { AdwComboRow } from './elements/adw-combo-row.js';
13
15
  import type { AdwExpanderRow } from './elements/adw-expander-row.js';
14
16
  import type { AdwSpinRow } from './elements/adw-spin-row.js';
@@ -72,6 +74,53 @@ export const AdwRowStateTest = async () => {
72
74
  expect((row.querySelector('.adw-row-value') as HTMLSpanElement).textContent).toBe('');
73
75
  host.remove();
74
76
  });
77
+
78
+ for (const { count, presentsChooser, rule } of COMBO_CHOOSER_VECTORS) {
79
+ await it(`${count} option(s) → ${presentsChooser ? 'a chooser' : 'not a chooser'}: ${rule}`, async () => {
80
+ const items = JSON.stringify(Array.from({ length: count }, (_, i) => `Item ${i}`));
81
+ const { el: row, host } = parse<AdwComboRow>(
82
+ `<adw-combo-row title="Pick" items='${items}'></adw-combo-row>`,
83
+ 'adw-combo-row',
84
+ );
85
+ const value = row.querySelector('.adw-row-value') as HTMLSpanElement;
86
+ const select = row.querySelector('select') as HTMLSelectElement;
87
+
88
+ // The ARROW: `.adw-row-value::after` is masked in, and the
89
+ // `[data-presents-chooser='false']` block removes it. Asserted
90
+ // as "not none" rather than against the authored `inline-block`,
91
+ // because `.adw-row-value` is a flex container and blockifies
92
+ // its children's display.
93
+ expect(getComputedStyle(value, '::after').display === 'none').toBe(!presentsChooser);
94
+ // ...and the ROW being activatable, which on this port is the
95
+ // overlaid <select> taking clicks at all. Hiding the arrow
96
+ // without this leaves a row that still opens a one-entry popup.
97
+ expect(select.disabled).toBe(!presentsChooser);
98
+
99
+ host.remove();
100
+ });
101
+ }
102
+
103
+ await it('gains its arrow back when the model grows past one item', async () => {
104
+ const { el: row, host } = parse<AdwComboRow>(
105
+ `<adw-combo-row title="Pick" items='["Only"]'></adw-combo-row>`,
106
+ 'adw-combo-row',
107
+ );
108
+ const select = row.querySelector('select') as HTMLSelectElement;
109
+ expect(select.disabled).toBe(true);
110
+
111
+ // `items` is read at connect only, so the model is replaced through
112
+ // the state the element composes — the same path a consumer has.
113
+ (
114
+ row as unknown as { _state: { setOptions(o: { label: string; value: string }[]): void } }
115
+ )._state.setOptions([
116
+ { label: 'One', value: 'a' },
117
+ { label: 'Two', value: 'b' },
118
+ ]);
119
+ expect(select.disabled).toBe(false);
120
+ expect(row.dataset.presentsChooser).toBe('true');
121
+
122
+ host.remove();
123
+ });
75
124
  });
76
125
 
77
126
  await describe('adw-spin-row (SpinState)', async () => {