From 41f28b7d657e5be615db2db5faa371459f234430 Mon Sep 17 00:00:00 2001 From: Tim Disney Date: Mon, 6 Jul 2026 09:40:07 -0700 Subject: [PATCH 1/2] add home to mobile switcher --- .../components/feed/MobileFeedSwitcher.svelte | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/components/feed/MobileFeedSwitcher.svelte b/frontend/src/lib/components/feed/MobileFeedSwitcher.svelte index c739846..672f61b 100644 --- a/frontend/src/lib/components/feed/MobileFeedSwitcher.svelte +++ b/frontend/src/lib/components/feed/MobileFeedSwitcher.svelte @@ -39,6 +39,7 @@ let savedCount = $derived(itemLabelsStore.savedCount); type IconName = + | 'home' | 'inbox' | 'bookmark' | 'share' @@ -100,7 +101,7 @@ type SectionData = { section: string; - groupId?: 'everything' | 'saved' | 'other' | 'sources'; + groupId?: 'home' | 'everything' | 'saved' | 'other' | 'sources'; items: NavItem[]; }; @@ -149,6 +150,12 @@ let filteredItems = $derived.by((): SectionData[] => { const query = searchQuery.toLowerCase().trim(); + const homeItem: NavItem = { + type: 'view', + id: 'home', + label: 'Home', + icon: 'home', + }; const everythingItem: NavItem = { type: 'view', id: 'all', @@ -264,6 +271,11 @@ const sections: SectionData[] = []; + // Home: the default landing surface (a route, not a feed filter) + if (filterItem(homeItem)) { + sections.push({ section: '', groupId: 'home', items: [homeItem] }); + } + // Everything group: Everything + source channels (+ suggestions rendered inline in template) const everythingGroup = [everythingItem, ...sourceChannelItems].filter(filterItem); if (everythingGroup.length > 0 || (!query && channelSuggestions.suggestions.length > 0)) { @@ -300,6 +312,7 @@ // Get current filter from URL let currentFilter = $derived.by(() => { const url = $page.url; + if (url.pathname === '/home') return { type: 'home' }; const onFeeds = url.pathname === FEEDS_PATH; const onSaved = url.pathname === SAVED_PATH; const view = url.searchParams.get('view'); @@ -318,6 +331,7 @@ function isItemActive(item: NavItem): boolean { const filter = currentFilter; if (item.type === 'view') { + if (item.id === 'home' && filter.type === 'home') return true; if (item.id === 'all' && filter.type === 'all') return true; if (item.id === 'saved' && filter.type === 'saved') return true; if (item.id === 'shared' && filter.type === 'shared') return true; @@ -362,7 +376,8 @@ function selectItem(item: NavItem) { let url = FEEDS_PATH; if (item.type === 'view') { - if (item.id === 'saved') url = SAVED_PATH; + if (item.id === 'home') url = '/home'; + else if (item.id === 'saved') url = SAVED_PATH; else if (item.id === 'shared') url = `${FEEDS_PATH}?shared=true`; } else if (item.type === 'feed') { url = feedPath(item.id); From fe93bc6b74cf45942d27f7a65fb6acd0a2fa4143 Mon Sep 17 00:00:00 2001 From: Tim Disney Date: Mon, 6 Jul 2026 10:20:14 -0700 Subject: [PATCH 2/2] fixing up shortcuts --- .../src/lib/components/AddSourceInput.svelte | 15 +++++--- frontend/src/lib/components/AppShell.svelte | 34 ++++++++++++++----- .../components/KeyboardShortcutsModal.svelte | 30 +++++++++------- .../lib/components/NavigationDropdown.svelte | 20 +++++++---- frontend/src/lib/components/Sidebar.svelte | 3 -- .../components/feed/MobileFeedSwitcher.svelte | 4 --- .../hooks/useFeedKeyboardShortcuts.svelte.ts | 10 ------ frontend/src/lib/stores/sidebar.svelte.ts | 16 +++++++++ 8 files changed, 84 insertions(+), 48 deletions(-) diff --git a/frontend/src/lib/components/AddSourceInput.svelte b/frontend/src/lib/components/AddSourceInput.svelte index db48774..5b7e24e 100644 --- a/frontend/src/lib/components/AddSourceInput.svelte +++ b/frontend/src/lib/components/AddSourceInput.svelte @@ -4,7 +4,9 @@ import Icon from '$lib/components/Icon.svelte'; import { sidebarStore } from '$lib/stores/sidebar.svelte'; - let isOpen = $state(false); + // Open state lives in the store so the keyboard shortcut ("a") can toggle + // this menu as well as the trigger button. + let isOpen = $derived(sidebarStore.addMenuOpen); let inputValue = $state(''); let inputRef: HTMLInputElement | null = $state(null); let buttonRef: HTMLButtonElement | null = $state(null); @@ -40,7 +42,12 @@ function toggle(e: MouseEvent) { e.stopPropagation(); - isOpen = !isOpen; + sidebarStore.toggleAddMenu(); + } + + // On open (from a click or the keyboard shortcut), reset the input, position + // the menu, and focus the field. + $effect(() => { if (isOpen) { inputValue = ''; requestAnimationFrame(() => { @@ -48,10 +55,10 @@ inputRef?.focus(); }); } - } + }); function close() { - isOpen = false; + sidebarStore.closeAddMenu(); inputValue = ''; } diff --git a/frontend/src/lib/components/AppShell.svelte b/frontend/src/lib/components/AppShell.svelte index e40ab38..5f17eb8 100644 --- a/frontend/src/lib/components/AppShell.svelte +++ b/frontend/src/lib/components/AppShell.svelte @@ -73,7 +73,7 @@ onMount(() => { // View switching shortcuts keyboardStore.register({ - key: 'h', + key: '0', description: 'Home', category: 'Views', action: () => goto('/home'), @@ -98,22 +98,38 @@ keyboardStore.register({ key: '3', - description: 'Shared', + description: 'Linkblog', category: 'Views', - action: () => goto(`${FEEDS_PATH}?shared=true`), + action: () => goto('/linkblog'), condition: () => auth.isAuthenticated, }); keyboardStore.register({ key: '4', - description: 'Toggle Feeds section', + description: 'Highlights', category: 'Views', - action: () => sidebarStore.toggleSection('feeds'), + action: () => goto('/highlights'), condition: () => auth.isAuthenticated, }); keyboardStore.register({ - key: '0', + key: '5', + description: 'Discover', + category: 'Views', + action: () => goto('/discover'), + condition: () => auth.isAuthenticated, + }); + + keyboardStore.register({ + key: '6', + description: 'Manage Sources', + category: 'Views', + action: () => goto('/sources'), + condition: () => auth.isAuthenticated, + }); + + keyboardStore.register({ + key: '7', description: 'Settings', category: 'Views', action: () => goto('/settings'), @@ -137,12 +153,12 @@ condition: () => auth.isAuthenticated, }); - // Add feed shortcut + // Add menu shortcut (Add feed / @handle / Save URL / …) keyboardStore.register({ key: 'a', - description: 'Add feed', + description: 'Toggle add menu', category: 'Other', - action: () => sidebarStore.openAddFeedModal(), + action: () => sidebarStore.toggleAddMenu(), condition: () => auth.isAuthenticated, }); diff --git a/frontend/src/lib/components/KeyboardShortcutsModal.svelte b/frontend/src/lib/components/KeyboardShortcutsModal.svelte index 881ec6a..51d5175 100644 --- a/frontend/src/lib/components/KeyboardShortcutsModal.svelte +++ b/frontend/src/lib/components/KeyboardShortcutsModal.svelte @@ -9,36 +9,42 @@ { category: 'Navigation', key: 'k', description: 'Previous item' }, { category: 'Navigation', key: 'Enter', description: 'Toggle expand' }, { category: 'Navigation', key: 'o', description: 'Open in new tab' }, + { category: 'Navigation', key: 'f', description: 'Read in full screen' }, // Views - { category: 'Views', key: '1', description: 'All' }, + { category: 'Views', key: '0', description: 'Home' }, + { category: 'Views', key: '1', description: 'Feeds' }, { category: 'Views', key: '2', description: 'Saved' }, - { category: 'Views', key: '3', description: 'Shared' }, - { category: 'Views', key: '4', description: 'Feeds' }, - { category: 'Views', key: '5', description: 'Following' }, - { category: 'Views', key: '6', description: 'Discover' }, - { category: 'Views', key: '0', description: 'Settings' }, + { category: 'Views', key: '3', description: 'Linkblog' }, + { category: 'Views', key: '4', description: 'Highlights' }, + { category: 'Views', key: '5', description: 'Discover' }, + { category: 'Views', key: '6', description: 'Manage Sources' }, + { category: 'Views', key: '7', description: 'Settings' }, - // Feed/User cycling - { category: 'Feed/User', key: '[', description: 'Previous feed/user' }, - { category: 'Feed/User', key: ']', description: 'Next feed/user' }, + // Feed cycling + { category: 'Feed', key: '[', description: 'Previous feed' }, + { category: 'Feed', key: ']', description: 'Next feed' }, // Article actions - { category: 'Article', key: 's', description: 'Save' }, + { category: 'Article', key: 's', description: 'Toggle save' }, { category: 'Article', key: 'S', description: 'Share/unshare' }, { category: 'Article', key: 'm', description: 'Mark read/unread' }, + { category: 'Article', key: 'A', description: 'Mark all as read' }, + { category: 'Article', key: 't', description: 'Tag item' }, + { category: 'Article', key: 'e', description: 'Archive/unarchive saved item' }, + { category: 'Article', key: 'h', description: 'Toggle highlight on paragraph' }, { category: 'Article', key: '+', description: 'Increase font size' }, { category: 'Article', key: '_', description: 'Decrease font size' }, { category: 'Article', key: ')', description: 'Reset font size' }, // Other { category: 'Other', key: 'u', description: 'Toggle unread filter' }, - { category: 'Other', key: 'a', description: 'Add feed' }, + { category: 'Other', key: 'a', description: 'Toggle add menu' }, { category: 'Other', key: '?', description: 'Show shortcuts' }, { category: 'Other', key: 'Esc', description: 'Close modal / Deselect' }, ]; - const categories = ['Navigation', 'Views', 'Feed/User', 'Article', 'Other']; + const categories = ['Navigation', 'Views', 'Feed', 'Article', 'Other']; { const query = searchQuery.toLowerCase().trim(); + const homeItem: NavItem = { + type: 'utility', + id: 'home', + label: 'Home', + icon: 'home', + }; const everythingItem: NavItem = { type: 'view', id: 'all', @@ -299,6 +306,12 @@ const sections: SectionData[] = []; + // Home: the default landing surface (a route, not a feed filter) + const homeGroup = [homeItem].filter(filterItem); + if (homeGroup.length > 0) { + sections.push({ section: '', items: homeGroup }); + } + // Everything group: Everything + nested source channels const everythingGroup = [everythingItem, ...sourceChannelItems].filter(filterItem); if (everythingGroup.length > 0) { @@ -357,6 +370,7 @@ const pathname = $page.url.pathname; // Utility pages (separate routes) + if (pathname === '/home') return { type: 'icon', name: 'home' }; if (pathname === '/linkblog') return { type: 'icon', name: 'share' }; if (pathname === '/highlights') return { type: 'icon', name: 'highlighter' }; if (pathname === '/discover') return { type: 'icon', name: 'users' }; @@ -367,12 +381,10 @@ const url = $page.url; const feed = url.searchParams.get('feed'); const saved = pathname === '/saved' || url.searchParams.get('saved'); - const shared = url.searchParams.get('shared'); const view = url.searchParams.get('view'); if (view) return { type: 'icon', name: 'filter' }; if (saved) return { type: 'icon', name: 'bookmark' }; - if (shared) return { type: 'icon', name: 'share' }; if (feed) { const sub = subscriptions.find((s) => s.id === parseInt(feed)); @@ -402,9 +414,7 @@ if (onSaved) return { type: 'saved' }; if (!onFeeds) return { type: 'none' }; const feed = url.searchParams.get('feed'); - const shared = url.searchParams.get('shared'); if (feed) return { type: 'feed', id: parseInt(feed) }; - if (shared) return { type: 'shared' }; return { type: 'all' }; }); @@ -413,7 +423,6 @@ if (item.type === 'view') { if (item.id === 'all' && filter.type === 'all') return true; if (item.id === 'saved' && filter.type === 'saved') return true; - if (item.id === 'shared' && filter.type === 'shared') return true; } if (item.type === 'feed' && filter.type === 'feed' && filter.id === item.id) return true; if (item.type === 'filteredView' && filter.type === 'filteredView' && filter.id === item.id) @@ -482,7 +491,6 @@ let url = FEEDS_PATH; if (item.type === 'view') { if (item.id === 'saved') url = SAVED_PATH; - else if (item.id === 'shared') url = `${FEEDS_PATH}?shared=true`; } else if (item.type === 'feed') { url = feedPath(item.id); } else if (item.type === 'filteredView') { diff --git a/frontend/src/lib/components/Sidebar.svelte b/frontend/src/lib/components/Sidebar.svelte index 2287974..778ddee 100644 --- a/frontend/src/lib/components/Sidebar.svelte +++ b/frontend/src/lib/components/Sidebar.svelte @@ -112,11 +112,9 @@ if (path === FEEDS_PATH) { const feed = sp.get('feed'); const category = sp.get('category'); - const shared = sp.get('shared'); if (view) return { type: 'view' as const, id: view }; if (feed) return { type: 'feed' as const, id: parseInt(feed) }; if (category) return { type: 'category' as const, name: category }; - if (shared) return { type: 'shared' as const }; return { type: 'all' as const }; } if (path === SAVED_PATH) { @@ -225,7 +223,6 @@ else if (type === 'feed' && id != null) url = feedPath(id); else if (type === 'category' && id != null) url = categoryPath(String(id)); else if (type === 'saved') url = SAVED_PATH; - else if (type === 'shared') url = `${FEEDS_PATH}?shared=true`; goto(url); diff --git a/frontend/src/lib/components/feed/MobileFeedSwitcher.svelte b/frontend/src/lib/components/feed/MobileFeedSwitcher.svelte index 672f61b..84abbe3 100644 --- a/frontend/src/lib/components/feed/MobileFeedSwitcher.svelte +++ b/frontend/src/lib/components/feed/MobileFeedSwitcher.svelte @@ -320,11 +320,9 @@ if (onSaved) return { type: 'saved' }; if (!onFeeds) return { type: 'none' }; const feed = url.searchParams.get('feed'); - const shared = url.searchParams.get('shared'); const category = url.searchParams.get('category'); if (feed) return { type: 'feed', id: parseInt(feed) }; if (category) return { type: 'category', name: category }; - if (shared) return { type: 'shared' }; return { type: 'all' }; }); @@ -334,7 +332,6 @@ if (item.id === 'home' && filter.type === 'home') return true; if (item.id === 'all' && filter.type === 'all') return true; if (item.id === 'saved' && filter.type === 'saved') return true; - if (item.id === 'shared' && filter.type === 'shared') return true; } if (item.type === 'feed' && filter.type === 'feed' && filter.id === item.id) return true; if (item.type === 'category' && filter.type === 'category' && filter.name === item.id) @@ -378,7 +375,6 @@ if (item.type === 'view') { if (item.id === 'home') url = '/home'; else if (item.id === 'saved') url = SAVED_PATH; - else if (item.id === 'shared') url = `${FEEDS_PATH}?shared=true`; } else if (item.type === 'feed') { url = feedPath(item.id); } else if (item.type === 'category') { diff --git a/frontend/src/lib/hooks/useFeedKeyboardShortcuts.svelte.ts b/frontend/src/lib/hooks/useFeedKeyboardShortcuts.svelte.ts index 5fd04a6..7287dfe 100644 --- a/frontend/src/lib/hooks/useFeedKeyboardShortcuts.svelte.ts +++ b/frontend/src/lib/hooks/useFeedKeyboardShortcuts.svelte.ts @@ -6,7 +6,6 @@ import { feedViewStore, type FeedDisplayItem } from '$lib/stores/feedView.svelte import { subscriptionsStore } from '$lib/stores/subscriptions.svelte'; import { itemLabelsStore } from '$lib/stores/itemLabels.svelte'; import { linkblogStore } from '$lib/stores/linkblog.svelte'; -import { sidebarStore } from '$lib/stores/sidebar.svelte'; import type { Article, Subscription } from '$lib/types'; interface KeyboardShortcutsParams { @@ -312,15 +311,6 @@ export function useFeedKeyboardShortcuts(params: KeyboardShortcutsParams) { condition: () => auth.isAuthenticated && !!feedViewStore.feedFilter, }); - // Save article from URL (works from any view) - keyboardStore.register({ - key: 'a', - description: 'Save article by URL', - category: 'Article', - action: () => sidebarStore.openSaveArticleModal(), - condition: () => auth.isAuthenticated, - }); - // Bookmarks-specific: archive item (works for all item types) keyboardStore.register({ key: 'e', diff --git a/frontend/src/lib/stores/sidebar.svelte.ts b/frontend/src/lib/stores/sidebar.svelte.ts index 29472bd..3133346 100644 --- a/frontend/src/lib/stores/sidebar.svelte.ts +++ b/frontend/src/lib/stores/sidebar.svelte.ts @@ -6,6 +6,7 @@ interface SidebarState { addHandleModalOpen: boolean; // For add @handle modal saveArticleModalOpen: boolean; // For save article by URL modal navigationDropdownOpen: boolean; // For navigation dropdown + addMenuOpen: boolean; // For the "+" add menu (Add feed / @handle / Save URL / …) expandedSections: { shared: boolean; feeds: boolean; @@ -29,6 +30,7 @@ function createSidebarStore() { addHandleModalOpen: false, saveArticleModalOpen: false, navigationDropdownOpen: false, + addMenuOpen: false, expandedSections: { shared: false, feeds: false, @@ -178,6 +180,15 @@ function createSidebarStore() { state.navigationDropdownOpen = false; } + // The sidebar "+" add source menu (AddSourceInput). + function toggleAddMenu() { + state.addMenuOpen = !state.addMenuOpen; + } + + function closeAddMenu() { + state.addMenuOpen = false; + } + function setSortedFeedIds(ids: number[]) { state.sortedFeedIds = ids; } @@ -204,6 +215,9 @@ function createSidebarStore() { get navigationDropdownOpen() { return state.navigationDropdownOpen; }, + get addMenuOpen() { + return state.addMenuOpen; + }, get expandedSections() { return state.expandedSections; }, @@ -241,6 +255,8 @@ function createSidebarStore() { closeSaveArticleModal, toggleNavigationDropdown, closeNavigationDropdown, + toggleAddMenu, + closeAddMenu, setSortedFeedIds, openChannelModal, closeChannelModal,