@gjsify/adwaita-web 0.51.1 → 0.52.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 (103) hide show
  1. package/dist/adwaita-web.css +255 -27
  2. package/dist/adwaita-web.css.map +1 -1
  3. package/lib/types/blueprint-layout.spec.d.ts +1 -0
  4. package/lib/types/blueprint-markup.spec.d.ts +4 -0
  5. package/lib/types/blueprint-tree.spec.d.ts +1 -0
  6. package/lib/types/elements/adw-bottom-sheet.d.ts +31 -13
  7. package/lib/types/elements/adw-carousel.d.ts +5 -0
  8. package/lib/types/elements/adw-header-bar.d.ts +15 -0
  9. package/lib/types/elements/adw-inline-view-switcher.d.ts +1 -0
  10. package/lib/types/elements/adw-navigation-view.d.ts +1 -0
  11. package/lib/types/elements/adw-sidebar.d.ts +10 -0
  12. package/lib/types/elements/adw-tab-view.d.ts +1 -0
  13. package/lib/types/elements/adw-toggle-group.d.ts +11 -0
  14. package/lib/types/elements/gtk-action-bar.d.ts +19 -0
  15. package/lib/types/elements/gtk-box.d.ts +17 -0
  16. package/lib/types/elements/gtk-button.d.ts +2 -0
  17. package/lib/types/elements/gtk-label.d.ts +122 -0
  18. package/lib/types/gio-menu.spec.d.ts +1 -0
  19. package/lib/types/gtk-box.spec.d.ts +1 -0
  20. package/lib/types/gtk-label.spec.d.ts +1 -0
  21. package/lib/types/gtk-value-doors.spec.d.ts +1 -0
  22. package/lib/types/index.d.ts +4 -1
  23. package/lib/types/namespace/gio.d.ts +1 -0
  24. package/lib/types/namespace/gtk.d.ts +5 -0
  25. package/lib/types/shared-tree-builder.d.ts +64 -0
  26. package/lib/types/source-view/asm6502.d.ts +1 -20
  27. package/lib/types/source-view/index.d.ts +3 -2
  28. package/lib/types/source-view/theme.d.ts +1 -1
  29. package/lib/types/string-list-slot.d.ts +4 -0
  30. package/lib/types/styles.generated.d.ts +1 -1
  31. package/lib/types/tags.spec.d.ts +1 -0
  32. package/lib/types/value-lists.spec.d.ts +1 -0
  33. package/package.json +8 -6
  34. package/scss/_action_bar.scss +51 -0
  35. package/scss/_banner.scss +8 -1
  36. package/scss/_bottom_sheet.scss +57 -0
  37. package/scss/_box.scss +23 -0
  38. package/scss/_button.scss +29 -7
  39. package/scss/_button_content.scss +6 -1
  40. package/scss/_button_row.scss +5 -2
  41. package/scss/_carousel.scss +6 -0
  42. package/scss/_labels.scss +126 -12
  43. package/scss/_navigation_split_view.scss +18 -0
  44. package/scss/_overlay_split_view.scss +5 -0
  45. package/scss/_reset.scss +29 -4
  46. package/scss/_split_button.scss +5 -2
  47. package/scss/_status_page.scss +31 -17
  48. package/scss/_theme.scss +11 -3
  49. package/scss/_toolbar_view.scss +10 -0
  50. package/scss/_variables.scss +1 -0
  51. package/scss/_widget.scss +55 -0
  52. package/scss/_window_title.scss +9 -0
  53. package/scss/adwaita-skin.scss +8 -2
  54. package/src/adw-banner.spec.ts +16 -0
  55. package/src/adw-button-row.spec.ts +8 -0
  56. package/src/adw-header-bar.spec.ts +17 -0
  57. package/src/blueprint-layout.spec.ts +223 -0
  58. package/src/blueprint-markup.spec.ts +155 -0
  59. package/src/blueprint-tree.spec.ts +174 -0
  60. package/src/bottom-sheet.spec.ts +166 -1
  61. package/src/chrome.spec.ts +5 -2
  62. package/src/elements/adw-bottom-sheet.ts +166 -59
  63. package/src/elements/adw-carousel.ts +10 -5
  64. package/src/elements/adw-clamp.ts +12 -0
  65. package/src/elements/adw-combo-row.ts +13 -4
  66. package/src/elements/adw-header-bar.ts +46 -3
  67. package/src/elements/adw-inline-view-switcher.ts +11 -0
  68. package/src/elements/adw-navigation-view.ts +10 -0
  69. package/src/elements/adw-sidebar.ts +22 -0
  70. package/src/elements/adw-spin-row.ts +44 -5
  71. package/src/elements/adw-status-page.ts +12 -6
  72. package/src/elements/adw-tab-view.ts +20 -3
  73. package/src/elements/adw-toggle-group.ts +45 -4
  74. package/src/elements/adw-toolbar-view.ts +9 -0
  75. package/src/elements/gtk-action-bar.ts +90 -0
  76. package/src/elements/gtk-box.ts +87 -0
  77. package/src/elements/gtk-button.ts +60 -13
  78. package/src/elements/gtk-drop-down.ts +10 -1
  79. package/src/elements/gtk-label.ts +415 -0
  80. package/src/gio-menu.spec.ts +125 -0
  81. package/src/globals.d.ts +7 -0
  82. package/src/gtk-box.spec.ts +168 -0
  83. package/src/gtk-button.spec.ts +123 -1
  84. package/src/gtk-label.spec.ts +595 -0
  85. package/src/gtk-value-doors.spec.ts +104 -0
  86. package/src/index.ts +14 -1
  87. package/src/keyboard-operable.spec.ts +2 -1
  88. package/src/namespace/gio.ts +19 -0
  89. package/src/namespace/gtk.ts +14 -0
  90. package/src/shared-tree-builder.ts +268 -0
  91. package/src/shared-trees.spec.ts +296 -32
  92. package/src/source-view/adw-source-view.ts +3 -3
  93. package/src/source-view/asm6502.ts +11 -191
  94. package/src/source-view/index.ts +17 -11
  95. package/src/source-view/theme.ts +16 -16
  96. package/src/split-button.spec.ts +7 -0
  97. package/src/string-list-slot.ts +21 -0
  98. package/src/styles.generated.ts +1 -1
  99. package/src/tags.spec.ts +51 -0
  100. package/src/test.browser.mts +18 -0
  101. package/src/value-lists.spec.ts +93 -0
  102. package/lib/types/source-view/hex.d.ts +0 -21
  103. package/src/source-view/hex.ts +0 -37
