@rific/auto-paper 0.6.4 → 0.8.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.mjs CHANGED
@@ -61,11 +61,19 @@ var Appbar = {
61
61
  import { SegmentedButtons } from "react-native-paper";
62
62
  import { jsx as jsx2 } from "react/jsx-runtime";
63
63
  var APPEARANCES = [
64
- { label: "System", value: "system", icon: "brightness-auto" },
65
- { label: "Light", value: "light", icon: "white-balance-sunny" },
66
- { label: "Dark", value: "dark", icon: "weather-night" }
64
+ { label: "System", value: "system" },
65
+ { label: "Light", value: "light" },
66
+ { label: "Dark", value: "dark" }
67
67
  ];
68
- var AppearancePicker = ({ value, onChange }) => /* @__PURE__ */ jsx2(SegmentedButtons, { value, onValueChange: (v) => onChange(v), buttons: APPEARANCES.map((a) => ({ value: a.value, label: a.label, icon: a.icon })) });
68
+ var DEFAULT_ICONS = {
69
+ system: "monitor",
70
+ light: "white-balance-sunny",
71
+ dark: "weather-night"
72
+ };
73
+ var AppearancePicker = ({ value, onChange, showLabels = true, icons }) => {
74
+ const resolvedIcons = { ...DEFAULT_ICONS, ...icons };
75
+ return /* @__PURE__ */ jsx2(SegmentedButtons, { value, onValueChange: (v) => onChange(v), buttons: APPEARANCES.map((a) => ({ value: a.value, label: showLabels ? a.label : void 0, icon: resolvedIcons[a.value], accessibilityLabel: a.label })) });
76
+ };
69
77
 
70
78
  // src/components/BlurView.tsx
71
79
  import { useContext as useContext3 } from "react";
@@ -95,7 +103,7 @@ var BlurView = ({ blur = true, children, elevation, style, tintColor, tintOpacit
95
103
  tint,
96
104
  children
97
105
  ] });
