alouette
Version:
A modern, customizable design system built on top of NativeWind v5 with configurable defaults
5,294 lines • 150 kB
JavaScript
import { jsx, jsxs, Fragment as Fragment$1 } from 'react/jsx-runtime';
import { VariableContextProvider, styled as styled$1, useUnstableNativeVariable } from 'nativewind';
import { createContext, useContext, forwardRef, Children, cloneElement, Fragment, useRef, useState, useEffect, isValidElement, useId, useReducer, useCallback, useMemo } from 'react';
import { useColorScheme, View as View$1, Text as Text$1, ScrollView as ScrollView$1, FlatList as FlatList$1, SectionList as SectionList$1, Pressable, Platform, Modal as Modal$1, Linking, useWindowDimensions, TextInput, Switch as Switch$1 } from 'react-native';
import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context';
export { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context';
import { extendTailwindMerge, twMerge as twMerge$1 } from 'tailwind-merge';
import { tv } from 'tailwind-variants';
import { PencilSimpleRegularIcon } from 'alouette-icons/phosphor-icons/PencilSimpleRegularIcon';
import { CheckCircleRegularIcon } from 'alouette-icons/phosphor-icons/CheckCircleRegularIcon';
import { WarningDuotoneIcon } from 'alouette-icons/phosphor-icons/WarningDuotoneIcon';
import * as WebBrowser from 'expo-web-browser';
import { WebBrowserPresentationStyle } from 'expo-web-browser';
import Animated, { Easing, useSharedValue, withTiming, useAnimatedProps } from 'react-native-reanimated';
import { Circle, Svg } from 'react-native-svg';
import { XRegularIcon } from 'alouette-icons/phosphor-icons/XRegularIcon';
import { CheckRegularIcon } from 'alouette-icons/phosphor-icons/CheckRegularIcon';
import { InfoRegularIcon } from 'alouette-icons/phosphor-icons/InfoRegularIcon';
import { QuestionRegularIcon } from 'alouette-icons/phosphor-icons/QuestionRegularIcon';
import { WarningRegularIcon } from 'alouette-icons/phosphor-icons/WarningRegularIcon';
import { ArrowSquareOutRegularIcon } from 'alouette-icons/phosphor-icons/ArrowSquareOutRegularIcon';
import { CaretDownRegularIcon } from 'alouette-icons/phosphor-icons/CaretDownRegularIcon';
import { DesktopDuotoneIcon } from 'alouette-icons/phosphor-icons/DesktopDuotoneIcon';
import { DesktopRegularIcon } from 'alouette-icons/phosphor-icons/DesktopRegularIcon';
import { MoonDuotoneIcon } from 'alouette-icons/phosphor-icons/MoonDuotoneIcon';
import { MoonRegularIcon } from 'alouette-icons/phosphor-icons/MoonRegularIcon';
import { SunDuotoneIcon } from 'alouette-icons/phosphor-icons/SunDuotoneIcon';
import { SunRegularIcon } from 'alouette-icons/phosphor-icons/SunRegularIcon';
import { CaretRightRegularIcon } from 'alouette-icons/phosphor-icons/CaretRightRegularIcon';
import { AsteriskSimpleRegularIcon } from 'alouette-icons/phosphor-icons/AsteriskSimpleRegularIcon';
import { useForm, FormProvider, useFormContext, Controller, useFieldArray } from 'react-hook-form';
import { PlusRegularIcon } from 'alouette-icons/phosphor-icons/PlusRegularIcon';
import { TrashRegularIcon } from 'alouette-icons/phosphor-icons/TrashRegularIcon';
const NativeThemeVariablesContext = createContext(
null
);
const ThemeContext = createContext("light");
function useCurrentTheme() {
return useContext(ThemeContext);
}
function useCurrentMode() {
return useContext(ThemeContext).startsWith("dark") ? "dark" : "light";
}
function ScopedTheme({ theme, children }) {
const themeVariables = useContext(NativeThemeVariablesContext);
return /* @__PURE__ */ jsx(ThemeContext.Provider, { value: theme, children: /* @__PURE__ */ jsx(VariableContextProvider, { value: themeVariables[theme], children }) });
}
function AlouetteProvider({
children,
themeVariables
}) {
const colorScheme = useColorScheme();
return /* @__PURE__ */ jsx(NativeThemeVariablesContext.Provider, { value: themeVariables, children: /* @__PURE__ */ 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__ */ jsx(SafeAreaProvider, { children: /* @__PURE__ */ jsx(AlouetteProvider, { themeVariables, children: /* @__PURE__ */ jsx(ScopedTheme, { theme, children: storyFn(context) }) }) });
};
const allSafeAreaEdges = [
"top",
"bottom",
"left",
"right"
];
const ConsumedSafeAreaEdgesContext = createContext([]);
function useConsumedSafeAreaEdges() {
return useContext(ConsumedSafeAreaEdgesContext);
}
function SafeAreaScope({
consumedEdges,
children
}) {
const parentEdges = useConsumedSafeAreaEdges();
const mergedEdges = allSafeAreaEdges.filter(
(edge) => parentEdges.includes(edge) || consumedEdges.includes(edge)
);
return /* @__PURE__ */ jsx(ConsumedSafeAreaEdgesContext.Provider, { value: mergedEdges, children });
}
function useScreenSafeAreaPadding(edges) {
const insets = useSafeAreaInsets();
const consumedEdges = useConsumedSafeAreaEdges();
const appliedEdges = edges ?? allSafeAreaEdges.filter((edge) => !consumedEdges.includes(edge));
const padding = {};
if (insets.top > 0 && appliedEdges.includes("top")) {
padding.paddingTop = insets.top;
}
if (insets.bottom > 0 && appliedEdges.includes("bottom")) {
padding.paddingBottom = insets.bottom;
}
if (insets.left > 0 && appliedEdges.includes("left")) {
padding.paddingLeft = insets.left;
}
if (insets.right > 0 && appliedEdges.includes("right")) {
padding.paddingRight = insets.right;
}
return Object.keys(padding).length === 0 ? void 0 : padding;
}
function useSystemColorMode() {
return useColorScheme() === "dark" ? "dark" : "light";
}
function useResolvedColorMode(preference) {
const systemMode = useSystemColorMode();
return preference === "system" ? systemMode : preference;
}
const View = forwardRef((props, ref) => {
return /* @__PURE__ */ jsx(View$1, { ref, ...props });
});
function AccentScope({
mode: forcedMode,
accent,
children
}) {
const currentMode = useCurrentMode();
if (!accent) {
return children;
}
const mode = forcedMode ?? currentMode;
return /* @__PURE__ */ jsx(ScopedTheme, { theme: accent === "none" ? mode : `${mode}_${accent}`, children });
}
const twMerge = 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 = forwardRef(
({ className, accent, ...props }, ref) => {
return /* @__PURE__ */ jsx(AccentScope, { accent, children: /* @__PURE__ */ jsx(
Text$1,
{
ref,
className: twMerge("font-body text-sharp", className),
...props
}
) });
}
);
const Paragraph = forwardRef(
({ className, ...props }, ref) => {
return /* @__PURE__ */ jsx(
Text,
{
ref,
role: "paragraph",
className: `select-auto ${className ?? ""}`,
...props
}
);
}
);
const ScrollView = styled$1(
ScrollView$1,
{
className: "style",
contentContainerClassName: "contentContainerStyle"
}
);
const FlatList = styled$1(
FlatList$1,
{
className: "style",
contentContainerClassName: "contentContainerStyle",
columnWrapperClassName: "columnWrapperStyle"
}
);
const SectionList = styled$1(
SectionList$1,
{
className: "style",
contentContainerClassName: "contentContainerStyle"
}
);
const Stack = forwardRef(
({ className, ...props }, ref) => {
return /* @__PURE__ */ jsx(
View$1,
{
ref,
className: `flex-row flex-wrap ${className ?? ""}`,
...props
}
);
}
);
const HStack = forwardRef(
({ className, ...props }, ref) => {
return /* @__PURE__ */ jsx(View$1, { ref, className: `flex-row ${className ?? ""}`, ...props });
}
);
const VStack = forwardRef(
({ className, ...props }, ref) => {
return /* @__PURE__ */ jsx(View$1, { ref, className: `flex-col ${className ?? ""}`, ...props });
}
);
const separatorVariants = 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 = forwardRef(
({ className, vertical, ...props }, ref) => {
return /* @__PURE__ */ jsx(
View$1,
{
ref,
className: separatorVariants({ vertical, className }),
...props
}
);
}
);
const boxBaseClasses = "shrink";
const Box = forwardRef(
({ className, accent, ...props }, ref) => {
return /* @__PURE__ */ jsx(AccentScope, { accent, children: /* @__PURE__ */ jsx(
View$1,
{
ref,
className: `${boxBaseClasses} ${className ?? ""}`,
...props
}
) });
}
);
const interactiveBoxVariants = 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",
// `outline-none` cannot express this: react-native-css keeps solid,
// dotted and dashed outline styles only and drops `none`, so the
// browser's own focus ring is overridden with a zero-width one instead.
false: "outline-solid outline-0"
}
}
});
const InteractiveBox = forwardRef(
({ withFocusVisibleOutline, className, ...rest }, ref) => /* @__PURE__ */ jsx(
Pressable,
{
ref,
pointerEvents: "auto",
...rest,
className: interactiveBoxVariants({ withFocusVisibleOutline, className })
}
)
);
forwardRef(
({ withFocusVisibleOutline, children, className, ...rest }, ref) => {
const child = Children.only(children);
return /* @__PURE__ */ jsx(
Pressable,
{
ref,
pointerEvents: "auto",
className: `flex-center ${className ?? ""}`,
...rest,
children: cloneElement(child, {
className: interactiveBoxVariants({
withFocusVisibleOutline,
className: child.props.className
})
})
}
);
}
);
const SafeAreaBox = forwardRef(
(props, ref) => {
const insets = useSafeAreaInsets();
return /* @__PURE__ */ jsx(
Box,
{
ref,
style: {
paddingTop: insets.top,
paddingBottom: insets.bottom,
paddingLeft: insets.left,
paddingRight: insets.right
},
...props
}
);
}
);
const surfaceVariants = 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 = forwardRef(
({ className, size, variant, shadow, accent, ...props }, ref) => {
const resolvedShadow = shadow ?? (variant === "lowered" ? "lowered" : "s");
return /* @__PURE__ */ jsx(AccentScope, { accent, children: /* @__PURE__ */ jsx(
Box,
{
ref,
className: surfaceVariants({
size,
variant,
shadow: resolvedShadow,
className
}),
...props
}
) });
}
);
function styled(Component, defaultClassName) {
function StyledComponent({ className, ...props }) {
return /* @__PURE__ */ jsx(
Component,
{
className: twMerge$1(defaultClassName, className),
...props
}
);
}
StyledComponent.displayName = `Styled(${Component.displayName ?? Component.name ?? "Component"})`;
StyledComponent.__isStyledComponent = true;
return StyledComponent;
}
const storyTitleVariants = 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 = forwardRef(
({ className, level, ...props }, ref) => {
return /* @__PURE__ */ 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__ */ jsx(InternalStorySection, { className: "pb-xl bg-screen", children: withSurface ? /* @__PURE__ */ jsxs(Surface, { children: [
/* @__PURE__ */ jsx(StoryTitle, { level: level + 1, children: title }),
/* @__PURE__ */ jsx(VStack, { className: "gap-m", children })
] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
/* @__PURE__ */ jsx(StoryTitle, { level: level + 1, children: title }),
/* @__PURE__ */ jsx(VStack, { className: "gap-m", children })
] }) });
if (modeTheme) {
return /* @__PURE__ */ jsx(ScopedTheme, { theme: modeTheme, children: content });
}
if (accent) {
return /* @__PURE__ */ jsx(AccentScope, { accent, children: content });
}
return content;
}
function StorySubSection({
title,
children,
modeTheme,
accent,
withSurface = false
}) {
const content = /* @__PURE__ */ jsx(InternalStorySection, { className: "mb-m", children: withSurface ? /* @__PURE__ */ jsxs(Surface, { children: [
/* @__PURE__ */ jsx(StoryTitle, { level: 3, children: title }),
/* @__PURE__ */ jsx(VStack, { className: "gap-m", children })
] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
/* @__PURE__ */ jsx(StoryTitle, { level: 3, children: title }),
/* @__PURE__ */ jsx(VStack, { className: "gap-m", children })
] }) });
if (modeTheme) {
return /* @__PURE__ */ jsx(ScopedTheme, { theme: modeTheme, children: content });
}
if (accent) {
return /* @__PURE__ */ jsx(AccentScope, { accent, children: content });
}
return content;
}
const ScrollWrapper = Platform.OS === "web" ? Fragment : ScrollView;
function Story({
documentation,
children,
noDarkMode
}) {
return /* @__PURE__ */ jsxs(ScrollWrapper, { children: [
documentation && /* @__PURE__ */ jsx(Surface, { accent: "info", className: "mb-xxl", children: documentation }),
["light", ...noDarkMode ? [] : ["dark"]].map(
(mode) => /* @__PURE__ */ jsx(ScopedTheme, { theme: mode, children: /* @__PURE__ */ jsx(ScrollView, { className: "h-full bg-screen px-l", children }) }, mode)
)
] });
}
Story.Section = StorySection;
Story.SubSection = StorySubSection;
function StoryContainer({
title,
children
}) {
return /* @__PURE__ */ jsx(ScopedTheme, { theme: "light", children: /* @__PURE__ */ jsxs(ScrollView, { className: "bg-white p-3xl", children: [
/* @__PURE__ */ jsx(StoryTitle, { level: 1, children: title }),
children
] }) });
}
const StoryDecorator = (storyFn, { name, parameters }) => {
if (parameters?.container === false) return storyFn();
return /* @__PURE__ */ jsx(StoryContainer, { title: name, children: storyFn() });
};
const rowVariants = 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 = 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__ */ jsx(View, { className: rowVariants({ breakpoint, flexWrap }), children: Children.map(children, (child) => /* @__PURE__ */ jsx(View, { className: itemVariants({ breakpoint, flexWrap, loose }), children: child })) });
}
function StoryGridCol({
title,
children,
platform = "all"
}) {
const isNative = Platform.OS === "ios" || Platform.OS === "android";
if (Platform.OS === "web" && platform === "native") {
return null;
}
if (isNative && platform === "web") {
return null;
}
return title ? /* @__PURE__ */ jsxs(VStack, { children: [
/* @__PURE__ */ 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();
const theme = (() => {
if (!accent) return currentTheme;
if (accent === "none") return forcedMode ?? currentMode;
return `${forcedMode ?? currentMode}_${accent}`;
})();
return /* @__PURE__ */ jsx(ScopedTheme, { theme, children });
}
function PortalAccentScope({
accent,
children
}) {
return /* @__PURE__ */ jsx(StableAccentScope, { accent, children });
}
function joinClasses(...classes) {
return classes.filter(Boolean).join(" ");
}
function usePresence(activeKey, exitDurationMs, children) {
const [exiting, setExiting] = useState([]);
const previousRef = useRef({ key: activeKey, node: children });
const childrenRef = useRef(children);
childrenRef.current = children;
const timersRef = useRef([]);
useEffect(
() => () => {
timersRef.current.forEach(clearTimeout);
},
[]
);
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 Children.toArray(children).filter(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 = useRef(/* @__PURE__ */ new Map());
for (const item of items) {
nodesRef.current.set(item.key, item.node);
}
const liveKeysRef = useRef(liveKeys);
liveKeysRef.current = liveKeys;
const [order, setOrder] = useState(liveKeys);
const orderRef = useRef(order);
orderRef.current = order;
const timersRef = useRef(/* @__PURE__ */ new Map());
useEffect(
() => () => {
timersRef.current.forEach(clearTimeout);
},
[]
);
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__ */ jsx(Fragment$1, { children: items.map((item) => /* @__PURE__ */ 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__ */ jsxs(Fragment$1, { children: [
exiting.map((item) => {
const node = item.node;
return cloneElement(node, {
key: item.key,
className: joinClasses(
node.props.className,
className,
exitClassName
)
});
}),
cloneElement(children, {
key: activeKey,
className: joinClasses(
children.props.className,
className,
enterClassName
)
})
] });
}
const animationDurationsMs = {
slide: 600,
collapse: 800,
progress: 600,
fade: 300,
fast: 200
};
const statusBarGap = 8;
function PopoverOverlay({
placement,
"aria-label": ariaLabel,
onClose,
children
}) {
const insets = useSafeAreaInsets();
return /* @__PURE__ */ jsxs(
View,
{
className: placement === "top" ? "flex-1 justify-start px-xl" : "flex-1 justify-center px-xl",
style: placement === "top" ? { paddingTop: insets.top + statusBarGap } : void 0,
children: [
/* @__PURE__ */ jsx(
Pressable,
{
"aria-hidden": true,
focusable: false,
className: "absolute inset-0 bg-translucent",
onPress: onClose
}
),
/* @__PURE__ */ jsx(View, { "aria-label": ariaLabel, className: "w-full", children })
]
}
);
}
function Popover({
open,
onClose,
placement = "center",
accent,
"aria-label": ariaLabel,
children
}) {
return /* @__PURE__ */ jsx(
Modal$1,
{
transparent: true,
visible: open,
animationType: "fade",
onRequestClose: onClose,
children: /* @__PURE__ */ jsx(SafeAreaProvider, { children: /* @__PURE__ */ jsx(PortalAccentScope, { accent, children: /* @__PURE__ */ jsx(
PopoverOverlay,
{
placement,
"aria-label": ariaLabel,
onClose,
children
}
) }) })
}
);
}
const useColorVariable = useUnstableNativeVariable;
function useColorToken(className) {
const token = className.split(/\s+/).find((part) => part.startsWith("text-"))?.slice("text-".length);
return useColorVariable(`--color-${token ?? "sharp"}`);
}
function Icon({
icon,
size = 20,
className = "text-sharp"
}) {
const color = useColorToken(className);
return cloneElement(icon, {
color,
width: size,
height: size
});
}
const interactiveIconVariants = tv({
slots: {
frame: "relative shrink-0",
rest: "transition-opacity duration-fast ease-in group-hover:opacity-0 group-focus:opacity-0 group-active:opacity-0",
active: "absolute inset-0 opacity-0 transition-opacity duration-fast ease-in group-hover:opacity-100 group-focus:opacity-100 group-active:opacity-100"
},
variants: {
active: {
true: { rest: "opacity-0", active: "opacity-100" },
false: {}
}
},
defaultVariants: { active: false }
});
function InteractiveIcon({
icon,
activeIcon,
activeAccent,
active = false,
disabled = false,
size = 20,
className = "text-sharp"
}) {
if (activeIcon === void 0 || disabled) {
return /* @__PURE__ */ jsx(
Icon,
{
icon: active && activeIcon ? activeIcon : icon,
size,
className
}
);
}
const styles = interactiveIconVariants({ active });
const activeClassName = activeAccent ? "text-accent" : className;
return /* @__PURE__ */ jsxs(
View,
{
className: styles.frame({ className }),
style: { width: size, height: size },
children: [
/* @__PURE__ */ jsx(View, { className: styles.rest(), children: /* @__PURE__ */ jsx(Icon, { icon, size, className }) }),
/* @__PURE__ */ jsx(View, { className: styles.active(), children: /* @__PURE__ */ jsx(AccentScope, { accent: activeAccent, children: /* @__PURE__ */ jsx(Icon, { icon: activeIcon, size, className: activeClassName }) }) })
]
}
);
}
const useOpenExternalLink = () => {
const textSharp = useColorVariable("text-sharp");
const bgSurface = useColorVariable("bg-surface");
return async (href, openLinkBehavior) => {
switch (openLinkBehavior.native) {
case "webBrowser": {
return WebBrowser.openBrowserAsync(href, {
controlsColor: textSharp,
dismissButtonStyle: "close",
presentationStyle: WebBrowserPresentationStyle.PAGE_SHEET,
toolbarColor: bgSurface,
secondaryToolbarColor: bgSurface,
readerMode: false,
enableBarCollapsing: false,
showTitle: true,
enableDefaultShareMenuItem: true
});
}
case "linking": {
return 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__ */ jsx(C, { ...props, onPress: handlePress });
}
const defaultExternalOpenLinkBehavior = {
native: "webBrowser",
web: "targetBlank"
};
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
const easeOut = 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 = useSharedValue(scaledDashoffset ?? 0);
useEffect(() => {
animatedOffset.value = withTiming(scaledDashoffset ?? 0, {
duration: animationDurationsMs.progress,
easing: easeOut
});
}, [animatedOffset, scaledDashoffset]);
const animatedProps = useAnimatedProps(() => ({
strokeDashoffset: animatedOffset.value
}));
return /* @__PURE__ */ jsx(Svg, { color, width, height, viewBox: "0 0 256 256", children: strokeDasharray == null ? /* @__PURE__ */ jsx(
Circle,
{
cx: 128,
cy: 128,
r: scaledRadius,
stroke: "currentColor",
strokeWidth: scaledStrokeWidth,
fill: "none"
}
) : /* @__PURE__ */ 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] = useState(1);
const [hidden, setHidden] = useState(!loading);
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]);
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 = 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__ */ jsx(RingCircle, { center, radius, strokeWidth });
const fillRing = /* @__PURE__ */ jsx(
RingCircle,
{
center,
radius,
strokeWidth,
strokeDasharray: circumference,
strokeDashoffset: dashOffset
}
);
return /* @__PURE__ */ jsx(AccentScope, { accent, children: /* @__PURE__ */ jsxs(
View,
{
className: ring({ hidden }),
style: { width: diameter, height: diameter },
children: [
/* @__PURE__ */ jsx(View, { className: "absolute inset-0", children: /* @__PURE__ */ jsx(
Icon,
{
icon: trackRing,
size: diameter,
className: "text-border-muted"
}
) }),
/* @__PURE__ */ jsx(View, { className: "absolute inset-0", children: /* @__PURE__ */ jsx(Icon, { icon: fillRing, size: diameter, className: "text-accent" }) })
]
}
) });
}
function IndeterminateCircularProgress({
loading,
accent,
size
}) {
const { progress, hidden } = useSimulatedProgress(loading);
return /* @__PURE__ */ jsx(
CircularProgress,
{
progress,
hidden,
accent,
size
}
);
}
const pressableBoxVariants = tv(
{
extend: interactiveBoxVariants,
// `group`: a child styles itself from the pressable's state — the icon
// swapped by InteractiveIcon, and anything an app composes on top.
base: "group 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(" "),
// No ground and no border at rest: the affordance is the fill arriving
// on hover, like a listbox row (ListboxOption). The fill is a tone of
// the surrounding surface, not the accent, so the label keeps its own
// color. No radius either (twMerge is off here, so a variant radius
// would collide with the caller's own).
soft: [
process.env.EXPO_PUBLIC_STORYBOOK_ENABLED ? "" : "bg-transparent",
"hover:bg-interactive-soft-hover",
"focus:bg-interactive-soft-focus",
"active:bg-interactive-soft-active",
"disabled:bg-transparent",
"aria-disabled:bg-transparent",
"focus-visible:outline-offset-0 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"
},
/* soft */
{
variant: "soft",
forceStyle: void 0,
className: "bg-transparent"
},
{
variant: "soft",
forceStyle: "hover",
className: "bg-interactive-soft-hover"
},
{
variant: "soft",
forceStyle: "focus",
className: "bg-interactive-soft-focus"
},
{
variant: "soft",
forceStyle: "press",
className: "bg-interactive-soft-active"
}
] : void 0,
defaultVariants: {
variant: "contained"
}
},
{ twMerge: false }
);
const PressableBox = forwardRef(
({
className,
variant,
forceStyle,
accent,
href,
withFocusVisibleOutline = true,
...props
}, ref) => {
return /* @__PURE__ */ jsx(AccentScope, { accent, children: /* @__PURE__ */ jsx(
InteractiveBox,
{
ref,
withFocusVisibleOutline,
role: "button",
className: pressableBoxVariants({
variant,
className,
forceStyle
}),
...href === void 0 ? {} : { href, role: "link" },
...props
}
) });
}
);
const buttonHeight = {
sm: 38,
md: 44
};
const buttonVariants = 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-sm 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" },
soft: { 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: "soft", 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" }
},
{
variant: "soft",
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__ */ jsx(CheckCircleRegularIcon, {}),
terminalIconAccent: "success"
};
}
if (state === "failed") {
return {
terminalIcon: /* @__PURE__ */ jsx(WarningDuotoneIcon, {}),
terminalIconAccent: "danger"
};
}
return { terminalIcon: void 0, terminalIconAccent: void 0 };
}
function isButtonDisabled({
disabled,
state
}) {
return disabled === true || state != null;
}
function Button({
icon,
activeIcon,
text,
disabled,
state,
accent = "brand",
variant = "contained",
size = "md",
className,
forceStyle,
...pressableProps
}) {
const isLoading = state === "loading";
const [showSpinner, setShowSpinner] = useState(isLoading);
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__ */ jsxs(
PressableBox,
{
accent,
variant,
disabled: isDisabled,
forceStyle,
className: styles.frame({ className }),
...pressableProps,
children: [
hasOverlayIcon ? /* @__PURE__ */ jsx(View, { className: styles.overlayIconContainer(), children: showSpinner || !terminalIcon ? /* @__PURE__ */ jsx(
IndeterminateCircularProgress,
{
loading: isLoading,
accent,
size: size === "sm" ? "xs" : "sm"
}
) : /* @__PURE__ */ jsx(AccentScope, { accent: terminalIconAccent, children: /* @__PURE__ */ jsx(
Icon,
{
icon: terminalIcon,
className: styles.terminalIcon(),
size: size === "sm" ? 24 : 32
}
) }) }) : null,
icon ? /* @__PURE__ */ jsx(
InteractiveIcon,
{
icon,
activeIcon,
active: forceStyle !== void 0,
disabled: isDisabled,
className: styles.icon(),
size: size === "sm" ? 16 : 20
}
) : null,
/* @__PURE__ */ jsx(Text, { "aria-disabled": isDisabled, className: styles.text(), children: text })
]
}
);
}
function ExternalLinkButton({
href,
openLinkBehavior = defaultExternalOpenLinkBehavior,
onPress,
...buttonProps
}) {
return /* @__PURE__ */ jsx(
ExternalLink,
{
as: Button,
href: isButtonDisabled(buttonProps) ? "" : href,
openLinkBehavior,
role: "link",
onPress: onPress ?? void 0,
...buttonProps
}
);
}
function InternalLinkButton({
href: _href,
...buttonProps
}) {
return /* @__PURE__ */ jsx(Button, { ...buttonProps, role: "link" });
}
const iconButtonVariants = tv(
{
slots: {
frame: "shrink-0 flex-center rounded-full",
icon: ""
},
variants: {
variant: {
contained: {},
outlined: {},
ghost: {},
soft: {}
},
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: "soft",
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" }
},
{
variant: "soft",
disabled: true,
class: { icon: "text-disabled-muted" }
}
],
defaultVariants: { variant: "contained" }
},
{ twMerge: false }
);
function IconButton({
icon,
activeIcon,
disabled,
size = "md",
iconSize,
variant = "contained",
className,
forceStyle,
...pressableProps
}) {
const diameter = typeof size === "number" ? size : buttonHeight[size];
const styles = iconButtonVariants({ variant, disabled: disabled === true });
return /* @__PURE__ */ jsx(
PressableBox,
{
variant,
disabled,
forceStyle,
className: styles.frame({ className }),
style: { width: diameter, height: diameter },
...pressableProps,
children: /* @__PURE__ */ jsx(
InteractiveIcon,
{
icon,
activeIcon,
active: forceStyle !== void 0,
disabled: disabled === true,
size: diameter * (iconSize === "fill" ? 0.8 : 0.55),
className: styles.icon()
}
)
}
);
}
function EditableSurface({
title,
titleBadge,
details,
editAriaLabel,
editIcon = /* @__PURE__ */ jsx(PencilSimpleRegularIcon, {}),
editIconVariant,
accent,
className,
shadow,
size,
variant,
disabled,
onEdit,
children
}) {
const titleId = useId();
return /* @__PURE__ */ jsx(
Surface,
{
role: "region",
"aria-labelledby": titleId,
accent,
shadow,
size,
variant,
className,
children: /* @__PURE__ */ jsxs(VStack, { className: "gap-sm", children: [
/* @__PURE__ */ jsxs(HStack, { className: "items-start justify-between gap-sm", children: [
/* @__PURE__ */ jsxs(VStack, { className: "shrink items-start", children: [
/* @__PURE__ */ jsxs(HStack, { className: "items-center gap-sm", children: [
/* @__PURE__ */ jsx(Text, { nativeID: titleId, className: "font-heading-bold text-xl", children: title }),
titleBadge ? /* @__PURE__ */ jsx(View, { children: titleBadge }) : null
] }),
details ? /* @__PURE__ */ jsx(Text, { className: "text-muted text-sm", children: details }) : null
] }),
/* @__PURE__ */ jsx(
IconButton,
{
size: "sm",
icon: editIcon,
variant: editIconVariant,
disabled,
"aria-label": editAriaLabel,
onPress: onEdit
}
)
] }),
children
] })
}
);
}
const scrollEndToleranceInPx = 1;
function useScrollEndState() {
const [isScrolledToEnd, setIsScrolledToEnd] = useState(true);
const viewportHeightRef = useRef(0);
const contentHeightRef = useRef(0);
const scrollOffsetRef = 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 supportsStickyPosition = Platform.OS === "web";
const modalVariants = 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 } = useWindowDimensions();
const titleId = 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__ */ jsx(HStack, { className: styles.footer(), children: footer });
return /* @__PURE__ */ jsx(
Modal$1,
{
transparent: true,
visible,
animationType: "fade",
onRequestClose: onClose,
children: /* @__PURE__ */ jsx(PortalAccentScope, { accent, children: /* @__PURE__ */ jsxs(View, { className: "flex-1 flex-center p-l", children: [
/* @__PURE__ */ jsx(
Pressable,
{
"aria-hidden": true,
focusable: false,
className: "absolute inset-0 bg-translucent",
onPress: onClose
}
),
/* @__PURE__ */ jsx(
View,
{
"aria-modal": true,
role,
"aria-labelledby": titleId,
"aria-describedby": ariaDescribedby,
testID,
className: styles.panel(),
children: /* @__PURE__ */ jsxs(View, { className: styles.inset(), children: [
/* @__PURE__ */ jsxs(
HStack,
{
className: styles.header(),
style: { minHeight: buttonHeight[iconSize] },
children: [
icon === void 0 ? null : /* @__PURE__ */ jsx(Icon, { icon, size: 24, className: "text-accent" }),
/* @__PURE__ */ jsx(
Text,
{
nativeID: titleId,
className: "shrink grow font-heading-bold text-xl leading-tight text-sharp",
children: title
}
),
hideCloseButton ? null : /* @__PURE__ */ jsx(
IconButton,
{
icon: /* @__PURE__ */ jsx(XRegularIcon, {}),
variant: "ghost",
size: iconSize,
"aria-label": closeButtonAriaLabel,
onPress: onClose
}
)
]
}
),
/* @__PURE__ */ jsxs(
ScrollView,
{
className: "shrink",
style: { maxHeight: windowHeight * 0.7 },
contentContainerClassName: styles.scrollContent(),
...scrollViewProps,
children: [
children,
supportsStickyPosition ? footerElement : null
]
}
),
supportsStickyPosition ? null : footerElement
] })
}
)
] }) })
}
);
}
const messageFrameVariants = 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__ */ jsx(AccentScope, { accent, children: /* @__PURE__ */ jsxs(Box, { className: messageFrameVariants({ size, variant }), children: [
/* @__PURE__ */ jsx(Icon, { icon, size: ICON_SIZE$1[size], className: "text-accent" }),
/* @__PURE__ */ jsx(Text, { className: "text-sharp shrink grow", children }),
onDismiss ? /* @__PURE__ */ jsx(
Box,
{
style: { width: dismissDiameter, height: dismissDiameter },
className: "shrink-0 flex-center",
children: /* @__PURE__ */ jsx(
IconButton,
{
icon: /* @__PURE__ */ jsx(XRegularIcon, {}),
iconSize: size === "sm" ? "fill" : void 0,
size: dismissDiameter,
variant: "ghost",
"aria-label": dismissIconAriaLabel,
onPress: onDismiss
}
)
}
) : null
] }) });
}
function InfoMessage(props) {
return /* @__PURE__ */ jsx(Message, { ...props, accent: "info", icon: /* @__PURE__ */ jsx(InfoRegularIcon, {}) });
}
function ConfirmationMessage(props) {
return /* @__PURE__ */ jsx(Message, { ...props, accent: "success", icon: /* @__PURE__ */ jsx(CheckRegularIcon, {}) });
}
function WarningMessage(props) {
return /* @__PURE__ */ jsx(Message, { ...props, accent: "warning", icon: /* @__PURE__ */ jsx(WarningRegularIcon, {}) });
}
function ErrorMessage(props) {
return /* @__PURE__ */ jsx(Message, { ...props, accent: "danger", icon: /* @__PURE__ */ jsx(WarningDuotoneIcon, {}) });
}
function CollapsibleErrorMessage({
error,
errorToMessage,
variant
}) {
return /* @__PURE__ */ 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__ */ 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] = useReducer(pressAsyncReducer, idleState);
const settledTimerRef = useRef(void 0);
useEffect(() => {
return () => {
clearTimeout(settledTimerRef.current);
};
}, []);
function handlePress(event) {
if (pressAsyncState.buttonState === "loading") return;
clearTimeout(settledTimerRef.current);
const result = onPress(event);
if (!(result instanceof Promise)) return;
dispatch({ type: "start" });
function scheduleSettledTimeout() {
settledTimerRef.current = setTimeout(() => {
dispatch({ type: "settledTimeout" });
}, settledDisplayDurationMs);
}
result.then(() => {
dispatch({ type: "resolve" });
scheduleSettledTimeout();
}).catch((caughtError) => {
const normalizedError = caughtError instanceof Error ? caughtError : new Error(String(caughtError));
console.error(
"Unexpected error caught in usePressAsync",
normalizedError
);
dispatch({ type: "reject", error: normalizedError });
scheduleSettledTimeout();
});
}
return { ...pressAsyncState, handlePress };
}
function noop() {
}
function ActionFooter({
children,
errorToMessage,
error
}) {
const errorMessage = errorToMessage === void 0 ? null : (
// The dialog panel is already a raised surface, so the message is flat.
/* @__PURE__ */ jsx(
CollapsibleErrorMessage,
{
error,
errorToMessage,
variant: "flat"
}
)
);
return /* @__PURE__ */ jsxs(VStack, { className: "w-full gap-sm", children: [
/* @__PURE__ */ jsx(HStack, { className: "items-center justify-end gap-m", children }),
errorMessage
] });
}
function resolveVariant(props, {
accent,
buttonState,
error,
isPending,
handleConfirm
}) {
switch (props.variant) {
case "alert": {
const { onClose, closeText } = props;
return {
onDismiss: onClose,
footer: /* @__PURE__ */ jsx(Button, { accent, text: closeText ?? "OK", onPress: onClose })
};
}
case "required": {
const { confirmText, confirmDisabled, errorToMessage } = props;
return {
// Non-dismissible: only the explicit action closes it.
onDismiss: noop,
footer: /* @__PURE__ */ jsx(ActionFooter, { error, errorToMessage, children: /* @__PURE__ */ jsx(
Button,
{
accent,
text: confirmText ?? "OK",
state: buttonState,
disabled: confirmDisabled,
onPress: handleConfirm
}
) })
};
}
case "confirm":
case void 0:
default: {
const {
onCancel,
confirmText,
cancelText,
confirmDisabled,
errorToMessage
} = props;
return {
onDismiss: isPending ? noop : onCancel,
footer: /* @__PURE__ */ jsxs(ActionFooter, { error, errorToMessage, children: [
/* @__PURE__ */ jsx(
Button,
{
variant: "outlined",
text: cancelText ?? "Cancel",
disabled: isPending,
onPress: onCancel
}
),
/* @__PURE__ */ jsx(
Button,
{
accent,
text: confirmText ?? "Confirm",
state: buttonState,
disabled: confirmDisabled,
onPress: handleConfirm
}
)
] })
};
}
}
}
function resolveConfirmHandler(props) {
return props.variant === "alert" ? noop : props.onConfirm;
}
function AlertDialog(props) {
const {
visible,
title,
children,
accent = "danger",
icon,
size = "md",
testID
} = props;
const descriptionId = useId();
const { buttonState, error, handlePress } = usePressAsync(
resolveConfirmHandler(props)
);
const isPending = buttonState === "loading";
const { footer, onDismiss } = resolveVariant(props, {
accent,
buttonState,
error,
isPending,
handleConfirm: handlePress
});
return /* @__PURE__ */ jsx(
Modal,
{
hideCloseButton: true,
visible,
role: "alertdialog",
accent,
size,
title,
icon,
"aria-describedby": children === void 0 ? void 0 : descriptionId,
testID,
footer,
onClose: onDismiss,
children: children === void 0 ? null : /* @__PURE__ */ jsx(Text, { nativeID: descriptionId, className: "text-base text-muted", children })
}
);
}
function QuestionAlertDialog(props) {
return /* @__PURE__ */ jsx(AlertDialog, { ...props, icon: /* @__PURE__ */ jsx(QuestionRegularIcon, {}) });
}
function WarningAlertDialog(props) {
return /* @__PURE__ */ jsx(AlertDialog, { ...props, icon: /* @__PURE__ */ jsx(WarningRegularIcon, {}) });
}
function InfoAlertDialog(props) {
return /* @__PURE__ */ jsx(AlertDialog, { ...props, icon: /* @__PURE__ */ jsx(InfoRegularIcon, {}) });
}
function SuccessAlertDialog(props) {
return /* @__PURE__ */ jsx(AlertDialog, { ...props, icon: /* @__PURE__ */ jsx(CheckRegularIcon, {}) });
}
const linkTextVariants = tv(
{
slots: {
frame: "group flex-row items-center gap-xxs self-start focus-visible:outline-interactive-outlined-outline-focus",
text: "shrink font-body-bold underline transition-[color] duration-fast ease-in",
icon: ""
},
variants: {
size: {
sm: { text: "text-sm" },
md: { text: "text-base" }
},
disabled: {
true: {
text: "text-disabled-muted",
icon: "text-disabled-muted"
},
false: {
text: "text-interactive-pressable group-hover:text-interactive-hover group-active:text-interactive-active",
icon: "text-interactive-pressable group-hover:text-interactive-hover group-active:text-interactive-active"
}
}
},
defaultVariants: { size: "md", disabled: false }
},
{ twMerge: false }
);
function linkTextIconSize(size) {
return size === "sm" ? 16 : 20;
}
function LinkPressable(props) {
return /* @__PURE__ */ jsx(InteractiveBox, { ...props });
}
function LinkText({
href,
text,
icon,
accent,
size = "md",
disabled,
className,
...pressableProps
}) {
const isDisabled = disabled === true;
const styles = linkTextVariants({ size, disabled: isDisabled });
return /* @__PURE__ */ jsx(AccentScope, { accent, children: /* @__PURE__ */ jsxs(
LinkPressable,
{
withFocusVisibleOutline: true,
role: "link",
href: isDisabled ? void 0 : href,
"aria-disabled": isDisabled,
disabled,
className: styles.frame({ className }),
...pressableProps,
children: [
icon ? /* @__PURE__ */ jsx(
Icon,
{
icon,
size: linkTextIconSize(size),
className: styles.icon()
}
) : null,
/* @__PURE__ */ jsx(Text, { className: styles.text(), children: text })
]
}
) });
}
function ExternalLinkText({
href,
openLinkBehavior = defaultExternalOpenLinkBehavior,
text,
icon = /* @__PURE__ */ jsx(ArrowSquareOutRegularIcon, {}),
accent,
size = "md",
disabled,
className,
onPress,
...pressableProps
}) {
const isDisabled = disabled === true;
const styles = linkTextVariants({ size, disabled: isDisabled });
return /* @__PURE__ */ jsx(AccentScope, { accent, children: /* @__PURE__ */ jsxs(
ExternalLink,
{
withFocusVisibleOutline: true,
as: InteractiveBox,
href: isDisabled ? "" : href,
openLinkBehavior,
role: "link",
"aria-disabled": isDisabled,
disabled,
className: styles.frame({ className }),
onPress: onPress ?? void 0,
...pressableProps,
children: [
/* @__PURE__ */ jsx(
Icon,
{
icon,
size: linkTextIconSize(size),
className: styles.icon()
}
),
/* @__PURE__ */ jsx(Text, { className: styles.text(), children: text })
]
}
) });
}
function ActionButton({
onPress,
errorToMessage,
errorMessageVariant,
...buttonProps
}) {
const { buttonState, error, handlePress } = usePressAsync(onPress);
return /* @__PURE__ */ jsxs(VStack, { className: "shrink", children: [
/* @__PURE__ */ jsx(Button, { ...buttonProps, state: buttonState, onPress: handlePress }),
/* @__PURE__ */ jsx(
CollapsibleErrorMessage,
{
error,
errorToMessage,
variant: errorMessageVariant
}
)
] });
}
const MenuContext = createContext(void 0);
const MenuContextProvider = MenuContext.Provider;
function useMenuContext() {
const context = useContext(MenuContext);
if (!context) {
throw new Error("MenuItem must be rendered inside a Menu.");
}
return context;
}
function Menu({
render,
label,
header,
accent,
onOpenChange,
children
}) {
const triggerRef = useRef(null);
const [menuNode, setMenuNode] = useState(null);
const [open, setOpen] = useState(false);
const setOpenState = useCallback(
(next) => {
setOpen(next);
onOpenChange?.(next);
},
[onOpenChange]
);
const close = useCallback(() => {
setOpenState(false);
}, [setOpenState]);
const toggle = useCallback(() => {
setOpenState(!open);
}, [open, setOpenState]);
const contextValue = useMemo(() => ({ close }), [close]);
return /* @__PURE__ */ jsxs(Fragment$1, { children: [
render({
ref: triggerRef,
onPress: toggle,
"aria-haspopup": "menu",
"aria-expanded": open
}),
/* @__PURE__ */ jsx(
Popover,
{
open,
anchorRef: triggerRef,
align: "end",
width: "content",
placement: "top",
accent: accent ?? "none",
onClose: close,
children: /* @__PURE__ */ jsx(View, { className: "pt-xxs", children: /* @__PURE__ */ jsxs(
Surface,
{
variant: "highlight",
shadow: "l",
size: "sm",
className: "p-xs min-w-[220px]",
children: [
header === void 0 ? null : /* @__PURE__ */ jsx(View, { className: "px-m py-xs", children: header }),
/* @__PURE__ */ jsx(MenuContextProvider, { value: contextValue, children: /* @__PURE__ */ jsx(View, { ref: setMenuNode, role: "menu", "aria-label": label, children }) })
]
}
) })
}
)
] });
}
const menuItemVariants = tv({
slots: {
frame: "flex-row items-center gap-xs rounded-xs px-m min-h-[44px]",
icon: "text-muted",
label: "flex-1 text-base text-sharp"
},
variants: {
accented: {
true: { icon: "text-accent", label: "text-accent" },
false: {}
},
disabled: {
true: { icon: "text-disabled-muted", label: "text-disabled-sharp" },
false: {}
}
},
defaultVariants: { accented: false, disabled: false }
});
function MenuItem({
label,
icon,
activeIcon,
accent,
href,
disabled,
onPress
}) {
const { close } = useMenuContext();
const styles = menuItemVariants({
accented: accent !== void 0,
disabled
});
const press = (event) => {
onPress?.(event);
close();
};
return /* @__PURE__ */ jsxs(
PressableBox,
{
variant: "soft",
withFocusVisibleOutline: false,
accent,
className: styles.frame(),
disabled,
...{
role: "menuitem",
// A disabled Pressable never sees the press, so dropping the href is
// the only thing that stops the browser from following the link anyway.
href: disabled === true ? void 0 : href,
"aria-disabled": disabled === true,
onPress: press
},
children: [
icon ? /* @__PURE__ */ jsx(
InteractiveIcon,
{
icon,
activeIcon,
disabled: disabled === true,
size: 20,
className: styles.icon()
}
) : null,
/* @__PURE__ */ jsx(Text, { className: styles.label(), children: label })
]
}
);
}
const inputVariants = tv(
{
base: [
"bg-highlight text-sharp",
"border",
"transition-[border-color,background-color,outline-color] duration-fast ease-in",
"outline-interactive-outlined-pressable",
// to have proper outline color transition
process.env.EXPO_PUBLIC_STORYBOOK_ENABLED ? "" : "border-interactive-outlined-pressable",
"hover:border-interactive-outlined-hover",
"focus:border-interactive-outlined-focus",
"focus:outline-1 focus:outline-interactive-outlined-focus focus:outline-offset-0",
"active:border-interactive-outlined-active",
"disabled:bg-disabled-interactive-muted disabled:border-interactive-outlined-disabled disabled:text-form-disabled-text disabled:cursor-not-allowed",
"placeholder:text-form-placeholder"
].join(" "),
variants: {
multiline: {
// Centering the text of a single-line field is per-platform. iOS
// centers the line itself, but only without a line-height —
// `text-base-size-only` is `text-base` minus the 1.4 line-height the
// scale pairs with it, which iOS would turn into leading above the
// glyphs (the value then sits ~3pt low while the placeholder, drawn
// without those attributes, stays centered). Android lays the text out
// from the top of the box — `min-h-[44px]` makes it taller than the
// line — until `align-middle` sets its gravity (RN maps the style
// `verticalAlign` to `textAlignVertical`); on web that would be a real
// `vertical-align` on the `<input>`, hence the platform scope. Web
// keeps the scale: an `<input>` centers its text whatever the
// line-height is.
false: "web:text-base native:text-base-size-only android:align-middle min-h-[44px] rounded-md px-m py-xs",
// Multiline is a paragraph: there the line-height is what spaces the
// lines, and the text belongs at the top of the box.
true: "text-base min-h-[80px] resize-y rounded-xs px-xs py-xs"
},
forceStyle: {
undefined: process.env.EXPO_PUBLIC_STORYBOOK_ENABLED ? "border-interactive-outlined-pressable" : "",
hover: process.env.EXPO_PUBLIC_STORYBOOK_ENABLED ? "border-interactive-outlined-hover" : "",
focus: process.env.EXPO_PUBLIC_STORYBOOK_ENABLED ? "border-interactive-outlined-focus outline-1 outline-interactive-outlined-focus outline-offset-0" : "",
press: process.env.EXPO_PUBLIC_STORYBOOK_ENABLED ? "border-interactive-outlined-active" : ""
}
},
defaultVariants: {
forceStyle: "undefined"
}
},
{ twMerge: false }
);
const MODE_PROPS = {
password: {
secureTextEntry: true,
autoComplete: "current-password"
},
number: {
inputMode: "numeric",
keyboardType: "numeric"
},
tel: {
inputMode: "tel",
autoComplete: "tel",
keyboardType: "phone-pad"
},
email: {
inputMode: "email",
autoComplete: "email",
keyboardType: "email-address"
},
url: {
inputMode: "url",
keyboardType: "url"
},
search: {
inputMode: "search"
},
webSearch: {
inputMode: "search",
keyboardType: "web-search"
}
};
const InputText = forwardRef(
({ className, disabled, mode, multiline, forceStyle, ...props }, ref) => {
const placeholderColor = Platform.OS === "web" ? void 0 : (
// eslint-disable-next-line react-hooks/rules-of-hooks -- native only, web is set via css.
useColorVariable("--color-form-placeholder")
);
const modeProps = mode ? MODE_PROPS[mode] : void 0;
return /* @__PURE__ */ jsx(
TextInput,
{
ref,
editable: !disabled,
disabled,
"aria-disabled": disabled === true,
multiline: multiline === true,
placeholderTextColor: placeholderColor,
className: inputVariants({ multiline, forceStyle, className }),
...modeProps,
...props
}
);
}
);
function useControllableValue({
value: controlledValue,
defaultValue,
onValueChange
}) {
const [internalValue, setInternalValue] = useState(defaultValue);
const value = controlledValue ?? internalValue;
const setValue = useCallback(
(next) => {
if (controlledValue === void 0) {
setInternalValue(next);
}
if (next !== value) {
onValueChange?.(next);
}
},
[controlledValue, onValueChange, value]
);
return [value, setValue];
}
const optionVariants = tv(
{
base: [
"flex-row items-center justify-between gap-xxs rounded-xs px-m py-xs min-h-[44px]",
"active:bg-interactive-soft-active",
// The row's fill is the cursor, and the combobox input keeps the focus:
// an outline here would ring a row the keyboard never lands on. A
// zero-width one, because react-native-css drops `outline-style: none`
// (`outline-none`) and leaves the browser's own ring in place.
"outline-solid outline-0"
].join(" "),
variants: {
cursor: {
// A listbox driving its cursor from JS owns both the pointer and the
// keyboard position, so leaving CSS hover on would light a second row
// while the arrow keys move elsewhere.
rest: "",
highlighted: "bg-interactive-soft-hover",
hover: "hover:bg-interactive-soft-hover focus:bg-interactive-soft-focus"
},
disabled: {
true: "opacity-50",
false: ""
}
},
defaultVariants: { cursor: "hover", disabled: false }
},
{ twMerge: false }
);
function cursorState(highlighted) {
if (highlighted === void 0) return "hover";
return highlighted ? "highlighted" : "rest";
}
const ListboxOption = forwardRef(
({ option, selected, highlighted, ...props }, ref) => {
return /* @__PURE__ */ jsxs(
Pressable,
{
ref,
role: "option",
...props,
"aria-disabled": option.disabled === true,
"aria-selected": selected,
disabled: option.disabled,
className: optionVariants({
cursor: cursorState(highlighted),
disabled: option.disabled
}),
children: [
/* @__PURE__ */ jsx(Text, { numberOfLines: 1, className: "flex-1 text-base text-sharp", children: option.label }),
selected ? /* @__PURE__ */ jsx(Icon, { icon: /* @__PURE__ */ jsx(CheckRegularIcon, {}), size: 18, className: "text-accent" }) : null
]
}
);
}
);
const { useCombobox } = require("downshift/react-native");
function noScrollIntoView() {
}
function defaultFilterOption(option, inputValue) {
return option.label.toLowerCase().includes(inputValue.toLowerCase());
}
function optionToString(option) {
return option ? option.label : "";
}
function useAutocomplete({
options,
value,
defaultValue,
onValueChange,
inputValue,
defaultInputValue,
onInputValueChange,
disabled,
filterOption,
scrollIntoView,
inputInPopover,
"aria-label": ariaLabel,
"aria-labelledby": ariaLabelledby
}) {
const [currentValue, setCurrentValue] = useControllableValue({
value,
defaultValue,
onValueChange
});
const selectedOption = options.find((option) => option.value === currentValue) ?? null;
const [currentInputValue = "", setCurrentInputValue] = useControllableValue({
value: inputValue,
defaultValue: defaultInputValue ?? // `value` as well as `defaultValue`: a caller controlling the selection
// without controlling the text still expects the field to start on the
// selected label rather than empty.
options.find((option) => option.value === (defaultValue ?? value))?.label,
onValueChange: onInputValueChange
});
const selectedLabel = selectedOption?.label;
const visibleOptions = useMemo(() => {
if (currentInputValue === "" || currentInputValue === selectedLabel) {
return options;
}
return options.filter((option) => filterOption(option, currentInputValue));
}, [options, currentInputValue, selectedLabel, filterOption]);
const {
isOpen,
highlightedIndex,
getInputProps,
getMenuProps,
getItemProps,
openMenu,
closeMenu
} = useCombobox({
items: visibleOptions,
itemToString: optionToString,
inputValue: currentInputValue,
selectedItem: selectedOption,
isItemDisabled: (option) => option.disabled === true,
onInputValueChange: ({ inputValue: nextInputValue }) => {
setCurrentInputValue(nextInputValue);
},
onSelectedItemChange: ({ selectedItem }) => {
setCurrentValue(selectedItem ? selectedItem.value : "");
},
...{ scrollIntoView: noScrollIntoView }
});
return {
isOpen,
visibleOptions,
currentValue,
currentInputValue,
highlightedIndex,
inputProps: getInputProps(
{
disabled,
"aria-label": ariaLabel,
// Overrides the id downshift points at by default: this combobox
// renders no label element of its own, so that id would dangle.
"aria-labelledby": ariaLabelledby
},
{ suppressRefError: !isOpen }
),
menuProps: getMenuProps(
{
// Same override as the input: without a label element of our own,
// downshift's default `aria-labelledby` would point at nothing. The
// listbox is left unnamed unless the caller supplies a real label.
"aria-labelledby": ariaLabelledby
},
// The menu only exists while open — it lives in a Popover, which renders
// nothing until then, so downshift has no ref to hold in between.
{ suppressRefError: !isOpen }
),
getItemProps: (params) => getItemProps(params),
openMenu,
closeMenu
};
}
function AutocompleteMenu({
visibleOptions,
currentValue,
highlightedIndex,
menuProps,
getItemProps,
emptyLabel
}) {
const { ref: menuRef, ...restMenuProps } = menuProps;
return /* @__PURE__ */ jsxs(Surface, { variant: "highlight", shadow: "l", size: "sm", className: "p-xs pl-md", children: [
visibleOptions.length === 0 ? /* @__PURE__ */ jsx(Text, { role: "status", className: "px-m py-xs text-base text-muted", children: emptyLabel }) : null,
/* @__PURE__ */ jsx(View, { ref: menuRef, ...restMenuProps, children: /* @__PURE__ */ jsx(
ScrollView,
{
className: "max-h-[240px] pr-xs",
contentContainerClassName: "gap-1",
keyboardShouldPersistTaps: "handled",
children: visibleOptions.map((option, index) => {
const {
ref: itemRef,
onClick: onItemClick,
onPress: onItemPress,
onMouseMove: onItemMouseMove,
...itemProps
} = getItemProps({ item: option, index });
const selected = option.value === currentValue;
return /* @__PURE__ */ jsx(
ListboxOption,
{
ref: itemRef,
...itemProps,
option,
selected,
highlighted: index === highlightedIndex,
onPress: onItemPress ?? onItemClick,
onHoverIn: onItemMouseMove
},
option.value
);
})
}
) })
] });
}
function InputTextAutocomplete({
filterOption = defaultFilterOption,
emptyLabel = "No result",
placeholder,
disabled,
accent,
mode,
className,
testID,
...rest
}) {
const {
isOpen,
currentInputValue,
inputProps,
openMenu,
closeMenu,
...menu
} = useAutocomplete({
...rest,
disabled,
filterOption,
scrollIntoView: false,
inputInPopover: true
});
const { ref: inputRef, onKeyDown, onClick, ...restInputProps } = inputProps;
return /* @__PURE__ */ jsxs(Fragment$1, { children: [
/* @__PURE__ */ jsx(AccentScope, { accent, children: /* @__PURE__ */ jsx(
InputText,
{
readOnly: true,
role: "combobox",
"aria-expanded": isOpen,
"aria-label": rest["aria-label"],
"aria-labelledby": rest["aria-labelledby"],
value: currentInputValue,
mode,
placeholder,
disabled,
testID,
className,
onPressIn: () => {
if (!disabled) openMenu();
}
}
) }),
/* @__PURE__ */ jsx(
Popover,
{
open: isOpen,
placement: "top",
accent: "none",
"aria-label": rest["aria-label"],
onClose: closeMenu,
children: /* @__PURE__ */ jsxs(View, { className: "gap-xs", children: [
/* @__PURE__ */ jsx(
InputText,
{
ref: inputRef,
autoFocus: true,
mode: mode ?? "search",
placeholder,
...restInputProps,
onPressIn: onClick
}
),
/* @__PURE__ */ jsx(AutocompleteMenu, { ...menu, emptyLabel })
] })
}
)
] });
}
const TextArea = forwardRef((props, ref) => {
return /* @__PURE__ */ jsx(InputText, { ref, multiline: true, ...props });
});
function useControllableChecked(controlled, onValueChange) {
const [internal, setInternal] = useState(controlled ?? false);
const value = controlled ?? internal;
const onChange = useCallback(
(next) => {
if (controlled === void 0) {
setInternal(next);
}
if (next !== value) {
onValueChange?.(next);
}
},
[controlled, onValueChange, value]
);
return [value, onChange];
}
function SwitchInner({
checked,
disabled,
onValueChange,
...props
}) {
const [value, setValue] = useControllableChecked(checked, onValueChange);
const trackBg = useColorVariable("--color-lowered");
const thumb = useColorVariable("--color-highlight");
const disabledTrackBg = useColorVariable(
"--color-disabled-interactive-muted"
);
const disabledThumb = useColorVariable("--color-disabled-muted");
const track = disabled ? disabledTrackBg : trackBg;
const thumbColor = disabled ? disabledThumb : thumb;
return /* @__PURE__ */ jsx(
Switch$1,
{
value,
disabled,
ios_backgroundColor: track,
trackColor: { false: track, true: track },
thumbColor,
onValueChange: setValue,
...props
}
);
}
function Switch({ accent, ...rest }) {
return /* @__PURE__ */ jsx(AccentScope, { accent, children: /* @__PURE__ */ jsx(SwitchInner, { ...rest }) });
}
const selectTriggerBaseClassName = [
"flex-row items-center justify-between gap-xs",
"rounded-md border px-m py-xs min-h-[44px]",
"transition-[border-color,outline-color,background-color] duration-fast ease-in"
].join(" ");
const triggerLabelVariants = tv({
base: "flex-1 text-base",
variants: {
// Mirrors InputText: sharp value, form-placeholder, form-disabled-text.
state: {
value: "text-sharp",
placeholder: "text-form-placeholder",
disabled: "text-form-disabled-text"
}
},
defaultVariants: { state: "value" }
});
function SelectTriggerContent({
label,
placeholder,
disabled
}) {
const state = (() => {
if (label === void 0) return "placeholder";
if (disabled) return "disabled";
return "value";
})();
return /* @__PURE__ */ jsxs(Fragment$1, { children: [
/* @__PURE__ */ jsx(Text, { numberOfLines: 1, className: triggerLabelVariants({ state }), children: label ?? placeholder ?? "" }),
/* @__PURE__ */ jsx(
Icon,
{
icon: /* @__PURE__ */ jsx(CaretDownRegularIcon, {}),
size: 18,
className: disabled ? "text-form-disabled-text" : "text-muted"
}
)
] });
}
const triggerVariants = tv(
{
base: selectTriggerBaseClassName,
variants: {
// bg lives in each branch (not the shared base) so the disabled bg never
// competes with bg-highlight at equal specificity.
disabled: {
true: "bg-disabled-interactive-muted border-interactive-outlined-disabled",
false: [
"bg-highlight",
"border-interactive-outlined-pressable",
"hover:border-interactive-outlined-hover",
"focus:border-interactive-outlined-focus",
"active:border-interactive-outlined-active"
].join(" ")
}
},
defaultVariants: { disabled: false }
},
{ twMerge: false }
);
function SelectInner({
options,
value,
defaultValue,
onValueChange,
placeholder,
disabled,
testID,
"aria-label": ariaLabel,
"aria-labelledby": ariaLabelledby
}) {
const [current, setValue] = useControllableValue({
value,
defaultValue,
onValueChange
});
const [open, setOpen] = useState(false);
const { height: windowHeight } = useWindowDimensions();
const selected = options.find((option) => option.value === current);
const onSelect = (next) => {
setValue(next);
setOpen(false);
};
return /* @__PURE__ */ jsxs(Fragment$1, { children: [
/* @__PURE__ */ jsx(
InteractiveBox,
{
withFocusVisibleOutline: true,
role: "combobox",
"aria-expanded": open,
"aria-disabled": disabled === true,
disabled,
testID,
"aria-label": ariaLabel,
"aria-labelledby": ariaLabelledby,
className: triggerVariants({ disabled }),
onPress: () => {
setOpen(true);
},
children: /* @__PURE__ */ jsx(
SelectTriggerContent,
{
label: selected?.label,
placeholder,
disabled
}
)
}
),
/* @__PURE__ */ jsx(
Popover,
{
open,
"aria-label": ariaLabel,
onClose: () => {
setOpen(false);
},
children: /* @__PURE__ */ jsx(Surface, { variant: "highlight", shadow: "l", size: "sm", className: "py-xs", children: /* @__PURE__ */ jsx(
ScrollView,
{
contentContainerClassName: "gap-1",
style: { maxHeight: windowHeight * 0.7 },
children: options.map((option) => /* @__PURE__ */ jsx(
ListboxOption,
{
option,
selected: option.value === current,
onPress: () => {
onSelect(option.value);
}
},
option.value
))
}
) })
}
)
] });
}
function Select({ accent, ...rest }) {
return /* @__PURE__ */ jsx(AccentScope, { accent, children: /* @__PURE__ */ jsx(SelectInner, { ...rest }) });
}
function createSelectionContext(missingProviderMessage) {
const Context = createContext(void 0);
return {
SelectionContextProvider: Context.Provider,
useSelection: () => {
const context = useContext(Context);
if (!context) {
throw new Error(missingProviderMessage);
}
return context;
}
};
}
function useSelectionValue({
value: controlledValue,
defaultValue,
onValueChange,
disabled,
compact,
orientation,
stretch,
variant
}) {
const [value, onSelect] = useControllableValue({
value: controlledValue,
defaultValue,
onValueChange
});
return useMemo(
() => ({
value,
onSelect,
disabled,
compact,
orientation,
stretch,
variant
}),
[value, onSelect, disabled, compact, orientation, stretch, variant]
);
}
const {
SelectionContextProvider: RadioContextProvider,
useSelection: useRadioContext
} = createSelectionContext(
"Radio, RadioButton and RadioCard must be rendered inside a RadioGroup, RadioButtonGroup or RadioCardGroup."
);
function RadioGroup({
value,
defaultValue,
onValueChange,
accent,
disabled,
children,
...props
}) {
const context = useSelectionValue({
value,
defaultValue,
onValueChange,
disabled
});
return /* @__PURE__ */ jsx(AccentScope, { accent, children: /* @__PURE__ */ jsx(RadioContextProvider, { value: context, children: /* @__PURE__ */ jsx(View, { role: "radiogroup", ...props, children }) }) });
}
function DefaultAccentScope({
children
}) {
const currentTheme = useCurrentTheme();
const currentMode = useCurrentMode();
return /* @__PURE__ */ jsx(
ScopedTheme,
{
theme: currentTheme === currentMode ? `${currentTheme}_brand` : currentTheme,
children
}
);
}
const radioIndicatorVariants = tv({
slots: {
ring: "size-[22px] rounded-full border-2 items-center justify-center transition-[border-color] duration-fast ease-in",
dot: "size-[10px] rounded-full bg-accent transition-transform duration-fast ease-in"
},
variants: {
selected: {
true: { ring: "border-accent", dot: "scale-100" },
false: {
ring: "border-interactive-outlined-pressable group-hover:border-interactive-outlined-hover group-active:border-interactive-outlined-active",
dot: "scale-0"
}
},
onAccent: {
true: { ring: "border-on-accent", dot: "bg-on-accent" },
false: {}
},
disabled: {
true: {
ring: "border-interactive-outlined-disabled",
dot: "bg-disabled-muted"
},
false: {}
}
},
// On the disabled contained fill, `interactive-outlined-disabled` is the same
// color as the background — the ring needs the foreground disabled token the
// label next to it already uses.
compoundVariants: [
{
disabled: true,
onAccent: true,
class: { ring: "border-disabled-sharp", dot: "bg-disabled-sharp" }
}
]
});
function RadioIndicator({
selected,
disabled,
onAccent
}) {
const styles = radioIndicatorVariants({ selected, disabled, onAccent });
return /* @__PURE__ */ jsx(DefaultAccentScope, { children: /* @__PURE__ */ jsx(View, { className: styles.ring(), children: /* @__PURE__ */ jsx(View, { className: styles.dot() }) }) });
}
const labelVariants = tv({
base: "text-base",
variants: {
disabled: {
true: "text-disabled-sharp",
false: "text-sharp"
}
}
});
function Radio({ value, label, disabled }) {
const {
value: selectedValue,
onSelect,
disabled: groupDisabled
} = useRadioContext();
const selected = selectedValue === value;
const isDisabled = disabled === true || groupDisabled === true;
return /* @__PURE__ */ jsxs(
InteractiveBox,
{
withFocusVisibleOutline: true,
role: "radio",
"aria-checked": selected,
"aria-disabled": isDisabled,
"aria-label": label,
disabled: isDisabled,
className: "group flex-row items-center gap-xs self-start rounded-xs px-xs min-h-11 focus-visible:outline-interactive-outlined-outline-focus",
onPress: () => {
onSelect(value);
},
children: [
/* @__PURE__ */ jsx(RadioIndicator, { selected, disabled: isDisabled }),
/* @__PURE__ */ jsx(Text, { className: labelVariants({ disabled: isDisabled }), children: label })
]
}
);
}
const segmentedBarVariants = tv({
base: "items-stretch px-xs py-0",
variants: {
orientation: {
horizontal: "flex-row min-h-[44px]",
vertical: "flex-col py-xs"
},
stretch: {
true: "self-stretch",
false: "self-start"
},
// A bar of square icon chips is a stadium at the 44px height, so the track
// takes the same radius as the chips it holds. It drops its gap and its
// horizontal padding too: the chip is already inset inside its own 44px tap
// target, so keeping either would add to that slack and leave the icons
// floating far apart.
variant: {
segmented: "gap-xxs",
icon: "rounded-md gap-0"
}
},
defaultVariants: {
orientation: "horizontal",
stretch: false,
variant: "segmented"
}
});
function SegmentedBar({
orientation,
stretch,
variant,
className,
...props
}) {
return /* @__PURE__ */ jsx(
Surface,
{
variant: "lowered",
size: "sm",
className: segmentedBarVariants({
orientation,
stretch,
variant,
className
}),
...props
}
);
}
function RadioButtonGroup({
value,
defaultValue,
onValueChange,
accent,
disabled,
variant,
compact,
children,
...props
}) {
const context = useSelectionValue({
value,
defaultValue,
onValueChange,
disabled,
compact,
variant
});
return /* @__PURE__ */ jsx(RadioContextProvider, { value: context, children: /* @__PURE__ */ jsx(
SegmentedBar,
{
role: "radiogroup",
variant,
accent,
...props,
children
}
) });
}
const segmentedItemVariants = tv({
slots: {
pressable: "group flex-center min-h-[44px] rounded-xs",
segment: "relative flex-row flex-center gap-xxs min-h-[32px] rounded-xs border border-transparent transition-[border-color] duration-fast ease-in group-focus-visible:outline-2 group-focus-visible:outline-offset-2 group-focus-visible:outline-interactive-outlined-outline-focus",
chip: "absolute inset-0 rounded-xs transition-opacity duration-fast ease-in",
foreground: "z-1 transition-[color] duration-fast ease-in",
label: "select-none font-body-bold text-base text-center",
// indicator — a badge over the glyph's top-right, not beside it: the icon
// chip is a circle and the lunes its 20px glyph leaves in the corners are
// ~4px wide, too narrow to hold anything. Its halo is the chip's own fill,
// so the badge punches out of the glyph it overlaps, and these insets keep
// its painted circle inside the chip in both sizes the icon variant takes
// (36px in a row, 40px stacked or stretched): 1.1px and 0.3px of clearance.
indicator: "absolute right-[4px] top-[4px] z-1 flex-center size-[14px] rounded-full transition-[background-color] duration-fast ease-in"
},
variants: {
selected: {
true: {
chip: "opacity-100",
foreground: "text-on-accent",
indicator: "bg-interactive-contained-pressable"
},
false: {
chip: "opacity-0",
foreground: "text-muted group-hover:text-sharp",
// The chip is transparent here, so the halo takes what shows through it:
// the lowered SegmentedBar behind.
indicator: "bg-lowered"
}
},
disabled: {
true: {
chip: "bg-interactive-contained-disabled",
foreground: "text-disabled-muted group-hover:text-disabled-muted",
indicator: "bg-interactive-contained-disabled"
},
false: { chip: "bg-interactive-contained-pressable shadow-s" }
},
compact: { true: { segment: "px-xs" }, false: { segment: "px-m" } },
orientation: {
horizontal: {},
// A stacked item spans the bar's width, so the chip stretches with it
// instead of shrinking to its own label, and stands taller: a rail reads
// as rows, not as chips floating in a column.
vertical: {
pressable: "items-stretch",
segment: "self-stretch min-h-[40px]"
}
},
// A stretched bar hands its extra width to its items; a stacked one already
// spans that width, so only a row shares it.
stretch: { true: {}, false: {} },
// Declared last so its radius and padding land after the ones `compact` and
// `orientation` set, and win the merge.
variant: {
segmented: {},
// The chip is a 40px square with no label, so the pressable carries the
// tap target's width the way it already carries its height — the chip
// alone is 8px short of the 44px minimum. 40 and not 32: the 4px of slack
// that leaves on every side is exactly the focus ring (2px offset + 2px
// width), and it is the whole frame around the chip, the bar having
// dropped its own horizontal padding.
icon: {
pressable: "min-w-[44px]",
segment: "rounded-md self-center w-[36px] min-h-[36px] px-0",
chip: "rounded-md"
}
}
},
defaultVariants: {
compact: false,
orientation: "horizontal",
stretch: false,
variant: "segmented"
},
compoundVariants: [
{
stretch: true,
orientation: "horizontal",
// `grow`, not `flex-1`: a zero basis would make every item an equal share
// of the bar and truncate the longer labels the moment the bar is only as
// wide as its content. Growing from the natural width instead leaves the
// labels intact and only shares the space a stretched bar has to spare —
// with the chip stretching too, so the row reads as adjacent segments
// instead of labels floating in their own space.
class: {
pressable: "grow items-stretch",
segment: "self-stretch"
}
},
{
// A square chip stays square whatever width the item is given, so a
// stretched or stacked icon bar centers it instead of stretching it.
variant: "icon",
stretch: true,
class: { segment: "self-center w-[40px]" }
},
{
variant: "icon",
orientation: "vertical",
class: { segment: "self-center w-[40px] min-h-[40px]" }
},
{
selected: false,
disabled: false,
class: {
segment: "group-hover:border-interactive-outlined-hover group-active:border-interactive-outlined-active"
}
},
{
selected: true,
disabled: true,
class: {
foreground: "text-disabled-sharp group-hover:text-disabled-sharp"
}
},
{
// `disabled` is declared after `selected`, so it would hand the halo the
// disabled chip's fill on an item that has no chip showing at all.
selected: false,
disabled: true,
class: { indicator: "bg-lowered" }
}
]
});
function SegmentedItem({
label,
icon,
activeIcon,
activeAccent,
indicator,
selected,
disabled,
compact,
orientation,
stretch,
variant,
...props
}) {
const styles = segmentedItemVariants({
selected,
disabled: disabled === true,
compact,
orientation,
stretch,
variant
});
return /* @__PURE__ */ jsx(
InteractiveBox,
{
"aria-label": label,
withFocusVisibleOutline: false,
disabled,
className: styles.pressable(),
...props,
children: /* @__PURE__ */ jsxs(View, { className: styles.segment(), children: [
/* @__PURE__ */ jsx(View, { className: styles.chip() }),
icon ? /* @__PURE__ */ jsx(
InteractiveIcon,
{
icon,
activeIcon,
activeAccent,
active: selected,
disabled: disabled === true,
size: 20,
className: styles.foreground()
}
) : null,
variant === "icon" ? null : /* @__PURE__ */ jsx(
Text,
{
numberOfLines: 1,
className: styles.label({ class: styles.foreground() }),
children: label
}
),
variant === "icon" && indicator ? /* @__PURE__ */ jsx(View, { className: styles.indicator(), children: /* @__PURE__ */ jsx(Icon, { icon: indicator, size: 10, className: styles.foreground() }) }) : null
] })
}
);
}
function RadioButton({
value,
label,
icon,
activeIcon,
activeAccent,
indicator,
disabled,
onPress
}) {
const {
value: selectedValue,
onSelect,
disabled: groupDisabled,
compact,
variant
} = useRadioContext();
const selected = selectedValue === value;
const isDisabled = disabled === true || groupDisabled === true;
return /* @__PURE__ */ jsx(
SegmentedItem,
{
role: "radio",
"aria-checked": selected,
"aria-disabled": isDisabled,
label,
icon,
activeIcon,
activeAccent,
indicator,
selected,
disabled: isDisabled,
compact,
variant,
onPress: onPress ?? (() => {
onSelect(value);
})
}
);
}
function ColorModeOption({
mode,
label,
indicator,
onPress
}) {
if (mode === "dark") {
return /* @__PURE__ */ jsx(
RadioButton,
{
value: "dark",
label,
icon: /* @__PURE__ */ jsx(MoonRegularIcon, {}),
activeIcon: /* @__PURE__ */ jsx(MoonDuotoneIcon, {}),
indicator,
onPress
}
);
}
return /* @__PURE__ */ jsx(
RadioButton,
{
value: "light",
label,
icon: /* @__PURE__ */ jsx(SunRegularIcon, {}),
activeIcon: /* @__PURE__ */ jsx(SunDuotoneIcon, {}),
indicator,
onPress
}
);
}
function SystemColorModeOption({
value,
label,
onPress
}) {
return /* @__PURE__ */ jsx(
RadioButton,
{
value,
label,
icon: /* @__PURE__ */ jsx(DesktopRegularIcon, {}),
activeIcon: /* @__PURE__ */ jsx(DesktopDuotoneIcon, {}),
onPress
}
);
}
function ColorModeLockOption({
mode,
label,
followsSystem,
followingSystemLabel,
onPress
}) {
return /* @__PURE__ */ jsx(
ColorModeOption,
{
mode,
label: followsSystem ? followingSystemLabel(label) : label,
indicator: followsSystem ? /* @__PURE__ */ jsx(DesktopRegularIcon, {}) : void 0,
onPress
}
);
}
function nextLockPreference({
mode,
preference,
resolvedMode,
systemMode
}) {
if (mode !== resolvedMode) return mode === systemMode ? "system" : mode;
if (mode !== systemMode) return mode;
return preference === "system" ? mode : "system";
}
function ColorModePicker({
value,
defaultValue = "system",
onValueChange,
variant = "system-lock",
accent,
disabled,
"aria-label": ariaLabel = "Color mode",
lightLabel = "Light",
darkLabel = "Dark",
systemLabel = "System",
followingSystemLabel = (modeLabel) => `${modeLabel} (system)`
}) {
const [preference = "system", setPreference] = useControllableValue({
value,
defaultValue,
onValueChange
});
const systemMode = useSystemColorMode();
if (variant === "system-lock") {
const resolvedMode = preference === "system" ? systemMode : preference;
const pressLock = (mode) => () => {
setPreference(
nextLockPreference({ mode, preference, resolvedMode, systemMode })
);
};
return (
// The group tracks the mode in effect, not the preference: `system` has no
// chip of its own here, it shows as the chip it resolves to.
/* @__PURE__ */ jsxs(
RadioButtonGroup,
{
variant: "icon",
"aria-label": ariaLabel,
accent,
disabled,
value: resolvedMode,
children: [
/* @__PURE__ */ jsx(
ColorModeLockOption,
{
mode: "light",
label: lightLabel,
followsSystem: preference === "system" && systemMode === "light",
followingSystemLabel,
onPress: pressLock("light")
}
),
/* @__PURE__ */ jsx(
ColorModeLockOption,
{
mode: "dark",
label: darkLabel,
followsSystem: preference === "system" && systemMode === "dark",
followingSystemLabel,
onPress: pressLock("dark")
}
)
]
}
)
);
}
return /* @__PURE__ */ jsxs(
RadioButtonGroup,
{
variant: "icon",
"aria-label": ariaLabel,
accent,
disabled,
value: preference,
children: [
/* @__PURE__ */ jsx(
ColorModeOption,
{
mode: "light",
label: lightLabel,
onPress: () => {
setPreference("light");
}
}
),
/* @__PURE__ */ jsx(
ColorModeOption,
{
mode: "dark",
label: darkLabel,
onPress: () => {
setPreference("dark");
}
}
),
/* @__PURE__ */ jsx(
SystemColorModeOption,
{
value: "system",
label: systemLabel,
onPress: () => {
setPreference("system");
}
}
)
]
}
);
}
const radioCardGroupVariants = tv({
base: "gap-xs",
variants: {
variant: {
list: "flex-col",
stack: "flex-row flex-wrap"
}
},
defaultVariants: { variant: "list" }
});
const RadioCardGroupVariantContext = createContext("list");
function useRadioCardGroupVariant() {
return useContext(RadioCardGroupVariantContext);
}
function RadioCardGroup({
value,
defaultValue,
onValueChange,
accent,
disabled,
variant,
className,
children,
...props
}) {
const context = useSelectionValue({
value,
defaultValue,
onValueChange,
disabled
});
return /* @__PURE__ */ jsx(AccentScope, { accent, children: /* @__PURE__ */ jsx(RadioContextProvider, { value: context, children: /* @__PURE__ */ jsx(RadioCardGroupVariantContext, { value: variant ?? "list", children: /* @__PURE__ */ jsx(
View,
{
role: "radiogroup",
className: radioCardGroupVariants({ variant, className }),
...props,
children
}
) }) }) });
}
const radioCardVariants = tv(
{
slots: {
frame: "flex-row gap-m rounded-sm p-m min-h-[44px]",
icon: "",
label: "font-body-bold text-base",
description: "text-sm"
},
variants: {
// In a wrapping group the cards share each row instead of sizing to text,
// and the icon and the indicator hold the card's top corners.
layout: {
list: { frame: "items-center" },
stack: { frame: "items-start grow shrink basis-[240px]" }
},
selected: {
true: {
icon: "text-on-accent",
label: "text-on-accent",
description: "text-on-accent-muted"
},
false: {
icon: "text-muted",
label: "text-sharp",
description: "text-muted"
}
},
disabled: {
true: {
icon: "text-disabled-muted",
label: "text-disabled-sharp",
description: "text-disabled-muted"
},
false: {}
}
}
},
{ twMerge: false }
);
function RadioCard({
value,
label,
description,
icon,
disabled,
className
}) {
const {
value: selectedValue,
onSelect,
disabled: groupDisabled
} = useRadioContext();
const layout = useRadioCardGroupVariant();
const selected = selectedValue === value;
const isDisabled = disabled === true || groupDisabled === true;
const styles = radioCardVariants({ layout, selected, disabled: isDisabled });
return /* @__PURE__ */ jsx(DefaultAccentScope, { children: /* @__PURE__ */ jsxs(
PressableBox,
{
variant: selected ? "contained" : "outlined",
role: "radio",
"aria-checked": selected,
"aria-disabled": isDisabled,
"aria-label": label,
disabled: isDisabled,
className: styles.frame({ className }),
onPress: () => {
onSelect(value);
},
children: [
icon ? /* @__PURE__ */ jsx(Icon, { icon, size: 24, className: styles.icon() }) : null,
/* @__PURE__ */ jsxs(VStack, { className: "flex-1 gap-xxs", children: [
/* @__PURE__ */ jsx(Text, { className: styles.label(), children: label }),
description ? /* @__PURE__ */ jsx(Text, { className: styles.description(), children: description }) : null
] }),
/* @__PURE__ */ jsx(
RadioIndicator,
{
selected,
disabled: isDisabled,
onAccent: selected
}
)
]
}
) });
}
const {
SelectionContextProvider: NavBarContextProvider,
useSelection: useNavBarContext
} = createSelectionContext("NavBarItem must be rendered inside a NavBar.");
function NavBar({
value,
defaultValue,
onValueChange,
accent,
disabled,
orientation,
stretch,
variant,
children,
...props
}) {
const context = useSelectionValue({
value,
defaultValue,
onValueChange,
disabled,
orientation,
stretch,
variant
});
return /* @__PURE__ */ jsx(NavBarContextProvider, { value: context, children: /* @__PURE__ */ jsx(
SegmentedBar,
{
role: "navigation",
orientation,
stretch,
variant,
accent,
...props,
children
}
) });
}
function NavBarItem({
href,
label,
icon,
activeIcon,
activeAccent,
disabled,
onPress
}) {
const {
value: currentValue,
onSelect,
disabled: navBarDisabled,
orientation,
stretch,
variant
} = useNavBarContext();
const selected = href !== void 0 && currentValue === href;
const isDisabled = disabled === true || navBarDisabled === true;
const selectHref = href === void 0 ? void 0 : (event) => {
event.preventDefault();
onSelect(href);
};
return /* @__PURE__ */ jsx(
SegmentedItem,
{
role: "link",
href: isDisabled ? void 0 : href,
"aria-current": selected ? "page" : void 0,
"aria-disabled": isDisabled,
label,
icon,
activeIcon,
activeAccent,
selected,
disabled: isDisabled,
orientation,
stretch,
variant,
onPress: onPress ?? selectHref
}
);
}
const BreadcrumbItemContext = createContext(void 0);
const BreadcrumbItemContextProvider = BreadcrumbItemContext.Provider;
function useBreadcrumbItemContext() {
const context = useContext(BreadcrumbItemContext);
if (!context) {
throw new Error("BreadcrumbItem must be rendered inside Breadcrumbs.");
}
return context;
}
const breadcrumbsVariants = tv({
slots: {
frame: "flex-row flex-wrap items-center",
separator: "text-muted"
}
});
function BreadcrumbSlot({
current,
disabled,
onNavigate,
children
}) {
const context = useMemo(
() => ({ current, disabled, onNavigate }),
[current, disabled, onNavigate]
);
return /* @__PURE__ */ jsx(BreadcrumbItemContextProvider, { value: context, children });
}
function Breadcrumbs({
"aria-label": ariaLabel = "Breadcrumb",
separator = /* @__PURE__ */ jsx(CaretRightRegularIcon, {}),
onNavigate,
accent,
disabled = false,
children,
className
}) {
const styles = breadcrumbsVariants();
const lastIndex = Children.count(children) - 1;
return /* @__PURE__ */ jsx(
Box,
{
role: "navigation",
"aria-label": ariaLabel,
accent,
className: styles.frame({ className }),
children: Children.map(children, (child, index) => /* @__PURE__ */ jsxs(
BreadcrumbSlot,
{
current: index === lastIndex,
disabled,
onNavigate,
children: [
index > 0 ? /* @__PURE__ */ jsx(Icon, { icon: separator, size: 16, className: styles.separator() }) : null,
child
]
}
))
}
);
}
const breadcrumbItemVariants = tv({
slots: {
// LinkText is already the row; it only needs the tap target around it.
link: "min-h-[44px] px-xxs",
page: "flex-row items-center gap-xxs min-h-[44px] px-xxs",
icon: "text-sharp",
label: "select-none font-body-bold text-base text-sharp"
}
});
function CurrentCrumbText(props) {
return /* @__PURE__ */ jsx(Text, { ...props });
}
function BreadcrumbItem({
href,
label,
icon,
disabled,
onPress
}) {
const {
current,
disabled: breadcrumbsDisabled,
onNavigate
} = useBreadcrumbItemContext();
const isDisabled = disabled === true || breadcrumbsDisabled;
const styles = breadcrumbItemVariants();
if (current) {
return /* @__PURE__ */ jsxs(View, { className: styles.page(), children: [
icon ? /* @__PURE__ */ jsx(
Icon,
{
icon,
size: linkTextIconSize("md"),
className: styles.icon()
}
) : null,
/* @__PURE__ */ jsx(CurrentCrumbText, { "aria-current": "page", className: styles.label(), children: label })
] });
}
const navigate = onNavigate === void 0 || href === void 0 ? void 0 : (event) => {
event.preventDefault();
onNavigate(href);
};
return /* @__PURE__ */ jsx(
LinkText,
{
href,
text: label,
icon,
"aria-label": label,
disabled: isDisabled,
className: styles.link(),
onPress: onPress ?? navigate
}
);
}
const {
SelectionContextProvider: TabsContextProvider,
useSelection: useTabsContext
} = createSelectionContext("Tab must be rendered inside Tabs.");
function Tabs({
value,
defaultValue,
onValueChange,
accent,
disabled,
variant,
children,
...props
}) {
const context = useSelectionValue({
value,
defaultValue,
onValueChange,
disabled,
variant
});
return /* @__PURE__ */ jsx(TabsContextProvider, { value: context, children: /* @__PURE__ */ jsx(SegmentedBar, { role: "tablist", variant, accent, ...props, children }) });
}
function Tab({
value,
label,
icon,
activeIcon,
activeAccent,
disabled,
onPress,
...props
}) {
const {
value: currentValue,
onSelect,
disabled: tabsDisabled,
variant
} = useTabsContext();
const selected = currentValue === value;
const isDisabled = disabled === true || tabsDisabled === true;
return /* @__PURE__ */ jsx(
SegmentedItem,
{
role: "tab",
"aria-selected": selected,
"aria-disabled": isDisabled,
label,
icon,
activeIcon,
activeAccent,
selected,
disabled: isDisabled,
variant,
onPress: onPress ?? (() => {
onSelect(value);
}),
...props
}
);
}
function FormItem({
label,
details,
error,
isRequiredError,
required,
indented,
onLabelPress,
render
}) {
const labelId = useId();
const hasError = Boolean(error);
const showWarningIcon = hasError && !isRequiredError;
const marker = (() => {
if (required === true) {
return /* @__PURE__ */ jsxs(HStack, { className: "gap-xxs items-center", children: [
/* @__PURE__ */ jsx(
Icon,
{
icon: /* @__PURE__ */ jsx(AsteriskSimpleRegularIcon, {}),
size: 12,
className: "text-accent"
}
),
showWarningIcon ? /* @__PURE__ */ jsx(
Icon,
{
icon: /* @__PURE__ */ jsx(WarningRegularIcon, {}),
size: 16,
className: "text-accent"
}
) : null
] });
}
if (required) {
return required;
}
if (showWarningIcon) {
return /* @__PURE__ */ jsx(Icon, { icon: /* @__PURE__ */ jsx(WarningRegularIcon, {}), size: 16, className: "text-accent" });
}
return null;
})();
return /* @__PURE__ */ jsxs(VStack, { className: "gap-xxs", children: [
/* @__PURE__ */ jsx(Pressable, { onPress: onLabelPress, children: /* @__PURE__ */ jsxs(VStack, { children: [
/* @__PURE__ */ jsxs(HStack, { className: "gap-xxs items-center", children: [
/* @__PURE__ */ jsx(
Text,
{
nativeID: labelId,
accent: hasError ? "danger" : void 0,
className: `font-body-bold text-md ${hasError ? "text-accent" : ""}`,
children: label
}
),
marker ? /* @__PURE__ */ jsx(View, { "aria-hidden": true, children: hasError ? /* @__PURE__ */ jsx(AccentScope, { accent: "danger", children: marker }) : marker }) : null
] }),
details ? /* @__PURE__ */ jsx(Text, { className: "text-muted text-sm", children: details }) : null
] }) }),
indented ? /* @__PURE__ */ jsx(View, { className: "border-l border-border-muted pl-m", children: render(labelId) }) : render(labelId),
error ? /* @__PURE__ */ jsx(View, { className: "px-m", children: /* @__PURE__ */ jsx(Text, { role: "alert", accent: "danger", className: "text-accent text-sm", children: error }) }) : null
] });
}
class FormValidationError extends Error {
constructor() {
super("Form validation failed.");
this.name = "FormValidationError";
}
}
function Form({
defaultValues,
mode = "onTouched",
onSubmit,
onSubmitError,
render
}) {
const form = useForm({ mode, defaultValues });
function submit() {
let valid = true;
const result = form.handleSubmit(onSubmit, () => {
valid = false;
})().then(() => {
if (!valid) throw new FormValidationError();
});
if (onSubmitError) result.catch(onSubmitError);
return result;
}
return /* @__PURE__ */ jsx(FormProvider, { ...form, children: render({ control: form.control, submit }) });
}
function FormField({
control,
name,
label,
required,
validate,
renderError,
render
}) {
const { setFocus } = useFormContext();
return /* @__PURE__ */ jsx(
Controller,
{
control,
name,
rules: { required: Boolean(required), validate },
render: ({ field, fieldState }) => {
const requiredError = fieldState.error?.type === "required" && required !== true ? required : void 0;
return /* @__PURE__ */ jsx(
FormItem,
{
label,
required: Boolean(required),
isRequiredError: fieldState.error?.type === "required",
error: renderError ? renderError(fieldState.error) : requiredError ?? fieldState.error?.message,
render: (labelId) => render({ field, labelId }),
onLabelPress: () => {
setFocus(name);
}
}
);
}
}
);
}
function FormFieldArrayItem({
control,
name,
itemLabel,
removeLabel,
index,
removable,
onRemove,
render
}) {
const [pendingRemoval, setPendingRemoval] = useState(false);
return /* @__PURE__ */ jsx(StableAccentScope, { accent: pendingRemoval ? "danger" : void 0, children: /* @__PURE__ */ jsxs(HStack, { className: "gap-sm items-center p-xxs", children: [
/* @__PURE__ */ jsx(View, { className: "grow shrink basis-0", children: render({ control, name, index, label: itemLabel }) }),
removable ? /* @__PURE__ */ jsx(
IconButton,
{
variant: "ghost",
icon: /* @__PURE__ */ jsx(TrashRegularIcon, {}),
"aria-label": removeLabel,
onHoverIn: () => {
setPendingRemoval(true);
},
onHoverOut: () => {
setPendingRemoval(false);
},
onPress: onRemove
}
) : null
] }) });
}
function FormFieldArray({
control,
name,
label,
details,
emptyValue,
minSize = 0,
addLabel = "Add item",
disableAdd,
removeLabel = (itemLabel) => `Remove ${itemLabel}`,
render
}) {
const { fields, append, remove } = useFieldArray({
control,
name
});
const appendedItem = emptyValue;
const paddedRef = useRef(false);
useEffect(() => {
if (paddedRef.current) return;
paddedRef.current = true;
const shortfall = minSize - fields.length;
if (shortfall > 0) {
append(
Array.from({ length: shortfall }, () => appendedItem),
{ shouldFocus: false }
);
}
}, [append, appendedItem, fields.length, minSize]);
return /* @__PURE__ */ jsx(
FormItem,
{
label,
details,
render: () => /* @__PURE__ */ jsxs(VStack, { className: "gap-xs", children: [
fields.map((field, index) => /* @__PURE__ */ jsx(
FormFieldArrayItem,
{
control,
name: `${name}.${index}`,
itemLabel: `${label} ${index + 1}`,
removeLabel: removeLabel(`${label} ${index + 1}`),
index,
removable: index >= minSize,
render,
onRemove: () => {
remove(index);
}
},
field.id
)),
/* @__PURE__ */ jsx(
Button,
{
size: "sm",
variant: "outlined",
icon: /* @__PURE__ */ jsx(PlusRegularIcon, {}),
text: addLabel,
className: "self-start",
disabled: disableAdd,
onPress: () => {
append(appendedItem);
}
}
)
] })
}
);
}
function FormSubmitButton({
label,
onPress,
errorToMessage
}) {
return /* @__PURE__ */ jsx(
ActionButton,
{
text: label,
errorToMessage,
onPress
}
);
}
function SimpleVForm({
submitLabel,
submitErrorToMessage,
className,
render,
...formProps
}) {
return /* @__PURE__ */ jsx(
Form,
{
...formProps,
render: ({ control, submit }) => /* @__PURE__ */ jsxs(VStack, { className: className ?? "gap-l", children: [
render({ control, submit }),
/* @__PURE__ */ jsx(
FormSubmitButton,
{
label: submitLabel,
errorToMessage: submitErrorToMessage,
onPress: submit
}
)
] })
}
);
}
function EditableItem({
label,
summary,
details,
editAriaLabel,
editIcon = /* @__PURE__ */ jsx(PencilSimpleRegularIcon, {}),
variant,
accent,
disabled,
onEdit,
children
}) {
return /* @__PURE__ */ jsxs(VStack, { className: "gap-xs", children: [
/* @__PURE__ */ jsxs(HStack, { className: "items-center justify-between gap-sm", children: [
/* @__PURE__ */ jsxs(VStack, { className: "shrink", children: [
/* @__PURE__ */ jsxs(HStack, { className: "items-center gap-sm", children: [
/* @__PURE__ */ jsx(Text, { className: "font-body-bold text-md", children: label }),
summary
] }),
details ? /* @__PURE__ */ jsx(Text, { className: "text-muted text-sm", children: details }) : null
] }),
/* @__PURE__ */ jsx(
IconButton,
{
size: "sm",
icon: editIcon,
variant,
accent,
disabled,
"aria-label": editAriaLabel,
onPress: onEdit
}
)
] }),
children
] });
}
function useFormEditorModal({
title,
size,
accent,
closeButtonAriaLabel,
cancelLabel,
submitLabel,
submitErrorToMessage,
defaultValues,
mode,
onSubmit,
render
}) {
const [editing, setEditing] = useState(false);
function open() {
setEditing(true);
}
function close() {
setEditing(false);
}
const handleSubmit = async (values, event) => {
await onSubmit(values, event);
setEditing(false);
};
return {
open,
editor: editing ? /* @__PURE__ */ jsx(
Form,
{
defaultValues,
mode,
render: ({ control, submit }) => /* @__PURE__ */ jsx(
Modal,
{
visible: true,
title,
accent,
size,
closeButtonAriaLabel,
footer: /* @__PURE__ */ jsxs(Fragment$1, { children: [
/* @__PURE__ */ jsx(Button, { variant: "outlined", text: cancelLabel, onPress: close }),
/* @__PURE__ */ jsx(
FormSubmitButton,
{
label: submitLabel,
errorToMessage: submitErrorToMessage,
onPress: submit
}
)
] }),
onClose: close,
children: render({ control })
}
),
onSubmit: handleSubmit
}
) : null
};
}
function FormEditableItem({
label,
summary,
details,
editAriaLabel,
editIcon,
variant,
accent,
disabled,
title,
...editorProps
}) {
const { open, editor } = useFormEditorModal({
...editorProps,
title: title ?? label,
accent
});
return /* @__PURE__ */ jsx(
EditableItem,
{
label,
summary,
details,
editAriaLabel,
editIcon,
variant,
accent,
disabled,
onEdit: open,
children: editor
}
);
}
function FormEditableSurface({
title,
titleBadge,
details,
editAriaLabel,
editIcon,
editIconVariant,
accent,
className,
shadow,
size,
variant,
disabled,
modalSize,
modalTitle,
children,
...editorProps
}) {
const { open, editor } = useFormEditorModal({
...editorProps,
title: modalTitle ?? title,
size: modalSize,
accent
});
return /* @__PURE__ */ jsxs(
EditableSurface,
{
title,
titleBadge,
details,
editAriaLabel,
editIcon,
editIconVariant,
accent,
className,
shadow,
size,
variant,
disabled,
onEdit: open,
children: [
children,
editor
]
}
);
}
const avatarVariants = tv({
slots: {
frame: "flex-center shrink-0 rounded-full bg-enabled",
label: "font-body-bold text-on-accent"
},
variants: {
size: {
sm: { frame: "size-[28px]", label: "text-xs" },
md: { frame: "size-[32px]", label: "text-sm" },
lg: { frame: "size-[40px]", label: "text-base" }
}
},
defaultVariants: { size: "md" }
});
const avatarIconSize = { sm: 16, md: 18, lg: 22 };
function initialsFromName(name) {
return name.trim().split(/\s+/).slice(0, 2).map((part) => part.slice(0, 1)).join("").toUpperCase();
}
function Avatar({
name,
icon,
accent = "brand",
size,
className
}) {
const styles = avatarVariants({ size });
return /* @__PURE__ */ jsx(Box, { accent, className: styles.frame({ className }), children: icon ? /* @__PURE__ */ jsx(
Icon,
{
icon,
size: avatarIconSize[size ?? "md"],
className: "text-on-accent"
}
) : /* @__PURE__ */ jsx(Text, { className: styles.label(), children: name === void 0 ? "" : initialsFromName(name) }) });
}
const badgeVariants = tv(
{
slots: {
frame: "flex-row items-center self-start rounded-full",
text: "font-body-bold",
icon: ""
},
variants: {
size: {
sm: { frame: "gap-xxs px-xs py-xxs", text: "text-xs", icon: "" },
md: { frame: "gap-xs px-sm py-xxs", text: "text-sm", icon: "" }
},
variant: {
solid: {
frame: "bg-highlight-accent",
text: "text-sharp",
icon: "text-sharp"
},
"solid.enabled": {
frame: "bg-enabled",
text: "text-on-accent",
icon: "text-on-accent"
},
outlined: {
frame: "border border-accent",
text: "text-accent",
icon: "text-accent"
}
}
},
defaultVariants: { size: "md", variant: "solid" }
},
{ twMerge: false }
);
const ICON_SIZE = { sm: 12, md: 16 };
function Badge({
accent = "brand",
size = "md",
variant = "solid",
icon,
children
}) {
const styles = badgeVariants({ size, variant });
return /* @__PURE__ */ jsx(AccentScope, { accent, children: /* @__PURE__ */ jsxs(Box, { className: styles.frame(), children: [
icon ? /* @__PURE__ */ jsx(Icon, { icon, size: ICON_SIZE[size], className: styles.icon() }) : null,
/* @__PURE__ */ jsx(Text, { className: styles.text(), children })
] }) });
}
function Bullet({ icon, children }) {
return /* @__PURE__ */ jsxs(HStack, { className: "gap-sm items-start", children: [
/* @__PURE__ */ jsx(Icon, { icon, className: "text-accent" }),
/* @__PURE__ */ jsx(Text, { className: "shrink", children })
] });
}
const Code = forwardRef(
({ className, ...props }, ref) => {
return /* @__PURE__ */ jsx(
Text,
{
ref,
role: "code",
className: `font-mono bg-highlight rounded-xs px-xxs py-px select-auto ${className ?? ""}`,
...props
}
);
}
);
const codeBlockVariants = tv({
slots: {
frame: "gap-xs",
title: "font-mono text-xs text-muted",
// web:whitespace-pre so a long line scrolls instead of wrapping; native
// already keeps the line intact inside the horizontal ScrollView, which
// gives the Text an unconstrained width.
code: "font-mono text-sharp select-auto web:whitespace-pre"
},
variants: {
size: {
sm: { code: "text-xs" },
md: { code: "text-sm" }
}
},
defaultVariants: { size: "md" }
});
function CodeBlock({
title,
size,
className,
children
}) {
const styles = codeBlockVariants({ size });
return /* @__PURE__ */ jsxs(
Surface,
{
variant: "lowered",
size: "sm",
className: styles.frame({ className }),
children: [
title === void 0 ? null : /* @__PURE__ */ jsx(Text, { className: styles.title(), children: title }),
/* @__PURE__ */ jsx(ScrollView, { horizontal: true, children: /* @__PURE__ */ jsx(Text, { role: "code", className: styles.code(), children }) })
]
}
);
}
const blockquoteRole = "blockquote";
const blockquoteVariants = tv({
slots: {
// The accent rule is the whole affordance: no fill, so a quote reads as a
// quote wherever it sits (screen, Surface, Message).
frame: "gap-xs border-l-4 border-accent pl-m",
quote: "text-sharp"
},
variants: {
size: {
sm: { quote: "text-base" },
md: { quote: "text-lg" }
}
},
defaultVariants: { size: "md" }
});
function Blockquote({
children,
citation,
accent,
size,
className
}) {
const styles = blockquoteVariants({ size });
return /* @__PURE__ */ jsxs(
Box,
{
accent,
role: blockquoteRole,
className: styles.frame({ className }),
children: [
/* @__PURE__ */ jsx(Paragraph, { className: styles.quote(), children }),
citation
]
}
);
}
const citationVariants = tv({
slots: {
frame: "flex-row items-center gap-xxs",
text: "text-muted select-auto"
},
variants: {
size: {
sm: { text: "text-xs" },
md: { text: "text-sm" }
}
},
defaultVariants: { size: "md" }
});
function Citation({
children,
href,
openLinkBehavior = defaultExternalOpenLinkBehavior,
accent,
size,
className
}) {
const styles = citationVariants({ size });
return /* @__PURE__ */ jsxs(Box, { accent, className: styles.frame({ className }), children: [
/* @__PURE__ */ jsx(Text, { className: styles.text(), children: "\u2014" }),
href === void 0 ? /* @__PURE__ */ jsx(Text, { className: styles.text(), children }) : /* @__PURE__ */ jsx(
ExternalLinkText,
{
href,
openLinkBehavior,
size: "sm",
text: children
}
)
] });
}
const connectedHoldMs = 1200;
function ConnectionState({
state,
forceHidden,
forceVisible,
children
}) {
const connected = state === "connected";
const [hideAfterHold, setHideAfterHold] = useState(false);
useEffect(() => {
if (!connected || forceVisible) {
setHideAfterHold(false);
return void 0;
}
const timer = setTimeout(() => {
setHideAfterHold(true);
}, connectedHoldMs);
return () => {
clearTimeout(timer);
};
}, [connected, forceVisible]);
const hidden = forceHidden || !forceVisible && (!state || connected && hideAfterHold);
const accent = connected ? "success" : "danger";
return /* @__PURE__ */ jsx(AccentScope, { accent, children: /* @__PURE__ */ jsx(
View,
{
className: `absolute inset-x-0 top-0 z-9 h-0.5 bg-interactive-contained-pressable shadow-m transition-transform duration-slide ease-in-out ${hidden ? "-translate-y-6" : "translate-y-0"}`,
children: state ? /* @__PURE__ */ jsx(Text, { className: "absolute left-1/2 top-0.5 h-5.5 w-50 -translate-x-1/2 rounded-b-sm bg-interactive-contained-pressable text-center leading-5.5 text-on-accent transition-colors duration-fast", children }) : null
}
) });
}
const track = tv({
base: "absolute inset-x-0 top-0 z-10 overflow-hidden transition-opacity duration-fade",
variants: {
size: {
xs: "h-0.5",
sm: "h-1",
md: "h-1.5",
lg: "h-2"
},
hidden: {
true: "opacity-0",
false: "opacity-100"
}
},
defaultVariants: { size: "md", hidden: false }
});
function LinearProgress({
progress,
hidden = false,
accent = "brand",
size = "md"
}) {
return /* @__PURE__ */ jsx(AccentScope, { accent, children: /* @__PURE__ */ jsx(View, { pointerEvents: "none", className: track({ size, hidden }), children: /* @__PURE__ */ jsx(
View,
{
className: "h-full bg-accent transition-[width] duration-progress ease-out",
style: { width: `${progress}%` }
}
) }) });
}
function PressableListItem({
variant = "contained",
role = "button",
accent,
children,
onPress
}) {
return /* @__PURE__ */ jsxs(
PressableBox,
{
variant,
role,
accent,
className: "flex-row items-center justify-between mx-xs my-xxs px-m py-m",
onPress,
children: [
/* @__PURE__ */ jsx(View$1, { className: "flex-1", children }),
/* @__PURE__ */ jsx(View$1, { className: "justify-center", children: /* @__PURE__ */ jsx(
Icon,
{
className: variant === "contained" ? "text-on-accent-muted" : "text-muted",
icon: /* @__PURE__ */ jsx(CaretRightRegularIcon, {}),
size: 18
}
) })
]
}
);
}
function GradientBackground({
accent,
children
}) {
return /* @__PURE__ */ jsx(AccentScope, { accent, children: /* @__PURE__ */ jsx(View$1, { className: "absolute inset-0 bg-linear-to-t from-screen-gradient-end from-5% via-screen-gradient-middle via-80% to-screen-gradient-start to-98%", children }) });
}
const GradientScrollViewInner = forwardRef(({ children, ...scrollViewProps }, ref) => {
return /* @__PURE__ */ jsxs(ScrollView$1, { ref, ...scrollViewProps, children: [
/* @__PURE__ */ jsx(View$1, { className: "absolute left-0 right-0 top-[-600] height-[600] bg-screen-gradient-start" }),
/* @__PURE__ */ jsx(View$1, { className: "absolute left-0 right-0 bottom-[-600] height-[600] bg-screen-gradient-end" }),
/* @__PURE__ */ jsx(GradientBackground, {}),
children
] });
});
const GradientScrollView = forwardRef(({ accent, children, ...scrollViewProps }, ref) => {
return /* @__PURE__ */ jsx(AccentScope, { accent, children: /* @__PURE__ */ jsx(GradientScrollViewInner, { ref, ...scrollViewProps, children }) });
});
function ScreenCenterLayout({
header,
content,
footer
}) {
return /* @__PURE__ */ jsxs(VStack, { className: "grow gap-xl min-h-screen", children: [
header,
/* @__PURE__ */ jsx(View$1, { className: "grow flex-center", children: content }),
footer
] });
}
function useScreenContainerProps({
className,
contentContainerClassName,
contentContainerStyle,
edges
}) {
const safeAreaPadding = useScreenSafeAreaPadding(edges);
return {
className: twMerge$1("bg-screen min-h-full", className),
contentContainerClassName: twMerge$1("grow", contentContainerClassName),
contentContainerStyle: safeAreaPadding ? [contentContainerStyle, safeAreaPadding] : contentContainerStyle
};
}
function ScreenScrollView({
className,
contentContainerClassName,
contentContainerStyle,
edges,
...props
}) {
const containerProps = useScreenContainerProps({
className,
contentContainerClassName,
contentContainerStyle,
edges
});
return /* @__PURE__ */ jsx(ScrollView, { ...containerProps, ...props });
}
function ScreenFlatList({
className,
contentContainerClassName,
contentContainerStyle,
edges,
...props
}) {
const containerProps = useScreenContainerProps({
className,
contentContainerClassName,
contentContainerStyle,
edges
});
return /* @__PURE__ */ jsx(FlatList, { ...containerProps, ...props });
}
function ScreenSectionList({
className,
contentContainerClassName,
contentContainerStyle,
edges,
...props
}) {
const containerProps = useScreenContainerProps({
className,
contentContainerClassName,
contentContainerStyle,
edges
});
return /* @__PURE__ */ jsx(SectionList, { ...containerProps, ...props });
}
const appShellVariants = tv({
slots: {
frame: "min-h-full",
// The page's ground rides with the content, which is opaque and always
// fills the shell — the frame's own ground shows in one place only: the
// band a bounce opens past an edge.
content: "grow bg-screen",
body: "grow flex-col md:flex-row",
sidebar: "shrink-0 self-stretch p-sm md:p-m web:sticky web:top-0 web:max-h-screen",
// `shrink` is not redundant with `grow`: React Native's shrink is 0, so the
// screen would otherwise take its content's widest line as its width and
// widen the row past the shell — and the scroll is vertical, so what leaves
// the right edge is clipped, not reachable.
main: "grow shrink"
},
variants: {
withHeader: {
// That band bares the scroll container, never the content that slid away,
// so the frame carries the ground each end needs: the `bar` header's
// above, the screen's below, split at the middle where no band reaches.
// React Native cannot paint a gradient on a ScrollView, so native keeps
// the flat bar ground.
true: {
frame: "bg-highlight web:bg-linear-to-b web:from-highlight web:from-50% web:to-screen web:to-50%"
},
false: { frame: "bg-screen" }
}
}
});
function AppShell({
header,
footer,
children,
className,
contentContainerClassName,
contentContainerStyle,
...props
}) {
const styles = appShellVariants({ withHeader: header !== void 0 });
const consumedEdges = useConsumedSafeAreaEdges();
const paddedEdges = allSafeAreaEdges.filter(
(edge) => !consumedEdges.includes(edge) && !(header !== void 0 && edge === "top")
);
const safeAreaPadding = useScreenSafeAreaPadding(paddedEdges);
return /* @__PURE__ */ jsxs(
ScrollView,
{
className: styles.frame({ className }),
contentContainerClassName: styles.content({
className: contentContainerClassName
}),
contentContainerStyle: safeAreaPadding ? [contentContainerStyle, safeAreaPadding] : contentContainerStyle,
...props,
children: [
header,
/* @__PURE__ */ jsxs(SafeAreaScope, { consumedEdges: allSafeAreaEdges, children: [
/* @__PURE__ */ jsx(View, { className: styles.body(), children }),
footer
] })
]
}
);
}
function AppShellSidebar({
children,
className
}) {
return /* @__PURE__ */ jsx(View, { className: appShellVariants().sidebar({ className }), children });
}
function AppShellMain({
children,
className
}) {
return /* @__PURE__ */ jsx(View, { role: "main", className: appShellVariants().main({ className }), children });
}
function AppLayout({
sidebar,
children,
...props
}) {
return /* @__PURE__ */ jsxs(AppShell, { ...props, children: [
sidebar ? /* @__PURE__ */ jsx(AppShellSidebar, { children: sidebar }) : null,
/* @__PURE__ */ jsx(AppShellMain, { children })
] });
}
const appHeaderVariants = tv({
slots: {
frame: "",
inner: "w-full self-center flex-row flex-wrap items-center justify-between",
startSlot: "items-start web:md:flex-1",
endSlot: "items-end web:md:order-3 web:md:flex-1",
navSlot: "w-full items-stretch web:md:order-2 web:md:w-auto web:md:items-center"
},
variants: {
size: {
xs: { inner: "gap-xs px-xs md:px-m py-1 md:gap-xs" },
sm: { inner: "gap-xs px-m md:px-l py-xs md:gap-sm" },
md: { inner: "gap-sm px-m md:px-l py-sm md:gap-m" }
},
variant: {
// `shadow-bar` casts downwards only: the header sits above the page, it is
// not a raised control catching a highlight on its own top edge.
bar: { frame: "bg-highlight shadow-bar" },
// Part of the page it heads (a landing hero): no ground of its own, so
// whatever is behind shows through.
transparent: { frame: "bg-transparent" }
},
contentWidth: {
boxed: { inner: "max-w-[1200px]" },
full: {}
},
withActions: {
// Without actions the end slot is a pure spacer: it only has to exist on
// the single-line layout, where it balances the start slot.
false: { endSlot: "hidden web:md:flex" },
true: {}
}
},
defaultVariants: {
size: "md",
variant: "bar",
contentWidth: "boxed"
}
});
function AppHeader({
brand,
actions,
children,
size,
variant,
contentWidth,
withSafeAreaTop = true,
className,
...props
}) {
const consumedEdges = useConsumedSafeAreaEdges();
const safeAreaPadding = useScreenSafeAreaPadding(
withSafeAreaTop && !consumedEdges.includes("top") ? ["top"] : []
);
const styles = appHeaderVariants({
size,
variant,
contentWidth,
withActions: actions !== void 0
});
return /* @__PURE__ */ jsx(
Box,
{
role: "banner",
className: styles.frame({ className }),
style: safeAreaPadding,
...props,
children: /* @__PURE__ */ jsxs(View, { className: styles.inner(), children: [
/* @__PURE__ */ jsx(View, { className: styles.startSlot(), children: brand }),
/* @__PURE__ */ jsx(View, { className: styles.endSlot(), children: actions }),
children ? /* @__PURE__ */ jsx(View, { className: styles.navSlot(), children }) : null
] })
}
);
}
const appHeaderBrandVariants = tv({
slots: {
// The header slot aligns the brand.
frame: "items-center gap-xs",
title: "font-heading-bold text-xl",
subtitle: "text-muted text-sm"
},
variants: {
interactive: {
// A Pressable is not an HStack, hence the explicit row; it also needs
// room for its hover fill and focus outline. `-ml-xs` pulls that leading
// padding back out, so the fill bleeds into the header's gutter and the
// mark stays flush with the content edge — a linked brand lands exactly
// where a display-only one does. The trailing padding is kept: it only
// extends the hit area towards the navigation, where nothing lines up.
true: {
frame: "flex-row rounded-sm py-xxs px-xs md:px-sm -ml-xs md:-ml-sm"
},
false: {}
}
},
defaultVariants: { interactive: false }
});
function AppHeaderBrand({
title,
subtitle,
brandLogo,
href,
onPress,
...props
}) {
const interactive = href !== void 0 || onPress !== void 0;
const styles = appHeaderBrandVariants({ interactive });
const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [
brandLogo,
/* @__PURE__ */ jsxs(VStack, { children: [
/* @__PURE__ */ jsx(Text, { className: styles.title(), children: title }),
subtitle ? /* @__PURE__ */ jsx(Text, { className: styles.subtitle(), children: subtitle }) : null
] })
] });
if (!interactive) {
return /* @__PURE__ */ jsx(HStack, { className: styles.frame(), ...props, children: content });
}
return /* @__PURE__ */ jsx(
PressableBox,
{
variant: "soft",
className: styles.frame(),
...{
role: href === void 0 ? "button" : "link",
href,
onPress,
...props
},
children: content
}
);
}
function BrandLogo({
icon,
accent = "brand"
}) {
return /* @__PURE__ */ jsx(
Box,
{
accent,
className: "flex-center shrink-0 size-[32px] rounded-full bg-enabled",
children: /* @__PURE__ */ jsx(Icon, { icon, size: 22, className: "text-on-accent" })
}
);
}
const appHeaderActionsVariants = tv({
base: "items-center gap-xs"
});
function AppHeaderActions({
className,
...props
}) {
return /* @__PURE__ */ jsx(HStack, { className: appHeaderActionsVariants({ className }), ...props });
}
const appHeaderAccountVariants = tv({
base: "flex-row items-center gap-xxs rounded-sm px-xxs min-h-[44px]"
});
function AppHeaderAccount({
name,
icon,
accent,
header,
children
}) {
return /* @__PURE__ */ jsx(
Menu,
{
label: name,
header,
render: (triggerProps) => /* @__PURE__ */ jsxs(
PressableBox,
{
variant: "soft",
"aria-label": name,
className: appHeaderAccountVariants(),
...triggerProps,
children: [
/* @__PURE__ */ jsx(Avatar, { name, icon, accent }),
/* @__PURE__ */ jsx(
Icon,
{
icon: /* @__PURE__ */ jsx(CaretDownRegularIcon, {}),
size: 14,
className: "text-muted"
}
)
]
}
),
children
}
);
}
function AppHeaderSignIn({
label,
size = "sm",
...buttonProps
}) {
return /* @__PURE__ */ jsx(Button, { size, text: label, ...buttonProps });
}
const Breakpoints = {
/**
* min-width: 0
*/
BASE: 0,
/**
* min-width: 480px
*/
SMALL: 480,
/**
* min-width: 768px
*/
MEDIUM: 768,
/**
* min-width: 1024px
*/
LARGE: 1024,
/**
* min-width: 1280px
*/
WIDE: 1280
};
var BreakpointNameEnum = /* @__PURE__ */ ((BreakpointNameEnum2) => {
BreakpointNameEnum2["BASE"] = "base";
BreakpointNameEnum2["SMALL"] = "small";
BreakpointNameEnum2["MEDIUM"] = "medium";
BreakpointNameEnum2["LARGE"] = "large";
BreakpointNameEnum2["WIDE"] = "wide";
return BreakpointNameEnum2;
})(BreakpointNameEnum || {});
function useCurrentBreakpointName() {
const { width } = useWindowDimensions();
if (width >= Breakpoints.WIDE) return BreakpointNameEnum.WIDE;
if (width >= Breakpoints.LARGE) return BreakpointNameEnum.LARGE;
if (width >= Breakpoints.MEDIUM) return BreakpointNameEnum.MEDIUM;
if (width >= Breakpoints.SMALL) return BreakpointNameEnum.SMALL;
return BreakpointNameEnum.BASE;
}
function useCurrentBreakpointNameFiltered(names) {
const current = useCurrentBreakpointName();
const ordered = [
BreakpointNameEnum.WIDE,
BreakpointNameEnum.LARGE,
BreakpointNameEnum.MEDIUM,
BreakpointNameEnum.SMALL,
BreakpointNameEnum.BASE
];
const startIndex = ordered.indexOf(current);
for (let i = startIndex; i < ordered.length; i++) {
const candidate = ordered[i];
if (names.includes(candidate)) return candidate;
}
return BreakpointNameEnum.BASE;
}
const VISIBILITY_CLASS = {
"base:end": "flex",
"base:small": "flex sm:hidden",
"base:medium": "flex md:hidden",
"base:large": "flex lg:hidden",
"base:wide": "flex xl:hidden",
"small:end": "hidden sm:flex",
"small:medium": "hidden sm:flex md:hidden",
"small:large": "hidden sm:flex lg:hidden",
"small:wide": "hidden sm:flex xl:hidden",
"medium:end": "hidden md:flex",
"medium:large": "hidden md:flex lg:hidden",
"medium:wide": "hidden md:flex xl:hidden",
"large:end": "hidden lg:flex",
"large:wide": "hidden lg:flex xl:hidden",
"wide:end": "hidden xl:flex"
};
function SwitchBreakpointsUsingDisplayNone({
...breakpoints
}) {
const entries = Object.entries(breakpoints);
return entries.map(([name, node], index) => {
const next = entries[index + 1]?.[0] ?? "end";
const className = VISIBILITY_CLASS[`${name}:${next}`] ?? "flex";
return /* @__PURE__ */ jsx(View$1, { className, children: node }, name);
});
}
function SwitchBreakpointsUsingNull({
children,
...breakpoints
}) {
const currentBreakpointName = useCurrentBreakpointNameFiltered(
Object.keys(breakpoints)
);
return breakpoints[currentBreakpointName] ?? null;
}
export { AccentScope, ActionButton, AlertDialog, AlouetteDecorator, AlouetteProvider, AppHeader, AppHeaderAccount, AppHeaderActions, AppHeaderBrand, AppHeaderSignIn, AppLayout, AppShell, AppShellMain, AppShellSidebar, Avatar, Badge, Blockquote, Box, BrandLogo, BreadcrumbItem, Breadcrumbs, BreakpointNameEnum, Breakpoints, Bullet, Button, CircularProgress, Citation, Code, CodeBlock, ColorModePicker, ConfirmationMessage, ConnectionState, EditableItem, EditableSurface, ErrorMessage, ExternalLink, ExternalLinkButton, ExternalLinkText, FlatList, Form, FormEditableItem, FormEditableSurface, FormField, FormFieldArray, FormItem, FormSubmitButton, FormValidationError, GradientBackground, GradientScrollView, HStack, Icon, IconButton, InfoAlertDialog, InfoMessage, InputText, InputTextAutocomplete, InteractiveBox, InteractiveIcon, InternalLinkButton, LinearProgress, LinkText, Menu, MenuItem, Message, Modal, NavBar, NavBarItem, Paragraph, Popover, PortalAccentScope, PresenceList, PresenceOne, PressableBox, PressableListItem, QuestionAlertDialog, Radio, RadioButton, RadioButtonGroup, RadioCard, RadioCardGroup, RadioGroup, SafeAreaBox, SafeAreaScope, ScopedTheme, ScreenCenterLayout, ScreenFlatList, ScreenScrollView, ScreenSectionList, ScrollView, SectionList, Select, Separator, SimpleVForm, StableAccentScope, Stack, Story, StoryContainer, StoryDecorator, StoryGrid, StoryTitle, SuccessAlertDialog, Surface, Switch, SwitchBreakpointsUsingDisplayNone, SwitchBreakpointsUsingNull, Tab, Tabs, Text, TextArea, VStack, View, WarningAlertDialog, WarningMessage, animationDurationsMs, styled, useConsumedSafeAreaEdges, useCurrentBreakpointName, useCurrentBreakpointNameFiltered, useCurrentMode, useCurrentTheme, useResolvedColorMode, useScreenSafeAreaPadding, useSystemColorMode };
//# sourceMappingURL=index-node22.mjs.map