From 6b6966756718b25c0d8ec4a3a40845298e0041b1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 7 Sep 2026 22:09:59 +0000 Subject: [PATCH] Settle the compose pill: border follows the pill, less width change, light tint - Blend the bar's top border away from the pill's own visibility rather than a per-screen border contribution. The latter was faded on a spring when a screen was popped, so the border flashed back mid-pop between two screens that both show the pill. - Give the current label a small head start when picking the most present screen, so the two cannot trade places repeatedly as they cross. - Grow the up-state side inset to 16pt so the pill changes width less on its way down to the safe-area inset. - Add a half-strength tint to the clear glass. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01QoggCVt9XgpKbjrTZjoJD9 --- .../composePrompt/ComposePromptPill.tsx | 9 ++++++--- src/features/composePrompt/context.tsx | 15 +++++++++------ src/lib/hooks/useHideBottomBarBorder.tsx | 18 +++++++----------- src/view/shell/bottom-bar/BottomBar.tsx | 5 ++++- src/view/shell/bottom-bar/BottomBarWeb.tsx | 5 ++++- 5 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/features/composePrompt/ComposePromptPill.tsx b/src/features/composePrompt/ComposePromptPill.tsx index e9766e761e..be39f4fcfd 100644 --- a/src/features/composePrompt/ComposePromptPill.tsx +++ b/src/features/composePrompt/ComposePromptPill.tsx @@ -15,7 +15,7 @@ import {useSession} from '#/state/session' import {useMinimalShellMode} from '#/state/shell/minimal-mode' import {useShellLayout} from '#/state/shell/shell-layout' import {UserAvatar} from '#/view/com/util/UserAvatar' -import {atoms as a, ios, tokens, useTheme} from '#/alf' +import {atoms as a, ios, tokens, useTheme, utils} from '#/alf' import {Button} from '#/components/Button' import {GlassView, IS_GLASS_AVAILABLE} from '#/components/GlassView' import {Camera_Stroke2_Corner0_Rounded as CameraIcon} from '#/components/icons/Camera' @@ -28,9 +28,11 @@ import {useComposePromptMedia} from './useComposePromptMedia' const AVATAR_SIZE = 32 /** - * Horizontal inset of the pill while the bottom bar is showing. + * Horizontal inset of the pill while the bottom bar is showing. Once the bar + * hides, the inset grows to the bottom safe area for equal spacing, so the + * closer this is to that, the less the pill changes width on the way down. */ -const SIDE_MARGIN_UP = tokens.space.sm +const SIDE_MARGIN_UP = tokens.space.lg /** * Gap between the pill and the top of the bottom bar. */ @@ -145,6 +147,7 @@ export function ComposePromptPill() { style: glassVisible ? 'clear' : 'none', animate: true, }} + tintColor={utils.alpha(t.palette.contrast_50, 0.5)} style={[a.rounded_full]} fallbackStyle={[ a.border, diff --git a/src/features/composePrompt/context.tsx b/src/features/composePrompt/context.tsx index 4088ef2c48..a5ab3f1261 100644 --- a/src/features/composePrompt/context.tsx +++ b/src/features/composePrompt/context.tsx @@ -19,7 +19,6 @@ import { import {scheduleOnRN} from 'react-native-worklets' import {SHELL_SPRING_CONFIG} from '#/lib/custom-animations/springs' -import {useHideBottomBarBorderForScreen} from '#/lib/hooks/useHideBottomBarBorder' import { useScreenCoverage, useScreenPresence, @@ -101,10 +100,12 @@ const actionsContext = createContext(null) actionsContext.displayName = 'ComposePromptActionsContext' let nextId = 0 +const LABEL_SWITCH_MARGIN = 0.1 export function Provider({children}: {children: React.ReactNode}) { const [entries, setEntries] = useState([]) const [activeId, setActiveId] = useState(null) + const activeIdOnUI = useSharedValue(null) const coverage = useScreenCoverage() const visibility = useDerivedValue(() => { @@ -118,13 +119,17 @@ export function Provider({children}: {children: React.ReactNode}) { /* * The pill shows the label of whichever screen is the most present, so * that during a push or swipe-back the text switches at the midpoint of - * the transition rather than when the outgoing screen unmounts. + * the transition rather than when the outgoing screen unmounts. The current + * label gets a small head start so the two cannot trade places repeatedly + * as they cross. */ const mostPresentId = useDerivedValue(() => { let best: number | null = null let bestPresence = 0 for (const entry of entries) { - const presence = entryPresence(entry, coverage.get()) + const presence = + entryPresence(entry, coverage.get()) + + (entry.id === activeIdOnUI.get() ? LABEL_SWITCH_MARGIN : 0) if (presence > bestPresence) { best = entry.id bestPresence = presence @@ -138,6 +143,7 @@ export function Provider({children}: {children: React.ReactNode}) { (current, previous) => { // keep the last label while everything fades out if (current !== previous && current !== null) { + activeIdOnUI.set(current) scheduleOnRN(setActiveId, current) } }, @@ -223,9 +229,6 @@ export function useComposePromptForScreen(config: ComposePromptConfig | null) { const idRef = useRef(null) const enabled = config !== null - // the bar's top border would cut across the pill's gradient - useHideBottomBarBorderForScreen({enabled}) - const getConfig = useEffectEvent(() => config) useEffect(() => { diff --git a/src/lib/hooks/useHideBottomBarBorder.tsx b/src/lib/hooks/useHideBottomBarBorder.tsx index 6a835f2c9e..78d0ae5c68 100644 --- a/src/lib/hooks/useHideBottomBarBorder.tsx +++ b/src/lib/hooks/useHideBottomBarBorder.tsx @@ -36,7 +36,7 @@ function useHideBottomBarBorderSetter() { * Hides the bottom bar's top border while the surrounding screen is present, * fading it with the screen transition. */ -export function useHideBottomBarBorderForScreen({enabled} = {enabled: true}) { +export function useHideBottomBarBorderForScreen() { const register = useHideBottomBarBorderSetter() const {presence} = useScreenPresence() const contribution = useSharedValue(0) @@ -44,18 +44,13 @@ export function useHideBottomBarBorderForScreen({enabled} = {enabled: true}) { useAnimatedReaction( () => presence.get(), (current, previous) => { - if (enabled && current !== previous) { + if (current !== previous) { contribution.set(current) } }, - [enabled], ) - useEffect(() => { - if (!enabled) return - contribution.set(presence.get()) - return register(contribution) - }, [enabled, register, contribution, presence]) + useEffect(() => register(contribution), [register, contribution]) } /** @@ -73,16 +68,17 @@ export function useHideBottomBarBorder() { /** * Animated border color for the bottom bar, blending the border into the - * background as screens that hide it come and go. + * background as screens that hide it come and go. `alsoHidden` is a further + * 0..1 hidden amount to combine in, for shell UI that sits on the bar. */ -export function useBottomBarBorderStyle() { +export function useBottomBarBorderStyle(alsoHidden?: SharedValue) { const t = useTheme() const hideBorder = useHideBottomBarBorder() const visibleColor = t.atoms.border_contrast_low.borderColor const hiddenColor = t.atoms.bg.backgroundColor return useAnimatedStyle(() => ({ borderColor: interpolateColor( - hideBorder.get(), + Math.max(hideBorder.get(), alsoHidden?.get() ?? 0), [0, 1], [visibleColor, hiddenColor], ), diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx index 97bd1db55b..7bdcc8705b 100644 --- a/src/view/shell/bottom-bar/BottomBar.tsx +++ b/src/view/shell/bottom-bar/BottomBar.tsx @@ -59,6 +59,7 @@ import {useAnalytics} from '#/analytics' import { ComposePromptGradient, ComposePromptPill, + useComposePromptState, } from '#/features/composePrompt' import {useActorStatus} from '#/features/liveNow' import {useDemoMode} from '#/storage/hooks/demo-mode' @@ -84,7 +85,9 @@ export function BottomBar({navigation}: BottomTabBarProps) { const accountSwitchControl = useDialogControl() const messagesMenuControl = Menu.useMenuControl() const playHaptic = useHaptics() - const borderStyle = useBottomBarBorderStyle() + const {visibility: composePromptVisibility} = useComposePromptState() + // the border would cut across the compose pill's gradient + const borderStyle = useBottomBarBorderStyle(composePromptVisibility) const iconWidth = 28 const showSignIn = useCallback(() => { diff --git a/src/view/shell/bottom-bar/BottomBarWeb.tsx b/src/view/shell/bottom-bar/BottomBarWeb.tsx index 1f34df8a27..4620fef494 100644 --- a/src/view/shell/bottom-bar/BottomBarWeb.tsx +++ b/src/view/shell/bottom-bar/BottomBarWeb.tsx @@ -48,6 +48,7 @@ import {useAnalytics} from '#/analytics' import { ComposePromptGradient, ComposePromptPill, + useComposePromptState, } from '#/features/composePrompt' import {styles} from './BottomBarStyles' @@ -61,7 +62,9 @@ export function BottomBarWeb() { const {requestSwitchToAccount} = useLoggedOutViewControls() const closeAllActiveElements = useCloseAllActiveElements() const {footerHeight} = useShellLayout() - const borderStyle = useBottomBarBorderStyle() + const {visibility: composePromptVisibility} = useComposePromptState() + // the border would cut across the compose pill's gradient + const borderStyle = useBottomBarBorderStyle(composePromptVisibility) const accountSwitchControl = useDialogControl() const {data: profile} = useProfileQuery({did: currentAccount?.did}) const iconWidth = 26