@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
@@ -0,0 +1,299 @@
1
+ // <adw-checkbox> and <adw-radio> — the two form-control primitives, in ONE
2
+ // module, for the same reason `scss/_checks.scss` is one partial: upstream
3
+ // merges them (`refs/libadwaita/src/stylesheet/widgets/_checks.scss` has no
4
+ // `_radio.scss` beside it), and everything but the corner radius, the glyph and
5
+ // the group is shared. `refs/adwaita-web` split them into two files and the two
6
+ // promptly drifted apart.
7
+ //
8
+ // WHAT LIVES IN `@gjsify/adwaita-core`, AND WHAT DOES NOT
9
+ //
10
+ // The checkbox has no core class: it is one boolean plus a third state, which
11
+ // ADR 0004 calls out as too trivial to model. What IS core is the pair of
12
+ // pieces a second renderer would otherwise re-invent:
13
+ //
14
+ // - {@link RadioGroupState} — group exclusivity. The browser gives the inner
15
+ // `<input type="radio" name="…">` exclusivity for free, and STOPS THERE: it
16
+ // unchecks the sibling INPUT and leaves the sibling `<adw-radio>` host's
17
+ // `checked` attribute — the published state, and the selector
18
+ // `_checks.scss` paints from — stale, so a group left to the browser draws
19
+ // two selected radios. The shared state is what repaints the loser.
20
+ // - {@link resolveCheckState} — indeterminate outranks checked, which is read
21
+ // off the cascade in `_checks.scss:79-81` and is what `aria-checked` needs.
22
+ //
23
+ // WHAT IS NOT VERIFIABLE IN THIS TREE. `GtkCheckButton` is a GTK widget:
24
+ // `refs/gtk` is EMPTY here and libadwaita vendors no `adw-checkbox.c`, so its
25
+ // group semantics (`gtk_check_button_set_group`, whether a fresh group starts
26
+ // with a member active) and the exact meaning of `GtkCheckButton:inconsistent`
27
+ // are NOT verifiable and are not guessed at. Where a rule was needed the HTML
28
+ // one is used and cited as such — clearing `indeterminate` on activation is the
29
+ // HTML pre-click activation step, not a reading of GTK.
30
+ //
31
+ // NAMING. The state attribute is `checked`, not GtkCheckButton's `active`:
32
+ // these elements wrap a real `<input>`, `checked` is the spelling every author
33
+ // already writes, and it is what the stylesheet selects on. The published event
34
+ // follows the attribute (`notify::checked`), not the GObject property.
35
+ //
36
+ // Attributes (both):
37
+ // checked — boolean; reflects (and drives) the checked state.
38
+ // indeterminate — boolean; the third state, drawn as a dash. Outranks checked.
39
+ // disabled — boolean; the control does not respond and is dimmed.
40
+ // label — the text beside the indicator. When absent, the element's
41
+ // existing child nodes become the label (so a server-rendered
42
+ // `<adw-checkbox>Enable</adw-checkbox>` upgrades intact).
43
+ // Attributes (<adw-radio> only):
44
+ // name — the group. Exclusivity is scoped to it, exactly as `name`
45
+ // scopes `<input type="radio">` — including the empty case:
46
+ // HTML forms a radio button group only from a NON-EMPTY name,
47
+ // so a nameless radio checks itself and unchecks nothing.
48
+ // value — this member's value. Defaults to the label text, so a group
49
+ // written without values still has distinguishable members.
50
+ // Properties: checked / indeterminate / disabled / label (+ name / value).
51
+ // Events:
52
+ // `notify::checked` (CustomEvent, bubbles, detail = { checked }) — fires on
53
+ // every change, programmatic included, on EVERY member a radio pick moves.
54
+ //
55
+ // A11Y: the inner input carries the role (native `checkbox` / `radio`) and an
56
+ // explicit `aria-checked` derived from {@link resolveCheckState}, so the
57
+ // indeterminate state announces as `mixed` for BOTH — HTML has a native mixed
58
+ // checkbox (`input.indeterminate`) but no mixed radio.
59
+ //
60
+ // Reference: refs/libadwaita/src/stylesheet/widgets/_checks.scss
61
+ // Reference: refs/adwaita-web/adwaita-web/scss/{_checkbox,_radio}.scss
62
+ // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
63
+ // Copyright (c) 2025 csm (adwaita-web). MIT License.
64
+ // Modifications: Implemented as Web Components for @gjsify/adwaita-web.
65
+
66
+ import { RadioGroupState, resolveCheckState } from '@gjsify/adwaita-core';
67
+
68
+ /**
69
+ * The document's radio groups. ONE registry, as global as a bare
70
+ * `<input type="radio" name="x">` outside a form is — two independent groups
71
+ * therefore need two names.
72
+ */
73
+ const radioGroups = new RadioGroupState();
74
+
75
+ /** The markup + state shared by `<adw-checkbox>` and `<adw-radio>`. */
76
+ abstract class AdwCheckBase extends HTMLElement {
77
+ protected _input!: HTMLInputElement;
78
+ protected _indicator!: HTMLSpanElement;
79
+ protected _label!: HTMLSpanElement;
80
+ private _initialized = false;
81
+
82
+ /** `checkbox` or `radio` — the native control this element is built around. */
83
+ protected abstract get inputType(): 'checkbox' | 'radio';
84
+
85
+ /** What a click / Space / arrow key on the control means for this kind. */
86
+ protected abstract activate(): void;
87
+
88
+ /** Whether the control is checked. */
89
+ get checked(): boolean {
90
+ return this.hasAttribute('checked');
91
+ }
92
+
93
+ set checked(value: boolean) {
94
+ // `toggleAttribute` with a force flag is a no-op when the attribute
95
+ // already holds that state, so re-setting the current value mutates
96
+ // nothing and the notify never runs.
97
+ this.toggleAttribute('checked', !!value);
98
+ }
99
+
100
+ /** Whether the control is in the third, inconsistent state. */
101
+ get indeterminate(): boolean {
102
+ return this.hasAttribute('indeterminate');
103
+ }
104
+
105
+ set indeterminate(value: boolean) {
106
+ this.toggleAttribute('indeterminate', !!value);
107
+ }
108
+
109
+ /** Whether the control is inert. */
110
+ get disabled(): boolean {
111
+ return this.hasAttribute('disabled');
112
+ }
113
+
114
+ set disabled(value: boolean) {
115
+ this.toggleAttribute('disabled', !!value);
116
+ }
117
+
118
+ /** The label text beside the indicator. */
119
+ get label(): string {
120
+ return this.getAttribute('label') ?? this._label?.textContent ?? '';
121
+ }
122
+
123
+ set label(value: string) {
124
+ this.setAttribute('label', value);
125
+ }
126
+
127
+ connectedCallback() {
128
+ if (this._initialized) {
129
+ this.onConnected();
130
+ return;
131
+ }
132
+ this._initialized = true;
133
+
134
+ // Captured BEFORE the element takes over its own children: a
135
+ // server-rendered `<adw-checkbox>Enable</adw-checkbox>` keeps its text.
136
+ const authored = [...this.childNodes];
137
+
138
+ this._input = document.createElement('input');
139
+ this._input.type = this.inputType;
140
+ this._input.className = 'adw-check-input';
141
+
142
+ this._indicator = document.createElement('span');
143
+ this._indicator.className = 'adw-check-indicator';
144
+
145
+ this._label = document.createElement('span');
146
+ this._label.className = 'adw-check-label';
147
+ const authoredLabel = this.getAttribute('label');
148
+ if (authoredLabel === null) this._label.append(...authored);
149
+ else this._label.textContent = authoredLabel;
150
+
151
+ this.replaceChildren(this._input, this._indicator, this._label);
152
+
153
+ // Keyboard (Space, and the arrow keys that move within a radio group)
154
+ // and a direct programmatic `input.click()` both surface as `change`.
155
+ this._input.addEventListener('change', () => this.activate());
156
+
157
+ // The indicator and the label cover the control; the input is 0x0, so
158
+ // every pointer click lands on one of them. A click that DID reach the
159
+ // input is already handled above — activating again would undo it.
160
+ this.addEventListener('click', (event) => {
161
+ if (this.disabled) return;
162
+ if (event.target === this._input) return;
163
+ this.activate();
164
+ });
165
+
166
+ this.onConnected();
167
+ this.render();
168
+ }
169
+
170
+ disconnectedCallback() {
171
+ this.onDisconnected();
172
+ }
173
+
174
+ attributeChangedCallback(name: string) {
175
+ if (!this._initialized) return;
176
+ if (name === 'label') {
177
+ this._label.textContent = this.getAttribute('label') ?? '';
178
+ }
179
+ this.render();
180
+ if (name === 'checked') {
181
+ this.dispatchEvent(
182
+ new CustomEvent('notify::checked', { bubbles: true, detail: { checked: this.checked } }),
183
+ );
184
+ }
185
+ }
186
+
187
+ /** Extra wiring for a subclass on connect. */
188
+ protected onConnected(): void {}
189
+
190
+ /** Extra teardown for a subclass on disconnect. */
191
+ protected onDisconnected(): void {}
192
+
193
+ protected render(): void {
194
+ const state = resolveCheckState(this.checked, this.indeterminate);
195
+ this._input.checked = state === 'checked';
196
+ this._input.indeterminate = state === 'indeterminate';
197
+ // Explicit, because HTML has a native mixed CHECKBOX (the property
198
+ // above) but no mixed radio — this is the one spelling that announces
199
+ // the third state for both.
200
+ this._input.setAttribute('aria-checked', state === 'indeterminate' ? 'mixed' : String(state === 'checked'));
201
+ this._input.disabled = this.disabled;
202
+ this._label.hidden = this._label.childNodes.length === 0;
203
+ }
204
+ }
205
+
206
+ /** The Adwaita checkbox — `checkbutton > check` as a custom element. */
207
+ export class AdwCheckbox extends AdwCheckBase {
208
+ static get observedAttributes() {
209
+ return ['checked', 'indeterminate', 'disabled', 'label'];
210
+ }
211
+
212
+ protected get inputType(): 'checkbox' {
213
+ return 'checkbox';
214
+ }
215
+
216
+ protected activate(): void {
217
+ // HTML's pre-click activation steps clear `indeterminate` before the
218
+ // toggle, so a user can always reach a definite state. That rule is the
219
+ // HTML spec's, not a reading of GtkCheckButton:inconsistent, whose
220
+ // behaviour is not verifiable in this tree.
221
+ this.indeterminate = false;
222
+ this.checked = !this.checked;
223
+ }
224
+ }
225
+
226
+ /** The Adwaita radio — `checkbutton > radio`, exclusive within its `name`. */
227
+ export class AdwRadio extends AdwCheckBase {
228
+ private _unsubscribe: (() => void) | null = null;
229
+
230
+ static get observedAttributes() {
231
+ return ['checked', 'indeterminate', 'disabled', 'label', 'name', 'value'];
232
+ }
233
+
234
+ protected get inputType(): 'radio' {
235
+ return 'radio';
236
+ }
237
+
238
+ /** The group this member belongs to. */
239
+ get name(): string {
240
+ return this.getAttribute('name') ?? '';
241
+ }
242
+
243
+ set name(value: string) {
244
+ this.setAttribute('name', value);
245
+ }
246
+
247
+ /**
248
+ * This member's value. Falls back to the label text so a group authored
249
+ * without values still has members the group state can tell apart.
250
+ */
251
+ get value(): string {
252
+ return this.getAttribute('value') ?? this.label.trim();
253
+ }
254
+
255
+ set value(value: string) {
256
+ this.setAttribute('value', value);
257
+ }
258
+
259
+ protected activate(): void {
260
+ this.indeterminate = false;
261
+ // An UNNAMED radio is not in a group — HTML forms a radio button group
262
+ // only from a non-empty `name`, so a nameless one checks itself and
263
+ // never unchecks anything. Without this guard every nameless radio in
264
+ // the document would join one `''` group and fight over it.
265
+ if (this.name === '') {
266
+ this.checked = true;
267
+ return;
268
+ }
269
+ // The pick goes through the shared state, never straight onto this
270
+ // element: the member that LOSES the selection is a different element
271
+ // and only the state knows which one it is.
272
+ radioGroups.select(this.name, this.value);
273
+ }
274
+
275
+ protected onConnected(): void {
276
+ this._input.name = this.name;
277
+ if (this.name === '') return; // ungrouped — nothing to subscribe to
278
+ this._unsubscribe ??= radioGroups.subscribe((change) => {
279
+ if (change.name !== this.name) return;
280
+ this.checked = change.selected === this.value;
281
+ });
282
+ // A member declared checked seeds the group, so the first user pick
283
+ // knows what it is deselecting.
284
+ if (this.checked) radioGroups.select(this.name, this.value);
285
+ }
286
+
287
+ protected onDisconnected(): void {
288
+ this._unsubscribe?.();
289
+ this._unsubscribe = null;
290
+ }
291
+
292
+ protected render(): void {
293
+ super.render();
294
+ this._input.name = this.name;
295
+ }
296
+ }
297
+
298
+ customElements.define('adw-checkbox', AdwCheckbox);
299
+ customElements.define('adw-radio', AdwRadio);
@@ -1,27 +1,97 @@
1
- // <adw-clamp> — Constrains its child to a maximum width and centers it.
2
- // Attributes: maximum-size (px, default 600), tightening-threshold (unused;
3
- // accepted for API parity with Adw.Clamp).
1
+ // <adw-clamp> — Constrains its children to a maximum width and centers them.
2
+ // Attributes: maximum-size (px, default 600), tightening-threshold (px, default 400).
3
+ //
4
+ // The clamp is NOT a `max-width`, which is what this element used to be. Adwaita
5
+ // eases the child from "all the available width" into the cap across a tightening
6
+ // region, and stamps a `small`/`medium`/`large` class on it — the arithmetic lives
7
+ // in `@gjsify/adwaita-core` (`clampAllocate`) so this element and the NativeScript
8
+ // widget produce the same width for the same viewport.
9
+ //
10
+ // The cap now sits on the CHILDREN, not on this element. The element has to keep
11
+ // reporting the AVAILABLE width — it is what the curve is evaluated against — and
12
+ // an element that caps itself reports the capped width back to its own
13
+ // ResizeObserver, which is a feedback loop, not a measurement.
14
+ //
15
+ // `childMin` is passed as 0 rather than measured. libadwaita's
16
+ // `lower = MAX (…, child_min)` says a child is never squeezed below its own
17
+ // minimum, and the browser already applies that rule at the other end: `min-width`
18
+ // wins over `max-width` (CSS 2.1 §10.4), so a child that declares a minimum keeps
19
+ // it whatever cap this element writes. A child that declares none has a minimum of
20
+ // 0, which is the value passed here — measuring `min-content` per child on every
21
+ // resize would cost a synchronous reflow to arrive at the same layout.
22
+ //
23
+ // Reference: refs/libadwaita/src/adw-clamp-layout.c (child_size_from_clamp, allocate)
4
24
  // Reference: refs/adwaita-web/adwaita-web/docs/widgets/clamp.md
