Skip to content

Commit 301855c

Browse files
committed
feat: left pannel bar
1 parent 494ff5f commit 301855c

File tree

6 files changed

+500
-332
lines changed

6 files changed

+500
-332
lines changed

app/components/editor/LeftPanel.tsx

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ interface LeftPanelProps {
2525
handleDeleteFromContext: () => void;
2626
handleSplitAudioFromContext: () => void;
2727
handleCloseContextMenu: () => void;
28+
// When true, renders the horizontal tab headers (default). Set false to hide headers
29+
showTabs?: boolean;
30+
// Persisted MediaBin view state
31+
arrangeMode?: "default" | "group";
32+
sortBy?: "default" | "name_asc" | "name_desc";
33+
onArrangeModeChange?: (mode: "default" | "group") => void;
34+
onSortByChange?: (sort: "default" | "name_asc" | "name_desc") => void;
2835
}
2936

3037
export default function LeftPanel({
@@ -37,6 +44,11 @@ export default function LeftPanel({
3744
handleDeleteFromContext,
3845
handleSplitAudioFromContext,
3946
handleCloseContextMenu,
47+
showTabs = true,
48+
arrangeMode,
49+
sortBy,
50+
onArrangeModeChange,
51+
onSortByChange,
4052
}: LeftPanelProps) {
4153
const location = useLocation();
4254

@@ -54,37 +66,39 @@ export default function LeftPanel({
5466
<div className="h-full flex flex-col bg-background">
5567
<Tabs value={activeTab} className="h-full flex flex-col">
5668
{/* Tab Headers */}
57-
<div className="border-b border-border bg-muted/30">
58-
<TabsList className="grid w-full grid-cols-3 h-9 bg-transparent p-0">
59-
<TabsTrigger
60-
value="media-bin"
61-
asChild
62-
className="h-8 text-xs data-[state=active]:bg-background data-[state=active]:shadow-sm">
63-
<Link to="media-bin" className="flex items-center gap-1.5">
64-
<FileImage className="h-3 w-3" />
65-
</Link>
66-
</TabsTrigger>
67-
<TabsTrigger
68-
value="text-editor"
69-
asChild
70-
className="h-8 text-xs data-[state=active]:bg-background data-[state=active]:shadow-sm">
71-
<Link to="text-editor" className="flex items-center gap-1.5">
72-
<Type className="h-3 w-3" />
73-
</Link>
74-
</TabsTrigger>
75-
<TabsTrigger
76-
value="transitions"
77-
asChild
78-
className="h-8 text-xs data-[state=active]:bg-background data-[state=active]:shadow-sm">
79-
<Link to="transitions" className="flex items-center gap-1.5">
80-
<BetweenVerticalEnd className="h-3 w-3" />
81-
</Link>
82-
</TabsTrigger>
83-
</TabsList>
84-
</div>
69+
{showTabs && (
70+
<div className="border-b border-border bg-muted/30">
71+
<TabsList className="grid w-full grid-cols-3 h-9 bg-transparent p-0">
72+
<TabsTrigger
73+
value="media-bin"
74+
asChild
75+
className="h-8 text-xs data-[state=active]:bg-background data-[state=active]:shadow-sm">
76+
<Link to="media-bin" className="flex items-center gap-1.5">
77+
<FileImage className="h-3 w-3" />
78+
</Link>
79+
</TabsTrigger>
80+
<TabsTrigger
81+
value="text-editor"
82+
asChild
83+
className="h-8 text-xs data-[state=active]:bg-background data-[state=active]:shadow-sm">
84+
<Link to="text-editor" className="flex items-center gap-1.5">
85+
<Type className="h-3 w-3" />
86+
</Link>
87+
</TabsTrigger>
88+
<TabsTrigger
89+
value="transitions"
90+
asChild
91+
className="h-8 text-xs data-[state=active]:bg-background data-[state=active]:shadow-sm">
92+
<Link to="transitions" className="flex items-center gap-1.5">
93+
<BetweenVerticalEnd className="h-3 w-3" />
94+
</Link>
95+
</TabsTrigger>
96+
</TabsList>
97+
</div>
98+
)}
8599

86100
{/* Tab Content */}
87-
<div className="flex-1 overflow-hidden">
101+
<div className="flex-1 overflow-hidden p-2">
88102
<Outlet
89103
context={{
90104
// MediaBin props
@@ -97,6 +111,10 @@ export default function LeftPanel({
97111
handleDeleteFromContext,
98112
handleSplitAudioFromContext,
99113
handleCloseContextMenu,
114+
arrangeModeExternal: arrangeMode,
115+
sortByExternal: sortBy,
116+
onArrangeModeChange,
117+
onSortByChange,
100118
}}
101119
/>
102120
</div>

app/components/media/TextEditor.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,15 @@ export default function TextEditor() {
5454

5555
return (
5656
<div className="h-full flex flex-col bg-background">
57-
<div className="flex-1 overflow-y-auto p-3">
57+
<div className="flex-1 overflow-y-auto px-4 py-4">
5858
<Card className="border-border/50">
59-
<CardHeader className="pb-3">
59+
<CardHeader className="pt-3 pb-3">
6060
<div className="flex items-center gap-2">
6161
<Type className="h-4 w-4 text-primary" />
6262
<CardTitle className="text-sm">Text Properties</CardTitle>
6363
</div>
6464
</CardHeader>
65-
<CardContent className="space-y-4">
65+
<CardContent className="space-y-4 pb-4">
6666
{/* Text Content */}
6767
<div className="space-y-2">
6868
<Label className="text-xs font-medium">Content</Label>

app/components/timeline/MediaBin.tsx

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ interface MediaBinProps {
5555
handleDeleteFromContext: () => Promise<void>;
5656
handleSplitAudioFromContext: () => Promise<void>;
5757
handleCloseContextMenu: () => void;
58+
// Persisted UI state from parent so grouping/sorting survives tab switches
59+
arrangeModeExternal?: "default" | "group";
60+
sortByExternal?: "default" | "name_asc" | "name_desc";
61+
onArrangeModeChange?: (mode: "default" | "group") => void;
62+
onSortByChange?: (sort: "default" | "name_asc" | "name_desc") => void;
5863
}
5964

6065
// Memoized component for video thumbnails to prevent flickering
@@ -222,14 +227,42 @@ export default function MediaBin() {
222227
handleDeleteFromContext,
223228
handleSplitAudioFromContext,
224229
handleCloseContextMenu,
230+
arrangeModeExternal,
231+
sortByExternal,
232+
onArrangeModeChange,
233+
onSortByChange,
225234
} = useOutletContext<MediaBinProps>();
226235

227236
// Drag & Drop state for external file imports
228237
const [isDragOver, setIsDragOver] = useState(false);
229238

230-
// Arrange & sorting state
231-
const [arrangeMode, setArrangeMode] = useState<"default" | "group">("default");
232-
const [sortBy, setSortBy] = useState<"default" | "name_asc" | "name_desc">("default");
239+
// Arrange & sorting state (controlled by parent when provided)
240+
const [arrangeMode, setArrangeMode] = useState<"default" | "group">(arrangeModeExternal ?? "default");
241+
const [sortBy, setSortBy] = useState<"default" | "name_asc" | "name_desc">(sortByExternal ?? "default");
242+
243+
// Sync from parent if it changes
244+
useEffect(() => {
245+
if (arrangeModeExternal && arrangeModeExternal !== arrangeMode) setArrangeMode(arrangeModeExternal);
246+
}, [arrangeModeExternal]);
247+
useEffect(() => {
248+
if (sortByExternal && sortByExternal !== sortBy) setSortBy(sortByExternal);
249+
}, [sortByExternal]);
250+
251+
const updateArrangeMode = useCallback(
252+
(mode: "default" | "group") => {
253+
setArrangeMode(mode);
254+
onArrangeModeChange?.(mode);
255+
},
256+
[onArrangeModeChange],
257+
);
258+
259+
const updateSortBy = useCallback(
260+
(sort: "default" | "name_asc" | "name_desc") => {
261+
setSortBy(sort);
262+
onSortByChange?.(sort);
263+
},
264+
[onSortByChange],
265+
);
233266
const [collapsed, setCollapsed] = useState<{
234267
[key in "videos" | "gifs" | "images" | "audio" | "text"]: boolean;
235268
}>({
@@ -458,7 +491,7 @@ export default function MediaBin() {
458491
className={`h-5 w-5 p-0 bg-transparent hover:bg-transparent ${
459492
arrangeMode === "default" ? "text-primary" : "text-muted-foreground/70 hover:text-foreground"
460493
}`}
461-
onClick={() => setArrangeMode("default")}
494+
onClick={() => updateArrangeMode("default")}
462495
title="Default order"
463496
aria-pressed={arrangeMode === "default"}>
464497
<List className="h-2 w-2" />
@@ -469,7 +502,7 @@ export default function MediaBin() {
469502
className={`h-5 w-5 p-0 bg-transparent hover:bg-transparent ${
470503
arrangeMode === "group" ? "text-primary" : "text-muted-foreground/70 hover:text-foreground"
471504
}`}
472-
onClick={() => setArrangeMode("group")}
505+
onClick={() => updateArrangeMode("group")}
473506
title="Smart Group"
474507
aria-pressed={arrangeMode === "group"}>
475508
<Layers className="h-2 w-2" />
@@ -493,19 +526,19 @@ export default function MediaBin() {
493526
<DropdownMenuLabel className="text-[11px]">Sort</DropdownMenuLabel>
494527
<DropdownMenuSeparator />
495528
<DropdownMenuItem
496-
onClick={() => setSortBy("default")}
529+
onClick={() => updateSortBy("default")}
497530
className={`text-[12px] gap-2 ${sortBy === "default" ? "text-primary" : ""}`}
498531
data-variant="ghost">
499532
<ArrowUpDown className={`h-3 w-3 ${sortBy === "default" ? "text-primary" : ""}`} /> Original order
500533
</DropdownMenuItem>
501534
<DropdownMenuItem
502-
onClick={() => setSortBy("name_asc")}
535+
onClick={() => updateSortBy("name_asc")}
503536
className={`text-[12px] gap-2 ${sortBy === "name_asc" ? "text-primary" : ""}`}
504537
data-variant="ghost">
505538
<ChevronUp className={`h-3 w-3 ${sortBy === "name_asc" ? "text-primary" : ""}`} /> Name A–Z
506539
</DropdownMenuItem>
507540
<DropdownMenuItem
508-
onClick={() => setSortBy("name_desc")}
541+
onClick={() => updateSortBy("name_desc")}
509542
className={`text-[12px] gap-2 ${sortBy === "name_desc" ? "text-primary" : ""}`}
510543
data-variant="ghost">
511544
<ChevronDown className={`h-3 w-3 ${sortBy === "name_desc" ? "text-primary" : ""}`} /> Name Z–A

app/components/ui/resizable.tsx

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,48 @@
1-
import * as React from "react"
2-
import { GripVerticalIcon } from "lucide-react"
3-
import * as ResizablePrimitive from "react-resizable-panels"
1+
import * as React from "react";
2+
import { GripVerticalIcon } from "lucide-react";
3+
import * as ResizablePrimitive from "react-resizable-panels";
44

5-
import { cn } from "~/lib/utils"
5+
import { cn } from "~/lib/utils";
66

7-
function ResizablePanelGroup({
8-
className,
9-
...props
10-
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
7+
function ResizablePanelGroup({ className, ...props }: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
118
return (
129
<ResizablePrimitive.PanelGroup
1310
data-slot="resizable-panel-group"
14-
className={cn(
15-
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
16-
className
17-
)}
11+
className={cn("flex h-full w-full data-[panel-group-direction=vertical]:flex-col", className)}
1812
{...props}
1913
/>
20-
)
14+
);
2115
}
2216

23-
function ResizablePanel({
24-
...props
25-
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
26-
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />
27-
}
17+
const ResizablePanel = React.forwardRef<
18+
ResizablePrimitive.ImperativePanelHandle,
19+
React.ComponentProps<typeof ResizablePrimitive.Panel>
20+
>(function ResizablePanel(props, ref) {
21+
return <ResizablePrimitive.Panel ref={ref} data-slot="resizable-panel" {...props} />;
22+
});
2823

2924
function ResizableHandle({
3025
withHandle,
3126
className,
3227
...props
3328
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
34-
withHandle?: boolean
29+
withHandle?: boolean;
3530
}) {
3631
return (
3732
<ResizablePrimitive.PanelResizeHandle
3833
data-slot="resizable-handle"
3934
className={cn(
4035
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
41-
className
36+
className,
4237
)}
43-
{...props}
44-
>
38+
{...props}>
4539
{withHandle && (
4640
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
4741
<GripVerticalIcon className="size-2.5" />
4842
</div>
4943
)}
5044
</ResizablePrimitive.PanelResizeHandle>
51-
)
45+
);
5246
}
5347

54-
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
48+
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };

0 commit comments

Comments
 (0)