@rific/auto-paper 0.9.1 → 0.9.3

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 CHANGED
@@ -33,7 +33,7 @@ npm install expo-blur # frosted-glass BlurView
33
33
  npm install expo-navigation-bar # (>= 56.0.0, SDK 54+) auto-syncs Android nav bar icon style to the theme when BottomNavigation is mounted
34
34
  ```
35
35
 
36
- Neither is auto-detected: pass them to `Provider` (`expoBlur`, `navigationBar`, see the prop table below) and `BlurView`/`BottomNavigation` pick them up automatically. Omit either and you get a working fallback instead: `BlurView` renders its solid (non-blur) look, and the Android nav bar simply isn't synced.
36
+ These integrations are opt-in: pass them to `Provider` (`expoBlur`, `navigationBar`, and `reanimated` — see the prop table below), and the relevant wrappers pick them up automatically. Omit them and you still get working fallbacks: `BlurView` renders its solid (non-blur) look, `BottomNavigation` simply skips nav-bar syncing, and `Dialog` treats `animatedStyle` as a no-op unless a reanimated module is injected.
37
37
 
38
38
  The exported Redux slice has no dependency on `@reduxjs/toolkit`: it works with RTK stores, vanilla Redux, or no Redux at all.
39
39
 
@@ -238,6 +238,7 @@ export default function App() {
238
238
  | `navigationBar` | `ExpoNavigationBarModule` | Injects `expo-navigation-bar` (`import * as ExpoNavigationBar from 'expo-navigation-bar'`) so the Android nav bar icon style auto-syncs with the theme while a `BottomNavigation` is mounted. Omit (and don't pass `onNavBarChange`) to skip nav bar syncing entirely. |
239
239
  | `onNavBarChange` | `(color: string, dark: boolean) => void` | Overrides the built-in nav bar sync: called on Android when the theme changes while a `BottomNavigation` is mounted, instead of the automatic `navigationBar`-driven icon-style sync |
240
240
  | `onReady` | `() => void` | Called once when the theme first resolves |
241
+ | `reanimated` | `ReanimatedModule` | Injects `react-native-reanimated` (for example `import Reanimated from 'react-native-reanimated'`) so `<Dialog animatedStyle={...}>` can animate its card on the UI thread. Omit to keep `animatedStyle` as a no-op. |
241
242
  | `statusBarProps` | `StatusBarProps` | Spread over the auto-derived `StatusBar` defaults |
242
243
  | `style` | `StyleProp<ViewStyle>` | Applied to the wrapper `View` |
243
244
 
@@ -255,6 +256,8 @@ export default function App() {
255
256
 
256
257
  Returns `{ settings: ThemeSettings, set: (patch: Partial<ThemeSettings>) => void }`. Use this hook from any component inside `Provider` to read or update the current theme settings.
257
258
 
259
+ `useReanimatedModule()` is also available inside the provider tree when you need to read the injected reanimated module directly; it returns `undefined` unless `Provider` was given a `reanimated` prop.
260
+
258
261
  ```tsx
259
262
  import { useThemeSettings } from '@rific/auto-paper'
260
263
 
