diff options
| -rw-r--r-- | .devcontainer/Dockerfile | 7 | ||||
| -rw-r--r-- | .devcontainer/devcontainer.json | 55 | ||||
| -rw-r--r-- | .thicket/tickets.jsonl | 9 | ||||
| -rw-r--r-- | README.md | 18 | ||||
| -rw-r--r-- | frontend/eslint.config.js | 7 | ||||
| -rw-r--r-- | frontend/src/App.test.tsx | 17 | ||||
| -rw-r--r-- | frontend/src/components/FeedItem.test.tsx | 6 | ||||
| -rw-r--r-- | frontend/src/components/FeedItems.test.tsx | 31 | ||||
| -rw-r--r-- | frontend/src/components/FeedItems.tsx | 3 | ||||
| -rw-r--r-- | frontend/src/components/FeedList.test.tsx | 58 | ||||
| -rw-r--r-- | frontend/src/components/FeedList.tsx | 4 | ||||
| -rw-r--r-- | frontend/src/components/Login.test.tsx | 10 | ||||
| -rw-r--r-- | frontend/src/components/Login.tsx | 2 | ||||
| -rw-r--r-- | frontend/src/components/Settings.test.tsx | 26 | ||||
| -rw-r--r-- | frontend/src/components/Settings.tsx | 7 | ||||
| -rw-r--r-- | frontend/src/components/TagView.test.tsx | 25 | ||||
| -rw-r--r-- | web/dist/v2/assets/index-BYhh91HH.css | 1 | ||||
| -rw-r--r-- | web/dist/v2/assets/index-CO97K-eb.css | 1 | ||||
| -rw-r--r-- | web/dist/v2/assets/index-WckTN3Bl.js (renamed from web/dist/v2/assets/index-BdhwF6aQ.js) | 0 | ||||
| -rw-r--r-- | web/dist/v2/index.html | 4 |
20 files changed, 192 insertions, 99 deletions
diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..111d1b9 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,7 @@ +FROM mcr.microsoft.com/devcontainers/go:1.24-bullseye + +# Install additional OS packages for CGO and SQLite +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install --no-install-recommends build-essential sqlite3 libsqlite3-dev + +# [Optional] Install Node.js is handled by features in devcontainer.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..af4ea12 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,55 @@ +{ + "name": "Neko (Podman)", + "build": { + "dockerfile": "Dockerfile", + "context": ".." + }, + "features": { + "ghcr.io/devcontainers/features/node:1": { + "node": "20" + } + }, + "customizations": { + "vscode": { + "settings": { + "go.toolsManagement.checkForUpdates": "local", + "go.useLanguageServer": true, + "go.gopath": "/go", + "editor.formatOnSave": true, + "eslint.workingDirectories": [ + "./frontend" + ] + }, + "extensions": [ + "golang.Go", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "vitest.explorer", + "ms-azuretools.vscode-docker" + ] + } + }, + "forwardPorts": [ + 8080, + 5173 + ], + "portsAttributes": { + "5173": { + "label": "Frontend (Vite)", + "onAutoForward": "notify" + }, + "8080": { + "label": "Backend (Neko)", + "onAutoForward": "notify" + } + }, + "postCreateCommand": "go mod download && (cd frontend && npm install)", + "remoteUser": "vscode", + "runArgs": [ + "--security-opt", + "label=disable" + ], + "containerEnv": { + "NEKO_PORT": "8080" + } +}
\ No newline at end of file diff --git a/.thicket/tickets.jsonl b/.thicket/tickets.jsonl index f848457..b14552c 100644 --- a/.thicket/tickets.jsonl +++ b/.thicket/tickets.jsonl @@ -23,9 +23,10 @@ {"id":"NK-6q9nyg","title":"Refactor HTTP-dependent functions for testability","description":"Several functions use http.Get or external libraries directly (GetFullContent uses goose, ResolveFeedURL uses http.Get + goquery, imageProxyHandler uses http.Client). Refactor these to accept interfaces for HTTP fetching so they can be unit tested with mocks. This is the primary blocker for reaching 90% coverage.","type":"cleanup","status":"closed","priority":3,"labels":null,"assignee":"","created":"2026-02-13T03:54:37.630148644Z","updated":"2026-02-14T02:44:05.328784994Z"} {"id":"NK-7jh6re","title":"sidebar still ugly","description":"still very ugly, even compared to the original v1 static version\n\neither make it nicer or just copy the v1 version more directly","type":"feature","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-14T17:59:17.948112909Z","updated":"2026-02-14T18:01:26.48034794Z"} {"id":"NK-7tzbql","title":"Fix TUI Content View Navigation and Interaction","description":"The TUI content view (reading a single item) is currently non-functional or severely limited. Users cannot easily navigate back, scroll, or interact with the content. This task involves improving the 'viewContent' state in the TUI.","type":"bug","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-13T05:02:57.382793121Z","updated":"2026-02-13T05:06:15.144485446Z"} +{"id":"NK-7u97bb","title":"cleaning up old entries","description":"I have been running neko for so long that my production database is 1.4GB. Come up with a tool (ok to run it from command line) that purges some super old feed items to save space. think carefully about the algorithm! it should be accessible from the CLI to start, although maybe we should show \"space used\" in settings too with an option to clean up.","type":"feature","status":"open","priority":2,"labels":null,"assignee":"","created":"2026-02-15T03:01:05.643515805Z","updated":"2026-02-15T03:01:05.643515805Z"} {"id":"NK-7xuajb","title":"[security] Add HTTP Security Headers","description":"Add middleware to set standard security headers: Content-Security-Policy (restrict sources), X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: strict-origin-when-cross-origin.","type":"","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-14T16:35:59.320775688Z","updated":"2026-02-14T17:20:46.397582923Z"} -{"id":"NK-897v23","title":"Enhance UI with better loading indicators and error states","description":"The application should have a consistent and premium feel for loading and error states. Currently, it uses simple text. We should implement skeleton screens or more polished animations.","type":"task","status":"open","priority":3,"labels":null,"assignee":"","created":"2026-02-14T22:49:05.942464799Z","updated":"2026-02-14T22:49:05.942464799Z"} -{"id":"NK-8d1uzw","title":"Clean up unused font CSS variables","description":"The font CSS variables might have duplicates or unused entries after the fix. Audit them.","type":"task","status":"open","priority":3,"labels":null,"assignee":"","created":"2026-02-15T02:24:06.398437323Z","updated":"2026-02-15T02:24:06.398437323Z"} +{"id":"NK-897v23","title":"Enhance UI with better loading indicators and error states","description":"The application should have a consistent and premium feel for loading and error states. Currently, it uses simple text. We should implement skeleton screens or more polished animations.","type":"task","status":"closed","priority":3,"labels":null,"assignee":"","created":"2026-02-14T22:49:05.942464799Z","updated":"2026-02-14T22:49:05.942464799Z"} +{"id":"NK-8d1uzw","title":"Clean up unused font CSS variables","description":"The font CSS variables might have duplicates or unused entries after the fix. Audit them.","type":"task","status":"open","priority":2,"labels":null,"assignee":"","created":"2026-02-15T02:24:06.398437323Z","updated":"2026-02-15T02:24:06.398437323Z"} {"id":"NK-8hu7z1","title":"scrape full text button","description":"add this feature back in to the v2 ui and verify it is still working (not sure if we have any tests)","type":"feature","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-14T17:27:49.815938946Z","updated":"2026-02-14T17:58:19.083695149Z"} {"id":"NK-8rhpp3","title":"v2 frontend: when selected, don't change style of feed items","description":"Just leave them the same when j/k \"selects\" an item. No blue side thing, no change in background, it's distracting. Just scroll it to the right place.","type":"bug","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-14T00:39:50.01934312Z","updated":"2026-02-14T01:02:54.204739756Z"} {"id":"NK-8s75ec","title":"page size and performance","description":"Do some analysis of page size (css/html/javascript) on the legacy version vs. new version and give me a report. We want it to be small and fast! If the new version is much worse file some tickets to investigate further.","type":"task","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-13T20:16:13.898081788Z","updated":"2026-02-13T21:50:12.004391671Z"} @@ -51,7 +52,9 @@ {"id":"NK-fkc119","title":"setup github ci","description":"Maybe it'd be nice to have github run the tests. Is that a thing we can try to setup","type":"feature","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-14T03:16:32.574415787Z","updated":"2026-02-14T03:23:01.837550873Z"} {"id":"NK-fm15vq","title":"UI: Improve accessibility for star icon","description":"The new star button should have proper aria-labels and potentially better focus states for screen readers.","type":"task","status":"closed","priority":3,"labels":null,"assignee":"","created":"2026-02-13T20:27:36.768034045Z","updated":"2026-02-13T20:27:36.768034045Z"} {"id":"NK-fnaohu","title":"UI Styling: Dark Mode Support","description":"","type":"","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-13T18:05:19.59504351Z","updated":"2026-02-13T18:11:46.326064329Z"} +{"id":"NK-fnbaaf","title":"stress test with a REALLY big database","description":"I have been using a version of neko for over a decade and my prod database is 1.4GB sqlite file. it's in big_neko.db\n\nrun some tests with that big database and see if anything messes up! DO NOT CHECK IN THAT BIG DATABASE EVER.\n\nif there are bugs or performance issues with such a big db, file some tickets for us to evaluate and we can create some synthetic data or issues later on.","type":"task","status":"open","priority":1,"labels":null,"assignee":"","created":"2026-02-15T02:58:54.859954523Z","updated":"2026-02-15T02:58:54.859954523Z"} {"id":"NK-fpzx66","title":"v2 ui - title styling","description":"The title of the article stays blue and bold regardless of read state.","type":"bug","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-14T03:22:55.339956853Z","updated":"2026-02-14T03:28:01.555909701Z"} +{"id":"NK-fzjyay","title":"github CI fails","description":"Both the backend and frontend failed the CI jobs failed the lint checks.\n\nare we running those properly locally before submitting\n\nalso the frontent consistency check failed","type":"task","status":"open","priority":0,"labels":null,"assignee":"","created":"2026-02-15T02:41:02.747760248Z","updated":"2026-02-15T02:41:02.747760248Z"} {"id":"NK-g7ya0n","title":"Style Settings page to match Glass sidebar","description":"The settings page currently uses the old retro aesthetic. It should be updated to match the new minimalist 'glass' sidebar style for visual consistency. This should include updating form elements, backgrounds, and typography.","type":"feature","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-14T23:53:46.590222566Z","updated":"2026-02-15T00:51:14.771780934Z"} {"id":"NK-g818qn","title":"Improve mobile responsiveness of React UI","description":"The React UI should be fully responsive and work well on small screens. Now that the vanilla JS prototype is removed, we should ensure the main interface is a great experience for mobile users.","type":"task","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-14T22:46:32.850472479Z","updated":"2026-02-14T22:49:01.411224187Z"} {"id":"NK-gdf99z","title":"TUI is terrible and needs fixing","description":"The TUI doesn't really work and doesn't make sense. Think very hard and look at the v2 HTML UI implementation and make something cool like that. Probably needs tests too.","type":"epic","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-14T03:51:59.882212859Z","updated":"2026-02-14T04:31:28.290051717Z"} @@ -88,6 +91,7 @@ {"id":"NK-pwogze","title":"Crawler testing","description":"The general usage of neko is to run it and have it crawl feeds in the background after X minutes\n\nDo we have a test that can verify that's happening","type":"task","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-14T18:43:04.957220621Z","updated":"2026-02-14T20:44:09.76303647Z"} {"id":"NK-qwef98","title":"UI Styling: Controls \u0026 Header","description":"","type":"","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-13T18:05:18.450759919Z","updated":"2026-02-13T18:11:46.291830432Z"} {"id":"NK-r1aqiw","title":"Implement Subscription List and UserInfo endpoints","description":"","type":"task","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-15T00:21:51.619650383Z","updated":"2026-02-15T00:44:41.428045944Z"} +{"id":"NK-r39tqq","title":"username + password","description":"it's too weird to have just a password -- in the old UI i had a username but it was just ignored. but password managers get confused by this new no username thing.\n\nlet's make it so you can enter a username and password. to start, just let that be a no-op (it ignores the username and just pays attention to the password.)\n\nwe can consider later on if we want to make the username real and definable too.","type":"bug","status":"open","priority":1,"labels":null,"assignee":"","created":"2026-02-15T02:56:35.68970604Z","updated":"2026-02-15T02:56:35.68970604Z"} {"id":"NK-r6nhj0","title":"import/export","description":"Import/Export has only ever been partially implemented. Let's finish it up across OPML (de facto standard) but also simple txt line oriented input/output. We may need to file a ticket to deal with the async crawling as part of this.","type":"feature","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-14T16:45:04.739162003Z","updated":"2026-02-14T17:42:20.713094047Z"} {"id":"NK-rhelrq","title":"Add end-to-end integration test for complete crawl cycle","description":"Create an integration test that verifies the complete crawl workflow: start server with background crawling enabled, add a feed via API, wait for background crawl to execute, verify items are fetched and stored. This would require mocking or using a test RSS feed and potentially adjusting timing for faster test execution.","type":"task","status":"open","priority":3,"labels":null,"assignee":"","created":"2026-02-14T20:44:31.052207214Z","updated":"2026-02-14T20:44:31.052207214Z"} {"id":"NK-ric1zs","title":"Migrate frontend to /api/ endpoints","description":"The backend now provides a clean REST API at /api/. Update the frontend UI to use these new endpoints instead of the legacy backward-compatibility routes (/stream/, /feed/, etc.). This will allow for cleaner separation and better utilization of proper REST patterns.","type":"cleanup","status":"closed","priority":3,"labels":null,"assignee":"","created":"2026-02-13T04:26:55.864725765Z","updated":"2026-02-13T04:26:55.864725765Z"} @@ -100,6 +104,7 @@ {"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"} {"id":"NK-t7m31s","title":"Wire Reader API into web.go","description":"","type":"task","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-15T00:21:55.393639254Z","updated":"2026-02-15T00:44:41.579714853Z"} +{"id":"NK-tgmc9s","title":"make sure the github CI jobs are included in the tests/jobs locally!","description":"","type":"task","status":"open","priority":0,"labels":null,"assignee":"","created":"2026-02-15T02:44:00.333972105Z","updated":"2026-02-15T02:44:00.333972105Z"} {"id":"NK-thq2oq","title":"v2 ui - font size adjustments","description":"Move font-size: 18px to :root so rem units resolve correctly. Adjust title size to ~24px.","type":"bug","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-14T03:30:58.751447802Z","updated":"2026-02-14T03:31:56.358775833Z"} {"id":"NK-tw0nga","title":"E2E Testing","description":"Set up E2E testing with Playwright or Cypress to verify full flows: Login -\u003e View Feeds -\u003e View Items -\u003e Logout","type":"","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-13T15:01:33.817314728Z","updated":"2026-02-13T15:46:57.094062908Z"} {"id":"NK-ucckki","title":"security changes broke legacy","description":"I think some of the security policies make it so the old legacy one doesn't work. this may just be WAI but have a look\n\n[Warning] jQuery.Deferred exception: Refused to evaluate a string as JavaScript because 'unsafe-eval' or 'trusted-types-eval' is not an allowed source of script in the following Content Security Policy directive: \"script-src 'self'\". (jquery-3.3.1.min.js, line 2)\n (2)\n\"Function@[native code]\no@http://localhost:4994/static/jquery.tmpl.min.js:10:3543\ntemplate@http://localhost:4994/static/jquery.tmpl.min.js:10:1914\ntmpl@http://localhost:4994/static/jquery.tmpl.min.js:10:1422\nrender@http://localhost:4994/static/ui.js:208:23\nnr@http://localhost:4994/static/underscore-1.13.1.min.js:6:7308\n@http://localhost:4994/static/underscore-1.13.1.min.js:6:7733\n@http://localhost:4994/static/underscore-1.13.1.min.js:6:786\nboot@http://localhost:4994/static/ui.js:598:28\n@http://localhost:4994/static/ui.js:8:9\nl@http://localhost:4994/static/jquery-3.3.1.min.js:2:29380\n@http://localhost:4994/static/jquery-3.3.1.min.js:2:29678\"\nundefined","type":"bug","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-14T17:41:02.255772514Z","updated":"2026-02-14T17:41:02.255772514Z"} @@ -26,16 +26,17 @@ - [Building](#building) - [Docker](#docker) - [Build from Source](#build-from-source) + - [Development and Testing](#development-and-testing) - [Configuration](#configuration) - [Storage](#storage) - [Usage](#usage) - [Web Interface](#web-interface) + - [Sidebar Variants](#sidebar-variants) - [Add Feed](#add-feed) - [Crawl Feeds](#crawl-feeds) - [Export](#export) - [All Command Line Options](#all-command-line-options) - [Configuration File](#configuration-file) -- [TODO](#todo) - [History](#history) - [Early 2017](#early-2017) - [July 2018 -- v0.2](#july-2018----v02) @@ -65,7 +66,6 @@ Backend is written in `Go` and there is a modern `React/Vite` SPA frontend. * automatically marks items read in an infinite stream of never-ending content (until you run out of content and it ends) * full text search * scrapes full text of pages on demand - * multiple sidebar variants (Glass, Minimal, Swiss/Type, and Playful/Banana) * collapsible sidebar sections for Feeds and Tags ## Screenshots @@ -276,20 +276,6 @@ password: VeryLongRandomStringBecauseSecurityIsFun ``` - -# TODO - - * manually initiate crawl/refresh from web interface (done) - * auto-refresh feeds from web interface (done) - * import (done) - * mark all as read (done) - * rewrite frontend in a modern js framework (done: React/Vite) - * prettify interface (done) - * sidebar variants and personalization (done) - * cross-compilation of binaries for "normal" platforms - * implement Gzip compression (done) - * unit and E2E testing infrastructure (done) - # History ## Early 2017 diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 043ab7a..d2de7f8 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -6,7 +6,7 @@ import tseslint from 'typescript-eslint'; import eslintConfigPrettier from 'eslint-config-prettier'; export default tseslint.config( - { ignores: ['dist'] }, + { ignores: ['dist', 'coverage', 'playwright-report'] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], files: ['**/*.{ts,tsx}'], @@ -21,6 +21,11 @@ export default tseslint.config( rules: { ...reactHooks.configs.recommended.rules, 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + '@typescript-eslint/no-unused-vars': ['warn', { + argsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_' + }], + '@typescript-eslint/no-explicit-any': 'warn', }, }, eslintConfigPrettier diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 196f32a..1ef9763 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -11,20 +11,21 @@ describe('App', () => { }); it('renders login on initial load (unauthenticated)', async () => { - (global.fetch as any).mockResolvedValueOnce({ + vi.mocked(global.fetch).mockResolvedValueOnce({ ok: false, - }); + } as Response); window.history.pushState({}, 'Test page', '/v2/login'); render(<App />); expect(screen.getByRole('button', { name: /login/i })).toBeInTheDocument(); }); it('renders dashboard when authenticated', async () => { - (global.fetch as any).mockImplementation((url: string) => { - if (url.includes('/api/auth')) return Promise.resolve({ ok: true }); - if (url.includes('/api/feed/')) return Promise.resolve({ ok: true, json: async () => [] }); - if (url.includes('/api/tag')) return Promise.resolve({ ok: true, json: async () => [] }); - return Promise.resolve({ ok: true }); // Fallback + vi.mocked(global.fetch).mockImplementation((url) => { + const urlStr = url.toString(); + if (urlStr.includes('/api/auth')) return Promise.resolve({ ok: true } as Response); + if (urlStr.includes('/api/feed/')) return Promise.resolve({ ok: true, json: async () => [] } as Response); + if (urlStr.includes('/api/tag')) return Promise.resolve({ ok: true, json: async () => [] } as Response); + return Promise.resolve({ ok: true } as Response); // Fallback }); window.history.pushState({}, 'Test page', '/v2/'); @@ -44,7 +45,7 @@ describe('App', () => { value: { href: '' }, }); - (global.fetch as any).mockResolvedValueOnce({ ok: true }); + vi.mocked(global.fetch).mockResolvedValueOnce({ ok: true } as Response); fireEvent.click(logoutBtn); diff --git a/frontend/src/components/FeedItem.test.tsx b/frontend/src/components/FeedItem.test.tsx index 4c7d887..1c51dc3 100644 --- a/frontend/src/components/FeedItem.test.tsx +++ b/frontend/src/components/FeedItem.test.tsx @@ -31,7 +31,7 @@ describe('FeedItem Component', () => { }); it('toggles star status', async () => { - (global.fetch as any).mockResolvedValueOnce({ ok: true, json: async () => ({}) }); + vi.mocked(global.fetch).mockResolvedValueOnce({ ok: true, json: async () => ({}) } as Response); render(<FeedItem item={mockItem} />); @@ -73,10 +73,10 @@ describe('FeedItem Component', () => { }); it('loads full content', async () => { - (global.fetch as any).mockResolvedValueOnce({ + vi.mocked(global.fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ ...mockItem, full_content: '<p>Full Content Loaded</p>' }), - }); + } as Response); render(<FeedItem item={mockItem} />); diff --git a/frontend/src/components/FeedItems.test.tsx b/frontend/src/components/FeedItems.test.tsx index cf1e708..25ade58 100644 --- a/frontend/src/components/FeedItems.test.tsx +++ b/frontend/src/components/FeedItems.test.tsx @@ -17,11 +17,12 @@ describe('FeedItems Component', () => { unobserve = vi.fn(); disconnect = vi.fn(); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any window.IntersectionObserver = MockIntersectionObserver as any; }); it('renders loading state', () => { - (global.fetch as any).mockImplementation(() => new Promise(() => { })); + vi.mocked(global.fetch).mockImplementation(() => new Promise(() => { })); render( <MemoryRouter initialEntries={['/feed/1']}> <Routes> @@ -50,10 +51,10 @@ describe('FeedItems Component', () => { }, ]; - (global.fetch as any).mockResolvedValueOnce({ + vi.mocked(global.fetch).mockResolvedValueOnce({ ok: true, json: async () => mockItems, - }); + } as Response); render( <MemoryRouter initialEntries={['/feed/1']}> @@ -79,10 +80,10 @@ describe('FeedItems Component', () => { { _id: 102, title: 'Item 2', url: 'u2', read: true, starred: false }, ]; - (global.fetch as any).mockResolvedValue({ + vi.mocked(global.fetch).mockResolvedValue({ ok: true, json: async () => mockItems, - }); + } as Response); render( <MemoryRouter> @@ -134,10 +135,10 @@ describe('FeedItems Component', () => { it('marks items as read when scrolled past', async () => { const mockItems = [{ _id: 101, title: 'Item 1', url: 'u1', read: false, starred: false }]; - (global.fetch as any).mockResolvedValue({ + vi.mocked(global.fetch).mockResolvedValue({ ok: true, json: async () => mockItems, - }); + } as Response); // Capture both callbacks const observerCallbacks: IntersectionObserverCallback[] = []; @@ -151,8 +152,10 @@ describe('FeedItems Component', () => { unobserve = vi.fn(); disconnect = vi.fn(); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any window.IntersectionObserver = MockIntersectionObserver as any; + render( <MemoryRouter> <FeedItems /> @@ -201,9 +204,9 @@ describe('FeedItems Component', () => { const initialItems = [{ _id: 101, title: 'Item 1', url: 'u1', read: true, starred: false }]; const moreItems = [{ _id: 100, title: 'Item 0', url: 'u0', read: true, starred: false }]; - (global.fetch as any) - .mockResolvedValueOnce({ ok: true, json: async () => initialItems }) - .mockResolvedValueOnce({ ok: true, json: async () => moreItems }); + vi.mocked(global.fetch) + .mockResolvedValueOnce({ ok: true, json: async () => initialItems } as Response) + .mockResolvedValueOnce({ ok: true, json: async () => moreItems } as Response); const observerCallbacks: IntersectionObserverCallback[] = []; class MockIntersectionObserver { @@ -214,8 +217,10 @@ describe('FeedItems Component', () => { unobserve = vi.fn(); disconnect = vi.fn(); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any window.IntersectionObserver = MockIntersectionObserver as any; + render( <MemoryRouter> <FeedItems /> @@ -267,9 +272,9 @@ describe('FeedItems Component', () => { { _id: 100, title: 'Item 0', url: 'u0', read: true, starred: false }, ]; - (global.fetch as any) - .mockResolvedValueOnce({ ok: true, json: async () => initialItems }) - .mockResolvedValueOnce({ ok: true, json: async () => moreItems }); + vi.mocked(global.fetch) + .mockResolvedValueOnce({ ok: true, json: async () => initialItems } as Response) + .mockResolvedValueOnce({ ok: true, json: async () => moreItems } as Response); render( <MemoryRouter> diff --git a/frontend/src/components/FeedItems.tsx b/frontend/src/components/FeedItems.tsx index f43852b..8c69905 100644 --- a/frontend/src/components/FeedItems.tsx +++ b/frontend/src/components/FeedItems.tsx @@ -89,6 +89,7 @@ export default function FeedItems() { useEffect(() => { fetchItems(); setSelectedIndex(-1); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [feedId, tagName, filterFn, searchParams]); @@ -166,6 +167,7 @@ export default function FeedItems() { window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [items, hasMore, loadingMore]); @@ -214,6 +216,7 @@ export default function FeedItems() { itemObserver.disconnect(); sentinelObserver.disconnect(); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [items, loadingMore, hasMore]); if (loading) return <div className="feed-items-loading">Loading items...</div>; diff --git a/frontend/src/components/FeedList.test.tsx b/frontend/src/components/FeedList.test.tsx index 059d8a4..9ef2349 100644 --- a/frontend/src/components/FeedList.test.tsx +++ b/frontend/src/components/FeedList.test.tsx @@ -13,11 +13,15 @@ describe('FeedList Component', () => { }); it('renders loading state initially', () => { - (global.fetch as any).mockImplementation(() => new Promise(() => { })); + vi.mocked(global.fetch).mockImplementation(() => new Promise(() => { })); render( <BrowserRouter> - {/* @ts-ignore */} - <FeedList theme="light" setTheme={() => { }} /> + <FeedList + theme="light" + setTheme={() => { }} + setSidebarVisible={() => { }} + isMobile={false} + /> </BrowserRouter> ); expect(screen.getByText(/loading feeds/i)).toBeInTheDocument(); @@ -41,26 +45,31 @@ describe('FeedList Component', () => { }, ]; - (global.fetch as any).mockImplementation((url: string) => { - if (url.includes('/api/feed/')) { + vi.mocked(global.fetch).mockImplementation((url) => { + const urlStr = url.toString(); + if (urlStr.includes('/api/feed/')) { return Promise.resolve({ ok: true, json: async () => mockFeeds, - }); + } as Response); } - if (url.includes('/api/tag')) { + if (urlStr.includes('/api/tag')) { return Promise.resolve({ ok: true, json: async () => [{ title: 'Tech' }], - }); + } as Response); } return Promise.reject(new Error(`Unknown URL: ${url}`)); }); render( <BrowserRouter> - {/* @ts-ignore */} - <FeedList theme="light" setTheme={() => { }} /> + <FeedList + theme="light" + setTheme={() => { }} + setSidebarVisible={() => { }} + isMobile={false} + /> </BrowserRouter> ); @@ -80,12 +89,16 @@ describe('FeedList Component', () => { }); it('handles fetch error', async () => { - (global.fetch as any).mockImplementation(() => Promise.reject(new Error('API Error'))); + vi.mocked(global.fetch).mockImplementation(() => Promise.reject(new Error('API Error'))); render( <BrowserRouter> - {/* @ts-ignore */} - <FeedList theme="light" setTheme={() => { }} setSidebarVisible={() => { }} /> + <FeedList + theme="light" + setTheme={() => { }} + setSidebarVisible={() => { }} + isMobile={false} + /> </BrowserRouter> ); @@ -95,26 +108,31 @@ describe('FeedList Component', () => { }); it('handles empty feed list', async () => { - (global.fetch as any).mockImplementation((url: string) => { - if (url.includes('/api/feed/')) { + vi.mocked(global.fetch).mockImplementation((url) => { + const urlStr = url.toString(); + if (urlStr.includes('/api/feed/')) { return Promise.resolve({ ok: true, json: async () => [], - }); + } as Response); } - if (url.includes('/api/tag')) { + if (urlStr.includes('/api/tag')) { return Promise.resolve({ ok: true, json: async () => [], - }); + } as Response); } return Promise.reject(new Error(`Unknown URL: ${url}`)); }); render( <BrowserRouter> - {/* @ts-ignore */} - <FeedList theme="light" setTheme={() => { }} setSidebarVisible={() => { }} /> + <FeedList + theme="light" + setTheme={() => { }} + setSidebarVisible={() => { }} + isMobile={false} + /> </BrowserRouter> ); diff --git a/frontend/src/components/FeedList.tsx b/frontend/src/components/FeedList.tsx index a4ecccf..fed9196 100644 --- a/frontend/src/components/FeedList.tsx +++ b/frontend/src/components/FeedList.tsx @@ -66,11 +66,11 @@ export default function FeedList({ Promise.all([ apiFetch('/api/feed/').then((res) => { if (!res.ok) throw new Error('Failed to fetch feeds'); - return res.json(); + return res.json() as Promise<Feed[]>; }), apiFetch('/api/tag').then((res) => { if (!res.ok) throw new Error('Failed to fetch tags'); - return res.json(); + return res.json() as Promise<Category[]>; }), ]) .then(([feedsData, tagsData]) => { diff --git a/frontend/src/components/Login.test.tsx b/frontend/src/components/Login.test.tsx index aea7042..cf69eb1 100644 --- a/frontend/src/components/Login.test.tsx +++ b/frontend/src/components/Login.test.tsx @@ -28,9 +28,9 @@ describe('Login Component', () => { }); it('handles successful login', async () => { - (global.fetch as any).mockResolvedValueOnce({ + vi.mocked(global.fetch).mockResolvedValueOnce({ ok: true, - }); + } as Response); renderLogin(); @@ -51,10 +51,10 @@ describe('Login Component', () => { }); it('handles failed login', async () => { - (global.fetch as any).mockResolvedValueOnce({ + vi.mocked(global.fetch).mockResolvedValueOnce({ ok: false, json: async () => ({ message: 'Bad credentials' }), - }); + } as Response); renderLogin(); @@ -67,7 +67,7 @@ describe('Login Component', () => { }); it('handles network error', async () => { - (global.fetch as any).mockRejectedValueOnce(new Error('Network error')); + vi.mocked(global.fetch).mockRejectedValueOnce(new Error('Network error')); renderLogin(); diff --git a/frontend/src/components/Login.tsx b/frontend/src/components/Login.tsx index ba2cd96..b62acea 100644 --- a/frontend/src/components/Login.tsx +++ b/frontend/src/components/Login.tsx @@ -29,7 +29,7 @@ export default function Login() { const data = await res.json(); setError(data.message || 'Login failed'); } - } catch (err) { + } catch (_err) { setError('Network error'); } }; diff --git a/frontend/src/components/Settings.test.tsx b/frontend/src/components/Settings.test.tsx index b7de3bb..a0e7de4 100644 --- a/frontend/src/components/Settings.test.tsx +++ b/frontend/src/components/Settings.test.tsx @@ -18,10 +18,10 @@ describe('Settings Component', () => { { _id: 2, title: 'Gaming', url: 'http://gaming.com/rss', category: 'gaming' }, ]; - (global.fetch as any).mockResolvedValueOnce({ + vi.mocked(global.fetch).mockResolvedValueOnce({ ok: true, json: async () => mockFeeds, - }); + } as Response); render(<Settings />); @@ -33,13 +33,13 @@ describe('Settings Component', () => { }); it('adds a new feed', async () => { - (global.fetch as any) - .mockResolvedValueOnce({ ok: true, json: async () => [] }) // Initial load - .mockResolvedValueOnce({ ok: true, json: async () => ({}) }) // Add feed + vi.mocked(global.fetch) + .mockResolvedValueOnce({ ok: true, json: async () => [] } as Response) // Initial load + .mockResolvedValueOnce({ ok: true, json: async () => ({}) } as Response) // Add feed .mockResolvedValueOnce({ ok: true, json: async () => [{ _id: 3, title: 'New Feed', url: 'http://new.com/rss' }], - }); // Refresh load + } as Response); // Refresh load render(<Settings />); @@ -75,9 +75,9 @@ describe('Settings Component', () => { { _id: 1, title: 'Tech News', url: 'http://tech.com/rss', category: 'tech' }, ]; - (global.fetch as any) - .mockResolvedValueOnce({ ok: true, json: async () => mockFeeds }) // Initial load - .mockResolvedValueOnce({ ok: true }); // Delete + vi.mocked(global.fetch) + .mockResolvedValueOnce({ ok: true, json: async () => mockFeeds } as Response) // Initial load + .mockResolvedValueOnce({ ok: true } as Response); // Delete render(<Settings />); @@ -100,13 +100,13 @@ describe('Settings Component', () => { }); it('imports an OPML file', async () => { - (global.fetch as any) - .mockResolvedValueOnce({ ok: true, json: async () => [] }) // Initial load - .mockResolvedValueOnce({ ok: true, json: async () => ({ status: 'ok' }) }) // Import + vi.mocked(global.fetch) + .mockResolvedValueOnce({ ok: true, json: async () => [] } as Response) // Initial load + .mockResolvedValueOnce({ ok: true, json: async () => ({ status: 'ok' }) } as Response) // Import .mockResolvedValueOnce({ ok: true, json: async () => [{ _id: 1, title: 'Imported Feed', url: 'http://imported.com/rss' }], - }); // Refresh load + } as Response); // Refresh load render(<Settings />); diff --git a/frontend/src/components/Settings.tsx b/frontend/src/components/Settings.tsx index c174c32..b218775 100644 --- a/frontend/src/components/Settings.tsx +++ b/frontend/src/components/Settings.tsx @@ -18,7 +18,7 @@ export default function Settings({ fontTheme, setFontTheme }: SettingsProps) { const [importFile, setImportFile] = useState<File | null>(null); /* ... existing fetchFeeds ... */ - const fetchFeeds = () => { + const fetchFeeds = React.useCallback(() => { setLoading(true); apiFetch('/api/feed/') .then((res) => { @@ -33,11 +33,12 @@ export default function Settings({ fontTheme, setFontTheme }: SettingsProps) { setError(err.message); setLoading(false); }); - }; + }, []); useEffect(() => { + // eslint-disable-next-line fetchFeeds(); - }, []); + }, [fetchFeeds]); /* ... existing handlers ... */ const handleAddFeed = (e: React.FormEvent) => { diff --git a/frontend/src/components/TagView.test.tsx b/frontend/src/components/TagView.test.tsx index 8f7eb86..16fdee7 100644 --- a/frontend/src/components/TagView.test.tsx +++ b/frontend/src/components/TagView.test.tsx @@ -17,25 +17,31 @@ describe('Tag View Integration', () => { ]; const mockTags = [{ title: 'Tech' }, { title: 'News' }]; - (global.fetch as any).mockImplementation((url: string) => { - if (url.includes('/api/feed/')) { + vi.mocked(global.fetch).mockImplementation((url) => { + const urlStr = url.toString(); + if (urlStr.includes('/api/feed/')) { return Promise.resolve({ ok: true, json: async () => mockFeeds, - }); + } as Response); } - if (url.includes('/api/tag')) { + if (urlStr.includes('/api/tag')) { return Promise.resolve({ ok: true, json: async () => mockTags, - }); + } as Response); } return Promise.reject(new Error(`Unknown URL: ${url}`)); }); render( <MemoryRouter> - <FeedList theme="light" setTheme={() => { }} /> + <FeedList + theme="light" + setTheme={() => { }} + setSidebarVisible={() => { }} + isMobile={false} + /> </MemoryRouter> ); @@ -55,12 +61,13 @@ describe('Tag View Integration', () => { { _id: 101, title: 'Tag Item 1', url: 'http://example.com/1', feed_title: 'Feed 1' }, ]; - (global.fetch as any).mockImplementation((url: string) => { - if (url.includes('/api/stream')) { + vi.mocked(global.fetch).mockImplementation((url) => { + const urlStr = url.toString(); + if (urlStr.includes('/api/stream')) { return Promise.resolve({ ok: true, json: async () => mockItems, - }); + } as Response); } return Promise.reject(new Error(`Unknown URL: ${url}`)); }); diff --git a/web/dist/v2/assets/index-BYhh91HH.css b/web/dist/v2/assets/index-BYhh91HH.css deleted file mode 100644 index 13b2864..0000000 --- a/web/dist/v2/assets/index-BYhh91HH.css +++ /dev/null @@ -1 +0,0 @@ -:root{--font-body: Palatino, "Palatino Linotype", "Palatino LT STD", "Book Antiqua", Georgia, serif;--font-heading: "Helvetica Neue", Helvetica, Arial, sans-serif}body{font-family:var(--font-body)}h1,h2,h3,h4,h5,.logo,.nav-link,.logout-btn{font-family:var(--font-heading);font-weight:700}.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;--bg-color: #ffffff;--text-color: rgba(0, 0, 0, .87);--sidebar-bg: #ccc;--link-color: #0000ee;color-scheme:light dark;color:var(--text-color);background-color:var(--bg-color)}.theme-light{--bg-color: #ffffff;--text-color: rgba(0, 0, 0, .87);--sidebar-bg: #ccc;--link-color: #0000ee;--border-color: #999;background-color:var(--bg-color);color:var(--text-color)}@media(prefers-color-scheme:dark){:root{--bg-color: #24292e;--text-color: #ffffff;--sidebar-bg: #1b1f23;--link-color: rgb(90, 200, 250)}}.theme-dark{--bg-color: #000000;--text-color: #ffffff;--sidebar-bg: #111111;--link-color: rgb(90, 200, 250);--border-color: #333;background-color:var(--bg-color);color:var(--text-color)}.theme-dark button{background-color:#333;color:#fff}body{margin:0;min-width:320px;min-height:100vh}h1{font-size:3.2em;line-height:1.1}button{border-radius:8px;border:1px solid transparent;padding:.6em 1.2em;font-size:1em;font-weight:700;font-family:inherit;background-color:#1a1a1a;cursor:pointer;transition:border-color .25s}button:hover{border-color:#646cff}button:focus,button:focus-visible{outline:4px auto -webkit-focus-ring-color}a{color:var(--link-color);text-decoration:none}@media(prefers-color-scheme:light){:root{color:#213547;background-color:#fff}a:hover{color:#00f;text-decoration:underline}button{background-color:#f9f9f9}}.login-container{display:flex;justify-content:center;align-items:center;height:100vh;background-color:#f5f5f5}.login-form{background:#fff;padding:2rem;border-radius:8px;box-shadow:0 4px 6px #0000001a;width:100%;max-width:400px}.login-form h1{margin-bottom:2rem;text-align:center;color:#333}.form-group{margin-bottom:1.5rem}.form-group label{display:block;margin-bottom:.5rem;font-weight:700;color:#555}.form-group input{width:100%;padding:.75rem;border:1px solid #ddd;border-radius:4px;font-size:1rem}.error-message{color:#dc3545;margin-bottom:1rem;text-align:center}button[type=submit]{width:100%;padding:.75rem;background-color:#007bff;color:#fff;border:none;border-radius:4px;font-size:1rem;cursor:pointer;transition:background-color .2s}button[type=submit]:hover{background-color:#0056b3}*{box-sizing:border-box}body{margin:0}.dashboard{display:flex;flex-direction:column;height:100vh;overflow:hidden}.dashboard-content{display:flex;flex:1;overflow:hidden;position:relative}.dashboard-sidebar{width:11rem;background:transparent;border-right:1px solid var(--border-color);display:flex;flex-direction:column;overflow-y:auto;transition:margin-left .4s ease}.dashboard-sidebar.hidden{margin-left:-11rem}.dashboard-main{flex:1;padding:2rem;overflow-y:auto;background:var(--bg-color);margin-left:0}.dashboard-main>*{max-width:35em;margin:0 auto}.fixed-toggle{position:absolute;top:1rem;left:1rem;z-index:1000;background:var(--bg-color);border:none;font-size:2rem;line-height:1;cursor:pointer;padding:.2rem;color:var(--text-color);border-radius:50%;box-shadow:0 2px 5px #0000001a;display:flex;align-items:center;justify-content:center}.fixed-toggle:hover{transform:scale(1.1)}@media(max-width:768px){.dashboard-sidebar{position:fixed;top:0;left:0;bottom:0;z-index:1100;box-shadow:2px 0 10px #0003;width:14rem}.dashboard-sidebar.hidden{margin-left:-14rem}.dashboard-main{padding:4rem 1rem 1rem}.dashboard-main>*{max-width:100%}.sidebar-backdrop{position:fixed;inset:0;background:#0006;z-index:1050;animation:fadeIn .3s ease}.dashboard.sidebar-visible:after{display:none}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.feed-list{padding:1rem;font-family:var(--font-heading);color:#777;font-size:.8rem;background:var(--sidebar-bg);min-height:100%;flex:1}.feed-list h1.logo{font-size:2rem;margin:0 0 1rem;line-height:1;cursor:pointer;position:sticky;top:0;background:var(--sidebar-bg);z-index:10;padding-bottom:.5rem;color:var(--text-color)}.theme-light .feed-list h1.logo{color:#333}.theme-dark .feed-list h1.logo{color:#eee}.search-section{margin-bottom:1rem}.search-input{width:100%;padding:.25rem;border:1px solid var(--border-color, #999);background:var(--bg-color);color:var(--text-color);font-size:.8rem;font-family:inherit;border-radius:0}.section-header{font-size:1rem;font-weight:700;margin:1rem 0 .25rem;cursor:pointer;-webkit-user-select:none;user-select:none;font-family:var(--font-heading);color:#333;text-transform:lowercase;font-variant:small-caps;display:flex;align-items:center;gap:.5rem}.caret{display:inline-block;font-size:.6rem;transition:transform .2s ease;color:#777}.caret.expanded{transform:rotate(90deg)}.filter-list,.tag-list-items,.feed-list-items,.nav-list{list-style:none;padding:0;margin:0}.filter-list li,.nav-list li{margin-bottom:.1rem}.filter-list a,.nav-list a,.tag-link,.feed-title,.logout-link{text-decoration:none;color:var(--link-color, blue);font-size:.8rem;display:block;cursor:pointer;background:none;border:none;padding:0;font-family:inherit;font-variant:small-caps;text-transform:lowercase;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.filter-list a:hover,.nav-list a:hover,.tag-link:hover,.feed-title:hover,.logout-link:hover{text-decoration:underline;color:var(--link-color, blue)}.filter-list a.active,.tag-link.active,.feed-title.active{font-weight:700;color:#000}.tag-item,.sidebar-feed-item{margin-bottom:0}.feed-category{display:none}.nav-section{margin-top:2rem;border-top:1px solid var(--border-color, #eee);padding-top:1rem}.logout-link{text-align:left;width:100%;color:#777;display:block}.nav-link,.logout-link{padding:.25rem 0}.logout-link:hover{color:var(--link-color, blue);text-decoration:underline}.theme-section{margin-top:1rem}.theme-selector{display:flex;gap:.5rem;margin-top:.5rem}.theme-selector button{background:#0000000d;border:none;cursor:pointer;padding:.4rem;font-size:1rem;border-radius:8px;line-height:1;transition:all .2s ease;display:flex;align-items:center;justify-content:center}.theme-selector button:hover{background:#0000001a;transform:translateY(-2px)}.theme-selector button.active{background:var(--border-color, #999);color:#fff;box-shadow:0 4px 8px #0000001a}.theme-dark .theme-selector button{background:#ffffff1a}.theme-dark .theme-selector button:hover{background:#fff3}.dashboard-sidebar::-webkit-scrollbar{width:4px}.dashboard-sidebar::-webkit-scrollbar-thumb{background-color:var(--border-color, #999)}.feed-list.variant-glass{background:#ffffff0d;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-right:1px solid rgba(255,255,255,.1);padding:1.5rem;font-family:system-ui,-apple-system,sans-serif;color:var(--text-color)}.feed-list.variant-glass .logo{font-size:1.5rem;background:transparent!important;margin-bottom:2rem;opacity:.8}.feed-list.variant-glass .section-header{font-size:.75rem;text-transform:uppercase;letter-spacing:.1em;color:var(--text-color);opacity:.5;margin-top:2rem;font-weight:600}.feed-list.variant-glass a,.feed-list.variant-glass .feed-title,.feed-list.variant-glass .tag-link{padding:.4rem .8rem;margin:.2rem 0;border-radius:8px;transition:all .2s ease;font-weight:500;text-decoration:none!important;color:var(--text-color);opacity:.8;border:none}.feed-list.variant-glass a:hover,.feed-list.variant-glass .feed-title:hover,.feed-list.variant-glass .tag-link:hover{background:#ffffff1a;opacity:1;transform:translate(4px);color:var(--text-color)}.feed-list.variant-glass a.active,.feed-list.variant-glass .feed-title.active,.feed-list.variant-glass .tag-link.active{background:#ffffff40;color:var(--text-color);font-weight:700;opacity:1;box-shadow:0 4px 12px #0000001a;border:1px solid rgba(255,255,255,.2)}.feed-list.variant-glass .search-input{border-radius:20px;background:#0000000d;border:1px solid rgba(255,255,255,.1);color:var(--text-color);padding:.5rem 1rem}.feed-list.variant-glass .nav-section{border-top:1px solid rgba(255,255,255,.1);margin-top:2rem;padding-top:1.5rem}.feed-list.variant-glass .nav-link,.feed-list.variant-glass .logout-link{opacity:.6;padding:.5rem .8rem;border-radius:8px}.feed-list.variant-glass .nav-link:hover,.feed-list.variant-glass .logout-link:hover{background:#ffffff0d;opacity:1;text-decoration:none}.feed-list.variant-glass .theme-selector button{background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:12px}.feed-list.variant-glass .theme-selector button.active{background:#fff3;border-color:#ffffff4d}.feed-list.variant-banana{background:#fdfdfd;padding:1rem;font-family:Poppins,Verdana,sans-serif;border-right:none;box-shadow:4px 0 24px #0000000a}.theme-dark .feed-list.variant-banana{background:#111}.feed-list.variant-banana .logo{font-size:2.5rem;text-shadow:2px 2px 0px #FFD700;background:transparent;transform:rotate(-3deg);display:inline-block;margin-bottom:2rem}.feed-list.variant-banana .section-header{background:gold;color:#000;display:inline-block;padding:.2rem .5rem;transform:skew(-10deg);font-size:.8rem;font-weight:800;margin-bottom:1rem;border-radius:4px}.feed-list.variant-banana .search-input{border:2px solid #000;border-radius:8px;box-shadow:2px 2px #000;transition:all .2s}.feed-list.variant-banana .search-input:focus{transform:translate(1px,1px);box-shadow:1px 1px #000;outline:none}.theme-dark .feed-list.variant-banana .search-input{border-color:#fff;box-shadow:2px 2px #fff;background:#222;color:#fff}.feed-list.variant-banana a,.feed-list.variant-banana .feed-title,.feed-list.variant-banana .tag-link{border:1px solid transparent;padding:.5rem;border-radius:8px;transition:transform .2s cubic-bezier(.34,1.56,.64,1);font-weight:600;text-decoration:none!important;color:var(--text-color)}.feed-list.variant-banana a:hover,.feed-list.variant-banana .feed-title:hover,.feed-list.variant-banana .tag-link:hover{transform:scale(1.05) rotate(1deg);background:#fff9c4;color:#000;box-shadow:0 4px 12px #ffd7004d}.theme-dark .feed-list.variant-banana a:hover,.theme-dark .feed-list.variant-banana .feed-title:hover,.theme-dark .feed-list.variant-banana .tag-link:hover{background:#333;color:gold}.feed-list.variant-banana a.active,.feed-list.variant-banana .feed-title.active,.feed-list.variant-banana .tag-link.active{background:gold;color:#000!important;transform:scale(1.02);box-shadow:3px 3px #0000001a;border:2px solid #000}.feed-list.variant-banana .nav-section{border-top:2px dashed #FFD700;margin-top:2rem;padding-top:1rem}.feed-list.variant-banana .theme-selector button{border:2px solid #000;box-shadow:2px 2px #000;border-radius:4px}.feed-list.variant-banana .theme-selector button.active{background:gold;transform:translate(1px,1px);box-shadow:1px 1px #000}.feed-list.variant-type{background:var(--bg-color);padding:2rem 1rem;font-family:Helvetica Neue,Arial,sans-serif;border-right:4px solid var(--text-color)}.feed-list.variant-type .logo{font-size:3rem;letter-spacing:-2px;font-weight:900;background:transparent;line-height:.8;margin-bottom:3rem;color:var(--text-color)}.feed-list.variant-type .section-header{font-size:1.2rem;font-weight:900;border-bottom:2px solid var(--text-color);padding-bottom:.5rem;margin-top:3rem;margin-bottom:1rem;letter-spacing:-.5px;color:var(--text-color)}.feed-list.variant-type a,.feed-list.variant-type .feed-title,.feed-list.variant-type .tag-link{font-size:1rem;font-weight:700;text-decoration:none!important;border-left:0px solid var(--text-color);padding-left:0;transition:padding-left .2s,border-left-width .2s;opacity:.6;color:var(--text-color);padding:.5rem 0;display:block}.feed-list.variant-type a:hover,.feed-list.variant-type .feed-title:hover,.feed-list.variant-type .tag-link:hover{padding-left:1rem;border-left:4px solid var(--text-color);opacity:1;color:var(--text-color)}.feed-list.variant-type a.active,.feed-list.variant-type .feed-title.active,.feed-list.variant-type .tag-link.active{padding-left:1rem;border-left:8px solid var(--text-color);opacity:1;color:var(--text-color)}.feed-list.variant-type .search-input{border:none;border-bottom:2px solid var(--text-color);background:transparent;border-radius:0;padding:1rem 0;font-weight:700;font-size:1.2rem}.feed-list.variant-type .search-input:focus{outline:none;border-bottom-width:4px}.feed-list.variant-type .nav-section{border-top:4px solid var(--text-color);margin-top:4rem;padding-top:1rem}.feed-list.variant-type .nav-link,.feed-list.variant-type .logout-link{font-size:1.2rem;font-weight:900}.feed-list.variant-type .theme-selector button{border-radius:0;border:2px solid var(--text-color);background:transparent}.feed-list.variant-type .theme-selector button.active{background:var(--text-color);color:var(--bg-color)}.feed-item{padding:1rem;margin-top:5rem;list-style:none;border-bottom:none}.item-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:.5rem}.item-title{font-family:var(--font-heading);font-size:1.8rem;font-weight:700;text-decoration:none;color:var(--link-color);display:block;flex:1}.item-title:hover{text-decoration:none;color:var(--link-color)}.item-actions{display:flex;gap:.5rem;margin-left:1rem}.star-btn{background:none;border:none;cursor:pointer;font-size:1.25rem;padding:0 0 0 .5rem;vertical-align:middle;transition:color .2s;line-height:1}.star-btn.is-starred{color:#00f}.star-btn.is-unstarred{color:var(--text-color);opacity:.3}.star-btn:hover{color:#00f}.action-btn{background:var(--sidebar-bg);border:1px solid var(--border-color, #ccc);cursor:pointer;padding:2px 6px;font-size:1rem;color:#00f;font-weight:700}.action-btn:hover{background-color:#eee}.dateline{margin-top:0;font-weight:400;font-size:.75em;color:#ccc;margin-bottom:1rem}.dateline a{color:#ccc;text-decoration:none}.item-description{color:var(--text-color);line-height:1.5;font-size:1rem;margin-top:1rem}.item-description img{max-width:100%;height:auto;display:block;margin:1rem 0}.item-description blockquote{padding:1rem 1rem 0;border-left:4px solid var(--sidebar-bg);color:var(--text-color);opacity:.8;margin-left:0}.scrape-btn{background:var(--bg-color);border:1px solid var(--border-color, #ccc);color:#00f;cursor:pointer;font-family:var(--font-heading);font-weight:700;font-size:.8rem;padding:2px 6px;margin-left:.5rem}.scrape-btn:hover{background:var(--sidebar-bg)}@media(max-width:768px){.feed-item{margin-top:2rem;padding:.5rem}.item-title{font-size:1.4rem;word-break:break-word}.item-header{flex-direction:column;gap:.5rem}.item-actions{margin-left:0;margin-bottom:.5rem}}.feed-items{padding:1rem 0}.feed-items h2{margin-top:0;border-bottom:2px solid var(--border-color);padding-bottom:.5rem}.item-list{list-style:none;padding:0}.loading-more{padding:2rem;text-align:center;color:#888;font-size:.9rem;min-height:50px}.settings-page.variant-glass{padding:2.5rem;max-width:800px;margin:0 auto;background:#ffffff0d;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-radius:24px;border:1px solid rgba(255,255,255,.1);font-family:system-ui,-apple-system,sans-serif;color:var(--text-color);margin-top:2rem;margin-bottom:2rem}.settings-page.variant-glass h2,.settings-page.variant-glass h3{font-weight:700;letter-spacing:-.02em;color:var(--text-color);opacity:.9}.add-feed-section,.appearance-section,.import-section,.export-section,.feed-list-section{background:#ffffff08;padding:1.5rem;border-radius:16px;margin-bottom:2rem;border:1px solid rgba(255,255,255,.05);transition:all .3s ease}.add-feed-section:hover,.appearance-section:hover,.import-section:hover,.export-section:hover,.feed-list-section:hover{background:#ffffff0f;border-color:#ffffff1a}.font-selector{display:flex;align-items:center;gap:1rem}.font-select{padding:.6rem 1rem;border:1px solid rgba(255,255,255,.1);background:#0000001a;color:var(--text-color);border-radius:20px;font-size:1rem;min-width:200px;cursor:pointer;outline:none;transition:border-color .2s}.font-select:focus{border-color:#ffffff4d}.add-feed-form{display:flex;gap:1rem}.feed-input{flex:1;padding:.6rem 1.2rem;border:1px solid rgba(255,255,255,.1);background:#0000001a;color:var(--text-color);border-radius:20px;font-size:1rem;outline:none;transition:border-color .2s}.feed-input:focus{border-color:#ffffff4d}.error-message{color:#ff5252;margin-top:1rem;font-weight:600}.settings-feed-list{list-style:none;padding:0;border:1px solid rgba(255,255,255,.05);border-radius:12px;overflow:hidden}.settings-feed-item{display:flex;justify-content:space-between;align-items:center;padding:1.2rem;border-bottom:1px solid rgba(255,255,255,.05);transition:background .2s}.settings-feed-item:hover{background:#ffffff05}.settings-feed-item:last-child{border-bottom:none}.feed-info{display:flex;flex-direction:column;gap:.2rem}.feed-title{font-weight:600;font-size:1.05rem;opacity:.9}.feed-url{color:var(--text-color);opacity:.5;font-size:.85rem}.delete-btn{background:#ff525226;color:#ff8a80;border:1px solid rgba(255,82,82,.2);padding:.5rem 1rem;border-radius:12px;cursor:pointer;font-weight:600;transition:all .2s}.delete-btn:hover:not(:disabled){background:#ff52524d;color:#fff;border-color:#ff525266;transform:scale(1.05)}.import-export-section{display:flex;gap:2rem}@media(max-width:600px){.settings-page.variant-glass{padding:1.5rem;margin-top:1rem}.add-feed-form{flex-direction:column}.import-export-section{flex-direction:column;gap:1rem}.settings-feed-item{flex-direction:column;align-items:flex-start;gap:1rem}}.import-form{display:flex;flex-direction:column;gap:1.2rem}.file-input{font-size:.9rem;max-width:100%;color:var(--text-color);opacity:.8}.export-buttons{display:flex;gap:.8rem;flex-wrap:wrap}.export-btn{display:inline-block;padding:.6rem 1.2rem;background:#ffffff0d;color:var(--text-color);text-decoration:none;border:1px solid rgba(255,255,255,.1);border-radius:12px;font-weight:600;transition:all .2s}.export-btn:hover{background:#ffffff1a;transform:translateY(-2px);box-shadow:0 4px 12px #0000001a}button:not(.delete-btn){cursor:pointer;padding:.6rem 1.2rem;border-radius:12px;border:1px solid rgba(255,255,255,.1);background:#ffffff1a;color:var(--text-color);font-weight:600;transition:all .2s}button:not(.delete-btn):hover:not(:disabled){background:#fff3;transform:scale(1.02)}button:disabled{opacity:.4;cursor:not-allowed} diff --git a/web/dist/v2/assets/index-CO97K-eb.css b/web/dist/v2/assets/index-CO97K-eb.css new file mode 100644 index 0000000..98613f9 --- /dev/null +++ b/web/dist/v2/assets/index-CO97K-eb.css @@ -0,0 +1 @@ +:root{--font-body: Palatino, "Palatino Linotype", "Palatino LT STD", "Book Antiqua", Georgia, serif;--font-heading: "Helvetica Neue", Helvetica, Arial, sans-serif}body{font-family:var(--font-body)}h1,h2,h3,h4,h5,.logo,.nav-link,.logout-btn{font-family:var(--font-heading);font-weight:700}.font-default{--font-body: Palatino, "Palatino Linotype", "Palatino LT STD", "Book Antiqua", Georgia, serif;--font-heading: "Helvetica Neue", Helvetica, Arial, sans-serif;font-family:var(--font-body)}.font-serif{--font-body: Georgia, "Times New Roman", Times, serif;--font-heading: Georgia, "Times New Roman", Times, serif;font-family:var(--font-body)}.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-family:var(--font-body)}.font-mono{--font-body: Menlo, Monaco, Consolas, "Courier New", monospace;--font-heading: Menlo, Monaco, Consolas, "Courier New", monospace;font-family:var(--font-body)}:root{line-height:1.5;font-size:18px;--bg-color: #ffffff;--text-color: rgba(0, 0, 0, .87);--sidebar-bg: #ccc;--link-color: #0000ee;color-scheme:light dark;color:var(--text-color);background-color:var(--bg-color)}.theme-light{--bg-color: #ffffff;--text-color: rgba(0, 0, 0, .87);--sidebar-bg: #ccc;--link-color: #0000ee;--border-color: #999;background-color:var(--bg-color);color:var(--text-color)}@media(prefers-color-scheme:dark){:root{--bg-color: #24292e;--text-color: #ffffff;--sidebar-bg: #1b1f23;--link-color: rgb(90, 200, 250)}}.theme-dark{--bg-color: #000000;--text-color: #ffffff;--sidebar-bg: #111111;--link-color: rgb(90, 200, 250);--border-color: #333;background-color:var(--bg-color);color:var(--text-color)}.theme-dark button{background-color:#333;color:#fff}body{margin:0;min-width:320px;min-height:100vh}h1{font-size:3.2em;line-height:1.1}button{border-radius:8px;border:1px solid transparent;padding:.6em 1.2em;font-size:1em;font-weight:700;font-family:inherit;background-color:#1a1a1a;cursor:pointer;transition:border-color .25s}button:hover{border-color:#646cff}button:focus,button:focus-visible{outline:4px auto -webkit-focus-ring-color}a{color:var(--link-color);text-decoration:none}@media(prefers-color-scheme:light){:root{color:#213547;background-color:#fff}a:hover{color:#00f;text-decoration:underline}button{background-color:#f9f9f9}}.login-container{display:flex;justify-content:center;align-items:center;height:100vh;background-color:#f5f5f5}.login-form{background:#fff;padding:2rem;border-radius:8px;box-shadow:0 4px 6px #0000001a;width:100%;max-width:400px}.login-form h1{margin-bottom:2rem;text-align:center;color:#333}.form-group{margin-bottom:1.5rem}.form-group label{display:block;margin-bottom:.5rem;font-weight:700;color:#555}.form-group input{width:100%;padding:.75rem;border:1px solid #ddd;border-radius:4px;font-size:1rem}.error-message{color:#dc3545;margin-bottom:1rem;text-align:center}button[type=submit]{width:100%;padding:.75rem;background-color:#007bff;color:#fff;border:none;border-radius:4px;font-size:1rem;cursor:pointer;transition:background-color .2s}button[type=submit]:hover{background-color:#0056b3}*{box-sizing:border-box}body{margin:0}.dashboard{display:flex;flex-direction:column;height:100vh;overflow:hidden}.dashboard-content{display:flex;flex:1;overflow:hidden;position:relative}.dashboard-sidebar{width:11rem;background:transparent;border-right:1px solid var(--border-color);display:flex;flex-direction:column;overflow-y:auto;transition:margin-left .4s ease}.dashboard-sidebar.hidden{margin-left:-11rem}.dashboard-main{flex:1;padding:2rem;overflow-y:auto;background:var(--bg-color);margin-left:0}.dashboard-main>*{max-width:35em;margin:0 auto}.fixed-toggle{position:absolute;top:1rem;left:1rem;z-index:1000;background:var(--bg-color);border:none;font-size:2rem;line-height:1;cursor:pointer;padding:.2rem;color:var(--text-color);border-radius:50%;box-shadow:0 2px 5px #0000001a;display:flex;align-items:center;justify-content:center}.fixed-toggle:hover{transform:scale(1.1)}@media(max-width:768px){.dashboard-sidebar{position:fixed;top:0;left:0;bottom:0;z-index:1100;box-shadow:2px 0 10px #0003;width:14rem}.dashboard-sidebar.hidden{margin-left:-14rem}.dashboard-main{padding:4rem 1rem 1rem}.dashboard-main>*{max-width:100%}.sidebar-backdrop{position:fixed;inset:0;background:#0006;z-index:1050;animation:fadeIn .3s ease}.dashboard.sidebar-visible:after{display:none}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.feed-list{padding:1rem;font-family:var(--font-heading);color:#777;font-size:.8rem;background:var(--sidebar-bg);min-height:100%;flex:1}.feed-list h1.logo{font-size:2rem;margin:0 0 1rem;line-height:1;cursor:pointer;position:sticky;top:0;background:var(--sidebar-bg);z-index:10;padding-bottom:.5rem;color:var(--text-color)}.theme-light .feed-list h1.logo{color:#333}.theme-dark .feed-list h1.logo{color:#eee}.search-section{margin-bottom:1rem}.search-input{width:100%;padding:.25rem;border:1px solid var(--border-color, #999);background:var(--bg-color);color:var(--text-color);font-size:.8rem;font-family:inherit;border-radius:0}.section-header{font-size:1rem;font-weight:700;margin:1rem 0 .25rem;cursor:pointer;-webkit-user-select:none;user-select:none;font-family:var(--font-heading);color:#333;text-transform:lowercase;font-variant:small-caps;display:flex;align-items:center;gap:.5rem}.caret{display:inline-block;font-size:.6rem;transition:transform .2s ease;color:#777}.caret.expanded{transform:rotate(90deg)}.filter-list,.tag-list-items,.feed-list-items,.nav-list{list-style:none;padding:0;margin:0}.filter-list li,.nav-list li{margin-bottom:.1rem}.filter-list a,.nav-list a,.tag-link,.feed-title,.logout-link{text-decoration:none;color:var(--link-color, blue);font-size:.8rem;display:block;cursor:pointer;background:none;border:none;padding:0;font-family:inherit;font-variant:small-caps;text-transform:lowercase;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.filter-list a:hover,.nav-list a:hover,.tag-link:hover,.feed-title:hover,.logout-link:hover{text-decoration:underline;color:var(--link-color, blue)}.filter-list a.active,.tag-link.active,.feed-title.active{font-weight:700;color:#000}.tag-item,.sidebar-feed-item{margin-bottom:0}.feed-category{display:none}.nav-section{margin-top:2rem;border-top:1px solid var(--border-color, #eee);padding-top:1rem}.logout-link{text-align:left;width:100%;color:#777;display:block}.nav-link,.logout-link{padding:.25rem 0}.logout-link:hover{color:var(--link-color, blue);text-decoration:underline}.theme-section{margin-top:1rem}.theme-selector{display:flex;gap:.5rem;margin-top:.5rem}.theme-selector button{background:#0000000d;border:none;cursor:pointer;padding:.4rem;font-size:1rem;border-radius:8px;line-height:1;transition:all .2s ease;display:flex;align-items:center;justify-content:center}.theme-selector button:hover{background:#0000001a;transform:translateY(-2px)}.theme-selector button.active{background:var(--border-color, #999);color:#fff;box-shadow:0 4px 8px #0000001a}.theme-dark .theme-selector button{background:#ffffff1a}.theme-dark .theme-selector button:hover{background:#fff3}.dashboard-sidebar::-webkit-scrollbar{width:4px}.dashboard-sidebar::-webkit-scrollbar-thumb{background-color:var(--border-color, #999)}.feed-list.variant-glass{background:#ffffff0d;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-right:1px solid rgba(255,255,255,.1);padding:1.5rem;font-family:system-ui,-apple-system,sans-serif;color:var(--text-color)}.feed-list.variant-glass .logo{font-size:1.5rem;background:transparent!important;margin-bottom:2rem;opacity:.8}.feed-list.variant-glass .section-header{font-size:.75rem;text-transform:uppercase;letter-spacing:.1em;color:var(--text-color);opacity:.5;margin-top:2rem;font-weight:600}.feed-list.variant-glass a,.feed-list.variant-glass .feed-title,.feed-list.variant-glass .tag-link{padding:.4rem .8rem;margin:.2rem 0;border-radius:8px;transition:all .2s ease;font-weight:500;text-decoration:none!important;color:var(--text-color);opacity:.8;border:none}.feed-list.variant-glass a:hover,.feed-list.variant-glass .feed-title:hover,.feed-list.variant-glass .tag-link:hover{background:#ffffff1a;opacity:1;transform:translate(4px);color:var(--text-color)}.feed-list.variant-glass a.active,.feed-list.variant-glass .feed-title.active,.feed-list.variant-glass .tag-link.active{background:#ffffff40;color:var(--text-color);font-weight:700;opacity:1;box-shadow:0 4px 12px #0000001a;border:1px solid rgba(255,255,255,.2)}.feed-list.variant-glass .search-input{border-radius:20px;background:#0000000d;border:1px solid rgba(255,255,255,.1);color:var(--text-color);padding:.5rem 1rem}.feed-list.variant-glass .nav-section{border-top:1px solid rgba(255,255,255,.1);margin-top:2rem;padding-top:1.5rem}.feed-list.variant-glass .nav-link,.feed-list.variant-glass .logout-link{opacity:.6;padding:.5rem .8rem;border-radius:8px}.feed-list.variant-glass .nav-link:hover,.feed-list.variant-glass .logout-link:hover{background:#ffffff0d;opacity:1;text-decoration:none}.feed-list.variant-glass .theme-selector button{background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:12px}.feed-list.variant-glass .theme-selector button.active{background:#fff3;border-color:#ffffff4d}.feed-list.variant-banana{background:#fdfdfd;padding:1rem;font-family:Poppins,Verdana,sans-serif;border-right:none;box-shadow:4px 0 24px #0000000a}.theme-dark .feed-list.variant-banana{background:#111}.feed-list.variant-banana .logo{font-size:2.5rem;text-shadow:2px 2px 0px #FFD700;background:transparent;transform:rotate(-3deg);display:inline-block;margin-bottom:2rem}.feed-list.variant-banana .section-header{background:gold;color:#000;display:inline-block;padding:.2rem .5rem;transform:skew(-10deg);font-size:.8rem;font-weight:800;margin-bottom:1rem;border-radius:4px}.feed-list.variant-banana .search-input{border:2px solid #000;border-radius:8px;box-shadow:2px 2px #000;transition:all .2s}.feed-list.variant-banana .search-input:focus{transform:translate(1px,1px);box-shadow:1px 1px #000;outline:none}.theme-dark .feed-list.variant-banana .search-input{border-color:#fff;box-shadow:2px 2px #fff;background:#222;color:#fff}.feed-list.variant-banana a,.feed-list.variant-banana .feed-title,.feed-list.variant-banana .tag-link{border:1px solid transparent;padding:.5rem;border-radius:8px;transition:transform .2s cubic-bezier(.34,1.56,.64,1);font-weight:600;text-decoration:none!important;color:var(--text-color)}.feed-list.variant-banana a:hover,.feed-list.variant-banana .feed-title:hover,.feed-list.variant-banana .tag-link:hover{transform:scale(1.05) rotate(1deg);background:#fff9c4;color:#000;box-shadow:0 4px 12px #ffd7004d}.theme-dark .feed-list.variant-banana a:hover,.theme-dark .feed-list.variant-banana .feed-title:hover,.theme-dark .feed-list.variant-banana .tag-link:hover{background:#333;color:gold}.feed-list.variant-banana a.active,.feed-list.variant-banana .feed-title.active,.feed-list.variant-banana .tag-link.active{background:gold;color:#000!important;transform:scale(1.02);box-shadow:3px 3px #0000001a;border:2px solid #000}.feed-list.variant-banana .nav-section{border-top:2px dashed #FFD700;margin-top:2rem;padding-top:1rem}.feed-list.variant-banana .theme-selector button{border:2px solid #000;box-shadow:2px 2px #000;border-radius:4px}.feed-list.variant-banana .theme-selector button.active{background:gold;transform:translate(1px,1px);box-shadow:1px 1px #000}.feed-list.variant-type{background:var(--bg-color);padding:2rem 1rem;font-family:Helvetica Neue,Arial,sans-serif;border-right:4px solid var(--text-color)}.feed-list.variant-type .logo{font-size:3rem;letter-spacing:-2px;font-weight:900;background:transparent;line-height:.8;margin-bottom:3rem;color:var(--text-color)}.feed-list.variant-type .section-header{font-size:1.2rem;font-weight:900;border-bottom:2px solid var(--text-color);padding-bottom:.5rem;margin-top:3rem;margin-bottom:1rem;letter-spacing:-.5px;color:var(--text-color)}.feed-list.variant-type a,.feed-list.variant-type .feed-title,.feed-list.variant-type .tag-link{font-size:1rem;font-weight:700;text-decoration:none!important;border-left:0px solid var(--text-color);padding-left:0;transition:padding-left .2s,border-left-width .2s;opacity:.6;color:var(--text-color);padding:.5rem 0;display:block}.feed-list.variant-type a:hover,.feed-list.variant-type .feed-title:hover,.feed-list.variant-type .tag-link:hover{padding-left:1rem;border-left:4px solid var(--text-color);opacity:1;color:var(--text-color)}.feed-list.variant-type a.active,.feed-list.variant-type .feed-title.active,.feed-list.variant-type .tag-link.active{padding-left:1rem;border-left:8px solid var(--text-color);opacity:1;color:var(--text-color)}.feed-list.variant-type .search-input{border:none;border-bottom:2px solid var(--text-color);background:transparent;border-radius:0;padding:1rem 0;font-weight:700;font-size:1.2rem}.feed-list.variant-type .search-input:focus{outline:none;border-bottom-width:4px}.feed-list.variant-type .nav-section{border-top:4px solid var(--text-color);margin-top:4rem;padding-top:1rem}.feed-list.variant-type .nav-link,.feed-list.variant-type .logout-link{font-size:1.2rem;font-weight:900}.feed-list.variant-type .theme-selector button{border-radius:0;border:2px solid var(--text-color);background:transparent}.feed-list.variant-type .theme-selector button.active{background:var(--text-color);color:var(--bg-color)}.feed-item{padding:1rem;margin-top:5rem;list-style:none;border-bottom:none}.item-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:.5rem}.item-title{font-family:var(--font-heading);font-size:1.8rem;font-weight:700;text-decoration:none;color:var(--link-color);display:block;flex:1}.item-title:hover{text-decoration:none;color:var(--link-color)}.item-actions{display:flex;gap:.5rem;margin-left:1rem}.star-btn{background:none;border:none;cursor:pointer;font-size:1.25rem;padding:0 0 0 .5rem;vertical-align:middle;transition:color .2s;line-height:1}.star-btn.is-starred{color:#00f}.star-btn.is-unstarred{color:var(--text-color);opacity:.3}.star-btn:hover{color:#00f}.action-btn{background:var(--sidebar-bg);border:1px solid var(--border-color, #ccc);cursor:pointer;padding:2px 6px;font-size:1rem;color:#00f;font-weight:700}.action-btn:hover{background-color:#eee}.dateline{margin-top:0;font-weight:400;font-size:.75em;color:#ccc;margin-bottom:1rem}.dateline a{color:#ccc;text-decoration:none}.item-description{color:var(--text-color);line-height:1.5;font-size:1rem;margin-top:1rem}.item-description img{max-width:100%;height:auto;display:block;margin:1rem 0}.item-description blockquote{padding:1rem 1rem 0;border-left:4px solid var(--sidebar-bg);color:var(--text-color);opacity:.8;margin-left:0}.scrape-btn{background:var(--bg-color);border:1px solid var(--border-color, #ccc);color:#00f;cursor:pointer;font-family:var(--font-heading);font-weight:700;font-size:.8rem;padding:2px 6px;margin-left:.5rem}.scrape-btn:hover{background:var(--sidebar-bg)}@media(max-width:768px){.feed-item{margin-top:2rem;padding:.5rem}.item-title{font-size:1.4rem;word-break:break-word}.item-header{flex-direction:column;gap:.5rem}.item-actions{margin-left:0;margin-bottom:.5rem}}.feed-items{padding:1rem 0}.feed-items h2{margin-top:0;border-bottom:2px solid var(--border-color);padding-bottom:.5rem}.item-list{list-style:none;padding:0}.loading-more{padding:2rem;text-align:center;color:#888;font-size:.9rem;min-height:50px}.settings-page.variant-glass{padding:2.5rem;max-width:800px;margin:0 auto;background:#ffffff0d;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-radius:24px;border:1px solid rgba(255,255,255,.1);font-family:system-ui,-apple-system,sans-serif;color:var(--text-color);margin-top:2rem;margin-bottom:2rem}.settings-page.variant-glass h2,.settings-page.variant-glass h3{font-weight:700;letter-spacing:-.02em;color:var(--text-color);opacity:.9}.add-feed-section,.appearance-section,.import-section,.export-section,.feed-list-section{background:#ffffff08;padding:1.5rem;border-radius:16px;margin-bottom:2rem;border:1px solid rgba(255,255,255,.05);transition:all .3s ease}.add-feed-section:hover,.appearance-section:hover,.import-section:hover,.export-section:hover,.feed-list-section:hover{background:#ffffff0f;border-color:#ffffff1a}.font-selector{display:flex;align-items:center;gap:1rem}.font-select{padding:.6rem 1rem;border:1px solid rgba(255,255,255,.1);background:#0000001a;color:var(--text-color);border-radius:20px;font-size:1rem;min-width:200px;cursor:pointer;outline:none;transition:border-color .2s}.font-select:focus{border-color:#ffffff4d}.add-feed-form{display:flex;gap:1rem}.feed-input{flex:1;padding:.6rem 1.2rem;border:1px solid rgba(255,255,255,.1);background:#0000001a;color:var(--text-color);border-radius:20px;font-size:1rem;outline:none;transition:border-color .2s}.feed-input:focus{border-color:#ffffff4d}.error-message{color:#ff5252;margin-top:1rem;font-weight:600}.settings-feed-list{list-style:none;padding:0;border:1px solid rgba(255,255,255,.05);border-radius:12px;overflow:hidden}.settings-feed-item{display:flex;justify-content:space-between;align-items:center;padding:1.2rem;border-bottom:1px solid rgba(255,255,255,.05);transition:background .2s}.settings-feed-item:hover{background:#ffffff05}.settings-feed-item:last-child{border-bottom:none}.feed-info{display:flex;flex-direction:column;gap:.2rem}.feed-title{font-weight:600;font-size:1.05rem;opacity:.9}.feed-url{color:var(--text-color);opacity:.5;font-size:.85rem}.delete-btn{background:#ff525226;color:#ff8a80;border:1px solid rgba(255,82,82,.2);padding:.5rem 1rem;border-radius:12px;cursor:pointer;font-weight:600;transition:all .2s}.delete-btn:hover:not(:disabled){background:#ff52524d;color:#fff;border-color:#ff525266;transform:scale(1.05)}.import-export-section{display:flex;gap:2rem}@media(max-width:600px){.settings-page.variant-glass{padding:1.5rem;margin-top:1rem}.add-feed-form{flex-direction:column}.import-export-section{flex-direction:column;gap:1rem}.settings-feed-item{flex-direction:column;align-items:flex-start;gap:1rem}}.import-form{display:flex;flex-direction:column;gap:1.2rem}.file-input{font-size:.9rem;max-width:100%;color:var(--text-color);opacity:.8}.export-buttons{display:flex;gap:.8rem;flex-wrap:wrap}.export-btn{display:inline-block;padding:.6rem 1.2rem;background:#ffffff0d;color:var(--text-color);text-decoration:none;border:1px solid rgba(255,255,255,.1);border-radius:12px;font-weight:600;transition:all .2s}.export-btn:hover{background:#ffffff1a;transform:translateY(-2px);box-shadow:0 4px 12px #0000001a}button:not(.delete-btn){cursor:pointer;padding:.6rem 1.2rem;border-radius:12px;border:1px solid rgba(255,255,255,.1);background:#ffffff1a;color:var(--text-color);font-weight:600;transition:all .2s}button:not(.delete-btn):hover:not(:disabled){background:#fff3;transform:scale(1.02)}button:disabled{opacity:.4;cursor:not-allowed} diff --git a/web/dist/v2/assets/index-BdhwF6aQ.js b/web/dist/v2/assets/index-WckTN3Bl.js index 3ea42da..3ea42da 100644 --- a/web/dist/v2/assets/index-BdhwF6aQ.js +++ b/web/dist/v2/assets/index-WckTN3Bl.js diff --git a/web/dist/v2/index.html b/web/dist/v2/index.html index 9b1c846..5975a93 100644 --- a/web/dist/v2/index.html +++ b/web/dist/v2/index.html @@ -5,8 +5,8 @@ <link rel="icon" type="image/svg+xml" href="/v2/vite.svg" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Neko Reader</title> - <script type="module" crossorigin src="/v2/assets/index-BdhwF6aQ.js"></script> - <link rel="stylesheet" crossorigin href="/v2/assets/index-BYhh91HH.css"> + <script type="module" crossorigin src="/v2/assets/index-WckTN3Bl.js"></script> + <link rel="stylesheet" crossorigin href="/v2/assets/index-CO97K-eb.css"> </head> <body> <div id="root"></div> |
