@gjsify/adwaita-web 0.30.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,35 +1,85 @@
1
1
  // <adw-overlay-split-view> — Responsive sidebar that docks or overlays content.
2
2
  // Reference: Adw.OverlaySplitView from libadwaita.
3
3
  // Docs: refs/adwaita-web/adwaita-web/docs/widgets/overlaysplitview.md
4
+ //
5
+ // `breakpoint` takes an Adwaita condition (e.g. "max-width: 720px") and drives
6
+ // `collapsed` from the element's own box — the browser counterpart to an
7
+ // Adw.Breakpoint with an add_setter(), so the sidebar overlays on narrow widths
8
+ // without the application wiring a media query itself.
9
+
10
+ import {
11
+ DEFAULT_MAX_SIDEBAR_WIDTH,
12
+ DEFAULT_MIN_SIDEBAR_WIDTH,
13
+ DEFAULT_SIDEBAR_WIDTH_FRACTION,
14
+ OverlaySplitViewState,
15
+ resolveSidebarBounds,
16
+ type AdwPackType,
17
+ } from '@gjsify/adwaita-core';
18
+
19
+ import { bindBreakpointSetter } from '../breakpoints.js';
20
+
21
+ /**
22
+ * Read a boolean attribute the way a GTK property with a TRUE default has to be
23
+ * read: absence means the default, not false.
24
+ *
25
+ * HTML boolean attributes are presence-only, which can only ever express a FALSE
26
+ * default — so `show-sidebar` used to start hidden while `Adw.OverlaySplitView`
27
+ * starts shown (adw-overlay-split-view.c:974-976). `show-sidebar="false"` is the
28
+ * explicit off switch.
29
+ */
30
+ function readTriStateAttr(element: Element, name: string, fallback: boolean): boolean {
31
+ const raw = element.getAttribute(name);
32
+ if (raw === null) return fallback;
33
+ return raw !== 'false' && raw !== '0';
34
+ }
4
35
 
