From 0f8bca31cdee316fbc8f933876770798ae3ff3e2 Mon Sep 17 00:00:00 2001 From: Steffen Becker Date: Mon, 24 Aug 2026 16:54:03 +0000 Subject: [PATCH] feat: built-in template gallery with a DIN 5008 letter lib/templates/: registry + one module per template, localized via t() at build time. applyTemplate (App.svelte) reuses the extracted resetDocumentState() from handleNew, then assigns the template's margins, styles and fold marks; no file handle, so Save asks where. Gallery dialog reachable from the ribbon File menu and the modern chrome; first template is a DIN 5008 Form B business letter built from placeholder fields, with the address field at 45mm and a 10cm info tab. --- src/App.svelte | 54 +++++- .../components/TemplateGalleryDialog.svelte | 175 ++++++++++++++++++ src/lib/components/ribbon/Ribbon.svelte | 6 +- src/lib/i18n/locales/de.ts | 30 +++ src/lib/i18n/locales/en.ts | 30 +++ src/lib/templates/din5008b.ts | 69 +++++++ src/lib/templates/registry.ts | 4 + src/lib/templates/types.ts | 21 +++ 8 files changed, 381 insertions(+), 8 deletions(-) create mode 100644 src/lib/components/TemplateGalleryDialog.svelte create mode 100644 src/lib/templates/din5008b.ts create mode 100644 src/lib/templates/registry.ts create mode 100644 src/lib/templates/types.ts diff --git a/src/App.svelte b/src/App.svelte index b3b34dd..fee0285 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -51,6 +51,8 @@ import LanguagePicker from './lib/components/LanguagePicker.svelte'; import UiLanguagePicker from './lib/components/UiLanguagePicker.svelte'; import AboutDialog from './lib/components/AboutDialog.svelte'; + import TemplateGalleryDialog from './lib/components/TemplateGalleryDialog.svelte'; + import type { TemplateEntry } from './lib/templates/types'; import DocPropertiesDialog from './lib/components/DocPropertiesDialog.svelte'; import CommentsPane from './lib/components/CommentsPane.svelte'; import RevisionsPane from './lib/components/RevisionsPane.svelte'; @@ -184,6 +186,7 @@ let pageDecor: PageDecor = $state(loadPageDecor()); let lineNumbering: LineNumbering = $state(loadLineNumbering()); let foldMarks = $state(loadFoldMarks()); + let templateGalleryOpen = $state(false); let autoCorrectOpen = $state(false); let autoTextOpen = $state(false); let thesaurusOpen = $state(false); @@ -535,13 +538,9 @@ editor?.chain().setMeta(RECORDING, true).setContent(content).run(); } - function handleNew() { - if (!editor) return; - if (isDocNonEmpty() && !confirm(t().dialogs.confirmNew)) return; - loadContent('

'); // onUpdate fires → autosave - documentEpoch++; - resetHistory(); - // Reset everything to defaults; the $effects persist these. + // Reset every document side-car to its default; the $effects persist these. + // Shared by New and New-from-template, which then assigns the template's own values. + function resetDocumentState() { hfActive = null; headerDoc = null; footerDoc = null; @@ -575,6 +574,37 @@ setNoteSettings(DEFAULT_NOTE_SETTINGS); clearEmbeddedFonts(); void clearEmbeddedFontStore(); + } + + function handleNew() { + if (!editor) return; + if (isDocNonEmpty() && !confirm(t().dialogs.confirmNew)) return; + loadContent('

'); // onUpdate fires → autosave + documentEpoch++; + resetHistory(); + resetDocumentState(); + editor.commands.focus(); + } + + // A built-in template: a full reset, then the template's content and side-cars — + // the same set an .ott import adopts. No file handle, so the first Save asks where. + function applyTemplate(entry: TemplateEntry) { + if (!editor) return; + if (isDocNonEmpty() && !confirm(t().dialogs.confirmNew)) return; + const data = entry.build(); + loadContent(data.content); + documentEpoch++; + resetHistory(); + resetDocumentState(); + if (data.margins) pageMargins = { ...data.margins }; + if (data.styles?.length) { + const sheet = styleSheet(); + const paragraph = { ...sheet.paragraph }; + for (const s of data.styles) paragraph[s.name] = s; + setStyleSheet({ ...sheet, paragraph }); + } + foldMarks = data.foldMarks === true; + documentName = entry.name(); editor.commands.focus(); } @@ -1118,6 +1148,7 @@ {docxBusy} {pdfBusy} onNew={handleNew} + onNewFromTemplate={() => (templateGalleryOpen = true)} onOpen={handleOpen} onSave={handleSave} onSaveAs={handleSaveAs} @@ -1193,6 +1224,14 @@ + +

{t().templates.title}

+
+ {#each TEMPLATES as entry (entry.id)} + + {/each} +
+ + + + diff --git a/src/lib/components/ribbon/Ribbon.svelte b/src/lib/components/ribbon/Ribbon.svelte index 28cb5af..8e911b2 100644 --- a/src/lib/components/ribbon/Ribbon.svelte +++ b/src/lib/components/ribbon/Ribbon.svelte @@ -68,7 +68,7 @@ onSelectTheme, docxBusy = false, pdfBusy = false, - onNew, onOpen, onSave, onSaveAs, onSaveDocx, onSaveTemplate, onExportPdf, onPrintPdf, onPrint, onAbout, onDocProperties, onAutoCorrect, onAutoText, onNewComment, commentsOpen = false, onToggleComments, revisionsOpen = false, onToggleRevisions, + onNew, onNewFromTemplate, onOpen, onSave, onSaveAs, onSaveDocx, onSaveTemplate, onExportPdf, onPrintPdf, onPrint, onAbout, onDocProperties, onAutoCorrect, onAutoText, onNewComment, commentsOpen = false, onToggleComments, revisionsOpen = false, onToggleRevisions, navigatorOpen = false, onToggleNavigator, recentFiles = [], onOpenRecent, onForgetRecent, }: { @@ -108,6 +108,7 @@ docxBusy?: boolean; pdfBusy?: boolean; onNew?: () => void; + onNewFromTemplate?: () => void; onOpen?: () => void; onSave?: () => void; onSaveAs?: () => void; @@ -202,6 +203,9 @@ +