diff options
| author | Fuwn <[email protected]> | 2026-02-07 05:45:41 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-07 05:45:41 -0800 |
| commit | 6368c74432ced80e0ac6ad2c5fe9c2495d1bc6ae (patch) | |
| tree | c8b583a21bd489170b8f664e8c028fbbd9d95d49 /apps/web | |
| parent | fix: resolve 6 pre-ship audit bugs (diff) | |
| download | asa.news-6368c74432ced80e0ac6ad2c5fe9c2495d1bc6ae.tar.xz asa.news-6368c74432ced80e0ac6ad2c5fe9c2495d1bc6ae.zip | |
feat: resolve 7 pre-ship QoL items
- Space/Shift+Space: page down/up in detail panel (80% scroll)
- Content font: sans-serif/serif/monospace selector in appearance
settings, applied to article content in detail panel
- Accessibility: entry-list-item uses button instead of div, folder
toggles have aria-expanded, shortcut keys have aria-labels
- Share notes: replaced window.prompt with proper modal dialog
matching existing UI patterns
- Worker .env.example: template with all 10 environment variables
- Worker poisoned messages: archive unprocessable queue messages
instead of leaving them stuck forever
- Worker pool Submit: check return value, reschedule dropped feeds
30s into the future, log warnings for rejected submissions
Diffstat (limited to 'apps/web')
7 files changed, 140 insertions, 6 deletions
diff --git a/apps/web/app/reader/_components/entry-detail-panel.tsx b/apps/web/app/reader/_components/entry-detail-panel.tsx index 5825b1e..4d15e1f 100644 --- a/apps/web/app/reader/_components/entry-detail-panel.tsx +++ b/apps/web/app/reader/_components/entry-detail-panel.tsx @@ -68,6 +68,14 @@ export function EntryDetailPanel({ const showReadingTime = useUserInterfaceStore( (state) => state.showReadingTime ) + const contentFont = useUserInterfaceStore((state) => state.contentFont) + + const contentFontClass = + contentFont === "serif" + ? "font-serif" + : contentFont === "monospace" + ? "font-mono" + : "font-sans" const proseContainerReference = useRef<HTMLDivElement>(null) const [selectionToolbarState, setSelectionToolbarState] = useState<{ @@ -82,6 +90,9 @@ export function EntryDetailPanel({ containerRect: DOMRect } | null>(null) const [unpositionedHighlights, setUnpositionedHighlights] = useState<Highlight[]>([]) + const [isShareNoteDialogOpen, setIsShareNoteDialogOpen] = useState(false) + const [shareNoteText, setShareNoteText] = useState("") + const shareNoteTextareaReference = useRef<HTMLTextAreaElement>(null) const { data: timelineData } = useTimeline() const currentEntry = timelineData?.pages @@ -270,6 +281,32 @@ export function EntryDetailPanel({ return () => container.removeEventListener("click", handleMarkClick) }, [highlightsData]) + useEffect(() => { + if (isShareNoteDialogOpen) { + setTimeout(() => shareNoteTextareaReference.current?.focus(), 0) + } + }, [isShareNoteDialogOpen]) + + useEffect(() => { + if (!isShareNoteDialogOpen) return + + function handleKeyDown(event: KeyboardEvent) { + if (event.key === "Escape") { + event.preventDefault() + event.stopPropagation() + setIsShareNoteDialogOpen(false) + } + } + + document.addEventListener("keydown", handleKeyDown, true) + return () => document.removeEventListener("keydown", handleKeyDown, true) + }, [isShareNoteDialogOpen]) + + function handleShareConfirm() { + shareMutation.mutate(shareNoteText.trim() || null) + setIsShareNoteDialogOpen(false) + } + function handleCreateHighlight(note: string | null) { const container = proseContainerReference.current if (!container || !selectionToolbarState) return @@ -382,8 +419,8 @@ export function EntryDetailPanel({ <button type="button" onClick={() => { - const note = window.prompt("add a note (optional):") - shareMutation.mutate(note || null) + setShareNoteText("") + setIsShareNoteDialogOpen(true) }} className="shrink-0 whitespace-nowrap border border-border px-2 py-1 text-text-secondary transition-colors hover:bg-background-tertiary hover:text-text-primary" > @@ -461,7 +498,7 @@ export function EntryDetailPanel({ <div className="relative"> <div ref={proseContainerReference} - className="prose-reader text-text-secondary" + className={`prose-reader text-text-secondary ${contentFontClass}`} /> {selectionToolbarState && ( <HighlightSelectionToolbar @@ -484,6 +521,55 @@ export function EntryDetailPanel({ )} </div> </article> + {isShareNoteDialogOpen && ( + <div className="fixed inset-0 z-50 flex items-center justify-center"> + <div + className="fixed inset-0 bg-background-primary/80" + onClick={() => setIsShareNoteDialogOpen(false)} + /> + <div className="relative w-full max-w-md border border-border bg-background-secondary p-6"> + <h2 className="mb-4 text-text-primary">share entry</h2> + <form + onSubmit={(event) => { + event.preventDefault() + handleShareConfirm() + }} + className="space-y-4" + > + <div className="space-y-2"> + <label htmlFor="share-note-textarea" className="text-text-secondary"> + add a note (optional) + </label> + <textarea + ref={shareNoteTextareaReference} + id="share-note-textarea" + value={shareNoteText} + onChange={(event) => setShareNoteText(event.target.value)} + rows={4} + placeholder="Write a note to accompany this shared entry..." + className="w-full resize-y border border-border bg-background-primary px-3 py-2 text-text-primary outline-none placeholder:text-text-dim focus:border-text-dim" + /> + </div> + <div className="flex gap-2"> + <button + type="button" + onClick={() => setIsShareNoteDialogOpen(false)} + className="flex-1 border border-border px-4 py-2 text-text-secondary transition-colors hover:bg-background-tertiary hover:text-text-primary" + > + cancel + </button> + <button + type="submit" + disabled={shareMutation.isPending} + className="flex-1 border border-border bg-background-tertiary px-4 py-2 text-text-primary transition-colors hover:bg-border disabled:opacity-50" + > + {shareMutation.isPending ? "sharing..." : "share"} + </button> + </div> + </form> + </div> + </div> + )} </div> ) } diff --git a/apps/web/app/reader/_components/entry-list-item.tsx b/apps/web/app/reader/_components/entry-list-item.tsx index d192081..1669658 100644 --- a/apps/web/app/reader/_components/entry-list-item.tsx +++ b/apps/web/app/reader/_components/entry-list-item.tsx @@ -45,12 +45,13 @@ export function EntryListItem({ const displayTitle = entry.customTitle ?? entry.feedTitle return ( - <div + <button + type="button" ref={measureReference} data-index={virtualItem.index} onClick={onSelect} className={classNames( - "absolute left-0 top-0 w-full cursor-pointer border-b border-border px-4 transition-colors", + "absolute left-0 top-0 w-full cursor-pointer border-b border-border bg-transparent px-4 text-left font-[inherit] text-[inherit] transition-colors", isSelected ? "bg-background-tertiary" : isFocused @@ -130,6 +131,6 @@ export function EntryListItem({ )} </div> )} - </div> + </button> ) } diff --git a/apps/web/app/reader/_components/keyboard-shortcuts-dialog.tsx b/apps/web/app/reader/_components/keyboard-shortcuts-dialog.tsx index 139249b..f6e95eb 100644 --- a/apps/web/app/reader/_components/keyboard-shortcuts-dialog.tsx +++ b/apps/web/app/reader/_components/keyboard-shortcuts-dialog.tsx @@ -110,6 +110,7 @@ export function KeyboardShortcutsDialog() { {shortcut.keys.map((key) => ( <kbd key={key} + aria-label={`${key}: ${shortcut.description}`} className="border border-border bg-background-secondary px-1.5 py-0.5 font-mono text-[0.75rem] text-text-primary" > {key} diff --git a/apps/web/app/reader/_components/sidebar-content.tsx b/apps/web/app/reader/_components/sidebar-content.tsx index e13926d..17d07bb 100644 --- a/apps/web/app/reader/_components/sidebar-content.tsx +++ b/apps/web/app/reader/_components/sidebar-content.tsx @@ -279,6 +279,7 @@ export function SidebarContent() { > <button type="button" + aria-expanded={isExpanded} onClick={() => toggleFolderExpansion(folder.folderIdentifier) } diff --git a/apps/web/app/reader/settings/_components/appearance-settings.tsx b/apps/web/app/reader/settings/_components/appearance-settings.tsx index 6c04f00..508051f 100644 --- a/apps/web/app/reader/settings/_components/appearance-settings.tsx +++ b/apps/web/app/reader/settings/_components/appearance-settings.tsx @@ -31,6 +31,10 @@ export function AppearanceSettings() { ) const fontSize = useUserInterfaceStore((state) => state.fontSize) const setFontSize = useUserInterfaceStore((state) => state.setFontSize) + const contentFont = useUserInterfaceStore((state) => state.contentFont) + const setContentFont = useUserInterfaceStore( + (state) => state.setContentFont + ) const timeDisplayFormat = useUserInterfaceStore( (state) => state.timeDisplayFormat ) @@ -155,6 +159,25 @@ export function AppearanceSettings() { </select> </div> <div className="mb-6"> + <h3 className="mb-2 text-text-primary">content font</h3> + <p className="mb-3 text-text-dim"> + controls the typeface used for article content + </p> + <select + value={contentFont} + onChange={(event) => + setContentFont( + event.target.value as "sans-serif" | "serif" | "monospace" + ) + } + className="border border-border bg-background-primary px-3 py-2 text-text-primary outline-none focus:border-text-dim" + > + <option value="sans-serif">sans-serif</option> + <option value="serif">serif</option> + <option value="monospace">monospace</option> + </select> + </div> + <div className="mb-6"> <h3 className="mb-2 text-text-primary">time display</h3> <p className="mb-3 text-text-dim"> choose between relative timestamps (e.g. “2h ago”) or diff --git a/apps/web/lib/hooks/use-keyboard-navigation.ts b/apps/web/lib/hooks/use-keyboard-navigation.ts index 24a4761..8685396 100644 --- a/apps/web/lib/hooks/use-keyboard-navigation.ts +++ b/apps/web/lib/hooks/use-keyboard-navigation.ts @@ -368,6 +368,7 @@ export function useKeyboardNavigation() { function handleDetailPanelKeyDown(event: KeyboardEvent) { const SCROLL_AMOUNT = 100 + const PAGE_SCROLL_FRACTION = 0.8 const detailArticle = document.querySelector<HTMLElement>( "[data-detail-article]" ) @@ -385,6 +386,19 @@ export function useKeyboardNavigation() { if (detailArticle) detailArticle.scrollTop -= SCROLL_AMOUNT break } + case " ": { + event.preventDefault() + if (detailArticle) { + const pageScrollAmount = + detailArticle.clientHeight * PAGE_SCROLL_FRACTION + if (event.shiftKey) { + detailArticle.scrollTop -= pageScrollAmount + } else { + detailArticle.scrollTop += pageScrollAmount + } + } + break + } case "?": { event.preventDefault() toggleShortcutsDialog() diff --git a/apps/web/lib/stores/user-interface-store.ts b/apps/web/lib/stores/user-interface-store.ts index 01dceba..fdee347 100644 --- a/apps/web/lib/stores/user-interface-store.ts +++ b/apps/web/lib/stores/user-interface-store.ts @@ -7,6 +7,8 @@ type DisplayDensity = "compact" | "default" | "spacious" type FontSize = "small" | "default" | "large" +type ContentFont = "sans-serif" | "serif" | "monospace" + type TimeDisplayFormat = "relative" | "absolute" type FocusedPanel = "sidebar" | "entryList" | "detailPanel" @@ -38,6 +40,7 @@ interface UserInterfaceState { showFeedFavicons: boolean focusFollowsInteraction: boolean fontSize: FontSize + contentFont: ContentFont timeDisplayFormat: TimeDisplayFormat showEntryImages: boolean showReadingTime: boolean @@ -62,6 +65,7 @@ interface UserInterfaceState { setShowFeedFavicons: (show: boolean) => void setFocusFollowsInteraction: (enabled: boolean) => void setFontSize: (size: FontSize) => void + setContentFont: (font: ContentFont) => void setTimeDisplayFormat: (format: TimeDisplayFormat) => void setShowEntryImages: (show: boolean) => void setShowReadingTime: (show: boolean) => void @@ -90,6 +94,7 @@ export const useUserInterfaceStore = create<UserInterfaceState>()( showFeedFavicons: true, focusFollowsInteraction: false, fontSize: "default", + contentFont: "sans-serif", timeDisplayFormat: "relative", showEntryImages: true, showReadingTime: true, @@ -134,6 +139,8 @@ export const useUserInterfaceStore = create<UserInterfaceState>()( setFontSize: (size) => set({ fontSize: size }), + setContentFont: (font) => set({ contentFont: font }), + setTimeDisplayFormat: (format) => set({ timeDisplayFormat: format }), setShowEntryImages: (show) => set({ showEntryImages: show }), @@ -187,6 +194,7 @@ export const useUserInterfaceStore = create<UserInterfaceState>()( focusFollowsInteraction: state.focusFollowsInteraction, expandedFolderIdentifiers: state.expandedFolderIdentifiers, fontSize: state.fontSize, + contentFont: state.contentFont, timeDisplayFormat: state.timeDisplayFormat, showEntryImages: state.showEntryImages, showReadingTime: state.showReadingTime, |