UNPKG

alouette

Version:

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

1,682 lines (1,638 loc) 106 kB
'use strict'; Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); const jsxRuntime = require('react/jsx-runtime'); const nativewind = require('nativewind'); const react = require('react'); const reactNative = require('react-native'); const reactNativeSafeAreaContext = require('react-native-safe-area-context'); const tailwindMerge = require('tailwind-merge'); const tailwindVariants = require('tailwind-variants'); const XRegularIcon = require('alouette-icons/phosphor-icons/XRegularIcon'); const CheckCircleRegularIcon = require('alouette-icons/phosphor-icons/CheckCircleRegularIcon'); const WarningDuotoneIcon = require('alouette-icons/phosphor-icons/WarningDuotoneIcon'); const WebBrowser = require('expo-web-browser'); const Animated = require('react-native-reanimated'); const reactNativeSvg = require('react-native-svg'); const CheckRegularIcon = require('alouette-icons/phosphor-icons/CheckRegularIcon'); const InfoRegularIcon = require('alouette-icons/phosphor-icons/InfoRegularIcon'); const QuestionRegularIcon = require('alouette-icons/phosphor-icons/QuestionRegularIcon'); const WarningRegularIcon = require('alouette-icons/phosphor-icons/WarningRegularIcon'); const ArrowSquareOutRegularIcon = require('alouette-icons/phosphor-icons/ArrowSquareOutRegularIcon'); const CaretDownRegularIcon = require('alouette-icons/phosphor-icons/CaretDownRegularIcon'); const AsteriskSimpleRegularIcon = require('alouette-icons/phosphor-icons/AsteriskSimpleRegularIcon'); const reactHookForm = require('react-hook-form'); const PlusRegularIcon = require('alouette-icons/phosphor-icons/PlusRegularIcon'); const TrashRegularIcon = require('alouette-icons/phosphor-icons/TrashRegularIcon'); const PencilSimpleRegularIcon = require('alouette-icons/phosphor-icons/PencilSimpleRegularIcon'); const CaretRightRegularIcon = require('alouette-icons/phosphor-icons/CaretRightRegularIcon'); function _interopNamespaceDefault(e) { const n = Object.create(null, { [Symbol.toStringTag]: { value: 'Module' } }); if (e) { for (const k in e) { n[k] = e[k]; } } n.default = e; return n; } const WebBrowser__namespace = /*#__PURE__*/_interopNamespaceDefault(WebBrowser); const NativeThemeVariablesContext = react.createContext( null ); const ThemeContext = react.createContext("light"); function useCurrentTheme() { return react.useContext(ThemeContext); } function useCurrentMode() { return react.useContext(ThemeContext).startsWith("dark") ? "dark" : "light"; } function ScopedTheme({ theme, children }) { const themeVariables = react.useContext(NativeThemeVariablesContext); return /* @__PURE__ */ jsxRuntime.jsx(ThemeContext.Provider, { value: theme, children: /* @__PURE__ */ jsxRuntime.jsx(nativewind.VariableContextProvider, { value: themeVariables[theme], children }) }); } function AlouetteProvider({ children, themeVariables }) { const colorScheme = reactNative.useColorScheme(); return /* @__PURE__ */ jsxRuntime.jsx(NativeThemeVariablesContext.Provider, { value: themeVariables, children: /* @__PURE__ */ jsxRuntime.jsx(ScopedTheme, { theme: colorScheme === "dark" ? "dark" : "light", children }) }); } const AlouetteDecorator = (storyFn, context) => { const theme = context.globals.mode === "dark" ? "dark" : "light"; const themeVariables = context.parameters.alouette?.themeVariables; if (!themeVariables) { throw new Error( 'AlouetteDecorator: missing "themeVariables" in parameters.alouette' ); } return /* @__PURE__ */ jsxRuntime.jsx(reactNativeSafeAreaContext.SafeAreaProvider, { children: /* @__PURE__ */ jsxRuntime.jsx(AlouetteProvider, { themeVariables, children: /* @__PURE__ */ jsxRuntime.jsx(ScopedTheme, { theme, children: storyFn(context) }) }) }); }; const View = react.forwardRef((props, ref) => { return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ref, ...props }); }); function AccentScope({ mode: forcedMode, accent, children }) { const currentMode = useCurrentMode(); if (!accent) { return children; } const mode = forcedMode ?? currentMode; return /* @__PURE__ */ jsxRuntime.jsx(ScopedTheme, { theme: `${mode}_${accent}`, children }); } const twMerge = tailwindMerge.extendTailwindMerge({ extend: { classGroups: { "font-family": [ "font-body", "font-body-bold", "font-body-extrabold", "font-heading", "font-heading-bold", "font-heading-extrabold", "font-mono", "font-mono-bold", "font-mono-extrabold" ] } } }); const Text = react.forwardRef( ({ className, accent, ...props }, ref) => { return /* @__PURE__ */ jsxRuntime.jsx(AccentScope, { accent, children: /* @__PURE__ */ jsxRuntime.jsx( reactNative.Text, { ref, className: twMerge("font-body text-sharp", className), ...props } ) }); } ); const Paragraph = react.forwardRef( ({ className, ...props }, ref) => { return /* @__PURE__ */ jsxRuntime.jsx( Text, { ref, role: "paragraph", className: `select-auto ${className ?? ""}`, ...props } ); } ); const ScrollView = nativewind.styled( reactNative.ScrollView, { className: "style", contentContainerClassName: "contentContainerStyle" } ); const FlatList = nativewind.styled( reactNative.FlatList, { className: "style", contentContainerClassName: "contentContainerStyle", columnWrapperClassName: "columnWrapperStyle" } ); const SectionList = nativewind.styled( reactNative.SectionList, { className: "style", contentContainerClassName: "contentContainerStyle" } ); const Stack = react.forwardRef( ({ className, ...props }, ref) => { return /* @__PURE__ */ jsxRuntime.jsx( reactNative.View, { ref, className: `flex-row flex-wrap ${className ?? ""}`, ...props } ); } ); const HStack = react.forwardRef( ({ className, ...props }, ref) => { return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ref, className: `flex-row ${className ?? ""}`, ...props }); } ); const VStack = react.forwardRef( ({ className, ...props }, ref) => { return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ref, className: `flex-col ${className ?? ""}`, ...props }); } ); const separatorVariants = tailwindVariants.tv({ base: "border-border-sharp", variants: { vertical: { true: "self-stretch border-r w-px", false: "self-stretch border-b h-px" } }, defaultVariants: { vertical: false } }); const Separator = react.forwardRef( ({ className, vertical, ...props }, ref) => { return /* @__PURE__ */ jsxRuntime.jsx( reactNative.View, { ref, className: separatorVariants({ vertical, className }), ...props } ); } ); const boxBaseClasses = "shrink"; const Box = react.forwardRef( ({ className, accent, ...props }, ref) => { return /* @__PURE__ */ jsxRuntime.jsx(AccentScope, { accent, children: /* @__PURE__ */ jsxRuntime.jsx( reactNative.View, { ref, className: `${boxBaseClasses} ${className ?? ""}`, ...props } ) }); } ); const interactiveBoxVariants = tailwindVariants.tv({ base: [ boxBaseClasses, "cursor-pointer", "transition-[transform,background-color,border-color] duration-fast ease-in", "disabled:cursor-not-allowed disabled:opacity-70 aria-disabled:cursor-not-allowed aria-disabled:opacity-70", "active:scale-[0.975]" ].join(" "), variants: { withFocusVisibleOutline: { true: "focus-visible:outline-2 focus-visible:outline-offset-2" } } }); const InteractiveBox = react.forwardRef( ({ withFocusVisibleOutline, className, ...rest }, ref) => /* @__PURE__ */ jsxRuntime.jsx( reactNative.Pressable, { ref, pointerEvents: "auto", ...rest, className: interactiveBoxVariants({ withFocusVisibleOutline, className }) } ) ); react.forwardRef( ({ withFocusVisibleOutline, children, className, ...rest }, ref) => { const child = react.Children.only(children); return /* @__PURE__ */ jsxRuntime.jsx( reactNative.Pressable, { ref, pointerEvents: "auto", className: `flex-center ${className ?? ""}`, ...rest, children: react.cloneElement(child, { className: interactiveBoxVariants({ withFocusVisibleOutline, className: child.props.className }) }) } ); } ); const SafeAreaBox = react.forwardRef( (props, ref) => { const insets = reactNativeSafeAreaContext.useSafeAreaInsets(); return /* @__PURE__ */ jsxRuntime.jsx( Box, { ref, style: { paddingTop: insets.top, paddingBottom: insets.bottom, paddingLeft: insets.left, paddingRight: insets.right }, ...props } ); } ); const surfaceVariants = tailwindVariants.tv({ // overflow-hidden so the multi-layer shadow respects the rounded corners. base: "overflow-hidden transition-background duration-fast", variants: { size: { xxs: "p-xs rounded-xs", xs: "p-sm rounded-xs", sm: "p-m rounded-sm", md: "p-xl rounded-sm", lg: "p-xxl rounded-md" }, variant: { surface: "bg-surface", highlight: "bg-highlight", "highlight-accent": "bg-highlight-accent", lowered: "bg-lowered", translucent: "bg-translucent" }, shadow: { none: "shadow-none", s: "shadow-s", m: "shadow-m", l: "shadow-l", lowered: "shadow-lowered" } }, defaultVariants: { size: "md", variant: "surface" } }); const Surface = react.forwardRef( ({ className, size, variant, shadow, accent, ...props }, ref) => { const resolvedShadow = shadow ?? (variant === "lowered" ? "lowered" : "s"); return /* @__PURE__ */ jsxRuntime.jsx(AccentScope, { accent, children: /* @__PURE__ */ jsxRuntime.jsx( Box, { ref, className: surfaceVariants({ size, variant, shadow: resolvedShadow, className }), ...props } ) }); } ); function styled(Component, defaultClassName) { function StyledComponent({ className, ...props }) { return /* @__PURE__ */ jsxRuntime.jsx( Component, { className: tailwindMerge.twMerge(defaultClassName, className), ...props } ); } StyledComponent.displayName = `Styled(${Component.displayName ?? Component.name ?? "Component"})`; StyledComponent.__isStyledComponent = true; return StyledComponent; } const storyTitleVariants = tailwindVariants.tv({ base: "font-heading-extrabold text-sharp", variants: { level: { 1: "text-4xl mb-xl", 2: "text-3xl mb-xl", 3: "text-2xl mb-m", 4: "text-xl mb-m" } }, defaultVariants: { level: 1 } }); const StoryTitle = react.forwardRef( ({ className, level, ...props }, ref) => { return /* @__PURE__ */ jsxRuntime.jsx( Text, { ref, className: storyTitleVariants({ level, className }), ...props } ); } ); const InternalStorySection = styled(View, "-mx-l px-l"); function StorySection({ title, children, level = 1, modeTheme, accent, withSurface = false }) { const content = /* @__PURE__ */ jsxRuntime.jsx(InternalStorySection, { className: "pb-xl bg-screen", children: withSurface ? /* @__PURE__ */ jsxRuntime.jsxs(Surface, { children: [ /* @__PURE__ */ jsxRuntime.jsx(StoryTitle, { level: level + 1, children: title }), /* @__PURE__ */ jsxRuntime.jsx(VStack, { className: "gap-m", children }) ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [ /* @__PURE__ */ jsxRuntime.jsx(StoryTitle, { level: level + 1, children: title }), /* @__PURE__ */ jsxRuntime.jsx(VStack, { className: "gap-m", children }) ] }) }); if (modeTheme) { return /* @__PURE__ */ jsxRuntime.jsx(ScopedTheme, { theme: modeTheme, children: content }); } if (accent) { return /* @__PURE__ */ jsxRuntime.jsx(AccentScope, { accent, children: content }); } return content; } function StorySubSection({ title, children, modeTheme, accent, withSurface = false }) { const content = /* @__PURE__ */ jsxRuntime.jsx(InternalStorySection, { className: "mb-m", children: withSurface ? /* @__PURE__ */ jsxRuntime.jsxs(Surface, { children: [ /* @__PURE__ */ jsxRuntime.jsx(StoryTitle, { level: 3, children: title }), /* @__PURE__ */ jsxRuntime.jsx(VStack, { className: "gap-m", children }) ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [ /* @__PURE__ */ jsxRuntime.jsx(StoryTitle, { level: 3, children: title }), /* @__PURE__ */ jsxRuntime.jsx(VStack, { className: "gap-m", children }) ] }) }); if (modeTheme) { return /* @__PURE__ */ jsxRuntime.jsx(ScopedTheme, { theme: modeTheme, children: content }); } if (accent) { return /* @__PURE__ */ jsxRuntime.jsx(AccentScope, { accent, children: content }); } return content; } const ScrollWrapper = reactNative.Platform.OS === "web" ? react.Fragment : ScrollView; function Story({ documentation, children, noDarkMode }) { return /* @__PURE__ */ jsxRuntime.jsxs(ScrollWrapper, { children: [ documentation && /* @__PURE__ */ jsxRuntime.jsx(Surface, { accent: "info", className: "mb-xxl", children: documentation }), ["light", ...noDarkMode ? [] : ["dark"]].map( (mode) => /* @__PURE__ */ jsxRuntime.jsx(ScopedTheme, { theme: mode, children: /* @__PURE__ */ jsxRuntime.jsx(View, { className: "bg-screen p-l", children }) }, mode) ) ] }); } Story.Section = StorySection; Story.SubSection = StorySubSection; function StoryContainer({ title, children }) { return /* @__PURE__ */ jsxRuntime.jsx(ScopedTheme, { theme: "light", children: /* @__PURE__ */ jsxRuntime.jsxs(ScrollView, { className: "bg-white p-3xl", children: [ /* @__PURE__ */ jsxRuntime.jsx(StoryTitle, { level: 1, children: title }), children ] }) }); } const StoryDecorator = (storyFn, { name, parameters }) => { if (parameters?.container === false) return storyFn(); return /* @__PURE__ */ jsxRuntime.jsx(StoryContainer, { title: name, children: storyFn() }); }; const rowVariants = tailwindVariants.tv( { base: "flex-col", variants: { breakpoint: { small: "sm:flex-row sm:mb-xl", medium: "md:flex-row md:mb-xl" }, flexWrap: { true: "" } }, compoundVariants: [ { breakpoint: "small", flexWrap: true, class: "sm:flex-wrap sm:gap-m" }, { breakpoint: "medium", flexWrap: true, class: "md:flex-wrap md:gap-m" } ] }, { twMerge: false } ); const itemVariants = tailwindVariants.tv( { base: "pt-m pb-xl", variants: { breakpoint: { small: "sm:pt-0 sm:pb-0 sm:my-xxs shrink", medium: "md:pt-0 md:pb-0 md:my-xxs shrink" }, flexWrap: { true: "", false: "" }, loose: { true: "", false: "grow" } }, compoundVariants: [ { breakpoint: "small", flexWrap: false, class: "sm:basis-0" }, { breakpoint: "medium", flexWrap: false, class: "md:basis-0" } ], defaultVariants: { flexWrap: false } }, { twMerge: false } ); function StoryGridRow({ children, breakpoint = "small", flexWrap, loose }) { return /* @__PURE__ */ jsxRuntime.jsx(View, { className: rowVariants({ breakpoint, flexWrap }), children: react.Children.map(children, (child) => /* @__PURE__ */ jsxRuntime.jsx(View, { className: itemVariants({ breakpoint, flexWrap, loose }), children: child })) }); } function StoryGridCol({ title, children, platform = "all" }) { const isNative = reactNative.Platform.OS === "ios" || reactNative.Platform.OS === "android"; if (reactNative.Platform.OS === "web" && platform === "native") { return null; } if (isNative && platform === "web") { return null; } return title ? /* @__PURE__ */ jsxRuntime.jsxs(VStack, { children: [ /* @__PURE__ */ jsxRuntime.jsx(StoryTitle, { level: 4, numberOfLines: 1, children: title }), children ] }) : children; } const StoryGrid = { Row: StoryGridRow, Col: StoryGridCol }; function StableAccentScope({ mode: forcedMode, accent, children }) { const currentTheme = useCurrentTheme(); const currentMode = useCurrentMode(); return /* @__PURE__ */ jsxRuntime.jsx( ScopedTheme, { theme: accent ? `${forcedMode ?? currentMode}_${accent}` : currentTheme, children } ); } function PortalAccentScope({ accent, children }) { return /* @__PURE__ */ jsxRuntime.jsx(StableAccentScope, { accent, children }); } function joinClasses(...classes) { return classes.filter(Boolean).join(" "); } function usePresence(activeKey, exitDurationMs, children) { const [exiting, setExiting] = react.useState([]); const previousRef = react.useRef({ key: activeKey, node: children }); const childrenRef = react.useRef(children); childrenRef.current = children; const timersRef = react.useRef([]); react.useEffect( () => () => { timersRef.current.forEach(clearTimeout); }, [] ); react.useEffect(() => { const previous = previousRef.current; if (previous.key === activeKey) { return; } previousRef.current = { key: activeKey, node: childrenRef.current }; setExiting((list) => [...list, previous]); const timer = setTimeout(() => { setExiting((list) => list.filter((item) => item !== previous)); timersRef.current = timersRef.current.filter((t) => t !== timer); }, exitDurationMs); timersRef.current.push(timer); }, [activeKey, exitDurationMs]); return exiting; } function toItems(children) { return react.Children.toArray(children).filter(react.isValidElement).map((child) => ({ key: child.key, node: child })); } function mergeKeys(previous, next) { const nextSet = new Set(next); const pendingByNext = /* @__PURE__ */ new Map(); let pending = []; for (const key of previous) { if (nextSet.has(key)) { if (pending.length > 0) { pendingByNext.set(key, pending); pending = []; } } else { pending.push(key); } } const result = []; for (const key of next) { const before = pendingByNext.get(key); if (before) { result.push(...before); } result.push(key); } result.push(...pending); return result; } function usePresenceList(children, exitDurationMs) { const items = toItems(children); const liveKeys = items.map((item) => item.key); const signature = liveKeys.join("\0"); const nodesRef = react.useRef(/* @__PURE__ */ new Map()); for (const item of items) { nodesRef.current.set(item.key, item.node); } const liveKeysRef = react.useRef(liveKeys); liveKeysRef.current = liveKeys; const [order, setOrder] = react.useState(liveKeys); const orderRef = react.useRef(order); orderRef.current = order; const timersRef = react.useRef(/* @__PURE__ */ new Map()); react.useEffect( () => () => { timersRef.current.forEach(clearTimeout); }, [] ); react.useEffect(() => { const live2 = new Set(liveKeysRef.current); const newOrder = mergeKeys(orderRef.current, liveKeysRef.current); for (const key of liveKeysRef.current) { const timer = timersRef.current.get(key); if (timer) { clearTimeout(timer); timersRef.current.delete(key); } } for (const key of newOrder) { if (!live2.has(key) && !timersRef.current.has(key)) { const timer = setTimeout(() => { timersRef.current.delete(key); nodesRef.current.delete(key); setOrder((current) => current.filter((k) => k !== key)); }, exitDurationMs); timersRef.current.set(key, timer); } } setOrder(newOrder); }, [signature, exitDurationMs]); const live = new Set(liveKeys); return order.map((key) => ({ key, node: nodesRef.current.get(key), exiting: !live.has(key) })); } function PresenceList({ exitDurationMs, enterClassName, exitClassName, className, children }) { const items = usePresenceList(children, exitDurationMs); return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: items.map((item) => /* @__PURE__ */ jsxRuntime.jsx( View, { className: joinClasses( className, item.exiting ? exitClassName : enterClassName ), children: item.node }, item.key )) }); } function PresenceOne({ activeKey, exitDurationMs, enterClassName, exitClassName, className, children }) { const exiting = usePresence(activeKey, exitDurationMs, children); return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [ exiting.map((item) => { const node = item.node; return react.cloneElement(node, { key: item.key, className: joinClasses( node.props.className, className, exitClassName ) }); }), react.cloneElement(children, { key: activeKey, className: joinClasses( children.props.className, className, enterClassName ) }) ] }); } const animationDurationsMs = { "slide": 600, "collapse": 800, "progress": 600, "fade": 300, "fast": 200 }; const scrollEndToleranceInPx = 1; function useScrollEndState() { const [isScrolledToEnd, setIsScrolledToEnd] = react.useState(true); const viewportHeightRef = react.useRef(0); const contentHeightRef = react.useRef(0); const scrollOffsetRef = react.useRef(0); const updateIsScrolledToEnd = () => { setIsScrolledToEnd( contentHeightRef.current - scrollOffsetRef.current <= viewportHeightRef.current + scrollEndToleranceInPx ); }; return { isScrolledToEnd, scrollViewProps: { scrollEventThrottle: 16, onLayout: (event) => { viewportHeightRef.current = event.nativeEvent.layout.height; updateIsScrolledToEnd(); }, onContentSizeChange: (_width, height) => { contentHeightRef.current = height; updateIsScrolledToEnd(); }, onScroll: (event) => { const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent; scrollOffsetRef.current = contentOffset.y; contentHeightRef.current = contentSize.height; viewportHeightRef.current = layoutMeasurement.height; updateIsScrolledToEnd(); } } }; } const useColorVariable = nativewind.useUnstableNativeVariable; function useColorToken(className) { const token = className.split(/\s+/).find((part) => part.startsWith("text-"))?.slice("text-".length); return useColorVariable(`--color-${token ?? "sharp"}`); } const useOpenExternalLink = () => { const textSharp = useColorVariable("text-sharp"); const bgSurface = useColorVariable("bg-surface"); return async (href, openLinkBehavior) => { switch (openLinkBehavior.native) { case "webBrowser": { return WebBrowser__namespace.openBrowserAsync(href, { controlsColor: textSharp, dismissButtonStyle: "close", presentationStyle: WebBrowser.WebBrowserPresentationStyle.PAGE_SHEET, toolbarColor: bgSurface, secondaryToolbarColor: bgSurface, readerMode: false, enableBarCollapsing: false, showTitle: true, enableDefaultShareMenuItem: true }); } case "linking": { return reactNative.Linking.openURL(href); } default: { throw new Error( `Unsupported openLinkBehavior.native: ${openLinkBehavior.native}` ); } } }; }; function ExternalLink({ as: C, href, openLinkBehavior, onPress, ...props }) { const openExternalLink = useOpenExternalLink(); const handlePress = (e) => { if (onPress) { onPress(e); if (e?.defaultPrevented) return; } if (!href) return; return openExternalLink(href, openLinkBehavior); }; return /* @__PURE__ */ jsxRuntime.jsx(C, { ...props, onPress: handlePress }); } const defaultExternalOpenLinkBehavior = { native: "webBrowser", web: "targetBlank" }; function Icon({ icon, size = 20, className = "text-sharp" }) { const color = useColorToken(className); return react.cloneElement(icon, { color, width: size, height: size }); } const AnimatedCircle = Animated.createAnimatedComponent(reactNativeSvg.Circle); const easeOut = Animated.Easing.bezier(0, 0, 0.58, 1); function RingCircle({ center, radius, strokeWidth, strokeDasharray, strokeDashoffset, color, width, height }) { const scale = 208 / (center * 2); const scaledRadius = radius * scale; const scaledStrokeWidth = strokeWidth * scale; const scaledDashoffset = strokeDashoffset == null ? void 0 : strokeDashoffset * scale; const animatedOffset = Animated.useSharedValue(scaledDashoffset ?? 0); react.useEffect(() => { animatedOffset.value = Animated.withTiming(scaledDashoffset ?? 0, { duration: animationDurationsMs.progress, easing: easeOut }); }, [animatedOffset, scaledDashoffset]); const animatedProps = Animated.useAnimatedProps(() => ({ strokeDashoffset: animatedOffset.value })); return /* @__PURE__ */ jsxRuntime.jsx(reactNativeSvg.Svg, { color, width, height, viewBox: "0 0 256 256", children: strokeDasharray == null ? /* @__PURE__ */ jsxRuntime.jsx( reactNativeSvg.Circle, { cx: 128, cy: 128, r: scaledRadius, stroke: "currentColor", strokeWidth: scaledStrokeWidth, fill: "none" } ) : /* @__PURE__ */ jsxRuntime.jsx( AnimatedCircle, { animatedProps, cx: 128, cy: 128, r: scaledRadius, stroke: "currentColor", strokeWidth: scaledStrokeWidth, strokeDasharray: strokeDasharray * scale, strokeLinecap: "round", transform: "rotate(-90 128 128)", fill: "none" } ) }); } const startDelayMs = 100; const stepIntervalMs = 500; const completeDelayMs = 500; const resetDelayMs = 1e3; const indeterminateExitDurationMs = resetDelayMs + animationDurationsMs.fade; const random = () => Math.ceil(Math.random() * 100) / 100; function nextSimulatedProgress(progress) { if (progress < 60) return progress + random() * 10 + 5; if (progress < 70) return progress + random() * 10 + 3; if (progress < 80) return progress + random() + 5; if (progress < 90) return progress + random() + 1; if (progress < 95) return progress + 0.1; return progress; } function useSimulatedProgress(loading) { const [progress, setProgress] = react.useState(1); const [hidden, setHidden] = react.useState(!loading); react.useEffect(() => { if (!loading) return void 0; setHidden(false); const startTimer = setTimeout(() => { setProgress(20); }, startDelayMs); const stepTimer = setInterval(() => { setProgress(nextSimulatedProgress); }, stepIntervalMs); return () => { clearTimeout(startTimer); clearInterval(stepTimer); }; }, [loading]); react.useEffect(() => { if (loading) return void 0; const completeTimer = setTimeout(() => { setProgress(100); }, completeDelayMs); const resetTimer = setTimeout(() => { setHidden(true); setProgress(1); }, resetDelayMs); return () => { clearTimeout(completeTimer); clearTimeout(resetTimer); }; }, [loading]); return { progress, hidden }; } const diameterBySize = { xs: 16, sm: 32, md: 64, lg: 128 }; const strokeWidthBySize = { xs: 2, sm: 4, md: 8, lg: 16 }; const ring = tailwindVariants.tv({ base: "relative transition-opacity duration-fade", variants: { hidden: { true: "opacity-0", false: "opacity-100" } }, defaultVariants: { hidden: false } }); function CircularProgress({ progress, hidden = false, accent = "brand", size = "md" }) { const diameter = diameterBySize[size]; const strokeWidth = strokeWidthBySize[size]; const radius = (diameter - strokeWidth) / 2; const circumference = 2 * Math.PI * radius; const clampedProgress = Math.min(Math.max(progress, 0), 100); const dashOffset = circumference * (1 - clampedProgress / 100); const center = diameter / 2; const trackRing = /* @__PURE__ */ jsxRuntime.jsx(RingCircle, { center, radius, strokeWidth }); const fillRing = /* @__PURE__ */ jsxRuntime.jsx( RingCircle, { center, radius, strokeWidth, strokeDasharray: circumference, strokeDashoffset: dashOffset } ); return /* @__PURE__ */ jsxRuntime.jsx(AccentScope, { accent, children: /* @__PURE__ */ jsxRuntime.jsxs( View, { className: ring({ hidden }), style: { width: diameter, height: diameter }, children: [ /* @__PURE__ */ jsxRuntime.jsx(View, { className: "absolute inset-0", children: /* @__PURE__ */ jsxRuntime.jsx( Icon, { icon: trackRing, size: diameter, className: "text-border-muted" } ) }), /* @__PURE__ */ jsxRuntime.jsx(View, { className: "absolute inset-0", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon: fillRing, size: diameter, className: "text-accent" }) }) ] } ) }); } function IndeterminateCircularProgress({ loading, accent, size }) { const { progress, hidden } = useSimulatedProgress(loading); return /* @__PURE__ */ jsxRuntime.jsx( CircularProgress, { progress, hidden, accent, size } ); } const pressableBoxVariants = tailwindVariants.tv( { extend: interactiveBoxVariants, base: "overflow-hidden", variants: { variant: { contained: [ "rounded-sm", process.env.EXPO_PUBLIC_STORYBOOK_ENABLED ? "" : "shadow-s bg-interactive-contained-pressable", "hover:bg-interactive-contained-hover", "focus:bg-interactive-contained-focus", "active:bg-interactive-contained-active", "disabled:bg-interactive-contained-disabled disabled:shadow-none", "aria-disabled:bg-interactive-contained-disabled aria-disabled:shadow-none", "focus-visible:outline-border-muted" ].join(" "), outlined: [ "border bg-highlight", process.env.EXPO_PUBLIC_STORYBOOK_ENABLED ? "" : "border-interactive-outlined-pressable", "hover:border-interactive-outlined-hover", "focus:border-interactive-outlined-focus", "active:border-interactive-outlined-active", "disabled:border-interactive-outlined-disabled", "aria-disabled:border-interactive-outlined-disabled", "focus-visible:outline-interactive-outlined-outline-focus" ].join(" "), ghost: [ "border border-transparent", "hover:border hover:border-interactive-outlined-hover", "focus:border focus:border-interactive-outlined-focus", "active:border active:border-interactive-outlined-active", "disabled:border-interactive-outlined-disabled", "aria-disabled:border-interactive-outlined-disabled", "focus-visible:outline-interactive-outlined-outline-focus" ].join(" ") }, forceStyle: { hover: "", focus: "", press: "scale-[0.975]" } }, compoundVariants: process.env.EXPO_PUBLIC_STORYBOOK_ENABLED ? [ /* contained */ { variant: "contained", forceStyle: void 0, ghost: false, className: "shadow-s bg-interactive-contained-pressable" }, { variant: "contained", forceStyle: "hover", className: "shadow-s bg-interactive-contained-hover" }, { variant: "contained", forceStyle: "focus", className: "shadow-s bg-interactive-contained-focus" }, { variant: "contained", forceStyle: "press", className: "shadow-s bg-interactive-contained-active" }, /* outlined */ { variant: "outlined", forceStyle: void 0, ghost: false, className: "border-interactive-outlined-pressable" }, { variant: "outlined", forceStyle: "hover", className: "border-interactive-outlined-hover" }, { variant: "outlined", forceStyle: "focus", className: "border-interactive-outlined-focus" }, { variant: "outlined", forceStyle: "press", className: "border-interactive-outlined-active" }, /* ghost */ { variant: "ghost", forceStyle: void 0, className: "border-transparent" }, { variant: "ghost", forceStyle: "hover", className: "border-interactive-outlined-hover" }, { variant: "ghost", forceStyle: "focus", className: "border-interactive-outlined-focus" }, { variant: "ghost", forceStyle: "press", className: "border-interactive-outlined-active" } ] : void 0, defaultVariants: { variant: "contained" } }, { twMerge: false } ); const PressableBox = react.forwardRef( ({ className, variant, forceStyle, accent, ...props }, ref) => { return /* @__PURE__ */ jsxRuntime.jsx(AccentScope, { accent, children: /* @__PURE__ */ jsxRuntime.jsx( InteractiveBox, { ref, withFocusVisibleOutline: true, role: "button", className: pressableBoxVariants({ variant, className, forceStyle }), ...props } ) }); } ); const buttonHeight = { sm: 38, md: 44 }; const buttonVariants = tailwindVariants.tv( { slots: { frame: "flex-row flex-center relative", text: "font-body-bold text-center shrink transition-opacity duration-fade", icon: "", terminalIcon: "text-accent", overlayIconContainer: "absolute inset-0 flex-center" }, variants: { size: { sm: { frame: "rounded-sm px-xs gap-xxs min-h-[38px]", text: "text-sm py-xxs" }, md: { frame: "rounded-sm px-m gap-xs min-h-[44px]", text: "text-base py-xs" } }, variant: { contained: { text: "text-on-accent" }, outlined: { text: "text-sharp" }, ghost: { text: "text-sharp" } }, disabled: { true: {}, false: {} }, dimmed: { true: { text: "opacity-30", icon: "opacity-30" }, false: {} } }, compoundVariants: [ { variant: "contained", disabled: false, ghost: false, class: { icon: "text-on-accent" } }, { variant: "contained", disabled: false, ghost: true, class: { text: "text-sharp hover:text-on-accent", icon: "text-sharp hover:text-on-accent" } }, { variant: "outlined", disabled: false, class: { icon: "text-sharp" } }, { variant: "contained", disabled: true, class: { icon: "text-disabled-sharp", text: "text-disabled-sharp" } }, { variant: "outlined", disabled: true, class: { icon: "text-disabled-muted", text: "text-disabled-muted" } } ], defaultVariants: { size: "md", variant: "contained" } }, { twMerge: false } ); function resolveTerminalIcon(state) { if (state === "success") { return { terminalIcon: /* @__PURE__ */ jsxRuntime.jsx(CheckCircleRegularIcon.CheckCircleRegularIcon, {}), terminalIconAccent: "success" }; } if (state === "failed") { return { terminalIcon: /* @__PURE__ */ jsxRuntime.jsx(WarningDuotoneIcon.WarningDuotoneIcon, {}), terminalIconAccent: "danger" }; } return { terminalIcon: void 0, terminalIconAccent: void 0 }; } function isButtonDisabled({ disabled, state }) { return disabled === true || state != null; } function Button({ icon, text, disabled, state, accent = "brand", variant = "contained", size = "md", className, ...pressableProps }) { const isLoading = state === "loading"; const [showSpinner, setShowSpinner] = react.useState(isLoading); react.useEffect(() => { if (isLoading) { setShowSpinner(true); return void 0; } const timer = setTimeout(() => { setShowSpinner(false); }, indeterminateExitDurationMs); return () => { clearTimeout(timer); }; }, [isLoading]); const { terminalIcon, terminalIconAccent } = resolveTerminalIcon(state); const hasOverlayIcon = showSpinner || terminalIcon !== void 0; const isDisabled = isButtonDisabled({ disabled, state }); const styles = buttonVariants({ size, variant, disabled: isDisabled, dimmed: hasOverlayIcon }); return /* @__PURE__ */ jsxRuntime.jsxs( PressableBox, { accent, variant, disabled: isDisabled, className: styles.frame({ className }), ...pressableProps, children: [ hasOverlayIcon ? /* @__PURE__ */ jsxRuntime.jsx(View, { className: styles.overlayIconContainer(), children: showSpinner || !terminalIcon ? /* @__PURE__ */ jsxRuntime.jsx( IndeterminateCircularProgress, { loading: isLoading, accent, size: size === "sm" ? "xs" : "sm" } ) : /* @__PURE__ */ jsxRuntime.jsx(AccentScope, { accent: terminalIconAccent, children: /* @__PURE__ */ jsxRuntime.jsx( Icon, { icon: terminalIcon, className: styles.terminalIcon(), size: size === "sm" ? 24 : 32 } ) }) }) : null, icon ? /* @__PURE__ */ jsxRuntime.jsx( Icon, { icon, className: styles.icon(), size: size === "sm" ? 16 : 20 } ) : null, /* @__PURE__ */ jsxRuntime.jsx(Text, { "aria-disabled": isDisabled, className: styles.text(), children: text }) ] } ); } function ExternalLinkButton({ href, openLinkBehavior = defaultExternalOpenLinkBehavior, onPress, ...buttonProps }) { return /* @__PURE__ */ jsxRuntime.jsx( ExternalLink, { as: Button, href: isButtonDisabled(buttonProps) ? "" : href, openLinkBehavior, role: "link", onPress: onPress ?? void 0, ...buttonProps } ); } function InternalLinkButton({ href: _href, ...buttonProps }) { return /* @__PURE__ */ jsxRuntime.jsx(Button, { ...buttonProps, role: "link" }); } const iconButtonVariants = tailwindVariants.tv( { slots: { frame: "shrink-0 flex-center rounded-full", icon: "" }, variants: { variant: { contained: {}, outlined: {}, ghost: {} }, disabled: { true: {}, false: {} } }, compoundVariants: [ { variant: "contained", disabled: false, class: { icon: "text-on-accent" } }, { variant: "outlined", disabled: false, class: { icon: "text-sharp" } }, { variant: "ghost", disabled: false, class: { icon: "text-sharp" } }, { variant: "contained", disabled: true, class: { icon: "text-disabled-sharp" } }, { variant: "outlined", disabled: true, class: { icon: "text-disabled-muted" } }, { variant: "ghost", disabled: true, class: { icon: "text-disabled-muted" } } ], defaultVariants: { variant: "contained" } }, { twMerge: false } ); function IconButton({ icon, disabled, size = "md", iconSize, variant = "contained", className, ...pressableProps }) { const diameter = typeof size === "number" ? size : buttonHeight[size]; const styles = iconButtonVariants({ variant, disabled: disabled === true }); return /* @__PURE__ */ jsxRuntime.jsx( PressableBox, { variant, disabled, className: styles.frame({ className }), style: { width: diameter, height: diameter }, ...pressableProps, children: /* @__PURE__ */ jsxRuntime.jsx( Icon, { icon, size: diameter * (iconSize === "fill" ? 0.8 : 0.55), className: styles.icon() } ) } ); } const supportsStickyPosition = reactNative.Platform.OS === "web"; const modalVariants = tailwindVariants.tv({ slots: { // w-full so the panel shrinks on small screens (the backdrop padding keeps a // margin); max-w caps it on wide viewports. panel: "w-full max-h-full", inset: "bg-highlight shadow-l", header: "items-center gap-xs", scrollContent: "", // `sticky` pins it to the bottom of the scroll box on web. The border is // transparent at rest so toggling it can't shift the layout. footer: "items-center justify-end gap-m sticky bottom-0 bg-highlight border-t border-transparent" }, variants: { size: { sm: { panel: "max-w-[360px]", inset: "rounded-sm p-xs", header: "pl-xs", scrollContent: "p-xs", footer: "py-xs" }, md: { panel: "max-w-[520px]", inset: "rounded-sm p-m", header: "pl-m", scrollContent: "p-m", footer: "py-sm" }, lg: { panel: "max-w-[720px]", inset: "rounded-md p-l", header: "pl-l", scrollContent: "p-l", footer: "py-m" } }, withFooter: { true: { scrollContent: "pb-0" } }, // Native has no sticky positioning, so the footer sits below the scroll box // instead of inside it — outside the scroll content container it has to // carry that container's horizontal padding itself to stay aligned with the // body. detachedFooter: { true: {} }, // Only while the footer overlaps scrolled-past content does it need a rule // separating it from the body; at the end of the scroll it sits in flow. stuck: { true: { footer: "border-border-muted" } } }, compoundVariants: [ { size: "sm", detachedFooter: true, class: { footer: "px-xs" } }, { size: "md", detachedFooter: true, class: { footer: "px-m" } }, { size: "lg", detachedFooter: true, class: { footer: "px-l" } } ], defaultVariants: { size: "md" } }); function Modal({ visible, onClose, children, icon, footer, accent, size = "md", title, hideCloseButton = false, closeButtonAriaLabel = "Close", role = "dialog", "aria-describedby": ariaDescribedby, testID }) { const { height: windowHeight } = reactNative.useWindowDimensions(); const titleId = react.useId(); const iconSize = size === "lg" ? "md" : size; const { isScrolledToEnd, scrollViewProps } = useScrollEndState(); const styles = modalVariants({ size, withFooter: footer !== void 0, stuck: footer !== void 0 && !isScrolledToEnd, detachedFooter: !supportsStickyPosition }); const footerElement = footer === void 0 ? null : /* @__PURE__ */ jsxRuntime.jsx(HStack, { className: styles.footer(), children: footer }); return /* @__PURE__ */ jsxRuntime.jsx( reactNative.Modal, { transparent: true, visible, animationType: "fade", onRequestClose: onClose, children: /* @__PURE__ */ jsxRuntime.jsx(PortalAccentScope, { accent, children: /* @__PURE__ */ jsxRuntime.jsxs(View, { className: "flex-1 flex-center p-l", children: [ /* @__PURE__ */ jsxRuntime.jsx( reactNative.Pressable, { "aria-hidden": true, focusable: false, className: "absolute inset-0 bg-translucent", onPress: onClose } ), /* @__PURE__ */ jsxRuntime.jsx( View, { "aria-modal": true, role, "aria-labelledby": titleId, "aria-describedby": ariaDescribedby, testID, className: styles.panel(), children: /* @__PURE__ */ jsxRuntime.jsxs(View, { className: styles.inset(), children: [ /* @__PURE__ */ jsxRuntime.jsxs( HStack, { className: styles.header(), style: { minHeight: buttonHeight[iconSize] }, children: [ icon === void 0 ? null : /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon, size: 24, className: "text-accent" }), /* @__PURE__ */ jsxRuntime.jsx( Text, { nativeID: titleId, className: "shrink grow font-heading-bold text-xl leading-tight text-sharp", children: title } ), hideCloseButton ? null : /* @__PURE__ */ jsxRuntime.jsx( IconButton, { icon: /* @__PURE__ */ jsxRuntime.jsx(XRegularIcon.XRegularIcon, {}), variant: "ghost", size: iconSize, "aria-label": closeButtonAriaLabel, onPress: onClose } ) ] } ), /* @__PURE__ */ jsxRuntime.jsxs( ScrollView, { className: "shrink", style: { maxHeight: windowHeight * 0.7 }, contentContainerClassName: styles.scrollContent(), ...scrollViewProps, children: [ children, supportsStickyPosition ? footerElement : null ] } ), supportsStickyPosition ? null : footerElement ] }) } ) ] }) }) } ); } const messageFrameVariants = tailwindVariants.tv( { base: "flex-row items-center overflow-hidden bg-highlight-accent", variants: { size: { sm: "gap-xs p-sm rounded-xs", md: "gap-m p-m rounded-sm", lg: "gap-l p-l rounded-md" }, variant: { // Raised: the banner is its own layer above the screen background. surface: "shadow-m", // Flush: for a banner already inside a raised surface, where a second // elevation would read as a card stacked on a card. flat: "shadow-none border-border-muted border" } }, defaultVariants: { size: "md", variant: "surface" } }, { twMerge: false } ); const ICON_SIZE$1 = { sm: 20, md: 24, lg: 28 }; const DISMISS_BUTTON_SIZE = { sm: 24, md: 40, lg: 40 }; function Message({ icon, size = "md", variant, accent, children, onDismiss, dismissIconAriaLabel }) { const dismissDiameter = DISMISS_BUTTON_SIZE[size]; return /* @__PURE__ */ jsxRuntime.jsx(AccentScope, { accent, children: /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: messageFrameVariants({ size, variant }), children: [ /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon, size: ICON_SIZE$1[size], className: "text-accent" }), /* @__PURE__ */ jsxRuntime.jsx(Text, { className: "text-sharp shrink grow", children }), onDismiss ? /* @__PURE__ */ jsxRuntime.jsx( Box, { style: { width: dismissDiameter, height: dismissDiameter }, className: "shrink-0 flex-center", children: /* @__PURE__ */ jsxRuntime.jsx( IconButton, { icon: /* @__PURE__ */ jsxRuntime.jsx(XRegularIcon.XRegularIcon, {}), iconSize: size === "sm" ? "fill" : void 0, size: dismissDiameter, variant: "ghost", "aria-label": dismissIconAriaLabel, onPress: onDismiss } ) } ) : null ] }) }); } function InfoMessage(props) { return /* @__PURE__ */ jsxRuntime.jsx(Message, { ...props, accent: "info", icon: /* @__PURE__ */ jsxRuntime.jsx(InfoRegularIcon.InfoRegularIcon, {}) }); } function ConfirmationMessage(props) { return /* @__PURE__ */ jsxRuntime.jsx(Message, { ...props, accent: "success", icon: /* @__PURE__ */ jsxRuntime.jsx(CheckRegularIcon.CheckRegularIcon, {}) }); } function WarningMessage(props) { return /* @__PURE__ */ jsxRuntime.jsx(Message, { ...props, accent: "warning", icon: /* @__PURE__ */ jsxRuntime.jsx(WarningRegularIcon.WarningRegularIcon, {}) }); } function ErrorMessage(props) { return /* @__PURE__ */ jsxRuntime.jsx(Message, { ...props, accent: "danger", icon: /* @__PURE__ */ jsxRuntime.jsx(WarningDuotoneIcon.WarningDuotoneIcon, {}) }); } function CollapsibleErrorMessage({ error, errorToMessage, variant }) { return /* @__PURE__ */ jsxRuntime.jsx( View, { role: "alert", className: `overflow-hidden transition-[height,opacity] duration-collapse ${error ? "p-sm h-auto opacity-100" : "absolute h-0 opacity-0"}`, children: error === null ? null : /* @__PURE__ */ jsxRuntime.jsx(ErrorMessage, { size: "sm", variant, children: errorToMessage(error) }) } ); } const settledDisplayDurationMs = 4e3; const idleState = { buttonState: void 0, error: null }; function pressAsyncReducer(previousState, action) { switch (action.type) { case "start": return { buttonState: "loading", error: null }; case "resolve": return { buttonState: "success", error: null }; case "reject": return { buttonState: "failed", error: action.error }; case "settledTimeout": return { buttonState: void 0, error: previousState.error }; default: throw new Error(`Unhandled action: ${JSON.stringify(action)}`); } } function usePressAsync(onPress) { const [pressAsyncState, dispatch] = react.useReducer(pressAsyncReducer,