From 1d5e6d4c59918247de0767956b1dd9207cff2f51 Mon Sep 17 00:00:00 2001 From: Jake Date: Fri, 15 May 2026 19:03:44 +0100 Subject: [PATCH] v0.3 Phase 5f: History page migrated to quietware skeleton MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Calm local archive instead of SaaS table. Same gated-layout pattern as Phases 5c/5d. v0.2 surface unchanged; quietware path renders HistoryPage via LumotiaPageSkeleton. Zones. Header "History" title + "Search and reopen saved transcripts." subtext + Live/Trash tab toggle with live counts in mono. Primary surface search input + scrollable list of items, or empty-state typography when nothing saved. Mono footer N saved · N in trash · Local only. Action dock omitted — actions live per-row (Open / Copy / Delete) so the dock doesn't add noise when the user is browsing a list of items each with their own context. State + handlers preserved. history (store), trashItems, viewMode, searchQuery, filtered (derived), trashError, trashLoading, restoringId, compactPreviewText, copyItem, removeItem, openViewer, restoreFromTrash, formatTimestamp. Bulk actions, audio playback, tag chips, ClearAll modal and Phase 9 multi-select toolbar all stay accessible via the v0.2 fallback path when quietware is off. The quietware view focuses on the core archive flow. Per-item row. Mic icon if source=dictation, FileText icon if source=file. Title (or first 80 chars of preview if untitled). Two-line preview clamp. Mono timestamp via formatTimestamp. Open / Copy / Delete actions in tertiary buttons on the right. Empty states. Live mode, no items: FileText icon + Young Serif italic "Nothing saved yet." + Work Sans support. Live mode, search returns nothing: same icon + "Nothing matched." + "Try different keywords or clear the search." Trash mode, no items: Clock icon + "Trash is empty." + 30-day retention copy. Search input. Lives at the top of the primary surface. Standard search input with focus ring picking up --button-primary-bg. Capture script. scripts/capture-quietware-screenshots.mjs now navigates to the History page via the sidebar nav button after Files, and screenshots both quietware modes. Verified. - npm run check: 0 errors, 0 warnings across 5707 files. - Captures confirm both empty and populated states render correctly (populated state captured with temporary mock data in the history store, reverted before this commit). Phase 5f is complete. Phase 5e (Tasks) is the last layout migration before Phase 6 / 7. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/capture-quietware-screenshots.mjs | 20 +++ src/lib/pages/HistoryPage.svelte | 191 +++++++++++++++++++++- 2 files changed, 209 insertions(+), 2 deletions(-) diff --git a/scripts/capture-quietware-screenshots.mjs b/scripts/capture-quietware-screenshots.mjs index 535ef72..2ebbc0d 100644 --- a/scripts/capture-quietware-screenshots.mjs +++ b/scripts/capture-quietware-screenshots.mjs @@ -103,5 +103,25 @@ try { console.error(`files page skipped: ${e.message}`); } +// History page — SPA navigation via the sidebar item. +try { + console.log("\n=== history page (best effort) ==="); + await page.evaluate(() => { + const items = Array.from(document.querySelectorAll('button, a, [role="button"], li')); + const target = items.find(el => (el.textContent || "").trim().toLowerCase() === "history"); + if (target) target.click(); + }); + await page.waitForTimeout(1500); + for (const mode of MODES.slice(1, 3)) { + await setMode(mode); + await page.waitForTimeout(400); + const file = `${OUT}/history__${mode.id}.png`; + await page.screenshot({ path: file, fullPage: false }); + console.log(` history ${mode.id}`); + } +} catch (e) { + console.error(`history page skipped: ${e.message}`); +} + await browser.close(); console.log(`\nWrote screenshots to ${OUT}/`); diff --git a/src/lib/pages/HistoryPage.svelte b/src/lib/pages/HistoryPage.svelte index f6eb36f..1d65f58 100644 --- a/src/lib/pages/HistoryPage.svelte +++ b/src/lib/pages/HistoryPage.svelte @@ -25,7 +25,11 @@ import LumotiaCard from "$lib/ui/LumotiaCard.svelte"; import LumotiaEmptyState from "$lib/ui/LumotiaEmptyState.svelte"; import LumotiaButton from "$lib/ui/LumotiaButton.svelte"; - import { formatTime, formatDuration } from "$lib/utils/time.js"; + import LumotiaNotice from "$lib/ui/LumotiaNotice.svelte"; + // v0.3 Phase 5f — quietware History layout. + import LumotiaPageSkeleton from "$lib/ui/LumotiaPageSkeleton.svelte"; + import { onMount } from "svelte"; + import { formatTime, formatDuration, formatTimestamp } from "$lib/utils/time.js"; import { PLAYBACK_SPEEDS } from "$lib/utils/constants.js"; import { Search, Clock, Play, Pause, FileText, Mic, ChevronDown, ExternalLink, Star, Tag } from 'lucide-svelte'; @@ -203,7 +207,27 @@ : "" ); + // v0.3 Phase 5f — quietware layout switch. Same observer pattern + // as Dictation / Files. v0.2 surface unchanged without quietware. + let isQuietware = $state(false); + let quietwareObserver: MutationObserver | null = null; + + onMount(() => { + if (typeof document === "undefined") return; + const sync = () => { + isQuietware = document.documentElement.dataset.design === "quietware"; + }; + sync(); + quietwareObserver = new MutationObserver(sync); + quietwareObserver.observe(document.documentElement, { + attributes: true, + attributeFilter: ["data-design"], + }); + }); + onDestroy(() => { + quietwareObserver?.disconnect(); + quietwareObserver = null; stopPlayback(); if (bulkDeleteArmTimer) clearTimeout(bulkDeleteArmTimer); }); @@ -683,7 +707,7 @@ }); -
+
@@ -693,6 +717,168 @@ aria-atomic="true" >{confirmAnnouncement}
+ {#if isQuietware} + + + {#snippet header()} +
+
+

History

+

+ {viewMode === "trash" ? "Recover transcripts within 30 days." : "Search and reopen saved transcripts."} +

+
+
+ + +
+
+ {/snippet} + + {#snippet primary()} +
+ {#if trashError && viewMode === "trash"} +
+ trashError = ""}> + {trashError} + +
+ {/if} + + +
+
+
+
+ + +
+ {#if viewMode === "live"} + {#if filtered.length === 0} +
+
+ {:else} +
    + {#each filtered as item (item.id)} +
  • +
    + {#if item.source === "file"} +
    +
    +

    + {item.title || item.preview?.slice(0, 80) || "Untitled"} +

    +

    {compactPreviewText(item)}

    +

    {formatTimestamp(item.savedAt || item.timestamp)}

    +
    +
    + openViewer(item)}>Open + copyItem(item)}>Copy + removeItem(item)}>Delete +
    +
  • + {/each} +
+ {/if} + {:else} + + {#if trashLoading && trashItems.length === 0} +
Loading trash…
+ {:else if trashItems.length === 0} +
+
+ {:else} +
    + {#each trashItems as item (item.id)} +
  • +
    +
    +
    +

    + {item.title || item.preview?.slice(0, 80) || "Untitled"} +

    +

    {item.preview || ""}

    +

    Deleted {formatTrashTimestamp(item.deleted_at)}

    +
    +
    + restoreFromTrash(item.id)} + >{restoringId === item.id ? "Restoring…" : "Restore"} +
    +
  • + {/each} +
+ {/if} + {/if} +
+
+ {/snippet} + + {#snippet metadata()} +
+ {history.length} saved · {trashItems.length} in trash · Local only +
+ {/snippet} +
+ {:else}

History

@@ -1223,4 +1409,5 @@
{/if} + {/if}