@gjsify/adwaita-web 0.31.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 (183) hide show
  1. package/dist/adwaita-web.css +835 -299
  2. package/dist/adwaita-web.css.map +1 -1
  3. package/lib/types/adw-about-dialog.spec.d.ts +1 -0
  4. package/lib/types/adw-action-rows.spec.d.ts +1 -0
  5. package/lib/types/adw-avatar.spec.d.ts +1 -0
  6. package/lib/types/adw-banner.spec.d.ts +1 -0
  7. package/lib/types/adw-button-content.spec.d.ts +1 -0
  8. package/lib/types/adw-carousel.spec.d.ts +1 -0
  9. package/lib/types/adw-checks.spec.d.ts +1 -0
  10. package/lib/types/adw-header-bar.spec.d.ts +1 -0
  11. package/lib/types/adw-icon.spec.d.ts +1 -0
  12. package/lib/types/adw-progress-bar.spec.d.ts +1 -0
  13. package/lib/types/adw-sidebar.spec.d.ts +1 -0
  14. package/lib/types/adw-switch.spec.d.ts +1 -0
  15. package/lib/types/adw-wrap-box.spec.d.ts +1 -0
  16. package/lib/types/bottom-sheet.spec.d.ts +1 -0
  17. package/lib/types/breakpoints.d.ts +22 -0
  18. package/lib/types/breakpoints.spec.d.ts +1 -0
  19. package/lib/types/chrome.spec.d.ts +1 -0
  20. package/lib/types/elements/adw-about-dialog.d.ts +47 -8
  21. package/lib/types/elements/adw-action-row.d.ts +35 -0
  22. package/lib/types/elements/adw-avatar.d.ts +11 -0
  23. package/lib/types/elements/adw-bottom-sheet.d.ts +33 -4
  24. package/lib/types/elements/adw-button-content.d.ts +12 -0
  25. package/lib/types/elements/adw-button-row.d.ts +10 -1
  26. package/lib/types/elements/adw-carousel.d.ts +111 -8
  27. package/lib/types/elements/adw-checks.d.ts +57 -0
  28. package/lib/types/elements/adw-clamp.d.ts +4 -1
  29. package/lib/types/elements/adw-combo-row.d.ts +10 -0
  30. package/lib/types/elements/adw-data-grid.d.ts +11 -31
  31. package/lib/types/elements/adw-drop-down.d.ts +17 -20
  32. package/lib/types/elements/adw-entry-row.d.ts +93 -3
  33. package/lib/types/elements/adw-expander-row.d.ts +7 -1
  34. package/lib/types/elements/adw-header-bar.d.ts +15 -0
  35. package/lib/types/elements/adw-icon.d.ts +25 -0
  36. package/lib/types/elements/adw-inline-view-switcher.d.ts +73 -11
  37. package/lib/types/elements/adw-menu-button.d.ts +6 -6
  38. package/lib/types/elements/adw-navigation-split-view.d.ts +48 -0
  39. package/lib/types/elements/adw-navigation-view.d.ts +63 -20
  40. package/lib/types/elements/adw-overlay-split-view.d.ts +94 -0
  41. package/lib/types/elements/adw-password-entry-row.d.ts +19 -10
  42. package/lib/types/elements/adw-popover.d.ts +58 -0
  43. package/lib/types/elements/adw-preferences-dialog.d.ts +57 -2
  44. package/lib/types/elements/adw-preferences-group.d.ts +31 -0
  45. package/lib/types/elements/adw-progress-bar.d.ts +38 -0
  46. package/lib/types/elements/adw-sidebar.d.ts +37 -5
  47. package/lib/types/elements/adw-spinner.d.ts +23 -0
  48. package/lib/types/elements/adw-split-button.d.ts +53 -7
  49. package/lib/types/elements/adw-switch-row.d.ts +11 -2
  50. package/lib/types/elements/adw-switch.d.ts +15 -0
  51. package/lib/types/elements/adw-tab-view.d.ts +129 -12
  52. package/lib/types/elements/adw-toolbar-view.d.ts +5 -0
  53. package/lib/types/elements/adw-view-stack.d.ts +39 -19
  54. package/lib/types/elements/adw-view-switcher-bar.d.ts +25 -7
  55. package/lib/types/elements/adw-view-switcher.d.ts +76 -13
  56. package/lib/types/elements/adw-window-title.d.ts +3 -1
  57. package/lib/types/elements/adw-wrap-box.d.ts +58 -3
  58. package/lib/types/elements/view-switcher-dom.d.ts +44 -0
  59. package/lib/types/entry-rows.spec.d.ts +1 -0
  60. package/lib/types/index.d.ts +7 -0
  61. package/lib/types/navigation-view.spec.d.ts +1 -0
  62. package/lib/types/preferences-search.d.ts +20 -0
  63. package/lib/types/preferences.spec.d.ts +1 -0
  64. package/lib/types/split-button.spec.d.ts +1 -0
  65. package/lib/types/split-views.spec.d.ts +1 -0
  66. package/lib/types/styles.generated.d.ts +1 -1
  67. package/lib/types/tab-view.spec.d.ts +1 -0
  68. package/lib/types/view-stack.spec.d.ts +1 -0
  69. package/lib/types/view-switcher.spec.d.ts +1 -0
  70. package/package.json +6 -6
  71. package/scss/_banner.scss +13 -0
  72. package/scss/_bottom_sheet.scss +5 -6
  73. package/scss/_button.scss +3 -26
  74. package/scss/_button_content.scss +28 -0
  75. package/scss/_checks.scss +228 -0
  76. package/scss/_clamp.scss +9 -3
  77. package/scss/_combo_row.scss +19 -0
  78. package/scss/_data_grid.scss +15 -7
  79. package/scss/_drop_down.scss +12 -55
  80. package/scss/_expander_row.scss +5 -50
  81. package/scss/_icon.scss +51 -0
  82. package/scss/_icons.generated.scss +6 -0
  83. package/scss/_inline_view_switcher.scss +34 -0
  84. package/scss/_labels.scss +135 -0
  85. package/scss/_menu_button.scss +10 -70
  86. package/scss/_navigation_split_view.scss +47 -8
  87. package/scss/_overlay_split_view.scss +117 -32
  88. package/scss/_popover.scss +186 -0
  89. package/scss/_preferences.scss +29 -0
  90. package/scss/_preferences_dialog.scss +17 -0
  91. package/scss/_progress_bar.scss +161 -0
  92. package/scss/_reset.scss +18 -7
  93. package/scss/_spinner.scss +37 -19
  94. package/scss/_split_button.scss +6 -50
  95. package/scss/_switch.scss +90 -0
  96. package/scss/_switch_row.scss +6 -52
  97. package/scss/_tab_view.scss +63 -0
  98. package/scss/_theme.scss +10 -0
  99. package/scss/_toolbar_view.scss +74 -0
  100. package/scss/_variables.scss +34 -0
  101. package/scss/_view_switcher.scss +40 -0
  102. package/scss/_view_switcher_bar.scss +37 -0
  103. package/scss/_wrap_box.scss +65 -6
  104. package/scss/adwaita-skin.scss +23 -0
  105. package/src/adw-about-dialog.spec.ts +350 -0
  106. package/src/adw-action-rows.spec.ts +342 -0
  107. package/src/adw-avatar.spec.ts +176 -0
  108. package/src/adw-banner.spec.ts +152 -0
  109. package/src/adw-button-content.spec.ts +207 -0
  110. package/src/adw-button-row.spec.ts +12 -6
  111. package/src/adw-carousel.spec.ts +389 -0
  112. package/src/adw-checks.spec.ts +368 -0
  113. package/src/adw-drop-down.spec.ts +123 -0
  114. package/src/adw-header-bar.spec.ts +83 -0
  115. package/src/adw-icon.spec.ts +167 -0
  116. package/src/adw-progress-bar.spec.ts +173 -0
  117. package/src/adw-row-state.spec.ts +49 -0
  118. package/src/adw-sidebar.spec.ts +332 -0
  119. package/src/adw-switch.spec.ts +242 -0
  120. package/src/adw-tab-view.spec.ts +12 -3
  121. package/src/adw-view-switcher-bar.spec.ts +61 -13
  122. package/src/adw-wrap-box.spec.ts +341 -0
  123. package/src/bottom-sheet.spec.ts +325 -0
  124. package/src/breakpoints.spec.ts +147 -0
  125. package/src/breakpoints.ts +67 -0
  126. package/src/chrome.spec.ts +400 -0
  127. package/src/elements/adw-about-dialog.ts +223 -163
  128. package/src/elements/adw-action-row.ts +131 -11
  129. package/src/elements/adw-avatar.ts +48 -54
  130. package/src/elements/adw-banner.ts +46 -10
  131. package/src/elements/adw-bottom-sheet.ts +145 -60
  132. package/src/elements/adw-button-content.ts +107 -33
  133. package/src/elements/adw-button-row.ts +61 -37
  134. package/src/elements/adw-button.ts +5 -7
  135. package/src/elements/adw-carousel.ts +467 -105
  136. package/src/elements/adw-checks.ts +299 -0
  137. package/src/elements/adw-clamp.ts +80 -10
  138. package/src/elements/adw-combo-row.ts +32 -7
  139. package/src/elements/adw-data-grid.ts +43 -91
  140. package/src/elements/adw-drop-down.ts +117 -128
  141. package/src/elements/adw-entry-row.ts +404 -36
  142. package/src/elements/adw-expander-row.ts +61 -29
  143. package/src/elements/adw-header-bar.ts +31 -6
  144. package/src/elements/adw-icon.ts +148 -0
  145. package/src/elements/adw-inline-view-switcher.ts +325 -131
  146. package/src/elements/adw-menu-button.ts +93 -70
  147. package/src/elements/adw-navigation-split-view.ts +220 -14
  148. package/src/elements/adw-navigation-view.ts +286 -148
  149. package/src/elements/adw-overlay-split-view.ts +536 -28
  150. package/src/elements/adw-password-entry-row.ts +111 -89
  151. package/src/elements/adw-popover.ts +283 -0
  152. package/src/elements/adw-preferences-dialog.ts +120 -21
  153. package/src/elements/adw-preferences-group.ts +128 -22
  154. package/src/elements/adw-progress-bar.ts +189 -0
  155. package/src/elements/adw-sidebar.ts +301 -142
  156. package/src/elements/adw-spin-row.ts +11 -5
  157. package/src/elements/adw-spinner.ts +168 -9
  158. package/src/elements/adw-split-button.ts +371 -92
  159. package/src/elements/adw-status-page.ts +9 -9
  160. package/src/elements/adw-switch-row.ts +100 -30
  161. package/src/elements/adw-switch.ts +129 -0
  162. package/src/elements/adw-tab-view.ts +700 -129
  163. package/src/elements/adw-toggle-group.ts +5 -7
  164. package/src/elements/adw-toolbar-view.ts +84 -21
  165. package/src/elements/adw-view-stack.ts +183 -85
  166. package/src/elements/adw-view-switcher-bar.ts +171 -81
  167. package/src/elements/adw-view-switcher.ts +328 -117
  168. package/src/elements/adw-window-title.ts +44 -11
  169. package/src/elements/adw-wrap-box.ts +329 -78
  170. package/src/elements/view-switcher-dom.ts +108 -0
  171. package/src/entry-rows.spec.ts +391 -0
  172. package/src/index.ts +23 -0
  173. package/src/navigation-view.spec.ts +435 -0
  174. package/src/preferences-search.ts +135 -0
  175. package/src/preferences.spec.ts +250 -0
  176. package/src/source-view/adw-source-view.ts +2 -5
  177. package/src/split-button.spec.ts +470 -0
  178. package/src/split-views.spec.ts +893 -0
  179. package/src/styles.generated.ts +1 -1
  180. package/src/tab-view.spec.ts +451 -0
  181. package/src/test.browser.mts +49 -0
  182. package/src/view-stack.spec.ts +272 -0
  183. package/src/view-switcher.spec.ts +665 -0