@@ -324,6 +327,27 @@ function SettingsScreen() {
324
327
  | `showLabels` | `boolean` | Set `false` for icon-only segments, useful in tight layouts where "System" would truncate. `accessibilityLabel` is always set regardless, so screen readers still announce the full word. Defaults to `true` |
325
328
  | `icons` | `AppearanceIcons` | Per-value icon overrides: pass just the ones you want to change (e.g. `{ system: 'theme-light-dark' }`), merged over the defaults (`monitor` / `white-balance-sunny` / `weather-night`) |
326
329
 
330
+ ### `HarmonyPicker`
331
+
332
+ Switches between the 6 `ColorHarmony` modes (`triadic`, `split-complementary`, `analogous`, `square`, `complementary`, `double-split`) via `react-native-paper`'s `SegmentedButtons`. `PalettePicker` can embed this same picker in its own dialog instead of standing alone — see `onHarmonyChange` below.
333
+
334
+ ```tsx
335
+ import { HarmonyPicker, useThemeSettings } from '@rific/auto-paper'
336
+
337
+ function SettingsScreen() {
338
+ const { settings: { harmony }, set } = useThemeSettings()
339
+ return <HarmonyPicker value={harmony} onChange={(h) => set({ harmony: h })} showLabels={false} />
340
+ }
341
+ ```
342
+
343
+ | Prop | Type | Description |
344
+ |---|---|---|
345
+ | `value` | `ColorHarmony` | Currently selected harmony |
346
+ | `onChange` | `(harmony: ColorHarmony) => void` | Called when a segment is tapped |
347
+ | `showLabels` | `boolean` | Set `false` for icon-only segments. Also lowers Paper's default 76px-per-segment minimum width down to 0, so all 6 segments can always shrink to fit — however narrow the container, they resize down together instead of overflowing it. `accessibilityLabel` is always set regardless, so screen readers still announce the full harmony name either way. Defaults to `true` |
348
+ | `checkedColor` / `uncheckedColor` | `string` | Icon color for the checked and unchecked segments, matching Paper's own segmented-button vocabulary |
349
+ | `checkedContainerColor` / `uncheckedContainerColor` | `string` | Fill color for the checked and unchecked segments. Omit any of these four color props to fall back to Paper's default segmented-button styling |
350
+
327
351
  ### `Appbar`
328
352
 
329
353
  A thin wrapper around `react-native-paper`'s `Appbar` that automatically syncs `StatusBar` background color and bar style to the current theme surface color.
@@ -377,6 +401,35 @@ import { Chip } from '@rific/auto-paper'
377
401
 
378
402
  `ChipProps` is exported from `@rific/auto-paper` and extends `react-native-paper`'s `ChipProps` with the `variant` field. Use `PaperChipProps` if you need the base paper type.
379
403
 
404
+ ### `Dialog`
405
+
406
+ A thin wrapper around `react-native-paper`'s `Dialog` with built-in blur support and optional UI-thread animation hooks. `blur` overrides the ambient blur setting for that dialog, and `animatedStyle` can be used to animate the card wrapper when a reanimated module has been injected into `Provider`.
407
+
408
+ ```tsx
409
+ import { Dialog, Provider } from '@rific/auto-paper'
410
+ import Reanimated, { useAnimatedStyle } from 'react-native-reanimated'
411
+
412
+ function SettingsDialog({ visible }: { visible: boolean }) {
413
+ const animatedStyle = useAnimatedStyle(() => ({ opacity: 1 }))
414
+
415
+ return (
416
+ <Provider initialValue={{ appearance: 'system', color: '#6750a4' }} reanimated={Reanimated}>
417
+ <Dialog visible={visible} onDismiss={() => {}} animatedStyle={animatedStyle} blur>
418
+ {/* dialog contents */}
419
+ </Dialog>
420
+ </Provider>
421
+ )
422
+ }
423
+ ```
424
+
425
+ | Prop | Type | Description |
426
+ |---|---|---|
427
+ | `blur` | `boolean` | Overrides the ambient blur setting for this component's dialog |
428
+ | `animatedStyle` | `unknown` | Applied to the card's outer wrapper when a reanimated module has been injected; otherwise ignored |
429
+ | ...all `DialogProps` | | All props from `react-native-paper`'s `Dialog` are supported |
430
+
431
+ `DialogProps` is exported from `@rific/auto-paper` and extends `react-native-paper`'s `DialogProps` with the `blur` and `animatedStyle` fields. Use `PaperDialogProps` if you need the base paper type.
432
+
380
433
  ### `IconButton`
381
434
 
382
435
  A thin wrapper around `react-native-paper`'s `IconButton` that adds a `variant` prop for applying theme-derived container and icon colors.
@@ -400,14 +453,14 @@ import { IconButton } from '@rific/auto-paper'
400
453
 
401
454
  ### `PalettePicker`
402
455
 
403
- 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.
456
+ 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. Pass `onHarmonyChange` to fold a compact, icon-only `HarmonyPicker` row into the same dialog, above the swatch grid — every swatch's own pie reshapes live as harmony changes, so there's one place to tune both instead of two separate controls. Omit it and this renders exactly as a plain color-only picker always has.
404
457
 
405
458
  ```tsx
406
459
  import { PalettePicker, useThemeSettings } from '@rific/auto-paper'
407
460
 
408
461
  function SettingsScreen() {
409
462
  const { settings: { color, harmony }, set } = useThemeSettings()
410
- return <PalettePicker value={color} harmony={harmony} onChange={(c) => set({ color: c })} />
463
+ return <PalettePicker value={color} harmony={harmony} onChange={(c) => set({ color: c })} onHarmonyChange={(h) => set({ harmony: h })} />
411
464
  }
412
465
  ```
413
466
 
@@ -416,6 +469,7 @@ function SettingsScreen() {
416
469
  | `value` | `string` | Currently selected seed color |
417
470
  | `onChange` | `(color: string) => void` | Called with the seed color when a swatch is tapped |
418
471
  | `harmony` | `ColorHarmony` | Harmony used to compute each swatch's preview palette. Defaults to `'split-complementary'` |
472
+ | `onHarmonyChange` | `(harmony: ColorHarmony) => void` | Opt-in: passing this renders the `HarmonyPicker` row inside the dialog. Gated on the callback itself rather than a separate boolean, so there's no way to pass one without the other |
419
473
  | `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 |
420
474
  | `colors` | `SeedColor[]` | Seed color swatches to offer. Defaults to the same set as `ColorPicker` |
421
475
  | `blur` | `boolean` | Overrides the ambient blur setting for this component's dialog |
package/dist/index.d.mts CHANGED
@@ -127,11 +127,19 @@ type Props$2 = {
127
127
  };
128
128
  declare const ColorPicker: ({ value, onChange, colors, blur: blurProp }: Props$2) => react.JSX.Element;
129
129
 
130
+ interface ReanimatedModule {
131
+ View: ComponentType<{
132
+ children?: ReactNode;
133
+ style?: any;
134
+ testID?: string;
135
+ }>;
136
+ }
130
137
  type DialogProps = Omit<DialogProps$1, 'style'> & {
131
138
  blur?: boolean;
132
139
  style?: StyleProp<ViewStyle>;
140
+ animatedStyle?: unknown;
133
141
  };
134
- declare function DialogComponent({ blur: blurProp, dismissable, dismissableBackButton, visible, onDismiss, children, style, testID }: DialogProps): react__default.JSX.Element | null;
142
+ declare function DialogComponent({ animatedStyle, blur: blurProp, dismissable, dismissableBackButton, visible, onDismiss, children, style, testID }: DialogProps): react__default.JSX.Element | null;
135
143
  declare namespace DialogComponent {
136
144
  var Title: react__default.FC<DialogTitleProps>;
137
145
  var Content: react__default.FC<DialogContentProps>;
@@ -145,9 +153,13 @@ declare function FAB(props: FABProps): react.JSX.Element;
145
153
  type Props$1 = {
146
154
  value: ColorHarmony;
147
155
  onChange: (harmony: ColorHarmony) => void;
148
- blur?: boolean;
156
+ showLabels?: boolean;
157
+ checkedColor?: string;
158
+ uncheckedColor?: string;
159
+ checkedContainerColor?: string;
160
+ uncheckedContainerColor?: string;
149
161
  };
150
- declare const HarmonyPicker: ({ value, onChange, blur: blurProp }: Props$1) => react.JSX.Element;
162
+ declare const HarmonyPicker: ({ value, onChange, showLabels, checkedColor, uncheckedColor, checkedContainerColor, uncheckedContainerColor }: Props$1) => react.JSX.Element;
151
163
 
152
164
  type IconButtonProps = IconButtonProps$1 & {
153
165
  variant?: 'primary' | 'secondary' | 'tertiary' | 'surface';
@@ -170,11 +182,12 @@ type Props = {
170
182
  value: string;
171
183
  onChange: (color: string) => void;
172
184
  harmony?: ColorHarmony;
185
+ onHarmonyChange?: (harmony: ColorHarmony) => void;
173
186
  weights?: PaletteWeights;
174
187
  colors?: SeedColor[];
175
188
  blur?: boolean;
176
189
  };
177
- declare const PalettePicker: ({ value, onChange, harmony, weights, colors, blur: blurProp }: Props) => react.JSX.Element;
190
+ declare const PalettePicker: ({ value, onChange, harmony, onHarmonyChange, weights, colors, blur: blurProp }: Props) => react.JSX.Element;
178
191
 
179
192
  declare const TextInput: react.ForwardRefExoticComponent<Omit<TextInputProps, "ref"> & react.RefAttributes<TextInput$1>>;
180
193
 
@@ -273,6 +286,7 @@ type NavBarContextType = {
273
286
  };
274
287
  declare const useNavBarContext: () => NavBarContextType;
275
288
  declare const useBlurModule: () => ExpoBlurModule | undefined;
289
+ declare const useReanimatedModule: () => ReanimatedModule | undefined;
276
290
  type ProviderProps = {
277
291
  children: ReactNode;
278
292
  defaults?: PaperDefaults;
@@ -284,10 +298,12 @@ type ProviderProps = {
284
298
  onChange?: (settings: ThemeSettings) => void;
285
299
  onNavBarChange?: (color: string, dark: boolean) => void;
286
300
  onReady?: () => void;
301
+ /** 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. */
302
+ reanimated?: ReanimatedModule;
287
303
  statusBarProps?: StatusBarProps;
288
304
  style?: StyleProp<ViewStyle>;
289
305
  };
290
- declare function Provider({ children, defaults, expoBlur, initialValue, navigationBar, onChange, onNavBarChange, onReady, statusBarProps, style }: ProviderProps): react.JSX.Element | null;
306
+ declare function Provider({ children, defaults, expoBlur, initialValue, navigationBar, onChange, onNavBarChange, onReady, reanimated, statusBarProps, style }: ProviderProps): react.JSX.Element | null;
291
307
 
292
308
  declare const useAutoPaperTheme: () => AutoPaperTheme;
293
309
 
@@ -323,4 +339,4 @@ declare const getTonalColor: (color: string, lightness: number) => string;
323
339
 
324
340
  declare const isDarkColor: (value: string) => boolean;
325
341
 
326
- export { Appbar, type AppearanceIcons, AppearancePicker, type AutoPaperTheme, BlurView, type BlurViewProps, BottomNavigation, Button, Chip, type ChipProps, type ColorHarmony, ColorPicker, type ColorRoles, Dialog, type DialogProps, type ExpoBlurModule, type ExpoNavigationBarModule, FAB, HarmonyPicker, IconButton, type IconButtonProps, Menu, type MenuProps, PalettePicker, type PaletteWeights, type PaperDefaults, Provider, type ProviderProps, type RGB, SEMANTIC_BASE_COLORS, type SeedColor, type SemanticColorRoles, TextInput, type ThemeAppearance, type ThemeSettings, ThemeSettingsContext, type ThemeSettingsContextType, type ThemeState, type TriadicPalette, createThemeReducer, defaultColors, defaultThemeSettings, getBlendedColor, getColorRoles, getContrastColor, getHex, getRgb, getThirdColor, getTintTextColor, getTonalColor, getTriadicPalette, isDarkColor, selectThemeAppearance, selectThemeBlur, selectThemeColor, selectThemeHarmony, themeActions, themeReducer, useAutoPaperTheme, useBlur, useBlurModule, useComputedTheme, useNavBarContext, usePaperDefaults, useThemeSettings };
342
+ export { Appbar, type AppearanceIcons, AppearancePicker, type AutoPaperTheme, BlurView, type BlurViewProps, BottomNavigation, Button, Chip, type ChipProps, type ColorHarmony, ColorPicker, type ColorRoles, Dialog, type DialogProps, type ExpoBlurModule, type ExpoNavigationBarModule, FAB, HarmonyPicker, IconButton, type IconButtonProps, Menu, type MenuProps, PalettePicker, type PaletteWeights, type PaperDefaults, Provider, type ProviderProps, type RGB, type ReanimatedModule, SEMANTIC_BASE_COLORS, type SeedColor, type SemanticColorRoles, TextInput, type ThemeAppearance, type ThemeSettings, ThemeSettingsContext, type ThemeSettingsContextType, type ThemeState, type TriadicPalette, createThemeReducer, defaultColors, defaultThemeSettings, getBlendedColor, getColorRoles, getContrastColor, getHex, getRgb, getThirdColor, getTintTextColor, getTonalColor, getTriadicPalette, isDarkColor, selectThemeAppearance, selectThemeBlur, selectThemeColor, selectThemeHarmony, themeActions, themeReducer, useAutoPaperTheme, useBlur, useBlurModule, useComputedTheme, useNavBarContext, usePaperDefaults, useReanimatedModule, useThemeSettings };
package/dist/index.d.ts CHANGED
@@ -127,11 +127,19 @@ type Props$2 = {
127
127
  };
128
128
  declare const ColorPicker: ({ value, onChange, colors, blur: blurProp }: Props$2) => react.JSX.Element;
129
129
 
130
+ interface ReanimatedModule {
131
+ View: ComponentType<{
132
+ children?: ReactNode;
133
+ style?: any;
134
+ testID?: string;
135
+ }>;
136
+ }
130
137
  type DialogProps = Omit<DialogProps$1, 'style'> & {
131
138
  blur?: boolean;
132
139
  style?: StyleProp<ViewStyle>;
140
+ animatedStyle?: unknown;
133
141
  };
134
- declare function DialogComponent({ blur: blurProp, dismissable, dismissableBackButton, visible, onDismiss, children, style, testID }: DialogProps): react__default.JSX.Element | null;
142
+ declare function DialogComponent({ animatedStyle, blur: blurProp, dismissable, dismissableBackButton, visible, onDismiss, children, style, testID }: DialogProps): react__default.JSX.Element | null;
135
143
  declare namespace DialogComponent {
136
144
  var Title: react__default.FC<DialogTitleProps>;
137
145
  var Content: react__default.FC<DialogContentProps>;
@@ -145,9 +153,13 @@ declare function FAB(props: FABProps): react.JSX.Element;
145
153
  type Props$1 = {
146
154
  value: ColorHarmony;
147
155
  onChange: (harmony: ColorHarmony) => void;
148
- blur?: boolean;
156
+ showLabels?: boolean;
157
+ checkedColor?: string;
158
+ uncheckedColor?: string;
159
+ checkedContainerColor?: string;
160
+ uncheckedContainerColor?: string;
149
161
  };
150
- declare const HarmonyPicker: ({ value, onChange, blur: blurProp }: Props$1) => react.JSX.Element;
162
+ declare const HarmonyPicker: ({ value, onChange, showLabels, checkedColor, uncheckedColor, checkedContainerColor, uncheckedContainerColor }: Props$1) => react.JSX.Element;
151
163
 
152
164
  type IconButtonProps = IconButtonProps$1 & {
153
165
  variant?: 'primary' | 'secondary' | 'tertiary' | 'surface';
@@ -170,11 +182,12 @@ type Props = {
170
182
  value: string;
171
183
  onChange: (color: string) => void;
172
184
  harmony?: ColorHarmony;
185
+ onHarmonyChange?: (harmony: ColorHarmony) => void;
173
186
  weights?: PaletteWeights;
174
187
  colors?: SeedColor[];
175
188
  blur?: boolean;
176
189
  };
177
- declare const PalettePicker: ({ value, onChange, harmony, weights, colors, blur: blurProp }: Props) => react.JSX.Element;
190
+ declare const PalettePicker: ({ value, onChange, harmony, onHarmonyChange, weights, colors, blur: blurProp }: Props) => react.JSX.Element;
178
191
 
179
192
  declare const TextInput: react.ForwardRefExoticComponent<Omit<TextInputProps, "ref"> & react.RefAttributes<TextInput$1>>;
180
193
 
@@ -273,6 +286,7 @@ type NavBarContextType = {
273
286
  };
274
287
  declare const useNavBarContext: () => NavBarContextType;
275
288
  declare const useBlurModule: () => ExpoBlurModule | undefined;
289
+ declare const useReanimatedModule: () => ReanimatedModule | undefined;
276
290
  type ProviderProps = {
277
291
  children: ReactNode;
278
292
  defaults?: PaperDefaults;
@@ -284,10 +298,12 @@ type ProviderProps = {
284
298
  onChange?: (settings: ThemeSettings) => void;
285
299
  onNavBarChange?: (color: string, dark: boolean) => void;
286
300
  onReady?: () => void;
301
+ /** 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. */
302
+ reanimated?: ReanimatedModule;
287
303
  statusBarProps?: StatusBarProps;
288
304
  style?: StyleProp<ViewStyle>;
289
305
  };
290
- declare function Provider({ children, defaults, expoBlur, initialValue, navigationBar, onChange, onNavBarChange, onReady, statusBarProps, style }: ProviderProps): react.JSX.Element | null;
306
+ declare function Provider({ children, defaults, expoBlur, initialValue, navigationBar, onChange, onNavBarChange, onReady, reanimated, statusBarProps, style }: ProviderProps): react.JSX.Element | null;
291
307
 
292
308
  declare const useAutoPaperTheme: () => AutoPaperTheme;
293
309
 
@@ -323,4 +339,4 @@ declare const getTonalColor: (color: string, lightness: number) => string;
323
339
 
324
340
  declare const isDarkColor: (value: string) => boolean;
325
341
 
326
- export { Appbar, type AppearanceIcons, AppearancePicker, type AutoPaperTheme, BlurView, type BlurViewProps, BottomNavigation, Button, Chip, type ChipProps, type ColorHarmony, ColorPicker, type ColorRoles, Dialog, type DialogProps, type ExpoBlurModule, type ExpoNavigationBarModule, FAB, HarmonyPicker, IconButton, type IconButtonProps, Menu, type MenuProps, PalettePicker, type PaletteWeights, type PaperDefaults, Provider, type ProviderProps, type RGB, SEMANTIC_BASE_COLORS, type SeedColor, type SemanticColorRoles, TextInput, type ThemeAppearance, type ThemeSettings, ThemeSettingsContext, type ThemeSettingsContextType, type ThemeState, type TriadicPalette, createThemeReducer, defaultColors, defaultThemeSettings, getBlendedColor, getColorRoles, getContrastColor, getHex, getRgb, getThirdColor, getTintTextColor, getTonalColor, getTriadicPalette, isDarkColor, selectThemeAppearance, selectThemeBlur, selectThemeColor, selectThemeHarmony, themeActions, themeReducer, useAutoPaperTheme, useBlur, useBlurModule, useComputedTheme, useNavBarContext, usePaperDefaults, useThemeSettings };
342
+ export { Appbar, type AppearanceIcons, AppearancePicker, type AutoPaperTheme, BlurView, type BlurViewProps, BottomNavigation, Button, Chip, type ChipProps, type ColorHarmony, ColorPicker, type ColorRoles, Dialog, type DialogProps, type ExpoBlurModule, type ExpoNavigationBarModule, FAB, HarmonyPicker, IconButton, type IconButtonProps, Menu, type MenuProps, PalettePicker, type PaletteWeights, type PaperDefaults, Provider, type ProviderProps, type RGB, type ReanimatedModule, SEMANTIC_BASE_COLORS, type SeedColor, type SemanticColorRoles, TextInput, type ThemeAppearance, type ThemeSettings, ThemeSettingsContext, type ThemeSettingsContextType, type ThemeState, type TriadicPalette, createThemeReducer, defaultColors, defaultThemeSettings, getBlendedColor, getColorRoles, getContrastColor, getHex, getRgb, getThirdColor, getTintTextColor, getTonalColor, getTriadicPalette, isDarkColor, selectThemeAppearance, selectThemeBlur, selectThemeColor, selectThemeHarmony, themeActions, themeReducer, useAutoPaperTheme, useBlur, useBlurModule, useComputedTheme, useNavBarContext, usePaperDefaults, useReanimatedModule, useThemeSettings };
package/dist/index.js CHANGED
@@ -72,6 +72,7 @@ __export(index_exports, {
72
72
  useComputedTheme: () => useComputedTheme,
73
73
  useNavBarContext: () => useNavBarContext,
74
74
  usePaperDefaults: () => usePaperDefaults,
75
+ useReanimatedModule: () => useReanimatedModule,
75
76
  useThemeSettings: () => useThemeSettings
76
77
  });
77
78
  module.exports = __toCommonJS(index_exports);
@@ -634,7 +635,9 @@ var NavBarContext = (0, import_react5.createContext)({});
634
635
  var useNavBarContext = () => (0, import_react5.useContext)(NavBarContext);
635
636
  var BlurModuleContext = (0, import_react5.createContext)(void 0);
636
637
  var useBlurModule = () => (0, import_react5.useContext)(BlurModuleContext);
637
- function Provider({ children, defaults, expoBlur, initialValue, navigationBar, onChange, onNavBarChange, onReady, statusBarProps, style }) {
638
+ var ReanimatedModuleContext = (0, import_react5.createContext)(void 0);
639
+ var useReanimatedModule = () => (0, import_react5.useContext)(ReanimatedModuleContext);
640
+ function Provider({ children, defaults, expoBlur, initialValue, navigationBar, onChange, onNavBarChange, onReady, reanimated, statusBarProps, style }) {
638
641
  const [settings, setSettings] = (0, import_react5.useState)(() => ({ ...defaultThemeSettings, ...initialValue }));
639
642
  const set = (0, import_react5.useCallback)((patch) => {
640
643
  setSettings((prev) => ({ ...prev, ...patch }));
@@ -659,7 +662,7 @@ function Provider({ children, defaults, expoBlur, initialValue, navigationBar, o
659
662
  if (!theme) return null;
660
663
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ThemeSettingsContext.Provider, { value: { settings, set }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native_paper4.Provider, { theme, children: [
661
664
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native3.StatusBar, { backgroundColor: theme.colors.background, barStyle: theme.dark ? "light-content" : "dark-content", ...statusBarProps }),
662
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PaperDefaultsContext.Provider, { value: defaults ?? {}, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(NavBarContext.Provider, { value: { navigationBar, onNavBarChange }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(BlurModuleContext.Provider, { value: expoBlur, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native3.View, { style: [styles.flex, { backgroundColor: theme.colors.background }, style], children }) }) }) })
665
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PaperDefaultsContext.Provider, { value: defaults ?? {}, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(NavBarContext.Provider, { value: { navigationBar, onNavBarChange }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(BlurModuleContext.Provider, { value: expoBlur, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ReanimatedModuleContext.Provider, { value: reanimated, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native3.View, { style: [styles.flex, { backgroundColor: theme.colors.background }, style], children }) }) }) }) })
663
666
  ] }) });
664
667
  }
665
668
  var styles = import_react_native3.StyleSheet.create({
@@ -778,8 +781,9 @@ var import_react_native_safe_area_context = require("react-native-safe-area-cont
778
781
  var import_jsx_runtime8 = require("react/jsx-runtime");
779
782
  var ANIMATION_DURATION = 220;
780
783
  var DIALOG_MARGIN = 32;
781
- function DialogComponent({ blur: blurProp, dismissable = true, dismissableBackButton = dismissable, visible, onDismiss, children, style, testID = "dialog" }) {
784
+ function DialogComponent({ animatedStyle, blur: blurProp, dismissable = true, dismissableBackButton = dismissable, visible, onDismiss, children, style, testID = "dialog" }) {
782
785
  const blur = useBlur(blurProp);
786
+ const reanimated = useReanimatedModule();
783
787
  const theme = (0, import_react_native_paper9.useTheme)();
784
788
  const { colors, isV3, roundness } = theme;
785
789
  const { scale } = theme.animation;
@@ -820,7 +824,7 @@ function DialogComponent({ blur: blurProp, dismissable = true, dismissableBackBu
820
824
  const borderRadius = (isV3 ? 7 : 1) * roundness;
821
825
  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: [
822
826
  /* @__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` }),
