From 9d3b2a90316a1a5f735845f61abbd8a875529060 Mon Sep 17 00:00:00 2001 From: Adam Mathes Date: Sat, 14 Feb 2026 10:12:22 -0800 Subject: feat: add font theme support (fixing NK-rn4nzp) --- .thicket/tickets.jsonl | 6 +++--- frontend/src/App.tsx | 26 ++++++++++++++++++++++---- frontend/src/components/FeedItem.css | 4 ++-- frontend/src/components/FeedList.css | 7 ++----- frontend/src/components/Settings.css | 21 +++++++++++++++++++-- frontend/src/components/Settings.tsx | 29 ++++++++++++++++++++++++++++- frontend/src/index.css | 31 +++++++++++++++++++++++++++++-- 7 files changed, 105 insertions(+), 19 deletions(-) diff --git a/.thicket/tickets.jsonl b/.thicket/tickets.jsonl index be32f34..1df9ff0 100644 --- a/.thicket/tickets.jsonl +++ b/.thicket/tickets.jsonl @@ -26,7 +26,7 @@ {"id":"NK-9hx0y7","title":"Implement Frontend Login","description":"Create login page and auth logic in the new React frontend. Port functionality from legacy login.html.","type":"","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-13T05:44:01.546395342Z","updated":"2026-02-13T05:50:33.877452063Z"} {"id":"NK-9pgjph","title":"v2 ui - font size 18px","description":"Compare your font sizes with the legacy version -- I think they're a little too small (16 vs 18 baseline)","type":"bug","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-14T03:21:48.453217898Z","updated":"2026-02-14T03:24:25.316927886Z"} {"id":"NK-a217qm","title":"font styles","description":"Switch the default font stack and size to match the legacy UI","type":"feature","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-14T00:59:37.686539676Z","updated":"2026-02-14T01:25:03.119825567Z"} -{"id":"NK-a7c6lb","title":"coverage status","description":"check coverage status -- are we still close to 80%\nit's ok to ignore the old static legacy javascript or vanilla js prototype\nif it's low file a ticket to get coverage back up","type":"task","status":"open","priority":1,"labels":null,"assignee":"","created":"2026-02-14T17:32:19.995215347Z","updated":"2026-02-14T17:32:19.995215347Z"} +{"id":"NK-a7c6lb","title":"coverage status","description":"check coverage status -- are we still close to 80%\nit's ok to ignore the old static legacy javascript or vanilla js prototype\nif it's low file a ticket to get coverage back up","type":"task","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-14T17:32:19.995215347Z","updated":"2026-02-14T18:03:41.748377361Z"} {"id":"NK-acq08a","title":"update Makefile","description":"Ensure the Makefile builds things and works\nTest it by running it regularly before checking in!","type":"task","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-14T00:55:40.127322076Z","updated":"2026-02-14T01:26:31.564799193Z"} {"id":"NK-ahzf5c","title":"drop \"mark read\" button","description":"there's no mark read/unread buttons, it's just by scrolling!","type":"task","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-13T19:28:20.708443259Z","updated":"2026-02-13T20:26:43.029168286Z"} {"id":"NK-aiaza3","title":"clean up root directory of project","description":"There are some scripts in the root directory like run_e2e.sh that probably should be in a subdirectory -- look into it and make things a little tidier where approopriate.","type":"task","status":"open","priority":2,"labels":null,"assignee":"","created":"2026-02-14T17:42:14.8736959Z","updated":"2026-02-14T17:42:14.8736959Z"} @@ -62,7 +62,7 @@ {"id":"NK-mgmn5m","title":"serve \"legacy\" version UI at /v1/ instead of /","description":"Let's \"softly\" start to deprecated the legacy version by moving it to /v1/ -- ideally this won't require any changes but there may be some relative/absolute URLs to adjust in the static files there or in rouoting","type":"task","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-14T16:41:04.710679944Z","updated":"2026-02-14T17:38:25.35292336Z"} {"id":"NK-mwf9q2","title":"Implement Tag View","description":"Create frontend view for browsing items by tag/category. Use /tag/:id endpoint.","type":"","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-13T15:04:12.441165286Z","updated":"2026-02-13T18:04:38.644796168Z"} {"id":"NK-n7nuyy","title":"Fix TypeScript Lint Errors in Tests","description":"There are lint errors in test files regarding jest-dom matchers (toBeInTheDocument, etc). Ensure proper types are included.","type":"bug","status":"closed","priority":3,"labels":null,"assignee":"","created":"2026-02-13T21:50:15.140702806Z","updated":"2026-02-13T21:50:15.140702806Z"} -{"id":"NK-nx8dhw","title":"fix github ci","description":"seems to be broken, are the right things still in there given all the refactorings","type":"bug","status":"open","priority":1,"labels":null,"assignee":"","created":"2026-02-14T18:02:19.485418738Z","updated":"2026-02-14T18:02:19.485418738Z"} +{"id":"NK-nx8dhw","title":"fix github ci","description":"seems to be broken, are the right things still in there given all the refactorings","type":"bug","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-14T18:02:19.485418738Z","updated":"2026-02-14T18:08:16.790742187Z"} {"id":"NK-o3n9jf","title":"[security] Run Docker Container as Non-Root User","description":"Update the Dockerfile to create and use a non-privileged user. 1. Create a user (e.g., neko) in the final stage. 2. Ensure the /app/data directory is owned by this user. 3. Switch to this user using USER neko before the CMD.","type":"","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-14T16:35:58.328232962Z","updated":"2026-02-14T17:18:34.748238191Z"} {"id":"NK-ojdcmq","title":"UI: Add skeleton loaders for feed item loading","description":"The currently 'Loading more...' text is basic. We should add skeleton loaders for a smoother infinite scroll experience.","type":"task","status":"closed","priority":3,"labels":null,"assignee":"","created":"2026-02-13T19:45:07.376295295Z","updated":"2026-02-13T19:45:07.376295295Z"} {"id":"NK-op5594","title":"Ensure 80% Frontend Test Coverage","description":"Configure coverage reporting in vitest and ensure the frontend codebase maintains at least 80% test coverage.","type":"task","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-13T05:46:24.13314466Z","updated":"2026-02-13T05:50:46.728239299Z"} @@ -74,7 +74,7 @@ {"id":"NK-rn4nzp","title":"font themes","description":"in the v2 ui, let's offer a few different font stacks the user can switch through. primarily this should just change font-face, maybe size, but don't worry about colors or anything right now.\n\nthe current default (helvetica neue, palatino)\na fancy all serif stack\na no-nonsense modern san-serif stack\na terminal inspired fixed width stack","type":"feature","status":"open","priority":2,"labels":null,"assignee":"","created":"2026-02-14T17:10:02.185477382Z","updated":"2026-02-14T17:10:02.185477382Z"} {"id":"NK-rohuiq","title":"titles changing on read state and hover","description":"Titles are changing on read state from blue to grey. They should just stay blue all the time.\n\nTitles are getting underlined on hover. They should have no underline regardless of hover state.","type":"bug","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-14T03:36:26.36373162Z","updated":"2026-02-14T03:37:50.73870586Z"} {"id":"NK-shpyxh","title":"add search to new ui","description":"","type":"epic","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-13T19:29:44.251257089Z","updated":"2026-02-14T01:02:58.547025683Z"} -{"id":"NK-sk6pym","title":"fix docker-compose","description":"bug when trying to build in docker -- we may want to add an automated test for this later (though it may be hard since we're building in a vm to nest these)\n\n--\u003e 668e2e2c4b44\n[2/3] STEP 3/9: WORKDIR /app\n--\u003e b42c7c265b7f\n[2/3] STEP 4/9: COPY go.mod go.sum ./\n--\u003e 093e4d9b623e\n[2/3] STEP 5/9: RUN go mod download\n--\u003e 208c8aaac5eb\n[2/3] STEP 6/9: COPY . .\n--\u003e 7c44260c3ac0\n[2/3] STEP 7/9: COPY --from=frontend-builder /app/frontend/dist ./frontend/dist\n--\u003e 09749e6660e1\n[2/3] STEP 8/9: RUN rice -i ./web embed-go\n2026/02/14 17:34:13 no calls to rice.FindBox() found\n--\u003e cdc88c64da36\n[2/3] STEP 9/9: RUN go build -o neko .\nno Go files in /app\nError: building at STEP \"RUN go build -o neko .\": while running runtime: exit status 1","type":"bug","status":"open","priority":1,"labels":null,"assignee":"","created":"2026-02-14T17:38:05.696339994Z","updated":"2026-02-14T17:38:05.696339994Z"} +{"id":"NK-sk6pym","title":"fix docker-compose","description":"bug when trying to build in docker -- we may want to add an automated test for this later (though it may be hard since we're building in a vm to nest these)\n\n--\u003e 668e2e2c4b44\n[2/3] STEP 3/9: WORKDIR /app\n--\u003e b42c7c265b7f\n[2/3] STEP 4/9: COPY go.mod go.sum ./\n--\u003e 093e4d9b623e\n[2/3] STEP 5/9: RUN go mod download\n--\u003e 208c8aaac5eb\n[2/3] STEP 6/9: COPY . .\n--\u003e 7c44260c3ac0\n[2/3] STEP 7/9: COPY --from=frontend-builder /app/frontend/dist ./frontend/dist\n--\u003e 09749e6660e1\n[2/3] STEP 8/9: RUN rice -i ./web embed-go\n2026/02/14 17:34:13 no calls to rice.FindBox() found\n--\u003e cdc88c64da36\n[2/3] STEP 9/9: RUN go build -o neko .\nno Go files in /app\nError: building at STEP \"RUN go build -o neko .\": while running runtime: exit status 1","type":"bug","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-14T17:38:05.696339994Z","updated":"2026-02-14T18:06:42.659012133Z"} {"id":"NK-sne5ox","title":"Implement Export/Import UI","description":"Add UI in settings to download OPML export and upload OPML import. Use /export/ and /import/ (need to check if import exists).","type":"epic","status":"icebox","priority":3,"labels":null,"assignee":"","created":"2026-02-13T15:05:23.266731399Z","updated":"2026-02-13T15:05:23.266731399Z"} {"id":"NK-sxcm7y","title":"Enable Gzip Compression in Go Backend","description":"Check if the Go backend is serving content with gzip compression. If not, implement it to reduce page size and improve performance. Add tests to verify.","type":"feature","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-13T21:57:24.578388732Z","updated":"2026-02-13T22:22:49.350223751Z"} {"id":"NK-t0nmbj","title":"new web frontend","description":"The current frontend uses an old version of backbone and jquery. Let's \"deprecate\" it -- keep it arouond so we can test against it and use it, but let's be able to also serve and use a nice shiny new frontend written in either simiple, highly efficient vanilla javascript, or put together something in react or similar. Needs to feel fast and low latency!\n\nIt's very important that this new frontend has all the functionality of the existing one AND looks similar (use same style, etc, but adjust a little if needed.)\n\nALSO make it highly testable and have high test coverage as you go. I don't want it to use the Chrome browser plugin thing, just test it on your own using things from the command line you can do.","type":"epic","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-13T02:01:37.2107893Z","updated":"2026-02-13T05:43:47.613995925Z"} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 74ae89e..1812451 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -36,12 +36,19 @@ import FeedList from './components/FeedList'; import FeedItems from './components/FeedItems'; import Settings from './components/Settings'; -function Dashboard({ theme, setTheme }: { theme: string; setTheme: (t: string) => void }) { +interface DashboardProps { + theme: string; + setTheme: (t: string) => void; + fontTheme: string; + setFontTheme: (t: string) => void; +} + +function Dashboard({ theme, setTheme, fontTheme, setFontTheme }: DashboardProps) { const [sidebarVisible, setSidebarVisible] = useState(true); return (
{!sidebarVisible && ( @@ -60,7 +67,7 @@ function Dashboard({ theme, setTheme }: { theme: string; setTheme: (t: string) = } /> } /> - } /> + } /> } /> @@ -71,12 +78,18 @@ function Dashboard({ theme, setTheme }: { theme: string; setTheme: (t: string) = function App() { const [theme, setTheme] = useState(localStorage.getItem('neko-theme') || 'light'); + const [fontTheme, setFontTheme] = useState(localStorage.getItem('neko-font-theme') || 'default'); const handleSetTheme = (newTheme: string) => { setTheme(newTheme); localStorage.setItem('neko-theme', newTheme); }; + const handleSetFontTheme = (newFontTheme: string) => { + setFontTheme(newFontTheme); + localStorage.setItem('neko-font-theme', newFontTheme); + }; + const basename = window.location.pathname.startsWith('/v2') ? '/v2' : '/'; return ( @@ -87,7 +100,12 @@ function App() { path="/*" element={ - + } /> diff --git a/frontend/src/components/FeedItem.css b/frontend/src/components/FeedItem.css index 3becb20..4eca2b1 100644 --- a/frontend/src/components/FeedItem.css +++ b/frontend/src/components/FeedItem.css @@ -15,7 +15,7 @@ } .item-title { - font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-family: var(--font-heading); font-size: 1.8rem; font-weight: bold; text-decoration: none; @@ -114,7 +114,7 @@ border: 1px solid var(--border-color, #ccc); color: blue; cursor: pointer; - font-family: 'Helvetica Neue'; + font-family: var(--font-heading); font-weight: bold; font-size: 0.8rem; padding: 2px 6px; diff --git a/frontend/src/components/FeedList.css b/frontend/src/components/FeedList.css index 7bb0f4c..7c39901 100644 --- a/frontend/src/components/FeedList.css +++ b/frontend/src/components/FeedList.css @@ -1,6 +1,6 @@ .feed-list { padding: 1rem; - font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-family: var(--font-heading); color: #777; /* specific v1 color */ font-size: 0.8rem; @@ -18,9 +18,6 @@ z-index: 10; padding-bottom: 0.5rem; color: var(--text-color); - /* Usually dark/white depending on theme, v1 was white on blue? No, white on fixed header? No, v1 logo class says color: white. But sidebar is #ccc. */ - /* In v1 logo was fixed top left (blue header bar?). In v2 sidebar is #ccc. - Let's use theme text color but maybe bolder? */ } /* Override logo color if necessary for themes */ @@ -55,7 +52,7 @@ margin: 1rem 0 0.25rem 0; cursor: pointer; user-select: none; - font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-family: var(--font-heading); color: #333; /* Darker than list items */ text-transform: lowercase; diff --git a/frontend/src/components/Settings.css b/frontend/src/components/Settings.css index ec6fc83..c8784a0 100644 --- a/frontend/src/components/Settings.css +++ b/frontend/src/components/Settings.css @@ -4,7 +4,8 @@ margin: 0 auto; } -.add-feed-section { +.add-feed-section, +.appearance-section { background: var(--sidebar-bg); padding: 1.5rem; border-radius: 8px; @@ -12,6 +13,22 @@ border: 1px solid var(--border-color); } +.font-selector { + display: flex; + align-items: center; + gap: 1rem; +} + +.font-select { + padding: 0.5rem; + border: 1px solid var(--border-color); + background: var(--bg-color); + color: var(--text-color); + border-radius: 4px; + font-size: 1rem; + min-width: 200px; +} + .add-feed-form { display: flex; gap: 1rem; @@ -57,7 +74,7 @@ } .feed-title { - font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-family: var(--font-heading); font-weight: bold; font-size: 1.1rem; } diff --git a/frontend/src/components/Settings.tsx b/frontend/src/components/Settings.tsx index 16cf6a3..80e3068 100644 --- a/frontend/src/components/Settings.tsx +++ b/frontend/src/components/Settings.tsx @@ -3,14 +3,21 @@ import type { Feed } from '../types'; import './Settings.css'; import { apiFetch } from '../utils'; -export default function Settings() { +interface SettingsProps { + fontTheme?: string; + setFontTheme?: (t: string) => void; +} + +export default function Settings({ fontTheme, setFontTheme }: SettingsProps) { const [feeds, setFeeds] = useState([]); + /* ... existing state ... */ const [newFeedUrl, setNewFeedUrl] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [importFile, setImportFile] = useState(null); + /* ... existing fetchFeeds ... */ const fetchFeeds = () => { setLoading(true); apiFetch('/api/feed/') @@ -32,6 +39,7 @@ export default function Settings() { fetchFeeds(); }, []); + /* ... existing handlers ... */ const handleAddFeed = (e: React.FormEvent) => { e.preventDefault(); if (!newFeedUrl) return; @@ -106,6 +114,25 @@ export default function Settings() {

Settings

+ {setFontTheme && ( +
+

Appearance

+
+ + +
+
+ )} +

Add New Feed

diff --git a/frontend/src/index.css b/frontend/src/index.css index 3365d34..6396633 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,5 +1,11 @@ +:root { + /* Font Variables */ + --font-body: Palatino, 'Palatino Linotype', 'Palatino LT STD', 'Book Antiqua', Georgia, serif; + --font-heading: 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + body { - font-family: Palatino, 'Palatino Linotype', 'Palatino LT STD', 'Book Antiqua', Georgia, serif; + font-family: var(--font-body); } h1, @@ -10,10 +16,31 @@ h5, .logo, .nav-link, .logout-btn { - font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-family: var(--font-heading); font-weight: bold; } +/* Font Themes */ +.font-default { + --font-body: Palatino, 'Palatino Linotype', 'Palatino LT STD', 'Book Antiqua', Georgia, serif; + --font-heading: 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +.font-serif { + --font-body: Georgia, 'Times New Roman', Times, serif; + --font-heading: Georgia, 'Times New Roman', Times, serif; +} + +.font-sans { + --font-body: Inter, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + --font-heading: Inter, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; +} + +.font-mono { + --font-body: Menlo, Monaco, Consolas, 'Courier New', monospace; + --font-heading: Menlo, Monaco, Consolas, 'Courier New', monospace; +} + :root { line-height: 1.5; font-size: 18px; -- cgit v1.2.3