@@ -1,70 +1,274 @@
1
1
  // <adw-wrap-box> — A box-like container that lays its children out in a line and
2
- // wraps them onto new lines when they run out of room (the web analog of
3
- // flexbox `flex-wrap: wrap` with a gap). Mirrors Adw.WrapBox.
4
- // Attributes:
5
- // child-spacing (px, default 0) — spacing between children on the same line
6
- // → CSS column-gap
7
- // line-spacing (px, default 0) — spacing between lines → CSS row-gap
8
- // orientation ("horizontal" | "vertical", default "horizontal") — the main
9
- // axis children are packed along
10
- // align (0..1, default 0) — alignment of children within each line
11
- // (cross axis); 0 = start, 0.5 = center, 1 = end
12
- // justify ("none" | "fill" | "spread", default "none") — whether/how
13
- // each line is stretched to fill the widget
14
- // justify-last-line (boolean) — also justify the last (incomplete) line
15
- // line-homogeneous (boolean) — make every line take the same amount of space
16
- // pack-direction ("start-to-end" | "end-to-start", default "start-to-end") —
17
- // direction children are packed within a line
18
- // wrap-reverse (boolean) — reverse the wrap direction (lines wrap upwards)
19
- // Events: notify::child-spacing, notify::line-spacing (CustomEvent, bubbles —
20
- // mirrors GObject signal naming).
2
+ // wraps them onto new lines when they run out of room (the web analog of flexbox
3
+ // `flex-wrap: wrap` with a gap). Mirrors Adw.WrapBox.
4
+ //
5
+ // Attributes — all fourteen of the widget's properties, each with a
6
+ // `notify::<property>` (adw-wrap-box.c:284-497). The element used to carry nine
7
+ // attributes and notify for two, so a consumer binding to any of the other
8
+ // twelve was listening to a signal that could not fire.
9
+ //
10
+ // child-spacing / line-spacing (int >= 0, default 0) -> column-gap / row-gap
11
+ // child-spacing-unit / line-spacing-unit ("px" | "pt" | "sp", default "px")
12
+ // orientation ("horizontal" | "vertical", default "horizontal")
13
+ // pack-direction ("start-to-end" | "end-to-start", default "start-to-end")
14
+ // align (0..1, default 0) — MAIN-axis offset of the whole line block
15
+ // justify ("none" | "fill" | "spread", default "none")
16
+ // justify-last-line (boolean)
17
+ // line-homogeneous (boolean)
18
+ // wrap-reverse (boolean)
19
+ // wrap-policy ("minimum" | "natural", default "natural")
20
+ // natural-line-length (int >= -1, default -1 = unset) + -unit
21
+ //
22
+ // The property CONTRACT — the normalisers and the justify/align/last-line
23
+ // decision — is `@gjsify/adwaita-core`'s `wrap-box.ts`, held to
24
+ // `WRAP_BOX_LINE_VECTORS` and friends, which the NativeScript suite drives too.
25
+ // This element used to carry its own copy of all of it.
26
+ //
21
27
  // Reference: refs/libadwaita/src/adw-wrap-box.c / adw-wrap-layout.c (Adw.WrapBox)
