From d51e700e4b245a3307fa6940ff564f7dc82721fa Mon Sep 17 00:00:00 2001 From: Jacob Schmidt Date: Fri, 27 Feb 2026 21:04:54 -0600 Subject: [PATCH] Implement calendar timeline workspace and main-panel results --- .../src/lib/components/CalendarWidget.svelte | 81 ++- .../src/lib/components/EditorPanel.svelte | 209 +++++- .../src/lib/components/SidePanel.svelte | 652 +++++++++++++++--- Journal.App/src/routes/+page.svelte | 16 + 4 files changed, 860 insertions(+), 98 deletions(-) diff --git a/Journal.App/src/lib/components/CalendarWidget.svelte b/Journal.App/src/lib/components/CalendarWidget.svelte index d548315..84de1e8 100644 --- a/Journal.App/src/lib/components/CalendarWidget.svelte +++ b/Journal.App/src/lib/components/CalendarWidget.svelte @@ -2,6 +2,8 @@ export let onVisibleMonthChange: (month: { year: number; month: number; label: string }) => void = () => {}; export let onSelectedDateChange: (payload: { year: number; month: number; day: number; key: string }) => void = () => {}; + export let onDateActivate: (payload: { year: number; month: number; day: number; key: string }) => void = () => {}; + export let signalsByDate: Record = {}; const today = new Date(); let currentYear = today.getFullYear(); @@ -31,6 +33,11 @@ currentMonth = next.getMonth(); } + function signalFor(cell: CalendarCell): { count: number; hasTrigger: boolean; hasMood: boolean; hasOpenTodos: boolean } | null { + const key = getDateKey(cell.year, cell.month, cell.day); + return signalsByDate[key] ?? null; + } + function changeMonth(offset: number) { setViewDate(currentYear, currentMonth + offset); } @@ -39,7 +46,14 @@ if (!cell.inMonth) { setViewDate(cell.year, cell.month); } - selectedDateKey = getDateKey(cell.year, cell.month, cell.day); + const key = getDateKey(cell.year, cell.month, cell.day); + selectedDateKey = key; + onDateActivate({ + year: cell.year, + month: cell.month, + day: cell.day, + key + }); } function getCalendarCells(year: number, month: number): CalendarCell[] { @@ -139,7 +153,17 @@ aria-label={`Day ${cell.day}`} on:click={() => selectCell(cell)} > - {cell.day} + {cell.day} + {#if signalFor(cell)} + {#if signalFor(cell)!.count > 0} + {signalFor(cell)!.count} + {/if} + + {/if} {/each} @@ -213,12 +237,63 @@ } .calendar-cell { - height: 30px; + height: 36px; border-radius: 7px; border: 1px solid transparent; font-size: 0.76rem; color: var(--text-muted); cursor: pointer; + position: relative; + display: flex; + align-items: center; + justify-content: center; + } + + .day-number { + line-height: 1; + } + + .entry-count { + position: absolute; + top: 3px; + right: 3px; + min-width: 12px; + height: 12px; + padding: 0 3px; + border-radius: 999px; + background: var(--surface-3); + color: var(--text-primary); + font-size: 0.58rem; + line-height: 12px; + border: 1px solid var(--border-soft); + } + + .signals { + position: absolute; + bottom: 3px; + left: 50%; + transform: translateX(-50%); + display: inline-flex; + gap: 2px; + } + + .signal { + width: 4px; + height: 4px; + border-radius: 999px; + display: inline-block; + } + + .signal.mood { + background: #6ba7ff; + } + + .signal.trigger { + background: #f08c6c; + } + + .signal.todo { + background: #f2c266; } .calendar-cell:hover { diff --git a/Journal.App/src/lib/components/EditorPanel.svelte b/Journal.App/src/lib/components/EditorPanel.svelte index 7cc3aa8..b58a529 100644 --- a/Journal.App/src/lib/components/EditorPanel.svelte +++ b/Journal.App/src/lib/components/EditorPanel.svelte @@ -23,7 +23,65 @@ 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);
@@ -35,7 +93,39 @@ {/if} - {#if !openDocumentId} + {#if activeSection === "calendar"} +
+
+

Filtered Entries

+
+ {#if calendarBusy} +

Loading timeline...

+ {:else if calendarError} +

{calendarError}

+ {:else if calendarItems.length === 0} +

No entries matched the current filters.

+ {:else} +
    + {#each calendarCards as item} +
  • + +
  • + {/each} +
+ {/if} +
+ {:else if !openDocumentId}
edit_note

Select or create an item to get started

@@ -129,4 +219,121 @@ 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; + } diff --git a/Journal.App/src/lib/components/SidePanel.svelte b/Journal.App/src/lib/components/SidePanel.svelte index cea5d5a..efa3747 100644 --- a/Journal.App/src/lib/components/SidePanel.svelte +++ b/Journal.App/src/lib/components/SidePanel.svelte @@ -1,7 +1,8 @@
@@ -356,26 +634,101 @@ {#if isCalendarSection} +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+

Saved Views

+
+ {#each builtInViews as view} + + {/each} + +
+ {#if showSaveViewInput} +
+ { + if (event.key === "Enter") saveCurrentView(); + if (event.key === "Escape") { + showSaveViewInput = false; + saveViewName = ""; + } + }} + on:blur={saveCurrentView} + /> +
+ {/if} + {#if calendarSavedViews.length > 0} +
    + {#each calendarSavedViews as view} +
  • + + +
  • + {/each} +
+ {/if} +
+
+
-

{calendarMonthLabel} {calendarYear} Entries

-
    - {#each calendarEntries as item} -
  • - -
  • - {/each} -
+

{calendarMonthLabel} {calendarYear} Timeline

+

Filtered items are shown in the main panel.

{:else}