summaryrefslogtreecommitdiff
path: root/apps/web/app/reader
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-07 04:13:35 -0800
committerFuwn <[email protected]>2026-02-07 04:13:35 -0800
commit346fe93a012218888eb28cc3cbee315d7ed4219d (patch)
tree98ca4523132cf8e6a252c8de4f785fb94f26eab1 /apps/web/app/reader
parentfix: use fixed rem-based sidebar min/default with whitespace-nowrap (diff)
downloadasa.news-346fe93a012218888eb28cc3cbee315d7ed4219d.tar.xz
asa.news-346fe93a012218888eb28cc3cbee315d7ed4219d.zip
feat: dynamically compute sidebar max width from item text widths
Measures all sidebar items (nav links, feeds, folders, custom feeds, footer) using Canvas measureText to determine the narrowest width that avoids truncation, then passes it as the Panel maxSize.
Diffstat (limited to 'apps/web/app/reader')
-rw-r--r--apps/web/app/reader/_components/reader-layout-shell.tsx107
1 files changed, 105 insertions, 2 deletions
diff --git a/apps/web/app/reader/_components/reader-layout-shell.tsx b/apps/web/app/reader/_components/reader-layout-shell.tsx
index cd5f52c..b32d105 100644
--- a/apps/web/app/reader/_components/reader-layout-shell.tsx
+++ b/apps/web/app/reader/_components/reader-layout-shell.tsx
@@ -1,6 +1,6 @@
"use client"
-import { Suspense, useEffect, useState } from "react"
+import { Suspense, useEffect, useMemo, useState } from "react"
import { Group, Panel, Separator, useDefaultLayout } from "react-resizable-panels"
import { useUserInterfaceStore } from "@/lib/stores/user-interface-store"
import { classNames } from "@/lib/utilities"
@@ -14,6 +14,9 @@ import { KeyboardShortcutsDialog } from "./keyboard-shortcuts-dialog"
import { MfaChallenge } from "./mfa-challenge"
import { useKeyboardNavigation } from "@/lib/hooks/use-keyboard-navigation"
import { createSupabaseBrowserClient } from "@/lib/supabase/client"
+import { useSubscriptions } from "@/lib/queries/use-subscriptions"
+import { useCustomFeeds } from "@/lib/queries/use-custom-feeds"
+import { useUserProfile } from "@/lib/queries/use-user-profile"
const DENSITY_FONT_SIZE_MAP: Record<string, string> = {
compact: "0.875rem",
@@ -48,6 +51,106 @@ export function ReaderLayoutShell({
(state) => state.focusFollowsInteraction
)
const isMobile = useIsMobile()
+ const showFeedFavicons = useUserInterfaceStore(
+ (state) => state.showFeedFavicons
+ )
+ const { data: subscriptionsData } = useSubscriptions()
+ const { data: customFeedsData } = useCustomFeeds()
+ const { data: userProfile } = useUserProfile()
+
+ const sidebarMaxWidth = useMemo(() => {
+ if (typeof window === "undefined") return "35%"
+
+ const canvas = document.createElement("canvas")
+ const maybeContext = canvas.getContext("2d")
+ if (!maybeContext) return "35%"
+
+ const canvasContext = maybeContext
+ const computedStyle = window.getComputedStyle(document.body)
+ const fontFamily = computedStyle.fontFamily
+ const baseFontSizePixels = parseFloat(computedStyle.fontSize)
+ const feedFontSizePixels = baseFontSizePixels * 0.85
+
+ function measureText(text: string, fontSize: number): number {
+ canvasContext.font = `${fontSize}px ${fontFamily}`
+ return canvasContext.measureText(text).width
+ }
+
+ let maximumItemWidth = 0
+
+ const faviconSpace = showFeedFavicons ? 24 : 0
+ const NAV_PADDING = 32
+ const FEED_PADDING = 40
+ const FOLDER_FEED_PADDING = 48
+ const FOLDER_PADDING = 46
+ const FOOTER_PADDING = 32
+ const BADGE_SPACE = 40
+ const NOTIFICATION_BADGE_SPACE = 28
+ const SCROLLBAR_WIDTH = 8
+
+ for (const label of ["all entries", "saved", "highlights", "shares"]) {
+ maximumItemWidth = Math.max(
+ maximumItemWidth,
+ NAV_PADDING + measureText(label, baseFontSizePixels) + BADGE_SPACE
+ )
+ }
+ maximumItemWidth = Math.max(
+ maximumItemWidth,
+ NAV_PADDING + measureText("+ add feed", baseFontSizePixels)
+ )
+
+ if (customFeedsData) {
+ for (const customFeed of customFeedsData) {
+ const iconSpace = customFeed.iconUrl ? 24 : 0
+ maximumItemWidth = Math.max(
+ maximumItemWidth,
+ FEED_PADDING + iconSpace + measureText(customFeed.name, feedFontSizePixels)
+ )
+ }
+ }
+
+ const subscriptions = subscriptionsData?.subscriptions ?? []
+ const folders = subscriptionsData?.folders ?? []
+
+ for (const subscription of subscriptions) {
+ const name = subscription.customTitle || subscription.feedTitle || "untitled"
+ const indent = subscription.folderIdentifier ? FOLDER_FEED_PADDING : FEED_PADDING
+ const extras =
+ faviconSpace +
+ (subscription.feedType === "podcast" ? 14 : 0) +
+ (subscription.consecutiveFailures > 0 ? 24 : 0) +
+ BADGE_SPACE
+ maximumItemWidth = Math.max(
+ maximumItemWidth,
+ indent + extras + measureText(name, feedFontSizePixels)
+ )
+ }
+
+ for (const folder of folders) {
+ const iconSpace = folder.iconUrl ? 24 : 0
+ maximumItemWidth = Math.max(
+ maximumItemWidth,
+ FOLDER_PADDING + iconSpace + measureText(folder.name, baseFontSizePixels) + BADGE_SPACE
+ )
+ }
+
+ const displayName = userProfile?.displayName ?? "account"
+ for (const label of [displayName, "settings", "sign out"]) {
+ maximumItemWidth = Math.max(
+ maximumItemWidth,
+ FOOTER_PADDING + measureText(label, baseFontSizePixels)
+ )
+ }
+ maximumItemWidth = Math.max(
+ maximumItemWidth,
+ FOOTER_PADDING + measureText("notifications", baseFontSizePixels) + NOTIFICATION_BADGE_SPACE
+ )
+
+ maximumItemWidth += SCROLLBAR_WIDTH
+ maximumItemWidth = Math.max(maximumItemWidth, 192)
+
+ return `${Math.ceil(maximumItemWidth)}px`
+ }, [subscriptionsData, customFeedsData, userProfile, displayDensity, showFeedFavicons])
const sidebarLayout = useDefaultLayout({
id: "asa-sidebar-layout",
@@ -207,7 +310,7 @@ export function ReaderLayoutShell({
>
{!isSidebarCollapsed && (
<>
- <Panel id="sidebar" defaultSize="16rem" minSize="12rem" maxSize="35%">
+ <Panel id="sidebar" defaultSize="16rem" minSize="12rem" maxSize={sidebarMaxWidth}>
<aside
data-panel-zone="sidebar"
className={classNames(