Skip to main content

max / goingson

Daily notes, implicit contacts, UX polish, calendar views, search Daily notes (migration 046), recurrence chain backfill (047), implicit contacts (048), contact dashboard, email compose highlighting, calendar month/week views, command palette search, plan/review toggle, task overview, settings page, and assorted UX improvements.
Co-Authored-By
Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-07 03:25 UTC
Commit: 73d3f4aec0d2a87cc4a8b68ff80a7693a823e9d7
Parent: 9f94745
68 files changed, +4840 insertions, -466 deletions
M Cargo.lock +2 -2
@@ -1935,7 +1935,7 @@
1935 1935 "openssl",
1936 1936 "parking_lot",
1937 1937 "pter",
1938 - "rand 0.8.5",
1938 + "rand 0.9.2",
1939 1939 "reqwest 0.12.28",
1940 1940 "serde",
1941 1941 "serde_json",
@@ -5630,7 +5630,7 @@
5630 5630 "chrono",
5631 5631 "keyring",
5632 5632 "parking_lot",
5633 - "rand 0.8.5",
5633 + "rand 0.9.2",
5634 5634 "reqwest 0.12.28",
5635 5635 "serde",
5636 5636 "serde_json",
M Cargo.toml +2 -2
@@ -10,7 +10,7 @@
10 10
11 11 [workspace.package]
12 12 version = "0.3.1"
13 - edition = "2021"
13 + edition = "2024"
14 14 license-file = "LICENSE"
15 15
16 16 [workspace.dependencies]
@@ -44,7 +44,7 @@
44 44 reqwest = { version = "0.12", default-features = false, features = ["json", "native-tls"] }
45 45
46 46 # Security / OAuth
47 - rand = "0.8"
47 + rand = "0.9"
48 48 base64 = "0.22"
49 49 sha2 = "0.10"
50 50
@@ -1,7 +1,7 @@
1 1 [package]
2 2 name = "goingson-desktop"
3 3 version = "0.3.0"
4 - edition = "2021"
4 + edition = "2024"
5 5
6 6 [[bin]]
7 7 name = "goingson-desktop"
@@ -4,6 +4,39 @@
4 4
5 5 ---
6 6
7 + ## Overflow Menus + Settings Page (2026-05-06)
8 +
9 + From usability audit discoverability and learnability findings.
10 +
11 + - [x] Three-dot overflow (kebab) menus on email rows, event rows, and project cards — shared `.kebab-btn` CSS class generalizes the existing `.task-kebab-btn` pattern. Emails/events show on hover; project cards always visible. Context menu actions (Focus Mode, Track Time, email-to-task, etc.) now discoverable without right-click.
12 + - [x] Settings page replaces settings modal — `#settings-view` with sidebar navigation (6 sections: Appearance, Notifications, Planning & Review, Plugins, Cloud Sync, Import & Export). Each section renders inline including Cloud Sync (all 4 states), plugin manager, and backup settings. No more close-modal-open-modal pattern. Back button returns to previous view. `previousView` tracking in navigation.js. Mobile responsive (sidebar becomes horizontal pills). Sync indicator button navigates to settings + auto-selects Cloud Sync section.
13 +
14 + ---
15 +
16 + ## UX Quick Wins Batch (2026-05-06)
17 +
18 + From usability audit (complexity, completeness, learnability, discoverability). All frontend JS/HTML changes.
19 +
20 + - [x] Search button in header — visible entry point for Cmd+K command palette
21 + - [x] Welcome modal email button fixed — opens email accounts modal (was broken, opened Settings which has no email section)
22 + - [x] `go-welcomed` localStorage timing — only set on button click, not on modal open; re-shows if accidentally dismissed
23 + - [x] Email empty state — shows "Add Account" when no accounts exist, "Compose" when accounts exist
24 + - [x] Stray backtick removed from emails.js empty state code
25 + - [x] Time tab pill labels — Day → Day Plan, Week → Weekly Review, Month → Monthly Review (navigation uses data-subview, not text)
26 + - [x] "Add Annotation" → "Add Note" in task action modal and context menu
27 + - [x] `n` shortcut works on Contacts view (was missing from `newItemForCurrentView`)
28 + - [x] "Create Task" promoted to top-level button in email reader (was buried in Actions dropdown)
29 + - [x] Paint-to-create defaults to "Link to Task" when unscheduled tasks exist (was always "Event")
30 + - [x] App subtitle: "Project Management" → "Personal Productivity"
31 +
32 + ---
33 +
34 + ## Command Palette (2026-05-05)
35 +
36 + - [x] Global search / command palette (Cmd+K) — `search.js` IIFE module with overlay, 150ms debounced search via existing FTS5 backend, arrow key + Enter navigation, mouse hover/click, type icons (task/project/email/event/contact), filter tag display, navigates to item on selection. Escape closes. Registered in `keyboard.js` (works from any context including text inputs). Added to shortcuts help overlay.
37 +
38 + ---
39 +
7 40 ## Email Compose
8 41
9 42 - [x] HTML email body conversion to readable markdown via pter (replaces hand-rolled strip_html)
@@ -63,10 +63,16 @@
63 63 color: var(--text-muted);
64 64 }
65 65
66 - .header-input:focus {
67 - background: var(--bg-secondary);
66 + .autocomplete-wrapper {
67 + background: transparent;
68 68 border-radius: var(--radius-sm);
69 69 }
70 + .autocomplete-wrapper:focus-within {
71 + background: var(--bg-secondary);
72 + }
73 + .header-input:focus {
74 + background: transparent;
75 + }
70 76
71 77 .header-select {
72 78 flex: 1;
@@ -252,6 +258,7 @@
252 258 <div class="compose-attachments" id="attachments-bar" style="display: none;"></div>
253 259 <div class="status-bar" id="status-bar">Ready</div>
254 260
261 + <script src="js/address-highlight.js"></script>
255 262 <script>
256 263 let accounts = [];
257 264 let tauriInvoke = null;
@@ -319,6 +326,19 @@
319 326 }
320 327 }
321 328
329 + function showSaveContactPrompt(name, onSave, onDismiss) {
330 + const bar = document.createElement('div');
331 + bar.style.cssText = 'position: fixed; bottom: 2rem; left: 50%; transform: translateX(-50%); background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 8px; padding: 0.75rem 1rem; display: flex; align-items: center; gap: 0.75rem; z-index: 1000; box-shadow: 0 4px 12px rgba(0,0,0,0.15); font-size: 0.875rem;';
332 + bar.innerHTML = `
333 + <span>Save <strong>${name.replace(/</g, '&lt;')}</strong> as a contact?</span>
334 + <button style="background: var(--accent-primary); color: white; border: none; padding: 0.375rem 0.75rem; border-radius: 4px; cursor: pointer; font-size: 0.8125rem;">Save</button>
335 + <button style="background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 0.8125rem;">Dismiss</button>
336 + `;
337 + bar.querySelector('button:first-of-type').onclick = () => { bar.remove(); onSave(); };
338 + bar.querySelector('button:last-of-type').onclick = () => { bar.remove(); onDismiss(); };
339 + document.body.appendChild(bar);
340 + }
341 +
322 342 async function sendEmail() {
323 343 const accountId = document.getElementById('from-account').value;
324 344 const toAddress = document.getElementById('to-address').value.trim();
@@ -347,7 +367,7 @@
347 367 setStatus(isReply ? 'Sending reply...' : 'Sending...');
348 368
349 369 try {
350 - await invoke('send_email', {
370 + const result = await invoke('send_email', {
351 371 input: {
352 372 accountId: accountId,
353 373 toAddress: toAddress,
@@ -365,10 +385,34 @@
365 385
366 386 setStatus(isReply ? 'Reply sent!' : 'Email sent!', 'success');
367 387
368 - // Close window after short delay
369 - setTimeout(() => {
370 - window.__TAURI__.webviewWindow.getCurrentWebviewWindow().close();
371 - }, 1000);
388 + const implicitContacts = result?.newImplicitContacts || [];
389 + if (implicitContacts.length === 0) {
390 + setTimeout(() => {
391 + window.__TAURI__.webviewWindow.getCurrentWebviewWindow().close();
392 + }, 1000);
393 + } else {
394 + // Show save-contact prompts, delay close
395 + const closeWindow = () => window.__TAURI__.webviewWindow.getCurrentWebviewWindow().close();
396 + let closeTimer = setTimeout(closeWindow, 8000);
397 +
398 + for (const c of implicitContacts) {
399 + const name = c.displayName || c.display_name || '';
400 + showSaveContactPrompt(name, async () => {
401 + clearTimeout(closeTimer);
402 + try {
403 + await invoke('promote_contact', { id: c.id });
404 + setStatus(`${name} saved as contact`, 'success');
405 + setTimeout(closeWindow, 1500);
406 + } catch (_) {
407 + closeWindow();
408 + }
409 + }, () => {
410 + // Dismiss — close after short delay if no other prompts
411 + clearTimeout(closeTimer);
412 + closeTimer = setTimeout(closeWindow, 1000);
413 + });
414 + }
415 + }
372 416 } catch (err) {
373 417 document.body.classList.remove('compose-loading');
374 418 setStatus('Failed to send: ' + err, 'error');
@@ -495,9 +539,9 @@
495 539 const bodyEl = document.getElementById('body');
496 540 let body = bodyEl.value;
497 541
498 - // Remove previously appended signature
542 + // Remove previously appended signature (with -- separator)
499 543 if (currentSignature) {
500 - const sigBlock = '\n\n' + currentSignature;
544 + const sigBlock = '\n\n-- \n' + currentSignature;
501 545 if (body.endsWith(sigBlock)) {
502 546 body = body.slice(0, -sigBlock.length);
503 547 }
@@ -509,7 +553,7 @@
509 553 currentSignature = sig || '';
510 554
511 555 if (sig) {
512 - bodyEl.value = body + '\n\n' + sig;
556 + bodyEl.value = body + '\n\n-- \n' + sig;
513 557 } else {
514 558 bodyEl.value = body;
515 559 }
@@ -523,17 +567,18 @@
523 567
524 568 async function loadContactEmails() {
525 569 try {
526 - const contacts = await invoke('list_contacts');
570 + const contacts = await invoke('list_contacts_filtered', { search: null, tag: null, includeImplicit: true });
527 571 contactEmails = [];
528 572 for (const c of contacts) {
529 573 const name = c.displayName || c.display_name || '';
574 + const isImplicit = c.isImplicit || false;
530 575 if (c.emails && c.emails.length > 0) {
531 576 for (const e of c.emails) {
532 - contactEmails.push({ name, email: e.address });
577 + contactEmails.push({ name, email: e.address, isImplicit });
533 578 }
534 579 }
535 580 if (c.primaryEmail && !c.emails?.some(e => e.address === c.primaryEmail)) {
536 - contactEmails.push({ name, email: c.primaryEmail });
581 + contactEmails.push({ name, email: c.primaryEmail, isImplicit });
537 582 }
538 583 }
539 584 } catch (_) { /* contacts unavailable */ }
@@ -656,6 +701,14 @@
656 701 setupAutocomplete(document.getElementById('cc-address'));
657 702 setupAutocomplete(document.getElementById('bcc-address'));
658 703
704 + // Wire up address highlighting with ghost text
705 + if (typeof attachAddressHighlight === 'function') {
706 + const hlOpts = { invoke, contacts: () => contactEmails };
707 + attachAddressHighlight(document.getElementById('to-address'), hlOpts);
708 + attachAddressHighlight(document.getElementById('cc-address'), hlOpts);
709 + attachAddressHighlight(document.getElementById('bcc-address'), hlOpts);
710 + }
711 +
659 712 // Check for draft ID to resume editing
660 713 const params = getUrlParams();
661 714 if (params.draftId) {
@@ -680,6 +733,10 @@
680 733 replyContext.threadId = draft.threadId;
681 734 }
682 735 setStatus('Editing draft');
736 + // Trigger address highlight sync for loaded values
737 + for (const id of ['to-address', 'cc-address', 'bcc-address']) {
738 + document.getElementById(id).dispatchEvent(new Event('input'));
739 + }
683 740 }
684 741 } catch (_) { /* draft not found, start fresh */ }
685 742 }
@@ -687,6 +744,7 @@
687 744 // Apply reply context from URL params (skip if draft was loaded)
688 745 if (!currentDraftId && params.to) {
689 746 document.getElementById('to-address').value = params.to;
747 + document.getElementById('to-address').dispatchEvent(new Event('input'));
690 748 }
691 749 if (!currentDraftId && params.subject) {
692 750 document.getElementById('subject').value = params.subject;
@@ -13,7 +13,7 @@
13 13 <header class="app-header">
14 14 <div class="header-content">
15 15 <h1 class="app-title">GoingsOn</h1>
16 - <span class="app-subtitle">Project Management</span>
16 + <span class="app-subtitle">Personal Productivity</span>
17 17 <span class="mobile-view-title" id="mobile-view-title"></span>
18 18 </div>
19 19 <nav class="tab-navigation" role="tablist" aria-label="Main navigation">
@@ -31,6 +31,7 @@
31 31 <button class="sync-indicator" id="sync-indicator" onclick="GoingsOn.settings.openCloudSync()" title="Cloud Sync" aria-label="Cloud sync status" style="display:none;">
32 32 <span class="sync-dot" id="sync-dot"></span>
33 33 </button>
34 + <button class="settings-btn" onclick="GoingsOn.search.open()" title="Search (Cmd+K)" aria-label="Search">Search</button>
34 35 <button class="settings-btn" onclick="GoingsOn.settings.open()" title="Settings" aria-label="Open settings">Settings</button>
35 36 <button class="settings-btn shortcut-hint-btn" onclick="GoingsOn.keyboard.showShortcuts()" title="Keyboard shortcuts (?)" aria-label="Show keyboard shortcuts">?</button>
36 37 </div>
@@ -211,14 +212,31 @@
211 212 </div>
212 213 </div>
213 214 </div>
215 + <!-- Task Overview Sub-View -->
216 + <div id="task-overview-view" class="subview hidden">
217 + <div class="page-header">
218 + <div style="display: flex; align-items: center; gap: 1rem;">
219 + <button class="btn btn-secondary" onclick="GoingsOn.taskOverview.close()">← Back</button>
220 + <h2 class="page-title" id="task-overview-title">Task Overview</h2>
221 + </div>
222 + <div id="task-overview-actions"></div>
223 + </div>
224 + <div id="task-overview-content">
225 + <div class="skeleton-shimmer" aria-label="Loading task overview">
226 + <div class="skeleton-row"><div class="skeleton-lines"><div class="skeleton-line long"></div><div class="skeleton-line medium"></div></div></div>
227 + <div class="skeleton-row"><div class="skeleton-lines"><div class="skeleton-line medium"></div><div class="skeleton-line short"></div></div></div>
228 + </div>
229 + </div>
230 + </div>
231 +
214 232 </div><!-- /work-view -->
215 233
216 234 <!-- Time Tab Group -->
217 235 <div id="time-view" class="view tab-group hidden">
218 236 <div class="pill-nav" role="tablist" aria-label="Time views">
219 - <button class="pill active" data-subview="day-plan">Day</button>
220 - <button class="pill" data-subview="weekly-review">Week</button>
221 - <button class="pill" data-subview="monthly-review">Month</button>
237 + <button class="pill active" data-subview="day-plan">Day Plan</button>
238 + <button class="pill" data-subview="weekly-review">Weekly Review</button>
239 + <button class="pill" data-subview="monthly-review">Monthly Review</button>
222 240 <button class="pill" data-subview="events">Events</button>
223 241 <button class="pill" data-subview="timer">Timer</button>
224 242 </div>
@@ -233,11 +251,13 @@
233 251 <input type="date" id="day-plan-date" class="day-plan-date-picker" onchange="GoingsOn.dayPlan.onDatePickerChange()">
234 252 <span id="day-plan-date-display" class="day-plan-date-display"></span>
235 253 </div>
236 - <button class="btn btn-primary" onclick="GoingsOn.dayPlan.openDailyReview()" title="Review your day">
237 - Review Day
238 - </button>
254 + <div class="view-toggle" id="day-plan-review-toggle" data-plan-pane="day-plan-pane" data-review-pane="day-review-pane">
255 + <button class="view-toggle-btn active" data-mode="plan" onclick="GoingsOn.planReviewToggle.setMode('day', 'plan')">Plan</button>
256 + <button class="view-toggle-btn" data-mode="review" onclick="GoingsOn.planReviewToggle.setMode('day', 'review')">Review</button>
257 + </div>
239 258 </div>
240 - <div class="day-plan-content">
259 + <!-- Plan pane -->
260 + <div id="day-plan-pane" class="day-plan-content">
241 261 <div class="day-plan-main" id="day-plan-container">
242 262 <div class="timeline-container" id="timeline-container">
243 263 <p class="timeline-hint" style="text-align: center; color: var(--text-secondary); font-size: 0.8rem; margin: 0.25rem 0 0;">Drag across time slots to block time</p>
@@ -262,12 +282,22 @@
262 282 </div>
263 283 </div>
264 284 </div>
285 + <!-- Review pane -->
286 + <div id="day-review-pane" class="hidden">
287 + <div id="day-review-content" class="day-review-inline">
288 + <p style="color: var(--text-secondary);">Loading...</p>
289 + </div>
290 + </div>
265 291 </div>
266 292
267 293 <!-- Weekly Review Sub-View -->
268 294 <div id="weekly-review-view" class="subview hidden" role="tabpanel" aria-labelledby="weekly-review-tab">
269 295 <div class="page-header">
270 296 <h2 class="page-title">Weekly Review</h2>
297 + <div class="view-toggle" id="week-plan-review-toggle" data-plan-pane="week-plan-pane" data-review-pane="week-review-pane">
298 + <button class="view-toggle-btn active" data-mode="plan" onclick="GoingsOn.planReviewToggle.setMode('week', 'plan')">Plan</button>
299 + <button class="view-toggle-btn" data-mode="review" onclick="GoingsOn.planReviewToggle.setMode('week', 'review')">Review</button>
300 + </div>
271 301 </div>
272 302 <div id="weekly-review-content" class="weekly-review-content">
273 303 <div class="skeleton-shimmer" aria-label="Loading weekly review">
@@ -287,6 +317,10 @@
287 317 <button class="btn btn-secondary" onclick="GoingsOn.monthlyReview.nextMonth()" title="Next month">&rarr;</button>
288 318 <span id="monthly-review-month-display" class="monthly-review-month-display"></span>
289 319 </div>
320 + <div class="view-toggle" id="month-plan-review-toggle" data-plan-pane="month-plan-pane" data-review-pane="month-review-pane">
321 + <button class="view-toggle-btn active" data-mode="plan" onclick="GoingsOn.planReviewToggle.setMode('month', 'plan')">Plan</button>
322 + <button class="view-toggle-btn" data-mode="review" onclick="GoingsOn.planReviewToggle.setMode('month', 'review')">Review</button>
323 + </div>
290 324 </div>
291 325 <div id="monthly-review-content" class="monthly-review-content">
292 326 <div class="skeleton-shimmer" aria-label="Loading monthly review">
@@ -310,7 +344,20 @@
310 344 <div id="events-view" class="subview hidden" role="tabpanel" aria-labelledby="events-tab">
311 345 <div class="page-header">
312 346 <h2 class="page-title">Events</h2>
313 - <button class="btn btn-primary" onclick="GoingsOn.events.openNew()" title="New event (n)">+ New Event</button>
347 + <div style="display: flex; align-items: center; gap: 1rem;">
348 + <div class="view-toggle" id="events-view-toggle">
349 + <button class="view-toggle-btn active" data-mode="list" onclick="GoingsOn.events.setViewMode('list')">List</button>
350 + <button class="view-toggle-btn" data-mode="month" onclick="GoingsOn.events.setViewMode('month')">Month</button>
351 + <button class="view-toggle-btn" data-mode="week" onclick="GoingsOn.events.setViewMode('week')">Week</button>
352 + </div>
353 + <button class="btn btn-primary" onclick="GoingsOn.events.openNew()" title="New event (n)">+ New Event</button>
354 + </div>
355 + </div>
356 + <div id="events-bulk-bar" class="bulk-actions-bar hidden">
357 + <span class="bulk-count">0 selected</span>
358 + <button class="btn btn-sm" onclick="GoingsOn.events.selectAllEvents()">Select All</button>
359 + <button class="btn btn-sm" onclick="GoingsOn.events.bulkDelete()">Delete</button>
360 + <button class="btn btn-sm" onclick="GoingsOn.events.clearEventSelection()">Cancel</button>
314 361 </div>
315 362 <div class="events-list" id="events-list">
316 363 <div class="event-table-virtual" id="event-table" role="grid" aria-label="Events list">
@@ -319,6 +366,7 @@
319 366 <div class="event-cell" role="columnheader">Time</div>
320 367 <div class="event-cell" role="columnheader">Event</div>
321 368 <div class="event-cell" role="columnheader">Location</div>
369 + <div class="event-cell" role="columnheader"></div>
322 370 </div>
323 371 <div class="event-list-container" id="event-list-container" aria-live="polite">
324 372 <div class="skeleton-shimmer" aria-label="Loading events">
@@ -345,6 +393,27 @@
345 393 </div>
346 394 </details>
347 395 </div>
396 + <!-- Month Calendar View -->
397 + <div id="events-month-view" class="events-calendar-container hidden">
398 + <div class="calendar-nav">
399 + <button class="btn btn-secondary" onclick="GoingsOn.eventsCalendar.prevMonth()">&larr;</button>
400 + <button class="btn btn-secondary" onclick="GoingsOn.eventsCalendar.goToThisMonth()">This Month</button>
401 + <button class="btn btn-secondary" onclick="GoingsOn.eventsCalendar.nextMonth()">&rarr;</button>
402 + <span class="calendar-nav-label" id="month-calendar-label"></span>
403 + </div>
404 + <div id="month-calendar-grid"></div>
405 + <div id="month-day-detail" class="month-day-detail hidden"></div>
406 + </div>
407 + <!-- Week Calendar View -->
408 + <div id="events-week-view" class="events-calendar-container hidden">
409 + <div class="calendar-nav">
410 + <button class="btn btn-secondary" onclick="GoingsOn.eventsCalendar.prevWeek()">&larr;</button>
411 + <button class="btn btn-secondary" onclick="GoingsOn.eventsCalendar.goToThisWeek()">This Week</button>
412 + <button class="btn btn-secondary" onclick="GoingsOn.eventsCalendar.nextWeek()">&rarr;</button>
413 + <span class="calendar-nav-label" id="week-calendar-label"></span>
414 + </div>
415 + <div id="week-calendar-grid"></div>
416 + </div>
348 417 </div>
349 418 </div><!-- /time-view -->
350 419
@@ -413,6 +482,13 @@
413 482 <button class="btn btn-primary" onclick="GoingsOn.contacts.openNew()" title="New contact (n)">+ New Contact</button>
414 483 </div>
415 484 </div>
485 + <div id="contacts-bulk-bar" class="bulk-actions-bar hidden">
486 + <span class="bulk-count">0 selected</span>
487 + <button class="btn btn-sm" onclick="GoingsOn.contacts.selectAll()">Select All</button>
488 + <button class="btn btn-sm" onclick="GoingsOn.contacts.bulkTag()">Tag</button>
489 + <button class="btn btn-sm" onclick="GoingsOn.contacts.bulkDelete()">Delete</button>
490 + <button class="btn btn-sm" onclick="GoingsOn.contacts.clearSelection()">Cancel</button>
491 + </div>
416 492 <div class="cards-grid" id="contacts-grid">
417 493 <div class="skeleton-shimmer" aria-label="Loading contacts">
418 494 <div class="skeleton-row"><div class="skeleton-avatar"></div><div class="skeleton-lines"><div class="skeleton-line long"></div><div class="skeleton-line short"></div></div></div>
@@ -421,7 +497,45 @@
421 497 </div>
422 498 </div>
423 499 </div>
500 + <!-- Contact Dashboard Sub-View -->
501 + <div id="contact-dashboard-view" class="subview hidden">
502 + <div class="page-header">
503 + <div style="display: flex; align-items: center; gap: 1rem;">
504 + <button class="btn btn-secondary" onclick="GoingsOn.contactDashboard.close()">&larr; Back</button>
505 + <h2 class="page-title" id="contact-dashboard-title">Contact</h2>
506 + </div>
507 + <div id="contact-dashboard-actions"></div>
508 + </div>
509 + <div id="contact-dashboard-content">
510 + <div class="skeleton-shimmer" aria-label="Loading contact">
511 + <div class="skeleton-row"><div class="skeleton-avatar"></div><div class="skeleton-lines"><div class="skeleton-line long"></div><div class="skeleton-line medium"></div></div></div>
512 + <div class="skeleton-row"><div class="skeleton-lines"><div class="skeleton-line long"></div><div class="skeleton-line short"></div></div></div>
513 + </div>
514 + </div>
515 + </div>
516 +
424 517 </div><!-- /messages-view -->
518 +
519 + <!-- Settings View (standalone, not a tab group) -->
520 + <div id="settings-view" class="view hidden">
521 + <div class="settings-page-layout">
522 + <nav class="settings-sidebar">
523 + <button class="settings-back-btn" onclick="GoingsOn.settings.goBack()" aria-label="Go back">&larr; Back</button>
524 + <div class="settings-nav-items">
525 + <button class="settings-nav-item active" data-section="appearance" onclick="GoingsOn.settings.showSection('appearance')">Appearance</button>
526 + <button class="settings-nav-item" data-section="notifications" onclick="GoingsOn.settings.showSection('notifications')">Notifications</button>
527 + <button class="settings-nav-item" data-section="planning" onclick="GoingsOn.settings.showSection('planning')">Planning &amp; Review</button>
528 + <button class="settings-nav-item" data-section="plugins" onclick="GoingsOn.settings.showSection('plugins')">Plugins</button>
529 + <button class="settings-nav-item" data-section="sync" onclick="GoingsOn.settings.showSection('sync')">Cloud Sync</button>
530 + <button class="settings-nav-item" data-section="data" onclick="GoingsOn.settings.showSection('data')">Import &amp; Export</button>
531 + </div>
532 + </nav>
533 + <div class="settings-content" id="settings-content">
534 + <!-- Populated by JS -->
535 + </div>
536 + </div>
537 + </div><!-- /settings-view -->
538 +
425 539 </main>
426 540 </div><!-- /app-body -->
427 541
@@ -519,11 +633,16 @@
519 633 <script src="js/task-board.js"></script>
520 634 <script src="js/attachments.js"></script>
521 635 <script src="js/autocomplete.js"></script>
636 + <script src="js/address-highlight.js"></script>
522 637 <script src="js/tasks.js"></script>
638 + <script src="js/task-overview.js"></script>
523 639 <script src="js/events.js"></script>
640 + <script src="js/events-calendar.js"></script>
524 641 <script src="js/emails.js"></script>
525 642 <script src="js/email-accounts.js"></script>
526 643 <script src="js/contacts.js"></script>
644 + <script src="js/contact-dashboard.js"></script>
645 + <script src="js/plan-review-toggle.js"></script>
527 646 <script src="js/day-planning.js"></script>
528 647 <script src="js/weekly-review.js"></script>
529 648 <script src="js/monthly-review.js"></script>
@@ -534,6 +653,7 @@
534 653
535 654 <!-- Features (with domain dependencies) -->
536 655 <script src="js/bulk-actions.js"></script>
656 + <script src="js/search.js"></script>
537 657 <script src="js/keyboard.js"></script>
538 658
539 659 <!-- UI -->