From c570b0bf37d5b7cad25a9e16651d1e5b6bba8a21 Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Tue, 9 Jun 2026 13:14:26 -0500 Subject: [PATCH] feat: search UI --- CHANGELOG.md | 7 +- src/lib/atproto/repo.svelte.ts | 35 +++++++ src/lib/components/BootSplash.svelte | 91 ++++------------- src/lib/components/CollectionBrowser.svelte | 105 +++++++++++++++++++- src/routes/+layout.svelte | 10 ++ 5 files changed, 168 insertions(+), 80 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12b324a..34f47b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,5 @@ # Changelog -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -and this project targets [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - ## [Unreleased] Target release: `v1.0.0`. @@ -20,7 +15,7 @@ Target release: `v1.0.0`. recovery actions. - Cache-aware AT Protocol browsing: live record fetches populate PGlite and offline failures can fall back to cached collections/records. -- Shared `errorMessage()` utility for consistent unknown-error messages. +- Full-text cache search in the Nautilus-style collection browser. ### 2026-06-08 diff --git a/src/lib/atproto/repo.svelte.ts b/src/lib/atproto/repo.svelte.ts index 683593c..625e281 100644 --- a/src/lib/atproto/repo.svelte.ts +++ b/src/lib/atproto/repo.svelte.ts @@ -6,6 +6,7 @@ import { getDatabase, listCachedCollections, listCachedRecords, + searchCachedRecords, updateCollectionSyncState, type CachedRecord, type CachedRecordInput @@ -27,6 +28,8 @@ class RepoBrowserState { records = $state([]); isLoadingCollections = $state(false); isLoadingRecords = $state(false); + isSearching = $state(false); + searchQuery = $state(''); error = $state(null); loadedDid = $state(null); selectedRecord = $state(null); @@ -71,6 +74,7 @@ class RepoBrowserState { async selectCollection(identity: AccountIdentity, collectionName: string) { this.selectedCollection = collectionName; + this.searchQuery = ''; this.isLoadingRecords = true; this.error = null; @@ -127,6 +131,35 @@ class RepoBrowserState { } } + async searchRecords(identity: AccountIdentity, query: string) { + const normalizedQuery = query.trim(); + this.searchQuery = normalizedQuery; + + if (!normalizedQuery) { + if (this.selectedCollection) { + await this.selectCollection(identity, this.selectedCollection); + } + return; + } + + this.isSearching = true; + this.error = null; + + try { + const db = await getDatabase(); + const results = await searchCachedRecords(db, normalizedQuery, { + repoDid: identity.did, + collection: this.selectedCollection ?? undefined + }); + this.records = results.map((record) => summarizeCachedRecord(record, identity.handle)); + } catch (searchError) { + this.records = []; + this.error = errorMessage(searchError, 'Could not search cached records.'); + } finally { + this.isSearching = false; + } + } + async loadRecordsFromCache(identity: AccountIdentity, collectionName: string) { try { const db = await getDatabase(); @@ -152,6 +185,8 @@ class RepoBrowserState { this.selectedRecord = null; this.isLoadingCollections = false; this.isLoadingRecords = false; + this.isSearching = false; + this.searchQuery = ''; this.error = null; this.loadedDid = null; } diff --git a/src/lib/components/BootSplash.svelte b/src/lib/components/BootSplash.svelte index 3ec833b..ed8591a 100644 --- a/src/lib/components/BootSplash.svelte +++ b/src/lib/components/BootSplash.svelte @@ -14,7 +14,6 @@
-
@@ -44,98 +43,62 @@ z-index: 9999; display: grid; place-items: center; - overflow: hidden; - color: #fff7e8; - background: - radial-gradient(circle at 52% 42%, rgb(255 132 34 / 0.28), transparent 0 11rem), - radial-gradient(circle at 30% 30%, rgb(119 36 87 / 0.72), transparent 0 22rem), - radial-gradient(circle at 78% 78%, rgb(238 105 16 / 0.28), transparent 0 24rem), - linear-gradient(145deg, #2c001e 0%, #441134 48%, #230017 100%); - } - - .boot-splash::before { - position: absolute; - inset: 0; - content: ''; - background-image: - linear-gradient(rgb(255 255 255 / 0.035) 1px, transparent 1px), - linear-gradient(90deg, rgb(255 255 255 / 0.025) 1px, transparent 1px); - background-size: 18px 18px; - mask-image: radial-gradient(circle at center, black, transparent 78%); - } - - .boot-orb { - position: absolute; - width: min(30rem, 74vw); - aspect-ratio: 1; - border-radius: 50%; - background: radial-gradient(circle, rgb(255 151 51 / 0.18), transparent 62%); - box-shadow: 0 0 6rem rgb(255 107 24 / 0.32); - animation: breathe 2.8s ease-in-out infinite; + color: #eeeeec; + background: #300a24; } .boot-card { - position: relative; display: grid; justify-items: center; width: min(24rem, calc(100vw - 2rem)); - padding: var(--space-8) var(--space-6) var(--space-6); + padding: var(--space-8) var(--space-6); text-align: center; - background: linear-gradient(rgb(255 255 255 / 0.08), rgb(0 0 0 / 0.06)); - border: 1px solid rgb(255 229 198 / 0.22); - border-radius: 10px; - box-shadow: - 0 2rem 5rem rgb(0 0 0 / 0.38), - 0 1px 0 rgb(255 255 255 / 0.18) inset; - backdrop-filter: blur(4px); } .boot-logo { width: 6rem; height: 6rem; - padding: var(--space-3); - border-radius: 1.4rem; - filter: drop-shadow(0 1rem 1.5rem rgb(0 0 0 / 0.4)); - background: radial-gradient(circle at 38% 32%, rgb(255 255 255 / 0.14), rgb(255 255 255 / 0.04)); + color: #e95420; + filter: drop-shadow(0 0.35rem 0.75rem rgb(0 0 0 / 0.28)); } .boot-copy { margin-top: var(--space-5); } + .boot-kicker, + .boot-copy h1 { + color: #e95420; + } + .boot-kicker { - color: #f4b15f; font-size: var(--text-1); font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; - text-shadow: 0 1px 1px rgb(0 0 0 / 0.55); } .boot-copy h1 { margin-top: var(--space-1); font-size: clamp(1.6rem, 4vw, 2.2rem); line-height: var(--leading-tight); - text-shadow: 0 2px 8px rgb(0 0 0 / 0.45); } .boot-step { min-height: 1.3rem; margin-top: var(--space-3); - color: #f0d6c2; + color: #eeeeec; font-size: var(--text-2); } .boot-meter { position: relative; width: min(13rem, 70vw); - height: 0.55rem; + height: 0.45rem; margin-top: var(--space-6); overflow: hidden; - background: rgb(24 0 17 / 0.62); - border: 1px solid rgb(255 255 255 / 0.16); + background: rgb(238 238 236 / 0.18); border-radius: var(--radius-round); - box-shadow: 0 1px 2px rgb(0 0 0 / 0.45) inset; } .boot-meter span { @@ -145,7 +108,7 @@ left: -45%; width: 45%; border-radius: inherit; - background: linear-gradient(90deg, transparent, #f28c22, #ffd08a, transparent); + background: #e95420; animation: loading 1.35s ease-in-out infinite; } @@ -159,20 +122,19 @@ .boot-actions button { padding: 0.45rem 0.75rem; - color: #2c001e; + color: #eeeeec; font: inherit; font-size: var(--text-1); font-weight: 700; - background: linear-gradient(#fff4d8, #d9b06e); - border: 1px solid #7a481a; + background: #e95420; + border: 1px solid #e95420; border-radius: var(--radius-2); - box-shadow: var(--shadow-raised); } .boot-actions button.quiet { - color: #fff3df; - background: rgb(255 255 255 / 0.08); - border-color: rgb(255 255 255 / 0.22); + color: #eeeeec; + background: transparent; + border-color: rgb(238 238 236 / 0.45); } @keyframes loading { @@ -181,22 +143,11 @@ } } - @keyframes breathe { - 50% { - transform: scale(1.05); - opacity: 0.82; - } - } - @media (prefers-reduced-motion: reduce) { - .boot-orb, - .boot-meter span { - animation: none; - } - .boot-meter span { left: 0; width: 100%; + animation: none; } } diff --git a/src/lib/components/CollectionBrowser.svelte b/src/lib/components/CollectionBrowser.svelte index 10c13c3..5b7bc50 100644 --- a/src/lib/components/CollectionBrowser.svelte +++ b/src/lib/components/CollectionBrowser.svelte @@ -5,6 +5,8 @@ import type { RepoRecordSummary } from '$lib/atproto/types'; import { windowManager } from '$lib/window-manager.svelte'; + let searchQuery = $state(''); + onMount(() => { const identity = accountSetup.identity; @@ -17,10 +19,24 @@ const identity = accountSetup.identity; if (identity) { + searchQuery = ''; repoBrowser.selectCollection(identity, collectionName); } } + function searchRecords() { + const identity = accountSetup.identity; + + if (identity) { + repoBrowser.searchRecords(identity, searchQuery); + } + } + + function clearSearch() { + searchQuery = ''; + searchRecords(); + } + function openRecord(record: RepoRecordSummary) { repoBrowser.selectedRecord = record; windowManager.setTitle('gedit', `${record.rkey}.json - gedit`, '/icons/humanity/apps/accessories-text-editor.svg'); @@ -68,11 +84,27 @@

{#if repoBrowser.isLoadingRecords} Fetching public records from {accountSetup.identity?.pds ?? 'the public API'}… + {:else if repoBrowser.searchQuery} + Searching cached records for “{repoBrowser.searchQuery}”. {:else} Browse records as if they were tidy Nautilus files. {/if}

+
@@ -84,10 +116,12 @@ {#if repoBrowser.error}

{repoBrowser.error}

- {:else if repoBrowser.isLoadingRecords} -

Loading records…

+ {:else if repoBrowser.isLoadingRecords || repoBrowser.isSearching} +

{repoBrowser.isSearching ? 'Searching cached records…' : 'Loading records…'}

{:else if repoBrowser.records.length === 0} -

No public records found for this collection.

+

+ {repoBrowser.searchQuery ? 'No cached records matched that search.' : 'No public records found for this collection.'} +

{:else} {#each repoBrowser.records as record (record.uri)}