@rific/auto-paper 0.12.4 → 0.13.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 +22 -1
- package/dist/index.d.mts +19 -4
- package/dist/index.d.ts +19 -4
- package/dist/index.js +115 -25
- package/dist/index.mjs +104 -17
- package/package.json +6 -1
- package/src/ThemeProvider.tsx +16 -8
- package/src/components/FadingScrollView.tsx +127 -0
- package/src/index.ts +3 -1
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ npm install expo-blur # frosted-glass BlurView
|
|
|
34
34
|
npm install expo-navigation-bar # (>= 56.0.0, SDK 54+) auto-syncs Android nav bar icon style to the theme when BottomNavigation is mounted
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
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
|
+
These integrations are opt-in: pass them to `Provider` (`expoBlur`, `maskedView`, `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, `FadingScrollView` scrolls without its fade on iOS and Android, `BottomNavigation` simply skips nav-bar syncing, and `Dialog` treats `animatedStyle` as a no-op unless a reanimated module is injected.
|
|
38
38
|
|
|
39
39
|
The exported Redux slice has no dependency on `@reduxjs/toolkit`: it works with RTK stores, vanilla Redux, or no Redux at all.
|
|
40
40
|
|
|
@@ -266,6 +266,7 @@ export default function App() {
|
|
|
266
266
|
| `children` | `ReactNode` | |
|
|
267
267
|
| `defaults` | `PaperDefaults` | Prop defaults for wrapper components (see below) |
|
|
268
268
|
| `expoBlur` | `ExpoBlurModule` | Injects `expo-blur` (`import * as ExpoBlur from 'expo-blur'`) so `<BlurView>` renders the real frosted-glass effect. Omit to always render its solid fallback. On Android, expo-blur only blurs when given a `blurTarget` and a `blurMethod` (`'dimezisBlurView'`, or `'dimezisBlurViewSdk31Plus'` on API 31+); without both, `<BlurView>` renders the solid fallback there too, rather than expo-blur's unblurred translucent tint. |
|
|
269
|
+
| `maskedView` | `MaskedViewModule` | Injects `@react-native-masked-view/masked-view` (`import MaskedView from '@react-native-masked-view/masked-view'`) so `<FadingScrollView>` fades its content out above a pinned footer on iOS and Android. Omit to have it scroll without the fade there; web needs nothing. |
|
|
269
270
|
| `fontFamily` | `string` | Applied to every Paper typography variant (`bodyLarge`, `headlineMedium`, `labelSmall`, ...) via `configureFonts`'s flat-config mode - each variant keeps its own MD3 fontSize/lineHeight/weight, only fontFamily changes. Omit to keep each platform's system font. |
|
|
270
271
|
| `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. |
|
|
271
272
|
| `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 |
|
|
@@ -514,6 +515,26 @@ function SettingsDialog({ visible }: { visible: boolean }) {
|
|
|
514
515
|
|
|
515
516
|
`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.
|
|
516
517
|
|
|
518
|
+
### `FadingScrollView`
|
|
519
|
+
|
|
520
|
+
A vertical `ScrollView` whose content fades to transparent over `fadeHeight` (default `SCROLL_FADE_HEIGHT`, 24) at each edge while there is more to scroll past it: at the bottom until the end is reached (the cue that rows continue below a pinned footer, such as a dialog's Done button), and at the top once scrolled down (never at rest, so a header at the top is never dimmed). It masks the content instead of painting over it, so whatever is behind it (a blurred `Dialog`/`BlurView` surface or a solid one, light or dark) stays one continuous surface. A second `BlurView` as a frosted footer leaves a seam instead: a blur nested inside a blurred card can't frost the card's own content.
|
|
521
|
+
|
|
522
|
+
```tsx
|
|
523
|
+
import MaskedView from '@react-native-masked-view/masked-view'
|
|
524
|
+
import { Dialog, FadingScrollView, Provider } from '@rific/auto-paper'
|
|
525
|
+
|
|
526
|
+
<Provider maskedView={MaskedView}>
|
|
527
|
+
<Dialog visible onDismiss={close}>
|
|
528
|
+
<Dialog.ScrollArea>
|
|
529
|
+
<FadingScrollView>{rows}</FadingScrollView>
|
|
530
|
+
</Dialog.ScrollArea>
|
|
531
|
+
<Dialog.Actions>{done}</Dialog.Actions>
|
|
532
|
+
</Dialog>
|
|
533
|
+
</Provider>
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
On iOS and Android it needs `maskedView` injected into `Provider` (and React Native 0.77+, for the native gradient it masks with); without it, it scrolls with no fade. On web it uses a CSS `mask-image` and needs nothing (react-native-masked-view's web build doesn't mask). `style` goes on the outer box, so `flexGrow`/`flexShrink`/`alignSelf` work as on a plain `ScrollView`; every other prop, `onScroll`/`onLayout`/`onContentSizeChange` included, reaches the `ScrollView`.
|
|
537
|
+
|
|
517
538
|
### `IconButton`
|
|
518
539
|
|
|
519
540
|
A thin wrapper around `react-native-paper`'s `IconButton` that adds a `variant` prop for applying theme-derived container and icon colors.
|
package/dist/index.d.mts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import * as react_native_paper_lib_typescript_utils_forwardRef from 'react-native-paper/lib/typescript/utils/forwardRef';
|
|
2
2
|
import * as react_native from 'react-native';
|
|
3
|
-
import { View, ViewProps, StyleProp, ViewStyle, TextInput as TextInput$1, StatusBarProps } from 'react-native';
|
|
3
|
+
import { View, ViewProps, StyleProp, ViewStyle, ScrollViewProps, TextInput as TextInput$1, StatusBarProps } from 'react-native';
|
|
4
4
|
import * as react_native_paper from 'react-native-paper';
|
|
5
5
|
import { AppbarHeaderProps, MD3Theme, BottomNavigationRoute, BottomNavigationProps, BottomNavigation as BottomNavigation$1, ButtonProps, ChipProps as ChipProps$1, DialogProps as DialogProps$1, DialogTitleProps, DialogContentProps, DialogActionsProps, DialogScrollAreaProps, FABProps, IconButtonProps as IconButtonProps$1, MenuProps as MenuProps$1, MenuItemProps, TextInputProps } from 'react-native-paper';
|
|
6
6
|
export { AppbarHeaderProps, BottomNavigationProps, BottomNavigationRoute, ButtonProps, FABProps, ChipProps as PaperChipProps, DialogProps as PaperDialogProps, IconButtonProps as PaperIconButtonProps, MenuProps as PaperMenuProps, TextInputProps } from 'react-native-paper';
|
|
7
7
|
import * as react from 'react';
|
|
8
|
-
import react__default, { RefObject, ReactNode, ComponentType } from 'react';
|
|
8
|
+
import react__default, { RefObject, ReactNode, ComponentType, ReactElement } from 'react';
|
|
9
9
|
import * as _rific_core from '@rific/core';
|
|
10
10
|
import { SettingsContextValue } from '@rific/core';
|
|
11
11
|
|
|
@@ -181,6 +181,18 @@ declare const Dialog: typeof DialogComponent;
|
|
|
181
181
|
|
|
182
182
|
declare function FAB(props: FABProps): react.JSX.Element;
|
|
183
183
|
|
|
184
|
+
type MaskedViewModule = ComponentType<{
|
|
185
|
+
children?: ReactNode;
|
|
186
|
+
maskElement: ReactElement;
|
|
187
|
+
style?: StyleProp<ViewStyle>;
|
|
188
|
+
}>;
|
|
189
|
+
declare const SCROLL_FADE_HEIGHT = 24;
|
|
190
|
+
type FadingScrollViewProps = Omit<ScrollViewProps, 'style'> & {
|
|
191
|
+
style?: StyleProp<ViewStyle>;
|
|
192
|
+
fadeHeight?: number;
|
|
193
|
+
};
|
|
194
|
+
declare function FadingScrollView({ style, fadeHeight, onScroll, onLayout, onContentSizeChange, scrollEventThrottle, children, ...rest }: FadingScrollViewProps): react.JSX.Element;
|
|
195
|
+
|
|
184
196
|
type Props = {
|
|
185
197
|
value: ColorHarmony;
|
|
186
198
|
onChange: (harmony: ColorHarmony) => void;
|
|
@@ -292,6 +304,7 @@ type NavBarContextType = {
|
|
|
292
304
|
declare const useNavBarContext: () => NavBarContextType;
|
|
293
305
|
declare const useBlurModule: () => ExpoBlurModule | undefined;
|
|
294
306
|
declare const useReanimatedModule: () => ReanimatedModule | undefined;
|
|
307
|
+
declare const useMaskedViewModule: () => MaskedViewModule | undefined;
|
|
295
308
|
type ProviderProps = {
|
|
296
309
|
children: ReactNode;
|
|
297
310
|
defaults?: PaperDefaults;
|
|
@@ -304,6 +317,8 @@ type ProviderProps = {
|
|
|
304
317
|
* to keep each platform's system font (MD3's own default). */
|
|
305
318
|
fontFamily?: string;
|
|
306
319
|
initialValue?: Partial<ThemeSettings>;
|
|
320
|
+
/** Injects @react-native-masked-view/masked-view so `<FadingScrollView>` can fade its content out above a pinned footer on iOS and Android. Pass `import MaskedView from '@react-native-masked-view/masked-view'`; omit to have it scroll without the fade there (web never needs it). */
|
|
321
|
+
maskedView?: MaskedViewModule;
|
|
307
322
|
/** Injects expo-navigation-bar so the Android nav bar icon style auto-syncs with the theme. Pass `import * as ExpoNavigationBar from 'expo-navigation-bar'`; omit (and don't pass onNavBarChange) to skip nav bar syncing entirely. */
|
|
308
323
|
navigationBar?: ExpoNavigationBarModule;
|
|
309
324
|
onChange?: (settings: ThemeSettings) => void;
|
|
@@ -314,7 +329,7 @@ type ProviderProps = {
|
|
|
314
329
|
statusBarProps?: StatusBarProps;
|
|
315
330
|
style?: StyleProp<ViewStyle>;
|
|
316
331
|
};
|
|
317
|
-
declare function Provider({ children, defaults, expoBlur, fontFamily, initialValue, navigationBar, onChange, onNavBarChange, onReady, reanimated, statusBarProps, style }: ProviderProps): react.JSX.Element;
|
|
332
|
+
declare function Provider({ children, defaults, expoBlur, fontFamily, initialValue, maskedView, navigationBar, onChange, onNavBarChange, onReady, reanimated, statusBarProps, style }: ProviderProps): react.JSX.Element;
|
|
318
333
|
|
|
319
334
|
declare const useAutoPaperTheme: () => AutoPaperTheme;
|
|
320
335
|
|
|
@@ -393,4 +408,4 @@ declare const isDarkColor: (value: string) => boolean;
|
|
|
393
408
|
|
|
394
409
|
declare const resolveSeedColor: (color: string | TriadicPalette) => string;
|
|
395
410
|
|
|
396
|
-
export { Appbar, type AppearanceIcons, AppearancePicker, AutoAppearancePicker, AutoPalettePicker, 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 ThemeBridgeProps, type ThemeSettings, ThemeSettingsContext, type ThemeSettingsContextType, type ThemeState, type TriadicPalette, createThemeReducer, defaultColors, defaultThemeSettings, getBlendedColor, getColorRoles, getContrastColor, getHex, getRgb, getThirdColor, getTintTextColor, getTonalColor, getTriadicPalette, isDarkColor, resolveSeedColor, selectThemeAppearance, selectThemeBlur, selectThemeColor, selectThemeHarmony, themeActions, themeReducer, useAutoPaperTheme, useBlur, useBlurModule, useComputedTheme, useNavBarContext, usePaperDefaults, useReanimatedModule, useThemeBridgeProps, useThemeSettings };
|
|
411
|
+
export { Appbar, type AppearanceIcons, AppearancePicker, AutoAppearancePicker, AutoPalettePicker, type AutoPaperTheme, BlurView, type BlurViewProps, BottomNavigation, Button, Chip, type ChipProps, type ColorHarmony, ColorPicker, type ColorRoles, Dialog, type DialogProps, type ExpoBlurModule, type ExpoNavigationBarModule, FAB, FadingScrollView, type FadingScrollViewProps, HarmonyPicker, IconButton, type IconButtonProps, type MaskedViewModule, Menu, type MenuProps, PalettePicker, type PaletteWeights, type PaperDefaults, Provider, type ProviderProps, type RGB, type ReanimatedModule, SCROLL_FADE_HEIGHT, SEMANTIC_BASE_COLORS, type SeedColor, type SemanticColorRoles, TextInput, type ThemeAppearance, type ThemeBridgeProps, type ThemeSettings, ThemeSettingsContext, type ThemeSettingsContextType, type ThemeState, type TriadicPalette, createThemeReducer, defaultColors, defaultThemeSettings, getBlendedColor, getColorRoles, getContrastColor, getHex, getRgb, getThirdColor, getTintTextColor, getTonalColor, getTriadicPalette, isDarkColor, resolveSeedColor, selectThemeAppearance, selectThemeBlur, selectThemeColor, selectThemeHarmony, themeActions, themeReducer, useAutoPaperTheme, useBlur, useBlurModule, useComputedTheme, useMaskedViewModule, useNavBarContext, usePaperDefaults, useReanimatedModule, useThemeBridgeProps, useThemeSettings };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import * as react_native_paper_lib_typescript_utils_forwardRef from 'react-native-paper/lib/typescript/utils/forwardRef';
|
|
2
2
|
import * as react_native from 'react-native';
|
|
3
|
-
import { View, ViewProps, StyleProp, ViewStyle, TextInput as TextInput$1, StatusBarProps } from 'react-native';
|
|
3
|
+
import { View, ViewProps, StyleProp, ViewStyle, ScrollViewProps, TextInput as TextInput$1, StatusBarProps } from 'react-native';
|
|
4
4
|
import * as react_native_paper from 'react-native-paper';
|
|
5
5
|
import { AppbarHeaderProps, MD3Theme, BottomNavigationRoute, BottomNavigationProps, BottomNavigation as BottomNavigation$1, ButtonProps, ChipProps as ChipProps$1, DialogProps as DialogProps$1, DialogTitleProps, DialogContentProps, DialogActionsProps, DialogScrollAreaProps, FABProps, IconButtonProps as IconButtonProps$1, MenuProps as MenuProps$1, MenuItemProps, TextInputProps } from 'react-native-paper';
|
|
6
6
|
export { AppbarHeaderProps, BottomNavigationProps, BottomNavigationRoute, ButtonProps, FABProps, ChipProps as PaperChipProps, DialogProps as PaperDialogProps, IconButtonProps as PaperIconButtonProps, MenuProps as PaperMenuProps, TextInputProps } from 'react-native-paper';
|
|
7
7
|
import * as react from 'react';
|
|
8
|
-
import react__default, { RefObject, ReactNode, ComponentType } from 'react';
|
|
8
|
+
import react__default, { RefObject, ReactNode, ComponentType, ReactElement } from 'react';
|
|
9
9
|
import * as _rific_core from '@rific/core';
|
|
10
10
|
import { SettingsContextValue } from '@rific/core';
|
|
11
11
|
|
|
@@ -181,6 +181,18 @@ declare const Dialog: typeof DialogComponent;
|
|
|
181
181
|
|
|
182
182
|
declare function FAB(props: FABProps): react.JSX.Element;
|
|
183
183
|
|
|
184
|
+
type MaskedViewModule = ComponentType<{
|
|
185
|
+
children?: ReactNode;
|
|
186
|
+
maskElement: ReactElement;
|
|
187
|
+
style?: StyleProp<ViewStyle>;
|
|
188
|
+
}>;
|
|
189
|
+
declare const SCROLL_FADE_HEIGHT = 24;
|
|
190
|
+
type FadingScrollViewProps = Omit<ScrollViewProps, 'style'> & {
|
|
191
|
+
style?: StyleProp<ViewStyle>;
|
|
192
|
+
fadeHeight?: number;
|
|
193
|
+
};
|
|
194
|
+
declare function FadingScrollView({ style, fadeHeight, onScroll, onLayout, onContentSizeChange, scrollEventThrottle, children, ...rest }: FadingScrollViewProps): react.JSX.Element;
|
|
195
|
+
|
|
184
196
|
type Props = {
|
|
185
197
|
value: ColorHarmony;
|
|
186
198
|
onChange: (harmony: ColorHarmony) => void;
|
|
@@ -292,6 +304,7 @@ type NavBarContextType = {
|
|
|
292
304
|
declare const useNavBarContext: () => NavBarContextType;
|
|
293
305
|
declare const useBlurModule: () => ExpoBlurModule | undefined;
|
|
294
306
|
declare const useReanimatedModule: () => ReanimatedModule | undefined;
|
|
307
|
+
declare const useMaskedViewModule: () => MaskedViewModule | undefined;
|
|
295
308
|
type ProviderProps = {
|
|
296
309
|
children: ReactNode;
|
|
297
310
|
defaults?: PaperDefaults;
|
|
@@ -304,6 +317,8 @@ type ProviderProps = {
|
|
|
304
317
|
* to keep each platform's system font (MD3's own default). */
|
|
305
318
|
fontFamily?: string;
|
|
306
319
|
initialValue?: Partial<ThemeSettings>;
|
|
320
|
+
/** Injects @react-native-masked-view/masked-view so `<FadingScrollView>` can fade its content out above a pinned footer on iOS and Android. Pass `import MaskedView from '@react-native-masked-view/masked-view'`; omit to have it scroll without the fade there (web never needs it). */
|
|
321
|
+
maskedView?: MaskedViewModule;
|
|
307
322
|
/** Injects expo-navigation-bar so the Android nav bar icon style auto-syncs with the theme. Pass `import * as ExpoNavigationBar from 'expo-navigation-bar'`; omit (and don't pass onNavBarChange) to skip nav bar syncing entirely. */
|
|
308
323
|
navigationBar?: ExpoNavigationBarModule;
|
|
309
324
|
onChange?: (settings: ThemeSettings) => void;
|
|
@@ -314,7 +329,7 @@ type ProviderProps = {
|
|
|
314
329
|
statusBarProps?: StatusBarProps;
|
|
315
330
|
style?: StyleProp<ViewStyle>;
|
|
316
331
|
};
|
|
317
|
-
declare function Provider({ children, defaults, expoBlur, fontFamily, initialValue, navigationBar, onChange, onNavBarChange, onReady, reanimated, statusBarProps, style }: ProviderProps): react.JSX.Element;
|
|
332
|
+
declare function Provider({ children, defaults, expoBlur, fontFamily, initialValue, maskedView, navigationBar, onChange, onNavBarChange, onReady, reanimated, statusBarProps, style }: ProviderProps): react.JSX.Element;
|
|
318
333
|
|
|
319
334
|
declare const useAutoPaperTheme: () => AutoPaperTheme;
|
|
320
335
|
|
|
@@ -393,4 +408,4 @@ declare const isDarkColor: (value: string) => boolean;
|
|
|
393
408
|
|
|
394
409
|
declare const resolveSeedColor: (color: string | TriadicPalette) => string;
|
|
395
410
|
|
|
396
|
-
export { Appbar, type AppearanceIcons, AppearancePicker, AutoAppearancePicker, AutoPalettePicker, 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 ThemeBridgeProps, type ThemeSettings, ThemeSettingsContext, type ThemeSettingsContextType, type ThemeState, type TriadicPalette, createThemeReducer, defaultColors, defaultThemeSettings, getBlendedColor, getColorRoles, getContrastColor, getHex, getRgb, getThirdColor, getTintTextColor, getTonalColor, getTriadicPalette, isDarkColor, resolveSeedColor, selectThemeAppearance, selectThemeBlur, selectThemeColor, selectThemeHarmony, themeActions, themeReducer, useAutoPaperTheme, useBlur, useBlurModule, useComputedTheme, useNavBarContext, usePaperDefaults, useReanimatedModule, useThemeBridgeProps, useThemeSettings };
|
|
411
|
+
export { Appbar, type AppearanceIcons, AppearancePicker, AutoAppearancePicker, AutoPalettePicker, type AutoPaperTheme, BlurView, type BlurViewProps, BottomNavigation, Button, Chip, type ChipProps, type ColorHarmony, ColorPicker, type ColorRoles, Dialog, type DialogProps, type ExpoBlurModule, type ExpoNavigationBarModule, FAB, FadingScrollView, type FadingScrollViewProps, HarmonyPicker, IconButton, type IconButtonProps, type MaskedViewModule, Menu, type MenuProps, PalettePicker, type PaletteWeights, type PaperDefaults, Provider, type ProviderProps, type RGB, type ReanimatedModule, SCROLL_FADE_HEIGHT, SEMANTIC_BASE_COLORS, type SeedColor, type SemanticColorRoles, TextInput, type ThemeAppearance, type ThemeBridgeProps, type ThemeSettings, ThemeSettingsContext, type ThemeSettingsContextType, type ThemeState, type TriadicPalette, createThemeReducer, defaultColors, defaultThemeSettings, getBlendedColor, getColorRoles, getContrastColor, getHex, getRgb, getThirdColor, getTintTextColor, getTonalColor, getTriadicPalette, isDarkColor, resolveSeedColor, selectThemeAppearance, selectThemeBlur, selectThemeColor, selectThemeHarmony, themeActions, themeReducer, useAutoPaperTheme, useBlur, useBlurModule, useComputedTheme, useMaskedViewModule, useNavBarContext, usePaperDefaults, useReanimatedModule, useThemeBridgeProps, useThemeSettings };
|
package/dist/index.js
CHANGED
|
@@ -41,11 +41,13 @@ __export(index_exports, {
|
|
|
41
41
|
ColorPicker: () => ColorPicker,
|
|
42
42
|
Dialog: () => Dialog,
|
|
43
43
|
FAB: () => FAB,
|
|
44
|
+
FadingScrollView: () => FadingScrollView,
|
|
44
45
|
HarmonyPicker: () => HarmonyPicker,
|
|
45
46
|
IconButton: () => IconButton,
|
|
46
47
|
Menu: () => Menu,
|
|
47
48
|
PalettePicker: () => PalettePicker,
|
|
48
49
|
Provider: () => Provider,
|
|
50
|
+
SCROLL_FADE_HEIGHT: () => SCROLL_FADE_HEIGHT,
|
|
49
51
|
SEMANTIC_BASE_COLORS: () => SEMANTIC_BASE_COLORS,
|
|
50
52
|
TextInput: () => TextInput,
|
|
51
53
|
ThemeSettingsContext: () => ThemeSettingsContext,
|
|
@@ -73,6 +75,7 @@ __export(index_exports, {
|
|
|
73
75
|
useBlur: () => useBlur,
|
|
74
76
|
useBlurModule: () => useBlurModule,
|
|
75
77
|
useComputedTheme: () => useComputedTheme,
|
|
78
|
+
useMaskedViewModule: () => useMaskedViewModule,
|
|
76
79
|
useNavBarContext: () => useNavBarContext,
|
|
77
80
|
usePaperDefaults: () => usePaperDefaults,
|
|
78
81
|
useReanimatedModule: () => useReanimatedModule,
|
|
@@ -684,11 +687,13 @@ var BlurModuleContext = (0, import_react5.createContext)(void 0);
|
|
|
684
687
|
var useBlurModule = () => (0, import_react5.useContext)(BlurModuleContext);
|
|
685
688
|
var ReanimatedModuleContext = (0, import_react5.createContext)(void 0);
|
|
686
689
|
var useReanimatedModule = () => (0, import_react5.useContext)(ReanimatedModuleContext);
|
|
687
|
-
|
|
688
|
-
|
|
690
|
+
var MaskedViewModuleContext = (0, import_react5.createContext)(void 0);
|
|
691
|
+
var useMaskedViewModule = () => (0, import_react5.useContext)(MaskedViewModuleContext);
|
|
692
|
+
function Provider({ children, defaults, expoBlur, fontFamily, initialValue, maskedView, navigationBar, onChange, onNavBarChange, onReady, reanimated, statusBarProps, style }) {
|
|
693
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ThemeSettingsProvider, { initialValue, onChange, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ThemeProviderBody, { defaults, expoBlur, fontFamily, maskedView, navigationBar, onNavBarChange, onReady, reanimated, statusBarProps, style, children }) });
|
|
689
694
|
}
|
|
690
695
|
var ThemeSettingsProvider = themeSettingsProvider;
|
|
691
|
-
function ThemeProviderBody({ children, defaults, expoBlur, fontFamily, navigationBar, onNavBarChange, onReady, reanimated, statusBarProps, style }) {
|
|
696
|
+
function ThemeProviderBody({ children, defaults, expoBlur, fontFamily, maskedView, navigationBar, onNavBarChange, onReady, reanimated, statusBarProps, style }) {
|
|
692
697
|
const { settings } = useThemeSettingsInternal();
|
|
693
698
|
const computedTheme = useComputedTheme(settings.appearance, settings.color, settings.harmony);
|
|
694
699
|
const theme = (0, import_react5.useMemo)(() => {
|
|
@@ -706,7 +711,7 @@ function ThemeProviderBody({ children, defaults, expoBlur, fontFamily, navigatio
|
|
|
706
711
|
if (!theme) return null;
|
|
707
712
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native_paper4.Provider, { theme, children: [
|
|
708
713
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native3.StatusBar, { backgroundColor: theme.colors.background, barStyle: theme.dark ? "light-content" : "dark-content", ...statusBarProps }),
|
|
709
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PaperDefaultsContext.Provider, { value: defaults ?? {}, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(NavBarContext.Provider, { value: { navigationBar, onNavBarChange }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(BlurModuleContext.Provider, { value: expoBlur, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ReanimatedModuleContext.Provider, { value: reanimated, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper4.Portal.Host, { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native3.View, { style: [styles.flex, { backgroundColor: theme.colors.background }, style], children }) }) }) }) }) })
|
|
714
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PaperDefaultsContext.Provider, { value: defaults ?? {}, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(NavBarContext.Provider, { value: { navigationBar, onNavBarChange }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(BlurModuleContext.Provider, { value: expoBlur, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ReanimatedModuleContext.Provider, { value: reanimated, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(MaskedViewModuleContext.Provider, { value: maskedView, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper4.Portal.Host, { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native3.View, { style: [styles.flex, { backgroundColor: theme.colors.background }, style], children }) }) }) }) }) }) })
|
|
710
715
|
] });
|
|
711
716
|
}
|
|
712
717
|
var styles = import_react_native3.StyleSheet.create({
|
|
@@ -1038,59 +1043,141 @@ function FAB(props) {
|
|
|
1038
1043
|
return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper13.FAB, { ...defaults.FAB, ...props });
|
|
1039
1044
|
}
|
|
1040
1045
|
|
|
1046
|
+
// src/components/FadingScrollView.tsx
|
|
1047
|
+
var import_react11 = require("react");
|
|
1048
|
+
var import_react_native9 = require("react-native");
|
|
1049
|
+
var import_jsx_runtime15 = require("react/jsx-runtime");
|
|
1050
|
+
var SCROLL_FADE_HEIGHT = 24;
|
|
1051
|
+
function FadingScrollView({ style, fadeHeight = SCROLL_FADE_HEIGHT, onScroll, onLayout, onContentSizeChange, scrollEventThrottle = 16, children, ...rest }) {
|
|
1052
|
+
const MaskedView = useMaskedViewModule();
|
|
1053
|
+
const sizes = (0, import_react11.useRef)({ viewport: 0, content: 0, offset: 0 });
|
|
1054
|
+
const [edges, setEdges] = (0, import_react11.useState)(NO_EDGES);
|
|
1055
|
+
const update = (0, import_react11.useCallback)(() => {
|
|
1056
|
+
const { viewport, content, offset } = sizes.current;
|
|
1057
|
+
const above = offset > 1;
|
|
1058
|
+
const below = content - viewport - offset > 1;
|
|
1059
|
+
setEdges((prev) => prev.above === above && prev.below === below ? prev : { above, below });
|
|
1060
|
+
}, []);
|
|
1061
|
+
const scroll = /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
1062
|
+
import_react_native9.ScrollView,
|
|
1063
|
+
{
|
|
1064
|
+
...rest,
|
|
1065
|
+
style: styles5.fill,
|
|
1066
|
+
scrollEventThrottle,
|
|
1067
|
+
onScroll: (e) => {
|
|
1068
|
+
sizes.current.offset = e.nativeEvent.contentOffset.y;
|
|
1069
|
+
update();
|
|
1070
|
+
onScroll?.(e);
|
|
1071
|
+
},
|
|
1072
|
+
onLayout: (e) => {
|
|
1073
|
+
sizes.current.viewport = e.nativeEvent.layout.height;
|
|
1074
|
+
update();
|
|
1075
|
+
onLayout?.(e);
|
|
1076
|
+
},
|
|
1077
|
+
onContentSizeChange: (width, height) => {
|
|
1078
|
+
sizes.current.content = height;
|
|
1079
|
+
update();
|
|
1080
|
+
onContentSizeChange?.(width, height);
|
|
1081
|
+
},
|
|
1082
|
+
children
|
|
1083
|
+
}
|
|
1084
|
+
);
|
|
1085
|
+
if (import_react_native9.Platform.OS === "web") return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native9.View, { style: [styles5.box, style, (edges.above || edges.below) && webFadeStyle(fadeHeight, edges)], children: scroll });
|
|
1086
|
+
if (!MaskedView) return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native9.View, { style: [styles5.box, style], children: scroll });
|
|
1087
|
+
return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
1088
|
+
MaskedView,
|
|
1089
|
+
{
|
|
1090
|
+
style: [styles5.box, style],
|
|
1091
|
+
maskElement: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_react_native9.View, { style: styles5.fill, children: [
|
|
1092
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native9.View, { style: [{ height: fadeHeight }, edges.above ? FADE_IN_BAND : styles5.opaque] }),
|
|
1093
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native9.View, { style: styles5.fillOpaque }),
|
|
1094
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native9.View, { style: [{ height: fadeHeight }, edges.below ? FADE_OUT_BAND : styles5.opaque] })
|
|
1095
|
+
] }),
|
|
1096
|
+
children: scroll
|
|
1097
|
+
}
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
var FADE_IN_BAND = { experimental_backgroundImage: "linear-gradient(transparent, #000)" };
|
|
1101
|
+
var FADE_OUT_BAND = { experimental_backgroundImage: "linear-gradient(#000, transparent)" };
|
|
1102
|
+
var NO_EDGES = { above: false, below: false };
|
|
1103
|
+
function webFadeStyle(fadeHeight, edges) {
|
|
1104
|
+
const top = edges.above ? `transparent 0, #000 ${fadeHeight}px` : "#000 0";
|
|
1105
|
+
const bottom = edges.below ? `#000 calc(100% - ${fadeHeight}px), transparent 100%` : "#000 100%";
|
|
1106
|
+
const mask = `linear-gradient(to bottom, ${top}, ${bottom})`;
|
|
1107
|
+
return { maskImage: mask, WebkitMaskImage: mask };
|
|
1108
|
+
}
|
|
1109
|
+
var styles5 = import_react_native9.StyleSheet.create({
|
|
1110
|
+
// A plain ScrollView's own flex defaults, so it sizes the same in its parent.
|
|
1111
|
+
box: {
|
|
1112
|
+
flexGrow: 1,
|
|
1113
|
+
flexShrink: 1
|
|
1114
|
+
},
|
|
1115
|
+
fill: {
|
|
1116
|
+
flexGrow: 1,
|
|
1117
|
+
flexShrink: 1
|
|
1118
|
+
},
|
|
1119
|
+
fillOpaque: {
|
|
1120
|
+
backgroundColor: "#000",
|
|
1121
|
+
flexGrow: 1
|
|
1122
|
+
},
|
|
1123
|
+
opaque: {
|
|
1124
|
+
backgroundColor: "#000"
|
|
1125
|
+
}
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1041
1128
|
// src/components/IconButton.tsx
|
|
1042
1129
|
var import_react_native_paper14 = require("react-native-paper");
|
|
1043
|
-
var
|
|
1130
|
+
var import_jsx_runtime16 = require("react/jsx-runtime");
|
|
1044
1131
|
function IconButton({ variant, iconColor, containerColor, ...props }) {
|
|
1045
1132
|
const defaults = usePaperDefaults();
|
|
1046
1133
|
const { colors } = (0, import_react_native_paper14.useTheme)();
|
|
1047
1134
|
const variantContainerColor = variant === "primary" ? colors.primaryContainer : variant === "secondary" ? colors.secondaryContainer : variant === "tertiary" ? colors.tertiaryContainer : variant === "surface" ? colors.surface : void 0;
|
|
1048
1135
|
const variantIconColor = variant === "primary" ? colors.onPrimaryContainer : variant === "secondary" ? colors.onSecondaryContainer : variant === "tertiary" ? colors.onTertiaryContainer : variant === "surface" ? colors.onSurface : void 0;
|
|
1049
|
-
return /* @__PURE__ */ (0,
|
|
1136
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native_paper14.IconButton, { ...defaults.IconButton, containerColor: containerColor ?? variantContainerColor, iconColor: iconColor ?? variantIconColor, ...props });
|
|
1050
1137
|
}
|
|
1051
1138
|
|
|
1052
1139
|
// src/components/Menu.tsx
|
|
1053
|
-
var
|
|
1054
|
-
var
|
|
1140
|
+
var import_react12 = require("react");
|
|
1141
|
+
var import_react_native10 = require("react-native");
|
|
1055
1142
|
var import_react_native_paper15 = require("react-native-paper");
|
|
1056
|
-
var
|
|
1143
|
+
var import_jsx_runtime17 = require("react/jsx-runtime");
|
|
1057
1144
|
var ANIMATION_DURATION2 = 250;
|
|
1058
|
-
var EASING =
|
|
1145
|
+
var EASING = import_react_native10.Easing.bezier(0.4, 0, 0.2, 1);
|
|
1059
1146
|
var DEFAULT_ELEVATION = 2;
|
|
1060
1147
|
var SHADOW_HEIGHT = [0, 1, 2, 4, 6, 8];
|
|
1061
1148
|
var SHADOW_RADIUS = [0, 3, 6, 8, 10, 12];
|
|
1062
|
-
var AnimatedBlurView2 =
|
|
1149
|
+
var AnimatedBlurView2 = import_react_native10.Animated.createAnimatedComponent(BlurView);
|
|
1063
1150
|
var FadingBlurView = ({ blur, children, elevation, theme: themeOverrides, visible }) => {
|
|
1064
1151
|
const { animation, roundness } = (0, import_react_native_paper15.useTheme)(themeOverrides);
|
|
1065
|
-
const [opacity] = (0,
|
|
1066
|
-
const [closing, setClosing] = (0,
|
|
1152
|
+
const [opacity] = (0, import_react12.useState)(() => new import_react_native10.Animated.Value(0));
|
|
1153
|
+
const [closing, setClosing] = (0, import_react12.useState)(false);
|
|
1067
1154
|
if (!visible && !closing) setClosing(true);
|
|
1068
|
-
(0,
|
|
1069
|
-
|
|
1155
|
+
(0, import_react12.useEffect)(() => {
|
|
1156
|
+
import_react_native10.Animated.timing(opacity, { duration: animation.scale * ANIMATION_DURATION2, easing: EASING, toValue: closing ? 0 : 1, useNativeDriver: false }).start();
|
|
1070
1157
|
}, [animation.scale, closing, opacity]);
|
|
1071
1158
|
const shadow = elevation > 0 ? { shadowColor: "#000000", shadowOffset: { height: SHADOW_HEIGHT[elevation], width: 0 }, shadowOpacity: 0.3, shadowRadius: SHADOW_RADIUS[elevation] } : null;
|
|
1072
|
-
return /* @__PURE__ */ (0,
|
|
1159
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(AnimatedBlurView2, { blur, style: [{ borderRadius: roundness, opacity }, shadow], children });
|
|
1073
1160
|
};
|
|
1074
1161
|
var MenuComponent = ({ blur: blurProp, children, ...props }) => {
|
|
1075
1162
|
const blur = useBlur(blurProp);
|
|
1076
|
-
const web =
|
|
1077
|
-
return /* @__PURE__ */ (0,
|
|
1163
|
+
const web = import_react_native10.Platform.OS === "web";
|
|
1164
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native_paper15.Menu, { ...props, contentStyle: [styles6.content, web && styles6.webSurface, props.contentStyle], children: web ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(FadingBlurView, { blur, elevation: props.mode === "flat" ? 0 : props.elevation ?? DEFAULT_ELEVATION, theme: props.theme, visible: props.visible, children }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(BlurView, { blur, children }) });
|
|
1078
1165
|
};
|
|
1079
1166
|
var Menu = Object.assign(MenuComponent, {
|
|
1080
1167
|
Item: import_react_native_paper15.Menu.Item
|
|
1081
1168
|
});
|
|
1082
|
-
var
|
|
1169
|
+
var styles6 = import_react_native10.StyleSheet.create({
|
|
1083
1170
|
content: { backgroundColor: "transparent", paddingVertical: 0 },
|
|
1084
1171
|
webSurface: { opacity: 1, shadowOpacity: 0 }
|
|
1085
1172
|
});
|
|
1086
1173
|
|
|
1087
1174
|
// src/components/TextInput.tsx
|
|
1088
|
-
var
|
|
1175
|
+
var import_react13 = require("react");
|
|
1089
1176
|
var import_react_native_paper16 = require("react-native-paper");
|
|
1090
|
-
var
|
|
1091
|
-
var TextInput = (0,
|
|
1177
|
+
var import_jsx_runtime18 = require("react/jsx-runtime");
|
|
1178
|
+
var TextInput = (0, import_react13.forwardRef)(function TextInput2(props, ref) {
|
|
1092
1179
|
const defaults = usePaperDefaults();
|
|
1093
|
-
return /* @__PURE__ */ (0,
|
|
1180
|
+
return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native_paper16.TextInput, { ref, ...defaults.TextInput, ...props });
|
|
1094
1181
|
});
|
|
1095
1182
|
|
|
1096
1183
|
// src/redux/themeSlice.ts
|
|
@@ -1119,10 +1206,10 @@ var import_react_native_paper17 = require("react-native-paper");
|
|
|
1119
1206
|
var useAutoPaperTheme = () => (0, import_react_native_paper17.useTheme)();
|
|
1120
1207
|
|
|
1121
1208
|
// src/useThemeBridgeProps.ts
|
|
1122
|
-
var
|
|
1209
|
+
var import_react14 = require("react");
|
|
1123
1210
|
function useThemeBridgeProps(props) {
|
|
1124
1211
|
const { initialValue, onChange, onReady } = props;
|
|
1125
|
-
return (0,
|
|
1212
|
+
return (0, import_react14.useMemo)(() => ({ initialValue, onChange, onReady }), [initialValue, onChange, onReady]);
|
|
1126
1213
|
}
|
|
1127
1214
|
|
|
1128
1215
|
// src/utils/getThirdColor.ts
|
|
@@ -1219,11 +1306,13 @@ var getTintTextColor = (tintColor, backdropColor, tintOpacity) => {
|
|
|
1219
1306
|
ColorPicker,
|
|
1220
1307
|
Dialog,
|
|
1221
1308
|
FAB,
|
|
1309
|
+
FadingScrollView,
|
|
1222
1310
|
HarmonyPicker,
|
|
1223
1311
|
IconButton,
|
|
1224
1312
|
Menu,
|
|
1225
1313
|
PalettePicker,
|
|
1226
1314
|
Provider,
|
|
1315
|
+
SCROLL_FADE_HEIGHT,
|
|
1227
1316
|
SEMANTIC_BASE_COLORS,
|
|
1228
1317
|
TextInput,
|
|
1229
1318
|
ThemeSettingsContext,
|
|
@@ -1251,6 +1340,7 @@ var getTintTextColor = (tintColor, backdropColor, tintOpacity) => {
|
|
|
1251
1340
|
useBlur,
|
|
1252
1341
|
useBlurModule,
|
|
1253
1342
|
useComputedTheme,
|
|
1343
|
+
useMaskedViewModule,
|
|
1254
1344
|
useNavBarContext,
|
|
1255
1345
|
usePaperDefaults,
|
|
1256
1346
|
useReanimatedModule,
|
package/dist/index.mjs
CHANGED
|
@@ -601,11 +601,13 @@ var BlurModuleContext = createContext2(void 0);
|
|
|
601
601
|
var useBlurModule = () => useContext3(BlurModuleContext);
|
|
602
602
|
var ReanimatedModuleContext = createContext2(void 0);
|
|
603
603
|
var useReanimatedModule = () => useContext3(ReanimatedModuleContext);
|
|
604
|
-
|
|
605
|
-
|
|
604
|
+
var MaskedViewModuleContext = createContext2(void 0);
|
|
605
|
+
var useMaskedViewModule = () => useContext3(MaskedViewModuleContext);
|
|
606
|
+
function Provider({ children, defaults, expoBlur, fontFamily, initialValue, maskedView, navigationBar, onChange, onNavBarChange, onReady, reanimated, statusBarProps, style }) {
|
|
607
|
+
return /* @__PURE__ */ jsx4(ThemeSettingsProvider, { initialValue, onChange, children: /* @__PURE__ */ jsx4(ThemeProviderBody, { defaults, expoBlur, fontFamily, maskedView, navigationBar, onNavBarChange, onReady, reanimated, statusBarProps, style, children }) });
|
|
606
608
|
}
|
|
607
609
|
var ThemeSettingsProvider = themeSettingsProvider;
|
|
608
|
-
function ThemeProviderBody({ children, defaults, expoBlur, fontFamily, navigationBar, onNavBarChange, onReady, reanimated, statusBarProps, style }) {
|
|
610
|
+
function ThemeProviderBody({ children, defaults, expoBlur, fontFamily, maskedView, navigationBar, onNavBarChange, onReady, reanimated, statusBarProps, style }) {
|
|
609
611
|
const { settings } = useThemeSettingsInternal();
|
|
610
612
|
const computedTheme = useComputedTheme(settings.appearance, settings.color, settings.harmony);
|
|
611
613
|
const theme = useMemo(() => {
|
|
@@ -623,7 +625,7 @@ function ThemeProviderBody({ children, defaults, expoBlur, fontFamily, navigatio
|
|
|
623
625
|
if (!theme) return null;
|
|
624
626
|
return /* @__PURE__ */ jsxs2(PaperProvider, { theme, children: [
|
|
625
627
|
/* @__PURE__ */ jsx4(StatusBar2, { backgroundColor: theme.colors.background, barStyle: theme.dark ? "light-content" : "dark-content", ...statusBarProps }),
|
|
626
|
-
/* @__PURE__ */ jsx4(PaperDefaultsContext.Provider, { value: defaults ?? {}, children: /* @__PURE__ */ jsx4(NavBarContext.Provider, { value: { navigationBar, onNavBarChange }, children: /* @__PURE__ */ jsx4(BlurModuleContext.Provider, { value: expoBlur, children: /* @__PURE__ */ jsx4(ReanimatedModuleContext.Provider, { value: reanimated, children: /* @__PURE__ */ jsx4(Portal.Host, { children: /* @__PURE__ */ jsx4(View, { style: [styles.flex, { backgroundColor: theme.colors.background }, style], children }) }) }) }) }) })
|
|
628
|
+
/* @__PURE__ */ jsx4(PaperDefaultsContext.Provider, { value: defaults ?? {}, children: /* @__PURE__ */ jsx4(NavBarContext.Provider, { value: { navigationBar, onNavBarChange }, children: /* @__PURE__ */ jsx4(BlurModuleContext.Provider, { value: expoBlur, children: /* @__PURE__ */ jsx4(ReanimatedModuleContext.Provider, { value: reanimated, children: /* @__PURE__ */ jsx4(MaskedViewModuleContext.Provider, { value: maskedView, children: /* @__PURE__ */ jsx4(Portal.Host, { children: /* @__PURE__ */ jsx4(View, { style: [styles.flex, { backgroundColor: theme.colors.background }, style], children }) }) }) }) }) }) })
|
|
627
629
|
] });
|
|
628
630
|
}
|
|
629
631
|
var styles = StyleSheet.create({
|
|
@@ -955,22 +957,104 @@ function FAB(props) {
|
|
|
955
957
|
return /* @__PURE__ */ jsx14(PaperFAB, { ...defaults.FAB, ...props });
|
|
956
958
|
}
|
|
957
959
|
|
|
960
|
+
// src/components/FadingScrollView.tsx
|
|
961
|
+
import { useCallback as useCallback2, useRef as useRef2, useState as useState6 } from "react";
|
|
962
|
+
import { Platform as Platform4, ScrollView, StyleSheet as StyleSheet6, View as View6 } from "react-native";
|
|
963
|
+
import { jsx as jsx15, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
964
|
+
var SCROLL_FADE_HEIGHT = 24;
|
|
965
|
+
function FadingScrollView({ style, fadeHeight = SCROLL_FADE_HEIGHT, onScroll, onLayout, onContentSizeChange, scrollEventThrottle = 16, children, ...rest }) {
|
|
966
|
+
const MaskedView = useMaskedViewModule();
|
|
967
|
+
const sizes = useRef2({ viewport: 0, content: 0, offset: 0 });
|
|
968
|
+
const [edges, setEdges] = useState6(NO_EDGES);
|
|
969
|
+
const update = useCallback2(() => {
|
|
970
|
+
const { viewport, content, offset } = sizes.current;
|
|
971
|
+
const above = offset > 1;
|
|
972
|
+
const below = content - viewport - offset > 1;
|
|
973
|
+
setEdges((prev) => prev.above === above && prev.below === below ? prev : { above, below });
|
|
974
|
+
}, []);
|
|
975
|
+
const scroll = /* @__PURE__ */ jsx15(
|
|
976
|
+
ScrollView,
|
|
977
|
+
{
|
|
978
|
+
...rest,
|
|
979
|
+
style: styles5.fill,
|
|
980
|
+
scrollEventThrottle,
|
|
981
|
+
onScroll: (e) => {
|
|
982
|
+
sizes.current.offset = e.nativeEvent.contentOffset.y;
|
|
983
|
+
update();
|
|
984
|
+
onScroll?.(e);
|
|
985
|
+
},
|
|
986
|
+
onLayout: (e) => {
|
|
987
|
+
sizes.current.viewport = e.nativeEvent.layout.height;
|
|
988
|
+
update();
|
|
989
|
+
onLayout?.(e);
|
|
990
|
+
},
|
|
991
|
+
onContentSizeChange: (width, height) => {
|
|
992
|
+
sizes.current.content = height;
|
|
993
|
+
update();
|
|
994
|
+
onContentSizeChange?.(width, height);
|
|
995
|
+
},
|
|
996
|
+
children
|
|
997
|
+
}
|
|
998
|
+
);
|
|
999
|
+
if (Platform4.OS === "web") return /* @__PURE__ */ jsx15(View6, { style: [styles5.box, style, (edges.above || edges.below) && webFadeStyle(fadeHeight, edges)], children: scroll });
|
|
1000
|
+
if (!MaskedView) return /* @__PURE__ */ jsx15(View6, { style: [styles5.box, style], children: scroll });
|
|
1001
|
+
return /* @__PURE__ */ jsx15(
|
|
1002
|
+
MaskedView,
|
|
1003
|
+
{
|
|
1004
|
+
style: [styles5.box, style],
|
|
1005
|
+
maskElement: /* @__PURE__ */ jsxs7(View6, { style: styles5.fill, children: [
|
|
1006
|
+
/* @__PURE__ */ jsx15(View6, { style: [{ height: fadeHeight }, edges.above ? FADE_IN_BAND : styles5.opaque] }),
|
|
1007
|
+
/* @__PURE__ */ jsx15(View6, { style: styles5.fillOpaque }),
|
|
1008
|
+
/* @__PURE__ */ jsx15(View6, { style: [{ height: fadeHeight }, edges.below ? FADE_OUT_BAND : styles5.opaque] })
|
|
1009
|
+
] }),
|
|
1010
|
+
children: scroll
|
|
1011
|
+
}
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
var FADE_IN_BAND = { experimental_backgroundImage: "linear-gradient(transparent, #000)" };
|
|
1015
|
+
var FADE_OUT_BAND = { experimental_backgroundImage: "linear-gradient(#000, transparent)" };
|
|
1016
|
+
var NO_EDGES = { above: false, below: false };
|
|
1017
|
+
function webFadeStyle(fadeHeight, edges) {
|
|
1018
|
+
const top = edges.above ? `transparent 0, #000 ${fadeHeight}px` : "#000 0";
|
|
1019
|
+
const bottom = edges.below ? `#000 calc(100% - ${fadeHeight}px), transparent 100%` : "#000 100%";
|
|
1020
|
+
const mask = `linear-gradient(to bottom, ${top}, ${bottom})`;
|
|
1021
|
+
return { maskImage: mask, WebkitMaskImage: mask };
|
|
1022
|
+
}
|
|
1023
|
+
var styles5 = StyleSheet6.create({
|
|
1024
|
+
// A plain ScrollView's own flex defaults, so it sizes the same in its parent.
|
|
1025
|
+
box: {
|
|
1026
|
+
flexGrow: 1,
|
|
1027
|
+
flexShrink: 1
|
|
1028
|
+
},
|
|
1029
|
+
fill: {
|
|
1030
|
+
flexGrow: 1,
|
|
1031
|
+
flexShrink: 1
|
|
1032
|
+
},
|
|
1033
|
+
fillOpaque: {
|
|
1034
|
+
backgroundColor: "#000",
|
|
1035
|
+
flexGrow: 1
|
|
1036
|
+
},
|
|
1037
|
+
opaque: {
|
|
1038
|
+
backgroundColor: "#000"
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
|
|
958
1042
|
// src/components/IconButton.tsx
|
|
959
1043
|
import { IconButton as PaperIconButton, useTheme as useTheme8 } from "react-native-paper";
|
|
960
|
-
import { jsx as
|
|
1044
|
+
import { jsx as jsx16 } from "react/jsx-runtime";
|
|
961
1045
|
function IconButton({ variant, iconColor, containerColor, ...props }) {
|
|
962
1046
|
const defaults = usePaperDefaults();
|
|
963
1047
|
const { colors } = useTheme8();
|
|
964
1048
|
const variantContainerColor = variant === "primary" ? colors.primaryContainer : variant === "secondary" ? colors.secondaryContainer : variant === "tertiary" ? colors.tertiaryContainer : variant === "surface" ? colors.surface : void 0;
|
|
965
1049
|
const variantIconColor = variant === "primary" ? colors.onPrimaryContainer : variant === "secondary" ? colors.onSecondaryContainer : variant === "tertiary" ? colors.onTertiaryContainer : variant === "surface" ? colors.onSurface : void 0;
|
|
966
|
-
return /* @__PURE__ */
|
|
1050
|
+
return /* @__PURE__ */ jsx16(PaperIconButton, { ...defaults.IconButton, containerColor: containerColor ?? variantContainerColor, iconColor: iconColor ?? variantIconColor, ...props });
|
|
967
1051
|
}
|
|
968
1052
|
|
|
969
1053
|
// src/components/Menu.tsx
|
|
970
|
-
import { useEffect as useEffect5, useState as
|
|
971
|
-
import { Animated as Animated2, Easing as Easing2, Platform as
|
|
1054
|
+
import { useEffect as useEffect5, useState as useState7 } from "react";
|
|
1055
|
+
import { Animated as Animated2, Easing as Easing2, Platform as Platform5, StyleSheet as StyleSheet7 } from "react-native";
|
|
972
1056
|
import { Menu as PaperMenu, useTheme as useTheme9 } from "react-native-paper";
|
|
973
|
-
import { jsx as
|
|
1057
|
+
import { jsx as jsx17 } from "react/jsx-runtime";
|
|
974
1058
|
var ANIMATION_DURATION2 = 250;
|
|
975
1059
|
var EASING = Easing2.bezier(0.4, 0, 0.2, 1);
|
|
976
1060
|
var DEFAULT_ELEVATION = 2;
|
|
@@ -979,24 +1063,24 @@ var SHADOW_RADIUS = [0, 3, 6, 8, 10, 12];
|
|
|
979
1063
|
var AnimatedBlurView2 = Animated2.createAnimatedComponent(BlurView);
|
|
980
1064
|
var FadingBlurView = ({ blur, children, elevation, theme: themeOverrides, visible }) => {
|
|
981
1065
|
const { animation, roundness } = useTheme9(themeOverrides);
|
|
982
|
-
const [opacity] =
|
|
983
|
-
const [closing, setClosing] =
|
|
1066
|
+
const [opacity] = useState7(() => new Animated2.Value(0));
|
|
1067
|
+
const [closing, setClosing] = useState7(false);
|
|
984
1068
|
if (!visible && !closing) setClosing(true);
|
|
985
1069
|
useEffect5(() => {
|
|
986
1070
|
Animated2.timing(opacity, { duration: animation.scale * ANIMATION_DURATION2, easing: EASING, toValue: closing ? 0 : 1, useNativeDriver: false }).start();
|
|
987
1071
|
}, [animation.scale, closing, opacity]);
|
|
988
1072
|
const shadow = elevation > 0 ? { shadowColor: "#000000", shadowOffset: { height: SHADOW_HEIGHT[elevation], width: 0 }, shadowOpacity: 0.3, shadowRadius: SHADOW_RADIUS[elevation] } : null;
|
|
989
|
-
return /* @__PURE__ */
|
|
1073
|
+
return /* @__PURE__ */ jsx17(AnimatedBlurView2, { blur, style: [{ borderRadius: roundness, opacity }, shadow], children });
|
|
990
1074
|
};
|
|
991
1075
|
var MenuComponent = ({ blur: blurProp, children, ...props }) => {
|
|
992
1076
|
const blur = useBlur(blurProp);
|
|
993
|
-
const web =
|
|
994
|
-
return /* @__PURE__ */
|
|
1077
|
+
const web = Platform5.OS === "web";
|
|
1078
|
+
return /* @__PURE__ */ jsx17(PaperMenu, { ...props, contentStyle: [styles6.content, web && styles6.webSurface, props.contentStyle], children: web ? /* @__PURE__ */ jsx17(FadingBlurView, { blur, elevation: props.mode === "flat" ? 0 : props.elevation ?? DEFAULT_ELEVATION, theme: props.theme, visible: props.visible, children }) : /* @__PURE__ */ jsx17(BlurView, { blur, children }) });
|
|
995
1079
|
};
|
|
996
1080
|
var Menu = Object.assign(MenuComponent, {
|
|
997
1081
|
Item: PaperMenu.Item
|
|
998
1082
|
});
|
|
999
|
-
var
|
|
1083
|
+
var styles6 = StyleSheet7.create({
|
|
1000
1084
|
content: { backgroundColor: "transparent", paddingVertical: 0 },
|
|
1001
1085
|
webSurface: { opacity: 1, shadowOpacity: 0 }
|
|
1002
1086
|
});
|
|
@@ -1004,10 +1088,10 @@ var styles5 = StyleSheet6.create({
|
|
|
1004
1088
|
// src/components/TextInput.tsx
|
|
1005
1089
|
import { forwardRef } from "react";
|
|
1006
1090
|
import { TextInput as PaperTextInput } from "react-native-paper";
|
|
1007
|
-
import { jsx as
|
|
1091
|
+
import { jsx as jsx18 } from "react/jsx-runtime";
|
|
1008
1092
|
var TextInput = forwardRef(function TextInput2(props, ref) {
|
|
1009
1093
|
const defaults = usePaperDefaults();
|
|
1010
|
-
return /* @__PURE__ */
|
|
1094
|
+
return /* @__PURE__ */ jsx18(PaperTextInput, { ref, ...defaults.TextInput, ...props });
|
|
1011
1095
|
});
|
|
1012
1096
|
|
|
1013
1097
|
// src/redux/themeSlice.ts
|
|
@@ -1135,11 +1219,13 @@ export {
|
|
|
1135
1219
|
ColorPicker,
|
|
1136
1220
|
Dialog,
|
|
1137
1221
|
FAB,
|
|
1222
|
+
FadingScrollView,
|
|
1138
1223
|
HarmonyPicker,
|
|
1139
1224
|
IconButton,
|
|
1140
1225
|
Menu,
|
|
1141
1226
|
PalettePicker,
|
|
1142
1227
|
Provider,
|
|
1228
|
+
SCROLL_FADE_HEIGHT,
|
|
1143
1229
|
SEMANTIC_BASE_COLORS,
|
|
1144
1230
|
TextInput,
|
|
1145
1231
|
ThemeSettingsContext,
|
|
@@ -1167,6 +1253,7 @@ export {
|
|
|
1167
1253
|
useBlur,
|
|
1168
1254
|
useBlurModule,
|
|
1169
1255
|
useComputedTheme,
|
|
1256
|
+
useMaskedViewModule,
|
|
1170
1257
|
useNavBarContext,
|
|
1171
1258
|
usePaperDefaults,
|
|
1172
1259
|
useReanimatedModule,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rific/auto-paper",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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",
|
|
@@ -71,6 +71,7 @@
|
|
|
71
71
|
"@infinitetoken/eslint-config": "0.2.0",
|
|
72
72
|
"@infinitetoken/jest-config": "^0.4.0",
|
|
73
73
|
"@infinitetoken/tsconfig": "^0.5.0",
|
|
74
|
+
"@react-native-masked-view/masked-view": "^0.3.2",
|
|
74
75
|
"@reduxjs/toolkit": "^2.0.0",
|
|
75
76
|
"@rific/core": "^0.2.0",
|
|
76
77
|
"@testing-library/dom": "^10.4.1",
|
|
@@ -94,6 +95,7 @@
|
|
|
94
95
|
"typescript": "^6.0.3"
|
|
95
96
|
},
|
|
96
97
|
"peerDependencies": {
|
|
98
|
+
"@react-native-masked-view/masked-view": ">=0.3.0",
|
|
97
99
|
"@rific/core": ">=0.1.1",
|
|
98
100
|
"expo-blur": ">=56.0.0",
|
|
99
101
|
"expo-navigation-bar": ">=56.0.0",
|
|
@@ -103,6 +105,9 @@
|
|
|
103
105
|
"react-native-safe-area-context": "*"
|
|
104
106
|
},
|
|
105
107
|
"peerDependenciesMeta": {
|
|
108
|
+
"@react-native-masked-view/masked-view": {
|
|
109
|
+
"optional": true
|
|
110
|
+
},
|
|
106
111
|
"expo-blur": {
|
|
107
112
|
"optional": true
|
|
108
113
|
},
|
package/src/ThemeProvider.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { configureFonts, Portal, Provider as PaperProvider } from 'react-native-
|
|
|
4
4
|
|
|
5
5
|
import type { ExpoBlurModule } from './components/BlurView'
|
|
6
6
|
import type { ReanimatedModule } from './components/Dialog'
|
|
7
|
+
import type { MaskedViewModule } from './components/FadingScrollView'
|
|
7
8
|
import type { ExpoNavigationBarModule } from './navigation-bar'
|
|
8
9
|
import { type PaperDefaults, PaperDefaultsContext } from './PaperDefaultsContext'
|
|
9
10
|
import { type ThemeSettings, themeSettingsProvider, useThemeSettingsInternal } from './ThemeSettingsContext'
|
|
@@ -24,6 +25,9 @@ export const useBlurModule = () => useContext(BlurModuleContext)
|
|
|
24
25
|
const ReanimatedModuleContext = createContext<ReanimatedModule | undefined>(undefined)
|
|
25
26
|
export const useReanimatedModule = () => useContext(ReanimatedModuleContext)
|
|
26
27
|
|
|
28
|
+
const MaskedViewModuleContext = createContext<MaskedViewModule | undefined>(undefined)
|
|
29
|
+
export const useMaskedViewModule = () => useContext(MaskedViewModuleContext)
|
|
30
|
+
|
|
27
31
|
export type ProviderProps = {
|
|
28
32
|
children: ReactNode
|
|
29
33
|
defaults?: PaperDefaults
|
|
@@ -36,6 +40,8 @@ export type ProviderProps = {
|
|
|
36
40
|
* to keep each platform's system font (MD3's own default). */
|
|
37
41
|
fontFamily?: string
|
|
38
42
|
initialValue?: Partial<ThemeSettings>
|
|
43
|
+
/** Injects @react-native-masked-view/masked-view so `<FadingScrollView>` can fade its content out above a pinned footer on iOS and Android. Pass `import MaskedView from '@react-native-masked-view/masked-view'`; omit to have it scroll without the fade there (web never needs it). */
|
|
44
|
+
maskedView?: MaskedViewModule
|
|
39
45
|
/** Injects expo-navigation-bar so the Android nav bar icon style auto-syncs with the theme. Pass `import * as ExpoNavigationBar from 'expo-navigation-bar'`; omit (and don't pass onNavBarChange) to skip nav bar syncing entirely. */
|
|
40
46
|
navigationBar?: ExpoNavigationBarModule
|
|
41
47
|
onChange?: (settings: ThemeSettings) => void
|
|
@@ -53,10 +59,10 @@ export type ProviderProps = {
|
|
|
53
59
|
// wrapper for the settings half; ThemeProviderBody (an actual descendant, reached via
|
|
54
60
|
// useThemeSettingsInternal()) does all the theme-computation/gating work the single combined
|
|
55
61
|
// component used to do directly.
|
|
56
|
-
export function Provider({ children, defaults, expoBlur, fontFamily, initialValue, navigationBar, onChange, onNavBarChange, onReady, reanimated, statusBarProps, style }: ProviderProps) {
|
|
62
|
+
export function Provider({ children, defaults, expoBlur, fontFamily, initialValue, maskedView, navigationBar, onChange, onNavBarChange, onReady, reanimated, statusBarProps, style }: ProviderProps) {
|
|
57
63
|
return (
|
|
58
64
|
<ThemeSettingsProvider initialValue={initialValue} onChange={onChange}>
|
|
59
|
-
<ThemeProviderBody defaults={defaults} expoBlur={expoBlur} fontFamily={fontFamily} navigationBar={navigationBar} onNavBarChange={onNavBarChange} onReady={onReady} reanimated={reanimated} statusBarProps={statusBarProps} style={style}>
|
|
65
|
+
<ThemeProviderBody defaults={defaults} expoBlur={expoBlur} fontFamily={fontFamily} maskedView={maskedView} navigationBar={navigationBar} onNavBarChange={onNavBarChange} onReady={onReady} reanimated={reanimated} statusBarProps={statusBarProps} style={style}>
|
|
60
66
|
{children}
|
|
61
67
|
</ThemeProviderBody>
|
|
62
68
|
</ThemeSettingsProvider>
|
|
@@ -67,7 +73,7 @@ const ThemeSettingsProvider = themeSettingsProvider
|
|
|
67
73
|
|
|
68
74
|
type ThemeProviderBodyProps = Omit<ProviderProps, 'initialValue' | 'onChange'>
|
|
69
75
|
|
|
70
|
-
function ThemeProviderBody({ children, defaults, expoBlur, fontFamily, navigationBar, onNavBarChange, onReady, reanimated, statusBarProps, style }: ThemeProviderBodyProps) {
|
|
76
|
+
function ThemeProviderBody({ children, defaults, expoBlur, fontFamily, maskedView, navigationBar, onNavBarChange, onReady, reanimated, statusBarProps, style }: ThemeProviderBodyProps) {
|
|
71
77
|
const { settings } = useThemeSettingsInternal()
|
|
72
78
|
|
|
73
79
|
const computedTheme = useComputedTheme(settings.appearance, settings.color, settings.harmony)
|
|
@@ -99,17 +105,19 @@ function ThemeProviderBody({ children, defaults, expoBlur, fontFamily, navigatio
|
|
|
99
105
|
<NavBarContext.Provider value={{ navigationBar, onNavBarChange }}>
|
|
100
106
|
<BlurModuleContext.Provider value={expoBlur}>
|
|
101
107
|
<ReanimatedModuleContext.Provider value={reanimated}>
|
|
102
|
-
{
|
|
108
|
+
<MaskedViewModuleContext.Provider value={maskedView}>
|
|
109
|
+
{/* PaperProvider's own PortalHost sits above every context provided here, and Paper's
|
|
103
110
|
Portal only carries its theme and settings across, so anything rendered through a
|
|
104
111
|
Portal (this package's Dialog, Paper's Menu and Tooltip, or anything an app wraps
|
|
105
|
-
in <Portal> itself) would otherwise lose expoBlur, reanimated, defaults and the nav
|
|
112
|
+
in <Portal> itself) would otherwise lose expoBlur, reanimated, maskedView, defaults and the nav
|
|
106
113
|
bar hooks: Dialog/Menu's blur silently fell back to solid. This nested host is the
|
|
107
114
|
nearest one for every Portal under Provider, so their content mounts inside all of
|
|
108
115
|
these contexts instead. It wraps the styled View, not the other way round, so
|
|
109
116
|
portals still cover the full area regardless of `style`. */}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
117
|
+
<Portal.Host>
|
|
118
|
+
<View style={[styles.flex, { backgroundColor: theme.colors.background }, style]}>{children}</View>
|
|
119
|
+
</Portal.Host>
|
|
120
|
+
</MaskedViewModuleContext.Provider>
|
|
113
121
|
</ReanimatedModuleContext.Provider>
|
|
114
122
|
</BlurModuleContext.Provider>
|
|
115
123
|
</NavBarContext.Provider>
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { type ComponentType, type ReactElement, type ReactNode, useCallback, useRef, useState } from 'react'
|
|
2
|
+
import { type LayoutChangeEvent, type NativeScrollEvent, type NativeSyntheticEvent, Platform, ScrollView, type ScrollViewProps, type StyleProp, StyleSheet, View, type ViewStyle } from 'react-native'
|
|
3
|
+
|
|
4
|
+
import { useMaskedViewModule } from '../ThemeProvider'
|
|
5
|
+
|
|
6
|
+
// Minimal local shape of @react-native-masked-view/masked-view's default export, the same way
|
|
7
|
+
// ExpoBlurModule stands in for expo-blur: the optional peer's real types are never imported, so a
|
|
8
|
+
// consumer that never installed it doesn't need them. <Provider maskedView={MaskedView}> hands it in.
|
|
9
|
+
export type MaskedViewModule = ComponentType<{ children?: ReactNode; maskElement: ReactElement; style?: StyleProp<ViewStyle> }>
|
|
10
|
+
|
|
11
|
+
// How far in from each edge a FadingScrollView's content fades out while there is more past that edge.
|
|
12
|
+
export const SCROLL_FADE_HEIGHT = 24
|
|
13
|
+
|
|
14
|
+
export type FadingScrollViewProps = Omit<ScrollViewProps, 'style'> & {
|
|
15
|
+
// Placed on the outer box (the flex item), so a caller's flexGrow/flexShrink/alignSelf work as they
|
|
16
|
+
// did on a plain ScrollView.
|
|
17
|
+
style?: StyleProp<ViewStyle>
|
|
18
|
+
fadeHeight?: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// A vertical ScrollView whose content fades to transparent along an edge while there is more to scroll
|
|
22
|
+
// past it: at the bottom, the cue that rows continue below a pinned footer, like a dialog's Done
|
|
23
|
+
// button; at the top, the same once rows have scrolled up past the edge (Jay, 2026-10-15: one treatment
|
|
24
|
+
// at both ends instead of a hard cut at the top). It masks
|
|
25
|
+
// the content itself rather than painting anything over it, so whatever is behind (a blurred
|
|
26
|
+
// Dialog/BlurView surface, a plain one, light or dark) shows through unchanged: no band, no seam. (A
|
|
27
|
+
// second BlurView as a frosted footer was tried in @tastic/hud first and left a seam on iOS and web: a
|
|
28
|
+
// blur nested in a blurred card can't frost the card's own content.) Once the end is reached the fade
|
|
29
|
+
// goes away, and the top fade only shows once scrolled down, so neither the last row nor the first
|
|
30
|
+
// (a dialog's header, say) is ever dimmed at rest.
|
|
31
|
+
//
|
|
32
|
+
// iOS/Android: a MaskedView (from <Provider maskedView={...}>) whose top and bottom bands are React
|
|
33
|
+
// Native's native linear gradient (experimental_backgroundImage). Without one passed in, content just scrolls
|
|
34
|
+
// with no fade. Web: react-native-masked-view's web build renders only the mask element (it doesn't
|
|
35
|
+
// mask), so the outer box gets a CSS mask-image instead, which react-native-web passes through; no
|
|
36
|
+
// module needed there.
|
|
37
|
+
export function FadingScrollView({ style, fadeHeight = SCROLL_FADE_HEIGHT, onScroll, onLayout, onContentSizeChange, scrollEventThrottle = 16, children, ...rest }: FadingScrollViewProps) {
|
|
38
|
+
const MaskedView = useMaskedViewModule()
|
|
39
|
+
// Sizes live in a ref and only the yes/no answers are state, so scrolling re-renders nothing until a
|
|
40
|
+
// fade has to appear or go.
|
|
41
|
+
const sizes = useRef({ viewport: 0, content: 0, offset: 0 })
|
|
42
|
+
const [edges, setEdges] = useState(NO_EDGES)
|
|
43
|
+
const update = useCallback(() => {
|
|
44
|
+
const { viewport, content, offset } = sizes.current
|
|
45
|
+
const above = offset > 1
|
|
46
|
+
const below = content - viewport - offset > 1
|
|
47
|
+
setEdges((prev) => (prev.above === above && prev.below === below ? prev : { above, below }))
|
|
48
|
+
}, [])
|
|
49
|
+
|
|
50
|
+
const scroll = (
|
|
51
|
+
<ScrollView
|
|
52
|
+
{...rest}
|
|
53
|
+
style={styles.fill}
|
|
54
|
+
scrollEventThrottle={scrollEventThrottle}
|
|
55
|
+
onScroll={(e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
56
|
+
sizes.current.offset = e.nativeEvent.contentOffset.y
|
|
57
|
+
update()
|
|
58
|
+
onScroll?.(e)
|
|
59
|
+
}}
|
|
60
|
+
onLayout={(e: LayoutChangeEvent) => {
|
|
61
|
+
sizes.current.viewport = e.nativeEvent.layout.height
|
|
62
|
+
update()
|
|
63
|
+
onLayout?.(e)
|
|
64
|
+
}}
|
|
65
|
+
onContentSizeChange={(width: number, height: number) => {
|
|
66
|
+
sizes.current.content = height
|
|
67
|
+
update()
|
|
68
|
+
onContentSizeChange?.(width, height)
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
{children}
|
|
72
|
+
</ScrollView>
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if (Platform.OS === 'web') return <View style={[styles.box, style, (edges.above || edges.below) && webFadeStyle(fadeHeight, edges)]}>{scroll}</View>
|
|
76
|
+
if (!MaskedView) return <View style={[styles.box, style]}>{scroll}</View>
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<MaskedView
|
|
80
|
+
style={[styles.box, style]}
|
|
81
|
+
maskElement={
|
|
82
|
+
<View style={styles.fill}>
|
|
83
|
+
<View style={[{ height: fadeHeight }, edges.above ? FADE_IN_BAND : styles.opaque]} />
|
|
84
|
+
<View style={styles.fillOpaque} />
|
|
85
|
+
<View style={[{ height: fadeHeight }, edges.below ? FADE_OUT_BAND : styles.opaque]} />
|
|
86
|
+
</View>
|
|
87
|
+
}
|
|
88
|
+
>
|
|
89
|
+
{scroll}
|
|
90
|
+
</MaskedView>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// React Native's native linear gradient (0.77+, the New Architecture; the fleet's apps are on 0.86), not
|
|
95
|
+
// in this package's own 0.76 dev types, hence the cast. A mask's alpha is what shows: opaque keeps the
|
|
96
|
+
// content, transparent hides it.
|
|
97
|
+
const FADE_IN_BAND = { experimental_backgroundImage: 'linear-gradient(transparent, #000)' } as unknown as ViewStyle
|
|
98
|
+
const FADE_OUT_BAND = { experimental_backgroundImage: 'linear-gradient(#000, transparent)' } as unknown as ViewStyle
|
|
99
|
+
|
|
100
|
+
const NO_EDGES = { above: false, below: false }
|
|
101
|
+
|
|
102
|
+
// mask-image isn't in React Native's style types, but react-native-web passes it to the DOM as is.
|
|
103
|
+
function webFadeStyle(fadeHeight: number, edges: { above: boolean; below: boolean }): ViewStyle {
|
|
104
|
+
const top = edges.above ? `transparent 0, #000 ${fadeHeight}px` : '#000 0'
|
|
105
|
+
const bottom = edges.below ? `#000 calc(100% - ${fadeHeight}px), transparent 100%` : '#000 100%'
|
|
106
|
+
const mask = `linear-gradient(to bottom, ${top}, ${bottom})`
|
|
107
|
+
return { maskImage: mask, WebkitMaskImage: mask } as unknown as ViewStyle
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const styles = StyleSheet.create({
|
|
111
|
+
// A plain ScrollView's own flex defaults, so it sizes the same in its parent.
|
|
112
|
+
box: {
|
|
113
|
+
flexGrow: 1,
|
|
114
|
+
flexShrink: 1
|
|
115
|
+
},
|
|
116
|
+
fill: {
|
|
117
|
+
flexGrow: 1,
|
|
118
|
+
flexShrink: 1
|
|
119
|
+
},
|
|
120
|
+
fillOpaque: {
|
|
121
|
+
backgroundColor: '#000',
|
|
122
|
+
flexGrow: 1
|
|
123
|
+
},
|
|
124
|
+
opaque: {
|
|
125
|
+
backgroundColor: '#000'
|
|
126
|
+
}
|
|
127
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,8 @@ export { ColorPicker, defaultColors } from './components/ColorPicker'
|
|
|
15
15
|
export type { DialogProps, ReanimatedModule } from './components/Dialog'
|
|
16
16
|
export { Dialog } from './components/Dialog'
|
|
17
17
|
export { FAB } from './components/FAB'
|
|
18
|
+
export type { FadingScrollViewProps, MaskedViewModule } from './components/FadingScrollView'
|
|
19
|
+
export { FadingScrollView, SCROLL_FADE_HEIGHT } from './components/FadingScrollView'
|
|
18
20
|
export { HarmonyPicker } from './components/HarmonyPicker'
|
|
19
21
|
export type { IconButtonProps } from './components/IconButton'
|
|
20
22
|
export { IconButton } from './components/IconButton'
|
|
@@ -29,7 +31,7 @@ export { usePaperDefaults } from './PaperDefaultsContext'
|
|
|
29
31
|
export type { ThemeState } from './redux/themeSlice'
|
|
30
32
|
export { createThemeReducer, selectThemeAppearance, selectThemeBlur, selectThemeColor, selectThemeHarmony, themeActions, themeReducer } from './redux/themeSlice'
|
|
31
33
|
export type { ProviderProps } from './ThemeProvider'
|
|
32
|
-
export { Provider, useBlurModule, useNavBarContext, useReanimatedModule } from './ThemeProvider'
|
|
34
|
+
export { Provider, useBlurModule, useMaskedViewModule, useNavBarContext, useReanimatedModule } from './ThemeProvider'
|
|
33
35
|
export type { ThemeSettings, ThemeSettingsContextType } from './ThemeSettingsContext'
|
|
34
36
|
export { defaultThemeSettings, ThemeSettingsContext } from './ThemeSettingsContext'
|
|
35
37
|
export type { AutoPaperTheme, SemanticColorRoles, ThemeAppearance } from './useComputedTheme'
|