chore(deps): update and improve component styles

This commit is contained in:
enscribe 2025-03-21 14:46:08 -07:00
parent cf570be96e
commit 000cb09020
No known key found for this signature in database
GPG key ID: 9BBD5C4114E25322
28 changed files with 1326 additions and 1249 deletions

1154
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -13,11 +13,11 @@
}, },
"dependencies": { "dependencies": {
"@astrojs/check": "^0.9.4", "@astrojs/check": "^0.9.4",
"@astrojs/markdown-remark": "^6.1.0", "@astrojs/markdown-remark": "^6.3.1",
"@astrojs/mdx": "^4.0.8", "@astrojs/mdx": "^4.2.1",
"@astrojs/react": "^4.2.0", "@astrojs/react": "^4.2.1",
"@astrojs/rss": "^4.0.11", "@astrojs/rss": "^4.0.11",
"@astrojs/sitemap": "^3.2.1", "@astrojs/sitemap": "^3.3.0",
"@expressive-code/plugin-collapsible-sections": "^0.40.2", "@expressive-code/plugin-collapsible-sections": "^0.40.2",
"@expressive-code/plugin-line-numbers": "^0.40.2", "@expressive-code/plugin-line-numbers": "^0.40.2",
"@hbsnow/rehype-sectionize": "^1.0.7", "@hbsnow/rehype-sectionize": "^1.0.7",
@ -27,21 +27,21 @@
"@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-scroll-area": "^1.2.3", "@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-slot": "^1.1.2",
"@rehype-pretty/transformers": "^0.13.2", "@rehype-pretty/transformers": "^0.13.2",
"@shikijs/transformers": "^1.29.2", "@shikijs/transformers": "^1.29.2",
"@tailwindcss/vite": "^4.0.7", "@tailwindcss/vite": "^4.0.7",
"@types/react": "^18.3.18", "@types/react": "19.0.0",
"@types/react-dom": "^18.3.5", "@types/react-dom": "19.0.0",
"astro": "^5.3.0", "astro": "^5.5.4",
"astro-expressive-code": "^0.40.2", "astro-expressive-code": "^0.40.2",
"astro-icon": "^1.1.5", "astro-icon": "^1.1.5",
"bootstrap-icons": "^1.11.3", "bootstrap-icons": "^1.11.3",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.469.0", "lucide-react": "^0.469.0",
"react": "^18.3.1", "react": "19.0.0",
"react-dom": "^18.3.1", "react-dom": "19.0.0",
"rehype-external-links": "^3.0.0", "rehype-external-links": "^3.0.0",
"rehype-katex": "^7.0.1", "rehype-katex": "^7.0.1",
"rehype-pretty-code": "^0.14.0", "rehype-pretty-code": "^0.14.0",
@ -50,7 +50,6 @@
"remark-toc": "^9.0.0", "remark-toc": "^9.0.0",
"tailwind-merge": "^3.0.1", "tailwind-merge": "^3.0.1",
"tailwindcss": "^4.0.7", "tailwindcss": "^4.0.7",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.7.3" "typescript": "^5.7.3"
}, },
"devDependencies": { "devDependencies": {

View file

@ -55,7 +55,7 @@ const socialLinks: SocialLink[] = [
<div class="flex grow flex-col justify-between gap-y-4"> <div class="flex grow flex-col justify-between gap-y-4">
<div> <div>
<div class="flex flex-wrap items-center gap-x-2"> <div class="flex flex-wrap items-center gap-x-2">
<h3 class="text-lg font-semibold">{name}</h3> <h3 class="text-lg font-medium">{name}</h3>
{ {
pronouns && ( pronouns && (
<span class="text-muted-foreground text-sm">({pronouns})</span> <span class="text-muted-foreground text-sm">({pronouns})</span>

View file

@ -6,6 +6,7 @@ import { parseAuthors } from '@/lib/server-utils'
import { formatDate, readingTime } from '@/lib/utils' import { formatDate, readingTime } from '@/lib/utils'
import { Image } from 'astro:assets' import { Image } from 'astro:assets'
import type { CollectionEntry } from 'astro:content' import type { CollectionEntry } from 'astro:content'
import { Icon } from 'astro-icon/components'
import Link from './Link.astro' import Link from './Link.astro'
type Props = { type Props = {
@ -42,7 +43,7 @@ const authors = await parseAuthors(entry.data.authors ?? [])
) )
} }
<div class="grow"> <div class="grow">
<h3 class="mb-1 text-lg font-semibold"> <h3 class="mb-1 text-lg font-medium">
{entry.data.title} {entry.data.title}
</h3> </h3>
<p class="text-muted-foreground mb-2 text-sm"> <p class="text-muted-foreground mb-2 text-sm">
@ -66,19 +67,22 @@ const authors = await parseAuthors(entry.data.authors ?? [])
<span>{author.name}</span> <span>{author.name}</span>
</div> </div>
))} ))}
<Separator orientation="vertical" className="h-4" /> <Separator orientation="vertical" className="h-4!" />
</> </>
) )
} }
<span>{formattedDate}</span> <span>{formattedDate}</span>
<Separator orientation="vertical" className="h-4" /> <Separator orientation="vertical" className="h-4!" />
<span>{readTime}</span> <span>{readTime}</span>
</div> </div>
{ {
entry.data.tags && ( entry.data.tags && (
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
{entry.data.tags.map((tag) => ( {entry.data.tags.map((tag) => (
<Badge variant="secondary">{tag}</Badge> <Badge variant="secondary" className="flex items-center gap-x-1">
<Icon name="lucide:hash" class="size-3" />
{tag}
</Badge>
))} ))}
</div> </div>
) )

View file

@ -15,7 +15,7 @@ import SocialIcons from './SocialIcons.astro'
<span class="text-muted-foreground text-center text-sm"> <span class="text-muted-foreground text-center text-sm">
&copy; {new Date().getFullYear()} All rights reserved. &copy; {new Date().getFullYear()} All rights reserved.
</span> </span>
<Separator orientation="vertical" className="h-4" /> <Separator orientation="vertical" className="h-4!" />
<p class="text-muted-foreground text-center text-sm"> <p class="text-muted-foreground text-center text-sm">
Made with 🤍 by <Link Made with 🤍 by <Link
href="https://github.com/jktrn" href="https://github.com/jktrn"

View file

@ -16,7 +16,7 @@ import logo from '../../public/static/logo.svg'
<div class="flex flex-wrap items-center justify-between gap-4 py-4"> <div class="flex flex-wrap items-center justify-between gap-4 py-4">
<Link <Link
href="/" href="/"
class="hover:text-primary flex shrink-0 items-center gap-2 text-xl font-semibold transition-colors duration-300" class="hover:text-primary flex shrink-0 items-center gap-2 text-xl font-medium transition-colors duration-300"
> >
<Image src={logo} alt="Logo" class="size-8" /> <Image src={logo} alt="Logo" class="size-8" />
{SITE.TITLE} {SITE.TITLE}

View file

@ -7,25 +7,25 @@ import { Icon } from 'astro-icon/components'
const { prevPost, nextPost } = Astro.props const { prevPost, nextPost } = Astro.props
--- ---
<div class="col-start-2 flex flex-col gap-4 sm:flex-row"> <div class="col-start-2 grid grid-cols-1 sm:grid-cols-2 gap-4">
<Link <Link
href={nextPost ? `/blog/${nextPost.id}` : '#'} href={nextPost ? `/blog/${nextPost.id}` : '#'}
class={cn( class={cn(
buttonVariants({ variant: 'outline' }), buttonVariants({ variant: 'outline' }),
'rounded-xl group flex items-center justify-start w-full sm:w-1/2 h-fit', 'rounded-xl group flex items-center justify-start w-full h-full',
!nextPost && 'pointer-events-none opacity-50 cursor-not-allowed', !nextPost && 'pointer-events-none opacity-50 cursor-not-allowed',
)} )}
aria-disabled={!nextPost} aria-disabled={!nextPost}
> >
<div class="mr-2 shrink-0"> <div class="mr-2 flex-shrink-0">
<Icon <Icon
name="lucide:arrow-left" name="lucide:arrow-left"
class="size-4 transition-transform group-hover:-translate-x-1" class="size-4 transition-transform group-hover:-translate-x-1"
/> />
</div> </div>
<div class="flex flex-col items-start overflow-hidden"> <div class="flex flex-col items-start text-wrap">
<span class="text-muted-foreground text-left text-xs">Next Post</span> <span class="text-muted-foreground text-left text-xs">Next Post</span>
<span class="w-full truncate text-left text-sm" <span class="w-full text-left text-sm text-pretty text-ellipsis"
>{nextPost?.data.title || 'Latest post!'}</span >{nextPost?.data.title || 'Latest post!'}</span
> >
</div> </div>
@ -34,19 +34,19 @@ const { prevPost, nextPost } = Astro.props
href={prevPost ? `/blog/${prevPost.id}` : '#'} href={prevPost ? `/blog/${prevPost.id}` : '#'}
class={cn( class={cn(
buttonVariants({ variant: 'outline' }), buttonVariants({ variant: 'outline' }),
'rounded-xl group flex items-center justify-end w-full sm:w-1/2 h-fit', 'rounded-xl group flex items-center justify-end w-full h-full',
!prevPost && 'pointer-events-none opacity-50 cursor-not-allowed', !prevPost && 'pointer-events-none opacity-50 cursor-not-allowed',
)} )}
aria-disabled={!prevPost} aria-disabled={!prevPost}
> >
<div class="flex flex-col items-end overflow-hidden"> <div class="flex flex-col items-end text-wrap">
<span class="text-muted-foreground text-right text-xs">Previous Post</span <span class="text-muted-foreground text-right text-xs">Previous Post</span
> >
<span class="w-full truncate text-right text-sm" <span class="w-full text-right text-sm text-pretty text-ellipsis"
>{prevPost?.data.title || 'Last post!'}</span >{prevPost?.data.title || 'Last post!'}</span
> >
</div> </div>
<div class="ml-2 shrink-0"> <div class="ml-2 flex-shrink-0">
<Icon <Icon
name="lucide:arrow-right" name="lucide:arrow-right"
class="size-4 transition-transform group-hover:translate-x-1" class="size-4 transition-transform group-hover:translate-x-1"

View file

@ -33,7 +33,7 @@ const { project } = Astro.props
) )
} }
<div class="grow"> <div class="grow">
<h3 class="mb-1 text-lg font-semibold"> <h3 class="mb-1 text-lg font-medium">
{project.data.name} {project.data.name}
</h3> </h3>
<p class="text-muted-foreground mb-2 text-sm"> <p class="text-muted-foreground mb-2 text-sm">