@@ -16,6 +16,14 @@
16
16
  // the NativeScript row and the React Native widget take. The same markup door
17
17
  // `<adw-combo-row model=…>` opens for its list, on the same reasoning.
18
18
  //
19
+ // A `.blp` writes the range as an OBJECT, `adjustment: Adjustment { upper: 20; }`, and the
20
+ // shared-tree builder hands that over as a `<gtk-adjustment slot="adjustment" upper="20">`
21
+ // child. The slot consumes it: the child is data, read for the six `Gtk.Adjustment`
22
+ // properties under their GObject spelling, then dropped — GtkBuilder leaves no adjustment
23
+ // in the widget tree either. It REPLACES the range rather than merging into it, because
24
+ // `adjustment:` sets the property to a whole new object: an unwritten field takes the
25
+ // adjustment's own default, not what the row held (`ADJUSTMENT_AUTHORED_VECTORS`).
26
+ //
19
27
  // Adapted from Adwaita Web UI Framework (https://github.com/mclellac/adwaita-web).
20
28
  // Copyright (c) 2025 csm. MIT License.
21
29
  // Modifications: Reimplemented as Web Component for @gjsify/adwaita-web;
@@ -24,6 +32,29 @@
24
32
 
25
33
  import { SpinState, deriveRowLabels, normalizeAdjustment, parseAdjustment } from '@gjsify/adwaita-core';
26
34
  import type { AdwAdjustment, AdwAdjustmentInput } from '@gjsify/adwaita-core';
35
+ import { attributeOf } from '@gjsify/adwaita-core/tags';
36
+
37
+ import { bindSlottedChildren } from '../slotted-children.js';
38
+
39
+ /** `Gtk.Adjustment`'s six properties, as `AdwAdjustmentInput` names them. */
40
+ const ADJUSTMENT_FIELDS = ['value', 'lower', 'upper', 'stepIncrement', 'pageIncrement', 'pageSize'] as const;
41
+
42
+ /**
43
+ * The range a `<gtk-adjustment>` child authors: every field written as a number, under its
44
+ * GObject attribute spelling (`step-increment`). An absent or non-numeric field is not
45
+ * authored, so it cannot overwrite what the row already holds — the same contract
46
+ * `parseAdjustment` gives the JSON attribute.
47
+ */
48
+ function authoredAdjustment(node: Node): AdwAdjustmentInput {
49
+ const input: Record<string, number> = {};
50
+ if (!(node instanceof Element)) return input;
51
+ for (const field of ADJUSTMENT_FIELDS) {
52
+ const raw = node.getAttribute(attributeOf(field));
53
+ const value = raw === null ? Number.NaN : Number.parseFloat(raw);
54
+ if (Number.isFinite(value)) input[field] = value;
55
+ }
56
+ return input;
57
+ }
27
58
 
