@gjsify/adwaita-web 0.32.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.
Files changed (38) hide show
  1. package/dist/adwaita-web.css +72 -50
  2. package/dist/adwaita-web.css.map +1 -1
  3. package/lib/types/elements/adw-combo-row.d.ts +10 -0
  4. package/lib/types/elements/adw-inline-view-switcher.d.ts +37 -1
  5. package/lib/types/elements/adw-navigation-split-view.d.ts +42 -0
  6. package/lib/types/elements/adw-overlay-split-view.d.ts +56 -0
  7. package/lib/types/elements/adw-spinner.d.ts +23 -0
  8. package/lib/types/elements/adw-view-stack.d.ts +12 -0
  9. package/lib/types/elements/adw-view-switcher-bar.d.ts +7 -5
  10. package/lib/types/elements/adw-view-switcher.d.ts +55 -3
  11. package/lib/types/elements/adw-wrap-box.d.ts +58 -3
  12. package/lib/types/styles.generated.d.ts +1 -1
  13. package/package.json +6 -6
  14. package/scss/_combo_row.scss +19 -0
  15. package/scss/_navigation_split_view.scss +47 -8
  16. package/scss/_overlay_split_view.scss +117 -32
  17. package/scss/_spinner.scss +35 -21
  18. package/scss/_tab_view.scss +12 -13
  19. package/scss/_wrap_box.scss +9 -0
  20. package/src/adw-avatar.spec.ts +37 -0
  21. package/src/adw-button-content.spec.ts +24 -0
  22. package/src/adw-row-state.spec.ts +49 -0
  23. package/src/adw-wrap-box.spec.ts +148 -1
  24. package/src/chrome.spec.ts +111 -12
  25. package/src/elements/adw-action-row.ts +11 -1
  26. package/src/elements/adw-combo-row.ts +17 -0
  27. package/src/elements/adw-inline-view-switcher.ts +91 -8
  28. package/src/elements/adw-navigation-split-view.ts +179 -14
  29. package/src/elements/adw-overlay-split-view.ts +378 -20
  30. package/src/elements/adw-spinner.ts +165 -21
  31. package/src/elements/adw-tab-view.ts +20 -2
  32. package/src/elements/adw-view-stack.ts +21 -0
  33. package/src/elements/adw-view-switcher-bar.ts +13 -5
  34. package/src/elements/adw-view-switcher.ts +103 -11
  35. package/src/elements/adw-wrap-box.ts +290 -135
  36. package/src/split-views.spec.ts +586 -2
  37. package/src/styles.generated.ts +1 -1
  38. package/src/view-switcher.spec.ts +219 -0
@@ -2,31 +2,74 @@
2
2
  // visible when expanded; when `collapsed` it shows one pane at a time
3
3
  // (`show-content` decides which), mirroring Adw.NavigationSplitView's
4
4
  // push-navigation on narrow widths.
5
- // Attributes: collapsed, show-content, min-sidebar-width, max-sidebar-width,
6
- // breakpoint (an Adwaita condition, e.g. "max-width: 720px", that collapses
7
- // the view by itself — the counterpart to an Adw.Breakpoint add_setter()).
5
+ //
6
+ // Attributes: collapsed, show-content, sidebar-position, min-sidebar-width,
7
+ // max-sidebar-width, breakpoint (an Adwaita condition, e.g. "max-width: 720px",
8
+ // that collapses the view by itself — the counterpart to an Adw.Breakpoint
9
+ // add_setter()). A slotted pane may carry `tag`, which is
10
+ // `Adw.NavigationPage:tag`.
8
11
  // Slots: slot="sidebar", slot="content".
