diff --git a/apps/i15-1/src/router.tsx b/apps/i15-1/src/router.tsx
index ceb370cb..9000e93f 100644
--- a/apps/i15-1/src/router.tsx
+++ b/apps/i15-1/src/router.tsx
@@ -9,8 +9,8 @@ import {
import Robot from "./routes/Robot";
import Dashboard from "./routes/Dashboard";
import Playlist from "./routes/Playlist";
-import Plans from "./routes/Plans";
import { QueueView } from "./routes/QueueView";
+import { PlanBrowser } from "@atlas/blueapi-ui";
const navigation: SectionGroup[] = [
{
@@ -64,7 +64,7 @@ const navigation: SectionGroup[] = [
},
{
name: "Plans",
- element: ,
+ element: ,
},
],
},
diff --git a/apps/i15-1/src/routes/Plans.tsx b/apps/i15-1/src/routes/Plans.tsx
deleted file mode 100644
index 1ba9ec6b..00000000
--- a/apps/i15-1/src/routes/Plans.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { PlanBrowser } from "@atlas/blueapi-ui";
-import { PlanParameters } from "@atlas/blueapi-ui";
-import { usePlans } from "@atlas/blueapi-query";
-
-function JsonFormsPlans() {
- const { data } = usePlans();
-
- return (
- }
- />
- );
-}
-
-export default JsonFormsPlans;
diff --git a/apps/visr/src/routes/Plans.tsx b/apps/visr/src/routes/Plans.tsx
index 0bbe264f..765e3fb0 100644
--- a/apps/visr/src/routes/Plans.tsx
+++ b/apps/visr/src/routes/Plans.tsx
@@ -1,16 +1,7 @@
import { PlanBrowser } from "@atlas/blueapi-ui";
-import { PlanParameters } from "@atlas/blueapi-ui";
-import { usePlans } from "@atlas/blueapi-query";
function JsonFormsPlans() {
- const { data } = usePlans();
-
- return (
- }
- />
- );
+ return ;
}
export default JsonFormsPlans;
diff --git a/packages/blueapi-ui/package.json b/packages/blueapi-ui/package.json
index deea929e..cba77235 100644
--- a/packages/blueapi-ui/package.json
+++ b/packages/blueapi-ui/package.json
@@ -17,6 +17,7 @@
"@jsonforms/material-renderers": "3.6.0",
"@jsonforms/react": "3.6.0",
"@tanstack/react-query": "^5.90.21",
+ "lucide-react": "^1.24.0",
"react-error-boundary": "^6.0.0"
},
"devDependencies": {
diff --git a/packages/blueapi-ui/src/PlanBrowser.test.tsx b/packages/blueapi-ui/src/PlanBrowser.test.tsx
deleted file mode 100644
index 0b0ea64d..00000000
--- a/packages/blueapi-ui/src/PlanBrowser.test.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-import type { Plan } from "@atlas/blueapi";
-import { render, screen, userEvent } from "@atlas/vitest-conf";
-import { PlanBrowser } from "./PlanBrowser";
-
-const plans: Plan[] = [
- { name: "Plan 1", schema: {}, description: "" },
- { name: "Plan 2", schema: {}, description: "" },
- { name: "Plan 3", schema: {}, description: "" },
-];
-
-const renderPlan = (plan: Plan) => (
- {plan.name}
-);
-
-function renderBrowser() {
- return render();
-}
-
-describe("PlanBrowser", () => {
- it("shows a placeholder before initial plan selection", () => {
- renderBrowser();
-
- expect(screen.getByText("Select a plan")).toBeInTheDocument();
- expect(
- screen.getByText("Choose from the list on the left to see details."),
- ).toBeInTheDocument();
- });
-
- it("does not invoke renderPlan before selection", () => {
- const mockRender = vi.fn();
- render();
- expect(mockRender).not.toBeCalled();
- });
-
- it("renders plan details when a plan is selected", async () => {
- renderBrowser();
-
- const selectedPlan = screen.getByRole("button", { name: "Plan 2" });
- const user = userEvent.setup();
- await user.click(selectedPlan);
-
- // placeholder disappears...
- expect(screen.queryByText("Select a plan")).not.toBeInTheDocument();
-
- // ...plan details appear
- const planDetails = screen.getByTestId("plan-view");
- expect(planDetails).toBeInTheDocument();
- expect(planDetails).toHaveTextContent("Plan 2");
- });
-
- it("renders plan details with every selection", async () => {
- renderBrowser();
-
- const user = userEvent.setup();
- await user.click(screen.getByRole("button", { name: "Plan 3" }));
-
- const planDetails = screen.getByTestId("plan-view");
- expect(planDetails).toBeInTheDocument();
- expect(planDetails).toHaveTextContent("Plan 3");
-
- await user.click(screen.getByRole("button", { name: "Plan 1" }));
- expect(planDetails).toHaveTextContent("Plan 1");
- });
-
- it("persists plan details through search/filtering", async () => {
- renderBrowser();
- const user = userEvent.setup();
-
- // select plan 1
- await user.click(screen.getByRole("button", { name: "Plan 1" }));
-
- // plan 1 details appear
- const planDetails = screen.getByTestId("plan-view");
- expect(planDetails).toHaveTextContent("Plan 1");
-
- // search for a different plan
- const searchbox = screen.getByRole("textbox", { name: /search plans/i });
- await user.type(searchbox, "Plan 3");
-
- // but user has not selected it, so plan 1 details remain
- expect(planDetails).toHaveTextContent("Plan 1");
- });
-});
diff --git a/packages/blueapi-ui/src/PlanBrowser.tsx b/packages/blueapi-ui/src/PlanBrowser.tsx
deleted file mode 100644
index 4d051d9b..00000000
--- a/packages/blueapi-ui/src/PlanBrowser.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import { useState, type ReactNode } from "react";
-import type { Plan } from "@atlas/blueapi";
-import {
- Box,
- Container,
- Grid2 as Grid,
- Paper,
- Typography,
-} from "@mui/material";
-import SearchablePlanList from "./SearchablePlanList";
-
-export type PlanBrowserProps = {
- plans: Plan[];
- renderPlan: (plan: Plan) => ReactNode;
-};
-
-export function PlanBrowser({ plans, renderPlan }: PlanBrowserProps) {
- const [selectedPlan, setSelectedPlan] = useState(null);
-
- return (
-
-
-
-
-
-
-
-
-
- {selectedPlan ? (
-
- {renderPlan(selectedPlan)}
-
- ) : (
-
-
- Select a plan
-
-
- Choose from the list on the left to see details.
-
-
- )}
-
-
-
-
- );
-}
diff --git a/packages/blueapi-ui/src/PlanBrowser/PlanBrowser.test.tsx b/packages/blueapi-ui/src/PlanBrowser/PlanBrowser.test.tsx
new file mode 100644
index 00000000..c905b997
--- /dev/null
+++ b/packages/blueapi-ui/src/PlanBrowser/PlanBrowser.test.tsx
@@ -0,0 +1,135 @@
+import type { Plan, PlansResponse } from "@atlas/blueapi";
+import { render, screen, userEvent } from "@atlas/vitest-conf";
+import { PlanBrowser } from "./PlanBrowser";
+import { usePlans } from "@atlas/blueapi-query";
+import { PlanParameters } from "./PlanParameters";
+import { useMediaQuery } from "@mui/material";
+
+// mock usePlans hook
+vi.mock("@atlas/blueapi-query");
+const usePlansMock = vi.mocked(usePlans);
+const plansResponse: PlansResponse = {
+ plans: [
+ { name: "Plan 1", schema: {}, description: "" },
+ { name: "Plan 2", schema: {}, description: "" },
+ { name: "Plan 3", schema: {}, description: "" },
+ ],
+};
+usePlansMock.mockReturnValue({ data: plansResponse } as any);
+
+// mock JSONForms
+const renderPlan = ({ plan }: { plan: Plan }) => (
+ {plan.name}
+);
+
+vi.mock("./PlanParameters");
+const paramsComponentMock = vi.mocked(PlanParameters);
+paramsComponentMock.mockImplementation(renderPlan);
+
+// render component under test
+function renderBrowser() {
+ return render();
+}
+
+vi.mock("@mui/material", async () => {
+ const actual =
+ await vi.importActual("@mui/material");
+
+ return {
+ ...actual,
+ useMediaQuery: vi.fn(),
+ };
+});
+
+const mockedUseMediaQuery = vi.mocked(useMediaQuery);
+
+describe("PlanBrowser", () => {
+ describe("Full layout", () => {
+ beforeEach(() => {
+ mockedUseMediaQuery.mockReturnValue(true);
+ });
+
+ it("shows a placeholder before initial plan selection and list of plans", () => {
+ renderBrowser();
+
+ expect(screen.getByText("Select a plan")).toBeInTheDocument();
+ expect(
+ screen.getByText("Choose from the list to see details."),
+ ).toBeInTheDocument();
+ });
+
+ it("renders plan details when a plan is selected", async () => {
+ renderBrowser();
+
+ const selectedPlan = screen.getByRole("button", { name: "Plan 2" });
+ const user = userEvent.setup();
+ await user.click(selectedPlan);
+
+ // placeholder disappears...
+ expect(screen.queryByText("Select a plan")).not.toBeInTheDocument();
+
+ // ...plan details appear
+ const planDetails = screen.getByTestId("plan-view");
+ expect(planDetails).toBeInTheDocument();
+ expect(planDetails).toHaveTextContent("Plan 2");
+ });
+
+ it("renders plan details with every selection", async () => {
+ renderBrowser();
+
+ const user = userEvent.setup();
+ await user.click(screen.getByRole("button", { name: "Plan 3" }));
+
+ const planDetails = screen.getByTestId("plan-view");
+ expect(planDetails).toBeInTheDocument();
+ expect(planDetails).toHaveTextContent("Plan 3");
+
+ await user.click(screen.getByRole("button", { name: "Plan 1" }));
+ expect(planDetails).toHaveTextContent("Plan 1");
+ });
+
+ it("persists plan details through search/filtering", async () => {
+ renderBrowser();
+ const user = userEvent.setup();
+
+ // select plan 1
+ await user.click(screen.getByRole("button", { name: "Plan 1" }));
+
+ // plan 1 details appear
+ const planDetails = screen.getByTestId("plan-view");
+ expect(planDetails).toHaveTextContent("Plan 1");
+
+ // search for a different plan
+ const searchbox = screen.getByRole("textbox", { name: /search plans/i });
+ await user.type(searchbox, "Plan 3");
+
+ // but user has not selected it, so plan 1 details remain
+ expect(planDetails).toHaveTextContent("Plan 1");
+ });
+ });
+
+ describe("Compact layout", () => {
+ beforeEach(() => {
+ mockedUseMediaQuery.mockReturnValue(false);
+ });
+
+ it("shows a placeholder before initial plan selection and 'View plans' button", () => {
+ renderBrowser();
+
+ expect(screen.getByText("View plans")).toBeInTheDocument();
+ expect(
+ screen.getByText("Choose from the list to see details."),
+ ).toBeInTheDocument();
+ });
+
+ it("opens drawer with plans list when 'View plans' button is clicked", async () => {
+ renderBrowser();
+ const user = userEvent.setup();
+ const button = screen.getByRole("button", { name: "View plans" });
+ await user.click(button);
+ plansResponse.plans.forEach(plan =>
+ expect(screen.getByText(plan.name)).toBeVisible(),
+ );
+ });
+ });
+});
diff --git a/packages/blueapi-ui/src/PlanBrowser/PlanBrowser.tsx b/packages/blueapi-ui/src/PlanBrowser/PlanBrowser.tsx
new file mode 100644
index 00000000..87404eee
--- /dev/null
+++ b/packages/blueapi-ui/src/PlanBrowser/PlanBrowser.tsx
@@ -0,0 +1,150 @@
+import { useState } from "react";
+import type { Plan } from "@atlas/blueapi";
+import {
+ Box,
+ Button,
+ Drawer,
+ Grid2 as Grid,
+ Paper,
+ Stack,
+ Toolbar,
+ Typography,
+ useMediaQuery,
+ useTheme,
+} from "@mui/material";
+import SearchablePlanList from "./SearchablePlanList";
+import { usePlans } from "@atlas/blueapi-query";
+import { PlanParameters } from "./PlanParameters";
+import { LucideArrowRight } from "lucide-react";
+
+type Props = {
+ plans: Plan[];
+ selected: Plan | null;
+ select: (plan: Plan) => void;
+};
+
+function CompactLayout({ plans, selected, select }: Props) {
+ const [open, setOpen] = useState(false);
+ return (
+
+ setOpen(false)}>
+
+
+
+
+
+
+
+
+ }
+ onClick={() => setOpen(true)}
+ >
+ View plans
+
+
+
+
+
+
+ );
+}
+
+function PlanParametersWrapper({ selected }: { selected: Plan | null }) {
+ return (
+
+ {selected ? (
+
+
+
+ ) : (
+
+
+ Select a plan
+
+
+ Choose from the list to see details.
+
+
+ )}
+
+ );
+}
+
+function FullLayout({ plans, selected, select }: Props) {
+ return (
+
+
+
+
+
+ a.name.localeCompare(b.name, undefined, {
+ sensitivity: "base",
+ }),
+ )}
+ selectedPlan={selected}
+ updateSelection={select}
+ />
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export function PlanBrowser() {
+ const [selectedPlan, setSelectedPlan] = useState(null);
+
+ const { data } = usePlans();
+ const plans = data
+ ? data.plans.sort((a, b) =>
+ a.name.localeCompare(b.name, undefined, {
+ sensitivity: "base",
+ }),
+ )
+ : [];
+
+ const theme = useTheme();
+ const bigEnoughForFullLayout = useMediaQuery(theme.breakpoints.up("md"));
+
+ if (bigEnoughForFullLayout) {
+ return (
+
+ );
+ }
+ return (
+
+ );
+}
diff --git a/packages/blueapi-ui/src/PlanParameters.test.tsx b/packages/blueapi-ui/src/PlanBrowser/PlanParameters.test.tsx
similarity index 97%
rename from packages/blueapi-ui/src/PlanParameters.test.tsx
rename to packages/blueapi-ui/src/PlanBrowser/PlanParameters.test.tsx
index 571adde5..8dd2c1ea 100644
--- a/packages/blueapi-ui/src/PlanParameters.test.tsx
+++ b/packages/blueapi-ui/src/PlanBrowser/PlanParameters.test.tsx
@@ -17,7 +17,7 @@ vi.mock("@jsonforms/react", () => {
export function RunPlanButton() {
return ;
}
-vi.mock("./RunPlanButton", () => ({
+vi.mock("../RunPlanButton", () => ({
RunPlanButton: RunPlanButton,
}));
diff --git a/packages/blueapi-ui/src/PlanParameters.tsx b/packages/blueapi-ui/src/PlanBrowser/PlanParameters.tsx
similarity index 70%
rename from packages/blueapi-ui/src/PlanParameters.tsx
rename to packages/blueapi-ui/src/PlanBrowser/PlanParameters.tsx
index 3a34fdd9..a143f22d 100644
--- a/packages/blueapi-ui/src/PlanParameters.tsx
+++ b/packages/blueapi-ui/src/PlanBrowser/PlanParameters.tsx
@@ -5,9 +5,9 @@ import {
materialRenderers,
materialCells,
} from "@jsonforms/material-renderers";
-import { sanitisePlan, type SchemaNode } from "./utils/schema";
+import { sanitisePlan, type SchemaNode } from "../utils/schema";
import type { Plan } from "@atlas/blueapi";
-import { RunPlanButton } from "./RunPlanButton";
+import { RunPlanButton } from "../RunPlanButton";
import { ErrorBoundary } from "react-error-boundary";
@@ -25,18 +25,12 @@ function UIFallback() {
);
}
-type PlanParametersProps = {
- plan: Plan;
-};
-
interface PlansParameters {
[key: string]: any;
}
-export const PlanParameters: React.FC = (
- props: PlanParametersProps,
-) => {
- const plan = sanitisePlan(props.plan);
+export function PlanParameters({ plan }: { plan: Plan }) {
+ const sanitisedPlan = sanitisePlan(plan);
const [planParameters, setPlanParameters] = useState({});
// TODO: Remove InstrumentSession box and state, retrieve from context when submitting.
@@ -44,30 +38,30 @@ export const PlanParameters: React.FC = (
const [instrumentSession, setInstrumentSession] = useState("cm12345-1");
return (
-
+
- {props.plan.name}
+ {plan.name}
- {props.plan.description && (
+ {plan.description && (
- {props.plan.description}
+ {plan.description}
)}
- {(plan.schema as SchemaNode).skip ? (
+ {(sanitisedPlan.schema as SchemaNode).skip ? (
) : (
- setPlanParameters({ ...planParameters, [props.plan.name]: data })
+ setPlanParameters({ ...planParameters, [plan.name]: data })
}
/>
)}
@@ -79,16 +73,16 @@ export const PlanParameters: React.FC = (
id="instrumentSession"
label="Instrument Session"
defaultValue={instrumentSession}
- onChange={(e) => setInstrumentSession(e.target.value)}
+ onChange={e => setInstrumentSession(e.target.value)}
>
);
-};
+}
diff --git a/packages/blueapi-ui/src/SearchablePlanList.test.tsx b/packages/blueapi-ui/src/PlanBrowser/SearchablePlanList.test.tsx
similarity index 89%
rename from packages/blueapi-ui/src/SearchablePlanList.test.tsx
rename to packages/blueapi-ui/src/PlanBrowser/SearchablePlanList.test.tsx
index 6b019866..2251f46c 100644
--- a/packages/blueapi-ui/src/SearchablePlanList.test.tsx
+++ b/packages/blueapi-ui/src/PlanBrowser/SearchablePlanList.test.tsx
@@ -26,7 +26,7 @@ function renderList(
}
describe("SearchablePlanList", () => {
- it("renders search, heading, and all plans initially", () => {
+ it("renders search and all plans initially", () => {
renderList();
// search field
@@ -34,9 +34,6 @@ describe("SearchablePlanList", () => {
screen.getByRole("textbox", { name: /search plans/i }),
).toBeInTheDocument();
- // heading
- expect(screen.getByText("Plans")).toBeInTheDocument();
-
// all items
for (const p of plans) {
expect(screen.getByRole("button", { name: p.name })).toBeInTheDocument();
@@ -45,7 +42,7 @@ describe("SearchablePlanList", () => {
// no selection initially
for (const p of plans) {
expect(screen.getByRole("button", { name: p.name })).toHaveAttribute(
- "aria-selected",
+ "aria-current",
"false",
);
}
@@ -72,16 +69,16 @@ describe("SearchablePlanList", () => {
const selectedButton = screen.getByRole("button", { name: "Dark Current" });
// none selected initially
- expect(selectedButton).toHaveAttribute("aria-selected", "false");
+ expect(selectedButton).toHaveAttribute("aria-current", "false");
// re-render with selected plan
const selected = { name: "Dark Current", schema: {}, description: "" };
rerender();
- // selected plan marked so with aria-selected
- expect(selectedButton).toHaveAttribute("aria-selected", "true");
+ // selected plan marked so with aria-current
+ expect(selectedButton).toHaveAttribute("aria-current", "true");
// others remain unselected
expect(screen.getByRole("button", { name: "Align Beam" })).toHaveAttribute(
- "aria-selected",
+ "aria-current",
"false",
);
});
@@ -123,8 +120,6 @@ describe("SearchablePlanList", () => {
it("handles empty plans array", () => {
renderList({ plans: [] });
- // List is empty, but the heading and search are present
- expect(screen.getByText("Plans")).toBeInTheDocument();
expect(
screen.getByRole("textbox", { name: /search plans/i }),
).toBeInTheDocument();
diff --git a/packages/blueapi-ui/src/PlanBrowser/SearchablePlanList.tsx b/packages/blueapi-ui/src/PlanBrowser/SearchablePlanList.tsx
new file mode 100644
index 00000000..41f3a873
--- /dev/null
+++ b/packages/blueapi-ui/src/PlanBrowser/SearchablePlanList.tsx
@@ -0,0 +1,83 @@
+import {
+ Box,
+ List,
+ ListItemButton,
+ Paper,
+ TextField,
+ Typography,
+} from "@mui/material";
+import type { Plan } from "@atlas/blueapi";
+import { useMemo, useState } from "react";
+
+type Props = {
+ plans: Plan[];
+ selectedPlan: Plan | null;
+ updateSelection: (plan: Plan) => void;
+};
+export default function SearchablePlanList({
+ plans,
+ selectedPlan,
+ updateSelection,
+}: Props) {
+ const [query, setQuery] = useState("");
+
+ const matchingPlans = useMemo(() => {
+ const q = query.trim().toLowerCase();
+ if (!q) return plans;
+ return plans.filter(plan => plan.name.toLowerCase().includes(q));
+ }, [plans, query]);
+
+ return (
+
+
+ setQuery(e.target.value)}
+ />
+
+
+
+
+ {matchingPlans.map(plan => {
+ const selected = selectedPlan?.name === plan.name;
+ return (
+ updateSelection(plan)}
+ aria-label={plan.name}
+ aria-current={selected}
+ >
+ {plan.name}
+
+ );
+ })}
+ {matchingPlans.length === 0 && plans.length > 0 && (
+
+
+ No plans match “{query}”.
+
+
+ )}
+
+
+
+
+ );
+}
diff --git a/packages/blueapi-ui/src/SearchablePlanList.tsx b/packages/blueapi-ui/src/SearchablePlanList.tsx
deleted file mode 100644
index b043321f..00000000
--- a/packages/blueapi-ui/src/SearchablePlanList.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import {
- Box,
- List,
- ListItemButton,
- TextField,
- Typography,
-} from "@mui/material";
-import type { Plan } from "@atlas/blueapi";
-import { useMemo, useState } from "react";
-
-type Props = {
- plans: Plan[];
- selectedPlan: Plan | null;
- updateSelection: (plan: Plan) => void;
-};
-export default function SearchablePlanList({
- plans,
- selectedPlan,
- updateSelection,
-}: Props) {
- const [query, setQuery] = useState("");
-
- const matchingPlans = useMemo(() => {
- const q = query.trim().toLowerCase();
- if (!q) return plans;
- return plans.filter((plan) => plan.name.toLowerCase().includes(q));
- }, [plans, query]);
-
- return (
- <>
-
-
- Plans
-
-
- Run or queue predefined acquisition plans for this beamline.
-
-
-
-
- setQuery(e.target.value)}
- />
-
- {/* */}
- {matchingPlans.map((plan) => {
- const selected = selectedPlan?.name === plan.name;
- return (
- updateSelection(plan)}
- >
- {plan.name}
-
- );
- })}
- {matchingPlans.length === 0 && plans.length > 0 && (
-
-
- No plans match “{query}”.
-
-
- )}
-
- >
- );
-}
diff --git a/packages/blueapi-ui/src/index.tsx b/packages/blueapi-ui/src/index.tsx
index c6fa52fe..e6c18af7 100644
--- a/packages/blueapi-ui/src/index.tsx
+++ b/packages/blueapi-ui/src/index.tsx
@@ -1,3 +1,3 @@
-export * from "./PlanBrowser";
-export * from "./PlanParameters";
+export * from "./PlanBrowser/PlanBrowser";
+export * from "./PlanBrowser/PlanParameters";
export * from "./RunPlanButton";
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 62702b06..fe5e19ac 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -71,7 +71,7 @@ importers:
version: link:../../packages/pvws-config
'@diamondlightsource/cs-web-lib':
specifier: 0.10.15
- version: 0.10.15(@mui/material@6.5.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.28)(graphql@16.14.2)(plotly.js@2.35.3(mapbox-gl@1.13.3)(webpack@5.106.2(postcss@8.5.15)))(react-dom@18.3.1(react@18.3.1))(react-gauge-component@2.0.29(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-redux@7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-router@7.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
+ version: 0.10.15(@mui/material@6.5.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.28)(graphql@16.14.2)(plotly.js@2.35.3(mapbox-gl@1.13.3)(webpack@5.106.2))(react-dom@18.3.1(react@18.3.1))(react-gauge-component@2.0.29(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-redux@7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-router@7.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
'@diamondlightsource/sci-react-ui':
specifier: ^0.4.0
version: 0.4.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@jsonforms/core@3.6.0)(@jsonforms/material-renderers@3.6.0(a2958437c922d885fd4a4f8b5e701907))(@jsonforms/react@3.6.0(@jsonforms/core@3.6.0)(react@18.3.1))(@mui/icons-material@6.5.0(@mui/material@6.5.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@mui/material@6.5.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
@@ -411,6 +411,9 @@ importers:
'@tanstack/react-query':
specifier: ^5.90.21
version: 5.90.21(react@18.3.1)
+ lucide-react:
+ specifier: ^1.24.0
+ version: 1.24.0(react@18.3.1)
react-error-boundary:
specifier: ^6.0.0
version: 6.1.1(react@18.3.1)
@@ -429,7 +432,7 @@ importers:
dependencies:
'@diamondlightsource/cs-web-lib':
specifier: 0.10.15
- version: 0.10.15(@mui/material@6.5.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.28)(plotly.js@2.35.3(mapbox-gl@1.13.3)(webpack@5.106.2))(react-dom@18.3.1(react@18.3.1))(react-gauge-component@2.0.29(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-redux@7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-router@7.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
+ version: 0.10.15(@mui/material@6.5.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.28)(graphql@16.14.2)(plotly.js@2.35.3(mapbox-gl@1.13.3)(webpack@5.106.2(postcss@8.5.15)))(react-dom@18.3.1(react@18.3.1))(react-gauge-component@2.0.29(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-redux@7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-router@7.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
'@mui/material':
specifier: <7.0.0
version: 6.5.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -4362,6 +4365,11 @@ packages:
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ lucide-react@1.24.0:
+ resolution: {integrity: sha512-YT6mBD8lGKkg4nM39enlm94/sfJIiW0YKUT60fBy4YK8tai31ylg1VhGNWxkpSKHo9UagfnZqwIff3HTDQwXeA==}
+ peerDependencies:
+ react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
lz-string@1.5.0:
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
hasBin: true
@@ -6379,7 +6387,7 @@ snapshots:
- graphql
- plotly.js
- '@diamondlightsource/cs-web-lib@0.10.15(@mui/material@6.5.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.28)(plotly.js@2.35.3(mapbox-gl@1.13.3)(webpack@5.106.2))(react-dom@18.3.1(react@18.3.1))(react-gauge-component@2.0.29(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-redux@7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-router@7.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)':
+ '@diamondlightsource/cs-web-lib@0.10.15(@mui/material@6.5.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.28)(graphql@16.14.2)(plotly.js@2.35.3(mapbox-gl@1.13.3)(webpack@5.106.2))(react-dom@18.3.1(react@18.3.1))(react-gauge-component@2.0.29(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-redux@7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-router@7.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)':
dependencies:
'@mui/icons-material': 7.3.11(@mui/material@6.5.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.28)(react@18.3.1)
'@reduxjs/toolkit': 2.12.0(react-redux@7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
@@ -10507,6 +10515,10 @@ snapshots:
dependencies:
react: 18.3.1
+ lucide-react@1.24.0(react@18.3.1):
+ dependencies:
+ react: 18.3.1
+
lz-string@1.5.0: {}
maath@0.10.8(@types/three@0.164.1)(three@0.164.1):