24 Commits

Author SHA1 Message Date
ed5a976735 Merge pull request 'dev' (#13) from dev into main
Reviewed-on: #13
2025-11-12 00:56:16 +08:00
jimleerx
f4e7112d4c 修复断言错误 2025-11-12 00:55:22 +08:00
jimleerx
bc4155ce82 fix unuse import 2025-11-12 00:41:11 +08:00
jimleerx
f1bda537b4 隐藏光标 2025-11-12 00:39:45 +08:00
jimleerx
1b2f79f65c 增加光晕特效 2025-11-12 00:33:12 +08:00
jimleerx
62383aaf9e 给格子加上光晕特效 2025-11-12 00:18:29 +08:00
jimleerx
dcc5654603 修复改为hexagon实现后遮挡背景图的问题 2025-11-12 00:17:50 +08:00
jimleerx
cf36b71990 调整快捷方式gird的实现为代码实现hexagon,而不是图片和边距 2025-11-12 00:12:49 +08:00
jimleerx
9cda45f112 patch shikijs冲突 2025-11-12 00:12:49 +08:00
ef7ef44033 修复某些地方点击会显示光标 2025-11-11 02:00:06 +08:00
3ca63f62fb 调整sm窗口快捷方式偏移量 2025-11-11 01:38:23 +08:00
4c255a2672 调整移动端主页格子大小 2025-11-11 01:03:43 +08:00
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
46 changed files with 3290 additions and 419 deletions

4
.gitignore vendored
View File

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

BIN
dist.zip

Binary file not shown.

1008
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,8 @@
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro",
"prettier": "prettier --write ./src/**/*.{astro,ts,tsx,css}"
"prettier": "prettier --write ./src/**/*.{astro,ts,tsx,css}",
"postinstall": "patch-package"
},
"dependencies": {
"@astrojs/check": "^0.9.4",
@@ -60,6 +61,7 @@
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.15",
"patch-package": "^8.0.1",
"prettier": "^3.4.2",
"prettier-plugin-astro": "^0.13.0",
"prettier-plugin-astro-organize-imports": "^0.4.11",

View File

@@ -0,0 +1,14 @@
diff --git a/node_modules/@shikijs/transformers/dist/index.mjs b/node_modules/@shikijs/transformers/dist/index.mjs
index db4db63..c7f3ea2 100644
--- a/node_modules/@shikijs/transformers/dist/index.mjs
+++ b/node_modules/@shikijs/transformers/dist/index.mjs
@@ -410,6 +410,9 @@ function transformerRenderWhitespace(options = {}) {
return token;
if (position === "trailing" && index !== last)
return token;
+ if (token.children.length === 0) {
+ return token;
+ }
const node = token.children[0];
if (node.type !== "text" || !node.value)
return token;

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

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

@@ -63,15 +63,15 @@ import Link from './Link.astro'
// ]
const SHORTS_CUTS_CLASS_NAMES: string[] = [
'z-10 max-w max-h mt-[2.6em] inline-block sm:mt-[2.35em]',
'z-10 max-w max-h ml-[0.1em] mt-[-3.7em] inline-block sm:ml-[0em] sm:mt-[-3.4em]',
'z-10 max-w max-h ml-[-0.1em] mt-[2.5em] inline-block sm:ml-[0em] sm:mt-[2.5em]',
'z-10 max-w max-h mt-[-4em] inline-block sm:mt-[-3em]',
'z-10 max-w max-h ml-[0.1em] mt-[-4.7em] inline-block sm:ml-[0em] sm:mt-[-3.4em]',
'z-10 max-w max-h ml-[-0.2em] mt-[3.0em] inline-block sm:ml-[0em] sm:mt-[2.5em]',
'z-10 max-w max-h mt-[-5.2em] inline-block sm:mt-[-3em]',
'z-10 max-w max-h mt-[0.1em] inline-block sm:mt-[0.6em]',
'z-10 max-w max-h ml-[0.1em] mt-[-6.5em] inline-block sm:ml-[0em] sm:mt-[-5.3em]',
'z-10 max-w max-h mt-[0.2em] inline-block sm:mt-[0.6em]',
'z-10 max-w max-h mt-[-6.5em] inline-block sm:mt-[-5.3em]',
'z-10 max-w max-h mt-[1.49em] inline-block sm:mt-[1em]',
'z-10 max-w max-h mt-[-5.2em] inline-block sm:mt-[-4.3em]'
'z-10 max-w max-h ml-[0.15em] mt-[-7.5em] inline-block sm:ml-[0em] sm:mt-[-4.8em]',
'z-10 max-w max-h ml-[-0.15em] mt-[0.2em] inline-block sm:mt-[0.9em] sm:ml-[0em]',
'z-10 max-w max-h mt-[-7.5em] inline-block sm:mt-[-4.8em]',
'z-10 max-w max-h mt-[1.88em] inline-block sm:mt-[1em]',
'z-10 max-w max-h ml-[0.15em] mt-[-5.8em] inline-block sm:ml-[0em] sm:mt-[-4em]'
]
---
@@ -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

@@ -0,0 +1,406 @@
---
import { DEV_LINKS } from '@/consts'
import { Icon } from 'astro-icon/components'
// ------------- Hex math helpers (ported from react-hexgrid) -------------
type Point = { x: number; y: number }
type HexCoord = { q: number; r: number; s: number }
type Orientation = {
f0: number
f1: number
f2: number
f3: number
b0: number
b1: number
b2: number
b3: number
startAngle: number
}
type LayoutDimension = {
size: Point
spacing: number
origin: Point
orientation: Orientation
}
const SQRT3 = Math.sqrt(3)
const ORIENTATION_FLAT: Orientation = {
f0: 3 / 2,
f1: 0,
f2: SQRT3 / 2,
f3: SQRT3,
b0: 2 / 3,
b1: 0,
b2: -1 / 3,
b3: SQRT3 / 3,
startAngle: 0,
}
const BASE_HEX_SIZE = 104
const HEX_SIZE = 168
const BORDER_WIDTH = 24
const layout: LayoutDimension = {
size: { x: HEX_SIZE, y: HEX_SIZE },
spacing: 1,
origin: { x: 0, y: 0 },
orientation: ORIENTATION_FLAT,
}
const GRID_WIDTH = 5
const GRID_HEIGHT = 5
function calculatePolygonPoints(size: number, flat = false) {
const angleOffset = flat ? 0 : Math.PI / 6
const corners: Point[] = []
for (let i = 0; i < 6; i++) {
const angle = (2 * Math.PI * i) / 6 + angleOffset
corners.push({
x: size * Math.cos(angle),
y: size * Math.sin(angle),
})
}
return corners.map((corner) => `${corner.x.toFixed(2)},${corner.y.toFixed(2)}`).join(' ')
}
function coordKey(hex: HexCoord) {
return `${hex.q},${hex.r},${hex.s}`
}
function hexToPixel(hex: HexCoord, layout: LayoutDimension): Point {
const { orientation: M, size, spacing, origin } = layout
let x = (M.f0 * hex.q + M.f1 * hex.r) * size.x
let y = (M.f2 * hex.q + M.f3 * hex.r) * size.y
x *= spacing
y *= spacing
return { x: x + origin.x, y: y + origin.y }
}
function rectangle(mapWidth: number, mapHeight: number): HexCoord[] {
const cells: HexCoord[] = []
for (let r = 0; r < mapHeight; r++) {
const offset = Math.floor(r / 2)
for (let q = -offset; q < mapWidth - offset; q++) {
cells.push({ q, r, s: -q - r })
}
}
return cells
}
function hexagon(mapRadius: number): HexCoord[] {
const cells: HexCoord[] = []
for (let q = -mapRadius; q <= mapRadius; q++) {
const r1 = Math.max(-mapRadius, -q - mapRadius)
const r2 = Math.min(mapRadius, -q + mapRadius)
for (let r = r1; r <= r2; r++) {
cells.push({ q, r, s: -q - r })
}
}
return cells
}
// 根据cells的下标与对应值控制显示在哪些格子里
const ACTIVE_COORDS: HexCoord[] = [
// { q: -1, r: 0, s: 1 },
// { q: 0, r: 0, s: 0 },
// { q: 1, r: 0, s: -1 },
{ q: 2, r: 0, s: -2 },
{ q: 3, r: 0, s: -3 },
{ q: 0, r: 1, s: -1 },
{ q: 1, r: 1, s: -2 },
{ q: 2, r: 1, s: -3 },
{ q: 0, r: 2, s: -2 },
{ q: 1, r: 2, s: -3 },
{ q: 2, r: 2, s: -4 },
{ q: 0, r: 3, s: -3 },
{ q: 1, r: 3, s: -4 },
{ q: 2, r: 3, s: -5 },
]
const gridCoords = rectangle(GRID_WIDTH, GRID_HEIGHT)
const gridKeyMap = new Map(gridCoords.map((hex) => [coordKey(hex), hex]))
const preferredCoords = ACTIVE_COORDS.map((hex) => gridKeyMap.get(coordKey(hex))).filter(
Boolean,
) as HexCoord[]
const preferredKeySet = new Set(preferredCoords.map((hex) => coordKey(hex)))
const remainingGridCoords = gridCoords.filter((hex) => !preferredKeySet.has(coordKey(hex)))
let orderedActiveSlots = [...preferredCoords, ...remainingGridCoords]
if (DEV_LINKS.length > orderedActiveSlots.length) {
const usedKeys = new Set(orderedActiveSlots.map((hex) => coordKey(hex)))
let radius = Math.max(GRID_WIDTH, GRID_HEIGHT)
while (orderedActiveSlots.length < DEV_LINKS.length) {
radius += 1
for (const hex of hexagon(radius)) {
const key = coordKey(hex)
if (usedKeys.has(key)) continue
orderedActiveSlots.push(hex)
usedKeys.add(key)
if (orderedActiveSlots.length === DEV_LINKS.length) break
}
}
}
const assignedCoords = orderedActiveSlots.slice(0, DEV_LINKS.length)
const coordToLink = new Map<string, (typeof DEV_LINKS)[number]>()
assignedCoords.forEach((hex, index) => {
const link = DEV_LINKS[index]
if (!hex || !link) return
coordToLink.set(coordKey(hex), link)
})
const renderCoordMap = new Map<string, HexCoord>()
gridCoords.forEach((hex) => renderCoordMap.set(coordKey(hex), hex))
assignedCoords.forEach((hex) => {
if (!hex) return
renderCoordMap.set(coordKey(hex), hex)
})
const renderCoords = Array.from(renderCoordMap.values())
const hexagonPoints = calculatePolygonPoints(layout.size.x, true)
// const circleRadius = layout.size.x * 0.84
const circleRadius = 80
const baseCells = renderCoords.map((hex) => ({
hex,
link: coordToLink.get(coordKey(hex)) ?? null,
center: hexToPixel(hex, layout),
}))
const xs = baseCells.map((cell) => cell.center.x)
const ys = baseCells.map((cell) => cell.center.y)
const padding = layout.size.x * 0.45
const minX = Math.min(...xs) - padding
const maxX = Math.max(...xs) + padding
const minY = Math.min(...ys) - padding
const maxY = Math.max(...ys) + padding
const sizeScale = HEX_SIZE / BASE_HEX_SIZE
const rawWidth = maxX - minX
const rawHeight = maxY - minY
const centerX = (minX + maxX) / 2
const centerY = (minY + maxY) / 2
const viewWidth = rawWidth / sizeScale
const viewHeight = rawHeight / sizeScale
const viewMinX = centerX - viewWidth / 2
const viewMinY = centerY - viewHeight / 2
const viewBox = `${viewMinX} ${viewMinY} ${viewWidth} ${viewHeight}`
const iconClipId = 'hexIconClip'
const cells = baseCells.map((cell) => ({
...cell,
showContent: Boolean(cell.link),
}))
---
<svg
class="hex-grid hex-grid--polygons"
width="100%"
height="100%"
viewBox={viewBox}
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid slice"
>
<g class="hex-grid__group">
{cells.map(({ link, center }, index) => {
const isGhost = !link
return (
<polygon
class={`hexagon-bg ${isGhost ? 'hexagon-bg--ghost' : ''}`}
points={hexagonPoints}
fill="transparent"
stroke="#252525"
stroke-width={BORDER_WIDTH}
transform={`translate(${center.x}, ${center.y})`}
data-index={index}
/>
)
})}
</g>
</svg>
<svg
class="hex-grid hex-grid--icons"
width="100%"
height="100%"
viewBox={viewBox}
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid slice"
>
<defs>
<clipPath id={iconClipId} clipPathUnits="userSpaceOnUse">
<circle cx="0" cy="0" r={circleRadius} />
</clipPath>
</defs>
<g class="hex-grid__group">
{cells.map(({ link, center, showContent }, index) => {
if (!link || !showContent) return null
return (
<g
class="hexagon-wrapper"
transform={`translate(${center.x}, ${center.y})`}
data-index={index}
>
<a
href={link.href}
target="_blank"
rel="noopener noreferrer"
aria-label={link.label}
>
<title>{link.title}</title>
<g class="icon-group">
<circle
r={circleRadius}
fill="hsl(var(--secondary))"
fill-opacity="0.45"
stroke="hsl(var(--border))"
stroke-width={Math.max(4, BORDER_WIDTH * 0.32)}
class="icon-circle z-10"
/>
{link.icon.startsWith('mdi:') ? (
<foreignObject
x={-(layout.size.x * 0.7)}
y={-(layout.size.y * 0.7)}
width={layout.size.x * 1.4}
height={layout.size.y * 1.4}
clip-path={`url(#${iconClipId})`}
class="icon-foreign"
>
<div class="icon-container">
<Icon
name={link.icon}
class="hexagon-icon"
style="color: rgb(233, 211, 182);"
aria-hidden="true"
/>
</div>
</foreignObject>
) : (
<image
href={link.icon}
x={-layout.size.x * 0.3}
y={-layout.size.y * 0.3}
width={layout.size.x * 0.6}
height={layout.size.y * 0.6}
clip-path={`url(#${iconClipId})`}
class="hexagon-image"
/>
)}
</g>
</a>
</g>
)
})}
</g>
</svg>
<style>
.hex-grid {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.hex-grid--polygons {
z-index: 1;
}
.hex-grid--icons {
z-index: 20;
}
.hex-grid__group {
transform-origin: center;
transform: translateX(-114px) translateY(60px) scale(1.08);
}
.hexagon-wrapper {
cursor: pointer;
pointer-events: auto;
user-select: none;
-webkit-user-select: none;
-webkit-tap-highlight-color: transparent;
}
.hexagon-wrapper a {
outline: none;
-webkit-tap-highlight-color: transparent;
pointer-events: auto;
cursor: pointer;
}
.hexagon-wrapper a title {
pointer-events: none;
}
.hexagon-bg,
.icon-circle,
.icon-group {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.icon-circle {
stroke-width: 4px;
transform-origin: center;
transform-box: fill-box;
}
.icon-group {
transform-origin: center;
transform-box: fill-box;
filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.45));
}
.hexagon-wrapper:hover .icon-circle {
fill-opacity: 0.85;
stroke: hsl(var(--primary));
stroke-width: 4px;
transform: scale(1.08);
}
.hexagon-wrapper:hover .icon-group {
transform: rotate(12deg);
transform-origin: center;
}
.hexagon-bg--ghost {
stroke-dasharray: none;
}
.icon-container {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
border-radius: 9999px;
}
.hexagon-icon {
width: 100% !important;
height: 100% !important;
max-width: 88px;
max-height: 88px;
}
.hexagon-image {
object-fit: contain;
}
.icon-foreign {
overflow: visible;
}
:global(.hexagon-icon) {
color: rgb(233, 211, 182) !important;
}
</style>

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
@@ -13,18 +15,19 @@ 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" />
<Link href="/" class="logo-link">
<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)] transition-all duration-300" />
</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>
))
}
@@ -35,3 +38,31 @@ import logo from '../../public/static/logo.webp'
</div>
</Container>
</header>
<style>
.logo-link:hover img {
border-color: hsl(var(--primary));
box-shadow:
0 0 12px rgba(241, 140, 110, 0.6),
0 0 24px rgba(241, 140, 110, 0.4),
0 0 36px rgba(241, 140, 110, 0.2),
4px 4px 0 rgba(0, 0, 0, 0.2);
transform: scale(1.05);
}
nav :global(.pixel-button:hover) {
box-shadow:
0 0 12px rgba(241, 140, 110, 0.5),
0 0 24px rgba(241, 140, 110, 0.3),
0 0 36px rgba(241, 140, 110, 0.15),
4px 4px 0 rgba(0, 0, 0, 0.2) !important;
}
.dark nav :global(.pixel-button:hover) {
box-shadow:
0 0 12px rgba(241, 140, 110, 0.5),
0 0 24px rgba(241, 140, 110, 0.3),
0 0 36px rgba(241, 140, 110, 0.15),
4px 4px 0 rgba(0, 0, 0, 0.4) !important;
}
</style>

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

