feat(ui): improve readability and fix workspace regressions

Separate interface typography from document text and enlarge controls across the workspace and public reader. Keep tree menus within the viewport and make constrained dialogs scrollable.

Preserve nested image references, escape moved link paths, synchronize note creation and daily-note opening, and hydrate shared headings. Add regression coverage for settings and workspace behavior.
This commit is contained in:
eezd committed 2026-10-23 10:55:53 +08:00
1 parent 4d1339851b
commit a716794e41
31 files changed
+556 -246

No files matched your search

+7
View File
@@ -1810,6 +1810,13 @@
"dark"
]
},
"uiFontSize": {
"type": "integer",
"minimum": 14,
"maximum": 20,
"default": 16,
"description": "界面字号,与正文排版字号独立"
},
"fontSize": {
"type": "integer"
},
+1 -1
View File
@@ -154,7 +154,7 @@ func (s *Server) publicAppearance(w http.ResponseWriter, r *http.Request) {
return
}
out := map[string]any{}
for _, key := range []string{"colorMode", "fontSize", "contentWidth", "textFont", "codeFont", "lineHeight", "paragraphSpacing", "glassOpacity"} {
for _, key := range []string{"colorMode", "uiFontSize", "fontSize", "contentWidth", "textFont", "codeFont", "lineHeight", "paragraphSpacing", "glassOpacity"} {
if value, ok := settings[key]; ok {
out[key] = value
}
+2 -2
View File
@@ -43,7 +43,7 @@ func TestThemeAndShareAppearanceBoundaries(t *testing.T) {
t.Fatal("未读取到主题 CSS")
}
f.write("笔记.md", "正文")
f.do(f.admin, "PATCH", "/api/v1/settings", `{"theme":"paper","colorMode":"dark","fontSize":18,"privateSetting":"不公开","cssSnippets":[{"id":"a","name":"启用","css":"body { color: red }","enabled":true},{"id":"b","name":"停用","css":"不公开片段","enabled":false}]}`, nil)
f.do(f.admin, "PATCH", "/api/v1/settings", `{"theme":"paper","colorMode":"dark","fontSize":18,"uiFontSize":20,"privateSetting":"不公开","cssSnippets":[{"id":"a","name":"启用","css":"body { color: red }","enabled":true},{"id":"b","name":"停用","css":"不公开片段","enabled":false}]}`, nil)
sh := decode[shareView](t, f.do(f.admin, "POST", "/api/v1/shares", `{"path":"笔记.md","password":"访问密码"}`, nil))
base := "/api/v1/public/shares/" + sh.Token
if code := f.status(guest, "GET", base+"/appearance", "", nil); code != 401 {
@@ -59,7 +59,7 @@ func TestThemeAndShareAppearanceBoundaries(t *testing.T) {
if err := json.Unmarshal(raw, &appearance); err != nil {
t.Fatal(err)
}
if res.StatusCode != 200 || appearance["colorMode"] != "dark" || !strings.Contains(appearance["themeCSS"].(string), "#faf6ed") {
if res.StatusCode != 200 || appearance["colorMode"] != "dark" || appearance["uiFontSize"] != float64(20) || appearance["fontSize"] != float64(18) || !strings.Contains(appearance["themeCSS"].(string), "#faf6ed") {
t.Fatalf("分享外观错误:%s", raw)
}
if strings.Contains(string(raw), "不公开") || len(appearance["cssSnippets"].([]any)) != 1 {
+2 -2
View File
@@ -19,8 +19,8 @@ import (
"rawmark/internal/vault"
)
// 修改表结构时递增,旧索引会被丢弃重建
const schemaVersion = 2
// 表结构或派生元数据的解析规则变化时递增,旧索引会被丢弃重建。
const schemaVersion = 3
const schema = `
CREATE TABLE docs (
+19
View File
@@ -205,3 +205,22 @@ func TestMetadataTracksChangesAndRebuild(t *testing.T) {
t.Fatalf("索引恢复错误:%+v %v", notes, err)
}
}
func TestRebuildsLinkedImageMetadataFromOldIndex(t *testing.T) {
f := newFixture(t)
f.write("a.md", "[![预览](assets/used.png)](https://example.com)")
f.sync()
// 模拟旧版索引漏记图片引用;笔记正文和 rev 均不改变。
if _, err := f.idx.db.Exec(`UPDATE docs SET metadata = ?`, `{"title":"a","tags":[],"links":[],"assets":[],"warnings":[]}`); err != nil {
t.Fatal(err)
}
if _, err := f.idx.db.Exec(`PRAGMA user_version = 2`); err != nil {
t.Fatal(err)
}
f.reopen()
f.sync()
notes, ready, err := f.idx.Metadata()
if err != nil || !ready || len(notes) != 1 || len(notes[0].Assets) != 1 || notes[0].Assets[0].Target != "assets/used.png" {
t.Fatalf("旧索引未重新提取图片引用:%+v %v", notes, err)
}
}
+30
View File
@@ -66,3 +66,33 @@ func TestFolderUploadReusesDeduplication(t *testing.T) {
t.Fatalf("目标目录不存在应报错:%v", err)
}
}
func TestCleanupProtectsLinkedImages(t *testing.T) {
for _, style := range []string{"内联链接", "引用式链接"} {
t.Run(style, func(t *testing.T) {
v := newVault(t)
link, err := v.SaveFolderAsset("", "preview.png", strings.NewReader("image"), 100)
if err != nil {
t.Fatal(err)
}
content := "[![预览](" + link + ")](https://example.com)"
if style == "引用式链接" {
content = "[![预览][image]][site]\n\n[image]: " + link + "\n[site]: https://example.com\n"
}
if _, err := v.Write("a.md", []byte(content), Precondition{}); err != nil {
t.Fatal(err)
}
metadata := Analyze("a.md", []byte(content))
if len(metadata.Assets) != 1 || metadata.Assets[0].Target != link {
t.Fatalf("链接内图片未记录引用:%+v", metadata)
}
result, err := v.CleanupImages([]string{link}, func() ([]string, error) { return nil, nil })
if err != nil || len(result.Deleted) != 0 || len(result.Skipped) != 1 || result.Skipped[0].Reason != "仍被笔记引用" {
t.Fatalf("链接内图片未受保护:%+v %v", result, err)
}
if _, err := v.Stat(link); err != nil {
t.Fatal("正文引用的图片不应被移走", err)
}
})
}
}
+1 -1
View File
@@ -132,7 +132,7 @@ func Analyze(note string, content []byte) Metadata {
return ast.WalkSkipChildren, nil
case *ast.Link:
addRef(node.Destination.Str(source), node.Pos())
return ast.WalkSkipChildren, nil
// 链接文字中可以嵌套图片,仍须记录其资源引用。
case *ast.Image:
addRef(node.Destination.Str(source), node.Pos())
return ast.WalkSkipChildren, nil
+24
View File
@@ -93,3 +93,27 @@ func TestRenamedAssetsPreserveMathExamples(t *testing.T) {
t.Fatalf("公式示例被修改:%s", got)
}
}
func TestMovePreservesEntityShapedFilenames(t *testing.T) {
v := newVault(t)
for p, body := range map[string]string{"source.md": "[目标](target.md)", "target.md": "正文"} {
if _, err := v.Write(p, []byte(body), Precondition{}); err != nil {
t.Fatal(err)
}
}
const target = "a©.md"
if _, err := v.Move("target.md", target, nil); err != nil {
t.Fatal(err)
}
body, _, err := v.Read("source.md")
if err != nil {
t.Fatal(err)
}
metadata := Analyze("source.md", body)
if string(body) != "[目标](a%26copy;.md)" || len(metadata.Links) != 1 || metadata.Links[0].Target != target {
t.Fatalf("改名后的链接指向错误:%s %+v", body, metadata.Links)
}
if _, _, err := v.Read(metadata.Links[0].Target); err != nil {
t.Fatal("改写后的链接无法打开", err)
}
}
+1 -1
View File
@@ -785,4 +785,4 @@ func EscapeLinkPath(s string) string {
return linkEscaper.Replace(s)
}
var linkEscaper = strings.NewReplacer("%", "%25", "#", "%23", "?", "%3F", " ", "%20", "(", "%28", ")", "%29", "<", "%3C", ">", "%3E")
var linkEscaper = strings.NewReplacer("%", "%25", "&", "%26", "#", "%23", "?", "%3F", " ", "%20", "(", "%28", ")", "%29", "<", "%3C", ">", "%3E")