@gjsify/adwaita-web 0.11.0 → 0.13.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,321 @@
1
+ // <adw-carousel> — a horizontally swipeable pager (the web counterpart of
2
+ // Adw.Carousel). Pages are declared as direct children; the carousel lays them
3
+ // out in a horizontal scroll-snap strip, tracks the fractional scroll position,
4
+ // and exposes a `position` (fractional page index) + `n-pages`, mirroring the
5
+ // AdwCarousel properties. Navigation: drag/scroll-snap (touch + trackpad),
6
+ // optional scroll-wheel paging, and `scroll-to(index)`.
7
+ //
8
+ // Indicators are SEPARATE elements bound to a carousel (exactly as the native
9
+ // Adw.CarouselIndicatorDots / Adw.CarouselIndicatorLines bind to an
10
+ // Adw.Carousel via their `carousel` property): set `el.carousel = carouselEl`
11
+ // or place a `for` attribute referencing the carousel's id. The indicator
12
+ // listens to the carousel's `notify::position` / `notify::n-pages` events and
13
+ // repaints; clicking an indicator scrolls the carousel to that page.
14
+ //
15
+ // adw-carousel
16
+ // Attributes: allow-scroll-wheel (bool, default present), allow-long-swipes
17
+ // (bool — a flick can skip multiple pages at once), position (number,
18
+ // fractional page index — reflected, settable to snap to a page).
19
+ // Events: `notify::position` (fractional scroll position changed),
20
+ // `notify::n-pages` (page count changed), `page-changed` (settled on a new
21
+ // page; `detail = { index }`) — mirroring the AdwCarousel signals the native
22
+ // story uses.
23
+ // adw-carousel-indicator-dots / adw-carousel-indicator-lines
24
+ // Properties: carousel (the bound AdwCarousel element), or a `for` attribute
25
+ // with the carousel element's id.
26
+ //
27
+ // Reference: refs/libadwaita/src/adw-carousel.c (AdwCarousel behaviour)
28
+ // Reference: refs/libadwaita/src/adw-carousel-indicator-dots.c
29
+ // (DOTS_RADIUS 3, DOTS_RADIUS_SELECTED 4, DOTS_OPACITY 0.3,
30
+ // DOTS_OPACITY_SELECTED 0.9, DOTS_SPACING 7)
31
+ // Reference: refs/libadwaita/src/adw-carousel-indicator-lines.c
32
+ // (LINE_WIDTH 3, LINE_LENGTH 35, LINE_SPACING 5,
33
+ // LINE_OPACITY 0.3, LINE_OPACITY_ACTIVE 0.9)
34
+ // Reference: refs/adwaita-web/adwaita-web/scss/_carousel_indicators.scss
35
+ // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
36
+ // Modifications: Implemented as Web Components for @gjsify/adwaita-web.
37
+
38
+ /** Common surface an indicator binds to. The concrete element is AdwCarousel. */
39
+ interface CarouselHost extends HTMLElement {
40
+ readonly nPages: number;
41
+ readonly position: number;
42
+ scrollToPage(index: number): void;
43
+ addEventListener(type: string, listener: EventListenerOrEventListenerObject): void;
44
+ removeEventListener(type: string, listener: EventListenerOrEventListenerObject): void;
45
+ }
46
+
47
+ export class AdwCarousel extends HTMLElement implements CarouselHost {
48
+ private _trackEl!: HTMLDivElement;
49
+ private _pages: HTMLElement[] = [];
50
+ private _position = 0;
51
+ private _scrollRaf = 0;
52
+ private _wheelAccum = 0;
53
+ private _initialized = false;
54
+
55
+ static get observedAttributes() {
56
+ return ['allow-scroll-wheel', 'allow-long-swipes', 'position'];
57
+ }
58
+
59
+ /** Number of pages in the carousel. */
60
+ get nPages(): number {
61
+ return this._pages.length;
62
+ }
63
+
64
+ /** Fractional scroll position (0 = first page, 1 = second, …). */
65
+ get position(): number {
66
+ return this._position;
67
+ }
68
+
69
+ set position(value: number) {
70
+ const clamped = Math.min(Math.max(value, 0), Math.max(0, this.nPages - 1));
71
+ this.scrollToPage(Math.round(clamped));
72
+ }
73
+
74
+ /** Whether the scroll wheel navigates between pages. */
75
+ get allowScrollWheel(): boolean {
76
+ return this.hasAttribute('allow-scroll-wheel');
77
+ }
78
+
79
+ set allowScrollWheel(value: boolean) {
80
+ this.toggleAttribute('allow-scroll-wheel', value);
81
+ }
82
+
83
+ /** Whether a single flick can skip more than one page. */
84
+ get allowLongSwipes(): boolean {
85
+ return this.hasAttribute('allow-long-swipes');
86
+ }
87
+
88
+ set allowLongSwipes(value: boolean) {
89
+ this.toggleAttribute('allow-long-swipes', value);
90
+ }
91
+
92
+ connectedCallback() {
93
+ if (this._initialized) return;
94
+ this._initialized = true;
95
+
96
+ // The declared children become the carousel's pages. Snapshot them
97
+ // before we move them into the scroll track.
98
+ this._pages = Array.from(this.children).filter((c): c is HTMLElement => c instanceof HTMLElement);
99
+
100
+ this._trackEl = document.createElement('div');
101
+ this._trackEl.className = 'adw-carousel-track';
102
+ for (const page of this._pages) {
103
+ const slot = document.createElement('div');
104
+ slot.className = 'adw-carousel-page';
105
+ slot.appendChild(page);
106
+ this._trackEl.appendChild(slot);
107
+ }
108
+ this.replaceChildren(this._trackEl);
109
+
110
+ this._trackEl.addEventListener('scroll', this._onScroll, { passive: true });
111
+ this._trackEl.addEventListener('wheel', this._onWheel, { passive: false });
112
+
113
+ // Surface the page count once the DOM is wired so indicators can paint.
114
+ this.dispatchEvent(new CustomEvent('notify::n-pages', { bubbles: true, detail: { nPages: this.nPages } }));
115
+ this._emitPosition();
116
+ }
117
+
118
+ attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
119
+ if (!this._initialized) return;
120
+ if (name === 'position' && newValue !== null) {
121
+ const next = Number.parseInt(newValue, 10);
122
+ if (!Number.isNaN(next) && next !== Math.round(this._position)) this.scrollToPage(next);
123
+ }
124
+ // allow-scroll-wheel / allow-long-swipes are read live in the handlers.
125
+ }
126
+
127
+ /** Scroll (snap) to the page at `index`, clamped to the valid range. */
128
+ scrollToPage(index: number): void {
129
+ if (!this._trackEl) return;
130
+ const clamped = Math.min(Math.max(index, 0), Math.max(0, this.nPages - 1));
131
+ const slot = this._trackEl.children[clamped] as HTMLElement | undefined;
132
+ if (!slot) return;
133
+ this._trackEl.scrollTo({ left: slot.offsetLeft, behavior: 'smooth' });
134
+ }
135
+
136
+ private _onScroll = (): void => {
137
+ if (this._scrollRaf) return;
138
+ this._scrollRaf = requestAnimationFrame(() => {
139
+ this._scrollRaf = 0;
140
+ this._updatePositionFromScroll();
141
+ });
142
+ };
143
+
144
+ private _onWheel = (event: WheelEvent): void => {
145
+ if (!this.allowScrollWheel || this.nPages === 0) return;
146
+ // A horizontal scroll already pans the snap strip natively; only the
147
+ // vertical wheel needs translating into page steps (mirrors AdwCarousel,
148
+ // which lets vertical scrolling page a horizontal carousel).
149
+ if (Math.abs(event.deltaY) <= Math.abs(event.deltaX)) return;
150
+ event.preventDefault();
151
+ this._wheelAccum += event.deltaY;
152
+ const threshold = 40;
153
+ if (Math.abs(this._wheelAccum) < threshold) return;
154
+ const step = this._wheelAccum > 0 ? 1 : -1;
155
+ this._wheelAccum = 0;
156
+ const current = Math.round(this._position);
157
+ const target = this.allowLongSwipes ? current + step * 1 : current + step;
158
+ this.scrollToPage(target);
159
+ };
160
+
161
+ private _updatePositionFromScroll(): void {
162
+ if (!this._trackEl) return;
163
+ const slotWidth = this._slotWidth();
164
+ if (slotWidth <= 0) return;
165
+ const next = this._trackEl.scrollLeft / slotWidth;
166
+ const settledBefore = Math.round(this._position);
167
+ this._position = next;
168
+ this._emitPosition();
169
+ const settledAfter = Math.round(next);
170
+ // Snap landed cleanly on a page boundary → page-changed (within 0.02 of
171
+ // an integer index, matching a finished scroll-snap).
172
+ if (settledAfter !== settledBefore && Math.abs(next - settledAfter) < 0.02) {
173
+ this.dispatchEvent(new CustomEvent('page-changed', { bubbles: true, detail: { index: settledAfter } }));
174
+ }
175
+ }
176
+
177
+ private _slotWidth(): number {
178
+ const first = this._trackEl?.children[0] as HTMLElement | undefined;
179
+ return first ? first.offsetWidth : 0;
180
+ }
181
+
182
+ private _emitPosition(): void {
183
+ this.dispatchEvent(new CustomEvent('notify::position', { bubbles: true, detail: { position: this._position } }));
184
+ }
185
+ }
186
+
187
+ /** Shared base for the dot / line indicator elements. */
188
+ abstract class AdwCarouselIndicator extends HTMLElement {
189
+ protected _carousel: CarouselHost | null = null;
190
+ protected _initialized = false;
191
+
192
+ static get observedAttributes() {
193
+ return ['for'];
194
+ }
195
+
196
+ /** The bound carousel. Mirrors AdwCarouselIndicator*'s `carousel` property. */
197
+ get carousel(): CarouselHost | null {
198
+ return this._carousel;
199
+ }
200
+
201
+ set carousel(value: CarouselHost | null) {
202
+ if (value === this._carousel) return;
203
+ this._detach();
204
+ this._carousel = value;
205
+ this._attach();
206
+ }
207
+
208
+ connectedCallback() {
209
+ this._initialized = true;
210
+ if (!this._carousel) this._resolveForAttr();
211
+ this._attach();
212
+ this._render();
213
+ }
214
+
215
+ disconnectedCallback() {
216
+ this._detach();
217
+ }
218
+
219
+ attributeChangedCallback(name: string) {
220
+ if (name === 'for' && this._initialized) {
221
+ this._detach();
222
+ this._carousel = null;
223
+ this._resolveForAttr();
224
+ this._attach();
225
+ this._render();
226
+ }
227
+ }
228
+
229
+ private _resolveForAttr(): void {
230
+ const id = this.getAttribute('for');
231
+ if (!id) return;
232
+ const root = this.getRootNode() as Document | ShadowRoot;
233
+ const el = root.getElementById?.(id) ?? document.getElementById(id);
234
+ if (el instanceof AdwCarousel) this._carousel = el;
235
+ }
236
+
237
+ private _attach(): void {
238
+ if (!this._carousel || !this._initialized) return;
239
+ this._carousel.addEventListener('notify::position', this._onChange);
240
+ this._carousel.addEventListener('notify::n-pages', this._onChange);
241
+ this._render();
242
+ }
243
+
244
+ private _detach(): void {
245
+ if (!this._carousel) return;
246
+ this._carousel.removeEventListener('notify::position', this._onChange);
247
+ this._carousel.removeEventListener('notify::n-pages', this._onChange);
248
+ }
249
+
250
+ private _onChange = (): void => {
251
+ this._render();
252
+ };
253
+
254
+ protected nPages(): number {
255
+ return this._carousel?.nPages ?? 0;
256
+ }
257
+
258
+ protected position(): number {
259
+ return this._carousel?.position ?? 0;
260
+ }
261
+
262
+ protected goTo(index: number): void {
263
+ this._carousel?.scrollToPage(index);
264
+ }
265
+
266
+ protected abstract _render(): void;
267
+ }
268
+
269
+ export class AdwCarouselIndicatorDots extends AdwCarouselIndicator {
270
+ protected _render(): void {
271
+ renderIndicator(this, 'adw-carousel-dot', this.nPages(), this.position(), (i) => this.goTo(i));
272
+ }
273
+ }
274
+
275
+ export class AdwCarouselIndicatorLines extends AdwCarouselIndicator {
276
+ protected _render(): void {
277
+ renderIndicator(this, 'adw-carousel-line', this.nPages(), this.position(), (i) => this.goTo(i));
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Shared rebuild for both indicator kinds: one child per page, the marker
283
+ * closest to the fractional position carries `.active`, and clicking scrolls
284
+ * the carousel. The dot/line look (size, growth, opacity ramp) is in SCSS; the
285
+ * `--adw-carousel-progress` custom property carries the per-marker proximity so
286
+ * the active marker can grow/brighten the way libadwaita lerps between the
287
+ * inactive and selected states.
288
+ */
289
+ function renderIndicator(
290
+ host: HTMLElement,
291
+ markerClass: string,
292
+ nPages: number,
293
+ position: number,
294
+ onClick: (index: number) => void,
295
+ ): void {
296
+ if (host.childElementCount !== nPages) {
297
+ const markers: HTMLButtonElement[] = [];
298
+ for (let i = 0; i < nPages; i++) {
299
+ const marker = document.createElement('button');
300
+ marker.type = 'button';
301
+ marker.className = markerClass;
302
+ marker.setAttribute('aria-label', `Page ${i + 1}`);
303
+ marker.addEventListener('click', () => onClick(i));
304
+ markers.push(marker);
305
+ }
306
+ host.replaceChildren(...markers);
307
+ }
308
+ Array.from(host.children).forEach((child, i) => {
309
+ // Proximity in [0,1]: 1 on the focused page, ramping to 0 one page away
310
+ // (matches AdwCarousel's per-dot lerp progress = 1 - |position - i|).
311
+ const progress = Math.max(0, 1 - Math.abs(position - i));
312
+ const marker = child as HTMLElement;
313
+ marker.style.setProperty('--adw-carousel-progress', progress.toFixed(3));
314
+ marker.classList.toggle('active', progress > 0.5);
315
+ marker.setAttribute('aria-current', progress > 0.5 ? 'true' : 'false');
316
+ });
317
+ }
318
+
319
+ customElements.define('adw-carousel', AdwCarousel);
320
+ customElements.define('adw-carousel-indicator-dots', AdwCarouselIndicatorDots);
321
+ customElements.define('adw-carousel-indicator-lines', AdwCarouselIndicatorLines);
@@ -0,0 +1,28 @@
1
+ // <adw-clamp> — Constrains its child to a maximum width and centers it.
2
+ // Attributes: maximum-size (px, default 600), tightening-threshold (unused;
3
+ // accepted for API parity with Adw.Clamp).
4
+ // Reference: refs/adwaita-web/adwaita-web/docs/widgets/clamp.md
5
+ // Reference: refs/libadwaita (Adw.Clamp)
6
+ // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
7
+ // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
8
+
9
+ export class AdwClamp extends HTMLElement {
10
+ static get observedAttributes() {
11
+ return ['maximum-size'];
12
+ }
13
+
14
+ connectedCallback() {
15
+ this._sync();
16
+ }
17
+
18
+ attributeChangedCallback() {
19
+ this._sync();
20
+ }
21
+
22
+ private _sync() {
23
+ const max = parseFloat(this.getAttribute('maximum-size') || '600');
24
+ this.style.maxWidth = `${max}px`;
25
+ }
26
+ }
27
+
28
+ customElements.define('adw-clamp', AdwClamp);
@@ -1,19 +1,22 @@
1
- // <adw-combo-row> — Row with a label and dropdown select.
2
- // Attributes: title, items (JSON string[]), selected (index number)
1
+ // <adw-combo-row> — Row with a title/subtitle and a dropdown select.
2
+ // Attributes: title, subtitle, items (JSON string[]), selected (index number)
3
3
  // Events: notify::selected (CustomEvent, mirrors GJS GObject signal naming)
4
4
  // The native <select> is stretched invisibly over the row so clicking anywhere opens it.
5
5
  // Adapted from Adwaita Web UI Framework (https://github.com/mclellac/adwaita-web).
6
6
  // Copyright (c) 2025 csm. MIT License.
7
- // Modifications: Reimplemented as Web Component for @gjsify/adwaita-web.
7
+ // Modifications: Reimplemented as Web Component for @gjsify/adwaita-web;
8
+ // title/subtitle text column added to match Adw.ComboRow.
8
9
 
9
10
  export class AdwComboRow extends HTMLElement {
10
11
  private _select!: HTMLSelectElement;
11
12
  private _valueEl!: HTMLSpanElement;
13
+ private _titleEl!: HTMLSpanElement;
14
+ private _subtitleEl!: HTMLSpanElement;
12
15
  private _items: string[] = [];
13
16
  private _initialized = false;
14
17
 
15
18
  static get observedAttributes() {
16
- return ['selected'];
19
+ return ['title', 'subtitle', 'selected'];
17
20
  }
18
21
 
19
22
  get selected(): number {
@@ -28,20 +31,21 @@ export class AdwComboRow extends HTMLElement {
28
31
  if (this._initialized) return;
29
32
  this._initialized = true;
30
33
 
31
- const title = this.getAttribute('title') || '';
32
34
  this._items = JSON.parse(this.getAttribute('items') || '[]');
33
35
  const selectedIdx = parseInt(this.getAttribute('selected') || '0', 10);
34
36
 
35
- // Title label
36
- const titleEl = document.createElement('span');
37
- titleEl.className = 'adw-row-title';
38
- titleEl.textContent = title;
37
+ const text = document.createElement('div');
38
+ text.className = 'adw-row-text';
39
+ this._titleEl = document.createElement('span');
40
+ this._titleEl.className = 'adw-row-title';
41
+ this._subtitleEl = document.createElement('span');
42
+ this._subtitleEl.className = 'adw-row-subtitle';
43
+ text.append(this._titleEl, this._subtitleEl);
39
44
 
40
45
  // Visible selected value display
41
- const valueEl = document.createElement('span');
42
- valueEl.className = 'adw-row-value';
43
- valueEl.textContent = this._items[selectedIdx] ?? '';
44
- this._valueEl = valueEl;
46
+ this._valueEl = document.createElement('span');
47
+ this._valueEl.className = 'adw-row-value';
48
+ this._valueEl.textContent = this._items[selectedIdx] ?? '';
45
49
 
46
50
  // Hidden select overlaying the entire row
47
51
  const select = document.createElement('select');
@@ -53,9 +57,7 @@ export class AdwComboRow extends HTMLElement {
53
57
  select.appendChild(option);
54
58
  });
55
59
 
56
- this.appendChild(titleEl);
57
- this.appendChild(valueEl);
58
- this.appendChild(select);
60
+ this.replaceChildren(text, this._valueEl, select);
59
61
 
60
62
  this._select = select;
61
63
  this._select.addEventListener('change', () => {
@@ -69,17 +71,27 @@ export class AdwComboRow extends HTMLElement {
69
71
  }),
70
72
  );
71
73
  });
74
+
75
+ this._renderText();
72
76
  }
73
77
 
74
78
  attributeChangedCallback(name: string, _old: string | null, value: string | null) {
75
- if (name === 'selected' && this._select) {
79
+ if (!this._initialized) return;
80
+ if (name === 'selected') {
76
81
  const idx = parseInt(value || '0', 10);
77
82
  this._select.selectedIndex = idx;
78
- if (this._valueEl) {
79
- this._valueEl.textContent = this._items[idx] ?? '';
80
- }
83
+ this._valueEl.textContent = this._items[idx] ?? '';
84
+ } else {
85
+ this._renderText();
81
86
  }
82
87
  }
88
+
89
+ private _renderText() {
90
+ this._titleEl.textContent = this.getAttribute('title') ?? '';
91
+ const subtitle = this.getAttribute('subtitle') ?? '';
92
+ this._subtitleEl.textContent = subtitle;
93
+ this._subtitleEl.hidden = subtitle.length === 0;
94
+ }
83
95
  }
84
96
 
85
97
  customElements.define('adw-combo-row', AdwComboRow);
@@ -0,0 +1,69 @@
1
+ // <adw-entry-row> — A boxed-list row that is itself a text entry: the title
2
+ // floats as a label and the editable text sits inline on the row.
3
+ // Attributes: title, text.
4
+ // Property: text (get/set, proxies the inner input).
5
+ // Events: `changed` (CustomEvent) on every edit — mirrors Adw.EntryRow's
6
+ // `changed` signal so it drives the same code path as the GTK renderer.
7
+ // Reference: refs/adwaita-web/adwaita-web/docs/widgets/entryrow.md
8
+ // Reference: refs/libadwaita/src/stylesheet/widgets/_entries.scss
9
+ // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
10
+ // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
11
+
12
+ export class AdwEntryRow extends HTMLElement {
13
+ private _input!: HTMLInputElement;
14
+ private _titleEl!: HTMLSpanElement;
15
+ private _initialized = false;
16
+
17
+ static get observedAttributes() {
18
+ return ['title', 'text'];
19
+ }
20
+
21
+ get text(): string {
22
+ return this._input ? this._input.value : (this.getAttribute('text') ?? '');
23
+ }
24
+
25
+ set text(value: string) {
26
+ if (this._input) this._input.value = value;
27
+ else this.setAttribute('text', value);
28
+ }
29
+
30
+ connectedCallback() {
31
+ if (this._initialized) return;
32
+ this._initialized = true;
33
+
34
+ this._titleEl = document.createElement('span');
35
+ this._titleEl.className = 'adw-row-title';
36
+ this._titleEl.textContent = this.getAttribute('title') ?? '';
37
+
38
+ this._input = document.createElement('input');
39
+ this._input.className = 'adw-entry-row-input';
40
+ this._input.type = 'text';
41
+ this._input.value = this.getAttribute('text') ?? '';
42
+ this._input.addEventListener('input', () => {
43
+ this.dispatchEvent(new CustomEvent('changed', { bubbles: true, detail: { text: this._input.value } }));
44
+ });
45
+
46
+ // Title floats as a small label above the value (the filled
47
+ // Adw.EntryRow look), both left-aligned in a text column.
48
+ const text = document.createElement('div');
49
+ text.className = 'adw-entry-row-text';
50
+ text.append(this._titleEl, this._input);
51
+
52
+ // Trailing edit affordance, mirroring Adw.EntryRow's pencil; clicking
53
+ // it focuses the entry.
54
+ const edit = document.createElement('span');
55
+ edit.className = 'adw-row-edit adw-icon adw-icon--document-edit';
56
+ edit.setAttribute('aria-hidden', 'true');
57
+ edit.addEventListener('click', () => this._input.focus());
58
+
59
+ this.replaceChildren(text, edit);
60
+ }
61
+
62
+ attributeChangedCallback(name: string, _old: string | null, value: string | null) {
63
+ if (!this._initialized) return;
64
+ if (name === 'title') this._titleEl.textContent = value ?? '';
65
+ else if (name === 'text' && this._input.value !== (value ?? '')) this._input.value = value ?? '';
66
+ }
67
+ }
68
+
69
+ customElements.define('adw-entry-row', AdwEntryRow);
@@ -0,0 +1,61 @@
1
+ // <adw-entry> — Adwaita single-line text entry (e.g. a browser URL bar).
2
+ // Attributes: value, placeholder, type, disabled.
3
+ // Property: value (get/set, proxies the inner input).
4
+ // Events: native `input` bubbles from the inner input; `activate` (CustomEvent)
5
+ // fires on Enter — mirroring Gtk.Entry's `activate` signal.
6
+ // Reference: refs/libadwaita/src/stylesheet/widgets/_entries.scss
7
+ // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
8
+ // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
9
+
10
+ export class AdwEntry extends HTMLElement {
11
+ private _input!: HTMLInputElement;
12
+ private _initialized = false;
13
+
14
+ static get observedAttributes() {
15
+ return ['value', 'placeholder', 'type', 'disabled'];
16
+ }
17
+
18
+ get value(): string {
19
+ return this._input ? this._input.value : (this.getAttribute('value') ?? '');
20
+ }
21
+
22
+ set value(v: string) {
23
+ if (this._input) this._input.value = v;
24
+ else this.setAttribute('value', v);
25
+ }
26
+
27
+ /** The inner native input (for focus/selection). */
28
+ get input(): HTMLInputElement {
29
+ return this._input;
30
+ }
31
+
32
+ connectedCallback() {
33
+ if (this._initialized) return;
34
+ this._initialized = true;
35
+
36
+ const input = document.createElement('input');
37
+ input.className = 'adw-entry';
38
+ input.type = this.getAttribute('type') || 'text';
39
+ input.value = this.getAttribute('value') ?? '';
40
+ input.placeholder = this.getAttribute('placeholder') ?? '';
41
+ input.disabled = this.hasAttribute('disabled');
42
+ input.addEventListener('keydown', (e) => {
43
+ if (e.key === 'Enter') {
44
+ this.dispatchEvent(new CustomEvent('activate', { bubbles: true, detail: { value: input.value } }));
45
+ }
46
+ });
47
+
48
+ this._input = input;
49
+ this.replaceChildren(input);
50
+ }
51
+
52
+ attributeChangedCallback(name: string, _old: string | null, value: string | null) {
53
+ if (!this._input) return;
54
+ if (name === 'value') this._input.value = value ?? '';
55
+ else if (name === 'placeholder') this._input.placeholder = value ?? '';
56
+ else if (name === 'type') this._input.type = value || 'text';
57
+ else if (name === 'disabled') this._input.disabled = value !== null;
58
+ }
59
+ }
60
+
61
+ customElements.define('adw-entry', AdwEntry);