feat: add font size selection to extended toolbar

This commit is contained in:
Steffen Becker committed 2026-05-25 23:19:01 +02:00
1 parent b72b7a1631
commit 0f1ff5c969
4 files changed
+154 -17

No files matched your search

+2 -1
View File
@@ -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
+147 -12
View File
@@ -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<number, string> = { 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<string, string> }[] }, parent: { type: { name: string }; attrs: Record<string, number> } | 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<typeof effectiveSize>[0], parent as Parameters<typeof effectiveSize>[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); } };
}
</script>
<div class="toolbar-expanded">
@@ -96,14 +159,24 @@
<div class="toolbar-separator"></div>
<div class="toolbar-group">
<select
class="font-size-select"
disabled
title="Font size (coming soon)"
>
<option>16</option>
</select>
<div class="size-picker" use:sizePickerClickOutside>
<button class="size-trigger" onclick={openSizePicker} title="Font size">
<span class="size-trigger-label">{currentFontSize ? currentFontSize.replace('pt', '') : ''}</span>
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" aria-hidden="true">
<path d="M1 2.5l3 3 3-3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
{#if sizeOpen}
<div class="size-dropdown">
{#each FONT_SIZES as size}
<button
class="size-option"
class:active={currentFontSize === `${size}pt`}
onclick={() => pickSize(size)}
>{size}</button>
{/each}
</div>
{/if}
</div>
<div class="toolbar-separator"></div>
@@ -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;
}
</style>
+2 -1
View File
@@ -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,
+3 -3
View File
@@ -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,