diff --git a/CHANGELOG.md b/CHANGELOG.md index a704e81..7ad22c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,8 @@ - Print-ready @media print styles with @page A4 sizing - add bottom toolbar to show page number - Zoom feature -- font size +- font selection +- font size selection ### Not yet implemented - ODT import / open existing file diff --git a/src/lib/editor/ToolbarExpanded.svelte b/src/lib/editor/ToolbarExpanded.svelte index f3c91c2..d62ed50 100644 --- a/src/lib/editor/ToolbarExpanded.svelte +++ b/src/lib/editor/ToolbarExpanded.svelte @@ -40,12 +40,49 @@ return mixed ? '' : (font ?? DEFAULT_EDITOR_FONT); }); + // Must match the common document default and editor.css heading sizes (in pt) + const DEFAULT_FONT_SIZE = '12pt'; + const HEADING_SIZES: Record = { 1: '20pt', 2: '16pt', 3: '14pt' }; + const FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 24, 28, 36, 48, 72]; + + function effectiveSize(node: { isText: boolean; marks: readonly { type: { name: string }; attrs: Record }[] }, parent: { type: { name: string }; attrs: Record } | null): string { + const explicit = node.marks.find(m => m.type.name === 'textStyle')?.attrs.fontSize; + if (explicit) return explicit; + if (parent?.type.name === 'heading') return HEADING_SIZES[parent.attrs.level] ?? DEFAULT_FONT_SIZE; + return DEFAULT_FONT_SIZE; + } + + let currentFontSize = $derived.by(() => { + if (tick < 0 || !editor) return ''; + const { from, to, empty } = editor.state.selection; + if (empty) { + const head = editor.state.selection.$head; + const marks = editor.state.storedMarks ?? head.marks(); + const explicit = marks.find(m => m.type.name === 'textStyle')?.attrs.fontSize; + if (explicit) return explicit; + const parent = head.parent; + if (parent.type.name === 'heading') return HEADING_SIZES[parent.attrs.level] ?? DEFAULT_FONT_SIZE; + return DEFAULT_FONT_SIZE; + } + let size: string | undefined; + let mixed = false; + editor.state.doc.nodesBetween(from, to, (node, _pos, parent) => { + if (mixed || !node.isText) return; + const s = effectiveSize(node as Parameters[0], parent as Parameters[1]); + if (size === undefined) size = s; + else if (size !== s) mixed = true; + }); + return mixed ? '' : (size ?? DEFAULT_FONT_SIZE); + }); + let fontOpen = $state(false); + let sizeOpen = $state(false); let savedFrom: number | null = null; let savedTo: number | null = null; function openFontPicker() { if (!editor) return; + sizeOpen = false; // Save selection before the picker button steals focus. savedFrom = editor.state.selection.from; savedTo = editor.state.selection.to; @@ -69,6 +106,32 @@ window.addEventListener('mousedown', handler); return { destroy() { window.removeEventListener('mousedown', handler); } }; } + + function openSizePicker() { + if (!editor) return; + fontOpen = false; + savedFrom = editor.state.selection.from; + savedTo = editor.state.selection.to; + sizeOpen = !sizeOpen; + } + + function pickSize(size: number) { + if (!editor) return; + sizeOpen = false; + const from = savedFrom ?? editor.state.selection.from; + const to = savedTo ?? editor.state.selection.to; + savedFrom = null; + savedTo = null; + editor.chain().focus().setTextSelection({ from, to }).setFontSize(`${size}pt`).run(); + } + + function sizePickerClickOutside(node: HTMLElement) { + function handler(e: MouseEvent) { + if (!node.contains(e.target as Node)) sizeOpen = false; + } + window.addEventListener('mousedown', handler); + return { destroy() { window.removeEventListener('mousedown', handler); } }; + }
@@ -96,14 +159,24 @@
-
- +
+ + {#if sizeOpen} +
+ {#each FONT_SIZES as size} + + {/each} +
+ {/if}
@@ -327,16 +400,78 @@ color: white; } - .font-size-select { + .size-picker { + position: relative; + } + + .size-trigger { + display: inline-flex; + align-items: center; + gap: 4px; height: 2rem; - padding: 0 0.5rem; + padding: 0 0.4rem 0 0.6rem; border: 1px solid var(--color-border); border-radius: var(--radius); background: var(--color-surface); color: var(--color-text); font-size: 0.8rem; font-family: var(--font-sans); - cursor: not-allowed; - opacity: 0.35; + cursor: pointer; + width: 62px; + transition: border-color 0.15s; + } + + .size-trigger:hover { + border-color: var(--color-primary); + } + + .size-trigger-label { + flex: 1; + text-align: left; + } + + .size-dropdown { + position: absolute; + top: calc(100% + 3px); + left: 0; + min-width: 100%; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); + z-index: 200; + padding: 2px; + display: flex; + flex-direction: column; + max-height: 260px; + overflow-y: auto; + } + + .size-option { + display: block; + width: 100%; + padding: 0.3rem 0.6rem; + border: none; + border-radius: calc(var(--radius) - 2px); + background: transparent; + color: var(--color-text); + font-size: 0.85rem; + font-family: var(--font-sans); + text-align: left; + cursor: pointer; + white-space: nowrap; + transition: background 0.1s; + min-width: unset; + height: auto; + justify-content: flex-start; + } + + .size-option:hover { + background: var(--color-btn-hover); + } + + .size-option.active { + background: var(--color-primary); + color: white; } diff --git a/src/lib/editor/extensions.ts b/src/lib/editor/extensions.ts index d4f8517..3f64fd7 100644 --- a/src/lib/editor/extensions.ts +++ b/src/lib/editor/extensions.ts @@ -4,7 +4,7 @@ import Text from '@tiptap/extension-text'; import Bold from '@tiptap/extension-bold'; import Italic from '@tiptap/extension-italic'; import Underline from '@tiptap/extension-underline'; -import { TextStyle, FontFamily } from '@tiptap/extension-text-style'; +import { TextStyle, FontFamily, FontSize } from '@tiptap/extension-text-style'; import Heading from '@tiptap/extension-heading'; import BulletList from '@tiptap/extension-bullet-list'; import OrderedList from '@tiptap/extension-ordered-list'; @@ -22,6 +22,7 @@ export const extensions = [ Underline, TextStyle, FontFamily, + FontSize, Heading.configure({ levels: [1, 2, 3] }), BulletList, OrderedList, diff --git a/src/styles/editor.css b/src/styles/editor.css index 670ff0e..0bfe664 100644 --- a/src/styles/editor.css +++ b/src/styles/editor.css @@ -108,15 +108,15 @@ } .paper .tiptap h1 { - font-size: 1.75rem; + font-size: 20pt; } .paper .tiptap h2 { - font-size: 1.4rem; + font-size: 16pt; } .paper .tiptap h3 { - font-size: 1.15rem; + font-size: 14pt; } .paper .tiptap ul,