From ea716b26287c187f8459e84d04ea71e4de652cd7 Mon Sep 17 00:00:00 2001 From: wusumac <736139669@qq.com> Date: Wed, 12 Aug 2026 18:43:33 +0800 Subject: [PATCH] feat: localize config and fix mobile viewport --- client/html/index.html | 2 +- client/styles/plainleaf_workspace.scss | 40 +++- client/styles/plainleaf_workspace.test.ts | 31 +++ .../ui/components/configuration_tab.tsx | 50 +++-- .../ui/config_zh_cn.test.ts | 66 ++++++ .../configuration-manager/ui/config_zh_cn.ts | 208 ++++++++++++++++++ .../ui/configuration.scss | 13 +- 7 files changed, 385 insertions(+), 25 deletions(-) create mode 100644 client/styles/plainleaf_workspace.test.ts create mode 100644 plugs/configuration-manager/ui/config_zh_cn.test.ts create mode 100644 plugs/configuration-manager/ui/config_zh_cn.ts diff --git a/client/html/index.html b/client/html/index.html index b58070ad..7da33734 100644 --- a/client/html/index.html +++ b/client/html/index.html @@ -3,7 +3,7 @@ diff --git a/client/styles/plainleaf_workspace.scss b/client/styles/plainleaf_workspace.scss index df35dcd8..bd012909 100644 --- a/client/styles/plainleaf_workspace.scss +++ b/client/styles/plainleaf_workspace.scss @@ -26,6 +26,7 @@ background: var(--pl-canvas); font-family: var(--ui-font); letter-spacing: 0; + overflow: hidden; } html[data-theme="dark"] #pl-workspace-shell { @@ -835,11 +836,16 @@ button.pl-setting-row:hover { min-height: 0; color: var(--pl-text); background: var(--pl-surface); - overflow: auto; + overflow-x: hidden; + overflow-y: auto; + overscroll-behavior: contain; + -webkit-overflow-scrolling: touch; } .pl-home-inner { + box-sizing: border-box; width: min(100%, 920px); + max-width: 100%; margin: 0 auto; padding: 48px 80px 90px; } @@ -902,6 +908,12 @@ button.pl-setting-row:hover { border-radius: 7px; } +.pl-home-actions > button > span:last-child, +.pl-home-grid > section, +.pl-settings-main-row > span:first-child { + min-width: 0; +} + .pl-home-actions .is-green { color: var(--pl-green); background: var(--pl-green-soft); } .pl-home-actions .is-coral { color: var(--pl-coral); background: var(--pl-coral-soft); } .pl-home-actions .is-blue { color: var(--pl-blue); background: var(--pl-blue-soft); } @@ -911,6 +923,7 @@ button.pl-setting-row:hover { .pl-home-task strong, .pl-home-task small { display: block; + min-width: 0; } .pl-home-actions strong { @@ -991,6 +1004,10 @@ button.pl-setting-row:hover { .pl-home-recent span { grid-column: 1 / -1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .pl-home-task { @@ -1032,7 +1049,9 @@ button.pl-setting-row:hover { } .pl-settings-inner { + box-sizing: border-box; width: min(100%, 820px); + max-width: 100%; margin: 0 auto; padding: 48px 80px 90px; } @@ -1075,8 +1094,10 @@ button.pl-setting-row:hover { .pl-settings-main-row small { margin-top: 3px; + overflow: hidden; color: var(--pl-faint); font-size: 11px; + text-overflow: ellipsis; } .pl-settings-main-row > button { @@ -1148,7 +1169,9 @@ button.pl-setting-row:hover { } .pl-new-note-dialog { + box-sizing: border-box; width: min(440px, calc(100vw - 32px)); + max-width: 100%; padding: 18px; border: 1px solid var(--pl-border); border-radius: 8px; @@ -1290,7 +1313,12 @@ button.pl-setting-row:hover { #pl-workspace-shell { display: block; box-sizing: border-box; - padding-bottom: calc(62px + env(safe-area-inset-bottom)); + width: 100%; + max-width: 100vw; + height: 100vh; + height: 100dvh; + padding-bottom: 0; + overflow: hidden; } .pl-sidebar { @@ -1331,7 +1359,10 @@ button.pl-setting-row:hover { .pl-editor-workspace { width: 100%; + max-width: 100vw; height: calc(100vh - 62px - env(safe-area-inset-bottom)); + height: calc(100dvh - 62px - env(safe-area-inset-bottom)); + overflow: hidden; } .pl-mobile-menu-button { @@ -1357,6 +1388,7 @@ button.pl-setting-row:hover { .pl-home-inner, .pl-settings-inner { width: 100%; + max-width: 100%; padding: 54px 22px 70px; } @@ -1397,6 +1429,9 @@ button.pl-setting-row:hover { right: 0; bottom: 0; left: 0; + box-sizing: border-box; + width: 100%; + max-width: 100vw; height: calc(62px + env(safe-area-inset-bottom)); padding: 4px 6px env(safe-area-inset-bottom); border-top: 1px solid var(--pl-border); @@ -1437,6 +1472,7 @@ button.pl-setting-row:hover { .pl-new-note-dialog { width: 100%; + max-width: 100vw; padding: 18px 18px calc(18px + env(safe-area-inset-bottom)); border-right: 0; border-bottom: 0; diff --git a/client/styles/plainleaf_workspace.test.ts b/client/styles/plainleaf_workspace.test.ts new file mode 100644 index 00000000..89c563c1 --- /dev/null +++ b/client/styles/plainleaf_workspace.test.ts @@ -0,0 +1,31 @@ +import { readFileSync } from "node:fs"; +import { expect, test } from "vitest"; + +const workspaceStyles = readFileSync( + new URL("./plainleaf_workspace.scss", import.meta.url), + "utf8", +); +const clientHtml = readFileSync( + new URL("../html/index.html", import.meta.url), + "utf8", +); + +test("keeps mobile workspace within the dynamic viewport", () => { + expect(workspaceStyles).toContain("height: 100dvh;"); + expect(workspaceStyles).toContain( + "height: calc(100dvh - 62px - env(safe-area-inset-bottom));", + ); + expect(clientHtml).toContain("viewport-fit=cover"); +}); + +test("uses internal vertical scrolling without widening the page", () => { + expect(workspaceStyles).toMatch( + /\.pl-home-surface,\s*\.pl-settings-surface\s*\{[^}]*overflow-x: hidden;[^}]*overflow-y: auto;/s, + ); + expect(workspaceStyles).toMatch( + /\.pl-home-inner\s*\{[^}]*box-sizing: border-box;[^}]*max-width: 100%;/s, + ); + expect(workspaceStyles).toMatch( + /\.pl-settings-inner\s*\{[^}]*box-sizing: border-box;[^}]*max-width: 100%;/s, + ); +}); diff --git a/plugs/configuration-manager/ui/components/configuration_tab.tsx b/plugs/configuration-manager/ui/components/configuration_tab.tsx index f7071b27..0ad9b6cc 100644 --- a/plugs/configuration-manager/ui/components/configuration_tab.tsx +++ b/plugs/configuration-manager/ui/components/configuration_tab.tsx @@ -1,10 +1,18 @@ +import { Checkbox, Input, Select } from "@silverbulletmd/silverbullet/ui"; import { useEffect, useRef, useState } from "preact/hooks"; import { RotateCcw } from "preact-feather"; import { useCfg } from "../cfg_context.tsx"; +import { + configurationCategoryDescriptionZhCN, + configurationCategoryZhCN, + configurationDescriptionZhCN, + configurationFieldMatches, + configurationLabelZhCN, + configurationOptionZhCN, +} from "../config_zh_cn.ts"; import { useConfig } from "../editors_context.tsx"; -import { cls } from "./chord_display.tsx"; -import { Checkbox, Input, Select } from "@silverbulletmd/silverbullet/ui"; import type { UiSchema } from "../schema.ts"; +import { cls } from "./chord_display.tsx"; function Control({ path, @@ -35,7 +43,7 @@ function Control({ > {schema.enum.map((opt: string) => ( ))} @@ -66,7 +74,7 @@ function Control({ /> ); } - return Configure manually in CONFIG; + return 请在 CONFIG 页面中手动配置; } function Field({ path, schema }: { path: string; schema: any }) { @@ -75,9 +83,13 @@ function Field({ path, schema }: { path: string; schema: any }) { return (
-
{schema.ui?.label || path}
- {schema.description && ( -
{schema.description}
+
+ {configurationLabelZhCN(path, schema.ui?.label || path)} +
+ {configurationDescriptionZhCN(path, schema.description) && ( +
+ {configurationDescriptionZhCN(path, schema.description)} +
)}
@@ -88,24 +100,17 @@ function Field({ path, schema }: { path: string; schema: any }) { />
); } -function fieldMatches(path: string, schema: any, query: string): boolean { - if (!query) return true; - const label = schema.ui?.label || path; - const description = schema.description || ""; - return `${label} ${description} ${path}`.toLowerCase().includes(query); -} - function Category({ name, fields, @@ -116,12 +121,17 @@ function Category({ query: string; }) { const { cfg } = useCfg(); - const visible = fields.filter((f) => fieldMatches(f.path, f.schema, query)); + const visible = fields.filter((f) => + configurationFieldMatches(f.path, f.schema, query), + ); if (visible.length === 0) return null; - const description = cfg.categories?.[name]?.description; + const description = configurationCategoryDescriptionZhCN( + name, + cfg.categories?.[name]?.description, + ); return (
-

{name}

+

{configurationCategoryZhCN(name)}

{description &&
{description}
} {visible.map((f) => ( @@ -144,7 +154,7 @@ export function ConfigurationTab() { inputRef={inputRef} class="cfg-search" type="text" - placeholder="Filter configuration options..." + placeholder="搜索设置名称、说明或配置路径…" value={search} onInput={(e) => setSearch((e.currentTarget as HTMLInputElement).value)} /> diff --git a/plugs/configuration-manager/ui/config_zh_cn.test.ts b/plugs/configuration-manager/ui/config_zh_cn.test.ts new file mode 100644 index 00000000..50833158 --- /dev/null +++ b/plugs/configuration-manager/ui/config_zh_cn.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, test } from "vitest"; +import { + configurationCategoryDescriptionZhCN, + configurationCategoryZhCN, + configurationDescriptionZhCN, + configurationFieldMatches, + configurationLabelZhCN, + configurationOptionZhCN, +} from "./config_zh_cn.ts"; + +describe("configuration Chinese display names", () => { + test("translates built-in categories and fields", () => { + expect(configurationCategoryZhCN("Editor")).toBe("编辑器"); + expect(configurationCategoryDescriptionZhCN("Journal")).toContain( + "每日笔记", + ); + expect( + configurationLabelZhCN("journal.prefix", "Journal page prefix"), + ).toBe("日记保存位置"); + expect( + configurationDescriptionZhCN( + "sync.documents", + "Sync document files locally", + ), + ).toContain("离线"); + expect( + configurationOptionZhCN("frontmatterFolding.foldByDefault", "always"), + ).toBe("总是"); + }); + + test("preserves configuration supplied by third-party extensions", () => { + expect(configurationCategoryZhCN("My Extension")).toBe("My Extension"); + expect(configurationLabelZhCN("my.setting", "Custom setting")).toBe( + "Custom setting", + ); + expect( + configurationDescriptionZhCN("my.setting", "Custom description"), + ).toBe("Custom description"); + expect(configurationOptionZhCN("my.setting", "custom-value")).toBe( + "custom-value", + ); + }); +}); + +describe("configuration search", () => { + const schema = { + description: "Page name to use as the template for new journal entries", + ui: { label: "Journal template page" }, + }; + + test.each([ + "日记模板", + "journal template", + "journal.template", + ])("matches %s without changing the underlying path", (query) => { + expect(configurationFieldMatches("journal.template", schema, query)).toBe( + true, + ); + }); + + test("does not match unrelated text", () => { + expect( + configurationFieldMatches("journal.template", schema, "GitHub"), + ).toBe(false); + }); +}); diff --git a/plugs/configuration-manager/ui/config_zh_cn.ts b/plugs/configuration-manager/ui/config_zh_cn.ts new file mode 100644 index 00000000..6f4b2fa1 --- /dev/null +++ b/plugs/configuration-manager/ui/config_zh_cn.ts @@ -0,0 +1,208 @@ +type LocalizedConfiguration = { + label: string; + description?: string; + options?: Record; +}; + +const categories: Record = { + Editor: { + label: "编辑器", + description: "设置括号补全、双链显示和页面属性折叠等编辑体验。", + }, + "Smart Quotes": { + label: "智能引号", + description: "输入时自动替换直引号,并设置使用的引号字符。", + }, + Comments: { + label: "批注", + description: "设置行内批注的署名信息。", + }, + Sync: { + label: "同步", + description: "设置当前设备与服务器之间同步的内容。", + }, + Indexing: { + label: "索引", + description: "选择哪些段落、列表项和待办可以被查询。", + }, + Query: { + label: "查询", + description: "设置查询结果中的文本比较和排序规则。", + }, + Widgets: { + label: "页面组件", + description: "开启并设置目录、关联提及和关联待办等内置组件。", + }, + Journal: { + label: "日记", + description: "设置每日笔记的模板、保存位置和标签。", + }, + GitHub: { + label: "GitHub 集成", + description: "设置写入 GitHub 仓库和 Gist 时使用的身份与凭据。", + }, +}; + +const fields: Record = { + "sync.documents": { + label: "同步附件文件", + description: "将非 Markdown 文件同步到本机,离线时也能访问附件。", + }, + "index.paragraph.all": { + label: "索引所有段落", + description: "没有标签的普通段落也加入索引。", + }, + "index.item.all": { + label: "索引所有列表项", + description: "没有标签的列表项也加入索引。", + }, + "index.task.all": { + label: "索引所有待办", + description: "没有标签的待办也加入索引。", + }, + shortWikiLinks: { + label: "简化双链名称", + description: "双链只显示最后一段名称,例如“人物/小明”显示为“小明”。", + }, + "frontmatterFolding.foldByDefault": { + label: "自动折叠页面属性", + description: "打开笔记时,何时自动折叠顶部的 YAML 页面属性。", + options: { + never: "从不", + long: "内容较长时", + always: "总是", + }, + }, + "frontmatterFolding.foldByDefaultLines": { + label: "页面属性折叠行数", + description: "页面属性超过此行数时自动折叠,仅在“内容较长时”生效。", + }, + "smartQuotes.enabled": { + label: "启用智能引号", + description: "输入时自动将直引号替换为排版引号。", + }, + "smartQuotes.double.left": { + label: "左双引号", + description: "左侧双引号使用的字符。", + }, + "smartQuotes.double.right": { + label: "右双引号", + description: "右侧双引号使用的字符。", + }, + "smartQuotes.single.left": { + label: "左单引号", + description: "左侧单引号使用的字符。", + }, + "smartQuotes.single.right": { + label: "右单引号", + description: "右侧单引号使用的字符。", + }, + "comments.author": { + label: "批注署名", + description: "填写批注签名;留空时只记录日期。", + }, + "queryCollation.enabled": { + label: "启用本地化排序", + description: "按照语言规则比较文本,而不是只按字符编码排序。", + }, + "queryCollation.locale": { + label: "排序语言", + description: "用于文本排序的语言标签,例如 zh-CN。", + }, + "std.widgets.toc.enabled": { + label: "显示页面目录", + description: "在页面顶部显示目录。", + }, + "std.widgets.toc.minHeaders": { + label: "显示目录所需标题数", + description: "页面至少包含多少个标题时才显示目录。", + }, + "std.widgets.linkedMentions.enabled": { + label: "显示关联提及", + description: "在页面底部显示其他笔记中对当前页面的引用。", + }, + "std.widgets.linkedTasks.enabled": { + label: "显示关联待办", + description: "在页面顶部显示与当前页面相关的未完成待办。", + }, + "journal.enabled": { + label: "启用日记", + description: "启用内置的每日日记功能和相关操作。", + }, + "journal.template": { + label: "日记模板页面", + description: "新建日记时使用的模板页面。", + }, + "journal.prefix": { + label: "日记保存位置", + description: "日记页面的路径前缀,例如 Journal/。以 / 结尾可归入文件夹。", + }, + "journal.tag": { + label: "日记标签", + description: "用于识别日记页面的标签。", + }, + "github.name": { + label: "Git 提交者姓名", + description: "写入 GitHub 仓库时使用的提交者姓名。", + }, + "github.email": { + label: "Git 提交者邮箱", + description: "写入 GitHub 仓库时使用的提交者邮箱。", + }, + "github.token": { + label: "GitHub 访问令牌", + description: "访问 GitHub API 所需的个人令牌。", + }, +}; + +export function configurationCategoryZhCN(name: string): string { + return categories[name]?.label ?? name; +} + +export function configurationCategoryDescriptionZhCN( + name: string, + fallback?: string, +): string | undefined { + return categories[name]?.description ?? fallback; +} + +export function configurationLabelZhCN(path: string, fallback: string): string { + return fields[path]?.label ?? fallback; +} + +export function configurationDescriptionZhCN( + path: string, + fallback?: string, +): string | undefined { + return fields[path]?.description ?? fallback; +} + +export function configurationOptionZhCN(path: string, option: string): string { + return fields[path]?.options?.[option] ?? option; +} + +export function configurationFieldMatches( + path: string, + schema: any, + query: string, +): boolean { + if (!query) return true; + const originalLabel = schema.ui?.label || path; + const originalDescription = schema.description || ""; + const localizedLabel = configurationLabelZhCN(path, originalLabel); + const localizedDescription = configurationDescriptionZhCN( + path, + originalDescription, + ); + return [ + path, + originalLabel, + originalDescription, + localizedLabel, + localizedDescription, + ] + .filter(Boolean) + .join(" ") + .toLocaleLowerCase() + .includes(query.toLocaleLowerCase()); +} diff --git a/plugs/configuration-manager/ui/configuration.scss b/plugs/configuration-manager/ui/configuration.scss index 1eea3a91..849fbdec 100644 --- a/plugs/configuration-manager/ui/configuration.scss +++ b/plugs/configuration-manager/ui/configuration.scss @@ -24,7 +24,7 @@ html[data-theme="dark"] { --cfg-recording-bg: #3a3520; } -html, body { height: 100%; background: var(--cfg-bg); color: var(--cfg-color); } +html, body { width: 100%; height: 100%; overflow: hidden; background: var(--cfg-bg); color: var(--cfg-color); } #cfg-root { display: flex; flex-direction: column; height: 100%; @@ -293,9 +293,16 @@ html[data-theme="dark"] .lib-badge-pull { background: #1f2a40; color: #89a0e5; } @media only screen and (max-width: 600px) { #cfg-header { padding: 12px 14px 0; } - #cfg-content { padding: 14px; } + #cfg-content { + min-width: 0; + padding: 14px; + overflow-x: hidden; + overscroll-behavior: contain; + -webkit-overflow-scrolling: touch; + } #cfg-footer { padding: 10px 14px; flex-wrap: wrap; gap: 8px; } #cfg-close { top: 8px; right: 8px; } + #cfg-edit-config { display: none; } .cfg-field { flex-direction: column; @@ -303,6 +310,7 @@ html[data-theme="dark"] .lib-badge-pull { background: #1f2a40; color: #89a0e5; } gap: 8px; } .cfg-field-control { + min-width: 0; margin-left: 0; justify-content: flex-end; } @@ -311,6 +319,7 @@ html[data-theme="dark"] .lib-badge-pull { background: #1f2a40; color: #89a0e5; } .cfg-field-control select { flex: 1; min-width: 0; + max-width: 100%; } .lib-row { flex-direction: column; align-items: stretch; }