summaryrefslogtreecommitdiff
path: root/apps/web
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-07 05:45:41 -0800
committerFuwn <[email protected]>2026-02-07 05:45:41 -0800
commit6368c74432ced80e0ac6ad2c5fe9c2495d1bc6ae (patch)
treec8b583a21bd489170b8f664e8c028fbbd9d95d49 /apps/web
parentfix: resolve 6 pre-ship audit bugs (diff)
downloadasa.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')
-rw-r--r--apps/web/app/reader/_components/entry-detail-panel.tsx92
-rw-r--r--apps/web/app/reader/_components/entry-list-item.tsx7
-rw-r--r--apps/web/app/reader/_components/keyboard-shortcuts-dialog.tsx1
-rw-r--r--apps/web/app/reader/_components/sidebar-content.tsx1
-rw-r--r--apps/web/app/reader/settings/_components/appearance-settings.tsx23
-rw-r--r--apps/web/lib/hooks/use-keyboard-navigation.ts14
-rw-r--r--apps/web/lib/stores/user-interface-store.ts8
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. &ldquo;2h ago&rdquo;) 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,