@rific/auto-paper 0.6.4 → 0.7.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 +24 -2
- package/dist/index.d.mts +25 -9
- package/dist/index.d.ts +25 -9
- package/dist/index.js +143 -32
- package/dist/index.mjs +128 -28
- package/package.json +4 -2
- package/src/components/BlurView.tsx +1 -1
- package/src/components/ColorPicker.tsx +11 -5
- package/src/components/Dialog.tsx +57 -22
- package/src/components/PalettePicker.tsx +117 -0
- package/src/index.ts +2 -0
- package/src/useSwatchGrid.ts +18 -0
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Adaptive theming for [`react-native-paper`](https://callstack.github.io/react-na
|
|
|
8
8
|
- System/light/dark appearance with live updates via `Appearance` API
|
|
9
9
|
- Tinted surface, surfaceVariant, outline, and elevation levels derived from the seed
|
|
10
10
|
- Optional Redux slice for wiring appearance and color into your store
|
|
11
|
-
- Wrapper components (`Appbar`, `AppearancePicker`, `BlurView`, `BottomNavigation`, `Button`, `Chip`, `ColorPicker`, `Dialog`, `FAB`, `HarmonyPicker`, `IconButton`, `Menu`, `TextInput`) with prop defaults via context
|
|
11
|
+
- Wrapper components (`Appbar`, `AppearancePicker`, `BlurView`, `BottomNavigation`, `Button`, `Chip`, `ColorPicker`, `Dialog`, `FAB`, `HarmonyPicker`, `IconButton`, `Menu`, `PalettePicker`, `TextInput`) with prop defaults via context
|
|
12
12
|
- All color utilities exported for standalone use
|
|
13
13
|
|
|
14
14
|
## Installation
|
|
@@ -111,7 +111,7 @@ import { Button, Chip, Provider as AutoPaperProvider } from '@rific/auto-paper'
|
|
|
111
111
|
</AutoPaperProvider>
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
-
Available wrapper components: `Appbar`, `AppearancePicker`, `BlurView`, `BottomNavigation`, `Button`, `Chip`, `ColorPicker`, `Dialog`, `FAB`, `HarmonyPicker`, `IconButton`, `Menu`, `TextInput`. Each is a thin wrapper around the matching `react-native-paper` component and accepts the same props.
|
|
114
|
+
Available wrapper components: `Appbar`, `AppearancePicker`, `BlurView`, `BottomNavigation`, `Button`, `Chip`, `ColorPicker`, `Dialog`, `FAB`, `HarmonyPicker`, `IconButton`, `Menu`, `PalettePicker`, `TextInput`. Each is a thin wrapper around the matching `react-native-paper` component and accepts the same props.
|
|
115
115
|
|
|
116
116
|
### With Redux
|
|
117
117
|
|
|
@@ -309,6 +309,28 @@ import { IconButton } from '@rific/auto-paper'
|
|
|
309
309
|
|
|
310
310
|
`IconButtonProps` is exported from `@rific/auto-paper` and extends `react-native-paper`'s `IconButtonProps` with the `variant` field. Use `PaperIconButtonProps` if you need the base paper type.
|
|
311
311
|
|
|
312
|
+
### `PalettePicker`
|
|
313
|
+
|
|
314
|
+
Like `ColorPicker`, but each swatch — and the trigger itself — renders the seed's full triadic palette as a 3-wedge pie instead of a flat color, so you see the whole result before picking.
|
|
315
|
+
|
|
316
|
+
```tsx
|
|
317
|
+
import { PalettePicker, useThemeSettings } from '@rific/auto-paper'
|
|
318
|
+
|
|
319
|
+
function SettingsScreen() {
|
|
320
|
+
const { settings: { color, harmony }, set } = useThemeSettings()
|
|
321
|
+
return <PalettePicker value={color} harmony={harmony} onChange={(c) => set({ color: c })} />
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
| Prop | Type | Description |
|
|
326
|
+
|---|---|---|
|
|
327
|
+
| `value` | `string` | Currently selected seed color |
|
|
328
|
+
| `onChange` | `(color: string) => void` | Called with the seed color when a swatch is tapped |
|
|
329
|
+
| `harmony` | `ColorHarmony` | Harmony used to compute each swatch's preview palette. Defaults to `'split-complementary'` |
|
|
330
|
+
| `weights` | `PaletteWeights` | Relative size of each wedge — `{ primary, secondary, tertiary }`. Defaults to `{ primary: 2, secondary: 1, tertiary: 1 }` (primary half the pie, secondary/tertiary a quarter each). Values are normalized, so any positive ratio works; keep each share at or under half the total so its wedge stays a single slice |
|
|
331
|
+
| `colors` | `SeedColor[]` | Seed color swatches to offer. Defaults to the same set as `ColorPicker` |
|
|
332
|
+
| `blur` | `boolean` | Overrides the ambient blur setting for this component's dialog |
|
|
333
|
+
|
|
312
334
|
### `PaperDefaults` / `usePaperDefaults`
|
|
313
335
|
|
|
314
336
|
`PaperDefaults` is the type for the `defaults` prop on `Provider`:
|
package/dist/index.d.mts
CHANGED
|
@@ -31,11 +31,11 @@ declare function getTriadicPalette(primaryColor: string, harmony?: ColorHarmony)
|
|
|
31
31
|
type ThemeAppearance = 'system' | 'light' | 'dark';
|
|
32
32
|
declare function useComputedTheme(appearance: ThemeAppearance, color: string, harmony?: ColorHarmony): MD3Theme | null;
|
|
33
33
|
|
|
34
|
-
type Props$
|
|
34
|
+
type Props$3 = {
|
|
35
35
|
value: ThemeAppearance;
|
|
36
36
|
onChange: (appearance: ThemeAppearance) => void;
|
|
37
37
|
};
|
|
38
|
-
declare const AppearancePicker: ({ value, onChange }: Props$
|
|
38
|
+
declare const AppearancePicker: ({ value, onChange }: Props$3) => react.JSX.Element;
|
|
39
39
|
|
|
40
40
|
type ExpoBlurTint = 'light' | 'dark' | 'default' | 'extraLight' | 'regular' | 'prominent' | 'systemUltraThinMaterial' | 'systemThinMaterial' | 'systemMaterial' | 'systemThickMaterial' | 'systemChromeMaterial' | 'systemUltraThinMaterialLight' | 'systemThinMaterialLight' | 'systemMaterialLight' | 'systemThickMaterialLight' | 'systemChromeMaterialLight' | 'systemUltraThinMaterialDark' | 'systemThinMaterialDark' | 'systemMaterialDark' | 'systemThickMaterialDark' | 'systemChromeMaterialDark';
|
|
41
41
|
type ExpoBlurMethod = 'none' | 'dimezisBlurView' | 'dimezisBlurViewSdk31Plus';
|
|
@@ -86,18 +86,19 @@ type SeedColor = {
|
|
|
86
86
|
value: string;
|
|
87
87
|
};
|
|
88
88
|
declare const defaultColors: SeedColor[];
|
|
89
|
-
type Props$
|
|
89
|
+
type Props$2 = {
|
|
90
90
|
value: string;
|
|
91
91
|
onChange: (color: string) => void;
|
|
92
92
|
colors?: SeedColor[];
|
|
93
93
|
blur?: boolean;
|
|
94
94
|
};
|
|
95
|
-
declare const ColorPicker: ({ value, onChange, colors, blur: blurProp }: Props$
|
|
95
|
+
declare const ColorPicker: ({ value, onChange, colors, blur: blurProp }: Props$2) => react.JSX.Element;
|
|
96
96
|
|
|
97
|
-
type DialogProps = DialogProps$1 & {
|
|
97
|
+
type DialogProps = Omit<DialogProps$1, 'style'> & {
|
|
98
98
|
blur?: boolean;
|
|
99
|
+
style?: StyleProp<ViewStyle>;
|
|
99
100
|
};
|
|
100
|
-
declare function DialogComponent({ blur: blurProp, visible, onDismiss, children, style,
|
|
101
|
+
declare function DialogComponent({ blur: blurProp, dismissable, dismissableBackButton, visible, onDismiss, children, style, testID }: DialogProps): react__default.JSX.Element | null;
|
|
101
102
|
declare namespace DialogComponent {
|
|
102
103
|
var Title: react__default.FC<DialogTitleProps>;
|
|
103
104
|
var Content: react__default.FC<DialogContentProps>;
|
|
@@ -108,12 +109,12 @@ declare const Dialog: typeof DialogComponent;
|
|
|
108
109
|
|
|
109
110
|
declare function FAB(props: FABProps): react.JSX.Element;
|
|
110
111
|
|
|
111
|
-
type Props = {
|
|
112
|
+
type Props$1 = {
|
|
112
113
|
value: ColorHarmony;
|
|
113
114
|
onChange: (harmony: ColorHarmony) => void;
|
|
114
115
|
blur?: boolean;
|
|
115
116
|
};
|
|
116
|
-
declare const HarmonyPicker: ({ value, onChange, blur: blurProp }: Props) => react.JSX.Element;
|
|
117
|
+
declare const HarmonyPicker: ({ value, onChange, blur: blurProp }: Props$1) => react.JSX.Element;
|
|
117
118
|
|
|
118
119
|
type IconButtonProps = IconButtonProps$1 & {
|
|
119
120
|
variant?: 'primary' | 'secondary' | 'tertiary' | 'surface';
|
|
@@ -127,6 +128,21 @@ declare const Menu: (({ blur: blurProp, children, ...props }: MenuProps) => reac
|
|
|
127
128
|
Item: react__default.FC<MenuItemProps>;
|
|
128
129
|
};
|
|
129
130
|
|
|
131
|
+
type PaletteWeights = {
|
|
132
|
+
primary: number;
|
|
133
|
+
secondary: number;
|
|
134
|
+
tertiary: number;
|
|
135
|
+
};
|
|
136
|
+
type Props = {
|
|
137
|
+
value: string;
|
|
138
|
+
onChange: (color: string) => void;
|
|
139
|
+
harmony?: ColorHarmony;
|
|
140
|
+
weights?: PaletteWeights;
|
|
141
|
+
colors?: SeedColor[];
|
|
142
|
+
blur?: boolean;
|
|
143
|
+
};
|
|
144
|
+
declare const PalettePicker: ({ value, onChange, harmony, weights, colors, blur: blurProp }: Props) => react.JSX.Element;
|
|
145
|
+
|
|
130
146
|
declare const TextInput: react.ForwardRefExoticComponent<Omit<TextInputProps, "ref"> & react.RefAttributes<TextInput$1>>;
|
|
131
147
|
|
|
132
148
|
type PaperDefaults = {
|
|
@@ -244,4 +260,4 @@ declare const getTonalColor: (color: string, lightness: number) => string;
|
|
|
244
260
|
|
|
245
261
|
declare const isDarkColor: (value: string) => boolean;
|
|
246
262
|
|
|
247
|
-
export { Appbar, AppearancePicker, BlurView, type BlurViewProps, BottomNavigation, Button, Chip, type ChipProps, type ColorHarmony, ColorPicker, Dialog, type DialogProps, FAB, HarmonyPicker, IconButton, type IconButtonProps, Menu, type MenuProps, type PaperDefaults, Provider, type ProviderProps, type RGB, type SeedColor, TextInput, type ThemeAppearance, type ThemeSettings, ThemeSettingsContext, type ThemeSettingsContextType, type ThemeState, type TriadicPalette, createThemeReducer, defaultColors, defaultThemeSettings, getBlendedColor, getHex, getRgb, getTintTextColor, getTonalColor, getTriadicPalette, isDarkColor, selectThemeAppearance, selectThemeBlur, selectThemeColor, selectThemeHarmony, themeActions, themeReducer, useBlur, useComputedTheme, usePaperDefaults, useThemeSettings };
|
|
263
|
+
export { Appbar, AppearancePicker, BlurView, type BlurViewProps, BottomNavigation, Button, Chip, type ChipProps, type ColorHarmony, ColorPicker, Dialog, type DialogProps, FAB, HarmonyPicker, IconButton, type IconButtonProps, Menu, type MenuProps, PalettePicker, type PaletteWeights, type PaperDefaults, Provider, type ProviderProps, type RGB, type SeedColor, TextInput, type ThemeAppearance, type ThemeSettings, ThemeSettingsContext, type ThemeSettingsContextType, type ThemeState, type TriadicPalette, createThemeReducer, defaultColors, defaultThemeSettings, getBlendedColor, getHex, getRgb, getTintTextColor, getTonalColor, getTriadicPalette, isDarkColor, selectThemeAppearance, selectThemeBlur, selectThemeColor, selectThemeHarmony, themeActions, themeReducer, useBlur, useComputedTheme, usePaperDefaults, useThemeSettings };
|
package/dist/index.d.ts
CHANGED
|
@@ -31,11 +31,11 @@ declare function getTriadicPalette(primaryColor: string, harmony?: ColorHarmony)
|
|
|
31
31
|
type ThemeAppearance = 'system' | 'light' | 'dark';
|
|
32
32
|
declare function useComputedTheme(appearance: ThemeAppearance, color: string, harmony?: ColorHarmony): MD3Theme | null;
|
|
33
33
|
|
|
34
|
-
type Props$
|
|
34
|
+
type Props$3 = {
|
|
35
35
|
value: ThemeAppearance;
|
|
36
36
|
onChange: (appearance: ThemeAppearance) => void;
|
|
37
37
|
};
|
|
38
|
-
declare const AppearancePicker: ({ value, onChange }: Props$
|
|
38
|
+
declare const AppearancePicker: ({ value, onChange }: Props$3) => react.JSX.Element;
|
|
39
39
|
|
|
40
40
|
type ExpoBlurTint = 'light' | 'dark' | 'default' | 'extraLight' | 'regular' | 'prominent' | 'systemUltraThinMaterial' | 'systemThinMaterial' | 'systemMaterial' | 'systemThickMaterial' | 'systemChromeMaterial' | 'systemUltraThinMaterialLight' | 'systemThinMaterialLight' | 'systemMaterialLight' | 'systemThickMaterialLight' | 'systemChromeMaterialLight' | 'systemUltraThinMaterialDark' | 'systemThinMaterialDark' | 'systemMaterialDark' | 'systemThickMaterialDark' | 'systemChromeMaterialDark';
|
|
41
41
|
type ExpoBlurMethod = 'none' | 'dimezisBlurView' | 'dimezisBlurViewSdk31Plus';
|
|
@@ -86,18 +86,19 @@ type SeedColor = {
|
|
|
86
86
|
value: string;
|
|
87
87
|
};
|
|
88
88
|
declare const defaultColors: SeedColor[];
|
|
89
|
-
type Props$
|
|
89
|
+
type Props$2 = {
|
|
90
90
|
value: string;
|
|
91
91
|
onChange: (color: string) => void;
|
|
92
92
|
colors?: SeedColor[];
|
|
93
93
|
blur?: boolean;
|
|
94
94
|
};
|
|
95
|
-
declare const ColorPicker: ({ value, onChange, colors, blur: blurProp }: Props$
|
|
95
|
+
declare const ColorPicker: ({ value, onChange, colors, blur: blurProp }: Props$2) => react.JSX.Element;
|
|
96
96
|
|
|
97
|
-
type DialogProps = DialogProps$1 & {
|
|
97
|
+
type DialogProps = Omit<DialogProps$1, 'style'> & {
|
|
98
98
|
blur?: boolean;
|
|
99
|
+
style?: StyleProp<ViewStyle>;
|
|
99
100
|
};
|
|
100
|
-
declare function DialogComponent({ blur: blurProp, visible, onDismiss, children, style,
|
|
101
|
+
declare function DialogComponent({ blur: blurProp, dismissable, dismissableBackButton, visible, onDismiss, children, style, testID }: DialogProps): react__default.JSX.Element | null;
|
|
101
102
|
declare namespace DialogComponent {
|
|
102
103
|
var Title: react__default.FC<DialogTitleProps>;
|
|
103
104
|
var Content: react__default.FC<DialogContentProps>;
|
|
@@ -108,12 +109,12 @@ declare const Dialog: typeof DialogComponent;
|
|
|
108
109
|
|
|
109
110
|
declare function FAB(props: FABProps): react.JSX.Element;
|
|
110
111
|
|
|
111
|
-
type Props = {
|
|
112
|
+
type Props$1 = {
|
|
112
113
|
value: ColorHarmony;
|
|
113
114
|
onChange: (harmony: ColorHarmony) => void;
|
|
114
115
|
blur?: boolean;
|
|
115
116
|
};
|
|
116
|
-
declare const HarmonyPicker: ({ value, onChange, blur: blurProp }: Props) => react.JSX.Element;
|
|
117
|
+
declare const HarmonyPicker: ({ value, onChange, blur: blurProp }: Props$1) => react.JSX.Element;
|
|
117
118
|
|
|
118
119
|
type IconButtonProps = IconButtonProps$1 & {
|
|
119
120
|
variant?: 'primary' | 'secondary' | 'tertiary' | 'surface';
|
|
@@ -127,6 +128,21 @@ declare const Menu: (({ blur: blurProp, children, ...props }: MenuProps) => reac
|
|
|
127
128
|
Item: react__default.FC<MenuItemProps>;
|
|
128
129
|
};
|
|
129
130
|
|
|
131
|
+
type PaletteWeights = {
|
|
132
|
+
primary: number;
|
|
133
|
+
secondary: number;
|
|
134
|
+
tertiary: number;
|
|
135
|
+
};
|
|
136
|
+
type Props = {
|
|
137
|
+
value: string;
|
|
138
|
+
onChange: (color: string) => void;
|
|
139
|
+
harmony?: ColorHarmony;
|
|
140
|
+
weights?: PaletteWeights;
|
|
141
|
+
colors?: SeedColor[];
|
|
142
|
+
blur?: boolean;
|
|
143
|
+
};
|
|
144
|
+
declare const PalettePicker: ({ value, onChange, harmony, weights, colors, blur: blurProp }: Props) => react.JSX.Element;
|
|
145
|
+
|
|
130
146
|
declare const TextInput: react.ForwardRefExoticComponent<Omit<TextInputProps, "ref"> & react.RefAttributes<TextInput$1>>;
|
|
131
147
|
|
|
132
148
|
type PaperDefaults = {
|
|
@@ -244,4 +260,4 @@ declare const getTonalColor: (color: string, lightness: number) => string;
|
|
|
244
260
|
|
|
245
261
|
declare const isDarkColor: (value: string) => boolean;
|
|
246
262
|
|
|
247
|
-
export { Appbar, AppearancePicker, BlurView, type BlurViewProps, BottomNavigation, Button, Chip, type ChipProps, type ColorHarmony, ColorPicker, Dialog, type DialogProps, FAB, HarmonyPicker, IconButton, type IconButtonProps, Menu, type MenuProps, type PaperDefaults, Provider, type ProviderProps, type RGB, type SeedColor, TextInput, type ThemeAppearance, type ThemeSettings, ThemeSettingsContext, type ThemeSettingsContextType, type ThemeState, type TriadicPalette, createThemeReducer, defaultColors, defaultThemeSettings, getBlendedColor, getHex, getRgb, getTintTextColor, getTonalColor, getTriadicPalette, isDarkColor, selectThemeAppearance, selectThemeBlur, selectThemeColor, selectThemeHarmony, themeActions, themeReducer, useBlur, useComputedTheme, usePaperDefaults, useThemeSettings };
|
|
263
|
+
export { Appbar, AppearancePicker, BlurView, type BlurViewProps, BottomNavigation, Button, Chip, type ChipProps, type ColorHarmony, ColorPicker, Dialog, type DialogProps, FAB, HarmonyPicker, IconButton, type IconButtonProps, Menu, type MenuProps, PalettePicker, type PaletteWeights, type PaperDefaults, Provider, type ProviderProps, type RGB, type SeedColor, TextInput, type ThemeAppearance, type ThemeSettings, ThemeSettingsContext, type ThemeSettingsContextType, type ThemeState, type TriadicPalette, createThemeReducer, defaultColors, defaultThemeSettings, getBlendedColor, getHex, getRgb, getTintTextColor, getTonalColor, getTriadicPalette, isDarkColor, selectThemeAppearance, selectThemeBlur, selectThemeColor, selectThemeHarmony, themeActions, themeReducer, useBlur, useComputedTheme, usePaperDefaults, useThemeSettings };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -32,6 +42,7 @@ __export(index_exports, {
|
|
|
32
42
|
HarmonyPicker: () => HarmonyPicker,
|
|
33
43
|
IconButton: () => IconButton,
|
|
34
44
|
Menu: () => Menu,
|
|
45
|
+
PalettePicker: () => PalettePicker,
|
|
35
46
|
Provider: () => Provider,
|
|
36
47
|
TextInput: () => TextInput,
|
|
37
48
|
ThemeSettingsContext: () => ThemeSettingsContext,
|
|
@@ -148,7 +159,7 @@ var BlurView = ({ blur = true, children, elevation, style, tintColor, tintOpacit
|
|
|
148
159
|
tint,
|
|
149
160
|
children
|
|
150
161
|
] });
|
|
151
|
-
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native2.View, { style: [{ backgroundColor: surfaceColor }, style], children: [
|
|
162
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native2.View, { ...props, style: [{ backgroundColor: surfaceColor }, style], children: [
|
|
152
163
|
tint,
|
|
153
164
|
children
|
|
154
165
|
] });
|
|
@@ -688,35 +699,72 @@ function Chip({ variant, style, selectedColor, ...props }) {
|
|
|
688
699
|
}
|
|
689
700
|
|
|
690
701
|
// src/components/ColorPicker.tsx
|
|
691
|
-
var
|
|
702
|
+
var import_react10 = require("react");
|
|
692
703
|
var import_react_native7 = require("react-native");
|
|
693
704
|
var import_react_native_paper10 = require("react-native-paper");
|
|
694
705
|
|
|
695
|
-
// src/
|
|
706
|
+
// src/useSwatchGrid.ts
|
|
696
707
|
var import_react8 = require("react");
|
|
708
|
+
var SWATCH_GRID_COLUMNS = 5;
|
|
709
|
+
var SWATCH_GRID_GAP = 12;
|
|
710
|
+
function useSwatchSize(fallback) {
|
|
711
|
+
const [width, setWidth] = (0, import_react8.useState)(0);
|
|
712
|
+
const onLayout = ({ nativeEvent }) => setWidth(nativeEvent.layout.width);
|
|
713
|
+
const size = width > 0 ? (width - SWATCH_GRID_GAP * (SWATCH_GRID_COLUMNS - 1)) / SWATCH_GRID_COLUMNS : fallback;
|
|
714
|
+
return { onLayout, size };
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// src/components/Dialog.tsx
|
|
718
|
+
var import_react9 = __toESM(require("react"));
|
|
697
719
|
var import_react_native6 = require("react-native");
|
|
698
720
|
var import_react_native_paper9 = require("react-native-paper");
|
|
721
|
+
var import_react_native_safe_area_context = require("react-native-safe-area-context");
|
|
699
722
|
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
700
|
-
|
|
723
|
+
var ANIMATION_DURATION = 220;
|
|
724
|
+
var DIALOG_MARGIN = 32;
|
|
725
|
+
function DialogComponent({ blur: blurProp, dismissable = true, dismissableBackButton = dismissable, visible, onDismiss, children, style, testID = "dialog" }) {
|
|
701
726
|
const blur = useBlur(blurProp);
|
|
702
|
-
const
|
|
703
|
-
const
|
|
704
|
-
const
|
|
705
|
-
(0,
|
|
727
|
+
const theme = (0, import_react_native_paper9.useTheme)();
|
|
728
|
+
const { colors, isV3, roundness } = theme;
|
|
729
|
+
const { scale } = theme.animation;
|
|
730
|
+
const { bottom, left, right, top } = (0, import_react_native_safe_area_context.useSafeAreaInsets)();
|
|
731
|
+
const opacity = (0, import_react9.useRef)(new import_react_native6.Animated.Value(visible ? 1 : 0)).current;
|
|
732
|
+
const [mounted, setMounted] = (0, import_react9.useState)(visible);
|
|
733
|
+
(0, import_react9.useEffect)(() => {
|
|
706
734
|
if (visible) {
|
|
707
735
|
setMounted(true);
|
|
708
|
-
import_react_native6.Animated.timing(opacity, {
|
|
736
|
+
import_react_native6.Animated.timing(opacity, { duration: scale * ANIMATION_DURATION, easing: import_react_native6.Easing.out(import_react_native6.Easing.cubic), toValue: 1, useNativeDriver: true }).start();
|
|
709
737
|
} else {
|
|
710
|
-
import_react_native6.Animated.timing(opacity, {
|
|
738
|
+
import_react_native6.Animated.timing(opacity, { duration: scale * ANIMATION_DURATION, easing: import_react_native6.Easing.out(import_react_native6.Easing.cubic), toValue: 0, useNativeDriver: true }).start(({ finished }) => {
|
|
739
|
+
if (finished) setMounted(false);
|
|
740
|
+
});
|
|
711
741
|
}
|
|
712
742
|
}, [visible]);
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
743
|
+
(0, import_react9.useEffect)(() => {
|
|
744
|
+
if (!visible) return void 0;
|
|
745
|
+
const onHardwareBackPress = () => {
|
|
746
|
+
if (dismissable || dismissableBackButton) onDismiss?.();
|
|
747
|
+
return true;
|
|
748
|
+
};
|
|
749
|
+
const subscription = import_react_native6.BackHandler.addEventListener("hardwareBackPress", onHardwareBackPress);
|
|
750
|
+
return () => subscription.remove();
|
|
751
|
+
}, [dismissable, dismissableBackButton, onDismiss, visible]);
|
|
716
752
|
if (!mounted) return null;
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
753
|
+
const content = import_react9.default.Children.toArray(children).filter((child) => child != null && typeof child !== "boolean").map((child, i) => {
|
|
754
|
+
if (isV3) {
|
|
755
|
+
if (i === 0 && import_react9.default.isValidElement(child)) {
|
|
756
|
+
return import_react9.default.cloneElement(child, { style: [{ marginTop: 24 }, child.props.style] });
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
if (i === 0 && import_react9.default.isValidElement(child) && child.type === import_react_native_paper9.Dialog.Content) {
|
|
760
|
+
return import_react9.default.cloneElement(child, { style: [{ paddingTop: 24 }, child.props.style] });
|
|
761
|
+
}
|
|
762
|
+
return child;
|
|
763
|
+
});
|
|
764
|
+
const borderRadius = (isV3 ? 7 : 1) * roundness;
|
|
765
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native_paper9.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native6.Animated.View, { accessibilityLiveRegion: "polite", accessibilityViewIsModal: true, pointerEvents: visible ? "box-none" : "none", style: [import_react_native6.StyleSheet.absoluteFill, { opacity }], testID, children: [
|
|
766
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native6.Pressable, { accessibilityLabel: "Close modal", accessibilityRole: "button", disabled: !dismissable, onPress: dismissable ? onDismiss : void 0, style: [import_react_native6.StyleSheet.absoluteFill, { backgroundColor: colors.backdrop }], testID: `${testID}-backdrop` }),
|
|
767
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native6.View, { pointerEvents: "box-none", style: [styles2.wrapper, { marginBottom: bottom, marginTop: top, paddingHorizontal: Math.max(left, right, DIALOG_MARGIN) }], testID: `${testID}-wrapper`, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(BlurView, { blur, style: [styles2.card, { borderRadius }, style], testID: `${testID}-surface`, children: content }) })
|
|
720
768
|
] }) });
|
|
721
769
|
}
|
|
722
770
|
DialogComponent.Title = import_react_native_paper9.Dialog.Title;
|
|
@@ -727,12 +775,12 @@ var Dialog = DialogComponent;
|
|
|
727
775
|
var styles2 = import_react_native6.StyleSheet.create({
|
|
728
776
|
// Width cap keeps dialogs card-sized on wide viewports (web/tablet); no effect on phones.
|
|
729
777
|
card: { alignSelf: "center", maxWidth: 400, overflow: "hidden", width: "100%" },
|
|
730
|
-
|
|
731
|
-
wrapper: { flex: 1, justifyContent: "center", paddingHorizontal: 26 }
|
|
778
|
+
wrapper: { flex: 1, justifyContent: "center", paddingVertical: DIALOG_MARGIN }
|
|
732
779
|
});
|
|
733
780
|
|
|
734
781
|
// src/components/ColorPicker.tsx
|
|
735
782
|
var import_jsx_runtime9 = require("react/jsx-runtime");
|
|
783
|
+
var SIZE = 48;
|
|
736
784
|
var defaultColors = [
|
|
737
785
|
{ label: "Red", value: "#f44336" },
|
|
738
786
|
{ label: "Coral", value: "#ff5722" },
|
|
@@ -743,6 +791,7 @@ var defaultColors = [
|
|
|
743
791
|
{ label: "Emerald", value: "#4caf50" },
|
|
744
792
|
{ label: "Teal", value: "#009688" },
|
|
745
793
|
{ label: "Cyan", value: "#00bcd4" },
|
|
794
|
+
{ label: "Sky", value: "#03a9f4" },
|
|
746
795
|
{ label: "Blue", value: "#2196f3" },
|
|
747
796
|
{ label: "Indigo", value: "#3f51b5" },
|
|
748
797
|
{ label: "Deep Purple", value: "#673ab7" },
|
|
@@ -751,21 +800,23 @@ var defaultColors = [
|
|
|
751
800
|
{ label: "Pink", value: "#ec407a" },
|
|
752
801
|
{ label: "Rose", value: "#e91e63" },
|
|
753
802
|
{ label: "Brown", value: "#795548" },
|
|
803
|
+
{ label: "Taupe", value: "#8d7b68" },
|
|
754
804
|
{ label: "Blue Grey", value: "#607d8b" }
|
|
755
805
|
];
|
|
756
806
|
var ColorPicker = ({ value, onChange, colors = defaultColors, blur: blurProp }) => {
|
|
757
807
|
const blur = useBlur(blurProp);
|
|
758
|
-
const [open, setOpen] = (0,
|
|
808
|
+
const [open, setOpen] = (0, import_react10.useState)(false);
|
|
809
|
+
const { onLayout, size } = useSwatchSize(SIZE);
|
|
759
810
|
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
|
|
760
811
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_paper10.TouchableRipple, { onPress: () => setOpen(true), style: [styles3.trigger, { backgroundColor: value }], borderless: true, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_paper10.Icon, { source: "palette", size: 20, color: "white" }) }),
|
|
761
|
-
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Dialog, { blur, visible: open, onDismiss: () => setOpen(false), children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Dialog.Content, { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native7.View, { style: styles3.swatches, children: colors.map(({ value: hex }) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
812
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Dialog, { blur, visible: open, onDismiss: () => setOpen(false), children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Dialog.Content, { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native7.View, { style: styles3.swatches, onLayout, children: colors.map(({ value: hex }) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
762
813
|
import_react_native7.TouchableOpacity,
|
|
763
814
|
{
|
|
764
815
|
onPress: () => {
|
|
765
816
|
onChange(hex);
|
|
766
817
|
setOpen(false);
|
|
767
818
|
},
|
|
768
|
-
style: [styles3.swatch, { backgroundColor: hex }, value === hex && styles3.selected],
|
|
819
|
+
style: [styles3.swatch, { backgroundColor: hex, borderRadius: size / 2, height: size, width: size }, value === hex && styles3.selected],
|
|
769
820
|
children: value === hex && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_paper10.Icon, { source: "check", size: 20, color: "white" })
|
|
770
821
|
},
|
|
771
822
|
hex
|
|
@@ -774,9 +825,9 @@ var ColorPicker = ({ value, onChange, colors = defaultColors, blur: blurProp })
|
|
|
774
825
|
};
|
|
775
826
|
var styles3 = import_react_native7.StyleSheet.create({
|
|
776
827
|
selected: { borderColor: "white", borderWidth: 3 },
|
|
777
|
-
swatch: { alignItems: "center",
|
|
778
|
-
swatches: { flexDirection: "row", flexWrap: "wrap", gap:
|
|
779
|
-
trigger: { alignItems: "center", alignSelf: "flex-start", borderRadius:
|
|
828
|
+
swatch: { alignItems: "center", justifyContent: "center" },
|
|
829
|
+
swatches: { flexDirection: "row", flexWrap: "wrap", gap: SWATCH_GRID_GAP, paddingTop: 12 },
|
|
830
|
+
trigger: { alignItems: "center", alignSelf: "flex-start", borderRadius: SIZE / 2, height: SIZE, justifyContent: "center", width: SIZE }
|
|
780
831
|
});
|
|
781
832
|
|
|
782
833
|
// src/components/FAB.tsx
|
|
@@ -788,7 +839,7 @@ function FAB(props) {
|
|
|
788
839
|
}
|
|
789
840
|
|
|
790
841
|
// src/components/HarmonyPicker.tsx
|
|
791
|
-
var
|
|
842
|
+
var import_react11 = require("react");
|
|
792
843
|
var import_react_native_paper13 = require("react-native-paper");
|
|
793
844
|
|
|
794
845
|
// src/components/Menu.tsx
|
|
@@ -818,7 +869,7 @@ var HARMONIES = [
|
|
|
818
869
|
];
|
|
819
870
|
var HarmonyPicker = ({ value, onChange, blur: blurProp }) => {
|
|
820
871
|
const blur = useBlur(blurProp);
|
|
821
|
-
const [open, setOpen] = (0,
|
|
872
|
+
const [open, setOpen] = (0, import_react11.useState)(false);
|
|
822
873
|
const selected = HARMONIES.find((h) => h.value === value);
|
|
823
874
|
return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
824
875
|
Menu,
|
|
@@ -854,13 +905,72 @@ function IconButton({ variant, iconColor, containerColor, ...props }) {
|
|
|
854
905
|
return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react_native_paper14.IconButton, { ...defaults.IconButton, containerColor: containerColor ?? variantContainerColor, iconColor: iconColor ?? variantIconColor, ...props });
|
|
855
906
|
}
|
|
856
907
|
|
|
857
|
-
// src/components/
|
|
858
|
-
var
|
|
908
|
+
// src/components/PalettePicker.tsx
|
|
909
|
+
var import_react12 = require("react");
|
|
910
|
+
var import_react_native9 = require("react-native");
|
|
859
911
|
var import_react_native_paper15 = require("react-native-paper");
|
|
860
912
|
var import_jsx_runtime14 = require("react/jsx-runtime");
|
|
861
|
-
var
|
|
913
|
+
var SIZE2 = 48;
|
|
914
|
+
var defaultWeights = { primary: 2, secondary: 1, tertiary: 1 };
|
|
915
|
+
var buildWedges = (weights) => {
|
|
916
|
+
const total = weights.primary + weights.secondary + weights.tertiary;
|
|
917
|
+
const primary = weights.primary / total * 360;
|
|
918
|
+
const secondary = weights.secondary / total * 360;
|
|
919
|
+
const tertiary = weights.tertiary / total * 360;
|
|
920
|
+
return [
|
|
921
|
+
{ key: "primary", rotate: -primary / 2, share: primary },
|
|
922
|
+
{ key: "tertiary", rotate: primary / 2, share: tertiary },
|
|
923
|
+
{ key: "secondary", rotate: primary / 2 + tertiary, share: secondary }
|
|
924
|
+
];
|
|
925
|
+
};
|
|
926
|
+
var Pie = ({ palette, weights = defaultWeights, size = SIZE2 }) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { style: [styles5.pie, { borderRadius: size / 2, height: size, width: size }], children: buildWedges(weights).map(({ key, rotate, share }) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { style: [styles5.rotator, { transform: [{ rotate: `${rotate}deg` }] }], children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { style: styles5.halfMask, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { style: [styles5.innerRotator, { transform: [{ rotate: `${share - 180}deg` }] }], children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { style: [styles5.wedgeFill, { backgroundColor: palette[key] }] }) }) }) }, key)) });
|
|
927
|
+
var PalettePicker = ({ value, onChange, harmony = "split-complementary", weights, colors = defaultColors, blur: blurProp }) => {
|
|
928
|
+
const blur = useBlur(blurProp);
|
|
929
|
+
const [open, setOpen] = (0, import_react12.useState)(false);
|
|
930
|
+
const { onLayout, size } = useSwatchSize(SIZE2);
|
|
931
|
+
return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
|
|
932
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper15.TouchableRipple, { onPress: () => setOpen(true), style: styles5.trigger, borderless: true, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native9.View, { style: styles5.triggerContent, children: [
|
|
933
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Pie, { palette: getTriadicPalette(value, harmony), weights }),
|
|
934
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { pointerEvents: "none", style: styles5.triggerIcon, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper15.Icon, { source: "palette", size: 20, color: "white" }) })
|
|
935
|
+
] }) }),
|
|
936
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Dialog, { blur, visible: open, onDismiss: () => setOpen(false), children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Dialog.Content, { children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { style: styles5.swatches, onLayout, children: colors.map(({ value: hex }) => /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
|
|
937
|
+
import_react_native9.TouchableOpacity,
|
|
938
|
+
{
|
|
939
|
+
onPress: () => {
|
|
940
|
+
onChange(hex);
|
|
941
|
+
setOpen(false);
|
|
942
|
+
},
|
|
943
|
+
style: [styles5.swatch, { height: size, width: size }],
|
|
944
|
+
children: [
|
|
945
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Pie, { palette: getTriadicPalette(hex, harmony), weights, size }),
|
|
946
|
+
value === hex && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { pointerEvents: "none", style: [styles5.selectedRing, { borderRadius: size / 2 }], children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper15.Icon, { source: "check", size: 20, color: "white" }) })
|
|
947
|
+
]
|
|
948
|
+
},
|
|
949
|
+
hex
|
|
950
|
+
)) }) }) })
|
|
951
|
+
] });
|
|
952
|
+
};
|
|
953
|
+
var styles5 = import_react_native9.StyleSheet.create({
|
|
954
|
+
halfMask: { height: "100%", left: "50%", overflow: "hidden", position: "absolute", top: 0, width: "50%" },
|
|
955
|
+
innerRotator: { height: "100%", left: "-100%", overflow: "hidden", position: "absolute", top: 0, width: "200%" },
|
|
956
|
+
pie: { overflow: "hidden" },
|
|
957
|
+
rotator: { height: "100%", left: 0, overflow: "hidden", position: "absolute", top: 0, width: "100%" },
|
|
958
|
+
selectedRing: { alignItems: "center", borderColor: "white", borderWidth: 3, bottom: 0, justifyContent: "center", left: 0, position: "absolute", right: 0, top: 0 },
|
|
959
|
+
swatch: { alignItems: "center", justifyContent: "center" },
|
|
960
|
+
swatches: { flexDirection: "row", flexWrap: "wrap", gap: SWATCH_GRID_GAP, paddingTop: 12 },
|
|
961
|
+
trigger: { alignItems: "center", alignSelf: "flex-start", borderRadius: SIZE2 / 2, height: SIZE2, justifyContent: "center", width: SIZE2 },
|
|
962
|
+
triggerContent: { height: SIZE2, width: SIZE2 },
|
|
963
|
+
triggerIcon: { alignItems: "center", bottom: 0, justifyContent: "center", left: 0, position: "absolute", right: 0, top: 0 },
|
|
964
|
+
wedgeFill: { height: "100%", left: "50%", position: "absolute", top: 0, width: "50%" }
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
// src/components/TextInput.tsx
|
|
968
|
+
var import_react13 = require("react");
|
|
969
|
+
var import_react_native_paper16 = require("react-native-paper");
|
|
970
|
+
var import_jsx_runtime15 = require("react/jsx-runtime");
|
|
971
|
+
var TextInput = (0, import_react13.forwardRef)(function TextInput2(props, ref) {
|
|
862
972
|
const defaults = usePaperDefaults();
|
|
863
|
-
return /* @__PURE__ */ (0,
|
|
973
|
+
return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native_paper16.TextInput, { ref, ...defaults.TextInput, ...props });
|
|
864
974
|
});
|
|
865
975
|
|
|
866
976
|
// src/redux/themeSlice.ts
|
|
@@ -901,8 +1011,8 @@ var selectThemeColor = (state) => state.color;
|
|
|
901
1011
|
var selectThemeHarmony = (state) => state.harmony;
|
|
902
1012
|
|
|
903
1013
|
// src/useThemeSettings.ts
|
|
904
|
-
var
|
|
905
|
-
var useThemeSettings = () => (0,
|
|
1014
|
+
var import_react14 = require("react");
|
|
1015
|
+
var useThemeSettings = () => (0, import_react14.useContext)(ThemeSettingsContext);
|
|
906
1016
|
|
|
907
1017
|
// src/utils/getTintTextColor.ts
|
|
908
1018
|
var getTintTextColor = (tintColor, backdropColor, tintOpacity) => {
|
|
@@ -923,6 +1033,7 @@ var getTintTextColor = (tintColor, backdropColor, tintOpacity) => {
|
|
|
923
1033
|
HarmonyPicker,
|
|
924
1034
|
IconButton,
|
|
925
1035
|
Menu,
|
|
1036
|
+
PalettePicker,
|
|
926
1037
|
Provider,
|
|
927
1038
|
TextInput,
|
|
928
1039
|
ThemeSettingsContext,
|
package/dist/index.mjs
CHANGED
|
@@ -95,7 +95,7 @@ var BlurView = ({ blur = true, children, elevation, style, tintColor, tintOpacit
|
|
|
95
95
|
tint,
|
|
96
96
|
children
|
|
97
97
|
] });
|
|
98
|
-
return /* @__PURE__ */ jsxs2(View, { style: [{ backgroundColor: surfaceColor }, style], children: [
|
|
98
|
+
return /* @__PURE__ */ jsxs2(View, { ...props, style: [{ backgroundColor: surfaceColor }, style], children: [
|
|
99
99
|
tint,
|
|
100
100
|
children
|
|
101
101
|
] });
|
|
@@ -635,35 +635,72 @@ function Chip({ variant, style, selectedColor, ...props }) {
|
|
|
635
635
|
}
|
|
636
636
|
|
|
637
637
|
// src/components/ColorPicker.tsx
|
|
638
|
-
import { useState as
|
|
638
|
+
import { useState as useState5 } from "react";
|
|
639
639
|
import { StyleSheet as StyleSheet4, TouchableOpacity, View as View4 } from "react-native";
|
|
640
640
|
import { Icon, TouchableRipple } from "react-native-paper";
|
|
641
641
|
|
|
642
|
+
// src/useSwatchGrid.ts
|
|
643
|
+
import { useState as useState3 } from "react";
|
|
644
|
+
var SWATCH_GRID_COLUMNS = 5;
|
|
645
|
+
var SWATCH_GRID_GAP = 12;
|
|
646
|
+
function useSwatchSize(fallback) {
|
|
647
|
+
const [width, setWidth] = useState3(0);
|
|
648
|
+
const onLayout = ({ nativeEvent }) => setWidth(nativeEvent.layout.width);
|
|
649
|
+
const size = width > 0 ? (width - SWATCH_GRID_GAP * (SWATCH_GRID_COLUMNS - 1)) / SWATCH_GRID_COLUMNS : fallback;
|
|
650
|
+
return { onLayout, size };
|
|
651
|
+
}
|
|
652
|
+
|
|
642
653
|
// src/components/Dialog.tsx
|
|
643
|
-
import { useEffect as useEffect4, useRef as useRef2, useState as
|
|
644
|
-
import { Animated, StyleSheet as StyleSheet3,
|
|
654
|
+
import React2, { useEffect as useEffect4, useRef as useRef2, useState as useState4 } from "react";
|
|
655
|
+
import { Animated, BackHandler, Easing, Pressable, StyleSheet as StyleSheet3, View as View3 } from "react-native";
|
|
645
656
|
import { Dialog as PaperDialog, Portal, useTheme as useTheme5 } from "react-native-paper";
|
|
657
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
646
658
|
import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
647
|
-
|
|
659
|
+
var ANIMATION_DURATION = 220;
|
|
660
|
+
var DIALOG_MARGIN = 32;
|
|
661
|
+
function DialogComponent({ blur: blurProp, dismissable = true, dismissableBackButton = dismissable, visible, onDismiss, children, style, testID = "dialog" }) {
|
|
648
662
|
const blur = useBlur(blurProp);
|
|
649
|
-
const
|
|
650
|
-
const
|
|
651
|
-
const
|
|
663
|
+
const theme = useTheme5();
|
|
664
|
+
const { colors, isV3, roundness } = theme;
|
|
665
|
+
const { scale } = theme.animation;
|
|
666
|
+
const { bottom, left, right, top } = useSafeAreaInsets();
|
|
667
|
+
const opacity = useRef2(new Animated.Value(visible ? 1 : 0)).current;
|
|
668
|
+
const [mounted, setMounted] = useState4(visible);
|
|
652
669
|
useEffect4(() => {
|
|
653
670
|
if (visible) {
|
|
654
671
|
setMounted(true);
|
|
655
|
-
Animated.timing(opacity, {
|
|
672
|
+
Animated.timing(opacity, { duration: scale * ANIMATION_DURATION, easing: Easing.out(Easing.cubic), toValue: 1, useNativeDriver: true }).start();
|
|
656
673
|
} else {
|
|
657
|
-
Animated.timing(opacity, {
|
|
674
|
+
Animated.timing(opacity, { duration: scale * ANIMATION_DURATION, easing: Easing.out(Easing.cubic), toValue: 0, useNativeDriver: true }).start(({ finished }) => {
|
|
675
|
+
if (finished) setMounted(false);
|
|
676
|
+
});
|
|
658
677
|
}
|
|
659
678
|
}, [visible]);
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
679
|
+
useEffect4(() => {
|
|
680
|
+
if (!visible) return void 0;
|
|
681
|
+
const onHardwareBackPress = () => {
|
|
682
|
+
if (dismissable || dismissableBackButton) onDismiss?.();
|
|
683
|
+
return true;
|
|
684
|
+
};
|
|
685
|
+
const subscription = BackHandler.addEventListener("hardwareBackPress", onHardwareBackPress);
|
|
686
|
+
return () => subscription.remove();
|
|
687
|
+
}, [dismissable, dismissableBackButton, onDismiss, visible]);
|
|
663
688
|
if (!mounted) return null;
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
689
|
+
const content = React2.Children.toArray(children).filter((child) => child != null && typeof child !== "boolean").map((child, i) => {
|
|
690
|
+
if (isV3) {
|
|
691
|
+
if (i === 0 && React2.isValidElement(child)) {
|
|
692
|
+
return React2.cloneElement(child, { style: [{ marginTop: 24 }, child.props.style] });
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
if (i === 0 && React2.isValidElement(child) && child.type === PaperDialog.Content) {
|
|
696
|
+
return React2.cloneElement(child, { style: [{ paddingTop: 24 }, child.props.style] });
|
|
697
|
+
}
|
|
698
|
+
return child;
|
|
699
|
+
});
|
|
700
|
+
const borderRadius = (isV3 ? 7 : 1) * roundness;
|
|
701
|
+
return /* @__PURE__ */ jsx8(Portal, { children: /* @__PURE__ */ jsxs4(Animated.View, { accessibilityLiveRegion: "polite", accessibilityViewIsModal: true, pointerEvents: visible ? "box-none" : "none", style: [StyleSheet3.absoluteFill, { opacity }], testID, children: [
|
|
702
|
+
/* @__PURE__ */ jsx8(Pressable, { accessibilityLabel: "Close modal", accessibilityRole: "button", disabled: !dismissable, onPress: dismissable ? onDismiss : void 0, style: [StyleSheet3.absoluteFill, { backgroundColor: colors.backdrop }], testID: `${testID}-backdrop` }),
|
|
703
|
+
/* @__PURE__ */ jsx8(View3, { pointerEvents: "box-none", style: [styles2.wrapper, { marginBottom: bottom, marginTop: top, paddingHorizontal: Math.max(left, right, DIALOG_MARGIN) }], testID: `${testID}-wrapper`, children: /* @__PURE__ */ jsx8(BlurView, { blur, style: [styles2.card, { borderRadius }, style], testID: `${testID}-surface`, children: content }) })
|
|
667
704
|
] }) });
|
|
668
705
|
}
|
|
669
706
|
DialogComponent.Title = PaperDialog.Title;
|
|
@@ -674,12 +711,12 @@ var Dialog = DialogComponent;
|
|
|
674
711
|
var styles2 = StyleSheet3.create({
|
|
675
712
|
// Width cap keeps dialogs card-sized on wide viewports (web/tablet); no effect on phones.
|
|
676
713
|
card: { alignSelf: "center", maxWidth: 400, overflow: "hidden", width: "100%" },
|
|
677
|
-
|
|
678
|
-
wrapper: { flex: 1, justifyContent: "center", paddingHorizontal: 26 }
|
|
714
|
+
wrapper: { flex: 1, justifyContent: "center", paddingVertical: DIALOG_MARGIN }
|
|
679
715
|
});
|
|
680
716
|
|
|
681
717
|
// src/components/ColorPicker.tsx
|
|
682
718
|
import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
719
|
+
var SIZE = 48;
|
|
683
720
|
var defaultColors = [
|
|
684
721
|
{ label: "Red", value: "#f44336" },
|
|
685
722
|
{ label: "Coral", value: "#ff5722" },
|
|
@@ -690,6 +727,7 @@ var defaultColors = [
|
|
|
690
727
|
{ label: "Emerald", value: "#4caf50" },
|
|
691
728
|
{ label: "Teal", value: "#009688" },
|
|
692
729
|
{ label: "Cyan", value: "#00bcd4" },
|
|
730
|
+
{ label: "Sky", value: "#03a9f4" },
|
|
693
731
|
{ label: "Blue", value: "#2196f3" },
|
|
694
732
|
{ label: "Indigo", value: "#3f51b5" },
|
|
695
733
|
{ label: "Deep Purple", value: "#673ab7" },
|
|
@@ -698,21 +736,23 @@ var defaultColors = [
|
|
|
698
736
|
{ label: "Pink", value: "#ec407a" },
|
|
699
737
|
{ label: "Rose", value: "#e91e63" },
|
|
700
738
|
{ label: "Brown", value: "#795548" },
|
|
739
|
+
{ label: "Taupe", value: "#8d7b68" },
|
|
701
740
|
{ label: "Blue Grey", value: "#607d8b" }
|
|
702
741
|
];
|
|
703
742
|
var ColorPicker = ({ value, onChange, colors = defaultColors, blur: blurProp }) => {
|
|
704
743
|
const blur = useBlur(blurProp);
|
|
705
|
-
const [open, setOpen] =
|
|
744
|
+
const [open, setOpen] = useState5(false);
|
|
745
|
+
const { onLayout, size } = useSwatchSize(SIZE);
|
|
706
746
|
return /* @__PURE__ */ jsxs5(Fragment2, { children: [
|
|
707
747
|
/* @__PURE__ */ jsx9(TouchableRipple, { onPress: () => setOpen(true), style: [styles3.trigger, { backgroundColor: value }], borderless: true, children: /* @__PURE__ */ jsx9(Icon, { source: "palette", size: 20, color: "white" }) }),
|
|
708
|
-
/* @__PURE__ */ jsx9(Dialog, { blur, visible: open, onDismiss: () => setOpen(false), children: /* @__PURE__ */ jsx9(Dialog.Content, { children: /* @__PURE__ */ jsx9(View4, { style: styles3.swatches, children: colors.map(({ value: hex }) => /* @__PURE__ */ jsx9(
|
|
748
|
+
/* @__PURE__ */ jsx9(Dialog, { blur, visible: open, onDismiss: () => setOpen(false), children: /* @__PURE__ */ jsx9(Dialog.Content, { children: /* @__PURE__ */ jsx9(View4, { style: styles3.swatches, onLayout, children: colors.map(({ value: hex }) => /* @__PURE__ */ jsx9(
|
|
709
749
|
TouchableOpacity,
|
|
710
750
|
{
|
|
711
751
|
onPress: () => {
|
|
712
752
|
onChange(hex);
|
|
713
753
|
setOpen(false);
|
|
714
754
|
},
|
|
715
|
-
style: [styles3.swatch, { backgroundColor: hex }, value === hex && styles3.selected],
|
|
755
|
+
style: [styles3.swatch, { backgroundColor: hex, borderRadius: size / 2, height: size, width: size }, value === hex && styles3.selected],
|
|
716
756
|
children: value === hex && /* @__PURE__ */ jsx9(Icon, { source: "check", size: 20, color: "white" })
|
|
717
757
|
},
|
|
718
758
|
hex
|
|
@@ -721,9 +761,9 @@ var ColorPicker = ({ value, onChange, colors = defaultColors, blur: blurProp })
|
|
|
721
761
|
};
|
|
722
762
|
var styles3 = StyleSheet4.create({
|
|
723
763
|
selected: { borderColor: "white", borderWidth: 3 },
|
|
724
|
-
swatch: { alignItems: "center",
|
|
725
|
-
swatches: { flexDirection: "row", flexWrap: "wrap", gap:
|
|
726
|
-
trigger: { alignItems: "center", alignSelf: "flex-start", borderRadius:
|
|
764
|
+
swatch: { alignItems: "center", justifyContent: "center" },
|
|
765
|
+
swatches: { flexDirection: "row", flexWrap: "wrap", gap: SWATCH_GRID_GAP, paddingTop: 12 },
|
|
766
|
+
trigger: { alignItems: "center", alignSelf: "flex-start", borderRadius: SIZE / 2, height: SIZE, justifyContent: "center", width: SIZE }
|
|
727
767
|
});
|
|
728
768
|
|
|
729
769
|
// src/components/FAB.tsx
|
|
@@ -735,7 +775,7 @@ function FAB(props) {
|
|
|
735
775
|
}
|
|
736
776
|
|
|
737
777
|
// src/components/HarmonyPicker.tsx
|
|
738
|
-
import { useState as
|
|
778
|
+
import { useState as useState6 } from "react";
|
|
739
779
|
import { Button as Button2 } from "react-native-paper";
|
|
740
780
|
|
|
741
781
|
// src/components/Menu.tsx
|
|
@@ -765,7 +805,7 @@ var HARMONIES = [
|
|
|
765
805
|
];
|
|
766
806
|
var HarmonyPicker = ({ value, onChange, blur: blurProp }) => {
|
|
767
807
|
const blur = useBlur(blurProp);
|
|
768
|
-
const [open, setOpen] =
|
|
808
|
+
const [open, setOpen] = useState6(false);
|
|
769
809
|
const selected = HARMONIES.find((h) => h.value === value);
|
|
770
810
|
return /* @__PURE__ */ jsx12(
|
|
771
811
|
Menu,
|
|
@@ -801,13 +841,72 @@ function IconButton({ variant, iconColor, containerColor, ...props }) {
|
|
|
801
841
|
return /* @__PURE__ */ jsx13(PaperIconButton, { ...defaults.IconButton, containerColor: containerColor ?? variantContainerColor, iconColor: iconColor ?? variantIconColor, ...props });
|
|
802
842
|
}
|
|
803
843
|
|
|
844
|
+
// src/components/PalettePicker.tsx
|
|
845
|
+
import { useState as useState7 } from "react";
|
|
846
|
+
import { StyleSheet as StyleSheet6, TouchableOpacity as TouchableOpacity2, View as View5 } from "react-native";
|
|
847
|
+
import { Icon as Icon2, TouchableRipple as TouchableRipple2 } from "react-native-paper";
|
|
848
|
+
import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
849
|
+
var SIZE2 = 48;
|
|
850
|
+
var defaultWeights = { primary: 2, secondary: 1, tertiary: 1 };
|
|
851
|
+
var buildWedges = (weights) => {
|
|
852
|
+
const total = weights.primary + weights.secondary + weights.tertiary;
|
|
853
|
+
const primary = weights.primary / total * 360;
|
|
854
|
+
const secondary = weights.secondary / total * 360;
|
|
855
|
+
const tertiary = weights.tertiary / total * 360;
|
|
856
|
+
return [
|
|
857
|
+
{ key: "primary", rotate: -primary / 2, share: primary },
|
|
858
|
+
{ key: "tertiary", rotate: primary / 2, share: tertiary },
|
|
859
|
+
{ key: "secondary", rotate: primary / 2 + tertiary, share: secondary }
|
|
860
|
+
];
|
|
861
|
+
};
|
|
862
|
+
var Pie = ({ palette, weights = defaultWeights, size = SIZE2 }) => /* @__PURE__ */ jsx14(View5, { style: [styles5.pie, { borderRadius: size / 2, height: size, width: size }], children: buildWedges(weights).map(({ key, rotate, share }) => /* @__PURE__ */ jsx14(View5, { style: [styles5.rotator, { transform: [{ rotate: `${rotate}deg` }] }], children: /* @__PURE__ */ jsx14(View5, { style: styles5.halfMask, children: /* @__PURE__ */ jsx14(View5, { style: [styles5.innerRotator, { transform: [{ rotate: `${share - 180}deg` }] }], children: /* @__PURE__ */ jsx14(View5, { style: [styles5.wedgeFill, { backgroundColor: palette[key] }] }) }) }) }, key)) });
|
|
863
|
+
var PalettePicker = ({ value, onChange, harmony = "split-complementary", weights, colors = defaultColors, blur: blurProp }) => {
|
|
864
|
+
const blur = useBlur(blurProp);
|
|
865
|
+
const [open, setOpen] = useState7(false);
|
|
866
|
+
const { onLayout, size } = useSwatchSize(SIZE2);
|
|
867
|
+
return /* @__PURE__ */ jsxs6(Fragment3, { children: [
|
|
868
|
+
/* @__PURE__ */ jsx14(TouchableRipple2, { onPress: () => setOpen(true), style: styles5.trigger, borderless: true, children: /* @__PURE__ */ jsxs6(View5, { style: styles5.triggerContent, children: [
|
|
869
|
+
/* @__PURE__ */ jsx14(Pie, { palette: getTriadicPalette(value, harmony), weights }),
|
|
870
|
+
/* @__PURE__ */ jsx14(View5, { pointerEvents: "none", style: styles5.triggerIcon, children: /* @__PURE__ */ jsx14(Icon2, { source: "palette", size: 20, color: "white" }) })
|
|
871
|
+
] }) }),
|
|
872
|
+
/* @__PURE__ */ jsx14(Dialog, { blur, visible: open, onDismiss: () => setOpen(false), children: /* @__PURE__ */ jsx14(Dialog.Content, { children: /* @__PURE__ */ jsx14(View5, { style: styles5.swatches, onLayout, children: colors.map(({ value: hex }) => /* @__PURE__ */ jsxs6(
|
|
873
|
+
TouchableOpacity2,
|
|
874
|
+
{
|
|
875
|
+
onPress: () => {
|
|
876
|
+
onChange(hex);
|
|
877
|
+
setOpen(false);
|
|
878
|
+
},
|
|
879
|
+
style: [styles5.swatch, { height: size, width: size }],
|
|
880
|
+
children: [
|
|
881
|
+
/* @__PURE__ */ jsx14(Pie, { palette: getTriadicPalette(hex, harmony), weights, size }),
|
|
882
|
+
value === hex && /* @__PURE__ */ jsx14(View5, { pointerEvents: "none", style: [styles5.selectedRing, { borderRadius: size / 2 }], children: /* @__PURE__ */ jsx14(Icon2, { source: "check", size: 20, color: "white" }) })
|
|
883
|
+
]
|
|
884
|
+
},
|
|
885
|
+
hex
|
|
886
|
+
)) }) }) })
|
|
887
|
+
] });
|
|
888
|
+
};
|
|
889
|
+
var styles5 = StyleSheet6.create({
|
|
890
|
+
halfMask: { height: "100%", left: "50%", overflow: "hidden", position: "absolute", top: 0, width: "50%" },
|
|
891
|
+
innerRotator: { height: "100%", left: "-100%", overflow: "hidden", position: "absolute", top: 0, width: "200%" },
|
|
892
|
+
pie: { overflow: "hidden" },
|
|
893
|
+
rotator: { height: "100%", left: 0, overflow: "hidden", position: "absolute", top: 0, width: "100%" },
|
|
894
|
+
selectedRing: { alignItems: "center", borderColor: "white", borderWidth: 3, bottom: 0, justifyContent: "center", left: 0, position: "absolute", right: 0, top: 0 },
|
|
895
|
+
swatch: { alignItems: "center", justifyContent: "center" },
|
|
896
|
+
swatches: { flexDirection: "row", flexWrap: "wrap", gap: SWATCH_GRID_GAP, paddingTop: 12 },
|
|
897
|
+
trigger: { alignItems: "center", alignSelf: "flex-start", borderRadius: SIZE2 / 2, height: SIZE2, justifyContent: "center", width: SIZE2 },
|
|
898
|
+
triggerContent: { height: SIZE2, width: SIZE2 },
|
|
899
|
+
triggerIcon: { alignItems: "center", bottom: 0, justifyContent: "center", left: 0, position: "absolute", right: 0, top: 0 },
|
|
900
|
+
wedgeFill: { height: "100%", left: "50%", position: "absolute", top: 0, width: "50%" }
|
|
901
|
+
});
|
|
902
|
+
|
|
804
903
|
// src/components/TextInput.tsx
|
|
805
904
|
import { forwardRef } from "react";
|
|
806
905
|
import { TextInput as PaperTextInput } from "react-native-paper";
|
|
807
|
-
import { jsx as
|
|
906
|
+
import { jsx as jsx15 } from "react/jsx-runtime";
|
|
808
907
|
var TextInput = forwardRef(function TextInput2(props, ref) {
|
|
809
908
|
const defaults = usePaperDefaults();
|
|
810
|
-
return /* @__PURE__ */
|
|
909
|
+
return /* @__PURE__ */ jsx15(PaperTextInput, { ref, ...defaults.TextInput, ...props });
|
|
811
910
|
});
|
|
812
911
|
|
|
813
912
|
// src/redux/themeSlice.ts
|
|
@@ -869,6 +968,7 @@ export {
|
|
|
869
968
|
HarmonyPicker,
|
|
870
969
|
IconButton,
|
|
871
970
|
Menu,
|
|
971
|
+
PalettePicker,
|
|
872
972
|
Provider,
|
|
873
973
|
TextInput,
|
|
874
974
|
ThemeSettingsContext,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rific/auto-paper",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Adaptive react-native-paper theming: triadic palette from a seed color + system/light/dark appearance",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|
|
@@ -86,6 +86,7 @@
|
|
|
86
86
|
"react-dom": "^19.0.0",
|
|
87
87
|
"react-native": "^0.76.0",
|
|
88
88
|
"react-native-paper": "^5.12.0",
|
|
89
|
+
"react-native-safe-area-context": "^5.5.2",
|
|
89
90
|
"ts-jest": "^29.4.9",
|
|
90
91
|
"ts-node": "^10.9.2",
|
|
91
92
|
"tsup": "^8.0.0",
|
|
@@ -97,7 +98,8 @@
|
|
|
97
98
|
"expo-navigation-bar": ">=56.0.0",
|
|
98
99
|
"react": ">=18.0.0",
|
|
99
100
|
"react-native": ">=0.76.0",
|
|
100
|
-
"react-native-paper": ">=5.0.0"
|
|
101
|
+
"react-native-paper": ">=5.0.0",
|
|
102
|
+
"react-native-safe-area-context": "*"
|
|
101
103
|
},
|
|
102
104
|
"peerDependenciesMeta": {
|
|
103
105
|
"expo-blur": {
|
|
@@ -67,7 +67,7 @@ export const BlurView = ({ blur = true, children, elevation, style, tintColor, t
|
|
|
67
67
|
</ExpoBlurView>
|
|
68
68
|
)
|
|
69
69
|
return (
|
|
70
|
-
<View style={[{ backgroundColor: surfaceColor }, style]}>
|
|
70
|
+
<View {...props} style={[{ backgroundColor: surfaceColor }, style]}>
|
|
71
71
|
{tint}
|
|
72
72
|
{children}
|
|
73
73
|
</View>
|
|
@@ -3,8 +3,11 @@ import { StyleSheet, TouchableOpacity, View } from 'react-native'
|
|
|
3
3
|
import { Icon, TouchableRipple } from 'react-native-paper'
|
|
4
4
|
|
|
5
5
|
import { useBlur } from '../BlurContext'
|
|
6
|
+
import { SWATCH_GRID_GAP, useSwatchSize } from '../useSwatchGrid'
|
|
6
7
|
import { Dialog } from './Dialog'
|
|
7
8
|
|
|
9
|
+
const SIZE = 48
|
|
10
|
+
|
|
8
11
|
export type SeedColor = { label: string; value: string }
|
|
9
12
|
|
|
10
13
|
export const defaultColors: SeedColor[] = [
|
|
@@ -17,6 +20,7 @@ export const defaultColors: SeedColor[] = [
|
|
|
17
20
|
{ label: 'Emerald', value: '#4caf50' },
|
|
18
21
|
{ label: 'Teal', value: '#009688' },
|
|
19
22
|
{ label: 'Cyan', value: '#00bcd4' },
|
|
23
|
+
{ label: 'Sky', value: '#03a9f4' },
|
|
20
24
|
{ label: 'Blue', value: '#2196f3' },
|
|
21
25
|
{ label: 'Indigo', value: '#3f51b5' },
|
|
22
26
|
{ label: 'Deep Purple', value: '#673ab7' },
|
|
@@ -25,6 +29,7 @@ export const defaultColors: SeedColor[] = [
|
|
|
25
29
|
{ label: 'Pink', value: '#ec407a' },
|
|
26
30
|
{ label: 'Rose', value: '#e91e63' },
|
|
27
31
|
{ label: 'Brown', value: '#795548' },
|
|
32
|
+
{ label: 'Taupe', value: '#8d7b68' },
|
|
28
33
|
{ label: 'Blue Grey', value: '#607d8b' }
|
|
29
34
|
]
|
|
30
35
|
|
|
@@ -38,6 +43,7 @@ type Props = {
|
|
|
38
43
|
export const ColorPicker = ({ value, onChange, colors = defaultColors, blur: blurProp }: Props) => {
|
|
39
44
|
const blur = useBlur(blurProp)
|
|
40
45
|
const [open, setOpen] = useState(false)
|
|
46
|
+
const { onLayout, size } = useSwatchSize(SIZE)
|
|
41
47
|
|
|
42
48
|
return (
|
|
43
49
|
<>
|
|
@@ -47,7 +53,7 @@ export const ColorPicker = ({ value, onChange, colors = defaultColors, blur: blu
|
|
|
47
53
|
|
|
48
54
|
<Dialog blur={blur} visible={open} onDismiss={() => setOpen(false)}>
|
|
49
55
|
<Dialog.Content>
|
|
50
|
-
<View style={styles.swatches}>
|
|
56
|
+
<View style={styles.swatches} onLayout={onLayout}>
|
|
51
57
|
{colors.map(({ value: hex }) => (
|
|
52
58
|
<TouchableOpacity
|
|
53
59
|
key={hex}
|
|
@@ -55,7 +61,7 @@ export const ColorPicker = ({ value, onChange, colors = defaultColors, blur: blu
|
|
|
55
61
|
onChange(hex)
|
|
56
62
|
setOpen(false)
|
|
57
63
|
}}
|
|
58
|
-
style={[styles.swatch, { backgroundColor: hex }, value === hex && styles.selected]}
|
|
64
|
+
style={[styles.swatch, { backgroundColor: hex, borderRadius: size / 2, height: size, width: size }, value === hex && styles.selected]}
|
|
59
65
|
>
|
|
60
66
|
{value === hex && <Icon source='check' size={20} color='white' />}
|
|
61
67
|
</TouchableOpacity>
|
|
@@ -69,7 +75,7 @@ export const ColorPicker = ({ value, onChange, colors = defaultColors, blur: blu
|
|
|
69
75
|
|
|
70
76
|
const styles = StyleSheet.create({
|
|
71
77
|
selected: { borderColor: 'white', borderWidth: 3 },
|
|
72
|
-
swatch: { alignItems: 'center',
|
|
73
|
-
swatches: { flexDirection: 'row', flexWrap: 'wrap', gap:
|
|
74
|
-
trigger: { alignItems: 'center', alignSelf: 'flex-start', borderRadius:
|
|
78
|
+
swatch: { alignItems: 'center', justifyContent: 'center' },
|
|
79
|
+
swatches: { flexDirection: 'row', flexWrap: 'wrap', gap: SWATCH_GRID_GAP, paddingTop: 12 },
|
|
80
|
+
trigger: { alignItems: 'center', alignSelf: 'flex-start', borderRadius: SIZE / 2, height: SIZE, justifyContent: 'center', width: SIZE }
|
|
75
81
|
})
|
|
@@ -1,47 +1,83 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from 'react'
|
|
2
|
-
import { Animated,
|
|
2
|
+
import { Animated, BackHandler, Easing, Pressable, type StyleProp, StyleSheet, View, type ViewStyle } from 'react-native'
|
|
3
3
|
import { Dialog as PaperDialog, type DialogActionsProps, type DialogContentProps, type DialogProps as PaperDialogProps, type DialogScrollAreaProps, type DialogTitleProps, Portal, useTheme } from 'react-native-paper'
|
|
4
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
4
5
|
|
|
5
6
|
import { useBlur } from '../BlurContext'
|
|
6
7
|
import { BlurView } from './BlurView'
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
// `style` is narrowed to a plain ViewStyle: unlike react-native-paper's Dialog, the surface here is
|
|
10
|
+
// never wrapped in Animated.View, so it can't drive an Animated-value style the way upstream's can.
|
|
11
|
+
export type DialogProps = Omit<PaperDialogProps, 'style'> & {
|
|
9
12
|
blur?: boolean
|
|
13
|
+
style?: StyleProp<ViewStyle>
|
|
10
14
|
}
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
// Matches react-native-paper's Modal: DEFAULT_DURATION * theme.animation.scale, eased the same way,
|
|
17
|
+
// so toggling blur never changes how the dialog animates in/out.
|
|
18
|
+
const ANIMATION_DURATION = 220
|
|
19
|
+
|
|
20
|
+
// Minimum breathing room between the card and the screen edge, clamped up from safe-area insets.
|
|
21
|
+
const DIALOG_MARGIN = 32
|
|
22
|
+
|
|
23
|
+
function DialogComponent({ blur: blurProp, dismissable = true, dismissableBackButton = dismissable, visible, onDismiss, children, style, testID = 'dialog' }: DialogProps) {
|
|
13
24
|
const blur = useBlur(blurProp)
|
|
14
|
-
const
|
|
15
|
-
const
|
|
25
|
+
const theme = useTheme()
|
|
26
|
+
const { colors, isV3, roundness } = theme
|
|
27
|
+
const { scale } = theme.animation
|
|
28
|
+
const { bottom, left, right, top } = useSafeAreaInsets()
|
|
29
|
+
const opacity = useRef(new Animated.Value(visible ? 1 : 0)).current
|
|
16
30
|
const [mounted, setMounted] = useState(visible)
|
|
17
31
|
|
|
18
32
|
useEffect(() => {
|
|
19
33
|
if (visible) {
|
|
20
34
|
setMounted(true)
|
|
21
|
-
Animated.timing(opacity, {
|
|
35
|
+
Animated.timing(opacity, { duration: scale * ANIMATION_DURATION, easing: Easing.out(Easing.cubic), toValue: 1, useNativeDriver: true }).start()
|
|
22
36
|
} else {
|
|
23
|
-
Animated.timing(opacity, {
|
|
37
|
+
Animated.timing(opacity, { duration: scale * ANIMATION_DURATION, easing: Easing.out(Easing.cubic), toValue: 0, useNativeDriver: true }).start(({ finished }) => {
|
|
38
|
+
if (finished) setMounted(false)
|
|
39
|
+
})
|
|
24
40
|
}
|
|
25
41
|
}, [visible])
|
|
26
42
|
|
|
27
|
-
|
|
28
|
-
return
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!visible) return undefined
|
|
45
|
+
const onHardwareBackPress = () => {
|
|
46
|
+
if (dismissable || dismissableBackButton) onDismiss?.()
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
const subscription = BackHandler.addEventListener('hardwareBackPress', onHardwareBackPress)
|
|
50
|
+
return () => subscription.remove()
|
|
51
|
+
}, [dismissable, dismissableBackButton, onDismiss, visible])
|
|
34
52
|
|
|
35
53
|
if (!mounted) return null
|
|
36
54
|
|
|
55
|
+
// Mirrors react-native-paper's own Dialog: the first child gets extra top spacing so the
|
|
56
|
+
// header sits the same distance from the edge whether the surface is blurred or solid.
|
|
57
|
+
const content = React.Children.toArray(children)
|
|
58
|
+
.filter((child) => child != null && typeof child !== 'boolean')
|
|
59
|
+
.map((child, i) => {
|
|
60
|
+
if (isV3) {
|
|
61
|
+
if (i === 0 && React.isValidElement<DialogContentProps>(child)) {
|
|
62
|
+
return React.cloneElement(child, { style: [{ marginTop: 24 }, child.props.style] })
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (i === 0 && React.isValidElement<DialogContentProps>(child) && child.type === PaperDialog.Content) {
|
|
66
|
+
return React.cloneElement(child, { style: [{ paddingTop: 24 }, child.props.style] })
|
|
67
|
+
}
|
|
68
|
+
return child
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const borderRadius = (isV3 ? 7 : 1) * roundness
|
|
72
|
+
|
|
37
73
|
return (
|
|
38
74
|
<Portal>
|
|
39
|
-
<Animated.View
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
75
|
+
<Animated.View accessibilityLiveRegion='polite' accessibilityViewIsModal pointerEvents={visible ? 'box-none' : 'none'} style={[StyleSheet.absoluteFill, { opacity }]} testID={testID}>
|
|
76
|
+
<Pressable accessibilityLabel='Close modal' accessibilityRole='button' disabled={!dismissable} onPress={dismissable ? onDismiss : undefined} style={[StyleSheet.absoluteFill, { backgroundColor: colors.backdrop }]} testID={`${testID}-backdrop`} />
|
|
77
|
+
<View pointerEvents='box-none' style={[styles.wrapper, { marginBottom: bottom, marginTop: top, paddingHorizontal: Math.max(left, right, DIALOG_MARGIN) }]} testID={`${testID}-wrapper`}>
|
|
78
|
+
<BlurView blur={blur} style={[styles.card, { borderRadius }, style]} testID={`${testID}-surface`}>
|
|
79
|
+
{content}
|
|
80
|
+
</BlurView>
|
|
45
81
|
</View>
|
|
46
82
|
</Animated.View>
|
|
47
83
|
</Portal>
|
|
@@ -58,6 +94,5 @@ export const Dialog = DialogComponent
|
|
|
58
94
|
const styles = StyleSheet.create({
|
|
59
95
|
// Width cap keeps dialogs card-sized on wide viewports (web/tablet); no effect on phones.
|
|
60
96
|
card: { alignSelf: 'center', maxWidth: 400, overflow: 'hidden', width: '100%' },
|
|
61
|
-
|
|
62
|
-
wrapper: { flex: 1, justifyContent: 'center', paddingHorizontal: 26 }
|
|
97
|
+
wrapper: { flex: 1, justifyContent: 'center', paddingVertical: DIALOG_MARGIN }
|
|
63
98
|
})
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { StyleSheet, TouchableOpacity, View } from 'react-native'
|
|
3
|
+
import { Icon, TouchableRipple } from 'react-native-paper'
|
|
4
|
+
|
|
5
|
+
import { useBlur } from '../BlurContext'
|
|
6
|
+
import { SWATCH_GRID_GAP, useSwatchSize } from '../useSwatchGrid'
|
|
7
|
+
import type { ColorHarmony, TriadicPalette } from '../utils/getTriadicPalette'
|
|
8
|
+
import { getTriadicPalette } from '../utils/getTriadicPalette'
|
|
9
|
+
import type { SeedColor } from './ColorPicker'
|
|
10
|
+
import { defaultColors } from './ColorPicker'
|
|
11
|
+
import { Dialog } from './Dialog'
|
|
12
|
+
|
|
13
|
+
const SIZE = 48
|
|
14
|
+
|
|
15
|
+
export type PaletteWeights = { primary: number; secondary: number; tertiary: number }
|
|
16
|
+
|
|
17
|
+
const defaultWeights: PaletteWeights = { primary: 2, secondary: 1, tertiary: 1 }
|
|
18
|
+
|
|
19
|
+
// Each wedge = a static right-half mask, doubly rotated: an outer spin positions the slice's
|
|
20
|
+
// leading edge around the circle, then an inner spin (its own share, minus 180deg) carves the
|
|
21
|
+
// trailing edge — together covering exactly `share` of weights' total, for any share <= 50%.
|
|
22
|
+
// Both rotators are sized to exactly overlap the full circle so React Native's default
|
|
23
|
+
// (own-center) rotation pivot is already correct — transformOrigin is intentionally avoided
|
|
24
|
+
// since it isn't reliably honored across RN renderers. Primary is centered at 12 o'clock,
|
|
25
|
+
// tertiary fills clockwise from there (bottom-right), secondary fills the remainder (bottom-left).
|
|
26
|
+
const buildWedges = (weights: PaletteWeights) => {
|
|
27
|
+
const total = weights.primary + weights.secondary + weights.tertiary
|
|
28
|
+
const primary = (weights.primary / total) * 360
|
|
29
|
+
const secondary = (weights.secondary / total) * 360
|
|
30
|
+
const tertiary = (weights.tertiary / total) * 360
|
|
31
|
+
|
|
32
|
+
return [
|
|
33
|
+
{ key: 'primary' as const, rotate: -primary / 2, share: primary },
|
|
34
|
+
{ key: 'tertiary' as const, rotate: primary / 2, share: tertiary },
|
|
35
|
+
{ key: 'secondary' as const, rotate: primary / 2 + tertiary, share: secondary }
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const Pie = ({ palette, weights = defaultWeights, size = SIZE }: { palette: TriadicPalette; weights?: PaletteWeights; size?: number }) => (
|
|
40
|
+
<View style={[styles.pie, { borderRadius: size / 2, height: size, width: size }]}>
|
|
41
|
+
{buildWedges(weights).map(({ key, rotate, share }) => (
|
|
42
|
+
<View key={key} style={[styles.rotator, { transform: [{ rotate: `${rotate}deg` }] }]}>
|
|
43
|
+
<View style={styles.halfMask}>
|
|
44
|
+
<View style={[styles.innerRotator, { transform: [{ rotate: `${share - 180}deg` }] }]}>
|
|
45
|
+
<View style={[styles.wedgeFill, { backgroundColor: palette[key] }]} />
|
|
46
|
+
</View>
|
|
47
|
+
</View>
|
|
48
|
+
</View>
|
|
49
|
+
))}
|
|
50
|
+
</View>
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
type Props = {
|
|
54
|
+
value: string
|
|
55
|
+
onChange: (color: string) => void
|
|
56
|
+
harmony?: ColorHarmony
|
|
57
|
+
weights?: PaletteWeights
|
|
58
|
+
colors?: SeedColor[]
|
|
59
|
+
blur?: boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const PalettePicker = ({ value, onChange, harmony = 'split-complementary', weights, colors = defaultColors, blur: blurProp }: Props) => {
|
|
63
|
+
const blur = useBlur(blurProp)
|
|
64
|
+
const [open, setOpen] = useState(false)
|
|
65
|
+
const { onLayout, size } = useSwatchSize(SIZE)
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<>
|
|
69
|
+
<TouchableRipple onPress={() => setOpen(true)} style={styles.trigger} borderless>
|
|
70
|
+
<View style={styles.triggerContent}>
|
|
71
|
+
<Pie palette={getTriadicPalette(value, harmony)} weights={weights} />
|
|
72
|
+
<View pointerEvents='none' style={styles.triggerIcon}>
|
|
73
|
+
<Icon source='palette' size={20} color='white' />
|
|
74
|
+
</View>
|
|
75
|
+
</View>
|
|
76
|
+
</TouchableRipple>
|
|
77
|
+
|
|
78
|
+
<Dialog blur={blur} visible={open} onDismiss={() => setOpen(false)}>
|
|
79
|
+
<Dialog.Content>
|
|
80
|
+
<View style={styles.swatches} onLayout={onLayout}>
|
|
81
|
+
{colors.map(({ value: hex }) => (
|
|
82
|
+
<TouchableOpacity
|
|
83
|
+
key={hex}
|
|
84
|
+
onPress={() => {
|
|
85
|
+
onChange(hex)
|
|
86
|
+
setOpen(false)
|
|
87
|
+
}}
|
|
88
|
+
style={[styles.swatch, { height: size, width: size }]}
|
|
89
|
+
>
|
|
90
|
+
<Pie palette={getTriadicPalette(hex, harmony)} weights={weights} size={size} />
|
|
91
|
+
{value === hex && (
|
|
92
|
+
<View pointerEvents='none' style={[styles.selectedRing, { borderRadius: size / 2 }]}>
|
|
93
|
+
<Icon source='check' size={20} color='white' />
|
|
94
|
+
</View>
|
|
95
|
+
)}
|
|
96
|
+
</TouchableOpacity>
|
|
97
|
+
))}
|
|
98
|
+
</View>
|
|
99
|
+
</Dialog.Content>
|
|
100
|
+
</Dialog>
|
|
101
|
+
</>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const styles = StyleSheet.create({
|
|
106
|
+
halfMask: { height: '100%', left: '50%', overflow: 'hidden', position: 'absolute', top: 0, width: '50%' },
|
|
107
|
+
innerRotator: { height: '100%', left: '-100%', overflow: 'hidden', position: 'absolute', top: 0, width: '200%' },
|
|
108
|
+
pie: { overflow: 'hidden' },
|
|
109
|
+
rotator: { height: '100%', left: 0, overflow: 'hidden', position: 'absolute', top: 0, width: '100%' },
|
|
110
|
+
selectedRing: { alignItems: 'center', borderColor: 'white', borderWidth: 3, bottom: 0, justifyContent: 'center', left: 0, position: 'absolute', right: 0, top: 0 },
|
|
111
|
+
swatch: { alignItems: 'center', justifyContent: 'center' },
|
|
112
|
+
swatches: { flexDirection: 'row', flexWrap: 'wrap', gap: SWATCH_GRID_GAP, paddingTop: 12 },
|
|
113
|
+
trigger: { alignItems: 'center', alignSelf: 'flex-start', borderRadius: SIZE / 2, height: SIZE, justifyContent: 'center', width: SIZE },
|
|
114
|
+
triggerContent: { height: SIZE, width: SIZE },
|
|
115
|
+
triggerIcon: { alignItems: 'center', bottom: 0, justifyContent: 'center', left: 0, position: 'absolute', right: 0, top: 0 },
|
|
116
|
+
wedgeFill: { height: '100%', left: '50%', position: 'absolute', top: 0, width: '50%' }
|
|
117
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -17,6 +17,8 @@ export type { IconButtonProps } from './components/IconButton'
|
|
|
17
17
|
export { IconButton } from './components/IconButton'
|
|
18
18
|
export type { MenuProps } from './components/Menu'
|
|
19
19
|
export { Menu } from './components/Menu'
|
|
20
|
+
export type { PaletteWeights } from './components/PalettePicker'
|
|
21
|
+
export { PalettePicker } from './components/PalettePicker'
|
|
20
22
|
export { TextInput } from './components/TextInput'
|
|
21
23
|
export type { PaperDefaults } from './PaperDefaultsContext'
|
|
22
24
|
export { usePaperDefaults } from './PaperDefaultsContext'
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import type { LayoutChangeEvent } from 'react-native'
|
|
3
|
+
|
|
4
|
+
// Fixed column count so the swatch grid always fills complete rows, regardless of dialog width
|
|
5
|
+
// (phone vs tablet vs web) — sizing the swatches to fit, rather than wrapping fixed-size swatches
|
|
6
|
+
// and hoping the total divides evenly into whatever column count the width happens to produce.
|
|
7
|
+
export const SWATCH_GRID_COLUMNS = 5
|
|
8
|
+
export const SWATCH_GRID_GAP = 12
|
|
9
|
+
|
|
10
|
+
// `fallback` is returned before the container's first layout pass reports a real width.
|
|
11
|
+
export function useSwatchSize(fallback: number) {
|
|
12
|
+
const [width, setWidth] = useState(0)
|
|
13
|
+
|
|
14
|
+
const onLayout = ({ nativeEvent }: LayoutChangeEvent) => setWidth(nativeEvent.layout.width)
|
|
15
|
+
const size = width > 0 ? (width - SWATCH_GRID_GAP * (SWATCH_GRID_COLUMNS - 1)) / SWATCH_GRID_COLUMNS : fallback
|
|
16
|
+
|
|
17
|
+
return { onLayout, size }
|
|
18
|
+
}
|