@sierragridteam/design-system 0.0.0-stage → 0.3.1
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 +17 -2
- package/assets/sierra-logo-full.png +0 -0
- package/assets/sierra-mark.png +0 -0
- package/package.json +36 -4
- package/src/components/Badge.astro +13 -0
- package/src/components/Button.astro +36 -0
- package/src/components/Callout.astro +17 -0
- package/src/components/Card.astro +33 -0
- package/src/components/CopyButton.astro +81 -0
- package/src/components/DataDiff.astro +50 -0
- package/src/components/DataRow.astro +23 -0
- package/src/components/DataStatus.astro +26 -0
- package/src/components/DataTable.astro +25 -0
- package/src/components/EmptyState.astro +17 -0
- package/src/components/Field.astro +25 -0
- package/src/components/FilterBar.astro +10 -0
- package/src/components/GeoJSONMap.astro +280 -0
- package/src/components/GeometryPreview.astro +11 -0
- package/src/components/Icon.astro +19 -0
- package/src/components/IconSprite.astro +68 -0
- package/src/components/IdentifierList.astro +54 -0
- package/src/components/Logo.astro +75 -0
- package/src/components/MapFrame.astro +25 -0
- package/src/components/MetricGrid.astro +35 -0
- package/src/components/PageHeader.astro +24 -0
- package/src/components/Select.astro +202 -0
- package/src/components/ValueTable.astro +32 -0
- package/src/lib/basemap-style.ts +269 -0
- package/src/lib/controls.ts +126 -0
- package/src/lib/data-values.ts +45 -0
- package/src/lib/dialog.ts +85 -0
- package/src/lib/dom.ts +68 -0
- package/src/lib/toast.ts +22 -0
- package/src/styles/base.css +165 -0
- package/src/styles/components.css +2235 -0
- package/src/styles/global.css +4 -0
- package/src/styles/shell.css +501 -0
- package/src/styles/tokens.css +172 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modal dialogs on the native <dialog> element: focus trapping, Escape and the
|
|
3
|
+
* top layer come from the browser. Used by the admin editors.
|
|
4
|
+
*/
|
|
5
|
+
import { el, icon } from './dom';
|
|
6
|
+
|
|
7
|
+
export interface DialogOptions {
|
|
8
|
+
title: string;
|
|
9
|
+
/** Shown above the title, e.g. "New resource". */
|
|
10
|
+
kicker?: string;
|
|
11
|
+
wide?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Called when the dialog is about to close by Escape, the close button or
|
|
14
|
+
* Cancel. Return false to keep it open (e.g. to confirm discarding changes).
|
|
15
|
+
*/
|
|
16
|
+
beforeClose?: () => boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Dialog {
|
|
20
|
+
root: HTMLDialogElement;
|
|
21
|
+
/** Replace the scrolling body. */
|
|
22
|
+
body: HTMLElement;
|
|
23
|
+
/** Replace the footer's contents (actions). */
|
|
24
|
+
footer: HTMLElement;
|
|
25
|
+
setTitle(title: string): void;
|
|
26
|
+
/** Close without asking beforeClose. */
|
|
27
|
+
close(): void;
|
|
28
|
+
/** Close as if the admin cancelled (asks beforeClose). */
|
|
29
|
+
requestClose(): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let dialogCount = 0;
|
|
33
|
+
|
|
34
|
+
export function openDialog(opts: DialogOptions): Dialog {
|
|
35
|
+
const titleId = `dialog-title-${++dialogCount}`;
|
|
36
|
+
const title = el('h2', { class: 'dialog__title', id: titleId }, opts.title);
|
|
37
|
+
const body = el('div', { class: 'dialog__body' });
|
|
38
|
+
const footer = el('div', { class: 'dialog__footer' });
|
|
39
|
+
const closeBtn = el(
|
|
40
|
+
'button',
|
|
41
|
+
{ class: 'btn btn--ghost btn--sm btn--icon', type: 'button', 'aria-label': 'Close' },
|
|
42
|
+
icon('x')
|
|
43
|
+
);
|
|
44
|
+
const root = el(
|
|
45
|
+
'dialog',
|
|
46
|
+
{ class: `dialog${opts.wide ? ' dialog--wide' : ''}`, 'aria-labelledby': titleId },
|
|
47
|
+
el(
|
|
48
|
+
'div',
|
|
49
|
+
{ class: 'dialog__header' },
|
|
50
|
+
el('div', {}, opts.kicker && el('span', { class: 'kicker' }, opts.kicker), title),
|
|
51
|
+
closeBtn
|
|
52
|
+
),
|
|
53
|
+
body,
|
|
54
|
+
footer
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const close = () => {
|
|
58
|
+
if (root.open) root.close();
|
|
59
|
+
root.remove();
|
|
60
|
+
};
|
|
61
|
+
const requestClose = () => {
|
|
62
|
+
if (opts.beforeClose && !opts.beforeClose()) return;
|
|
63
|
+
close();
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
closeBtn.addEventListener('click', requestClose);
|
|
67
|
+
// The browser may close a modal itself (Chrome does on a second Escape without
|
|
68
|
+
// a click in between, skipping "cancel"); treat that as discarding.
|
|
69
|
+
root.addEventListener('close', () => root.remove());
|
|
70
|
+
root.addEventListener('cancel', (e) => {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
requestClose();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
document.body.appendChild(root);
|
|
76
|
+
root.showModal();
|
|
77
|
+
return {
|
|
78
|
+
root,
|
|
79
|
+
body,
|
|
80
|
+
footer,
|
|
81
|
+
setTitle: (t) => (title.textContent = t),
|
|
82
|
+
close,
|
|
83
|
+
requestClose,
|
|
84
|
+
};
|
|
85
|
+
}
|
package/src/lib/dom.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** Small DOM helpers for client-rendered UI. No framework, on purpose. */
|
|
2
|
+
|
|
3
|
+
type Child = Node | string | number | null | undefined | false | Child[];
|
|
4
|
+
|
|
5
|
+
export function el<K extends keyof HTMLElementTagNameMap>(
|
|
6
|
+
tag: K,
|
|
7
|
+
attrs: Record<string, string | number | boolean | null | undefined | ((e: Event) => void)> = {},
|
|
8
|
+
...children: Child[]
|
|
9
|
+
): HTMLElementTagNameMap[K] {
|
|
10
|
+
const node = document.createElement(tag);
|
|
11
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
12
|
+
if (value === null || value === undefined || value === false) continue;
|
|
13
|
+
if (typeof value === 'function') {
|
|
14
|
+
node.addEventListener(key.replace(/^on/, '').toLowerCase(), value as EventListener);
|
|
15
|
+
} else if (key === 'class') {
|
|
16
|
+
node.className = String(value);
|
|
17
|
+
} else if (value === true) {
|
|
18
|
+
node.setAttribute(key, '');
|
|
19
|
+
} else {
|
|
20
|
+
node.setAttribute(key, String(value));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
append(node, children);
|
|
24
|
+
return node;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function append(parent: Node, children: Child[]): void {
|
|
28
|
+
for (const child of children) {
|
|
29
|
+
if (child === null || child === undefined || child === false) continue;
|
|
30
|
+
if (Array.isArray(child)) append(parent, child);
|
|
31
|
+
else parent.appendChild(child instanceof Node ? child : document.createTextNode(String(child)));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** An <svg><use> referencing the page's icon sprite. */
|
|
36
|
+
export function icon(name: string, cls = 'icon'): SVGSVGElement {
|
|
37
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
38
|
+
svg.setAttribute('class', cls);
|
|
39
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
40
|
+
const use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
|
|
41
|
+
use.setAttribute('href', `#i-${name}`);
|
|
42
|
+
svg.appendChild(use);
|
|
43
|
+
return svg;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function qs<T extends Element = HTMLElement>(
|
|
47
|
+
selector: string,
|
|
48
|
+
root: ParentNode = document
|
|
49
|
+
): T {
|
|
50
|
+
const node = root.querySelector<T>(selector);
|
|
51
|
+
if (!node) throw new Error(`missing element: ${selector}`);
|
|
52
|
+
return node;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function replaceChildren(parent: Element, ...children: Child[]): void {
|
|
56
|
+
parent.replaceChildren();
|
|
57
|
+
append(parent, children);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function setBusy(button: HTMLButtonElement, busy: boolean): void {
|
|
61
|
+
button.classList.toggle('is-busy', busy);
|
|
62
|
+
button.disabled = busy;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Skeleton rows for a list while it loads. */
|
|
66
|
+
export function skeletonRows(count = 3): HTMLElement[] {
|
|
67
|
+
return Array.from({ length: count }, () => el('div', { class: 'skeleton skeleton--row' }));
|
|
68
|
+
}
|
package/src/lib/toast.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { el, icon } from './dom';
|
|
2
|
+
|
|
3
|
+
export type ToastTone = 'info' | 'error' | 'warn';
|
|
4
|
+
|
|
5
|
+
/** Non-blocking notice, bottom right. Errors stay until dismissed. */
|
|
6
|
+
export function toast(message: string, tone: ToastTone = 'info'): void {
|
|
7
|
+
const host = document.getElementById('toasts');
|
|
8
|
+
if (!host) return;
|
|
9
|
+
const node = el('div', {
|
|
10
|
+
class: `toast${tone === 'info' ? '' : ` toast--${tone}`}`,
|
|
11
|
+
role: 'status',
|
|
12
|
+
});
|
|
13
|
+
const close = el(
|
|
14
|
+
'button',
|
|
15
|
+
{ class: 'toast__close', type: 'button', 'aria-label': 'Dismiss' },
|
|
16
|
+
icon('x')
|
|
17
|
+
);
|
|
18
|
+
close.addEventListener('click', () => node.remove());
|
|
19
|
+
node.append(el('span', {}, message), close);
|
|
20
|
+
host.appendChild(node);
|
|
21
|
+
if (tone !== 'error') setTimeout(() => node.remove(), 5000);
|
|
22
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/* Element defaults and accessibility primitives. Tokens only. */
|
|
2
|
+
|
|
3
|
+
*,
|
|
4
|
+
*::before,
|
|
5
|
+
*::after {
|
|
6
|
+
box-sizing: border-box;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
* {
|
|
10
|
+
margin: 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
html {
|
|
14
|
+
color-scheme: light;
|
|
15
|
+
scroll-behavior: smooth;
|
|
16
|
+
scroll-padding-top: calc(var(--topbar-h) + var(--space-4));
|
|
17
|
+
-webkit-text-size-adjust: 100%;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
body {
|
|
21
|
+
min-height: 100dvh;
|
|
22
|
+
font-family: var(--font-ui);
|
|
23
|
+
font-size: var(--fs-body);
|
|
24
|
+
font-weight: var(--fw-regular);
|
|
25
|
+
line-height: var(--lh-body);
|
|
26
|
+
color: var(--ink-body);
|
|
27
|
+
background: var(--surface-page);
|
|
28
|
+
-webkit-font-smoothing: antialiased;
|
|
29
|
+
text-rendering: optimizelegibility;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
h1,
|
|
33
|
+
h2 {
|
|
34
|
+
font-family: var(--font-display);
|
|
35
|
+
font-weight: var(--fw-medium);
|
|
36
|
+
line-height: var(--lh-heading);
|
|
37
|
+
letter-spacing: var(--tracking-tight);
|
|
38
|
+
color: var(--ink-strong);
|
|
39
|
+
text-wrap: balance;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
h1 {
|
|
43
|
+
font-size: var(--fs-h1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
h2 {
|
|
47
|
+
font-size: var(--fs-h2);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
h3,
|
|
51
|
+
h4 {
|
|
52
|
+
font-family: var(--font-ui);
|
|
53
|
+
font-size: var(--fs-h3);
|
|
54
|
+
font-weight: var(--fw-semibold);
|
|
55
|
+
line-height: var(--lh-ui);
|
|
56
|
+
color: var(--ink-strong);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
p {
|
|
60
|
+
text-wrap: pretty;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
a {
|
|
64
|
+
color: var(--brand-green);
|
|
65
|
+
text-decoration: none;
|
|
66
|
+
text-underline-offset: 0.18em;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
a:hover {
|
|
70
|
+
color: var(--brand-green-hover);
|
|
71
|
+
text-decoration: underline;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
p a,
|
|
75
|
+
li a {
|
|
76
|
+
text-decoration: underline;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
strong {
|
|
80
|
+
font-weight: var(--fw-semibold);
|
|
81
|
+
color: var(--ink-strong);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
code,
|
|
85
|
+
kbd,
|
|
86
|
+
samp {
|
|
87
|
+
font-family: var(--font-mono);
|
|
88
|
+
font-size: 0.92em;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
img,
|
|
92
|
+
svg {
|
|
93
|
+
display: block;
|
|
94
|
+
max-width: 100%;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
button,
|
|
98
|
+
input,
|
|
99
|
+
select,
|
|
100
|
+
textarea {
|
|
101
|
+
font: inherit;
|
|
102
|
+
color: inherit;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
button {
|
|
106
|
+
cursor: pointer;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
::selection {
|
|
110
|
+
background: color-mix(in srgb, var(--brand-green) 18%, transparent);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
:where(a, button, input, textarea, select, summary, [tabindex]):focus-visible {
|
|
114
|
+
outline: 2px solid var(--brand-green);
|
|
115
|
+
outline-offset: 2px;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.skip-link {
|
|
119
|
+
position: absolute;
|
|
120
|
+
top: var(--space-2);
|
|
121
|
+
left: var(--space-2);
|
|
122
|
+
z-index: var(--z-skiplink);
|
|
123
|
+
padding: var(--space-2) var(--space-4);
|
|
124
|
+
background: var(--brand-green);
|
|
125
|
+
color: var(--surface-page);
|
|
126
|
+
font-size: var(--fs-label-lg);
|
|
127
|
+
font-weight: var(--fw-semibold);
|
|
128
|
+
letter-spacing: var(--tracking-label);
|
|
129
|
+
text-transform: uppercase;
|
|
130
|
+
border-radius: var(--radius-sm);
|
|
131
|
+
transform: translateY(-200%);
|
|
132
|
+
transition: transform var(--transition);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.skip-link:focus {
|
|
136
|
+
transform: none;
|
|
137
|
+
text-decoration: none;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.sr-only {
|
|
141
|
+
position: absolute;
|
|
142
|
+
width: 1px;
|
|
143
|
+
height: 1px;
|
|
144
|
+
padding: 0;
|
|
145
|
+
margin: -1px;
|
|
146
|
+
overflow: hidden;
|
|
147
|
+
clip: rect(0, 0, 0, 0);
|
|
148
|
+
white-space: nowrap;
|
|
149
|
+
border: 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
[hidden] {
|
|
153
|
+
display: none !important;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@media (prefers-reduced-motion: reduce) {
|
|
157
|
+
*,
|
|
158
|
+
*::before,
|
|
159
|
+
*::after {
|
|
160
|
+
animation-duration: 0.001ms !important;
|
|
161
|
+
animation-iteration-count: 1 !important;
|
|
162
|
+
transition-duration: 0.001ms !important;
|
|
163
|
+
scroll-behavior: auto !important;
|
|
164
|
+
}
|
|
165
|
+
}
|