@rific/auto-paper 0.12.3 → 0.12.4
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 +32 -9
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +13 -8
- package/dist/index.mjs +13 -8
- package/package.json +1 -1
- package/src/ThemeProvider.tsx +3 -3
- package/src/components/AutoAppearancePicker.tsx +1 -1
- package/src/components/AutoPalettePicker.tsx +2 -2
- package/src/components/BlurView.tsx +24 -10
- package/src/components/Dialog.tsx +3 -3
- package/src/components/HarmonyPicker.tsx +5 -5
- package/src/components/PalettePicker.tsx +1 -1
- package/src/useComputedTheme.ts +1 -1
- package/src/utils/getTonalColor.ts +1 -1
- package/src/utils/resolveSeedColor.ts +3 -3
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`
|
|
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.
|
|
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
|
|
|
@@ -221,7 +221,7 @@ export function Theme({ children }: { children: React.ReactNode }) {
|
|
|
221
221
|
}
|
|
222
222
|
```
|
|
223
223
|
|
|
224
|
-
Keep the scope honest: this only replaces the final `<Provider initialValue={...} onChange={...} onReady={...}>` prop list with `<Provider {...bridgeProps}>`. It doesn't do the `useSelector`/`useDispatch` wiring above it
|
|
224
|
+
Keep the scope honest: this only replaces the final `<Provider initialValue={...} onChange={...} onReady={...}>` prop list with `<Provider {...bridgeProps}>`. It doesn't do the `useSelector`/`useDispatch` wiring above it - that's still yours to write, since `RootState`'s shape is app-local and this package has no opinion on it (or on state management being Redux at all; `useThemeBridgeProps` takes plain values and callbacks, not a `dispatch` function).
|
|
225
225
|
|
|
226
226
|
### With `useComputedTheme`
|
|
227
227
|
|
|
@@ -266,7 +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
|
-
| `fontFamily` | `string` | Applied to every Paper typography variant (`bodyLarge`, `headlineMedium`, `labelSmall`, ...) via `configureFonts`'s flat-config mode
|
|
269
|
+
| `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
270
|
| `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
271
|
| `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 |
|
|
272
272
|
| `onReady` | `() => void` | Called once when the theme first resolves |
|
|
@@ -288,7 +288,7 @@ export default function App() {
|
|
|
288
288
|
|
|
289
289
|
### `useThemeBridgeProps({ initialValue, onChange, onReady })`
|
|
290
290
|
|
|
291
|
-
Reshapes already-resolved values into the exact `Pick<ProviderProps, 'initialValue' | 'onChange' | 'onReady'>` shape `Provider` expects, memoized on those three fields so the returned object is referentially stable across re-renders until an input actually changes. Meant for a consuming app's own Redux-backed "Theme" component
|
|
291
|
+
Reshapes already-resolved values into the exact `Pick<ProviderProps, 'initialValue' | 'onChange' | 'onReady'>` shape `Provider` expects, memoized on those three fields so the returned object is referentially stable across re-renders until an input actually changes. Meant for a consuming app's own Redux-backed "Theme" component - see "With Redux" above - but takes plain values and callbacks rather than a `dispatch` function or anything Redux-specific, so it works with any state management, or none.
|
|
292
292
|
|
|
293
293
|
It only replaces the final prop list handed to `Provider`; it does not do any `useSelector`/`dispatch` wiring itself, and `onReady` is threaded through unchanged.
|
|
294
294
|
|
|
@@ -378,7 +378,7 @@ function SettingsScreen() {
|
|
|
378
378
|
|
|
379
379
|
### `AutoAppearancePicker`
|
|
380
380
|
|
|
381
|
-
`AppearancePicker` wired straight to the `Provider`'s own `ThemeSettings`
|
|
381
|
+
`AppearancePicker` wired straight to the `Provider`'s own `ThemeSettings` - for the common case where the appearance being edited IS the whole app's own theme. Reads/writes `useThemeSettings()` internally, so it takes neither `value` nor `onChange`; every other `AppearancePicker` prop still passes through. Reach for the plain `AppearancePicker` instead when it needs to be controlled by something other than the global theme (a local preview before committing, one-off UI unrelated to `Provider`).
|
|
382
382
|
|
|
383
383
|
```tsx
|
|
384
384
|
import { AutoAppearancePicker } from '@rific/auto-paper'
|
|
@@ -390,7 +390,7 @@ function SettingsScreen() {
|
|
|
390
390
|
|
|
391
391
|
### `HarmonyPicker`
|
|
392
392
|
|
|
393
|
-
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
|
|
393
|
+
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.
|
|
394
394
|
|
|
395
395
|
```tsx
|
|
396
396
|
import { HarmonyPicker, useThemeSettings } from '@rific/auto-paper'
|
|
@@ -405,7 +405,7 @@ function SettingsScreen() {
|
|
|
405
405
|
|---|---|---|
|
|
406
406
|
| `value` | `ColorHarmony` | Currently selected harmony |
|
|
407
407
|
| `onChange` | `(harmony: ColorHarmony) => void` | Called when a segment is tapped |
|
|
408
|
-
| `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
|
|
408
|
+
| `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` |
|
|
409
409
|
| `checkedColor` / `uncheckedColor` | `string` | Icon color for the checked and unchecked segments, matching Paper's own segmented-button vocabulary |
|
|
410
410
|
| `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 |
|
|
411
411
|
|
|
@@ -425,6 +425,29 @@ import { Appbar } from '@rific/auto-paper'
|
|
|
425
425
|
|
|
426
426
|
`Appbar.Content`, `Appbar.Action`, and `Appbar.BackAction` are re-exported directly from `react-native-paper`. Only `Appbar.Header` is wrapped: it applies `AppbarHeader` defaults from context and renders the synced `StatusBar`.
|
|
427
427
|
|
|
428
|
+
### `BlurView`
|
|
429
|
+
|
|
430
|
+
A frosted-glass surface that follows the theme. It blurs when `blur` is true (the default) and `expoBlur` is injected into `Provider` (on Android, only with a `blurTarget` and a `blurMethod`, see the `Provider` table); otherwise it renders a solid `surface` (or `surfaceVariant`) background. `Dialog` and `Menu` use it for their surfaces, passing the ambient `settings.blur`.
|
|
431
|
+
|
|
432
|
+
```tsx
|
|
433
|
+
import { BlurView, useBlur } from '@rific/auto-paper'
|
|
434
|
+
|
|
435
|
+
<BlurView blur={useBlur()} elevation={2} style={{ borderRadius: 16, overflow: 'hidden' }}>
|
|
436
|
+
<Text>Frosted</Text>
|
|
437
|
+
</BlurView>
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
| Prop | Type | Description |
|
|
441
|
+
|---|---|---|
|
|
442
|
+
| `blur` | `boolean` | Whether to blur at all (default `true`). Flipping it while mounted never remounts the children: their state, scroll position and focus survive |
|
|
443
|
+
| `elevation` | `0`-`5` | Tints the surface with `colors.primary` at the MD3 elevation opacity |
|
|
444
|
+
| `variant` | `'primary' \| 'secondary' \| 'tertiary' \| 'error' \| 'surface' \| 'surfaceVariant'` | Theme-derived tint (or, for `surfaceVariant`, the base surface color) |
|
|
445
|
+
| `tintColor` / `tintOpacity` | `string` / `number` | An explicit tint overlay, overriding `variant` and `elevation` |
|
|
446
|
+
| `intensity`, `blurMethod`, `blurTarget`, `blurReductionFactor` | | Passed to expo-blur's `BlurView`, as in expo-blur |
|
|
447
|
+
| ...all `ViewProps` | | Applied to the outer `View` |
|
|
448
|
+
|
|
449
|
+
Whether or not it blurs, `BlurView` is always the same outer `View`, holding the blurTint surface layer (at `settings.blurTint`), the tint, and the children. On iOS and Android the blur is an absolutely filled expo-blur layer behind them, the same structure as expo-blur's own native `BlurView`, so (as with expo-blur) rounded corners only clip the blur when the style also sets `overflow: 'hidden'`. On web the outer `View` is itself the CSS `backdrop-filter` element, styled the way expo-blur's web `BlurView` styles its own, so an opacity animated on `BlurView` (as `Dialog` and `Menu` do on web) fades the blur correctly instead of cutting it off.
|
|
450
|
+
|
|
428
451
|
### `BottomNavigation`
|
|
429
452
|
|
|
430
453
|
A thin wrapper around `react-native-paper`'s `BottomNavigation` that keeps the Android system navigation bar icons readable: when `navigationBar` is injected into `Provider`, the icon style automatically follows the theme's darkness (edge-to-edge safe, no background color calls). Pass `onNavBarChange` to the `Provider` to take full control instead; with neither, it silently no-ops.
|
|
@@ -514,7 +537,7 @@ import { IconButton } from '@rific/auto-paper'
|
|
|
514
537
|
|
|
515
538
|
### `PalettePicker`
|
|
516
539
|
|
|
517
|
-
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
|
|
540
|
+
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.
|
|
518
541
|
|
|
519
542
|
```tsx
|
|
520
543
|
import { PalettePicker, useThemeSettings } from '@rific/auto-paper'
|
|
@@ -537,7 +560,7 @@ function SettingsScreen() {
|
|
|
537
560
|
|
|
538
561
|
### `AutoPalettePicker`
|
|
539
562
|
|
|
540
|
-
`PalettePicker` wired straight to the `Provider`'s own `ThemeSettings`
|
|
563
|
+
`PalettePicker` wired straight to the `Provider`'s own `ThemeSettings` - both color AND harmony - for the common case where the palette being edited IS the whole app's own theme. Reads/writes `useThemeSettings()` internally (narrowing `ThemeSettings.color`'s `string | TriadicPalette` down to a plain string via `resolveSeedColor`), so it takes none of `value`/`onChange`/`harmony`/`onHarmonyChange`; `weights`/`colors`/`blur` still pass through. Reach for the plain `PalettePicker` instead when it needs to be controlled by something other than the global theme.
|
|
541
564
|
|
|
542
565
|
```tsx
|
|
543
566
|
import { AutoPalettePicker } from '@rific/auto-paper'
|
package/dist/index.d.mts
CHANGED
|
@@ -133,7 +133,7 @@ type BlurViewProps = ExpoBlurViewProps & {
|
|
|
133
133
|
tintOpacity?: number;
|
|
134
134
|
variant?: 'primary' | 'secondary' | 'tertiary' | 'error' | 'surface' | 'surfaceVariant';
|
|
135
135
|
};
|
|
136
|
-
declare const BlurView: ({ blur, children, elevation, style, tintColor, tintOpacity, variant, ...props }: BlurViewProps) => react.JSX.Element;
|
|
136
|
+
declare const BlurView: ({ blur, blurMethod, blurReductionFactor, blurTarget, children, elevation, experimentalBlurMethod, intensity, style, tint: _tint, tintColor, tintOpacity, variant, ...props }: BlurViewProps) => react.JSX.Element;
|
|
137
137
|
|
|
138
138
|
declare function BottomNavigationComponent<Route extends BottomNavigationRoute>(props: BottomNavigationProps<Route>): react__default.JSX.Element;
|
|
139
139
|
declare namespace BottomNavigationComponent {
|
|
@@ -298,7 +298,7 @@ type ProviderProps = {
|
|
|
298
298
|
/** Injects expo-blur for `<BlurView>`. Pass `import * as ExpoBlur from 'expo-blur'`; omit to always render BlurView's solid fallback. */
|
|
299
299
|
expoBlur?: ExpoBlurModule;
|
|
300
300
|
/** Applied to every Paper typography variant (bodyLarge, headlineMedium, labelSmall, ...) via
|
|
301
|
-
* `configureFonts`'s flat-config mode
|
|
301
|
+
* `configureFonts`'s flat-config mode - each variant keeps its own MD3 fontSize/lineHeight/weight,
|
|
302
302
|
* only fontFamily changes. The one-line way to reskin an app's whole text to a custom typeface,
|
|
303
303
|
* instead of passing `fontFamily` to every individual Text/TextInput/Button across the app. Omit
|
|
304
304
|
* to keep each platform's system font (MD3's own default). */
|
package/dist/index.d.ts
CHANGED
|
@@ -133,7 +133,7 @@ type BlurViewProps = ExpoBlurViewProps & {
|
|
|
133
133
|
tintOpacity?: number;
|
|
134
134
|
variant?: 'primary' | 'secondary' | 'tertiary' | 'error' | 'surface' | 'surfaceVariant';
|
|
135
135
|
};
|
|
136
|
-
declare const BlurView: ({ blur, children, elevation, style, tintColor, tintOpacity, variant, ...props }: BlurViewProps) => react.JSX.Element;
|
|
136
|
+
declare const BlurView: ({ blur, blurMethod, blurReductionFactor, blurTarget, children, elevation, experimentalBlurMethod, intensity, style, tint: _tint, tintColor, tintOpacity, variant, ...props }: BlurViewProps) => react.JSX.Element;
|
|
137
137
|
|
|
138
138
|
declare function BottomNavigationComponent<Route extends BottomNavigationRoute>(props: BottomNavigationProps<Route>): react__default.JSX.Element;
|
|
139
139
|
declare namespace BottomNavigationComponent {
|
|
@@ -298,7 +298,7 @@ type ProviderProps = {
|
|
|
298
298
|
/** Injects expo-blur for `<BlurView>`. Pass `import * as ExpoBlur from 'expo-blur'`; omit to always render BlurView's solid fallback. */
|
|
299
299
|
expoBlur?: ExpoBlurModule;
|
|
300
300
|
/** Applied to every Paper typography variant (bodyLarge, headlineMedium, labelSmall, ...) via
|
|
301
|
-
* `configureFonts`'s flat-config mode
|
|
301
|
+
* `configureFonts`'s flat-config mode - each variant keeps its own MD3 fontSize/lineHeight/weight,
|
|
302
302
|
* only fontFamily changes. The one-line way to reskin an app's whole text to a custom typeface,
|
|
303
303
|
* instead of passing `fontFamily` to every individual Text/TextInput/Button across the app. Omit
|
|
304
304
|
* to keep each platform's system font (MD3's own default). */
|
package/dist/index.js
CHANGED
|
@@ -725,7 +725,12 @@ var blursOnAndroid = ({ blurMethod, blurTarget, experimentalBlurMethod }) => {
|
|
|
725
725
|
};
|
|
726
726
|
var ELEVATION_OPACITY = { 0: 0, 1: 0.05, 2: 0.08, 3: 0.11, 4: 0.12, 5: 0.14 };
|
|
727
727
|
var VARIANT_TINT_OPACITY = { light: 0.12, dark: 0.18 };
|
|
728
|
-
var
|
|
728
|
+
var getWebBlurStyle = (intensity, dark) => {
|
|
729
|
+
const amount = Math.min(intensity, 100);
|
|
730
|
+
const filter = `saturate(180%) blur(${amount * 0.2}px)`;
|
|
731
|
+
return { backdropFilter: filter, backgroundColor: `rgba(${dark ? "25,25,25" : "249,249,249"},${amount / 100 * 0.78})`, WebkitBackdropFilter: filter };
|
|
732
|
+
};
|
|
733
|
+
var BlurView = ({ blur = true, blurMethod, blurReductionFactor, blurTarget, children, elevation, experimentalBlurMethod, intensity, style, tint: _tint, tintColor, tintOpacity, variant = "surface", ...props }) => {
|
|
729
734
|
const { dark, colors } = (0, import_react_native_paper5.useTheme)();
|
|
730
735
|
const { settings } = (0, import_react6.useContext)(ThemeSettingsContext);
|
|
731
736
|
const expoBlur = useBlurModule();
|
|
@@ -734,13 +739,13 @@ var BlurView = ({ blur = true, children, elevation, style, tintColor, tintOpacit
|
|
|
734
739
|
const resolvedTintColor = tintColor ?? variantColor ?? (elevation !== void 0 ? colors.primary : void 0);
|
|
735
740
|
const resolvedTintOpacity = tintOpacity ?? (tintColor ? dark ? 0.24 : 0.16 : variantColor ? dark ? VARIANT_TINT_OPACITY.dark : VARIANT_TINT_OPACITY.light : elevation !== void 0 ? ELEVATION_OPACITY[elevation] : 0);
|
|
736
741
|
const tint = resolvedTintColor && resolvedTintOpacity > 0 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native4.View, { style: [import_react_native4.StyleSheet.absoluteFill, { backgroundColor: resolvedTintColor, opacity: resolvedTintOpacity }] }) : null;
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
742
|
+
const blurProps = { blurMethod, blurReductionFactor, blurTarget, experimentalBlurMethod, intensity };
|
|
743
|
+
const blurs = blur && expoBlur !== void 0 && (import_react_native4.Platform.OS !== "android" || blursOnAndroid(blurProps));
|
|
744
|
+
const web = import_react_native4.Platform.OS === "web";
|
|
745
|
+
const containerStyle = !blurs ? [{ backgroundColor: surfaceColor }, style] : web ? [style, getWebBlurStyle(intensity ?? 50, dark)] : style;
|
|
746
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native4.View, { ...props, style: containerStyle, children: [
|
|
747
|
+
blurs && !web ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(expoBlur.BlurView, { ...blurProps, style: import_react_native4.StyleSheet.absoluteFill, tint: dark ? "dark" : "light" }) : null,
|
|
748
|
+
blurs ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native4.View, { style: [import_react_native4.StyleSheet.absoluteFill, { backgroundColor: surfaceColor, opacity: settings.blurTint }] }) : null,
|
|
744
749
|
tint,
|
|
745
750
|
children
|
|
746
751
|
] });
|
package/dist/index.mjs
CHANGED
|
@@ -642,7 +642,12 @@ var blursOnAndroid = ({ blurMethod, blurTarget, experimentalBlurMethod }) => {
|
|
|
642
642
|
};
|
|
643
643
|
var ELEVATION_OPACITY = { 0: 0, 1: 0.05, 2: 0.08, 3: 0.11, 4: 0.12, 5: 0.14 };
|
|
644
644
|
var VARIANT_TINT_OPACITY = { light: 0.12, dark: 0.18 };
|
|
645
|
-
var
|
|
645
|
+
var getWebBlurStyle = (intensity, dark) => {
|
|
646
|
+
const amount = Math.min(intensity, 100);
|
|
647
|
+
const filter = `saturate(180%) blur(${amount * 0.2}px)`;
|
|
648
|
+
return { backdropFilter: filter, backgroundColor: `rgba(${dark ? "25,25,25" : "249,249,249"},${amount / 100 * 0.78})`, WebkitBackdropFilter: filter };
|
|
649
|
+
};
|
|
650
|
+
var BlurView = ({ blur = true, blurMethod, blurReductionFactor, blurTarget, children, elevation, experimentalBlurMethod, intensity, style, tint: _tint, tintColor, tintOpacity, variant = "surface", ...props }) => {
|
|
646
651
|
const { dark, colors } = useTheme2();
|
|
647
652
|
const { settings } = useContext4(ThemeSettingsContext);
|
|
648
653
|
const expoBlur = useBlurModule();
|
|
@@ -651,13 +656,13 @@ var BlurView = ({ blur = true, children, elevation, style, tintColor, tintOpacit
|
|
|
651
656
|
const resolvedTintColor = tintColor ?? variantColor ?? (elevation !== void 0 ? colors.primary : void 0);
|
|
652
657
|
const resolvedTintOpacity = tintOpacity ?? (tintColor ? dark ? 0.24 : 0.16 : variantColor ? dark ? VARIANT_TINT_OPACITY.dark : VARIANT_TINT_OPACITY.light : elevation !== void 0 ? ELEVATION_OPACITY[elevation] : 0);
|
|
653
658
|
const tint = resolvedTintColor && resolvedTintOpacity > 0 ? /* @__PURE__ */ jsx5(View2, { style: [StyleSheet2.absoluteFill, { backgroundColor: resolvedTintColor, opacity: resolvedTintOpacity }] }) : null;
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
659
|
+
const blurProps = { blurMethod, blurReductionFactor, blurTarget, experimentalBlurMethod, intensity };
|
|
660
|
+
const blurs = blur && expoBlur !== void 0 && (Platform.OS !== "android" || blursOnAndroid(blurProps));
|
|
661
|
+
const web = Platform.OS === "web";
|
|
662
|
+
const containerStyle = !blurs ? [{ backgroundColor: surfaceColor }, style] : web ? [style, getWebBlurStyle(intensity ?? 50, dark)] : style;
|
|
663
|
+
return /* @__PURE__ */ jsxs3(View2, { ...props, style: containerStyle, children: [
|
|
664
|
+
blurs && !web ? /* @__PURE__ */ jsx5(expoBlur.BlurView, { ...blurProps, style: StyleSheet2.absoluteFill, tint: dark ? "dark" : "light" }) : null,
|
|
665
|
+
blurs ? /* @__PURE__ */ jsx5(View2, { style: [StyleSheet2.absoluteFill, { backgroundColor: surfaceColor, opacity: settings.blurTint }] }) : null,
|
|
661
666
|
tint,
|
|
662
667
|
children
|
|
663
668
|
] });
|
package/package.json
CHANGED
package/src/ThemeProvider.tsx
CHANGED
|
@@ -30,7 +30,7 @@ export type ProviderProps = {
|
|
|
30
30
|
/** Injects expo-blur for `<BlurView>`. Pass `import * as ExpoBlur from 'expo-blur'`; omit to always render BlurView's solid fallback. */
|
|
31
31
|
expoBlur?: ExpoBlurModule
|
|
32
32
|
/** Applied to every Paper typography variant (bodyLarge, headlineMedium, labelSmall, ...) via
|
|
33
|
-
* `configureFonts`'s flat-config mode
|
|
33
|
+
* `configureFonts`'s flat-config mode - each variant keeps its own MD3 fontSize/lineHeight/weight,
|
|
34
34
|
* only fontFamily changes. The one-line way to reskin an app's whole text to a custom typeface,
|
|
35
35
|
* instead of passing `fontFamily` to every individual Text/TextInput/Button across the app. Omit
|
|
36
36
|
* to keep each platform's system font (MD3's own default). */
|
|
@@ -48,7 +48,7 @@ export type ProviderProps = {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
// createSettingsContext's generated Provider only exposes `settings` to descendants (via its
|
|
51
|
-
// generated useSettings())
|
|
51
|
+
// generated useSettings()) - it can't itself also compute the theme/gate mounting on it being
|
|
52
52
|
// ready, since that logic needs `settings` in scope. Provider (below) is just this thin outer
|
|
53
53
|
// wrapper for the settings half; ThemeProviderBody (an actual descendant, reached via
|
|
54
54
|
// useThemeSettingsInternal()) does all the theme-computation/gating work the single combined
|
|
@@ -71,7 +71,7 @@ function ThemeProviderBody({ children, defaults, expoBlur, fontFamily, navigatio
|
|
|
71
71
|
const { settings } = useThemeSettingsInternal()
|
|
72
72
|
|
|
73
73
|
const computedTheme = useComputedTheme(settings.appearance, settings.color, settings.harmony)
|
|
74
|
-
// Layered on top of useComputedTheme's own output rather than folded into it
|
|
74
|
+
// Layered on top of useComputedTheme's own output rather than folded into it - that hook only
|
|
75
75
|
// ever recomputes for a color/appearance/harmony change, and fontFamily is a completely
|
|
76
76
|
// independent axis; merging it there would mean either re-running the whole color computation on
|
|
77
77
|
// every fontFamily change (wasteful) or threading fontFamily through as a fourth computeTheme
|
|
@@ -8,7 +8,7 @@ type Props = {
|
|
|
8
8
|
|
|
9
9
|
// AppearancePicker wired straight to the Provider's own ThemeSettings, for the common case where
|
|
10
10
|
// the appearance being edited IS the whole app's own theme. Takes neither `value` nor `onChange`
|
|
11
|
-
//
|
|
11
|
+
// - reads/writes useThemeSettings() itself, so there's nothing for a consumer to wire up. Reach
|
|
12
12
|
// for the plain AppearancePicker instead when it needs to be controlled by something other than
|
|
13
13
|
// the global theme (a local preview before committing, one-off UI unrelated to Provider).
|
|
14
14
|
export const AutoAppearancePicker = (props: Props) => {
|
|
@@ -9,9 +9,9 @@ type Props = {
|
|
|
9
9
|
blur?: boolean
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
// PalettePicker wired straight to the Provider's own ThemeSettings
|
|
12
|
+
// PalettePicker wired straight to the Provider's own ThemeSettings - both color AND harmony -
|
|
13
13
|
// for the common case where the palette being edited IS the whole app's own theme. Takes none of
|
|
14
|
-
// `value`/`onChange`/`harmony`/`onHarmonyChange`
|
|
14
|
+
// `value`/`onChange`/`harmony`/`onHarmonyChange` - reads/writes useThemeSettings() itself
|
|
15
15
|
// (resolveSeedColor narrows ThemeSettings.color's `string | TriadicPalette` down to the plain
|
|
16
16
|
// string PalettePicker's own `value` wants). Reach for the plain PalettePicker instead when it
|
|
17
17
|
// needs to be controlled by something other than the global theme.
|
|
@@ -58,7 +58,16 @@ export type BlurViewProps = ExpoBlurViewProps & {
|
|
|
58
58
|
variant?: 'primary' | 'secondary' | 'tertiary' | 'error' | 'surface' | 'surfaceVariant'
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
// The style expo-blur's web BlurView puts on its View (getBlurStyle in its src/BlurView.web.tsx, with
|
|
62
|
+
// getBackgroundColor's 'dark' and 'light' tints, the only two this component uses): a CSS
|
|
63
|
+
// backdrop-filter plus a translucent tint. On web BlurView applies it to its own View instead.
|
|
64
|
+
const getWebBlurStyle = (intensity: number, dark: boolean) => {
|
|
65
|
+
const amount = Math.min(intensity, 100)
|
|
66
|
+
const filter = `saturate(180%) blur(${amount * 0.2}px)`
|
|
67
|
+
return { backdropFilter: filter, backgroundColor: `rgba(${dark ? '25,25,25' : '249,249,249'},${(amount / 100) * 0.78})`, WebkitBackdropFilter: filter } as ViewStyle
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const BlurView = ({ blur = true, blurMethod, blurReductionFactor, blurTarget, children, elevation, experimentalBlurMethod, intensity, style, tint: _tint, tintColor, tintOpacity, variant = 'surface', ...props }: BlurViewProps) => {
|
|
62
71
|
const { dark, colors } = useTheme()
|
|
63
72
|
const { settings } = useContext(ThemeSettingsContext)
|
|
64
73
|
const expoBlur = useBlurModule()
|
|
@@ -67,16 +76,21 @@ export const BlurView = ({ blur = true, children, elevation, style, tintColor, t
|
|
|
67
76
|
const resolvedTintColor = tintColor ?? variantColor ?? (elevation !== undefined ? colors.primary : undefined)
|
|
68
77
|
const resolvedTintOpacity = tintOpacity ?? (tintColor ? (dark ? 0.24 : 0.16) : variantColor ? (dark ? VARIANT_TINT_OPACITY.dark : VARIANT_TINT_OPACITY.light) : elevation !== undefined ? ELEVATION_OPACITY[elevation] : 0)
|
|
69
78
|
const tint = resolvedTintColor && resolvedTintOpacity > 0 ? <View style={[StyleSheet.absoluteFill, { backgroundColor: resolvedTintColor, opacity: resolvedTintOpacity }]} /> : null
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
const blurProps = { blurMethod, blurReductionFactor, blurTarget, experimentalBlurMethod, intensity }
|
|
80
|
+
const blurs = blur && expoBlur !== undefined && (Platform.OS !== 'android' || blursOnAndroid(blurProps))
|
|
81
|
+
const web = Platform.OS === 'web'
|
|
82
|
+
// The View holding the children is the same element whether or not blur applies, so flipping
|
|
83
|
+
// `blur` (a Dialog or Menu following the player's live settings.blur) never remounts them: their
|
|
84
|
+
// state, scroll offset, focus and ripples all survive. On iOS and Android the blur is an absoluteFill
|
|
85
|
+
// layer behind everything else, the same structure as expo-blur's own native BlurView. On web it
|
|
86
|
+
// can't be: Dialog and Menu fade this View's own opacity there, and an ancestor below opacity 1 cuts
|
|
87
|
+
// a CSS backdrop-filter off from the page behind it, so on web this View itself is the
|
|
88
|
+
// backdrop-filter element, styled the way expo-blur's web BlurView styles its own.
|
|
89
|
+
const containerStyle = !blurs ? [{ backgroundColor: surfaceColor }, style] : web ? [style, getWebBlurStyle(intensity ?? 50, dark)] : style
|
|
78
90
|
return (
|
|
79
|
-
<View {...props} style={
|
|
91
|
+
<View {...props} style={containerStyle}>
|
|
92
|
+
{blurs && !web ? <expoBlur.BlurView {...blurProps} style={StyleSheet.absoluteFill} tint={dark ? 'dark' : 'light'} /> : null}
|
|
93
|
+
{blurs ? <View style={[StyleSheet.absoluteFill, { backgroundColor: surfaceColor, opacity: settings.blurTint }]} /> : null}
|
|
80
94
|
{tint}
|
|
81
95
|
{children}
|
|
82
96
|
</View>
|
|
@@ -7,11 +7,11 @@ import { useBlur } from '../BlurContext'
|
|
|
7
7
|
import { useReanimatedModule } from '../ThemeProvider'
|
|
8
8
|
import { BlurView } from './BlurView'
|
|
9
9
|
|
|
10
|
-
// Minimal local mirror of react-native-reanimated's Animated.View
|
|
10
|
+
// Minimal local mirror of react-native-reanimated's Animated.View - same reasoning as BlurView's
|
|
11
11
|
// ExpoBlurModule: Metro doesn't rewrite a require()-in-try/catch call into its module graph inside
|
|
12
12
|
// an ESM build, so the already-imported peer module is injected via <Provider reanimated={...}>
|
|
13
13
|
// (see ThemeProvider.tsx) instead of required directly. Only `View` is needed here. `style: any`,
|
|
14
|
-
// not a real style type
|
|
14
|
+
// not a real style type - reanimated's actual AnimatedView only accepts its own branded
|
|
15
15
|
// AnimatedStyle shape, which this package never imports (no reanimated dependency at all), so `any`
|
|
16
16
|
// is the only type that both sides (this interface and the real component passed in) can agree on.
|
|
17
17
|
export interface ReanimatedModule {
|
|
@@ -23,7 +23,7 @@ export type DialogProps = Omit<PaperDialogProps, 'style'> & {
|
|
|
23
23
|
blur?: boolean
|
|
24
24
|
style?: StyleProp<ViewStyle>
|
|
25
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={...}>
|
|
26
|
+
// reanimated module has been injected via <Provider reanimated={...}> - otherwise ignored. `style`
|
|
27
27
|
// above is a plain object read once per render, so a fast-changing value (e.g. tracking the
|
|
28
28
|
// keyboard height every frame) driven through it needs a full JS re-render per update to paint at
|
|
29
29
|
// all; wiring the same value through `animatedStyle` (e.g. the result of Reanimated's
|
|
@@ -13,12 +13,12 @@ const HARMONIES: { label: string; value: ColorHarmony; icon: string }[] = [
|
|
|
13
13
|
|
|
14
14
|
// Paper's own SegmentedButtonItem hardcodes `minWidth: 76` per segment (sized for a labeled text
|
|
15
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)
|
|
16
|
+
// narrower still once its padding and safe-area margins are subtracted) - they just overflow past
|
|
17
17
|
// its `overflow: hidden` edge instead of shrinking. A fixed smaller guess (say 44) only moves the
|
|
18
|
-
// breakpoint rather than removing it
|
|
18
|
+
// breakpoint rather than removing it - 6x44 still exceeds the card's content width on an ordinary
|
|
19
19
|
// ~360-375px phone. Zeroing the floor instead lets each segment's own `flex: 1` divide whatever
|
|
20
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
|
|
21
|
+
// exceed their container at any screen size - same "shrink to fit, never overflow" contract
|
|
22
22
|
// useSwatchGrid.ts's own SWATCH_GRID_COLUMNS already uses for the swatches below this row. Left
|
|
23
23
|
// untouched in labeled mode, where the wider floor is still doing its job of fitting the text.
|
|
24
24
|
const COMPACT_MIN_WIDTH = 0
|
|
@@ -26,13 +26,13 @@ const COMPACT_MIN_WIDTH = 0
|
|
|
26
26
|
type Props = {
|
|
27
27
|
value: ColorHarmony
|
|
28
28
|
onChange: (harmony: ColorHarmony) => void
|
|
29
|
-
// Hides text labels, leaving just the icon per segment
|
|
29
|
+
// Hides text labels, leaving just the icon per segment - mirrors AppearancePicker's own
|
|
30
30
|
// showLabels, useful here since there are 6 segments to fit instead of 3.
|
|
31
31
|
// accessibilityLabel is always set regardless, so screen readers still announce the full
|
|
32
32
|
// harmony name either way.
|
|
33
33
|
showLabels?: boolean
|
|
34
34
|
// Per-segment fill, e.g. theme.colors.secondary/onSecondary for the checked segment and
|
|
35
|
-
// secondaryContainer/onSecondaryContainer for the rest
|
|
35
|
+
// secondaryContainer/onSecondaryContainer for the rest - matches Paper's own checked/
|
|
36
36
|
// unchecked + container color vocabulary. Omit to fall back to Paper's default segmented-
|
|
37
37
|
// button styling.
|
|
38
38
|
checkedColor?: string
|
|
@@ -58,7 +58,7 @@ type Props = {
|
|
|
58
58
|
harmony?: ColorHarmony
|
|
59
59
|
// Opt-in: passing this turns on a compact, icon-only HarmonyPicker row inside the dialog, above
|
|
60
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
|
|
61
|
+
// and this renders exactly as a plain PalettePicker always has - same trigger, same dialog,
|
|
62
62
|
// harmony fixed at whatever the `harmony` prop says. Gated on the callback itself rather than a
|
|
63
63
|
// separate boolean so there's no way to pass one without the other.
|
|
64
64
|
onHarmonyChange?: (harmony: ColorHarmony) => void
|
package/src/useComputedTheme.ts
CHANGED
|
@@ -29,7 +29,7 @@ export type ThemeAppearance = 'system' | 'light' | 'dark'
|
|
|
29
29
|
// HelperText, Badge) read theme.colors.error/onError directly, so a consumer that wants a
|
|
30
30
|
// getColorRoles-derived "high severity" color without changing what those components render needs
|
|
31
31
|
// its own name. #B00020 already clears WCAG AA for both onDanger-vs-danger (6.16:1) and
|
|
32
|
-
// dangerContainer/onDangerContainer (12
|
|
32
|
+
// dangerContainer/onDangerContainer (12-17:1 across modes), verified the same way as warning's
|
|
33
33
|
// base above.
|
|
34
34
|
export const SEMANTIC_BASE_COLORS = {
|
|
35
35
|
success: '#2E7D32',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getHex } from './getHex'
|
|
2
2
|
import { getRgb } from './getRgb'
|
|
3
3
|
|
|
4
|
-
// Returns the color with lightness clamped to the given target (0
|
|
4
|
+
// Returns the color with lightness clamped to the given target (0-1), preserving hue and saturation.
|
|
5
5
|
export const getTonalColor = (color: string, lightness: number): string => {
|
|
6
6
|
const rgb = getRgb(color)
|
|
7
7
|
if (!rgb) return color
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { TriadicPalette } from './getTriadicPalette'
|
|
2
2
|
|
|
3
|
-
// ThemeSettings.color is `string | TriadicPalette`
|
|
3
|
+
// ThemeSettings.color is `string | TriadicPalette` - a plain seed hex normally, or an explicit
|
|
4
4
|
// triad when a consumer bypassed harmony math entirely (see the README's own "Explicit triad"
|
|
5
|
-
// section). Anything that needs a single representative color from it
|
|
6
|
-
// a card's accent
|
|
5
|
+
// section). Anything that needs a single representative color from it - a picker's own `value`,
|
|
6
|
+
// a card's accent - narrows via this: `.primary` is that triad's own seed, so it's the
|
|
7
7
|
// representative color on the rare path where this branch is actually hit.
|
|
8
8
|
export const resolveSeedColor = (color: string | TriadicPalette): string => (typeof color === 'string' ? color : color.primary)
|