22
28
  // Reference: refs/adwaita-web/adwaita-web/scss/_wrap_box.scss
23
29
  // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
24
30
  // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
25
31
 
26
- /** Adw.JustifyMode → flexbox justify-content. */
27
- const JUSTIFY_MAP: Record<string, string> = {
28
- none: 'flex-start',
29
- fill: 'space-between',
30
- spread: 'space-between',
31
- };
32
+ import {
33
+ ADW_WRAP_BOX_NATURAL_LINE_LENGTH_UNSET,
34
+ normalizeNaturalLineLength,
35
+ normalizeWrapBoxAlign,
36
+ normalizeWrapBoxJustify,
37
+ normalizeWrapBoxLengthUnit,
38
+ normalizeWrapBoxPackDirection,
39
+ normalizeWrapBoxSpacing,
40
+ normalizeWrapPolicy,
41
+ resolveWrapBoxChildOrder,
42
+ resolveWrapBoxLine,
43
+ wrapBoxLengthToPx,
44
+ wrapPolicyFlexShrink,
45
+ type AdwLengthUnit,
46
+ } from '@gjsify/adwaita-core';
47
+
48
+ /**
49
+ * The attributes that carry a property, in the order the C installs them.
50
+ *
51
+ * Doubles as the `notify::` roster: every entry gets an event on a real change,
52
+ * so adding an attribute without its notification is not expressible here.
53
+ */
54
+ const PROPERTY_ATTRIBUTES = [
55
+ 'child-spacing',
56
+ 'child-spacing-unit',
57
+ 'pack-direction',
58
+ 'align',
59
+ 'justify',
60
+ 'justify-last-line',
61
+ 'line-spacing',
62
+ 'line-spacing-unit',
63
+ 'line-homogeneous',
64
+ 'natural-line-length',
65
+ 'natural-line-length-unit',
66
+ 'wrap-reverse',
67
+ 'wrap-policy',
68
+ 'orientation',
69
+ ] as const;
70
+
71
+ /** One attribute's normalised value, for the "did it really change" comparison. */
72
+ type PropertyValue = string | number | boolean;
73
+
74
+ /**
75
+ * `align` as a `justify-content` keyword.
76
+ *
77
+ * C offsets the whole line block by `roundf (length_delta * align)` — a
78
+ * continuum. Flexbox has three main-axis positions, so the nearest one is taken;
79
+ * that is the renderer's approximation, not libadwaita's rule, which is why it
80
+ * lives here and not in the conformance table.
81
+ */
82
+ function alignToJustifyContent(align: number): string {
83
+ if (align < 0.25) return 'flex-start';
84
+ if (align < 0.75) return 'center';
85
+ return 'flex-end';
86
+ }
32
87
 
