@muja-ui/native 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createContext, forwardRef, useState, useContext, useCallback, useMemo, useRef, useEffect } from 'react';
2
- import { View, Text as Text$1, Pressable, ActivityIndicator, TextInput, Platform, useColorScheme, StyleSheet, useWindowDimensions, Animated, Easing, PanResponder, Modal as Modal$1, ScrollView, RefreshControl, Image } from 'react-native';
3
- import { darkTheme, lightTheme, getIcon } from '@muja-ui/core';
2
+ import { View, Text as Text$1, Pressable, ActivityIndicator, TextInput, StyleSheet, Platform, useColorScheme, Switch as Switch$1, useWindowDimensions, PanResponder, Animated, Modal as Modal$1, ScrollView, Easing, RefreshControl, Image } from 'react-native';
3
+ import { createTheme, darkTheme, lightTheme, getIcon } from '@muja-ui/core';
4
4
  export { createTheme, darkTheme, lightTheme, registerIcons } from '@muja-ui/core';
5
5
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
6
  import { warnOnce, clamp } from '@muja-ui/utils';
@@ -9,6 +9,25 @@ import { MinusIcon, CheckIcon, ChevronDownIcon, InfoIcon, AlertCircleIcon, Alert
9
9
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
10
10
 
11
11
  // src/components/Box.tsx
12
+ function isIOS() {
13
+ return Platform.OS === "ios";
14
+ }
15
+ var iosThemeOverride = {
16
+ typography: {
17
+ fontSize: { xs: 13, sm: 15, md: 17, lg: 20, xl: 22, "2xl": 28, "3xl": 34, "4xl": 40 }
18
+ },
19
+ radius: { sm: 6, md: 10, lg: 12, xl: 14, "2xl": 20 }
20
+ };
21
+ function adaptThemeToPlatform(theme) {
22
+ return isIOS() ? createTheme(theme, iosThemeOverride) : theme;
23
+ }
24
+ var IOS_PRESSED_OPACITY = 0.6;
25
+ var sheetSpring = {
26
+ stiffness: 320,
27
+ damping: 34,
28
+ mass: 1,
29
+ overshootClamping: true
30
+ };
12
31
  var ThemeContext = createContext(null);
13
32
  function ThemeProvider({
14
33
  theme = lightTheme,
@@ -16,6 +35,7 @@ function ThemeProvider({
16
35
  defaultColorMode = "system",
17
36
  colorMode: colorModeProp,
18
37
  onColorModeChange,
38
+ platformAdaptive = true,
19
39
  children
20
40
  }) {
21
41
  const [uncontrolledMode, setUncontrolledMode] = useState(defaultColorMode);
@@ -32,15 +52,20 @@ function ThemeProvider({
32
52
  const toggleColorMode = useCallback(() => {
33
53
  setColorMode(resolvedColorMode === "dark" ? "light" : "dark");
34
54
  }, [resolvedColorMode, setColorMode]);
55
+ const activeTheme = resolvedColorMode === "dark" ? darkTheme2 : theme;
56
+ const renderedTheme = useMemo(
57
+ () => platformAdaptive ? adaptThemeToPlatform(activeTheme) : activeTheme,
58
+ [activeTheme, platformAdaptive]
59
+ );
35
60
  const value = useMemo(
36
61
  () => ({
37
- theme: resolvedColorMode === "dark" ? darkTheme2 : theme,
62
+ theme: renderedTheme,
38
63
  colorMode,
39
64
  resolvedColorMode,
40
65
  setColorMode,
41
66
  toggleColorMode
42
67
  }),
43
- [theme, darkTheme2, colorMode, resolvedColorMode, setColorMode, toggleColorMode]
68
+ [renderedTheme, colorMode, resolvedColorMode, setColorMode, toggleColorMode]
44
69
  );
45
70
  return /* @__PURE__ */ jsx(ThemeContext.Provider, { value, children });
46
71
  }
@@ -199,6 +224,50 @@ function Divider({ orientation = "horizontal", color = "border", style }) {
199
224
  }
200
225
  );
201
226
  }
227
+ var SYSTEM_SANS = /* @__PURE__ */ new Set([
228
+ "-apple-system",
229
+ "blinkmacsystemfont",
230
+ "system-ui",
231
+ "ui-sans-serif",
232
+ "ui-rounded",
233
+ "sans-serif",
234
+ "system",
235
+ "roboto"
236
+ ]);
237
+ var SYSTEM_MONO = /* @__PURE__ */ new Set(["ui-monospace", "monospace"]);
238
+ var SYSTEM_SERIF = /* @__PURE__ */ new Set(["ui-serif", "serif"]);
239
+ var IOS_ONLY = /* @__PURE__ */ new Set([
240
+ "helvetica neue",
241
+ "georgia",
242
+ "menlo",
243
+ "times new roman",
244
+ "times",
245
+ "arial"
246
+ ]);
247
+ var NEVER_INSTALLED = /* @__PURE__ */ new Set([
248
+ "segoe ui",
249
+ "sfmono-regular",
250
+ "sf mono",
251
+ "liberation mono",
252
+ "consolas",
253
+ "cambria"
254
+ ]);
255
+ function nativeFontFamily(stack) {
256
+ if (!stack) return void 0;
257
+ const ios = Platform.OS === "ios";
258
+ for (const raw of stack.split(",")) {
259
+ const name = raw.trim().replace(/^['"]|['"]$/g, "");
260
+ if (!name) continue;
261
+ const key = name.toLowerCase();
262
+ if (SYSTEM_SANS.has(key)) return void 0;
263
+ if (SYSTEM_MONO.has(key)) return ios ? "Menlo" : "monospace";
264
+ if (SYSTEM_SERIF.has(key)) return ios ? "Georgia" : "serif";
265
+ if (NEVER_INSTALLED.has(key)) continue;
266
+ if (!ios && IOS_ONLY.has(key)) continue;
267
+ return name;
268
+ }
269
+ return void 0;
270
+ }
202
271
  var Text = forwardRef(function Text2(props, ref) {
203
272
  const theme = useTheme();
204
273
  const {
@@ -214,13 +283,10 @@ var Text = forwardRef(function Text2(props, ref) {
214
283
  numberOfLines,
215
284
  ...restProps
216
285
  } = props;
217
- const { style: tokenStyle, rest } = splitStyleProps(
218
- restProps,
219
- theme
220
- );
286
+ const { style: tokenStyle, rest } = splitStyleProps(restProps, theme);
221
287
  const fontSize = theme.typography.fontSize[size];
222
288
  const textStyle = {
223
- fontFamily: theme.typography.fontFamily[family],
289
+ fontFamily: nativeFontFamily(theme.typography.fontFamily[family]),
224
290
  fontSize,
225
291
  color: theme.colors[color],
226
292
  fontWeight: weight ? theme.typography.fontWeight[weight] : void 0,
@@ -247,7 +313,8 @@ var defaultSize = {
247
313
  5: "md",
248
314
  6: "sm"
249
315
  };
250
- var Heading = forwardRef(function Heading2({ level = 2, size, weight = "semibold", leading = "tight", ...rest }, ref) {
316
+ var Heading = forwardRef(function Heading2({ level = 2, size, weight, leading = "tight", ...rest }, ref) {
317
+ const resolvedWeight = weight ?? (isIOS() && level <= 2 ? "bold" : "semibold");
251
318
  return /* @__PURE__ */ jsx(
252
319
  Text,
253
320
  {
@@ -255,7 +322,7 @@ var Heading = forwardRef(function Heading2({ level = 2, size, weight = "semibold
255
322
  accessibilityRole: "header",
256
323
  "aria-level": level,
257
324
  size: size ?? defaultSize[level],
258
- weight,
325
+ weight: resolvedWeight,
259
326
  leading,
260
327
  ...rest
261
328
  }
@@ -308,6 +375,7 @@ function Icon({
308
375
  // src/system/variants.ts
309
376
  function variantColors(variant, theme) {
310
377
  const { colors, borderWidth } = theme;
378
+ const ios = isIOS();
311
379
  switch (variant) {
312
380
  case "primary":
313
381
  return {
@@ -334,6 +402,13 @@ function variantColors(variant, theme) {
334
402
  foreground: "onDanger"
335
403
  };
336
404
  case "outline":
405
+ if (ios) {
406
+ return {
407
+ background: colors.primarySubtle,
408
+ backgroundPressed: colors.primarySubtleHover,
409
+ foreground: "primaryText"
410
+ };
411
+ }
337
412
  return {
338
413
  background: "transparent",
339
414
  backgroundPressed: colors.surfaceActive,
@@ -344,8 +419,8 @@ function variantColors(variant, theme) {
344
419
  case "ghost":
345
420
  return {
346
421
  background: "transparent",
347
- backgroundPressed: colors.surfaceActive,
348
- foreground: "text"
422
+ backgroundPressed: ios ? "transparent" : colors.surfaceActive,
423
+ foreground: ios ? "primaryText" : "text"
349
424
  };
350
425
  case "link":
351
426
  return {
@@ -356,6 +431,10 @@ function variantColors(variant, theme) {
356
431
  };
357
432
  }
358
433
  }
434
+ function pressFeedback(pressed, colors) {
435
+ if (!pressed) return { backgroundColor: colors.background, opacity: 1 };
436
+ return isIOS() ? { backgroundColor: colors.background, opacity: IOS_PRESSED_OPACITY } : { backgroundColor: colors.backgroundPressed, opacity: 1 };
437
+ }
359
438
  function sizeMetrics(size, theme) {
360
439
  switch (size) {
361
440
  case "sm":
@@ -403,6 +482,7 @@ var Button = forwardRef(function Button2({
403
482
  const metrics = sizeMetrics(size, theme);
404
483
  const isInert = disabled || loading;
405
484
  const isLink = variant === "link";
485
+ const slop = metrics.height < 44 ? (44 - metrics.height) / 2 : 0;
406
486
  return /* @__PURE__ */ jsxs(
407
487
  Pressable,
408
488
  {
@@ -411,23 +491,27 @@ var Button = forwardRef(function Button2({
411
491
  accessibilityState: { disabled: isInert, busy: loading },
412
492
  accessibilityLabel,
413
493
  disabled: isInert,
414
- style: ({ pressed }) => [
415
- {
416
- flexDirection: "row",
417
- alignItems: "center",
418
- justifyContent: "center",
419
- gap: metrics.gap,
420
- height: isLink ? void 0 : metrics.height,
421
- paddingHorizontal: isLink ? 0 : metrics.paddingHorizontal,
422
- borderRadius: isLink ? 0 : theme.radius.md,
423
- backgroundColor: pressed ? colors.backgroundPressed : colors.background,
424
- borderColor: colors.borderColor,
425
- borderWidth: colors.borderWidth,
426
- alignSelf: fullWidth ? "stretch" : "flex-start",
427
- opacity: isInert ? theme.opacity.disabled : 1
428
- },
429
- style
430
- ],
494
+ hitSlop: slop > 0 ? { top: slop, bottom: slop } : void 0,
495
+ style: ({ pressed }) => {
496
+ const feedback = pressFeedback(pressed, colors);
497
+ return [
498
+ {
499
+ flexDirection: "row",
500
+ alignItems: "center",
501
+ justifyContent: "center",
502
+ gap: metrics.gap,
503
+ minHeight: isLink ? void 0 : metrics.height,
504
+ paddingHorizontal: isLink ? 0 : metrics.paddingHorizontal,
505
+ borderRadius: isLink ? 0 : size === "lg" ? theme.radius.lg : theme.radius.md,
506
+ backgroundColor: feedback.backgroundColor,
507
+ borderColor: colors.borderColor,
508
+ borderWidth: colors.borderWidth,
509
+ alignSelf: fullWidth ? "stretch" : "flex-start",
510
+ opacity: isInert ? theme.opacity.disabled : feedback.opacity
511
+ },
512
+ style
513
+ ];
514
+ },
431
515
  ...rest,
432
516
  children: [
433
517
  loading ? /* @__PURE__ */ jsx(ActivityIndicator, { size: "small", color: theme.colors[colors.foreground] }) : leftIcon ? /* @__PURE__ */ jsx(View, { children: leftIcon }) : null,
@@ -469,20 +553,23 @@ var IconButton = forwardRef(function IconButton2({
469
553
  accessibilityState: { disabled: isInert, busy: loading },
470
554
  disabled: isInert,
471
555
  hitSlop: dimension < 44 ? (44 - dimension) / 2 : void 0,
472
- style: ({ pressed }) => [
473
- {
474
- width: dimension,
475
- height: dimension,
476
- alignItems: "center",
477
- justifyContent: "center",
478
- borderRadius: round ? theme.radius.full : theme.radius.md,
479
- backgroundColor: pressed ? colors.backgroundPressed : colors.background,
480
- borderColor: colors.borderColor,
481
- borderWidth: colors.borderWidth,
482
- opacity: isInert ? theme.opacity.disabled : 1
483
- },
484
- style
485
- ],
556
+ style: ({ pressed }) => {
557
+ const feedback = pressFeedback(pressed, colors);
558
+ return [
559
+ {
560
+ width: dimension,
561
+ height: dimension,
562
+ alignItems: "center",
563
+ justifyContent: "center",
564
+ borderRadius: round ? theme.radius.full : theme.radius.md,
565
+ backgroundColor: feedback.backgroundColor,
566
+ borderColor: colors.borderColor,
567
+ borderWidth: colors.borderWidth,
568
+ opacity: isInert ? theme.opacity.disabled : feedback.opacity
569
+ },
570
+ style
571
+ ];
572
+ },
486
573
  ...rest,
487
574
  children: loading ? /* @__PURE__ */ jsx(ActivityIndicator, { size: "small", color: theme.colors[colors.foreground] }) : icon
488
575
  }
@@ -500,9 +587,13 @@ var Input = forwardRef(function Input2({
500
587
  ...rest
501
588
  }, ref) {
502
589
  const theme = useTheme();
590
+ const { resolvedColorMode } = useColorMode();
503
591
  const metrics = sizeMetrics(size, theme);
504
592
  const [focused, setFocused] = useState(false);
593
+ const ios = isIOS();
505
594
  const borderColor = invalid ? theme.colors.danger : focused ? theme.colors.focusRing : theme.colors.border;
595
+ const borderWidth = ios ? focused || invalid ? theme.borderWidth.thin : 0 : focused || invalid ? theme.borderWidth.medium : theme.borderWidth.thin;
596
+ const backgroundColor = ios ? disabled ? theme.colors.bgSubtle : theme.colors.bgMuted : disabled ? theme.colors.bgMuted : theme.colors.surface;
506
597
  return /* @__PURE__ */ jsxs(
507
598
  View,
508
599
  {
@@ -511,12 +602,12 @@ var Input = forwardRef(function Input2({
511
602
  flexDirection: "row",
512
603
  alignItems: "center",
513
604
  gap: metrics.gap,
514
- height: metrics.height,
605
+ minHeight: metrics.height,
515
606
  paddingHorizontal: metrics.paddingHorizontal,
516
607
  borderRadius: theme.radius.md,
517
- borderWidth: focused || invalid ? theme.borderWidth.medium : theme.borderWidth.thin,
608
+ borderWidth,
518
609
  borderColor,
519
- backgroundColor: disabled ? theme.colors.bgMuted : theme.colors.surface
610
+ backgroundColor
520
611
  },
521
612
  style
522
613
  ],
@@ -531,6 +622,8 @@ var Input = forwardRef(function Input2({
531
622
  "aria-disabled": disabled || void 0,
532
623
  placeholderTextColor: theme.colors.textMuted,
533
624
  selectionColor: theme.colors.primary,
625
+ keyboardAppearance: resolvedColorMode,
626
+ clearButtonMode: ios && !rightElement ? "while-editing" : void 0,
534
627
  onFocus: (event) => {
535
628
  setFocused(true);
536
629
  onFocus?.(event);
@@ -541,9 +634,9 @@ var Input = forwardRef(function Input2({
541
634
  },
542
635
  style: {
543
636
  flex: 1,
544
- // Android adds its own vertical padding; zero it so `height` wins.
637
+ // Android adds its own vertical padding; zero it so `minHeight` wins.
545
638
  paddingVertical: 0,
546
- fontFamily: theme.typography.fontFamily.sans,
639
+ fontFamily: nativeFontFamily(theme.typography.fontFamily.sans),
547
640
  fontSize: theme.typography.fontSize[metrics.fontSize],
548
641
  color: disabled ? theme.colors.textDisabled : theme.colors.text
549
642
  },
@@ -557,10 +650,12 @@ var Input = forwardRef(function Input2({
557
650
  });
558
651
  var Textarea = forwardRef(function Textarea2({ invalid = false, disabled = false, rows = 4, style, onFocus, onBlur, ...rest }, ref) {
559
652
  const theme = useTheme();
653
+ const { resolvedColorMode } = useColorMode();
560
654
  const [focused, setFocused] = useState(false);
561
- const lineHeight = Math.round(
562
- theme.typography.fontSize.md * theme.typography.lineHeight.normal
563
- );
655
+ const ios = isIOS();
656
+ const lineHeight = Math.round(theme.typography.fontSize.md * theme.typography.lineHeight.normal);
657
+ const borderWidth = ios ? focused || invalid ? theme.borderWidth.thin : 0 : focused || invalid ? theme.borderWidth.medium : theme.borderWidth.thin;
658
+ const backgroundColor = ios ? disabled ? theme.colors.bgSubtle : theme.colors.bgMuted : disabled ? theme.colors.bgMuted : theme.colors.surface;
564
659
  return /* @__PURE__ */ jsx(
565
660
  TextInput,
566
661
  {
@@ -571,6 +666,7 @@ var Textarea = forwardRef(function Textarea2({ invalid = false, disabled = false
571
666
  "aria-invalid": invalid || void 0,
572
667
  placeholderTextColor: theme.colors.textMuted,
573
668
  selectionColor: theme.colors.primary,
669
+ keyboardAppearance: resolvedColorMode,
574
670
  onFocus: (event) => {
575
671
  setFocused(true);
576
672
  onFocus?.(event);
@@ -584,10 +680,10 @@ var Textarea = forwardRef(function Textarea2({ invalid = false, disabled = false
584
680
  minHeight: rows * lineHeight + theme.space[4],
585
681
  padding: theme.space[3],
586
682
  borderRadius: theme.radius.md,
587
- borderWidth: focused || invalid ? theme.borderWidth.medium : theme.borderWidth.thin,
683
+ borderWidth,
588
684
  borderColor: invalid ? theme.colors.danger : focused ? theme.colors.focusRing : theme.colors.border,
589
- backgroundColor: disabled ? theme.colors.bgMuted : theme.colors.surface,
590
- fontFamily: theme.typography.fontFamily.sans,
685
+ backgroundColor,
686
+ fontFamily: nativeFontFamily(theme.typography.fontFamily.sans),
591
687
  fontSize: theme.typography.fontSize.md,
592
688
  lineHeight,
593
689
  color: disabled ? theme.colors.textDisabled : theme.colors.text
@@ -644,13 +740,10 @@ function Checkbox({
644
740
  style
645
741
  }) {
646
742
  const theme = useTheme();
647
- const [checked, setChecked] = useControllableState(
648
- controlledChecked,
649
- defaultChecked,
650
- onChange
651
- );
743
+ const [checked, setChecked] = useControllableState(controlledChecked, defaultChecked, onChange);
652
744
  const dimension = boxSize2[size];
653
745
  const filled = checked || indeterminate;
746
+ const ios = isIOS();
654
747
  return /* @__PURE__ */ jsxs(
655
748
  Pressable,
656
749
  {
@@ -680,7 +773,7 @@ function Checkbox({
680
773
  height: dimension,
681
774
  alignItems: "center",
682
775
  justifyContent: "center",
683
- borderRadius: theme.radius.sm,
776
+ borderRadius: ios ? theme.radius.full : theme.radius.sm,
684
777
  borderWidth: theme.borderWidth.medium,
685
778
  borderColor: invalid ? theme.colors.danger : filled ? theme.colors.primary : theme.colors.borderStrong,
686
779
  backgroundColor: filled ? theme.colors.primary : theme.colors.surface
@@ -696,7 +789,7 @@ function Checkbox({
696
789
  ) : null
697
790
  }
698
791
  ),
699
- children != null ? typeof children === "string" ? /* @__PURE__ */ jsx(Text, { size: size === "lg" ? "md" : "sm", style: { flexShrink: 1 }, children }) : children : null
792
+ children != null ? typeof children === "string" ? /* @__PURE__ */ jsx(Text, { size: size === "lg" || ios ? "md" : "sm", style: { flexShrink: 1 }, children }) : children : null
700
793
  ]
701
794
  }
702
795
  );
@@ -706,6 +799,7 @@ var track = {
706
799
  md: { width: 44, height: 26, thumb: 22 },
707
800
  lg: { width: 52, height: 32, thumb: 28 }
708
801
  };
802
+ var iosScale = { sm: 0.8, md: 1, lg: 1 };
709
803
  function Switch({
710
804
  checked: controlledChecked,
711
805
  defaultChecked = false,
@@ -716,11 +810,40 @@ function Switch({
716
810
  style
717
811
  }) {
718
812
  const theme = useTheme();
719
- const [checked, setChecked] = useControllableState(
720
- controlledChecked,
721
- defaultChecked,
722
- onChange
723
- );
813
+ const [checked, setChecked] = useControllableState(controlledChecked, defaultChecked, onChange);
814
+ const row = {
815
+ flexDirection: "row",
816
+ alignItems: "center",
817
+ justifyContent: "space-between",
818
+ gap: theme.space[3]
819
+ };
820
+ if (isIOS()) {
821
+ const label = typeof children === "string" ? children : void 0;
822
+ const scale = iosScale[size];
823
+ return /* @__PURE__ */ jsxs(View, { style: [row, style], children: [
824
+ children != null ? label !== void 0 ? /* @__PURE__ */ jsx(
825
+ Text,
826
+ {
827
+ size: "md",
828
+ style: { flexShrink: 1, opacity: disabled ? theme.opacity.disabled : 1 },
829
+ children: label
830
+ }
831
+ ) : children : null,
832
+ /* @__PURE__ */ jsx(
833
+ Switch$1,
834
+ {
835
+ value: checked,
836
+ onValueChange: setChecked,
837
+ disabled,
838
+ accessibilityLabel: label,
839
+ trackColor: { false: theme.colors.secondaryActive, true: theme.colors.primary },
840
+ thumbColor: theme.colors.onPrimary,
841
+ ios_backgroundColor: theme.colors.secondaryActive,
842
+ style: scale !== 1 ? { transform: [{ scale }] } : void 0
843
+ }
844
+ )
845
+ ] });
846
+ }
724
847
  const metrics = track[size];
725
848
  const inset = (metrics.height - metrics.thumb) / 2;
726
849
  return /* @__PURE__ */ jsxs(
@@ -730,16 +853,7 @@ function Switch({
730
853
  accessibilityState: { checked, disabled },
731
854
  disabled,
732
855
  onPress: () => setChecked(!checked),
733
- style: [
734
- {
735
- flexDirection: "row",
736
- alignItems: "center",
737
- justifyContent: "space-between",
738
- gap: theme.space[3],
739
- opacity: disabled ? theme.opacity.disabled : 1
740
- },
741
- style
742
- ],
856
+ style: [row, { opacity: disabled ? theme.opacity.disabled : 1 }, style],
743
857
  children: [
744
858
  children != null ? typeof children === "string" ? /* @__PURE__ */ jsx(Text, { size: "sm", style: { flexShrink: 1 }, children }) : children : null,
745
859
  /* @__PURE__ */ jsx(
@@ -866,11 +980,42 @@ function Radio({ value, disabled = false, children, style }) {
866
980
  ) : null
867
981
  }
868
982
  ),
869
- children != null ? typeof children === "string" ? /* @__PURE__ */ jsx(Text, { size: group.size === "lg" ? "md" : "sm", style: { flexShrink: 1 }, children }) : children : null
983
+ children != null ? typeof children === "string" ? /* @__PURE__ */ jsx(Text, { size: group.size === "lg" || isIOS() ? "md" : "sm", style: { flexShrink: 1 }, children }) : children : null
870
984
  ]
871
985
  }
872
986
  );
873
987
  }
988
+ function useSheetTransition(open, distance) {
989
+ const theme = useTheme();
990
+ const translateY = useRef(new Animated.Value(distance)).current;
991
+ const backdrop = useRef(new Animated.Value(0)).current;
992
+ useEffect(() => {
993
+ const animation = open ? Animated.parallel([
994
+ Animated.spring(translateY, { toValue: 0, ...sheetSpring, useNativeDriver: true }),
995
+ Animated.timing(backdrop, {
996
+ toValue: 1,
997
+ duration: theme.motion.duration.normal,
998
+ easing: Easing.out(Easing.quad),
999
+ useNativeDriver: true
1000
+ })
1001
+ ]) : Animated.parallel([
1002
+ Animated.timing(translateY, {
1003
+ toValue: distance,
1004
+ duration: theme.motion.duration.fast,
1005
+ easing: Easing.in(Easing.cubic),
1006
+ useNativeDriver: true
1007
+ }),
1008
+ Animated.timing(backdrop, {
1009
+ toValue: 0,
1010
+ duration: theme.motion.duration.fast,
1011
+ useNativeDriver: true
1012
+ })
1013
+ ]);
1014
+ animation.start();
1015
+ return () => animation.stop();
1016
+ }, [open, distance, translateY, backdrop, theme.motion.duration]);
1017
+ return { translateY, backdrop };
1018
+ }
874
1019
  var DISMISS_THRESHOLD = 96;
875
1020
  function BottomSheet({
876
1021
  open,
@@ -885,16 +1030,9 @@ function BottomSheet({
885
1030
  const theme = useTheme();
886
1031
  const insets = useSafeAreaInsets();
887
1032
  const { height } = useWindowDimensions();
888
- const translateY = useRef(new Animated.Value(height)).current;
1033
+ const { translateY, backdrop } = useSheetTransition(open, height);
889
1034
  const dragY = useRef(0);
890
- useEffect(() => {
891
- Animated.timing(translateY, {
892
- toValue: open ? 0 : height,
893
- duration: open ? theme.motion.duration.normal : theme.motion.duration.fast,
894
- easing: Easing.out(Easing.cubic),
895
- useNativeDriver: true
896
- }).start();
897
- }, [open, height, translateY, theme.motion.duration]);
1035
+ const ios = isIOS();
898
1036
  const panResponder = useRef(
899
1037
  PanResponder.create({
900
1038
  onMoveShouldSetPanResponder: (_event, gesture) => gesture.dy > 4,
@@ -908,7 +1046,11 @@ function BottomSheet({
908
1046
  if (dragY.current > DISMISS_THRESHOLD) {
909
1047
  onClose();
910
1048
  } else {
911
- Animated.spring(translateY, { toValue: 0, useNativeDriver: true }).start();
1049
+ Animated.spring(translateY, {
1050
+ toValue: 0,
1051
+ ...sheetSpring,
1052
+ useNativeDriver: true
1053
+ }).start();
912
1054
  }
913
1055
  dragY.current = 0;
914
1056
  }
@@ -924,11 +1066,17 @@ function BottomSheet({
924
1066
  onRequestClose: onClose,
925
1067
  children: /* @__PURE__ */ jsxs(View, { style: { flex: 1, justifyContent: "flex-end" }, children: [
926
1068
  /* @__PURE__ */ jsx(
927
- Pressable,
1069
+ Animated.View,
928
1070
  {
929
- accessibilityLabel: closeOnOverlayPress ? "Close" : void 0,
930
- onPress: closeOnOverlayPress ? onClose : void 0,
931
- style: { flex: 1, backgroundColor: theme.colors.overlay }
1071
+ style: { flex: 1, backgroundColor: theme.colors.overlay, opacity: backdrop },
1072
+ children: /* @__PURE__ */ jsx(
1073
+ Pressable,
1074
+ {
1075
+ accessibilityLabel: closeOnOverlayPress ? "Close" : void 0,
1076
+ onPress: closeOnOverlayPress ? onClose : void 0,
1077
+ style: { flex: 1 }
1078
+ }
1079
+ )
932
1080
  }
933
1081
  ),
934
1082
  /* @__PURE__ */ jsxs(
@@ -948,32 +1096,39 @@ function BottomSheet({
948
1096
  style
949
1097
  ],
950
1098
  children: [
951
- /* @__PURE__ */ jsxs(View, { ...panResponder.panHandlers, style: { paddingTop: theme.space[3] }, children: [
952
- /* @__PURE__ */ jsx(
953
- View,
954
- {
955
- style: {
956
- alignSelf: "center",
957
- width: 40,
958
- height: 4,
959
- borderRadius: theme.radius.full,
960
- backgroundColor: theme.colors.borderStrong
961
- }
962
- }
963
- ),
964
- title ? /* @__PURE__ */ jsx(
965
- Heading,
966
- {
967
- level: 3,
968
- size: "lg",
969
- style: {
970
- paddingHorizontal: theme.space[5],
971
- paddingTop: theme.space[4]
972
- },
973
- children: title
974
- }
975
- ) : null
976
- ] }),
1099
+ /* @__PURE__ */ jsxs(
1100
+ View,
1101
+ {
1102
+ ...panResponder.panHandlers,
1103
+ style: { paddingTop: ios ? theme.space[1.5] : theme.space[3] },
1104
+ children: [
1105
+ /* @__PURE__ */ jsx(
1106
+ View,
1107
+ {
1108
+ style: {
1109
+ alignSelf: "center",
1110
+ width: ios ? 36 : 40,
1111
+ height: ios ? 5 : 4,
1112
+ borderRadius: theme.radius.full,
1113
+ backgroundColor: theme.colors.borderStrong
1114
+ }
1115
+ }
1116
+ ),
1117
+ title ? /* @__PURE__ */ jsx(
1118
+ Heading,
1119
+ {
1120
+ level: 3,
1121
+ size: "lg",
1122
+ style: {
1123
+ paddingHorizontal: theme.space[5],
1124
+ paddingTop: theme.space[4]
1125
+ },
1126
+ children: title
1127
+ }
1128
+ ) : null
1129
+ ]
1130
+ }
1131
+ ),
977
1132
  children
978
1133
  ]
979
1134
  }
@@ -998,6 +1153,7 @@ function Select({
998
1153
  const metrics = sizeMetrics(size, theme);
999
1154
  const [open, setOpen] = useState(false);
1000
1155
  const selected = options.find((option) => option.value === value);
1156
+ const ios = isIOS();
1001
1157
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1002
1158
  /* @__PURE__ */ jsxs(
1003
1159
  Pressable,
@@ -1015,12 +1171,13 @@ function Select({
1015
1171
  alignItems: "center",
1016
1172
  justifyContent: "space-between",
1017
1173
  gap: metrics.gap,
1018
- height: metrics.height,
1174
+ minHeight: metrics.height,
1019
1175
  paddingHorizontal: metrics.paddingHorizontal,
1020
1176
  borderRadius: theme.radius.md,
1021
- borderWidth: invalid ? theme.borderWidth.medium : theme.borderWidth.thin,
1177
+ // Mirrors Input: iOS is a filled rect that only gains a stroke when invalid.
1178
+ borderWidth: invalid ? ios ? theme.borderWidth.thin : theme.borderWidth.medium : ios ? 0 : theme.borderWidth.thin,
1022
1179
  borderColor: invalid ? theme.colors.danger : theme.colors.border,
1023
- backgroundColor: disabled ? theme.colors.bgMuted : theme.colors.surface,
1180
+ backgroundColor: ios ? disabled ? theme.colors.bgSubtle : theme.colors.bgMuted : disabled ? theme.colors.bgMuted : theme.colors.surface,
1024
1181
  opacity: disabled ? theme.opacity.disabled : 1
1025
1182
  },
1026
1183
  style
@@ -1244,6 +1401,7 @@ function ToastCard({
1244
1401
  const theme = useTheme();
1245
1402
  const tone = entry.tone ?? "neutral";
1246
1403
  const enter = useRef(new Animated.Value(0)).current;
1404
+ const ios = isIOS();
1247
1405
  useEffect(() => {
1248
1406
  Animated.timing(enter, {
1249
1407
  toValue: 1,
@@ -1268,8 +1426,8 @@ function ToastCard({
1268
1426
  alignItems: "flex-start",
1269
1427
  gap: theme.space[3],
1270
1428
  padding: theme.space[3.5],
1271
- borderRadius: theme.radius.lg,
1272
- borderWidth: theme.borderWidth.thin,
1429
+ borderRadius: ios ? theme.radius.xl : theme.radius.lg,
1430
+ borderWidth: ios ? 0 : theme.borderWidth.thin,
1273
1431
  borderColor: theme.colors.border,
1274
1432
  backgroundColor: theme.colors.surface,
1275
1433
  opacity: enter,
@@ -1465,7 +1623,7 @@ var Card = forwardRef(function Card2({ variant = "outline", onPress, accessibili
1465
1623
  const base = {
1466
1624
  borderRadius: theme.radius.lg,
1467
1625
  backgroundColor: variant === "filled" ? theme.colors.bgSubtle : theme.colors.surface,
1468
- borderWidth: variant === "outline" ? theme.borderWidth.thin : 0,
1626
+ borderWidth: variant === "outline" ? isIOS() ? StyleSheet.hairlineWidth : theme.borderWidth.thin : 0,
1469
1627
  borderColor: theme.colors.border,
1470
1628
  ...variant === "elevated" ? shadowStyle(theme.shadow.sm) : null
1471
1629
  };
@@ -1490,7 +1648,16 @@ var Card = forwardRef(function Card2({ variant = "outline", onPress, accessibili
1490
1648
  });
1491
1649
  function CardHeader({ children, style }) {
1492
1650
  const theme = useTheme();
1493
- return /* @__PURE__ */ jsx(View, { style: [{ padding: theme.space[4], paddingBottom: theme.space[2], gap: theme.space[1] }, style], children });
1651
+ return /* @__PURE__ */ jsx(
1652
+ View,
1653
+ {
1654
+ style: [
1655
+ { padding: theme.space[4], paddingBottom: theme.space[2], gap: theme.space[1] },
1656
+ style
1657
+ ],
1658
+ children
1659
+ }
1660
+ );
1494
1661
  }
1495
1662
  function CardTitle({ children, style }) {
1496
1663
  return /* @__PURE__ */ jsx(Heading, { level: 3, size: "md", style, children });
@@ -1589,6 +1756,7 @@ function Screen({
1589
1756
  style: [{ flex: 1, backgroundColor: theme.colors[bg] }, style],
1590
1757
  contentContainerStyle: [padding, contentContainerStyle],
1591
1758
  keyboardShouldPersistTaps: "handled",
1759
+ keyboardDismissMode: isIOS() ? "interactive" : "on-drag",
1592
1760
  refreshControl: onRefresh ? /* @__PURE__ */ jsx(
1593
1761
  RefreshControl,
1594
1762
  {
@@ -1613,21 +1781,30 @@ function ListRow({
1613
1781
  style
1614
1782
  }) {
1615
1783
  const theme = useTheme();
1784
+ const ios = isIOS();
1616
1785
  const content = /* @__PURE__ */ jsxs(Fragment, { children: [
1617
1786
  left,
1618
1787
  /* @__PURE__ */ jsxs(View, { style: { flex: 1, gap: theme.space[0.5] }, children: [
1619
1788
  /* @__PURE__ */ jsx(Text, { size: "md", numberOfLines: 1, children: title }),
1620
- subtitle ? /* @__PURE__ */ jsx(Text, { size: "sm", color: "textMuted", numberOfLines: 2, children: subtitle }) : null
1789
+ subtitle ? /* @__PURE__ */ jsx(Text, { size: "sm", color: ios ? "textSecondary" : "textMuted", numberOfLines: 2, children: subtitle }) : null
1621
1790
  ] }),
1622
- right ?? (onPress && !hideChevron ? /* @__PURE__ */ jsx(Icon, { icon: ChevronRightIcon, size: 18, color: "textMuted" }) : null)
1791
+ right ?? (onPress && !hideChevron ? /* @__PURE__ */ jsx(
1792
+ Icon,
1793
+ {
1794
+ icon: ChevronRightIcon,
1795
+ size: ios ? 16 : 18,
1796
+ strokeWidth: ios ? 2.5 : 2,
1797
+ color: "textMuted"
1798
+ }
1799
+ ) : null)
1623
1800
  ] });
1624
1801
  const layout = {
1625
1802
  flexDirection: "row",
1626
1803
  alignItems: "center",
1627
1804
  gap: theme.space[3],
1628
1805
  paddingHorizontal: theme.space[4],
1629
- paddingVertical: theme.space[3.5],
1630
- minHeight: 56,
1806
+ paddingVertical: ios ? theme.space[2.5] : theme.space[3.5],
1807
+ minHeight: ios ? 44 : 56,
1631
1808
  opacity: disabled ? theme.opacity.disabled : 1
1632
1809
  };
1633
1810
  if (!onPress) return /* @__PURE__ */ jsx(View, { style: [layout, style], children: content });
@@ -1709,6 +1886,7 @@ function Tabs({
1709
1886
  onChange
1710
1887
  );
1711
1888
  const isSegmented = variant === "segmented";
1889
+ const iosSegmented = isSegmented && isIOS();
1712
1890
  const tabs = items.map((item) => {
1713
1891
  const selected = item.value === value;
1714
1892
  return /* @__PURE__ */ jsxs(
@@ -1723,20 +1901,22 @@ function Tabs({
1723
1901
  alignItems: "center",
1724
1902
  justifyContent: "center",
1725
1903
  gap: theme.space[1.5],
1904
+ minHeight: iosSegmented ? 28 : void 0,
1726
1905
  paddingHorizontal: theme.space[3],
1727
- paddingVertical: isSegmented ? theme.space[2] : theme.space[3],
1728
- borderRadius: isSegmented ? theme.radius.md : 0,
1906
+ paddingVertical: iosSegmented ? theme.space[1] : isSegmented ? theme.space[2] : theme.space[3],
1907
+ borderRadius: iosSegmented ? theme.radius.md - 2 : isSegmented ? theme.radius.md : 0,
1729
1908
  backgroundColor: isSegmented && selected ? theme.colors.surface : "transparent",
1730
1909
  borderBottomWidth: isSegmented ? 0 : theme.borderWidth.medium,
1731
- borderBottomColor: selected ? theme.colors.primary : "transparent"
1910
+ borderBottomColor: selected ? theme.colors.primary : "transparent",
1911
+ ...iosSegmented && selected ? shadowStyle(theme.shadow.sm) : null
1732
1912
  },
1733
1913
  children: [
1734
1914
  /* @__PURE__ */ jsx(
1735
1915
  Text,
1736
1916
  {
1737
- size: "sm",
1917
+ size: iosSegmented ? "xs" : "sm",
1738
1918
  weight: selected ? "semibold" : "medium",
1739
- color: selected ? isSegmented ? "text" : "primaryText" : "textMuted",
1919
+ color: selected ? isSegmented ? "text" : "primaryText" : iosSegmented ? "text" : "textMuted",
1740
1920
  numberOfLines: 1,
1741
1921
  children: item.label
1742
1922
  }
@@ -1747,7 +1927,12 @@ function Tabs({
1747
1927
  item.value
1748
1928
  );
1749
1929
  });
1750
- const container = isSegmented ? {
1930
+ const container = iosSegmented ? {
1931
+ flexDirection: "row",
1932
+ padding: theme.space[0.5],
1933
+ borderRadius: theme.radius.md,
1934
+ backgroundColor: theme.colors.bgMuted
1935
+ } : isSegmented ? {
1751
1936
  flexDirection: "row",
1752
1937
  gap: theme.space[1],
1753
1938
  padding: theme.space[1],
@@ -1771,7 +1956,15 @@ function Tabs({
1771
1956
  }
1772
1957
  );
1773
1958
  }
1774
- return /* @__PURE__ */ jsx(View, { accessibilityRole: "tablist", accessibilityLabel, style: [container, style], children: tabs });
1959
+ return /* @__PURE__ */ jsx(
1960
+ View,
1961
+ {
1962
+ accessibilityRole: "tablist",
1963
+ accessibilityLabel,
1964
+ style: [container, style],
1965
+ children: tabs
1966
+ }
1967
+ );
1775
1968
  }
1776
1969
  function Accordion({
1777
1970
  items,
@@ -2220,6 +2413,7 @@ function ModalFooter({
2220
2413
  style
2221
2414
  }) {
2222
2415
  const theme = useTheme();
2416
+ const ios = isIOS();
2223
2417
  return /* @__PURE__ */ jsx(
2224
2418
  View,
2225
2419
  {
@@ -2229,8 +2423,10 @@ function ModalFooter({
2229
2423
  justifyContent: "flex-end",
2230
2424
  gap: theme.space[2],
2231
2425
  paddingHorizontal: theme.space[5],
2232
- paddingTop: theme.space[3],
2233
- paddingBottom: theme.space[5]
2426
+ paddingTop: ios ? theme.space[4] : theme.space[3],
2427
+ paddingBottom: theme.space[5],
2428
+ borderTopWidth: ios ? StyleSheet.hairlineWidth : 0,
2429
+ borderTopColor: theme.colors.border
2234
2430
  },
2235
2431
  style
2236
2432
  ],
@@ -2329,7 +2525,136 @@ function Drawer({
2329
2525
  }
2330
2526
  );
2331
2527
  }
2332
- function ActionSheet({
2528
+ function ActionSheet(props) {
2529
+ return isIOS() ? /* @__PURE__ */ jsx(IOSActionSheet, { ...props }) : /* @__PURE__ */ jsx(SheetActionSheet, { ...props });
2530
+ }
2531
+ function IOSActionSheet({
2532
+ open,
2533
+ onClose,
2534
+ title,
2535
+ description,
2536
+ actions,
2537
+ cancelLabel = "Cancel",
2538
+ style
2539
+ }) {
2540
+ const theme = useTheme();
2541
+ const insets = useSafeAreaInsets();
2542
+ const { height } = useWindowDimensions();
2543
+ const { translateY, backdrop } = useSheetTransition(open, height);
2544
+ const hasHeader = Boolean(title || description);
2545
+ const group = {
2546
+ borderRadius: theme.radius.xl,
2547
+ backgroundColor: theme.colors.surface,
2548
+ overflow: "hidden"
2549
+ };
2550
+ const separator = {
2551
+ borderTopWidth: StyleSheet.hairlineWidth,
2552
+ borderTopColor: theme.colors.border
2553
+ };
2554
+ const row = (pressed, leading) => ({
2555
+ minHeight: 56,
2556
+ flexDirection: "row",
2557
+ alignItems: "center",
2558
+ justifyContent: leading ? "flex-start" : "center",
2559
+ gap: theme.space[3],
2560
+ paddingHorizontal: theme.space[4],
2561
+ backgroundColor: pressed ? theme.colors.surfaceActive : "transparent"
2562
+ });
2563
+ return /* @__PURE__ */ jsx(
2564
+ Modal$1,
2565
+ {
2566
+ visible: open,
2567
+ transparent: true,
2568
+ animationType: "none",
2569
+ statusBarTranslucent: true,
2570
+ onRequestClose: onClose,
2571
+ children: /* @__PURE__ */ jsxs(View, { style: { flex: 1, justifyContent: "flex-end" }, children: [
2572
+ /* @__PURE__ */ jsx(
2573
+ Animated.View,
2574
+ {
2575
+ style: { flex: 1, backgroundColor: theme.colors.overlay, opacity: backdrop },
2576
+ children: /* @__PURE__ */ jsx(Pressable, { accessibilityLabel: "Close", onPress: onClose, style: { flex: 1 } })
2577
+ }
2578
+ ),
2579
+ /* @__PURE__ */ jsxs(
2580
+ Animated.View,
2581
+ {
2582
+ accessibilityViewIsModal: true,
2583
+ accessibilityLabel: title,
2584
+ style: [
2585
+ {
2586
+ paddingHorizontal: theme.space[2],
2587
+ paddingBottom: insets.bottom + theme.space[2],
2588
+ gap: theme.space[2],
2589
+ transform: [{ translateY }]
2590
+ },
2591
+ style
2592
+ ],
2593
+ children: [
2594
+ /* @__PURE__ */ jsxs(View, { style: group, accessibilityRole: "menu", children: [
2595
+ hasHeader ? /* @__PURE__ */ jsxs(
2596
+ View,
2597
+ {
2598
+ style: {
2599
+ alignItems: "center",
2600
+ gap: theme.space[1],
2601
+ paddingHorizontal: theme.space[4],
2602
+ paddingVertical: theme.space[3.5]
2603
+ },
2604
+ children: [
2605
+ title ? /* @__PURE__ */ jsx(Text, { size: "xs", weight: "semibold", color: "textSecondary", align: "center", children: title }) : null,
2606
+ description ? /* @__PURE__ */ jsx(Text, { size: "xs", color: "textSecondary", align: "center", children: description }) : null
2607
+ ]
2608
+ }
2609
+ ) : null,
2610
+ actions.map((action, index) => /* @__PURE__ */ jsxs(
2611
+ Pressable,
2612
+ {
2613
+ accessibilityRole: "menuitem",
2614
+ accessibilityState: { disabled: action.disabled },
2615
+ disabled: action.disabled,
2616
+ onPress: () => {
2617
+ onClose();
2618
+ action.onPress();
2619
+ },
2620
+ style: ({ pressed }) => [
2621
+ row(pressed, action.icon != null),
2622
+ index > 0 || hasHeader ? separator : null,
2623
+ action.disabled ? { opacity: theme.opacity.disabled } : null
2624
+ ],
2625
+ children: [
2626
+ action.icon,
2627
+ /* @__PURE__ */ jsx(
2628
+ Text,
2629
+ {
2630
+ size: "lg",
2631
+ color: action.destructive ? "dangerText" : "primaryText",
2632
+ numberOfLines: 1,
2633
+ children: action.label
2634
+ }
2635
+ )
2636
+ ]
2637
+ },
2638
+ action.label
2639
+ ))
2640
+ ] }),
2641
+ cancelLabel ? /* @__PURE__ */ jsx(
2642
+ Pressable,
2643
+ {
2644
+ accessibilityRole: "button",
2645
+ onPress: onClose,
2646
+ style: ({ pressed }) => [group, row(pressed, false)],
2647
+ children: /* @__PURE__ */ jsx(Text, { size: "lg", weight: "semibold", color: "primaryText", children: cancelLabel })
2648
+ }
2649
+ ) : null
2650
+ ]
2651
+ }
2652
+ )
2653
+ ] })
2654
+ }
2655
+ );
2656
+ }
2657
+ function SheetActionSheet({
2333
2658
  open,
2334
2659
  onClose,
2335
2660
  title,
@@ -2395,6 +2720,6 @@ function ActionSheet({
2395
2720
  ] });
2396
2721
  }
2397
2722
 
2398
- export { Accordion, ActionSheet, Avatar, Badge, BottomSheet, Box, Button, Calendar, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Carousel, Checkbox, Chip, Collapse, Container, Divider, Drawer, EmptyState, Flex, FormField, HStack, Heading, Icon, IconButton, Input, Label, ListRow, Modal, ModalBody, ModalFooter, ModalHeader, ModalTitle, Progress, Radio, RadioGroup, Screen, Section, Select, Skeleton, Spacer, Spinner, Stack, Switch, Tabs, Text, Textarea, ThemeProvider, ToastProvider, Tooltip, addDays, addMonths, dayKey, isSameDay, monthGrid, shadowStyle, sizeMetrics, splitStyleProps, startOfDay, startOfMonth, useColorMode, useColorModeValue, useTheme, useToast, variantColors };
2723
+ export { Accordion, ActionSheet, Avatar, Badge, BottomSheet, Box, Button, Calendar, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Carousel, Checkbox, Chip, Collapse, Container, Divider, Drawer, EmptyState, Flex, FormField, HStack, Heading, Icon, IconButton, Input, Label, ListRow, Modal, ModalBody, ModalFooter, ModalHeader, ModalTitle, Progress, Radio, RadioGroup, Screen, Section, Select, Skeleton, Spacer, Spinner, Stack, Switch, Tabs, Text, Textarea, ThemeProvider, ToastProvider, Tooltip, adaptThemeToPlatform, addDays, addMonths, dayKey, iosThemeOverride, isIOS, isSameDay, monthGrid, nativeFontFamily, pressFeedback, shadowStyle, sizeMetrics, splitStyleProps, startOfDay, startOfMonth, useColorMode, useColorModeValue, useTheme, useToast, variantColors };
2399
2724
  //# sourceMappingURL=index.js.map
2400
2725
  //# sourceMappingURL=index.js.map