@gjsify/adwaita-web 0.10.0 → 0.12.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 (120) hide show
  1. package/README.md +5 -5
  2. package/dist/adwaita-web.css +2742 -263
  3. package/dist/adwaita-web.css.map +1 -1
  4. package/lib/types/elements/adw-about-dialog.d.ts +77 -0
  5. package/lib/types/elements/adw-action-row.d.ts +15 -0
  6. package/lib/types/elements/adw-alert-dialog.d.ts +80 -0
  7. package/lib/types/elements/adw-avatar.d.ts +9 -0
  8. package/lib/types/elements/adw-banner.d.ts +9 -0
  9. package/lib/types/elements/adw-bottom-sheet.d.ts +35 -0
  10. package/lib/types/elements/adw-button-content.d.ts +10 -0
  11. package/lib/types/elements/adw-button-row.d.ts +11 -0
  12. package/lib/types/elements/adw-button.d.ts +11 -0
  13. package/lib/types/elements/adw-carousel.d.ts +64 -0
  14. package/lib/types/elements/adw-clamp.d.ts +6 -0
  15. package/lib/types/elements/adw-combo-row.d.ts +3 -0
  16. package/lib/types/elements/adw-entry-row.d.ts +10 -0
  17. package/lib/types/elements/adw-entry.d.ts +11 -0
  18. package/lib/types/elements/adw-expander-row.d.ts +26 -0
  19. package/lib/types/elements/adw-header-bar.d.ts +4 -0
  20. package/lib/types/elements/adw-inline-view-switcher.d.ts +30 -0
  21. package/lib/types/elements/adw-navigation-split-view.d.ts +15 -0
  22. package/lib/types/elements/adw-navigation-view.d.ts +66 -0
  23. package/lib/types/elements/adw-password-entry-row.d.ts +16 -0
  24. package/lib/types/elements/adw-preferences-dialog.d.ts +36 -0
  25. package/lib/types/elements/adw-preferences-group.d.ts +8 -0
  26. package/lib/types/elements/adw-sidebar.d.ts +29 -0
  27. package/lib/types/elements/adw-spin-row.d.ts +3 -0
  28. package/lib/types/elements/adw-spinner.d.ts +6 -0
  29. package/lib/types/elements/adw-split-button.d.ts +18 -0
  30. package/lib/types/elements/adw-status-page.d.ts +11 -0
  31. package/lib/types/elements/adw-switch-row.d.ts +3 -0
  32. package/lib/types/elements/adw-tab-view.d.ts +24 -0
  33. package/lib/types/elements/adw-toast-overlay.d.ts +19 -4
  34. package/lib/types/elements/adw-toggle-group.d.ts +19 -0
  35. package/lib/types/elements/adw-toolbar-view.d.ts +13 -0
  36. package/lib/types/elements/adw-view-switcher.d.ts +29 -0
  37. package/lib/types/elements/adw-window-title.d.ts +9 -0
  38. package/lib/types/elements/adw-wrap-box.d.ts +13 -0
  39. package/lib/types/index.d.ts +29 -0
  40. package/lib/types/styles.generated.d.ts +1 -0
  41. package/package.json +5 -5
  42. package/scss/_about_dialog.scss +257 -0
  43. package/scss/_action_row.scss +65 -0
  44. package/scss/_alert_dialog.scss +151 -0
  45. package/scss/_avatar.scss +31 -0
  46. package/scss/_banner.scss +46 -0
  47. package/scss/_bottom_sheet.scss +113 -0
  48. package/scss/_button.scss +168 -0
  49. package/scss/_button_content.scss +48 -0
  50. package/scss/_button_row.scss +100 -0
  51. package/scss/_carousel.scss +130 -0
  52. package/scss/_clamp.scss +11 -0
  53. package/scss/_entry.scss +49 -0
  54. package/scss/_entry_row.scss +54 -0
  55. package/scss/_expander_row.scss +150 -0
  56. package/scss/_headerbar.scss +1 -1
  57. package/scss/_inline_view_switcher.scss +174 -0
  58. package/scss/_navigation_split_view.scss +48 -0
  59. package/scss/_navigation_view.scss +80 -0
  60. package/scss/_overlay_split_view.scss +19 -0
  61. package/scss/_password_entry_row.scss +67 -0
  62. package/scss/_preferences.scss +26 -3
  63. package/scss/_preferences_dialog.scss +154 -0
  64. package/scss/_row.scss +31 -0
  65. package/scss/_sidebar.scss +213 -0
  66. package/scss/_spin_row.scss +0 -1
  67. package/scss/_spinner.scss +28 -0
  68. package/scss/_split_button.scss +222 -0
  69. package/scss/_status_page.scss +40 -0
  70. package/scss/_switch_row.scss +0 -1
  71. package/scss/_tab_view.scss +193 -0
  72. package/scss/_theme.scss +12 -0
  73. package/scss/_toast.scss +122 -12
  74. package/scss/_toggle_group.scss +124 -0
  75. package/scss/_toolbar_view.scss +31 -0
  76. package/scss/_variables.scss +19 -0
  77. package/scss/_view_switcher.scss +133 -0
  78. package/scss/_window_title.scss +33 -0
  79. package/scss/_wrap_box.scss +20 -0
  80. package/scss/adwaita-skin.scss +38 -0
  81. package/src/elements/adw-about-dialog.ts +629 -0
  82. package/src/elements/adw-action-row.ts +82 -0
  83. package/src/elements/adw-alert-dialog.ts +385 -0
  84. package/src/elements/adw-avatar.ts +108 -0
  85. package/src/elements/adw-banner.ts +57 -0
  86. package/src/elements/adw-bottom-sheet.ts +205 -0
  87. package/src/elements/adw-button-content.ts +69 -0
  88. package/src/elements/adw-button-row.ts +75 -0
  89. package/src/elements/adw-button.ts +81 -0
  90. package/src/elements/adw-carousel.ts +321 -0
  91. package/src/elements/adw-clamp.ts +28 -0
  92. package/src/elements/adw-combo-row.ts +32 -20
  93. package/src/elements/adw-entry-row.ts +69 -0
  94. package/src/elements/adw-entry.ts +61 -0
  95. package/src/elements/adw-expander-row.ts +167 -0
  96. package/src/elements/adw-header-bar.ts +21 -8
  97. package/src/elements/adw-inline-view-switcher.ts +222 -0
  98. package/src/elements/adw-navigation-split-view.ts +89 -0
  99. package/src/elements/adw-navigation-view.ts +344 -0
  100. package/src/elements/adw-password-entry-row.ts +121 -0
  101. package/src/elements/adw-preferences-dialog.ts +240 -0
  102. package/src/elements/adw-preferences-group.ts +79 -12
  103. package/src/elements/adw-sidebar.ts +238 -0
  104. package/src/elements/adw-spin-row.ts +28 -14
  105. package/src/elements/adw-spinner.ts +31 -0
  106. package/src/elements/adw-split-button.ts +206 -0
  107. package/src/elements/adw-status-page.ts +66 -0
  108. package/src/elements/adw-switch-row.ts +29 -23
  109. package/src/elements/adw-tab-view.ts +184 -0
  110. package/src/elements/adw-toast-overlay.ts +98 -19
  111. package/src/elements/adw-toggle-group.ts +126 -0
  112. package/src/elements/adw-toolbar-view.ts +60 -0
  113. package/src/elements/adw-view-switcher.ts +200 -0
  114. package/src/elements/adw-window-title.ts +48 -0
  115. package/src/elements/adw-wrap-box.ts +139 -0
  116. package/src/index.js +1 -1
  117. package/src/index.ts +46 -3
  118. package/src/styles.generated.js +2976 -0
  119. package/src/styles2.generated.js +2976 -0
  120. package/style.css.d.ts +17 -4