5
36
  export class AdwOverlaySplitView extends HTMLElement {
6
37
  private _initialized = false;
7
38
  private _sidebarEl!: HTMLDivElement;
8
39
  private _contentEl!: HTMLDivElement;
9
40
  private _backdropEl!: HTMLDivElement;
41
+ private _disposeBreakpoint: (() => void) | undefined;
42
+ /**
43
+ * The single source of truth for the property interplay, shared with the
44
+ * NativeScript renderer (ADR 0004). The element owns the attributes and the
45
+ * painting; every rule about what `collapsed` + `show-sidebar` + `pin-sidebar`
46
+ * MEAN together lives in core, held to `OVERLAY_COLLAPSE_VECTORS`.
47
+ */
48
+ private _state = new OverlaySplitViewState();
49
+ /** Re-entrancy guard for {@link _reflectShowSidebar}. */
50
+ private _reflecting = false;
10
51
 
52
+ /**
53
+ * Every attribute {@link _readAttribute} handles must be listed here — an
54
+ * unobserved attribute never reaches `attributeChangedCallback`, so its branch
55
+ * is DEAD after construction. `pin-sidebar` was missing, which made the
56
+ * property that suppresses the collapse coupling settable only in the markup
57
+ * a parser saw: `view.setAttribute('pin-sidebar', '')` did nothing at all.
58
+ */
11
59
  static get observedAttributes() {
12
60
  return [
13
61
  'show-sidebar',
14
62
  'collapsed',
63
+ 'pin-sidebar',
15
64
  'sidebar-position',
16
65
  'min-sidebar-width',
17
66
  'max-sidebar-width',
18
67
  'sidebar-width-fraction',
68
+ 'breakpoint',
19
69
  ];
20
70
  }
21
71
 
22
72
  get showSidebar(): boolean {
23
- return this.hasAttribute('show-sidebar');
73
+ return this._state.showSidebar;
24
74
  }
25
75
 
26
76
  set showSidebar(v: boolean) {
27
- if (v) this.setAttribute('show-sidebar', '');
28
- else this.removeAttribute('show-sidebar');
77
+ // `false` has to be spelled out — absence means the libadwaita default.
78
+ this.setAttribute('show-sidebar', v ? '' : 'false');
29
79
  }
30
80
 
31
81
  get collapsed(): boolean {
32
- return this.hasAttribute('collapsed');
82
+ return this._state.collapsed;
33
83
  }
34
84
 
35
85
  set collapsed(v: boolean) {
@@ -37,16 +87,23 @@ export class AdwOverlaySplitView extends HTMLElement {
37
87
  else this.removeAttribute('collapsed');
38
88
  }
39
89
 
90
+ // The three defaults come from the widget, not from an app's taste: 180 /
91
+ // 280 / 0.25 (adw-overlay-split-view.c:1036-1075). They used to be 280 /
92
+ // 400 / 0.30 here, which are the values the three.js and canvas2d showcases
93
+ // set EXPLICITLY in their `.blp` — so an app's preference had been copied
94
+ // into the widget. The visible cost was in the storybook, whose whole
95
+ // purpose is a 1:1 comparison: the GTK story takes the real defaults and the
96
+ // browser story took these, so the two sides showed different sidebars.
40
97
  get minSidebarWidth(): number {
41
- return parseFloat(this.getAttribute('min-sidebar-width') || '280');
98
+ return parseFloat(this.getAttribute('min-sidebar-width') || String(DEFAULT_MIN_SIDEBAR_WIDTH));
42
99
  }
43
100
 
44
101
  get maxSidebarWidth(): number {
45
- return parseFloat(this.getAttribute('max-sidebar-width') || '400');
102
+ return parseFloat(this.getAttribute('max-sidebar-width') || String(DEFAULT_MAX_SIDEBAR_WIDTH));
46
103
  }
47
104
 
48
105
  get sidebarWidthFraction(): number {
49
- return parseFloat(this.getAttribute('sidebar-width-fraction') || '0.30');
106
+ return parseFloat(this.getAttribute('sidebar-width-fraction') || String(DEFAULT_SIDEBAR_WIDTH_FRACTION));
50
107
  }
51
108
 
52
109
  connectedCallback() {
@@ -83,18 +140,95 @@ export class AdwOverlaySplitView extends HTMLElement {
83
140
  this.append(this._sidebarEl, this._contentEl, this._backdropEl);
84
141
 
85
142
  // Apply initial state
143
+ // The attributes present at parse time ARE the construction options —
144
+ // `Adw.OverlaySplitView` is built with its properties, it is not built
145
+ // and then mutated. Applying them as sequential setters instead would
146
+ // fire the collapse transition and auto-hide a sidebar that the markup
147
+ // explicitly asked to keep shown.
148
+ this._state = new OverlaySplitViewState({
149
+ collapsed: this.hasAttribute('collapsed'),
150
+ showSidebar: readTriStateAttr(this, 'show-sidebar', true),
151
+ pinSidebar: this.hasAttribute('pin-sidebar'),
152
+ sidebarPosition: this.getAttribute('sidebar-position') === 'end' ? 'end' : ('start' as AdwPackType),
153
+ });
154
+ this._reflectShowSidebar();
86
155
  this._syncClasses();
87
156
  this._syncSidebarWidth();
157
+ this._syncBreakpoint();
158
+ }
159
+
160
+ disconnectedCallback() {
161
+ this._disposeBreakpoint?.();
162
+ this._disposeBreakpoint = undefined;
88
163
  }
89
164
 
90
165
  attributeChangedCallback(_name: string, _old: string | null, _val: string | null) {
91
166
  if (!this._initialized) return;
167
+ if (_name === 'breakpoint') {
168
+ this._syncBreakpoint();
169
+ return;
170
+ }
171
+ this._readAttribute(_name);
92
172
  this._syncClasses();
93
173
  if (_name === 'min-sidebar-width' || _name === 'max-sidebar-width' || _name === 'sidebar-width-fraction') {
94
174
  this._syncSidebarWidth();
95
175
  }
96
176
  }
97
177
 
178
+ /**
179
+ * Push ONE attribute into the state, then reflect back what the state made
180
+ * of it.
181
+ *
182
+ * Per-attribute rather than a re-read of all of them, because `show-sidebar`
183
+ * is a value the state ALSO owns: collapsing auto-hides an unpinned sidebar,
184
+ * and a blanket re-read would immediately clobber that decision with the
185
+ * stale attribute.
186
+ */
187
+ private _readAttribute(name: string) {
188
+ const state = this._state;
189
+ // `pin-sidebar` defaults FALSE (adw-overlay-split-view.c:990-992), so it
190
+ // is an ordinary presence attribute; `show-sidebar` below is the one that
191
+ // cannot be, and `sidebar-position` is an enum defaulting to 'start'.
192
+ if (name === 'pin-sidebar') state.setPinSidebar(this.hasAttribute('pin-sidebar'));
193
+ if (name === 'sidebar-position') {
194
+ const position = this.getAttribute('sidebar-position');
195
+ state.setSidebarPosition(position === 'end' ? 'end' : ('start' as AdwPackType));
196
+ }
197
+ if (name === 'show-sidebar') {
198
+ state.setShowSidebar(readTriStateAttr(this, 'show-sidebar', true));
199
+ }
200
+ // Last: it can change `show-sidebar`, so it must not be overwritten after.
201
+ if (name === 'collapsed') state.setCollapsed(this.hasAttribute('collapsed'));
202
+ this._reflectShowSidebar();
203
+ }
204
+
205
+ /**
206
+ * Mirror the state's `showSidebar` back onto the attribute, so the DOM keeps
207
+ * telling the truth after an auto-hide.
208
+ *
209
+ * Guarded: writing the attribute re-enters `attributeChangedCallback`, and
210
+ * without the flag that is an infinite reflection loop rather than a render.
211
+ */
212
+ private _reflectShowSidebar() {
213
+ if (this._reflecting) return;
214
+ const wanted = this._state.showSidebar ? '' : 'false';
215
+ if (this.getAttribute('show-sidebar') === wanted) return;
216
+ this._reflecting = true;
217
+ try {
218
+ this.setAttribute('show-sidebar', wanted);
219
+ } finally {
220
+ this._reflecting = false;
221
+ }
222
+ }
223
+
224
+ /** (Re)bind the `breakpoint` condition to `collapsed`. */
225
+ private _syncBreakpoint() {
226
+ this._disposeBreakpoint?.();
227
+ this._disposeBreakpoint = bindBreakpointSetter(this, this.getAttribute('breakpoint'), (active) => {
228
+ this.collapsed = active;
229
+ });
230
+ }
231
+
98
232
  openSidebar() {
99
233
  this.showSidebar = true;
100
234
  this.dispatchEvent(new CustomEvent('sidebar-toggled', { detail: { isVisible: true } }));
@@ -115,11 +249,18 @@ export class AdwOverlaySplitView extends HTMLElement {
115
249
  }
116
250
 
117
251
  private _syncClasses() {
118
- this.classList.toggle('collapsed', this.collapsed);
119
- this.classList.toggle('show-sidebar', this.showSidebar);
120
- const pos = this.getAttribute('sidebar-position') || 'start';
121
- this.classList.toggle('sidebar-start', pos === 'start');
122
- this.classList.toggle('sidebar-end', pos === 'end');
252
+ const state = this._state;
253
+ this.classList.toggle('collapsed', state.collapsed);
254
+ this.classList.toggle('show-sidebar', state.showSidebar);
255
+ this.classList.toggle('sidebar-start', state.sidebarPosition === 'start');
256
+ this.classList.toggle('sidebar-end', state.sidebarPosition === 'end');
257
+
258
+ // Derived, not re-decided here: the shield only exists while collapsed
259
+ // AND revealed, and each pane is reachable by keyboard only when it is
260
+ // the one on screen.
261
+ if (this._backdropEl) this._backdropEl.hidden = !state.shieldVisible;
262
+ this._sidebarEl?.setAttribute('aria-hidden', String(!state.sidebarFocusable));
263
+ this._contentEl?.setAttribute('aria-hidden', String(!state.contentFocusable));
123
264
 
124
265
  // In docked mode, use negative margin to collapse sidebar space
125
266
  // while keeping the sidebar's intrinsic width (slide animation).
@@ -135,8 +276,17 @@ export class AdwOverlaySplitView extends HTMLElement {
135
276
 
136
277
  private _syncSidebarWidth() {
137
278
  if (!this._sidebarEl) return;
138
- this._sidebarEl.style.minWidth = `${this.minSidebarWidth}px`;
139
- this._sidebarEl.style.maxWidth = `${this.maxSidebarWidth}px`;
279
+ // Normalise through the core rather than writing the raw attributes:
280
+ // libadwaita never lets `max` fall below `min`, and CSS resolves that
281
+ // conflict the other way round (min-width wins over max-width), so an
282
+ // inverted pair would render differently here than in GTK.
283
+ const bounds = resolveSidebarBounds(
284
+ { minSidebarWidth: this.minSidebarWidth, maxSidebarWidth: this.maxSidebarWidth },
285
+ 0,
286
+ { ceil: true },
287
+ );
288
+ this._sidebarEl.style.minWidth = `${bounds.min}px`;
289
+ this._sidebarEl.style.maxWidth = `${bounds.max}px`;
140
290
  this._sidebarEl.style.width = `${this.sidebarWidthFraction * 100}%`;
141
291
  }
142
292
  }
@@ -1,120 +1,142 @@
1
- // <adw-password-entry-row> — A boxed-list entry row tailored for secrets: the
2
- // title floats as a label, the editable text is masked, and a trailing peek
3
- // button toggles between hiding and revealing the contents. A specialization of
4
- // <adw-entry-row> (Adw.PasswordEntryRow extends Adw.EntryRow).
5
- // Attributes: title, text, revealed (boolean — show the contents in clear text).
6
- // Property: text (get/set, proxies the inner input); revealed (get/set).
7
- // Events: `changed` (CustomEvent) on every edit — mirrors Adw.EntryRow's
8
- // `changed` signal so it drives the same code path as the GTK renderer.
9
- // `notify::revealed` (CustomEvent) when the peek toggle flips visibility.
1
+ // <adw-password-entry-row> — An entry row tailored for secrets: the value is
2
+ // masked, a trailing peek button reveals it, and a warning shows while Caps Lock
3
+ // is engaged.
4
+ //
5
+ // It now EXTENDS `<adw-entry-row>`, as `AdwPasswordEntryRow` extends
6
+ // `AdwEntryRow` in the C (adw-password-entry-row.c:50). This file used to be a
7
+ // near copy-paste of `adw-entry-row.ts` — a normalized diff showed the
8
+ // connectedCallback and attributeChangedCallback bodies were the same code — so
9
+ // every entry-row fix had to be made twice and one of the two always drifted.
10
+ // What is left here is only what the C subclass adds: the masked input, the peek
11
+ // toggle, and the caps-lock source.
12
+ //
13
+ // The reveal/caps-lock DERIVATION is HEADLESS and lives in
14
+ // `@gjsify/adwaita-core` (ADR 0004) as {@link PasswordEntryRowState}, which
15
+ // composes the parent's `EntryRowState` exactly like the C drives its
16
+ // parent through `adw_entry_row_set_show_indicator`. Two rules fall out of it
17
+ // that neither port had: peeking suppresses the caps-lock warning, and so does
18
+ // losing focus.
19
+ //
20
+ // Attributes: everything `<adw-entry-row>` observes, plus `revealed`.
21
+ // Properties: `revealed`, `capsLockOn` (+ everything inherited).
22
+ // Events: `notify::revealed` — only on a REAL change. The old implementation
23
+ // fired it on every `setAttribute`, including a redundant one, because
24
+ // attributeChangedCallback runs for a same-value write; the core's setter
25
+ // guard (the house rule at adw-entry-row.c:984/:1198/:1247) is what fixes it.
26
+ //
27
+ // Reference: refs/libadwaita/src/adw-password-entry-row.c
10
28
  // Reference: refs/adwaita-web/adwaita-web/docs/widgets/passwordentryrow.md
11
- // Reference: refs/libadwaita/src/adw-password-entry-row.c (peek toggle: the
12
- // view-reveal / view-conceal symbolic swap; CSS node `row` carries `.entry`
13
- // and `.password` style classes).
14
29
  // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
15
30
  // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
16
31
 
17
- export class AdwPasswordEntryRow extends HTMLElement {
18
- private _input!: HTMLInputElement;
19
- private _titleEl!: HTMLSpanElement;
20
- private _toggle!: HTMLButtonElement;
21
- private _toggleIcon!: HTMLSpanElement;
22
- private _initialized = false;
32
+ import { PasswordEntryRowState, type PasswordEntryRowRenderState } from '@gjsify/adwaita-core';
23
33
 
24
- static get observedAttributes() {
25
- return ['title', 'text', 'revealed'];
26
- }
34
+ import { type AdwIcon, createAdwIcon } from './adw-icon.js';
35
+
36
+ import { AdwEntryRow } from './adw-entry-row.js';
37
+
38
+ export class AdwPasswordEntryRow extends AdwEntryRow {
39
+ /** The headless peek + caps-lock derivation, composing the parent row's state. */
40
+ private readonly _password = new PasswordEntryRowState(this._state);
41
+ private _toggle!: HTMLButtonElement;
42
+ private _toggleIcon!: AdwIcon;
27
43
 
28
- get text(): string {
29
- return this._input ? this._input.value : (this.getAttribute('text') ?? '');
44
+ static get observedAttributes(): string[] {
45
+ return [...AdwEntryRow.observedAttributes, 'revealed'];
30
46
  }
31
47
 
32
- set text(value: string) {
33
- if (this._input) this._input.value = value;
34
- else this.setAttribute('text', value);
48
+ // `_password_entry_row.scss` scopes its rules to `.adw-password-entry-row-*`,
49
+ // so the inherited DOM builder must name the parts that way.
50
+ protected override get _classPrefix(): string {
51
+ return 'adw-password-entry-row';
35
52
  }
36
53
 
37
- /** Whether the contents are shown in clear text (peek toggle is engaged). */
54
+ /** Whether the contents are shown in clear text (`GtkText:visibility`). */
38
55
  get revealed(): boolean {
39
- return this.hasAttribute('revealed');
56
+ return this._password.revealed;
40
57
  }
41
58
 
42
59
  set revealed(value: boolean) {
43
- if (value) this.setAttribute('revealed', '');
44
- else this.removeAttribute('revealed');
60
+ this._password.setRevealed(value);
45
61
  }
46
62
 
47
- connectedCallback() {
48
- if (this._initialized) return;
49
- this._initialized = true;
63
+ /** Whether Caps Lock is engaged, as last observed from a keyboard event. */
64
+ get capsLockOn(): boolean {
65
+ return this._password.capsLockOn;
66
+ }
50
67
 
51
- this._titleEl = document.createElement('span');
52
- this._titleEl.className = 'adw-row-title';
53
- this._titleEl.textContent = this.getAttribute('title') ?? '';
68
+ /**
69
+ * Feed the platform's Caps Lock state in — GDK gets it from the keyboard
70
+ * device (adw-password-entry-row.c:111-115). Public because a host that
71
+ * knows better than `KeyboardEvent.getModifierState` (a virtual keyboard, a
72
+ * test) can drive it directly.
73
+ */
74
+ setCapsLockOn(on: boolean): void {
75
+ this._password.setCapsLockOn(on);
76
+ }
54
77
 
55
- this._input = document.createElement('input');
56
- this._input.className = 'adw-password-entry-row-input';
57
- // Masked by default — Adw.PasswordEntryRow sets GTK_INPUT_PURPOSE_PASSWORD
58
- // and starts with visibility off.
59
- this._input.type = this.hasAttribute('revealed') ? 'text' : 'password';
60
- this._input.value = this.getAttribute('text') ?? '';
78
+ protected override _onConnected(): void {
79
+ // `gtk_text_set_visibility (…, FALSE)` + GTK_INPUT_PURPOSE_PASSWORD (C:158-160).
80
+ this._input.type = 'password';
61
81
  this._input.autocomplete = 'off';
62
- this._input.addEventListener('input', () => {
63
- this.dispatchEvent(new CustomEvent('changed', { bubbles: true, detail: { text: this._input.value } }));
64
- });
65
-
66
- // Title floats as a small label above the masked value, both left-aligned
67
- // in a column (the filled Adw.EntryRow look).
68
- const text = document.createElement('div');
69
- text.className = 'adw-password-entry-row-text';
70
- text.append(this._titleEl, this._input);
71
82
 
72
- // Trailing peek toggle — a flat icon button, vertically centred. Swaps
73
- // between the reveal / conceal symbolics like the native widget.
74
83
  this._toggle = document.createElement('button');
75
- this._toggle.className = 'adw-button flat circular icon-only adw-password-entry-row-toggle';
76
84
  this._toggle.type = 'button';
77
- this._toggleIcon = document.createElement('span');
78
- this._toggleIcon.setAttribute('aria-hidden', 'true');
79
- this._toggle.appendChild(this._toggleIcon);
80
- this._toggle.addEventListener('click', () => {
81
- this.revealed = !this.revealed;
85
+ this._toggle.className = `adw-button flat circular icon-only ${this._classPrefix}-toggle`;
86
+ this._toggleIcon = createAdwIcon(null);
87
+ this._toggle.append(this._toggleIcon);
88
+ this._toggle.addEventListener('click', () => this._password.togglePeek());
89
+ // C:152 — installed through add_suffix, so it is the FIRST suffix and a
90
+ // consumer's own suffix lands after it instead of replacing it.
91
+ this.addSuffix(this._toggle);
92
+
93
+ // GDK reads caps lock off the keyboard device; the browser exposes it
94
+ // only on keyboard events, so those are the source here.
95
+ const readCapsLock = (event: KeyboardEvent) => this._password.setCapsLockOn(event.getModifierState('CapsLock'));
96
+ this._input.addEventListener('keydown', readCapsLock);
97
+ this._input.addEventListener('keyup', readCapsLock);
98
+
99
+ // The indicator icon + tooltip are set once at init (C:169-171); their
100
+ // canonical names come from the core so both ports spell them alike.
101
+ const initial = this._password.state;
102
+ this.setIndicatorIconName(initial.indicatorIconName);
103
+ this.setIndicatorTooltip(initial.indicatorTooltip);
104
+
105
+ // Seed from the attribute BEFORE subscribing, so the first paint emits no
106
+ // `notify::revealed`.
107
+ if (this.hasAttribute('revealed')) this._password.setRevealed(this.getAttribute('revealed') !== 'false');
108
+ this._renderPassword(this._password.state);
109
+ this._password.subscribe((state) => {
110
+ this._renderPassword(state);
111
+ this.dispatchEvent(
112
+ new CustomEvent('notify::revealed', { bubbles: true, detail: { revealed: state.revealed } }),
113
+ );
82
114
  });
83
-
84
- // Trailing edit pencil (before the peek toggle), mirroring native.
85
- const edit = document.createElement('span');
86
- edit.className = 'adw-row-edit adw-icon adw-icon--document-edit';
87
- edit.setAttribute('aria-hidden', 'true');
88
- edit.addEventListener('click', () => this._input.focus());
89
-
90
- this.replaceChildren(text, edit, this._toggle);
91
- this._renderRevealed();
92
115
  }
93
116
 
94
- attributeChangedCallback(name: string, _old: string | null, value: string | null) {
95
- if (!this._initialized) return;
96
- if (name === 'title') {
97
- this._titleEl.textContent = value ?? '';
98
- } else if (name === 'text') {
99
- if (this._input.value !== (value ?? '')) this._input.value = value ?? '';
100
- } else if (name === 'revealed') {
101
- this._renderRevealed();
102
- this.dispatchEvent(
103
- new CustomEvent('notify::revealed', { bubbles: true, detail: { revealed: this.revealed } }),
104
- );
117
+ override attributeChangedCallback(name: string, oldValue: string | null, value: string | null) {
118
+ if (name !== 'revealed') {
119
+ super.attributeChangedCallback(name, oldValue, value);
120
+ return;
105
121
  }
122
+ if (!this._initialized) return;
123
+ // The core guard makes this idempotent, which is what keeps the reflection
124
+ // in `_renderPassword` from bouncing back as a second notification.
125
+ this._password.setRevealed(value !== null && value !== 'false');
106
126
  }
107
127
 
108
- private _renderRevealed() {
109
- const revealed = this.revealed;
110
- this._input.type = revealed ? 'text' : 'password';
111
- // view-conceal when revealed (offer to hide), view-reveal when masked.
112
- const icon = revealed ? 'view-conceal' : 'view-reveal';
113
- this._toggleIcon.className = `adw-icon adw-icon--${icon}`;
114
- const labelText = revealed ? 'Hide Password' : 'Show Password';
115
- this._toggle.title = labelText;
116
- this._toggle.setAttribute('aria-label', labelText);
117
- this._toggle.setAttribute('aria-pressed', String(revealed));
128
+ /** `notify_visibility_cb` (C:62-81), applied to the DOM. */
129
+ private _renderPassword(state: PasswordEntryRowRenderState): void {
130
+ this._input.type = state.revealed ? 'text' : 'password';
131
+ // The libadwaita name travels in `data-icon-name`; the mask class is the
132
+ // curated @gjsify/adwaita-icons spelling of the same symbolic.
133
+ this._toggleIcon.dataset.iconName = state.peekIconName;
134
+ this._toggleIcon.iconName = state.peekIconName;
135
+ this._toggle.title = state.peekLabel;
136
+ this._toggle.setAttribute('aria-label', state.peekLabel);
137
+ this._toggle.setAttribute('aria-pressed', String(state.revealed));
138
+ if (state.revealed) this.setAttribute('revealed', '');
139
+ else this.removeAttribute('revealed');
118
140
  }
119
141
  }
120
142