@gjsify/adwaita-web 0.36.0 → 0.37.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 (146) hide show
  1. package/dist/adwaita-web.css +433 -33
  2. package/dist/adwaita-web.css.map +1 -1
  3. package/lib/types/adw-shortcut-label.spec.d.ts +2 -0
  4. package/lib/types/breakpoints.d.ts +10 -12
  5. package/lib/types/elements/adw-about-dialog.d.ts +19 -36
  6. package/lib/types/elements/adw-action-row.d.ts +9 -12
  7. package/lib/types/elements/adw-alert-dialog.d.ts +0 -12
  8. package/lib/types/elements/adw-avatar.d.ts +1 -0
  9. package/lib/types/elements/adw-bottom-sheet.d.ts +7 -12
  10. package/lib/types/elements/adw-button-content.d.ts +3 -4
  11. package/lib/types/elements/adw-button-row.d.ts +1 -1
  12. package/lib/types/elements/adw-carousel.d.ts +28 -49
  13. package/lib/types/elements/adw-checks.d.ts +2 -8
  14. package/lib/types/elements/adw-combo-row.d.ts +6 -6
  15. package/lib/types/elements/adw-data-grid.d.ts +5 -10
  16. package/lib/types/elements/adw-drop-down.d.ts +3 -11
  17. package/lib/types/elements/adw-entry-row.d.ts +20 -23
  18. package/lib/types/elements/adw-icon.d.ts +2 -4
  19. package/lib/types/elements/adw-inline-view-switcher.d.ts +18 -31
  20. package/lib/types/elements/adw-navigation-split-view.d.ts +7 -12
  21. package/lib/types/elements/adw-navigation-view.d.ts +2 -10
  22. package/lib/types/elements/adw-overlay-split-view.d.ts +28 -52
  23. package/lib/types/elements/adw-password-entry-row.d.ts +4 -5
  24. package/lib/types/elements/adw-popover.d.ts +2 -7
  25. package/lib/types/elements/adw-preferences-dialog.d.ts +21 -34
  26. package/lib/types/elements/adw-preferences-group.d.ts +12 -14
  27. package/lib/types/elements/adw-progress-bar.d.ts +3 -4
  28. package/lib/types/elements/adw-shortcut-label.d.ts +20 -0
  29. package/lib/types/elements/adw-sidebar.d.ts +5 -10
  30. package/lib/types/elements/adw-spinner.d.ts +3 -5
  31. package/lib/types/elements/adw-split-button.d.ts +6 -16
  32. package/lib/types/elements/adw-switch-row.d.ts +3 -3
  33. package/lib/types/elements/adw-tab-view.d.ts +16 -26
  34. package/lib/types/elements/adw-toast-overlay.d.ts +8 -13
  35. package/lib/types/elements/adw-view-stack.d.ts +7 -17
  36. package/lib/types/elements/adw-view-switcher-bar.d.ts +4 -8
  37. package/lib/types/elements/adw-view-switcher.d.ts +18 -38
  38. package/lib/types/elements/adw-wrap-box.d.ts +7 -17
  39. package/lib/types/elements/view-switcher-dom.d.ts +10 -13
  40. package/lib/types/index.d.ts +1 -0
  41. package/lib/types/source-view/hex.d.ts +7 -13
  42. package/lib/types/style-classes.spec.d.ts +11 -0
  43. package/lib/types/styles.generated.d.ts +1 -1
  44. package/package.json +6 -6
  45. package/scss/_about_dialog.scss +3 -2
  46. package/scss/_action_row.scss +33 -0
  47. package/scss/_avatar.scss +12 -0
  48. package/scss/_button.scss +44 -22
  49. package/scss/_card.scss +11 -8
  50. package/scss/_checks.scss +2 -4
  51. package/scss/_dialog.scss +3 -2
  52. package/scss/_headerbar.scss +23 -0
  53. package/scss/_popover.scss +8 -4
  54. package/scss/_preferences.scss +7 -7
  55. package/scss/_progress_bar.scss +9 -6
  56. package/scss/_reset.scss +1 -1
  57. package/scss/_shortcut_label.scss +80 -0
  58. package/scss/_sidebar.scss +57 -0
  59. package/scss/_status_page.scss +20 -0
  60. package/scss/_style_classes.scss +407 -0
  61. package/scss/_switch.scss +4 -3
  62. package/scss/_variables.scss +25 -0
  63. package/scss/adwaita-skin.scss +12 -0
  64. package/src/adw-about-dialog.spec.ts +34 -49
  65. package/src/adw-action-rows.spec.ts +25 -33
  66. package/src/adw-avatar.spec.ts +51 -20
  67. package/src/adw-banner.spec.ts +5 -8
  68. package/src/adw-button-content.spec.ts +9 -12
  69. package/src/adw-button-row.spec.ts +2 -4
  70. package/src/adw-carousel.spec.ts +40 -53
  71. package/src/adw-checks.spec.ts +36 -47
  72. package/src/adw-drop-down.spec.ts +25 -32
  73. package/src/adw-header-bar.spec.ts +5 -7
  74. package/src/adw-icon.spec.ts +23 -32
  75. package/src/adw-progress-bar.spec.ts +10 -12
  76. package/src/adw-row-state.spec.ts +9 -10
  77. package/src/adw-shortcut-label.spec.ts +112 -0
  78. package/src/adw-sidebar.spec.ts +17 -24
  79. package/src/adw-switch.spec.ts +11 -17
  80. package/src/adw-tab-view.spec.ts +7 -10
  81. package/src/adw-toast-overlay.spec.ts +7 -11
  82. package/src/adw-view-switcher-bar.spec.ts +10 -14
  83. package/src/adw-wrap-box.spec.ts +25 -36
  84. package/src/bottom-sheet.spec.ts +30 -36
  85. package/src/breakpoints.ts +15 -18
  86. package/src/chrome.spec.ts +13 -21
  87. package/src/elements/adw-about-dialog.ts +71 -114
  88. package/src/elements/adw-action-row.ts +22 -37
  89. package/src/elements/adw-alert-dialog.ts +25 -50
  90. package/src/elements/adw-avatar.ts +40 -15
  91. package/src/elements/adw-banner.ts +14 -18
  92. package/src/elements/adw-bottom-sheet.ts +31 -52
  93. package/src/elements/adw-button-content.ts +30 -37
  94. package/src/elements/adw-button-row.ts +14 -24
  95. package/src/elements/adw-carousel.ts +87 -148
  96. package/src/elements/adw-checks.ts +43 -56
  97. package/src/elements/adw-clamp.ts +8 -9
  98. package/src/elements/adw-combo-row.ts +17 -25
  99. package/src/elements/adw-data-grid.ts +26 -38
  100. package/src/elements/adw-dialog.ts +25 -56
  101. package/src/elements/adw-drop-down.ts +31 -51
  102. package/src/elements/adw-entry-row.ts +39 -46
  103. package/src/elements/adw-expander-row.ts +30 -53
  104. package/src/elements/adw-icon.ts +30 -53
  105. package/src/elements/adw-inline-view-switcher.ts +38 -67
  106. package/src/elements/adw-menu-button.ts +28 -58
  107. package/src/elements/adw-navigation-split-view.ts +35 -47
  108. package/src/elements/adw-navigation-view.ts +12 -28
  109. package/src/elements/adw-overlay-split-view.ts +100 -175
  110. package/src/elements/adw-password-entry-row.ts +18 -28
  111. package/src/elements/adw-popover.ts +28 -44
  112. package/src/elements/adw-preferences-dialog.ts +41 -66
  113. package/src/elements/adw-preferences-group.ts +33 -46
  114. package/src/elements/adw-progress-bar.ts +27 -47
  115. package/src/elements/adw-shortcut-label.ts +138 -0
  116. package/src/elements/adw-sidebar.ts +31 -39
  117. package/src/elements/adw-spin-row.ts +7 -14
  118. package/src/elements/adw-spinner.ts +31 -43
  119. package/src/elements/adw-split-button.ts +45 -75
  120. package/src/elements/adw-switch-row.ts +29 -46
  121. package/src/elements/adw-switch.ts +28 -47
  122. package/src/elements/adw-tab-view.ts +62 -103
  123. package/src/elements/adw-toast-overlay.ts +28 -35
  124. package/src/elements/adw-toggle-group.ts +9 -16
  125. package/src/elements/adw-view-stack.ts +32 -51
  126. package/src/elements/adw-view-switcher-bar.ts +26 -50
  127. package/src/elements/adw-view-switcher.ts +60 -110
  128. package/src/elements/adw-window-title.ts +4 -4
  129. package/src/elements/adw-wrap-box.ts +33 -59
  130. package/src/elements/view-switcher-dom.ts +15 -22
  131. package/src/entry-rows.spec.ts +18 -24
  132. package/src/index.ts +13 -15
  133. package/src/navigation-view.spec.ts +7 -10
  134. package/src/preferences-search.ts +11 -15
  135. package/src/preferences.spec.ts +13 -15
  136. package/src/source-view/adw-source-view.ts +6 -19
  137. package/src/source-view/hex.ts +7 -13
  138. package/src/split-button.spec.ts +32 -44
  139. package/src/split-views.spec.ts +83 -149
  140. package/src/style-classes.spec.ts +181 -0
  141. package/src/style-isolation.spec.ts +3 -6
  142. package/src/styles.generated.ts +1 -1
  143. package/src/tab-view.spec.ts +26 -35
  144. package/src/test.browser.mts +4 -0
  145. package/src/view-stack.spec.ts +23 -34
  146. package/src/view-switcher.spec.ts +56 -84
