diff --git a/client/components.json b/client/components.json
index a94867e9..11f2a95c 100644
--- a/client/components.json
+++ b/client/components.json
@@ -11,6 +11,6 @@
},
"aliases": {
"components": "@/components",
- "utils": "@/lib/utils"
+ "utils": "@/lib/classnames"
}
}
\ No newline at end of file
diff --git a/client/e2e/example.spec.ts b/client/e2e/example.spec.ts
index 02d1f1fd..28b37c50 100644
--- a/client/e2e/example.spec.ts
+++ b/client/e2e/example.spec.ts
@@ -4,7 +4,7 @@ test("has title", async ({ page }) => {
await page.goto(`/`);
// Expect a title "to contain" a substring.
- await expect(page).toHaveTitle(/Create Next App/);
+ await expect(page).toHaveTitle(/CCSA/);
});
// test("has title", async ({ page }) => {
diff --git a/client/package.json b/client/package.json
index 90ddd923..09c53828 100644
--- a/client/package.json
+++ b/client/package.json
@@ -13,6 +13,10 @@
"postinstall": "cd .. && husky install client/.husky"
},
"dependencies": {
+ "@radix-ui/react-checkbox": "1.0.4",
+ "@radix-ui/react-label": "2.0.2",
+ "@radix-ui/react-popover": "1.0.7",
+ "@radix-ui/react-radio-group": "1.1.3",
"@radix-ui/react-tooltip": "1.0.7",
"@tanstack/react-query": "4.35.3",
"@typescript-eslint/eslint-plugin": "6.7.3",
@@ -29,6 +33,8 @@
"react-dom": "18.2.0",
"react-icons": "4.11.0",
"react-map-gl": "7.1.6",
+ "recoil": "0.7.7",
+ "recoil-sync": "0.2.0",
"rooks": "7.14.1",
"tailwind-merge": "1.14.0",
"tailwindcss-animate": "1.0.7",
diff --git a/client/public/images/map/light.jpeg b/client/public/images/map/light.jpeg
new file mode 100644
index 00000000..83c09d8b
Binary files /dev/null and b/client/public/images/map/light.jpeg differ
diff --git a/client/public/images/map/satellite.jpeg b/client/public/images/map/satellite.jpeg
new file mode 100644
index 00000000..1c22d6ad
Binary files /dev/null and b/client/public/images/map/satellite.jpeg differ
diff --git a/client/src/app/layout-providers.tsx b/client/src/app/layout-providers.tsx
index 0de0afd2..5f19218c 100644
--- a/client/src/app/layout-providers.tsx
+++ b/client/src/app/layout-providers.tsx
@@ -1,21 +1,42 @@
"use client";
-import { PropsWithChildren, useState } from "react";
+import { PropsWithChildren, useCallback, useState } from "react";
import { MapProvider } from "react-map-gl";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { RecoilRoot } from "recoil";
+
+import { RecoilURLSyncNext } from "@/lib/recoil";
+import type { Deserialize, Serialize } from "@/lib/recoil";
+// import RecoilDevTools from "@/lib/recoil/devtools";
import { TooltipProvider } from "@/components/ui/tooltip";
export default function LayoutProviders({ children }: PropsWithChildren) {
const [queryClient] = useState(() => new QueryClient());
+ const serialize: Serialize = useCallback((x) => {
+ return x === undefined ? "" : JSON.stringify(x);
+ }, []);
+
+ //Demo of custom deserialization
+ const deserialize: Deserialize = useCallback((x: string) => {
+ return JSON.parse(x);
+ }, []);
return (
-
- {children}
-
+
+
+
+ {children}
+
+
+
);
}
diff --git a/client/src/app/layout.tsx b/client/src/app/layout.tsx
index b7fdf189..19c02259 100644
--- a/client/src/app/layout.tsx
+++ b/client/src/app/layout.tsx
@@ -10,7 +10,7 @@ import LayoutProviders from "@/app/layout-providers";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
- title: "Create Next App",
+ title: "CCSA",
description: "Generated by create next app",
};
diff --git a/client/src/components/map/constants.ts b/client/src/components/map/constants.ts
index 736469c8..e1a21f23 100644
--- a/client/src/components/map/constants.ts
+++ b/client/src/components/map/constants.ts
@@ -5,3 +5,70 @@ export const DEFAULT_VIEW_STATE: Partial = {
latitude: 0,
longitude: 0,
};
+
+export const BASEMAPS = [
+ {
+ label: "Light",
+ value: "basemap-light",
+ preview: `/images/map/light.jpeg`,
+ settings: {
+ labels: "labels-dark",
+ boundaries: "boundaries-dark",
+ roads: "roads-dark",
+ },
+ },
+ {
+ label: "Satellite",
+ value: "basemap-satellite",
+ preview: `/images/map/satellite.jpeg`,
+ settings: {
+ labels: "labels-light",
+ boundaries: "boundaries-light",
+ roads: "roads-light",
+ },
+ },
+];
+
+export const LABELS = [
+ {
+ id: "1059d2b8cfa87b8d894b5373ea556666",
+ label: "Dark labels",
+ slug: "labels-dark",
+ },
+ {
+ id: "5924e7eeda116f817dd89f1d8d418721",
+ label: "Light labels",
+ slug: "labels-light",
+ },
+ {
+ id: "asdfasdfasdfasdf",
+ label: "No labels",
+ slug: "labels-none",
+ },
+];
+
+export const BOUNDARIES = [
+ {
+ id: "ae861f3122c21ad7754e66d3cead38e6",
+ label: "Dark boundaries",
+ slug: "boundaries-dark",
+ },
+ {
+ id: "31b240eba06a254ade36f1dde6a3c07e",
+ label: "Light boundaries",
+ slug: "boundaries-light",
+ },
+];
+
+export const ROADS = [
+ {
+ id: "4e240a8b884456747dcd07d41b4d5543",
+ label: "Dark roads",
+ slug: "roads-dark",
+ },
+ {
+ id: "edb80ef589e776ec6c2568b2fc6ad74c",
+ label: "Light roads",
+ slug: "roads-light",
+ },
+];
diff --git a/client/src/components/map/controls/settings/index.tsx b/client/src/components/map/controls/settings/index.tsx
new file mode 100644
index 00000000..9cdd2b75
--- /dev/null
+++ b/client/src/components/map/controls/settings/index.tsx
@@ -0,0 +1,62 @@
+"use client";
+
+import { FC, HTMLAttributes, PropsWithChildren } from "react";
+
+import { PopoverArrow } from "@radix-ui/react-popover";
+import { TooltipPortal } from "@radix-ui/react-tooltip";
+import { LuSettings } from "react-icons/lu";
+
+import { cn } from "@/lib/classnames";
+
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
+import { Tooltip, TooltipArrow, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
+
+import { CONTROL_BUTTON_STYLES } from "../constants";
+
+interface SettingsControlProps {
+ className?: HTMLAttributes["className"];
+}
+
+export const SettingsControl: FC> = ({
+ className,
+ children,
+}: PropsWithChildren) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ Map settings
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+ );
+};
+
+export default SettingsControl;
diff --git a/client/src/components/map/index.tsx b/client/src/components/map/index.tsx
index 8045c150..e3966b8d 100644
--- a/client/src/components/map/index.tsx
+++ b/client/src/components/map/index.tsx
@@ -145,7 +145,7 @@ export const Map: FC = ({
mapboxAccessToken={env.NEXT_PUBLIC_MAPBOX_API_TOKEN}
onMove={handleMapMove}
onLoad={handleMapLoad}
- mapStyle="mapbox://styles/marxan/ckn4fr7d71qg817kgd9vuom4s"
+ mapStyle="mapbox://styles/layer-manager/clj8fgofm000t01pjcu21agsd"
{...mapboxProps}
{...localViewState}
>
diff --git a/client/src/components/ui/checkbox.tsx b/client/src/components/ui/checkbox.tsx
new file mode 100644
index 00000000..a02d279b
--- /dev/null
+++ b/client/src/components/ui/checkbox.tsx
@@ -0,0 +1,29 @@
+"use client";
+
+import * as React from "react";
+
+import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
+import { LuCheck } from "react-icons/lu";
+
+import { cn } from "@/lib/classnames";
+
+const Checkbox = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+));
+Checkbox.displayName = CheckboxPrimitive.Root.displayName;
+
+export { Checkbox };
diff --git a/client/src/components/ui/label.tsx b/client/src/components/ui/label.tsx
new file mode 100644
index 00000000..467db53e
--- /dev/null
+++ b/client/src/components/ui/label.tsx
@@ -0,0 +1,22 @@
+"use client";
+
+import * as React from "react";
+
+import * as LabelPrimitive from "@radix-ui/react-label";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/classnames";
+
+const labelVariants = cva(
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
+);
+
+const Label = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & VariantProps
+>(({ className, ...props }, ref) => (
+
+));
+Label.displayName = LabelPrimitive.Root.displayName;
+
+export { Label };
diff --git a/client/src/components/ui/popover.tsx b/client/src/components/ui/popover.tsx
new file mode 100644
index 00000000..77dfca53
--- /dev/null
+++ b/client/src/components/ui/popover.tsx
@@ -0,0 +1,32 @@
+"use client";
+
+import * as React from "react";
+
+import * as PopoverPrimitive from "@radix-ui/react-popover";
+
+import { cn } from "@/lib/classnames";
+
+const Popover = PopoverPrimitive.Root;
+
+const PopoverTrigger = PopoverPrimitive.Trigger;
+
+const PopoverContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
+
+
+
+));
+PopoverContent.displayName = PopoverPrimitive.Content.displayName;
+
+export { Popover, PopoverTrigger, PopoverContent };
diff --git a/client/src/components/ui/radio-group.tsx b/client/src/components/ui/radio-group.tsx
new file mode 100644
index 00000000..6e673e31
--- /dev/null
+++ b/client/src/components/ui/radio-group.tsx
@@ -0,0 +1,39 @@
+"use client";
+
+import * as React from "react";
+
+import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
+import { LuCircle } from "react-icons/lu";
+
+import { cn } from "@/lib/classnames";
+
+const RadioGroup = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ return ;
+});
+RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
+
+const RadioGroupItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ return (
+
+
+
+
+
+ );
+});
+RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
+
+export { RadioGroup, RadioGroupItem };
diff --git a/client/src/containers/home/index.tsx b/client/src/containers/home/index.tsx
index 2d05ae4f..670919ed 100644
--- a/client/src/containers/home/index.tsx
+++ b/client/src/containers/home/index.tsx
@@ -1,16 +1,24 @@
"use client";
+import MapSettingsManagerPanel from "@/containers/home/map-settings";
+import MapSettingsManager from "@/containers/home/map-settings/manager";
+
import Map from "@/components/map";
import Controls from "@/components/map/controls";
+import SettingsControl from "@/components/map/controls/settings";
import ZoomControl from "@/components/map/controls/zoom";
const Home = (): JSX.Element => {
return (
);
diff --git a/client/src/containers/home/map-settings/basemaps/index.tsx b/client/src/containers/home/map-settings/basemaps/index.tsx
new file mode 100644
index 00000000..f5c5dc2d
--- /dev/null
+++ b/client/src/containers/home/map-settings/basemaps/index.tsx
@@ -0,0 +1,17 @@
+import { BASEMAPS } from "@/components/map/constants";
+
+import BasemapItem from "./item";
+
+const Basemaps = () => {
+ return (
+
+ {BASEMAPS.map((b) => (
+ -
+
+
+ ))}
+
+ );
+};
+
+export default Basemaps;
diff --git a/client/src/containers/home/map-settings/basemaps/item/index.tsx b/client/src/containers/home/map-settings/basemaps/item/index.tsx
new file mode 100644
index 00000000..dde5293f
--- /dev/null
+++ b/client/src/containers/home/map-settings/basemaps/item/index.tsx
@@ -0,0 +1,59 @@
+import { useCallback } from "react";
+
+import Image from "next/image";
+
+import { useRecoilValue, useSetRecoilState } from "recoil";
+
+import { cn } from "@/lib/classnames";
+
+import { mapSettingsAtom } from "@/store/map";
+
+export interface BasemapItemProps {
+ label: string;
+ value: string;
+ preview: string;
+}
+
+const BasemapItem = ({ label, value, preview }: BasemapItemProps) => {
+ const { basemap } = useRecoilValue(mapSettingsAtom);
+ const setMapSettings = useSetRecoilState(mapSettingsAtom);
+
+ const handleToggleBasemap = useCallback(() => {
+ setMapSettings((prev) => ({
+ ...prev,
+ basemap: value,
+ }));
+ }, [value, setMapSettings]);
+
+ return (
+
+ );
+};
+
+export default BasemapItem;
diff --git a/client/src/containers/home/map-settings/boundaries/index.tsx b/client/src/containers/home/map-settings/boundaries/index.tsx
new file mode 100644
index 00000000..755d0c42
--- /dev/null
+++ b/client/src/containers/home/map-settings/boundaries/index.tsx
@@ -0,0 +1,38 @@
+import { useCallback } from "react";
+
+import { useRecoilValue, useSetRecoilState } from "recoil";
+
+import { mapSettingsAtom } from "@/store/map";
+
+import { Checkbox } from "@/components/ui/checkbox";
+import { Label } from "@/components/ui/label";
+
+const Boundaries = () => {
+ const { boundaries } = useRecoilValue(mapSettingsAtom);
+ const setMapSettings = useSetRecoilState(mapSettingsAtom);
+
+ const handleChange = useCallback(
+ (v: boolean) => {
+ setMapSettings((prev) => ({
+ ...prev,
+ boundaries: v,
+ }));
+ },
+ [setMapSettings],
+ );
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default Boundaries;
diff --git a/client/src/containers/home/map-settings/index.tsx b/client/src/containers/home/map-settings/index.tsx
new file mode 100644
index 00000000..40f235ff
--- /dev/null
+++ b/client/src/containers/home/map-settings/index.tsx
@@ -0,0 +1,29 @@
+import Basemaps from "./basemaps";
+import Boundaries from "./boundaries";
+import Labels from "./labels";
+import Roads from "./roads";
+
+const MapSettings = () => {
+ return (
+
+ );
+};
+
+export default MapSettings;
diff --git a/client/src/containers/home/map-settings/labels/index.tsx b/client/src/containers/home/map-settings/labels/index.tsx
new file mode 100644
index 00000000..1b83eb6a
--- /dev/null
+++ b/client/src/containers/home/map-settings/labels/index.tsx
@@ -0,0 +1,42 @@
+import { useCallback } from "react";
+
+import { useRecoilValue, useSetRecoilState } from "recoil";
+
+import { mapSettingsAtom } from "@/store/map";
+
+import { LABELS } from "@/components/map/constants";
+import { Label } from "@/components/ui/label";
+import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
+
+const Labels = () => {
+ const { labels } = useRecoilValue(mapSettingsAtom);
+ const setMapSettings = useSetRecoilState(mapSettingsAtom);
+
+ const handleChange = useCallback(
+ (v: string) => {
+ setMapSettings((prev) => ({
+ ...prev,
+ labels: v,
+ }));
+ },
+ [setMapSettings],
+ );
+
+ return (
+
+ {LABELS.map((l) => (
+
+
+
+
+ ))}
+
+ );
+};
+
+export default Labels;
diff --git a/client/src/containers/home/map-settings/manager/index.tsx b/client/src/containers/home/map-settings/manager/index.tsx
new file mode 100644
index 00000000..a6a4baf2
--- /dev/null
+++ b/client/src/containers/home/map-settings/manager/index.tsx
@@ -0,0 +1,103 @@
+import { useCallback, useEffect } from "react";
+
+import { useMap } from "react-map-gl";
+
+import { AnyLayer } from "mapbox-gl";
+import { useRecoilValue } from "recoil";
+
+import { mapSettingsAtom } from "@/store/map";
+
+import { BASEMAPS } from "@/components/map/constants";
+
+type AnyLayerWithMetadata = AnyLayer & {
+ metadata: Record;
+};
+
+const MapSettingsManager = () => {
+ const { default: mapRef } = useMap();
+ const loaded = mapRef?.loaded();
+ const { basemap, labels, boundaries, roads } = useRecoilValue(mapSettingsAtom);
+
+ const handleGroup = useCallback(
+ (groups: string[], groupId: string, visible = true) => {
+ if (!mapRef) return;
+ const map = mapRef.getMap();
+ const { layers, metadata = {} } = mapRef.getStyle();
+
+ const lys = layers as AnyLayerWithMetadata[];
+
+ const GROUPS = Object.keys(metadata["mapbox:groups"] || {}).filter((k) => {
+ const { name } = metadata["mapbox:groups"][k];
+
+ const matchedGroups = groups.map((rgr) => name.toLowerCase().includes(rgr));
+
+ return matchedGroups.some((bool) => bool);
+ });
+
+ const GROUPS_META = GROUPS.map((gId) => ({
+ ...metadata["mapbox:groups"][gId],
+ id: gId,
+ }));
+ const GROUP_TO_DISPLAY = GROUPS_META.find((_group) => _group.name.includes(groupId)) || {};
+
+ const GROUPS_LAYERS = lys.filter((l) => {
+ const { metadata: layerMetadata } = l;
+ if (!layerMetadata) return false;
+
+ const gr = layerMetadata["mapbox:group"];
+ return GROUPS.includes(gr);
+ });
+
+ GROUPS_LAYERS.forEach((_layer) => {
+ const match = _layer.metadata["mapbox:group"] === GROUP_TO_DISPLAY.id && visible;
+ if (!match) {
+ map.setLayoutProperty(_layer.id, "visibility", "none");
+ } else {
+ map.setLayoutProperty(_layer.id, "visibility", "visible");
+ }
+ });
+ },
+ [mapRef],
+ );
+
+ const handleStyleLoad = useCallback(() => {
+ const B = BASEMAPS.find((b) => b.value === basemap);
+
+ handleGroup(["basemap"], basemap);
+ handleGroup(["labels"], labels);
+
+ if (B) {
+ handleGroup(["boundaries"], B.settings.boundaries, boundaries);
+ handleGroup(["roads"], B.settings.roads, roads);
+ }
+ }, [basemap, labels, boundaries, roads, handleGroup]);
+
+ // * handle style load
+ useEffect(() => {
+ if (!mapRef) return;
+ mapRef.on("style.load", handleStyleLoad);
+
+ return () => {
+ mapRef.off("style.load", handleStyleLoad);
+ };
+ }, [mapRef, loaded, handleStyleLoad]);
+
+ // * handle basemap, labels, boundaries, roads
+ useEffect(() => {
+ if (!mapRef) return;
+
+ const B = BASEMAPS.find((b) => b.value === basemap);
+
+ handleGroup(["basemap"], basemap);
+ handleGroup(["labels"], labels);
+
+ if (B) {
+ handleGroup(["boundaries"], B.settings.boundaries, boundaries);
+ handleGroup(["roads"], B.settings.roads, roads);
+ }
+ }, [mapRef, loaded, basemap, labels, boundaries, roads, handleGroup]);
+
+ return null;
+};
+
+export default MapSettingsManager;
diff --git a/client/src/containers/home/map-settings/roads/index.tsx b/client/src/containers/home/map-settings/roads/index.tsx
new file mode 100644
index 00000000..ad801f8d
--- /dev/null
+++ b/client/src/containers/home/map-settings/roads/index.tsx
@@ -0,0 +1,38 @@
+import { useCallback } from "react";
+
+import { useRecoilValue, useSetRecoilState } from "recoil";
+
+import { mapSettingsAtom } from "@/store/map";
+
+import { Checkbox } from "@/components/ui/checkbox";
+import { Label } from "@/components/ui/label";
+
+const Roads = () => {
+ const { roads } = useRecoilValue(mapSettingsAtom);
+ const setMapSettings = useSetRecoilState(mapSettingsAtom);
+
+ const handleChange = useCallback(
+ (v: boolean) => {
+ setMapSettings((prev) => ({
+ ...prev,
+ roads: v,
+ }));
+ },
+ [setMapSettings],
+ );
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default Roads;
diff --git a/client/src/lib/recoil/devtools.tsx b/client/src/lib/recoil/devtools.tsx
new file mode 100644
index 00000000..b92edd32
--- /dev/null
+++ b/client/src/lib/recoil/devtools.tsx
@@ -0,0 +1,18 @@
+import { useEffect } from "react";
+
+import { useRecoilSnapshot } from "recoil";
+
+function RecoilDevTools() {
+ const snapshot = useRecoilSnapshot();
+
+ useEffect(() => {
+ console.debug("The following atoms were modified:");
+ for (const node of snapshot.getNodes_UNSTABLE({ isModified: true })) {
+ console.debug(node.key, snapshot.getLoadable(node));
+ }
+ }, [snapshot]);
+
+ return null;
+}
+
+export default RecoilDevTools;
diff --git a/client/src/lib/recoil/index.tsx b/client/src/lib/recoil/index.tsx
new file mode 100644
index 00000000..3c37598d
--- /dev/null
+++ b/client/src/lib/recoil/index.tsx
@@ -0,0 +1,31 @@
+import { RecoilURLSync, RecoilURLSyncOptions } from "recoil-sync";
+
+import { useSyncURLNext } from "./useSyncURLNext";
+
+type Props = Omit & {
+ decodedQueryParams?: boolean;
+};
+
+export type Serialize = (data: unknown) => string;
+
+export type Deserialize = (str: string) => unknown;
+
+export const RecoilURLSyncNext: React.FC = ({ children, ...options }) => {
+ const { decodedQueryParams = true } = options;
+
+ const { browserInterface, ...defaultOptions } = useSyncURLNext({
+ decodedQueryParams,
+ });
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/client/src/lib/recoil/useSyncURLNext.ts b/client/src/lib/recoil/useSyncURLNext.ts
new file mode 100644
index 00000000..7af40822
--- /dev/null
+++ b/client/src/lib/recoil/useSyncURLNext.ts
@@ -0,0 +1,58 @@
+"use client";
+
+import { useCallback } from "react";
+
+import { usePathname, useRouter, useSearchParams } from "next/navigation";
+
+import { BrowserInterface, RecoilURLSyncOptions } from "recoil-sync";
+
+type UseSyncURLNextOptions = {
+ decodedQueryParams?: boolean;
+};
+
+export function useSyncURLNext(
+ options: UseSyncURLNextOptions,
+): Partial> {
+ const { decodedQueryParams } = options;
+
+ const pathname = usePathname();
+ const searchParams = useSearchParams();
+ const { push } = useRouter();
+
+ const browserInterface: BrowserInterface = {
+ replaceURL: useCallback(
+ (url: string) => {
+ const u = decodedQueryParams ? decodeURIComponent(url) : url;
+ return window.history.replaceState({}, "", u);
+ },
+ [decodedQueryParams],
+ ),
+
+ pushURL: useCallback(
+ (url: string) => {
+ const u = decodedQueryParams ? decodeURIComponent(url) : url;
+ return push(u);
+ },
+ [decodedQueryParams, push],
+ ),
+
+ getURL: useCallback(() => {
+ const url = new URL(
+ `${pathname}${searchParams ? `?${searchParams.toString()}` : ""}`,
+ globalThis?.document?.location?.href ?? `http://localhost:${process.env.PORT}`,
+ );
+
+ return url.toString();
+ }, [pathname, searchParams]),
+
+ listenChangeURL: useCallback((handler2: () => void) => {
+ return () => {
+ handler2();
+ };
+ }, []),
+ };
+
+ return {
+ browserInterface,
+ };
+}
diff --git a/client/src/store/map.ts b/client/src/store/map.ts
new file mode 100644
index 00000000..610ffb06
--- /dev/null
+++ b/client/src/store/map.ts
@@ -0,0 +1,25 @@
+"use client";
+
+import { bool, object, string } from "@recoiljs/refine";
+import { atom } from "recoil";
+import { urlSyncEffect } from "recoil-sync";
+
+export const mapSettingsAtom = atom({
+ key: "map-settings",
+ default: {
+ basemap: "basemap-light",
+ labels: "labels-dark",
+ boundaries: false,
+ roads: false,
+ },
+ effects: [
+ urlSyncEffect({
+ refine: object({
+ basemap: string(),
+ labels: string(),
+ boundaries: bool(),
+ roads: bool(),
+ }),
+ }),
+ ],
+});
diff --git a/client/yarn.lock b/client/yarn.lock
index 4c1e98ac..ee4f82f8 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -446,6 +446,56 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/react-checkbox@npm:1.0.4":
+ version: 1.0.4
+ resolution: "@radix-ui/react-checkbox@npm:1.0.4"
+ dependencies:
+ "@babel/runtime": ^7.13.10
+ "@radix-ui/primitive": 1.0.1
+ "@radix-ui/react-compose-refs": 1.0.1
+ "@radix-ui/react-context": 1.0.1
+ "@radix-ui/react-presence": 1.0.1
+ "@radix-ui/react-primitive": 1.0.3
+ "@radix-ui/react-use-controllable-state": 1.0.1
+ "@radix-ui/react-use-previous": 1.0.1
+ "@radix-ui/react-use-size": 1.0.1
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 6dac5bddd9e1c42b149555501440918d9eae70da13b6d8539c3bf46b6c07681119d865d2106a43f729884ae8e2043bedc34c4d00a09a527b3bf0feade088d188
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-collection@npm:1.0.3":
+ version: 1.0.3
+ resolution: "@radix-ui/react-collection@npm:1.0.3"
+ dependencies:
+ "@babel/runtime": ^7.13.10
+ "@radix-ui/react-compose-refs": 1.0.1
+ "@radix-ui/react-context": 1.0.1
+ "@radix-ui/react-primitive": 1.0.3
+ "@radix-ui/react-slot": 1.0.2
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: acfbc9b0b2c553d343c22f02c9f098bc5cfa99e6e48df91c0d671855013f8b877ade9c657b7420a7aa523b5aceadea32a60dd72c23b1291f415684fb45d00cff
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-compose-refs@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-compose-refs@npm:1.0.1"
@@ -476,6 +526,21 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/react-direction@npm:1.0.1":
+ version: 1.0.1
+ resolution: "@radix-ui/react-direction@npm:1.0.1"
+ dependencies:
+ "@babel/runtime": ^7.13.10
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 5336a8b0d4f1cde585d5c2b4448af7b3d948bb63a1aadb37c77771b0e5902dc6266e409cf35fd0edaca7f33e26424be19e64fb8f9d7f7be2d6f1714ea2764210
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-dismissable-layer@npm:1.0.5":
version: 1.0.5
resolution: "@radix-ui/react-dismissable-layer@npm:1.0.5"
@@ -500,6 +565,43 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/react-focus-guards@npm:1.0.1":
+ version: 1.0.1
+ resolution: "@radix-ui/react-focus-guards@npm:1.0.1"
+ dependencies:
+ "@babel/runtime": ^7.13.10
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 1f8ca8f83b884b3612788d0742f3f054e327856d90a39841a47897dbed95e114ee512362ae314177de226d05310047cabbf66b686ae86ad1b65b6b295be24ef7
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-focus-scope@npm:1.0.4":
+ version: 1.0.4
+ resolution: "@radix-ui/react-focus-scope@npm:1.0.4"
+ dependencies:
+ "@babel/runtime": ^7.13.10
+ "@radix-ui/react-compose-refs": 1.0.1
+ "@radix-ui/react-primitive": 1.0.3
+ "@radix-ui/react-use-callback-ref": 1.0.1
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 3481db1a641513a572734f0bcb0e47fefeba7bccd6ec8dde19f520719c783ef0b05a55ef0d5292078ed051cc5eda46b698d5d768da02e26e836022f46b376fd1
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-id@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-id@npm:1.0.1"
@@ -516,6 +618,60 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/react-label@npm:2.0.2":
+ version: 2.0.2
+ resolution: "@radix-ui/react-label@npm:2.0.2"
+ dependencies:
+ "@babel/runtime": ^7.13.10
+ "@radix-ui/react-primitive": 1.0.3
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: fe3bd8902bc523fb5125fa96167a13b8a60d007413787eae9573e4b00b0edff0487c4c0620ea5dc37e6da13833ebc4f8d7e00b6c846f2a5686e7f173672b8dde
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-popover@npm:1.0.7":
+ version: 1.0.7
+ resolution: "@radix-ui/react-popover@npm:1.0.7"
+ dependencies:
+ "@babel/runtime": ^7.13.10
+ "@radix-ui/primitive": 1.0.1
+ "@radix-ui/react-compose-refs": 1.0.1
+ "@radix-ui/react-context": 1.0.1
+ "@radix-ui/react-dismissable-layer": 1.0.5
+ "@radix-ui/react-focus-guards": 1.0.1
+ "@radix-ui/react-focus-scope": 1.0.4
+ "@radix-ui/react-id": 1.0.1
+ "@radix-ui/react-popper": 1.1.3
+ "@radix-ui/react-portal": 1.0.4
+ "@radix-ui/react-presence": 1.0.1
+ "@radix-ui/react-primitive": 1.0.3
+ "@radix-ui/react-slot": 1.0.2
+ "@radix-ui/react-use-controllable-state": 1.0.1
+ aria-hidden: ^1.1.1
+ react-remove-scroll: 2.5.5
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 3ec15c0923ea457f586aa34f77e17fabffa02dffeab622951560ec21c38df2f43718ff088d24bf9fd1d9cd0db62436fc19cae5b122d90f72de4945a1f508dc59
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-popper@npm:1.1.3":
version: 1.1.3
resolution: "@radix-ui/react-popper@npm:1.1.3"
@@ -606,6 +762,63 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/react-radio-group@npm:1.1.3":
+ version: 1.1.3
+ resolution: "@radix-ui/react-radio-group@npm:1.1.3"
+ dependencies:
+ "@babel/runtime": ^7.13.10
+ "@radix-ui/primitive": 1.0.1
+ "@radix-ui/react-compose-refs": 1.0.1
+ "@radix-ui/react-context": 1.0.1
+ "@radix-ui/react-direction": 1.0.1
+ "@radix-ui/react-presence": 1.0.1
+ "@radix-ui/react-primitive": 1.0.3
+ "@radix-ui/react-roving-focus": 1.0.4
+ "@radix-ui/react-use-controllable-state": 1.0.1
+ "@radix-ui/react-use-previous": 1.0.1
+ "@radix-ui/react-use-size": 1.0.1
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 88f7007610817ab30f471a7e1f6605e94cc507a31fb4bb218116d65cc48c9b3149fce500f386716a3ed5fb0089d65faf32d3e01971322cd4a14b51003ec82bc2
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-roving-focus@npm:1.0.4":
+ version: 1.0.4
+ resolution: "@radix-ui/react-roving-focus@npm:1.0.4"
+ dependencies:
+ "@babel/runtime": ^7.13.10
+ "@radix-ui/primitive": 1.0.1
+ "@radix-ui/react-collection": 1.0.3
+ "@radix-ui/react-compose-refs": 1.0.1
+ "@radix-ui/react-context": 1.0.1
+ "@radix-ui/react-direction": 1.0.1
+ "@radix-ui/react-id": 1.0.1
+ "@radix-ui/react-primitive": 1.0.3
+ "@radix-ui/react-use-callback-ref": 1.0.1
+ "@radix-ui/react-use-controllable-state": 1.0.1
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 69b1c82c2d9db3ba71549a848f2704200dab1b2cd22d050c1e081a78b9a567dbfdc7fd0403ee010c19b79652de69924d8ca2076cd031d6552901e4213493ffc7
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-slot@npm:1.0.2":
version: 1.0.2
resolution: "@radix-ui/react-slot@npm:1.0.2"
@@ -715,6 +928,21 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/react-use-previous@npm:1.0.1":
+ version: 1.0.1
+ resolution: "@radix-ui/react-use-previous@npm:1.0.1"
+ dependencies:
+ "@babel/runtime": ^7.13.10
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 66b4312e857c58b75f3bf62a2048ef090b79a159e9da06c19a468c93e62336969c33dbef60ff16969f00b20386cc25d138f6a353f1658b35baac0a6eff4761b9
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-use-rect@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-use-rect@npm:1.0.1"
@@ -776,6 +1004,13 @@ __metadata:
languageName: node
linkType: hard
+"@recoiljs/refine@npm:^0.1.1":
+ version: 0.1.1
+ resolution: "@recoiljs/refine@npm:0.1.1"
+ checksum: 63b3e468f4ce7417cd13470f39a85a7318b902d411121759fc0a7ee427cd8882328a2c999ef0913e086f0c99f732e5478378b4ebe514bd811bb6e96333c747c3
+ languageName: node
+ linkType: hard
+
"@rushstack/eslint-patch@npm:^1.3.3":
version: 1.5.0
resolution: "@rushstack/eslint-patch@npm:1.5.0"
@@ -1335,6 +1570,15 @@ __metadata:
languageName: node
linkType: hard
+"aria-hidden@npm:^1.1.1":
+ version: 1.2.3
+ resolution: "aria-hidden@npm:1.2.3"
+ dependencies:
+ tslib: ^2.0.0
+ checksum: 7d7d211629eef315e94ed3b064c6823d13617e609d3f9afab1c2ed86399bb8e90405f9bdd358a85506802766f3ecb468af985c67c846045a34b973bcc0289db9
+ languageName: node
+ linkType: hard
+
"aria-query@npm:^5.1.3":
version: 5.3.0
resolution: "aria-query@npm:5.3.0"
@@ -1733,6 +1977,10 @@ __metadata:
resolution: "ccsa-client@workspace:."
dependencies:
"@playwright/test": 1.38.1
+ "@radix-ui/react-checkbox": 1.0.4
+ "@radix-ui/react-label": 2.0.2
+ "@radix-ui/react-popover": 1.0.7
+ "@radix-ui/react-radio-group": 1.1.3
"@radix-ui/react-tooltip": 1.0.7
"@t3-oss/env-nextjs": 0.6.1
"@tanstack/eslint-plugin-query": 4.34.1
@@ -1765,6 +2013,8 @@ __metadata:
react-dom: 18.2.0
react-icons: 4.11.0
react-map-gl: 7.1.6
+ recoil: 0.7.7
+ recoil-sync: 0.2.0
rooks: 7.14.1
tailwind-merge: 1.14.0
tailwindcss: 3.3.3
@@ -2087,6 +2337,13 @@ __metadata:
languageName: node
linkType: hard
+"detect-node-es@npm:^1.1.0":
+ version: 1.1.0
+ resolution: "detect-node-es@npm:1.1.0"
+ checksum: e46307d7264644975b71c104b9f028ed1d3d34b83a15b8a22373640ce5ea630e5640b1078b8ea15f202b54641da71e4aa7597093bd4b91f113db520a26a37449
+ languageName: node
+ linkType: hard
+
"didyoumean@npm:^1.2.2":
version: 1.2.2
resolution: "didyoumean@npm:1.2.2"
@@ -3052,6 +3309,13 @@ __metadata:
languageName: node
linkType: hard
+"get-nonce@npm:^1.0.0":
+ version: 1.0.1
+ resolution: "get-nonce@npm:1.0.1"
+ checksum: e2614e43b4694c78277bb61b0f04583d45786881289285c73770b07ded246a98be7e1f78b940c80cbe6f2b07f55f0b724e6db6fd6f1bcbd1e8bdac16521074ed
+ languageName: node
+ linkType: hard
+
"get-stream@npm:^6.0.0, get-stream@npm:^6.0.1":
version: 6.0.1
resolution: "get-stream@npm:6.0.1"
@@ -3249,6 +3513,13 @@ __metadata:
languageName: node
linkType: hard
+"hamt_plus@npm:1.0.2":
+ version: 1.0.2
+ resolution: "hamt_plus@npm:1.0.2"
+ checksum: af26ea32db03009019cc83dfa9411521a2fa16079443de1a502c9be46d8b3c975acda8ed93fc5750ef08d3186d35901e2d8cfe717dd54bea67b358601fa74e4c
+ languageName: node
+ linkType: hard
+
"has-bigints@npm:^1.0.1, has-bigints@npm:^1.0.2":
version: 1.0.2
resolution: "has-bigints@npm:1.0.2"
@@ -3478,6 +3749,15 @@ __metadata:
languageName: node
linkType: hard
+"invariant@npm:^2.2.4":
+ version: 2.2.4
+ resolution: "invariant@npm:2.2.4"
+ dependencies:
+ loose-envify: ^1.0.0
+ checksum: cc3182d793aad82a8d1f0af697b462939cb46066ec48bbf1707c150ad5fad6406137e91a262022c269702e01621f35ef60269f6c0d7fd178487959809acdfb14
+ languageName: node
+ linkType: hard
+
"ip@npm:^2.0.0":
version: 2.0.0
resolution: "ip@npm:2.0.0"
@@ -4017,7 +4297,7 @@ __metadata:
languageName: node
linkType: hard
-"loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0":
+"loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0":
version: 1.4.0
resolution: "loose-envify@npm:1.4.0"
dependencies:
@@ -5279,6 +5559,58 @@ __metadata:
languageName: node
linkType: hard
+"react-remove-scroll-bar@npm:^2.3.3":
+ version: 2.3.4
+ resolution: "react-remove-scroll-bar@npm:2.3.4"
+ dependencies:
+ react-style-singleton: ^2.2.1
+ tslib: ^2.0.0
+ peerDependencies:
+ "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: b5ce5f2f98d65c97a3e975823ae4043a4ba2a3b63b5ba284b887e7853f051b5cd6afb74abde6d57b421931c52f2e1fdbb625dc858b1cb5a32c27c14ab85649d4
+ languageName: node
+ linkType: hard
+
+"react-remove-scroll@npm:2.5.5":
+ version: 2.5.5
+ resolution: "react-remove-scroll@npm:2.5.5"
+ dependencies:
+ react-remove-scroll-bar: ^2.3.3
+ react-style-singleton: ^2.2.1
+ tslib: ^2.1.0
+ use-callback-ref: ^1.3.0
+ use-sidecar: ^1.1.2
+ peerDependencies:
+ "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 2c7fe9cbd766f5e54beb4bec2e2efb2de3583037b23fef8fa511ab426ed7f1ae992382db5acd8ab5bfb030a4b93a06a2ebca41377d6eeaf0e6791bb0a59616a4
+ languageName: node
+ linkType: hard
+
+"react-style-singleton@npm:^2.2.1":
+ version: 2.2.1
+ resolution: "react-style-singleton@npm:2.2.1"
+ dependencies:
+ get-nonce: ^1.0.0
+ invariant: ^2.2.4
+ tslib: ^2.0.0
+ peerDependencies:
+ "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 7ee8ef3aab74c7ae1d70ff34a27643d11ba1a8d62d072c767827d9ff9a520905223e567002e0bf6c772929d8ea1c781a3ba0cc4a563e92b1e3dc2eaa817ecbe8
+ languageName: node
+ linkType: hard
+
"react@npm:18.2.0":
version: 18.2.0
resolution: "react@npm:18.2.0"
@@ -5337,6 +5669,34 @@ __metadata:
languageName: node
linkType: hard
+"recoil-sync@npm:0.2.0":
+ version: 0.2.0
+ resolution: "recoil-sync@npm:0.2.0"
+ dependencies:
+ "@recoiljs/refine": ^0.1.1
+ transit-js: ^0.8.874
+ peerDependencies:
+ recoil: ">=0.7.3"
+ checksum: a0bd98acbc92ae58099a283056cb0c3cfcc08312bbe8bccd2662ecb3410b856fe3db33df765c7f5e9f030f73c44630e2c0f39266123eb1fcb7285d0f4b53c52a
+ languageName: node
+ linkType: hard
+
+"recoil@npm:0.7.7":
+ version: 0.7.7
+ resolution: "recoil@npm:0.7.7"
+ dependencies:
+ hamt_plus: 1.0.2
+ peerDependencies:
+ react: ">=16.13.1"
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+ react-native:
+ optional: true
+ checksum: 65edecbcb8d2cde89bfd61ec679c200483472a6cd343c33e4e9142b6ce524fb17d1fecc2bfd8c392926aaa8178c81457f165b32abce2a9662f51f98822c0a9cc
+ languageName: node
+ linkType: hard
+
"reflect.getprototypeof@npm:^1.0.4":
version: 1.0.4
resolution: "reflect.getprototypeof@npm:1.0.4"
@@ -6148,6 +6508,13 @@ __metadata:
languageName: node
linkType: hard
+"transit-js@npm:^0.8.874":
+ version: 0.8.874
+ resolution: "transit-js@npm:0.8.874"
+ checksum: a1d3a78a0ce926320ba32cdd59a74104e1b440855497753b91f8e71831302b23adfb21416cdba30153305cf41cf96b75421a607a1799b676572fe6072ee04798
+ languageName: node
+ linkType: hard
+
"ts-api-utils@npm:^1.0.1":
version: 1.0.3
resolution: "ts-api-utils@npm:1.0.3"
@@ -6176,7 +6543,7 @@ __metadata:
languageName: node
linkType: hard
-"tslib@npm:^2.4.0, tslib@npm:^2.5.0, tslib@npm:^2.6.0":
+"tslib@npm:^2.0.0, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.5.0, tslib@npm:^2.6.0":
version: 2.6.2
resolution: "tslib@npm:2.6.2"
checksum: 329ea56123005922f39642318e3d1f0f8265d1e7fcb92c633e0809521da75eeaca28d2cf96d7248229deb40e5c19adf408259f4b9640afd20d13aecc1430f3ad
@@ -6371,6 +6738,37 @@ __metadata:
languageName: node
linkType: hard
+"use-callback-ref@npm:^1.3.0":
+ version: 1.3.0
+ resolution: "use-callback-ref@npm:1.3.0"
+ dependencies:
+ tslib: ^2.0.0
+ peerDependencies:
+ "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 7913df383a5a6fcb399212eedefaac2e0c6f843555202d4e3010bac3848afe38ecaa3d0d6500ad1d936fbeffd637e6c517e68edb024af5e6beca7f27f3ce7b21
+ languageName: node
+ linkType: hard
+
+"use-sidecar@npm:^1.1.2":
+ version: 1.1.2
+ resolution: "use-sidecar@npm:1.1.2"
+ dependencies:
+ detect-node-es: ^1.1.0
+ tslib: ^2.0.0
+ peerDependencies:
+ "@types/react": ^16.9.0 || ^17.0.0 || ^18.0.0
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 925d1922f9853e516eaad526b6fed1be38008073067274f0ecc3f56b17bb8ab63480140dd7c271f94150027c996cea4efe83d3e3525e8f3eda22055f6a39220b
+ languageName: node
+ linkType: hard
+
"use-sync-external-store@npm:^1.2.0":
version: 1.2.0
resolution: "use-sync-external-store@npm:1.2.0"