@isomoes/iread 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,6 +6,8 @@ There is no auth, no multi-user, no cloud sync, and no background scheduler. Ref
6
6
 
7
7
  ![iread UI](https://github.com/user-attachments/assets/ca9489c9-630f-4a67-9cd5-60c18ce3544e)
8
8
 
9
+ 📺 Watch the [v0.2.2 intro video](https://www.bilibili.com/video/BV1cmJg6SEso/) on Bilibili.
10
+
9
11
  ## Quick start
10
12
 
11
13
  ```sh
@@ -28,6 +30,26 @@ Options:
28
30
  -h, --help Show this help and exit
29
31
  ```
30
32
 
33
+ ## Run with Docker
34
+
35
+ Prebuilt multi-arch images (amd64 + arm64) are published to the GitHub Container Registry, so no Node.js on the host is required:
36
+
37
+ ```sh
38
+ docker run -d --name iread -p 8787:8787 -v iread-data:/data ghcr.io/isomoes/iread:latest
39
+ ```
40
+
41
+ Then open http://localhost:8787. The SQLite database and the auto-saved `feeds.opml` mirror live under `/data` in the container — here the named `iread-data` volume — so your subscriptions survive container upgrades.
42
+
43
+ A `docker-compose.yml` is included for the same thing:
44
+
45
+ ```sh
46
+ docker compose up -d
47
+ ```
48
+
49
+ Build the image from the checkout instead of pulling it with `docker compose up -d --build`, or `docker build -t iread .`.
50
+
51
+ `PORT` (default 8787), `HOST` (default all interfaces), `DB_PATH` (default `/data/iread.db`), and `OPML_PATH` (default `/data/feeds.opml`) are configurable via `-e`/`environment:`. With host networking, set `HOST=127.0.0.1` to restrict access to localhost; with bridge networking, leave `HOST` unset and restrict the published port instead. To store data in a host directory instead of a named volume, bind-mount it and make it writable by the image's unprivileged `node` user (uid 1000), e.g. `mkdir iread-data && sudo chown 1000:1000 iread-data` then `-v "$PWD/iread-data:/data"`.
52
+
31
53
  ## Features
32
54
 
33
55
  - Add a feed by URL (title resolved from feed metadata), delete a feed.
@@ -43,18 +65,61 @@ Options:
43
65
  - OPML import (bulk add) and OPML export, plus an always-current `feeds.opml` auto-saved next to the database on every change for quick sharing and backup. The auto-saved file is a one-way snapshot of the database (overwritten on every change and at startup), so use it for backup and sharing — to bring feeds in, use OPML import.
44
66
  - Light, dark, and system theme, persisted to localStorage.
45
67
 
68
+ ## Usage
69
+
70
+ 1. Start the app (dev or production) and open it in your browser.
71
+ 2. Add a feed by pasting its RSS or Atom URL into the add-feed form in the sidebar, or import an OPML file from the OPML menu. A `config/sample-feeds.opml` file is included for a quick start, and [`isomoes/arch-config/iread/feeds.opml`](https://github.com/isomoes/arch-config/blob/master/iread/feeds.opml) is a real-world example (the feed set isomoes actually reads) you can import directly.
72
+ 3. Select a feed or a smart view (All, Unread, Starred) in the sidebar.
73
+ 4. Navigate the article list with `j` and `k`, open an article with `Enter`, and read it in the right pane.
74
+ 5. Press `r` to refresh the current feed or `R` to refresh all feeds. Refresh fetches remote feeds; client polling only refreshes local data and never re-fetches remotely.
75
+ 6. Star with `s`, toggle read with `m`, mark a whole scope read with `A`, and search with `/`.
76
+ 7. Press `?` at any time to see the full keyboard map. Press `t` to cycle the theme.
77
+
78
+ ## Keyboard shortcuts
79
+
80
+ Keys are case-sensitive (Shift matters). Bindings fire only when you are not typing in an input and no Ctrl, Meta, or Alt modifier is held.
81
+
82
+ | Key | Action | Effect |
83
+ | ------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
84
+ | `j` / Down | Down in focused pane | Pane-contextual: move the article selection (list), the feed/view selection (sidebar), or the reader scroll down one. Stops at the end (no wrap). |
85
+ | `k` / Up | Up in focused pane | As `j`, upward. Stops at the start. |
86
+ | `n` | Next unread | Jump to the next unread item below; wrap to the first unread from the top if none below. |
87
+ | `g` | Top | Pane-contextual: in the list, select the first item; in the sidebar, jump to the first feed/view; in the reader, scroll the article to the top. |
88
+ | `G` | Bottom | Pane-contextual: in the list, select the last item; in the sidebar, jump to the last feed/view; in the reader, scroll the article to the bottom. |
89
+ | `Enter` / `o` | Open / focus reader | Render the selected item, mark it read, and move focus into the reader. |
90
+ | `J` / `]` | Next feed/view | Move sidebar selection down and load its items, selecting the first one. |
91
+ | `K` / `[` | Previous feed/view | Move sidebar selection up and load its items. |
92
+ | `m` | Toggle read/unread | Flip the read state of the selected item; counts update. |
93
+ | `s` | Toggle star | Flip the starred state; in the Starred view an unstarred item leaves the list. |
94
+ | `A` | Mark feed/view read | Mark the current scope read and offer an Undo toast. |
95
+ | `r` | Refresh current feed | Refresh the selected feed, or the feed of the selected article in a smart view. |
96
+ | `R` | Refresh all feeds | Refresh every feed and update counts on completion. |
97
+ | `v` | Open original | Open the article link in a new tab. |
98
+ | `#` then N | Open link by number | Links in the article body are numbered inline `[N]`; press `#`, type the number, and it opens in a new tab — instantly once the number is unambiguous, otherwise `Enter` confirms; `Esc` cancels. |
99
+ | `/` | Focus search | Focus and select the search input. |
100
+ | `Esc` | Contextual dismiss | Close help, clear search, or return focus from the reader to the list. |
101
+ | `?` | Help overlay | Toggle the keybinding overlay. |
102
+ | `t` | Toggle theme | Cycle light, dark, and system theme; persisted. |
103
+
104
+ ## Project layout
105
+
106
+ - `src/shared/` shared, type-only DTOs used by both server and web.
107
+ - `src/server/` Hono API, SQLite access, feed fetch/parse/sanitize, OPML, SSRF guard.
108
+ - `src/web/` React app: three-pane layout, hooks, components, styles.
109
+ - `data/` development SQLite database (gitignored); production data lives in `~/.config/iread/`.
110
+
46
111
  ## Requirements (development)
47
112
 
48
113
  - Node.js 24 or newer (the server uses the built-in `node:sqlite` module).
49
114
  - pnpm.
50
115
 
51
- ## Install
116
+ ### Install
52
117
 
53
118
  ```sh
54
119
  pnpm install
55
120
  ```
56
121
 
57
- ## Develop
122
+ ### Develop
58
123
 
59
124
  ```sh
60
125
  pnpm dev
@@ -83,51 +148,4 @@ pnpm start
83
148
 
84
149
  Open http://localhost:8787
85
150
 
86
- `PORT` (default 8787), `DB_PATH` (default `~/.config/iread/iread.db`), and `OPML_PATH` (default `feeds.opml` next to the database; set empty to disable the auto-saved mirror) are read from the environment. See `config/.env.example`.
87
-
88
- ## Publish to npm
89
-
90
- `pnpm publish` runs the full build via the `prepack` hook and ships only `dist/` (server, shared types, and the prebuilt web bundle); the `iread` bin points at `dist/server/cli.js`. The web framework dependencies are devDependencies, so `npx @isomoes/iread` installs only the small server runtime set.
91
-
92
- ## Usage
93
-
94
- 1. Start the app (dev or production) and open it in your browser.
95
- 2. Add a feed by pasting its RSS or Atom URL into the add-feed form in the sidebar, or import an OPML file from the OPML menu. A `config/sample-feeds.opml` file is included for a quick start.
96
- 3. Select a feed or a smart view (All, Unread, Starred) in the sidebar.
97
- 4. Navigate the article list with `j` and `k`, open an article with `Enter`, and read it in the right pane.
98
- 5. Press `r` to refresh the current feed or `R` to refresh all feeds. Refresh fetches remote feeds; client polling only refreshes local data and never re-fetches remotely.
99
- 6. Star with `s`, toggle read with `m`, mark a whole scope read with `A`, and search with `/`.
100
- 7. Press `?` at any time to see the full keyboard map. Press `t` to cycle the theme.
101
-
102
- ## Keyboard shortcuts
103
-
104
- Keys are case-sensitive (Shift matters). Bindings fire only when you are not typing in an input and no Ctrl, Meta, or Alt modifier is held.
105
-
106
- | Key | Action | Effect |
107
- |---|---|---|
108
- | `j` / Down | Next article | Move selection down one row. Stops at the last item (no wrap). |
109
- | `k` / Up | Previous article | Move selection up one row. Stops at the first item. |
110
- | `n` | Next unread | Jump to the next unread item below; wrap to the first unread from the top if none below. |
111
- | `g` | Top | Select the first item and scroll to the top. |
112
- | `G` | Bottom | Select the last item and scroll to the bottom. |
113
- | `Enter` / `o` | Open / focus reader | Render the selected item, mark it read, and move focus into the reader. |
114
- | `J` / `]` | Next feed/view | Move sidebar selection down and load its items, selecting the first one. |
115
- | `K` / `[` | Previous feed/view | Move sidebar selection up and load its items. |
116
- | `m` | Toggle read/unread | Flip the read state of the selected item; counts update. |
117
- | `s` | Toggle star | Flip the starred state; in the Starred view an unstarred item leaves the list. |
118
- | `A` | Mark feed/view read | Mark the current scope read and offer an Undo toast. |
119
- | `r` | Refresh current feed | Refresh the selected feed, or the feed of the selected article in a smart view. |
120
- | `R` | Refresh all feeds | Refresh every feed and update counts on completion. |
121
- | `v` | Open original | Open the article link in a new tab. |
122
- | `#` then N | Open link by number | Links in the article body are numbered inline `[N]`; press `#`, type the number, and it opens in a new tab — instantly once the number is unambiguous, otherwise `Enter` confirms; `Esc` cancels. |
123
- | `/` | Focus search | Focus and select the search input. |
124
- | `Esc` | Contextual dismiss | Close help, clear search, or return focus from the reader to the list. |
125
- | `?` | Help overlay | Toggle the keybinding overlay. |
126
- | `t` | Toggle theme | Cycle light, dark, and system theme; persisted. |
127
-
128
- ## Project layout
129
-
130
- - `src/shared/` shared, type-only DTOs used by both server and web.
131
- - `src/server/` Hono API, SQLite access, feed fetch/parse/sanitize, OPML, SSRF guard.
132
- - `src/web/` React app: three-pane layout, hooks, components, styles.
133
- - `data/` development SQLite database (gitignored); production data lives in `~/.config/iread/`.
151
+ `PORT` (default 8787), `HOST` (default all interfaces; set `127.0.0.1` for localhost only), `DB_PATH` (default `~/.config/iread/iread.db`), and `OPML_PATH` (default `feeds.opml` next to the database; set empty to disable the auto-saved mirror) are read from the environment. See `config/.env.example`.
@@ -13,6 +13,7 @@ import { feeds } from './routes/feeds.js';
13
13
  import { items } from './routes/items.js';
14
14
  import { opml } from './routes/opml.js';
15
15
  const PORT = Number(process.env.PORT ?? 8787);
16
+ const HOST = process.env.HOST || undefined;
16
17
  const IS_PROD = process.env.NODE_ENV === 'production';
17
18
  // Importing ./db has the side effect of opening the DB and running migrations.
18
19
  // Do it explicitly so startup failures surface immediately.
@@ -56,7 +57,7 @@ if (IS_PROD) {
56
57
  });
57
58
  }
58
59
  // --- Start ------------------------------------------------------------------
59
- serve({ fetch: app.fetch, port: PORT }, (info) => {
60
+ serve({ fetch: app.fetch, port: PORT, hostname: HOST }, (info) => {
60
61
  const url = `http://localhost:${info.port}`;
61
62
  console.log(`[iread] listening on ${url}`);
62
63
  });
@@ -0,0 +1,2 @@
1
+ /*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-outline-style:solid}}}@layer theme{:root,:host{--font-sans:"Geist Variable", ui-sans-serif, system-ui, sans-serif;--font-mono:"Geist Mono Variable", ui-monospace, "SF Mono", monospace;--spacing:.25rem;--container-xs:20rem;--container-sm:24rem;--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-wide:.025em;--leading-tight:1.25;--radius-xs:.25rem;--radius-sm:.375rem;--radius-md:.5rem;--radius-lg:.75rem;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-bg:var(--bg);--color-surface:var(--surface);--color-surface-elevated:var(--surface-elevated);--color-border:var(--border);--color-text-primary:var(--text-primary);--color-text-secondary:var(--text-secondary);--color-text-muted:var(--text-muted);--color-accent:var(--accent);--color-accent-foreground:var(--accent-foreground);--color-unread:var(--unread);--color-danger:var(--danger)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*{border-color:var(--color-border)}html{-webkit-text-size-adjust:100%}body{font-family:var(--font-sans);background-color:var(--color-bg);color:var(--color-text-primary);font-feature-settings:"cv11", "ss01";-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;font-size:.875rem;line-height:1.5}.num,kbd,time,.count{font-family:var(--font-mono);font-variant-numeric:tabular-nums;font-feature-settings:"tnum"}:focus-visible{outline:2px solid var(--color-accent);outline-offset:2px;border-radius:var(--radius-sm)}::selection{background-color:var(--color-accent)}@supports (color:color-mix(in lab, red, red)){::selection{background-color:color-mix(in oklch, var(--color-accent) 28%, transparent)}}.reader-prose{max-width:68ch;font-size:1rem;line-height:1.7}.reader-prose>:first-child{margin-top:0}.reader-prose>:last-child{margin-bottom:0}.reader-prose p{margin-block:.85em}.reader-prose a{color:var(--color-accent);text-underline-offset:2px;text-decoration:underline;text-decoration-thickness:from-font}.reader-prose .linknum{font-family:var(--font-mono);vertical-align:super;letter-spacing:-.03em;color:var(--color-text-muted);-webkit-user-select:none;user-select:none;margin-inline-start:.1em;font-size:.68em;line-height:0}.reader-prose.link-jump-active .linknum{color:var(--color-accent);font-weight:600}.reader-prose a[data-linknum].iread-link-target{outline:2px solid var(--color-accent);outline-offset:2px;border-radius:var(--radius-xs);background:var(--color-accent)}@supports (color:color-mix(in lab, red, red)){.reader-prose a[data-linknum].iread-link-target{background:color-mix(in oklch, var(--color-accent) 14%, transparent)}}.reader-prose h1,.reader-prose h2,.reader-prose h3,.reader-prose h4,.reader-prose h5,.reader-prose h6{text-wrap:balance;margin-top:1.6em;margin-bottom:.6em;font-weight:600;line-height:1.3}.reader-prose h1{font-size:1.5em}.reader-prose h2{font-size:1.3em}.reader-prose h3{font-size:1.15em}.reader-prose h4{font-size:1em}.reader-prose h5{color:var(--color-text-secondary);font-size:.9em}.reader-prose h6{color:var(--color-text-secondary);font-size:.85em}.reader-prose ul,.reader-prose ol{margin-block:.85em;padding-inline-start:1.6em}.reader-prose ul{list-style:outside}.reader-prose ol{list-style:decimal}.reader-prose ul ul{list-style:circle}.reader-prose ul ul ul{list-style:square}.reader-prose li{margin-block:.3em}.reader-prose li::marker{color:var(--color-text-muted)}.reader-prose :is(ul,ol) :is(ul,ol){margin-block:.3em}.reader-prose dl{margin-block:.85em}.reader-prose dt{margin-top:.6em;font-weight:600}.reader-prose dd{margin-inline-start:1.6em}.reader-prose img,.reader-prose video{border-radius:var(--radius-md);max-width:100%;height:auto}.reader-prose audio{width:100%;max-width:100%}.reader-prose figure{margin-block:1.2em}.reader-prose figcaption{color:var(--color-text-muted);text-align:center;margin-top:.5em;font-size:.85em}.reader-prose code{font-family:var(--font-mono);background:var(--color-surface-elevated);border-radius:var(--radius-xs);padding:.1em .35em;font-size:.875em}.reader-prose pre{font-family:var(--font-mono);background:var(--color-surface-elevated);border:1px solid var(--color-border);border-radius:var(--radius-md);margin-block:1em;padding:.85rem 1rem;line-height:1.5;overflow-x:auto}.reader-prose pre code{background:0 0;padding:0;font-size:.85em}.reader-prose blockquote{border-left:3px solid var(--color-accent);color:var(--color-text-secondary);margin-block:1em;padding-left:1rem;font-style:italic}.reader-prose hr{border:0;border-top:1px solid var(--color-border);margin-block:1.6em}.reader-prose mark{background:var(--color-accent)}@supports (color:color-mix(in lab, red, red)){.reader-prose mark{background:color-mix(in oklch, var(--color-accent) 25%, transparent)}}.reader-prose mark{color:inherit;border-radius:var(--radius-xs);padding:0 .15em}.reader-prose table{border-collapse:collapse;width:max-content;max-width:100%;margin-block:1.2em;font-size:.9em;display:block;overflow-x:auto}.reader-prose th,.reader-prose td{border:1px solid var(--color-border);text-align:left;vertical-align:top;padding:.4em .7em}.reader-prose th{background:var(--color-surface-elevated);font-weight:600}.reader-prose thead th{border-bottom-width:2px}.reader-prose tbody tr:nth-child(2n){background:var(--color-surface-elevated)}@supports (color:color-mix(in lab, red, red)){.reader-prose tbody tr:nth-child(2n){background:color-mix(in oklch, var(--color-surface-elevated) 55%, transparent)}}.reader-prose caption{caption-side:bottom;color:var(--color-text-muted);text-align:left;margin-top:.5em;font-size:.85em}}@layer components;@layer utilities{.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing) * 0)}.inset-y-0{inset-block:calc(var(--spacing) * 0)}.top-0{top:calc(var(--spacing) * 0)}.right-2{right:calc(var(--spacing) * 2)}.right-4{right:calc(var(--spacing) * 4)}.bottom-4{bottom:calc(var(--spacing) * 4)}.left-0{left:calc(var(--spacing) * 0)}.left-1\/2{left:50%}.left-2\.5{left:calc(var(--spacing) * 2.5)}.z-10{z-index:10}.z-20{z-index:20}.z-50{z-index:50}.container{width:100%}@media (width>=40rem){.container{max-width:40rem}}@media (width>=48rem){.container{max-width:48rem}}@media (width>=64rem){.container{max-width:64rem}}@media (width>=80rem){.container{max-width:80rem}}@media (width>=96rem){.container{max-width:96rem}}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.ml-3{margin-left:calc(var(--spacing) * 3)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.size-2{width:calc(var(--spacing) * 2);height:calc(var(--spacing) * 2)}.size-8{width:calc(var(--spacing) * 8);height:calc(var(--spacing) * 8)}.h-0{height:calc(var(--spacing) * 0)}.h-0\.5{height:calc(var(--spacing) * .5)}.h-3{height:calc(var(--spacing) * 3)}.h-3\.5{height:calc(var(--spacing) * 3.5)}.h-6{height:calc(var(--spacing) * 6)}.h-full{height:100%}.max-h-\[80vh\]{max-height:80vh}.min-h-0{min-height:calc(var(--spacing) * 0)}.w-0\.5{width:calc(var(--spacing) * .5)}.w-1\/2{width:50%}.w-2\/3{width:66.6667%}.w-2\/5{width:40%}.w-3\/4{width:75%}.w-4\/5{width:80%}.w-6{width:calc(var(--spacing) * 6)}.w-full{width:100%}.max-w-md{max-width:var(--container-md)}.max-w-sm{max-width:var(--container-sm)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-5{min-width:calc(var(--spacing) * 5)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.origin-left{transform-origin:0}.-translate-x-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.animate-pulse{animation:var(--animate-pulse)}.cursor-pointer{cursor:pointer}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xs{border-radius:var(--radius-xs)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-border{border-color:var(--color-border)}.bg-\[color-mix\(in_oklch\,oklch\(0\.17_0\.004_264\)_45\%\,transparent\)\]{background-color:oklch(17% .004 264/.45)}.bg-accent,.bg-accent\/10{background-color:var(--color-accent)}@supports (color:color-mix(in lab, red, red)){.bg-accent\/10{background-color:color-mix(in oklab, var(--color-accent) 10%, transparent)}}.bg-accent\/15{background-color:var(--color-accent)}@supports (color:color-mix(in lab, red, red)){.bg-accent\/15{background-color:color-mix(in oklab, var(--color-accent) 15%, transparent)}}.bg-bg{background-color:var(--color-bg)}.bg-danger{background-color:var(--color-danger)}.bg-surface{background-color:var(--color-surface)}.bg-surface-elevated{background-color:var(--color-surface-elevated)}.bg-transparent{background-color:#0000}.bg-unread{background-color:var(--color-unread)}.p-1{padding:calc(var(--spacing) * 1)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.px-1{padding-inline:calc(var(--spacing) * 1)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-12{padding-block:calc(var(--spacing) * 12)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pr-8{padding-right:calc(var(--spacing) * 8)}.pb-1{padding-bottom:calc(var(--spacing) * 1)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pl-8{padding-left:calc(var(--spacing) * 8)}.text-center{text-align:center}.text-left{text-align:left}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[10px\]{font-size:10px}.leading-none{--tw-leading:1;line-height:1}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-accent{color:var(--color-accent)}.text-accent-foreground{color:var(--color-accent-foreground)}.text-danger{color:var(--color-danger)}.text-text-muted{color:var(--color-text-muted)}.text-text-primary{color:var(--color-text-primary)}.text-text-secondary{color:var(--color-text-secondary)}.uppercase{text-transform:uppercase}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.opacity-0{opacity:0}.opacity-100{opacity:1}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.outline-none{--tw-outline-style:none;outline-style:none}@media (hover:hover){.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}}.placeholder\:text-text-muted::placeholder{color:var(--color-text-muted)}@media (hover:hover){.hover\:bg-surface:hover{background-color:var(--color-surface)}.hover\:bg-surface-elevated:hover{background-color:var(--color-surface-elevated)}.hover\:text-danger:hover{color:var(--color-danger)}.hover\:text-text-primary:hover{color:var(--color-text-primary)}.hover\:opacity-80:hover{opacity:.8}.hover\:opacity-90:hover{opacity:.9}}.focus\:not-sr-only:focus{clip-path:none;white-space:normal;width:auto;height:auto;margin:0;padding:0;position:static;overflow:visible}.focus\:absolute:focus{position:absolute}.focus\:top-3:focus{top:calc(var(--spacing) * 3)}.focus\:left-3:focus{left:calc(var(--spacing) * 3)}.focus\:z-50:focus{z-index:50}.focus\:rounded-md:focus{border-radius:var(--radius-md)}.focus\:bg-accent:focus{background-color:var(--color-accent)}.focus\:px-3:focus{padding-inline:calc(var(--spacing) * 3)}.focus\:py-2:focus{padding-block:calc(var(--spacing) * 2)}.focus\:text-accent-foreground:focus{color:var(--color-accent-foreground)}.focus-visible\:opacity-100:focus-visible{opacity:1}.focus-visible\:outline:focus-visible{outline-style:var(--tw-outline-style);outline-width:1px}.focus-visible\:outline-2:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px}.focus-visible\:outline-offset-1:focus-visible{outline-offset:1px}.focus-visible\:outline-accent:focus-visible{outline-color:var(--color-accent)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.disabled\:opacity-60:disabled{opacity:.6}@media (width>=48rem){.md\:border-r{border-right-style:var(--tw-border-style);border-right-width:1px}}}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2)format("woff2-variations");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2)format("woff2-variations");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2)format("woff2-variations");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2)format("woff2-variations");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-latin-wght-normal-BgDaEnEv.woff2)format("woff2-variations");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Geist Mono Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-mono-cyrillic-ext-wght-normal-I4S5GZfc.woff2)format("woff2-variations");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Geist Mono Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-mono-cyrillic-wght-normal-BmXc_FBt.woff2)format("woff2-variations");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Geist Mono Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-mono-symbols2-wght-normal-GZpp1pK2.woff2)format("woff2-variations");unicode-range:U+2000-2001,U+2004-2008,U+200A,U+23B8-23BD,U+2500-259F}@font-face{font-family:Geist Mono Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-mono-vietnamese-wght-normal-D8KDMBhC.woff2)format("woff2-variations");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Geist Mono Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-mono-latin-ext-wght-normal-DrnZ1wKl.woff2)format("woff2-variations");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Geist Mono Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-mono-latin-wght-normal-B_7UjwxQ.woff2)format("woff2-variations");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{--bg:oklch(98.5% .002 247);--surface:oklch(99.5% .001 247);--surface-elevated:oklch(99.8% .001 247);--border:oklch(92% .003 247);--text-primary:oklch(21% .004 264);--text-secondary:oklch(44% .006 264);--text-muted:oklch(55% .006 264);--accent:oklch(58% .13 244);--accent-foreground:oklch(99% .002 247);--unread:oklch(58% .13 244);--danger:oklch(55% .18 27);--focus-ring:oklch(58% .13 244);--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light}.dark{--bg:oklch(17% .004 264);--surface:oklch(20.5% .004 264);--surface-elevated:oklch(24.5% .005 264);--border:oklch(30% .006 264);--text-primary:oklch(96% .002 247);--text-secondary:oklch(71% .004 264);--text-muted:oklch(60% .005 264);--accent:oklch(70% .13 244);--accent-foreground:oklch(17% .01 264);--unread:oklch(70% .13 244);--danger:oklch(70% .16 27);--focus-ring:oklch(70% .13 244);--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}@media (prefers-color-scheme:dark){:root:not(.theme-light):not(.dark){--bg:oklch(17% .004 264);--surface:oklch(20.5% .004 264);--surface-elevated:oklch(24.5% .005 264);--border:oklch(30% .006 264);--text-primary:oklch(96% .002 247);--text-secondary:oklch(71% .004 264);--text-muted:oklch(60% .005 264);--accent:oklch(70% .13 244);--accent-foreground:oklch(17% .01 264);--unread:oklch(70% .13 244);--danger:oklch(70% .16 27);--focus-ring:oklch(70% .13 244);--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}}.app-shell{grid-template:"topbar topbar topbar""sidebar list reader"1fr/256px 360px minmax(0,1fr);height:100dvh;display:grid}@media (width<=767px){.app-shell{grid-template-columns:1fr;grid-template-areas:"topbar""active-pane"}}@media (prefers-reduced-motion:reduce){*,:before,:after{transition-duration:.001ms!important;animation-duration:.001ms!important;animation-iteration-count:1!important}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@keyframes pulse{50%{opacity:.5}}