@elabs-ai/components-maps 4.1.0 → 5.0.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 -1456
- 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 +421 -1
- package/src/map-canvas/map-canvas.tsx +466 -20
- 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 +383 -3
- package/src/map-marker/map-marker.tsx +414 -44
- 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 +18 -11
- 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 +267 -20
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { cleanup, render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import type { MapPlanRegion } from "../lib/plan-regions";
|
|
6
|
+
import type { PlanStatus } from "../lib/plan-status";
|
|
7
|
+
import { MapPlanTable } from "./map-plan-table";
|
|
8
|
+
|
|
9
|
+
const REGIONS: MapPlanRegion[] = [
|
|
10
|
+
{ id: "a1", label: "Cell A1", bounds: { x: 0, y: 0, width: 10, height: 10 } },
|
|
11
|
+
{ id: "a2", label: "Cell A2", bounds: { x: 10, y: 0, width: 10, height: 10 } },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const STATUS: Record<string, PlanStatus> = { a1: "down", a2: "free" };
|
|
15
|
+
|
|
16
|
+
afterEach(cleanup);
|
|
17
|
+
|
|
18
|
+
describe("MapPlanTable", () => {
|
|
19
|
+
it("repeats the plan as words, one row per region", () => {
|
|
20
|
+
render(
|
|
21
|
+
<MapPlanTable
|
|
22
|
+
regions={REGIONS}
|
|
23
|
+
caption="Every cell on the line, with its state"
|
|
24
|
+
status={(region) => STATUS[region.id]}
|
|
25
|
+
statusLabels={{ down: "stopped", free: "free" }}
|
|
26
|
+
statusHeader="State"
|
|
27
|
+
columns={[{ key: "throughput", header: "Units/h", numeric: true, cell: () => 120 }]}
|
|
28
|
+
/>,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const table = screen.getByRole("table", { name: "Every cell on the line, with its state" });
|
|
32
|
+
expect(table).toBeInTheDocument();
|
|
33
|
+
expect(screen.getAllByRole("row")).toHaveLength(REGIONS.length + 1);
|
|
34
|
+
// The status word is the point: a status that exists only as a colour on the
|
|
35
|
+
// canvas is invisible both to a screen reader and in greyscale.
|
|
36
|
+
expect(screen.getByText("stopped")).toBeInTheDocument();
|
|
37
|
+
expect(screen.getByText("free")).toBeInTheDocument();
|
|
38
|
+
expect(screen.getByRole("columnheader", { name: "State" })).toBeInTheDocument();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("keeps a print-only table out of the screen layout but in the DOM", () => {
|
|
42
|
+
render(<MapPlanTable regions={REGIONS} caption="Cells" printOnly />);
|
|
43
|
+
|
|
44
|
+
const root = document.querySelector('[data-slot="map-plan-table"]')!;
|
|
45
|
+
expect(root.className).toContain("hidden");
|
|
46
|
+
expect(root.className).toContain("print:block");
|
|
47
|
+
expect(screen.getByRole("table")).toBeInTheDocument();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("makes each row a second way to reach a region", async () => {
|
|
51
|
+
const user = userEvent.setup();
|
|
52
|
+
const onSelect = vi.fn();
|
|
53
|
+
render(<MapPlanTable regions={REGIONS} caption="Cells" onSelect={onSelect} />);
|
|
54
|
+
|
|
55
|
+
await user.click(screen.getByRole("button", { name: "Cell A2" }));
|
|
56
|
+
|
|
57
|
+
expect(onSelect).toHaveBeenCalledWith("a2", REGIONS[1]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("mirrors the selection, single id or many", () => {
|
|
61
|
+
const { rerender } = render(
|
|
62
|
+
<MapPlanTable regions={REGIONS} caption="Cells" onSelect={() => {}} selectedId="a1" />,
|
|
63
|
+
);
|
|
64
|
+
const rows = () => screen.getAllByRole("row").slice(1);
|
|
65
|
+
expect(rows().map((row) => row.getAttribute("aria-selected"))).toEqual(["true", "false"]);
|
|
66
|
+
|
|
67
|
+
rerender(
|
|
68
|
+
<MapPlanTable
|
|
69
|
+
regions={REGIONS}
|
|
70
|
+
caption="Cells"
|
|
71
|
+
onSelect={() => {}}
|
|
72
|
+
selectedId={["a1", "a2"]}
|
|
73
|
+
/>,
|
|
74
|
+
);
|
|
75
|
+
expect(rows().map((row) => row.getAttribute("aria-selected"))).toEqual(["true", "true"]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("does not claim selectability when there is nothing to select", () => {
|
|
79
|
+
render(<MapPlanTable regions={REGIONS} caption="Cells" selectedId="a1" />);
|
|
80
|
+
|
|
81
|
+
for (const row of screen.getAllByRole("row").slice(1)) {
|
|
82
|
+
expect(row).not.toHaveAttribute("aria-selected");
|
|
83
|
+
}
|
|
84
|
+
expect(screen.queryByRole("button")).not.toBeInTheDocument();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Table,
|
|
6
|
+
TableBody,
|
|
7
|
+
TableCaption,
|
|
8
|
+
TableCell,
|
|
9
|
+
TableHead,
|
|
10
|
+
TableHeader,
|
|
11
|
+
TableRow,
|
|
12
|
+
} from "@elabs-ai/components-ui";
|
|
13
|
+
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
14
|
+
|
|
15
|
+
import type { MapPlanRegion } from "../lib/plan-regions";
|
|
16
|
+
import { PLAN_STATUS_ENCODING, type PlanStatus } from "../lib/plan-status";
|
|
17
|
+
|
|
18
|
+
export interface MapPlanTableColumn {
|
|
19
|
+
/** Column key, used for the cell lookup and the React key. */
|
|
20
|
+
key: string;
|
|
21
|
+
/** Column header text. */
|
|
22
|
+
header: string;
|
|
23
|
+
/** Right-align the column — numbers belong on the end edge. */
|
|
24
|
+
numeric?: boolean;
|
|
25
|
+
/** The cell's content for one region. */
|
|
26
|
+
cell: (region: MapPlanRegion) => ReactNode;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface MapPlanTableProps {
|
|
30
|
+
/** The same regions the plan draws. */
|
|
31
|
+
regions: readonly MapPlanRegion[];
|
|
32
|
+
/** What the table is a table of ("Every room on level 3, with its state"). */
|
|
33
|
+
caption: string;
|
|
34
|
+
/** Extra columns after the name. The status column is built in when `status` is given. */
|
|
35
|
+
columns?: readonly MapPlanTableColumn[];
|
|
36
|
+
/** A region's status, if the plan has one. Adds a status column with the word. */
|
|
37
|
+
status?: (region: MapPlanRegion) => PlanStatus | undefined;
|
|
38
|
+
/** The word for each status — supplied by the consumer, so it can be localized. */
|
|
39
|
+
statusLabels?: Partial<Record<PlanStatus, string>>;
|
|
40
|
+
/** Header for the built-in status column. */
|
|
41
|
+
statusHeader?: string;
|
|
42
|
+
/** Header for the name column. */
|
|
43
|
+
nameHeader?: string;
|
|
44
|
+
/** The selected region id(s), mirrored as a row state. */
|
|
45
|
+
selectedId?: string | readonly string[] | null;
|
|
46
|
+
/** Called when a row is chosen — the table is a second way to reach a region. */
|
|
47
|
+
onSelect?: (id: string, region: MapPlanRegion) => void;
|
|
48
|
+
/**
|
|
49
|
+
* Hide the table on screen and keep it for print (default false). A WebGL
|
|
50
|
+
* canvas prints blank, so a plan that is meant to be printed needs this.
|
|
51
|
+
*/
|
|
52
|
+
printOnly?: boolean;
|
|
53
|
+
className?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The plan as words — the parallel channel for anyone who cannot see the canvas,
|
|
58
|
+
* for anyone who needs to sort or search it, and for the printer, which gets a
|
|
59
|
+
* blank rectangle where the WebGL canvas was.
|
|
60
|
+
*
|
|
61
|
+
* Visible by default rather than `sr-only`: the information is useful to
|
|
62
|
+
* everyone, and a hidden twin quietly rots.
|
|
63
|
+
*/
|
|
64
|
+
export function MapPlanTable({
|
|
65
|
+
regions,
|
|
66
|
+
caption,
|
|
67
|
+
columns,
|
|
68
|
+
status,
|
|
69
|
+
statusLabels,
|
|
70
|
+
statusHeader,
|
|
71
|
+
nameHeader = "Name",
|
|
72
|
+
selectedId,
|
|
73
|
+
onSelect,
|
|
74
|
+
printOnly = false,
|
|
75
|
+
className,
|
|
76
|
+
}: MapPlanTableProps) {
|
|
77
|
+
const selected = selectedId == null ? [] : Array.isArray(selectedId) ? selectedId : [selectedId];
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div data-slot="map-plan-table" className={cn(printOnly && "hidden print:block", className)}>
|
|
81
|
+
<Table>
|
|
82
|
+
<TableCaption>{caption}</TableCaption>
|
|
83
|
+
<TableHeader>
|
|
84
|
+
<TableRow>
|
|
85
|
+
<TableHead scope="col">{nameHeader}</TableHead>
|
|
86
|
+
{status && <TableHead scope="col">{statusHeader}</TableHead>}
|
|
87
|
+
{columns?.map((column) => (
|
|
88
|
+
<TableHead key={column.key} scope="col" className={column.numeric ? "text-end" : ""}>
|
|
89
|
+
{column.header}
|
|
90
|
+
</TableHead>
|
|
91
|
+
))}
|
|
92
|
+
</TableRow>
|
|
93
|
+
</TableHeader>
|
|
94
|
+
<TableBody>
|
|
95
|
+
{regions.map((region) => {
|
|
96
|
+
const regionStatus = status?.(region);
|
|
97
|
+
const encoding = regionStatus ? PLAN_STATUS_ENCODING[regionStatus] : undefined;
|
|
98
|
+
const isSelected = selected.includes(region.id);
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<TableRow
|
|
102
|
+
key={region.id}
|
|
103
|
+
data-slot="map-plan-table-row"
|
|
104
|
+
data-selected={isSelected || undefined}
|
|
105
|
+
data-status={regionStatus}
|
|
106
|
+
aria-selected={onSelect ? isSelected : undefined}
|
|
107
|
+
>
|
|
108
|
+
<TableCell>
|
|
109
|
+
{onSelect ? (
|
|
110
|
+
<button
|
|
111
|
+
type="button"
|
|
112
|
+
onClick={() => onSelect(region.id, region)}
|
|
113
|
+
className="rounded-sm text-start focus-ring"
|
|
114
|
+
>
|
|
115
|
+
{region.label}
|
|
116
|
+
</button>
|
|
117
|
+
) : (
|
|
118
|
+
region.label
|
|
119
|
+
)}
|
|
120
|
+
</TableCell>
|
|
121
|
+
{status && (
|
|
122
|
+
<TableCell>
|
|
123
|
+
{regionStatus && encoding ? (
|
|
124
|
+
<span className="inline-flex items-center gap-1.5">
|
|
125
|
+
{encoding.icon && (
|
|
126
|
+
<encoding.icon
|
|
127
|
+
className={cn("size-3.5", encoding.textClass)}
|
|
128
|
+
aria-hidden="true"
|
|
129
|
+
/>
|
|
130
|
+
)}
|
|
131
|
+
<span className={encoding.textClass}>
|
|
132
|
+
{statusLabels?.[regionStatus] ?? regionStatus}
|
|
133
|
+
</span>
|
|
134
|
+
</span>
|
|
135
|
+
) : null}
|
|
136
|
+
</TableCell>
|
|
137
|
+
)}
|
|
138
|
+
{columns?.map((column) => (
|
|
139
|
+
<TableCell
|
|
140
|
+
key={column.key}
|
|
141
|
+
className={column.numeric ? "text-end tabular-nums" : ""}
|
|
142
|
+
>
|
|
143
|
+
{column.cell(region)}
|
|
144
|
+
</TableCell>
|
|
145
|
+
))}
|
|
146
|
+
</TableRow>
|
|
147
|
+
);
|
|
148
|
+
})}
|
|
149
|
+
</TableBody>
|
|
150
|
+
</Table>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useMemo, useState } from "react";
|
|
4
|
+
|
|
5
|
+
export interface UsePlanSelectionOptions {
|
|
6
|
+
/** Allow more than one region to be selected at a time (default false). */
|
|
7
|
+
multiple?: boolean;
|
|
8
|
+
/** The ids selected to begin with. */
|
|
9
|
+
initialSelectedIds?: readonly string[];
|
|
10
|
+
/** Called whenever the selection changes. */
|
|
11
|
+
onChange?: (selectedIds: string[]) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PlanSelection {
|
|
15
|
+
/** The single selected id, or the last of a multiple selection; `null` for none. */
|
|
16
|
+
selectedId: string | null;
|
|
17
|
+
/** Every selected id, in selection order. */
|
|
18
|
+
selectedIds: string[];
|
|
19
|
+
isSelected: (id: string) => boolean;
|
|
20
|
+
/** Toggle a region; `null` clears everything. */
|
|
21
|
+
select: (id: string | null) => void;
|
|
22
|
+
clear: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Selection state for a plan, keyed by region **id** rather than index — a
|
|
27
|
+
* status tick that re-orders the regions must not move the selection with it.
|
|
28
|
+
*
|
|
29
|
+
* Hand `selectedIds` to `<MapGeoJSON selectedId>` for the paint and to
|
|
30
|
+
* `<MapPlanOverlay selectedId>` for the pressed state, so both read one source.
|
|
31
|
+
*/
|
|
32
|
+
export function usePlanSelection({
|
|
33
|
+
multiple = false,
|
|
34
|
+
initialSelectedIds,
|
|
35
|
+
onChange,
|
|
36
|
+
}: UsePlanSelectionOptions = {}): PlanSelection {
|
|
37
|
+
const [selectedIds, setSelectedIds] = useState<string[]>(() => [...(initialSelectedIds ?? [])]);
|
|
38
|
+
|
|
39
|
+
const commit = useCallback(
|
|
40
|
+
(next: string[]) => {
|
|
41
|
+
setSelectedIds(next);
|
|
42
|
+
onChange?.(next);
|
|
43
|
+
},
|
|
44
|
+
[onChange],
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const select = useCallback(
|
|
48
|
+
(id: string | null) => {
|
|
49
|
+
if (id === null) {
|
|
50
|
+
commit([]);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
setSelectedIds((current) => {
|
|
54
|
+
const next = current.includes(id)
|
|
55
|
+
? current.filter((entry) => entry !== id)
|
|
56
|
+
: multiple
|
|
57
|
+
? [...current, id]
|
|
58
|
+
: [id];
|
|
59
|
+
onChange?.(next);
|
|
60
|
+
return next;
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
[commit, multiple, onChange],
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const clear = useCallback(() => commit([]), [commit]);
|
|
67
|
+
|
|
68
|
+
return useMemo(
|
|
69
|
+
() => ({
|
|
70
|
+
selectedId: selectedIds.at(-1) ?? null,
|
|
71
|
+
selectedIds,
|
|
72
|
+
isSelected: (id: string) => selectedIds.includes(id),
|
|
73
|
+
select,
|
|
74
|
+
clear,
|
|
75
|
+
}),
|
|
76
|
+
[selectedIds, select, clear],
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { MapPlanImage, type MapPlanImageProps, type MapPlanImageSource } from "./map-plan-image";
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
3
|
+
import { expect } from "storybook/test";
|
|
4
|
+
import { StatePanel } from "@elabs-ai/components-ui";
|
|
5
|
+
|
|
6
|
+
import { MapCanvas } from "../map-canvas";
|
|
7
|
+
import { MapGeoJSON } from "../map-geojson";
|
|
8
|
+
import { useTokenColor } from "../lib/use-token-color";
|
|
9
|
+
import { MapPlanImage } from "./map-plan-image";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A small plan, so the story is about the PICTURE rather than about a building:
|
|
13
|
+
* 1200 × 600 of whatever unit the caller works in.
|
|
14
|
+
*/
|
|
15
|
+
const PLAN = { width: 1200, height: 600, unit: "cm" } as const;
|
|
16
|
+
|
|
17
|
+
/** Two rooms and a corridor, in plan units — the ink that sits on the picture. */
|
|
18
|
+
const SHAPES: GeoJSON.FeatureCollection<GeoJSON.Polygon, { id: string; name: string }> = {
|
|
19
|
+
type: "FeatureCollection",
|
|
20
|
+
features: [
|
|
21
|
+
{
|
|
22
|
+
type: "Feature",
|
|
23
|
+
id: "west",
|
|
24
|
+
properties: { id: "west", name: "West wing" },
|
|
25
|
+
geometry: {
|
|
26
|
+
type: "Polygon",
|
|
27
|
+
coordinates: [
|
|
28
|
+
[
|
|
29
|
+
[60, 60],
|
|
30
|
+
[560, 60],
|
|
31
|
+
[560, 540],
|
|
32
|
+
[60, 540],
|
|
33
|
+
[60, 60],
|
|
34
|
+
],
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
type: "Feature",
|
|
40
|
+
id: "east",
|
|
41
|
+
properties: { id: "east", name: "East wing" },
|
|
42
|
+
geometry: {
|
|
43
|
+
type: "Polygon",
|
|
44
|
+
coordinates: [
|
|
45
|
+
[
|
|
46
|
+
[640, 60],
|
|
47
|
+
[1140, 60],
|
|
48
|
+
[1140, 540],
|
|
49
|
+
[640, 540],
|
|
50
|
+
[640, 60],
|
|
51
|
+
],
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Stands in for a scan or a CAD export: a grid drawn onto a canvas, which is one of
|
|
60
|
+
* the sources `MapPlanImage` takes as-is. `hue` shifts the drawing so a floor swap
|
|
61
|
+
* is visible without a second asset.
|
|
62
|
+
*/
|
|
63
|
+
function usePlanDrawing(ink: string, step = 100) {
|
|
64
|
+
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
65
|
+
const [drawing, setDrawing] = useState<HTMLCanvasElement | null>(null);
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const canvas = canvasRef.current ?? document.createElement("canvas");
|
|
69
|
+
canvasRef.current = canvas;
|
|
70
|
+
canvas.width = PLAN.width;
|
|
71
|
+
canvas.height = PLAN.height;
|
|
72
|
+
const context = canvas.getContext("2d");
|
|
73
|
+
if (!context) return;
|
|
74
|
+
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
75
|
+
context.strokeStyle = ink;
|
|
76
|
+
context.lineWidth = 2;
|
|
77
|
+
for (let x = 0; x <= canvas.width; x += step) {
|
|
78
|
+
context.beginPath();
|
|
79
|
+
context.moveTo(x, 0);
|
|
80
|
+
context.lineTo(x, canvas.height);
|
|
81
|
+
context.stroke();
|
|
82
|
+
}
|
|
83
|
+
for (let y = 0; y <= canvas.height; y += step) {
|
|
84
|
+
context.beginPath();
|
|
85
|
+
context.moveTo(0, y);
|
|
86
|
+
context.lineTo(canvas.width, y);
|
|
87
|
+
context.stroke();
|
|
88
|
+
}
|
|
89
|
+
setDrawing(canvas);
|
|
90
|
+
}, [ink, step]);
|
|
91
|
+
|
|
92
|
+
return drawing;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const meta = {
|
|
96
|
+
title: "Maps/MapPlanImage",
|
|
97
|
+
component: MapPlanImage,
|
|
98
|
+
tags: ["autodocs"],
|
|
99
|
+
parameters: {
|
|
100
|
+
layout: "fullscreen",
|
|
101
|
+
docs: {
|
|
102
|
+
description: {
|
|
103
|
+
component:
|
|
104
|
+
"The custom background picture of a plan map: a floor plan, a scanned drawing, a CAD export or a canvas, laid out in the plan’s own units rather than on a world map. It takes a URL, a decoded image, a bitmap or a canvas, covers the whole plan by default, and accepts an `extent` in plan units for one wing of it. A swap goes through the existing source, so nothing refetches and nothing cross-fades.\n\nThe picture is context and the data on top is the ink — hold a drawing with baked-in colours back at low opacity, or leave it out and draw the plan as shapes, which themes correctly and never blurs.",
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
} satisfies Meta<typeof MapPlanImage>;
|
|
109
|
+
export default meta;
|
|
110
|
+
type Story = StoryObj<typeof meta>;
|
|
111
|
+
|
|
112
|
+
/** The whole plan: the picture underneath, the shapes on top. */
|
|
113
|
+
export const Default: Story = {
|
|
114
|
+
render: () => <PictureUnderShapes />,
|
|
115
|
+
play: async ({ canvas }) => {
|
|
116
|
+
// The picture's own parallel channel: `alt` is the one thing about a WebGL raster
|
|
117
|
+
// that assistive technology can read, so the story locks it.
|
|
118
|
+
await expect(await canvas.findByText("Survey grid, one square per metre")).toBeInTheDocument();
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
function PictureUnderShapes() {
|
|
123
|
+
return (
|
|
124
|
+
<div className="h-[420px] p-4">
|
|
125
|
+
<MapCanvas blank plan={PLAN} className="h-full">
|
|
126
|
+
<Layers />
|
|
127
|
+
</MapCanvas>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function Layers({ extent }: { extent?: { x: number; y: number; width: number; height: number } }) {
|
|
133
|
+
const ink = useTokenColor("--border-strong");
|
|
134
|
+
const outline = useTokenColor("--foreground");
|
|
135
|
+
const drawing = usePlanDrawing(ink);
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<>
|
|
139
|
+
{drawing && (
|
|
140
|
+
<MapPlanImage
|
|
141
|
+
id="drawing"
|
|
142
|
+
src={drawing}
|
|
143
|
+
extent={extent}
|
|
144
|
+
alt="Survey grid, one square per metre"
|
|
145
|
+
opacity={0.35}
|
|
146
|
+
resampling="nearest"
|
|
147
|
+
/>
|
|
148
|
+
)}
|
|
149
|
+
<MapGeoJSON
|
|
150
|
+
id="wings"
|
|
151
|
+
data={SHAPES}
|
|
152
|
+
promoteId="id"
|
|
153
|
+
interactive={false}
|
|
154
|
+
fillPaint={false}
|
|
155
|
+
linePaint={{ "line-color": outline, "line-width": 2 }}
|
|
156
|
+
/>
|
|
157
|
+
</>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** One wing only: `extent` places the picture in plan units, not across the whole plan. */
|
|
162
|
+
export const OverOneWing: Story = {
|
|
163
|
+
render: () => (
|
|
164
|
+
<div className="h-[420px] p-4">
|
|
165
|
+
<MapCanvas blank plan={PLAN} className="h-full">
|
|
166
|
+
<Layers extent={{ x: 640, y: 60, width: 500, height: 480 }} />
|
|
167
|
+
</MapCanvas>
|
|
168
|
+
</div>
|
|
169
|
+
),
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/** A floor swap: the same source, a new drawing — no refetch, no cross-fade. */
|
|
173
|
+
export const FloorSwap: Story = {
|
|
174
|
+
render: () => {
|
|
175
|
+
function Demo() {
|
|
176
|
+
const [fine, setFine] = useState(false);
|
|
177
|
+
return (
|
|
178
|
+
<div className="flex h-[460px] flex-col gap-3 p-4">
|
|
179
|
+
<label className="flex items-center gap-2 text-caption">
|
|
180
|
+
<input
|
|
181
|
+
type="checkbox"
|
|
182
|
+
checked={fine}
|
|
183
|
+
onChange={(event) => setFine(event.target.checked)}
|
|
184
|
+
/>
|
|
185
|
+
Half-metre grid
|
|
186
|
+
</label>
|
|
187
|
+
<div className="min-h-0 flex-1">
|
|
188
|
+
<MapCanvas blank plan={PLAN} className="h-full">
|
|
189
|
+
<SwappedDrawing step={fine ? 50 : 100} />
|
|
190
|
+
</MapCanvas>
|
|
191
|
+
</div>
|
|
192
|
+
<p className="text-meta text-muted-foreground">
|
|
193
|
+
The grid ink is a semantic token, resolved inside the canvas — WebGL cannot read a CSS
|
|
194
|
+
variable.
|
|
195
|
+
</p>
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
return <Demo />;
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
function SwappedDrawing({ step }: { step: number }) {
|
|
204
|
+
const ink = useTokenColor("--border-strong");
|
|
205
|
+
const drawing = usePlanDrawing(ink, step);
|
|
206
|
+
return drawing ? (
|
|
207
|
+
<MapPlanImage id="drawing" src={drawing} alt={`Survey grid, ${step} cm squares`} />
|
|
208
|
+
) : null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** `visible={false}` toggles the layer’s visibility; the source stays, so nothing reloads. */
|
|
212
|
+
export const Hidden: Story = {
|
|
213
|
+
render: () => {
|
|
214
|
+
function Demo() {
|
|
215
|
+
const [shown, setShown] = useState(false);
|
|
216
|
+
return (
|
|
217
|
+
<div className="flex h-[460px] flex-col gap-3 p-4">
|
|
218
|
+
<label className="flex items-center gap-2 text-caption">
|
|
219
|
+
<input
|
|
220
|
+
type="checkbox"
|
|
221
|
+
checked={shown}
|
|
222
|
+
onChange={(event) => setShown(event.target.checked)}
|
|
223
|
+
/>
|
|
224
|
+
Show the drawing
|
|
225
|
+
</label>
|
|
226
|
+
<div className="min-h-0 flex-1">
|
|
227
|
+
<MapCanvas blank plan={PLAN} className="h-full">
|
|
228
|
+
<ToggledDrawing visible={shown} />
|
|
229
|
+
</MapCanvas>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
return <Demo />;
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
function ToggledDrawing({ visible }: { visible: boolean }) {
|
|
239
|
+
const ink = useTokenColor("--border-strong");
|
|
240
|
+
const drawing = usePlanDrawing(ink);
|
|
241
|
+
return drawing ? (
|
|
242
|
+
<MapPlanImage id="drawing" src={drawing} visible={visible} alt="Survey grid" />
|
|
243
|
+
) : null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Nothing loaded yet: the canvas keeps the plan’s shape and says so once. */
|
|
247
|
+
export const Loading: Story = {
|
|
248
|
+
render: () => (
|
|
249
|
+
<div className="h-[420px] p-4">
|
|
250
|
+
<MapCanvas blank plan={PLAN} loading className="h-full" />
|
|
251
|
+
</div>
|
|
252
|
+
),
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The picture failed; the plan did not. `onError` reports it and the shapes stay, so
|
|
257
|
+
* the consumer decides what the gap should look like.
|
|
258
|
+
*/
|
|
259
|
+
export const MissingPicture: Story = {
|
|
260
|
+
render: () => {
|
|
261
|
+
function Demo() {
|
|
262
|
+
const [failed, setFailed] = useState(false);
|
|
263
|
+
return (
|
|
264
|
+
<div className="flex h-[460px] flex-col gap-2 p-4">
|
|
265
|
+
{failed && (
|
|
266
|
+
<StatePanel
|
|
267
|
+
kind="error"
|
|
268
|
+
title="Floor drawing unavailable"
|
|
269
|
+
description="The plan is still usable — every wing is drawn from its own coordinates."
|
|
270
|
+
/>
|
|
271
|
+
)}
|
|
272
|
+
<div className="min-h-0 flex-1">
|
|
273
|
+
<MapCanvas blank plan={PLAN} className="h-full">
|
|
274
|
+
<MapPlanImage
|
|
275
|
+
id="drawing"
|
|
276
|
+
src="./missing-drawing.png"
|
|
277
|
+
onError={() => setFailed(true)}
|
|
278
|
+
/>
|
|
279
|
+
<ShapesOnly />
|
|
280
|
+
</MapCanvas>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
return <Demo />;
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
function ShapesOnly() {
|
|
290
|
+
const outline = useTokenColor("--foreground");
|
|
291
|
+
return (
|
|
292
|
+
<MapGeoJSON
|
|
293
|
+
id="wings"
|
|
294
|
+
data={SHAPES}
|
|
295
|
+
promoteId="id"
|
|
296
|
+
interactive={false}
|
|
297
|
+
fillPaint={false}
|
|
298
|
+
linePaint={{ "line-color": outline, "line-width": 2 }}
|
|
299
|
+
/>
|
|
300
|
+
);
|
|
301
|
+
}
|