summaryrefslogtreecommitdiff
path: root/apps/web/app/reader
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-08 08:50:10 -0800
committerFuwn <[email protected]>2026-02-08 08:50:10 -0800
commit1157a51cd2e527d30d61583d0888494716d1dc88 (patch)
tree42fdfac5f89c89c2616e16e1c6643ae8a6eb2ac2 /apps/web/app/reader
parentfeat: enforce tier-based history retention (14d free, 90d pro/dev) (diff)
downloadasa.news-1157a51cd2e527d30d61583d0888494716d1dc88.tar.xz
asa.news-1157a51cd2e527d30d61583d0888494716d1dc88.zip
feat: add toolbar position setting (top or bottom)
Diffstat (limited to 'apps/web/app/reader')
-rw-r--r--apps/web/app/reader/_components/reader-shell.tsx175
-rw-r--r--apps/web/app/reader/settings/_components/appearance-settings.tsx22
2 files changed, 115 insertions, 82 deletions
diff --git a/apps/web/app/reader/_components/reader-shell.tsx b/apps/web/app/reader/_components/reader-shell.tsx
index 0cad5cd..eb63f63 100644
--- a/apps/web/app/reader/_components/reader-shell.tsx
+++ b/apps/web/app/reader/_components/reader-shell.tsx
@@ -55,6 +55,9 @@ export function ReaderShell({
const toggleShortcutsDialog = useUserInterfaceStore(
(state) => state.toggleShortcutsDialog
)
+ const toolbarPosition = useUserInterfaceStore(
+ (state) => state.toolbarPosition
+ )
const detailLayout = useDefaultLayout({
id: "asa-detail-layout",
@@ -175,108 +178,115 @@ export function ReaderShell({
)
const allAreRead = totalUnreadCount === 0
- return (
- <div className={classNames(
- "flex h-full flex-col",
- fontSize === "small" ? "text-sm" : fontSize === "large" ? "text-lg" : "text-base"
+ const toolbar = (
+ <header className={classNames(
+ "flex items-center justify-between border-border px-4 py-3",
+ toolbarPosition === "top" ? "border-b" : "border-t"
)}>
- <header className="flex items-center justify-between border-b border-border px-4 py-3">
- {isMobile && selectedEntryIdentifier ? (
+ {isMobile && selectedEntryIdentifier ? (
+ <button
+ type="button"
+ onClick={() => setSelectedEntryIdentifier(null)}
+ className="text-text-secondary transition-colors hover:text-text-primary"
+ >
+ &larr; back
+ </button>
+ ) : isRenamingTitle ? (
+ <div className="flex items-center gap-2">
+ <input
+ type="text"
+ value={renameValue}
+ onChange={(event) => setRenameValue(event.target.value)}
+ onKeyDown={(event) => {
+ if (event.key === "Enter") handleSaveRename()
+ if (event.key === "Escape") setIsRenamingTitle(false)
+ }}
+ className="min-w-0 border border-border bg-background-primary px-2 py-1 text-text-primary outline-none focus:border-text-dim"
+ autoFocus
+ />
+ <button
+ type="button"
+ onClick={handleSaveRename}
+ className="text-text-secondary transition-colors hover:text-text-primary"
+ >
+ save
+ </button>
<button
type="button"
- onClick={() => setSelectedEntryIdentifier(null)}
+ onClick={() => setIsRenamingTitle(false)}
className="text-text-secondary transition-colors hover:text-text-primary"
>
- &larr; back
+ cancel
</button>
- ) : isRenamingTitle ? (
- <div className="flex items-center gap-2">
- <input
- type="text"
- value={renameValue}
- onChange={(event) => setRenameValue(event.target.value)}
- onKeyDown={(event) => {
- if (event.key === "Enter") handleSaveRename()
- if (event.key === "Escape") setIsRenamingTitle(false)
- }}
- className="min-w-0 border border-border bg-background-primary px-2 py-1 text-text-primary outline-none focus:border-text-dim"
- autoFocus
- />
+ </div>
+ ) : (
+ <div className="flex items-center gap-2">
+ <h1 className="text-text-primary">{pageTitle}</h1>
+ {isRenameable && (
<button
type="button"
- onClick={handleSaveRename}
- className="text-text-secondary transition-colors hover:text-text-primary"
+ onClick={handleStartRename}
+ className="text-text-dim transition-colors hover:text-text-secondary"
>
- save
+ rename
</button>
+ )}
+ </div>
+ )}
+ <div className="flex items-center gap-3">
+ {!(isMobile && selectedEntryIdentifier) && (
+ <>
<button
type="button"
- onClick={() => setIsRenamingTitle(false)}
- className="text-text-secondary transition-colors hover:text-text-primary"
+ onClick={() => setSearchOpen(true)}
+ className="text-text-dim transition-colors hover:text-text-secondary"
>
- cancel
+ search
</button>
- </div>
- ) : (
- <div className="flex items-center gap-2">
- <h1 className="text-text-primary">{pageTitle}</h1>
- {isRenameable && (
+ {feedFilter === "all" && (
<button
type="button"
- onClick={handleStartRename}
- className="text-text-dim transition-colors hover:text-text-secondary"
+ onClick={() =>
+ markAllAsRead.mutate({ readState: !allAreRead })
+ }
+ disabled={markAllAsRead.isPending}
+ className="text-text-dim transition-colors hover:text-text-secondary disabled:opacity-50"
>
- rename
+ {allAreRead ? "mark all unread" : "mark all read"}
</button>
)}
- </div>
+ <select
+ value={entryListViewMode}
+ onChange={(event) =>
+ setEntryListViewMode(
+ event.target.value as "compact" | "comfortable" | "expanded"
+ )
+ }
+ className="hidden border border-border bg-background-primary px-2 py-1 text-text-secondary outline-none sm:block"
+ >
+ <option value="compact">compact</option>
+ <option value="comfortable">comfortable</option>
+ <option value="expanded">expanded</option>
+ </select>
+ <button
+ type="button"
+ onClick={() => toggleShortcutsDialog()}
+ className="hidden text-text-dim transition-colors hover:text-text-secondary sm:block"
+ >
+ shortcuts
+ </button>
+ </>
)}
- <div className="flex items-center gap-3">
- {!(isMobile && selectedEntryIdentifier) && (
- <>
- <button
- type="button"
- onClick={() => setSearchOpen(true)}
- className="text-text-dim transition-colors hover:text-text-secondary"
- >
- search
- </button>
- {feedFilter === "all" && (
- <button
- type="button"
- onClick={() =>
- markAllAsRead.mutate({ readState: !allAreRead })
- }
- disabled={markAllAsRead.isPending}
- className="text-text-dim transition-colors hover:text-text-secondary disabled:opacity-50"
- >
- {allAreRead ? "mark all unread" : "mark all read"}
- </button>
- )}
- <select
- value={entryListViewMode}
- onChange={(event) =>
- setEntryListViewMode(
- event.target.value as "compact" | "comfortable" | "expanded"
- )
- }
- className="hidden border border-border bg-background-primary px-2 py-1 text-text-secondary outline-none sm:block"
- >
- <option value="compact">compact</option>
- <option value="comfortable">comfortable</option>
- <option value="expanded">expanded</option>
- </select>
- <button
- type="button"
- onClick={() => toggleShortcutsDialog()}
- className="hidden text-text-dim transition-colors hover:text-text-secondary sm:block"
- >
- shortcuts
- </button>
- </>
- )}
- </div>
- </header>
+ </div>
+ </header>
+ )
+
+ return (
+ <div className={classNames(
+ "flex h-full flex-col",
+ fontSize === "small" ? "text-sm" : fontSize === "large" ? "text-lg" : "text-base"
+ )}>
+ {toolbarPosition === "top" && toolbar}
<ErrorBoundary>
{isMobile ? (
selectedEntryIdentifier ? (
@@ -342,6 +352,7 @@ export function ReaderShell({
</Group>
)}
</ErrorBoundary>
+ {toolbarPosition === "bottom" && toolbar}
</div>
)
}
diff --git a/apps/web/app/reader/settings/_components/appearance-settings.tsx b/apps/web/app/reader/settings/_components/appearance-settings.tsx
index 458d2b6..0f0d793 100644
--- a/apps/web/app/reader/settings/_components/appearance-settings.tsx
+++ b/apps/web/app/reader/settings/_components/appearance-settings.tsx
@@ -61,6 +61,12 @@ export function AppearanceSettings() {
const setShowFoldersAboveFeeds = useUserInterfaceStore(
(state) => state.setShowFoldersAboveFeeds
)
+ const toolbarPosition = useUserInterfaceStore(
+ (state) => state.toolbarPosition
+ )
+ const setToolbarPosition = useUserInterfaceStore(
+ (state) => state.setToolbarPosition
+ )
return (
<div className="px-4 py-3">
<div className="mb-6">
@@ -79,6 +85,22 @@ export function AppearanceSettings() {
</select>
</div>
<div className="mb-6">
+ <h3 className="mb-2 text-text-primary">toolbar position</h3>
+ <p className="mb-3 text-text-dim">
+ place the toolbar at the top or bottom of the reader
+ </p>
+ <select
+ value={toolbarPosition}
+ onChange={(event) =>
+ setToolbarPosition(event.target.value as "top" | "bottom")
+ }
+ className="border border-border bg-background-primary px-3 py-2 text-text-primary outline-none focus:border-text-dim"
+ >
+ <option value="top">top</option>
+ <option value="bottom">bottom</option>
+ </select>
+ </div>
+ <div className="mb-6">
<h3 className="mb-2 text-text-primary">display density</h3>
<p className="mb-3 text-text-dim">
controls the overall text size and spacing