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)}> + + + + + + + + + + + + + + + ); +} + +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):