diff options
Diffstat (limited to 'frontend-vanilla/src')
| -rw-r--r-- | frontend-vanilla/src/main.ts | 84 | ||||
| -rw-r--r-- | frontend-vanilla/src/store.ts | 9 | ||||
| -rw-r--r-- | frontend-vanilla/src/style.css | 59 |
3 files changed, 149 insertions, 3 deletions
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%; |