33
88
  export class AdwWrapBox extends HTMLElement {
34
89
  private _initialized = false;
35
90
 
36
91
  static get observedAttributes() {
37
- return [
38
- 'child-spacing',
39
- 'line-spacing',
40
- 'orientation',
41
- 'align',
42
- 'justify',
43
- 'justify-last-line',
44
- 'line-homogeneous',
45
- 'pack-direction',
46
- 'wrap-reverse',
47
- ];
48
- }
49
-
50
- /** Spacing between children on the same line, in px (CSS column-gap). */
92
+ return [...PROPERTY_ATTRIBUTES];
93
+ }
94
+
95
+ // --- properties ---
96
+
97
+ /** Spacing between children on the same line, in `child-spacing-unit`. */
51
98
  get childSpacing(): number {
52
- return parseFloat(this.getAttribute('child-spacing') || '0');
99
+ return normalizeWrapBoxSpacing(this.getAttribute('child-spacing'));
53
100
  }
54
101
 
55
102
  set childSpacing(value: number) {
56
103
  this.setAttribute('child-spacing', String(value));
57
104
  }
58
105
 
59
- /** Spacing between lines, in px (CSS row-gap). */
106
+ /** The unit `child-spacing` is written in. Defaults to `px`, as in C. */
107
+ get childSpacingUnit(): AdwLengthUnit {
108
+ return normalizeWrapBoxLengthUnit(this.getAttribute('child-spacing-unit'));
109
+ }
110
+
111
+ set childSpacingUnit(value: AdwLengthUnit) {
112
+ this.setAttribute('child-spacing-unit', value);
113
+ }
114
+
115
+ /** Spacing between lines, in `line-spacing-unit`. */
60
116
  get lineSpacing(): number {
61
- return parseFloat(this.getAttribute('line-spacing') || '0');
117
+ return normalizeWrapBoxSpacing(this.getAttribute('line-spacing'));
62
118
  }
63
119
 
64
120
  set lineSpacing(value: number) {
65
121
  this.setAttribute('line-spacing', String(value));
66
122
  }
67
123
 
