Updated sidebar layout (#1661)

This commit is contained in:
Luke Vella 2025-04-14 15:11:59 +01:00 committed by GitHub
parent 8c0814b92b
commit 72ca1d4c38
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
104 changed files with 3268 additions and 1331 deletions

View file

@ -0,0 +1,84 @@
import * as Portal from "@radix-ui/react-portal";
import * as React from "react";
import { cn } from "./lib/utils";
const ACTION_BAR_PORTAL_ID = "action-bar-portal";
const ActionBar = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"pointer-events-none sticky bottom-8 flex justify-center pb-5",
className,
)}
id={ACTION_BAR_PORTAL_ID}
{...props}
/>
));
ActionBar.displayName = "ActionBar";
const ActionBarPortal = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<Portal.Root
container={document.getElementById(ACTION_BAR_PORTAL_ID)}
ref={ref}
className={className}
{...props}
/>
));
ActionBarPortal.displayName = "ActionBarPortal";
const ActionBarContainer = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
className={cn(
"bg-action-bar text-action-bar-foreground pointer-events-auto z-50 mx-auto inline-flex w-full max-w-2xl items-center gap-4 rounded-xl p-2 shadow-lg",
className,
)}
{...props}
/>
);
});
ActionBarContainer.displayName = "ActionBarContainer";
const ActionBarContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center px-2.5", className)}
{...props}
/>
));
ActionBarContent.displayName = "ActionBarContent";
const ActionBarGroup = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center gap-2", className)}
{...props}
/>
));
ActionBarGroup.displayName = "ActionBarGroup";
export {
ActionBar,
ActionBarContainer,
ActionBarContent,
ActionBarGroup,
ActionBarPortal,
};

View file

