@gjsify/adwaita-web 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +5 -5
  2. package/dist/adwaita-web.css +2742 -263
  3. package/dist/adwaita-web.css.map +1 -1
  4. package/lib/types/elements/adw-about-dialog.d.ts +77 -0
  5. package/lib/types/elements/adw-action-row.d.ts +15 -0
  6. package/lib/types/elements/adw-alert-dialog.d.ts +80 -0
  7. package/lib/types/elements/adw-avatar.d.ts +9 -0
  8. package/lib/types/elements/adw-banner.d.ts +9 -0
  9. package/lib/types/elements/adw-bottom-sheet.d.ts +35 -0
  10. package/lib/types/elements/adw-button-content.d.ts +10 -0
  11. package/lib/types/elements/adw-button-row.d.ts +11 -0
  12. package/lib/types/elements/adw-button.d.ts +11 -0
  13. package/lib/types/elements/adw-carousel.d.ts +64 -0
  14. package/lib/types/elements/adw-clamp.d.ts +6 -0
  15. package/lib/types/elements/adw-combo-row.d.ts +3 -0
  16. package/lib/types/elements/adw-entry-row.d.ts +10 -0
  17. package/lib/types/elements/adw-entry.d.ts +11 -0
  18. package/lib/types/elements/adw-expander-row.d.ts +26 -0
  19. package/lib/types/elements/adw-header-bar.d.ts +4 -0
  20. package/lib/types/elements/adw-inline-view-switcher.d.ts +30 -0
  21. package/lib/types/elements/adw-navigation-split-view.d.ts +15 -0
  22. package/lib/types/elements/adw-navigation-view.d.ts +66 -0
  23. package/lib/types/elements/adw-password-entry-row.d.ts +16 -0
  24. package/lib/types/elements/adw-preferences-dialog.d.ts +36 -0
  25. package/lib/types/elements/adw-preferences-group.d.ts +8 -0
  26. package/lib/types/elements/adw-sidebar.d.ts +29 -0
  27. package/lib/types/elements/adw-spin-row.d.ts +3 -0
  28. package/lib/types/elements/adw-spinner.d.ts +6 -0
  29. package/lib/types/elements/adw-split-button.d.ts +18 -0
  30. package/lib/types/elements/adw-status-page.d.ts +11 -0
  31. package/lib/types/elements/adw-switch-row.d.ts +3 -0
  32. package/lib/types/elements/adw-tab-view.d.ts +24 -0
  33. package/lib/types/elements/adw-toast-overlay.d.ts +19 -4
  34. package/lib/types/elements/adw-toggle-group.d.ts +19 -0
  35. package/lib/types/elements/adw-toolbar-view.d.ts +13 -0
  36. package/lib/types/elements/adw-view-switcher.d.ts +29 -0
  37. package/lib/types/elements/adw-window-title.d.ts +9 -0
  38. package/lib/types/elements/adw-wrap-box.d.ts +13 -0
  39. package/lib/types/index.d.ts +29 -0
  40. package/lib/types/styles.generated.d.ts +1 -0
  41. package/package.json +5 -5
  42. package/scss/_about_dialog.scss +257 -0
  43. package/scss/_action_row.scss +65 -0
  44. package/scss/_alert_dialog.scss +151 -0
  45. package/scss/_avatar.scss +31 -0
  46. package/scss/_banner.scss +46 -0
  47. package/scss/_bottom_sheet.scss +113 -0
  48. package/scss/_button.scss +168 -0
  49. package/scss/_button_content.scss +48 -0
  50. package/scss/_button_row.scss +100 -0
  51. package/scss/_carousel.scss +130 -0
  52. package/scss/_clamp.scss +11 -0
  53. package/scss/_entry.scss +49 -0
  54. package/scss/_entry_row.scss +54 -0
  55. package/scss/_expander_row.scss +150 -0
  56. package/scss/_headerbar.scss +1 -1
  57. package/scss/_inline_view_switcher.scss +174 -0
  58. package/scss/_navigation_split_view.scss +48 -0
  59. package/scss/_navigation_view.scss +80 -0
  60. package/scss/_overlay_split_view.scss +19 -0
  61. package/scss/_password_entry_row.scss +67 -0
  62. package/scss/_preferences.scss +26 -3
  63. package/scss/_preferences_dialog.scss +154 -0
  64. package/scss/_row.scss +31 -0
  65. package/scss/_sidebar.scss +213 -0
  66. package/scss/_spin_row.scss +0 -1
  67. package/scss/_spinner.scss +28 -0
  68. package/scss/_split_button.scss +222 -0
  69. package/scss/_status_page.scss +40 -0
  70. package/scss/_switch_row.scss +0 -1
  71. package/scss/_tab_view.scss +193 -0
  72. package/scss/_theme.scss +12 -0
  73. package/scss/_toast.scss +122 -12
  74. package/scss/_toggle_group.scss +124 -0
  75. package/scss/_toolbar_view.scss +31 -0
  76. package/scss/_variables.scss +19 -0
  77. package/scss/_view_switcher.scss +133 -0
  78. package/scss/_window_title.scss +33 -0
  79. package/scss/_wrap_box.scss +20 -0
  80. package/scss/adwaita-skin.scss +38 -0
  81. package/src/elements/adw-about-dialog.ts +629 -0
  82. package/src/elements/adw-action-row.ts +82 -0
  83. package/src/elements/adw-alert-dialog.ts +385 -0
  84. package/src/elements/adw-avatar.ts +108 -0
  85. package/src/elements/adw-banner.ts +57 -0
  86. package/src/elements/adw-bottom-sheet.ts +205 -0
  87. package/src/elements/adw-button-content.ts +69 -0
  88. package/src/elements/adw-button-row.ts +75 -0
  89. package/src/elements/adw-button.ts +81 -0
  90. package/src/elements/adw-carousel.ts +321 -0
  91. package/src/elements/adw-clamp.ts +28 -0
  92. package/src/elements/adw-combo-row.ts +32 -20
  93. package/src/elements/adw-entry-row.ts +69 -0
  94. package/src/elements/adw-entry.ts +61 -0
  95. package/src/elements/adw-expander-row.ts +167 -0
  96. package/src/elements/adw-header-bar.ts +21 -8
  97. package/src/elements/adw-inline-view-switcher.ts +222 -0
  98. package/src/elements/adw-navigation-split-view.ts +89 -0
  99. package/src/elements/adw-navigation-view.ts +344 -0
  100. package/src/elements/adw-password-entry-row.ts +121 -0
  101. package/src/elements/adw-preferences-dialog.ts +240 -0
  102. package/src/elements/adw-preferences-group.ts +79 -12
  103. package/src/elements/adw-sidebar.ts +238 -0
  104. package/src/elements/adw-spin-row.ts +28 -14
  105. package/src/elements/adw-spinner.ts +31 -0
  106. package/src/elements/adw-split-button.ts +206 -0
  107. package/src/elements/adw-status-page.ts +66 -0
  108. package/src/elements/adw-switch-row.ts +29 -23
  109. package/src/elements/adw-tab-view.ts +184 -0
  110. package/src/elements/adw-toast-overlay.ts +98 -19
  111. package/src/elements/adw-toggle-group.ts +126 -0
  112. package/src/elements/adw-toolbar-view.ts +60 -0
  113. package/src/elements/adw-view-switcher.ts +200 -0
  114. package/src/elements/adw-window-title.ts +48 -0
  115. package/src/elements/adw-wrap-box.ts +139 -0
  116. package/src/index.js +1 -1
  117. package/src/index.ts +46 -3
  118. package/src/styles.generated.js +2976 -0
  119. package/src/styles2.generated.js +2976 -0
  120. package/style.css.d.ts +17 -4
