@gjsify/adwaita-web 0.31.0 → 0.32.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 (176) hide show
  1. package/dist/adwaita-web.css +786 -272
  2. package/dist/adwaita-web.css.map +1 -1
  3. package/lib/types/adw-about-dialog.spec.d.ts +1 -0
  4. package/lib/types/adw-action-rows.spec.d.ts +1 -0
  5. package/lib/types/adw-avatar.spec.d.ts +1 -0
  6. package/lib/types/adw-banner.spec.d.ts +1 -0
  7. package/lib/types/adw-button-content.spec.d.ts +1 -0
  8. package/lib/types/adw-carousel.spec.d.ts +1 -0
  9. package/lib/types/adw-checks.spec.d.ts +1 -0
  10. package/lib/types/adw-header-bar.spec.d.ts +1 -0
  11. package/lib/types/adw-icon.spec.d.ts +1 -0
  12. package/lib/types/adw-progress-bar.spec.d.ts +1 -0
  13. package/lib/types/adw-sidebar.spec.d.ts +1 -0
  14. package/lib/types/adw-switch.spec.d.ts +1 -0
  15. package/lib/types/adw-wrap-box.spec.d.ts +1 -0
  16. package/lib/types/bottom-sheet.spec.d.ts +1 -0
  17. package/lib/types/breakpoints.d.ts +22 -0
  18. package/lib/types/breakpoints.spec.d.ts +1 -0
  19. package/lib/types/chrome.spec.d.ts +1 -0
  20. package/lib/types/elements/adw-about-dialog.d.ts +47 -8
  21. package/lib/types/elements/adw-action-row.d.ts +35 -0
  22. package/lib/types/elements/adw-avatar.d.ts +11 -0
  23. package/lib/types/elements/adw-bottom-sheet.d.ts +33 -4
  24. package/lib/types/elements/adw-button-content.d.ts +12 -0
  25. package/lib/types/elements/adw-button-row.d.ts +10 -1
  26. package/lib/types/elements/adw-carousel.d.ts +111 -8
  27. package/lib/types/elements/adw-checks.d.ts +57 -0
  28. package/lib/types/elements/adw-clamp.d.ts +4 -1
  29. package/lib/types/elements/adw-data-grid.d.ts +11 -31
  30. package/lib/types/elements/adw-drop-down.d.ts +17 -20
  31. package/lib/types/elements/adw-entry-row.d.ts +93 -3
  32. package/lib/types/elements/adw-expander-row.d.ts +7 -1
  33. package/lib/types/elements/adw-header-bar.d.ts +15 -0
  34. package/lib/types/elements/adw-icon.d.ts +25 -0
  35. package/lib/types/elements/adw-inline-view-switcher.d.ts +37 -11
  36. package/lib/types/elements/adw-menu-button.d.ts +6 -6
  37. package/lib/types/elements/adw-navigation-split-view.d.ts +6 -0
  38. package/lib/types/elements/adw-navigation-view.d.ts +63 -20
  39. package/lib/types/elements/adw-overlay-split-view.d.ts +38 -0
  40. package/lib/types/elements/adw-password-entry-row.d.ts +19 -10
  41. package/lib/types/elements/adw-popover.d.ts +58 -0
  42. package/lib/types/elements/adw-preferences-dialog.d.ts +57 -2
  43. package/lib/types/elements/adw-preferences-group.d.ts +31 -0
  44. package/lib/types/elements/adw-progress-bar.d.ts +38 -0
  45. package/lib/types/elements/adw-sidebar.d.ts +37 -5
  46. package/lib/types/elements/adw-split-button.d.ts +53 -7
  47. package/lib/types/elements/adw-switch-row.d.ts +11 -2
  48. package/lib/types/elements/adw-switch.d.ts +15 -0
  49. package/lib/types/elements/adw-tab-view.d.ts +129 -12
  50. package/lib/types/elements/adw-toolbar-view.d.ts +5 -0
  51. package/lib/types/elements/adw-view-stack.d.ts +27 -19
  52. package/lib/types/elements/adw-view-switcher-bar.d.ts +23 -7
  53. package/lib/types/elements/adw-view-switcher.d.ts +22 -11
  54. package/lib/types/elements/adw-window-title.d.ts +3 -1
  55. package/lib/types/elements/view-switcher-dom.d.ts +44 -0
  56. package/lib/types/entry-rows.spec.d.ts +1 -0
  57. package/lib/types/index.d.ts +7 -0
  58. package/lib/types/navigation-view.spec.d.ts +1 -0
  59. package/lib/types/preferences-search.d.ts +20 -0
  60. package/lib/types/preferences.spec.d.ts +1 -0
  61. package/lib/types/split-button.spec.d.ts +1 -0
  62. package/lib/types/split-views.spec.d.ts +1 -0
  63. package/lib/types/styles.generated.d.ts +1 -1
  64. package/lib/types/tab-view.spec.d.ts +1 -0
  65. package/lib/types/view-stack.spec.d.ts +1 -0
  66. package/lib/types/view-switcher.spec.d.ts +1 -0
  67. package/package.json +6 -6
  68. package/scss/_banner.scss +13 -0
  69. package/scss/_bottom_sheet.scss +5 -6
  70. package/scss/_button.scss +3 -26
  71. package/scss/_button_content.scss +28 -0
  72. package/scss/_checks.scss +228 -0
  73. package/scss/_clamp.scss +9 -3
  74. package/scss/_data_grid.scss +15 -7
  75. package/scss/_drop_down.scss +12 -55
  76. package/scss/_expander_row.scss +5 -50
  77. package/scss/_icon.scss +51 -0
  78. package/scss/_icons.generated.scss +6 -0
  79. package/scss/_inline_view_switcher.scss +34 -0
  80. package/scss/_labels.scss +135 -0
  81. package/scss/_menu_button.scss +10 -70
  82. package/scss/_popover.scss +186 -0
  83. package/scss/_preferences.scss +29 -0
  84. package/scss/_preferences_dialog.scss +17 -0
  85. package/scss/_progress_bar.scss +161 -0
  86. package/scss/_reset.scss +18 -7
  87. package/scss/_spinner.scss +7 -3
  88. package/scss/_split_button.scss +6 -50
  89. package/scss/_switch.scss +90 -0
  90. package/scss/_switch_row.scss +6 -52
  91. package/scss/_tab_view.scss +64 -0
  92. package/scss/_theme.scss +10 -0
  93. package/scss/_toolbar_view.scss +74 -0
  94. package/scss/_variables.scss +34 -0
  95. package/scss/_view_switcher.scss +40 -0
  96. package/scss/_view_switcher_bar.scss +37 -0
  97. package/scss/_wrap_box.scss +56 -6
  98. package/scss/adwaita-skin.scss +23 -0
  99. package/src/adw-about-dialog.spec.ts +350 -0
  100. package/src/adw-action-rows.spec.ts +342 -0
  101. package/src/adw-avatar.spec.ts +139 -0
  102. package/src/adw-banner.spec.ts +152 -0
  103. package/src/adw-button-content.spec.ts +183 -0
  104. package/src/adw-button-row.spec.ts +12 -6
  105. package/src/adw-carousel.spec.ts +389 -0
  106. package/src/adw-checks.spec.ts +368 -0
  107. package/src/adw-drop-down.spec.ts +123 -0
  108. package/src/adw-header-bar.spec.ts +83 -0
  109. package/src/adw-icon.spec.ts +167 -0
  110. package/src/adw-progress-bar.spec.ts +173 -0
  111. package/src/adw-sidebar.spec.ts +332 -0
  112. package/src/adw-switch.spec.ts +242 -0
  113. package/src/adw-tab-view.spec.ts +12 -3
  114. package/src/adw-view-switcher-bar.spec.ts +61 -13
  115. package/src/adw-wrap-box.spec.ts +194 -0
  116. package/src/bottom-sheet.spec.ts +325 -0
  117. package/src/breakpoints.spec.ts +147 -0
  118. package/src/breakpoints.ts +67 -0
  119. package/src/chrome.spec.ts +301 -0
  120. package/src/elements/adw-about-dialog.ts +223 -163
  121. package/src/elements/adw-action-row.ts +120 -10
  122. package/src/elements/adw-avatar.ts +48 -54
  123. package/src/elements/adw-banner.ts +46 -10
  124. package/src/elements/adw-bottom-sheet.ts +145 -60
  125. package/src/elements/adw-button-content.ts +107 -33
  126. package/src/elements/adw-button-row.ts +61 -37
  127. package/src/elements/adw-button.ts +5 -7
  128. package/src/elements/adw-carousel.ts +467 -105
  129. package/src/elements/adw-checks.ts +299 -0
  130. package/src/elements/adw-clamp.ts +80 -10
  131. package/src/elements/adw-combo-row.ts +15 -7
  132. package/src/elements/adw-data-grid.ts +43 -91
  133. package/src/elements/adw-drop-down.ts +117 -128
  134. package/src/elements/adw-entry-row.ts +404 -36
  135. package/src/elements/adw-expander-row.ts +61 -29
  136. package/src/elements/adw-header-bar.ts +31 -6
  137. package/src/elements/adw-icon.ts +148 -0
  138. package/src/elements/adw-inline-view-switcher.ts +240 -129
  139. package/src/elements/adw-menu-button.ts +93 -70
  140. package/src/elements/adw-navigation-split-view.ts +48 -7
  141. package/src/elements/adw-navigation-view.ts +286 -148
  142. package/src/elements/adw-overlay-split-view.ts +164 -14
  143. package/src/elements/adw-password-entry-row.ts +111 -89
  144. package/src/elements/adw-popover.ts +283 -0
  145. package/src/elements/adw-preferences-dialog.ts +120 -21
  146. package/src/elements/adw-preferences-group.ts +128 -22
  147. package/src/elements/adw-progress-bar.ts +189 -0
  148. package/src/elements/adw-sidebar.ts +301 -142
  149. package/src/elements/adw-spin-row.ts +11 -5
  150. package/src/elements/adw-spinner.ts +23 -8
  151. package/src/elements/adw-split-button.ts +371 -92
  152. package/src/elements/adw-status-page.ts +9 -9
  153. package/src/elements/adw-switch-row.ts +100 -30
  154. package/src/elements/adw-switch.ts +129 -0
  155. package/src/elements/adw-tab-view.ts +682 -129
  156. package/src/elements/adw-toggle-group.ts +5 -7
  157. package/src/elements/adw-toolbar-view.ts +84 -21
  158. package/src/elements/adw-view-stack.ts +162 -85
  159. package/src/elements/adw-view-switcher-bar.ts +163 -81
  160. package/src/elements/adw-view-switcher.ts +231 -112
  161. package/src/elements/adw-window-title.ts +44 -11
  162. package/src/elements/adw-wrap-box.ts +131 -35
  163. package/src/elements/view-switcher-dom.ts +108 -0
  164. package/src/entry-rows.spec.ts +391 -0
  165. package/src/index.ts +23 -0
  166. package/src/navigation-view.spec.ts +435 -0
  167. package/src/preferences-search.ts +135 -0
  168. package/src/preferences.spec.ts +250 -0
  169. package/src/source-view/adw-source-view.ts +2 -5
  170. package/src/split-button.spec.ts +470 -0
  171. package/src/split-views.spec.ts +309 -0
  172. package/src/styles.generated.ts +1 -1
  173. package/src/tab-view.spec.ts +451 -0
  174. package/src/test.browser.mts +49 -0
  175. package/src/view-stack.spec.ts +272 -0
  176. package/src/view-switcher.spec.ts +446 -0
