@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/README.md +11 -1
- package/dist/index.d.mts +39 -4
- package/dist/index.d.ts +39 -4
- package/dist/index.js +166 -57
- package/dist/index.mjs +137 -33
- package/package.json +1 -1
- package/src/ThemeProvider.tsx +25 -9
- package/src/components/AutoPalettePicker.tsx +11 -2
- package/src/components/PalettePicker.tsx +23 -9
- package/src/index.ts +5 -1
- package/src/useComputedTheme.ts +11 -3
- package/src/utils/getChalkboardColor.ts +77 -0
- package/src/utils/getOklch.ts +68 -0
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
|
|
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 = `#${
|
|
251
|
+
const hexString = `#${toHex2(rgb.r)}${toHex2(rgb.g)}${toHex2(rgb.b)}`;
|
|
252
252
|
if (rgb.a !== void 0) {
|
|
253
|
-
return `${hexString}${
|
|
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
|
|
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 `#${
|
|
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
|
|
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:
|
|
874
|
-
/* @__PURE__ */ jsx9(View5, { pointerEvents: "none", style: [styles4.triggerIcon, { borderRadius: SIZE2 / 2 }, getSwatchRing(
|
|
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:
|
|
888
|
-
/* @__PURE__ */ jsx9(View5, { pointerEvents: "none", style: [styles4.ring, { borderRadius: size / 2 }, getSwatchRing(
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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 } =
|
|
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
|
|
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 =
|
|
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
|
|
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 } =
|
|
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
|
|
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 } =
|
|
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
|
|
1159
|
+
import { useMemo as useMemo3 } from "react";
|
|
1144
1160
|
function useThemeBridgeProps(props) {
|
|
1145
1161
|
const { initialValue, onChange, onReady } = props;
|
|
1146
|
-
return
|
|
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
package/src/ThemeProvider.tsx
CHANGED
|
@@ -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
|
-
{
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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={
|
|
81
|
-
<View pointerEvents='none' style={[styles.triggerIcon, { borderRadius: SIZE / 2 }, getSwatchRing(
|
|
82
|
-
<Icon source='palette' size={20} color={getContrastColor(
|
|
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={
|
|
105
|
-
<View pointerEvents='none' style={[styles.ring, { borderRadius: size / 2 }, getSwatchRing(
|
|
106
|
-
{value === hex && <Icon source='check' size={20} color={getContrastColor(
|
|
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'
|
package/src/useComputedTheme.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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)
|