12
+ //
13
+ // The element used to toggle two CSS classes and nothing else, so THREE rules
14
+ // from the C had no expression here at all: the navigation-stack ordering table
15
+ // (a LONE child stays visible whatever `show-content` says; with
16
+ // `sidebar-position: end` the CONTENT is the root page), the duplicate-tag
17
+ // guard, and the `navigation.push` / `navigation.pop` routing that lets a nested
18
+ // split view forward a push outwards. All three are `NavigationSplitViewState`
19
+ // in `@gjsify/adwaita-core`, held to NAVIGATION_STACK_VECTORS,
20
+ // NAVIGATION_SPLIT_VIEW_CRITICALS and NAVIGATION_ACTION_VECTORS — the last of
21
+ // which had no consumer at all before this.
22
+ //
9
23
  // Reference: refs/adwaita-web/adwaita-web/docs/widgets/navigationsplitview.md
24
+ // Reference: refs/libadwaita/src/adw-navigation-split-view.c
10
25
  // Reference: refs/libadwaita/src/stylesheet/widgets/_sidebars.scss
11
26
  // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
12
27
  // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
13
28
 
14
- import { DEFAULT_MAX_SIDEBAR_WIDTH, DEFAULT_MIN_SIDEBAR_WIDTH, resolveSidebarBounds } from '@gjsify/adwaita-core';
29
+ import {
30
+ DEFAULT_MAX_SIDEBAR_WIDTH,
31
+ DEFAULT_MIN_SIDEBAR_WIDTH,
32
+ NavigationSplitViewState,
33
+ isSidebarAtVisualStart,
34
+ resolveSidebarBounds,
35
+ type AdwPackType,
36
+ type AdwTextDirection,
37
+ type NavigationActionResult,
38
+ } from '@gjsify/adwaita-core';
15
39
 
16
40
  import { bindBreakpointSetter } from '../breakpoints.js';
17
41
 