@@ -1,19 +1,50 @@
1
1
  // <adw-action-row> — A boxed-list row with a title, optional subtitle, and
2
2
  // prefix / suffix widget slots. The most fundamental Adwaita row type.
3
3
  // Attributes: title, subtitle, activatable.
4
+ // Properties: activatableWidget (get/set) — the Adw.ActionRow:activatable-widget
5
+ // the row forwards its activation to, and whose sensitivity drives whether the
6
+ // row is activatable at all.
4
7
  // Slots: slot="prefix" (icons/widgets before the text), slot="suffix" (controls
5
8
  // after the text — switches, buttons, value labels).
6
9
  // Events: `activated` (CustomEvent) when an `activatable` row is clicked.
10
+ //
11
+ // The LABEL VISIBILITY rule and the activatable-widget coupling are HEADLESS and
12
+ // live in `@gjsify/adwaita-core` (ADR 0004) as {@link ActionRowState}; this
13
+ // element composes it and keeps only the DOM render half.
14
+ // `@gjsify/adwaita-nativescript` composes the same state, so both ports share one
15
+ // behaviour.
16
+ //
7
17
  // Reference: refs/adwaita-web/adwaita-web/docs/widgets/actionrow.md
18
+ // Reference: refs/libadwaita/src/adw-action-row.c, adw-action-row.ui
8
19
  // Reference: refs/libadwaita/src/stylesheet/widgets/_lists.scss (.row styling)
