💄 Paragraph spacing

This commit is contained in:
2026-06-08 17:26:56 +10:00
parent bfece01c67
commit 6382d4f46b
4 changed files with 61 additions and 3 deletions

View File

@@ -16,6 +16,7 @@ export default function App(): JSX.Element {
focusMode, toggleFocusMode, focusMode, toggleFocusMode,
theme, toggleTheme, theme, toggleTheme,
fontSize, setFontSize, fontSize, setFontSize,
showInvisibles, toggleInvisibles,
activeStoryId, isDirty, activeStoryPath, activeStoryContent, markSaved, activeStoryId, isDirty, activeStoryPath, activeStoryContent, markSaved,
stories, setStories, stories, setStories,
setMarkets, setMarkets,
@@ -159,6 +160,11 @@ export default function App(): JSX.Element {
title="Revision history" title="Revision history"
disabled={!activeStoryId} disabled={!activeStoryId}
></button> ></button>
<button
className={`app-titlebar-btn${showInvisibles ? ' active' : ''}`}
onClick={toggleInvisibles}
title="Show invisibles"
></button>
<button <button
className={`app-titlebar-btn${focusMode ? ' active' : ''}`} className={`app-titlebar-btn${focusMode ? ' active' : ''}`}
onClick={toggleFocusMode} onClick={toggleFocusMode}

View File

@@ -1,5 +1,5 @@
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'
import { EditorView, keymap, Decoration, type DecorationSet, ViewPlugin, type ViewUpdate } from '@codemirror/view' import { EditorView, keymap, Decoration, type DecorationSet, ViewPlugin, type ViewUpdate, WidgetType } from '@codemirror/view'
import { EditorState, StateField, StateEffect, RangeSetBuilder, Compartment } from '@codemirror/state' import { EditorState, StateField, StateEffect, RangeSetBuilder, Compartment } from '@codemirror/state'
import { markdown } from '@codemirror/lang-markdown' import { markdown } from '@codemirror/lang-markdown'
import { syntaxHighlighting, HighlightStyle } from '@codemirror/language' import { syntaxHighlighting, HighlightStyle } from '@codemirror/language'
@@ -48,6 +48,41 @@ const annPlugin = ViewPlugin.fromClass(class {
} }
}, { decorations: v => v.decorations }) }, { decorations: v => v.decorations })
class SpaceDot extends WidgetType {
toDOM(): HTMLElement {
const span = document.createElement('span')
span.className = 'cm-space-dot'
span.textContent = '·'
return span
}
ignoreEvent(): boolean { return false }
}
const showInvisiblesCompartment = new Compartment()
function buildInvisiblesPlugin(enabled: boolean) {
if (!enabled) return []
return ViewPlugin.fromClass(class {
decorations: DecorationSet
constructor(view: EditorView) { this.decorations = this.build(view) }
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) this.decorations = this.build(update.view)
}
build(view: EditorView): DecorationSet {
const builder = new RangeSetBuilder<Decoration>()
for (const { from, to } of view.visibleRanges) {
const text = view.state.doc.sliceString(from, to)
for (let i = 0; i < text.length; i++) {
if (text[i] === ' ') {
builder.add(from + i, from + i + 1, Decoration.replace({ widget: new SpaceDot() }))
}
}
}
return builder.finish()
}
}, { decorations: v => v.decorations })
}
const themeCompartment = new Compartment() const themeCompartment = new Compartment()
const fontSizeCompartment = new Compartment() const fontSizeCompartment = new Compartment()
@@ -78,7 +113,7 @@ const markdownHighlight = HighlightStyle.define([
export function MarkdownEditor(): JSX.Element { export function MarkdownEditor(): JSX.Element {
const { activeStoryPath, activeStoryContent, activeStoryId, const { activeStoryPath, activeStoryContent, activeStoryId,
annotations, theme, fontSize, revisionPanelOpen, toggleRevisionPanel, revisions, setRevisions } = useBorgesStore() annotations, theme, fontSize, showInvisibles, revisionPanelOpen, toggleRevisionPanel, revisions, setRevisions } = useBorgesStore()
const editorRef = useRef<HTMLDivElement>(null) const editorRef = useRef<HTMLDivElement>(null)
const viewRef = useRef<EditorView | null>(null) const viewRef = useRef<EditorView | null>(null)
const lastPathRef = useRef<string | null>(null) const lastPathRef = useRef<string | null>(null)
@@ -102,6 +137,7 @@ export function MarkdownEditor(): JSX.Element {
annPlugin, annPlugin,
themeCompartment.of(buildTheme(fontSize, isDark)), themeCompartment.of(buildTheme(fontSize, isDark)),
fontSizeCompartment.of([]), fontSizeCompartment.of([]),
showInvisiblesCompartment.of(buildInvisiblesPlugin(false)),
EditorView.lineWrapping, EditorView.lineWrapping,
EditorView.updateListener.of((update) => { EditorView.updateListener.of((update) => {
if (update.docChanged) { if (update.docChanged) {
@@ -161,6 +197,11 @@ export function MarkdownEditor(): JSX.Element {
viewRef.current?.dispatch({ effects: themeCompartment.reconfigure(buildTheme(fontSize, theme === 'dark')) }) viewRef.current?.dispatch({ effects: themeCompartment.reconfigure(buildTheme(fontSize, theme === 'dark')) })
}, [theme, fontSize]) }, [theme, fontSize])
// Update show invisibles
useEffect(() => {
viewRef.current?.dispatch({ effects: showInvisiblesCompartment.reconfigure(buildInvisiblesPlugin(showInvisibles)) })
}, [showInvisibles])
if (!activeStoryId) { if (!activeStoryId) {
return <div className="no-story-placeholder">Select a story or create a new one</div> return <div className="no-story-placeholder">Select a story or create a new one</div>
} }

View File

@@ -72,6 +72,8 @@ interface BorgesState {
toggleTheme: () => void toggleTheme: () => void
fontSize: number fontSize: number
setFontSize: (size: number) => void setFontSize: (size: number) => void
showInvisibles: boolean
toggleInvisibles: () => void
initPrefs: () => Promise<void> initPrefs: () => Promise<void>
// Session // Session
@@ -230,6 +232,8 @@ export const useBorgesStore = create<BorgesState>((set, get) => ({
window.api.writeConfig({ fontSize: clamped }) window.api.writeConfig({ fontSize: clamped })
set({ fontSize: clamped }) set({ fontSize: clamped })
}, },
showInvisibles: false,
toggleInvisibles: () => set((s) => ({ showInvisibles: !s.showInvisibles })),
initPrefs: async () => { initPrefs: async () => {
const cfg = await window.api.readConfig() const cfg = await window.api.readConfig()
const fontSize = Math.max(11, Math.min(24, cfg.fontSize ?? 15)) const fontSize = Math.max(11, Math.min(24, cfg.fontSize ?? 15))

View File

@@ -303,7 +303,8 @@ textarea { resize: vertical; }
.cm-editor.cm-focused { outline: none; } .cm-editor.cm-focused { outline: none; }
.cm-scroller { font-family: 'Georgia', 'Times New Roman', serif; line-height: 1.8; } .cm-scroller { font-family: 'Georgia', 'Times New Roman', serif; line-height: 1.8; }
.cm-content { max-width: 680px; margin: 0 auto; padding: 0 24px; } .cm-content { max-width: 680px; margin: 0 auto; padding: 0 24px; }
.cm-line { padding: 0; } .cm-line { padding: 0; margin-bottom: 0.75em; }
.cm-line:last-child { margin-bottom: 0; }
.cm-cursor { border-left-color: var(--accent) !important; } .cm-cursor { border-left-color: var(--accent) !important; }
.cm-selectionBackground { background: rgba(200, 169, 110, 0.25) !important; } .cm-selectionBackground { background: rgba(200, 169, 110, 0.25) !important; }
.cm-focused .cm-selectionBackground { background: rgba(200, 169, 110, 0.35) !important; } .cm-focused .cm-selectionBackground { background: rgba(200, 169, 110, 0.35) !important; }
@@ -318,6 +319,12 @@ textarea { resize: vertical; }
} }
.ann-highlight:hover { background: rgba(200, 169, 110, 0.28); } .ann-highlight:hover { background: rgba(200, 169, 110, 0.28); }
.cm-space-dot {
color: var(--text3);
pointer-events: none;
user-select: none;
}
/* Dashboard */ /* Dashboard */
.dashboard { .dashboard {
flex: 1; flex: 1;