@timmo001/groundwork-ui 0.0.0-stage → 0.0.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/LICENSE +201 -0
- package/README.md +32 -2
- package/package.json +38 -4
- package/src/dismiss.ts +165 -0
- package/src/element.ts +375 -0
- package/src/field-info.ts +56 -0
- package/src/fields.ts +346 -0
- package/src/gw-alert.ts +117 -0
- package/src/gw-badge.ts +171 -0
- package/src/gw-banner.ts +120 -0
- package/src/gw-button.ts +438 -0
- package/src/gw-card.ts +187 -0
- package/src/gw-checkbox.ts +113 -0
- package/src/gw-chip.ts +122 -0
- package/src/gw-command-menu.ts +283 -0
- package/src/gw-dialog.ts +246 -0
- package/src/gw-dot.ts +89 -0
- package/src/gw-empty-state.ts +107 -0
- package/src/gw-expansion.ts +224 -0
- package/src/gw-facts.ts +141 -0
- package/src/gw-fade-in.ts +108 -0
- package/src/gw-gauge.ts +328 -0
- package/src/gw-input.ts +127 -0
- package/src/gw-menu-button.ts +386 -0
- package/src/gw-meter.ts +194 -0
- package/src/gw-notification-center.ts +155 -0
- package/src/gw-notification.ts +222 -0
- package/src/gw-option-list.ts +616 -0
- package/src/gw-page-header.ts +125 -0
- package/src/gw-popover.ts +382 -0
- package/src/gw-section-title.ts +71 -0
- package/src/gw-segmented.ts +301 -0
- package/src/gw-select.ts +361 -0
- package/src/gw-setting-row.ts +168 -0
- package/src/gw-skeleton.ts +178 -0
- package/src/gw-spinner.ts +84 -0
- package/src/gw-toast.ts +167 -0
- package/src/index.ts +76 -0
- package/src/layout.ts +182 -0
- package/src/motion.ts +105 -0
- package/src/styles.ts +122 -0
package/src/gw-dialog.ts
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gw-dialog`: a modal on the native `<dialog>`, for what blocks the whole app.
|
|
3
|
+
* Docs: https://groundwork.timmo.dev/components/overlays/#dialog
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import { Dismissal, dismissalStyles } from "./dismiss.js";
|
|
8
|
+
import { GwElement, css, h } from "./element.js";
|
|
9
|
+
import "./gw-button.js";
|
|
10
|
+
import { base } from "./styles.js";
|
|
11
|
+
|
|
12
|
+
// MDI's close
|
|
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
|
+
/**
|
|
17
|
+
* A modal on the native `<dialog>`, so focus is trapped and returned, Esc
|
|
18
|
+
* closes it and the page behind is inert. Open it with `show()`. Put buttons
|
|
19
|
+
* in the `actions` slot; a click on one with a `value` closes the dialog with
|
|
20
|
+
* that value, and fires `close`.
|
|
21
|
+
*
|
|
22
|
+
* @element gw-dialog
|
|
23
|
+
* @summary A modal for what blocks the whole app.
|
|
24
|
+
*
|
|
25
|
+
* @attr {string} heading - The heading beside the close button.
|
|
26
|
+
* @attr {boolean} open - Whether it shows.
|
|
27
|
+
*
|
|
28
|
+
* @slot - The content.
|
|
29
|
+
* @slot actions - Buttons; one with a `value` closes it with that value.
|
|
30
|
+
*
|
|
31
|
+
* @fires close - When it closes; `returnValue` holds the button's value.
|
|
32
|
+
*/
|
|
33
|
+
export class GwDialog extends GwElement {
|
|
34
|
+
static override properties = {
|
|
35
|
+
heading: {},
|
|
36
|
+
open: { type: "boolean", reflect: true },
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
39
|
+
static override styles = [
|
|
40
|
+
base,
|
|
41
|
+
dismissalStyles,
|
|
42
|
+
css`
|
|
43
|
+
dialog {
|
|
44
|
+
width: min(32rem, calc(100vw - 2 * var(--gw-space-3)));
|
|
45
|
+
padding: 0;
|
|
46
|
+
color: var(--gw-color-text-primary);
|
|
47
|
+
background: var(--gw-color-surface-overlay);
|
|
48
|
+
border: var(--gw-border-width) solid var(--gw-color-border-normal);
|
|
49
|
+
border-radius: var(--gw-radius-panel);
|
|
50
|
+
box-shadow: var(--gw-shadow-l);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
dialog::backdrop {
|
|
54
|
+
background: rgb(0 0 0 / 50%);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* Fades and lifts in and out, the backdrop with it */
|
|
58
|
+
dialog,
|
|
59
|
+
dialog::backdrop {
|
|
60
|
+
transition:
|
|
61
|
+
opacity var(--gw-motion-normal) var(--gw-motion-easing),
|
|
62
|
+
scale var(--gw-motion-normal) var(--gw-motion-easing),
|
|
63
|
+
display var(--gw-motion-normal) allow-discrete,
|
|
64
|
+
overlay var(--gw-motion-normal) allow-discrete;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
dialog,
|
|
68
|
+
dialog::backdrop {
|
|
69
|
+
opacity: 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
dialog {
|
|
73
|
+
scale: 0.96;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
dialog[open],
|
|
77
|
+
dialog[open]::backdrop {
|
|
78
|
+
opacity: 1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
dialog[open] {
|
|
82
|
+
scale: 1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@starting-style {
|
|
86
|
+
dialog[open],
|
|
87
|
+
dialog[open]::backdrop {
|
|
88
|
+
opacity: 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
dialog[open] {
|
|
92
|
+
scale: 0.96;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
header {
|
|
97
|
+
display: flex;
|
|
98
|
+
gap: var(--gw-space-2);
|
|
99
|
+
align-items: center;
|
|
100
|
+
padding: var(--gw-space-3) var(--gw-space-3) 0 var(--gw-space-4);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
h2 {
|
|
104
|
+
flex: 1;
|
|
105
|
+
margin: 0;
|
|
106
|
+
font-size: var(--gw-font-size-l);
|
|
107
|
+
font-weight: var(--gw-font-weight-strong);
|
|
108
|
+
color: var(--gw-color-text-strong);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.body {
|
|
112
|
+
padding: var(--gw-space-3) var(--gw-space-4);
|
|
113
|
+
font-size: var(--gw-font-size-s);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
footer {
|
|
117
|
+
display: flex;
|
|
118
|
+
flex-wrap: wrap;
|
|
119
|
+
gap: var(--gw-space-1);
|
|
120
|
+
justify-content: flex-end;
|
|
121
|
+
padding: 0 var(--gw-space-4) var(--gw-space-4);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
dialog > .discard {
|
|
125
|
+
padding: var(--gw-space-4);
|
|
126
|
+
}
|
|
127
|
+
`,
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
declare heading: string;
|
|
131
|
+
declare open: boolean;
|
|
132
|
+
|
|
133
|
+
/** The value of the action that closed the dialog, or "" for Esc. */
|
|
134
|
+
returnValue = "";
|
|
135
|
+
|
|
136
|
+
constructor() {
|
|
137
|
+
super();
|
|
138
|
+
this.heading = "";
|
|
139
|
+
this.open = false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
readonly #heading = h("h2", { id: "heading" });
|
|
143
|
+
|
|
144
|
+
readonly #footer = h("footer", {}, h("slot", { name: "actions" }));
|
|
145
|
+
|
|
146
|
+
readonly #closeButton = h(
|
|
147
|
+
"gw-button",
|
|
148
|
+
{
|
|
149
|
+
class: "close",
|
|
150
|
+
variant: "neutral",
|
|
151
|
+
appearance: "plain",
|
|
152
|
+
size: "small",
|
|
153
|
+
labels: "hide",
|
|
154
|
+
icon: closeIcon,
|
|
155
|
+
},
|
|
156
|
+
"Close",
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
readonly #dialog = h(
|
|
160
|
+
"dialog",
|
|
161
|
+
{ "aria-labelledby": "heading" },
|
|
162
|
+
h("header", {}, this.#heading, this.#closeButton),
|
|
163
|
+
h("div", { class: "body" }, h("slot")),
|
|
164
|
+
this.#footer,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
readonly #dismissal = new Dismissal(this, this.#dialog, () => this.close());
|
|
168
|
+
|
|
169
|
+
/** Whether its content has changed since it opened, so closing it asks first. */
|
|
170
|
+
get dirty() {
|
|
171
|
+
return this.#dismissal.dirty;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
set dirty(dirty: boolean) {
|
|
175
|
+
this.#dismissal.mark(dirty);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
show() {
|
|
179
|
+
this.#dismissal.reset();
|
|
180
|
+
this.open = true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
close(value = "") {
|
|
184
|
+
this.returnValue = value;
|
|
185
|
+
this.open = false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
readonly #onAction = (event: Event) => {
|
|
189
|
+
const target = event.target;
|
|
190
|
+
|
|
191
|
+
if (target instanceof HTMLElement && target.hasAttribute("value")) {
|
|
192
|
+
this.close(target.getAttribute("value") ?? "");
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
readonly #onClose = () => {
|
|
197
|
+
this.open = false;
|
|
198
|
+
this.dispatchEvent(new Event("close", { bubbles: true, composed: true }));
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// Esc and the close button close it at once while nothing has changed,
|
|
202
|
+
// and ask first once something has
|
|
203
|
+
readonly #onCancel = (event: Event) => {
|
|
204
|
+
event.preventDefault();
|
|
205
|
+
this.#dismissal.request();
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// A click on the backdrop closes it too, but only while nothing has
|
|
209
|
+
// changed, so a stray click never throws work away
|
|
210
|
+
readonly #onBackdrop = (event: MouseEvent) => {
|
|
211
|
+
if (event.target === this.#dialog && !this.#dismissal.dirty) this.close();
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
protected override create(root: ShadowRoot) {
|
|
215
|
+
this.#dialog.addEventListener("close", this.#onClose);
|
|
216
|
+
this.#dialog.addEventListener("cancel", this.#onCancel);
|
|
217
|
+
this.#dialog.addEventListener("click", this.#onBackdrop);
|
|
218
|
+
this.#closeButton.addEventListener("click", () => {
|
|
219
|
+
this.#dismissal.request();
|
|
220
|
+
});
|
|
221
|
+
this.#footer.addEventListener("click", this.#onAction);
|
|
222
|
+
root.append(this.#dialog);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
protected override update() {
|
|
226
|
+
const dialog = this.#dialog;
|
|
227
|
+
|
|
228
|
+
this.#heading.textContent = this.heading;
|
|
229
|
+
|
|
230
|
+
if (this.open && !dialog.open) {
|
|
231
|
+
dialog.showModal();
|
|
232
|
+
// Toasts rise back above it
|
|
233
|
+
document.dispatchEvent(new Event("gw-dialog-open"));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!this.open && dialog.open) dialog.close(this.returnValue);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
GwDialog.define("gw-dialog");
|
|
241
|
+
|
|
242
|
+
declare global {
|
|
243
|
+
interface HTMLElementTagNameMap {
|
|
244
|
+
"gw-dialog": GwDialog;
|
|
245
|
+
}
|
|
246
|
+
}
|
package/src/gw-dot.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gw-dot`: a small status dot.
|
|
3
|
+
* Docs: https://groundwork.timmo.dev/components/display/#chip-and-dot
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import { GwElement, css, h } from "./element.js";
|
|
8
|
+
import { base, type Tone } from "./styles.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A small status dot. Give it a `label`, such as "Online", unless text
|
|
12
|
+
* beside it already says the same.
|
|
13
|
+
*
|
|
14
|
+
* @element gw-dot
|
|
15
|
+
* @summary A small status dot.
|
|
16
|
+
*
|
|
17
|
+
* @attr {"neutral"|"accent"|"info"|"success"|"warning"|"danger"|"important"} tone - The color. Defaults to `neutral`.
|
|
18
|
+
* @attr {string} label - What it means, for assistive technology.
|
|
19
|
+
* @attr {boolean} pulse - Pulses, for something live.
|
|
20
|
+
*/
|
|
21
|
+
export class GwDot extends GwElement {
|
|
22
|
+
static override properties = {
|
|
23
|
+
tone: { reflect: true },
|
|
24
|
+
label: {},
|
|
25
|
+
pulse: { type: "boolean", reflect: true },
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
static override styles = [
|
|
29
|
+
base,
|
|
30
|
+
css`
|
|
31
|
+
:host {
|
|
32
|
+
display: inline-block;
|
|
33
|
+
flex: none;
|
|
34
|
+
width: 0.5rem;
|
|
35
|
+
height: 0.5rem;
|
|
36
|
+
background: var(--gw-tone, var(--gw-color-text-secondary));
|
|
37
|
+
border-radius: 50%;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
:host([pulse]) {
|
|
41
|
+
animation: pulse 1.6s ease-in-out infinite;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@keyframes pulse {
|
|
45
|
+
50% {
|
|
46
|
+
opacity: 0.4;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
span {
|
|
51
|
+
position: absolute;
|
|
52
|
+
width: 1px;
|
|
53
|
+
height: 1px;
|
|
54
|
+
overflow: hidden;
|
|
55
|
+
clip-path: inset(50%);
|
|
56
|
+
}
|
|
57
|
+
`,
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
declare tone: Tone;
|
|
61
|
+
declare label: string;
|
|
62
|
+
declare pulse: boolean;
|
|
63
|
+
|
|
64
|
+
readonly #label = h("span");
|
|
65
|
+
|
|
66
|
+
constructor() {
|
|
67
|
+
super();
|
|
68
|
+
this.tone = "neutral";
|
|
69
|
+
this.label = "";
|
|
70
|
+
this.pulse = false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
protected override create(root: ShadowRoot) {
|
|
74
|
+
root.append(this.#label);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
protected override update() {
|
|
78
|
+
this.#label.textContent = this.label;
|
|
79
|
+
this.#label.hidden = !this.label;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
GwDot.define("gw-dot");
|
|
84
|
+
|
|
85
|
+
declare global {
|
|
86
|
+
interface HTMLElementTagNameMap {
|
|
87
|
+
"gw-dot": GwDot;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gw-empty-state`: what a list or page shows when there's nothing in it.
|
|
3
|
+
* Docs: https://groundwork.timmo.dev/components/feedback/#empty-state
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import { GwElement, css, h } from "./element.js";
|
|
8
|
+
import { base, createIcon, setIconPath } from "./styles.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* What a list or page shows when there's nothing in it, or nothing to do:
|
|
12
|
+
* an icon, a heading, a line of text and an optional action.
|
|
13
|
+
*
|
|
14
|
+
* @element gw-empty-state
|
|
15
|
+
* @summary What a list or page shows when there's nothing in it.
|
|
16
|
+
*
|
|
17
|
+
* @attr {string} icon - SVG path data for the icon.
|
|
18
|
+
* @attr {string} heading - The heading.
|
|
19
|
+
*
|
|
20
|
+
* @slot - A line of text.
|
|
21
|
+
* @slot actions - An action, such as adding the first item.
|
|
22
|
+
*
|
|
23
|
+
* @csspart empty - The block.
|
|
24
|
+
*/
|
|
25
|
+
export class GwEmptyState extends GwElement {
|
|
26
|
+
static override properties = {
|
|
27
|
+
icon: {},
|
|
28
|
+
heading: {},
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
static override styles = [
|
|
32
|
+
base,
|
|
33
|
+
css`
|
|
34
|
+
:host {
|
|
35
|
+
display: block;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.empty {
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-direction: column;
|
|
41
|
+
gap: var(--gw-space-1);
|
|
42
|
+
align-items: center;
|
|
43
|
+
justify-content: center;
|
|
44
|
+
padding: var(--gw-space-12) var(--gw-space-3);
|
|
45
|
+
color: var(--gw-color-text-secondary);
|
|
46
|
+
text-align: center;
|
|
47
|
+
--gw-icon-size: 2.5rem;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
h2 {
|
|
51
|
+
margin: 0;
|
|
52
|
+
font-size: var(--gw-font-size-m);
|
|
53
|
+
font-weight: var(--gw-font-weight-strong);
|
|
54
|
+
color: var(--gw-color-text-strong);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
::slotted(p) {
|
|
58
|
+
margin: 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
::slotted([slot="actions"]) {
|
|
62
|
+
margin-block-start: var(--gw-space-2);
|
|
63
|
+
}
|
|
64
|
+
`,
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
declare icon: string;
|
|
68
|
+
declare heading: string;
|
|
69
|
+
|
|
70
|
+
readonly #icon = createIcon("");
|
|
71
|
+
|
|
72
|
+
readonly #heading = h("h2");
|
|
73
|
+
|
|
74
|
+
constructor() {
|
|
75
|
+
super();
|
|
76
|
+
this.icon = "";
|
|
77
|
+
this.heading = "";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
protected override create(root: ShadowRoot) {
|
|
81
|
+
root.append(
|
|
82
|
+
h(
|
|
83
|
+
"div",
|
|
84
|
+
{ class: "empty", part: "empty" },
|
|
85
|
+
this.#icon,
|
|
86
|
+
this.#heading,
|
|
87
|
+
h("slot"),
|
|
88
|
+
h("slot", { name: "actions" }),
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
protected override update() {
|
|
94
|
+
setIconPath(this.#icon, this.icon);
|
|
95
|
+
this.#icon.toggleAttribute("hidden", !this.icon);
|
|
96
|
+
this.#heading.textContent = this.heading;
|
|
97
|
+
this.#heading.hidden = !this.heading;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
GwEmptyState.define("gw-empty-state");
|
|
102
|
+
|
|
103
|
+
declare global {
|
|
104
|
+
interface HTMLElementTagNameMap {
|
|
105
|
+
"gw-empty-state": GwEmptyState;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gw-expansion`: a heading that opens and closes the content under it.
|
|
3
|
+
* Docs: https://groundwork.timmo.dev/components/display/#expansion-panel
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import { GwElement, css, h } from "./element.js";
|
|
8
|
+
import { base } from "./styles.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A heading that opens and closes the content under it, on a native
|
|
12
|
+
* `<details>`, so find-in-page and the keyboard work as they do anywhere. The
|
|
13
|
+
* content glides open and shut rather than jumping, and the chevron turns
|
|
14
|
+
* with it. `open` reflects its state and it fires `toggle`.
|
|
15
|
+
*
|
|
16
|
+
* @element gw-expansion
|
|
17
|
+
* @summary A heading that opens and closes the content under it.
|
|
18
|
+
*
|
|
19
|
+
* @attr {string} heading - The heading in the summary row.
|
|
20
|
+
* @attr {string} description - A line under the heading.
|
|
21
|
+
* @attr {boolean} open - Whether the content shows.
|
|
22
|
+
*
|
|
23
|
+
* @slot - The content.
|
|
24
|
+
* @slot icon - An icon before the heading.
|
|
25
|
+
*
|
|
26
|
+
* @csspart details - The native `<details>`.
|
|
27
|
+
* @csspart summary - The summary row.
|
|
28
|
+
* @csspart content - The content's wrapper.
|
|
29
|
+
*
|
|
30
|
+
* @fires toggle - When it opens or closes.
|
|
31
|
+
*/
|
|
32
|
+
export class GwExpansion extends GwElement {
|
|
33
|
+
static override properties = {
|
|
34
|
+
heading: {},
|
|
35
|
+
description: {},
|
|
36
|
+
open: { type: "boolean", reflect: true },
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
39
|
+
static override styles = [
|
|
40
|
+
base,
|
|
41
|
+
css`
|
|
42
|
+
:host {
|
|
43
|
+
display: block;
|
|
44
|
+
interpolate-size: allow-keywords;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
details {
|
|
48
|
+
background: var(--gw-color-surface-raised);
|
|
49
|
+
border: var(--gw-border-width) solid var(--gw-color-border-normal);
|
|
50
|
+
border-radius: var(--gw-radius-panel);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
details::details-content {
|
|
54
|
+
block-size: 0;
|
|
55
|
+
overflow: clip;
|
|
56
|
+
transition:
|
|
57
|
+
block-size var(--gw-motion-normal) var(--gw-motion-easing),
|
|
58
|
+
content-visibility var(--gw-motion-normal) allow-discrete;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
details[open]::details-content {
|
|
62
|
+
block-size: auto;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
summary {
|
|
66
|
+
display: flex;
|
|
67
|
+
gap: var(--gw-space-3);
|
|
68
|
+
align-items: center;
|
|
69
|
+
min-height: 3rem;
|
|
70
|
+
padding: var(--gw-space-2) var(--gw-space-4);
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
list-style: none;
|
|
73
|
+
border-radius: calc(var(--gw-radius-panel) - var(--gw-border-width));
|
|
74
|
+
user-select: none;
|
|
75
|
+
transition:
|
|
76
|
+
background-color var(--gw-motion-fast) var(--gw-motion-easing),
|
|
77
|
+
border-radius var(--gw-motion-normal) var(--gw-motion-easing);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
summary::-webkit-details-marker {
|
|
81
|
+
display: none;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
details[open] summary {
|
|
85
|
+
border-end-start-radius: 0;
|
|
86
|
+
border-end-end-radius: 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
summary:hover {
|
|
90
|
+
background: var(--gw-color-fill-neutral-hovered);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
summary:active {
|
|
94
|
+
background: var(--gw-color-fill-neutral-pressed);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
summary:focus-visible {
|
|
98
|
+
outline: var(--gw-focus-width) solid var(--gw-color-focus);
|
|
99
|
+
outline-offset: var(--gw-focus-offset);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.text {
|
|
103
|
+
display: flex;
|
|
104
|
+
flex: 1;
|
|
105
|
+
flex-direction: column;
|
|
106
|
+
gap: var(--gw-space-0-5);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.heading {
|
|
110
|
+
font-size: var(--gw-font-size-s);
|
|
111
|
+
font-weight: var(--gw-font-weight-strong);
|
|
112
|
+
color: var(--gw-color-text-strong);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.description {
|
|
116
|
+
font-size: var(--gw-font-size-xs);
|
|
117
|
+
color: var(--gw-color-text-secondary);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* The chevron sits in a round neutral button, so the heading reads as
|
|
121
|
+
something to press */
|
|
122
|
+
.toggle {
|
|
123
|
+
display: grid;
|
|
124
|
+
flex: none;
|
|
125
|
+
place-items: center;
|
|
126
|
+
width: 2rem;
|
|
127
|
+
height: 2rem;
|
|
128
|
+
background: var(--gw-color-fill-neutral-resting);
|
|
129
|
+
border-radius: 50%;
|
|
130
|
+
transition: background-color var(--gw-motion-fast)
|
|
131
|
+
var(--gw-motion-easing);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
summary:hover .toggle {
|
|
135
|
+
background: var(--gw-color-fill-neutral-pressed);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
summary:active .toggle {
|
|
139
|
+
background: var(--gw-color-fill-neutral-selected);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.chevron {
|
|
143
|
+
width: 0.45rem;
|
|
144
|
+
height: 0.45rem;
|
|
145
|
+
border-inline-end: 1.5px solid var(--gw-color-text-primary);
|
|
146
|
+
border-block-end: 1.5px solid var(--gw-color-text-primary);
|
|
147
|
+
rotate: 45deg;
|
|
148
|
+
translate: 0 -0.125rem;
|
|
149
|
+
transition:
|
|
150
|
+
rotate var(--gw-motion-normal) var(--gw-motion-easing),
|
|
151
|
+
translate var(--gw-motion-normal) var(--gw-motion-easing);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
details[open] .chevron {
|
|
155
|
+
rotate: 225deg;
|
|
156
|
+
translate: 0 0.125rem;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.content {
|
|
160
|
+
padding: var(--gw-space-3) var(--gw-space-4) var(--gw-space-4);
|
|
161
|
+
border-block-start: var(--gw-border-width) solid
|
|
162
|
+
var(--gw-color-border-quiet);
|
|
163
|
+
font-size: var(--gw-font-size-s);
|
|
164
|
+
}
|
|
165
|
+
`,
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
declare heading: string;
|
|
169
|
+
declare description: string;
|
|
170
|
+
declare open: boolean;
|
|
171
|
+
|
|
172
|
+
readonly #heading = h("span", { class: "heading" });
|
|
173
|
+
|
|
174
|
+
readonly #description = h("span", { class: "description" });
|
|
175
|
+
|
|
176
|
+
readonly #details = h(
|
|
177
|
+
"details",
|
|
178
|
+
{ part: "details" },
|
|
179
|
+
h(
|
|
180
|
+
"summary",
|
|
181
|
+
{ part: "summary" },
|
|
182
|
+
h("slot", { name: "icon" }),
|
|
183
|
+
h("span", { class: "text" }, this.#heading, this.#description),
|
|
184
|
+
h(
|
|
185
|
+
"span",
|
|
186
|
+
{ class: "toggle", "aria-hidden": "true" },
|
|
187
|
+
h("span", { class: "chevron" }),
|
|
188
|
+
),
|
|
189
|
+
),
|
|
190
|
+
h("div", { class: "content", part: "content" }, h("slot")),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
constructor() {
|
|
194
|
+
super();
|
|
195
|
+
this.heading = "";
|
|
196
|
+
this.description = "";
|
|
197
|
+
this.open = false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
readonly #onToggle = () => {
|
|
201
|
+
this.open = this.#details.open;
|
|
202
|
+
this.dispatchEvent(new Event("toggle", { bubbles: true, composed: true }));
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
protected override create(root: ShadowRoot) {
|
|
206
|
+
this.#details.addEventListener("toggle", this.#onToggle);
|
|
207
|
+
root.append(this.#details);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
protected override update() {
|
|
211
|
+
this.#details.open = this.open;
|
|
212
|
+
this.#heading.textContent = this.heading;
|
|
213
|
+
this.#description.textContent = this.description;
|
|
214
|
+
this.#description.hidden = !this.description;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
GwExpansion.define("gw-expansion");
|
|
219
|
+
|
|
220
|
+
declare global {
|
|
221
|
+
interface HTMLElementTagNameMap {
|
|
222
|
+
"gw-expansion": GwExpansion;
|
|
223
|
+
}
|
|
224
|
+
}
|