@ -9,7 +9,7 @@ const badgeVariants = cva(
variants: {
variant: {
primary: "bg-primary text-primary-50",
default: "bg-gray-50 border text-secondary-foreground",
default: "bg-gray-50 border text-muted-foreground",
destructive: "bg-destructive text-destructive-foreground",
outline: "text-foreground",
green: "bg-green-600 text-white",

View file

@ -19,12 +19,13 @@ const buttonVariants = cva(
"focus:ring-offset-1 border-primary bg-primary hover:bg-primary-500 disabled:bg-gray-400 disabled:border-transparent text-white shadow-sm",
destructive:
"focus:ring-offset-1 bg-destructive shadow-sm text-destructive-foreground active:bg-destructive border-destructive hover:bg-destructive/90",
default:
"focus:ring-offset-1 hover:bg-gray-100 bg-gray-50 active:bg-gray-200",
default: "focus:ring-offset-1 hover:bg-gray-50 bg-white",
secondary:
"focus:ring-offset-1 bg-secondary text-secondary-foreground hover:bg-secondary/80",
"focus:ring-offset-1 border-secondary bg-secondary hover:bg-secondary/80 text-secondary-foreground",
ghost:
"border-transparent bg-transparent data-[state=open]:bg-gray-500/20 text-gray-800 hover:bg-gray-500/10 active:bg-gray-500/20",
actionBar:
"border-transparent bg-transparent data-[state=open]:bg-gray-500/20 text-gray-800 hover:bg-gray-700 active:bg-gray-700/50",
link: "underline-offset-4 border-transparent hover:underline text-primary",
},
size: {
@ -32,6 +33,7 @@ const buttonVariants = cva(
sm: "h-8 text-sm px-2 gap-x-1.5 rounded-md",
lg: "h-12 text-base gap-x-3 px-4 rounded-lg",
icon: "size-7 text-sm gap-x-1.5 rounded-md",
"icon-lg": "size-8 rounded-full",
},
},
defaultVariants: {

View file

@ -6,6 +6,7 @@ import { SearchIcon } from "lucide-react";
import * as React from "react";
import { Dialog, DialogContent } from "./dialog";
import { usePlatform } from "./hooks/use-platform";
import { Icon } from "./icon";
import { cn } from "./lib/utils";
@ -29,8 +30,13 @@ type CommandDialogProps = DialogProps;
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="shadow-huge w-full max-w-3xl overflow-hidden p-0">
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
<DialogContent
hideCloseButton={true}
size="xl"
position="top"
className="shadow-huge p-0"
>
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:size-4 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:p-2 [&_[cmdk-item]_svg]:size-4">
{children}
</Command>
</DialogContent>
@ -68,7 +74,7 @@ const CommandList = React.forwardRef<
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
className={cn("h-[320px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
));
@ -95,7 +101,7 @@ const CommandGroup = React.forwardRef<
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-gray-500",
"overflow-hidden p-2 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-sm [&_[cmdk-group-heading]]:text-gray-500",
className,
)}
{...props}
@ -123,7 +129,23 @@ const CommandItem = React.forwardRef<
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex h-9 cursor-default select-none items-center rounded-md px-2 text-sm outline-none aria-selected:bg-gray-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"relative flex cursor-default select-none items-center gap-2 rounded-xl p-2 text-sm outline-none aria-selected:bg-gray-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
/>
));
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandItemShortcut = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex h-12 cursor-pointer select-none items-center gap-3 rounded-md px-3 font-medium outline-none aria-selected:bg-gray-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
@ -148,6 +170,17 @@ const CommandShortcut = ({
};
CommandShortcut.displayName = "CommandShortcut";
// Renders the Command (⌘) symbol on macs and Ctrl on windows
const CommandShortcutSymbol = ({ symbol }: { symbol: string }) => {
const { isMac } = usePlatform();
return (
<CommandShortcut>
{isMac ? "⌘" : "Ctrl"}+{symbol}
</CommandShortcut>
);
};
CommandShortcutSymbol.displayName = "CommandShortcutSymbol";
export {
Command,
CommandDialog,
@ -155,7 +188,9 @@ export {
CommandGroup,
CommandInput,
CommandItem,
CommandItemShortcut,
CommandList,
CommandSeparator,
CommandShortcut,
CommandShortcutSymbol,
};

View file

@ -1,6 +1,8 @@
"use client";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
import { XIcon } from "lucide-react";
import * as React from "react";
@ -32,47 +34,72 @@ const DialogOverlay = React.forwardRef<
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const dialogContentVariants = cva(
cn(
//style
"bg-background sm:rounded-lg sm:border shadow-lg p-6 gap-4",
// position
"fixed z-50 grid w-full top-0 left-1/2 -translate-x-1/2",
// animation
"duration-200 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-[state=open]:slide-in-from-left-1/2 data-[state=closed]:slide-out-to-left-1/2",
),
{
variants: {
position: {
top: "sm:top-48 data-[state=closed]:slide-out-to-top-[10%] data-[state=open]:slide-in-from-top-[10%]",
center:
"sm:top-[50%] sm:translate-y-[-50%] data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%]",
},
size: {
sm: "sm:max-w-sm",
md: "sm:max-w-md",
lg: "sm:max-w-lg",
xl: "sm:max-w-xl",
"2xl": "sm:max-w-2xl",
"3xl": "sm:max-w-3xl",
"4xl": "sm:max-w-4xl",
"5xl": "sm:max-w-5xl",
},
},
defaultVariants: {
position: "center",
size: "md",
},
},
);
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
size?: "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl" | "5xl";
hideCloseButton?: boolean;
}
>(({ className, children, size = "md", hideCloseButton, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"bg-background 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-[50%] top-0 z-50 grid w-full max-w-full translate-x-[-50%] gap-4 p-6 shadow-lg duration-200 sm:top-[50%] sm:translate-y-[-50%] sm:rounded-lg sm:border",
{
"sm:max-w-sm": size === "sm",
"sm:max-w-md": size === "md",
"sm:max-w-lg": size === "lg",
"sm:max-w-xl": size === "xl",
"sm:max-w-2xl": size === "2xl",
"sm:max-w-3xl": size === "3xl",
"sm:max-w-4xl": size === "4xl",
"sm:max-w-5xl": size === "5xl",
},
className,
)}
{...props}
>
{children}
{!hideCloseButton ? (
<DialogClose asChild className="absolute right-4 top-4">
<Button size="icon" variant="ghost">
<Icon>
<XIcon />
</Icon>
<span className="sr-only">Close</span>
</Button>
</DialogClose>
) : null}
</DialogPrimitive.Content>
</DialogPortal>
));
} & VariantProps<typeof dialogContentVariants>
>(
(
{ className, children, position, size = "md", hideCloseButton, ...props },
ref,
) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn("", dialogContentVariants({ position, size }), className)}
{...props}
>
{children}
{!hideCloseButton ? (
<DialogClose asChild className="absolute right-4 top-4">
<Button size="icon" variant="ghost">
<Icon>
<XIcon />
</Icon>
<span className="sr-only">Close</span>
</Button>
</DialogClose>
) : null}
</DialogPrimitive.Content>
</DialogPortal>
),
);
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({

View file

@ -120,12 +120,12 @@ const DropdownMenuCheckboxItem = React.forwardRef<
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Icon variant="success">
<Icon>
<CheckIcon />
</Icon>
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
<span className="flex items-center gap-2 text-sm">{children}</span>
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName =

View file

@ -0,0 +1,5 @@
"use client";
export function usePlatform() {
return { isMac: navigator.userAgent.includes("Mac") };
}

View file

@ -30,12 +30,18 @@ export interface IconProps extends VariantProps<typeof iconVariants> {
children?: React.ReactNode;
}
export function Icon({ children, size, variant }: IconProps) {
export function Icon({
children,
className,
size,
variant,
}: { className?: string } & IconProps) {
return (
<Slot
className={cn(
iconVariants({ size, variant }),
"group-[.bg-primary]:text-primary-50 group-[.bg-destructive]:text-destructive-foreground group shrink-0",
className,
)}
>
{children}

View file

@ -0,0 +1,53 @@
"use client";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import * as React from "react";
import { cn } from "./lib/utils";
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn("-mb-px flex space-x-4 border-b border-gray-200", className)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"ring-offset-background focus-visible:ring-ring inline-flex h-9 items-center whitespace-nowrap rounded-none border-b-2 px-1 pb-1 pt-1 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
"data-[state=active]:border-indigo-500 data-[state=inactive]:border-transparent data-[state=active]:text-indigo-600 data-[state=inactive]:text-gray-500 data-[state=inactive]:hover:border-gray-300 data-[state=inactive]:hover:text-gray-700",
className,
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"ring-offset-background focus-visible:ring-ring mt-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
className,
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsContent, TabsList, TabsTrigger };

View file

@ -0,0 +1,28 @@
"use client";
import * as ProgressPrimitive from "@radix-ui/react-progress";
import * as React from "react";
import { cn } from "./lib/utils";
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"bg-secondary relative h-1 w-full overflow-hidden rounded-full",
className,
)}
{...props}
>
<ProgressPrimitive.Indicator
className="bg-primary h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
));
Progress.displayName = ProgressPrimitive.Root.displayName;
export { Progress };

