Merge branch 'master' of https://gitea.innovativedevsolutions.org/J.Schmidt92/journal
This commit is contained in:
commit
bcdf74d9e5
@ -2,6 +2,8 @@
|
|||||||
export let onVisibleMonthChange: (month: { year: number; month: number; label: string }) => void = () => {};
|
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 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<string, { count: number; hasTrigger: boolean; hasMood: boolean; hasOpenTodos: boolean }> = {};
|
||||||
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
let currentYear = today.getFullYear();
|
let currentYear = today.getFullYear();
|
||||||
@ -31,6 +33,11 @@
|
|||||||
currentMonth = next.getMonth();
|
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) {
|
function changeMonth(offset: number) {
|
||||||
setViewDate(currentYear, currentMonth + offset);
|
setViewDate(currentYear, currentMonth + offset);
|
||||||
}
|
}
|
||||||
@ -39,7 +46,14 @@
|
|||||||
if (!cell.inMonth) {
|
if (!cell.inMonth) {
|
||||||
setViewDate(cell.year, cell.month);
|
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[] {
|
function getCalendarCells(year: number, month: number): CalendarCell[] {
|
||||||
@ -139,7 +153,17 @@
|
|||||||
aria-label={`Day ${cell.day}`}
|
aria-label={`Day ${cell.day}`}
|
||||||
on:click={() => selectCell(cell)}
|
on:click={() => selectCell(cell)}
|
||||||
>
|
>
|
||||||
{cell.day}
|
<span class="day-number">{cell.day}</span>
|
||||||
|
{#if signalFor(cell)}
|
||||||
|
{#if signalFor(cell)!.count > 0}
|
||||||
|
<span class="entry-count">{signalFor(cell)!.count}</span>
|
||||||
|
{/if}
|
||||||
|
<span class="signals" aria-hidden="true">
|
||||||
|
{#if signalFor(cell)!.hasMood}<i class="signal mood"></i>{/if}
|
||||||
|
{#if signalFor(cell)!.hasTrigger}<i class="signal trigger"></i>{/if}
|
||||||
|
{#if signalFor(cell)!.hasOpenTodos}<i class="signal todo"></i>{/if}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@ -213,12 +237,63 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.calendar-cell {
|
.calendar-cell {
|
||||||
height: 30px;
|
height: 36px;
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
font-size: 0.76rem;
|
font-size: 0.76rem;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
cursor: pointer;
|
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 {
|
.calendar-cell:hover {
|
||||||
|
|||||||
@ -23,7 +23,65 @@
|
|||||||
export let onDeleteDocument: (id: string) => void = () => {};
|
export let onDeleteDocument: (id: string) => void = () => {};
|
||||||
export let showLinkedBackButton = false;
|
export let showLinkedBackButton = false;
|
||||||
export let onLinkedBack: () => void = () => {};
|
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;
|
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>
|
</script>
|
||||||
|
|
||||||
<main class="editor-panel" aria-label="Editor area">
|
<main class="editor-panel" aria-label="Editor area">
|
||||||
@ -35,7 +93,39 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !openDocumentId}
|
{#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">
|
<div class="editor-empty">
|
||||||
<span class="material-symbols-outlined empty-icon">edit_note</span>
|
<span class="material-symbols-outlined empty-icon">edit_note</span>
|
||||||
<p>Select or create an item to get started</p>
|
<p>Select or create an item to get started</p>
|
||||||
@ -129,4 +219,121 @@
|
|||||||
font-size: 0.88rem;
|
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>
|
</style>
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { listEntryTemplates, type EntryTemplateItemDto } from "$lib/backend/templates";
|
import { listEntryTemplates, type EntryTemplateItemDto } from "$lib/backend/templates";
|
||||||
|
import { sendCommand } from "$lib/backend/client";
|
||||||
import CalendarWidget from "$lib/components/CalendarWidget.svelte";
|
import CalendarWidget from "$lib/components/CalendarWidget.svelte";
|
||||||
import { entriesBusyStore, entriesStore } from "$lib/stores/entries";
|
import { entriesBusyStore, entriesStore, searchEntriesAsItems } from "$lib/stores/entries";
|
||||||
import { createFragmentDraft, fragmentsStore } from "$lib/stores/fragments";
|
import { createFragmentDraft, fragmentsStore } from "$lib/stores/fragments";
|
||||||
import { createListDraft, createListFromLabel, listsStore } from "$lib/stores/lists";
|
import { createListDraft, createListFromLabel, listsStore } from "$lib/stores/lists";
|
||||||
import { createTodoListDraft, createTodoListFromLabel, serializeTodoList, todoListsStore, todosStore } from "$lib/stores/todos";
|
import { createTodoListDraft, createTodoListFromLabel, serializeTodoList, todoListsStore, todosStore } from "$lib/stores/todos";
|
||||||
@ -12,6 +13,7 @@
|
|||||||
export let onOpenDocument: (doc: { id: string; label: string; initialContent: string }) => void = () => {};
|
export let onOpenDocument: (doc: { id: string; label: string; initialContent: string }) => void = () => {};
|
||||||
export let onEditItem: (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 = () => {};
|
export let onDeleteItem: (doc: { id: string; label: string }) => void = () => {};
|
||||||
|
export let onCalendarStateChange: (state: { items: SidePanelItem[]; busy: boolean; error: string }) => void = () => {};
|
||||||
|
|
||||||
let showNewItemInput = false;
|
let showNewItemInput = false;
|
||||||
let newItemName = "";
|
let newItemName = "";
|
||||||
@ -23,8 +25,29 @@
|
|||||||
label: string;
|
label: string;
|
||||||
initialContent: string;
|
initialContent: string;
|
||||||
};
|
};
|
||||||
|
type CalendarViewMode = "day" | "week" | "month";
|
||||||
|
type CalendarSortMode = "asc" | "desc";
|
||||||
|
type CalendarSignal = {
|
||||||
|
count: number;
|
||||||
|
hasTrigger: boolean;
|
||||||
|
hasMood: boolean;
|
||||||
|
hasOpenTodos: boolean;
|
||||||
|
};
|
||||||
|
type SavedCalendarView = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
viewMode: CalendarViewMode;
|
||||||
|
sortMode: CalendarSortMode;
|
||||||
|
query: string;
|
||||||
|
tags: string;
|
||||||
|
types: string;
|
||||||
|
checked: string;
|
||||||
|
unchecked: string;
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
};
|
||||||
|
|
||||||
let todoDocuments: SidePanelItem[] = [];
|
let todoDocuments: SidePanelItem[] = [];
|
||||||
let customCalendarEntries: SidePanelItem[] = [];
|
|
||||||
let templateItems: SidePanelItem[] = [];
|
let templateItems: SidePanelItem[] = [];
|
||||||
let templatesBusy = false;
|
let templatesBusy = false;
|
||||||
let templateError = "";
|
let templateError = "";
|
||||||
@ -45,6 +68,28 @@
|
|||||||
let calendarMonthLabel = new Date(calendarYear, calendarMonth, 1).toLocaleString(undefined, {
|
let calendarMonthLabel = new Date(calendarYear, calendarMonth, 1).toLocaleString(undefined, {
|
||||||
month: "long"
|
month: "long"
|
||||||
});
|
});
|
||||||
|
let calendarViewMode: CalendarViewMode = "month";
|
||||||
|
let calendarSortMode: CalendarSortMode = "desc";
|
||||||
|
let calendarQuery = "";
|
||||||
|
let calendarTags = "";
|
||||||
|
let calendarTypes = "";
|
||||||
|
let calendarChecked = "";
|
||||||
|
let calendarUnchecked = "";
|
||||||
|
let calendarStartDate = "";
|
||||||
|
let calendarEndDate = "";
|
||||||
|
let calendarTimelineItems: SidePanelItem[] = [];
|
||||||
|
let calendarSignals: Record<string, CalendarSignal> = {};
|
||||||
|
let calendarBusy = false;
|
||||||
|
let calendarError = "";
|
||||||
|
let calendarSavedViews: SavedCalendarView[] = [];
|
||||||
|
let showSaveViewInput = false;
|
||||||
|
let saveViewName = "";
|
||||||
|
let hasLoadedSavedViews = false;
|
||||||
|
let lastCalendarTimelineKey = "";
|
||||||
|
let lastCalendarSignalsKey = "";
|
||||||
|
let calendarTimelineDebounce: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
const CALENDAR_SAVED_VIEWS_KEY = "journal.calendar.savedViews";
|
||||||
|
|
||||||
let selectedCalendarDate: { year: number; month: number; day: number; key: string } | null = {
|
let selectedCalendarDate: { year: number; month: number; day: number; key: string } | null = {
|
||||||
year: today.getFullYear(),
|
year: today.getFullYear(),
|
||||||
month: today.getMonth(),
|
month: today.getMonth(),
|
||||||
@ -58,18 +103,277 @@
|
|||||||
calendarMonthLabel = payload.label;
|
calendarMonthLabel = payload.label;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toMonthKey(year: number, month: number): string {
|
|
||||||
return `${year}-${String(month + 1).padStart(2, "0")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toDateKey(year: number, month: number, day: number): string {
|
function toDateKey(year: number, month: number, day: number): string {
|
||||||
return `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
return `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseCsv(input: string): string[] {
|
||||||
|
return input
|
||||||
|
.split(",")
|
||||||
|
.map((token) => token.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDateLabel(value: string): Date | null {
|
||||||
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return null;
|
||||||
|
const date = new Date(`${value}T00:00:00`);
|
||||||
|
return Number.isNaN(date.getTime()) ? null : date;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toIsoDate(date: Date): string {
|
||||||
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveDateRange(): { startDate: string; endDate: string } {
|
||||||
|
if (calendarStartDate && calendarEndDate) {
|
||||||
|
return { startDate: calendarStartDate, endDate: calendarEndDate };
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = selectedCalendarDate ?? {
|
||||||
|
year: calendarYear,
|
||||||
|
month: calendarMonth,
|
||||||
|
day: 1
|
||||||
|
};
|
||||||
|
const selectedDate = new Date(selected.year, selected.month, selected.day);
|
||||||
|
|
||||||
|
if (calendarViewMode === "day") {
|
||||||
|
const date = toIsoDate(selectedDate);
|
||||||
|
return { startDate: date, endDate: date };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (calendarViewMode === "week") {
|
||||||
|
const weekDay = (selectedDate.getDay() + 6) % 7;
|
||||||
|
const start = new Date(selectedDate);
|
||||||
|
start.setDate(selectedDate.getDate() - weekDay);
|
||||||
|
const end = new Date(start);
|
||||||
|
end.setDate(start.getDate() + 6);
|
||||||
|
return { startDate: toIsoDate(start), endDate: toIsoDate(end) };
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthStart = new Date(calendarYear, calendarMonth, 1);
|
||||||
|
const monthEnd = new Date(calendarYear, calendarMonth + 1, 0);
|
||||||
|
return { startDate: toIsoDate(monthStart), endDate: toIsoDate(monthEnd) };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDataDirectory(): Promise<string> {
|
||||||
|
const config = await sendCommand<{ dataDirectory?: string; DataDirectory?: string }>({
|
||||||
|
action: "config.get"
|
||||||
|
});
|
||||||
|
return (config.dataDirectory ?? config.DataDirectory ?? "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSignal(dateKey: string): CalendarSignal {
|
||||||
|
const existing = calendarSignals[dateKey];
|
||||||
|
if (existing) return existing;
|
||||||
|
return { count: 0, hasTrigger: false, hasMood: false, hasOpenTodos: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
function upsertSignal(dateKey: string, next: CalendarSignal) {
|
||||||
|
calendarSignals = { ...calendarSignals, [dateKey]: next };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshCalendarSignals(): Promise<void> {
|
||||||
|
const monthStart = toDateKey(calendarYear, calendarMonth, 1);
|
||||||
|
const monthEnd = toDateKey(calendarYear, calendarMonth, new Date(calendarYear, calendarMonth + 1, 0).getDate());
|
||||||
|
const dataDirectory = await getDataDirectory();
|
||||||
|
if (!dataDirectory) {
|
||||||
|
calendarSignals = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await searchEntriesAsItems({
|
||||||
|
dataDirectory,
|
||||||
|
startDate: monthStart,
|
||||||
|
endDate: monthEnd
|
||||||
|
});
|
||||||
|
|
||||||
|
let nextSignals: Record<string, CalendarSignal> = {};
|
||||||
|
for (const item of results) {
|
||||||
|
const dateKey = item.label.trim();
|
||||||
|
const date = parseDateLabel(dateKey);
|
||||||
|
if (!date) continue;
|
||||||
|
const signal = nextSignals[dateKey] ?? { count: 0, hasTrigger: false, hasMood: false, hasOpenTodos: false };
|
||||||
|
signal.count += 1;
|
||||||
|
const content = item.initialContent ?? "";
|
||||||
|
const lower = content.toLowerCase();
|
||||||
|
signal.hasTrigger = signal.hasTrigger || lower.includes("!trigger") || lower.includes("#trigger") || lower.includes("#stress");
|
||||||
|
signal.hasMood = signal.hasMood || lower.includes("mental / emotional snapshot") || lower.includes("cognitive state");
|
||||||
|
signal.hasOpenTodos = signal.hasOpenTodos || /-\s*\[\s\]/.test(content);
|
||||||
|
nextSignals[dateKey] = signal;
|
||||||
|
}
|
||||||
|
|
||||||
|
calendarSignals = nextSignals;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshCalendarTimeline(): Promise<void> {
|
||||||
|
calendarBusy = true;
|
||||||
|
calendarError = "";
|
||||||
|
try {
|
||||||
|
const dataDirectory = await getDataDirectory();
|
||||||
|
if (!dataDirectory) {
|
||||||
|
calendarTimelineItems = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { startDate, endDate } = getActiveDateRange();
|
||||||
|
const items = await searchEntriesAsItems({
|
||||||
|
dataDirectory,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
query: calendarQuery.trim() || undefined,
|
||||||
|
tags: parseCsv(calendarTags),
|
||||||
|
types: parseCsv(calendarTypes),
|
||||||
|
checked: parseCsv(calendarChecked),
|
||||||
|
unchecked: parseCsv(calendarUnchecked)
|
||||||
|
});
|
||||||
|
|
||||||
|
const sorted = [...items].sort((a, b) => {
|
||||||
|
const aDate = parseDateLabel(a.label)?.getTime() ?? 0;
|
||||||
|
const bDate = parseDateLabel(b.label)?.getTime() ?? 0;
|
||||||
|
return calendarSortMode === "asc" ? aDate - bDate : bDate - aDate;
|
||||||
|
});
|
||||||
|
calendarTimelineItems = sorted;
|
||||||
|
} catch (error) {
|
||||||
|
calendarError = String(error);
|
||||||
|
calendarTimelineItems = [];
|
||||||
|
} finally {
|
||||||
|
calendarBusy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSavedViews() {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
const raw = window.localStorage.getItem(CALENDAR_SAVED_VIEWS_KEY);
|
||||||
|
if (!raw) {
|
||||||
|
calendarSavedViews = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw) as SavedCalendarView[];
|
||||||
|
calendarSavedViews = Array.isArray(parsed) ? parsed : [];
|
||||||
|
} catch {
|
||||||
|
calendarSavedViews = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistSavedViews() {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
window.localStorage.setItem(CALENDAR_SAVED_VIEWS_KEY, JSON.stringify(calendarSavedViews));
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySavedView(view: SavedCalendarView) {
|
||||||
|
calendarViewMode = view.viewMode;
|
||||||
|
calendarSortMode = view.sortMode;
|
||||||
|
calendarQuery = view.query;
|
||||||
|
calendarTags = view.tags;
|
||||||
|
calendarTypes = view.types;
|
||||||
|
calendarChecked = view.checked;
|
||||||
|
calendarUnchecked = view.unchecked;
|
||||||
|
calendarStartDate = view.startDate;
|
||||||
|
calendarEndDate = view.endDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCurrentView() {
|
||||||
|
const name = saveViewName.trim();
|
||||||
|
if (!name) {
|
||||||
|
showSaveViewInput = false;
|
||||||
|
saveViewName = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const view: SavedCalendarView = {
|
||||||
|
id: `view-${Date.now()}`,
|
||||||
|
name,
|
||||||
|
viewMode: calendarViewMode,
|
||||||
|
sortMode: calendarSortMode,
|
||||||
|
query: calendarQuery,
|
||||||
|
tags: calendarTags,
|
||||||
|
types: calendarTypes,
|
||||||
|
checked: calendarChecked,
|
||||||
|
unchecked: calendarUnchecked,
|
||||||
|
startDate: calendarStartDate,
|
||||||
|
endDate: calendarEndDate
|
||||||
|
};
|
||||||
|
calendarSavedViews = [view, ...calendarSavedViews];
|
||||||
|
persistSavedViews();
|
||||||
|
showSaveViewInput = false;
|
||||||
|
saveViewName = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteSavedView(id: string) {
|
||||||
|
calendarSavedViews = calendarSavedViews.filter((view) => view.id !== id);
|
||||||
|
persistSavedViews();
|
||||||
|
}
|
||||||
|
|
||||||
|
const builtInViews: SavedCalendarView[] = [
|
||||||
|
{
|
||||||
|
id: "builtin-week",
|
||||||
|
name: "This Week",
|
||||||
|
viewMode: "week",
|
||||||
|
sortMode: "desc",
|
||||||
|
query: "",
|
||||||
|
tags: "",
|
||||||
|
types: "",
|
||||||
|
checked: "",
|
||||||
|
unchecked: "",
|
||||||
|
startDate: "",
|
||||||
|
endDate: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "builtin-trigger",
|
||||||
|
name: "Trigger Review",
|
||||||
|
viewMode: "month",
|
||||||
|
sortMode: "desc",
|
||||||
|
query: "",
|
||||||
|
tags: "stress, trigger",
|
||||||
|
types: "!TRIGGER",
|
||||||
|
checked: "",
|
||||||
|
unchecked: "",
|
||||||
|
startDate: "",
|
||||||
|
endDate: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "builtin-open-todos",
|
||||||
|
name: "Open Checklist",
|
||||||
|
viewMode: "month",
|
||||||
|
sortMode: "desc",
|
||||||
|
query: "",
|
||||||
|
tags: "",
|
||||||
|
types: "",
|
||||||
|
checked: "",
|
||||||
|
unchecked: "todo, follow up, check",
|
||||||
|
startDate: "",
|
||||||
|
endDate: ""
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
function openOrCreateDailyEntry(dateKey: string) {
|
||||||
|
const existing = $entriesStore.find((item) => item.label === dateKey && !item.id.startsWith("entries/template-draft-"));
|
||||||
|
if (existing) {
|
||||||
|
onOpenDocument(existing);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const draft: SidePanelItem = {
|
||||||
|
id: `entries/draft-${Date.now()}`,
|
||||||
|
label: dateKey,
|
||||||
|
initialContent: `# ${dateKey}\n\n`
|
||||||
|
};
|
||||||
|
entriesStore.update((items) => [draft, ...items]);
|
||||||
|
onEditItem(draft);
|
||||||
|
}
|
||||||
|
|
||||||
function handleSelectedDateChange(payload: { year: number; month: number; day: number; key: string }) {
|
function handleSelectedDateChange(payload: { year: number; month: number; day: number; key: string }) {
|
||||||
selectedCalendarDate = payload;
|
selectedCalendarDate = payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleDateActivate(payload: { year: number; month: number; day: number; key: string }) {
|
||||||
|
selectedCalendarDate = payload;
|
||||||
|
if (activeSection !== "calendar") return;
|
||||||
|
openOrCreateDailyEntry(payload.key);
|
||||||
|
}
|
||||||
|
|
||||||
function toTemplateStoreId(filePath: string): string {
|
function toTemplateStoreId(filePath: string): string {
|
||||||
return `entries/template-file/${encodeURIComponent(filePath)}`;
|
return `entries/template-file/${encodeURIComponent(filePath)}`;
|
||||||
}
|
}
|
||||||
@ -101,68 +405,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function daysInMonth(year: number, month: number): number {
|
|
||||||
return new Date(year, month + 1, 0).getDate();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCalendarEntries(year: number, month: number): SidePanelItem[] {
|
|
||||||
const monthKey = toMonthKey(year, month);
|
|
||||||
const shortLabel = new Date(year, month, 1).toLocaleString(undefined, {
|
|
||||||
month: "short",
|
|
||||||
year: "numeric"
|
|
||||||
});
|
|
||||||
const totalDays = daysInMonth(year, month);
|
|
||||||
const anchors = [3, 10, 17, 24].map((day) => Math.min(day, totalDays));
|
|
||||||
|
|
||||||
const entries: Array<SidePanelItem & { day: number; dateKey: string }> = [
|
|
||||||
{
|
|
||||||
id: `calendar/${monthKey}/overview`,
|
|
||||||
label: `${shortLabel} Overview`,
|
|
||||||
initialContent: `# ${shortLabel} Overview\n\nMonthly highlights and notes.`,
|
|
||||||
day: anchors[0],
|
|
||||||
dateKey: toDateKey(year, month, anchors[0])
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `calendar/${monthKey}/goals`,
|
|
||||||
label: `${shortLabel} Goals`,
|
|
||||||
initialContent: `# ${shortLabel} Goals\n\n- Goal 1\n- Goal 2\n- Goal 3`,
|
|
||||||
day: anchors[1],
|
|
||||||
dateKey: toDateKey(year, month, anchors[1])
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `calendar/${monthKey}/review`,
|
|
||||||
label: `${shortLabel} Review`,
|
|
||||||
initialContent: `# ${shortLabel} Review\n\nWhat went well and what to improve next month.`,
|
|
||||||
day: anchors[2],
|
|
||||||
dateKey: toDateKey(year, month, anchors[2])
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `calendar/${monthKey}/notes`,
|
|
||||||
label: `${shortLabel} Notes`,
|
|
||||||
initialContent: `# ${shortLabel} Notes\n\nFreeform notes for this month.`,
|
|
||||||
day: anchors[3],
|
|
||||||
dateKey: toDateKey(year, month, anchors[3])
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const selected = selectedCalendarDate;
|
|
||||||
const isSelectedInVisibleMonth = selected && selected.year === year && selected.month === month;
|
|
||||||
|
|
||||||
if (!isSelectedInVisibleMonth || !selected) {
|
|
||||||
return entries.sort((a, b) => a.day - b.day);
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedDay = selected.day;
|
|
||||||
return entries
|
|
||||||
.sort((a, b) => {
|
|
||||||
const distA = Math.abs(a.day - selectedDay);
|
|
||||||
const distB = Math.abs(b.day - selectedDay);
|
|
||||||
if (distA !== distB) return distA - distB;
|
|
||||||
return a.day - b.day;
|
|
||||||
})
|
|
||||||
.map(({ day, dateKey, ...item }) => item);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleAddItem() {
|
function handleAddItem() {
|
||||||
if (activeSection === "entries" || activeSection === "todos" || activeSection === "lists") {
|
if (activeSection === "entries" || activeSection === "todos" || activeSection === "lists") {
|
||||||
if (activeSection === "entries") {
|
if (activeSection === "entries") {
|
||||||
@ -186,17 +428,7 @@
|
|||||||
day: 1,
|
day: 1,
|
||||||
key: toDateKey(calendarYear, calendarMonth, 1)
|
key: toDateKey(calendarYear, calendarMonth, 1)
|
||||||
};
|
};
|
||||||
const monthLabel = new Date(selected.year, selected.month, selected.day).toLocaleString(undefined, {
|
openOrCreateDailyEntry(selected.key);
|
||||||
month: "short"
|
|
||||||
});
|
|
||||||
const label = `${monthLabel} ${selected.day} Note`;
|
|
||||||
const item: SidePanelItem = {
|
|
||||||
id: `calendar/${selected.key}/note-${Date.now()}`,
|
|
||||||
label,
|
|
||||||
initialContent: `# ${label}\n\nDate: ${selected.key}\n\nAdd your note...`
|
|
||||||
};
|
|
||||||
customCalendarEntries = [item, ...customCalendarEntries];
|
|
||||||
onOpenDocument(item);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -316,7 +548,6 @@
|
|||||||
? $listsStore
|
? $listsStore
|
||||||
: [];
|
: [];
|
||||||
$: isCalendarSection = activeSection === "calendar";
|
$: isCalendarSection = activeSection === "calendar";
|
||||||
$: calendarEntries = [...customCalendarEntries, ...getCalendarEntries(calendarYear, calendarMonth)];
|
|
||||||
$: showItemActions = activeSection === "entries" || activeSection === "todos" || activeSection === "lists" || activeSection === "fragments";
|
$: showItemActions = activeSection === "entries" || activeSection === "todos" || activeSection === "lists" || activeSection === "fragments";
|
||||||
$: entryItems = activeSection === "entries"
|
$: entryItems = activeSection === "entries"
|
||||||
? $entriesStore.filter((item) => !item.id.startsWith("entries/template-draft-"))
|
? $entriesStore.filter((item) => !item.id.startsWith("entries/template-draft-"))
|
||||||
@ -335,6 +566,53 @@
|
|||||||
$: if (activeSection !== "entries") {
|
$: if (activeSection !== "entries") {
|
||||||
wasEntriesSection = false;
|
wasEntriesSection = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: if (activeSection === "calendar" && !hasLoadedSavedViews) {
|
||||||
|
hasLoadedSavedViews = true;
|
||||||
|
loadSavedViews();
|
||||||
|
}
|
||||||
|
|
||||||
|
$: calendarTimelineRefreshKey = JSON.stringify({
|
||||||
|
activeSection,
|
||||||
|
calendarYear,
|
||||||
|
calendarMonth,
|
||||||
|
selectedCalendarDate,
|
||||||
|
calendarViewMode,
|
||||||
|
calendarSortMode,
|
||||||
|
calendarQuery,
|
||||||
|
calendarTags,
|
||||||
|
calendarTypes,
|
||||||
|
calendarChecked,
|
||||||
|
calendarUnchecked,
|
||||||
|
calendarStartDate,
|
||||||
|
calendarEndDate
|
||||||
|
});
|
||||||
|
$: if (activeSection === "calendar" && calendarTimelineRefreshKey !== lastCalendarTimelineKey) {
|
||||||
|
lastCalendarTimelineKey = calendarTimelineRefreshKey;
|
||||||
|
if (calendarTimelineDebounce) {
|
||||||
|
clearTimeout(calendarTimelineDebounce);
|
||||||
|
}
|
||||||
|
calendarTimelineDebounce = setTimeout(() => {
|
||||||
|
void refreshCalendarTimeline();
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: calendarSignalsRefreshKey = JSON.stringify({
|
||||||
|
activeSection,
|
||||||
|
calendarYear,
|
||||||
|
calendarMonth
|
||||||
|
});
|
||||||
|
$: if (activeSection === "calendar" && calendarSignalsRefreshKey !== lastCalendarSignalsKey) {
|
||||||
|
lastCalendarSignalsKey = calendarSignalsRefreshKey;
|
||||||
|
void refreshCalendarSignals();
|
||||||
|
}
|
||||||
|
$: if (activeSection === "calendar") {
|
||||||
|
onCalendarStateChange({
|
||||||
|
items: calendarTimelineItems,
|
||||||
|
busy: calendarBusy,
|
||||||
|
error: calendarError
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="side-panel" aria-label="Section panel">
|
<section class="side-panel" aria-label="Section panel">
|
||||||
@ -356,26 +634,101 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
{#if isCalendarSection}
|
{#if isCalendarSection}
|
||||||
|
<div class="calendar-controls">
|
||||||
|
<div class="calendar-control-row">
|
||||||
|
<label>
|
||||||
|
View
|
||||||
|
<select bind:value={calendarViewMode}>
|
||||||
|
<option value="day">Day</option>
|
||||||
|
<option value="week">Week</option>
|
||||||
|
<option value="month">Month</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Sort
|
||||||
|
<select bind:value={calendarSortMode}>
|
||||||
|
<option value="desc">Newest</option>
|
||||||
|
<option value="asc">Oldest</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="calendar-control-row">
|
||||||
|
<label>
|
||||||
|
Start
|
||||||
|
<input type="date" bind:value={calendarStartDate} />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
End
|
||||||
|
<input type="date" bind:value={calendarEndDate} />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="calendar-control-row">
|
||||||
|
<input type="text" bind:value={calendarQuery} placeholder="Text query" />
|
||||||
|
<input type="text" bind:value={calendarTags} placeholder="Tags (comma)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="calendar-control-row">
|
||||||
|
<input type="text" bind:value={calendarTypes} placeholder="Fragment types (comma)" />
|
||||||
|
<input type="text" bind:value={calendarChecked} placeholder="Checked todos (comma)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="calendar-control-row">
|
||||||
|
<input type="text" bind:value={calendarUnchecked} placeholder="Unchecked todos (comma)" />
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="calendar-saved-views">
|
||||||
|
<h3>Saved Views</h3>
|
||||||
|
<div class="saved-view-actions">
|
||||||
|
{#each builtInViews as view}
|
||||||
|
<button type="button" class="saved-view-btn" on:click={() => applySavedView(view)}>{view.name}</button>
|
||||||
|
{/each}
|
||||||
|
<button type="button" class="saved-view-btn" on:click={() => (showSaveViewInput = true)}>Save Current</button>
|
||||||
|
</div>
|
||||||
|
{#if showSaveViewInput}
|
||||||
|
<div class="saved-view-input">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={saveViewName}
|
||||||
|
placeholder="View name"
|
||||||
|
on:keydown={(event) => {
|
||||||
|
if (event.key === "Enter") saveCurrentView();
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
showSaveViewInput = false;
|
||||||
|
saveViewName = "";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
on:blur={saveCurrentView}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if calendarSavedViews.length > 0}
|
||||||
|
<ul class="saved-view-list">
|
||||||
|
{#each calendarSavedViews as view}
|
||||||
|
<li>
|
||||||
|
<button type="button" class="saved-view-btn" on:click={() => applySavedView(view)}>{view.name}</button>
|
||||||
|
<button type="button" class="saved-view-delete" on:click={() => deleteSavedView(view.id)} aria-label="Delete saved view">
|
||||||
|
<span class="material-symbols-outlined">delete</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<CalendarWidget
|
<CalendarWidget
|
||||||
onVisibleMonthChange={handleVisibleMonthChange}
|
onVisibleMonthChange={handleVisibleMonthChange}
|
||||||
onSelectedDateChange={handleSelectedDateChange}
|
onSelectedDateChange={handleSelectedDateChange}
|
||||||
|
onDateActivate={handleDateActivate}
|
||||||
|
signalsByDate={calendarSignals}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="calendar-entries">
|
<div class="calendar-entries">
|
||||||
<h3>{calendarMonthLabel} {calendarYear} Entries</h3>
|
<h3>{calendarMonthLabel} {calendarYear} Timeline</h3>
|
||||||
<ul class="panel-list">
|
<p class="section-copy">Filtered items are shown in the main panel.</p>
|
||||||
{#each calendarEntries as item}
|
|
||||||
<li class:is-active={item.id === activeDocumentId}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="item-label"
|
|
||||||
on:click={() => onOpenDocument(item)}
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="panel-search">
|
<div class="panel-search">
|
||||||
@ -663,6 +1016,117 @@
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.calendar-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
border: 1px solid var(--border-soft);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--surface-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-control-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-control-row label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-control-row input,
|
||||||
|
.calendar-control-row select {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid var(--border-soft);
|
||||||
|
border-radius: 7px;
|
||||||
|
background: var(--surface-2);
|
||||||
|
color: var(--text-primary);
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-saved-views {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-saved-views h3 {
|
||||||
|
font-size: 0.76rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saved-view-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saved-view-btn {
|
||||||
|
border: 1px solid var(--border-soft);
|
||||||
|
border-radius: 7px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
font-size: 0.74rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
background: var(--surface-2);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saved-view-btn:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-color: var(--border-strong);
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.saved-view-input input {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid var(--border-soft);
|
||||||
|
border-radius: 7px;
|
||||||
|
background: var(--surface-2);
|
||||||
|
color: var(--text-primary);
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saved-view-list {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saved-view-list li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saved-view-delete {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
color: var(--text-dim);
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saved-view-delete:hover {
|
||||||
|
color: #e06c75;
|
||||||
|
border-color: var(--border-soft);
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
.calendar-entries h3 {
|
.calendar-entries h3 {
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|||||||
@ -26,6 +26,11 @@
|
|||||||
section: StartupView;
|
section: StartupView;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
type CalendarPanelState = {
|
||||||
|
items: OpenDocument[];
|
||||||
|
busy: boolean;
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
|
||||||
const initialEntry = getDefaultEntry(get(entriesStore));
|
const initialEntry = getDefaultEntry(get(entriesStore));
|
||||||
|
|
||||||
@ -55,6 +60,11 @@
|
|||||||
let pendingDeleteItemId = "";
|
let pendingDeleteItemId = "";
|
||||||
let templateRefreshToken = 0;
|
let templateRefreshToken = 0;
|
||||||
let linkedBackTarget: OpenDocument["linkedFrom"] | null = null;
|
let linkedBackTarget: OpenDocument["linkedFrom"] | null = null;
|
||||||
|
let calendarPanelState: CalendarPanelState = {
|
||||||
|
items: [],
|
||||||
|
busy: false,
|
||||||
|
error: ""
|
||||||
|
};
|
||||||
|
|
||||||
function resolveStartupSection(value: string): StartupView {
|
function resolveStartupSection(value: string): StartupView {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
@ -512,6 +522,9 @@
|
|||||||
onOpenDocument={handleOpenDocument}
|
onOpenDocument={handleOpenDocument}
|
||||||
onEditItem={handleEditItem}
|
onEditItem={handleEditItem}
|
||||||
onDeleteItem={handleDeleteItem}
|
onDeleteItem={handleDeleteItem}
|
||||||
|
onCalendarStateChange={(state) => {
|
||||||
|
calendarPanelState = state;
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<EditorPanel
|
<EditorPanel
|
||||||
@ -524,6 +537,9 @@
|
|||||||
onDeleteDocument={handleDeleteDocument}
|
onDeleteDocument={handleDeleteDocument}
|
||||||
showLinkedBackButton={linkedBackTarget !== null}
|
showLinkedBackButton={linkedBackTarget !== null}
|
||||||
onLinkedBack={handleLinkedBack}
|
onLinkedBack={handleLinkedBack}
|
||||||
|
calendarItems={calendarPanelState.items}
|
||||||
|
calendarBusy={calendarPanelState.busy}
|
||||||
|
calendarError={calendarPanelState.error}
|
||||||
previewOnly={!editMode}
|
previewOnly={!editMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user