@timmo001/groundwork-ui 0.0.0-stage → 0.3.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 +39 -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
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gw-setting-row`: a setting's name and description beside its control.
|
|
3
|
+
* Docs: https://groundwork.timmo.dev/components/inputs/#setting-row
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import { GwElement, css, h, setAttribute } from "./element.js";
|
|
8
|
+
import "./gw-skeleton.js";
|
|
9
|
+
import { fadeIn } from "./motion.js";
|
|
10
|
+
import { base } from "./styles.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A setting: its name and description on one side, its control on the
|
|
14
|
+
* other. A wide control drops under the text when there isn't room. Rows
|
|
15
|
+
* after the first in their parent get a line above them. While `loading`, a
|
|
16
|
+
* skeleton stands in for the control, and for the text until it's known.
|
|
17
|
+
*
|
|
18
|
+
* @element gw-setting-row
|
|
19
|
+
* @summary A setting's name and description beside its control.
|
|
20
|
+
*
|
|
21
|
+
* @attr {string} heading - The setting's name.
|
|
22
|
+
* @attr {string} description - What it does.
|
|
23
|
+
* @attr {boolean} loading - Shows skeletons for the control and any unknown text.
|
|
24
|
+
*
|
|
25
|
+
* @slot - The control.
|
|
26
|
+
*
|
|
27
|
+
* @csspart row - The row.
|
|
28
|
+
*
|
|
29
|
+
* @cssprop --gw-row-padding - The padding above and below. Defaults to `--gw-space-2`.
|
|
30
|
+
*/
|
|
31
|
+
export class GwSettingRow extends GwElement {
|
|
32
|
+
static override properties = {
|
|
33
|
+
heading: {},
|
|
34
|
+
description: {},
|
|
35
|
+
loading: { type: "boolean", reflect: true },
|
|
36
|
+
} as const;
|
|
37
|
+
|
|
38
|
+
static override styles = [
|
|
39
|
+
base,
|
|
40
|
+
css`
|
|
41
|
+
:host {
|
|
42
|
+
display: block;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.row {
|
|
46
|
+
display: flex;
|
|
47
|
+
flex-wrap: wrap;
|
|
48
|
+
gap: var(--gw-space-1) var(--gw-space-3);
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: space-between;
|
|
51
|
+
padding: var(--gw-row-padding, var(--gw-space-2)) 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* A full line even in themes without borders, since it separates rows */
|
|
55
|
+
:host(:not(:first-of-type)) .row {
|
|
56
|
+
border-block-start: max(var(--gw-border-width), 1px) solid
|
|
57
|
+
var(--gw-color-border-quiet);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.text {
|
|
61
|
+
flex: 1 1 12rem;
|
|
62
|
+
min-width: 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.heading {
|
|
66
|
+
font-size: var(--gw-font-size-s);
|
|
67
|
+
font-weight: var(--gw-font-weight-strong);
|
|
68
|
+
color: var(--gw-color-text-strong);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.description {
|
|
72
|
+
font-size: var(--gw-font-size-xs);
|
|
73
|
+
color: var(--gw-color-text-secondary);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.control {
|
|
77
|
+
max-width: 100%;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.control-skeleton {
|
|
81
|
+
width: 3.25rem;
|
|
82
|
+
min-height: 0;
|
|
83
|
+
height: 2rem;
|
|
84
|
+
--gw-skeleton-radius: var(--gw-radius-pill);
|
|
85
|
+
}
|
|
86
|
+
`,
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
declare heading: string;
|
|
90
|
+
declare description: string;
|
|
91
|
+
declare loading: boolean;
|
|
92
|
+
|
|
93
|
+
readonly #row = h("div", { class: "row", part: "row" });
|
|
94
|
+
|
|
95
|
+
readonly #heading = h("span");
|
|
96
|
+
|
|
97
|
+
readonly #headingSkeleton = h("gw-skeleton-text", {
|
|
98
|
+
style: "--gw-skeleton-width: 8rem",
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
readonly #description = h("div", { class: "description" });
|
|
102
|
+
|
|
103
|
+
readonly #descriptionSkeleton = h(
|
|
104
|
+
"div",
|
|
105
|
+
{ class: "description" },
|
|
106
|
+
h("gw-skeleton-text", { style: "--gw-skeleton-width: 12rem" }),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
readonly #slot = h("slot");
|
|
110
|
+
|
|
111
|
+
readonly #controlSkeleton = h("gw-skeleton", { class: "control-skeleton" });
|
|
112
|
+
|
|
113
|
+
readonly #control = h(
|
|
114
|
+
"div",
|
|
115
|
+
{ class: "control" },
|
|
116
|
+
this.#controlSkeleton,
|
|
117
|
+
this.#slot,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
#rendered = false;
|
|
121
|
+
|
|
122
|
+
constructor() {
|
|
123
|
+
super();
|
|
124
|
+
this.heading = "";
|
|
125
|
+
this.description = "";
|
|
126
|
+
this.loading = false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
protected override create(root: ShadowRoot) {
|
|
130
|
+
this.#row.append(
|
|
131
|
+
h(
|
|
132
|
+
"div",
|
|
133
|
+
{ class: "text" },
|
|
134
|
+
h("div", { class: "heading" }, this.#heading, this.#headingSkeleton),
|
|
135
|
+
this.#description,
|
|
136
|
+
this.#descriptionSkeleton,
|
|
137
|
+
),
|
|
138
|
+
this.#control,
|
|
139
|
+
);
|
|
140
|
+
root.append(this.#row);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
protected override update(changed: ReadonlySet<string>) {
|
|
144
|
+
setAttribute(this.#row, "aria-busy", this.loading ? "true" : null);
|
|
145
|
+
this.#heading.textContent = this.heading;
|
|
146
|
+
this.#headingSkeleton.hidden = !this.loading || this.heading !== "";
|
|
147
|
+
this.#description.textContent = this.description;
|
|
148
|
+
this.#description.hidden = !this.description;
|
|
149
|
+
this.#descriptionSkeleton.hidden =
|
|
150
|
+
!this.loading || this.heading !== "" || this.description !== "";
|
|
151
|
+
this.#controlSkeleton.hidden = !this.loading;
|
|
152
|
+
this.#slot.hidden = this.loading;
|
|
153
|
+
|
|
154
|
+
if (this.#rendered && changed.has("loading") && !this.loading) {
|
|
155
|
+
fadeIn(this.#control);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this.#rendered = true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
GwSettingRow.define("gw-setting-row");
|
|
163
|
+
|
|
164
|
+
declare global {
|
|
165
|
+
interface HTMLElementTagNameMap {
|
|
166
|
+
"gw-setting-row": GwSettingRow;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gw-skeleton` and its text, lines and icon shapes: placeholders for what's
|
|
3
|
+
* loading.
|
|
4
|
+
* Docs: https://groundwork.timmo.dev/components/feedback/#loading
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
import { GwElement, css, h } from "./element.js";
|
|
9
|
+
import { base } from "./styles.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A placeholder for what's loading, sized by its container or by `width`
|
|
13
|
+
* and `height` in CSS. The shapes below extend it, and components with a
|
|
14
|
+
* `loading` attribute draw their own from them, so a spinner is only needed
|
|
15
|
+
* for an action in progress. It's hidden from assistive technology, so mark
|
|
16
|
+
* the region it sits in `aria-busy`.
|
|
17
|
+
*
|
|
18
|
+
* @element gw-skeleton
|
|
19
|
+
* @summary A pulsing placeholder for what's loading.
|
|
20
|
+
*
|
|
21
|
+
* @cssprop --gw-skeleton-color - The fill. Defaults to the neutral fill.
|
|
22
|
+
* @cssprop --gw-skeleton-radius - The corners. Defaults to the control radius.
|
|
23
|
+
*/
|
|
24
|
+
export class GwSkeleton extends GwElement {
|
|
25
|
+
static override styles: readonly CSSStyleSheet[] = [
|
|
26
|
+
base,
|
|
27
|
+
css`
|
|
28
|
+
:host {
|
|
29
|
+
display: block;
|
|
30
|
+
min-height: 1rem;
|
|
31
|
+
background: var(
|
|
32
|
+
--gw-skeleton-color,
|
|
33
|
+
var(--gw-color-fill-neutral-resting)
|
|
34
|
+
);
|
|
35
|
+
border-radius: var(--gw-skeleton-radius, var(--gw-radius-control));
|
|
36
|
+
animation: pulse 2s ease-in-out infinite;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@keyframes pulse {
|
|
40
|
+
50% {
|
|
41
|
+
opacity: 0.45;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@media (forced-colors: active) {
|
|
46
|
+
:host {
|
|
47
|
+
background: GrayText;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
`,
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
override connectedCallback() {
|
|
54
|
+
super.connectedCallback();
|
|
55
|
+
this.setAttribute("aria-hidden", "true");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A line of text. Its height follows the font size around it, so it lines up
|
|
61
|
+
* with the text it stands in for.
|
|
62
|
+
*
|
|
63
|
+
* @element gw-skeleton-text
|
|
64
|
+
* @summary A placeholder for a line of text.
|
|
65
|
+
*
|
|
66
|
+
* @cssprop --gw-skeleton-width - Defaults to `8rem`.
|
|
67
|
+
*/
|
|
68
|
+
export class GwSkeletonText extends GwSkeleton {
|
|
69
|
+
static override styles = [
|
|
70
|
+
...GwSkeleton.styles,
|
|
71
|
+
css`
|
|
72
|
+
:host {
|
|
73
|
+
display: inline-block;
|
|
74
|
+
width: var(--gw-skeleton-width, 8rem);
|
|
75
|
+
max-width: 100%;
|
|
76
|
+
height: 1em;
|
|
77
|
+
min-height: 0;
|
|
78
|
+
vertical-align: middle;
|
|
79
|
+
}
|
|
80
|
+
`,
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Several lines of text, as a paragraph, with a shorter last line so it
|
|
86
|
+
* doesn't read as a solid block.
|
|
87
|
+
*
|
|
88
|
+
* @element gw-skeleton-lines
|
|
89
|
+
* @summary A placeholder for a paragraph.
|
|
90
|
+
*
|
|
91
|
+
* @attr {number} lines - How many lines. Defaults to 3.
|
|
92
|
+
*/
|
|
93
|
+
export class GwSkeletonLines extends GwElement {
|
|
94
|
+
static override properties = {
|
|
95
|
+
lines: { type: "number" },
|
|
96
|
+
} as const;
|
|
97
|
+
|
|
98
|
+
static override styles = [
|
|
99
|
+
base,
|
|
100
|
+
css`
|
|
101
|
+
:host {
|
|
102
|
+
display: flex;
|
|
103
|
+
flex-direction: column;
|
|
104
|
+
gap: 0.5em;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
gw-skeleton-text {
|
|
108
|
+
--gw-skeleton-width: 100%;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
gw-skeleton-text:last-child:not(:first-child) {
|
|
112
|
+
--gw-skeleton-width: 60%;
|
|
113
|
+
}
|
|
114
|
+
`,
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
declare lines: number;
|
|
118
|
+
|
|
119
|
+
constructor() {
|
|
120
|
+
super();
|
|
121
|
+
this.lines = 3;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
override connectedCallback() {
|
|
125
|
+
super.connectedCallback();
|
|
126
|
+
this.setAttribute("aria-hidden", "true");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
protected override update() {
|
|
130
|
+
this.renderRoot.replaceChildren(
|
|
131
|
+
...Array.from({ length: Math.max(1, this.lines || 1) }, () =>
|
|
132
|
+
h("gw-skeleton-text"),
|
|
133
|
+
),
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* A round placeholder for an icon or avatar.
|
|
140
|
+
*
|
|
141
|
+
* @element gw-skeleton-icon
|
|
142
|
+
* @summary A round placeholder for an icon or avatar.
|
|
143
|
+
*
|
|
144
|
+
* @cssprop --gw-icon-size - Its size. Defaults to `1.25rem`.
|
|
145
|
+
*/
|
|
146
|
+
export class GwSkeletonIcon extends GwSkeleton {
|
|
147
|
+
static override styles = [
|
|
148
|
+
...GwSkeleton.styles,
|
|
149
|
+
css`
|
|
150
|
+
:host {
|
|
151
|
+
display: inline-block;
|
|
152
|
+
flex: none;
|
|
153
|
+
width: var(--gw-icon-size, 1.25rem);
|
|
154
|
+
height: var(--gw-icon-size, 1.25rem);
|
|
155
|
+
min-height: 0;
|
|
156
|
+
vertical-align: middle;
|
|
157
|
+
border-radius: var(--gw-radius-pill);
|
|
158
|
+
}
|
|
159
|
+
`,
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
GwSkeleton.define("gw-skeleton");
|
|
164
|
+
|
|
165
|
+
GwSkeletonText.define("gw-skeleton-text");
|
|
166
|
+
|
|
167
|
+
GwSkeletonLines.define("gw-skeleton-lines");
|
|
168
|
+
|
|
169
|
+
GwSkeletonIcon.define("gw-skeleton-icon");
|
|
170
|
+
|
|
171
|
+
declare global {
|
|
172
|
+
interface HTMLElementTagNameMap {
|
|
173
|
+
"gw-skeleton": GwSkeleton;
|
|
174
|
+
"gw-skeleton-text": GwSkeletonText;
|
|
175
|
+
"gw-skeleton-lines": GwSkeletonLines;
|
|
176
|
+
"gw-skeleton-icon": GwSkeletonIcon;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gw-spinner`: a spinning ring, for waits a skeleton can't stand in for.
|
|
3
|
+
* Docs: https://groundwork.timmo.dev/components/feedback/#spinner
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import { GwElement, css, h } from "./element.js";
|
|
8
|
+
import { base } from "./styles.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A spinning ring while something loads, announced by its `label`.
|
|
12
|
+
*
|
|
13
|
+
* @element gw-spinner
|
|
14
|
+
* @summary A spinning ring for a wait a skeleton can't stand in for.
|
|
15
|
+
*
|
|
16
|
+
* @attr {string} label - What's loading, for assistive technology. Defaults to `Loading`.
|
|
17
|
+
*
|
|
18
|
+
* @cssprop --gw-spinner-size - The ring's size. Defaults to `1.25rem`.
|
|
19
|
+
*/
|
|
20
|
+
export class GwSpinner extends GwElement {
|
|
21
|
+
static override properties = {
|
|
22
|
+
label: {},
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
static override styles = [
|
|
26
|
+
base,
|
|
27
|
+
css`
|
|
28
|
+
:host {
|
|
29
|
+
display: inline-flex;
|
|
30
|
+
width: var(--gw-spinner-size, 1.25rem);
|
|
31
|
+
height: var(--gw-spinner-size, 1.25rem);
|
|
32
|
+
color: var(--gw-color-accent);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.ring {
|
|
36
|
+
width: 100%;
|
|
37
|
+
height: 100%;
|
|
38
|
+
border: 2px solid var(--gw-color-border-quiet);
|
|
39
|
+
border-top-color: currentColor;
|
|
40
|
+
border-radius: 50%;
|
|
41
|
+
animation: spin 0.9s linear infinite;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@keyframes spin {
|
|
45
|
+
to {
|
|
46
|
+
transform: rotate(360deg);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.label {
|
|
51
|
+
position: absolute;
|
|
52
|
+
width: 1px;
|
|
53
|
+
height: 1px;
|
|
54
|
+
overflow: hidden;
|
|
55
|
+
clip-path: inset(50%);
|
|
56
|
+
}
|
|
57
|
+
`,
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
declare label: string;
|
|
61
|
+
|
|
62
|
+
readonly #label = h("span", { class: "label" });
|
|
63
|
+
|
|
64
|
+
constructor() {
|
|
65
|
+
super();
|
|
66
|
+
this.label = "Loading";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
protected override create(root: ShadowRoot) {
|
|
70
|
+
root.append(h("span", { class: "ring", role: "status" }, this.#label));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
protected override update() {
|
|
74
|
+
this.#label.textContent = this.label;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
GwSpinner.define("gw-spinner");
|
|
79
|
+
|
|
80
|
+
declare global {
|
|
81
|
+
interface HTMLElementTagNameMap {
|
|
82
|
+
"gw-spinner": GwSpinner;
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/gw-toast.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gw-toaster`: brief, non-interactive toasts in the bottom end corner.
|
|
3
|
+
* Docs: https://groundwork.timmo.dev/components/feedback/#toast
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import { GwElement, css, h } from "./element.js";
|
|
8
|
+
import { fadeEasing, motionDuration, reducedMotion } from "./motion.js";
|
|
9
|
+
import { base, type Tone } from "./styles.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Where toasts appear: the bottom corner on the end side, away from what
|
|
13
|
+
* people click. Call `show(message, tone)`; each toast is announced politely
|
|
14
|
+
* and fades out after `duration` milliseconds.
|
|
15
|
+
*
|
|
16
|
+
* A toast only tells; it has no actions and lets every click through, since
|
|
17
|
+
* it may sit over something interactive and leaves before it can be used.
|
|
18
|
+
* It's in the top layer and rises above any dialog each time one shows. It's
|
|
19
|
+
* a last resort: show an outcome where it happened instead.
|
|
20
|
+
*
|
|
21
|
+
* @element gw-toaster
|
|
22
|
+
* @summary Brief, non-interactive toasts in the bottom end corner.
|
|
23
|
+
*
|
|
24
|
+
* @attr {number} duration - Milliseconds each toast shows for. Defaults to 5000.
|
|
25
|
+
*/
|
|
26
|
+
export class GwToaster extends GwElement {
|
|
27
|
+
static override properties = {
|
|
28
|
+
duration: { type: "number" },
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
static override styles = [
|
|
32
|
+
base,
|
|
33
|
+
css`
|
|
34
|
+
:host {
|
|
35
|
+
position: fixed;
|
|
36
|
+
inset: auto var(--gw-space-4) var(--gw-space-4) auto;
|
|
37
|
+
width: min(22rem, calc(100vw - 2 * var(--gw-space-4)));
|
|
38
|
+
padding: 0;
|
|
39
|
+
margin: 0;
|
|
40
|
+
overflow: visible;
|
|
41
|
+
color: inherit;
|
|
42
|
+
pointer-events: none;
|
|
43
|
+
user-select: none;
|
|
44
|
+
background: none;
|
|
45
|
+
border: 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
:host(:dir(rtl)) {
|
|
49
|
+
inset: auto auto var(--gw-space-4) var(--gw-space-4);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
:host(:not(:popover-open)) {
|
|
53
|
+
display: none;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.list {
|
|
57
|
+
display: flex;
|
|
58
|
+
flex-direction: column;
|
|
59
|
+
gap: var(--gw-space-1);
|
|
60
|
+
align-items: flex-end;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.toast {
|
|
64
|
+
max-width: 100%;
|
|
65
|
+
padding: var(--gw-space-2) var(--gw-space-3);
|
|
66
|
+
font-size: var(--gw-font-size-s);
|
|
67
|
+
color: var(--gw-color-text-strong);
|
|
68
|
+
background: var(--gw-color-surface-overlay);
|
|
69
|
+
border: var(--gw-border-width) solid var(--gw-color-border-normal);
|
|
70
|
+
border-inline-start: calc(var(--gw-border-width) * 4) solid
|
|
71
|
+
var(--gw-toast-tone);
|
|
72
|
+
border-radius: var(--gw-radius-control);
|
|
73
|
+
box-shadow: var(--gw-shadow-m);
|
|
74
|
+
}
|
|
75
|
+
`,
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
declare duration: number;
|
|
79
|
+
|
|
80
|
+
readonly #list = h("div", {
|
|
81
|
+
class: "list",
|
|
82
|
+
role: "status",
|
|
83
|
+
"aria-live": "polite",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
constructor() {
|
|
87
|
+
super();
|
|
88
|
+
this.duration = 5000;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
override connectedCallback() {
|
|
92
|
+
this.setAttribute("popover", "manual");
|
|
93
|
+
super.connectedCallback();
|
|
94
|
+
document.addEventListener("gw-dialog-open", this.#onDialogOpen);
|
|
95
|
+
// It stays open while empty, as nothing in it can be clicked, so the
|
|
96
|
+
// live region is in place before a message arrives.
|
|
97
|
+
this.#raise();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
override disconnectedCallback() {
|
|
101
|
+
super.disconnectedCallback();
|
|
102
|
+
document.removeEventListener("gw-dialog-open", this.#onDialogOpen);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
readonly #onDialogOpen = () => {
|
|
106
|
+
if (this.#list.childElementCount > 0) this.#raise();
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
protected override create(root: ShadowRoot) {
|
|
110
|
+
root.append(this.#list);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
show(message: string, tone: Tone = "neutral") {
|
|
114
|
+
const toast = h("div", { class: "toast" }, message);
|
|
115
|
+
|
|
116
|
+
toast.style.setProperty(
|
|
117
|
+
"--gw-toast-tone",
|
|
118
|
+
tone === "neutral"
|
|
119
|
+
? "var(--gw-color-border-loud)"
|
|
120
|
+
: `var(--gw-color-${tone})`,
|
|
121
|
+
);
|
|
122
|
+
this.#raise();
|
|
123
|
+
this.#list.append(toast);
|
|
124
|
+
|
|
125
|
+
const reduced = reducedMotion();
|
|
126
|
+
const slow = motionDuration(this, "--gw-motion-slow", 350);
|
|
127
|
+
|
|
128
|
+
toast.animate(
|
|
129
|
+
[
|
|
130
|
+
{ opacity: 0, translate: "0 0.5rem" },
|
|
131
|
+
{ opacity: 1, translate: "0 0" },
|
|
132
|
+
],
|
|
133
|
+
{ duration: reduced ? 0 : slow, easing: fadeEasing },
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
setTimeout(() => {
|
|
137
|
+
const leave = toast.animate([{ opacity: 1 }, { opacity: 0 }], {
|
|
138
|
+
duration: reduced ? 0 : slow,
|
|
139
|
+
easing: fadeEasing,
|
|
140
|
+
fill: "forwards",
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
leave.addEventListener("finish", () => toast.remove());
|
|
144
|
+
leave.addEventListener("cancel", () => toast.remove());
|
|
145
|
+
}, this.duration);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Puts the toaster on top of the top layer, above any dialog opened since
|
|
150
|
+
* it last showed.
|
|
151
|
+
*/
|
|
152
|
+
#raise() {
|
|
153
|
+
if (!this.isConnected) return;
|
|
154
|
+
|
|
155
|
+
if (this.matches(":popover-open")) this.hidePopover();
|
|
156
|
+
|
|
157
|
+
this.showPopover();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
GwToaster.define("gw-toaster");
|
|
162
|
+
|
|
163
|
+
declare global {
|
|
164
|
+
interface HTMLElementTagNameMap {
|
|
165
|
+
"gw-toaster": GwToaster;
|
|
166
|
+
}
|
|
167
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Groundwork's web components: framework-free custom elements styled by the
|
|
3
|
+
* `--gw-*` tokens. Importing this defines every `gw-` element.
|
|
4
|
+
* Docs: https://groundwork.timmo.dev/components
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
export * from "./element.js";
|
|
9
|
+
|
|
10
|
+
export * from "./styles.js";
|
|
11
|
+
|
|
12
|
+
export * from "./motion.js";
|
|
13
|
+
|
|
14
|
+
export * from "./fields.js";
|
|
15
|
+
|
|
16
|
+
export * from "./layout.js";
|
|
17
|
+
|
|
18
|
+
export * from "./gw-alert.js";
|
|
19
|
+
|
|
20
|
+
export * from "./gw-badge.js";
|
|
21
|
+
|
|
22
|
+
export * from "./gw-banner.js";
|
|
23
|
+
|
|
24
|
+
export * from "./gw-button.js";
|
|
25
|
+
|
|
26
|
+
export * from "./gw-card.js";
|
|
27
|
+
|
|
28
|
+
export * from "./gw-checkbox.js";
|
|
29
|
+
|
|
30
|
+
export * from "./gw-chip.js";
|
|
31
|
+
|
|
32
|
+
export * from "./gw-command-menu.js";
|
|
33
|
+
|
|
34
|
+
export * from "./gw-dialog.js";
|
|
35
|
+
|
|
36
|
+
export * from "./gw-dot.js";
|
|
37
|
+
|
|
38
|
+
export * from "./gw-empty-state.js";
|
|
39
|
+
|
|
40
|
+
export * from "./gw-expansion.js";
|
|
41
|
+
|
|
42
|
+
export * from "./gw-facts.js";
|
|
43
|
+
|
|
44
|
+
export * from "./gw-fade-in.js";
|
|
45
|
+
|
|
46
|
+
export * from "./gw-gauge.js";
|
|
47
|
+
|
|
48
|
+
export * from "./gw-input.js";
|
|
49
|
+
|
|
50
|
+
export * from "./gw-menu-button.js";
|
|
51
|
+
|
|
52
|
+
export * from "./gw-meter.js";
|
|
53
|
+
|
|
54
|
+
export * from "./gw-notification.js";
|
|
55
|
+
|
|
56
|
+
export * from "./gw-notification-center.js";
|
|
57
|
+
|
|
58
|
+
export * from "./gw-option-list.js";
|
|
59
|
+
|
|
60
|
+
export * from "./gw-page-header.js";
|
|
61
|
+
|
|
62
|
+
export * from "./gw-popover.js";
|
|
63
|
+
|
|
64
|
+
export * from "./gw-section-title.js";
|
|
65
|
+
|
|
66
|
+
export * from "./gw-segmented.js";
|
|
67
|
+
|
|
68
|
+
export * from "./gw-select.js";
|
|
69
|
+
|
|
70
|
+
export * from "./gw-setting-row.js";
|
|
71
|
+
|
|
72
|
+
export * from "./gw-skeleton.js";
|
|
73
|
+
|
|
74
|
+
export * from "./gw-spinner.js";
|
|
75
|
+
|
|
76
|
+
export * from "./gw-toast.js";
|