@rific/auto-paper 0.13.4 → 0.14.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
@@ -244,13 +244,13 @@ var getRgb = (value) => {
244
244
  var getHex = (value) => {
245
245
  const rgb = getRgb(value);
246
246
  if (!rgb) return null;
247
- const toHex = (n) => {
247
+ const toHex2 = (n) => {
248
248
  const hex = Math.round(n).toString(16);
249
249
  return hex.length === 1 ? "0" + hex : hex;
250
250
  };
251
- const hexString = `#${toHex(rgb.r)}${toHex(rgb.g)}${toHex(rgb.b)}`;
251
+ const hexString = `#${toHex2(rgb.r)}${toHex2(rgb.g)}${toHex2(rgb.b)}`;
252
252
  if (rgb.a !== void 0) {
253
- return `${hexString}${toHex(rgb.a * 255)}`;
253
+ return `${hexString}${toHex2(rgb.a * 255)}`;
254
254
  }
255
255
  return hexString;
256
256
  };
@@ -432,11 +432,11 @@ function hslToHex(h, s, l) {
432
432
  g = 0;
433
433
  b = x;
434
434
  }
435
- const toHex = (value) => {
435
+ const toHex2 = (value) => {
436
436
  const hex = Math.round((value + m) * 255).toString(16);
437
437
  return hex.length === 1 ? `0${hex}` : hex;
438
438
  };
439
- return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
439
+ return `#${toHex2(r)}${toHex2(g)}${toHex2(b)}`;
440
440
  }
441
441
 
442
442
  // src/useComputedTheme.ts
@@ -449,7 +449,7 @@ var SEMANTIC_BASE_COLORS = {
449
449
  warning: "#A15C00",
450
450
  danger: "#B00020"
451
451
  };
452
- function useComputedTheme(appearance, color, harmony = "split-complementary") {
452
+ function useComputedTheme(appearance, color, harmony = "split-complementary", paletteTone) {
453
453
  const [theme, setTheme] = useState(null);
454
454
  const primaryInput = typeof color === "string" ? color : color.primary;
455
455
  const secondaryInput = typeof color === "string" ? null : color.secondary;
@@ -482,7 +482,8 @@ function useComputedTheme(appearance, color, harmony = "split-complementary") {
482
482
  onDangerContainer: dangerRoles.onContainer
483
483
  }
484
484
  };
485
- const palette = secondaryInput !== null && tertiaryInput !== null ? { primary: normalizeExplicitColor(primaryInput), secondary: normalizeExplicitColor(secondaryInput), tertiary: normalizeExplicitColor(tertiaryInput) } : getTriadicPalette(primaryInput, harmony);
485
+ const untoned = secondaryInput !== null && tertiaryInput !== null ? { primary: normalizeExplicitColor(primaryInput), secondary: normalizeExplicitColor(secondaryInput), tertiary: normalizeExplicitColor(tertiaryInput) } : getTriadicPalette(primaryInput, harmony);
486
+ const palette = paletteTone ? { primary: paletteTone(untoned.primary, base.dark), secondary: paletteTone(untoned.secondary, base.dark), tertiary: paletteTone(untoned.tertiary, base.dark) } : untoned;
486
487
  const primaryRoles = getColorRoles(palette.primary, surface);
487
488
  _theme.colors.primary = primaryRoles.color;
488
489
  _theme.colors.onPrimary = primaryRoles.onColor;
@@ -515,7 +516,7 @@ function useComputedTheme(appearance, color, harmony = "split-complementary") {
515
516
  level5: getBlendedColor(blendedGrey, _theme.colors.surface, 0.7)
516
517
  };
517
518
  setTheme(_theme);
518
- }, [appearance, primaryInput, secondaryInput, tertiaryInput, harmony]);
519
+ }, [appearance, primaryInput, secondaryInput, tertiaryInput, harmony, paletteTone]);
519
520
  useEffect(() => {
520
521
  computeTheme();
521
522
  }, [computeTheme]);
@@ -536,15 +537,17 @@ var ReanimatedModuleContext = createContext2(void 0);
536
537
  var useReanimatedModule = () => useContext3(ReanimatedModuleContext);
537
538
  var MaskedViewModuleContext = createContext2(void 0);
538
539
  var useMaskedViewModule = () => useContext3(MaskedViewModuleContext);
540
+ var PaletteToneContext = createContext2(void 0);
541
+ var usePaletteTone = () => useContext3(PaletteToneContext);
539
542
  var StatusBarEnabledContext = createContext2(true);
540
543
  var useStatusBarEnabled = () => useContext3(StatusBarEnabledContext);
