流体交互层:按压反馈 + 真半透明材质 + 主题交叉过渡 + 弹层材质化
按 apple-design(Designing Fluid Interfaces)审查落地: - 全站补 :active 按压缩放反馈(按钮/tab/链接/命令面板/前后栏导航),交互元素加 touch-action - TopBar、首页标题栏、浮动主题钮改 color-mix 半透明 + blur 真材质,配 reduced-transparency 回退 - 主题切换 250ms 交叉过渡(theme-anim 瞬时类),消除亮度突变 - CommandPalette/ShortcutsPanel 过 <Transition name="pop">:scrim 淡入 + 面板自顶部锚点 scale 进入,退出原路返回 - 首页标题栏收缩由二值跳变改为滚动进度 --sq 连续插值(16–56px) - reduced-motion 例外:弹层保留短交叉淡入,禁位移与主题过渡 - 触控目标增大(主题钮 40/44px、tab、后台导航项),文章大标题负字距 + 收紧行高
This commit is contained in:
@@ -156,8 +156,12 @@ watch(() => route.path, () => loadCounts())
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CommandPalette v-if="paletteOpen" @close="paletteOpen = false" />
|
<Transition name="pop">
|
||||||
<ShortcutsPanel v-if="shortcutsOpen" @close="shortcutsOpen = false" />
|
<CommandPalette v-if="paletteOpen" @close="paletteOpen = false" />
|
||||||
|
</Transition>
|
||||||
|
<Transition name="pop">
|
||||||
|
<ShortcutsPanel v-if="shortcutsOpen" @close="shortcutsOpen = false" />
|
||||||
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="checking">检查登录状态…</div>
|
<div v-else class="checking">检查登录状态…</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -58,7 +58,12 @@ const classes = computed(() => ['theme-switcher', `theme-switcher--${props.varia
|
|||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease;
|
touch-action: manipulation;
|
||||||
|
transition:
|
||||||
|
color 0.15s ease,
|
||||||
|
background-color 0.15s ease,
|
||||||
|
border-color 0.15s ease,
|
||||||
|
transform 100ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-switcher:hover {
|
.theme-switcher:hover {
|
||||||
@@ -67,6 +72,11 @@ const classes = computed(() => ['theme-switcher', `theme-switcher--${props.varia
|
|||||||
border-color: var(--admin-accent-line);
|
border-color: var(--admin-accent-line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 按下瞬间就有反馈 */
|
||||||
|
.theme-switcher:active {
|
||||||
|
transform: scale(0.92);
|
||||||
|
}
|
||||||
|
|
||||||
.theme-switcher .ic {
|
.theme-switcher .ic {
|
||||||
font-size: 19px;
|
font-size: 19px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@@ -75,8 +85,8 @@ const classes = computed(() => ['theme-switcher', `theme-switcher--${props.varia
|
|||||||
/* ---------- inline 形态:嵌入左栏底部 ---------- */
|
/* ---------- inline 形态:嵌入左栏底部 ---------- */
|
||||||
|
|
||||||
.theme-switcher--inline {
|
.theme-switcher--inline {
|
||||||
width: 36px;
|
width: 40px;
|
||||||
height: 36px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- floating 形态:右下角浮动 ---------- */
|
/* ---------- floating 形态:右下角浮动 ---------- */
|
||||||
@@ -86,18 +96,24 @@ const classes = computed(() => ['theme-switcher', `theme-switcher--${props.varia
|
|||||||
right: 18px;
|
right: 18px;
|
||||||
bottom: 18px;
|
bottom: 18px;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
width: 42px;
|
width: 44px;
|
||||||
height: 42px;
|
height: 44px;
|
||||||
backdrop-filter: blur(6px);
|
background: color-mix(in srgb, var(--admin-card) 78%, transparent);
|
||||||
|
backdrop-filter: blur(16px) saturate(180%);
|
||||||
box-shadow: var(--admin-shadow);
|
box-shadow: var(--admin-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-transparency: reduce) {
|
||||||
|
.theme-switcher--floating {
|
||||||
|
background: var(--admin-card);
|
||||||
|
backdrop-filter: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.theme-switcher--floating {
|
.theme-switcher--floating {
|
||||||
right: 12px;
|
right: 12px;
|
||||||
bottom: 12px;
|
bottom: 12px;
|
||||||
width: 38px;
|
|
||||||
height: 38px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -29,11 +29,18 @@ import { site } from '../site'
|
|||||||
top: 0;
|
top: 0;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
padding: 10px 18px;
|
padding: 10px 18px;
|
||||||
background-color: var(--paper);
|
background: color-mix(in srgb, var(--paper) 80%, transparent);
|
||||||
backdrop-filter: blur(6px);
|
backdrop-filter: blur(10px) saturate(160%);
|
||||||
border-bottom: 1px solid var(--line);
|
border-bottom: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-transparency: reduce) {
|
||||||
|
.topbar {
|
||||||
|
background: var(--paper);
|
||||||
|
backdrop-filter: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.brand {
|
.brand {
|
||||||
font-family: var(--serif);
|
font-family: var(--serif);
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
|
|||||||
@@ -64,7 +64,13 @@ export const site = reactive({
|
|||||||
|
|
||||||
// data-theme → 前台皮肤(paper / sage / rose / ink)
|
// data-theme → 前台皮肤(paper / sage / rose / ink)
|
||||||
// data-admin-theme → 后台明暗(light / dark),跟 site.themeMode 同步
|
// data-admin-theme → 后台明暗(light / dark),跟 site.themeMode 同步
|
||||||
function applyTheme() {
|
function applyTheme(animate = false) {
|
||||||
|
// 亮度突变是 Apple 明确点名的反模式:用户主动切换时给一次 250ms 交叉过渡
|
||||||
|
if (animate && typeof document !== 'undefined') {
|
||||||
|
const el = document.documentElement
|
||||||
|
el.classList.add('theme-anim')
|
||||||
|
setTimeout(() => el.classList.remove('theme-anim'), 300)
|
||||||
|
}
|
||||||
const isDark = site.themeMode === 'dark' || (site.themeMode === 'auto' && systemPrefersDark())
|
const isDark = site.themeMode === 'dark' || (site.themeMode === 'auto' && systemPrefersDark())
|
||||||
document.documentElement.setAttribute('data-theme', effectiveTheme(site.themeMode, site.light_skin_id))
|
document.documentElement.setAttribute('data-theme', effectiveTheme(site.themeMode, site.light_skin_id))
|
||||||
document.documentElement.setAttribute('data-admin-theme', isDark ? 'dark' : 'light')
|
document.documentElement.setAttribute('data-admin-theme', isDark ? 'dark' : 'light')
|
||||||
@@ -95,7 +101,7 @@ export async function loadSite() {
|
|||||||
return site
|
return site
|
||||||
}
|
}
|
||||||
|
|
||||||
// 访客切模式 → 落 localStorage + 立刻应用
|
// 访客切模式 → 落 localStorage + 立刻应用(带交叉过渡)
|
||||||
watch(
|
watch(
|
||||||
() => site.themeMode,
|
() => site.themeMode,
|
||||||
() => {
|
() => {
|
||||||
@@ -104,7 +110,7 @@ watch(
|
|||||||
} catch (_) {
|
} catch (_) {
|
||||||
/* private mode 等场景静默 */
|
/* private mode 等场景静默 */
|
||||||
}
|
}
|
||||||
applyTheme()
|
applyTheme(true)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+76
-1
@@ -620,7 +620,7 @@ h4 {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 8px 12px;
|
padding: 10px 12px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--admin-ink-soft);
|
color: var(--admin-ink-soft);
|
||||||
@@ -1825,3 +1825,78 @@ body:not(.admin-shell-active) :where(a, button, input, select, textarea, [tabind
|
|||||||
.cmd-overlay {
|
.cmd-overlay {
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
流体交互层(apple-design 审查落地)
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
/* 按压反馈:从按下瞬间就有反应,而不是 hover 或松手 */
|
||||||
|
button:active:not(:disabled),
|
||||||
|
a.btn:active,
|
||||||
|
a.pill:active,
|
||||||
|
.tab:active,
|
||||||
|
.tag-chip:active,
|
||||||
|
.rail-left .item:active,
|
||||||
|
.admin-side .item:active,
|
||||||
|
.cmd-panel .row:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 触屏:去掉浏览器对双击缩放的猜测延迟 */
|
||||||
|
button,
|
||||||
|
a.btn,
|
||||||
|
a.pill,
|
||||||
|
.tab,
|
||||||
|
.tag-chip,
|
||||||
|
.rail-left .item,
|
||||||
|
.admin-side .item {
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主题切换:亮度突变改为一次性的 250ms 交叉过渡(仅切换瞬间挂载) */
|
||||||
|
html.theme-anim,
|
||||||
|
html.theme-anim * {
|
||||||
|
transition: background-color 250ms ease, color 250ms ease, border-color 250ms ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹层材质化:从触发源方向 scale+fade 进入,退出走同一条路径 */
|
||||||
|
.cmd-panel {
|
||||||
|
transform-origin: top center;
|
||||||
|
}
|
||||||
|
.pop-enter-active {
|
||||||
|
transition: opacity 160ms ease-out;
|
||||||
|
}
|
||||||
|
.pop-enter-active .cmd-panel {
|
||||||
|
transition: transform 240ms cubic-bezier(0.2, 0.8, 0.25, 1), opacity 160ms ease-out;
|
||||||
|
}
|
||||||
|
.pop-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.pop-enter-from .cmd-panel {
|
||||||
|
transform: scale(0.96) translateY(-10px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.pop-leave-active {
|
||||||
|
transition: opacity 130ms ease-in;
|
||||||
|
}
|
||||||
|
.pop-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* reduce ≠ 无反馈:保留短交叉淡入,去掉位移/缩放与主题过渡 */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.pop-enter-active,
|
||||||
|
.pop-leave-active {
|
||||||
|
transition: opacity 150ms ease !important;
|
||||||
|
}
|
||||||
|
.pop-enter-active .cmd-panel {
|
||||||
|
transition: opacity 150ms ease !important;
|
||||||
|
}
|
||||||
|
.pop-enter-from .cmd-panel {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
html.theme-anim,
|
||||||
|
html.theme-anim * {
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,10 +20,13 @@ const tag = computed(() => route.query.tag || '')
|
|||||||
const page = computed(() => Number(route.query.page || 1))
|
const page = computed(() => Number(route.query.page || 1))
|
||||||
const size = computed(() => site.posts_per_page || 10)
|
const size = computed(() => site.posts_per_page || 10)
|
||||||
|
|
||||||
// 滚动时收缩顶部标题栏:隐藏大标题,只留 tabs 贴顶
|
// 滚动收缩标题栏:16–56px 区间内连续插值 --sq(0→1),
|
||||||
const scrolled = ref(false)
|
// 反馈跟随滚动本身,而不是在阈值处二值跳变
|
||||||
|
const headEl = ref(null)
|
||||||
function onScroll() {
|
function onScroll() {
|
||||||
scrolled.value = window.scrollY > 24
|
if (!headEl.value) return
|
||||||
|
const p = Math.min(1, Math.max(0, (window.scrollY - 16) / 40))
|
||||||
|
headEl.value.style.setProperty('--sq', p.toFixed(3))
|
||||||
}
|
}
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
onScroll()
|
onScroll()
|
||||||
@@ -88,7 +91,7 @@ applyDocTitle('')
|
|||||||
<LeftNav />
|
<LeftNav />
|
||||||
|
|
||||||
<main class="main">
|
<main class="main">
|
||||||
<header class="sticky-head" :class="{ 'is-compact': scrolled }">
|
<header ref="headEl" class="sticky-head">
|
||||||
<h1 class="page-title">{{ heading }}</h1>
|
<h1 class="page-title">{{ heading }}</h1>
|
||||||
<nav class="tabs">
|
<nav class="tabs">
|
||||||
<button
|
<button
|
||||||
@@ -149,14 +152,22 @@ applyDocTitle('')
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sticky-head {
|
.sticky-head {
|
||||||
|
--sq: 0;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
background-color: var(--paper);
|
background: color-mix(in srgb, var(--paper) 82%, transparent);
|
||||||
backdrop-filter: blur(6px);
|
backdrop-filter: blur(8px) saturate(160%);
|
||||||
border-bottom: 1px solid var(--line);
|
border-bottom: 1px solid var(--line);
|
||||||
padding: 8px 16px 0;
|
padding: 8px 16px 0;
|
||||||
transition: padding 0.2s ease;
|
}
|
||||||
|
|
||||||
|
/* 系统要求减少半透明时回退为实心材质 */
|
||||||
|
@media (prefers-reduced-transparency: reduce) {
|
||||||
|
.sticky-head {
|
||||||
|
background: var(--paper);
|
||||||
|
backdrop-filter: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-title {
|
.page-title {
|
||||||
@@ -164,40 +175,25 @@ applyDocTitle('')
|
|||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-height: 26px;
|
max-height: calc(26px * (1 - var(--sq)));
|
||||||
opacity: 1;
|
opacity: calc(1 - var(--sq));
|
||||||
transition:
|
|
||||||
max-height 0.2s ease,
|
|
||||||
opacity 0.2s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
margin-top: 4px;
|
margin-top: calc(4px * (1 - var(--sq)));
|
||||||
transition: margin-top 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 滚动后:收起大标题,只留 tabs 紧贴顶部 */
|
|
||||||
.sticky-head.is-compact {
|
|
||||||
padding-top: 6px;
|
|
||||||
}
|
|
||||||
.sticky-head.is-compact .page-title {
|
|
||||||
max-height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
.sticky-head.is-compact .tabs {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 5px 12px 7px;
|
padding: 8px 12px 9px;
|
||||||
background: none;
|
background: none;
|
||||||
border: 0;
|
border: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 13.5px;
|
font-size: 13.5px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
transition: transform 100ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab:hover {
|
.tab:hover {
|
||||||
|
|||||||
@@ -147,7 +147,9 @@ const initial = computed(() => (site.author_name || 'O').trim().slice(0, 1).toUp
|
|||||||
.title {
|
.title {
|
||||||
margin: 20px 0 0;
|
margin: 20px 0 0;
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
line-height: 1.35;
|
line-height: 1.2;
|
||||||
|
/* 大号衬线标题需要负字距,越紧越像"排"出来的而不是"摆"出来的 */
|
||||||
|
letter-spacing: -0.015em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lede {
|
.lede {
|
||||||
|
|||||||
Reference in New Issue
Block a user