diff --git a/docs/rebuild-spec/02-features.md b/docs/rebuild-spec/02-features.md index 35c7f50..0befb39 100644 --- a/docs/rebuild-spec/02-features.md +++ b/docs/rebuild-spec/02-features.md @@ -12,7 +12,7 @@ ## A. 浏览与布局 - [x] 三栏布局:左导航 / 中 Feed / 右栏小组件 — SSR(桌面;≤900px 隐藏右栏) -- [ ] 浅色 / 暗色主题;跟随系统偏好并本地记忆 +- [x] 浅色 / 暗色主题;跟随系统偏好并本地记忆 — `data-theme` + `localStorage`(`j13-theme`);顶栏循环:跟随系统 / 浅色 / 暗色 - [ ] 响应式:平板/手机收起侧栏 - [ ] 长列表虚拟滚动或等价流畅方案 - [x] Feed 排序:`latest`(最新发帖)/ `reply`(最新回复)/ `hot`(热门) diff --git a/docs/rebuild-spec/06-pages-ux.md b/docs/rebuild-spec/06-pages-ux.md index e8d2dff..116be5e 100644 --- a/docs/rebuild-spec/06-pages-ux.md +++ b/docs/rebuild-spec/06-pages-ux.md @@ -105,7 +105,10 @@ ### 2.5 主题 -- 浅色 / 暗色;`localStorage` 记忆;可跟随系统 +- 浅色 / 暗色;默认 **跟随系统**;`localStorage` 键 `j13-theme`(`system` | `light` | `dark`) +- ``;`` 内联脚本防首屏闪烁 +- 顶栏按钮循环切换;系统偏好变化时在 `system` 模式下即时更新 +- 兼容旧 SPA 仅存 `light`/`dark` 的值 --- diff --git a/docs/rebuild-spec/09-ssr-progress.md b/docs/rebuild-spec/09-ssr-progress.md index de7fa03..1b5cf5e 100644 --- a/docs/rebuild-spec/09-ssr-progress.md +++ b/docs/rebuild-spec/09-ssr-progress.md @@ -10,15 +10,15 @@ | 项 | 值 | |----|-----| -| **上一刀** | Markdown 编辑器(工具栏 + 预览,共用 md_editor) | -| **下一刀** | 需点名(OIDC / S3 / Gitea / 主题 / 表格表情…) | +| **上一刀** | 浅色/暗色主题(跟随系统 + localStorage) | +| **下一刀** | 需点名(OIDC / S3 / Gitea / 表格表情…) | | **工作区** | 应干净 | --- ## 一句话状态 -核心论坛、Admin、Limits/伪静态、嵌套评论、**Markdown 编辑器**已齐。TipTap / 表格 / 表情贴纸仍后置。 +核心论坛、Admin、Limits/伪静态、嵌套评论、Markdown 编辑器、**主题**已齐。余量多为后置与体验打磨。 --- @@ -30,12 +30,13 @@ | Limits / 伪静态 / 品牌图 | 已迁 | | 评论嵌套树 UI | 已迁 | | Markdown 工具栏 + preview | 已迁 | +| 浅色 / 暗色 / 跟随系统 | 已迁 | --- ## 未完成 -- P3:主题、TipTip、游客评论、修订 diff +- P3:TipTip、游客评论、修订 diff、响应式侧栏抽屉 - 编辑器后置:TipTap、表格、图片组、表情贴纸 - 后置:OIDC、S3 热切换、Gitea `/projects` diff --git a/public/assets/site.css b/public/assets/site.css index 5945bc4..d8e34a7 100644 --- a/public/assets/site.css +++ b/public/assets/site.css @@ -1,13 +1,66 @@ /* 姜十三论坛 SSR 骨架样式(web_src → public/assets) */ :root { + color-scheme: light; --j13-bg: #f6f4ef; --j13-surface: #fffdf8; --j13-text: #1c1917; --j13-muted: #78716c; --j13-accent: #0f766e; + --j13-on-accent: #fff; --j13-border: #e7e5e4; --j13-radius: 8px; --j13-font: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; + --j13-danger: #b91c1c; + --j13-ok-bg: #ecfdf5; + --j13-ok-fg: #065f46; + --j13-warn-bg: #fff7ed; + --j13-warn-fg: #9a3412; + --j13-warn-border: #fed7aa; + --j13-err-bg: #fef2f2; + --j13-err-fg: #991b1b; + --j13-err-border: #fecaca; + --j13-pin-bg: #fef3c7; + --j13-pin-fg: #92400e; + --j13-feat-bg: #ede9fe; + --j13-feat-fg: #5b21b6; + --j13-neutral-bg: #f8fafc; + --j13-neutral-fg: #475569; + --j13-self-bg: #eff6ff; + --j13-self-border: #bfdbfe; + --j13-delta-pos: #15803d; + --j13-delta-neg: #b91c1c; + --j13-overlay: rgba(255, 255, 255, 0.85); +} + +html[data-theme="dark"] { + color-scheme: dark; + --j13-bg: #1c1917; + --j13-surface: #292524; + --j13-text: #f5f5f4; + --j13-muted: #a8a29e; + --j13-accent: #2dd4bf; + --j13-on-accent: #134e4a; + --j13-border: #44403c; + --j13-danger: #f87171; + --j13-ok-bg: #064e3b; + --j13-ok-fg: #a7f3d0; + --j13-warn-bg: #7c2d12; + --j13-warn-fg: #fed7aa; + --j13-warn-border: #9a3412; + --j13-err-bg: #7f1d1d; + --j13-err-fg: #fecaca; + --j13-err-border: #991b1b; + --j13-pin-bg: #78350f; + --j13-pin-fg: #fde68a; + --j13-feat-bg: #4c1d95; + --j13-feat-fg: #ddd6fe; + --j13-neutral-bg: #334155; + --j13-neutral-fg: #e2e8f0; + --j13-self-bg: #1e3a5f; + --j13-self-border: #1d4ed8; + --j13-delta-pos: #4ade80; + --j13-delta-neg: #f87171; + --j13-overlay: rgba(28, 25, 23, 0.85); } * { box-sizing: border-box; } @@ -62,9 +115,25 @@ body.j13-body { .j13-brand__text { display: flex; flex-direction: column; } .j13-brand__text strong { font-size: 1rem; } .j13-brand__slogan { font-size: 0.75rem; color: var(--j13-muted); } -.j13-nav { display: flex; gap: 0.85rem; flex-wrap: wrap; } +.j13-nav { display: flex; gap: 0.85rem; flex-wrap: wrap; align-items: center; } .j13-nav a { color: var(--j13-accent); text-decoration: none; font-size: 0.9rem; } .j13-nav a:hover { text-decoration: underline; } +.j13-theme-toggle { + appearance: none; + border: 1px solid var(--j13-border); + background: var(--j13-surface); + color: var(--j13-muted); + border-radius: var(--j13-radius); + padding: 0.2rem 0.55rem; + font: inherit; + font-size: 0.8rem; + cursor: pointer; + line-height: 1.3; +} +.j13-theme-toggle:hover { + color: var(--j13-accent); + border-color: var(--j13-accent); +} .j13-layout { max-width: 1200px; @@ -270,7 +339,7 @@ body.j13-body { } .j13-search__err { margin: 0.5rem 0 0; - color: #b91c1c; + color: var(--j13-danger); font-size: 0.875rem; } @@ -297,15 +366,15 @@ body.j13-body { align-items: center; } .j13-tag { - background: #ecfdf5; + background: var(--j13-ok-bg); color: var(--j13-accent); padding: 0.1rem 0.4rem; border-radius: 4px; font-size: 0.75rem; } -.j13-tag--pin { background: #fef3c7; color: #92400e; } -.j13-tag--pending { background: #fff7ed; color: #9a3412; } -.j13-tag--rejected { background: #fef2f2; color: #991b1b; } +.j13-tag--pin { background: var(--j13-pin-bg); color: var(--j13-pin-fg); } +.j13-tag--pending { background: var(--j13-warn-bg); color: var(--j13-warn-fg); } +.j13-tag--rejected { background: var(--j13-err-bg); color: var(--j13-err-fg); } .j13-mod-banner { margin: 0.5rem 0 0.75rem; padding: 0.65rem 0.85rem; @@ -314,18 +383,18 @@ body.j13-body { line-height: 1.4; } .j13-mod-banner--pending { - background: #fff7ed; - color: #9a3412; - border: 1px solid #fed7aa; + background: var(--j13-warn-bg); + color: var(--j13-warn-fg); + border: 1px solid var(--j13-warn-border); } .j13-mod-banner--rejected { - background: #fef2f2; - color: #991b1b; - border: 1px solid #fecaca; + background: var(--j13-err-bg); + color: var(--j13-err-fg); + border: 1px solid var(--j13-err-border); } -.j13-tag--feat { background: #ede9fe; color: #5b21b6; } -.j13-tag--resolved { background: #ecfdf5; color: #065f46; } -.j13-tag--unresolved { background: #f8fafc; color: #475569; } +.j13-tag--feat { background: var(--j13-feat-bg); color: var(--j13-feat-fg); } +.j13-tag--resolved { background: var(--j13-ok-bg); color: var(--j13-ok-fg); } +.j13-tag--unresolved { background: var(--j13-neutral-bg); color: var(--j13-neutral-fg); } .j13-empty { color: var(--j13-muted); } .j13-pager { @@ -351,8 +420,8 @@ body.j13-body { padding: 0.65rem 1rem; border-radius: var(--j13-radius); } -.j13-flash { background: #ecfdf5; color: #065f46; } -.j13-alert--error { background: #fef2f2; color: #991b1b; } +.j13-flash { background: var(--j13-ok-bg); color: var(--j13-ok-fg); } +.j13-alert--error { background: var(--j13-err-bg); color: var(--j13-err-fg); } .j13-main--solo { max-width: 480px; @@ -372,6 +441,8 @@ body.j13-body { border: 1px solid var(--j13-border); border-radius: var(--j13-radius); font: inherit; + background: var(--j13-surface); + color: var(--j13-text); } .j13-form button, .j13-post__actions button, .j13-comment-form button { margin-top: 0.5rem; @@ -379,7 +450,7 @@ body.j13-body { border: 0; border-radius: var(--j13-radius); background: var(--j13-accent); - color: #fff; + color: var(--j13-on-accent); cursor: pointer; font: inherit; } @@ -478,7 +549,8 @@ body.j13-body { border: 1px solid var(--j13-border); border-radius: var(--j13-radius); font: inherit; - background: #fff; + background: var(--j13-surface); + color: var(--j13-text); } .j13-form__row { display: flex; @@ -770,8 +842,8 @@ body.j13-body { vertical-align: top; } .j13-profile__ledger th { color: var(--j13-muted); font-weight: 600; } -.j13-delta--pos { color: #15803d; } -.j13-delta--neg { color: #b91c1c; } +.j13-delta--pos { color: var(--j13-delta-pos); } +.j13-delta--neg { color: var(--j13-delta-neg); } .j13-btn { display: inline-block; padding: 0.35rem 0.85rem; @@ -862,8 +934,8 @@ body.j13-body { } .j13-msg-bubble.is-self { align-self: flex-end; - background: #eff6ff; - border-color: #bfdbfe; + background: var(--j13-self-bg); + border-color: var(--j13-self-border); } .j13-msg-bubble.is-system { max-width: 100%; } .j13-msg-bubble__subj { font-weight: 600; margin-bottom: 0.25rem; } @@ -980,7 +1052,7 @@ points-only[data-locked="false"] { height: 48px; object-fit: contain; border-radius: 6px; - background: #fff; + background: var(--j13-surface); } .j13-links__logo--sm { width: 24px; @@ -1211,7 +1283,7 @@ points-only[data-locked="false"] { top: 0.4rem; left: 0.4rem; z-index: 1; - background: rgba(255,255,255,0.85); + background: var(--j13-overlay); border-radius: 3px; padding: 0.1rem 0.2rem; } diff --git a/public/assets/site.js b/public/assets/site.js index 89acb95..948e40d 100644 --- a/public/assets/site.js +++ b/public/assets/site.js @@ -1,6 +1,73 @@ // 姜十三论坛 SSR 渐进增强 document.documentElement.dataset.j13Ssr = "1"; +(function themeToggle() { + var KEY = "j13-theme"; + var LABELS = { system: "跟随系统", light: "浅色", dark: "暗色" }; + var ORDER = ["system", "light", "dark"]; + + function normalizePref(raw) { + if (raw === "light" || raw === "dark" || raw === "system") return raw; + return "system"; + } + + function resolve(pref) { + if (pref === "dark") return "dark"; + if (pref === "light") return "light"; + try { + return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; + } catch (e) { + return "light"; + } + } + + function apply(pref) { + pref = normalizePref(pref); + var resolved = resolve(pref); + document.documentElement.setAttribute("data-theme", resolved); + document.documentElement.setAttribute("data-theme-pref", pref); + document.documentElement.style.colorScheme = resolved; + try { + localStorage.setItem(KEY, pref); + } catch (e) {} + var btn = document.getElementById("j13-theme-toggle"); + if (btn) { + btn.textContent = LABELS[pref] || LABELS.system; + btn.setAttribute("aria-label", "主题:" + (LABELS[pref] || "")); + btn.title = "当前:" + (LABELS[pref] || "") + "(点击切换)"; + } + } + + function currentPref() { + try { + return normalizePref(localStorage.getItem(KEY) || document.documentElement.getAttribute("data-theme-pref") || "system"); + } catch (e) { + return "system"; + } + } + + apply(currentPref()); + + var btn = document.getElementById("j13-theme-toggle"); + if (btn) { + btn.addEventListener("click", function () { + var cur = currentPref(); + var idx = ORDER.indexOf(cur); + var next = ORDER[(idx + 1) % ORDER.length]; + apply(next); + }); + } + + try { + var mq = window.matchMedia("(prefers-color-scheme: dark)"); + var onChange = function () { + if (currentPref() === "system") apply("system"); + }; + if (mq.addEventListener) mq.addEventListener("change", onChange); + else if (mq.addListener) mq.addListener(onChange); + } catch (e) {} +})(); + (function mdEditors() { function insertAtCursor(textarea, text) { const start = textarea.selectionStart || 0; diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl index 421b72f..c9fdcfb 100644 --- a/templates/base/head.tmpl +++ b/templates/base/head.tmpl @@ -10,6 +10,20 @@ {{if .Description}}{{end}} {{if .OGImageURL}}{{end}} + diff --git a/templates/base/navbar.tmpl b/templates/base/navbar.tmpl index c3bcd85..3c0836f 100644 --- a/templates/base/navbar.tmpl +++ b/templates/base/navbar.tmpl @@ -13,6 +13,7 @@