9
20
  // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
10
- // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
21
+ // Modifications: Implemented as a Web Component for @gjsify/adwaita-web; the
22
+ // label + activation state composed from @gjsify/adwaita-core.
23
+
24
+ import { ActionRowState } from '@gjsify/adwaita-core';
25
+
26
+ /**
27
+ * Attributes whose change means the activatable widget's sensitivity may have
28
+ * changed. `disabled` is the DOM's `GtkWidget:sensitive` for form controls;
29
+ * `aria-disabled` is the only spelling a non-form element has.
30
+ */
31
+ const SENSITIVITY_ATTRIBUTES = ['disabled', 'aria-disabled'];
32
+
33
+ /** Whether `widget` is sensitive, i.e. NOT disabled in either DOM spelling. */
34
+ function isSensitive(widget: HTMLElement): boolean {
35
+ if ((widget as HTMLElement & { disabled?: boolean }).disabled === true) return false;
36
+ return widget.getAttribute('aria-disabled') !== 'true';
37
+ }
11
38
 
12
39
  export class AdwActionRow extends HTMLElement {
13
40
  private _titleEl!: HTMLSpanElement;
14
41
  private _subtitleEl!: HTMLSpanElement;
15
42
  private _suffixEl!: HTMLDivElement;
16
43
  private _prefixEl!: HTMLDivElement;
44
+ /** The headless labels + activatable-widget coupling (ADR 0004). */
45
+ private readonly _state = new ActionRowState<HTMLElement>();
46
+ /** Watches the activatable widget's `disabled`/`aria-disabled` — the live binding. */
47
+ private _sensitivityObserver: MutationObserver | null = null;
17
48
  private _initialized = false;
18
49
 
19
50
  static get observedAttributes() {
@@ -30,6 +61,28 @@ export class AdwActionRow extends HTMLElement {
30
61
  return this._prefixEl;
31
62
  }
32
63
 
64
+ /** `Adw.ActionRow:activatable-widget` — the widget this row activates. */
65
+ get activatableWidget(): HTMLElement | null {
66
+ return this._state.activatableWidget;
67
+ }
68
+
69
+ /**
70
+ * Set the activatable widget. Its sensitivity is copied into `activatable`
71
+ * right away and then tracked, exactly as libadwaita's `sensitive` →
72
+ * `activatable` binding does (adw-action-row.c:729-732) — including the part
73
+ * that surprises: clearing the widget LEAVES the row activatable (C:709).
74
+ */
75
+ set activatableWidget(widget: HTMLElement | null) {
76
+ if (!this._state.setActivatableWidget(widget, widget ? isSensitive(widget) : true)) return;
77
+ this._observeSensitivity(widget);
78
+ this._syncActivatable();
79
+ }
80
+
81
+ /** Whether a click activates this row — `GtkListBoxRow:activatable`. */
82
+ get activatable(): boolean {
83
+ return this._state.activatable;
84
+ }
85
+
33
86
  connectedCallback() {
34
87
  if (this._initialized) return;
35
88
  this._initialized = true;
@@ -54,28 +107,85 @@ export class AdwActionRow extends HTMLElement {
54
107
  for (const child of suffixChildren) this._suffixEl.appendChild(child);
55
108
 
56
109
  this.replaceChildren(this._prefixEl, textEl, this._suffixEl);
110
+ // Adopt the declared attributes; `attributeChangedCallback` was guarded
111
+ // until now, so a parsed row would otherwise start with a stale state.
112
+ this._adoptAttributes();
57
113
  this._render();
58
114
 
59
- this.addEventListener('click', () => {
60
- if (this.hasAttribute('activatable')) {
61
- this.dispatchEvent(new CustomEvent('activated', { bubbles: true }));
62
- }
63
- });
115
+ this.addEventListener('click', (event) => this._onClick(event));
116
+ }
117
+
118
+ /** Copy the declarative attributes into the headless state. */
119
+ private _adoptAttributes() {
120
+ this._state.setTitle(this.getAttribute('title'));
121
+ this._state.setSubtitle(this.getAttribute('subtitle'));
122
+ this._state.setActivatable(this.hasAttribute('activatable'));
123
+ }
124
+
125
+ disconnectedCallback() {
126
+ this._sensitivityObserver?.disconnect();
127
+ this._sensitivityObserver = null;
64
128
  }
65
129
 
66
130
  attributeChangedCallback() {
131
+ if (!this._initialized) return;
132
+ this._adoptAttributes();
133
+ this._render();
134
+ }
135
+
136
+ /**
137
+ * `adw_action_row_activate_real` (adw-action-row.c:297-307): forward the
138
+ * activation to the activatable widget, then emit `activated`.
139
+ *
140
+ * A click that ORIGINATED inside the activatable widget does not activate the
141
+ * row at all — no forwarding, no `activated`. That is GTK's answer too: the
142
+ * widget claims the gesture, so `GtkListBox` never emits `row-activated`, and
143
+ * `pressed_cb` denies anything picked below the row / header / prefixes /
144
+ * suffixes (C:120-137). It is also what keeps the forwarding from looping —
145
+ * the synthetic click dispatched below bubbles straight back into this same
146
+ * listener, and without the guard one activation emitted `activated` twice.
147
+ */
148
+ private _onClick(event: Event) {
149
+ if (!this._state.activatable) return;
150
+ const target = this._state.activate();
151
+ if (target && event.target instanceof Node && target.contains(event.target)) return;
152
+ target?.click();
153
+ this.dispatchEvent(new CustomEvent('activated', { bubbles: true }));
154
+ }
155
+
156
+ /** (Re)point the sensitivity watcher at `widget`, or stop watching. */
157
+ private _observeSensitivity(widget: HTMLElement | null) {
158
+ this._sensitivityObserver?.disconnect();
159
+ if (!widget) {
160
+ this._sensitivityObserver = null;
161
+ return;
162
+ }
163
+ this._sensitivityObserver ??= new MutationObserver(() => {
164
+ const current = this._state.activatableWidget;
165
+ if (current && this._state.setActivatableWidgetSensitive(isSensitive(current))) this._syncActivatable();
166
+ });
167
+ this._sensitivityObserver.observe(widget, { attributeFilter: SENSITIVITY_ATTRIBUTES });
168
+ }
169
+
170
+ /** Reflect the state's `activatable` back onto the attribute + class. */
171
+ private _syncActivatable() {
172
+ // Re-enters attributeChangedCallback, whose `setActivatable` is then a
173
+ // no-op — the state already holds this value.
174
+ this.toggleAttribute('activatable', this._state.activatable);
67
175
  if (this._initialized) this._render();
68
176
  }
69
177
 
70
178
  private _render() {
71
- const title = this.getAttribute('title') ?? '';
72
- const subtitle = this.getAttribute('subtitle') ?? '';
179
+ const { title, titleVisible, subtitle, subtitleVisible } = this._state.state;
73
180
  this._titleEl.textContent = title;
181
+ // `string_is_not_empty` applies to the TITLE as much as to the subtitle
182
+ // (adw-action-row.ui:49-53) — an empty title used to keep its line here.
183
+ this._titleEl.hidden = !titleVisible;
74
184
  this._subtitleEl.textContent = subtitle;
75
- this._subtitleEl.hidden = subtitle.length === 0;
185
+ this._subtitleEl.hidden = !subtitleVisible;
76
186
  this._prefixEl.hidden = this._prefixEl.childElementCount === 0;
77
187
  this._suffixEl.hidden = this._suffixEl.childElementCount === 0;
78
- this.classList.toggle('activatable', this.hasAttribute('activatable'));
188
+ this.classList.toggle('activatable', this._state.activatable);
79
189
  }
80
190
  }
81
191
 
@@ -3,53 +3,26 @@
3
3
  // Attributes: text, size (px, default 48), show-initials (boolean),
4
4
  // icon (symbolic fallback name, with or without -symbolic).
5
5
  // Reference: refs/adwaita-web/adwaita-web/scss/_avatar.scss
6
- // Reference: refs/libadwaita/src/adw-avatar.c (initials + colour derivation)
7
6
  // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
8
- // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
7
+ // Modifications: Implemented as a Web Component for @gjsify/adwaita-web; the
8
+ // fallback icon node is <adw-icon>.
9
+ //
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`.
9
18
 
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
- ];
19
+ import { avatarColor, avatarFontSize, avatarInitials, avatarMaxFontSize, avatarMode } from '@gjsify/adwaita-core';
30
20
 
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
- }
21
+ import { type AdwIcon, createAdwIcon } from './adw-icon.js';
49
22
 
50
23
  export class AdwAvatar extends HTMLElement {
51
24
  private _textEl!: HTMLSpanElement;
52
- private _iconEl!: HTMLSpanElement;
25
+ private _iconEl!: AdwIcon;
53
26
  private _initialized = false;
54
27
 
55
28
  static get observedAttributes() {
@@ -63,9 +36,7 @@ export class AdwAvatar extends HTMLElement {
63
36
  this._textEl = document.createElement('span');
64
37
  this._textEl.className = 'adw-avatar-text';
65
38
 
66
- this._iconEl = document.createElement('span');
67
- this._iconEl.className = 'adw-avatar-icon adw-icon';
68
- this._iconEl.setAttribute('aria-hidden', 'true');
39
+ this._iconEl = createAdwIcon(null, 'adw-avatar-icon');
69
40
 
70
41
  this.replaceChildren(this._textEl, this._iconEl);
71
42
  this._render();
@@ -79,30 +50,53 @@ export class AdwAvatar extends HTMLElement {
79
50
  const size = parseFloat(this.getAttribute('size') || '48');
80
51
  this.style.width = `${size}px`;
81
52
  this.style.height = `${size}px`;
82
- this.style.fontSize = `${Math.round(size * (size < 32 ? 0.5 : 0.4))}px`;
83
53
 
84
54
  const text = this.getAttribute('text') ?? '';
85
- const initials = initialsOf(text);
86
- const showInitials = this.hasAttribute('show-initials') && initials.length > 0;
55
+ // The gate is the TEXT, not the derived initials — `update_visibility`
56
+ // keeps a whitespace-only name in initials mode with a blank label.
57
+ const mode = avatarMode({
58
+ hasCustomImage: false,
59
+ showInitials: this.hasAttribute('show-initials'),
60
+ text,
61
+ });
87
62
 
88
- if (showInitials) {
89
- const { fg, start, stop } = colorFor(text || 'default');
63
+ if (mode === 'initials') {
64
+ const { fg, start, stop } = avatarColor(text);
90
65
  this.style.backgroundImage = `linear-gradient(${start}, ${stop})`;
91
66
  this.style.color = fg;
92
- this._textEl.textContent = initials;
67
+ this._textEl.textContent = avatarInitials(text);
93
68
  this._textEl.hidden = false;
94
69
  this._iconEl.hidden = true;
70
+ this._applyFontSize(size);
95
71
  } else {
96
72
  this.style.backgroundImage = '';
97
73
  this.style.color = '';
98
74
  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`;
75
+ this._iconEl.iconName = this.getAttribute('icon') ?? 'avatar-default';
76
+ // The glyph scales with the avatar: `size` sizes the BOX and the mask
77
+ // together, which is what `.adw-avatar-icon`'s `mask-size: contain`
78
+ // was there to approximate.
79
+ this._iconEl.size = Math.round(size * 0.55);
103
80
  this._iconEl.hidden = false;
104
81
  }
105
82
  }
