@aceshooting/lyra-ui 0.1.0 → 0.1.2
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 +77 -6
- package/custom-elements.json +12015 -0
- package/dist/components/chart/bar-chart.d.ts +14 -0
- package/dist/components/chart/bar-chart.js +14 -0
- package/dist/components/chart/box-plot.d.ts +46 -0
- package/dist/components/chart/box-plot.js +145 -0
- package/dist/components/chart/box-plot.styles.d.ts +1 -0
- package/dist/components/chart/box-plot.styles.js +22 -0
- package/dist/components/chart/bubble-chart.d.ts +19 -0
- package/dist/components/chart/bubble-chart.js +18 -0
- package/dist/components/chart/chart-loader.d.ts +8 -0
- package/dist/components/chart/chart-loader.js +26 -0
- package/dist/components/chart/chart.d.ts +91 -0
- package/dist/components/chart/chart.js +275 -0
- package/dist/components/chart/chart.styles.d.ts +1 -0
- package/dist/components/chart/chart.styles.js +33 -0
- package/dist/components/chart/doughnut-chart.d.ts +15 -0
- package/dist/components/chart/doughnut-chart.js +14 -0
- package/dist/components/chart/histogram-bin.d.ts +6 -0
- package/dist/components/chart/histogram-bin.js +19 -0
- package/dist/components/chart/histogram.d.ts +20 -0
- package/dist/components/chart/histogram.js +70 -0
- package/dist/components/chart/histogram.styles.d.ts +1 -0
- package/dist/components/chart/histogram.styles.js +1 -0
- package/dist/components/chart/line-chart.d.ts +14 -0
- package/dist/components/chart/line-chart.js +14 -0
- package/dist/components/chart/pie-chart.d.ts +16 -0
- package/dist/components/chart/pie-chart.js +15 -0
- package/dist/components/chart/polar-area-chart.d.ts +15 -0
- package/dist/components/chart/polar-area-chart.js +14 -0
- package/dist/components/chart/radar-chart.d.ts +15 -0
- package/dist/components/chart/radar-chart.js +14 -0
- package/dist/components/chart/scatter-chart.d.ts +15 -0
- package/dist/components/chart/scatter-chart.js +15 -0
- package/dist/components/combobox/combobox.d.ts +16 -0
- package/dist/components/combobox/combobox.js +113 -6
- package/dist/components/combobox/combobox.styles.js +68 -7
- package/dist/components/combobox/option.d.ts +2 -2
- package/dist/components/combobox/option.js +11 -7
- package/dist/components/date-picker/date-input.d.ts +12 -1
- package/dist/components/date-picker/date-input.js +68 -6
- package/dist/components/date-picker/date-input.styles.js +58 -6
- package/dist/components/date-picker/date-picker.js +5 -2
- package/dist/components/date-picker/date-picker.styles.js +9 -2
- package/dist/components/empty/empty.d.ts +32 -0
- package/dist/components/empty/empty.js +90 -0
- package/dist/components/empty/empty.styles.d.ts +1 -0
- package/dist/components/empty/empty.styles.js +42 -0
- package/dist/components/export-button/csv.d.ts +10 -0
- package/dist/components/export-button/csv.js +28 -0
- package/dist/components/export-button/export-button.d.ts +41 -0
- package/dist/components/export-button/export-button.js +151 -0
- package/dist/components/export-button/export-button.styles.d.ts +1 -0
- package/dist/components/export-button/export-button.styles.js +79 -0
- package/dist/components/file-input/accept.d.ts +14 -0
- package/dist/components/file-input/accept.js +30 -0
- package/dist/components/file-input/file-input.d.ts +47 -0
- package/dist/components/file-input/file-input.js +182 -0
- package/dist/components/file-input/file-input.styles.d.ts +1 -0
- package/dist/components/file-input/file-input.styles.js +44 -0
- package/dist/components/flag/flag.d.ts +4 -0
- package/dist/components/flag/flag.js +24 -1
- package/dist/components/flag/flag.styles.js +1 -1
- package/dist/components/gauge/gauge.d.ts +29 -0
- package/dist/components/gauge/gauge.js +133 -0
- package/dist/components/gauge/gauge.styles.d.ts +1 -0
- package/dist/components/gauge/gauge.styles.js +60 -0
- package/dist/components/graph/graph.d.ts +68 -0
- package/dist/components/graph/graph.js +276 -0
- package/dist/components/graph/graph.styles.d.ts +1 -0
- package/dist/components/graph/graph.styles.js +34 -0
- package/dist/components/heatmap/heatmap-scale.d.ts +8 -0
- package/dist/components/heatmap/heatmap-scale.js +17 -0
- package/dist/components/heatmap/heatmap.d.ts +66 -0
- package/dist/components/heatmap/heatmap.js +252 -0
- package/dist/components/heatmap/heatmap.styles.d.ts +1 -0
- package/dist/components/heatmap/heatmap.styles.js +39 -0
- package/dist/components/map/map-loader.d.ts +8 -0
- package/dist/components/map/map-loader.js +17 -0
- package/dist/components/map/map.d.ts +49 -0
- package/dist/components/map/map.js +189 -0
- package/dist/components/map/map.styles.d.ts +1 -0
- package/dist/components/map/map.styles.js +48 -0
- package/dist/components/playback/playback.d.ts +51 -0
- package/dist/components/playback/playback.js +146 -0
- package/dist/components/playback/playback.styles.d.ts +1 -0
- package/dist/components/playback/playback.styles.js +40 -0
- package/dist/components/skeleton/skeleton.d.ts +26 -0
- package/dist/components/skeleton/skeleton.js +65 -0
- package/dist/components/skeleton/skeleton.styles.d.ts +1 -0
- package/dist/components/skeleton/skeleton.styles.js +57 -0
- package/dist/components/split/split.d.ts +41 -0
- package/dist/components/split/split.js +171 -0
- package/dist/components/split/split.styles.d.ts +1 -0
- package/dist/components/split/split.styles.js +51 -0
- package/dist/components/stat/stat.d.ts +36 -0
- package/dist/components/stat/stat.js +117 -0
- package/dist/components/stat/stat.styles.d.ts +1 -0
- package/dist/components/stat/stat.styles.js +86 -0
- package/dist/components/table/table.d.ts +54 -0
- package/dist/components/table/table.js +201 -0
- package/dist/components/table/table.styles.d.ts +1 -0
- package/dist/components/table/table.styles.js +98 -0
- package/dist/components/time-range/time-range.d.ts +40 -0
- package/dist/components/time-range/time-range.js +219 -0
- package/dist/components/time-range/time-range.styles.d.ts +1 -0
- package/dist/components/time-range/time-range.styles.js +78 -0
- package/dist/components/toast/toast-item.js +10 -2
- package/dist/components/toast/toast-item.styles.js +13 -4
- package/dist/components/toast/toast.styles.js +0 -1
- package/dist/components/tree/tree-node.d.ts +50 -0
- package/dist/components/tree/tree-node.js +196 -0
- package/dist/components/tree/tree.d.ts +65 -0
- package/dist/components/tree/tree.js +227 -0
- package/dist/components/tree/tree.styles.d.ts +1 -0
- package/dist/components/tree/tree.styles.js +13 -0
- package/dist/internal/form-associated.js +49 -1
- package/dist/internal/icons.d.ts +11 -0
- package/dist/internal/icons.js +64 -0
- package/dist/internal/tokens.styles.js +41 -1
- package/dist/lyra.d.ts +67 -0
- package/dist/lyra.js +55 -0
- package/llms-full.txt +1425 -0
- package/llms.txt +47 -0
- package/package.json +60 -11
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { html } from 'lit';
|
|
8
|
+
import { property, query } from 'lit/decorators.js';
|
|
9
|
+
import { LyraElement } from '../../internal/lyra-element.js';
|
|
10
|
+
import { defineElement } from '../../internal/prefix.js';
|
|
11
|
+
import { linearAlpha, sqrtStep } from './heatmap-scale.js';
|
|
12
|
+
import { styles } from './heatmap.styles.js';
|
|
13
|
+
const PAD_LEFT = 60;
|
|
14
|
+
const PAD_TOP = 20;
|
|
15
|
+
const NO_DATA_FILL = 'rgba(128,128,128,0.25)';
|
|
16
|
+
const RAMP_STEPS = 7;
|
|
17
|
+
const FALLBACK_SCALE_LO = '#cde2fb';
|
|
18
|
+
const FALLBACK_SCALE_HI = '#0969da';
|
|
19
|
+
const HEX_RE = /^([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
20
|
+
const RGB_RE = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i;
|
|
21
|
+
/**
|
|
22
|
+
* Parses a strict `#rgb` or `#rrggbb` hex string into an `[r, g, b]` triple,
|
|
23
|
+
* or `null` if `hex` isn't one (rather than silently coercing an unparsable
|
|
24
|
+
* string to `0` via `Number.parseInt(..., 16)` returning `NaN`).
|
|
25
|
+
*/
|
|
26
|
+
export function hexToRgb(hex) {
|
|
27
|
+
const clean = hex.trim().replace('#', '');
|
|
28
|
+
if (!HEX_RE.test(clean))
|
|
29
|
+
return null;
|
|
30
|
+
const full = clean.length === 3 ? clean.split('').map((c) => c + c).join('') : clean;
|
|
31
|
+
const num = Number.parseInt(full, 16);
|
|
32
|
+
return [(num >> 16) & 255, (num >> 8) & 255, num & 255];
|
|
33
|
+
}
|
|
34
|
+
function parseRgbString(value) {
|
|
35
|
+
const match = RGB_RE.exec(value);
|
|
36
|
+
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
|
|
37
|
+
}
|
|
38
|
+
let scratchCtx;
|
|
39
|
+
/** A detached 1x1 canvas used solely to normalize CSS color strings. */
|
|
40
|
+
function getScratchCtx() {
|
|
41
|
+
if (scratchCtx === undefined) {
|
|
42
|
+
scratchCtx = document.createElement('canvas').getContext('2d');
|
|
43
|
+
}
|
|
44
|
+
return scratchCtx;
|
|
45
|
+
}
|
|
46
|
+
const warnedInvalidColors = new Set();
|
|
47
|
+
function warnInvalidColor(color) {
|
|
48
|
+
if (warnedInvalidColors.has(color))
|
|
49
|
+
return;
|
|
50
|
+
warnedInvalidColors.add(color);
|
|
51
|
+
console.warn(`<lyra-heatmap> could not parse "${color}" (set via --lyra-heatmap-scale-lo/-hi) as a CSS ` +
|
|
52
|
+
'color; falling back to the default ramp endpoint.');
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Resolves any syntactically valid CSS `<color>` — hex, `rgb()`, `hsl()`,
|
|
56
|
+
* `oklch()`, a named color, etc. — to an `[r, g, b]` triple.
|
|
57
|
+
*
|
|
58
|
+
* Hand-rolling a parser for every CSS color syntax is unnecessary and
|
|
59
|
+
* error-prone (a naive hex-only parser silently turns an unrecognized format
|
|
60
|
+
* into `NaN` -> `0`, i.e. solid black). The canvas 2D context already
|
|
61
|
+
* implements the full CSS color grammar via its `fillStyle` setter, so this
|
|
62
|
+
* normalizes through that instead. Assigning an unparsable string to
|
|
63
|
+
* `fillStyle` is a spec'd no-op (the previous value is kept, it never
|
|
64
|
+
* throws), so a sentinel round-trip is used to detect that case and fall
|
|
65
|
+
* back to `fallbackHex` (with a one-time `console.warn`) instead of
|
|
66
|
+
* silently drawing the wrong color.
|
|
67
|
+
*/
|
|
68
|
+
export function resolveRgb(color, fallbackHex) {
|
|
69
|
+
const fallback = hexToRgb(fallbackHex) ?? [0, 0, 0];
|
|
70
|
+
const direct = hexToRgb(color);
|
|
71
|
+
if (direct)
|
|
72
|
+
return direct;
|
|
73
|
+
const ctx = getScratchCtx();
|
|
74
|
+
if (!ctx)
|
|
75
|
+
return fallback;
|
|
76
|
+
const sentinel = 'rgb(1, 2, 3)';
|
|
77
|
+
ctx.fillStyle = sentinel;
|
|
78
|
+
const sentinelNormalized = ctx.fillStyle;
|
|
79
|
+
ctx.fillStyle = color;
|
|
80
|
+
if (ctx.fillStyle === sentinelNormalized && color.trim() !== sentinel) {
|
|
81
|
+
warnInvalidColor(color);
|
|
82
|
+
return fallback;
|
|
83
|
+
}
|
|
84
|
+
const normalized = ctx.fillStyle;
|
|
85
|
+
return hexToRgb(normalized) ?? parseRgbString(normalized) ?? fallback;
|
|
86
|
+
}
|
|
87
|
+
/** Linearly interpolates between two already-resolved `[r, g, b]` triples at `t` in `[0, 1]`. */
|
|
88
|
+
function mixRgb(from, to, t) {
|
|
89
|
+
const clamped = Math.min(1, Math.max(0, t));
|
|
90
|
+
const r = Math.round(from[0] + (to[0] - from[0]) * clamped);
|
|
91
|
+
const g = Math.round(from[1] + (to[1] - from[1]) * clamped);
|
|
92
|
+
const b = Math.round(from[2] + (to[2] - from[2]) * clamped);
|
|
93
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
94
|
+
}
|
|
95
|
+
/** Linearly interpolates between two CSS colors (any valid syntax) at `t` in `[0, 1]`. */
|
|
96
|
+
export function mixColor(fromColor, toColor, t) {
|
|
97
|
+
return mixRgb(resolveRgb(fromColor, FALLBACK_SCALE_LO), resolveRgb(toColor, FALLBACK_SCALE_HI), t);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* `<lyra-heatmap>` — a Canvas matrix heatmap with a DPR-aware, resize-aware
|
|
101
|
+
* redraw loop. `-1` cells are treated as "no data". The sequential color
|
|
102
|
+
* ramp's endpoints are read from the `--lyra-heatmap-scale-lo`/`-hi` custom
|
|
103
|
+
* properties (declared in `heatmap.styles.ts`) so hosts can retheme it —
|
|
104
|
+
* canvas can't consume `var()` directly, so they're resolved once per draw
|
|
105
|
+
* via `getComputedStyle`, then normalized to RGB by `resolveRgb()` (any
|
|
106
|
+
* valid CSS color syntax, not just hex — see its doc comment).
|
|
107
|
+
*
|
|
108
|
+
* @customElement lyra-heatmap
|
|
109
|
+
* @csspart base, canvas, legend
|
|
110
|
+
*/
|
|
111
|
+
export class LyraHeatmap extends LyraElement {
|
|
112
|
+
constructor() {
|
|
113
|
+
super(...arguments);
|
|
114
|
+
this.rowLabels = [];
|
|
115
|
+
this.colLabels = [];
|
|
116
|
+
this.values = [];
|
|
117
|
+
this.cellSize = 22;
|
|
118
|
+
this.valueLabel = 'value';
|
|
119
|
+
this.scale = 'linear';
|
|
120
|
+
this.onDprChange = () => {
|
|
121
|
+
this.watchDpr();
|
|
122
|
+
this.draw();
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
static { this.styles = [LyraElement.styles, styles]; }
|
|
126
|
+
connectedCallback() {
|
|
127
|
+
super.connectedCallback();
|
|
128
|
+
this.resizeObserver = new ResizeObserver(() => this.draw());
|
|
129
|
+
this.resizeObserver.observe(this);
|
|
130
|
+
this.watchDpr();
|
|
131
|
+
}
|
|
132
|
+
disconnectedCallback() {
|
|
133
|
+
super.disconnectedCallback();
|
|
134
|
+
this.resizeObserver?.disconnect();
|
|
135
|
+
this.dprQuery?.removeEventListener('change', this.onDprChange);
|
|
136
|
+
}
|
|
137
|
+
watchDpr() {
|
|
138
|
+
this.dprQuery = matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
|
|
139
|
+
this.dprQuery.addEventListener('change', this.onDprChange);
|
|
140
|
+
}
|
|
141
|
+
willUpdate() {
|
|
142
|
+
const rows = this.rowLabels.length;
|
|
143
|
+
const cols = this.colLabels.length;
|
|
144
|
+
const flat = this.values.flat().filter((v) => v >= 0);
|
|
145
|
+
const range = flat.length ? `${Math.min(...flat)}–${Math.max(...flat)}` : 'no data';
|
|
146
|
+
this.setAttribute('role', 'img');
|
|
147
|
+
this.setAttribute('aria-label', `Heatmap of ${rows} × ${cols} cells, ${this.valueLabel} range ${range}`);
|
|
148
|
+
}
|
|
149
|
+
updated() {
|
|
150
|
+
this.draw();
|
|
151
|
+
}
|
|
152
|
+
/** Reads the customizable ramp endpoints off the host's computed style. */
|
|
153
|
+
scaleEndpoints() {
|
|
154
|
+
const cs = getComputedStyle(this);
|
|
155
|
+
const lo = cs.getPropertyValue('--lyra-heatmap-scale-lo').trim() || FALLBACK_SCALE_LO;
|
|
156
|
+
const hi = cs.getPropertyValue('--lyra-heatmap-scale-hi').trim() || FALLBACK_SCALE_HI;
|
|
157
|
+
return [lo, hi];
|
|
158
|
+
}
|
|
159
|
+
/** Resolves the `--lyra-color-text-quiet` chrome token for axis labels. */
|
|
160
|
+
labelColor() {
|
|
161
|
+
return getComputedStyle(this).getPropertyValue('--lyra-color-text-quiet').trim() || '#6b7280';
|
|
162
|
+
}
|
|
163
|
+
draw() {
|
|
164
|
+
if (!this.canvas)
|
|
165
|
+
return;
|
|
166
|
+
const rows = this.rowLabels.length;
|
|
167
|
+
const cols = this.colLabels.length;
|
|
168
|
+
const w = PAD_LEFT + cols * this.cellSize;
|
|
169
|
+
const h = PAD_TOP + rows * this.cellSize;
|
|
170
|
+
const dpr = window.devicePixelRatio || 1;
|
|
171
|
+
this.canvas.width = w * dpr;
|
|
172
|
+
this.canvas.height = h * dpr;
|
|
173
|
+
this.canvas.style.width = `${w}px`;
|
|
174
|
+
this.canvas.style.height = `${h}px`;
|
|
175
|
+
const ctx = this.canvas.getContext('2d');
|
|
176
|
+
if (!ctx)
|
|
177
|
+
return;
|
|
178
|
+
ctx.scale(dpr, dpr);
|
|
179
|
+
ctx.clearRect(0, 0, w, h);
|
|
180
|
+
const flat = this.values.flat().filter((v) => v >= 0);
|
|
181
|
+
const lo = flat.length ? Math.min(...flat) : 0;
|
|
182
|
+
const hi = flat.length ? Math.max(...flat) : 1;
|
|
183
|
+
const [scaleLo, scaleHi] = this.scaleEndpoints();
|
|
184
|
+
const loRgb = resolveRgb(scaleLo, FALLBACK_SCALE_LO);
|
|
185
|
+
const hiRgb = resolveRgb(scaleHi, FALLBACK_SCALE_HI);
|
|
186
|
+
const ramp = Array.from({ length: RAMP_STEPS }, (_, i) => mixRgb(loRgb, hiRgb, i / (RAMP_STEPS - 1)));
|
|
187
|
+
for (let r = 0; r < rows; r++) {
|
|
188
|
+
for (let c = 0; c < cols; c++) {
|
|
189
|
+
const v = this.values[r]?.[c] ?? -1;
|
|
190
|
+
const x = PAD_LEFT + c * this.cellSize;
|
|
191
|
+
const y = PAD_TOP + r * this.cellSize;
|
|
192
|
+
if (v < 0) {
|
|
193
|
+
ctx.fillStyle = NO_DATA_FILL;
|
|
194
|
+
}
|
|
195
|
+
else if (this.scale === 'sqrt') {
|
|
196
|
+
const step = sqrtStep(v, hi, RAMP_STEPS);
|
|
197
|
+
ctx.fillStyle = step < 0 ? NO_DATA_FILL : ramp[step];
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
// linearAlpha() returns a 0.1-1.0 ramp position; reused here as a
|
|
201
|
+
// mix ratio between the two ramp-endpoint colors (rather than as a
|
|
202
|
+
// literal canvas alpha channel) so the 'linear' scale also respects
|
|
203
|
+
// --lyra-heatmap-scale-lo/-hi.
|
|
204
|
+
const t = linearAlpha(v, lo, hi);
|
|
205
|
+
ctx.fillStyle = mixRgb(loRgb, hiRgb, t);
|
|
206
|
+
}
|
|
207
|
+
ctx.fillRect(x, y, this.cellSize - 1, this.cellSize - 1);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
ctx.fillStyle = this.labelColor();
|
|
211
|
+
ctx.font = '10px sans-serif';
|
|
212
|
+
this.rowLabels.forEach((label, r) => {
|
|
213
|
+
ctx.fillText(label, 4, PAD_TOP + r * this.cellSize + this.cellSize / 2 + 3);
|
|
214
|
+
});
|
|
215
|
+
this.colLabels.forEach((label, c) => {
|
|
216
|
+
ctx.fillText(label, PAD_LEFT + c * this.cellSize + 2, PAD_TOP - 6);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
render() {
|
|
220
|
+
return html `
|
|
221
|
+
<div part="base">
|
|
222
|
+
<canvas part="canvas"></canvas>
|
|
223
|
+
<div part="legend">
|
|
224
|
+
<span class="bar"></span>
|
|
225
|
+
<span>${this.valueLabel}</span>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
`;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
__decorate([
|
|
232
|
+
property({ attribute: false })
|
|
233
|
+
], LyraHeatmap.prototype, "rowLabels", void 0);
|
|
234
|
+
__decorate([
|
|
235
|
+
property({ attribute: false })
|
|
236
|
+
], LyraHeatmap.prototype, "colLabels", void 0);
|
|
237
|
+
__decorate([
|
|
238
|
+
property({ attribute: false })
|
|
239
|
+
], LyraHeatmap.prototype, "values", void 0);
|
|
240
|
+
__decorate([
|
|
241
|
+
property({ type: Number, attribute: 'cell-size' })
|
|
242
|
+
], LyraHeatmap.prototype, "cellSize", void 0);
|
|
243
|
+
__decorate([
|
|
244
|
+
property({ attribute: 'value-label' })
|
|
245
|
+
], LyraHeatmap.prototype, "valueLabel", void 0);
|
|
246
|
+
__decorate([
|
|
247
|
+
property()
|
|
248
|
+
], LyraHeatmap.prototype, "scale", void 0);
|
|
249
|
+
__decorate([
|
|
250
|
+
query('canvas')
|
|
251
|
+
], LyraHeatmap.prototype, "canvas", void 0);
|
|
252
|
+
defineElement('heatmap', LyraHeatmap);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const styles: import("lit").CSSResult;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { css } from 'lit';
|
|
2
|
+
export const styles = css `
|
|
3
|
+
:host {
|
|
4
|
+
display: block;
|
|
5
|
+
/* Sequential ramp endpoints for the data-driven cell colors (canvas can't
|
|
6
|
+
consume var() directly, so heatmap.ts resolves these via
|
|
7
|
+
getComputedStyle and interpolates between them). Componentspecific
|
|
8
|
+
business color per Global Constraints — exposed so hosts can retheme
|
|
9
|
+
the ramp without raw hex leaking into the public API. */
|
|
10
|
+
--lyra-heatmap-scale-lo: #cde2fb;
|
|
11
|
+
--lyra-heatmap-scale-hi: #0969da;
|
|
12
|
+
}
|
|
13
|
+
[part='base'] {
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
gap: var(--lyra-space-xs);
|
|
17
|
+
}
|
|
18
|
+
canvas {
|
|
19
|
+
display: block;
|
|
20
|
+
inline-size: 100%;
|
|
21
|
+
}
|
|
22
|
+
[part='legend'] {
|
|
23
|
+
display: flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
gap: var(--lyra-space-xs);
|
|
26
|
+
font-size: 0.75rem;
|
|
27
|
+
color: var(--lyra-color-text-quiet);
|
|
28
|
+
}
|
|
29
|
+
[part='legend'] .bar {
|
|
30
|
+
inline-size: 6rem;
|
|
31
|
+
block-size: 0.5rem;
|
|
32
|
+
border-radius: 2px;
|
|
33
|
+
background: linear-gradient(
|
|
34
|
+
to right,
|
|
35
|
+
var(--lyra-heatmap-scale-lo),
|
|
36
|
+
var(--lyra-heatmap-scale-hi)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type * as MaplibreModule from 'maplibre-gl';
|
|
2
|
+
/**
|
|
3
|
+
* Lazily loads the optional peer dependency `maplibre-gl` once per page.
|
|
4
|
+
* Resolves to `null` (with a one-time warning) if it isn't installed —
|
|
5
|
+
* mirrors `<lyra-flag>`'s peer-dependency pattern. Consumers must separately
|
|
6
|
+
* import `maplibre-gl/dist/maplibre-gl.css` once they install the peer.
|
|
7
|
+
*/
|
|
8
|
+
export declare function loadMaplibre(): Promise<typeof MaplibreModule | null>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
let maplibre;
|
|
2
|
+
/**
|
|
3
|
+
* Lazily loads the optional peer dependency `maplibre-gl` once per page.
|
|
4
|
+
* Resolves to `null` (with a one-time warning) if it isn't installed —
|
|
5
|
+
* mirrors `<lyra-flag>`'s peer-dependency pattern. Consumers must separately
|
|
6
|
+
* import `maplibre-gl/dist/maplibre-gl.css` once they install the peer.
|
|
7
|
+
*/
|
|
8
|
+
export function loadMaplibre() {
|
|
9
|
+
if (!maplibre) {
|
|
10
|
+
maplibre = import('maplibre-gl').catch(() => {
|
|
11
|
+
console.warn('<lyra-map> needs the optional peer dependency `maplibre-gl` — install it with ' +
|
|
12
|
+
'`pnpm add maplibre-gl` and import `maplibre-gl/dist/maplibre-gl.css` once in your app.');
|
|
13
|
+
return null;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return maplibre;
|
|
17
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type TemplateResult, type PropertyValues } from 'lit';
|
|
2
|
+
import type { Map as MaplibreMap, StyleSpecification } from 'maplibre-gl';
|
|
3
|
+
import { LyraElement } from '../../internal/lyra-element.js';
|
|
4
|
+
import '../skeleton/skeleton.js';
|
|
5
|
+
export interface LegendEntry {
|
|
6
|
+
color: string;
|
|
7
|
+
label: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ChoroplethLayer {
|
|
10
|
+
sourceId: string;
|
|
11
|
+
geojson: GeoJSON.FeatureCollection;
|
|
12
|
+
field: string;
|
|
13
|
+
stops: [number, string][];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* `<lyra-map>` — a maplibre-gl wrapper with a declarative legend and
|
|
17
|
+
* choropleth GeoJSON layer, plus a raw `map` escape hatch. Requires the
|
|
18
|
+
* optional peer dep `maplibre-gl` (consumers also import its CSS once).
|
|
19
|
+
*
|
|
20
|
+
* @customElement lyra-map
|
|
21
|
+
* @event lyra-map-load - Fired once the underlying maplibregl.Map loads.
|
|
22
|
+
* @event lyra-map-click - `detail: { lngLat, feature? }`.
|
|
23
|
+
* @csspart base, container, legend, legend-swatch
|
|
24
|
+
*/
|
|
25
|
+
export declare class LyraMap extends LyraElement {
|
|
26
|
+
static styles: import("lit").CSSResultGroup[];
|
|
27
|
+
center: [number, number];
|
|
28
|
+
zoom: number;
|
|
29
|
+
mapStyle: StyleSpecification | string;
|
|
30
|
+
legend: LegendEntry[];
|
|
31
|
+
choropleth?: ChoroplethLayer;
|
|
32
|
+
/** True until the lazy-loaded `maplibre-gl` peer dependency has settled (success or failure). */
|
|
33
|
+
private loading;
|
|
34
|
+
private containerEl?;
|
|
35
|
+
private _map?;
|
|
36
|
+
private _styleLoaded;
|
|
37
|
+
/** The raw `maplibregl.Map` instance — escape hatch for anything this wrapper doesn't expose. */
|
|
38
|
+
get map(): MaplibreMap | undefined;
|
|
39
|
+
connectedCallback(): void;
|
|
40
|
+
disconnectedCallback(): void;
|
|
41
|
+
protected updated(changed: PropertyValues): void;
|
|
42
|
+
private applyChoropleth;
|
|
43
|
+
render(): TemplateResult;
|
|
44
|
+
}
|
|
45
|
+
declare global {
|
|
46
|
+
interface HTMLElementTagNameMap {
|
|
47
|
+
'lyra-map': LyraMap;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
/// <reference types="geojson" />
|
|
8
|
+
import { html, nothing } from 'lit';
|
|
9
|
+
import { property, query, state } from 'lit/decorators.js';
|
|
10
|
+
import { LyraElement } from '../../internal/lyra-element.js';
|
|
11
|
+
import { defineElement } from '../../internal/prefix.js';
|
|
12
|
+
import { loadMaplibre } from './map-loader.js';
|
|
13
|
+
import { styles } from './map.styles.js';
|
|
14
|
+
import '../skeleton/skeleton.js';
|
|
15
|
+
const DEFAULT_STYLE = {
|
|
16
|
+
version: 8,
|
|
17
|
+
sources: {
|
|
18
|
+
'lyra-osm': {
|
|
19
|
+
type: 'raster',
|
|
20
|
+
tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
|
|
21
|
+
tileSize: 256,
|
|
22
|
+
attribution: '© OpenStreetMap contributors',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
layers: [{ id: 'lyra-osm', type: 'raster', source: 'lyra-osm' }],
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* `<lyra-map>` — a maplibre-gl wrapper with a declarative legend and
|
|
29
|
+
* choropleth GeoJSON layer, plus a raw `map` escape hatch. Requires the
|
|
30
|
+
* optional peer dep `maplibre-gl` (consumers also import its CSS once).
|
|
31
|
+
*
|
|
32
|
+
* @customElement lyra-map
|
|
33
|
+
* @event lyra-map-load - Fired once the underlying maplibregl.Map loads.
|
|
34
|
+
* @event lyra-map-click - `detail: { lngLat, feature? }`.
|
|
35
|
+
* @csspart base, container, legend, legend-swatch
|
|
36
|
+
*/
|
|
37
|
+
export class LyraMap extends LyraElement {
|
|
38
|
+
constructor() {
|
|
39
|
+
super(...arguments);
|
|
40
|
+
this.center = [0, 0];
|
|
41
|
+
this.zoom = 2;
|
|
42
|
+
this.mapStyle = DEFAULT_STYLE;
|
|
43
|
+
this.legend = [];
|
|
44
|
+
/** True until the lazy-loaded `maplibre-gl` peer dependency has settled (success or failure). */
|
|
45
|
+
this.loading = true;
|
|
46
|
+
// Tracks whether the style has fired its initial 'load' (i.e. addSource/
|
|
47
|
+
// addLayer/setPaintProperty are now safe to call), rather than re-querying
|
|
48
|
+
// `this._map.isStyleLoaded()`: that also reflects in-flight *tile* loading
|
|
49
|
+
// for every source, so it flips back to `false` as soon as the choropleth's
|
|
50
|
+
// own GeoJSON source is added — which would wrongly block subsequent
|
|
51
|
+
// choropleth updates from ever re-running.
|
|
52
|
+
this._styleLoaded = false;
|
|
53
|
+
}
|
|
54
|
+
static { this.styles = [LyraElement.styles, styles]; }
|
|
55
|
+
/** The raw `maplibregl.Map` instance — escape hatch for anything this wrapper doesn't expose. */
|
|
56
|
+
get map() {
|
|
57
|
+
return this._map;
|
|
58
|
+
}
|
|
59
|
+
connectedCallback() {
|
|
60
|
+
super.connectedCallback();
|
|
61
|
+
void loadMaplibre().then(async (mod) => {
|
|
62
|
+
this.loading = false;
|
|
63
|
+
if (!mod)
|
|
64
|
+
return;
|
|
65
|
+
// `[part="container"]` only exists once `loading` flips to `false` and
|
|
66
|
+
// Lit re-renders — wait for that render to land before querying it.
|
|
67
|
+
await this.updateComplete;
|
|
68
|
+
// The element may have been removed from the DOM while the async
|
|
69
|
+
// loadMaplibre()/updateComplete window was in flight — don't spin up
|
|
70
|
+
// a maplibre Map (WebGL context + event listeners) for a detached
|
|
71
|
+
// instance (disconnectedCallback's cleanup already ran).
|
|
72
|
+
if (!this.containerEl || !this.isConnected)
|
|
73
|
+
return;
|
|
74
|
+
this._map = new mod.Map({
|
|
75
|
+
container: this.containerEl,
|
|
76
|
+
style: this.mapStyle,
|
|
77
|
+
center: this.center,
|
|
78
|
+
zoom: this.zoom,
|
|
79
|
+
});
|
|
80
|
+
this._map.on('load', () => {
|
|
81
|
+
this._styleLoaded = true;
|
|
82
|
+
this.applyChoropleth();
|
|
83
|
+
this.emit('lyra-map-load');
|
|
84
|
+
});
|
|
85
|
+
this._map.on('click', (e) => {
|
|
86
|
+
// Guard on the fill layer actually existing, not just `this.choropleth` being
|
|
87
|
+
// set: applyChoropleth() only adds it once the style has finished loading, so
|
|
88
|
+
// a click landing in that window (choropleth set before the map's own 'load')
|
|
89
|
+
// would otherwise query a layer id maplibre-gl doesn't know about yet.
|
|
90
|
+
const fillLayerId = this.choropleth ? `${this.choropleth.sourceId}-fill` : undefined;
|
|
91
|
+
const features = fillLayerId && this._map.getLayer(fillLayerId)
|
|
92
|
+
? this._map.queryRenderedFeatures(e.point, { layers: [fillLayerId] })
|
|
93
|
+
: [];
|
|
94
|
+
this.emit('lyra-map-click', {
|
|
95
|
+
lngLat: [e.lngLat.lng, e.lngLat.lat],
|
|
96
|
+
feature: features[0],
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
disconnectedCallback() {
|
|
102
|
+
super.disconnectedCallback();
|
|
103
|
+
this._map?.remove();
|
|
104
|
+
this._map = undefined;
|
|
105
|
+
this._styleLoaded = false;
|
|
106
|
+
}
|
|
107
|
+
updated(changed) {
|
|
108
|
+
if (this.loading)
|
|
109
|
+
this.setAttribute('aria-busy', 'true');
|
|
110
|
+
else
|
|
111
|
+
this.removeAttribute('aria-busy');
|
|
112
|
+
if (changed.has('choropleth') && this._styleLoaded)
|
|
113
|
+
this.applyChoropleth();
|
|
114
|
+
if (changed.has('center') && this._map)
|
|
115
|
+
this._map.setCenter(this.center);
|
|
116
|
+
if (changed.has('zoom') && this._map)
|
|
117
|
+
this._map.setZoom(this.zoom);
|
|
118
|
+
}
|
|
119
|
+
applyChoropleth() {
|
|
120
|
+
if (!this.choropleth || !this._map)
|
|
121
|
+
return;
|
|
122
|
+
const { sourceId, geojson, field, stops } = this.choropleth;
|
|
123
|
+
const fillLayerId = `${sourceId}-fill`;
|
|
124
|
+
const colorExpr = ['interpolate', ['linear'], ['get', field]];
|
|
125
|
+
for (const [value, color] of stops)
|
|
126
|
+
colorExpr.push(value, color);
|
|
127
|
+
const existingSource = this._map.getSource(sourceId);
|
|
128
|
+
if (existingSource) {
|
|
129
|
+
// Re-apply both the data and the color expression: `field`/`stops` may have
|
|
130
|
+
// changed even though `sourceId` didn't (e.g. switching which metric colors
|
|
131
|
+
// the same GeoJSON), so the paint property must be refreshed too.
|
|
132
|
+
existingSource.setData(geojson);
|
|
133
|
+
this._map.setPaintProperty(fillLayerId, 'fill-color', colorExpr);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
this._map.addSource(sourceId, { type: 'geojson', data: geojson, promoteId: 'id' });
|
|
137
|
+
this._map.addLayer({
|
|
138
|
+
id: fillLayerId,
|
|
139
|
+
type: 'fill',
|
|
140
|
+
source: sourceId,
|
|
141
|
+
paint: { 'fill-color': colorExpr, 'fill-opacity': 0.75 },
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
render() {
|
|
146
|
+
// Lit's own whitespace/comment marker nodes around the `${...}` binding mean
|
|
147
|
+
// `[part="legend"]` is never truly `:empty` in CSS even with zero entries, so
|
|
148
|
+
// the panel is omitted from the template entirely when `legend` is empty
|
|
149
|
+
// (same `nothing` pattern `stat.ts` uses to omit its optional trend section)
|
|
150
|
+
// rather than relying on a CSS `:empty` selector that can never match.
|
|
151
|
+
return html `
|
|
152
|
+
<div part="base">
|
|
153
|
+
${this.loading
|
|
154
|
+
? html `<lyra-skeleton variant="rect"></lyra-skeleton>`
|
|
155
|
+
: html `<div part="container"></div>`}
|
|
156
|
+
${this.legend.length
|
|
157
|
+
? html `<div part="legend">
|
|
158
|
+
${this.legend.map((entry) => html `<div class="legend-row">
|
|
159
|
+
<span part="legend-swatch" style=${`background:${entry.color}`}></span>
|
|
160
|
+
<span>${entry.label}</span>
|
|
161
|
+
</div>`)}
|
|
162
|
+
</div>`
|
|
163
|
+
: nothing}
|
|
164
|
+
</div>
|
|
165
|
+
`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
__decorate([
|
|
169
|
+
property({ type: Array })
|
|
170
|
+
], LyraMap.prototype, "center", void 0);
|
|
171
|
+
__decorate([
|
|
172
|
+
property({ type: Number })
|
|
173
|
+
], LyraMap.prototype, "zoom", void 0);
|
|
174
|
+
__decorate([
|
|
175
|
+
property({ attribute: false })
|
|
176
|
+
], LyraMap.prototype, "mapStyle", void 0);
|
|
177
|
+
__decorate([
|
|
178
|
+
property({ attribute: false })
|
|
179
|
+
], LyraMap.prototype, "legend", void 0);
|
|
180
|
+
__decorate([
|
|
181
|
+
property({ attribute: false })
|
|
182
|
+
], LyraMap.prototype, "choropleth", void 0);
|
|
183
|
+
__decorate([
|
|
184
|
+
state()
|
|
185
|
+
], LyraMap.prototype, "loading", void 0);
|
|
186
|
+
__decorate([
|
|
187
|
+
query('[part="container"]')
|
|
188
|
+
], LyraMap.prototype, "containerEl", void 0);
|
|
189
|
+
defineElement('map', LyraMap);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const styles: import("lit").CSSResult;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { css } from 'lit';
|
|
2
|
+
export const styles = css `
|
|
3
|
+
:host {
|
|
4
|
+
display: block;
|
|
5
|
+
position: relative;
|
|
6
|
+
inline-size: 100%;
|
|
7
|
+
block-size: 24rem;
|
|
8
|
+
}
|
|
9
|
+
[part='base'] {
|
|
10
|
+
position: relative;
|
|
11
|
+
inline-size: 100%;
|
|
12
|
+
block-size: 100%;
|
|
13
|
+
}
|
|
14
|
+
lyra-skeleton {
|
|
15
|
+
--lyra-skeleton-w: 100%;
|
|
16
|
+
--lyra-skeleton-h: 100%;
|
|
17
|
+
}
|
|
18
|
+
[part='container'] {
|
|
19
|
+
position: absolute;
|
|
20
|
+
inset: 0;
|
|
21
|
+
}
|
|
22
|
+
[part='legend'] {
|
|
23
|
+
position: absolute;
|
|
24
|
+
inset-block-end: var(--lyra-space-s);
|
|
25
|
+
inset-inline-start: var(--lyra-space-s);
|
|
26
|
+
z-index: 1;
|
|
27
|
+
display: flex;
|
|
28
|
+
flex-direction: column;
|
|
29
|
+
gap: 0.2rem;
|
|
30
|
+
padding: var(--lyra-space-xs) var(--lyra-space-s);
|
|
31
|
+
background: var(--lyra-color-surface);
|
|
32
|
+
border: 1px solid var(--lyra-color-border);
|
|
33
|
+
border-radius: var(--lyra-radius);
|
|
34
|
+
box-shadow: var(--lyra-shadow);
|
|
35
|
+
font-size: 0.75rem;
|
|
36
|
+
}
|
|
37
|
+
.legend-row {
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
gap: 0.35rem;
|
|
41
|
+
}
|
|
42
|
+
[part='legend-swatch'] {
|
|
43
|
+
inline-size: 0.75rem;
|
|
44
|
+
block-size: 0.75rem;
|
|
45
|
+
border-radius: 2px;
|
|
46
|
+
flex: 0 0 auto;
|
|
47
|
+
}
|
|
48
|
+
`;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type TemplateResult, type PropertyValues } from 'lit';
|
|
2
|
+
import { LyraElement } from '../../internal/lyra-element.js';
|
|
3
|
+
/**
|
|
4
|
+
* `<lyra-playback>` — steps an index through `[0, length)` on a fixed
|
|
5
|
+
* interval (play/pause), like the manual play-timers duplicated across
|
|
6
|
+
* per-repo time-series dashboards.
|
|
7
|
+
*
|
|
8
|
+
* @customElement lyra-playback
|
|
9
|
+
* @event lyra-play
|
|
10
|
+
* @event lyra-pause
|
|
11
|
+
* @event lyra-step - `detail: { index }`, fired on every tick and manual step.
|
|
12
|
+
* @csspart base, play-button, slider
|
|
13
|
+
*/
|
|
14
|
+
export declare class LyraPlayback extends LyraElement {
|
|
15
|
+
static styles: import("lit").CSSResultGroup[];
|
|
16
|
+
length: number;
|
|
17
|
+
index: number;
|
|
18
|
+
intervalMs: number;
|
|
19
|
+
playing: boolean;
|
|
20
|
+
loop: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Re-declared as a Lit reactive property (shadowing the inherited plain
|
|
23
|
+
* `HTMLElement.hidden` IDL property) so that setting `el.hidden = true` —
|
|
24
|
+
* or the `hidden` attribute — actually enters Lit's change-tracking system
|
|
25
|
+
* and the `willUpdate` auto-pause guard below can see it via `changed`.
|
|
26
|
+
* `reflect: true` preserves the native attribute-reflection behavior.
|
|
27
|
+
*/
|
|
28
|
+
hidden: boolean;
|
|
29
|
+
private timer?;
|
|
30
|
+
disconnectedCallback(): void;
|
|
31
|
+
protected willUpdate(changed: PropertyValues): void;
|
|
32
|
+
/** Start playback; no-op if there's nothing to advance through. */
|
|
33
|
+
play(): void;
|
|
34
|
+
/** Stop playback. */
|
|
35
|
+
pause(): void;
|
|
36
|
+
toggle(): void;
|
|
37
|
+
private tick;
|
|
38
|
+
private setIndex;
|
|
39
|
+
/** Advance one step without starting playback. */
|
|
40
|
+
next(): void;
|
|
41
|
+
/** Go back one step without starting playback. */
|
|
42
|
+
previous(): void;
|
|
43
|
+
/** Jump to an explicit index, pausing playback. */
|
|
44
|
+
goTo(index: number): void;
|
|
45
|
+
render(): TemplateResult;
|
|
46
|
+
}
|
|
47
|
+
declare global {
|
|
48
|
+
interface HTMLElementTagNameMap {
|
|
49
|
+
'lyra-playback': LyraPlayback;
|
|
50
|
+
}
|
|
51
|
+
}
|