diff --git a/CHANGELOG.md b/CHANGELOG.md index 75b2be1..a704e81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,12 +30,13 @@ - Dynamic page snapping (editor always shows full pages) - Print-ready @media print styles with @page A4 sizing - add bottom toolbar to show page number +- Zoom feature +- font size ### Not yet implemented - ODT import / open existing file -- font size +- undo history - Images, tables -- Zoom feature - Custom right-click context menu (formatting, cut/copy) - Multi-document management - PWA / offline support diff --git a/package-lock.json b/package-lock.json index d8b60fb..d952fca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@tiptap/extension-bold": "^3.23.6", "@tiptap/extension-bullet-list": "^3.23.6", "@tiptap/extension-document": "^3.23.6", + "@tiptap/extension-font-family": "^3.23.6", "@tiptap/extension-heading": "^3.23.6", "@tiptap/extension-history": "^3.23.6", "@tiptap/extension-italic": "^3.23.6", @@ -20,6 +21,7 @@ "@tiptap/extension-paragraph": "^3.23.6", "@tiptap/extension-placeholder": "^3.23.6", "@tiptap/extension-text": "^3.23.6", + "@tiptap/extension-text-style": "^3.23.6", "@tiptap/extension-underline": "^3.23.6", "@tiptap/pm": "^3.23.6", "odf-kit": "^0.13.4" @@ -509,6 +511,19 @@ "@tiptap/core": "3.23.6" } }, + "node_modules/@tiptap/extension-font-family": { + "version": "3.23.6", + "resolved": "https://registry.npmjs.org/@tiptap/extension-font-family/-/extension-font-family-3.23.6.tgz", + "integrity": "sha512-V7ttSAWf+vhXU9fACPOqkIiBjNI4XRDhVpghloDgYt1LqBczyFMey97cG8tZpkNVvWmVpy/wJpHHl46AHkLc1g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-text-style": "3.23.6" + } + }, "node_modules/@tiptap/extension-heading": { "version": "3.23.6", "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.23.6.tgz", @@ -628,6 +643,19 @@ "@tiptap/core": "3.23.6" } }, + "node_modules/@tiptap/extension-text-style": { + "version": "3.23.6", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-3.23.6.tgz", + "integrity": "sha512-Auasgj469wkQ5ip+Zi2gaRzvqxx9qKG58+1mkT8yPE3QAGnOIg/AaKyQ7pTV77UyL3FHvLnU+KsWCad+qcobww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.23.6" + } + }, "node_modules/@tiptap/extension-underline": { "version": "3.23.6", "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.23.6.tgz", diff --git a/package.json b/package.json index fb2e56a..93ec325 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@tiptap/extension-bold": "^3.23.6", "@tiptap/extension-bullet-list": "^3.23.6", "@tiptap/extension-document": "^3.23.6", + "@tiptap/extension-font-family": "^3.23.6", "@tiptap/extension-heading": "^3.23.6", "@tiptap/extension-history": "^3.23.6", "@tiptap/extension-italic": "^3.23.6", @@ -28,6 +29,7 @@ "@tiptap/extension-paragraph": "^3.23.6", "@tiptap/extension-placeholder": "^3.23.6", "@tiptap/extension-text": "^3.23.6", + "@tiptap/extension-text-style": "^3.23.6", "@tiptap/extension-underline": "^3.23.6", "@tiptap/pm": "^3.23.6", "odf-kit": "^0.13.4" diff --git a/src/lib/editor/ToolbarExpanded.svelte b/src/lib/editor/ToolbarExpanded.svelte index a3bae78..c129989 100644 --- a/src/lib/editor/ToolbarExpanded.svelte +++ b/src/lib/editor/ToolbarExpanded.svelte @@ -7,18 +7,87 @@ let isAlignCenter = $derived(tick >= 0 && !!editor?.isActive({ textAlign: 'center' })); let isAlignRight = $derived(tick >= 0 && !!editor?.isActive({ textAlign: 'right' })); let isAlignJustify = $derived(tick >= 0 && !!editor?.isActive({ textAlign: 'justify' })); + + const FONTS = [ + { value: 'Arial', label: 'Arial' }, + { value: 'Verdana', label: 'Verdana' }, + { value: 'Trebuchet MS', label: 'Trebuchet MS' }, + { value: 'Georgia', label: 'Georgia' }, + { value: 'Times New Roman', label: 'Times New Roman' }, + { value: 'Courier New', label: 'Courier New' }, + ] as const; + + // Returns the uniform font of the selection, or '' when fonts are mixed. + let currentFont = $derived.by(() => { + if (tick < 0 || !editor) return ''; + const { from, to, empty } = editor.state.selection; + if (empty) { + const marks = editor.state.storedMarks ?? editor.state.selection.$head.marks(); + return marks.find(m => m.type.name === 'textStyle')?.attrs.fontFamily ?? ''; + } + let font: string | undefined; + let mixed = false; + editor.state.doc.nodesBetween(from, to, (node) => { + if (mixed || !node.isText) return; + const f: string = node.marks.find(m => m.type.name === 'textStyle')?.attrs.fontFamily ?? ''; + if (font === undefined) font = f; + else if (font !== f) mixed = true; + }); + return mixed ? '' : (font ?? ''); + }); + + let fontOpen = $state(false); + let savedFrom: number | null = null; + let savedTo: number | null = null; + + function openFontPicker() { + if (!editor) return; + // Save selection before the picker button steals focus. + savedFrom = editor.state.selection.from; + savedTo = editor.state.selection.to; + fontOpen = !fontOpen; + } + + function pickFont(value: string) { + if (!editor) return; + fontOpen = 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 }).setFontFamily(value).run(); + } + + function fontPickerClickOutside(node: HTMLElement) { + function handler(e: MouseEvent) { + if (!node.contains(e.target as Node)) fontOpen = false; + } + window.addEventListener('mousedown', handler); + return { destroy() { window.removeEventListener('mousedown', handler); } }; + }
{#if editor} -
- +
+ + {#if fontOpen} +
+ {#each FONTS as font} + + {/each} +
+ {/if}
@@ -178,18 +247,80 @@ cursor: not-allowed; } - .font-select { + .font-picker { + position: relative; + } + + .font-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; - max-width: 110px; + cursor: pointer; + width: 145px; + transition: border-color 0.15s; + } + + .font-trigger:hover { + border-color: var(--color-primary); + } + + .font-trigger-label { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-align: left; + font-size: 0.8rem; + } + + .font-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; + } + + .font-option { + display: block; + width: 100%; + padding: 0.35rem 0.6rem; + border: none; + border-radius: calc(var(--radius) - 2px); + background: transparent; + color: var(--color-text); + font-size: 0.85rem; + text-align: left; + cursor: pointer; + white-space: nowrap; + transition: background 0.1s; + min-width: unset; + height: auto; + justify-content: flex-start; + } + + .font-option:hover { + background: var(--color-btn-hover); + } + + .font-option.active { + background: var(--color-primary); + color: white; } .font-size-select { diff --git a/src/lib/editor/extensions.ts b/src/lib/editor/extensions.ts index 53595dd..d4f8517 100644 --- a/src/lib/editor/extensions.ts +++ b/src/lib/editor/extensions.ts @@ -4,6 +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 Heading from '@tiptap/extension-heading'; import BulletList from '@tiptap/extension-bullet-list'; import OrderedList from '@tiptap/extension-ordered-list'; @@ -19,6 +20,8 @@ export const extensions = [ Bold, Italic, Underline, + TextStyle, + FontFamily, Heading.configure({ levels: [1, 2, 3] }), BulletList, OrderedList,