Move template management into Entries side panel
This commit is contained in:
parent
64c06081f0
commit
a72ddf6aec
@ -1,12 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { listEntryTemplates, type EntryTemplateItemDto } from "$lib/backend/templates";
|
||||
import CalendarWidget from "$lib/components/CalendarWidget.svelte";
|
||||
import { entriesStore } from "$lib/stores/entries";
|
||||
import { entriesBusyStore, entriesStore } from "$lib/stores/entries";
|
||||
import { createFragmentDraft, fragmentsStore } from "$lib/stores/fragments";
|
||||
import { createListDraft, createListFromLabel, listsStore } from "$lib/stores/lists";
|
||||
import { createTodoListDraft, createTodoListFromLabel, serializeTodoList, todoListsStore, todosStore } from "$lib/stores/todos";
|
||||
|
||||
export let activeSection = "entries";
|
||||
export let activeDocumentId = "";
|
||||
export let templateRefreshToken = 0;
|
||||
export let onOpenDocument: (doc: { id: string; label: string; initialContent: string }) => void = () => {};
|
||||
export let onEditItem: (doc: { id: string; label: string; initialContent: string }) => void = () => {};
|
||||
export let onDeleteItem: (doc: { id: string; label: string }) => void = () => {};
|
||||
@ -14,6 +16,7 @@
|
||||
let showNewItemInput = false;
|
||||
let newItemName = "";
|
||||
let newItemInput: HTMLInputElement | null = null;
|
||||
let createTemplateMode = false;
|
||||
|
||||
type SidePanelItem = {
|
||||
id: string;
|
||||
@ -22,6 +25,11 @@
|
||||
};
|
||||
let todoDocuments: SidePanelItem[] = [];
|
||||
let customCalendarEntries: SidePanelItem[] = [];
|
||||
let templateItems: SidePanelItem[] = [];
|
||||
let templatesBusy = false;
|
||||
let templateError = "";
|
||||
let wasEntriesSection = false;
|
||||
let lastTemplateRefreshToken = -1;
|
||||
|
||||
const sectionTitles: Record<string, string> = {
|
||||
entries: "Entries",
|
||||
@ -62,6 +70,37 @@
|
||||
selectedCalendarDate = payload;
|
||||
}
|
||||
|
||||
function toTemplateStoreId(filePath: string): string {
|
||||
return `entries/template-file/${encodeURIComponent(filePath)}`;
|
||||
}
|
||||
|
||||
function toTemplateLabel(fileName: string): string {
|
||||
return fileName.replace(/\.template\.md$/i, "");
|
||||
}
|
||||
|
||||
function mapTemplateDto(item: EntryTemplateItemDto): SidePanelItem {
|
||||
return {
|
||||
id: toTemplateStoreId(item.filePath),
|
||||
label: toTemplateLabel(item.fileName),
|
||||
initialContent: ""
|
||||
};
|
||||
}
|
||||
|
||||
async function refreshTemplates() {
|
||||
if (activeSection !== "entries") return;
|
||||
templatesBusy = true;
|
||||
templateError = "";
|
||||
try {
|
||||
const templates = await listEntryTemplates();
|
||||
templateItems = templates.map(mapTemplateDto);
|
||||
} catch (error) {
|
||||
templateError = String(error);
|
||||
templateItems = [];
|
||||
} finally {
|
||||
templatesBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
function daysInMonth(year: number, month: number): number {
|
||||
return new Date(year, month + 1, 0).getDate();
|
||||
}
|
||||
@ -126,6 +165,9 @@
|
||||
|
||||
function handleAddItem() {
|
||||
if (activeSection === "entries" || activeSection === "todos" || activeSection === "lists") {
|
||||
if (activeSection === "entries") {
|
||||
createTemplateMode = false;
|
||||
}
|
||||
showNewItemInput = true;
|
||||
newItemName = "";
|
||||
queueMicrotask(() => newItemInput?.focus());
|
||||
@ -158,6 +200,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleAddTemplate() {
|
||||
if (activeSection !== "entries") return;
|
||||
createTemplateMode = true;
|
||||
showNewItemInput = true;
|
||||
newItemName = "";
|
||||
queueMicrotask(() => newItemInput?.focus());
|
||||
}
|
||||
|
||||
async function confirmNewItem() {
|
||||
const label = newItemName.trim();
|
||||
if (!label) {
|
||||
@ -168,10 +218,17 @@
|
||||
newItemName = "";
|
||||
|
||||
if (activeSection === "entries") {
|
||||
const id = `entries/draft-${Date.now()}`;
|
||||
const item = { id, label, initialContent: `# ${label}\n\n` };
|
||||
const isTemplate = createTemplateMode;
|
||||
const displayLabel = isTemplate
|
||||
? label.endsWith("_template")
|
||||
? label
|
||||
: `${label}_template`
|
||||
: label;
|
||||
const id = isTemplate ? `entries/template-draft-${Date.now()}` : `entries/draft-${Date.now()}`;
|
||||
const item = { id, label: displayLabel, initialContent: `# ${displayLabel}\n\n` };
|
||||
entriesStore.update((items) => [item, ...items]);
|
||||
onEditItem(item);
|
||||
createTemplateMode = false;
|
||||
} else if (activeSection === "todos") {
|
||||
try {
|
||||
const { meta, items: todoItems } = await createTodoListFromLabel(label);
|
||||
@ -208,6 +265,7 @@
|
||||
function cancelNewItem() {
|
||||
showNewItemInput = false;
|
||||
newItemName = "";
|
||||
createTemplateMode = false;
|
||||
}
|
||||
|
||||
function handleNewItemKeydown(event: KeyboardEvent) {
|
||||
@ -237,14 +295,38 @@
|
||||
$: isCalendarSection = activeSection === "calendar";
|
||||
$: calendarEntries = [...customCalendarEntries, ...getCalendarEntries(calendarYear, calendarMonth)];
|
||||
$: showItemActions = activeSection === "entries" || activeSection === "todos" || activeSection === "lists" || activeSection === "fragments";
|
||||
$: entryItems = activeSection === "entries"
|
||||
? $entriesStore.filter((item) => !item.id.startsWith("entries/template-draft-"))
|
||||
: [];
|
||||
$: templateDraftItems = activeSection === "entries"
|
||||
? $entriesStore.filter((item) => item.id.startsWith("entries/template-draft-"))
|
||||
: [];
|
||||
$: allTemplateItems = [...templateDraftItems, ...templateItems];
|
||||
|
||||
$: if (activeSection === "entries" && (!wasEntriesSection || templateRefreshToken !== lastTemplateRefreshToken)) {
|
||||
wasEntriesSection = true;
|
||||
lastTemplateRefreshToken = templateRefreshToken;
|
||||
void refreshTemplates();
|
||||
}
|
||||
|
||||
$: if (activeSection !== "entries") {
|
||||
wasEntriesSection = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="side-panel" aria-label="Section panel">
|
||||
<header class="panel-header">
|
||||
<h2>{panelTitle}</h2>
|
||||
<button type="button" class="panel-action" aria-label="Add item" on:click={handleAddItem}>
|
||||
<span class="material-symbols-outlined">add</span>
|
||||
</button>
|
||||
<div class="panel-header-actions">
|
||||
{#if activeSection === "entries"}
|
||||
<button type="button" class="panel-action" aria-label="Add template" title="Add template" on:click={handleAddTemplate}>
|
||||
<span class="material-symbols-outlined">palette</span>
|
||||
</button>
|
||||
{/if}
|
||||
<button type="button" class="panel-action" aria-label="Add item" title="Add item" on:click={handleAddItem}>
|
||||
<span class="material-symbols-outlined">add</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{#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}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<ul class="panel-list">
|
||||
{#each items as item}
|
||||
<li class:is-active={item.id === activeDocumentId}>
|
||||
<button
|
||||
type="button"
|
||||
class="item-label"
|
||||
on:click={() => onOpenDocument(item)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
{#if showItemActions}
|
||||
<div class="item-actions">
|
||||
<button type="button" class="item-action" on:click|stopPropagation={() => onEditItem(item)} aria-label="Edit">
|
||||
<span class="material-symbols-outlined">edit</span>
|
||||
</button>
|
||||
<button type="button" class="item-action item-action-danger" on:click|stopPropagation={() => onDeleteItem(item)} aria-label="Delete">
|
||||
<span class="material-symbols-outlined">delete</span>
|
||||
</button>
|
||||
</div>
|
||||
{#if activeSection === "entries"}
|
||||
<div class="panel-subsection">
|
||||
<h3>Entries</h3>
|
||||
{#if $entriesBusyStore}
|
||||
<p class="section-copy">Loading entries...</p>
|
||||
{:else}
|
||||
<ul class="panel-list">
|
||||
{#each entryItems as item}
|
||||
<li class:is-active={item.id === activeDocumentId}>
|
||||
<button
|
||||
type="button"
|
||||
class="item-label"
|
||||
on:click={() => onOpenDocument(item)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
<div class="item-actions">
|
||||
<button type="button" class="item-action" on:click|stopPropagation={() => onEditItem(item)} aria-label="Edit">
|
||||
<span class="material-symbols-outlined">edit</span>
|
||||
</button>
|
||||
<button type="button" class="item-action item-action-danger" on:click|stopPropagation={() => onDeleteItem(item)} aria-label="Delete">
|
||||
<span class="material-symbols-outlined">delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{#if !entryItems.length}
|
||||
<p class="section-copy">No entries found.</p>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="panel-subsection">
|
||||
<h3>Templates</h3>
|
||||
{#if templatesBusy}
|
||||
<p class="section-copy">Loading templates...</p>
|
||||
{:else}
|
||||
<ul class="panel-list">
|
||||
{#each allTemplateItems as item}
|
||||
<li class:is-active={item.id === activeDocumentId}>
|
||||
<button
|
||||
type="button"
|
||||
class="item-label"
|
||||
on:click={() => onOpenDocument(item)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
<div class="item-actions">
|
||||
<button type="button" class="item-action" on:click|stopPropagation={() => onEditItem(item)} aria-label="Edit">
|
||||
<span class="material-symbols-outlined">edit</span>
|
||||
</button>
|
||||
<button type="button" class="item-action item-action-danger" on:click|stopPropagation={() => onDeleteItem(item)} aria-label="Delete">
|
||||
<span class="material-symbols-outlined">delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{#if !allTemplateItems.length}
|
||||
<p class="section-copy">No templates found.</p>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if templateError}
|
||||
<p class="template-error">{templateError}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<ul class="panel-list">
|
||||
{#each items as item}
|
||||
<li class:is-active={item.id === activeDocumentId}>
|
||||
<button
|
||||
type="button"
|
||||
class="item-label"
|
||||
on:click={() => onOpenDocument(item)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
{#if showItemActions}
|
||||
<div class="item-actions">
|
||||
<button type="button" class="item-action" on:click|stopPropagation={() => onEditItem(item)} aria-label="Edit">
|
||||
<span class="material-symbols-outlined">edit</span>
|
||||
</button>
|
||||
<button type="button" class="item-action item-action-danger" on:click|stopPropagation={() => onDeleteItem(item)} aria-label="Delete">
|
||||
<span class="material-symbols-outlined">delete</span>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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 @@
|
||||
<SidePanel
|
||||
activeSection={selectedSection}
|
||||
{activeDocumentId}
|
||||
{templateRefreshToken}
|
||||
onOpenDocument={handleOpenDocument}
|
||||
onEditItem={handleEditItem}
|
||||
onDeleteItem={handleDeleteItem}
|
||||
@ -402,4 +472,3 @@
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="app-shell panel-closed">
|
||||
@ -392,57 +300,6 @@
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="route-card">
|
||||
<h2>Entry Templates</h2>
|
||||
<p class="section-copy">Create reusable markdown templates stored as <code>.template.md</code> files in your data directory.</p>
|
||||
|
||||
<div class="template-grid">
|
||||
<div class="template-editor">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Template name (example: Weekly Review)"
|
||||
bind:value={templateName}
|
||||
/>
|
||||
<textarea
|
||||
rows="8"
|
||||
placeholder="Template markdown content"
|
||||
bind:value={templateContent}
|
||||
></textarea>
|
||||
<div class="row-actions">
|
||||
<button type="button" class="secondary-btn" on:click={saveTemplate}>
|
||||
{templateFilePath ? "Update Template" : "Create Template"}
|
||||
</button>
|
||||
<button type="button" class="ghost-btn" on:click={resetTemplateEditor}>New</button>
|
||||
<button type="button" class="ghost-btn" on:click={refreshTemplates}>Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="template-list">
|
||||
{#if templatesBusy}
|
||||
<p class="section-copy">Loading templates...</p>
|
||||
{:else if templates.length === 0}
|
||||
<p class="section-copy">No templates found.</p>
|
||||
{:else}
|
||||
<ul class="item-list">
|
||||
{#each templates as item}
|
||||
<li class="item-row">
|
||||
<span>{item.fileName.replace(/\.template\.md$/i, "")}</span>
|
||||
<div class="row-actions">
|
||||
<button type="button" class="ghost-btn" on:click={() => editTemplate(item)}>Edit</button>
|
||||
<button type="button" class="danger-btn" on:click={() => removeTemplate(item)}>Delete</button>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if templateError}
|
||||
<p class="error-text">{templateError}</p>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="route-card">
|
||||
<h2>Sidecar</h2>
|
||||
<p class="section-copy">Root directory containing the Journal.Sidecar project.</p>
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user