journal/Journal.App/src/lib/components/EditorPanel.svelte

340 lines
8.8 KiB
Svelte

<script lang="ts">
import FragmentEditor from "$lib/components/editor/FragmentEditor.svelte";
import ListEditor from "$lib/components/editor/ListEditor.svelte";
import TodoEditor from "$lib/components/editor/TodoEditor.svelte";
import MarkdownEditor from "$lib/components/editor/MarkdownEditor.svelte";
export let activeSection = "entries";
export let openDocumentId = "entries/daily-notes";
export let openDocumentName = "Daily Notes";
export let openDocumentContent = "";
export let onDocumentContentChange: (content: string) => void = () => {};
export let onOpenDocument: (doc: {
id: string;
label: string;
initialContent: string;
linkedFrom?: {
id: string;
label: string;
initialContent: string;
section: "entries" | "fragments" | "todos" | "lists" | "calendar";
};
}) => void = () => {};
export let onDeleteDocument: (id: string) => void = () => {};
export let showLinkedBackButton = false;
export let onLinkedBack: () => void = () => {};
export let calendarItems: Array<{ id: string; label: string; initialContent: string }> = [];
export let calendarBusy = false;
export let calendarError = "";
export let previewOnly = true;
type CalendarCard = {
id: string;
label: string;
initialContent: string;
title: string;
summary: string;
hasTrigger: boolean;
hasMood: boolean;
hasOpenTodos: boolean;
};
function deriveSummary(content: string): string {
const lines = content.replace(/\r\n/g, "\n").split("\n");
let inFrontmatter = false;
let frontmatterDone = false;
for (const rawLine of lines) {
const line = rawLine.trim();
if (!frontmatterDone && line === "---") {
inFrontmatter = !inFrontmatter;
if (!inFrontmatter) frontmatterDone = true;
continue;
}
if (inFrontmatter || !line) continue;
if (/^#/.test(line)) continue;
if (/^\*\*Date:\*\*/i.test(line)) continue;
if (/^Date:/i.test(line)) continue;
if (/^(Type:|Tags:)/i.test(line)) continue;
return line.length > 180 ? `${line.slice(0, 177)}...` : line;
}
return "No summary available.";
}
function deriveTitle(label: string, content: string): string {
const heading = content.match(/^#\s+(.+)$/m)?.[1]?.trim();
if (heading) return heading;
return label?.trim() || "Untitled Entry";
}
function toCalendarCard(item: { id: string; label: string; initialContent: string }): CalendarCard {
const content = item.initialContent ?? "";
const lower = content.toLowerCase();
return {
...item,
title: deriveTitle(item.label, content),
summary: deriveSummary(content),
hasTrigger: lower.includes("!trigger") || lower.includes("#trigger") || lower.includes("#stress"),
hasMood: lower.includes("mental / emotional snapshot") || lower.includes("cognitive state"),
hasOpenTodos: /-\s*\[\s\]/.test(content)
};
}
$: calendarCards = calendarItems.map(toCalendarCard);
</script>
<main class="editor-panel" aria-label="Editor area">
{#if showLinkedBackButton}
<div class="editor-nav">
<button type="button" class="back-btn" on:click={onLinkedBack} aria-label="Back to source entry">
<span class="material-symbols-outlined" aria-hidden="true">arrow_back</span>
</button>
</div>
{/if}
{#if activeSection === "calendar"}
<section class="calendar-main" aria-label="Calendar timeline results">
<header class="calendar-main-header">
<h2>Filtered Entries</h2>
</header>
{#if calendarBusy}
<p class="calendar-copy">Loading timeline...</p>
{:else if calendarError}
<p class="calendar-copy is-error">{calendarError}</p>
{:else if calendarItems.length === 0}
<p class="calendar-copy">No entries matched the current filters.</p>
{:else}
<ul class="calendar-list">
{#each calendarCards as item}
<li class:is-active={item.id === openDocumentId}>
<button type="button" class="calendar-item-btn" on:click={() => onOpenDocument(item)}>
<div class="calendar-item-head">
<h3>{item.title}</h3>
<span class="calendar-date">{item.label}</span>
</div>
<p class="calendar-summary">{item.summary}</p>
<div class="calendar-badges">
{#if item.hasMood}<span class="badge mood">Mood</span>{/if}
{#if item.hasTrigger}<span class="badge trigger">Trigger</span>{/if}
{#if item.hasOpenTodos}<span class="badge todo">Open To-Dos</span>{/if}
</div>
</button>
</li>
{/each}
</ul>
{/if}
</section>
{:else if !openDocumentId}
<div class="editor-empty">
<span class="material-symbols-outlined empty-icon">edit_note</span>
<p>Select or create an item to get started</p>
</div>
{:else if activeSection === "fragments"}
<FragmentEditor
{openDocumentId}
{openDocumentName}
{openDocumentContent}
{onDocumentContentChange}
{onOpenDocument}
{onDeleteDocument}
externalEditRequested={!previewOnly}
/>
{:else if activeSection === "todos"}
<TodoEditor
{openDocumentId}
{openDocumentName}
{openDocumentContent}
{onDocumentContentChange}
/>
{:else if activeSection === "lists"}
<ListEditor
{openDocumentId}
{openDocumentName}
{openDocumentContent}
{onDocumentContentChange}
/>
{:else}
<MarkdownEditor
{openDocumentId}
{openDocumentName}
{openDocumentContent}
{onDocumentContentChange}
{onOpenDocument}
{previewOnly}
/>
{/if}
</main>
<style>
.editor-panel {
background: var(--bg-editor);
padding: 18px 20px;
display: flex;
flex-direction: column;
gap: 16px;
min-height: 0;
overflow: hidden;
}
.editor-nav {
display: flex;
align-items: center;
justify-content: flex-start;
}
.back-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border-radius: 8px;
border: 1px solid var(--border-soft);
background: color-mix(in srgb, var(--surface-1) 90%, var(--bg-editor) 10%);
color: var(--text-primary);
padding: 0;
cursor: pointer;
}
.back-btn:hover {
background: var(--bg-hover);
}
.editor-empty {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
color: var(--text-dim);
.empty-icon {
font-size: 2.4rem;
opacity: 0.5;
}
p {
font-size: 0.88rem;
}
}
.calendar-main {
flex: 1;
min-height: 0;
overflow: auto;
padding: 4px 8px;
display: flex;
flex-direction: column;
gap: 10px;
}
.calendar-main-header h2 {
font-size: 0.96rem;
font-weight: 600;
color: var(--text-primary);
}
.calendar-copy {
font-size: 0.84rem;
color: var(--text-dim);
}
.calendar-copy.is-error {
color: #e74c3c;
}
.calendar-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 6px;
}
.calendar-list li {
border-radius: 8px;
border: 1px solid var(--border-soft);
background: color-mix(in srgb, var(--surface-1) 92%, var(--bg-editor) 8%);
}
.calendar-list li:hover {
background: var(--bg-hover);
}
.calendar-list li.is-active {
border-color: var(--border-strong);
background: var(--bg-active);
}
.calendar-item-btn {
width: 100%;
text-align: left;
padding: 10px 12px;
color: var(--text-primary);
font-size: 0.86rem;
cursor: pointer;
display: flex;
flex-direction: column;
gap: 7px;
}
.calendar-item-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
}
.calendar-item-head h3 {
font-size: 0.9rem;
font-weight: 600;
color: var(--text-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.calendar-date {
font-size: 0.74rem;
color: var(--text-dim);
white-space: nowrap;
}
.calendar-summary {
font-size: 0.82rem;
color: var(--text-muted);
line-height: 1.45;
}
.calendar-badges {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.badge {
font-size: 0.68rem;
border-radius: 999px;
border: 1px solid var(--border-soft);
padding: 2px 7px;
color: var(--text-dim);
background: color-mix(in srgb, var(--surface-2) 88%, var(--bg-editor) 12%);
}
.badge.mood {
border-color: color-mix(in srgb, #6ba7ff 40%, var(--border-soft) 60%);
color: #8dbbff;
}
.badge.trigger {
border-color: color-mix(in srgb, #f08c6c 40%, var(--border-soft) 60%);
color: #f5ad95;
}
.badge.todo {
border-color: color-mix(in srgb, #f2c266 40%, var(--border-soft) 60%);
color: #f4d690;
}
</style>