Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from '@/components/ui/sidebar';
import { useSidebar } from '@/components/ui/sidebar-context';
import { cn } from '@/lib/utils';

export interface SidebarNavItem {
Expand Down
8 changes: 2 additions & 6 deletions src/components/common/LobeProviderIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const PROVIDER_ICON_MAP: Record<string, MonoIconComponent> = {
'x-ai': XAIIcon,
};

export interface LobeProviderIconProps {
interface LobeProviderIconProps {
className?: string;
fallbackLabel?: string;
provider?: string | null;
Expand All @@ -70,16 +70,12 @@ export interface LobeProviderIconProps {
title?: string;
}

export const normalizeLobeProviderKey = (value?: string | null) =>
const normalizeLobeProviderKey = (value?: string | null) =>
String(value ?? '')
.trim()
.toLowerCase()
.replace(/[\s_]+/g, '-');

export function hasLobeProviderIcon(provider?: string | null): boolean {
return Boolean(PROVIDER_ICON_MAP[normalizeLobeProviderKey(provider)]);
}

export function LobeProviderIcon({
className,
fallbackLabel,
Expand Down
2 changes: 1 addition & 1 deletion src/components/config/VisualConfigEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
TimerIcon,
} from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { buttonVariants } from '@/components/ui/button';
import { buttonVariants } from '@/components/ui/button-variants';
import {
Field,
FieldDescription,
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,4 @@ function Badge({
)
}

export { Badge, badgeVariants }
export { Badge }
1 change: 0 additions & 1 deletion src/components/ui/button-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,4 @@ export {
ButtonGroup,
ButtonGroupSeparator,
ButtonGroupText,
buttonGroupVariants,
}
46 changes: 46 additions & 0 deletions src/components/ui/button-variants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { cva, type VariantProps } from "class-variance-authority"

const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/80",
primary: "bg-primary text-primary-foreground hover:bg-primary/80",
outline:
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost:
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
destructive:
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
danger:
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default:
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
md: "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
icon: "size-8",
"icon-xs":
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
"icon-sm":
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
"icon-lg": "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)

type ButtonVariantProps = VariantProps<typeof buttonVariants>

export { buttonVariants, type ButtonVariantProps }
69 changes: 23 additions & 46 deletions src/components/ui/button.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,10 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { LoaderCircleIcon } from "lucide-react"
import { Slot } from "radix-ui"

import { buttonVariants, type ButtonVariantProps } from "@/components/ui/button-variants"
import { cn } from "@/lib/utils"

const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/80",
primary: "bg-primary text-primary-foreground hover:bg-primary/80",
outline:
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost:
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
destructive:
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
danger:
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default:
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
md: "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
icon: "size-8",
"icon-xs":
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
"icon-sm":
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
"icon-lg": "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)

function Button({
children,
className,
Expand All @@ -57,12 +16,24 @@ function Button({
disabled,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
ButtonVariantProps & {
asChild?: boolean
fullWidth?: boolean
loading?: boolean
}) {
const Comp = asChild ? Slot.Root : "button"
const loader = loading ? (
<LoaderCircleIcon data-icon="inline-start" className="animate-spin" />
) : null
const slottedChildren =
asChild && loader && React.isValidElement(children)
? React.cloneElement(
children,
undefined,
loader,
(children.props as { children?: React.ReactNode }).children
)
: children
Comment on lines +28 to +36

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When asChild is true and the slotted element contains multiple children (e.g., <Button asChild><a><Icon /> <span>Text</span></a></Button>), children.props.children will be an array. Passing this array directly as an argument to React.cloneElement alongside loader will cause React to treat it as a nested array and trigger a console warning:

Warning: Each child in a list should have a unique "key" prop.

To prevent this warning, we can flatten and safely key the children using React.Children.toArray and spread them into React.cloneElement.

Suggested change
const slottedChildren =
asChild && loader && React.isValidElement(children)
? React.cloneElement(
children,
undefined,
loader,
(children.props as { children?: React.ReactNode }).children
)
: children
const slottedChildren =
asChild && loader && React.isValidElement(children)
? React.cloneElement(
children,
undefined,
loader,
...React.Children.toArray((children.props as { children?: React.ReactNode }).children)
)
: children


return (
<Comp
Expand All @@ -73,10 +44,16 @@ function Button({
disabled={disabled || loading}
{...props}
>
{loading && <LoaderCircleIcon data-icon="inline-start" className="animate-spin" />}
{children}
{asChild ? (
slottedChildren
) : (
<>
{loader}
{children}
</>
)}
</Comp>
)
}

export { Button, buttonVariants }
export { Button }
24 changes: 24 additions & 0 deletions src/components/ui/sidebar-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from "react"

type SidebarContextProps = {
state: "expanded" | "collapsed"
open: boolean
setOpen: (open: boolean) => void
openMobile: boolean
setOpenMobile: (open: boolean) => void
isMobile: boolean
toggleSidebar: () => void
}

const SidebarContext = React.createContext<SidebarContextProps | null>(null)

function useSidebar() {
const context = React.useContext(SidebarContext)
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.")
}

return context
}

export { SidebarContext, useSidebar, type SidebarContextProps }
27 changes: 5 additions & 22 deletions src/components/ui/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import {
SidebarContext,
useSidebar,
type SidebarContextProps,
} from "@/components/ui/sidebar-context"
import {
Sheet,
SheetContent,
Expand All @@ -29,27 +34,6 @@ const SIDEBAR_WIDTH_MOBILE = "18rem"
const SIDEBAR_WIDTH_ICON = "3rem"
const SIDEBAR_KEYBOARD_SHORTCUT = "b"

type SidebarContextProps = {
state: "expanded" | "collapsed"
open: boolean
setOpen: (open: boolean) => void
openMobile: boolean
setOpenMobile: (open: boolean) => void
isMobile: boolean
toggleSidebar: () => void
}

const SidebarContext = React.createContext<SidebarContextProps | null>(null)

function useSidebar() {
const context = React.useContext(SidebarContext)
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.")
}

return context
}

function SidebarProvider({
defaultOpen = true,
open: openProp,
Expand Down Expand Up @@ -696,5 +680,4 @@ export {
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
}
2 changes: 1 addition & 1 deletion src/components/ui/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,4 @@ function TabsContent({
)
}

export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
export { Tabs, TabsList, TabsTrigger, TabsContent }
9 changes: 4 additions & 5 deletions src/components/ui/toggle-group.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
"use client"

import * as React from "react"
import { type VariantProps } from "class-variance-authority"
import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui"

import { cn } from "@/lib/utils"
import { toggleVariants } from "@/components/ui/toggle"
import { toggleVariants, type ToggleVariantProps } from "@/components/ui/toggle-variants"

const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants> & {
ToggleVariantProps & {
spacing?: number
orientation?: "horizontal" | "vertical"
}
Expand All @@ -28,7 +27,7 @@ function ToggleGroup({
children,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants> & {
ToggleVariantProps & {
spacing?: number
orientation?: "horizontal" | "vertical"
}) {
Expand Down Expand Up @@ -62,7 +61,7 @@ function ToggleGroupItem({
size = "default",
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>) {
ToggleVariantProps) {
const context = React.useContext(ToggleGroupContext)

return (
Expand Down
27 changes: 27 additions & 0 deletions src/components/ui/toggle-variants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { cva, type VariantProps } from "class-variance-authority"

const toggleVariants = cva(
"group/toggle inline-flex items-center justify-center gap-1 rounded-lg text-sm font-medium whitespace-nowrap transition-all outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted data-[state=on]:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-transparent",
outline: "border border-input bg-transparent hover:bg-muted",
},
size: {
default:
"h-8 min-w-8 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
sm: "h-7 min-w-7 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-9 min-w-9 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)

type ToggleVariantProps = VariantProps<typeof toggleVariants>

export { toggleVariants, type ToggleVariantProps }
28 changes: 3 additions & 25 deletions src/components/ui/toggle.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,16 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Toggle as TogglePrimitive } from "radix-ui"

import { cn } from "@/lib/utils"

const toggleVariants = cva(
"group/toggle inline-flex items-center justify-center gap-1 rounded-lg text-sm font-medium whitespace-nowrap transition-all outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted data-[state=on]:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-transparent",
outline: "border border-input bg-transparent hover:bg-muted",
},
size: {
default:
"h-8 min-w-8 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
sm: "h-7 min-w-7 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-9 min-w-9 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
import { toggleVariants, type ToggleVariantProps } from "@/components/ui/toggle-variants"

function Toggle({
className,
variant = "default",
size = "default",
...props
}: React.ComponentProps<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>) {
ToggleVariantProps) {
return (
<TogglePrimitive.Root
data-slot="toggle"
Expand All @@ -42,4 +20,4 @@ function Toggle({
)
}

export { Toggle, toggleVariants }
export { Toggle }
Loading