/** * Landing page entry point — wires together state, DOM refs, and * the extracted modules (render, events, create, import, toast). */ import type { DocumentMeta, Folder, FolderAssignments, StarMap } from './landing-types.js'; import { syncKeys, getLocalKeys } from './lib/key-sync.js'; import { ensureWrappingKey } from './lib/key-passphrase.js'; import { createCommandPalette, type PaletteAction } from './command-palette.js'; import { BUILT_IN_TEMPLATES } from './templates.js'; import { DEFAULT_SORT } from './landing-utils.js'; import { showToast } from './landing-toast.js'; import { renderDocuments as doRender, docPath } from './landing-render.js'; import type { RenderDeps } from './landing-render.js'; import { setupDelegatedListeners, setupOneOffListeners, initUsername } from './landing-events.js'; import type { EventDeps } from './landing-events.js'; import { createDocument, createFromTemplate, openDailyNote, openCalendar } from './landing-create.js'; import type { CreateDeps } from './landing-create.js'; import { setupDragAndDrop } from './landing-import.js'; import { listDocuments, listTrashedDocuments } from './lib/local-store.js'; import { mountOfflineIndicator } from './lib/offline-indicator.js'; import { initAuth } from './lib/auth.js'; // --- DOM refs --- const docListEl = document.getElementById('doc-list') as HTMLElement; const folderListEl = document.getElementById('folder-list') as HTMLElement; const noResultsEl = document.getElementById('no-results') as HTMLElement; const newBtn = document.getElementById('new-btn') as HTMLElement; const newDropdown = document.getElementById('new-dropdown') as HTMLElement; const newMenu = document.getElementById('new-menu') as HTMLElement; const dailyNoteBtn = document.getElementById('daily-note') as HTMLElement; const moreBtn = document.getElementById('more-btn') as HTMLElement; const moreMenu = document.getElementById('more-menu') as HTMLElement; const moreMenuPanel = document.getElementById('more-menu-panel') as HTMLElement; const searchInput = document.getElementById('search-input') as HTMLInputElement; const searchClear = document.getElementById('search-clear') as HTMLElement; const sortBtn = document.getElementById('sort-btn') as HTMLElement; const sortLabel = document.getElementById('sort-label') as HTMLElement; const sortMenu = document.getElementById('sort-menu') as HTMLElement; const newFolderBtn = document.getElementById('new-folder-btn') as HTMLElement; const backupExportBtn = document.getElementById('backup-export-btn') as HTMLElement; const backupImportBtn = document.getElementById('backup-import-btn') as HTMLElement; const backupImportInput = document.getElementById('backup-import-input') as HTMLInputElement; const viewToggleBtn = document.getElementById('view-toggle') as HTMLElement; const breadcrumbsEl = document.getElementById('breadcrumbs') as HTMLElement; const trashSection = document.getElementById('trash-section') as HTMLElement; const trashToggle = document.getElementById('trash-toggle') as HTMLElement; const trashCount = document.getElementById('trash-count') as HTMLElement; const trashListEl = document.getElementById('trash-list') as HTMLElement; // Modals const usernameModal = document.getElementById('username-modal') as HTMLElement; const usernameInput = document.getElementById('username-input') as HTMLInputElement; const usernameConfirm = document.getElementById('username-confirm') as HTMLElement; const folderModal = document.getElementById('folder-modal') as HTMLElement; const folderModalTitle = document.getElementById('folder-modal-title') as HTMLElement; const folderNameInput = document.getElementById('folder-name-input') as HTMLInputElement; const folderCancel = document.getElementById('folder-cancel') as HTMLElement; const folderConfirmBtn = document.getElementById('folder-confirm') as HTMLElement; const moveModal = document.getElementById('move-modal') as HTMLElement; const moveFolderList = document.getElementById('move-folder-list') as HTMLElement; const moveCancel = document.getElementById('move-cancel') as HTMLElement; const userBadge = document.getElementById('user-badge') as HTMLElement; // --- State --- let allDocs: DocumentMeta[] = []; let trashedDocs: DocumentMeta[] = []; let currentSort: string = localStorage.getItem('atmos-sort') || DEFAULT_SORT; let stars: StarMap = JSON.parse(localStorage.getItem('atmos-stars') || '{}'); let folders: Folder[] = JSON.parse(localStorage.getItem('atmos-folders') || '[]'); let folderAssignments: FolderAssignments = JSON.parse(localStorage.getItem('atmos-folder-assignments') || '{}'); let currentFolderId: string | null = null; let recentIds: string[] = JSON.parse(localStorage.getItem('atmos-recent') || '[]'); let searchQuery = ''; let activeTagFilter: string | null = null; let activeTypeFilter: string | null = null; let trashExpanded = false; let viewMode: 'list' | 'grid' = (localStorage.getItem('atmos-view-mode') as 'list' | 'grid') || 'list'; let moveDocId: string | null = null; // --- Build shared deps objects --- const renderDeps: RenderDeps = { docListEl, folderListEl, noResultsEl, breadcrumbsEl, trashSection, trashCount, trashListEl, viewToggleBtn, moveFolderList, moveModal, getAllDocs: () => allDocs, getTrashedDocs: () => trashedDocs, getStars: () => stars, getFolders: () => folders, getFolderAssignments: () => folderAssignments, getCurrentFolderId: () => currentFolderId, getSearchQuery: () => searchQuery, getActiveTagFilter: () => activeTagFilter, getActiveTypeFilter: () => activeTypeFilter, getCurrentSort: () => currentSort, getViewMode: () => viewMode, getTrashExpanded: () => trashExpanded, getRecentIds: () => recentIds, setMoveDocId: (id) => { moveDocId = id; }, }; function renderDocuments() { doRender(renderDeps); } const eventDeps: EventDeps = { // DOM elements docListEl, folderListEl, trashListEl, breadcrumbsEl, moveFolderList, moveModal, searchInput, searchClear, sortBtn, sortLabel, sortMenu, newFolderBtn, viewToggleBtn, trashToggle, usernameModal, usernameInput, usernameConfirm, folderModal, folderModalTitle, folderNameInput, folderCancel, folderConfirm: folderConfirmBtn, moveCancel, userBadge, backupExportBtn, backupImportBtn, backupImportInput, // State accessors getAllDocs: () => allDocs, setAllDocs: (d) => { allDocs = d; }, getTrashedDocs: () => trashedDocs, setTrashedDocs: (d) => { trashedDocs = d; }, getStars: () => stars, setStars: (s) => { stars = s; }, getFolders: () => folders, setFolders: (f) => { folders = f; }, getFolderAssignments: () => folderAssignments, setFolderAssignments: (a) => { folderAssignments = a; }, getCurrentFolderId: () => currentFolderId, setCurrentFolderId: (id) => { currentFolderId = id; }, getRecentIds: () => recentIds, setRecentIds: (ids) => { recentIds = ids; }, getSearchQuery: () => searchQuery, setSearchQuery: (q) => { searchQuery = q; }, getActiveTagFilter: () => activeTagFilter, setActiveTagFilter: (t) => { activeTagFilter = t; }, getActiveTypeFilter: () => activeTypeFilter, setActiveTypeFilter: (t: string | null) => { activeTypeFilter = t; }, getTrashExpanded: () => trashExpanded, setTrashExpanded: (v) => { trashExpanded = v; }, getViewMode: () => viewMode, setViewMode: (m) => { viewMode = m; }, getCurrentSort: () => currentSort, setCurrentSort: (s) => { currentSort = s; }, getMoveDocId: () => moveDocId, setMoveDocId: (id) => { moveDocId = id; }, // Callbacks renderDocuments, loadDocuments, getRenderDeps: () => renderDeps, }; const createDeps: CreateDeps = { getCurrentFolderId: () => currentFolderId, getFolderAssignments: () => folderAssignments, setFolderAssignments: (a) => { folderAssignments = a; }, getRecentIds: () => recentIds, setRecentIds: (ids) => { recentIds = ids; }, getAllDocs: () => allDocs, }; // --- Wire up event listeners --- setupDelegatedListeners(eventDeps); setupOneOffListeners(eventDeps); // --- New document menu --- function handleNewAction(kind: string): void { switch (kind) { case 'doc': case 'sheet': case 'form': case 'diagram': createDocument(createDeps, kind); break; case 'calendar': openCalendar(createDeps); break; } } function closeNewMenu(): void { newDropdown.classList.remove('open'); newBtn.setAttribute('aria-expanded', 'false'); } function toggleNewMenu(): void { const open = newDropdown.classList.toggle('open'); newBtn.setAttribute('aria-expanded', open ? 'true' : 'false'); if (open) closeMoreMenu(); } newBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); toggleNewMenu(); }); newMenu.addEventListener('click', (e) => { const target = (e.target as HTMLElement).closest('.new-menu-item') as HTMLElement | null; if (!target) return; if ((target as HTMLButtonElement).disabled) return; const kind = target.dataset.new; if (!kind) return; closeNewMenu(); handleNewAction(kind); }); dailyNoteBtn.addEventListener('click', (e) => { e.preventDefault(); openDailyNote(createDeps); }); // --- More menu (folder / import / backup) --- function closeMoreMenu(): void { moreMenu.classList.remove('open'); moreBtn.setAttribute('aria-expanded', 'false'); } function toggleMoreMenu(): void { const open = moreMenu.classList.toggle('open'); moreBtn.setAttribute('aria-expanded', open ? 'true' : 'false'); if (open) closeNewMenu(); } moreBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); toggleMoreMenu(); }); // Clicking any item inside the more menu closes it afterward. // The item's own handler (wired in landing-events.ts) still runs. moreMenuPanel.addEventListener('click', (e) => { const target = (e.target as HTMLElement).closest('.more-menu-item'); if (target) closeMoreMenu(); }); // Dismiss menus on outside click / Escape document.addEventListener('click', (e) => { const t = e.target as HTMLElement; if (!newDropdown.contains(t)) closeNewMenu(); if (!moreMenu.contains(t)) closeMoreMenu(); }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { closeNewMenu(); closeMoreMenu(); } }); // --- Drag-and-drop file import --- setupDragAndDrop({ getCurrentFolderId: () => currentFolderId, getFolderAssignments: () => folderAssignments, setFolderAssignments: (a) => { folderAssignments = a; }, }); // --- Attach key strings to documents for URL navigation --- async function attachKeyStrings(docs: DocumentMeta[]): Promise { const keys = await getLocalKeys(); for (const doc of docs) { doc._keyStr = keys[doc.id]; } } // --- Load & render --- async function loadDocuments() { try { const [active, trashed] = await Promise.all([ listDocuments(), listTrashedDocuments(), ]); const docs: DocumentMeta[] = active.map(d => ({ id: d.id, type: d.type, name: d.name, deleted_at: d.deleted_at, tags: d.tags, created_at: d.created_at, updated_at: d.updated_at, })); const trash: DocumentMeta[] = trashed.map(d => ({ id: d.id, type: d.type, name: d.name, deleted_at: d.deleted_at, tags: d.tags, created_at: d.created_at, updated_at: d.updated_at, })); await Promise.all([attachKeyStrings(docs), attachKeyStrings(trash)]); allDocs = docs; trashedDocs = trash; renderDocuments(); } catch (err) { console.error('Failed to load documents:', err); showToast('Failed to load documents — try refreshing', 5000, true); } } // Mount offline indicator badge (fixed position, hidden until offline) mountOfflineIndicator(); // --- Command Palette --- createCommandPalette({ actions: [ { id: 'new-doc', label: 'New Document', category: 'action', icon: '\u270e', action: () => createDocument(createDeps, 'doc') }, { id: 'new-sheet', label: 'New Spreadsheet', category: 'action', icon: '\u25a6', action: () => createDocument(createDeps, 'sheet') }, { id: 'new-form', label: 'New Form', category: 'action', icon: '\u2637', action: () => createDocument(createDeps, 'form') }, { id: 'new-diagram', label: 'New Diagram', category: 'action', icon: '\u25d3', action: () => createDocument(createDeps, 'diagram') }, { id: 'new-calendar', label: 'Calendar', category: 'action', icon: '\u2630', action: () => openCalendar(createDeps) }, { id: 'daily-note', label: "Today's Note", category: 'action', icon: '\u2666', action: () => openDailyNote(createDeps) }, { id: 'backup-export', label: 'Export Backup', category: 'action', icon: '\u2913', action: () => backupExportBtn.click() }, { id: 'backup-import', label: 'Restore Backup', category: 'action', icon: '\u2912', action: () => backupImportBtn.click() }, ...BUILT_IN_TEMPLATES.map(t => ({ id: `template-${t.id}`, label: `Template: ${t.name}`, category: 'action' as const, icon: t.icon, action: () => createFromTemplate(createDeps, t.id), })), ], fetchDocuments: async (): Promise => { return allDocs .filter(doc => doc._keyStr) .map(doc => { const path = docPath(doc.type); return { id: `doc-${doc.id}`, label: doc.name || 'Untitled', category: 'document' as const, icon: { doc: '\u270e', sheet: '\u25a6', form: '\u2637', slide: '\u25eb', diagram: '\u25d3', calendar: '\u2630' }[doc.type] || '\u25a6', action: () => { window.location.href = `${path}/${doc.id}#${doc._keyStr}`; }, }; }); }, }); // --- Instance info panel --- document.getElementById('instance-info-btn')?.addEventListener('click', async () => { const { getInstanceInfo, describeInstance } = await import('./lib/instance-info.js'); const info = await getInstanceInfo(); const desc = describeInstance(info); const { showInstanceInfoModal } = await import('./lib/instance-info-modal.js'); showInstanceInfoModal(desc, info); }); // --- Init --- initUsername(eventDeps); ensureWrappingKey() .then(() => syncKeys()) .then(async () => { await initAuth(); const { ensurePdsIdentity } = await import('./lib/pds-setup.js'); const ready = await ensurePdsIdentity(); if (ready) { const storageText = document.getElementById('storage-status-text'); if (storageText) storageText.textContent = 'Documents are encrypted and synced to your PDS.'; const { pushLocalDocuments } = await import('./lib/pds-push-sync.js'); await pushLocalDocuments(); const { pullRemoteDocuments } = await import('./lib/pds-pull-sync.js'); await pullRemoteDocuments(); } }) .then(() => loadDocuments()); // --- Handle PWA shortcut actions (?action=new-doc, etc.) --- const urlAction = new URLSearchParams(window.location.search).get('action'); if (urlAction) { const actionMap: Record void> = { 'new-doc': () => createDocument(createDeps, 'doc'), 'new-sheet': () => createDocument(createDeps, 'sheet'), 'new-form': () => createDocument(createDeps, 'form'), 'new-diagram': () => createDocument(createDeps, 'diagram'), 'new-calendar': () => openCalendar(createDeps), }; const handler = actionMap[urlAction]; if (handler) { history.replaceState(null, '', '/'); handler(); } }