823
- /* @__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 }) })
827
+ /* @__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: reanimated && animatedStyle ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(reanimated.View, { style: animatedStyle, testID: `${testID}-animated-wrapper`, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(BlurView, { blur, style: [styles2.card, { borderRadius }, style], testID: `${testID}-surface`, children: content }) }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(BlurView, { blur, style: [styles2.card, { borderRadius }, style], testID: `${testID}-surface`, children: content }) })
824
828
  ] }) });
825
829
  }
826
830
  DialogComponent.Title = import_react_native_paper9.Dialog.Title;
@@ -895,26 +899,8 @@ function FAB(props) {
895
899
  }
896
900
 
897
901
  // src/components/HarmonyPicker.tsx
898
- var import_react11 = require("react");
899
- var import_react_native_paper13 = require("react-native-paper");
900
-
901
- // src/components/Menu.tsx
902
- var import_react_native8 = require("react-native");
903
902
  var import_react_native_paper12 = require("react-native-paper");
904
903
  var import_jsx_runtime11 = require("react/jsx-runtime");
905
- var MenuComponent = ({ blur: blurProp, children, ...props }) => {
906
- const blur = useBlur(blurProp);
907
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native_paper12.Menu, { ...props, contentStyle: [styles4.content, props.contentStyle], children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(BlurView, { blur, children }) });
908
- };
909
- var Menu = Object.assign(MenuComponent, {
910
- Item: import_react_native_paper12.Menu.Item
911
- });
912
- var styles4 = import_react_native8.StyleSheet.create({
913
- content: { backgroundColor: "transparent", paddingVertical: 0 }
914
- });
915
-
916
- // src/components/HarmonyPicker.tsx
917
- var import_jsx_runtime12 = require("react/jsx-runtime");
918
904
  var HARMONIES = [
919
905
  { label: "Triadic", value: "triadic", icon: "triangle-outline" },
920
906
  { label: "Split", value: "split-complementary", icon: "arrow-split-vertical" },
@@ -923,46 +909,56 @@ var HARMONIES = [
923
909
  { label: "Complement", value: "complementary", icon: "circle-half-full" },
924
910
  { label: "Dbl Split", value: "double-split", icon: "arrow-expand-horizontal" }
925
911
  ];
926
- var HarmonyPicker = ({ value, onChange, blur: blurProp }) => {
927
- const blur = useBlur(blurProp);
928
- const [open, setOpen] = (0, import_react11.useState)(false);
929
- const selected = HARMONIES.find((h) => h.value === value);
930
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
931
- Menu,
932
- {
933
- blur,
934
- visible: open,
935
- onDismiss: () => setOpen(false),
936
- anchor: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react_native_paper13.Button, { mode: "outlined", icon: selected.icon, onPress: () => setOpen(true), children: selected.label }),
937
- children: HARMONIES.map((h) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
938
- Menu.Item,
939
- {
940
- leadingIcon: h.value === value ? "check" : h.icon,
941
- title: h.label,
942
- onPress: () => {
943
- onChange(h.value);
944
- setOpen(false);
945
- }
946
- },
947
- h.value
948
- ))
949
- }
950
- );
951
- };
912
+ var COMPACT_MIN_WIDTH = 0;
913
+ var HarmonyPicker = ({ value, onChange, showLabels = true, checkedColor, uncheckedColor, checkedContainerColor, uncheckedContainerColor }) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
914
+ import_react_native_paper12.SegmentedButtons,
915
+ {
916
+ value,
917
+ onValueChange: (v) => onChange(v),
918
+ buttons: HARMONIES.map((h) => {
919
+ const checked = h.value === value;
920
+ const containerColor = checked ? checkedContainerColor : uncheckedContainerColor;
921
+ return {
922
+ value: h.value,
923
+ icon: h.icon,
924
+ label: showLabels ? h.label : void 0,
925
+ accessibilityLabel: h.label,
926
+ checkedColor,
927
+ uncheckedColor,
928
+ style: [showLabels ? void 0 : { minWidth: COMPACT_MIN_WIDTH }, containerColor ? { backgroundColor: containerColor } : void 0]
929
+ };
930
+ })
931
+ }
932
+ );
952
933
 
953
934
  // src/components/IconButton.tsx
954
- var import_react_native_paper14 = require("react-native-paper");
955
- var import_jsx_runtime13 = require("react/jsx-runtime");
935
+ var import_react_native_paper13 = require("react-native-paper");
936
+ var import_jsx_runtime12 = require("react/jsx-runtime");
956
937
  function IconButton({ variant, iconColor, containerColor, ...props }) {
957
938
  const defaults = usePaperDefaults();
958
- const { colors } = (0, import_react_native_paper14.useTheme)();
939
+ const { colors } = (0, import_react_native_paper13.useTheme)();
959
940
  const variantContainerColor = variant === "primary" ? colors.primaryContainer : variant === "secondary" ? colors.secondaryContainer : variant === "tertiary" ? colors.tertiaryContainer : variant === "surface" ? colors.surface : void 0;
960
941
  const variantIconColor = variant === "primary" ? colors.onPrimaryContainer : variant === "secondary" ? colors.onSecondaryContainer : variant === "tertiary" ? colors.onTertiaryContainer : variant === "surface" ? colors.onSurface : void 0;
961
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react_native_paper14.IconButton, { ...defaults.IconButton, containerColor: containerColor ?? variantContainerColor, iconColor: iconColor ?? variantIconColor, ...props });
942
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react_native_paper13.IconButton, { ...defaults.IconButton, containerColor: containerColor ?? variantContainerColor, iconColor: iconColor ?? variantIconColor, ...props });
962
943
  }
963
944
 
945
+ // src/components/Menu.tsx
946
+ var import_react_native8 = require("react-native");
947
+ var import_react_native_paper14 = require("react-native-paper");
948
+ var import_jsx_runtime13 = require("react/jsx-runtime");
949
+ var MenuComponent = ({ blur: blurProp, children, ...props }) => {
950
+ const blur = useBlur(blurProp);
951
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react_native_paper14.Menu, { ...props, contentStyle: [styles4.content, props.contentStyle], children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(BlurView, { blur, children }) });
952
+ };
953
+ var Menu = Object.assign(MenuComponent, {
954
+ Item: import_react_native_paper14.Menu.Item
955
+ });
956
+ var styles4 = import_react_native8.StyleSheet.create({
957
+ content: { backgroundColor: "transparent", paddingVertical: 0 }
958
+ });
959
+
964
960
  // src/components/PalettePicker.tsx
965
- var import_react12 = require("react");
961
+ var import_react11 = require("react");
966
962
  var import_react_native9 = require("react-native");
967
963
  var import_react_native_paper15 = require("react-native-paper");
968
964
  var import_jsx_runtime14 = require("react/jsx-runtime");
@@ -980,35 +976,39 @@ var buildWedges = (weights) => {
980
976
  ];
981
977
  };
982
978
  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)) });
983
- var PalettePicker = ({ value, onChange, harmony = "split-complementary", weights, colors = defaultColors, blur: blurProp }) => {
979
+ var PalettePicker = ({ value, onChange, harmony = "split-complementary", onHarmonyChange, weights, colors = defaultColors, blur: blurProp }) => {
984
980
  const blur = useBlur(blurProp);
985
981
  const { colors: themeColors } = (0, import_react_native_paper15.useTheme)();
986
- const [open, setOpen] = (0, import_react12.useState)(false);
982
+ const [open, setOpen] = (0, import_react11.useState)(false);
987
983
  const { onLayout, size } = useSwatchSize(SIZE2);
988
984
  return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
989
985
  /* @__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: [
990
986
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Pie, { palette: getTriadicPalette(value, harmony), weights }),
991
987
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { pointerEvents: "none", style: [styles5.triggerIcon, { borderRadius: SIZE2 / 2 }, getSwatchRing(value, false, themeColors.outlineVariant)], children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper15.Icon, { source: "palette", size: 20, color: getContrastColor(value) }) })
992
988
  ] }) }),
993
- /* @__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)(
994
- import_react_native9.TouchableOpacity,
995
- {
996
- onPress: () => {
997
- onChange(hex);
998
- setOpen(false);
989
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Dialog, { blur, visible: open, onDismiss: () => setOpen(false), children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(Dialog.Content, { children: [
990
+ onHarmonyChange && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { style: styles5.harmonyRow, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(HarmonyPicker, { value: harmony, onChange: onHarmonyChange, showLabels: false }) }),
991
+ /* @__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)(
992
+ import_react_native9.TouchableOpacity,
993
+ {
994
+ onPress: () => {
995
+ onChange(hex);
996
+ setOpen(false);
997
+ },
998
+ style: [styles5.swatch, { height: size, width: size }],
999
+ children: [
1000
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Pie, { palette: getTriadicPalette(hex, harmony), weights, size }),
1001
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { pointerEvents: "none", style: [styles5.ring, { borderRadius: size / 2 }, getSwatchRing(hex, value === hex, themeColors.outlineVariant)], children: value === hex && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper15.Icon, { source: "check", size: 20, color: getContrastColor(hex) }) })
1002
+ ]
999
1003
  },
1000
- style: [styles5.swatch, { height: size, width: size }],
1001
- children: [
1002
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Pie, { palette: getTriadicPalette(hex, harmony), weights, size }),
1003
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { pointerEvents: "none", style: [styles5.ring, { borderRadius: size / 2 }, getSwatchRing(hex, value === hex, themeColors.outlineVariant)], children: value === hex && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper15.Icon, { source: "check", size: 20, color: getContrastColor(hex) }) })
1004
- ]
1005
- },
1006
- hex
1007
- )) }) }) })
1004
+ hex
1005
+ )) })
1006
+ ] }) })
1008
1007
  ] });
1009
1008
  };
1010
1009
  var styles5 = import_react_native9.StyleSheet.create({
1011
1010
  halfMask: { height: "100%", left: "50%", overflow: "hidden", position: "absolute", top: 0, width: "50%" },
1011
+ harmonyRow: { marginBottom: 4 },
1012
1012
  innerRotator: { height: "100%", left: "-100%", overflow: "hidden", position: "absolute", top: 0, width: "200%" },
1013
1013
  pie: { overflow: "hidden" },
1014
1014
  rotator: { height: "100%", left: 0, overflow: "hidden", position: "absolute", top: 0, width: "100%" },
@@ -1022,10 +1022,10 @@ var styles5 = import_react_native9.StyleSheet.create({
1022
1022
  });
1023
1023
 
1024
1024
  // src/components/TextInput.tsx
1025
- var import_react13 = require("react");
1025
+ var import_react12 = require("react");
1026
1026
  var import_react_native_paper16 = require("react-native-paper");
1027
1027
  var import_jsx_runtime15 = require("react/jsx-runtime");
1028
- var TextInput = (0, import_react13.forwardRef)(function TextInput2(props, ref) {
1028
+ var TextInput = (0, import_react12.forwardRef)(function TextInput2(props, ref) {
1029
1029
  const defaults = usePaperDefaults();
1030
1030
  return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native_paper16.TextInput, { ref, ...defaults.TextInput, ...props });
1031
1031
  });
@@ -1072,8 +1072,8 @@ var import_react_native_paper17 = require("react-native-paper");
1072
1072
  var useAutoPaperTheme = () => (0, import_react_native_paper17.useTheme)();
1073
1073
 
1074
1074
  // src/useThemeSettings.ts
1075
- var import_react14 = require("react");
1076
- var useThemeSettings = () => (0, import_react14.useContext)(ThemeSettingsContext);
1075
+ var import_react13 = require("react");
1076
+ var useThemeSettings = () => (0, import_react13.useContext)(ThemeSettingsContext);
1077
1077
 
1078
1078
  // src/utils/getThirdColor.ts
1079
1079
  function rgbToHsl(r, g, b) {
@@ -1200,5 +1200,6 @@ var getTintTextColor = (tintColor, backdropColor, tintOpacity) => {
1200
1200
  useComputedTheme,
1201
1201
  useNavBarContext,
1202
1202
  usePaperDefaults,
1203
+ useReanimatedModule,
1203
1204
  useThemeSettings
1204
1205
  });
package/dist/index.mjs CHANGED
@@ -556,7 +556,9 @@ var NavBarContext = createContext3({});
556
556
  var useNavBarContext = () => useContext3(NavBarContext);
557
557
  var BlurModuleContext = createContext3(void 0);
558
558
  var useBlurModule = () => useContext3(BlurModuleContext);
559
- function Provider({ children, defaults, expoBlur, initialValue, navigationBar, onChange, onNavBarChange, onReady, statusBarProps, style }) {
559
+ var ReanimatedModuleContext = createContext3(void 0);
560
+ var useReanimatedModule = () => useContext3(ReanimatedModuleContext);
561
+ function Provider({ children, defaults, expoBlur, initialValue, navigationBar, onChange, onNavBarChange, onReady, reanimated, statusBarProps, style }) {
560
562
  const [settings, setSettings] = useState2(() => ({ ...defaultThemeSettings, ...initialValue }));
561
563
  const set = useCallback2((patch) => {
562
564
  setSettings((prev) => ({ ...prev, ...patch }));
@@ -581,7 +583,7 @@ function Provider({ children, defaults, expoBlur, initialValue, navigationBar, o
581
583
  if (!theme) return null;
582
584
  return /* @__PURE__ */ jsx3(ThemeSettingsContext.Provider, { value: { settings, set }, children: /* @__PURE__ */ jsxs2(PaperProvider, { theme, children: [
583
585
  /* @__PURE__ */ jsx3(StatusBar2, { backgroundColor: theme.colors.background, barStyle: theme.dark ? "light-content" : "dark-content", ...statusBarProps }),
584
- /* @__PURE__ */ jsx3(PaperDefaultsContext.Provider, { value: defaults ?? {}, children: /* @__PURE__ */ jsx3(NavBarContext.Provider, { value: { navigationBar, onNavBarChange }, children: /* @__PURE__ */ jsx3(BlurModuleContext.Provider, { value: expoBlur, children: /* @__PURE__ */ jsx3(View, { style: [styles.flex, { backgroundColor: theme.colors.background }, style], children }) }) }) })
586
+ /* @__PURE__ */ jsx3(PaperDefaultsContext.Provider, { value: defaults ?? {}, children: /* @__PURE__ */ jsx3(NavBarContext.Provider, { value: { navigationBar, onNavBarChange }, children: /* @__PURE__ */ jsx3(BlurModuleContext.Provider, { value: expoBlur, children: /* @__PURE__ */ jsx3(ReanimatedModuleContext.Provider, { value: reanimated, children: /* @__PURE__ */ jsx3(View, { style: [styles.flex, { backgroundColor: theme.colors.background }, style], children }) }) }) }) })
585
587
  ] }) });
