@timmo001/groundwork-ui 0.3.0 → 0.4.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/README.md +10 -1
- package/custom-elements.json +20865 -0
- package/package.json +7 -2
- package/src/events.ts +44 -0
- package/src/fields.ts +34 -0
- package/src/gw-app-bar.ts +343 -0
- package/src/gw-command-menu.ts +1 -1
- package/src/gw-dialog.ts +1 -1
- package/src/gw-expansion.ts +1 -1
- package/src/gw-menu-button.ts +1 -1
- package/src/gw-notification-center.ts +1 -1
- package/src/gw-notification.ts +1 -1
- package/src/gw-option-list.ts +51 -5
- package/src/gw-popover.ts +1 -1
- package/src/gw-segmented.ts +1 -1
- package/src/gw-select.ts +87 -9
- package/src/gw-theme-picker.ts +200 -0
- package/src/index.ts +6 -0
- package/src/on.ts +52 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timmo001/groundwork-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Groundwork's framework-free web components, styled by the --gw-* tokens",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"src",
|
|
13
|
+
"custom-elements.json",
|
|
13
14
|
"README.md",
|
|
14
15
|
"LICENSE"
|
|
15
16
|
],
|
|
17
|
+
"customElements": ".manifest-check/custom-elements.json",
|
|
16
18
|
"sideEffects": true,
|
|
17
19
|
"publishConfig": {
|
|
18
20
|
"access": "public",
|
|
@@ -28,9 +30,12 @@
|
|
|
28
30
|
"lint": "oxlint .",
|
|
29
31
|
"typecheck": "tsc",
|
|
30
32
|
"format": "prettier --write .",
|
|
31
|
-
"format:check": "prettier --check ."
|
|
33
|
+
"format:check": "prettier --check .",
|
|
34
|
+
"gen": "bun run scripts/manifest.ts",
|
|
35
|
+
"gen:check": "bun run scripts/manifest.ts --check"
|
|
32
36
|
},
|
|
33
37
|
"devDependencies": {
|
|
38
|
+
"@custom-elements-manifest/analyzer": "0.11.0",
|
|
34
39
|
"@oxlint/plugins": "1.90.0",
|
|
35
40
|
"@timmo001/oxlint-rules": "0.6.3",
|
|
36
41
|
"oxlint": "1.90.0",
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Generated by `mise run gen` from the elements' `@fires` docstrings. Don't edit.
|
|
2
|
+
|
|
3
|
+
/** The events each `gw-` element fires, by tag. `on()` reads it. */
|
|
4
|
+
export interface GwEventMap {
|
|
5
|
+
"gw-app-bar": {
|
|
6
|
+
toggle: Event;
|
|
7
|
+
};
|
|
8
|
+
"gw-command-menu": {
|
|
9
|
+
pick: CustomEvent<{ value: string }>;
|
|
10
|
+
};
|
|
11
|
+
"gw-dialog": {
|
|
12
|
+
close: Event;
|
|
13
|
+
};
|
|
14
|
+
"gw-expansion": {
|
|
15
|
+
toggle: Event;
|
|
16
|
+
};
|
|
17
|
+
"gw-menu-button": {
|
|
18
|
+
select: CustomEvent<{ value: string }>;
|
|
19
|
+
};
|
|
20
|
+
"gw-notification": {
|
|
21
|
+
dismiss: Event;
|
|
22
|
+
};
|
|
23
|
+
"gw-notification-center": {
|
|
24
|
+
clear: Event;
|
|
25
|
+
};
|
|
26
|
+
"gw-option-list": {
|
|
27
|
+
pick: CustomEvent<{ value: string }>;
|
|
28
|
+
};
|
|
29
|
+
"gw-popover": {
|
|
30
|
+
close: Event;
|
|
31
|
+
};
|
|
32
|
+
"gw-segmented": {
|
|
33
|
+
change: Event;
|
|
34
|
+
};
|
|
35
|
+
"gw-select": {
|
|
36
|
+
change: Event;
|
|
37
|
+
};
|
|
38
|
+
"gw-theme-picker": {
|
|
39
|
+
reset: Event;
|
|
40
|
+
mode: CustomEvent<{ mode: string }>;
|
|
41
|
+
theme: CustomEvent<{ id: string }>;
|
|
42
|
+
import: CustomEvent<{ text: string, name: string }>;
|
|
43
|
+
};
|
|
44
|
+
}
|
package/src/fields.ts
CHANGED
|
@@ -216,6 +216,40 @@ export const fieldStyles = css`
|
|
|
216
216
|
background: rgb(0 0 0 / 40%);
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
/* With multiple, a full-width Clear under the list empties the choice */
|
|
220
|
+
gw-select .gw-select-clear {
|
|
221
|
+
display: block;
|
|
222
|
+
width: 100%;
|
|
223
|
+
min-height: 2.5rem;
|
|
224
|
+
padding: 0 var(--gw-space-3);
|
|
225
|
+
font: inherit;
|
|
226
|
+
font-size: var(--gw-font-size-s);
|
|
227
|
+
font-weight: var(--gw-font-weight-strong);
|
|
228
|
+
color: var(--gw-color-text-secondary);
|
|
229
|
+
cursor: pointer;
|
|
230
|
+
background: none;
|
|
231
|
+
border: 0;
|
|
232
|
+
border-block-start: var(--gw-border-width) solid
|
|
233
|
+
var(--gw-color-border-quiet);
|
|
234
|
+
transition:
|
|
235
|
+
color var(--gw-motion-fast) var(--gw-motion-easing),
|
|
236
|
+
background-color var(--gw-motion-fast) var(--gw-motion-easing);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
gw-select .gw-select-clear:hover {
|
|
240
|
+
color: var(--gw-color-text-strong);
|
|
241
|
+
background: var(--gw-color-fill-neutral-hovered);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
gw-select .gw-select-clear:focus-visible {
|
|
245
|
+
outline: var(--gw-focus-width) solid var(--gw-color-focus);
|
|
246
|
+
outline-offset: calc(var(--gw-focus-width) * -1);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
gw-select .gw-select-clear[hidden] {
|
|
250
|
+
display: none;
|
|
251
|
+
}
|
|
252
|
+
|
|
219
253
|
:where(gw-input, gw-select) :where(input, .gw-select-trigger):hover {
|
|
220
254
|
border-color: var(--gw-color-border-loud);
|
|
221
255
|
}
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gw-app-bar`: a header pinned to the top, floating once the page scrolls.
|
|
3
|
+
* Docs: https://groundwork.timmo.dev/components/layout/#app-bar
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import { GwElement, css, h, setAttribute } from "./element.js";
|
|
8
|
+
import { base, createIcon, setIconPath } from "./styles.js";
|
|
9
|
+
|
|
10
|
+
// MDI's menu and close
|
|
11
|
+
const menuIcon = "M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z";
|
|
12
|
+
|
|
13
|
+
const closeIcon =
|
|
14
|
+
"M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z";
|
|
15
|
+
|
|
16
|
+
/** Below this width the nav moves into a menu. */
|
|
17
|
+
const menuBelow = 600;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A site or app header pinned to the top of the page, with a brand, nav
|
|
21
|
+
* links and actions, full width with its content kept to the content width.
|
|
22
|
+
* It's transparent at first, then gets a blurred surface and a bottom border
|
|
23
|
+
* once the page scrolls, or once the element
|
|
24
|
+
* `reveal-after` names scrolls out of view, such as a hero. With
|
|
25
|
+
* `reveal-after` the brand stays hidden until then, so a large logo in the
|
|
26
|
+
* hero hands over to the small one in the bar.
|
|
27
|
+
*
|
|
28
|
+
* `overlay` lays it over the top of the page rather than taking room, for a
|
|
29
|
+
* page that opens with a hero. Below 600px wide the nav moves into a menu
|
|
30
|
+
* opened from a button, which closes on Esc, a click outside or a link.
|
|
31
|
+
*
|
|
32
|
+
* @element gw-app-bar
|
|
33
|
+
* @summary A header pinned to the top, floating once the page scrolls.
|
|
34
|
+
*
|
|
35
|
+
* @attr {string} reveal-after - A CSS selector for the element, such as a hero, to scroll past before the bar floats and the brand shows.
|
|
36
|
+
* @attr {boolean} overlay - Lays the bar over the top of the page instead of taking room.
|
|
37
|
+
* @attr {boolean} floating - Whether it floats. Set by the bar as the page scrolls.
|
|
38
|
+
* @attr {boolean} menu-open - Whether the nav menu is open on narrow screens.
|
|
39
|
+
* @attr {string} menu-label - The menu button's accessible name. Defaults to `Menu`.
|
|
40
|
+
*
|
|
41
|
+
* @slot brand - A logo or name linking home.
|
|
42
|
+
* @slot - The nav links.
|
|
43
|
+
* @slot actions - Icon links and buttons at the end.
|
|
44
|
+
*
|
|
45
|
+
* @csspart bar - The bar.
|
|
46
|
+
* @csspart nav - The nav links' container, the menu panel on narrow screens.
|
|
47
|
+
*
|
|
48
|
+
* @fires {Event} toggle - When the nav menu opens or closes.
|
|
49
|
+
*/
|
|
50
|
+
export class GwAppBar extends GwElement {
|
|
51
|
+
static override properties = {
|
|
52
|
+
revealAfter: { attribute: "reveal-after" },
|
|
53
|
+
overlay: { type: "boolean", reflect: true },
|
|
54
|
+
floating: { type: "boolean", reflect: true },
|
|
55
|
+
menuOpen: { type: "boolean", attribute: "menu-open", reflect: true },
|
|
56
|
+
menuLabel: { attribute: "menu-label" },
|
|
57
|
+
} as const;
|
|
58
|
+
|
|
59
|
+
static override styles = [
|
|
60
|
+
base,
|
|
61
|
+
css`
|
|
62
|
+
:host {
|
|
63
|
+
position: sticky;
|
|
64
|
+
inset-block-start: 0;
|
|
65
|
+
z-index: 50;
|
|
66
|
+
display: block;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
:host([overlay]) {
|
|
70
|
+
position: fixed;
|
|
71
|
+
inset-inline: 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.bar {
|
|
75
|
+
position: relative;
|
|
76
|
+
display: flex;
|
|
77
|
+
gap: var(--gw-space-3);
|
|
78
|
+
align-items: center;
|
|
79
|
+
min-height: 3.5rem;
|
|
80
|
+
padding: var(--gw-space-1)
|
|
81
|
+
max(var(--gw-space-3), (100% - var(--gw-content-width)) / 2);
|
|
82
|
+
border-block-end: max(var(--gw-border-width), 1px) solid transparent;
|
|
83
|
+
transition:
|
|
84
|
+
background-color var(--gw-motion-normal) var(--gw-motion-easing),
|
|
85
|
+
border-color var(--gw-motion-normal) var(--gw-motion-easing),
|
|
86
|
+
backdrop-filter var(--gw-motion-normal) var(--gw-motion-easing);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
:host([floating]) .bar,
|
|
90
|
+
:host([menu-open]) .bar {
|
|
91
|
+
background: color-mix(
|
|
92
|
+
in oklab,
|
|
93
|
+
var(--gw-color-surface-default) 82%,
|
|
94
|
+
transparent
|
|
95
|
+
);
|
|
96
|
+
border-color: var(--gw-color-border-quiet);
|
|
97
|
+
backdrop-filter: blur(12px) saturate(1.4);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.brand {
|
|
101
|
+
display: flex;
|
|
102
|
+
flex: none;
|
|
103
|
+
align-items: center;
|
|
104
|
+
transition:
|
|
105
|
+
opacity var(--gw-motion-normal) var(--gw-motion-easing),
|
|
106
|
+
translate var(--gw-motion-normal) var(--gw-motion-easing);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
:host([reveal-after]:not([floating])) .brand {
|
|
110
|
+
pointer-events: none;
|
|
111
|
+
visibility: hidden;
|
|
112
|
+
opacity: 0;
|
|
113
|
+
translate: 0 -0.25rem;
|
|
114
|
+
transition:
|
|
115
|
+
opacity var(--gw-motion-normal) var(--gw-motion-easing),
|
|
116
|
+
translate var(--gw-motion-normal) var(--gw-motion-easing),
|
|
117
|
+
visibility var(--gw-motion-normal) allow-discrete;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.nav {
|
|
121
|
+
display: flex;
|
|
122
|
+
flex: 1;
|
|
123
|
+
gap: var(--gw-space-1);
|
|
124
|
+
align-items: center;
|
|
125
|
+
min-width: 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.actions {
|
|
129
|
+
display: flex;
|
|
130
|
+
flex: none;
|
|
131
|
+
gap: var(--gw-space-1);
|
|
132
|
+
align-items: center;
|
|
133
|
+
margin-inline-start: auto;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.menu {
|
|
137
|
+
display: none;
|
|
138
|
+
place-items: center;
|
|
139
|
+
width: 2.5rem;
|
|
140
|
+
height: 2.5rem;
|
|
141
|
+
padding: 0;
|
|
142
|
+
color: var(--gw-color-text-strong);
|
|
143
|
+
cursor: pointer;
|
|
144
|
+
background: none;
|
|
145
|
+
border: 0;
|
|
146
|
+
border-radius: var(--gw-radius-pill);
|
|
147
|
+
transition: background-color var(--gw-motion-fast)
|
|
148
|
+
var(--gw-motion-easing);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.menu:hover {
|
|
152
|
+
background: var(--gw-color-fill-neutral-hovered);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@media (max-width: ${menuBelow - 0.02}px) {
|
|
156
|
+
.menu {
|
|
157
|
+
display: inline-grid;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.nav {
|
|
161
|
+
position: absolute;
|
|
162
|
+
inset-block-start: 100%;
|
|
163
|
+
inset-inline: 0;
|
|
164
|
+
flex-direction: column;
|
|
165
|
+
gap: var(--gw-space-0-5);
|
|
166
|
+
align-items: stretch;
|
|
167
|
+
padding: var(--gw-space-2) var(--gw-space-3);
|
|
168
|
+
visibility: hidden;
|
|
169
|
+
background: var(--gw-color-surface-default);
|
|
170
|
+
border-block-end: max(var(--gw-border-width), 1px) solid
|
|
171
|
+
var(--gw-color-border-quiet);
|
|
172
|
+
box-shadow: var(--gw-shadow-m);
|
|
173
|
+
opacity: 0;
|
|
174
|
+
translate: 0 -0.5rem;
|
|
175
|
+
transition:
|
|
176
|
+
opacity var(--gw-motion-fast) var(--gw-motion-easing),
|
|
177
|
+
translate var(--gw-motion-normal) var(--gw-motion-easing),
|
|
178
|
+
visibility var(--gw-motion-normal) allow-discrete;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
:host([menu-open]) .nav {
|
|
182
|
+
visibility: visible;
|
|
183
|
+
opacity: 1;
|
|
184
|
+
translate: 0 0;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
`,
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
declare revealAfter: string;
|
|
191
|
+
declare overlay: boolean;
|
|
192
|
+
declare floating: boolean;
|
|
193
|
+
declare menuOpen: boolean;
|
|
194
|
+
declare menuLabel: string;
|
|
195
|
+
|
|
196
|
+
readonly #menuIcon = createIcon(menuIcon);
|
|
197
|
+
|
|
198
|
+
readonly #menu = h(
|
|
199
|
+
"button",
|
|
200
|
+
{ type: "button", class: "menu", "aria-expanded": "false" },
|
|
201
|
+
this.#menuIcon,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
readonly #nav = h("div", { class: "nav", part: "nav" }, h("slot"));
|
|
205
|
+
|
|
206
|
+
#observer: IntersectionObserver | undefined;
|
|
207
|
+
|
|
208
|
+
#frame = 0;
|
|
209
|
+
|
|
210
|
+
constructor() {
|
|
211
|
+
super();
|
|
212
|
+
this.revealAfter = "";
|
|
213
|
+
this.overlay = false;
|
|
214
|
+
this.floating = false;
|
|
215
|
+
this.menuOpen = false;
|
|
216
|
+
this.menuLabel = "Menu";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
override connectedCallback() {
|
|
220
|
+
super.connectedCallback();
|
|
221
|
+
document.addEventListener("keydown", this.#onKeydown);
|
|
222
|
+
document.addEventListener("pointerdown", this.#onOutside, true);
|
|
223
|
+
|
|
224
|
+
if (document.readyState === "loading") {
|
|
225
|
+
document.addEventListener("DOMContentLoaded", () => this.#watch(), {
|
|
226
|
+
once: true,
|
|
227
|
+
});
|
|
228
|
+
} else {
|
|
229
|
+
this.#watch();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
override disconnectedCallback() {
|
|
234
|
+
this.#unwatch();
|
|
235
|
+
document.removeEventListener("keydown", this.#onKeydown);
|
|
236
|
+
document.removeEventListener("pointerdown", this.#onOutside, true);
|
|
237
|
+
super.disconnectedCallback();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Opens or closes the nav menu on narrow screens. */
|
|
241
|
+
toggleMenu(open = !this.menuOpen) {
|
|
242
|
+
if (open === this.menuOpen) return;
|
|
243
|
+
|
|
244
|
+
this.menuOpen = open;
|
|
245
|
+
this.dispatchEvent(new Event("toggle"));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
readonly #onScroll = () => {
|
|
249
|
+
if (this.#frame !== 0) return;
|
|
250
|
+
|
|
251
|
+
this.#frame = requestAnimationFrame(() => {
|
|
252
|
+
this.#frame = 0;
|
|
253
|
+
this.floating = window.scrollY > 0;
|
|
254
|
+
});
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
readonly #onKeydown = (event: KeyboardEvent) => {
|
|
258
|
+
if (event.key === "Escape" && this.menuOpen) {
|
|
259
|
+
this.toggleMenu(false);
|
|
260
|
+
this.#menu.focus();
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
readonly #onOutside = (event: PointerEvent) => {
|
|
265
|
+
if (this.menuOpen && !event.composedPath().includes(this)) {
|
|
266
|
+
this.toggleMenu(false);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
#unwatch() {
|
|
271
|
+
this.#observer?.disconnect();
|
|
272
|
+
this.#observer = undefined;
|
|
273
|
+
window.removeEventListener("scroll", this.#onScroll);
|
|
274
|
+
cancelAnimationFrame(this.#frame);
|
|
275
|
+
this.#frame = 0;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Floats past the `reveal-after` element, or once the page scrolls without one. */
|
|
279
|
+
#watch() {
|
|
280
|
+
this.#unwatch();
|
|
281
|
+
|
|
282
|
+
const target = this.revealAfter
|
|
283
|
+
? document.querySelector(this.revealAfter)
|
|
284
|
+
: null;
|
|
285
|
+
|
|
286
|
+
if (target === null) {
|
|
287
|
+
window.addEventListener("scroll", this.#onScroll, { passive: true });
|
|
288
|
+
this.floating = window.scrollY > 0;
|
|
289
|
+
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// The bar floats once the target's bottom passes under the bar
|
|
294
|
+
this.#observer = new IntersectionObserver(
|
|
295
|
+
([entry]) => {
|
|
296
|
+
if (entry !== undefined) this.floating = !entry.isIntersecting;
|
|
297
|
+
},
|
|
298
|
+
{ rootMargin: `-${this.offsetHeight}px 0px 0px 0px` },
|
|
299
|
+
);
|
|
300
|
+
this.#observer.observe(target);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
protected override create(root: ShadowRoot) {
|
|
304
|
+
this.#menu.addEventListener("click", () => {
|
|
305
|
+
this.toggleMenu();
|
|
306
|
+
});
|
|
307
|
+
// A link picked from the menu closes it
|
|
308
|
+
this.#nav.addEventListener("click", (event) => {
|
|
309
|
+
if (
|
|
310
|
+
event.composedPath().some((node) => node instanceof HTMLAnchorElement)
|
|
311
|
+
) {
|
|
312
|
+
this.toggleMenu(false);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
root.append(
|
|
317
|
+
h(
|
|
318
|
+
"div",
|
|
319
|
+
{ class: "bar", part: "bar" },
|
|
320
|
+
h("div", { class: "brand" }, h("slot", { name: "brand" })),
|
|
321
|
+
this.#menu,
|
|
322
|
+
this.#nav,
|
|
323
|
+
h("div", { class: "actions" }, h("slot", { name: "actions" })),
|
|
324
|
+
),
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
protected override update(changed: ReadonlySet<string>) {
|
|
329
|
+
setAttribute(this.#menu, "aria-label", this.menuLabel);
|
|
330
|
+
setAttribute(this.#menu, "aria-expanded", this.menuOpen ? "true" : "false");
|
|
331
|
+
setIconPath(this.#menuIcon, this.menuOpen ? closeIcon : menuIcon);
|
|
332
|
+
|
|
333
|
+
if (changed.has("revealAfter") && this.isConnected) this.#watch();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
GwAppBar.define("gw-app-bar");
|
|
338
|
+
|
|
339
|
+
declare global {
|
|
340
|
+
interface HTMLElementTagNameMap {
|
|
341
|
+
"gw-app-bar": GwAppBar;
|
|
342
|
+
}
|
|
343
|
+
}
|
package/src/gw-command-menu.ts
CHANGED
|
@@ -28,7 +28,7 @@ const sheetBelow = 600;
|
|
|
28
28
|
* @attr {string} shortcut - The key that opens it with Cmd or Ctrl, such as `mod+k`, or `none`. Defaults to `mod+k`.
|
|
29
29
|
* @attr {boolean} open - Whether it shows.
|
|
30
30
|
*
|
|
31
|
-
* @fires pick - When a command is picked, with its `value` in `detail`.
|
|
31
|
+
* @fires {CustomEvent<{ value: string }>} pick - When a command is picked, with its `value` in `detail`.
|
|
32
32
|
*/
|
|
33
33
|
export class GwCommandMenu extends GwElement {
|
|
34
34
|
static override properties = {
|
package/src/gw-dialog.ts
CHANGED
|
@@ -28,7 +28,7 @@ const closeIcon =
|
|
|
28
28
|
* @slot - The content.
|
|
29
29
|
* @slot actions - Buttons; one with a `value` closes it with that value.
|
|
30
30
|
*
|
|
31
|
-
* @fires close - When it closes; `returnValue` holds the button's value.
|
|
31
|
+
* @fires {Event} close - When it closes; `returnValue` holds the button's value.
|
|
32
32
|
*/
|
|
33
33
|
export class GwDialog extends GwElement {
|
|
34
34
|
static override properties = {
|
package/src/gw-expansion.ts
CHANGED
|
@@ -27,7 +27,7 @@ import { base } from "./styles.js";
|
|
|
27
27
|
* @csspart summary - The summary row.
|
|
28
28
|
* @csspart content - The content's wrapper.
|
|
29
29
|
*
|
|
30
|
-
* @fires toggle - When it opens or closes.
|
|
30
|
+
* @fires {Event} toggle - When it opens or closes.
|
|
31
31
|
*/
|
|
32
32
|
export class GwExpansion extends GwElement {
|
|
33
33
|
static override properties = {
|
package/src/gw-menu-button.ts
CHANGED
|
@@ -41,7 +41,7 @@ export interface MenuItem {
|
|
|
41
41
|
*
|
|
42
42
|
* @csspart trigger - The button that opens the menu.
|
|
43
43
|
*
|
|
44
|
-
* @fires select - When an item or action is chosen, with its `value` in `detail`.
|
|
44
|
+
* @fires {CustomEvent<{ value: string }>} select - When an item or action is chosen, with its `value` in `detail`.
|
|
45
45
|
*/
|
|
46
46
|
export class GwMenuButton extends GwElement {
|
|
47
47
|
static override properties = {
|
|
@@ -29,7 +29,7 @@ const emptyIcon =
|
|
|
29
29
|
*
|
|
30
30
|
* @slot - The `gw-notification` cards.
|
|
31
31
|
*
|
|
32
|
-
* @fires clear - When Clear all is pressed.
|
|
32
|
+
* @fires {Event} clear - When Clear all is pressed.
|
|
33
33
|
*/
|
|
34
34
|
export class GwNotificationCenter extends GwElement {
|
|
35
35
|
static override properties = {
|
package/src/gw-notification.ts
CHANGED
|
@@ -34,7 +34,7 @@ const closeIcon =
|
|
|
34
34
|
*
|
|
35
35
|
* @csspart card - The card.
|
|
36
36
|
*
|
|
37
|
-
* @fires dismiss - When the close button is pressed. Cancel it to keep the card.
|
|
37
|
+
* @fires {Event} dismiss - When the close button is pressed. Cancel it to keep the card.
|
|
38
38
|
*/
|
|
39
39
|
export class GwNotification extends GwElement {
|
|
40
40
|
static override properties = {
|
package/src/gw-option-list.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface OptionItem {
|
|
|
22
22
|
readonly keywords?: readonly string[];
|
|
23
23
|
/** Colors shown as small swatches, such as a theme's palette. */
|
|
24
24
|
readonly swatches?: readonly string[];
|
|
25
|
+
/** One color shown as a dot before the label, such as a tag's color. */
|
|
26
|
+
readonly swatch?: string;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
// MDI's magnify and check
|
|
@@ -65,23 +67,30 @@ let lastList = 0;
|
|
|
65
67
|
* End and Page Up or Down, and pick with Enter or a click; it fires `pick`
|
|
66
68
|
* with the option's value. Call `reset()` each time it's shown.
|
|
67
69
|
*
|
|
70
|
+
* With `multiple`, every value in `values` is marked as chosen and the list
|
|
71
|
+
* is announced as multi-select; the owner toggles `values` on each `pick`.
|
|
72
|
+
*
|
|
68
73
|
* @element gw-option-list
|
|
69
74
|
* @summary A searchable, windowed list of options.
|
|
70
75
|
*
|
|
71
76
|
* @attr {OptionItem[]} options - The options, as JSON.
|
|
72
77
|
* @attr {string} value - The selected option's value.
|
|
78
|
+
* @attr {boolean} multiple - Marks every value in `values` as chosen, for picking several.
|
|
79
|
+
* @attr {string[]} values - The chosen values with `multiple`, as JSON.
|
|
73
80
|
* @attr {string} label - The list's accessible name.
|
|
74
81
|
* @attr {boolean} search - Shows a search field above the list.
|
|
75
82
|
* @attr {string} placeholder - The search placeholder. Defaults to `Search`.
|
|
76
83
|
*
|
|
77
84
|
* @cssprop --gw-option-list-height - The list's maximum height. Defaults to `20rem`.
|
|
78
85
|
*
|
|
79
|
-
* @fires pick - When an option is picked, with its `value` in `detail`.
|
|
86
|
+
* @fires {CustomEvent<{ value: string }>} pick - When an option is picked, with its `value` in `detail`.
|
|
80
87
|
*/
|
|
81
88
|
export class GwOptionList extends GwElement {
|
|
82
89
|
static override properties = {
|
|
83
90
|
options: { type: "json" },
|
|
84
91
|
value: {},
|
|
92
|
+
multiple: { type: "boolean" },
|
|
93
|
+
values: { type: "json" },
|
|
85
94
|
label: {},
|
|
86
95
|
search: { type: "boolean", reflect: true },
|
|
87
96
|
placeholder: {},
|
|
@@ -211,7 +220,8 @@ export class GwOptionList extends GwElement {
|
|
|
211
220
|
flex: none;
|
|
212
221
|
}
|
|
213
222
|
|
|
214
|
-
.swatches span
|
|
223
|
+
.swatches span,
|
|
224
|
+
.swatch {
|
|
215
225
|
width: 0.875rem;
|
|
216
226
|
height: 0.875rem;
|
|
217
227
|
margin-inline-start: -0.25rem;
|
|
@@ -220,6 +230,13 @@ export class GwOptionList extends GwElement {
|
|
|
220
230
|
border-radius: 50%;
|
|
221
231
|
}
|
|
222
232
|
|
|
233
|
+
.swatch {
|
|
234
|
+
flex: none;
|
|
235
|
+
width: 0.625rem;
|
|
236
|
+
height: 0.625rem;
|
|
237
|
+
margin-inline: 0.125rem;
|
|
238
|
+
}
|
|
239
|
+
|
|
223
240
|
.none {
|
|
224
241
|
padding: var(--gw-space-4);
|
|
225
242
|
font-size: var(--gw-font-size-s);
|
|
@@ -235,6 +252,8 @@ export class GwOptionList extends GwElement {
|
|
|
235
252
|
|
|
236
253
|
declare options: readonly OptionItem[];
|
|
237
254
|
declare value: string;
|
|
255
|
+
declare multiple: boolean;
|
|
256
|
+
declare values: readonly string[];
|
|
238
257
|
declare label: string;
|
|
239
258
|
declare search: boolean;
|
|
240
259
|
declare placeholder: string;
|
|
@@ -276,11 +295,19 @@ export class GwOptionList extends GwElement {
|
|
|
276
295
|
super();
|
|
277
296
|
this.options = [];
|
|
278
297
|
this.value = "";
|
|
298
|
+
this.multiple = false;
|
|
299
|
+
this.values = [];
|
|
279
300
|
this.label = "";
|
|
280
301
|
this.search = false;
|
|
281
302
|
this.placeholder = "Search";
|
|
282
303
|
}
|
|
283
304
|
|
|
305
|
+
#isChosen(option: OptionItem) {
|
|
306
|
+
return this.multiple
|
|
307
|
+
? Array.isArray(this.values) && this.values.includes(option.value)
|
|
308
|
+
: option.value === this.value;
|
|
309
|
+
}
|
|
310
|
+
|
|
284
311
|
/** The options left after the search, in order. */
|
|
285
312
|
get filtered() {
|
|
286
313
|
return this.#filtered;
|
|
@@ -296,7 +323,7 @@ export class GwOptionList extends GwElement {
|
|
|
296
323
|
this.#build();
|
|
297
324
|
this.#active = Math.max(
|
|
298
325
|
0,
|
|
299
|
-
this.#filtered.findIndex((option) => option
|
|
326
|
+
this.#filtered.findIndex((option) => this.#isChosen(option)),
|
|
300
327
|
);
|
|
301
328
|
(this.search ? this.#input : this.#listbox).focus({ preventScroll: true });
|
|
302
329
|
this.#reveal(true);
|
|
@@ -420,8 +447,18 @@ export class GwOptionList extends GwElement {
|
|
|
420
447
|
setAttribute(this.#input, "placeholder", this.placeholder);
|
|
421
448
|
setAttribute(this.#input, "aria-label", this.label || this.placeholder);
|
|
422
449
|
setAttribute(this.#listbox, "aria-label", this.label || null);
|
|
450
|
+
setAttribute(
|
|
451
|
+
this.#listbox,
|
|
452
|
+
"aria-multiselectable",
|
|
453
|
+
this.multiple ? "true" : null,
|
|
454
|
+
);
|
|
423
455
|
|
|
424
|
-
if (
|
|
456
|
+
if (
|
|
457
|
+
changed.has("options") ||
|
|
458
|
+
changed.has("value") ||
|
|
459
|
+
changed.has("values") ||
|
|
460
|
+
changed.has("multiple")
|
|
461
|
+
) {
|
|
425
462
|
this.#build();
|
|
426
463
|
this.#render();
|
|
427
464
|
}
|
|
@@ -559,7 +596,7 @@ export class GwOptionList extends GwElement {
|
|
|
559
596
|
}
|
|
560
597
|
|
|
561
598
|
const { option, index } = row;
|
|
562
|
-
const selected = option
|
|
599
|
+
const selected = this.#isChosen(option);
|
|
563
600
|
|
|
564
601
|
const element = h(
|
|
565
602
|
"div",
|
|
@@ -574,6 +611,7 @@ export class GwOptionList extends GwElement {
|
|
|
574
611
|
"data-active": index === this.#active,
|
|
575
612
|
},
|
|
576
613
|
option.icon === undefined ? null : createIcon(option.icon),
|
|
614
|
+
option.swatch === undefined ? null : this.#swatch(option.swatch),
|
|
577
615
|
h(
|
|
578
616
|
"span",
|
|
579
617
|
{ class: "text" },
|
|
@@ -605,6 +643,14 @@ export class GwOptionList extends GwElement {
|
|
|
605
643
|
|
|
606
644
|
return element;
|
|
607
645
|
}
|
|
646
|
+
|
|
647
|
+
#swatch(color: string) {
|
|
648
|
+
const swatch = h("span", { class: "swatch", "aria-hidden": "true" });
|
|
649
|
+
|
|
650
|
+
swatch.style.background = color;
|
|
651
|
+
|
|
652
|
+
return swatch;
|
|
653
|
+
}
|
|
608
654
|
}
|
|
609
655
|
|
|
610
656
|
GwOptionList.define("gw-option-list");
|
package/src/gw-popover.ts
CHANGED
|
@@ -38,7 +38,7 @@ const sheetBelow = 600;
|
|
|
38
38
|
* @slot - The content.
|
|
39
39
|
* @slot actions - Buttons; one with a `value` closes it with that value.
|
|
40
40
|
*
|
|
41
|
-
* @fires close - When it closes; `returnValue` holds the button's value.
|
|
41
|
+
* @fires {Event} close - When it closes; `returnValue` holds the button's value.
|
|
42
42
|
*/
|
|
43
43
|
export class GwPopover extends GwElement {
|
|
44
44
|
static override properties = {
|
package/src/gw-segmented.ts
CHANGED
|
@@ -34,7 +34,7 @@ export interface Segment {
|
|
|
34
34
|
*
|
|
35
35
|
* @csspart indicator - The pill that slides to the chosen option.
|
|
36
36
|
*
|
|
37
|
-
* @fires change - When the chosen option changes.
|
|
37
|
+
* @fires {Event} change - When the chosen option changes.
|
|
38
38
|
*/
|
|
39
39
|
export class GwSegmented extends GwElement {
|
|
40
40
|
static override properties = {
|