@timmo001/groundwork-ui 0.4.1 → 0.6.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/custom-elements.json +1027 -18
- package/package.json +2 -2
- package/src/events.ts +3 -0
- package/src/gw-alert.ts +4 -0
- package/src/gw-app-bar.ts +106 -42
- package/src/gw-badge.ts +11 -2
- package/src/gw-banner.ts +4 -0
- package/src/gw-button.ts +91 -12
- package/src/gw-card.ts +6 -3
- package/src/gw-chip.ts +3 -0
- package/src/gw-command-menu.ts +7 -3
- package/src/gw-dialog.ts +15 -5
- package/src/gw-dot.ts +2 -0
- package/src/gw-expansion.ts +7 -2
- package/src/gw-facts.ts +13 -5
- package/src/gw-gauge.ts +11 -5
- package/src/gw-meter.ts +17 -4
- package/src/gw-notification-center.ts +14 -3
- package/src/gw-notification.ts +2 -0
- package/src/gw-option-list.ts +26 -5
- package/src/gw-page-header.ts +7 -3
- package/src/gw-popover.ts +17 -5
- package/src/gw-section-title.ts +3 -1
- package/src/gw-segmented.ts +2 -1
- package/src/gw-skeleton.ts +3 -1
- package/src/gw-spinner.ts +5 -1
- package/src/gw-theme-gallery.ts +493 -0
- package/src/gw-theme-picker.ts +187 -34
- package/src/gw-toast.ts +3 -1
- package/src/index.ts +2 -0
package/src/gw-theme-picker.ts
CHANGED
|
@@ -10,17 +10,29 @@ import type { OptionItem } from "./gw-option-list.js";
|
|
|
10
10
|
import "./gw-popover.js";
|
|
11
11
|
import "./gw-segmented.js";
|
|
12
12
|
import "./gw-select.js";
|
|
13
|
+
import "./gw-theme-gallery.js";
|
|
13
14
|
import { base } from "./styles.js";
|
|
14
15
|
|
|
15
16
|
// MDI's palette
|
|
16
17
|
const paletteIcon =
|
|
17
18
|
"M17.5,12A1.5,1.5 0 0,1 16,10.5A1.5,1.5 0 0,1 17.5,9A1.5,1.5 0 0,1 19,10.5A1.5,1.5 0 0,1 17.5,12M14.5,8A1.5,1.5 0 0,1 13,6.5A1.5,1.5 0 0,1 14.5,5A1.5,1.5 0 0,1 16,6.5A1.5,1.5 0 0,1 14.5,8M9.5,8A1.5,1.5 0 0,1 8,6.5A1.5,1.5 0 0,1 9.5,5A1.5,1.5 0 0,1 11,6.5A1.5,1.5 0 0,1 9.5,8M6.5,12A1.5,1.5 0 0,1 5,10.5A1.5,1.5 0 0,1 6.5,9A1.5,1.5 0 0,1 8,10.5A1.5,1.5 0 0,1 6.5,12M12,3A9,9 0 0,0 3,12A9,9 0 0,0 12,21A1.5,1.5 0 0,0 13.5,19.5C13.5,19.11 13.35,18.76 13.11,18.5C12.88,18.23 12.73,17.88 12.73,17.5A1.5,1.5 0 0,1 14.23,16H16A5,5 0 0,0 21,11C21,6.58 16.97,3 12,3Z";
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
// MDI's sunny, moon and night (a moon with stars), for cycle's button. Auto
|
|
21
|
+
// keeps the palette
|
|
22
|
+
const modeIcons = {
|
|
23
|
+
auto: paletteIcon,
|
|
24
|
+
light:
|
|
25
|
+
"M12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9M12,2L14.39,5.42C13.65,5.15 12.84,5 12,5C11.16,5 10.35,5.15 9.61,5.42L12,2M3.34,7L7.5,6.65C6.9,7.16 6.36,7.78 5.94,8.5C5.5,9.24 5.25,10 5.11,10.79L3.34,7M3.36,17L5.12,13.23C5.26,14 5.53,14.78 5.95,15.5C6.37,16.24 6.91,16.86 7.5,17.37L3.36,17M20.65,7L18.88,10.79C18.74,10 18.47,9.23 18.05,8.5C17.63,7.78 17.1,7.15 16.5,6.64L20.65,7M20.64,17L16.5,17.36C17.09,16.85 17.62,16.22 18.04,15.5C18.46,14.77 18.73,14 18.87,13.21L20.64,17M12,22L9.59,18.56C10.33,18.83 11.14,19 12,19C12.82,19 13.63,18.83 14.37,18.56L12,22Z",
|
|
26
|
+
dark: "M2 12A10 10 0 0 0 15 21.54A10 10 0 0 1 15 2.46A10 10 0 0 0 2 12Z",
|
|
27
|
+
darker:
|
|
28
|
+
"M17.75,4.09L15.22,6.03L16.13,9.09L13.5,7.28L10.87,9.09L11.78,6.03L9.25,4.09L12.44,4L13.5,1L14.56,4L17.75,4.09M21.25,11L19.61,12.25L20.2,14.23L18.5,13.06L16.8,14.23L17.39,12.25L15.75,11L17.81,10.95L18.5,9L19.19,10.95L21.25,11M18.97,15.95C19.8,15.87 20.69,17.05 20.16,17.8C19.84,18.25 19.5,18.67 19.08,19.07C15.17,23 8.84,23 4.94,19.07C1.03,15.17 1.03,8.83 4.94,4.93C5.34,4.53 5.76,4.17 6.21,3.85C6.96,3.32 8.14,4.21 8.06,5.04C7.79,7.9 8.75,10.87 10.95,13.06C13.14,15.26 16.1,16.22 18.97,15.95M17.33,17.97C14.5,17.81 11.7,16.64 9.53,14.5C7.36,12.31 6.2,9.5 6.04,6.68C3.23,9.82 3.34,14.64 6.35,17.66C9.37,20.67 14.19,20.78 17.33,17.97Z",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const allModes = [
|
|
32
|
+
{ value: "auto", label: "Auto", icon: modeIcons.auto },
|
|
33
|
+
{ value: "light", label: "Light", icon: modeIcons.light },
|
|
34
|
+
{ value: "dark", label: "Dark", icon: modeIcons.dark },
|
|
35
|
+
{ value: "darker", label: "Darker", icon: modeIcons.darker },
|
|
24
36
|
] as const;
|
|
25
37
|
|
|
26
38
|
/**
|
|
@@ -33,14 +45,31 @@ const modes = [
|
|
|
33
45
|
* `@timmo001/groundwork-tokens/page` does all of that. Show an import's
|
|
34
46
|
* result, such as contrast misses, in `status`.
|
|
35
47
|
*
|
|
48
|
+
* `gallery` swaps the theme select for `gw-theme-gallery`'s cards, which
|
|
49
|
+
* preview each theme in the chosen mode.
|
|
50
|
+
*
|
|
51
|
+
* `layout` sets how it shows. `popover`, the default, is the palette button
|
|
52
|
+
* and its popover. `cycle` is one button showing the mode in use, which steps
|
|
53
|
+
* to the next mode on each press, for an app bar. `inline` lays the controls
|
|
54
|
+
* out in place, in one wrapping row with Reset and Import, for a footer or a
|
|
55
|
+
* settings page; set `justify-content` on it to align the row. `modes` picks which modes it offers, such as `light dark` without
|
|
56
|
+
* Auto.
|
|
57
|
+
*
|
|
36
58
|
* @element gw-theme-picker
|
|
37
59
|
* @summary A button opening the page's mode, theme and import.
|
|
38
60
|
*
|
|
61
|
+
* @attr {"popover"|"cycle"|"inline"} layout - How it shows: a button and popover, a button cycling the modes, or the controls in place. Defaults to `popover`.
|
|
62
|
+
* @attr {string} modes - The modes to offer, in order and separated by spaces. Defaults to `auto light dark darker`.
|
|
39
63
|
* @attr {"auto"|"light"|"dark"|"darker"} mode - The mode in use. Defaults to `auto`.
|
|
40
64
|
* @attr {string} theme - The theme in use, by id.
|
|
41
|
-
* @attr {OptionItem[]} themes - The themes to offer, as JSON.
|
|
65
|
+
* @attr {OptionItem[]} themes - The themes to offer, as JSON. The select hides with fewer than two. With `gallery`, a `group` gathers a family's themes.
|
|
66
|
+
* @attr {boolean} gallery - Shows the themes as preview cards rather than a select.
|
|
42
67
|
* @attr {string} status - A line under the controls, such as an import's result.
|
|
43
|
-
* @attr {string} label - The button's accessible name and the popover's heading. Defaults to `Theme`.
|
|
68
|
+
* @attr {string} label - The button's accessible name and the popover's heading. Defaults to `Theme`. With `cycle`, the button's name also gives the mode in use.
|
|
69
|
+
*
|
|
70
|
+
* @csspart gallery - The theme gallery, with `gallery`.
|
|
71
|
+
* @csspart controls - The mode, theme and status.
|
|
72
|
+
* @csspart actions - Reset and Import, with `inline`.
|
|
44
73
|
*
|
|
45
74
|
* @fires {CustomEvent<{ mode: string }>} mode - When a mode is picked.
|
|
46
75
|
* @fires {CustomEvent<{ id: string }>} theme - When a theme is picked.
|
|
@@ -52,8 +81,11 @@ export class GwThemePicker extends GwElement {
|
|
|
52
81
|
mode: { reflect: true },
|
|
53
82
|
theme: { reflect: true },
|
|
54
83
|
themes: { type: "json" },
|
|
84
|
+
gallery: { type: "boolean", reflect: true },
|
|
55
85
|
status: {},
|
|
56
86
|
label: {},
|
|
87
|
+
layout: { reflect: true },
|
|
88
|
+
modes: {},
|
|
57
89
|
} as const;
|
|
58
90
|
|
|
59
91
|
static override styles = [
|
|
@@ -63,6 +95,18 @@ export class GwThemePicker extends GwElement {
|
|
|
63
95
|
display: inline-block;
|
|
64
96
|
}
|
|
65
97
|
|
|
98
|
+
:host([gallery]) gw-popover {
|
|
99
|
+
--gw-popover-width: 34rem;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.gallery {
|
|
103
|
+
max-height: min(24rem, 50vh);
|
|
104
|
+
overflow: auto;
|
|
105
|
+
/* Room for the cards' focus rings */
|
|
106
|
+
padding: var(--gw-space-1);
|
|
107
|
+
margin: calc(var(--gw-space-1) * -1);
|
|
108
|
+
}
|
|
109
|
+
|
|
66
110
|
.controls {
|
|
67
111
|
display: flex;
|
|
68
112
|
flex-direction: column;
|
|
@@ -75,14 +119,41 @@ export class GwThemePicker extends GwElement {
|
|
|
75
119
|
font-size: var(--gw-font-size-s);
|
|
76
120
|
color: var(--gw-color-text-secondary);
|
|
77
121
|
}
|
|
122
|
+
|
|
123
|
+
/* One wrapping row: the controls, then Reset and Import. The host is
|
|
124
|
+
the flex container, so a page can set its justify-content */
|
|
125
|
+
:host([layout="inline"]) {
|
|
126
|
+
display: flex;
|
|
127
|
+
flex-wrap: wrap;
|
|
128
|
+
gap: var(--gw-space-4);
|
|
129
|
+
align-items: center;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
:host([layout="inline"]) .controls {
|
|
133
|
+
flex-flow: row wrap;
|
|
134
|
+
align-items: center;
|
|
135
|
+
margin-block-start: 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.actions {
|
|
139
|
+
display: flex;
|
|
140
|
+
flex-wrap: wrap;
|
|
141
|
+
gap: var(--gw-space-2);
|
|
142
|
+
align-items: center;
|
|
143
|
+
}
|
|
78
144
|
`,
|
|
79
145
|
];
|
|
80
146
|
|
|
81
147
|
declare mode: string;
|
|
82
148
|
declare theme: string;
|
|
83
149
|
declare themes: readonly OptionItem[];
|
|
150
|
+
declare gallery: boolean;
|
|
84
151
|
declare status: string;
|
|
85
152
|
declare label: string;
|
|
153
|
+
/** How it shows: a button and popover, a button cycling the modes, or the controls in place. */
|
|
154
|
+
declare layout: "popover" | "cycle" | "inline";
|
|
155
|
+
/** The modes to offer, in order and separated by spaces. */
|
|
156
|
+
declare modes: string;
|
|
86
157
|
|
|
87
158
|
readonly #button = h("gw-button", {
|
|
88
159
|
variant: "neutral",
|
|
@@ -95,6 +166,11 @@ export class GwThemePicker extends GwElement {
|
|
|
95
166
|
|
|
96
167
|
readonly #theme = h("gw-select", { label: "Theme" });
|
|
97
168
|
|
|
169
|
+
readonly #gallery = h("gw-theme-gallery", {
|
|
170
|
+
class: "gallery",
|
|
171
|
+
part: "gallery",
|
|
172
|
+
});
|
|
173
|
+
|
|
98
174
|
readonly #status = h("p", { class: "status", role: "status" });
|
|
99
175
|
|
|
100
176
|
readonly #file = h("input", {
|
|
@@ -103,39 +179,87 @@ export class GwThemePicker extends GwElement {
|
|
|
103
179
|
hidden: true,
|
|
104
180
|
});
|
|
105
181
|
|
|
106
|
-
readonly #
|
|
107
|
-
"
|
|
108
|
-
{},
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
),
|
|
120
|
-
h(
|
|
121
|
-
"gw-button",
|
|
122
|
-
{ slot: "actions", appearance: "outlined", class: "import" },
|
|
123
|
-
"Import",
|
|
124
|
-
),
|
|
182
|
+
readonly #controls = h(
|
|
183
|
+
"div",
|
|
184
|
+
{ class: "controls", part: "controls" },
|
|
185
|
+
this.#mode,
|
|
186
|
+
this.#theme,
|
|
187
|
+
this.#gallery,
|
|
188
|
+
this.#status,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
readonly #reset = h(
|
|
192
|
+
"gw-button",
|
|
193
|
+
{ variant: "neutral", appearance: "plain" },
|
|
194
|
+
"Reset",
|
|
125
195
|
);
|
|
126
196
|
|
|
197
|
+
readonly #import = h("gw-button", { appearance: "outlined" }, "Import");
|
|
198
|
+
|
|
199
|
+
readonly #popover = h("gw-popover");
|
|
200
|
+
|
|
201
|
+
readonly #actions = h("div", { class: "actions", part: "actions" });
|
|
202
|
+
|
|
127
203
|
constructor() {
|
|
128
204
|
super();
|
|
129
205
|
this.mode = "auto";
|
|
130
206
|
this.theme = "";
|
|
131
207
|
this.themes = [];
|
|
208
|
+
this.gallery = false;
|
|
132
209
|
this.status = "";
|
|
133
210
|
this.label = "Theme";
|
|
211
|
+
this.layout = "popover";
|
|
212
|
+
this.modes = "auto light dark darker";
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** The modes on offer, in the order `modes` gives them. */
|
|
216
|
+
#offered() {
|
|
217
|
+
const wanted = String(this.modes).split(/\s+/);
|
|
218
|
+
const offered = allModes.filter(({ value }) => wanted.includes(value));
|
|
219
|
+
|
|
220
|
+
return offered.length > 0 ? offered : allModes;
|
|
134
221
|
}
|
|
135
222
|
|
|
136
|
-
/** Opens the popover under the button. */
|
|
223
|
+
/** Opens the popover under the button, or with `cycle`, steps to the next mode. */
|
|
137
224
|
show() {
|
|
225
|
+
if (this.layout === "inline") return;
|
|
226
|
+
|
|
227
|
+
if (this.layout === "cycle") {
|
|
228
|
+
const offered = this.#offered();
|
|
229
|
+
const at = offered.findIndex(({ value }) => value === this.mode);
|
|
230
|
+
const next = offered[(at + 1) % offered.length];
|
|
231
|
+
|
|
232
|
+
if (next !== undefined) {
|
|
233
|
+
this.mode = next.value;
|
|
234
|
+
this.dispatchEvent(this.#detail("mode", { mode: this.mode }));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
138
240
|
this.#popover.show(this.#button);
|
|
241
|
+
|
|
242
|
+
// A custom theme's CSS may have changed since the cards were drawn
|
|
243
|
+
if (this.gallery) this.#gallery.refresh();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Puts the controls and their buttons in the popover, or in place for `inline`. */
|
|
247
|
+
#place(root: ShadowRoot) {
|
|
248
|
+
const inline = this.layout === "inline";
|
|
249
|
+
|
|
250
|
+
this.#reset.slot = inline ? "" : "actions";
|
|
251
|
+
this.#import.slot = inline ? "" : "actions";
|
|
252
|
+
|
|
253
|
+
if (inline) {
|
|
254
|
+
this.#actions.append(this.#reset, this.#import);
|
|
255
|
+
root.append(this.#controls, this.#actions);
|
|
256
|
+
this.#button.remove();
|
|
257
|
+
this.#popover.remove();
|
|
258
|
+
} else {
|
|
259
|
+
this.#popover.append(this.#controls, this.#reset, this.#import);
|
|
260
|
+
root.prepend(this.#button, this.#popover);
|
|
261
|
+
this.#actions.remove();
|
|
262
|
+
}
|
|
139
263
|
}
|
|
140
264
|
|
|
141
265
|
#detail<Detail>(type: string, detail: Detail) {
|
|
@@ -143,7 +267,6 @@ export class GwThemePicker extends GwElement {
|
|
|
143
267
|
}
|
|
144
268
|
|
|
145
269
|
protected override create(root: ShadowRoot) {
|
|
146
|
-
this.#mode.options = modes;
|
|
147
270
|
this.#button.addEventListener("click", () => {
|
|
148
271
|
this.show();
|
|
149
272
|
});
|
|
@@ -157,10 +280,15 @@ export class GwThemePicker extends GwElement {
|
|
|
157
280
|
this.theme = this.#theme.value;
|
|
158
281
|
this.dispatchEvent(this.#detail("theme", { id: this.theme }));
|
|
159
282
|
});
|
|
160
|
-
this.#
|
|
283
|
+
this.#gallery.addEventListener("theme", (event) => {
|
|
284
|
+
event.stopPropagation();
|
|
285
|
+
this.theme = this.#gallery.theme;
|
|
286
|
+
this.dispatchEvent(this.#detail("theme", { id: this.theme }));
|
|
287
|
+
});
|
|
288
|
+
this.#import.addEventListener("click", () => {
|
|
161
289
|
this.#file.click();
|
|
162
290
|
});
|
|
163
|
-
this.#
|
|
291
|
+
this.#reset.addEventListener("click", () => {
|
|
164
292
|
this.dispatchEvent(new Event("reset", { bubbles: true, composed: true }));
|
|
165
293
|
});
|
|
166
294
|
this.#file.addEventListener("change", async () => {
|
|
@@ -175,17 +303,42 @@ export class GwThemePicker extends GwElement {
|
|
|
175
303
|
this.dispatchEvent(this.#detail("import", { text, name: file.name }));
|
|
176
304
|
});
|
|
177
305
|
|
|
178
|
-
root.append(this.#
|
|
306
|
+
root.append(this.#file);
|
|
307
|
+
this.#place(root);
|
|
179
308
|
}
|
|
180
309
|
|
|
181
|
-
protected override update() {
|
|
182
|
-
|
|
183
|
-
|
|
310
|
+
protected override update(changed: ReadonlySet<string>) {
|
|
311
|
+
const root = this.shadowRoot;
|
|
312
|
+
|
|
313
|
+
if (changed.has("layout") && root !== null) this.#place(root);
|
|
314
|
+
|
|
315
|
+
const offered = this.#offered();
|
|
316
|
+
const current = offered.find(({ value }) => value === this.mode);
|
|
317
|
+
|
|
318
|
+
if (this.layout === "cycle") {
|
|
319
|
+
const name = `${this.label}: ${current?.label ?? this.mode}`;
|
|
320
|
+
|
|
321
|
+
this.#button.label = name;
|
|
322
|
+
this.#button.textContent = name;
|
|
323
|
+
this.#button.icon = current?.icon ?? paletteIcon;
|
|
324
|
+
} else {
|
|
325
|
+
this.#button.label = this.label;
|
|
326
|
+
this.#button.textContent = this.label;
|
|
327
|
+
this.#button.icon = paletteIcon;
|
|
328
|
+
}
|
|
329
|
+
|
|
184
330
|
this.#popover.heading = this.label;
|
|
185
331
|
this.#mode.label = "Mode";
|
|
332
|
+
this.#mode.options = offered.map(({ value, label }) => ({ value, label }));
|
|
186
333
|
this.#mode.value = this.mode;
|
|
187
334
|
this.#theme.options = Array.isArray(this.themes) ? this.themes : [];
|
|
188
335
|
this.#theme.value = this.theme;
|
|
336
|
+
this.#theme.hidden = this.gallery || this.#theme.options.length < 2;
|
|
337
|
+
this.#gallery.hidden = !this.gallery;
|
|
338
|
+
this.#gallery.themes = Array.isArray(this.themes) ? this.themes : [];
|
|
339
|
+
this.#gallery.theme = this.theme;
|
|
340
|
+
// Auto leaves it to follow the page, or the themed container around it
|
|
341
|
+
this.#gallery.mode = this.mode === "auto" ? "" : this.mode;
|
|
189
342
|
this.#status.textContent = this.status;
|
|
190
343
|
this.#status.hidden = !this.status;
|
|
191
344
|
}
|
package/src/gw-toast.ts
CHANGED
|
@@ -22,6 +22,8 @@ import { base, type Tone } from "./styles.js";
|
|
|
22
22
|
* @summary Brief, non-interactive toasts in the bottom end corner.
|
|
23
23
|
*
|
|
24
24
|
* @attr {number} duration - Milliseconds each toast shows for. Defaults to 5000.
|
|
25
|
+
*
|
|
26
|
+
* @csspart toast - Each toast.
|
|
25
27
|
*/
|
|
26
28
|
export class GwToaster extends GwElement {
|
|
27
29
|
static override properties = {
|
|
@@ -111,7 +113,7 @@ export class GwToaster extends GwElement {
|
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
show(message: string, tone: Tone = "neutral") {
|
|
114
|
-
const toast = h("div", { class: "toast" }, message);
|
|
116
|
+
const toast = h("div", { class: "toast", part: "toast" }, message);
|
|
115
117
|
|
|
116
118
|
toast.style.setProperty(
|
|
117
119
|
"--gw-toast-tone",
|