@gjsify/adwaita-web 0.1.3
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/lib/types/adwaita-css.d.ts +1 -0
- package/lib/types/elements/adw-combo-row.d.ts +11 -0
- package/lib/types/elements/adw-header-bar.d.ts +10 -0
- package/lib/types/elements/adw-overlay-split-view.d.ts +21 -0
- package/lib/types/elements/adw-preferences-group.d.ts +4 -0
- package/lib/types/elements/adw-spin-row.d.ts +17 -0
- package/lib/types/elements/adw-switch-row.d.ts +9 -0
- package/lib/types/elements/adw-toast-overlay.d.ts +8 -0
- package/lib/types/elements/adw-window.d.ts +3 -0
- package/lib/types/index.d.ts +9 -0
- package/package.json +29 -0
- package/src/adwaita-css.ts +592 -0
- package/src/elements/adw-combo-row.ts +81 -0
- package/src/elements/adw-header-bar.ts +50 -0
- package/src/elements/adw-overlay-split-view.ts +135 -0
- package/src/elements/adw-preferences-group.ts +34 -0
- package/src/elements/adw-spin-row.ts +123 -0
- package/src/elements/adw-switch-row.ts +69 -0
- package/src/elements/adw-toast-overlay.ts +43 -0
- package/src/elements/adw-window.ts +18 -0
- package/src/index.ts +23 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const adwaitaCSS: string;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare class AdwComboRow extends HTMLElement {
|
|
2
|
+
private _select;
|
|
3
|
+
private _valueEl;
|
|
4
|
+
private _items;
|
|
5
|
+
private _initialized;
|
|
6
|
+
static get observedAttributes(): string[];
|
|
7
|
+
get selected(): number;
|
|
8
|
+
set selected(value: number);
|
|
9
|
+
connectedCallback(): void;
|
|
10
|
+
attributeChangedCallback(name: string, _old: string | null, value: string | null): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare class AdwHeaderBar extends HTMLElement {
|
|
2
|
+
private _initialized;
|
|
3
|
+
private _startEl;
|
|
4
|
+
private _endEl;
|
|
5
|
+
/** The start (left) section container — append buttons/widgets here. */
|
|
6
|
+
get startSection(): HTMLDivElement | null;
|
|
7
|
+
/** The end (right) section container — append buttons/widgets here. */
|
|
8
|
+
get endSection(): HTMLDivElement | null;
|
|
9
|
+
connectedCallback(): void;
|
|
10
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare class AdwOverlaySplitView extends HTMLElement {
|
|
2
|
+
private _initialized;
|
|
3
|
+
private _sidebarEl;
|
|
4
|
+
private _contentEl;
|
|
5
|
+
private _backdropEl;
|
|
6
|
+
static get observedAttributes(): string[];
|
|
7
|
+
get showSidebar(): boolean;
|
|
8
|
+
set showSidebar(v: boolean);
|
|
9
|
+
get collapsed(): boolean;
|
|
10
|
+
set collapsed(v: boolean);
|
|
11
|
+
get minSidebarWidth(): number;
|
|
12
|
+
get maxSidebarWidth(): number;
|
|
13
|
+
get sidebarWidthFraction(): number;
|
|
14
|
+
connectedCallback(): void;
|
|
15
|
+
attributeChangedCallback(_name: string, _old: string | null, _val: string | null): void;
|
|
16
|
+
openSidebar(): void;
|
|
17
|
+
hideSidebar(): void;
|
|
18
|
+
toggleSidebar(): void;
|
|
19
|
+
private _syncClasses;
|
|
20
|
+
private _syncSidebarWidth;
|
|
21
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare class AdwSpinRow extends HTMLElement {
|
|
2
|
+
private _input;
|
|
3
|
+
private _min;
|
|
4
|
+
private _max;
|
|
5
|
+
private _step;
|
|
6
|
+
private _value;
|
|
7
|
+
private _initialized;
|
|
8
|
+
static get observedAttributes(): string[];
|
|
9
|
+
get value(): number;
|
|
10
|
+
set value(v: number);
|
|
11
|
+
connectedCallback(): void;
|
|
12
|
+
attributeChangedCallback(name: string, _old: string | null, val: string | null): void;
|
|
13
|
+
private _adjust;
|
|
14
|
+
private _emitChange;
|
|
15
|
+
private _countDecimals;
|
|
16
|
+
private _formatValue;
|
|
17
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare class AdwSwitchRow extends HTMLElement {
|
|
2
|
+
private _checkbox;
|
|
3
|
+
private _initialized;
|
|
4
|
+
static get observedAttributes(): string[];
|
|
5
|
+
get active(): boolean;
|
|
6
|
+
set active(value: boolean);
|
|
7
|
+
connectedCallback(): void;
|
|
8
|
+
attributeChangedCallback(name: string, _old: string | null, value: string | null): void;
|
|
9
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare class AdwToastOverlay extends HTMLElement {
|
|
2
|
+
/**
|
|
3
|
+
* Show a toast notification.
|
|
4
|
+
* @param title - The text to display.
|
|
5
|
+
* @param timeout - Time in ms before the toast auto-dismisses (default 2000).
|
|
6
|
+
*/
|
|
7
|
+
addToast(title: string, timeout?: number): void;
|
|
8
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import '@gjsify/adwaita-fonts';
|
|
2
|
+
export { AdwWindow } from './elements/adw-window.js';
|
|
3
|
+
export { AdwHeaderBar } from './elements/adw-header-bar.js';
|
|
4
|
+
export { AdwPreferencesGroup } from './elements/adw-preferences-group.js';
|
|
5
|
+
export { AdwSwitchRow } from './elements/adw-switch-row.js';
|
|
6
|
+
export { AdwComboRow } from './elements/adw-combo-row.js';
|
|
7
|
+
export { AdwSpinRow } from './elements/adw-spin-row.js';
|
|
8
|
+
export { AdwToastOverlay } from './elements/adw-toast-overlay.js';
|
|
9
|
+
export { AdwOverlaySplitView } from './elements/adw-overlay-split-view.js';
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gjsify/adwaita-web",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Adwaita/Libadwaita web components for browser targets",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.ts",
|
|
11
|
+
"default": "./src/index.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"clear": "rm -rf lib tsconfig.tsbuildinfo || exit 0",
|
|
16
|
+
"check": "tsc --noEmit",
|
|
17
|
+
"build": "yarn build:gjsify && yarn build:types",
|
|
18
|
+
"build:gjsify": "gjsify build --library 'src/**/*.ts' --exclude 'src/**/*.spec.ts'",
|
|
19
|
+
"build:types": "tsc"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@gjsify/adwaita-fonts": "^0.1.3",
|
|
23
|
+
"@gjsify/adwaita-icons": "^0.1.3"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@gjsify/cli": "^0.1.3",
|
|
27
|
+
"typescript": "^6.0.2"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
// Adwaita CSS subset for browser targets.
|
|
2
|
+
// Colors from refs/libadwaita/src/stylesheet/_colors.scss (canonical).
|
|
3
|
+
// Component styles adapted from Adwaita Web UI Framework (https://github.com/mclellac/adwaita-web).
|
|
4
|
+
// Copyright (c) GNOME contributors (libadwaita), LGPLv2.1+.
|
|
5
|
+
// Copyright (c) 2025 csm (adwaita-web). MIT License.
|
|
6
|
+
// Modifications: Extracted as CSS-in-JS for @gjsify/adwaita-web.
|
|
7
|
+
|
|
8
|
+
import { editPasteSymbolic, goDownSymbolic, sidebarShowSymbolic } from '@gjsify/adwaita-icons/actions';
|
|
9
|
+
import { toDataUri } from '@gjsify/adwaita-icons/utils';
|
|
10
|
+
|
|
11
|
+
const goDownDataUri = toDataUri(goDownSymbolic);
|
|
12
|
+
const editPasteDataUri = toDataUri(editPasteSymbolic);
|
|
13
|
+
const sidebarShowDataUri = toDataUri(sidebarShowSymbolic);
|
|
14
|
+
|
|
15
|
+
export const adwaitaCSS = `
|
|
16
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
17
|
+
Adwaita CSS Custom Properties — Light Theme (default)
|
|
18
|
+
═══════════════════════════════════════════════════════════════ */
|
|
19
|
+
:root {
|
|
20
|
+
/* Icons (Adwaita symbolic, encoded as data-URIs) */
|
|
21
|
+
--icon-edit-paste: ${editPasteDataUri};
|
|
22
|
+
--icon-go-down: ${goDownDataUri};
|
|
23
|
+
--icon-sidebar-show: ${sidebarShowDataUri};
|
|
24
|
+
|
|
25
|
+
/* Window */
|
|
26
|
+
--window-bg-color: #fafafb;
|
|
27
|
+
--window-fg-color: rgba(0, 0, 6, 0.8);
|
|
28
|
+
|
|
29
|
+
/* Views */
|
|
30
|
+
--view-bg-color: #ffffff;
|
|
31
|
+
--view-fg-color: rgba(0, 0, 6, 0.8);
|
|
32
|
+
|
|
33
|
+
/* Header bar */
|
|
34
|
+
--headerbar-bg-color: #ffffff;
|
|
35
|
+
--headerbar-fg-color: rgba(0, 0, 6, 0.8);
|
|
36
|
+
--headerbar-shade-color: rgba(0, 0, 6, 0.12);
|
|
37
|
+
|
|
38
|
+
/* Sidebar */
|
|
39
|
+
--sidebar-bg-color: #ebebed;
|
|
40
|
+
|
|
41
|
+
/* Cards / boxed lists */
|
|
42
|
+
--card-bg-color: #ffffff;
|
|
43
|
+
--card-fg-color: rgba(0, 0, 6, 0.8);
|
|
44
|
+
--card-shade-color: rgba(0, 0, 6, 0.07);
|
|
45
|
+
|
|
46
|
+
/* Accent */
|
|
47
|
+
--accent-bg-color: #3584e4;
|
|
48
|
+
--accent-fg-color: #ffffff;
|
|
49
|
+
--accent-color: #1c71d8;
|
|
50
|
+
|
|
51
|
+
/* Switch */
|
|
52
|
+
--switch-off-bg: rgba(0, 0, 0, 0.2);
|
|
53
|
+
--switch-knob-bg: #ffffff;
|
|
54
|
+
|
|
55
|
+
/* Layout */
|
|
56
|
+
--window-radius: 15px;
|
|
57
|
+
--card-radius: 12px;
|
|
58
|
+
--button-radius: 9px;
|
|
59
|
+
|
|
60
|
+
/* Spacing */
|
|
61
|
+
--spacing-xs: 6px;
|
|
62
|
+
--spacing-s: 9px;
|
|
63
|
+
--spacing-m: 12px;
|
|
64
|
+
--spacing-l: 18px;
|
|
65
|
+
--spacing-xl: 24px;
|
|
66
|
+
|
|
67
|
+
/* Typography — GNOME default: Adwaita Sans 11 */
|
|
68
|
+
--font-family: 'Adwaita Sans', 'Cantarell', 'Inter', 'Segoe UI', sans-serif;
|
|
69
|
+
--font-size-base: 11pt;
|
|
70
|
+
--font-size-small: 9pt;
|
|
71
|
+
--font-size-heading: 12pt;
|
|
72
|
+
--dim-opacity: 0.55;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
76
|
+
Dark Theme — auto via prefers-color-scheme
|
|
77
|
+
═══════════════════════════════════════════════════════════════ */
|
|
78
|
+
@media (prefers-color-scheme: dark) {
|
|
79
|
+
:root:not(.theme-light) {
|
|
80
|
+
--window-bg-color: #222226;
|
|
81
|
+
--window-fg-color: #ffffff;
|
|
82
|
+
--view-bg-color: #1d1d20;
|
|
83
|
+
--view-fg-color: #ffffff;
|
|
84
|
+
--headerbar-bg-color: #2e2e32;
|
|
85
|
+
--headerbar-fg-color: #ffffff;
|
|
86
|
+
--headerbar-shade-color: rgba(0, 0, 6, 0.36);
|
|
87
|
+
--sidebar-bg-color: #2e2e32;
|
|
88
|
+
--card-bg-color: rgba(255, 255, 255, 0.08);
|
|
89
|
+
--card-fg-color: #ffffff;
|
|
90
|
+
--card-shade-color: rgba(0, 0, 6, 0.36);
|
|
91
|
+
--accent-color: #78aeed;
|
|
92
|
+
--switch-off-bg: rgba(255, 255, 255, 0.2);
|
|
93
|
+
--switch-knob-bg: #deddda;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* Dark theme manual override */
|
|
98
|
+
:root.theme-dark {
|
|
99
|
+
--window-bg-color: #222226;
|
|
100
|
+
--window-fg-color: #ffffff;
|
|
101
|
+
--view-bg-color: #1d1d20;
|
|
102
|
+
--view-fg-color: #ffffff;
|
|
103
|
+
--headerbar-bg-color: #2e2e32;
|
|
104
|
+
--headerbar-fg-color: #ffffff;
|
|
105
|
+
--headerbar-shade-color: rgba(0, 0, 6, 0.36);
|
|
106
|
+
--sidebar-bg-color: #2e2e32;
|
|
107
|
+
--card-bg-color: rgba(255, 255, 255, 0.08);
|
|
108
|
+
--card-fg-color: #ffffff;
|
|
109
|
+
--card-shade-color: rgba(0, 0, 6, 0.36);
|
|
110
|
+
--accent-color: #78aeed;
|
|
111
|
+
--switch-off-bg: rgba(255, 255, 255, 0.2);
|
|
112
|
+
--switch-knob-bg: #deddda;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
116
|
+
Component Styles
|
|
117
|
+
═══════════════════════════════════════════════════════════════ */
|
|
118
|
+
|
|
119
|
+
/* --- adw-window --- */
|
|
120
|
+
adw-window {
|
|
121
|
+
display: flex;
|
|
122
|
+
flex-direction: column;
|
|
123
|
+
background-color: var(--window-bg-color);
|
|
124
|
+
color: var(--window-fg-color);
|
|
125
|
+
font-family: var(--font-family);
|
|
126
|
+
font-size: var(--font-size-base);
|
|
127
|
+
border-radius: var(--window-radius);
|
|
128
|
+
box-shadow:
|
|
129
|
+
0 0 14px 5px rgb(0 0 0 / 15%),
|
|
130
|
+
0 0 5px 2px rgb(0 0 0 / 10%),
|
|
131
|
+
0 0 0 1px rgb(0 0 0 / 5%);
|
|
132
|
+
overflow: hidden;
|
|
133
|
+
outline: 1px solid rgb(255 255 255 / 7%);
|
|
134
|
+
outline-offset: -1px;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* --- adw-header-bar --- */
|
|
138
|
+
adw-header-bar {
|
|
139
|
+
display: flex;
|
|
140
|
+
align-items: center;
|
|
141
|
+
min-height: 47px;
|
|
142
|
+
padding: 0 6px;
|
|
143
|
+
background-color: var(--headerbar-bg-color);
|
|
144
|
+
color: var(--headerbar-fg-color);
|
|
145
|
+
box-shadow: inset 0 -1px var(--headerbar-shade-color);
|
|
146
|
+
flex-shrink: 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
adw-header-bar .adw-header-bar-center {
|
|
150
|
+
flex: 1;
|
|
151
|
+
display: flex;
|
|
152
|
+
justify-content: center;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
adw-header-bar .adw-header-bar-title {
|
|
156
|
+
font-weight: bold;
|
|
157
|
+
font-size: var(--font-size-base);
|
|
158
|
+
white-space: nowrap;
|
|
159
|
+
overflow: hidden;
|
|
160
|
+
text-overflow: ellipsis;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
adw-header-bar .adw-header-bar-start {
|
|
164
|
+
display: flex;
|
|
165
|
+
align-items: center;
|
|
166
|
+
padding: 0 6px;
|
|
167
|
+
gap: 4px;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
adw-header-bar .adw-header-bar-end {
|
|
171
|
+
display: flex;
|
|
172
|
+
align-items: center;
|
|
173
|
+
padding: 0 6px;
|
|
174
|
+
gap: 4px;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
adw-header-bar .adw-header-btn {
|
|
178
|
+
position: relative;
|
|
179
|
+
width: 34px;
|
|
180
|
+
height: 34px;
|
|
181
|
+
border: none;
|
|
182
|
+
border-radius: var(--button-radius);
|
|
183
|
+
background: transparent;
|
|
184
|
+
color: var(--headerbar-fg-color);
|
|
185
|
+
cursor: pointer;
|
|
186
|
+
padding: 0;
|
|
187
|
+
margin: 0;
|
|
188
|
+
flex-shrink: 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
adw-header-bar .adw-header-btn:hover {
|
|
192
|
+
background: rgba(128, 128, 128, 0.12);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
adw-header-bar .adw-header-btn:active {
|
|
196
|
+
background: rgba(128, 128, 128, 0.22);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* --- adw-preferences-group --- */
|
|
200
|
+
adw-preferences-group {
|
|
201
|
+
display: block;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
adw-preferences-group .adw-preferences-group-header {
|
|
205
|
+
padding: var(--spacing-xs) 2px;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
adw-preferences-group .adw-preferences-group-title {
|
|
209
|
+
font-size: var(--font-size-small);
|
|
210
|
+
font-weight: 700;
|
|
211
|
+
text-transform: uppercase;
|
|
212
|
+
letter-spacing: 0.5px;
|
|
213
|
+
opacity: var(--dim-opacity);
|
|
214
|
+
margin: 0;
|
|
215
|
+
color: var(--window-fg-color);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
adw-preferences-group .adw-preferences-group-listbox {
|
|
219
|
+
background-color: var(--card-bg-color);
|
|
220
|
+
color: var(--card-fg-color);
|
|
221
|
+
border-radius: var(--card-radius);
|
|
222
|
+
box-shadow:
|
|
223
|
+
0 0 0 1px rgb(0 0 6 / 3%),
|
|
224
|
+
0 1px 3px 1px rgb(0 0 6 / 7%),
|
|
225
|
+
0 2px 6px 2px rgb(0 0 6 / 3%);
|
|
226
|
+
overflow: hidden;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* --- Row base (shared by switch-row, combo-row) --- */
|
|
230
|
+
adw-switch-row,
|
|
231
|
+
adw-combo-row {
|
|
232
|
+
display: flex;
|
|
233
|
+
align-items: center;
|
|
234
|
+
justify-content: space-between;
|
|
235
|
+
min-height: 50px;
|
|
236
|
+
padding: var(--spacing-s) var(--spacing-m);
|
|
237
|
+
gap: var(--spacing-m);
|
|
238
|
+
border-bottom: 1px solid var(--card-shade-color);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
adw-switch-row:last-child,
|
|
242
|
+
adw-combo-row:last-child {
|
|
243
|
+
border-bottom: none;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/* --- adw-switch-row --- */
|
|
247
|
+
adw-switch-row .adw-row-title {
|
|
248
|
+
flex: 1;
|
|
249
|
+
font-size: var(--font-size-base);
|
|
250
|
+
color: var(--card-fg-color);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
adw-switch-row .adw-switch {
|
|
254
|
+
position: relative;
|
|
255
|
+
display: inline-block;
|
|
256
|
+
width: 44px;
|
|
257
|
+
height: 24px;
|
|
258
|
+
flex-shrink: 0;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
adw-switch-row .adw-switch input {
|
|
262
|
+
opacity: 0;
|
|
263
|
+
width: 0;
|
|
264
|
+
height: 0;
|
|
265
|
+
position: absolute;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
adw-switch-row .adw-switch-slider {
|
|
269
|
+
position: absolute;
|
|
270
|
+
cursor: pointer;
|
|
271
|
+
inset: 0;
|
|
272
|
+
background-color: var(--switch-off-bg);
|
|
273
|
+
border-radius: 12px;
|
|
274
|
+
transition: background-color 0.15s ease-out;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
adw-switch-row .adw-switch-slider::before {
|
|
278
|
+
content: "";
|
|
279
|
+
position: absolute;
|
|
280
|
+
height: 20px;
|
|
281
|
+
width: 20px;
|
|
282
|
+
left: 2px;
|
|
283
|
+
bottom: 2px;
|
|
284
|
+
background-color: var(--switch-knob-bg);
|
|
285
|
+
border-radius: 50%;
|
|
286
|
+
transition: transform 0.15s ease-out;
|
|
287
|
+
box-shadow: 0 1px 2px rgb(0 0 0 / 20%);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
adw-switch-row .adw-switch input:checked + .adw-switch-slider {
|
|
291
|
+
background-color: var(--accent-bg-color);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
adw-switch-row .adw-switch input:checked + .adw-switch-slider::before {
|
|
295
|
+
transform: translateX(20px);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
adw-switch-row .adw-switch input:focus-visible + .adw-switch-slider {
|
|
299
|
+
outline: 2px solid var(--accent-color);
|
|
300
|
+
outline-offset: 2px;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/* --- adw-combo-row --- */
|
|
304
|
+
adw-combo-row {
|
|
305
|
+
position: relative;
|
|
306
|
+
cursor: pointer;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
adw-combo-row .adw-row-title {
|
|
310
|
+
font-size: var(--font-size-base);
|
|
311
|
+
color: var(--card-fg-color);
|
|
312
|
+
pointer-events: none;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
adw-combo-row select {
|
|
316
|
+
appearance: none;
|
|
317
|
+
-webkit-appearance: none;
|
|
318
|
+
background-color: transparent;
|
|
319
|
+
border: none;
|
|
320
|
+
padding: 6px 24px 6px 8px;
|
|
321
|
+
font-family: var(--font-family);
|
|
322
|
+
font-size: var(--font-size-base);
|
|
323
|
+
color: var(--card-fg-color);
|
|
324
|
+
cursor: pointer;
|
|
325
|
+
text-align: right;
|
|
326
|
+
background-image: ${goDownDataUri};
|
|
327
|
+
background-repeat: no-repeat;
|
|
328
|
+
background-position: right 4px center;
|
|
329
|
+
background-size: 12px 12px;
|
|
330
|
+
/* Stretch select over the entire row to make it clickable anywhere */
|
|
331
|
+
position: absolute;
|
|
332
|
+
inset: 0;
|
|
333
|
+
opacity: 0;
|
|
334
|
+
width: 100%;
|
|
335
|
+
height: 100%;
|
|
336
|
+
z-index: 1;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
adw-combo-row .adw-row-value {
|
|
340
|
+
font-size: var(--font-size-base);
|
|
341
|
+
color: var(--card-fg-color);
|
|
342
|
+
opacity: var(--dim-opacity);
|
|
343
|
+
display: flex;
|
|
344
|
+
align-items: center;
|
|
345
|
+
gap: 4px;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
adw-combo-row .adw-row-value::after {
|
|
349
|
+
content: "";
|
|
350
|
+
display: inline-block;
|
|
351
|
+
width: 12px;
|
|
352
|
+
height: 12px;
|
|
353
|
+
background-image: ${goDownDataUri};
|
|
354
|
+
background-repeat: no-repeat;
|
|
355
|
+
background-size: contain;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
adw-combo-row select:focus-visible {
|
|
359
|
+
outline: 2px solid var(--accent-color);
|
|
360
|
+
outline-offset: -1px;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
adw-combo-row select option {
|
|
364
|
+
background-color: var(--window-bg-color);
|
|
365
|
+
color: var(--window-fg-color);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/* --- Utility: separator --- */
|
|
369
|
+
.adw-separator-vertical {
|
|
370
|
+
width: 1px;
|
|
371
|
+
align-self: stretch;
|
|
372
|
+
background-color: var(--card-shade-color);
|
|
373
|
+
flex-shrink: 0;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/* --- adw-spin-row --- */
|
|
377
|
+
adw-spin-row {
|
|
378
|
+
display: flex;
|
|
379
|
+
align-items: center;
|
|
380
|
+
justify-content: space-between;
|
|
381
|
+
min-height: 50px;
|
|
382
|
+
padding: var(--spacing-s) var(--spacing-m);
|
|
383
|
+
gap: var(--spacing-m);
|
|
384
|
+
border-bottom: 1px solid var(--card-shade-color);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
adw-spin-row:last-child {
|
|
388
|
+
border-bottom: none;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
adw-spin-row .adw-row-title {
|
|
392
|
+
flex: 1;
|
|
393
|
+
font-size: var(--font-size-base);
|
|
394
|
+
color: var(--card-fg-color);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.adw-spin-control {
|
|
398
|
+
display: flex;
|
|
399
|
+
align-items: center;
|
|
400
|
+
gap: 2px;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.adw-spin-control button {
|
|
404
|
+
width: 32px;
|
|
405
|
+
height: 32px;
|
|
406
|
+
border: none;
|
|
407
|
+
border-radius: var(--button-radius);
|
|
408
|
+
background: transparent;
|
|
409
|
+
color: var(--card-fg-color);
|
|
410
|
+
cursor: pointer;
|
|
411
|
+
font-size: 16px;
|
|
412
|
+
display: flex;
|
|
413
|
+
align-items: center;
|
|
414
|
+
justify-content: center;
|
|
415
|
+
flex-shrink: 0;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.adw-spin-control button:hover {
|
|
419
|
+
background: rgba(128, 128, 128, 0.15);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.adw-spin-control input {
|
|
423
|
+
width: 56px;
|
|
424
|
+
text-align: center;
|
|
425
|
+
border: none;
|
|
426
|
+
border-radius: var(--button-radius);
|
|
427
|
+
background: rgba(128, 128, 128, 0.1);
|
|
428
|
+
color: var(--card-fg-color);
|
|
429
|
+
font-family: var(--font-family);
|
|
430
|
+
font-size: var(--font-size-base);
|
|
431
|
+
padding: 4px 2px;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.adw-spin-control input:focus {
|
|
435
|
+
outline: 2px solid var(--accent-color);
|
|
436
|
+
outline-offset: -1px;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/* --- adw-toast-overlay ---
|
|
440
|
+
Reference: refs/adwaita-web/adwaita-web/scss/_toast_overlay.scss */
|
|
441
|
+
adw-toast-overlay {
|
|
442
|
+
position: fixed;
|
|
443
|
+
bottom: 24px;
|
|
444
|
+
left: 50%;
|
|
445
|
+
transform: translateX(-50%);
|
|
446
|
+
z-index: 9999;
|
|
447
|
+
display: flex;
|
|
448
|
+
flex-direction: column-reverse;
|
|
449
|
+
gap: 9px;
|
|
450
|
+
align-items: center;
|
|
451
|
+
pointer-events: none;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/* --- adw-toast ---
|
|
455
|
+
Reference: refs/adwaita-web/adwaita-web/scss/_toast.scss */
|
|
456
|
+
.adw-toast {
|
|
457
|
+
background-color: #3d3846;
|
|
458
|
+
color: #ffffff;
|
|
459
|
+
padding: 9px 12px;
|
|
460
|
+
border-radius: var(--card-radius);
|
|
461
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.25);
|
|
462
|
+
display: flex;
|
|
463
|
+
align-items: center;
|
|
464
|
+
gap: 9px;
|
|
465
|
+
width: max-content;
|
|
466
|
+
max-width: 400px;
|
|
467
|
+
min-height: 36px;
|
|
468
|
+
pointer-events: auto;
|
|
469
|
+
font-family: var(--font-family);
|
|
470
|
+
font-size: var(--font-size-base);
|
|
471
|
+
|
|
472
|
+
opacity: 0;
|
|
473
|
+
transform: translateY(100%) scale(0.9);
|
|
474
|
+
transition: opacity 0.2s cubic-bezier(0, 0, 0.2, 1),
|
|
475
|
+
transform 0.2s cubic-bezier(0, 0, 0.2, 1);
|
|
476
|
+
will-change: transform, opacity;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.adw-toast.visible {
|
|
480
|
+
opacity: 1;
|
|
481
|
+
transform: translateY(0) scale(1);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
.adw-toast.hiding {
|
|
485
|
+
opacity: 0;
|
|
486
|
+
transform: translateY(100%) scale(0.9);
|
|
487
|
+
transition-duration: 0.15s;
|
|
488
|
+
transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.adw-toast .adw-toast-title {
|
|
492
|
+
font-weight: normal;
|
|
493
|
+
line-height: 1.3;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
497
|
+
adw-overlay-split-view
|
|
498
|
+
Reference: Adw.OverlaySplitView from libadwaita
|
|
499
|
+
═══════════════════════════════════════════════════════════════ */
|
|
500
|
+
adw-overlay-split-view {
|
|
501
|
+
display: flex;
|
|
502
|
+
flex-direction: row;
|
|
503
|
+
flex: 1;
|
|
504
|
+
min-height: 0;
|
|
505
|
+
position: relative;
|
|
506
|
+
overflow: hidden;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
adw-overlay-split-view .adw-osv-sidebar {
|
|
510
|
+
background-color: var(--sidebar-bg-color);
|
|
511
|
+
overflow-y: auto;
|
|
512
|
+
flex-shrink: 0;
|
|
513
|
+
transition: transform 0.2s ease, opacity 0.2s ease, margin 0.2s ease;
|
|
514
|
+
z-index: 10;
|
|
515
|
+
align-self: stretch;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
adw-overlay-split-view .adw-osv-content {
|
|
519
|
+
flex: 1;
|
|
520
|
+
min-width: 0;
|
|
521
|
+
min-height: 0;
|
|
522
|
+
display: flex;
|
|
523
|
+
flex-direction: column;
|
|
524
|
+
align-self: stretch;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/* Docked mode (not collapsed) — sidebar beside content */
|
|
528
|
+
adw-overlay-split-view:not(.collapsed) .adw-osv-sidebar {
|
|
529
|
+
position: relative;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/* Docked hide: slide left and collapse space via negative margin.
|
|
533
|
+
Sidebar keeps its intrinsic width so internal elements don't reflow. */
|
|
534
|
+
adw-overlay-split-view:not(.collapsed):not(.show-sidebar) .adw-osv-sidebar {
|
|
535
|
+
transform: translateX(-100%);
|
|
536
|
+
opacity: 0;
|
|
537
|
+
pointer-events: none;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/* Overlay mode (collapsed) — sidebar on top of content */
|
|
541
|
+
adw-overlay-split-view.collapsed .adw-osv-sidebar {
|
|
542
|
+
position: absolute;
|
|
543
|
+
top: 0;
|
|
544
|
+
bottom: 0;
|
|
545
|
+
left: 0;
|
|
546
|
+
transform: translateX(-100%);
|
|
547
|
+
opacity: 0;
|
|
548
|
+
pointer-events: none;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
adw-overlay-split-view.collapsed.sidebar-end .adw-osv-sidebar {
|
|
552
|
+
left: auto;
|
|
553
|
+
right: 0;
|
|
554
|
+
transform: translateX(100%);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
adw-overlay-split-view.collapsed.show-sidebar .adw-osv-sidebar {
|
|
558
|
+
transform: translateX(0);
|
|
559
|
+
opacity: 1;
|
|
560
|
+
pointer-events: auto;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/* Backdrop — visible only in overlay mode when sidebar is open */
|
|
564
|
+
adw-overlay-split-view .adw-osv-backdrop {
|
|
565
|
+
display: none;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
adw-overlay-split-view.collapsed.show-sidebar .adw-osv-backdrop {
|
|
569
|
+
display: block;
|
|
570
|
+
position: absolute;
|
|
571
|
+
inset: 0;
|
|
572
|
+
background: rgba(0, 0, 0, 0.3);
|
|
573
|
+
z-index: 9;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/* Sidebar toggle button — sidebar-show-symbolic icon via CSS mask */
|
|
577
|
+
.adw-sidebar-toggle-icon::after {
|
|
578
|
+
content: '';
|
|
579
|
+
position: absolute;
|
|
580
|
+
inset: 0;
|
|
581
|
+
background-color: currentColor;
|
|
582
|
+
-webkit-mask-image: var(--icon-sidebar-show);
|
|
583
|
+
mask-image: var(--icon-sidebar-show);
|
|
584
|
+
-webkit-mask-repeat: no-repeat;
|
|
585
|
+
mask-repeat: no-repeat;
|
|
586
|
+
-webkit-mask-size: 16px 16px;
|
|
587
|
+
mask-size: 16px 16px;
|
|
588
|
+
-webkit-mask-position: center;
|
|
589
|
+
mask-position: center;
|
|
590
|
+
pointer-events: none;
|
|
591
|
+
}
|
|
592
|
+
`;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// <adw-combo-row> — Row with a label and dropdown select.
|
|
2
|
+
// Attributes: title, items (JSON string[]), selected (index number)
|
|
3
|
+
// Events: notify::selected (CustomEvent, mirrors GJS GObject signal naming)
|
|
4
|
+
// The native <select> is stretched invisibly over the row so clicking anywhere opens it.
|
|
5
|
+
// Adapted from Adwaita Web UI Framework (https://github.com/mclellac/adwaita-web).
|
|
6
|
+
// Copyright (c) 2025 csm. MIT License.
|
|
7
|
+
// Modifications: Reimplemented as Web Component for @gjsify/adwaita-web.
|
|
8
|
+
|
|
9
|
+
export class AdwComboRow extends HTMLElement {
|
|
10
|
+
private _select!: HTMLSelectElement;
|
|
11
|
+
private _valueEl!: HTMLSpanElement;
|
|
12
|
+
private _items: string[] = [];
|
|
13
|
+
private _initialized = false;
|
|
14
|
+
|
|
15
|
+
static get observedAttributes() { return ['selected']; }
|
|
16
|
+
|
|
17
|
+
get selected(): number {
|
|
18
|
+
return this._select ? this._select.selectedIndex : parseInt(this.getAttribute('selected') || '0', 10);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
set selected(value: number) {
|
|
22
|
+
this.setAttribute('selected', String(value));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
connectedCallback() {
|
|
26
|
+
if (this._initialized) return;
|
|
27
|
+
this._initialized = true;
|
|
28
|
+
|
|
29
|
+
const title = this.getAttribute('title') || '';
|
|
30
|
+
this._items = JSON.parse(this.getAttribute('items') || '[]');
|
|
31
|
+
const selectedIdx = parseInt(this.getAttribute('selected') || '0', 10);
|
|
32
|
+
|
|
33
|
+
// Title label
|
|
34
|
+
const titleEl = document.createElement('span');
|
|
35
|
+
titleEl.className = 'adw-row-title';
|
|
36
|
+
titleEl.textContent = title;
|
|
37
|
+
|
|
38
|
+
// Visible selected value display
|
|
39
|
+
const valueEl = document.createElement('span');
|
|
40
|
+
valueEl.className = 'adw-row-value';
|
|
41
|
+
valueEl.textContent = this._items[selectedIdx] ?? '';
|
|
42
|
+
this._valueEl = valueEl;
|
|
43
|
+
|
|
44
|
+
// Hidden select overlaying the entire row
|
|
45
|
+
const select = document.createElement('select');
|
|
46
|
+
this._items.forEach((item, i) => {
|
|
47
|
+
const option = document.createElement('option');
|
|
48
|
+
option.value = String(i);
|
|
49
|
+
option.textContent = item;
|
|
50
|
+
if (i === selectedIdx) option.selected = true;
|
|
51
|
+
select.appendChild(option);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
this.appendChild(titleEl);
|
|
55
|
+
this.appendChild(valueEl);
|
|
56
|
+
this.appendChild(select);
|
|
57
|
+
|
|
58
|
+
this._select = select;
|
|
59
|
+
this._select.addEventListener('change', () => {
|
|
60
|
+
const idx = this._select.selectedIndex;
|
|
61
|
+
this._valueEl.textContent = this._items[idx] ?? '';
|
|
62
|
+
this.setAttribute('selected', String(idx));
|
|
63
|
+
this.dispatchEvent(new CustomEvent('notify::selected', {
|
|
64
|
+
bubbles: true,
|
|
65
|
+
detail: { selected: idx },
|
|
66
|
+
}));
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
attributeChangedCallback(name: string, _old: string | null, value: string | null) {
|
|
71
|
+
if (name === 'selected' && this._select) {
|
|
72
|
+
const idx = parseInt(value || '0', 10);
|
|
73
|
+
this._select.selectedIndex = idx;
|
|
74
|
+
if (this._valueEl) {
|
|
75
|
+
this._valueEl.textContent = this._items[idx] ?? '';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
customElements.define('adw-combo-row', AdwComboRow);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// <adw-header-bar> — Adwaita header bar with centered title and start/end button slots.
|
|
2
|
+
// Adapted from Adwaita Web UI Framework (https://github.com/mclellac/adwaita-web).
|
|
3
|
+
// Copyright (c) 2025 csm. MIT License.
|
|
4
|
+
// Modifications: Reimplemented as Web Component for @gjsify/adwaita-web.
|
|
5
|
+
|
|
6
|
+
export class AdwHeaderBar extends HTMLElement {
|
|
7
|
+
private _initialized = false;
|
|
8
|
+
private _startEl: HTMLDivElement | null = null;
|
|
9
|
+
private _endEl: HTMLDivElement | null = null;
|
|
10
|
+
|
|
11
|
+
/** The start (left) section container — append buttons/widgets here. */
|
|
12
|
+
get startSection(): HTMLDivElement | null { return this._startEl; }
|
|
13
|
+
|
|
14
|
+
/** The end (right) section container — append buttons/widgets here. */
|
|
15
|
+
get endSection(): HTMLDivElement | null { return this._endEl; }
|
|
16
|
+
|
|
17
|
+
connectedCallback() {
|
|
18
|
+
if (this._initialized) return;
|
|
19
|
+
this._initialized = true;
|
|
20
|
+
|
|
21
|
+
const title = this.getAttribute('title') || '';
|
|
22
|
+
|
|
23
|
+
// Capture any pre-existing slotted children before clearing
|
|
24
|
+
const startChildren = Array.from(this.querySelectorAll(':scope > [slot="start"]'));
|
|
25
|
+
const endChildren = Array.from(this.querySelectorAll(':scope > [slot="end"]'));
|
|
26
|
+
|
|
27
|
+
// Start section
|
|
28
|
+
this._startEl = document.createElement('div');
|
|
29
|
+
this._startEl.className = 'adw-header-bar-start';
|
|
30
|
+
for (const child of startChildren) this._startEl.appendChild(child);
|
|
31
|
+
|
|
32
|
+
// Center section with title
|
|
33
|
+
const center = document.createElement('div');
|
|
34
|
+
center.className = 'adw-header-bar-center';
|
|
35
|
+
const titleEl = document.createElement('span');
|
|
36
|
+
titleEl.className = 'adw-header-bar-title';
|
|
37
|
+
titleEl.textContent = title;
|
|
38
|
+
center.appendChild(titleEl);
|
|
39
|
+
|
|
40
|
+
// End section
|
|
41
|
+
this._endEl = document.createElement('div');
|
|
42
|
+
this._endEl.className = 'adw-header-bar-end';
|
|
43
|
+
for (const child of endChildren) this._endEl.appendChild(child);
|
|
44
|
+
|
|
45
|
+
// Replace all children atomically
|
|
46
|
+
this.replaceChildren(this._startEl, center, this._endEl);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
customElements.define('adw-header-bar', AdwHeaderBar);
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// <adw-overlay-split-view> — Responsive sidebar that docks or overlays content.
|
|
2
|
+
// Reference: Adw.OverlaySplitView from libadwaita.
|
|
3
|
+
// Docs: refs/adwaita-web/adwaita-web/docs/widgets/overlaysplitview.md
|
|
4
|
+
|
|
5
|
+
export class AdwOverlaySplitView extends HTMLElement {
|
|
6
|
+
private _initialized = false;
|
|
7
|
+
private _sidebarEl!: HTMLDivElement;
|
|
8
|
+
private _contentEl!: HTMLDivElement;
|
|
9
|
+
private _backdropEl!: HTMLDivElement;
|
|
10
|
+
|
|
11
|
+
static get observedAttributes() {
|
|
12
|
+
return ['show-sidebar', 'collapsed', 'sidebar-position', 'min-sidebar-width', 'max-sidebar-width', 'sidebar-width-fraction'];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
get showSidebar(): boolean {
|
|
16
|
+
return this.hasAttribute('show-sidebar');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
set showSidebar(v: boolean) {
|
|
20
|
+
if (v) this.setAttribute('show-sidebar', '');
|
|
21
|
+
else this.removeAttribute('show-sidebar');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get collapsed(): boolean {
|
|
25
|
+
return this.hasAttribute('collapsed');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
set collapsed(v: boolean) {
|
|
29
|
+
if (v) this.setAttribute('collapsed', '');
|
|
30
|
+
else this.removeAttribute('collapsed');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get minSidebarWidth(): number {
|
|
34
|
+
return parseFloat(this.getAttribute('min-sidebar-width') || '280');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get maxSidebarWidth(): number {
|
|
38
|
+
return parseFloat(this.getAttribute('max-sidebar-width') || '400');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get sidebarWidthFraction(): number {
|
|
42
|
+
return parseFloat(this.getAttribute('sidebar-width-fraction') || '0.30');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
connectedCallback() {
|
|
46
|
+
if (this._initialized) return;
|
|
47
|
+
this._initialized = true;
|
|
48
|
+
|
|
49
|
+
// Capture slotted children before rebuilding DOM
|
|
50
|
+
const sidebarChildren = Array.from(this.querySelectorAll('[slot="sidebar"]'));
|
|
51
|
+
const contentChildren = Array.from(this.querySelectorAll('[slot="content"]'));
|
|
52
|
+
// Any remaining unslotted children go to content
|
|
53
|
+
const unslotted = Array.from(this.childNodes).filter(
|
|
54
|
+
n => !sidebarChildren.includes(n as Element) && !contentChildren.includes(n as Element),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// Clear children safely
|
|
58
|
+
this.replaceChildren();
|
|
59
|
+
|
|
60
|
+
// Sidebar container
|
|
61
|
+
this._sidebarEl = document.createElement('div');
|
|
62
|
+
this._sidebarEl.className = 'adw-osv-sidebar';
|
|
63
|
+
sidebarChildren.forEach(c => this._sidebarEl.appendChild(c));
|
|
64
|
+
|
|
65
|
+
// Content container
|
|
66
|
+
this._contentEl = document.createElement('div');
|
|
67
|
+
this._contentEl.className = 'adw-osv-content';
|
|
68
|
+
contentChildren.forEach(c => this._contentEl.appendChild(c));
|
|
69
|
+
unslotted.forEach(c => this._contentEl.appendChild(c));
|
|
70
|
+
|
|
71
|
+
// Backdrop for overlay dismiss
|
|
72
|
+
this._backdropEl = document.createElement('div');
|
|
73
|
+
this._backdropEl.className = 'adw-osv-backdrop';
|
|
74
|
+
this._backdropEl.addEventListener('click', () => this.hideSidebar());
|
|
75
|
+
|
|
76
|
+
this.append(this._sidebarEl, this._contentEl, this._backdropEl);
|
|
77
|
+
|
|
78
|
+
// Apply initial state
|
|
79
|
+
this._syncClasses();
|
|
80
|
+
this._syncSidebarWidth();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
attributeChangedCallback(_name: string, _old: string | null, _val: string | null) {
|
|
84
|
+
if (!this._initialized) return;
|
|
85
|
+
this._syncClasses();
|
|
86
|
+
if (_name === 'min-sidebar-width' || _name === 'max-sidebar-width' || _name === 'sidebar-width-fraction') {
|
|
87
|
+
this._syncSidebarWidth();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
openSidebar() {
|
|
92
|
+
this.showSidebar = true;
|
|
93
|
+
this.dispatchEvent(new CustomEvent('sidebar-toggled', { detail: { isVisible: true } }));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
hideSidebar() {
|
|
97
|
+
this.showSidebar = false;
|
|
98
|
+
this.dispatchEvent(new CustomEvent('sidebar-toggled', { detail: { isVisible: false } }));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
toggleSidebar() {
|
|
102
|
+
this.showSidebar = !this.showSidebar;
|
|
103
|
+
this.dispatchEvent(new CustomEvent('sidebar-toggled', {
|
|
104
|
+
detail: { isVisible: this.showSidebar },
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private _syncClasses() {
|
|
109
|
+
this.classList.toggle('collapsed', this.collapsed);
|
|
110
|
+
this.classList.toggle('show-sidebar', this.showSidebar);
|
|
111
|
+
const pos = this.getAttribute('sidebar-position') || 'start';
|
|
112
|
+
this.classList.toggle('sidebar-start', pos === 'start');
|
|
113
|
+
this.classList.toggle('sidebar-end', pos === 'end');
|
|
114
|
+
|
|
115
|
+
// In docked mode, use negative margin to collapse sidebar space
|
|
116
|
+
// while keeping the sidebar's intrinsic width (slide animation).
|
|
117
|
+
if (this._sidebarEl && !this.collapsed) {
|
|
118
|
+
if (!this.showSidebar) {
|
|
119
|
+
const w = this._sidebarEl.offsetWidth;
|
|
120
|
+
this._sidebarEl.style.marginRight = `-${w}px`;
|
|
121
|
+
} else {
|
|
122
|
+
this._sidebarEl.style.marginRight = '';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private _syncSidebarWidth() {
|
|
128
|
+
if (!this._sidebarEl) return;
|
|
129
|
+
this._sidebarEl.style.minWidth = `${this.minSidebarWidth}px`;
|
|
130
|
+
this._sidebarEl.style.maxWidth = `${this.maxSidebarWidth}px`;
|
|
131
|
+
this._sidebarEl.style.width = `${this.sidebarWidthFraction * 100}%`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
customElements.define('adw-overlay-split-view', AdwOverlaySplitView);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// <adw-preferences-group> — Groups child rows in a boxed list with a title label.
|
|
2
|
+
// Adapted from Adwaita Web UI Framework (https://github.com/mclellac/adwaita-web).
|
|
3
|
+
// Copyright (c) 2025 csm. MIT License.
|
|
4
|
+
// Modifications: Reimplemented as Web Component for @gjsify/adwaita-web.
|
|
5
|
+
|
|
6
|
+
export class AdwPreferencesGroup extends HTMLElement {
|
|
7
|
+
private _initialized = false;
|
|
8
|
+
|
|
9
|
+
connectedCallback() {
|
|
10
|
+
if (this._initialized) return;
|
|
11
|
+
this._initialized = true;
|
|
12
|
+
|
|
13
|
+
const title = this.getAttribute('title') || '';
|
|
14
|
+
const children = Array.from(this.childNodes);
|
|
15
|
+
|
|
16
|
+
// Build header with title
|
|
17
|
+
const header = document.createElement('div');
|
|
18
|
+
header.className = 'adw-preferences-group-header';
|
|
19
|
+
const titleEl = document.createElement('span');
|
|
20
|
+
titleEl.className = 'adw-preferences-group-title';
|
|
21
|
+
titleEl.textContent = title;
|
|
22
|
+
header.appendChild(titleEl);
|
|
23
|
+
|
|
24
|
+
// Build boxed list container and move children into it
|
|
25
|
+
const listbox = document.createElement('div');
|
|
26
|
+
listbox.className = 'adw-preferences-group-listbox';
|
|
27
|
+
children.forEach(child => listbox.appendChild(child));
|
|
28
|
+
|
|
29
|
+
this.appendChild(header);
|
|
30
|
+
this.appendChild(listbox);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
customElements.define('adw-preferences-group', AdwPreferencesGroup);
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// <adw-spin-row> — Row with a label and numeric spin control (+/− buttons).
|
|
2
|
+
// Attributes: title, min, max, step, value
|
|
3
|
+
// Events: notify::value (CustomEvent, mirrors GJS GObject signal naming)
|
|
4
|
+
// Adapted from Adwaita Web UI Framework (https://github.com/mclellac/adwaita-web).
|
|
5
|
+
// Copyright (c) 2025 csm. MIT License.
|
|
6
|
+
// Modifications: Reimplemented as Web Component for @gjsify/adwaita-web.
|
|
7
|
+
|
|
8
|
+
export class AdwSpinRow extends HTMLElement {
|
|
9
|
+
private _input!: HTMLInputElement;
|
|
10
|
+
private _min = 0;
|
|
11
|
+
private _max = 100;
|
|
12
|
+
private _step = 1;
|
|
13
|
+
private _value = 0;
|
|
14
|
+
private _initialized = false;
|
|
15
|
+
|
|
16
|
+
static get observedAttributes() { return ['value', 'min', 'max', 'step']; }
|
|
17
|
+
|
|
18
|
+
get value(): number {
|
|
19
|
+
return this._value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
set value(v: number) {
|
|
23
|
+
const clamped = Math.min(this._max, Math.max(this._min, v));
|
|
24
|
+
// Round to step precision to avoid floating-point drift
|
|
25
|
+
const decimals = this._countDecimals(this._step);
|
|
26
|
+
this._value = parseFloat(clamped.toFixed(decimals));
|
|
27
|
+
if (this._input) {
|
|
28
|
+
this._input.value = this._formatValue(this._value);
|
|
29
|
+
}
|
|
30
|
+
this.setAttribute('value', String(this._value));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
connectedCallback() {
|
|
34
|
+
if (this._initialized) return;
|
|
35
|
+
this._initialized = true;
|
|
36
|
+
|
|
37
|
+
const title = this.getAttribute('title') || '';
|
|
38
|
+
this._min = parseFloat(this.getAttribute('min') || '0');
|
|
39
|
+
this._max = parseFloat(this.getAttribute('max') || '100');
|
|
40
|
+
this._step = parseFloat(this.getAttribute('step') || '1');
|
|
41
|
+
this._value = parseFloat(this.getAttribute('value') || String(this._min));
|
|
42
|
+
|
|
43
|
+
// Title
|
|
44
|
+
const titleEl = document.createElement('span');
|
|
45
|
+
titleEl.className = 'adw-row-title';
|
|
46
|
+
titleEl.textContent = title;
|
|
47
|
+
|
|
48
|
+
// Spin control container
|
|
49
|
+
const control = document.createElement('div');
|
|
50
|
+
control.className = 'adw-spin-control';
|
|
51
|
+
|
|
52
|
+
// Decrement button
|
|
53
|
+
const decBtn = document.createElement('button');
|
|
54
|
+
decBtn.className = 'adw-spin-dec';
|
|
55
|
+
decBtn.textContent = '−';
|
|
56
|
+
decBtn.addEventListener('click', () => this._adjust(-this._step));
|
|
57
|
+
|
|
58
|
+
// Value input
|
|
59
|
+
const input = document.createElement('input');
|
|
60
|
+
input.type = 'text';
|
|
61
|
+
input.value = this._formatValue(this._value);
|
|
62
|
+
input.addEventListener('change', () => {
|
|
63
|
+
const parsed = parseFloat(input.value);
|
|
64
|
+
if (!isNaN(parsed)) {
|
|
65
|
+
this.value = parsed;
|
|
66
|
+
this._emitChange();
|
|
67
|
+
} else {
|
|
68
|
+
input.value = this._formatValue(this._value);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Increment button
|
|
73
|
+
const incBtn = document.createElement('button');
|
|
74
|
+
incBtn.className = 'adw-spin-inc';
|
|
75
|
+
incBtn.textContent = '+';
|
|
76
|
+
incBtn.addEventListener('click', () => this._adjust(this._step));
|
|
77
|
+
|
|
78
|
+
control.append(decBtn, input, incBtn);
|
|
79
|
+
this.append(titleEl, control);
|
|
80
|
+
this._input = input;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
attributeChangedCallback(name: string, _old: string | null, val: string | null) {
|
|
84
|
+
if (!this._initialized) return;
|
|
85
|
+
const num = parseFloat(val || '0');
|
|
86
|
+
switch (name) {
|
|
87
|
+
case 'value':
|
|
88
|
+
if (num !== this._value) {
|
|
89
|
+
this._value = Math.min(this._max, Math.max(this._min, num));
|
|
90
|
+
this._input.value = this._formatValue(this._value);
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
case 'min': this._min = num; break;
|
|
94
|
+
case 'max': this._max = num; break;
|
|
95
|
+
case 'step': this._step = num; break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private _adjust(delta: number) {
|
|
100
|
+
this.value = this._value + delta;
|
|
101
|
+
this._emitChange();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private _emitChange() {
|
|
105
|
+
this.dispatchEvent(new CustomEvent('notify::value', {
|
|
106
|
+
bubbles: true,
|
|
107
|
+
detail: { value: this._value },
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private _countDecimals(n: number): number {
|
|
112
|
+
const s = String(n);
|
|
113
|
+
const dot = s.indexOf('.');
|
|
114
|
+
return dot === -1 ? 0 : s.length - dot - 1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private _formatValue(v: number): string {
|
|
118
|
+
const decimals = this._countDecimals(this._step);
|
|
119
|
+
return decimals > 0 ? v.toFixed(decimals) : String(v);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
customElements.define('adw-spin-row', AdwSpinRow);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// <adw-switch-row> — Row with a label and toggle switch.
|
|
2
|
+
// Attributes: title, active (boolean)
|
|
3
|
+
// Events: notify::active (CustomEvent, mirrors GJS GObject signal naming)
|
|
4
|
+
// Adapted from Adwaita Web UI Framework (https://github.com/mclellac/adwaita-web).
|
|
5
|
+
// Copyright (c) 2025 csm. MIT License.
|
|
6
|
+
// Modifications: Reimplemented as Web Component for @gjsify/adwaita-web.
|
|
7
|
+
|
|
8
|
+
export class AdwSwitchRow extends HTMLElement {
|
|
9
|
+
private _checkbox!: HTMLInputElement;
|
|
10
|
+
private _initialized = false;
|
|
11
|
+
|
|
12
|
+
static get observedAttributes() { return ['active']; }
|
|
13
|
+
|
|
14
|
+
get active(): boolean {
|
|
15
|
+
return this.hasAttribute('active');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
set active(value: boolean) {
|
|
19
|
+
if (value) this.setAttribute('active', '');
|
|
20
|
+
else this.removeAttribute('active');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
connectedCallback() {
|
|
24
|
+
if (this._initialized) return;
|
|
25
|
+
this._initialized = true;
|
|
26
|
+
|
|
27
|
+
const title = this.getAttribute('title') || '';
|
|
28
|
+
const checked = this.hasAttribute('active');
|
|
29
|
+
|
|
30
|
+
// Title
|
|
31
|
+
const titleEl = document.createElement('span');
|
|
32
|
+
titleEl.className = 'adw-row-title';
|
|
33
|
+
titleEl.textContent = title;
|
|
34
|
+
|
|
35
|
+
// Switch label with hidden checkbox + slider
|
|
36
|
+
const label = document.createElement('label');
|
|
37
|
+
label.className = 'adw-switch';
|
|
38
|
+
|
|
39
|
+
const input = document.createElement('input');
|
|
40
|
+
input.type = 'checkbox';
|
|
41
|
+
input.checked = checked;
|
|
42
|
+
|
|
43
|
+
const slider = document.createElement('span');
|
|
44
|
+
slider.className = 'adw-switch-slider';
|
|
45
|
+
|
|
46
|
+
label.appendChild(input);
|
|
47
|
+
label.appendChild(slider);
|
|
48
|
+
|
|
49
|
+
this.appendChild(titleEl);
|
|
50
|
+
this.appendChild(label);
|
|
51
|
+
|
|
52
|
+
this._checkbox = input;
|
|
53
|
+
this._checkbox.addEventListener('change', () => {
|
|
54
|
+
this.toggleAttribute('active', this._checkbox.checked);
|
|
55
|
+
this.dispatchEvent(new CustomEvent('notify::active', {
|
|
56
|
+
bubbles: true,
|
|
57
|
+
detail: { active: this._checkbox.checked },
|
|
58
|
+
}));
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
attributeChangedCallback(name: string, _old: string | null, value: string | null) {
|
|
63
|
+
if (name === 'active' && this._checkbox) {
|
|
64
|
+
this._checkbox.checked = value !== null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
customElements.define('adw-switch-row', AdwSwitchRow);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// <adw-toast-overlay> — Adwaita toast notification overlay.
|
|
2
|
+
// Displays temporary feedback messages at the bottom of the viewport.
|
|
3
|
+
// Adapted from Adwaita Web UI Framework (https://github.com/mclellac/adwaita-web).
|
|
4
|
+
// Reference: refs/adwaita-web/adwaita-web/scss/_toast.scss, _toast_overlay.scss
|
|
5
|
+
// Reference: refs/libadwaita/src/adw-toast-overlay.c
|
|
6
|
+
// Copyright (c) 2025 csm (adwaita-web). MIT License.
|
|
7
|
+
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
8
|
+
// Modifications: Reimplemented as Web Component for @gjsify/adwaita-web.
|
|
9
|
+
|
|
10
|
+
export class AdwToastOverlay extends HTMLElement {
|
|
11
|
+
/**
|
|
12
|
+
* Show a toast notification.
|
|
13
|
+
* @param title - The text to display.
|
|
14
|
+
* @param timeout - Time in ms before the toast auto-dismisses (default 2000).
|
|
15
|
+
*/
|
|
16
|
+
addToast(title: string, timeout = 2000): void {
|
|
17
|
+
const toast = document.createElement('div');
|
|
18
|
+
toast.className = 'adw-toast';
|
|
19
|
+
|
|
20
|
+
const titleEl = document.createElement('span');
|
|
21
|
+
titleEl.className = 'adw-toast-title';
|
|
22
|
+
titleEl.textContent = title;
|
|
23
|
+
toast.appendChild(titleEl);
|
|
24
|
+
|
|
25
|
+
this.appendChild(toast);
|
|
26
|
+
|
|
27
|
+
// Trigger enter animation on next frame
|
|
28
|
+
requestAnimationFrame(() => {
|
|
29
|
+
toast.classList.add('visible');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Auto-dismiss after timeout
|
|
33
|
+
setTimeout(() => {
|
|
34
|
+
toast.classList.remove('visible');
|
|
35
|
+
toast.classList.add('hiding');
|
|
36
|
+
toast.addEventListener('transitionend', () => toast.remove(), { once: true });
|
|
37
|
+
// Fallback if transitionend doesn't fire
|
|
38
|
+
setTimeout(() => { if (toast.parentNode) toast.remove(); }, 300);
|
|
39
|
+
}, timeout);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
customElements.define('adw-toast-overlay', AdwToastOverlay);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// <adw-window> — Adwaita application window frame.
|
|
2
|
+
// Provides rounded corners, shadow, and flex column layout.
|
|
3
|
+
// Adapted from Adwaita Web UI Framework (https://github.com/mclellac/adwaita-web).
|
|
4
|
+
// Copyright (c) 2025 csm. MIT License.
|
|
5
|
+
// Modifications: Reimplemented as Web Component for @gjsify/adwaita-web.
|
|
6
|
+
|
|
7
|
+
export class AdwWindow extends HTMLElement {
|
|
8
|
+
connectedCallback() {
|
|
9
|
+
// Light DOM: children remain in place, element itself is the styled container.
|
|
10
|
+
// Set dimensions from attributes if provided.
|
|
11
|
+
const w = this.getAttribute('width');
|
|
12
|
+
const h = this.getAttribute('height');
|
|
13
|
+
if (w) this.style.width = `${w}px`;
|
|
14
|
+
if (h) this.style.height = `${h}px`;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
customElements.define('adw-window', AdwWindow);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// @gjsify/adwaita-web — Adwaita/Libadwaita web components for browser targets.
|
|
2
|
+
// Importing this module registers all custom elements and injects the Adwaita CSS.
|
|
3
|
+
// Reference: refs/libadwaita (colors/sizing), refs/adwaita-web (component patterns).
|
|
4
|
+
|
|
5
|
+
import '@gjsify/adwaita-fonts'; // Registers @font-face (fontsource pattern)
|
|
6
|
+
import { adwaitaCSS } from './adwaita-css.js';
|
|
7
|
+
|
|
8
|
+
// Register custom elements (side-effect imports)
|
|
9
|
+
export { AdwWindow } from './elements/adw-window.js';
|
|
10
|
+
export { AdwHeaderBar } from './elements/adw-header-bar.js';
|
|
11
|
+
export { AdwPreferencesGroup } from './elements/adw-preferences-group.js';
|
|
12
|
+
export { AdwSwitchRow } from './elements/adw-switch-row.js';
|
|
13
|
+
export { AdwComboRow } from './elements/adw-combo-row.js';
|
|
14
|
+
export { AdwSpinRow } from './elements/adw-spin-row.js';
|
|
15
|
+
export { AdwToastOverlay } from './elements/adw-toast-overlay.js';
|
|
16
|
+
export { AdwOverlaySplitView } from './elements/adw-overlay-split-view.js';
|
|
17
|
+
|
|
18
|
+
// Inject Adwaita CSS into the document
|
|
19
|
+
if (typeof document !== 'undefined') {
|
|
20
|
+
const style = document.createElement('style');
|
|
21
|
+
style.textContent = adwaitaCSS;
|
|
22
|
+
document.head.appendChild(style);
|
|
23
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "ESNext",
|
|
4
|
+
"target": "ESNext",
|
|
5
|
+
"lib": ["ESNext", "DOM"],
|
|
6
|
+
"rootDir": "src",
|
|
7
|
+
"outDir": "lib",
|
|
8
|
+
"declarationDir": "lib/types",
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"emitDeclarationOnly": true,
|
|
13
|
+
"strict": false
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*.ts"]
|
|
16
|
+
}
|