586
588
  }
587
589
  var styles = StyleSheet.create({
@@ -700,8 +702,9 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
700
702
  import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
701
703
  var ANIMATION_DURATION = 220;
702
704
  var DIALOG_MARGIN = 32;
703
- function DialogComponent({ blur: blurProp, dismissable = true, dismissableBackButton = dismissable, visible, onDismiss, children, style, testID = "dialog" }) {
705
+ function DialogComponent({ animatedStyle, blur: blurProp, dismissable = true, dismissableBackButton = dismissable, visible, onDismiss, children, style, testID = "dialog" }) {
704
706
  const blur = useBlur(blurProp);
707
+ const reanimated = useReanimatedModule();
705
708
  const theme = useTheme5();
706
709
  const { colors, isV3, roundness } = theme;
707
710
  const { scale } = theme.animation;
@@ -742,7 +745,7 @@ function DialogComponent({ blur: blurProp, dismissable = true, dismissableBackBu
742
745
  const borderRadius = (isV3 ? 7 : 1) * roundness;
743
746
  return /* @__PURE__ */ jsx8(Portal, { children: /* @__PURE__ */ jsxs4(Animated.View, { accessibilityLiveRegion: "polite", accessibilityViewIsModal: true, pointerEvents: visible ? "box-none" : "none", style: [StyleSheet3.absoluteFill, { opacity }], testID, children: [
744
747
  /* @__PURE__ */ jsx8(Pressable, { accessibilityLabel: "Close modal", accessibilityRole: "button", disabled: !dismissable, onPress: dismissable ? onDismiss : void 0, style: [StyleSheet3.absoluteFill, { backgroundColor: colors.backdrop }], testID: `${testID}-backdrop` }),
745
- /* @__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 }) })
748
+ /* @__PURE__ */ jsx8(View3, { pointerEvents: "box-none", style: [styles2.wrapper, { marginBottom: bottom, marginTop: top, paddingHorizontal: Math.max(left, right, DIALOG_MARGIN) }], testID: `${testID}-wrapper`, children: reanimated && animatedStyle ? /* @__PURE__ */ jsx8(reanimated.View, { style: animatedStyle, testID: `${testID}-animated-wrapper`, children: /* @__PURE__ */ jsx8(BlurView, { blur, style: [styles2.card, { borderRadius }, style], testID: `${testID}-surface`, children: content }) }) : /* @__PURE__ */ jsx8(BlurView, { blur, style: [styles2.card, { borderRadius }, style], testID: `${testID}-surface`, children: content }) })
746
749
  ] }) });
747
750
  }
748
751
  DialogComponent.Title = PaperDialog.Title;
@@ -817,26 +820,8 @@ function FAB(props) {
817
820
  }
818
821
 
819
822
  // src/components/HarmonyPicker.tsx
820
- import { useState as useState6 } from "react";
821
- import { Button as Button2 } from "react-native-paper";
822
-
823
- // src/components/Menu.tsx
824
- import { StyleSheet as StyleSheet5 } from "react-native";
825
- import { Menu as PaperMenu } from "react-native-paper";
823
+ import { SegmentedButtons as SegmentedButtons2 } from "react-native-paper";
826
824
  import { jsx as jsx11 } from "react/jsx-runtime";
827
- var MenuComponent = ({ blur: blurProp, children, ...props }) => {
828
- const blur = useBlur(blurProp);
829
- return /* @__PURE__ */ jsx11(PaperMenu, { ...props, contentStyle: [styles4.content, props.contentStyle], children: /* @__PURE__ */ jsx11(BlurView, { blur, children }) });
830
- };
831
- var Menu = Object.assign(MenuComponent, {
832
- Item: PaperMenu.Item
833
- });
834
- var styles4 = StyleSheet5.create({
835
- content: { backgroundColor: "transparent", paddingVertical: 0 }
836
- });
837
-
838
- // src/components/HarmonyPicker.tsx
839
- import { jsx as jsx12 } from "react/jsx-runtime";
840
825
  var HARMONIES = [
841
826
  { label: "Triadic", value: "triadic", icon: "triangle-outline" },
842
827
  { label: "Split", value: "split-complementary", icon: "arrow-split-vertical" },
@@ -845,46 +830,56 @@ var HARMONIES = [
845
830
  { label: "Complement", value: "complementary", icon: "circle-half-full" },
846
831
  { label: "Dbl Split", value: "double-split", icon: "arrow-expand-horizontal" }
847
832
  ];
848
- var HarmonyPicker = ({ value, onChange, blur: blurProp }) => {
849
- const blur = useBlur(blurProp);
850
- const [open, setOpen] = useState6(false);
851
- const selected = HARMONIES.find((h) => h.value === value);
852
- return /* @__PURE__ */ jsx12(
853
- Menu,
854
- {
855
- blur,
856
- visible: open,
857
- onDismiss: () => setOpen(false),
858
- anchor: /* @__PURE__ */ jsx12(Button2, { mode: "outlined", icon: selected.icon, onPress: () => setOpen(true), children: selected.label }),
859
- children: HARMONIES.map((h) => /* @__PURE__ */ jsx12(
860
- Menu.Item,
861
- {
862
- leadingIcon: h.value === value ? "check" : h.icon,
863
- title: h.label,
864
- onPress: () => {
865
- onChange(h.value);
866
- setOpen(false);
867
- }
868
- },
869
- h.value
870
- ))
871
- }
872
- );
873
- };
833
+ var COMPACT_MIN_WIDTH = 0;
834
+ var HarmonyPicker = ({ value, onChange, showLabels = true, checkedColor, uncheckedColor, checkedContainerColor, uncheckedContainerColor }) => /* @__PURE__ */ jsx11(
835
+ SegmentedButtons2,
836
+ {
837
+ value,
838
+ onValueChange: (v) => onChange(v),
839
+ buttons: HARMONIES.map((h) => {
840
+ const checked = h.value === value;
841
+ const containerColor = checked ? checkedContainerColor : uncheckedContainerColor;
842
+ return {
843
+ value: h.value,
844
+ icon: h.icon,
845
+ label: showLabels ? h.label : void 0,
846
+ accessibilityLabel: h.label,
847
+ checkedColor,
848
+ uncheckedColor,
849
+ style: [showLabels ? void 0 : { minWidth: COMPACT_MIN_WIDTH }, containerColor ? { backgroundColor: containerColor } : void 0]
850
+ };
851
+ })
852
+ }
853
+ );
874
854
 
875
855
  // src/components/IconButton.tsx
876
856
  import { IconButton as PaperIconButton, useTheme as useTheme7 } from "react-native-paper";
877
- import { jsx as jsx13 } from "react/jsx-runtime";
857
+ import { jsx as jsx12 } from "react/jsx-runtime";
878
858
  function IconButton({ variant, iconColor, containerColor, ...props }) {
879
859
  const defaults = usePaperDefaults();
880
860
  const { colors } = useTheme7();
881
861
  const variantContainerColor = variant === "primary" ? colors.primaryContainer : variant === "secondary" ? colors.secondaryContainer : variant === "tertiary" ? colors.tertiaryContainer : variant === "surface" ? colors.surface : void 0;
882
862
  const variantIconColor = variant === "primary" ? colors.onPrimaryContainer : variant === "secondary" ? colors.onSecondaryContainer : variant === "tertiary" ? colors.onTertiaryContainer : variant === "surface" ? colors.onSurface : void 0;
883
- return /* @__PURE__ */ jsx13(PaperIconButton, { ...defaults.IconButton, containerColor: containerColor ?? variantContainerColor, iconColor: iconColor ?? variantIconColor, ...props });
863
+ return /* @__PURE__ */ jsx12(PaperIconButton, { ...defaults.IconButton, containerColor: containerColor ?? variantContainerColor, iconColor: iconColor ?? variantIconColor, ...props });
884
864
  }
885
865
 
866
+ // src/components/Menu.tsx
867
+ import { StyleSheet as StyleSheet5 } from "react-native";
868
+ import { Menu as PaperMenu } from "react-native-paper";
869
+ import { jsx as jsx13 } from "react/jsx-runtime";
870
+ var MenuComponent = ({ blur: blurProp, children, ...props }) => {
871
+ const blur = useBlur(blurProp);
872
+ return /* @__PURE__ */ jsx13(PaperMenu, { ...props, contentStyle: [styles4.content, props.contentStyle], children: /* @__PURE__ */ jsx13(BlurView, { blur, children }) });
873
+ };
874
+ var Menu = Object.assign(MenuComponent, {
875
+ Item: PaperMenu.Item
876
+ });
877
+ var styles4 = StyleSheet5.create({
878
+ content: { backgroundColor: "transparent", paddingVertical: 0 }
879
+ });
880
+
886
881
  // src/components/PalettePicker.tsx
887
- import { useState as useState7 } from "react";
882
+ import { useState as useState6 } from "react";
888
883
  import { StyleSheet as StyleSheet6, TouchableOpacity as TouchableOpacity2, View as View5 } from "react-native";
889
884
  import { Icon as Icon2, TouchableRipple as TouchableRipple2, useTheme as useTheme8 } from "react-native-paper";
890
885
  import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs6 } from "react/jsx-runtime";
@@ -902,35 +897,39 @@ var buildWedges = (weights) => {
902
897
  ];
903
898
  };
904
899
  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)) });