@@ -0,0 +1,344 @@
1
+ // <adw-navigation-view> — A page-based navigation container (the web
2
+ // counterpart of Adw.NavigationView). It presents one page at a time and keeps
3
+ // a navigation stack controlled with push()/pop()/replace(); the visible page
4
+ // fills the view. Only <adw-navigation-page> children are managed.
5
+ //
6
+ // Pages are declared as <adw-navigation-page> children (each with a `title` and
7
+ // optional `tag` attribute). The view snapshots them at connect time. Like
8
+ // Adw.NavigationView, a statically-added page is kept around but not shown until
9
+ // pushed (the first added page is pushed automatically). push() accepts a page
10
+ // element OR an existing page's tag (push-by-tag); a dynamically-pushed page is
11
+ // removed from the DOM again once it is popped.
12
+ //
13
+ // Header Bar integration: mirroring libadwaita, when the visible page is above
14
+ // the root the view surfaces an automatic back button at the start of that
15
+ // page's header bar (the first <adw-header-bar> found in the page content),
16
+ // unless the page sets `no-back-button` (the equivalent of
17
+ // AdwHeaderBar:show-back-button = FALSE) or `can-pop` is "false".
18
+ //
19
+ // Attributes (on <adw-navigation-view>):
20
+ // animate-transitions (boolean, default present — slide animation on push/pop;
21
+ // mirrors Adw.NavigationView:animate-transitions)
22
+ // Attributes (on <adw-navigation-page>):
23
+ // title (the page title — shown in the header bar's window title)
24
+ // tag (a unique identifier for push-by-tag)
25
+ // can-pop ("false" disables the back button / popping; mirrors
26
+ // AdwNavigationPage:can-pop)
27
+ // no-back-button (boolean — suppresses the automatic back button only)
28
+ // Events (CustomEvent, bubbles):
29
+ // `pushed` (detail = { tag }) after a page is pushed — mirrors
30
+ // AdwNavigationView::pushed.
31
+ // `popped` (detail = { tag }) after a page is popped — mirrors
32
+ // AdwNavigationView::popped.
33
+ // `replaced` after the whole stack is replaced — mirrors
34
+ // AdwNavigationView::replaced.
35
+ // `notify::visible-page` (detail = { tag }) whenever the visible page changes
36
+ // — mirrors the Adw.NavigationView:visible-page property.
37
+ // Reference: refs/libadwaita/src/adw-navigation-view.c (AdwNavigationView behaviour)
38
+ // Reference: refs/adwaita-web/adwaita-web/docs/widgets/navigationview.md
39
+ // Copyright (c) 2022-2023 Purism SPC / GNOME contributors (libadwaita). LGPLv2.1+.
40
+ // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
41
+
42
+ /** A single navigation page. Children of <adw-navigation-view>. */
43
+ export class AdwNavigationPage extends HTMLElement {
44
+ static get observedAttributes() {
45
+ return ['title', 'tag', 'can-pop', 'no-back-button'];
46
+ }
47
+
48
+ get title(): string {
49
+ return this.getAttribute('title') ?? '';
50
+ }
51
+
52
+ set title(value: string) {
53
+ this.setAttribute('title', value);
54
+ }
55
+
56
+ get tag(): string | null {
57
+ return this.getAttribute('tag');
58
+ }
59
+
60
+ set tag(value: string | null) {
61
+ if (value === null) this.removeAttribute('tag');
62
+ else this.setAttribute('tag', value);
63
+ }
64
+
65
+ /** Whether the page can be popped (and thus shows a back button). */
66
+ get canPop(): boolean {
67
+ return this.getAttribute('can-pop') !== 'false';
68
+ }
69
+
70
+ set canPop(value: boolean) {
71
+ this.setAttribute('can-pop', value ? 'true' : 'false');
72
+ }
73
+ }
74
+
75
+ export class AdwNavigationView extends HTMLElement {
76
+ private _pagesEl!: HTMLDivElement;
77
+ // Every page known to the view (static + dynamically pushed), in DOM order.
78
+ private _pages: AdwNavigationPage[] = [];
79
+ // The navigation stack — references into _pages, bottom-first.
80
+ private _stack: AdwNavigationPage[] = [];
81
+ // Pages added dynamically via push() (destroyed when popped off).
82
+ private _dynamic = new Set<AdwNavigationPage>();
83
+ // The back button injected into each visible page (so we can remove it).
84
+ private _backButtons = new WeakMap<AdwNavigationPage, HTMLElement>();
85
+ private _initialized = false;
86
+
87
+ static get observedAttributes() {
88
+ return ['animate-transitions'];
89
+ }
90
+
91
+ /** Whether push/pop transitions are animated. */
92
+ get animateTransitions(): boolean {
93
+ // Absent attribute defaults to true (matches Adw.NavigationView).
94
+ return this.getAttribute('animate-transitions') !== 'false';
95
+ }
96
+
97
+ set animateTransitions(value: boolean) {
98
+ this.setAttribute('animate-transitions', value ? 'true' : 'false');
99
+ }
100
+
101
+ /** The currently-visible page (top of the stack), or null when empty. */
102
+ get visiblePage(): AdwNavigationPage | null {
103
+ return this._stack.length > 0 ? this._stack[this._stack.length - 1] : null;
104
+ }
105
+
106
+ /** The tag of the visible page, or null. */
107
+ get visiblePageTag(): string | null {
108
+ return this.visiblePage?.tag ?? null;
109
+ }
110
+
111
+ connectedCallback() {
112
+ if (this._initialized) return;
113
+ this._initialized = true;
114
+
115
+ // Snapshot the declared <adw-navigation-page> children — they become the
116
+ // statically-added pages. The first one is pushed automatically.
117
+ const pages = Array.from(this.querySelectorAll(':scope > adw-navigation-page')) as AdwNavigationPage[];
118
+
119
+ this._pagesEl = document.createElement('div');
120
+ this._pagesEl.className = 'adw-navigation-view-pages';
121
+
122
+ this._pages = [];
123
+ this._stack = [];
124
+
125
+ for (const page of pages) {
126
+ page.classList.add('adw-navigation-page');
127
+ this._pagesEl.appendChild(page);
128
+ this._pages.push(page);
129
+ }
130
+
131
+ this.replaceChildren(this._pagesEl);
132
+
133
+ // Auto-push the first added page (Adw.NavigationView behaviour).
134
+ if (this._pages.length > 0) {
135
+ this._stack.push(this._pages[0]);
136
+ }
137
+
138
+ this._syncClasses();
139
+ this._render();
140
+ }
141
+
142
+ attributeChangedCallback() {
143
+ if (!this._initialized) return;
144
+ // animate-transitions only affects future transitions; just keep the
145
+ // class in sync so CSS can gate the slide animation.
146
+ this._syncClasses();
147
+ }
148
+
149
+ /**
150
+ * Add a static page without pushing it (mirrors AdwNavigationView.add). The
151
+ * view keeps a reference; the page becomes reachable via push-by-tag. Named
152
+ * `addPage` rather than `add` to avoid clashing with any DOM method.
153
+ */
154
+ addPage(page: AdwNavigationPage): void {
155
+ if (this._pages.includes(page)) return;
156
+ page.classList.add('adw-navigation-page');
157
+ this._pagesEl.appendChild(page);
158
+ this._pages.push(page);
159
+ this._render();
160
+ }
161
+
162
+ /**
163
+ * Remove a static page (mirrors AdwNavigationView.remove). Named `removePage`
164
+ * to avoid overriding HTMLElement.remove() (which takes no arguments).
165
+ */
166
+ removePage(page: AdwNavigationPage): void {
167
+ if (this._stack.includes(page)) return; // can't remove a page on the stack
168
+ const index = this._pages.indexOf(page);
169
+ if (index === -1) return;
170
+ this._pages.splice(index, 1);
171
+ this._dynamic.delete(page);
172
+ if (page.parentNode === this._pagesEl) this._pagesEl.removeChild(page);
173
+ this._render();
174
+ }
175
+
176
+ /**
177
+ * Push a page onto the navigation stack (mirrors AdwNavigationView.push).
178
+ * Accepts a page element or, when given a string, the tag of a known page
179
+ * (push-by-tag, mirrors AdwNavigationView.push_by_tag). A page that is not
180
+ * already known is added dynamically and destroyed when it is popped.
181
+ */
182
+ push(pageOrTag: AdwNavigationPage | string): void {
183
+ const page = this._resolvePage(pageOrTag);
184
+ if (!page) {
185
+ console.warn(`[adw-navigation-view] No page found for "${String(pageOrTag)}"`);
186
+ return;
187
+ }
188
+ if (this._stack.includes(page)) {
189
+ console.warn('[adw-navigation-view] Page is already in the navigation stack');
190
+ return;
191
+ }
192
+ if (!this._pages.includes(page)) {
193
+ // A brand-new dynamic page — keep a reference + mark for cleanup.
194
+ page.classList.add('adw-navigation-page');
195
+ this._pagesEl.appendChild(page);
196
+ this._pages.push(page);
197
+ this._dynamic.add(page);
198
+ }
199
+ this._stack.push(page);
200
+ this._render();
201
+ this.dispatchEvent(new CustomEvent('pushed', { bubbles: true, detail: { tag: page.tag } }));
202
+ this.dispatchEvent(new CustomEvent('notify::visible-page', { bubbles: true, detail: { tag: page.tag } }));
203
+ }
204
+
205
+ /**
206
+ * Pop the visible page off the stack (mirrors AdwNavigationView.pop).
207
+ * Returns true when a page was popped. A dynamically-pushed page is removed
208
+ * from the DOM. The root page cannot be popped.
209
+ */
210
+ pop(): boolean {
211
+ if (this._stack.length <= 1) return false;
212
+ const visible = this.visiblePage;
213
+ if (visible && !visible.canPop) return false;
214
+
215
+ const popped = this._stack.pop() as AdwNavigationPage;
216
+ // Destroy dynamically-pushed pages once they leave the stack.
217
+ if (this._dynamic.has(popped)) {
218
+ this._dynamic.delete(popped);
219
+ const index = this._pages.indexOf(popped);
220
+ if (index !== -1) this._pages.splice(index, 1);
221
+ if (popped.parentNode === this._pagesEl) this._pagesEl.removeChild(popped);
222
+ }
223
+ this._render();
224
+ this.dispatchEvent(new CustomEvent('popped', { bubbles: true, detail: { tag: popped.tag } }));
225
+ const next = this.visiblePage;
226
+ this.dispatchEvent(new CustomEvent('notify::visible-page', { bubbles: true, detail: { tag: next?.tag ?? null } }));
227
+ return true;
228
+ }
229
+
230
+ /** Pop every page down to (and including pushing) the given tag's page. */
231
+ popToTag(tag: string): boolean {
232
+ const target = this._pages.find((p) => p.tag === tag);
233
+ if (!target) return false;
234
+ const index = this._stack.indexOf(target);
235
+ if (index === -1 || index === this._stack.length - 1) return false;
236
+ let popped = false;
237
+ while (this._stack.length - 1 > index) {
238
+ if (!this.pop()) break;
239
+ popped = true;
240
+ }
241
+ return popped;
242
+ }
243
+
244
+ /**
245
+ * Replace the whole navigation stack with the given pages (mirrors
246
+ * AdwNavigationView.replace). The last page becomes visible.
247
+ */
248
+ replace(pages: Array<AdwNavigationPage | string>): void {
249
+ // Clear the current stack first (without firing popped events — replace
250
+ // is its own signal in libadwaita).
251
+ for (const page of this._stack) {
252
+ if (this._dynamic.has(page)) {
253
+ this._dynamic.delete(page);
254
+ const index = this._pages.indexOf(page);
255
+ if (index !== -1) this._pages.splice(index, 1);
256
+ if (page.parentNode === this._pagesEl) this._pagesEl.removeChild(page);
257
+ }
258
+ }
259
+ this._stack = [];
260
+
261
+ for (const entry of pages) {
262
+ const page = this._resolvePage(entry);
263
+ if (!page) continue;
264
+ if (!this._pages.includes(page)) {
265
+ page.classList.add('adw-navigation-page');
266
+ this._pagesEl.appendChild(page);
267
+ this._pages.push(page);
268
+ this._dynamic.add(page);
269
+ }
270
+ this._stack.push(page);
271
+ }
272
+
273
+ this._render();
274
+ this.dispatchEvent(new CustomEvent('replaced', { bubbles: true }));
275
+ this.dispatchEvent(
276
+ new CustomEvent('notify::visible-page', { bubbles: true, detail: { tag: this.visiblePageTag } }),
277
+ );
278
+ }
279
+
280
+ private _resolvePage(pageOrTag: AdwNavigationPage | string): AdwNavigationPage | null {
281
+ if (typeof pageOrTag === 'string') {
282
+ return this._pages.find((p) => p.tag === pageOrTag) ?? null;
283
+ }
284
+ return pageOrTag;
285
+ }
286
+
287
+ private _syncClasses(): void {
288
+ this.classList.toggle('animated', this.animateTransitions);
289
+ }
290
+
291
+ private _render(): void {
292
+ const visible = this.visiblePage;
293
+
294
+ for (const page of this._pages) {
295
+ const isVisible = page === visible;
296
+ page.classList.toggle('visible-page', isVisible);
297
+ page.hidden = !isVisible;
298
+ // The back button only belongs on the visible page; clean up the
299
+ // others so a stale button never lingers when the stack shrinks.
300
+ if (!isVisible) this._removeBackButton(page);
301
+ }
302
+
303
+ if (visible) this._syncBackButton(visible);
304
+ }
305
+
306
+ /** Inject (or remove) the automatic back button on the visible page. */
307
+ private _syncBackButton(page: AdwNavigationPage): void {
308
+ // Pop is possible only when there is a page below this one on the stack.
309
+ const poppable = this._stack.indexOf(page) > 0 && page.canPop && !page.hasAttribute('no-back-button');
310
+ if (!poppable) {
311
+ this._removeBackButton(page);
312
+ return;
313
+ }
314
+ if (this._backButtons.has(page)) return;
315
+
316
+ // Find the page's header bar's start section. We surface the button into
317
+ // the first <adw-header-bar> inside the page content, the way
318
+ // AdwHeaderBar grows a back button when placed inside an AdwNavigationView.
319
+ const headerBar = page.querySelector('adw-header-bar') as HTMLElement | null;
320
+ if (!headerBar) return;
321
+ const start = (headerBar as { startSection?: HTMLElement | null }).startSection ?? null;
322
+ if (!start) return;
323
+
324
+ const back = document.createElement('adw-button');
325
+ back.classList.add('adw-navigation-back-button');
326
+ back.setAttribute('icon', 'go-previous');
327
+ back.setAttribute('flat', '');
328
+ back.setAttribute('tooltip', 'Back');
329
+ back.addEventListener('click', () => this.pop());
330
+ // Place the back button at the very start of the header.
331
+ start.insertBefore(back, start.firstChild);
332
+ this._backButtons.set(page, back);
333
+ }
334
+
335
+ private _removeBackButton(page: AdwNavigationPage): void {
336
+ const back = this._backButtons.get(page);
337
+ if (!back) return;
338
+ if (back.parentNode) back.parentNode.removeChild(back);
339
+ this._backButtons.delete(page);
340
+ }
341
+ }
342
+
343
+ customElements.define('adw-navigation-page', AdwNavigationPage);
344
+ customElements.define('adw-navigation-view', AdwNavigationView);
@@ -0,0 +1,121 @@
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.
10
+ // 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
+ // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
15
+ // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
16
+
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;
23
+
24
+ static get observedAttributes() {
25
+ return ['title', 'text', 'revealed'];
26
+ }
27
+
28
+ get text(): string {
29
+ return this._input ? this._input.value : (this.getAttribute('text') ?? '');
30
+ }
31
+
32
+ set text(value: string) {
33
+ if (this._input) this._input.value = value;
34
+ else this.setAttribute('text', value);
35
+ }
36
+
37
+ /** Whether the contents are shown in clear text (peek toggle is engaged). */
38
+ get revealed(): boolean {
39
+ return this.hasAttribute('revealed');
40
+ }
41
+
42
+ set revealed(value: boolean) {
43
+ if (value) this.setAttribute('revealed', '');
44
+ else this.removeAttribute('revealed');
45
+ }
46
+
47
+ connectedCallback() {
48
+ if (this._initialized) return;
49
+ this._initialized = true;
50
+
51
+ this._titleEl = document.createElement('span');
52
+ this._titleEl.className = 'adw-row-title';
53
+ this._titleEl.textContent = this.getAttribute('title') ?? '';
54
+
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') ?? '';
61
+ 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
+
72
+ // Trailing peek toggle — a flat icon button, vertically centred. Swaps
73
+ // between the reveal / conceal symbolics like the native widget.
74
+ this._toggle = document.createElement('button');
75
+ this._toggle.className = 'adw-button flat circular icon-only adw-password-entry-row-toggle';
76
+ 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;
82
+ });
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
+ }
93
+
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
+ );
105
+ }
106
+ }
107
+
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));
118
+ }
119
+ }
120
+
121
+ customElements.define('adw-password-entry-row', AdwPasswordEntryRow);