mirror of
https://github.com/lukevella/rallly.git
synced 2025-07-29 14:17:49 +02:00
✨ Updated sidebar layout (#1661)
This commit is contained in:
parent
8c0814b92b
commit
72ca1d4c38
104 changed files with 3268 additions and 1331 deletions
84
packages/ui/src/action-bar.tsx
Normal file
84
packages/ui/src/action-bar.tsx
Normal 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,
|
||||
};
|
|
@ -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",
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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 = ({
|
||||
|
|
|
@ -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 =
|
||||
|
|
5
packages/ui/src/hooks/use-platform.ts
Normal file
5
packages/ui/src/hooks/use-platform.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
"use client";
|
||||
|
||||
export function usePlatform() {
|
||||
return { isMac: navigator.userAgent.includes("Mac") };
|
||||
}
|
|
@ -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}
|
||||
|
|
53
packages/ui/src/page-tabs.tsx
Normal file
53
packages/ui/src/page-tabs.tsx
Normal 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 };
|
28
packages/ui/src/progress.tsx
Normal file
28
packages/ui/src/progress.tsx
Normal 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 };
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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
73
packages/ui/src/tile.tsx
Normal 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 };
|
Loading…
Add table
Add a link
Reference in a new issue