import type { ComponentProps, ReactNode } from "react"; import { useCallback, useMemo, useState } from "react"; import { Pressable, ScrollView, View, type LayoutChangeEvent, type NativeScrollEvent, type NativeSyntheticEvent, type StyleProp, type ViewStyle, } from "react-native"; import { useThemeColor } from "../lib/useThemeColor"; import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider"; import { themeColorWithAlpha } from "../lib/mobileTheme"; import { cn } from "../lib/cn"; import { AppText as Text } from "./AppText"; import { SymbolView } from "./AppSymbol"; const COMPOSER_TOOLBAR_GAP = 8; const COMPOSER_TOOLBAR_FADE_WIDTH = 18; const COMPOSER_TOOLBAR_SCROLL_EPSILON = 4; /** * Quiet inline composer control used inside cards and their context rows. * Unlike ComposerToolbarButton, this does not draw another pill inside the * composer surface, so model and workspace controls read as part of the card. */ export function ComposerInlineControl(props: { readonly accessibilityHint?: string; readonly accessibilityLabel?: string; readonly disabled?: boolean; readonly emphasized?: boolean; readonly icon?: ComponentProps["name"]; readonly iconNode?: ReactNode; readonly label: string; readonly maxWidth?: number; readonly onPress?: () => void; readonly selected?: boolean; readonly static?: boolean; readonly chevronDirection?: "down" | "right"; readonly showChevron?: boolean; }) { const iconColor = useThemeColor( props.emphasized || props.selected ? "--color-icon" : "--color-icon-muted", ); return ( {props.iconNode ? ( {props.iconNode} ) : props.icon ? ( ) : null} {props.label} {props.showChevron === false ? null : ( )} ); } export function ComposerToolbarRow(props: { readonly children: ReactNode; readonly paddingBottom?: number; readonly paddingHorizontal?: number; readonly paddingTop?: number; readonly style?: StyleProp; }) { return ( {props.children} ); } export function ComposerToolbarScroller(props: { readonly children: ReactNode; readonly fadeOpaque: string; readonly fadeTransparent: string; readonly contentPaddingRight?: number; }) { const [metrics, setMetrics] = useState({ contentWidth: 0, offsetX: 0, viewportWidth: 0, }); const scrollEdges = useMemo(() => { const maxOffset = Math.max(0, metrics.contentWidth - metrics.viewportWidth); return { showLeftFade: metrics.offsetX > COMPOSER_TOOLBAR_SCROLL_EPSILON, showRightFade: metrics.offsetX < maxOffset - COMPOSER_TOOLBAR_SCROLL_EPSILON, }; }, [metrics]); const handleLayout = useCallback((event: LayoutChangeEvent) => { const viewportWidth = event.nativeEvent.layout.width; setMetrics((current) => current.viewportWidth === viewportWidth ? current : { ...current, viewportWidth }, ); }, []); const handleContentSizeChange = useCallback((contentWidth: number) => { setMetrics((current) => current.contentWidth === contentWidth ? current : { ...current, contentWidth }, ); }, []); const handleScroll = useCallback((event: NativeSyntheticEvent) => { const offsetX = event.nativeEvent.contentOffset.x; setMetrics((current) => Math.abs(current.offsetX - offsetX) < 1 ? current : { ...current, offsetX }, ); }, []); return ( {props.children} {scrollEdges.showLeftFade ? ( ) : null} {scrollEdges.showRightFade ? ( ) : null} ); } export function ComposerToolbarButton(props: { readonly icon?: ComponentProps["name"]; readonly iconNode?: ReactNode; readonly label?: string; readonly accessibilityLabel?: string; readonly active?: boolean; readonly disabled?: boolean; readonly maxWidth?: number; readonly minWidth?: number; readonly onPress?: () => void; readonly showChevron?: boolean; readonly textTransform?: "none" | "uppercase"; readonly variant?: "default" | "primary" | "danger"; readonly className?: string; readonly style?: StyleProp; }) { const { themeAppearance } = useAppearancePreferences(); const isDarkMode = themeAppearance === "dark"; const iconColor = useThemeColor("--color-icon"); const iconSubtle = useThemeColor("--color-icon-subtle"); const primaryFg = useThemeColor("--color-primary-foreground"); const dangerFg = useThemeColor("--color-danger-foreground"); const variant = props.variant ?? "default"; const isCircle = !props.label && props.showChevron === false; const defaultBorderColor = useThemeColor("--color-border-subtle"); const activeBorderColor = useThemeColor("--color-border"); const filledBorderColor = variant === "danger" ? themeColorWithAlpha(String(dangerFg), 0.14) : props.disabled ? defaultBorderColor : themeColorWithAlpha(String(primaryFg), 0.18); const iconTintColor = variant === "primary" ? props.disabled ? iconSubtle : primaryFg : variant === "danger" ? dangerFg : iconColor; return ( [ { borderColor: variant === "default" ? props.active ? activeBorderColor : defaultBorderColor : filledBorderColor, borderWidth: 1, maxWidth: props.maxWidth, minWidth: props.minWidth, opacity: props.disabled ? 0.55 : pressed ? 0.72 : 1, shadowColor: "#000", shadowOffset: { width: 0, height: isDarkMode ? 3 : 2 }, shadowOpacity: props.disabled ? 0 : isDarkMode ? 0.24 : 0.08, shadowRadius: isDarkMode ? 10 : 8, }, props.style, ]} > {props.iconNode ? ( {props.iconNode} ) : props.icon ? ( ) : null} {props.label ? ( {props.label} ) : null} {props.showChevron === false ? null : ( )} ); }