83
+
84
+ /**
85
+ * Size the initials the way `update_font_size` does: measure the label, then
86
+ * scale its aspect ratio against the cap.
87
+ *
88
+ * Only the ratio matters and it is font-size invariant, so measuring AT the
89
+ * cap is the browser's equivalent of Pango's reset-then-measure. The old
90
+ * `size * (size < 32 ? 0.5 : 0.4)` guess overflowed that cap at sizes 28, 30,
91
+ * 31 and everything from 64 up — the initials spilled out of the circle —
92
+ * and was not monotonic across the 32px boundary.
93
+ */
94
+ private _applyFontSize(size: number) {
95
+ const cap = avatarMaxFontSize(size);
96
+ this._textEl.style.fontSize = `${cap}px`;
97
+ const { width, height } = this._textEl.getBoundingClientRect();
98
+ this._textEl.style.fontSize = `${avatarFontSize(size, { width, height })}px`;
99
+ }
106
100
  }
107
101
 
108
102
  customElements.define('adw-avatar', AdwAvatar);
@@ -1,19 +1,36 @@
1
1
  // <adw-banner> — A full-width strip carrying a single in-context message and an
2
2
  // optional action button.
3
- // Attributes: title, button-label, revealed (boolean), use-markup (boolean).
3
+ // Attributes: title, button-label, revealed (boolean), use-markup (boolean),
4
+ // button-style ("default" | "suggested").
4
5
  // 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
