harebear-ui 0.4.0 → 0.6.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.
@@ -1,3 +1,4 @@
1
+ import { SidebarLayoutProps } from "./sidebar-layout.js";
1
2
  import * as React from "react";
2
3
  export interface AppNavItem {
3
4
  label: string;
@@ -29,22 +30,28 @@ declare function AppBrand({ name, subtitle, icon, action, className, ...props }:
29
30
  export interface AppShellProps {
30
31
  /** Sidebar header content, usually <AppBrand />. */
31
32
  brand?: React.ReactNode;
33
+ /** Under the brand, e.g. a primary "New" button. */
34
+ sidebarActions?: React.ReactNode;
32
35
  nav?: AppNavSection[];
33
- /** Extra sidebar content under the nav (e.g. a usage card). */
36
+ /** Extra sidebar content under the nav (e.g. a usage card). Hidden in the icon rail. */
34
37
  sidebarExtra?: React.ReactNode;
35
- /** Sidebar bottom content, usually <UserChip />. */
38
+ /** Sidebar bottom content, usually <UserMenu />. */
36
39
  sidebarFooter?: React.ReactNode;
37
40
  /** Top bar content inside the main area (search, actions). */
38
41
  header?: React.ReactNode;
39
42
  /** Component used for nav links. Default "a". Pass Next.js Link or React Router Link. */
40
43
  linkComponent?: React.ElementType;
44
+ /** How the sidebar collapses on desktop. Default "icon": a collapse button shrinks it to an icon rail. */
45
+ collapsible?: SidebarLayoutProps["collapsible"];
46
+ /** Remember the sidebar's expanded or collapsed state in localStorage under this key. */
47
+ storageKey?: string;
41
48
  defaultOpen?: boolean;
42
49
  className?: string;
43
50
  children?: React.ReactNode;
44
51
  }
45
52
  /**
46
- * Full admin layout: collapsible sidebar (sheet on mobile) + main column.
47
- * Built on the Sidebar primitives; drop to those for anything custom.
53
+ * Full admin layout on SidebarLayout: nav sidebar that collapses to an icon rail (a sheet on mobile) + main column.
54
+ * Nav items show their label as a tooltip in the rail. Drop to SidebarLayout or the Sidebar primitives for anything custom.
48
55
  */
49
- declare function AppShell({ brand, nav, sidebarExtra, sidebarFooter, header, linkComponent: Link, defaultOpen, className, children }: AppShellProps): React.JSX.Element;
56
+ declare function AppShell({ brand, sidebarActions, nav, sidebarExtra, sidebarFooter, header, linkComponent: Link, collapsible, storageKey, defaultOpen, className, children }: AppShellProps): React.JSX.Element;
50
57
  export { AppBrand, AppShell };
@@ -1,6 +1,7 @@
1
1
  "use client";
2
+ import { SidebarLayout } from "./sidebar-layout.js";
2
3
  import { Badge } from "../ui/badge.js";
3
- import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarHeader, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider, SidebarTrigger } from "../ui/sidebar.js";
4
+ import { SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarTrigger } from "../ui/sidebar.js";
4
5
  import { cn } from "../../lib/utils.js";
5
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
7
  // src/components/admin/app-shell.tsx
@@ -36,75 +37,70 @@ function AppBrand({ name, subtitle, icon, action, className, ...props }) {
36
37
  });
37
38
  }
38
39
  /**
39
- * Full admin layout: collapsible sidebar (sheet on mobile) + main column.
40
- * Built on the Sidebar primitives; drop to those for anything custom.
40
+ * Full admin layout on SidebarLayout: nav sidebar that collapses to an icon rail (a sheet on mobile) + main column.
41
+ * Nav items show their label as a tooltip in the rail. Drop to SidebarLayout or the Sidebar primitives for anything custom.
41
42
  */
