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