905
- var PalettePicker = ({ value, onChange, harmony = "split-complementary", weights, colors = defaultColors, blur: blurProp }) => {
900
+ var PalettePicker = ({ value, onChange, harmony = "split-complementary", onHarmonyChange, weights, colors = defaultColors, blur: blurProp }) => {
906
901
  const blur = useBlur(blurProp);
907
902
  const { colors: themeColors } = useTheme8();
908
- const [open, setOpen] = useState7(false);
903
+ const [open, setOpen] = useState6(false);
909
904
  const { onLayout, size } = useSwatchSize(SIZE2);
910
905
  return /* @__PURE__ */ jsxs6(Fragment3, { children: [
911
906
  /* @__PURE__ */ jsx14(TouchableRipple2, { onPress: () => setOpen(true), style: styles5.trigger, borderless: true, children: /* @__PURE__ */ jsxs6(View5, { style: styles5.triggerContent, children: [
912
907
  /* @__PURE__ */ jsx14(Pie, { palette: getTriadicPalette(value, harmony), weights }),
913
908
  /* @__PURE__ */ jsx14(View5, { pointerEvents: "none", style: [styles5.triggerIcon, { borderRadius: SIZE2 / 2 }, getSwatchRing(value, false, themeColors.outlineVariant)], children: /* @__PURE__ */ jsx14(Icon2, { source: "palette", size: 20, color: getContrastColor(value) }) })
914
909
  ] }) }),
915
- /* @__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(
916
- TouchableOpacity2,
917
- {
918
- onPress: () => {
919
- onChange(hex);
920
- setOpen(false);
910
+ /* @__PURE__ */ jsx14(Dialog, { blur, visible: open, onDismiss: () => setOpen(false), children: /* @__PURE__ */ jsxs6(Dialog.Content, { children: [
911
+ onHarmonyChange && /* @__PURE__ */ jsx14(View5, { style: styles5.harmonyRow, children: /* @__PURE__ */ jsx14(HarmonyPicker, { value: harmony, onChange: onHarmonyChange, showLabels: false }) }),
912
+ /* @__PURE__ */ jsx14(View5, { style: styles5.swatches, onLayout, children: colors.map(({ value: hex }) => /* @__PURE__ */ jsxs6(
913
+ TouchableOpacity2,
914
+ {
915
+ onPress: () => {
916
+ onChange(hex);
917
+ setOpen(false);
918
+ },
919
+ style: [styles5.swatch, { height: size, width: size }],
920
+ children: [
921
+ /* @__PURE__ */ jsx14(Pie, { palette: getTriadicPalette(hex, harmony), weights, size }),
922
+ /* @__PURE__ */ jsx14(View5, { pointerEvents: "none", style: [styles5.ring, { borderRadius: size / 2 }, getSwatchRing(hex, value === hex, themeColors.outlineVariant)], children: value === hex && /* @__PURE__ */ jsx14(Icon2, { source: "check", size: 20, color: getContrastColor(hex) }) })
923
+ ]
921
924
  },
922
- style: [styles5.swatch, { height: size, width: size }],
923
- children: [
924
- /* @__PURE__ */ jsx14(Pie, { palette: getTriadicPalette(hex, harmony), weights, size }),
925
- /* @__PURE__ */ jsx14(View5, { pointerEvents: "none", style: [styles5.ring, { borderRadius: size / 2 }, getSwatchRing(hex, value === hex, themeColors.outlineVariant)], children: value === hex && /* @__PURE__ */ jsx14(Icon2, { source: "check", size: 20, color: getContrastColor(hex) }) })
926
- ]
927
- },
928
- hex
929
- )) }) }) })
925
+ hex
926
+ )) })
927
+ ] }) })
930
928
  ] });