42
- function AppShell({ brand, nav = [], sidebarExtra, sidebarFooter, header, linkComponent: Link = "a", defaultOpen = true, className, children }) {
43
- return /* @__PURE__ */ jsxs(SidebarProvider, {
43
+ function AppShell({ brand, sidebarActions, nav = [], sidebarExtra, sidebarFooter, header, linkComponent: Link = "a", collapsible, storageKey, defaultOpen = true, className, children }) {
44
+ return /* @__PURE__ */ jsxs(SidebarLayout, {
45
+ brand,
46
+ sidebarActions,
47
+ sidebarFooter,
48
+ collapsible,
49
+ storageKey,
44
50
  defaultOpen,
45
- style: { "--sidebar-width": "15.75rem" },
46
- children: [/* @__PURE__ */ jsxs(Sidebar, { children: [
47
- brand && /* @__PURE__ */ jsx(SidebarHeader, {
48
- className: "px-4 pt-6 pb-2",
49
- children: brand
50
- }),
51
- /* @__PURE__ */ jsxs(SidebarContent, {
52
- className: "gap-5 px-2",
53
- children: [nav.map((section, i) => /* @__PURE__ */ jsxs(SidebarGroup, { children: [section.label && /* @__PURE__ */ jsx(SidebarGroupLabel, {
54
- className: "text-xs font-medium",
55
- children: section.label
56
- }), /* @__PURE__ */ jsx(SidebarMenu, {
57
- className: "gap-1",
58
- children: section.items.map((item) => /* @__PURE__ */ jsx(SidebarMenuItem, { children: /* @__PURE__ */ jsx(SidebarMenuButton, {
59
- asChild: !!item.href,
60
- isActive: item.active,
61
- onClick: item.onSelect,
62
- children: item.href ? /* @__PURE__ */ jsxs(Link, {
63
- href: item.href,
64
- children: [
65
- item.icon,
66
- /* @__PURE__ */ jsx("span", {
67
- className: "flex-1",
68
- children: item.label
69
- }),
70
- item.badge !== void 0 && /* @__PURE__ */ jsx(NavBadge, { children: item.badge })
71
- ]
72
- }) : /* @__PURE__ */ jsxs(Fragment, { children: [
51
+ width: "15.75rem",
52
+ className,
53
+ sidebar: /* @__PURE__ */ jsxs("div", {
54
+ className: "flex flex-1 flex-col gap-5",
55
+ children: [nav.map((section, i) => /* @__PURE__ */ jsxs(SidebarGroup, { children: [section.label && /* @__PURE__ */ jsx(SidebarGroupLabel, {
56
+ className: "text-xs font-medium",
57
+ children: section.label
58
+ }), /* @__PURE__ */ jsx(SidebarMenu, {
59
+ className: "gap-1 group-data-[collapsible=icon]:items-center",
60
+ children: section.items.map((item) => /* @__PURE__ */ jsx(SidebarMenuItem, { children: /* @__PURE__ */ jsx(SidebarMenuButton, {
61
+ asChild: !!item.href,
62
+ isActive: item.active,
63
+ onClick: item.onSelect,
64
+ tooltip: item.label,
65
+ children: item.href ? /* @__PURE__ */ jsxs(Link, {
66
+ href: item.href,
67
+ children: [
73
68
  item.icon,
74
69
  /* @__PURE__ */ jsx("span", {
75
70
  className: "flex-1",
76
71
  children: item.label
77
72
  }),
78
73
  item.badge !== void 0 && /* @__PURE__ */ jsx(NavBadge, { children: item.badge })
79
- ] })
80
- }) }, item.label))
81
- })] }, section.label ?? i)), sidebarExtra && /* @__PURE__ */ jsx("div", {
82
- className: "mt-auto px-2",
83
- children: sidebarExtra
84
- })]
85
- }),
86
- sidebarFooter && /* @__PURE__ */ jsx(SidebarFooter, {
87
- className: "px-4 pb-6",
88
- children: sidebarFooter
89
- })
90
- ] }), /* @__PURE__ */ jsxs(SidebarInset, {
91
- className: cn("min-w-0", className),
92
- children: [header !== void 0 && /* @__PURE__ */ jsxs("header", {
93
- "data-slot": "app-header",
94
- className: "flex flex-wrap items-center gap-3 px-4 pt-5 md:px-8",
95
- children: [/* @__PURE__ */ jsx(SidebarTrigger, { className: "md:hidden" }), header]
96
- }), /* @__PURE__ */ jsx("div", {
97
- "data-slot": "app-content",
98
- className: "flex min-w-0 flex-col gap-7 px-4 pt-7 pb-12 md:px-8",
99
- children
74
+ ]
75
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [
76
+ item.icon,
77
+ /* @__PURE__ */ jsx("span", {
78
+ className: "flex-1",
79
+ children: item.label
80
+ }),
81
+ item.badge !== void 0 && /* @__PURE__ */ jsx(NavBadge, { children: item.badge })
82
+ ] })
83
+ }) }, item.label))
84
+ })] }, section.label ?? i)), sidebarExtra && /* @__PURE__ */ jsx("div", {
85
+ className: "mt-auto px-2 group-data-[collapsible=icon]:hidden",
86
+ children: sidebarExtra
100
87
  })]
88
+ }),
89
+ children: [header !== void 0 && /* @__PURE__ */ jsxs("header", {
90
+ "data-slot": "app-header",
91
+ className: "flex flex-wrap items-center gap-3 px-4 pt-5 md:px-8",
92
+ children: [/* @__PURE__ */ jsx(SidebarTrigger, { className: "md:hidden" }), header]
93
+ }), /* @__PURE__ */ jsx("div", {
94
+ "data-slot": "app-content",
95
+ className: "flex min-w-0 flex-col gap-7 px-4 pt-7 pb-12 md:px-8",
96
+ children
101
97
  })]
102
98
  });
103
99
  }
104
100
  function NavBadge({ children }) {
105
101
  return /* @__PURE__ */ jsx(Badge, {
106
102
  variant: "soft",
107
- className: "h-5 px-2",
103
+ className: "h-5 px-2 group-data-[collapsible=icon]:hidden",
108
104
  children
109
105
  });
110
106
  }
@@ -0,0 +1,41 @@
1
+ import * as React from "react";
2
+ export interface SidebarLayoutProps {
3
+ /** Sidebar top, usually <AppBrand />. Collapses to its logo tile in the icon rail. */
4
+ brand?: React.ReactNode;
5
+ /** Under the brand, e.g. <NewConversationButton /> or a search box. Should cope with the icon rail. */
6
+ sidebarActions?: React.ReactNode;
7
+ /** Scrolling sidebar body: nav menus, <ConversationList />… */
8
+ sidebar?: React.ReactNode;
9
+ /** Sidebar bottom, usually <UserMenu />. */
10
+ sidebarFooter?: React.ReactNode;
11
+ /**
12
+ * - "icon" (default): a collapse button shrinks the sidebar to an icon rail on desktop.
13
+ * - "offcanvas": the sidebar can be hidden entirely (toggle it with a SidebarTrigger).
14
+ * - "none": always expanded on desktop.
15
+ * On mobile the sidebar is always a sheet opened with a SidebarTrigger.
16
+ */
17
+ collapsible?: "icon" | "offcanvas" | "none";
18
+ /** Remember the expanded or collapsed state in localStorage under this key. */
19
+ storageKey?: string;
20
+ /** Expanded at first (when nothing is stored). Default true. */
21
+ defaultOpen?: boolean;
22
+ /** Expanded sidebar width. Default "16rem". The icon rail is 4.25rem. */
23
+ width?: string;
24
+ /** Classes for the main column (a SidebarInset). */
25
+ className?: string;
26
+ /** The main column. */
27
+ children?: React.ReactNode;
28
+ }
29
+ /**
30
+ * The base app layout: a sidebar (brand, actions, body, footer) that collapses to an icon rail on desktop and is a
31
+ * sheet on mobile, next to the main column. AppShell and ChatShell are built on it; use it directly for other layouts.
32
+ */
33
+ declare function SidebarLayout({ brand, sidebarActions, sidebar, sidebarFooter, collapsible, storageKey, defaultOpen, width, className, children }: SidebarLayoutProps): React.JSX.Element;
34
+ /**
35
+ * Collapse / expand control for a desktop sidebar with an icon rail, with a tooltip. Renders nothing on mobile,
36
+ * where the sidebar is a sheet. SidebarLayout includes one; use it in custom Sidebar layouts.
37
+ */
38
+ declare function SidebarCollapseButton({ className }: {
39
+ className?: string;
40
+ }): React.JSX.Element | null;
41
+ export { SidebarCollapseButton, SidebarLayout };
@@ -0,0 +1,104 @@
1
+ "use client";
2
+ import { Button } from "../ui/button.js";
3
+ import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarInset, SidebarProvider, useSidebar } from "../ui/sidebar.js";
4
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip.js";
5
+ import { cn } from "../../lib/utils.js";
6
+ import * as React from "react";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ import { PanelLeftCloseIcon, PanelLeftOpenIcon } from "lucide-react";
9
+ // src/components/admin/sidebar-layout.tsx
10
+ function readStored(key) {
11
+ try {
12
+ const v = window.localStorage.getItem(key);
13
+ return v === null ? null : v !== "collapsed";
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+ const useIsomorphicLayoutEffect = typeof window === "undefined" ? React.useEffect : React.useLayoutEffect;
19
+ /**
20
+ * The base app layout: a sidebar (brand, actions, body, footer) that collapses to an icon rail on desktop and is a
21
+ * sheet on mobile, next to the main column. AppShell and ChatShell are built on it; use it directly for other layouts.
22
+ */
23
+ function SidebarLayout({ brand, sidebarActions, sidebar, sidebarFooter, collapsible = "icon", storageKey, defaultOpen = true, width = "16rem", className, children }) {
24
+ const [open, setOpen] = React.useState(defaultOpen);
25
+ useIsomorphicLayoutEffect(() => {
26
+ if (!storageKey) return;
27
+ const stored = readStored(storageKey);
28
+ if (stored !== null) setOpen(stored);
29
+ }, [storageKey]);
30
+ const onOpenChange = (next) => {
31
+ setOpen(next);
32
+ if (!storageKey) return;
33
+ try {
34
+ window.localStorage.setItem(storageKey, next ? "open" : "collapsed");
35
+ } catch {}
36
+ };
37
+ return /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(SidebarProvider, {
38
+ open: collapsible === "none" ? true : open,
39
+ onOpenChange,
40
+ style: {
41
+ "--sidebar-width": width,
42
+ "--sidebar-width-icon": "4.25rem"
43
+ },
44
+ children: [/* @__PURE__ */ jsxs(Sidebar, {
45
+ collapsible: collapsible === "none" ? "offcanvas" : collapsible,
46
+ "data-slot": "sidebar-layout-sidebar",
47
+ children: [
48
+ /* @__PURE__ */ jsxs(SidebarHeader, {
49
+ className: "gap-5 px-3.5 pt-[max(1.25rem,env(safe-area-inset-top))] group-data-[collapsible=icon]:items-center group-data-[collapsible=icon]:gap-3 group-data-[collapsible=icon]:px-0",
50
+ children: [/* @__PURE__ */ jsxs("div", {
51
+ className: "flex min-w-0 items-center gap-2 group-data-[collapsible=icon]:flex-col group-data-[collapsible=icon]:gap-3",
52
+ children: [brand && /* @__PURE__ */ jsx("div", {
53
+ className: "min-w-0 flex-1 pl-2 group-data-[collapsible=icon]:pl-0",
54
+ children: brand
55
+ }), collapsible === "icon" && /* @__PURE__ */ jsx(SidebarCollapseButton, {})]
56
+ }), sidebarActions && /* @__PURE__ */ jsx("div", {
57
+ className: "flex flex-col gap-2.5 group-data-[collapsible=icon]:items-center",
58
+ children: sidebarActions
59
+ })]
60
+ }),
61
+ /* @__PURE__ */ jsx(SidebarContent, {
62
+ className: "px-3.5 pt-2 group-data-[collapsible=icon]:px-0",
63
+ children: sidebar
64
+ }),
65
+ sidebarFooter && /* @__PURE__ */ jsx(SidebarFooter, {
66
+ className: "px-3.5 pb-[max(1.25rem,env(safe-area-inset-bottom))] group-data-[collapsible=icon]:px-0",
67
+ children: /* @__PURE__ */ jsx("div", {
68
+ className: "px-2 group-data-[collapsible=icon]:px-0 has-data-[slot=user-menu-trigger]:px-0",
69
+ children: sidebarFooter
70
+ })
71
+ })
72
+ ]
73
+ }), /* @__PURE__ */ jsx(SidebarInset, {
74
+ "data-slot": "sidebar-layout",
75
+ className: cn("min-w-0", className),
76
+ children
77
+ })]
78
+ }) });
79
+ }
80
+ /**
81
+ * Collapse / expand control for a desktop sidebar with an icon rail, with a tooltip. Renders nothing on mobile,
82
+ * where the sidebar is a sheet. SidebarLayout includes one; use it in custom Sidebar layouts.
83
+ */
84
+ function SidebarCollapseButton({ className }) {
85
+ const { state, isMobile, toggleSidebar } = useSidebar();
86
+ if (isMobile) return null;
87
+ const label = state === "collapsed" ? "Expand sidebar" : "Collapse sidebar";
88
+ return /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
89
+ asChild: true,
90
+ children: /* @__PURE__ */ jsx(Button, {
91
+ "data-slot": "sidebar-collapse-button",
92
+ variant: "ghost",
93
+ size: state === "collapsed" ? "icon" : "icon-sm",
94
+ "aria-label": label,
95
+ onClick: toggleSidebar,
96
+ className,
97
+ children: state === "collapsed" ? /* @__PURE__ */ jsx(PanelLeftOpenIcon, {}) : /* @__PURE__ */ jsx(PanelLeftCloseIcon, {})
98
+ })
99
+ }), /* @__PURE__ */ jsx(TooltipContent, {
100
+ side: "right",
101
+ children: label
102
+ })] });
103
+ }
104
+ export { SidebarCollapseButton, SidebarLayout };
@@ -12,7 +12,9 @@ export interface StatusBadgeProps extends React.ComponentProps<typeof Badge> {
12
12
  tone?: StatusTone;
13
13
  /** Pulse the dot. */
14
14
  pulse?: boolean;
15
+ /** Below 640px show only the dot; the label stays for screen readers. For crowded mobile headers. */
16
+ compact?: boolean;
15
17
  }
16
18
  /** Pill with a status dot and label, e.g. Paid / Pending / Refunded. `size="lg"` lines up with sm buttons. */
17
- declare function StatusBadge({ tone, pulse, variant, children, className, ...props }: StatusBadgeProps): React.JSX.Element;
19
+ declare function StatusBadge({ tone, pulse, compact, variant, children, className, ...props }: StatusBadgeProps): React.JSX.Element;
18
20
  export { StatusBadge, StatusDot };
@@ -20,7 +20,7 @@ function StatusDot({ tone = "neutral", pulse, className, ...props }) {
20
20
  });
21
21
  }
22
22
  /** Pill with a status dot and label, e.g. Paid / Pending / Refunded. `size="lg"` lines up with sm buttons. */
23
- function StatusBadge({ tone = "neutral", pulse, variant = "secondary", children, className, ...props }) {
23
+ function StatusBadge({ tone = "neutral", pulse, compact, variant = "secondary", children, className, ...props }) {
24
24
  return /* @__PURE__ */ jsxs(Badge, {
25
25
  "data-slot": "status-badge",
26
26
  variant,
@@ -29,7 +29,10 @@ function StatusBadge({ tone = "neutral", pulse, variant = "secondary", children,
29
29
  children: [/* @__PURE__ */ jsx(StatusDot, {
30
30
  tone,
31
31
  pulse
32
- }), children]
32
+ }), compact ? /* @__PURE__ */ jsx("span", {
33
+ className: "sr-only sm:not-sr-only",
34
+ children
35
+ }) : children]
33
36
  });
34
37
  }
35
38
  export { StatusBadge, StatusDot };
@@ -14,12 +14,14 @@ export interface ApprovalCardProps extends Omit<React.ComponentProps<"section">,
14
14
  actions?: React.ReactNode;
15
15
  /** Small print under the actions. */
16
16
  footer?: React.ReactNode;
17
+ /** Why the agent wants this, shown first in the body after a bold "Why:". */
18
+ reason?: React.ReactNode;
17
19
  }