@@ -1,12 +1,15 @@
1
- // <adw-spin-row> — Row with a label and numeric spin control (+/− buttons).
2
- // Attributes: title, min, max, step, value
1
+ // <adw-spin-row> — Row with a title/subtitle and a numeric spin control (+/− buttons).
2
+ // Attributes: title, subtitle, min, max, step, value
3
3
  // Events: notify::value (CustomEvent, mirrors GJS GObject signal naming)
4
4
  // Adapted from Adwaita Web UI Framework (https://github.com/mclellac/adwaita-web).
5
5
  // Copyright (c) 2025 csm. MIT License.
6
- // Modifications: Reimplemented as Web Component for @gjsify/adwaita-web.
6
+ // Modifications: Reimplemented as Web Component for @gjsify/adwaita-web;
7
+ // title/subtitle text column added to match Adw.SpinRow.
7
8
 
8
9
  export class AdwSpinRow extends HTMLElement {
9
10
  private _input!: HTMLInputElement;
11
+ private _titleEl!: HTMLSpanElement;
12
+ private _subtitleEl!: HTMLSpanElement;
10
13
  private _min = 0;
11
14
  private _max = 100;
12
15
  private _step = 1;
@@ -14,7 +17,7 @@ export class AdwSpinRow extends HTMLElement {
14
17
  private _initialized = false;
15
18
 
16
19
  static get observedAttributes() {
17
- return ['value', 'min', 'max', 'step'];
20
+ return ['title', 'subtitle', 'value', 'min', 'max', 'step'];
18
21
  }
19
22
 
20
23
  get value(): number {
@@ -36,34 +39,34 @@ export class AdwSpinRow extends HTMLElement {
36
39
  if (this._initialized) return;
37
40
  this._initialized = true;
38
41
 
39
- const title = this.getAttribute('title') || '';
40
42
  this._min = parseFloat(this.getAttribute('min') || '0');
41
43
  this._max = parseFloat(this.getAttribute('max') || '100');
42
44
  this._step = parseFloat(this.getAttribute('step') || '1');
43
45
  this._value = parseFloat(this.getAttribute('value') || String(this._min));
44
46
 
45
- // Title
46
- const titleEl = document.createElement('span');
47
- titleEl.className = 'adw-row-title';
48
- titleEl.textContent = title;
47
+ const text = document.createElement('div');
48
+ text.className = 'adw-row-text';
49
+ this._titleEl = document.createElement('span');
50
+ this._titleEl.className = 'adw-row-title';
51
+ this._subtitleEl = document.createElement('span');
52
+ this._subtitleEl.className = 'adw-row-subtitle';
53
+ text.append(this._titleEl, this._subtitleEl);
49
54
 
50
55
  // Spin control container
51
56
  const control = document.createElement('div');
52
57
  control.className = 'adw-spin-control';
53
58
 
54
- // Decrement button
55
59
  const decBtn = document.createElement('button');
56
60
  decBtn.className = 'adw-spin-dec';
57
61
  decBtn.textContent = '−';
58
62
  decBtn.addEventListener('click', () => this._adjust(-this._step));
59
63
 
60
- // Value input
61
64
  const input = document.createElement('input');
62
65
  input.type = 'text';
63
66
  input.value = this._formatValue(this._value);
64
67
  input.addEventListener('change', () => {
65
68
  const parsed = parseFloat(input.value);
66
- if (!isNaN(parsed)) {
69
+ if (!Number.isNaN(parsed)) {
67
70
  this.value = parsed;
68
71
  this._emitChange();
69
72
  } else {
@@ -71,19 +74,23 @@ export class AdwSpinRow extends HTMLElement {
71
74
  }
72
75
  });
73
76
 
74
- // Increment button
75
77
  const incBtn = document.createElement('button');
76
78
  incBtn.className = 'adw-spin-inc';
77
79
  incBtn.textContent = '+';
78
80
  incBtn.addEventListener('click', () => this._adjust(this._step));
79
81
 
80
82
  control.append(decBtn, input, incBtn);
81
- this.append(titleEl, control);
83
+ this.replaceChildren(text, control);
82
84
  this._input = input;
85
+ this._renderText();
83
86
  }
84
87
 
85
88
  attributeChangedCallback(name: string, _old: string | null, val: string | null) {
86
89
  if (!this._initialized) return;
90
+ if (name === 'title' || name === 'subtitle') {
91
+ this._renderText();
92
+ return;
93
+ }
87
94
  const num = parseFloat(val || '0');
88
95
  switch (name) {
89
96
  case 'value':
@@ -104,6 +111,13 @@ export class AdwSpinRow extends HTMLElement {
104
111
  }
105
112
  }
106
113
 
114
+ private _renderText() {
115
+ this._titleEl.textContent = this.getAttribute('title') ?? '';
116
+ const subtitle = this.getAttribute('subtitle') ?? '';
117
+ this._subtitleEl.textContent = subtitle;
118
+ this._subtitleEl.hidden = subtitle.length === 0;
119
+ }
120
+
107
121
  private _adjust(delta: number) {
108
122
  this.value = this._value + delta;
109
123
  this._emitChange();
@@ -0,0 +1,31 @@
1
+ // <adw-spinner> — A continuously-spinning loading indicator.
2
+ // Attributes: size (px, default 24).
3
+ // Reference: refs/adwaita-web/adwaita-web/scss/_spinner.scss
4
+ // Reference: refs/libadwaita (Adw.Spinner)
5
+ // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
6
+ // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
7
+
8
+ export class AdwSpinner extends HTMLElement {
9
+ static get observedAttributes() {
10
+ return ['size'];
11
+ }
12
+
13
+ connectedCallback() {
14
+ this._sync();
15
+ }
16
+
17
+ attributeChangedCallback() {
18
+ this._sync();
19
+ }
20
+
21
+ private _sync() {
22
+ const size = parseFloat(this.getAttribute('size') || '24');
23
+ this.style.width = `${size}px`;
24
+ this.style.height = `${size}px`;
25
+ // Scale the ring thickness with the diameter (libadwaita's arc thickens
26
+ // with size); clamp so tiny spinners stay visible.
27
+ this.style.borderWidth = `${Math.max(2, Math.round(size / 12))}px`;
28
+ }
29
+ }
30
+
31
+ customElements.define('adw-spinner', AdwSpinner);
@@ -0,0 +1,206 @@
1
+ // <adw-split-button> — Adwaita split button: a primary action button paired
2
+ // with an attached dropdown that opens a menu of related actions. The two
3
+ // halves are separated by a thin divider; the .suggested-action /
4
+ // .destructive-action / .flat style classes recolor the whole control like the
5
+ // matching Adwaita button variants.
6
+ // Attributes: label, icon-name (symbolic name, e.g. "document-save"; an icon
7
+ // without a label collapses the action half to icon-only), tooltip,
8
+ // dropdown-tooltip, disabled, and the boolean variant flags
9
+ // suggested / destructive / flat.
10
+ // Menu entries are provided as a JSON array on the `menu` attribute, each
11
+ // entry `{ "label": "Save as…", "action": "app.save-as" }`.
12
+ // Events (all CustomEvent, bubbles — mirror the Adw.SplitButton GObject signals):
13
+ // `clicked` — the primary action half was activated.
14
+ // `notify::active` — the dropdown menu was opened/closed (detail.active).
15
+ // `menu-activated` — a menu entry was chosen (detail.action / detail.label).
16
+ // Reference: refs/libadwaita/src/adw-split-button.c (AdwSplitButton)
17
+ // Reference: refs/adwaita-web/adwaita-web/scss/_split_button.scss
18
+ // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
19
+ // Copyright (c) 2025 csm (adwaita-web). MIT License.
20
+ // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
21
+
22
+ const VARIANT_CLASSES: Record<string, string> = {
23
+ flat: 'flat',
24
+ suggested: 'suggested-action',
25
+ destructive: 'destructive-action',
26
+ };
27
+
28
+ interface MenuEntry {
29
+ label: string;
30
+ action?: string;
31
+ }
32
+
33
+ export class AdwSplitButton extends HTMLElement {
34
+ private _actionEl!: HTMLButtonElement;
35
+ private _dropdownEl!: HTMLButtonElement;
36
+ private _menuEl!: HTMLDivElement;
37
+ private _label = '';
38
+ private _active = false;
39
+ private _initialized = false;
40
+ private _onDocumentPointerDown = (event: Event): void => {
41
+ if (!this.contains(event.target as Node)) this._setActive(false);
42
+ };
43
+
44
+ static get observedAttributes() {
45
+ return [
46
+ 'label',
47
+ 'icon-name',
48
+ 'tooltip',
49
+ 'dropdown-tooltip',
50
+ 'menu',
51
+ 'disabled',
52
+ 'flat',
53
+ 'suggested',
54
+ 'destructive',
55
+ ];
56
+ }
57
+
58
+ /** Whether the dropdown menu is currently open. */
59
+ get active(): boolean {
60
+ return this._active;
61
+ }
62
+
63
+ connectedCallback() {
64
+ if (this._initialized) return;
65
+ this._initialized = true;
66
+
67
+ // Capture any inline text as the label before we take over the subtree.
68
+ this._label = (this.getAttribute('label') ?? this.textContent ?? '').trim();
69
+
70
+ this.classList.add('adw-split-button');
71
+
72
+ this._actionEl = document.createElement('button');
73
+ this._actionEl.type = 'button';
74
+ this._actionEl.className = 'adw-split-button-action';
75
+
76
+ this._dropdownEl = document.createElement('button');
77
+ this._dropdownEl.type = 'button';
78
+ this._dropdownEl.className = 'adw-split-button-dropdown';
79
+ const arrow = document.createElement('span');
80
+ arrow.className = 'adw-icon adw-icon--go-down';
81
+ arrow.setAttribute('aria-hidden', 'true');
82
+ this._dropdownEl.appendChild(arrow);
83
+
84
+ this._menuEl = document.createElement('div');
85
+ this._menuEl.className = 'adw-split-button-menu';
86
+ this._menuEl.hidden = true;
87
+
88
+ this.replaceChildren(this._actionEl, this._dropdownEl, this._menuEl);
89
+
90
+ this._actionEl.addEventListener('click', () => {
91
+ if (this.hasAttribute('disabled')) return;
92
+ this.dispatchEvent(new CustomEvent('clicked', { bubbles: true }));
93
+ });
94
+ this._dropdownEl.addEventListener('click', () => {
95
+ if (this.hasAttribute('disabled')) return;
96
+ this._setActive(!this._active);
97
+ });
98
+
99
+ this._render();
100
+ }
101
+
102
+ disconnectedCallback() {
103
+ document.removeEventListener('pointerdown', this._onDocumentPointerDown);
104
+ }
105
+
106
+ attributeChangedCallback() {
107
+ if (this._initialized) this._render();
108
+ }
109
+
110
+ private _setActive(active: boolean): void {
111
+ if (this._active === active) return;
112
+ this._active = active;
113
+ this._menuEl.hidden = !active;
114
+ this.classList.toggle('active', active);
115
+ this._dropdownEl.setAttribute('aria-expanded', String(active));
116
+ if (active) {
117
+ document.addEventListener('pointerdown', this._onDocumentPointerDown);
118
+ } else {
119
+ document.removeEventListener('pointerdown', this._onDocumentPointerDown);
120
+ }
121
+ this.dispatchEvent(new CustomEvent('notify::active', { bubbles: true, detail: { active } }));
122
+ }
123
+
124
+ private _parseMenu(): MenuEntry[] {
125
+ const raw = this.getAttribute('menu');
126
+ if (!raw) return [];
127
+ try {
128
+ const parsed: unknown = JSON.parse(raw);
129
+ if (!Array.isArray(parsed)) return [];
130
+ return parsed
131
+ .filter((entry): entry is MenuEntry => typeof entry === 'object' && entry !== null && 'label' in entry)
132
+ .map((entry) => ({ label: String(entry.label), action: entry.action }));
133
+ } catch {
134
+ return [];
135
+ }
136
+ }
137
+
138
+ private _render() {
139
+ const disabled = this.hasAttribute('disabled');
140
+ this.classList.toggle('disabled', disabled);
141
+ this._actionEl.disabled = disabled;
142
+ this._dropdownEl.disabled = disabled;
143
+
144
+ this.classList.remove('flat', 'suggested-action', 'destructive-action');
145
+ for (const [attr, cls] of Object.entries(VARIANT_CLASSES)) {
146
+ if (this.hasAttribute(attr)) this.classList.add(cls);
147
+ }
148
+
149
+ // --- Primary action half ---
150
+ const icon = this.getAttribute('icon-name');
151
+ const label = (this.getAttribute('label') ?? this._label).trim();
152
+ this._actionEl.classList.toggle('icon-only', Boolean(icon) && !label);
153
+
154
+ this._actionEl.replaceChildren();
155
+ if (icon) {
156
+ const span = document.createElement('span');
157
+ span.className = `adw-icon adw-icon--${icon}`;
158
+ span.setAttribute('aria-hidden', 'true');
159
+ this._actionEl.appendChild(span);
160
+ }
161
+ if (label) this._actionEl.appendChild(document.createTextNode(label));
162
+
163
+ const tooltip = this.getAttribute('tooltip');
164
+ this._actionEl.title = tooltip ?? '';
165
+ // An icon-only action half has no text, so give it an accessible name —
166
+ // prefer the tooltip, fall back to the symbolic icon name (WCAG 4.1.2).
167
+ if (icon && !label) {
168
+ this._actionEl.setAttribute('aria-label', tooltip ?? icon);
169
+ } else {
170
+ this._actionEl.removeAttribute('aria-label');
171
+ }
172
+
173
+ // --- Dropdown half ---
174
+ const dropdownTooltip = this.getAttribute('dropdown-tooltip');
175
+ this._dropdownEl.title = dropdownTooltip ?? '';
176
+ this._dropdownEl.setAttribute('aria-label', dropdownTooltip ?? 'Menu');
177
+ this._dropdownEl.setAttribute('aria-haspopup', 'menu');
178
+ this._dropdownEl.setAttribute('aria-expanded', String(this._active));
179
+
180
+ // --- Menu ---
181
+ const entries = this._parseMenu();
182
+ this._menuEl.replaceChildren();
183
+ this._menuEl.setAttribute('role', 'menu');
184
+ for (const entry of entries) {
185
+ const item = document.createElement('button');
186
+ item.type = 'button';
187
+ item.className = 'adw-split-button-menu-item';
188
+ item.setAttribute('role', 'menuitem');
189
+ item.textContent = entry.label;
190
+ item.addEventListener('click', () => {
191
+ this._setActive(false);
192
+ this.dispatchEvent(
193
+ new CustomEvent('menu-activated', {
194
+ bubbles: true,
195
+ detail: { label: entry.label, action: entry.action },
196
+ }),
197
+ );
198
+ });
199
+ this._menuEl.appendChild(item);
200
+ }
201
+ // A split button with no menu entries hides its (empty) popover entirely.
202
+ if (entries.length === 0) this._setActive(false);
203
+ }
204
+ }
205
+
206
+ customElements.define('adw-split-button', AdwSplitButton);
@@ -0,0 +1,66 @@
1
+ // <adw-status-page> — A centered empty/placeholder state: a large symbolic
2
+ // icon, a title, a description and an optional action (slotted child).
3
+ // Attributes: icon (symbolic name, with or without -symbolic), title, description.
4
+ // Reference: refs/adwaita-web/adwaita-web/scss/_status_page.scss
5
+ // Reference: refs/libadwaita/src/stylesheet/widgets/_status-page.scss
6
+ // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
7
+ // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
8
+
9
+ export class AdwStatusPage extends HTMLElement {
10
+ private _iconEl!: HTMLSpanElement;
11
+ private _titleEl!: HTMLSpanElement;
12
+ private _descEl!: HTMLSpanElement;
13
+ private _childEl!: HTMLDivElement;
14
+ private _initialized = false;
15
+
16
+ static get observedAttributes() {
17
+ return ['icon', 'title', 'description'];
18
+ }
19
+
20
+ connectedCallback() {
21
+ if (this._initialized) return;
22
+ this._initialized = true;
23
+
24
+ const children = Array.from(this.childNodes);
25
+
26
+ this._iconEl = document.createElement('span');
27
+ this._iconEl.className = 'adw-status-page-icon adw-icon';
28
+ this._iconEl.setAttribute('aria-hidden', 'true');
29
+
30
+ this._titleEl = document.createElement('span');
31
+ this._titleEl.className = 'adw-status-page-title';
32
+
33
+ this._descEl = document.createElement('span');
34
+ this._descEl.className = 'adw-status-page-description';
35
+
36
+ this._childEl = document.createElement('div');
37
+ this._childEl.className = 'adw-status-page-child';
38
+ for (const child of children) this._childEl.appendChild(child);
39
+
40
+ this.replaceChildren(this._iconEl, this._titleEl, this._descEl, this._childEl);
41
+ this._render();
42
+ }
43
+
44
+ attributeChangedCallback() {
45
+ if (this._initialized) this._render();
46
+ }
47
+
48
+ private _render() {
49
+ const icon = (this.getAttribute('icon') ?? '').replace(/-symbolic$/, '');
50
+ // Swap the icon mask class (keep the base + size class).
51
+ this._iconEl.className = `adw-status-page-icon adw-icon${icon ? ` adw-icon--${icon}` : ''}`;
52
+ this._iconEl.hidden = icon.length === 0;
53
+
54
+ const title = this.getAttribute('title') ?? '';
55
+ this._titleEl.textContent = title;
56
+ this._titleEl.hidden = title.length === 0;
57
+
58
+ const description = this.getAttribute('description') ?? '';
59
+ this._descEl.textContent = description;
60
+ this._descEl.hidden = description.length === 0;
61
+
62
+ this._childEl.hidden = this._childEl.childElementCount === 0;
63
+ }
64
+ }
65
+
66
+ customElements.define('adw-status-page', AdwStatusPage);
@@ -1,16 +1,19 @@
1
- // <adw-switch-row> — Row with a label and toggle switch.
2
- // Attributes: title, active (boolean)
1
+ // <adw-switch-row> — Row with a title/subtitle and a trailing toggle switch.
2
+ // Attributes: title, subtitle, active (boolean)
3
3
  // Events: notify::active (CustomEvent, mirrors GJS GObject signal naming)
4
4
  // Adapted from Adwaita Web UI Framework (https://github.com/mclellac/adwaita-web).
5
5
  // Copyright (c) 2025 csm. MIT License.
6
- // Modifications: Reimplemented as Web Component for @gjsify/adwaita-web.
6
+ // Modifications: Reimplemented as Web Component for @gjsify/adwaita-web;
7
+ // title/subtitle text column added to match Adw.SwitchRow.
7
8
 
8
9
  export class AdwSwitchRow extends HTMLElement {
9
10
  private _checkbox!: HTMLInputElement;
11
+ private _titleEl!: HTMLSpanElement;
12
+ private _subtitleEl!: HTMLSpanElement;
10
13
  private _initialized = false;
11
14
 
12
15
  static get observedAttributes() {
13
- return ['active'];
16
+ return ['title', 'subtitle', 'active'];
14
17
  }
15
18
 
16
19
  get active(): boolean {
@@ -26,30 +29,24 @@ export class AdwSwitchRow extends HTMLElement {
26
29
  if (this._initialized) return;
27
30
  this._initialized = true;
28
31
 
29
- const title = this.getAttribute('title') || '';
30
- const checked = this.hasAttribute('active');
32
+ const text = document.createElement('div');
33
+ text.className = 'adw-row-text';
34
+ this._titleEl = document.createElement('span');
35
+ this._titleEl.className = 'adw-row-title';
36
+ this._subtitleEl = document.createElement('span');
37
+ this._subtitleEl.className = 'adw-row-subtitle';
38
+ text.append(this._titleEl, this._subtitleEl);
31
39
 
32
- // Title
33
- const titleEl = document.createElement('span');
34
- titleEl.className = 'adw-row-title';
35
- titleEl.textContent = title;
36
-
37
- // Switch label with hidden checkbox + slider
38
40
  const label = document.createElement('label');
39
41
  label.className = 'adw-switch';
40
-
41
42
  const input = document.createElement('input');
42
43
  input.type = 'checkbox';
43
- input.checked = checked;
44
-
44
+ input.checked = this.hasAttribute('active');
45
45
  const slider = document.createElement('span');
46
46
  slider.className = 'adw-switch-slider';
47
+ label.append(input, slider);
47
48
 
48
- label.appendChild(input);
49
- label.appendChild(slider);
50
-
51
- this.appendChild(titleEl);
52
- this.appendChild(label);
49
+ this.replaceChildren(text, label);
53
50
 
54
51
  this._checkbox = input;
55
52
  this._checkbox.addEventListener('change', () => {
@@ -61,12 +58,21 @@ export class AdwSwitchRow extends HTMLElement {
61
58
  }),
62
59
  );
63
60
  });
61
+
62
+ this._renderText();
64
63
  }
65
64
 
66
65
  attributeChangedCallback(name: string, _old: string | null, value: string | null) {
67
- if (name === 'active' && this._checkbox) {
68
- this._checkbox.checked = value !== null;
69
- }
66
+ if (!this._initialized) return;
67
+ if (name === 'active') this._checkbox.checked = value !== null;
68
+ else this._renderText();
69
+ }
70
+
71
+ private _renderText() {
72
+ this._titleEl.textContent = this.getAttribute('title') ?? '';
73
+ const subtitle = this.getAttribute('subtitle') ?? '';
74
+ this._subtitleEl.textContent = subtitle;
75
+ this._subtitleEl.hidden = subtitle.length === 0;
70
76
  }
71
77
  }
72
78