@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/layout.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gw-page`, `gw-stack`, `gw-row` and `gw-grid`: page width and spacing.
|
|
3
|
+
* Docs: https://groundwork.timmo.dev/components/layout/#stack-row-and-grid
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import { GwElement, css, h } from "./element.js";
|
|
8
|
+
import { base } from "./styles.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Centers a page's content at the theme's width, with the page gap between
|
|
12
|
+
* its children.
|
|
13
|
+
*
|
|
14
|
+
* @element gw-page
|
|
15
|
+
* @summary A page's content column.
|
|
16
|
+
*
|
|
17
|
+
* @attr {boolean} narrow - Uses the narrow content width, for forms and settings.
|
|
18
|
+
*
|
|
19
|
+
* @slot - The page's content.
|
|
20
|
+
*
|
|
21
|
+
* @csspart page - The content column.
|
|
22
|
+
*/
|
|
23
|
+
export class GwPage extends GwElement {
|
|
24
|
+
static override properties = {
|
|
25
|
+
narrow: { type: "boolean", reflect: true },
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
static override styles = [
|
|
29
|
+
base,
|
|
30
|
+
css`
|
|
31
|
+
:host {
|
|
32
|
+
display: block;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.page {
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
gap: var(--gw-space-6);
|
|
39
|
+
width: 100%;
|
|
40
|
+
max-width: var(--gw-content-width);
|
|
41
|
+
margin-inline: auto;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
:host([narrow]) .page {
|
|
45
|
+
max-width: var(--gw-content-width-narrow);
|
|
46
|
+
}
|
|
47
|
+
`,
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
declare narrow: boolean;
|
|
51
|
+
|
|
52
|
+
constructor() {
|
|
53
|
+
super();
|
|
54
|
+
this.narrow = false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
protected override create(root: ShadowRoot) {
|
|
58
|
+
root.append(h("div", { class: "page", part: "page" }, h("slot")));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const gaps = css`
|
|
63
|
+
:host([gap="s"]) {
|
|
64
|
+
--gw-gap: var(--gw-space-1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
:host([gap="l"]) {
|
|
68
|
+
--gw-gap: var(--gw-space-6);
|
|
69
|
+
}
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
/** A layout element whose shadow root is a single slot. */
|
|
73
|
+
class GwSlotted extends GwElement {
|
|
74
|
+
protected override create(root: ShadowRoot) {
|
|
75
|
+
root.append(h("slot"));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Stacks its children in a column.
|
|
81
|
+
*
|
|
82
|
+
* @element gw-stack
|
|
83
|
+
* @summary Children in a column.
|
|
84
|
+
*
|
|
85
|
+
* @attr {"s"|"l"} gap - A smaller or larger gap.
|
|
86
|
+
*
|
|
87
|
+
* @slot - The children.
|
|
88
|
+
*
|
|
89
|
+
* @cssprop --gw-gap - The gap. Defaults to `--gw-space-3`.
|
|
90
|
+
*/
|
|
91
|
+
export class GwStack extends GwSlotted {
|
|
92
|
+
static override styles = [
|
|
93
|
+
base,
|
|
94
|
+
gaps,
|
|
95
|
+
css`
|
|
96
|
+
:host {
|
|
97
|
+
display: flex;
|
|
98
|
+
flex-direction: column;
|
|
99
|
+
gap: var(--gw-gap, var(--gw-space-3));
|
|
100
|
+
}
|
|
101
|
+
`,
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Lines its children up in a row, wrapping when they run out of room.
|
|
107
|
+
*
|
|
108
|
+
* @element gw-row
|
|
109
|
+
* @summary Children in a wrapping row.
|
|
110
|
+
*
|
|
111
|
+
* @attr {"s"|"l"} gap - A smaller or larger gap.
|
|
112
|
+
* @attr {boolean} nowrap - Keeps the children on one line.
|
|
113
|
+
*
|
|
114
|
+
* @slot - The children.
|
|
115
|
+
*
|
|
116
|
+
* @cssprop --gw-gap - The gap. Defaults to `--gw-space-2`.
|
|
117
|
+
*/
|
|
118
|
+
export class GwRow extends GwSlotted {
|
|
119
|
+
static override styles = [
|
|
120
|
+
base,
|
|
121
|
+
gaps,
|
|
122
|
+
css`
|
|
123
|
+
:host {
|
|
124
|
+
display: flex;
|
|
125
|
+
flex-wrap: wrap;
|
|
126
|
+
gap: var(--gw-gap, var(--gw-space-2));
|
|
127
|
+
align-items: center;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
:host([nowrap]) {
|
|
131
|
+
flex-wrap: nowrap;
|
|
132
|
+
}
|
|
133
|
+
`,
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Fills as many columns as fit, each at least `--gw-grid-min` wide.
|
|
139
|
+
*
|
|
140
|
+
* @element gw-grid
|
|
141
|
+
* @summary Children in responsive columns.
|
|
142
|
+
*
|
|
143
|
+
* @attr {"s"|"l"} gap - A smaller or larger gap.
|
|
144
|
+
*
|
|
145
|
+
* @slot - The children.
|
|
146
|
+
*
|
|
147
|
+
* @cssprop --gw-grid-min - The narrowest a column gets. Defaults to `18rem`.
|
|
148
|
+
* @cssprop --gw-gap - The gap. Defaults to `--gw-space-3`.
|
|
149
|
+
*/
|
|
150
|
+
export class GwGrid extends GwSlotted {
|
|
151
|
+
static override styles = [
|
|
152
|
+
base,
|
|
153
|
+
gaps,
|
|
154
|
+
css`
|
|
155
|
+
:host {
|
|
156
|
+
display: grid;
|
|
157
|
+
grid-template-columns: repeat(
|
|
158
|
+
auto-fill,
|
|
159
|
+
minmax(min(100%, var(--gw-grid-min, 18rem)), 1fr)
|
|
160
|
+
);
|
|
161
|
+
gap: var(--gw-gap, var(--gw-space-3));
|
|
162
|
+
}
|
|
163
|
+
`,
|
|
164
|
+
];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
GwPage.define("gw-page");
|
|
168
|
+
|
|
169
|
+
GwStack.define("gw-stack");
|
|
170
|
+
|
|
171
|
+
GwRow.define("gw-row");
|
|
172
|
+
|
|
173
|
+
GwGrid.define("gw-grid");
|
|
174
|
+
|
|
175
|
+
declare global {
|
|
176
|
+
interface HTMLElementTagNameMap {
|
|
177
|
+
"gw-page": GwPage;
|
|
178
|
+
"gw-stack": GwStack;
|
|
179
|
+
"gw-row": GwRow;
|
|
180
|
+
"gw-grid": GwGrid;
|
|
181
|
+
}
|
|
182
|
+
}
|
package/src/motion.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Motion helpers: fades that never pop, heights that glide instead of jump,
|
|
3
|
+
* and View Transitions for whole-view changes. All respect reduced motion.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Whether the person asked for reduced motion. */
|
|
9
|
+
export const reducedMotion = () =>
|
|
10
|
+
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Reads a motion duration token in milliseconds, such as
|
|
14
|
+
* `--gw-motion-slow`, falling back when the tokens aren't loaded.
|
|
15
|
+
*/
|
|
16
|
+
export const motionDuration = (
|
|
17
|
+
element: Element,
|
|
18
|
+
token: string,
|
|
19
|
+
fallback: number,
|
|
20
|
+
) =>
|
|
21
|
+
Number.parseFloat(getComputedStyle(element).getPropertyValue(token)) ||
|
|
22
|
+
fallback;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The curve for fades. A decelerating curve lands most of an opacity change
|
|
26
|
+
* in the first frames and reads as a pop, so fades ease in and out.
|
|
27
|
+
*/
|
|
28
|
+
export const fadeEasing = "cubic-bezier(0.4, 0, 0.2, 1)";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Fades `element` in, after `delay`, over `duration` (`--gw-motion-slow` by
|
|
32
|
+
* default). Use it when content replaces a skeleton. With reduced motion it
|
|
33
|
+
* shows at once.
|
|
34
|
+
*/
|
|
35
|
+
export const fadeIn = (
|
|
36
|
+
element: HTMLElement,
|
|
37
|
+
{ delay = 0, duration }: { delay?: number; duration?: number } = {},
|
|
38
|
+
) =>
|
|
39
|
+
element.animate([{ opacity: 0 }, { opacity: 1 }], {
|
|
40
|
+
delay,
|
|
41
|
+
duration: reducedMotion()
|
|
42
|
+
? 0
|
|
43
|
+
: (duration ?? motionDuration(element, "--gw-motion-slow", 350)),
|
|
44
|
+
easing: fadeEasing,
|
|
45
|
+
fill: "backwards",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Glides `element` from the height it had before a change to the height it
|
|
50
|
+
* has after, as an expansion panel opens, so content arriving or going never
|
|
51
|
+
* jumps the layout. Measure with `before = element.offsetHeight` ahead of the
|
|
52
|
+
* change, then call this once it's rendered. Skipped when motion is reduced.
|
|
53
|
+
*/
|
|
54
|
+
export const animateHeight = (element: HTMLElement, before: number) => {
|
|
55
|
+
const after = element.offsetHeight;
|
|
56
|
+
|
|
57
|
+
if (before === after || before === 0 || reducedMotion()) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const duration = motionDuration(element, "--gw-motion-normal", 250);
|
|
62
|
+
|
|
63
|
+
element.animate(
|
|
64
|
+
{ height: [`${before}px`, `${after}px`], overflow: ["clip", "clip"] },
|
|
65
|
+
{ duration, easing: "cubic-bezier(0.2, 0, 0, 1)" },
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Runs a DOM change inside a View Transition where the browser supports one
|
|
71
|
+
* and motion isn't reduced, otherwise runs it straight away, after Home
|
|
72
|
+
* Assistant's `withViewTransition()`. Use it for changes to the whole view,
|
|
73
|
+
* such as switching theme or mode; component state uses CSS transitions.
|
|
74
|
+
*
|
|
75
|
+
* The change may return a promise, such as a Lit element's
|
|
76
|
+
* `updateComplete`, so the new view is captured once it has rendered. Keep
|
|
77
|
+
* it quick: the page is frozen until it settles.
|
|
78
|
+
*/
|
|
79
|
+
export const withViewTransition = (
|
|
80
|
+
change: () => void | Promise<void>,
|
|
81
|
+
): Promise<void> => {
|
|
82
|
+
const reduced = reducedMotion();
|
|
83
|
+
|
|
84
|
+
if (document.startViewTransition === undefined || reduced) {
|
|
85
|
+
return Promise.resolve(change());
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let changed = false;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const transition = document.startViewTransition(() => {
|
|
92
|
+
changed = true;
|
|
93
|
+
|
|
94
|
+
return change();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// A transition skipped by a newer one rejects with AbortError; the change
|
|
98
|
+
// itself still happened.
|
|
99
|
+
transition.ready.catch(() => undefined);
|
|
100
|
+
|
|
101
|
+
return transition.finished.catch(() => undefined);
|
|
102
|
+
} catch {
|
|
103
|
+
return changed ? Promise.resolve() : Promise.resolve(change());
|
|
104
|
+
}
|
|
105
|
+
};
|
package/src/styles.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Styles and helpers every component shares: the base sheet, tones and icons.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { css } from "./element.js";
|
|
7
|
+
|
|
8
|
+
const svgNamespace = "http://www.w3.org/2000/svg";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* What every component shares: box sizing, the focus ring, icons and the
|
|
12
|
+
* tones that set `--gw-tone` from a `tone` attribute.
|
|
13
|
+
*/
|
|
14
|
+
export const base = css`
|
|
15
|
+
:host {
|
|
16
|
+
font-family: var(--gw-font-sans);
|
|
17
|
+
color: var(--gw-color-text-primary);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
*,
|
|
21
|
+
*::before,
|
|
22
|
+
*::after {
|
|
23
|
+
box-sizing: border-box;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
[hidden] {
|
|
27
|
+
display: none !important;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
:focus-visible {
|
|
31
|
+
outline: var(--gw-focus-width) solid var(--gw-color-focus);
|
|
32
|
+
outline-offset: var(--gw-focus-offset);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.icon {
|
|
36
|
+
flex: none;
|
|
37
|
+
width: var(--gw-icon-size, 1.25rem);
|
|
38
|
+
height: var(--gw-icon-size, 1.25rem);
|
|
39
|
+
fill: currentColor;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
:host([tone="accent"]) {
|
|
43
|
+
--gw-tone: var(--gw-color-accent);
|
|
44
|
+
--gw-tone-fill: var(--gw-color-fill-accent-quiet);
|
|
45
|
+
--gw-tone-border: var(--gw-color-border-accent);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
:host([tone="info"]) {
|
|
49
|
+
--gw-tone: var(--gw-color-info);
|
|
50
|
+
--gw-tone-fill: var(--gw-color-fill-info-quiet);
|
|
51
|
+
--gw-tone-border: var(--gw-color-border-info);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
:host([tone="success"]) {
|
|
55
|
+
--gw-tone: var(--gw-color-success);
|
|
56
|
+
--gw-tone-fill: var(--gw-color-fill-success-quiet);
|
|
57
|
+
--gw-tone-border: var(--gw-color-border-success);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
:host([tone="warning"]) {
|
|
61
|
+
--gw-tone: var(--gw-color-warning);
|
|
62
|
+
--gw-tone-fill: var(--gw-color-fill-warning-quiet);
|
|
63
|
+
--gw-tone-border: var(--gw-color-border-warning);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
:host([tone="danger"]) {
|
|
67
|
+
--gw-tone: var(--gw-color-danger);
|
|
68
|
+
--gw-tone-fill: var(--gw-color-fill-danger-quiet);
|
|
69
|
+
--gw-tone-border: var(--gw-color-border-danger);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
:host([tone="important"]) {
|
|
73
|
+
--gw-tone: var(--gw-color-important);
|
|
74
|
+
--gw-tone-fill: var(--gw-color-fill-important-quiet);
|
|
75
|
+
--gw-tone-border: var(--gw-color-border-important);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@media (prefers-reduced-motion: reduce) {
|
|
79
|
+
*,
|
|
80
|
+
*::before,
|
|
81
|
+
*::after {
|
|
82
|
+
animation-duration: 0s !important;
|
|
83
|
+
transition-duration: 0s !important;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
/** The tones a component's `tone` attribute takes. */
|
|
89
|
+
export type Tone =
|
|
90
|
+
| "neutral"
|
|
91
|
+
| "accent"
|
|
92
|
+
| "info"
|
|
93
|
+
| "success"
|
|
94
|
+
| "warning"
|
|
95
|
+
| "danger"
|
|
96
|
+
| "important";
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* An icon from SVG path data, such as one of `@mdi/js`'s or Lucide's paths,
|
|
100
|
+
* so each app picks its own set. Without a label it's hidden from assistive
|
|
101
|
+
* technology. Change its path later with `setIconPath`.
|
|
102
|
+
*/
|
|
103
|
+
export const createIcon = (path: string, label?: string) => {
|
|
104
|
+
const element = document.createElementNS(svgNamespace, "svg");
|
|
105
|
+
|
|
106
|
+
element.classList.add("icon");
|
|
107
|
+
element.setAttribute("viewBox", "0 0 24 24");
|
|
108
|
+
element.setAttribute("role", label === undefined ? "presentation" : "img");
|
|
109
|
+
element.setAttribute("aria-hidden", label === undefined ? "true" : "false");
|
|
110
|
+
|
|
111
|
+
if (label !== undefined) element.setAttribute("aria-label", label);
|
|
112
|
+
|
|
113
|
+
element.append(document.createElementNS(svgNamespace, "path"));
|
|
114
|
+
setIconPath(element, path);
|
|
115
|
+
|
|
116
|
+
return element;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/** Changes an icon's path data. */
|
|
120
|
+
export const setIconPath = (icon: SVGSVGElement, path: string) => {
|
|
121
|
+
icon.querySelector("path")?.setAttribute("d", path);
|
|
122
|
+
};
|