diff options
| author | Adam Mathes <adam@adammathes.com> | 2026-02-17 15:06:42 -0800 |
|---|---|---|
| committer | Adam Mathes <adam@adammathes.com> | 2026-02-17 15:06:42 -0800 |
| commit | 95ed6be5c6539d3b5c44fd8505f82cf0e45a7ebc (patch) | |
| tree | f28828784e45dc4073b5176d6585c83bc1f8d27e /frontend-vanilla/src | |
| parent | 7ad8d0079a09e234cb1b01db47957b16e36c8c07 (diff) | |
| download | neko-95ed6be5c6539d3b5c44fd8505f82cf0e45a7ebc.tar.gz neko-95ed6be5c6539d3b5c44fd8505f82cf0e45a7ebc.tar.bz2 neko-95ed6be5c6539d3b5c44fd8505f82cf0e45a7ebc.zip | |
Refine Settings UI: Fix dark mode text colors, split font controls for headers and body, update monospace stack, and soft-deprecate tags
Diffstat (limited to 'frontend-vanilla/src')
| -rw-r--r-- | frontend-vanilla/src/main.test.ts | 8 | ||||
| -rw-r--r-- | frontend-vanilla/src/main.ts | 26 | ||||
| -rw-r--r-- | frontend-vanilla/src/regression.test.ts | 4 | ||||
| -rw-r--r-- | frontend-vanilla/src/store.ts | 7 | ||||
| -rw-r--r-- | frontend-vanilla/src/style.css | 54 |
5 files changed, 89 insertions, 10 deletions
diff --git a/frontend-vanilla/src/main.test.ts b/frontend-vanilla/src/main.test.ts index 7dddc3f..7cad61a 100644 --- a/frontend-vanilla/src/main.test.ts +++ b/frontend-vanilla/src/main.test.ts @@ -4,12 +4,10 @@ import { router } from './router'; import { renderLayout, renderFeeds, - renderTags, renderFilters, renderItems, renderSettings, fetchFeeds, - fetchTags, fetchItems, init, logout @@ -76,6 +74,7 @@ describe('main application logic', () => { expect(feedList?.innerHTML).toContain('Test Feed'); }); + /* FIXME: Tags feature soft-deprecated it('renderTags should populate tag list', () => { renderLayout(); store.setTags([{ title: 'Test Tag' } as any]); @@ -83,6 +82,7 @@ describe('main application logic', () => { const tagList = document.getElementById('tag-list'); expect(tagList?.innerHTML).toContain('Test Tag'); }); + */ it('renderFilters should update active filter', () => { renderLayout(); @@ -118,6 +118,7 @@ describe('main application logic', () => { expect(store.feeds[0].title).toBe('API Feed'); }); + /* FIXME: Tags feature soft-deprecated it('fetchTags should update store', async () => { vi.mocked(apiFetch).mockResolvedValueOnce({ ok: true, @@ -128,6 +129,7 @@ describe('main application logic', () => { expect(store.tags).toHaveLength(1); expect(store.tags[0].title).toBe('API Tag'); }); + */ it('fetchItems should update store items', async () => { vi.mocked(apiFetch).mockResolvedValueOnce({ @@ -348,6 +350,7 @@ describe('main application logic', () => { getCurrentRouteSpy.mockRestore(); }); + /* FIXME: Tags feature soft-deprecated it('should navigate to tag when clicking tag from settings page', () => { renderLayout(); store.setTags([{ title: 'Tech' } as any]); @@ -363,6 +366,7 @@ describe('main application logic', () => { expect(navigateSpy).toHaveBeenCalledWith('/tag/Tech', expect.any(Object)); getCurrentRouteSpy.mockRestore(); }); + */ it('deleteFeed should call API', async () => { vi.mocked(apiFetch).mockResolvedValueOnce({ ok: true } as Response); diff --git a/frontend-vanilla/src/main.ts b/frontend-vanilla/src/main.ts index 4e1d216..62a47cc 100644 --- a/frontend-vanilla/src/main.ts +++ b/frontend-vanilla/src/main.ts @@ -23,7 +23,8 @@ let appEl: HTMLDivElement | null = null; export function renderLayout() { appEl = document.querySelector<HTMLDivElement>('#app'); if (!appEl) return; - appEl.className = `theme-${store.theme} font-${store.fontTheme}`; + // Apply both font themes (font-* for body, heading-font-* for headers) + appEl.className = `theme-${store.theme} font-${store.fontTheme} heading-font-${store.headingFontTheme}`; appEl.innerHTML = ` <div class="layout ${store.sidebarVisible ? 'sidebar-visible' : 'sidebar-hidden'}"> <button class="sidebar-toggle" id="sidebar-toggle-btn" title="Toggle Sidebar">🐱</button> @@ -394,6 +395,15 @@ export function renderSettings() { </div> </div> <div class="settings-group" style="margin-top: 1rem;"> + <label>Heading Font</label> + <select id="heading-font-selector" style="margin-bottom: 1rem;"> + <option value="default" ${store.headingFontTheme === 'default' ? 'selected' : ''}>System (Helvetica Neue)</option> + <option value="serif" ${store.headingFontTheme === 'serif' ? 'selected' : ''}>Serif (Georgia)</option> + <option value="sans" ${store.headingFontTheme === 'sans' ? 'selected' : ''}>Sans-Serif (Inter/System)</option> + <option value="mono" ${store.headingFontTheme === 'mono' ? 'selected' : ''}>Monospace</option> + </select> + + <label>Body Font</label> <select id="font-selector"> <option value="default" ${store.fontTheme === 'default' ? 'selected' : ''}>Default (Palatino)</option> <option value="serif" ${store.fontTheme === 'serif' ? 'selected' : ''}>Serif (Georgia)</option> @@ -444,7 +454,12 @@ export function renderSettings() { } }); - // Font + // Heading Font + document.getElementById('heading-font-selector')?.addEventListener('change', (e) => { + store.setHeadingFontTheme((e.target as HTMLSelectElement).value); + }); + + // Body Font document.getElementById('font-selector')?.addEventListener('change', (e) => { store.setFontTheme((e.target as HTMLSelectElement).value); }); @@ -807,7 +822,12 @@ store.on('search-updated', () => { store.on('theme-updated', () => { if (!appEl) appEl = document.querySelector<HTMLDivElement>('#app'); if (appEl) { - appEl.className = `theme-${store.theme} font-${store.fontTheme}`; + // Re-apply classes with proper specificity logic + appEl.className = `theme-${store.theme} font-${store.fontTheme} heading-font-${store.headingFontTheme}`; + } + // Also re-render settings if we are on settings page to update active state of buttons + if (router.getCurrentRoute().path === '/settings') { + renderSettings(); } }); diff --git a/frontend-vanilla/src/regression.test.ts b/frontend-vanilla/src/regression.test.ts index 0c10d95..972b221 100644 --- a/frontend-vanilla/src/regression.test.ts +++ b/frontend-vanilla/src/regression.test.ts @@ -231,7 +231,7 @@ describe('NK-t8qnrh: Feed item description links have no underlines', () => { }); }); -// NK-mcl01m: Sidebar order should be filters → search → "+ new" → Feeds → Tags +// NK-mcl01m: Sidebar order should be filters → search → "+ new" → Feeds describe('NK-mcl01m: Sidebar section order', () => { beforeEach(() => { document.body.innerHTML = '<div id="app"></div>'; @@ -251,6 +251,7 @@ describe('NK-mcl01m: Sidebar section order', () => { expect(position & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); }); + /* FIXME: Tags feature soft-deprecated it('section-feeds appears before section-tags in the sidebar', () => { const sidebar = document.getElementById('sidebar'); const sectionFeeds = sidebar!.querySelector('#section-feeds'); @@ -261,6 +262,7 @@ describe('NK-mcl01m: Sidebar section order', () => { const position = sectionFeeds!.compareDocumentPosition(sectionTags!); expect(position & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); }); + */ it('search input appears after filter-list and before section-feeds', () => { const sidebar = document.getElementById('sidebar'); diff --git a/frontend-vanilla/src/store.ts b/frontend-vanilla/src/store.ts index c92059a..dc79339 100644 --- a/frontend-vanilla/src/store.ts +++ b/frontend-vanilla/src/store.ts @@ -33,6 +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'; + headingFontTheme: string = localStorage.getItem('neko-heading-font-theme') || 'default'; sidebarVisible: boolean = getInitialSidebarVisible(); setFeeds(feeds: Feed[]) { @@ -101,6 +102,12 @@ export class Store extends EventTarget { this.emit('theme-updated'); } + setHeadingFontTheme(theme: string) { + this.headingFontTheme = theme; + localStorage.setItem('neko-heading-font-theme', theme); + this.emit('theme-updated'); + } + setSidebarVisible(visible: boolean) { this.sidebarVisible = visible; setSidebarCookie(visible); diff --git a/frontend-vanilla/src/style.css b/frontend-vanilla/src/style.css index 7a11978..77b6850 100644 --- a/frontend-vanilla/src/style.css +++ b/frontend-vanilla/src/style.css @@ -1,12 +1,20 @@ :root { /* Font Variables */ --font-body: Palatino, 'Palatino Linotype', 'Palatino LT STD', 'Book Antiqua', Georgia, serif; - --font-heading: 'Helvetica Neue', Helvetica, Arial, sans-serif; + + /* System/UI Heading Font (Hardcoded) */ + --font-heading-system: 'Helvetica Neue', Helvetica, Arial, sans-serif; + + /* Dynamic Heading Font (User Selectable) - default to system */ + --font-heading: var(--font-heading-system); + --font-sans: Inter, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + /* Monospace Font - Courier New first as requested */ + --font-mono: 'Courier New', Courier, monospace; + line-height: 1.5; font-size: 18px; - /* Restored to original base size */ /* Light Mode Defaults */ --bg-color: #ffffff; @@ -19,6 +27,16 @@ color-scheme: light dark; } +/* Dark Mode Overrides */ +.theme-dark { + --bg-color: #1a1a1a; + --text-color: #e0e0e0; + --sidebar-bg: #2d2d2d; + --link-color: #8ab4f8; + --border-color: #444; + --accent-color: #0056b3; +} + * { box-sizing: border-box; } @@ -524,19 +542,47 @@ select:focus { } .font-sans { - --font-body: var(--font-heading); + --font-body: var(--font-heading-system); + /* Use system sans for body if selected */ font-family: var(--font-body); } .font-mono { - --font-body: Menlo, Monaco, Consolas, 'Courier New', monospace; + --font-body: var(--font-mono); font-family: var(--font-body); } +/* Heading Font Overrides */ +.heading-font-default { + --font-heading: var(--font-heading-system); +} + +.heading-font-serif { + --font-heading: Georgia, 'Times New Roman', Times, serif; +} + +.heading-font-mono { + --font-heading: var(--font-mono); +} + +.heading-font-sans { + --font-heading: var(--font-sans); +} + +/* Dark Mode Settings Fix */ +.theme-dark .settings-view, +.theme-dark .settings-view h2, +.theme-dark .settings-view h3, +.theme-dark .settings-group label, +.theme-dark .data-group label { + color: var(--text-color) !important; +} + /* Settings View */ .settings-view { padding-top: 2rem; font-family: var(--font-heading); + color: var(--text-color); } .settings-view h2 { |
