@elabs-ai/components-maps 4.2.0 → 5.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 +1 -1
- package/dist/chunk-2FPB53NG.js +36 -0
- package/dist/chunk-2FPB53NG.js.map +1 -0
- package/dist/chunk-2VVJRT66.js +37 -0
- package/dist/chunk-2VVJRT66.js.map +1 -0
- package/dist/chunk-3HXFONRP.js +2 -0
- package/dist/chunk-3HXFONRP.js.map +1 -0
- package/dist/chunk-3SJN3IYT.js +2 -0
- package/dist/chunk-3SJN3IYT.js.map +1 -0
- package/dist/chunk-4DANBD5B.js +55 -0
- package/dist/chunk-4DANBD5B.js.map +1 -0
- package/dist/chunk-5PDVG53B.js +2 -0
- package/dist/chunk-5PDVG53B.js.map +1 -0
- package/dist/chunk-643R7IJC.js +2 -0
- package/dist/chunk-643R7IJC.js.map +1 -0
- package/dist/chunk-B3ZDBQ6I.js +164 -0
- package/dist/chunk-B3ZDBQ6I.js.map +1 -0
- package/dist/chunk-BGCE3RR5.js +35 -0
- package/dist/chunk-BGCE3RR5.js.map +1 -0
- package/dist/chunk-C4VI37TD.js +485 -0
- package/dist/chunk-C4VI37TD.js.map +1 -0
- package/dist/chunk-CJJT6AEI.js +179 -0
- package/dist/chunk-CJJT6AEI.js.map +1 -0
- package/dist/chunk-D2RCPTRO.js +2 -0
- package/dist/chunk-D2RCPTRO.js.map +1 -0
- package/dist/chunk-DTV7JZVN.js +2 -0
- package/dist/chunk-DTV7JZVN.js.map +1 -0
- package/dist/chunk-E65Z3MSV.js +80 -0
- package/dist/chunk-E65Z3MSV.js.map +1 -0
- package/dist/chunk-E7VPNINZ.js +70 -0
- package/dist/chunk-E7VPNINZ.js.map +1 -0
- package/dist/chunk-FHV5IA6N.js +100 -0
- package/dist/chunk-FHV5IA6N.js.map +1 -0
- package/dist/chunk-FQ6V7BFZ.js +347 -0
- package/dist/chunk-FQ6V7BFZ.js.map +1 -0
- package/dist/chunk-G7JDXDJN.js +19 -0
- package/dist/chunk-G7JDXDJN.js.map +1 -0
- package/dist/chunk-GWULZOMW.js +21 -0
- package/dist/chunk-GWULZOMW.js.map +1 -0
- package/dist/chunk-GYDTTAVD.js +120 -0
- package/dist/chunk-GYDTTAVD.js.map +1 -0
- package/dist/chunk-HCOBWIM4.js +48 -0
- package/dist/chunk-HCOBWIM4.js.map +1 -0
- package/dist/chunk-IAHK5MVT.js +2 -0
- package/dist/chunk-IAHK5MVT.js.map +1 -0
- package/dist/chunk-JGJ6442I.js +235 -0
- package/dist/chunk-JGJ6442I.js.map +1 -0
- package/dist/chunk-JYFERXZT.js +18 -0
- package/dist/chunk-JYFERXZT.js.map +1 -0
- package/dist/chunk-LU2UEKFM.js +53 -0
- package/dist/chunk-LU2UEKFM.js.map +1 -0
- package/dist/chunk-LVYM6C5C.js +2 -0
- package/dist/chunk-LVYM6C5C.js.map +1 -0
- package/dist/chunk-MIMDP3EU.js +104 -0
- package/dist/chunk-MIMDP3EU.js.map +1 -0
- package/dist/chunk-NNBURJHY.js +2 -0
- package/dist/chunk-NNBURJHY.js.map +1 -0
- package/dist/chunk-NPUC3V37.js +2 -0
- package/dist/chunk-NPUC3V37.js.map +1 -0
- package/dist/chunk-OEVVNPN3.js +201 -0
- package/dist/chunk-OEVVNPN3.js.map +1 -0
- package/dist/chunk-PF3R2BXM.js +200 -0
- package/dist/chunk-PF3R2BXM.js.map +1 -0
- package/dist/chunk-Q2I5YODW.js +2 -0
- package/dist/chunk-Q2I5YODW.js.map +1 -0
- package/dist/chunk-QDWWP3N7.js +247 -0
- package/dist/chunk-QDWWP3N7.js.map +1 -0
- package/dist/chunk-QU33KPMP.js +2 -0
- package/dist/chunk-QU33KPMP.js.map +1 -0
- package/dist/chunk-RK4BUMHS.js +2 -0
- package/dist/chunk-RK4BUMHS.js.map +1 -0
- package/dist/chunk-RNSLG64F.js +26 -0
- package/dist/chunk-RNSLG64F.js.map +1 -0
- package/dist/chunk-RV673TB4.js +68 -0
- package/dist/chunk-RV673TB4.js.map +1 -0
- package/dist/chunk-S7N2WMVT.js +180 -0
- package/dist/chunk-S7N2WMVT.js.map +1 -0
- package/dist/chunk-T3Z6TCDD.js +2 -0
- package/dist/chunk-T3Z6TCDD.js.map +1 -0
- package/dist/chunk-T4TJTVVA.js +105 -0
- package/dist/chunk-T4TJTVVA.js.map +1 -0
- package/dist/chunk-UMOYRCDX.js +103 -0
- package/dist/chunk-UMOYRCDX.js.map +1 -0
- package/dist/chunk-V2REM6UT.js +185 -0
- package/dist/chunk-V2REM6UT.js.map +1 -0
- package/dist/chunk-VFBRFGID.js +89 -0
- package/dist/chunk-VFBRFGID.js.map +1 -0
- package/dist/chunk-W76NLFER.js +103 -0
- package/dist/chunk-W76NLFER.js.map +1 -0
- package/dist/chunk-WNU4IOGL.js +116 -0
- package/dist/chunk-WNU4IOGL.js.map +1 -0
- package/dist/chunk-WS4Z7ENS.js +494 -0
- package/dist/chunk-WS4Z7ENS.js.map +1 -0
- package/dist/chunk-XQT7YLJY.js +497 -0
- package/dist/chunk-XQT7YLJY.js.map +1 -0
- package/dist/chunk-Y44MEKJV.js +21 -0
- package/dist/chunk-Y44MEKJV.js.map +1 -0
- package/dist/chunk-Z4EPSOB4.js +53 -0
- package/dist/chunk-Z4EPSOB4.js.map +1 -0
- package/dist/chunk-Z5OPDBDC.js +2 -0
- package/dist/chunk-Z5OPDBDC.js.map +1 -0
- package/dist/index.d.ts +1241 -46
- package/dist/index.js +189 -1471
- package/dist/index.js.map +1 -1
- package/dist/lib/arc-math.js +8 -0
- package/dist/lib/arc-math.js.map +1 -0
- package/dist/lib/map-position.js +11 -0
- package/dist/lib/map-position.js.map +1 -0
- package/dist/lib/merge-hover-paint.js +10 -0
- package/dist/lib/merge-hover-paint.js.map +1 -0
- package/dist/lib/plan-crs.js +14 -0
- package/dist/lib/plan-crs.js.map +1 -0
- package/dist/lib/plan-patterns.js +14 -0
- package/dist/lib/plan-patterns.js.map +1 -0
- package/dist/lib/plan-regions.js +18 -0
- package/dist/lib/plan-regions.js.map +1 -0
- package/dist/lib/plan-status.js +14 -0
- package/dist/lib/plan-status.js.map +1 -0
- package/dist/lib/route-arrow.js +12 -0
- package/dist/lib/route-arrow.js.map +1 -0
- package/dist/lib/use-map-breakpoint.js +33 -0
- package/dist/lib/use-map-breakpoint.js.map +1 -0
- package/dist/lib/use-plan-projection.js +9 -0
- package/dist/lib/use-plan-projection.js.map +1 -0
- package/dist/lib/use-token-color.js +10 -0
- package/dist/lib/use-token-color.js.map +1 -0
- package/dist/lib/warn-once.js +10 -0
- package/dist/lib/warn-once.js.map +1 -0
- package/dist/map-annotation/anchor.js +14 -0
- package/dist/map-annotation/anchor.js.map +1 -0
- package/dist/map-annotation/index.js +21 -0
- package/dist/map-annotation/index.js.map +1 -0
- package/dist/map-annotation/map-annotation.js +18 -0
- package/dist/map-annotation/map-annotation.js.map +1 -0
- package/dist/map-arc/index.js +16 -0
- package/dist/map-arc/index.js.map +1 -0
- package/dist/map-arc/map-arc.js +13 -0
- package/dist/map-arc/map-arc.js.map +1 -0
- package/dist/map-canvas/index.css +11 -0
- package/dist/map-canvas/index.css.map +1 -0
- package/dist/map-canvas/index.js +20 -0
- package/dist/map-canvas/index.js.map +1 -0
- package/dist/map-canvas/map-canvas.css +11 -0
- package/dist/map-canvas/map-canvas.css.map +1 -0
- package/dist/map-canvas/map-canvas.js +17 -0
- package/dist/map-canvas/map-canvas.js.map +1 -0
- package/dist/map-canvas/map-context.js +11 -0
- package/dist/map-canvas/map-context.js.map +1 -0
- package/dist/map-canvas/use-resolved-basemap-theme.js +9 -0
- package/dist/map-canvas/use-resolved-basemap-theme.js.map +1 -0
- package/dist/map-cluster-layer/index.js +12 -0
- package/dist/map-cluster-layer/index.js.map +1 -0
- package/dist/map-cluster-layer/map-cluster-layer.js +12 -0
- package/dist/map-cluster-layer/map-cluster-layer.js.map +1 -0
- package/dist/map-controls/index.js +11 -0
- package/dist/map-controls/index.js.map +1 -0
- package/dist/map-controls/map-controls.js +11 -0
- package/dist/map-controls/map-controls.js.map +1 -0
- package/dist/map-geojson/index.js +13 -0
- package/dist/map-geojson/index.js.map +1 -0
- package/dist/map-geojson/map-geojson.js +13 -0
- package/dist/map-geojson/map-geojson.js.map +1 -0
- package/dist/map-inset/index.css +11 -0
- package/dist/map-inset/index.css.map +1 -0
- package/dist/map-inset/index.js +20 -0
- package/dist/map-inset/index.js.map +1 -0
- package/dist/map-inset/map-inset.css +11 -0
- package/dist/map-inset/map-inset.css.map +1 -0
- package/dist/map-inset/map-inset.js +20 -0
- package/dist/map-inset/map-inset.js.map +1 -0
- package/dist/map-legend/index.js +14 -0
- package/dist/map-legend/index.js.map +1 -0
- package/dist/map-legend/map-corner.js +10 -0
- package/dist/map-legend/map-corner.js.map +1 -0
- package/dist/map-legend/map-legend.js +11 -0
- package/dist/map-legend/map-legend.js.map +1 -0
- package/dist/map-marker/index.js +22 -0
- package/dist/map-marker/index.js.map +1 -0
- package/dist/map-marker/map-marker.js +22 -0
- package/dist/map-marker/map-marker.js.map +1 -0
- package/dist/map-north-arrow/index.js +11 -0
- package/dist/map-north-arrow/index.js.map +1 -0
- package/dist/map-north-arrow/map-north-arrow.js +11 -0
- package/dist/map-north-arrow/map-north-arrow.js.map +1 -0
- package/dist/map-plan/index.js +30 -0
- package/dist/map-plan/index.js.map +1 -0
- package/dist/map-plan/map-plan-legend.js +10 -0
- package/dist/map-plan/map-plan-legend.js.map +1 -0
- package/dist/map-plan/map-plan-overlay.js +13 -0
- package/dist/map-plan/map-plan-overlay.js.map +1 -0
- package/dist/map-plan/map-plan-status.js +9 -0
- package/dist/map-plan/map-plan-status.js.map +1 -0
- package/dist/map-plan/map-plan-table.js +10 -0
- package/dist/map-plan/map-plan-table.js.map +1 -0
- package/dist/map-plan/use-plan-selection.js +9 -0
- package/dist/map-plan/use-plan-selection.js.map +1 -0
- package/dist/map-plan-image/index.js +11 -0
- package/dist/map-plan-image/index.js.map +1 -0
- package/dist/map-plan-image/map-plan-image.js +11 -0
- package/dist/map-plan-image/map-plan-image.js.map +1 -0
- package/dist/map-popup/index.js +12 -0
- package/dist/map-popup/index.js.map +1 -0
- package/dist/map-popup/map-popup.js +12 -0
- package/dist/map-popup/map-popup.js.map +1 -0
- package/dist/map-route/index.js +12 -0
- package/dist/map-route/index.js.map +1 -0
- package/dist/map-route/map-route.js +12 -0
- package/dist/map-route/map-route.js.map +1 -0
- package/dist/map-scale-bar/index.js +18 -0
- package/dist/map-scale-bar/index.js.map +1 -0
- package/dist/map-scale-bar/map-scale-bar.js +18 -0
- package/dist/map-scale-bar/map-scale-bar.js.map +1 -0
- package/package.json +5 -5
- package/src/index.ts +83 -0
- package/src/lib/arc-math.test.ts +19 -0
- package/src/lib/arc-math.ts +6 -1
- package/src/lib/map-position.ts +77 -0
- package/src/lib/merge-hover-paint.ts +44 -13
- package/src/lib/plan-crs.parity.test.ts +39 -0
- package/src/lib/plan-crs.test.ts +248 -0
- package/src/lib/plan-crs.ts +252 -0
- package/src/lib/plan-patterns.test.ts +255 -0
- package/src/lib/plan-patterns.ts +160 -0
- package/src/lib/plan-regions.test.ts +186 -0
- package/src/lib/plan-regions.ts +209 -0
- package/src/lib/plan-status.test.ts +87 -0
- package/src/lib/plan-status.ts +147 -0
- package/src/lib/route-arrow.test.ts +88 -0
- package/src/lib/route-arrow.ts +59 -0
- package/src/lib/use-map-breakpoint.test.ts +83 -0
- package/src/lib/use-map-breakpoint.ts +238 -0
- package/src/lib/use-plan-projection.ts +157 -0
- package/src/lib/warn-once.ts +21 -0
- package/src/map-annotation/anchor.ts +92 -0
- package/src/map-annotation/index.ts +15 -0
- package/src/map-annotation/map-annotation.stories.tsx +113 -0
- package/src/map-annotation/map-annotation.test.tsx +106 -0
- package/src/map-annotation/map-annotation.tsx +356 -0
- package/src/map-arc/map-arc.tsx +26 -5
- package/src/map-canvas/map-canvas.stories.tsx +211 -0
- package/src/map-canvas/map-canvas.test.tsx +389 -0
- package/src/map-canvas/map-canvas.tsx +457 -17
- package/src/map-canvas/map-context.ts +11 -0
- package/src/map-cluster-layer/map-cluster-layer.tsx +14 -1
- package/src/map-controls/map-controls.stories.tsx +16 -0
- package/src/map-controls/map-controls.test.tsx +145 -1
- package/src/map-controls/map-controls.tsx +57 -12
- package/src/map-geojson/map-geojson.stories.tsx +153 -3
- package/src/map-geojson/map-geojson.test.tsx +453 -0
- package/src/map-geojson/map-geojson.tsx +591 -51
- package/src/map-inset/index.ts +1 -0
- package/src/map-inset/map-inset.stories.tsx +61 -0
- package/src/map-inset/map-inset.test.tsx +117 -0
- package/src/map-inset/map-inset.tsx +264 -0
- package/src/map-legend/index.ts +10 -0
- package/src/map-legend/map-corner.ts +23 -0
- package/src/map-legend/map-legend.stories.tsx +171 -0
- package/src/map-legend/map-legend.test.tsx +136 -0
- package/src/map-legend/map-legend.tsx +264 -0
- package/src/map-marker/map-marker.stories.tsx +136 -4
- package/src/map-marker/map-marker.test.tsx +328 -3
- package/src/map-marker/map-marker.tsx +370 -33
- package/src/map-north-arrow/index.ts +1 -0
- package/src/map-north-arrow/map-north-arrow.stories.tsx +65 -0
- package/src/map-north-arrow/map-north-arrow.test.tsx +39 -0
- package/src/map-north-arrow/map-north-arrow.tsx +82 -0
- package/src/map-plan/index.ts +13 -0
- package/src/map-plan/map-plan-legend.test.tsx +61 -0
- package/src/map-plan/map-plan-legend.tsx +83 -0
- package/src/map-plan/map-plan-overlay.stories.tsx +260 -0
- package/src/map-plan/map-plan-overlay.test.tsx +302 -0
- package/src/map-plan/map-plan-overlay.tsx +531 -0
- package/src/map-plan/map-plan-status.test.tsx +71 -0
- package/src/map-plan/map-plan-status.tsx +77 -0
- package/src/map-plan/map-plan-table.test.tsx +86 -0
- package/src/map-plan/map-plan-table.tsx +153 -0
- package/src/map-plan/use-plan-selection.ts +78 -0
- package/src/map-plan-image/index.ts +1 -0
- package/src/map-plan-image/map-plan-image.stories.tsx +301 -0
- package/src/map-plan-image/map-plan-image.test.tsx +176 -0
- package/src/map-plan-image/map-plan-image.tsx +279 -0
- package/src/map-popup/map-popup.tsx +15 -10
- package/src/map-route/index.ts +1 -1
- package/src/map-route/map-route.test.tsx +216 -0
- package/src/map-route/map-route.tsx +105 -6
- package/src/map-scale-bar/index.ts +9 -0
- package/src/map-scale-bar/map-scale-bar.stories.tsx +63 -0
- package/src/map-scale-bar/map-scale-bar.test.tsx +65 -0
- package/src/map-scale-bar/map-scale-bar.tsx +145 -0
- package/src/test-utils/locator-fixture.ts +75 -0
- package/src/test-utils/maplibre-mock.ts +256 -19
|
@@ -41,3 +41,19 @@ export const TopLeft: Story = {
|
|
|
41
41
|
</div>
|
|
42
42
|
),
|
|
43
43
|
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A static editorial map (`interactive={false}`) shows no zoom chrome: the
|
|
47
|
+
* gestures those buttons invite are switched off, so the affordance would lie.
|
|
48
|
+
* The host can still ask for a group explicitly — the fullscreen toggle here
|
|
49
|
+
* works either way.
|
|
50
|
+
*/
|
|
51
|
+
export const StaticMap: Story = {
|
|
52
|
+
render: () => (
|
|
53
|
+
<div className="h-[480px]">
|
|
54
|
+
<MapCanvas center={[2.3522, 48.8566]} interactive={false} zoom={11}>
|
|
55
|
+
<MapControls showFullscreen />
|
|
56
|
+
</MapCanvas>
|
|
57
|
+
</div>
|
|
58
|
+
),
|
|
59
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { cleanup, render, screen } from "@testing-library/react";
|
|
1
|
+
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
|
2
2
|
import userEvent from "@testing-library/user-event";
|
|
3
3
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { LocaleProvider } from "@elabs-ai/components-ui";
|
|
4
5
|
|
|
5
6
|
vi.mock("maplibre-gl", async () => {
|
|
6
7
|
const { createMaplibreMock } = await import("../test-utils/maplibre-mock");
|
|
@@ -30,6 +31,40 @@ describe("MapControls", () => {
|
|
|
30
31
|
).not.toBeInTheDocument();
|
|
31
32
|
});
|
|
32
33
|
|
|
34
|
+
it("hides zoom chrome on a static map, and renders no control box at all", async () => {
|
|
35
|
+
const { container } = render(
|
|
36
|
+
<MapCanvas interactive={false}>
|
|
37
|
+
<MapControls />
|
|
38
|
+
</MapCanvas>,
|
|
39
|
+
);
|
|
40
|
+
await waitFor(() => {
|
|
41
|
+
expect(container.querySelector("[data-slot='map-canvas']")).not.toBeNull();
|
|
42
|
+
});
|
|
43
|
+
expect(screen.queryByRole("button", { name: "Zoom in" })).not.toBeInTheDocument();
|
|
44
|
+
expect(screen.queryByRole("button", { name: "Zoom out" })).not.toBeInTheDocument();
|
|
45
|
+
// No empty, absolutely-positioned control box left behind either.
|
|
46
|
+
expect(container.querySelector(".absolute.z-10")).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("keeps zoom chrome on a static map when the host asks for it", async () => {
|
|
50
|
+
render(
|
|
51
|
+
<MapCanvas interactive={false}>
|
|
52
|
+
<MapControls showZoom />
|
|
53
|
+
</MapCanvas>,
|
|
54
|
+
);
|
|
55
|
+
expect(await screen.findByRole("button", { name: "Zoom in" })).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("still renders an explicitly enabled group on a static map", async () => {
|
|
59
|
+
render(
|
|
60
|
+
<MapCanvas interactive={false}>
|
|
61
|
+
<MapControls showFullscreen />
|
|
62
|
+
</MapCanvas>,
|
|
63
|
+
);
|
|
64
|
+
expect(await screen.findByRole("button", { name: "Toggle fullscreen" })).toBeInTheDocument();
|
|
65
|
+
expect(screen.queryByRole("button", { name: "Zoom in" })).not.toBeInTheDocument();
|
|
66
|
+
});
|
|
67
|
+
|
|
33
68
|
it("renders compass, locate and fullscreen controls when enabled", async () => {
|
|
34
69
|
render(
|
|
35
70
|
<MapCanvas>
|
|
@@ -53,4 +88,113 @@ describe("MapControls", () => {
|
|
|
53
88
|
await user.click(await screen.findByRole("button", { name: "Zoom in" }));
|
|
54
89
|
expect(MockMap.instances[0]!.zoomToCalls.length).toBeGreaterThan(0);
|
|
55
90
|
});
|
|
91
|
+
|
|
92
|
+
it("takes every label from the locale seam, so an app can translate them", async () => {
|
|
93
|
+
render(
|
|
94
|
+
<LocaleProvider
|
|
95
|
+
locale="de-DE"
|
|
96
|
+
messages={{
|
|
97
|
+
"maps.controls.zoomIn": "Hineinzoomen",
|
|
98
|
+
"maps.controls.zoomOut": "Herauszoomen",
|
|
99
|
+
"maps.controls.resetBearing": "Nach Norden ausrichten",
|
|
100
|
+
"maps.controls.locate": "Meinen Standort finden",
|
|
101
|
+
"maps.controls.fullscreen": "Vollbild umschalten",
|
|
102
|
+
"maps.controls.fit": "Ganzen Plan zeigen",
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
<MapCanvas plan={{ width: 100, height: 50 }}>
|
|
106
|
+
<MapControls showCompass showLocate showFullscreen />
|
|
107
|
+
</MapCanvas>
|
|
108
|
+
</LocaleProvider>,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
for (const name of [
|
|
112
|
+
"Hineinzoomen",
|
|
113
|
+
"Herauszoomen",
|
|
114
|
+
"Nach Norden ausrichten",
|
|
115
|
+
"Meinen Standort finden",
|
|
116
|
+
"Ganzen Plan zeigen",
|
|
117
|
+
"Vollbild umschalten",
|
|
118
|
+
]) {
|
|
119
|
+
expect(await screen.findByRole("button", { name })).toBeInTheDocument();
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("MapControls fit button", () => {
|
|
125
|
+
it("stays off a geographic map and appears on a plan", async () => {
|
|
126
|
+
const { unmount } = render(
|
|
127
|
+
<MapCanvas>
|
|
128
|
+
<MapControls />
|
|
129
|
+
</MapCanvas>,
|
|
130
|
+
);
|
|
131
|
+
expect(await screen.findByRole("button", { name: "Zoom in" })).toBeInTheDocument();
|
|
132
|
+
expect(screen.queryByRole("button", { name: "Fit the whole plan" })).not.toBeInTheDocument();
|
|
133
|
+
unmount();
|
|
134
|
+
|
|
135
|
+
render(
|
|
136
|
+
<MapCanvas plan={{ width: 100, height: 50 }}>
|
|
137
|
+
<MapControls />
|
|
138
|
+
</MapCanvas>,
|
|
139
|
+
);
|
|
140
|
+
expect(await screen.findByRole("button", { name: "Fit the whole plan" })).toBeInTheDocument();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("frames the plan's own extent and reports back", async () => {
|
|
144
|
+
const user = userEvent.setup();
|
|
145
|
+
const onFit = vi.fn();
|
|
146
|
+
render(
|
|
147
|
+
<MapCanvas plan={{ width: 100, height: 50 }}>
|
|
148
|
+
<MapControls onFit={onFit} />
|
|
149
|
+
</MapCanvas>,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const map = MockMap.instances[0]!;
|
|
153
|
+
const before = map.fitBoundsCalls.length;
|
|
154
|
+
await user.click(await screen.findByRole("button", { name: "Fit the whole plan" }));
|
|
155
|
+
|
|
156
|
+
expect(map.fitBoundsCalls.length).toBe(before + 1);
|
|
157
|
+
expect(map.fitBoundsCalls.at(-1)!.options).toMatchObject({ padding: 24 });
|
|
158
|
+
expect(onFit).toHaveBeenCalledTimes(1);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("frames caller bounds when they are given", async () => {
|
|
162
|
+
const user = userEvent.setup();
|
|
163
|
+
const bounds: [[number, number], [number, number]] = [
|
|
164
|
+
[-1, -2],
|
|
165
|
+
[3, 4],
|
|
166
|
+
];
|
|
167
|
+
render(
|
|
168
|
+
<MapCanvas plan={{ width: 100, height: 50 }}>
|
|
169
|
+
<MapControls fitBounds={bounds} />
|
|
170
|
+
</MapCanvas>,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const map = MockMap.instances[0]!;
|
|
174
|
+
await user.click(await screen.findByRole("button", { name: "Fit the whole plan" }));
|
|
175
|
+
|
|
176
|
+
expect(map.fitBoundsCalls.at(-1)!.bounds).toEqual(bounds);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// A static plan map drops the zoom chrome but still needs the fit button: it is
|
|
180
|
+
// the only way back for a reader who cannot pan.
|
|
181
|
+
it("survives a static plan map, where the zoom chrome is gone", async () => {
|
|
182
|
+
render(
|
|
183
|
+
<MapCanvas interactive={false} plan={{ width: 100, height: 50 }}>
|
|
184
|
+
<MapControls />
|
|
185
|
+
</MapCanvas>,
|
|
186
|
+
);
|
|
187
|
+
expect(await screen.findByRole("button", { name: "Fit the whole plan" })).toBeInTheDocument();
|
|
188
|
+
expect(screen.queryByRole("button", { name: "Zoom in" })).not.toBeInTheDocument();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("can be turned off on a plan", async () => {
|
|
192
|
+
render(
|
|
193
|
+
<MapCanvas plan={{ width: 100, height: 50 }}>
|
|
194
|
+
<MapControls showFit={false} />
|
|
195
|
+
</MapCanvas>,
|
|
196
|
+
);
|
|
197
|
+
expect(await screen.findByRole("button", { name: "Zoom in" })).toBeInTheDocument();
|
|
198
|
+
expect(screen.queryByRole("button", { name: "Fit the whole plan" })).not.toBeInTheDocument();
|
|
199
|
+
});
|
|
56
200
|
});
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
|
|
4
|
-
import { Locate, Maximize, Minus, Plus } from "lucide-react";
|
|
5
|
-
import { Spinner } from "@elabs-ai/components-ui";
|
|
4
|
+
import { Locate, Maximize, Minus, Plus, Scan } from "lucide-react";
|
|
5
|
+
import { Spinner, useLocale } from "@elabs-ai/components-ui";
|
|
6
6
|
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
7
7
|
|
|
8
|
+
import { useMapFrame } from "../lib/use-map-breakpoint";
|
|
8
9
|
import { useMap } from "../map-canvas/map-context";
|
|
9
10
|
|
|
10
11
|
export interface MapControlsProps {
|
|
11
12
|
/** Position of the controls on the map (default: "bottom-right"). */
|
|
12
13
|
position?: "top-left" | "top-right" | "bottom-left" | "bottom-right";
|
|
13
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Show zoom in/out buttons. Default: true on an interactive map, false on a
|
|
16
|
+
* static one (`<MapCanvas interactive={false}>`), whose whole point is a
|
|
17
|
+
* fixed view — set `showZoom` explicitly to override either way.
|
|
18
|
+
*/
|
|
14
19
|
showZoom?: boolean;
|
|
15
20
|
/** Show a compass button to reset bearing/pitch (default: false). */
|
|
16
21
|
showCompass?: boolean;
|
|
@@ -18,6 +23,18 @@ export interface MapControlsProps {
|
|
|
18
23
|
showLocate?: boolean;
|
|
19
24
|
/** Show a fullscreen toggle button (default: false). */
|
|
20
25
|
showFullscreen?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Show a button that frames the whole plan again — a plan map has an edge to
|
|
28
|
+
* come back to, so this defaults to ON there and off on a geographic map.
|
|
29
|
+
*/
|
|
30
|
+
showFit?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* What the fit button frames. Defaults to the plan's own extent; pass bounds
|
|
33
|
+
* to frame something else (one wing, the selected cluster).
|
|
34
|
+
*/
|
|
35
|
+
fitBounds?: [[number, number], [number, number]];
|
|
36
|
+
/** Called after the camera has been asked to frame the plan. */
|
|
37
|
+
onFit?: () => void;
|
|
21
38
|
/** Additional CSS classes for the controls container. */
|
|
22
39
|
className?: string;
|
|
23
40
|
/** Callback with user coordinates when located. */
|
|
@@ -74,16 +91,26 @@ function ControlButton({
|
|
|
74
91
|
/** Branded zoom / compass / locate / fullscreen controls. Render inside `<MapCanvas>`. */
|
|
75
92
|
export function MapControls({
|
|
76
93
|
position = "bottom-right",
|
|
77
|
-
showZoom
|
|
94
|
+
showZoom,
|
|
78
95
|
showCompass = false,
|
|
79
96
|
showLocate = false,
|
|
80
97
|
showFullscreen = false,
|
|
98
|
+
showFit,
|
|
99
|
+
fitBounds,
|
|
81
100
|
className,
|
|
82
101
|
onLocate,
|
|
83
102
|
onLocateError,
|
|
103
|
+
onFit,
|
|
84
104
|
}: MapControlsProps) {
|
|
85
|
-
const { map } = useMap();
|
|
105
|
+
const { map, plan } = useMap();
|
|
106
|
+
const { t } = useLocale();
|
|
107
|
+
const { interactive } = useMapFrame();
|
|
108
|
+
// A static map (`interactive={false}`) is a fixed editorial view: zoom chrome
|
|
109
|
+
// invites a gesture the map will not answer, and a click leaves the reader on
|
|
110
|
+
// a frame they cannot pan back from. The host may still ask for it.
|
|
111
|
+
const zoomVisible = showZoom ?? interactive;
|
|
86
112
|
const [waitingForLocation, setWaitingForLocation] = useState(false);
|
|
113
|
+
const fitVisible = showFit ?? plan != null;
|
|
87
114
|
|
|
88
115
|
const handleZoomIn = useCallback(() => {
|
|
89
116
|
map?.zoomTo(map.getZoom() + 1, { duration: 300 });
|
|
@@ -121,6 +148,13 @@ export function MapControls({
|
|
|
121
148
|
);
|
|
122
149
|
}, [map, onLocate, onLocateError]);
|
|
123
150
|
|
|
151
|
+
const handleFit = useCallback(() => {
|
|
152
|
+
const bounds = fitBounds ?? plan?.bounds;
|
|
153
|
+
if (!map || !bounds) return;
|
|
154
|
+
map.fitBounds(bounds, { padding: 24, duration: 300 });
|
|
155
|
+
onFit?.();
|
|
156
|
+
}, [fitBounds, map, onFit, plan]);
|
|
157
|
+
|
|
124
158
|
const handleFullscreen = useCallback(() => {
|
|
125
159
|
const container = map?.getContainer();
|
|
126
160
|
if (!container) return;
|
|
@@ -131,16 +165,19 @@ export function MapControls({
|
|
|
131
165
|
}
|
|
132
166
|
}, [map]);
|
|
133
167
|
|
|
168
|
+
// Nothing to show (a static map with no explicit group) renders no box at all.
|
|
169
|
+
if (!zoomVisible && !fitVisible && !showCompass && !showLocate && !showFullscreen) return null;
|
|
170
|
+
|
|
134
171
|
return (
|
|
135
172
|
<div
|
|
136
173
|
className={cn("absolute z-10 flex flex-col gap-1.5", positionClasses[position], className)}
|
|
137
174
|
>
|
|
138
|
-
{
|
|
175
|
+
{zoomVisible && (
|
|
139
176
|
<ControlGroup>
|
|
140
|
-
<ControlButton onClick={handleZoomIn} label="
|
|
177
|
+
<ControlButton onClick={handleZoomIn} label={t("maps.controls.zoomIn")}>
|
|
141
178
|
<Plus className="size-4" aria-hidden="true" />
|
|
142
179
|
</ControlButton>
|
|
143
|
-
<ControlButton onClick={handleZoomOut} label="
|
|
180
|
+
<ControlButton onClick={handleZoomOut} label={t("maps.controls.zoomOut")}>
|
|
144
181
|
<Minus className="size-4" aria-hidden="true" />
|
|
145
182
|
</ControlButton>
|
|
146
183
|
</ControlGroup>
|
|
@@ -154,20 +191,27 @@ export function MapControls({
|
|
|
154
191
|
<ControlGroup>
|
|
155
192
|
<ControlButton
|
|
156
193
|
onClick={handleLocate}
|
|
157
|
-
label="
|
|
194
|
+
label={t("maps.controls.locate")}
|
|
158
195
|
disabled={waitingForLocation}
|
|
159
196
|
>
|
|
160
197
|
{waitingForLocation ? (
|
|
161
|
-
<Spinner label="
|
|
198
|
+
<Spinner label={t("maps.controls.locating")} className="size-4 text-foreground" />
|
|
162
199
|
) : (
|
|
163
200
|
<Locate className="size-4" aria-hidden="true" />
|
|
164
201
|
)}
|
|
165
202
|
</ControlButton>
|
|
166
203
|
</ControlGroup>
|
|
167
204
|
)}
|
|
205
|
+
{fitVisible && (
|
|
206
|
+
<ControlGroup>
|
|
207
|
+
<ControlButton onClick={handleFit} label={t("maps.controls.fit")}>
|
|
208
|
+
<Scan className="size-4" aria-hidden="true" />
|
|
209
|
+
</ControlButton>
|
|
210
|
+
</ControlGroup>
|
|
211
|
+
)}
|
|
168
212
|
{showFullscreen && (
|
|
169
213
|
<ControlGroup>
|
|
170
|
-
<ControlButton onClick={handleFullscreen} label="
|
|
214
|
+
<ControlButton onClick={handleFullscreen} label={t("maps.controls.fullscreen")}>
|
|
171
215
|
<Maximize className="size-4" aria-hidden="true" />
|
|
172
216
|
</ControlButton>
|
|
173
217
|
</ControlGroup>
|
|
@@ -178,6 +222,7 @@ export function MapControls({
|
|
|
178
222
|
|
|
179
223
|
function CompassButton({ onClick }: { onClick: () => void }) {
|
|
180
224
|
const { map } = useMap();
|
|
225
|
+
const { t } = useLocale();
|
|
181
226
|
const compassRef = useRef<SVGSVGElement>(null);
|
|
182
227
|
|
|
183
228
|
useEffect(() => {
|
|
@@ -202,7 +247,7 @@ function CompassButton({ onClick }: { onClick: () => void }) {
|
|
|
202
247
|
}, [map]);
|
|
203
248
|
|
|
204
249
|
return (
|
|
205
|
-
<ControlButton onClick={onClick} label="
|
|
250
|
+
<ControlButton onClick={onClick} label={t("maps.controls.resetBearing")}>
|
|
206
251
|
<svg
|
|
207
252
|
ref={compassRef}
|
|
208
253
|
viewBox="0 0 24 24"
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
3
|
+
import { expect, waitFor } from "storybook/test";
|
|
3
4
|
import { resolveTokenColor } from "@elabs-ai/components-tokens";
|
|
4
5
|
|
|
5
|
-
import { MapCanvas } from "../map-canvas";
|
|
6
|
+
import { MapCanvas, type MapCanvasRef } from "../map-canvas";
|
|
7
|
+
import { GREATER_TORONTO, LAKE_ONTARIO, LOCATOR_BOUNDS } from "../test-utils/locator-fixture";
|
|
6
8
|
import { MapGeoJSON } from "./map-geojson";
|
|
7
9
|
|
|
8
10
|
/**
|
|
@@ -64,6 +66,39 @@ function makeRegions(): GeoJSON.FeatureCollection<GeoJSON.Polygon, RegionProps>
|
|
|
64
66
|
|
|
65
67
|
const regions = makeRegions();
|
|
66
68
|
|
|
69
|
+
/**
|
|
70
|
+
* The regions' own extent, as `[[west, south], [east, north]]` (c-9). A fixed
|
|
71
|
+
* `center`/`zoom` framed the grid for a wide box only: at 380 px two of the six
|
|
72
|
+
* columns fell outside the map with no edge cue and no way to pan a static map
|
|
73
|
+
* back to them. Fitting the data keeps every region inside the box at every
|
|
74
|
+
* width, exactly as the `PatternAndVignette` story already does.
|
|
75
|
+
*/
|
|
76
|
+
function boundsOf(
|
|
77
|
+
collection: GeoJSON.FeatureCollection<GeoJSON.Polygon, RegionProps>,
|
|
78
|
+
): [[number, number], [number, number]] {
|
|
79
|
+
let west = Infinity,
|
|
80
|
+
south = Infinity,
|
|
81
|
+
east = -Infinity,
|
|
82
|
+
north = -Infinity;
|
|
83
|
+
for (const feature of collection.features) {
|
|
84
|
+
for (const ring of feature.geometry.coordinates) {
|
|
85
|
+
for (const [lng, lat] of ring) {
|
|
86
|
+
west = Math.min(west, lng);
|
|
87
|
+
east = Math.max(east, lng);
|
|
88
|
+
south = Math.min(south, lat);
|
|
89
|
+
north = Math.max(north, lat);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return [
|
|
94
|
+
[west, south],
|
|
95
|
+
[east, north],
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const REGION_BOUNDS = boundsOf(regions);
|
|
100
|
+
const REGION_FIT = { padding: 16 };
|
|
101
|
+
|
|
67
102
|
const meta = {
|
|
68
103
|
title: "Maps/MapGeoJSON",
|
|
69
104
|
component: MapGeoJSON,
|
|
@@ -73,15 +108,68 @@ const meta = {
|
|
|
73
108
|
export default meta;
|
|
74
109
|
type Story = StoryObj<typeof meta>;
|
|
75
110
|
|
|
111
|
+
/** The map the framing assertion below measures. */
|
|
112
|
+
let framedMap: MapCanvasRef | null = null;
|
|
113
|
+
|
|
76
114
|
/** Neutral fills + hairline outlines from the theme tokens, on a blank canvas. */
|
|
77
115
|
export const Default: Story = {
|
|
78
116
|
render: () => (
|
|
79
117
|
<div className="h-[480px]">
|
|
80
|
-
<MapCanvas
|
|
118
|
+
<MapCanvas
|
|
119
|
+
blank
|
|
120
|
+
bounds={REGION_BOUNDS}
|
|
121
|
+
fitBoundsOptions={REGION_FIT}
|
|
122
|
+
ref={(map) => {
|
|
123
|
+
framedMap = map;
|
|
124
|
+
}}
|
|
125
|
+
>
|
|
81
126
|
<MapGeoJSON data={regions} />
|
|
82
127
|
</MapCanvas>
|
|
83
128
|
</div>
|
|
84
129
|
),
|
|
130
|
+
play: async ({ canvasElement }) => {
|
|
131
|
+
// c-9: at phone width the fixed `center`/`zoom` put two of the six region
|
|
132
|
+
// columns outside the box — 38.7 % of the data's area, with no edge cue,
|
|
133
|
+
// no scale change and no message, on a map nobody can pan. Squeeze the
|
|
134
|
+
// box to the reviewer's 380 px and check every corner of the data is
|
|
135
|
+
// still painted inside it.
|
|
136
|
+
const box = canvasElement.querySelector<HTMLElement>("div");
|
|
137
|
+
if (!box) throw new Error("the story's map box is missing");
|
|
138
|
+
box.style.width = "380px";
|
|
139
|
+
|
|
140
|
+
const [[west, south], [east, north]] = REGION_BOUNDS;
|
|
141
|
+
await waitFor(
|
|
142
|
+
() => {
|
|
143
|
+
const map = framedMap;
|
|
144
|
+
expect(map).not.toBeNull();
|
|
145
|
+
const canvas = map!.getContainer().getBoundingClientRect();
|
|
146
|
+
expect(Math.round(canvas.width)).toBe(380);
|
|
147
|
+
for (const corner of [
|
|
148
|
+
[west, south],
|
|
149
|
+
[east, south],
|
|
150
|
+
[east, north],
|
|
151
|
+
[west, north],
|
|
152
|
+
] as [number, number][]) {
|
|
153
|
+
const point = map!.project(corner);
|
|
154
|
+
expect(point.x).toBeGreaterThanOrEqual(0);
|
|
155
|
+
expect(point.x).toBeLessThanOrEqual(canvas.width);
|
|
156
|
+
expect(point.y).toBeGreaterThanOrEqual(0);
|
|
157
|
+
expect(point.y).toBeLessThanOrEqual(canvas.height);
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
{ timeout: 8000 },
|
|
161
|
+
);
|
|
162
|
+
// Hand the story back its own width — a play runs in the dev Storybook
|
|
163
|
+
// too — and check the map re-frames itself on the way back out.
|
|
164
|
+
box.style.width = "";
|
|
165
|
+
await waitFor(() => {
|
|
166
|
+
const canvas = framedMap!.getContainer().getBoundingClientRect();
|
|
167
|
+
expect(Math.round(canvas.width)).toBeGreaterThan(380);
|
|
168
|
+
const point = framedMap!.project([east, north]);
|
|
169
|
+
expect(point.x).toBeLessThanOrEqual(canvas.width);
|
|
170
|
+
expect(point.x).toBeGreaterThanOrEqual(0);
|
|
171
|
+
});
|
|
172
|
+
},
|
|
85
173
|
};
|
|
86
174
|
|
|
87
175
|
function ChoroplethDemo() {
|
|
@@ -91,7 +179,7 @@ function ChoroplethDemo() {
|
|
|
91
179
|
const primary = useThemedTokenColor("--primary");
|
|
92
180
|
return (
|
|
93
181
|
<div className="relative h-[480px]">
|
|
94
|
-
<MapCanvas blank
|
|
182
|
+
<MapCanvas blank bounds={REGION_BOUNDS} fitBoundsOptions={REGION_FIT}>
|
|
95
183
|
<MapGeoJSON<RegionProps>
|
|
96
184
|
data={regions}
|
|
97
185
|
promoteId="name"
|
|
@@ -109,6 +197,12 @@ function ChoroplethDemo() {
|
|
|
109
197
|
] as never,
|
|
110
198
|
}}
|
|
111
199
|
fillHoverPaint={{ "fill-opacity": 1 }}
|
|
200
|
+
// c-2: the fill lives in WebGL, so without this the 24 values are
|
|
201
|
+
// reachable by mouse only. One visually-hidden button per region
|
|
202
|
+
// carries the same sentence the readout prints.
|
|
203
|
+
featureLabel={(feature) =>
|
|
204
|
+
`${feature.properties.name}: ${(feature.properties.value * 100).toFixed(0)}%`
|
|
205
|
+
}
|
|
112
206
|
onHover={(e) => setHovered(e?.feature.properties ?? null)}
|
|
113
207
|
/>
|
|
114
208
|
</MapCanvas>
|
|
@@ -121,4 +215,60 @@ function ChoroplethDemo() {
|
|
|
121
215
|
|
|
122
216
|
export const InteractiveHover: Story = {
|
|
123
217
|
render: () => <ChoroplethDemo />,
|
|
218
|
+
play: async ({ canvasElement }) => {
|
|
219
|
+
// c-2 (WCAG 2.1.1 / 1.3.1): before the fix the only tab stop in the story
|
|
220
|
+
// was the map canvas, and none of the 24 region values had any DOM at all.
|
|
221
|
+
const list = await waitFor(() => {
|
|
222
|
+
const element = canvasElement.querySelector('[data-slot="map-geojson-keyboard-list"]');
|
|
223
|
+
expect(element).not.toBeNull();
|
|
224
|
+
return element as HTMLElement;
|
|
225
|
+
});
|
|
226
|
+
const buttons = [...list.querySelectorAll("button")];
|
|
227
|
+
await expect(buttons).toHaveLength(24);
|
|
228
|
+
await expect(buttons[0]).toHaveAccessibleName("Region 1: 37%");
|
|
229
|
+
|
|
230
|
+
// Focusing a region updates the very readout the pointer drives.
|
|
231
|
+
buttons[9]?.focus();
|
|
232
|
+
await waitFor(() =>
|
|
233
|
+
expect(canvasElement.textContent).toContain(
|
|
234
|
+
buttons[9]?.textContent ?? "no region under focus",
|
|
235
|
+
),
|
|
236
|
+
);
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
function AreaMarkersDemo() {
|
|
241
|
+
const lake = useThemedTokenColor("--chart-1");
|
|
242
|
+
const region = useThemedTokenColor("--chart-2");
|
|
243
|
+
return (
|
|
244
|
+
<div className="w-full p-4">
|
|
245
|
+
<MapCanvas interactive={false} bounds={LOCATOR_BOUNDS} fitBoundsOptions={{ padding: 32 }}>
|
|
246
|
+
<MapGeoJSON
|
|
247
|
+
id="gta"
|
|
248
|
+
data={GREATER_TORONTO}
|
|
249
|
+
fillPaint={{ "fill-color": region }}
|
|
250
|
+
fillOpacity={0.3}
|
|
251
|
+
vignette={{ width: 10, opacity: 0.4 }}
|
|
252
|
+
linePaint={false}
|
|
253
|
+
/>
|
|
254
|
+
<MapGeoJSON
|
|
255
|
+
id="lake"
|
|
256
|
+
data={LAKE_ONTARIO}
|
|
257
|
+
fillPaint={{ "fill-color": lake }}
|
|
258
|
+
pattern={{ kind: "stripes", width: 2, gap: 4 }}
|
|
259
|
+
vignette={{ width: 14, opacity: 0.35 }}
|
|
260
|
+
linePaint={{ "line-color": lake, "line-width": 1 }}
|
|
261
|
+
/>
|
|
262
|
+
</MapCanvas>
|
|
263
|
+
</div>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Area markers for a locator: a striped fill (a second channel besides
|
|
269
|
+
* colour — it survives greyscale) and a soft vignette glow along the edge.
|
|
270
|
+
* `fillOpacity` sets the tint under the stripes.
|
|
271
|
+
*/
|
|
272
|
+
export const PatternAndVignette: Story = {
|
|
273
|
+
render: () => <AreaMarkersDemo />,
|
|
124
274
|
};
|