From a72ddf6aecc176cae35cc9289bb40ed260a52109 Mon Sep 17 00:00:00 2001 From: Jacob Schmidt Date: Fri, 27 Feb 2026 06:29:05 -0600 Subject: [PATCH] Move template management into Entries side panel --- .../src/lib/components/SidePanel.svelte | 245 +++++++++++++++--- Journal.App/src/routes/+page.svelte | 75 +++++- Journal.App/src/routes/settings/+page.svelte | 170 ------------ 3 files changed, 288 insertions(+), 202 deletions(-) diff --git a/Journal.App/src/lib/components/SidePanel.svelte b/Journal.App/src/lib/components/SidePanel.svelte index 28d36f5..ceda883 100644 --- a/Journal.App/src/lib/components/SidePanel.svelte +++ b/Journal.App/src/lib/components/SidePanel.svelte @@ -1,12 +1,14 @@

{panelTitle}

- +
+ {#if activeSection === "entries"} + + {/if} + +
{#if isCalendarSection} @@ -281,36 +363,111 @@ type="text" bind:this={newItemInput} bind:value={newItemName} - placeholder={activeSection === "entries" ? "Entry name..." : activeSection === "todos" ? "Todo list name..." : "List name..."} + placeholder={activeSection === "entries" + ? createTemplateMode + ? "Template name..." + : "Entry name..." + : activeSection === "todos" + ? "Todo list name..." + : "List name..."} on:keydown={handleNewItemKeydown} on:blur={confirmNewItem} /> {/if} - + {/if} + + +
+

Templates

+ {#if templatesBusy} +

Loading templates...

+ {:else} +
    + {#each allTemplateItems as item} +
  • + +
    + + +
    +
  • + {/each} +
+ {#if !allTemplateItems.length} +

No templates found.

+ {/if} + {/if} + {#if templateError} +

{templateError}

+ {/if} +
+ {:else} + + {/if} {/if}
@@ -332,6 +489,12 @@ justify-content: space-between; } + .panel-header-actions { + display: flex; + align-items: center; + gap: 6px; + } + .panel-header h2 { font-size: 0.95rem; font-weight: 600; @@ -481,6 +644,30 @@ letter-spacing: 0.01em; } + .panel-subsection { + display: flex; + flex-direction: column; + gap: 6px; + min-height: 0; + } + + .panel-subsection h3 { + font-size: 0.78rem; + font-weight: 600; + color: var(--text-muted); + letter-spacing: 0.01em; + } + + .section-copy { + color: var(--text-dim); + font-size: 0.78rem; + } + + .template-error { + color: #e74c3c; + font-size: 0.78rem; + } + .new-item-input { padding: 0 2px; diff --git a/Journal.App/src/routes/+page.svelte b/Journal.App/src/routes/+page.svelte index 27f0b88..eeb5987 100644 --- a/Journal.App/src/routes/+page.svelte +++ b/Journal.App/src/routes/+page.svelte @@ -2,6 +2,7 @@ import { goto } from "$app/navigation"; import { unlockVaultWorkspace } from "$lib/backend/auth"; import AppModal from "$lib/components/AppModal.svelte"; + import { deleteEntryTemplate, loadEntryTemplate, saveEntryTemplate } from "$lib/backend/templates"; import { deleteEntryByStoreId, entriesStore, getDefaultEntry, hydrateEntries, loadEntryByStoreId, saveEntryFromStore } from "$lib/stores/entries"; import { deleteFragmentByStoreId, hydrateFragments } from "$lib/stores/fragments"; import { deleteListByStoreId, hydrateLists, updateListByStoreId } from "$lib/stores/lists"; @@ -45,6 +46,25 @@ let unlockResolver: ((password: string | null) => void) | null = null; let fragmentBootstrapInFlight = false; let pendingDeleteItemId = ""; + let templateRefreshToken = 0; + + function toTemplatePath(id: string): string | null { + const prefix = "entries/template-file/"; + if (!id.startsWith(prefix)) return null; + const encoded = id.slice(prefix.length).trim(); + if (!encoded) return null; + try { + const decoded = decodeURIComponent(encoded); + return decoded || null; + } catch { + return null; + } + } + + function templateNameFromPath(path: string): string { + const fileName = path.split(/[\\/]/).pop() ?? path; + return fileName.replace(/\.template\.md$/i, ""); + } function showModal(options: { action: "logout-confirm" | "logout-info" | "unlock-vault" | "delete-confirm"; @@ -160,6 +180,7 @@ if (isVaultReady()) { try { await hydrateEntries(); + templateRefreshToken += 1; const firstEntry = getDefaultEntry(get(entriesStore)); if (firstEntry && activeDocumentId === "entries/daily-notes") { await handleOpenDocument(firstEntry); @@ -185,6 +206,7 @@ setVaultSession(password); await hydrateEntries(); + templateRefreshToken += 1; const firstEntry = getDefaultEntry(get(entriesStore)); if (firstEntry && activeDocumentId === "entries/daily-notes") { await handleOpenDocument(firstEntry); @@ -211,7 +233,31 @@ if (!content?.trim()) return; try { - if (selectedSection === "entries" && (activeDocumentId.startsWith("entries/file/") || activeDocumentId.startsWith("entries/draft-"))) { + if (selectedSection === "entries" && activeDocumentId.startsWith("entries/template-draft-")) { + const draft = get(entriesStore).find((item) => item.id === activeDocumentId); + const draftLabel = (draft?.label ?? activeDocumentLabel ?? "").trim(); + const templateName = draftLabel.replace(/_template$/i, "").trim() || draftLabel; + await saveEntryTemplate({ + name: templateName, + content + }); + templateRefreshToken += 1; + entriesStore.update((items) => items.filter((item) => item.id !== activeDocumentId)); + const { [activeDocumentId]: _, ...rest } = openDocuments; + openDocuments = rest; + activeDocumentId = ""; + activeDocumentLabel = ""; + editMode = false; + } else if (selectedSection === "entries" && activeDocumentId.startsWith("entries/template-file/")) { + const filePath = toTemplatePath(activeDocumentId); + if (!filePath) return; + await saveEntryTemplate({ + name: templateNameFromPath(filePath), + content, + filePath + }); + templateRefreshToken += 1; + } else if (selectedSection === "entries" && (activeDocumentId.startsWith("entries/file/") || activeDocumentId.startsWith("entries/draft-"))) { const saved = await saveEntryFromStore(activeDocumentId, content, "Overwrite"); if (saved && saved.id !== activeDocumentId) { const { [activeDocumentId]: _, ...rest } = openDocuments; @@ -281,6 +327,20 @@ } catch { // entry content will use initialContent fallback } + } else if (selectedSection === "entries" && doc.id.startsWith("entries/template-file/") && !doc.initialContent) { + try { + const filePath = toTemplatePath(doc.id); + if (filePath) { + const loaded = await loadEntryTemplate(filePath); + resolvedDoc = { + id: doc.id, + label: loaded.fileName.replace(/\.template\.md$/i, ""), + initialContent: loaded.content + }; + } + } catch { + // template content will use initialContent fallback + } } if (!(resolvedDoc.id in openDocuments)) { @@ -303,7 +363,16 @@ try { let ok = false; if (selectedSection === "entries") { - ok = await deleteEntryByStoreId(id); + const templatePath = toTemplatePath(id); + if (templatePath) { + ok = await deleteEntryTemplate(templatePath); + if (ok) templateRefreshToken += 1; + } else if (id.startsWith("entries/template-draft-")) { + entriesStore.update((items) => items.filter((item) => item.id !== id)); + ok = true; + } else { + ok = await deleteEntryByStoreId(id); + } } else if (selectedSection === "todos") { ok = await deleteTodoListByStoreId(id); } else if (selectedSection === "lists") { @@ -356,6 +425,7 @@ - diff --git a/Journal.App/src/routes/settings/+page.svelte b/Journal.App/src/routes/settings/+page.svelte index 10a4455..8c387b1 100644 --- a/Journal.App/src/routes/settings/+page.svelte +++ b/Journal.App/src/routes/settings/+page.svelte @@ -2,13 +2,6 @@ import { goto } from "$app/navigation"; import AppModal from "$lib/components/AppModal.svelte"; import Navbar from "$lib/components/Navbar.svelte"; - import { - deleteEntryTemplate, - listEntryTemplates, - loadEntryTemplate, - saveEntryTemplate, - type EntryTemplateItemDto - } from "$lib/backend/templates"; import { addFragmentType, addSettingsTag, @@ -41,12 +34,6 @@ let sidecarRoot = ""; let sidecarRootIsCustom = false; let sidecarRootError = ""; - let templates: EntryTemplateItemDto[] = []; - let templateName = ""; - let templateContent = ""; - let templateFilePath: string | null = null; - let templateError = ""; - let templatesBusy = false; onMount(async () => { try { @@ -56,8 +43,6 @@ } catch (e) { sidecarRootError = String(e); } - - await refreshTemplates(); }); async function saveSidecarRoot() { @@ -208,83 +193,6 @@ } } - async function refreshTemplates() { - templatesBusy = true; - templateError = ""; - try { - templates = await listEntryTemplates(); - } catch (error) { - templateError = String(error); - } finally { - templatesBusy = false; - } - } - - function resetTemplateEditor() { - templateName = ""; - templateContent = ""; - templateFilePath = null; - } - - async function editTemplate(item: EntryTemplateItemDto) { - templateError = ""; - try { - const loaded = await loadEntryTemplate(item.filePath); - templateFilePath = loaded.filePath; - templateName = loaded.fileName.replace(/\.template\.md$/i, ""); - templateContent = loaded.content; - } catch (error) { - templateError = String(error); - } - } - - async function saveTemplate() { - templateError = ""; - if (!templateName.trim()) { - templateError = "Template name is required."; - return; - } - if (!templateContent.trim()) { - templateError = "Template content is required."; - return; - } - - try { - const wasCreate = !templateFilePath; - const saved = await saveEntryTemplate({ - name: templateName, - content: templateContent, - filePath: templateFilePath ?? undefined - }); - - if (wasCreate) { - resetTemplateEditor(); - } else { - templateFilePath = saved.filePath; - } - await refreshTemplates(); - } catch (error) { - templateError = String(error); - } - } - - async function removeTemplate(item: EntryTemplateItemDto) { - templateError = ""; - try { - const ok = await deleteEntryTemplate(item.filePath); - if (!ok) { - templateError = "Failed to delete template."; - return; - } - - if (templateFilePath === item.filePath) { - resetTemplateEditor(); - } - await refreshTemplates(); - } catch (error) { - templateError = String(error); - } - }
@@ -392,57 +300,6 @@ -
-

Entry Templates

-

Create reusable markdown templates stored as .template.md files in your data directory.

- -
-
- - -
- - - -
-
- -
- {#if templatesBusy} -

Loading templates...

- {:else if templates.length === 0} -

No templates found.

- {:else} -
    - {#each templates as item} -
  • - {item.fileName.replace(/\.template\.md$/i, "")} -
    - - -
    -
  • - {/each} -
- {/if} -
-
- - {#if templateError} -

{templateError}

- {/if} -
-

Sidecar

Root directory containing the Journal.Sidecar project.

@@ -625,31 +482,4 @@ color: #e74c3c; font-size: 0.82rem; } - - .template-grid { - display: grid; - grid-template-columns: 1fr; - gap: 10px; - } - - .template-editor { - display: flex; - flex-direction: column; - gap: 8px; - } - - .template-editor textarea { - border: 1px solid var(--border-soft); - border-radius: 7px; - background: var(--bg-app); - color: var(--text-primary); - padding: 8px 9px; - resize: vertical; - } - - @media (min-width: 980px) { - .template-grid { - grid-template-columns: 1.2fr 1fr; - } - }