28
59
  export class AdwSpinRow extends HTMLElement {
29
60
  private _input!: HTMLInputElement;
@@ -73,10 +104,6 @@ export class AdwSpinRow extends HTMLElement {
73
104
  // rather than against whatever a partly-seeded state held.
74
105
  const range = normalizeAdjustment(parseAdjustment(this.getAttribute('adjustment')));
75
106
  this._state.configure(range);
76
- const authoredValue = this.getAttribute('value');
77
- this._state.setValue(
78
- this._roundToStep(authoredValue === null ? range.value : Number.parseFloat(authoredValue)),
79
- );
80
107
 
81
108
  const text = document.createElement('div');
82
109
  text.className = 'adw-row-text';
@@ -113,7 +140,19 @@ export class AdwSpinRow extends HTMLElement {
113
140
  incBtn.addEventListener('click', () => this._adjust(this._state.adjustment.stepIncrement));
114
141
 
115
142
  control.append(decBtn, input, incBtn);
116
- this.replaceChildren(text, control);
143
+ // The install reads an authored `<gtk-adjustment>` into the range; the authored
144
+ // `value` is applied after it, against the range it was authored beside.
145
+ bindSlottedChildren(this, [
146
+ {
147
+ name: 'adjustment',
148
+ consume: (node) => this._state.configure(normalizeAdjustment(authoredAdjustment(node))),
149
+ },
150
+ ]).install(text, control);
151
+ const authoredValue = this.getAttribute('value');
152
+ this._state.setValue(
153
+ this._roundToStep(authoredValue === null ? this._state.value : Number.parseFloat(authoredValue)),
154
+ );
155
+ input.value = this._formatValue(this._state.value);
117
156
  this._input = input;
118
157
 
119
158
  // Every core change (a set, or a re-clamp after a bound moved) refreshes
@@ -1,6 +1,7 @@
1
1
  // <adw-status-page> — A centered empty/placeholder state: a large symbolic
2
2
  // icon, a title, a description and an optional action (slotted child).
3
- // Attributes: icon (symbolic name, with or without -symbolic), title, description.
3
+ // Attributes: icon (symbolic name, with or without -symbolic) or icon-name, the GObject
4
+ // spelling a projected `.blp` writes; title, description.
4
5
  // Reference: refs/adwaita-web/adwaita-web/scss/_status_page.scss
5
6
  // Reference: refs/libadwaita/src/stylesheet/widgets/_misc.scss (AdwStatusPage)
6
7
  // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
@@ -21,7 +22,7 @@ export class AdwStatusPage extends HTMLElement {
21
22
  private _initialized = false;
22
23
 
23
24
  static get observedAttributes() {
24
- return ['icon', 'title', 'description'];
25
+ return ['icon', 'icon-name', 'title', 'description'];
25
26
  }
26
27
 
27
28
  connectedCallback() {
@@ -41,8 +42,12 @@ export class AdwStatusPage extends HTMLElement {
41
42
 
42
43
  // `Adw.StatusPage:child` is a property, so the action a status page offers can be
43
44
  // set at any point — a button appended after connect has to land in the child box,
44
- // not beside it. `src/slotted-children.ts` has the incident.
45
- bindSlottedChildren(this, [{ into: this._childEl }]).install(
45
+ // not beside it. `src/slotted-children.ts` has the incident. NAMED beside the
46
+ // default for the same reason `adw-toolbar-view.ts` names `content` beside ITS
47
+ // default: an authored `child: …` carries `slot: 'child'`
48
+ // (`shared-tree-builder.ts`'s `refuseUnknownSlots`), a name the bare default claim
49
+ // does not answer to, so the placement was refused sight unseen until this line.
50
+ bindSlottedChildren(this, [{ name: 'child', into: this._childEl }, { into: this._childEl }]).install(
46
51
  this._iconEl,
47
52
  this._titleEl,
48
53
  this._descEl,
@@ -59,15 +64,16 @@ export class AdwStatusPage extends HTMLElement {
59
64
  }
60
65
 
61
66
  private _render() {
67
+ const icon = this.getAttribute('icon') ?? this.getAttribute('icon-name');
62
68
  // The element swaps the mask class and keeps the size class.
63
- this._iconEl.iconName = this.getAttribute('icon');
69
+ this._iconEl.iconName = icon;
64
70
  // `has_image`: `paintable || (icon_name && icon_name[0])` — a name that was GIVEN
65
71
  // shows the image whether or not the theme can resolve it, and GTK draws
66
72
  // `image-missing` in it. Reading `resolvedIconName` instead hid the slot for every
67
73
  // name that is not one CSS token, a branch the C does not have.
68
74
  // Reference: refs/libadwaita/src/adw-status-page.c:88 (has_image), bound from
69
75
  // `<binding name="visible">` in adw-status-page.ui:27-31.
70
- this._iconEl.hidden = !stringIsNotEmpty(this.getAttribute('icon'));
76
+ this._iconEl.hidden = !stringIsNotEmpty(icon);
71
77
 
72
78
  // `string_is_not_empty` from the core, not a local `.length === 0`: the same
73
79
  // closure the NativeScript port binds its labels to, and the reason a title of
@@ -48,6 +48,7 @@
48
48
  import { TabViewState, tabCloseVisible, tabIconState, tabTooltip, tabsRevealed } from '@gjsify/adwaita-core';
49
49
  import type { AdwTabPageSpec, AdwTabPageState, TabViewPagesChange, TabViewSelectionChange } from '@gjsify/adwaita-core';
50
50
 
51
+ import { bindSlottedChildren } from '../slotted-children.js';
51
52
  import { type GtkImage, createGtkImage } from './gtk-image.js';
52
53
 
53
54
  export type AdwTabViewPage = AdwTabPageState<HTMLElement>;
@@ -71,6 +72,13 @@ export class AdwTabPage extends HTMLElement {
71
72
  return PAGE_ATTRIBUTES;
72
73
  }
73
74
 
75
+ connectedCallback(): void {
76
+ // `AdwTabPage:child` is a PROPERTY, so a `.blp`'s `child: …` authors `slot="child"`.
77
+ // The page's children already ARE its panel content, so this only enrols the
78
+ // name, for the reason `adw-clamp.ts` gives for not calling `.install()`.
79
+ bindSlottedChildren(this, [{ name: 'child', into: this }]);
80
+ }
81
+
74
82
  attributeChangedCallback(name: string, _old: string | null, value: string | null) {
75
83
  // `closest` still finds the view after adoption: the element is moved
76
84
  // into the view's page container, not replaced by a wrapper.
@@ -194,10 +202,19 @@ export class AdwTabView extends HTMLElement {
194
202
  const declaredSelection = this.getAttribute('selected-page');
195
203
 
196
204
  // Snapshot the declared pages, then take over the subtree. The elements
197
- // themselves become the panels, so they must survive replaceChildren.
198
- const declared = Array.from(this.querySelectorAll(':scope > adw-tab-page')) as AdwTabPage[];
205
+ // themselves become the panels, so they must survive replaceChildren. A child
206
+ // that is not an `<adw-tab-page>` is a page too, untitled and around itself, as
207
+ // `adw_tab_view_append` makes one of a bare widget (adw-tab-view.c:2884) — it was
208
+ // dropped by the `replaceChildren` before.
209
+ const declared = Array.from(this.children) as HTMLElement[];
199
210
  this.replaceChildren(this._barEl, this._pagesEl);
200
- for (const pageEl of declared) this._adoptDeclaredPage(pageEl);
211
+ for (const el of declared) {
212
+ // By NAME, not `instanceof`: this view is defined before `<adw-tab-page>`, so
213
+ // markup parsed before the module loaded reaches here with its pages not yet
214
+ // upgraded — and `_adoptDeclaredPage` reads only attributes, which they have.
215
+ if (el.localName === 'adw-tab-page') this._adoptDeclaredPage(el as AdwTabPage);
216
+ else this.appendPage({ id: this._nextId(), content: el });
217
+ }
201
218
 
202
219
  if (declaredSelection !== null) this._applySelectedAttribute(declaredSelection);
203
220
  this._applyBarVisibility();
@@ -1,6 +1,7 @@
1
1
  // <adw-toggle-group> — the web counterpart of Adw.ToggleGroup: a linked set of toggle
2
2
  // buttons where exactly one is active. Toggles are `<adw-toggle>` children carrying
3
- // `label` and/or `icon-name`, and the `flat` / `round` attributes mirror the upstream
3
+ // `label` and/or `icon-name` and an optional `name`, which the group's `active-name`
4
+ // selects by (read at connect, like the rest). The `flat` / `round` attributes mirror the upstream
4
5
  // `.flat` / `.round` style classes. `notify::active` (CustomEvent, bubbles, detail
5
6
  // `{ active }`) mirrors the `active` GObject property.
6
7
  //
@@ -54,7 +55,7 @@
54
55
  // selection state machine composed from @gjsify/adwaita-core, the icon node
55
56
  // from <gtk-image>.
56
57
 
57
- import { ToggleGroupState } from '@gjsify/adwaita-core';
58
+ import { ToggleGroupState, keptToggles, toggleIndexOfName } from '@gjsify/adwaita-core';
58
59
 
59
60
  import { createGtkImage } from './gtk-image.js';
60
61
  import { attachRovingFocus } from './roving-focus.js';
@@ -74,9 +75,11 @@ export class AdwToggleGroup extends HTMLElement {
74
75
  private _initialized = false;
75
76
  /** `aria-checked` under `radio`, `aria-selected` under `tab` — decided at connect. */
76
77
  private _stateAttr: 'aria-checked' | 'aria-selected' = 'aria-checked';
78
+ /** Each toggle's `name`, in order — what `active-name` resolves against. */
79
+ private _names: (string | null)[] = [];
77
80
 
78
81
  static get observedAttributes() {
79
- return ['active', 'flat', 'round'];
82
+ return ['active', 'active-name', 'flat', 'round'];
80
83
  }
81
84
 
82
85
  /** Zero-based index of the active toggle. */
@@ -88,13 +91,31 @@ export class AdwToggleGroup extends HTMLElement {
88
91
  this.setAttribute('active', String(value));
89
92
  }
90
93
 
94
+ /** `Adw.ToggleGroup:active-name` — the `name` of the active toggle, or `null`. */
95
+ get activeName(): string | null {
96
+ return this._names[this._state.selected] ?? null;
97
+ }
98
+
99
+ set activeName(value: string | null) {
100
+ if (value === null) this.removeAttribute('active-name');
101
+ else this.setAttribute('active-name', value);
102
+ }
103
+
91
104
  connectedCallback() {
92
105
  if (this._initialized) return;
93
106
  this._initialized = true;
94
107
 
95
108
  // Snapshot the declared <adw-toggle> children before we take over the
96
109
  // subtree — their label / icon-name become the rendered buttons.
97
- const toggles = Array.from(this.querySelectorAll('adw-toggle')) as AdwToggle[];
110
+ // A second toggle claiming a name is refused whole, as `add_toggle` refuses it
111
+ // (adw-toggle-group.c:849) — the core's rule, not this element's.
112
+ const authored = Array.from(this.querySelectorAll('adw-toggle')) as AdwToggle[];
113
+ const toggles = keptToggles(authored.map((el) => ({ el, name: el.getAttribute('name') }))).map(
114
+ (toggle) => toggle.el,
115
+ );
116
+ if (toggles.length < authored.length) {
117
+ console.warn('[adw-toggle-group] a toggle name already exists; the later toggle is not added');
118
+ }
98
119
 
99
120
  // `gtk_widget_class_set_accessible_role` sets a CLASS DEFAULT that an instance's
100
121
  // construct-time `accessible-role` overrides, and `add_toggle` then reads
@@ -159,8 +180,12 @@ export class AdwToggleGroup extends HTMLElement {
159
180
  this._state.setLabels(
160
181
  toggles.map((toggle) => toggle.getAttribute('label') || toggle.getAttribute('icon-name') || ''),
161
182
  );
183
+ this._names = toggles.map((toggle) => toggle.getAttribute('name'));
162
184
  this._state.subscribe(() => this._render());
163
185
  this._state.setSelected(this._readActiveAttr());
186
+ // After `active`, and after the toggles are in, as `parser_finished` applies it
187
+ // (adw-toggle-group.c:1247): a group whose markup names both ends on the name.
188
+ this._applyActiveName();
164
189
  this._render();
165
190
 
166
191
  // No `disabled`/`hidden` filter, because no `<adw-toggle>` attribute can produce
@@ -185,10 +210,26 @@ export class AdwToggleGroup extends HTMLElement {
185
210
  this._state.setSelected(this._readActiveAttr());
186
211
  return;
187
212
  }
213
+ if (name === 'active-name') {
214
+ this._applyActiveName();
215
+ return;
216
+ }
188
217
  // flat / round are styling-only.
189
218
  this._render();
190
219
  }
191
220
 
221
+ /**
222
+ * `adw_toggle_group_set_active_name`: the toggle carrying the name becomes active, and
223
+ * a name nothing carries leaves the selection where it was — the C's `g_critical` and
224
+ * return (:2001). Which toggle a name means is the core's answer, not this element's.
225
+ */
226
+ private _applyActiveName(): void {
227
+ const name = this.getAttribute('active-name');
228
+ if (name === null) return;
229
+ const index = toggleIndexOfName(this._names, name);
230
+ if (index !== -1) this._state.setSelected(index);
231
+ }
232
+
192
233
  private _readActiveAttr(): number {
193
234
  const raw = Number.parseInt(this.getAttribute('active') ?? '0', 10);
194
235
  if (Number.isNaN(raw)) return 0;
@@ -85,9 +85,18 @@ export class AdwToolbarView extends HTMLElement {
85
85
  // reach the same box whenever it is written — including after connect, which a
86
86
  // snapshot here cannot see. The unnamed slot is the content, the
87
87
  // Adw.ToolbarView buildable default. `src/slotted-children.ts` has the incident.
88
+ //
89
+ // `content` IS NAMED BESIDE THAT DEFAULT, and it is the same destination rather
90
+ // than a fourth one: `Adw.ToolbarView:content` is the property a source that
91
+ // spells the placement writes it at, and GTK reaches this box through either —
92
+ // the buildable default for a bare child, `adw_toolbar_view_set_content` for a
93
+ // named one. A tree authored at the property position (a Blueprint
94
+ // `content: …`) carried a name this element had no destination for, so the
95
+ // child stayed a stray sibling of all three boxes.
88
96
  bindSlottedChildren(this, [
89
97
  { name: 'top', into: this._topEl },
90
98
  { name: 'bottom', into: this._bottomEl },
99
+ { name: 'content', into: this._contentEl },
91
100
  { into: this._contentEl },
92
101
  ]).install(this._topEl, this._contentEl, this._bottomEl);
93
102
 
@@ -0,0 +1,90 @@
1
+ // <gtk-action-bar> — GTK's bottom bar: widgets packed from the start, from the end, and
2
+ // one in the centre.
3
+ //
4
+ // NAMED FOR THE LIBRARY THAT OWNS THE GTYPE (ADR 0034 clause 1): `Gtk.ActionBar` is GTK's,
5
+ // and libadwaita only styles it (`actionbar > revealer > box` in `_toolbars.scss`).
6
+ //
7
+ // THE PLACEMENT IS GtkBuildable's (gtkactionbar.c:220): `[start]` packs from the start,
8
+ // `[end]` packs from the end, `[center]` is the centre widget, and a child with no type
9
+ // packs from the start. There is no `center-widget` slot: GTK has `set_center_widget` but
10
+ // no property of that name, and `Gtk.Builder` refuses a `.blp` that writes one. Packing
11
+ // from the end is `gtk_box_insert_child_after (end_box, child, NULL)` — a PREPEND — so the
12
+ // first `[end]` child sits nearest the edge and each later one lands in front of it,
13
+ // exactly as `adw_header_bar_pack_end` does.
14
+ //
15
+ // `revealed` defaults to TRUE, as `GtkActionBar:revealed` does: only `revealed="false"`
16
+ // hides the bar.
17
+ //
18
+ // Reference: refs/gtk/gtk/gtkactionbar.c (the buildable child types, pack_start/pack_end)
19
+ // Reference: refs/libadwaita/src/stylesheet/widgets/_toolbars.scss (GtkActionBar)
20
+ // Copyright (c) The GTK Team. LGPLv2.1+.
21
+
22
+ import { bindSlottedChildren } from '../slotted-children.js';
23
+
24
+ export class GtkActionBar extends HTMLElement {
25
+ private _initialized = false;
26
+ private _startEl: HTMLDivElement | null = null;
27
+ private _centerEl: HTMLDivElement | null = null;
28
+ private _endEl: HTMLDivElement | null = null;
29
+
30
+ static get observedAttributes() {
31
+ return ['revealed'];
32
+ }
33
+
34
+ /** `GtkActionBar:revealed` — whether the bar shows its contents. */
35
+ get revealed(): boolean {
36
+ return this.getAttribute('revealed') !== 'false';
37
+ }
38
+
39
+ set revealed(value: boolean) {
40
+ this.setAttribute('revealed', value ? 'true' : 'false');
41
+ }
42
+
43
+ /** The start box — what `pack_start` appends to. */
44
+ get startSection(): HTMLDivElement | null {
45
+ return this._startEl;
46
+ }
47
+
48
+ /** The centre — what `gtk_action_bar_set_center_widget` sets. */
49
+ get centerSection(): HTMLDivElement | null {
50
+ return this._centerEl;
51
+ }
52
+
53
+ /** The end box — what `pack_end` prepends to. */
54
+ get endSection(): HTMLDivElement | null {
55
+ return this._endEl;
56
+ }
57
+
58
+ connectedCallback() {
59
+ if (this._initialized) return;
60
+ this._initialized = true;
61
+
62
+ this._startEl = document.createElement('div');
63
+ this._startEl.className = 'adw-action-bar-start';
64
+ this._centerEl = document.createElement('div');
65
+ this._centerEl.className = 'adw-action-bar-center';
66
+ this._endEl = document.createElement('div');
67
+ this._endEl.className = 'adw-action-bar-end';
68
+ const endEl = this._endEl;
69
+
70
+ bindSlottedChildren(this, [
71
+ { name: 'start', into: this._startEl },
72
+ { name: 'center', into: this._centerEl },
73
+ { name: 'end', consume: (node) => endEl.prepend(node) },
74
+ // An untyped child packs from the start (gtkactionbar.c:233).
75
+ { into: this._startEl },
76
+ ]).install(this._startEl, this._centerEl, this._endEl);
77
+
78
+ this._renderRevealed();
79
+ }
80
+
81
+ attributeChangedCallback() {
82
+ if (this._initialized) this._renderRevealed();
83
+ }
84
+
85
+ private _renderRevealed(): void {
86
+ this.hidden = !this.revealed;
87
+ }
88
+ }
89
+
90
+ customElements.define('gtk-action-bar', GtkActionBar);
@@ -0,0 +1,87 @@
1
+ // <gtk-box> — GTK's one-axis container: children in a row or a column, a fixed gap
2
+ // between them.
3
+ //
4
+ // NAMED FOR THE LIBRARY THAT OWNS THE GTYPE (ADR 0034 clause 1): libadwaita ships no box,
5
+ // it styles what a caller puts in one. Without this element a Blueprint layout mounted
6
+ // through `mountSharedTree` realised every `Gtk.Box` as an unknown inline element, so a
7
+ // vertical stack of preference groups laid out as one run of inline boxes.
8
+ //
9
+ // NO CORE STATE MACHINE (ADR 0004, trivial behaviour): the stacking is flexbox's. What is
10
+ // shared is how an authored value becomes the property — `normalizeBoxSpacing` and
11
+ // `normalizeBoxOrientation` in `@gjsify/adwaita-core`, which the NativeScript `GtkBox`
12
+ // reads too. The gap is the flex `gap`, which is GTK's spacing exactly: between children
13
+ // only, never at the box's own edges.
14
+ //
15
+ // `homogeneous` gives every child an equal share along the axis (`flex: 1 1 0`, in
16
+ // `_box.scss`). A child's minimum still holds, as in `gtk_box_layout`'s homogeneous
17
+ // allocation, because flex never shrinks an item below its min-content size.
18
+ //
19
+ // Children are not moved or wrapped: the element IS the flex container, so a child in the
20
+ // light DOM is a child of the layout, which is what `gtk_box_append` means.
21
+ //
22
+ // Reference: refs/gtk/gtk/gtkbox.c (the three properties, the `box` CSS name)
23
+ // Copyright (c) The GTK Team. LGPLv2.1+.
24
+
25
+ import { normalizeBoxOrientation, normalizeBoxSpacing, type BoxOrientation } from '@gjsify/adwaita-core';
26
+
27
+ /** The attributes that carry a property — also the `notify::` roster. */
28
+ const PROPERTY_ATTRIBUTES = ['orientation', 'spacing', 'homogeneous'] as const;
29
+
30
+ export class GtkBox extends HTMLElement {
31
+ static get observedAttributes() {
32
+ return [...PROPERTY_ATTRIBUTES];
33
+ }
34
+
35
+ /** `GtkOrientable:orientation` — `horizontal` (the default) or `vertical`. */
36
+ get orientation(): BoxOrientation {
37
+ return normalizeBoxOrientation(this.getAttribute('orientation'));
38
+ }
39
+
40
+ set orientation(value: BoxOrientation) {
41
+ this.setAttribute('orientation', value);
42
+ }
43
+
44
+ /** `Gtk.Box:spacing` — the gap between children, in px. Defaults to 0. */
45
+ get spacing(): number {
46
+ return normalizeBoxSpacing(this.getAttribute('spacing'));
47
+ }
48
+
49
+ set spacing(value: number) {
50
+ this.setAttribute('spacing', String(value));
51
+ }
52
+
53
+ /** `Gtk.Box:homogeneous` — whether every child gets the same size along the axis. */
54
+ get homogeneous(): boolean {
55
+ return this.hasAttribute('homogeneous');
56
+ }
57
+
58
+ set homogeneous(value: boolean) {
59
+ this.toggleAttribute('homogeneous', !!value);
60
+ }
61
+
62
+ connectedCallback() {
63
+ this._sync();
64
+ }
65
+
66
+ attributeChangedCallback(name: string, old: string | null, value: string | null) {
67
+ this._sync();
68
+ const next = this._normalized(name, value);
69
+ if (next === this._normalized(name, old)) return;
70
+ this.dispatchEvent(new CustomEvent(`notify::${name}`, { bubbles: true, detail: { [name]: next } }));
71
+ }
72
+
73
+ private _normalized(name: string, raw: string | null): string | number | boolean {
74
+ if (name === 'orientation') return normalizeBoxOrientation(raw);
75
+ if (name === 'spacing') return normalizeBoxSpacing(raw);
76
+ return raw !== null;
77
+ }
78
+
79
+ private _sync(): void {
80
+ // Inline, from the NORMALISED values, so an unknown nick or a negative spacing
81
+ // lays out as the GTK default rather than whatever the raw attribute says.
82
+ this.style.flexDirection = this.orientation === 'vertical' ? 'column' : 'row';
83
+ this.style.gap = `${this.spacing}px`;
84
+ }
85
+ }
86
+
87
+ customElements.define('gtk-box', GtkBox);
@@ -1,9 +1,14 @@
1
1
  // <gtk-button> — Adwaita button.
2
- // Attributes: icon (symbolic name, e.g. "go-previous" / "view-refresh"),
3
- // label, tooltip, disabled, and the boolean variant flags
4
- // flat / suggested / destructive / circular / pill.
2
+ // Attributes: icon (symbolic name, e.g. "go-previous" / "view-refresh"), or
3
+ // icon-name, the GObject spelling a projected `.blp` writes; label, tooltip or
4
+ // tooltip-text (the `.blp` spelling again),
5
+ // disabled, and the boolean variant flags flat / suggested / destructive /
6
+ // circular / pill. The GTK style classes on the host (`class="pill"`, which is
7
+ // what a `.blp`'s `styles ["pill"]` becomes) select the same classes.
5
8
  // Renders an inner <button class="adw-button …">; `click` bubbles to the host,
6
- // so `adwButton.addEventListener('click', …)` works.
9
+ // so `adwButton.addEventListener('click', …)` works. An element child — what a
10
+ // `.blp`'s `child: Adw.ButtonContent { … }` becomes — is `Gtk.Button:child` and
11
+ // replaces the icon and the label inside that inner button.
7
12
  // Reference: refs/libadwaita/src/stylesheet/widgets/_buttons.scss
8
13
  // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
9
14
  // Modifications: Implemented as a Web Component for @gjsify/adwaita-web; the
@@ -11,6 +16,7 @@
11
16
 
12
17
  import { buttonStyleClasses } from '@gjsify/adwaita-core';
13
18
 
19
+ import { bindSlottedChildren } from '../slotted-children.js';
14
20
  import { createGtkImage } from './gtk-image.js';
15
21
 
16
22
  /** The boolean attributes that select a style class; the mapping lives in the core. */
@@ -20,9 +26,24 @@ export class GtkButton extends HTMLElement {
20
26
  private _button!: HTMLButtonElement;
21
27
  private _label = '';
22
28
  private _initialized = false;
29
+ /** `Gtk.Button:child`, or `null` while the icon and the label fill the button. */
30
+ private _child: Element | null = null;
23
31
 
24
32
  static get observedAttributes() {
25
- return ['icon', 'label', 'tooltip', 'disabled', 'flat', 'suggested', 'destructive', 'circular', 'pill'];
33
+ return [
34
+ 'icon',
35
+ 'icon-name',
36
+ 'label',
37
+ 'tooltip',
38
+ 'tooltip-text',
39
+ 'disabled',
40
+ 'flat',
41
+ 'suggested',
42
+ 'destructive',
43
+ 'circular',
44
+ 'pill',
45
+ 'class',
46
+ ];
26
47
  }
27
48
 
28
49
  /** The inner native button (for focus/imperative access). */
@@ -33,10 +54,28 @@ export class GtkButton extends HTMLElement {
33
54
  connectedCallback() {
34
55
  if (this._initialized) return;
35
56
  this._initialized = true;
36
- // Capture any inline text as the label before we take over the subtree.
37
- this._label = (this.getAttribute('label') ?? this.textContent ?? '').trim();
57
+ // Capture any inline text as the label before we take over the subtree. Only the
58
+ // host's own text: an element child is `child`, and its text is not a label.
59
+ const inlineText = Array.from(this.childNodes)
60
+ .filter((node) => node.nodeType === Node.TEXT_NODE)
61
+ .map((node) => node.textContent ?? '')
62
+ .join('');
63
+ this._label = (this.getAttribute('label') ?? inlineText).trim();
38
64
  this._button = document.createElement('button');
39
- this.replaceChildren(this._button);
65
+ // An authored `child: …` names the `child` slot, and a bare element child means the
66
+ // same thing, as GtkBuildable's untyped `<child>` does. Consumed rather than moved:
67
+ // `_render` rebuilds the inner button's content, so it is `_render` that places it.
68
+ // Stray text is claimed by nothing and leaves with the host's old children.
69
+ bindSlottedChildren(this, [
70
+ {
71
+ name: 'child',
72
+ claims: (node) => node instanceof Element,
73
+ consume: (node) => {
74
+ this._child = node as Element;
75
+ this._render();
76
+ },
77
+ },
78
+ ]).install(this._button);
40
79
  this._render();
41
80
  }
42
81
 
@@ -50,17 +89,25 @@ export class GtkButton extends HTMLElement {
50
89
  // The attribute → class mapping is `@gjsify/adwaita-core`'s, so this element
51
90
  // and the NativeScript one cannot disagree about which classes exist —
52
91
  // `circular` was in this table and missing from that one.
53
- btn.classList.add(...buttonStyleClasses(STYLE_ATTRIBUTES.filter((attr) => this.hasAttribute(attr))));
92
+ // The host's own classes go through the same table: a built `.blp` spells the
93
+ // style as GTK does, a class on the widget, and reading only the attributes
94
+ // rendered its pill and flat buttons as plain ones.
95
+ const styles = [...STYLE_ATTRIBUTES.filter((attr) => this.hasAttribute(attr)), ...this.classList];
96
+ btn.classList.add(...buttonStyleClasses(styles));
54
97
 
55
- const icon = this.getAttribute('icon');
56
- const label = (this.getAttribute('label') ?? this._label).trim();
98
+ const child = this._child;
99
+ const icon = child ? null : (this.getAttribute('icon') ?? this.getAttribute('icon-name'));
100
+ const label = child ? '' : (this.getAttribute('label') ?? this._label).trim();
57
101
  if (icon && !label) btn.classList.add('icon-only');
102
+ // GTK's `.text-button`: what `gtk_button_set_label` stamps on a label-only button.
103
+ if (label && !icon) btn.classList.add('text-button');
58
104
 
59
- btn.replaceChildren();
105
+ if (child) btn.replaceChildren(child);
106
+ else btn.replaceChildren();
60
107
  if (icon) btn.appendChild(createGtkImage(icon));
61
108
  if (label) btn.appendChild(document.createTextNode(label));
62
109
 
63
- const tooltip = this.getAttribute('tooltip');
110
+ const tooltip = this.getAttribute('tooltip') ?? this.getAttribute('tooltip-text');
64
111
  btn.title = tooltip ?? '';
65
112
  // An icon-only button has no text content, so screen readers would
66
113
  // announce it as unlabeled. Give it an accessible name — prefer the
@@ -68,6 +68,9 @@ import type { GtkPopover } from './gtk-popover.js';
68
68
  import { ComboState, normalizeComboOptions, parseListModel } from '@gjsify/adwaita-core';
69
69
  import type { AdwComboOption, AdwListItemsChanged, AdwListModelInput } from '@gjsify/adwaita-core';
70
70
 
71
+ import { bindSlottedChildren } from '../slotted-children.js';
72
+ import { stringListSlot } from '../string-list-slot.js';
73
+
71
74
  import { createGtkImage } from './gtk-image.js';
72
75
 
73
76
  /**
@@ -176,6 +179,13 @@ export class GtkDropDown extends HTMLElement {
176
179
  this._listEl.className = 'adw-drop-down-list';
177
180
  this._popoverEl.appendChild(this._listEl);
178
181
 
182
+ // A `<gtk-string-list slot="model">` child is the list a `.blp` authored (ADR 0072),
183
+ // consumed first so the attribute seed below sees a model and leaves it alone.
184
+ bindSlottedChildren(this, [stringListSlot((model) => this._state.setModel(model))]).install(
185
+ this._buttonEl,
186
+ this._popoverEl,
187
+ );
188
+
179
189
  // Seed options BEFORE anything subscribes, so building the initial DOM below is
180
190
  // not driven by a change notification.
181
191
  if (this._state.count === 0) this._state.setModel(parseListModel(this.getAttribute('model')));
@@ -186,7 +196,6 @@ export class GtkDropDown extends HTMLElement {
186
196
  // assignment reaches the splice rather than a rebuild.
187
197
  this._state.subscribeItems((change) => this._applyItemsChanged(change));
188
198
 
189
- this.replaceChildren(this._buttonEl, this._popoverEl);
190
199
  this._popoverEl.anchor = this._buttonEl;
191
200
  this._popoverEl.subscribe((open) => this._onPopoverToggled(open));
192
201
  // Type-ahead is the ONE key class the shared popover deliberately leaves to the