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 { 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?: ViewStyle["maxWidth"]; readonly onPress?: () => void; readonly selected?: boolean; readonly static?: boolean; readonly chevronDirection?: "down" | "right"; readonly showChevron?: boolean; }) { 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 align?: "start" | "end"; /** Only for non-Uniwind surfaces such as the native terminal palette. */ readonly fadeOpaque?: string; /** Only for non-Uniwind surfaces such as the native terminal palette. */ readonly fadeTransparent?: string; /** Semantic Uniwind surface behind the toolbar. Defaults to card. */ readonly fadeSurface?: "card" | "sheet"; 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 ComposerActionButton(props: { readonly accessibilityLabel: string; readonly disabled?: boolean; readonly icon: ComponentProps["name"]; readonly onPress: () => void; readonly variant?: "primary" | "danger"; }) { return ( ); } 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 variant = props.variant ?? "default"; const isCircle = !props.label && props.showChevron === false; const iconTintClassName = variant === "primary" ? props.disabled ? "accent-icon-subtle" : "accent-primary-foreground" : variant === "danger" ? "accent-danger-foreground" : "accent-icon"; return ( [ { maxWidth: props.maxWidth, minWidth: props.minWidth, opacity: props.disabled ? 0.55 : pressed ? 0.72 : 1, }, props.style, ]} > {props.iconNode ? ( {props.iconNode} ) : props.icon ? ( ) : null} {props.label ? ( {props.label} ) : null} {props.showChevron === false ? null : ( )} ); }