@rific/auto-paper 0.7.0 → 0.8.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 +53 -3
- package/dist/index.d.mts +40 -3
- package/dist/index.d.ts +40 -3
- package/dist/index.js +99 -35
- package/dist/index.mjs +99 -39
- package/package.json +1 -1
- package/src/components/AppearancePicker.tsx +25 -5
- package/src/components/ColorPicker.tsx +7 -6
- package/src/components/PalettePicker.tsx +9 -9
- package/src/index.ts +7 -2
- package/src/useComputedTheme.ts +87 -21
- package/src/useTheme.ts +11 -0
- package/src/utils/getColorRoles.ts +29 -0
- package/src/utils/getSwatchContrast.ts +26 -0
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ Adaptive theming for [`react-native-paper`](https://callstack.github.io/react-na
|
|
|
7
7
|
- Color harmony palette from a single seed color (primary → secondary → tertiary) — 6 harmony modes available, defaulting to `split-complementary`
|
|
8
8
|
- System/light/dark appearance with live updates via `Appearance` API
|
|
9
9
|
- Tinted surface, surfaceVariant, outline, and elevation levels derived from the seed
|
|
10
|
+
- Fixed `success`/`warning`/`danger` semantic color roles (each with `on*`/`*Container` variants) alongside MD3's built-in `error`
|
|
10
11
|
- Optional Redux slice for wiring appearance and color into your store
|
|
11
12
|
- Wrapper components (`Appbar`, `AppearancePicker`, `BlurView`, `BottomNavigation`, `Button`, `Chip`, `ColorPicker`, `Dialog`, `FAB`, `HarmonyPicker`, `IconButton`, `Menu`, `PalettePicker`, `TextInput`) with prop defaults via context
|
|
12
13
|
- All color utilities exported for standalone use
|
|
@@ -228,13 +229,60 @@ set({ harmony: 'triadic' })
|
|
|
228
229
|
|
|
229
230
|
### `useComputedTheme(appearance, color, harmony?)`
|
|
230
231
|
|
|
231
|
-
Returns `
|
|
232
|
+
Returns `AutoPaperTheme | null` — `MD3Theme` plus `success`/`warning`/`danger` color roles (see below). `null` on the first render while the theme computes.
|
|
232
233
|
|
|
233
234
|
```ts
|
|
234
235
|
const theme = useComputedTheme('system', '#6750a4')
|
|
235
236
|
const theme = useComputedTheme('system', '#6750a4', 'triadic')
|
|
236
237
|
```
|
|
237
238
|
|
|
239
|
+
Alongside the standard MD3 `primary`/`secondary`/`tertiary` roles derived from the seed color, `theme.colors` also carries three fixed semantic roles, each with the usual `color`/`onColor`/`Container`/`onContainer` quadruple (`success`, `onSuccess`, `successContainer`, `onSuccessContainer`, and likewise for `warning` and `danger`):
|
|
240
|
+
|
|
241
|
+
| Role | Base color |
|
|
242
|
+
|---|---|
|
|
243
|
+
| `success` | `SEMANTIC_BASE_COLORS.success` (`#2E7D32`) |
|
|
244
|
+
| `warning` | `SEMANTIC_BASE_COLORS.warning` (`#A15C00`) |
|
|
245
|
+
| `danger` | `SEMANTIC_BASE_COLORS.danger` (`#B00020`) |
|
|
246
|
+
|
|
247
|
+
These are fixed brand colors — like MD3's own `error`, which is left untouched — not derived from the seed; only their container blending adapts to light/dark mode. `danger` is deliberately separate from `error`: `react-native-paper` components (`TextInput`'s error state, `HelperText`, `Badge`) read `colors.error`/`colors.onError` directly, so redefining `error` itself would change form-validation colors app-wide.
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
import { SEMANTIC_BASE_COLORS } from '@rific/auto-paper'
|
|
251
|
+
import type { AutoPaperTheme, SemanticColorRoles } from '@rific/auto-paper'
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### `useAutoPaperTheme()`
|
|
255
|
+
|
|
256
|
+
A typed alternative to `react-native-paper`'s own `useTheme()` that returns `AutoPaperTheme` — the same theme, with `colors.success`/`warning`/`danger` (and their `on*`/`*Container` variants) typed in, so you get autocomplete without redeclaring the generic yourself at every call site.
|
|
257
|
+
|
|
258
|
+
```tsx
|
|
259
|
+
import { useAutoPaperTheme } from '@rific/auto-paper'
|
|
260
|
+
|
|
261
|
+
const { colors } = useAutoPaperTheme()
|
|
262
|
+
|
|
263
|
+
<Text style={{ color: colors.warning }}>Check your connection</Text>
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### `AppearancePicker`
|
|
267
|
+
|
|
268
|
+
A thin wrapper around `react-native-paper`'s `SegmentedButtons` for switching between `system`/`light`/`dark` appearance.
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
import { AppearancePicker, useThemeSettings } from '@rific/auto-paper'
|
|
272
|
+
|
|
273
|
+
function SettingsScreen() {
|
|
274
|
+
const { settings: { appearance }, set } = useThemeSettings()
|
|
275
|
+
return <AppearancePicker value={appearance} onChange={(a) => set({ appearance: a })} />
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
| Prop | Type | Description |
|
|
280
|
+
|---|---|---|
|
|
281
|
+
| `value` | `ThemeAppearance` | Currently selected appearance |
|
|
282
|
+
| `onChange` | `(appearance: ThemeAppearance) => void` | Called when a segment is tapped |
|
|
283
|
+
| `showLabels` | `boolean` | Set `false` for icon-only segments — useful in tight layouts where "System" would truncate. `accessibilityLabel` is always set regardless, so screen readers still announce the full word. Defaults to `true` |
|
|
284
|
+
| `icons` | `AppearanceIcons` | Per-value icon overrides — pass just the ones you want to change (e.g. `{ system: 'theme-light-dark' }`), merged over the defaults (`monitor` / `white-balance-sunny` / `weather-night`) |
|
|
285
|
+
|
|
238
286
|
### `Appbar`
|
|
239
287
|
|
|
240
288
|
A thin wrapper around `react-native-paper`'s `Appbar` that automatically syncs `StatusBar` background color and bar style to the current theme surface color.
|
|
@@ -387,7 +435,7 @@ const appearance = useSelector((state: RootState) => selectThemeAppearance(state
|
|
|
387
435
|
### Color utilities
|
|
388
436
|
|
|
389
437
|
```ts
|
|
390
|
-
import { getTriadicPalette, getBlendedColor, isDarkColor, getRgb, getHex, getTonalColor, getTintTextColor } from '@rific/auto-paper'
|
|
438
|
+
import { getTriadicPalette, getBlendedColor, getColorRoles, getContrastColor, isDarkColor, getRgb, getHex, getTonalColor, getTintTextColor } from '@rific/auto-paper'
|
|
391
439
|
import type { ColorHarmony } from '@rific/auto-paper'
|
|
392
440
|
|
|
393
441
|
getTriadicPalette('#6750a4') // split-complementary (default)
|
|
@@ -397,7 +445,9 @@ getTriadicPalette('#6750a4', 'square')
|
|
|
397
445
|
getTriadicPalette('#6750a4', 'complementary')
|
|
398
446
|
getTriadicPalette('#6750a4', 'double-split')
|
|
399
447
|
|
|
400
|
-
getBlendedColor('#ff0000', '#0000ff', 0.5)
|
|
448
|
+
getBlendedColor('#ff0000', '#0000ff', 0.5) // → '#800080'
|
|
449
|
+
getColorRoles('#6750a4', '#fffbfe') // → { color, onColor, container, onContainer } — the MD3 color/onColor/container/onContainer math, generalized (this is what useComputedTheme uses internally for every role)
|
|
450
|
+
getContrastColor('#6750a4') // → '#ffffff' — black/white pick for content sitting on a fully saturated fill
|
|
401
451
|
isDarkColor('#6750a4') // → true
|
|
402
452
|
getRgb('coral') // → { r: 255, g: 127, b: 80 }
|
|
403
453
|
getHex('rgb(255, 0, 0)') // → '#ff0000'
|
package/dist/index.d.mts
CHANGED
|
@@ -29,13 +29,38 @@ type TriadicPalette = {
|
|
|
29
29
|
declare function getTriadicPalette(primaryColor: string, harmony?: ColorHarmony): TriadicPalette;
|
|
30
30
|
|
|
31
31
|
type ThemeAppearance = 'system' | 'light' | 'dark';
|
|
32
|
-
declare
|
|
32
|
+
declare const SEMANTIC_BASE_COLORS: {
|
|
33
|
+
success: string;
|
|
34
|
+
warning: string;
|
|
35
|
+
danger: string;
|
|
36
|
+
};
|
|
37
|
+
type SemanticColorRoles = {
|
|
38
|
+
success: string;
|
|
39
|
+
onSuccess: string;
|
|
40
|
+
successContainer: string;
|
|
41
|
+
onSuccessContainer: string;
|
|
42
|
+
warning: string;
|
|
43
|
+
onWarning: string;
|
|
44
|
+
warningContainer: string;
|
|
45
|
+
onWarningContainer: string;
|
|
46
|
+
danger: string;
|
|
47
|
+
onDanger: string;
|
|
48
|
+
dangerContainer: string;
|
|
49
|
+
onDangerContainer: string;
|
|
50
|
+
};
|
|
51
|
+
type AutoPaperTheme = Omit<MD3Theme, 'colors'> & {
|
|
52
|
+
colors: MD3Theme['colors'] & SemanticColorRoles;
|
|
53
|
+
};
|
|
54
|
+
declare function useComputedTheme(appearance: ThemeAppearance, color: string, harmony?: ColorHarmony): AutoPaperTheme | null;
|
|
33
55
|
|
|
56
|
+
type AppearanceIcons = Partial<Record<ThemeAppearance, string>>;
|
|
34
57
|
type Props$3 = {
|
|
35
58
|
value: ThemeAppearance;
|
|
36
59
|
onChange: (appearance: ThemeAppearance) => void;
|
|
60
|
+
showLabels?: boolean;
|
|
61
|
+
icons?: AppearanceIcons;
|
|
37
62
|
};
|
|
38
|
-
declare const AppearancePicker: ({ value, onChange }: Props$3) => react.JSX.Element;
|
|
63
|
+
declare const AppearancePicker: ({ value, onChange, showLabels, icons }: Props$3) => react.JSX.Element;
|
|
39
64
|
|
|
40
65
|
type ExpoBlurTint = 'light' | 'dark' | 'default' | 'extraLight' | 'regular' | 'prominent' | 'systemUltraThinMaterial' | 'systemThinMaterial' | 'systemMaterial' | 'systemThickMaterial' | 'systemChromeMaterial' | 'systemUltraThinMaterialLight' | 'systemThinMaterialLight' | 'systemMaterialLight' | 'systemThickMaterialLight' | 'systemChromeMaterialLight' | 'systemUltraThinMaterialDark' | 'systemThinMaterialDark' | 'systemMaterialDark' | 'systemThickMaterialDark' | 'systemChromeMaterialDark';
|
|
41
66
|
type ExpoBlurMethod = 'none' | 'dimezisBlurView' | 'dimezisBlurViewSdk31Plus';
|
|
@@ -240,10 +265,20 @@ type ProviderProps = {
|
|
|
240
265
|
};
|
|
241
266
|
declare function Provider({ initialValue, onChange, children, defaults, onNavBarChange, onReady, statusBarProps, style }: ProviderProps): react.JSX.Element | null;
|
|
242
267
|
|
|
268
|
+
declare const useAutoPaperTheme: () => AutoPaperTheme;
|
|
269
|
+
|
|
243
270
|
declare const useThemeSettings: () => ThemeSettingsContextType;
|
|
244
271
|
|
|
245
272
|
declare const getBlendedColor: (foreground: string, background: string, alpha: number) => string;
|
|
246
273
|
|
|
274
|
+
type ColorRoles = {
|
|
275
|
+
color: string;
|
|
276
|
+
onColor: string;
|
|
277
|
+
container: string;
|
|
278
|
+
onContainer: string;
|
|
279
|
+
};
|
|
280
|
+
declare const getColorRoles: (base: string, surface: string, containerAlpha?: number) => ColorRoles;
|
|
281
|
+
|
|
247
282
|
declare const getHex: (value: string) => string | null;
|
|
248
283
|
|
|
249
284
|
type RGB = {
|
|
@@ -254,10 +289,12 @@ type RGB = {
|
|
|
254
289
|
};
|
|
255
290
|
declare const getRgb: (value: string) => RGB | null;
|
|
256
291
|
|
|
292
|
+
declare const getContrastColor: (color: string) => string;
|
|
293
|
+
|
|
257
294
|
declare const getTintTextColor: (tintColor: string, backdropColor: string, tintOpacity: number) => string;
|
|
258
295
|
|
|
259
296
|
declare const getTonalColor: (color: string, lightness: number) => string;
|
|
260
297
|
|
|
261
298
|
declare const isDarkColor: (value: string) => boolean;
|
|
262
299
|
|
|
263
|
-
export { Appbar, AppearancePicker, BlurView, type BlurViewProps, BottomNavigation, Button, Chip, type ChipProps, type ColorHarmony, ColorPicker, Dialog, type DialogProps, FAB, HarmonyPicker, IconButton, type IconButtonProps, Menu, type MenuProps, PalettePicker, type PaletteWeights, type PaperDefaults, Provider, type ProviderProps, type RGB, type SeedColor, TextInput, type ThemeAppearance, type ThemeSettings, ThemeSettingsContext, type ThemeSettingsContextType, type ThemeState, type TriadicPalette, createThemeReducer, defaultColors, defaultThemeSettings, getBlendedColor, getHex, getRgb, getTintTextColor, getTonalColor, getTriadicPalette, isDarkColor, selectThemeAppearance, selectThemeBlur, selectThemeColor, selectThemeHarmony, themeActions, themeReducer, useBlur, useComputedTheme, usePaperDefaults, useThemeSettings };
|
|
300
|
+
export { Appbar, type AppearanceIcons, AppearancePicker, type AutoPaperTheme, BlurView, type BlurViewProps, BottomNavigation, Button, Chip, type ChipProps, type ColorHarmony, ColorPicker, type ColorRoles, Dialog, type DialogProps, FAB, HarmonyPicker, IconButton, type IconButtonProps, Menu, type MenuProps, PalettePicker, type PaletteWeights, type PaperDefaults, Provider, type ProviderProps, type RGB, SEMANTIC_BASE_COLORS, type SeedColor, type SemanticColorRoles, TextInput, type ThemeAppearance, type ThemeSettings, ThemeSettingsContext, type ThemeSettingsContextType, type ThemeState, type TriadicPalette, createThemeReducer, defaultColors, defaultThemeSettings, getBlendedColor, getColorRoles, getContrastColor, getHex, getRgb, getTintTextColor, getTonalColor, getTriadicPalette, isDarkColor, selectThemeAppearance, selectThemeBlur, selectThemeColor, selectThemeHarmony, themeActions, themeReducer, useAutoPaperTheme, useBlur, useComputedTheme, usePaperDefaults, useThemeSettings };
|
package/dist/index.d.ts
CHANGED
|
@@ -29,13 +29,38 @@ type TriadicPalette = {
|
|
|
29
29
|
declare function getTriadicPalette(primaryColor: string, harmony?: ColorHarmony): TriadicPalette;
|
|
30
30
|
|
|
31
31
|
type ThemeAppearance = 'system' | 'light' | 'dark';
|
|
32
|
-
declare
|
|
32
|
+
declare const SEMANTIC_BASE_COLORS: {
|
|
33
|
+
success: string;
|
|
34
|
+
warning: string;
|
|
35
|
+
danger: string;
|
|
36
|
+
};
|
|
37
|
+
type SemanticColorRoles = {
|
|
38
|
+
success: string;
|
|
39
|
+
onSuccess: string;
|
|
40
|
+
successContainer: string;
|
|
41
|
+
onSuccessContainer: string;
|
|
42
|
+
warning: string;
|
|
43
|
+
onWarning: string;
|
|
44
|
+
warningContainer: string;
|
|
45
|
+
onWarningContainer: string;
|
|
46
|
+
danger: string;
|
|
47
|
+
onDanger: string;
|
|
48
|
+
dangerContainer: string;
|
|
49
|
+
onDangerContainer: string;
|
|
50
|
+
};
|
|
51
|
+
type AutoPaperTheme = Omit<MD3Theme, 'colors'> & {
|
|
52
|
+
colors: MD3Theme['colors'] & SemanticColorRoles;
|
|
53
|
+
};
|
|
54
|
+
declare function useComputedTheme(appearance: ThemeAppearance, color: string, harmony?: ColorHarmony): AutoPaperTheme | null;
|
|
33
55
|
|
|
56
|
+
type AppearanceIcons = Partial<Record<ThemeAppearance, string>>;
|
|
34
57
|
type Props$3 = {
|
|
35
58
|
value: ThemeAppearance;
|
|
36
59
|
onChange: (appearance: ThemeAppearance) => void;
|
|
60
|
+
showLabels?: boolean;
|
|
61
|
+
icons?: AppearanceIcons;
|
|
37
62
|
};
|
|
38
|
-
declare const AppearancePicker: ({ value, onChange }: Props$3) => react.JSX.Element;
|
|
63
|
+
declare const AppearancePicker: ({ value, onChange, showLabels, icons }: Props$3) => react.JSX.Element;
|
|
39
64
|
|
|
40
65
|
type ExpoBlurTint = 'light' | 'dark' | 'default' | 'extraLight' | 'regular' | 'prominent' | 'systemUltraThinMaterial' | 'systemThinMaterial' | 'systemMaterial' | 'systemThickMaterial' | 'systemChromeMaterial' | 'systemUltraThinMaterialLight' | 'systemThinMaterialLight' | 'systemMaterialLight' | 'systemThickMaterialLight' | 'systemChromeMaterialLight' | 'systemUltraThinMaterialDark' | 'systemThinMaterialDark' | 'systemMaterialDark' | 'systemThickMaterialDark' | 'systemChromeMaterialDark';
|
|
41
66
|
type ExpoBlurMethod = 'none' | 'dimezisBlurView' | 'dimezisBlurViewSdk31Plus';
|
|
@@ -240,10 +265,20 @@ type ProviderProps = {
|
|
|
240
265
|
};
|
|
241
266
|
declare function Provider({ initialValue, onChange, children, defaults, onNavBarChange, onReady, statusBarProps, style }: ProviderProps): react.JSX.Element | null;
|
|
242
267
|
|
|
268
|
+
declare const useAutoPaperTheme: () => AutoPaperTheme;
|
|
269
|
+
|
|
243
270
|
declare const useThemeSettings: () => ThemeSettingsContextType;
|
|
244
271
|
|
|
245
272
|
declare const getBlendedColor: (foreground: string, background: string, alpha: number) => string;
|
|
246
273
|
|
|
274
|
+
type ColorRoles = {
|
|
275
|
+
color: string;
|
|
276
|
+
onColor: string;
|
|
277
|
+
container: string;
|
|
278
|
+
onContainer: string;
|
|
279
|
+
};
|
|
280
|
+
declare const getColorRoles: (base: string, surface: string, containerAlpha?: number) => ColorRoles;
|
|
281
|
+
|
|
247
282
|
declare const getHex: (value: string) => string | null;
|
|
248
283
|
|
|
249
284
|
type RGB = {
|
|
@@ -254,10 +289,12 @@ type RGB = {
|
|
|
254
289
|
};
|
|
255
290
|
declare const getRgb: (value: string) => RGB | null;
|
|
256
291
|
|
|
292
|
+
declare const getContrastColor: (color: string) => string;
|
|
293
|
+
|
|
257
294
|
declare const getTintTextColor: (tintColor: string, backdropColor: string, tintOpacity: number) => string;
|
|
258
295
|
|
|
259
296
|
declare const getTonalColor: (color: string, lightness: number) => string;
|
|
260
297
|
|
|
261
298
|
declare const isDarkColor: (value: string) => boolean;
|
|
262
299
|
|
|
263
|
-
export { Appbar, AppearancePicker, BlurView, type BlurViewProps, BottomNavigation, Button, Chip, type ChipProps, type ColorHarmony, ColorPicker, Dialog, type DialogProps, FAB, HarmonyPicker, IconButton, type IconButtonProps, Menu, type MenuProps, PalettePicker, type PaletteWeights, type PaperDefaults, Provider, type ProviderProps, type RGB, type SeedColor, TextInput, type ThemeAppearance, type ThemeSettings, ThemeSettingsContext, type ThemeSettingsContextType, type ThemeState, type TriadicPalette, createThemeReducer, defaultColors, defaultThemeSettings, getBlendedColor, getHex, getRgb, getTintTextColor, getTonalColor, getTriadicPalette, isDarkColor, selectThemeAppearance, selectThemeBlur, selectThemeColor, selectThemeHarmony, themeActions, themeReducer, useBlur, useComputedTheme, usePaperDefaults, useThemeSettings };
|
|
300
|
+
export { Appbar, type AppearanceIcons, AppearancePicker, type AutoPaperTheme, BlurView, type BlurViewProps, BottomNavigation, Button, Chip, type ChipProps, type ColorHarmony, ColorPicker, type ColorRoles, Dialog, type DialogProps, FAB, HarmonyPicker, IconButton, type IconButtonProps, Menu, type MenuProps, PalettePicker, type PaletteWeights, type PaperDefaults, Provider, type ProviderProps, type RGB, SEMANTIC_BASE_COLORS, type SeedColor, type SemanticColorRoles, TextInput, type ThemeAppearance, type ThemeSettings, ThemeSettingsContext, type ThemeSettingsContextType, type ThemeState, type TriadicPalette, createThemeReducer, defaultColors, defaultThemeSettings, getBlendedColor, getColorRoles, getContrastColor, getHex, getRgb, getTintTextColor, getTonalColor, getTriadicPalette, isDarkColor, selectThemeAppearance, selectThemeBlur, selectThemeColor, selectThemeHarmony, themeActions, themeReducer, useAutoPaperTheme, useBlur, useComputedTheme, usePaperDefaults, useThemeSettings };
|
package/dist/index.js
CHANGED
|
@@ -44,12 +44,15 @@ __export(index_exports, {
|
|
|
44
44
|
Menu: () => Menu,
|
|
45
45
|
PalettePicker: () => PalettePicker,
|
|
46
46
|
Provider: () => Provider,
|
|
47
|
+
SEMANTIC_BASE_COLORS: () => SEMANTIC_BASE_COLORS,
|
|
47
48
|
TextInput: () => TextInput,
|
|
48
49
|
ThemeSettingsContext: () => ThemeSettingsContext,
|
|
49
50
|
createThemeReducer: () => createThemeReducer,
|
|
50
51
|
defaultColors: () => defaultColors,
|
|
51
52
|
defaultThemeSettings: () => defaultThemeSettings,
|
|
52
53
|
getBlendedColor: () => getBlendedColor,
|
|
54
|
+
getColorRoles: () => getColorRoles,
|
|
55
|
+
getContrastColor: () => getContrastColor,
|
|
53
56
|
getHex: () => getHex,
|
|
54
57
|
getRgb: () => getRgb,
|
|
55
58
|
getTintTextColor: () => getTintTextColor,
|
|
@@ -62,6 +65,7 @@ __export(index_exports, {
|
|
|
62
65
|
selectThemeHarmony: () => selectThemeHarmony,
|
|
63
66
|
themeActions: () => themeActions,
|
|
64
67
|
themeReducer: () => themeReducer,
|
|
68
|
+
useAutoPaperTheme: () => useAutoPaperTheme,
|
|
65
69
|
useBlur: () => useBlur,
|
|
66
70
|
useComputedTheme: () => useComputedTheme,
|
|
67
71
|
usePaperDefaults: () => usePaperDefaults,
|
|
@@ -125,11 +129,19 @@ var Appbar = {
|
|
|
125
129
|
var import_react_native_paper2 = require("react-native-paper");
|
|
126
130
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
127
131
|
var APPEARANCES = [
|
|
128
|
-
{ label: "System", value: "system"
|
|
129
|
-
{ label: "Light", value: "light"
|
|
130
|
-
{ label: "Dark", value: "dark"
|
|
132
|
+
{ label: "System", value: "system" },
|
|
133
|
+
{ label: "Light", value: "light" },
|
|
134
|
+
{ label: "Dark", value: "dark" }
|
|
131
135
|
];
|
|
132
|
-
var
|
|
136
|
+
var DEFAULT_ICONS = {
|
|
137
|
+
system: "monitor",
|
|
138
|
+
light: "white-balance-sunny",
|
|
139
|
+
dark: "weather-night"
|
|
140
|
+
};
|
|
141
|
+
var AppearancePicker = ({ value, onChange, showLabels = true, icons }) => {
|
|
142
|
+
const resolvedIcons = { ...DEFAULT_ICONS, ...icons };
|
|
143
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_paper2.SegmentedButtons, { value, onValueChange: (v) => onChange(v), buttons: APPEARANCES.map((a) => ({ value: a.value, label: showLabels ? a.label : void 0, icon: resolvedIcons[a.value], accessibilityLabel: a.label })) });
|
|
144
|
+
};
|
|
133
145
|
|
|
134
146
|
// src/components/BlurView.tsx
|
|
135
147
|
var import_react4 = require("react");
|
|
@@ -471,6 +483,27 @@ var getTonalColor = (color, lightness) => {
|
|
|
471
483
|
return getHex(`rgb(${toInt(r2)}, ${toInt(g2)}, ${toInt(b2)})`) ?? color;
|
|
472
484
|
};
|
|
473
485
|
|
|
486
|
+
// src/utils/isDarkColor.ts
|
|
487
|
+
var isDarkColor = (value) => {
|
|
488
|
+
const rgb = getRgb(value);
|
|
489
|
+
if (!rgb) return false;
|
|
490
|
+
const [r, g, b] = [rgb.r / 255, rgb.g / 255, rgb.b / 255];
|
|
491
|
+
const [rs, gs, bs] = [r, g, b].map((c) => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
|
|
492
|
+
const luminance = 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
|
|
493
|
+
return luminance < 0.5;
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
// src/utils/getColorRoles.ts
|
|
497
|
+
var getColorRoles = (base, surface, containerAlpha = 0.15) => {
|
|
498
|
+
const container = getBlendedColor(base, surface, containerAlpha);
|
|
499
|
+
return {
|
|
500
|
+
color: base,
|
|
501
|
+
onColor: getTonalColor(base, isDarkColor(base) ? 0.95 : 0.08),
|
|
502
|
+
container,
|
|
503
|
+
onContainer: getTonalColor(base, isDarkColor(container) ? 0.92 : 0.12)
|
|
504
|
+
};
|
|
505
|
+
};
|
|
506
|
+
|
|
474
507
|
// src/utils/getTriadicPalette.ts
|
|
475
508
|
var HARMONY_OFFSETS = {
|
|
476
509
|
analogous: [1 / 12, 2 / 12],
|
|
@@ -552,17 +585,12 @@ function hslToHex(h, s, l) {
|
|
|
552
585
|
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
553
586
|
}
|
|
554
587
|
|
|
555
|
-
// src/utils/isDarkColor.ts
|
|
556
|
-
var isDarkColor = (value) => {
|
|
557
|
-
const rgb = getRgb(value);
|
|
558
|
-
if (!rgb) return false;
|
|
559
|
-
const [r, g, b] = [rgb.r / 255, rgb.g / 255, rgb.b / 255];
|
|
560
|
-
const [rs, gs, bs] = [r, g, b].map((c) => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
|
|
561
|
-
const luminance = 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
|
|
562
|
-
return luminance < 0.5;
|
|
563
|
-
};
|
|
564
|
-
|
|
565
588
|
// src/useComputedTheme.ts
|
|
589
|
+
var SEMANTIC_BASE_COLORS = {
|
|
590
|
+
success: "#2E7D32",
|
|
591
|
+
warning: "#A15C00",
|
|
592
|
+
danger: "#B00020"
|
|
593
|
+
};
|
|
566
594
|
function useComputedTheme(appearance, color, harmony = "split-complementary") {
|
|
567
595
|
const [theme, setTheme] = (0, import_react5.useState)(null);
|
|
568
596
|
const computeTheme = (0, import_react5.useCallback)(() => {
|
|
@@ -570,27 +598,45 @@ function useComputedTheme(appearance, color, harmony = "split-complementary") {
|
|
|
570
598
|
if (resolved === "system") resolved = import_react_native3.Appearance.getColorScheme();
|
|
571
599
|
if (resolved === "no-preference" || !resolved) resolved = "light";
|
|
572
600
|
const base = resolved === "dark" ? import_react_native_paper4.MD3DarkTheme : import_react_native_paper4.MD3LightTheme;
|
|
601
|
+
const surface = base.colors.surface;
|
|
602
|
+
const successRoles = getColorRoles(SEMANTIC_BASE_COLORS.success, surface);
|
|
603
|
+
const warningRoles = getColorRoles(SEMANTIC_BASE_COLORS.warning, surface);
|
|
604
|
+
const dangerRoles = getColorRoles(SEMANTIC_BASE_COLORS.danger, surface);
|
|
573
605
|
const _theme = {
|
|
574
606
|
...base,
|
|
575
607
|
colors: {
|
|
576
608
|
...base.colors,
|
|
577
|
-
elevation: { ...base.colors.elevation }
|
|
609
|
+
elevation: { ...base.colors.elevation },
|
|
610
|
+
success: successRoles.color,
|
|
611
|
+
onSuccess: successRoles.onColor,
|
|
612
|
+
successContainer: successRoles.container,
|
|
613
|
+
onSuccessContainer: successRoles.onContainer,
|
|
614
|
+
warning: warningRoles.color,
|
|
615
|
+
onWarning: warningRoles.onColor,
|
|
616
|
+
warningContainer: warningRoles.container,
|
|
617
|
+
onWarningContainer: warningRoles.onContainer,
|
|
618
|
+
danger: dangerRoles.color,
|
|
619
|
+
onDanger: dangerRoles.onColor,
|
|
620
|
+
dangerContainer: dangerRoles.container,
|
|
621
|
+
onDangerContainer: dangerRoles.onContainer
|
|
578
622
|
}
|
|
579
623
|
};
|
|
580
624
|
const palette = getTriadicPalette(color, harmony);
|
|
581
|
-
const
|
|
582
|
-
_theme.colors.primary =
|
|
583
|
-
_theme.colors.onPrimary =
|
|
584
|
-
_theme.colors.primaryContainer =
|
|
585
|
-
_theme.colors.onPrimaryContainer =
|
|
586
|
-
|
|
587
|
-
_theme.colors.
|
|
588
|
-
_theme.colors.
|
|
589
|
-
_theme.colors.
|
|
590
|
-
_theme.colors.
|
|
591
|
-
|
|
592
|
-
_theme.colors.
|
|
593
|
-
_theme.colors.
|
|
625
|
+
const primaryRoles = getColorRoles(palette.primary, surface);
|
|
626
|
+
_theme.colors.primary = primaryRoles.color;
|
|
627
|
+
_theme.colors.onPrimary = primaryRoles.onColor;
|
|
628
|
+
_theme.colors.primaryContainer = primaryRoles.container;
|
|
629
|
+
_theme.colors.onPrimaryContainer = primaryRoles.onContainer;
|
|
630
|
+
const secondaryRoles = getColorRoles(palette.secondary, surface);
|
|
631
|
+
_theme.colors.secondary = secondaryRoles.color;
|
|
632
|
+
_theme.colors.onSecondary = secondaryRoles.onColor;
|
|
633
|
+
_theme.colors.secondaryContainer = secondaryRoles.container;
|
|
634
|
+
_theme.colors.onSecondaryContainer = secondaryRoles.onContainer;
|
|
635
|
+
const tertiaryRoles = getColorRoles(palette.tertiary, surface);
|
|
636
|
+
_theme.colors.tertiary = tertiaryRoles.color;
|
|
637
|
+
_theme.colors.onTertiary = tertiaryRoles.onColor;
|
|
638
|
+
_theme.colors.tertiaryContainer = tertiaryRoles.container;
|
|
639
|
+
_theme.colors.onTertiaryContainer = tertiaryRoles.onContainer;
|
|
594
640
|
const blended = getBlendedColor(color, palette.secondary, 0.5);
|
|
595
641
|
const tintSurface = (alpha) => getBlendedColor(blended, surface, alpha);
|
|
596
642
|
_theme.colors.background = base.dark ? "#0F0F0F" : "#F0F0F0";
|
|
@@ -714,6 +760,15 @@ function useSwatchSize(fallback) {
|
|
|
714
760
|
return { onLayout, size };
|
|
715
761
|
}
|
|
716
762
|
|
|
763
|
+
// src/utils/getSwatchContrast.ts
|
|
764
|
+
var RING_WIDTH = 1;
|
|
765
|
+
var SELECTED_RING_WIDTH = 3;
|
|
766
|
+
var getContrastColor = (color) => isDarkColor(color) ? "#ffffff" : "#000000";
|
|
767
|
+
var getSwatchRing = (color, selected, outlineColor) => ({
|
|
768
|
+
borderColor: selected ? getContrastColor(color) : outlineColor,
|
|
769
|
+
borderWidth: selected ? SELECTED_RING_WIDTH : RING_WIDTH
|
|
770
|
+
});
|
|
771
|
+
|
|
717
772
|
// src/components/Dialog.tsx
|
|
718
773
|
var import_react9 = __toESM(require("react"));
|
|
719
774
|
var import_react_native6 = require("react-native");
|
|
@@ -805,10 +860,11 @@ var defaultColors = [
|
|
|
805
860
|
];
|
|
806
861
|
var ColorPicker = ({ value, onChange, colors = defaultColors, blur: blurProp }) => {
|
|
807
862
|
const blur = useBlur(blurProp);
|
|
863
|
+
const { colors: themeColors } = (0, import_react_native_paper10.useTheme)();
|
|
808
864
|
const [open, setOpen] = (0, import_react10.useState)(false);
|
|
809
865
|
const { onLayout, size } = useSwatchSize(SIZE);
|
|
810
866
|
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
|
|
811
|
-
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_paper10.TouchableRipple, { onPress: () => setOpen(true), style: [styles3.trigger, { backgroundColor: value }], borderless: true, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_paper10.Icon, { source: "palette", size: 20, color:
|
|
867
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_paper10.TouchableRipple, { onPress: () => setOpen(true), style: [styles3.trigger, { backgroundColor: value }, getSwatchRing(value, false, themeColors.outlineVariant)], borderless: true, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_paper10.Icon, { source: "palette", size: 20, color: getContrastColor(value) }) }),
|
|
812
868
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Dialog, { blur, visible: open, onDismiss: () => setOpen(false), children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Dialog.Content, { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native7.View, { style: styles3.swatches, onLayout, children: colors.map(({ value: hex }) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
813
869
|
import_react_native7.TouchableOpacity,
|
|
814
870
|
{
|
|
@@ -816,15 +872,14 @@ var ColorPicker = ({ value, onChange, colors = defaultColors, blur: blurProp })
|
|
|
816
872
|
onChange(hex);
|
|
817
873
|
setOpen(false);
|
|
818
874
|
},
|
|
819
|
-
style: [styles3.swatch, { backgroundColor: hex, borderRadius: size / 2, height: size, width: size }, value === hex
|
|
820
|
-
children: value === hex && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_paper10.Icon, { source: "check", size: 20, color:
|
|
875
|
+
style: [styles3.swatch, { backgroundColor: hex, borderRadius: size / 2, height: size, width: size }, getSwatchRing(hex, value === hex, themeColors.outlineVariant)],
|
|
876
|
+
children: value === hex && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native_paper10.Icon, { source: "check", size: 20, color: getContrastColor(hex) })
|
|
821
877
|
},
|
|
822
878
|
hex
|
|
823
879
|
)) }) }) })
|
|
824
880
|
] });
|
|
825
881
|
};
|
|
826
882
|
var styles3 = import_react_native7.StyleSheet.create({
|
|
827
|
-
selected: { borderColor: "white", borderWidth: 3 },
|
|
828
883
|
swatch: { alignItems: "center", justifyContent: "center" },
|
|
829
884
|
swatches: { flexDirection: "row", flexWrap: "wrap", gap: SWATCH_GRID_GAP, paddingTop: 12 },
|
|
830
885
|
trigger: { alignItems: "center", alignSelf: "flex-start", borderRadius: SIZE / 2, height: SIZE, justifyContent: "center", width: SIZE }
|
|
@@ -926,12 +981,13 @@ var buildWedges = (weights) => {
|
|
|
926
981
|
var Pie = ({ palette, weights = defaultWeights, size = SIZE2 }) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { style: [styles5.pie, { borderRadius: size / 2, height: size, width: size }], children: buildWedges(weights).map(({ key, rotate, share }) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { style: [styles5.rotator, { transform: [{ rotate: `${rotate}deg` }] }], children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { style: styles5.halfMask, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { style: [styles5.innerRotator, { transform: [{ rotate: `${share - 180}deg` }] }], children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { style: [styles5.wedgeFill, { backgroundColor: palette[key] }] }) }) }) }, key)) });
|
|
927
982
|
var PalettePicker = ({ value, onChange, harmony = "split-complementary", weights, colors = defaultColors, blur: blurProp }) => {
|
|
928
983
|
const blur = useBlur(blurProp);
|
|
984
|
+
const { colors: themeColors } = (0, import_react_native_paper15.useTheme)();
|
|
929
985
|
const [open, setOpen] = (0, import_react12.useState)(false);
|
|
930
986
|
const { onLayout, size } = useSwatchSize(SIZE2);
|
|
931
987
|
return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
|
|
932
988
|
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper15.TouchableRipple, { onPress: () => setOpen(true), style: styles5.trigger, borderless: true, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native9.View, { style: styles5.triggerContent, children: [
|
|
933
989
|
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Pie, { palette: getTriadicPalette(value, harmony), weights }),
|
|
934
|
-
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { pointerEvents: "none", style: styles5.triggerIcon, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper15.Icon, { source: "palette", size: 20, color:
|
|
990
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { pointerEvents: "none", style: [styles5.triggerIcon, { borderRadius: SIZE2 / 2 }, getSwatchRing(value, false, themeColors.outlineVariant)], children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper15.Icon, { source: "palette", size: 20, color: getContrastColor(value) }) })
|
|
935
991
|
] }) }),
|
|
936
992
|
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Dialog, { blur, visible: open, onDismiss: () => setOpen(false), children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Dialog.Content, { children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { style: styles5.swatches, onLayout, children: colors.map(({ value: hex }) => /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
|
|
937
993
|
import_react_native9.TouchableOpacity,
|
|
@@ -943,7 +999,7 @@ var PalettePicker = ({ value, onChange, harmony = "split-complementary", weights
|
|
|
943
999
|
style: [styles5.swatch, { height: size, width: size }],
|
|
944
1000
|
children: [
|
|
945
1001
|
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Pie, { palette: getTriadicPalette(hex, harmony), weights, size }),
|
|
946
|
-
|
|
1002
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { pointerEvents: "none", style: [styles5.ring, { borderRadius: size / 2 }, getSwatchRing(hex, value === hex, themeColors.outlineVariant)], children: value === hex && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper15.Icon, { source: "check", size: 20, color: getContrastColor(hex) }) })
|
|
947
1003
|
]
|
|
948
1004
|
},
|
|
949
1005
|
hex
|
|
@@ -955,7 +1011,7 @@ var styles5 = import_react_native9.StyleSheet.create({
|
|
|
955
1011
|
innerRotator: { height: "100%", left: "-100%", overflow: "hidden", position: "absolute", top: 0, width: "200%" },
|
|
956
1012
|
pie: { overflow: "hidden" },
|
|
957
1013
|
rotator: { height: "100%", left: 0, overflow: "hidden", position: "absolute", top: 0, width: "100%" },
|
|
958
|
-
|
|
1014
|
+
ring: { alignItems: "center", bottom: 0, justifyContent: "center", left: 0, position: "absolute", right: 0, top: 0 },
|
|
959
1015
|
swatch: { alignItems: "center", justifyContent: "center" },
|
|
960
1016
|
swatches: { flexDirection: "row", flexWrap: "wrap", gap: SWATCH_GRID_GAP, paddingTop: 12 },
|
|
961
1017
|
trigger: { alignItems: "center", alignSelf: "flex-start", borderRadius: SIZE2 / 2, height: SIZE2, justifyContent: "center", width: SIZE2 },
|
|
@@ -1010,6 +1066,10 @@ var selectThemeBlur = (state) => state.blur;
|
|
|
1010
1066
|
var selectThemeColor = (state) => state.color;
|
|
1011
1067
|
var selectThemeHarmony = (state) => state.harmony;
|
|
1012
1068
|
|
|
1069
|
+
// src/useTheme.ts
|
|
1070
|
+
var import_react_native_paper17 = require("react-native-paper");
|
|
1071
|
+
var useAutoPaperTheme = () => (0, import_react_native_paper17.useTheme)();
|
|
1072
|
+
|
|
1013
1073
|
// src/useThemeSettings.ts
|
|
1014
1074
|
var import_react14 = require("react");
|
|
1015
1075
|
var useThemeSettings = () => (0, import_react14.useContext)(ThemeSettingsContext);
|
|
@@ -1035,12 +1095,15 @@ var getTintTextColor = (tintColor, backdropColor, tintOpacity) => {
|
|
|
1035
1095
|
Menu,
|
|
1036
1096
|
PalettePicker,
|
|
1037
1097
|
Provider,
|
|
1098
|
+
SEMANTIC_BASE_COLORS,
|
|
1038
1099
|
TextInput,
|
|
1039
1100
|
ThemeSettingsContext,
|
|
1040
1101
|
createThemeReducer,
|
|
1041
1102
|
defaultColors,
|
|
1042
1103
|
defaultThemeSettings,
|
|
1043
1104
|
getBlendedColor,
|
|
1105
|
+
getColorRoles,
|
|
1106
|
+
getContrastColor,
|
|
1044
1107
|
getHex,
|
|
1045
1108
|
getRgb,
|
|
1046
1109
|
getTintTextColor,
|
|
@@ -1053,6 +1116,7 @@ var getTintTextColor = (tintColor, backdropColor, tintOpacity) => {
|
|
|
1053
1116
|
selectThemeHarmony,
|
|
1054
1117
|
themeActions,
|
|
1055
1118
|
themeReducer,
|
|
1119
|
+
useAutoPaperTheme,
|
|
1056
1120
|
useBlur,
|
|
1057
1121
|
useComputedTheme,
|
|
1058
1122
|
usePaperDefaults,
|
package/dist/index.mjs
CHANGED
|
@@ -61,11 +61,19 @@ var Appbar = {
|
|
|
61
61
|
import { SegmentedButtons } from "react-native-paper";
|
|
62
62
|
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
63
63
|
var APPEARANCES = [
|
|
64
|
-
{ label: "System", value: "system"
|
|
65
|
-
{ label: "Light", value: "light"
|
|
66
|
-
{ label: "Dark", value: "dark"
|
|
64
|
+
{ label: "System", value: "system" },
|
|
65
|
+
{ label: "Light", value: "light" },
|
|
66
|
+
{ label: "Dark", value: "dark" }
|
|
67
67
|
];
|
|
68
|
-
var
|
|
68
|
+
var DEFAULT_ICONS = {
|
|
69
|
+
system: "monitor",
|
|
70
|
+
light: "white-balance-sunny",
|
|
71
|
+
dark: "weather-night"
|
|
72
|
+
};
|
|
73
|
+
var AppearancePicker = ({ value, onChange, showLabels = true, icons }) => {
|
|
74
|
+
const resolvedIcons = { ...DEFAULT_ICONS, ...icons };
|
|
75
|
+
return /* @__PURE__ */ jsx2(SegmentedButtons, { value, onValueChange: (v) => onChange(v), buttons: APPEARANCES.map((a) => ({ value: a.value, label: showLabels ? a.label : void 0, icon: resolvedIcons[a.value], accessibilityLabel: a.label })) });
|
|
76
|
+
};
|
|
69
77
|
|
|
70
78
|
// src/components/BlurView.tsx
|
|
71
79
|
import { useContext as useContext3 } from "react";
|
|
@@ -407,6 +415,27 @@ var getTonalColor = (color, lightness) => {
|
|
|
407
415
|
return getHex(`rgb(${toInt(r2)}, ${toInt(g2)}, ${toInt(b2)})`) ?? color;
|
|
408
416
|
};
|
|
409
417
|
|
|
418
|
+
// src/utils/isDarkColor.ts
|
|
419
|
+
var isDarkColor = (value) => {
|
|
420
|
+
const rgb = getRgb(value);
|
|
421
|
+
if (!rgb) return false;
|
|
422
|
+
const [r, g, b] = [rgb.r / 255, rgb.g / 255, rgb.b / 255];
|
|
423
|
+
const [rs, gs, bs] = [r, g, b].map((c) => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
|
|
424
|
+
const luminance = 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
|
|
425
|
+
return luminance < 0.5;
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
// src/utils/getColorRoles.ts
|
|
429
|
+
var getColorRoles = (base, surface, containerAlpha = 0.15) => {
|
|
430
|
+
const container = getBlendedColor(base, surface, containerAlpha);
|
|
431
|
+
return {
|
|
432
|
+
color: base,
|
|
433
|
+
onColor: getTonalColor(base, isDarkColor(base) ? 0.95 : 0.08),
|
|
434
|
+
container,
|
|
435
|
+
onContainer: getTonalColor(base, isDarkColor(container) ? 0.92 : 0.12)
|
|
436
|
+
};
|
|
437
|
+
};
|
|
438
|
+
|
|
410
439
|
// src/utils/getTriadicPalette.ts
|
|
411
440
|
var HARMONY_OFFSETS = {
|
|
412
441
|
analogous: [1 / 12, 2 / 12],
|
|
@@ -488,17 +517,12 @@ function hslToHex(h, s, l) {
|
|
|
488
517
|
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
489
518
|
}
|
|
490
519
|
|
|
491
|
-
// src/utils/isDarkColor.ts
|
|
492
|
-
var isDarkColor = (value) => {
|
|
493
|
-
const rgb = getRgb(value);
|
|
494
|
-
if (!rgb) return false;
|
|
495
|
-
const [r, g, b] = [rgb.r / 255, rgb.g / 255, rgb.b / 255];
|
|
496
|
-
const [rs, gs, bs] = [r, g, b].map((c) => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
|
|
497
|
-
const luminance = 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
|
|
498
|
-
return luminance < 0.5;
|
|
499
|
-
};
|
|
500
|
-
|
|
501
520
|
// src/useComputedTheme.ts
|
|
521
|
+
var SEMANTIC_BASE_COLORS = {
|
|
522
|
+
success: "#2E7D32",
|
|
523
|
+
warning: "#A15C00",
|
|
524
|
+
danger: "#B00020"
|
|
525
|
+
};
|
|
502
526
|
function useComputedTheme(appearance, color, harmony = "split-complementary") {
|
|
503
527
|
const [theme, setTheme] = useState(null);
|
|
504
528
|
const computeTheme = useCallback(() => {
|
|
@@ -506,27 +530,45 @@ function useComputedTheme(appearance, color, harmony = "split-complementary") {
|
|
|
506
530
|
if (resolved === "system") resolved = Appearance.getColorScheme();
|
|
507
531
|
if (resolved === "no-preference" || !resolved) resolved = "light";
|
|
508
532
|
const base = resolved === "dark" ? MD3DarkTheme : MD3LightTheme;
|
|
533
|
+
const surface = base.colors.surface;
|
|
534
|
+
const successRoles = getColorRoles(SEMANTIC_BASE_COLORS.success, surface);
|
|
535
|
+
const warningRoles = getColorRoles(SEMANTIC_BASE_COLORS.warning, surface);
|
|
536
|
+
const dangerRoles = getColorRoles(SEMANTIC_BASE_COLORS.danger, surface);
|
|
509
537
|
const _theme = {
|
|
510
538
|
...base,
|
|
511
539
|
colors: {
|
|
512
540
|
...base.colors,
|
|
513
|
-
elevation: { ...base.colors.elevation }
|
|
541
|
+
elevation: { ...base.colors.elevation },
|
|
542
|
+
success: successRoles.color,
|
|
543
|
+
onSuccess: successRoles.onColor,
|
|
544
|
+
successContainer: successRoles.container,
|
|
545
|
+
onSuccessContainer: successRoles.onContainer,
|
|
546
|
+
warning: warningRoles.color,
|
|
547
|
+
onWarning: warningRoles.onColor,
|
|
548
|
+
warningContainer: warningRoles.container,
|
|
549
|
+
onWarningContainer: warningRoles.onContainer,
|
|
550
|
+
danger: dangerRoles.color,
|
|
551
|
+
onDanger: dangerRoles.onColor,
|
|
552
|
+
dangerContainer: dangerRoles.container,
|
|
553
|
+
onDangerContainer: dangerRoles.onContainer
|
|
514
554
|
}
|
|
515
555
|
};
|
|
516
556
|
const palette = getTriadicPalette(color, harmony);
|
|
517
|
-
const
|
|
518
|
-
_theme.colors.primary =
|
|
519
|
-
_theme.colors.onPrimary =
|
|
520
|
-
_theme.colors.primaryContainer =
|
|
521
|
-
_theme.colors.onPrimaryContainer =
|
|
522
|
-
|
|
523
|
-
_theme.colors.
|
|
524
|
-
_theme.colors.
|
|
525
|
-
_theme.colors.
|
|
526
|
-
_theme.colors.
|
|
527
|
-
|
|
528
|
-
_theme.colors.
|
|
529
|
-
_theme.colors.
|
|
557
|
+
const primaryRoles = getColorRoles(palette.primary, surface);
|
|
558
|
+
_theme.colors.primary = primaryRoles.color;
|
|
559
|
+
_theme.colors.onPrimary = primaryRoles.onColor;
|
|
560
|
+
_theme.colors.primaryContainer = primaryRoles.container;
|
|
561
|
+
_theme.colors.onPrimaryContainer = primaryRoles.onContainer;
|
|
562
|
+
const secondaryRoles = getColorRoles(palette.secondary, surface);
|
|
563
|
+
_theme.colors.secondary = secondaryRoles.color;
|
|
564
|
+
_theme.colors.onSecondary = secondaryRoles.onColor;
|
|
565
|
+
_theme.colors.secondaryContainer = secondaryRoles.container;
|
|
566
|
+
_theme.colors.onSecondaryContainer = secondaryRoles.onContainer;
|
|
567
|
+
const tertiaryRoles = getColorRoles(palette.tertiary, surface);
|
|
568
|
+
_theme.colors.tertiary = tertiaryRoles.color;
|
|
569
|
+
_theme.colors.onTertiary = tertiaryRoles.onColor;
|
|
570
|
+
_theme.colors.tertiaryContainer = tertiaryRoles.container;
|
|
571
|
+
_theme.colors.onTertiaryContainer = tertiaryRoles.onContainer;
|
|
530
572
|
const blended = getBlendedColor(color, palette.secondary, 0.5);
|
|
531
573
|
const tintSurface = (alpha) => getBlendedColor(blended, surface, alpha);
|
|
532
574
|
_theme.colors.background = base.dark ? "#0F0F0F" : "#F0F0F0";
|
|
@@ -637,7 +679,7 @@ function Chip({ variant, style, selectedColor, ...props }) {
|
|
|
637
679
|
// src/components/ColorPicker.tsx
|
|
638
680
|
import { useState as useState5 } from "react";
|
|
639
681
|
import { StyleSheet as StyleSheet4, TouchableOpacity, View as View4 } from "react-native";
|
|
640
|
-
import { Icon, TouchableRipple } from "react-native-paper";
|
|
682
|
+
import { Icon, TouchableRipple, useTheme as useTheme6 } from "react-native-paper";
|
|
641
683
|
|
|
642
684
|
// src/useSwatchGrid.ts
|
|
643
685
|
import { useState as useState3 } from "react";
|
|
@@ -650,6 +692,15 @@ function useSwatchSize(fallback) {
|
|
|
650
692
|
return { onLayout, size };
|
|
651
693
|
}
|
|
652
694
|
|
|
695
|
+
// src/utils/getSwatchContrast.ts
|
|
696
|
+
var RING_WIDTH = 1;
|
|
697
|
+
var SELECTED_RING_WIDTH = 3;
|
|
698
|
+
var getContrastColor = (color) => isDarkColor(color) ? "#ffffff" : "#000000";
|
|
699
|
+
var getSwatchRing = (color, selected, outlineColor) => ({
|
|
700
|
+
borderColor: selected ? getContrastColor(color) : outlineColor,
|
|
701
|
+
borderWidth: selected ? SELECTED_RING_WIDTH : RING_WIDTH
|
|
702
|
+
});
|
|
703
|
+
|
|
653
704
|
// src/components/Dialog.tsx
|
|
654
705
|
import React2, { useEffect as useEffect4, useRef as useRef2, useState as useState4 } from "react";
|
|
655
706
|
import { Animated, BackHandler, Easing, Pressable, StyleSheet as StyleSheet3, View as View3 } from "react-native";
|
|
@@ -741,10 +792,11 @@ var defaultColors = [
|
|
|
741
792
|
];
|
|
742
793
|
var ColorPicker = ({ value, onChange, colors = defaultColors, blur: blurProp }) => {
|
|
743
794
|
const blur = useBlur(blurProp);
|
|
795
|
+
const { colors: themeColors } = useTheme6();
|
|
744
796
|
const [open, setOpen] = useState5(false);
|
|
745
797
|
const { onLayout, size } = useSwatchSize(SIZE);
|
|
746
798
|
return /* @__PURE__ */ jsxs5(Fragment2, { children: [
|
|
747
|
-
/* @__PURE__ */ jsx9(TouchableRipple, { onPress: () => setOpen(true), style: [styles3.trigger, { backgroundColor: value }], borderless: true, children: /* @__PURE__ */ jsx9(Icon, { source: "palette", size: 20, color:
|
|
799
|
+
/* @__PURE__ */ jsx9(TouchableRipple, { onPress: () => setOpen(true), style: [styles3.trigger, { backgroundColor: value }, getSwatchRing(value, false, themeColors.outlineVariant)], borderless: true, children: /* @__PURE__ */ jsx9(Icon, { source: "palette", size: 20, color: getContrastColor(value) }) }),
|
|
748
800
|
/* @__PURE__ */ jsx9(Dialog, { blur, visible: open, onDismiss: () => setOpen(false), children: /* @__PURE__ */ jsx9(Dialog.Content, { children: /* @__PURE__ */ jsx9(View4, { style: styles3.swatches, onLayout, children: colors.map(({ value: hex }) => /* @__PURE__ */ jsx9(
|
|
749
801
|
TouchableOpacity,
|
|
750
802
|
{
|
|
@@ -752,15 +804,14 @@ var ColorPicker = ({ value, onChange, colors = defaultColors, blur: blurProp })
|
|
|
752
804
|
onChange(hex);
|
|
753
805
|
setOpen(false);
|
|
754
806
|
},
|
|
755
|
-
style: [styles3.swatch, { backgroundColor: hex, borderRadius: size / 2, height: size, width: size }, value === hex
|
|
756
|
-
children: value === hex && /* @__PURE__ */ jsx9(Icon, { source: "check", size: 20, color:
|
|
807
|
+
style: [styles3.swatch, { backgroundColor: hex, borderRadius: size / 2, height: size, width: size }, getSwatchRing(hex, value === hex, themeColors.outlineVariant)],
|
|
808
|
+
children: value === hex && /* @__PURE__ */ jsx9(Icon, { source: "check", size: 20, color: getContrastColor(hex) })
|
|
757
809
|
},
|
|
758
810
|
hex
|
|
759
811
|
)) }) }) })
|
|
760
812
|
] });
|
|
761
813
|
};
|
|
762
814
|
var styles3 = StyleSheet4.create({
|
|
763
|
-
selected: { borderColor: "white", borderWidth: 3 },
|
|
764
815
|
swatch: { alignItems: "center", justifyContent: "center" },
|
|
765
816
|
swatches: { flexDirection: "row", flexWrap: "wrap", gap: SWATCH_GRID_GAP, paddingTop: 12 },
|
|
766
817
|
trigger: { alignItems: "center", alignSelf: "flex-start", borderRadius: SIZE / 2, height: SIZE, justifyContent: "center", width: SIZE }
|
|
@@ -831,11 +882,11 @@ var HarmonyPicker = ({ value, onChange, blur: blurProp }) => {
|
|
|
831
882
|
};
|
|
832
883
|
|
|
833
884
|
// src/components/IconButton.tsx
|
|
834
|
-
import { IconButton as PaperIconButton, useTheme as
|
|
885
|
+
import { IconButton as PaperIconButton, useTheme as useTheme7 } from "react-native-paper";
|
|
835
886
|
import { jsx as jsx13 } from "react/jsx-runtime";
|
|
836
887
|
function IconButton({ variant, iconColor, containerColor, ...props }) {
|
|
837
888
|
const defaults = usePaperDefaults();
|
|
838
|
-
const { colors } =
|
|
889
|
+
const { colors } = useTheme7();
|
|
839
890
|
const variantContainerColor = variant === "primary" ? colors.primaryContainer : variant === "secondary" ? colors.secondaryContainer : variant === "tertiary" ? colors.tertiaryContainer : variant === "surface" ? colors.surface : void 0;
|
|
840
891
|
const variantIconColor = variant === "primary" ? colors.onPrimaryContainer : variant === "secondary" ? colors.onSecondaryContainer : variant === "tertiary" ? colors.onTertiaryContainer : variant === "surface" ? colors.onSurface : void 0;
|
|
841
892
|
return /* @__PURE__ */ jsx13(PaperIconButton, { ...defaults.IconButton, containerColor: containerColor ?? variantContainerColor, iconColor: iconColor ?? variantIconColor, ...props });
|
|
@@ -844,7 +895,7 @@ function IconButton({ variant, iconColor, containerColor, ...props }) {
|
|
|
844
895
|
// src/components/PalettePicker.tsx
|
|
845
896
|
import { useState as useState7 } from "react";
|
|
846
897
|
import { StyleSheet as StyleSheet6, TouchableOpacity as TouchableOpacity2, View as View5 } from "react-native";
|
|
847
|
-
import { Icon as Icon2, TouchableRipple as TouchableRipple2 } from "react-native-paper";
|
|
898
|
+
import { Icon as Icon2, TouchableRipple as TouchableRipple2, useTheme as useTheme8 } from "react-native-paper";
|
|
848
899
|
import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
849
900
|
var SIZE2 = 48;
|
|
850
901
|
var defaultWeights = { primary: 2, secondary: 1, tertiary: 1 };
|
|
@@ -862,12 +913,13 @@ var buildWedges = (weights) => {
|
|
|
862
913
|
var Pie = ({ palette, weights = defaultWeights, size = SIZE2 }) => /* @__PURE__ */ jsx14(View5, { style: [styles5.pie, { borderRadius: size / 2, height: size, width: size }], children: buildWedges(weights).map(({ key, rotate, share }) => /* @__PURE__ */ jsx14(View5, { style: [styles5.rotator, { transform: [{ rotate: `${rotate}deg` }] }], children: /* @__PURE__ */ jsx14(View5, { style: styles5.halfMask, children: /* @__PURE__ */ jsx14(View5, { style: [styles5.innerRotator, { transform: [{ rotate: `${share - 180}deg` }] }], children: /* @__PURE__ */ jsx14(View5, { style: [styles5.wedgeFill, { backgroundColor: palette[key] }] }) }) }) }, key)) });
|
|
863
914
|
var PalettePicker = ({ value, onChange, harmony = "split-complementary", weights, colors = defaultColors, blur: blurProp }) => {
|
|
864
915
|
const blur = useBlur(blurProp);
|
|
916
|
+
const { colors: themeColors } = useTheme8();
|
|
865
917
|
const [open, setOpen] = useState7(false);
|
|
866
918
|
const { onLayout, size } = useSwatchSize(SIZE2);
|
|
867
919
|
return /* @__PURE__ */ jsxs6(Fragment3, { children: [
|
|
868
920
|
/* @__PURE__ */ jsx14(TouchableRipple2, { onPress: () => setOpen(true), style: styles5.trigger, borderless: true, children: /* @__PURE__ */ jsxs6(View5, { style: styles5.triggerContent, children: [
|
|
869
921
|
/* @__PURE__ */ jsx14(Pie, { palette: getTriadicPalette(value, harmony), weights }),
|
|
870
|
-
/* @__PURE__ */ jsx14(View5, { pointerEvents: "none", style: styles5.triggerIcon, children: /* @__PURE__ */ jsx14(Icon2, { source: "palette", size: 20, color:
|
|
922
|
+
/* @__PURE__ */ jsx14(View5, { pointerEvents: "none", style: [styles5.triggerIcon, { borderRadius: SIZE2 / 2 }, getSwatchRing(value, false, themeColors.outlineVariant)], children: /* @__PURE__ */ jsx14(Icon2, { source: "palette", size: 20, color: getContrastColor(value) }) })
|
|
871
923
|
] }) }),
|
|
872
924
|
/* @__PURE__ */ jsx14(Dialog, { blur, visible: open, onDismiss: () => setOpen(false), children: /* @__PURE__ */ jsx14(Dialog.Content, { children: /* @__PURE__ */ jsx14(View5, { style: styles5.swatches, onLayout, children: colors.map(({ value: hex }) => /* @__PURE__ */ jsxs6(
|
|
873
925
|
TouchableOpacity2,
|
|
@@ -879,7 +931,7 @@ var PalettePicker = ({ value, onChange, harmony = "split-complementary", weights
|
|
|
879
931
|
style: [styles5.swatch, { height: size, width: size }],
|
|
880
932
|
children: [
|
|
881
933
|
/* @__PURE__ */ jsx14(Pie, { palette: getTriadicPalette(hex, harmony), weights, size }),
|
|
882
|
-
|
|
934
|
+
/* @__PURE__ */ jsx14(View5, { pointerEvents: "none", style: [styles5.ring, { borderRadius: size / 2 }, getSwatchRing(hex, value === hex, themeColors.outlineVariant)], children: value === hex && /* @__PURE__ */ jsx14(Icon2, { source: "check", size: 20, color: getContrastColor(hex) }) })
|
|
883
935
|
]
|
|
884
936
|
},
|
|
885
937
|
hex
|
|
@@ -891,7 +943,7 @@ var styles5 = StyleSheet6.create({
|
|
|
891
943
|
innerRotator: { height: "100%", left: "-100%", overflow: "hidden", position: "absolute", top: 0, width: "200%" },
|
|
892
944
|
pie: { overflow: "hidden" },
|
|
893
945
|
rotator: { height: "100%", left: 0, overflow: "hidden", position: "absolute", top: 0, width: "100%" },
|
|
894
|
-
|
|
946
|
+
ring: { alignItems: "center", bottom: 0, justifyContent: "center", left: 0, position: "absolute", right: 0, top: 0 },
|
|
895
947
|
swatch: { alignItems: "center", justifyContent: "center" },
|
|
896
948
|
swatches: { flexDirection: "row", flexWrap: "wrap", gap: SWATCH_GRID_GAP, paddingTop: 12 },
|
|
897
949
|
trigger: { alignItems: "center", alignSelf: "flex-start", borderRadius: SIZE2 / 2, height: SIZE2, justifyContent: "center", width: SIZE2 },
|
|
@@ -946,6 +998,10 @@ var selectThemeBlur = (state) => state.blur;
|
|
|
946
998
|
var selectThemeColor = (state) => state.color;
|
|
947
999
|
var selectThemeHarmony = (state) => state.harmony;
|
|
948
1000
|
|
|
1001
|
+
// src/useTheme.ts
|
|
1002
|
+
import { useTheme as usePaperTheme } from "react-native-paper";
|
|
1003
|
+
var useAutoPaperTheme = () => usePaperTheme();
|
|
1004
|
+
|
|
949
1005
|
// src/useThemeSettings.ts
|
|
950
1006
|
import { useContext as useContext5 } from "react";
|
|
951
1007
|
var useThemeSettings = () => useContext5(ThemeSettingsContext);
|
|
@@ -970,12 +1026,15 @@ export {
|
|
|
970
1026
|
Menu,
|
|
971
1027
|
PalettePicker,
|
|
972
1028
|
Provider,
|
|
1029
|
+
SEMANTIC_BASE_COLORS,
|
|
973
1030
|
TextInput,
|
|
974
1031
|
ThemeSettingsContext,
|
|
975
1032
|
createThemeReducer,
|
|
976
1033
|
defaultColors,
|
|
977
1034
|
defaultThemeSettings,
|
|
978
1035
|
getBlendedColor,
|
|
1036
|
+
getColorRoles,
|
|
1037
|
+
getContrastColor,
|
|
979
1038
|
getHex,
|
|
980
1039
|
getRgb,
|
|
981
1040
|
getTintTextColor,
|
|
@@ -988,6 +1047,7 @@ export {
|
|
|
988
1047
|
selectThemeHarmony,
|
|
989
1048
|
themeActions,
|
|
990
1049
|
themeReducer,
|
|
1050
|
+
useAutoPaperTheme,
|
|
991
1051
|
useBlur,
|
|
992
1052
|
useComputedTheme,
|
|
993
1053
|
usePaperDefaults,
|
package/package.json
CHANGED
|
@@ -2,15 +2,35 @@ import { SegmentedButtons } from 'react-native-paper'
|
|
|
2
2
|
|
|
3
3
|
import type { ThemeAppearance } from '../useComputedTheme'
|
|
4
4
|
|
|
5
|
-
const APPEARANCES: { label: string; value: ThemeAppearance
|
|
6
|
-
{ label: 'System', value: 'system'
|
|
7
|
-
{ label: 'Light', value: 'light'
|
|
8
|
-
{ label: 'Dark', value: 'dark'
|
|
5
|
+
const APPEARANCES: { label: string; value: ThemeAppearance }[] = [
|
|
6
|
+
{ label: 'System', value: 'system' },
|
|
7
|
+
{ label: 'Light', value: 'light' },
|
|
8
|
+
{ label: 'Dark', value: 'dark' }
|
|
9
9
|
]
|
|
10
10
|
|
|
11
|
+
export type AppearanceIcons = Partial<Record<ThemeAppearance, string>>
|
|
12
|
+
|
|
13
|
+
// 'monitor' (a plain device/screen glyph) reads more clearly as "match my device" than
|
|
14
|
+
// 'brightness-auto' (an exposure/brightness symbol) — swap via the `icons` prop for a different
|
|
15
|
+
// convention, e.g. `theme-light-dark`'s half-sun/half-moon.
|
|
16
|
+
const DEFAULT_ICONS: Record<ThemeAppearance, string> = {
|
|
17
|
+
system: 'monitor',
|
|
18
|
+
light: 'white-balance-sunny',
|
|
19
|
+
dark: 'weather-night'
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
type Props = {
|
|
12
23
|
value: ThemeAppearance
|
|
13
24
|
onChange: (appearance: ThemeAppearance) => void
|
|
25
|
+
// Hides the text label, leaving just the icon per segment — useful in tight layouts where
|
|
26
|
+
// "System" truncates. accessibilityLabel is always set regardless, so screen readers still
|
|
27
|
+
// announce the full word either way.
|
|
28
|
+
showLabels?: boolean
|
|
29
|
+
// Per-value icon overrides, merged over DEFAULT_ICONS — pass just the ones you want to change.
|
|
30
|
+
icons?: AppearanceIcons
|
|
14
31
|
}
|
|
15
32
|
|
|
16
|
-
export const AppearancePicker = ({ value, onChange
|
|
33
|
+
export const AppearancePicker = ({ value, onChange, showLabels = true, icons }: Props) => {
|
|
34
|
+
const resolvedIcons = { ...DEFAULT_ICONS, ...icons }
|
|
35
|
+
return <SegmentedButtons value={value} onValueChange={(v) => onChange(v as ThemeAppearance)} buttons={APPEARANCES.map((a) => ({ value: a.value, label: showLabels ? a.label : undefined, icon: resolvedIcons[a.value], accessibilityLabel: a.label }))} />
|
|
36
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
import { StyleSheet, TouchableOpacity, View } from 'react-native'
|
|
3
|
-
import { Icon, TouchableRipple } from 'react-native-paper'
|
|
3
|
+
import { Icon, TouchableRipple, useTheme } from 'react-native-paper'
|
|
4
4
|
|
|
5
5
|
import { useBlur } from '../BlurContext'
|
|
6
6
|
import { SWATCH_GRID_GAP, useSwatchSize } from '../useSwatchGrid'
|
|
7
|
+
import { getContrastColor, getSwatchRing } from '../utils/getSwatchContrast'
|
|
7
8
|
import { Dialog } from './Dialog'
|
|
8
9
|
|
|
9
10
|
const SIZE = 48
|
|
@@ -42,13 +43,14 @@ type Props = {
|
|
|
42
43
|
|
|
43
44
|
export const ColorPicker = ({ value, onChange, colors = defaultColors, blur: blurProp }: Props) => {
|
|
44
45
|
const blur = useBlur(blurProp)
|
|
46
|
+
const { colors: themeColors } = useTheme()
|
|
45
47
|
const [open, setOpen] = useState(false)
|
|
46
48
|
const { onLayout, size } = useSwatchSize(SIZE)
|
|
47
49
|
|
|
48
50
|
return (
|
|
49
51
|
<>
|
|
50
|
-
<TouchableRipple onPress={() => setOpen(true)} style={[styles.trigger, { backgroundColor: value }]} borderless>
|
|
51
|
-
<Icon source='palette' size={20} color=
|
|
52
|
+
<TouchableRipple onPress={() => setOpen(true)} style={[styles.trigger, { backgroundColor: value }, getSwatchRing(value, false, themeColors.outlineVariant)]} borderless>
|
|
53
|
+
<Icon source='palette' size={20} color={getContrastColor(value)} />
|
|
52
54
|
</TouchableRipple>
|
|
53
55
|
|
|
54
56
|
<Dialog blur={blur} visible={open} onDismiss={() => setOpen(false)}>
|
|
@@ -61,9 +63,9 @@ export const ColorPicker = ({ value, onChange, colors = defaultColors, blur: blu
|
|
|
61
63
|
onChange(hex)
|
|
62
64
|
setOpen(false)
|
|
63
65
|
}}
|
|
64
|
-
style={[styles.swatch, { backgroundColor: hex, borderRadius: size / 2, height: size, width: size }, value === hex
|
|
66
|
+
style={[styles.swatch, { backgroundColor: hex, borderRadius: size / 2, height: size, width: size }, getSwatchRing(hex, value === hex, themeColors.outlineVariant)]}
|
|
65
67
|
>
|
|
66
|
-
{value === hex && <Icon source='check' size={20} color=
|
|
68
|
+
{value === hex && <Icon source='check' size={20} color={getContrastColor(hex)} />}
|
|
67
69
|
</TouchableOpacity>
|
|
68
70
|
))}
|
|
69
71
|
</View>
|
|
@@ -74,7 +76,6 @@ export const ColorPicker = ({ value, onChange, colors = defaultColors, blur: blu
|
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
const styles = StyleSheet.create({
|
|
77
|
-
selected: { borderColor: 'white', borderWidth: 3 },
|
|
78
79
|
swatch: { alignItems: 'center', justifyContent: 'center' },
|
|
79
80
|
swatches: { flexDirection: 'row', flexWrap: 'wrap', gap: SWATCH_GRID_GAP, paddingTop: 12 },
|
|
80
81
|
trigger: { alignItems: 'center', alignSelf: 'flex-start', borderRadius: SIZE / 2, height: SIZE, justifyContent: 'center', width: SIZE }
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
import { StyleSheet, TouchableOpacity, View } from 'react-native'
|
|
3
|
-
import { Icon, TouchableRipple } from 'react-native-paper'
|
|
3
|
+
import { Icon, TouchableRipple, useTheme } from 'react-native-paper'
|
|
4
4
|
|
|
5
5
|
import { useBlur } from '../BlurContext'
|
|
6
6
|
import { SWATCH_GRID_GAP, useSwatchSize } from '../useSwatchGrid'
|
|
7
|
+
import { getContrastColor, getSwatchRing } from '../utils/getSwatchContrast'
|
|
7
8
|
import type { ColorHarmony, TriadicPalette } from '../utils/getTriadicPalette'
|
|
8
9
|
import { getTriadicPalette } from '../utils/getTriadicPalette'
|
|
9
10
|
import type { SeedColor } from './ColorPicker'
|
|
@@ -61,6 +62,7 @@ type Props = {
|
|
|
61
62
|
|
|
62
63
|
export const PalettePicker = ({ value, onChange, harmony = 'split-complementary', weights, colors = defaultColors, blur: blurProp }: Props) => {
|
|
63
64
|
const blur = useBlur(blurProp)
|
|
65
|
+
const { colors: themeColors } = useTheme()
|
|
64
66
|
const [open, setOpen] = useState(false)
|
|
65
67
|
const { onLayout, size } = useSwatchSize(SIZE)
|
|
66
68
|
|
|
@@ -69,8 +71,8 @@ export const PalettePicker = ({ value, onChange, harmony = 'split-complementary'
|
|
|
69
71
|
<TouchableRipple onPress={() => setOpen(true)} style={styles.trigger} borderless>
|
|
70
72
|
<View style={styles.triggerContent}>
|
|
71
73
|
<Pie palette={getTriadicPalette(value, harmony)} weights={weights} />
|
|
72
|
-
<View pointerEvents='none' style={styles.triggerIcon}>
|
|
73
|
-
<Icon source='palette' size={20} color=
|
|
74
|
+
<View pointerEvents='none' style={[styles.triggerIcon, { borderRadius: SIZE / 2 }, getSwatchRing(value, false, themeColors.outlineVariant)]}>
|
|
75
|
+
<Icon source='palette' size={20} color={getContrastColor(value)} />
|
|
74
76
|
</View>
|
|
75
77
|
</View>
|
|
76
78
|
</TouchableRipple>
|
|
@@ -88,11 +90,9 @@ export const PalettePicker = ({ value, onChange, harmony = 'split-complementary'
|
|
|
88
90
|
style={[styles.swatch, { height: size, width: size }]}
|
|
89
91
|
>
|
|
90
92
|
<Pie palette={getTriadicPalette(hex, harmony)} weights={weights} size={size} />
|
|
91
|
-
{value === hex
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
</View>
|
|
95
|
-
)}
|
|
93
|
+
<View pointerEvents='none' style={[styles.ring, { borderRadius: size / 2 }, getSwatchRing(hex, value === hex, themeColors.outlineVariant)]}>
|
|
94
|
+
{value === hex && <Icon source='check' size={20} color={getContrastColor(hex)} />}
|
|
95
|
+
</View>
|
|
96
96
|
</TouchableOpacity>
|
|
97
97
|
))}
|
|
98
98
|
</View>
|
|
@@ -107,7 +107,7 @@ const styles = StyleSheet.create({
|
|
|
107
107
|
innerRotator: { height: '100%', left: '-100%', overflow: 'hidden', position: 'absolute', top: 0, width: '200%' },
|
|
108
108
|
pie: { overflow: 'hidden' },
|
|
109
109
|
rotator: { height: '100%', left: 0, overflow: 'hidden', position: 'absolute', top: 0, width: '100%' },
|
|
110
|
-
|
|
110
|
+
ring: { alignItems: 'center', bottom: 0, justifyContent: 'center', left: 0, position: 'absolute', right: 0, top: 0 },
|
|
111
111
|
swatch: { alignItems: 'center', justifyContent: 'center' },
|
|
112
112
|
swatches: { flexDirection: 'row', flexWrap: 'wrap', gap: SWATCH_GRID_GAP, paddingTop: 12 },
|
|
113
113
|
trigger: { alignItems: 'center', alignSelf: 'flex-start', borderRadius: SIZE / 2, height: SIZE, justifyContent: 'center', width: SIZE },
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { useBlur } from './BlurContext'
|
|
2
2
|
export { Appbar } from './components/Appbar'
|
|
3
|
+
export type { AppearanceIcons } from './components/AppearancePicker'
|
|
3
4
|
export { AppearancePicker } from './components/AppearancePicker'
|
|
4
5
|
export type { BlurViewProps } from './components/BlurView'
|
|
5
6
|
export { BlurView } from './components/BlurView'
|
|
@@ -28,13 +29,17 @@ export type { ProviderProps } from './ThemeProvider'
|
|
|
28
29
|
export { Provider } from './ThemeProvider'
|
|
29
30
|
export type { ThemeSettings, ThemeSettingsContextType } from './ThemeSettingsContext'
|
|
30
31
|
export { defaultThemeSettings, ThemeSettingsContext } from './ThemeSettingsContext'
|
|
31
|
-
export type { ThemeAppearance } from './useComputedTheme'
|
|
32
|
-
export { useComputedTheme } from './useComputedTheme'
|
|
32
|
+
export type { AutoPaperTheme, SemanticColorRoles, ThemeAppearance } from './useComputedTheme'
|
|
33
|
+
export { SEMANTIC_BASE_COLORS, useComputedTheme } from './useComputedTheme'
|
|
34
|
+
export { useAutoPaperTheme } from './useTheme'
|
|
33
35
|
export { useThemeSettings } from './useThemeSettings'
|
|
34
36
|
export { getBlendedColor } from './utils/getBlendedColor'
|
|
37
|
+
export type { ColorRoles } from './utils/getColorRoles'
|
|
38
|
+
export { getColorRoles } from './utils/getColorRoles'
|
|
35
39
|
export { getHex } from './utils/getHex'
|
|
36
40
|
export type { RGB } from './utils/getRgb'
|
|
37
41
|
export { getRgb } from './utils/getRgb'
|
|
42
|
+
export { getContrastColor } from './utils/getSwatchContrast'
|
|
38
43
|
export { getTintTextColor } from './utils/getTintTextColor'
|
|
39
44
|
export { getTonalColor } from './utils/getTonalColor'
|
|
40
45
|
export type { ColorHarmony, TriadicPalette } from './utils/getTriadicPalette'
|
package/src/useComputedTheme.ts
CHANGED
|
@@ -3,46 +3,112 @@ import { Appearance } from 'react-native'
|
|
|
3
3
|
import { MD3DarkTheme, MD3LightTheme, MD3Theme } from 'react-native-paper'
|
|
4
4
|
|
|
5
5
|
import { getBlendedColor } from './utils/getBlendedColor'
|
|
6
|
-
import {
|
|
6
|
+
import { getColorRoles } from './utils/getColorRoles'
|
|
7
7
|
import { type ColorHarmony, getTriadicPalette } from './utils/getTriadicPalette'
|
|
8
|
-
import { isDarkColor } from './utils/isDarkColor'
|
|
9
8
|
|
|
10
9
|
export type ThemeAppearance = 'system' | 'light' | 'dark'
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
// Fixed hue/saturation, like MD3's own `error` role (see LightTheme.tsx/DarkTheme.tsx — error is a
|
|
12
|
+
// static Material palette constant, never derived from the seed color either). A "warning" amber
|
|
13
|
+
// shouldn't drift toward the app's accent hue any more than the built-in danger red does; only
|
|
14
|
+
// lightness adapts per light/dark mode below, via getColorRoles. #A15C00 (not the more obvious
|
|
15
|
+
// goldenrod #B8860B) was picked because its onWarning text fails WCAG AA (3.05:1) against the base
|
|
16
|
+
// color itself — see getColorRoles.test.ts's "onColor" caveat: the onColor heuristic (isDarkColor-
|
|
17
|
+
// based binary lightness pick) isn't a verified guarantee like onContainer is, so the base hex
|
|
18
|
+
// itself has to be chosen to land safely, not assumed to.
|
|
19
|
+
//
|
|
20
|
+
// `danger` is deliberately a separate namespace from MD3's own `error` (below, untouched) rather
|
|
21
|
+
// than a fourth tier folded into it — react-native-paper's own components (TextInput's error state,
|
|
22
|
+
// HelperText, Badge) read theme.colors.error/onError directly, so a consumer that wants a
|
|
23
|
+
// getColorRoles-derived "high severity" color without changing what those components render needs
|
|
24
|
+
// its own name. #B00020 already clears WCAG AA for both onDanger-vs-danger (6.16:1) and
|
|
25
|
+
// dangerContainer/onDangerContainer (12–17:1 across modes) — verified the same way as warning's
|
|
26
|
+
// base above.
|
|
27
|
+
export const SEMANTIC_BASE_COLORS = {
|
|
28
|
+
success: '#2E7D32',
|
|
29
|
+
warning: '#A15C00',
|
|
30
|
+
danger: '#B00020'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type SemanticColorRoles = {
|
|
34
|
+
success: string
|
|
35
|
+
onSuccess: string
|
|
36
|
+
successContainer: string
|
|
37
|
+
onSuccessContainer: string
|
|
38
|
+
warning: string
|
|
39
|
+
onWarning: string
|
|
40
|
+
warningContainer: string
|
|
41
|
+
onWarningContainer: string
|
|
42
|
+
danger: string
|
|
43
|
+
onDanger: string
|
|
44
|
+
dangerContainer: string
|
|
45
|
+
onDangerContainer: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// MD3Theme's `colors` is a closed `type` (not an `interface`), so it can't be widened via
|
|
49
|
+
// TypeScript's declare-module augmentation — this is the extension point instead, meant to be used
|
|
50
|
+
// with react-native-paper's own `useTheme<T>()` generic (see useTheme.ts's useAutoPaperTheme).
|
|
51
|
+
export type AutoPaperTheme = Omit<MD3Theme, 'colors'> & { colors: MD3Theme['colors'] & SemanticColorRoles }
|
|
52
|
+
|
|
53
|
+
export function useComputedTheme(appearance: ThemeAppearance, color: string, harmony: ColorHarmony = 'split-complementary'): AutoPaperTheme | null {
|
|
54
|
+
const [theme, setTheme] = useState<AutoPaperTheme | null>(null)
|
|
14
55
|
|
|
15
56
|
const computeTheme = useCallback(() => {
|
|
16
57
|
let resolved: string | null | undefined = appearance
|
|
17
58
|
if (resolved === 'system') resolved = Appearance.getColorScheme()
|
|
18
59
|
if (resolved === 'no-preference' || !resolved) resolved = 'light'
|
|
19
60
|
const base = resolved === 'dark' ? MD3DarkTheme : MD3LightTheme
|
|
61
|
+
const surface = base.colors.surface
|
|
20
62
|
|
|
21
|
-
|
|
63
|
+
// error/onError/errorContainer/onErrorContainer are deliberately left untouched below (not run
|
|
64
|
+
// through getColorRoles like success/warning/primary/secondary/tertiary are) — they're a fixed,
|
|
65
|
+
// Google-verified Material palette that react-native-paper's own components (TextInput's error
|
|
66
|
+
// state, HelperText, Badge) read directly, so re-deriving them would change form-validation
|
|
67
|
+
// colors across every consuming app and risk diverging from a contrast guarantee that's already
|
|
68
|
+
// correct as shipped.
|
|
69
|
+
const successRoles = getColorRoles(SEMANTIC_BASE_COLORS.success, surface)
|
|
70
|
+
const warningRoles = getColorRoles(SEMANTIC_BASE_COLORS.warning, surface)
|
|
71
|
+
const dangerRoles = getColorRoles(SEMANTIC_BASE_COLORS.danger, surface)
|
|
72
|
+
|
|
73
|
+
const _theme: AutoPaperTheme = {
|
|
22
74
|
...base,
|
|
23
75
|
colors: {
|
|
24
76
|
...base.colors,
|
|
25
|
-
elevation: { ...base.colors.elevation }
|
|
77
|
+
elevation: { ...base.colors.elevation },
|
|
78
|
+
success: successRoles.color,
|
|
79
|
+
onSuccess: successRoles.onColor,
|
|
80
|
+
successContainer: successRoles.container,
|
|
81
|
+
onSuccessContainer: successRoles.onContainer,
|
|
82
|
+
warning: warningRoles.color,
|
|
83
|
+
onWarning: warningRoles.onColor,
|
|
84
|
+
warningContainer: warningRoles.container,
|
|
85
|
+
onWarningContainer: warningRoles.onContainer,
|
|
86
|
+
danger: dangerRoles.color,
|
|
87
|
+
onDanger: dangerRoles.onColor,
|
|
88
|
+
dangerContainer: dangerRoles.container,
|
|
89
|
+
onDangerContainer: dangerRoles.onContainer
|
|
26
90
|
}
|
|
27
91
|
}
|
|
28
92
|
|
|
29
93
|
const palette = getTriadicPalette(color, harmony)
|
|
30
|
-
const surface = base.colors.surface
|
|
31
|
-
|
|
32
|
-
_theme.colors.primary = palette.primary
|
|
33
|
-
_theme.colors.onPrimary = getTonalColor(palette.primary, isDarkColor(palette.primary) ? 0.95 : 0.08)
|
|
34
|
-
_theme.colors.primaryContainer = getBlendedColor(palette.primary, surface, 0.15)
|
|
35
|
-
_theme.colors.onPrimaryContainer = getTonalColor(palette.primary, isDarkColor(_theme.colors.primaryContainer) ? 0.92 : 0.12)
|
|
36
|
-
|
|
37
|
-
_theme.colors.secondary = palette.secondary
|
|
38
|
-
_theme.colors.onSecondary = getTonalColor(palette.secondary, isDarkColor(palette.secondary) ? 0.95 : 0.08)
|
|
39
|
-
_theme.colors.secondaryContainer = getBlendedColor(palette.secondary, surface, 0.15)
|
|
40
|
-
_theme.colors.onSecondaryContainer = getTonalColor(palette.secondary, isDarkColor(_theme.colors.secondaryContainer) ? 0.92 : 0.12)
|
|
41
94
|
|
|
42
|
-
|
|
43
|
-
_theme.colors.
|
|
44
|
-
_theme.colors.
|
|
45
|
-
_theme.colors.
|
|
95
|
+
const primaryRoles = getColorRoles(palette.primary, surface)
|
|
96
|
+
_theme.colors.primary = primaryRoles.color
|
|
97
|
+
_theme.colors.onPrimary = primaryRoles.onColor
|
|
98
|
+
_theme.colors.primaryContainer = primaryRoles.container
|
|
99
|
+
_theme.colors.onPrimaryContainer = primaryRoles.onContainer
|
|
100
|
+
|
|
101
|
+
const secondaryRoles = getColorRoles(palette.secondary, surface)
|
|
102
|
+
_theme.colors.secondary = secondaryRoles.color
|
|
103
|
+
_theme.colors.onSecondary = secondaryRoles.onColor
|
|
104
|
+
_theme.colors.secondaryContainer = secondaryRoles.container
|
|
105
|
+
_theme.colors.onSecondaryContainer = secondaryRoles.onContainer
|
|
106
|
+
|
|
107
|
+
const tertiaryRoles = getColorRoles(palette.tertiary, surface)
|
|
108
|
+
_theme.colors.tertiary = tertiaryRoles.color
|
|
109
|
+
_theme.colors.onTertiary = tertiaryRoles.onColor
|
|
110
|
+
_theme.colors.tertiaryContainer = tertiaryRoles.container
|
|
111
|
+
_theme.colors.onTertiaryContainer = tertiaryRoles.onContainer
|
|
46
112
|
|
|
47
113
|
const blended = getBlendedColor(color, palette.secondary, 0.5)
|
|
48
114
|
const tintSurface = (alpha: number) => getBlendedColor(blended, surface, alpha)
|
package/src/useTheme.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useTheme as usePaperTheme } from 'react-native-paper'
|
|
2
|
+
|
|
3
|
+
import type { AutoPaperTheme } from './useComputedTheme'
|
|
4
|
+
|
|
5
|
+
// react-native-paper's own useTheme is generic (`useTheme<T = MD3Theme>()`) specifically so
|
|
6
|
+
// consumers can extend MD3Theme with their own fields — this is that extension point, so any
|
|
7
|
+
// component can read theme.colors.success/warning (and their onX/Container variants) with full
|
|
8
|
+
// type safety, without redeclaring the type parameter at every call site. Named distinctly from
|
|
9
|
+
// react-native-paper's own `useTheme` (not re-exported under the same name) to avoid the two being
|
|
10
|
+
// silently interchangeable at an import statement.
|
|
11
|
+
export const useAutoPaperTheme = (): AutoPaperTheme => usePaperTheme<AutoPaperTheme>()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { getBlendedColor } from './getBlendedColor'
|
|
2
|
+
import { getTonalColor } from './getTonalColor'
|
|
3
|
+
import { isDarkColor } from './isDarkColor'
|
|
4
|
+
|
|
5
|
+
export type ColorRoles = {
|
|
6
|
+
color: string
|
|
7
|
+
onColor: string
|
|
8
|
+
container: string
|
|
9
|
+
onContainer: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// The MD3 "color / onColor / container / onContainer" quadruple, generalized from the math
|
|
13
|
+
// useComputedTheme already uses for primary/secondary/tertiary: onColor is tuned to sit on the
|
|
14
|
+
// vivid color itself, container is base blended toward the theme's real surface (so it inherits
|
|
15
|
+
// that surface's actual lightness rather than a fixed light/dark guess), and onContainer is tuned
|
|
16
|
+
// against the *computed* container rather than a light/dark assumption — so it stays readable
|
|
17
|
+
// whatever surface color it's handed. onContainer also reads safely directly on `surface` itself
|
|
18
|
+
// (not just on `container`), since container is only lightly blended toward it.
|
|
19
|
+
export const getColorRoles = (base: string, surface: string, containerAlpha = 0.15): ColorRoles => {
|
|
20
|
+
const container = getBlendedColor(base, surface, containerAlpha)
|
|
21
|
+
return {
|
|
22
|
+
color: base,
|
|
23
|
+
onColor: getTonalColor(base, isDarkColor(base) ? 0.95 : 0.08),
|
|
24
|
+
container,
|
|
25
|
+
onContainer: getTonalColor(base, isDarkColor(container) ? 0.92 : 0.12)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default getColorRoles
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { isDarkColor } from './isDarkColor'
|
|
2
|
+
|
|
3
|
+
// Width of the outline every swatch carries so its edge is defined against the surface behind it.
|
|
4
|
+
const RING_WIDTH = 1
|
|
5
|
+
// The selected swatch trades that outline for a thicker one in a contrasting colour.
|
|
6
|
+
const SELECTED_RING_WIDTH = 3
|
|
7
|
+
|
|
8
|
+
// Colour for content sitting directly on a fill of `color` — a check mark or a 20px icon. Plain
|
|
9
|
+
// white/black rather than a tinted tone from getTonalColor, because this has to stay legible on
|
|
10
|
+
// fully saturated fills and on pure black/white alike.
|
|
11
|
+
export const getContrastColor = (color: string): string => (isDarkColor(color) ? '#ffffff' : '#000000')
|
|
12
|
+
|
|
13
|
+
// Two different jobs, which is why the unselected and selected rings resolve against different
|
|
14
|
+
// things:
|
|
15
|
+
//
|
|
16
|
+
// - Unselected: separate the swatch from the *surface* it sits on. A white swatch on a light
|
|
17
|
+
// dialog and a black swatch on a dark one are otherwise edgeless — invisible, not just subtle.
|
|
18
|
+
// The theme's outline colour tracks the surface, so it works in both appearances.
|
|
19
|
+
// - Selected: read as a marker against the *swatch's own fill*. A hardcoded white ring vanished on
|
|
20
|
+
// the white swatch, so picking white gave no feedback that anything had been picked.
|
|
21
|
+
export const getSwatchRing = (color: string, selected: boolean, outlineColor: string) => ({
|
|
22
|
+
borderColor: selected ? getContrastColor(color) : outlineColor,
|
|
23
|
+
borderWidth: selected ? SELECTED_RING_WIDTH : RING_WIDTH
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export default getContrastColor
|