diff options
| author | Fuwn <[email protected]> | 2026-02-07 04:01:02 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-07 04:01:02 -0800 |
| commit | 66061028eacd955d767df3157c9f9ede2eb00182 (patch) | |
| tree | 409d1e07fe70c211e4b8e5ad849a2f3051b54207 /apps/web/app/reader | |
| parent | fix: measure text nodes in sidebar min width calculation (diff) | |
| download | asa.news-66061028eacd955d767df3157c9f9ede2eb00182.tar.xz asa.news-66061028eacd955d767df3157c9f9ede2eb00182.zip | |
fix: use fixed rem-based sidebar min/default with whitespace-nowrap
The dynamic measurement approach failed because the library caches
Panel constraints at mount and ignores state-driven prop updates.
Now uses fixed rem values (12rem min, 16rem default) which scale
with font size, plus whitespace-nowrap on all sidebar items to
prevent text wrapping at any width.
Diffstat (limited to 'apps/web/app/reader')
| -rw-r--r-- | apps/web/app/reader/_components/reader-layout-shell.tsx | 76 | ||||
| -rw-r--r-- | apps/web/app/reader/_components/sidebar-content.tsx | 2 | ||||
| -rw-r--r-- | apps/web/app/reader/_components/sidebar-footer.tsx | 8 |
3 files changed, 7 insertions, 79 deletions
diff --git a/apps/web/app/reader/_components/reader-layout-shell.tsx b/apps/web/app/reader/_components/reader-layout-shell.tsx index 6114bd9..cd5f52c 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, useCallback, useEffect, useState } from "react" +import { Suspense, useEffect, 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" @@ -48,72 +48,6 @@ export function ReaderLayoutShell({ (state) => state.focusFollowsInteraction ) const isMobile = useIsMobile() - const [sidebarMinimumWidth, setSidebarMinimumWidth] = useState("150px") - const [sidebarDefaultWidth, setSidebarDefaultWidth] = useState("220px") - - const measureSidebarWidths = useCallback(() => { - const sidebarElement = document.querySelector("[data-panel-zone='sidebar']") - if (!sidebarElement) return - - const canvas = document.createElement("canvas") - const canvasContext = canvas.getContext("2d") - if (!canvasContext) return - - function measureElementContentWidth( - element: Element, - context: CanvasRenderingContext2D - ): number { - const parentStyle = getComputedStyle(element) - const parentFont = `${parentStyle.fontWeight} ${parentStyle.fontSize} ${parentStyle.fontFamily}` - - let width = 0 - for (const node of element.childNodes) { - if (node.nodeType === Node.TEXT_NODE) { - const text = node.textContent?.trim() - if (text) { - context.font = parentFont - width += Math.ceil(context.measureText(text).width) - } - } else if (node.nodeType === Node.ELEMENT_NODE) { - const childStyle = getComputedStyle(node as Element) - context.font = `${childStyle.fontWeight} ${childStyle.fontSize} ${childStyle.fontFamily}` - width += Math.ceil( - context.measureText(node.textContent ?? "").width - ) - } - } - return width - } - - let widestRowWidth = 0 - - const allMeasurableItems = sidebarElement.querySelectorAll( - "[data-sidebar-nav-item], [data-sidebar-footer] a, [data-sidebar-footer] button" - ) - for (const item of allMeasurableItems) { - const rowWidth = measureElementContentWidth(item, canvasContext) - widestRowWidth = Math.max(widestRowWidth, rowWidth) - } - - const sidebarStyle = getComputedStyle(sidebarElement) - canvasContext.font = `400 0.625rem ${sidebarStyle.fontFamily}` - const badgeReservedWidth = Math.ceil( - canvasContext.measureText("999+").width - ) - - const itemHorizontalPadding = 16 - const containerHorizontalPadding = 16 - const minimumGap = 8 - const measuredMinimumWidth = - widestRowWidth + - badgeReservedWidth + - itemHorizontalPadding + - containerHorizontalPadding + - minimumGap - - setSidebarMinimumWidth(`${measuredMinimumWidth}px`) - setSidebarDefaultWidth(`${Math.round(measuredMinimumWidth * 1.5)}px`) - }, []) const sidebarLayout = useDefaultLayout({ id: "asa-sidebar-layout", @@ -124,12 +58,6 @@ export function ReaderLayoutShell({ useKeyboardNavigation() useEffect(() => { - if (isSidebarCollapsed || isMobile) return - const timeoutIdentifier = setTimeout(measureSidebarWidths, 100) - return () => clearTimeout(timeoutIdentifier) - }, [isSidebarCollapsed, isMobile, measureSidebarWidths]) - - useEffect(() => { async function checkAssuranceLevel() { const supabaseClient = createSupabaseBrowserClient() const { data } = await supabaseClient.auth.mfa.getAuthenticatorAssuranceLevel() @@ -279,7 +207,7 @@ export function ReaderLayoutShell({ > {!isSidebarCollapsed && ( <> - <Panel id="sidebar" defaultSize={sidebarDefaultWidth} minSize={sidebarMinimumWidth} maxSize="35%"> + <Panel id="sidebar" defaultSize="16rem" minSize="12rem" maxSize="35%"> <aside data-panel-zone="sidebar" className={classNames( diff --git a/apps/web/app/reader/_components/sidebar-content.tsx b/apps/web/app/reader/_components/sidebar-content.tsx index be59390..e13926d 100644 --- a/apps/web/app/reader/_components/sidebar-content.tsx +++ b/apps/web/app/reader/_components/sidebar-content.tsx @@ -9,7 +9,7 @@ import { useUserInterfaceStore } from "@/lib/stores/user-interface-store" import { classNames } from "@/lib/utilities" const NAVIGATION_LINK_CLASS = - "block px-2 py-1 text-text-secondary transition-colors hover:bg-background-tertiary hover:text-text-primary" + "block whitespace-nowrap px-2 py-1 text-text-secondary transition-colors hover:bg-background-tertiary hover:text-text-primary" const ACTIVE_LINK_CLASS = "bg-background-tertiary text-text-primary" diff --git a/apps/web/app/reader/_components/sidebar-footer.tsx b/apps/web/app/reader/_components/sidebar-footer.tsx index a790332..4b1b293 100644 --- a/apps/web/app/reader/_components/sidebar-footer.tsx +++ b/apps/web/app/reader/_components/sidebar-footer.tsx @@ -35,14 +35,14 @@ export function SidebarFooter() { setActiveSettingsTab("account") closeSidebarOnMobile() }} - className="block truncate px-2 py-1 text-text-secondary transition-colors hover:bg-background-tertiary hover:text-text-primary" + className="block whitespace-nowrap truncate px-2 py-1 text-text-secondary transition-colors hover:bg-background-tertiary hover:text-text-primary" > {displayName} </Link> <Link href="/reader/settings" onClick={closeSidebarOnMobile} - className="block px-2 py-1 text-text-secondary transition-colors hover:bg-background-tertiary hover:text-text-primary" + className="block whitespace-nowrap px-2 py-1 text-text-secondary transition-colors hover:bg-background-tertiary hover:text-text-primary" > settings </Link> @@ -50,7 +50,7 @@ export function SidebarFooter() { <button type="button" onClick={() => setIsNotificationPanelOpen(!isNotificationPanelOpen)} - className="w-full px-2 py-1 text-left text-text-secondary transition-colors hover:bg-background-tertiary hover:text-text-primary" + className="w-full whitespace-nowrap px-2 py-1 text-left text-text-secondary transition-colors hover:bg-background-tertiary hover:text-text-primary" > notifications {unviewedNotificationCount > 0 && ( @@ -69,7 +69,7 @@ export function SidebarFooter() { <button type="submit" onClick={closeSidebarOnMobile} - className="w-full px-2 py-1 text-left text-text-secondary transition-colors hover:bg-background-tertiary hover:text-text-primary" + className="w-full whitespace-nowrap px-2 py-1 text-left text-text-secondary transition-colors hover:bg-background-tertiary hover:text-text-primary" > sign out </button> |