diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx
index ae754f82..fc7cead5 100644
--- a/apps/desktop/src/App.tsx
+++ b/apps/desktop/src/App.tsx
@@ -1,19 +1,7 @@
-import { useState } from 'react'
-import { Button } from '@multica/ui/components/button'
+import { ComponentExample } from '@multica/ui/components/component-example'
function App() {
- const [count, setCount] = useState(0)
-
- return (
-
-
-
Desktop App
-
-
-
- )
+ return
}
export default App
diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx
index 14115273..5e476424 100644
--- a/apps/web/app/page.tsx
+++ b/apps/web/app/page.tsx
@@ -1,5 +1,7 @@
-import { ComponentExample } from "@/components/component-example";
+"use client"
+
+import { ComponentExample } from "@multica/ui/components/component-example";
export default function Page() {
-return ;
+ return ;
}
\ No newline at end of file
diff --git a/apps/web/components.json b/apps/web/components.json
index eb352119..38168ef0 100644
--- a/apps/web/components.json
+++ b/apps/web/components.json
@@ -15,7 +15,7 @@
"hooks": "@/hooks",
"lib": "@/lib",
"utils": "@multica/ui/lib/utils",
- "ui": "@multica/ui/components"
+ "ui": "@multica/ui/components/ui"
},
"menuColor": "default",
"menuAccent": "subtle"
diff --git a/packages/ui/components.json b/packages/ui/components.json
index 7723b5bc..b439442e 100644
--- a/packages/ui/components.json
+++ b/packages/ui/components.json
@@ -15,6 +15,6 @@
"utils": "@multica/ui/lib/utils",
"hooks": "@multica/ui/hooks",
"lib": "@multica/ui/lib",
- "ui": "@multica/ui/components"
+ "ui": "@multica/ui/components/ui"
}
}
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 1f3de6bf..2bd86e54 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -8,6 +8,7 @@
"./postcss.config": "./postcss.config.mjs",
"./lib/*": "./src/lib/*.ts",
"./components/*": "./src/components/*.tsx",
+ "./components/ui/*": "./src/components/ui/*.tsx",
"./hooks/*": "./src/hooks/*.ts"
},
"dependencies": {
diff --git a/apps/web/components/component-example.tsx b/packages/ui/src/components/component-example.tsx
similarity index 97%
rename from apps/web/components/component-example.tsx
rename to packages/ui/src/components/component-example.tsx
index 47125245..2b4a96ee 100644
--- a/apps/web/components/component-example.tsx
+++ b/packages/ui/src/components/component-example.tsx
@@ -1,11 +1,9 @@
-"use client"
-
import * as React from "react"
import {
Example,
ExampleWrapper,
-} from "@/components/example"
+} from "@multica/ui/components/example"
import {
AlertDialog,
AlertDialogAction,
@@ -17,9 +15,9 @@ import {
AlertDialogMedia,
AlertDialogTitle,
AlertDialogTrigger,
-} from "@multica/ui/components/alert-dialog"
-import { Badge } from "@multica/ui/components/badge"
-import { Button } from "@multica/ui/components/button"
+} from "@multica/ui/components/ui/alert-dialog"
+import { Badge } from "@multica/ui/components/ui/badge"
+import { Button } from "@multica/ui/components/ui/button"
import {
Card,
CardAction,
@@ -28,7 +26,7 @@ import {
CardFooter,
CardHeader,
CardTitle,
-} from "@multica/ui/components/card"
+} from "@multica/ui/components/ui/card"
import {
Combobox,
ComboboxContent,
@@ -36,7 +34,7 @@ import {
ComboboxInput,
ComboboxItem,
ComboboxList,
-} from "@multica/ui/components/combobox"
+} from "@multica/ui/components/ui/combobox"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
@@ -53,9 +51,9 @@ import {
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
-} from "@multica/ui/components/dropdown-menu"
-import { Field, FieldGroup, FieldLabel } from "@multica/ui/components/field"
-import { Input } from "@multica/ui/components/input"
+} from "@multica/ui/components/ui/dropdown-menu"
+import { Field, FieldGroup, FieldLabel } from "@multica/ui/components/ui/field"
+import { Input } from "@multica/ui/components/ui/input"
import {
Select,
SelectContent,
@@ -63,8 +61,8 @@ import {
SelectItem,
SelectTrigger,
SelectValue,
-} from "@multica/ui/components/select"
-import { Textarea } from "@multica/ui/components/textarea"
+} from "@multica/ui/components/ui/select"
+import { Textarea } from "@multica/ui/components/ui/textarea"
import { HugeiconsIcon } from "@hugeicons/react"
import { PlusSignIcon, BluetoothIcon, MoreVerticalCircle01Icon, FileIcon, FolderIcon, FolderOpenIcon, CodeIcon, MoreHorizontalCircle01Icon, SearchIcon, FloppyDiskIcon, DownloadIcon, EyeIcon, LayoutIcon, PaintBoardIcon, SunIcon, MoonIcon, ComputerIcon, UserIcon, CreditCardIcon, SettingsIcon, KeyboardIcon, LanguageCircleIcon, NotificationIcon, MailIcon, ShieldIcon, HelpCircleIcon, File01Icon, LogoutIcon } from "@hugeicons/core-free-icons"
diff --git a/apps/web/components/example.tsx b/packages/ui/src/components/example.tsx
similarity index 100%
rename from apps/web/components/example.tsx
rename to packages/ui/src/components/example.tsx
diff --git a/packages/ui/src/components/alert-dialog.tsx b/packages/ui/src/components/ui/alert-dialog.tsx
similarity index 98%
rename from packages/ui/src/components/alert-dialog.tsx
rename to packages/ui/src/components/ui/alert-dialog.tsx
index 898a4907..2414ceb3 100644
--- a/packages/ui/src/components/alert-dialog.tsx
+++ b/packages/ui/src/components/ui/alert-dialog.tsx
@@ -4,7 +4,7 @@ import * as React from "react"
import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog"
import { cn } from "@multica/ui/lib/utils"
-import { Button } from "@multica/ui/components/button"
+import { Button } from "@multica/ui/components/ui/button"
function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) {
return
diff --git a/packages/ui/src/components/badge.tsx b/packages/ui/src/components/ui/badge.tsx
similarity index 100%
rename from packages/ui/src/components/badge.tsx
rename to packages/ui/src/components/ui/badge.tsx
diff --git a/packages/ui/src/components/button.tsx b/packages/ui/src/components/ui/button.tsx
similarity index 100%
rename from packages/ui/src/components/button.tsx
rename to packages/ui/src/components/ui/button.tsx
diff --git a/packages/ui/src/components/card.tsx b/packages/ui/src/components/ui/card.tsx
similarity index 100%
rename from packages/ui/src/components/card.tsx
rename to packages/ui/src/components/ui/card.tsx
diff --git a/packages/ui/src/components/combobox.tsx b/packages/ui/src/components/ui/combobox.tsx
similarity index 98%
rename from packages/ui/src/components/combobox.tsx
rename to packages/ui/src/components/ui/combobox.tsx
index f0d61a65..e660471d 100644
--- a/packages/ui/src/components/combobox.tsx
+++ b/packages/ui/src/components/ui/combobox.tsx
@@ -4,13 +4,13 @@ import * as React from "react"
import { Combobox as ComboboxPrimitive } from "@base-ui/react"
import { cn } from "@multica/ui/lib/utils"
-import { Button } from "@multica/ui/components/button"
+import { Button } from "@multica/ui/components/ui/button"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
-} from "@multica/ui/components/input-group"
+} from "@multica/ui/components/ui/input-group"
import { HugeiconsIcon } from "@hugeicons/react"
import { ArrowDown01Icon, Cancel01Icon, Tick02Icon } from "@hugeicons/core-free-icons"
diff --git a/packages/ui/src/components/dropdown-menu.tsx b/packages/ui/src/components/ui/dropdown-menu.tsx
similarity index 100%
rename from packages/ui/src/components/dropdown-menu.tsx
rename to packages/ui/src/components/ui/dropdown-menu.tsx
diff --git a/packages/ui/src/components/field.tsx b/packages/ui/src/components/ui/field.tsx
similarity index 97%
rename from packages/ui/src/components/field.tsx
rename to packages/ui/src/components/ui/field.tsx
index 272c7fef..8a7af6da 100644
--- a/packages/ui/src/components/field.tsx
+++ b/packages/ui/src/components/ui/field.tsx
@@ -4,8 +4,8 @@ import { useMemo } from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@multica/ui/lib/utils"
-import { Label } from "@multica/ui/components/label"
-import { Separator } from "@multica/ui/components/separator"
+import { Label } from "@multica/ui/components/ui/label"
+import { Separator } from "@multica/ui/components/ui/separator"
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
return (
diff --git a/packages/ui/src/components/input-group.tsx b/packages/ui/src/components/ui/input-group.tsx
similarity index 96%
rename from packages/ui/src/components/input-group.tsx
rename to packages/ui/src/components/ui/input-group.tsx
index 1b65fed5..16c431be 100644
--- a/packages/ui/src/components/input-group.tsx
+++ b/packages/ui/src/components/ui/input-group.tsx
@@ -4,9 +4,9 @@ import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@multica/ui/lib/utils"
-import { Button } from "@multica/ui/components/button"
-import { Input } from "@multica/ui/components/input"
-import { Textarea } from "@multica/ui/components/textarea"
+import { Button } from "@multica/ui/components/ui/button"
+import { Input } from "@multica/ui/components/ui/input"
+import { Textarea } from "@multica/ui/components/ui/textarea"
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
diff --git a/packages/ui/src/components/input.tsx b/packages/ui/src/components/ui/input.tsx
similarity index 100%
rename from packages/ui/src/components/input.tsx
rename to packages/ui/src/components/ui/input.tsx
diff --git a/packages/ui/src/components/label.tsx b/packages/ui/src/components/ui/label.tsx
similarity index 100%
rename from packages/ui/src/components/label.tsx
rename to packages/ui/src/components/ui/label.tsx
diff --git a/packages/ui/src/components/select.tsx b/packages/ui/src/components/ui/select.tsx
similarity index 100%
rename from packages/ui/src/components/select.tsx
rename to packages/ui/src/components/ui/select.tsx
diff --git a/packages/ui/src/components/separator.tsx b/packages/ui/src/components/ui/separator.tsx
similarity index 100%
rename from packages/ui/src/components/separator.tsx
rename to packages/ui/src/components/ui/separator.tsx
diff --git a/packages/ui/src/components/ui/sheet.tsx b/packages/ui/src/components/ui/sheet.tsx
new file mode 100644
index 00000000..69aea14e
--- /dev/null
+++ b/packages/ui/src/components/ui/sheet.tsx
@@ -0,0 +1,129 @@
+"use client"
+
+import * as React from "react"
+import { Dialog as SheetPrimitive } from "@base-ui/react/dialog"
+
+import { cn } from "@multica/ui/lib/utils"
+import { Button } from "@multica/ui/components/ui/button"
+import { HugeiconsIcon } from "@hugeicons/react"
+import { Cancel01Icon } from "@hugeicons/core-free-icons"
+
+function Sheet({ ...props }: SheetPrimitive.Root.Props) {
+ return
+}
+
+function SheetTrigger({ ...props }: SheetPrimitive.Trigger.Props) {
+ return
+}
+
+function SheetClose({ ...props }: SheetPrimitive.Close.Props) {
+ return
+}
+
+function SheetPortal({ ...props }: SheetPrimitive.Portal.Props) {
+ return
+}
+
+function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
+ return (
+
+ )
+}
+
+function SheetContent({
+ className,
+ children,
+ side = "right",
+ showCloseButton = true,
+ ...props
+}: SheetPrimitive.Popup.Props & {
+ side?: "top" | "right" | "bottom" | "left"
+ showCloseButton?: boolean
+}) {
+ return (
+
+
+
+ {children}
+ {showCloseButton && (
+
+ }
+ >
+
+ Close
+
+ )}
+
+
+ )
+}
+
+function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
+ return (
+
+ )
+}
+
+function SheetDescription({
+ className,
+ ...props
+}: SheetPrimitive.Description.Props) {
+ return (
+
+ )
+}
+
+export {
+ Sheet,
+ SheetTrigger,
+ SheetClose,
+ SheetContent,
+ SheetHeader,
+ SheetFooter,
+ SheetTitle,
+ SheetDescription,
+}
diff --git a/packages/ui/src/components/ui/sidebar.tsx b/packages/ui/src/components/ui/sidebar.tsx
new file mode 100644
index 00000000..67b9f5c7
--- /dev/null
+++ b/packages/ui/src/components/ui/sidebar.tsx
@@ -0,0 +1,723 @@
+"use client"
+
+import * as React from "react"
+import { mergeProps } from "@base-ui/react/merge-props"
+import { useRender } from "@base-ui/react/use-render"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@multica/ui/lib/utils"
+import { Button } from "@multica/ui/components/ui/button"
+import { Input } from "@multica/ui/components/ui/input"
+import { Separator } from "@multica/ui/components/ui/separator"
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetHeader,
+ SheetTitle,
+} from "@multica/ui/components/ui/sheet"
+import { Skeleton } from "@multica/ui/components/ui/skeleton"
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@multica/ui/components/ui/tooltip"
+import { useIsMobile } from "@multica/ui/hooks/use-mobile"
+import { HugeiconsIcon } from "@hugeicons/react"
+import { SidebarLeftIcon } from "@hugeicons/core-free-icons"
+
+const SIDEBAR_COOKIE_NAME = "sidebar_state"
+const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
+const SIDEBAR_WIDTH = "16rem"
+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(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,
+ onOpenChange: setOpenProp,
+ className,
+ style,
+ children,
+ ...props
+}: React.ComponentProps<"div"> & {
+ defaultOpen?: boolean
+ open?: boolean
+ onOpenChange?: (open: boolean) => void
+}) {
+ const isMobile = useIsMobile()
+ const [openMobile, setOpenMobile] = React.useState(false)
+
+ // This is the internal state of the sidebar.
+ // We use openProp and setOpenProp for control from outside the component.
+ const [_open, _setOpen] = React.useState(defaultOpen)
+ const open = openProp ?? _open
+ const setOpen = React.useCallback(
+ (value: boolean | ((value: boolean) => boolean)) => {
+ const openState = typeof value === "function" ? value(open) : value
+ if (setOpenProp) {
+ setOpenProp(openState)
+ } else {
+ _setOpen(openState)
+ }
+
+ // This sets the cookie to keep the sidebar state.
+ document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
+ },
+ [setOpenProp, open]
+ )
+
+ // Helper to toggle the sidebar.
+ const toggleSidebar = React.useCallback(() => {
+ return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
+ }, [isMobile, setOpen, setOpenMobile])
+
+ // Adds a keyboard shortcut to toggle the sidebar.
+ React.useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (
+ event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
+ (event.metaKey || event.ctrlKey)
+ ) {
+ event.preventDefault()
+ toggleSidebar()
+ }
+ }
+
+ window.addEventListener("keydown", handleKeyDown)
+ return () => window.removeEventListener("keydown", handleKeyDown)
+ }, [toggleSidebar])
+
+ // We add a state so that we can do data-state="expanded" or "collapsed".
+ // This makes it easier to style the sidebar with Tailwind classes.
+ const state = open ? "expanded" : "collapsed"
+
+ const contextValue = React.useMemo(
+ () => ({
+ state,
+ open,
+ setOpen,
+ isMobile,
+ openMobile,
+ setOpenMobile,
+ toggleSidebar,
+ }),
+ [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
+ )
+
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+function Sidebar({
+ side = "left",
+ variant = "sidebar",
+ collapsible = "offcanvas",
+ className,
+ children,
+ ...props
+}: React.ComponentProps<"div"> & {
+ side?: "left" | "right"
+ variant?: "sidebar" | "floating" | "inset"
+ collapsible?: "offcanvas" | "icon" | "none"
+}) {
+ const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
+
+ if (collapsible === "none") {
+ return (
+
+ {children}
+
+ )
+ }
+
+ if (isMobile) {
+ return (
+
+
+
+ Sidebar
+ Displays the mobile sidebar.
+
+ {children}
+
+
+ )
+ }
+
+ return (
+
+ {/* This is what handles the sidebar gap on desktop */}
+
+
+
+ )
+}
+
+function SidebarTrigger({
+ className,
+ onClick,
+ ...props
+}: React.ComponentProps) {
+ const { toggleSidebar } = useSidebar()
+
+ return (
+
+ )
+}
+
+function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
+ const { toggleSidebar } = useSidebar()
+
+ return (
+
+ )
+}
+
+function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
+ return (
+
+ )
+}
+
+function SidebarInput({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SidebarSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SidebarGroupLabel({
+ className,
+ render,
+ ...props
+}: useRender.ComponentProps<"div"> & React.ComponentProps<"div">) {
+ return useRender({
+ defaultTagName: "div",
+ props: mergeProps<"div">(
+ {
+ className: cn(
+ "text-sidebar-foreground/70 ring-sidebar-ring h-8 rounded-md px-2 text-xs font-medium transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 flex shrink-0 items-center outline-hidden [&>svg]:shrink-0",
+ className
+ ),
+ },
+ props
+ ),
+ render,
+ state: {
+ slot: "sidebar-group-label",
+ sidebar: "group-label",
+ },
+ })
+}
+
+function SidebarGroupAction({
+ className,
+ render,
+ ...props
+}: useRender.ComponentProps<"button"> & React.ComponentProps<"button">) {
+ return useRender({
+ defaultTagName: "button",
+ props: mergeProps<"button">(
+ {
+ className: cn(
+ "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 w-5 rounded-md p-0 focus-visible:ring-2 [&>svg]:size-4 flex aspect-square items-center justify-center outline-hidden transition-transform [&>svg]:shrink-0 after:absolute after:-inset-2 md:after:hidden group-data-[collapsible=icon]:hidden",
+ className
+ ),
+ },
+ props
+ ),
+ render,
+ state: {
+ slot: "sidebar-group-action",
+ sidebar: "group-action",
+ },
+ })
+}
+
+function SidebarGroupContent({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
+ return (
+
+ )
+}
+
+function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
+ return (
+
+ )
+}
+
+const sidebarMenuButtonVariants = cva(
+ "ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground gap-2 rounded-md p-2 text-left text-sm transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! focus-visible:ring-2 data-active:font-medium peer/menu-button flex w-full items-center overflow-hidden outline-hidden group/menu-button disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
+ outline: "bg-background hover:bg-sidebar-accent hover:text-sidebar-accent-foreground shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
+ },
+ size: {
+ default: "h-8 text-sm",
+ sm: "h-7 text-xs",
+ lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+function SidebarMenuButton({
+ render,
+ isActive = false,
+ variant = "default",
+ size = "default",
+ tooltip,
+ className,
+ ...props
+}: useRender.ComponentProps<"button"> &
+ React.ComponentProps<"button"> & {
+ isActive?: boolean
+ tooltip?: string | React.ComponentProps
+ } & VariantProps) {
+ const { isMobile, state } = useSidebar()
+ const comp = useRender({
+ defaultTagName: "button",
+ props: mergeProps<"button">(
+ {
+ className: cn(sidebarMenuButtonVariants({ variant, size }), className),
+ },
+ props
+ ),
+ render: !tooltip ? render : TooltipTrigger,
+ state: {
+ slot: "sidebar-menu-button",
+ sidebar: "menu-button",
+ size,
+ active: isActive,
+ },
+ })
+
+ if (!tooltip) {
+ return comp
+ }
+
+ if (typeof tooltip === "string") {
+ tooltip = {
+ children: tooltip,
+ }
+ }
+
+ return (
+
+ {comp}
+
+
+ )
+}
+
+function SidebarMenuAction({
+ className,
+ render,
+ showOnHover = false,
+ ...props
+}: useRender.ComponentProps<"button"> &
+ React.ComponentProps<"button"> & {
+ showOnHover?: boolean
+ }) {
+ return useRender({
+ defaultTagName: "button",
+ props: mergeProps<"button">(
+ {
+ className: cn(
+ "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 aspect-square w-5 rounded-md p-0 peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 focus-visible:ring-2 [&>svg]:size-4 flex items-center justify-center outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 md:after:hidden [&>svg]:shrink-0",
+ showOnHover &&
+ "peer-data-active/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-open:opacity-100 md:opacity-0",
+ className
+ ),
+ },
+ props
+ ),
+ render,
+ state: {
+ slot: "sidebar-menu-action",
+ sidebar: "menu-action",
+ },
+ })
+}
+
+function SidebarMenuBadge({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SidebarMenuSkeleton({
+ className,
+ showIcon = false,
+ ...props
+}: React.ComponentProps<"div"> & {
+ showIcon?: boolean
+}) {
+ // Random width between 50 to 90%.
+ const [width] = React.useState(() => {
+ return `${Math.floor(Math.random() * 40) + 50}%`
+ })
+
+ return (
+
+ {showIcon && (
+
+ )}
+
+
+ )
+}
+
+function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
+ return (
+
+ )
+}
+
+function SidebarMenuSubItem({
+ className,
+ ...props
+}: React.ComponentProps<"li">) {
+ return (
+
+ )
+}
+
+function SidebarMenuSubButton({
+ render,
+ size = "md",
+ isActive = false,
+ className,
+ ...props
+}: useRender.ComponentProps<"a"> &
+ React.ComponentProps<"a"> & {
+ size?: "sm" | "md"
+ isActive?: boolean
+ }) {
+ return useRender({
+ defaultTagName: "a",
+ props: mergeProps<"a">(
+ {
+ className: cn(
+ "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground h-7 gap-2 rounded-md px-2 focus-visible:ring-2 data-[size=md]:text-sm data-[size=sm]:text-xs [&>svg]:size-4 flex min-w-0 -translate-x-px items-center overflow-hidden outline-hidden group-data-[collapsible=icon]:hidden disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:shrink-0",
+ className
+ ),
+ },
+ props
+ ),
+ render,
+ state: {
+ slot: "sidebar-menu-sub-button",
+ sidebar: "menu-sub-button",
+ size,
+ active: isActive,
+ },
+ })
+}
+
+export {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarGroup,
+ SidebarGroupAction,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarHeader,
+ SidebarInput,
+ SidebarInset,
+ SidebarMenu,
+ SidebarMenuAction,
+ SidebarMenuBadge,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarMenuSkeleton,
+ SidebarMenuSub,
+ SidebarMenuSubButton,
+ SidebarMenuSubItem,
+ SidebarProvider,
+ SidebarRail,
+ SidebarSeparator,
+ SidebarTrigger,
+ useSidebar,
+}
diff --git a/packages/ui/src/components/ui/skeleton.tsx b/packages/ui/src/components/ui/skeleton.tsx
new file mode 100644
index 00000000..32afbcc1
--- /dev/null
+++ b/packages/ui/src/components/ui/skeleton.tsx
@@ -0,0 +1,13 @@
+import { cn } from "@multica/ui/lib/utils"
+
+function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export { Skeleton }
diff --git a/packages/ui/src/components/ui/switch.tsx b/packages/ui/src/components/ui/switch.tsx
new file mode 100644
index 00000000..cb7c1f51
--- /dev/null
+++ b/packages/ui/src/components/ui/switch.tsx
@@ -0,0 +1,32 @@
+"use client"
+
+import { Switch as SwitchPrimitive } from "@base-ui/react/switch"
+
+import { cn } from "@multica/ui/lib/utils"
+
+function Switch({
+ className,
+ size = "default",
+ ...props
+}: SwitchPrimitive.Root.Props & {
+ size?: "sm" | "default"
+}) {
+ return (
+
+
+
+ )
+}
+
+export { Switch }
diff --git a/packages/ui/src/components/textarea.tsx b/packages/ui/src/components/ui/textarea.tsx
similarity index 100%
rename from packages/ui/src/components/textarea.tsx
rename to packages/ui/src/components/ui/textarea.tsx
diff --git a/packages/ui/src/components/tooltip.tsx b/packages/ui/src/components/ui/tooltip.tsx
similarity index 100%
rename from packages/ui/src/components/tooltip.tsx
rename to packages/ui/src/components/ui/tooltip.tsx
diff --git a/packages/ui/src/styles/globals.css b/packages/ui/src/styles/globals.css
index e0b3cca3..98c7c9eb 100644
--- a/packages/ui/src/styles/globals.css
+++ b/packages/ui/src/styles/globals.css
@@ -3,6 +3,7 @@
@source "../**/*.{ts,tsx}";
@import "tw-animate-css";
+@import "shadcn/tailwind.css";
@custom-variant dark (&:is(.dark *));