余白 UI:按设计稿模块化还原 + 站主自定义 CSS + 详情 neighbors + 静态缓存头
前端(新增 ui/yohaku/,各视图按 ui_id=vivid 分支渲染;classic 与后台不受影响) - 令牌层:设计稿数值原样落地,28 个 --v-* 为站主接口(自定义 CSS 可覆盖, 放 @layer 让出优先级);另加防线,中和 styles.css 裸选择器 (h1 衬线 / body 行高 / 后台 .stat/.toolbar/.chip 卡片)漏进余白元素 - 顶栏(滚动收缩态)/ 阅读进度(顶部 2px + READ n%)/ 页脚 - 首页:hero(统计条)/ 筛选(类型分段 + 标签 chips + 搜索)/ 长文卡片与短文纸条 / 分页 / 作品展示区(3 件 + 查看所有,参照 diygod.cc); 面板栅格改 1.6fr 1fr 1fr 且高度自适应内容(标签云明显大一圈) - 文章页:三栏「纸」—— 左时间线(前后各两篇)/ 中纸张 / 右目录与阅读进度; 短文详情不显示标题(h1 视觉隐藏保留无障碍) - 全部规则收敛在 html[data-ui='vivid'] 前缀下;无 !important 后端 - GET /api/posts/:slug 增 neighbors(前后各两篇,详情页侧栏时间线用) - SPA 缓存头:index.html no-cache,assets/ 带 hash 长期 immutable (此前无缓存头,浏览器会一直拿旧构建) 其他 - 站主自定义 CSS 管线(customCss.js + ui/sections.js,按分区注入,仅 vivid) - 删除旧 vivid 覆盖层实现(VividNav/Hero/Footer/Timeline/Toc、ui/vivid.css) - 设计稿原型附于仓库根 index.html;作品表清掉外链 diygod.cc 的示例封面, 改用本地占位图(public/covers/)
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
// 自定义 CSS 的分区清单。
|
||||
//
|
||||
// 必须与后端 store.ValidCSSSections(backend/internal/store/store.go)保持一致 ——
|
||||
// 后端会丢掉不在白名单里的分区,两边不一致会出现「保存了但不生效」。
|
||||
export const SECTIONS = [
|
||||
{ id: 'global', label: '全局', hint: '每个前台页面都会加载,适合放变量和通用微调' },
|
||||
{ id: 'home', label: '首页', hint: '时间线 / 标签筛选页' },
|
||||
{ id: 'post', label: '文章页', hint: '文章详情,含短文' },
|
||||
{ id: 'archive', label: '归档', hint: '' },
|
||||
{ id: 'tags', label: '标签', hint: '标签云与单个标签下的列表' },
|
||||
{ id: 'projects', label: '作品', hint: '' },
|
||||
{ id: 'about', label: '关于', hint: '' }
|
||||
]
|
||||
|
||||
export const SECTION_IDS = SECTIONS.map((s) => s.id)
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { site } from '../../site'
|
||||
|
||||
// 余白关于页。对应设计稿 viewAbout():一张 about-card(kicker + 姓名 + 身份 + 简介 + 四个事实)。
|
||||
// 四个数字与首页头版同源,由调用方传入,避免两处各算一遍。
|
||||
|
||||
const props = defineProps({
|
||||
postCount: { type: Number, default: 0 },
|
||||
tagCount: { type: Number, default: 0 },
|
||||
projectCount: { type: Number, default: 0 },
|
||||
longCount: { type: Number, default: 0 }
|
||||
})
|
||||
|
||||
const facts = computed(() => [
|
||||
{ n: props.postCount, label: '篇文章' },
|
||||
{ n: props.tagCount, label: '个标签' },
|
||||
{ n: props.projectCount, label: '件作品' },
|
||||
{ n: props.longCount, label: '长文' }
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wrap wrap-lead">
|
||||
<div class="about-card">
|
||||
<div class="kicker kicker-block">About</div>
|
||||
<h1>{{ site.author_name }}</h1>
|
||||
<div class="role">Writer · Builder</div>
|
||||
<p class="bio">{{ site.author_bio }}</p>
|
||||
<div class="about-facts">
|
||||
<div v-for="f in facts" :key="f.label">
|
||||
<b>{{ f.n }}</b><span>{{ f.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,48 @@
|
||||
<script setup>
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
// 余白归档。对应设计稿 viewArchive():page-head + 年块(年 + 篇数)+ 月分组。
|
||||
// 短文没有标题,列表里用正文开头(后端对 short 返回全文 content_html,够用)。
|
||||
|
||||
const props = defineProps({ years: { type: Array, default: () => [] } })
|
||||
|
||||
const fmt = new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' })
|
||||
const fmtDate = (iso) => fmt.format(new Date(iso)).replace(/\//g, '-')
|
||||
const kindLabel = (k) => (k === 'long' ? '长文' : '短文')
|
||||
const plain = (html) => {
|
||||
if (!html) return ''
|
||||
const el = document.createElement('div')
|
||||
el.innerHTML = html
|
||||
return (el.textContent || '').trim()
|
||||
}
|
||||
const titleOf = (p) => (p.kind === 'long' ? p.title : plain(p.content_html).slice(0, 48))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-head">
|
||||
<div class="kicker">Archive</div>
|
||||
<h1>沿着时间<em>往回走</em></h1>
|
||||
<p>按年 → 月分组,来自 <code>GET /api/archive</code>。</p>
|
||||
</div>
|
||||
|
||||
<div class="wrap wrap-flush">
|
||||
<div v-for="y in props.years" :key="y.year" class="year-block">
|
||||
<div class="year-head">
|
||||
<span class="y">{{ y.year }}</span>
|
||||
<span class="c">{{ y.count }} 篇</span>
|
||||
</div>
|
||||
<div v-for="m in y.months" :key="m.month" class="month">
|
||||
<h3>{{ y.year }} / {{ m.month }}</h3>
|
||||
<ul>
|
||||
<li v-for="p in m.posts" :key="p.id || p.slug">
|
||||
<RouterLink :to="`/post/${encodeURIComponent(p.slug)}`">
|
||||
<span class="d">{{ fmtDate(p.published_at).slice(5) }}</span>
|
||||
<span class="kd" :class="p.kind">{{ kindLabel(p.kind) }}</span>
|
||||
<span class="t">{{ titleOf(p) }}</span>
|
||||
</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,193 @@
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { sanitizeHtml } from '../../utils'
|
||||
|
||||
// 余白文章页。对应设计稿 viewPost() 的三栏「纸」:
|
||||
// 左 side-l : 时间线(当前篇前后各两篇,标签来自位置)
|
||||
// 中 main : back + article.paper(元信息 / 标题 / 导语 / 正文 / 标签)
|
||||
// 右 side-r : 目录(正文 h2)+ 阅读进度
|
||||
// 数据与消毒都由 PostView 负责(contentHtml 已注入 h2 id),这里只做呈现与交互。
|
||||
|
||||
const props = defineProps({
|
||||
post: { type: Object, default: null },
|
||||
headings: { type: Array, default: () => [] },
|
||||
contentHtml: { type: String, default: '' },
|
||||
neighbors: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false },
|
||||
error: { type: String, default: '' }
|
||||
})
|
||||
|
||||
const fmtDate = (iso) => {
|
||||
const d = new Date(iso)
|
||||
const p = (n) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`
|
||||
}
|
||||
const nf = new Intl.NumberFormat('zh-CN')
|
||||
const plain = (html) => {
|
||||
const el = document.createElement('div')
|
||||
el.innerHTML = html || ''
|
||||
return (el.textContent || '').trim()
|
||||
}
|
||||
|
||||
const isShort = computed(() => props.post?.kind === 'short')
|
||||
const title = computed(() =>
|
||||
isShort.value ? plain(props.post?.content_html).slice(0, 40) + '…' : props.post?.title || ''
|
||||
)
|
||||
|
||||
/* 左栏时间线:后端已按时间线顺序给好(最旧在前、最新在后),
|
||||
* 找到当前篇,按相对位置打标签 : 与设计稿的 更早/上一篇/当前/下一篇/更新 一致。 */
|
||||
const tl = computed(() => {
|
||||
const nb = props.neighbors || []
|
||||
const idx = nb.findIndex((x) => x.slug === props.post?.slug)
|
||||
if (idx < 0) return []
|
||||
const label = (d) => (d === 0 ? '当前' : d === -1 ? '下一篇' : d === -2 ? '更新' : d === 1 ? '上一篇' : '更早')
|
||||
return nb.map((p, i) => ({
|
||||
label: label(i - idx),
|
||||
now: i === idx,
|
||||
date: fmtDate(p.published_at),
|
||||
text: p.kind === 'long' ? p.title : plain(p.content_html).slice(0, 20) + '…',
|
||||
slug: p.slug
|
||||
}))
|
||||
})
|
||||
|
||||
/* ---------- 阅读进度 + 目录高亮 + 右栏 meter ---------- */
|
||||
const pbar = ref(null)
|
||||
const ppct = ref(null)
|
||||
const meterFill = ref(null)
|
||||
const meterNum = ref(null)
|
||||
const tocEl = ref(null)
|
||||
let ticking = false
|
||||
|
||||
function onScroll() {
|
||||
if (ticking) return
|
||||
ticking = true
|
||||
requestAnimationFrame(() => {
|
||||
ticking = false
|
||||
const h = document.documentElement.scrollHeight - window.innerHeight
|
||||
const p = h > 0 ? Math.min(100, Math.max(0, (window.scrollY / h) * 100)) : 0
|
||||
if (pbar.value) pbar.value.querySelector('i').style.width = p.toFixed(1) + '%'
|
||||
if (ppct.value) ppct.value.querySelector('b').textContent = String(Math.round(p))
|
||||
if (meterFill.value) meterFill.value.style.width = p.toFixed(1) + '%'
|
||||
if (meterNum.value) meterNum.value.textContent = Math.round(p) + '%'
|
||||
// 目录高亮:视口 120px 以上最后一个 h2
|
||||
let active = null
|
||||
document.querySelectorAll('.prose h2').forEach((h) => {
|
||||
if (h.getBoundingClientRect().top < 120) active = h
|
||||
})
|
||||
if (tocEl.value) {
|
||||
tocEl.value.querySelectorAll('a').forEach((a) => {
|
||||
a.classList.toggle('on', !!active && a.getAttribute('href') === '#' + active.id)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', onScroll, { passive: true })
|
||||
onScroll()
|
||||
})
|
||||
onBeforeUnmount(() => window.removeEventListener('scroll', onScroll))
|
||||
|
||||
/* 弹簧滚动:TOC 点击用(设计稿 springScrollTo 的同款手感) */
|
||||
function springScrollTo(y) {
|
||||
const start = window.scrollY
|
||||
const dist = y - start
|
||||
if (Math.abs(dist) < 2) return
|
||||
const dur = Math.min(700, Math.max(350, Math.abs(dist) * 0.4))
|
||||
const t0 = performance.now()
|
||||
const overshoot = 1.15
|
||||
function frame(t) {
|
||||
const p = Math.min(1, (t - t0) / dur)
|
||||
const e = 1 - Math.pow(1 - p, 3)
|
||||
const wobble = p < 1 ? Math.sin(p * Math.PI) * overshoot * 0.06 * (1 - p) : 0
|
||||
window.scrollTo(0, start + dist * (e + wobble))
|
||||
if (p < 1) requestAnimationFrame(frame)
|
||||
else window.scrollTo(0, y)
|
||||
}
|
||||
requestAnimationFrame(frame)
|
||||
}
|
||||
|
||||
function onTocClick(e) {
|
||||
const a = e.target.closest('a')
|
||||
if (!a) return
|
||||
const id = a.getAttribute('href')
|
||||
if (!id || !id.startsWith('#h-')) return
|
||||
e.preventDefault()
|
||||
const el = document.getElementById(id.slice(1))
|
||||
if (el) springScrollTo(el.getBoundingClientRect().top + window.scrollY - 80)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 顶部阅读进度:只在这类长页出现 -->
|
||||
<div class="progress" ref="pbar"><i /></div>
|
||||
<div class="progress-pct">READ <b>0</b>%</div>
|
||||
|
||||
<div v-if="props.loading" class="wrap"><div class="empty"><h3>载入中…</h3></div></div>
|
||||
|
||||
<div v-else-if="props.error" class="wrap">
|
||||
<div class="empty"><h3>出错了</h3><p>{{ props.error }}</p><a class="chip on" href="/">回时间线</a></div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!props.post" class="wrap">
|
||||
<div class="empty"><h3>没有这篇</h3><p>链接可能过期了。</p><a class="chip on" href="/">回时间线</a></div>
|
||||
</div>
|
||||
|
||||
<div v-else class="article-layout">
|
||||
<aside class="side-l">
|
||||
<div class="tl">
|
||||
<div
|
||||
v-for="x in tl"
|
||||
:key="x.slug + x.label"
|
||||
class="tl-item"
|
||||
:class="{ now: x.now }"
|
||||
>
|
||||
<b>{{ x.label }}</b>
|
||||
<time>{{ x.date }}</time>
|
||||
<div>{{ x.text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main>
|
||||
<a class="back" href="/">← 返回时间线</a>
|
||||
<article class="paper">
|
||||
<div class="meta">
|
||||
<span class="badge" :class="props.post.kind">{{ props.post.kind.toUpperCase() }}</span>
|
||||
<time :datetime="props.post.published_at">{{ fmtDate(props.post.published_at) }}</time>
|
||||
<span>· 约 {{ props.post.reading_minutes }} 分钟</span>
|
||||
<span>· {{ nf.format(props.post.content_len || 0) }} 字</span>
|
||||
</div>
|
||||
|
||||
<template v-if="!isShort">
|
||||
<h1 class="serif">{{ props.post.title }}</h1>
|
||||
<p v-if="props.post.summary" class="dek">{{ props.post.summary }}</p>
|
||||
</template>
|
||||
<!-- 短文不渲染标题:正文第一句就是全部,再给个截断标题是重复 -->
|
||||
<h1 v-if="isShort" class="sr-only">{{ title }}</h1>
|
||||
|
||||
<div class="prose" v-html="sanitizeHtml(props.contentHtml)" />
|
||||
|
||||
<div class="end">
|
||||
<span class="lbl">标签</span>
|
||||
<RouterLink v-for="t in props.post.tags || []" :key="t" class="tg" to="/tags">{{ t }}</RouterLink>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<aside class="side-r">
|
||||
<nav class="toc" ref="tocEl" aria-label="本文目录">
|
||||
<h4>目录</h4>
|
||||
<ol>
|
||||
<li v-for="h in props.headings" :key="h.id">
|
||||
<a :href="`#${h.id}`" @click="onTocClick">{{ h.text }}</a>
|
||||
</li>
|
||||
</ol>
|
||||
<ol v-if="!props.headings.length"><li><a>(本文无小标题)</a></li></ol>
|
||||
</nav>
|
||||
<div class="read-meter">
|
||||
<div class="bar"><i ref="meterFill" /></div>
|
||||
<div class="num"><span>阅读进度</span><b ref="meterNum">0%</b></div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,65 @@
|
||||
<script setup>
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
// 余白列表。对应设计稿的 <div class="feed">:长文是卡片(封面 + 元信息 + 标题 + 摘要 + 标签),
|
||||
// 短文是纸条(直接铺正文)。两种密度混在同一条时间线里,这是设计稿的主张。
|
||||
|
||||
const props = defineProps({
|
||||
items: { type: Array, default: () => [] }
|
||||
})
|
||||
|
||||
const fmt = new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' })
|
||||
const fmtDate = (iso) => fmt.format(new Date(iso)).replace(/\//g, '-')
|
||||
const fmtDateTime = (iso) => {
|
||||
const d = new Date(iso)
|
||||
const p = (n) => String(n).padStart(2, '0')
|
||||
return `${fmtDate(iso)} ${p(d.getHours())}:${p(d.getMinutes())}`
|
||||
}
|
||||
const nf = new Intl.NumberFormat('zh-CN')
|
||||
// 短文没有标题,列表里用正文开头当标题(设计稿同此处理)
|
||||
const plain = (html) => {
|
||||
if (!html) return ''
|
||||
const el = document.createElement('div')
|
||||
el.innerHTML = html
|
||||
return (el.textContent || '').trim()
|
||||
}
|
||||
const shortTitle = (p) => plain(p.content_html).slice(0, 40) + '…'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="feed">
|
||||
<template v-for="p in props.items" :key="p.id || p.slug">
|
||||
<!-- 长文:卡片 -->
|
||||
<RouterLink v-if="p.kind === 'long'" class="card" :to="`/post/${encodeURIComponent(p.slug)}`">
|
||||
<div v-if="p.cover_url" class="cov">
|
||||
<img :src="p.cover_url" alt="" loading="lazy">
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="meta">
|
||||
<span class="badge long">LONG</span>
|
||||
<time :datetime="p.published_at">{{ fmtDate(p.published_at) }}</time>
|
||||
<span>· 约 {{ p.reading_minutes }} 分钟</span>
|
||||
<span>· {{ nf.format(p.content_len || 0) }} 字</span>
|
||||
</div>
|
||||
<h2>{{ p.title }}</h2>
|
||||
<p class="sum">{{ p.summary }}</p>
|
||||
<div class="tags">
|
||||
<span v-for="t in p.tags || []" :key="t" class="tg">{{ t }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</RouterLink>
|
||||
|
||||
<!-- 短文:纸条,正文直接铺开 -->
|
||||
<RouterLink v-else class="note" :to="`/post/${encodeURIComponent(p.slug)}`">
|
||||
<div class="meta">
|
||||
<span class="badge short">SHORT</span>
|
||||
<time :datetime="p.published_at">{{ fmtDateTime(p.published_at) }}</time>
|
||||
</div>
|
||||
<div class="body" v-html="p.content_html" />
|
||||
<div class="tags">
|
||||
<span v-for="t in p.tags || []" :key="t" class="tg">{{ t }}</span>
|
||||
</div>
|
||||
</RouterLink>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup>
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { site } from '../../site'
|
||||
|
||||
// 余白页脚。对应设计稿的 <footer class="foot">。
|
||||
//
|
||||
// 设计稿底部正中还有一枚 <div class="switch">「余白 / 当前主题」的药丸,
|
||||
// 按站主要求去掉了(它只标识当前主题名,本站没有多套主题,属于纯装饰,
|
||||
// 而且固定在底部会挡住内容)。对应的 .switch 样式也一并从
|
||||
// chrome.css / pages.css 里删除,避免留下死规则。
|
||||
|
||||
const links = [
|
||||
{ to: '/', label: '时间线' },
|
||||
{ to: '/archive', label: '归档' },
|
||||
{ to: '/tags', label: '标签' },
|
||||
{ to: '/projects', label: '作品' },
|
||||
{ to: '/about', label: '关于' }
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="foot">
|
||||
<div class="quote">What’s left unsaid holds the most weight.</div>
|
||||
<div>{{ site.footer_note }}<template v-if="site.icp"> · {{ site.icp }}</template></div>
|
||||
<div class="foot-nav">
|
||||
<template v-for="(l, i) in links" :key="l.to">
|
||||
<span v-if="i" class="sep">·</span>
|
||||
<RouterLink :to="l.to">{{ l.label }}</RouterLink>
|
||||
</template>
|
||||
<span class="sep">·</span>
|
||||
<a href="/rss.xml">RSS</a>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
@@ -0,0 +1,38 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { site } from '../../site'
|
||||
|
||||
// 余白头版。对应设计稿的 <section class="hero">。
|
||||
// 四个数字全部由传入的数据算出来,不写死 : 设计稿里也是算的。
|
||||
|
||||
const props = defineProps({
|
||||
postCount: { type: Number, default: 0 },
|
||||
charCount: { type: Number, default: 0 },
|
||||
tagCount: { type: Number, default: 0 },
|
||||
projectCount: { type: Number, default: 0 }
|
||||
})
|
||||
|
||||
const nf = new Intl.NumberFormat('zh-CN')
|
||||
const stats = computed(() => [
|
||||
{ n: props.postCount, label: '篇文章' },
|
||||
{ n: props.charCount, label: '累计字数' },
|
||||
{ n: props.tagCount, label: '个标签' },
|
||||
{ n: props.projectCount, label: '件作品' }
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="hero">
|
||||
<div class="kicker">Personal Site · 在余白处书写</div>
|
||||
<h1>把想法写下来,<br><span class="hl">留白也是内容</span>。</h1>
|
||||
<p class="lead">
|
||||
{{ site.site_desc }}<br>
|
||||
<strong>全部内容自托管</strong>,没有推荐算法,只有时间线。
|
||||
</p>
|
||||
<div class="stats">
|
||||
<div v-for="s in stats" :key="s.label" class="stat">
|
||||
<b>{{ nf.format(s.n) }}</b><span>{{ s.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,89 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import YohakuHero from './YohakuHero.vue'
|
||||
import YohakuToolbar from './YohakuToolbar.vue'
|
||||
import YohakuFeed from './YohakuFeed.vue'
|
||||
import YohakuPager from './YohakuPager.vue'
|
||||
|
||||
// 首页装配。对应设计稿 viewHome() 的 DOM 顺序:
|
||||
// hero → wrap(toolbar + feed + empty + pager) → panels。
|
||||
// 数据与筛选状态由 HomeView 持有(它读路由 query),这里只做呈现。
|
||||
|
||||
const props = defineProps({
|
||||
items: { type: Array, default: () => [] },
|
||||
total: { type: Number, default: 0 },
|
||||
page: { type: Number, default: 1 },
|
||||
maxPage: { type: Number, default: 1 },
|
||||
kind: { type: String, default: '' },
|
||||
tag: { type: String, default: '' },
|
||||
query: { type: String, default: '' },
|
||||
tags: { type: Array, default: () => [] },
|
||||
projects: { type: Array, default: () => [] },
|
||||
statPosts: { type: Number, default: 0 },
|
||||
statChars: { type: Number, default: 0 },
|
||||
statTags: { type: Number, default: 0 },
|
||||
statProjects: { type: Number, default: 0 }
|
||||
})
|
||||
|
||||
// 展示区只放前 3 件(diygod.cc 的做法:部分 + 查看所有)
|
||||
const showcase = computed(() => (props.projects || []).slice(0, 3))
|
||||
const emit = defineEmits(['update:kind', 'update:tag', 'update:query', 'go'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<YohakuHero
|
||||
:post-count="statPosts"
|
||||
:char-count="statChars"
|
||||
:tag-count="statTags"
|
||||
:project-count="statProjects"
|
||||
/>
|
||||
|
||||
|
||||
<div class="wrap">
|
||||
<YohakuToolbar
|
||||
:kind="kind"
|
||||
:tag="tag"
|
||||
:query="query"
|
||||
:tags="tags"
|
||||
@update:kind="emit('update:kind', $event)"
|
||||
@update:tag="emit('update:tag', $event)"
|
||||
@update:query="emit('update:query', $event)"
|
||||
/>
|
||||
|
||||
<YohakuFeed :items="items" />
|
||||
|
||||
<div v-if="!items.length" class="empty">
|
||||
<h3>这里还没有落笔</h3>
|
||||
<p>换个筛选或搜索词试试。</p>
|
||||
<button class="chip on" type="button" @click="emit('update:kind', ''), emit('update:tag', ''), emit('update:query', '')">看全部</button>
|
||||
</div>
|
||||
|
||||
<YohakuPager :page="page" :max-page="maxPage" :total="total" @go="emit('go', $event)" />
|
||||
</div>
|
||||
|
||||
<section class="showcase">
|
||||
<div class="show-head">
|
||||
<h2 class="serif">做过的东西</h2>
|
||||
<span class="en mono">Projects</span>
|
||||
<a class="push mono" href="/projects">全部作品 →</a>
|
||||
</div>
|
||||
<div class="proj-grid">
|
||||
<RouterLink
|
||||
v-for="p in showcase"
|
||||
:key="p.id || p.slug"
|
||||
class="proj"
|
||||
to="/projects"
|
||||
>
|
||||
<div class="cov">
|
||||
<img v-if="p.cover_url" :src="p.cover_url" alt="" loading="lazy">
|
||||
<span v-if="p.status" class="st">{{ p.status }}</span>
|
||||
</div>
|
||||
<h3>{{ p.title }}</h3>
|
||||
<p>{{ p.summary }}</p>
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div class="show-more">
|
||||
<a class="chip on" href="/projects">查看所有作品</a>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script setup>
|
||||
// 余白分页。对应设计稿的 <div class="pager">。设计稿只列「上一页 / 当前 / 下一页 / 共 n 篇」,
|
||||
// 不做页码列表 : 保持原样。
|
||||
|
||||
const props = defineProps({
|
||||
page: { type: Number, default: 1 },
|
||||
maxPage: { type: Number, default: 1 },
|
||||
total: { type: Number, default: 0 }
|
||||
})
|
||||
const emit = defineEmits(['go'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="props.total" class="pager">
|
||||
<template v-if="props.maxPage > 1">
|
||||
<button type="button" :disabled="props.page <= 1" @click="emit('go', props.page - 1)">← 上一页</button>
|
||||
<span class="cur">{{ props.page }} / {{ props.maxPage }}</span>
|
||||
<button type="button" :disabled="props.page >= props.maxPage" @click="emit('go', props.page + 1)">下一页 →</button>
|
||||
</template>
|
||||
<span class="info">共 {{ props.total }} 篇</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup>
|
||||
// 余白作品。对应设计稿 viewProjects():page-head + proj-grid,
|
||||
// 每件是封面 + 状态角标 + 标题 + 说明 + 外链/仓库两行。
|
||||
|
||||
const props = defineProps({ projects: { type: Array, default: () => [] } })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-head">
|
||||
<div class="kicker">Projects</div>
|
||||
<h1>做过的<em>东西</em></h1>
|
||||
<p>来自 <code>GET /api/projects</code>,可挂外链与仓库。</p>
|
||||
</div>
|
||||
|
||||
<div class="wrap wrap-flush">
|
||||
<div class="proj-grid">
|
||||
<article v-for="p in props.projects" :key="p.id || p.slug" class="proj">
|
||||
<!-- 封面块只在有图时渲染,否则会留下 150px 的空色块 -->
|
||||
<div v-if="p.cover_url" class="cov">
|
||||
<img :src="p.cover_url" alt="" loading="lazy">
|
||||
<span v-if="p.status" class="st">{{ p.status }}</span>
|
||||
</div>
|
||||
<div class="body">
|
||||
<h3>
|
||||
{{ p.title }}
|
||||
<span v-if="!p.cover_url && p.status" class="st st-plain">{{ p.status }}</span>
|
||||
</h3>
|
||||
<p>{{ p.summary }}</p>
|
||||
<div class="acts">
|
||||
<a v-if="p.url" :href="p.url" target="_blank" rel="noopener noreferrer">↗ 主页</a>
|
||||
<a v-if="p.repo_url" :href="p.repo_url" target="_blank" rel="noopener noreferrer">⌥ 仓库</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
// 余白标签云。对应设计稿 viewTags():page-head + tag-grid,每格是一个 tag-tile :
|
||||
// 大号计数 + 色块(颜色来自接口的 color 字段)+ 名字 + 「n 篇 · /tag/slug」。
|
||||
// 点格子回首页并按该标签筛选(设计稿就是这么做的)。
|
||||
|
||||
const props = defineProps({ tags: { type: Array, default: () => [] } })
|
||||
const router = useRouter()
|
||||
|
||||
function open(t) {
|
||||
router.push({ path: '/', query: { tag: t.name } })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-head">
|
||||
<div class="kicker">Tags</div>
|
||||
<h1>标签<em>如常</em></h1>
|
||||
<p>颜色来自接口的 <code>color</code>,计数来自 <code>count</code>。</p>
|
||||
</div>
|
||||
|
||||
<div class="wrap wrap-flush">
|
||||
<div class="tag-grid">
|
||||
<a
|
||||
v-for="t in props.tags"
|
||||
:key="t.slug || t.name"
|
||||
class="tag-tile"
|
||||
href="#"
|
||||
:style="{ '--tc': t.color || 'var(--accent)' }"
|
||||
@click.prevent="open(t)"
|
||||
>
|
||||
<span class="big">{{ t.count }}</span>
|
||||
<div class="sw" />
|
||||
<span class="nm">{{ t.name }}</span>
|
||||
<span class="ct">{{ t.count }} 篇 · /tag/{{ t.slug }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,69 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
// 余白筛选条。对应设计稿的 <div class="toolbar">:类型分段 + 标签 chips + 搜索。
|
||||
// 状态不在组件里 : 全部走路由 query(kind / tag / q / page),
|
||||
// 这样筛选结果可以直接分享,也和后端分页对得上。
|
||||
|
||||
const props = defineProps({
|
||||
kind: { type: String, default: '' },
|
||||
tag: { type: String, default: '' },
|
||||
query: { type: String, default: '' },
|
||||
tags: { type: Array, default: () => [] }
|
||||
})
|
||||
const emit = defineEmits(['update:kind', 'update:tag', 'update:query'])
|
||||
|
||||
const kinds = [
|
||||
{ value: '', label: '全部' },
|
||||
{ value: 'long', label: '长文' },
|
||||
{ value: 'short', label: '短文' }
|
||||
]
|
||||
|
||||
const draft = ref(props.query)
|
||||
function submit() {
|
||||
emit('update:query', draft.value.trim())
|
||||
}
|
||||
function pickKind(v) {
|
||||
// 设计稿里点类型会清掉标签:「全部」是真正的复位
|
||||
emit('update:tag', '')
|
||||
emit('update:kind', v)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="toolbar">
|
||||
<div class="seg" role="tablist" aria-label="按类型筛选">
|
||||
<button
|
||||
v-for="k in kinds"
|
||||
:key="k.label"
|
||||
type="button"
|
||||
role="tab"
|
||||
:aria-selected="props.kind === k.value && !props.tag"
|
||||
:class="{ on: props.kind === k.value && !props.tag }"
|
||||
@click="pickKind(k.value)"
|
||||
>{{ k.label }}</button>
|
||||
</div>
|
||||
|
||||
<div class="chips">
|
||||
<button
|
||||
v-for="t in props.tags"
|
||||
:key="t.slug || t.name"
|
||||
type="button"
|
||||
class="chip"
|
||||
:class="{ on: props.tag === t.name }"
|
||||
@click="emit('update:tag', props.tag === t.name ? '' : t.name)"
|
||||
>{{ t.name }}</button>
|
||||
</div>
|
||||
|
||||
<label class="search">
|
||||
<input
|
||||
v-model="draft"
|
||||
type="search"
|
||||
placeholder="搜索…"
|
||||
aria-label="搜索文章"
|
||||
@keydown.enter.prevent="submit"
|
||||
>
|
||||
<button type="button" aria-label="搜索" @click="submit">⌕</button>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,61 @@
|
||||
<script setup>
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { RouterLink, useRoute } from 'vue-router'
|
||||
import { site } from '../../site'
|
||||
|
||||
// 余白顶栏。对应设计稿的 <header class="top">:品牌 / 导航 / 操作三块,
|
||||
// 滚动过 8px 加 .scrolled(设计稿的「四态里的第一态」)。
|
||||
// 样式在 ui/yohaku/chrome.css,选择器已收敛到 html[data-ui="vivid"] 下。
|
||||
|
||||
const route = useRoute()
|
||||
const scrolled = ref(false)
|
||||
|
||||
const links = [
|
||||
{ to: '/', label: '时间线' },
|
||||
{ to: '/archive', label: '归档' },
|
||||
{ to: '/tags', label: '标签' },
|
||||
{ to: '/projects', label: '作品' },
|
||||
{ to: '/about', label: '关于' }
|
||||
]
|
||||
|
||||
const initial = () => (site.author_name || site.site_title || 'O').trim().slice(0, 1).toUpperCase()
|
||||
|
||||
function onScroll() {
|
||||
scrolled.value = (window.scrollY || 0) > 8
|
||||
}
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', onScroll, { passive: true })
|
||||
onScroll()
|
||||
})
|
||||
onBeforeUnmount(() => window.removeEventListener('scroll', onScroll))
|
||||
|
||||
// 设计稿里主题键是 ◐ / ◑:亮色时显示 ◐(点它去暗),暗色时显示 ◑。
|
||||
function toggleTheme() {
|
||||
// site.themeMode 是站点级偏好(auto / light / dark),site.js 里有 watcher 负责落盘与写 data-theme。
|
||||
// 这里直接改它,不自己碰 document : 单一数据源。
|
||||
const dark = document.documentElement.getAttribute('data-theme') === 'vivid-dark'
|
||||
site.themeMode = dark ? 'light' : 'dark'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="top" :class="{ scrolled }">
|
||||
<div class="top-in">
|
||||
<RouterLink class="brand" to="/">
|
||||
<span class="av">{{ initial() }}</span>
|
||||
<span>{{ site.site_title?.split(/[·|]/)[0]?.trim() || 'ONE' }}</span>
|
||||
</RouterLink>
|
||||
<nav class="nav" aria-label="站点导航">
|
||||
<RouterLink
|
||||
v-for="l in links"
|
||||
:key="l.to"
|
||||
:to="l.to"
|
||||
:class="{ on: l.to === '/' ? route.path === '/' : route.path.startsWith(l.to) }"
|
||||
>{{ l.label }}</RouterLink>
|
||||
</nav>
|
||||
<div class="top-acts">
|
||||
<button class="icon-btn" type="button" title="切换明暗" aria-label="切换明暗" @click="toggleTheme">◐</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
@@ -0,0 +1,182 @@
|
||||
/* ONE · 余白(yohaku)· 由设计稿按节拆分 / 窄屏
|
||||
*
|
||||
* 由设计稿(仓库根 index.html 的 <style>)按它自己的分节原样拆分。
|
||||
* 只做了一件事:给每条规则加 html[data-ui='vivid'] 前缀(@media 内部的
|
||||
* 规则也逐条加,at-rule 本身不加),并把 :root[data-theme="dark"] 映射到
|
||||
* 本站的 vivid-dark。数值一律未改,规则也没有被拆开过。
|
||||
*
|
||||
* 前缀是硬要求:组件 scoped 样式编译后是 .foo[data-v-HASH](0,2,0),
|
||||
* html[data-ui='vivid'] .foo 是(0,2,1)才能稳定取胜;scoped 是懒加载
|
||||
* chunk,入口 CSS 之后才到位,平手时按源码顺序反而会赢。
|
||||
*/
|
||||
|
||||
|
||||
/* ---- 设计稿分节:详情:三栏「纸」 ---- */
|
||||
/* ============================================================
|
||||
详情:三栏「纸」· 左时间线 / 中纸张 / 右目录+进度
|
||||
============================================================ */
|
||||
html[data-ui='vivid'] .article-layout{
|
||||
max-width:1100px;margin:0 auto;padding:36px 28px 60px;
|
||||
display:grid;
|
||||
grid-template-columns:140px minmax(0,1fr) 180px;
|
||||
gap:32px;align-items:start;
|
||||
}
|
||||
@media (max-width:960px) {
|
||||
html[data-ui='vivid'] .article-layout{grid-template-columns:1fr;gap:0}
|
||||
html[data-ui='vivid'] .side-l, html[data-ui='vivid'] .side-r{display:none}
|
||||
html[data-ui='vivid'] .progress-pct{display:none}
|
||||
}
|
||||
/* 左:时间线 */
|
||||
html[data-ui='vivid'] .side-l{position:sticky;top:80px;padding-top:8px}
|
||||
html[data-ui='vivid'] .tl{position:relative;padding-left:14px}
|
||||
html[data-ui='vivid'] .tl::before{
|
||||
content:"";position:absolute;left:3px;top:6px;bottom:6px;width:1.5px;
|
||||
background:linear-gradient(180deg,var(--accent),transparent);
|
||||
border-radius:2px;
|
||||
}
|
||||
html[data-ui='vivid'] .tl-item{
|
||||
position:relative;padding:0 0 18px 0;font-size:12px;color:var(--muted);
|
||||
}
|
||||
html[data-ui='vivid'] .tl-item::before{
|
||||
content:"";position:absolute;left:-14px;top:5px;
|
||||
width:7px;height:7px;border-radius:50%;
|
||||
background:var(--card);border:2px solid var(--accent);
|
||||
}
|
||||
html[data-ui='vivid'] .tl-item.now::before{
|
||||
background:var(--accent);
|
||||
box-shadow:0 0 0 4px var(--accent-soft);
|
||||
}
|
||||
html[data-ui='vivid'] .tl-item b{display:block;color:var(--soft);font-weight:700;font-size:12px}
|
||||
html[data-ui='vivid'] .tl-item.now b{color:var(--accent)}
|
||||
/* 中:纸张 */
|
||||
html[data-ui='vivid'] .paper{
|
||||
background:var(--card);
|
||||
border:1px solid var(--line);
|
||||
border-radius:var(--radius-lg);
|
||||
box-shadow:var(--shadow-lg);
|
||||
padding:44px 48px 52px;
|
||||
position:relative;
|
||||
overflow:hidden;
|
||||
animation:paperIn .7s var(--spring) both;
|
||||
}
|
||||
@keyframes paperIn {
|
||||
html[data-ui='vivid'] from{opacity:0;transform:translateY(24px) scale(.985)}
|
||||
html[data-ui='vivid'] to{opacity:1;transform:none}
|
||||
}
|
||||
/* 纸的纹理感:右上角一抹强调色 */
|
||||
html[data-ui='vivid'] .paper::before{
|
||||
content:"";position:absolute;top:0;right:0;
|
||||
width:120px;height:120px;
|
||||
background:radial-gradient(circle at 100% 0,var(--accent-soft),transparent 70%);
|
||||
pointer-events:none;
|
||||
}
|
||||
html[data-ui='vivid'] .paper .meta{
|
||||
display:flex;gap:12px;align-items:center;flex-wrap:wrap;
|
||||
font-size:12.5px;color:var(--muted);font-weight:600;margin-bottom:18px;
|
||||
}
|
||||
html[data-ui='vivid'] .paper h1{
|
||||
margin:0 0 20px;font-size:clamp(28px,4vw,40px);font-weight:800;
|
||||
letter-spacing:-.03em;line-height:1.25;color:var(--ink);
|
||||
}
|
||||
html[data-ui='vivid'] .paper .dek{
|
||||
margin:0 0 32px;font-size:17px;line-height:1.8;color:var(--soft);
|
||||
padding-left:16px;border-left:3px solid var(--accent);
|
||||
font-family:var(--serif);
|
||||
}
|
||||
html[data-ui='vivid'] .prose{font-size:16.5px;line-height:1.95;color:var(--text);max-width:var(--measure)}
|
||||
html[data-ui='vivid'] .prose p{margin:0 0 1.35em}
|
||||
html[data-ui='vivid'] .prose h2{
|
||||
font-size:21px;font-weight:800;margin:2.2em 0 .8em;
|
||||
letter-spacing:-.02em;color:var(--ink);
|
||||
padding-bottom:10px;border-bottom:1px solid var(--line);
|
||||
}
|
||||
html[data-ui='vivid'] .prose a{color:var(--accent);font-weight:600;border-bottom:1.5px solid var(--accent-soft);transition:border-color .25s}
|
||||
html[data-ui='vivid'] .prose a:hover{border-bottom-color:var(--accent)}
|
||||
html[data-ui='vivid'] .prose code{
|
||||
font-family:var(--mono);font-size:.88em;
|
||||
background:var(--accent-soft);color:var(--accent);
|
||||
padding:2px 7px;border-radius:6px;
|
||||
}
|
||||
html[data-ui='vivid'] .prose pre{
|
||||
background:color-mix(in srgb,var(--ink) 92%,var(--accent));
|
||||
color:#e8e8f0;padding:18px 20px;border-radius:12px;
|
||||
overflow-x:auto;font-size:13.5px;line-height:1.7;
|
||||
box-shadow:var(--shadow-lg);
|
||||
}
|
||||
html[data-ui='vivid'] .prose pre code{background:none;padding:0;color:inherit;font-size:inherit}
|
||||
html[data-ui='vivid'] .prose blockquote{
|
||||
margin:1.8em 0;padding:16px 20px;
|
||||
background:var(--accent-soft);
|
||||
border-left:3px solid var(--accent);
|
||||
border-radius:0 12px 12px 0;
|
||||
font-family:var(--serif);font-size:1.05em;
|
||||
color:var(--soft);font-style:italic;
|
||||
}
|
||||
html[data-ui='vivid'] .prose ul{padding-left:1.3em}
|
||||
html[data-ui='vivid'] .prose li{margin-bottom:.55em}
|
||||
html[data-ui='vivid'] .prose hr{border:0;border-top:1px dashed var(--line-strong);margin:2.5em 0}
|
||||
html[data-ui='vivid'] .paper .end{
|
||||
margin-top:40px;padding-top:24px;border-top:1px dashed var(--line-strong);
|
||||
display:flex;gap:8px;flex-wrap:wrap;align-items:center;
|
||||
}
|
||||
html[data-ui='vivid'] .paper .end .lbl{font-size:12px;color:var(--muted);font-weight:700;margin-right:4px}
|
||||
html[data-ui='vivid'] .back{
|
||||
display:inline-flex;align-items:center;gap:6px;
|
||||
font-size:13px;font-weight:700;color:var(--accent);
|
||||
margin-bottom:20px;padding:6px 0;
|
||||
transition:gap .4s var(--spring);
|
||||
}
|
||||
html[data-ui='vivid'] .back:hover{gap:12px}
|
||||
/* 右:目录 + 进度 */
|
||||
html[data-ui='vivid'] .side-r{position:sticky;top:80px;padding-top:8px}
|
||||
html[data-ui='vivid'] .toc h4{
|
||||
margin:0 0 12px;font-size:11px;font-weight:800;
|
||||
letter-spacing:.14em;text-transform:uppercase;color:var(--muted);
|
||||
}
|
||||
html[data-ui='vivid'] .toc ol{list-style:none;margin:0;padding:0;counter-reset:toc}
|
||||
html[data-ui='vivid'] .toc li{counter-increment:toc;margin-bottom:2px}
|
||||
html[data-ui='vivid'] .toc a{
|
||||
display:flex;gap:8px;font-size:12.5px;color:var(--soft);
|
||||
padding:6px 8px;border-radius:8px;line-height:1.4;
|
||||
transition:all .4s var(--spring);
|
||||
}
|
||||
html[data-ui='vivid'] .toc a::before{
|
||||
content:counter(toc,decimal-leading-zero);
|
||||
font-family:var(--mono);font-size:10px;color:var(--muted);
|
||||
flex:none;padding-top:2px;
|
||||
}
|
||||
html[data-ui='vivid'] .toc a:hover, html[data-ui='vivid'] .toc a.on{
|
||||
background:var(--accent-soft);color:var(--accent);transform:translateX(4px);
|
||||
}
|
||||
html[data-ui='vivid'] .toc a:hover::before, html[data-ui='vivid'] .toc a.on::before{color:var(--accent)}
|
||||
html[data-ui='vivid'] .read-meter{
|
||||
margin-top:20px;padding-top:16px;border-top:1px solid var(--line);
|
||||
}
|
||||
html[data-ui='vivid'] .read-meter .bar{
|
||||
height:3px;background:color-mix(in srgb,var(--ink) 8%,transparent);
|
||||
border-radius:999px;overflow:hidden;
|
||||
}
|
||||
html[data-ui='vivid'] .read-meter .bar i{
|
||||
display:block;height:100%;width:0%;
|
||||
background:linear-gradient(90deg,var(--accent),var(--accent-deep));
|
||||
border-radius:999px;transition:width .15s linear;
|
||||
}
|
||||
html[data-ui='vivid'] .read-meter .num{
|
||||
display:flex;justify-content:space-between;
|
||||
font-family:var(--mono);font-size:11px;color:var(--muted);
|
||||
margin-top:8px;font-weight:600;
|
||||
}
|
||||
html[data-ui='vivid'] .read-meter .num b{color:var(--accent)}
|
||||
|
||||
/* 视觉隐藏但保留可访问性:短文详情用它放一个文档级 h1(站主要求短文不显示标题) */
|
||||
html[data-ui='vivid'] .sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/* ONE · 余白(yohaku)· 由设计稿按节拆分 / 窄屏
|
||||
*
|
||||
* 由设计稿(仓库根 index.html 的 <style>)按它自己的分节原样拆分。
|
||||
* 只做了一件事:给每条规则加 html[data-ui='vivid'] 前缀(@media 内部的
|
||||
* 规则也逐条加,at-rule 本身不加),并把 :root[data-theme="dark"] 映射到
|
||||
* 本站的 vivid-dark。数值一律未改,规则也没有被拆开过。
|
||||
*
|
||||
* 前缀是硬要求:组件 scoped 样式编译后是 .foo[data-v-HASH](0,2,0),
|
||||
* html[data-ui='vivid'] .foo 是(0,2,1)才能稳定取胜;scoped 是懒加载
|
||||
* chunk,入口 CSS 之后才到位,平手时按源码顺序反而会赢。
|
||||
*/
|
||||
|
||||
|
||||
/* ---- 设计稿分节:顶部导航 ---- */
|
||||
/* ---- 顶部导航:四态里的第一态(悬浮常规条) ---- */
|
||||
html[data-ui='vivid'] .top{
|
||||
position:sticky;top:0;z-index:100;
|
||||
backdrop-filter:blur(16px) saturate(1.3);
|
||||
-webkit-backdrop-filter:blur(16px) saturate(1.3);
|
||||
background:color-mix(in srgb,var(--paper) 78%,transparent);
|
||||
border-bottom:1px solid transparent;
|
||||
transition:border-color .3s var(--ease),background .3s var(--ease),box-shadow .3s var(--ease);
|
||||
}
|
||||
html[data-ui='vivid'] .top.scrolled{
|
||||
border-bottom-color:var(--line);
|
||||
box-shadow:0 1px 0 var(--line);
|
||||
}
|
||||
html[data-ui='vivid'] .top-in{
|
||||
max-width:1100px;margin:0 auto;
|
||||
padding:0 28px;height:60px;
|
||||
display:flex;align-items:center;gap:20px;
|
||||
}
|
||||
html[data-ui='vivid'] .brand{
|
||||
display:flex;align-items:center;gap:10px;
|
||||
font-weight:700;font-size:15px;letter-spacing:.02em;
|
||||
transition:transform .5s var(--spring);
|
||||
}
|
||||
html[data-ui='vivid'] .brand:hover{transform:scale(1.04)}
|
||||
html[data-ui='vivid'] .brand .av{
|
||||
width:30px;height:30px;border-radius:50%;
|
||||
background:linear-gradient(135deg,var(--accent),var(--accent-deep));
|
||||
display:grid;place-items:center;color:#fff;font-size:13px;font-weight:800;
|
||||
box-shadow:0 0 0 2px var(--paper),0 0 0 3.5px var(--accent-soft);
|
||||
position:relative;
|
||||
}
|
||||
html[data-ui='vivid'] .brand .av::after{
|
||||
content:"";position:absolute;right:-1px;bottom:-1px;
|
||||
width:9px;height:9px;border-radius:50%;
|
||||
background:#34c759;border:2px solid var(--paper);
|
||||
}
|
||||
html[data-ui='vivid'] .nav{display:flex;gap:2px;flex:1;justify-content:center}
|
||||
html[data-ui='vivid'] .nav a{
|
||||
position:relative;font-size:13.5px;font-weight:600;color:var(--soft);
|
||||
padding:8px 14px;border-radius:999px;
|
||||
transition:color .25s var(--ease),background .25s var(--ease),transform .5s var(--spring);
|
||||
}
|
||||
html[data-ui='vivid'] .nav a:hover{color:var(--ink);background:var(--accent-soft);transform:translateY(-1px)}
|
||||
html[data-ui='vivid'] .nav a.on{color:var(--accent)}
|
||||
html[data-ui='vivid'] .nav a.on::after{
|
||||
content:"";position:absolute;left:50%;bottom:4px;
|
||||
width:4px;height:4px;border-radius:50%;
|
||||
background:var(--accent);transform:translateX(-50%);
|
||||
}
|
||||
html[data-ui='vivid'] .top-acts{display:flex;align-items:center;gap:6px}
|
||||
html[data-ui='vivid'] .icon-btn{
|
||||
width:36px;height:36px;border-radius:50%;
|
||||
display:grid;place-items:center;color:var(--soft);font-size:15px;
|
||||
transition:background .25s var(--ease),color .25s var(--ease),transform .5s var(--spring);
|
||||
}
|
||||
html[data-ui='vivid'] .icon-btn:hover{background:var(--accent-soft);color:var(--accent);transform:scale(1.08)}
|
||||
@media (max-width:720px) {
|
||||
html[data-ui='vivid'] .nav{display:none}
|
||||
}
|
||||
|
||||
/* ---- 设计稿分节:阅读进度(详情页) ---- */
|
||||
/* ---- 阅读进度(详情页) ---- */
|
||||
html[data-ui='vivid'] .progress{
|
||||
position:fixed;top:0;left:0;right:0;height:2px;z-index:200;
|
||||
background:transparent;pointer-events:none;
|
||||
}
|
||||
html[data-ui='vivid'] .progress i{
|
||||
display:block;height:100%;width:0%;
|
||||
background:linear-gradient(90deg,var(--accent),var(--accent-deep));
|
||||
transition:width .1s linear;
|
||||
}
|
||||
html[data-ui='vivid'] .progress-pct{
|
||||
position:fixed;right:22px;top:50%;transform:translateY(-50%);
|
||||
z-index:90;font-family:var(--mono);font-size:11px;font-weight:700;
|
||||
color:var(--muted);letter-spacing:.06em;
|
||||
writing-mode:vertical-rl;
|
||||
opacity:0;transition:opacity .4s var(--ease);
|
||||
pointer-events:none;
|
||||
}
|
||||
html[data-ui='vivid'] .progress-pct.show{opacity:1}
|
||||
html[data-ui='vivid'] .progress-pct b{color:var(--accent);font-weight:800}
|
||||
|
||||
/* ---- 设计稿分节:页脚 ---- */
|
||||
/* ---- 页脚 ---- */
|
||||
html[data-ui='vivid'] .foot{
|
||||
max-width:1100px;margin:40px auto 0;padding:28px 28px 120px;
|
||||
border-top:1px solid var(--line);
|
||||
text-align:center;color:var(--muted);font-size:12.5px;line-height:2;
|
||||
}
|
||||
html[data-ui='vivid'] .foot a{color:var(--accent);font-weight:600;transition:opacity .25s}
|
||||
html[data-ui='vivid'] .foot a:hover{opacity:.75}
|
||||
html[data-ui='vivid'] .foot .sep{margin:0 8px;opacity:.4}
|
||||
html[data-ui='vivid'] .foot .quote{
|
||||
font-family:var(--serif);font-style:italic;
|
||||
color:var(--soft);margin-bottom:8px;font-size:13.5px;
|
||||
}
|
||||
|
||||
/* ---- 设计稿分节:切换器 / 窄屏 ---- */
|
||||
/* ---- 切换器 ---- */
|
||||
html[data-ui='vivid'] .switch{
|
||||
position:fixed;z-index:99;left:50%;bottom:18px;transform:translateX(-50%);
|
||||
display:flex;gap:6px;padding:6px;border-radius:999px;
|
||||
background:rgba(28,28,30,.88);backdrop-filter:blur(14px);
|
||||
box-shadow:0 10px 36px rgba(0,0,0,.35);
|
||||
border:1px solid rgba(255,255,255,.08);
|
||||
}
|
||||
html[data-ui='vivid'] .switch button{
|
||||
padding:9px 15px;border-radius:999px;font-size:12.5px;font-weight:700;
|
||||
color:rgba(255,255,255,.55);transition:.2s;
|
||||
}
|
||||
html[data-ui='vivid'] .switch button:hover{color:#fff;background:rgba(255,255,255,.1)}
|
||||
html[data-ui='vivid'] .switch button.on{background:#fff;color:#111}
|
||||
html[data-ui='vivid'] .switch .lbl{color:rgba(255,255,255,.35);font-size:11px;align-self:center;padding:0 8px;letter-spacing:.08em}
|
||||
@media (max-width:640px) {
|
||||
html[data-ui='vivid'] .hero{padding:48px 20px 32px}
|
||||
html[data-ui='vivid'] .wrap{padding:8px 18px 36px}
|
||||
html[data-ui='vivid'] .paper{padding:28px 22px 36px;border-radius:16px}
|
||||
html[data-ui='vivid'] .about-card{padding:32px 22px}
|
||||
html[data-ui='vivid'] .panels{padding:16px 18px 12px}
|
||||
html[data-ui='vivid'] .toolbar{flex-direction:column;align-items:stretch}
|
||||
html[data-ui='vivid'] .search{width:100%}
|
||||
html[data-ui='vivid'] .search input{flex:1;width:auto}
|
||||
html[data-ui='vivid'] .top-in{padding:0 16px;gap:12px}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
/* ONE · 余白(yohaku)· 由设计稿按节拆分 / 窄屏
|
||||
*
|
||||
* 由设计稿(仓库根 index.html 的 <style>)按它自己的分节原样拆分。
|
||||
* 只做了一件事:给每条规则加 html[data-ui='vivid'] 前缀(@media 内部的
|
||||
* 规则也逐条加,at-rule 本身不加),并把 :root[data-theme="dark"] 映射到
|
||||
* 本站的 vivid-dark。数值一律未改,规则也没有被拆开过。
|
||||
*
|
||||
* 前缀是硬要求:组件 scoped 样式编译后是 .foo[data-v-HASH](0,2,0),
|
||||
* html[data-ui='vivid'] .foo 是(0,2,1)才能稳定取胜;scoped 是懒加载
|
||||
* chunk,入口 CSS 之后才到位,平手时按源码顺序反而会赢。
|
||||
*/
|
||||
|
||||
|
||||
/* ---- 设计稿分节:Hero ---- */
|
||||
/* ---- Hero ---- */
|
||||
html[data-ui='vivid'] .hero{
|
||||
max-width:1100px;margin:0 auto;padding:72px 28px 48px;
|
||||
position:relative;
|
||||
}
|
||||
html[data-ui='vivid'] .hero .kicker{
|
||||
display:inline-flex;align-items:center;gap:8px;
|
||||
font-size:12px;font-weight:700;letter-spacing:.14em;
|
||||
color:var(--accent);text-transform:uppercase;
|
||||
margin-bottom:20px;
|
||||
}
|
||||
html[data-ui='vivid'] .hero .kicker::before{
|
||||
content:"";width:20px;height:1.5px;background:var(--accent);border-radius:2px;
|
||||
}
|
||||
html[data-ui='vivid'] .hero h1{
|
||||
margin:0;font-size:clamp(36px,5.5vw,56px);font-weight:800;
|
||||
letter-spacing:-.03em;line-height:1.15;max-width:16ch;
|
||||
color:var(--ink);
|
||||
}
|
||||
html[data-ui='vivid'] .hero h1 .hl{
|
||||
color:var(--accent);
|
||||
position:relative;
|
||||
}
|
||||
html[data-ui='vivid'] .hero h1 .hl::after{
|
||||
content:"";position:absolute;left:0;right:0;bottom:4px;height:8px;
|
||||
background:var(--accent-soft);z-index:-1;border-radius:2px;
|
||||
}
|
||||
html[data-ui='vivid'] .hero .lead{
|
||||
margin:18px 0 0;max-width:480px;font-size:16px;
|
||||
color:var(--soft);line-height:1.8;
|
||||
}
|
||||
html[data-ui='vivid'] .hero .lead strong{color:var(--text);font-weight:700}
|
||||
html[data-ui='vivid'] .stats{display:flex;gap:28px;margin-top:32px;flex-wrap:wrap}
|
||||
html[data-ui='vivid'] .stat b{
|
||||
display:block;font-size:22px;font-weight:800;letter-spacing:-.02em;
|
||||
color:var(--ink);font-variant-numeric:tabular-nums;
|
||||
}
|
||||
html[data-ui='vivid'] .stat span{font-size:12px;color:var(--muted);font-weight:600}
|
||||
|
||||
/* ---- 设计稿分节:筛选 ---- */
|
||||
/* ---- 筛选 ---- */
|
||||
html[data-ui='vivid'] .wrap{max-width:1100px;margin:0 auto;padding:8px 28px 48px}
|
||||
html[data-ui='vivid'] .toolbar{
|
||||
display:flex;gap:10px;flex-wrap:wrap;align-items:center;
|
||||
padding-bottom:20px;margin-bottom:8px;
|
||||
border-bottom:1px solid var(--line);
|
||||
}
|
||||
html[data-ui='vivid'] .seg{
|
||||
display:inline-flex;background:color-mix(in srgb,var(--ink) 5%,transparent);
|
||||
border-radius:999px;padding:3px;gap:2px;
|
||||
}
|
||||
html[data-ui='vivid'] .seg button{
|
||||
font-size:13px;font-weight:600;color:var(--soft);
|
||||
padding:7px 16px;border-radius:999px;
|
||||
transition:background .3s var(--ease),color .3s var(--ease),transform .5s var(--spring);
|
||||
}
|
||||
html[data-ui='vivid'] .seg button:hover{color:var(--ink)}
|
||||
html[data-ui='vivid'] .seg button.on{
|
||||
background:var(--card);color:var(--accent);
|
||||
box-shadow:var(--shadow);
|
||||
transform:scale(1.02);
|
||||
}
|
||||
html[data-ui='vivid'] .chips{display:flex;gap:6px;flex-wrap:wrap;flex:1}
|
||||
html[data-ui='vivid'] .chip{
|
||||
font-size:12.5px;font-weight:600;color:var(--soft);
|
||||
padding:7px 14px;border-radius:999px;
|
||||
border:1px solid var(--line);
|
||||
transition:all .35s var(--spring);
|
||||
}
|
||||
html[data-ui='vivid'] .chip:hover{border-color:var(--accent);color:var(--accent);transform:translateY(-2px)}
|
||||
html[data-ui='vivid'] .chip.on{
|
||||
background:var(--accent);border-color:var(--accent);color:#fff;
|
||||
box-shadow:0 6px 16px var(--accent-glow);
|
||||
transform:translateY(-1px);
|
||||
}
|
||||
html[data-ui='vivid'] .search{
|
||||
display:flex;align-items:center;gap:6px;
|
||||
background:color-mix(in srgb,var(--ink) 4%,transparent);
|
||||
border-radius:999px;padding:2px 4px 2px 14px;
|
||||
border:1px solid transparent;
|
||||
transition:border-color .3s,background .3s,box-shadow .3s;
|
||||
}
|
||||
html[data-ui='vivid'] .search:focus-within{
|
||||
border-color:var(--accent);background:var(--card);
|
||||
box-shadow:0 0 0 3px var(--accent-soft);
|
||||
}
|
||||
html[data-ui='vivid'] .search input{
|
||||
border:0;background:transparent;outline:none;font:inherit;font-size:13.5px;
|
||||
width:150px;padding:7px 0;color:var(--ink);
|
||||
}
|
||||
html[data-ui='vivid'] .search input::placeholder{color:var(--muted)}
|
||||
html[data-ui='vivid'] .search button{
|
||||
width:32px;height:32px;border-radius:50%;
|
||||
background:var(--accent);color:#fff;font-size:13px;
|
||||
transition:transform .5s var(--spring),box-shadow .3s;
|
||||
}
|
||||
html[data-ui='vivid'] .search button:hover{transform:scale(1.1);box-shadow:0 4px 12px var(--accent-glow)}
|
||||
|
||||
/* ---- 设计稿分节:列表:长文卡片 / 短文纸条 ---- */
|
||||
/* ---- 列表:长文卡片 / 短文纸条 ---- */
|
||||
html[data-ui='vivid'] .feed{display:flex;flex-direction:column;gap:18px;padding-top:24px}
|
||||
/* 长文:干净卡片 */
|
||||
html[data-ui='vivid'] .card{
|
||||
display:block;background:var(--card);
|
||||
border:1px solid var(--line);
|
||||
border-radius:var(--radius-lg);
|
||||
overflow:hidden;
|
||||
box-shadow:var(--shadow);
|
||||
transition:transform .55s var(--spring),box-shadow .45s var(--ease),border-color .3s;
|
||||
position:relative;
|
||||
}
|
||||
html[data-ui='vivid'] .card:hover{
|
||||
transform:translateY(-4px) scale(1.005);
|
||||
box-shadow:var(--shadow-lg);
|
||||
border-color:color-mix(in srgb,var(--accent) 35%,transparent);
|
||||
}
|
||||
html[data-ui='vivid'] .card:active{transform:translateY(-1px) scale(.998);transition-duration:.15s}
|
||||
html[data-ui='vivid'] .card .cov{
|
||||
height:180px;background:linear-gradient(
|
||||
135deg,
|
||||
color-mix(in srgb,var(--accent) 25%,var(--paper)),
|
||||
color-mix(in srgb,var(--accent) 8%,var(--paper))
|
||||
);
|
||||
position:relative;overflow:hidden;
|
||||
}
|
||||
html[data-ui='vivid'] .card .cov img{width:100%;height:100%;object-fit:cover;transition:transform .8s var(--ease)}
|
||||
html[data-ui='vivid'] .card:hover .cov img{transform:scale(1.04)}
|
||||
html[data-ui='vivid'] .card .body{padding:22px 26px 24px}
|
||||
html[data-ui='vivid'] .card .meta{
|
||||
display:flex;gap:10px;align-items:center;flex-wrap:wrap;
|
||||
font-size:12px;color:var(--muted);font-weight:600;margin-bottom:10px;
|
||||
}
|
||||
html[data-ui='vivid'] .badge{
|
||||
font-size:10.5px;font-weight:800;letter-spacing:.06em;
|
||||
padding:3px 9px;border-radius:999px;
|
||||
}
|
||||
html[data-ui='vivid'] .badge.long{background:var(--accent-soft);color:var(--accent)}
|
||||
html[data-ui='vivid'] .badge.short{background:rgba(52,199,89,.14);color:#28a745}
|
||||
html[data-ui='vivid'] .card h2{
|
||||
margin:0 0 10px;font-size:clamp(19px,2.2vw,23px);font-weight:800;
|
||||
letter-spacing:-.02em;line-height:1.35;color:var(--ink);
|
||||
transition:color .25s;
|
||||
}
|
||||
html[data-ui='vivid'] .card:hover h2{color:var(--accent)}
|
||||
html[data-ui='vivid'] .card .sum{
|
||||
margin:0;font-size:14.5px;line-height:1.75;color:var(--soft);
|
||||
display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;
|
||||
}
|
||||
html[data-ui='vivid'] .card .tags{display:flex;gap:6px;flex-wrap:wrap;margin-top:14px}
|
||||
html[data-ui='vivid'] .tg{
|
||||
font-size:11.5px;font-weight:700;color:var(--soft);
|
||||
padding:4px 11px;border-radius:999px;
|
||||
background:color-mix(in srgb,var(--ink) 5%,transparent);
|
||||
transition:all .4s var(--spring);
|
||||
}
|
||||
html[data-ui='vivid'] .tg:hover{background:var(--accent-soft);color:var(--accent);transform:translateY(-2px)}
|
||||
/* 短文:纸条,无框更轻 */
|
||||
html[data-ui='vivid'] .note{
|
||||
display:block;background:transparent;
|
||||
border-left:3px solid var(--accent);
|
||||
border-radius:0 var(--radius) var(--radius) 0;
|
||||
padding:18px 24px;
|
||||
margin-left:12px;
|
||||
position:relative;
|
||||
transition:background .35s var(--ease),transform .55s var(--spring),border-color .3s;
|
||||
}
|
||||
html[data-ui='vivid'] .note:hover{
|
||||
background:var(--card);
|
||||
transform:translateX(6px);
|
||||
box-shadow:var(--shadow);
|
||||
}
|
||||
html[data-ui='vivid'] .note .meta{
|
||||
display:flex;gap:10px;align-items:center;
|
||||
font-size:12px;color:var(--muted);font-weight:600;margin-bottom:8px;
|
||||
}
|
||||
html[data-ui='vivid'] .note .body{
|
||||
margin:0;font-size:15.5px;line-height:1.8;color:var(--text);
|
||||
}
|
||||
html[data-ui='vivid'] .note .body a{
|
||||
color:var(--accent);font-weight:600;
|
||||
border-bottom:1px solid color-mix(in srgb,var(--accent) 40%,transparent);
|
||||
transition:border-color .25s;
|
||||
}
|
||||
html[data-ui='vivid'] .note .body a:hover{border-bottom-color:var(--accent)}
|
||||
html[data-ui='vivid'] .note .tags{display:flex;gap:6px;flex-wrap:wrap;margin-top:12px}
|
||||
|
||||
/* ---- 设计稿分节:分页 ---- */
|
||||
/* ---- 分页 ---- */
|
||||
html[data-ui='vivid'] .pager{
|
||||
display:flex;gap:8px;align-items:center;justify-content:center;
|
||||
margin-top:36px;flex-wrap:wrap;
|
||||
}
|
||||
html[data-ui='vivid'] .pager button{
|
||||
font-size:13px;font-weight:700;padding:9px 16px;border-radius:999px;
|
||||
color:var(--soft);border:1px solid var(--line);background:var(--card);
|
||||
transition:all .45s var(--spring);
|
||||
}
|
||||
html[data-ui='vivid'] .pager button:hover:not(:disabled){
|
||||
border-color:var(--accent);color:var(--accent);transform:translateY(-2px);
|
||||
box-shadow:0 6px 16px var(--accent-soft);
|
||||
}
|
||||
html[data-ui='vivid'] .pager button:disabled{opacity:.4;cursor:not-allowed}
|
||||
html[data-ui='vivid'] .pager .cur{
|
||||
background:var(--accent);border-color:var(--accent);color:#fff;
|
||||
box-shadow:0 6px 16px var(--accent-glow);
|
||||
}
|
||||
html[data-ui='vivid'] .pager .info{border:0;background:none;color:var(--muted);font-weight:600;font-size:12.5px}
|
||||
|
||||
/* ---- 首页作品展示区(参照 diygod.cc:只展示部分 + 查看所有) ---- */
|
||||
html[data-ui='vivid'] .showcase{
|
||||
max-width:1100px;margin:56px auto 0;padding:0 28px;
|
||||
}
|
||||
html[data-ui='vivid'] .show-head{
|
||||
display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;margin-bottom:20px;
|
||||
}
|
||||
html[data-ui='vivid'] .show-head h2{
|
||||
margin:0;font-size:1.4rem;font-weight:650;letter-spacing:0.01em;
|
||||
}
|
||||
html[data-ui='vivid'] .show-head .en{color:var(--muted)}
|
||||
html[data-ui='vivid'] .show-head .push{margin-left:auto;color:var(--muted)}
|
||||
html[data-ui='vivid'] .show-head .push:hover{color:var(--accent-deep)}
|
||||
html[data-ui='vivid'] .show-more{display:flex;justify-content:center;margin-top:26px}
|
||||
html[data-ui='vivid'] .show-more .chip{font-size:13px;padding:9px 22px}
|
||||
@media (max-width:860px){
|
||||
html[data-ui='vivid'] .showcase{
|
||||
padding-left:max(18px, env(safe-area-inset-left));
|
||||
padding-right:max(18px, env(safe-area-inset-right));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/* ONE · 余白(yohaku)· 由设计稿按节拆分 / 窄屏
|
||||
*
|
||||
* 由设计稿(仓库根 index.html 的 <style>)按它自己的分节原样拆分。
|
||||
* 只做了一件事:给每条规则加 html[data-ui='vivid'] 前缀(@media 内部的
|
||||
* 规则也逐条加,at-rule 本身不加),并把 :root[data-theme="dark"] 映射到
|
||||
* 本站的 vivid-dark。数值一律未改,规则也没有被拆开过。
|
||||
*
|
||||
* 前缀是硬要求:组件 scoped 样式编译后是 .foo[data-v-HASH](0,2,0),
|
||||
* html[data-ui='vivid'] .foo 是(0,2,1)才能稳定取胜;scoped 是懒加载
|
||||
* chunk,入口 CSS 之后才到位,平手时按源码顺序反而会赢。
|
||||
*/
|
||||
|
||||
|
||||
/* ---- 设计稿分节:归档 / 标签 / 作品 / 关于 ---- */
|
||||
/* ---- 归档 / 标签 / 作品 / 关于 ---- */
|
||||
html[data-ui='vivid'] .page-head{
|
||||
max-width:1100px;margin:0 auto;padding:56px 28px 32px;
|
||||
}
|
||||
html[data-ui='vivid'] .page-head code{font-family:var(--mono);font-size:.9em}
|
||||
.page-head .kicker{
|
||||
display:inline-flex;align-items:center;gap:8px;
|
||||
font-size:12px;font-weight:700;letter-spacing:.14em;
|
||||
color:var(--accent);text-transform:uppercase;margin-bottom:16px;
|
||||
}
|
||||
html[data-ui='vivid'] .page-head .kicker::before{content:"";width:20px;height:1.5px;background:var(--accent)}
|
||||
html[data-ui='vivid'] .page-head h1{
|
||||
margin:0;font-size:clamp(32px,5vw,48px);font-weight:800;
|
||||
letter-spacing:-.03em;color:var(--ink);
|
||||
}
|
||||
html[data-ui='vivid'] .page-head h1 em{font-style:normal;color:var(--accent)}
|
||||
html[data-ui='vivid'] .page-head p{margin:12px 0 0;max-width:480px;color:var(--soft);font-size:15px;line-height:1.75}
|
||||
html[data-ui='vivid'] .year-block{margin-bottom:36px}
|
||||
html[data-ui='vivid'] .year-head{display:flex;align-items:baseline;gap:14px;margin-bottom:14px}
|
||||
html[data-ui='vivid'] .year-head .y{
|
||||
font-size:clamp(36px,5vw,52px);font-weight:800;letter-spacing:-.03em;
|
||||
color:var(--accent);font-variant-numeric:tabular-nums;
|
||||
}
|
||||
html[data-ui='vivid'] .year-head .c{
|
||||
font-size:12px;font-weight:700;color:var(--muted);
|
||||
padding:5px 12px;border-radius:999px;
|
||||
background:color-mix(in srgb,var(--ink) 5%,transparent);
|
||||
}
|
||||
html[data-ui='vivid'] .month{
|
||||
background:var(--card);border:1px solid var(--line);
|
||||
border-radius:var(--radius-lg);margin-bottom:12px;overflow:hidden;
|
||||
box-shadow:var(--shadow);
|
||||
transition:box-shadow .35s,transform .55s var(--spring);
|
||||
}
|
||||
html[data-ui='vivid'] .month:hover{box-shadow:var(--shadow-lg);transform:translateY(-2px)}
|
||||
html[data-ui='vivid'] .month h3{
|
||||
margin:0;padding:14px 22px 10px;font-size:12px;font-weight:800;
|
||||
letter-spacing:.1em;color:var(--accent);
|
||||
}
|
||||
html[data-ui='vivid'] .month ul{list-style:none;margin:0;padding:0 10px 10px}
|
||||
html[data-ui='vivid'] .month li a{
|
||||
display:flex;gap:14px;padding:10px 14px;border-radius:10px;
|
||||
font-size:14px;align-items:baseline;
|
||||
transition:background .3s,transform .5s var(--spring);
|
||||
}
|
||||
html[data-ui='vivid'] .month li a:hover{background:var(--accent-soft);transform:translateX(6px)}
|
||||
html[data-ui='vivid'] .month .d{font-family:var(--mono);font-size:12px;color:var(--muted);flex:none;font-weight:600}
|
||||
html[data-ui='vivid'] .month .kd{font-size:10.5px;font-weight:800;padding:2px 8px;border-radius:999px;flex:none}
|
||||
html[data-ui='vivid'] .month .kd.s{background:rgba(52,199,89,.14);color:#28a745}
|
||||
html[data-ui='vivid'] .month .kd.l{background:var(--accent-soft);color:var(--accent)}
|
||||
html[data-ui='vivid'] .month .t{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)}
|
||||
html[data-ui='vivid'] .month li a:hover .t{color:var(--ink)}
|
||||
html[data-ui='vivid'] .tag-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:16px}
|
||||
html[data-ui='vivid'] .tag-tile{
|
||||
background:var(--card);border:1px solid var(--line);
|
||||
border-radius:var(--radius-lg);padding:22px;
|
||||
box-shadow:var(--shadow);position:relative;overflow:hidden;
|
||||
transition:all .55s var(--spring);
|
||||
}
|
||||
html[data-ui='vivid'] .tag-tile:hover{
|
||||
transform:translateY(-6px);
|
||||
box-shadow:var(--shadow-lg);
|
||||
border-color:color-mix(in srgb,var(--tc,var(--accent)) 40%,transparent);
|
||||
}
|
||||
html[data-ui='vivid'] .tag-tile .sw{
|
||||
width:32px;height:32px;border-radius:10px 10px 10px 4px;
|
||||
background:var(--tc,var(--accent));margin-bottom:14px;
|
||||
box-shadow:0 6px 14px -4px var(--tc,var(--accent));
|
||||
transition:transform .55s var(--spring);
|
||||
}
|
||||
html[data-ui='vivid'] .tag-tile:hover .sw{transform:rotate(-8deg) scale(1.1)}
|
||||
html[data-ui='vivid'] .tag-tile .nm{font-size:18px;font-weight:800;display:block;margin-bottom:4px;color:var(--ink)}
|
||||
html[data-ui='vivid'] .tag-tile .ct{font-size:12.5px;color:var(--muted);font-weight:600}
|
||||
html[data-ui='vivid'] .tag-tile .big{
|
||||
position:absolute;right:14px;top:10px;font-size:42px;font-weight:800;
|
||||
color:color-mix(in srgb,var(--tc,var(--accent)) 12%,transparent);
|
||||
letter-spacing:-.04em;line-height:1;
|
||||
}
|
||||
html[data-ui='vivid'] .proj-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:20px}
|
||||
html[data-ui='vivid'] .proj{
|
||||
background:var(--card);border:1px solid var(--line);
|
||||
border-radius:var(--radius-lg);overflow:hidden;
|
||||
box-shadow:var(--shadow);display:flex;flex-direction:column;
|
||||
transition:all .55s var(--spring);
|
||||
}
|
||||
html[data-ui='vivid'] .proj:hover{transform:translateY(-6px);box-shadow:var(--shadow-lg);border-color:var(--accent-soft)}
|
||||
html[data-ui='vivid'] .proj .cov{height:150px;position:relative;overflow:hidden;background:var(--accent-soft)}
|
||||
html[data-ui='vivid'] .proj .cov img{width:100%;height:100%;object-fit:cover;transition:transform .8s var(--ease)}
|
||||
html[data-ui='vivid'] .proj:hover .cov img{transform:scale(1.05)}
|
||||
html[data-ui='vivid'] .proj .st{
|
||||
position:absolute;top:12px;left:12px;font-size:10.5px;font-weight:800;
|
||||
background:color-mix(in srgb,var(--card) 90%,transparent);
|
||||
backdrop-filter:blur(8px);color:var(--accent);
|
||||
padding:5px 11px;border-radius:999px;
|
||||
}
|
||||
html[data-ui='vivid'] .proj .body{padding:18px 20px 20px;flex:1;display:flex;flex-direction:column;gap:8px}
|
||||
html[data-ui='vivid'] .proj h3{margin:0;font-size:17px;font-weight:800;color:var(--ink);letter-spacing:-.01em}
|
||||
html[data-ui='vivid'] .proj p{margin:0;font-size:13.5px;line-height:1.7;color:var(--soft);flex:1}
|
||||
html[data-ui='vivid'] .proj .acts{display:flex;gap:8px;flex-wrap:wrap;margin-top:4px}
|
||||
html[data-ui='vivid'] .proj .acts span{
|
||||
font-size:11.5px;font-weight:700;padding:6px 12px;border-radius:999px;
|
||||
background:color-mix(in srgb,var(--ink) 5%,transparent);color:var(--soft);
|
||||
transition:all .45s var(--spring);
|
||||
}
|
||||
html[data-ui='vivid'] .proj:hover .acts span{background:var(--accent-soft);color:var(--accent)}
|
||||
html[data-ui='vivid'] .about-card{
|
||||
max-width:680px;margin:0 auto;background:var(--card);
|
||||
border:1px solid var(--line);border-radius:var(--radius-lg);
|
||||
padding:48px 44px;box-shadow:var(--shadow-lg);
|
||||
animation:paperIn .7s var(--spring) both;
|
||||
}
|
||||
html[data-ui='vivid'] .about-card h1{margin:0 0 8px;font-size:clamp(32px,5vw,44px);font-weight:800;letter-spacing:-.03em;color:var(--ink)}
|
||||
html[data-ui='vivid'] .about-card .role{
|
||||
font-size:13px;font-weight:700;letter-spacing:.12em;color:var(--accent);
|
||||
text-transform:uppercase;margin-bottom:22px;
|
||||
}
|
||||
html[data-ui='vivid'] .about-card .bio{
|
||||
font-size:16.5px;line-height:1.95;color:var(--soft);
|
||||
max-width:46ch;margin:0 0 32px;font-family:var(--serif);
|
||||
}
|
||||
html[data-ui='vivid'] .about-facts{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px}
|
||||
html[data-ui='vivid'] .about-facts div{
|
||||
background:color-mix(in srgb,var(--accent) 8%,transparent);
|
||||
border-radius:var(--radius);padding:16px;
|
||||
}
|
||||
html[data-ui='vivid'] .about-facts b{
|
||||
display:block;font-size:24px;font-weight:800;color:var(--accent);
|
||||
letter-spacing:-.02em;font-variant-numeric:tabular-nums;
|
||||
}
|
||||
html[data-ui='vivid'] .about-facts span{font-size:12px;color:var(--muted);font-weight:700}
|
||||
html[data-ui='vivid'] .empty{
|
||||
max-width:480px;margin:40px auto;background:var(--card);
|
||||
border:1px dashed var(--line-strong);border-radius:var(--radius-lg);
|
||||
padding:48px 32px;text-align:center;box-shadow:var(--shadow);
|
||||
}
|
||||
html[data-ui='vivid'] .empty h3{margin:0 0 8px;font-size:20px;font-weight:800;color:var(--ink)}
|
||||
html[data-ui='vivid'] .empty p{margin:0 0 18px;color:var(--muted);font-size:14px}
|
||||
|
||||
/* 无封面作品卡:状态角标内联到标题旁(设计稿 mock 全有封面,此场景是补的) */
|
||||
html[data-ui='vivid'] .proj .st-plain{
|
||||
position:static;display:inline-block;margin-left:9px;vertical-align:2px;
|
||||
font-size:10px;font-weight:800;letter-spacing:.06em;
|
||||
padding:2px 9px;border-radius:999px;
|
||||
background:color-mix(in srgb,var(--card) 90%,transparent);
|
||||
color:var(--muted);box-shadow:inset 0 0 0 1px var(--line-strong);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/* ONE · 余白(yohaku)· 由设计稿按节拆分 / 窄屏
|
||||
*
|
||||
* 由设计稿(仓库根 index.html 的 <style>)按它自己的分节原样拆分。
|
||||
* 只做了一件事:给每条规则加 html[data-ui='vivid'] 前缀(@media 内部的
|
||||
* 规则也逐条加,at-rule 本身不加),并把 :root[data-theme="dark"] 映射到
|
||||
* 本站的 vivid-dark。数值一律未改,规则也没有被拆开过。
|
||||
*
|
||||
* 前缀是硬要求:组件 scoped 样式编译后是 .foo[data-v-HASH](0,2,0),
|
||||
* html[data-ui='vivid'] .foo 是(0,2,1)才能稳定取胜;scoped 是懒加载
|
||||
* chunk,入口 CSS 之后才到位,平手时按源码顺序反而会赢。
|
||||
*/
|
||||
|
||||
|
||||
/* ---- 设计稿分节:底部:标签 / 作品 / 关于 ---- */
|
||||
/* ---- 底部:标签 / 作品 / 关于 ---- */
|
||||
html[data-ui='vivid'] .panels{
|
||||
max-width:1100px;margin:0 auto;padding:20px 28px 20px;
|
||||
display:grid;/* 设计稿此处是 1.2fr 1fr 1fr;站主要求标签云卡明显大一圈,改为 1.6fr(实测 444 vs 277px)。
|
||||
高度策略:align-items:start —— 每张卡自适应内容,顶边齐平,不互相拉伸。
|
||||
(试过等高拉伸 / 跨行加高,标签云内容少时会空出一大块,站主否了。) */
|
||||
grid-template-columns:1.6fr 1fr 1fr;align-items:start;
|
||||
}
|
||||
@media (max-width:860px) {
|
||||
html[data-ui='vivid'] .panels{grid-template-columns:1fr}
|
||||
}
|
||||
html[data-ui='vivid'] .panel{
|
||||
background:var(--card);border:1px solid var(--line);
|
||||
border-radius:var(--radius-lg);padding:24px;
|
||||
box-shadow:var(--shadow);
|
||||
transition:transform .55s var(--spring),box-shadow .4s;
|
||||
}
|
||||
|
||||
/* 标签云卡比另外两张高出一截(站主要求):三张**顶边齐平**,
|
||||
作品 / 关于两卡通过负 bottom margin 各自收短 40px ——
|
||||
于是标签云的底边比它们低出 40px。栅格行高由标签云决定,无需补偿。 */
|
||||
|
||||
html[data-ui='vivid'] .panel:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg)}
|
||||
html[data-ui='vivid'] .panel h3{
|
||||
margin:0 0 16px;font-size:11px;font-weight:800;
|
||||
letter-spacing:.16em;text-transform:uppercase;color:var(--muted);
|
||||
display:flex;align-items:center;gap:10px;
|
||||
}
|
||||
html[data-ui='vivid'] .panel h3::after{content:"";flex:1;height:1px;background:var(--line)}
|
||||
html[data-ui='vivid'] .cloud{display:flex;flex-wrap:wrap;gap:8px}
|
||||
html[data-ui='vivid'] .cloud a{
|
||||
font-size:13px;font-weight:700;padding:7px 14px;border-radius:999px;
|
||||
background:color-mix(in srgb,var(--accent) 10%,transparent);
|
||||
color:var(--accent);
|
||||
transition:all .5s var(--spring);
|
||||
}
|
||||
html[data-ui='vivid'] .cloud a:hover{
|
||||
background:var(--accent);color:#fff;
|
||||
transform:translateY(-3px) scale(1.05);
|
||||
box-shadow:0 8px 20px var(--accent-glow);
|
||||
}
|
||||
html[data-ui='vivid'] .cloud a span{opacity:.7;font-size:11px;margin-left:4px;font-weight:600}
|
||||
html[data-ui='vivid'] .plist{display:flex;flex-direction:column;gap:10px}
|
||||
html[data-ui='vivid'] .plist a{
|
||||
display:block;padding:13px 15px;border-radius:var(--radius);
|
||||
background:color-mix(in srgb,var(--ink) 3%,transparent);
|
||||
transition:all .5s var(--spring);
|
||||
}
|
||||
html[data-ui='vivid'] .plist a:hover{
|
||||
background:var(--accent-soft);transform:translateX(6px);
|
||||
}
|
||||
html[data-ui='vivid'] .plist b{display:block;font-size:14px;font-weight:700;color:var(--ink)}
|
||||
html[data-ui='vivid'] .plist small{color:var(--muted);font-size:12px}
|
||||
html[data-ui='vivid'] .about-box .name{
|
||||
font-size:18px;font-weight:800;color:var(--ink);margin:0 0 8px;letter-spacing:-.02em;
|
||||
}
|
||||
html[data-ui='vivid'] .about-box p{margin:0;font-size:13.5px;line-height:1.8;color:var(--soft)}
|
||||
|
||||
|
||||
/* 卡变大了一圈,chip 也相应放大一号,避免大卡里塞小字显得空 */
|
||||
html[data-ui='vivid'] .panels .cloud{gap:11px}
|
||||
html[data-ui='vivid'] .panels .cloud a{
|
||||
font-size:15px;padding:10px 19px;
|
||||
}
|
||||
html[data-ui='vivid'] .panels .cloud a span{font-size:12.5px;margin-left:5px}
|
||||
@@ -0,0 +1,249 @@
|
||||
/* ONE · 余白(yohaku)· 令牌层
|
||||
*
|
||||
* 这一层是整套 UI 的地基,做两件事:
|
||||
*
|
||||
* 1. 定义**站主接口** --v-*(28 个),值是设计稿的原始数值。
|
||||
* 站主在后台「自定义 CSS」里写 :root{--v-accent:…} 就能覆盖 :
|
||||
* 不需要去背 html[data-ui='vivid'] 这种选择器。
|
||||
*
|
||||
* 2. 把**设计稿的令牌名**(--paper / --ink / --accent / …)全部指回 --v-*。
|
||||
* 设计稿里 700 多行规则读的都是这些名字;接上之后站主只改一处,
|
||||
* 整套 UI 连同 classic 的共享组件(PostCard / RightRail / .prose / .btn)
|
||||
* 一起换皮。
|
||||
*
|
||||
* 为什么放进 @layer:未分层的规则永远赢过分层的规则(层级顺序先于特指度)。
|
||||
* 站主的自定义 CSS 是未分层的普通 <style>,所以写进 @layer 是**让出**优先级,
|
||||
* 而不是去压别人。这是刻意的。
|
||||
*
|
||||
* 数值来源:仓库根 index.html(设计稿)的 :root 与 :root[data-theme="dark"],
|
||||
* 一个数都没有改。
|
||||
*/
|
||||
|
||||
@layer one-yohaku {
|
||||
/* ==========================================================================
|
||||
1. 站主接口 + 设计稿令牌(亮色)
|
||||
========================================================================== */
|
||||
html[data-ui='vivid'] {
|
||||
/* ---- 站主接口:颜色 ---- */
|
||||
--v-bg: #faf7f0;
|
||||
--v-surface: #fffefb;
|
||||
--v-surface-2: #f4f1ea;
|
||||
--v-text: #1c1c1e;
|
||||
--v-text-2: #2c2c2e;
|
||||
--v-text-soft: #5c5c66;
|
||||
--v-muted: #8e8e99;
|
||||
--v-line: rgba(28, 28, 30, 0.1);
|
||||
--v-line-strong: rgba(28, 28, 30, 0.16);
|
||||
--v-accent: #69a6cc;
|
||||
--v-accent-2: #4a8bb3;
|
||||
--v-accent-3: color-mix(in srgb, var(--v-accent) 70%, var(--v-accent-2));
|
||||
--v-accent-soft: rgba(105, 166, 204, 0.14);
|
||||
--v-accent-glow: rgba(105, 166, 204, 0.35);
|
||||
|
||||
/* ---- 站主接口:形状与尺寸 ---- */
|
||||
--v-radius: 14px;
|
||||
--v-radius-sm: 10px;
|
||||
--v-radius-lg: 20px;
|
||||
--v-radius-pill: 999px;
|
||||
--v-content-width: 1100px;
|
||||
--v-measure: 680px;
|
||||
--v-nav-h: 64px;
|
||||
--v-motion: 1; /* 动效强度倍数;置 0 即全部即时生效 */
|
||||
|
||||
/* ---- 站主接口:阴影与动效 ---- */
|
||||
--v-shadow: 0 1px 2px rgba(28, 28, 30, 0.04), 0 8px 28px -8px rgba(28, 28, 30, 0.08);
|
||||
--v-shadow-sm: 0 1px 2px rgba(28, 28, 30, 0.04), 0 4px 14px -6px rgba(28, 28, 30, 0.08);
|
||||
--v-shadow-lg: 0 2px 4px rgba(28, 28, 30, 0.05), 0 20px 50px -16px rgba(28, 28, 30, 0.14);
|
||||
--v-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
--v-ease: cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||
|
||||
/* ---- 站主接口:字体与噪点 ---- */
|
||||
--v-font-display: 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Noto Sans SC', system-ui,
|
||||
-apple-system, sans-serif;
|
||||
--v-font-serif: 'Songti SC', 'Noto Serif SC', 'SimSun', Georgia, serif;
|
||||
--v-noise-opacity: 0.45;
|
||||
--v-noise-blend: overlay;
|
||||
|
||||
/* 单一强调色下的「渐变」= 同色微变,不再三色跳 */
|
||||
--v-grad: linear-gradient(
|
||||
135deg,
|
||||
var(--v-accent),
|
||||
color-mix(in srgb, var(--v-accent) 86%, var(--v-accent-2))
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
2. 暗色:只覆盖 --v-* 的默认值,上面的别名自动跟随
|
||||
========================================================================== */
|
||||
html[data-ui='vivid'][data-theme='vivid-dark'] {
|
||||
--v-bg: #1c1c1e;
|
||||
--v-surface: #232326;
|
||||
--v-surface-2: #2a2a2d;
|
||||
--v-text: #f2f2f7;
|
||||
--v-text-2: #e8e8ed;
|
||||
--v-text-soft: #a1a1aa;
|
||||
--v-muted: #6e6e78;
|
||||
--v-line: rgba(242, 242, 247, 0.1);
|
||||
--v-line-strong: rgba(242, 242, 247, 0.16);
|
||||
--v-accent: #a0a7d4;
|
||||
--v-accent-2: #b8bee6;
|
||||
--v-accent-soft: rgba(160, 167, 212, 0.16);
|
||||
--v-accent-glow: rgba(160, 167, 212, 0.3);
|
||||
--v-shadow: 0 1px 2px rgba(0, 0, 0, 0.2), 0 8px 28px -8px rgba(0, 0, 0, 0.35);
|
||||
--v-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2), 0 4px 14px -6px rgba(0, 0, 0, 0.3);
|
||||
--v-shadow-lg: 0 2px 4px rgba(0, 0, 0, 0.25), 0 20px 50px -16px rgba(0, 0, 0, 0.5);
|
||||
--v-noise-opacity: 0.3;
|
||||
--v-noise-blend: soft-light;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
2. 设计稿的令牌名 → 全部指回 --v-*
|
||||
--------------------------------------------------------------------------
|
||||
⚠ 这一段**必须留在 @layer 外面**。
|
||||
层级顺序优先于特指度:未分层的规则永远赢过分层的规则。styles.css 里
|
||||
`:root { --accent: #3d7f9c }` 是未分层的,如果这里放进 @layer,
|
||||
设计稿的配色就会被 classic 的令牌整片压掉 : 页面「看着像,细节全不对」。
|
||||
(--v-* 的默认值留在层里是为了让站主的自定义 CSS 能覆盖,这一段不是。)
|
||||
========================================================================== */
|
||||
html[data-ui='vivid'] {
|
||||
/* ---- 设计稿的令牌名 → 全部指回 --v-* ----
|
||||
* 下面这些名字是设计稿 700 多行规则在用的。接上之后,
|
||||
* 站主改 --v-* 就等于改整站;classic 的共享组件也一起换皮。 */
|
||||
--paper: var(--v-bg);
|
||||
--card: var(--v-surface);
|
||||
--paper-sunken: var(--v-surface-2);
|
||||
--ink: var(--v-text);
|
||||
--text: var(--v-text-2);
|
||||
--soft: var(--v-text-soft);
|
||||
--ink-soft: var(--v-text-soft);
|
||||
--muted: var(--v-muted);
|
||||
--line: var(--v-line);
|
||||
--line-soft: var(--v-line);
|
||||
--line-strong: var(--v-line-strong);
|
||||
--accent: var(--v-accent);
|
||||
--accent-2: var(--v-accent-2);
|
||||
--accent-deep: var(--v-accent-2);
|
||||
--accent-line: var(--v-accent-2);
|
||||
--accent-soft: var(--v-accent-soft);
|
||||
--accent-glow: var(--v-accent-glow);
|
||||
--shadow: var(--v-shadow);
|
||||
--shadow-lg: var(--v-shadow-lg);
|
||||
--radius: var(--v-radius);
|
||||
--radius-sm: var(--v-radius-sm);
|
||||
--radius-lg: var(--v-radius-lg);
|
||||
--spring: var(--v-spring);
|
||||
--spring-soft: cubic-bezier(0.22, 1.2, 0.36, 1);
|
||||
--ease: var(--v-ease);
|
||||
--sans: var(--v-font-display);
|
||||
--serif: var(--v-font-serif);
|
||||
--mono: 'SF Mono', 'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace;
|
||||
--measure: var(--v-measure);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
3. 复位与肌理
|
||||
--------------------------------------------------------------------------
|
||||
设计稿里这几条是全局的。必须逐条前缀收敛到 vivid,
|
||||
否则 a{text-decoration:none} / *{box-sizing} 会连后台一起改掉。
|
||||
========================================================================== */
|
||||
html[data-ui='vivid'] * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html[data-ui='vivid'] {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
html[data-ui='vivid'] body {
|
||||
margin: 0;
|
||||
font-family: var(--sans);
|
||||
color: var(--text);
|
||||
background: var(--paper);
|
||||
min-height: 100vh;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
transition: background 0.4s var(--ease), color 0.4s var(--ease);
|
||||
}
|
||||
html[data-ui='vivid'] a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
html[data-ui='vivid'] button {
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: inherit;
|
||||
}
|
||||
html[data-ui='vivid'] ::selection {
|
||||
background: var(--accent-glow);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
/* 噪点:强调色染色的细颗粒 */
|
||||
html[data-ui='vivid'] body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
opacity: var(--v-noise-opacity);
|
||||
mix-blend-mode: var(--v-noise-blend);
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0.42 0 0 0 0 0.65 0 0 0 0 0.8 0 0 0 0.55 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
|
||||
background-size: 180px 180px;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
4. 与 classic / 后台样式共存的防线
|
||||
--------------------------------------------------------------------------
|
||||
设计稿是一个独立单页,它默认「没人动过这些属性」。但真站里 styles.css
|
||||
(含后台那一段)是全局、未分层加载的,它的
|
||||
· h1,h2,h3,h4 { font-family: var(--serif) }
|
||||
· body { line-height: 1.8 }
|
||||
· .stat / .toolbar / .chip(后台那套带卡片的)
|
||||
会漏进余白的元素里 : 凡是设计稿「没声明、靠继承或默认值」的属性都会被改掉。
|
||||
所以这里把设计稿的期望值**显式写死**。
|
||||
|
||||
为什么这样不会误伤设计稿自己的衬线:详情页那 4 处 `font-family: var(--serif)`
|
||||
是 `.xxx .yyy` 形式,前缀后特指度 (0,2,1),高于这里的 (0,1,1) : 它们照样赢。
|
||||
这里只负责挡掉裸选择器。
|
||||
|
||||
⚠ 这一段必须留在 @layer 外面:未分层才能压住 styles.css 的未分层规则。
|
||||
========================================================================== */
|
||||
html[data-ui='vivid'] body {
|
||||
line-height: normal;
|
||||
}
|
||||
html[data-ui='vivid'] h1,
|
||||
html[data-ui='vivid'] h2,
|
||||
html[data-ui='vivid'] h3,
|
||||
html[data-ui='vivid'] h4,
|
||||
html[data-ui='vivid'] h5,
|
||||
html[data-ui='vivid'] h6 {
|
||||
font-family: inherit;
|
||||
/* styles.css 还给 h1..h4 设了 line-height:1.4;设计稿对这些标题是「靠继承」,
|
||||
所以这里一并交还给继承(设计稿自己声明了行高的标题特指度更高,照样赢)。 */
|
||||
line-height: inherit;
|
||||
}
|
||||
/* 后台有同名类且带卡片外观,逐个还原成设计稿里的裸元素 */
|
||||
html[data-ui='vivid'] .stat {
|
||||
background: none;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
gap: normal;
|
||||
display: block;
|
||||
}
|
||||
html[data-ui='vivid'] .toolbar {
|
||||
background: none;
|
||||
border-top: 0;
|
||||
border-radius: 0;
|
||||
padding: 0 0 20px;
|
||||
}
|
||||
html[data-ui='vivid'] .chip {
|
||||
display: inline-block;
|
||||
gap: normal;
|
||||
}
|
||||
/* styles.css 里有 .panel + .panel { margin-top: 16px }(classic 的纵向排布用),
|
||||
在余白的三栏栅格里它会把第 2、3 张卡往下压 16px,顶边就不齐了 —— 中和掉。 */
|
||||
html[data-ui='vivid'] .panels > .panel + .panel {
|
||||
margin-top: 0;
|
||||
}
|
||||
Reference in New Issue
Block a user