Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
8 changes: 7 additions & 1 deletion web/src/components/storyboard/ShotCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,13 @@ const ShotCardInner: React.FC<ShotCardProps> = ({
pulse={meta.pulse}
/>
{!readOnly && (
<HoverActionGroup triggerSelector=".shot-card:hover" gap={0}>
<HoverActionGroup
triggerSelector=".shot-card:hover"
gap={0}
// Touch devices can't hover; keep the row actions (reorder,
// delete) visible there instead of hiding behind the reveal.
sx={{ "@media (pointer: coarse)": { opacity: 1 } }}
>
<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.
[theme.breakpoints.down("sm")]: {
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
118 changes: 104 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,34 @@ 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 /> }
];

const MOBILE_PANES: readonly MobilePane[] = ["boards", "board", "assistant"];

const isMobilePane = (value: string): value is MobilePane =>
(MOBILE_PANES as readonly string[]).includes(value);

/**
* 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 +100,83 @@ 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) => {
if (isMobilePane(value)) {
setMobilePane(value);
}
}}
size="small"
fullWidth
sx={{
flexShrink: 0,
borderBottom: `1px solid ${theme.vars.palette.divider}`
}}
/>
{/* One pane visible at a time; each fills the switcher body and its
child owns the layout, so plain block boxes (toggled via display)
suffice — no flex wrapper needed. */}
<div style={{ flex: 1, minHeight: 0 }}>
<div
style={{
height: "100%",
display: mobilePane === "boards" ? "block" : "none"
}}
>
<StoryboardSidebar activeBoardId={refId} />
</div>
<div
style={{
height: "100%",
display: mobilePane === "board" ? "block" : "none"
}}
>
{board}
</div>
<div
style={{
height: "100%",
display: mobilePane === "assistant" ? "block" : "none"
}}
>
<StoryboardAgentPanel boardId={refId} />
</div>
</div>
<StoryboardQueueOverlay boardId={refId} />
</FlexColumn>
);
}

return (
<div
style={{
Expand All @@ -87,18 +188,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