aboutsummaryrefslogtreecommitdiffstats
path: root/frontend-vanilla
diff options
context:
space:
mode:
Diffstat (limited to 'frontend-vanilla')
-rw-r--r--frontend-vanilla/THEMES.md151
-rw-r--r--frontend-vanilla/public/themes/codex.css451
-rw-r--r--frontend-vanilla/public/themes/refined.css272
-rw-r--r--frontend-vanilla/public/themes/sakura.css466
-rw-r--r--frontend-vanilla/public/themes/terminal.css481
-rw-r--r--frontend-vanilla/src/main.ts84
-rw-r--r--frontend-vanilla/src/store.ts9
-rw-r--r--frontend-vanilla/src/style.css59
8 files changed, 1970 insertions, 3 deletions
diff --git a/frontend-vanilla/THEMES.md b/frontend-vanilla/THEMES.md
new file mode 100644
index 0000000..8367046
--- /dev/null
+++ b/frontend-vanilla/THEMES.md
@@ -0,0 +1,151 @@
+# Creating a New Theme
+
+Neko's style theme system layers an additional CSS file on top of the base
+stylesheet. The base `style.css` defines CSS custom properties (variables) and
+all component styles. A theme file overrides whichever of those it wants.
+
+## Quick Start
+
+1. Create a new CSS file in `frontend-vanilla/public/themes/yourtheme.css`
+2. Register the theme name in `frontend-vanilla/src/main.ts` — add it to the
+ `STYLE_THEMES` array:
+ ```ts
+ const STYLE_THEMES = ['default', 'refined', 'terminal', 'codex', 'sakura', 'yourtheme'] as const;
+ ```
+3. Rebuild: `cd frontend-vanilla && npm run build`
+4. Copy output: `cp -r dist/* ../web/dist/v3/`
+
+That's it. The theme will appear in Settings > Style and in the sidebar cycle
+button.
+
+## How it works
+
+When a user picks a style theme, JavaScript inserts a `<link>` tag:
+
+```html
+<link id="style-theme-link" rel="stylesheet" href="/v3/themes/yourtheme.css">
+```
+
+This loads *after* the base stylesheet, so any rules you write will override
+the defaults via normal CSS cascade (same specificity = last one wins). When
+the user switches to "Default," the link tag is removed entirely.
+
+The theme CSS files live in `public/themes/` so Vite copies them as-is into the
+build output (no hashing, no bundling). They're plain CSS — no build step
+required for the theme file itself.
+
+## CSS Variables You Can Override
+
+The base stylesheet defines these custom properties on `:root` (light mode)
+and `.theme-dark` (dark mode):
+
+```css
+/* Colors */
+--bg-color /* Page background */
+--text-color /* Primary text */
+--sidebar-bg /* Sidebar background */
+--link-color /* Links and titles */
+--border-color /* Borders, rules, dividers */
+--accent-color /* Interactive accent (selection, focus) */
+
+/* Fonts */
+--font-body /* Body/article text (default: Palatino stack) */
+--font-heading /* Dynamic heading font (user-selectable) */
+--font-heading-system /* System heading font (Helvetica Neue stack) */
+--font-sans /* Sans-serif stack (Inter/system-ui) */
+--font-mono /* Monospace stack (Courier New) */
+```
+
+### Supporting both light and dark mode
+
+Your theme should define colors for both modes:
+
+```css
+/* Light mode */
+:root {
+ --bg-color: #faf8f4;
+ --text-color: #2c2c2c;
+ --link-color: #8b4513;
+ /* ... */
+}
+
+/* Dark mode */
+.theme-dark {
+ --bg-color: #1c1a17;
+ --text-color: #d4cfc6;
+ --link-color: #c9956b;
+ /* ... */
+}
+```
+
+The light/dark toggle adds/removes the `theme-dark` class on `#app`. Your
+theme's `.theme-dark` rules will override your `:root` rules when active.
+
+**Important:** The base stylesheet has some `.theme-dark .sidebar` rules that
+set the sidebar to a grey background with dark text. If your theme wants a
+different dark-mode sidebar, you'll need to override those specifically:
+
+```css
+.theme-dark .sidebar { background: ...; }
+.theme-dark .sidebar-section li a { color: ...; }
+.theme-dark .sidebar-section h3 { color: ...; }
+.theme-dark .sidebar-footer a { color: ...; }
+```
+
+## Key Selectors Reference
+
+### Layout
+- `.sidebar` — Fixed sidebar (11rem wide)
+- `.main-content` — Scrollable content area
+- `.main-content > *` — Content max-width container (default: 35em)
+
+### Feed items
+- `.feed-item` — Individual article wrapper
+- `.item-title` — Article title/link
+- `.dateline` — Date + feed source line
+- `.item-description` — Article body content
+- `.star-btn` / `.star-btn.is-starred` / `.star-btn.is-unstarred` — Star toggle
+- `.scrape-btn` — "text" button for full-content fetch
+
+### Settings page
+- `.settings-view` — Settings container
+- `.settings-section` / `.settings-section h3` — Section blocks
+- `.settings-group` / `.data-group` — Form groups
+- `.theme-options` — Button row for theme/style selectors
+- `.button-group` — Button row for export/import
+- `.manage-feed-list` / `.manage-feed-item` — Feed management list
+
+### Buttons
+- `button` / `.button` — All buttons and button-styled links
+- `button.active` — Currently selected option
+
+### Sidebar details
+- `.sidebar-section h3` — Section headers (FEEDS, TAGS)
+- `.sidebar-section li a` — Feed/filter links
+- `.sidebar-section li.active a` — Selected item
+- `.sidebar-footer` — Bottom area (settings, logout, controls)
+- `.sidebar-quick-controls` / `.sidebar-icon-btn` — Theme toggle icons
+
+## Tips
+
+- **Override fonts broadly.** Set `font-family` on `body` for article text and
+ on `.sidebar`, `.settings-view`, `button` for UI elements.
+- **Use the cascade.** You don't need `!important` — your theme loads after the
+ base stylesheet. Just match or exceed the specificity of the base rule.
+- **Keep it to overrides.** Don't redefine layout or structural properties
+ unless you have a reason. Focus on colors, typography, and spacing.
+- **Test both modes.** Always check your theme in both light and dark.
+- **Note on font overrides:** The user can independently select heading and body
+ fonts in Settings > Fonts. These apply CSS classes like `.font-sans`,
+ `.heading-font-serif`, etc. Your theme's font choices will be the "default"
+ that users see before changing font settings.
+
+## Existing Themes
+
+| Theme | Character |
+|------------|-----------|
+| `default` | No extra CSS — the base stylesheet as-is |
+| `refined` | Tightened spacing, better typographic rhythm, polished details |
+| `terminal` | Monospace, green phosphor accent, CRT scanlines in dark mode |
+| `codex` | Book-inspired: warm cream paper, serif type, fleuron separators |
+| `sakura` | Japanese aesthetic: restrained palette, muted rose accent, calm spacing |
diff --git a/frontend-vanilla/public/themes/codex.css b/frontend-vanilla/public/themes/codex.css
new file mode 100644
index 0000000..ece9e2d
--- /dev/null
+++ b/frontend-vanilla/public/themes/codex.css
@@ -0,0 +1,451 @@
+/*
+ * Codex Theme
+ * The book as interface. Tufte, Bringhurst, and the long tradition
+ * of typographic craft applied to the screen.
+ * Warm, readable, unhurried. Content is sovereign.
+ */
+
+/* ---- Color System ---- */
+:root {
+ --bg-color: #faf8f4;
+ --text-color: #2c2c2c;
+ --sidebar-bg: #f0ece4;
+ --link-color: #8b4513;
+ --border-color: #d4cfc6;
+ --accent-color: #8b4513;
+
+ --codex-paper: #faf8f4;
+ --codex-ink: #2c2c2c;
+ --codex-sienna: #8b4513;
+ --codex-muted: #8a8478;
+ --codex-rule: #d4cfc6;
+
+ color-scheme: light;
+}
+
+/* Dark mode adaptation -- parchment by candlelight */
+.theme-dark {
+ --bg-color: #1c1a17;
+ --text-color: #d4cfc6;
+ --sidebar-bg: #23211d;
+ --link-color: #c9956b;
+ --border-color: #3a3630;
+ --accent-color: #c9956b;
+
+ --codex-paper: #1c1a17;
+ --codex-ink: #d4cfc6;
+ --codex-sienna: #c9956b;
+ --codex-muted: #8a8478;
+ --codex-rule: #3a3630;
+}
+
+/* ---- Typography ---- */
+
+body {
+ font-family: 'Iowan Old Style', 'Palatino Linotype', Palatino, 'Book Antiqua', 'Crimson Text', Georgia, serif;
+ font-size: 19px;
+ line-height: 1.7;
+ letter-spacing: 0.005em;
+ background-color: var(--bg-color);
+ color: var(--text-color);
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
+ font-feature-settings: 'liga' 1, 'kern' 1, 'onum' 1;
+}
+
+/* ---- Sidebar: Table of Contents ---- */
+
+.sidebar {
+ background: var(--sidebar-bg);
+ backdrop-filter: none;
+ -webkit-backdrop-filter: none;
+ border-right: 1px solid var(--codex-rule);
+ font-family: 'Iowan Old Style', Palatino, Georgia, serif;
+}
+
+.theme-dark .sidebar {
+ background: var(--sidebar-bg);
+ border-right-color: var(--codex-rule);
+}
+
+.sidebar-section h3 {
+ font-family: 'Iowan Old Style', Palatino, Georgia, serif;
+ font-size: 0.7rem;
+ font-weight: 400;
+ font-variant: small-caps;
+ text-transform: lowercase;
+ letter-spacing: 0.15em;
+ color: var(--codex-muted);
+ opacity: 1;
+}
+
+.theme-dark .sidebar-section h3 {
+ color: var(--codex-muted);
+}
+
+.sidebar-section li a {
+ font-family: 'Iowan Old Style', Palatino, Georgia, serif;
+ color: var(--text-color);
+ font-size: 0.85rem;
+ border-radius: 3px;
+ font-weight: 400;
+}
+
+.theme-dark .sidebar-section li a {
+ color: var(--text-color);
+}
+
+.sidebar-section li a:hover {
+ background: rgba(139, 69, 19, 0.06);
+ color: var(--codex-sienna);
+ transform: none;
+}
+
+.theme-dark .sidebar-section li a:hover {
+ background: rgba(201, 149, 107, 0.08);
+ color: var(--codex-sienna);
+}
+
+.sidebar-section li.active a {
+ background: rgba(139, 69, 19, 0.08);
+ color: var(--codex-sienna);
+ box-shadow: none;
+ border: none;
+ border-radius: 3px;
+ font-weight: 600;
+}
+
+.theme-dark .sidebar-section li.active a {
+ background: rgba(201, 149, 107, 0.1);
+ color: var(--codex-sienna);
+}
+
+.sidebar-footer {
+ border-top-color: var(--codex-rule);
+}
+
+.sidebar-footer a {
+ font-family: 'Iowan Old Style', Palatino, Georgia, serif;
+ color: var(--text-color);
+ opacity: 0.5;
+ font-size: 0.85rem;
+}
+
+.theme-dark .sidebar-footer a {
+ color: var(--text-color);
+}
+
+/* Search */
+.sidebar-search input {
+ background: rgba(0, 0, 0, 0.03);
+ border: 1px solid var(--codex-rule);
+ border-radius: 3px;
+ color: var(--text-color);
+ font-family: inherit;
+}
+
+.theme-dark .sidebar-search input {
+ background: rgba(255, 255, 255, 0.05);
+ color: var(--text-color);
+ border-color: var(--codex-rule);
+}
+
+.sidebar-search input:focus {
+ border-color: var(--codex-sienna);
+ box-shadow: none;
+}
+
+/* ---- Toggle ---- */
+.theme-dark .sidebar-toggle {
+ background: none;
+}
+
+/* ---- Main Content ---- */
+
+.main-content {
+ background-color: var(--bg-color);
+ padding: 2.5rem 2rem;
+}
+
+.main-content > * {
+ max-width: 33em;
+}
+
+/* ---- Feed Items: The Reading Experience ---- */
+
+.feed-item {
+ padding: 0;
+ margin-top: 2.5rem;
+ border-radius: 0;
+ padding-bottom: 2.5rem;
+ border-bottom: none;
+}
+
+/* Decorative separator between items -- a subtle fleuron */
+.feed-item + .feed-item::before {
+ content: '\2766';
+ display: block;
+ text-align: center;
+ font-size: 1rem;
+ color: var(--codex-rule);
+ margin-bottom: 2.5rem;
+ margin-top: -2.5rem;
+}
+
+.feed-item.selected {
+ background: none;
+ box-shadow: inset 3px 0 0 var(--codex-sienna);
+ padding-left: 1rem;
+ border-radius: 0;
+}
+
+.theme-dark .feed-item.selected {
+ background: none;
+}
+
+.item-title {
+ font-family: 'Iowan Old Style', Palatino, Georgia, serif;
+ font-size: 1.65rem;
+ line-height: 1.3;
+ font-weight: 400;
+ color: var(--codex-sienna);
+ letter-spacing: -0.005em;
+}
+
+.item-title:hover {
+ color: var(--codex-ink);
+ text-decoration: none;
+}
+
+.dateline {
+ font-family: 'Iowan Old Style', Palatino, Georgia, serif;
+ font-variant: small-caps;
+ text-transform: lowercase;
+ font-size: 0.78em;
+ letter-spacing: 0.06em;
+ color: var(--codex-muted);
+ margin-bottom: 1.2rem;
+}
+
+.dateline a {
+ color: var(--codex-muted);
+}
+
+.item-description {
+ font-family: 'Iowan Old Style', 'Palatino Linotype', Palatino, 'Book Antiqua', Georgia, serif;
+ font-size: 1rem;
+ line-height: 1.75;
+ color: var(--text-color);
+ hyphens: auto;
+ -webkit-hyphens: auto;
+}
+
+.item-description a {
+ color: var(--codex-sienna);
+ text-decoration: none;
+ border-bottom: 1px solid rgba(139, 69, 19, 0.3);
+}
+
+.item-description a:hover {
+ border-bottom-color: var(--codex-sienna);
+}
+
+.item-description blockquote {
+ border-left: 3px solid var(--codex-rule);
+ margin-left: 0;
+ padding-left: 1.5em;
+ font-style: italic;
+ color: var(--codex-muted);
+}
+
+.item-description pre {
+ background: rgba(0, 0, 0, 0.03);
+ border: 1px solid var(--codex-rule);
+ border-radius: 2px;
+ font-family: 'Courier New', Courier, monospace;
+ font-size: 0.88em;
+}
+
+.item-description img {
+ border-radius: 2px;
+}
+
+/* Star */
+.star-btn.is-starred {
+ color: var(--codex-sienna);
+}
+
+.star-btn.is-unstarred {
+ color: var(--codex-muted);
+ opacity: 0.25;
+}
+
+/* Scrape */
+.scrape-btn {
+ font-family: inherit;
+ font-variant: small-caps;
+ text-transform: lowercase;
+ letter-spacing: 0.06em;
+ color: var(--codex-sienna);
+ border-color: var(--codex-rule);
+ background: transparent;
+ border-radius: 2px;
+ font-weight: 400;
+}
+
+/* ---- Buttons ---- */
+
+button,
+.button {
+ font-family: 'Iowan Old Style', Palatino, Georgia, serif;
+ font-variant: small-caps;
+ text-transform: lowercase;
+ letter-spacing: 0.06em;
+ border-radius: 3px;
+ border-color: var(--codex-rule);
+ background: transparent;
+ color: var(--text-color);
+ font-size: 0.85rem;
+ font-weight: 400;
+}
+
+button:hover,
+.button:hover {
+ border-color: var(--codex-sienna);
+ color: var(--codex-sienna);
+ background: rgba(139, 69, 19, 0.04);
+}
+
+.theme-dark button,
+.theme-dark .button {
+ background: transparent;
+ color: var(--text-color);
+ border-color: var(--codex-rule);
+}
+
+.theme-dark button:hover,
+.theme-dark .button:hover {
+ border-color: var(--codex-sienna);
+ color: var(--codex-sienna);
+ background: rgba(201, 149, 107, 0.08);
+}
+
+button.active,
+.theme-dark button.active {
+ background: var(--codex-sienna);
+ color: var(--codex-paper);
+ border-color: var(--codex-sienna);
+}
+
+/* ---- Settings ---- */
+
+.settings-view {
+ font-family: 'Iowan Old Style', Palatino, Georgia, serif;
+}
+
+.settings-view h2 {
+ font-weight: 400;
+ font-size: 1.8rem;
+ letter-spacing: -0.01em;
+}
+
+.settings-section h3 {
+ font-family: inherit;
+ font-weight: 400;
+ font-variant: small-caps;
+ text-transform: lowercase;
+ letter-spacing: 0.08em;
+ font-size: 1.15rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 1px solid var(--codex-rule);
+}
+
+.settings-group label,
+.data-group label {
+ font-family: inherit;
+ font-variant: small-caps;
+ text-transform: lowercase;
+ letter-spacing: 0.1em;
+ font-size: 0.85rem;
+ font-weight: 400;
+ opacity: 0.65;
+}
+
+.theme-dark .settings-view,
+.theme-dark .settings-view h2,
+.theme-dark .settings-view h3,
+.theme-dark .settings-group label,
+.theme-dark .data-group label {
+ color: var(--text-color) !important;
+}
+
+/* Inputs */
+input[type="text"],
+input[type="url"],
+input[type="search"],
+select {
+ font-family: inherit;
+ background: transparent;
+ border: 1px solid var(--codex-rule);
+ border-radius: 3px;
+ color: var(--text-color);
+}
+
+input:focus,
+select:focus {
+ border-color: var(--codex-sienna);
+ box-shadow: none;
+}
+
+/* Feed list */
+.manage-feed-list {
+ border-top-color: var(--codex-rule);
+}
+
+.manage-feed-item {
+ border-bottom-color: var(--codex-rule);
+}
+
+.feed-title {
+ font-family: inherit;
+ font-weight: 600;
+}
+
+.feed-url {
+ font-family: inherit;
+}
+
+.delete-feed-btn {
+ color: #a0522d !important;
+ border-color: rgba(160, 82, 45, 0.3) !important;
+}
+
+.delete-feed-btn:hover {
+ background: rgba(160, 82, 45, 0.06) !important;
+ border-color: #a0522d !important;
+}
+
+/* ---- Scrollbars ---- */
+
+::-webkit-scrollbar {
+ width: 6px;
+}
+
+::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--codex-rule);
+ border-radius: 3px;
+}
+
+/* ---- Backdrop ---- */
+.sidebar-backdrop {
+ background: rgba(250, 248, 244, 0.6);
+ backdrop-filter: blur(3px);
+}
+
+.theme-dark .sidebar-backdrop {
+ background: rgba(28, 26, 23, 0.6);
+}
diff --git a/frontend-vanilla/public/themes/refined.css b/frontend-vanilla/public/themes/refined.css
new file mode 100644
index 0000000..fab2b96
--- /dev/null
+++ b/frontend-vanilla/public/themes/refined.css
@@ -0,0 +1,272 @@
+/*
+ * Refined Theme
+ * A careful refinement of the default aesthetic.
+ * Fixes spacing inconsistencies, improves typographic rhythm,
+ * and adds subtle polish while preserving the original character.
+ */
+
+/* ---- Typographic Scale & Rhythm ---- */
+/* Base unit: 0.5rem. All spacing aligns to this grid. */
+
+:root {
+ --spacing-xs: 0.25rem;
+ --spacing-sm: 0.5rem;
+ --spacing-md: 1rem;
+ --spacing-lg: 1.5rem;
+ --spacing-xl: 2rem;
+}
+
+/* Dark mode: a warmer, softer link color */
+.theme-dark {
+ --link-color: #a0c4e8;
+ --accent-color: #6fa8d6;
+}
+
+/* Slightly refined base typography */
+body {
+ font-size: 18px;
+ line-height: 1.6;
+ letter-spacing: 0.005em;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ text-rendering: optimizeLegibility;
+}
+
+/* ---- Sidebar Refinements ---- */
+
+.sidebar {
+ padding: var(--spacing-lg);
+ padding-top: 5rem;
+}
+
+.sidebar-section h3 {
+ font-size: 0.65rem;
+ letter-spacing: 0.14em;
+ margin-top: var(--spacing-xl);
+ margin-bottom: var(--spacing-sm);
+}
+
+.sidebar-section li a {
+ padding: 0.35rem 0.75rem;
+ margin: 2px 0;
+ font-size: 0.82rem;
+ letter-spacing: 0.01em;
+ border-radius: 6px;
+}
+
+.sidebar-section li.active a {
+ border-radius: 6px;
+}
+
+.sidebar-footer {
+ padding-top: var(--spacing-lg);
+}
+
+.sidebar-footer a {
+ font-size: 0.82rem;
+ padding: 0.4rem 0.75rem;
+ letter-spacing: 0.01em;
+}
+
+/* ---- Main Content Refinements ---- */
+
+.main-content {
+ padding: var(--spacing-xl) var(--spacing-xl);
+}
+
+@media (max-width: 1024px) {
+ .main-content {
+ padding: var(--spacing-md);
+ padding-top: 4rem;
+ }
+}
+
+/* ---- Feed Item Refinements ---- */
+
+.feed-item {
+ padding: var(--spacing-md) var(--spacing-sm);
+ margin-top: var(--spacing-xl);
+}
+
+.item-title {
+ font-size: 1.7rem;
+ line-height: 1.25;
+ letter-spacing: -0.015em;
+}
+
+.item-header {
+ margin-bottom: var(--spacing-sm);
+}
+
+.dateline {
+ font-size: 0.72em;
+ letter-spacing: 0.02em;
+ margin-bottom: var(--spacing-md);
+}
+
+.item-description {
+ line-height: 1.6;
+ margin-top: var(--spacing-md);
+}
+
+.item-description p {
+ margin-top: 0;
+ margin-bottom: var(--spacing-md);
+}
+
+.item-description p:last-child {
+ margin-bottom: 0;
+}
+
+/* ---- Settings Page Refinements ---- */
+
+.settings-view {
+ padding-top: var(--spacing-lg);
+}
+
+.settings-view h2 {
+ font-size: 1.8rem;
+ font-weight: 700;
+ margin-bottom: var(--spacing-xl);
+ letter-spacing: -0.025em;
+}
+
+.settings-grid {
+ gap: var(--spacing-lg);
+ margin-bottom: var(--spacing-lg);
+}
+
+.settings-section {
+ margin: 0;
+ padding: 0;
+}
+
+.settings-section h3 {
+ font-size: 1.1rem;
+ font-weight: 700;
+ margin-bottom: var(--spacing-sm);
+ padding-bottom: var(--spacing-xs);
+ border-bottom: 1px solid var(--border-color);
+}
+
+.settings-group,
+.data-group {
+ margin-bottom: var(--spacing-sm);
+}
+
+.settings-group label,
+.data-group label {
+ font-size: 0.75rem;
+ letter-spacing: 0.1em;
+ margin-bottom: var(--spacing-xs);
+}
+
+.theme-options {
+ gap: var(--spacing-sm);
+}
+
+/* Consistent button sizing & vertical centering fix */
+button,
+.button {
+ height: 2.2rem;
+ padding: 0 var(--spacing-md);
+ font-size: 0.75rem;
+ letter-spacing: 0.06em;
+ border-radius: 5px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* Add feed form alignment */
+.add-feed-form {
+ gap: var(--spacing-sm);
+ margin-bottom: var(--spacing-lg);
+}
+
+.add-feed-form input {
+ height: 2.2rem;
+}
+
+/* Manage feeds list consistency */
+.manage-feeds-section {
+ margin-top: var(--spacing-xl);
+}
+
+.manage-feed-item {
+ padding: var(--spacing-md) 0;
+ gap: var(--spacing-md);
+}
+
+.feed-title {
+ font-size: 0.95rem;
+ margin-bottom: var(--spacing-xs);
+}
+
+.feed-url {
+ font-size: 0.8rem;
+}
+
+/* Select inputs consistent with buttons */
+select {
+ height: 2.2rem;
+ font-size: 0.85rem;
+}
+
+/* ---- Subtle Polish ---- */
+
+/* Slightly softer selection highlight */
+.feed-item.selected {
+ border-radius: 6px;
+}
+
+/* Smoother scrollbar (webkit) */
+.sidebar-scroll::-webkit-scrollbar,
+.main-content::-webkit-scrollbar {
+ width: 6px;
+}
+
+.sidebar-scroll::-webkit-scrollbar-thumb,
+.main-content::-webkit-scrollbar-thumb {
+ background: rgba(128, 128, 128, 0.3);
+ border-radius: 3px;
+}
+
+.sidebar-scroll::-webkit-scrollbar-thumb:hover,
+.main-content::-webkit-scrollbar-thumb:hover {
+ background: rgba(128, 128, 128, 0.5);
+}
+
+/* Refined focus states */
+input:focus,
+select:focus {
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.15);
+}
+
+/* Cleaner star button */
+.star-btn {
+ font-size: 1.1rem;
+ padding: 0 0 0 0.75rem;
+ transition: opacity 0.15s, color 0.15s;
+}
+
+.star-btn.is-unstarred {
+ opacity: 0.2;
+}
+
+.star-btn.is-unstarred:hover {
+ opacity: 0.5;
+}
+
+/* Import/export button groups: consistent spacing */
+.data-group .button-group {
+ gap: var(--spacing-sm);
+}
+
+/* Better scrape button */
+.scrape-btn {
+ font-size: 0.7rem;
+ padding: 2px 8px;
+ border-radius: 4px;
+ letter-spacing: 0.04em;
+}
diff --git a/frontend-vanilla/public/themes/sakura.css b/frontend-vanilla/public/themes/sakura.css
new file mode 100644
index 0000000..f0fc990
--- /dev/null
+++ b/frontend-vanilla/public/themes/sakura.css
@@ -0,0 +1,466 @@
+/*
+ * Sakura Theme -- Claude's Choice
+ * Named for the app: neko is Japanese for cat.
+ * A theme inspired by the quiet beauty of Japanese graphic design.
+ * Ma (negative space), wabi-sabi (beauty in imperfection),
+ * and the restrained palette of ukiyo-e woodblock prints.
+ * Warm grays, muted pinks, deliberate whitespace, and a sense of calm.
+ */
+
+/* ---- Color System ---- */
+:root {
+ --bg-color: #fafaf8;
+ --text-color: #373737;
+ --sidebar-bg: #f2f0ec;
+ --link-color: #c45b78;
+ --border-color: #e0ddd6;
+ --accent-color: #c45b78;
+
+ --sakura-blossom: #c45b78;
+ --sakura-petal: #f0c4cf;
+ --sakura-stone: #8c8a85;
+ --sakura-ink: #373737;
+ --sakura-paper: #fafaf8;
+ --sakura-cloud: #f2f0ec;
+ --sakura-shadow: #e0ddd6;
+ --sakura-indigo: #4a5568;
+
+ color-scheme: light;
+}
+
+/* Dark: Evening in Kyoto */
+.theme-dark {
+ --bg-color: #1a1a1e;
+ --text-color: #d5d3ce;
+ --sidebar-bg: #222226;
+ --link-color: #e8879e;
+ --border-color: #3a3a3e;
+ --accent-color: #e8879e;
+
+ --sakura-blossom: #e8879e;
+ --sakura-petal: #5c3a45;
+ --sakura-stone: #8c8a85;
+ --sakura-ink: #d5d3ce;
+ --sakura-paper: #1a1a1e;
+ --sakura-cloud: #222226;
+ --sakura-shadow: #3a3a3e;
+ --sakura-indigo: #a0aec0;
+}
+
+/* ---- Typography ---- */
+
+body {
+ font-family: 'Hiragino Mincho ProN', 'Noto Serif', 'Iowan Old Style', Palatino, Georgia, serif;
+ font-size: 18px;
+ line-height: 1.8;
+ letter-spacing: 0.01em;
+ background-color: var(--bg-color);
+ color: var(--text-color);
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
+}
+
+/* ---- Sidebar ---- */
+
+.sidebar {
+ background: var(--sakura-cloud);
+ backdrop-filter: none;
+ -webkit-backdrop-filter: none;
+ border-right: 1px solid var(--sakura-shadow);
+ padding: 1.5rem;
+ padding-top: 5rem;
+}
+
+.theme-dark .sidebar {
+ background: var(--sakura-cloud);
+ border-right-color: var(--sakura-shadow);
+}
+
+.sidebar-section h3 {
+ font-family: 'Hiragino Kaku Gothic ProN', 'Noto Sans JP', 'Helvetica Neue', Helvetica, sans-serif;
+ font-size: 0.6rem;
+ font-weight: 500;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ color: var(--sakura-stone);
+ opacity: 1;
+}
+
+.theme-dark .sidebar-section h3 {
+ color: var(--sakura-stone);
+}
+
+.sidebar-section li a {
+ font-family: 'Hiragino Kaku Gothic ProN', 'Noto Sans JP', 'Helvetica Neue', Helvetica, sans-serif;
+ color: var(--text-color);
+ font-size: 0.82rem;
+ border-radius: 4px;
+ font-weight: 400;
+ letter-spacing: 0.01em;
+}
+
+.theme-dark .sidebar-section li a {
+ color: var(--text-color);
+}
+
+.sidebar-section li a:hover {
+ background: rgba(196, 91, 120, 0.06);
+ color: var(--sakura-blossom);
+ transform: none;
+}
+
+.theme-dark .sidebar-section li a:hover {
+ background: rgba(232, 135, 158, 0.08);
+ color: var(--sakura-blossom);
+}
+
+.sidebar-section li.active a {
+ background: rgba(196, 91, 120, 0.08);
+ color: var(--sakura-blossom);
+ box-shadow: none;
+ border: none;
+ border-left: 2px solid var(--sakura-blossom);
+ border-radius: 0 4px 4px 0;
+ padding-left: calc(0.8rem - 2px);
+ font-weight: 600;
+}
+
+.theme-dark .sidebar-section li.active a {
+ background: rgba(232, 135, 158, 0.1);
+ color: var(--sakura-blossom);
+}
+
+.sidebar-footer {
+ border-top: 1px solid var(--sakura-shadow);
+}
+
+.sidebar-footer a {
+ font-family: 'Hiragino Kaku Gothic ProN', 'Noto Sans JP', 'Helvetica Neue', Helvetica, sans-serif;
+ color: var(--text-color);
+ opacity: 0.45;
+ font-size: 0.82rem;
+}
+
+.theme-dark .sidebar-footer a {
+ color: var(--text-color);
+}
+
+.sidebar-footer a:hover {
+ opacity: 1;
+ color: var(--sakura-blossom);
+ background: rgba(196, 91, 120, 0.04);
+}
+
+/* Search */
+.sidebar-search input {
+ background: rgba(0, 0, 0, 0.03);
+ border: 1px solid var(--sakura-shadow);
+ border-radius: 4px;
+ color: var(--text-color);
+ font-family: 'Hiragino Kaku Gothic ProN', 'Noto Sans JP', sans-serif;
+}
+
+.theme-dark .sidebar-search input {
+ background: rgba(255, 255, 255, 0.04);
+ color: var(--text-color);
+ border-color: var(--sakura-shadow);
+}
+
+.sidebar-search input:focus {
+ border-color: var(--sakura-blossom);
+ box-shadow: 0 0 0 2px rgba(196, 91, 120, 0.1);
+}
+
+/* ---- Toggle ---- */
+.theme-dark .sidebar-toggle {
+ background: none;
+}
+
+/* ---- Main Content ---- */
+
+.main-content {
+ background-color: var(--bg-color);
+ padding: 2.5rem 2rem;
+}
+
+.main-content > * {
+ max-width: 34em;
+}
+
+/* ---- Feed Items ---- */
+
+.feed-item {
+ padding: 0.5rem 0;
+ margin-top: 2.5rem;
+ border-radius: 0;
+ border-bottom: none;
+}
+
+/* Subtle separator -- a single thin rule, Japanese-style restraint */
+.feed-item + .feed-item {
+ border-top: 1px solid var(--sakura-shadow);
+ padding-top: 2.5rem;
+}
+
+.feed-item.selected {
+ background: rgba(196, 91, 120, 0.03);
+ box-shadow: inset 3px 0 0 var(--sakura-blossom);
+ padding-left: 1rem;
+ border-radius: 0;
+}
+
+.theme-dark .feed-item.selected {
+ background: rgba(232, 135, 158, 0.04);
+}
+
+.item-title {
+ font-family: 'Hiragino Mincho ProN', 'Noto Serif', Palatino, Georgia, serif;
+ font-size: 1.55rem;
+ line-height: 1.4;
+ font-weight: 600;
+ color: var(--sakura-ink);
+ letter-spacing: 0;
+}
+
+.theme-dark .item-title {
+ color: var(--text-color);
+}
+
+.item-title:hover {
+ color: var(--sakura-blossom);
+ text-decoration: none;
+}
+
+.dateline {
+ font-family: 'Hiragino Kaku Gothic ProN', 'Noto Sans JP', 'Helvetica Neue', sans-serif;
+ font-size: 0.72em;
+ letter-spacing: 0.03em;
+ color: var(--sakura-stone);
+ margin-bottom: 1rem;
+}
+
+.dateline a {
+ color: var(--sakura-stone);
+}
+
+.dateline a:hover {
+ color: var(--sakura-blossom);
+}
+
+.item-description {
+ font-family: 'Hiragino Mincho ProN', 'Noto Serif', 'Iowan Old Style', Palatino, Georgia, serif;
+ font-size: 1rem;
+ line-height: 1.85;
+ color: var(--text-color);
+}
+
+.item-description a {
+ color: var(--sakura-blossom);
+ text-decoration: none;
+ border-bottom: 1px solid rgba(196, 91, 120, 0.25);
+}
+
+.item-description a:hover {
+ border-bottom-color: var(--sakura-blossom);
+}
+
+.item-description blockquote {
+ border-left: 2px solid var(--sakura-petal);
+ margin-left: 0;
+ padding-left: 1.25em;
+ color: var(--sakura-stone);
+}
+
+.item-description pre {
+ background: rgba(0, 0, 0, 0.02);
+ border: 1px solid var(--sakura-shadow);
+ border-radius: 3px;
+ font-family: 'Courier New', Courier, monospace;
+ font-size: 0.88em;
+}
+
+.item-description img {
+ border-radius: 3px;
+}
+
+/* Star -- blossom colored when starred */
+.star-btn.is-starred {
+ color: var(--sakura-blossom);
+}
+
+.star-btn.is-unstarred {
+ color: var(--sakura-stone);
+ opacity: 0.2;
+}
+
+/* Scrape */
+.scrape-btn {
+ font-family: 'Hiragino Kaku Gothic ProN', 'Noto Sans JP', sans-serif;
+ color: var(--sakura-blossom);
+ border-color: var(--sakura-shadow);
+ background: transparent;
+ border-radius: 3px;
+ font-size: 0.72rem;
+ letter-spacing: 0.04em;
+}
+
+/* ---- Buttons ---- */
+
+button,
+.button {
+ font-family: 'Hiragino Kaku Gothic ProN', 'Noto Sans JP', 'Helvetica Neue', sans-serif;
+ border-radius: 4px;
+ border-color: var(--sakura-shadow);
+ background: transparent;
+ color: var(--text-color);
+ font-size: 0.78rem;
+ font-weight: 500;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+}
+
+button:hover,
+.button:hover {
+ border-color: var(--sakura-blossom);
+ color: var(--sakura-blossom);
+ background: rgba(196, 91, 120, 0.04);
+}
+
+.theme-dark button,
+.theme-dark .button {
+ background: transparent;
+ color: var(--text-color);
+ border-color: var(--sakura-shadow);
+}
+
+.theme-dark button:hover,
+.theme-dark .button:hover {
+ border-color: var(--sakura-blossom);
+ color: var(--sakura-blossom);
+ background: rgba(232, 135, 158, 0.06);
+}
+
+button.active,
+.theme-dark button.active {
+ background: var(--sakura-blossom);
+ color: #ffffff;
+ border-color: var(--sakura-blossom);
+}
+
+/* ---- Settings ---- */
+
+.settings-view {
+ font-family: 'Hiragino Kaku Gothic ProN', 'Noto Sans JP', 'Helvetica Neue', sans-serif;
+}
+
+.settings-view h2 {
+ font-family: 'Hiragino Mincho ProN', 'Noto Serif', Palatino, Georgia, serif;
+ font-weight: 400;
+ font-size: 1.8rem;
+ letter-spacing: 0.02em;
+}
+
+.settings-section h3 {
+ font-family: inherit;
+ font-weight: 500;
+ font-size: 1rem;
+ letter-spacing: 0.04em;
+ padding-bottom: 0.5rem;
+ border-bottom: 1px solid var(--sakura-shadow);
+}
+
+.settings-group label,
+.data-group label {
+ font-family: inherit;
+ letter-spacing: 0.1em;
+ font-size: 0.72rem;
+ font-weight: 500;
+}
+
+.theme-dark .settings-view,
+.theme-dark .settings-view h2,
+.theme-dark .settings-view h3,
+.theme-dark .settings-group label,
+.theme-dark .data-group label {
+ color: var(--text-color) !important;
+}
+
+/* Inputs */
+input[type="text"],
+input[type="url"],
+input[type="search"],
+select {
+ font-family: inherit;
+ background: transparent;
+ border: 1px solid var(--sakura-shadow);
+ border-radius: 4px;
+ color: var(--text-color);
+}
+
+input:focus,
+select:focus {
+ border-color: var(--sakura-blossom);
+ box-shadow: 0 0 0 2px rgba(196, 91, 120, 0.1);
+}
+
+/* Feed list */
+.manage-feed-list {
+ border-top-color: var(--sakura-shadow);
+}
+
+.manage-feed-item {
+ border-bottom-color: var(--sakura-shadow);
+}
+
+.feed-title {
+ font-family: 'Hiragino Kaku Gothic ProN', 'Noto Sans JP', sans-serif;
+ font-weight: 600;
+}
+
+.feed-url {
+ font-family: 'Hiragino Kaku Gothic ProN', 'Noto Sans JP', sans-serif;
+}
+
+.delete-feed-btn {
+ color: #c45b78 !important;
+ border-color: rgba(196, 91, 120, 0.3) !important;
+}
+
+.delete-feed-btn:hover {
+ background: rgba(196, 91, 120, 0.06) !important;
+ border-color: var(--sakura-blossom) !important;
+}
+
+/* ---- Scrollbars ---- */
+
+::-webkit-scrollbar {
+ width: 5px;
+}
+
+::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--sakura-shadow);
+ border-radius: 3px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: var(--sakura-stone);
+}
+
+/* ---- Backdrop ---- */
+.sidebar-backdrop {
+ background: rgba(250, 250, 248, 0.5);
+ backdrop-filter: blur(4px);
+}
+
+.theme-dark .sidebar-backdrop {
+ background: rgba(26, 26, 30, 0.5);
+}
+
+/* ---- Loading/Empty ---- */
+.loading, .empty {
+ color: var(--sakura-stone);
+}
diff --git a/frontend-vanilla/public/themes/terminal.css b/frontend-vanilla/public/themes/terminal.css
new file mode 100644
index 0000000..dd9c1b2
--- /dev/null
+++ b/frontend-vanilla/public/themes/terminal.css
@@ -0,0 +1,481 @@
+/*
+ * Terminal Theme
+ * Born-digital. CRT phosphor. The hum of the machine.
+ * Inspired by green-screen terminals, REZ, and the beauty of text on glass.
+ * Content-first -- the digital medium is the accent, not the show.
+ */
+
+/* ---- Color System ---- */
+/* Light mode: bright terminal on a pale steel background */
+:root {
+ --bg-color: #f0f2f0;
+ --text-color: #1e2d1e;
+ --sidebar-bg: #e4e8e4;
+ --link-color: #1a7a2e;
+ --border-color: #c4cec4;
+ --accent-color: #1a7a2e;
+
+ --terminal-accent: #1a7a2e;
+ --terminal-glow: rgba(26, 122, 46, 0.08);
+ --terminal-dim: rgba(26, 122, 46, 0.03);
+ --terminal-amber: #a66800;
+ --terminal-red: #c62828;
+
+ color-scheme: light dark;
+}
+
+/* Dark mode: phosphor green on black */
+.theme-dark {
+ --bg-color: #0a0e0a;
+ --text-color: #b0bfb0;
+ --sidebar-bg: #0d110d;
+ --link-color: #4ae54a;
+ --border-color: #1e2a1e;
+ --accent-color: #4ae54a;
+
+ --terminal-accent: #4ae54a;
+ --terminal-glow: rgba(74, 229, 74, 0.08);
+ --terminal-dim: rgba(74, 229, 74, 0.03);
+ --terminal-amber: #ffab00;
+ --terminal-red: #ff5252;
+}
+
+/* ---- Typography ---- */
+
+body {
+ font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', 'JetBrains Mono', 'Courier New', monospace;
+ font-size: 15px;
+ line-height: 1.65;
+ letter-spacing: 0.02em;
+ background-color: var(--bg-color);
+ color: var(--text-color);
+ -webkit-font-smoothing: antialiased;
+}
+
+/* Subtle scanline overlay -- only in dark mode */
+.theme-dark body::after {
+ content: '';
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ z-index: 9999;
+ background: repeating-linear-gradient(
+ to bottom,
+ transparent,
+ transparent 2px,
+ rgba(0, 0, 0, 0.03) 2px,
+ rgba(0, 0, 0, 0.03) 4px
+ );
+}
+
+/* ---- Sidebar ---- */
+
+.sidebar {
+ background: var(--sidebar-bg);
+ backdrop-filter: none;
+ -webkit-backdrop-filter: none;
+ border-right: 1px solid var(--border-color);
+ font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', 'Courier New', monospace;
+}
+
+.theme-dark .sidebar {
+ background: var(--sidebar-bg);
+ border-right-color: var(--border-color);
+}
+
+.sidebar-section h3 {
+ font-family: inherit;
+ color: var(--terminal-accent);
+ opacity: 0.6;
+ font-size: 0.65rem;
+ letter-spacing: 0.2em;
+}
+
+.theme-dark .sidebar-section h3 {
+ color: var(--terminal-accent);
+}
+
+.sidebar-section li a {
+ font-family: inherit;
+ color: var(--text-color);
+ border-radius: 2px;
+ font-size: 0.8rem;
+}
+
+.theme-dark .sidebar-section li a {
+ color: var(--text-color);
+}
+
+.sidebar-section li a:hover {
+ background: var(--terminal-glow);
+ color: var(--terminal-accent);
+ transform: none;
+}
+
+.theme-dark .sidebar-section li a:hover {
+ background: var(--terminal-glow);
+ color: var(--terminal-accent);
+}
+
+.sidebar-section li.active a {
+ background: var(--terminal-glow);
+ color: var(--terminal-accent);
+ box-shadow: inset 2px 0 0 var(--terminal-accent);
+ border: none;
+ border-radius: 0;
+ font-weight: 500;
+}
+
+.theme-dark .sidebar-section li.active a {
+ background: var(--terminal-glow);
+ color: var(--terminal-accent);
+}
+
+.sidebar-footer {
+ border-top-color: var(--border-color);
+}
+
+.sidebar-footer a {
+ font-family: inherit;
+ color: var(--text-color);
+ opacity: 0.5;
+ font-size: 0.8rem;
+}
+
+.theme-dark .sidebar-footer a {
+ color: var(--text-color);
+}
+
+.sidebar-footer a:hover {
+ color: var(--terminal-accent);
+ background: var(--terminal-dim);
+ opacity: 1;
+}
+
+/* Search */
+.sidebar-search input {
+ background: var(--terminal-dim);
+ border: 1px solid var(--border-color);
+ border-radius: 2px;
+ color: var(--text-color);
+ font-family: inherit;
+}
+
+.theme-dark .sidebar-search input {
+ background: var(--terminal-dim);
+ color: var(--text-color);
+ border-color: var(--border-color);
+}
+
+.sidebar-search input:focus {
+ border-color: var(--terminal-accent);
+ box-shadow: 0 0 8px rgba(74, 229, 74, 0.15);
+}
+
+.sidebar-search input::placeholder {
+ color: rgba(0, 0, 0, 0.25);
+}
+
+.theme-dark .sidebar-search input::placeholder {
+ color: rgba(176, 191, 176, 0.3);
+}
+
+/* ---- Toggle ---- */
+.sidebar-toggle {
+ opacity: 0.6;
+}
+
+.theme-dark .sidebar-toggle {
+ background: none;
+}
+
+.sidebar-toggle:hover {
+ opacity: 1;
+}
+
+/* ---- Main Content ---- */
+
+.main-content {
+ background-color: var(--bg-color);
+}
+
+/* ---- Feed Items ---- */
+
+.feed-item {
+ border-radius: 0;
+ padding: 1.25rem 0.5rem;
+ margin-top: 1.5rem;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.feed-item.selected {
+ background-color: var(--terminal-dim);
+ box-shadow: inset 3px 0 0 var(--terminal-accent);
+ border-radius: 0;
+}
+
+.theme-dark .feed-item.selected {
+ background-color: var(--terminal-dim);
+}
+
+.item-title {
+ font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', 'Courier New', monospace;
+ font-size: 1.4rem;
+ line-height: 1.35;
+ color: var(--terminal-accent);
+ font-weight: 600;
+ letter-spacing: -0.01em;
+}
+
+.item-title:hover {
+ text-decoration: none;
+}
+
+.theme-dark .item-title:hover {
+ color: #ffffff;
+ text-shadow: 0 0 12px rgba(74, 229, 74, 0.3);
+}
+
+.dateline {
+ color: var(--text-color);
+ opacity: 0.35;
+ font-family: inherit;
+ font-size: 0.75em;
+ letter-spacing: 0.04em;
+}
+
+.dateline a {
+ color: inherit;
+}
+
+.dateline a:hover {
+ color: var(--terminal-accent);
+ opacity: 1;
+}
+
+.item-description {
+ font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', 'Courier New', monospace;
+ font-size: 0.9rem;
+ line-height: 1.7;
+ color: var(--text-color);
+}
+
+.item-description a {
+ color: var(--terminal-accent);
+}
+
+.theme-dark .item-description a:hover {
+ text-shadow: 0 0 8px rgba(74, 229, 74, 0.2);
+}
+
+.item-description pre {
+ background: var(--terminal-dim);
+ border: 1px solid var(--border-color);
+ border-radius: 2px;
+ font-family: inherit;
+}
+
+/* Star */
+.star-btn.is-starred {
+ color: var(--terminal-amber);
+}
+
+.star-btn.is-unstarred {
+ color: var(--text-color);
+ opacity: 0.15;
+}
+
+/* Scrape */
+.scrape-btn {
+ font-family: inherit;
+ color: var(--terminal-accent);
+ border-color: var(--border-color);
+ background: transparent;
+ border-radius: 2px;
+}
+
+.scrape-btn:hover {
+ border-color: var(--terminal-accent);
+}
+
+.theme-dark .scrape-btn:hover {
+ box-shadow: 0 0 6px rgba(74, 229, 74, 0.15);
+}
+
+/* ---- Buttons ---- */
+
+button,
+.button {
+ font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', 'Courier New', monospace;
+ border-radius: 2px;
+ border-color: var(--border-color);
+ background: transparent;
+ color: var(--text-color);
+ font-size: 0.72rem;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+button:hover,
+.button:hover {
+ border-color: var(--terminal-accent);
+ color: var(--terminal-accent);
+ background: var(--terminal-dim);
+}
+
+.theme-dark button,
+.theme-dark .button {
+ background: transparent;
+ color: var(--text-color);
+ border-color: var(--border-color);
+}
+
+.theme-dark button:hover,
+.theme-dark .button:hover {
+ border-color: var(--terminal-accent);
+ color: var(--terminal-accent);
+ background: var(--terminal-dim);
+}
+
+button.active,
+.theme-dark button.active {
+ background: var(--terminal-accent);
+ color: var(--bg-color);
+ border-color: var(--terminal-accent);
+}
+
+/* ---- Settings ---- */
+
+.settings-view {
+ font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', 'Courier New', monospace;
+}
+
+.settings-view h2 {
+ color: var(--terminal-accent);
+ font-weight: 600;
+ letter-spacing: 0.05em;
+ font-size: 1.5rem;
+}
+
+.settings-section h3 {
+ color: var(--text-color);
+ font-family: inherit;
+ font-size: 1rem;
+ letter-spacing: 0.04em;
+}
+
+.settings-group label,
+.data-group label {
+ color: var(--text-color);
+ font-family: inherit;
+ letter-spacing: 0.12em;
+}
+
+.theme-dark .settings-view,
+.theme-dark .settings-view h2,
+.theme-dark .settings-view h3,
+.theme-dark .settings-group label,
+.theme-dark .data-group label {
+ color: var(--text-color) !important;
+}
+
+.theme-dark .settings-view h2 {
+ color: var(--terminal-accent) !important;
+}
+
+/* Inputs */
+input[type="text"],
+input[type="url"],
+input[type="search"],
+select {
+ font-family: inherit;
+ background: var(--terminal-dim);
+ border: 1px solid var(--border-color);
+ border-radius: 2px;
+ color: var(--text-color);
+}
+
+input:focus,
+select:focus {
+ border-color: var(--terminal-accent);
+ box-shadow: 0 0 8px rgba(74, 229, 74, 0.15);
+}
+
+/* Feed list in settings */
+.manage-feed-list {
+ border-top-color: var(--border-color);
+}
+
+.manage-feed-item {
+ border-bottom-color: var(--border-color);
+}
+
+.feed-title {
+ font-family: inherit;
+ color: var(--text-color);
+}
+
+.feed-url {
+ font-family: inherit;
+ color: var(--text-color);
+ opacity: 0.4;
+}
+
+/* Delete button */
+.delete-feed-btn {
+ color: var(--terminal-red) !important;
+ border-color: rgba(198, 40, 40, 0.3) !important;
+}
+
+.delete-feed-btn:hover {
+ border-color: var(--terminal-red) !important;
+ background: rgba(198, 40, 40, 0.06) !important;
+}
+
+.theme-dark .delete-feed-btn:hover {
+ box-shadow: 0 0 6px rgba(255, 82, 82, 0.2) !important;
+ background: rgba(255, 82, 82, 0.08) !important;
+}
+
+/* ---- Scrollbars ---- */
+
+::-webkit-scrollbar {
+ width: 5px;
+}
+
+::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+ background: rgba(74, 229, 74, 0.15);
+ border-radius: 0;
+}
+
+.theme-dark ::-webkit-scrollbar-thumb {
+ background: rgba(74, 229, 74, 0.15);
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: rgba(74, 229, 74, 0.3);
+}
+
+/* ---- Backdrop ---- */
+.sidebar-backdrop {
+ background: rgba(240, 242, 240, 0.6);
+ backdrop-filter: blur(2px);
+}
+
+.theme-dark .sidebar-backdrop {
+ background: rgba(0, 0, 0, 0.6);
+}
+
+/* ---- Loading/Empty States ---- */
+.loading, .empty {
+ color: var(--text-color);
+ opacity: 0.4;
+ font-family: inherit;
+}
diff --git a/frontend-vanilla/src/main.ts b/frontend-vanilla/src/main.ts
index 9c8f2b3..901e316 100644
--- a/frontend-vanilla/src/main.ts
+++ b/frontend-vanilla/src/main.ts
@@ -13,6 +13,24 @@ declare global {
}
}
+// Style theme management: load/unload CSS files
+const STYLE_THEMES = ['default', 'refined', 'terminal', 'codex', 'sakura'] as const;
+
+function loadStyleTheme(theme: string) {
+ // Remove any existing theme stylesheet
+ const existing = document.getElementById('style-theme-link');
+ if (existing) existing.remove();
+
+ // 'default' means no extra stylesheet
+ if (theme === 'default') return;
+
+ const link = document.createElement('link');
+ link.id = 'style-theme-link';
+ link.rel = 'stylesheet';
+ link.href = `/v3/themes/${theme}.css`;
+ document.head.appendChild(link);
+}
+
// Global App State
let activeItemId: number | null = null;
@@ -58,6 +76,15 @@ export function renderLayout() {
-->
</div>
<div class="sidebar-footer">
+ <div class="sidebar-quick-controls">
+ <button id="sidebar-theme-toggle" class="sidebar-icon-btn" title="${store.theme === 'light' ? 'Switch to dark mode' : 'Switch to light mode'}">${store.theme === 'light' ? '☽' : '☀'}</button>
+ <span class="sidebar-controls-divider"></span>
+ <button class="sidebar-icon-btn sidebar-style-btn ${store.styleTheme === 'default' ? 'active' : ''}" data-style-theme="default" title="Default">○</button>
+ <button class="sidebar-icon-btn sidebar-style-btn ${store.styleTheme === 'refined' ? 'active' : ''}" data-style-theme="refined" title="Refined">◆</button>
+ <button class="sidebar-icon-btn sidebar-style-btn ${store.styleTheme === 'terminal' ? 'active' : ''}" data-style-theme="terminal" title="Terminal">▮</button>
+ <button class="sidebar-icon-btn sidebar-style-btn ${store.styleTheme === 'codex' ? 'active' : ''}" data-style-theme="codex" title="Codex">❧</button>
+ <button class="sidebar-icon-btn sidebar-style-btn ${store.styleTheme === 'sakura' ? 'active' : ''}" data-style-theme="sakura" title="Sakura">❀</button>
+ </div>
<a href="/v3/settings" data-nav="settings">Settings</a>
<a href="#" id="logout-button">Logout</a>
</div>
@@ -86,6 +113,19 @@ export function attachLayoutListeners() {
logout();
});
+ // Sidebar quick controls: light/dark toggle
+ document.getElementById('sidebar-theme-toggle')?.addEventListener('click', () => {
+ store.setTheme(store.theme === 'light' ? 'dark' : 'light');
+ });
+
+ // Sidebar quick controls: style theme emoji buttons
+ document.querySelectorAll('.sidebar-style-btn').forEach(btn => {
+ btn.addEventListener('click', () => {
+ const theme = btn.getAttribute('data-style-theme');
+ if (theme) store.setStyleTheme(theme);
+ });
+ });
+
document.getElementById('sidebar-toggle-btn')?.addEventListener('click', () => {
store.toggleSidebar();
});
@@ -394,7 +434,20 @@ export function renderSettings() {
<button class="${store.theme === 'dark' ? 'active' : ''}" data-theme="dark">Dark</button>
</div>
</div>
- <div class="settings-group" style="margin-top: 1rem;">
+ </section>
+
+ <section class="settings-section">
+ <h3>Style</h3>
+ <div class="settings-group">
+ <div class="theme-options" id="style-theme-options">
+ ${STYLE_THEMES.map(t => `<button class="${store.styleTheme === t ? 'active' : ''}" data-style-theme="${t}">${t.charAt(0).toUpperCase() + t.slice(1)}</button>`).join('\n ')}
+ </div>
+ </div>
+ </section>
+
+ <section class="settings-section">
+ <h3>Fonts</h3>
+ <div class="settings-group">
<label>System & headings</label>
<select id="heading-font-selector" style="margin-bottom: 1rem;">
<option value="default" ${store.headingFontTheme === 'default' ? 'selected' : ''}>System (Helvetica Neue)</option>
@@ -445,7 +498,7 @@ export function renderSettings() {
// --- Listeners ---
- // Theme
+ // Theme (light/dark)
document.getElementById('theme-options')?.addEventListener('click', (e) => {
const btn = (e.target as HTMLElement).closest('button');
if (btn) {
@@ -454,6 +507,14 @@ export function renderSettings() {
}
});
+ // Style Theme
+ document.getElementById('style-theme-options')?.addEventListener('click', (e) => {
+ const btn = (e.target as HTMLElement).closest('button');
+ if (btn) {
+ store.setStyleTheme(btn.getAttribute('data-style-theme')!);
+ }
+ });
+
// Heading Font
document.getElementById('heading-font-selector')?.addEventListener('change', (e) => {
store.setHeadingFontTheme((e.target as HTMLSelectElement).value);
@@ -825,6 +886,12 @@ store.on('theme-updated', () => {
// Re-apply classes with proper specificity logic
appEl.className = `theme-${store.theme} font-${store.fontTheme} heading-font-${store.headingFontTheme}`;
}
+ // Update sidebar toggle icon
+ const toggleBtn = document.getElementById('sidebar-theme-toggle');
+ if (toggleBtn) {
+ toggleBtn.textContent = store.theme === 'light' ? '☽' : '☀';
+ toggleBtn.title = store.theme === 'light' ? 'Switch to dark mode' : 'Switch to light mode';
+ }
// Also re-render settings if we are on settings page to update active state of buttons
if (router.getCurrentRoute().path === '/settings') {
renderSettings();
@@ -844,6 +911,18 @@ store.on('sidebar-toggle', () => {
}
});
+store.on('style-theme-updated', () => {
+ loadStyleTheme(store.styleTheme);
+ // Update sidebar style emoji buttons
+ document.querySelectorAll('.sidebar-style-btn').forEach(btn => {
+ btn.classList.toggle('active', btn.getAttribute('data-style-theme') === store.styleTheme);
+ });
+ // Re-render settings if on settings page to update active state
+ if (router.getCurrentRoute().path === '/settings') {
+ renderSettings();
+ }
+});
+
store.on('items-updated', renderItems);
store.on('loading-state-changed', renderItems);
@@ -864,6 +943,7 @@ export async function init() {
}
renderLayout();
+ loadStyleTheme(store.styleTheme);
renderFilters();
try {
await Promise.all([fetchFeeds(), fetchTags()]);
diff --git a/frontend-vanilla/src/store.ts b/frontend-vanilla/src/store.ts
index dc79339..bfbc55e 100644
--- a/frontend-vanilla/src/store.ts
+++ b/frontend-vanilla/src/store.ts
@@ -1,6 +1,6 @@
import type { Feed, Item, Category } from './types.ts';
-export type StoreEvent = 'feeds-updated' | 'tags-updated' | 'items-updated' | 'active-feed-updated' | 'active-tag-updated' | 'loading-state-changed' | 'filter-updated' | 'search-updated' | 'theme-updated' | 'sidebar-toggle';
+export type StoreEvent = 'feeds-updated' | 'tags-updated' | 'items-updated' | 'active-feed-updated' | 'active-tag-updated' | 'loading-state-changed' | 'filter-updated' | 'search-updated' | 'theme-updated' | 'sidebar-toggle' | 'style-theme-updated';
export type FilterType = 'unread' | 'all' | 'starred';
@@ -34,6 +34,7 @@ export class Store extends EventTarget {
theme: string = localStorage.getItem('neko-theme') || 'light';
fontTheme: string = localStorage.getItem('neko-font-theme') || 'default';
headingFontTheme: string = localStorage.getItem('neko-heading-font-theme') || 'default';
+ styleTheme: string = localStorage.getItem('neko-style-theme') || 'default';
sidebarVisible: boolean = getInitialSidebarVisible();
setFeeds(feeds: Feed[]) {
@@ -108,6 +109,12 @@ export class Store extends EventTarget {
this.emit('theme-updated');
}
+ setStyleTheme(styleTheme: string) {
+ this.styleTheme = styleTheme;
+ localStorage.setItem('neko-style-theme', styleTheme);
+ this.emit('style-theme-updated');
+ }
+
setSidebarVisible(visible: boolean) {
this.sidebarVisible = visible;
setSidebarCookie(visible);
diff --git a/frontend-vanilla/src/style.css b/frontend-vanilla/src/style.css
index e9512b7..f58ae24 100644
--- a/frontend-vanilla/src/style.css
+++ b/frontend-vanilla/src/style.css
@@ -215,6 +215,65 @@ html {
opacity: 1;
}
+/* Quick controls row in sidebar footer */
+.sidebar-quick-controls {
+ display: flex;
+ align-items: center;
+ gap: 0.15rem;
+ margin-bottom: 0.25rem;
+}
+
+.sidebar-controls-divider {
+ width: 1px;
+ height: 1rem;
+ background: rgba(128, 128, 128, 0.25);
+ margin: 0 0.25rem;
+}
+
+.sidebar-icon-btn {
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-size: 0.8rem;
+ padding: 0.25rem 0.35rem;
+ border-radius: 4px;
+ color: var(--text-color);
+ opacity: 0.35;
+ transition: opacity 0.15s, background 0.15s;
+ font-family: inherit;
+ text-transform: none;
+ font-weight: 400;
+ height: auto;
+ line-height: 1;
+}
+
+.sidebar-icon-btn:hover {
+ opacity: 0.8;
+ background: rgba(255, 255, 255, 0.08);
+ border: none;
+}
+
+.sidebar-icon-btn.active {
+ opacity: 1;
+ background: rgba(255, 255, 255, 0.15);
+ color: var(--text-color);
+}
+
+.theme-dark .sidebar-icon-btn {
+ color: rgba(0, 0, 0, 0.87);
+ border: none;
+ background: none;
+}
+
+.theme-dark .sidebar-icon-btn:hover {
+ background: rgba(0, 0, 0, 0.06);
+ border: none;
+}
+
+.theme-dark .sidebar-icon-btn.active {
+ background: rgba(0, 0, 0, 0.12);
+}
+
/* Main Content area - always fills full width (sidebar overlays) */
.main-content {
width: 100%;