541
- function Provider({ children, defaults, expoBlur, fontFamily, initialValue, maskedView, navigationBar, onChange, onNavBarChange, onReady, reanimated, statusBar, statusBarProps, style }) {
542
- return /* @__PURE__ */ jsx(ThemeSettingsProvider, { initialValue, onChange, children: /* @__PURE__ */ jsx(ThemeProviderBody, { defaults, expoBlur, fontFamily, maskedView, navigationBar, onNavBarChange, onReady, reanimated, statusBar, statusBarProps, style, children }) });
544
+ function Provider({ children, defaults, expoBlur, fontFamily, initialValue, maskedView, navigationBar, onChange, onNavBarChange, onReady, paletteTone, reanimated, statusBar, statusBarProps, style }) {
545
+ return /* @__PURE__ */ jsx(ThemeSettingsProvider, { initialValue, onChange, children: /* @__PURE__ */ jsx(ThemeProviderBody, { defaults, expoBlur, fontFamily, maskedView, navigationBar, onNavBarChange, onReady, paletteTone, reanimated, statusBar, statusBarProps, style, children }) });
543
546
  }
544
547
  var ThemeSettingsProvider = themeSettingsProvider;
545
- function ThemeProviderBody({ children, defaults, expoBlur, fontFamily, maskedView, navigationBar, onNavBarChange, onReady, reanimated, statusBar = true, statusBarProps, style }) {
548
+ function ThemeProviderBody({ children, defaults, expoBlur, fontFamily, maskedView, navigationBar, onNavBarChange, onReady, paletteTone, reanimated, statusBar = true, statusBarProps, style }) {
546
549
  const { settings } = useThemeSettingsInternal();
547
- const computedTheme = useComputedTheme(settings.appearance, settings.color, settings.harmony);
550
+ const computedTheme = useComputedTheme(settings.appearance, settings.color, settings.harmony, paletteTone);
548
551
  const theme = useMemo(() => {
549
552
  if (!computedTheme) return null;
550
553
  if (!fontFamily) return computedTheme;
@@ -560,7 +563,7 @@ function ThemeProviderBody({ children, defaults, expoBlur, fontFamily, maskedVie
560
563
  if (!theme) return null;
561
564
  return /* @__PURE__ */ jsxs(PaperProvider, { theme, children: [
562
565
  statusBar && /* @__PURE__ */ jsx(StatusBar, { barStyle: theme.dark ? "light-content" : "dark-content", ...statusBarProps }),
563
- /* @__PURE__ */ jsx(StatusBarEnabledContext.Provider, { value: statusBar, children: /* @__PURE__ */ jsx(PaperDefaultsContext.Provider, { value: defaults ?? {}, children: /* @__PURE__ */ jsx(NavBarContext.Provider, { value: { navigationBar, onNavBarChange }, children: /* @__PURE__ */ jsx(BlurModuleContext.Provider, { value: expoBlur, children: /* @__PURE__ */ jsx(ReanimatedModuleContext.Provider, { value: reanimated, children: /* @__PURE__ */ jsx(MaskedViewModuleContext.Provider, { value: maskedView, children: /* @__PURE__ */ jsx(Portal.Host, { children: /* @__PURE__ */ jsx(View, { style: [styles.flex, { backgroundColor: theme.colors.background }, style], children }) }) }) }) }) }) }) })
566
+ /* @__PURE__ */ jsx(StatusBarEnabledContext.Provider, { value: statusBar, children: /* @__PURE__ */ jsx(PaperDefaultsContext.Provider, { value: defaults ?? {}, children: /* @__PURE__ */ jsx(NavBarContext.Provider, { value: { navigationBar, onNavBarChange }, children: /* @__PURE__ */ jsx(BlurModuleContext.Provider, { value: expoBlur, children: /* @__PURE__ */ jsx(ReanimatedModuleContext.Provider, { value: reanimated, children: /* @__PURE__ */ jsx(MaskedViewModuleContext.Provider, { value: maskedView, children: /* @__PURE__ */ jsx(PaletteToneContext.Provider, { value: paletteTone, children: /* @__PURE__ */ jsx(Portal.Host, { children: /* @__PURE__ */ jsx(View, { style: [styles.flex, { backgroundColor: theme.colors.background }, style], children }) }) }) }) }) }) }) }) })
564
567
  ] });
565
568
  }
566
569
  var styles = StyleSheet.create({
@@ -613,11 +616,15 @@ var AutoAppearancePicker = (props) => {
613
616
  return /* @__PURE__ */ jsx4(AppearancePicker, { ...props, value: settings.appearance, onChange: (appearance) => set({ appearance }) });
614
617
  };
615
618
 
619
+ // src/components/AutoPalettePicker.tsx
620
+ import { useCallback as useCallback2 } from "react";
621
+ import { useTheme as useTheme6 } from "react-native-paper";
622
+
616
623
  // src/utils/resolveSeedColor.ts
617
624
  var resolveSeedColor = (color) => typeof color === "string" ? color : color.primary;
618
625
 
619
626
  // src/components/PalettePicker.tsx
620
- import { useState as useState5 } from "react";
627
+ import { useMemo as useMemo2, useState as useState5 } from "react";
621
628
  import { StyleSheet as StyleSheet5, TouchableOpacity as TouchableOpacity2, View as View5 } from "react-native";
622
629
  import { Icon as Icon2, TouchableRipple as TouchableRipple2, useTheme as useTheme5 } from "react-native-paper";
623
630
 
@@ -863,19 +870,25 @@ var buildWedges = (weights) => {
863
870
  ];
864
871
  };
865
872
  var Pie = ({ palette, weights = defaultWeights, size = SIZE2 }) => /* @__PURE__ */ jsx9(View5, { style: [styles4.pie, { borderRadius: size / 2, height: size, width: size }], children: buildWedges(weights).map(({ key, rotate, share }) => /* @__PURE__ */ jsx9(View5, { style: [styles4.rotator, { transform: [{ rotate: `${rotate}deg` }] }], children: /* @__PURE__ */ jsx9(View5, { style: styles4.halfMask, children: /* @__PURE__ */ jsx9(View5, { style: [styles4.innerRotator, { transform: [{ rotate: `${share - 180}deg` }] }], children: /* @__PURE__ */ jsx9(View5, { style: [styles4.wedgeFill, { backgroundColor: palette[key] }] }) }) }) }, key)) });
866
- var PalettePicker = ({ value, onChange, harmony = "split-complementary", onHarmonyChange, weights, colors = defaultColors, blur: blurProp }) => {
873
+ var PalettePicker = ({ value, onChange, harmony = "split-complementary", onHarmonyChange, weights, colors = defaultColors, blur: blurProp, previewColor }) => {
867
874
  const blur = useBlur(blurProp);
868
875
  const { colors: themeColors } = useTheme5();
869
876
  const [open, setOpen] = useState5(false);
870
877
  const { onLayout, size } = useSwatchSize(SIZE2);
878
+ const shade = useMemo2(
879
+ () => (palette) => previewColor ? { primary: previewColor(palette.primary), secondary: previewColor(palette.secondary), tertiary: previewColor(palette.tertiary) } : palette,
880
+ [previewColor]
881
+ );
882
+ const triggerPalette = shade(getTriadicPalette(value, harmony));
883
+ const swatchPalettes = useMemo2(() => colors.map(({ value: hex }) => shade(getTriadicPalette(hex, harmony))), [colors, harmony, shade]);
871
884
  return /* @__PURE__ */ jsxs6(Fragment3, { children: [
872
885
  /* @__PURE__ */ jsx9(TouchableRipple2, { onPress: () => setOpen(true), style: styles4.trigger, borderless: true, children: /* @__PURE__ */ jsxs6(View5, { style: styles4.triggerContent, children: [
873
- /* @__PURE__ */ jsx9(Pie, { palette: getTriadicPalette(value, harmony), weights }),
874
- /* @__PURE__ */ jsx9(View5, { pointerEvents: "none", style: [styles4.triggerIcon, { borderRadius: SIZE2 / 2 }, getSwatchRing(value, false, themeColors.outlineVariant)], children: /* @__PURE__ */ jsx9(Icon2, { source: "palette", size: 20, color: getContrastColor(value) }) })
886
+ /* @__PURE__ */ jsx9(Pie, { palette: triggerPalette, weights }),
887
+ /* @__PURE__ */ jsx9(View5, { pointerEvents: "none", style: [styles4.triggerIcon, { borderRadius: SIZE2 / 2 }, getSwatchRing(triggerPalette.primary, false, themeColors.outlineVariant)], children: /* @__PURE__ */ jsx9(Icon2, { source: "palette", size: 20, color: getContrastColor(triggerPalette.primary) }) })
875
888
  ] }) }),
876
889
  /* @__PURE__ */ jsx9(Dialog, { blur, visible: open, onDismiss: () => setOpen(false), children: /* @__PURE__ */ jsxs6(Dialog.Content, { children: [
877
890
  onHarmonyChange && /* @__PURE__ */ jsx9(View5, { style: styles4.harmonyRow, children: /* @__PURE__ */ jsx9(HarmonyPicker, { value: harmony, onChange: onHarmonyChange, showLabels: false }) }),
878
- /* @__PURE__ */ jsx9(View5, { style: styles4.swatches, onLayout, children: colors.map(({ value: hex }) => /* @__PURE__ */ jsxs6(
891
+ /* @__PURE__ */ jsx9(View5, { style: styles4.swatches, onLayout, children: colors.map(({ value: hex }, index) => /* @__PURE__ */ jsxs6(
879
892
  TouchableOpacity2,
880
893
  {
881
894
  onPress: () => {
@@ -884,8 +897,8 @@ var PalettePicker = ({ value, onChange, harmony = "split-complementary", onHarmo
884
897
  },
885
898
  style: [styles4.swatch, { height: size, width: size }],
886
899
  children: [
887
- /* @__PURE__ */ jsx9(Pie, { palette: getTriadicPalette(hex, harmony), weights, size }),
888
- /* @__PURE__ */ jsx9(View5, { pointerEvents: "none", style: [styles4.ring, { borderRadius: size / 2 }, getSwatchRing(hex, value === hex, themeColors.outlineVariant)], children: value === hex && /* @__PURE__ */ jsx9(Icon2, { source: "check", size: 20, color: getContrastColor(hex) }) })
900
+ /* @__PURE__ */ jsx9(Pie, { palette: swatchPalettes[index], weights, size }),
901
+ /* @__PURE__ */ jsx9(View5, { pointerEvents: "none", style: [styles4.ring, { borderRadius: size / 2 }, getSwatchRing(swatchPalettes[index].primary, value === hex, themeColors.outlineVariant)], children: value === hex && /* @__PURE__ */ jsx9(Icon2, { source: "check", size: 20, color: getContrastColor(swatchPalettes[index].primary) }) })
889
902
  ]
890
903
  },
891
904
  hex
@@ -912,13 +925,16 @@ var styles4 = StyleSheet5.create({
912
925
  import { jsx as jsx10 } from "react/jsx-runtime";
913
926
  var AutoPalettePicker = (props) => {
914
927
  const { settings, set } = useThemeSettings();
915
- return /* @__PURE__ */ jsx10(PalettePicker, { ...props, value: resolveSeedColor(settings.color), onChange: (color) => set({ color }), harmony: settings.harmony, onHarmonyChange: (harmony) => set({ harmony }) });
928
+ const tone = usePaletteTone();
929
+ const { dark } = useTheme6();
930
+ const toned = useCallback2((color) => tone(color, dark), [tone, dark]);
931
+ return /* @__PURE__ */ jsx10(PalettePicker, { ...props, value: resolveSeedColor(settings.color), onChange: (color) => set({ color }), harmony: settings.harmony, onHarmonyChange: (harmony) => set({ harmony }), previewColor: tone ? toned : void 0 });
916
932
  };
917
933
 
918
934
  // src/components/BottomNavigation.tsx
919
935
  import { useEffect as useEffect4 } from "react";
920
936
  import { Platform as Platform3 } from "react-native";
921
- import { BottomNavigation as PaperBottomNavigation, useTheme as useTheme6 } from "react-native-paper";
937
+ import { BottomNavigation as PaperBottomNavigation, useTheme as useTheme7 } from "react-native-paper";
922
938
 
923
939
  // src/navigation-bar.ts
924
940
  var setNavigationBarStyle = (navigationBar, dark) => {
@@ -929,7 +945,7 @@ var setNavigationBarStyle = (navigationBar, dark) => {
929
945
  // src/components/BottomNavigation.tsx
930
946
  import { jsx as jsx11 } from "react/jsx-runtime";
931
947
  function useNavigationBarSync() {
932
- const theme = useTheme6();
948
+ const theme = useTheme7();
933
949
  const { navigationBar, onNavBarChange } = useNavBarContext();
934
950
  useEffect4(() => {
935
951
  if (Platform3.OS !== "android") return;
@@ -959,11 +975,11 @@ function Button(props) {
959
975
  }
960
976
 
961
977
  // src/components/Chip.tsx
962
- import { Chip as PaperChip, useTheme as useTheme7 } from "react-native-paper";
978
+ import { Chip as PaperChip, useTheme as useTheme8 } from "react-native-paper";
963
979
  import { jsx as jsx13 } from "react/jsx-runtime";
964
980
  function Chip({ variant, style, selectedColor, ...props }) {
965
981
  const defaults = usePaperDefaults();
966
- const { colors } = useTheme7();
982
+ const { colors } = useTheme8();
967
983
  const variantStyle = variant === "primary" ? { backgroundColor: colors.primaryContainer } : variant === "secondary" ? { backgroundColor: colors.secondaryContainer } : variant === "tertiary" ? { backgroundColor: colors.tertiaryContainer } : variant === "surface" ? { backgroundColor: colors.surface } : void 0;
968
984
  const variantSelectedColor = variant === "primary" ? colors.onPrimaryContainer : variant === "secondary" ? colors.onSecondaryContainer : variant === "tertiary" ? colors.onTertiaryContainer : variant === "surface" ? colors.onSurface : void 0;
969
985
  return /* @__PURE__ */ jsx13(PaperChip, { ...defaults.Chip, selectedColor: selectedColor ?? variantSelectedColor, ...props, style: [variantStyle, style] });
@@ -978,7 +994,7 @@ function FAB(props) {
978
994
  }
979
995
 
980
996
  // src/components/FadingScrollView.tsx
981
- import { useCallback as useCallback2, useRef as useRef2, useState as useState6 } from "react";
997
+ import { useCallback as useCallback3, useRef as useRef2, useState as useState6 } from "react";
982
998
  import { Platform as Platform4, ScrollView, StyleSheet as StyleSheet6, View as View6 } from "react-native";
983
999
  import { jsx as jsx15, jsxs as jsxs7 } from "react/jsx-runtime";
984
1000
  var SCROLL_FADE_HEIGHT = 24;
@@ -986,7 +1002,7 @@ function FadingScrollView({ style, fadeHeight = SCROLL_FADE_HEIGHT, onScroll, on
986
1002
  const MaskedView = useMaskedViewModule();
987
1003
  const sizes = useRef2({ viewport: 0, content: 0, offset: 0 });
988
1004
  const [edges, setEdges] = useState6(NO_EDGES);
989
- const update = useCallback2(() => {
1005
+ const update = useCallback3(() => {
990
1006
  const { viewport, content, offset } = sizes.current;
991
1007
  const above = offset > 1;
992
1008
  const below = content - viewport - offset > 1;
@@ -1060,11 +1076,11 @@ var styles5 = StyleSheet6.create({
1060
1076
  });
1061
1077
 
1062
1078
  // src/components/IconButton.tsx
1063
- import { IconButton as PaperIconButton, useTheme as useTheme8 } from "react-native-paper";
1079
+ import { IconButton as PaperIconButton, useTheme as useTheme9 } from "react-native-paper";
1064
1080
  import { jsx as jsx16 } from "react/jsx-runtime";
1065
1081
  function IconButton({ variant, iconColor, containerColor, ...props }) {
1066
1082
  const defaults = usePaperDefaults();
1067
- const { colors } = useTheme8();
1083
+ const { colors } = useTheme9();
1068
1084
  const variantContainerColor = variant === "primary" ? colors.primaryContainer : variant === "secondary" ? colors.secondaryContainer : variant === "tertiary" ? colors.tertiaryContainer : variant === "surface" ? colors.surface : void 0;
1069
1085
  const variantIconColor = variant === "primary" ? colors.onPrimaryContainer : variant === "secondary" ? colors.onSecondaryContainer : variant === "tertiary" ? colors.onTertiaryContainer : variant === "surface" ? colors.onSurface : void 0;
1070
1086
  return /* @__PURE__ */ jsx16(PaperIconButton, { ...defaults.IconButton, containerColor: containerColor ?? variantContainerColor, iconColor: iconColor ?? variantIconColor, ...props });
@@ -1073,7 +1089,7 @@ function IconButton({ variant, iconColor, containerColor, ...props }) {
1073
1089
  // src/components/Menu.tsx
1074
1090
  import { useEffect as useEffect5, useState as useState7 } from "react";
1075
1091
  import { Animated as Animated2, Easing as Easing2, Platform as Platform5, StyleSheet as StyleSheet7 } from "react-native";
1076
- import { Menu as PaperMenu, useTheme as useTheme9 } from "react-native-paper";
1092
+ import { Menu as PaperMenu, useTheme as useTheme10 } from "react-native-paper";
1077
1093
  import { jsx as jsx17 } from "react/jsx-runtime";
1078
1094
  var ANIMATION_DURATION2 = 250;
1079
1095
  var EASING = Easing2.bezier(0.4, 0, 0.2, 1);
@@ -1082,7 +1098,7 @@ var SHADOW_HEIGHT = [0, 1, 2, 4, 6, 8];
1082
1098
  var SHADOW_RADIUS = [0, 3, 6, 8, 10, 12];
1083
1099
  var AnimatedBlurView2 = Animated2.createAnimatedComponent(BlurView);
1084
1100
  var FadingBlurView = ({ blur, children, elevation, theme: themeOverrides, visible }) => {
1085
- const { animation, roundness } = useTheme9(themeOverrides);
1101
+ const { animation, roundness } = useTheme10(themeOverrides);
1086
1102
  const [opacity] = useState7(() => new Animated2.Value(0));
1087
1103
  const [closing, setClosing] = useState7(false);
1088
1104
  if (!visible && !closing) setClosing(true);
@@ -1140,12 +1156,95 @@ import { useTheme as usePaperTheme } from "react-native-paper";
1140
1156
  var useAutoPaperTheme = () => usePaperTheme();
1141
1157
 
1142
1158
  // src/useThemeBridgeProps.ts
1143
- import { useMemo as useMemo2 } from "react";
1159
+ import { useMemo as useMemo3 } from "react";
1144
1160
  function useThemeBridgeProps(props) {
1145
1161
  const { initialValue, onChange, onReady } = props;
1146
- return useMemo2(() => ({ initialValue, onChange, onReady }), [initialValue, onChange, onReady]);
1162
+ return useMemo3(() => ({ initialValue, onChange, onReady }), [initialValue, onChange, onReady]);
1147
1163
  }
1148
1164
 
1165
+ // src/utils/getOklch.ts
1166
+ var toLinear = (c) => c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
1167
+ var toGamma = (c) => c <= 31308e-7 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055;
1168
+ var linearToOklab = ([r, g, b]) => {
1169
+ const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
1170
+ const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
1171
+ const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
1172
+ return [0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s, 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s, 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s];
1173
+ };
1174
+ var oklabToLinear = ([L, a, b]) => {
1175
+ const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
1176
+ const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
1177
+ const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
1178
+ return [4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s];
1179
+ };
1180
+ var inGamut = (rgb) => rgb.every((v) => v >= -1e-6 && v <= 1 + 1e-6);
1181
+ var clamp01 = (v) => Math.min(1, Math.max(0, v));
1182
+ var toHex = (rgb) => `#${rgb.map(
1183
+ (v) => Math.round(clamp01(toGamma(clamp01(v))) * 255).toString(16).padStart(2, "0")
1184
+ ).join("")}`;
1185
+ var getOklch = (value) => {
1186
+ const rgb = getRgb(value);
1187
+ if (!rgb) return null;
1188
+ const [l, a, b] = linearToOklab([toLinear(rgb.r / 255), toLinear(rgb.g / 255), toLinear(rgb.b / 255)]);
1189
+ return { l, c: Math.hypot(a, b), h: (Math.atan2(b, a) * 180 / Math.PI + 360) % 360 };
1190
+ };
1191
+ var getOklchHex = ({ l, c, h }) => {
1192
+ const lightness = Math.min(1, Math.max(0, l));
1193
+ const radians = h * Math.PI / 180;
1194
+ const atChroma = (chroma) => oklabToLinear([lightness, chroma * Math.cos(radians), chroma * Math.sin(radians)]);
1195
+ const direct = atChroma(Math.max(0, c));
1196
+ if (inGamut(direct)) return toHex(direct);
1197
+ let low = 0;
1198
+ let high = c;
1199
+ for (let i = 0; i < 24; i++) {
1200
+ const mid = (low + high) / 2;
1201
+ if (inGamut(atChroma(mid))) low = mid;
1202
+ else high = mid;
1203
+ }
1204
+ return toHex(atChroma(low));
1205
+ };
1206
+
1207
+ // src/utils/getChalkboardColor.ts
1208
+ var MARKER = { maxLightness: 0.6, chroma: 0.85 };
1209
+ var CHALK = { lightness: 0.86, lift: 0.5, chroma: 0.8, minLuminance: 0.5 };
1210
+ var CACHE_LIMIT = 512;
1211
+ var createChalkboardTone = (options = {}) => {
1212
+ const marker = { ...MARKER, ...options.marker };
1213
+ const chalk = { ...CHALK, ...options.chalk };
1214
+ const cache = /* @__PURE__ */ new Map();
1215
+ const toMarker = (color) => {
1216
+ const oklch = getOklch(color);
1217
+ if (!oklch) return color;
1218
+ return getOklchHex({ l: Math.min(oklch.l, marker.maxLightness), c: oklch.c * marker.chroma, h: oklch.h });
1219
+ };
1220
+ const toChalk = (color) => {
1221
+ const oklch = getOklch(color);
1222
+ if (!oklch) return color;
1223
+ const c = oklch.c * chalk.chroma;
1224
+ const lifted = oklch.l < chalk.lightness ? oklch.l + (chalk.lightness - oklch.l) * chalk.lift : oklch.l;
1225
+ const result = getOklchHex({ l: lifted, c, h: oklch.h });
1226
+ if (getLuminance(result) >= chalk.minLuminance) return result;
1227
+ let low = lifted;
1228
+ let high = 1;
1229
+ for (let i = 0; i < 20; i++) {
1230
+ const mid = (low + high) / 2;
1231
+ if (getLuminance(getOklchHex({ l: mid, c, h: oklch.h })) >= chalk.minLuminance) high = mid;
1232
+ else low = mid;
1233
+ }
1234
+ return getOklchHex({ l: high, c, h: oklch.h });
1235
+ };
1236
+ return (color, dark) => {
1237
+ const key = `${dark ? "d" : "l"}${color}`;
1238
+ const cached = cache.get(key);
1239
+ if (cached !== void 0) return cached;
1240
+ const result = dark ? toChalk(color) : toMarker(color);
1241
+ if (cache.size >= CACHE_LIMIT) cache.clear();
1242
+ cache.set(key, result);
1243
+ return result;
1244
+ };
1245
+ };
1246
+ var getChalkboardColor = createChalkboardTone();
1247
+
1149
1248
  // src/utils/getThirdColor.ts
1150
1249
  function rgbToHsl(r, g, b) {
1151
1250
  r /= 255;
@@ -1249,15 +1348,19 @@ export {
1249
1348
  SEMANTIC_BASE_COLORS,
1250
1349
  TextInput,
1251
1350
  ThemeSettingsContext,
1351
+ createChalkboardTone,
1252
1352
  createThemeReducer,
1253
1353
  defaultColors,
1254
1354
  defaultThemeSettings,
1255
1355
  getBlendedColor,
1356
+ getChalkboardColor,
1256
1357
  getColorRoles,
1257
1358
  getContrastColor,
1258
1359
  getContrastRatio,
1259
1360
  getHex,
1260
1361
  getLuminance,
1362
+ getOklch,
1363
+ getOklchHex,
1261
1364
  getRgb,
1262
1365
  getThirdColor,
1263
1366
  getTintTextColor,
@@ -1277,6 +1380,7 @@ export {
1277
1380
  useComputedTheme,
1278
1381
  useMaskedViewModule,
1279
1382
  useNavBarContext,
1383
+ usePaletteTone,
1280
1384
  usePaperDefaults,
1281
1385
  useReanimatedModule,
1282
1386
  useThemeBridgeProps,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rific/auto-paper",
3
- "version": "0.13.4",
3
+ "version": "0.14.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",
@@ -9,6 +9,7 @@ import type { ExpoNavigationBarModule } from './navigation-bar'
9
9
  import { type PaperDefaults, PaperDefaultsContext } from './PaperDefaultsContext'
10
10
  import { type ThemeSettings, themeSettingsProvider, useThemeSettingsInternal } from './ThemeSettingsContext'
11
11
  import { useComputedTheme } from './useComputedTheme'
12
+ import type { PaletteTone } from './utils/getChalkboardColor'
12
13
 
13
14
  // expo-navigation-bar/expo-blur are no longer auto-detected via require() - Metro doesn't
14
15
  // rewrite a require()-in-try/catch call into its module graph inside an ESM (.mjs) build, so
@@ -28,6 +29,10 @@ export const useReanimatedModule = () => useContext(ReanimatedModuleContext)
28
29
  const MaskedViewModuleContext = createContext<MaskedViewModule | undefined>(undefined)
29
30
  export const useMaskedViewModule = () => useContext(MaskedViewModuleContext)
30
31
 
32
+ // The Provider's `paletteTone`, for AutoPalettePicker to preview swatches the way the theme will draw them.
33
+ const PaletteToneContext = createContext<PaletteTone | undefined>(undefined)
34
+ export const usePaletteTone = () => useContext(PaletteToneContext)
35
+
31
36
  // Whether this package's own components (Provider, Appbar.Header) render a React Native StatusBar - see `statusBar` below.
32
37
  const StatusBarEnabledContext = createContext(true)
33
38
  export const useStatusBarEnabled = () => useContext(StatusBarEnabledContext)
@@ -51,6 +56,15 @@ export type ProviderProps = {
51
56
  onChange?: (settings: ThemeSettings) => void
52
57
  onNavBarChange?: (color: string, dark: boolean) => void
53
58
  onReady?: () => void
59
+ /** Maps each palette color (primary, secondary and tertiary, after the harmony rotation) before the
60
+ * theme derives anything from it, for an app that wants its whole palette toned, e.g.
61
+ * `getChalkboardColor` (marker in light mode, chalk in dark). The stored seed stays the player's
62
+ * raw pick, so nothing compounds on save, and `AutoPalettePicker` previews its swatches through
63
+ * the same tone. onColor/onContainer follow the toned colors by the usual light/dark split, so a
64
+ * tone decides their polarity: chalk's luminance floor is what gives its pastels dark text.
65
+ * Signal colors (success, warning, danger, MD3's error) are never toned. Must be referentially stable (a module-level
66
+ * function): the theme recomputes whenever it changes. Omit for the untoned palette. */
67
+ paletteTone?: PaletteTone
54
68
  /** Injects react-native-reanimated so `<Dialog animatedStyle={...}>` (e.g. from `useAnimatedStyle()`) can animate its card on the UI thread. Pass `import Reanimated from 'react-native-reanimated'`; omit to keep Dialog's `animatedStyle` prop a no-op. */
55
69
  reanimated?: ReanimatedModule
56
70
  /** Renders React Native's `StatusBar`, styled from the theme (here and in `Appbar.Header`). Default true. Pass false
@@ -68,10 +82,10 @@ export type ProviderProps = {
68
82
  // wrapper for the settings half; ThemeProviderBody (an actual descendant, reached via
69
83
  // useThemeSettingsInternal()) does all the theme-computation/gating work the single combined
70
84
  // component used to do directly.
71
- export function Provider({ children, defaults, expoBlur, fontFamily, initialValue, maskedView, navigationBar, onChange, onNavBarChange, onReady, reanimated, statusBar, statusBarProps, style }: ProviderProps) {
85
+ export function Provider({ children, defaults, expoBlur, fontFamily, initialValue, maskedView, navigationBar, onChange, onNavBarChange, onReady, paletteTone, reanimated, statusBar, statusBarProps, style }: ProviderProps) {
72
86
  return (
73
87
  <ThemeSettingsProvider initialValue={initialValue} onChange={onChange}>
74
- <ThemeProviderBody defaults={defaults} expoBlur={expoBlur} fontFamily={fontFamily} maskedView={maskedView} navigationBar={navigationBar} onNavBarChange={onNavBarChange} onReady={onReady} reanimated={reanimated} statusBar={statusBar} statusBarProps={statusBarProps} style={style}>
88
+ <ThemeProviderBody defaults={defaults} expoBlur={expoBlur} fontFamily={fontFamily} maskedView={maskedView} navigationBar={navigationBar} onNavBarChange={onNavBarChange} onReady={onReady} paletteTone={paletteTone} reanimated={reanimated} statusBar={statusBar} statusBarProps={statusBarProps} style={style}>
75
89
  {children}
76
90
  </ThemeProviderBody>
77
91
  </ThemeSettingsProvider>
@@ -82,10 +96,10 @@ const ThemeSettingsProvider = themeSettingsProvider
82
96
 
83
97
  type ThemeProviderBodyProps = Omit<ProviderProps, 'initialValue' | 'onChange'>
84
98
 
85
- function ThemeProviderBody({ children, defaults, expoBlur, fontFamily, maskedView, navigationBar, onNavBarChange, onReady, reanimated, statusBar = true, statusBarProps, style }: ThemeProviderBodyProps) {
99
+ function ThemeProviderBody({ children, defaults, expoBlur, fontFamily, maskedView, navigationBar, onNavBarChange, onReady, paletteTone, reanimated, statusBar = true, statusBarProps, style }: ThemeProviderBodyProps) {
86
100
  const { settings } = useThemeSettingsInternal()
87
101
 
88
- const computedTheme = useComputedTheme(settings.appearance, settings.color, settings.harmony)
102
+ const computedTheme = useComputedTheme(settings.appearance, settings.color, settings.harmony, paletteTone)
89
103
  // Layered on top of useComputedTheme's own output rather than folded into it - that hook only
90
104
  // ever recomputes for a color/appearance/harmony change, and fontFamily is a completely
91
105
  // independent axis; merging it there would mean either re-running the whole color computation on
@@ -116,17 +130,19 @@ function ThemeProviderBody({ children, defaults, expoBlur, fontFamily, maskedVie
116
130
  <BlurModuleContext.Provider value={expoBlur}>
117
131
  <ReanimatedModuleContext.Provider value={reanimated}>
118
132
  <MaskedViewModuleContext.Provider value={maskedView}>
119
- {/* PaperProvider's own PortalHost sits above every context provided here, and Paper's
133
+ <PaletteToneContext.Provider value={paletteTone}>
134
+ {/* PaperProvider's own PortalHost sits above every context provided here, and Paper's
120
135
  Portal only carries its theme and settings across, so anything rendered through a
121
136
  Portal (this package's Dialog, Paper's Menu and Tooltip, or anything an app wraps
122
- in <Portal> itself) would otherwise lose expoBlur, reanimated, maskedView, defaults, statusBar and the nav
137
+ in <Portal> itself) would otherwise lose expoBlur, reanimated, maskedView, paletteTone, defaults, statusBar and the nav
123
138
  bar hooks: Dialog/Menu's blur silently fell back to solid. This nested host is the
124
139
  nearest one for every Portal under Provider, so their content mounts inside all of
125
140
  these contexts instead. It wraps the styled View, not the other way round, so
126
141
  portals still cover the full area regardless of `style`. */}
127
- <Portal.Host>
128
- <View style={[styles.flex, { backgroundColor: theme.colors.background }, style]}>{children}</View>
129
- </Portal.Host>
142
+ <Portal.Host>
143
+ <View style={[styles.flex, { backgroundColor: theme.colors.background }, style]}>{children}</View>
144
+ </Portal.Host>
145
+ </PaletteToneContext.Provider>
130
146
  </MaskedViewModuleContext.Provider>
131
147
  </ReanimatedModuleContext.Provider>
132
148
  </BlurModuleContext.Provider>
@@ -1,3 +1,7 @@
1
+ import { useCallback } from 'react'
2
+ import { useTheme } from 'react-native-paper'
3
+
4
+ import { usePaletteTone } from '../ThemeProvider'
1
5
  import { useThemeSettings } from '../useThemeSettings'
2
6
  import { resolveSeedColor } from '../utils/resolveSeedColor'
3
7
  import type { SeedColor } from './ColorPicker'
@@ -14,8 +18,13 @@ type Props = {
14
18
  // `value`/`onChange`/`harmony`/`onHarmonyChange` - reads/writes useThemeSettings() itself
15
19
  // (resolveSeedColor narrows ThemeSettings.color's `string | TriadicPalette` down to the plain
16
20
  // string PalettePicker's own `value` wants). Reach for the plain PalettePicker instead when it
17
- // needs to be controlled by something other than the global theme.
21
+ // needs to be controlled by something other than the global theme. It also previews the swatches
22
+ // through the Provider's `paletteTone`, if there is one, in the current appearance, so they match
23
+ // the theme they'd produce.
18
24
  export const AutoPalettePicker = (props: Props) => {
19
25
  const { settings, set } = useThemeSettings()
20
- return <PalettePicker {...props} value={resolveSeedColor(settings.color)} onChange={(color) => set({ color })} harmony={settings.harmony} onHarmonyChange={(harmony) => set({ harmony })} />
26
+ const tone = usePaletteTone()
27
+ const { dark } = useTheme()
28
+ const toned = useCallback((color: string) => tone!(color, dark), [tone, dark])
29
+ return <PalettePicker {...props} value={resolveSeedColor(settings.color)} onChange={(color) => set({ color })} harmony={settings.harmony} onHarmonyChange={(harmony) => set({ harmony })} previewColor={tone ? toned : undefined} />
21
30
  }
@@ -1,4 +1,4 @@
1
- import { useState } from 'react'
1
+ import { useMemo, useState } from 'react'
2
2
  import { StyleSheet, TouchableOpacity, View } from 'react-native'
3
3
  import { Icon, TouchableRipple, useTheme } from 'react-native-paper'
4
4
 
@@ -65,21 +65,35 @@ type Props = {
65
65
  weights?: PaletteWeights
66
66
  colors?: SeedColor[]
67
67
  blur?: boolean
68
+ // Opt-in: maps each wedge's color (after the harmony rotation) to what's actually drawn, so the
69
+ // swatches preview a toned theme (see Provider's `paletteTone`; AutoPalettePicker wires it from
70
+ // there). Display only: `value`, the check mark's match and onChange stay the raw seed hex.
71
+ previewColor?: (color: string) => string
68
72
  }
69
73
 
70
- export const PalettePicker = ({ value, onChange, harmony = 'split-complementary', onHarmonyChange, weights, colors = defaultColors, blur: blurProp }: Props) => {
74
+ export const PalettePicker = ({ value, onChange, harmony = 'split-complementary', onHarmonyChange, weights, colors = defaultColors, blur: blurProp, previewColor }: Props) => {
71
75
  const blur = useBlur(blurProp)
72
76
  const { colors: themeColors } = useTheme()
73
77
  const [open, setOpen] = useState(false)
74
78
  const { onLayout, size } = useSwatchSize(SIZE)
79
+ // What each pie draws. The rings and check marks contrast with the drawn primary, not the raw
80
+ // seed, since a toned swatch can be much lighter or darker than its seed.
81
+ const shade = useMemo(
82
+ () =>
83
+ (palette: TriadicPalette): TriadicPalette =>
84
+ previewColor ? { primary: previewColor(palette.primary), secondary: previewColor(palette.secondary), tertiary: previewColor(palette.tertiary) } : palette,
85
+ [previewColor]
86
+ )
87
+ const triggerPalette = shade(getTriadicPalette(value, harmony))
88
+ const swatchPalettes = useMemo(() => colors.map(({ value: hex }) => shade(getTriadicPalette(hex, harmony))), [colors, harmony, shade])
75
89
 
76
90
  return (
77
91
  <>
78
92
  <TouchableRipple onPress={() => setOpen(true)} style={styles.trigger} borderless>
79
93
  <View style={styles.triggerContent}>
80
- <Pie palette={getTriadicPalette(value, harmony)} weights={weights} />
81
- <View pointerEvents='none' style={[styles.triggerIcon, { borderRadius: SIZE / 2 }, getSwatchRing(value, false, themeColors.outlineVariant)]}>
82
- <Icon source='palette' size={20} color={getContrastColor(value)} />
94
+ <Pie palette={triggerPalette} weights={weights} />
95
+ <View pointerEvents='none' style={[styles.triggerIcon, { borderRadius: SIZE / 2 }, getSwatchRing(triggerPalette.primary, false, themeColors.outlineVariant)]}>
96
+ <Icon source='palette' size={20} color={getContrastColor(triggerPalette.primary)} />
83
97
  </View>
84
98
  </View>
85
99
  </TouchableRipple>
@@ -92,7 +106,7 @@ export const PalettePicker = ({ value, onChange, harmony = 'split-complementary'
92
106
  </View>
93
107
  )}
94
108
  <View style={styles.swatches} onLayout={onLayout}>
95
- {colors.map(({ value: hex }) => (
109
+ {colors.map(({ value: hex }, index) => (
96
110
  <TouchableOpacity
97
111
  key={hex}
98
112
  onPress={() => {
@@ -101,9 +115,9 @@ export const PalettePicker = ({ value, onChange, harmony = 'split-complementary'
101
115
  }}
102
116
  style={[styles.swatch, { height: size, width: size }]}
103
117
  >
104
- <Pie palette={getTriadicPalette(hex, harmony)} weights={weights} size={size} />
105
- <View pointerEvents='none' style={[styles.ring, { borderRadius: size / 2 }, getSwatchRing(hex, value === hex, themeColors.outlineVariant)]}>
106
- {value === hex && <Icon source='check' size={20} color={getContrastColor(hex)} />}
118
+ <Pie palette={swatchPalettes[index]} weights={weights} size={size} />
119
+ <View pointerEvents='none' style={[styles.ring, { borderRadius: size / 2 }, getSwatchRing(swatchPalettes[index].primary, value === hex, themeColors.outlineVariant)]}>
120
+ {value === hex && <Icon source='check' size={20} color={getContrastColor(swatchPalettes[index].primary)} />}
107
121
  </View>
108
122
  </TouchableOpacity>
109
123
  ))}
package/src/index.ts CHANGED
@@ -31,7 +31,7 @@ export { usePaperDefaults } from './PaperDefaultsContext'
31
31
  export type { ThemeState } from './redux/themeSlice'
32
32
  export { createThemeReducer, selectThemeAppearance, selectThemeBlur, selectThemeColor, selectThemeHarmony, themeActions, themeReducer } from './redux/themeSlice'
33
33
  export type { ProviderProps } from './ThemeProvider'
34
- export { Provider, useBlurModule, useMaskedViewModule, useNavBarContext, useReanimatedModule } from './ThemeProvider'
34
+ export { Provider, useBlurModule, useMaskedViewModule, useNavBarContext, usePaletteTone, useReanimatedModule } from './ThemeProvider'
35
35
  export type { ThemeSettings, ThemeSettingsContextType } from './ThemeSettingsContext'
36
36
  export { defaultThemeSettings, ThemeSettingsContext } from './ThemeSettingsContext'
37
37
  export type { AutoPaperTheme, SemanticColorRoles, ThemeAppearance } from './useComputedTheme'
@@ -41,10 +41,14 @@ export type { ThemeBridgeProps } from './useThemeBridgeProps'
41
41
  export { useThemeBridgeProps } from './useThemeBridgeProps'
42
42
  export { useThemeSettings } from './useThemeSettings'
43
43
  export { getBlendedColor } from './utils/getBlendedColor'
44
+ export type { ChalkboardToneOptions, PaletteTone } from './utils/getChalkboardColor'
45
+ export { createChalkboardTone, getChalkboardColor } from './utils/getChalkboardColor'
44
46
  export type { ColorRoles } from './utils/getColorRoles'
45
47
  export { getColorRoles } from './utils/getColorRoles'
46
48
  export { getContrastRatio, getLuminance } from './utils/getContrastRatio'
47
49
  export { getHex } from './utils/getHex'
50
+ export type { Oklch } from './utils/getOklch'
51
+ export { getOklch, getOklchHex } from './utils/getOklch'
48
52
  export type { RGB } from './utils/getRgb'
49
53
  export { getRgb } from './utils/getRgb'
50
54
  export { getContrastColor } from './utils/getSwatchContrast'
@@ -3,6 +3,7 @@ import { Appearance } from 'react-native'
3
3
  import { MD3DarkTheme, MD3LightTheme, MD3Theme } from 'react-native-paper'
4
4
 
5
5
  import { getBlendedColor } from './utils/getBlendedColor'
6
+ import type { PaletteTone } from './utils/getChalkboardColor'
6
7
  import { getColorRoles } from './utils/getColorRoles'
7
8
  import { getHex } from './utils/getHex'
8
9
  import { getRgb } from './utils/getRgb'
@@ -57,7 +58,9 @@ export type SemanticColorRoles = {
57
58
  // with react-native-paper's own `useTheme<T>()` generic (see useTheme.ts's useAutoPaperTheme).
58
59
  export type AutoPaperTheme = Omit<MD3Theme, 'colors'> & { colors: MD3Theme['colors'] & SemanticColorRoles }
59
60
 
60
- export function useComputedTheme(appearance: ThemeAppearance, color: string | TriadicPalette, harmony: ColorHarmony = 'split-complementary'): AutoPaperTheme | null {
61
+ // `paletteTone` (optional, see Provider's prop of the same name) maps each resolved palette color
62
+ // before anything is derived from it. It must be referentially stable: it's a dependency.
63
+ export function useComputedTheme(appearance: ThemeAppearance, color: string | TriadicPalette, harmony: ColorHarmony = 'split-complementary', paletteTone?: PaletteTone): AutoPaperTheme | null {
61
64
  const [theme, setTheme] = useState<AutoPaperTheme | null>(null)
62
65
 
63
66
  // Destructured to primitives (rather than depending on `color` itself below) so that passing a
@@ -104,7 +107,12 @@ export function useComputedTheme(appearance: ThemeAppearance, color: string | Tr
104
107
  }
105
108
  }
106
109
 
107
- const palette: TriadicPalette = secondaryInput !== null && tertiaryInput !== null ? { primary: normalizeExplicitColor(primaryInput), secondary: normalizeExplicitColor(secondaryInput), tertiary: normalizeExplicitColor(tertiaryInput) } : getTriadicPalette(primaryInput, harmony)
110
+ const untoned: TriadicPalette = secondaryInput !== null && tertiaryInput !== null ? { primary: normalizeExplicitColor(primaryInput), secondary: normalizeExplicitColor(secondaryInput), tertiary: normalizeExplicitColor(tertiaryInput) } : getTriadicPalette(primaryInput, harmony)
111
+ // Toned per member, after the harmony rotation: a tone that moves lightness doesn't commute
112
+ // with rotating hue, so toning the seed first would give secondary and tertiary colors the
113
+ // pickers never showed. Everything below (roles, containers, outline, surfaceVariant,
114
+ // elevation) derives from the toned palette; the semantic roles above never pass through it.
115
+ const palette: TriadicPalette = paletteTone ? { primary: paletteTone(untoned.primary, base.dark), secondary: paletteTone(untoned.secondary, base.dark), tertiary: paletteTone(untoned.tertiary, base.dark) } : untoned
108
116
 
109
117
  const primaryRoles = getColorRoles(palette.primary, surface)
110
118
  _theme.colors.primary = primaryRoles.color
@@ -144,7 +152,7 @@ export function useComputedTheme(appearance: ThemeAppearance, color: string | Tr
144
152
  }
145
153
 
146
154
  setTheme(_theme)
147
- }, [appearance, primaryInput, secondaryInput, tertiaryInput, harmony])
155
+ }, [appearance, primaryInput, secondaryInput, tertiaryInput, harmony, paletteTone])
148
156
 
149
157
  useEffect(() => {
150
158
  // eslint-disable-next-line react-hooks/set-state-in-effect -- intentional: first render must stay theme-less (README's onReady/splash-screen contract) and Appearance must not be read during render (SSR/hydration risk)