340 lines
8.8 KiB
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>
|