@@ -17,10 +17,9 @@
17
17
  // open (boolean — whether the dialog is shown; default closed)
18
18
  // prefer-wide-layout (boolean — lay responses out horizontally when they fit)
19
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.
20
+ // Buttons are stacked vertically by default (the Adw.AlertDialog default at medium
21
+ // sizes) and laid out horizontally when prefer-wide-layout is set and there are no more
22
+ // than two of them — the libadwaita measure/allocate heuristic in spirit, not to the px.
24
23
  //
25
24
  // Events:
26
25
  // `response` (CustomEvent, bubbles, `detail = { response }`) when a response
@@ -29,13 +28,10 @@
29
28
  // `notify::heading` / `notify::body` / `notify::open` (CustomEvent, bubbles)
30
29
  // mirroring the matching GObject properties.
31
30
  //
32
- // The RESPONSE MODEL — the response registry (id/label/appearance/enabled), the
33
- // default/close-response semantics and the resolve-to-chosen-id contract — is
34
- // HEADLESS and lives in `@gjsify/adwaita-core` (ADR 0004) as
35
- // {@link AdwAlertResponses}; this element composes it and keeps only the DOM
36
- // render half: the scrim + floating sheet, one `<button>` per registered
37
- // response, and the `response` event. `@gjsify/adwaita-nativescript` composes the
38
- // same model, so both ports share one behaviour.
31
+ // The RESPONSE MODEL — the registry (id/label/appearance/enabled), the
32
+ // default/close-response semantics and the resolve-to-chosen-id contract — is HEADLESS
33
+ // and lives in `@gjsify/adwaita-core` (ADR 0004) as {@link AdwAlertResponses}, composed
34
+ // by `@gjsify/adwaita-nativescript` too. This element keeps only the DOM render half.
39
35
  //
