@gjsify/adwaita-web 0.31.0 → 0.33.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.
- package/dist/adwaita-web.css +835 -299
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/adw-about-dialog.spec.d.ts +1 -0
- package/lib/types/adw-action-rows.spec.d.ts +1 -0
- package/lib/types/adw-avatar.spec.d.ts +1 -0
- package/lib/types/adw-banner.spec.d.ts +1 -0
- package/lib/types/adw-button-content.spec.d.ts +1 -0
- package/lib/types/adw-carousel.spec.d.ts +1 -0
- package/lib/types/adw-checks.spec.d.ts +1 -0
- package/lib/types/adw-header-bar.spec.d.ts +1 -0
- package/lib/types/adw-icon.spec.d.ts +1 -0
- package/lib/types/adw-progress-bar.spec.d.ts +1 -0
- package/lib/types/adw-sidebar.spec.d.ts +1 -0
- package/lib/types/adw-switch.spec.d.ts +1 -0
- package/lib/types/adw-wrap-box.spec.d.ts +1 -0
- package/lib/types/bottom-sheet.spec.d.ts +1 -0
- package/lib/types/breakpoints.d.ts +22 -0
- package/lib/types/breakpoints.spec.d.ts +1 -0
- package/lib/types/chrome.spec.d.ts +1 -0
- package/lib/types/elements/adw-about-dialog.d.ts +47 -8
- package/lib/types/elements/adw-action-row.d.ts +35 -0
- package/lib/types/elements/adw-avatar.d.ts +11 -0
- package/lib/types/elements/adw-bottom-sheet.d.ts +33 -4
- package/lib/types/elements/adw-button-content.d.ts +12 -0
- package/lib/types/elements/adw-button-row.d.ts +10 -1
- package/lib/types/elements/adw-carousel.d.ts +111 -8
- package/lib/types/elements/adw-checks.d.ts +57 -0
- package/lib/types/elements/adw-clamp.d.ts +4 -1
- package/lib/types/elements/adw-combo-row.d.ts +10 -0
- package/lib/types/elements/adw-data-grid.d.ts +11 -31
- package/lib/types/elements/adw-drop-down.d.ts +17 -20
- package/lib/types/elements/adw-entry-row.d.ts +93 -3
- package/lib/types/elements/adw-expander-row.d.ts +7 -1
- package/lib/types/elements/adw-header-bar.d.ts +15 -0
- package/lib/types/elements/adw-icon.d.ts +25 -0
- package/lib/types/elements/adw-inline-view-switcher.d.ts +73 -11
- package/lib/types/elements/adw-menu-button.d.ts +6 -6
- package/lib/types/elements/adw-navigation-split-view.d.ts +48 -0
- package/lib/types/elements/adw-navigation-view.d.ts +63 -20
- package/lib/types/elements/adw-overlay-split-view.d.ts +94 -0
- package/lib/types/elements/adw-password-entry-row.d.ts +19 -10
- package/lib/types/elements/adw-popover.d.ts +58 -0
- package/lib/types/elements/adw-preferences-dialog.d.ts +57 -2
- package/lib/types/elements/adw-preferences-group.d.ts +31 -0
- package/lib/types/elements/adw-progress-bar.d.ts +38 -0
- package/lib/types/elements/adw-sidebar.d.ts +37 -5
- package/lib/types/elements/adw-spinner.d.ts +23 -0
- package/lib/types/elements/adw-split-button.d.ts +53 -7
- package/lib/types/elements/adw-switch-row.d.ts +11 -2
- package/lib/types/elements/adw-switch.d.ts +15 -0
- package/lib/types/elements/adw-tab-view.d.ts +129 -12
- package/lib/types/elements/adw-toolbar-view.d.ts +5 -0
- package/lib/types/elements/adw-view-stack.d.ts +39 -19
- package/lib/types/elements/adw-view-switcher-bar.d.ts +25 -7
- package/lib/types/elements/adw-view-switcher.d.ts +76 -13
- package/lib/types/elements/adw-window-title.d.ts +3 -1
- package/lib/types/elements/adw-wrap-box.d.ts +58 -3
- package/lib/types/elements/view-switcher-dom.d.ts +44 -0
- package/lib/types/entry-rows.spec.d.ts +1 -0
- package/lib/types/index.d.ts +7 -0
- package/lib/types/navigation-view.spec.d.ts +1 -0
- package/lib/types/preferences-search.d.ts +20 -0
- package/lib/types/preferences.spec.d.ts +1 -0
- package/lib/types/split-button.spec.d.ts +1 -0
- package/lib/types/split-views.spec.d.ts +1 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/lib/types/tab-view.spec.d.ts +1 -0
- package/lib/types/view-stack.spec.d.ts +1 -0
- package/lib/types/view-switcher.spec.d.ts +1 -0
- package/package.json +6 -6
- package/scss/_banner.scss +13 -0
- package/scss/_bottom_sheet.scss +5 -6
- package/scss/_button.scss +3 -26
- package/scss/_button_content.scss +28 -0
- package/scss/_checks.scss +228 -0
- package/scss/_clamp.scss +9 -3
- package/scss/_combo_row.scss +19 -0
- package/scss/_data_grid.scss +15 -7
- package/scss/_drop_down.scss +12 -55
- package/scss/_expander_row.scss +5 -50
- package/scss/_icon.scss +51 -0
- package/scss/_icons.generated.scss +6 -0
- package/scss/_inline_view_switcher.scss +34 -0
- package/scss/_labels.scss +135 -0
- package/scss/_menu_button.scss +10 -70
- package/scss/_navigation_split_view.scss +47 -8
- package/scss/_overlay_split_view.scss +117 -32
- package/scss/_popover.scss +186 -0
- package/scss/_preferences.scss +29 -0
- package/scss/_preferences_dialog.scss +17 -0
- package/scss/_progress_bar.scss +161 -0
- package/scss/_reset.scss +18 -7
- package/scss/_spinner.scss +37 -19
- package/scss/_split_button.scss +6 -50
- package/scss/_switch.scss +90 -0
- package/scss/_switch_row.scss +6 -52
- package/scss/_tab_view.scss +63 -0
- package/scss/_theme.scss +10 -0
- package/scss/_toolbar_view.scss +74 -0
- package/scss/_variables.scss +34 -0
- package/scss/_view_switcher.scss +40 -0
- package/scss/_view_switcher_bar.scss +37 -0
- package/scss/_wrap_box.scss +65 -6
- package/scss/adwaita-skin.scss +23 -0
- package/src/adw-about-dialog.spec.ts +350 -0
- package/src/adw-action-rows.spec.ts +342 -0
- package/src/adw-avatar.spec.ts +176 -0
- package/src/adw-banner.spec.ts +152 -0
- package/src/adw-button-content.spec.ts +207 -0
- package/src/adw-button-row.spec.ts +12 -6
- package/src/adw-carousel.spec.ts +389 -0
- package/src/adw-checks.spec.ts +368 -0
- package/src/adw-drop-down.spec.ts +123 -0
- package/src/adw-header-bar.spec.ts +83 -0
- package/src/adw-icon.spec.ts +167 -0
- package/src/adw-progress-bar.spec.ts +173 -0
- package/src/adw-row-state.spec.ts +49 -0
- package/src/adw-sidebar.spec.ts +332 -0
- package/src/adw-switch.spec.ts +242 -0
- package/src/adw-tab-view.spec.ts +12 -3
- package/src/adw-view-switcher-bar.spec.ts +61 -13
- package/src/adw-wrap-box.spec.ts +341 -0
- package/src/bottom-sheet.spec.ts +325 -0
- package/src/breakpoints.spec.ts +147 -0
- package/src/breakpoints.ts +67 -0
- package/src/chrome.spec.ts +400 -0
- package/src/elements/adw-about-dialog.ts +223 -163
- package/src/elements/adw-action-row.ts +131 -11
- package/src/elements/adw-avatar.ts +48 -54
- package/src/elements/adw-banner.ts +46 -10
- package/src/elements/adw-bottom-sheet.ts +145 -60
- package/src/elements/adw-button-content.ts +107 -33
- package/src/elements/adw-button-row.ts +61 -37
- package/src/elements/adw-button.ts +5 -7
- package/src/elements/adw-carousel.ts +467 -105
- package/src/elements/adw-checks.ts +299 -0
- package/src/elements/adw-clamp.ts +80 -10
- package/src/elements/adw-combo-row.ts +32 -7
- package/src/elements/adw-data-grid.ts +43 -91
- package/src/elements/adw-drop-down.ts +117 -128
- package/src/elements/adw-entry-row.ts +404 -36
- package/src/elements/adw-expander-row.ts +61 -29
- package/src/elements/adw-header-bar.ts +31 -6
- package/src/elements/adw-icon.ts +148 -0
- package/src/elements/adw-inline-view-switcher.ts +325 -131
- package/src/elements/adw-menu-button.ts +93 -70
- package/src/elements/adw-navigation-split-view.ts +220 -14
- package/src/elements/adw-navigation-view.ts +286 -148
- package/src/elements/adw-overlay-split-view.ts +536 -28
- package/src/elements/adw-password-entry-row.ts +111 -89
- package/src/elements/adw-popover.ts +283 -0
- package/src/elements/adw-preferences-dialog.ts +120 -21
- package/src/elements/adw-preferences-group.ts +128 -22
- package/src/elements/adw-progress-bar.ts +189 -0
- package/src/elements/adw-sidebar.ts +301 -142
- package/src/elements/adw-spin-row.ts +11 -5
- package/src/elements/adw-spinner.ts +168 -9
- package/src/elements/adw-split-button.ts +371 -92
- package/src/elements/adw-status-page.ts +9 -9
- package/src/elements/adw-switch-row.ts +100 -30
- package/src/elements/adw-switch.ts +129 -0
- package/src/elements/adw-tab-view.ts +700 -129
- package/src/elements/adw-toggle-group.ts +5 -7
- package/src/elements/adw-toolbar-view.ts +84 -21
- package/src/elements/adw-view-stack.ts +183 -85
- package/src/elements/adw-view-switcher-bar.ts +171 -81
- package/src/elements/adw-view-switcher.ts +328 -117
- package/src/elements/adw-window-title.ts +44 -11
- package/src/elements/adw-wrap-box.ts +329 -78
- package/src/elements/view-switcher-dom.ts +108 -0
- package/src/entry-rows.spec.ts +391 -0
- package/src/index.ts +23 -0
- package/src/navigation-view.spec.ts +435 -0
- package/src/preferences-search.ts +135 -0
- package/src/preferences.spec.ts +250 -0
- package/src/source-view/adw-source-view.ts +2 -5
- package/src/split-button.spec.ts +470 -0
- package/src/split-views.spec.ts +893 -0
- package/src/styles.generated.ts +1 -1
- package/src/tab-view.spec.ts +451 -0
- package/src/test.browser.mts +49 -0
- package/src/view-stack.spec.ts +272 -0
- package/src/view-switcher.spec.ts +665 -0
|
@@ -5,11 +5,21 @@
|
|
|
5
5
|
// shows the dialog title and a trailing close button.
|
|
6
6
|
//
|
|
7
7
|
// Pages are declared as <adw-preferences-page> children (each with optional
|
|
8
|
-
// `title` / `icon-name` attributes + arbitrary group
|
|
9
|
-
//
|
|
8
|
+
// `title` / `icon-name` / `name` / `use-underline` attributes + arbitrary group
|
|
9
|
+
// content) and are moved — as ELEMENTS, keeping their identity — into the
|
|
10
|
+
// scrolled body. `add(page)` accepts one at any later time, mirroring
|
|
11
|
+
// adw_preferences_dialog_add.
|
|
10
12
|
// (Adw shows a view-switcher when there is more than one page and the window is
|
|
11
|
-
// wide; the web port renders the pages
|
|
12
|
-
// case the storybook exercises.
|
|
13
|
+
// wide, and shows exactly one page at a time; the web port renders the pages
|
|
14
|
+
// stacked, matching the single-page common case the storybook exercises. The
|
|
15
|
+
// page COLLECTION model — visible-page, visible-page-name, the auto-pick —
|
|
16
|
+
// belongs to Adw.ViewStack and is available as `ViewStackState` in
|
|
17
|
+
// @gjsify/adwaita-core when this port grows a switcher.)
|
|
18
|
+
//
|
|
19
|
+
// `search(query)` implements the dialog's search over those pages: the corpus,
|
|
20
|
+
// the filter and the `page → group` result subtitles all come from
|
|
21
|
+
// @gjsify/adwaita-core, so the browser and NativeScript dialogs return the same
|
|
22
|
+
// results for the same tree.
|
|
13
23
|
//
|
|
14
24
|
// Like Adw.Dialog the dialog starts hidden; `present()` / setting the `open`
|
|
15
25
|
// attribute reveals it. It is dismissed by Escape, by clicking the scrim, or by
|
|
@@ -42,16 +52,57 @@
|
|
|
42
52
|
// Copyright (c) 2023-2024 GNOME Foundation Inc. (libadwaita). LGPLv2.1+.
|
|
43
53
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
44
54
|
|
|
55
|
+
import type { PreferencesSearchResult, SearchPreferencesOptions } from '@gjsify/adwaita-core';
|
|
56
|
+
import { searchPreferencesDom } from '../preferences-search.js';
|
|
57
|
+
|
|
45
58
|
const DEFAULT_CONTENT_WIDTH = 640;
|
|
46
59
|
|
|
47
60
|
/**
|
|
48
|
-
* A single preferences page. Child of <adw-preferences-dialog
|
|
49
|
-
*
|
|
61
|
+
* A single preferences page. Child of <adw-preferences-dialog>. Carries optional
|
|
62
|
+
* `title` / `icon-name` / `name` / `use-underline` attributes and any
|
|
50
63
|
* <adw-preferences-group> content — mirrors Adw.PreferencesPage.
|
|
64
|
+
*
|
|
65
|
+
* The dialog keeps the page ELEMENT rather than hoisting its children out,
|
|
66
|
+
* because the page's identity is load-bearing: `adw_preferences_dialog_add`
|
|
67
|
+
* binds title / icon-name / name / use-underline / visible onto the view-stack
|
|
68
|
+
* page (adw-preferences-dialog.c:707-711), and `create_search_row_subtitle`
|
|
69
|
+
* needs the title back the moment a second page is visible.
|
|
51
70
|
*/
|
|
52
71
|
export class AdwPreferencesPage extends HTMLElement {
|
|
53
72
|
static get observedAttributes() {
|
|
54
|
-
return ['title', 'icon-name', 'open'];
|
|
73
|
+
return ['title', 'icon-name', 'name', 'use-underline', 'open'];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** `adw_preferences_page_get_name` — the view-stack child name. */
|
|
77
|
+
get name(): string {
|
|
78
|
+
return this.getAttribute('name') ?? '';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
set name(value: string) {
|
|
82
|
+
this.setAttribute('name', value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** `AdwPreferencesPage:icon-name`, shown by a view switcher. */
|
|
86
|
+
get iconName(): string {
|
|
87
|
+
return this.getAttribute('icon-name') ?? '';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
set iconName(value: string) {
|
|
91
|
+
this.setAttribute('icon-name', value);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** `AdwPreferencesPage:use-underline` — the title carries a mnemonic. */
|
|
95
|
+
get useUnderline(): boolean {
|
|
96
|
+
return this.hasAttribute('use-underline');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
set useUnderline(value: boolean) {
|
|
100
|
+
this.toggleAttribute('use-underline', value);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The page's groups, in document order. */
|
|
104
|
+
get groups(): HTMLElement[] {
|
|
105
|
+
return Array.from(this.querySelectorAll('adw-preferences-group'));
|
|
55
106
|
}
|
|
56
107
|
}
|
|
57
108
|
|
|
@@ -61,6 +112,7 @@ export class AdwPreferencesDialog extends HTMLElement {
|
|
|
61
112
|
private _dialogEl!: HTMLDivElement;
|
|
62
113
|
private _titleEl!: HTMLSpanElement;
|
|
63
114
|
private _bodyEl!: HTMLDivElement;
|
|
115
|
+
private _pagesEl!: HTMLDivElement;
|
|
64
116
|
private _closeBtn!: HTMLButtonElement;
|
|
65
117
|
|
|
66
118
|
static get observedAttributes() {
|
|
@@ -116,18 +168,66 @@ export class AdwPreferencesDialog extends HTMLElement {
|
|
|
116
168
|
this.open = false;
|
|
117
169
|
}
|
|
118
170
|
|
|
171
|
+
/**
|
|
172
|
+
* `adw_preferences_dialog_add` (adw-preferences-dialog.c:693-716) — add a
|
|
173
|
+
* page at ANY time.
|
|
174
|
+
*
|
|
175
|
+
* Before this the pages were snapshotted once, in `connectedCallback`; a
|
|
176
|
+
* page appended later stayed a sibling of the scrim and rendered OUTSIDE
|
|
177
|
+
* the dialog card. GTK and the NativeScript port both accept a page
|
|
178
|
+
* whenever it arrives.
|
|
179
|
+
*/
|
|
180
|
+
add(page: AdwPreferencesPage): void {
|
|
181
|
+
this._pagesEl.appendChild(page);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* `adw_preferences_dialog_remove` (:723-745). C logs
|
|
186
|
+
* `ADW_CRITICAL_CANNOT_REMOVE_CHILD` and no-ops for a page it does not own;
|
|
187
|
+
* returning `false` is the same contract without a console write.
|
|
188
|
+
*
|
|
189
|
+
* NOT named `remove`: `ChildNode.remove()` already exists on every element
|
|
190
|
+
* and takes no arguments, so overriding it with a different signature makes
|
|
191
|
+
* the class un-assignable to `HTMLElement` — which `customElements.define`
|
|
192
|
+
* requires.
|
|
193
|
+
*/
|
|
194
|
+
removePage(page: AdwPreferencesPage): boolean {
|
|
195
|
+
if (page.parentNode !== this._pagesEl) return false;
|
|
196
|
+
this._pagesEl.removeChild(page);
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** The dialog's pages, in add order. */
|
|
201
|
+
get pages(): AdwPreferencesPage[] {
|
|
202
|
+
return Array.from(this._pagesEl.querySelectorAll('adw-preferences-page')) as AdwPreferencesPage[];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The preferences search (`filter_search_results` +
|
|
207
|
+
* `create_search_row_subtitle`), over this dialog's live page subtree.
|
|
208
|
+
*
|
|
209
|
+
* The whole pipeline is `@gjsify/adwaita-core`'s, so this dialog and the
|
|
210
|
+
* NativeScript one answer identically for the same tree — including the
|
|
211
|
+
* three corpus filters (visible page → visible group → titled visible row)
|
|
212
|
+
* and the case fold that makes `strasse` find `Straße`.
|
|
213
|
+
*
|
|
214
|
+
* An empty query returns the WHOLE corpus, which is what C does and what
|
|
215
|
+
* makes the results list non-empty before the user types.
|
|
216
|
+
*/
|
|
217
|
+
search(query: string, options?: SearchPreferencesOptions): PreferencesSearchResult<Element>[] {
|
|
218
|
+
return searchPreferencesDom(this._pagesEl, query, options);
|
|
219
|
+
}
|
|
220
|
+
|
|
119
221
|
connectedCallback() {
|
|
120
222
|
if (this._initialized) return;
|
|
121
223
|
this._initialized = true;
|
|
122
224
|
|
|
123
|
-
// Snapshot the declared
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
const claimed = new Set<Node>([...pages, ...pageContent]);
|
|
130
|
-
const unwrapped = Array.from(this.childNodes).filter((node) => !claimed.has(node));
|
|
225
|
+
// Snapshot the declared children before taking over the subtree. The
|
|
226
|
+
// page ELEMENTS move into the body intact — their title / name /
|
|
227
|
+
// icon-name are what a search result subtitle and a view switcher read
|
|
228
|
+
// — and any unwrapped children follow them (Adw.PreferencesDialog's
|
|
229
|
+
// buildable default adds a child as a page).
|
|
230
|
+
const declared = Array.from(this.childNodes);
|
|
131
231
|
|
|
132
232
|
// Full-cover scrim — clicking it dismisses the dialog (or raises
|
|
133
233
|
// close-attempt when locked).
|
|
@@ -169,15 +269,14 @@ export class AdwPreferencesDialog extends HTMLElement {
|
|
|
169
269
|
|
|
170
270
|
header.append(leading, this._titleEl, trailing);
|
|
171
271
|
|
|
172
|
-
// Scrolled body — a clamped column of the declared pages
|
|
272
|
+
// Scrolled body — a clamped column of the declared pages.
|
|
173
273
|
this._bodyEl = document.createElement('div');
|
|
174
274
|
this._bodyEl.className = 'adw-preferences-dialog-body';
|
|
175
275
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
for (const child of
|
|
179
|
-
|
|
180
|
-
this._bodyEl.appendChild(clamp);
|
|
276
|
+
this._pagesEl = document.createElement('div');
|
|
277
|
+
this._pagesEl.className = 'adw-preferences-dialog-pages';
|
|
278
|
+
for (const child of declared) this._pagesEl.appendChild(child);
|
|
279
|
+
this._bodyEl.appendChild(this._pagesEl);
|
|
181
280
|
|
|
182
281
|
this._dialogEl.append(header, this._bodyEl);
|
|
183
282
|
|
|
@@ -6,19 +6,42 @@
|
|
|
6
6
|
// Header-suffix: any child carrying `slot="header-suffix"` is hoisted into the
|
|
7
7
|
// trailing edge of the header instead of the boxed list — mirroring
|
|
8
8
|
// Adw.PreferencesGroup's `header-suffix` property / buildable type.
|
|
9
|
+
//
|
|
10
|
+
// The five visibility states (title, description, header, `single-line`,
|
|
11
|
+
// listbox) are NOT derived here: they come from
|
|
12
|
+
// `derivePreferencesGroupHeader` in `@gjsify/adwaita-core`, which both this
|
|
13
|
+
// element and the NativeScript group delegate to (ADR 0004). This element used
|
|
14
|
+
// to compute two of the five by hand and was missing the other three — no
|
|
15
|
+
// `single-line` class, and a `.boxed-list` card that stayed painted (with its
|
|
16
|
+
// full-width `box-shadow` hairline) over an empty group.
|
|
17
|
+
//
|
|
18
|
+
// Reference: refs/libadwaita/src/adw-preferences-group.c
|
|
19
|
+
// (update_title_visibility, update_listbox_visibility, is_single_line,
|
|
20
|
+
// update_header_visibility, adw_preferences_group_add)
|
|
9
21
|
// Reference: refs/libadwaita/src/adw-preferences-group.ui (header_box layout)
|
|
10
22
|
// Reference: refs/adwaita-web/adwaita-web/scss/_preferences.scss
|
|
11
23
|
// Adapted from Adwaita Web UI Framework (https://github.com/mclellac/adwaita-web).
|
|
12
24
|
// Copyright (c) 2025 csm. MIT License.
|
|
13
25
|
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
14
26
|
// Modifications: Reimplemented as Web Component for @gjsify/adwaita-web;
|
|
15
|
-
// description line + header-suffix slot added to match Adw.PreferencesGroup
|
|
27
|
+
// description line + header-suffix slot added to match Adw.PreferencesGroup;
|
|
28
|
+
// header/listbox state delegated to @gjsify/adwaita-core.
|
|
29
|
+
|
|
30
|
+
import { derivePreferencesGroupHeader } from '@gjsify/adwaita-core';
|
|
31
|
+
|
|
32
|
+
/** A child that asked to sit in the header rather than in the boxed list. */
|
|
33
|
+
function isHeaderSuffix(node: Node): boolean {
|
|
34
|
+
return node instanceof Element && node.getAttribute('slot') === 'header-suffix';
|
|
35
|
+
}
|
|
16
36
|
|
|
17
37
|
export class AdwPreferencesGroup extends HTMLElement {
|
|
38
|
+
private _headerEl!: HTMLDivElement;
|
|
18
39
|
private _titleEl!: HTMLSpanElement;
|
|
19
40
|
private _descriptionEl!: HTMLSpanElement;
|
|
20
41
|
private _suffixEl!: HTMLDivElement;
|
|
42
|
+
private _listboxEl!: HTMLDivElement;
|
|
21
43
|
private _initialized = false;
|
|
44
|
+
private _observer: MutationObserver | null = null;
|
|
22
45
|
|
|
23
46
|
static get observedAttributes() {
|
|
24
47
|
return ['title', 'description'];
|
|
@@ -33,22 +56,52 @@ export class AdwPreferencesGroup extends HTMLElement {
|
|
|
33
56
|
else this.removeAttribute('description');
|
|
34
57
|
}
|
|
35
58
|
|
|
59
|
+
/**
|
|
60
|
+
* `adw_preferences_group_add` (adw-preferences-group.c:396-411): a row goes
|
|
61
|
+
* into the boxed list, a `header-suffix` child into the header.
|
|
62
|
+
*
|
|
63
|
+
* Exposed as a method AND wired to a `MutationObserver` on the host, because
|
|
64
|
+
* both spellings occur: `group.addRow(row)` in code, `group.append(row)` in
|
|
65
|
+
* a framework's rendering. Before this, a child appended after connect
|
|
66
|
+
* stayed a bare child of the host — outside the card, and invisible to the
|
|
67
|
+
* row count that decides whether the card is painted at all.
|
|
68
|
+
*/
|
|
69
|
+
addRow(child: Node): void {
|
|
70
|
+
if (isHeaderSuffix(child)) this._suffixEl.appendChild(child);
|
|
71
|
+
else this._listboxEl.appendChild(child);
|
|
72
|
+
this._renderHeader();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* `adw_preferences_group_remove` (:421-440). C logs
|
|
77
|
+
* `ADW_CRITICAL_CANNOT_REMOVE_CHILD` and no-ops for a child it does not
|
|
78
|
+
* own; returning `false` is the same contract without a console write.
|
|
79
|
+
*/
|
|
80
|
+
removeRow(child: Node): boolean {
|
|
81
|
+
const parent = child.parentNode;
|
|
82
|
+
if (parent !== this._listboxEl && parent !== this._suffixEl) return false;
|
|
83
|
+
parent.removeChild(child);
|
|
84
|
+
this._renderHeader();
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
36
88
|
connectedCallback() {
|
|
37
|
-
if (this._initialized)
|
|
89
|
+
if (this._initialized) {
|
|
90
|
+
this._observeHost();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
38
93
|
this._initialized = true;
|
|
39
94
|
|
|
40
95
|
const children = Array.from(this.childNodes);
|
|
41
96
|
// Children opting into the header-suffix slot are hoisted to the
|
|
42
97
|
// trailing edge of the header; everything else goes into the boxed list.
|
|
43
|
-
const suffixChildren = children.filter(
|
|
44
|
-
(child) => child instanceof Element && child.getAttribute('slot') === 'header-suffix',
|
|
45
|
-
);
|
|
98
|
+
const suffixChildren = children.filter(isHeaderSuffix);
|
|
46
99
|
const rowChildren = children.filter((child) => !suffixChildren.includes(child));
|
|
47
100
|
|
|
48
101
|
// Header: a flex row with a title/description label column on the left
|
|
49
102
|
// and the optional header-suffix on the trailing edge.
|
|
50
|
-
|
|
51
|
-
|
|
103
|
+
this._headerEl = document.createElement('div');
|
|
104
|
+
this._headerEl.className = 'adw-preferences-group-header';
|
|
52
105
|
|
|
53
106
|
const labels = document.createElement('div');
|
|
54
107
|
labels.className = 'adw-preferences-group-labels';
|
|
@@ -63,38 +116,91 @@ export class AdwPreferencesGroup extends HTMLElement {
|
|
|
63
116
|
|
|
64
117
|
this._suffixEl = document.createElement('div');
|
|
65
118
|
this._suffixEl.className = 'adw-preferences-group-suffix';
|
|
66
|
-
|
|
119
|
+
for (const child of suffixChildren) this._suffixEl.appendChild(child);
|
|
67
120
|
|
|
68
|
-
|
|
121
|
+
this._headerEl.append(labels, this._suffixEl);
|
|
69
122
|
|
|
70
123
|
// Boxed list container — the remaining children move into it.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
124
|
+
this._listboxEl = document.createElement('div');
|
|
125
|
+
this._listboxEl.className = 'adw-preferences-group-listbox';
|
|
126
|
+
for (const child of rowChildren) this._listboxEl.appendChild(child);
|
|
74
127
|
|
|
75
|
-
this.replaceChildren(
|
|
128
|
+
this.replaceChildren(this._headerEl, this._listboxEl);
|
|
76
129
|
|
|
77
130
|
this._renderHeader();
|
|
131
|
+
this._observeHost();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
disconnectedCallback() {
|
|
135
|
+
// The observer holds this element alive through the host node; drop it
|
|
136
|
+
// with the connection that created it, not at some later "cleanup".
|
|
137
|
+
this._observer?.disconnect();
|
|
138
|
+
this._observer = null;
|
|
78
139
|
}
|
|
79
140
|
|
|
80
141
|
attributeChangedCallback() {
|
|
81
142
|
if (this._initialized) this._renderHeader();
|
|
82
143
|
}
|
|
83
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Adopt children appended to the HOST after the subtree was built, so
|
|
147
|
+
* `group.append(row)` behaves like `adw_preferences_group_add`.
|
|
148
|
+
*
|
|
149
|
+
* C gets this for free — `gtk_widget_observe_children` on the listbox drives
|
|
150
|
+
* `update_listbox_visibility` through `items-changed`
|
|
151
|
+
* (adw-preferences-group.c:335-339) — and a DOM element needs the observer
|
|
152
|
+
* to have the same rule hold over time rather than only at connect.
|
|
153
|
+
*/
|
|
154
|
+
private _observeHost(): void {
|
|
155
|
+
if (this._observer) return;
|
|
156
|
+
this._observer = new MutationObserver((records) => {
|
|
157
|
+
for (const record of records) {
|
|
158
|
+
// Only the HOST's own children need re-homing. Records from the
|
|
159
|
+
// listbox are the CONSEQUENCE of that move; re-adopting them
|
|
160
|
+
// would append each row a second time and reorder the list.
|
|
161
|
+
if (record.target !== this) continue;
|
|
162
|
+
for (const node of record.addedNodes) {
|
|
163
|
+
if (!(node instanceof Element)) continue;
|
|
164
|
+
if (node === this._headerEl || node === this._listboxEl) continue;
|
|
165
|
+
this.addRow(node);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// A removal straight out of the listbox still changes the row count.
|
|
169
|
+
this._renderHeader();
|
|
170
|
+
});
|
|
171
|
+
this._observer.observe(this, { childList: true });
|
|
172
|
+
this._observer.observe(this._listboxEl, { childList: true });
|
|
173
|
+
}
|
|
174
|
+
|
|
84
175
|
private _renderHeader() {
|
|
85
176
|
const title = this.getAttribute('title') ?? '';
|
|
86
|
-
this._titleEl.textContent = title;
|
|
87
|
-
this._titleEl.hidden = title.length === 0;
|
|
88
|
-
|
|
89
177
|
const description = this.getAttribute('description') ?? '';
|
|
178
|
+
|
|
179
|
+
this._titleEl.textContent = title;
|
|
90
180
|
this._descriptionEl.textContent = description;
|
|
91
|
-
this._descriptionEl.hidden = description.length === 0;
|
|
92
181
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
182
|
+
const state = derivePreferencesGroupHeader({
|
|
183
|
+
title,
|
|
184
|
+
description,
|
|
185
|
+
hasHeaderSuffix: this._suffixEl.childElementCount > 0,
|
|
186
|
+
// The RAW child count, matching `gtk_widget_observe_children` — a
|
|
187
|
+
// row with an empty title still keeps the card painted.
|
|
188
|
+
rowCount: this._listboxEl.childElementCount,
|
|
189
|
+
// The labels are `textContent`, not markup: what is displayed IS the
|
|
190
|
+
// attribute value, so visibility must be judged on the raw string.
|
|
191
|
+
// libadwaita's labels are `use-markup=True`; rendering Pango markup
|
|
192
|
+
// in the browser is a separate, still-open gap, and half-closing it
|
|
193
|
+
// here would hide a title this element visibly renders.
|
|
194
|
+
useMarkup: false,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
this._titleEl.hidden = !state.titleVisible;
|
|
198
|
+
this._descriptionEl.hidden = !state.descriptionVisible;
|
|
199
|
+
this._headerEl.hidden = !state.headerVisible;
|
|
200
|
+
// `single-line` is not cosmetic: the stylesheet keys the header's
|
|
201
|
+
// min-height / margin-bottom off it.
|
|
202
|
+
this._headerEl.classList.toggle('single-line', state.singleLine);
|
|
203
|
+
this._listboxEl.hidden = !state.listboxVisible;
|
|
98
204
|
}
|
|
99
205
|
}
|
|
100
206
|
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// <adw-progress-bar> — the DETERMINATE progress indicator, and the partner to
|
|
2
|
+
// <adw-spinner>, which only ever covers "busy, no idea how long".
|
|
3
|
+
//
|
|
4
|
+
// NO CORE STATE MACHINE. The whole behaviour is `CLAMP(fraction, 0, 1)` plus a
|
|
5
|
+
// pulsing flag — ADR 0004 is explicit that a widget with genuinely trivial
|
|
6
|
+
// behaviour does not get a core class, and the clamp itself is already shared:
|
|
7
|
+
// `glibClamp` (GLib's CLAMP, high bound tested FIRST) comes from
|
|
8
|
+
// `@gjsify/adwaita-core`. Nothing was added there for this element.
|
|
9
|
+
//
|
|
10
|
+
// WHAT IS NOT VERIFIABLE IN THIS TREE. `GtkProgressBar` is a GTK widget:
|
|
11
|
+
// `refs/gtk` is EMPTY here and libadwaita vendors no `adw-progress-bar.c`, so
|
|
12
|
+
// `gtk_progress_bar_pulse()`'s step semantics — the `pulse-step` default, how
|
|
13
|
+
// the block reflects at the ends, whether it resets `fraction` — are NOT
|
|
14
|
+
// verifiable and are NOT reproduced. What ships instead is the web idiom the
|
|
15
|
+
// vendored port uses: a CSS keyframe animation that runs while `pulsing` is set
|
|
16
|
+
// (scss/_progress_bar.scss, which cites which half came from which source), so
|
|
17
|
+
// {@link AdwProgressBar.pulse} ENTERS that state and is idempotent afterwards
|
|
18
|
+
// rather than advancing an offset nothing here could check. The
|
|
19
|
+
// default `show-text` label — `NN%` — is likewise this port's, not a
|
|
20
|
+
// reproduction of GTK's format string.
|
|
21
|
+
//
|
|
22
|
+
// Attributes:
|
|
23
|
+
// fraction — 0…1, clamped. How much of the bar is filled.
|
|
24
|
+
// pulsing — boolean; indeterminate. The block bounces instead of filling.
|
|
25
|
+
// show-text — boolean; reveal the text node under the bar.
|
|
26
|
+
// text — the text to show. Defaults to the fraction as a percentage.
|
|
27
|
+
// inverted — boolean; fill from the far end.
|
|
28
|
+
// osd — boolean; libadwaita's `.osd` STYLE CLASS — a 2px troughless bar
|
|
29
|
+
// for under a header bar (_progress-bar.scss:59-83).
|
|
30
|
+
// disabled — boolean; dims the widget.
|
|
31
|
+
// Properties:
|
|
32
|
+
// fraction / pulsing / showText / text / inverted / osd / disabled (get/set).
|
|
33
|
+
// Methods:
|
|
34
|
+
// pulse() — switch to indeterminate. GtkProgressBar's own vocabulary; see
|
|
35
|
+
// the caveat above for how far the resemblance goes.
|
|
36
|
+
//
|
|
37
|
+
// A11Y: `role="progressbar"` with `aria-valuemin`/`aria-valuemax` fixed at 0/1
|
|
38
|
+
// and `aria-valuenow` carrying the clamped fraction. While pulsing,
|
|
39
|
+
// `aria-valuenow` is REMOVED — that is how ARIA spells "indeterminate", and a
|
|
40
|
+
// stale value there would be announced as real progress.
|
|
41
|
+
//
|
|
42
|
+
// Reference: refs/libadwaita/src/stylesheet/widgets/_progress-bar.scss
|
|
43
|
+
// Reference: refs/adwaita-web/adwaita-web/scss/_progressbar.scss (the indeterminate animation only)
|
|
44
|
+
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
45
|
+
// Copyright (c) 2025 csm (adwaita-web). MIT License.
|
|
46
|
+
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
47
|
+
|
|
48
|
+
import { glibClamp } from '@gjsify/adwaita-core';
|
|
49
|
+
|
|
50
|
+
export class AdwProgressBar extends HTMLElement {
|
|
51
|
+
private _trough!: HTMLSpanElement;
|
|
52
|
+
private _progress!: HTMLSpanElement;
|
|
53
|
+
private _text!: HTMLSpanElement;
|
|
54
|
+
private _initialized = false;
|
|
55
|
+
|
|
56
|
+
static get observedAttributes() {
|
|
57
|
+
return ['fraction', 'pulsing', 'show-text', 'text', 'inverted', 'osd', 'disabled'];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** How much of the bar is filled, 0…1. Always the CLAMPED value. */
|
|
61
|
+
get fraction(): number {
|
|
62
|
+
const raw = Number.parseFloat(this.getAttribute('fraction') ?? '');
|
|
63
|
+
// A missing or unparseable attribute is 0, GtkProgressBar's default —
|
|
64
|
+
// NOT NaN, which would reach the width and the a11y value.
|
|
65
|
+
if (!Number.isFinite(raw)) return 0;
|
|
66
|
+
return glibClamp(raw, 0, 1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
set fraction(value: number) {
|
|
70
|
+
this.setAttribute('fraction', String(value));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Whether the bar is indeterminate. */
|
|
74
|
+
get pulsing(): boolean {
|
|
75
|
+
return this.hasAttribute('pulsing');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
set pulsing(value: boolean) {
|
|
79
|
+
this.toggleAttribute('pulsing', !!value);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Whether the text node is shown. */
|
|
83
|
+
get showText(): boolean {
|
|
84
|
+
return this.hasAttribute('show-text');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
set showText(value: boolean) {
|
|
88
|
+
this.toggleAttribute('show-text', !!value);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The shown text. Falls back to the fraction as a percentage. */
|
|
92
|
+
get text(): string {
|
|
93
|
+
const explicit = this.getAttribute('text');
|
|
94
|
+
if (explicit !== null) return explicit;
|
|
95
|
+
return `${Math.round(this.fraction * 100)}%`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
set text(value: string) {
|
|
99
|
+
this.setAttribute('text', value);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Whether the bar fills from the far end. */
|
|
103
|
+
get inverted(): boolean {
|
|
104
|
+
return this.hasAttribute('inverted');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
set inverted(value: boolean) {
|
|
108
|
+
this.toggleAttribute('inverted', !!value);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Whether the `.osd` hairline variant is used. */
|
|
112
|
+
get osd(): boolean {
|
|
113
|
+
return this.hasAttribute('osd');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
set osd(value: boolean) {
|
|
117
|
+
this.toggleAttribute('osd', !!value);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Whether the widget is dimmed. */
|
|
121
|
+
get disabled(): boolean {
|
|
122
|
+
return this.hasAttribute('disabled');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
set disabled(value: boolean) {
|
|
126
|
+
this.toggleAttribute('disabled', !!value);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Switch the bar to indeterminate — GtkProgressBar's method name for the
|
|
131
|
+
* same intent. See the module header: the bouncing block is a CSS animation
|
|
132
|
+
* here, so this ENTERS the state and repeat calls are no-ops; GTK's
|
|
133
|
+
* `pulse-step` arithmetic is not verifiable in this tree and is not faked.
|
|
134
|
+
*/
|
|
135
|
+
pulse(): void {
|
|
136
|
+
this.pulsing = true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
connectedCallback() {
|
|
140
|
+
if (this._initialized) return;
|
|
141
|
+
this._initialized = true;
|
|
142
|
+
|
|
143
|
+
this._trough = document.createElement('span');
|
|
144
|
+
this._trough.className = 'adw-progress-bar-trough';
|
|
145
|
+
this._progress = document.createElement('span');
|
|
146
|
+
this._progress.className = 'adw-progress-bar-progress';
|
|
147
|
+
this._trough.appendChild(this._progress);
|
|
148
|
+
|
|
149
|
+
this._text = document.createElement('span');
|
|
150
|
+
this._text.className = 'adw-progress-bar-text';
|
|
151
|
+
|
|
152
|
+
this.replaceChildren(this._trough, this._text);
|
|
153
|
+
|
|
154
|
+
this.setAttribute('role', 'progressbar');
|
|
155
|
+
this.setAttribute('aria-valuemin', '0');
|
|
156
|
+
this.setAttribute('aria-valuemax', '1');
|
|
157
|
+
|
|
158
|
+
this._render();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
attributeChangedCallback() {
|
|
162
|
+
if (!this._initialized) return;
|
|
163
|
+
this._render();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private _render(): void {
|
|
167
|
+
const fraction = this.fraction;
|
|
168
|
+
const pulsing = this.pulsing;
|
|
169
|
+
|
|
170
|
+
// `> trough.empty > progress { all: unset }` (_progress-bar.scss:85) —
|
|
171
|
+
// the indicator has to VANISH at 0, not draw a rounded stub. A pulsing
|
|
172
|
+
// bar is never empty: its block is what says "working".
|
|
173
|
+
const empty = !pulsing && fraction === 0;
|
|
174
|
+
this._trough.classList.toggle('empty', empty);
|
|
175
|
+
// The inline width is dropped in both the pulsing and the empty case, so
|
|
176
|
+
// the rules that own the indicator there — the keyframes, and
|
|
177
|
+
// `all: unset` — are not fighting an inline declaration they cannot win
|
|
178
|
+
// against (inline styles beat any author rule, `all: unset` included).
|
|
179
|
+
this._progress.style.width = pulsing || empty ? '' : `${fraction * 100}%`;
|
|
180
|
+
|
|
181
|
+
this._text.textContent = this.text;
|
|
182
|
+
this._text.hidden = !this.showText;
|
|
183
|
+
|
|
184
|
+
if (pulsing) this.removeAttribute('aria-valuenow');
|
|
185
|
+
else this.setAttribute('aria-valuenow', String(fraction));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
customElements.define('adw-progress-bar', AdwProgressBar);
|