fix(editor): unify text and blank line heights
Apply configured line height to the editor scroller and remove independent paragraph spacing. Update settings, typography preview, and regression coverage.
This commit is contained in:
1 parent
11f5e4fcf4
commit
59603afc94
8 files changed
+22
-14
No files matched your search
@@ -181,7 +181,6 @@ async function changePassword() {
|
|||||||
<SettingRow id="font-size" control="rm-font-size" resettable @reset="change({ fontSize: DEFAULTS.fontSize })"><label class="rm-font-input"><input id="rm-font-size" type="number" min="0" step="any" :value="settings.fontSize" @input="changeFont" @blur="changeFont" @keydown.enter="($event.target as HTMLInputElement).blur()" /> px</label></SettingRow>
|
<SettingRow id="font-size" control="rm-font-size" resettable @reset="change({ fontSize: DEFAULTS.fontSize })"><label class="rm-font-input"><input id="rm-font-size" type="number" min="0" step="any" :value="settings.fontSize" @input="changeFont" @blur="changeFont" @keydown.enter="($event.target as HTMLInputElement).blur()" /> px</label></SettingRow>
|
||||||
<SettingRow id="content-width" resettable @reset="change({ contentWidth: DEFAULTS.contentWidth })"><div class="rm-segmented" role="radiogroup" aria-label="正文宽度"><button v-for="w in CONTENT_WIDTHS" :key="w.value" role="radio" :aria-checked="settings.contentWidth === w.value" :class="{ on: settings.contentWidth === w.value }" @click="change({ contentWidth: w.value })">{{ w.label }}</button></div></SettingRow>
|
<SettingRow id="content-width" resettable @reset="change({ contentWidth: DEFAULTS.contentWidth })"><div class="rm-segmented" role="radiogroup" aria-label="正文宽度"><button v-for="w in CONTENT_WIDTHS" :key="w.value" role="radio" :aria-checked="settings.contentWidth === w.value" :class="{ on: settings.contentWidth === w.value }" @click="change({ contentWidth: w.value })">{{ w.label }}</button></div></SettingRow>
|
||||||
<SettingRow id="line-height" control="rm-line-height" resettable @reset="change({ lineHeight: DEFAULTS.lineHeight })"><input id="rm-line-height" type="range" min="1.2" max="2.5" step="0.05" :value="settings.lineHeight" @change="change({ lineHeight: Number(($event.target as HTMLInputElement).value) })" /><output for="rm-line-height">{{ settings.lineHeight.toFixed(2) }}</output></SettingRow>
|
<SettingRow id="line-height" control="rm-line-height" resettable @reset="change({ lineHeight: DEFAULTS.lineHeight })"><input id="rm-line-height" type="range" min="1.2" max="2.5" step="0.05" :value="settings.lineHeight" @change="change({ lineHeight: Number(($event.target as HTMLInputElement).value) })" /><output for="rm-line-height">{{ settings.lineHeight.toFixed(2) }}</output></SettingRow>
|
||||||
<SettingRow id="paragraph-spacing" control="rm-paragraph-spacing" resettable @reset="change({ paragraphSpacing: DEFAULTS.paragraphSpacing })"><input id="rm-paragraph-spacing" type="range" min="0" max="40" step="2" :value="settings.paragraphSpacing" @change="change({ paragraphSpacing: Number(($event.target as HTMLInputElement).value) })" /><output for="rm-paragraph-spacing">{{ settings.paragraphSpacing }}px</output></SettingRow>
|
|
||||||
<details id="rm-setting-typography-preview" class="rm-settings-details" tabindex="-1"><summary>排版预览</summary><article class="rm-typography-preview"><h2>让文字自由呼吸</h2><p>用简洁的记录,连接每一个想法。Markdown 文件始终属于你。</p><p>真实内容,自然排版,专注当下的灵感。</p><code>const idea = 'Hello, RawMark'</code></article></details>
|
<details id="rm-setting-typography-preview" class="rm-settings-details" tabindex="-1"><summary>排版预览</summary><article class="rm-typography-preview"><h2>让文字自由呼吸</h2><p>用简洁的记录,连接每一个想法。Markdown 文件始终属于你。</p><p>真实内容,自然排版,专注当下的灵感。</p><code>const idea = 'Hello, RawMark'</code></article></details>
|
||||||
<h3 class="rm-settings-group-title">编辑行为</h3>
|
<h3 class="rm-settings-group-title">编辑行为</h3>
|
||||||
<SettingRow id="selection-toolbar" control="rm-selection-toolbar"><input id="rm-selection-toolbar" type="checkbox" :checked="settings.selectionToolbar" @change="change({ selectionToolbar: ($event.target as HTMLInputElement).checked })" /></SettingRow>
|
<SettingRow id="selection-toolbar" control="rm-selection-toolbar"><input id="rm-selection-toolbar" type="checkbox" :checked="settings.selectionToolbar" @change="change({ selectionToolbar: ($event.target as HTMLInputElement).checked })" /></SettingRow>
|
||||||
|
|||||||
+2
-1
@@ -465,7 +465,8 @@ button {
|
|||||||
.rm-settings :is(.rm-setting, .rm-settings-details, .rm-settings-form):focus { outline: 2px solid var(--rm-accent); outline-offset: -2px; border-radius: 6px; }
|
.rm-settings :is(.rm-setting, .rm-settings-details, .rm-settings-form):focus { outline: 2px solid var(--rm-accent); outline-offset: -2px; border-radius: 6px; }
|
||||||
.rm-typography-preview { margin: 8px 0 0; padding: 16px; border: 1px solid var(--rm-divider); border-radius: 8px; background: var(--rm-sidebar-bg); font: var(--rm-font-size)/var(--rm-line-height) var(--rm-font-editor, var(--rm-font-text)); overflow-wrap: anywhere; }
|
.rm-typography-preview { margin: 8px 0 0; padding: 16px; border: 1px solid var(--rm-divider); border-radius: 8px; background: var(--rm-sidebar-bg); font: var(--rm-font-size)/var(--rm-line-height) var(--rm-font-editor, var(--rm-font-text)); overflow-wrap: anywhere; }
|
||||||
.rm-typography-preview h2 { margin-top: 0; font-family: var(--rm-font-heading); font-size: 1.5em; }
|
.rm-typography-preview h2 { margin-top: 0; font-family: var(--rm-font-heading); font-size: 1.5em; }
|
||||||
.rm-typography-preview p { margin-bottom: var(--rm-paragraph-spacing); }
|
.rm-typography-preview p { margin: 0; }
|
||||||
|
.rm-typography-preview p + p { margin-top: 1lh; }
|
||||||
.rm-typography-preview code { font-family: var(--rm-font-mono); font-size: .9em; color: var(--rm-inline-code-text); }
|
.rm-typography-preview code { font-family: var(--rm-font-mono); font-size: .9em; color: var(--rm-inline-code-text); }
|
||||||
.rm-settings .rm-share-note, .rm-settings .rm-appearance-note { margin: 12px 0; overflow-wrap: anywhere; }
|
.rm-settings .rm-share-note, .rm-settings .rm-appearance-note { margin: 12px 0; overflow-wrap: anywhere; }
|
||||||
.rm-settings .rm-share-form { flex-wrap: wrap; padding: 16px 0 0; }
|
.rm-settings .rm-share-form { flex-wrap: wrap; padding: 16px 0 0; }
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ export const SETTING_ITEMS = {
|
|||||||
'font-size': { section: 'editor', title: '编辑器字号', description: '调整正文与代码的基础字号,不影响界面字号。', keywords: '正文 大小' },
|
'font-size': { section: 'editor', title: '编辑器字号', description: '调整正文与代码的基础字号,不影响界面字号。', keywords: '正文 大小' },
|
||||||
'content-width': { section: 'editor', title: '正文宽度', description: '限制正文最大宽度,或使用可用空间的全宽。', keywords: '窄 标准 宽 全宽' },
|
'content-width': { section: 'editor', title: '正文宽度', description: '限制正文最大宽度,或使用可用空间的全宽。', keywords: '窄 标准 宽 全宽' },
|
||||||
'line-height': { section: 'editor', title: '正文行高', description: '调整正文各行之间的距离。', keywords: '行距 排版' },
|
'line-height': { section: 'editor', title: '正文行高', description: '调整正文各行之间的距离。', keywords: '行距 排版' },
|
||||||
'paragraph-spacing': { section: 'editor', title: '段落间距', description: '调整相邻段落之间的距离。', keywords: '段距 排版' },
|
|
||||||
'selection-toolbar': { section: 'editor', title: '选中文本浮动工具栏', description: '选中文字后显示常用格式操作。', keywords: '格式 编辑' },
|
'selection-toolbar': { section: 'editor', title: '选中文本浮动工具栏', description: '选中文字后显示常用格式操作。', keywords: '格式 编辑' },
|
||||||
'spellcheck': { section: 'editor', title: '拼写检查', description: '使用浏览器的拼写检查,检查语言取决于浏览器设置。', keywords: '语言 拼写' },
|
'spellcheck': { section: 'editor', title: '拼写检查', description: '使用浏览器的拼写检查,检查语言取决于浏览器设置。', keywords: '语言 拼写' },
|
||||||
'tab-size': { section: 'editor', title: '制表符宽度', description: '仅调整 Tab 字符的显示宽度,不改写正文;Tab 键仍按两个空格缩进。', keywords: 'Tab 缩进 空格' },
|
'tab-size': { section: 'editor', title: '制表符宽度', description: '仅调整 Tab 字符的显示宽度,不改写正文;Tab 键仍按两个空格缩进。', keywords: 'Tab 缩进 空格' },
|
||||||
|
|||||||
@@ -1038,7 +1038,7 @@ export async function startWorkspace() {
|
|||||||
)
|
)
|
||||||
// 字号、正文宽度变化后行高随之改变,编辑器需要重新测量
|
// 字号、正文宽度变化后行高随之改变,编辑器需要重新测量
|
||||||
watch(
|
watch(
|
||||||
() => [settings.uiFont, settings.uiFontSize, settings.fontSize, settings.contentWidth, settings.sidebarWidth, settings.textFont, settings.codeFont, settings.lineHeight, settings.paragraphSpacing],
|
() => [settings.uiFont, settings.uiFontSize, settings.fontSize, settings.contentWidth, settings.sidebarWidth, settings.textFont, settings.codeFont, settings.lineHeight],
|
||||||
() => view?.requestMeasure(),
|
() => view?.requestMeasure(),
|
||||||
)
|
)
|
||||||
watch(
|
watch(
|
||||||
|
|||||||
@@ -125,9 +125,6 @@ function buildDecorations(view: EditorView): DecorationSet {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case 'Paragraph':
|
|
||||||
if (!node.parent || !['ListItem', 'Blockquote'].includes(node.parent.name)) out.push(lineDeco('cm-rm-paragraph-end').range(doc.lineAt(node.to).from))
|
|
||||||
return
|
|
||||||
case 'HeaderMark': {
|
case 'HeaderMark': {
|
||||||
if (isActive(node.from)) return
|
if (isActive(node.from)) return
|
||||||
const parent = node.parent!
|
const parent = node.parent!
|
||||||
|
|||||||
@@ -30,7 +30,6 @@
|
|||||||
--rm-header-height: calc(var(--rm-ui-font-size) + 48px);
|
--rm-header-height: calc(var(--rm-ui-font-size) + 48px);
|
||||||
--rm-line-height: 1.75;
|
--rm-line-height: 1.75;
|
||||||
--rm-content-width: 760px;
|
--rm-content-width: 760px;
|
||||||
--rm-paragraph-spacing: 16px;
|
|
||||||
--rm-glass-opacity: 0.75;
|
--rm-glass-opacity: 0.75;
|
||||||
--rm-bg: #faf8ff;
|
--rm-bg: #faf8ff;
|
||||||
--rm-sidebar-bg: #f2f3ff;
|
--rm-sidebar-bg: #f2f3ff;
|
||||||
@@ -114,7 +113,6 @@ button, input, select, textarea { font: inherit; }
|
|||||||
|
|
||||||
.cm-editor {
|
.cm-editor {
|
||||||
font-size: var(--rm-font-size);
|
font-size: var(--rm-font-size);
|
||||||
line-height: var(--rm-line-height);
|
|
||||||
background: var(--rm-bg);
|
background: var(--rm-bg);
|
||||||
color: var(--rm-text);
|
color: var(--rm-text);
|
||||||
}
|
}
|
||||||
@@ -123,6 +121,7 @@ button, input, select, textarea { font: inherit; }
|
|||||||
}
|
}
|
||||||
.cm-editor .cm-scroller {
|
.cm-editor .cm-scroller {
|
||||||
font-family: var(--rm-font-editor, var(--rm-font-text));
|
font-family: var(--rm-font-editor, var(--rm-font-text));
|
||||||
|
line-height: var(--rm-line-height);
|
||||||
}
|
}
|
||||||
.cm-editor .cm-content {
|
.cm-editor .cm-content {
|
||||||
max-width: var(--rm-content-width);
|
max-width: var(--rm-content-width);
|
||||||
@@ -348,7 +347,6 @@ button, input, select, textarea { font: inherit; }
|
|||||||
.cm-rm-code-lang { font-family: var(--rm-font-mono); }
|
.cm-rm-code-lang { font-family: var(--rm-font-mono); }
|
||||||
.cm-line.cm-rm-code-begin::before { border-top: 1px solid var(--rm-divider); }
|
.cm-line.cm-rm-code-begin::before { border-top: 1px solid var(--rm-divider); }
|
||||||
.cm-line.cm-rm-code-end::before { border-bottom: 1px solid var(--rm-divider); }
|
.cm-line.cm-rm-code-end::before { border-bottom: 1px solid var(--rm-divider); }
|
||||||
.cm-line.cm-rm-paragraph-end { padding-bottom: var(--rm-paragraph-spacing); }
|
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
.cm-line.cm-rm-h1 { font-size: 1.75em; }
|
.cm-line.cm-rm-h1 { font-size: 1.75em; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,21 @@ describe('界面与正文排版设置', () => {
|
|||||||
expect(sanitizeDisplaySettings({ fontSize: 24 })).toMatchObject({ uiFontSize: 14, fontSize: 24 })
|
expect(sanitizeDisplaySettings({ fontSize: 24 })).toMatchObject({ uiFontSize: 14, fontSize: 24 })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('旧段间距设置不再进入正文排版,保留已设置的字号和行距', () => {
|
||||||
|
const display = sanitizeDisplaySettings({ paragraphSpacing: 40, fontSize: 24, lineHeight: 2 })
|
||||||
|
expect(display).not.toHaveProperty('paragraphSpacing')
|
||||||
|
applyDisplaySettings(display)
|
||||||
|
expect(document.documentElement.style.getPropertyValue('--rm-font-size')).toBe('24px')
|
||||||
|
expect(document.documentElement.style.getPropertyValue('--rm-line-height')).toBe('2')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each([1.2, 1.75, 2.5])('保留并应用允许的正文行距 %s', lineHeight => {
|
||||||
|
const display = sanitizeDisplaySettings({ lineHeight })
|
||||||
|
expect(display.lineHeight).toBe(lineHeight)
|
||||||
|
applyDisplaySettings(display)
|
||||||
|
expect(document.documentElement.style.getPropertyValue('--rm-line-height')).toBe(String(lineHeight))
|
||||||
|
})
|
||||||
|
|
||||||
it.each([14, 16, 20])('接受范围内的整数界面字号 %s', uiFontSize => {
|
it.each([14, 16, 20])('接受范围内的整数界面字号 %s', uiFontSize => {
|
||||||
expect(sanitizeDisplaySettings({ uiFontSize }).uiFontSize).toBe(uiFontSize)
|
expect(sanitizeDisplaySettings({ uiFontSize }).uiFontSize).toBe(uiFontSize)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ export const CODE_FONTS = {
|
|||||||
}
|
}
|
||||||
export const UI_FONT_SIZE = { min: 14, max: 20, default: 14 }
|
export const UI_FONT_SIZE = { min: 14, max: 20, default: 14 }
|
||||||
export const FONT_PRESETS: Record<string, { label: string; css: string }> = { ...TEXT_FONTS, jetbrains: CODE_FONTS.jetbrains }
|
export const FONT_PRESETS: Record<string, { label: string; css: string }> = { ...TEXT_FONTS, jetbrains: CODE_FONTS.jetbrains }
|
||||||
export const DISPLAY_DEFAULTS = { uiFont: 'maple', uiFontSize: UI_FONT_SIZE.default, fontSize: 16, contentWidth: 760, textFont: 'maple', codeFont: 'maple', lineHeight: 1.75, paragraphSpacing: 16, glassOpacity: .75 }
|
export const DISPLAY_DEFAULTS = { uiFont: 'maple', uiFontSize: UI_FONT_SIZE.default, fontSize: 16, contentWidth: 760, textFont: 'maple', codeFont: 'maple', lineHeight: 1.75, glassOpacity: .75 }
|
||||||
export type DisplaySettings = typeof DISPLAY_DEFAULTS
|
export type DisplaySettings = typeof DISPLAY_DEFAULTS
|
||||||
export function sanitizeDisplaySettings(raw: Record<string, unknown>, customIDs: string[] = [], hiddenFonts: string[] = []): DisplaySettings {
|
export function sanitizeDisplaySettings(raw: Record<string, unknown>, customIDs: string[] = [], hiddenFonts: string[] = []): 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]
|
const ranged = (key: 'lineHeight' | 'glassOpacity', min: number, max: number) => typeof raw[key] === 'number' && Number.isFinite(raw[key]) && raw[key] >= min && raw[key] <= max ? raw[key] : DISPLAY_DEFAULTS[key]
|
||||||
const font = (value: unknown) => typeof value === 'string' && !hiddenFonts.includes(value) && (Object.hasOwn(FONT_PRESETS, value) || customIDs.includes(value)) ? value : 'maple'
|
const font = (value: unknown) => typeof value === 'string' && !hiddenFonts.includes(value) && (Object.hasOwn(FONT_PRESETS, value) || customIDs.includes(value)) ? value : 'maple'
|
||||||
return {
|
return {
|
||||||
uiFont: font(raw.uiFont),
|
uiFont: font(raw.uiFont),
|
||||||
@@ -25,7 +25,7 @@ export function sanitizeDisplaySettings(raw: Record<string, unknown>, customIDs:
|
|||||||
contentWidth: typeof raw.contentWidth === 'number' && [0, 680, 760, 960].includes(raw.contentWidth) ? raw.contentWidth : 760,
|
contentWidth: typeof raw.contentWidth === 'number' && [0, 680, 760, 960].includes(raw.contentWidth) ? raw.contentWidth : 760,
|
||||||
textFont: font(raw.textFont),
|
textFont: font(raw.textFont),
|
||||||
codeFont: font(raw.codeFont),
|
codeFont: font(raw.codeFont),
|
||||||
lineHeight: ranged('lineHeight', 1.2, 2.5), paragraphSpacing: ranged('paragraphSpacing', 0, 40), glassOpacity: ranged('glassOpacity', .4, 1),
|
lineHeight: ranged('lineHeight', 1.2, 2.5), glassOpacity: ranged('glassOpacity', .4, 1),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export function applyDisplaySettings(settings: DisplaySettings) {
|
export function applyDisplaySettings(settings: DisplaySettings) {
|
||||||
@@ -38,6 +38,5 @@ export function applyDisplaySettings(settings: DisplaySettings) {
|
|||||||
root.setProperty('--rm-font-editor', css(settings.textFont))
|
root.setProperty('--rm-font-editor', css(settings.textFont))
|
||||||
root.setProperty('--rm-font-mono', css(settings.codeFont, true))
|
root.setProperty('--rm-font-mono', css(settings.codeFont, true))
|
||||||
root.setProperty('--rm-line-height', String(settings.lineHeight))
|
root.setProperty('--rm-line-height', String(settings.lineHeight))
|
||||||
root.setProperty('--rm-paragraph-spacing', settings.paragraphSpacing + 'px')
|
|
||||||
root.setProperty('--rm-glass-opacity', String(settings.glassOpacity))
|
root.setProperty('--rm-glass-opacity', String(settings.glassOpacity))
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user