Skip to content

Commit 044592b

Browse files
committed
Adds animated panel to a storybook
1 parent bf736a7 commit 044592b

3 files changed

Lines changed: 187 additions & 2 deletions

File tree

apps/webapp/app/components/primitives/Resizable.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import React from "react";
3+
import React, { useRef } from "react";
44
import { PanelGroup, Panel, PanelResizer } from "react-window-splitter";
55
import { cn } from "~/utils/cn";
66

@@ -69,6 +69,12 @@ const ResizableHandle = ({
6969
</PanelResizer>
7070
);
7171

72-
export { ResizableHandle, ResizablePanel, ResizablePanelGroup };
72+
function useFrozenValue<T>(value: T | null | undefined): T | null | undefined {
73+
const ref = useRef(value);
74+
if (value != null) ref.current = value;
75+
return ref.current;
76+
}
77+
78+
export { ResizableHandle, ResizablePanel, ResizablePanelGroup, useFrozenValue };
7379

7480
export type ResizableSnapshot = React.ComponentProps<typeof PanelGroup>["snapshot"];
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { useState } from "react";
2+
import { ExitIcon } from "~/assets/icons/ExitIcon";
3+
import { Button } from "~/components/primitives/Buttons";
4+
import { Header2 } from "~/components/primitives/Headers";
5+
import { Paragraph } from "~/components/primitives/Paragraph";
6+
import * as Property from "~/components/primitives/PropertyTable";
7+
import {
8+
ResizableHandle,
9+
ResizablePanel,
10+
ResizablePanelGroup,
11+
useFrozenValue,
12+
} from "~/components/primitives/Resizable";
13+
import {
14+
Table,
15+
TableBody,
16+
TableCell,
17+
TableHeader,
18+
TableHeaderCell,
19+
TableRow,
20+
} from "~/components/primitives/Table";
21+
import { cn } from "~/utils/cn";
22+
23+
type DemoItem = {
24+
id: string;
25+
name: string;
26+
status: "completed" | "running" | "failed" | "queued";
27+
duration: string;
28+
task: string;
29+
};
30+
31+
const demoItems: DemoItem[] = [
32+
{ id: "run_a1b2c3d4", name: "Process invoices", status: "completed", duration: "2.3s", task: "invoice/process" },
33+
{ id: "run_e5f6g7h8", name: "Send welcome email", status: "running", duration: "0.8s", task: "email/welcome" },
34+
{ id: "run_i9j0k1l2", name: "Generate report", status: "failed", duration: "12.1s", task: "report/generate" },
35+
{ id: "run_m3n4o5p6", name: "Sync inventory", status: "completed", duration: "5.7s", task: "inventory/sync" },
36+
{ id: "run_q7r8s9t0", name: "Resize images", status: "queued", duration: "—", task: "image/resize" },
37+
{ id: "run_u1v2w3x4", name: "Update search index", status: "completed", duration: "1.1s", task: "search/index" },
38+
{ id: "run_y5z6a7b8", name: "Calculate analytics", status: "running", duration: "8.4s", task: "analytics/calc" },
39+
{ id: "run_c9d0e1f2", name: "Deploy preview", status: "completed", duration: "34.2s", task: "deploy/preview" },
40+
{ id: "run_g3h4i5j6", name: "Run migrations", status: "failed", duration: "0.3s", task: "db/migrate" },
41+
{ id: "run_k7l8m9n0", name: "Notify Slack", status: "completed", duration: "0.5s", task: "notify/slack" },
42+
];
43+
44+
const statusColors: Record<DemoItem["status"], string> = {
45+
completed: "text-success",
46+
running: "text-blue-500",
47+
failed: "text-error",
48+
queued: "text-text-dimmed",
49+
};
50+
51+
function DetailPanel({ item, onClose }: { item: DemoItem; onClose: () => void }) {
52+
return (
53+
<div className="grid h-full max-h-full grid-rows-[2.5rem_1fr] overflow-hidden bg-background-bright">
54+
<div className="flex items-center justify-between gap-2 overflow-x-hidden border-b border-grid-bright px-3 pr-2">
55+
<Header2 className="truncate text-text-bright">{item.name}</Header2>
56+
<Button
57+
onClick={onClose}
58+
variant="minimal/small"
59+
TrailingIcon={ExitIcon}
60+
shortcut={{ key: "esc" }}
61+
shortcutPosition="before-trailing-icon"
62+
className="pl-1"
63+
/>
64+
</div>
65+
<div className="overflow-y-auto px-3 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
66+
<Property.Table>
67+
<Property.Item>
68+
<Property.Label>Run ID</Property.Label>
69+
<Property.Value>{item.id}</Property.Value>
70+
</Property.Item>
71+
<Property.Item>
72+
<Property.Label>Task</Property.Label>
73+
<Property.Value>{item.task}</Property.Value>
74+
</Property.Item>
75+
<Property.Item>
76+
<Property.Label>Status</Property.Label>
77+
<Property.Value>
78+
<span className={statusColors[item.status]}>
79+
{item.status.charAt(0).toUpperCase() + item.status.slice(1)}
80+
</span>
81+
</Property.Value>
82+
</Property.Item>
83+
<Property.Item>
84+
<Property.Label>Duration</Property.Label>
85+
<Property.Value>{item.duration}</Property.Value>
86+
</Property.Item>
87+
</Property.Table>
88+
<div className="mt-4">
89+
<Paragraph variant="small" className="text-text-dimmed">
90+
This is a demo detail panel showing the animated slide-in/out behavior using
91+
react-window-splitter&apos;s collapseAnimation. Click a different row to change the
92+
detail, or press Esc / click the close button to dismiss.
93+
</Paragraph>
94+
</div>
95+
</div>
96+
</div>
97+
);
98+
}
99+
100+
export default function Story() {
101+
const [selectedItem, setSelectedItem] = useState<DemoItem | null>(null);
102+
const show = !!selectedItem;
103+
const frozenItem = useFrozenValue(selectedItem);
104+
const displayItem = selectedItem ?? frozenItem;
105+
106+
return (
107+
<div className="h-full">
108+
<ResizablePanelGroup orientation="horizontal" className="max-h-full">
109+
<ResizablePanel id="animated-panel-main" min="200px">
110+
<div className="grid h-full max-h-full grid-rows-[2.5rem_1fr] overflow-hidden">
111+
<div className="flex items-center border-b border-grid-bright px-3">
112+
<Header2 className="text-text-bright">Runs</Header2>
113+
</div>
114+
<Table containerClassName="max-h-full" showTopBorder={false}>
115+
<TableHeader>
116+
<TableRow>
117+
<TableHeaderCell>Run ID</TableHeaderCell>
118+
<TableHeaderCell>Name</TableHeaderCell>
119+
<TableHeaderCell>Task</TableHeaderCell>
120+
<TableHeaderCell>Status</TableHeaderCell>
121+
<TableHeaderCell alignment="right">Duration</TableHeaderCell>
122+
</TableRow>
123+
</TableHeader>
124+
<TableBody>
125+
{demoItems.map((item) => (
126+
<TableRow key={item.id} isSelected={selectedItem?.id === item.id}>
127+
<TableCell onClick={() => setSelectedItem(item)} isTabbableCell>
128+
{item.id}
129+
</TableCell>
130+
<TableCell onClick={() => setSelectedItem(item)}>{item.name}</TableCell>
131+
<TableCell onClick={() => setSelectedItem(item)}>{item.task}</TableCell>
132+
<TableCell onClick={() => setSelectedItem(item)}>
133+
<span className={statusColors[item.status]}>
134+
{item.status.charAt(0).toUpperCase() + item.status.slice(1)}
135+
</span>
136+
</TableCell>
137+
<TableCell onClick={() => setSelectedItem(item)} alignment="right">
138+
{item.duration}
139+
</TableCell>
140+
</TableRow>
141+
))}
142+
</TableBody>
143+
</Table>
144+
</div>
145+
</ResizablePanel>
146+
<ResizableHandle
147+
id="animated-panel-handle"
148+
className={cn("transition-opacity duration-200", !show && "pointer-events-none opacity-0")}
149+
/>
150+
<ResizablePanel
151+
id="animated-panel-detail"
152+
min="280px"
153+
default="380px"
154+
max="500px"
155+
className="overflow-hidden"
156+
collapsible
157+
collapsed={!show}
158+
onCollapseChange={() => {}}
159+
collapsedSize="0px"
160+
collapseAnimation={{ easing: "ease-in-out", duration: 200 }}
161+
>
162+
<div className="h-full" style={{ minWidth: 280 }}>
163+
{displayItem && (
164+
<DetailPanel
165+
key={displayItem.id}
166+
item={displayItem}
167+
onClose={() => setSelectedItem(null)}
168+
/>
169+
)}
170+
</div>
171+
</ResizablePanel>
172+
</ResizablePanelGroup>
173+
</div>
174+
);
175+
}

apps/webapp/app/routes/storybook/route.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import { requireUser } from "~/services/session.server";
88
import { cn } from "~/utils/cn";
99

1010
const stories: Story[] = [
11+
{
12+
name: "Animated panel",
13+
slug: "animated-panel",
14+
},
1115
{
1216
name: "Avatar",
1317
slug: "avatar",

0 commit comments

Comments
 (0)