@rific/auto-paper 0.13.3 → 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 +14 -2
- package/dist/index.d.mts +42 -4
- package/dist/index.d.ts +42 -4
- package/dist/index.js +188 -58
- package/dist/index.mjs +157 -34
- package/package.json +2 -2
- package/src/ThemeProvider.tsx +25 -9
- package/src/components/AutoPalettePicker.tsx +11 -2
- package/src/components/PalettePicker.tsx +23 -9
- package/src/index.ts +6 -1
- package/src/useComputedTheme.ts +11 -3
- package/src/utils/getChalkboardColor.ts +77 -0
- package/src/utils/getColorRoles.ts +13 -2
- package/src/utils/getContrastRatio.ts +20 -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
|
};
|
|
@@ -267,6 +267,21 @@ var getBlendedColor = (foreground, background, alpha) => {
|
|
|
267
267
|
return getHex(`rgb(${r}, ${g}, ${b})`) ?? background;
|
|
268
268
|
};
|
|
269
269
|
|
|
270
|
+
// src/utils/getContrastRatio.ts
|
|
271
|
+
var getLuminance = (value) => {
|
|
272
|
+
const rgb = getRgb(value);
|
|
273
|
+
if (!rgb) return 0;
|
|
274
|
+
const [r, g, b] = [rgb.r, rgb.g, rgb.b].map((channel) => {
|
|
275
|
+
const c = channel / 255;
|
|
276
|
+
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
277
|
+
});
|
|
278
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
279
|
+
};
|
|
280
|
+
var getContrastRatio = (a, b) => {
|
|
281
|
+
const [lighter, darker] = [getLuminance(a), getLuminance(b)].sort((x, y) => y - x);
|
|
282
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
283
|
+
};
|
|
284
|
+
|
|
270
285
|
// src/utils/getTonalColor.ts
|
|
271
286
|
var getTonalColor = (color, lightness) => {
|
|
272
287
|
const rgb = getRgb(color);
|
|
@@ -331,11 +346,13 @@ var isDarkColor = (value) => {
|
|
|
331
346
|
};
|
|
332
347
|
|
|
333
348
|
// src/utils/getColorRoles.ts
|
|
349
|
+
var MIN_LIGHT_ON_COLOR_CONTRAST = 2.5;
|
|
334
350
|
var getColorRoles = (base, surface, containerAlpha = 0.15) => {
|
|
335
351
|
const container = getBlendedColor(base, surface, containerAlpha);
|
|
352
|
+
const lightOnColor = getTonalColor(base, 0.95);
|
|
336
353
|
return {
|
|
337
354
|
color: base,
|
|
338
|
-
onColor:
|
|
355
|
+
onColor: getContrastRatio(lightOnColor, base) >= MIN_LIGHT_ON_COLOR_CONTRAST ? lightOnColor : getTonalColor(base, 0.08),
|
|
339
356
|
container,
|
|
340
357
|
onContainer: getTonalColor(base, isDarkColor(container) ? 0.92 : 0.12)
|
|
341
358
|
};
|
|
@@ -415,11 +432,11 @@ function hslToHex(h, s, l) {
|
|
|
415
432
|
g = 0;
|
|
416
433
|
b = x;
|
|
417
434
|
}
|
|
418
|
-
const
|
|
435
|
+
const toHex2 = (value) => {
|
|
419
436
|
const hex = Math.round((value + m) * 255).toString(16);
|
|
420
437
|
return hex.length === 1 ? `0${hex}` : hex;
|
|
421
438
|
};
|
|
422
|
-
return `#${
|
|
439
|
+
return `#${toHex2(r)}${toHex2(g)}${toHex2(b)}`;
|
|
423
440
|
}
|
|
424
441
|
|
|
425
442
|
// src/useComputedTheme.ts
|
|
@@ -432,7 +449,7 @@ var SEMANTIC_BASE_COLORS = {
|
|
|
432
449
|
warning: "#A15C00",
|
|
433
450
|
danger: "#B00020"
|
|
434
451
|
};
|
|
435
|
-
function useComputedTheme(appearance, color, harmony = "split-complementary") {
|
|
452
|
+
function useComputedTheme(appearance, color, harmony = "split-complementary", paletteTone) {
|
|
436
453
|
const [theme, setTheme] = useState(null);
|
|
437
454
|
const primaryInput = typeof color === "string" ? color : color.primary;
|
|
438
455
|
const secondaryInput = typeof color === "string" ? null : color.secondary;
|
|
@@ -465,7 +482,8 @@ function useComputedTheme(appearance, color, harmony = "split-complementary") {
|
|
|
465
482
|
onDangerContainer: dangerRoles.onContainer
|
|
466
483
|
}
|
|
467
484
|
};
|
|
468
|
-
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;
|
|
469
487
|
const primaryRoles = getColorRoles(palette.primary, surface);
|
|
470
488
|
_theme.colors.primary = primaryRoles.color;
|
|
471
489
|
_theme.colors.onPrimary = primaryRoles.onColor;
|
|
@@ -498,7 +516,7 @@ function useComputedTheme(appearance, color, harmony = "split-complementary") {
|
|
|
498
516
|
level5: getBlendedColor(blendedGrey, _theme.colors.surface, 0.7)
|
|
499
517
|
};
|
|
500
518
|
setTheme(_theme);
|
|
501
|
-
}, [appearance, primaryInput, secondaryInput, tertiaryInput, harmony]);
|
|
519
|
+
}, [appearance, primaryInput, secondaryInput, tertiaryInput, harmony, paletteTone]);
|
|
502
520
|
useEffect(() => {
|
|
503
521
|
computeTheme();
|
|
504
522
|
}, [computeTheme]);
|
|
@@ -519,15 +537,17 @@ var ReanimatedModuleContext = createContext2(void 0);
|
|
|
519
537
|
var useReanimatedModule = () => useContext3(ReanimatedModuleContext);
|
|
520
538
|
var MaskedViewModuleContext = createContext2(void 0);
|
|
521
539
|
var useMaskedViewModule = () => useContext3(MaskedViewModuleContext);
|
|
540
|
+
var PaletteToneContext = createContext2(void 0);
|
|
541
|
+
var usePaletteTone = () => useContext3(PaletteToneContext);
|
|
522
542
|
var StatusBarEnabledContext = createContext2(true);
|
|
523
543
|
var useStatusBarEnabled = () => useContext3(StatusBarEnabledContext);
|
|
524
|
-
function Provider({ children, defaults, expoBlur, fontFamily, initialValue, maskedView, navigationBar, onChange, onNavBarChange, onReady, reanimated, statusBar, statusBarProps, style }) {
|
|
525
|
-
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 }) });
|
|
526
546
|
}
|
|
527
547
|
var ThemeSettingsProvider = themeSettingsProvider;
|
|
528
|
-
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 }) {
|
|
529
549
|
const { settings } = useThemeSettingsInternal();
|
|
530
|
-
const computedTheme = useComputedTheme(settings.appearance, settings.color, settings.harmony);
|
|
550
|
+
const computedTheme = useComputedTheme(settings.appearance, settings.color, settings.harmony, paletteTone);
|
|
531
551
|
const theme = useMemo(() => {
|
|
532
552
|
if (!computedTheme) return null;
|
|
533
553
|
if (!fontFamily) return computedTheme;
|
|
@@ -543,7 +563,7 @@ function ThemeProviderBody({ children, defaults, expoBlur, fontFamily, maskedVie
|
|
|
543
563
|
if (!theme) return null;
|
|
544
564
|
return /* @__PURE__ */ jsxs(PaperProvider, { theme, children: [
|
|
545
565
|
statusBar && /* @__PURE__ */ jsx(StatusBar, { barStyle: theme.dark ? "light-content" : "dark-content", ...statusBarProps }),
|
|
546
|
-
/* @__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 }) }) }) }) }) }) }) }) })
|
|
547
567
|
] });
|
|
548
568
|
}
|
|
549
569
|
var styles = StyleSheet.create({
|
|
@@ -596,11 +616,15 @@ var AutoAppearancePicker = (props) => {
|
|
|
596
616
|
return /* @__PURE__ */ jsx4(AppearancePicker, { ...props, value: settings.appearance, onChange: (appearance) => set({ appearance }) });
|
|
597
617
|
};
|
|
598
618
|
|
|
619
|
+
// src/components/AutoPalettePicker.tsx
|
|
620
|
+
import { useCallback as useCallback2 } from "react";
|
|
621
|
+
import { useTheme as useTheme6 } from "react-native-paper";
|
|
622
|
+
|
|
599
623
|
// src/utils/resolveSeedColor.ts
|
|
600
624
|
var resolveSeedColor = (color) => typeof color === "string" ? color : color.primary;
|
|
601
625
|
|
|
602
626
|
// src/components/PalettePicker.tsx
|
|
603
|
-
import { useState as useState5 } from "react";
|
|
627
|
+
import { useMemo as useMemo2, useState as useState5 } from "react";
|
|
604
628
|
import { StyleSheet as StyleSheet5, TouchableOpacity as TouchableOpacity2, View as View5 } from "react-native";
|
|
605
629
|
import { Icon as Icon2, TouchableRipple as TouchableRipple2, useTheme as useTheme5 } from "react-native-paper";
|
|
606
630
|
|
|
@@ -846,19 +870,25 @@ var buildWedges = (weights) => {
|
|
|
846
870
|
];
|
|
847
871
|
};
|
|
848
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)) });
|
|
849
|
-
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 }) => {
|
|
850
874
|
const blur = useBlur(blurProp);
|
|
851
875
|
const { colors: themeColors } = useTheme5();
|
|
852
876
|
const [open, setOpen] = useState5(false);
|
|
853
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]);
|
|
854
884
|
return /* @__PURE__ */ jsxs6(Fragment3, { children: [
|
|
855
885
|
/* @__PURE__ */ jsx9(TouchableRipple2, { onPress: () => setOpen(true), style: styles4.trigger, borderless: true, children: /* @__PURE__ */ jsxs6(View5, { style: styles4.triggerContent, children: [
|
|
856
|
-
/* @__PURE__ */ jsx9(Pie, { palette:
|
|
857
|
-
/* @__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) }) })
|
|
858
888
|
] }) }),
|
|
859
889
|
/* @__PURE__ */ jsx9(Dialog, { blur, visible: open, onDismiss: () => setOpen(false), children: /* @__PURE__ */ jsxs6(Dialog.Content, { children: [
|
|
860
890
|
onHarmonyChange && /* @__PURE__ */ jsx9(View5, { style: styles4.harmonyRow, children: /* @__PURE__ */ jsx9(HarmonyPicker, { value: harmony, onChange: onHarmonyChange, showLabels: false }) }),
|
|
861
|
-
/* @__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(
|
|
862
892
|
TouchableOpacity2,
|
|
863
893
|
{
|
|
864
894
|
onPress: () => {
|
|
@@ -867,8 +897,8 @@ var PalettePicker = ({ value, onChange, harmony = "split-complementary", onHarmo
|
|
|
867
897
|
},
|
|
868
898
|
style: [styles4.swatch, { height: size, width: size }],
|
|
869
899
|
children: [
|
|
870
|
-
/* @__PURE__ */ jsx9(Pie, { palette:
|
|
871
|
-
/* @__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) }) })
|
|
872
902
|
]
|
|
873
903
|
},
|
|
874
904
|
hex
|
|
@@ -895,13 +925,16 @@ var styles4 = StyleSheet5.create({
|
|
|
895
925
|
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
896
926
|
var AutoPalettePicker = (props) => {
|
|
897
927
|
const { settings, set } = useThemeSettings();
|
|
898
|
-
|
|
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 });
|
|
899
932
|
};
|
|
900
933
|
|
|
901
934
|
// src/components/BottomNavigation.tsx
|
|
902
935
|
import { useEffect as useEffect4 } from "react";
|
|
903
936
|
import { Platform as Platform3 } from "react-native";
|
|
904
|
-
import { BottomNavigation as PaperBottomNavigation, useTheme as
|
|
937
|
+
import { BottomNavigation as PaperBottomNavigation, useTheme as useTheme7 } from "react-native-paper";
|
|
905
938
|
|
|
906
939
|
// src/navigation-bar.ts
|
|
907
940
|
var setNavigationBarStyle = (navigationBar, dark) => {
|
|
@@ -912,7 +945,7 @@ var setNavigationBarStyle = (navigationBar, dark) => {
|
|
|
912
945
|
// src/components/BottomNavigation.tsx
|
|
913
946
|
import { jsx as jsx11 } from "react/jsx-runtime";
|
|
914
947
|
function useNavigationBarSync() {
|
|
915
|
-
const theme =
|
|
948
|
+
const theme = useTheme7();
|
|
916
949
|
const { navigationBar, onNavBarChange } = useNavBarContext();
|
|
917
950
|
useEffect4(() => {
|
|
918
951
|
if (Platform3.OS !== "android") return;
|
|
@@ -942,11 +975,11 @@ function Button(props) {
|
|
|
942
975
|
}
|
|
943
976
|
|
|
944
977
|
// src/components/Chip.tsx
|
|
945
|
-
import { Chip as PaperChip, useTheme as
|
|
978
|
+
import { Chip as PaperChip, useTheme as useTheme8 } from "react-native-paper";
|
|
946
979
|
import { jsx as jsx13 } from "react/jsx-runtime";
|
|
947
980
|
function Chip({ variant, style, selectedColor, ...props }) {
|
|
948
981
|
const defaults = usePaperDefaults();
|
|
949
|
-
const { colors } =
|
|
982
|
+
const { colors } = useTheme8();
|
|
950
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;
|
|
951
984
|
const variantSelectedColor = variant === "primary" ? colors.onPrimaryContainer : variant === "secondary" ? colors.onSecondaryContainer : variant === "tertiary" ? colors.onTertiaryContainer : variant === "surface" ? colors.onSurface : void 0;
|
|
952
985
|
return /* @__PURE__ */ jsx13(PaperChip, { ...defaults.Chip, selectedColor: selectedColor ?? variantSelectedColor, ...props, style: [variantStyle, style] });
|
|
@@ -961,7 +994,7 @@ function FAB(props) {
|
|
|
961
994
|
}
|
|
962
995
|
|
|
963
996
|
// src/components/FadingScrollView.tsx
|
|
964
|
-
import { useCallback as
|
|
997
|
+
import { useCallback as useCallback3, useRef as useRef2, useState as useState6 } from "react";
|
|
965
998
|
import { Platform as Platform4, ScrollView, StyleSheet as StyleSheet6, View as View6 } from "react-native";
|
|
966
999
|
import { jsx as jsx15, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
967
1000
|
var SCROLL_FADE_HEIGHT = 24;
|
|
@@ -969,7 +1002,7 @@ function FadingScrollView({ style, fadeHeight = SCROLL_FADE_HEIGHT, onScroll, on
|
|
|
969
1002
|
const MaskedView = useMaskedViewModule();
|
|
970
1003
|
const sizes = useRef2({ viewport: 0, content: 0, offset: 0 });
|
|
971
1004
|
const [edges, setEdges] = useState6(NO_EDGES);
|
|
972
|
-
const update =
|
|
1005
|
+
const update = useCallback3(() => {
|
|
973
1006
|
const { viewport, content, offset } = sizes.current;
|
|
974
1007
|
const above = offset > 1;
|
|
975
1008
|
const below = content - viewport - offset > 1;
|
|
@@ -1043,11 +1076,11 @@ var styles5 = StyleSheet6.create({
|
|
|
1043
1076
|
});
|
|
1044
1077
|
|
|
1045
1078
|
// src/components/IconButton.tsx
|
|
1046
|
-
import { IconButton as PaperIconButton, useTheme as
|
|
1079
|
+
import { IconButton as PaperIconButton, useTheme as useTheme9 } from "react-native-paper";
|
|
1047
1080
|
import { jsx as jsx16 } from "react/jsx-runtime";
|
|
1048
1081
|
function IconButton({ variant, iconColor, containerColor, ...props }) {
|
|
1049
1082
|
const defaults = usePaperDefaults();
|
|
1050
|
-
const { colors } =
|
|
1083
|
+
const { colors } = useTheme9();
|
|
1051
1084
|
const variantContainerColor = variant === "primary" ? colors.primaryContainer : variant === "secondary" ? colors.secondaryContainer : variant === "tertiary" ? colors.tertiaryContainer : variant === "surface" ? colors.surface : void 0;
|
|
1052
1085
|
const variantIconColor = variant === "primary" ? colors.onPrimaryContainer : variant === "secondary" ? colors.onSecondaryContainer : variant === "tertiary" ? colors.onTertiaryContainer : variant === "surface" ? colors.onSurface : void 0;
|
|
1053
1086
|
return /* @__PURE__ */ jsx16(PaperIconButton, { ...defaults.IconButton, containerColor: containerColor ?? variantContainerColor, iconColor: iconColor ?? variantIconColor, ...props });
|
|
@@ -1056,7 +1089,7 @@ function IconButton({ variant, iconColor, containerColor, ...props }) {
|
|
|
1056
1089
|
// src/components/Menu.tsx
|
|
1057
1090
|
import { useEffect as useEffect5, useState as useState7 } from "react";
|
|
1058
1091
|
import { Animated as Animated2, Easing as Easing2, Platform as Platform5, StyleSheet as StyleSheet7 } from "react-native";
|
|
1059
|
-
import { Menu as PaperMenu, useTheme as
|
|
1092
|
+
import { Menu as PaperMenu, useTheme as useTheme10 } from "react-native-paper";
|
|
1060
1093
|
import { jsx as jsx17 } from "react/jsx-runtime";
|
|
1061
1094
|
var ANIMATION_DURATION2 = 250;
|
|
1062
1095
|
var EASING = Easing2.bezier(0.4, 0, 0.2, 1);
|
|
@@ -1065,7 +1098,7 @@ var SHADOW_HEIGHT = [0, 1, 2, 4, 6, 8];
|
|
|
1065
1098
|
var SHADOW_RADIUS = [0, 3, 6, 8, 10, 12];
|
|
1066
1099
|
var AnimatedBlurView2 = Animated2.createAnimatedComponent(BlurView);
|
|
1067
1100
|
var FadingBlurView = ({ blur, children, elevation, theme: themeOverrides, visible }) => {
|
|
1068
|
-
const { animation, roundness } =
|
|
1101
|
+
const { animation, roundness } = useTheme10(themeOverrides);
|
|
1069
1102
|
const [opacity] = useState7(() => new Animated2.Value(0));
|
|
1070
1103
|
const [closing, setClosing] = useState7(false);
|
|
1071
1104
|
if (!visible && !closing) setClosing(true);
|
|
@@ -1123,12 +1156,95 @@ import { useTheme as usePaperTheme } from "react-native-paper";
|
|
|
1123
1156
|
var useAutoPaperTheme = () => usePaperTheme();
|
|
1124
1157
|
|
|
1125
1158
|
// src/useThemeBridgeProps.ts
|
|
1126
|
-
import { useMemo as
|
|
1159
|
+
import { useMemo as useMemo3 } from "react";
|
|
1127
1160
|
function useThemeBridgeProps(props) {
|
|
1128
1161
|
const { initialValue, onChange, onReady } = props;
|
|
1129
|
-
return
|
|
1162
|
+
return useMemo3(() => ({ initialValue, onChange, onReady }), [initialValue, onChange, onReady]);
|
|
1130
1163
|
}
|
|
1131
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
|
+
|
|
1132
1248
|
// src/utils/getThirdColor.ts
|
|
1133
1249
|
function rgbToHsl(r, g, b) {
|
|
1134
1250
|
r /= 255;
|
|
@@ -1232,13 +1348,19 @@ export {
|
|
|
1232
1348
|
SEMANTIC_BASE_COLORS,
|
|
1233
1349
|
TextInput,
|
|
1234
1350
|
ThemeSettingsContext,
|
|
1351
|
+
createChalkboardTone,
|
|
1235
1352
|
createThemeReducer,
|
|
1236
1353
|
defaultColors,
|
|
1237
1354
|
defaultThemeSettings,
|
|
1238
1355
|
getBlendedColor,
|
|
1356
|
+
getChalkboardColor,
|
|
1239
1357
|
getColorRoles,
|
|
1240
1358
|
getContrastColor,
|
|
1359
|
+
getContrastRatio,
|
|
1241
1360
|
getHex,
|
|
1361
|
+
getLuminance,
|
|
1362
|
+
getOklch,
|
|
1363
|
+
getOklchHex,
|
|
1242
1364
|
getRgb,
|
|
1243
1365
|
getThirdColor,
|
|
1244
1366
|
getTintTextColor,
|
|
@@ -1258,6 +1380,7 @@ export {
|
|
|
1258
1380
|
useComputedTheme,
|
|
1259
1381
|
useMaskedViewModule,
|
|
1260
1382
|
useNavBarContext,
|
|
1383
|
+
usePaletteTone,
|
|
1261
1384
|
usePaperDefaults,
|
|
1262
1385
|
useReanimatedModule,
|
|
1263
1386
|
useThemeBridgeProps,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rific/auto-paper",
|
|
3
|
-
"version": "0.
|
|
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",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@infinitetoken/eslint-config": "0.2.0",
|
|
72
|
-
"@infinitetoken/jest-config": "^0.5.
|
|
72
|
+
"@infinitetoken/jest-config": "^0.5.1",
|
|
73
73
|
"@infinitetoken/tsconfig": "^0.6.0",
|
|
74
74
|
"@react-native-masked-view/masked-view": "^0.3.2",
|
|
75
75
|
"@reduxjs/toolkit": "^2.0.0",
|
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,9 +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'
|
|
48
|
+
export { getContrastRatio, getLuminance } from './utils/getContrastRatio'
|
|
46
49
|
export { getHex } from './utils/getHex'
|
|
50
|
+
export type { Oklch } from './utils/getOklch'
|
|
51
|
+
export { getOklch, getOklchHex } from './utils/getOklch'
|
|
47
52
|
export type { RGB } from './utils/getRgb'
|
|
48
53
|
export { getRgb } from './utils/getRgb'
|
|
49
54
|
export { getContrastColor } from './utils/getSwatchContrast'
|