@neynar/ui 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ui/resizable.d.ts +15 -2
- package/dist/components/ui/resizable.d.ts.map +1 -1
- package/dist/components/ui/resizable.js +71 -3
- package/dist/components/ui/resizable.js.map +1 -1
- package/llm/components/resizable.llm.md +63 -0
- package/package.json +1 -1
- package/src/components/neynar/typography/stories/title.stories.tsx +3 -1
- package/src/components/ui/resizable.tsx +111 -3
- package/src/components/ui/stories/resizable.stories.tsx +99 -0
- package/src/styles/styles.css +4 -0
|
@@ -6,12 +6,25 @@ type ResizablePanelGroupProps = React.ComponentProps<typeof PanelGroup>;
|
|
|
6
6
|
* Supports both horizontal and vertical layouts with optional persistence via `id` prop.
|
|
7
7
|
*/
|
|
8
8
|
declare function ResizablePanelGroup({ className, ...props }: ResizablePanelGroupProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
-
type ResizablePanelProps = React.ComponentProps<typeof Panel
|
|
9
|
+
type ResizablePanelProps = Omit<React.ComponentProps<typeof Panel>, "panelRef"> & {
|
|
10
|
+
/** Enable smooth CSS transition for collapse/expand. Drag resizing stays instant. @default false */
|
|
11
|
+
animated?: boolean;
|
|
12
|
+
/** Controlled collapsed state. When provided, the panel syncs to this value. */
|
|
13
|
+
collapsed?: boolean;
|
|
14
|
+
/** Animation duration in milliseconds. Only applies when `animated` is true. @default 400 */
|
|
15
|
+
duration?: number;
|
|
16
|
+
};
|
|
10
17
|
/**
|
|
11
18
|
* Individual resizable panel within a ResizablePanelGroup.
|
|
12
19
|
* Supports size constraints (minSize, maxSize), default sizing, and collapse behavior.
|
|
20
|
+
*
|
|
21
|
+
* Control collapse state declaratively via the `collapsed` prop.
|
|
22
|
+
* When `animated` is true, collapse/expand transitions are smooth.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* <ResizablePanel collapsed={isCollapsed} animated collapsible>
|
|
13
26
|
*/
|
|
14
|
-
declare function ResizablePanel({ ...props }: ResizablePanelProps): import("react/jsx-runtime").JSX.Element;
|
|
27
|
+
declare function ResizablePanel({ className, animated, collapsed, duration, ...props }: ResizablePanelProps): import("react/jsx-runtime").JSX.Element;
|
|
15
28
|
type ResizableHandleProps = React.ComponentProps<typeof PanelResizeHandle> & {
|
|
16
29
|
/** Display a visible grip indicator on the resize handle. @default false */
|
|
17
30
|
withHandle?: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resizable.d.ts","sourceRoot":"","sources":["../../../src/components/ui/resizable.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EACL,KAAK,IAAI,UAAU,EACnB,KAAK,EACL,SAAS,IAAI,iBAAiB,
|
|
1
|
+
{"version":3,"file":"resizable.d.ts","sourceRoot":"","sources":["../../../src/components/ui/resizable.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EACL,KAAK,IAAI,UAAU,EACnB,KAAK,EACL,SAAS,IAAI,iBAAiB,EAE/B,MAAM,wBAAwB,CAAC;AAgBhC,KAAK,wBAAwB,GAAG,KAAK,CAAC,cAAc,CAAC,OAAO,UAAU,CAAC,CAAC;AAExE;;;GAGG;AACH,iBAAS,mBAAmB,CAAC,EAC3B,SAAS,EACT,GAAG,KAAK,EACT,EAAE,wBAAwB,2CAW1B;AAED,KAAK,mBAAmB,GAAG,IAAI,CAC7B,KAAK,CAAC,cAAc,CAAC,OAAO,KAAK,CAAC,EAClC,UAAU,CACX,GAAG;IACF,oGAAoG;IACpG,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gFAAgF;IAChF,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,6FAA6F;IAC7F,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;;;;;;GASG;AACH,iBAAS,cAAc,CAAC,EACtB,SAAS,EACT,QAAQ,EACR,SAAS,EACT,QAA2B,EAC3B,GAAG,KAAK,EACT,EAAE,mBAAmB,2CA2ErB;AAED,KAAK,oBAAoB,GAAG,KAAK,CAAC,cAAc,CAAC,OAAO,iBAAiB,CAAC,GAAG;IAC3E,4EAA4E;IAC5E,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF;;;GAGG;AACH,iBAAS,eAAe,CAAC,EACvB,UAAU,EACV,SAAS,EACT,GAAG,KAAK,EACT,EAAE,oBAAoB,2CAetB;AAED,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,eAAe,EACf,KAAK,wBAAwB,EAC7B,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,GAC1B,CAAC"}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
|
-
import "react";
|
|
3
|
+
import * as React from "react";
|
|
4
4
|
import { Group as Tt, Panel as $t, Separator as Ht } from "../../node_modules/react-resizable-panels/dist/react-resizable-panels.js";
|
|
5
5
|
import { cn } from "../../lib/utils.js";
|
|
6
|
+
const DEFAULT_DURATION = 400;
|
|
7
|
+
const DEFAULT_EASING = "cubic-bezier(0.16, 1, 0.3, 1)";
|
|
8
|
+
function getTransitionStyle(durationMs, easing) {
|
|
9
|
+
const seconds = durationMs / 1e3;
|
|
10
|
+
return `flex-grow ${seconds}s ${easing}, flex-basis ${seconds}s ${easing}`;
|
|
11
|
+
}
|
|
6
12
|
function ResizablePanelGroup({
|
|
7
13
|
className,
|
|
8
14
|
...props
|
|
@@ -19,8 +25,70 @@ function ResizablePanelGroup({
|
|
|
19
25
|
}
|
|
20
26
|
);
|
|
21
27
|
}
|
|
22
|
-
function ResizablePanel({
|
|
23
|
-
|
|
28
|
+
function ResizablePanel({
|
|
29
|
+
className,
|
|
30
|
+
animated,
|
|
31
|
+
collapsed,
|
|
32
|
+
duration = DEFAULT_DURATION,
|
|
33
|
+
...props
|
|
34
|
+
}) {
|
|
35
|
+
const panelRef = React.useRef(null);
|
|
36
|
+
const elementRef = React.useRef(null);
|
|
37
|
+
const animatedRef = React.useRef(animated);
|
|
38
|
+
const durationRef = React.useRef(duration);
|
|
39
|
+
const isFirstRender = React.useRef(true);
|
|
40
|
+
React.useEffect(() => {
|
|
41
|
+
animatedRef.current = animated;
|
|
42
|
+
durationRef.current = duration;
|
|
43
|
+
}, [animated, duration]);
|
|
44
|
+
const withTransition = React.useCallback(
|
|
45
|
+
(action, skipAnimation = false) => {
|
|
46
|
+
const panelEl = elementRef.current?.closest(
|
|
47
|
+
'[data-panel][data-slot="resizable-panel"]'
|
|
48
|
+
);
|
|
49
|
+
if (animatedRef.current && panelEl && !skipAnimation) {
|
|
50
|
+
const ms = durationRef.current;
|
|
51
|
+
const easing = getComputedStyle(panelEl).getPropertyValue("--resizable-easing").trim() || DEFAULT_EASING;
|
|
52
|
+
panelEl.style.transition = getTransitionStyle(ms, easing);
|
|
53
|
+
action();
|
|
54
|
+
setTimeout(() => {
|
|
55
|
+
panelEl.style.transition = "";
|
|
56
|
+
}, ms);
|
|
57
|
+
} else {
|
|
58
|
+
action();
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
[]
|
|
62
|
+
);
|
|
63
|
+
React.useEffect(() => {
|
|
64
|
+
if (collapsed === void 0) return;
|
|
65
|
+
if (isFirstRender.current) {
|
|
66
|
+
isFirstRender.current = false;
|
|
67
|
+
requestAnimationFrame(() => {
|
|
68
|
+
if (collapsed) {
|
|
69
|
+
panelRef.current?.collapse();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const isCurrentlyCollapsed = panelRef.current?.isCollapsed() ?? false;
|
|
75
|
+
if (collapsed === isCurrentlyCollapsed) return;
|
|
76
|
+
if (collapsed) {
|
|
77
|
+
withTransition(() => panelRef.current?.collapse());
|
|
78
|
+
} else {
|
|
79
|
+
withTransition(() => panelRef.current?.expand());
|
|
80
|
+
}
|
|
81
|
+
}, [collapsed, withTransition]);
|
|
82
|
+
return /* @__PURE__ */ jsx(
|
|
83
|
+
$t,
|
|
84
|
+
{
|
|
85
|
+
"data-slot": "resizable-panel",
|
|
86
|
+
panelRef,
|
|
87
|
+
elementRef,
|
|
88
|
+
className: cn(className),
|
|
89
|
+
...props
|
|
90
|
+
}
|
|
91
|
+
);
|
|
24
92
|
}
|
|
25
93
|
function ResizableHandle({
|
|
26
94
|
withHandle,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resizable.js","sources":["../../../src/components/ui/resizable.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport {\n Group as PanelGroup,\n Panel,\n Separator as PanelResizeHandle,\n} from \"react-resizable-panels\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype ResizablePanelGroupProps = React.ComponentProps<typeof PanelGroup>;\n\n/**\n * Container for resizable panels. Manages layout orientation and panel resizing behavior.\n * Supports both horizontal and vertical layouts with optional persistence via `id` prop.\n */\nfunction ResizablePanelGroup({\n className,\n ...props\n}: ResizablePanelGroupProps) {\n return (\n <PanelGroup\n data-slot=\"resizable-panel-group\"\n className={cn(\n \"flex h-full w-full aria-[orientation=vertical]:flex-col\",\n className,\n )}\n {...props}\n />\n );\n}\n\ntype ResizablePanelProps = React.ComponentProps<typeof Panel
|
|
1
|
+
{"version":3,"file":"resizable.js","sources":["../../../src/components/ui/resizable.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport {\n Group as PanelGroup,\n Panel,\n Separator as PanelResizeHandle,\n type PanelImperativeHandle,\n} from \"react-resizable-panels\";\n\nimport { cn } from \"@/lib/utils\";\n\n/** Default duration of the collapse/expand animation in milliseconds. */\nconst DEFAULT_DURATION = 400;\n\n/** Default easing function for collapse/expand animations. */\nconst DEFAULT_EASING = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Generate CSS transition value for animated collapse/expand. */\nfunction getTransitionStyle(durationMs: number, easing: string): string {\n const seconds = durationMs / 1000;\n return `flex-grow ${seconds}s ${easing}, flex-basis ${seconds}s ${easing}`;\n}\n\ntype ResizablePanelGroupProps = React.ComponentProps<typeof PanelGroup>;\n\n/**\n * Container for resizable panels. Manages layout orientation and panel resizing behavior.\n * Supports both horizontal and vertical layouts with optional persistence via `id` prop.\n */\nfunction ResizablePanelGroup({\n className,\n ...props\n}: ResizablePanelGroupProps) {\n return (\n <PanelGroup\n data-slot=\"resizable-panel-group\"\n className={cn(\n \"flex h-full w-full aria-[orientation=vertical]:flex-col\",\n className,\n )}\n {...props}\n />\n );\n}\n\ntype ResizablePanelProps = Omit<\n React.ComponentProps<typeof Panel>,\n \"panelRef\"\n> & {\n /** Enable smooth CSS transition for collapse/expand. Drag resizing stays instant. @default false */\n animated?: boolean;\n /** Controlled collapsed state. When provided, the panel syncs to this value. */\n collapsed?: boolean;\n /** Animation duration in milliseconds. Only applies when `animated` is true. @default 400 */\n duration?: number;\n};\n\n/**\n * Individual resizable panel within a ResizablePanelGroup.\n * Supports size constraints (minSize, maxSize), default sizing, and collapse behavior.\n *\n * Control collapse state declaratively via the `collapsed` prop.\n * When `animated` is true, collapse/expand transitions are smooth.\n *\n * @example\n * <ResizablePanel collapsed={isCollapsed} animated collapsible>\n */\nfunction ResizablePanel({\n className,\n animated,\n collapsed,\n duration = DEFAULT_DURATION,\n ...props\n}: ResizablePanelProps) {\n const panelRef = React.useRef<PanelImperativeHandle>(null);\n const elementRef = React.useRef<HTMLDivElement>(null);\n const animatedRef = React.useRef(animated);\n const durationRef = React.useRef(duration);\n const isFirstRender = React.useRef(true);\n\n // Keep the refs in sync with prop changes\n React.useEffect(() => {\n animatedRef.current = animated;\n durationRef.current = duration;\n }, [animated, duration]);\n\n /** Apply transition, call action, then remove transition after duration */\n const withTransition = React.useCallback(\n (action: () => void, skipAnimation = false) => {\n const panelEl = elementRef.current?.closest(\n '[data-panel][data-slot=\"resizable-panel\"]',\n ) as HTMLElement | null;\n\n if (animatedRef.current && panelEl && !skipAnimation) {\n const ms = durationRef.current;\n // Read easing from CSS variable, fallback to default\n const easing =\n getComputedStyle(panelEl)\n .getPropertyValue(\"--resizable-easing\")\n .trim() || DEFAULT_EASING;\n panelEl.style.transition = getTransitionStyle(ms, easing);\n action();\n setTimeout(() => {\n panelEl.style.transition = \"\";\n }, ms);\n } else {\n action();\n }\n },\n [],\n );\n\n // Sync collapsed prop to panel state\n React.useEffect(() => {\n if (collapsed === undefined) return;\n\n // On first render, defer to allow panel registration\n if (isFirstRender.current) {\n isFirstRender.current = false;\n // Use requestAnimationFrame to ensure panel is registered\n requestAnimationFrame(() => {\n if (collapsed) {\n panelRef.current?.collapse();\n }\n });\n return;\n }\n\n // After first render, check current state before acting\n const isCurrentlyCollapsed = panelRef.current?.isCollapsed() ?? false;\n if (collapsed === isCurrentlyCollapsed) return;\n\n if (collapsed) {\n withTransition(() => panelRef.current?.collapse());\n } else {\n withTransition(() => panelRef.current?.expand());\n }\n }, [collapsed, withTransition]);\n\n return (\n <Panel\n data-slot=\"resizable-panel\"\n panelRef={panelRef}\n elementRef={elementRef}\n className={cn(className)}\n {...props}\n />\n );\n}\n\ntype ResizableHandleProps = React.ComponentProps<typeof PanelResizeHandle> & {\n /** Display a visible grip indicator on the resize handle. @default false */\n withHandle?: boolean;\n};\n\n/**\n * Resize handle between panels. Appears as a thin line with optional visible grip indicator.\n * Supports keyboard navigation and focus states.\n */\nfunction ResizableHandle({\n withHandle,\n className,\n ...props\n}: ResizableHandleProps) {\n return (\n <PanelResizeHandle\n data-slot=\"resizable-handle\"\n className={cn(\n \"group bg-border focus-visible:ring-primary/50 focus-visible:bg-primary/50 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:ring-offset-background focus-visible:outline-hidden data-[resize-handle-state=hover]:bg-primary/50 data-[resize-handle-state=drag]:bg-primary/50 data-[resize-handle-state=active]:bg-primary/50 aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 [&[aria-orientation=horizontal]>div]:rotate-90\",\n className,\n )}\n {...props}\n >\n {withHandle && (\n <div className=\"bg-border group-focus-visible:bg-primary/50 group-data-[resize-handle-state=hover]:bg-primary/50 group-data-[resize-handle-state=drag]:bg-primary/50 group-data-[resize-handle-state=active]:bg-primary/50 h-6 w-1 rounded-lg z-10 flex shrink-0\" />\n )}\n </PanelResizeHandle>\n );\n}\n\nexport {\n ResizablePanelGroup,\n ResizablePanel,\n ResizableHandle,\n type ResizablePanelGroupProps,\n type ResizablePanelProps,\n type ResizableHandleProps,\n};\n"],"names":["PanelGroup","Panel","PanelResizeHandle"],"mappings":";;;;;AAaA;AAGA;AAGA;AACE;AACA;AACF;AAQA;AAA6B;AAC3B;AAEF;AACE;AACE;AAACA;AAAA;AACW;AACC;AACT;AACA;AAAA;AAEE;AAAA;AAGV;AAwBA;AAAwB;AACtB;AACA;AACA;AACW;AAEb;AACE;AACA;AACA;AACA;AACA;AAGA;AACE;AACA;AAAsB;AAIxB;AAA6B;AAEzB;AAAoC;AAClC;AAGF;AACE;AAEA;AAIA;AACA;AACA;AACE;AAA2B;AACxB;AAEL;AAAA;AACF;AACF;AACA;AAIF;AACE;AAGA;AACE;AAEA;AACE;AACE;AAAkB;AACpB;AAEF;AAAA;AAIF;AACA;AAEA;AACE;AAAiD;AAEjD;AAA+C;AACjD;AAGF;AACE;AAACC;AAAA;AACW;AACV;AACA;AACuB;AACnB;AAAA;AAGV;AAWA;AAAyB;AACvB;AACA;AAEF;AACE;AACE;AAACC;AAAA;AACW;AACC;AACT;AACA;AAAA;AAEE;AAGgQ;AAAA;AAI1Q;;;;;;"}
|
|
@@ -70,7 +70,12 @@ All panels inherit props from react-resizable-panels Panel component.
|
|
|
70
70
|
| maxSize | number \| string | 100 | Maximum size (number=pixels, string=percentage or CSS unit) |
|
|
71
71
|
| collapsible | boolean | false | Allow panel to collapse below minSize |
|
|
72
72
|
| collapsedSize | number \| string | 0 | Size when collapsed |
|
|
73
|
+
| collapsed | boolean | - | Controlled collapsed state. Panel syncs to this value. |
|
|
74
|
+
| animated | boolean | false | Enable smooth CSS transition for collapse/expand. Drag resizing stays instant. |
|
|
75
|
+
| duration | number | 400 | Animation duration in milliseconds. Only applies when `animated` is true. |
|
|
73
76
|
| onResize | (size: { asPercentage: number; inPixels: number }) => void | - | Called when panel is resized |
|
|
77
|
+
| onCollapse | () => void | - | Called when panel collapses |
|
|
78
|
+
| onExpand | () => void | - | Called when panel expands |
|
|
74
79
|
| className | string | - | Additional CSS classes |
|
|
75
80
|
|
|
76
81
|
### ResizableHandle
|
|
@@ -222,6 +227,64 @@ function MonitoredPanels() {
|
|
|
222
227
|
}
|
|
223
228
|
```
|
|
224
229
|
|
|
230
|
+
### Animated Collapse/Expand
|
|
231
|
+
|
|
232
|
+
Use `collapsed` prop for declarative control and `animated` for smooth transitions. Drag resizing remains instant (no animation lag).
|
|
233
|
+
|
|
234
|
+
**Customization:**
|
|
235
|
+
- `duration` prop controls speed (default 400ms)
|
|
236
|
+
- `--resizable-easing` CSS variable controls easing curve (default `cubic-bezier(0.16, 1, 0.3, 1)`)
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
// Custom duration
|
|
240
|
+
<ResizablePanel animated duration={200} collapsed={isCollapsed} />
|
|
241
|
+
|
|
242
|
+
// Custom easing via Tailwind arbitrary property
|
|
243
|
+
<ResizablePanel
|
|
244
|
+
className="[--resizable-easing:ease-out]"
|
|
245
|
+
animated
|
|
246
|
+
collapsed={isCollapsed}
|
|
247
|
+
/>
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
```tsx
|
|
251
|
+
import { useState } from "react"
|
|
252
|
+
import {
|
|
253
|
+
ResizablePanelGroup,
|
|
254
|
+
ResizablePanel,
|
|
255
|
+
ResizableHandle,
|
|
256
|
+
} from "@neynar/ui/resizable"
|
|
257
|
+
import { Button } from "@neynar/ui/button"
|
|
258
|
+
|
|
259
|
+
function CollapsibleSidebar() {
|
|
260
|
+
const [isCollapsed, setIsCollapsed] = useState(false)
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<div className="h-[400px]">
|
|
264
|
+
<Button onClick={() => setIsCollapsed(!isCollapsed)}>
|
|
265
|
+
{isCollapsed ? "Expand" : "Collapse"} Sidebar
|
|
266
|
+
</Button>
|
|
267
|
+
<ResizablePanelGroup orientation="horizontal">
|
|
268
|
+
<ResizablePanel
|
|
269
|
+
collapsed={isCollapsed}
|
|
270
|
+
animated
|
|
271
|
+
defaultSize="25%"
|
|
272
|
+
minSize="15%"
|
|
273
|
+
collapsible
|
|
274
|
+
collapsedSize="0%"
|
|
275
|
+
>
|
|
276
|
+
<div className="p-4">Sidebar content</div>
|
|
277
|
+
</ResizablePanel>
|
|
278
|
+
<ResizableHandle withHandle />
|
|
279
|
+
<ResizablePanel>
|
|
280
|
+
<div className="p-4">Main content</div>
|
|
281
|
+
</ResizablePanel>
|
|
282
|
+
</ResizablePanelGroup>
|
|
283
|
+
</div>
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
225
288
|
## Keyboard
|
|
226
289
|
|
|
227
290
|
| Key | Action |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neynar/ui",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Neynar Inc.",
|
|
6
6
|
"description": "AI-first React component library for coding agents. LLM-optimized docs, sensible defaults, zero config. Built on shadcn patterns, Base UI, and Tailwind CSS v4.",
|
|
@@ -343,7 +343,9 @@ export const LargeMultiLine: Story = {
|
|
|
343
343
|
<div className="w-full max-w-2xl space-y-8">
|
|
344
344
|
<section className="space-y-4">
|
|
345
345
|
<div>
|
|
346
|
-
<h3 className="text-lg font-semibold">
|
|
346
|
+
<h3 className="text-lg font-semibold">
|
|
347
|
+
Large Sizes with Multi-line Text
|
|
348
|
+
</h3>
|
|
347
349
|
<p className="text-muted-foreground text-sm">
|
|
348
350
|
5xl and 6xl sizes with proper line-height to prevent text overlap.
|
|
349
351
|
</p>
|
|
@@ -5,10 +5,23 @@ import {
|
|
|
5
5
|
Group as PanelGroup,
|
|
6
6
|
Panel,
|
|
7
7
|
Separator as PanelResizeHandle,
|
|
8
|
+
type PanelImperativeHandle,
|
|
8
9
|
} from "react-resizable-panels";
|
|
9
10
|
|
|
10
11
|
import { cn } from "@/lib/utils";
|
|
11
12
|
|
|
13
|
+
/** Default duration of the collapse/expand animation in milliseconds. */
|
|
14
|
+
const DEFAULT_DURATION = 400;
|
|
15
|
+
|
|
16
|
+
/** Default easing function for collapse/expand animations. */
|
|
17
|
+
const DEFAULT_EASING = "cubic-bezier(0.16, 1, 0.3, 1)";
|
|
18
|
+
|
|
19
|
+
/** Generate CSS transition value for animated collapse/expand. */
|
|
20
|
+
function getTransitionStyle(durationMs: number, easing: string): string {
|
|
21
|
+
const seconds = durationMs / 1000;
|
|
22
|
+
return `flex-grow ${seconds}s ${easing}, flex-basis ${seconds}s ${easing}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
12
25
|
type ResizablePanelGroupProps = React.ComponentProps<typeof PanelGroup>;
|
|
13
26
|
|
|
14
27
|
/**
|
|
@@ -31,14 +44,109 @@ function ResizablePanelGroup({
|
|
|
31
44
|
);
|
|
32
45
|
}
|
|
33
46
|
|
|
34
|
-
type ResizablePanelProps =
|
|
47
|
+
type ResizablePanelProps = Omit<
|
|
48
|
+
React.ComponentProps<typeof Panel>,
|
|
49
|
+
"panelRef"
|
|
50
|
+
> & {
|
|
51
|
+
/** Enable smooth CSS transition for collapse/expand. Drag resizing stays instant. @default false */
|
|
52
|
+
animated?: boolean;
|
|
53
|
+
/** Controlled collapsed state. When provided, the panel syncs to this value. */
|
|
54
|
+
collapsed?: boolean;
|
|
55
|
+
/** Animation duration in milliseconds. Only applies when `animated` is true. @default 400 */
|
|
56
|
+
duration?: number;
|
|
57
|
+
};
|
|
35
58
|
|
|
36
59
|
/**
|
|
37
60
|
* Individual resizable panel within a ResizablePanelGroup.
|
|
38
61
|
* Supports size constraints (minSize, maxSize), default sizing, and collapse behavior.
|
|
62
|
+
*
|
|
63
|
+
* Control collapse state declaratively via the `collapsed` prop.
|
|
64
|
+
* When `animated` is true, collapse/expand transitions are smooth.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* <ResizablePanel collapsed={isCollapsed} animated collapsible>
|
|
39
68
|
*/
|
|
40
|
-
function ResizablePanel({
|
|
41
|
-
|
|
69
|
+
function ResizablePanel({
|
|
70
|
+
className,
|
|
71
|
+
animated,
|
|
72
|
+
collapsed,
|
|
73
|
+
duration = DEFAULT_DURATION,
|
|
74
|
+
...props
|
|
75
|
+
}: ResizablePanelProps) {
|
|
76
|
+
const panelRef = React.useRef<PanelImperativeHandle>(null);
|
|
77
|
+
const elementRef = React.useRef<HTMLDivElement>(null);
|
|
78
|
+
const animatedRef = React.useRef(animated);
|
|
79
|
+
const durationRef = React.useRef(duration);
|
|
80
|
+
const isFirstRender = React.useRef(true);
|
|
81
|
+
|
|
82
|
+
// Keep the refs in sync with prop changes
|
|
83
|
+
React.useEffect(() => {
|
|
84
|
+
animatedRef.current = animated;
|
|
85
|
+
durationRef.current = duration;
|
|
86
|
+
}, [animated, duration]);
|
|
87
|
+
|
|
88
|
+
/** Apply transition, call action, then remove transition after duration */
|
|
89
|
+
const withTransition = React.useCallback(
|
|
90
|
+
(action: () => void, skipAnimation = false) => {
|
|
91
|
+
const panelEl = elementRef.current?.closest(
|
|
92
|
+
'[data-panel][data-slot="resizable-panel"]',
|
|
93
|
+
) as HTMLElement | null;
|
|
94
|
+
|
|
95
|
+
if (animatedRef.current && panelEl && !skipAnimation) {
|
|
96
|
+
const ms = durationRef.current;
|
|
97
|
+
// Read easing from CSS variable, fallback to default
|
|
98
|
+
const easing =
|
|
99
|
+
getComputedStyle(panelEl)
|
|
100
|
+
.getPropertyValue("--resizable-easing")
|
|
101
|
+
.trim() || DEFAULT_EASING;
|
|
102
|
+
panelEl.style.transition = getTransitionStyle(ms, easing);
|
|
103
|
+
action();
|
|
104
|
+
setTimeout(() => {
|
|
105
|
+
panelEl.style.transition = "";
|
|
106
|
+
}, ms);
|
|
107
|
+
} else {
|
|
108
|
+
action();
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
[],
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Sync collapsed prop to panel state
|
|
115
|
+
React.useEffect(() => {
|
|
116
|
+
if (collapsed === undefined) return;
|
|
117
|
+
|
|
118
|
+
// On first render, defer to allow panel registration
|
|
119
|
+
if (isFirstRender.current) {
|
|
120
|
+
isFirstRender.current = false;
|
|
121
|
+
// Use requestAnimationFrame to ensure panel is registered
|
|
122
|
+
requestAnimationFrame(() => {
|
|
123
|
+
if (collapsed) {
|
|
124
|
+
panelRef.current?.collapse();
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// After first render, check current state before acting
|
|
131
|
+
const isCurrentlyCollapsed = panelRef.current?.isCollapsed() ?? false;
|
|
132
|
+
if (collapsed === isCurrentlyCollapsed) return;
|
|
133
|
+
|
|
134
|
+
if (collapsed) {
|
|
135
|
+
withTransition(() => panelRef.current?.collapse());
|
|
136
|
+
} else {
|
|
137
|
+
withTransition(() => panelRef.current?.expand());
|
|
138
|
+
}
|
|
139
|
+
}, [collapsed, withTransition]);
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<Panel
|
|
143
|
+
data-slot="resizable-panel"
|
|
144
|
+
panelRef={panelRef}
|
|
145
|
+
elementRef={elementRef}
|
|
146
|
+
className={cn(className)}
|
|
147
|
+
{...props}
|
|
148
|
+
/>
|
|
149
|
+
);
|
|
42
150
|
}
|
|
43
151
|
|
|
44
152
|
type ResizableHandleProps = React.ComponentProps<typeof PanelResizeHandle> & {
|
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
CodeIcon,
|
|
4
4
|
FileIcon,
|
|
5
5
|
FolderIcon,
|
|
6
|
+
PanelLeftCloseIcon,
|
|
7
|
+
PanelLeftOpenIcon,
|
|
6
8
|
PlayIcon,
|
|
7
9
|
TerminalIcon,
|
|
8
10
|
} from "lucide-react";
|
|
@@ -167,6 +169,103 @@ export function Counter() {
|
|
|
167
169
|
},
|
|
168
170
|
};
|
|
169
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Animated collapse/expand using the `collapsed` and `animated` props.
|
|
174
|
+
* Control collapse state declaratively - no ref needed.
|
|
175
|
+
* Toggle the animated prop to compare instant vs smooth transitions.
|
|
176
|
+
*/
|
|
177
|
+
export const AnimatedCollapse: Story = {
|
|
178
|
+
parameters: {
|
|
179
|
+
layout: "padded",
|
|
180
|
+
},
|
|
181
|
+
render: () => {
|
|
182
|
+
function AnimatedCollapseDemo() {
|
|
183
|
+
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
184
|
+
const [animated, setAnimated] = useState(true);
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<div className="space-y-4">
|
|
188
|
+
<div className="flex gap-2">
|
|
189
|
+
<Button
|
|
190
|
+
onClick={() => setIsCollapsed(!isCollapsed)}
|
|
191
|
+
variant="default"
|
|
192
|
+
>
|
|
193
|
+
{isCollapsed ? (
|
|
194
|
+
<PanelLeftOpenIcon data-icon="inline-start" />
|
|
195
|
+
) : (
|
|
196
|
+
<PanelLeftCloseIcon data-icon="inline-start" />
|
|
197
|
+
)}
|
|
198
|
+
{isCollapsed ? "Expand" : "Collapse"}
|
|
199
|
+
</Button>
|
|
200
|
+
<Button
|
|
201
|
+
onClick={() => setAnimated(!animated)}
|
|
202
|
+
variant={animated ? "secondary" : "outline"}
|
|
203
|
+
>
|
|
204
|
+
Animated: {animated ? "On" : "Off"}
|
|
205
|
+
</Button>
|
|
206
|
+
</div>
|
|
207
|
+
<div className="border-border h-[400px] w-full rounded-lg border">
|
|
208
|
+
<ResizablePanelGroup orientation="horizontal">
|
|
209
|
+
<ResizablePanel
|
|
210
|
+
collapsed={isCollapsed}
|
|
211
|
+
animated={animated}
|
|
212
|
+
defaultSize="25%"
|
|
213
|
+
minSize="15%"
|
|
214
|
+
maxSize="40%"
|
|
215
|
+
collapsible
|
|
216
|
+
collapsedSize="0%"
|
|
217
|
+
>
|
|
218
|
+
<div className="flex h-full flex-col">
|
|
219
|
+
<div className="border-b p-3">
|
|
220
|
+
<h3 className="flex items-center gap-2 text-sm font-semibold">
|
|
221
|
+
<FolderIcon className="size-4" />
|
|
222
|
+
Sidebar
|
|
223
|
+
</h3>
|
|
224
|
+
</div>
|
|
225
|
+
<div className="flex-1 overflow-auto p-4">
|
|
226
|
+
<div className="space-y-2">
|
|
227
|
+
{["Dashboard", "Analytics", "Reports", "Settings"].map(
|
|
228
|
+
(item) => (
|
|
229
|
+
<div
|
|
230
|
+
key={item}
|
|
231
|
+
className="hover:bg-accent rounded px-2 py-1.5 text-sm transition-colors"
|
|
232
|
+
>
|
|
233
|
+
{item}
|
|
234
|
+
</div>
|
|
235
|
+
),
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
</ResizablePanel>
|
|
241
|
+
<ResizableHandle withHandle />
|
|
242
|
+
<ResizablePanel defaultSize="75%">
|
|
243
|
+
<div className="flex h-full flex-col">
|
|
244
|
+
<div className="border-b p-3">
|
|
245
|
+
<h3 className="text-sm font-semibold">Main Content</h3>
|
|
246
|
+
</div>
|
|
247
|
+
<div className="flex-1 p-4">
|
|
248
|
+
<p className="text-muted-foreground text-sm">
|
|
249
|
+
The sidebar uses{" "}
|
|
250
|
+
<code className="bg-muted rounded px-1 text-xs">
|
|
251
|
+
collapsed={"{isCollapsed}"}
|
|
252
|
+
</code>{" "}
|
|
253
|
+
for declarative control. Toggle the button above to
|
|
254
|
+
compare instant vs animated transitions.
|
|
255
|
+
</p>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
</ResizablePanel>
|
|
259
|
+
</ResizablePanelGroup>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return <AnimatedCollapseDemo />;
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
|
|
170
269
|
/**
|
|
171
270
|
* Complete design system reference showing all resizable panel configurations,
|
|
172
271
|
* directions, handle styles, and complex layouts.
|
package/src/styles/styles.css
CHANGED
|
@@ -73,6 +73,10 @@
|
|
|
73
73
|
body {
|
|
74
74
|
@apply font-sans bg-background text-foreground;
|
|
75
75
|
}
|
|
76
|
+
:root {
|
|
77
|
+
/* Resizable panel animation easing - override in theme or component */
|
|
78
|
+
--resizable-easing: cubic-bezier(0.16, 1, 0.3, 1);
|
|
79
|
+
}
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
/* Surface blur effect - value comes from theme's --surface-blur variable */
|