40
36
  // Reference: refs/libadwaita/src/adw-alert-dialog.c (AdwAlertDialog behaviour)
41
37
  // Reference: refs/libadwaita/src/adw-dialog.c (shared dialog base — Escape/close)
@@ -76,16 +72,14 @@ export class AdwAlertDialog extends HTMLElement {
76
72
  private _model = new AdwAlertResponses();
77
73
  /** The rendered button per registered response id (the DOM half of the model). */
78
74
  private readonly _buttons = new Map<string, HTMLButtonElement>();
79
- // Set while a response is being emitted from a button so the close handler
80
- // does not also fire the close-response (mirrors AdwAlertDialog's
81
- // block_close_response flag).
75
+ // Set while a response is being emitted from a button, so the close handler does not
76
+ // also fire the close-response (AdwAlertDialog's `block_close_response`).
82
77
  private _blockCloseResponse = false;
83
78
 
84
79
  static get observedAttributes() {
85
80
  return ['heading', 'body', 'open', 'prefer-wide-layout'];
86
81
  }
87
82
 
88
- /** The heading of the dialog. */
89
83
  get heading(): string {
90
84
  return this.getAttribute('heading') ?? '';
91
85
  }
@@ -94,7 +88,6 @@ export class AdwAlertDialog extends HTMLElement {
94
88
  this.setAttribute('heading', value);
95
89
  }
96
90
 
97
- /** The body text of the dialog. */
98
91
  get body(): string {
99
92
  return this.getAttribute('body') ?? '';
100
93
  }
@@ -103,7 +96,6 @@ export class AdwAlertDialog extends HTMLElement {
103
96
  this.setAttribute('body', value);
104
97
  }
105
98
 
106
- /** Whether the dialog is currently shown. */
107
99
  get open(): boolean {
108
100
  return this.hasAttribute('open');
109
101
  }
@@ -145,18 +137,16 @@ export class AdwAlertDialog extends HTMLElement {
145
137
  if (this._initialized) return;
146
138
  this._initialized = true;
147
139
 
148
- // Snapshot any declared <adw-alert-response> children before we take
140
+ // Declared <adw-alert-response> children are snapshotted before the element takes
149
141
  // over the subtree; everything else becomes the extra child.
150
142
  const declared = Array.from(this.querySelectorAll(':scope > adw-alert-response')) as AdwAlertResponse[];
151
143
  const declaredSet = new Set<Node>(declared);
152
144
  const extraChildren = Array.from(this.childNodes).filter((n) => !declaredSet.has(n));
153
145
 
154
- // The fixed full-cover scrim. Clicking it dismisses the dialog.
155
146
  this._scrimEl = document.createElement('div');
156
147
  this._scrimEl.className = 'adw-alert-dialog-scrim';
157
148
  this._scrimEl.addEventListener('click', () => this._dismiss());
158
149
 
159
- // The centred dialog box.
160
150
  this._dialogEl = document.createElement('div');
161
151
  this._dialogEl.className = 'adw-alert-dialog-box';
162
152
  this._dialogEl.setAttribute('role', 'alertdialog');
@@ -164,7 +154,6 @@ export class AdwAlertDialog extends HTMLElement {
164
154
  // Clicks inside the box must not bubble to the scrim's dismiss handler.
165
155
  this._dialogEl.addEventListener('click', (event) => event.stopPropagation());
166
156
 
167
- // The scrolled message area (heading + body + extra child).
168
157
  const messageArea = document.createElement('div');
169
158
  messageArea.className = 'adw-alert-dialog-message';
170
159
 
@@ -180,15 +169,14 @@ export class AdwAlertDialog extends HTMLElement {
180
169
 
181
170
  messageArea.append(this._headingEl, this._bodyEl, this._childEl);
182
171
 
183
- // The response button area.
184
172
  this._responseAreaEl = document.createElement('div');
185
173
  this._responseAreaEl.className = 'adw-alert-dialog-responses';
186
174
 
187
175
  this._dialogEl.append(messageArea, this._responseAreaEl);
188
176
  this.replaceChildren(this._scrimEl, this._dialogEl);
189
177
 
190
- // Escape dismisses the dialog when open (mirrors the AdwDialog Escape
191
- // shortcut → close-response).
178
+ // Escape dismisses the dialog when open (the AdwDialog Escape shortcut →
179
+ // close-response).
192
180
  this.addEventListener('keydown', (event) => {
193
181
  if (event.key === 'Escape' && this.open) {
194
182
  event.stopPropagation();
@@ -196,7 +184,6 @@ export class AdwAlertDialog extends HTMLElement {
196
184
  }
197
185
  });
198
186
 
199
- // Adopt declared responses now that the response area exists.
200
187
  for (const el of declared) {
201
188
  const id = el.getAttribute('id');
202
189
  if (!id) continue;
@@ -206,8 +193,8 @@ export class AdwAlertDialog extends HTMLElement {
206
193
  if (el.getAttribute('enabled') === 'false') this.setResponseEnabled(id, false);
207
194
  }
208
195
 
209
- // Seed the headless model's text from the parsed markup (the attributes
210
- // stay the source of truth for what is rendered).
196
+ // Seed the headless model's text from the parsed markup; the attributes stay the
197
+ // source of truth for what is rendered.
211
198
  this._model.heading = this.heading;
212
199
  this._model.body = this.body;
213
200
 
@@ -219,8 +206,8 @@ export class AdwAlertDialog extends HTMLElement {
219
206
  if (oldValue === newValue) return;
220
207
  this._render();
221
208
  if (name === 'heading') {
222
- // Keep the headless model a complete description of the dialog; the
223
- // attribute stays the source of truth for the rendered heading.
209
+ // Keep the headless model a complete description of the dialog; the attribute
210
+ // stays the source of truth for the rendered heading.
224
211
  this._model.heading = this.heading;
225
212
  this.dispatchEvent(
226
213
  new CustomEvent('notify::heading', { bubbles: true, detail: { heading: this.heading } }),
@@ -239,10 +226,9 @@ export class AdwAlertDialog extends HTMLElement {
239
226
  this.open = true;
240
227
  }
241
228
 
242
- /** Adds a response button with the given ID and label. */
243
229
  addResponse(id: string, label: string): void {
244
- // The core registry UPDATES a duplicate id in place; AdwAlertDialog
245
- // refuses it, so the guard stays here.
230
+ // The core registry UPDATES a duplicate id in place; AdwAlertDialog refuses it,
231
+ // so the guard stays here.
246
232
  if (this._model.hasResponse(id)) {
247
233
  // Mirror AdwAlertDialog's g_critical without throwing.
248
234
  console.warn(
@@ -270,13 +256,12 @@ export class AdwAlertDialog extends HTMLElement {
270
256
  }
271
257
  }
272
258
 
273
- /** Removes a response. */
274
259
  removeResponse(id: string): void {
275
260
  if (!this._model.hasResponse(id)) return;
276
261
  this._buttons.get(id)?.remove();
277
262
  this._buttons.delete(id);
278
263
  // `AdwAlertResponses` exposes no removal (a gap against
279
- // adw_alert_dialog_remove_response), so the registry is rebuilt from the
264
+ // `adw_alert_dialog_remove_response`), so the registry is rebuilt from the
280
265
  // survivors — core stays the single owner of the response state.
281
266
  const survivors = this._model.responses.filter((response) => response.id !== id);
282
267
  const rebuilt = new AdwAlertResponses(this._model.heading, this._model.body);
@@ -292,17 +277,14 @@ export class AdwAlertDialog extends HTMLElement {
292
277
  this._render();
293
278
  }
294
279
 
295
- /** Whether a response with the given ID exists. */
296
280
  hasResponse(id: string): boolean {
297
281
  return this._model.hasResponse(id);
298
282
  }
299
283
 
300
- /** Gets a response's label. */
301
284
  getResponseLabel(id: string): string | null {
302
285
  return this._model.responses.find((response) => response.id === id)?.label ?? null;
303
286
  }
304
287
 
305
- /** Sets a response's label. */
306
288
  setResponseLabel(id: string, label: string): void {
307
289
  const button = this._buttons.get(id);
308
290
  if (!button) return;
@@ -311,12 +293,10 @@ export class AdwAlertDialog extends HTMLElement {
311
293
  button.textContent = label;
312
294
  }
313
295
 
314
- /** Gets a response's appearance. */
315
296
  getResponseAppearance(id: string): AdwResponseAppearance | null {
316
297
  return this._model.hasResponse(id) ? this._model.getResponseAppearance(id) : null;
317
298
  }
318
299
 
319
- /** Sets a response's appearance (default / suggested / destructive). */
320
300
  setResponseAppearance(id: string, appearance: AdwResponseAppearance): void {
321
301
  const button = this._buttons.get(id);
322
302
  if (!button) return;
@@ -325,7 +305,6 @@ export class AdwAlertDialog extends HTMLElement {
325
305
  button.classList.toggle('destructive-action', appearance === 'destructive');
326
306
  }
327
307
 
328
- /** Gets whether a response is enabled. */
329
308
  getResponseEnabled(id: string): boolean {
330
309
  return this._model.hasResponse(id) ? this._model.getResponseEnabled(id) : false;
331
310
  }
@@ -349,16 +328,13 @@ export class AdwAlertDialog extends HTMLElement {
349
328
  this._model.closeResponse = id;
350
329
  }
351
330
 
352
- // ── internals ──────────────────────────────────────────────────────────
353
-
354
- /** A response button was activated: close, then emit `response`. */
355
331
  private _activateResponse(id: string): void {
356
332
  if (!this._model.getResponseEnabled(id)) return;
357
333
  this._blockCloseResponse = true;
358
334
  this.open = false;
359
335
  this._blockCloseResponse = false;
360
- // resolveById validates against the registry (the core resolve-to-chosen-id
361
- // contract) and falls back to the close response.
336
+ // `resolveById` validates against the registry and falls back to the close
337
+ // response.
362
338
  this._emitResponse(this._model.resolveById(id));
363
339
  }
364
340
 
@@ -398,14 +374,13 @@ export class AdwAlertDialog extends HTMLElement {
398
374
 
399
375
  this._childEl.hidden = this._childEl.childElementCount === 0;
400
376
 
401
- // Layout: stack vertically by default; lay out horizontally only when
402
- // prefer-wide-layout is set and there are at most two responses (the
403
- // AdwAlertDialog heuristic for keeping wide buttons readable).
377
+ // Horizontal only when prefer-wide-layout is set AND there are at most two
378
+ // responses — the AdwAlertDialog heuristic for keeping wide buttons readable.
404
379
  const horizontal = this.preferWideLayout && this._model.responses.length <= 2;
405
380
  this._responseAreaEl.classList.toggle('horizontal', horizontal);
406
381
 
407
- // Reflect the default response on its button (suggested highlight) unless
408
- // the response already carries an explicit appearance.
382
+ // The default response gets the suggested highlight unless it already carries an
383
+ // explicit appearance.
409
384
  const defaultId = this._model.defaultResponse;
410
385
  for (const response of this._model.responses) {
411
386
  const isDefault = response.id === defaultId && response.appearance === 'default';
@@ -1,20 +1,23 @@
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.
1
+ // <adw-avatar> — A round avatar showing a custom image, initials derived from a
2
+ // name (with a colour picked from the name), or a symbolic fallback icon.
3
3
  // Attributes: text, size (px, default 48), show-initials (boolean),
4
- // icon (symbolic fallback name, with or without -symbolic).
4
+ // icon (symbolic fallback name, with or without -symbolic),
5
+ // custom-image (an image URL — `Adw.Avatar:custom-image` is a GdkPaintable,
6
+ // and a URL is what a browser draws one from).
5
7
  // Reference: refs/adwaita-web/adwaita-web/scss/_avatar.scss
6
8
  // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
7
9
  // Modifications: Implemented as a Web Component for @gjsify/adwaita-web; the
8
10
  // fallback icon node is <adw-icon>.
9
11
  //
10
- // The two derivations — initials from the name, and the palette colour the name
11
- // hashes to — are HEADLESS and live in `@gjsify/adwaita-core` (ADR 0004) as
12
- // {@link avatarInitials} / {@link avatarColor}; this element only paints them.
13
- // They used to be a local copy that had drifted from both libadwaita and the
14
- // NativeScript renderer (single-word names rendered TWO letters here and one
15
- // there, and accented names picked the wrong colour in both). The shared
16
- // vectors in `@gjsify/adwaita-core/conformance` now pin this element to the C
17
- // source — see `src/adw-avatar.spec.ts`.
12
+ // The two derivations — initials from the name, and the palette colour the name hashes to
13
+ // — are HEADLESS and live in `@gjsify/adwaita-core` (ADR 0004) as {@link avatarInitials} /
14
+ // {@link avatarColor}; this element only paints them. The shared vectors in
15
+ // `@gjsify/adwaita-core/conformance` pin it to the C source — see `src/adw-avatar.spec.ts`.
16
+ //
17
+ // `custom-image` was UNPORTED until #1049's follow-up: this element passed
18
+ // `hasCustomImage: false` unconditionally, so the `image` branch of `update_visibility`
19
+ // (adw-avatar.c:117-125) — the branch that wins over both others — could not be reached,
20
+ // and the vector row covering it had nothing to drive.
18
21
 
19
22
  import { avatarColor, avatarFontSize, avatarInitials, avatarMaxFontSize, avatarMode } from '@gjsify/adwaita-core';
20
23
 
@@ -23,10 +26,11 @@ import { type AdwIcon, createAdwIcon } from './adw-icon.js';
23
26
  export class AdwAvatar extends HTMLElement {
24
27
  private _textEl!: HTMLSpanElement;
25
28
  private _iconEl!: AdwIcon;
29
+ private _imageEl!: HTMLImageElement;
26
30
  private _initialized = false;
27
31
 
28
32
  static get observedAttributes() {
29
- return ['text', 'size', 'show-initials', 'icon'];
33
+ return ['text', 'size', 'show-initials', 'icon', 'custom-image'];
30
34
  }
31
35
 
32
36
  connectedCallback() {
@@ -38,7 +42,14 @@ export class AdwAvatar extends HTMLElement {
38
42
 
39
43
  this._iconEl = createAdwIcon(null, 'adw-avatar-icon');
40
44
 
41
- this.replaceChildren(this._textEl, this._iconEl);
45
+ this._imageEl = document.createElement('img');
46
+ this._imageEl.className = 'adw-avatar-custom-image';
47
+ // The image is the avatar's own presentation, not content of its own —
48
+ // the accessible name comes from the surrounding row, as in GTK where
49
+ // the custom image is a plain GtkImage inside the widget.
50
+ this._imageEl.alt = '';
51
+
52
+ this.replaceChildren(this._textEl, this._iconEl, this._imageEl);
42
53
  this._render();
43
54
  }
44
55
 
@@ -52,15 +63,29 @@ export class AdwAvatar extends HTMLElement {
52
63
  this.style.height = `${size}px`;
53
64
 
54
65
  const text = this.getAttribute('text') ?? '';
66
+ const customImage = this.getAttribute('custom-image') ?? '';
55
67
  // The gate is the TEXT, not the derived initials — `update_visibility`
56
68
  // keeps a whitespace-only name in initials mode with a blank label.
57
69
  const mode = avatarMode({
58
- hasCustomImage: false,
70
+ hasCustomImage: customImage.length > 0,
59
71
  showInitials: this.hasAttribute('show-initials'),
60
72
  text,
61
73
  });
62
74
 
63
- if (mode === 'initials') {
75
+ this._imageEl.hidden = mode !== 'image';
76
+ if (mode === 'image') {
77
+ // Only touch `src` when it changed: re-assigning the same URL
78
+ // restarts the load and flashes the avatar on every unrelated
79
+ // attribute change.
80
+ if (this._imageEl.getAttribute('src') !== customImage) this._imageEl.src = customImage;
81
+ // No gradient behind an image: `set_class_color` still runs in the C,
82
+ // but nothing of it is visible, and leaving it would show through a
83
+ // transparent PNG.
84
+ this.style.backgroundImage = '';
85
+ this.style.color = '';
86
+ this._textEl.hidden = true;
87
+ this._iconEl.hidden = true;
88
+ } else if (mode === 'initials') {
64
89
  const { fg, start, stop } = avatarColor(text);
65
90
  this.style.backgroundImage = `linear-gradient(${start}, ${stop})`;
66
91
  this.style.color = fg;
@@ -1,18 +1,14 @@
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
- // button-style ("default" | "suggested").
5
- // Events: `button-clicked` (CustomEvent) when the action button is pressed.
1
+ // <adw-banner> — a full-width strip carrying a single in-context message and an optional
2
+ // action button. `button-style` is `"default" | "suggested"`, and `button-clicked`
3
+ // (CustomEvent) fires when the button is pressed.
6
4
  //
7
- // The defaults and the derivations come from `@gjsify/adwaita-core` (ADR 0004),
8
- // so this element and `@gjsify/adwaita-nativescript` answer the same markup the
9
- // same way. They used not to: `revealed` was opt-in here and initialised TRUE
10
- // there, and neither renderer had `button-style` (Since 1.7) or stripped the
11
- // mnemonic marker the template pins onto the button (adw-banner.ui:33).
5
+ // The defaults and the derivations (including stripping the mnemonic marker the upstream
6
+ // template pins onto the button label) come from `@gjsify/adwaita-core` (ADR 0004), so
7
+ // this element and the NativeScript one answer the same markup the same way.
12
8
  //
13
9
  // Reference: refs/libadwaita/src/adw-banner.c (AdwBanner)
14
10
  // Reference: refs/libadwaita/src/adw-banner.ui
15
- // Reference: refs/libadwaita/src/stylesheet/widgets/_toolbars.scss (banner :243-262)
11
+ // Reference: refs/libadwaita/src/stylesheet/widgets/_toolbars.scss (banner)
16
12
  // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
17
13
  // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
18
14
 
@@ -57,7 +53,7 @@ export class AdwBanner extends HTMLElement {
57
53
  private _render() {
58
54
  const title = this.getAttribute('title') ?? '';
59
55
  // DELIBERATE DEPARTURE FROM THE C DEFAULT. `Adw.Banner:use-markup`
60
- // defaults to TRUE (adw-banner.c:422-425) and `@gjsify/adwaita-core`
56
+ // defaults to TRUE and `@gjsify/adwaita-core`
61
57
  // carries that as the spec value — but Pango markup is not HTML. Pango
62
58
  // has a fixed tag set and no scripting; `innerHTML` will run an
63
59
  // `onerror` handler. Adopting the C default here would turn every
@@ -65,27 +61,27 @@ export class AdwBanner extends HTMLElement {
65
61
  // in GTK, so on this renderer markup is OPT-IN and the default is text.
66
62
  // The opt-in is the developer asserting the title is trusted.
67
63
  // The title label carries `use-underline=False` in the template
68
- // (adw-banner.ui:20), so its underscores are literal — no stripping here.
64
+ //, so its underscores are literal — no stripping here.
69
65
  if (this.hasAttribute('use-markup')) this._titleEl.innerHTML = title;
70
66
  else this._titleEl.textContent = title;
71
67
 
72
- // `label && label[0]` (:663) — a first-character test, so a label of
68
+ // `label && label[0]` — a first-character test, so a label of
73
69
  // spaces still shows a button. The template pins the BUTTON to
74
- // `use-underline=True` (adw-banner.ui:33), so the painted text drops the
70
+ // `use-underline=True`, so the painted text drops the
75
71
  // mnemonic markers the attribute keeps.
76
72
  const buttonLabel = this.getAttribute('button-label') ?? '';
77
73
  this._button.textContent = bannerButtonText(buttonLabel);
78
74
  this._button.hidden = !bannerButtonVisible(buttonLabel);
79
75
 
80
- // `button-style` (Since 1.7, :443-447) adds or removes exactly one class
81
- // (:764-774) — swap within the managed set so nothing else is touched.
76
+ // `button-style` (Since 1.7) adds or removes exactly one class
77
+ // — swap within the managed set so nothing else is touched.
82
78
  const style = parseBannerButtonStyle(this.getAttribute('button-style'));
83
79
  const applied = bannerButtonStyleClasses(style);
84
80
  for (const cls of ADW_BANNER_BUTTON_STYLE_CLASSES) {
85
81
  this._button.classList.toggle(cls, applied.includes(cls));
86
82
  }
87
83
 
88
- // `revealed` (default FALSE, :456-459) slides the strip in/out.
84
+ // `revealed` (default FALSE) slides the strip in/out.
89
85
  this.classList.toggle('revealed', this.hasAttribute('revealed'));
90
86
  }
91
87
  }
@@ -5,18 +5,14 @@
5
5
  // layer covers the content and dismisses the sheet on click (unless `can-close`
6
6
  // is false, in which case a close attempt is signalled instead).
7
7
  //
8
- // The open/closed state and the dismissal gate are NOT implemented here: they
9
- // live in `@gjsify/adwaita-core` as `BottomSheetPresentation` +
10
- // `resolveBottomSheetClose`, ported from the C source and shared with
11
- // `@gjsify/adwaita-nativescript` (ADR 0004). This element owns only the DOM.
12
- // Three things changed when it moved:
13
- // - `open="false"` now means CLOSED. The old getter was `hasAttribute('open')`
14
- // while every other boolean used `!== 'false'`, so one element carried two
15
- // incompatible conventions and `<adw-bottom-sheet open="false">` rendered
16
- // open (adw-bottom-sheet.c:1670 — `open = !!open` on a strict gboolean).
17
- // - `notify::open` follows the STATE, not the attribute VALUE. Rewriting the
18
- // attribute to another truthy spelling no longer fires a notification
19
- // carrying an unchanged payload (adw-bottom-sheet.c:1672-1682).
8
+ // The open/closed state and the dismissal gate are NOT implemented here: they live in
9
+ // `@gjsify/adwaita-core` as `BottomSheetPresentation` + `resolveBottomSheetClose`,
10
+ // ported from the C source and shared with `@gjsify/adwaita-nativescript` (ADR 0004).
11
+ // This element owns only the DOM. Three rules are easy to get wrong:
12
+ // - `open="false"` means CLOSED, like every other boolean here — C's setter is
13
+ // `open = !!open` on a strict gboolean, so `hasAttribute` is the wrong test.
14
+ // - `notify::open` follows the STATE, not the attribute VALUE, so rewriting the
15
+ // attribute to another truthy spelling notifies nobody.
20
16
  // - the drag handle is decorative, not a close button — see below.
21
17
  //
22
18
  // Children are declared as <adw-bottom-sheet-content> (the persistent content)
@@ -38,10 +34,9 @@
38
34
  // state changes — mirrors the Adw.BottomSheet `open` GObject property.
39
35
  // `close-attempt` (CustomEvent, bubbles) when a dismissal is attempted that the
40
36
  // sheet refuses — mirrors the Adw.BottomSheet `close-attempt` signal.
41
- // `sheet.close` (CustomEvent, bubbles) when the sheet.close action is used on
42
- // an ALREADY-closed sheet, so an enclosing sheet/dialog can handle it — the
43
- // DOM equivalent of `gtk_widget_activate_action (parent, "sheet.close")`
44
- // (adw-bottom-sheet.c:382-385).
37
+ // `sheet.close` (CustomEvent, bubbles) when the sheet.close action is used on an
38
+ // ALREADY-closed sheet, so an enclosing sheet/dialog can handle it — the DOM
39
+ // equivalent of `gtk_widget_activate_action (parent, "sheet.close")`.
45
40
  //
46
41
  // Reference: refs/libadwaita/src/adw-bottom-sheet.c (AdwBottomSheet behaviour)
47
42
  // Reference: refs/libadwaita/src/stylesheet/widgets/_bottom-sheet.scss
@@ -72,7 +67,6 @@ export class AdwBottomSheet extends HTMLElement {
72
67
  return ['open', 'modal', 'can-close', 'show-drag-handle'];
73
68
  }
74
69
 
75
- /** Whether the sheet is revealed. */
76
70
  get open(): boolean {
77
71
  return this._state.open;
78
72
  }
@@ -81,7 +75,6 @@ export class AdwBottomSheet extends HTMLElement {
81
75
  this._setOpen(!!value);
82
76
  }
83
77
 
84
- /** Whether the content is dimmed + blocked while the sheet is open. */
85
78
  get modal(): boolean {
86
79
  return this._boolAttr('modal', true);
87
80
  }
@@ -99,7 +92,6 @@ export class AdwBottomSheet extends HTMLElement {
99
92
  this._setBoolAttr('can-close', value);
100
93
  }
101
94
 
102
- /** Whether the overlaid drag handle is shown. */
103
95
  get showDragHandle(): boolean {
104
96
  return this._boolAttr('show-drag-handle', true);
105
97
  }
@@ -109,14 +101,13 @@ export class AdwBottomSheet extends HTMLElement {
109
101
  }
110
102
 
111
103
  /**
112
- * Route a dismissal affordance through the shared gate and act on the
113
- * verdict: close, raise `close-attempt`, forward `sheet.close` to an
114
- * ancestor, or do nothing. The element calls this for the dimming layer and
115
- * Escape; a close button inside the sheet calls it with `'close-button'`.
104
+ * Route a dismissal affordance through the shared gate and act on the verdict: close,
105
+ * raise `close-attempt`, forward `sheet.close` to an ancestor, or do nothing. Called
106
+ * for the dimming layer and Escape; a close button inside the sheet passes
107
+ * `'close-button'`.
116
108
  *
117
- * The verdict differs per source — a locked sheet signals a scrim click but
118
- * swallows a swipe, and the drag handle never closes anything. See
119
- * `resolveBottomSheetClose` in `@gjsify/adwaita-core`.
109
+ * The verdict differs PER SOURCE — a locked sheet signals a scrim click but swallows
110
+ * a swipe, and the drag handle never closes anything (`resolveBottomSheetClose`).
120
111
  */
121
112
  requestClose(source: BottomSheetCloseSource): BottomSheetCloseOutcome {
122
113
  const outcome = this._state.requestClose(source);
@@ -132,37 +123,30 @@ export class AdwBottomSheet extends HTMLElement {
132
123
  if (this._initialized) return;
133
124
  this._initialized = true;
134
125
 
135
- // Snapshot declared children before taking over the subtree. The sheet
136
- // and content are placed by slot; anything unslotted falls back to the
126
+ // The sheet and content are placed by slot; anything unslotted falls back to the
137
127
  // content (the AdwBottomSheet GtkBuildable default).
138
128
  const claimed = new Set<Node>();
139
129
  const sheetChildren = this._collectSlot('adw-bottom-sheet-sheet', 'sheet', claimed);
140
130
  const contentChildren = this._collectSlot('adw-bottom-sheet-content', 'content', claimed);
141
131
  const unslotted = Array.from(this.childNodes).filter((n) => !claimed.has(n));
142
132
 
143
- // Persistent content layer.
144
133
  this._contentEl = document.createElement('div');
145
134
  this._contentEl.className = 'adw-bottom-sheet-content';
146
135
  for (const child of contentChildren) this._contentEl.appendChild(child);
147
136
  for (const child of unslotted) this._contentEl.appendChild(child);
148
137
 
149
- // Dimming layer — covers the content while the sheet is open (modal).
150
- // Clicking it dismisses the sheet (or raises close-attempt when locked).
151
138
  this._dimmingEl = document.createElement('div');
152
139
  this._dimmingEl.className = 'adw-bottom-sheet-dimming';
153
140
  this._dimmingEl.addEventListener('click', () => this.requestClose('dimming'));
154
141
 
155
- // The slide-up sheet with its overlaid drag handle.
156
142
  this._sheetEl = document.createElement('div');
157
143
  this._sheetEl.className = 'adw-bottom-sheet-sheet';
158
144
 
159
- // libadwaita builds the handle with can_focus = FALSE and
160
- // can_target = FALSE (adw-bottom-sheet.c:1197-1198): it is a decorative
161
- // pill that cannot be clicked or focused at all. Its only behavioural
162
- // role is elsewhere — `allow_mouse_drag = show_drag_handle || bottom_bar`
163
- // (adw-bottom-sheet.c:455-457). This element used to give it
164
- // role="button", tabindex=0 and a click-to-close handler, which is the
165
- // one behaviour it provably does not have.
145
+ // libadwaita builds the handle with can_focus = FALSE and can_target = FALSE: a
146
+ // decorative pill that cannot be clicked or focused at all. Its only behavioural
147
+ // role is elsewhere — `allow_mouse_drag = show_drag_handle || bottom_bar`. Giving
148
+ // it role="button" and a click-to-close handler is the one behaviour it provably
149
+ // does not have.
166
150
  this._dragHandleEl = document.createElement('div');
167
151
  this._dragHandleEl.className = 'adw-bottom-sheet-drag-handle';
168
152
  this._dragHandleEl.setAttribute('aria-hidden', 'true');
@@ -175,12 +159,11 @@ export class AdwBottomSheet extends HTMLElement {
175
159
 
176
160
  this.replaceChildren(this._contentEl, this._dimmingEl, this._sheetEl);
177
161
 
178
- // Escape → maybe_close_cb (adw-bottom-sheet.c:389-400). In GTK the
179
- // shortcut sits on the sheet itself, so it fires exactly while the sheet
180
- // has focus — INCLUDING while the sheet is closed, which is the corner
181
- // that still emits `close-attempt`. This port has no focus transfer into
182
- // the sheet, so an open sheet counts as the focused surface and a closed
183
- // one only when focus is still inside its (off-screen) body.
162
+ // Escape → `maybe_close_cb`. In GTK the shortcut sits on the sheet itself, so it
163
+ // fires exactly while the sheet has focus — INCLUDING while the sheet is closed,
164
+ // which is the corner that still emits `close-attempt`. This port has no focus
165
+ // transfer into the sheet, so an open sheet counts as the focused surface and a
166
+ // closed one only when focus is still inside its (off-screen) body.
184
167
  this.addEventListener('keydown', (event) => {
185
168
  if (event.key !== 'Escape') return;
186
169
  if (!this.open && !this._sheetEl.contains(event.target as Node)) return;
@@ -188,9 +171,8 @@ export class AdwBottomSheet extends HTMLElement {
188
171
  this.requestClose('escape');
189
172
  });
190
173
 
191
- // Adopt whatever the author declared, then start mirroring state changes
192
- // back out. Seeding before the subscription keeps the initial value
193
- // silent, as an attribute set during upgrade has always been.
174
+ // Seeding BEFORE the subscription keeps the initial value silent, which is what
175
+ // an attribute set during upgrade must be.
194
176
  this._state.setCanClose(this._boolAttr('can-close', true));
195
177
  this._state.setOpen(this._boolAttr('open', false));
196
178
  this._state.subscribe((open) => {
@@ -234,13 +216,10 @@ export class AdwBottomSheet extends HTMLElement {
234
216
  this.classList.toggle('modal', this.modal);
235
217
  this.classList.toggle('has-drag-handle', this.showDragHandle);
236
218
 
237
- // The dimming layer is only meaningful while modal; it fades in with the
238
- // sheet and is non-interactive when closed.
239
219
  this._dimmingEl.classList.toggle('visible', open && this.modal);
240
220
  this._dragHandleEl.hidden = !this.showDragHandle;
241
221
  }
242
222
 
243
- /** Mirror the state onto the `open` attribute (the CSS + markup hook). */
244
223
  private _reflectOpen(open: boolean): void {
245
224
  this._reflecting = true;
246
225
  if (open) this.setAttribute('open', '');
@@ -265,7 +244,7 @@ export class AdwBottomSheet extends HTMLElement {
265
244
  *
266
245
  * The `<adw-bottom-sheet-sheet>` / `<adw-bottom-sheet-content>` wrappers are
267
246
  * markup, not widgets — GtkBuilder's `<child type="sheet">` leaves nothing in
268
- * the tree (adw-bottom-sheet.c:1278-1293) — so the wrapper itself is claimed
247
+ * the tree — so the wrapper itself is claimed
269
248
  * too. It used to fall through to `unslotted` and be appended to the content
270
249
  * layer, leaving an empty custom element behind after its children were
271
250
  * pulled out.