Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement basic sidebar layout #60

Merged
merged 12 commits into from
Apr 5, 2024
2 changes: 1 addition & 1 deletion __tests__/main.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { expect, test } from "vitest";

import { PointCanvas } from "../src/lib/PointCanvas.ts";
import Scene from "../src/components/scene";
import Scene from "../src/components/Scene";
import React from "react";
import { render } from "@testing-library/react";

Expand Down
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/components/main.tsx"></script>
<script type="module" src="/src/components/Main.tsx"></script>
</body>
</html>
Binary file added public/zebrahub-favicon-60x60.png
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are the zebrahub icons if we want a bigger one. We probably also want to set this as our favicon unless we come up with a separate logo for this.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
120 changes: 89 additions & 31 deletions src/components/app.tsx → src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useState, useEffect } from "react";
import "@/css/app.css";

import { Box } from "@mui/material";
import { Box, Divider, Drawer } from "@mui/material";

import Scene from "@/components/scene.tsx";
import DataControls from "@/components/dataControls.tsx";
import PlaybackControls from "@/components/playbackControls.tsx";
import Scene from "@/components/Scene";
import DataControls from "@/components/DataControls";
import TrackControls from "@/components/TrackControls";
import PlaybackControls from "@/components/PlaybackControls";

import useSelectionBox from "@/hooks/useSelectionBox";

Expand All @@ -18,6 +19,8 @@ const initialViewerState = ViewerState.fromUrlHash(window.location.hash);
console.log("initial viewer state: %s", JSON.stringify(initialViewerState));
clearUrlHash();

const drawerWidth = 256;

export default function App() {
// Use references here for two things:
// * manage objects that should never change, even when the component re-renders
Expand Down Expand Up @@ -190,33 +193,88 @@ export default function App() {
}, [numTimes, curTime, playing]);

return (
<Box sx={{ display: "flex", flexDirection: "column", width: "80%", height: "100%" }}>
<DataControls
dataUrl={dataUrl}
initialDataUrl={initialViewerState.dataUrl}
trackManager={trackManager}
trackHighlightLength={trackHighlightLength}
setDataUrl={setDataUrl}
setTrackHighlightLength={setTrackHighlightLength}
copyShareableUrlToClipboard={copyShareableUrlToClipboard}
clearTracks={() => canvas?.removeAllTracks()}
/>
<Scene
setCanvas={setCanvas}
loading={loading}
initialCameraPosition={initialViewerState.cameraPosition}
initialCameraTarget={initialViewerState.cameraTarget}
/>
<PlaybackControls
enabled={true}
autoRotate={autoRotate}
playing={playing}
curTime={curTime}
numTimes={numTimes}
setAutoRotate={setAutoRotate}
setPlaying={setPlaying}
setCurTime={setCurTime}
/>
<Box sx={{ display: "flex", width: "100%", height: "100%" }}>
{/* TODO: components *could* go deeper still for organization */}
<Drawer
anchor="left"
variant="permanent"
sx={{
"width": drawerWidth,
"flexShrink": 0,
"& .MuiDrawer-paper": { width: drawerWidth, boxSizing: "border-box" },
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
width: "100%",
height: "100%",
}}
>
<Box
sx={{
flexGrow: 0,
padding: "1em 1.5em",
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
}}
>
<img src="/zebrahub-favicon-60x60.png" alt="logo" />
<Divider orientation="vertical" flexItem />
<h2>ZEBRAHUB</h2>
</Box>
<Box flexGrow={1} padding="2em">
<TrackControls
trackManager={trackManager}
trackHighlightLength={trackHighlightLength}
setTrackHighlightLength={setTrackHighlightLength}
clearTracks={() => canvas?.removeAllTracks()}
/>
</Box>
<Divider />
<Box flexGrow={0} padding="1em">
<DataControls
dataUrl={dataUrl}
initialDataUrl={initialViewerState.dataUrl}
setDataUrl={setDataUrl}
copyShareableUrlToClipboard={copyShareableUrlToClipboard}
trackManager={trackManager}
/>
</Box>
</Box>
</Drawer>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
height: "100%",
overflow: "hidden",
}}
>
<Scene
setCanvas={setCanvas}
loading={loading}
initialCameraPosition={initialViewerState.cameraPosition}
initialCameraTarget={initialViewerState.cameraTarget}
/>
<Box flexGrow={0} padding="1em">
<PlaybackControls
enabled={true}
autoRotate={autoRotate}
playing={playing}
curTime={curTime}
numTimes={numTimes}
setAutoRotate={setAutoRotate}
setPlaying={setPlaying}
setCurTime={setCurTime}
/>
</Box>
</Box>
</Box>
);
}
137 changes: 137 additions & 0 deletions src/components/DataControls.tsx
aganders3 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { useState } from "react";

import { Alert, Box, Popover, Snackbar, Stack, Typography } from "@mui/material";
import { styled } from "@mui/material/styles";
import { fontBodyXs } from "czifui";

import { Button, ButtonIcon, InputText } from "@czi-sds/components";

import { TrackManager } from "@/lib/TrackManager";

interface DataControlsProps {
dataUrl: string;
initialDataUrl: string;
setDataUrl: (dataUrl: string) => void;
copyShareableUrlToClipboard: () => void;
trackManager: TrackManager | null; // TODO: remove this?
}