124
+ /** The unit `line-spacing` is written in. Defaults to `px`, as in C. */
125
+ get lineSpacingUnit(): AdwLengthUnit {
126
+ return normalizeWrapBoxLengthUnit(this.getAttribute('line-spacing-unit'));
127
+ }
128
+
129
+ set lineSpacingUnit(value: AdwLengthUnit) {
130
+ this.setAttribute('line-spacing-unit', value);
131
+ }
132
+
133
+ /** Where the children sit ALONG the line (main axis): 0 start, 0.5 middle, 1 end. */
134
+ get align(): number {
135
+ return normalizeWrapBoxAlign(this.getAttribute('align'));
136
+ }
137
+
138
+ set align(value: number) {
139
+ this.setAttribute('align', String(value));
140
+ }
141
+
142
+ /** Whether and how each line is stretched to fill the widget. */
143
+ get justify(): 'none' | 'fill' | 'spread' {
144
+ return normalizeWrapBoxJustify(this.getAttribute('justify'));
145
+ }
146
+
147
+ set justify(value: 'none' | 'fill' | 'spread') {
148
+ this.setAttribute('justify', value);
149
+ }
150
+
151
+ /** Whether the FINAL line is justified too. Default false, as in C. */
152
+ get justifyLastLine(): boolean {
153
+ return this.hasAttribute('justify-last-line');
154
+ }
155
+
156
+ set justifyLastLine(value: boolean) {
157
+ this.toggleAttribute('justify-last-line', !!value);
158
+ }
159
+
160
+ /** Whether an overflowing line squeezes its children or lets them spill. */
161
+ get wrapPolicy(): 'minimum' | 'natural' {
162
+ return normalizeWrapPolicy(this.getAttribute('wrap-policy'));
163
+ }
164
+
165
+ set wrapPolicy(value: 'minimum' | 'natural') {
166
+ this.setAttribute('wrap-policy', value);
167
+ }
168
+
169
+ /** The direction children are packed in each line. */
170
+ get packDirection(): 'start-to-end' | 'end-to-start' {
171
+ return normalizeWrapBoxPackDirection(this.getAttribute('pack-direction'));
172
+ }
173
+
174
+ set packDirection(value: 'start-to-end' | 'end-to-start') {
175
+ this.setAttribute('pack-direction', value);
176
+ }
177
+
178
+ /** Whether lines wrap upwards (horizontal) / towards the start (vertical). */
179
+ get wrapReverse(): boolean {
180
+ return this.hasAttribute('wrap-reverse');
181
+ }
182
+
183
+ set wrapReverse(value: boolean) {
184
+ this.toggleAttribute('wrap-reverse', !!value);
185
+ }
186
+
187
+ /** Whether every line takes the same amount of space. */
188
+ get lineHomogeneous(): boolean {
189
+ return this.hasAttribute('line-homogeneous');
190
+ }
191
+
192
+ set lineHomogeneous(value: boolean) {
193
+ this.toggleAttribute('line-homogeneous', !!value);
194
+ }
195
+
196
+ /** The natural line length in `natural-line-length-unit`, or `-1` when unset. */
197
+ get naturalLineLength(): number {
198
+ return normalizeNaturalLineLength(this.getAttribute('natural-line-length'));
199
+ }
200
+
201
+ set naturalLineLength(value: number) {
202
+ this.setAttribute('natural-line-length', String(value));
203
+ }
204
+
205
+ /** The unit `natural-line-length` is written in. Defaults to `px`, as in C. */
206
+ get naturalLineLengthUnit(): AdwLengthUnit {
207
+ return normalizeWrapBoxLengthUnit(this.getAttribute('natural-line-length-unit'));
208
+ }
209
+
210
+ set naturalLineLengthUnit(value: AdwLengthUnit) {
211
+ this.setAttribute('natural-line-length-unit', value);
212
+ }
213
+
214
+ /** The axis children are packed along. */
215
+ get orientation(): 'horizontal' | 'vertical' {
216
+ return this.getAttribute('orientation') === 'vertical' ? 'vertical' : 'horizontal';
217
+ }
218
+
219
+ set orientation(value: 'horizontal' | 'vertical') {
220
+ this.setAttribute('orientation', value);
221
+ }
222
+
223
+ // --- child list (adw_wrap_box_insert_child_after / reorder / remove_all) ---
224
+ //
225
+ // `adw_wrap_box_append`, `_prepend` and `_remove` are deliberately NOT
226
+ // redeclared: `ParentNode.append` / `.prepend` and `Node.removeChild` are
227
+ // those three, with the same semantics, and shadowing them would give this
228
+ // element two spellings of one operation. Only the three the DOM has no
229
+ // counterpart for are added.
230
+
231
+ /** The element children, which are what the layout actually lays out. */
232
+ private get _children(): Element[] {
233
+ return [...this.children];
234
+ }
235
+
236
+ /**
237
+ * `adw_wrap_box_insert_child_after` (adw-wrap-box.c:1283-1300).
238
+ *
239
+ * A NULL/absent `sibling` inserts at the FIRST position, not the last —
240
+ * `gtk_widget_insert_after`'s documented rule, pinned by
241
+ * `WRAP_BOX_CHILD_ORDER_VECTORS`. Returns whether the insert happened; a
242
+ * refusal is where C would have hit a `g_return_if_fail`.
243
+ */
244
+ insertChildAfter(child: Element, sibling: Element | null = null): boolean {
245
+ return this._applyOrder('insert-after', child, sibling);
246
+ }
247
+
248
+ /** `adw_wrap_box_reorder_child_after` (adw-wrap-box.c:1315-1332). Same NULL rule. */
249
+ reorderChildAfter(child: Element, sibling: Element | null = null): boolean {
250
+ return this._applyOrder('reorder-after', child, sibling);
251
+ }
252
+
253
+ /** `adw_wrap_box_remove_all` (adw-wrap-box.c:1406-1414). */
254
+ removeAll(): void {
255
+ for (const child of this._children) this.removeChild(child);
256
+ }
257
+
258
+ private _applyOrder(op: 'insert-after' | 'reorder-after', child: Element, sibling: Element | null): boolean {
259
+ const next = resolveWrapBoxChildOrder({ children: this._children, child, sibling, op });
260
+ if (next === null) return false;
261
+ // Move exactly the one node: `insertBefore` relocates a node already in
262
+ // the tree, and a null reference node appends. Re-appending the whole
263
+ // resolved list would reach the same order while detaching and
264
+ // re-attaching every sibling, which resets focus and any element state.
265
+ const at = next.indexOf(child);
266
+ this.insertBefore(child, next[at + 1] ?? null);
267
+ return true;
268
+ }
269
+
270
+ // --- lifecycle ---
271
+
68
272
  connectedCallback() {
69
273
  if (this._initialized) return;
70
274
  this._initialized = true;
@@ -74,41 +278,58 @@ export class AdwWrapBox extends HTMLElement {
74
278
  attributeChangedCallback(name: string, old: string | null, value: string | null) {
75
279
  if (!this._initialized) return;
76
280
  this._sync();
77
- if (old === value) return;
78
- if (name === 'child-spacing') {
79
- this.dispatchEvent(
80
- new CustomEvent('notify::child-spacing', {
81
- bubbles: true,
82
- detail: { childSpacing: this.childSpacing },
83
- }),
84
- );
85
- } else if (name === 'line-spacing') {
86
- this.dispatchEvent(
87
- new CustomEvent('notify::line-spacing', {
88
- bubbles: true,
89
- detail: { lineSpacing: this.lineSpacing },
90
- }),
91
- );
281
+ // C compares AFTER normalising and returns early when nothing changed
282
+ // (adw-wrap-box.c:592-593), so `child-spacing="-5"` on a box already at 0
283
+ // notifies nobody. The same gate applies to every property, not just the
284
+ // two spacings this element used to special-case.
285
+ const next = this._normalized(name, value);
286
+ if (next === this._normalized(name, old)) return;
287
+ this.dispatchEvent(new CustomEvent(`notify::${name}`, { bubbles: true, detail: { [name]: next } }));
288
+ }
289
+
290
+ /** One attribute's value as the widget stores it — the notify comparison key. */
291
+ private _normalized(name: string, raw: string | null): PropertyValue {
292
+ switch (name) {
293
+ case 'child-spacing':
294
+ case 'line-spacing':
295
+ return normalizeWrapBoxSpacing(raw);
296
+ case 'child-spacing-unit':
297
+ case 'line-spacing-unit':
298
+ case 'natural-line-length-unit':
299
+ return normalizeWrapBoxLengthUnit(raw);
300
+ case 'align':
301
+ return normalizeWrapBoxAlign(raw);
302
+ case 'justify':
303
+ return normalizeWrapBoxJustify(raw);
304
+ case 'wrap-policy':
305
+ return normalizeWrapPolicy(raw);
306
+ case 'pack-direction':
307
+ return normalizeWrapBoxPackDirection(raw);
308
+ case 'natural-line-length':
309
+ return normalizeNaturalLineLength(raw);
310
+ case 'orientation':
311
+ return raw === 'vertical' ? 'vertical' : 'horizontal';
312
+ default:
313
+ // The three booleans: a present attribute is true whatever it says.
314
+ return raw !== null;
92
315
  }
93
316
  }
94
317
 
95
318
  private _sync() {
96
319
  const style = this.style;
97
- const vertical = this.getAttribute('orientation') === 'vertical';
98
- const reverseWrap = this.hasAttribute('wrap-reverse');
99
- const packEndToStart = this.getAttribute('pack-direction') === 'end-to-start';
320
+ const vertical = this.orientation === 'vertical';
100
321
 
101
322
  // Main axis is the packing direction; lines wrap along the cross axis.
102
323
  const direction = vertical ? 'column' : 'row';
103
- const reversed = packEndToStart ? `${direction}-reverse` : direction;
104
- style.flexDirection = reversed;
105
- style.flexWrap = reverseWrap ? 'wrap-reverse' : 'wrap';
324
+ style.flexDirection = this.packDirection === 'end-to-start' ? `${direction}-reverse` : direction;
325
+ style.flexWrap = this.wrapReverse ? 'wrap-reverse' : 'wrap';
106
326
 
107
327
  // child-spacing is along the line (main axis), line-spacing is between
108
328
  // lines (cross axis). For a horizontal box that maps to column-gap /
109
- // row-gap respectively; the mapping flips for a vertical box.
110
- const childGap = `${this.childSpacing}px`;
111
- const lineGap = `${this.lineSpacing}px`;
329
+ // row-gap respectively; the mapping flips for a vertical box. Both are
330
+ // resolved through their OWN unit first, which is why they can disagree.
331
+ const childGap = `${wrapBoxLengthToPx(this.childSpacing, this.childSpacingUnit)}px`;
332
+ const lineGap = `${wrapBoxLengthToPx(this.lineSpacing, this.lineSpacingUnit)}px`;
112
333
  if (vertical) {
113
334
  style.rowGap = childGap;
114
335
  style.columnGap = lineGap;
@@ -116,23 +337,53 @@ export class AdwWrapBox extends HTMLElement {
116
337
  style.columnGap = childGap;
117
338
  style.rowGap = lineGap;
118
339
  }
340
+ // Read back by the last-line filler, which has to cancel exactly one
341
+ // main-axis gap so it can never push a full line into a new one.
342
+ style.setProperty('--adw-wrap-box-child-spacing', childGap);
343
+
344
+ const justify = this.justify;
345
+ const justifyLastLine = this.justifyLastLine;
346
+ const align = this.align;
119
347
 
120
- // justify-content stretches each complete line along the main axis; the
121
- // last (incomplete) line is only stretched when justify-last-line is set.
122
- const justify = this.getAttribute('justify') ?? 'none';
123
- style.justifyContent = JUSTIFY_MAP[justify] ?? 'flex-start';
348
+ // A flex container has ONE `justify-content` and applies it to every
349
+ // line, so it carries the COMPLETE-line rule; the final-line rule is
350
+ // published beside it for the stylesheet, which reaches that line through
351
+ // the only two selectors CSS offers — `:only-child` (a box with one child
352
+ // has one line, and it is the last) and the generated `::after` filler.
353
+ // The pair is deliberately never collapsed into one attribute: flexbox
354
+ // cannot express "every line but the last", and pretending otherwise is
355
+ // what made `justify-last-line` a no-op in the first place.
356
+ const line = resolveWrapBoxLine({ justify, justifyLastLine, align, lastLine: false, childrenInLine: 2 });
357
+ const lastLine = resolveWrapBoxLine({ justify, justifyLastLine, align, lastLine: true, childrenInLine: 2 });
358
+ this.dataset.lineJustify = line.justify;
359
+ this.dataset.lastLineJustify = lastLine.justify;
124
360
 
125
- // align (0..1) positions children within each line on the cross axis.
126
- // It is only honoured when the line is not justified.
127
- const align = Math.max(0, Math.min(1, parseFloat(this.getAttribute('align') || '0')));
128
- let crossAlign = 'flex-start';
129
- if (align >= 0.75) crossAlign = 'flex-end';
130
- else if (align >= 0.25) crossAlign = 'center';
131
- style.alignItems = crossAlign;
361
+ // SPREAD widens the gaps; FILL grows the children (stylesheet) and leaves
362
+ // the gaps alone; NONE offsets the whole line block by `align` — along
363
+ // the MAIN axis, never the cross one.
364
+ style.justifyContent = line.growGaps ? 'space-between' : alignToJustifyContent(line.align);
132
365
 
133
366
  // line-homogeneous makes every line take the same amount of space, which
134
367
  // flexbox approximates by stretching the lines across the cross axis.
135
- style.alignContent = this.hasAttribute('line-homogeneous') ? 'stretch' : 'flex-start';
368
+ style.alignContent = this.lineHomogeneous ? 'stretch' : 'flex-start';
369
+
370
+ // `wrap-policy` decides whether an overflowing line squeezes its children
371
+ // or lets them spill. CSS defaults `flex-shrink` to 1, i.e. to
372
+ // ADW_WRAP_MINIMUM — the value libadwaita does NOT default to, so an
373
+ // unset policy has been drawing the wrong one on every wrap box. What
374
+ // flexbox cannot do either way is pack MORE children onto a line, since
375
+ // line breaking runs on hypothetical sizes; see `wrapPolicyFlexShrink`.
376
+ style.setProperty('--adw-wrap-box-child-shrink', String(wrapPolicyFlexShrink(this.wrapPolicy)));
377
+
378
+ // DELIBERATE DEVIATION: libadwaita's `natural-line-length` caps the box's
379
+ // NATURAL size request, leaving a larger allocation free to happen; CSS
380
+ // has no property that caps only the intrinsic contribution, so this is a
381
+ // max-size on the main axis and therefore caps the allocation too. It is
382
+ // the intended use (limiting line length inside a popover) either way.
383
+ const natural = wrapBoxLengthToPx(this.naturalLineLength, this.naturalLineLengthUnit);
384
+ const cap = natural === ADW_WRAP_BOX_NATURAL_LINE_LENGTH_UNSET ? '' : `${natural}px`;
385
+ style.maxWidth = vertical ? '' : cap;
386
+ style.maxHeight = vertical ? cap : '';
136
387
  }
137
388
  }
138
389
 
@@ -0,0 +1,108 @@
1
+ // The DOM half of the view-switcher family — what `@gjsify/adwaita-core` cannot
2
+ // decide because it is genuinely per-renderer.
3
+ //
4
+ // Everything that decides WHAT a switcher shows (the button-visibility rule, the
5
+ // `image-missing` fallback, the badge label + screen-reader description, the
6
+ // mnemonic-stripped label, the tooltip, the reveal gate, the two index spaces)
7
+ // lives in `@gjsify/adwaita-core` and is pinned by the conformance vectors.
8
+ // What is left, and lives here, is how a derived model becomes nodes: an icon
9
+ // NAME turned into a CSS mask class, an indicator span, and a `setTimeout`
10
+ // wrapped in the scheduler seam the core takes for the drag-hover dwell.
11
+ //
12
+ // Shared by all three switcher elements because it was three copies before:
13
+ // `<adw-view-switcher>`, `<adw-inline-view-switcher>` and
14
+ // `<adw-view-switcher-bar>` each built their own icon span and their own page
15
+ // parsing, and the copies had already drifted (a titleless page rendered `''` in
16
+ // one and `page-<index>` in another).
17
+ //
18
+ // Reference: refs/libadwaita/src/adw-view-switcher.c
19
+ // Reference: refs/libadwaita/src/adw-indicator-bin.c
20
+ // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
21
+ // Modifications: Implemented for @gjsify/adwaita-web Web Components.
22
+
23
+ import { viewSwitcherIconName } from '@gjsify/adwaita-core';
24
+ import type { AdwViewSwitcherPageInit, ViewSwitcherScheduler, ViewSwitcherTimerHandle } from '@gjsify/adwaita-core';
25
+
26
+ import { type AdwIcon, createAdwIcon } from './adw-icon.js';
27
+
28
+ /**
29
+ * The browser's timer behind the core's scheduler seam.
30
+ *
31
+ * One shared instance rather than one per element: it holds no state, and the
32
+ * seam exists so the CORE stays free of a global timer, not so each element
33
+ * invents its own.
34
+ */
35
+ export const domViewSwitcherScheduler: ViewSwitcherScheduler = {
36
+ schedule(callback: () => void, ms: number): ViewSwitcherTimerHandle {
37
+ return setTimeout(callback, ms);
38
+ },
39
+ cancel(handle: ViewSwitcherTimerHandle): void {
40
+ clearTimeout(handle as ReturnType<typeof setTimeout>);
41
+ },
42
+ };
43
+
44
+ /**
45
+ * Build a switcher button's icon node, carrying its own position class.
46
+ *
47
+ * The core resolves NULL/empty to `image-missing` (which the stylesheet carries
48
+ * a real glyph for); the `-symbolic` strip and the CSS-token guard are
49
+ * `normalizeIconName`'s, applied by `<adw-icon>` — that strip is this package's
50
+ * own mask-class convention and has no counterpart in C, where the name reaches
51
+ * `GtkImage` untouched.
52
+ */
53
+ export function createSwitcherIcon(iconName: string, extra: string): AdwIcon {
54
+ return createAdwIcon(viewSwitcherIconName(iconName), extra);
55
+ }
56
+
57
+ /** Repaint an existing switcher icon from a page's declared name. */
58
+ export function applySwitcherIcon(icon: AdwIcon, iconName: string): void {
59
+ icon.iconName = viewSwitcherIconName(iconName);
60
+ }
61
+
62
+ /**
63
+ * Read a declared page element into the core's page init.
64
+ *
65
+ * `getAttribute` returns `null` for an absent attribute, which is EXACTLY C's
66
+ * NULL — so the values are passed through unchanged rather than defaulted to
67
+ * `''`. That `?? ''` is what made the button-visibility rule unimplementable in
68
+ * the old code: once an absent title has become the empty string, "the page has
69
+ * no title" is no longer expressible.
70
+ */
71
+ export function readSwitcherPage(element: Element): AdwViewSwitcherPageInit {
72
+ const badge = Number.parseInt(element.getAttribute('badge-number') ?? '', 10);
73
+ return {
74
+ // A page with no `name` is the page named '' — the same choice
75
+ // `<adw-view-stack>` made, and for the same reason: C stores the name
76
+ // verbatim (adw-view-stack.c:1170) and an invented `page-<index>` is
77
+ // addressable by a name the author never wrote.
78
+ name: element.getAttribute('name') ?? '',
79
+ title: element.getAttribute('title'),
80
+ iconName: element.getAttribute('icon-name'),
81
+ // `hidden` is the DOM spelling of AdwViewStackPage:visible.
82
+ visible: !element.hasAttribute('hidden'),
83
+ useUnderline: element.hasAttribute('use-underline'),
84
+ badgeNumber: Number.isNaN(badge) ? 0 : badge,
85
+ needsAttention: element.hasAttribute('needs-attention'),
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Paint `AdwIndicatorBin`'s badge/dot onto a span: the badge text when there is
91
+ * one, a bare dot when only `needs-attention` is set, nothing otherwise
92
+ * (adw-indicator-bin.c:58-68 + the `.needs-attention` style class).
93
+ */
94
+ export function applyIndicator(indicator: HTMLElement, badgeLabel: string, needsAttention: boolean): void {
95
+ indicator.textContent = badgeLabel;
96
+ indicator.classList.toggle('needs-attention', needsAttention && badgeLabel.length === 0);
97
+ indicator.hidden = badgeLabel.length === 0 && !needsAttention;
98
+ }
99
+
100
+ /**
101
+ * Mirror the derived screen-reader description onto the element, REMOVING the
102
+ * attribute when there is none — `update_description_cb` resets the accessible
103
+ * property rather than setting it to `""` (adw-view-switcher-button.c:104-113).
104
+ */
105
+ export function applyDescription(element: HTMLElement, description: string): void {
106
+ if (description) element.setAttribute('aria-description', description);
107
+ else element.removeAttribute('aria-description');
108
+ }