From 156f2d25524a7823e443b84aa4a783bb6ba29eb3 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Wed, 19 Aug 2026 09:39:47 +0300 Subject: [PATCH 1/2] fix(layout-v2): stop the panel content reflowing while it opens The context panel animates its own width from 0 to 240px, and its children stretch to that animating width, so everything inside is being laid out again on every frame of the transition. List panels survive it because their rows are left-aligned and simply get clipped, but the streak panel is built from a 10-column calendar grid and justify-between hero rows: the dot columns redistribute and the rows slide apart for the length of the animation. Pinning the content to the panel's open width leaves the layout settled and lets overflow-hidden reveal it, which is what the other panels already looked like they were doing. Settings keeps stretching, since that panel legitimately fills the whole sidebar. Co-Authored-By: Claude Opus 5 --- .../components/sidebar/SidebarDesktopV2.tsx | 103 ++++++++++-------- 1 file changed, 56 insertions(+), 47 deletions(-) diff --git a/packages/shared/src/components/sidebar/SidebarDesktopV2.tsx b/packages/shared/src/components/sidebar/SidebarDesktopV2.tsx index eaa5518046..42a7e14e87 100644 --- a/packages/shared/src/components/sidebar/SidebarDesktopV2.tsx +++ b/packages/shared/src/components/sidebar/SidebarDesktopV2.tsx @@ -2345,60 +2345,69 @@ export const SidebarDesktopV2 = ({ suppressTransition, )} > - {/* pl-5 lines the panel title up with the list rows' icon glyphs - (icons sit ~8px into their w-9 column) and the section titles. */} -
- {isSettingsSelected ? ( - - ) : ( -
- - {utilityPanelTitle} - -
+ {/* Pinned to the open width so the content does not reflow while + the panel animates its own width. */} +
- - {isLoggedIn && !isUtilityPanelSelected && additionalButtons && ( -
- {additionalButtons} + > + {/* pl-5 lines the panel title up with the list rows' icon glyphs + (icons sit ~8px into their w-9 column) and the section titles. */} +
+ {isSettingsSelected ? ( + + ) : ( +
+ + {utilityPanelTitle} + +
+ )}
- )} - + {additionalButtons} +
)} - > - - - - {!isUtilityPanelSelected && } - {showFeedbackWidget && !isUtilityPanelSelected && ( -
- -
- )} + + + + {!isUtilityPanelSelected && } + {showFeedbackWidget && !isUtilityPanelSelected && ( +
+ +
+ )} +
From c57c49f512ec38169ce222316b052f6cc46ac156 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Wed, 19 Aug 2026 09:45:14 +0300 Subject: [PATCH 2/2] feat(layout-v2): ship the rail compact by default The rail's label-free density is the one we want accounts to land on, so an unset `sidebarCompact` now reads as compact. The flag stays three-valued rather than being flipped: unset means the user never chose and gets the new default, while an explicit `false` still means they picked Comfortable and keeps their labels. All three readers go through one hook, because they have to agree without looking related. The rail sizes itself from the flag, MainLayout pads the page content to match that width, and the appearance settings page renders the selected density. Two of them disagreeing puts the content over the rail or leaves a gap beside it. Co-Authored-By: Claude Opus 5 --- packages/shared/src/components/MainLayout.tsx | 5 +++-- .../src/components/sidebar/SidebarDesktopV2.tsx | 9 ++++++--- packages/shared/src/hooks/useSettingsBooleanFlag.ts | 9 ++++++--- packages/shared/src/hooks/useSidebarCompact.ts | 12 ++++++++++++ packages/webapp/pages/settings/appearance.tsx | 3 ++- 5 files changed, 29 insertions(+), 9 deletions(-) create mode 100644 packages/shared/src/hooks/useSidebarCompact.ts diff --git a/packages/shared/src/components/MainLayout.tsx b/packages/shared/src/components/MainLayout.tsx index b9eed590b5..cc4c74e4eb 100644 --- a/packages/shared/src/components/MainLayout.tsx +++ b/packages/shared/src/components/MainLayout.tsx @@ -22,6 +22,7 @@ import { SharedFeedPage } from './utilities'; import { isTesting, onboardingUrl } from '../lib/constants'; import { isOnboardingFeedPathname } from '../lib/onboarding'; import { useBanner } from '../hooks/useBanner'; +import { useSidebarCompact } from '../hooks/useSidebarCompact'; import { useGrowthBookContext } from './GrowthBookProvider'; import { ActiveFeedNameContextProvider, @@ -109,9 +110,9 @@ function MainLayoutComponent({ const { growthbook } = useGrowthBookContext(); const { sidebarRendered } = useSidebarRendered(); const { isAvailable: isBannerAvailable } = useBanner(); - const { sidebarExpanded, autoDismissNotifications, loadedSettings, flags } = + const { sidebarExpanded, autoDismissNotifications, loadedSettings } = useContext(SettingsContext); - const isSidebarCompact = !!flags?.sidebarCompact; + const { value: isSidebarCompact } = useSidebarCompact(); const v2CollapsedPadding = isSidebarCompact ? 'tablet:pl-16 laptop:pl-16' : 'tablet:pl-16 laptop:pl-20'; diff --git a/packages/shared/src/components/sidebar/SidebarDesktopV2.tsx b/packages/shared/src/components/sidebar/SidebarDesktopV2.tsx index 42a7e14e87..10f01b3b0e 100644 --- a/packages/shared/src/components/sidebar/SidebarDesktopV2.tsx +++ b/packages/shared/src/components/sidebar/SidebarDesktopV2.tsx @@ -90,7 +90,7 @@ import { TerminalIcon, TrendingIcon, } from '../icons'; -import { useSettingsBooleanFlag } from '../../hooks/useSettingsBooleanFlag'; +import { useSidebarCompact } from '../../hooks/useSidebarCompact'; import { IconSize } from '../Icon'; import { Tooltip } from '../tooltip/Tooltip'; import { RailHoverPanel } from './RailHoverPanel'; @@ -729,7 +729,7 @@ export const SidebarDesktopV2 = ({ if (isExtension) { myFeedPath = `${webappUrl}my-feed`; } - const { value: isCompact } = useSettingsBooleanFlag('sidebarCompact'); + const { value: isCompact } = useSidebarCompact(); // Compact mode reverts to the original icon-only widths (pre-label rail). // Both width sets are known-good; MainLayout mirrors the collapsed/expanded // padding so the content never overlaps the rail. @@ -2363,7 +2363,10 @@ export const SidebarDesktopV2 = ({ size={ButtonSize.Small} // Smaller glyph, flipped to point left (it's a back action). icon={ - + } onClick={onBackToApp} className="-ml-1" diff --git a/packages/shared/src/hooks/useSettingsBooleanFlag.ts b/packages/shared/src/hooks/useSettingsBooleanFlag.ts index e3ab0b60be..547505e575 100644 --- a/packages/shared/src/hooks/useSettingsBooleanFlag.ts +++ b/packages/shared/src/hooks/useSettingsBooleanFlag.ts @@ -17,15 +17,18 @@ interface UseSettingsBooleanFlag { /** * Reads a boolean flag from `SettingsFlags` and exposes setters that persist - * through the shared `useSettingsContext`. Coerces undefined to `false` so - * callers can use the value directly. Only accepts keys whose value type is + * through the shared `useSettingsContext`. An unset flag falls back to + * `defaultValue`, so a flag that ships on by default stays distinguishable from + * one the user explicitly turned off. Only accepts keys whose value type is * `boolean | undefined`. */ export const useSettingsBooleanFlag = ( key: K, + defaultValue = false, ): UseSettingsBooleanFlag => { const { flags, updateFlag } = useSettingsContext(); - const value = Boolean(flags?.[key]); + const stored = flags?.[key]; + const value = stored === undefined ? defaultValue : Boolean(stored); return { value, set: (next) => updateFlag(key, next as SettingsFlags[K]), diff --git a/packages/shared/src/hooks/useSidebarCompact.ts b/packages/shared/src/hooks/useSidebarCompact.ts new file mode 100644 index 0000000000..c07795a3aa --- /dev/null +++ b/packages/shared/src/hooks/useSidebarCompact.ts @@ -0,0 +1,12 @@ +import { useSettingsBooleanFlag } from './useSettingsBooleanFlag'; + +// The v2 rail ships without the labels under its icons, so an account that +// never touched the density setting gets compact. Only an explicit `false` +// (the user picking Comfortable) brings the labels back. +// +// Read it through here rather than the raw flag: the rail sets its own width +// from this and MainLayout pads the content to match, so the two disagreeing +// would leave the content overlapping the rail or short of it. +export const useSidebarCompact = (): ReturnType< + typeof useSettingsBooleanFlag +> => useSettingsBooleanFlag('sidebarCompact', true); diff --git a/packages/webapp/pages/settings/appearance.tsx b/packages/webapp/pages/settings/appearance.tsx index c2a37e05ac..9e3ab546ac 100644 --- a/packages/webapp/pages/settings/appearance.tsx +++ b/packages/webapp/pages/settings/appearance.tsx @@ -7,6 +7,7 @@ import { ThemeSection } from '@dailydotdev/shared/src/components/ProfileMenu/sec import { useSettingsContext } from '@dailydotdev/shared/src/contexts/SettingsContext'; import { useViewSize, ViewSize } from '@dailydotdev/shared/src/hooks'; import { useSettingsBooleanFlag } from '@dailydotdev/shared/src/hooks/useSettingsBooleanFlag'; +import { useSidebarCompact } from '@dailydotdev/shared/src/hooks/useSidebarCompact'; import { useLayoutVariant } from '@dailydotdev/shared/src/hooks/layout/useLayoutVariant'; import { useReaderModalEligibility } from '@dailydotdev/shared/src/components/post/reader/hooks/useReaderModalEligibility'; import { useLegacyPostLayoutOptOut } from '@dailydotdev/shared/src/components/post/reader/hooks/useLegacyPostLayoutOptOut'; @@ -71,7 +72,7 @@ const AccountManageSubscriptionPage = (): ReactElement => { flags?.readerInstallPromptAcknowledged ?? false; const { isV2: isLayoutV2 } = useLayoutVariant(); const { value: isSidebarCompact, toggle: toggleSidebarCompact } = - useSettingsBooleanFlag('sidebarCompact'); + useSidebarCompact(); const onToggleReadInside = () => { if (isReadInsideEnabled) { optOut();