@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.cjs CHANGED
@@ -14,6 +14,25 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
14
14
  var Svg__default = /*#__PURE__*/_interopDefault(Svg);
15
15
 
16
16
  // src/components/Box.tsx
17
+ function isIOS() {
18
+ return reactNative.Platform.OS === "ios";
19
+ }
20
+ var iosThemeOverride = {
21
+ typography: {
22
+ fontSize: { xs: 13, sm: 15, md: 17, lg: 20, xl: 22, "2xl": 28, "3xl": 34, "4xl": 40 }
23
+ },
24
+ radius: { sm: 6, md: 10, lg: 12, xl: 14, "2xl": 20 }
25
+ };
26
+ function adaptThemeToPlatform(theme) {
27
+ return isIOS() ? core.createTheme(theme, iosThemeOverride) : theme;
28
+ }
29
+ var IOS_PRESSED_OPACITY = 0.6;
30
+ var sheetSpring = {
31
+ stiffness: 320,
32
+ damping: 34,
33
+ mass: 1,
34
+ overshootClamping: true
35
+ };
17
36
  var ThemeContext = react.createContext(null);
18
37
  function ThemeProvider({
19
38
  theme = core.lightTheme,
@@ -21,6 +40,7 @@ function ThemeProvider({
21
40
  defaultColorMode = "system",
22
41
  colorMode: colorModeProp,
23
42
  onColorModeChange,
43
+ platformAdaptive = true,
24
44
  children
25
45
  }) {
26
46
  const [uncontrolledMode, setUncontrolledMode] = react.useState(defaultColorMode);
@@ -37,15 +57,20 @@ function ThemeProvider({
37
57
  const toggleColorMode = react.useCallback(() => {
38
58
  setColorMode(resolvedColorMode === "dark" ? "light" : "dark");
39
59
  }, [resolvedColorMode, setColorMode]);
60
+ const activeTheme = resolvedColorMode === "dark" ? darkTheme2 : theme;
61
+ const renderedTheme = react.useMemo(
62
+ () => platformAdaptive ? adaptThemeToPlatform(activeTheme) : activeTheme,
63
+ [activeTheme, platformAdaptive]
64
+ );
40
65
  const value = react.useMemo(
41
66
  () => ({
42
- theme: resolvedColorMode === "dark" ? darkTheme2 : theme,
67
+ theme: renderedTheme,
43
68
  colorMode,
44
69
  resolvedColorMode,
45
70
  setColorMode,
46
71
  toggleColorMode
47
72
  }),
48
- [theme, darkTheme2, colorMode, resolvedColorMode, setColorMode, toggleColorMode]
73
+ [renderedTheme, colorMode, resolvedColorMode, setColorMode, toggleColorMode]
49
74
  );
50
75
  return /* @__PURE__ */ jsxRuntime.jsx(ThemeContext.Provider, { value, children });
51
76
  }
@@ -204,6 +229,50 @@ function Divider({ orientation = "horizontal", color = "border", style }) {
204
229
  }
205
230
  );
206
231
  }
232
+ var SYSTEM_SANS = /* @__PURE__ */ new Set([
233
+ "-apple-system",
234
+ "blinkmacsystemfont",
235
+ "system-ui",
236
+ "ui-sans-serif",
237
+ "ui-rounded",
238
+ "sans-serif",
239
+ "system",
240
+ "roboto"
241
+ ]);
242
+ var SYSTEM_MONO = /* @__PURE__ */ new Set(["ui-monospace", "monospace"]);
243
+ var SYSTEM_SERIF = /* @__PURE__ */ new Set(["ui-serif", "serif"]);
244
+ var IOS_ONLY = /* @__PURE__ */ new Set([
245
+ "helvetica neue",
246
+ "georgia",
247
+ "menlo",
248
+ "times new roman",
249
+ "times",
250
+ "arial"
251
+ ]);
252
+ var NEVER_INSTALLED = /* @__PURE__ */ new Set([
253
+ "segoe ui",
254
+ "sfmono-regular",
255
+ "sf mono",
256
+ "liberation mono",
257
+ "consolas",
258
+ "cambria"
259
+ ]);
260
+ function nativeFontFamily(stack) {
261
+ if (!stack) return void 0;
262
+ const ios = reactNative.Platform.OS === "ios";
263
+ for (const raw of stack.split(",")) {
264
+ const name = raw.trim().replace(/^['"]|['"]$/g, "");
265
+ if (!name) continue;
266
+ const key = name.toLowerCase();
267
+ if (SYSTEM_SANS.has(key)) return void 0;
268
+ if (SYSTEM_MONO.has(key)) return ios ? "Menlo" : "monospace";
269
+ if (SYSTEM_SERIF.has(key)) return ios ? "Georgia" : "serif";
270
+ if (NEVER_INSTALLED.has(key)) continue;
271
+ if (!ios && IOS_ONLY.has(key)) continue;
272
+ return name;
273
+ }
274
+ return void 0;
275
+ }
207
276
  var Text = react.forwardRef(function Text2(props, ref) {
208
277
  const theme = useTheme();
209
278
  const {
@@ -219,13 +288,10 @@ var Text = react.forwardRef(function Text2(props, ref) {
219
288
  numberOfLines,
220
289
  ...restProps
221
290
  } = props;
222
- const { style: tokenStyle, rest } = splitStyleProps(
223
- restProps,
224
- theme
225
- );
291
+ const { style: tokenStyle, rest } = splitStyleProps(restProps, theme);
226
292
  const fontSize = theme.typography.fontSize[size];
227
293
  const textStyle = {
228
- fontFamily: theme.typography.fontFamily[family],
294
+ fontFamily: nativeFontFamily(theme.typography.fontFamily[family]),
229
295
  fontSize,
230
296
  color: theme.colors[color],
231
297
  fontWeight: weight ? theme.typography.fontWeight[weight] : void 0,
@@ -252,7 +318,8 @@ var defaultSize = {
252
318
  5: "md",
253
319
  6: "sm"
254
320
  };
255
- var Heading = react.forwardRef(function Heading2({ level = 2, size, weight = "semibold", leading = "tight", ...rest }, ref) {
321
+ var Heading = react.forwardRef(function Heading2({ level = 2, size, weight, leading = "tight", ...rest }, ref) {
322
+ const resolvedWeight = weight ?? (isIOS() && level <= 2 ? "bold" : "semibold");
256
323
  return /* @__PURE__ */ jsxRuntime.jsx(
257
324
  Text,
258
325
  {
@@ -260,7 +327,7 @@ var Heading = react.forwardRef(function Heading2({ level = 2, size, weight = "se
260
327
  accessibilityRole: "header",
261
328
  "aria-level": level,
262
329
  size: size ?? defaultSize[level],
263
- weight,
330
+ weight: resolvedWeight,
264
331
  leading,
265
332
  ...rest
266
333
  }
@@ -313,6 +380,7 @@ function Icon({
313
380
  // src/system/variants.ts
314
381
  function variantColors(variant, theme) {
315
382
  const { colors, borderWidth } = theme;
383
+ const ios = isIOS();
316
384
  switch (variant) {
317
385
  case "primary":
318
386
  return {
@@ -339,6 +407,13 @@ function variantColors(variant, theme) {
339
407
  foreground: "onDanger"
340
408
  };
341
409
  case "outline":
410
+ if (ios) {
411
+ return {
412
+ background: colors.primarySubtle,
413
+ backgroundPressed: colors.primarySubtleHover,
414
+ foreground: "primaryText"
415
+ };
416
+ }
342
417
  return {
343
418
  background: "transparent",
344
419
  backgroundPressed: colors.surfaceActive,
@@ -349,8 +424,8 @@ function variantColors(variant, theme) {
349
424
  case "ghost":
350
425
  return {
351
426
  background: "transparent",
352
- backgroundPressed: colors.surfaceActive,
353
- foreground: "text"
427
+ backgroundPressed: ios ? "transparent" : colors.surfaceActive,
428
+ foreground: ios ? "primaryText" : "text"
354
429
  };
355
430
  case "link":
356
431
  return {
@@ -361,6 +436,10 @@ function variantColors(variant, theme) {
361
436
  };
362
437
  }
363
438
  }
439
+ function pressFeedback(pressed, colors) {
440
+ if (!pressed) return { backgroundColor: colors.background, opacity: 1 };
441
+ return isIOS() ? { backgroundColor: colors.background, opacity: IOS_PRESSED_OPACITY } : { backgroundColor: colors.backgroundPressed, opacity: 1 };
442
+ }
364
443
  function sizeMetrics(size, theme) {
365
444
  switch (size) {
366
445
  case "sm":
@@ -408,6 +487,7 @@ var Button = react.forwardRef(function Button2({
408
487
  const metrics = sizeMetrics(size, theme);
409
488
  const isInert = disabled || loading;
410
489
  const isLink = variant === "link";
490
+ const slop = metrics.height < 44 ? (44 - metrics.height) / 2 : 0;
411
491
  return /* @__PURE__ */ jsxRuntime.jsxs(
412
492
  reactNative.Pressable,
413
493
  {
@@ -416,23 +496,27 @@ var Button = react.forwardRef(function Button2({
416
496
  accessibilityState: { disabled: isInert, busy: loading },
417
497
  accessibilityLabel,
418
498
  disabled: isInert,
419
- style: ({ pressed }) => [
420
- {
421
- flexDirection: "row",
422
- alignItems: "center",
423
- justifyContent: "center",
424
- gap: metrics.gap,
425
- height: isLink ? void 0 : metrics.height,
426
- paddingHorizontal: isLink ? 0 : metrics.paddingHorizontal,
427
- borderRadius: isLink ? 0 : theme.radius.md,
428
- backgroundColor: pressed ? colors.backgroundPressed : colors.background,
429
- borderColor: colors.borderColor,
430
- borderWidth: colors.borderWidth,
431
- alignSelf: fullWidth ? "stretch" : "flex-start",
432
- opacity: isInert ? theme.opacity.disabled : 1
433
- },
434
- style
435
- ],
499
+ hitSlop: slop > 0 ? { top: slop, bottom: slop } : void 0,
500
+ style: ({ pressed }) => {
501
+ const feedback = pressFeedback(pressed, colors);
502
+ return [
503
+ {
504
+ flexDirection: "row",
505
+ alignItems: "center",
506
+ justifyContent: "center",
507
+ gap: metrics.gap,
508
+ minHeight: isLink ? void 0 : metrics.height,
509
+ paddingHorizontal: isLink ? 0 : metrics.paddingHorizontal,
510
+ borderRadius: isLink ? 0 : size === "lg" ? theme.radius.lg : theme.radius.md,
511
+ backgroundColor: feedback.backgroundColor,
512
+ borderColor: colors.borderColor,
513
+ borderWidth: colors.borderWidth,
514
+ alignSelf: fullWidth ? "stretch" : "flex-start",
515
+ opacity: isInert ? theme.opacity.disabled : feedback.opacity
516
+ },
517
+ style
518
+ ];
519
+ },
436
520
  ...rest,
437
521
  children: [
438
522
  loading ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { size: "small", color: theme.colors[colors.foreground] }) : leftIcon ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { children: leftIcon }) : null,
@@ -474,20 +558,23 @@ var IconButton = react.forwardRef(function IconButton2({
474
558
  accessibilityState: { disabled: isInert, busy: loading },
475
559
  disabled: isInert,
476
560
  hitSlop: dimension < 44 ? (44 - dimension) / 2 : void 0,
477
- style: ({ pressed }) => [
478
- {
479
- width: dimension,
480
- height: dimension,
481
- alignItems: "center",
482
- justifyContent: "center",
483
- borderRadius: round ? theme.radius.full : theme.radius.md,
484
- backgroundColor: pressed ? colors.backgroundPressed : colors.background,
485
- borderColor: colors.borderColor,
486
- borderWidth: colors.borderWidth,
487
- opacity: isInert ? theme.opacity.disabled : 1
488
- },
489
- style
490
- ],
561
+ style: ({ pressed }) => {
562
+ const feedback = pressFeedback(pressed, colors);
563
+ return [
564
+ {
565
+ width: dimension,
566
+ height: dimension,
567
+ alignItems: "center",
568
+ justifyContent: "center",
569
+ borderRadius: round ? theme.radius.full : theme.radius.md,
570
+ backgroundColor: feedback.backgroundColor,
571
+ borderColor: colors.borderColor,
572
+ borderWidth: colors.borderWidth,
573
+ opacity: isInert ? theme.opacity.disabled : feedback.opacity
574
+ },
575
+ style
576
+ ];
577
+ },
491
578
  ...rest,
492
579
  children: loading ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { size: "small", color: theme.colors[colors.foreground] }) : icon
493
580
  }
@@ -505,9 +592,13 @@ var Input = react.forwardRef(function Input2({
505
592
  ...rest
506
593
  }, ref) {
507
594
  const theme = useTheme();
595
+ const { resolvedColorMode } = useColorMode();
508
596
  const metrics = sizeMetrics(size, theme);
509
597
  const [focused, setFocused] = react.useState(false);
598
+ const ios = isIOS();
510
599
  const borderColor = invalid ? theme.colors.danger : focused ? theme.colors.focusRing : theme.colors.border;
600
+ const borderWidth = ios ? focused || invalid ? theme.borderWidth.thin : 0 : focused || invalid ? theme.borderWidth.medium : theme.borderWidth.thin;
601
+ const backgroundColor = ios ? disabled ? theme.colors.bgSubtle : theme.colors.bgMuted : disabled ? theme.colors.bgMuted : theme.colors.surface;
511
602
  return /* @__PURE__ */ jsxRuntime.jsxs(
512
603
  reactNative.View,
513
604
  {
@@ -516,12 +607,12 @@ var Input = react.forwardRef(function Input2({
516
607
  flexDirection: "row",
517
608
  alignItems: "center",
518
609
  gap: metrics.gap,
519
- height: metrics.height,
610
+ minHeight: metrics.height,
520
611
  paddingHorizontal: metrics.paddingHorizontal,
521
612
  borderRadius: theme.radius.md,
522
- borderWidth: focused || invalid ? theme.borderWidth.medium : theme.borderWidth.thin,
613
+ borderWidth,
523
614
  borderColor,
524
- backgroundColor: disabled ? theme.colors.bgMuted : theme.colors.surface
615
+ backgroundColor
525
616
  },
526
617
  style
527
618
  ],
@@ -536,6 +627,8 @@ var Input = react.forwardRef(function Input2({
536
627
  "aria-disabled": disabled || void 0,
537
628
  placeholderTextColor: theme.colors.textMuted,
538
629
  selectionColor: theme.colors.primary,
630
+ keyboardAppearance: resolvedColorMode,
631
+ clearButtonMode: ios && !rightElement ? "while-editing" : void 0,
539
632
  onFocus: (event) => {
540
633
  setFocused(true);
541
634
  onFocus?.(event);
@@ -546,9 +639,9 @@ var Input = react.forwardRef(function Input2({
546
639
  },
547
640
  style: {
548
641
  flex: 1,
549
- // Android adds its own vertical padding; zero it so `height` wins.
642
+ // Android adds its own vertical padding; zero it so `minHeight` wins.
550
643
  paddingVertical: 0,
551
- fontFamily: theme.typography.fontFamily.sans,
644
+ fontFamily: nativeFontFamily(theme.typography.fontFamily.sans),
552
645
  fontSize: theme.typography.fontSize[metrics.fontSize],
553
646
  color: disabled ? theme.colors.textDisabled : theme.colors.text
554
647
  },
@@ -562,10 +655,12 @@ var Input = react.forwardRef(function Input2({
562
655
  });
563
656
  var Textarea = react.forwardRef(function Textarea2({ invalid = false, disabled = false, rows = 4, style, onFocus, onBlur, ...rest }, ref) {
564
657
  const theme = useTheme();
658
+ const { resolvedColorMode } = useColorMode();
565
659
  const [focused, setFocused] = react.useState(false);
566
- const lineHeight = Math.round(
567
- theme.typography.fontSize.md * theme.typography.lineHeight.normal
568
- );
660
+ const ios = isIOS();
661
+ const lineHeight = Math.round(theme.typography.fontSize.md * theme.typography.lineHeight.normal);
662
+ const borderWidth = ios ? focused || invalid ? theme.borderWidth.thin : 0 : focused || invalid ? theme.borderWidth.medium : theme.borderWidth.thin;
663
+ const backgroundColor = ios ? disabled ? theme.colors.bgSubtle : theme.colors.bgMuted : disabled ? theme.colors.bgMuted : theme.colors.surface;
569
664
  return /* @__PURE__ */ jsxRuntime.jsx(
570
665
  reactNative.TextInput,
571
666
  {
@@ -576,6 +671,7 @@ var Textarea = react.forwardRef(function Textarea2({ invalid = false, disabled =
576
671
  "aria-invalid": invalid || void 0,
577
672
  placeholderTextColor: theme.colors.textMuted,
578
673
  selectionColor: theme.colors.primary,
674
+ keyboardAppearance: resolvedColorMode,
579
675
  onFocus: (event) => {
580
676
  setFocused(true);
581
677
  onFocus?.(event);
@@ -589,10 +685,10 @@ var Textarea = react.forwardRef(function Textarea2({ invalid = false, disabled =
589
685
  minHeight: rows * lineHeight + theme.space[4],
590
686
  padding: theme.space[3],
591
687
  borderRadius: theme.radius.md,
592
- borderWidth: focused || invalid ? theme.borderWidth.medium : theme.borderWidth.thin,
688
+ borderWidth,
593
689
  borderColor: invalid ? theme.colors.danger : focused ? theme.colors.focusRing : theme.colors.border,
594
- backgroundColor: disabled ? theme.colors.bgMuted : theme.colors.surface,
595
- fontFamily: theme.typography.fontFamily.sans,
690
+ backgroundColor,
691
+ fontFamily: nativeFontFamily(theme.typography.fontFamily.sans),
596
692
  fontSize: theme.typography.fontSize.md,
597
693
  lineHeight,
598
694
  color: disabled ? theme.colors.textDisabled : theme.colors.text
@@ -649,13 +745,10 @@ function Checkbox({
649
745
  style
650
746
  }) {
651
747
  const theme = useTheme();
652
- const [checked, setChecked] = useControllableState(
653
- controlledChecked,
654
- defaultChecked,
655
- onChange
656
- );
748
+ const [checked, setChecked] = useControllableState(controlledChecked, defaultChecked, onChange);
657
749
  const dimension = boxSize2[size];
658
750
  const filled = checked || indeterminate;
751
+ const ios = isIOS();
659
752
  return /* @__PURE__ */ jsxRuntime.jsxs(
660
753
  reactNative.Pressable,
661
754
  {
@@ -685,7 +778,7 @@ function Checkbox({
685
778
  height: dimension,
686
779
  alignItems: "center",
687
780
  justifyContent: "center",
688
- borderRadius: theme.radius.sm,
781
+ borderRadius: ios ? theme.radius.full : theme.radius.sm,
689
782
  borderWidth: theme.borderWidth.medium,
690
783
  borderColor: invalid ? theme.colors.danger : filled ? theme.colors.primary : theme.colors.borderStrong,
691
784
  backgroundColor: filled ? theme.colors.primary : theme.colors.surface
@@ -701,7 +794,7 @@ function Checkbox({
701
794
  ) : null
702
795
  }
703
796
  ),
704
- children != null ? typeof children === "string" ? /* @__PURE__ */ jsxRuntime.jsx(Text, { size: size === "lg" ? "md" : "sm", style: { flexShrink: 1 }, children }) : children : null
797
+ children != null ? typeof children === "string" ? /* @__PURE__ */ jsxRuntime.jsx(Text, { size: size === "lg" || ios ? "md" : "sm", style: { flexShrink: 1 }, children }) : children : null
705
798
  ]
706
799
  }
707
800
  );
@@ -711,6 +804,7 @@ var track = {
711
804
  md: { width: 44, height: 26, thumb: 22 },
712
805
  lg: { width: 52, height: 32, thumb: 28 }
713
806
  };
807
+ var iosScale = { sm: 0.8, md: 1, lg: 1 };
714
808
  function Switch({
715
809
  checked: controlledChecked,
716
810
  defaultChecked = false,
@@ -721,11 +815,40 @@ function Switch({
721
815
  style
722
816
  }) {
723
817
  const theme = useTheme();
724
- const [checked, setChecked] = useControllableState(
725
- controlledChecked,
726
- defaultChecked,
727
- onChange
728
- );
818
+ const [checked, setChecked] = useControllableState(controlledChecked, defaultChecked, onChange);
819
+ const row = {
820
+ flexDirection: "row",
821
+ alignItems: "center",
822
+ justifyContent: "space-between",
823
+ gap: theme.space[3]
824
+ };
825
+ if (isIOS()) {
826
+ const label = typeof children === "string" ? children : void 0;
827
+ const scale = iosScale[size];
828
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [row, style], children: [
829
+ children != null ? label !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(
830
+ Text,
831
+ {
832
+ size: "md",
833
+ style: { flexShrink: 1, opacity: disabled ? theme.opacity.disabled : 1 },
834
+ children: label
835
+ }
836
+ ) : children : null,
837
+ /* @__PURE__ */ jsxRuntime.jsx(
838
+ reactNative.Switch,
839
+ {
840
+ value: checked,
841
+ onValueChange: setChecked,
842
+ disabled,
843
+ accessibilityLabel: label,
844
+ trackColor: { false: theme.colors.secondaryActive, true: theme.colors.primary },
845
+ thumbColor: theme.colors.onPrimary,
846
+ ios_backgroundColor: theme.colors.secondaryActive,
847
+ style: scale !== 1 ? { transform: [{ scale }] } : void 0
848
+ }
849
+ )
850
+ ] });
851
+ }
729
852
  const metrics = track[size];
730
853
  const inset = (metrics.height - metrics.thumb) / 2;
731
854
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -735,16 +858,7 @@ function Switch({
735
858
  accessibilityState: { checked, disabled },
736
859
  disabled,
737
860
  onPress: () => setChecked(!checked),
738
- style: [
739
- {
740
- flexDirection: "row",
741
- alignItems: "center",
742
- justifyContent: "space-between",
743
- gap: theme.space[3],
744
- opacity: disabled ? theme.opacity.disabled : 1
745
- },
746
- style
747
- ],
861
+ style: [row, { opacity: disabled ? theme.opacity.disabled : 1 }, style],
748
862
  children: [
749
863
  children != null ? typeof children === "string" ? /* @__PURE__ */ jsxRuntime.jsx(Text, { size: "sm", style: { flexShrink: 1 }, children }) : children : null,
750
864
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -871,11 +985,42 @@ function Radio({ value, disabled = false, children, style }) {
871
985
  ) : null
872
986
  }
873
987
  ),
874
- children != null ? typeof children === "string" ? /* @__PURE__ */ jsxRuntime.jsx(Text, { size: group.size === "lg" ? "md" : "sm", style: { flexShrink: 1 }, children }) : children : null
988
+ children != null ? typeof children === "string" ? /* @__PURE__ */ jsxRuntime.jsx(Text, { size: group.size === "lg" || isIOS() ? "md" : "sm", style: { flexShrink: 1 }, children }) : children : null
875
989
  ]
876
990
  }
877
991
  );
878
992
  }
993
+ function useSheetTransition(open, distance) {
994
+ const theme = useTheme();
995
+ const translateY = react.useRef(new reactNative.Animated.Value(distance)).current;
996
+ const backdrop = react.useRef(new reactNative.Animated.Value(0)).current;
997
+ react.useEffect(() => {
998
+ const animation = open ? reactNative.Animated.parallel([
999
+ reactNative.Animated.spring(translateY, { toValue: 0, ...sheetSpring, useNativeDriver: true }),
1000
+ reactNative.Animated.timing(backdrop, {
1001
+ toValue: 1,
1002
+ duration: theme.motion.duration.normal,
1003
+ easing: reactNative.Easing.out(reactNative.Easing.quad),
1004
+ useNativeDriver: true
1005
+ })
1006
+ ]) : reactNative.Animated.parallel([
1007
+ reactNative.Animated.timing(translateY, {
1008
+ toValue: distance,
1009
+ duration: theme.motion.duration.fast,
1010
+ easing: reactNative.Easing.in(reactNative.Easing.cubic),
1011
+ useNativeDriver: true
1012
+ }),
1013
+ reactNative.Animated.timing(backdrop, {
1014
+ toValue: 0,
1015
+ duration: theme.motion.duration.fast,
1016
+ useNativeDriver: true
1017
+ })
1018
+ ]);
1019
+ animation.start();
1020
+ return () => animation.stop();
1021
+ }, [open, distance, translateY, backdrop, theme.motion.duration]);
1022
+ return { translateY, backdrop };
1023
+ }
879
1024
  var DISMISS_THRESHOLD = 96;
880
1025
  function BottomSheet({
881
1026
  open,
@@ -890,16 +1035,9 @@ function BottomSheet({
890
1035
  const theme = useTheme();
891
1036
  const insets = reactNativeSafeAreaContext.useSafeAreaInsets();
892
1037
  const { height } = reactNative.useWindowDimensions();
893
- const translateY = react.useRef(new reactNative.Animated.Value(height)).current;
1038
+ const { translateY, backdrop } = useSheetTransition(open, height);
894
1039
  const dragY = react.useRef(0);
895
- react.useEffect(() => {
896
- reactNative.Animated.timing(translateY, {
897
- toValue: open ? 0 : height,
898
- duration: open ? theme.motion.duration.normal : theme.motion.duration.fast,
899
- easing: reactNative.Easing.out(reactNative.Easing.cubic),
900
- useNativeDriver: true
901
- }).start();
902
- }, [open, height, translateY, theme.motion.duration]);
1040
+ const ios = isIOS();
903
1041
  const panResponder = react.useRef(
904
1042
  reactNative.PanResponder.create({
905
1043
  onMoveShouldSetPanResponder: (_event, gesture) => gesture.dy > 4,
@@ -913,7 +1051,11 @@ function BottomSheet({
913
1051
  if (dragY.current > DISMISS_THRESHOLD) {
914
1052
  onClose();
915
1053
  } else {
916
- reactNative.Animated.spring(translateY, { toValue: 0, useNativeDriver: true }).start();
1054
+ reactNative.Animated.spring(translateY, {
1055
+ toValue: 0,
1056
+ ...sheetSpring,
1057
+ useNativeDriver: true
1058
+ }).start();
917
1059
  }
918
1060
  dragY.current = 0;
919
1061
  }
@@ -929,11 +1071,17 @@ function BottomSheet({
929
1071
  onRequestClose: onClose,
930
1072
  children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { flex: 1, justifyContent: "flex-end" }, children: [
931
1073
  /* @__PURE__ */ jsxRuntime.jsx(
932
- reactNative.Pressable,
1074
+ reactNative.Animated.View,
933
1075
  {
934
- accessibilityLabel: closeOnOverlayPress ? "Close" : void 0,
935
- onPress: closeOnOverlayPress ? onClose : void 0,
936
- style: { flex: 1, backgroundColor: theme.colors.overlay }
1076
+ style: { flex: 1, backgroundColor: theme.colors.overlay, opacity: backdrop },
1077
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1078
+ reactNative.Pressable,
1079
+ {
1080
+ accessibilityLabel: closeOnOverlayPress ? "Close" : void 0,
1081
+ onPress: closeOnOverlayPress ? onClose : void 0,
1082
+ style: { flex: 1 }
1083
+ }
1084
+ )
937
1085
  }
938
1086
  ),
939
1087
  /* @__PURE__ */ jsxRuntime.jsxs(
@@ -953,32 +1101,39 @@ function BottomSheet({
953
1101
  style
954
1102
  ],
955
1103
  children: [
956
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { ...panResponder.panHandlers, style: { paddingTop: theme.space[3] }, children: [
957
- /* @__PURE__ */ jsxRuntime.jsx(
958
- reactNative.View,
959
- {
960
- style: {
961
- alignSelf: "center",
962
- width: 40,
963
- height: 4,
964
- borderRadius: theme.radius.full,
965
- backgroundColor: theme.colors.borderStrong
966
- }
967
- }
968
- ),
969
- title ? /* @__PURE__ */ jsxRuntime.jsx(
970
- Heading,
971
- {
972
- level: 3,
973
- size: "lg",
974
- style: {
975
- paddingHorizontal: theme.space[5],
976
- paddingTop: theme.space[4]
977
- },
978
- children: title
979
- }
980
- ) : null
981
- ] }),
1104
+ /* @__PURE__ */ jsxRuntime.jsxs(
1105
+ reactNative.View,
1106
+ {
1107
+ ...panResponder.panHandlers,
1108
+ style: { paddingTop: ios ? theme.space[1.5] : theme.space[3] },
1109
+ children: [
1110
+ /* @__PURE__ */ jsxRuntime.jsx(
1111
+ reactNative.View,
1112
+ {
1113
+ style: {
1114
+ alignSelf: "center",
1115
+ width: ios ? 36 : 40,
1116
+ height: ios ? 5 : 4,
1117
+ borderRadius: theme.radius.full,
1118
+ backgroundColor: theme.colors.borderStrong
1119
+ }
1120
+ }
1121
+ ),
1122
+ title ? /* @__PURE__ */ jsxRuntime.jsx(
1123
+ Heading,
1124
+ {
1125
+ level: 3,
1126
+ size: "lg",
1127
+ style: {
1128
+ paddingHorizontal: theme.space[5],
1129
+ paddingTop: theme.space[4]
1130
+ },
1131
+ children: title
1132
+ }
1133
+ ) : null
1134
+ ]
1135
+ }
1136
+ ),
982
1137
  children
983
1138
  ]
984
1139
  }
@@ -1003,6 +1158,7 @@ function Select({
1003
1158
  const metrics = sizeMetrics(size, theme);
1004
1159
  const [open, setOpen] = react.useState(false);
1005
1160
  const selected = options.find((option) => option.value === value);
1161
+ const ios = isIOS();
1006
1162
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1007
1163
  /* @__PURE__ */ jsxRuntime.jsxs(
1008
1164
  reactNative.Pressable,
@@ -1020,12 +1176,13 @@ function Select({
1020
1176
  alignItems: "center",
1021
1177
  justifyContent: "space-between",
1022
1178
  gap: metrics.gap,
1023
- height: metrics.height,
1179
+ minHeight: metrics.height,
1024
1180
  paddingHorizontal: metrics.paddingHorizontal,
1025
1181
  borderRadius: theme.radius.md,
1026
- borderWidth: invalid ? theme.borderWidth.medium : theme.borderWidth.thin,
1182
+ // Mirrors Input: iOS is a filled rect that only gains a stroke when invalid.
1183
+ borderWidth: invalid ? ios ? theme.borderWidth.thin : theme.borderWidth.medium : ios ? 0 : theme.borderWidth.thin,
1027
1184
  borderColor: invalid ? theme.colors.danger : theme.colors.border,
1028
- backgroundColor: disabled ? theme.colors.bgMuted : theme.colors.surface,
1185
+ backgroundColor: ios ? disabled ? theme.colors.bgSubtle : theme.colors.bgMuted : disabled ? theme.colors.bgMuted : theme.colors.surface,
1029
1186
  opacity: disabled ? theme.opacity.disabled : 1
1030
1187
  },
1031
1188
  style
@@ -1249,6 +1406,7 @@ function ToastCard({
1249
1406
  const theme = useTheme();
1250
1407
  const tone = entry.tone ?? "neutral";
1251
1408
  const enter = react.useRef(new reactNative.Animated.Value(0)).current;
1409
+ const ios = isIOS();
1252
1410
  react.useEffect(() => {
1253
1411
  reactNative.Animated.timing(enter, {
1254
1412
  toValue: 1,
@@ -1273,8 +1431,8 @@ function ToastCard({
1273
1431
  alignItems: "flex-start",
1274
1432
  gap: theme.space[3],
1275
1433
  padding: theme.space[3.5],
1276
- borderRadius: theme.radius.lg,
1277
- borderWidth: theme.borderWidth.thin,
1434
+ borderRadius: ios ? theme.radius.xl : theme.radius.lg,
1435
+ borderWidth: ios ? 0 : theme.borderWidth.thin,
1278
1436
  borderColor: theme.colors.border,
1279
1437
  backgroundColor: theme.colors.surface,
1280
1438
  opacity: enter,
@@ -1470,7 +1628,7 @@ var Card = react.forwardRef(function Card2({ variant = "outline", onPress, acces
1470
1628
  const base = {
1471
1629
  borderRadius: theme.radius.lg,
1472
1630
  backgroundColor: variant === "filled" ? theme.colors.bgSubtle : theme.colors.surface,
1473
- borderWidth: variant === "outline" ? theme.borderWidth.thin : 0,
1631
+ borderWidth: variant === "outline" ? isIOS() ? reactNative.StyleSheet.hairlineWidth : theme.borderWidth.thin : 0,
1474
1632
  borderColor: theme.colors.border,
1475
1633
  ...variant === "elevated" ? shadowStyle(theme.shadow.sm) : null
1476
1634
  };
@@ -1495,7 +1653,16 @@ var Card = react.forwardRef(function Card2({ variant = "outline", onPress, acces
1495
1653
  });
1496
1654
  function CardHeader({ children, style }) {
1497
1655
  const theme = useTheme();
1498
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [{ padding: theme.space[4], paddingBottom: theme.space[2], gap: theme.space[1] }, style], children });
1656
+ return /* @__PURE__ */ jsxRuntime.jsx(
1657
+ reactNative.View,
1658
+ {
1659
+ style: [
1660
+ { padding: theme.space[4], paddingBottom: theme.space[2], gap: theme.space[1] },
1661
+ style
1662
+ ],
1663
+ children
1664
+ }
1665
+ );
1499
1666
  }
1500
1667
  function CardTitle({ children, style }) {
1501
1668
  return /* @__PURE__ */ jsxRuntime.jsx(Heading, { level: 3, size: "md", style, children });
@@ -1594,6 +1761,7 @@ function Screen({
1594
1761
  style: [{ flex: 1, backgroundColor: theme.colors[bg] }, style],
1595
1762
  contentContainerStyle: [padding, contentContainerStyle],
1596
1763
  keyboardShouldPersistTaps: "handled",
1764
+ keyboardDismissMode: isIOS() ? "interactive" : "on-drag",
1597
1765
  refreshControl: onRefresh ? /* @__PURE__ */ jsxRuntime.jsx(
1598
1766
  reactNative.RefreshControl,
1599
1767
  {
@@ -1618,21 +1786,30 @@ function ListRow({
1618
1786
  style
1619
1787
  }) {
1620
1788
  const theme = useTheme();
1789
+ const ios = isIOS();
1621
1790
  const content = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1622
1791
  left,
1623
1792
  /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { flex: 1, gap: theme.space[0.5] }, children: [
1624
1793
  /* @__PURE__ */ jsxRuntime.jsx(Text, { size: "md", numberOfLines: 1, children: title }),
1625
- subtitle ? /* @__PURE__ */ jsxRuntime.jsx(Text, { size: "sm", color: "textMuted", numberOfLines: 2, children: subtitle }) : null
1794
+ subtitle ? /* @__PURE__ */ jsxRuntime.jsx(Text, { size: "sm", color: ios ? "textSecondary" : "textMuted", numberOfLines: 2, children: subtitle }) : null
1626
1795
  ] }),
1627
- right ?? (onPress && !hideChevron ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon: icons.ChevronRightIcon, size: 18, color: "textMuted" }) : null)
1796
+ right ?? (onPress && !hideChevron ? /* @__PURE__ */ jsxRuntime.jsx(
1797
+ Icon,
1798
+ {
1799
+ icon: icons.ChevronRightIcon,
1800
+ size: ios ? 16 : 18,
1801
+ strokeWidth: ios ? 2.5 : 2,
1802
+ color: "textMuted"
1803
+ }
1804
+ ) : null)
1628
1805
  ] });
1629
1806
  const layout = {
1630
1807
  flexDirection: "row",
1631
1808
  alignItems: "center",
1632
1809
  gap: theme.space[3],
1633
1810
  paddingHorizontal: theme.space[4],
1634
- paddingVertical: theme.space[3.5],
1635
- minHeight: 56,
1811
+ paddingVertical: ios ? theme.space[2.5] : theme.space[3.5],
1812
+ minHeight: ios ? 44 : 56,
1636
1813
  opacity: disabled ? theme.opacity.disabled : 1
1637
1814
  };
1638
1815
  if (!onPress) return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [layout, style], children: content });
@@ -1714,6 +1891,7 @@ function Tabs({
1714
1891
  onChange
1715
1892
  );
1716
1893
  const isSegmented = variant === "segmented";
1894
+ const iosSegmented = isSegmented && isIOS();
1717
1895
  const tabs = items.map((item) => {
1718
1896
  const selected = item.value === value;
1719
1897
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -1728,20 +1906,22 @@ function Tabs({
1728
1906
  alignItems: "center",
1729
1907
  justifyContent: "center",
1730
1908
  gap: theme.space[1.5],
1909
+ minHeight: iosSegmented ? 28 : void 0,
1731
1910
  paddingHorizontal: theme.space[3],
1732
- paddingVertical: isSegmented ? theme.space[2] : theme.space[3],
1733
- borderRadius: isSegmented ? theme.radius.md : 0,
1911
+ paddingVertical: iosSegmented ? theme.space[1] : isSegmented ? theme.space[2] : theme.space[3],
1912
+ borderRadius: iosSegmented ? theme.radius.md - 2 : isSegmented ? theme.radius.md : 0,
1734
1913
  backgroundColor: isSegmented && selected ? theme.colors.surface : "transparent",
1735
1914
  borderBottomWidth: isSegmented ? 0 : theme.borderWidth.medium,
1736
- borderBottomColor: selected ? theme.colors.primary : "transparent"
1915
+ borderBottomColor: selected ? theme.colors.primary : "transparent",
1916
+ ...iosSegmented && selected ? shadowStyle(theme.shadow.sm) : null
1737
1917
  },
1738
1918
  children: [
1739
1919
  /* @__PURE__ */ jsxRuntime.jsx(
1740
1920
  Text,
1741
1921
  {
1742
- size: "sm",
1922
+ size: iosSegmented ? "xs" : "sm",
1743
1923
  weight: selected ? "semibold" : "medium",
1744
- color: selected ? isSegmented ? "text" : "primaryText" : "textMuted",
1924
+ color: selected ? isSegmented ? "text" : "primaryText" : iosSegmented ? "text" : "textMuted",
1745
1925
  numberOfLines: 1,
1746
1926
  children: item.label
1747
1927
  }
@@ -1752,7 +1932,12 @@ function Tabs({
1752
1932
  item.value
1753
1933
  );
1754
1934
  });
1755
- const container = isSegmented ? {
1935
+ const container = iosSegmented ? {
1936
+ flexDirection: "row",
1937
+ padding: theme.space[0.5],
1938
+ borderRadius: theme.radius.md,
1939
+ backgroundColor: theme.colors.bgMuted
1940
+ } : isSegmented ? {
1756
1941
  flexDirection: "row",
1757
1942
  gap: theme.space[1],
1758
1943
  padding: theme.space[1],
@@ -1776,7 +1961,15 @@ function Tabs({
1776
1961
  }
1777
1962
  );
1778
1963
  }
1779
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { accessibilityRole: "tablist", accessibilityLabel, style: [container, style], children: tabs });
1964
+ return /* @__PURE__ */ jsxRuntime.jsx(
1965
+ reactNative.View,
1966
+ {
1967
+ accessibilityRole: "tablist",
1968
+ accessibilityLabel,
1969
+ style: [container, style],
1970
+ children: tabs
1971
+ }
1972
+ );
1780
1973
  }
1781
1974
  function Accordion({
1782
1975
  items,
@@ -2225,6 +2418,7 @@ function ModalFooter({
2225
2418
  style
2226
2419
  }) {
2227
2420
  const theme = useTheme();
2421
+ const ios = isIOS();
2228
2422
  return /* @__PURE__ */ jsxRuntime.jsx(
2229
2423
  reactNative.View,
2230
2424
  {
@@ -2234,8 +2428,10 @@ function ModalFooter({
2234
2428
  justifyContent: "flex-end",
2235
2429
  gap: theme.space[2],
2236
2430
  paddingHorizontal: theme.space[5],
2237
- paddingTop: theme.space[3],
2238
- paddingBottom: theme.space[5]
2431
+ paddingTop: ios ? theme.space[4] : theme.space[3],
2432
+ paddingBottom: theme.space[5],
2433
+ borderTopWidth: ios ? reactNative.StyleSheet.hairlineWidth : 0,
2434
+ borderTopColor: theme.colors.border
2239
2435
  },
2240
2436
  style
2241
2437
  ],
@@ -2334,7 +2530,136 @@ function Drawer({
2334
2530
  }
2335
2531
  );
2336
2532
  }
2337
- function ActionSheet({
2533
+ function ActionSheet(props) {
2534
+ return isIOS() ? /* @__PURE__ */ jsxRuntime.jsx(IOSActionSheet, { ...props }) : /* @__PURE__ */ jsxRuntime.jsx(SheetActionSheet, { ...props });
2535
+ }
2536
+ function IOSActionSheet({
2537
+ open,
2538
+ onClose,
2539
+ title,
2540
+ description,
2541
+ actions,
2542
+ cancelLabel = "Cancel",
2543
+ style
2544
+ }) {
2545
+ const theme = useTheme();
2546
+ const insets = reactNativeSafeAreaContext.useSafeAreaInsets();
2547
+ const { height } = reactNative.useWindowDimensions();
2548
+ const { translateY, backdrop } = useSheetTransition(open, height);
2549
+ const hasHeader = Boolean(title || description);
2550
+ const group = {
2551
+ borderRadius: theme.radius.xl,
2552
+ backgroundColor: theme.colors.surface,
2553
+ overflow: "hidden"
2554
+ };
2555
+ const separator = {
2556
+ borderTopWidth: reactNative.StyleSheet.hairlineWidth,
2557
+ borderTopColor: theme.colors.border
2558
+ };
2559
+ const row = (pressed, leading) => ({
2560
+ minHeight: 56,
2561
+ flexDirection: "row",
2562
+ alignItems: "center",
2563
+ justifyContent: leading ? "flex-start" : "center",
2564
+ gap: theme.space[3],
2565
+ paddingHorizontal: theme.space[4],
2566
+ backgroundColor: pressed ? theme.colors.surfaceActive : "transparent"
2567
+ });
2568
+ return /* @__PURE__ */ jsxRuntime.jsx(
2569
+ reactNative.Modal,
2570
+ {
2571
+ visible: open,
2572
+ transparent: true,
2573
+ animationType: "none",
2574
+ statusBarTranslucent: true,
2575
+ onRequestClose: onClose,
2576
+ children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { flex: 1, justifyContent: "flex-end" }, children: [
2577
+ /* @__PURE__ */ jsxRuntime.jsx(
2578
+ reactNative.Animated.View,
2579
+ {
2580
+ style: { flex: 1, backgroundColor: theme.colors.overlay, opacity: backdrop },
2581
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { accessibilityLabel: "Close", onPress: onClose, style: { flex: 1 } })
2582
+ }
2583
+ ),
2584
+ /* @__PURE__ */ jsxRuntime.jsxs(
2585
+ reactNative.Animated.View,
2586
+ {
2587
+ accessibilityViewIsModal: true,
2588
+ accessibilityLabel: title,
2589
+ style: [
2590
+ {
2591
+ paddingHorizontal: theme.space[2],
2592
+ paddingBottom: insets.bottom + theme.space[2],
2593
+ gap: theme.space[2],
2594
+ transform: [{ translateY }]
2595
+ },
2596
+ style
2597
+ ],
2598
+ children: [
2599
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: group, accessibilityRole: "menu", children: [
2600
+ hasHeader ? /* @__PURE__ */ jsxRuntime.jsxs(
2601
+ reactNative.View,
2602
+ {
2603
+ style: {
2604
+ alignItems: "center",
2605
+ gap: theme.space[1],
2606
+ paddingHorizontal: theme.space[4],
2607
+ paddingVertical: theme.space[3.5]
2608
+ },
2609
+ children: [
2610
+ title ? /* @__PURE__ */ jsxRuntime.jsx(Text, { size: "xs", weight: "semibold", color: "textSecondary", align: "center", children: title }) : null,
2611
+ description ? /* @__PURE__ */ jsxRuntime.jsx(Text, { size: "xs", color: "textSecondary", align: "center", children: description }) : null
2612
+ ]
2613
+ }
2614
+ ) : null,
2615
+ actions.map((action, index) => /* @__PURE__ */ jsxRuntime.jsxs(
2616
+ reactNative.Pressable,
2617
+ {
2618
+ accessibilityRole: "menuitem",
2619
+ accessibilityState: { disabled: action.disabled },
2620
+ disabled: action.disabled,
2621
+ onPress: () => {
2622
+ onClose();
2623
+ action.onPress();
2624
+ },
2625
+ style: ({ pressed }) => [
2626
+ row(pressed, action.icon != null),
2627
+ index > 0 || hasHeader ? separator : null,
2628
+ action.disabled ? { opacity: theme.opacity.disabled } : null
2629
+ ],
2630
+ children: [
2631
+ action.icon,
2632
+ /* @__PURE__ */ jsxRuntime.jsx(
2633
+ Text,
2634
+ {
2635
+ size: "lg",
2636
+ color: action.destructive ? "dangerText" : "primaryText",
2637
+ numberOfLines: 1,
2638
+ children: action.label
2639
+ }
2640
+ )
2641
+ ]
2642
+ },
2643
+ action.label
2644
+ ))
2645
+ ] }),
2646
+ cancelLabel ? /* @__PURE__ */ jsxRuntime.jsx(
2647
+ reactNative.Pressable,
2648
+ {
2649
+ accessibilityRole: "button",
2650
+ onPress: onClose,
2651
+ style: ({ pressed }) => [group, row(pressed, false)],
2652
+ children: /* @__PURE__ */ jsxRuntime.jsx(Text, { size: "lg", weight: "semibold", color: "primaryText", children: cancelLabel })
2653
+ }
2654
+ ) : null
2655
+ ]
2656
+ }
2657
+ )
2658
+ ] })
2659
+ }
2660
+ );
2661
+ }
2662
+ function SheetActionSheet({
2338
2663
  open,
2339
2664
  onClose,
2340
2665
  title,
@@ -2469,11 +2794,16 @@ exports.Textarea = Textarea;
2469
2794
  exports.ThemeProvider = ThemeProvider;
2470
2795
  exports.ToastProvider = ToastProvider;
2471
2796
  exports.Tooltip = Tooltip;
2797
+ exports.adaptThemeToPlatform = adaptThemeToPlatform;
2472
2798
  exports.addDays = addDays;
2473
2799
  exports.addMonths = addMonths;
2474
2800
  exports.dayKey = dayKey;
2801
+ exports.iosThemeOverride = iosThemeOverride;
2802
+ exports.isIOS = isIOS;
2475
2803
  exports.isSameDay = isSameDay;
2476
2804
  exports.monthGrid = monthGrid;
2805
+ exports.nativeFontFamily = nativeFontFamily;
2806
+ exports.pressFeedback = pressFeedback;
2477
2807
  exports.shadowStyle = shadowStyle;
2478
2808
  exports.sizeMetrics = sizeMetrics;
2479
2809
  exports.splitStyleProps = splitStyleProps;