@neynar/ui 1.0.6 → 1.2.0

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.
@@ -6,12 +6,23 @@ 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
+ };
10
15
  /**
11
16
  * Individual resizable panel within a ResizablePanelGroup.
12
17
  * Supports size constraints (minSize, maxSize), default sizing, and collapse behavior.
18
+ *
19
+ * Control collapse state declaratively via the `collapsed` prop.
20
+ * When `animated` is true, collapse/expand transitions are smooth.
21
+ *
22
+ * @example
23
+ * <ResizablePanel collapsed={isCollapsed} animated collapsible>
13
24
  */
14
- declare function ResizablePanel({ ...props }: ResizablePanelProps): import("react/jsx-runtime").JSX.Element;
25
+ declare function ResizablePanel({ className, animated, collapsed, ...props }: ResizablePanelProps): import("react/jsx-runtime").JSX.Element;
15
26
  type ResizableHandleProps = React.ComponentProps<typeof PanelResizeHandle> & {
16
27
  /** Display a visible grip indicator on the resize handle. @default false */
17
28
  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,EAC/B,MAAM,wBAAwB,CAAC;AAIhC,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,KAAK,CAAC,cAAc,CAAC,OAAO,KAAK,CAAC,CAAC;AAE9D;;;GAGG;AACH,iBAAS,cAAc,CAAC,EAAE,GAAG,KAAK,EAAE,EAAE,mBAAmB,2CAExD;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
+ {"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;AAYhC,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;CACrB,CAAC;AAEF;;;;;;;;;GASG;AACH,iBAAS,cAAc,CAAC,EACtB,SAAS,EACT,QAAQ,EACR,SAAS,EACT,GAAG,KAAK,EACT,EAAE,mBAAmB,2CAmErB;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,10 @@
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 TRANSITION_DURATION = 300;
7
+ const TRANSITION_STYLE = "flex-grow 0.3s cubic-bezier(0.16, 1, 0.3, 1), flex-basis 0.3s cubic-bezier(0.16, 1, 0.3, 1)";
6
8
  function ResizablePanelGroup({
7
9
  className,
8
10
  ...props
@@ -19,8 +21,65 @@ function ResizablePanelGroup({
19
21
  }
20
22
  );
21
23
  }
22
- function ResizablePanel({ ...props }) {
23
- return /* @__PURE__ */ jsx($t, { "data-slot": "resizable-panel", ...props });
24
+ function ResizablePanel({
25
+ className,
26
+ animated,
27
+ collapsed,
28
+ ...props
29
+ }) {
30
+ const panelRef = React.useRef(null);
31
+ const elementRef = React.useRef(null);
32
+ const animatedRef = React.useRef(animated);
33
+ const isFirstRender = React.useRef(true);
34
+ React.useEffect(() => {
35
+ animatedRef.current = animated;
36
+ }, [animated]);
37
+ const withTransition = React.useCallback(
38
+ (action, skipAnimation = false) => {
39
+ const panelEl = elementRef.current?.closest(
40
+ '[data-panel][data-slot="resizable-panel"]'
41
+ );
42
+ if (animatedRef.current && panelEl && !skipAnimation) {
43
+ panelEl.style.transition = TRANSITION_STYLE;
44
+ action();
45
+ setTimeout(() => {
46
+ panelEl.style.transition = "";
47
+ }, TRANSITION_DURATION);
48
+ } else {
49
+ action();
50
+ }
51
+ },
52
+ []
53
+ );
54
+ React.useEffect(() => {
55
+ if (collapsed === void 0) return;
56
+ if (isFirstRender.current) {
57
+ isFirstRender.current = false;
58
+ requestAnimationFrame(() => {
59
+ if (collapsed) {
60
+ panelRef.current?.collapse();
61
+ }
62
+ });
63
+ return;
64
+ }
65
+ const isCurrentlyCollapsed = panelRef.current?.isCollapsed() ?? false;
66
+ if (collapsed === isCurrentlyCollapsed) return;
67
+ if (collapsed) {
68
+ withTransition(() => panelRef.current?.collapse());
69
+ } else {
70
+ withTransition(() => panelRef.current?.expand());
71
+ }
72
+ }, [collapsed, withTransition]);
73
+ return /* @__PURE__ */ jsx(
74
+ $t,
75
+ {
76
+ "data-slot": "resizable-panel",
77
+ panelRef,
78
+ elementRef,
79
+ className: cn(className),
80
+ ...props
81
+ }
82
+ );
24
83
  }
25
84
  function ResizableHandle({
26
85
  withHandle,
@@ -32,11 +91,11 @@ function ResizableHandle({
32
91
  {
33
92
  "data-slot": "resizable-handle",
34
93
  className: cn(
35
- "group bg-border focus-visible:ring-primary 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-[resize-handle-state=hover]:bg-primary data-[resize-handle-state=drag]:bg-primary 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",
94
+ "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",
36
95
  className
37
96
  ),
38
97
  ...props,
39
- children: withHandle && /* @__PURE__ */ jsx("div", { className: "bg-border group-data-[resize-handle-state=hover]:bg-primary group-data-[resize-handle-state=drag]:bg-primary h-6 w-1 rounded-lg z-10 flex shrink-0" })
98
+ children: withHandle && /* @__PURE__ */ jsx("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" })
40
99
  }
41
100
  );
42
101
  }
@@ -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>;\n\n/**\n * Individual resizable panel within a ResizablePanelGroup.\n * Supports size constraints (minSize, maxSize), default sizing, and collapse behavior.\n */\nfunction ResizablePanel({ ...props }: ResizablePanelProps) {\n return <Panel data-slot=\"resizable-panel\" {...props} />;\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 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-[resize-handle-state=hover]:bg-primary data-[resize-handle-state=drag]:bg-primary 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-data-[resize-handle-state=hover]:bg-primary group-data-[resize-handle-state=drag]:bg-primary 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","PanelResizeHandle"],"mappings":";;;;;AAiBA;AAA6B;AAC3B;AAEF;AACE;AACE;AAACA;AAAA;AACW;AACC;AACT;AACA;AAAA;AAEE;AAAA;AAGV;AAQA;AACE;AACF;AAWA;AAAyB;AACvB;AACA;AAEF;AACE;AACE;AAACC;AAAA;AACW;AACC;AACT;AACA;AAAA;AAEE;AAGkK;AAAA;AAI5K;;;;;;"}
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/** Duration of the collapse/expand animation in milliseconds. */\nconst TRANSITION_DURATION = 300;\n\n/** CSS transition value for animated collapse/expand. */\nconst TRANSITION_STYLE =\n \"flex-grow 0.3s cubic-bezier(0.16, 1, 0.3, 1), flex-basis 0.3s cubic-bezier(0.16, 1, 0.3, 1)\";\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};\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 ...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 isFirstRender = React.useRef(true);\n\n // Keep the ref in sync with prop changes\n React.useEffect(() => {\n animatedRef.current = animated;\n }, [animated]);\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 panelEl.style.transition = TRANSITION_STYLE;\n action();\n setTimeout(() => {\n panelEl.style.transition = \"\";\n }, TRANSITION_DURATION);\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;AAUA;AAA6B;AAC3B;AAEF;AACE;AACE;AAACA;AAAA;AACW;AACC;AACT;AACA;AAAA;AAEE;AAAA;AAGV;AAsBA;AAAwB;AACtB;AACA;AACA;AAEF;AACE;AACA;AACA;AACA;AAGA;AACE;AAAsB;AAIxB;AAA6B;AAEzB;AAAoC;AAClC;AAGF;AACE;AACA;AACA;AACE;AAA2B;AACP;AAEtB;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;;;;;;"}
@@ -75,8 +75,8 @@ const titleVariants = cva("", {
75
75
  "2xl": "text-2xl leading-8 tracking-tight",
76
76
  "3xl": "text-3xl leading-9 tracking-tight",
77
77
  "4xl": "text-4xl leading-10 tracking-tighter",
78
- "5xl": "text-5xl leading-none tracking-tighter",
79
- "6xl": "text-6xl leading-none tracking-tighter"
78
+ "5xl": "text-5xl leading-[1.1] tracking-tighter",
79
+ "6xl": "text-6xl leading-[1.1] tracking-tighter"
80
80
  },
81
81
  weight: {
82
82
  normal: "font-normal",
@@ -1 +1 @@
1
- {"version":3,"file":"variants.js","sources":["../../src/lib/variants.ts"],"sourcesContent":["/**\n * @internal\n * Shared CVA variants used by menu and typography components.\n * Not exported publicly - use component props instead:\n * - Menu styling: <DropdownMenuItem variant=\"destructive\">\n * - Typography: <Text color=\"muted\" size=\"sm\">\n */\n\nimport { cva } from \"class-variance-authority\";\n\n/**\n * Shared menu item variants for DropdownMenuItem, ContextMenuItem, and MenubarItem.\n * Provides consistent styling across all menu-style components.\n */\nexport const menuItemVariants = cva(\n \"gap-2 rounded-sm px-2 py-1.5 text-sm [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0\",\n {\n variants: {\n variant: {\n default:\n \"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground\",\n destructive:\n \"text-destructive focus:bg-destructive/10 dark:focus:bg-destructive/20 focus:text-destructive *:[svg]:text-destructive\",\n success:\n \"text-success focus:bg-success/10 dark:focus:bg-success/20 focus:text-success *:[svg]:text-success\",\n warning:\n \"text-warning focus:bg-warning/10 dark:focus:bg-warning/20 focus:text-warning *:[svg]:text-warning\",\n info: \"text-info focus:bg-info/10 dark:focus:bg-info/20 focus:text-info *:[svg]:text-info\",\n },\n inset: {\n true: \"pl-8\",\n false: \"\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n inset: false,\n },\n },\n);\n\n/** Variant options for menu items: default, destructive, success, warning, info */\nexport type MenuItemVariant =\n | \"default\"\n | \"destructive\"\n | \"success\"\n | \"warning\"\n | \"info\";\n\n/**\n * Typography color variants shared across Title, Text, Code, and Blockquote components.\n * Provides semantic color options for consistent text styling.\n *\n * @example\n * ```tsx\n * import { typographyColorVariants } from \"@neynar/ui/lib/variants\"\n * import { cn } from \"@neynar/ui/lib/utils\"\n *\n * <span className={cn(typographyColorVariants({ color: \"muted\" }))}>\n * Secondary text\n * </span>\n * ```\n */\nexport const typographyColorVariants = cva(\"\", {\n variants: {\n color: {\n default: \"text-foreground\",\n muted: \"text-muted-foreground\",\n subtle: \"text-subtle-foreground\",\n destructive: \"text-destructive\",\n success: \"text-success\",\n warning: \"text-warning\",\n info: \"text-info\",\n },\n },\n defaultVariants: {\n color: \"default\",\n },\n});\n\n/** Semantic color options for typography: default, muted, subtle, destructive, success, warning, info */\nexport type TypographyColor =\n | \"default\"\n | \"muted\"\n | \"subtle\"\n | \"destructive\"\n | \"success\"\n | \"warning\"\n | \"info\";\n\n/**\n * Title order-based size defaults (only used when size prop is not specified).\n * Maps heading levels (h1-h6) to responsive font sizes.\n *\n * @example\n * ```tsx\n * // order: 1 → \"text-3xl md:text-4xl lg:text-5xl\"\n * // order: 6 → \"text-sm md:text-base lg:text-lg\"\n * ```\n */\nexport const titleOrderSizeVariants = cva(\"\", {\n variants: {\n order: {\n 1: \"text-3xl md:text-4xl lg:text-5xl\",\n 2: \"text-2xl md:text-3xl lg:text-4xl\",\n 3: \"text-xl md:text-2xl lg:text-3xl\",\n 4: \"text-lg md:text-xl lg:text-2xl\",\n 5: \"text-base md:text-lg lg:text-xl\",\n 6: \"text-sm md:text-base lg:text-lg\",\n },\n },\n});\n\n/**\n * Title order-based style defaults (weight, tracking, leading).\n * Provides appropriate font-weight and line-height for each heading level.\n *\n * @example\n * ```tsx\n * // order: 1 → \"font-bold tracking-tight leading-[1.1]\"\n * // order: 6 → \"font-medium leading-normal\"\n * ```\n */\nexport const titleOrderStyleVariants = cva(\"\", {\n variants: {\n order: {\n 1: \"font-bold tracking-tight leading-[1.1]\",\n 2: \"font-semibold tracking-tight leading-[1.2]\",\n 3: \"font-semibold tracking-tight leading-[1.25]\",\n 4: \"font-semibold leading-[1.3]\",\n 5: \"font-medium leading-[1.4]\",\n 6: \"font-medium leading-normal\",\n },\n },\n defaultVariants: {\n order: 2,\n },\n});\n\n/**\n * Title component variants for explicit size and weight overrides.\n * Use when you need to override the order-based defaults.\n *\n * @example\n * ```tsx\n * import { titleVariants } from \"@neynar/ui/lib/variants\"\n * import { cn } from \"@neynar/ui/lib/utils\"\n *\n * <h2 className={cn(titleVariants({ size: \"4xl\", weight: \"bold\" }))}>\n * Large Bold Title\n * </h2>\n * ```\n */\nexport const titleVariants = cva(\"\", {\n variants: {\n size: {\n xs: \"text-xs leading-4\",\n sm: \"text-sm leading-5\",\n base: \"text-base leading-6\",\n lg: \"text-lg leading-7\",\n xl: \"text-xl leading-7 tracking-tight\",\n \"2xl\": \"text-2xl leading-8 tracking-tight\",\n \"3xl\": \"text-3xl leading-9 tracking-tight\",\n \"4xl\": \"text-4xl leading-10 tracking-tighter\",\n \"5xl\": \"text-5xl leading-none tracking-tighter\",\n \"6xl\": \"text-6xl leading-none tracking-tighter\",\n },\n weight: {\n normal: \"font-normal\",\n medium: \"font-medium\",\n semibold: \"font-semibold\",\n bold: \"font-bold\",\n },\n },\n});\n\n/** Heading level 1-6, maps to h1-h6 elements */\nexport type TitleOrder = 1 | 2 | 3 | 4 | 5 | 6;\n\n/** Font size options from xs to 6xl */\nexport type TitleSize =\n | \"xs\"\n | \"sm\"\n | \"base\"\n | \"lg\"\n | \"xl\"\n | \"2xl\"\n | \"3xl\"\n | \"4xl\"\n | \"5xl\"\n | \"6xl\";\n\n/** Font weight options: normal, medium, semibold, bold */\nexport type TitleWeight = \"normal\" | \"medium\" | \"semibold\" | \"bold\";\n\n/**\n * Text component variants with auto-scaling line-height and letter-spacing.\n * Provides consistent paragraph and body text styling.\n *\n * @example\n * ```tsx\n * import { textVariants } from \"@neynar/ui/lib/variants\"\n * import { cn } from \"@neynar/ui/lib/utils\"\n *\n * <p className={cn(textVariants({ size: \"lg\", weight: \"medium\", align: \"center\" }))}>\n * Centered large text\n * </p>\n * ```\n */\nexport const textVariants = cva(\"\", {\n variants: {\n size: {\n xs: \"text-xs leading-4\",\n sm: \"text-sm leading-5\",\n base: \"text-base leading-6\",\n lg: \"text-lg leading-7\",\n xl: \"text-xl leading-7 tracking-tight\",\n \"2xl\": \"text-2xl leading-8 tracking-tighter\",\n },\n weight: {\n normal: \"font-normal\",\n medium: \"font-medium\",\n semibold: \"font-semibold\",\n bold: \"font-bold\",\n },\n align: {\n left: \"text-left\",\n center: \"text-center\",\n right: \"text-right\",\n },\n transform: {\n uppercase: \"uppercase tracking-wider\",\n lowercase: \"lowercase\",\n capitalize: \"capitalize\",\n },\n },\n defaultVariants: {\n size: \"base\",\n },\n});\n"],"names":[],"mappings":";AAcO,MAAM,mBAAmB;AAAA,EAC9B;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,SAAS;AAAA,QACP,SACE;AAAA,QACF,aACE;AAAA,QACF,SACE;AAAA,QACF,SACE;AAAA,QACF,MAAM;AAAA,MAAA;AAAA,MAER,OAAO;AAAA,QACL,MAAM;AAAA,QACN,OAAO;AAAA,MAAA;AAAA,IACT;AAAA,IAEF,iBAAiB;AAAA,MACf,SAAS;AAAA,MACT,OAAO;AAAA,IAAA;AAAA,EACT;AAEJ;AAwBO,MAAM,0BAA0B,IAAI,IAAI;AAAA,EAC7C,UAAU;AAAA,IACR,OAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,SAAS;AAAA,MACT,SAAS;AAAA,MACT,MAAM;AAAA,IAAA;AAAA,EACR;AAAA,EAEF,iBAAiB;AAAA,IACf,OAAO;AAAA,EAAA;AAEX,CAAC;AAsBM,MAAM,yBAAyB,IAAI,IAAI;AAAA,EAC5C,UAAU;AAAA,IACR,OAAO;AAAA,MACL,GAAG;AAAA,MACH,GAAG;AAAA,MACH,GAAG;AAAA,MACH,GAAG;AAAA,MACH,GAAG;AAAA,MACH,GAAG;AAAA,IAAA;AAAA,EACL;AAEJ,CAAC;AAYM,MAAM,0BAA0B,IAAI,IAAI;AAAA,EAC7C,UAAU;AAAA,IACR,OAAO;AAAA,MACL,GAAG;AAAA,MACH,GAAG;AAAA,MACH,GAAG;AAAA,MACH,GAAG;AAAA,MACH,GAAG;AAAA,MACH,GAAG;AAAA,IAAA;AAAA,EACL;AAAA,EAEF,iBAAiB;AAAA,IACf,OAAO;AAAA,EAAA;AAEX,CAAC;AAgBM,MAAM,gBAAgB,IAAI,IAAI;AAAA,EACnC,UAAU;AAAA,IACR,MAAM;AAAA,MACJ,IAAI;AAAA,MACJ,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,IAAI;AAAA,MACJ,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,OAAO;AAAA,MACP,OAAO;AAAA,MACP,OAAO;AAAA,MACP,OAAO;AAAA,IAAA;AAAA,IAET,QAAQ;AAAA,MACN,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,MAAM;AAAA,IAAA;AAAA,EACR;AAEJ,CAAC;AAmCM,MAAM,eAAe,IAAI,IAAI;AAAA,EAClC,UAAU;AAAA,IACR,MAAM;AAAA,MACJ,IAAI;AAAA,MACJ,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,IAAI;AAAA,MACJ,IAAI;AAAA,MACJ,OAAO;AAAA,IAAA;AAAA,IAET,QAAQ;AAAA,MACN,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,MAAM;AAAA,IAAA;AAAA,IAER,OAAO;AAAA,MACL,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,OAAO;AAAA,IAAA;AAAA,IAET,WAAW;AAAA,MACT,WAAW;AAAA,MACX,WAAW;AAAA,MACX,YAAY;AAAA,IAAA;AAAA,EACd;AAAA,EAEF,iBAAiB;AAAA,IACf,MAAM;AAAA,EAAA;AAEV,CAAC;"}
1
+ {"version":3,"file":"variants.js","sources":["../../src/lib/variants.ts"],"sourcesContent":["/**\n * @internal\n * Shared CVA variants used by menu and typography components.\n * Not exported publicly - use component props instead:\n * - Menu styling: <DropdownMenuItem variant=\"destructive\">\n * - Typography: <Text color=\"muted\" size=\"sm\">\n */\n\nimport { cva } from \"class-variance-authority\";\n\n/**\n * Shared menu item variants for DropdownMenuItem, ContextMenuItem, and MenubarItem.\n * Provides consistent styling across all menu-style components.\n */\nexport const menuItemVariants = cva(\n \"gap-2 rounded-sm px-2 py-1.5 text-sm [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0\",\n {\n variants: {\n variant: {\n default:\n \"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground\",\n destructive:\n \"text-destructive focus:bg-destructive/10 dark:focus:bg-destructive/20 focus:text-destructive *:[svg]:text-destructive\",\n success:\n \"text-success focus:bg-success/10 dark:focus:bg-success/20 focus:text-success *:[svg]:text-success\",\n warning:\n \"text-warning focus:bg-warning/10 dark:focus:bg-warning/20 focus:text-warning *:[svg]:text-warning\",\n info: \"text-info focus:bg-info/10 dark:focus:bg-info/20 focus:text-info *:[svg]:text-info\",\n },\n inset: {\n true: \"pl-8\",\n false: \"\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n inset: false,\n },\n },\n);\n\n/** Variant options for menu items: default, destructive, success, warning, info */\nexport type MenuItemVariant =\n | \"default\"\n | \"destructive\"\n | \"success\"\n | \"warning\"\n | \"info\";\n\n/**\n * Typography color variants shared across Title, Text, Code, and Blockquote components.\n * Provides semantic color options for consistent text styling.\n *\n * @example\n * ```tsx\n * import { typographyColorVariants } from \"@neynar/ui/lib/variants\"\n * import { cn } from \"@neynar/ui/lib/utils\"\n *\n * <span className={cn(typographyColorVariants({ color: \"muted\" }))}>\n * Secondary text\n * </span>\n * ```\n */\nexport const typographyColorVariants = cva(\"\", {\n variants: {\n color: {\n default: \"text-foreground\",\n muted: \"text-muted-foreground\",\n subtle: \"text-subtle-foreground\",\n destructive: \"text-destructive\",\n success: \"text-success\",\n warning: \"text-warning\",\n info: \"text-info\",\n },\n },\n defaultVariants: {\n color: \"default\",\n },\n});\n\n/** Semantic color options for typography: default, muted, subtle, destructive, success, warning, info */\nexport type TypographyColor =\n | \"default\"\n | \"muted\"\n | \"subtle\"\n | \"destructive\"\n | \"success\"\n | \"warning\"\n | \"info\";\n\n/**\n * Title order-based size defaults (only used when size prop is not specified).\n * Maps heading levels (h1-h6) to responsive font sizes.\n *\n * @example\n * ```tsx\n * // order: 1 → \"text-3xl md:text-4xl lg:text-5xl\"\n * // order: 6 → \"text-sm md:text-base lg:text-lg\"\n * ```\n */\nexport const titleOrderSizeVariants = cva(\"\", {\n variants: {\n order: {\n 1: \"text-3xl md:text-4xl lg:text-5xl\",\n 2: \"text-2xl md:text-3xl lg:text-4xl\",\n 3: \"text-xl md:text-2xl lg:text-3xl\",\n 4: \"text-lg md:text-xl lg:text-2xl\",\n 5: \"text-base md:text-lg lg:text-xl\",\n 6: \"text-sm md:text-base lg:text-lg\",\n },\n },\n});\n\n/**\n * Title order-based style defaults (weight, tracking, leading).\n * Provides appropriate font-weight and line-height for each heading level.\n *\n * @example\n * ```tsx\n * // order: 1 → \"font-bold tracking-tight leading-[1.1]\"\n * // order: 6 → \"font-medium leading-normal\"\n * ```\n */\nexport const titleOrderStyleVariants = cva(\"\", {\n variants: {\n order: {\n 1: \"font-bold tracking-tight leading-[1.1]\",\n 2: \"font-semibold tracking-tight leading-[1.2]\",\n 3: \"font-semibold tracking-tight leading-[1.25]\",\n 4: \"font-semibold leading-[1.3]\",\n 5: \"font-medium leading-[1.4]\",\n 6: \"font-medium leading-normal\",\n },\n },\n defaultVariants: {\n order: 2,\n },\n});\n\n/**\n * Title component variants for explicit size and weight overrides.\n * Use when you need to override the order-based defaults.\n *\n * @example\n * ```tsx\n * import { titleVariants } from \"@neynar/ui/lib/variants\"\n * import { cn } from \"@neynar/ui/lib/utils\"\n *\n * <h2 className={cn(titleVariants({ size: \"4xl\", weight: \"bold\" }))}>\n * Large Bold Title\n * </h2>\n * ```\n */\nexport const titleVariants = cva(\"\", {\n variants: {\n size: {\n xs: \"text-xs leading-4\",\n sm: \"text-sm leading-5\",\n base: \"text-base leading-6\",\n lg: \"text-lg leading-7\",\n xl: \"text-xl leading-7 tracking-tight\",\n \"2xl\": \"text-2xl leading-8 tracking-tight\",\n \"3xl\": \"text-3xl leading-9 tracking-tight\",\n \"4xl\": \"text-4xl leading-10 tracking-tighter\",\n \"5xl\": \"text-5xl leading-[1.1] tracking-tighter\",\n \"6xl\": \"text-6xl leading-[1.1] tracking-tighter\",\n },\n weight: {\n normal: \"font-normal\",\n medium: \"font-medium\",\n semibold: \"font-semibold\",\n bold: \"font-bold\",\n },\n },\n});\n\n/** Heading level 1-6, maps to h1-h6 elements */\nexport type TitleOrder = 1 | 2 | 3 | 4 | 5 | 6;\n\n/** Font size options from xs to 6xl */\nexport type TitleSize =\n | \"xs\"\n | \"sm\"\n | \"base\"\n | \"lg\"\n | \"xl\"\n | \"2xl\"\n | \"3xl\"\n | \"4xl\"\n | \"5xl\"\n | \"6xl\";\n\n/** Font weight options: normal, medium, semibold, bold */\nexport type TitleWeight = \"normal\" | \"medium\" | \"semibold\" | \"bold\";\n\n/**\n * Text component variants with auto-scaling line-height and letter-spacing.\n * Provides consistent paragraph and body text styling.\n *\n * @example\n * ```tsx\n * import { textVariants } from \"@neynar/ui/lib/variants\"\n * import { cn } from \"@neynar/ui/lib/utils\"\n *\n * <p className={cn(textVariants({ size: \"lg\", weight: \"medium\", align: \"center\" }))}>\n * Centered large text\n * </p>\n * ```\n */\nexport const textVariants = cva(\"\", {\n variants: {\n size: {\n xs: \"text-xs leading-4\",\n sm: \"text-sm leading-5\",\n base: \"text-base leading-6\",\n lg: \"text-lg leading-7\",\n xl: \"text-xl leading-7 tracking-tight\",\n \"2xl\": \"text-2xl leading-8 tracking-tighter\",\n },\n weight: {\n normal: \"font-normal\",\n medium: \"font-medium\",\n semibold: \"font-semibold\",\n bold: \"font-bold\",\n },\n align: {\n left: \"text-left\",\n center: \"text-center\",\n right: \"text-right\",\n },\n transform: {\n uppercase: \"uppercase tracking-wider\",\n lowercase: \"lowercase\",\n capitalize: \"capitalize\",\n },\n },\n defaultVariants: {\n size: \"base\",\n },\n});\n"],"names":[],"mappings":";AAcO,MAAM,mBAAmB;AAAA,EAC9B;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,SAAS;AAAA,QACP,SACE;AAAA,QACF,aACE;AAAA,QACF,SACE;AAAA,QACF,SACE;AAAA,QACF,MAAM;AAAA,MAAA;AAAA,MAER,OAAO;AAAA,QACL,MAAM;AAAA,QACN,OAAO;AAAA,MAAA;AAAA,IACT;AAAA,IAEF,iBAAiB;AAAA,MACf,SAAS;AAAA,MACT,OAAO;AAAA,IAAA;AAAA,EACT;AAEJ;AAwBO,MAAM,0BAA0B,IAAI,IAAI;AAAA,EAC7C,UAAU;AAAA,IACR,OAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,SAAS;AAAA,MACT,SAAS;AAAA,MACT,MAAM;AAAA,IAAA;AAAA,EACR;AAAA,EAEF,iBAAiB;AAAA,IACf,OAAO;AAAA,EAAA;AAEX,CAAC;AAsBM,MAAM,yBAAyB,IAAI,IAAI;AAAA,EAC5C,UAAU;AAAA,IACR,OAAO;AAAA,MACL,GAAG;AAAA,MACH,GAAG;AAAA,MACH,GAAG;AAAA,MACH,GAAG;AAAA,MACH,GAAG;AAAA,MACH,GAAG;AAAA,IAAA;AAAA,EACL;AAEJ,CAAC;AAYM,MAAM,0BAA0B,IAAI,IAAI;AAAA,EAC7C,UAAU;AAAA,IACR,OAAO;AAAA,MACL,GAAG;AAAA,MACH,GAAG;AAAA,MACH,GAAG;AAAA,MACH,GAAG;AAAA,MACH,GAAG;AAAA,MACH,GAAG;AAAA,IAAA;AAAA,EACL;AAAA,EAEF,iBAAiB;AAAA,IACf,OAAO;AAAA,EAAA;AAEX,CAAC;AAgBM,MAAM,gBAAgB,IAAI,IAAI;AAAA,EACnC,UAAU;AAAA,IACR,MAAM;AAAA,MACJ,IAAI;AAAA,MACJ,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,IAAI;AAAA,MACJ,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,OAAO;AAAA,MACP,OAAO;AAAA,MACP,OAAO;AAAA,MACP,OAAO;AAAA,IAAA;AAAA,IAET,QAAQ;AAAA,MACN,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,MAAM;AAAA,IAAA;AAAA,EACR;AAEJ,CAAC;AAmCM,MAAM,eAAe,IAAI,IAAI;AAAA,EAClC,UAAU;AAAA,IACR,MAAM;AAAA,MACJ,IAAI;AAAA,MACJ,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,IAAI;AAAA,MACJ,IAAI;AAAA,MACJ,OAAO;AAAA,IAAA;AAAA,IAET,QAAQ;AAAA,MACN,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,MAAM;AAAA,IAAA;AAAA,IAER,OAAO;AAAA,MACL,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,OAAO;AAAA,IAAA;AAAA,IAET,WAAW;AAAA,MACT,WAAW;AAAA,MACX,WAAW;AAAA,MACX,YAAY;AAAA,IAAA;AAAA,EACd;AAAA,EAEF,iBAAiB;AAAA,IACf,MAAM;AAAA,EAAA;AAEV,CAAC;"}
@@ -70,7 +70,11 @@ 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. |
73
75
  | onResize | (size: { asPercentage: number; inPixels: number }) => void | - | Called when panel is resized |
76
+ | onCollapse | () => void | - | Called when panel collapses |
77
+ | onExpand | () => void | - | Called when panel expands |
74
78
  | className | string | - | Additional CSS classes |
75
79
 
76
80
  ### ResizableHandle
@@ -222,6 +226,48 @@ function MonitoredPanels() {
222
226
  }
223
227
  ```
224
228
 
229
+ ### Animated Collapse/Expand
230
+
231
+ Use `collapsed` prop for declarative control and `animated` for smooth transitions. Drag resizing remains instant (no animation lag).
232
+
233
+ ```tsx
234
+ import { useState } from "react"
235
+ import {
236
+ ResizablePanelGroup,
237
+ ResizablePanel,
238
+ ResizableHandle,
239
+ } from "@neynar/ui/resizable"
240
+ import { Button } from "@neynar/ui/button"
241
+
242
+ function CollapsibleSidebar() {
243
+ const [isCollapsed, setIsCollapsed] = useState(false)
244
+
245
+ return (
246
+ <div className="h-[400px]">
247
+ <Button onClick={() => setIsCollapsed(!isCollapsed)}>
248
+ {isCollapsed ? "Expand" : "Collapse"} Sidebar
249
+ </Button>
250
+ <ResizablePanelGroup orientation="horizontal">
251
+ <ResizablePanel
252
+ collapsed={isCollapsed}
253
+ animated
254
+ defaultSize="25%"
255
+ minSize="15%"
256
+ collapsible
257
+ collapsedSize="0%"
258
+ >
259
+ <div className="p-4">Sidebar content</div>
260
+ </ResizablePanel>
261
+ <ResizableHandle withHandle />
262
+ <ResizablePanel>
263
+ <div className="p-4">Main content</div>
264
+ </ResizablePanel>
265
+ </ResizablePanelGroup>
266
+ </div>
267
+ )
268
+ }
269
+ ```
270
+
225
271
  ## Keyboard
226
272
 
227
273
  | Key | Action |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neynar/ui",
3
- "version": "1.0.6",
3
+ "version": "1.2.0",
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.",
@@ -272,7 +272,7 @@
272
272
  "clean": "rm -rf dist",
273
273
  "docs:generate": "tsx scripts/llm-tsdoc-generator/index.ts llm-tsdoc-generator.config.json",
274
274
  "storybook": "storybook dev --no-open -p 6006",
275
- "build-storybook": "storybook build"
275
+ "build-storybook": "STORYBOOK_BASE_PATH=/ui/ storybook build"
276
276
  },
277
277
  "devDependencies": {
278
278
  "@eslint/js": "^9.39.2",
@@ -331,6 +331,41 @@ export const Variants: Story = {
331
331
  ),
332
332
  };
333
333
 
334
+ /**
335
+ * Large multi-line titles demonstrating proper line-height for 5xl and 6xl sizes.
336
+ * These sizes use leading-[1.1] to prevent text overlap on wrapped lines.
337
+ */
338
+ export const LargeMultiLine: Story = {
339
+ parameters: {
340
+ layout: "padded",
341
+ },
342
+ render: () => (
343
+ <div className="w-full max-w-2xl space-y-8">
344
+ <section className="space-y-4">
345
+ <div>
346
+ <h3 className="text-lg font-semibold">
347
+ Large Sizes with Multi-line Text
348
+ </h3>
349
+ <p className="text-muted-foreground text-sm">
350
+ 5xl and 6xl sizes with proper line-height to prevent text overlap.
351
+ </p>
352
+ </div>
353
+ <div className="space-y-6 border rounded-lg p-6">
354
+ <Title order={1} size="6xl">
355
+ Building the Future of Social Networks Together
356
+ </Title>
357
+ <Title order={1} size="5xl">
358
+ Decentralized Identity for the Next Generation of Apps
359
+ </Title>
360
+ <Title order={1} size="4xl">
361
+ Create, Connect, and Collaborate on Farcaster
362
+ </Title>
363
+ </div>
364
+ </section>
365
+ </div>
366
+ ),
367
+ };
368
+
334
369
  /**
335
370
  * Interactive playground for testing Title props.
336
371
  */
@@ -5,10 +5,19 @@ 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
+ /** Duration of the collapse/expand animation in milliseconds. */
14
+ const TRANSITION_DURATION = 300;
15
+
16
+ /** CSS transition value for animated collapse/expand. */
17
+ const TRANSITION_STYLE =
18
+ "flex-grow 0.3s cubic-bezier(0.16, 1, 0.3, 1), flex-basis 0.3s cubic-bezier(0.16, 1, 0.3, 1)";
19
+
20
+
12
21
  type ResizablePanelGroupProps = React.ComponentProps<typeof PanelGroup>;
13
22
 
14
23
  /**
@@ -31,14 +40,98 @@ function ResizablePanelGroup({
31
40
  );
32
41
  }
33
42
 
34
- type ResizablePanelProps = React.ComponentProps<typeof Panel>;
43
+ type ResizablePanelProps = Omit<
44
+ React.ComponentProps<typeof Panel>,
45
+ "panelRef"
46
+ > & {
47
+ /** Enable smooth CSS transition for collapse/expand. Drag resizing stays instant. @default false */
48
+ animated?: boolean;
49
+ /** Controlled collapsed state. When provided, the panel syncs to this value. */
50
+ collapsed?: boolean;
51
+ };
35
52
 
36
53
  /**
37
54
  * Individual resizable panel within a ResizablePanelGroup.
38
55
  * Supports size constraints (minSize, maxSize), default sizing, and collapse behavior.
56
+ *
57
+ * Control collapse state declaratively via the `collapsed` prop.
58
+ * When `animated` is true, collapse/expand transitions are smooth.
59
+ *
60
+ * @example
61
+ * <ResizablePanel collapsed={isCollapsed} animated collapsible>
39
62
  */
40
- function ResizablePanel({ ...props }: ResizablePanelProps) {
41
- return <Panel data-slot="resizable-panel" {...props} />;
63
+ function ResizablePanel({
64
+ className,
65
+ animated,
66
+ collapsed,
67
+ ...props
68
+ }: ResizablePanelProps) {
69
+ const panelRef = React.useRef<PanelImperativeHandle>(null);
70
+ const elementRef = React.useRef<HTMLDivElement>(null);
71
+ const animatedRef = React.useRef(animated);
72
+ const isFirstRender = React.useRef(true);
73
+
74
+ // Keep the ref in sync with prop changes
75
+ React.useEffect(() => {
76
+ animatedRef.current = animated;
77
+ }, [animated]);
78
+
79
+ /** Apply transition, call action, then remove transition after duration */
80
+ const withTransition = React.useCallback(
81
+ (action: () => void, skipAnimation = false) => {
82
+ const panelEl = elementRef.current?.closest(
83
+ '[data-panel][data-slot="resizable-panel"]',
84
+ ) as HTMLElement | null;
85
+
86
+ if (animatedRef.current && panelEl && !skipAnimation) {
87
+ panelEl.style.transition = TRANSITION_STYLE;
88
+ action();
89
+ setTimeout(() => {
90
+ panelEl.style.transition = "";
91
+ }, TRANSITION_DURATION);
92
+ } else {
93
+ action();
94
+ }
95
+ },
96
+ [],
97
+ );
98
+
99
+ // Sync collapsed prop to panel state
100
+ React.useEffect(() => {
101
+ if (collapsed === undefined) return;
102
+
103
+ // On first render, defer to allow panel registration
104
+ if (isFirstRender.current) {
105
+ isFirstRender.current = false;
106
+ // Use requestAnimationFrame to ensure panel is registered
107
+ requestAnimationFrame(() => {
108
+ if (collapsed) {
109
+ panelRef.current?.collapse();
110
+ }
111
+ });
112
+ return;
113
+ }
114
+
115
+ // After first render, check current state before acting
116
+ const isCurrentlyCollapsed = panelRef.current?.isCollapsed() ?? false;
117
+ if (collapsed === isCurrentlyCollapsed) return;
118
+
119
+ if (collapsed) {
120
+ withTransition(() => panelRef.current?.collapse());
121
+ } else {
122
+ withTransition(() => panelRef.current?.expand());
123
+ }
124
+ }, [collapsed, withTransition]);
125
+
126
+ return (
127
+ <Panel
128
+ data-slot="resizable-panel"
129
+ panelRef={panelRef}
130
+ elementRef={elementRef}
131
+ className={cn(className)}
132
+ {...props}
133
+ />
134
+ );
42
135
  }
43
136
 
44
137
  type ResizableHandleProps = React.ComponentProps<typeof PanelResizeHandle> & {
@@ -59,13 +152,13 @@ function ResizableHandle({
59
152
  <PanelResizeHandle
60
153
  data-slot="resizable-handle"
61
154
  className={cn(
62
- "group bg-border focus-visible:ring-primary 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-[resize-handle-state=hover]:bg-primary data-[resize-handle-state=drag]:bg-primary 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",
155
+ "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",
63
156
  className,
64
157
  )}
65
158
  {...props}
66
159
  >
67
160
  {withHandle && (
68
- <div className="bg-border group-data-[resize-handle-state=hover]:bg-primary group-data-[resize-handle-state=drag]:bg-primary h-6 w-1 rounded-lg z-10 flex shrink-0" />
161
+ <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" />
69
162
  )}
70
163
  </PanelResizeHandle>
71
164
  );
@@ -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.
@@ -162,8 +162,8 @@ export const titleVariants = cva("", {
162
162
  "2xl": "text-2xl leading-8 tracking-tight",
163
163
  "3xl": "text-3xl leading-9 tracking-tight",
164
164
  "4xl": "text-4xl leading-10 tracking-tighter",
165
- "5xl": "text-5xl leading-none tracking-tighter",
166
- "6xl": "text-6xl leading-none tracking-tighter",
165
+ "5xl": "text-5xl leading-[1.1] tracking-tighter",
166
+ "6xl": "text-6xl leading-[1.1] tracking-tighter",
167
167
  },
168
168
  weight: {
169
169
  normal: "font-normal",