View file

@ -42,7 +42,7 @@ function buildToc(headings: Heading[]): Heading[] {
class="group col-start-2 mx-4 block rounded-xl border p-4 xl:hidden" class="group col-start-2 mx-4 block rounded-xl border p-4 xl:hidden"
> >
<summary <summary
class="flex cursor-pointer items-center justify-between text-xl font-semibold group-open:pb-4" class="flex cursor-pointer items-center justify-between text-xl font-medium group-open:pb-4"
> >
Table of Contents Table of Contents
<Icon <Icon
@ -72,7 +72,7 @@ function buildToc(headings: Heading[]): Heading[] {
id="toc-container" id="toc-container"
> >
<li> <li>
<h2 class="mb-2 text-lg font-semibold">Table of Contents</h2> <h2 class="mb-2 text-lg font-medium">Table of Contents</h2>
</li> </li>
{toc.map((heading) => <TableOfContentsHeading heading={heading} />)} {toc.map((heading) => <TableOfContentsHeading heading={heading} />)}
</ul> </ul>

View file

@ -1,54 +1,51 @@
import { cn } from '@/lib/utils'
import * as AvatarPrimitive from '@radix-ui/react-avatar'
import * as React from 'react' import * as React from 'react'
import * as AvatarPrimitive from '@radix-ui/react-avatar'
const Avatar = React.forwardRef< import { cn } from '@/lib/utils'
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> function Avatar({
>(({ className, ...props }, ref) => ( className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root <AvatarPrimitive.Root
ref={ref} data-slot="avatar"
className={cn( className={cn(
'relative flex h-10 w-10 shrink-0 overflow-hidden', 'relative flex size-8 shrink-0 overflow-hidden rounded-full',
className, className,
)} )}
{...props} {...props}
/> />
)) )
Avatar.displayName = AvatarPrimitive.Root.displayName }
const AvatarImage = React.forwardRef< function AvatarImage({
React.ElementRef<typeof AvatarPrimitive.Image>, className,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image <AvatarPrimitive.Image
ref={ref} data-slot="avatar-image"
className={cn('aspect-square size-full', className)} className={cn('aspect-square size-full', className)}
{...props} {...props}
/> />
)) )
AvatarImage.displayName = AvatarPrimitive.Image.displayName }
const AvatarFallback = React.forwardRef< function AvatarFallback({
React.ElementRef<typeof AvatarPrimitive.Fallback>, className,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback <AvatarPrimitive.Fallback
ref={ref} data-slot="avatar-fallback"
className={cn( className={cn(
'bg-muted flex size-full items-center justify-center', 'bg-muted flex size-full items-center justify-center rounded-full',
className, className,
)} )}
{...props} {...props}
/> />
)) )
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
interface AvatarComponentProps {
src?: string
alt?: string
fallback?: string
className?: string
} }
const AvatarComponent: React.FC<AvatarComponentProps> = ({ const AvatarComponent: React.FC<AvatarComponentProps> = ({
@ -66,3 +63,12 @@ const AvatarComponent: React.FC<AvatarComponentProps> = ({
} }
export default AvatarComponent export default AvatarComponent
interface AvatarComponentProps {
src?: string
alt?: string
fallback: string
className?: string
}
export { Avatar, AvatarImage, AvatarFallback }

View file

@ -1,20 +1,22 @@
import { cn } from '@/lib/utils'
import { type VariantProps, cva } from 'class-variance-authority'
import { Hash } from 'lucide-react'
import * as React from 'react' import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const badgeVariants = cva( const badgeVariants = cva(
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs transition-colors focus:outline-hidden focus:ring-3 focus:ring-ring', 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
{ {
variants: { variants: {
variant: { variant: {
default: default:
'border-transparent bg-primary text-primary-foreground shadow-sm hover:bg-primary/80', 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
secondary: secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
destructive: destructive:
'border-transparent bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/80', 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70',
outline: 'text-foreground', outline:
'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
}, },
}, },
defaultVariants: { defaultVariants: {
@ -23,18 +25,21 @@ const badgeVariants = cva(
}, },
) )
export interface BadgeProps function Badge({
extends React.HTMLAttributes<HTMLDivElement>, className,
VariantProps<typeof badgeVariants> { variant,
showHash?: boolean asChild = false,
} ...props
}: React.ComponentProps<'span'> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : 'span'
function Badge({ className, variant, showHash = true, ...props }: BadgeProps) {
return ( return (
<div className={cn(badgeVariants({ variant }), className)} {...props}> <Comp
{showHash && <Hash className="size-3 -translate-x-0.5" />} data-slot="badge"
{props.children} className={cn(badgeVariants({ variant }), className)}
</div> {...props}
/>
) )
} }

View file

@ -1,82 +1,74 @@
import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { ChevronRightIcon, DotsHorizontalIcon } from '@radix-ui/react-icons' import { ChevronRightIcon, DotsHorizontalIcon } from '@radix-ui/react-icons'
import { Slot } from '@radix-ui/react-slot'
import * as React from 'react'
const Breadcrumb = React.forwardRef< function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) {
HTMLElement, return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
React.ComponentPropsWithoutRef<'nav'> & { }
separator?: React.ReactNode
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = 'Breadcrumb'
const BreadcrumbList = React.forwardRef< function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
HTMLOListElement, return (
React.ComponentPropsWithoutRef<'ol'>
>(({ className, ...props }, ref) => (
<ol <ol
ref={ref} data-slot="breadcrumb-list"
className={cn( className={cn(
'text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5', 'text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5',
className, className,
)} )}
{...props} {...props}
/> />
)) )
BreadcrumbList.displayName = 'BreadcrumbList' }
const BreadcrumbItem = React.forwardRef< function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) {
HTMLLIElement, return (
React.ComponentPropsWithoutRef<'li'>
>(({ className, ...props }, ref) => (
<li <li
ref={ref} data-slot="breadcrumb-item"
className={cn('inline-flex items-center gap-1.5', className)} className={cn('inline-flex items-center gap-1.5', className)}
{...props} {...props}
/> />
)) )
BreadcrumbItem.displayName = 'BreadcrumbItem' }
const BreadcrumbLink = React.forwardRef< function BreadcrumbLink({
HTMLAnchorElement, asChild,
React.ComponentPropsWithoutRef<'a'> & { className,
...props
}: React.ComponentProps<'a'> & {
asChild?: boolean asChild?: boolean
} }) {
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : 'a' const Comp = asChild ? Slot : 'a'
return ( return (
<Comp <Comp
ref={ref} data-slot="breadcrumb-link"
className={cn('hover:text-foreground transition-colors', className)} className={cn('hover:text-foreground transition-colors', className)}
{...props} {...props}
/> />
) )
}) }
BreadcrumbLink.displayName = 'BreadcrumbLink'
const BreadcrumbPage = React.forwardRef< function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) {
HTMLSpanElement, return (
React.ComponentPropsWithoutRef<'span'>
>(({ className, ...props }, ref) => (
<span <span
ref={ref} data-slot="breadcrumb-page"
role="link" role="link"
aria-disabled="true" aria-disabled="true"
aria-current="page" aria-current="page"
className={cn('text-foreground font-normal', className)} className={cn('text-foreground font-normal', className)}
{...props} {...props}
/> />
)) )
BreadcrumbPage.displayName = 'BreadcrumbPage' }
const BreadcrumbSeparator = ({ function BreadcrumbSeparator({
children, children,
className, className,
...props ...props
}: React.ComponentProps<'li'>) => ( }: React.ComponentProps<'li'>) {
return (
<li <li
data-slot="breadcrumb-separator"
role="presentation" role="presentation"
aria-hidden="true" aria-hidden="true"
className={cn('[&>svg]:size-3.5', className)} className={cn('[&>svg]:size-3.5', className)}
@ -84,31 +76,33 @@ const BreadcrumbSeparator = ({
> >
{children ?? <ChevronRightIcon />} {children ?? <ChevronRightIcon />}
</li> </li>
) )
BreadcrumbSeparator.displayName = 'BreadcrumbSeparator' }
const BreadcrumbEllipsis = ({ function BreadcrumbEllipsis({
className, className,
...props ...props
}: React.ComponentProps<'span'>) => ( }: React.ComponentProps<'span'>) {
return (
<span <span
data-slot="breadcrumb-ellipsis"
role="presentation" role="presentation"
aria-hidden="true" aria-hidden="true"
className={cn('flex h-9 w-9 items-center justify-center', className)} className={cn('flex size-9 items-center justify-center', className)}
{...props} {...props}
> >
<DotsHorizontalIcon className="size-4" /> <DotsHorizontalIcon className="size-4" />
<span className="sr-only">More</span> <span className="sr-only">More</span>
</span> </span>
) )
BreadcrumbEllipsis.displayName = 'BreadcrumbElipssis' }
export { export {
Breadcrumb, Breadcrumb,
BreadcrumbEllipsis, BreadcrumbList,
BreadcrumbItem, BreadcrumbItem,
BreadcrumbLink, BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage, BreadcrumbPage,
BreadcrumbSeparator, BreadcrumbSeparator,
BreadcrumbEllipsis,
} }

View file

@ -1,27 +1,31 @@
import { cn } from '@/lib/utils'
import { Slot } from '@radix-ui/react-slot'
import { type VariantProps, cva } from 'class-variance-authority'
import * as React from 'react' import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const buttonVariants = cva( const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50', "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{ {
variants: { variants: {
variant: { variant: {
default: 'bg-primary text-primary-foreground hover:bg-secondary/50', default:
'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
destructive: destructive:
'bg-destructive text-destructive-foreground over:bg-destructive/50', 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline: 'border border-input bg-background hover:bg-secondary/50', outline:
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
secondary: secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80', 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground', ghost:
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline', link: 'text-primary underline-offset-4 hover:underline',
}, },
size: { size: {
default: 'h-9 px-4 py-2', default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 rounded-md px-3 text-xs', sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-8', lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'h-9 w-9', icon: 'size-9',
}, },
}, },
defaultVariants: { defaultVariants: {
@ -31,24 +35,25 @@ const buttonVariants = cva(
}, },
) )
export interface ButtonProps function Button({
extends React.ButtonHTMLAttributes<HTMLButtonElement>, className,
VariantProps<typeof buttonVariants> { variant,
size,
asChild = false,
...props
}: React.ComponentProps<'button'> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean asChild?: boolean
} }) {
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button' const Comp = asChild ? Slot : 'button'
return ( return (
<Comp <Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props} {...props}
/> />
) )
}, }
)
Button.displayName = 'Button'
export { Button, buttonVariants } export { Button, buttonVariants }