@@ -1,9 +1,9 @@
import AvatarComponent from '@/components/ui/avatar'
const AuthorPresence = () => {
return (
<div className="relative overflow-hidden sm:aspect-square">
<div className="relative overflow-hidden sm:aspect-square select-none" style={{ cursor: 'default' }}>
<div className="grid size-full grid-rows-4">
<div className="bg-secondary/50"></div>
<div className="row-span-3 flex flex-col gap-3 p-3">
@@ -26,22 +26,22 @@ const AuthorPresence = () => {
/>
</div>
</div>
<div className="flex flex-col gap-y-1 rounded-xl bg-secondary/50 p-3">
<span className="text-base leading-none">jimlee</span>
<span className="text-xs leading-none text-muted-foreground">
<div className="flex flex-col gap-y-1 rounded-xl bg-secondary/50 p-3 select-none">
<span className="text-base leading-none select-none cursor-default">jimlee</span>
<span className="text-xs leading-none text-muted-foreground select-none cursor-default">
li@2ha.me
</span>
</div>
<div className="flex grow rounded-xl bg-secondary/50 px-3 py-2">
<div className="flex size-full flex-col items-center justify-center gap-1">
<div className="flex grow rounded-xl bg-secondary/50 px-3 py-2 select-none">
<div className="flex size-full flex-col items-center justify-center gap-1 select-none">
<img
src="/static/images/lieflat.svg"
alt="No Status Image"
width={64}
height={64}
className="h-full rounded-lg"
className="h-full rounded-lg select-none"
/>
<div className="text-[10px] text-muted-foreground">
<div className="text-[10px] text-muted-foreground select-none cursor-default">
</div>
</div>

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' },
]
@@ -51,10 +54,10 @@ export const DEV_LINKS: DevLink[] = [
icon: 'mdi:git',
},
{
href: 'https://maven.2ha.me',
href: 'https://img.2ha.me',
label: 'Nexus',
title: 'Maven仓库',
icon: 'mdi:chart-doughnut-variant',
title: '图床',
icon: 'mdi:image-multiple',
},
{
href: 'https://dms.2ha.me',
@@ -66,13 +69,13 @@ export const DEV_LINKS: DevLink[] = [
href: 'https://p.2ha.me',
label: 'Zfile',
title: '网盘',
icon: 'mdi:cloud-arrow-up',
icon: 'mdi:harddisk',
},
{
href: 'https://photo.2ha.me',
label: 'immich',
title: '相册',
icon: 'mdi:camera',
href: 'https://tz.2ha.me',
label: 'VPS Monitor',
title: '探针',
icon: 'mdi:chart-areaspline',
},
{
href: 'https://f.2ha.me',
@@ -80,7 +83,12 @@ export const DEV_LINKS: DevLink[] = [
title: '文件服务器',
icon: 'mdi:file-arrow-up-down-outline',
},
{ href: 'https://v.2ha.me', label: 'Emby', title: 'Emby', icon: 'mdi:emby' },
{
href: 'https://status.2ha.me',
label: 'Domain Status',
title: '站点检测',
icon: 'mdi:cloud-check'
},
{
href: 'https://in.2ha.me',
label: '2ha.me statistics',
@@ -88,15 +96,16 @@ 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',
// icon: 'mdi:cat',
},
{
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

@@ -30,6 +30,6 @@ export function transformerNotationSkip(
return false
},
undefined, // remove empty lines
)
) as ShikiTransformer
}

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 top-1/2 -translate-y-1/2 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,31 @@ 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>
<style>
ul li > :global(div:hover) {
box-shadow:
0 0 16px rgba(241, 140, 110, 0.5),
0 0 32px rgba(241, 140, 110, 0.3),
0 0 48px rgba(241, 140, 110, 0.15),
6px 6px 0 rgba(0, 0, 0, 0.26) !important;
}
.dark ul li > :global(div:hover) {
box-shadow:
0 0 16px rgba(241, 140, 110, 0.5),
0 0 32px rgba(241, 140, 110, 0.3),
0 0 48px rgba(241, 140, 110, 0.15),
6px 6px 0 rgba(0, 0, 0, 0.75) !important;
}
</style>

View File

@@ -12,7 +12,8 @@ import { getCollection } from 'astro:content'
import GiteaCalendar from '@/components/custom/GiteaCalendar'
import Music163Player from '@/components/custom/Music163Player'
import RandomAnimeBackground from '@/components/custom/RandomAnimeBackgrounds'
import DevShortCuts from '@/components/DevShortCuts.astro'
// import DevShortCuts from '@/components/DevShortCuts.astro'
import DevShortcutsHexagon from '@/components/DevShortcutsHexagon.astro'
const latestPost = await getCollection('blog').then((posts: any[]) =>
posts
@@ -28,11 +29,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 grid-cols-1 gap-4 px-4 [grid-template-areas:'a'_'d'_'b'_'i'_'e'_'g'_'f'_'j'_'k'] [grid-template-rows:repeat(9,auto)] *: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 *:w-full *:[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:[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:[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,41 +43,29 @@ 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 [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"
src="/static/images/shortcuts-bg.png"
class="overlay absolute bottom-0 right-0 no-repeat max-w-[28%] object-contain z-[100]"
src="/static/images/shortcuts-bg-mini.png"
/>
<!-- <DevStackIconsCloud client:load/> -->
<DevShortCuts />
<!-- <DevShortCuts /> -->
<DevShortcutsHexagon />
</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] aspect-square 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
class="overlay absolute inset-0 size-full rounded-3xl bg-[url('/static/images/lastblogbg-sm.webp')] bg-cover bg-center bg-no-repeat transition-opacity duration-200 sm:bg-[url('/static/images/lastblogbg.webp')] xl:opacity-100"
class="overlay absolute inset-0 size-full rounded-3xl bg-[url('/static/images/lastblogbg-sm.webp')] bg-cover bg-no-repeat transition-opacity duration-200 sm:bg-[url('/static/images/lastblogbg.webp')] xl:opacity-100"
>
</div>
{
@@ -89,7 +78,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 +98,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 +108,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 +153,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']}
@@ -188,3 +171,21 @@ const latestPost = await getCollection('blog').then((posts: any[]) =>
</div>
</section>
</Layout>
<style>
section > div:hover {
box-shadow:
0 0 16px rgba(241, 140, 110, 0.5),
0 0 32px rgba(241, 140, 110, 0.3),
0 0 48px rgba(241, 140, 110, 0.15),
6px 6px 0 rgba(0, 0, 0, 0.26) !important;
}
.dark section > div:hover {
box-shadow:
0 0 16px rgba(241, 140, 110, 0.5),
0 0 32px rgba(241, 140, 110, 0.3),
0 0 48px rgba(241, 140, 110, 0.15),
6px 6px 0 rgba(0, 0, 0, 0.75) !important;
}
</style>

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

@@ -0,0 +1,57 @@
---
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>
<style>
ul li > :global(div:hover) {
box-shadow:
0 0 16px rgba(241, 140, 110, 0.5),
0 0 32px rgba(241, 140, 110, 0.3),
0 0 48px rgba(241, 140, 110, 0.15),
6px 6px 0 rgba(0, 0, 0, 0.26) !important;
}
.dark ul li > :global(div:hover) {
box-shadow:
0 0 16px rgba(241, 140, 110, 0.5),
0 0 32px rgba(241, 140, 110, 0.3),
0 0 48px rgba(241, 140, 110, 0.15),
6px 6px 0 rgba(0, 0, 0, 0.75) !important;
}
</style>

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,105 @@
* {
@apply border-border;
}
/* Prevent text selection cursor on all elements by default */
* {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-webkit-tap-highlight-color: transparent;
cursor: default;
}
/* Allow text selection in content areas */
article,
article *,
.prose,
.prose *,
input,
textarea,
[contenteditable="true"] {
user-select: text;
-webkit-user-select: text;
}
/* Only inputs and textareas should have text cursor */
input,
textarea,
[contenteditable="true"] {
cursor: text;
}
/* Ensure proper cursor on interactive elements */
button,
a,
div,
img,
svg,
.pixel-button,
[role="button"],
[role="link"] {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
cursor: pointer;
}
/* Children of interactive elements should not have text cursor */
button *,
a *,
.pixel-button *,
[role="button"] *,
[role="link"] * {
cursor: pointer;
user-select: none;
}
/* Images should never show text cursor */
img,
svg,
video,
canvas {
user-select: none;
-webkit-user-select: none;
cursor: default;
}
/* Allow text selection for content elements but keep default cursor */
p,
span,
h1,
h2,
h3,
h4,
h5,
h6,
li {
user-select: text;
-webkit-user-select: text;
cursor: default;
}
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',