feat: redesign workspace UI — glassmorphism dropdowns, Chrome-style tabs, chat popover
- Move chat history from left sidebar to floating popover on tab bar - Add dench-ui components (Button, Card, Input, Label, Switch) with deps - Glassmorphism styling for all dropdowns/context menus with dark mode - Chrome-style active tab that merges with content area - Align sidebar header with tab bar (34px) - Condense sidebar header to single line - Move sidebar expand button into tab bar - Add next-themes for proper dark mode with system preference support - Add Tailwind v4 class-based dark mode via @custom-variant - Add dench-ui CSS tokens (light + dark) - Restore pointer cursor for all interactive elements - New chat button always visible in tab bar - "Delete this chat" label in dropdown menu Made-with: Cursor
This commit is contained in:
parent
45db1bcf54
commit
fbfdee21a5
@ -813,6 +813,8 @@ type ChatPanelProps = {
|
||||
subagentLabel?: string;
|
||||
/** Back button handler (subagent mode only). */
|
||||
onBack?: () => void;
|
||||
/** Hide the header action buttons (when they're rendered elsewhere, e.g. tab bar). */
|
||||
hideHeaderActions?: boolean;
|
||||
};
|
||||
|
||||
export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
|
||||
@ -834,6 +836,7 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
|
||||
subagentTask,
|
||||
subagentLabel,
|
||||
onBack,
|
||||
hideHeaderActions,
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
@ -2229,6 +2232,7 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
|
||||
</h2>
|
||||
)}
|
||||
</div>
|
||||
{!hideHeaderActions && (
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
{currentSessionId && onDeleteSession && (
|
||||
<DropdownMenu>
|
||||
@ -2259,13 +2263,12 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
|
||||
onSelect={() => onDeleteSession(currentSessionId)}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18" /><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" /><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" /></svg>
|
||||
Delete
|
||||
Delete this chat
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
{compact && (
|
||||
<button
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleNewSession()}
|
||||
className="p-1.5 rounded-lg"
|
||||
@ -2288,8 +2291,8 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
|
||||
<path d="M5 12h14" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</header>
|
||||
@ -2365,6 +2368,7 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
|
||||
{/* Hero greeting */}
|
||||
{greeting && (
|
||||
<motion.h1
|
||||
layout="position"
|
||||
className="text-4xl md:text-5xl font-light tracking-normal font-instrument mb-10 text-center"
|
||||
style={{ color: "var(--color-text)" }}
|
||||
initial="hidden"
|
||||
@ -2376,6 +2380,7 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
|
||||
transition: { staggerChildren: 0.12, delayChildren: 0.2 },
|
||||
},
|
||||
}}
|
||||
transition={{ layout: { type: "spring", stiffness: 260, damping: 30 } }}
|
||||
>
|
||||
{greeting.split(" ").map((word, i) => (
|
||||
<motion.span
|
||||
@ -2399,10 +2404,11 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
|
||||
|
||||
{/* Centered input bar */}
|
||||
<motion.div
|
||||
layout="position"
|
||||
className="w-full max-w-[720px] mx-auto px-4"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.8, ease: [0.22, 1, 0.36, 1] }}
|
||||
transition={{ duration: 0.8, delay: 0.8, ease: [0.22, 1, 0.36, 1], layout: { type: "spring", stiffness: 260, damping: 30 } }}
|
||||
>
|
||||
<motion.div
|
||||
layout
|
||||
@ -2415,10 +2421,11 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
|
||||
|
||||
{/* Prompt suggestion pills */}
|
||||
<motion.div
|
||||
layout="position"
|
||||
className="mt-6 flex flex-col gap-2.5 w-full max-w-[720px] mx-auto px-4"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 1.0, ease: [0.22, 1, 0.36, 1] }}
|
||||
transition={{ duration: 0.5, delay: 1.0, ease: [0.22, 1, 0.36, 1], layout: { type: "spring", stiffness: 260, damping: 30 } }}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2 flex-wrap">
|
||||
{visiblePrompts.slice(0, 3).map((template) => {
|
||||
|
||||
47
apps/web/app/components/ui/button.tsx
Normal file
47
apps/web/app/components/ui/button.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 outline-none ring-0 border-none",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline"
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9"
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
|
||||
}
|
||||
);
|
||||
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
36
apps/web/app/components/ui/card.tsx
Normal file
36
apps/web/app/components/ui/card.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("rounded-xl border bg-card text-card-foreground shadow", className)} {...props} />
|
||||
));
|
||||
Card.displayName = "Card";
|
||||
|
||||
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
||||
));
|
||||
CardHeader.displayName = "CardHeader";
|
||||
|
||||
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
||||
({ className, ...props }, ref) => <h3 ref={ref} className={cn("font-semibold leading-none tracking-tight", className)} {...props} />
|
||||
);
|
||||
CardTitle.displayName = "CardTitle";
|
||||
|
||||
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
||||
({ className, ...props }, ref) => <p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
||||
);
|
||||
CardDescription.displayName = "CardDescription";
|
||||
|
||||
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
));
|
||||
CardContent.displayName = "CardContent";
|
||||
|
||||
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
|
||||
));
|
||||
CardFooter.displayName = "CardFooter";
|
||||
|
||||
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };
|
||||
@ -58,7 +58,7 @@ function DropdownMenuContent({
|
||||
<MenuPrimitive.Popup
|
||||
data-slot="dropdown-menu-content"
|
||||
className={cn(
|
||||
"bg-neutral-100/[0.67] border border-white backdrop-blur-md text-[var(--color-text)] z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-3xl p-1 shadow-[0_0_25px_0_rgba(0,0,0,0.16)] outline-none",
|
||||
"bg-neutral-100/[0.67] dark:bg-neutral-900/[0.67] border border-white dark:border-white/10 backdrop-blur-md text-[var(--color-text)] z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-3xl p-1 shadow-[0_0_25px_0_rgba(0,0,0,0.16)] outline-none",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
|
||||
21
apps/web/app/components/ui/input.tsx
Normal file
21
apps/web/app/components/ui/input.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
||||
18
apps/web/app/components/ui/label.tsx
Normal file
18
apps/web/app/components/ui/label.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70");
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
|
||||
));
|
||||
Label.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { Label };
|
||||
28
apps/web/app/components/ui/switch.tsx
Normal file
28
apps/web/app/components/ui/switch.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
));
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName;
|
||||
|
||||
export { Switch };
|
||||
@ -452,11 +452,11 @@ export function ChatSessionsSidebar({
|
||||
</div>
|
||||
{/* Header overlay: backdrop blur + 80% bg; list scrolls under it */}
|
||||
<div
|
||||
className="absolute top-0 left-0 right-0 z-10 flex items-center justify-between border-b px-4 py-2 backdrop-blur-md"
|
||||
className={`absolute top-0 left-0 right-0 z-10 flex items-center justify-between px-4 py-2 backdrop-blur-md ${embedded ? "" : "border-b"}`}
|
||||
style={{
|
||||
height: headerHeight,
|
||||
borderColor: "var(--color-border)",
|
||||
background: "color-mix(in srgb, var(--color-sidebar-bg) 80%, transparent)",
|
||||
borderColor: embedded ? undefined : "var(--color-border)",
|
||||
background: embedded ? "transparent" : "color-mix(in srgb, var(--color-sidebar-bg) 80%, transparent)",
|
||||
}}
|
||||
>
|
||||
<div className="min-w-0 flex-1 flex items-center gap-1.5">
|
||||
|
||||
@ -160,14 +160,10 @@ export function ContextMenu({ x, y, target, onAction, onClose }: ContextMenuProp
|
||||
return createPortal(
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="fixed z-[9999] min-w-[200px] py-1 rounded-lg shadow-xl border"
|
||||
className="fixed z-[9999] min-w-[200px] p-1 rounded-2xl bg-neutral-100/[0.67] dark:bg-neutral-900/[0.67] border border-white dark:border-white/10 backdrop-blur-md shadow-[0_0_25px_0_rgba(0,0,0,0.16)]"
|
||||
style={{
|
||||
left: x,
|
||||
top: y,
|
||||
background: "var(--color-surface)",
|
||||
borderColor: "var(--color-border)",
|
||||
backdropFilter: "blur(20px)",
|
||||
WebkitBackdropFilter: "blur(20px)",
|
||||
animation: "contextMenuFadeIn 100ms ease-out",
|
||||
}}
|
||||
role="menu"
|
||||
@ -188,8 +184,7 @@ export function ContextMenu({ x, y, target, onAction, onClose }: ContextMenuProp
|
||||
return (
|
||||
<div
|
||||
key={`sep-${i}`}
|
||||
className="my-1 mx-2 border-t"
|
||||
style={{ borderColor: "var(--color-border)" }}
|
||||
className="my-0.5 mx-1 h-px bg-neutral-400/15"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -203,25 +198,13 @@ export function ContextMenu({ x, y, target, onAction, onClose }: ContextMenuProp
|
||||
type="button"
|
||||
role="menuitem"
|
||||
disabled={isDisabled}
|
||||
className="w-full flex items-center gap-2 px-3 py-1.5 text-[13px] text-left transition-colors"
|
||||
className={`w-full flex items-center gap-2 px-2.5 py-1.5 text-[13px] text-left rounded-xl transition-all ${isDisabled ? "opacity-50" : "hover:bg-neutral-400/15"}`}
|
||||
style={{
|
||||
color: isDisabled
|
||||
? "var(--color-text-muted)"
|
||||
: menuItem.danger
|
||||
? "#ef4444"
|
||||
? "var(--color-error)"
|
||||
: "var(--color-text)",
|
||||
opacity: isDisabled ? 0.5 : 1,
|
||||
cursor: isDisabled ? "default" : "pointer",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isDisabled) {
|
||||
(e.currentTarget as HTMLElement).style.background = menuItem.danger
|
||||
? "rgba(239, 68, 68, 0.1)"
|
||||
: "var(--color-surface-hover)";
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.currentTarget as HTMLElement).style.background = "transparent";
|
||||
}}
|
||||
onClick={() => handleItemClick(menuItem.action, isDisabled)}
|
||||
>
|
||||
|
||||
@ -199,69 +199,48 @@ export function ProfileSwitcher({
|
||||
|
||||
{showSwitcher && isOpen && (
|
||||
<div
|
||||
className="absolute left-0 top-full mt-1 w-64 rounded-lg overflow-hidden z-50"
|
||||
style={{
|
||||
background: "var(--color-surface-raised)",
|
||||
border: "1px solid var(--color-border)",
|
||||
boxShadow: "var(--shadow-lg)",
|
||||
}}
|
||||
className="absolute left-0 top-full mt-1.5 w-64 rounded-2xl overflow-hidden z-50 p-1 bg-neutral-100/[0.67] dark:bg-neutral-900/[0.67] border border-white dark:border-white/10 backdrop-blur-md shadow-[0_0_25px_0_rgba(0,0,0,0.16)]"
|
||||
>
|
||||
{/* Header */}
|
||||
<div
|
||||
className="px-3 py-2 text-xs font-medium"
|
||||
style={{
|
||||
color: "var(--color-text-muted)",
|
||||
borderBottom: "1px solid var(--color-border)",
|
||||
}}
|
||||
className="px-2.5 py-1.5 text-[11px] font-medium"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
Workspaces
|
||||
</div>
|
||||
|
||||
{/* Workspace list */}
|
||||
<div className="py-1 max-h-64 overflow-y-auto">
|
||||
<div className="max-h-64 overflow-y-auto">
|
||||
{workspaces.map((workspace) => {
|
||||
const isCurrent = workspace.name === activeWorkspace;
|
||||
return (
|
||||
<div key={workspace.name} className="flex items-center gap-1 px-1.5 py-0.5">
|
||||
<div key={workspace.name} className="flex items-center gap-0.5">
|
||||
<button
|
||||
onClick={() => void handleSwitch(workspace.name)}
|
||||
disabled={switching || !!deletingWorkspace}
|
||||
className="flex-1 min-w-0 flex items-center gap-2 px-1.5 py-1.5 rounded text-left text-sm transition-colors hover:bg-[var(--color-surface-hover)] disabled:opacity-50"
|
||||
className="flex-1 min-w-0 flex items-center gap-2 px-2.5 py-1.5 rounded-xl text-left text-sm transition-all hover:bg-neutral-400/15 disabled:opacity-50 cursor-pointer"
|
||||
style={{ color: "var(--color-text)" }}
|
||||
>
|
||||
{/* Active indicator */}
|
||||
<span
|
||||
className="w-2 h-2 rounded-full flex-shrink-0"
|
||||
className="w-2 h-2 rounded-full shrink-0"
|
||||
style={{
|
||||
background: isCurrent ? "var(--color-success)" : "transparent",
|
||||
border: isCurrent ? "none" : "1px solid var(--color-border-strong)",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="truncate font-medium">
|
||||
{workspace.name}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="text-xs truncate mt-0.5"
|
||||
<span className="truncate font-medium text-[13px] block">
|
||||
{workspace.name}
|
||||
</span>
|
||||
<span
|
||||
className="text-[11px] truncate block mt-0.5"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
{workspace.workspaceDir
|
||||
? shortenPath(workspace.workspaceDir)
|
||||
: "No workspace yet"}
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{isCurrent && (
|
||||
<span
|
||||
className="text-xs px-1.5 py-0.5 rounded"
|
||||
style={{
|
||||
background: "var(--color-accent-light)",
|
||||
color: "var(--color-accent)",
|
||||
}}
|
||||
>
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded-full shrink-0 bg-neutral-400/15" style={{ color: "var(--color-text-muted)" }}>
|
||||
Active
|
||||
</span>
|
||||
)}
|
||||
@ -272,28 +251,15 @@ export function ProfileSwitcher({
|
||||
onClick={() => void handleDeleteWorkspace(workspace.name)}
|
||||
disabled={switching || !!deletingWorkspace}
|
||||
title={`Delete workspace ${workspace.name}`}
|
||||
className="p-1.5 rounded transition-colors hover:bg-[var(--color-surface-hover)] disabled:opacity-50"
|
||||
className="p-1.5 rounded-xl transition-all hover:bg-neutral-400/15 disabled:opacity-50 shrink-0 cursor-pointer"
|
||||
style={{
|
||||
color: deletingWorkspace === workspace.name
|
||||
? "var(--color-text-muted)"
|
||||
: "var(--color-error)",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M3 6h18" />
|
||||
<path d="M8 6V4h8v2" />
|
||||
<path d="M19 6l-1 14H6L5 6" />
|
||||
<path d="M10 11v6" />
|
||||
<path d="M14 11v6" />
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M3 6h18" /><path d="M8 6V4h8v2" /><path d="M19 6l-1 14H6L5 6" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
@ -304,7 +270,7 @@ export function ProfileSwitcher({
|
||||
|
||||
{actionError && (
|
||||
<p
|
||||
className="mx-3 mb-2 mt-1 rounded px-2 py-1 text-xs"
|
||||
className="mx-2 mb-1 mt-1 rounded-xl px-2.5 py-1.5 text-xs"
|
||||
style={{
|
||||
background: "rgba(220, 38, 38, 0.08)",
|
||||
color: "var(--color-error)",
|
||||
@ -314,17 +280,16 @@ export function ProfileSwitcher({
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Create new */}
|
||||
<div style={{ borderTop: "1px solid var(--color-border)" }}>
|
||||
<div className="border-t border-neutral-400/15 mt-0.5 pt-0.5">
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
onCreateWorkspace?.();
|
||||
}}
|
||||
className="w-full flex items-center gap-2 px-3 py-2.5 text-sm transition-colors hover:bg-[var(--color-surface-hover)]"
|
||||
className="w-full flex items-center gap-2 px-2.5 py-1.5 text-sm rounded-xl transition-all hover:bg-neutral-400/15 cursor-pointer"
|
||||
style={{ color: "var(--color-accent)" }}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 5v14" /><path d="M5 12h14" />
|
||||
</svg>
|
||||
New Workspace
|
||||
|
||||
@ -14,6 +14,8 @@ type TabBarProps = {
|
||||
onCloseAll: () => void;
|
||||
onReorder: (fromIndex: number, toIndex: number) => void;
|
||||
onTogglePin: (tabId: string) => void;
|
||||
leftContent?: React.ReactNode;
|
||||
rightContent?: React.ReactNode;
|
||||
};
|
||||
|
||||
type ContextMenuState = {
|
||||
@ -32,6 +34,8 @@ export function TabBar({
|
||||
onCloseAll,
|
||||
onReorder,
|
||||
onTogglePin,
|
||||
leftContent,
|
||||
rightContent,
|
||||
}: TabBarProps) {
|
||||
const [contextMenu, setContextMenu] = useState<ContextMenuState>(null);
|
||||
const [dragIndex, setDragIndex] = useState<number | null>(null);
|
||||
@ -95,14 +99,22 @@ export function TabBar({
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="flex items-end overflow-x-auto flex-shrink-0"
|
||||
className="flex items-stretch flex-shrink-0 h-[34px]"
|
||||
style={{
|
||||
background: "var(--color-surface)",
|
||||
borderBottom: "1px solid var(--color-border)",
|
||||
scrollbarWidth: "none",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="flex items-stretch overflow-x-auto flex-1 min-w-0"
|
||||
style={{ scrollbarWidth: "none" }}
|
||||
>
|
||||
{leftContent && (
|
||||
<div className="flex items-center px-1.5 shrink-0">
|
||||
{leftContent}
|
||||
</div>
|
||||
)}
|
||||
{tabs.map((tab, index) => {
|
||||
const isActive = tab.id === activeTabId;
|
||||
const isDragOver = dragOverIndex === index && dragIndex !== index;
|
||||
@ -120,15 +132,14 @@ export function TabBar({
|
||||
onDragOver={isHome ? undefined : (e) => handleDragOver(e, index)}
|
||||
onDrop={isHome ? undefined : (e) => handleDrop(e, index)}
|
||||
onDragEnd={isHome ? undefined : handleDragEnd}
|
||||
className={`group flex items-center gap-1.5 h-[34px] text-[12.5px] font-medium cursor-pointer flex-shrink-0 relative transition-colors duration-75 select-none ${isHome ? "px-2.5" : "pl-3 pr-1.5"}`}
|
||||
className={`group flex items-center gap-1.5 text-[12.5px] font-medium cursor-pointer flex-shrink-0 relative transition-colors duration-75 select-none ${isHome ? "px-2.5" : "pl-3 pr-1.5"} ${isActive ? "mb-[-1px] rounded-t-lg" : ""}`}
|
||||
style={{
|
||||
color: isActive ? "var(--color-text)" : "var(--color-text-muted)",
|
||||
background: isActive ? "var(--color-bg)" : "transparent",
|
||||
borderBottom: isActive ? "2px solid var(--color-accent)" : "2px solid transparent",
|
||||
borderLeft: isDragOver && !isHome ? "2px solid var(--color-accent)" : "2px solid transparent",
|
||||
background: isActive ? "var(--color-main-bg)" : "transparent",
|
||||
borderBottom: isActive ? "1px solid var(--color-main-bg)" : "none",
|
||||
borderLeft: isDragOver && !isHome ? "2px solid var(--color-accent)" : undefined,
|
||||
opacity: dragIndex === index ? 0.5 : 1,
|
||||
maxWidth: isHome ? undefined : 200,
|
||||
borderRight: isHome ? "1px solid var(--color-border)" : undefined,
|
||||
}}
|
||||
title={isHome ? "Home (New Chat)" : undefined}
|
||||
>
|
||||
@ -162,24 +173,28 @@ export function TabBar({
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{rightContent && (
|
||||
<div className="relative flex items-center gap-0.5 px-2 shrink-0 h-[34px]">
|
||||
{rightContent}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Context menu */}
|
||||
{contextMenu && contextTab && (
|
||||
<div
|
||||
className="fixed z-[9999] min-w-[180px] rounded-lg border py-1 shadow-lg"
|
||||
className="fixed z-[9999] min-w-[180px] rounded-2xl p-1 bg-neutral-100/[0.67] dark:bg-neutral-900/[0.67] border border-white dark:border-white/10 backdrop-blur-md shadow-[0_0_25px_0_rgba(0,0,0,0.16)]"
|
||||
style={{
|
||||
left: contextMenu.x,
|
||||
top: contextMenu.y,
|
||||
background: "var(--color-surface)",
|
||||
borderColor: "var(--color-border)",
|
||||
}}
|
||||
>
|
||||
<ContextMenuItem
|
||||
label={contextTab.pinned ? "Unpin Tab" : "Pin Tab"}
|
||||
onClick={() => { onTogglePin(contextMenu.tabId); setContextMenu(null); }}
|
||||
/>
|
||||
<div className="h-px my-1" style={{ background: "var(--color-border)" }} />
|
||||
<div className="h-px my-0.5 mx-1 bg-neutral-400/15" />
|
||||
<ContextMenuItem
|
||||
label="Close"
|
||||
shortcut="⌘W"
|
||||
@ -220,14 +235,8 @@ function ContextMenuItem({
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
className="w-full flex items-center justify-between px-3 py-1.5 text-[12.5px] text-left transition-colors disabled:opacity-40 cursor-pointer disabled:cursor-default"
|
||||
className="w-full flex items-center justify-between px-2.5 py-1.5 text-[12.5px] text-left rounded-xl transition-all disabled:opacity-40 hover:bg-neutral-400/15"
|
||||
style={{ color: "var(--color-text)" }}
|
||||
onMouseEnter={(e) => {
|
||||
if (!disabled) (e.currentTarget as HTMLElement).style.background = "var(--color-surface-hover)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.currentTarget as HTMLElement).style.background = "transparent";
|
||||
}}
|
||||
>
|
||||
<span>{label}</span>
|
||||
{shortcut && (
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef, useCallback } from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { FileManagerTree, type TreeNode } from "./file-manager-tree";
|
||||
import { ProfileSwitcher } from "./profile-switcher";
|
||||
import { CreateWorkspaceDialog } from "./create-workspace-dialog";
|
||||
@ -105,66 +106,32 @@ function FolderOpenIcon() {
|
||||
/* ─── Theme toggle ─── */
|
||||
|
||||
function ThemeToggle() {
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
const { resolvedTheme, setTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
useEffect(() => setMounted(true), []);
|
||||
|
||||
useEffect(() => {
|
||||
setIsDark(document.documentElement.classList.contains("dark"));
|
||||
}, []);
|
||||
if (!mounted) return <div className="w-[28px] h-[28px]" />;
|
||||
|
||||
const toggle = () => {
|
||||
const next = !isDark;
|
||||
setIsDark(next);
|
||||
if (next) {
|
||||
document.documentElement.classList.add("dark");
|
||||
localStorage.setItem("theme", "dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
localStorage.setItem("theme", "light");
|
||||
}
|
||||
};
|
||||
const isDark = resolvedTheme === "dark";
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggle}
|
||||
onClick={() => setTheme(isDark ? "light" : "dark")}
|
||||
className="p-1.5 rounded-lg"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
title={isDark ? "Switch to light mode" : "Switch to dark mode"}
|
||||
>
|
||||
{isDark ? (
|
||||
/* Sun icon */
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<path d="M12 2v2" />
|
||||
<path d="M12 20v2" />
|
||||
<path d="m4.93 4.93 1.41 1.41" />
|
||||
<path d="m17.66 17.66 1.41 1.41" />
|
||||
<path d="M2 12h2" />
|
||||
<path d="M20 12h2" />
|
||||
<path d="m6.34 17.66-1.41 1.41" />
|
||||
<path d="m19.07 4.93-1.41 1.41" />
|
||||
<path d="M12 2v2" /><path d="M12 20v2" />
|
||||
<path d="m4.93 4.93 1.41 1.41" /><path d="m17.66 17.66 1.41 1.41" />
|
||||
<path d="M2 12h2" /><path d="M20 12h2" />
|
||||
<path d="m6.34 17.66-1.41 1.41" /><path d="m19.07 4.93-1.41 1.41" />
|
||||
</svg>
|
||||
) : (
|
||||
/* Moon icon */
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
|
||||
</svg>
|
||||
)}
|
||||
@ -467,44 +434,31 @@ export function WorkspaceSidebar({
|
||||
>
|
||||
{/* Header */}
|
||||
<div
|
||||
className="flex items-center gap-2 px-3 py-2.5 border-b"
|
||||
className="flex items-center gap-2 px-3 h-[34px] border-b"
|
||||
style={{ borderColor: "var(--color-border)" }}
|
||||
>
|
||||
{isBrowsing ? (
|
||||
<>
|
||||
<span
|
||||
className="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0"
|
||||
style={{
|
||||
background: "var(--color-surface-hover)",
|
||||
color: "var(--color-text-muted)",
|
||||
}}
|
||||
>
|
||||
<FolderOpenIcon />
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div
|
||||
className="text-sm font-medium truncate"
|
||||
<div className="flex-1 min-w-0 flex items-center gap-1.5">
|
||||
<span
|
||||
className="shrink-0"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
<FolderOpenIcon />
|
||||
</span>
|
||||
<span
|
||||
className="text-[12px] font-medium truncate"
|
||||
style={{ color: "var(--color-text)" }}
|
||||
title={browseDir}
|
||||
>
|
||||
{dirDisplayName(browseDir)}
|
||||
</div>
|
||||
<div
|
||||
className="text-[11px] truncate"
|
||||
style={{
|
||||
color: "var(--color-text-muted)",
|
||||
}}
|
||||
title={browseDir}
|
||||
>
|
||||
{browseDir}
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
{/* Home button to return to workspace */}
|
||||
{onGoHome && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onGoHome}
|
||||
className="p-1.5 rounded-lg flex-shrink-0"
|
||||
className="p-1 rounded-md shrink-0 transition-colors hover:bg-stone-200 dark:hover:bg-stone-700"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
title="Return to workspace"
|
||||
>
|
||||
@ -514,66 +468,48 @@ export function WorkspaceSidebar({
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void onGoToChat?.()}
|
||||
className="w-7 h-7 rounded-lg flex items-center justify-center shrink-0 cursor-pointer transition-colors hover:bg-stone-200 dark:hover:bg-stone-700"
|
||||
style={{
|
||||
background: "transparent",
|
||||
color: "var(--color-text-muted)",
|
||||
}}
|
||||
title="All Chats"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
||||
<polyline points="9 22 9 12 15 12 15 22" />
|
||||
</svg>
|
||||
</button>
|
||||
<div className="flex-1 min-w-0 px-1.5">
|
||||
<div className="text-[13px] font-semibold truncate text-stone-700 dark:text-stone-200">
|
||||
{orgName || "Workspace"}
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<ProfileSwitcher
|
||||
activeWorkspaceHint={activeWorkspace ?? null}
|
||||
onWorkspaceSwitch={() => {
|
||||
onWorkspaceChanged?.();
|
||||
}}
|
||||
onWorkspaceDelete={() => {
|
||||
onWorkspaceChanged?.();
|
||||
}}
|
||||
onCreateWorkspace={() => {
|
||||
setCreateWorkspaceOpen(true);
|
||||
}}
|
||||
trigger={({ onClick, activeWorkspace: workspaceName, switching }) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
disabled={switching}
|
||||
className="text-[11px] flex items-center gap-1 truncate w-full transition-colors"
|
||||
<div className="flex-1 min-w-0">
|
||||
<ProfileSwitcher
|
||||
activeWorkspaceHint={activeWorkspace ?? null}
|
||||
onWorkspaceSwitch={() => {
|
||||
onWorkspaceChanged?.();
|
||||
}}
|
||||
onWorkspaceDelete={() => {
|
||||
onWorkspaceChanged?.();
|
||||
}}
|
||||
onCreateWorkspace={() => {
|
||||
setCreateWorkspaceOpen(true);
|
||||
}}
|
||||
trigger={({ onClick, activeWorkspace: workspaceName, switching }) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
disabled={switching}
|
||||
className="text-[12px] flex items-center gap-1.5 truncate w-full transition-colors font-medium rounded-md px-1.5 py-1 -mx-1.5 hover:bg-stone-200/60 dark:hover:bg-stone-700/60"
|
||||
style={{ color: "var(--color-text)" }}
|
||||
title="Switch workspace"
|
||||
>
|
||||
<span className="truncate">{orgName || "Workspace"}</span>
|
||||
<span className="px-1 py-px rounded text-[10px] leading-tight shrink-0 bg-stone-200 text-stone-600 dark:bg-stone-700 dark:text-stone-300">
|
||||
{workspaceName || "-"}
|
||||
</span>
|
||||
<svg
|
||||
width="10"
|
||||
height="10"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="shrink-0"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
title="Switch workspace"
|
||||
>
|
||||
<span>Workspace</span>
|
||||
<span className="px-1 py-0.5 rounded text-[10px] shrink-0 bg-stone-200 text-stone-600 dark:bg-stone-700 dark:text-stone-300">
|
||||
{workspaceName || "-"}
|
||||
</span>
|
||||
<svg
|
||||
width="10"
|
||||
height="10"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@ -593,83 +529,33 @@ export function WorkspaceSidebar({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tab switcher */}
|
||||
{hasChatProps && !isBrowsing && (
|
||||
<div
|
||||
className="flex px-3 pt-2 pb-1 gap-1"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTab("files")}
|
||||
className="flex-1 text-[11px] font-medium py-1.5 rounded-md transition-colors"
|
||||
style={{
|
||||
color: currentTab === "files" ? "var(--color-text)" : "var(--color-text-muted)",
|
||||
background: currentTab === "files" ? "var(--color-surface-hover)" : "transparent",
|
||||
}}
|
||||
>
|
||||
Files
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTab("chats")}
|
||||
className="flex-1 text-[11px] font-medium py-1.5 rounded-md transition-colors"
|
||||
style={{
|
||||
color: currentTab === "chats" ? "var(--color-text)" : "var(--color-text-muted)",
|
||||
background: currentTab === "chats" ? "var(--color-surface-hover)" : "transparent",
|
||||
}}
|
||||
>
|
||||
Chats
|
||||
</button>
|
||||
</div>
|
||||
{onFileSearchSelect && (
|
||||
<FileSearch onSelect={onFileSearchSelect} />
|
||||
)}
|
||||
|
||||
{/* Tab content */}
|
||||
{currentTab === "files" || !hasChatProps ? (
|
||||
<>
|
||||
{onFileSearchSelect && (
|
||||
<FileSearch onSelect={onFileSearchSelect} />
|
||||
)}
|
||||
<div className="flex-1 overflow-y-auto px-1">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<UnicodeSpinner
|
||||
name="braille"
|
||||
className="text-2xl"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<FileManagerTree
|
||||
tree={tree}
|
||||
activePath={activePath}
|
||||
onSelect={onSelect}
|
||||
onRefresh={onRefresh}
|
||||
parentDir={parentDir}
|
||||
onNavigateUp={onNavigateUp}
|
||||
browseDir={browseDir}
|
||||
workspaceRoot={workspaceRoot}
|
||||
onExternalDrop={onExternalDrop}
|
||||
/>
|
||||
)}
|
||||
<div className="flex-1 overflow-y-auto px-1">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<UnicodeSpinner
|
||||
name="braille"
|
||||
className="text-2xl"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<ChatSessionsSidebar
|
||||
sessions={chatSessions ?? []}
|
||||
activeSessionId={activeChatSessionId ?? null}
|
||||
activeSessionTitle={activeChatSessionTitle}
|
||||
streamingSessionIds={chatStreamingSessionIds}
|
||||
subagents={chatSubagents}
|
||||
activeSubagentKey={chatActiveSubagentKey}
|
||||
loading={chatSessionsLoading}
|
||||
onSelectSession={onSelectChatSession ?? (() => {})}
|
||||
onNewSession={onNewChatSession ?? (() => {})}
|
||||
onSelectSubagent={onSelectChatSubagent}
|
||||
onDeleteSession={onDeleteChatSession}
|
||||
onRenameSession={onRenameChatSession}
|
||||
embedded
|
||||
/>
|
||||
)}
|
||||
) : (
|
||||
<FileManagerTree
|
||||
tree={tree}
|
||||
activePath={activePath}
|
||||
onSelect={onSelect}
|
||||
onRefresh={onRefresh}
|
||||
parentDir={parentDir}
|
||||
onNavigateUp={onNavigateUp}
|
||||
browseDir={browseDir}
|
||||
workspaceRoot={workspaceRoot}
|
||||
onExternalDrop={onExternalDrop}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
/* ============================================================
|
||||
Theme System — Light (default) & Dark (.dark)
|
||||
============================================================ */
|
||||
@ -70,6 +72,28 @@
|
||||
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
--shadow-xl: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
|
||||
/* Dench UI tokens (used by @denchhq/ui-react primitives) */
|
||||
--background: 0 0% 90%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
@ -138,6 +162,28 @@
|
||||
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||
--shadow-xl: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
|
||||
/* Dench UI tokens (used by @denchhq/ui-react primitives) */
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
/* Disable iframe pointer events during sidebar resize so the
|
||||
@ -281,6 +327,17 @@ a,
|
||||
transition-timing-function: ease;
|
||||
}
|
||||
|
||||
/* Restore pointer cursor for interactive elements (Tailwind v4 preflight resets to default) */
|
||||
button:not(:disabled),
|
||||
[type="button"]:not(:disabled),
|
||||
[role="button"]:not(:disabled),
|
||||
a[href],
|
||||
select:not(:disabled),
|
||||
summary,
|
||||
label {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Scrollbar
|
||||
============================================================ */
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import { Suspense } from "react";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import { getOrCreateAnonymousId } from "@/lib/telemetry";
|
||||
import { PostHogProvider } from "./components/posthog-provider";
|
||||
import "./globals.css";
|
||||
@ -38,19 +39,15 @@ export default function RootLayout({
|
||||
href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter:wght@300;400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
{/* Inline script to prevent FOUC — reads localStorage or system preference */}
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `try{if(localStorage.theme==="dark"||(!("theme" in localStorage)&&window.matchMedia("(prefers-color-scheme: dark)").matches)){document.documentElement.classList.add("dark")}else{document.documentElement.classList.remove("dark")}}catch(e){}`,
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
<body className="antialiased">
|
||||
<Suspense fallback={null}>
|
||||
<PostHogProvider anonymousId={anonymousId}>
|
||||
{children}
|
||||
</PostHogProvider>
|
||||
</Suspense>
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
|
||||
<Suspense fallback={null}>
|
||||
<PostHogProvider anonymousId={anonymousId}>
|
||||
{children}
|
||||
</PostHogProvider>
|
||||
</Suspense>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@ -43,6 +43,13 @@ import {
|
||||
autoDetectViewField,
|
||||
} from "@/lib/object-filters";
|
||||
import { UnicodeSpinner } from "../components/unicode-spinner";
|
||||
import { ChatSessionsSidebar } from "../components/workspace/chat-sessions-sidebar";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "../components/ui/dropdown-menu";
|
||||
import { resolveActiveViewSyncDecision } from "./object-view-active-view";
|
||||
import { resetWorkspaceStateOnSwitch } from "./workspace-switch";
|
||||
import { TabBar } from "../components/workspace/tab-bar";
|
||||
@ -501,6 +508,20 @@ function WorkspacePageInner() {
|
||||
// Sidebar collapse state (desktop only).
|
||||
const [leftSidebarCollapsed, setLeftSidebarCollapsed] = useState(false);
|
||||
const [rightSidebarCollapsed, setRightSidebarCollapsed] = useState(false);
|
||||
const [sidebarTab, setSidebarTab] = useState<"files" | "chats">("files");
|
||||
const [chatPopoverOpen, setChatPopoverOpen] = useState(false);
|
||||
const chatPopoverRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chatPopoverOpen) return;
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
if (chatPopoverRef.current && !chatPopoverRef.current.contains(e.target as Node)) {
|
||||
setChatPopoverOpen(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, [chatPopoverOpen]);
|
||||
|
||||
// Terminal drawer state
|
||||
const [terminalOpen, setTerminalOpen] = useState(false);
|
||||
@ -1829,6 +1850,8 @@ function WorkspacePageInner() {
|
||||
onSelectChatSubagent={handleSelectSubagent}
|
||||
onDeleteChatSession={handleDeleteSession}
|
||||
onRenameChatSession={handleRenameSession}
|
||||
activeTab={sidebarTab}
|
||||
onTabChange={setSidebarTab}
|
||||
mobile
|
||||
onClose={() => setSidebarOpen(false)}
|
||||
/>
|
||||
@ -1888,29 +1911,14 @@ function WorkspacePageInner() {
|
||||
onSelectChatSubagent={handleSelectSubagent}
|
||||
onDeleteChatSession={handleDeleteSession}
|
||||
onRenameChatSession={handleRenameSession}
|
||||
activeTab={sidebarTab}
|
||||
onTabChange={setSidebarTab}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Expand left sidebar button (shown when collapsed) */}
|
||||
{!isMobile && leftSidebarCollapsed && (
|
||||
<div className="shrink-0 flex flex-col items-center pt-2.5 px-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLeftSidebarCollapsed(false)}
|
||||
className="p-1.5 rounded-md transition-colors hover:bg-black/5"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
title="Show sidebar (⌘B)"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||
<path d="M9 3v18" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main content */}
|
||||
<main className="flex-1 flex flex-col min-w-0 overflow-hidden" style={{ background: "var(--color-main-bg)" }}>
|
||||
@ -1961,12 +1969,115 @@ function WorkspacePageInner() {
|
||||
tabs={tabState.tabs}
|
||||
activeTabId={tabState.activeTabId}
|
||||
onActivate={handleTabActivate}
|
||||
leftContent={leftSidebarCollapsed ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLeftSidebarCollapsed(false)}
|
||||
className="p-1.5 rounded-md transition-colors hover:bg-black/5 cursor-pointer"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
title="Show sidebar (⌘B)"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||
<path d="M9 3v18" />
|
||||
</svg>
|
||||
</button>
|
||||
) : undefined}
|
||||
onClose={handleTabClose}
|
||||
onCloseOthers={handleTabCloseOthers}
|
||||
onCloseToRight={handleTabCloseToRight}
|
||||
onCloseAll={handleTabCloseAll}
|
||||
onReorder={handleTabReorder}
|
||||
onTogglePin={handleTabTogglePin}
|
||||
rightContent={showMainChat ? (
|
||||
<>
|
||||
<div className="relative" ref={chatPopoverRef}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setChatPopoverOpen((v) => !v)}
|
||||
className="p-1.5 rounded-lg cursor-pointer"
|
||||
style={{ color: chatPopoverOpen ? "var(--color-accent)" : "var(--color-text-muted)" }}
|
||||
title="Chat history"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
{chatPopoverOpen && (
|
||||
<div
|
||||
className="absolute right-0 top-full mt-1.5 w-72 h-96 rounded-2xl overflow-hidden z-[9999] bg-neutral-100/[0.67] dark:bg-neutral-900/[0.67] border border-white dark:border-white/10 backdrop-blur-md shadow-[0_0_25px_0_rgba(0,0,0,0.16)] flex flex-col"
|
||||
>
|
||||
<ChatSessionsSidebar
|
||||
sessions={sessions}
|
||||
activeSessionId={activeSessionId}
|
||||
activeSessionTitle={activeSessionTitle}
|
||||
streamingSessionIds={streamingSessionIds}
|
||||
subagents={subagents}
|
||||
activeSubagentKey={activeSubagentKey}
|
||||
loading={sessionsLoading}
|
||||
onSelectSession={(sessionId) => {
|
||||
setActiveSessionId(sessionId);
|
||||
setActiveSubagentKey(null);
|
||||
void chatRef.current?.loadSession(sessionId);
|
||||
setChatPopoverOpen(false);
|
||||
}}
|
||||
onNewSession={() => {
|
||||
setActiveSessionId(null);
|
||||
setActiveSubagentKey(null);
|
||||
void chatRef.current?.newSession();
|
||||
setChatPopoverOpen(false);
|
||||
}}
|
||||
onSelectSubagent={(key) => {
|
||||
handleSelectSubagent(key);
|
||||
setChatPopoverOpen(false);
|
||||
}}
|
||||
onDeleteSession={handleDeleteSession}
|
||||
onRenameSession={handleRenameSession}
|
||||
embedded
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{activeSessionId && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
className="p-1.5 rounded-lg cursor-pointer"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
title="More options"
|
||||
aria-label="More options"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="1" /><circle cx="5" cy="12" r="1" /><circle cx="19" cy="12" r="1" />
|
||||
</svg>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" side="bottom">
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
onSelect={() => handleDeleteSession(activeSessionId)}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18" /><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" /><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" /></svg>
|
||||
Delete this chat
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setActiveSessionId(null);
|
||||
setActiveSubagentKey(null);
|
||||
void chatRef.current?.newSession();
|
||||
}}
|
||||
className="p-1.5 rounded-lg cursor-pointer"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
title="New chat"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 5v14" /><path d="M5 12h14" />
|
||||
</svg>
|
||||
</button>
|
||||
</>
|
||||
) : undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -2043,6 +2154,7 @@ function WorkspacePageInner() {
|
||||
subagentTask={activeSubagent?.task}
|
||||
subagentLabel={activeSubagent?.label}
|
||||
onBack={activeSubagent ? handleBackFromSubagent : undefined}
|
||||
hideHeaderActions={!isMobile}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -16,6 +16,9 @@
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@tanstack/match-sorter-utils": "^8.19.4",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tiptap/core": "^3.19.0",
|
||||
@ -45,6 +48,7 @@
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"ai": "^6.0.73",
|
||||
"chokidar": "^5.0.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"framer-motion": "^12.34.0",
|
||||
@ -54,6 +58,7 @@
|
||||
"mammoth": "^1.11.0",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"next": "^15.3.3",
|
||||
"next-themes": "^0.4.6",
|
||||
"node-pty": "^1.1.0",
|
||||
"posthog-js": "^1.358.1",
|
||||
"posthog-node": "^5.27.1",
|
||||
|
||||
273
pnpm-lock.yaml
generated
273
pnpm-lock.yaml
generated
@ -75,6 +75,15 @@ importers:
|
||||
'@monaco-editor/react':
|
||||
specifier: ^4.7.0
|
||||
version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@radix-ui/react-label':
|
||||
specifier: ^2.1.8
|
||||
version: 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@radix-ui/react-slot':
|
||||
specifier: ^1.2.4
|
||||
version: 1.2.4(@types/react@19.2.14)(react@19.2.4)
|
||||
'@radix-ui/react-switch':
|
||||
specifier: ^1.2.6
|
||||
version: 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@tanstack/match-sorter-utils':
|
||||
specifier: ^8.19.4
|
||||
version: 8.19.4
|
||||
@ -162,6 +171,9 @@ importers:
|
||||
chokidar:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
@ -189,6 +201,9 @@ importers:
|
||||
next:
|
||||
specifier: ^15.3.3
|
||||
version: 15.5.12(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
next-themes:
|
||||
specifier: ^0.4.6
|
||||
version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
node-pty:
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0
|
||||
@ -2013,6 +2028,142 @@ packages:
|
||||
'@quansync/fs@1.0.0':
|
||||
resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==}
|
||||
|
||||
'@radix-ui/primitive@1.1.3':
|
||||
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
|
||||
|
||||
'@radix-ui/react-compose-refs@1.1.2':
|
||||
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-context@1.1.2':
|
||||
resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-label@2.1.8':
|
||||
resolution: {integrity: sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-primitive@2.1.3':
|
||||
resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-primitive@2.1.4':
|
||||
resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-slot@1.2.3':
|
||||
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-slot@1.2.4':
|
||||
resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-switch@1.2.6':
|
||||
resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-controllable-state@1.2.2':
|
||||
resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-effect-event@0.0.2':
|
||||
resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-layout-effect@1.1.1':
|
||||
resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-previous@1.1.1':
|
||||
resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-size@1.1.1':
|
||||
resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@reduxjs/toolkit@2.11.2':
|
||||
resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==}
|
||||
peerDependencies:
|
||||
@ -3745,6 +3896,9 @@ packages:
|
||||
resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
class-variance-authority@0.7.1:
|
||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
||||
|
||||
classnames@2.5.1:
|
||||
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
|
||||
|
||||
@ -5368,6 +5522,12 @@ packages:
|
||||
resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
|
||||
next-themes@0.4.6:
|
||||
resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
|
||||
next-tick@0.2.2:
|
||||
resolution: {integrity: sha512-f7h4svPtl+QidoBv4taKXUjJ70G2asaZ8G28nS0OkqaalX8dwwrtWtyxEDPK62AC00ur/+/E0pUwBwY5EPn15Q==}
|
||||
|
||||
@ -9039,6 +9199,110 @@ snapshots:
|
||||
dependencies:
|
||||
quansync: 1.0.0
|
||||
|
||||
'@radix-ui/primitive@1.1.3': {}
|
||||
|
||||
'@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.4)':
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
|
||||
'@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.4)':
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
|
||||
'@radix-ui/react-label@2.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||
dependencies:
|
||||
'@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||
|
||||
'@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||
dependencies:
|
||||
'@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4)
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||
|
||||
'@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||
dependencies:
|
||||
'@radix-ui/react-slot': 1.2.4(@types/react@19.2.14)(react@19.2.4)
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||
|
||||
'@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.4)':
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
|
||||
react: 19.2.4
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
|
||||
'@radix-ui/react-slot@1.2.4(@types/react@19.2.14)(react@19.2.4)':
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
|
||||
react: 19.2.4
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
|
||||
'@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
|
||||
'@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
|
||||
'@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||
'@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||
|
||||
'@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.4)':
|
||||
dependencies:
|
||||
'@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4)
|
||||
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||
react: 19.2.4
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
|
||||
'@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.4)':
|
||||
dependencies:
|
||||
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||
react: 19.2.4
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
|
||||
'@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.4)':
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
|
||||
'@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.2.4)':
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
|
||||
'@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.2.4)':
|
||||
dependencies:
|
||||
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||
react: 19.2.4
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
|
||||
'@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1))(react@19.2.4)':
|
||||
dependencies:
|
||||
'@standard-schema/spec': 1.1.0
|
||||
@ -10956,6 +11220,10 @@ snapshots:
|
||||
|
||||
ci-info@4.4.0: {}
|
||||
|
||||
class-variance-authority@0.7.1:
|
||||
dependencies:
|
||||
clsx: 2.1.1
|
||||
|
||||
classnames@2.5.1: {}
|
||||
|
||||
cli-cursor@5.0.0:
|
||||
@ -12937,6 +13205,11 @@ snapshots:
|
||||
|
||||
netmask@2.0.2: {}
|
||||
|
||||
next-themes@0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
|
||||
next-tick@0.2.2: {}
|
||||
|
||||
next@15.5.12(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user