| ... | ... | @@ -6,6 +6,7 @@ import { |
| 6 | 6 | type LayoutChangeEvent, |
| 7 | 7 | Pressable, |
| 8 | 8 | Text as SystemText, |
| 9 | useWindowDimensions, |
| 9 | 10 | View, |
| 10 | 11 | } from "react-native"; |
| 11 | 12 | import Animated, { |
| ... | ... | @@ -65,6 +66,9 @@ export function FloatingWorkingControl(props: { |
| 65 | 66 | readonly showScrollToEnd: boolean; |
| 66 | 67 | readonly onScrollToEnd: () => void; |
| 67 | 68 | }) { |
| 69 | const { width: windowWidth } = useWindowDimensions(); |
| 70 | const [overlayWidth, setOverlayWidth] = useState(windowWidth); |
| 71 | const labelWidth = Math.max(0, Math.min(overlayWidth, windowWidth) - CONTROL_HEIGHT - 16); |
| 68 | 72 | const separationProgress = useSharedValue(props.showScrollToEnd ? 1 : 0); |
| 69 | 73 | |
| 70 | 74 | useEffect(() => { |
| ... | ... | @@ -78,17 +82,10 @@ export function FloatingWorkingControl(props: { |
| 78 | 82 | opacity: separationProgress.value, |
| 79 | 83 | })); |
| 80 | 84 | |
| 81 | | // The label swaps between connection, syncing, compacting, and working while |
| 82 | | // the capsule stays mounted. A layout transition on the capsule would move |
| 83 | | // its left edge, and labels laid out from that edge slide with it, so the pill |
| 84 | | // reads as shifting sideways. Instead an in-flow sizer animates to the |
| 85 | | // measured label width and the capsule takes its size from that, while the |
| 86 | | // labels sit centered on top. The row re-centers as the capsule grows, so its |
| 87 | | // midpoint never moves and the text underneath stays put. |
| 88 | | // |
| 89 | | // The sizer has to carry the width rather than the capsule itself: the native |
| 90 | | // glass view only picks up a size from a real layout pass, so an animated |
| 91 | | // width set straight on it leaves the glass stuck at its mounted size. |
| 85 | // Animate an in-flow sizer so native glass receives real layout updates. |
| 86 | // Measure labels in a separate, fixed-width host: measuring against the |
| 87 | // animated capsule constrains the incoming text to each intermediate width |
| 88 | // and repeatedly retargets the animation as it grows. |
| 92 | 89 | const capsuleWidth = useSharedValue<number | null>(null); |
| 93 | 90 | const measuredWidthRef = useRef<number | null>(null); |
| 94 | 91 | const handleLabelLayout = (event: LayoutChangeEvent) => { |
| ... | ... | @@ -123,15 +120,27 @@ export function FloatingWorkingControl(props: { |
| 123 | 120 | // Only the connection label is a button (tap to reconnect); the others |
| 124 | 121 | // pass touches through to the feed like before. |
| 125 | 122 | const statusInteractive = props.status?.kind === "connection"; |
| 126 | | // Yoga centers an absolute child that has no insets on its parent's align and |
| 127 | | // justify, so each label row lands centered on the capsule without measuring |
| 128 | | // itself, and the capsule clips whatever a wider label overhangs while it |
| 129 | | // catches up. |
| 123 | // The host stays centered on the capsule, but its measurement constraint |
| 124 | // comes from the overlay, independent of the capsule's current width. |
| 130 | 125 | const statusContent = |
| 131 | 126 | props.status !== null ? ( |
| 132 | 127 | <> |
| 133 | 128 | <Animated.View className="h-11" style={capsuleSizerStyle} /> |
| 134 | | <FloatingStatusLabel status={props.status} onLayout={handleLabelLayout} /> |
| 129 | <View |
| 130 | pointerEvents="box-none" |
| 131 | className="absolute h-11 items-center justify-center" |
| 132 | style={{ width: labelWidth }} |
| 133 | > |
| 134 | <FloatingStatusLabel |
| 135 | key={ |
| 136 | props.status.kind === "working" || props.status.kind === "compacting" |
| 137 | ? props.status.kind |
| 138 | : `${props.status.kind}:${props.status.label}` |
| 139 | } |
| 140 | status={props.status} |
| 141 | onLayout={handleLabelLayout} |
| 142 | /> |
| 143 | </View> |
| 135 | 144 | </> |
| 136 | 145 | ) : null; |
| 137 | 146 | |
| ... | ... | @@ -140,6 +149,7 @@ export function FloatingWorkingControl(props: { |
| 140 | 149 | pointerEvents="box-none" |
| 141 | 150 | className="absolute left-0 right-0 z-20 items-center" |
| 142 | 151 | style={{ top: -CONTROL_OVERLAY_OFFSET }} |
| 152 | onLayout={(event) => setOverlayWidth(event.nativeEvent.layout.width)} |
| 143 | 153 | entering={NATIVE_LIQUID_GLASS_SUPPORTED ? undefined : CONTROL_ENTERING} |
| 144 | 154 | exiting={NATIVE_LIQUID_GLASS_SUPPORTED ? undefined : CONTROL_EXITING} |
| 145 | 155 | > |
| ... | ... | @@ -252,7 +262,9 @@ function FloatingStatusLabel(props: { |
| 252 | 262 | onLayout={props.onLayout} |
| 253 | 263 | > |
| 254 | 264 | <ActivityIndicator size="small" colorClassName="accent-icon-muted" /> |
| 255 | | <Text className="font-t3-medium text-xs text-foreground">{props.status.label}</Text> |
| 265 | <Text className="shrink font-t3-medium text-xs text-foreground" numberOfLines={1}> |
| 266 | {props.status.label} |
| 267 | </Text> |
| 256 | 268 | </StatusLabelRow> |
| 257 | 269 | ); |
| 258 | 270 | } |
| ... | ... | @@ -274,7 +286,10 @@ function FloatingStatusLabel(props: { |
| 274 | 286 | ) : ( |
| 275 | 287 | <View className="h-2 w-2 rounded-full bg-red-500" /> |
| 276 | 288 | )} |
| 277 | | <Text className="max-w-[260px] font-t3-medium text-xs text-foreground" numberOfLines={1}> |
| 289 | <Text |
| 290 | className="max-w-[260px] shrink font-t3-medium text-xs text-foreground" |
| 291 | numberOfLines={1} |
| 292 | > |
| 278 | 293 | {props.status.label} |
| 279 | 294 | </Text> |
| 280 | 295 | </StatusLabelRow> |
| ... | ... | @@ -311,8 +326,7 @@ function FloatingStatusLabel(props: { |
| 311 | 326 | ); |
| 312 | 327 | } |
| 313 | 328 | |
| 314 | | // Rows are absolute with no insets, so the capsule centers them on itself and an |
| 315 | | // exiting row fading out never shifts the incoming one. |
| 329 | // Absolute rows cross-fade around the same center without affecting each other. |
| 316 | 330 | function StatusLabelRow(props: { |
| 317 | 331 | readonly accessibilityLabel: string; |
| 318 | 332 | readonly accessibilityRole?: "button"; |
| ... | ... | @@ -324,7 +338,7 @@ function StatusLabelRow(props: { |
| 324 | 338 | const rowClassName = `h-11 flex-row items-center px-4 ${props.className ?? ""}`; |
| 325 | 339 | return ( |
| 326 | 340 | <Animated.View |
| 327 | | className="absolute" |
| 341 | className="absolute max-w-full" |
| 328 | 342 | entering={LABEL_ENTERING} |
| 329 | 343 | exiting={LABEL_EXITING} |
| 330 | 344 | onLayout={props.onLayout} |