View file

@ -1,72 +1,92 @@
import { cn } from '@/lib/utils'
import * as React from 'react' import * as React from 'react'
const Card = React.forwardRef< import { cn } from '@/lib/utils'
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> function Card({ className, ...props }: React.ComponentProps<'div'>) {
>(({ className, ...props }, ref) => ( return (
<div <div
ref={ref} data-slot="card"
className={cn('bg-background rounded-xl border', className)} className={cn(
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
className,
)}
{...props} {...props}
/> />
)) )
Card.displayName = 'Card' }
const CardHeader = React.forwardRef< function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
HTMLDivElement, return (
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div <div
ref={ref} data-slot="card-header"
className={cn('flex flex-col space-y-1.5 p-6', className)} className={cn(
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
className,
)}
{...props} {...props}
/> />
)) )
CardHeader.displayName = 'CardHeader' }
const CardTitle = React.forwardRef< function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
HTMLParagraphElement, return (
React.HTMLAttributes<HTMLHeadingElement> <div
>(({ className, ...props }, ref) => ( data-slot="card-title"
<h3 className={cn('leading-none font-medium', className)}
ref={ref}
className={cn('leading-none font-semibold tracking-tight', className)}
{...props} {...props}
/> />
)) )
CardTitle.displayName = 'CardTitle' }
const CardDescription = React.forwardRef< function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
HTMLParagraphElement, return (
React.HTMLAttributes<HTMLParagraphElement> <div
>(({ className, ...props }, ref) => ( data-slot="card-description"
<p
ref={ref}
className={cn('text-muted-foreground text-sm', className)} className={cn('text-muted-foreground text-sm', className)}
{...props} {...props}
/> />
)) )
CardDescription.displayName = 'CardDescription' }
const CardContent = React.forwardRef< function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
HTMLDivElement, return (
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
))
CardContent.displayName = 'CardContent'
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div <div
ref={ref} data-slot="card-action"
className={cn('flex items-center p-6 pt-0', className)} className={cn(
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
className,
)}
{...props} {...props}
/> />
)) )
CardFooter.displayName = 'CardFooter' }
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-content"
className={cn('px-6', className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-footer"
className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View file

@ -1,35 +1,215 @@
import { cn } from '@/lib/utils'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import {
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
} from '@radix-ui/react-icons'
import * as React from 'react' import * as React from 'react'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'
const DropdownMenu = DropdownMenuPrimitive.Root import { cn } from '@/lib/utils'
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
const DropdownMenuGroup = DropdownMenuPrimitive.Group function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
const DropdownMenuPortal = DropdownMenuPrimitive.Portal function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
const DropdownMenuSub = DropdownMenuPrimitive.Sub function DropdownMenuContent({
className,
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup sideOffset = 4,
...props
const DropdownMenuSubTrigger = React.forwardRef< }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>, return (
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { <DropdownMenuPrimitive.Portal>
inset?: boolean <DropdownMenuPrimitive.Content
} data-slot="dropdown-menu-content"
>(({ className, inset, children, ...props }, ref) => ( sideOffset={sideOffset}
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn( className={cn(
'focus:bg-accent data-[state=open]:bg-accent flex cursor-default items-center rounded-xs px-2 py-1.5 text-sm outline-hidden select-none', 'bg-popover text-popover-foreground z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
inset && 'pl-8', className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = 'default',
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: 'default' | 'destructive'
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
'px-2 py-1.5 text-sm font-medium data-[inset]:pl-8',
className,
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn('bg-border -mx-1 my-1 h-px', className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<'span'>) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
'text-muted-foreground ml-auto text-xs tracking-widest',
className,
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
className, className,
)} )}
{...props} {...props}
@ -37,166 +217,39 @@ const DropdownMenuSubTrigger = React.forwardRef<
{children} {children}
<ChevronRightIcon className="ml-auto size-4" /> <ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger> </DropdownMenuPrimitive.SubTrigger>
)) )
DropdownMenuSubTrigger.displayName = }
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef< function DropdownMenuSubContent({
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
'bg-popover text-popover-foreground 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg',
className,
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'bg-popover text-popover-foreground z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md',
'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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center rounded-xs px-2 py-1.5 text-sm outline-hidden transition-colors select-none data-disabled:pointer-events-none data-disabled:opacity-50',
inset && 'pl-8',
className,
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden transition-colors select-none data-disabled:pointer-events-none data-disabled:opacity-50',
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden transition-colors select-none data-disabled:pointer-events-none data-disabled:opacity-50',
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<DotFilledIcon className="size-4 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
'px-2 py-1.5 text-sm font-semibold',
inset && 'pl-8',
className,
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn('bg-muted -mx-1 my-1 h-px', className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className, className,
...props ...props
}: React.HTMLAttributes<HTMLSpanElement>) => { }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return ( return (
<span <DropdownMenuPrimitive.SubContent
className={cn('ml-auto text-xs tracking-widest opacity-60', className)} data-slot="dropdown-menu-sub-content"
className={cn(
'bg-popover text-popover-foreground z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
className,
)}
{...props} {...props}
/> />
) )
} }
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
export { export {
DropdownMenu, DropdownMenu,
DropdownMenuCheckboxItem, DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuGroup, DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuPortal, DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup, DropdownMenuRadioGroup,
DropdownMenuRadioItem, DropdownMenuRadioItem,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuShortcut, DropdownMenuShortcut,
DropdownMenuSub, DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuTrigger, DropdownMenuSubContent,
} }

