Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions frontend/src/lib/components/AddSourceInput.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -40,18 +42,23 @@

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(() => {
updateMenuPosition();
inputRef?.focus();
});
}
}
});

function close() {
isOpen = false;
sidebarStore.closeAddMenu();
inputValue = '';
}

Expand Down
34 changes: 25 additions & 9 deletions frontend/src/lib/components/AppShell.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
onMount(() => {
// View switching shortcuts
keyboardStore.register({
key: 'h',
key: '0',
description: 'Home',
category: 'Views',
action: () => goto('/home'),
Expand All @@ -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'),
Expand All @@ -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,
});

Expand Down
30 changes: 18 additions & 12 deletions frontend/src/lib/components/KeyboardShortcutsModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
</script>

<Modal
Expand Down
20 changes: 14 additions & 6 deletions frontend/src/lib/components/NavigationDropdown.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@

// Icon names type (matches Icon.svelte)
type IconName =
| 'home'
| 'inbox'
| 'bookmark'
| 'share'
Expand Down Expand Up @@ -208,6 +209,12 @@
let filteredItems = $derived.by((): SectionData[] => {
const query = searchQuery.toLowerCase().trim();

const homeItem: NavItem = {
type: 'utility',
id: 'home',
label: 'Home',
icon: 'home',
};
const everythingItem: NavItem = {
type: 'view',
id: 'all',
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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' };
Expand All @@ -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));
Expand Down Expand Up @@ -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' };
});

Expand 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)
Expand Down Expand Up @@ -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') {
Expand Down
3 changes: 0 additions & 3 deletions frontend/src/lib/components/Sidebar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);

Expand Down
23 changes: 17 additions & 6 deletions frontend/src/lib/components/feed/MobileFeedSwitcher.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
let savedCount = $derived(itemLabelsStore.savedCount);

type IconName =
| 'home'
| 'inbox'
| 'bookmark'
| 'share'
Expand Down Expand Up @@ -100,7 +101,7 @@

type SectionData = {
section: string;
groupId?: 'everything' | 'saved' | 'other' | 'sources';
groupId?: 'home' | 'everything' | 'saved' | 'other' | 'sources';
items: NavItem[];
};

Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -300,27 +312,26 @@
// 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');
if (view && (onFeeds || onSaved)) return { type: 'filteredView', id: view };
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' };
});

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;
}
if (item.type === 'feed' && filter.type === 'feed' && filter.id === item.id) return true;
if (item.type === 'category' && filter.type === 'category' && filter.name === item.id)
Expand Down Expand Up @@ -362,8 +373,8 @@
function selectItem(item: NavItem) {
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`;
if (item.id === 'home') url = '/home';
else if (item.id === 'saved') url = SAVED_PATH;
} else if (item.type === 'feed') {
url = feedPath(item.id);
} else if (item.type === 'category') {
Expand Down
10 changes: 0 additions & 10 deletions frontend/src/lib/hooks/useFeedKeyboardShortcuts.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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',
Expand Down
Loading
Loading