1601 lines
42 KiB
Svelte
1601 lines
42 KiB
Svelte
<!-- @format -->
|
|
<script lang="ts">
|
|
import {
|
|
listEntryTemplates,
|
|
type EntryTemplateItemDto,
|
|
} from "$lib/backend/templates";
|
|
import { listFragments, type FragmentDto } from "$lib/backend/fragments";
|
|
import { listLists, type ListDocumentDto } from "$lib/backend/lists";
|
|
import { listTodoLists, type TodoListDto } from "$lib/backend/todos";
|
|
import CalendarWidget from "$lib/components/CalendarWidget.svelte";
|
|
import {
|
|
entriesBusyStore,
|
|
entriesStore,
|
|
searchEntriesAsItems,
|
|
} from "$lib/stores/entries";
|
|
import {
|
|
createFragmentDraft,
|
|
fragmentsStore,
|
|
serializeFragment,
|
|
} from "$lib/stores/fragments";
|
|
import {
|
|
createListDraft,
|
|
createListFromLabel,
|
|
listsStore,
|
|
} from "$lib/stores/lists";
|
|
import {
|
|
createTodoListDraft,
|
|
createTodoListFromLabel,
|
|
serializeTodoList,
|
|
todoListsStore,
|
|
todosStore,
|
|
} from "$lib/stores/todos";
|
|
import { vaultUnlocked } from "$lib/stores/session";
|
|
import { extractEntryTags } from "$lib/utils/metadata";
|
|
|
|
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 = () => {};
|
|
export let onCalendarStateChange: (state: {
|
|
items: SidePanelItem[];
|
|
busy: boolean;
|
|
error: string;
|
|
}) => void = () => {};
|
|
|
|
let showNewItemInput = false;
|
|
let newItemName = "";
|
|
let newItemInput: HTMLInputElement | null = null;
|
|
let createTemplateMode = false;
|
|
|
|
type SidePanelItem = {
|
|
id: string;
|
|
label: string;
|
|
initialContent: string;
|
|
sortDate?: string;
|
|
};
|
|
type CalendarViewMode = "day" | "week" | "month";
|
|
type CalendarSortMode = "asc" | "desc";
|
|
type SavedCalendarView = {
|
|
id: string;
|
|
name: string;
|
|
viewMode: CalendarViewMode;
|
|
sortMode: CalendarSortMode;
|
|
query: string;
|
|
tags: string;
|
|
types: string;
|
|
startDate: string;
|
|
endDate: string;
|
|
};
|
|
|
|
let todoDocuments: SidePanelItem[] = [];
|
|
let templateItems: SidePanelItem[] = [];
|
|
let templatesBusy = false;
|
|
let templateError = "";
|
|
let wasEntriesSection = false;
|
|
let lastTemplateRefreshToken = -1;
|
|
|
|
const sectionTitles: Record<string, string> = {
|
|
entries: "Entries",
|
|
calendar: "Calendar",
|
|
fragments: "Fragments",
|
|
todos: "To-Do List",
|
|
lists: "Lists",
|
|
};
|
|
|
|
const today = new Date();
|
|
let calendarYear = today.getFullYear();
|
|
let calendarMonth = today.getMonth();
|
|
let calendarMonthLabel = new Date(
|
|
calendarYear,
|
|
calendarMonth,
|
|
1,
|
|
).toLocaleString(undefined, {
|
|
month: "long",
|
|
});
|
|
let calendarViewMode: CalendarViewMode = "month";
|
|
let calendarSortMode: CalendarSortMode = "desc";
|
|
let calendarQuery = "";
|
|
let calendarTags = "";
|
|
let calendarTypes = "";
|
|
let calendarStartDate = "";
|
|
let calendarEndDate = "";
|
|
let calendarTimelineItems: SidePanelItem[] = [];
|
|
let calendarBusy = false;
|
|
let calendarError = "";
|
|
let calendarSavedViews: SavedCalendarView[] = [];
|
|
let showSaveViewInput = false;
|
|
let saveViewName = "";
|
|
let hasLoadedSavedViews = false;
|
|
let calendarTimelineDebounce: ReturnType<typeof setTimeout> | null = null;
|
|
let calendarDataVersion = 0;
|
|
let calendarScheduleKey = "";
|
|
let calendarRefreshRequestId = 0;
|
|
let lastActiveSection = "";
|
|
let calendarLastRefreshedAt = "";
|
|
let calendarDateExplicitlySelected = false;
|
|
const CALENDAR_SAVED_VIEWS_KEY = "journal.calendar.savedViews";
|
|
|
|
let selectedCalendarDate: {
|
|
year: number;
|
|
month: number;
|
|
day: number;
|
|
key: string;
|
|
} | null = {
|
|
year: today.getFullYear(),
|
|
month: today.getMonth(),
|
|
day: today.getDate(),
|
|
key: `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`,
|
|
};
|
|
|
|
function handleVisibleMonthChange(payload: {
|
|
year: number;
|
|
month: number;
|
|
label: string;
|
|
}) {
|
|
calendarYear = payload.year;
|
|
calendarMonth = payload.month;
|
|
calendarMonthLabel = payload.label;
|
|
}
|
|
|
|
function toDateKey(year: number, month: number, day: number): string {
|
|
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 {
|
|
const token = value
|
|
.trim()
|
|
.split(/\s*[|·]\s*/)[0]
|
|
.trim();
|
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(token)) return null;
|
|
const date = new Date(`${token}T00:00:00`);
|
|
return Number.isNaN(date.getTime()) ? null : date;
|
|
}
|
|
|
|
function parseIsoDate(value: string): Date | null {
|
|
if (!value) return null;
|
|
const parsed = new Date(value);
|
|
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
}
|
|
|
|
function toIsoDate(date: Date): string {
|
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
}
|
|
|
|
function isWithinRange(
|
|
date: Date,
|
|
startDate: string,
|
|
endDate: string,
|
|
): boolean {
|
|
const start = parseDateLabel(startDate);
|
|
const end = parseDateLabel(endDate);
|
|
if (!start || !end) return true;
|
|
const day = toIsoDate(date);
|
|
return day >= toIsoDate(start) && day <= toIsoDate(end);
|
|
}
|
|
|
|
function matchesTextQuery(content: string, query: string): boolean {
|
|
const trimmed = query.trim();
|
|
if (!trimmed) return true;
|
|
return content.toLowerCase().includes(trimmed.toLowerCase());
|
|
}
|
|
|
|
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) };
|
|
}
|
|
|
|
function toFragmentTimelineItem(fragment: FragmentDto): SidePanelItem {
|
|
const split = fragment.description.split(/\n{2,}/);
|
|
const title = (split[0] ?? "").trim() || "Untitled Fragment";
|
|
const body =
|
|
split.slice(1).join("\n\n").trim() || "Add details for this fragment.";
|
|
const date = parseIsoDate(fragment.time) ?? new Date();
|
|
const dateKey = toIsoDate(date);
|
|
return {
|
|
id: `fragments/${fragment.id}`,
|
|
label: `${dateKey} | Fragment | ${title}`,
|
|
initialContent: serializeFragment({
|
|
title,
|
|
type: fragment.type,
|
|
tags: fragment.tags ?? [],
|
|
body,
|
|
}),
|
|
sortDate: date.toISOString(),
|
|
};
|
|
}
|
|
|
|
function toListTimelineItem(list: ListDocumentDto): SidePanelItem {
|
|
const created =
|
|
parseIsoDate(list.createdAt) ??
|
|
parseIsoDate(list.updatedAt) ??
|
|
new Date();
|
|
const dateKey = toIsoDate(created);
|
|
return {
|
|
id: `lists/${list.id}`,
|
|
label: `${dateKey} | List | ${list.label}`,
|
|
initialContent: list.content || `# ${list.label}\n\n`,
|
|
sortDate: created.toISOString(),
|
|
};
|
|
}
|
|
|
|
function toTodoTimelineItem(list: TodoListDto): SidePanelItem {
|
|
const created = parseIsoDate(list.createdAt) ?? new Date();
|
|
const dateKey = toIsoDate(created);
|
|
const items = list.items.map((item, index) => ({
|
|
id: Date.now() + index,
|
|
text: item.text,
|
|
done: item.done,
|
|
}));
|
|
return {
|
|
id: `todos/${list.id}`,
|
|
label: `${dateKey} | To-Do | ${list.label}`,
|
|
initialContent: serializeTodoList(list.label, items),
|
|
sortDate: created.toISOString(),
|
|
};
|
|
}
|
|
|
|
function splitFilterTokens(input: string): string[] {
|
|
return parseCsv(input).map((token) => token.toLowerCase());
|
|
}
|
|
|
|
function matchesAnyToken(text: string, tokens: string[]): boolean {
|
|
if (tokens.length === 0) return true;
|
|
const lower = text.toLowerCase();
|
|
return tokens.some((token) => lower.includes(token));
|
|
}
|
|
|
|
function matchesTags(
|
|
itemTags: string[] | undefined,
|
|
tagTokens: string[],
|
|
): boolean {
|
|
if (tagTokens.length === 0) return true;
|
|
const normalized = (itemTags ?? [])
|
|
.map((tag) => tag.toLowerCase().trim())
|
|
.filter(Boolean);
|
|
return tagTokens.some((token) => normalized.includes(token));
|
|
}
|
|
|
|
function scheduleCalendarRefresh(delayMs = 200) {
|
|
if (activeSection !== "calendar") return;
|
|
|
|
const scheduleKey = [
|
|
selectedCalendarDate?.key ?? "",
|
|
calendarYear,
|
|
calendarMonth,
|
|
calendarViewMode,
|
|
calendarSortMode,
|
|
calendarQuery,
|
|
calendarTags,
|
|
calendarTypes,
|
|
calendarStartDate,
|
|
calendarEndDate,
|
|
calendarDataVersion,
|
|
].join("|");
|
|
|
|
if (scheduleKey === calendarScheduleKey && calendarTimelineDebounce) {
|
|
return;
|
|
}
|
|
|
|
calendarScheduleKey = scheduleKey;
|
|
if (calendarTimelineDebounce) {
|
|
clearTimeout(calendarTimelineDebounce);
|
|
}
|
|
|
|
calendarTimelineDebounce = setTimeout(() => {
|
|
void refreshCalendarTimeline();
|
|
}, delayMs);
|
|
}
|
|
|
|
async function refreshCalendarTimeline(): Promise<void> {
|
|
const requestId = ++calendarRefreshRequestId;
|
|
calendarBusy = true;
|
|
calendarError = "";
|
|
try {
|
|
if (requestId !== calendarRefreshRequestId) return;
|
|
if (!$vaultUnlocked) {
|
|
calendarTimelineItems = [];
|
|
return;
|
|
}
|
|
|
|
const { startDate, endDate } = getActiveDateRange();
|
|
const entryItems = await searchEntriesAsItems({
|
|
query: calendarQuery.trim() || undefined,
|
|
});
|
|
if (requestId !== calendarRefreshRequestId) return;
|
|
|
|
const [fragmentDtos, listDtos, todoDtos] = await Promise.all([
|
|
listFragments(),
|
|
listLists(),
|
|
listTodoLists(),
|
|
]);
|
|
if (requestId !== calendarRefreshRequestId) return;
|
|
|
|
const query = calendarQuery.trim();
|
|
const tagTokens = splitFilterTokens(calendarTags);
|
|
const typeTokens = splitFilterTokens(calendarTypes);
|
|
const hasTypeFilter = typeTokens.length > 0;
|
|
|
|
const fragmentItems = fragmentDtos
|
|
.filter((fragment) => {
|
|
const date = parseIsoDate(fragment.time);
|
|
if (!date || !isWithinRange(date, startDate, endDate)) return false;
|
|
if (!matchesTextQuery(fragment.description, query)) return false;
|
|
if (!matchesTags(fragment.tags, tagTokens)) return false;
|
|
if (
|
|
hasTypeFilter &&
|
|
!matchesAnyToken(fragment.type ?? "", typeTokens)
|
|
)
|
|
return false;
|
|
return true;
|
|
})
|
|
.map(toFragmentTimelineItem);
|
|
|
|
const listItems = listDtos.map(toListTimelineItem).filter((item) => {
|
|
if (hasTypeFilter) return false;
|
|
if (tagTokens.length > 0) return false;
|
|
const date = item.sortDate ? parseIsoDate(item.sortDate) : null;
|
|
if (!date || !isWithinRange(date, startDate, endDate)) return false;
|
|
if (
|
|
!matchesTextQuery(item.initialContent, query) &&
|
|
!matchesTextQuery(item.label, query)
|
|
)
|
|
return false;
|
|
return true;
|
|
});
|
|
|
|
const todoItems = todoDtos.map(toTodoTimelineItem).filter((item) => {
|
|
if (hasTypeFilter) return false;
|
|
if (tagTokens.length > 0) return false;
|
|
const date = item.sortDate ? parseIsoDate(item.sortDate) : null;
|
|
if (!date || !isWithinRange(date, startDate, endDate)) return false;
|
|
if (
|
|
!matchesTextQuery(item.initialContent, query) &&
|
|
!matchesTextQuery(item.label, query)
|
|
)
|
|
return false;
|
|
return true;
|
|
});
|
|
|
|
const entriesWithKind: SidePanelItem[] = entryItems
|
|
.map((item) => {
|
|
const date = parseDateLabel(item.label);
|
|
const dateKey = date ? toIsoDate(date) : item.label;
|
|
return {
|
|
id: item.id,
|
|
label: `${dateKey} | Entry | ${item.label}`,
|
|
initialContent: item.initialContent,
|
|
sortDate: date ? date.toISOString() : undefined,
|
|
};
|
|
})
|
|
.filter((item) => {
|
|
if (hasTypeFilter) return false;
|
|
if (!matchesTags(extractEntryTags(item.initialContent), tagTokens))
|
|
return false;
|
|
if (
|
|
!matchesTextQuery(item.initialContent, query) &&
|
|
!matchesTextQuery(item.label, query)
|
|
)
|
|
return false;
|
|
const date = item.sortDate ? parseIsoDate(item.sortDate) : null;
|
|
if (date && !isWithinRange(date, startDate, endDate)) return false;
|
|
return true;
|
|
});
|
|
|
|
const merged = [
|
|
...entriesWithKind,
|
|
...fragmentItems,
|
|
...listItems,
|
|
...todoItems,
|
|
];
|
|
const sorted = merged.sort((a, b) => {
|
|
const aDate = a.sortDate
|
|
? (parseIsoDate(a.sortDate)?.getTime() ?? 0)
|
|
: (parseDateLabel(a.label)?.getTime() ?? 0);
|
|
const bDate = b.sortDate
|
|
? (parseIsoDate(b.sortDate)?.getTime() ?? 0)
|
|
: (parseDateLabel(b.label)?.getTime() ?? 0);
|
|
return calendarSortMode === "asc" ? aDate - bDate : bDate - aDate;
|
|
});
|
|
calendarTimelineItems = sorted;
|
|
calendarLastRefreshedAt = new Date().toLocaleTimeString(undefined, {
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
second: "2-digit",
|
|
});
|
|
} catch (error) {
|
|
if (requestId !== calendarRefreshRequestId) return;
|
|
calendarError = String(error);
|
|
calendarTimelineItems = [];
|
|
} finally {
|
|
if (requestId === calendarRefreshRequestId) {
|
|
calendarBusy = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
async function forceRefreshCalendar(options?: {
|
|
allowWhileBusy?: boolean;
|
|
}): Promise<void> {
|
|
if (activeSection !== "calendar") return;
|
|
if (calendarBusy && !options?.allowWhileBusy) return;
|
|
if (calendarTimelineDebounce) {
|
|
clearTimeout(calendarTimelineDebounce);
|
|
calendarTimelineDebounce = null;
|
|
}
|
|
await refreshCalendarTimeline();
|
|
}
|
|
|
|
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;
|
|
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,
|
|
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: "",
|
|
startDate: "",
|
|
endDate: "",
|
|
},
|
|
{
|
|
id: "builtin-trigger",
|
|
name: "Trigger Review",
|
|
viewMode: "month",
|
|
sortMode: "desc",
|
|
query: "",
|
|
tags: "stress, trigger",
|
|
types: "!TRIGGER",
|
|
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;
|
|
}) {
|
|
selectedCalendarDate = payload;
|
|
}
|
|
|
|
function handleDateActivate(payload: {
|
|
year: number;
|
|
month: number;
|
|
day: number;
|
|
key: string;
|
|
}) {
|
|
selectedCalendarDate = payload;
|
|
if (activeSection !== "calendar") return;
|
|
calendarDateExplicitlySelected = true;
|
|
calendarStartDate = payload.key;
|
|
calendarEndDate = payload.key;
|
|
}
|
|
|
|
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;
|
|
if (!$vaultUnlocked) {
|
|
templatesBusy = false;
|
|
templateError = "";
|
|
templateItems = [];
|
|
return;
|
|
}
|
|
templatesBusy = true;
|
|
templateError = "";
|
|
try {
|
|
const templates = await listEntryTemplates();
|
|
templateItems = templates.map(mapTemplateDto);
|
|
} catch (error) {
|
|
templateError = String(error);
|
|
templateItems = [];
|
|
} finally {
|
|
templatesBusy = false;
|
|
}
|
|
}
|
|
|
|
function handleAddItem() {
|
|
if (
|
|
activeSection === "entries" ||
|
|
activeSection === "todos" ||
|
|
activeSection === "lists"
|
|
) {
|
|
if (activeSection === "entries") {
|
|
createTemplateMode = false;
|
|
}
|
|
showNewItemInput = true;
|
|
newItemName = "";
|
|
queueMicrotask(() => newItemInput?.focus());
|
|
return;
|
|
}
|
|
|
|
if (activeSection === "fragments") {
|
|
onOpenDocument(createFragmentDraft());
|
|
return;
|
|
}
|
|
|
|
if (activeSection === "calendar") {
|
|
if (calendarDateExplicitlySelected) {
|
|
const selected = selectedCalendarDate ?? {
|
|
year: calendarYear,
|
|
month: calendarMonth,
|
|
day: 1,
|
|
key: toDateKey(calendarYear, calendarMonth, 1),
|
|
};
|
|
openOrCreateDailyEntry(selected.key);
|
|
} else {
|
|
showNewItemInput = true;
|
|
newItemName = "";
|
|
queueMicrotask(() => newItemInput?.focus());
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleRefreshClick() {
|
|
void forceRefreshCalendar();
|
|
}
|
|
|
|
function handleAddTemplate() {
|
|
if (activeSection !== "entries") return;
|
|
createTemplateMode = true;
|
|
showNewItemInput = true;
|
|
newItemName = "";
|
|
queueMicrotask(() => newItemInput?.focus());
|
|
}
|
|
|
|
function toDailyNoteLabel(date: Date): string {
|
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
}
|
|
|
|
function handleAddDailyNote() {
|
|
if (activeSection !== "entries") return;
|
|
createTemplateMode = false;
|
|
showNewItemInput = false;
|
|
newItemName = "";
|
|
|
|
const label = toDailyNoteLabel(new Date());
|
|
const existing = $entriesStore.find(
|
|
(item) =>
|
|
item.label === label && !item.id.startsWith("entries/template-draft-"),
|
|
);
|
|
if (existing) {
|
|
onEditItem(existing);
|
|
return;
|
|
}
|
|
|
|
const id = `entries/draft-${Date.now()}`;
|
|
const item = { id, label, initialContent: `# ${label}\n\n` };
|
|
entriesStore.update((items) => [item, ...items]);
|
|
onEditItem(item);
|
|
}
|
|
|
|
async function confirmNewItem() {
|
|
const label = newItemName.trim();
|
|
if (!label) {
|
|
cancelNewItem();
|
|
return;
|
|
}
|
|
showNewItemInput = false;
|
|
newItemName = "";
|
|
|
|
if (activeSection === "entries") {
|
|
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 === "calendar") {
|
|
const id = `entries/draft-${Date.now()}`;
|
|
const item = { id, label, initialContent: `# ${label}\n\n` };
|
|
entriesStore.update((items) => [item, ...items]);
|
|
onEditItem(item);
|
|
} else if (activeSection === "todos") {
|
|
try {
|
|
const { meta, items: todoItems } = await createTodoListFromLabel(label);
|
|
onOpenDocument({
|
|
id: meta.id,
|
|
label: meta.label,
|
|
initialContent: serializeTodoList(meta.label, todoItems),
|
|
});
|
|
} catch (error) {
|
|
const draft = createTodoListDraft();
|
|
draft.meta.label = label;
|
|
todoListsStore.update((lists) => [draft.meta, ...lists]);
|
|
todosStore.update((lists) => ({
|
|
...lists,
|
|
[draft.meta.id]: draft.items,
|
|
}));
|
|
onOpenDocument({
|
|
id: draft.meta.id,
|
|
label: draft.meta.label,
|
|
initialContent: serializeTodoList(draft.meta.label, draft.items),
|
|
});
|
|
}
|
|
} else if (activeSection === "lists") {
|
|
try {
|
|
const item = await createListFromLabel(label);
|
|
onOpenDocument(item);
|
|
} catch (error) {
|
|
const item = createListDraft();
|
|
item.label = label;
|
|
item.initialContent = `# ${label}\n\n`;
|
|
listsStore.update((items) => [item, ...items]);
|
|
onOpenDocument(item);
|
|
}
|
|
}
|
|
}
|
|
|
|
function cancelNewItem() {
|
|
showNewItemInput = false;
|
|
newItemName = "";
|
|
createTemplateMode = false;
|
|
}
|
|
|
|
function handleNewItemKeydown(event: KeyboardEvent) {
|
|
if (event.key === "Enter") {
|
|
event.preventDefault();
|
|
confirmNewItem();
|
|
} else if (event.key === "Escape") {
|
|
cancelNewItem();
|
|
}
|
|
}
|
|
|
|
$: panelTitle = sectionTitles[activeSection] ?? "Entries";
|
|
$: todoDocuments = $todoListsStore.map(({ id, label }) => ({
|
|
id,
|
|
label,
|
|
initialContent: serializeTodoList(label, $todosStore[id] ?? []),
|
|
}));
|
|
$: items =
|
|
activeSection === "entries"
|
|
? $entriesStore
|
|
: activeSection === "todos"
|
|
? todoDocuments
|
|
: activeSection === "fragments"
|
|
? $fragmentsStore
|
|
: activeSection === "lists"
|
|
? $listsStore
|
|
: [];
|
|
$: isCalendarSection = activeSection === "calendar";
|
|
$: 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;
|
|
}
|
|
|
|
$: if (activeSection === "calendar" && !hasLoadedSavedViews) {
|
|
hasLoadedSavedViews = true;
|
|
loadSavedViews();
|
|
}
|
|
|
|
$: if (activeSection === "calendar") {
|
|
// Track store mutations without building large string signatures.
|
|
const storeInvalidationTick = [
|
|
$entriesStore,
|
|
$fragmentsStore,
|
|
$listsStore,
|
|
$todoListsStore,
|
|
$todosStore,
|
|
];
|
|
void storeInvalidationTick;
|
|
calendarDataVersion += 1;
|
|
}
|
|
|
|
$: if (activeSection === "calendar") {
|
|
const calendarTrigger = [
|
|
selectedCalendarDate?.key ?? "",
|
|
calendarYear,
|
|
calendarMonth,
|
|
calendarViewMode,
|
|
calendarSortMode,
|
|
calendarQuery,
|
|
calendarTags,
|
|
calendarTypes,
|
|
calendarStartDate,
|
|
calendarEndDate,
|
|
calendarDataVersion,
|
|
];
|
|
void calendarTrigger;
|
|
scheduleCalendarRefresh(200);
|
|
}
|
|
|
|
$: if (activeSection === "calendar" && lastActiveSection !== "calendar") {
|
|
calendarDateExplicitlySelected = false;
|
|
calendarScheduleKey = "";
|
|
void forceRefreshCalendar({ allowWhileBusy: true });
|
|
}
|
|
$: if (activeSection !== "calendar" && calendarTimelineDebounce) {
|
|
clearTimeout(calendarTimelineDebounce);
|
|
calendarTimelineDebounce = null;
|
|
}
|
|
$: lastActiveSection = activeSection;
|
|
|
|
$: if (activeSection === "calendar") {
|
|
onCalendarStateChange({
|
|
items: calendarTimelineItems,
|
|
busy: calendarBusy,
|
|
error: calendarError,
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<section class="side-panel" aria-label="Section panel">
|
|
<header class="panel-header">
|
|
<h2>{panelTitle}</h2>
|
|
<div class="panel-header-actions">
|
|
{#if activeSection === "calendar"}
|
|
<button
|
|
type="button"
|
|
class="panel-action"
|
|
aria-label="Refresh calendar"
|
|
title="Refresh calendar"
|
|
on:click={handleRefreshClick}
|
|
>
|
|
<span class="material-symbols-outlined">refresh</span>
|
|
</button>
|
|
{/if}
|
|
{#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>
|
|
<button
|
|
type="button"
|
|
class="panel-action"
|
|
aria-label="Add daily note"
|
|
title="Add daily note"
|
|
on:click={handleAddDailyNote}
|
|
>
|
|
<span class="material-symbols-outlined">calendar_month</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}
|
|
{#if showNewItemInput}
|
|
<div class="new-item-input">
|
|
<input
|
|
type="text"
|
|
bind:this={newItemInput}
|
|
bind:value={newItemName}
|
|
placeholder="Entry title..."
|
|
on:keydown={handleNewItemKeydown}
|
|
on:blur={confirmNewItem}
|
|
/>
|
|
</div>
|
|
{/if}
|
|
|
|
<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)"
|
|
/>
|
|
<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
|
|
onVisibleMonthChange={handleVisibleMonthChange}
|
|
onSelectedDateChange={handleSelectedDateChange}
|
|
onDateActivate={handleDateActivate}
|
|
/>
|
|
|
|
<div class="calendar-entries">
|
|
<h3>{calendarMonthLabel} {calendarYear} Timeline</h3>
|
|
<p class="section-copy">
|
|
Last refreshed: {calendarLastRefreshedAt || "Not yet"}
|
|
</p>
|
|
</div>
|
|
{:else}
|
|
<div class="panel-search">
|
|
<span class="material-symbols-outlined">search</span>
|
|
<input
|
|
type="text"
|
|
placeholder={`Search ${panelTitle.toLowerCase()}...`}
|
|
/>
|
|
</div>
|
|
|
|
{#if showNewItemInput}
|
|
<div class="new-item-input">
|
|
<input
|
|
type="text"
|
|
bind:this={newItemInput}
|
|
bind:value={newItemName}
|
|
placeholder={activeSection === "entries"
|
|
? createTemplateMode
|
|
? "Template name..."
|
|
: "Entry name..."
|
|
: activeSection === "todos"
|
|
? "Todo list name..."
|
|
: "List name..."}
|
|
on:keydown={handleNewItemKeydown}
|
|
on:blur={confirmNewItem}
|
|
/>
|
|
</div>
|
|
{/if}
|
|
|
|
{#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}
|
|
{/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>
|
|
|
|
<style>
|
|
.side-panel {
|
|
background: linear-gradient(
|
|
180deg,
|
|
var(--surface-2) 0%,
|
|
var(--bg-panel) 100%
|
|
);
|
|
border-right: 1px solid var(--border-soft);
|
|
padding: 16px 14px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.panel-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.panel-header-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.panel-header h2 {
|
|
font-size: 0.95rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.01em;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.panel-action {
|
|
width: 28px;
|
|
height: 28px;
|
|
display: grid;
|
|
place-items: center;
|
|
border-radius: 6px;
|
|
color: var(--text-muted);
|
|
cursor: pointer;
|
|
border: 1px solid transparent;
|
|
}
|
|
|
|
.panel-action:hover {
|
|
background: var(--bg-hover);
|
|
border-color: var(--border-soft);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.panel-action .material-symbols-outlined {
|
|
font-size: 1.05rem;
|
|
}
|
|
|
|
.panel-search {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
background: var(--surface-1);
|
|
border: 1px solid var(--border-soft);
|
|
border-radius: 8px;
|
|
padding: 7px 9px;
|
|
}
|
|
|
|
.panel-search .material-symbols-outlined {
|
|
color: var(--text-dim);
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.panel-search input {
|
|
width: 100%;
|
|
font-size: 0.84rem;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.panel-search input::placeholder {
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
.panel-list {
|
|
list-style: none;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
min-height: 0;
|
|
overflow: auto;
|
|
|
|
li {
|
|
display: flex;
|
|
align-items: center;
|
|
border-radius: 7px;
|
|
border: 1px solid transparent;
|
|
|
|
&:hover {
|
|
background: var(--bg-hover);
|
|
border-color: var(--border-soft);
|
|
}
|
|
|
|
&.is-active {
|
|
background: var(--bg-active);
|
|
border-color: var(--border-strong);
|
|
}
|
|
}
|
|
|
|
.item-label {
|
|
flex: 1;
|
|
min-width: 0;
|
|
text-align: left;
|
|
padding: 7px 9px;
|
|
font-size: 0.84rem;
|
|
color: var(--text-muted);
|
|
cursor: pointer;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
li:hover .item-label,
|
|
li.is-active .item-label {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.item-actions {
|
|
display: none;
|
|
flex-shrink: 0;
|
|
align-items: center;
|
|
gap: 2px;
|
|
padding-right: 4px;
|
|
}
|
|
|
|
li:hover .item-actions,
|
|
li.is-active .item-actions {
|
|
display: flex;
|
|
}
|
|
|
|
.item-action {
|
|
width: 24px;
|
|
height: 24px;
|
|
display: grid;
|
|
place-items: center;
|
|
border-radius: 4px;
|
|
color: var(--text-dim);
|
|
cursor: pointer;
|
|
border: 1px solid transparent;
|
|
|
|
&:hover {
|
|
background: var(--surface-2);
|
|
color: var(--text-primary);
|
|
border-color: var(--border-soft);
|
|
}
|
|
|
|
&.item-action-danger:hover {
|
|
color: #e06c75;
|
|
}
|
|
|
|
.material-symbols-outlined {
|
|
font-size: 0.85rem;
|
|
}
|
|
}
|
|
}
|
|
|
|
.calendar-entries {
|
|
margin-top: 2px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
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-color: var(--surface-2);
|
|
color: var(--text-primary);
|
|
padding: 6px 8px;
|
|
font-size: 0.78rem;
|
|
}
|
|
|
|
.calendar-control-row select {
|
|
padding-right: 30px;
|
|
}
|
|
|
|
.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 {
|
|
font-size: 0.78rem;
|
|
font-weight: 600;
|
|
color: var(--text-muted);
|
|
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;
|
|
|
|
input {
|
|
width: 100%;
|
|
font-size: 0.84rem;
|
|
color: var(--text-primary);
|
|
background: var(--surface-1);
|
|
border: 1px solid var(--border-strong);
|
|
border-radius: 7px;
|
|
padding: 7px 9px;
|
|
outline: none;
|
|
|
|
&:focus {
|
|
border-color: var(--accent, #6b8afd);
|
|
}
|
|
|
|
&::placeholder {
|
|
color: var(--text-dim);
|
|
}
|
|
}
|
|
}
|
|
|
|
@media (max-width: 980px) {
|
|
.side-panel {
|
|
padding: 12px 10px;
|
|
}
|
|
|
|
.panel-list .item-actions {
|
|
display: flex;
|
|
}
|
|
|
|
.panel-list .item-action {
|
|
width: 28px;
|
|
height: 28px;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 720px) {
|
|
.calendar-control-row {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|