@sierragridteam/design-system 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/README.md +17 -2
- package/assets/sierra-logo-full.png +0 -0
- package/assets/sierra-mark.png +0 -0
- package/package.json +35 -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,280 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { FeatureCollection, Geometry } from "geojson";
|
|
3
|
+
import Callout from "./Callout.astro";
|
|
4
|
+
interface Props {
|
|
5
|
+
data: FeatureCollection<Geometry | null>;
|
|
6
|
+
label: string;
|
|
7
|
+
compact?: boolean;
|
|
8
|
+
basemap?: boolean;
|
|
9
|
+
/** Disable feature selection for a treatment overview; pan and zoom remain available. */
|
|
10
|
+
selectable?: boolean;
|
|
11
|
+
}
|
|
12
|
+
const {
|
|
13
|
+
data,
|
|
14
|
+
label,
|
|
15
|
+
compact = false,
|
|
16
|
+
basemap = false,
|
|
17
|
+
selectable = true,
|
|
18
|
+
} = Astro.props;
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<sierra-geo-map
|
|
22
|
+
class:list={["ds-geo-map", compact && "ds-map-compact"]}
|
|
23
|
+
data-features={JSON.stringify(data)}
|
|
24
|
+
data-basemap={String(basemap)}
|
|
25
|
+
data-selectable={String(selectable)}
|
|
26
|
+
>
|
|
27
|
+
<div class="ds-map-canvas" aria-label={label}></div>
|
|
28
|
+
<p class="ds-map-fallback" role="status">
|
|
29
|
+
Loading map.
|
|
30
|
+
</p>
|
|
31
|
+
{selectable && (
|
|
32
|
+
<div class="ds-map-feature-list" aria-label="Map features">
|
|
33
|
+
{data.features.map((feature, index) => (
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
class="btn btn--outline btn--sm"
|
|
37
|
+
data-feature-index={index}
|
|
38
|
+
>
|
|
39
|
+
{String(feature.properties?.label ?? `Feature ${index + 1}`)}
|
|
40
|
+
{!feature.geometry && " · unlocated"}
|
|
41
|
+
</button>
|
|
42
|
+
))}
|
|
43
|
+
</div>
|
|
44
|
+
)}
|
|
45
|
+
<p
|
|
46
|
+
hidden={!selectable}
|
|
47
|
+
class="ds-map-fallback"
|
|
48
|
+
data-selection
|
|
49
|
+
aria-live="polite"
|
|
50
|
+
>
|
|
51
|
+
Select a feature on the map or in the list.
|
|
52
|
+
</p>
|
|
53
|
+
{!selectable && (
|
|
54
|
+
<ul class="sr-only" aria-label="Map features">
|
|
55
|
+
{data.features.map((feature) => (
|
|
56
|
+
<li>
|
|
57
|
+
{feature.properties?.label}: {feature.properties?.description}
|
|
58
|
+
</li>
|
|
59
|
+
))}
|
|
60
|
+
</ul>
|
|
61
|
+
)}
|
|
62
|
+
{!selectable && data.features.some((feature) => !feature.geometry) && (
|
|
63
|
+
<div class="ds-map-notice">
|
|
64
|
+
<Callout tone="brass" title="Reports without a location">
|
|
65
|
+
<p>
|
|
66
|
+
These records have no coordinates and cannot be placed on the map.
|
|
67
|
+
</p>
|
|
68
|
+
<ul>
|
|
69
|
+
{data.features
|
|
70
|
+
.filter((feature) => !feature.geometry)
|
|
71
|
+
.map((feature) => (
|
|
72
|
+
<li>{feature.properties?.label ?? "Unnamed report"}</li>
|
|
73
|
+
))}
|
|
74
|
+
</ul>
|
|
75
|
+
</Callout>
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
{basemap && (
|
|
79
|
+
<p class="ds-map-attribution">
|
|
80
|
+
<a href="https://www.openstreetmap.org/copyright">
|
|
81
|
+
© OpenStreetMap contributors
|
|
82
|
+
</a>{" "}
|
|
83
|
+
· <a href="https://openfreemap.org/">OpenFreeMap</a>
|
|
84
|
+
</p>
|
|
85
|
+
)}
|
|
86
|
+
</sierra-geo-map>
|
|
87
|
+
<script>
|
|
88
|
+
import maplibregl from "maplibre-gl";
|
|
89
|
+
import type { FeatureCollection, Geometry } from "geojson";
|
|
90
|
+
import "maplibre-gl/dist/maplibre-gl.css";
|
|
91
|
+
import { restyleBasemap, type StyleDoc } from "../lib/basemap-style";
|
|
92
|
+
class SierraGeoMap extends HTMLElement {
|
|
93
|
+
map?: maplibregl.Map;
|
|
94
|
+
observer?: ResizeObserver;
|
|
95
|
+
abort?: AbortController;
|
|
96
|
+
async connectedCallback() {
|
|
97
|
+
this.abort = new AbortController();
|
|
98
|
+
const { signal } = this.abort;
|
|
99
|
+
const data = JSON.parse(
|
|
100
|
+
this.dataset.features!,
|
|
101
|
+
) as FeatureCollection<Geometry | null>;
|
|
102
|
+
const status = this.querySelector<HTMLElement>("[role=status]")!;
|
|
103
|
+
const selection = this.querySelector<HTMLElement>("[data-selection]")!;
|
|
104
|
+
const colors = getComputedStyle(this);
|
|
105
|
+
const token = (name: string) => colors.getPropertyValue(name).trim();
|
|
106
|
+
const select = (index: number) => {
|
|
107
|
+
const feature = data.features[index];
|
|
108
|
+
if (!feature) return;
|
|
109
|
+
selection.textContent = `${feature.properties?.label ?? "Feature"} — ${feature.properties?.description ?? (feature.geometry ? feature.geometry.type : "Location unavailable")}`;
|
|
110
|
+
this.querySelectorAll<HTMLButtonElement>(
|
|
111
|
+
"[data-feature-index]",
|
|
112
|
+
).forEach((button, i) =>
|
|
113
|
+
button.setAttribute("aria-pressed", String(i === index)),
|
|
114
|
+
);
|
|
115
|
+
if (this.map?.getLayer("ds-points"))
|
|
116
|
+
this.map.setPaintProperty("ds-points", "circle-radius", [
|
|
117
|
+
"case",
|
|
118
|
+
["==", ["id"], index],
|
|
119
|
+
10,
|
|
120
|
+
6,
|
|
121
|
+
]);
|
|
122
|
+
this.dispatchEvent(
|
|
123
|
+
new CustomEvent("feature-select", {
|
|
124
|
+
detail: { feature, index },
|
|
125
|
+
bubbles: true,
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
this.querySelectorAll<HTMLButtonElement>("[data-feature-index]").forEach(
|
|
130
|
+
(button) =>
|
|
131
|
+
button.addEventListener(
|
|
132
|
+
"click",
|
|
133
|
+
() => select(Number(button.dataset.featureIndex)),
|
|
134
|
+
{ signal },
|
|
135
|
+
),
|
|
136
|
+
);
|
|
137
|
+
let style: maplibregl.StyleSpecification = {
|
|
138
|
+
version: 8,
|
|
139
|
+
sources: {},
|
|
140
|
+
layers: [
|
|
141
|
+
{
|
|
142
|
+
id: "ground",
|
|
143
|
+
type: "background",
|
|
144
|
+
paint: { "background-color": token("--surface-hero") },
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
};
|
|
148
|
+
let fallback = true;
|
|
149
|
+
if (this.dataset.basemap === "true") {
|
|
150
|
+
try {
|
|
151
|
+
const response = await fetch(
|
|
152
|
+
"https://tiles.openfreemap.org/styles/positron",
|
|
153
|
+
{ signal: AbortSignal.any([signal, AbortSignal.timeout(5000)]) },
|
|
154
|
+
);
|
|
155
|
+
if (!response.ok) throw Error("Basemap failed");
|
|
156
|
+
const original = (await response.json()) as StyleDoc;
|
|
157
|
+
const styled = restyleBasemap(original, {
|
|
158
|
+
land: token("--surface-hero"),
|
|
159
|
+
water: token("--map-water"),
|
|
160
|
+
road: token("--map-road"),
|
|
161
|
+
roadCasing: token("--map-road-casing"),
|
|
162
|
+
countyLine: token("--map-county-line"),
|
|
163
|
+
place: token("--map-place"),
|
|
164
|
+
shieldText: token("--map-shield-text"),
|
|
165
|
+
});
|
|
166
|
+
style = (styled ??
|
|
167
|
+
original) as unknown as maplibregl.StyleSpecification;
|
|
168
|
+
// Route-shield artwork belongs to the engine adapter; retain road labels without missing sprite images.
|
|
169
|
+
style.layers = style.layers.filter(
|
|
170
|
+
(layer) =>
|
|
171
|
+
!(layer.type === "symbol" && layer.layout?.["icon-image"]),
|
|
172
|
+
);
|
|
173
|
+
fallback = false;
|
|
174
|
+
} catch {
|
|
175
|
+
/* Explicit fallback message below. */
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (signal.aborted) return;
|
|
179
|
+
try {
|
|
180
|
+
const map = (this.map = new maplibregl.Map({
|
|
181
|
+
container: this.querySelector<HTMLElement>(".ds-map-canvas")!,
|
|
182
|
+
style,
|
|
183
|
+
center: [-120.4, 38.1],
|
|
184
|
+
zoom: 8,
|
|
185
|
+
scrollZoom: false,
|
|
186
|
+
attributionControl: false,
|
|
187
|
+
}));
|
|
188
|
+
map.addControl(
|
|
189
|
+
new maplibregl.NavigationControl({ showCompass: false }),
|
|
190
|
+
"bottom-right",
|
|
191
|
+
);
|
|
192
|
+
map.on("error", () => {
|
|
193
|
+
status.hidden = false;
|
|
194
|
+
status.textContent =
|
|
195
|
+
"Some map resources are unavailable. Geographic context may be incomplete.";
|
|
196
|
+
});
|
|
197
|
+
map.on("load", () => {
|
|
198
|
+
status.textContent = fallback
|
|
199
|
+
? "Geometry only — geographic basemap is not loaded."
|
|
200
|
+
: "";
|
|
201
|
+
status.hidden = !fallback;
|
|
202
|
+
const located = data.features.flatMap((f, i) =>
|
|
203
|
+
f.geometry ? [{ ...f, geometry: f.geometry, id: i }] : [],
|
|
204
|
+
);
|
|
205
|
+
map.addSource("features", {
|
|
206
|
+
type: "geojson",
|
|
207
|
+
data: { type: "FeatureCollection", features: located },
|
|
208
|
+
});
|
|
209
|
+
const color: maplibregl.ExpressionSpecification = [
|
|
210
|
+
"match",
|
|
211
|
+
["get", "tone"],
|
|
212
|
+
"alarm",
|
|
213
|
+
token("--signal-orange"),
|
|
214
|
+
"brass",
|
|
215
|
+
token("--brand-brass"),
|
|
216
|
+
token("--brand-green"),
|
|
217
|
+
];
|
|
218
|
+
map.addLayer({
|
|
219
|
+
id: "ds-areas",
|
|
220
|
+
type: "fill",
|
|
221
|
+
source: "features",
|
|
222
|
+
filter: ["==", ["geometry-type"], "Polygon"],
|
|
223
|
+
paint: { "fill-color": color, "fill-opacity": 0.12 },
|
|
224
|
+
});
|
|
225
|
+
map.addLayer({
|
|
226
|
+
id: "ds-lines",
|
|
227
|
+
type: "line",
|
|
228
|
+
source: "features",
|
|
229
|
+
filter: ["!=", ["geometry-type"], "Point"],
|
|
230
|
+
paint: { "line-color": color, "line-width": 2 },
|
|
231
|
+
});
|
|
232
|
+
map.addLayer({
|
|
233
|
+
id: "ds-points",
|
|
234
|
+
type: "circle",
|
|
235
|
+
source: "features",
|
|
236
|
+
filter: ["==", ["geometry-type"], "Point"],
|
|
237
|
+
paint: {
|
|
238
|
+
"circle-color": color,
|
|
239
|
+
"circle-radius": 6,
|
|
240
|
+
"circle-stroke-color": token("--surface-raised"),
|
|
241
|
+
"circle-stroke-width": 2,
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
if (this.dataset.selectable !== "false")
|
|
245
|
+
for (const layer of ["ds-areas", "ds-lines", "ds-points"])
|
|
246
|
+
map.on("click", layer, (event) => {
|
|
247
|
+
const id = event.features?.[0]?.id;
|
|
248
|
+
if (typeof id === "number") select(id);
|
|
249
|
+
});
|
|
250
|
+
const bounds = new maplibregl.LngLatBounds();
|
|
251
|
+
const positions = (value: unknown): void => {
|
|
252
|
+
if (!Array.isArray(value)) return;
|
|
253
|
+
if (typeof value[0] === "number" && typeof value[1] === "number")
|
|
254
|
+
bounds.extend([value[0], value[1]]);
|
|
255
|
+
else value.forEach(positions);
|
|
256
|
+
};
|
|
257
|
+
const geometry = (g: Geometry): void => {
|
|
258
|
+
if (g.type === "GeometryCollection") g.geometries.forEach(geometry);
|
|
259
|
+
else positions(g.coordinates);
|
|
260
|
+
};
|
|
261
|
+
located.forEach((f) => geometry(f.geometry!));
|
|
262
|
+
if (!bounds.isEmpty())
|
|
263
|
+
map.fitBounds(bounds, { padding: 40, maxZoom: 12, duration: 0 });
|
|
264
|
+
});
|
|
265
|
+
this.observer = new ResizeObserver(() => map.resize());
|
|
266
|
+
this.observer.observe(this);
|
|
267
|
+
} catch {
|
|
268
|
+
status.textContent =
|
|
269
|
+
"Interactive map unavailable. Feature descriptions remain available.";
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
disconnectedCallback() {
|
|
273
|
+
this.abort?.abort();
|
|
274
|
+
this.observer?.disconnect();
|
|
275
|
+
this.map?.remove();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (!customElements.get("sierra-geo-map"))
|
|
279
|
+
customElements.define("sierra-geo-map", SierraGeoMap);
|
|
280
|
+
</script>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
import GeoJSONMap from "./GeoJSONMap.astro";
|
|
3
|
+
import type { FeatureCollection, Geometry } from "geojson";
|
|
4
|
+
interface Props {
|
|
5
|
+
data: FeatureCollection<Geometry | null>;
|
|
6
|
+
label: string;
|
|
7
|
+
}
|
|
8
|
+
const { data, label } = Astro.props;
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<GeoJSONMap data={data} label={label} compact />
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** Icon — references a symbol from IconSprite. Decorative by default; pass `label` to make it meaningful. */
|
|
3
|
+
interface Props {
|
|
4
|
+
name: string;
|
|
5
|
+
label?: string;
|
|
6
|
+
size?: 'md' | 'lg';
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
const { name, label, size = 'md', class: cls } = Astro.props;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<svg
|
|
13
|
+
class:list={['icon', size === 'lg' && 'icon--lg', cls]}
|
|
14
|
+
aria-hidden={label ? undefined : 'true'}
|
|
15
|
+
role={label ? 'img' : undefined}
|
|
16
|
+
aria-label={label}
|
|
17
|
+
>
|
|
18
|
+
<use href={`#i-${name}`}></use>
|
|
19
|
+
</svg>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* IconSprite — inline SVG symbol sheet, rendered once per page by BaseLayout.
|
|
4
|
+
* Use <Icon name="…" /> in Astro or `icon('…')` from src/lib/dom.ts in client code.
|
|
5
|
+
* Paths are 24×24 stroke icons; colour comes from currentcolor via .icon.
|
|
6
|
+
*/
|
|
7
|
+
const icons: Record<string, string> = {
|
|
8
|
+
copy: '<rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V4H4v12h4"/>',
|
|
9
|
+
home: '<path d="M3 9.5 12 3l9 6.5V20a1 1 0 0 1-1 1h-5v-7H9v7H4a1 1 0 0 1-1-1z"/>',
|
|
10
|
+
grid: '<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/>',
|
|
11
|
+
folder:
|
|
12
|
+
'<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
|
|
13
|
+
file: '<path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5"/>',
|
|
14
|
+
note: '<path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5M9 13h6M9 17h6"/>',
|
|
15
|
+
external:
|
|
16
|
+
'<path d="M14 5h5v5M19 5l-9 9"/><path d="M17 13v5a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1h5"/>',
|
|
17
|
+
receipt:
|
|
18
|
+
'<path d="M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1z"/><path d="M16 8H8M16 12H8M13 16H8"/>',
|
|
19
|
+
users:
|
|
20
|
+
'<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.9M16 3.1a4 4 0 0 1 0 7.8"/>',
|
|
21
|
+
user: '<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>',
|
|
22
|
+
logout:
|
|
23
|
+
'<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="m16 17 5-5-5-5M21 12H9"/>',
|
|
24
|
+
menu: '<path d="M4 6h16M4 12h16M4 18h16"/>',
|
|
25
|
+
x: '<path d="M18 6 6 18M6 6l12 12"/>',
|
|
26
|
+
check: '<path d="M20 6 9 17l-5-5"/>',
|
|
27
|
+
download:
|
|
28
|
+
'<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5M12 15V3"/>',
|
|
29
|
+
"chevron-right": '<path d="m9 18 6-6-6-6"/>',
|
|
30
|
+
"chevron-left": '<path d="m15 18-6-6 6-6"/>',
|
|
31
|
+
"chevron-down": '<path d="m6 9 6 6 6-6"/>',
|
|
32
|
+
alert:
|
|
33
|
+
'<path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/><path d="M12 9v4M12 17h.01"/>',
|
|
34
|
+
mail: '<rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-10 7L2 7"/>',
|
|
35
|
+
shield: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>',
|
|
36
|
+
info: '<circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/>',
|
|
37
|
+
refresh: '<path d="M21 12a9 9 0 1 1-2.6-6.4L21 8"/><path d="M21 3v5h-5"/>',
|
|
38
|
+
clock: '<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>',
|
|
39
|
+
inbox:
|
|
40
|
+
'<path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.5 5.1 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.5-6.9A2 2 0 0 0 16.7 4H7.3a2 2 0 0 0-1.8 1.1z"/>',
|
|
41
|
+
search: '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
|
|
42
|
+
plus: '<path d="M12 5v14M5 12h14"/>',
|
|
43
|
+
ban: '<circle cx="12" cy="12" r="10"/><path d="m4.9 4.9 14.2 14.2"/>',
|
|
44
|
+
edit: '<path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"/>',
|
|
45
|
+
"arrow-up": '<path d="M12 19V5M5 12l7-7 7 7"/>',
|
|
46
|
+
"arrow-down": '<path d="M12 5v14M19 12l-7 7-7-7"/>',
|
|
47
|
+
link: '<path d="M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7l-1.7 1.7"/><path d="M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7l1.7-1.7"/>',
|
|
48
|
+
bold: '<path d="M6 4h8a4 4 0 0 1 0 8H6zM6 12h9a4 4 0 0 1 0 8H6z"/>',
|
|
49
|
+
italic: '<path d="M19 4h-9M14 20H5M15 4 9 20"/>',
|
|
50
|
+
heading: '<path d="M6 4v16M18 4v16M6 12h12"/>',
|
|
51
|
+
list: '<path d="M9 6h11M9 12h11M9 18h11"/><path d="M4 6h.01M4 12h.01M4 18h.01"/>',
|
|
52
|
+
"list-ordered":
|
|
53
|
+
'<path d="M10 6h10M10 12h10M10 18h10"/><path d="M4 4h1v4M4 8h2M4 14.5a1.5 1.5 0 0 1 2.6-1l-2.6 3.5h3"/>',
|
|
54
|
+
quote:
|
|
55
|
+
'<path d="M4 11h5v6H4zM4 11c0-3 1.5-5 4-6M14 11h5v6h-5zM14 11c0-3 1.5-5 4-6"/>',
|
|
56
|
+
code: '<path d="m16 18 6-6-6-6M8 6l-6 6 6 6"/>',
|
|
57
|
+
};
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
<svg
|
|
61
|
+
style="display: none"
|
|
62
|
+
aria-hidden="true"
|
|
63
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
64
|
+
>
|
|
65
|
+
{Object.entries(icons).map(([name, body]) => (
|
|
66
|
+
<symbol id={`i-${name}`} viewBox="0 0 24 24" set:html={body} />
|
|
67
|
+
))}
|
|
68
|
+
</svg>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
import CopyButton from "./CopyButton.astro";
|
|
3
|
+
interface Props {
|
|
4
|
+
label: string;
|
|
5
|
+
values: string[];
|
|
6
|
+
previewCount?: number;
|
|
7
|
+
emptyMessage?: string;
|
|
8
|
+
}
|
|
9
|
+
const {
|
|
10
|
+
label,
|
|
11
|
+
values,
|
|
12
|
+
previewCount = 8,
|
|
13
|
+
emptyMessage = "No identifiers supplied.",
|
|
14
|
+
} = Astro.props;
|
|
15
|
+
const limit = Math.max(1, Math.floor(previewCount));
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
<div class="ds-identifiers">
|
|
19
|
+
<div class="ds-identifiers-head">
|
|
20
|
+
<strong>
|
|
21
|
+
{label} <span class="meta">({values.length})</span>
|
|
22
|
+
</strong>
|
|
23
|
+
{values.length > 0 && (
|
|
24
|
+
<CopyButton value={values.join("\n")} label="Copy all identifiers" />
|
|
25
|
+
)}
|
|
26
|
+
</div>
|
|
27
|
+
{values.length ? (
|
|
28
|
+
<>
|
|
29
|
+
<ul class="ds-identifier-list" aria-label={label}>
|
|
30
|
+
{values.slice(0, limit).map((value) => (
|
|
31
|
+
<li>
|
|
32
|
+
<code>{value}</code>
|
|
33
|
+
<CopyButton value={value} label={`Copy ${value}`} />
|
|
34
|
+
</li>
|
|
35
|
+
))}
|
|
36
|
+
</ul>
|
|
37
|
+
{values.length > limit && (
|
|
38
|
+
<details>
|
|
39
|
+
<summary>Show {values.length - limit} more identifiers</summary>
|
|
40
|
+
<ul class="ds-identifier-list">
|
|
41
|
+
{values.slice(limit).map((value) => (
|
|
42
|
+
<li>
|
|
43
|
+
<code>{value}</code>
|
|
44
|
+
<CopyButton value={value} label={`Copy ${value}`} />
|
|
45
|
+
</li>
|
|
46
|
+
))}
|
|
47
|
+
</ul>
|
|
48
|
+
</details>
|
|
49
|
+
)}
|
|
50
|
+
</>
|
|
51
|
+
) : (
|
|
52
|
+
<p class="ds-data-note">{emptyMessage}</p>
|
|
53
|
+
)}
|
|
54
|
+
</div>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Logo — the S.I.E.R.R.A mark with a configurable site wordmark.
|
|
4
|
+
* The mark is the main site's raster asset, placed on parchment, never recoloured.
|
|
5
|
+
*/
|
|
6
|
+
interface Props {
|
|
7
|
+
href?: string;
|
|
8
|
+
/** Site name shown beneath S.I.E.R.R.A and used in the home link label. */
|
|
9
|
+
siteName?: string;
|
|
10
|
+
height?: number;
|
|
11
|
+
wordmark?: boolean;
|
|
12
|
+
class?: string;
|
|
13
|
+
}
|
|
14
|
+
const {
|
|
15
|
+
href = "/",
|
|
16
|
+
siteName = "Members",
|
|
17
|
+
height = 28,
|
|
18
|
+
wordmark = true,
|
|
19
|
+
class: cls,
|
|
20
|
+
} = Astro.props;
|
|
21
|
+
const width = Math.round((height * 271) / 154);
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
<a
|
|
25
|
+
class:list={["logo", cls]}
|
|
26
|
+
href={href}
|
|
27
|
+
aria-label={["S.I.E.R.R.A", siteName, "home"].filter(Boolean).join(" ")}
|
|
28
|
+
>
|
|
29
|
+
<img
|
|
30
|
+
src="/brand/sierra-mark.png"
|
|
31
|
+
alt=""
|
|
32
|
+
width={width}
|
|
33
|
+
height={height}
|
|
34
|
+
decoding="async"
|
|
35
|
+
/>
|
|
36
|
+
{wordmark && (
|
|
37
|
+
<span class="logo__text">
|
|
38
|
+
<span class="logo__org">S.I.E.R.R.A</span>
|
|
39
|
+
{siteName && <span class="logo__app">{siteName}</span>}
|
|
40
|
+
</span>
|
|
41
|
+
)}
|
|
42
|
+
</a>
|
|
43
|
+
|
|
44
|
+
<style>
|
|
45
|
+
.logo {
|
|
46
|
+
display: inline-flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
gap: var(--space-3);
|
|
49
|
+
color: inherit;
|
|
50
|
+
text-decoration: none;
|
|
51
|
+
}
|
|
52
|
+
.logo:hover {
|
|
53
|
+
text-decoration: none;
|
|
54
|
+
}
|
|
55
|
+
.logo__text {
|
|
56
|
+
display: flex;
|
|
57
|
+
flex-direction: column;
|
|
58
|
+
line-height: 1.1;
|
|
59
|
+
}
|
|
60
|
+
.logo__org {
|
|
61
|
+
font-family: var(--font-display);
|
|
62
|
+
font-size: var(--fs-body);
|
|
63
|
+
font-weight: var(--fw-semibold);
|
|
64
|
+
letter-spacing: 0.06em;
|
|
65
|
+
color: var(--ink-strong);
|
|
66
|
+
}
|
|
67
|
+
.logo__app {
|
|
68
|
+
font-family: var(--font-ui);
|
|
69
|
+
font-size: var(--fs-label);
|
|
70
|
+
font-weight: var(--fw-semibold);
|
|
71
|
+
letter-spacing: var(--tracking-label);
|
|
72
|
+
text-transform: uppercase;
|
|
73
|
+
color: var(--brand-brass-text);
|
|
74
|
+
}
|
|
75
|
+
</style>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
label: string;
|
|
4
|
+
caption?: string;
|
|
5
|
+
}
|
|
6
|
+
const { label, caption } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<figure class="ds-map-frame" aria-label={label}>
|
|
10
|
+
{Astro.slots.has("toolbar") && (
|
|
11
|
+
<div class="ds-map-toolbar">
|
|
12
|
+
<slot name="toolbar" />
|
|
13
|
+
</div>
|
|
14
|
+
)}
|
|
15
|
+
<slot />
|
|
16
|
+
<figcaption>
|
|
17
|
+
{caption && <p>{caption}</p>}
|
|
18
|
+
<slot name="legend" />
|
|
19
|
+
{Astro.slots.has("attribution") && (
|
|
20
|
+
<div class="ds-map-attribution">
|
|
21
|
+
<slot name="attribution" />
|
|
22
|
+
</div>
|
|
23
|
+
)}
|
|
24
|
+
</figcaption>
|
|
25
|
+
</figure>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
label: string;
|
|
4
|
+
items: {
|
|
5
|
+
label: string;
|
|
6
|
+
value?: string | number | null;
|
|
7
|
+
unit?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
missing?: string;
|
|
10
|
+
}[];
|
|
11
|
+
}
|
|
12
|
+
const { label, items } = Astro.props;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<dl class="ds-metric-grid" aria-label={label}>
|
|
16
|
+
{items.map((item) => (
|
|
17
|
+
<div class="ds-metric">
|
|
18
|
+
<dt>{item.label}</dt>
|
|
19
|
+
<dd>
|
|
20
|
+
<span
|
|
21
|
+
class:list={[
|
|
22
|
+
"ds-metric-value",
|
|
23
|
+
item.value == null && "ds-metric-missing",
|
|
24
|
+
]}
|
|
25
|
+
>
|
|
26
|
+
{item.value ?? item.missing ?? "Not reported"}
|
|
27
|
+
</span>
|
|
28
|
+
{item.value != null && item.unit && (
|
|
29
|
+
<span class="ds-metric-unit"> {item.unit}</span>
|
|
30
|
+
)}
|
|
31
|
+
{item.description && <p class="ds-data-note">{item.description}</p>}
|
|
32
|
+
</dd>
|
|
33
|
+
</div>
|
|
34
|
+
))}
|
|
35
|
+
</dl>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** PageHeader — kicker, h1, lead and an actions slot. One per page. */
|
|
3
|
+
interface Props {
|
|
4
|
+
title: string;
|
|
5
|
+
kicker?: string;
|
|
6
|
+
lead?: string;
|
|
7
|
+
}
|
|
8
|
+
const { title, kicker, lead } = Astro.props;
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<header class="page-header">
|
|
12
|
+
<div class="page-header__text">
|
|
13
|
+
{kicker && <span class="kicker">{kicker}</span>}
|
|
14
|
+
<h1>
|
|
15
|
+
<slot name="title">{title}</slot>
|
|
16
|
+
</h1>
|
|
17
|
+
{lead && <p class="lead">{lead}</p>}
|
|
18
|
+
</div>
|
|
19
|
+
{Astro.slots.has('actions') && (
|
|
20
|
+
<div class="page-header__actions">
|
|
21
|
+
<slot name="actions" />
|
|
22
|
+
</div>
|
|
23
|
+
)}
|
|
24
|
+
</header>
|