- 新建 components/ui/(Button/Input/Select/Modal/Tabs/Spinner/Skeleton 等),变体用联合类型约束,根治幻影变体 - admin/components.tsx 与 matches/ui.tsx 改为再导出壳,消除 Spinner 三份重复 - 日期工具收口到 lib/date.ts,滚动监听收口到 lib/useScroll.ts - index.css 引入 :root RGB 三元组令牌,tailwind.config 接 <alpha-value>(修复透明度修饰符静默失效) - 刘建毛草 5MB 全字库(jsDelivr 未锁版) → 1KB 自托管子集(仅「先知」二字,unicode-range 限定)
270 lines
8.4 KiB
CSS
270 lines
8.4 KiB
CSS
@tailwind base;
|
|
@tailwind components;
|
|
@tailwind utilities;
|
|
|
|
/*
|
|
设计令牌(颜色)的 CSS 侧来源。
|
|
─────────────────────────────────────────────────────────────
|
|
此前 index.css 里三处直接写死了十六进制字面量:
|
|
· :focus-visible 的 outline → #9e1b1b
|
|
· .masthead-rule 的 border-top → #17140f
|
|
· .font-brush 的 color → #9E1B1B(还是大写写法)
|
|
它们与 tailwind.config.js 的 press / ink 令牌是同一批颜色,却没有
|
|
共享来源。后果:调整品牌红时改令牌不会影响这三处 —— 焦点轮廓、
|
|
报头粗线、毛笔字会保持旧色,出现「两个印报红,其中一个没有名字」。
|
|
|
|
现在把颜色定义在 :root,由 tailwind.config.js 消费同一份变量,
|
|
使 CSS 与工具类共享单一来源。改色只需改这里一处。
|
|
|
|
⚠️ 值必须写成 RGB 三元组(如 23 20 15)而非 #17140F。
|
|
原因:Tailwind 的透明度修饰符(bg-press/60)需要把颜色拆成
|
|
rgb(R G B / <alpha-value>) 才能注入 alpha。若变量本身是完整
|
|
十六进制,`bg-press-wash/60` 这类写法会**静默不生成任何 CSS** ——
|
|
编译不报错,但样式消失(实测踩到过)。三元组写法同时兼容
|
|
不需要 alpha 的场景。
|
|
*/
|
|
:root {
|
|
/* 纸白:微暖底色,像新闻纸而不是纯白画布 */
|
|
--paper-50: 253 252 248;
|
|
--paper-100: 247 244 236;
|
|
--paper-200: 237 233 222;
|
|
--paper-300: 221 215 199;
|
|
|
|
/* 墨色:暖黑灰阶 */
|
|
--ink-900: 23 20 15;
|
|
--ink-700: 59 54 46;
|
|
--ink-500: 110 103 91;
|
|
--ink-400: 156 149 135;
|
|
--ink-300: 201 196 184;
|
|
--ink-200: 226 223 215;
|
|
|
|
/* 印报红:全站唯一强调色 */
|
|
--press: 158 27 27;
|
|
--press-dark: 124 20 20;
|
|
--press-wash: 247 233 228;
|
|
}
|
|
|
|
/* 毛体草书(刘建毛草) - 本地自托管子集(仅「先知」两字,约 1KB)
|
|
原先直连 jsDelivr 的 @main 分支(未锁版本、国内加载不稳、5MB 全字库)。
|
|
现仅保留报头用字,构建期随包发布,消除外部单点依赖。 */
|
|
@font-face {
|
|
font-family: 'Liu Jian Mao Cao';
|
|
src: url('./assets/fonts/LiuJianMaoCao-subset.woff2') format('woff2');
|
|
font-display: swap;
|
|
font-weight: 400;
|
|
unicode-range: U+5148, U+77E5; /* 先 U+5148 / 知 U+77E5 */
|
|
}
|
|
|
|
/* 粗毛笔字效果:笔触加粗 + 微描边 */
|
|
.font-brush {
|
|
font-weight: 700;
|
|
-webkit-text-stroke: 0.3px currentColor;
|
|
text-shadow: 0.5px 0.5px 0 currentColor;
|
|
}
|
|
|
|
/* 页面内容横向轻扫+淡入 */
|
|
.page-content-enter {
|
|
animation: pageSlideIn 0.3s ease-out;
|
|
}
|
|
@keyframes pageSlideIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateX(20px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateX(0);
|
|
}
|
|
}
|
|
|
|
@layer base {
|
|
html {
|
|
-webkit-text-size-adjust: 100%;
|
|
text-rendering: optimizeLegibility;
|
|
}
|
|
|
|
body {
|
|
@apply bg-paper-50 text-ink-800 font-sans antialiased;
|
|
font-feature-settings: 'tnum' 1; /* 数字等宽:比分/百分比不跳动 */
|
|
}
|
|
|
|
::selection {
|
|
@apply bg-press-wash text-press;
|
|
}
|
|
|
|
/* 统一焦点环:印报红细线,键盘可达 */
|
|
:focus-visible {
|
|
outline: 2px solid var(--press);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
/* 细滚动条 */
|
|
::-webkit-scrollbar {
|
|
width: 10px;
|
|
height: 10px;
|
|
}
|
|
::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
::-webkit-scrollbar-thumb {
|
|
@apply rounded-full bg-ink-300;
|
|
}
|
|
::-webkit-scrollbar-thumb:hover {
|
|
@apply bg-ink-400;
|
|
}
|
|
|
|
/* 尊重「减少动态效果」偏好 */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
*,
|
|
*::before,
|
|
*::after {
|
|
animation-duration: 0.01ms !important;
|
|
animation-iteration-count: 1 !important;
|
|
transition-duration: 0.01ms !important;
|
|
}
|
|
}
|
|
}
|
|
|
|
@layer components {
|
|
/* ── 报头双线:粗线在上、细线在下 ── */
|
|
.masthead-rule {
|
|
border-top: 3px solid var(--ink-900);
|
|
}
|
|
|
|
/* ── 按钮:方正边框式,悬停反白 ── */
|
|
.btn {
|
|
@apply inline-flex items-center justify-center gap-1.5 border border-ink-300 bg-transparent px-3
|
|
text-sm text-ink-700 transition-colors duration-150
|
|
hover:border-ink-900 hover:bg-ink-900 hover:text-paper-50
|
|
disabled:cursor-not-allowed disabled:opacity-40
|
|
disabled:hover:border-ink-300 disabled:hover:bg-transparent disabled:hover:text-ink-700
|
|
min-h-[44px] py-1.5;
|
|
}
|
|
.btn-sm {
|
|
@apply px-2.5 text-xs min-h-[36px];
|
|
}
|
|
.btn-solid {
|
|
@apply border-ink-900 bg-ink-900 text-paper-50 hover:border-press hover:bg-press;
|
|
}
|
|
.btn-danger {
|
|
@apply border-press bg-transparent text-press hover:bg-press hover:text-paper-50;
|
|
}
|
|
/* 描边确认按钮:次级动作中需要比默认 btn 更明确轮廓的场合
|
|
(此前三处使用但从未定义,样式静默失效) */
|
|
.btn-outline {
|
|
@apply border-ink-900 bg-transparent text-ink-900
|
|
hover:border-press hover:bg-press-wash hover:text-press-dark;
|
|
}
|
|
/* 幽灵按钮:无边、无底色,仅 hover 时浮出浅纸底。
|
|
用于「重置」「取消」等最轻量的次要动作。
|
|
(EvalPage 曾使用但从未定义,与 btn-outline 同型问题;
|
|
现由 src/components/ui 的 Button variant 类型约束兜底,不再可能静默失效) */
|
|
.btn-ghost {
|
|
@apply border-transparent bg-transparent text-ink-500
|
|
hover:bg-paper-200 hover:text-ink-900
|
|
disabled:hover:bg-transparent disabled:hover:text-ink-500;
|
|
}
|
|
|
|
/* ── 弹窗入场:遮罩淡入 + 面板上浮(仅 opacity/transform,GPU 友好) ── */
|
|
.modal-overlay-enter {
|
|
animation: modal-fade 0.18s ease-out both;
|
|
}
|
|
.modal-panel-enter {
|
|
animation: modal-rise 0.2s cubic-bezier(0.22, 1, 0.36, 1) both;
|
|
}
|
|
@keyframes modal-fade {
|
|
from { opacity: 0; }
|
|
to { opacity: 1; }
|
|
}
|
|
@keyframes modal-rise {
|
|
from { opacity: 0; transform: translateY(12px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
/* ── 不确定态进度条(indeterminate) ──
|
|
用于「耗时未知」的等待场景(如多专家预测)。与确定性进度条的区别
|
|
是它不表达百分比,只表达「仍在进行」—— 因此不存在「卡在 95%」
|
|
这种误导。transform-only,GPU 友好。 */
|
|
.predict-scan {
|
|
animation: predict-scan 1.4s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
}
|
|
@keyframes predict-scan {
|
|
0% { transform: translateX(-100%); }
|
|
100% { transform: translateX(300%); }
|
|
}
|
|
|
|
/* ── 统一空态 ── */
|
|
.empty-state {
|
|
@apply border-y border-ink-200 py-12 text-center;
|
|
}
|
|
.empty-state-title {
|
|
@apply font-serif text-sm text-ink-600;
|
|
}
|
|
.empty-state-sub {
|
|
@apply mt-1.5 text-xs text-ink-400;
|
|
}
|
|
.empty-state-action {
|
|
@apply mt-4 inline-flex items-center gap-2 text-2xs text-press hover:text-press-dark transition-colors;
|
|
}
|
|
|
|
/* ── 统一错误横幅(前台用,后台用 Alert) ── */
|
|
.error-banner {
|
|
@apply flex items-start justify-between gap-3 border border-press bg-press-wash px-4 py-3;
|
|
}
|
|
.error-banner-title {
|
|
@apply text-sm font-medium text-press;
|
|
}
|
|
.error-banner-detail {
|
|
@apply mt-0.5 text-xs text-ink-600;
|
|
}
|
|
|
|
/* ── 表单控件:方正、无圆角 ── */
|
|
.field {
|
|
@apply border border-ink-300 bg-transparent px-2.5 py-1.5 text-sm text-ink-800
|
|
transition-colors hover:border-ink-400
|
|
focus:border-press focus:outline-none;
|
|
}
|
|
|
|
/* ── 版面切换文字标签(联赛/状态/模式) ── */
|
|
.tab {
|
|
@apply relative whitespace-nowrap px-0.5 py-1 text-sm text-ink-500 transition-colors hover:text-ink-900;
|
|
}
|
|
.tab-on {
|
|
@apply font-medium text-press;
|
|
}
|
|
.tab-on::after {
|
|
content: '';
|
|
@apply absolute inset-x-0 bottom-0 h-0.5 bg-press;
|
|
}
|
|
|
|
/* ── 小节标题:宋体加粗 + 墨色底线 ── */
|
|
.section-head {
|
|
@apply border-b border-ink-900 pb-1.5 font-serif text-sm font-bold text-ink-900;
|
|
}
|
|
|
|
/* ── 骨架占位:低调脉动,不用渐变扫光 ── */
|
|
.skeleton {
|
|
@apply animate-pulse rounded-none bg-ink-200;
|
|
}
|
|
|
|
/* ── 侧边栏图标:统一样式 + hover过渡到品牌色 ── */
|
|
.nav-icon {
|
|
width: 1.125rem;
|
|
height: 1.125rem;
|
|
stroke-width: 1.5;
|
|
flex-shrink: 0;
|
|
opacity: 0.5;
|
|
transition: all 0.2s ease;
|
|
}
|
|
.nav-icon-wrap:hover .nav-icon,
|
|
.nav-icon-wrap.active .nav-icon {
|
|
opacity: 1;
|
|
color: var(--press);
|
|
stroke-width: 2;
|
|
}
|
|
}
|
|
|
|
/* ── Admin 后台 ──
|
|
与前台共用同一套报刊风组件(.btn/.field/.tab/.section-head/.skeleton),
|
|
不再单独维护暗色主题。 */
|