Update ThemeSelector to include system option
This commit is contained in:
parent
5f608c23f9
commit
d7a405eea1
@ -32,7 +32,7 @@ export function ArticleLayout({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
aria-label="Go back to articles"
|
aria-label="Go back to articles"
|
||||||
className="group mb-8 flex h-10 w-10 items-center justify-center rounded-full bg-white shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 transition dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0 dark:ring-white/10 dark:hover:border-zinc-700 dark:hover:ring-white/20 lg:absolute lg:-left-5 lg:-mt-2 lg:mb-0 xl:-top-1.5 xl:left-0 xl:mt-0"
|
className="group mb-8 flex h-10 w-10 items-center justify-center rounded-xl bg-white shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 transition dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0 dark:ring-white/10 dark:hover:border-zinc-700 dark:hover:ring-white/20 lg:absolute lg:-left-5 lg:-mt-2 lg:mb-0 xl:-top-1.5 xl:left-0 xl:mt-0"
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon className="h-4 w-4 stroke-zinc-500 transition group-hover:stroke-zinc-700 dark:stroke-zinc-500 dark:group-hover:stroke-zinc-400" />
|
<ArrowLeftIcon className="h-4 w-4 stroke-zinc-500 transition group-hover:stroke-zinc-700 dark:stroke-zinc-500 dark:group-hover:stroke-zinc-400" />
|
||||||
</button>
|
</button>
|
||||||
@ -46,7 +46,7 @@ export function ArticleLayout({
|
|||||||
dateTime={meta.date}
|
dateTime={meta.date}
|
||||||
className="order-first flex items-center text-base text-zinc-400 dark:text-zinc-500"
|
className="order-first flex items-center text-base text-zinc-400 dark:text-zinc-500"
|
||||||
>
|
>
|
||||||
<span className="h-4 w-0.5 rounded-full bg-zinc-200 dark:bg-zinc-500" />
|
<span className="h-4 w-0.5 rounded-xl bg-zinc-200 dark:bg-zinc-500" />
|
||||||
<span className="ml-3">{formatDate(meta.date)}</span>
|
<span className="ml-3">{formatDate(meta.date)}</span>
|
||||||
</time>
|
</time>
|
||||||
</header>
|
</header>
|
||||||
|
@ -74,7 +74,7 @@ Card.Eyebrow = function CardEyebrow({
|
|||||||
className="absolute inset-y-0 left-0 flex items-center"
|
className="absolute inset-y-0 left-0 flex items-center"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<span className="h-4 w-0.5 rounded-full bg-zinc-200 dark:bg-zinc-500" />
|
<span className="h-4 w-0.5 rounded-xl bg-zinc-200 dark:bg-zinc-500" />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{children}
|
{children}
|
||||||
|
@ -7,7 +7,8 @@ import clsx from 'clsx'
|
|||||||
|
|
||||||
import { Container } from '@/components/Container'
|
import { Container } from '@/components/Container'
|
||||||
import avatarImage from '@/images/avatar.jpg'
|
import avatarImage from '@/images/avatar.jpg'
|
||||||
import { ChevronDownIcon, CloseIcon, MoonIcon, SunIcon } from '@/components/SVGIcons'
|
import { ChevronDownIcon, CloseIcon } from '@/components/SVGIcons'
|
||||||
|
import { ThemeSelector } from './ThemeSelector'
|
||||||
|
|
||||||
function MobileNavItem({ href, children }) {
|
function MobileNavItem({ href, children }) {
|
||||||
return (
|
return (
|
||||||
@ -22,7 +23,7 @@ function MobileNavItem({ href, children }) {
|
|||||||
function MobileNavigation(props) {
|
function MobileNavigation(props) {
|
||||||
return (
|
return (
|
||||||
<Popover {...props}>
|
<Popover {...props}>
|
||||||
<Popover.Button className="group flex items-center rounded-full bg-white/90 px-4 py-2 text-sm font-medium text-zinc-800 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur dark:bg-zinc-800/90 dark:text-zinc-200 dark:ring-white/10 dark:hover:ring-white/20">
|
<Popover.Button className="group flex items-center rounded-xl bg-white/90 px-4 py-2 text-sm font-medium text-zinc-800 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur dark:bg-zinc-800/90 dark:text-zinc-200 dark:ring-white/10 dark:hover:ring-white/20">
|
||||||
Menu
|
Menu
|
||||||
<ChevronDownIcon className="ml-3 h-auto w-2 stroke-zinc-500 group-hover:stroke-zinc-700 dark:group-hover:stroke-zinc-400" />
|
<ChevronDownIcon className="ml-3 h-auto w-2 stroke-zinc-500 group-hover:stroke-zinc-700 dark:group-hover:stroke-zinc-400" />
|
||||||
</Popover.Button>
|
</Popover.Button>
|
||||||
@ -101,7 +102,7 @@ function NavItem({ href, children }) {
|
|||||||
function DesktopNavigation(props) {
|
function DesktopNavigation(props) {
|
||||||
return (
|
return (
|
||||||
<nav {...props}>
|
<nav {...props}>
|
||||||
<ul className="flex rounded-full bg-white/90 px-3 text-sm font-medium text-zinc-800 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur dark:bg-zinc-800/90 dark:text-zinc-200 dark:ring-white/10">
|
<ul className="flex rounded-xl bg-white/90 px-3 text-sm font-medium text-zinc-800 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur dark:bg-zinc-800/90 dark:text-zinc-200 dark:ring-white/10">
|
||||||
<NavItem href="/about">About</NavItem>
|
<NavItem href="/about">About</NavItem>
|
||||||
<NavItem href="/articles">Articles</NavItem>
|
<NavItem href="/articles">Articles</NavItem>
|
||||||
<NavItem href="/projects">Projects</NavItem>
|
<NavItem href="/projects">Projects</NavItem>
|
||||||
@ -112,41 +113,6 @@ function DesktopNavigation(props) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ModeToggle() {
|
|
||||||
function disableTransitionsTemporarily() {
|
|
||||||
document.documentElement.classList.add('[&_*]:!transition-none')
|
|
||||||
window.setTimeout(() => {
|
|
||||||
document.documentElement.classList.remove('[&_*]:!transition-none')
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleMode() {
|
|
||||||
disableTransitionsTemporarily()
|
|
||||||
|
|
||||||
let darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
|
||||||
let isSystemDarkMode = darkModeMediaQuery.matches
|
|
||||||
let isDarkMode = document.documentElement.classList.toggle('dark')
|
|
||||||
|
|
||||||
if (isDarkMode === isSystemDarkMode) {
|
|
||||||
delete window.localStorage.isDarkMode
|
|
||||||
} else {
|
|
||||||
window.localStorage.isDarkMode = isDarkMode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
aria-label="Toggle dark mode"
|
|
||||||
className="group rounded-full bg-white/90 px-3 py-2 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur transition dark:bg-zinc-800/90 dark:ring-white/10 dark:hover:ring-white/20"
|
|
||||||
onClick={toggleMode}
|
|
||||||
>
|
|
||||||
<SunIcon className="h-6 w-6 fill-zinc-100 stroke-zinc-500 transition group-hover:fill-zinc-200 group-hover:stroke-zinc-700 dark:hidden [@media(prefers-color-scheme:dark)]:fill-teal-50 [@media(prefers-color-scheme:dark)]:stroke-teal-500 [@media(prefers-color-scheme:dark)]:group-hover:fill-teal-50 [@media(prefers-color-scheme:dark)]:group-hover:stroke-teal-600" />
|
|
||||||
<MoonIcon className="hidden h-6 w-6 fill-zinc-700 stroke-zinc-500 transition dark:block [@media(prefers-color-scheme:dark)]:group-hover:stroke-zinc-400 [@media_not_(prefers-color-scheme:dark)]:fill-teal-400/10 [@media_not_(prefers-color-scheme:dark)]:stroke-teal-500" />
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function clamp(number, a, b) {
|
function clamp(number, a, b) {
|
||||||
let min = Math.min(a, b)
|
let min = Math.min(a, b)
|
||||||
let max = Math.max(a, b)
|
let max = Math.max(a, b)
|
||||||
@ -158,7 +124,7 @@ function AvatarContainer({ className, ...props }) {
|
|||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
className,
|
className,
|
||||||
'h-10 w-10 rounded-full bg-white/90 p-0.5 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur dark:bg-zinc-800/90 dark:ring-white/10'
|
'h-10 w-10 rounded-xl bg-white/90 p-0.5 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur dark:bg-zinc-800/90 dark:ring-white/10'
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -178,7 +144,7 @@ function Avatar({ large = false, className, ...props }) {
|
|||||||
alt=""
|
alt=""
|
||||||
sizes={large ? '4rem' : '2.25rem'}
|
sizes={large ? '4rem' : '2.25rem'}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'rounded-full bg-zinc-100 object-cover dark:bg-zinc-800',
|
'rounded-xl bg-zinc-100 object-cover dark:bg-zinc-800',
|
||||||
large ? 'h-16 w-16' : 'h-9 w-9'
|
large ? 'h-16 w-16' : 'h-9 w-9'
|
||||||
)}
|
)}
|
||||||
priority
|
priority
|
||||||
@ -354,7 +320,7 @@ export function Header() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end md:flex-1">
|
<div className="flex justify-end md:flex-1">
|
||||||
<div className="pointer-events-auto">
|
<div className="pointer-events-auto">
|
||||||
<ModeToggle />
|
<ThemeSelector className="relative z-10" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -216,3 +216,15 @@ export function SunIcon(props) {
|
|||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function SystemIcon(props) {
|
||||||
|
return (
|
||||||
|
<svg aria-hidden="true" viewBox="0 0 16 16" {...props}>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M1 4a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v4a3 3 0 0 1-3 3h-1.5l.31 1.242c.084.333.36.573.63.808.091.08.182.158.264.24A1 1 0 0 1 11 15H5a1 1 0 0 1-.704-1.71c.082-.082.173-.16.264-.24.27-.235.546-.475.63-.808L5.5 11H4a3 3 0 0 1-3-3V4Zm3-1a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H4Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
96
src/components/ThemeSelector.jsx
Normal file
96
src/components/ThemeSelector.jsx
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { Listbox } from '@headlessui/react'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
import { MoonIcon, SunIcon, SystemIcon } from '@/components//SVGIcons'
|
||||||
|
|
||||||
|
const themes = [
|
||||||
|
{ name: 'Light', value: 'light', icon: SunIcon },
|
||||||
|
{ name: 'Dark', value: 'dark', icon: MoonIcon },
|
||||||
|
{ name: 'System', value: 'system', icon: SystemIcon },
|
||||||
|
]
|
||||||
|
|
||||||
|
export function ThemeSelector(props) {
|
||||||
|
let [selectedTheme, setSelectedTheme] = useState(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedTheme) {
|
||||||
|
document.documentElement.setAttribute('data-theme', selectedTheme.value)
|
||||||
|
} else {
|
||||||
|
setSelectedTheme(
|
||||||
|
themes.find(
|
||||||
|
(theme) =>
|
||||||
|
theme.value === document.documentElement.getAttribute('data-theme')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [selectedTheme])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let handler = () =>
|
||||||
|
setSelectedTheme(
|
||||||
|
themes.find(
|
||||||
|
(theme) => theme.value === (window.localStorage.theme ?? 'system')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
window.addEventListener('storage', handler)
|
||||||
|
|
||||||
|
return () => window.removeEventListener('storage', handler)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Listbox
|
||||||
|
as="div"
|
||||||
|
value={selectedTheme}
|
||||||
|
onChange={setSelectedTheme}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Listbox.Label className="sr-only">Theme</Listbox.Label>
|
||||||
|
<Listbox.Button
|
||||||
|
className="group rounded-xl bg-white/90 px-3 py-2 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur transition dark:bg-zinc-800/90 dark:ring-white/10 dark:hover:ring-white/20"
|
||||||
|
aria-label={selectedTheme?.name}
|
||||||
|
>
|
||||||
|
<SunIcon className="hidden h-6 w-6 fill-teal-50 stroke-teal-500 [[data-theme=light]_&]:block" />
|
||||||
|
<MoonIcon className="hidden h-6 w-6 fill-teal-400/10 stroke-teal-500 [[data-theme=dark]_&]:block" />
|
||||||
|
<SunIcon className="hidden h-6 w-6 fill-zinc-100 stroke-zinc-500 transition group-hover:fill-zinc-200 group-hover:stroke-zinc-700 [:not(.dark)[data-theme=system]_&]:block" />
|
||||||
|
<MoonIcon className="hidden h-6 w-6 fill-zinc-700 stroke-zinc-500 transition group-hover:stroke-zinc-400 [.dark[data-theme=system]_&]:block" />
|
||||||
|
</Listbox.Button>
|
||||||
|
<Listbox.Options className="absolute left-1/2 top-full mt-3 w-36 -translate-x-1/2 space-y-1 rounded-xl bg-white p-3 text-sm font-medium shadow-md shadow-black/5 ring-1 ring-black/5 dark:bg-slate-800 dark:ring-white/5">
|
||||||
|
{themes.map((theme) => (
|
||||||
|
<Listbox.Option
|
||||||
|
key={theme.value}
|
||||||
|
value={theme}
|
||||||
|
className={({ active, selected }) =>
|
||||||
|
clsx(
|
||||||
|
'flex cursor-pointer select-none items-center rounded-[0.625rem] p-1',
|
||||||
|
{
|
||||||
|
'text-teal-400': selected,
|
||||||
|
'text-zinc-900 dark:text-white': active && !selected,
|
||||||
|
'text-zinc-700 dark:text-zinc-400': !active && !selected,
|
||||||
|
'bg-zinc-100 dark:bg-zinc-900/40': active,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ selected }) => (
|
||||||
|
<>
|
||||||
|
<div className="rounded-md bg-white p-1 shadow ring-1 ring-slate-900/5 dark:bg-slate-700 dark:ring-inset dark:ring-white/5">
|
||||||
|
<theme.icon
|
||||||
|
className={clsx(
|
||||||
|
'h-4 w-4',
|
||||||
|
selected
|
||||||
|
? 'fill-teal-400 stroke-teal-400'
|
||||||
|
: 'fill-zinc-500 stroke-zinc-500'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3">{theme.name}</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Listbox.Option>
|
||||||
|
))}
|
||||||
|
</Listbox.Options>
|
||||||
|
</Listbox>
|
||||||
|
)
|
||||||
|
}
|
@ -1,45 +1,48 @@
|
|||||||
import { Head, Html, Main, NextScript } from 'next/document'
|
import { Head, Html, Main, NextScript } from 'next/document'
|
||||||
|
|
||||||
const modeScript = `
|
const themeScript = `
|
||||||
let darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
let isDarkMode = window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
|
|
||||||
updateMode()
|
function updateTheme(theme) {
|
||||||
darkModeMediaQuery.addEventListener('change', updateModeWithoutTransitions)
|
theme = theme ?? window.localStorage.theme ?? 'system'
|
||||||
window.addEventListener('storage', updateModeWithoutTransitions)
|
|
||||||
|
|
||||||
function updateMode() {
|
if (theme === 'dark' || (theme === 'system' && isDarkMode.matches)) {
|
||||||
let isSystemDarkMode = darkModeMediaQuery.matches
|
|
||||||
let isDarkMode = window.localStorage.isDarkMode === 'true' || (!('isDarkMode' in window.localStorage) && isSystemDarkMode)
|
|
||||||
|
|
||||||
if (isDarkMode) {
|
|
||||||
document.documentElement.classList.add('dark')
|
document.documentElement.classList.add('dark')
|
||||||
} else {
|
} else if (theme === 'light' || (theme === 'system' && !isDarkMode.matches)) {
|
||||||
document.documentElement.classList.remove('dark')
|
document.documentElement.classList.remove('dark')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDarkMode === isSystemDarkMode) {
|
return theme
|
||||||
delete window.localStorage.isDarkMode
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function disableTransitionsTemporarily() {
|
function updateThemeWithoutTransitions(theme) {
|
||||||
|
updateTheme(theme)
|
||||||
document.documentElement.classList.add('[&_*]:!transition-none')
|
document.documentElement.classList.add('[&_*]:!transition-none')
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
document.documentElement.classList.remove('[&_*]:!transition-none')
|
document.documentElement.classList.remove('[&_*]:!transition-none')
|
||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateModeWithoutTransitions() {
|
document.documentElement.setAttribute('data-theme', updateTheme())
|
||||||
disableTransitionsTemporarily()
|
|
||||||
updateMode()
|
new MutationObserver(([{ oldValue }]) => {
|
||||||
}
|
let newValue = document.documentElement.getAttribute('data-theme')
|
||||||
|
if (newValue !== oldValue) {
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem('theme', newValue)
|
||||||
|
} catch {}
|
||||||
|
updateThemeWithoutTransitions(newValue)
|
||||||
|
}
|
||||||
|
}).observe(document.documentElement, { attributeFilter: ['data-theme'], attributeOldValue: true })
|
||||||
|
|
||||||
|
isDarkMode.addEventListener('change', () => updateThemeWithoutTransitions())
|
||||||
`
|
`
|
||||||
|
|
||||||
export default function Document() {
|
export default function Document() {
|
||||||
return (
|
return (
|
||||||
<Html className="h-full antialiased" lang="en">
|
<Html className="h-full antialiased" lang="en">
|
||||||
<Head>
|
<Head>
|
||||||
<script dangerouslySetInnerHTML={{ __html: modeScript }} />
|
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
|
||||||
<link
|
<link
|
||||||
rel="alternate"
|
rel="alternate"
|
||||||
type="application/rss+xml"
|
type="application/rss+xml"
|
||||||
|
@ -96,7 +96,7 @@ function Resume() {
|
|||||||
<ol className="mt-6 space-y-4">
|
<ol className="mt-6 space-y-4">
|
||||||
{resume.map((role, roleIndex) => (
|
{resume.map((role, roleIndex) => (
|
||||||
<li key={roleIndex} className="flex gap-4">
|
<li key={roleIndex} className="flex gap-4">
|
||||||
<div className="relative mt-1 flex h-10 w-10 flex-none items-center justify-center rounded-full shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0">
|
<div className="relative mt-1 flex h-10 w-10 flex-none items-center justify-center rounded-xl shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0">
|
||||||
<Image src={role.logo} alt="" className="h-7 w-7" unoptimized />
|
<Image src={role.logo} alt="" className="h-7 w-7" unoptimized />
|
||||||
</div>
|
</div>
|
||||||
<dl className="flex flex-auto flex-wrap gap-x-2">
|
<dl className="flex flex-auto flex-wrap gap-x-2">
|
||||||
@ -111,9 +111,8 @@ function Resume() {
|
|||||||
<dt className="sr-only">Date</dt>
|
<dt className="sr-only">Date</dt>
|
||||||
<dd
|
<dd
|
||||||
className="ml-auto text-xs text-zinc-400 dark:text-zinc-500"
|
className="ml-auto text-xs text-zinc-400 dark:text-zinc-500"
|
||||||
aria-label={`${role.start.label ?? role.start} until ${
|
aria-label={`${role.start.label ?? role.start} until ${role.end.label ?? role.end
|
||||||
role.end.label ?? role.end
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<time dateTime={role.start.dateTime ?? role.start}>
|
<time dateTime={role.start.dateTime ?? role.start}>
|
||||||
{role.start.label ?? role.start}
|
{role.start.label ?? role.start}
|
||||||
|
@ -68,7 +68,7 @@ export default function Projects() {
|
|||||||
>
|
>
|
||||||
{projects.map((project) => (
|
{projects.map((project) => (
|
||||||
<Card as="li" key={project.name}>
|
<Card as="li" key={project.name}>
|
||||||
<div className="relative z-10 flex h-12 w-12 items-center justify-center rounded-full bg-white shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0">
|
<div className="relative z-10 flex h-12 w-12 items-center justify-center rounded-xl bg-white shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0">
|
||||||
<Image
|
<Image
|
||||||
src={project.logo}
|
src={project.logo}
|
||||||
alt=""
|
alt=""
|
||||||
|
Loading…
x
Reference in New Issue
Block a user