余白 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:
@@ -113,6 +113,122 @@ func TestSettingsLightSkinFallback(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingsUIFallback(t *testing.T) {
|
||||
s := openTestStore(t)
|
||||
|
||||
st, err := s.GetSettings()
|
||||
if err != nil {
|
||||
t.Fatalf("get: %v", err)
|
||||
}
|
||||
if st.UIID != UIClassic {
|
||||
t.Errorf("default ui_id should be %q, got %q", UIClassic, st.UIID)
|
||||
}
|
||||
if st.CustomCSS == nil {
|
||||
t.Error("CustomCSS should never be nil")
|
||||
}
|
||||
|
||||
// An unknown ui_id is coerced to the default on write.
|
||||
st.UIID = "bogus"
|
||||
if err := s.UpdateSettings(st); err != nil {
|
||||
t.Fatalf("update: %v", err)
|
||||
}
|
||||
got, _ := s.GetSettings()
|
||||
if got.UIID != UIClassic {
|
||||
t.Errorf("bogus ui_id should fall back to %q, got %q", UIClassic, got.UIID)
|
||||
}
|
||||
|
||||
// A valid one persists.
|
||||
st.UIID = UIVivid
|
||||
if err := s.UpdateSettings(st); err != nil {
|
||||
t.Fatalf("update again: %v", err)
|
||||
}
|
||||
got, _ = s.GetSettings()
|
||||
if got.UIID != UIVivid {
|
||||
t.Errorf("ui_id should persist, got %q", got.UIID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingsUIFallbackOnRead(t *testing.T) {
|
||||
s := openTestStore(t)
|
||||
// A hand-edited / legacy row must not leak an unknown UI to clients.
|
||||
if _, err := s.db.Exec(s.db.Q(`INSERT INTO settings(key,value) VALUES (?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value`),
|
||||
"ui_id", "neon"); err != nil {
|
||||
t.Fatalf("seed ui_id: %v", err)
|
||||
}
|
||||
st, err := s.GetSettings()
|
||||
if err != nil {
|
||||
t.Fatalf("get: %v", err)
|
||||
}
|
||||
if st.UIID != UIClassic {
|
||||
t.Errorf("expected read-side fallback to %q, got %q", UIClassic, st.UIID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingsCustomCSSRoundTrip(t *testing.T) {
|
||||
s := openTestStore(t)
|
||||
|
||||
// Seeding a raw row with an unknown section + a valid one.
|
||||
if _, err := s.db.Exec(s.db.Q(`INSERT INTO settings(key,value) VALUES (?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value`),
|
||||
"custom_css", `{"home":"body{--v-accent:#f0f}","bogus":"x{}"}`); err != nil {
|
||||
t.Fatalf("seed custom_css: %v", err)
|
||||
}
|
||||
st, _ := s.GetSettings()
|
||||
if st.CustomCSS["home"] != "body{--v-accent:#f0f}" {
|
||||
t.Errorf("home css not read back: %+v", st.CustomCSS)
|
||||
}
|
||||
if _, ok := st.CustomCSS["bogus"]; ok {
|
||||
t.Error("unknown section should be dropped on read")
|
||||
}
|
||||
|
||||
// Write side: unknown sections dropped, XML-hostile chars survive, nil clears.
|
||||
st.CustomCSS = map[string]string{
|
||||
"home": `a::after{content:"<&>"}`,
|
||||
"bogus": "x{}",
|
||||
}
|
||||
if err := s.UpdateSettings(st); err != nil {
|
||||
t.Fatalf("update: %v", err)
|
||||
}
|
||||
got, _ := s.GetSettings()
|
||||
if got.CustomCSS["home"] != `a::after{content:"<&>"}` {
|
||||
t.Errorf("css mangled in round trip: %q", got.CustomCSS["home"])
|
||||
}
|
||||
if len(got.CustomCSS) != 1 {
|
||||
t.Errorf("unknown section should be dropped on write, got %+v", got.CustomCSS)
|
||||
}
|
||||
|
||||
// A nil map is a full clear, and still comes back as a non-nil empty map.
|
||||
got.CustomCSS = nil
|
||||
if err := s.UpdateSettings(got); err != nil {
|
||||
t.Fatalf("clear: %v", err)
|
||||
}
|
||||
after, _ := s.GetSettings()
|
||||
if after.CustomCSS == nil {
|
||||
t.Error("CustomCSS should be non-nil after clearing")
|
||||
}
|
||||
if len(after.CustomCSS) != 0 {
|
||||
t.Errorf("expected cleared map, got %+v", after.CustomCSS)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingsCustomCSSOverLongSectionDropped(t *testing.T) {
|
||||
s := openTestStore(t)
|
||||
st, _ := s.GetSettings()
|
||||
st.CustomCSS = map[string]string{
|
||||
"home": strings.Repeat("a", maxSectionCSSBytes+1),
|
||||
"about": "b{color:red}",
|
||||
}
|
||||
if err := s.UpdateSettings(st); err != nil {
|
||||
t.Fatalf("update: %v", err)
|
||||
}
|
||||
got, _ := s.GetSettings()
|
||||
if _, ok := got.CustomCSS["home"]; ok {
|
||||
t.Error("over-long section should be dropped")
|
||||
}
|
||||
if got.CustomCSS["about"] != "b{color:red}" {
|
||||
t.Errorf("valid section should survive, got %+v", got.CustomCSS)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStoreSQLite runs a small integration pass on top of an in-memory SQLite
|
||||
// database to exercise the new fields (cover_url, tag color) and the merge
|
||||
// + bulk + dashboard paths.
|
||||
|
||||
Reference in New Issue
Block a user