98
- return /* @__PURE__ */ jsxs2(View, { style: [{ backgroundColor: surfaceColor }, style], children: [
106
+ return /* @__PURE__ */ jsxs2(View, { ...props, style: [{ backgroundColor: surfaceColor }, style], children: [
99
107
  tint,
100
108
  children
101
109
  ] });
@@ -407,6 +415,27 @@ var getTonalColor = (color, lightness) => {
407
415
  return getHex(`rgb(${toInt(r2)}, ${toInt(g2)}, ${toInt(b2)})`) ?? color;
408
416
  };
409
417
 
418
+ // src/utils/isDarkColor.ts
419
+ var isDarkColor = (value) => {
420
+ const rgb = getRgb(value);
421
+ if (!rgb) return false;
422
+ const [r, g, b] = [rgb.r / 255, rgb.g / 255, rgb.b / 255];
423
+ const [rs, gs, bs] = [r, g, b].map((c) => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
424
+ const luminance = 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
425
+ return luminance < 0.5;
426
+ };
427
+
428
+ // src/utils/getColorRoles.ts
429
+ var getColorRoles = (base, surface, containerAlpha = 0.15) => {
430
+ const container = getBlendedColor(base, surface, containerAlpha);
431
+ return {
432
+ color: base,
433
+ onColor: getTonalColor(base, isDarkColor(base) ? 0.95 : 0.08),
434
+ container,
435
+ onContainer: getTonalColor(base, isDarkColor(container) ? 0.92 : 0.12)
436
+ };
437
+ };
438
+
410
439
  // src/utils/getTriadicPalette.ts
411
440
  var HARMONY_OFFSETS = {
412
441
  analogous: [1 / 12, 2 / 12],
@@ -488,17 +517,12 @@ function hslToHex(h, s, l) {
488
517
  return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
489
518
  }
490
519
 
491
- // src/utils/isDarkColor.ts
492
- var isDarkColor = (value) => {
493
- const rgb = getRgb(value);
494
- if (!rgb) return false;
495
- const [r, g, b] = [rgb.r / 255, rgb.g / 255, rgb.b / 255];
496
- const [rs, gs, bs] = [r, g, b].map((c) => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
497
- const luminance = 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
498
- return luminance < 0.5;
499
- };
500
-
501
520
  // src/useComputedTheme.ts
521
+ var SEMANTIC_BASE_COLORS = {
522
+ success: "#2E7D32",
523
+ warning: "#A15C00",
524
+ danger: "#B00020"
525
+ };
502
526
  function useComputedTheme(appearance, color, harmony = "split-complementary") {
503
527
  const [theme, setTheme] = useState(null);
504
528
  const computeTheme = useCallback(() => {
@@ -506,27 +530,45 @@ function useComputedTheme(appearance, color, harmony = "split-complementary") {
506
530
  if (resolved === "system") resolved = Appearance.getColorScheme();
507
531
  if (resolved === "no-preference" || !resolved) resolved = "light";
508
532
  const base = resolved === "dark" ? MD3DarkTheme : MD3LightTheme;
533
+ const surface = base.colors.surface;
534
+ const successRoles = getColorRoles(SEMANTIC_BASE_COLORS.success, surface);
535
+ const warningRoles = getColorRoles(SEMANTIC_BASE_COLORS.warning, surface);
536
+ const dangerRoles = getColorRoles(SEMANTIC_BASE_COLORS.danger, surface);
509
537
  const _theme = {
510
538
  ...base,
511
539
  colors: {
512
540
  ...base.colors,
513
- elevation: { ...base.colors.elevation }
541
+ elevation: { ...base.colors.elevation },
542
+ success: successRoles.color,
543
+ onSuccess: successRoles.onColor,
544
+ successContainer: successRoles.container,
545
+ onSuccessContainer: successRoles.onContainer,
546
+ warning: warningRoles.color,
547
+ onWarning: warningRoles.onColor,
548
+ warningContainer: warningRoles.container,
549
+ onWarningContainer: warningRoles.onContainer,
550
+ danger: dangerRoles.color,
551
+ onDanger: dangerRoles.onColor,
552
+ dangerContainer: dangerRoles.container,
553
+ onDangerContainer: dangerRoles.onContainer
514
554
  }
515
555
  };
516
556
  const palette = getTriadicPalette(color, harmony);
517
- const surface = base.colors.surface;
518
- _theme.colors.primary = palette.primary;
519
- _theme.colors.onPrimary = getTonalColor(palette.primary, isDarkColor(palette.primary) ? 0.95 : 0.08);
520
- _theme.colors.primaryContainer = getBlendedColor(palette.primary, surface, 0.15);
521
- _theme.colors.onPrimaryContainer = getTonalColor(palette.primary, isDarkColor(_theme.colors.primaryContainer) ? 0.92 : 0.12);
522
- _theme.colors.secondary = palette.secondary;
523
- _theme.colors.onSecondary = getTonalColor(palette.secondary, isDarkColor(palette.secondary) ? 0.95 : 0.08);
524
- _theme.colors.secondaryContainer = getBlendedColor(palette.secondary, surface, 0.15);
525
- _theme.colors.onSecondaryContainer = getTonalColor(palette.secondary, isDarkColor(_theme.colors.secondaryContainer) ? 0.92 : 0.12);
526
- _theme.colors.tertiary = palette.tertiary;
527
- _theme.colors.onTertiary = getTonalColor(palette.tertiary, isDarkColor(palette.tertiary) ? 0.95 : 0.08);
528
- _theme.colors.tertiaryContainer = getBlendedColor(palette.tertiary, surface, 0.15);
529
- _theme.colors.onTertiaryContainer = getTonalColor(palette.tertiary, isDarkColor(_theme.colors.tertiaryContainer) ? 0.92 : 0.12);
557
+ const primaryRoles = getColorRoles(palette.primary, surface);
558
+ _theme.colors.primary = primaryRoles.color;
559
+ _theme.colors.onPrimary = primaryRoles.onColor;
560
+ _theme.colors.primaryContainer = primaryRoles.container;
561
+ _theme.colors.onPrimaryContainer = primaryRoles.onContainer;
562
+ const secondaryRoles = getColorRoles(palette.secondary, surface);
563
+ _theme.colors.secondary = secondaryRoles.color;
564
+ _theme.colors.onSecondary = secondaryRoles.onColor;
565
+ _theme.colors.secondaryContainer = secondaryRoles.container;
566
+ _theme.colors.onSecondaryContainer = secondaryRoles.onContainer;
567
+ const tertiaryRoles = getColorRoles(palette.tertiary, surface);
568
+ _theme.colors.tertiary = tertiaryRoles.color;
569
+ _theme.colors.onTertiary = tertiaryRoles.onColor;
570
+ _theme.colors.tertiaryContainer = tertiaryRoles.container;
571
+ _theme.colors.onTertiaryContainer = tertiaryRoles.onContainer;
530
572
  const blended = getBlendedColor(color, palette.secondary, 0.5);
531
573
  const tintSurface = (alpha) => getBlendedColor(blended, surface, alpha);
532
574
  _theme.colors.background = base.dark ? "#0F0F0F" : "#F0F0F0";
@@ -635,35 +677,81 @@ function Chip({ variant, style, selectedColor, ...props }) {
635
677
  }
636
678
 
637
679
  // src/components/ColorPicker.tsx
638
- import { useState as useState4 } from "react";
680
+ import { useState as useState5 } from "react";
639
681
  import { StyleSheet as StyleSheet4, TouchableOpacity, View as View4 } from "react-native";
640
- import { Icon, TouchableRipple } from "react-native-paper";
682
+ import { Icon, TouchableRipple, useTheme as useTheme6 } from "react-native-paper";
683
+
684
+ // src/useSwatchGrid.ts
685
+ import { useState as useState3 } from "react";
686
+ var SWATCH_GRID_COLUMNS = 5;
687
+ var SWATCH_GRID_GAP = 12;
688
+ function useSwatchSize(fallback) {
689
+ const [width, setWidth] = useState3(0);
690
+ const onLayout = ({ nativeEvent }) => setWidth(nativeEvent.layout.width);
691
+ const size = width > 0 ? (width - SWATCH_GRID_GAP * (SWATCH_GRID_COLUMNS - 1)) / SWATCH_GRID_COLUMNS : fallback;
692
+ return { onLayout, size };
693
+ }
694
+
695
+ // src/utils/getSwatchContrast.ts
696
+ var RING_WIDTH = 1;
697
+ var SELECTED_RING_WIDTH = 3;
698
+ var getContrastColor = (color) => isDarkColor(color) ? "#ffffff" : "#000000";
699
+ var getSwatchRing = (color, selected, outlineColor) => ({
700
+ borderColor: selected ? getContrastColor(color) : outlineColor,
701
+ borderWidth: selected ? SELECTED_RING_WIDTH : RING_WIDTH
702
+ });
641
703
 
642
704
  // src/components/Dialog.tsx
643
- import { useEffect as useEffect4, useRef as useRef2, useState as useState3 } from "react";
644
- import { Animated, StyleSheet as StyleSheet3, TouchableWithoutFeedback, View as View3 } from "react-native";
705
+ import React2, { useEffect as useEffect4, useRef as useRef2, useState as useState4 } from "react";
706
+ import { Animated, BackHandler, Easing, Pressable, StyleSheet as StyleSheet3, View as View3 } from "react-native";
645
707
  import { Dialog as PaperDialog, Portal, useTheme as useTheme5 } from "react-native-paper";
708
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
646
709
  import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
647
- function DialogComponent({ blur: blurProp, visible, onDismiss, children, style, ...rest }) {
710
+ var ANIMATION_DURATION = 220;
711
+ var DIALOG_MARGIN = 32;
712
+ function DialogComponent({ blur: blurProp, dismissable = true, dismissableBackButton = dismissable, visible, onDismiss, children, style, testID = "dialog" }) {
648
713
  const blur = useBlur(blurProp);
649
- const { roundness, colors } = useTheme5();
650
- const opacity = useRef2(new Animated.Value(0)).current;
651
- const [mounted, setMounted] = useState3(visible);
714
+ const theme = useTheme5();
715
+ const { colors, isV3, roundness } = theme;
716
+ const { scale } = theme.animation;
717
+ const { bottom, left, right, top } = useSafeAreaInsets();
718
+ const opacity = useRef2(new Animated.Value(visible ? 1 : 0)).current;
719
+ const [mounted, setMounted] = useState4(visible);
652
720
  useEffect4(() => {
653
721
  if (visible) {
654
722
  setMounted(true);
655
- Animated.timing(opacity, { toValue: 1, duration: 200, useNativeDriver: true }).start();
723
+ Animated.timing(opacity, { duration: scale * ANIMATION_DURATION, easing: Easing.out(Easing.cubic), toValue: 1, useNativeDriver: true }).start();
656
724
  } else {
657
- Animated.timing(opacity, { toValue: 0, duration: 150, useNativeDriver: true }).start(() => setMounted(false));
725
+ Animated.timing(opacity, { duration: scale * ANIMATION_DURATION, easing: Easing.out(Easing.cubic), toValue: 0, useNativeDriver: true }).start(({ finished }) => {
726
+ if (finished) setMounted(false);
727
+ });
658
728
  }
659
729
  }, [visible]);
660
- if (!blur) {
661
- return /* @__PURE__ */ jsx8(PaperDialog, { visible, onDismiss, style: [styles2.sized, style], ...rest, children });
662
- }
730
+ useEffect4(() => {
731
+ if (!visible) return void 0;
732
+ const onHardwareBackPress = () => {
733
+ if (dismissable || dismissableBackButton) onDismiss?.();
734
+ return true;
735
+ };
736
+ const subscription = BackHandler.addEventListener("hardwareBackPress", onHardwareBackPress);
737
+ return () => subscription.remove();
738
+ }, [dismissable, dismissableBackButton, onDismiss, visible]);
663
739
  if (!mounted) return null;
664
- return /* @__PURE__ */ jsx8(Portal, { children: /* @__PURE__ */ jsxs4(Animated.View, { style: [StyleSheet3.absoluteFill, { opacity }], pointerEvents: visible ? "box-none" : "none", children: [
665
- /* @__PURE__ */ jsx8(TouchableWithoutFeedback, { onPress: onDismiss, children: /* @__PURE__ */ jsx8(View3, { style: [StyleSheet3.absoluteFill, { backgroundColor: colors.backdrop }] }) }),
666
- /* @__PURE__ */ jsx8(View3, { style: styles2.wrapper, pointerEvents: "box-none", children: /* @__PURE__ */ jsx8(BlurView, { style: [styles2.card, { borderRadius: roundness * 7 }], children }) })
740
+ const content = React2.Children.toArray(children).filter((child) => child != null && typeof child !== "boolean").map((child, i) => {
741
+ if (isV3) {
742
+ if (i === 0 && React2.isValidElement(child)) {
743
+ return React2.cloneElement(child, { style: [{ marginTop: 24 }, child.props.style] });
744
+ }
745
+ }
746
+ if (i === 0 && React2.isValidElement(child) && child.type === PaperDialog.Content) {
747
+ return React2.cloneElement(child, { style: [{ paddingTop: 24 }, child.props.style] });
748
+ }
749
+ return child;
750
+ });
751
+ const borderRadius = (isV3 ? 7 : 1) * roundness;
752
+ return /* @__PURE__ */ jsx8(Portal, { children: /* @__PURE__ */ jsxs4(Animated.View, { accessibilityLiveRegion: "polite", accessibilityViewIsModal: true, pointerEvents: visible ? "box-none" : "none", style: [StyleSheet3.absoluteFill, { opacity }], testID, children: [
753
+ /* @__PURE__ */ jsx8(Pressable, { accessibilityLabel: "Close modal", accessibilityRole: "button", disabled: !dismissable, onPress: dismissable ? onDismiss : void 0, style: [StyleSheet3.absoluteFill, { backgroundColor: colors.backdrop }], testID: `${testID}-backdrop` }),
754
+ /* @__PURE__ */ jsx8(View3, { pointerEvents: "box-none", style: [styles2.wrapper, { marginBottom: bottom, marginTop: top, paddingHorizontal: Math.max(left, right, DIALOG_MARGIN) }], testID: `${testID}-wrapper`, children: /* @__PURE__ */ jsx8(BlurView, { blur, style: [styles2.card, { borderRadius }, style], testID: `${testID}-surface`, children: content }) })
667
755
  ] }) });
668
756
  }
669
757
  DialogComponent.Title = PaperDialog.Title;
@@ -674,12 +762,12 @@ var Dialog = DialogComponent;
674
762
  var styles2 = StyleSheet3.create({
675
763
  // Width cap keeps dialogs card-sized on wide viewports (web/tablet); no effect on phones.
676
764
  card: { alignSelf: "center", maxWidth: 400, overflow: "hidden", width: "100%" },
677
- sized: { alignSelf: "center", maxWidth: 400, width: "100%" },
678
- wrapper: { flex: 1, justifyContent: "center", paddingHorizontal: 26 }
765
+ wrapper: { flex: 1, justifyContent: "center", paddingVertical: DIALOG_MARGIN }
679
766
  });
680
767
 
681
768
  // src/components/ColorPicker.tsx
682
769
  import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
770
+ var SIZE = 48;
683
771
  var defaultColors = [
684
772
  { label: "Red", value: "#f44336" },
685
773
  { label: "Coral", value: "#ff5722" },
@@ -690,6 +778,7 @@ var defaultColors = [
690
778
  { label: "Emerald", value: "#4caf50" },
691
779
  { label: "Teal", value: "#009688" },
692
780
  { label: "Cyan", value: "#00bcd4" },
781
+ { label: "Sky", value: "#03a9f4" },
693
782
  { label: "Blue", value: "#2196f3" },
694
783
  { label: "Indigo", value: "#3f51b5" },
695
784
  { label: "Deep Purple", value: "#673ab7" },
@@ -698,32 +787,34 @@ var defaultColors = [
698
787
  { label: "Pink", value: "#ec407a" },
699
788
  { label: "Rose", value: "#e91e63" },
700
789
  { label: "Brown", value: "#795548" },
790
+ { label: "Taupe", value: "#8d7b68" },
701
791
  { label: "Blue Grey", value: "#607d8b" }
702
792
  ];
703
793
  var ColorPicker = ({ value, onChange, colors = defaultColors, blur: blurProp }) => {
704
794
  const blur = useBlur(blurProp);
705
- const [open, setOpen] = useState4(false);
795
+ const { colors: themeColors } = useTheme6();
796
+ const [open, setOpen] = useState5(false);
797
+ const { onLayout, size } = useSwatchSize(SIZE);
706
798
  return /* @__PURE__ */ jsxs5(Fragment2, { children: [
707
- /* @__PURE__ */ jsx9(TouchableRipple, { onPress: () => setOpen(true), style: [styles3.trigger, { backgroundColor: value }], borderless: true, children: /* @__PURE__ */ jsx9(Icon, { source: "palette", size: 20, color: "white" }) }),
708
- /* @__PURE__ */ jsx9(Dialog, { blur, visible: open, onDismiss: () => setOpen(false), children: /* @__PURE__ */ jsx9(Dialog.Content, { children: /* @__PURE__ */ jsx9(View4, { style: styles3.swatches, children: colors.map(({ value: hex }) => /* @__PURE__ */ jsx9(
799
+ /* @__PURE__ */ jsx9(TouchableRipple, { onPress: () => setOpen(true), style: [styles3.trigger, { backgroundColor: value }, getSwatchRing(value, false, themeColors.outlineVariant)], borderless: true, children: /* @__PURE__ */ jsx9(Icon, { source: "palette", size: 20, color: getContrastColor(value) }) }),
800
+ /* @__PURE__ */ jsx9(Dialog, { blur, visible: open, onDismiss: () => setOpen(false), children: /* @__PURE__ */ jsx9(Dialog.Content, { children: /* @__PURE__ */ jsx9(View4, { style: styles3.swatches, onLayout, children: colors.map(({ value: hex }) => /* @__PURE__ */ jsx9(
709
801
  TouchableOpacity,
710
802
  {
711
803
  onPress: () => {
712
804
  onChange(hex);
713
805
  setOpen(false);
714
806
  },
715
- style: [styles3.swatch, { backgroundColor: hex }, value === hex && styles3.selected],
716
- children: value === hex && /* @__PURE__ */ jsx9(Icon, { source: "check", size: 20, color: "white" })
807
+ style: [styles3.swatch, { backgroundColor: hex, borderRadius: size / 2, height: size, width: size }, getSwatchRing(hex, value === hex, themeColors.outlineVariant)],
808
+ children: value === hex && /* @__PURE__ */ jsx9(Icon, { source: "check", size: 20, color: getContrastColor(hex) })
717
809
  },
718
810
  hex
719
811
  )) }) }) })
720
812
  ] });
721
813
  };
722
814
  var styles3 = StyleSheet4.create({
723
- selected: { borderColor: "white", borderWidth: 3 },
724
- swatch: { alignItems: "center", borderRadius: 24, height: 48, justifyContent: "center", width: 48 },
725
- swatches: { flexDirection: "row", flexWrap: "wrap", gap: 12, paddingTop: 12 },
726
- trigger: { alignItems: "center", alignSelf: "flex-start", borderRadius: 24, height: 48, justifyContent: "center", width: 48 }
815
+ swatch: { alignItems: "center", justifyContent: "center" },
816
+ swatches: { flexDirection: "row", flexWrap: "wrap", gap: SWATCH_GRID_GAP, paddingTop: 12 },
817
+ trigger: { alignItems: "center", alignSelf: "flex-start", borderRadius: SIZE / 2, height: SIZE, justifyContent: "center", width: SIZE }
727
818
  });
728
819
 
729
820
  // src/components/FAB.tsx
@@ -735,7 +826,7 @@ function FAB(props) {
735
826
  }
736
827
 
737
828
  // src/components/HarmonyPicker.tsx
738
- import { useState as useState5 } from "react";
829
+ import { useState as useState6 } from "react";
739
830
  import { Button as Button2 } from "react-native-paper";
740
831
 
741
832
  // src/components/Menu.tsx
@@ -765,7 +856,7 @@ var HARMONIES = [
765
856
  ];
766
857
  var HarmonyPicker = ({ value, onChange, blur: blurProp }) => {
767
858
  const blur = useBlur(blurProp);
768
- const [open, setOpen] = useState5(false);
859
+ const [open, setOpen] = useState6(false);
769
860
  const selected = HARMONIES.find((h) => h.value === value);
770
861
  return /* @__PURE__ */ jsx12(
771
862
  Menu,
@@ -791,23 +882,83 @@ var HarmonyPicker = ({ value, onChange, blur: blurProp }) => {
791
882
  };
792
883
 
793
884
  // src/components/IconButton.tsx
794
- import { IconButton as PaperIconButton, useTheme as useTheme6 } from "react-native-paper";
885
+ import { IconButton as PaperIconButton, useTheme as useTheme7 } from "react-native-paper";
795
886
  import { jsx as jsx13 } from "react/jsx-runtime";
796
887
  function IconButton({ variant, iconColor, containerColor, ...props }) {
797
888
  const defaults = usePaperDefaults();
798
- const { colors } = useTheme6();
889
+ const { colors } = useTheme7();
799
890
  const variantContainerColor = variant === "primary" ? colors.primaryContainer : variant === "secondary" ? colors.secondaryContainer : variant === "tertiary" ? colors.tertiaryContainer : variant === "surface" ? colors.surface : void 0;
800
891
  const variantIconColor = variant === "primary" ? colors.onPrimaryContainer : variant === "secondary" ? colors.onSecondaryContainer : variant === "tertiary" ? colors.onTertiaryContainer : variant === "surface" ? colors.onSurface : void 0;
801
892
  return /* @__PURE__ */ jsx13(PaperIconButton, { ...defaults.IconButton, containerColor: containerColor ?? variantContainerColor, iconColor: iconColor ?? variantIconColor, ...props });
802
893
  }
803
894
 
895
+ // src/components/PalettePicker.tsx
896
+ import { useState as useState7 } from "react";
897
+ import { StyleSheet as StyleSheet6, TouchableOpacity as TouchableOpacity2, View as View5 } from "react-native";
898
+ import { Icon as Icon2, TouchableRipple as TouchableRipple2, useTheme as useTheme8 } from "react-native-paper";
899
+ import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs6 } from "react/jsx-runtime";
900
+ var SIZE2 = 48;
901
+ var defaultWeights = { primary: 2, secondary: 1, tertiary: 1 };
902
+ var buildWedges = (weights) => {
903
+ const total = weights.primary + weights.secondary + weights.tertiary;
904
+ const primary = weights.primary / total * 360;
905
+ const secondary = weights.secondary / total * 360;
906
+ const tertiary = weights.tertiary / total * 360;
907
+ return [
908
+ { key: "primary", rotate: -primary / 2, share: primary },
909
+ { key: "tertiary", rotate: primary / 2, share: tertiary },
910
+ { key: "secondary", rotate: primary / 2 + tertiary, share: secondary }
911
+ ];
912
+ };
913
+ var Pie = ({ palette, weights = defaultWeights, size = SIZE2 }) => /* @__PURE__ */ jsx14(View5, { style: [styles5.pie, { borderRadius: size / 2, height: size, width: size }], children: buildWedges(weights).map(({ key, rotate, share }) => /* @__PURE__ */ jsx14(View5, { style: [styles5.rotator, { transform: [{ rotate: `${rotate}deg` }] }], children: /* @__PURE__ */ jsx14(View5, { style: styles5.halfMask, children: /* @__PURE__ */ jsx14(View5, { style: [styles5.innerRotator, { transform: [{ rotate: `${share - 180}deg` }] }], children: /* @__PURE__ */ jsx14(View5, { style: [styles5.wedgeFill, { backgroundColor: palette[key] }] }) }) }) }, key)) });
914
+ var PalettePicker = ({ value, onChange, harmony = "split-complementary", weights, colors = defaultColors, blur: blurProp }) => {
915
+ const blur = useBlur(blurProp);
916
+ const { colors: themeColors } = useTheme8();
917
+ const [open, setOpen] = useState7(false);
918
+ const { onLayout, size } = useSwatchSize(SIZE2);
919
+ return /* @__PURE__ */ jsxs6(Fragment3, { children: [
920
+ /* @__PURE__ */ jsx14(TouchableRipple2, { onPress: () => setOpen(true), style: styles5.trigger, borderless: true, children: /* @__PURE__ */ jsxs6(View5, { style: styles5.triggerContent, children: [
921
+ /* @__PURE__ */ jsx14(Pie, { palette: getTriadicPalette(value, harmony), weights }),
922
+ /* @__PURE__ */ jsx14(View5, { pointerEvents: "none", style: [styles5.triggerIcon, { borderRadius: SIZE2 / 2 }, getSwatchRing(value, false, themeColors.outlineVariant)], children: /* @__PURE__ */ jsx14(Icon2, { source: "palette", size: 20, color: getContrastColor(value) }) })
923
+ ] }) }),
924
+ /* @__PURE__ */ jsx14(Dialog, { blur, visible: open, onDismiss: () => setOpen(false), children: /* @__PURE__ */ jsx14(Dialog.Content, { children: /* @__PURE__ */ jsx14(View5, { style: styles5.swatches, onLayout, children: colors.map(({ value: hex }) => /* @__PURE__ */ jsxs6(
925
+ TouchableOpacity2,
926
+ {
927
+ onPress: () => {
928
+ onChange(hex);
929
+ setOpen(false);
930
+ },
931
+ style: [styles5.swatch, { height: size, width: size }],
932
+ children: [
933
+ /* @__PURE__ */ jsx14(Pie, { palette: getTriadicPalette(hex, harmony), weights, size }),
934
+ /* @__PURE__ */ jsx14(View5, { pointerEvents: "none", style: [styles5.ring, { borderRadius: size / 2 }, getSwatchRing(hex, value === hex, themeColors.outlineVariant)], children: value === hex && /* @__PURE__ */ jsx14(Icon2, { source: "check", size: 20, color: getContrastColor(hex) }) })
935
+ ]
936
+ },
937
+ hex
938
+ )) }) }) })
939
+ ] });
940
+ };
941
+ var styles5 = StyleSheet6.create({
942
+ halfMask: { height: "100%", left: "50%", overflow: "hidden", position: "absolute", top: 0, width: "50%" },
943
+ innerRotator: { height: "100%", left: "-100%", overflow: "hidden", position: "absolute", top: 0, width: "200%" },
944
+ pie: { overflow: "hidden" },
945
+ rotator: { height: "100%", left: 0, overflow: "hidden", position: "absolute", top: 0, width: "100%" },
946
+ ring: { alignItems: "center", bottom: 0, justifyContent: "center", left: 0, position: "absolute", right: 0, top: 0 },
947
+ swatch: { alignItems: "center", justifyContent: "center" },
948
+ swatches: { flexDirection: "row", flexWrap: "wrap", gap: SWATCH_GRID_GAP, paddingTop: 12 },
949
+ trigger: { alignItems: "center", alignSelf: "flex-start", borderRadius: SIZE2 / 2, height: SIZE2, justifyContent: "center", width: SIZE2 },
950
+ triggerContent: { height: SIZE2, width: SIZE2 },
951
+ triggerIcon: { alignItems: "center", bottom: 0, justifyContent: "center", left: 0, position: "absolute", right: 0, top: 0 },
952
+ wedgeFill: { height: "100%", left: "50%", position: "absolute", top: 0, width: "50%" }
953
+ });
954
+
804
955
  // src/components/TextInput.tsx
805
956
  import { forwardRef } from "react";
806
957
  import { TextInput as PaperTextInput } from "react-native-paper";
807
- import { jsx as jsx14 } from "react/jsx-runtime";
958
+ import { jsx as jsx15 } from "react/jsx-runtime";
808
959
  var TextInput = forwardRef(function TextInput2(props, ref) {
809
960
  const defaults = usePaperDefaults();
810
- return /* @__PURE__ */ jsx14(PaperTextInput, { ref, ...defaults.TextInput, ...props });
961
+ return /* @__PURE__ */ jsx15(PaperTextInput, { ref, ...defaults.TextInput, ...props });
811
962
  });
812
963
 
813
964
  // src/redux/themeSlice.ts
@@ -847,6 +998,10 @@ var selectThemeBlur = (state) => state.blur;
847
998
  var selectThemeColor = (state) => state.color;
848
999
  var selectThemeHarmony = (state) => state.harmony;
849
1000
 
1001
+ // src/useTheme.ts
1002
+ import { useTheme as usePaperTheme } from "react-native-paper";
1003
+ var useAutoPaperTheme = () => usePaperTheme();
1004
+
850
1005
  // src/useThemeSettings.ts
851
1006
  import { useContext as useContext5 } from "react";
852
1007
  var useThemeSettings = () => useContext5(ThemeSettingsContext);
@@ -869,13 +1024,17 @@ export {
869
1024
  HarmonyPicker,
870
1025
  IconButton,
871
1026
  Menu,
1027
+ PalettePicker,
872
1028
  Provider,
1029
+ SEMANTIC_BASE_COLORS,
873
1030
  TextInput,
874
1031
  ThemeSettingsContext,
875
1032
  createThemeReducer,
876
1033
  defaultColors,
877
1034
  defaultThemeSettings,
878
1035
  getBlendedColor,
1036
+ getColorRoles,
1037
+ getContrastColor,
879
1038
  getHex,
880
1039
  getRgb,
881
1040
  getTintTextColor,
@@ -888,6 +1047,7 @@ export {
888
1047
  selectThemeHarmony,
889
1048
  themeActions,
890
1049
  themeReducer,
1050
+ useAutoPaperTheme,
891
1051
  useBlur,
892
1052
  useComputedTheme,
893
1053
  usePaperDefaults,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rific/auto-paper",
3
- "version": "0.6.4",
3
+ "version": "0.8.0",
4
4
  "description": "Adaptive react-native-paper theming: triadic palette from a seed color + system/light/dark appearance",
5
5
  "keywords": [
6
6
  "react-native",
@@ -86,6 +86,7 @@
86
86
  "react-dom": "^19.0.0",
87
87
  "react-native": "^0.76.0",
88
88
  "react-native-paper": "^5.12.0",
89
+ "react-native-safe-area-context": "^5.5.2",
89
90
  "ts-jest": "^29.4.9",
90
91
  "ts-node": "^10.9.2",
91
92
  "tsup": "^8.0.0",
@@ -97,7 +98,8 @@
97
98
  "expo-navigation-bar": ">=56.0.0",
98
99
  "react": ">=18.0.0",
99
100
  "react-native": ">=0.76.0",
100
- "react-native-paper": ">=5.0.0"
101
+ "react-native-paper": ">=5.0.0",
102
+ "react-native-safe-area-context": "*"
101
103
  },
102
104
  "peerDependenciesMeta": {
103
105
  "expo-blur": {
@@ -2,15 +2,35 @@ import { SegmentedButtons } from 'react-native-paper'
2
2
 
3
3
  import type { ThemeAppearance } from '../useComputedTheme'
4
4
 
5
- const APPEARANCES: { label: string; value: ThemeAppearance; icon: string }[] = [
6
- { label: 'System', value: 'system', icon: 'brightness-auto' },
7
- { label: 'Light', value: 'light', icon: 'white-balance-sunny' },
8
- { label: 'Dark', value: 'dark', icon: 'weather-night' }
5
+ const APPEARANCES: { label: string; value: ThemeAppearance }[] = [
6
+ { label: 'System', value: 'system' },
7
+ { label: 'Light', value: 'light' },
8
+ { label: 'Dark', value: 'dark' }
9
9
  ]
10
10
 
11
+ export type AppearanceIcons = Partial<Record<ThemeAppearance, string>>
12
+
13
+ // 'monitor' (a plain device/screen glyph) reads more clearly as "match my device" than
14
+ // 'brightness-auto' (an exposure/brightness symbol) — swap via the `icons` prop for a different
15
+ // convention, e.g. `theme-light-dark`'s half-sun/half-moon.
16
+ const DEFAULT_ICONS: Record<ThemeAppearance, string> = {
17
+ system: 'monitor',
18
+ light: 'white-balance-sunny',
19
+ dark: 'weather-night'
20
+ }
21
+
11
22
  type Props = {
12
23
  value: ThemeAppearance
13
24
  onChange: (appearance: ThemeAppearance) => void
25
+ // Hides the text label, leaving just the icon per segment — useful in tight layouts where
26
+ // "System" truncates. accessibilityLabel is always set regardless, so screen readers still
27
+ // announce the full word either way.
28
+ showLabels?: boolean
29
+ // Per-value icon overrides, merged over DEFAULT_ICONS — pass just the ones you want to change.
30
+ icons?: AppearanceIcons
14
31
  }
15
32
 
16
- export const AppearancePicker = ({ value, onChange }: Props) => <SegmentedButtons value={value} onValueChange={(v) => onChange(v as ThemeAppearance)} buttons={APPEARANCES.map((a) => ({ value: a.value, label: a.label, icon: a.icon }))} />
33
+ export const AppearancePicker = ({ value, onChange, showLabels = true, icons }: Props) => {
34
+ const resolvedIcons = { ...DEFAULT_ICONS, ...icons }
35
+ return <SegmentedButtons value={value} onValueChange={(v) => onChange(v as ThemeAppearance)} buttons={APPEARANCES.map((a) => ({ value: a.value, label: showLabels ? a.label : undefined, icon: resolvedIcons[a.value], accessibilityLabel: a.label }))} />
36
+ }
@@ -67,7 +67,7 @@ export const BlurView = ({ blur = true, children, elevation, style, tintColor, t
67
67
  </ExpoBlurView>
68
68
  )
69
69
  return (
70
- <View style={[{ backgroundColor: surfaceColor }, style]}>
70
+ <View {...props} style={[{ backgroundColor: surfaceColor }, style]}>
71
71
  {tint}
72
72
  {children}
73
73
  </View>
@@ -1,10 +1,14 @@
1
1
  import { useState } from 'react'
2
2
  import { StyleSheet, TouchableOpacity, View } from 'react-native'
3
- import { Icon, TouchableRipple } from 'react-native-paper'
3
+ import { Icon, TouchableRipple, useTheme } from 'react-native-paper'
4
4
 
5
5
  import { useBlur } from '../BlurContext'
6
+ import { SWATCH_GRID_GAP, useSwatchSize } from '../useSwatchGrid'
7
+ import { getContrastColor, getSwatchRing } from '../utils/getSwatchContrast'
6
8
  import { Dialog } from './Dialog'
7
9
 
10
+ const SIZE = 48
11
+
8
12
  export type SeedColor = { label: string; value: string }
9
13
 
10
14
  export const defaultColors: SeedColor[] = [
@@ -17,6 +21,7 @@ export const defaultColors: SeedColor[] = [
17
21
  { label: 'Emerald', value: '#4caf50' },
18
22
  { label: 'Teal', value: '#009688' },
19
23
  { label: 'Cyan', value: '#00bcd4' },
24
+ { label: 'Sky', value: '#03a9f4' },
20
25
  { label: 'Blue', value: '#2196f3' },
21
26
  { label: 'Indigo', value: '#3f51b5' },
22
27
  { label: 'Deep Purple', value: '#673ab7' },
@@ -25,6 +30,7 @@ export const defaultColors: SeedColor[] = [
25
30
  { label: 'Pink', value: '#ec407a' },
26
31
  { label: 'Rose', value: '#e91e63' },
27
32
  { label: 'Brown', value: '#795548' },
33
+ { label: 'Taupe', value: '#8d7b68' },
28
34
  { label: 'Blue Grey', value: '#607d8b' }
29
35
  ]
30
36
 
@@ -37,17 +43,19 @@ type Props = {
37
43
 
38
44
  export const ColorPicker = ({ value, onChange, colors = defaultColors, blur: blurProp }: Props) => {
39
45
  const blur = useBlur(blurProp)
46
+ const { colors: themeColors } = useTheme()
40
47
  const [open, setOpen] = useState(false)
48
+ const { onLayout, size } = useSwatchSize(SIZE)
41
49
 
42
50
  return (
43
51
  <>
44
- <TouchableRipple onPress={() => setOpen(true)} style={[styles.trigger, { backgroundColor: value }]} borderless>
45
- <Icon source='palette' size={20} color='white' />
52
+ <TouchableRipple onPress={() => setOpen(true)} style={[styles.trigger, { backgroundColor: value }, getSwatchRing(value, false, themeColors.outlineVariant)]} borderless>
53
+ <Icon source='palette' size={20} color={getContrastColor(value)} />
46
54
  </TouchableRipple>
47
55
 
48
56
  <Dialog blur={blur} visible={open} onDismiss={() => setOpen(false)}>
49
57
  <Dialog.Content>
50
- <View style={styles.swatches}>
58
+ <View style={styles.swatches} onLayout={onLayout}>
51
59
  {colors.map(({ value: hex }) => (
52
60
  <TouchableOpacity
53
61
  key={hex}
@@ -55,9 +63,9 @@ export const ColorPicker = ({ value, onChange, colors = defaultColors, blur: blu
55
63
  onChange(hex)
56
64
  setOpen(false)
57
65
  }}
58
- style={[styles.swatch, { backgroundColor: hex }, value === hex && styles.selected]}
66
+ style={[styles.swatch, { backgroundColor: hex, borderRadius: size / 2, height: size, width: size }, getSwatchRing(hex, value === hex, themeColors.outlineVariant)]}
59
67
  >
60
- {value === hex && <Icon source='check' size={20} color='white' />}
68
+ {value === hex && <Icon source='check' size={20} color={getContrastColor(hex)} />}
61
69
  </TouchableOpacity>
62
70
  ))}
63
71
  </View>
@@ -68,8 +76,7 @@ export const ColorPicker = ({ value, onChange, colors = defaultColors, blur: blu
68
76
  }
69
77
 
70
78
  const styles = StyleSheet.create({
71
- selected: { borderColor: 'white', borderWidth: 3 },
72
- swatch: { alignItems: 'center', borderRadius: 24, height: 48, justifyContent: 'center', width: 48 },
73
- swatches: { flexDirection: 'row', flexWrap: 'wrap', gap: 12, paddingTop: 12 },
74
- trigger: { alignItems: 'center', alignSelf: 'flex-start', borderRadius: 24, height: 48, justifyContent: 'center', width: 48 }
79
+ swatch: { alignItems: 'center', justifyContent: 'center' },
80
+ swatches: { flexDirection: 'row', flexWrap: 'wrap', gap: SWATCH_GRID_GAP, paddingTop: 12 },
81
+ trigger: { alignItems: 'center', alignSelf: 'flex-start', borderRadius: SIZE / 2, height: SIZE, justifyContent: 'center', width: SIZE }
75
82
  })