init
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
---
|
||||
import { site } from '../config';
|
||||
---
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="footer-card">
|
||||
<div>
|
||||
<p class="eyebrow">Built for public sharing</p>
|
||||
<p class="footer-meta">
|
||||
<span>© {new Date().getFullYear()} {site.name}</span>
|
||||
<span>·</span>
|
||||
<span>Astro static site</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="inline-links">
|
||||
<a class="section-link" href="/logs">查看开发日志</a>
|
||||
<a class="section-link" href="/projects">浏览项目</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
const heatmapLevels = [
|
||||
'#0f172a',
|
||||
'#13233e',
|
||||
'#1d4ed8',
|
||||
'#2563eb',
|
||||
'#22c55e',
|
||||
];
|
||||
|
||||
const heatmap = [
|
||||
0, 1, 2, 0, 3, 1, 4, 2, 0, 1,
|
||||
1, 3, 0, 2, 1, 4, 2, 1, 0, 3,
|
||||
2, 0, 1, 3, 4, 1, 0, 2, 1, 3,
|
||||
0, 1, 4, 2, 0, 3, 1, 2, 4, 1,
|
||||
1, 2, 0, 3, 1, 4, 2, 0, 1, 2,
|
||||
0, 4, 1, 2, 3, 0, 2, 1, 4, 1,
|
||||
2, 1, 3, 0, 2, 1, 4, 2, 0, 1,
|
||||
];
|
||||
|
||||
const recentActivities = [
|
||||
{
|
||||
title: 'homepage landing',
|
||||
detail: '重建首页骨架与模块层级',
|
||||
time: '今天',
|
||||
},
|
||||
{
|
||||
title: 'design spec',
|
||||
detail: '完成工程编辑部风格设计文档',
|
||||
time: '今天',
|
||||
},
|
||||
{
|
||||
title: 'content seeds',
|
||||
detail: '补齐示例日志、项目与分享数据',
|
||||
time: '今天',
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<section class="panel activity-panel">
|
||||
<div class="section-heading" style="margin-bottom: 1rem;">
|
||||
<div>
|
||||
<span class="eyebrow">Gitea Activity</span>
|
||||
<h2>活跃度概览</h2>
|
||||
<p>首版先用静态面板承载未来的热力图与活动流位置,后续再接真实 API。</p>
|
||||
</div>
|
||||
<a class="section-link" href="https://gitea.sepcomet.xyz" target="_blank" rel="noreferrer">
|
||||
打开 Gitea
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="activity-grid">
|
||||
<div>
|
||||
<p class="activity-copy">贡献热力图区域预留给未来的客户端渲染模块。当前先用静态密度块确认布局节奏与深色模块层级。</p>
|
||||
<div class="heatmap" aria-label="示例贡献热力图">
|
||||
{
|
||||
heatmap.map((level) => (
|
||||
<span
|
||||
class="heatmap-cell"
|
||||
style={`background:${heatmapLevels[level]};`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div class="heatmap-legend">
|
||||
<span>贡献强度</span>
|
||||
<span class="legend-scale" aria-hidden="true">
|
||||
{heatmapLevels.map((color) => <span style={`background:${color};`} />)}
|
||||
</span>
|
||||
<span>低 → 高</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="activity-list">
|
||||
{
|
||||
recentActivities.map((item) => (
|
||||
<article class="activity-item">
|
||||
<strong class="mono">{item.title}</strong>
|
||||
<p class="activity-copy">{item.detail}</p>
|
||||
<p class="activity-meta" style="margin-top: 0.55rem;">
|
||||
<span>{item.time}</span>
|
||||
</p>
|
||||
</article>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
import { site } from '../config';
|
||||
|
||||
interface Props {
|
||||
currentPath?: string;
|
||||
}
|
||||
|
||||
const { currentPath = '/' } = Astro.props;
|
||||
|
||||
const navItems = [
|
||||
{ href: '/', label: 'Home' },
|
||||
{ href: '/logs', label: 'Logs' },
|
||||
{ href: '/projects', label: 'Projects' },
|
||||
{ href: '/shares', label: 'Shares' },
|
||||
];
|
||||
|
||||
const isActive = (href: string) =>
|
||||
href === '/'
|
||||
? currentPath === '/'
|
||||
: currentPath === href || currentPath.startsWith(`${href}/`);
|
||||
---
|
||||
|
||||
<header class="site-header">
|
||||
<a class="site-brand" href="/" aria-label="返回首页">
|
||||
<span class="site-brand__mark" aria-hidden="true">SC</span>
|
||||
<span class="site-brand__text">
|
||||
<span class="site-brand__title">{site.name}</span>
|
||||
<span class="site-brand__subtitle">{site.tagline}</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<nav class="site-nav" aria-label="主导航">
|
||||
{
|
||||
navItems.map((item) => (
|
||||
<a
|
||||
class="nav-link"
|
||||
href={item.href}
|
||||
aria-current={isActive(item.href) ? 'page' : undefined}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</nav>
|
||||
|
||||
<div class="site-links" aria-label="外部链接">
|
||||
{site.links.map((link) => (
|
||||
<a class="header-link" href={link.url} target="_blank" rel="noreferrer">
|
||||
{link.name}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</header>
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
interface Props {
|
||||
slug: string;
|
||||
title: string;
|
||||
date: Date;
|
||||
repo: string;
|
||||
summary: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
const { slug, title, date, repo, summary, tags } = Astro.props;
|
||||
const formattedDate = date.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
});
|
||||
---
|
||||
|
||||
<article class="card">
|
||||
<p class="log-meta">
|
||||
<span class="mono">{formattedDate}</span>
|
||||
<span class="mono">{repo}</span>
|
||||
</p>
|
||||
<h3 class="log-title">
|
||||
<a href={`/logs/${slug}/`}>{title}</a>
|
||||
</h3>
|
||||
<p>{summary}</p>
|
||||
|
||||
<div class="tag-list" style="margin-top: 1rem;">
|
||||
{tags.map((tag) => <span class="tag tag--tech">{tag}</span>)}
|
||||
</div>
|
||||
</article>
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
interface Props {
|
||||
name: string;
|
||||
description: string;
|
||||
coverImage: string;
|
||||
tags: string[];
|
||||
repo: string;
|
||||
featured?: boolean;
|
||||
demoVideo?: string;
|
||||
downloadLink?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
coverImage,
|
||||
tags,
|
||||
repo,
|
||||
featured = false,
|
||||
demoVideo = '',
|
||||
downloadLink = '',
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<article class="card">
|
||||
<div class="project-cover">
|
||||
<img src={coverImage} alt={`${name} 项目封面`} loading="lazy" />
|
||||
</div>
|
||||
|
||||
<div class="project-meta">
|
||||
<span class="mono">{repo}</span>
|
||||
{featured ? <span class="tag">featured</span> : null}
|
||||
</div>
|
||||
|
||||
<h3 class="project-title">{name}</h3>
|
||||
<p class="project-description">{description}</p>
|
||||
|
||||
<div class="tag-list" style="margin: 1rem 0 0.95rem;">
|
||||
{tags.map((tag) => <span class="tag tag--tech">{tag}</span>)}
|
||||
</div>
|
||||
|
||||
<div class="project-links">
|
||||
<a class="section-link" href={`https://gitea.sepcomet.xyz/${repo}`} target="_blank" rel="noreferrer">
|
||||
仓库
|
||||
</a>
|
||||
{demoVideo ? (
|
||||
<a class="section-link" href={demoVideo} target="_blank" rel="noreferrer">
|
||||
Demo
|
||||
</a>
|
||||
) : null}
|
||||
{downloadLink ? (
|
||||
<a class="section-link" href={downloadLink} target="_blank" rel="noreferrer">
|
||||
下载
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
</article>
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const galleryDirectory = fileURLToPath(
|
||||
new URL('../../public/images/gallery', import.meta.url),
|
||||
);
|
||||
|
||||
const supportedExtensions = new Set([
|
||||
'.avif',
|
||||
'.gif',
|
||||
'.jpeg',
|
||||
'.jpg',
|
||||
'.png',
|
||||
'.svg',
|
||||
'.webp',
|
||||
]);
|
||||
|
||||
const images = fs.existsSync(galleryDirectory)
|
||||
? fs
|
||||
.readdirSync(galleryDirectory)
|
||||
.filter((file) => supportedExtensions.has(path.extname(file).toLowerCase()))
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((file) => ({
|
||||
src: `/images/gallery/${file}`,
|
||||
alt: path
|
||||
.parse(file)
|
||||
.name.replace(/[-_]+/g, ' ')
|
||||
.replace(/\b\w/g, (match) => match.toUpperCase()),
|
||||
}))
|
||||
: [];
|
||||
|
||||
const fallbackImage = images[0];
|
||||
---
|
||||
|
||||
{
|
||||
images.length > 0 ? (
|
||||
<div class="corner-gallery" data-random-corner-gallery aria-hidden="true">
|
||||
<img
|
||||
class="corner-gallery__image"
|
||||
src={fallbackImage.src}
|
||||
alt={fallbackImage.alt}
|
||||
width="120"
|
||||
height="120"
|
||||
loading="eager"
|
||||
/>
|
||||
<script type="application/json" data-corner-gallery-data set:html={JSON.stringify(images)} />
|
||||
<script>
|
||||
const galleries = document.querySelectorAll('[data-random-corner-gallery]');
|
||||
|
||||
galleries.forEach((gallery) => {
|
||||
const dataElement = gallery.querySelector('[data-corner-gallery-data]');
|
||||
const imageElement = gallery.querySelector('.corner-gallery__image');
|
||||
|
||||
if (!dataElement || !imageElement) return;
|
||||
|
||||
try {
|
||||
const images = JSON.parse(dataElement.textContent || '[]');
|
||||
if (!Array.isArray(images) || images.length === 0) return;
|
||||
|
||||
const randomImage = images[Math.floor(Math.random() * images.length)];
|
||||
if (!randomImage?.src) return;
|
||||
|
||||
imageElement.src = randomImage.src;
|
||||
imageElement.alt = randomImage.alt || '随机图库图片';
|
||||
} catch {
|
||||
// Keep fallback image if parsing fails.
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
interface Props {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
description: string;
|
||||
href?: string;
|
||||
linkLabel?: string;
|
||||
}
|
||||
|
||||
const { eyebrow, title, description, href, linkLabel } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<span class="eyebrow">{eyebrow}</span>
|
||||
<h2>{title}</h2>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
|
||||
{href && linkLabel ? (
|
||||
<a class="section-link" href={href}>
|
||||
{linkLabel}
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -0,0 +1,19 @@
|
||||
export const site = {
|
||||
name: 'SepComet',
|
||||
title: '个人主页',
|
||||
tagline: 'Unity 开发者 / 持续构建中的个人工作台',
|
||||
description:
|
||||
'记录开发过程,整理项目输出,把日常构建沉淀成可浏览、可复盘的公开主页。',
|
||||
avatar: '/images/avatar.svg',
|
||||
location: 'Shanghai / Remote',
|
||||
gitea: {
|
||||
url: 'https://gitea.sepcomet.xyz',
|
||||
username: 'sepcomet',
|
||||
},
|
||||
links: [
|
||||
{ name: 'GitHub', url: 'https://github.com/sepcomet' },
|
||||
{ name: 'Gitea', url: 'https://gitea.sepcomet.xyz' },
|
||||
{ name: 'Blog', url: 'https://sepcomet.xyz' },
|
||||
],
|
||||
stacks: ['Unity', 'Astro', 'TypeScript', 'Docker'],
|
||||
} as const;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { defineCollection, z } from 'astro:content';
|
||||
import { glob } from 'astro/loaders';
|
||||
|
||||
const logs = defineCollection({
|
||||
loader: glob({ pattern: '**/*.md', base: './src/content/logs' }),
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
date: z.coerce.date(),
|
||||
repo: z.string(),
|
||||
tags: z.array(z.string()),
|
||||
summary: z.string(),
|
||||
commit: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = { logs };
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
title: "首页设计系统落稿"
|
||||
date: 2026-05-03
|
||||
repo: "sepcomet/personal-homepage"
|
||||
tags: ["design", "ui", "editorial"]
|
||||
summary: "确定工程编辑部风格:浅色编辑布局为主,局部保留终端式技术感。"
|
||||
commit: "draft"
|
||||
---
|
||||
|
||||
风格上不做炫技型作品集,而是做一个更像工程师公开工作台的主页。
|
||||
|
||||
重点约束:
|
||||
|
||||
- 首页以 **Hero / Logs / Projects / Activity** 为主顺序
|
||||
- 主体颜色保持黑白灰,强调色只保留蓝色
|
||||
- Gitea 统计模块允许独立使用深色面板,拉开视觉层次
|
||||
|
||||
这样能兼顾专业感、可读性和开发者身份辨识度。
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
title: "首页首版落地"
|
||||
date: 2026-05-03
|
||||
repo: "sepcomet/personal-homepage"
|
||||
tags: ["implementation", "homepage", "astro"]
|
||||
summary: "从零重建 Astro 首页骨架,补齐导航、内容卡片、活动面板和基础详情路由。"
|
||||
commit: "draft"
|
||||
---
|
||||
|
||||
原有首页内容已经清空,这一版从零开始重建。
|
||||
|
||||
首版关注点:
|
||||
|
||||
- 首页信息架构是否清楚
|
||||
- 内容卡片是否真实可点击
|
||||
- 日志、项目、分享入口是否能通到对应页面
|
||||
|
||||
等首页结构稳定后,再继续补内容系统与交互细节。
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: "个人主页需求梳理"
|
||||
date: 2026-05-03
|
||||
repo: "sepcomet/personal-homepage"
|
||||
tags: ["planning", "astro", "content-model"]
|
||||
summary: "整理首页、日志、项目和分享页范围,确定内容由 Markdown 与 JSON 驱动。"
|
||||
commit: "draft"
|
||||
---
|
||||
|
||||
这次先把个人主页定位为一个公开静态站点,而不是带后台能力的应用。
|
||||
|
||||
核心原则有两个:
|
||||
|
||||
1. **内容文件就是数据源**,站点本身不关心数据由谁写入。
|
||||
2. **页面结构优先服务求职与展示**,所以首页要同时承担身份介绍、日志入口和项目入口。
|
||||
|
||||
接下来的实现会先把首页骨架搭出来,再继续补日志与项目页。
|
||||
@@ -0,0 +1,32 @@
|
||||
[
|
||||
{
|
||||
"name": "personal-homepage",
|
||||
"description": "一个由 Markdown 与 JSON 驱动的公开个人主页,用于展示项目、开发日志与外部分享。",
|
||||
"gitea_repo": "sepcomet/personal-homepage",
|
||||
"cover_image": "/images/projects/personal-homepage.svg",
|
||||
"demo_video": "",
|
||||
"download_link": "",
|
||||
"tags": ["astro", "static-site", "content-driven"],
|
||||
"featured": true
|
||||
},
|
||||
{
|
||||
"name": "devlog-pipeline",
|
||||
"description": "规划中的开发日志生成与写入链路,用于把日常开发过程沉淀为结构化 Markdown 内容。",
|
||||
"gitea_repo": "sepcomet/devlog-pipeline",
|
||||
"cover_image": "/images/projects/devlog-pipeline.svg",
|
||||
"demo_video": "",
|
||||
"download_link": "",
|
||||
"tags": ["automation", "markdown", "workflow"],
|
||||
"featured": true
|
||||
},
|
||||
{
|
||||
"name": "gitea-activity-panel",
|
||||
"description": "面向个人主页的 Gitea 活动概览模块,计划提供贡献热力图与近期活动列表。",
|
||||
"gitea_repo": "sepcomet/gitea-activity-panel",
|
||||
"cover_image": "/images/projects/gitea-activity-panel.svg",
|
||||
"demo_video": "",
|
||||
"download_link": "",
|
||||
"tags": ["gitea", "visualization", "dashboard"],
|
||||
"featured": true
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"name": "个人主页需求文档",
|
||||
"description": "站点范围、页面结构、数据模型与部署思路的初稿。",
|
||||
"url": "#",
|
||||
"time": "2026-05-03"
|
||||
},
|
||||
{
|
||||
"name": "首页设计说明",
|
||||
"description": "工程编辑部风格的首页设计约束与组件边界。",
|
||||
"url": "#",
|
||||
"time": "2026-05-03"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
import Header from '../components/Header.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
import '../styles/global.css';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
currentPath?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
title = 'SepComet | 个人主页',
|
||||
description = '公开、静态、数据驱动的个人主页',
|
||||
currentPath = '/',
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content={description} />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page-shell">
|
||||
<Header currentPath={currentPath} />
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,126 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import GiteaActivity from '../components/GiteaActivity.astro';
|
||||
import LogCard from '../components/LogCard.astro';
|
||||
import ProjectCard from '../components/ProjectCard.astro';
|
||||
import RandomCornerImage from '../components/RandomCornerImage.astro';
|
||||
import SectionTitle from '../components/SectionTitle.astro';
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import { site } from '../config';
|
||||
import projects from '../content/projects/index.json';
|
||||
|
||||
const logs = (await getCollection('logs')).sort(
|
||||
(a, b) => b.data.date.getTime() - a.data.date.getTime(),
|
||||
);
|
||||
|
||||
const latestLogs = logs.slice(0, 3);
|
||||
const featuredProjects = projects.filter((project) => project.featured).slice(0, 3);
|
||||
const latestLog = latestLogs[0];
|
||||
---
|
||||
|
||||
<Layout title={`${site.name} | ${site.title}`} description={site.description} currentPath="/">
|
||||
<section class="page-hero">
|
||||
<div class="container hero-grid">
|
||||
<article class="hero-card">
|
||||
<span class="hero-kicker">公开静态站点 / 数据驱动内容</span>
|
||||
<h1 class="hero-title">{site.name}</h1>
|
||||
<p class="hero-subtitle">{site.tagline}</p>
|
||||
<p class="hero-meta">{site.description}</p>
|
||||
|
||||
<div class="hero-actions">
|
||||
<a class="button-primary" href="/projects">查看精选项目</a>
|
||||
<a class="button-secondary" href="/logs">阅读开发日志</a>
|
||||
</div>
|
||||
|
||||
<div class="inline-links" style="margin-top: 1rem;">
|
||||
{site.links.map((link) => (
|
||||
<a class="section-link" href={link.url} target="_blank" rel="noreferrer">
|
||||
{link.name}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<aside class="hero-card status-panel" aria-label="站点状态摘要">
|
||||
<div class="status-panel__head">
|
||||
<div>
|
||||
<p class="eyebrow">Current status</p>
|
||||
<h2 class="status-panel__title">持续构建中的个人工作台</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status-panel__grid">
|
||||
<section class="status-item">
|
||||
<p class="status-item__label">Latest log</p>
|
||||
<p class="status-item__value">{latestLog?.data.title ?? '暂无日志'}</p>
|
||||
</section>
|
||||
<section class="status-item">
|
||||
<p class="status-item__label">Active repo</p>
|
||||
<p class="status-item__value status-item__mono">sepcomet/personal-homepage</p>
|
||||
</section>
|
||||
<section class="status-item">
|
||||
<p class="status-item__label">Stack</p>
|
||||
<p class="status-item__value status-item__mono">{site.stacks.join(' / ')}</p>
|
||||
</section>
|
||||
<section class="status-item">
|
||||
<p class="status-item__label">Gitea</p>
|
||||
<p class="status-item__value status-item__mono">{site.gitea.username}</p>
|
||||
</section>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<SectionTitle
|
||||
eyebrow="Latest Logs"
|
||||
title="最近开发日志"
|
||||
description="用日志记录做了什么、为什么这么做,以及每一步是如何落到实际内容结构里的。"
|
||||
href="/logs"
|
||||
linkLabel="查看全部日志"
|
||||
/>
|
||||
|
||||
<div class="card-grid">
|
||||
{latestLogs.map((log) => <LogCard slug={log.id} {...log.data} />)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<SectionTitle
|
||||
eyebrow="Featured Projects"
|
||||
title="精选项目"
|
||||
description="首页先展示最能代表当前方向的几个项目或模块,作为后续内容填充的真实入口。"
|
||||
href="/projects"
|
||||
linkLabel="打开项目列表"
|
||||
/>
|
||||
|
||||
<div class="card-grid">
|
||||
{
|
||||
featuredProjects.map((project) => (
|
||||
<ProjectCard
|
||||
name={project.name}
|
||||
description={project.description}
|
||||
coverImage={project.cover_image}
|
||||
tags={project.tags}
|
||||
repo={project.gitea_repo}
|
||||
featured={project.featured}
|
||||
demoVideo={project.demo_video}
|
||||
downloadLink={project.download_link}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<GiteaActivity />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<RandomCornerImage />
|
||||
</Layout>
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
import { getCollection, render } from 'astro:content';
|
||||
import Layout from '../../layouts/Layout.astro';
|
||||
import { site } from '../../config';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const logs = await getCollection('logs');
|
||||
return logs.map((log) => ({
|
||||
params: { slug: log.id },
|
||||
props: { log },
|
||||
}));
|
||||
}
|
||||
|
||||
const { log } = Astro.props;
|
||||
const { Content } = await render(log);
|
||||
|
||||
const formattedDate = log.data.date.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
});
|
||||
---
|
||||
|
||||
<Layout
|
||||
title={`${log.data.title} | ${site.name}`}
|
||||
description={log.data.summary}
|
||||
currentPath="/logs"
|
||||
>
|
||||
<section class="list-page">
|
||||
<div class="container">
|
||||
<div class="page-intro">
|
||||
<span class="eyebrow">Log Detail</span>
|
||||
<h1>{log.data.title}</h1>
|
||||
<p>{log.data.summary}</p>
|
||||
</div>
|
||||
|
||||
<article class="prose">
|
||||
<p class="log-meta">
|
||||
<span class="mono">{formattedDate}</span>
|
||||
<span class="mono">{log.data.repo}</span>
|
||||
{log.data.commit ? <span class="mono">commit: {log.data.commit}</span> : null}
|
||||
</p>
|
||||
|
||||
<div class="tag-list" style="margin-bottom: 1.25rem;">
|
||||
{log.data.tags.map((tag: string) => <span class="tag tag--tech">{tag}</span>)}
|
||||
</div>
|
||||
|
||||
<Content />
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import LogCard from '../../components/LogCard.astro';
|
||||
import Layout from '../../layouts/Layout.astro';
|
||||
import { site } from '../../config';
|
||||
|
||||
const logs = (await getCollection('logs')).sort(
|
||||
(a, b) => b.data.date.getTime() - a.data.date.getTime(),
|
||||
);
|
||||
---
|
||||
|
||||
<Layout
|
||||
title={`开发日志 | ${site.name}`}
|
||||
description="按时间倒序展示开发日志摘要。"
|
||||
currentPath="/logs"
|
||||
>
|
||||
<section class="list-page">
|
||||
<div class="container">
|
||||
<div class="page-intro">
|
||||
<span class="eyebrow">Logs</span>
|
||||
<h1>开发日志</h1>
|
||||
<p>这里承载由 Markdown 驱动的开发日志。当前先放入 3 条示例内容,后续可由外部系统继续写入。</p>
|
||||
</div>
|
||||
|
||||
<div class="card-grid">
|
||||
{logs.map((log) => <LogCard slug={log.id} {...log.data} />)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
import Layout from '../../layouts/Layout.astro';
|
||||
import ProjectCard from '../../components/ProjectCard.astro';
|
||||
import { site } from '../../config';
|
||||
import projects from '../../content/projects/index.json';
|
||||
---
|
||||
|
||||
<Layout
|
||||
title={`项目展示 | ${site.name}`}
|
||||
description="展示精选项目与结构化项目数据。"
|
||||
currentPath="/projects"
|
||||
>
|
||||
<section class="list-page">
|
||||
<div class="container">
|
||||
<div class="page-intro">
|
||||
<span class="eyebrow">Projects</span>
|
||||
<h1>项目展示</h1>
|
||||
<p>项目数据当前来自本地 JSON 文件,首页与此页面共享同一份结构化数据来源。</p>
|
||||
</div>
|
||||
|
||||
<div class="card-grid">
|
||||
{
|
||||
projects.map((project) => (
|
||||
<ProjectCard
|
||||
name={project.name}
|
||||
description={project.description}
|
||||
coverImage={project.cover_image}
|
||||
tags={project.tags}
|
||||
repo={project.gitea_repo}
|
||||
featured={project.featured}
|
||||
demoVideo={project.demo_video}
|
||||
downloadLink={project.download_link}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import { site } from '../config';
|
||||
import shares from '../content/shares/index.json';
|
||||
---
|
||||
|
||||
<Layout
|
||||
title={`分享链接 | ${site.name}`}
|
||||
description="公开分享链接与资源入口。"
|
||||
currentPath="/shares"
|
||||
>
|
||||
<section class="list-page">
|
||||
<div class="container">
|
||||
<div class="page-intro">
|
||||
<span class="eyebrow">Shares</span>
|
||||
<h1>分享链接</h1>
|
||||
<p>这里预留给 Seafile 或其他公开资源的索引入口。当前先用静态数据确认列表版式。</p>
|
||||
</div>
|
||||
|
||||
<div class="card-grid" style="grid-template-columns: 1fr;">
|
||||
{shares.map((share) => (
|
||||
<article class="card">
|
||||
<p class="log-meta">
|
||||
<span class="mono">{share.time}</span>
|
||||
</p>
|
||||
<h2 class="project-title">{share.name}</h2>
|
||||
<p>{share.description}</p>
|
||||
<div class="project-links" style="margin-top: 1rem;">
|
||||
<a class="section-link" href={share.url}>打开链接</a>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
@@ -0,0 +1,657 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Archivo:wght@500;600;700&family=Fira+Code:wght@400;500&family=Space+Grotesk:wght@400;500;700&display=swap');
|
||||
|
||||
:root {
|
||||
--bg: #fafafa;
|
||||
--surface: rgba(255, 255, 255, 0.88);
|
||||
--surface-strong: #ffffff;
|
||||
--surface-muted: #f4f4f5;
|
||||
--panel-dark: #0f172a;
|
||||
--panel-border: rgba(148, 163, 184, 0.22);
|
||||
--text: #09090b;
|
||||
--muted: #52525b;
|
||||
--muted-strong: #27272a;
|
||||
--border: #e4e4e7;
|
||||
--border-strong: #d4d4d8;
|
||||
--accent: #2563eb;
|
||||
--accent-soft: rgba(37, 99, 235, 0.08);
|
||||
--success: #22c55e;
|
||||
--shadow-sm: 0 10px 30px rgba(15, 23, 42, 0.04);
|
||||
--shadow-md: 0 18px 40px rgba(15, 23, 42, 0.08);
|
||||
--shadow-lg: 0 24px 60px rgba(15, 23, 42, 0.14);
|
||||
--radius-sm: 14px;
|
||||
--radius-md: 22px;
|
||||
--radius-lg: 30px;
|
||||
--container: 1180px;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
font-family: 'Space Grotesk', system-ui, sans-serif;
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(37, 99, 235, 0.07), transparent 34%),
|
||||
linear-gradient(180deg, #ffffff 0%, var(--bg) 42%, #f5f7fb 100%);
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
button,
|
||||
a[role='button'] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: min(var(--container), calc(100% - 2rem));
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-shell {
|
||||
position: relative;
|
||||
padding: 1rem 0 4rem;
|
||||
}
|
||||
|
||||
.site-header {
|
||||
position: sticky;
|
||||
top: 1rem;
|
||||
z-index: 50;
|
||||
width: min(var(--container), calc(100% - 2rem));
|
||||
margin: 0 auto 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 0.9rem 1rem;
|
||||
border: 1px solid rgba(228, 228, 231, 0.9);
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
box-shadow: var(--shadow-sm);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.site-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.85rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.site-brand__mark {
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
font-family: 'Archivo', sans-serif;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
background: linear-gradient(135deg, #2563eb, #0f172a);
|
||||
}
|
||||
|
||||
.site-brand__text {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.site-brand__title {
|
||||
font-family: 'Archivo', sans-serif;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.site-brand__subtitle {
|
||||
font-size: 0.78rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.site-nav,
|
||||
.site-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.nav-link,
|
||||
.header-link {
|
||||
padding: 0.55rem 0.85rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.94rem;
|
||||
color: var(--muted);
|
||||
transition: background-color 180ms ease, color 180ms ease;
|
||||
}
|
||||
|
||||
.nav-link:hover,
|
||||
.nav-link:focus-visible,
|
||||
.header-link:hover,
|
||||
.header-link:focus-visible {
|
||||
color: var(--text);
|
||||
background: var(--surface-muted);
|
||||
}
|
||||
|
||||
.nav-link[aria-current='page'] {
|
||||
color: var(--accent);
|
||||
background: var(--accent-soft);
|
||||
}
|
||||
|
||||
.page-hero {
|
||||
padding: 2rem 0 1rem;
|
||||
}
|
||||
|
||||
.hero-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.45fr) minmax(320px, 0.95fr);
|
||||
gap: 1.5rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.hero-card,
|
||||
.card,
|
||||
.panel {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--surface);
|
||||
box-shadow: var(--shadow-sm);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.hero-card {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.hero-kicker,
|
||||
.eyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.35rem 0.7rem;
|
||||
border-radius: 999px;
|
||||
background: var(--surface-muted);
|
||||
color: var(--muted-strong);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
margin: 1.2rem 0 0.8rem;
|
||||
font-family: 'Archivo', sans-serif;
|
||||
font-size: clamp(2.6rem, 6vw, 4.8rem);
|
||||
line-height: 0.95;
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
margin: 0 0 1rem;
|
||||
max-width: 48rem;
|
||||
font-size: clamp(1.05rem, 2vw, 1.28rem);
|
||||
line-height: 1.7;
|
||||
color: var(--muted-strong);
|
||||
}
|
||||
|
||||
.hero-meta {
|
||||
margin: 0 0 1.2rem;
|
||||
font-size: 0.96rem;
|
||||
line-height: 1.7;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.hero-actions,
|
||||
.inline-links,
|
||||
.tag-list,
|
||||
.project-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.button-primary,
|
||||
.button-secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.45rem;
|
||||
min-height: 2.9rem;
|
||||
padding: 0.7rem 1rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid transparent;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
transition: transform 180ms ease, box-shadow 180ms ease, background-color 180ms ease, border-color 180ms ease;
|
||||
}
|
||||
|
||||
.button-primary {
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, var(--accent), #1d4ed8);
|
||||
box-shadow: 0 16px 30px rgba(37, 99, 235, 0.2);
|
||||
}
|
||||
|
||||
.button-secondary {
|
||||
color: var(--text);
|
||||
background: var(--surface-strong);
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.button-primary:hover,
|
||||
.button-primary:focus-visible,
|
||||
.button-secondary:hover,
|
||||
.button-secondary:focus-visible {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.status-panel {
|
||||
padding: 1.5rem;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(244, 244, 245, 0.95)),
|
||||
var(--surface);
|
||||
}
|
||||
|
||||
.status-panel__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
|
||||
.status-panel__title {
|
||||
margin: 0;
|
||||
font-family: 'Archivo', sans-serif;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.status-panel__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.9rem;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 1.1rem;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.status-item__label {
|
||||
margin: 0 0 0.35rem;
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.03em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.status-item__value {
|
||||
margin: 0;
|
||||
font-family: 'Archivo', sans-serif;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.status-item__mono {
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.section-heading h2 {
|
||||
margin: 0.2rem 0 0;
|
||||
font-family: 'Archivo', sans-serif;
|
||||
font-size: clamp(1.7rem, 3.5vw, 2.4rem);
|
||||
}
|
||||
|
||||
.section-heading p {
|
||||
margin: 0;
|
||||
max-width: 38rem;
|
||||
color: var(--muted);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.section-link {
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.stack-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.1fr 0.9fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1.35rem;
|
||||
transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease;
|
||||
}
|
||||
|
||||
.card:hover,
|
||||
.card:focus-within {
|
||||
transform: translateY(-2px);
|
||||
border-color: var(--border-strong);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.log-meta,
|
||||
.project-meta,
|
||||
.activity-meta,
|
||||
.footer-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.55rem 0.8rem;
|
||||
margin: 0 0 0.85rem;
|
||||
color: var(--muted);
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: 'Fira Code', monospace;
|
||||
}
|
||||
|
||||
.log-title,
|
||||
.project-title {
|
||||
margin: 0 0 0.7rem;
|
||||
font-family: 'Archivo', sans-serif;
|
||||
font-size: 1.18rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.card p,
|
||||
.project-description,
|
||||
.activity-copy,
|
||||
.empty-state {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 1.9rem;
|
||||
padding: 0.25rem 0.7rem;
|
||||
border-radius: 999px;
|
||||
background: var(--surface-muted);
|
||||
color: var(--muted-strong);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.tag--tech {
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.project-cover {
|
||||
aspect-ratio: 16 / 9;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: calc(var(--radius-md) - 8px);
|
||||
border: 1px solid var(--border);
|
||||
background: #eef2ff;
|
||||
}
|
||||
|
||||
.activity-panel {
|
||||
padding: 1.5rem;
|
||||
color: #f8fafc;
|
||||
border-color: var(--panel-border);
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(37, 99, 235, 0.4), transparent 28%),
|
||||
linear-gradient(180deg, #111c33, #0f172a 70%);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.activity-panel .section-heading p,
|
||||
.activity-panel .section-heading .eyebrow,
|
||||
.activity-panel .activity-copy,
|
||||
.activity-panel .activity-meta,
|
||||
.activity-panel .heatmap-legend {
|
||||
color: rgba(226, 232, 240, 0.86);
|
||||
}
|
||||
|
||||
.activity-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.1fr 0.9fr;
|
||||
gap: 1rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.heatmap {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(10, 1fr);
|
||||
gap: 0.45rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.heatmap-cell {
|
||||
aspect-ratio: 1;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||
}
|
||||
|
||||
.heatmap-legend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
margin-top: 0.9rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.legend-scale {
|
||||
display: inline-flex;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.legend-scale span {
|
||||
width: 0.95rem;
|
||||
height: 0.95rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||
}
|
||||
|
||||
.activity-list {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
padding: 0.95rem 1rem;
|
||||
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||
border-radius: 1rem;
|
||||
background: rgba(15, 23, 42, 0.32);
|
||||
}
|
||||
|
||||
.activity-item strong {
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.corner-gallery {
|
||||
position: absolute;
|
||||
right: clamp(0.75rem, 3vw, 2rem);
|
||||
bottom: 1.25rem;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.corner-gallery__image {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
object-fit: contain;
|
||||
object-position: right bottom;
|
||||
opacity: 0.95;
|
||||
filter: drop-shadow(0 18px 36px rgba(15, 23, 42, 0.18));
|
||||
}
|
||||
|
||||
.list-page {
|
||||
padding: 1.5rem 0 2rem;
|
||||
}
|
||||
|
||||
.page-intro {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.page-intro h1 {
|
||||
margin: 0.35rem 0 0.8rem;
|
||||
font-family: 'Archivo', sans-serif;
|
||||
font-size: clamp(2rem, 4vw, 3.3rem);
|
||||
}
|
||||
|
||||
.page-intro p {
|
||||
margin: 0;
|
||||
max-width: 44rem;
|
||||
color: var(--muted);
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.prose {
|
||||
padding: 2rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.prose h1,
|
||||
.prose h2,
|
||||
.prose h3 {
|
||||
font-family: 'Archivo', sans-serif;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.prose p,
|
||||
.prose li,
|
||||
.prose blockquote {
|
||||
color: var(--muted-strong);
|
||||
line-height: 1.85;
|
||||
}
|
||||
|
||||
.prose pre,
|
||||
.prose code {
|
||||
font-family: 'Fira Code', monospace;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 2rem 0 1rem;
|
||||
}
|
||||
|
||||
.footer-card {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1.2rem 1.4rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.footer-card p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.hero-grid,
|
||||
.stack-grid,
|
||||
.activity-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.site-header {
|
||||
border-radius: 1.25rem;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.page-shell {
|
||||
padding-bottom: 3rem;
|
||||
}
|
||||
|
||||
.hero-card,
|
||||
.activity-panel,
|
||||
.card,
|
||||
.prose {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.card-grid,
|
||||
.status-panel__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.section-heading,
|
||||
.footer-card {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.6rem;
|
||||
}
|
||||
|
||||
.corner-gallery {
|
||||
right: 0.6rem;
|
||||
bottom: 0.6rem;
|
||||
}
|
||||
|
||||
.corner-gallery__image {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user