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:
1 parent
4d1339851b
commit
a716794e41
31 files changed
+556
-246
No files matched your search
+30
-11
@@ -9,6 +9,16 @@
|
||||
--rm-font-heading: 'Noto Serif SC', 'Source Han Serif SC', serif;
|
||||
--rm-font-mono: 'JetBrains Mono', ui-monospace, monospace;
|
||||
--rm-font-size: 16px;
|
||||
/* 界面排版独立于正文;辅助文字随界面缩放,但不小于 13px。 */
|
||||
--rm-ui-font-size: 16px;
|
||||
--rm-ui-font-small: max(13px, calc(var(--rm-ui-font-size) - 2px));
|
||||
--rm-ui-font-caption: max(13px, calc(var(--rm-ui-font-size) - 3px));
|
||||
--rm-ui-font-heading: calc(var(--rm-ui-font-size) + 4px);
|
||||
--rm-ui-font-title: calc(var(--rm-ui-font-size) + 12px);
|
||||
--rm-icon-size: calc(var(--rm-ui-font-size) + 4px);
|
||||
--rm-control-size: max(40px, calc(var(--rm-ui-font-size) + 24px));
|
||||
--rm-control-compact: max(32px, calc(var(--rm-ui-font-size) + 16px));
|
||||
--rm-header-height: calc(var(--rm-ui-font-size) + 48px);
|
||||
--rm-line-height: 1.75;
|
||||
--rm-content-width: 760px;
|
||||
--rm-paragraph-spacing: 16px;
|
||||
@@ -89,6 +99,14 @@ body {
|
||||
font-family: var(--rm-font-text);
|
||||
}
|
||||
|
||||
body { font-size: var(--rm-ui-font-size); line-height: 1.5; }
|
||||
button, input, select, textarea { font: inherit; }
|
||||
.rm-icon { flex: none; width: var(--rm-icon-size); height: var(--rm-icon-size); }
|
||||
@media (pointer: coarse), (max-width: 767px) {
|
||||
:root { --rm-control-size: max(44px, calc(var(--rm-ui-font-size) + 24px)); --rm-control-compact: max(44px, calc(var(--rm-ui-font-size) + 16px)); }
|
||||
input:not([type=checkbox],[type=radio],[type=range]), select, textarea { font-size: max(16px, var(--rm-ui-font-size)); }
|
||||
}
|
||||
|
||||
.cm-editor {
|
||||
font-size: var(--rm-font-size);
|
||||
line-height: var(--rm-line-height);
|
||||
@@ -160,7 +178,7 @@ body {
|
||||
pointer-events: none;
|
||||
}
|
||||
.cm-line.cm-rm-code .cm-rm-mono { font-family: inherit; }
|
||||
.cm-line.cm-rm-code-begin { padding-top: 10px; min-height: 26px; }
|
||||
.cm-line.cm-rm-code-begin { padding-top: 10px; min-height: var(--rm-control-compact); }
|
||||
.cm-line.cm-rm-code-end { padding-bottom: 10px; }
|
||||
.cm-line.cm-rm-code-begin::before { border-radius: 8px 8px 0 0; }
|
||||
.cm-line.cm-rm-code-end::before { border-radius: 0 0 8px 8px; }
|
||||
@@ -168,13 +186,13 @@ body {
|
||||
/* 围栏展开前后保持同一行高,语言标签与复制按钮不参与行高计算,避免代码内容跳动。 */
|
||||
.cm-rm-code-header { display: inline-flex; align-items: center; gap: 6px; vertical-align: middle; user-select: none; }
|
||||
.cm-line.cm-rm-code-begin .cm-rm-code-header { position: absolute; top: 6px; right: 12px; }
|
||||
.cm-rm-code-lang { color: var(--rm-text-muted); font-size: 12px; line-height: 1; }
|
||||
.cm-rm-code-lang { color: var(--rm-text-muted); font-size: var(--rm-ui-font-small); line-height: 1; }
|
||||
.cm-rm-code-copy {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
width: var(--rm-control-compact);
|
||||
height: var(--rm-control-compact);
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
@@ -235,6 +253,8 @@ body {
|
||||
.cm-rm-table-widget:hover .cm-rm-table-toolbar,
|
||||
.cm-rm-table-widget:focus-within .cm-rm-table-toolbar { visibility: visible; opacity: 1; }
|
||||
.cm-rm-table-tools {
|
||||
max-width: 100%;
|
||||
flex-wrap: wrap;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px;
|
||||
@@ -244,22 +264,24 @@ body {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.cm-rm-table-tools button {
|
||||
min-width: var(--rm-control-compact);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: 26px;
|
||||
height: var(--rm-control-compact);
|
||||
padding: 0 8px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--rm-text-muted);
|
||||
font: 12px/1 var(--rm-font-text);
|
||||
font: var(--rm-ui-font-small)/1 var(--rm-font-text);
|
||||
cursor: pointer;
|
||||
transition: color 0.15s, background-color 0.15s;
|
||||
}
|
||||
.cm-rm-table-tools button:hover { color: var(--rm-accent-strong); background: var(--rm-selection); }
|
||||
.cm-rm-table-tools button.cm-rm-danger:hover { color: #e5484d; background: rgba(229, 72, 77, 0.1); }
|
||||
.cm-rm-table-tools svg { flex: none; }
|
||||
.cm-rm-table-tools svg { flex: none; width: var(--rm-ui-font-size); height: var(--rm-ui-font-size); }
|
||||
.cm-rm-code-copy svg { width: var(--rm-icon-size); height: var(--rm-icon-size); }
|
||||
.cm-rm-table-tools .cm-rm-sep { width: 1px; height: 16px; margin: 0 2px; background: var(--rm-divider); }
|
||||
.cm-rm-image { cursor: pointer; }
|
||||
|
||||
@@ -267,10 +289,7 @@ body {
|
||||
.cm-editor .cm-content { padding: 16px 16px 40vh; }
|
||||
/* 触屏没有悬停,工具栏常显,按钮加大触控热区 */
|
||||
.cm-rm-table-toolbar { visibility: visible; opacity: 1; }
|
||||
.cm-rm-table-tools button { height: 36px; padding: 0 10px; }
|
||||
.cm-rm-code-copy { width: 36px; height: 36px; }
|
||||
.cm-line.cm-rm-code { padding-left: 12px; padding-right: 12px; }
|
||||
.cm-line.cm-rm-code-begin { min-height: 36px; }
|
||||
}
|
||||
|
||||
/* ---- Typora 扩展语法 ---- */
|
||||
@@ -297,7 +316,7 @@ body {
|
||||
background: rgba(229, 72, 77, 0.08);
|
||||
color: #e5484d;
|
||||
font-family: var(--rm-font-mono);
|
||||
font-size: 12px;
|
||||
font-size: var(--rm-ui-font-small);
|
||||
text-align: left;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { applyDisplaySettings, DISPLAY_DEFAULTS, sanitizeDisplaySettings } from './typography'
|
||||
|
||||
afterEach(() => document.documentElement.removeAttribute('style'))
|
||||
|
||||
describe('界面与正文排版设置', () => {
|
||||
it('旧设置缺少界面字号时使用 16px,保留正文字号', () => {
|
||||
expect(sanitizeDisplaySettings({ fontSize: 24 })).toMatchObject({ uiFontSize: 16, fontSize: 24 })
|
||||
})
|
||||
|
||||
it.each([14, 16, 20])('接受范围内的整数界面字号 %s', uiFontSize => {
|
||||
expect(sanitizeDisplaySettings({ uiFontSize }).uiFontSize).toBe(uiFontSize)
|
||||
})
|
||||
|
||||
it.each([13, 21, 16.5, '18', null, Number.NaN, Number.POSITIVE_INFINITY])('拒绝非法界面字号 %s', uiFontSize => {
|
||||
expect(sanitizeDisplaySettings({ uiFontSize }).uiFontSize).toBe(16)
|
||||
})
|
||||
|
||||
it('应用界面字号不改变正文、代码字体和正文宽度', () => {
|
||||
const display = sanitizeDisplaySettings({ uiFontSize: 20, fontSize: 24, contentWidth: 960, textFont: 'serif', codeFont: 'system' })
|
||||
applyDisplaySettings(display)
|
||||
const style = document.documentElement.style
|
||||
expect(style.getPropertyValue('--rm-ui-font-size')).toBe('20px')
|
||||
expect(style.getPropertyValue('--rm-font-size')).toBe('24px')
|
||||
expect(style.getPropertyValue('--rm-content-width')).toBe('960px')
|
||||
expect(style.getPropertyValue('--rm-font-editor')).toContain('Noto Serif SC')
|
||||
expect(style.getPropertyValue('--rm-font-mono')).toBe('ui-monospace, monospace')
|
||||
applyDisplaySettings({ ...display, uiFontSize: DISPLAY_DEFAULTS.uiFontSize })
|
||||
expect(style.getPropertyValue('--rm-ui-font-size')).toBe('16px')
|
||||
expect(style.getPropertyValue('--rm-font-size')).toBe('24px')
|
||||
})
|
||||
})
|
||||
@@ -8,11 +8,13 @@ export const CODE_FONTS = {
|
||||
jetbrains: { label: 'JetBrains Mono', css: "'JetBrains Mono', ui-monospace, monospace" },
|
||||
system: { label: '系统等宽字体', css: 'ui-monospace, monospace' },
|
||||
}
|
||||
export const DISPLAY_DEFAULTS = { fontSize: 16, contentWidth: 760, textFont: 'sans' as keyof typeof TEXT_FONTS, codeFont: 'jetbrains' as keyof typeof CODE_FONTS, lineHeight: 1.75, paragraphSpacing: 16, glassOpacity: .75 }
|
||||
export const UI_FONT_SIZE = { min: 14, max: 20, default: 16 }
|
||||
export const DISPLAY_DEFAULTS = { uiFontSize: UI_FONT_SIZE.default, fontSize: 16, contentWidth: 760, textFont: 'sans' as keyof typeof TEXT_FONTS, codeFont: 'jetbrains' as keyof typeof CODE_FONTS, lineHeight: 1.75, paragraphSpacing: 16, glassOpacity: .75 }
|
||||
export type DisplaySettings = typeof DISPLAY_DEFAULTS
|
||||
export function sanitizeDisplaySettings(raw: Record<string, unknown>): DisplaySettings {
|
||||
const ranged = (key: 'lineHeight' | 'paragraphSpacing' | 'glassOpacity', min: number, max: number) => typeof raw[key] === 'number' && Number.isFinite(raw[key]) && raw[key] >= min && raw[key] <= max ? raw[key] : DISPLAY_DEFAULTS[key]
|
||||
return {
|
||||
uiFontSize: typeof raw.uiFontSize === 'number' && Number.isInteger(raw.uiFontSize) && raw.uiFontSize >= UI_FONT_SIZE.min && raw.uiFontSize <= UI_FONT_SIZE.max ? raw.uiFontSize : UI_FONT_SIZE.default,
|
||||
fontSize: typeof raw.fontSize === 'number' && Number.isFinite(raw.fontSize) && raw.fontSize > 0 ? raw.fontSize : 16,
|
||||
contentWidth: typeof raw.contentWidth === 'number' && [0, 680, 760, 960].includes(raw.contentWidth) ? raw.contentWidth : 760,
|
||||
textFont: raw.textFont === 'serif' || raw.textFont === 'system' ? raw.textFont : 'sans',
|
||||
@@ -22,6 +24,7 @@ export function sanitizeDisplaySettings(raw: Record<string, unknown>): DisplaySe
|
||||
}
|
||||
export function applyDisplaySettings(settings: DisplaySettings) {
|
||||
const root = document.documentElement.style
|
||||
root.setProperty('--rm-ui-font-size', settings.uiFontSize + 'px')
|
||||
root.setProperty('--rm-font-size', settings.fontSize + 'px')
|
||||
root.setProperty('--rm-content-width', settings.contentWidth ? settings.contentWidth + 'px' : 'none')
|
||||
root.setProperty('--rm-font-editor', TEXT_FONTS[settings.textFont].css)
|
||||
|
||||
Reference in new issue
Block a user