@gjsify/adwaita-web 0.13.2 → 0.15.1

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.
@@ -0,0 +1,174 @@
1
+ // <adw-view-stack> — A standalone, named-page CONTENT container: the web
2
+ // counterpart of Adw.ViewStack decoupled from any switcher. It holds a set of
3
+ // named pages and shows exactly one at a time; a paired <adw-view-switcher-bar>
4
+ // (or an adaptive header-bar switcher) binds to it and drives the selection.
5
+ // This is the piece the web package lacked — it already had switchers that own
6
+ // their own pages (<adw-view-switcher>, <adw-inline-view-switcher>) but no
7
+ // standalone stack a separate switcher can drive.
8
+ //
9
+ // Pages are declared as <adw-view-stack-page> children with `name`, `title` and
10
+ // `icon-name` attributes (their own children become the page body), OR added
11
+ // imperatively with `add(content, name, title?, icon?)`. The first page added
12
+ // is shown automatically.
13
+ //
14
+ // Attributes:
15
+ // visible-child-name — the name of the page to show. Reading/writing it stays
16
+ // in sync with the `visibleChildName` property so an adaptive layout can
17
+ // drive the stack declaratively (mirrors the Adw.ViewStack GObject prop).
18
+ // Properties (mirroring Adw.ViewStack):
19
+ // pages — readonly page descriptors ({ name, title, icon, element }).
20
+ // visibleChildName — name of the visible page ('' when empty).
21
+ // visibleChildIndex — zero-based index of the visible page (bounds-guarded).
22
+ // visibleChild — the visible page's content element (or null).
23
+ // Methods:
24
+ // add(content, name, title?, icon?) — append a page; returns its descriptor.
25
+ // addTitled(content, name, title) — Adw.ViewStack.add_titled convenience.
26
+ // Events:
27
+ // `notify::visible-child` (CustomEvent, bubbles, detail = { index, name, title })
28
+ // when the visible page CHANGES — mirrors Adw.ViewStack's visible-child
29
+ // notify. It does NOT fire for the initial/auto-shown page or on add().
30
+ // Reference: refs/libadwaita/src/adw-view-stack.c (AdwViewStack behaviour)
31
+ // Reference: packages/nativescript-bridge/adwaita/src/widgets/adw-view-stack.ts (NS twin)
32
+ // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
33
+ // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
34
+
35
+ /** A page descriptor exposed via `pages` (consumed by a bound switcher bar). */
36
+ export interface AdwViewStackPageInfo {
37
+ /** Stable id used by visibleChildName + switcher binding. */
38
+ name: string;
39
+ /** Human-facing title shown by a bound switcher. */
40
+ title: string;
41
+ /** Symbolic icon name (no `-symbolic` suffix), or '' for none. */
42
+ icon: string;
43
+ /** The page's content container (the visibility-toggled node). */
44
+ element: HTMLElement;
45
+ }
46
+
47
+ export class AdwViewStack extends HTMLElement {
48
+ private _pages: AdwViewStackPageInfo[] = [];
49
+ private _visibleIndex = 0;
50
+ private _initialized = false;
51
+
52
+ static get observedAttributes() {
53
+ return ['visible-child-name'];
54
+ }
55
+
56
+ /** All pages in declaration/add order. A bound switcher reads this. */
57
+ get pages(): readonly AdwViewStackPageInfo[] {
58
+ return this._pages;
59
+ }
60
+
61
+ /** Zero-based index of the visible page. Out-of-range/same values no-op. */
62
+ get visibleChildIndex(): number {
63
+ return this._visibleIndex;
64
+ }
65
+
66
+ set visibleChildIndex(value: number) {
67
+ if (!Number.isFinite(value) || value < 0 || value >= this._pages.length) return;
68
+ if (value === this._visibleIndex) return;
69
+ this._visibleIndex = value;
70
+ this._apply(true);
71
+ }
72
+
73
+ /** Name of the visible page, or '' when the stack is empty. */
74
+ get visibleChildName(): string {
75
+ return this._pages[this._visibleIndex]?.name ?? '';
76
+ }
77
+
78
+ set visibleChildName(name: string) {
79
+ const index = this._pages.findIndex((p) => p.name === name);
80
+ if (index >= 0) this.visibleChildIndex = index;
81
+ }
82
+
83
+ /** The visible page's content element, or null when empty. */
84
+ get visibleChild(): HTMLElement | null {
85
+ return this._pages[this._visibleIndex]?.element ?? null;
86
+ }
87
+
88
+ connectedCallback() {
89
+ if (this._initialized) return;
90
+ this._initialized = true;
91
+
92
+ // Snapshot the declared <adw-view-stack-page> children before we take
93
+ // over the subtree — their name / title / icon-name + child nodes become
94
+ // the pages. Wrapping each page's content lets us toggle exactly one.
95
+ const declared = Array.from(this.querySelectorAll(':scope > adw-view-stack-page')) as HTMLElement[];
96
+ const wrappers = declared.map((pageEl, index) => {
97
+ const name = pageEl.getAttribute('name') ?? `page-${index}`;
98
+ const title = pageEl.getAttribute('title') ?? name;
99
+ const icon = (pageEl.getAttribute('icon-name') ?? '').replace(/-symbolic$/, '');
100
+ const element = document.createElement('div');
101
+ element.className = 'adw-view-stack-child';
102
+ element.dataset.name = name;
103
+ for (const child of Array.from(pageEl.childNodes)) element.appendChild(child);
104
+ return { name, title, icon, element };
105
+ });
106
+
107
+ this._pages = wrappers;
108
+ this.replaceChildren(...wrappers.map((p) => p.element));
109
+
110
+ // A declared `visible-child-name` selects the initial page; otherwise the
111
+ // first page is auto-visible (matching Adw.ViewStack + the NS twin).
112
+ const initialName = this.getAttribute('visible-child-name');
113
+ const initialIndex = initialName ? this._pages.findIndex((p) => p.name === initialName) : -1;
114
+ this._visibleIndex = initialIndex >= 0 ? initialIndex : 0;
115
+ this._apply(false);
116
+ }
117
+
118
+ attributeChangedCallback(name: string, _old: string | null, value: string | null) {
119
+ if (!this._initialized) return;
120
+ if (name === 'visible-child-name' && value !== null && value !== this.visibleChildName) {
121
+ this.visibleChildName = value;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Append a page. The FIRST page added becomes visible automatically; later
127
+ * pages are added hidden. Mirrors Adw.ViewStack.add()/the NS twin's add().
128
+ */
129
+ add(content: HTMLElement, name: string, title?: string, icon?: string): AdwViewStackPageInfo {
130
+ content.classList.add('adw-view-stack-child');
131
+ content.dataset.name = name;
132
+ const page: AdwViewStackPageInfo = {
133
+ name,
134
+ title: title ?? name,
135
+ icon: (icon ?? '').replace(/-symbolic$/, ''),
136
+ element: content,
137
+ };
138
+ this._pages.push(page);
139
+ this.appendChild(content);
140
+ // Keep exactly one page shown without emitting (add is not a user change).
141
+ this._apply(false);
142
+ return page;
143
+ }
144
+
145
+ /** Convenience alias mirroring Adw.ViewStack.add_titled() (no icon). */
146
+ addTitled(content: HTMLElement, name: string, title: string): AdwViewStackPageInfo {
147
+ return this.add(content, name, title);
148
+ }
149
+
150
+ private _apply(emit: boolean): void {
151
+ this._pages.forEach((page, index) => {
152
+ const isVisible = index === this._visibleIndex;
153
+ page.element.classList.toggle('active-view', isVisible);
154
+ page.element.hidden = !isVisible;
155
+ });
156
+ // Reflect the current selection to the attribute (without re-entering the
157
+ // guarded callback — the value already matches, so it no-ops).
158
+ const current = this.visibleChildName;
159
+ if (current && this.getAttribute('visible-child-name') !== current) {
160
+ this.setAttribute('visible-child-name', current);
161
+ }
162
+ if (emit) {
163
+ const page = this._pages[this._visibleIndex];
164
+ this.dispatchEvent(
165
+ new CustomEvent('notify::visible-child', {
166
+ bubbles: true,
167
+ detail: { index: this._visibleIndex, name: page?.name ?? '', title: page?.title ?? '' },
168
+ }),
169
+ );
170
+ }
171
+ }
172
+ }
173
+
174
+ customElements.define('adw-view-stack', AdwViewStack);
@@ -0,0 +1,168 @@
1
+ // <adw-view-switcher-bar> — The narrow BOTTOM bar of icon-over-label buttons
2
+ // bound to an <adw-view-stack>, the web counterpart of Adw.ViewSwitcherBar. It
3
+ // owns no pages of its own: it reads the bound stack's `pages`, renders one
4
+ // homogeneous icon-over-label button per page, and is two-way synced to the
5
+ // stack's visible child — tapping a button sets the stack's visible page, and a
6
+ // change to the stack (from anywhere) re-highlights the matching button. The
7
+ // active button's label is accent-coloured (via the `.active` CSS class).
8
+ //
9
+ // A `revealed` flag lets an adaptive layout flip the bar on/off: shown on narrow
10
+ // screens, hidden on wide ones where a header-bar <adw-view-switcher> takes over
11
+ // (mirrors Adw.ViewSwitcherBar `reveal` driven by an Adw.Breakpoint).
12
+ //
13
+ // Attributes:
14
+ // stack — the id of the <adw-view-stack> to drive (like <label for>).
15
+ // revealed — boolean; when absent the bar is hidden (matches `reveal`).
16
+ // Properties (mirroring Adw.ViewSwitcherBar):
17
+ // stack — the bound AdwViewStack element (get/set; set also accepts an id).
18
+ // revealed — whether the bar is shown (get/set).
19
+ // Methods:
20
+ // setStack(stack) — bind to a stack element (or null); rebuilds the buttons.
21
+ // refresh() — rebuild the buttons after late page adds to the stack.
22
+ // Reference: refs/libadwaita/src/adw-view-switcher-bar.c (AdwViewSwitcherBar)
23
+ // Reference: refs/libadwaita/src/stylesheet/widgets/_view-switcher.scss (.narrow policy)
24
+ // Reference: packages/nativescript-bridge/adwaita/src/widgets/adw-view-switcher-bar.ts (NS twin)
25
+ // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
26
+ // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
27
+
28
+ import type { AdwViewStack } from './adw-view-stack.js';
29
+
30
+ export class AdwViewSwitcherBar extends HTMLElement {
31
+ private _barEl!: HTMLDivElement;
32
+ private _buttons: HTMLButtonElement[] = [];
33
+ private _stack: AdwViewStack | null = null;
34
+ private _revealed = false;
35
+ private _initialized = false;
36
+ private _onStackNotify = (): void => this._syncActive();
37
+
38
+ static get observedAttributes() {
39
+ return ['stack', 'revealed'];
40
+ }
41
+
42
+ /** The bound stack element, or null when unbound. */
43
+ get stack(): AdwViewStack | null {
44
+ return this._stack;
45
+ }
46
+
47
+ set stack(value: AdwViewStack | string | null) {
48
+ this.setStack(this._resolveStack(value));
49
+ }
50
+
51
+ /** Whether the bar is revealed (shown). Hidden when false. */
52
+ get revealed(): boolean {
53
+ return this._revealed;
54
+ }
55
+
56
+ set revealed(value: boolean) {
57
+ if (value) this.setAttribute('revealed', '');
58
+ else this.removeAttribute('revealed');
59
+ }
60
+
61
+ connectedCallback() {
62
+ if (!this._initialized) {
63
+ this._initialized = true;
64
+ this._barEl = document.createElement('div');
65
+ this._barEl.className = 'adw-view-switcher-bar-box';
66
+ this._barEl.setAttribute('role', 'tablist');
67
+ this.replaceChildren(this._barEl);
68
+ }
69
+ this._revealed = this.hasAttribute('revealed');
70
+ this._applyRevealed();
71
+ // Resolve the `stack` id ref now that the element is in the document — the
72
+ // referenced stack is normally earlier in the DOM (content above a bottom
73
+ // bar), so its pages are ready.
74
+ const ref = this.getAttribute('stack');
75
+ if (ref && !this._stack) this.setStack(this._resolveStack(ref));
76
+ else this._syncActive();
77
+ }
78
+
79
+ disconnectedCallback() {
80
+ if (this._stack) this._stack.removeEventListener('notify::visible-child', this._onStackNotify);
81
+ }
82
+
83
+ attributeChangedCallback(name: string, _old: string | null, value: string | null) {
84
+ if (!this._initialized) return;
85
+ if (name === 'revealed') {
86
+ this._revealed = this.hasAttribute('revealed');
87
+ this._applyRevealed();
88
+ return;
89
+ }
90
+ if (name === 'stack') {
91
+ this.setStack(this._resolveStack(value));
92
+ }
93
+ }
94
+
95
+ /** Bind to a stack element (or null). Unbinds the previous one and rebuilds. */
96
+ setStack(stack: AdwViewStack | null): void {
97
+ if (this._stack) this._stack.removeEventListener('notify::visible-child', this._onStackNotify);
98
+ this._stack = stack;
99
+ if (stack) stack.addEventListener('notify::visible-child', this._onStackNotify);
100
+ this._rebuild();
101
+ }
102
+
103
+ /** Rebuild the buttons — call after adding pages to the bound stack. */
104
+ refresh(): void {
105
+ this._rebuild();
106
+ }
107
+
108
+ private _resolveStack(value: AdwViewStack | string | null): AdwViewStack | null {
109
+ if (value === null) return null;
110
+ if (typeof value === 'string') {
111
+ return (document.getElementById(value) as AdwViewStack | null) ?? null;
112
+ }
113
+ return value;
114
+ }
115
+
116
+ private _rebuild(): void {
117
+ if (!this._initialized) return;
118
+ this._barEl.replaceChildren();
119
+ this._buttons = [];
120
+ const pages = this._stack?.pages ?? [];
121
+
122
+ pages.forEach((page, index) => {
123
+ const button = document.createElement('button');
124
+ button.type = 'button';
125
+ button.className = 'adw-view-switcher-bar-button';
126
+ button.setAttribute('role', 'tab');
127
+
128
+ const iconEl = document.createElement('span');
129
+ iconEl.className = `adw-icon adw-view-switcher-bar-icon${page.icon ? ` adw-icon--${page.icon}` : ''}`;
130
+ iconEl.setAttribute('aria-hidden', 'true'); // decorative (mask-image only)
131
+ iconEl.hidden = page.icon.length === 0;
132
+
133
+ const labelEl = document.createElement('span');
134
+ labelEl.className = 'adw-view-switcher-bar-label';
135
+ labelEl.textContent = page.title;
136
+ labelEl.hidden = page.title.length === 0;
137
+
138
+ button.append(iconEl, labelEl);
139
+ // An icon-only button (no title) needs an accessible name.
140
+ if (page.icon && !page.title) button.setAttribute('aria-label', page.icon);
141
+
142
+ button.addEventListener('click', () => {
143
+ if (this._stack) this._stack.visibleChildIndex = index;
144
+ });
145
+
146
+ this._barEl.appendChild(button);
147
+ this._buttons.push(button);
148
+ });
149
+
150
+ this._syncActive();
151
+ }
152
+
153
+ private _syncActive(): void {
154
+ const selected = this._stack?.visibleChildIndex ?? -1;
155
+ this._buttons.forEach((button, index) => {
156
+ const isActive = index === selected;
157
+ button.classList.toggle('active', isActive);
158
+ button.setAttribute('aria-selected', String(isActive));
159
+ button.tabIndex = isActive ? 0 : -1;
160
+ });
161
+ }
162
+
163
+ private _applyRevealed(): void {
164
+ this.hidden = !this._revealed;
165
+ }
166
+ }
167
+
168
+ customElements.define('adw-view-switcher-bar', AdwViewSwitcherBar);
package/src/index.ts CHANGED
@@ -53,6 +53,11 @@ export { AdwCarousel, AdwCarouselIndicatorDots, AdwCarouselIndicatorLines } from
53
53
  export { AdwTabPage, AdwTabView } from './elements/adw-tab-view.js';
54
54
  export { AdwViewSwitcher, AdwViewSwitcherPage } from './elements/adw-view-switcher.js';
55
55
  export { AdwInlineViewSwitcher, AdwViewStackPage } from './elements/adw-inline-view-switcher.js';
56
+ export { AdwViewStack } from './elements/adw-view-stack.js';
57
+ export type { AdwViewStackPageInfo } from './elements/adw-view-stack.js';
58
+ export { AdwViewSwitcherBar } from './elements/adw-view-switcher-bar.js';
59
+ export { AdwMenuButton } from './elements/adw-menu-button.js';
60
+ export type { AdwMenuItem } from './elements/adw-menu-button.js';
56
61
  export { AdwNavigationPage, AdwNavigationView } from './elements/adw-navigation-view.js';
57
62
  export { AdwBottomSheet, AdwBottomSheetContent, AdwBottomSheetSheet } from './elements/adw-bottom-sheet.js';
58
63
  export { AdwSidebar, AdwSidebarItem, AdwSidebarSection } from './elements/adw-sidebar.js';