@aceshooting/lyra-ui 1.0.1 → 1.1.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 +8 -9
- package/custom-elements.json +12479 -6535
- package/dist/components/chart/chart.d.ts +46 -1
- package/dist/components/chart/chart.js +148 -10
- package/dist/components/chart/chart.stories.d.ts +32 -0
- package/dist/components/chart/chart.stories.js +141 -0
- package/dist/components/chart/chart.styles.js +16 -0
- package/dist/components/chart/histogram.d.ts +2 -0
- package/dist/components/chart/histogram.js +18 -2
- package/dist/components/chart/line-chart.stories.d.ts +17 -0
- package/dist/components/chart/line-chart.stories.js +75 -0
- package/dist/components/chart/lite-chart.d.ts +74 -0
- package/dist/components/chart/lite-chart.js +384 -0
- package/dist/components/chart/lite-chart.stories.d.ts +12 -0
- package/dist/components/chart/lite-chart.stories.js +98 -0
- package/dist/components/chart/lite-chart.styles.d.ts +1 -0
- package/dist/components/chart/lite-chart.styles.js +83 -0
- package/dist/components/combobox/combobox.d.ts +9 -0
- package/dist/components/combobox/combobox.js +75 -12
- package/dist/components/combobox/combobox.stories.d.ts +18 -0
- package/dist/components/combobox/combobox.stories.js +75 -0
- package/dist/components/date-picker/calendar-core.js +13 -2
- package/dist/components/date-picker/date-input.d.ts +9 -1
- package/dist/components/date-picker/date-input.js +55 -7
- package/dist/components/date-picker/date-input.stories.d.ts +5 -0
- package/dist/components/date-picker/date-input.stories.js +36 -0
- package/dist/components/date-picker/date-picker.d.ts +11 -3
- package/dist/components/date-picker/date-picker.js +87 -37
- package/dist/components/date-picker/date-picker.stories.d.ts +4 -0
- package/dist/components/date-picker/date-picker.stories.js +12 -0
- package/dist/components/date-picker/date-picker.styles.js +1 -1
- package/dist/components/empty/empty.d.ts +12 -1
- package/dist/components/empty/empty.js +20 -3
- package/dist/components/empty/empty.stories.d.ts +2 -0
- package/dist/components/empty/empty.stories.js +20 -0
- package/dist/components/empty/empty.styles.js +11 -0
- package/dist/components/export-button/csv.js +6 -2
- package/dist/components/export-button/export-button.d.ts +15 -1
- package/dist/components/export-button/export-button.js +89 -12
- package/dist/components/export-button/export-button.stories.d.ts +1 -0
- package/dist/components/export-button/export-button.stories.js +5 -0
- package/dist/components/export-button/export-button.styles.js +11 -3
- package/dist/components/file-input/accept.d.ts +8 -2
- package/dist/components/file-input/accept.js +9 -3
- package/dist/components/file-input/file-input.d.ts +5 -2
- package/dist/components/file-input/file-input.js +23 -8
- package/dist/components/file-input/file-input.stories.d.ts +2 -0
- package/dist/components/file-input/file-input.stories.js +9 -0
- package/dist/components/flag/flag.d.ts +48 -3
- package/dist/components/flag/flag.js +65 -29
- package/dist/components/flag/flag.styles.js +1 -1
- package/dist/components/flag/language-map.d.ts +11 -0
- package/dist/components/flag/language-map.js +12 -1
- package/dist/components/gauge/gauge.d.ts +2 -0
- package/dist/components/gauge/gauge.js +19 -5
- package/dist/components/gauge/gauge.stories.d.ts +2 -0
- package/dist/components/gauge/gauge.stories.js +6 -0
- package/dist/components/graph/graph-loader.d.ts +28 -0
- package/dist/components/graph/graph-loader.js +44 -0
- package/dist/components/graph/graph.d.ts +41 -15
- package/dist/components/graph/graph.js +146 -71
- package/dist/components/graph/graph.stories.d.ts +3 -0
- package/dist/components/graph/graph.stories.js +38 -0
- package/dist/components/graph/graph.styles.js +6 -1
- package/dist/components/heatmap/calendar-grid.d.ts +7 -0
- package/dist/components/heatmap/calendar-grid.js +27 -10
- package/dist/components/heatmap/heatmap.d.ts +137 -12
- package/dist/components/heatmap/heatmap.js +427 -27
- package/dist/components/heatmap/heatmap.stories.d.ts +17 -0
- package/dist/components/heatmap/heatmap.stories.js +130 -0
- package/dist/components/heatmap/heatmap.styles.js +60 -2
- package/dist/components/map/map.d.ts +17 -1
- package/dist/components/map/map.js +68 -21
- package/dist/components/map/map.stories.d.ts +10 -0
- package/dist/components/map/map.stories.js +107 -12
- package/dist/components/map/map.styles.js +2 -2
- package/dist/components/playback/playback.d.ts +6 -2
- package/dist/components/playback/playback.js +36 -12
- package/dist/components/playback/playback.stories.d.ts +2 -0
- package/dist/components/playback/playback.stories.js +6 -0
- package/dist/components/playback/playback.styles.js +10 -2
- package/dist/components/select/select.d.ts +122 -0
- package/dist/components/select/select.js +516 -0
- package/dist/components/select/select.stories.d.ts +19 -0
- package/dist/components/select/select.stories.js +75 -0
- package/dist/components/select/select.styles.d.ts +1 -0
- package/dist/components/select/select.styles.js +188 -0
- package/dist/components/skeleton/skeleton.js +4 -7
- package/dist/components/sparkline/sparkline.d.ts +1 -1
- package/dist/components/sparkline/sparkline.js +40 -13
- package/dist/components/sparkline/sparkline.stories.d.ts +4 -0
- package/dist/components/sparkline/sparkline.stories.js +22 -0
- package/dist/components/split/split.d.ts +24 -3
- package/dist/components/split/split.js +155 -31
- package/dist/components/split/split.stories.d.ts +2 -0
- package/dist/components/split/split.stories.js +20 -0
- package/dist/components/stat/stat.d.ts +27 -1
- package/dist/components/stat/stat.js +56 -3
- package/dist/components/stat/stat.stories.d.ts +3 -0
- package/dist/components/stat/stat.stories.js +49 -1
- package/dist/components/stat/stat.styles.js +44 -0
- package/dist/components/table/table.d.ts +64 -2
- package/dist/components/table/table.js +213 -17
- package/dist/components/table/table.stories.d.ts +6 -0
- package/dist/components/table/table.stories.js +45 -0
- package/dist/components/table/table.styles.js +51 -4
- package/dist/components/time-range/time-range.d.ts +34 -5
- package/dist/components/time-range/time-range.js +217 -36
- package/dist/components/time-range/time-range.stories.d.ts +2 -0
- package/dist/components/time-range/time-range.stories.js +13 -0
- package/dist/components/time-range/time-range.styles.js +44 -2
- package/dist/components/toast/toast-item.d.ts +14 -2
- package/dist/components/toast/toast-item.js +134 -17
- package/dist/components/toast/toast-item.styles.js +28 -2
- package/dist/components/toast/toast.stories.d.ts +3 -0
- package/dist/components/toast/toast.stories.js +41 -1
- package/dist/components/toast/toast.styles.js +11 -11
- package/dist/components/toast/toaster.d.ts +1 -1
- package/dist/components/toast/toaster.js +13 -6
- package/dist/components/tree/tree-node.d.ts +13 -7
- package/dist/components/tree/tree-node.js +24 -10
- package/dist/components/tree/tree.d.ts +64 -15
- package/dist/components/tree/tree.js +113 -22
- package/dist/components/tree/tree.stories.d.ts +4 -0
- package/dist/components/tree/tree.stories.js +31 -1
- package/dist/components/tree/update-cascade.d.ts +13 -0
- package/dist/components/tree/update-cascade.js +13 -0
- package/dist/components/widget/widget.d.ts +15 -1
- package/dist/components/widget/widget.js +140 -5
- package/dist/components/widget/widget.stories.d.ts +1 -0
- package/dist/components/widget/widget.stories.js +10 -0
- package/dist/components/widget/widget.styles.js +11 -1
- package/dist/components/word-cloud/word-cloud-layout.d.ts +63 -0
- package/dist/components/word-cloud/word-cloud-layout.js +145 -0
- package/dist/components/word-cloud/word-cloud.d.ts +65 -0
- package/dist/components/word-cloud/word-cloud.js +255 -0
- package/dist/components/word-cloud/word-cloud.stories.d.ts +10 -0
- package/dist/components/word-cloud/word-cloud.stories.js +55 -0
- package/dist/components/word-cloud/word-cloud.styles.d.ts +1 -0
- package/dist/components/word-cloud/word-cloud.styles.js +54 -0
- package/dist/internal/a11y.js +2 -1
- package/dist/internal/rtl.d.ts +7 -0
- package/dist/internal/rtl.js +9 -0
- package/dist/lyra.d.ts +9 -1
- package/dist/lyra.js +6 -0
- package/llms-full.txt +892 -276
- package/llms.txt +12 -9
- package/package.json +6 -4
|
@@ -22,3 +22,133 @@ export const Default = {
|
|
|
22
22
|
></lyra-heatmap>
|
|
23
23
|
`,
|
|
24
24
|
};
|
|
25
|
+
export const SqrtScaleFitToWidth = {
|
|
26
|
+
render: () => html `
|
|
27
|
+
<lyra-heatmap
|
|
28
|
+
scale="sqrt"
|
|
29
|
+
fit-to-width
|
|
30
|
+
value-label="requests"
|
|
31
|
+
.rowLabels=${['Mon', 'Tue', 'Wed']}
|
|
32
|
+
.colLabels=${['0h', '6h', '12h', '18h']}
|
|
33
|
+
.values=${[
|
|
34
|
+
[1, 8, 400, 20],
|
|
35
|
+
[0, 2, 6, 3],
|
|
36
|
+
[-1, 1, 4, 7],
|
|
37
|
+
]}
|
|
38
|
+
></lyra-heatmap>
|
|
39
|
+
`,
|
|
40
|
+
};
|
|
41
|
+
export const CalendarMode = {
|
|
42
|
+
render: () => {
|
|
43
|
+
const days = [];
|
|
44
|
+
const start = Date.UTC(2026, 0, 1);
|
|
45
|
+
for (let i = 0; i < 120; i++) {
|
|
46
|
+
const date = new Date(start + i * 86_400_000).toISOString().slice(0, 10);
|
|
47
|
+
const value = Math.round(Math.abs(Math.sin(i / 6)) * 10);
|
|
48
|
+
days.push({ date, value });
|
|
49
|
+
}
|
|
50
|
+
return html `
|
|
51
|
+
<lyra-heatmap mode="calendar" value-label="commits" bucket-count="5" .days=${days}></lyra-heatmap>
|
|
52
|
+
`;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
export const CustomTheme = {
|
|
56
|
+
render: () => html `
|
|
57
|
+
<lyra-heatmap
|
|
58
|
+
style="
|
|
59
|
+
--lyra-heatmap-scale-lo: #fde7e9;
|
|
60
|
+
--lyra-heatmap-scale-hi: #b3122f;
|
|
61
|
+
--lyra-heatmap-no-data-fill: rgba(0, 0, 0, 0.08);
|
|
62
|
+
--lyra-heatmap-label-font: 11px monospace;
|
|
63
|
+
"
|
|
64
|
+
cell-size="24"
|
|
65
|
+
value-label="events"
|
|
66
|
+
.rowLabels=${['Mon', 'Tue', 'Wed']}
|
|
67
|
+
.colLabels=${['0h', '6h', '12h']}
|
|
68
|
+
.values=${[
|
|
69
|
+
[1, 4, 9],
|
|
70
|
+
[0, 2, 6],
|
|
71
|
+
[-1, 1, 4],
|
|
72
|
+
]}
|
|
73
|
+
></lyra-heatmap>
|
|
74
|
+
`,
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Hover a cell for its tooltip, tab to the grid and use the arrow keys to
|
|
78
|
+
* move the focus ring (a visually-hidden live region announces each move),
|
|
79
|
+
* and click or press Enter/Space on a cell to see its `lyra-cell-click`
|
|
80
|
+
* detail logged below.
|
|
81
|
+
*/
|
|
82
|
+
export const HoverFocusClick = {
|
|
83
|
+
render: () => {
|
|
84
|
+
const onCellClick = (e) => {
|
|
85
|
+
const detail = e.detail;
|
|
86
|
+
const out = document.getElementById('cell-click-out');
|
|
87
|
+
if (out)
|
|
88
|
+
out.textContent = JSON.stringify(detail);
|
|
89
|
+
};
|
|
90
|
+
return html `
|
|
91
|
+
<lyra-heatmap
|
|
92
|
+
cell-size="28"
|
|
93
|
+
value-label="events"
|
|
94
|
+
.rowLabels=${['Mon', 'Tue', 'Wed', 'Thu', 'Fri']}
|
|
95
|
+
.colLabels=${['0h', '6h', '12h', '18h']}
|
|
96
|
+
.values=${[
|
|
97
|
+
[1, 4, 9, 2],
|
|
98
|
+
[0, 2, 6, 3],
|
|
99
|
+
[5, 8, 3, 1],
|
|
100
|
+
[-1, 1, 4, 7],
|
|
101
|
+
[2, 3, 5, 6],
|
|
102
|
+
]}
|
|
103
|
+
@lyra-cell-click=${onCellClick}
|
|
104
|
+
></lyra-heatmap>
|
|
105
|
+
<p>Last <code>lyra-cell-click</code> detail: <code id="cell-click-out">(none yet)</code></p>
|
|
106
|
+
`;
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Cells flagged via `annotations` get a stroked ring; ones with a `label`
|
|
111
|
+
* also surface a swatch + text entry in the legend.
|
|
112
|
+
*/
|
|
113
|
+
export const Annotations = {
|
|
114
|
+
render: () => html `
|
|
115
|
+
<lyra-heatmap
|
|
116
|
+
cell-size="24"
|
|
117
|
+
value-label="events"
|
|
118
|
+
.rowLabels=${['Mon', 'Tue', 'Wed', 'Thu', 'Fri']}
|
|
119
|
+
.colLabels=${['0h', '6h', '12h', '18h']}
|
|
120
|
+
.values=${[
|
|
121
|
+
[1, 4, 9, 2],
|
|
122
|
+
[0, 2, 6, 3],
|
|
123
|
+
[5, 8, 3, 1],
|
|
124
|
+
[-1, 1, 4, 7],
|
|
125
|
+
[2, 3, 5, 6],
|
|
126
|
+
]}
|
|
127
|
+
.annotations=${[
|
|
128
|
+
{ row: 0, col: 2, label: 'Peak load' },
|
|
129
|
+
{ row: 2, col: 1, label: 'Incident' },
|
|
130
|
+
]}
|
|
131
|
+
></lyra-heatmap>
|
|
132
|
+
`,
|
|
133
|
+
};
|
|
134
|
+
/** `annotations` in calendar mode match by ISO `date` instead of `row`/`col`. */
|
|
135
|
+
export const CalendarAnnotations = {
|
|
136
|
+
render: () => {
|
|
137
|
+
const days = [];
|
|
138
|
+
const start = Date.UTC(2026, 0, 1);
|
|
139
|
+
for (let i = 0; i < 120; i++) {
|
|
140
|
+
const date = new Date(start + i * 86_400_000).toISOString().slice(0, 10);
|
|
141
|
+
const value = Math.round(Math.abs(Math.sin(i / 6)) * 10);
|
|
142
|
+
days.push({ date, value });
|
|
143
|
+
}
|
|
144
|
+
return html `
|
|
145
|
+
<lyra-heatmap
|
|
146
|
+
mode="calendar"
|
|
147
|
+
value-label="commits"
|
|
148
|
+
bucket-count="5"
|
|
149
|
+
.days=${days}
|
|
150
|
+
.annotations=${[{ date: '2026-02-14', label: 'Release day' }]}
|
|
151
|
+
></lyra-heatmap>
|
|
152
|
+
`;
|
|
153
|
+
},
|
|
154
|
+
};
|
|
@@ -4,8 +4,8 @@ export const styles = css `
|
|
|
4
4
|
display: block;
|
|
5
5
|
/* Sequential ramp endpoints for the data-driven cell colors (canvas can't
|
|
6
6
|
consume var() directly, so heatmap.ts resolves these via
|
|
7
|
-
getComputedStyle and interpolates between them).
|
|
8
|
-
business color
|
|
7
|
+
getComputedStyle and interpolates between them). Component-specific
|
|
8
|
+
business color — exposed so hosts can retheme
|
|
9
9
|
the ramp without raw hex leaking into the public API. */
|
|
10
10
|
--lyra-heatmap-scale-lo: #cde2fb;
|
|
11
11
|
--lyra-heatmap-scale-hi: #0969da;
|
|
@@ -13,8 +13,34 @@ export const styles = css `
|
|
|
13
13
|
getComputedStyle pattern as the ramp endpoints above, so hosts can
|
|
14
14
|
retheme it instead of it being a hardcoded literal in heatmap.ts. */
|
|
15
15
|
--lyra-heatmap-no-data-fill: rgba(128, 128, 128, 0.25);
|
|
16
|
+
/* Canvas-drawn axis/month/weekday label font — same resolve-via-
|
|
17
|
+
getComputedStyle pattern as the ramp endpoints above (canvas can't
|
|
18
|
+
consume var() directly). */
|
|
19
|
+
--lyra-heatmap-label-font: 10px sans-serif;
|
|
20
|
+
/* [part="tooltip"] is a real DOM element (not canvas-drawn), so unlike
|
|
21
|
+
the tokens above it consumes these var()s directly — no getComputedStyle
|
|
22
|
+
resolution needed. Own tokens (not the bare --lyra-color-surface/-text)
|
|
23
|
+
so a host can retheme just the heatmap tooltip, same rationale as
|
|
24
|
+
chart.ts's --lyra-chart-tooltip-bg/-text. */
|
|
25
|
+
--lyra-heatmap-tooltip-bg: var(--lyra-color-surface);
|
|
26
|
+
--lyra-heatmap-tooltip-text: var(--lyra-color-text);
|
|
27
|
+
/* Canvas-drawn focus-ring stroke around the keyboard-focused cell — same
|
|
28
|
+
resolve-via-getComputedStyle pattern as the ramp endpoints (canvas
|
|
29
|
+
can't consume var() directly). A dedicated token (rather than reusing
|
|
30
|
+
--lyra-focus-ring-color straight from tokens.styles.ts) so a host can
|
|
31
|
+
retheme the in-canvas ring independently of every other :focus-visible
|
|
32
|
+
outline in the library, while defaulting to that same brand color —
|
|
33
|
+
also reused by the [part="canvas"]:focus-visible outline below so the
|
|
34
|
+
two stay visually in sync. */
|
|
35
|
+
--lyra-heatmap-focus-ring-color: var(--lyra-focus-ring-color);
|
|
36
|
+
/* Canvas-drawn ring around annotated cells — same resolve-via-
|
|
37
|
+
getComputedStyle pattern. Defaults to --lyra-color-danger (a loud,
|
|
38
|
+
attention-grabbing color distinct from the sequential data ramp) so an
|
|
39
|
+
annotation reads clearly against any point on that ramp. */
|
|
40
|
+
--lyra-heatmap-annotation-color: var(--lyra-color-danger);
|
|
16
41
|
}
|
|
17
42
|
[part='base'] {
|
|
43
|
+
position: relative;
|
|
18
44
|
display: flex;
|
|
19
45
|
flex-direction: column;
|
|
20
46
|
gap: var(--lyra-space-xs);
|
|
@@ -23,6 +49,27 @@ export const styles = css `
|
|
|
23
49
|
display: block;
|
|
24
50
|
inline-size: 100%;
|
|
25
51
|
}
|
|
52
|
+
[part='canvas']:focus-visible {
|
|
53
|
+
outline: var(--lyra-focus-ring-width) solid var(--lyra-heatmap-focus-ring-color);
|
|
54
|
+
outline-offset: var(--lyra-focus-ring-offset);
|
|
55
|
+
}
|
|
56
|
+
[part='tooltip'] {
|
|
57
|
+
position: absolute;
|
|
58
|
+
transform: translate(-50%, -100%);
|
|
59
|
+
margin-block-start: -6px;
|
|
60
|
+
padding: 2px 6px;
|
|
61
|
+
border-radius: var(--lyra-radius);
|
|
62
|
+
background: var(--lyra-heatmap-tooltip-bg);
|
|
63
|
+
color: var(--lyra-heatmap-tooltip-text);
|
|
64
|
+
font-size: 0.75rem;
|
|
65
|
+
white-space: nowrap;
|
|
66
|
+
box-shadow: var(--lyra-shadow);
|
|
67
|
+
pointer-events: none;
|
|
68
|
+
z-index: 1;
|
|
69
|
+
}
|
|
70
|
+
[part='tooltip'][hidden] {
|
|
71
|
+
display: none;
|
|
72
|
+
}
|
|
26
73
|
[part='legend'] {
|
|
27
74
|
display: flex;
|
|
28
75
|
align-items: center;
|
|
@@ -40,4 +87,15 @@ export const styles = css `
|
|
|
40
87
|
var(--lyra-heatmap-scale-hi)
|
|
41
88
|
);
|
|
42
89
|
}
|
|
90
|
+
[part='legend-annotation'] {
|
|
91
|
+
display: inline-flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
gap: 3px;
|
|
94
|
+
}
|
|
95
|
+
[part='legend-annotation'] .ring-swatch {
|
|
96
|
+
inline-size: 0.6rem;
|
|
97
|
+
block-size: 0.6rem;
|
|
98
|
+
border-radius: 50%;
|
|
99
|
+
border: 2px solid var(--lyra-heatmap-annotation-color);
|
|
100
|
+
}
|
|
43
101
|
`;
|
|
@@ -6,10 +6,20 @@ export interface LegendEntry {
|
|
|
6
6
|
color: string;
|
|
7
7
|
label: string;
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Declarative GeoJSON fill layer, colored by interpolating `field`'s value
|
|
11
|
+
* across `stops`.
|
|
12
|
+
*/
|
|
9
13
|
export interface ChoroplethLayer {
|
|
10
14
|
sourceId: string;
|
|
11
15
|
geojson: GeoJSON.FeatureCollection;
|
|
12
16
|
field: string;
|
|
17
|
+
/**
|
|
18
|
+
* `[value, color]` pairs, ascending by `value`, fed to a maplibre-gl
|
|
19
|
+
* `interpolate` expression. Must contain at least one pair -- an empty
|
|
20
|
+
* array can't build a valid expression, so it's ignored (the existing fill
|
|
21
|
+
* layer, if any, is left as-is) rather than applied.
|
|
22
|
+
*/
|
|
13
23
|
stops: [number, string][];
|
|
14
24
|
}
|
|
15
25
|
export interface MapMarker {
|
|
@@ -17,6 +27,12 @@ export interface MapMarker {
|
|
|
17
27
|
lngLat: [number, number];
|
|
18
28
|
color?: string;
|
|
19
29
|
label?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Rendered as the marker's popup content via maplibre-gl's
|
|
32
|
+
* `Popup.setHTML()` -- parsed as raw markup, inline event handlers
|
|
33
|
+
* included. Only ever pass trusted content; sanitize anything derived
|
|
34
|
+
* from user input before assigning it here.
|
|
35
|
+
*/
|
|
20
36
|
html?: string;
|
|
21
37
|
}
|
|
22
38
|
/**
|
|
@@ -49,7 +65,7 @@ export declare class LyraMap extends LyraElement {
|
|
|
49
65
|
private _styleLoaded;
|
|
50
66
|
private _appliedChoroplethSourceId?;
|
|
51
67
|
private _appliedFillLayerId?;
|
|
52
|
-
private _maplibreModule
|
|
68
|
+
private _maplibreModule?;
|
|
53
69
|
private _markerInstances;
|
|
54
70
|
private _connectGeneration;
|
|
55
71
|
/** The raw `maplibregl.Map` instance — escape hatch for anything this wrapper doesn't expose. */
|
|
@@ -38,17 +38,27 @@ const DEFAULT_STYLE = {
|
|
|
38
38
|
layers: [{ id: 'lyra-osm', type: 'raster', source: 'lyra-osm' }],
|
|
39
39
|
};
|
|
40
40
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
|
|
41
|
+
* Matches only actual CSS color syntax: hex (`#fff`/`#ffffff`/`#ffffffff`),
|
|
42
|
+
* bare keywords (named colors, `transparent`, `currentColor`), the standard
|
|
43
|
+
* color functions, and `var(--custom-property)` references. Anything else --
|
|
44
|
+
* notably `url(...)`, which is otherwise valid `background` syntax -- is
|
|
45
|
+
* rejected.
|
|
46
|
+
*/
|
|
47
|
+
const SAFE_SWATCH_COLOR = /^(?:#[0-9a-fA-F]{3,8}|[a-zA-Z]+|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch)\([-+0-9.%,/\s]+\)|var\(--[\w-]+\))$/;
|
|
48
|
+
/**
|
|
49
|
+
* Rejects a `LegendEntry.color` that isn't recognizable CSS color syntax --
|
|
50
|
+
* both to stop it breaking out of the single `background` declaration it's
|
|
51
|
+
* assigned to (e.g. `;`, `{`, `}`, which terminate/reopen a declaration) and
|
|
52
|
+
* to stop non-color values such as `url(...)` from being accepted, which
|
|
53
|
+
* `background` also parses and would fetch as soon as the swatch renders.
|
|
54
|
+
* This matters even though the swatch's color is set via Lit's `styleMap`
|
|
55
|
+
* directive (not raw string interpolation): `styleMap`'s first commit for a
|
|
56
|
+
* given attribute part serializes the whole `style` value as a single string
|
|
47
57
|
* (only later updates go through the safe `CSSStyleDeclaration.setProperty()`
|
|
48
58
|
* path), so an unsanitized value could still inject on that first render.
|
|
49
59
|
*/
|
|
50
60
|
function sanitizeSwatchColor(color) {
|
|
51
|
-
return
|
|
61
|
+
return SAFE_SWATCH_COLOR.test(color.trim()) ? color : undefined;
|
|
52
62
|
}
|
|
53
63
|
/**
|
|
54
64
|
* `<lyra-map>` — a maplibre-gl wrapper with a declarative legend and
|
|
@@ -207,19 +217,34 @@ export class LyraMap extends LyraElement {
|
|
|
207
217
|
if (this._appliedChoroplethSourceId && this._appliedChoroplethSourceId !== sourceId) {
|
|
208
218
|
this.removeChoropleth();
|
|
209
219
|
}
|
|
210
|
-
const colorExpr = ['interpolate', ['linear'], ['get', field]];
|
|
211
|
-
for (const [value, color] of stops)
|
|
212
|
-
colorExpr.push(value, color);
|
|
213
220
|
const existingSource = this._map.getSource(sourceId);
|
|
214
221
|
if (existingSource) {
|
|
215
|
-
// Re-apply
|
|
216
|
-
// changed even though `sourceId` didn't
|
|
217
|
-
// the same GeoJSON), so the paint property must be refreshed too.
|
|
222
|
+
// Re-apply the data even if the color expression below ends up skipped:
|
|
223
|
+
// `geojson` may have changed even though `sourceId`/`stops` didn't.
|
|
218
224
|
existingSource.setData(geojson);
|
|
219
|
-
this._map.setPaintProperty(fillLayerId, 'fill-color', colorExpr);
|
|
220
225
|
}
|
|
221
226
|
else {
|
|
222
227
|
this._map.addSource(sourceId, { type: 'geojson', data: geojson, promoteId: 'id' });
|
|
228
|
+
}
|
|
229
|
+
this._appliedChoroplethSourceId = sourceId;
|
|
230
|
+
// An `interpolate` expression needs at least one [value, color] stop pair
|
|
231
|
+
// -- maplibre-gl's own expression parser requires it and otherwise fires a
|
|
232
|
+
// silently-ignored ErrorEvent instead of throwing (`addLayer`/
|
|
233
|
+
// `setPaintProperty` just no-op), so an empty `stops` would otherwise
|
|
234
|
+
// "succeed" here without ever creating/updating the fill layer, and every
|
|
235
|
+
// later update (valid `stops`, same `sourceId`) would find `existingSource`
|
|
236
|
+
// and wrongly assume `setPaintProperty` has a real layer to target. Bail
|
|
237
|
+
// before touching the layer at all and leave whatever fill layer already
|
|
238
|
+
// exists (if any) untouched until `stops` is non-empty again.
|
|
239
|
+
if (stops.length === 0)
|
|
240
|
+
return;
|
|
241
|
+
const colorExpr = ['interpolate', ['linear'], ['get', field]];
|
|
242
|
+
for (const [value, color] of stops)
|
|
243
|
+
colorExpr.push(value, color);
|
|
244
|
+
if (this._map.getLayer(fillLayerId)) {
|
|
245
|
+
this._map.setPaintProperty(fillLayerId, 'fill-color', colorExpr);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
223
248
|
this._map.addLayer({
|
|
224
249
|
id: fillLayerId,
|
|
225
250
|
type: 'fill',
|
|
@@ -227,7 +252,6 @@ export class LyraMap extends LyraElement {
|
|
|
227
252
|
paint: { 'fill-color': colorExpr, 'fill-opacity': 0.75 },
|
|
228
253
|
});
|
|
229
254
|
}
|
|
230
|
-
this._appliedChoroplethSourceId = sourceId;
|
|
231
255
|
this._appliedFillLayerId = fillLayerId;
|
|
232
256
|
}
|
|
233
257
|
/** Removes whatever choropleth layer/source is currently applied, if any. */
|
|
@@ -243,11 +267,18 @@ export class LyraMap extends LyraElement {
|
|
|
243
267
|
this._appliedChoroplethSourceId = undefined;
|
|
244
268
|
this._appliedFillLayerId = undefined;
|
|
245
269
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
270
|
+
// Deliberately synchronous (no `await loadMaplibre()`): `_maplibreModule` is
|
|
271
|
+
// already cached by the time `_map` exists (both are set in the same
|
|
272
|
+
// connectedCallback closure, see above), and every caller here is
|
|
273
|
+
// fire-and-forget. Re-awaiting the loader would open a window between the
|
|
274
|
+
// `_map` check and the marker mutations below where a disconnect (which
|
|
275
|
+
// synchronously clears `_map`) could resume into a torn-down map -- staying
|
|
276
|
+
// synchronous closes that window by construction instead of re-checking
|
|
277
|
+
// after the fact.
|
|
278
|
+
applyMarkers() {
|
|
279
|
+
const map = this._map;
|
|
280
|
+
const mod = this._maplibreModule;
|
|
281
|
+
if (!map || !mod)
|
|
251
282
|
return;
|
|
252
283
|
const visible = new Set();
|
|
253
284
|
for (const m of this.markers) {
|
|
@@ -264,11 +295,27 @@ export class LyraMap extends LyraElement {
|
|
|
264
295
|
popup.setText(m.label);
|
|
265
296
|
marker.setPopup(popup);
|
|
266
297
|
}
|
|
267
|
-
marker.addTo(
|
|
298
|
+
marker.addTo(map);
|
|
268
299
|
this._markerInstances.set(key, marker);
|
|
269
300
|
}
|
|
270
301
|
else {
|
|
271
302
|
existing.setLngLat(m.lngLat);
|
|
303
|
+
const popup = existing.getPopup();
|
|
304
|
+
if (m.html) {
|
|
305
|
+
if (popup)
|
|
306
|
+
popup.setHTML(m.html);
|
|
307
|
+
else
|
|
308
|
+
existing.setPopup(new mod.Popup({ offset: 12 }).setHTML(m.html));
|
|
309
|
+
}
|
|
310
|
+
else if (m.label) {
|
|
311
|
+
if (popup)
|
|
312
|
+
popup.setText(m.label);
|
|
313
|
+
else
|
|
314
|
+
existing.setPopup(new mod.Popup({ offset: 12 }).setText(m.label));
|
|
315
|
+
}
|
|
316
|
+
else if (popup) {
|
|
317
|
+
existing.setPopup(undefined);
|
|
318
|
+
}
|
|
272
319
|
}
|
|
273
320
|
}
|
|
274
321
|
for (const [key, marker] of this._markerInstances) {
|
|
@@ -3,3 +3,13 @@ declare const meta: Meta;
|
|
|
3
3
|
export default meta;
|
|
4
4
|
type Story = StoryObj;
|
|
5
5
|
export declare const Default: Story;
|
|
6
|
+
/**
|
|
7
|
+
* `choropleth` adds a GeoJSON fill layer, colored by interpolating `field`'s
|
|
8
|
+
* value across `stops` -- pair it with `legend` to label the color ramp.
|
|
9
|
+
*/
|
|
10
|
+
export declare const Choropleth: Story;
|
|
11
|
+
/**
|
|
12
|
+
* `markers` renders a pin per entry, each with an optional colored tint and
|
|
13
|
+
* an openable popup built from `label` (plain text) or `html` (raw markup).
|
|
14
|
+
*/
|
|
15
|
+
export declare const Markers: Story;
|
|
@@ -3,6 +3,18 @@ const legend = [
|
|
|
3
3
|
{ color: '#5b8def', label: 'Low' },
|
|
4
4
|
{ color: '#e5484d', label: 'High' },
|
|
5
5
|
];
|
|
6
|
+
const RASTER_STYLE = {
|
|
7
|
+
version: 8,
|
|
8
|
+
sources: {
|
|
9
|
+
demo: {
|
|
10
|
+
type: 'raster',
|
|
11
|
+
tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
|
|
12
|
+
tileSize: 256,
|
|
13
|
+
attribution: '© OpenStreetMap contributors',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
layers: [{ id: 'demo', type: 'raster', source: 'demo' }],
|
|
17
|
+
};
|
|
6
18
|
const meta = {
|
|
7
19
|
title: 'Map',
|
|
8
20
|
component: 'lyra-map',
|
|
@@ -23,18 +35,101 @@ export const Default = {
|
|
|
23
35
|
center="[2.3522, 48.8566]"
|
|
24
36
|
zoom="4"
|
|
25
37
|
.legend=${legend}
|
|
26
|
-
.mapStyle=${
|
|
27
|
-
version: 8,
|
|
28
|
-
sources: {
|
|
29
|
-
demo: {
|
|
30
|
-
type: 'raster',
|
|
31
|
-
tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
|
|
32
|
-
tileSize: 256,
|
|
33
|
-
attribution: '© OpenStreetMap contributors',
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
layers: [{ id: 'demo', type: 'raster', source: 'demo' }],
|
|
37
|
-
}}
|
|
38
|
+
.mapStyle=${RASTER_STYLE}
|
|
38
39
|
></lyra-map>
|
|
39
40
|
`,
|
|
40
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* `choropleth` adds a GeoJSON fill layer, colored by interpolating `field`'s
|
|
44
|
+
* value across `stops` -- pair it with `legend` to label the color ramp.
|
|
45
|
+
*/
|
|
46
|
+
export const Choropleth = {
|
|
47
|
+
render: () => {
|
|
48
|
+
const choropleth = {
|
|
49
|
+
sourceId: 'regions',
|
|
50
|
+
field: 'value',
|
|
51
|
+
stops: [
|
|
52
|
+
[0, '#5b8def'],
|
|
53
|
+
[50, '#f2c94c'],
|
|
54
|
+
[100, '#e5484d'],
|
|
55
|
+
],
|
|
56
|
+
geojson: {
|
|
57
|
+
type: 'FeatureCollection',
|
|
58
|
+
features: [
|
|
59
|
+
{
|
|
60
|
+
type: 'Feature',
|
|
61
|
+
properties: { value: 20 },
|
|
62
|
+
geometry: {
|
|
63
|
+
type: 'Polygon',
|
|
64
|
+
coordinates: [
|
|
65
|
+
[
|
|
66
|
+
[2.25, 48.9],
|
|
67
|
+
[2.35, 48.9],
|
|
68
|
+
[2.35, 48.85],
|
|
69
|
+
[2.25, 48.85],
|
|
70
|
+
[2.25, 48.9],
|
|
71
|
+
],
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
type: 'Feature',
|
|
77
|
+
properties: { value: 80 },
|
|
78
|
+
geometry: {
|
|
79
|
+
type: 'Polygon',
|
|
80
|
+
coordinates: [
|
|
81
|
+
[
|
|
82
|
+
[2.35, 48.9],
|
|
83
|
+
[2.45, 48.9],
|
|
84
|
+
[2.45, 48.85],
|
|
85
|
+
[2.35, 48.85],
|
|
86
|
+
[2.35, 48.9],
|
|
87
|
+
],
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
return html `
|
|
95
|
+
<lyra-map
|
|
96
|
+
style="height: 20rem"
|
|
97
|
+
center="[2.3522, 48.8566]"
|
|
98
|
+
zoom="10"
|
|
99
|
+
.legend=${[
|
|
100
|
+
{ color: '#5b8def', label: 'Low' },
|
|
101
|
+
{ color: '#f2c94c', label: 'Medium' },
|
|
102
|
+
{ color: '#e5484d', label: 'High' },
|
|
103
|
+
]}
|
|
104
|
+
.choropleth=${choropleth}
|
|
105
|
+
.mapStyle=${RASTER_STYLE}
|
|
106
|
+
></lyra-map>
|
|
107
|
+
`;
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* `markers` renders a pin per entry, each with an optional colored tint and
|
|
112
|
+
* an openable popup built from `label` (plain text) or `html` (raw markup).
|
|
113
|
+
*/
|
|
114
|
+
export const Markers = {
|
|
115
|
+
render: () => {
|
|
116
|
+
const markers = [
|
|
117
|
+
{ id: 'eiffel', lngLat: [2.2945, 48.8584], label: 'Eiffel Tower' },
|
|
118
|
+
{
|
|
119
|
+
id: 'louvre',
|
|
120
|
+
lngLat: [2.3364, 48.8606],
|
|
121
|
+
color: '#e5484d',
|
|
122
|
+
html: '<strong>Louvre</strong><br>Museum',
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
return html `
|
|
126
|
+
<lyra-map
|
|
127
|
+
style="height: 20rem"
|
|
128
|
+
center="[2.3522, 48.8566]"
|
|
129
|
+
zoom="12"
|
|
130
|
+
.markers=${markers}
|
|
131
|
+
.mapStyle=${RASTER_STYLE}
|
|
132
|
+
></lyra-map>
|
|
133
|
+
`;
|
|
134
|
+
},
|
|
135
|
+
};
|
|
@@ -26,7 +26,7 @@ export const styles = css `
|
|
|
26
26
|
z-index: 1;
|
|
27
27
|
display: flex;
|
|
28
28
|
flex-direction: column;
|
|
29
|
-
gap:
|
|
29
|
+
gap: var(--lyra-space-xs);
|
|
30
30
|
padding: var(--lyra-space-xs) var(--lyra-space-s);
|
|
31
31
|
background: var(--lyra-color-surface);
|
|
32
32
|
border: 1px solid var(--lyra-color-border);
|
|
@@ -37,7 +37,7 @@ export const styles = css `
|
|
|
37
37
|
.legend-row {
|
|
38
38
|
display: flex;
|
|
39
39
|
align-items: center;
|
|
40
|
-
gap:
|
|
40
|
+
gap: var(--lyra-space-xs);
|
|
41
41
|
}
|
|
42
42
|
[part='legend-swatch'] {
|
|
43
43
|
inline-size: 0.75rem;
|
|
@@ -6,8 +6,8 @@ import { LyraElement } from '../../internal/lyra-element.js';
|
|
|
6
6
|
* play-timers in time-series dashboards.
|
|
7
7
|
*
|
|
8
8
|
* @customElement lyra-playback
|
|
9
|
-
* @event lyra-play
|
|
10
|
-
* @event lyra-pause
|
|
9
|
+
* @event lyra-play - Fired when playback starts.
|
|
10
|
+
* @event lyra-pause - Fired when playback stops (including auto-pause).
|
|
11
11
|
* @event lyra-step - `detail: { index }`, fired on every tick and manual step.
|
|
12
12
|
* @csspart base, play-button, slider
|
|
13
13
|
*/
|
|
@@ -27,13 +27,17 @@ export declare class LyraPlayback extends LyraElement {
|
|
|
27
27
|
*/
|
|
28
28
|
hidden: boolean;
|
|
29
29
|
private timer?;
|
|
30
|
+
/** Largest index reachable at the current `length` (never negative). */
|
|
31
|
+
private get maxIndex();
|
|
30
32
|
disconnectedCallback(): void;
|
|
31
33
|
protected willUpdate(changed: PropertyValues): void;
|
|
32
34
|
/** Start playback; no-op if there's nothing to advance through. */
|
|
33
35
|
play(): void;
|
|
34
36
|
/** Stop playback. */
|
|
35
37
|
pause(): void;
|
|
38
|
+
/** Toggle between playing and paused. */
|
|
36
39
|
toggle(): void;
|
|
40
|
+
private scheduleTick;
|
|
37
41
|
private tick;
|
|
38
42
|
private setIndex;
|
|
39
43
|
/** Advance one step without starting playback. */
|