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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoggCVt9XgpKbjrTZjoJD9
This commit is contained in:
Claude
2026-09-07 22:09:59 +00:00
parent 5453044368
commit 6b69667567
5 changed files with 30 additions and 22 deletions
@@ -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,
+9 -6
View File
@@ -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<ActionsContext | null>(null)
actionsContext.displayName = 'ComposePromptActionsContext'
let nextId = 0
const LABEL_SWITCH_MARGIN = 0.1
export function Provider({children}: {children: React.ReactNode}) {
const [entries, setEntries] = useState<Entry[]>([])
const [activeId, setActiveId] = useState<number | null>(null)
const activeIdOnUI = useSharedValue<number | null>(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<number | null>(null)
const enabled = config !== null
// the bar's top border would cut across the pill's gradient
useHideBottomBarBorderForScreen({enabled})
const getConfig = useEffectEvent(() => config)
useEffect(() => {
+7 -11
View File
@@ -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<number>) {
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],
),
+4 -1
View File
@@ -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(() => {
+4 -1
View File
@@ -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