6
+ //
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).
12
+ //
13
+ // Reference: refs/libadwaita/src/adw-banner.c (AdwBanner)
14
+ // Reference: refs/libadwaita/src/adw-banner.ui
15
+ // Reference: refs/libadwaita/src/stylesheet/widgets/_toolbars.scss (banner :243-262)
7
16
  // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
8
17
  // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
9
18
 
19
+ import {
20
+ ADW_BANNER_BUTTON_STYLE_CLASSES,
21
+ bannerButtonStyleClasses,
22
+ bannerButtonText,
23
+ bannerButtonVisible,
24
+ parseBannerButtonStyle,
25
+ } from '@gjsify/adwaita-core';
26
+
10
27
  export class AdwBanner extends HTMLElement {
11
28
  private _titleEl!: HTMLSpanElement;
12
29
  private _button!: HTMLButtonElement;
13
30
  private _initialized = false;
14
31
 
15
32
  static get observedAttributes() {
16
- return ['title', 'button-label', 'revealed', 'use-markup'];
33
+ return ['title', 'button-label', 'revealed', 'use-markup', 'button-style'];
17
34
  }
18
35
 
19
36
  connectedCallback() {
@@ -39,17 +56,36 @@ export class AdwBanner extends HTMLElement {
39
56
 
40
57
  private _render() {
41
58
  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.
59
+ // DELIBERATE DEPARTURE FROM THE C DEFAULT. `Adw.Banner:use-markup`
60
+ // defaults to TRUE (adw-banner.c:422-425) and `@gjsify/adwaita-core`
61
+ // carries that as the spec value — but Pango markup is not HTML. Pango
62
+ // has a fixed tag set and no scripting; `innerHTML` will run an
63
+ // `onerror` handler. Adopting the C default here would turn every
64
+ // `title` attribute into an injection sink for a widget that is not one
65
+ // in GTK, so on this renderer markup is OPT-IN and the default is text.
66
+ // The opt-in is the developer asserting the title is trusted.
67
+ // The title label carries `use-underline=False` in the template
68
+ // (adw-banner.ui:20), so its underscores are literal — no stripping here.
45
69
  if (this.hasAttribute('use-markup')) this._titleEl.innerHTML = title;
46
70
  else this._titleEl.textContent = title;
47
71
 
72
+ // `label && label[0]` (:663) — a first-character test, so a label of
73
+ // 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
75
+ // mnemonic markers the attribute keeps.
48
76
  const buttonLabel = this.getAttribute('button-label') ?? '';
49
- this._button.textContent = buttonLabel;
50
- this._button.hidden = buttonLabel.length === 0;
77
+ this._button.textContent = bannerButtonText(buttonLabel);
78
+ this._button.hidden = !bannerButtonVisible(buttonLabel);
79
+
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.
82
+ const style = parseBannerButtonStyle(this.getAttribute('button-style'));
83
+ const applied = bannerButtonStyleClasses(style);
84
+ for (const cls of ADW_BANNER_BUTTON_STYLE_CLASSES) {
85
+ this._button.classList.toggle(cls, applied.includes(cls));
86
+ }
51
87
 
52
- // `revealed` (default false to match Adw.Banner) slides the strip in/out.
88
+ // `revealed` (default FALSE, :456-459) slides the strip in/out.
53
89
  this.classList.toggle('revealed', this.hasAttribute('revealed'));
54
90
  }
55
91
  }