export default function DataControls(props: DataControlsProps) {
const [copyUrlSnackBarOpen, setCopyUrlSnackBarOpen] = useState(false);
const [urlPopoverAnchor, setUrlPopoverAnchor] = useState<HTMLButtonElement | null>(null);

const copyShareableUrlToClipBoard = () => {
props.copyShareableUrlToClipboard();
setCopyUrlSnackBarOpen(true);
};

const handleShareableUrlSnackBarClose = () => {
setCopyUrlSnackBarOpen(false);
};

const showUrlPopover = (event: React.MouseEvent<HTMLButtonElement>) => {
setUrlPopoverAnchor(event.currentTarget);
};

const handleUrlPopoverClose = () => {
setUrlPopoverAnchor(null);
};

return (
<Box
sx={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
// 5px margin makes this bar match the PlaybackControls height
// because that component uses primary buttons, which hava a 5px margin
margin: "5px",
}}
>
{/* TODO: make this do something */}
<ButtonIcon
sdsIcon="infoCircle"
sdsSize="large"
sdsType="secondary"
onClick={() => {
window.alert("Not implemented :)");
}}
/>

<ButtonIcon
sdsIcon="share"
sdsSize="large"
sdsType="secondary"
disabled={!props.trackManager}
onClick={copyShareableUrlToClipBoard}
/>
<Snackbar
open={copyUrlSnackBarOpen}
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
autoHideDuration={2500}
onClose={handleShareableUrlSnackBarClose}
// This is a hack to make the snackbar appear above the bottom bar
sx={{
"&.MuiSnackbar-root": { bottom: "100px" },
}}
>
<Alert
// SDS alert does not work in here
severity="success"
variant="filled"
>
Shareable URL copied to clipboard!
</Alert>
</Snackbar>

<ButtonIcon sdsIcon="globeBasic" sdsSize="large" sdsType="secondary" onClick={showUrlPopover} />
<Popover
open={Boolean(urlPopoverAnchor)}
anchorEl={urlPopoverAnchor}
onClose={handleUrlPopoverClose}
anchorOrigin={{
vertical: "top",
horizontal: "right",
}}
transformOrigin={{
vertical: "bottom",
horizontal: "left",
}}
>
<Stack
spacing={4}
sx={{
"padding": "1em",
"width": "768px",
"& > label": { fontSize: "0.83em", fontWeight: "bold" },
}}
>
<InputText
id="url-input"
label="Zarr URL"
placeholder={props.initialDataUrl}
defaultValue={props.initialDataUrl}
onChange={(e) => props.setDataUrl(e.target.value)}
aganders3 marked this conversation as resolved.
Show resolved Hide resolved
fullWidth={true}
intent={props.trackManager ? "default" : "error"}
/>
<Note>
<strong>Note:</strong> Changing this URL will replace the image and reset the canvas.
</Note>
<Stack direction="row" spacing={4}>
<Button sdsStyle="square" sdsType="secondary" onClick={handleUrlPopoverClose}>
Cancel
</Button>
<Button sdsStyle="square" sdsType="primary">
Apply
</Button>
</Stack>
</Stack>
</Popover>
</Box>
);
}

const Note = styled(Typography)`
${fontBodyXs}
`;
2 changes: 1 addition & 1 deletion src/components/main.tsx → src/components/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ThemeProvider as EmotionThemeProvider } from "@emotion/react";
import { StyledEngineProvider, ThemeProvider } from "@mui/material/styles";
import { defaultTheme } from "@czi-sds/components";

import App from "@/components/app.tsx";
import App from "@/components/App.tsx";
import "@/css/index.css";

const domNode = document.getElementById("app")!;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Stack } from "@mui/material";

import { InputSlider, InputToggle } from "@czi-sds/components";
import { Box } from "@mui/material";
import { ButtonIcon, InputSlider } from "@czi-sds/components";

interface PlaybackControlsProps {
enabled: boolean;
Expand All @@ -15,22 +14,14 @@ interface PlaybackControlsProps {

export default function PlaybackControls(props: PlaybackControlsProps) {
return (
<Stack direction="row" spacing={8} sx={{ margin: "2em" }}>
<label htmlFor="auto-rotate-toggle">Auto Rotate</label>
<InputToggle
checked={props.autoRotate}
disabled={!props.enabled}
onChange={(e) => {
props.setAutoRotate((e.target as HTMLInputElement).checked);
}}
/>
<label htmlFor="playback-toggle">Playback</label>
<InputToggle
checked={props.playing}
<Box sx={{ display: "flex", flexDirection: "row", alignItems: "center", gap: "2em" }}>
<ButtonIcon
sdsIcon="play"
sdsSize="large"
sdsType="primary"
on={props.playing}
disabled={!props.enabled}
onChange={(e) => {
props.setPlaying((e.target as HTMLInputElement).checked);
}}
onClick={() => props.setPlaying(!props.playing)}
/>
<InputSlider
id="time-frame-slider"
Expand All @@ -42,6 +33,15 @@ export default function PlaybackControls(props: PlaybackControlsProps) {
onChange={(_, value) => props.setCurTime(value as number)}
value={props.curTime}
/>
</Stack>
{/* TODO: add control button groups - perhaps a separate component */}
<ButtonIcon
sdsIcon="dna"
sdsSize="large"
sdsType="primary"
on={props.autoRotate}
disabled={!props.enabled}
onClick={() => props.setAutoRotate(!props.autoRotate)}
/>
</Box>
);
}
File renamed without changes.
Loading
Loading