@gjsify/adwaita-web 0.31.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adwaita-web.css +786 -272
- 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-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 +37 -11
- package/lib/types/elements/adw-menu-button.d.ts +6 -6
- package/lib/types/elements/adw-navigation-split-view.d.ts +6 -0
- package/lib/types/elements/adw-navigation-view.d.ts +63 -20
- package/lib/types/elements/adw-overlay-split-view.d.ts +38 -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-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 +27 -19
- package/lib/types/elements/adw-view-switcher-bar.d.ts +23 -7
- package/lib/types/elements/adw-view-switcher.d.ts +22 -11
- package/lib/types/elements/adw-window-title.d.ts +3 -1
- 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/_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/_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 +7 -3
- 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 +64 -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 +56 -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 +139 -0
- package/src/adw-banner.spec.ts +152 -0
- package/src/adw-button-content.spec.ts +183 -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-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 +194 -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 +301 -0
- package/src/elements/adw-about-dialog.ts +223 -163
- package/src/elements/adw-action-row.ts +120 -10
- 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 +15 -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 +240 -129
- package/src/elements/adw-menu-button.ts +93 -70
- package/src/elements/adw-navigation-split-view.ts +48 -7
- package/src/elements/adw-navigation-view.ts +286 -148
- package/src/elements/adw-overlay-split-view.ts +164 -14
- 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 +23 -8
- 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 +682 -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 +162 -85
- package/src/elements/adw-view-switcher-bar.ts +163 -81
- package/src/elements/adw-view-switcher.ts +231 -112
- package/src/elements/adw-window-title.ts +44 -11
- package/src/elements/adw-wrap-box.ts +131 -35
- 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 +309 -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 +446 -0
|
@@ -6,40 +6,86 @@
|
|
|
6
6
|
// `navigation-sidebar`); in `page` mode it renders as boxed lists with a
|
|
7
7
|
// chevron arrow on each row (the AdwPreferencesPage look the native widget
|
|
8
8
|
// switches to when collapsed).
|
|
9
|
+
//
|
|
10
|
+
// The BEHAVIOUR — the flat index space, the out-of-range rule, which section
|
|
11
|
+
// headers render, the selection-vs-activation split, the filter and the empty
|
|
12
|
+
// state — is HEADLESS and lives in `@gjsify/adwaita-core` (ADR 0004) as
|
|
13
|
+
// {@link SidebarState}; this element only builds DOM for it and re-emits its
|
|
14
|
+
// changes as events. It used to carry its own copy, which clamped an
|
|
15
|
+
// out-of-range `selected` to the LAST row where libadwaita drops the selection
|
|
16
|
+
// entirely, keyed section headers off the DECLARATION index instead of the rows
|
|
17
|
+
// that actually render, and froze the row list after the first connect. The
|
|
18
|
+
// shared vectors in `@gjsify/adwaita-core/conformance` now pin this element to
|
|
19
|
+
// the C source — see `src/adw-sidebar.spec.ts`.
|
|
20
|
+
//
|
|
9
21
|
// Sub-elements (declared as children, consumed at connect time — mirrors
|
|
10
22
|
// adw-tab-page in adw-tab-view):
|
|
11
23
|
// <adw-sidebar-section title="…"> — a titled group of items. An untitled
|
|
12
24
|
// section renders a separator before its rows (first section: nothing),
|
|
13
25
|
// mirroring AdwSidebar's stack header (title vs separator child).
|
|
14
|
-
// <adw-sidebar-item title="…" subtitle="…" icon-name="…"> —
|
|
26
|
+
// <adw-sidebar-item title="…" subtitle="…" icon-name="…" disabled hidden> —
|
|
27
|
+
// one row. `disabled` is AdwSidebarItem:enabled=false (bound to the row's
|
|
28
|
+
// `sensitive`), `hidden` is AdwSidebarItem:visible=false.
|
|
15
29
|
// Attributes (on <adw-sidebar>):
|
|
16
30
|
// mode (sidebar | page, default sidebar) — mirrors Adw.Sidebar:mode.
|
|
17
|
-
// selected (zero-based flat item index
|
|
31
|
+
// selected (zero-based flat item index; anything that is not a valid position
|
|
32
|
+
// — out of range, negative, fractional, unparseable — means NO selection,
|
|
18
33
|
// mirroring Gtk.INVALID_LIST_POSITION) — mirrors Adw.Sidebar:selected.
|
|
34
|
+
// Properties: `sections` (the spec list), `filter` (Adw.Sidebar:filter).
|
|
19
35
|
// Events:
|
|
20
|
-
// `notify::selected` (CustomEvent, bubbles,
|
|
21
|
-
// selected
|
|
22
|
-
//
|
|
23
|
-
//
|
|
36
|
+
// `notify::selected` (CustomEvent, bubbles,
|
|
37
|
+
// `detail = { selected, previous, interactive }`) on EVERY selection change,
|
|
38
|
+
// programmatic ones included — mirrors g_object_notify_by_pspec (PROP_SELECTED).
|
|
39
|
+
// `activated` (CustomEvent, bubbles, `detail = { index }`) on every click of a
|
|
40
|
+
// row, including a re-click of the selected one — mirrors AdwSidebar::activated,
|
|
41
|
+
// the documented way to reveal a split view's content pane.
|
|
24
42
|
// Reference: refs/libadwaita/src/adw-sidebar.c (AdwSidebar behaviour)
|
|
25
43
|
// Reference: refs/libadwaita/src/adw-sidebar-item.h (AdwSidebarItem properties)
|
|
26
44
|
// Reference: refs/libadwaita/src/stylesheet/widgets/_sidebars.scss (.navigation-sidebar, sidebar)
|
|
27
45
|
// Copyright (c) 2025 GNOME Foundation Inc. (libadwaita). LGPLv2.1+.
|
|
28
|
-
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web; the
|
|
47
|
+
// icon nodes are <adw-icon>.
|
|
48
|
+
|
|
49
|
+
import {
|
|
50
|
+
ADW_SIDEBAR_NO_SELECTION,
|
|
51
|
+
SidebarState,
|
|
52
|
+
type AdwSidebarItemSpec,
|
|
53
|
+
type AdwSidebarSectionSpec,
|
|
54
|
+
type SidebarFlatItem,
|
|
55
|
+
type SidebarHeaderSpec,
|
|
56
|
+
type SidebarItemFilter,
|
|
57
|
+
type SidebarSelectionChange,
|
|
58
|
+
} from '@gjsify/adwaita-core';
|
|
59
|
+
|
|
60
|
+
import { createAdwIcon } from './adw-icon.js';
|
|
61
|
+
|
|
62
|
+
// The sidebar consumes its declared children and drops them from the tree, so a
|
|
63
|
+
// later `setAttribute` on one cannot find its sidebar with `closest()`. These
|
|
64
|
+
// keep the link — the web stand-in for the `g_object_bind_property` /
|
|
65
|
+
// `notify::icon-name` bindings that keep a live AdwSidebarItem's labels in sync
|
|
66
|
+
// (adw-sidebar.c:1393-1421). Before this, `observedAttributes` was declared on
|
|
67
|
+
// both child elements and no `attributeChangedCallback` existed to answer it.
|
|
68
|
+
const itemBindings = new WeakMap<AdwSidebarItem, { sidebar: AdwSidebar; spec: AdwSidebarItemSpec }>();
|
|
69
|
+
const sectionBindings = new WeakMap<AdwSidebarSection, { sidebar: AdwSidebar; spec: AdwSidebarSectionSpec }>();
|
|
38
70
|
|
|
39
71
|
/** A single sidebar item. Child of <adw-sidebar-section>; consumed at connect time. */
|
|
40
72
|
export class AdwSidebarItem extends HTMLElement {
|
|
41
73
|
static get observedAttributes() {
|
|
42
|
-
return ['title', 'subtitle', 'icon-name'];
|
|
74
|
+
return ['title', 'subtitle', 'icon-name', 'disabled', 'hidden'];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
|
|
78
|
+
const binding = itemBindings.get(this);
|
|
79
|
+
if (!binding) return;
|
|
80
|
+
|
|
81
|
+
const value = newValue ?? '';
|
|
82
|
+
if (name === 'title') binding.spec.title = value;
|
|
83
|
+
else if (name === 'subtitle') binding.spec.subtitle = value;
|
|
84
|
+
else if (name === 'icon-name') binding.spec.iconName = value;
|
|
85
|
+
else if (name === 'disabled') binding.spec.enabled = newValue === null;
|
|
86
|
+
else if (name === 'hidden') binding.spec.visible = newValue === null;
|
|
87
|
+
|
|
88
|
+
binding.sidebar.refresh();
|
|
43
89
|
}
|
|
44
90
|
}
|
|
45
91
|
|
|
@@ -48,14 +94,23 @@ export class AdwSidebarSection extends HTMLElement {
|
|
|
48
94
|
static get observedAttributes() {
|
|
49
95
|
return ['title'];
|
|
50
96
|
}
|
|
97
|
+
|
|
98
|
+
attributeChangedCallback(_name: string, _oldValue: string | null, newValue: string | null) {
|
|
99
|
+
const binding = sectionBindings.get(this);
|
|
100
|
+
if (!binding) return;
|
|
101
|
+
|
|
102
|
+
binding.spec.title = newValue ?? '';
|
|
103
|
+
binding.sidebar.refresh();
|
|
104
|
+
}
|
|
51
105
|
}
|
|
52
106
|
|
|
53
107
|
export class AdwSidebar extends HTMLElement {
|
|
108
|
+
private readonly _state = new SidebarState();
|
|
54
109
|
private _listEl!: HTMLDivElement;
|
|
55
|
-
private _rows: HTMLButtonElement[] = [];
|
|
56
|
-
private _selected = 0;
|
|
57
|
-
private _mode: 'sidebar' | 'page' = 'sidebar';
|
|
110
|
+
private _rows: { el: HTMLButtonElement; index: number }[] = [];
|
|
58
111
|
private _initialized = false;
|
|
112
|
+
/** Guards the `selected` attribute write we make ourselves from re-entering. */
|
|
113
|
+
private _reflecting = false;
|
|
59
114
|
|
|
60
115
|
static get observedAttributes() {
|
|
61
116
|
return ['mode', 'selected'];
|
|
@@ -63,172 +118,276 @@ export class AdwSidebar extends HTMLElement {
|
|
|
63
118
|
|
|
64
119
|
/** Zero-based flat index of the selected item, or -1 for no selection. */
|
|
65
120
|
get selected(): number {
|
|
66
|
-
return this.
|
|
121
|
+
return this._state.selected;
|
|
67
122
|
}
|
|
68
123
|
|
|
69
124
|
set selected(value: number) {
|
|
70
125
|
this.setAttribute('selected', String(value));
|
|
71
126
|
}
|
|
72
127
|
|
|
128
|
+
/** The selected item's spec, or undefined — Adw.Sidebar:selected-item. */
|
|
129
|
+
get selectedItem(): AdwSidebarItemSpec | undefined {
|
|
130
|
+
return this._state.selectedItem;
|
|
131
|
+
}
|
|
132
|
+
|
|
73
133
|
/** 'sidebar' (flat navigation list) or 'page' (boxed lists). */
|
|
74
134
|
get mode(): 'sidebar' | 'page' {
|
|
75
|
-
return this.
|
|
135
|
+
return this._state.mode;
|
|
76
136
|
}
|
|
77
137
|
|
|
78
138
|
set mode(value: 'sidebar' | 'page') {
|
|
79
139
|
this.setAttribute('mode', value);
|
|
80
140
|
}
|
|
81
141
|
|
|
142
|
+
/** The section/item model. Assigning replaces it, the way adw_sidebar_remove_all + append would. */
|
|
143
|
+
get sections(): readonly AdwSidebarSectionSpec[] {
|
|
144
|
+
return this._state.sections;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
set sections(value: readonly AdwSidebarSectionSpec[]) {
|
|
148
|
+
this._state.setSections(value);
|
|
149
|
+
this._rebuild();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Adw.Sidebar:filter — decides which rows render, never the selection index space. */
|
|
153
|
+
get filter(): SidebarItemFilter | null {
|
|
154
|
+
return this._state.filter;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
set filter(value: SidebarItemFilter | null) {
|
|
158
|
+
this._state.setFilter(value);
|
|
159
|
+
this._rebuild();
|
|
160
|
+
}
|
|
161
|
+
|
|
82
162
|
connectedCallback() {
|
|
83
163
|
if (this._initialized) return;
|
|
84
164
|
this._initialized = true;
|
|
85
165
|
|
|
86
|
-
// Snapshot the declared <adw-sidebar-section> children before we take
|
|
87
|
-
// over the subtree — their title + <adw-sidebar-item> descendants become
|
|
88
|
-
// the rendered sections and rows.
|
|
89
|
-
const sections = Array.from(this.querySelectorAll('adw-sidebar-section')) as AdwSidebarSection[];
|
|
90
|
-
|
|
91
166
|
this._listEl = document.createElement('div');
|
|
92
167
|
this._listEl.className = 'adw-sidebar-list';
|
|
93
168
|
this._listEl.setAttribute('role', 'listbox');
|
|
94
169
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
// separator (matches the `.header.first` rule).
|
|
105
|
-
const header = document.createElement('div');
|
|
106
|
-
header.className = 'adw-sidebar-section-header';
|
|
107
|
-
if (title) {
|
|
108
|
-
header.classList.add('has-title');
|
|
109
|
-
const heading = document.createElement('span');
|
|
110
|
-
heading.className = 'adw-sidebar-section-heading';
|
|
111
|
-
heading.textContent = title;
|
|
112
|
-
header.appendChild(heading);
|
|
113
|
-
if (sectionIndex === 0) header.classList.add('first');
|
|
114
|
-
} else {
|
|
115
|
-
header.classList.add('separator');
|
|
116
|
-
if (sectionIndex === 0) header.hidden = true;
|
|
117
|
-
}
|
|
118
|
-
this._listEl.appendChild(header);
|
|
119
|
-
|
|
120
|
-
const sectionEl = document.createElement('div');
|
|
121
|
-
sectionEl.className = 'adw-sidebar-section';
|
|
170
|
+
// Seed the model BEFORE subscribing: connecting is not a change anyone
|
|
171
|
+
// asked to hear about, and `setSections` runs the 0 → n auto-select.
|
|
172
|
+
this._state.setSections(this._readDeclaredSections());
|
|
173
|
+
this._state.setMode(this._readModeAttr());
|
|
174
|
+
// A declared `selected` wins over that auto-select. libadwaita would let
|
|
175
|
+
// the auto-select clobber a builder-set property (items arrive after
|
|
176
|
+
// properties, and items_changed_cb then selects 0); for a declarative
|
|
177
|
+
// element the authored attribute has to survive its own children.
|
|
178
|
+
if (this.hasAttribute('selected')) this._state.setSelected(this._readSelectedAttr());
|
|
122
179
|
|
|
123
|
-
|
|
124
|
-
const flatIndex = this._rows.length;
|
|
125
|
-
const itemTitle = item.getAttribute('title') ?? '';
|
|
126
|
-
const subtitle = item.getAttribute('subtitle') ?? '';
|
|
127
|
-
const icon = item.getAttribute('icon-name') ?? '';
|
|
128
|
-
|
|
129
|
-
const row = document.createElement('button');
|
|
130
|
-
row.type = 'button';
|
|
131
|
-
row.className = 'adw-sidebar-item';
|
|
132
|
-
row.setAttribute('role', 'option');
|
|
133
|
-
|
|
134
|
-
const iconEl = document.createElement('span');
|
|
135
|
-
iconEl.className = `${iconClass(icon)} adw-sidebar-item-icon`;
|
|
136
|
-
iconEl.setAttribute('aria-hidden', 'true'); // decorative (mask-image only)
|
|
137
|
-
iconEl.hidden = icon.length === 0;
|
|
138
|
-
row.appendChild(iconEl);
|
|
139
|
-
|
|
140
|
-
const textEl = document.createElement('span');
|
|
141
|
-
textEl.className = 'adw-sidebar-item-text';
|
|
142
|
-
|
|
143
|
-
const titleEl = document.createElement('span');
|
|
144
|
-
titleEl.className = 'adw-sidebar-item-title';
|
|
145
|
-
titleEl.textContent = itemTitle;
|
|
146
|
-
titleEl.hidden = itemTitle.length === 0;
|
|
147
|
-
textEl.appendChild(titleEl);
|
|
148
|
-
|
|
149
|
-
const subtitleEl = document.createElement('span');
|
|
150
|
-
subtitleEl.className = 'adw-sidebar-item-subtitle';
|
|
151
|
-
subtitleEl.textContent = subtitle;
|
|
152
|
-
subtitleEl.hidden = subtitle.length === 0;
|
|
153
|
-
textEl.appendChild(subtitleEl);
|
|
154
|
-
|
|
155
|
-
row.appendChild(textEl);
|
|
156
|
-
|
|
157
|
-
// Page mode adds a trailing chevron on every row, the way the
|
|
158
|
-
// boxed-list AdwActionRow carries a `go-next-symbolic` arrow.
|
|
159
|
-
const arrowEl = document.createElement('span');
|
|
160
|
-
arrowEl.className = 'adw-icon adw-icon--go-next adw-sidebar-item-arrow';
|
|
161
|
-
arrowEl.setAttribute('aria-hidden', 'true');
|
|
162
|
-
row.appendChild(arrowEl);
|
|
163
|
-
|
|
164
|
-
row.addEventListener('click', () => this._activateIndex(flatIndex));
|
|
165
|
-
sectionEl.appendChild(row);
|
|
166
|
-
this._rows.push(row);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
this._listEl.appendChild(sectionEl);
|
|
170
|
-
});
|
|
180
|
+
this._state.subscribe((change) => this._onSelectionChanged(change));
|
|
171
181
|
|
|
172
182
|
this.replaceChildren(this._listEl);
|
|
173
|
-
|
|
174
|
-
this.
|
|
175
|
-
this.
|
|
176
|
-
this._render();
|
|
183
|
+
this._applyMode();
|
|
184
|
+
this._rebuild();
|
|
185
|
+
this._reflectSelected();
|
|
177
186
|
}
|
|
178
187
|
|
|
179
188
|
attributeChangedCallback(name: string) {
|
|
180
|
-
if (!this._initialized) return;
|
|
189
|
+
if (!this._initialized || this._reflecting) return;
|
|
190
|
+
|
|
181
191
|
if (name === 'selected') {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
192
|
+
this._state.setSelected(this._readSelectedAttr());
|
|
193
|
+
// Normalise even when nothing moved, so `selected="5"` on a 3-item
|
|
194
|
+
// sidebar does not keep claiming 5 while the property reads -1.
|
|
195
|
+
this._reflectSelected();
|
|
187
196
|
return;
|
|
188
197
|
}
|
|
189
|
-
if (name === 'mode') {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
198
|
+
if (name === 'mode' && this._state.setMode(this._readModeAttr())) {
|
|
199
|
+
this._applyMode();
|
|
200
|
+
this._applySelection();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Re-derive after a declared `<adw-sidebar-item>` / `<adw-sidebar-section>`
|
|
206
|
+
* attribute changed. The item model keeps its shape, so the selection cannot
|
|
207
|
+
* move — exactly like the GObject property bindings this stands in for.
|
|
208
|
+
*/
|
|
209
|
+
refresh(): void {
|
|
210
|
+
if (!this._initialized) return;
|
|
211
|
+
this._state.refresh();
|
|
212
|
+
this._rebuild();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Snapshot the declared children into specs, and bind them for live updates. */
|
|
216
|
+
private _readDeclaredSections(): AdwSidebarSectionSpec[] {
|
|
217
|
+
const sections: AdwSidebarSectionSpec[] = [];
|
|
218
|
+
|
|
219
|
+
for (const sectionEl of Array.from(this.querySelectorAll('adw-sidebar-section')) as AdwSidebarSection[]) {
|
|
220
|
+
const items: AdwSidebarItemSpec[] = [];
|
|
221
|
+
|
|
222
|
+
for (const itemEl of Array.from(sectionEl.querySelectorAll('adw-sidebar-item')) as AdwSidebarItem[]) {
|
|
223
|
+
const item: AdwSidebarItemSpec = {
|
|
224
|
+
title: itemEl.getAttribute('title') ?? '',
|
|
225
|
+
subtitle: itemEl.getAttribute('subtitle') ?? '',
|
|
226
|
+
iconName: itemEl.getAttribute('icon-name') ?? '',
|
|
227
|
+
enabled: !itemEl.hasAttribute('disabled'),
|
|
228
|
+
visible: !itemEl.hasAttribute('hidden'),
|
|
229
|
+
};
|
|
230
|
+
itemBindings.set(itemEl, { sidebar: this, spec: item });
|
|
231
|
+
items.push(item);
|
|
194
232
|
}
|
|
233
|
+
|
|
234
|
+
const section: AdwSidebarSectionSpec = { title: sectionEl.getAttribute('title') ?? '', items };
|
|
235
|
+
sectionBindings.set(sectionEl, { sidebar: this, spec: section });
|
|
236
|
+
sections.push(section);
|
|
195
237
|
}
|
|
238
|
+
|
|
239
|
+
return sections;
|
|
196
240
|
}
|
|
197
241
|
|
|
198
242
|
private _readModeAttr(): 'sidebar' | 'page' {
|
|
199
243
|
return this.getAttribute('mode') === 'page' ? 'page' : 'sidebar';
|
|
200
244
|
}
|
|
201
245
|
|
|
246
|
+
/**
|
|
247
|
+
* `parseFloat`, not `parseInt`: the core rejects a fractional position, and
|
|
248
|
+
* truncating `"1.5"` to row 1 here would hide that from it. An unparseable
|
|
249
|
+
* value is NaN, which the core turns into "no selection".
|
|
250
|
+
*/
|
|
202
251
|
private _readSelectedAttr(): number {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
252
|
+
return Number.parseFloat(this.getAttribute('selected') ?? '');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private _reflectSelected(): void {
|
|
256
|
+
this._reflecting = true;
|
|
257
|
+
this.setAttribute('selected', String(this._state.selected));
|
|
258
|
+
this._reflecting = false;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private _onSelectionChanged(change: SidebarSelectionChange): void {
|
|
262
|
+
this._reflectSelected();
|
|
263
|
+
this._applySelection();
|
|
264
|
+
this.dispatchEvent(
|
|
265
|
+
new CustomEvent('notify::selected', {
|
|
266
|
+
bubbles: true,
|
|
267
|
+
detail: { selected: change.selected, previous: change.previous, interactive: change.interactive },
|
|
268
|
+
}),
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Click handler — the core applies the selection, then `activated` fires. */
|
|
273
|
+
private _activate(index: number): void {
|
|
274
|
+
if (!this._state.activate(index).activated) return;
|
|
220
275
|
this.dispatchEvent(new CustomEvent('activated', { bubbles: true, detail: { index } }));
|
|
221
276
|
}
|
|
222
277
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
278
|
+
/** Rebuild the rendered rows + headers from the core's derived model. */
|
|
279
|
+
private _rebuild(): void {
|
|
280
|
+
const headers = new Map<number, SidebarHeaderSpec>();
|
|
281
|
+
for (const header of this._state.headers) headers.set(header.sectionIndex, header);
|
|
282
|
+
|
|
283
|
+
// Consecutive rendered rows of one section share a header and a container.
|
|
284
|
+
const groups: { sectionIndex: number; items: SidebarFlatItem[] }[] = [];
|
|
285
|
+
for (const flat of this._state.visibleItems) {
|
|
286
|
+
const last = groups[groups.length - 1];
|
|
287
|
+
if (last && last.sectionIndex === flat.sectionIndex) last.items.push(flat);
|
|
288
|
+
else groups.push({ sectionIndex: flat.sectionIndex, items: [flat] });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
this._rows = [];
|
|
292
|
+
const children: HTMLElement[] = [];
|
|
293
|
+
|
|
294
|
+
for (const group of groups) {
|
|
295
|
+
const header = headers.get(group.sectionIndex);
|
|
296
|
+
if (header) children.push(this._createHeader(header));
|
|
297
|
+
|
|
298
|
+
const sectionEl = document.createElement('div');
|
|
299
|
+
sectionEl.className = 'adw-sidebar-section';
|
|
300
|
+
for (const flat of group.items) {
|
|
301
|
+
const row = this._createRow(flat);
|
|
302
|
+
sectionEl.appendChild(row);
|
|
303
|
+
this._rows.push({ el: row, index: flat.index });
|
|
304
|
+
}
|
|
305
|
+
children.push(sectionEl);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
this._listEl.replaceChildren(...children);
|
|
309
|
+
this.classList.toggle('empty', this._state.isEmpty);
|
|
310
|
+
this._applySelection();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private _createHeader(header: SidebarHeaderSpec): HTMLDivElement {
|
|
314
|
+
const el = document.createElement('div');
|
|
315
|
+
el.className = 'adw-sidebar-section-header';
|
|
316
|
+
|
|
317
|
+
if (header.kind === 'title') {
|
|
318
|
+
el.classList.add('has-title');
|
|
319
|
+
const heading = document.createElement('span');
|
|
320
|
+
heading.className = 'adw-sidebar-section-heading';
|
|
321
|
+
heading.textContent = header.title;
|
|
322
|
+
el.appendChild(heading);
|
|
323
|
+
} else {
|
|
324
|
+
el.classList.add('separator');
|
|
325
|
+
}
|
|
326
|
+
if (header.first) el.classList.add('first');
|
|
327
|
+
|
|
328
|
+
return el;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private _createRow(flat: SidebarFlatItem): HTMLButtonElement {
|
|
332
|
+
const item = flat.item;
|
|
333
|
+
|
|
334
|
+
const row = document.createElement('button');
|
|
335
|
+
row.type = 'button';
|
|
336
|
+
row.className = 'adw-sidebar-item';
|
|
337
|
+
row.setAttribute('role', 'option');
|
|
338
|
+
// `visible` / `enabled` are bound to the row's `visible` / `sensitive`
|
|
339
|
+
// in create_row (adw-sidebar.c:1382-1383): the row still EXISTS and still
|
|
340
|
+
// owns its flat index, it just does not show or respond.
|
|
341
|
+
row.hidden = item.visible === false;
|
|
342
|
+
row.disabled = item.enabled === false;
|
|
343
|
+
|
|
344
|
+
const iconEl = createAdwIcon(item.iconName ?? null, 'adw-sidebar-item-icon');
|
|
345
|
+
iconEl.hidden = !flat.iconVisible;
|
|
346
|
+
row.appendChild(iconEl);
|
|
347
|
+
|
|
348
|
+
const textEl = document.createElement('span');
|
|
349
|
+
textEl.className = 'adw-sidebar-item-text';
|
|
350
|
+
|
|
351
|
+
const titleEl = document.createElement('span');
|
|
352
|
+
titleEl.className = 'adw-sidebar-item-title';
|
|
353
|
+
titleEl.textContent = item.title;
|
|
354
|
+
titleEl.hidden = !flat.titleVisible;
|
|
355
|
+
textEl.appendChild(titleEl);
|
|
356
|
+
|
|
357
|
+
const subtitleEl = document.createElement('span');
|
|
358
|
+
subtitleEl.className = 'adw-sidebar-item-subtitle';
|
|
359
|
+
subtitleEl.textContent = item.subtitle ?? '';
|
|
360
|
+
subtitleEl.hidden = !flat.subtitleVisible;
|
|
361
|
+
textEl.appendChild(subtitleEl);
|
|
362
|
+
|
|
363
|
+
row.appendChild(textEl);
|
|
364
|
+
|
|
365
|
+
// Page mode adds a trailing chevron on every row, the way the
|
|
366
|
+
// boxed-list AdwActionRow carries a `go-next-symbolic` arrow.
|
|
367
|
+
row.appendChild(createAdwIcon('go-next', 'adw-sidebar-item-arrow'));
|
|
368
|
+
|
|
369
|
+
row.addEventListener('click', () => this._activate(flat.index));
|
|
370
|
+
|
|
371
|
+
return row;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private _applyMode(): void {
|
|
375
|
+
const page = this._state.mode === 'page';
|
|
376
|
+
this.classList.toggle('page', page);
|
|
377
|
+
this.classList.toggle('mode-sidebar', !page);
|
|
378
|
+
}
|
|
226
379
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
380
|
+
private _applySelection(): void {
|
|
381
|
+
const selected = this._state.selected;
|
|
382
|
+
// Page mode tracks the selection but never paints it (adw-sidebar.c:2948-2951):
|
|
383
|
+
// its rows are plain boxed-list AdwActionRows with no selected state.
|
|
384
|
+
const paint = this._state.selectionVisible;
|
|
385
|
+
|
|
386
|
+
this._rows.forEach((row, position) => {
|
|
387
|
+
const isSelected = row.index === selected;
|
|
388
|
+
row.el.classList.toggle('selected', isSelected && paint);
|
|
389
|
+
row.el.setAttribute('aria-selected', String(isSelected));
|
|
390
|
+
row.el.tabIndex = isSelected || (selected === ADW_SIDEBAR_NO_SELECTION && position === 0) ? 0 : -1;
|
|
232
391
|
});
|
|
233
392
|
}
|
|
234
393
|
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// title/subtitle text column added to match Adw.SpinRow; the adjustment state
|
|
18
18
|
// machine composed from @gjsify/adwaita-core.
|
|
19
19
|
|
|
20
|
-
import { SpinState } from '@gjsify/adwaita-core';
|
|
20
|
+
import { SpinState, deriveRowLabels } from '@gjsify/adwaita-core';
|
|
21
21
|
|
|
22
22
|
export class AdwSpinRow extends HTMLElement {
|
|
23
23
|
private _input!: HTMLInputElement;
|
|
@@ -123,10 +123,16 @@ export class AdwSpinRow extends HTMLElement {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
private _renderText() {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
// The `string_is_not_empty` label rule, from core — this block was one of
|
|
127
|
+
// six hand-rolled copies, all of which omitted the TITLE half.
|
|
128
|
+
const labels = deriveRowLabels({
|
|
129
|
+
title: this.getAttribute('title'),
|
|
130
|
+
subtitle: this.getAttribute('subtitle'),
|
|
131
|
+
});
|
|
132
|
+
this._titleEl.textContent = labels.title;
|
|
133
|
+
this._titleEl.hidden = !labels.titleVisible;
|
|
134
|
+
this._subtitleEl.textContent = labels.subtitle;
|
|
135
|
+
this._subtitleEl.hidden = !labels.subtitleVisible;
|
|
130
136
|
}
|
|
131
137
|
|
|
132
138
|
/** A stepper press — the interactive path, so it emits `notify::value`. */
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
// <adw-spinner> — A continuously-spinning loading indicator.
|
|
2
|
-
// Attributes: size (px,
|
|
2
|
+
// Attributes: size (px; the measured minimum, 16, when absent or unusable).
|
|
3
|
+
//
|
|
4
|
+
// The ring geometry is `Adw.Spinner`'s, not a guess: the diameter and the stroke
|
|
5
|
+
// come from `@gjsify/adwaita-core`'s `spinnerGeometry`, which is
|
|
6
|
+
// `AdwSpinnerPaintable`'s `radius = min(floor(min(w, h) / 2), 32)` with
|
|
7
|
+
// `lineWidth = diameter / 8`. This element used to default to 24px and stroke
|
|
8
|
+
// `max(2, round(size / 12))`, which draws 2px where Adwaita draws 3px at 24, 4px
|
|
9
|
+
// against 6px at 48 and 5px against 8px at 64 — the ring was visibly thinner than
|
|
10
|
+
// GTK's at every size above the minimum.
|
|
11
|
+
//
|
|
12
|
+
// Reference: refs/libadwaita/src/adw-spinner.c (MIN_SIZE, adw_spinner_measure)
|
|
13
|
+
// Reference: refs/libadwaita/src/adw-spinner-paintable.c (MAX_RADIUS, calculate_line_width)
|
|
3
14
|
// Reference: refs/adwaita-web/adwaita-web/scss/_spinner.scss
|
|
4
|
-
// Reference: refs/libadwaita (Adw.Spinner)
|
|
5
15
|
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
6
16
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
7
17
|
|
|
18
|
+
import { resolveSpinnerSize, spinnerGeometry } from '@gjsify/adwaita-core';
|
|
19
|
+
|
|
8
20
|
export class AdwSpinner extends HTMLElement {
|
|
9
21
|
static get observedAttributes() {
|
|
10
22
|
return ['size'];
|
|
@@ -19,12 +31,15 @@ export class AdwSpinner extends HTMLElement {
|
|
|
19
31
|
}
|
|
20
32
|
|
|
21
33
|
private _sync() {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
// The element IS the ring, so it takes the DRAWN diameter rather than the
|
|
35
|
+
// requested box: a request below 16 is not representable (GTK never
|
|
36
|
+
// allocates below a widget's minimum) and one above 64 draws a 64px ring,
|
|
37
|
+
// which is Adwaita's documented "never larger than 64×64".
|
|
38
|
+
const size = resolveSpinnerSize(this.getAttribute('size'));
|
|
39
|
+
const { diameter, lineWidth } = spinnerGeometry(size, size);
|
|
40
|
+
this.style.width = `${diameter}px`;
|
|
41
|
+
this.style.height = `${diameter}px`;
|
|
42
|
+
this.style.borderWidth = `${lineWidth}px`;
|
|
28
43
|
}
|
|
29
44
|
}
|
|
30
45
|
|