931
929
  };
932
930
  var styles5 = StyleSheet6.create({
933
931
  halfMask: { height: "100%", left: "50%", overflow: "hidden", position: "absolute", top: 0, width: "50%" },
932
+ harmonyRow: { marginBottom: 4 },
934
933
  innerRotator: { height: "100%", left: "-100%", overflow: "hidden", position: "absolute", top: 0, width: "200%" },
935
934
  pie: { overflow: "hidden" },
936
935
  rotator: { height: "100%", left: 0, overflow: "hidden", position: "absolute", top: 0, width: "100%" },
@@ -1121,5 +1120,6 @@ export {
1121
1120
  useComputedTheme,
1122
1121
  useNavBarContext,
1123
1122
  usePaperDefaults,
1123
+ useReanimatedModule,
1124
1124
  useThemeSettings
1125
1125
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rific/auto-paper",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
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",
@@ -3,6 +3,7 @@ import { StatusBar, type StatusBarProps, type StyleProp, StyleSheet, View, type
3
3
  import { Provider as PaperProvider } from 'react-native-paper'
4
4
 
5
5
  import type { ExpoBlurModule } from './components/BlurView'
6
+ import type { ReanimatedModule } from './components/Dialog'
6
7
  import type { ExpoNavigationBarModule } from './navigation-bar'
7
8
  import { type PaperDefaults, PaperDefaultsContext } from './PaperDefaultsContext'
8
9
  import { defaultThemeSettings, type ThemeSettings, ThemeSettingsContext } from './ThemeSettingsContext'
@@ -20,6 +21,9 @@ export const useNavBarContext = () => useContext(NavBarContext)
20
21
  const BlurModuleContext = createContext<ExpoBlurModule | undefined>(undefined)
21
22
  export const useBlurModule = () => useContext(BlurModuleContext)
22
23
 
24
+ const ReanimatedModuleContext = createContext<ReanimatedModule | undefined>(undefined)
25
+ export const useReanimatedModule = () => useContext(ReanimatedModuleContext)
26
+
23
27
  export type ProviderProps = {
24
28
  children: ReactNode
25
29
  defaults?: PaperDefaults
@@ -31,11 +35,13 @@ export type ProviderProps = {
31
35
  onChange?: (settings: ThemeSettings) => void
32
36
  onNavBarChange?: (color: string, dark: boolean) => void
33
37
  onReady?: () => void
38
+ /** 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. */
39
+ reanimated?: ReanimatedModule
34
40
  statusBarProps?: StatusBarProps
35
41
  style?: StyleProp<ViewStyle>
36
42
  }
37
43
 
38
- export function Provider({ children, defaults, expoBlur, initialValue, navigationBar, onChange, onNavBarChange, onReady, statusBarProps, style }: ProviderProps) {
44
+ export function Provider({ children, defaults, expoBlur, initialValue, navigationBar, onChange, onNavBarChange, onReady, reanimated, statusBarProps, style }: ProviderProps) {
39
45
  const [settings, setSettings] = useState<ThemeSettings>(() => ({ ...defaultThemeSettings, ...initialValue }))
40
46
 
41
47
  const set = useCallback((patch: Partial<ThemeSettings>) => {
@@ -71,7 +77,9 @@ export function Provider({ children, defaults, expoBlur, initialValue, navigatio
71
77
  <PaperDefaultsContext.Provider value={defaults ?? {}}>
72
78
  <NavBarContext.Provider value={{ navigationBar, onNavBarChange }}>
73
79
  <BlurModuleContext.Provider value={expoBlur}>
74
- <View style={[styles.flex, { backgroundColor: theme.colors.background }, style]}>{children}</View>
80
+ <ReanimatedModuleContext.Provider value={reanimated}>
81
+ <View style={[styles.flex, { backgroundColor: theme.colors.background }, style]}>{children}</View>
82
+ </ReanimatedModuleContext.Provider>
75
83
  </BlurModuleContext.Provider>
76
84
  </NavBarContext.Provider>
77
85
  </PaperDefaultsContext.Provider>
@@ -1,16 +1,35 @@
1
- import React, { useEffect, useRef, useState } from 'react'
1
+ import React, { type ComponentType, type ReactNode, useEffect, useRef, useState } from 'react'
2
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
4
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
5
5
 
6
6
  import { useBlur } from '../BlurContext'
7
+ import { useReanimatedModule } from '../ThemeProvider'
7
8
  import { BlurView } from './BlurView'
8
9
 
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.
10
+ // Minimal local mirror of react-native-reanimated's Animated.View — same reasoning as BlurView's
11
+ // ExpoBlurModule: Metro doesn't rewrite a require()-in-try/catch call into its module graph inside
12
+ // an ESM build, so the already-imported peer module is injected via <Provider reanimated={...}>
13
+ // (see ThemeProvider.tsx) instead of required directly. Only `View` is needed here. `style: any`,
14
+ // not a real style type — reanimated's actual AnimatedView only accepts its own branded
15
+ // AnimatedStyle shape, which this package never imports (no reanimated dependency at all), so `any`
16
+ // is the only type that both sides (this interface and the real component passed in) can agree on.
17
+ export interface ReanimatedModule {
18
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
+ View: ComponentType<{ children?: ReactNode; style?: any; testID?: string }>
20
+ }
21
+
11
22
  export type DialogProps = Omit<PaperDialogProps, 'style'> & {
12
23
  blur?: boolean
13
24
  style?: StyleProp<ViewStyle>
25
+ // Applied to the card's outer wrapper instead of the card itself, and only takes effect once a
26
+ // reanimated module has been injected via <Provider reanimated={...}> — otherwise ignored. `style`
27
+ // above is a plain object read once per render, so a fast-changing value (e.g. tracking the
28
+ // keyboard height every frame) driven through it needs a full JS re-render per update to paint at
29
+ // all; wiring the same value through `animatedStyle` (e.g. the result of Reanimated's
30
+ // useAnimatedStyle()) instead paints on the UI thread with no JS round-trip, which is the whole
31
+ // difference between a smooth follow and a laggy snap.
32
+ animatedStyle?: unknown
14
33
  }
15
34
 
16
35
  // Matches react-native-paper's Modal: DEFAULT_DURATION * theme.animation.scale, eased the same way,
@@ -20,8 +39,9 @@ const ANIMATION_DURATION = 220
20
39
  // Minimum breathing room between the card and the screen edge, clamped up from safe-area insets.
21
40
  const DIALOG_MARGIN = 32
22
41
 
23
- function DialogComponent({ blur: blurProp, dismissable = true, dismissableBackButton = dismissable, visible, onDismiss, children, style, testID = 'dialog' }: DialogProps) {
42
+ function DialogComponent({ animatedStyle, blur: blurProp, dismissable = true, dismissableBackButton = dismissable, visible, onDismiss, children, style, testID = 'dialog' }: DialogProps) {
24
43
  const blur = useBlur(blurProp)
44
+ const reanimated = useReanimatedModule()
25
45
  const theme = useTheme()
26
46
  const { colors, isV3, roundness } = theme
27
47
  const { scale } = theme.animation
@@ -75,9 +95,17 @@ function DialogComponent({ blur: blurProp, dismissable = true, dismissableBackBu
75
95
  <Animated.View accessibilityLiveRegion='polite' accessibilityViewIsModal pointerEvents={visible ? 'box-none' : 'none'} style={[StyleSheet.absoluteFill, { opacity }]} testID={testID}>
76
96
  <Pressable accessibilityLabel='Close modal' accessibilityRole='button' disabled={!dismissable} onPress={dismissable ? onDismiss : undefined} style={[StyleSheet.absoluteFill, { backgroundColor: colors.backdrop }]} testID={`${testID}-backdrop`} />
77
97
  <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>
98
+ {reanimated && animatedStyle ? (
99
+ <reanimated.View style={animatedStyle} testID={`${testID}-animated-wrapper`}>
100
+ <BlurView blur={blur} style={[styles.card, { borderRadius }, style]} testID={`${testID}-surface`}>
101
+ {content}
102
+ </BlurView>
103
+ </reanimated.View>
104
+ ) : (
105
+ <BlurView blur={blur} style={[styles.card, { borderRadius }, style]} testID={`${testID}-surface`}>
106
+ {content}
107
+ </BlurView>
108
+ )}
81
109
  </View>
82
110
  </Animated.View>
83
111
  </Portal>
@@ -1,9 +1,6 @@
1
- import { useState } from 'react'
2
- import { Button } from 'react-native-paper'
1
+ import { SegmentedButtons } from 'react-native-paper'
3
2
 
4
- import { useBlur } from '../BlurContext'
5
3
  import type { ColorHarmony } from '../utils/getTriadicPalette'
6
- import { Menu } from './Menu'
7
4
 
8
5
  const HARMONIES: { label: string; value: ColorHarmony; icon: string }[] = [
9
6
  { label: 'Triadic', value: 'triadic', icon: 'triangle-outline' },
@@ -14,39 +11,52 @@ const HARMONIES: { label: string; value: ColorHarmony; icon: string }[] = [
14
11
  { label: 'Dbl Split', value: 'double-split', icon: 'arrow-expand-horizontal' }
15
12
  ]
16
13
 
14
+ // Paper's own SegmentedButtonItem hardcodes `minWidth: 76` per segment (sized for a labeled text
15
+ // button), which 6 of never fit inside PalettePicker's own dialog card (capped at 400, and
16
+ // narrower still once its padding and safe-area margins are subtracted) — they just overflow past
17
+ // its `overflow: hidden` edge instead of shrinking. A fixed smaller guess (say 44) only moves the
18
+ // breakpoint rather than removing it — 6x44 still exceeds the card's content width on an ordinary
19
+ // ~360-375px phone. Zeroing the floor instead lets each segment's own `flex: 1` divide whatever
20
+ // width IS available with nothing left to enforce a minimum, so 6 of them mathematically can never
21
+ // exceed their container at any screen size — same "shrink to fit, never overflow" contract
22
+ // useSwatchGrid.ts's own SWATCH_GRID_COLUMNS already uses for the swatches below this row. Left
23
+ // untouched in labeled mode, where the wider floor is still doing its job of fitting the text.
24
+ const COMPACT_MIN_WIDTH = 0
25
+
17
26
  type Props = {
18
27
  value: ColorHarmony
19
28
  onChange: (harmony: ColorHarmony) => void
20
- blur?: boolean
29
+ // Hides text labels, leaving just the icon per segment — mirrors AppearancePicker's own
30
+ // showLabels, useful here since there are 6 segments to fit instead of 3.
31
+ // accessibilityLabel is always set regardless, so screen readers still announce the full
32
+ // harmony name either way.
33
+ showLabels?: boolean
34
+ // Per-segment fill, e.g. theme.colors.secondary/onSecondary for the checked segment and
35
+ // secondaryContainer/onSecondaryContainer for the rest — matches Paper's own checked/
36
+ // unchecked + container color vocabulary. Omit to fall back to Paper's default segmented-
37
+ // button styling.
38
+ checkedColor?: string
39
+ uncheckedColor?: string
40
+ checkedContainerColor?: string
41
+ uncheckedContainerColor?: string
21
42
  }
22
43
 
23
- export const HarmonyPicker = ({ value, onChange, blur: blurProp }: Props) => {
24
- const blur = useBlur(blurProp)
25
- const [open, setOpen] = useState(false)
26
- const selected = HARMONIES.find((h) => h.value === value)!
27
-
28
- return (
29
- <Menu
30
- blur={blur}
31
- visible={open}
32
- onDismiss={() => setOpen(false)}
33
- anchor={
34
- <Button mode='outlined' icon={selected.icon} onPress={() => setOpen(true)}>
35
- {selected.label}
36
- </Button>
44
+ export const HarmonyPicker = ({ value, onChange, showLabels = true, checkedColor, uncheckedColor, checkedContainerColor, uncheckedContainerColor }: Props) => (
45
+ <SegmentedButtons
46
+ value={value}
47
+ onValueChange={(v) => onChange(v as ColorHarmony)}
48
+ buttons={HARMONIES.map((h) => {
49
+ const checked = h.value === value
50
+ const containerColor = checked ? checkedContainerColor : uncheckedContainerColor
51
+ return {
52
+ value: h.value,
53
+ icon: h.icon,
54
+ label: showLabels ? h.label : undefined,
55
+ accessibilityLabel: h.label,
56
+ checkedColor,
57
+ uncheckedColor,
58
+ style: [showLabels ? undefined : { minWidth: COMPACT_MIN_WIDTH }, containerColor ? { backgroundColor: containerColor } : undefined]
37
59
  }
38
- >
39
- {HARMONIES.map((h) => (
40
- <Menu.Item
41
- key={h.value}
42
- leadingIcon={h.value === value ? 'check' : h.icon}
43
- title={h.label}
44
- onPress={() => {
45
- onChange(h.value)
46
- setOpen(false)
47
- }}
48
- />
49
- ))}
50
- </Menu>
51
- )
52
- }
60
+ })}
61
+ />
62
+ )
@@ -10,6 +10,7 @@ import { getTriadicPalette } from '../utils/getTriadicPalette'
10
10
  import type { SeedColor } from './ColorPicker'
11
11
  import { defaultColors } from './ColorPicker'
12
12
  import { Dialog } from './Dialog'
13
+ import { HarmonyPicker } from './HarmonyPicker'
13
14
 
14
15
  const SIZE = 48
15
16
 
@@ -55,12 +56,18 @@ type Props = {
55
56
  value: string
56
57
  onChange: (color: string) => void
57
58
  harmony?: ColorHarmony
59
+ // Opt-in: passing this turns on a compact, icon-only HarmonyPicker row inside the dialog, above
60
+ // the swatch grid, so every swatch's own Pie preview reshapes live as harmony changes. Omit it
61
+ // and this renders exactly as a plain PalettePicker always has — same trigger, same dialog,
62
+ // harmony fixed at whatever the `harmony` prop says. Gated on the callback itself rather than a
63
+ // separate boolean so there's no way to pass one without the other.
64
+ onHarmonyChange?: (harmony: ColorHarmony) => void
58
65
  weights?: PaletteWeights
59
66
  colors?: SeedColor[]
60
67
  blur?: boolean
61
68
  }
62
69
 
63
- export const PalettePicker = ({ value, onChange, harmony = 'split-complementary', weights, colors = defaultColors, blur: blurProp }: Props) => {
70
+ export const PalettePicker = ({ value, onChange, harmony = 'split-complementary', onHarmonyChange, weights, colors = defaultColors, blur: blurProp }: Props) => {
64
71
  const blur = useBlur(blurProp)
65
72
  const { colors: themeColors } = useTheme()
66
73
  const [open, setOpen] = useState(false)
@@ -79,6 +86,11 @@ export const PalettePicker = ({ value, onChange, harmony = 'split-complementary'
79
86
 
80
87
  <Dialog blur={blur} visible={open} onDismiss={() => setOpen(false)}>
81
88
  <Dialog.Content>
89
+ {onHarmonyChange && (
90
+ <View style={styles.harmonyRow}>
91
+ <HarmonyPicker value={harmony} onChange={onHarmonyChange} showLabels={false} />
92
+ </View>
93
+ )}
82
94
  <View style={styles.swatches} onLayout={onLayout}>
83
95
  {colors.map(({ value: hex }) => (
84
96
  <TouchableOpacity
@@ -104,6 +116,7 @@ export const PalettePicker = ({ value, onChange, harmony = 'split-complementary'
104
116
 
105
117
  const styles = StyleSheet.create({
106
118
  halfMask: { height: '100%', left: '50%', overflow: 'hidden', position: 'absolute', top: 0, width: '50%' },
119
+ harmonyRow: { marginBottom: 4 },
107
120
  innerRotator: { height: '100%', left: '-100%', overflow: 'hidden', position: 'absolute', top: 0, width: '200%' },
108
121
  pie: { overflow: 'hidden' },
109
122
  rotator: { height: '100%', left: 0, overflow: 'hidden', position: 'absolute', top: 0, width: '100%' },
package/src/index.ts CHANGED
@@ -10,7 +10,7 @@ export type { ChipProps } from './components/Chip'
10
10
  export { Chip } from './components/Chip'
11
11
  export type { SeedColor } from './components/ColorPicker'
12
12
  export { ColorPicker, defaultColors } from './components/ColorPicker'
13
- export type { DialogProps } from './components/Dialog'
13
+ export type { DialogProps, ReanimatedModule } from './components/Dialog'
14
14
  export { Dialog } from './components/Dialog'
15
15
  export { FAB } from './components/FAB'
16
16
  export { HarmonyPicker } from './components/HarmonyPicker'
@@ -27,7 +27,7 @@ export { usePaperDefaults } from './PaperDefaultsContext'
27
27
  export type { ThemeState } from './redux/themeSlice'
28
28
  export { createThemeReducer, selectThemeAppearance, selectThemeBlur, selectThemeColor, selectThemeHarmony, themeActions, themeReducer } from './redux/themeSlice'
29
29
  export type { ProviderProps } from './ThemeProvider'
30
- export { Provider, useBlurModule, useNavBarContext } from './ThemeProvider'
30
+ export { Provider, useBlurModule, useNavBarContext, useReanimatedModule } from './ThemeProvider'
31
31
  export type { ThemeSettings, ThemeSettingsContextType } from './ThemeSettingsContext'
32
32
  export { defaultThemeSettings, ThemeSettingsContext } from './ThemeSettingsContext'
33
33
  export type { AutoPaperTheme, SemanticColorRoles, ThemeAppearance } from './useComputedTheme'