summaryrefslogtreecommitdiff
path: root/apps/web/app/reader/settings/_components
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-09 21:24:24 -0800
committerFuwn <[email protected]>2026-02-09 21:24:24 -0800
commit047444beb0663d099717c9c392496fc662f79501 (patch)
tree8b878381ec5912c0275c4008bc6af470ab965cc7 /apps/web/app/reader/settings/_components
parentui: reorder sidebar footer, import/export tab, and subscription actions (diff)
downloadasa.news-047444beb0663d099717c9c392496fc662f79501.tar.xz
asa.news-047444beb0663d099717c9c392496fc662f79501.zip
ui: group appearance settings into collapsible sections
Diffstat (limited to 'apps/web/app/reader/settings/_components')
-rw-r--r--apps/web/app/reader/settings/_components/appearance-settings.tsx425
1 files changed, 228 insertions, 197 deletions
diff --git a/apps/web/app/reader/settings/_components/appearance-settings.tsx b/apps/web/app/reader/settings/_components/appearance-settings.tsx
index 0f0d793..babf1c0 100644
--- a/apps/web/app/reader/settings/_components/appearance-settings.tsx
+++ b/apps/web/app/reader/settings/_components/appearance-settings.tsx
@@ -3,6 +3,25 @@
import { useTheme } from "next-themes"
import { useUserInterfaceStore } from "@/lib/stores/user-interface-store"
+function SettingsSection({
+ title,
+ defaultOpen,
+ children,
+}: {
+ title: string
+ defaultOpen?: boolean
+ children: React.ReactNode
+}) {
+ return (
+ <details open={defaultOpen} className="group border-b border-border last:border-b-0">
+ <summary className="cursor-pointer select-none px-4 py-3 text-text-primary transition-colors hover:bg-background-tertiary">
+ {title}
+ </summary>
+ <div className="space-y-6 px-4 pb-4">{children}</div>
+ </details>
+ )
+}
+
export function AppearanceSettings() {
const { theme, setTheme } = useTheme()
const entryListViewMode = useUserInterfaceStore(
@@ -67,206 +86,218 @@ export function AppearanceSettings() {
const setToolbarPosition = useUserInterfaceStore(
(state) => state.setToolbarPosition
)
+
return (
- <div className="px-4 py-3">
- <div className="mb-6">
- <h3 className="mb-2 text-text-primary">theme</h3>
- <p className="mb-3 text-text-dim">
- controls the colour scheme of the application
- </p>
- <select
- value={theme ?? "system"}
- onChange={(event) => setTheme(event.target.value)}
- className="border border-border bg-background-primary px-3 py-2 text-text-primary outline-none focus:border-text-dim"
- >
- <option value="system">system</option>
- <option value="light">light</option>
- <option value="dark">dark</option>
- </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
- </p>
- <select
- value={displayDensity}
- onChange={(event) =>
- setDisplayDensity(
- event.target.value as "compact" | "default" | "spacious"
- )
- }
- className="border border-border bg-background-primary px-3 py-2 text-text-primary outline-none focus:border-text-dim"
- >
- <option value="compact">compact</option>
- <option value="default">default</option>
- <option value="spacious">spacious</option>
- </select>
- </div>
- <div className="mb-6">
- <h3 className="mb-2 text-text-primary">entry list view</h3>
- <p className="mb-3 text-text-dim">
- controls how entries are displayed in the list
- </p>
- <select
- value={entryListViewMode}
- onChange={(event) =>
- setEntryListViewMode(
- event.target.value as "compact" | "comfortable" | "expanded"
- )
- }
- className="border border-border bg-background-primary px-3 py-2 text-text-primary outline-none focus:border-text-dim"
- >
- <option value="compact">compact</option>
- <option value="comfortable">comfortable</option>
- <option value="expanded">expanded</option>
- </select>
- </div>
- <div className="mb-6">
- <h3 className="mb-2 text-text-primary">feed favicons</h3>
- <p className="mb-3 text-text-dim">
- show website icons next to feed names in the sidebar
- </p>
- <label className="flex cursor-pointer items-center gap-2 text-text-primary">
- <input
- type="checkbox"
- checked={showFeedFavicons}
- onChange={(event) => setShowFeedFavicons(event.target.checked)}
- className="accent-text-primary"
- />
- <span>show favicons</span>
- </label>
- </div>
- <div className="mb-6">
- <h3 className="mb-2 text-text-primary">entry favicons</h3>
- <p className="mb-3 text-text-dim">
- show website icons next to feed names in the entry list
- </p>
- <label className="flex cursor-pointer items-center gap-2 text-text-primary">
- <input
- type="checkbox"
- checked={showEntryFavicons}
- onChange={(event) => setShowEntryFavicons(event.target.checked)}
- className="accent-text-primary"
- />
- <span>show entry favicons</span>
- </label>
- </div>
- <div className="mb-6">
- <h3 className="mb-2 text-text-primary">focus follows interaction</h3>
- <p className="mb-3 text-text-dim">
- automatically move keyboard panel focus to the last pane you
- interacted with (clicked or scrolled)
- </p>
- <label className="flex cursor-pointer items-center gap-2 text-text-primary">
- <input
- type="checkbox"
- checked={focusFollowsInteraction}
+ <div className="py-1">
+ <SettingsSection title="general" defaultOpen>
+ <div>
+ <h3 className="mb-2 text-text-primary">theme</h3>
+ <p className="mb-3 text-text-dim">
+ controls the colour scheme of the application
+ </p>
+ <select
+ value={theme ?? "system"}
+ onChange={(event) => setTheme(event.target.value)}
+ className="border border-border bg-background-primary px-3 py-2 text-text-primary outline-none focus:border-text-dim"
+ >
+ <option value="system">system</option>
+ <option value="light">light</option>
+ <option value="dark">dark</option>
+ </select>
+ </div>
+ <div>
+ <h3 className="mb-2 text-text-primary">font size</h3>
+ <p className="mb-3 text-text-dim">
+ controls the base text size in the reader
+ </p>
+ <select
+ value={fontSize}
+ onChange={(event) =>
+ setFontSize(event.target.value as "small" | "default" | "large")
+ }
+ className="border border-border bg-background-primary px-3 py-2 text-text-primary outline-none focus:border-text-dim"
+ >
+ <option value="small">small</option>
+ <option value="default">default</option>
+ <option value="large">large</option>
+ </select>
+ </div>
+ <div>
+ <h3 className="mb-2 text-text-primary">display density</h3>
+ <p className="mb-3 text-text-dim">
+ controls the overall text size and spacing
+ </p>
+ <select
+ value={displayDensity}
+ onChange={(event) =>
+ setDisplayDensity(
+ event.target.value as "compact" | "default" | "spacious"
+ )
+ }
+ className="border border-border bg-background-primary px-3 py-2 text-text-primary outline-none focus:border-text-dim"
+ >
+ <option value="compact">compact</option>
+ <option value="default">default</option>
+ <option value="spacious">spacious</option>
+ </select>
+ </div>
+ <div>
+ <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>
+ </SettingsSection>
+
+ <SettingsSection title="entry list">
+ <div>
+ <h3 className="mb-2 text-text-primary">entry list view</h3>
+ <p className="mb-3 text-text-dim">
+ controls how entries are displayed in the list
+ </p>
+ <select
+ value={entryListViewMode}
onChange={(event) =>
- setFocusFollowsInteraction(event.target.checked)
+ setEntryListViewMode(
+ event.target.value as "compact" | "comfortable" | "expanded"
+ )
}
- className="accent-text-primary"
- />
- <span>enable focus follows interaction</span>
- </label>
- </div>
- <div className="mb-6">
- <h3 className="mb-2 text-text-primary">font size</h3>
- <p className="mb-3 text-text-dim">
- controls the base text size in the reader
- </p>
- <select
- value={fontSize}
- onChange={(event) =>
- setFontSize(event.target.value as "small" | "default" | "large")
- }
- className="border border-border bg-background-primary px-3 py-2 text-text-primary outline-none focus:border-text-dim"
- >
- <option value="small">small</option>
- <option value="default">default</option>
- <option value="large">large</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
- absolute dates
- </p>
- <select
- value={timeDisplayFormat}
- onChange={(event) =>
- setTimeDisplayFormat(
- event.target.value as "relative" | "absolute"
- )
- }
- className="border border-border bg-background-primary px-3 py-2 text-text-primary outline-none focus:border-text-dim"
- >
- <option value="relative">relative</option>
- <option value="absolute">absolute</option>
- </select>
- </div>
- <div className="mb-6">
- <h3 className="mb-2 text-text-primary">entry images</h3>
- <p className="mb-3 text-text-dim">
- show thumbnail images next to entries in the expanded view
- </p>
- <label className="flex cursor-pointer items-center gap-2 text-text-primary">
- <input
- type="checkbox"
- checked={showEntryImages}
- onChange={(event) => setShowEntryImages(event.target.checked)}
- className="accent-text-primary"
- />
- <span>show entry images</span>
- </label>
- </div>
- <div className="mb-6">
- <h3 className="mb-2 text-text-primary">sidebar ordering</h3>
- <p className="mb-3 text-text-dim">
- display folders above ungrouped feeds in the sidebar
- </p>
- <label className="flex cursor-pointer items-center gap-2 text-text-primary">
- <input
- type="checkbox"
- checked={showFoldersAboveFeeds}
- onChange={(event) => setShowFoldersAboveFeeds(event.target.checked)}
- className="accent-text-primary"
- />
- <span>show folders above feeds</span>
- </label>
- </div>
- <div>
- <h3 className="mb-2 text-text-primary">reading time</h3>
- <p className="mb-3 text-text-dim">
- display estimated reading time for articles
- </p>
- <label className="flex cursor-pointer items-center gap-2 text-text-primary">
- <input
- type="checkbox"
- checked={showReadingTime}
- onChange={(event) => setShowReadingTime(event.target.checked)}
- className="accent-text-primary"
- />
- <span>show reading time</span>
- </label>
- </div>
+ className="border border-border bg-background-primary px-3 py-2 text-text-primary outline-none focus:border-text-dim"
+ >
+ <option value="compact">compact</option>
+ <option value="comfortable">comfortable</option>
+ <option value="expanded">expanded</option>
+ </select>
+ </div>
+ <div>
+ <h3 className="mb-2 text-text-primary">entry images</h3>
+ <p className="mb-3 text-text-dim">
+ show thumbnail images next to entries in the expanded view
+ </p>
+ <label className="flex cursor-pointer items-center gap-2 text-text-primary">
+ <input
+ type="checkbox"
+ checked={showEntryImages}
+ onChange={(event) => setShowEntryImages(event.target.checked)}
+ className="accent-text-primary"
+ />
+ <span>show entry images</span>
+ </label>
+ </div>
+ <div>
+ <h3 className="mb-2 text-text-primary">entry favicons</h3>
+ <p className="mb-3 text-text-dim">
+ show website icons next to feed names in the entry list
+ </p>
+ <label className="flex cursor-pointer items-center gap-2 text-text-primary">
+ <input
+ type="checkbox"
+ checked={showEntryFavicons}
+ onChange={(event) => setShowEntryFavicons(event.target.checked)}
+ className="accent-text-primary"
+ />
+ <span>show entry favicons</span>
+ </label>
+ </div>
+ <div>
+ <h3 className="mb-2 text-text-primary">reading time</h3>
+ <p className="mb-3 text-text-dim">
+ display estimated reading time for articles
+ </p>
+ <label className="flex cursor-pointer items-center gap-2 text-text-primary">
+ <input
+ type="checkbox"
+ checked={showReadingTime}
+ onChange={(event) => setShowReadingTime(event.target.checked)}
+ className="accent-text-primary"
+ />
+ <span>show reading time</span>
+ </label>
+ </div>
+ <div>
+ <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
+ absolute dates
+ </p>
+ <select
+ value={timeDisplayFormat}
+ onChange={(event) =>
+ setTimeDisplayFormat(
+ event.target.value as "relative" | "absolute"
+ )
+ }
+ className="border border-border bg-background-primary px-3 py-2 text-text-primary outline-none focus:border-text-dim"
+ >
+ <option value="relative">relative</option>
+ <option value="absolute">absolute</option>
+ </select>
+ </div>
+ </SettingsSection>
+
+ <SettingsSection title="sidebar">
+ <div>
+ <h3 className="mb-2 text-text-primary">feed favicons</h3>
+ <p className="mb-3 text-text-dim">
+ show website icons next to feed names in the sidebar
+ </p>
+ <label className="flex cursor-pointer items-center gap-2 text-text-primary">
+ <input
+ type="checkbox"
+ checked={showFeedFavicons}
+ onChange={(event) => setShowFeedFavicons(event.target.checked)}
+ className="accent-text-primary"
+ />
+ <span>show favicons</span>
+ </label>
+ </div>
+ <div>
+ <h3 className="mb-2 text-text-primary">sidebar ordering</h3>
+ <p className="mb-3 text-text-dim">
+ display folders above ungrouped feeds in the sidebar
+ </p>
+ <label className="flex cursor-pointer items-center gap-2 text-text-primary">
+ <input
+ type="checkbox"
+ checked={showFoldersAboveFeeds}
+ onChange={(event) => setShowFoldersAboveFeeds(event.target.checked)}
+ className="accent-text-primary"
+ />
+ <span>show folders above feeds</span>
+ </label>
+ </div>
+ </SettingsSection>
+
+ <SettingsSection title="behaviour">
+ <div>
+ <h3 className="mb-2 text-text-primary">focus follows interaction</h3>
+ <p className="mb-3 text-text-dim">
+ automatically move keyboard panel focus to the last pane you
+ interacted with (clicked or scrolled)
+ </p>
+ <label className="flex cursor-pointer items-center gap-2 text-text-primary">
+ <input
+ type="checkbox"
+ checked={focusFollowsInteraction}
+ onChange={(event) =>
+ setFocusFollowsInteraction(event.target.checked)
+ }
+ className="accent-text-primary"
+ />
+ <span>enable focus follows interaction</span>
+ </label>
+ </div>
+ </SettingsSection>
</div>
)
}