summaryrefslogtreecommitdiff
path: root/apps/web/app/reader
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-07 04:01:02 -0800
committerFuwn <[email protected]>2026-02-07 04:01:02 -0800
commit66061028eacd955d767df3157c9f9ede2eb00182 (patch)
tree409d1e07fe70c211e4b8e5ad849a2f3051b54207 /apps/web/app/reader
parentfix: measure text nodes in sidebar min width calculation (diff)
downloadasa.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.tsx76
-rw-r--r--apps/web/app/reader/_components/sidebar-content.tsx2
-rw-r--r--apps/web/app/reader/_components/sidebar-footer.tsx8
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>