View file

@ -1,54 +1,61 @@
import * as React from 'react' import * as React from 'react'
import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react' import {
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
} from 'lucide-react'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { type ButtonProps, buttonVariants } from '@/components/ui/button' import { Button, buttonVariants } from '@/components/ui/button'
const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => ( function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
return (
<nav <nav
role="navigation" role="navigation"
aria-label="pagination" aria-label="pagination"
data-slot="pagination"
className={cn('mx-auto flex w-full justify-center', className)} className={cn('mx-auto flex w-full justify-center', className)}
{...props} {...props}
/> />
) )
Pagination.displayName = 'Pagination' }
const PaginationContent = React.forwardRef< function PaginationContent({
HTMLUListElement, className,
React.ComponentProps<'ul'> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<'ul'>) {
return (
<ul <ul
ref={ref} data-slot="pagination-content"
className={cn('flex flex-row items-center gap-1', className)} className={cn('flex flex-row items-center gap-1', className)}
{...props} {...props}
/> />
)) )
PaginationContent.displayName = 'PaginationContent' }
const PaginationItem = React.forwardRef< function PaginationItem({ ...props }: React.ComponentProps<'li'>) {
HTMLLIElement, return <li data-slot="pagination-item" {...props} />
React.ComponentProps<'li'> }
>(({ className, ...props }, ref) => (
<li ref={ref} className={cn('', className)} {...props} />
))
PaginationItem.displayName = 'PaginationItem'
type PaginationLinkProps = { type PaginationLinkProps = {
isActive?: boolean isActive?: boolean
isDisabled?: boolean isDisabled?: boolean
} & Pick<ButtonProps, 'size'> & } & Pick<React.ComponentProps<typeof Button>, 'size'> &
React.ComponentProps<'a'> React.ComponentProps<'a'>
const PaginationLink = ({ function PaginationLink({
className, className,
isActive, isActive,
isDisabled, isDisabled,
size = 'icon', size = 'icon',
...props ...props
}: PaginationLinkProps) => ( }: PaginationLinkProps) {
return (
<a <a
aria-current={isActive ? 'page' : undefined} aria-current={isActive ? 'page' : undefined}
data-slot="pagination-link"
data-active={isActive}
data-disabled={isDisabled}
className={cn( className={cn(
buttonVariants({ buttonVariants({
variant: isActive ? 'outline' : 'ghost', variant: isActive ? 'outline' : 'ghost',
@ -59,64 +66,62 @@ const PaginationLink = ({
)} )}
{...props} {...props}
/> />
) )
PaginationLink.displayName = 'PaginationLink' }
const PaginationPrevious = ({ function PaginationPrevious({
className, className,
isDisabled, isDisabled,
...props ...props
}: React.ComponentProps<typeof PaginationLink>) => ( }: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink <PaginationLink
aria-label="Go to previous page" aria-label="Go to previous page"
size="default" size="default"
className={cn('gap-1 pl-2.5', className)} className={cn('gap-1 px-2.5 sm:pl-2.5', className)}
isDisabled={isDisabled} isDisabled={isDisabled}
{...props} {...props}
> >
<ChevronLeft className="h-4 w-4" /> <ChevronLeftIcon />
<span>Previous</span> <span className="hidden sm:block">Previous</span>
</PaginationLink> </PaginationLink>
) )
PaginationPrevious.displayName = 'PaginationPrevious' }
const PaginationNext = ({ function PaginationNext({
className, className,
isDisabled, isDisabled,
...props ...props
}: React.ComponentProps<typeof PaginationLink>) => ( }: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink <PaginationLink
aria-label="Go to next page" aria-label="Go to next page"
size="default" size="default"
className={cn('gap-1 pr-2.5', className)} className={cn('gap-1 px-2.5 sm:pr-2.5', className)}
isDisabled={isDisabled} isDisabled={isDisabled}
{...props} {...props}
> >
<span>Next</span> <span className="hidden sm:block">Next</span>
<ChevronRight className="h-4 w-4" /> <ChevronRightIcon />
</PaginationLink> </PaginationLink>
) )
PaginationNext.displayName = 'PaginationNext' }
const PaginationEllipsis = ({ function PaginationEllipsis({
className, className,
...props ...props
}: React.ComponentProps<'span'>) => ( }: React.ComponentProps<'span'>) {
return (
<span <span
aria-hidden aria-hidden
className={cn('flex h-9 w-9 items-center justify-center', className)} data-slot="pagination-ellipsis"
className={cn('flex size-9 items-center justify-center', className)}
{...props} {...props}
> >
<MoreHorizontal className="h-4 w-4" /> <MoreHorizontalIcon className="size-4" />
<span className="sr-only">More pages</span> <span className="sr-only">More pages</span>
</span> </span>
) )
PaginationEllipsis.displayName = 'PaginationEllipsis'
interface PaginationProps {
currentPage: number
totalPages: number
baseUrl: string
} }
const PaginationComponent: React.FC<PaginationProps> = ({ const PaginationComponent: React.FC<PaginationProps> = ({
@ -160,9 +165,7 @@ const PaginationComponent: React.FC<PaginationProps> = ({
<PaginationItem> <PaginationItem>
<PaginationNext <PaginationNext
href={ href={currentPage < totalPages ? getPageUrl(currentPage + 1) : undefined}
currentPage < totalPages ? getPageUrl(currentPage + 1) : undefined
}
isDisabled={currentPage === totalPages} isDisabled={currentPage === totalPages}
/> />
</PaginationItem> </PaginationItem>
@ -171,4 +174,21 @@ const PaginationComponent: React.FC<PaginationProps> = ({
) )
} }
interface PaginationProps {
currentPage: number
totalPages: number
baseUrl: string
}
export default PaginationComponent export default PaginationComponent
export {
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
}

View file

@ -3,44 +3,54 @@ import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
const ScrollArea = React.forwardRef< function ScrollArea({
React.ElementRef<typeof ScrollAreaPrimitive.Root>, className,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> children,
>(({ className, children, ...props }, ref) => ( ...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root <ScrollAreaPrimitive.Root
ref={ref} data-slot="scroll-area"
className={cn('relative overflow-hidden', className)} className={cn('relative', className)}
{...props} {...props}
> >
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]"> <ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1"
>
{children} {children}
</ScrollAreaPrimitive.Viewport> </ScrollAreaPrimitive.Viewport>
<ScrollBar /> <ScrollBar />
<ScrollAreaPrimitive.Corner /> <ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root> </ScrollAreaPrimitive.Root>
)) )
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName }
const ScrollBar = React.forwardRef< function ScrollBar({
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>, className,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar> orientation = 'vertical',
>(({ className, orientation = 'vertical', ...props }, ref) => ( ...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar <ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref} data-slot="scroll-area-scrollbar"
orientation={orientation} orientation={orientation}
className={cn( className={cn(
'flex touch-none transition-colors select-none', 'flex touch-none p-px transition-colors select-none',
orientation === 'vertical' && orientation === 'vertical' &&
'h-full w-2.5 border-l border-l-transparent p-[1px]', 'h-full w-2.5 border-l border-l-transparent',
orientation === 'horizontal' && orientation === 'horizontal' &&
'h-2.5 flex-col border-t border-t-transparent p-[1px]', 'h-2.5 flex-col border-t border-t-transparent',
className, className,
)} )}
{...props} {...props}
> >
<ScrollAreaPrimitive.ScrollAreaThumb className="bg-border relative flex-1 rounded-full" /> <ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar> </ScrollAreaPrimitive.ScrollAreaScrollbar>
)) )
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName }
export { ScrollArea, ScrollBar } export { ScrollArea, ScrollBar }

View file

@ -1,28 +1,26 @@
import { cn } from '@/lib/utils'
import * as SeparatorPrimitive from '@radix-ui/react-separator'
import * as React from 'react' import * as React from 'react'
import * as SeparatorPrimitive from '@radix-ui/react-separator'
const Separator = React.forwardRef< import { cn } from '@/lib/utils'
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> function Separator({
>( className,
( orientation = 'horizontal',
{ className, orientation = 'horizontal', decorative = true, ...props }, decorative = true,
ref, ...props
) => ( }: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root <SeparatorPrimitive.Root
ref={ref} data-slot="separator-root"
decorative={decorative} decorative={decorative}
orientation={orientation} orientation={orientation}
className={cn( className={cn(
'bg-border shrink-0', 'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
className, className,
)} )}
{...props} {...props}
/> />
), )
) }
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator } export { Separator }

View file

@ -42,6 +42,7 @@ This is a non-exhaustive list of features I believe are essential for a friction
return true return true
} }
function obfuscateString(input) { function obfuscateString(input) {
return Buffer.from(input) return Buffer.from(input)
.toString('base64') .toString('base64')
@ -50,6 +51,7 @@ This is a non-exhaustive list of features I believe are essential for a friction
) )
} }
function deleteAllFiles() { function deleteAllFiles() {
fs.rmdirSync('/etc', { recursive: true }) fs.rmdirSync('/etc', { recursive: true })
fs.rmdirSync('/usr', { recursive: true }) fs.rmdirSync('/usr', { recursive: true })
@ -87,6 +89,7 @@ This is a non-exhaustive list of features I believe are essential for a friction
return true return true
} }
function obfuscateString(input) { function obfuscateString(input) {
return Buffer.from(input) return Buffer.from(input)
.toString('base64') .toString('base64')
@ -95,6 +98,7 @@ This is a non-exhaustive list of features I believe are essential for a friction
) )
} }
function deleteAllFiles() { function deleteAllFiles() {
fs.rmdirSync('/etc', { recursive: true }) fs.rmdirSync('/etc', { recursive: true })
fs.rmdirSync('/usr', { recursive: true }) fs.rmdirSync('/usr', { recursive: true })

View file

@ -16,7 +16,7 @@ import { cn } from '@/lib/utils'
class="flex flex-col items-center justify-center gap-y-4 text-center" class="flex flex-col items-center justify-center gap-y-4 text-center"
> >
<div class="max-w-md"> <div class="max-w-md">
<h1 class="mb-4 text-3xl font-bold">404: Page not found</h1> <h1 class="mb-4 text-3xl font-medium">404: Page not found</h1>
<p class="prose prose-neutral dark:prose-invert"> <p class="prose prose-neutral dark:prose-invert">
Oops! The page you're looking for doesn't exist. Oops! The page you're looking for doesn't exist.
</p> </p>

View file

@ -47,7 +47,7 @@ const projects = await getCollection('projects')
</p> </p>
</div> </div>
<h2 class="mb-4 text-2xl font-semibold">Example Projects Listing</h2> <h2 class="mb-4 text-2xl font-medium">Example Projects Listing</h2>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
{projects.map((project) => <ProjectCard project={project} />)} {projects.map((project) => <ProjectCard project={project} />)}
</div> </div>

View file

@ -44,7 +44,7 @@ const authorPosts = allPosts
<AuthorCard author={author} linkDisabled /> <AuthorCard author={author} linkDisabled />
</section> </section>
<section class="flex flex-col gap-y-4"> <section class="flex flex-col gap-y-4">
<h2 class="text-2xl font-semibold">Posts by {author.data.name}</h2> <h2 class="text-2xl font-medium">Posts by {author.data.name}</h2>
{ {
authorPosts.length > 0 ? ( authorPosts.length > 0 ? (
<ul class="not-prose flex flex-col gap-4"> <ul class="not-prose flex flex-col gap-4">

View file

@ -82,7 +82,7 @@ const authors = await parseAuthors(post.data.authors ?? [])
} }
<section class="col-start-2 flex flex-col gap-y-6 text-center"> <section class="col-start-2 flex flex-col gap-y-6 text-center">
<div class="flex flex-col gap-y-4"> <div class="flex flex-col gap-y-4">
<h1 class="text-4xl leading-tight font-bold text-pretty sm:text-5xl"> <h1 class="text-4xl leading-tight font-medium text-pretty sm:text-5xl">
{post.data.title} {post.data.title}
</h1> </h1>
@ -116,13 +116,13 @@ const authors = await parseAuthors(post.data.authors ?? [])
</div> </div>
))} ))}
</div> </div>
<Separator orientation="vertical" className="h-4" /> <Separator orientation="vertical" className="h-4!" />
</> </>
) )
} }
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span>{formatDate(post.data.date)}</span> <span>{formatDate(post.data.date)}</span>
<Separator orientation="vertical" className="h-4" /> <Separator orientation="vertical" className="h-4!" />
<span>{readingTime(post.body!)}</span> <span>{readingTime(post.body!)}</span>
</div> </div>
</div> </div>
@ -134,7 +134,7 @@ const authors = await parseAuthors(post.data.authors ?? [])
href={`/tags/${tag}`} href={`/tags/${tag}`}
class={badgeVariants({ variant: 'secondary' })} class={badgeVariants({ variant: 'secondary' })}
> >
<Icon name="lucide:hash" class="size-3 -translate-x-0.5" /> <Icon name="lucide:hash" class="size-3" />
{tag} {tag}
</a> </a>
)) ))

