diff options
| author | Fuwn <[email protected]> | 2026-02-07 04:13:35 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-07 04:13:35 -0800 |
| commit | 346fe93a012218888eb28cc3cbee315d7ed4219d (patch) | |
| tree | 98ca4523132cf8e6a252c8de4f785fb94f26eab1 /apps/web/app/reader | |
| parent | fix: use fixed rem-based sidebar min/default with whitespace-nowrap (diff) | |
| download | asa.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.tsx | 107 |
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( |