aboutsummaryrefslogtreecommitdiffstats
path: root/frontend-vanilla/src
diff options
context:
space:
mode:
Diffstat (limited to 'frontend-vanilla/src')
-rw-r--r--frontend-vanilla/src/main.test.ts8
-rw-r--r--frontend-vanilla/src/main.ts26
-rw-r--r--frontend-vanilla/src/regression.test.ts4
-rw-r--r--frontend-vanilla/src/store.ts7
-rw-r--r--frontend-vanilla/src/style.css54
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 {