View file

@ -30,7 +30,7 @@ import {
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_MOBILE = "16rem";
const SIDEBAR_WIDTH_ICON = "3rem";
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
@ -380,7 +380,7 @@ const SidebarHeader = React.forwardRef<
<div
ref={ref}
data-sidebar="header"
className={cn("flex flex-col gap-2", className)}
className={cn("flex flex-col gap-2 px-2 py-3", className)}
{...props}
/>
);

View file

@ -58,7 +58,7 @@ const TableRow = React.forwardRef<
<tr
ref={ref}
className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b",
"hover:bg-muted/50 data-[state=selected]:bg-muted group border-b",
className,
)}
{...props}

73
packages/ui/src/tile.tsx Normal file
View file

@ -0,0 +1,73 @@
"use client";
import { Slot } from "@radix-ui/react-slot";
import Link from "next/link";
import * as React from "react";
import { cn } from "./lib/utils";
const Tile = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<typeof Link>
>(({ className, children, ...props }, ref) => (
<Link
ref={ref}
className={cn(
"text-card-foreground bg-background flex flex-col justify-end rounded-xl border p-3 shadow-sm transition-shadow hover:bg-gray-50 active:shadow-none",
className,
)}
{...props}
>
{children}
</Link>
));
Tile.displayName = "Tile";
const TileIcon = React.forwardRef<
HTMLElement,
React.HTMLAttributes<HTMLElement>
>(({ className, children, ...props }, ref) => (
<span className={cn("mb-3", className)}>
<Slot ref={ref} className="size-4" {...props}>
{children}
</Slot>
</span>
));
TileIcon.displayName = "TileIcon";
const TileTitle = React.forwardRef<
HTMLHeadingElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3 ref={ref} className={cn("mt-3 text-sm", className)} {...props} />
));
TileTitle.displayName = "TileTitle";
const TileDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-muted-foreground text-center text-sm", className)}
{...props}
/>
));
TileDescription.displayName = "TileDescription";
const TileGrid = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"grid gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4",
className,
)}
{...props}
/>
));
TileGrid.displayName = "TileGrid";
export { Tile, TileDescription, TileGrid, TileIcon, TileTitle };