diff --git a/__tests__/main.test.tsx b/__tests__/main.test.tsx index d128ef50..72a5bba7 100644 --- a/__tests__/main.test.tsx +++ b/__tests__/main.test.tsx @@ -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"; diff --git a/index.html b/index.html index 9f2a2f58..7b6e6575 100644 --- a/index.html +++ b/index.html @@ -13,6 +13,6 @@
- + diff --git a/public/zebrahub-favicon-60x60.png b/public/zebrahub-favicon-60x60.png new file mode 100644 index 00000000..693cdc2d Binary files /dev/null and b/public/zebrahub-favicon-60x60.png differ diff --git a/src/components/app.tsx b/src/components/App.tsx similarity index 67% rename from src/components/app.tsx rename to src/components/App.tsx index ac5e5a8d..c9c3cd6b 100644 --- a/src/components/app.tsx +++ b/src/components/App.tsx @@ -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"; @@ -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 @@ -190,33 +193,88 @@ export default function App() { }, [numTimes, curTime, playing]); return ( - - canvas?.removeAllTracks()} - /> - - + + {/* TODO: components *could* go deeper still for organization */} + + + + logo + +

ZEBRAHUB

+
+ + canvas?.removeAllTracks()} + /> + + + + + +
+
+ + + + + +
); } diff --git a/src/components/DataControls.tsx b/src/components/DataControls.tsx new file mode 100644 index 00000000..210915de --- /dev/null +++ b/src/components/DataControls.tsx @@ -0,0 +1,154 @@ +import { useEffect, useState } from "react"; + +import { Alert, Box, Popover, Snackbar, Stack, Typography, styled } from "@mui/material"; + +import { Button, ButtonIcon, InputText, fontBodyXs } from "@czi-sds/components"; + +interface DataControlsProps { + dataUrl: string; + initialDataUrl: string; + setDataUrl: (dataUrl: string) => void; + copyShareableUrlToClipboard: () => void; + validTrackManager: boolean; +} + +export default function DataControls(props: DataControlsProps) { + const [copyUrlSnackBarOpen, setCopyUrlSnackBarOpen] = useState(false); + const [urlPopoverAnchor, setUrlPopoverAnchor] = useState(null); + + const copyShareableUrlToClipBoard = () => { + props.copyShareableUrlToClipboard(); + setCopyUrlSnackBarOpen(true); + }; + + const handleShareableUrlSnackBarClose = () => { + setCopyUrlSnackBarOpen(false); + }; + + const showUrlPopover = (event: React.MouseEvent) => { + setUrlPopoverAnchor(event.currentTarget); + }; + + const handleUrlPopoverClose = () => { + setUrlPopoverAnchor(null); + }; + + const handleDataUrlSubmit = (event: React.FormEvent) => { + event.preventDefault(); + const urlInput = document.getElementById("data-url-input") as HTMLInputElement; + if (urlInput) { + props.setDataUrl(urlInput.value); + } + }; + + // only close the popover if the URL gives a valid track manager + useEffect(() => { + if (props.validTrackManager && urlPopoverAnchor) { + setUrlPopoverAnchor(null); + } + }, [props.validTrackManager]); + + return ( + + {/* TODO: make this do something */} + { + window.alert("Not implemented :)"); + }} + /> + + + + + Shareable URL copied to clipboard! + + + + + +
+ + + + + Note: Changing this URL will replace the image and reset the canvas. + + + + + + +
+
+
+ ); +} + +const Note = styled(Typography)` + ${fontBodyXs} +`; diff --git a/src/components/main.tsx b/src/components/Main.tsx similarity index 94% rename from src/components/main.tsx rename to src/components/Main.tsx index c98f99d1..7163b788 100644 --- a/src/components/main.tsx +++ b/src/components/Main.tsx @@ -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")!; diff --git a/src/components/playbackControls.tsx b/src/components/PlaybackControls.tsx similarity index 54% rename from src/components/playbackControls.tsx rename to src/components/PlaybackControls.tsx index 88703be4..46dc7440 100644 --- a/src/components/playbackControls.tsx +++ b/src/components/PlaybackControls.tsx @@ -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; @@ -15,22 +14,14 @@ interface PlaybackControlsProps { export default function PlaybackControls(props: PlaybackControlsProps) { return ( - - - { - props.setAutoRotate((e.target as HTMLInputElement).checked); - }} - /> - - + { - props.setPlaying((e.target as HTMLInputElement).checked); - }} + onClick={() => props.setPlaying(!props.playing)} /> props.setCurTime(value as number)} value={props.curTime} /> - + {/* TODO: add control button groups - perhaps a separate component */} + props.setAutoRotate(!props.autoRotate)} + /> +
); } diff --git a/src/components/scene.tsx b/src/components/Scene.tsx similarity index 100% rename from src/components/scene.tsx rename to src/components/Scene.tsx diff --git a/src/components/TrackControls.tsx b/src/components/TrackControls.tsx new file mode 100644 index 00000000..4b1087fe --- /dev/null +++ b/src/components/TrackControls.tsx @@ -0,0 +1,40 @@ +import { TrackManager } from "@/lib/TrackManager"; +import { Button, InputSlider } from "@czi-sds/components"; +import { Stack } from "@mui/material"; + +interface TrackControlsProps { + trackManager: TrackManager | null; + trackHighlightLength: number; + setTrackHighlightLength: (trackHighlightLength: number) => void; + clearTracks: () => void; +} + +export default function TrackControls(props: TrackControlsProps) { + const numTimes = props.trackManager?.points.shape[0] ?? 0; + + return ( + + + `${value} frames`} + onChange={(_, value) => { + props.setTrackHighlightLength(value as number); + }} + value={props.trackHighlightLength} + /> + + + + + ); +} diff --git a/src/components/dataControls.tsx b/src/components/dataControls.tsx deleted file mode 100644 index 9dcb8912..00000000 --- a/src/components/dataControls.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { TrackManager } from "@/lib/TrackManager"; -import { Button, ButtonIcon, InputSlider, InputText } from "@czi-sds/components"; -import { Stack } from "@mui/material"; - -interface DataControlsProps { - dataUrl: string; - initialDataUrl: string; - trackManager: TrackManager | null; - trackHighlightLength: number; - setDataUrl: (dataUrl: string) => void; - setTrackHighlightLength: (trackHighlightLength: number) => void; - copyShareableUrlToClipboard: () => void; - clearTracks: () => void; -} - -export default function DataControls(props: DataControlsProps) { - const numTimes = props.trackManager?.points.shape[0] ?? 0; - const trackLengthPct = numTimes ? (props.trackHighlightLength / 2 / numTimes) * 100 : 0; - - console.log("trackLengthPct: %s", props.trackManager?.maxPointsPerTimepoint); - return ( - - props.setDataUrl(e.target.value)} - fullWidth={true} - intent={props.trackManager ? "default" : "error"} - /> - - `${value}%`} - onChange={(_, value) => { - if (!props.trackManager) return; - const v = ((value as number) / 100) * 2 * numTimes; - props.setTrackHighlightLength(v); - }} - value={Math.round(trackLengthPct)} - /> - - - - - - ); -}