💄 Paragraph spacing
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user