diff --git a/app/src/components/AppFrame/AppFrame.tsx b/app/src/components/AppFrame/AppFrame.tsx index 2caaa0dc..99bf192f 100644 --- a/app/src/components/AppFrame/AppFrame.tsx +++ b/app/src/components/AppFrame/AppFrame.tsx @@ -1,18 +1,35 @@ +import { useRouterState } from '@tanstack/react-router'; import { TitleBarDragRegion } from '@/components/TitleBarDragRegion'; import { AudioPlayer } from '@/components/AudioPlayer/AudioPlayer'; +import { StoryTrackEditor } from '@/components/StoriesTab/StoryTrackEditor'; import { TOP_SAFE_AREA_PADDING } from '@/lib/constants/ui'; import { cn } from '@/lib/utils/cn'; +import { useStoryStore } from '@/stores/storyStore'; +import { useStory } from '@/lib/hooks/useStories'; interface AppFrameProps { children: React.ReactNode; } export function AppFrame({ children }: AppFrameProps) { + const routerState = useRouterState(); + const isStoriesRoute = routerState.location.pathname === '/stories'; + + const selectedStoryId = useStoryStore((state) => state.selectedStoryId); + const { data: story } = useStory(selectedStoryId); + + // Show track editor when on stories route with a selected story that has items + const showTrackEditor = isStoriesRoute && selectedStoryId && story && story.items.length > 0; + return (
{children} - + {showTrackEditor ? ( + + ) : ( + + )}
); } diff --git a/app/src/components/Generation/FloatingGenerateBox.tsx b/app/src/components/Generation/FloatingGenerateBox.tsx index 39bda39b..91609c62 100644 --- a/app/src/components/Generation/FloatingGenerateBox.tsx +++ b/app/src/components/Generation/FloatingGenerateBox.tsx @@ -22,11 +22,11 @@ import { useToast } from '@/components/ui/use-toast'; import { cn } from '@/lib/utils/cn'; interface FloatingGenerateBoxProps { - isPlayerOpen: boolean; + isPlayerOpen?: boolean; showVoiceSelector?: boolean; } -export function FloatingGenerateBox({ isPlayerOpen, showVoiceSelector = false }: FloatingGenerateBoxProps) { +export function FloatingGenerateBox({ isPlayerOpen = false, showVoiceSelector = false }: FloatingGenerateBoxProps) { const selectedProfileId = useUIStore((state) => state.selectedProfileId); const setSelectedProfileId = useUIStore((state) => state.setSelectedProfileId); const { data: selectedProfile } = useProfile(selectedProfileId || ''); @@ -37,9 +37,13 @@ export function FloatingGenerateBox({ isPlayerOpen, showVoiceSelector = false }: const matchRoute = useMatchRoute(); const isStoriesRoute = matchRoute({ to: '/stories' }); const selectedStoryId = useStoryStore((state) => state.selectedStoryId); + const trackEditorHeight = useStoryStore((state) => state.trackEditorHeight); const { data: currentStory } = useStory(selectedStoryId); const addStoryItem = useAddStoryItem(); const { toast } = useToast(); + + // Calculate if track editor is visible (on stories route with items) + const hasTrackEditor = isStoriesRoute && currentStory && currentStory.items.length > 0; const { form, handleSubmit, isPending } = useGenerationForm({ onSuccess: async (generationId) => { @@ -110,7 +114,13 @@ export function FloatingGenerateBox({ isPlayerOpen, showVoiceSelector = false }: : 'left-[calc(5rem+2rem)] w-[calc((100%-5rem-4rem)/2-1rem)]', )} style={{ - bottom: isPlayerOpen ? 'calc(7rem + 1.5rem)' : '1.5rem', + // On stories route: offset by track editor height when visible + // On other routes: offset by audio player height when visible + bottom: hasTrackEditor + ? `${trackEditorHeight + 24}px` + : isPlayerOpen + ? 'calc(7rem + 1.5rem)' + : '1.5rem', }} > state.audioUrl); - const selectedStoryId = useStoryStore((state) => state.selectedStoryId); - const { data: story } = useStory(selectedStoryId); - - const hasTrackEditor = selectedStoryId && story && story.items.length > 0; - return (
{/* Main content area */} @@ -27,16 +17,9 @@ export function StoriesTab() {
- {/* Floating Generate Box */} - + {/* Floating Generate Box - position is managed via storyStore.trackEditorHeight */} + - - {/* Track Editor - at bottom when a story with items is selected */} - {hasTrackEditor && ( -
- -
- )} ); } diff --git a/app/src/components/StoriesTab/StoryContent.tsx b/app/src/components/StoriesTab/StoryContent.tsx index c06c4eae..bd38a987 100644 --- a/app/src/components/StoriesTab/StoryContent.tsx +++ b/app/src/components/StoriesTab/StoryContent.tsx @@ -17,15 +17,15 @@ import { Download, Pause, Play } from 'lucide-react'; import { useMemo, useRef } from 'react'; import { Button } from '@/components/ui/button'; import { useToast } from '@/components/ui/use-toast'; -import { BOTTOM_SAFE_AREA_PADDING } from '@/lib/constants/ui'; import { Slider } from '@/components/ui/slider'; import { useStory, useRemoveStoryItem, useExportStoryAudio, useReorderStoryItems } from '@/lib/hooks/useStories'; import { useStoryStore } from '@/stores/storyStore'; -import { usePlayerStore } from '@/stores/playerStore'; import { SortableStoryChatItem } from './StoryChatItem'; -import { cn } from '@/lib/utils/cn'; import { useStoryPlayback } from '@/lib/hooks/useStoryPlayback'; +// Height of the floating generate box plus some padding +const GENERATE_BOX_HEIGHT = 160; + export function StoryContent() { const selectedStoryId = useStoryStore((state) => state.selectedStoryId); const { data: story, isLoading } = useStory(selectedStoryId); @@ -34,8 +34,15 @@ export function StoryContent() { const exportAudio = useExportStoryAudio(); const { toast } = useToast(); const scrollRef = useRef(null); - const audioUrl = usePlayerStore((state) => state.audioUrl); - const isPlayerVisible = !!audioUrl; + + // Get track editor height from store for dynamic padding + const trackEditorHeight = useStoryStore((state) => state.trackEditorHeight); + + // Track editor is shown when story has items + const hasBottomBar = story && story.items.length > 0; + + // Calculate dynamic bottom padding: track editor + generate box + gap + const bottomPadding = hasBottomBar ? trackEditorHeight + GENERATE_BOX_HEIGHT + 24 : 0; // Drag and drop sensors const sensors = useSensors( @@ -261,10 +268,8 @@ export function StoryContent() { {/* Content */}
0 ? `${bottomPadding}px` : undefined }} > {sortedItems.length === 0 ? (
diff --git a/app/src/components/StoriesTab/StoryTrackEditor.tsx b/app/src/components/StoriesTab/StoryTrackEditor.tsx index 34e4d982..d46d2189 100644 --- a/app/src/components/StoriesTab/StoryTrackEditor.tsx +++ b/app/src/components/StoriesTab/StoryTrackEditor.tsx @@ -19,14 +19,12 @@ const DEFAULT_PIXELS_PER_SECOND = 50; const DEFAULT_TRACKS = [1, 0, -1]; // Default 3 tracks const MIN_EDITOR_HEIGHT = 120; const MAX_EDITOR_HEIGHT = 500; -const DEFAULT_EDITOR_HEIGHT = 200; export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) { const [pixelsPerSecond, setPixelsPerSecond] = useState(DEFAULT_PIXELS_PER_SECOND); const [draggingItem, setDraggingItem] = useState(null); const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); const [dragPosition, setDragPosition] = useState({ x: 0, y: 0 }); - const [editorHeight, setEditorHeight] = useState(DEFAULT_EDITOR_HEIGHT); const [isResizing, setIsResizing] = useState(false); const containerRef = useRef(null); const tracksRef = useRef(null); @@ -35,6 +33,10 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) { const moveItem = useMoveStoryItem(); const { toast } = useToast(); + // Track editor height from store (shared with FloatingGenerateBox) + const editorHeight = useStoryStore((state) => state.trackEditorHeight); + const setEditorHeight = useStoryStore((state) => state.setTrackEditorHeight); + // Playback state const currentTimeMs = useStoryStore((state) => state.currentTimeMs); const playbackStoryId = useStoryStore((state) => state.playbackStoryId); @@ -116,7 +118,7 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) { Math.max(MIN_EDITOR_HEIGHT, resizeStartHeight.current + deltaY) ); setEditorHeight(newHeight); - }, [isResizing]); + }, [isResizing, setEditorHeight]); const handleResizeEnd = useCallback(() => { setIsResizing(false); @@ -247,15 +249,12 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) { const timelineContainerHeight = editorHeight - 40; // Subtract toolbar height if (items.length === 0) { - return ( -
-

Add audio clips to see the track editor

-
- ); + return null; } return ( -
+
+
{/* Resize handle at top */}
+
); } diff --git a/app/src/lib/hooks/useStoryPlayback.ts b/app/src/lib/hooks/useStoryPlayback.ts index 80c0cdf2..86da1de3 100644 --- a/app/src/lib/hooks/useStoryPlayback.ts +++ b/app/src/lib/hooks/useStoryPlayback.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef, useCallback } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; import { apiClient } from '@/lib/api/client'; import type { StoryItemDetail } from '@/lib/api/types'; import { useStoryStore } from '@/stores/storyStore'; diff --git a/app/src/stores/storyStore.ts b/app/src/stores/storyStore.ts index e01b2d2a..8d862cea 100644 --- a/app/src/stores/storyStore.ts +++ b/app/src/stores/storyStore.ts @@ -6,6 +6,10 @@ interface StoryPlaybackState { selectedStoryId: string | null; setSelectedStoryId: (id: string | null) => void; + // Track editor UI state + trackEditorHeight: number; + setTrackEditorHeight: (height: number) => void; + // Playback state isPlaying: boolean; currentTimeMs: number; @@ -24,11 +28,17 @@ interface StoryPlaybackState { setPlaybackTiming: (contextTime: number, storyTime: number) => void; // Set timing anchors for Web Audio API } +const DEFAULT_TRACK_EDITOR_HEIGHT = 200; + export const useStoryStore = create((set, get) => ({ // Selection selectedStoryId: null, setSelectedStoryId: (id) => set({ selectedStoryId: id }), + // Track editor UI state + trackEditorHeight: DEFAULT_TRACK_EDITOR_HEIGHT, + setTrackEditorHeight: (height) => set({ trackEditorHeight: height }), + // Playback state isPlaying: false, currentTimeMs: 0,