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 ( +