@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
@@ -0,0 +1,82 @@
1
+ // <adw-action-row> — A boxed-list row with a title, optional subtitle, and
2
+ // prefix / suffix widget slots. The most fundamental Adwaita row type.
3
+ // Attributes: title, subtitle, activatable.
4
+ // Slots: slot="prefix" (icons/widgets before the text), slot="suffix" (controls
5
+ // after the text — switches, buttons, value labels).
6
+ // Events: `activated` (CustomEvent) when an `activatable` row is clicked.
7
+ // Reference: refs/adwaita-web/adwaita-web/docs/widgets/actionrow.md
8
+ // Reference: refs/libadwaita/src/stylesheet/widgets/_lists.scss (.row styling)
9
+ // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
10
+ // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
11
+
12
+ export class AdwActionRow extends HTMLElement {
13
+ private _titleEl!: HTMLSpanElement;
14
+ private _subtitleEl!: HTMLSpanElement;
15
+ private _suffixEl!: HTMLDivElement;
16
+ private _prefixEl!: HTMLDivElement;
17
+ private _initialized = false;
18
+
19
+ static get observedAttributes() {
20
+ return ['title', 'subtitle', 'activatable'];
21
+ }
22
+
23
+ /** The end (suffix) section — append controls here imperatively. */
24
+ get suffixSection(): HTMLDivElement {
25
+ return this._suffixEl;
26
+ }
27
+
28
+ /** The start (prefix) section — append icons/widgets here imperatively. */
29
+ get prefixSection(): HTMLDivElement {
30
+ return this._prefixEl;
31
+ }
32
+
33
+ connectedCallback() {
34
+ if (this._initialized) return;
35
+ this._initialized = true;
36
+
37
+ const prefixChildren = Array.from(this.querySelectorAll(':scope > [slot="prefix"]'));
38
+ const suffixChildren = Array.from(this.querySelectorAll(':scope > [slot="suffix"]'));
39
+
40
+ this._prefixEl = document.createElement('div');
41
+ this._prefixEl.className = 'adw-action-row-prefix';
42
+ for (const child of prefixChildren) this._prefixEl.appendChild(child);
43
+
44
+ const textEl = document.createElement('div');
45
+ textEl.className = 'adw-action-row-text';
46
+ this._titleEl = document.createElement('span');
47
+ this._titleEl.className = 'adw-row-title';
48
+ this._subtitleEl = document.createElement('span');
49
+ this._subtitleEl.className = 'adw-row-subtitle';
50
+ textEl.append(this._titleEl, this._subtitleEl);
51
+
52
+ this._suffixEl = document.createElement('div');
53
+ this._suffixEl.className = 'adw-action-row-suffix';
54
+ for (const child of suffixChildren) this._suffixEl.appendChild(child);
55
+
56
+ this.replaceChildren(this._prefixEl, textEl, this._suffixEl);
57
+ this._render();
58
+
59
+ this.addEventListener('click', () => {
60
+ if (this.hasAttribute('activatable')) {
61
+ this.dispatchEvent(new CustomEvent('activated', { bubbles: true }));
62
+ }
63
+ });
64
+ }
65
+
66
+ attributeChangedCallback() {
67
+ if (this._initialized) this._render();
68
+ }
69
+
70
+ private _render() {
71
+ const title = this.getAttribute('title') ?? '';
72
+ const subtitle = this.getAttribute('subtitle') ?? '';
73
+ this._titleEl.textContent = title;
74
+ this._subtitleEl.textContent = subtitle;
75
+ this._subtitleEl.hidden = subtitle.length === 0;
76
+ this._prefixEl.hidden = this._prefixEl.childElementCount === 0;
77
+ this._suffixEl.hidden = this._suffixEl.childElementCount === 0;
78
+ this.classList.toggle('activatable', this.hasAttribute('activatable'));
79
+ }
80
+ }
81
+
82
+ customElements.define('adw-action-row', AdwActionRow);
@@ -0,0 +1,385 @@
1
+ // <adw-alert-dialog> — A modal dialog presenting a message or a question (the
2
+ // web counterpart of Adw.AlertDialog). It renders a fixed full-cover scrim with
3
+ // a centred dialog box on top, holding a bold heading, body text, an optional
4
+ // extra-child slot, and one or more response buttons. Each response is a button
5
+ // with a unique string ID and a label; one may carry the suggested or
6
+ // destructive appearance.
7
+ //
8
+ // Responses are declared imperatively via add_response() / addResponse() (the
9
+ // Adw.AlertDialog API), or via <adw-alert-response> children consumed at connect
10
+ // time. The dialog is hidden until `present()` (or setting the `open` attribute)
11
+ // reveals it; Escape / scrim-click dismiss it — respecting the `close-response`
12
+ // — unless can-close is suppressed by the close response.
13
+ //
14
+ // Attributes:
15
+ // heading (the bold heading text)
16
+ // body (the body text below the heading)
17
+ // open (boolean — whether the dialog is shown; default closed)
18
+ // prefer-wide-layout (boolean — lay responses out horizontally when they fit)
19
+ //
20
+ // Buttons are stacked vertically by default (the Adw.AlertDialog default at
21
+ // medium sizes) and laid out horizontally when prefer-wide-layout is set and
22
+ // there are no more than two of them — mirroring the libadwaita measure/allocate
23
+ // heuristic in spirit.
24
+ //
25
+ // Events:
26
+ // `response` (CustomEvent, bubbles, `detail = { response }`) when a response
27
+ // button is activated OR the dialog is dismissed (then `response` is the
28
+ // close-response ID) — mirrors the Adw.AlertDialog::response signal.
29
+ // `notify::heading` / `notify::body` / `notify::open` (CustomEvent, bubbles)
30
+ // mirroring the matching GObject properties.
31
+ //
32
+ // Reference: refs/libadwaita/src/adw-alert-dialog.c (AdwAlertDialog behaviour)
33
+ // Reference: refs/libadwaita/src/adw-dialog.c (shared dialog base — Escape/close)
34
+ // Reference: refs/libadwaita/src/stylesheet/widgets/_dialogs.scss (floating-sheet)
35
+ // Reference: refs/adwaita-web/adwaita-web/scss/_dialog.scss (alert layout)
36
+ // Copyright (c) 2022 Purism SPC / 2024 GNOME Foundation Inc. (libadwaita). LGPLv2.1+.
37
+ // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
38
+
39
+ /** Response button appearance — mirrors Adw.ResponseAppearance. */
40
+ export type AdwResponseAppearance = 'default' | 'suggested' | 'destructive';
41
+
42
+ interface ResponseInfo {
43
+ id: string;
44
+ label: string;
45
+ appearance: AdwResponseAppearance;
46
+ enabled: boolean;
47
+ button: HTMLButtonElement;
48
+ }
49
+
50
+ /**
51
+ * A single response declared in markup. Child of <adw-alert-dialog>; its `id`
52
+ * attribute is the response ID, its text content the label. Optional
53
+ * `appearance="suggested|destructive"` and `enabled="false"` mirror the
54
+ * GtkBuildable `<response>` element attributes. Consumed at connect time.
55
+ */
56
+ export class AdwAlertResponse extends HTMLElement {
57
+ static get observedAttributes() {
58
+ return ['id', 'appearance', 'enabled'];
59
+ }
60
+ }
61
+
62
+ export class AdwAlertDialog extends HTMLElement {
63
+ private _initialized = false;
64
+ private _scrimEl!: HTMLDivElement;
65
+ private _dialogEl!: HTMLDivElement;
66
+ private _headingEl!: HTMLHeadingElement;
67
+ private _bodyEl!: HTMLParagraphElement;
68
+ private _childEl!: HTMLDivElement;
69
+ private _responseAreaEl!: HTMLDivElement;
70
+
71
+ private _responses: ResponseInfo[] = [];
72
+ private _byId = new Map<string, ResponseInfo>();
73
+ private _defaultResponse: string | null = null;
74
+ private _closeResponse = 'close';
75
+ // Set while a response is being emitted from a button so the close handler
76
+ // does not also fire the close-response (mirrors AdwAlertDialog's
77
+ // block_close_response flag).
78
+ private _blockCloseResponse = false;
79
+
80
+ static get observedAttributes() {
81
+ return ['heading', 'body', 'open', 'prefer-wide-layout'];
82
+ }
83
+
84
+ /** The heading of the dialog. */
85
+ get heading(): string {
86
+ return this.getAttribute('heading') ?? '';
87
+ }
88
+
89
+ set heading(value: string) {
90
+ this.setAttribute('heading', value);
91
+ }
92
+
93
+ /** The body text of the dialog. */
94
+ get body(): string {
95
+ return this.getAttribute('body') ?? '';
96
+ }
97
+
98
+ set body(value: string) {
99
+ this.setAttribute('body', value);
100
+ }
101
+
102
+ /** Whether the dialog is currently shown. */
103
+ get open(): boolean {
104
+ return this.hasAttribute('open');
105
+ }
106
+
107
+ set open(value: boolean) {
108
+ if (value) this.setAttribute('open', '');
109
+ else this.removeAttribute('open');
110
+ }
111
+
112
+ /** Whether to prefer horizontal button layout when the buttons fit. */
113
+ get preferWideLayout(): boolean {
114
+ return this.hasAttribute('prefer-wide-layout');
115
+ }
116
+
117
+ set preferWideLayout(value: boolean) {
118
+ if (value) this.setAttribute('prefer-wide-layout', '');
119
+ else this.removeAttribute('prefer-wide-layout');
120
+ }
121
+
122
+ /** The ID of the default response (its button is highlighted). */
123
+ get defaultResponse(): string | null {
124
+ return this._defaultResponse;
125
+ }
126
+
127
+ set defaultResponse(value: string | null) {
128
+ this.setDefaultResponse(value);
129
+ }
130
+
131
+ /** The ID passed to `response` when the dialog is dismissed. */
132
+ get closeResponse(): string {
133
+ return this._closeResponse;
134
+ }
135
+
136
+ set closeResponse(value: string) {
137
+ this.setCloseResponse(value);
138
+ }
139
+
140
+ connectedCallback() {
141
+ if (this._initialized) return;
142
+ this._initialized = true;
143
+
144
+ // Snapshot any declared <adw-alert-response> children before we take
145
+ // over the subtree; everything else becomes the extra child.
146
+ const declared = Array.from(this.querySelectorAll(':scope > adw-alert-response')) as AdwAlertResponse[];
147
+ const declaredSet = new Set<Node>(declared);
148
+ const extraChildren = Array.from(this.childNodes).filter((n) => !declaredSet.has(n));
149
+
150
+ // The fixed full-cover scrim. Clicking it dismisses the dialog.
151
+ this._scrimEl = document.createElement('div');
152
+ this._scrimEl.className = 'adw-alert-dialog-scrim';
153
+ this._scrimEl.addEventListener('click', () => this._dismiss());
154
+
155
+ // The centred dialog box.
156
+ this._dialogEl = document.createElement('div');
157
+ this._dialogEl.className = 'adw-alert-dialog-box';
158
+ this._dialogEl.setAttribute('role', 'alertdialog');
159
+ this._dialogEl.setAttribute('aria-modal', 'true');
160
+ // Clicks inside the box must not bubble to the scrim's dismiss handler.
161
+ this._dialogEl.addEventListener('click', (event) => event.stopPropagation());
162
+
163
+ // The scrolled message area (heading + body + extra child).
164
+ const messageArea = document.createElement('div');
165
+ messageArea.className = 'adw-alert-dialog-message';
166
+
167
+ this._headingEl = document.createElement('h2');
168
+ this._headingEl.className = 'adw-alert-dialog-heading';
169
+
170
+ this._bodyEl = document.createElement('p');
171
+ this._bodyEl.className = 'adw-alert-dialog-body';
172
+
173
+ this._childEl = document.createElement('div');
174
+ this._childEl.className = 'adw-alert-dialog-child';
175
+ for (const child of extraChildren) this._childEl.appendChild(child);
176
+
177
+ messageArea.append(this._headingEl, this._bodyEl, this._childEl);
178
+
179
+ // The response button area.
180
+ this._responseAreaEl = document.createElement('div');
181
+ this._responseAreaEl.className = 'adw-alert-dialog-responses';
182
+
183
+ this._dialogEl.append(messageArea, this._responseAreaEl);
184
+ this.replaceChildren(this._scrimEl, this._dialogEl);
185
+
186
+ // Escape dismisses the dialog when open (mirrors the AdwDialog Escape
187
+ // shortcut → close-response).
188
+ this.addEventListener('keydown', (event) => {
189
+ if (event.key === 'Escape' && this.open) {
190
+ event.stopPropagation();
191
+ this._dismiss();
192
+ }
193
+ });
194
+
195
+ // Adopt declared responses now that the response area exists.
196
+ for (const el of declared) {
197
+ const id = el.getAttribute('id');
198
+ if (!id) continue;
199
+ const appearance = (el.getAttribute('appearance') ?? 'default') as AdwResponseAppearance;
200
+ this.addResponse(id, (el.textContent ?? '').trim());
201
+ if (appearance !== 'default') this.setResponseAppearance(id, appearance);
202
+ if (el.getAttribute('enabled') === 'false') this.setResponseEnabled(id, false);
203
+ }
204
+
205
+ this._render();
206
+ }
207
+
208
+ attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
209
+ if (!this._initialized) return;
210
+ if (oldValue === newValue) return;
211
+ this._render();
212
+ if (name === 'heading') {
213
+ this.dispatchEvent(new CustomEvent('notify::heading', { bubbles: true, detail: { heading: this.heading } }));
214
+ } else if (name === 'body') {
215
+ this.dispatchEvent(new CustomEvent('notify::body', { bubbles: true, detail: { body: this.body } }));
216
+ } else if (name === 'open') {
217
+ this.dispatchEvent(new CustomEvent('notify::open', { bubbles: true, detail: { open: this.open } }));
218
+ if (this.open) this._focusInitial();
219
+ }
220
+ }
221
+
222
+ /** Present the dialog. `parent` is accepted for API parity; unused. */
223
+ present(_parent?: unknown): void {
224
+ this.open = true;
225
+ }
226
+
227
+ /** Adds a response button with the given ID and label. */
228
+ addResponse(id: string, label: string): void {
229
+ if (this._byId.has(id)) {
230
+ // Mirror AdwAlertDialog's g_critical without throwing.
231
+ console.warn(
232
+ `[adw-alert-dialog] Trying to add a response with id '${id}', but such a response already exists`,
233
+ );
234
+ return;
235
+ }
236
+
237
+ const button = document.createElement('button');
238
+ button.type = 'button';
239
+ button.className = 'adw-button adw-alert-dialog-response';
240
+ button.textContent = label;
241
+
242
+ const info: ResponseInfo = { id, label, appearance: 'default', enabled: true, button };
243
+ button.addEventListener('click', () => this._activateResponse(info));
244
+
245
+ this._responses.push(info);
246
+ this._byId.set(id, info);
247
+ if (this._responseAreaEl) this._responseAreaEl.appendChild(button);
248
+ this._render();
249
+ }
250
+
251
+ /** Adds multiple responses at once — id/label pairs (Adw.add_responses parity). */
252
+ addResponses(...idLabelPairs: string[]): void {
253
+ for (let i = 0; i + 1 < idLabelPairs.length; i += 2) {
254
+ this.addResponse(idLabelPairs[i], idLabelPairs[i + 1]);
255
+ }
256
+ }
257
+
258
+ /** Removes a response. */
259
+ removeResponse(id: string): void {
260
+ const info = this._byId.get(id);
261
+ if (!info) return;
262
+ info.button.remove();
263
+ this._responses = this._responses.filter((r) => r !== info);
264
+ this._byId.delete(id);
265
+ if (this._defaultResponse === id) this._defaultResponse = null;
266
+ this._render();
267
+ }
268
+
269
+ /** Whether a response with the given ID exists. */
270
+ hasResponse(id: string): boolean {
271
+ return this._byId.has(id);
272
+ }
273
+
274
+ /** Gets a response's label. */
275
+ getResponseLabel(id: string): string | null {
276
+ return this._byId.get(id)?.label ?? null;
277
+ }
278
+
279
+ /** Sets a response's label. */
280
+ setResponseLabel(id: string, label: string): void {
281
+ const info = this._byId.get(id);
282
+ if (!info) return;
283
+ info.label = label;
284
+ info.button.textContent = label;
285
+ }
286
+
287
+ /** Gets a response's appearance. */
288
+ getResponseAppearance(id: string): AdwResponseAppearance | null {
289
+ return this._byId.get(id)?.appearance ?? null;
290
+ }
291
+
292
+ /** Sets a response's appearance (default / suggested / destructive). */
293
+ setResponseAppearance(id: string, appearance: AdwResponseAppearance): void {
294
+ const info = this._byId.get(id);
295
+ if (!info) return;
296
+ info.appearance = appearance;
297
+ info.button.classList.toggle('suggested-action', appearance === 'suggested');
298
+ info.button.classList.toggle('destructive-action', appearance === 'destructive');
299
+ }
300
+
301
+ /** Gets whether a response is enabled. */
302
+ getResponseEnabled(id: string): boolean {
303
+ return this._byId.get(id)?.enabled ?? false;
304
+ }
305
+
306
+ /** Sets whether a response is enabled (disabled buttons can't be activated). */
307
+ setResponseEnabled(id: string, enabled: boolean): void {
308
+ const info = this._byId.get(id);
309
+ if (!info) return;
310
+ info.enabled = enabled;
311
+ info.button.disabled = !enabled;
312
+ }
313
+
314
+ /** Sets the default response — its button is highlighted as the suggested one. */
315
+ setDefaultResponse(id: string | null): void {
316
+ this._defaultResponse = id;
317
+ this._render();
318
+ }
319
+
320
+ /** Sets the close response — emitted on dismissal (Escape / scrim click). */
321
+ setCloseResponse(id: string): void {
322
+ this._closeResponse = id;
323
+ }
324
+
325
+ // ── internals ──────────────────────────────────────────────────────────
326
+
327
+ /** A response button was activated: close, then emit `response`. */
328
+ private _activateResponse(info: ResponseInfo): void {
329
+ if (!info.enabled) return;
330
+ this._blockCloseResponse = true;
331
+ this.open = false;
332
+ this._blockCloseResponse = false;
333
+ this._emitResponse(info.id);
334
+ }
335
+
336
+ /** Dismissal (Escape / scrim) — close, then emit the close-response. */
337
+ private _dismiss(): void {
338
+ if (!this.open) return;
339
+ this.open = false;
340
+ if (this._blockCloseResponse) return;
341
+ this._emitResponse(this._closeResponse);
342
+ }
343
+
344
+ private _emitResponse(response: string): void {
345
+ this.dispatchEvent(new CustomEvent('response', { bubbles: true, detail: { response } }));
346
+ }
347
+
348
+ /** Move focus to the default response button (or the first enabled one). */
349
+ private _focusInitial(): void {
350
+ const target =
351
+ (this._defaultResponse ? this._byId.get(this._defaultResponse)?.button : undefined) ??
352
+ this._responses.find((r) => r.enabled)?.button;
353
+ target?.focus();
354
+ }
355
+
356
+ private _render(): void {
357
+ this.classList.toggle('open', this.open);
358
+
359
+ const heading = this.heading;
360
+ this._headingEl.textContent = heading;
361
+ this._headingEl.hidden = heading.length === 0;
362
+
363
+ const body = this.body;
364
+ this._bodyEl.textContent = body;
365
+ this._bodyEl.hidden = body.length === 0;
366
+
367
+ this._childEl.hidden = this._childEl.childElementCount === 0;
368
+
369
+ // Layout: stack vertically by default; lay out horizontally only when
370
+ // prefer-wide-layout is set and there are at most two responses (the
371
+ // AdwAlertDialog heuristic for keeping wide buttons readable).
372
+ const horizontal = this.preferWideLayout && this._responses.length <= 2;
373
+ this._responseAreaEl.classList.toggle('horizontal', horizontal);
374
+
375
+ // Reflect the default response on its button (suggested highlight) unless
376
+ // the response already carries an explicit appearance.
377
+ for (const info of this._responses) {
378
+ const isDefault = info.id === this._defaultResponse && info.appearance === 'default';
379
+ info.button.classList.toggle('default-response', isDefault);
380
+ }
381
+ }
382
+ }
383
+
384
+ customElements.define('adw-alert-response', AdwAlertResponse);
385
+ customElements.define('adw-alert-dialog', AdwAlertDialog);
@@ -0,0 +1,108 @@
1
+ // <adw-avatar> — A round avatar showing initials derived from a name (with a
2
+ // colour picked from the name), or a symbolic fallback icon.
3
+ // Attributes: text, size (px, default 48), show-initials (boolean),
4
+ // icon (symbolic fallback name, with or without -symbolic).
5
+ // Reference: refs/adwaita-web/adwaita-web/scss/_avatar.scss
6
+ // Reference: refs/libadwaita/src/adw-avatar.c (initials + colour derivation)
7
+ // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
8
+ // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
9
+
10
+ // A subset of the libadwaita avatar palette (saturated background + light text).
11
+ // The exact libadwaita avatar palette: 14 entries of { fg (a light tint), and a
12
+ // start→stop gradient background }. Reference:
13
+ // refs/libadwaita/src/stylesheet/widgets/_avatar.scss ($avatarcolorlist).
14
+ const AVATAR_COLORS: ReadonlyArray<{ fg: string; start: string; stop: string }> = [
15
+ { fg: '#cfe1f5', start: '#83b6ec', stop: '#337fdc' }, // blue
16
+ { fg: '#caeaf2', start: '#7ad9f1', stop: '#0f9ac8' }, // cyan
17
+ { fg: '#cef8d8', start: '#8de6b1', stop: '#29ae74' }, // green
18
+ { fg: '#e6f9d7', start: '#b5e98a', stop: '#6ab85b' }, // lime
19
+ { fg: '#f9f4e1', start: '#f8e359', stop: '#d29d09' }, // yellow
20
+ { fg: '#ffead1', start: '#ffcb62', stop: '#d68400' }, // gold
21
+ { fg: '#ffe5c5', start: '#ffa95a', stop: '#ed5b00' }, // orange
22
+ { fg: '#f8d2ce', start: '#f78773', stop: '#e62d42' }, // raspberry
23
+ { fg: '#fac7de', start: '#e973ab', stop: '#e33b6a' }, // magenta
24
+ { fg: '#e7c2e8', start: '#cb78d4', stop: '#9945b5' }, // purple
25
+ { fg: '#d5d2f5', start: '#9e91e8', stop: '#7a59ca' }, // violet
26
+ { fg: '#f2eade', start: '#e3cf9c', stop: '#b08952' }, // beige
27
+ { fg: '#e5d6ca', start: '#be916d', stop: '#785336' }, // brown
28
+ { fg: '#d8d7d3', start: '#c0bfbc', stop: '#6e6d71' }, // gray
29
+ ];
30
+
31
+ function initialsOf(text: string): string {
32
+ const words = text.trim().split(/\s+/).filter(Boolean);
33
+ if (words.length === 0) return '';
34
+ if (words.length === 1) return words[0].slice(0, 2).toUpperCase();
35
+ return (words[0][0] + words[words.length - 1][0]).toUpperCase();
36
+ }
37
+
38
+ // GLib's g_str_hash (DJB2 variant: h = h*33 + c, as uint32) so the picked colour
39
+ // matches Adw.Avatar exactly (it uses g_str_hash(text) % 14).
40
+ function gStrHash(text: string): number {
41
+ let h = 5381;
42
+ for (let i = 0; i < text.length; i++) h = (Math.imul(h, 33) + text.charCodeAt(i)) >>> 0;
43
+ return h >>> 0;
44
+ }
45
+
46
+ function colorFor(text: string): { fg: string; start: string; stop: string } {
47
+ return AVATAR_COLORS[gStrHash(text) % AVATAR_COLORS.length];
48
+ }
49
+
50
+ export class AdwAvatar extends HTMLElement {
51
+ private _textEl!: HTMLSpanElement;
52
+ private _iconEl!: HTMLSpanElement;
53
+ private _initialized = false;
54
+
55
+ static get observedAttributes() {
56
+ return ['text', 'size', 'show-initials', 'icon'];
57
+ }
58
+
59
+ connectedCallback() {
60
+ if (this._initialized) return;
61
+ this._initialized = true;
62
+
63
+ this._textEl = document.createElement('span');
64
+ this._textEl.className = 'adw-avatar-text';
65
+
66
+ this._iconEl = document.createElement('span');
67
+ this._iconEl.className = 'adw-avatar-icon adw-icon';
68
+ this._iconEl.setAttribute('aria-hidden', 'true');
69
+
70
+ this.replaceChildren(this._textEl, this._iconEl);
71
+ this._render();
72
+ }
73
+
74
+ attributeChangedCallback() {
75
+ if (this._initialized) this._render();
76
+ }
77
+
78
+ private _render() {
79
+ const size = parseFloat(this.getAttribute('size') || '48');
80
+ this.style.width = `${size}px`;
81
+ this.style.height = `${size}px`;
82
+ this.style.fontSize = `${Math.round(size * (size < 32 ? 0.5 : 0.4))}px`;
83
+
84
+ const text = this.getAttribute('text') ?? '';
85
+ const initials = initialsOf(text);
86
+ const showInitials = this.hasAttribute('show-initials') && initials.length > 0;
87
+
88
+ if (showInitials) {
89
+ const { fg, start, stop } = colorFor(text || 'default');
90
+ this.style.backgroundImage = `linear-gradient(${start}, ${stop})`;
91
+ this.style.color = fg;
92
+ this._textEl.textContent = initials;
93
+ this._textEl.hidden = false;
94
+ this._iconEl.hidden = true;
95
+ } else {
96
+ this.style.backgroundImage = '';
97
+ this.style.color = '';
98
+ this._textEl.hidden = true;
99
+ const icon = (this.getAttribute('icon') ?? 'avatar-default').replace(/-symbolic$/, '');
100
+ this._iconEl.className = `adw-avatar-icon adw-icon adw-icon--${icon}`;
101
+ this._iconEl.style.width = `${Math.round(size * 0.55)}px`;
102
+ this._iconEl.style.height = `${Math.round(size * 0.55)}px`;
103
+ this._iconEl.hidden = false;
104
+ }
105
+ }
106
+ }
107
+
108
+ customElements.define('adw-avatar', AdwAvatar);
@@ -0,0 +1,57 @@
1
+ // <adw-banner> — A full-width strip carrying a single in-context message and an
2
+ // optional action button.
3
+ // Attributes: title, button-label, revealed (boolean), use-markup (boolean).
4
+ // Events: `button-clicked` (CustomEvent) when the action button is pressed.
5
+ // Reference: refs/adwaita-web/adwaita-web/scss/_banner.scss
6
+ // Reference: refs/libadwaita/src/stylesheet/widgets/_banner.scss
7
+ // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
8
+ // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
9
+
10
+ export class AdwBanner extends HTMLElement {
11
+ private _titleEl!: HTMLSpanElement;
12
+ private _button!: HTMLButtonElement;
13
+ private _initialized = false;
14
+
15
+ static get observedAttributes() {
16
+ return ['title', 'button-label', 'revealed', 'use-markup'];
17
+ }
18
+
19
+ connectedCallback() {
20
+ if (this._initialized) return;
21
+ this._initialized = true;
22
+
23
+ this._titleEl = document.createElement('span');
24
+ this._titleEl.className = 'adw-banner-title';
25
+
26
+ this._button = document.createElement('button');
27
+ this._button.className = 'adw-banner-button';
28
+ this._button.addEventListener('click', () => {
29
+ this.dispatchEvent(new CustomEvent('button-clicked', { bubbles: true }));
30
+ });
31
+
32
+ this.replaceChildren(this._titleEl, this._button);
33
+ this._render();
34
+ }
35
+
36
+ attributeChangedCallback() {
37
+ if (this._initialized) this._render();
38
+ }
39
+
40
+ private _render() {
41
+ const title = this.getAttribute('title') ?? '';
42
+ // `use-markup` mirrors Adw.Banner.use_markup: an opt-in (off by default)
43
+ // for developer-supplied markup in the title, not user input. Plain text
44
+ // goes through textContent.
45
+ if (this.hasAttribute('use-markup')) this._titleEl.innerHTML = title;
46
+ else this._titleEl.textContent = title;
47
+
48
+ const buttonLabel = this.getAttribute('button-label') ?? '';
49
+ this._button.textContent = buttonLabel;
50
+ this._button.hidden = buttonLabel.length === 0;
51
+
52
+ // `revealed` (default false to match Adw.Banner) slides the strip in/out.
53
+ this.classList.toggle('revealed', this.hasAttribute('revealed'));
54
+ }
55
+ }
56
+
57
+ customElements.define('adw-banner', AdwBanner);