18
20
  /**
19
21
  * Highlighted card for anything the agent is waiting on the user for: a tool approval, a question, a choice.
20
22
  * Body text goes in children (e.g. the agent's reason).
21
23
  */
22
- declare function ApprovalCard({ title, description, icon, code, codeLabel, actions, footer, className, children, ...props }: ApprovalCardProps): React.JSX.Element;
24
+ declare function ApprovalCard({ title, description, icon, code, codeLabel, actions, footer, reason, className, children, ...props }: ApprovalCardProps): React.JSX.Element;
23
25
  export interface ApprovalResultProps extends React.ComponentProps<"div"> {
24
26
  decision: "approved" | "denied";
25
27
  /** Trailing time, e.g. "10:42". */
@@ -9,7 +9,7 @@ import { CheckIcon, ShieldAlertIcon, XIcon } from "lucide-react";
9
9
  * Highlighted card for anything the agent is waiting on the user for: a tool approval, a question, a choice.
10
10
  * Body text goes in children (e.g. the agent's reason).
11
11
  */
12
- function ApprovalCard({ title, description, icon = /* @__PURE__ */ jsx(ShieldAlertIcon, {}), code, codeLabel, actions, footer, className, children, ...props }) {
12
+ function ApprovalCard({ title, description, icon = /* @__PURE__ */ jsx(ShieldAlertIcon, {}), code, codeLabel, actions, footer, reason, className, children, ...props }) {
13
13
  const id = React.useId();
14
14
  return /* @__PURE__ */ jsxs("section", {
15
15
  "data-slot": "approval-card",
@@ -34,9 +34,12 @@ function ApprovalCard({ title, description, icon = /* @__PURE__ */ jsx(ShieldAle
34
34
  })]
35
35
  })]
36
36
  }),
37
- children && /* @__PURE__ */ jsx("div", {
37
+ (reason || children) && /* @__PURE__ */ jsxs("div", {
38
38
  className: "text-[14.5px] leading-relaxed whitespace-pre-wrap wrap-anywhere [&_strong]:font-semibold",
39
- children
39
+ children: [reason && /* @__PURE__ */ jsxs("p", { children: [/* @__PURE__ */ jsx("strong", { children: "Why: " }), reason] }), reason && children ? /* @__PURE__ */ jsx("div", {
40
+ className: "mt-2",
41
+ children
42
+ }) : children]
40
43
  }),
41
44
  code !== void 0 && /* @__PURE__ */ jsxs(CodeBlock, { children: [/* @__PURE__ */ jsxs(CodeBlockHeader, { children: [/* @__PURE__ */ jsx(CodeBlockTitle, { children: codeLabel ?? "command" }), /* @__PURE__ */ jsx(CodeBlockActions, { children: /* @__PURE__ */ jsx(CodeBlockCopyButton, { value: code }) })] }), /* @__PURE__ */ jsx(CodeBlockContent, {
42
45
  wrap: true,
@@ -1,3 +1,4 @@
1
+ import { SidebarLayoutProps } from "../admin/sidebar-layout.js";
1
2
  import * as React from "react";
2
3
  export interface ChatShellProps {
3
4
  /** Sidebar top, usually <AppBrand />. Collapses to its logo tile in the icon rail. */
@@ -14,6 +15,8 @@ export interface ChatShellProps {
14
15
  detailsTitle?: string;
15
16
  /** Show the details panel at first on wide screens. Default true. */
16
17
  defaultDetailsOpen?: boolean;
18
+ /** How the sidebar collapses on desktop. Default "icon" (an icon rail). See SidebarLayout. */
19
+ collapsible?: SidebarLayoutProps["collapsible"];
17
20
  /** Remember the sidebar's expanded or collapsed state in localStorage under this key. */
18
21
  storageKey?: string;
19
22
  /** Sidebar expanded at first (when nothing is stored). Default true. */
@@ -23,10 +26,10 @@ export interface ChatShellProps {
23
26
  children: React.ReactNode;
24
27
  }
25
28
  /**
26
- * Full-height chat app layout: a sidebar that collapses to an icon rail (a sheet on mobile), the main chat
27
- * column, and an optional details panel. Put a ChatPanel inside.
29
+ * Full-height chat app layout: SidebarLayout (sidebar with an icon rail, a sheet on mobile) around the chat
30
+ * column, plus an optional details panel. Put a ChatPanel inside.
28
31
  */
29
- declare function ChatShell({ brand, sidebarActions, sidebar, sidebarFooter, details, detailsTitle, defaultDetailsOpen, storageKey, defaultOpen, className, children }: ChatShellProps): React.JSX.Element;
32
+ declare function ChatShell({ brand, sidebarActions, sidebar, sidebarFooter, details, detailsTitle, defaultDetailsOpen, collapsible, storageKey, defaultOpen, className, children }: ChatShellProps): React.JSX.Element;
30
33
  export interface ChatPanelProps extends Omit<React.ComponentProps<"section">, "title"> {
31
34
  /** Conversation title. */
32
35
  title?: React.ReactNode;
@@ -1,45 +1,23 @@
1
1
  "use client";
2
+ import { SidebarLayout } from "../admin/sidebar-layout.js";
2
3
  import { ChatThread } from "./chat.js";
3
4
  import { Alert, AlertDescription } from "../ui/alert.js";
4
5
  import { Button } from "../ui/button.js";
5
6
  import { Sheet, SheetContent, SheetHeader, SheetTitle } from "../ui/sheet.js";
6
- import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarInset, SidebarProvider, SidebarTrigger, useSidebar, useSidebarOptional } from "../ui/sidebar.js";
7
- import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip.js";
7
+ import { SidebarTrigger, useSidebarOptional } from "../ui/sidebar.js";
8
8
  import { useMediaQuery } from "../../hooks/use-media-query.js";
9
9
  import { cn } from "../../lib/utils.js";
10
10
  import * as React from "react";
11
11
  import { jsx, jsxs } from "react/jsx-runtime";
12
- import { PanelLeftCloseIcon, PanelLeftOpenIcon, PanelRightIcon, TriangleAlertIcon } from "lucide-react";
12
+ import { PanelRightIcon, TriangleAlertIcon } from "lucide-react";
13
13
  // src/components/ai/chat-shell.tsx
14
14
  const WIDE = "(min-width: 1280px)";
15
15
  const ChatShellContext = React.createContext(null);
16
- function readStored(key) {
17
- try {
18
- const v = window.localStorage.getItem(key);
19
- return v === null ? null : v !== "collapsed";
20
- } catch {
21
- return null;
22
- }
23
- }
24
- const useIsomorphicLayoutEffect = typeof window === "undefined" ? React.useEffect : React.useLayoutEffect;
25
16
  /**
26
- * Full-height chat app layout: a sidebar that collapses to an icon rail (a sheet on mobile), the main chat
27
- * column, and an optional details panel. Put a ChatPanel inside.
17
+ * Full-height chat app layout: SidebarLayout (sidebar with an icon rail, a sheet on mobile) around the chat
18
+ * column, plus an optional details panel. Put a ChatPanel inside.
28
19
  */
29
- function ChatShell({ brand, sidebarActions, sidebar, sidebarFooter, details, detailsTitle = "Details", defaultDetailsOpen = true, storageKey, defaultOpen = true, className, children }) {
30
- const [open, setOpen] = React.useState(defaultOpen);
31
- useIsomorphicLayoutEffect(() => {
32
- if (!storageKey) return;
33
- const stored = readStored(storageKey);
34
- if (stored !== null) setOpen(stored);
35
- }, [storageKey]);
36
- const onOpenChange = (next) => {
37
- setOpen(next);
38
- if (!storageKey) return;
39
- try {
40
- window.localStorage.setItem(storageKey, next ? "open" : "collapsed");
41
- } catch {}
42
- };
20
+ function ChatShell({ brand, sidebarActions, sidebar, sidebarFooter, details, detailsTitle = "Details", defaultDetailsOpen = true, collapsible, storageKey, defaultOpen = true, className, children }) {
43
21
  const wide = useMediaQuery(WIDE);
44
22
  const [inline, setInline] = React.useState(defaultDetailsOpen);
45
23
  const [sheet, setSheet] = React.useState(false);
@@ -55,90 +33,43 @@ function ChatShell({ brand, sidebarActions, sidebar, sidebarFooter, details, det
55
33
  inline,
56
34
  sheet
57
35
  ]);
58
- return /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsx(SidebarProvider, {
59
- open,
60
- onOpenChange,
61
- style: {
62
- "--sidebar-width": "17rem",
63
- "--sidebar-width-icon": "4.25rem"
64
- },
36
+ return /* @__PURE__ */ jsx(SidebarLayout, {
37
+ brand,
38
+ sidebarActions,
39
+ sidebar,
40
+ sidebarFooter,
41
+ collapsible,
42
+ storageKey,
43
+ defaultOpen,
44
+ width: "17rem",
45
+ className: cn("h-dvh overflow-hidden", className),
65
46
  children: /* @__PURE__ */ jsxs(ChatShellContext.Provider, {
66
47
  value: ctx,
67
- children: [/* @__PURE__ */ jsxs(Sidebar, {
68
- collapsible: "icon",
69
- children: [
70
- /* @__PURE__ */ jsxs(SidebarHeader, {
71
- className: "gap-5 px-3.5 pt-[max(1.25rem,env(safe-area-inset-top))] group-data-[collapsible=icon]:items-center group-data-[collapsible=icon]:gap-3 group-data-[collapsible=icon]:px-0",
72
- children: [/* @__PURE__ */ jsxs("div", {
73
- className: "flex min-w-0 items-center gap-2 group-data-[collapsible=icon]:flex-col group-data-[collapsible=icon]:gap-3",
74
- children: [brand && /* @__PURE__ */ jsx("div", {
75
- className: "min-w-0 flex-1 pl-2 group-data-[collapsible=icon]:pl-0",
76
- children: brand
77
- }), /* @__PURE__ */ jsx(SidebarCollapseButton, {})]
78
- }), sidebarActions && /* @__PURE__ */ jsx("div", {
79
- className: "flex flex-col gap-2.5 group-data-[collapsible=icon]:items-center",
80
- children: sidebarActions
81
- })]
82
- }),
83
- /* @__PURE__ */ jsx(SidebarContent, {
84
- className: "px-3.5 pt-2",
85
- children: sidebar
86
- }),
87
- sidebarFooter && /* @__PURE__ */ jsx(SidebarFooter, {
88
- className: "px-3.5 pb-[max(1.25rem,env(safe-area-inset-bottom))] group-data-[collapsible=icon]:px-0",
89
- children: /* @__PURE__ */ jsx("div", {
90
- className: "px-2 group-data-[collapsible=icon]:px-0 has-data-[slot=user-menu-trigger]:px-0",
91
- children: sidebarFooter
92
- })
93
- })
94
- ]
95
- }), /* @__PURE__ */ jsxs(SidebarInset, {
48
+ children: [/* @__PURE__ */ jsxs("div", {
96
49
  "data-slot": "chat-shell",
97
- className: cn("h-dvh min-w-0 overflow-hidden", className),
98
- children: [/* @__PURE__ */ jsxs("div", {
99
- className: "flex h-full min-h-0 gap-3 p-2 sm:p-3 md:pl-0",
100
- children: [children, hasDetails && wide && inline && /* @__PURE__ */ jsx("aside", {
101
- "data-slot": "chat-shell-details",
102
- "aria-label": detailsTitle,
103
- className: "flex w-75 shrink-0 flex-col gap-3 overflow-y-auto",
50
+ className: "flex h-full min-h-0 gap-3 p-2 sm:p-3 md:pl-0",
51
+ children: [children, hasDetails && wide && inline && /* @__PURE__ */ jsx("aside", {
52
+ "data-slot": "chat-shell-details",
53
+ "aria-label": detailsTitle,
54
+ className: "flex w-75 shrink-0 flex-col gap-3 overflow-y-auto",
55
+ children: details
56
+ })]
57
+ }), hasDetails && !wide && /* @__PURE__ */ jsx(Sheet, {
58
+ open: sheet,
59
+ onOpenChange: setSheet,
60
+ children: /* @__PURE__ */ jsxs(SheetContent, {
61
+ className: "overflow-y-auto bg-background p-4",
62
+ children: [/* @__PURE__ */ jsx(SheetHeader, {
63
+ className: "p-0",
64
+ children: /* @__PURE__ */ jsx(SheetTitle, { children: detailsTitle })
65
+ }), /* @__PURE__ */ jsx("div", {
66
+ className: "flex flex-col gap-3",
104
67
  children: details
105
68
  })]
106
- }), hasDetails && !wide && /* @__PURE__ */ jsx(Sheet, {
107
- open: sheet,
108
- onOpenChange: setSheet,
109
- children: /* @__PURE__ */ jsxs(SheetContent, {
110
- className: "overflow-y-auto bg-background p-4",
111
- children: [/* @__PURE__ */ jsx(SheetHeader, {
112
- className: "p-0",
113
- children: /* @__PURE__ */ jsx(SheetTitle, { children: detailsTitle })
114
- }), /* @__PURE__ */ jsx("div", {
115
- className: "flex flex-col gap-3",
116
- children: details
117
- })]
118
- })
119
- })]
69
+ })
120
70
  })]
121
71
  })
122
- }) });
123
- }
124
- /** Collapse / expand control for the desktop sidebar, with a tooltip. Hidden on mobile, where the sidebar is a sheet. */
125
- function SidebarCollapseButton() {
126
- const { state, isMobile, toggleSidebar } = useSidebar();
127
- if (isMobile) return null;
128
- const label = state === "collapsed" ? "Expand sidebar" : "Collapse sidebar";
129
- return /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
130
- asChild: true,
131
- children: /* @__PURE__ */ jsx(Button, {
132
- variant: "ghost",
133
- size: state === "collapsed" ? "icon" : "icon-sm",
134
- "aria-label": label,
135
- onClick: toggleSidebar,
136
- children: state === "collapsed" ? /* @__PURE__ */ jsx(PanelLeftOpenIcon, {}) : /* @__PURE__ */ jsx(PanelLeftCloseIcon, {})
137
- })
138
- }), /* @__PURE__ */ jsx(TooltipContent, {
139
- side: "right",
140
- children: label
141
- })] });
72
+ });
142
73
  }
143
74
  /**
144
75
  * The chat column as a card: header, optional notice, scrolling thread, composer. Inside ChatShell it adds the
@@ -10,8 +10,11 @@ export interface ChatThreadProps extends React.ComponentProps<"div"> {
10
10
  declare function ChatThread({ autoScroll, className, contentClassName, children, ...props }: ChatThreadProps): React.JSX.Element;
11
11
  export interface ChatMessageProps extends React.ComponentProps<"div"> {
12
12
  role: "user" | "assistant";
13
- /** Assistant avatar. Defaults to a sparkle in a tinted circle. */
14
- avatar?: React.ReactNode;
13
+ /**
14
+ * Assistant avatar. Defaults to a sparkle in a tinted circle. `false` renders no avatar and lets the content use
15
+ * the full width, for agent threads where one turn is many blocks (text, tool groups, approvals).
16
+ */
17
+ avatar?: React.ReactNode | false;
15
18
  /** Rendered under assistant content, usually <MessageActions />. */
16
19
  actions?: React.ReactNode;
17
20
  /** User bubble color: "muted" (gray, default) or "soft" (tinted with the accent). */
@@ -58,7 +58,7 @@ function ChatMessage({ role, avatar, actions, variant = "muted", className, chil
58
58
  "data-role": "assistant",
59
59
  className: cn("flex items-start gap-4", className),
60
60
  ...props,
61
- children: [avatar ?? /* @__PURE__ */ jsx(AssistantAvatar, {}), /* @__PURE__ */ jsxs("div", {
61
+ children: [avatar === false ? null : avatar ?? /* @__PURE__ */ jsx(AssistantAvatar, {}), /* @__PURE__ */ jsxs("div", {
62
62
  className: "flex min-w-0 flex-1 flex-col gap-3.5",
63
63
  children: [children, actions]
64
64
  })]
@@ -17,7 +17,11 @@ export interface CodeSnippetProps extends Omit<React.ComponentProps<typeof CodeB
17
17
  */
18
18
  declare function CodeSnippet({ code, language, highlight, lineNumbers, hideCopy, className, ...props }: CodeSnippetProps): React.JSX.Element;
19
19
  /** Inline monospace span for commands and identifiers. */
20
- declare function InlineCode({ className, ...props }: React.ComponentProps<"code">): React.JSX.Element;
20
+ export interface InlineCodeProps extends React.ComponentProps<"code"> {
21
+ /** Cut string children longer than this many characters with an ellipsis; the full text becomes the tooltip. */
22
+ truncate?: number;
23
+ }
24
+ declare function InlineCode({ truncate, className, children, title, ...props }: InlineCodeProps): React.JSX.Element;
21
25
  /**
22
26
  * Typography for long-form and model output. Styles plain HTML children
23
27
  * (p, ul, ol, li, strong, a, code, pre, h1-h4, blockquote, table), so it works