42
+ /** Detail of the `navigation-push` / `navigation-pop` delegation event. */
43
+ export interface NavigationDelegateDetail {
44
+ /** The tag a `push` names; absent for a `pop`. */
45
+ tag?: string;
46
+ /** Set by an ancestor that handled it — the `return TRUE` of the C's routing. */
47
+ handled: boolean;
48
+ }
49
+
18
50
  export class AdwNavigationSplitView extends HTMLElement {
19
51
  private _sidebarEl!: HTMLDivElement;
20
52
  private _contentEl!: HTMLDivElement;
21
53
  private _initialized = false;
22
54
  private _disposeBreakpoint: (() => void) | undefined;
55
+ /** The ordering table, the tag guard and the action routing (ADR 0004). */
56
+ private _state = new NavigationSplitViewState();
57
+ /** Re-entrancy guard for the `show-content` reflection. */
58
+ private _reflecting = false;
23
59
 
24
60
  static get observedAttributes() {
25
- return ['collapsed', 'show-content', 'min-sidebar-width', 'max-sidebar-width', 'breakpoint'];
61
+ return [
62
+ 'collapsed',
63
+ 'show-content',
64
+ 'sidebar-position',
65
+ 'min-sidebar-width',
66
+ 'max-sidebar-width',
67
+ 'breakpoint',
68
+ ];
26
69
  }
27
70
 
28
71
  get collapsed(): boolean {
29
- return this.hasAttribute('collapsed');
72
+ return this._initialized ? this._state.collapsed : this.hasAttribute('collapsed');
30
73
  }
31
74
 
32
75
  set collapsed(v: boolean) {
@@ -35,13 +78,49 @@ export class AdwNavigationSplitView extends HTMLElement {
35
78
 
36
79
  /** When collapsed, whether the content pane (true) or sidebar (false) shows. */
37
80
  get showContent(): boolean {
38
- return this.hasAttribute('show-content');
81
+ return this._initialized ? this._state.showContent : this.hasAttribute('show-content');
39
82
  }
40
83
 
41
84
  set showContent(v: boolean) {
42
85
  this.toggleAttribute('show-content', v);
43
86
  }
44
87
 
88
+ /** Which side the sidebar is packed on — `start` (default) or `end`. */
89
+ get sidebarPosition(): AdwPackType {
90
+ return this.getAttribute('sidebar-position') === 'end' ? 'end' : 'start';
91
+ }
92
+
93
+ set sidebarPosition(value: AdwPackType) {
94
+ this.setAttribute('sidebar-position', value);
95
+ }
96
+
97
+ /** The sidebar page's tag of record, or `null`. */
98
+ get sidebarTag(): string | null {
99
+ return this._state.sidebarTag;
100
+ }
101
+
102
+ /** The content page's tag of record, or `null`. */
103
+ get contentTag(): string | null {
104
+ return this._state.contentTag;
105
+ }
106
+
107
+ /**
108
+ * `navigation.push` with `tag` — `navigation_push_cb` (:644-685).
109
+ *
110
+ * An unmatched tag DELEGATES to the parent before it may become a critical,
111
+ * which is how nested split views forward a push outwards; on this port that
112
+ * is a bubbling `navigation-push` event whose `detail.handled` an ancestor
113
+ * sets. Returns what the routing decided.
114
+ */
115
+ push(tag: string): NavigationActionResult {
116
+ return this._state.push(tag);
117
+ }
118
+
119
+ /** `navigation.pop` — `navigation_pop_cb` (:687-702). */
120
+ pop(): NavigationActionResult {
121
+ return this._state.pop();
122
+ }
123
+
45
124
  connectedCallback() {
46
125
  if (this._initialized) return;
47
126
  this._initialized = true;
@@ -58,11 +137,68 @@ export class AdwNavigationSplitView extends HTMLElement {
58
137
  for (const child of contentChildren) this._contentEl.appendChild(child);
59
138
 
60
139
  this.replaceChildren(this._sidebarEl, this._contentEl);
140
+
141
+ // Parse-time attributes are the CONSTRUCTION properties, for the same
142
+ // reason the overlay view treats them so: applying them as sequential
143
+ // setters would emit a transition for a state the markup simply declared.
144
+ this._state = new NavigationSplitViewState({
145
+ sidebarPosition: this.sidebarPosition,
146
+ collapsed: this.hasAttribute('collapsed'),
147
+ showContent: this.hasAttribute('show-content'),
148
+ // `g_critical` has no browser counterpart that a consumer can see;
149
+ // `console.error` is the one a developer reads, and the TEXT is the
150
+ // C's verbatim so a GTK bug report and a browser one say the same.
151
+ onCritical: (message) => console.error(message),
152
+ onDelegate: (action, tag) => {
153
+ const detail: NavigationDelegateDetail = { tag, handled: false };
154
+ this.dispatchEvent(new CustomEvent(`navigation-${action}`, { bubbles: true, detail }));
155
+ return detail.handled;
156
+ },
157
+ });
158
+ // Which children EXIST decides the stack, so the panes are mounted into
159
+ // the state, not merely into the DOM — a lone child stays visible
160
+ // whatever `show-content` says (:389-401), which two CSS classes cannot
161
+ // express.
162
+ this._state.setSidebar(sidebarChildren.length > 0 ? { tag: this._tagOf(sidebarChildren) } : null);
163
+ this._state.setContent(contentChildren.length > 0 ? { tag: this._tagOf(contentChildren) } : null);
164
+ this._state.subscribe(() => {
165
+ this._reflectShowContent();
166
+ this._syncClasses();
167
+ });
168
+
61
169
  this._syncWidth();
62
170
  this._syncClasses();
63
171
  this._syncBreakpoint();
64
172
  }
65
173
 
174
+ /** `Adw.NavigationPage:tag` off the slotted pane, or `null` when untagged. */
175
+ private _tagOf(children: readonly Element[]): string | null {
176
+ for (const child of children) {
177
+ const tag = child.getAttribute('tag');
178
+ if (tag !== null) return tag;
179
+ }
180
+ return null;
181
+ }
182
+
183
+ /**
184
+ * Mirror the state's `showContent` onto the attribute, so the DOM keeps
185
+ * telling the truth after a `navigation.push` moved it.
186
+ *
187
+ * Guarded the way the overlay view's reflection is: writing the attribute
188
+ * re-enters `attributeChangedCallback`, which without the flag is a loop.
189
+ */
190
+ private _reflectShowContent() {
191
+ if (this._reflecting) return;
192
+ const wanted = this._state.showContent;
193
+ if (this.hasAttribute('show-content') === wanted) return;
194
+ this._reflecting = true;
195
+ try {
196
+ this.toggleAttribute('show-content', wanted);
197
+ } finally {
198
+ this._reflecting = false;
199
+ }
200
+ }
201
+
66
202
  disconnectedCallback() {
67
203
  this._disposeBreakpoint?.();
68
204
  this._disposeBreakpoint = undefined;
@@ -70,13 +206,20 @@ export class AdwNavigationSplitView extends HTMLElement {
70
206
 
71
207
  attributeChangedCallback(name: string) {
72
208
  if (!this._initialized) return;
73
- if (name === 'breakpoint') this._syncBreakpoint();
74
- else if (name === 'min-sidebar-width' || name === 'max-sidebar-width') this._syncWidth();
75
- else {
76
- this._syncClasses();
77
- // Collapsing flips whether the sidebar is width-capped, so re-apply.
209
+ if (name === 'breakpoint') {
210
+ this._syncBreakpoint();
211
+ return;
212
+ }
213
+ if (name === 'min-sidebar-width' || name === 'max-sidebar-width') {
78
214
  this._syncWidth();
215
+ return;
79
216
  }
217
+ if (name === 'collapsed') this._state.setCollapsed(this.hasAttribute('collapsed'));
218
+ if (name === 'show-content') this._state.setShowContent(this.hasAttribute('show-content'));
219
+ if (name === 'sidebar-position') this._state.setSidebarPosition(this.sidebarPosition);
220
+ this._syncClasses();
221
+ // Collapsing flips whether the sidebar is width-capped, so re-apply.
222
+ this._syncWidth();
80
223
  }
81
224
 
82
225
  private _syncWidth() {
@@ -113,9 +256,31 @@ export class AdwNavigationSplitView extends HTMLElement {
113
256
  return Number.isFinite(value) ? value : fallback;
114
257
  }
115
258
 
259
+ /** The reading direction `start` / `end` resolve against (`get_start_or_end`, :220-227). */
260
+ private get _direction(): AdwTextDirection {
261
+ return getComputedStyle(this).direction === 'rtl' ? 'rtl' : 'ltr';
262
+ }
263
+
116
264
  private _syncClasses() {
117
- this.classList.toggle('collapsed', this.collapsed);
118
- this.classList.toggle('show-content', this.showContent);
265
+ const state = this._state;
266
+ this.classList.toggle('collapsed', state.collapsed);
267
+ this.classList.toggle('show-content', state.showContent);
268
+ this.classList.toggle('sidebar-end', state.sidebarPosition === 'end');
269
+ // The VISUAL side, which under RTL is the opposite of the logical one —
270
+ // it decides where the pane is drawn AND which edge its divider is on.
271
+ this.classList.toggle(
272
+ 'sidebar-at-visual-start',
273
+ isSidebarAtVisualStart(state.sidebarPosition, this._direction),
274
+ );
275
+
276
+ // The STACK, not the two flags: `update_navigation_stack` (:342-405)
277
+ // keeps a LONE child visible whatever `show-content` says, and with
278
+ // `sidebar-position: end` the CONTENT is the root page — so the sidebar
279
+ // is PUSHED ON TOP of it rather than replacing it.
280
+ const { stack } = state.stack;
281
+ const visible = stack[stack.length - 1] ?? null;
282
+ if (this._sidebarEl) this._sidebarEl.dataset.paneVisible = String(!state.collapsed || visible === 'sidebar');
283
+ if (this._contentEl) this._contentEl.dataset.paneVisible = String(!state.collapsed || visible === 'content');
119
284
  }
120
285
 
121
286
  /** (Re)bind the `breakpoint` condition to `collapsed`. */