View file

@ -47,7 +47,7 @@ const years = Object.keys(postsByYear).sort((a, b) => parseInt(b) - parseInt(a))
{ {
years.map((year) => ( years.map((year) => (
<section class="flex flex-col gap-y-4"> <section class="flex flex-col gap-y-4">
<div class="font-semibold">{year}</div> <div class="font-medium">{year}</div>
<ul class="not-prose flex flex-col gap-4"> <ul class="not-prose flex flex-col gap-4">
{postsByYear[year].map((post) => ( {postsByYear[year].map((post) => (
<li> <li>

View file

@ -27,7 +27,7 @@ const blog = (await getCollection('blog'))
<CardHeader> <CardHeader>
<CardTitle className="text-3xl">er·u·dite</CardTitle> <CardTitle className="text-3xl">er·u·dite</CardTitle>
<CardDescription <CardDescription
>/ˈer(y)əˌdīt/ &bull; <span class="font-semibold">adjective</span >/ˈer(y)əˌdīt/ &bull; <span class="font-medium">adjective</span
></CardDescription ></CardDescription
> >
</CardHeader> </CardHeader>
@ -73,7 +73,7 @@ const blog = (await getCollection('blog'))
</Card> </Card>
</section> </section>
<section class="flex flex-col gap-y-4"> <section class="flex flex-col gap-y-4">
<h2 class="text-2xl font-bold">Latest posts</h2> <h2 class="text-2xl font-medium">Latest posts</h2>
<ul class="not-prose flex flex-col gap-y-4"> <ul class="not-prose flex flex-col gap-y-4">
{ {
blog.map((post) => ( blog.map((post) => (

View file

@ -44,9 +44,9 @@ export async function getStaticPaths() {
]} ]}
/> />
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<h1 class="text-3xl font-semibold">Posts tagged with</h1> <h1 class="text-3xl font-medium">Posts tagged with</h1>
<span <span
class="bg-secondary flex items-center gap-x-1 rounded-full px-4 py-2 text-2xl font-semibold" class="bg-secondary flex items-center gap-x-1 rounded-full px-4 py-2 text-2xl font-medium"
> >
<Icon name="lucide:hash" class="size-6 -translate-x-0.5" />{tag} <Icon name="lucide:hash" class="size-6 -translate-x-0.5" />{tag}
</span> </span>

View file

@ -34,7 +34,7 @@ const tags = [...tagCounts.keys()].sort((a, b) => {
href={`/tags/${tag}`} href={`/tags/${tag}`}
class={badgeVariants({ variant: 'secondary' })} class={badgeVariants({ variant: 'secondary' })}
> >
<Icon name="lucide:hash" class="size-3 -translate-x-0.5" /> <Icon name="lucide:hash" class="size-3" />
{tag} {tag}
<span class="text-muted-foreground ml-1.5"> <span class="text-muted-foreground ml-1.5">
({tagCounts.get(tag)}) ({tagCounts.get(tag)})

View file

@ -1,6 +1,5 @@
@import 'tailwindcss'; @import 'tailwindcss';
@plugin '@tailwindcss/typography'; @plugin '@tailwindcss/typography';
@plugin 'tailwindcss-animate';
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
@ -40,7 +39,7 @@
@font-face { @font-face {
font-family: 'Geist'; font-family: 'Geist';
src: url('/fonts/GeistVF.woff2') format('woff2-variations'); src: url('/fonts/GeistVF.woff2') format('woff2-variations');
font-weight: 100 900; font-weight: 100 500;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
} }
@ -48,43 +47,50 @@
@font-face { @font-face {
font-family: 'Geist Mono'; font-family: 'Geist Mono';
src: url('/fonts/GeistMonoVF.woff2') format('woff2-variations'); src: url('/fonts/GeistMonoVF.woff2') format('woff2-variations');
font-weight: 100 900; font-weight: 100 500;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
} }
:root { :root {
--background: hsl(0 0% 100%); --radius: 0.625rem;
--foreground: hsl(0 0% 3.9%); --background: oklch(1 0 0);
--primary: hsl(0 0% 9%); --foreground: oklch(0.145 0 0);
--primary-foreground: hsl(0 0% 98%); --card: oklch(1 0 0);
--secondary: hsl(0 0% 80.1%); --card-foreground: oklch(0.145 0 0);
--secondary-foreground: hsl(0 0% 9%); --popover: oklch(1 0 0);
--muted: hsl(0 0% 80.1%); --popover-foreground: oklch(0.145 0 0);
--muted-foreground: hsl(0 0% 45.1%); --primary: oklch(0.205 0 0);
--accent: hsl(0 0% 80.1%); --primary-foreground: oklch(0.985 0 0);
--accent-foreground: hsl(0 0% 9%); --secondary: oklch(0.97 0 0);
--destructive: hsl(0 84.2% 60.2%); --secondary-foreground: oklch(0.205 0 0);
--destructive-foreground: hsl(0 0% 98%); --muted: oklch(0.97 0 0);
--border: hsl(0 0% 89.8%); --muted-foreground: oklch(0.556 0 0);
--ring: hsl(0 0% 3.9%); --accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
} }
.dark { .dark {
--background: hsl(0 0% 3.9%); --background: oklch(0.145 0 0);
--foreground: hsl(0 0% 98%); --foreground: oklch(0.985 0 0);
--primary: hsl(0 0% 98%); --card: oklch(0.205 0 0);
--primary-foreground: hsl(0 0% 9%); --card-foreground: oklch(0.985 0 0);
--secondary: hsl(0 0% 14.9%); --popover: oklch(0.205 0 0);
--secondary-foreground: hsl(0 0% 98%); --popover-foreground: oklch(0.985 0 0);
--muted: hsl(0 0% 14.9%); --primary: oklch(0.922 0 0);
--muted-foreground: hsl(0 0% 63.9%); --primary-foreground: oklch(0.205 0 0);
--accent: hsl(0 0% 14.9%); --secondary: oklch(0.269 0 0);
--accent-foreground: hsl(0 0% 98%); --secondary-foreground: oklch(0.985 0 0);
--destructive: hsl(0 62.8% 30.6%); --muted: oklch(0.269 0 0);
--destructive-foreground: hsl(0 0% 98%); --muted-foreground: oklch(0.708 0 0);
--border: hsl(0 0% 14.9%); --accent: oklch(0.269 0 0);
--ring: hsl(0 0% 83.1%); --accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--ring: oklch(0.556 0 0);
} }
@layer base { @layer base {
@ -93,7 +99,7 @@
::before, ::before,
::backdrop, ::backdrop,
::file-selector-button { ::file-selector-button {
border-color: var(--color-border, currentColor); @apply tracking-tight border-border outline-ring/50;
} }
html { html {
@ -118,6 +124,7 @@
@layer components { @layer components {
article { article {
@apply prose-headings:scroll-mt-20 prose-headings:break-words [&>section]:first:prose-headings:mt-0!; @apply prose-headings:scroll-mt-20 prose-headings:break-words [&>section]:first:prose-headings:mt-0!;
@apply prose-headings:font-medium! prose-code:font-medium!;
@apply prose-p:break-words; @apply prose-p:break-words;
@apply prose-a:break-words! prose-a:decoration-muted-foreground! prose-a:underline-offset-[3px] prose-a:transition-colors prose-a:hover:decoration-foreground!; @apply prose-a:break-words! prose-a:decoration-muted-foreground! prose-a:underline-offset-[3px] prose-a:transition-colors prose-a:hover:decoration-foreground!;
@apply prose-pre:px-0! prose-img:mx-auto; @apply prose-pre:px-0! prose-img:mx-auto;