5
- // Reference: refs/libadwaita (Adw.Clamp)
6
25
  // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
7
26
  // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
8
27
 
28
+ import { ADW_CLAMP_DEFAULTS, ADW_CLAMP_SIZE_CLASSES, clampAllocate, normalizeClampSize } from '@gjsify/adwaita-core';
29
+
9
30
  export class AdwClamp extends HTMLElement {
31
+ private _resize: ResizeObserver | null = null;
32
+ private _mutations: MutationObserver | null = null;
33
+
10
34
  static get observedAttributes() {
11
- return ['maximum-size'];
35
+ return ['maximum-size', 'tightening-threshold'];
12
36
  }
13
37
 
14
38
  connectedCallback() {
15
- this._sync();
39
+ // ResizeObserver delivers an initial observation on observe(), so the
40
+ // first allocation lands before paint without a separate seeding pass.
41
+ this._resize = new ResizeObserver(() => this._allocate());
42
+ this._resize.observe(this);
43
+ // Children appended AFTER connect are clamped too. Without this they
44
+ // would keep whatever width they came with — the same "evaluated once at
45
+ // connect time" bug the toolbar view's empty-bar handling still has.
46
+ this._mutations = new MutationObserver(() => this._allocate());
47
+ this._mutations.observe(this, { childList: true });
48
+ this._allocate();
49
+ }
50
+
51
+ disconnectedCallback() {
52
+ this._resize?.disconnect();
53
+ this._resize = null;
54
+ this._mutations?.disconnect();
55
+ this._mutations = null;
16
56
  }
17
57
 
18
58
  attributeChangedCallback() {
19
- this._sync();
59
+ this._allocate();
20
60
  }
21
61
 
22
- private _sync() {
23
- const max = parseFloat(this.getAttribute('maximum-size') || '600');
24
- this.style.maxWidth = `${max}px`;
62
+ private _allocate(): void {
63
+ const params = {
64
+ maximumSize: normalizeClampSize(this.getAttribute('maximum-size'), ADW_CLAMP_DEFAULTS.maximumSize),
65
+ tighteningThreshold: normalizeClampSize(
66
+ this.getAttribute('tightening-threshold'),
67
+ ADW_CLAMP_DEFAULTS.tighteningThreshold,
68
+ ),
69
+ childMin: 0,
70
+ childNat: 0,
71
+ };
72
+
73
+ const available = this.clientWidth;
74
+ const children = Array.from(this.children) as HTMLElement[];
75
+
76
+ // No box yet (disconnected, or measured before the first layout). GTK's
77
+ // equivalent is `for_size = -1`, where the child reports its natural size
78
+ // rather than being squeezed to nothing — so leave the children alone
79
+ // instead of capping them at 0 and collapsing the page.
80
+ if (available <= 0) {
81
+ for (const child of children) {
82
+ child.style.maxWidth = '';
83
+ child.style.marginInline = '';
84
+ for (const cls of ADW_CLAMP_SIZE_CLASSES) child.classList.remove(cls);
85
+ }
86
+ return;
87
+ }
88
+
89
+ const { childSize, sizeClass } = clampAllocate(available, params);
90
+ for (const child of children) {
91
+ child.style.maxWidth = `${childSize}px`;
92
+ child.style.marginInline = 'auto';
93
+ for (const cls of ADW_CLAMP_SIZE_CLASSES) child.classList.toggle(cls, cls === sizeClass);
94
+ }
25
95
  }
26
96
  }
27
97
 
@@ -17,7 +17,7 @@
17
17
  // title/subtitle text column added to match Adw.ComboRow; the selection state
18
18
  // machine composed from @gjsify/adwaita-core.
19
19
 
20
- import { ComboState } from '@gjsify/adwaita-core';
20
+ import { ComboState, deriveRowLabels, normalizeComboOptions } from '@gjsify/adwaita-core';
21
21
 
22
22
  export class AdwComboRow extends HTMLElement {
23
23
  private _select!: HTMLSelectElement;
@@ -46,8 +46,10 @@ export class AdwComboRow extends HTMLElement {
46
46
 
47
47
  const items: string[] = JSON.parse(this.getAttribute('items') || '[]');
48
48
  // Seed the headless state BEFORE subscribing, so building the initial DOM
49
- // below is not driven by a change notification.
50
- this._state.setOptions(items.map((item) => ({ label: item, value: item })));
49
+ // below is not driven by a change notification. The string→descriptor
50
+ // mapping is core's: `<adw-drop-down>` had its own, richer copy, so the
51
+ // two selectors accepted different option vocabularies.
52
+ this._state.setOptions(normalizeComboOptions(items));
51
53
  this._state.setSelectedIndex(parseInt(this.getAttribute('selected') || '0', 10));
52
54
  const selectedIdx = this._state.selectedIndex;
53
55
 
@@ -83,6 +85,7 @@ export class AdwComboRow extends HTMLElement {
83
85
  this._state.subscribe((change) => {
84
86
  this._valueEl.textContent = change.label;
85
87
  if (this._select.selectedIndex !== change.selected) this._select.selectedIndex = change.selected;
88
+ this._syncChooser();
86
89
  if (!change.interactive) return;
87
90
  this.setAttribute('selected', String(change.selected));
88
91
  this.dispatchEvent(
@@ -99,6 +102,7 @@ export class AdwComboRow extends HTMLElement {
99
102
  this._state.select(this._select.selectedIndex);
100
103
  });
101
104
 
105
+ this._syncChooser();
102
106
  this._renderText();
103
107
  }
104
108
 
@@ -113,11 +117,32 @@ export class AdwComboRow extends HTMLElement {
113
117
  }
114
118
  }
115
119
 
120
+ /**
121
+ * `model_changed` (adw-combo-row.c:187-195) — one option or none is not a
122
+ * choice, so the arrow goes and the row stops being activatable.
123
+ *
124
+ * Both halves have to be expressed: `data-presents-chooser` gates the
125
+ * `.adw-row-value::after` mask in the stylesheet, and DISABLING the overlaid
126
+ * `<select>` is what makes the row inert — hiding the arrow alone would leave
127
+ * a row that still opens a one-entry popup on click.
128
+ */
129
+ private _syncChooser() {
130
+ const presents = this._state.presentsChooser;
131
+ this.dataset.presentsChooser = presents ? 'true' : 'false';
132
+ this._select.disabled = !presents;
133
+ }
134
+
116
135
  private _renderText() {
117
- this._titleEl.textContent = this.getAttribute('title') ?? '';
118
- const subtitle = this.getAttribute('subtitle') ?? '';
119
- this._subtitleEl.textContent = subtitle;
120
- this._subtitleEl.hidden = subtitle.length === 0;
136
+ // The `string_is_not_empty` label rule, from core — this block was one of
137
+ // six hand-rolled copies, all of which omitted the TITLE half.
138
+ const labels = deriveRowLabels({
139
+ title: this.getAttribute('title'),
140
+ subtitle: this.getAttribute('subtitle'),
141
+ });
142
+ this._titleEl.textContent = labels.title;
143
+ this._titleEl.hidden = !labels.titleVisible;
144
+ this._subtitleEl.textContent = labels.subtitle;
145
+ this._subtitleEl.hidden = !labels.subtitleVisible;
121
146
  }
122
147
  }
123
148