chore: cleanup, shadcn

This commit is contained in:
enscribe 2024-09-10 14:24:09 -07:00
parent 230dca64ca
commit ea68d4f02f
No known key found for this signature in database
GPG key ID: 9BBD5C4114E25322
38 changed files with 1073 additions and 1378 deletions

View file

@ -12,7 +12,7 @@ const { entry } = Astro.props as {
<a
href={`/${entry.collection}/${entry.slug}`}
class="not-prose group relative flex flex-nowrap rounded-lg border border-black/15 px-4 py-3 pr-10 transition-colors duration-300 ease-in-out hover:bg-black/5 hover:text-black focus-visible:bg-black/5 focus-visible:text-black dark:border-white/20 dark:hover:bg-white/5 dark:hover:text-white dark:focus-visible:bg-white/5 dark:focus-visible:text-white"
class="not-prose group relative flex flex-nowrap rounded-lg border px-4 py-3 pr-10 transition-colors duration-300 ease-in-out"
>
<div class="flex flex-1 flex-col truncate">
<div class="font-semibold">
@ -32,11 +32,11 @@ const { entry } = Astro.props as {
y1="12"
x2="19"
y2="12"
class="translate-x-3 scale-x-0 transition-transform duration-300 ease-in-out group-hover:translate-x-0 group-hover:scale-x-100 group-focus-visible:translate-x-0 group-focus-visible:scale-x-100"
class="translate-x-3 scale-x-0 transition-transform duration-300 ease-in-out group-focus-visible:translate-x-0 group-focus-visible:scale-x-100"
></line>
<polyline
points="12 5 19 12 12 19"
class="-translate-x-1 transition-transform duration-300 ease-in-out group-hover:translate-x-0 group-focus-visible:translate-x-0"
class="-translate-x-1 transition-transform duration-300 ease-in-out group-focus-visible:translate-x-0"
></polyline>
</svg>
</a>

View file

@ -8,7 +8,7 @@ const { href } = Astro.props
<a
href={href}
class="not-prose group relative flex w-fit flex-nowrap rounded border border-black/15 py-1.5 pl-7 pr-3 transition-colors duration-300 ease-in-out hover:bg-black/5 hover:text-black focus-visible:bg-black/5 focus-visible:text-black dark:border-white/20 dark:hover:bg-white/5 dark:hover:text-white dark:focus-visible:bg-white/5 dark:focus-visible:text-white"
class="not-prose group relative flex w-fit flex-nowrap rounded border py-1.5 pl-7 pr-3 transition-colors duration-300 ease-in-out"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -20,11 +20,11 @@ const { href } = Astro.props
y1="12"
x2="19"
y2="12"
class="translate-x-2 scale-x-0 transition-transform duration-300 ease-in-out group-hover:translate-x-0 group-hover:scale-x-100 group-focus-visible:translate-x-0 group-focus-visible:scale-x-100"
class="translate-x-2 scale-x-0 transition-transform duration-300 ease-in-out"
></line>
<polyline
points="12 5 5 12 12 19"
class="translate-x-1 transition-transform duration-300 ease-in-out group-hover:translate-x-0 group-focus-visible:translate-x-0"
class="translate-x-1 transition-transform duration-300 ease-in-out"
></polyline>
</svg>
<div class="text-sm">

View file

@ -1,27 +0,0 @@
---
---
<button
id="back-to-top"
class="group relative flex w-fit flex-nowrap rounded border border-black/15 py-1.5 pl-8 pr-3 transition-colors duration-300 ease-in-out hover:bg-black/5 hover:text-black focus-visible:bg-black/5 focus-visible:text-black dark:border-white/20 dark:hover:bg-white/5 dark:hover:text-white dark:focus-visible:bg-white/5 dark:focus-visible:text-white"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="absolute left-2 top-1/2 size-4 -translate-y-1/2 rotate-90 fill-none stroke-current stroke-2"
>
<line
x1="5"
y1="12"
x2="19"
y2="12"
class="translate-x-2 scale-x-0 transition-transform duration-300 ease-in-out group-hover:translate-x-0 group-hover:scale-x-100 group-focus-visible:translate-x-0 group-focus-visible:scale-x-100"
></line>
<polyline
points="12 5 5 12 12 19"
class="translate-x-1 transition-transform duration-300 ease-in-out group-hover:translate-x-0 group-focus-visible:translate-x-0"
></polyline>
</svg>
<div class="text-sm">Back to top</div>
</button>

View file

@ -1,44 +0,0 @@
---
interface Component {
type: 'default' | 'info' | 'warning' | 'error'
}
const { type = 'default' } = Astro.props
let emoji = '💡'
if (type === 'info') {
emoji = ''
} else if (type === 'warning') {
emoji = '⚠️'
} else if (type === 'error') {
emoji = '🚨'
}
---
<div class={`not-prose callout callout-${type}`}>
<span class="emoji pointer-events-none select-none">{emoji}</span>
<slot />
</div>
<style>
.callout {
@apply relative my-4 flex rounded border border-orange-800 bg-orange-100 p-3 text-orange-950 dark:border-orange-200/20 dark:bg-orange-950/20 dark:text-orange-200;
}
.emoji {
@apply pr-3 text-xl;
}
.callout-info {
@apply border-blue-800 bg-blue-100 text-blue-950 dark:border-blue-200/20 dark:bg-blue-950/20 dark:text-blue-200;
}
.callout-warning {
@apply border-yellow-800 bg-yellow-100 text-yellow-950 dark:border-yellow-200/20 dark:bg-yellow-950/20 dark:text-yellow-200;
}
.callout-error {
@apply border-red-800 bg-red-100 text-red-950 dark:border-red-200/20 dark:bg-red-950/20 dark:text-red-200;
}
</style>

View file

@ -1,21 +1,17 @@
---
import Container from '@components/Container.astro'
import { SITE } from '@consts'
import BackToTop from '@components/BackToTop.astro'
import SocialIcons from './SocialIcons.astro'
import { ModeToggle } from '@components/ui/mode-toggle'
---
<footer>
<footer class="py-8">
<Container>
<div class="my-2 flex justify-between">
<SocialIcons icon_size={'text-xl'} />
<BackToTop />
</div>
<div class="flex items-center justify-between">
<div>
&copy; {new Date().getFullYear()} • {SITE.TITLE} 👀<br />
Built with Astro
<div class="flex justify-between items-center">
<div class="flex items-center space-x-4">
<ModeToggle client:load />
<p class="text-sm text-muted-foreground">&copy; {new Date().getFullYear()} All rights reserved.</p>
</div>
<SocialIcons />
</div>
</Container>
</footer>

View file

@ -4,6 +4,8 @@ import '../styles/global.css'
import '@fontsource/geist-sans'
import '@fontsource/geist-mono'
import { ViewTransitions } from 'astro:transitions'
interface Props {
title: string
description: string
@ -37,73 +39,40 @@ const { title, description, image = '/blog-placeholder-1.jpg' } = Astro.props
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={new URL(image, Astro.url)} />
<ViewTransitions />
<script is:inline>
function init() {
onScroll()
addCopyCodeButtons()
const backToTop = document.getElementById('back-to-top')
backToTop?.addEventListener('click', (event) => scrollToTop(event))
const backToPrev = document.getElementById('back-to-prev')
backToPrev?.addEventListener('click', () => window.history.back())
document.addEventListener('scroll', onScroll)
}
function onScroll() {
if (window.scrollY > 0) {
document.documentElement.classList.add('scrolled')
} else {
document.documentElement.classList.remove('scrolled')
function setDarkMode(document) {
const getThemePreference = () => {
if (
typeof localStorage !== 'undefined' &&
localStorage.getItem('theme')
) {
return localStorage.getItem('theme')
}
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'theme-light'
}
}
const isDark = getThemePreference() === 'dark'
document.documentElement.classList[isDark ? 'add' : 'remove']('dark')
function scrollToTop(event) {
event.preventDefault()
window.scrollTo({
top: 0,
behavior: 'smooth',
})
}
function addCopyCodeButtons() {
let copyButtonLabel = '📋'
let codeBlocks = Array.from(document.querySelectorAll('pre'))
async function copyCode(codeBlock, copyButton) {
const codeText = codeBlock.innerText
const buttonText = copyButton.innerText
const textToCopy = codeText.replace(buttonText, '')
await navigator.clipboard.writeText(textToCopy)
copyButton.innerText = '✅'
setTimeout(() => {
copyButton.innerText = copyButtonLabel
}, 2000)
}
for (let codeBlock of codeBlocks) {
const wrapper = document.createElement('div')
wrapper.style.position = 'relative'
const copyButton = document.createElement('button')
copyButton.innerText = copyButtonLabel
copyButton.classList = 'copy-code'
codeBlock.setAttribute('tabindex', '0')
codeBlock.appendChild(copyButton)
codeBlock.parentNode.insertBefore(wrapper, codeBlock)
wrapper.appendChild(codeBlock)
copyButton?.addEventListener('click', async () => {
await copyCode(codeBlock, copyButton)
if (typeof localStorage !== 'undefined') {
const observer = new MutationObserver(() => {
const isDark = document.documentElement.classList.contains('dark')
localStorage.setItem('theme', isDark ? 'dark' : 'theme-light')
})
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class'],
})
}
}
document.addEventListener('DOMContentLoaded', () => init())
document.addEventListener('astro:after-swap', () => init())
setDarkMode(document)
document.addEventListener('astro:before-swap', (ev) => {
// Pass the incoming document to set the theme on it
setDarkMode(ev.newDocument)
})
</script>

View file

@ -15,7 +15,7 @@ const { href, external, underline = true, group = false, ...rest } = Astro.props
href={href}
target={external ? '_blank' : '_self'}
class={cn(
'inline-block decoration-black/30 hover:decoration-black/50 focus-visible:decoration-black/50 dark:decoration-white/30 dark:hover:decoration-white/50 dark:focus-visible:decoration-white/50 hover:text-cyan-500 focus-visible:text-black dark:hover:text-orange-500 dark:focus-visible:text-white transition-colors duration-300 ease-in-out',
'inline-block transition-colors duration-300 ease-in-out',
underline && 'underline underline-offset-[3px]',
group && 'group',
)}

View file

@ -11,7 +11,7 @@ const { name, avatar, bio } = member.data
---
<div
class="not-prose flex size-full flex-col gap-4 overflow-hidden rounded-xl border border-foreground bg-background p-6 hover:bg-secondary sm:flex-row sm:items-center"
class="not-prose flex size-full flex-col gap-4 overflow-hidden rounded-xl border p-6 hover:bg-secondary sm:flex-row sm:items-center"
>
<Image
src={avatar}

View file

@ -7,7 +7,7 @@ const { prevPost, nextPost } = Astro.props
prevPost?.slug ? (
<a
href={`/blog/${prevPost?.slug}`}
class="group relative flex flex-nowrap rounded-lg border border-black/15 px-4 py-3 pl-10 no-underline transition-colors duration-300 ease-in-out hover:bg-black/5 hover:text-black focus-visible:bg-black/5 focus-visible:text-black dark:border-white/20 dark:hover:bg-white/5 dark:hover:text-white dark:focus-visible:bg-white/5 dark:focus-visible:text-white"
class="group relative flex flex-nowrap rounded-lg border px-4 py-3 pl-10 no-underline transition-colors duration-300 ease-in-out"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -19,11 +19,11 @@ const { prevPost, nextPost } = Astro.props
y1="12"
x2="19"
y2="12"
class="translate-x-3 scale-x-0 transition-transform duration-300 ease-in-out group-hover:translate-x-0 group-hover:scale-x-100 group-focus-visible:translate-x-0 group-focus-visible:scale-x-100"
class="translate-x-3 scale-x-0 transition-transform duration-300 ease-in-out"
/>
<polyline
points="12 5 5 12 12 19"
class="translate-x-1 transition-transform duration-300 ease-in-out group-hover:translate-x-0 group-focus-visible:translate-x-0"
class="translate-x-1 transition-transform duration-300 ease-in-out"
/>
</svg>
<div class="flex items-center text-sm">{prevPost?.data.title}</div>
@ -37,7 +37,7 @@ const { prevPost, nextPost } = Astro.props
nextPost?.slug ? (
<a
href={`/blog/${nextPost?.slug}`}
class="group relative flex flex-grow flex-row-reverse flex-nowrap rounded-lg border border-black/15 px-4 py-4 pr-10 no-underline transition-colors duration-300 ease-in-out hover:bg-black/5 hover:text-black focus-visible:bg-black/5 focus-visible:text-black dark:border-white/20 dark:hover:bg-white/5 dark:hover:text-white dark:focus-visible:bg-white/5 dark:focus-visible:text-white"
class="group relative flex flex-grow flex-row-reverse flex-nowrap rounded-lg border px-4 py-4 pr-10 no-underline transition-colors duration-300 ease-in-out"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -49,11 +49,11 @@ const { prevPost, nextPost } = Astro.props
y1="12"
x2="19"
y2="12"
class="translate-x-3 scale-x-0 transition-transform duration-300 ease-in-out group-hover:translate-x-0 group-hover:scale-x-100 group-focus-visible:translate-x-0 group-focus-visible:scale-x-100"
class="translate-x-3 scale-x-0 transition-transform duration-300 ease-in-out"
/>
<polyline
points="12 5 19 12 12 19"
class="-translate-x-1 transition-transform duration-300 ease-in-out group-hover:translate-x-0 group-focus-visible:translate-x-0"
class="-translate-x-1 transition-transform duration-300 ease-in-out"
/>
</svg>
<div class="flex items-center text-sm">{nextPost?.data.title}</div>

View file

@ -13,7 +13,7 @@ const { URL, icon, icon_size } = Astro.props
<a
href={URL}
target={'_blank'}
class={`inline-block ${icon_size} decoration-black/30 dark:decoration-white/30 hover:decoration-black/50 focus-visible:decoration-black/50 dark:hover:decoration-white/50 dark:focus-visible:decoration-white/50 text-current hover:text-cyan-500 focus-visible:text-black dark:hover:text-orange-500 dark:focus-visible:text-white transition-colors duration-300 ease-in-out`}
class={`inline-block ${icon_size}`}
>
<i class={`bi bi-${icon}`}></i>
</a>

View file

@ -1,23 +1,14 @@
---
import SocialIcon from '@components/SocialIcon.astro'
import { Twitter, Github, Linkedin, Mail, GraduationCap, Rss } from 'lucide-react'
import { SITE } from '@consts'
export interface Props {
icon_size: string
}
const { icon_size } = Astro.props
---
<ul class="not-prose flex flex-wrap gap-2">
<SocialIcon icon_size={icon_size} URL="#" icon="twitter-x" />
<SocialIcon icon_size={icon_size} URL="#" icon="github" />
<SocialIcon icon_size={icon_size} URL="#" icon="linkedin" />
<SocialIcon icon_size={icon_size} URL="#" icon="envelope-fill" />
<SocialIcon icon_size={icon_size} URL="#" icon="mortarboard-fill" />
<SocialIcon
icon_size={icon_size}
URL={`${SITE.SITEURL}/rss.xml`}
icon="rss-fill"
/>
<a href="#" class="inline-block"><Twitter /></a>
<a href="#" class="inline-block"><Github /></a>
<a href="#" class="inline-block"><Linkedin /></a>
<a href="#" class="inline-block"><Mail /></a>
<a href="#" class="inline-block"><GraduationCap /></a>
<a href={`${SITE.SITEURL}/rss.xml`} class="inline-block"><Rss /></a>
</ul>

View file

@ -27,9 +27,9 @@ function buildToc(headings: Heading[]) {
}
---
<details open class="rounded-lg border border-black/15 dark:border-white/20">
<details open class="rounded-lg border">
<summary>Table of Contents</summary>
<nav class="">
<nav>
<ul class="py-3">
{toc.map((heading) => <TableOfContentsHeading heading={heading} />)}
</ul>
@ -40,12 +40,4 @@ function buildToc(headings: Heading[]) {
summary {
@apply cursor-pointer rounded-t-lg px-3 py-1.5 font-medium transition-colors;
}
summary:hover {
@apply bg-black/5 dark:bg-white/5;
}
details[open] summary {
@apply bg-black/5 dark:bg-white/5;
}
</style>

View file

@ -0,0 +1,57 @@
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(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View file

@ -0,0 +1,203 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import {
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
} from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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}
/>
))
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(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground 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(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground 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(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground 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="h-4 w-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(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground 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="h-4 w-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("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

View file

@ -0,0 +1,52 @@
import * as React from "react"
import { Moon, Sun } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
export function ModeToggle() {
const [theme, setThemeState] = React.useState<
"theme-light" | "dark" | "system"
>("theme-light")
React.useEffect(() => {
const isDarkMode = document.documentElement.classList.contains("dark")
setThemeState(isDarkMode ? "dark" : "theme-light")
}, [])
React.useEffect(() => {
const isDark =
theme === "dark" ||
(theme === "system" &&
window.matchMedia("(prefers-color-scheme: dark)").matches)
document.documentElement.classList[isDark ? "add" : "remove"]("dark")
}, [theme])
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setThemeState("theme-light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setThemeState("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setThemeState("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}