Compare commits
24 Commits
build_1.1.
...
v1.2.1
| Author | SHA1 | Date | |
|---|---|---|---|
| ed5a976735 | |||
|
|
f4e7112d4c | ||
|
|
bc4155ce82 | ||
|
|
f1bda537b4 | ||
|
|
1b2f79f65c | ||
|
|
62383aaf9e | ||
|
|
dcc5654603 | ||
|
|
cf36b71990 | ||
|
|
9cda45f112 | ||
| ef7ef44033 | |||
| 3ca63f62fb | |||
| 4c255a2672 | |||
| bc940a7a6e | |||
| 0f4eaf8523 | |||
| 6755f95e5e | |||
| b29f5c7c1e | |||
| 20b01c6f51 | |||
| 57e1724c34 | |||
| c720e605bf | |||
| 6575bf0c62 | |||
| 639e3b54db | |||
| 7be284fbd5 | |||
| c74c99e776 | |||
| 8026dd0321 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -24,4 +24,6 @@ pnpm-debug.log*
|
||||
.idea/
|
||||
|
||||
teaser.pptx
|
||||
~$teaser.pptx
|
||||
~$teaser.pptx
|
||||
|
||||
.claude
|
||||
1008
package-lock.json
generated
1008
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
14
patches/@shikijs+transformers+1.24.4.patch
Normal file
14
patches/@shikijs+transformers+1.24.4.patch
Normal 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;
|
||||
BIN
public/fonts/OPlusSans3-Medium.woff2
Normal file
BIN
public/fonts/OPlusSans3-Medium.woff2
Normal file
Binary file not shown.
BIN
public/static/images/shortcuts-bg-mini.png
Normal file
BIN
public/static/images/shortcuts-bg-mini.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
1
public/static/mmw.svg
Normal file
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 |
BIN
public/static/showcase-card.v1.png
Normal file
BIN
public/static/showcase-card.v1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 959 KiB |
@@ -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
192
src/components/Badge.astro
Normal 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>
|
||||
@@ -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}`}
|
||||
|
||||
@@ -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>
|
||||
|
||||
406
src/components/DevShortcutsHexagon.astro
Normal file
406
src/components/DevShortcutsHexagon.astro
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
36
src/components/PageHead.astro
Normal file
36
src/components/PageHead.astro
Normal 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" />
|
||||
@@ -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>
|
||||
|
||||
@@ -22,6 +22,7 @@ const iconMap = {
|
||||
Gitea: 'mdi:git',
|
||||
Maven: 'mavenrepo',
|
||||
DevIntro: 'lucide:info',
|
||||
HubProxy: 'lucide:rocket',
|
||||
}
|
||||
|
||||
const getSocialLink = ({ href, label }: SocialLink) => ({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}, [])
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
16
src/content/blog/buildnginx-2025/buildnginx.mdx
vendored
16
src/content/blog/buildnginx-2025/buildnginx.mdx
vendored
@@ -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
9
src/content/project.mdx
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
---
|
||||
|
||||
<h3 class="not-prose text-lg font-medium mb-1">
|
||||
Some work I’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>
|
||||
BIN
src/content/projects/assets/mmw.png
Normal file
BIN
src/content/projects/assets/mmw.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1013 KiB |
8
src/content/projects/miaomiaowu-clashmanager.md
Normal file
8
src/content/projects/miaomiaowu-clashmanager.md
Normal 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'
|
||||
---
|
||||
@@ -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
325
src/lib/data-utils.ts
Normal 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
|
||||
}
|
||||
@@ -30,6 +30,6 @@ export function transformerNotationSkip(
|
||||
return false
|
||||
},
|
||||
undefined, // remove empty lines
|
||||
)
|
||||
) as ShikiTransformer
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
57
src/pages/projects.astro
Normal 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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
533
src/styles/global.css.backup
Normal file
533
src/styles/global.css.backup
Normal 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;
|
||||
}
|
||||
252
src/styles/pixel-components.css
Normal file
252
src/styles/pixel-components.css
Normal 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;
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user