12 Commits

Author SHA1 Message Date
bc940a7a6e Merge pull request '修改readme说明中的首页图片' (#12) from dev into main
Reviewed-on: #12
2025-11-10 23:07:11 +08:00
0f4eaf8523 修改readme说明中的首页图片 2025-11-10 23:06:42 +08:00
6755f95e5e Merge pull request '优化整体布局,切换主题为拟物化样式' (#11) from dev into main
Reviewed-on: #11
2025-11-10 23:01:52 +08:00
b29f5c7c1e 修复代码中的错误 2025-11-10 22:54:58 +08:00
20b01c6f51 调整首页blog标题行高,因为字符g显示不全
修复原有圆角元素替换为直角
2025-11-10 22:36:24 +08:00
57e1724c34 调整整体菜单样式 2025-11-10 22:18:01 +08:00
c720e605bf 修改为妙妙屋主题 2025-11-10 21:31:16 +08:00
6575bf0c62 更新footer图标 2025-11-10 16:34:18 +08:00
639e3b54db 添加project菜单和妙妙屋项目 2025-11-10 16:07:12 +08:00
7be284fbd5 buildnginx.mdx update 2025-11-10 10:23:33 +08:00
c74c99e776 Merge branch 'dev' of code.2ha.me:dev.2ha.me/dev.2ha.me into dev 2025-10-16 15:54:05 +08:00
8026dd0321 更新开发时间 2025-10-16 15:53:48 +08:00
38 changed files with 1808 additions and 247 deletions

4
.gitignore vendored
View File

@@ -24,4 +24,6 @@ pnpm-debug.log*
.idea/
teaser.pptx
~$teaser.pptx
~$teaser.pptx
.claude

Binary file not shown.

1
public/static/mmw.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 959 KiB

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 959 KiB

View File

@@ -37,7 +37,7 @@ const socialLinks: SocialLink[] = [
---
<div
class="overflow-hidden rounded-xl border p-4 transition-colors duration-300 ease-in-out has-[a:hover]:bg-secondary/50"
class="border-2 border-[color-mix(in_srgb,hsl(var(--primary))_22%,hsl(var(--border)))] bg-secondary/25 p-4 [box-shadow:4px_4px_0_rgba(0,0,0,0.22)] transition-all duration-200 hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:[box-shadow:4px_4px_0_rgba(0,0,0,0.65)] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]"
>
<div class="flex flex-wrap gap-4">
<Link

192
src/components/Badge.astro Normal file
View File

@@ -0,0 +1,192 @@
---
import { Badge as BadgeComponent } from '@/components/ui/badge'
import { cn } from '@/lib/utils'
import { Icon } from 'astro-icon/components'
interface Props {
variant?: 'default' | 'secondary' | 'destructive' | 'outline'
className?: string
children?: any
text?: string
showIcon?: boolean
}
const {
variant = 'secondary',
className = '',
children,
text,
showIcon = true,
} = Astro.props
const categoryMappings = [
{
keywords: ['crypto'],
style: {
color:
'bg-yellow-50 text-yellow-700 dark:bg-yellow-950/30 dark:text-yellow-200',
icon: 'lucide:key',
},
},
{
keywords: ['web'],
style: {
color: 'bg-blue-50 text-blue-700 dark:bg-blue-950/30 dark:text-blue-200',
icon: 'lucide:globe',
},
},
{
keywords: ['reverse', 'rev'],
style: {
color:
'bg-orange-50 text-orange-700 dark:bg-orange-950/30 dark:text-orange-200',
icon: 'lucide:rotate-ccw',
},
},
{
keywords: ['pwn', 'binary exploitation'],
style: {
color: 'bg-red-50 text-red-700 dark:bg-red-950/30 dark:text-red-200',
icon: 'lucide:zap',
},
},
{
keywords: ['misc'],
style: {
color:
'bg-stone-50 text-stone-700 dark:bg-stone-950/30 dark:text-stone-200',
icon: 'lucide:puzzle',
},
},
{
keywords: ['forensic'],
style: {
color:
'bg-green-50 text-green-700 dark:bg-green-950/30 dark:text-green-200',
icon: 'lucide:search',
},
},
{
keywords: ['osint'],
style: {
color:
'bg-purple-50 text-purple-700 dark:bg-purple-950/30 dark:text-purple-200',
icon: 'lucide:eye',
},
},
{
keywords: ['blockchain'],
style: {
color: 'bg-teal-50 text-teal-700 dark:bg-teal-950/30 dark:text-teal-200',
icon: 'lucide:link',
},
},
{
keywords: ['ppc', 'programming'],
style: {
color:
'bg-indigo-50 text-indigo-700 dark:bg-indigo-950/30 dark:text-indigo-200',
icon: 'lucide:code',
},
},
{
keywords: ['commercial'],
style: {
color: 'text-foreground bg-foreground/10',
icon: 'lucide:building-2',
},
},
{
keywords: ['personal'],
style: {
// color: 'bg-sky-50 text-sky-700 dark:bg-sky-950/30 dark:text-sky-200',
icon: 'lucide:user',
},
},
{
keywords: ['open-source'],
style: {
// color: 'bg-emerald-50 text-emerald-700 dark:bg-emerald-950/30 dark:text-emerald-200',
icon: 'lucide:git-branch',
},
},
{
keywords: ['freelance'],
style: {
// color: 'bg-teal-50 text-teal-700 dark:bg-teal-950/30 dark:text-teal-200',
icon: 'lucide:briefcase',
},
},
{
keywords: ['team'],
style: {
// color: 'bg-violet-50 text-violet-700 dark:bg-violet-950/30 dark:text-violet-200',
icon: 'lucide:users',
},
},
{
keywords: ['contract'],
style: {
// color: 'bg-rose-50 text-rose-700 dark:bg-rose-950/30 dark:text-rose-200',
icon: 'lucide:file-text',
},
},
{
keywords: ['astro'],
style: {
// color: 'bg-orange-50 text-orange-700 dark:bg-orange-950/30 dark:text-orange-200',
icon: 'lucide:rocket',
},
},
{
keywords: ['shopify'],
style: {
// color: 'bg-green-50 text-green-700 dark:bg-green-950/30 dark:text-green-200',
icon: 'lucide:shopping-bag',
},
},
{
keywords: ['html'],
style: {
// color: 'bg-red-50 text-red-700 dark:bg-red-950/30 dark:text-red-200',
icon: 'lucide:code-2',
},
},
{
keywords: ['figma'],
style: {
// color: 'bg-purple-50 text-purple-700 dark:bg-purple-950/30 dark:text-purple-200',
icon: 'lucide:palette',
},
},
]
const getCategoryStyle = (content: string) => {
const lowerContent = content.toLowerCase()
const match = categoryMappings.find((category) =>
category.keywords.some((keyword) => lowerContent.includes(keyword)),
)
return match?.style || null
}
const content = text || (typeof children === 'string' ? children : '')
const categoryStyle = getCategoryStyle(content)
---
<BadgeComponent
variant={categoryStyle ? 'secondary' : variant}
className={cn(categoryStyle?.color, className)}
client:load
>
{
showIcon && (
<Icon
name={categoryStyle ? categoryStyle.icon : 'lucide:tag'}
class="size-3"
/>
)
}
<slot>{text}</slot>
</BadgeComponent>

View File

@@ -24,13 +24,14 @@ const subposts = allPosts.filter((p) => p.data.parentTitle === entry.data.title)
const totalBody = [entry.body!, ...subposts.map((p) => p.body!)]
.map(stripCodeBlocks)
.join('')
const readTime = readingTime(totalBody)
const wordCount = totalBody.split(/\s+/).filter(Boolean).length
const readTime = readingTime(wordCount)
const authors = await parseAuthors(entry.data.authors ?? [])
---
<div
class="not-prose rounded-xl border p-4 transition-colors duration-300 ease-in-out hover:bg-secondary/50"
class="not-prose border-2 border-[color-mix(in_srgb,hsl(var(--primary))_22%,hsl(var(--border)))] bg-secondary/25 p-4 [box-shadow:4px_4px_0_rgba(0,0,0,0.22)] transition-all duration-200 hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:[box-shadow:4px_4px_0_rgba(0,0,0,0.65)] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]"
>
<Link
href={`/${entry.collection}/${entry.id}`}

View File

@@ -87,14 +87,23 @@ const SHORTS_CUTS_CLASS_NAMES: string[] = [
target="_blank"
>
<div
class="bottom-0 right-0 w-fit items-end rounded-full border bg-secondary/50 p-3 text-primary transition-all duration-300 hover:rotate-12 hover:ring-1 hover:ring-primary"
class="bottom-0 right-0 w-fit items-end rounded-full border-2 border-[color-mix(in_srgb,hsl(var(--primary))_30%,hsl(var(--border)))] bg-secondary/50 p-3 text-primary transition-all duration-300 hover:rotate-12 hover:scale-110 hover:border-[color-mix(in_srgb,hsl(var(--primary))_50%,hsl(var(--border)))]"
>
<Icon
style="color: rgb(233, 211, 182);"
name={item.icon}
class="z-[1] size-1/2 size-8 text-primary sm:size-8"
aria-hidden="true"
/>
{item.icon.startsWith('mdi:') ? (
<Icon
style="color: rgb(233, 211, 182);"
name={item.icon}
class="z-[1] size-1/2 size-8 text-primary sm:size-8"
aria-hidden="true"
/>
) : (
<img
src={item.icon}
alt={item.title}
class="z-[1] size-1/2 size-8 sm:size-8"
aria-hidden="true"
/>
)}
</div>
</Link>
</div>

View File

@@ -4,7 +4,9 @@ import Link from '@/components/Link.astro'
import MobileMenu from '@/components/ui/mobile-menu'
import { NAV_LINKS } from '@/consts'
import { Image } from 'astro:assets'
import { Icon } from 'astro-icon/components'
import logo from '../../public/static/logo.webp'
---
<header
@@ -14,17 +16,18 @@ import logo from '../../public/static/logo.webp'
<Container>
<div class="flex flex-wrap items-center justify-between gap-4 py-4 ">
<Link href="/">
<Image src={logo} alt="Logo" class="size-8 -scale-x-100" />
<Image src={logo} alt="Logo" class="size-8 -scale-x-100 border-2 border-[color:rgba(241,140,110,0.4)] shadow-[4px_4px_0_rgba(0,0,0,0.2)]" />
</Link>
<div class="flex items-center gap-2 sm:gap-4">
<nav class="hidden items-center gap-4 text-base sm:flex sm:gap-6">
<nav class="hidden items-center gap-2 text-base sm:flex sm:gap-3">
{
NAV_LINKS.map((item) => (
<Link
href={item.href}
class="capitalize text-foreground/60 transition-colors hover:text-foreground/80"
class="pixel-button inline-flex items-center justify-center gap-2 px-4 py-2 h-9 text-sm font-semibold uppercase tracking-wider bg-background/75 text-foreground border-[color:rgba(137,110,96,0.45)] hover:bg-primary/20 hover:text-primary hover:border-[color:rgba(217,119,87,0.65)] dark:bg-input/30 dark:border-[color:rgba(255,255,255,0.18)] dark:hover:bg-primary/25 dark:hover:text-primary dark:hover:border-[color:rgba(241,140,110,0.75)] transition-all"
>
{item.label}
<Icon name={item.icon} class="size-[18px] shrink-0" />
<span>{item.label}</span>
</Link>
))
}

View File

@@ -0,0 +1,36 @@
---
import { SITE } from '@/consts'
interface Props {
title?: string
description?: string
noindex?: boolean
}
const {
title = SITE.TITLE,
description = SITE.DESCRIPTION,
noindex = false,
} = Astro.props
const image = new URL('/static/twitter-card.png', Astro.site)
---
<title>{`${title} | ${SITE.TITLE}`}</title>
<meta name="description" content={description} />
<link rel="canonical" href={SITE.SITEURL} />
{noindex && <meta name="robots" content="noindex, nofollow" />}
<meta property="og:title" content={`${title} | ${SITE.TITLE}`} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<meta property="og:image:alt" content={title} />
<meta property="og:type" content="website" />
<meta property="og:locale" content={SITE.locale} />
<meta property="og:site_name" content={SITE.TITLE} />
<meta property="og:url" content={Astro.url} />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={image} />
<meta name="twitter:image:alt" content={title} />
<meta name="twitter:card" content="summary_large_image" />

View File

@@ -1,10 +1,13 @@
---
import Badge from '@/components/Badge.astro'
import Link from '@/components/Link.astro'
import { Badge } from '@/components/ui/badge'
import { Separator } from '@/components/ui/separator'
import { extractDomain, formatMonthYear } from '@/lib/utils'
import { Icon } from 'astro-icon/components'
import { Image } from 'astro:assets'
import type { CollectionEntry } from 'astro:content'
type Props = {
interface Props {
project: CollectionEntry<'projects'>
}
@@ -12,30 +15,63 @@ const { project } = Astro.props
---
<div
class="overflow-hidden rounded-xl border transition-colors duration-300 ease-in-out hover:bg-secondary/50"
class="border-2 border-[color-mix(in_srgb,hsl(var(--primary))_22%,hsl(var(--border)))] bg-secondary/25 p-4 [box-shadow:4px_4px_0_rgba(0,0,0,0.22)] transition-all duration-200 hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:[box-shadow:4px_4px_0_rgba(0,0,0,0.65)] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]"
>
<Link href={project.data.link} class="block">
<Image
src={project.data.image}
alt={project.data.name}
width={400}
height={200}
class="w-full object-cover"
/>
<div class="p-4">
<h3 class="mb-2 text-lg font-semibold">{project.data.name}</h3>
<p class="mb-4 text-sm text-muted-foreground">
<Link
href={project.data.link}
class="flex flex-col gap-4 sm:flex-row"
target="_blank"
rel="noopener noreferrer"
>
{
project.data.image && (
<div class="max-w-[225px] sm:flex-shrink-0">
<Image
src={project.data.image}
alt={project.data.name}
width={1200}
height={630}
class="object-cover"
/>
</div>
)
}
<div class="grow">
<h3 class="mb-1 text-lg font-semibold">
{project.data.name}
</h3>
<p class="text-muted-foreground mb-2 text-sm">
{project.data.description}
</p>
<div class="flex flex-wrap gap-2">
{
project.data.tags.map((tag) => (
<Badge variant="secondary" showHash={false}>
{tag}
</Badge>
))
}
</div>
{
project.data.startDate && (
<div class="text-muted-foreground/70 mb-2 flex flex-wrap items-center gap-x-2 text-xs">
<span class="flex items-center gap-x-1.5">
<Icon name="lucide:calendar" class="size-3" />
<span>
{formatMonthYear(project.data.startDate)}
{project.data.endDate
? ` → ${formatMonthYear(project.data.endDate)}`
: ' → Present'}
</span>
</span>
<Separator orientation="vertical" className="h-4!" />
<span class="flex items-center gap-x-1">
<Icon name="lucide:external-link" class="size-3" />
<span>{extractDomain(project.data.link)}</span>
</span>
</div>
)
}
{
project.data.tags && (
<div class="flex flex-wrap gap-2">
{project.data.tags.map((tag: string) => (
<Badge text={tag} />
))}
</div>
)
}
</div>
</Link>
</div>

View File

@@ -22,6 +22,7 @@ const iconMap = {
Gitea: 'mdi:git',
Maven: 'mavenrepo',
DevIntro: 'lucide:info',
HubProxy: 'lucide:rocket',
}
const getSocialLink = ({ href, label }: SocialLink) => ({

View File

@@ -55,12 +55,12 @@ const WakatimeGraph = ({ }: Props) => {
useEffect(() => {
setLanguages([
{ name: 'java', hours: 1009, fill: 'hsl(var(--chart-1))' },
{ name: 'kotlin', hours: 346, fill: 'hsl(var(--chart-2))' },
{ name: 'javascript', hours: 311, fill: 'hsl(var(--chart-3))' },
{ name: 'typescript', hours: 287, fill: 'hsl(var(--chart-4))' },
{ name: 'python', hours: 120, fill: 'hsl(var(--chart-5))' },
{ name: 'react', hours: 85, fill: 'hsl(var(--chart-6))' },
{ name: 'go', hours: 9, fill: 'hsl(var(--chart-7))' },
{ name: 'javascript', hours: 476, fill: 'hsl(var(--chart-2))' },
{ name: 'kotlin', hours: 405, fill: 'hsl(var(--chart-3))' },
{ name: 'typescript', hours: 401, fill: 'hsl(var(--chart-4))' },
{ name: 'react', hours: 257, fill: 'hsl(var(--chart-5))' },
{ name: 'go', hours: 125, fill: 'hsl(var(--chart-6))' },
{ name: 'python', hours: 120, fill: 'hsl(var(--chart-7))' },
])
setIsLoading(false)
}, [])

View File

@@ -62,6 +62,13 @@ const Music163Player = () => {
})
}, [])
// Set audio volume to 50%
useEffect(() => {
if (audioRef.current) {
audioRef.current.volume = 0.5
}
}, [displayData])
if (isLoading) {
return (
<div className="relative flex h-full w-full flex-col justify-between rounded-3xl p-6">
@@ -102,7 +109,7 @@ const Music163Player = () => {
alt="Album art"
width={128}
height={128}
className="mb-2 w-[55%] rounded-xl border border-border"
className="mb-2 w-[55%] border-2 border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))]"
/>
</a>
<div className="flex min-w-0 flex-1 flex-col justify-end overflow-hidden">
@@ -141,7 +148,7 @@ const Music163Player = () => {
<span className="w-[85%] truncate text-xs text-muted-foreground">
<span className="font-semibold text-secondary-foreground">
<div>
<audio ref={audioRef} >
<audio ref={audioRef}>
<source src={outerurl} type="audio/mp3" />
<source src={backupurl} type="audio/mp3" />
</audio>

View File

@@ -58,7 +58,7 @@ const RandomAnimeBackground = () => {
// }
return (
<video ref={videoRef} width="100" height="100" className="no-repeat relative w-full justify-center rounded-[1.4em] object-cover"
<video ref={videoRef} width="100" height="100" className="no-repeat relative w-full justify-center object-cover"
src={'/static/anime-bg/' + videoBackgrounds[index]}
style={{ display: isLoading ? 'none' : 'block' }}
onEnded={handleVideoEnded}

View File

@@ -24,7 +24,7 @@ export default function GradientText({
// console.log(children)
return (
<div
className={`relative mx-auto ml-2 md:max-h-[1em] flex max-w-fit cursor-pointer flex-row items-center justify-center overflow-hidden rounded-[1.25rem] font-medium backdrop-blur transition-shadow duration-500 ${className}`}
className={`relative mx-auto ml-2 flex max-w-fit cursor-pointer flex-row items-center justify-center rounded-[1.25rem] font-medium backdrop-blur transition-shadow duration-500 ${className}`}
>
{showBorder && (
<div

View File

@@ -280,7 +280,7 @@ const LetterGlitch = ({
}, [glitchSpeed, smooth])
return (
<div className="relative h-full w-full overflow-hidden rounded-3xl bg-black">
<div className="relative h-full w-full overflow-hidden bg-black">
<canvas ref={canvasRef} className="block h-full w-full" />
{outerVignette && (
<div className="pointer-events-none absolute left-0 top-0 h-full w-full bg-[radial-gradient(circle,_rgba(0,0,0,0)_60%,_rgba(0,0,0,1)_100%)]"></div>

View File

@@ -5,25 +5,25 @@ import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 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 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
'inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border-2',
{
variants: {
variant: {
default:
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
'bg-primary text-primary-foreground [box-shadow:3px_3px_0_rgba(0,0,0,0.18)] hover:[box-shadow:4px_4px_0_rgba(0,0,0,0.22)] hover:-translate-y-[1px] active:[box-shadow:1px_1px_0_rgba(0,0,0,0.16)] active:translate-y-0 dark:[box-shadow:3px_3px_0_rgba(0,0,0,0.45)] dark:hover:[box-shadow:4px_4px_0_rgba(0,0,0,0.55)] dark:active:[box-shadow:1px_1px_0_rgba(0,0,0,0.35)]',
destructive:
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
'bg-destructive text-destructive-foreground [box-shadow:3px_3px_0_rgba(0,0,0,0.18)] hover:[box-shadow:4px_4px_0_rgba(0,0,0,0.22)] hover:-translate-y-[1px] dark:[box-shadow:3px_3px_0_rgba(0,0,0,0.45)]',
outline:
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
'border-input bg-background [box-shadow:3px_3px_0_rgba(0,0,0,0.18)] hover:bg-accent hover:text-accent-foreground hover:[box-shadow:4px_4px_0_rgba(0,0,0,0.22)] hover:-translate-y-[1px] dark:[box-shadow:3px_3px_0_rgba(0,0,0,0.45)] dark:hover:[box-shadow:4px_4px_0_rgba(0,0,0,0.55)]',
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',
'bg-secondary text-secondary-foreground [box-shadow:3px_3px_0_rgba(0,0,0,0.18)] hover:bg-secondary/80 hover:[box-shadow:4px_4px_0_rgba(0,0,0,0.22)] hover:-translate-y-[1px] dark:[box-shadow:3px_3px_0_rgba(0,0,0,0.45)]',
ghost: 'hover:bg-accent hover:text-accent-foreground border-transparent',
link: 'text-primary underline-offset-4 hover:underline border-transparent',
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
sm: 'h-8 px-3 text-xs',
lg: 'h-10 px-8',
icon: 'h-9 w-9',
},
},

View File

@@ -28,7 +28,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
<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',
'flex cursor-default select-none items-center px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
inset && 'pl-8',
className,
)}
@@ -48,7 +48,7 @@ const DropdownMenuSubContent = React.forwardRef<
<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',
'z-50 min-w-[8rem] overflow-hidden border-2 border-[color:rgba(241,140,110,0.22)] bg-popover p-1 text-popover-foreground [box-shadow:4px_4px_0_rgba(0,0,0,0.22)] dark:[box-shadow:4px_4px_0_rgba(0,0,0,0.65)] 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}
@@ -66,7 +66,7 @@ const DropdownMenuContent = React.forwardRef<
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',
'z-50 min-w-[8rem] overflow-hidden border-2 border-[color:rgba(241,140,110,0.22)] bg-popover p-1 text-popover-foreground [box-shadow:4px_4px_0_rgba(0,0,0,0.22)] dark:[box-shadow:4px_4px_0_rgba(0,0,0,0.65)]',
'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,
)}
@@ -85,7 +85,7 @@ const DropdownMenuItem = React.forwardRef<
<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',
'relative flex cursor-default select-none items-center 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,
)}
@@ -101,7 +101,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
<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',
'relative flex cursor-default select-none items-center 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}
@@ -125,7 +125,7 @@ const DropdownMenuRadioItem = React.forwardRef<
<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',
'relative flex cursor-default select-none items-center 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}

View File

@@ -7,7 +7,20 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { NAV_LINKS } from '@/consts'
import { Menu } from 'lucide-react'
import { Menu, Home, FileText, FolderGit2, BadgeInfo } from 'lucide-react'
const getIconComponent = (iconName?: string) => {
if (!iconName) return Home
const iconMap: Record<string, any> = {
'lucide:home': Home,
'lucide:file-text': FileText,
'lucide:folder-git-2': FolderGit2,
'lucide:badge-info': BadgeInfo,
}
return iconMap[iconName] || Home
}
const MobileMenu = () => {
const [isOpen, setIsOpen] = useState(false)
@@ -33,25 +46,29 @@ const MobileMenu = () => {
<Button
variant="outline"
size="icon"
className="sm:hidden"
className="sm:hidden pixel-button h-9 w-9 bg-background/75 border-[color:rgba(137,110,96,0.45)] hover:bg-primary/20 hover:text-primary hover:border-[color:rgba(217,119,87,0.65)] dark:bg-input/30 dark:border-[color:rgba(255,255,255,0.18)] dark:hover:bg-primary/25 dark:hover:text-primary dark:hover:border-[color:rgba(241,140,110,0.75)]"
title="Menu"
>
<Menu className="h-5 w-5" />
<span className="sr-only">Toggle menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="bg-background">
{NAV_LINKS.map((item) => (
<DropdownMenuItem key={item.href} asChild>
<a
href={item.href}
className="w-full text-lg font-medium capitalize"
onClick={() => setIsOpen(false)}
>
{item.label}
</a>
</DropdownMenuItem>
))}
<DropdownMenuContent align="end" className="bg-background min-w-[140px] w-auto p-0">
{NAV_LINKS.map((item) => {
const Icon = getIconComponent(item.icon)
return (
<DropdownMenuItem key={item.href} asChild className="p-0">
<a
href={item.href}
className="flex items-center justify-center gap-2 w-full px-4 py-2.5 text-base font-semibold uppercase tracking-wider cursor-pointer hover:bg-primary/20 hover:text-primary focus:bg-primary/20 focus:text-primary dark:hover:bg-primary/25 dark:hover:text-primary border-b border-[color:rgba(241,140,110,0.15)] last:border-b-0 transition-colors"
onClick={() => setIsOpen(false)}
>
<Icon className="size-[18px] shrink-0" />
<span>{item.label}</span>
</a>
</DropdownMenuItem>
)
})}
</DropdownMenuContent>
</DropdownMenu>
)

View File

@@ -5,11 +5,13 @@ export type Site = {
NUM_POSTS_ON_HOMEPAGE: number
POSTS_PER_PAGE: number
SITEURL: string
locale: string
}
export type Link = {
href: string
label: string
icon?: string
}
export type DevLink = {
@@ -26,19 +28,20 @@ export const SITE: Site = {
NUM_POSTS_ON_HOMEPAGE: 2,
POSTS_PER_PAGE: 4,
SITEURL: 'https://dev.2ha.me',
locale: 'zh-CN',
}
export const NAV_LINKS: Link[] = [
{ href: '/', label: '主页' },
{ href: '/blog', label: '博客' },
{ href: '/tags', label: '标签' },
// { href: '/authors', label: '作者' },
{ href: '/authors', label: '关于' },
{ href: '/', label: '主页', icon: 'lucide:home' },
{ href: '/blog', label: '博客', icon: 'lucide:file-text' },
// { href: '/tags', label: '标签' },
{ href: '/projects', label: '项目', icon: 'lucide:folder-git-2'},
{ href: '/authors', label: '关于', icon: 'lucide:badge-info' },
]
export const SOCIAL_LINKS: Link[] = [
{ href: 'https://github.com/jimleerx', label: 'GitHub' },
{ href: 'https://maven.2ha.me', label: 'Maven' },
{ href: 'https://1ms.cc', label: 'HubProxy' },
{ href: 'https://code.2ha.me', label: 'Gitea' },
{ href: 'li@2ha.me', label: 'Email' },
]
@@ -88,15 +91,15 @@ export const DEV_LINKS: DevLink[] = [
icon: 'mdi:sun-azimuth',
},
{
href: 'https://mp.2ha.me',
label: 'MoviePilot',
title: '媒体订阅工具',
icon: 'mdi:youtube-creator-studio',
href: 'https://mmwdemo.2ha.me/docs',
label: '妙妙屋',
title: '个人Clash订阅管理工具',
icon: '/static/mmw.svg',
},
{
href: 'https://g.2ha.me',
label: 'GHProxy',
title: 'GitHub代理',
href: 'https://1ms.cc',
label: 'hubproxy',
title: 'GitHub&DockerHub代理',
icon: 'mdi:rocket-launch-outline',
},
]

View File

@@ -11,6 +11,7 @@ const blog = defineCollection({
image: image().optional(),
tags: z.array(z.string()).optional(),
authors: z.array(z.string()).optional(),
order: z.number().optional(),
draft: z.boolean().optional(),
hidden: z.boolean().optional(),
parentTitle: z.string().optional(),
@@ -66,6 +67,9 @@ const projects = defineCollection({
tags: z.array(z.string()),
image: image(),
link: z.string().url(),
startDate: z.coerce.date().optional(),
endDate: z.coerce.date().optional(),
order: z.number().optional(),
}),
})

View File

@@ -15,15 +15,15 @@ debian默认软件库的nginx没有fancy-index模块, fancy-index是一个html
sudo apt install -y build-essential libpcre3 libpcre3-dev zlib1g-dev openssl libssl-dev
```
## 2. 下载并解压nginx源码
官网查看最新版本(当前20250111为1.26.3)
官网查看最新版本(当前20251111为1.29.3)
https://nginx.org/en/download.html
```shellscript title="shell"
wget https://nginx.org/download/nginx-1.26.3.tar.gz
tar -xf nginx-1.26.3.tar.gz
wget https://nginx.org/download/nginx-1.29.3.tar.gz
tar -xf nginx-1.29.3.tar.gz
```
## 3. 下载并解压nginx-fancyindex模块
```shellscript title="shell"
cd nginx-1.26.3
cd nginx-1.29.3
wget https://github.com/aperezdc/ngx-fancyindex/releases/download/v0.5.2/ngx-fancyindex-0.5.2.tar.xz
tar -xf ngx-fancyindex-0.5.2.tar.xz
```
@@ -34,11 +34,15 @@ mkdir -p /var/cache/nginx
mkdir -p /var/log/nginx
```
NGINX_ROOT_PATH = nginx的安装目录, 需要替换成你想要安装的目录
```
export NGINX_ROOT_PATH=/usr/local/nginx
```
```shellscript
./configure --add-module=./ngx-fancyindex-0.5.2 \
--prefix=${NGINX_ROOT_PATH} \
--user=jimlee \
--group=jimlee \
--user=$(whoami) \
--group=$(id -Gn) \
--sbin-path=${NGINX_ROOT_PATH}/sbin/nginx \
--conf-path=${NGINX_ROOT_PATH}/nginx.conf \
--error-log-path=/var/log/nginx/error.log \

9
src/content/project.mdx vendored Normal file
View File

@@ -0,0 +1,9 @@
---
---
<h3 class="not-prose text-lg font-medium mb-1">
Some work I&rsquo;ve done <span class="text-muted-foreground">ヽ(o^ ^o)ノ</span>
</h3>
<span class="not-prose text-muted-foreground text-xs">
Last updated: 2025-08-11
</span>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1013 KiB

View File

@@ -0,0 +1,8 @@
---
name: '妙妙屋(个人clash订阅管理工具)'
description: '妙妙屋是一个功能强大的Clash订阅管理平台帮助您轻松管理订阅、节点和用户。'
tags: ['open-source', 'personal', 'clash', 'substore']
image: 'assets/mmw.png'
link: 'https://mmwdemo.2ha.me'
startDate: '2025-10-10'
---

View File

@@ -14,14 +14,14 @@ const { title, description, image } = Astro.props
---
<!doctype html>
<html lang="zh">
<html lang="zh" class="dark">
<head>
<Head
title={`${title} | ${SITE.TITLE}`}
description={description}
image={image}
/>
<script defer src="https://in.2ha.me/script.js" data-website-id="34634aec-34a9-4ef4-9a8f-08ee96699a84"></script>
<script is:inline defer src="https://in.2ha.me/script.js" data-website-id="34634aec-34a9-4ef4-9a8f-08ee96699a84"></script>
</head>
<body>
<div

325
src/lib/data-utils.ts Normal file
View File

@@ -0,0 +1,325 @@
import { getCollection, render, type CollectionEntry } from 'astro:content'
import { readingTime, calculateWordCountFromHtml } from '@/lib/utils'
export async function getAllAuthors(): Promise<CollectionEntry<'authors'>[]> {
return await getCollection('authors')
}
export async function getAllPosts(): Promise<CollectionEntry<'blog'>[]> {
const posts = await getCollection('blog')
return posts
.filter((post) => !post.data.draft && !isSubpost(post.id))
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
}
export async function getAllPostsAndSubposts(): Promise<
CollectionEntry<'blog'>[]
> {
const posts = await getCollection('blog')
return posts
.filter((post) => !post.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
}
export async function getAllTags(): Promise<Map<string, number>> {
const posts = await getAllPosts()
return posts.reduce((acc, post) => {
post.data.tags?.forEach((tag) => {
acc.set(tag, (acc.get(tag) || 0) + 1)
})
return acc
}, new Map<string, number>())
}
export async function getAllProjects(): Promise<CollectionEntry<'projects'>[]> {
const projects = await getCollection('projects')
return projects.sort((a, b) => {
const orderA = a.data.order ?? 0
const orderB = b.data.order ?? 0
if (orderA !== orderB) {
return orderA - orderB
}
const dateA = a.data.startDate?.getTime() || 0
const dateB = b.data.startDate?.getTime() || 0
return dateB - dateA
})
}
export async function getAdjacentPosts(currentId: string): Promise<{
newer: CollectionEntry<'blog'> | null
older: CollectionEntry<'blog'> | null
parent: CollectionEntry<'blog'> | null
}> {
const allPosts = await getAllPosts()
if (isSubpost(currentId)) {
const parentId = getParentId(currentId)
const allPosts = await getAllPosts()
const parent = allPosts.find((post) => post.id === parentId) || null
const posts = await getCollection('blog')
const subposts = posts
.filter(
(post) =>
isSubpost(post.id) &&
getParentId(post.id) === parentId &&
!post.data.draft,
)
.sort((a, b) => {
const dateDiff = a.data.date.valueOf() - b.data.date.valueOf()
if (dateDiff !== 0) return dateDiff
const orderA = a.data.order ?? 0
const orderB = b.data.order ?? 0
return orderA - orderB
})
const currentIndex = subposts.findIndex((post) => post.id === currentId)
if (currentIndex === -1) {
return { newer: null, older: null, parent }
}
return {
newer:
currentIndex < subposts.length - 1 ? subposts[currentIndex + 1] : null,
older: currentIndex > 0 ? subposts[currentIndex - 1] : null,
parent,
}
}
const parentPosts = allPosts.filter((post) => !isSubpost(post.id))
const currentIndex = parentPosts.findIndex((post) => post.id === currentId)
if (currentIndex === -1) {
return { newer: null, older: null, parent: null }
}
return {
newer: currentIndex > 0 ? parentPosts[currentIndex - 1] : null,
older:
currentIndex < parentPosts.length - 1
? parentPosts[currentIndex + 1]
: null,
parent: null,
}
}
export async function getPostsByAuthor(
authorId: string,
): Promise<CollectionEntry<'blog'>[]> {
const posts = await getAllPosts()
return posts.filter((post) => post.data.authors?.includes(authorId))
}
export async function getPostsByTag(
tag: string,
): Promise<CollectionEntry<'blog'>[]> {
const posts = await getAllPosts()
return posts.filter((post) => post.data.tags?.includes(tag))
}
export async function getRecentPosts(
count: number,
): Promise<CollectionEntry<'blog'>[]> {
const posts = await getAllPosts()
return posts.slice(0, count)
}
export async function getSortedTags(): Promise<
{ tag: string; count: number }[]
> {
const tagCounts = await getAllTags()
return [...tagCounts.entries()]
.map(([tag, count]) => ({ tag, count }))
.sort((a, b) => {
const countDiff = b.count - a.count
return countDiff !== 0 ? countDiff : a.tag.localeCompare(b.tag)
})
}
export function getParentId(subpostId: string): string {
return subpostId.split('/')[0]
}
export async function getSubpostsForParent(
parentId: string,
): Promise<CollectionEntry<'blog'>[]> {
const posts = await getCollection('blog')
return posts
.filter(
(post) =>
!post.data.draft &&
isSubpost(post.id) &&
getParentId(post.id) === parentId,
)
.sort((a, b) => {
const dateDiff = a.data.date.valueOf() - b.data.date.valueOf()
if (dateDiff !== 0) return dateDiff
const orderA = a.data.order ?? 0
const orderB = b.data.order ?? 0
return orderA - orderB
})
}
export function groupPostsByYear(
posts: CollectionEntry<'blog'>[],
): Record<string, CollectionEntry<'blog'>[]> {
return posts.reduce(
(acc: Record<string, CollectionEntry<'blog'>[]>, post) => {
const year = post.data.date.getFullYear().toString()
;(acc[year] ??= []).push(post)
return acc
},
{},
)
}
export function groupProjectsByYear(
projects: CollectionEntry<'projects'>[],
): Record<string, CollectionEntry<'projects'>[]> {
return projects.reduce(
(acc: Record<string, CollectionEntry<'projects'>[]>, project) => {
// Use startDate for grouping, fallback to current year if no date
const year = project.data.startDate
? project.data.startDate.getFullYear().toString()
: new Date().getFullYear().toString()
;(acc[year] ??= []).push(project)
return acc
},
{},
)
}
export async function hasSubposts(postId: string): Promise<boolean> {
const subposts = await getSubpostsForParent(postId)
return subposts.length > 0
}
export function isSubpost(postId: string): boolean {
return postId.includes('/')
}
export async function getParentPost(
subpostId: string,
): Promise<CollectionEntry<'blog'> | null> {
if (!isSubpost(subpostId)) {
return null
}
const parentId = getParentId(subpostId)
const allPosts = await getAllPosts()
return allPosts.find((post) => post.id === parentId) || null
}
export async function parseAuthors(authorIds: string[] = []) {
if (!authorIds.length) return []
const allAuthors = await getAllAuthors()
const authorMap = new Map(allAuthors.map((author) => [author.id, author]))
return authorIds.map((id) => {
const author = authorMap.get(id)
return {
id,
name: author?.data?.name || id,
avatar: author?.data?.avatar || '/static/logo.png',
isRegistered: !!author,
}
})
}
export async function getPostById(
postId: string,
): Promise<CollectionEntry<'blog'> | null> {
const allPosts = await getAllPostsAndSubposts()
return allPosts.find((post) => post.id === postId) || null
}
export async function getSubpostCount(parentId: string): Promise<number> {
const subposts = await getSubpostsForParent(parentId)
return subposts.length
}
export async function getCombinedReadingTime(postId: string): Promise<string> {
const post = await getPostById(postId)
if (!post) return readingTime(0)
let totalWords = calculateWordCountFromHtml(post.body)
if (!isSubpost(postId)) {
const subposts = await getSubpostsForParent(postId)
for (const subpost of subposts) {
totalWords += calculateWordCountFromHtml(subpost.body)
}
}
return readingTime(totalWords)
}
export async function getPostReadingTime(postId: string): Promise<string> {
const post = await getPostById(postId)
if (!post) return readingTime(0)
const wordCount = calculateWordCountFromHtml(post.body)
return readingTime(wordCount)
}
export type TOCHeading = {
slug: string
text: string
depth: number
isSubpostTitle?: boolean
}
export type TOCSection = {
type: 'parent' | 'subpost'
title: string
headings: TOCHeading[]
subpostId?: string
}
export async function getTOCSections(postId: string): Promise<TOCSection[]> {
const post = await getPostById(postId)
if (!post) return []
const parentId = isSubpost(postId) ? getParentId(postId) : postId
const parentPost = isSubpost(postId) ? await getPostById(parentId) : post
if (!parentPost) return []
const sections: TOCSection[] = []
const { headings: parentHeadings } = await render(parentPost)
if (parentHeadings.length > 0) {
sections.push({
type: 'parent',
title: 'Overview',
headings: parentHeadings.map((heading) => ({
slug: heading.slug,
text: heading.text,
depth: heading.depth,
})),
})
}
const subposts = await getSubpostsForParent(parentId)
for (const subpost of subposts) {
const { headings: subpostHeadings } = await render(subpost)
if (subpostHeadings.length > 0) {
sections.push({
type: 'subpost',
title: subpost.data.title,
headings: subpostHeadings.map((heading, index) => ({
slug: heading.slug,
text: heading.text,
depth: heading.depth,
isSubpostTitle: index === 0,
})),
subpostId: subpost.id,
})
}
}
return sections
}

View File

@@ -13,13 +13,36 @@ export function formatDate(date: Date) {
}).format(date)
}
export function readingTime(html: string) {
export function formatMonthYear(date: Date) {
return Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
}).format(date)
}
export function calculateWordCountFromHtml(
html: string | null | undefined,
): number {
if (!html) return 0
const textOnly = html.replace(/<[^>]+>/g, '')
const wordCount = textOnly.split(/\s+/).length
const readingTimeMinutes = (wordCount / 200 + 1).toFixed()
return textOnly.split(/\s+/).filter(Boolean).length
}
export function readingTime(wordCount: number): string {
const readingTimeMinutes = Math.max(1, Math.round(wordCount / 200))
return `${readingTimeMinutes} min read`
}
export function getHeadingMargin(depth: number): string {
const margins: Record<number, string> = {
3: 'ml-4',
4: 'ml-8',
5: 'ml-12',
6: 'ml-16',
}
return margins[depth] || ''
}
export function getElapsedTime(unixTimestamp: number): string {
const createdAt = new Date(unixTimestamp)
const now = new Date()
@@ -33,3 +56,12 @@ export function getElapsedTime(unixTimestamp: number): string {
.toString()
.padStart(2, '0')}:${seconds.toString().padStart(2, '0')} elapsed`
}
export function extractDomain(url: string): string {
try {
const domain = new URL(url).hostname
return domain.startsWith('www.') ? domain.slice(4) : domain
} catch {
return url
}
}

View File

@@ -61,7 +61,8 @@ const subposts = allPosts.filter((p) => p.data.parentTitle === post.data.title)
const totalBody = [post.body!, ...subposts.map((p) => p.body!)]
.map(stripCodeBlocks)
.join('')
const readTime = readingTime(totalBody)
const wordCount = totalBody.split(/\s+/).filter(Boolean).length
const readTime = readingTime(wordCount)
---
<Layout

View File

@@ -57,35 +57,24 @@ const sortedTags = tagKeys.sort((a, b) => tagCounts[b] - tagCounts[a])
---
<Layout title="Blog" description="Blog">
<Container class="flex grow flex-col gap-y-6">
<Breadcrumbs
items={[
{ label: 'Blog', href: '/blog', icon: 'lucide:archive' },
{ label: `Page ${page.currentPage}`, icon: 'lucide:folder-open' },
]}
/>
<div
class="sm:none hidden h-full max-h-screen min-w-[280px] max-w-[280px] flex-wrap overflow-auto rounded-sm bg-gray-50 pt-5 shadow-md lg:contents dark:bg-gray-900/70 dark:shadow-gray-800/40"
>
<div
class="absolute left-4 top-[50%] px-4 py-2"
style="transform: translateY(-50%);"
>
<!-- Fixed Sidebar -->
<aside
class="hidden xl:block fixed left-4 top-32 w-[240px] px-4 py-3 rounded-sm bg-background/80 backdrop-blur-sm border-2 border-[color:rgba(241,140,110,0.22)] shadow-[4px_4px_0_rgba(0,0,0,0.22)] dark:shadow-[4px_4px_0_rgba(0,0,0,0.65)] z-10"
>
<Link
href={`/blog`}
class="hover:text-primary-500 dark:hover:text-primary-500 font-bold uppercase"
class="block mb-2 font-bold uppercase text-primary hover:text-primary/80 transition-colors"
>
All Posts
</Link>
<ul>
<ul class="space-y-0.5">
{
sortedTags.map((t) => {
return (
<li value={t} class="my-0">
<li value={t}>
<Link
href={`/tags/${slug(t)}`}
class="hover:text-primary-500 dark:hover:text-primary-500 px-3 py-2 text-sm font-medium uppercase text-gray-400 dark:text-gray-300"
class="block px-2 py-1.5 text-sm font-medium uppercase text-foreground/60 hover:text-primary hover:bg-primary/10 rounded transition-colors"
aria-label={`View posts tagged ${t}`}
>
{`${t} (${tagCounts[t]})`}
@@ -95,22 +84,25 @@ const sortedTags = tagKeys.sort((a, b) => tagCounts[b] - tagCounts[a])
})
}
</ul>
<span
class="ms-auto inline-flex h-6 items-center text-base sm:text-end"
<div class="mt-2 pt-2 border-t border-[color:rgba(241,140,110,0.15)]">
<a
aria-label="View all blog categories"
class="text-sm font-medium text-primary hover:text-primary/80 transition-colors"
href="/tags/"
>
<a
aria-label="View all blog categories"
class="sm:hover:text-accent-two font-medium text-accent"
style="color: #e9d3b6; font-size: 12px;"
href="/tags/"
>
View all →
</a>
</span>
View all →
</a>
</div>
</div>
</aside>
<div class="flex min-h-[calc(100vh-18rem)] flex-col gap-y-8">
<Container class="flex grow flex-col gap-y-6">
<Breadcrumbs
items={[
{ label: 'Blog', href: '/blog', icon: 'lucide:archive' },
{ label: `Page ${page.currentPage}`, icon: 'lucide:folder-open' },
]}
/>
<div class="flex min-h-[calc(100vh-18rem)] flex-col gap-y-8">
{
years.map((year) => (
<section class="flex flex-col gap-y-4">
@@ -125,13 +117,13 @@ const sortedTags = tagKeys.sort((a, b) => tagCounts[b] - tagCounts[a])
</section>
))
}
</div>
</div>
<PaginationComponent
<PaginationComponent
currentPage={page.currentPage}
totalPages={page.lastPage}
baseUrl="/blog/"
client:load
/>
/>
</Container>
</Layout>

View File

@@ -28,11 +28,11 @@ const latestPost = await getCollection('blog').then((posts: any[]) =>
<Layout title="主页" description={SITE.DESCRIPTION}>
<section
class="mx-auto grid max-w-[375px] grid-cols-2 gap-4 px-4 [grid-template-areas:'a_a'_'a_a'_'b_b'_'b_b'_'e_e'_'h_i'_'h_c'_'k_c'_'d_d'_'d_d'_'g_g'_'g_g'_'f_f'_'j_j'_'j_j'] *:rounded-3xl *:border *:bg-secondary/25 *:bg-cover *:bg-center *:bg-no-repeat sm:max-w-screen-sm sm:[grid-template-areas:'a_a'_'b_d'_'e_e'_'j_g'_'h_i'_'h_c'_'k_c'_'f_f'] xl:max-w-screen-xl xl:grid-cols-4 xl:[grid-template-areas:'a_a_b_c'_'d_e_e_c'_'h_f_f_g'_'h_i_j_k'] xl:[&:hover:has(>.has-overlay:hover)>.first>.overlay]:opacity-100 xl:[&:hover>*:not(.first):hover_.overlay]:opacity-100"
class="mx-auto grid w-full max-w-[300px] grid-cols-1 gap-4 [grid-template-areas:'a'_'b'_'d'_'i'_'e'_'g'_'f'_'j'_'k'] [grid-template-rows:repeat(9,300px)] *:border-2 *:border-[color-mix(in_srgb,hsl(var(--primary))_22%,hsl(var(--border)))] *:bg-secondary/25 *:bg-cover *:bg-center *:bg-no-repeat *:overflow-hidden *:max-h-[300px] *:max-w-[300px] *:justify-self-center *:[box-shadow:4px_4px_0_rgba(0,0,0,0.22)] *:transition-all *:duration-200 sm:max-w-screen-sm sm:grid-cols-2 sm:px-4 sm:[grid-template-areas:'a_a'_'b_d'_'e_e'_'j_g'_'f_f'_'i_k'] sm:[grid-template-rows:repeat(6,300px)] sm:*:max-w-[615px] xl:max-w-screen-xl xl:grid-cols-4 xl:px-4 xl:[grid-template-areas:'a_a_b_g'_'d_e_e_i'_'k_j_f_f'] xl:[grid-template-rows:repeat(3,300px)] xl:*:max-w-none dark:*:[box-shadow:4px_4px_0_rgba(0,0,0,0.65)] xl:[&:hover:has(>.has-overlay:hover)>.first>.overlay]:opacity-100 xl:[&>*:not(.first):hover_.overlay]:opacity-100"
aria-label="Personal information and activity grid"
>
<div
class="first flex flex-row xl:max-h-[298px] grow-[0] justify-center aspect-square rounded-3xl bg-[url('/static/loading.gif')] border bg-cover bg-center bg-position-inherit bg-no-repeat [grid-area:a] sm:aspect-[2.1/1] xl:aspect-auto"
class="first flex flex-row xl:max-h-[298px] xl:min-w-[615px] grow-[0] justify-center aspect-square bg-[url('/static/loading.gif')] border bg-cover bg-center bg-position-inherit bg-no-repeat [grid-area:a] sm:aspect-[2.1/1] xl:aspect-auto hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]"
role="img"
aria-label="Introduction"
@@ -42,37 +42,24 @@ const latestPost = await getCollection('blog').then((posts: any[]) =>
<div
class="has-overlay relative grid aspect-square grid-cols-4 grid-rows-3 items-center
justify-center bg-[url('/static/honeycomb.webp')] [grid-area:b] short-cuts-template"
justify-center bg-[url('/static/honeycomb.webp')] [grid-area:b] short-cuts-template hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]"
style="grid-template-columns: 2.85fr 2.95fr 2.9fr 1.3fr;grid-template-rows: 4.7fr 3.4fr 2.8fr;"
aria-label="Developer Stack Shortcuts"
>
<img
class="overlay absolute no-repeat w-full max-w-fit justify-center rounded-3xl object-cover z-9"
class="overlay absolute no-repeat w-full max-w-fit justify-center object-cover z-9"
src="/static/images/shortcuts-bg.png"
/>
<!-- <DevStackIconsCloud client:load/> -->
<DevShortCuts />
</div>
<div class="aspect-[1/2.1] [grid-area:c] bg-[url('/static/images/ump45.png')] xl:aspect-auto bg-color" aria-hidden="true">
<!-- <MagnetLines
client:load
rows={9}
columns={4}
containerSize="100%"
lineColor="#e9d3b6"
lineWidth="0.8vmin"
lineHeight="4vmin"
baseAngle={0}
/> -->
</div>
<div class="relative overflow-hidden [grid-area:d] sm:aspect-square min-h-[300px]">
<div class="relative overflow-hidden [grid-area:d] sm:aspect-square min-h-[300px] min-w-[300px] hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]">
<AuthorPresence client:load />
</div>
<div
class="has-overlay min-h-[300px] relative flex grid aspect-[6/5] grid-rows-2 items-start overflow-hidden p-1 [grid-area:e] sm:aspect-[2.1/1] sm:items-center xl:aspect-auto"
class="has-overlay min-h-[300px] relative flex grid aspect-[6/5] grid-rows-2 items-start overflow-hidden p-1 [grid-area:e] sm:aspect-[2.1/1] sm:items-center xl:aspect-auto hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]"
style="grid-template-rows: 9fr 1fr; "
>
<div
@@ -89,7 +76,7 @@ const latestPost = await getCollection('blog').then((posts: any[]) =>
alt={`Featured image for the latest post: ${latestPost.data.title}`}
width={477}
height={251}
class="w-full rounded-2xl border border-border sm:w-[82%] "
class="w-full border-2 border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] sm:w-[82%]"
/>
</Link>
@@ -109,7 +96,7 @@ const latestPost = await getCollection('blog').then((posts: any[]) =>
aria-label={`Read latest blog post: ${latestPost.data.title}`}
title={`Read latest blog post: ${latestPost.data.title}`}
>
<div class="absolute top-0 right-0 m-3 flex w-fit items-end rounded-full border bg-secondary/50 p-3 text-primary transition-all duration-300 hover:rotate-12 hover:ring-1 hover:ring-primary">
<div class="absolute top-0 right-0 m-3 flex w-fit items-end rounded-full border bg-secondary/50 p-3 text-primary transition-all duration-300 hover:rotate-12 hover:ring-1 hover:ring-primary z-10">
<Icon name="lucide:move-up-right" size={16} />
</div>
</Link>
@@ -119,36 +106,30 @@ const latestPost = await getCollection('blog').then((posts: any[]) =>
</div>
<div
class="has-overlay relative flex aspect-square items-center justify-center overflow-hidden [grid-area:f] sm:aspect-[2.1/1] xl:aspect-auto"
class="has-overlay xl:min-w-[615px] relative flex aspect-square items-center justify-center overflow-hidden [grid-area:f] sm:aspect-[2.1/1] xl:aspect-auto hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]"
>
<div
class="overlay absolute inset-0 z-[1] size-full rounded-3xl bg-[url('/static/images/contributions-square.png')] bg-cover bg-center bg-no-repeat transition-opacity duration-200 sm:bg-[url('/static/images/contributions.png')] xl:opacity-100"
class="overlay absolute inset-0 z-[1] size-full bg-[url('/static/images/contributions-square.png')] bg-cover bg-center bg-no-repeat transition-opacity duration-200 sm:bg-[url('/static/images/contributions.png')] xl:opacity-100"
>
</div>
<GiteaCalendar client:load />
</div>
<div
class="has-overlay relative aspect-square [grid-area:g] hover:bg-none"
class="has-overlay relative aspect-square [grid-area:g] hover:bg-none hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]"
>
<div
class="overlay absolute inset-0 z-0 size-full rounded-3xl bg-[url('/static/images/music163.png')] bg-cover bg-center bg-no-repeat transition-opacity duration-200 xl:opacity-100"
class="overlay absolute inset-0 z-0 size-full bg-[url('/static/images/music163.png')] bg-cover bg-center bg-no-repeat transition-opacity duration-200 xl:opacity-100"
>
</div>
<Music163Player client:load />
</div>
<div
class="aspect-[1/2.1] bg-[url('/static/images/ump9.png')] [grid-area:h] xl:aspect-auto bg-color"
aria-hidden="true"
>
</div>
<div
class="has-overlay relative flex aspect-square items-center justify-center [grid-area:i] "
class="has-overlay relative flex aspect-square items-center justify-center [grid-area:i] hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]"
>
<div
class="overlay absolute inset-0 size-full rounded-3xl bg-[url('/static/images/github.png')] bg-cover bg-center bg-no-repeat transition-opacity duration-200 xl:opacity-100"
class="overlay absolute inset-0 size-full bg-[url('/static/images/github.png')] bg-cover bg-center bg-no-repeat transition-opacity duration-200 xl:opacity-100"
>
</div>
<Icon
@@ -170,12 +151,12 @@ const latestPost = await getCollection('blog').then((posts: any[]) =>
</Link>
</div>
<div class="has-overlay aspect-square [grid-area:j] bg-[url('/static/images/waketime.png')] bg-cover bg-center bg-no-repeat ">
<div class="has-overlay aspect-square [grid-area:j] bg-[url('/static/images/waketime.png')] bg-cover bg-center bg-no-repeat hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]">
<WakatimeGraph omitLanguages={['Markdown', 'JSON']} client:load />
</div>
<!-- 字符滚动 -->
<div class="aspect-square [grid-area:k]">
<div class="aspect-square [grid-area:k] hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]">
<LetterGlitch
client:load
glitchColors={['#de17a5', '#5617de', '#e9d3b6']}

39
src/pages/projects.astro Normal file
View File

@@ -0,0 +1,39 @@
---
import Breadcrumbs from '@/components/Breadcrumbs.astro'
import Container from '@/components/Container.astro'
import PageHead from '@/components/PageHead.astro'
import ProjectCard from '@/components/ProjectCard.astro'
import Layout from '@/layouts/Layout.astro'
import { getAllProjects, groupProjectsByYear } from '@/lib/data-utils'
const projects = await getAllProjects()
const projectsByYear = groupProjectsByYear(projects)
const years = Object.keys(projectsByYear).sort(
(a, b) => parseInt(b) - parseInt(a),
)
---
<Layout title="项目" description="dev.2ha.me的项目">
<PageHead slot="head" title="Project" />
<Container class="flex grow flex-col gap-y-6">
<Breadcrumbs items={[{ label: 'Project', icon: 'lucide:briefcase' }]} />
<div class="flex min-h-[calc(100vh-18rem)] flex-col gap-y-8">
{
years.map((year) => (
<section class="flex flex-col gap-y-4">
<div class="font-semibold">{year}</div>
<ul class="not-prose flex flex-col gap-4">
{projectsByYear[year].map((project) => (
<li>
<ProjectCard project={project} />
</li>
))}
</ul>
</section>
))
}
</div>
</Container>
</Layout>

View File

@@ -1,3 +1,5 @@
@import './pixel-components.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
@@ -10,6 +12,16 @@
/* unicode-range: U+2E80-2EFF,U+3400-4DBF,U+4E00-9FFF; */
}
@font-face {
font-family: 'OPlusSans3-Medium';
src: url('/fonts/OPlusSans3-Medium.woff2') format('woff2'),
url('/fonts/OPlusSans3-Medium.woff') format('woff');
font-weight: 500;
font-style: normal;
font-display: swap;
/* unicode-range: U+2E80-2EFF,U+3400-4DBF,U+4E00-9FFF; */
}
@font-face {
font-family: 'JetBrains Mono';
src: url('/fonts/JetBrainsMono[wght].woff2') format('woff2-variations');
@@ -32,100 +44,117 @@
@layer base {
:root {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--primary: 34.12deg 53.68% 81.37%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
/* MiaoMiaoWu Brand Colors */
--brand-50: #fef5f2;
--brand-100: #fde8e2;
--brand-200: #fbd4c9;
--brand-300: #f7b5a3;
--brand-400: #f18c6e;
--brand-500: #d97757;
--brand-600: #c55438;
--brand-700: #a4432d;
--brand-800: #873829;
--brand-900: #713128;
/* Semantic Colors - Light Mode Default */
--background: 60 20% 99%;
--foreground: 16 62% 15%;
--primary: 15 66% 59%; /* #d97757 橙色 */
--primary-foreground: 33 100% 99%;
--secondary: 22 64% 93%;
--secondary-foreground: 15 60% 27%;
--muted: 30 43% 94%;
--muted-foreground: 15 25% 51%;
--accent: 15 62% 78%; /* #f7b5a3 */
--accent-foreground: 16 73% 11%;
--additive: 112 50% 36%;
--additive-foreground: 0 0% 9%;
--destructive: 0 62.8% 30.6%;
--destructive: 0 72% 51%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--border: 15 30% 55% / 0.38;
--ring: 15 66% 59% / 0.6;
--input: 15 30% 55% / 0.45;
--radius: 0; /* 无圆角! */
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--chart-1: 15 66% 59%;
--chart-2: 15 71% 63%;
--chart-3: 45 97% 63%;
--chart-4: 203 92% 70%;
--chart-5: 186 78% 54%;
--chart-6: 33 12% 33%;
--chart-7: 32 12% 25%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--input: 240 3.7% 15.9%;
--card: 60 20% 99%;
--card-foreground: 16 68% 13%;
--popover: 60 20% 99%;
--popover-foreground: 16 62% 14%;
}
.light {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
/* Same as :root for light mode */
--background: 60 20% 99%;
--foreground: 16 62% 15%;
--primary: 15 66% 59%;
--primary-foreground: 33 100% 99%;
--secondary: 22 64% 93%;
--secondary-foreground: 15 60% 27%;
--muted: 30 43% 94%;
--muted-foreground: 15 25% 51%;
--accent: 15 62% 78%;
--accent-foreground: 16 73% 11%;
--additive: 112 50% 36%;
--additive-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive: 0 72% 51%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--border: 15 30% 55% / 0.38;
--ring: 15 66% 59% / 0.6;
--input: 15 30% 55% / 0.45;
--radius: 0;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--chart-1: 15 66% 59%;
--chart-2: 15 71% 63%;
--chart-3: 45 97% 63%;
--chart-4: 203 92% 70%;
--chart-5: 186 78% 54%;
--chart-6: 33 12% 33%;
--chart-7: 32 12% 25%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--input: 240 5.9% 90%;
--radius: 0.5rem;
--card: 60 20% 99%;
--card-foreground: 16 68% 13%;
--popover: 60 20% 99%;
--popover-foreground: 16 62% 14%;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
/* Dark Mode - MiaoMiaoWu Style */
--background: 222 29% 8%; /* #10131c 深蓝灰 */
--foreground: 33 83% 97%; /* #f9f4f1 暖白 */
--primary: 15 71% 69%; /* #f18c6e 亮橙色 */
--primary-foreground: 16 65% 13%; /* #30160f 深色 */
--secondary: 225 23% 13%; /* #1d2232 深蓝灰 */
--secondary-foreground: 22 64% 93%;
--muted: 224 24% 12%; /* #1c2131 深蓝 */
--muted-foreground: 15 25% 75%; /* #cfb8af 浅灰棕 */
--accent: 15 71% 69% / 0.22; /* 半透明橙 */
--accent-foreground: 22 100% 93%; /* #ffe5da */
--additive: 112 50% 36%;
--additive-foreground: 0 0% 9%;
--destructive: 0 62.8% 30.6%;
--destructive: 0 70% 68%; /* #f87171 亮红 */
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--border: 0 0% 100% / 0.08; /* 半透明白 */
--ring: 15 71% 69% / 0.45;
--input: 0 0% 100% / 0.12;
--radius: 0;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--chart-1: 15 71% 69%;
--chart-2: 199 89% 61%;
--chart-3: 271 70% 67%;
--chart-4: 45 98% 54%;
--chart-5: 186 78% 54%;
--chart-6: 33 12% 33%;
--chart-7: 32 12% 25%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--input: 240 3.7% 15.9%;
--card: 222 29% 8%;
--card-foreground: 22 75% 94%;
--popover: 222 29% 8%;
--popover-foreground: 33 83% 97%;
}
*,
@@ -136,6 +165,7 @@
html {
color-scheme: dark;
overflow-y: scroll;
@apply bg-background text-foreground forced-color-adjust-none;
::-webkit-scrollbar-corner {
@@ -455,8 +485,37 @@
* {
@apply border-border;
}
/* Prevent text selection cursor on interactive elements */
button,
a,
.pixel-button,
[role="button"],
[role="link"] {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
body {
@apply bg-background text-foreground;
@apply text-foreground;
/* MiaoMiaoWu Background Gradients */
background-image:
radial-gradient(1100px circle at 5% -10%, rgba(217, 119, 87, 0.2), transparent 60%),
radial-gradient(900px circle at 90% 0%, rgba(96, 165, 250, 0.12), transparent 65%),
linear-gradient(180deg, rgba(255, 247, 242, 0.98), rgba(255, 247, 242, 1));
background-attachment: fixed;
background-size: cover;
background-color: hsl(var(--background));
}
.dark body {
background-image:
radial-gradient(900px circle at 15% -5%, rgba(241, 140, 110, 0.32), transparent 65%),
radial-gradient(800px circle at 82% 0%, rgba(56, 189, 248, 0.18), transparent 70%),
linear-gradient(180deg, rgba(16, 19, 28, 1), rgba(10, 12, 20, 1));
background-attachment: fixed;
background-size: cover;
}
}

View File

@@ -0,0 +1,533 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@font-face {
font-family: 'haipaiqiangdiaosenxiyuan';
src: url('/fonts/haipaiqiangdiaosenxiyuan.woff');
font-weight: 100 800;
font-style: normal;
/* unicode-range: U+2E80-2EFF,U+3400-4DBF,U+4E00-9FFF; */
}
@font-face {
font-family: 'OPlusSans3-Medium';
src: url('/fonts/OPlusSans3-Medium.woff');
font-weight: 100 800;
font-style: normal;
/* unicode-range: U+2E80-2EFF,U+3400-4DBF,U+4E00-9FFF; */
}
@font-face {
font-family: 'JetBrains Mono';
src: url('/fonts/JetBrainsMono[wght].woff2') format('woff2-variations');
font-weight: 100 800;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'JetBrains Mono';
src: url('/fonts/JetBrainsMono-Italic[wght].woff2') format('woff2-variations');
font-weight: 100 800;
font-style: italic;
font-display: swap;
}
.styles-module_calendar__sT1ND text {
fill: currentColor;
}
@layer base {
:root {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--primary: 34.12deg 53.68% 81.37%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--additive: 112 50% 36%;
--additive-foreground: 0 0% 9%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--chart-6: 33 12% 33%;
--chart-7: 32 12% 25%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--input: 240 3.7% 15.9%;
}
.light {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--additive: 112 50% 36%;
--additive-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--chart-6: 33 12% 33%;
--chart-7: 32 12% 25%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--input: 240 5.9% 90%;
--radius: 0.5rem;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--additive: 112 50% 36%;
--additive-foreground: 0 0% 9%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--chart-6: 33 12% 33%;
--chart-7: 32 12% 25%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--input: 240 3.7% 15.9%;
}
*,
*::before,
*::after {
@apply border-border;
}
html {
color-scheme: dark;
@apply bg-background text-foreground forced-color-adjust-none;
::-webkit-scrollbar-corner {
@apply bg-transparent;
}
}
.disable-transitions,
.disable-transitions * {
@apply !transition-none;
}
.theme {
--animate-gradient: gradient 8s linear infinite;
}
}
.svg-bg-color {
background-color: hsl(var(--primary));
}
:root,
::backdrop {
--sl-color-white: hsl(0, 0%, 100%);
--sl-color-gray-1: hsl(224, 20%, 94%);
--sl-color-gray-2: hsl(224, 6%, 77%);
--sl-color-gray-3: hsl(224, 6%, 56%);
--sl-color-gray-4: hsl(224, 7%, 36%);
--sl-color-gray-5: hsl(224, 10%, 23%);
--sl-color-gray-6: hsl(224, 14%, 16%);
--sl-color-black: hsl(224, 10%, 10%);
--sl-hue-orange: 41;
--sl-color-orange-low: hsl(var(--sl-hue-orange), 39%, 22%);
--sl-color-orange: hsl(var(--sl-hue-orange), 82%, 63%);
--sl-color-orange-high: hsl(var(--sl-hue-orange), 82%, 87%);
--sl-hue-green: 101;
--sl-color-green-low: hsl(var(--sl-hue-green), 39%, 22%);
--sl-color-green: hsl(var(--sl-hue-green), 82%, 63%);
--sl-color-green-high: hsl(var(--sl-hue-green), 82%, 80%);
--sl-hue-blue: 234;
--sl-color-blue-low: hsl(var(--sl-hue-blue), 54%, 20%);
--sl-color-blue: hsl(var(--sl-hue-blue), 100%, 60%);
--sl-color-blue-high: hsl(var(--sl-hue-blue), 100%, 87%);
--sl-hue-purple: 281;
--sl-color-purple-low: hsl(var(--sl-hue-purple), 39%, 22%);
--sl-color-purple: hsl(var(--sl-hue-purple), 82%, 63%);
--sl-color-purple-high: hsl(var(--sl-hue-purple), 82%, 89%);
--sl-hue-red: 339;
--sl-color-red-low: hsl(var(--sl-hue-red), 39%, 22%);
--sl-color-red: hsl(var(--sl-hue-red), 82%, 63%);
--sl-color-red-high: hsl(var(--sl-hue-red), 82%, 87%);
--sl-color-accent-low: hsl(224, 54%, 20%);
--sl-color-accent: hsl(224, 100%, 60%);
--sl-color-accent-high: hsl(224, 100%, 85%);
--sl-color-text: var(--sl-color-gray-2);
--sl-color-text-accent: var(--sl-color-accent-high);
--sl-color-text-invert: var(--sl-color-accent-low);
--sl-color-bg: var(--sl-color-black);
--sl-color-bg-nav: var(--sl-color-gray-6);
--sl-color-bg-sidebar: var(--sl-color-gray-6);
--sl-color-bg-inline-code: var(--sl-color-gray-5);
--sl-color-bg-accent: var(--sl-color-accent-high);
--sl-color-hairline-light: var(--sl-color-gray-5);
--sl-color-hairline: var(--sl-color-gray-6);
--sl-color-hairline-shade: var(--sl-color-black);
--sl-color-backdrop-overlay: hsla(223, 13%, 10%, 0.66);
--sl-shadow-sm: 0px 1px 1px hsla(0, 0%, 0%, 0.12),
0px 2px 1px hsla(0, 0%, 0%, 0.24);
--sl-shadow-md: 0px 8px 4px hsla(0, 0%, 0%, 0.08),
0px 5px 2px hsla(0, 0%, 0%, 0.08), 0px 3px 2px hsla(0, 0%, 0%, 0.12),
0px 1px 1px hsla(0, 0%, 0%, 0.15);
--sl-shadow-lg: 0px 25px 7px hsla(0, 0%, 0%, 0.03),
0px 16px 6px hsla(0, 0%, 0%, 0.1), 0px 9px 5px hsla(223, 13%, 10%, 0.33),
0px 4px 4px hsla(0, 0%, 0%, 0.75), 0px 4px 2px hsla(0, 0%, 0%, 0.25);
--sl-text-2xs: 0.75rem;
--sl-text-xs: 0.8125rem;
--sl-text-sm: 0.875rem;
--sl-text-base: 1rem;
--sl-text-lg: 1.125rem;
--sl-text-xl: 1.25rem;
--sl-text-2xl: 1.5rem;
--sl-text-3xl: 1.8125rem;
--sl-text-4xl: 2.1875rem;
--sl-text-5xl: 2.625rem;
--sl-text-6xl: 4rem;
--sl-text-body: var(--sl-text-base);
--sl-text-body-sm: var(--sl-text-xs);
--sl-text-code: var(--sl-text-sm);
--sl-text-code-sm: var(--sl-text-xs);
--sl-text-h1: var(--sl-text-4xl);
--sl-text-h2: var(--sl-text-3xl);
--sl-text-h3: var(--sl-text-2xl);
--sl-text-h4: var(--sl-text-xl);
--sl-text-h5: var(--sl-text-lg);
--sl-line-height: 1.75;
--sl-line-height-headings: 1.2;
--sl-font-system: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--sl-font-system-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
'Liberation Mono', 'Courier New', monospace;
--__sl-font: var(--sl-font, var(--sl-font-system)), var(--sl-font-system);
--__sl-font-mono: var(--sl-font-mono, var(--sl-font-system-mono)),
var(--sl-font-system-mono);
--sl-nav-height: 3.5rem;
--sl-nav-pad-x: 1rem;
--sl-nav-pad-y: 0.75rem;
--sl-mobile-toc-height: 3rem;
--sl-sidebar-width: 18.75rem;
--sl-sidebar-pad-x: 1rem;
--sl-content-width: 45rem;
--sl-content-pad-x: 1rem;
--sl-menu-button-size: 2rem;
--sl-nav-gap: var(--sl-content-pad-x);
--sl-outline-offset-inside: -0.1875rem;
--sl-z-index-toc: 4;
--sl-z-index-menu: 5;
--sl-z-index-navbar: 10;
--sl-z-index-skiplink: 20;
}
:root[data-theme='light'],
[data-theme='light'] ::backdrop {
--sl-color-white: hsl(224, 10%, 10%);
--sl-color-gray-1: hsl(224, 14%, 16%);
--sl-color-gray-2: hsl(224, 10%, 23%);
--sl-color-gray-3: hsl(224, 7%, 36%);
--sl-color-gray-4: hsl(224, 6%, 56%);
--sl-color-gray-5: hsl(224, 6%, 77%);
--sl-color-gray-6: hsl(224, 20%, 94%);
--sl-color-gray-7: hsl(224, 19%, 97%);
--sl-color-black: hsl(0, 0%, 100%);
--sl-color-orange-high: hsl(var(--sl-hue-orange), 80%, 25%);
--sl-color-orange: hsl(var(--sl-hue-orange), 90%, 60%);
--sl-color-orange-low: hsl(var(--sl-hue-orange), 90%, 88%);
--sl-color-green-high: hsl(var(--sl-hue-green), 80%, 22%);
--sl-color-green: hsl(var(--sl-hue-green), 90%, 46%);
--sl-color-green-low: hsl(var(--sl-hue-green), 85%, 90%);
--sl-color-blue-high: hsl(var(--sl-hue-blue), 80%, 30%);
--sl-color-blue: hsl(var(--sl-hue-blue), 90%, 60%);
--sl-color-blue-low: hsl(var(--sl-hue-blue), 88%, 90%);
--sl-color-purple-high: hsl(var(--sl-hue-purple), 90%, 30%);
--sl-color-purple: hsl(var(--sl-hue-purple), 90%, 60%);
--sl-color-purple-low: hsl(var(--sl-hue-purple), 80%, 90%);
--sl-color-red-high: hsl(var(--sl-hue-red), 80%, 30%);
--sl-color-red: hsl(var(--sl-hue-red), 90%, 60%);
--sl-color-red-low: hsl(var(--sl-hue-red), 80%, 90%);
--sl-color-accent-high: hsl(234, 80%, 30%);
--sl-color-accent: hsl(234, 90%, 60%);
--sl-color-accent-low: hsl(234, 88%, 90%);
--sl-color-text-accent: var(--sl-color-accent);
--sl-color-text-invert: var(--sl-color-black);
--sl-color-bg-nav: var(--sl-color-gray-7);
--sl-color-bg-sidebar: var(--sl-color-bg);
--sl-color-bg-inline-code: var(--sl-color-gray-6);
--sl-color-bg-accent: var(--sl-color-accent);
--sl-color-hairline-light: var(--sl-color-gray-6);
--sl-color-hairline-shade: var(--sl-color-gray-6);
--sl-color-backdrop-overlay: hsla(225, 9%, 36%, 0.66);
--sl-shadow-sm: 0px 1px 1px hsla(0, 0%, 0%, 0.06),
0px 2px 1px hsla(0, 0%, 0%, 0.06);
--sl-shadow-md: 0px 8px 4px hsla(0, 0%, 0%, 0.03),
0px 5px 2px hsla(0, 0%, 0%, 0.03), 0px 3px 2px hsla(0, 0%, 0%, 0.06),
0px 1px 1px hsla(0, 0%, 0%, 0.06);
--sl-shadow-lg: 0px 25px 7px rgba(0, 0, 0, 0.01),
0px 16px 6px hsla(0, 0%, 0%, 0.03), 0px 9px 5px hsla(223, 13%, 10%, 0.08),
0px 4px 4px hsla(0, 0%, 0%, 0.16), 0px 4px 2px hsla(0, 0%, 0%, 0.04);
}
@layer components {
article {
@apply prose-headings:scroll-mt-20 prose-headings:break-words;
@apply prose-p:break-words;
@apply prose-a:!text-primary prose-a:!decoration-primary/50 prose-a:!underline-offset-[3px] prose-a:transition-colors hover:prose-a:!decoration-inherit;
@apply prose-blockquote:!not-italic prose-blockquote:!text-muted-foreground;
@apply prose-pre:!px-0;
@apply prose-img:mx-auto prose-img:rounded-xl prose-img:border;
@apply prose-table:mx-auto prose-table:block prose-table:max-w-fit prose-table:overflow-x-auto prose-td:break-words;
@apply sm:prose-table:mx-0 sm:prose-table:table sm:prose-table:max-w-none;
/* Fixing Katex display */
.katex-display {
@apply overflow-x-auto overflow-y-hidden py-4;
}
/* Fixing Katex fractions */
.frac-line {
@apply border-foreground;
}
/* Removes background from <mark> elements */
mark {
@apply bg-transparent;
}
/* Blanket syntax highlighting */
code[data-theme*=' '] {
span {
color: var(--shiki-dark);
}
.dark & span {
color: var(--shiki-dark);
}
}
/* Inline code */
:not(pre) > code {
@apply relative rounded bg-muted/50 px-[0.3rem] py-[0.2rem] font-mono text-sm font-medium before:!content-none after:!content-none;
}
/* Code blocks */
figure[data-rehype-pretty-code-figure] {
@apply relative;
/* Code block titles */
[data-rehype-pretty-code-title] {
@apply break-words rounded-t-xl border-x border-t px-4 py-2 text-sm font-medium text-foreground;
/* Remove top margin from code block if a title is present */
& + pre {
@apply mt-0 rounded-t-none;
}
}
/* Shadcn-like scrollbar */
pre::-webkit-scrollbar {
@apply h-2.5 w-2.5;
}
pre::-webkit-scrollbar-track {
@apply bg-transparent;
}
pre::-webkit-scrollbar-thumb {
@apply rounded-full bg-border bg-clip-padding p-px;
}
/* Code block styles */
pre {
@apply static max-h-[600px] overflow-auto rounded-xl border bg-secondary/20 py-4 text-sm leading-loose;
/* Code block content */
> code {
@apply whitespace-pre-wrap;
counter-reset: line;
/* For code blocks with line numbers */
&[data-line-numbers] {
> [data-line]::before {
counter-increment: line;
content: counter(line);
@apply mr-4 inline-block w-4 text-right text-muted-foreground;
}
}
/* For each line in the code block */
> [data-line] {
@apply px-4;
}
/* Highlighted lines */
[data-highlighted-line] {
@apply bg-foreground/10;
}
/* Highlighted characters */
[data-highlighted-chars] > span {
@apply bg-muted-foreground/40 py-[7px];
}
.tab {
@apply relative;
}
.tab::before {
@apply absolute opacity-30;
content: '⇥';
}
/* Skip lines */
.skip {
@apply my-2 bg-foreground/5 text-center text-foreground;
&::before {
content: '' !important;
}
}
/* Diff lines */
.diff {
&.add {
@apply bg-additive/15;
}
&.remove {
@apply bg-destructive/15;
&::before {
content: '-';
counter-increment: none;
}
}
&.highlight {
@apply bg-foreground/10;
}
}
/* Copy button */
> button:has(> span) {
@apply right-1 top-1 m-0 size-8 rounded-lg bg-secondary p-1 backdrop-blur-none transition-all duration-200;
}
}
}
}
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
@keyframes shake {
0% {
transform: translate(1px, 1px) rotate(0deg);
}
10% {
transform: translate(-1px, -2px) rotate(-1deg);
}
20% {
transform: translate(-3px, 0px) rotate(1deg);
}
30% {
transform: translate(3px, 2px) rotate(0deg);
}
40% {
transform: translate(1px, -1px) rotate(1deg);
}
50% {
transform: translate(-1px, 2px) rotate(-1deg);
}
60% {
transform: translate(-3px, 1px) rotate(0deg);
}
70% {
transform: translate(3px, 1px) rotate(-1deg);
}
80% {
transform: translate(-1px, -1px) rotate(1deg);
}
90% {
transform: translate(1px, 2px) rotate(0deg);
}
100% {
transform: translate(1px, -2px) rotate(-1deg);
}
}
@theme inline {
@keyframes gradient {
to {
backgroundposition: var(--bg-size, 300%) 0;
}
}
}
/* patch */
.recharts-cartesian-grid-vertical {
display: none;
}
/*
@media screen and (min-width: 1280px) {
html {
zoom: 0.95;
}
} */
.bg-color {
background-color: #e9d3b6 !important;
}
.shiki-transformer-button-copy {
border: none !important;
background-color: hsl(0deg 0% 0% / 0%) !important;
}

View File

@@ -0,0 +1,252 @@
/* MiaoMiaoWu Pixel Style Components */
/* Pixel Border - 像素边框 */
.pixel-border {
position: relative;
border-width: 2px;
border-style: solid;
border-color: color-mix(in srgb, hsl(var(--primary)) 60%, transparent);
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.18),
inset 0 0 0 1px rgba(255, 255, 255, 0.08);
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
}
.pixel-border:hover {
border-color: color-mix(in srgb, hsl(var(--primary)) 75%, transparent);
box-shadow: 6px 6px 0 rgba(0, 0, 0, 0.24),
inset 0 0 0 1px rgba(255, 255, 255, 0.12);
transform: translateY(-1px);
}
.dark .pixel-border {
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.45),
inset 0 0 0 1px rgba(255, 255, 255, 0.08);
}
.dark .pixel-border:hover {
box-shadow: 6px 6px 0 rgba(0, 0, 0, 0.55),
inset 0 0 0 1px rgba(255, 255, 255, 0.12);
}
/* Pixel Button - 像素按钮 */
.pixel-button {
position: relative;
border-width: 2px;
border-style: solid;
border-color: transparent;
border-radius: 0;
box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.18);
transition:
transform 0.18s ease,
box-shadow 0.18s ease,
background-color 0.18s ease,
border-color 0.18s ease;
}
.pixel-button:hover {
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.22);
transform: translateY(-1px);
}
.pixel-button:active {
box-shadow: 1px 1px 0 rgba(0, 0, 0, 0.16);
transform: translate(0);
}
.dark .pixel-button {
box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.45);
}
.dark .pixel-button:hover {
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.55);
}
.dark .pixel-button:active {
box-shadow: 1px 1px 0 rgba(0, 0, 0, 0.35);
}
/* Pixel Card - 像素卡片 */
.pixel-card {
position: relative;
border-width: 2px;
border-style: solid;
border-color: color-mix(in srgb, hsl(var(--primary)) 22%, hsl(var(--border)));
background: hsl(var(--card));
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.22),
0 0 0 1px rgba(255, 255, 255, 0.04);
overflow: hidden;
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.pixel-card:hover {
transform: translateY(-2px);
border-color: color-mix(in srgb, hsl(var(--primary)) 40%, hsl(var(--border)));
box-shadow: 6px 6px 0 rgba(0, 0, 0, 0.26),
0 0 0 1px rgba(255, 255, 255, 0.06);
}
/* 悬停时显示斜纹背景 */
.pixel-card::before {
content: '';
position: absolute;
inset: 0;
background-image: repeating-linear-gradient(
45deg,
transparent 0,
transparent 6px,
rgba(255, 255, 255, 0.04) 6px,
rgba(255, 255, 255, 0.04) 12px
);
opacity: 0;
transition: opacity 0.2s ease;
z-index: 0;
}
.pixel-card:hover::before {
opacity: 1;
}
.pixel-card > * {
position: relative;
z-index: 1;
}
.dark .pixel-card {
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.65),
0 0 0 1px rgba(255, 255, 255, 0.04);
}
.dark .pixel-card:hover {
box-shadow: 6px 6px 0 rgba(0, 0, 0, 0.75),
0 0 0 1px rgba(255, 255, 255, 0.06);
}
/* Pixel Badge - 像素徽章 */
.pixel-badge {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.25rem 0.65rem;
border-radius: 0;
border-width: 2px;
border-style: solid;
border-color: rgba(241, 140, 110, 0.3);
background: rgba(241, 140, 110, 0.12);
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.08em;
font-weight: 600;
}
/* Pixel Pill - 药丸按钮 */
.pixel-pill {
display: inline-flex;
align-items: center;
gap: 0.5rem;
border-radius: 0;
border-width: 2px;
border-style: solid;
border-color: rgba(217, 119, 87, 0.45);
padding: 0.35rem 0.85rem;
background: color-mix(in srgb, hsl(var(--secondary)) 40%, transparent);
box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.15),
inset 0 0 0 1px rgba(255, 255, 255, 0.06);
transition: all 0.2s ease;
}
.pixel-pill:hover {
background: rgba(217, 119, 87, 0.16);
border-color: rgba(217, 119, 87, 0.65);
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.2),
inset 0 0 0 1px rgba(255, 255, 255, 0.12);
transform: translateY(-1px);
}
.dark .pixel-pill {
border-color: rgba(241, 140, 110, 0.55);
box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.45),
inset 0 0 0 1px rgba(255, 255, 255, 0.06);
}
.dark .pixel-pill:hover {
background: rgba(241, 140, 110, 0.22);
border-color: rgba(241, 140, 110, 0.75);
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.55),
inset 0 0 0 1px rgba(255, 255, 255, 0.12);
}
/* Pixel Text - 像素文字效果 */
.pixel-text {
letter-spacing: 0.08em;
text-transform: uppercase;
text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.25),
-1px -1px 0 rgba(255, 241, 232, 0.18);
}
.dark .pixel-text {
text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.55),
-1px -1px 0 rgba(255, 241, 232, 0.08);
}
/* Grid Pattern - 网格图案 */
.grid-pattern {
position: relative;
}
.grid-pattern::after {
content: '';
position: absolute;
inset: 0;
background-image:
linear-gradient(to right, rgba(241, 140, 110, 0.05) 1px, transparent 1px),
linear-gradient(to bottom, rgba(241, 140, 110, 0.05) 1px, transparent 1px);
background-size: 32px 32px;
opacity: 0.6;
pointer-events: none;
z-index: -1;
}
.dark .grid-pattern::after {
background-image:
linear-gradient(to right, rgba(241, 140, 110, 0.08) 1px, transparent 1px),
linear-gradient(to bottom, rgba(241, 140, 110, 0.08) 1px, transparent 1px);
opacity: 0.4;
}
/* Scanlines - 扫描线效果 */
.scanlines {
background-image: repeating-linear-gradient(
-45deg,
rgba(255, 255, 255, 0.05) 0,
rgba(255, 255, 255, 0.05) 1px,
transparent 1px,
transparent 6px
);
pointer-events: none;
}
.dark .scanlines {
background-image: repeating-linear-gradient(
-45deg,
rgba(255, 255, 255, 0.03) 0,
rgba(255, 255, 255, 0.03) 1px,
transparent 1px,
transparent 6px
);
}
/* Remove rounded corners from common elements */
button:not(.short-cuts-template *, [class*="rounded-full"]),
input,
textarea,
select,
.card,
.badge {
border-radius: 0 !important;
}
/* Keep rounded corners for shortcut buttons and other rounded-full elements */
.short-cuts-template div[class*="rounded-full"],
div[class*="rounded-full"] {
border-radius: 9999px !important;
}

View File

@@ -8,6 +8,7 @@ const config: Config = {
extend: {
fontFamily: {
sans: [
'OPlusSans3-Medium',
'haipaiqiangdiaosenxiyuan',
...defaultTheme.fontFamily.sans
],
@@ -19,6 +20,18 @@ const config: Config = {
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
brand: {
50: 'var(--brand-50)',
100: 'var(--brand-100)',
200: 'var(--brand-200)',
300: 'var(--brand-300)',
400: 'var(--brand-400)',
500: 'var(--brand-500)',
600: 'var(--brand-600)',
700: 'var(--brand-700)',
800: 'var(--brand-800)',
900: 'var(--brand-900)'
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
@@ -63,9 +76,10 @@ const config: Config = {
}
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
lg: '0',
md: '0',
sm: '0',
DEFAULT: '0'
},
animation: {
orbit: 'orbit calc(var(--duration)*1s) linear infinite',