aboutsummaryrefslogtreecommitdiffstats
path: root/frontend-vanilla
diff options
context:
space:
mode:
authorAdam Mathes <adam@adammathes.com>2026-02-16 11:28:01 -0800
committerAdam Mathes <adam@adammathes.com>2026-02-16 11:28:01 -0800
commit52ea335714f2b495b92f87636c269b73b4067066 (patch)
tree56ba60da3189fabe1a5796214956e2c466a5c92e /frontend-vanilla
parentcd4e599a6d7a13e08841d63257dca6d355eb2bb8 (diff)
downloadneko-52ea335714f2b495b92f87636c269b73b4067066.tar.gz
neko-52ea335714f2b495b92f87636c269b73b4067066.tar.bz2
neko-52ea335714f2b495b92f87636c269b73b4067066.zip
v3: persist sidebar state via cookie, default closed on tablet/mobile
Sidebar open/close state is now saved to a cookie (neko_sidebar) so it persists across page reloads. On first visit without a cookie, the sidebar defaults to closed on tablet and mobile (<=1024px viewport) and open on desktop. Removes the auto-open-on-resize behavior that was overriding user preference. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'frontend-vanilla')
-rw-r--r--frontend-vanilla/src/main.ts6
-rw-r--r--frontend-vanilla/src/store.test.ts42
-rw-r--r--frontend-vanilla/src/store.ts20
3 files changed, 61 insertions, 7 deletions
diff --git a/frontend-vanilla/src/main.ts b/frontend-vanilla/src/main.ts
index 7d75e14..3dc0574 100644
--- a/frontend-vanilla/src/main.ts
+++ b/frontend-vanilla/src/main.ts
@@ -87,11 +87,7 @@ export function attachLayoutListeners() {
store.setSidebarVisible(false);
});
- window.addEventListener('resize', () => {
- if (window.innerWidth > 768 && !store.sidebarVisible) {
- store.setSidebarVisible(true);
- }
- });
+ // Sidebar state is persisted via cookie; no auto-open on resize
// Collapsible sections
document.querySelectorAll('.sidebar-section.collapsible h3').forEach(header => {
diff --git a/frontend-vanilla/src/store.test.ts b/frontend-vanilla/src/store.test.ts
index 33deb7f..799db83 100644
--- a/frontend-vanilla/src/store.test.ts
+++ b/frontend-vanilla/src/store.test.ts
@@ -1,4 +1,4 @@
-import { describe, it, expect, vi } from 'vitest';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
import { Store } from './store';
describe('Store', () => {
@@ -105,4 +105,44 @@ describe('Store', () => {
expect(localStorage.getItem('neko-font-theme')).toBe('serif');
expect(callback).toHaveBeenCalled();
});
+
+ describe('sidebar cookie persistence', () => {
+ beforeEach(() => {
+ // Clear sidebar cookie
+ document.cookie = 'neko_sidebar=; path=/; max-age=0';
+ });
+
+ it('should persist sidebar state to cookie', () => {
+ const store = new Store();
+ store.setSidebarVisible(false);
+ expect(document.cookie).toContain('neko_sidebar=0');
+
+ store.setSidebarVisible(true);
+ expect(document.cookie).toContain('neko_sidebar=1');
+ });
+
+ it('should read sidebar state from cookie on init', () => {
+ document.cookie = 'neko_sidebar=1; path=/';
+ const store = new Store();
+ expect(store.sidebarVisible).toBe(true);
+
+ document.cookie = 'neko_sidebar=0; path=/';
+ const store2 = new Store();
+ expect(store2.sidebarVisible).toBe(false);
+ });
+
+ it('should default to closed on tablet/mobile when no cookie', () => {
+ // jsdom defaults innerWidth to 0, which is <= 1024
+ const store = new Store();
+ expect(store.sidebarVisible).toBe(false);
+ });
+
+ it('should emit sidebar-toggle when toggling', () => {
+ const store = new Store();
+ const callback = vi.fn();
+ store.on('sidebar-toggle', callback);
+ store.toggleSidebar();
+ expect(callback).toHaveBeenCalled();
+ });
+ });
});
diff --git a/frontend-vanilla/src/store.ts b/frontend-vanilla/src/store.ts
index a23934a..c92059a 100644
--- a/frontend-vanilla/src/store.ts
+++ b/frontend-vanilla/src/store.ts
@@ -4,6 +4,23 @@ export type StoreEvent = 'feeds-updated' | 'tags-updated' | 'items-updated' | 'a
export type FilterType = 'unread' | 'all' | 'starred';
+function getSidebarCookie(): boolean | null {
+ const match = document.cookie.split('; ').find(row => row.startsWith('neko_sidebar='));
+ if (!match) return null;
+ return match.split('=')[1] === '1';
+}
+
+function setSidebarCookie(visible: boolean) {
+ document.cookie = `neko_sidebar=${visible ? '1' : '0'}; path=/; max-age=31536000; SameSite=Lax`;
+}
+
+function getInitialSidebarVisible(): boolean {
+ const saved = getSidebarCookie();
+ if (saved !== null) return saved;
+ // Default: closed on tablet+mobile (<=1024px), open on desktop
+ return window.innerWidth > 1024;
+}
+
export class Store extends EventTarget {
feeds: Feed[] = [];
tags: Category[] = [];
@@ -16,7 +33,7 @@ export class Store extends EventTarget {
hasMore: boolean = true;
theme: string = localStorage.getItem('neko-theme') || 'light';
fontTheme: string = localStorage.getItem('neko-font-theme') || 'default';
- sidebarVisible: boolean = window.innerWidth > 768;
+ sidebarVisible: boolean = getInitialSidebarVisible();
setFeeds(feeds: Feed[]) {
this.feeds = feeds;
@@ -86,6 +103,7 @@ export class Store extends EventTarget {
setSidebarVisible(visible: boolean) {
this.sidebarVisible = visible;
+ setSidebarCookie(visible);
this.emit('sidebar-toggle');
}