diff --git a/AGENTS.md b/AGENTS.md
index 0392f3e..84f9100 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -31,7 +31,7 @@ src/
## Key Design Decisions
-- **Svelte 5 runes** — Use `$state`, `$derived`, `$effect`. Props-based event passing (no Svelte events).
+- **Svelte 5 runes** — Use `$state`, `$derived`, `$effect`. Props-based event passing (no Svelte events). Event handler attributes are all lower-case (e.g. oninput)
- **No external crypto libraries** — Uses the browser's native Web Crypto API exclusively.
- **Key never persisted** — Encryption key lives only in `$state` memory; cleared on lock, tab switch, or page close.
- **Single-file build** — `vite-plugin-singlefile` inlines all JS/CSS; post-build script inlines favicon.
diff --git a/dist/index.html b/dist/index.html
index 14c0c6e..7b2549d 100644
--- a/dist/index.html
+++ b/dist/index.html
@@ -5763,6 +5763,7 @@ function LockScreen($$anchor, $$props) {
}
//#endregion
//#region src/lib/stores/search.svelte.js
+var DEBOUNCE_MS = 300;
var SearchStore = class {
#query = /* @__PURE__ */ state("");
get query() {
@@ -5771,6 +5772,13 @@ var SearchStore = class {
set query(value) {
set(this.#query, value, true);
}
+ #debouncedQuery = /* @__PURE__ */ state("");
+ get debouncedQuery() {
+ return get(this.#debouncedQuery);
+ }
+ set debouncedQuery(value) {
+ set(this.#debouncedQuery, value, true);
+ }
#activeGroupId = /* @__PURE__ */ state("all");
get activeGroupId() {
return get(this.#activeGroupId);
@@ -5785,8 +5793,21 @@ var SearchStore = class {
set refreshTrigger(value) {
set(this.#refreshTrigger, value, true);
}
+ #debounceTimer = null;
+ setSearchQuery(value) {
+ this.query = value;
+ if (this.#debounceTimer) clearTimeout(this.#debounceTimer);
+ if (value === "") {
+ this.debouncedQuery = "";
+ this.#debounceTimer = null;
+ } else this.#debounceTimer = setTimeout(() => {
+ this.debouncedQuery = value;
+ this.#debounceTimer = null;
+ }, DEBOUNCE_MS);
+ }
clear() {
this.query = "";
+ this.debouncedQuery = "";
this.activeGroupId = "all";
}
/** Force subscribed components to re-fetch data. */
@@ -6007,13 +6028,13 @@ function Sidebar($$anchor, $$props) {
set_value(input, search.query);
set_class(button, 1, `group-item ${search.activeGroupId === "all" ? "active" : ""}`, "svelte-181dlmc");
});
- event("Input", input, (e) => search.query = e.target.value);
+ delegated("input", input, (e) => search.setSearchQuery(e.target.value));
delegated("click", button, () => search.activeGroupId = "all");
delegated("click", button_4, () => openGroupForm(null));
append($$anchor, div);
pop();
}
-delegate(["click"]);
+delegate(["input", "click"]);
//#endregion
//#region src/components/EntryList.svelte
var root_1$6 = /* @__PURE__ */ from_html(`
Loading entries...
`);
@@ -6034,7 +6055,7 @@ function EntryList($$anchor, $$props) {
set(loading, true);
set(error, "");
try {
- const query = search.query.trim();
+ const query = search.debouncedQuery.trim();
const groupId = search.activeGroupId;
if (query) set(entries, await searchEntries(query, groupId !== "all" ? { groupId } : {}), true);
else if (groupId !== "all") set(entries, await getEntries({ groupId }), true);
@@ -6046,7 +6067,7 @@ function EntryList($$anchor, $$props) {
set(loading, false);
}
user_effect(() => {
- search.query;
+ search.debouncedQuery;
search.activeGroupId;
search.refreshTrigger;
loadEntries();
@@ -6883,10 +6904,7 @@ function MainLayout($$anchor, $$props) {
if (get(sidebarOpen)) $$render(consequent);
});
var aside = sibling(node, 2);
- Sidebar(child(aside), { $$events: {
- back: handleBack,
- goList
- } });
+ Sidebar(child(aside), {});
reset(aside);
var main = sibling(aside, 2);
var div_2 = child(main);
diff --git a/src/components/EntryList.svelte b/src/components/EntryList.svelte
index 9a3842b..9acff08 100644
--- a/src/components/EntryList.svelte
+++ b/src/components/EntryList.svelte
@@ -13,7 +13,7 @@
loading = true
error = ''
try {
- const query = searchStore.query.trim()
+ const query = searchStore.debouncedQuery.trim()
const groupId = searchStore.activeGroupId
if (query) {
@@ -35,9 +35,9 @@
loading = false
}
- // Reload when search query, active group filter, or refresh trigger changes
+ // Reload when debounced search query, active group filter, or refresh trigger changes
$effect(() => {
- searchStore.query
+ searchStore.debouncedQuery
searchStore.activeGroupId
searchStore.refreshTrigger
loadEntries()
diff --git a/src/components/MainLayout.svelte b/src/components/MainLayout.svelte
index 971a1cb..0b29cc1 100644
--- a/src/components/MainLayout.svelte
+++ b/src/components/MainLayout.svelte
@@ -58,7 +58,7 @@
diff --git a/src/components/Sidebar.svelte b/src/components/Sidebar.svelte
index c82578e..02c39c5 100644
--- a/src/components/Sidebar.svelte
+++ b/src/components/Sidebar.svelte
@@ -96,7 +96,7 @@
type="text"
placeholder="Search entries..."
value={searchStore.query}
- onInput={(e) => searchStore.query = e.target.value}
+ oninput={(e) => searchStore.setSearchQuery(e.target.value)}
/>
diff --git a/src/lib/stores/search.svelte.js b/src/lib/stores/search.svelte.js
index 49eb32e..c86997f 100644
--- a/src/lib/stores/search.svelte.js
+++ b/src/lib/stores/search.svelte.js
@@ -3,13 +3,36 @@
* Shared between Sidebar and EntryList for coordinated filtering.
*/
+const DEBOUNCE_MS = 300
+
export class SearchStore {
- query = $state('')
+ query = $state('') // raw input value — bound to the search input
+ debouncedQuery = $state('') // debounced value — used for actual search
activeGroupId = $state('all') // 'all' or a group id
refreshTrigger = $state(0) // incremented to force a re-fetch
+ #debounceTimer = null
+
+ /**
+ * Update the search query with debouncing.
+ * Call this from the input handler instead of setting `query` directly.
+ */
+ setSearchQuery(value) {
+ this.query = value
+ if (this.#debounceTimer) clearTimeout(this.#debounceTimer)
+ if (value === '') {
+ this.debouncedQuery = ''
+ this.#debounceTimer = null
+ } else {
+ this.#debounceTimer = setTimeout(() => {
+ this.debouncedQuery = value
+ this.#debounceTimer = null
+ }, DEBOUNCE_MS)
+ }
+ }
clear() {
this.query = ''
+ this.debouncedQuery = ''
this.activeGroupId = 'all'
}
diff --git a/tests/lib/stores/search.test.js b/tests/lib/stores/search.test.js
index 352cdfc..eb3e916 100644
--- a/tests/lib/stores/search.test.js
+++ b/tests/lib/stores/search.test.js
@@ -18,12 +18,12 @@ describe('SearchStore', () => {
describe('query', () => {
it('should update query', () => {
- search.query = 'test'
+ search.setSearchQuery('test')
expect(search.query).toBe('test')
})
it('should handle unicode query', () => {
- search.query = 'тест'
+ search.setSearchQuery('тест')
expect(search.query).toBe('тест')
})
})
@@ -48,7 +48,7 @@ describe('SearchStore', () => {
describe('clear()', () => {
it('should reset query to empty string', () => {
- search.query = 'some search'
+ search.setSearchQuery('some search')
search.clear()
expect(search.query).toBe('')
})
@@ -60,7 +60,7 @@ describe('SearchStore', () => {
})
it('should reset both fields simultaneously', () => {
- search.query = 'some search'
+ search.setSearchQuery('some search')
search.activeGroupId = 'group-123'
search.clear()
expect(search.query).toBe('')