Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion web/src/components/storyboard/ShotCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import React, { memo, useCallback, useMemo, useState } from "react";
import { css } from "@emotion/react";
import { useMediaQuery } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import type { Theme } from "@mui/material/styles";
Comment thread
Copilot marked this conversation as resolved.
import type { Shot, ShotStatus } from "@nodetool-ai/protocol";
Expand Down Expand Up @@ -168,6 +169,9 @@ const ShotCardInner: React.FC<ShotCardProps> = ({
isLast
}) => {
const theme = useTheme();
// Touch devices can't hover, so the row actions (reorder, delete) must stay
// visible instead of hiding behind a hover reveal.
const coarsePointer = useMediaQuery("(pointer: coarse)");
const toggleShotEntity = useStoryboardStore((state) => state.toggleShotEntity);
Comment thread
Copilot marked this conversation as resolved.
Outdated
const moveShot = useStoryboardStore((state) => state.moveShot);
const removeShot = useStoryboardStore((state) => state.removeShot);
Expand Down Expand Up @@ -279,7 +283,11 @@ const ShotCardInner: React.FC<ShotCardProps> = ({
pulse={meta.pulse}
/>
{!readOnly && (
<HoverActionGroup triggerSelector=".shot-card:hover" gap={0}>
<HoverActionGroup
triggerSelector=".shot-card:hover"
gap={0}
alwaysVisible={coarsePointer}
>
Comment on lines +282 to +288
<ToolbarIconButton
icon={<ArrowUpwardIcon sx={{ fontSize: 16 }} />}
tooltip="Move up"
Expand Down
1 change: 1 addition & 0 deletions web/src/components/storyboard/StoryboardAgentPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import useGlobalChatStore from "../../stores/GlobalChatStore";
const styles = (_theme: Theme) =>
css({
"&": {
width: "100%",
height: "100%",
minHeight: 0,
display: "flex",
Expand Down
5 changes: 5 additions & 0 deletions web/src/components/storyboard/StoryboardBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ const styles = (theme: Theme) =>
display: "flex",
flexDirection: "column",
gap: getSpacingPx(SPACING.lg),
// Reclaim horizontal room for shots on phones.
"@media (max-width: 600px)": {
padding: getSpacingPx(SPACING.md),
gap: getSpacingPx(SPACING.md)
},
Comment thread
Copilot marked this conversation as resolved.
// Children must keep their natural height so the container scrolls;
// otherwise the header panel gets flex-shrunk under the grid.
"> *": { flexShrink: 0 },
Expand Down
12 changes: 11 additions & 1 deletion web/src/components/storyboard/StoryboardSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ const styles = (theme: Theme) =>
height: "100%",
overflowY: "auto",
borderRight: `1px solid ${theme.vars.palette.divider}`,
// On phones the sidebar is a full-width pane behind a segmented switcher
// (see StoryboardSurface), not a fixed rail beside the board.
[theme.breakpoints.down("sm")]: {
width: "100%",
borderRight: "none"
},
padding: getSpacingPx(SPACING.md),
display: "flex",
flexDirection: "column",
Expand All @@ -64,7 +70,11 @@ const styles = (theme: Theme) =>
"&:hover": { backgroundColor: theme.vars.palette.action.hover },
"&.active": { backgroundColor: theme.vars.palette.action.selected },
".delete-button": { opacity: 0, transition: MOTION.opacity },
"&:hover .delete-button": { opacity: 1 }
"&:hover .delete-button": { opacity: 1 },
// Touch devices have no hover; keep the delete affordance reachable.
"@media (pointer: coarse)": {
".delete-button": { opacity: 1 }
}
}
});

Expand Down
112 changes: 98 additions & 14 deletions web/src/components/workspace/StoryboardSurface.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
/** @jsxImportSource @emotion/react */
import { useCallback, useEffect } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useMediaQuery } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import ViewListRoundedIcon from "@mui/icons-material/ViewListRounded";
import TheatersIcon from "@mui/icons-material/Theaters";
import AutoAwesomeIcon from "@mui/icons-material/AutoAwesome";
import {
useWorkspaceTabsStore,
type WorkspaceTabMode
Expand All @@ -12,7 +16,7 @@ import { useDirectScreenplay } from "../../hooks/storyboard/useDirectScreenplay"
import { useStoryboardServerSync } from "../../hooks/storyboard/useStoryboardServerSync";
import { useAssembleTimeline } from "../../hooks/storyboard/useAssembleTimeline";
import { useDocumentUndoShortcuts } from "../../hooks/useDocumentUndoShortcuts";
import { FlexColumn } from "../ui_primitives";
import { FlexColumn, TabGroup } from "../ui_primitives";
import StoryboardBoard from "../storyboard/StoryboardBoard";
import StoryboardSidebar from "../storyboard/StoryboardSidebar";
import StoryboardQueueOverlay from "../storyboard/StoryboardQueueOverlay";
Expand All @@ -26,14 +30,29 @@ interface StoryboardSurfaceProps {
active: boolean;
}

type MobilePane = "boards" | "board" | "assistant";

const MOBILE_TABS = [
{ value: "boards", label: "Boards", icon: <ViewListRoundedIcon /> },
{ value: "board", label: "Board", icon: <TheatersIcon /> },
{ value: "assistant", label: "Assistant", icon: <AutoAwesomeIcon /> }
];

/**
* Workspace surface for a storyboard tab. `refId` is the board id. Ensures the
* board exists in the singleton store, mounts the agent bridge (registering this
* board under its id for the ui_storyboard_* tools) and the generation
* subscriptions, and renders the board read-only in view mode.
*
* On wide screens the sidebar, board, and assistant sit side by side. On phones
* three columns don't fit, so edit mode collapses to a single pane with a
* segmented switcher; every pane stays mounted (toggled via `display`) so
* board, chat, and scroll state survive switches.
*/
const StoryboardSurface = ({ refId, mode, active }: StoryboardSurfaceProps) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
const [mobilePane, setMobilePane] = useState<MobilePane>("board");
const ensureBoard = useStoryboardStore((state) => state.ensureBoard);
const undo = useStoryboardStore((state) => state.undo);
const redo = useStoryboardStore((state) => state.redo);
Expand Down Expand Up @@ -76,6 +95,82 @@ const StoryboardSurface = ({ refId, mode, active }: StoryboardSurfaceProps) => {
});
}, [assemble, refId]);

const board = useMemo(
() => (
<StoryboardBoard
boardId={refId}
readOnly={mode === "view"}
onDirect={handleDirect}
directing={directing}
directError={error}
onAssemble={handleAssemble}
assembling={assembling}
assembleError={assembleError}
/>
),
[
refId,
mode,
handleDirect,
directing,
error,
handleAssemble,
assembling,
assembleError
]
);

if (isMobile && mode !== "view") {
return (
<FlexColumn fullHeight sx={{ minHeight: 0, position: "relative" }}>
<TabGroup
tabs={MOBILE_TABS}
value={mobilePane}
onChange={(value) => setMobilePane(value as MobilePane)}
size="small"
fullWidth
sx={{
flexShrink: 0,
borderBottom: `1px solid ${theme.vars.palette.divider}`
}}
/>
Comment on lines +128 to +145
<div style={{ flex: 1, minHeight: 0, display: "flex" }}>
<div
style={{
flex: 1,
minWidth: 0,
minHeight: 0,
display: mobilePane === "boards" ? "flex" : "none"
}}
>
<StoryboardSidebar activeBoardId={refId} />
</div>
<div
style={{
flex: 1,
minWidth: 0,
minHeight: 0,
display: mobilePane === "board" ? "block" : "none"
}}
>
{board}
</div>
<div
style={{
flex: 1,
minWidth: 0,
minHeight: 0,
display: mobilePane === "assistant" ? "flex" : "none"
}}
>
<StoryboardAgentPanel boardId={refId} />
</div>
Comment on lines +166 to +173
</div>
<StoryboardQueueOverlay boardId={refId} />
</FlexColumn>
);
}

return (
<div
style={{
Expand All @@ -87,18 +182,7 @@ const StoryboardSurface = ({ refId, mode, active }: StoryboardSurfaceProps) => {
>
{mode !== "view" && <StoryboardSidebar activeBoardId={refId} />}
<StoryboardQueueOverlay boardId={refId} />
<div style={{ flex: 1, minWidth: 0 }}>
<StoryboardBoard
boardId={refId}
readOnly={mode === "view"}
onDirect={handleDirect}
directing={directing}
directError={error}
onAssemble={handleAssemble}
assembling={assembling}
assembleError={assembleError}
/>
</div>
<div style={{ flex: 1, minWidth: 0 }}>{board}</div>
{mode !== "view" && (
<FlexColumn
fullHeight
Expand Down
Loading