UNPKG

alouette

Version:

A modern, customizable design system built on top of NativeWind v5 with configurable defaults

1 lines 274 kB
{"version":3,"file":"index-node22.cjs","sources":["../src/core/NativeThemeVariablesContext.ts","../src/core/ThemeContext.ts","../src/ui/containers/ScopedTheme.tsx","../src/core/AlouetteProvider.tsx","../src/core/AlouetteDecorator.tsx","../src/ui/primitives/View.tsx","../src/ui/containers/AccentScope.tsx","../src/ui/primitives/Text.tsx","../src/ui/primitives/ScrollView.tsx","../src/ui/primitives/FlatList.tsx","../src/ui/primitives/SectionList.tsx","../src/ui/stacks/stacks.tsx","../src/ui/stacks/Separator.tsx","../src/ui/containers/Box.tsx","../src/ui/containers/Surface.tsx","../src/ui/styled.tsx","../src/ui/story-components/StoryTitle.tsx","../src/ui/story-components/Story.tsx","../src/ui/story-components/StoryContainer.tsx","../src/ui/story-components/StoryDecorator.tsx","../src/ui/story-components/StoryGrid.tsx","../src/ui/containers/StableAccentScope.tsx","../src/ui/containers/PortalAccentScope.tsx","../src/ui/containers/Presence.tsx","../src/animationDurationsMs.ts","../src/core/useScrollEndState.ts","../src/core/useColorToken.ts","../src/expo/ExternalLink.tsx","../src/expo/ExternalLink.shared.ts","../src/ui/primitives/Icon.tsx","../src/ui/feedback/RingCircle.tsx","../src/ui/feedback/useSimulatedProgress.ts","../src/ui/feedback/CircularProgress.tsx","../src/ui/actions/PressableBox.tsx","../src/ui/actions/Button.tsx","../src/ui/actions/IconButton.tsx","../src/ui/containers/Modal.tsx","../src/ui/feedback/Message.tsx","../src/ui/actions/CollapsibleErrorMessage.tsx","../src/ui/actions/usePressAsync.ts","../src/ui/containers/AlertDialog.tsx","../src/ui/actions/ExternalLinkText.tsx","../src/ui/actions/ActionButton.tsx","../src/ui/inputs/InputText.tsx","../src/ui/inputs/TextArea.tsx","../src/ui/inputs/Switch.tsx","../src/core/useControllableValue.ts","../src/ui/inputs/Select.shared.tsx","../src/ui/inputs/Select.tsx","../src/ui/selection/SelectionContext.tsx","../src/ui/inputs/RadioContext.tsx","../src/ui/inputs/RadioGroup.tsx","../src/ui/containers/DefaultAccentScope.tsx","../src/ui/selection/RadioIndicator.tsx","../src/ui/inputs/Radio.tsx","../src/ui/selection/SegmentedBar.tsx","../src/ui/inputs/RadioButtonGroup.tsx","../src/ui/selection/SegmentedItem.tsx","../src/ui/inputs/RadioButton.tsx","../src/ui/inputs/RadioCardGroup.tsx","../src/ui/inputs/RadioCard.tsx","../src/ui/navigation/NavBarContext.tsx","../src/ui/navigation/NavBar.tsx","../src/ui/navigation/NavBarItem.tsx","../src/ui/navigation/TabsContext.tsx","../src/ui/navigation/Tabs.tsx","../src/ui/navigation/Tab.tsx","../src/ui/forms/FormItem.tsx","../src/ui/forms/Form.tsx","../src/ui/forms/FormField.tsx","../src/ui/forms/FormFieldArray.tsx","../src/ui/forms/FormSubmitButton.tsx","../src/ui/forms/SimpleVForm.tsx","../src/ui/data/EditableItem.tsx","../src/ui/forms/FormEditableItem.tsx","../src/ui/data/Badge.tsx","../src/ui/data/Bullet.tsx","../src/ui/feedback/ConnectionState.tsx","../src/ui/feedback/LinearProgress.tsx","../src/ui/actions/PressableListItem.tsx","../src/ui/layout/GradientBackground.tsx","../src/ui/layout/GradientScrollView.tsx","../src/config/Breakpoints.ts","../src/windowSize/useCurrentBreakpointName.ts","../src/windowSize/SwitchBreakpoints.tsx"],"sourcesContent":["import { createContext, useContext } from \"react\";\nimport type { AlouetteTheme } from \"./AlouetteConfig\";\n\n/**\n * Resolved CSS-variable map for every theme — the JS mirror of the palette CSS,\n * consumed by `ScopedTheme` (feeds NativeWind's `VariableContextProvider`). This is the shape of `generateTheme(...).themeVariables`.\n */\nexport type ThemeVariablesMap = Record<\n AlouetteTheme,\n Record<`--${string}`, string>\n>;\n\n/**\n * Holds the active {@link ThemeVariablesMap}. Defaults to the bundled default\n * palette (`themeVariables`) so `ScopedTheme` works with no provider. A\n * BYO-palette app overrides it via `<AlouetteProvider themeVariables={...}>`\n * with its own `generateTheme(...).themeVariables`, keeping JS token reads in\n * sync with its palette CSS.\n */\nexport const NativeThemeVariablesContext = createContext<ThemeVariablesMap>(\n null as unknown as ThemeVariablesMap,\n);\n\nexport function useNativeThemeVariables(): ThemeVariablesMap {\n return useContext(NativeThemeVariablesContext);\n}\n","import { createContext, useContext } from \"react\";\nimport type { AlouetteModeTheme, AlouetteTheme } from \"./AlouetteConfig\";\n\n/**\n * Tracks the currently applied theme name (e.g. \"dark_brand\") so native reads\n * and accent composition (`AccentScope`) know which theme\n * is active. Set by `ScopedTheme` alongside NativeWind's variable context.\n *\n * Defaults to \"light\", matching the light defaults in the global `@theme` block.\n */\nexport const ThemeContext = createContext<AlouetteTheme>(\"light\");\n\nexport function useCurrentTheme(): AlouetteTheme {\n return useContext(ThemeContext);\n}\n\nexport function useCurrentMode(): AlouetteModeTheme {\n return useContext(ThemeContext).startsWith(\"dark\") ? \"dark\" : \"light\";\n}\n","import { VariableContextProvider } from \"nativewind\";\nimport type { ReactNode } from \"react\";\nimport { useContext } from \"react\";\nimport type { AlouetteTheme } from \"../../core/AlouetteConfig\";\nimport { NativeThemeVariablesContext } from \"../../core/NativeThemeVariablesContext\";\nimport { ThemeContext } from \"../../core/ThemeContext\";\n\nexport interface ScopedThemeProps {\n /** Full theme name, e.g. \"light\", \"dark\", \"light_brand\", \"dark_danger\". */\n theme: AlouetteTheme;\n children?: ReactNode;\n}\n\n/**\n * Applies a theme to its subtree by pushing the theme's resolved CSS variables\n * through NativeWind's `VariableContextProvider` (context only, layout-neutral).\n * The web build applies the theme as a className instead — see\n * `ScopedTheme.web.tsx`. It also records the active theme name in `ThemeContext`\n * so `AccentScope` can read it.\n */\nexport function ScopedTheme({ theme, children }: ScopedThemeProps): ReactNode {\n const themeVariables = useContext(NativeThemeVariablesContext);\n return (\n <ThemeContext.Provider value={theme}>\n <VariableContextProvider value={themeVariables[theme]}>\n {children}\n </VariableContextProvider>\n </ThemeContext.Provider>\n );\n}\n","import type { ReactNode } from \"react\";\nimport { useColorScheme } from \"react-native\";\nimport { ScopedTheme } from \"../ui/containers/ScopedTheme\";\nimport type { ThemeVariablesMap } from \"./NativeThemeVariablesContext\";\nimport { NativeThemeVariablesContext } from \"./NativeThemeVariablesContext\";\n\nexport interface AlouetteProviderProps {\n children: ReactNode;\n /**\n * The resolved theme-variable map JS token reads use. Defaults to the bundled\n * default palette. A BYO-palette app passes its own\n * `generateTheme(...).themeVariables` (from `alouette/theme-generator`) here so\n * JS reads match its palette CSS.\n */\n themeVariables: ThemeVariablesMap;\n}\n\nexport function AlouetteProvider({\n children,\n themeVariables,\n}: AlouetteProviderProps): ReactNode {\n // Apply the OS light/dark scheme as the root theme so base tokens resolve\n // correctly app-wide. Subtrees can override via ScopedTheme / AccentScope.\n const colorScheme = useColorScheme();\n return (\n <NativeThemeVariablesContext.Provider value={themeVariables}>\n <ScopedTheme theme={colorScheme === \"dark\" ? \"dark\" : \"light\"}>\n {children}\n </ScopedTheme>\n </NativeThemeVariablesContext.Provider>\n );\n}\n","/* eslint-disable react/destructuring-assignment */\nimport type { Decorator } from \"@storybook/react-vite\";\nimport { ScopedTheme } from \"../ui/containers/ScopedTheme\";\nimport { AlouetteProvider } from \"./AlouetteProvider\";\nimport { SafeAreaProvider } from \"./SafeAreaProvider\";\n\n// eslint-disable-next-line react/function-component-definition -- not a component\nexport const AlouetteDecorator: Decorator = (storyFn, context) => {\n const theme: \"dark\" | \"light\" =\n context.globals.mode === \"dark\" ? \"dark\" : \"light\";\n\n // The `colorFormat` toolbar global (web storybook only) previews the wide-gamut\n // palette. It defaults to sRGB so what is reviewed matches what native renders.\n const themeVariables = context.parameters.alouette?.themeVariables;\n\n if (!themeVariables) {\n throw new Error(\n 'AlouetteDecorator: missing \"themeVariables\" in parameters.alouette',\n );\n }\n\n return (\n <SafeAreaProvider>\n <AlouetteProvider themeVariables={themeVariables}>\n <ScopedTheme theme={theme}>{storyFn(context)}</ScopedTheme>\n </AlouetteProvider>\n </SafeAreaProvider>\n );\n};\n","import { forwardRef } from \"react\";\nimport { View as RNView, type ViewProps as RNViewProps } from \"react-native\";\n\nexport type ViewProps = RNViewProps;\n\nexport const View = forwardRef<RNView, ViewProps>((props, ref) => {\n return <RNView ref={ref} {...props} />;\n});\n","import type { ReactNode } from \"react\";\nimport type { Accent } from \"../../core/AlouetteConfig\";\nimport { useCurrentMode } from \"../../core/ThemeContext\";\nimport { ScopedTheme } from \"./ScopedTheme\";\n\nexport interface AccentScopeProps {\n mode?: \"dark\" | \"light\";\n accent?: Accent;\n children?: ReactNode;\n}\n\nexport function AccentScope({\n mode: forcedMode,\n accent,\n children,\n}: AccentScopeProps): ReactNode {\n const currentMode = useCurrentMode();\n if (!accent) {\n return children;\n }\n // ScopedTheme applies the accent theme's *resolved* variables (base mode + accent),\n // so a single scope works at any depth — no need to pre-apply the base mode.\n const mode = forcedMode ?? currentMode;\n return <ScopedTheme theme={`${mode}_${accent}`}>{children}</ScopedTheme>;\n}\n","import { forwardRef } from \"react\";\nimport { Text as RNText, type TextProps as RNTextProps } from \"react-native\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\nimport type { Accent } from \"../../core/AlouetteConfig\";\nimport { AccentScope } from \"../containers/AccentScope\";\n\nconst twMerge = extendTailwindMerge({\n extend: {\n classGroups: {\n \"font-family\": [\n \"font-body\",\n \"font-body-bold\",\n \"font-body-extrabold\",\n \"font-heading\",\n \"font-heading-bold\",\n \"font-heading-extrabold\",\n \"font-mono\",\n \"font-mono-bold\",\n \"font-mono-extrabold\",\n ],\n },\n },\n});\n\nexport interface TextProps extends RNTextProps {\n accent?: Accent;\n}\n\nexport const Text = forwardRef<RNText, TextProps>(\n ({ className, accent, ...props }, ref) => {\n return (\n <AccentScope accent={accent}>\n <RNText\n ref={ref}\n className={twMerge(\"font-body text-sharp\", className)}\n {...props}\n />\n </AccentScope>\n );\n },\n);\n\nexport type ParagraphProps = TextProps;\n\nexport const Paragraph = forwardRef<RNText, ParagraphProps>(\n ({ className, ...props }, ref) => {\n return (\n <Text\n ref={ref}\n role=\"paragraph\"\n className={`select-auto ${className ?? \"\"}`}\n {...props}\n />\n );\n },\n);\n","import { styled } from \"nativewind\";\nimport type { ComponentType } from \"react\";\nimport {\n ScrollView as RNScrollView,\n type ScrollViewProps as RNScrollViewProps,\n type StyleProp,\n type ViewStyle,\n} from \"react-native\";\n\nexport type ScrollViewProps = RNScrollViewProps;\n\ninterface StyledScrollViewProps {\n style?: StyleProp<ViewStyle>;\n contentContainerStyle?: StyleProp<ViewStyle>;\n}\n\nexport const ScrollView = styled(\n RNScrollView as unknown as ComponentType<StyledScrollViewProps>,\n {\n className: \"style\",\n contentContainerClassName: \"contentContainerStyle\",\n },\n) as ComponentType<ScrollViewProps>;\n","import { styled } from \"nativewind\";\nimport type { ComponentType, ReactNode } from \"react\";\nimport {\n FlatList as RNFlatList,\n type FlatListProps as RNFlatListProps,\n type StyleProp,\n type ViewStyle,\n} from \"react-native\";\n\nexport type FlatListProps<ItemT> = RNFlatListProps<ItemT>;\n\ninterface StyledFlatListProps {\n style?: StyleProp<ViewStyle>;\n contentContainerStyle?: StyleProp<ViewStyle>;\n columnWrapperStyle?: StyleProp<ViewStyle>;\n}\n\nexport const FlatList = styled(\n RNFlatList as unknown as ComponentType<StyledFlatListProps>,\n {\n className: \"style\",\n contentContainerClassName: \"contentContainerStyle\",\n columnWrapperClassName: \"columnWrapperStyle\",\n },\n) as <ItemT>(props: FlatListProps<ItemT>) => ReactNode;\n","import { styled } from \"nativewind\";\nimport type { ComponentType, ReactNode } from \"react\";\nimport {\n SectionList as RNSectionList,\n type SectionListProps as RNSectionListProps,\n type StyleProp,\n type ViewStyle,\n} from \"react-native\";\n\ntype DefaultSectionT = Record<string, unknown>;\n\nexport type SectionListProps<\n ItemT,\n SectionT = DefaultSectionT,\n> = RNSectionListProps<ItemT, SectionT>;\n\ninterface StyledSectionListProps {\n style?: StyleProp<ViewStyle>;\n contentContainerStyle?: StyleProp<ViewStyle>;\n}\n\nexport const SectionList = styled(\n RNSectionList as unknown as ComponentType<StyledSectionListProps>,\n {\n className: \"style\",\n contentContainerClassName: \"contentContainerStyle\",\n },\n) as <ItemT, SectionT = DefaultSectionT>(\n props: SectionListProps<ItemT, SectionT>,\n) => ReactNode;\n","import { forwardRef } from \"react\";\nimport { View as RNView, type ViewProps as RNViewProps } from \"react-native\";\n\nexport type StackProps = RNViewProps;\n\nexport const Stack = forwardRef<RNView, StackProps>(\n ({ className, ...props }, ref) => {\n return (\n <RNView\n ref={ref}\n className={`flex-row flex-wrap ${className ?? \"\"}`}\n {...props}\n />\n );\n },\n);\n\nexport type HStackProps = RNViewProps;\n\nexport const HStack = forwardRef<RNView, HStackProps>(\n ({ className, ...props }, ref) => {\n return (\n <RNView ref={ref} className={`flex-row ${className ?? \"\"}`} {...props} />\n );\n },\n);\n\nexport type VStackProps = RNViewProps;\n\nexport const VStack = forwardRef<RNView, VStackProps>(\n ({ className, ...props }, ref) => {\n return (\n <RNView ref={ref} className={`flex-col ${className ?? \"\"}`} {...props} />\n );\n },\n);\n","import { forwardRef } from \"react\";\nimport { View as RNView, type ViewProps as RNViewProps } from \"react-native\";\nimport { type VariantProps, tv } from \"tailwind-variants\";\n\nconst separatorVariants = tv({\n base: \"border-border-sharp\",\n variants: {\n vertical: {\n true: \"self-stretch border-r w-px\",\n false: \"self-stretch border-b h-px\",\n },\n },\n defaultVariants: {\n vertical: false,\n },\n});\n\ntype SeparatorVariantProps = VariantProps<typeof separatorVariants>;\n\nexport interface SeparatorProps extends RNViewProps, SeparatorVariantProps {}\n\nexport const Separator = forwardRef<RNView, SeparatorProps>(\n ({ className, vertical, ...props }, ref) => {\n return (\n <RNView\n ref={ref}\n className={separatorVariants({ vertical, className })}\n {...props}\n />\n );\n },\n);\n","import type { ReactElement } from \"react\";\nimport { Children, cloneElement, forwardRef } from \"react\";\nimport {\n Pressable,\n type PressableProps,\n View as RNView,\n type ViewProps as RNViewProps,\n} from \"react-native\";\nimport type { VariantProps } from \"tailwind-variants\";\nimport { tv } from \"tailwind-variants\";\nimport type { Accent } from \"../../core/AlouetteConfig\";\nimport { useSafeAreaInsets } from \"../../core/useSafeAreaInsets\";\nimport { AccentScope } from \"./AccentScope\";\n// Allow Box to shrink when used inside HStack/VStack (matches the original\n// BoxFrame default). overflow is intentionally left off so multi-layer\n// box-shadows are not clipped.\nexport const boxBaseClasses = \"shrink\";\n\nexport interface BoxProps extends RNViewProps {\n accent?: Accent;\n}\n\nexport const Box = forwardRef<RNView, BoxProps>(\n ({ className, accent, ...props }, ref) => {\n return (\n <AccentScope accent={accent}>\n <RNView\n ref={ref}\n className={`${boxBaseClasses} ${className ?? \"\"}`}\n {...props}\n />\n </AccentScope>\n );\n },\n);\n\nexport const interactiveBoxVariants = tv({\n base: [\n boxBaseClasses,\n \"cursor-pointer\",\n \"transition-[transform,background-color,border-color] duration-fast ease-in\",\n \"disabled:cursor-not-allowed disabled:opacity-70 aria-disabled:cursor-not-allowed aria-disabled:opacity-70\",\n \"active:scale-[0.975]\",\n ].join(\" \"),\n variants: {\n withFocusVisibleOutline: {\n true: \"focus-visible:outline-2 focus-visible:outline-offset-2\",\n },\n },\n});\n\nexport interface InteractiveBoxProps\n extends VariantProps<typeof interactiveBoxVariants>, PressableProps {}\n\nexport const InteractiveBox = forwardRef<RNView, InteractiveBoxProps>(\n ({ withFocusVisibleOutline, className, ...rest }, ref) => (\n <Pressable\n ref={ref}\n // override default behavior of Pressable which sets pointerEvents to \"none\" on disabled state. However this prevents cursor to display as\n pointerEvents=\"auto\"\n {...rest}\n className={interactiveBoxVariants({ withFocusVisibleOutline, className })}\n />\n ),\n);\n\nexport const InteractiveBoxHitSlop = forwardRef<RNView, InteractiveBoxProps>(\n ({ withFocusVisibleOutline, children, className, ...rest }, ref) => {\n const child = Children.only(children) as ReactElement<RNViewProps>;\n return (\n <Pressable\n ref={ref}\n // override default behavior of Pressable which sets pointerEvents to \"none\" on disabled state. However this prevents cursor to display as\n pointerEvents=\"auto\"\n className={`flex-center ${className ?? \"\"}`}\n {...rest}\n >\n {cloneElement(child, {\n className: interactiveBoxVariants({\n withFocusVisibleOutline,\n className: child.props.className,\n }),\n })}\n </Pressable>\n );\n },\n);\n\nexport type SafeAreaBoxProps = Omit<BoxProps, \"style\">;\n\nexport const SafeAreaBox = forwardRef<RNView, SafeAreaBoxProps>(\n (props, ref) => {\n const insets = useSafeAreaInsets();\n return (\n <Box\n ref={ref}\n style={{\n paddingTop: insets.top,\n paddingBottom: insets.bottom,\n paddingLeft: insets.left,\n paddingRight: insets.right,\n }}\n {...props}\n />\n );\n },\n);\n","import { forwardRef } from \"react\";\nimport type { View as RNView } from \"react-native\";\nimport { type VariantProps, tv } from \"tailwind-variants\";\nimport type { Accent } from \"../../core/AlouetteConfig\";\nimport { AccentScope } from \"./AccentScope\";\nimport { Box, type BoxProps } from \"./Box\";\n\nconst surfaceVariants = tv({\n // overflow-hidden so the multi-layer shadow respects the rounded corners.\n base: \"overflow-hidden transition-background duration-fast\",\n variants: {\n size: {\n xxs: \"p-xs rounded-xs\",\n xs: \"p-sm rounded-xs\",\n sm: \"p-m rounded-sm\",\n md: \"p-xl rounded-sm\",\n lg: \"p-xxl rounded-md\",\n },\n variant: {\n surface: \"bg-surface\",\n highlight: \"bg-highlight\",\n \"highlight-accent\": \"bg-highlight-accent\",\n lowered: \"bg-lowered\",\n translucent: \"bg-translucent\",\n },\n shadow: {\n none: \"shadow-none\",\n s: \"shadow-s\",\n m: \"shadow-m\",\n l: \"shadow-l\",\n lowered: \"shadow-lowered\",\n },\n },\n defaultVariants: {\n size: \"md\",\n variant: \"surface\",\n },\n});\n\ntype SurfaceVariantProps = VariantProps<typeof surfaceVariants>;\n\nexport interface SurfaceProps extends BoxProps, SurfaceVariantProps {\n accent?: Accent;\n}\n\nexport const Surface = forwardRef<RNView, SurfaceProps>(\n ({ className, size, variant, shadow, accent, ...props }, ref) => {\n // shadow defaults to \"s\", or \"lowered\" when variant=\"lowered\".\n const resolvedShadow = shadow ?? (variant === \"lowered\" ? \"lowered\" : \"s\");\n return (\n <AccentScope accent={accent}>\n <Box\n ref={ref}\n className={surfaceVariants({\n size,\n variant,\n shadow: resolvedShadow,\n className,\n })}\n {...props}\n />\n </AccentScope>\n );\n },\n);\n","import type { ComponentType } from \"react\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function styled<P extends { className?: string }>(\n Component: ComponentType<P>,\n defaultClassName: string,\n): ComponentType<P> {\n function StyledComponent({ className, ...props }: P) {\n return (\n <Component\n className={twMerge(defaultClassName, className)}\n {...(props as P)}\n />\n );\n }\n StyledComponent.displayName = `Styled(${Component.displayName ?? Component.name ?? \"Component\"})`;\n StyledComponent.__isStyledComponent = true;\n return StyledComponent;\n}\n","import { forwardRef } from \"react\";\nimport type { Text as RNText } from \"react-native\";\nimport { type VariantProps, tv } from \"tailwind-variants\";\nimport { Text, type TextProps } from \"../primitives/Text\";\n\nconst storyTitleVariants = tv({\n base: \"font-heading-extrabold text-sharp\",\n variants: {\n level: {\n 1: \"text-4xl mb-xl\",\n 2: \"text-3xl mb-xl\",\n 3: \"text-2xl mb-m\",\n 4: \"text-xl mb-m\",\n },\n },\n defaultVariants: {\n level: 1,\n },\n});\n\ntype StoryTitleVariantProps = VariantProps<typeof storyTitleVariants>;\n\nexport interface StoryTitleProps extends TextProps, StoryTitleVariantProps {}\n\nexport const StoryTitle = forwardRef<RNText, StoryTitleProps>(\n ({ className, level, ...props }, ref) => {\n return (\n <Text\n ref={ref}\n className={storyTitleVariants({ level, className })}\n {...props}\n />\n );\n },\n);\n","import { Fragment, type ReactNode } from \"react\";\nimport { Platform } from \"react-native\";\nimport type { Accent, AlouetteModeTheme } from \"../../core/AlouetteConfig\";\nimport { AccentScope } from \"../containers/AccentScope\";\nimport { ScopedTheme } from \"../containers/ScopedTheme\";\nimport { Surface } from \"../containers/Surface\";\nimport { ScrollView } from \"../primitives/ScrollView\";\nimport { View } from \"../primitives/View\";\nimport { VStack } from \"../stacks/stacks\";\nimport { styled } from \"../styled\";\nimport { StoryTitle } from \"./StoryTitle\";\n\nexport interface StorySectionProps {\n title: ReactNode;\n children: ReactNode;\n level?: 1 | 2;\n modeTheme?: AlouetteModeTheme;\n accent?: Accent;\n withSurface?: boolean;\n}\n\nconst InternalStorySection = styled(View, \"-mx-l px-l\");\n\nfunction StorySection({\n title,\n children,\n level = 1,\n modeTheme,\n accent,\n withSurface = false,\n}: StorySectionProps): ReactNode {\n const content = (\n <InternalStorySection className=\"pb-xl bg-screen\">\n {withSurface ? (\n <Surface>\n <StoryTitle level={(level + 1) as 2 | 3}>{title}</StoryTitle>\n <VStack className=\"gap-m\">{children}</VStack>\n </Surface>\n ) : (\n <>\n <StoryTitle level={(level + 1) as 2 | 3}>{title}</StoryTitle>\n <VStack className=\"gap-m\">{children}</VStack>\n </>\n )}\n </InternalStorySection>\n );\n\n if (modeTheme) {\n return <ScopedTheme theme={modeTheme}>{content}</ScopedTheme>;\n }\n if (accent) {\n return <AccentScope accent={accent}>{content}</AccentScope>;\n }\n return content;\n}\n\nfunction StorySubSection({\n title,\n children,\n modeTheme,\n accent,\n withSurface = false,\n}: StorySectionProps): ReactNode {\n const content = (\n <InternalStorySection className=\"mb-m\">\n {withSurface ? (\n <Surface>\n <StoryTitle level={3}>{title}</StoryTitle>\n <VStack className=\"gap-m\">{children}</VStack>\n </Surface>\n ) : (\n <>\n <StoryTitle level={3}>{title}</StoryTitle>\n <VStack className=\"gap-m\">{children}</VStack>\n </>\n )}\n </InternalStorySection>\n );\n if (modeTheme) {\n return <ScopedTheme theme={modeTheme}>{content}</ScopedTheme>;\n }\n if (accent) {\n return <AccentScope accent={accent}>{content}</AccentScope>;\n }\n return content;\n}\n\n// const SimpleWebScrollView = styled(View, \"h-full overflow-auto\");\n\nconst ScrollWrapper = Platform.OS === \"web\" ? Fragment : ScrollView;\n\nexport interface StoryProps {\n documentation?: NonNullable<ReactNode>;\n children?: NonNullable<ReactNode>;\n noDarkMode?: boolean;\n}\n\nexport function Story({\n documentation,\n children,\n noDarkMode,\n}: StoryProps): ReactNode {\n return (\n <ScrollWrapper>\n {documentation && (\n <Surface accent=\"info\" className=\"mb-xxl\">\n {documentation}\n </Surface>\n )}\n {([\"light\", ...(noDarkMode ? [] : [\"dark\"])] as (\"dark\" | \"light\")[]).map(\n (mode) => (\n <ScopedTheme key={mode} theme={mode}>\n <View className=\"bg-screen p-l\">{children}</View>\n </ScopedTheme>\n ),\n )}\n </ScrollWrapper>\n );\n}\n\nStory.Section = StorySection;\nStory.SubSection = StorySubSection;\n\nexport const accents: Accent[] = [\n \"brand\",\n \"danger\",\n \"info\",\n \"success\",\n \"warning\",\n];\n","import type { ReactNode } from \"react\";\nimport { ScopedTheme } from \"../containers/ScopedTheme\";\nimport { ScrollView } from \"../primitives/ScrollView\";\nimport { StoryTitle } from \"./StoryTitle\";\n\nexport interface StoryContainerProps {\n title: ReactNode;\n children: NonNullable<ReactNode>;\n}\n\nexport function StoryContainer({\n title,\n children,\n}: StoryContainerProps): ReactNode {\n return (\n <ScopedTheme theme=\"light\">\n <ScrollView className=\"bg-white p-3xl\">\n <StoryTitle level={1}>{title}</StoryTitle>\n {children}\n </ScrollView>\n </ScopedTheme>\n );\n}\n","import type { Decorator } from \"@storybook/react-vite\";\nimport { StoryContainer } from \"./StoryContainer\";\n\n// eslint-disable-next-line react/function-component-definition -- not a component, it's a decorator for storybook.\nexport const StoryDecorator: Decorator = (storyFn, { name, parameters }) => {\n if (parameters?.container === false) return storyFn();\n return <StoryContainer title={name}>{storyFn()}</StoryContainer>;\n};\n","import { Children, type ReactNode } from \"react\";\nimport { Platform } from \"react-native\";\nimport { tv } from \"tailwind-variants\";\nimport { View } from \"../primitives/View\";\nimport { VStack } from \"../stacks/stacks\";\nimport { StoryTitle } from \"./StoryTitle\";\n\nconst rowVariants = tv(\n {\n base: \"flex-col\",\n variants: {\n breakpoint: {\n small: \"sm:flex-row sm:mb-xl\",\n medium: \"md:flex-row md:mb-xl\",\n },\n flexWrap: { true: \"\" },\n },\n compoundVariants: [\n { breakpoint: \"small\", flexWrap: true, class: \"sm:flex-wrap sm:gap-m\" },\n { breakpoint: \"medium\", flexWrap: true, class: \"md:flex-wrap md:gap-m\" },\n ],\n },\n { twMerge: false },\n);\n\nconst itemVariants = tv(\n {\n base: \"pt-m pb-xl\",\n variants: {\n breakpoint: {\n small: \"sm:pt-0 sm:pb-0 sm:my-xxs shrink\",\n medium: \"md:pt-0 md:pb-0 md:my-xxs shrink\",\n },\n flexWrap: {\n true: \"\",\n false: \"\",\n },\n loose: {\n true: \"\",\n false: \"grow\",\n },\n },\n compoundVariants: [\n { breakpoint: \"small\", flexWrap: false, class: \"sm:basis-0\" },\n { breakpoint: \"medium\", flexWrap: false, class: \"md:basis-0\" },\n ],\n defaultVariants: {\n flexWrap: false,\n },\n },\n { twMerge: false },\n);\n\nexport interface StoryGridRowProps {\n children: NonNullable<ReactNode>;\n breakpoint?: \"medium\" | \"small\";\n flexWrap?: boolean;\n loose?: boolean;\n}\n\nfunction StoryGridRow({\n children,\n breakpoint = \"small\",\n flexWrap,\n loose,\n}: StoryGridRowProps): ReactNode {\n return (\n <View className={rowVariants({ breakpoint, flexWrap })}>\n {Children.map(children, (child) => (\n <View className={itemVariants({ breakpoint, flexWrap, loose })}>\n {child}\n </View>\n ))}\n </View>\n );\n}\n\nexport interface StoryGridColProps {\n children: NonNullable<ReactNode>;\n title?: string;\n platform?: \"all\" | \"native\" | \"web\";\n}\n\nfunction StoryGridCol({\n title,\n children,\n platform = \"all\",\n}: StoryGridColProps): ReactNode {\n const isNative = Platform.OS === \"ios\" || Platform.OS === \"android\";\n\n if (Platform.OS === \"web\" && platform === \"native\") {\n return null;\n }\n if (isNative && platform === \"web\") {\n return null;\n }\n\n return title ? (\n <VStack>\n <StoryTitle level={4} numberOfLines={1}>\n {title}\n </StoryTitle>\n {children}\n </VStack>\n ) : (\n children\n );\n}\n\nexport const StoryGrid = {\n Row: StoryGridRow,\n Col: StoryGridCol,\n};\n","import type { ReactNode } from \"react\";\nimport type { Accent } from \"../../core/AlouetteConfig\";\nimport { useCurrentMode, useCurrentTheme } from \"../../core/ThemeContext\";\nimport { ScopedTheme } from \"./ScopedTheme\";\n\nexport interface StableAccentScopeProps {\n mode?: \"dark\" | \"light\";\n accent?: Accent;\n children?: ReactNode;\n}\n\n/**\n * Like AccentScope, but always keeps a ScopedTheme mounted — when `accent` is\n * unset it re-applies the inherited theme instead of dropping the wrapper.\n * Toggling `accent` (e.g. on hover) therefore only changes the theme prop, so\n * the subtree — and any focused input inside it — is never remounted. Prefer\n * AccentScope when the accent is fixed; reach for this only when it toggles.\n */\nexport function StableAccentScope({\n mode: forcedMode,\n accent,\n children,\n}: StableAccentScopeProps): ReactNode {\n const currentTheme = useCurrentTheme();\n const currentMode = useCurrentMode();\n return (\n <ScopedTheme\n theme={accent ? `${forcedMode ?? currentMode}_${accent}` : currentTheme}\n >\n {children}\n </ScopedTheme>\n );\n}\n","import type { ReactNode } from \"react\";\nimport type { Accent } from \"../../core/AlouetteConfig\";\nimport { StableAccentScope } from \"./StableAccentScope\";\n\nexport interface PortalAccentScopeProps {\n accent?: Accent;\n children?: ReactNode;\n}\n\n/**\n * Theme scope for content rendered through a portal (Modal). Native has no\n * portal — `ScopedTheme` pushes the theme's fully merged variables through\n * context, which crosses the React tree wherever the host renders it — so a\n * single `StableAccentScope` is enough. The web build re-applies the base mode\n * first, see `PortalAccentScope.web.tsx`.\n */\nexport function PortalAccentScope({\n accent,\n children,\n}: PortalAccentScopeProps): ReactNode {\n return <StableAccentScope accent={accent}>{children}</StableAccentScope>;\n}\n","import {\n Children,\n type Key,\n type ReactElement,\n type ReactNode,\n cloneElement,\n isValidElement,\n useEffect,\n useRef,\n useState,\n} from \"react\";\nimport { View } from \"../primitives/View\";\n\nexport interface PresenceBaseProps {\n /**\n * Identity of the current child. When it changes, the previous child is kept\n * mounted (with `exitClassName`) for `exitDurationMs` so it can animate out\n * while the new child animates in — an AnimatePresence-style swap done with\n * pure CSS animations, no animation library.\n */\n activeKey: Key;\n /** How long to keep the exiting child mounted — match the exit animation. */\n exitDurationMs: number;\n /** Animation class applied to the entering (current) child. */\n enterClassName?: string;\n /** Animation class applied to the exiting (previous) child. */\n exitClassName?: string;\n /** Class applied to every item (e.g. `absolute inset-0` to overlap). */\n className?: string;\n}\n\ninterface Snapshot {\n key: Key;\n node: ReactNode;\n}\n\nfunction joinClasses(...classes: (string | undefined)[]): string {\n return classes.filter(Boolean).join(\" \");\n}\n\n/**\n * Shared keyed-swap logic. Keeps the previously rendered child as a snapshot\n * after `activeKey` changes so an exit animation can play, then removes it once\n * `exitDurationMs` has elapsed. The entering child is keyed by `activeKey`, so\n * it remounts on each change and replays its enter animation.\n *\n * Assumes one child whose content is derived from `activeKey` (the common\n * keyed-swap case). Updates to the active child between key changes are shown\n * live but not snapshotted for the next exit.\n */\nfunction usePresence(\n activeKey: Key,\n exitDurationMs: number,\n children: ReactNode,\n): Snapshot[] {\n const [exiting, setExiting] = useState<Snapshot[]>([]);\n const previousRef = useRef<Snapshot>({ key: activeKey, node: children });\n const childrenRef = useRef<ReactNode>(children);\n childrenRef.current = children;\n const timersRef = useRef<ReturnType<typeof setTimeout>[]>([]);\n\n useEffect(\n () => () => {\n timersRef.current.forEach(clearTimeout);\n },\n [],\n );\n\n useEffect(() => {\n const previous = previousRef.current;\n if (previous.key === activeKey) {\n return;\n }\n previousRef.current = { key: activeKey, node: childrenRef.current };\n setExiting((list) => [...list, previous]);\n const timer = setTimeout(() => {\n setExiting((list) => list.filter((item) => item !== previous));\n timersRef.current = timersRef.current.filter((t) => t !== timer);\n }, exitDurationMs);\n timersRef.current.push(timer);\n }, [activeKey, exitDurationMs]);\n\n return exiting;\n}\n\ninterface PresenceItem {\n key: Key;\n node: ReactNode;\n}\n\nfunction toItems(children: ReactNode): PresenceItem[] {\n return Children.toArray(children)\n .filter(isValidElement)\n .map((child) => ({ key: child.key as Key, node: child }));\n}\n\n/**\n * Order-preserving merge of the previously rendered key order with the current\n * live keys (the react-transition-group algorithm). Removed keys stay in their\n * old positions, so an exiting item animates out in place instead of jumping to\n * the end of the list.\n */\nfunction mergeKeys(previous: Key[], next: Key[]): Key[] {\n const nextSet = new Set(next);\n const pendingByNext = new Map<Key, Key[]>();\n let pending: Key[] = [];\n\n for (const key of previous) {\n if (nextSet.has(key)) {\n if (pending.length > 0) {\n pendingByNext.set(key, pending);\n pending = [];\n }\n } else {\n pending.push(key);\n }\n }\n\n const result: Key[] = [];\n for (const key of next) {\n const before = pendingByNext.get(key);\n if (before) {\n result.push(...before);\n }\n result.push(key);\n }\n result.push(...pending);\n return result;\n}\n\ninterface RenderedItem extends PresenceItem {\n exiting: boolean;\n}\n\n/**\n * Diffs a list of keyed children across renders. New keys are returned with\n * `exiting: false` (mounted fresh, so their enter animation plays); removed keys\n * are kept with `exiting: true` for `exitDurationMs` so they can animate out,\n * then dropped. Order is preserved via {@link mergeKeys}.\n */\nfunction usePresenceList(\n children: ReactNode,\n exitDurationMs: number,\n): RenderedItem[] {\n const items = toItems(children);\n const liveKeys = items.map((item) => item.key);\n const signature = liveKeys.join(\"\u0000\");\n\n const nodesRef = useRef<Map<Key, ReactNode>>(new Map());\n for (const item of items) {\n nodesRef.current.set(item.key, item.node);\n }\n const liveKeysRef = useRef(liveKeys);\n liveKeysRef.current = liveKeys;\n\n const [order, setOrder] = useState<Key[]>(liveKeys);\n const orderRef = useRef(order);\n orderRef.current = order;\n const timersRef = useRef<Map<Key, ReturnType<typeof setTimeout>>>(new Map());\n\n useEffect(\n () => () => {\n timersRef.current.forEach(clearTimeout);\n },\n [],\n );\n\n useEffect(() => {\n const live = new Set(liveKeysRef.current);\n const newOrder = mergeKeys(orderRef.current, liveKeysRef.current);\n\n // A key that came back before its timer fired cancels its pending exit.\n for (const key of liveKeysRef.current) {\n const timer = timersRef.current.get(key);\n if (timer) {\n clearTimeout(timer);\n timersRef.current.delete(key);\n }\n }\n // A key that's gone animates out, then is dropped once the timer fires.\n for (const key of newOrder) {\n if (!live.has(key) && !timersRef.current.has(key)) {\n const timer = setTimeout(() => {\n timersRef.current.delete(key);\n nodesRef.current.delete(key);\n setOrder((current) => current.filter((k) => k !== key));\n }, exitDurationMs);\n timersRef.current.set(key, timer);\n }\n }\n setOrder(newOrder);\n }, [signature, exitDurationMs]);\n\n const live = new Set(liveKeys);\n return order.map((key) => ({\n key,\n node: nodesRef.current.get(key),\n exiting: !live.has(key),\n }));\n}\n\nexport interface PresenceListProps {\n /** How long to keep a removed child mounted — match the exit animation. */\n exitDurationMs: number;\n /** Animation class applied to entering children. */\n enterClassName?: string;\n /** Animation class applied to exiting children. */\n exitClassName?: string;\n /** Class applied to every item's wrapper `<View>`. */\n className?: string;\n /**\n * A list of keyed elements — each child must have a stable `key`. Adding a key\n * animates that item in while the others stay put; removing a key animates only\n * that item out.\n */\n children: ReactNode;\n}\n\n/**\n * Keyed-list presence (AnimatePresence-style). Renders a list of keyed children,\n * each wrapped in its own `<View>`, and animates individual add/remove: added\n * keys mount with `enterClassName`, removed keys stay mounted with `exitClassName`\n * for `exitDurationMs` before unmounting. For swapping a single child, use\n * {@link PresenceOne}.\n */\nexport function PresenceList({\n exitDurationMs,\n enterClassName,\n exitClassName,\n className,\n children,\n}: PresenceListProps): ReactNode {\n const items = usePresenceList(children, exitDurationMs);\n\n return (\n <>\n {items.map((item) => (\n <View\n key={item.key}\n className={joinClasses(\n className,\n item.exiting ? exitClassName : enterClassName,\n )}\n >\n {item.node}\n </View>\n ))}\n </>\n );\n}\n\ntype StyledElement = ReactElement<{ className?: string }>;\n\nexport interface PresenceOneProps extends PresenceBaseProps {\n /**\n * A single element that accepts and forwards `className` to its root view.\n * The `className` + enter/exit animation classes are merged onto it directly,\n * so no extra wrapper `<View>` is added to the tree.\n */\n children: StyledElement;\n}\n\n/**\n * Keyed-swap presence that merges the animation classes onto the child element\n * itself via `cloneElement` — no wrapper `<View>`. Requires a single element\n * that forwards `className`; for anything else use {@link PresenceList}.\n */\nexport function PresenceOne({\n activeKey,\n exitDurationMs,\n enterClassName,\n exitClassName,\n className,\n children,\n}: PresenceOneProps): ReactNode {\n const exiting = usePresence(activeKey, exitDurationMs, children);\n\n return (\n <>\n {exiting.map((item) => {\n const node = item.node as StyledElement;\n return cloneElement(node, {\n key: item.key,\n className: joinClasses(\n node.props.className,\n className,\n exitClassName,\n ),\n });\n })}\n {cloneElement(children, {\n key: activeKey,\n className: joinClasses(\n children.props.className,\n className,\n enterClassName,\n ),\n })}\n </>\n );\n}\n","/* Generated by scripts/build-css.ts. DO NOT EDIT. */\n\n/**\n * Duration (in ms) of each generic motion, mirroring the `--animate-*` CSS\n * tokens. Pass the matching value as `exitDurationMs` to {@link PresenceOne} /\n * {@link PresenceList} so the exit timer matches the CSS animation.\n */\nexport const animationDurationsMs = {\n \"slide\": 600,\n \"collapse\": 800,\n \"progress\": 600,\n \"fade\": 300,\n \"fast\": 200\n} as const;\n","import { useRef, useState } from \"react\";\nimport type { ScrollViewProps } from \"react-native\";\n\nexport interface ScrollEndState {\n isScrolledToEnd: boolean;\n scrollViewProps: Required<\n Pick<\n ScrollViewProps,\n \"onContentSizeChange\" | \"onLayout\" | \"onScroll\" | \"scrollEventThrottle\"\n >\n >;\n}\n\n// Sub-pixel layout rounding makes an exact equality unreliable.\nconst scrollEndToleranceInPx = 1;\n\n// Tracks whether a ScrollView is scrolled to its end — true as well when the\n// content fits, since there is then nothing hidden below the fold. Spread the\n// returned props on the ScrollView: onScroll alone never fires for content that\n// doesn't overflow, so layout and content size feed the initial state.\nexport function useScrollEndState(): ScrollEndState {\n const [isScrolledToEnd, setIsScrolledToEnd] = useState(true);\n const viewportHeightRef = useRef(0);\n const contentHeightRef = useRef(0);\n const scrollOffsetRef = useRef(0);\n\n const updateIsScrolledToEnd = (): void => {\n setIsScrolledToEnd(\n contentHeightRef.current - scrollOffsetRef.current <=\n viewportHeightRef.current + scrollEndToleranceInPx,\n );\n };\n\n return {\n isScrolledToEnd,\n scrollViewProps: {\n scrollEventThrottle: 16,\n onLayout: (event) => {\n viewportHeightRef.current = event.nativeEvent.layout.height;\n updateIsScrolledToEnd();\n },\n onContentSizeChange: (_width, height) => {\n contentHeightRef.current = height;\n updateIsScrolledToEnd();\n },\n onScroll: (event) => {\n const { contentOffset, contentSize, layoutMeasurement } =\n event.nativeEvent;\n scrollOffsetRef.current = contentOffset.y;\n contentHeightRef.current = contentSize.height;\n viewportHeightRef.current = layoutMeasurement.height;\n updateIsScrolledToEnd();\n },\n },\n };\n}\n","import { useUnstableNativeVariable as useNativeVariable } from \"nativewind\";\n\nexport const useColorVariable = useNativeVariable as (\n variableName: string,\n) => string | undefined;\n\nexport type ColorClassName =\n | string // keeping string to allow tailwind-variants usage\n // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents\n | \"text-accent\"\n // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents\n | \"text-disabled-muted\"\n // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents\n | \"text-disabled\"\n // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents\n | \"text-muted\"\n // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents\n | \"text-on-accent-muted\"\n // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents\n | \"text-on-accent\"\n // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents\n | \"text-sharp\";\n/**\n * Resolves a `text-*` Tailwind className (e.g. `\"text-accent\"`) to its\n * concrete `--color-*` token value for the active theme. For native SVG\n * props (`color`, `stroke`, `fill`) that can't take a className directly and\n * so can't rely on CSS `currentColor` the way web SVG can.\n */\nexport function useColorToken(className: ColorClassName): string | undefined {\n const token = className\n .split(/\\s+/)\n .find((part) => part.startsWith(\"text-\"))\n ?.slice(\"text-\".length);\n return useColorVariable(`--color-${token ?? \"sharp\"}`);\n}\n","import * as WebBrowser from \"expo-web-browser\";\nimport { WebBrowserPresentationStyle } from \"expo-web-browser\";\nimport type { ComponentProps, FunctionComponent, ReactNode } from \"react\";\nimport { Linking } from \"react-native\";\nimport type { GestureResponderEvent } from \"react-native\";\nimport { useColorVariable } from \"../core/useColorToken\";\nimport type { ExternalOpenLinkBehavior } from \"./ExternalLink.shared\";\n\nexport interface ExternalLinkRequiredComponentProps {\n onPress?: (event: GestureResponderEvent) => Promise<void> | void;\n}\n\nconst useOpenExternalLink = () => {\n const textSharp = useColorVariable(\"text-sharp\");\n const bgSurface = useColorVariable(\"bg-surface\");\n\n return async (href: string, openLinkBehavior: ExternalOpenLinkBehavior) => {\n switch (openLinkBehavior.native) {\n case \"webBrowser\": {\n return WebBrowser.openBrowserAsync(href, {\n controlsColor: textSharp,\n dismissButtonStyle: \"close\",\n presentationStyle: WebBrowserPresentationStyle.PAGE_SHEET,\n toolbarColor: bgSurface,\n secondaryToolbarColor: bgSurface,\n readerMode: false,\n enableBarCollapsing: false,\n showTitle: true,\n enableDefaultShareMenuItem: true,\n });\n }\n case \"linking\": {\n return Linking.openURL(href);\n }\n default: {\n throw new Error(\n `Unsupported openLinkBehavior.native: ${openLinkBehavior.native as string}`,\n );\n }\n }\n };\n};\n\nexport interface ExternalLinkProps<C extends FunctionComponent<any>> {\n as: C;\n href: string;\n onPress?: (event: GestureResponderEvent) => void;\n openLinkBehavior: ExternalOpenLinkBehavior;\n}\n\ntype ExternalLinkSpreadProps<C extends FunctionComponent<any>> = Omit<\n ComponentProps<C>,\n keyof ExternalLinkProps<C>\n>;\n\nexport function ExternalLink<C extends FunctionComponent<any>>({\n as: C,\n href,\n openLinkBehavior,\n onPress,\n ...props\n}: ExternalLinkProps<C> & ExternalLinkSpreadProps<C>): ReactNode {\n const openExternalLink = useOpenExternalLink();\n const handlePress: ExternalLinkRequiredComponentProps[\"onPress\"] = (e) => {\n if (onPress) {\n onPress(e);\n if (e?.defaultPrevented) return;\n }\n\n if (!href) return;\n\n return openExternalLink(href, openLinkBehavior);\n };\n\n return <C {...(props as any)} onPress={handlePress} />;\n}\n","export interface ExternalOpenLinkBehavior {\n native: \"linking\" | \"webBrowser\";\n web: \"targetBlank\" | \"targetSelf\";\n}\n\n/** In-app themed browser sheet on native, new tab on web. */\nexport const defaultExternalOpenLinkBehavior: ExternalOpenLinkBehavior = {\n native: \"webBrowser\",\n web: \"targetBlank\",\n};\n","import {\n type ReactElement,\n type ReactNode,\n type SVGProps,\n cloneElement,\n} from \"react\";\nimport type { ColorClassName } from \"../../core/useColorToken\";\nimport { useColorToken } from \"../../core/useColorToken\";\n\nexport type SVGIconElement = ReactElement<SVGProps<SVGSVGElement>>;\n\nexport interface IconProps {\n icon: SVGIconElement;\n /** Square size in px. Defaults to 20. */\n size?: number;\n /**\n * Text-color className driving the icon tint, e.g. `text-sharp`,\n * `text-muted`, `text-accent`, `text-on-accent`, `text-disabled-muted`.\n * Defaults to `text-sharp`.\n */\n className?: ColorClassName;\n}\n\nexport function Icon({\n icon,\n size = 20,\n className = \"text-sharp\",\n}: IconProps): ReactNode {\n // RN SVG needs a concrete color, not a className. Resolve the text-* color\n // class to its --color-* token value via the active theme.\n const color = useColorToken(className);\n return cloneElement(icon, {\n color,\n width: size,\n height: size,\n });\n}\n","import type { ReactNode } from \"react\";\nimport { useEffect } from \"react\";\nimport Animated, {\n Easing,\n useAnimatedProps,\n useSharedValue,\n withTiming,\n} from \"react-native-reanimated\";\nimport { Circle, Svg } from \"react-native-svg\";\nimport { animationDurationsMs } from \"../../animationDurationsMs\";\n\nexport interface RingCircleProps {\n center: number;\n radius: number;\n strokeWidth: number;\n strokeDasharray?: number;\n strokeDashoffset?: number;\n /** Injected by the native `Icon` via `cloneElement`, resolved from a `useColorToken` string. */\n color?: string;\n /** Injected by the web `Icon` via `cloneElement` — the `text-*` class itself, resolved through CSS `currentColor`. */\n className?: string;\n width?: number;\n height?: number;\n}\n\nconst AnimatedCircle = Animated.createAnimatedComponent(Circle);\n\n// Matches the web ring's CSS `ease-out` (cubic-bezier(0, 0, 0.58, 1)).\nconst easeOut = Easing.bezier(0, 0, 0.58, 1);\n\nexport function RingCircle({\n center,\n radius,\n strokeWidth,\n strokeDasharray,\n strokeDashoffset,\n color,\n width,\n height,\n}: RingCircleProps): ReactNode {\n // Same viewBox as Phosphor icons (e.g. CheckCircleRegularIcon, viewBox\n // \"0 0 256 256\"), whose glyph is a 208-diameter circle inset within it.\n // Rendering into the same viewBox at the same glyph diameter gives the ring\n // the same padding, so it keeps a consistent visual weight when swapped for\n // a Phosphor icon in the same slot (Button's overlay spinner → success check).\n const scale = 208 / (center * 2);\n const scaledRadius = radius * scale;\n const scaledStrokeWidth = strokeWidth * scale;\n const sc