@rific/core 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jay Deaton
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,147 @@
1
+ # @rific/core
2
+
3
+ Required foundation for `@rific/*` packages: a home for cross-cutting utilities every
4
+ `@rific`-consuming app can use directly.
5
+
6
+ - `createSettingsContext` — a generic Context/Provider/hook factory for a live, patchable settings
7
+ object whose persistence the consuming app owns.
8
+ - `createSettingsSlice` — the same idea for Redux: a generic reducer + action-creator factory, with
9
+ zero dependency on `@reduxjs/toolkit` (or any Redux library at all).
10
+ - `createModuleConfig` — a generic module-level config singleton, for injecting an optional peer
11
+ module (`react-native-paper`, `expo-camera`, etc.) without a hard dependency on it.
12
+
13
+ ## Why this exists
14
+
15
+ `feedback-press` (sound + haptics), `scroll-view`, and `auto-paper`'s `ThemeProvider` each hand-roll
16
+ the same ~30-line shape independently: a Context holding `{settings, set}`, a Provider taking
17
+ `initialValue`/`onChange` so the app decides where (or whether) the setting is persisted, and an
18
+ inert no-Provider-mounted default so nothing crashes if it's used without one. Each of those same
19
+ three packages *also* independently hand-rolls an equivalent Redux slice — the exact same
20
+ `createAction`/`.match()`/if-chain reducer, copy-pasted verbatim across all four files that need it
21
+ (`hapticSlice.ts`, `soundSlice.ts`, `scrollViewSlice.ts`, `themeSlice.ts`). And `drawer`, `scanner`,
22
+ and `resizable-input` each hand-roll the same module-level `let config` / `configureX` / `getXConfig`
23
+ / `XProvider` singleton for one-time optional-peer injection. `createSettingsContext`,
24
+ `createSettingsSlice`, and `createModuleConfig` are each one of those shapes, written once.
25
+
26
+ ## `createSettingsContext` — Context-based settings
27
+
28
+ ```ts
29
+ import { createSettingsContext } from '@rific/core'
30
+
31
+ export type OrientationLockSettings = { locked: boolean }
32
+
33
+ export const { Provider: OrientationLockProvider, useSettings: useOrientationLockSettings } =
34
+ createSettingsContext<OrientationLockSettings>({ locked: false })
35
+ ```
36
+
37
+ ```tsx
38
+ // App root
39
+ <OrientationLockProvider
40
+ initialValue={{ locked: persistedLockOrientation }}
41
+ onChange={(settings) => dispatch(gameActions.setLockOrientation(settings.locked))}
42
+ >
43
+ <App />
44
+ </OrientationLockProvider>
45
+ ```
46
+
47
+ For the common case of a caller that only needs one field of the settings object as a plain
48
+ `{value, setValue}` pair (mirrors `useOrientationLock`/`useSoundSettings`/`useHapticSettings`'s own
49
+ shape), wrap the generated `useSettings` with `createSettingHook`:
50
+
51
+ ```ts
52
+ import { createSettingHook } from '@rific/core'
53
+
54
+ export const useOrientationLock = createSettingHook(useOrientationLockSettings, 'locked')
55
+ // const { value: locked, setValue: setLocked } = useOrientationLock()
56
+ ```
57
+
58
+ A settings object with several fields a caller patches together at once (e.g. sound + haptics) is
59
+ better served calling the generated `useSettings()` directly and patching as needed, rather than one
60
+ `createSettingHook` per field.
61
+
62
+ ## `createSettingsSlice` — Redux-based settings
63
+
64
+ Same settings shape, for apps that persist through Redux instead of (or alongside) a Context. No
65
+ dependency on `@reduxjs/toolkit`: works with RTK stores, vanilla Redux, or any reducer-based state
66
+ container.
67
+
68
+ ```ts
69
+ import { createSettingsSlice } from '@rific/core'
70
+
71
+ export type HapticSettings = { vibrate: boolean }
72
+
73
+ export const { actions: hapticActions, reducer: hapticReducer } = createSettingsSlice('haptic', {
74
+ initialState: { vibrate: true } as HapticSettings
75
+ })
76
+ // hapticActions.initialize({ vibrate: false }), hapticActions.setVibrate(false)
77
+ ```
78
+
79
+ By default every field of `initialState` gets its own `setX` action (`setVibrate`, `setEnabled`,
80
+ ...) and `initialize` replaces the whole state wholesale — matching the fleet's most common shape.
81
+ Three optional knobs cover the rest:
82
+
83
+ ```ts
84
+ export const { actions: themeActions, reducer: themeReducer, createReducer: createThemeReducer, selectors } = createSettingsSlice('theme', {
85
+ initialState: { appearance: 'system', blur: true, color: '#6750a4', harmony: 'split-complementary' } as ThemeState,
86
+ initializeMode: 'merge', // initialize(patch) merges instead of replacing
87
+ selectors: ['appearance', 'color'] // adds selectAppearance(state)/selectColor(state)
88
+ })
89
+
90
+ // createReducer lets a consumer override the default initial state at construction time —
91
+ // independent of the runtime `initialize` action:
92
+ const reducer = createThemeReducer({ color: DEFAULT_APP_COLOR })
93
+ ```
94
+
95
+ To expose zero per-field setters (only `initialize`), pass `fieldSetters: []` — useful for a
96
+ settings object an app only ever replaces wholesale, never patches one field at a time.
97
+
98
+ ## `createModuleConfig` — optional peer-module injection
99
+
100
+ ```ts
101
+ import { createModuleConfig } from '@rific/core'
102
+ import type { OptionalModule } from '@rific/core'
103
+
104
+ export type MyPackageConfig = {
105
+ paper?: OptionalModule<PaperModuleShape>
106
+ }
107
+
108
+ export const { configure: configureMyPackage, getConfig: getMyPackageConfig, Provider: MyPackageProvider } =
109
+ createModuleConfig<MyPackageConfig>()
110
+ ```
111
+
112
+ ```tsx
113
+ // App root — either call configure directly, or mount the generated Provider:
114
+ <MyPackageProvider paper={RNPaper}>
115
+ <App />
116
+ </MyPackageProvider>
117
+ ```
118
+
119
+ Plain module-level state, not React Context — this is one-time app setup ("does this app have
120
+ `react-native-paper`?"), not per-render reactive state, so calling `configure` again after
121
+ components have already rendered won't retroactively update them. Fine for startup config, not for
122
+ runtime toggling.
123
+
124
+ ## `OptionalModule<T>`
125
+
126
+ Names the convention several packages already follow for the `paper`/`camera`/`autoPaper`-style
127
+ props above: mirror only the small slice of the injected module's shape you actually use as a local
128
+ type (never `typeof import('the-real-package')`, which still forces the type-checker to resolve the
129
+ real module), accept it as an explicit prop rather than auto-detecting it (Metro doesn't rewrite a
130
+ `require()`-in-`try/catch` call into its module graph inside an ESM build, so auto-detection breaks
131
+ silently), and degrade gracefully when it's omitted. `OptionalModule<T>` is just `T | undefined` —
132
+ its only value is giving that convention one name to point back to instead of re-deriving the
133
+ reasoning fresh in every package's own doc comment.
134
+
135
+ ## Install
136
+
137
+ ```bash
138
+ npm install @rific/core
139
+ ```
140
+
141
+ ## Peer Dependencies
142
+
143
+ - `react` (>=19.0.0)
144
+ - `react-native` (>=0.76.0) — not imported by anything in this package yet (every export here is
145
+ plain `react`), but declared up front to match every other `@rific`/`@tastic` foundation package,
146
+ since this is meant to grow into a home for other cross-cutting utilities the same way
147
+ `@tastic/core` did.
@@ -0,0 +1,74 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ type ModuleConfigProviderProps<T> = T & {
4
+ children: ReactNode;
5
+ };
6
+ type ModuleConfigResult<T extends object> = {
7
+ configure: (next: Partial<T>) => void;
8
+ getConfig: () => T;
9
+ Provider: (props: ModuleConfigProviderProps<T>) => React.JSX.Element;
10
+ };
11
+ declare function createModuleConfig<T extends object>(defaults?: T): ModuleConfigResult<T>;
12
+
13
+ type SettingsContextValue<T> = {
14
+ settings: T;
15
+ set: (patch: Partial<T>) => void;
16
+ };
17
+ type SettingsProviderProps<T> = {
18
+ children: ReactNode;
19
+ initialValue?: Partial<T>;
20
+ onChange?: (settings: T) => void;
21
+ };
22
+ type SettingsContextResult<T> = {
23
+ Context: React.Context<SettingsContextValue<T>>;
24
+ Provider: (props: SettingsProviderProps<T>) => React.JSX.Element;
25
+ useSettings: () => SettingsContextValue<T>;
26
+ };
27
+ declare function createSettingsContext<T extends object>(defaults: T): SettingsContextResult<T>;
28
+ declare function createSettingHook<T extends object, K extends keyof T>(useSettings: () => SettingsContextValue<T>, key: K): () => {
29
+ value: T[K];
30
+ setValue: (value: T[K]) => void;
31
+ };
32
+
33
+ type SettingsAction<P> = {
34
+ payload: P;
35
+ type: string;
36
+ };
37
+ type ActionCreator<P> = {
38
+ (payload: P): SettingsAction<P>;
39
+ type: string;
40
+ match: (action: {
41
+ type: string;
42
+ }) => action is SettingsAction<P>;
43
+ };
44
+ type Capitalized<K extends PropertyKey> = K extends string ? Capitalize<K> : never;
45
+ type FieldSetterActions<T, Fields extends keyof T> = {
46
+ [K in Fields as `set${Capitalized<K>}`]: ActionCreator<T[K]>;
47
+ };
48
+ type FieldSelectors<T, Fields extends keyof T> = {
49
+ [K in Fields as `select${Capitalized<K>}`]: (state: T) => T[K];
50
+ };
51
+ type SettingsReducer<T> = (state: T | undefined, action: {
52
+ type: string;
53
+ }) => T;
54
+ type InitializeMode = 'replace' | 'merge';
55
+ type InitializePayload<T, Mode extends InitializeMode> = Mode extends 'merge' ? Partial<T> : T;
56
+ type CreateSettingsSliceOptions<T extends object, Mode extends InitializeMode, SetterFields extends keyof T, SelectorFields extends keyof T> = {
57
+ initialState: T;
58
+ initializeMode?: Mode;
59
+ fieldSetters?: SetterFields[];
60
+ selectors?: SelectorFields[];
61
+ };
62
+ type SettingsSliceResult<T extends object, Mode extends InitializeMode, SetterFields extends keyof T, SelectorFields extends keyof T> = {
63
+ actions: {
64
+ initialize: ActionCreator<InitializePayload<T, Mode>>;
65
+ } & FieldSetterActions<T, SetterFields>;
66
+ reducer: SettingsReducer<T>;
67
+ createReducer: (overrideInitialState?: Partial<T>) => SettingsReducer<T>;
68
+ selectors: FieldSelectors<T, SelectorFields>;
69
+ };
70
+ declare function createSettingsSlice<T extends object, Mode extends InitializeMode = 'replace', SetterFields extends keyof T = keyof T, SelectorFields extends keyof T = never>(namespace: string, options: CreateSettingsSliceOptions<T, Mode, SetterFields, SelectorFields>): SettingsSliceResult<T, Mode, SetterFields, SelectorFields>;
71
+
72
+ type OptionalModule<T> = T | undefined;
73
+
74
+ export { type CreateSettingsSliceOptions, type InitializeMode, type ModuleConfigProviderProps, type ModuleConfigResult, type OptionalModule, type SettingsAction, type SettingsContextResult, type SettingsContextValue, type SettingsProviderProps, type SettingsReducer, type SettingsSliceResult, createModuleConfig, createSettingHook, createSettingsContext, createSettingsSlice };
@@ -0,0 +1,74 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ type ModuleConfigProviderProps<T> = T & {
4
+ children: ReactNode;
5
+ };
6
+ type ModuleConfigResult<T extends object> = {
7
+ configure: (next: Partial<T>) => void;
8
+ getConfig: () => T;
9
+ Provider: (props: ModuleConfigProviderProps<T>) => React.JSX.Element;
10
+ };
11
+ declare function createModuleConfig<T extends object>(defaults?: T): ModuleConfigResult<T>;
12
+
13
+ type SettingsContextValue<T> = {
14
+ settings: T;
15
+ set: (patch: Partial<T>) => void;
16
+ };
17
+ type SettingsProviderProps<T> = {
18
+ children: ReactNode;
19
+ initialValue?: Partial<T>;
20
+ onChange?: (settings: T) => void;
21
+ };
22
+ type SettingsContextResult<T> = {
23
+ Context: React.Context<SettingsContextValue<T>>;
24
+ Provider: (props: SettingsProviderProps<T>) => React.JSX.Element;
25
+ useSettings: () => SettingsContextValue<T>;
26
+ };
27
+ declare function createSettingsContext<T extends object>(defaults: T): SettingsContextResult<T>;
28
+ declare function createSettingHook<T extends object, K extends keyof T>(useSettings: () => SettingsContextValue<T>, key: K): () => {
29
+ value: T[K];
30
+ setValue: (value: T[K]) => void;
31
+ };
32
+
33
+ type SettingsAction<P> = {
34
+ payload: P;
35
+ type: string;
36
+ };
37
+ type ActionCreator<P> = {
38
+ (payload: P): SettingsAction<P>;
39
+ type: string;
40
+ match: (action: {
41
+ type: string;
42
+ }) => action is SettingsAction<P>;
43
+ };
44
+ type Capitalized<K extends PropertyKey> = K extends string ? Capitalize<K> : never;
45
+ type FieldSetterActions<T, Fields extends keyof T> = {
46
+ [K in Fields as `set${Capitalized<K>}`]: ActionCreator<T[K]>;
47
+ };
48
+ type FieldSelectors<T, Fields extends keyof T> = {
49
+ [K in Fields as `select${Capitalized<K>}`]: (state: T) => T[K];
50
+ };
51
+ type SettingsReducer<T> = (state: T | undefined, action: {
52
+ type: string;
53
+ }) => T;
54
+ type InitializeMode = 'replace' | 'merge';
55
+ type InitializePayload<T, Mode extends InitializeMode> = Mode extends 'merge' ? Partial<T> : T;
56
+ type CreateSettingsSliceOptions<T extends object, Mode extends InitializeMode, SetterFields extends keyof T, SelectorFields extends keyof T> = {
57
+ initialState: T;
58
+ initializeMode?: Mode;
59
+ fieldSetters?: SetterFields[];
60
+ selectors?: SelectorFields[];
61
+ };
62
+ type SettingsSliceResult<T extends object, Mode extends InitializeMode, SetterFields extends keyof T, SelectorFields extends keyof T> = {
63
+ actions: {
64
+ initialize: ActionCreator<InitializePayload<T, Mode>>;
65
+ } & FieldSetterActions<T, SetterFields>;
66
+ reducer: SettingsReducer<T>;
67
+ createReducer: (overrideInitialState?: Partial<T>) => SettingsReducer<T>;
68
+ selectors: FieldSelectors<T, SelectorFields>;
69
+ };
70
+ declare function createSettingsSlice<T extends object, Mode extends InitializeMode = 'replace', SetterFields extends keyof T = keyof T, SelectorFields extends keyof T = never>(namespace: string, options: CreateSettingsSliceOptions<T, Mode, SetterFields, SelectorFields>): SettingsSliceResult<T, Mode, SetterFields, SelectorFields>;
71
+
72
+ type OptionalModule<T> = T | undefined;
73
+
74
+ export { type CreateSettingsSliceOptions, type InitializeMode, type ModuleConfigProviderProps, type ModuleConfigResult, type OptionalModule, type SettingsAction, type SettingsContextResult, type SettingsContextValue, type SettingsProviderProps, type SettingsReducer, type SettingsSliceResult, createModuleConfig, createSettingHook, createSettingsContext, createSettingsSlice };
package/dist/index.js ADDED
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ createModuleConfig: () => createModuleConfig,
24
+ createSettingHook: () => createSettingHook,
25
+ createSettingsContext: () => createSettingsContext,
26
+ createSettingsSlice: () => createSettingsSlice
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+
30
+ // src/createModuleConfig.tsx
31
+ var import_jsx_runtime = require("react/jsx-runtime");
32
+ function createModuleConfig(defaults = {}) {
33
+ let config = defaults;
34
+ const configure = (next) => {
35
+ config = { ...config, ...next };
36
+ };
37
+ const getConfig = () => config;
38
+ function Provider({ children, ...rest }) {
39
+ configure(rest);
40
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children });
41
+ }
42
+ return { configure, getConfig, Provider };
43
+ }
44
+
45
+ // src/createSettingsContext.tsx
46
+ var import_react = require("react");
47
+ var import_jsx_runtime2 = require("react/jsx-runtime");
48
+ function createSettingsContext(defaults) {
49
+ const Context = (0, import_react.createContext)({ settings: defaults, set: () => {
50
+ } });
51
+ function Provider({ children, initialValue, onChange }) {
52
+ const [settings, setSettings] = (0, import_react.useState)(() => ({ ...defaults, ...initialValue }));
53
+ const onChangeRef = (0, import_react.useRef)(onChange);
54
+ onChangeRef.current = onChange;
55
+ const set = (0, import_react.useCallback)((patch) => {
56
+ setSettings((prev) => {
57
+ const next = { ...prev, ...patch };
58
+ onChangeRef.current?.(next);
59
+ return next;
60
+ });
61
+ }, []);
62
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Context.Provider, { value: { settings, set }, children });
63
+ }
64
+ function useSettings() {
65
+ return (0, import_react.useContext)(Context);
66
+ }
67
+ return { Context, Provider, useSettings };
68
+ }
69
+ function createSettingHook(useSettings, key) {
70
+ return function useSetting() {
71
+ const { settings, set } = useSettings();
72
+ const setValue = (0, import_react.useCallback)((value) => set({ [key]: value }), [set]);
73
+ return { value: settings[key], setValue };
74
+ };
75
+ }
76
+
77
+ // src/createSettingsSlice.ts
78
+ function capitalize(key) {
79
+ const s = String(key);
80
+ return s.charAt(0).toUpperCase() + s.slice(1);
81
+ }
82
+ function createActionCreator(type) {
83
+ const actionCreator = ((payload) => ({ payload, type }));
84
+ actionCreator.type = type;
85
+ actionCreator.match = (action) => action.type === type;
86
+ return actionCreator;
87
+ }
88
+ function createSettingsSlice(namespace, options) {
89
+ const { initialState, selectors = [] } = options;
90
+ const initializeMode = options.initializeMode ?? "replace";
91
+ const fieldSetters = options.fieldSetters ?? Object.keys(initialState);
92
+ const initialize = createActionCreator(`${namespace}/initialize`);
93
+ const setterEntries = fieldSetters.map((field) => {
94
+ const actionName = `set${capitalize(field)}`;
95
+ const creator = createActionCreator(`${namespace}/${actionName}`);
96
+ return { actionName, field, creator };
97
+ });
98
+ const actions = {
99
+ initialize,
100
+ ...Object.fromEntries(setterEntries.map(({ actionName, creator }) => [actionName, creator]))
101
+ };
102
+ const reduce = (state, action) => {
103
+ if (initialize.match(action)) return initializeMode === "merge" ? { ...state, ...action.payload } : action.payload;
104
+ for (const { field, creator } of setterEntries) {
105
+ if (creator.match(action)) return { ...state, [field]: action.payload };
106
+ }
107
+ return state;
108
+ };
109
+ function createReducer(overrideInitialState) {
110
+ const initial = { ...initialState, ...overrideInitialState };
111
+ return (state = initial, action) => reduce(state, action);
112
+ }
113
+ const selectorsObj = Object.fromEntries(selectors.map((field) => [`select${capitalize(field)}`, (state) => state[field]]));
114
+ return { actions, reducer: createReducer(), createReducer, selectors: selectorsObj };
115
+ }
116
+ // Annotate the CommonJS export names for ESM import in node:
117
+ 0 && (module.exports = {
118
+ createModuleConfig,
119
+ createSettingHook,
120
+ createSettingsContext,
121
+ createSettingsSlice
122
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,92 @@
1
+ // src/createModuleConfig.tsx
2
+ import { Fragment, jsx } from "react/jsx-runtime";
3
+ function createModuleConfig(defaults = {}) {
4
+ let config = defaults;
5
+ const configure = (next) => {
6
+ config = { ...config, ...next };
7
+ };
8
+ const getConfig = () => config;
9
+ function Provider({ children, ...rest }) {
10
+ configure(rest);
11
+ return /* @__PURE__ */ jsx(Fragment, { children });
12
+ }
13
+ return { configure, getConfig, Provider };
14
+ }
15
+
16
+ // src/createSettingsContext.tsx
17
+ import { createContext, useCallback, useContext, useRef, useState } from "react";
18
+ import { jsx as jsx2 } from "react/jsx-runtime";
19
+ function createSettingsContext(defaults) {
20
+ const Context = createContext({ settings: defaults, set: () => {
21
+ } });
22
+ function Provider({ children, initialValue, onChange }) {
23
+ const [settings, setSettings] = useState(() => ({ ...defaults, ...initialValue }));
24
+ const onChangeRef = useRef(onChange);
25
+ onChangeRef.current = onChange;
26
+ const set = useCallback((patch) => {
27
+ setSettings((prev) => {
28
+ const next = { ...prev, ...patch };
29
+ onChangeRef.current?.(next);
30
+ return next;
31
+ });
32
+ }, []);
33
+ return /* @__PURE__ */ jsx2(Context.Provider, { value: { settings, set }, children });
34
+ }
35
+ function useSettings() {
36
+ return useContext(Context);
37
+ }
38
+ return { Context, Provider, useSettings };
39
+ }
40
+ function createSettingHook(useSettings, key) {
41
+ return function useSetting() {
42
+ const { settings, set } = useSettings();
43
+ const setValue = useCallback((value) => set({ [key]: value }), [set]);
44
+ return { value: settings[key], setValue };
45
+ };
46
+ }
47
+
48
+ // src/createSettingsSlice.ts
49
+ function capitalize(key) {
50
+ const s = String(key);
51
+ return s.charAt(0).toUpperCase() + s.slice(1);
52
+ }
53
+ function createActionCreator(type) {
54
+ const actionCreator = ((payload) => ({ payload, type }));
55
+ actionCreator.type = type;
56
+ actionCreator.match = (action) => action.type === type;
57
+ return actionCreator;
58
+ }
59
+ function createSettingsSlice(namespace, options) {
60
+ const { initialState, selectors = [] } = options;
61
+ const initializeMode = options.initializeMode ?? "replace";
62
+ const fieldSetters = options.fieldSetters ?? Object.keys(initialState);
63
+ const initialize = createActionCreator(`${namespace}/initialize`);
64
+ const setterEntries = fieldSetters.map((field) => {
65
+ const actionName = `set${capitalize(field)}`;
66
+ const creator = createActionCreator(`${namespace}/${actionName}`);
67
+ return { actionName, field, creator };
68
+ });
69
+ const actions = {
70
+ initialize,
71
+ ...Object.fromEntries(setterEntries.map(({ actionName, creator }) => [actionName, creator]))
72
+ };
73
+ const reduce = (state, action) => {
74
+ if (initialize.match(action)) return initializeMode === "merge" ? { ...state, ...action.payload } : action.payload;
75
+ for (const { field, creator } of setterEntries) {
76
+ if (creator.match(action)) return { ...state, [field]: action.payload };
77
+ }
78
+ return state;
79
+ };
80
+ function createReducer(overrideInitialState) {
81
+ const initial = { ...initialState, ...overrideInitialState };
82
+ return (state = initial, action) => reduce(state, action);
83
+ }
84
+ const selectorsObj = Object.fromEntries(selectors.map((field) => [`select${capitalize(field)}`, (state) => state[field]]));
85
+ return { actions, reducer: createReducer(), createReducer, selectors: selectorsObj };
86
+ }
87
+ export {
88
+ createModuleConfig,
89
+ createSettingHook,
90
+ createSettingsContext,
91
+ createSettingsSlice
92
+ };
package/package.json ADDED
@@ -0,0 +1,89 @@
1
+ {
2
+ "name": "@rific/core",
3
+ "version": "0.1.0",
4
+ "description": "Required foundation for @rific/* packages: createSettingsContext + createSettingsSlice for a live, patchable settings object (Context and Redux, respectively), and createModuleConfig for one-time optional-peer-module injection",
5
+ "keywords": [
6
+ "react-native",
7
+ "react",
8
+ "settings",
9
+ "context",
10
+ "provider",
11
+ "state",
12
+ "redux"
13
+ ],
14
+ "homepage": "https://github.com/jayrdeaton/react-native-core#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/jayrdeaton/react-native-core/issues"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/jayrdeaton/react-native-core.git"
21
+ },
22
+ "license": "MIT",
23
+ "author": "Jay Deaton",
24
+ "sideEffects": false,
25
+ "type": "commonjs",
26
+ "exports": {
27
+ ".": {
28
+ "react-native": "./src/index.ts",
29
+ "browser": "./src/index.ts",
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.mjs",
32
+ "require": "./dist/index.js"
33
+ }
34
+ },
35
+ "main": "dist/index.js",
36
+ "module": "dist/index.mjs",
37
+ "react-native": "src/index.ts",
38
+ "types": "dist/index.d.ts",
39
+ "files": [
40
+ "dist",
41
+ "src",
42
+ "!src/__tests__",
43
+ "!src/__mocks__"
44
+ ],
45
+ "scripts": {
46
+ "build": "tsup",
47
+ "build:watch": "tsup --watch",
48
+ "fix": "eslint --fix",
49
+ "lint": "eslint",
50
+ "prepublishOnly": "npm run build",
51
+ "release": "git push --follow-tags",
52
+ "release:major": "npm version major && npm run release",
53
+ "release:minor": "npm version minor && npm run release",
54
+ "release:patch": "npm version patch && npm run release",
55
+ "test": "jest",
56
+ "test:watch": "jest --watchAll",
57
+ "typecheck": "tsc --noEmit",
58
+ "verify": "npm run lint && npm test && npm run typecheck && npm run build",
59
+ "preversion": "npm run verify"
60
+ },
61
+ "prettier": "@infinitetoken/eslint-config/prettier",
62
+ "devDependencies": {
63
+ "@infinitetoken/eslint-config": "^0.2.0",
64
+ "@infinitetoken/jest-config": "^0.3.0",
65
+ "@infinitetoken/tsconfig": "^0.4.1",
66
+ "@testing-library/dom": "^10.4.1",
67
+ "@testing-library/react": "^16.3.2",
68
+ "@types/jest": "^30.0.0",
69
+ "@types/react": "^19.0.0",
70
+ "eslint": "^9.39.4",
71
+ "eslint-plugin-react-hooks": "^7.1.1",
72
+ "eslint-plugin-react-native": "^5.0.0",
73
+ "jest": "^30.4.2",
74
+ "jest-environment-jsdom": "^30.4.1",
75
+ "prettier": "^3.8.3",
76
+ "react": "^19.0.0",
77
+ "react-dom": "^19.0.0",
78
+ "react-native": "^0.85.3",
79
+ "tsup": "^8.0.0",
80
+ "typescript": "^6.0.3"
81
+ },
82
+ "peerDependencies": {
83
+ "react": ">=19.0.0",
84
+ "react-native": ">=0.76.0"
85
+ },
86
+ "publishConfig": {
87
+ "access": "public"
88
+ }
89
+ }
@@ -0,0 +1,19 @@
1
+ // Names the convention feedback-press, drawer, scanner, and auto-paper each already follow (and
2
+ // each re-explain from scratch in their own doc comments) for injecting an optional peer module
3
+ // (react-native-paper, expo-camera, expo-blur, react-native-reanimated, etc.) without a hard
4
+ // dependency on it:
5
+ //
6
+ // 1. Auto-detection via a require()-in-try/catch call doesn't work reliably: Metro doesn't rewrite
7
+ // that call into its module graph inside an ESM (.mjs) build, so module-level auto-detection
8
+ // silently breaks the moment a consumer's bundler resolves the package's ESM entry point.
9
+ // 2. So instead, accept the module as an explicit injected prop (e.g. `paper?: OptionalModule<PaperModuleShape>`)
10
+ // and have the consuming app pass `import * as RNPaper from 'react-native-paper'` itself.
11
+ // 3. Mirror only the small slice of the module's shape you actually use as a local type — never
12
+ // `typeof import('the-real-package')`, which still forces the type-checker to resolve the real
13
+ // module and defeats the point of not hard-depending on it.
14
+ // 4. Degrade gracefully when the prop is omitted (a plain fallback UI, a no-op), never throw.
15
+ //
16
+ // This type is deliberately trivial (`T | undefined`) — its value is giving that convention one
17
+ // name every package's own doc comment can point back to, instead of re-deriving the same
18
+ // reasoning independently each time a new optional peer gets injected.
19
+ export type OptionalModule<T> = T | undefined
@@ -0,0 +1,42 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ // Generalizes the module-level config-singleton shape duplicated identically across
4
+ // @rific/drawer's DrawerConfig.tsx, @rific/scanner's ScannerConfig.tsx, and
5
+ // @rific/resizable-input's ResizableInputConfig.tsx — each hand-rolls the same five pieces
6
+ // (a module-level `let config`, `configureX`/`getXConfig`, an `XProviderProps` type, and an
7
+ // `XProvider` that just calls `configureX` synchronously during render) to inject optional peer
8
+ // modules (react-native-paper, expo-camera, etc.) without a hard dependency on them. Plain
9
+ // module-level state rather than React Context, deliberately: this is one-time app setup ("does
10
+ // this app have react-native-paper?"), not per-render reactive state — see each real package's own
11
+ // comment on why a Provider-only API would be more ceremony than the problem needs. Not reactive:
12
+ // calling `configure` again after components have already rendered won't retroactively update
13
+ // them, which is fine for startup config, not for runtime toggling.
14
+
15
+ export type ModuleConfigProviderProps<T> = T & { children: ReactNode }
16
+
17
+ export type ModuleConfigResult<T extends object> = {
18
+ configure: (next: Partial<T>) => void
19
+ getConfig: () => T
20
+ // Thin wrapper around `configure` for consumers who'd rather mount a Provider than call the
21
+ // setup function directly. Calls `configure` synchronously during render (not in an effect), so
22
+ // the config is already set by the time any descendant component renders — effects run
23
+ // bottom-up after children have already rendered once, which would be one render too late here.
24
+ Provider: (props: ModuleConfigProviderProps<T>) => React.JSX.Element
25
+ }
26
+
27
+ export function createModuleConfig<T extends object>(defaults: T = {} as T): ModuleConfigResult<T> {
28
+ let config: T = defaults
29
+
30
+ const configure = (next: Partial<T>) => {
31
+ config = { ...config, ...next }
32
+ }
33
+
34
+ const getConfig = (): T => config
35
+
36
+ function Provider({ children, ...rest }: ModuleConfigProviderProps<T>) {
37
+ configure(rest as Partial<T>)
38
+ return <>{children}</>
39
+ }
40
+
41
+ return { configure, getConfig, Provider }
42
+ }
@@ -0,0 +1,75 @@
1
+ import { createContext, type ReactNode, useCallback, useContext, useRef, useState } from 'react'
2
+
3
+ export type SettingsContextValue<T> = {
4
+ settings: T
5
+ set: (patch: Partial<T>) => void
6
+ }
7
+
8
+ export type SettingsProviderProps<T> = {
9
+ children: ReactNode
10
+ // Rehydrates from the consuming app's own storage at mount time — this factory (like every
11
+ // hand-rolled settings Provider it replaces) deliberately does no persistence of its own. The app
12
+ // decides where, or whether, `settings` gets saved.
13
+ initialValue?: Partial<T>
14
+ // Fires with the full settings object on every `set` call — the app's hook into persisting it.
15
+ onChange?: (settings: T) => void
16
+ }
17
+
18
+ export type SettingsContextResult<T> = {
19
+ Context: React.Context<SettingsContextValue<T>>
20
+ Provider: (props: SettingsProviderProps<T>) => React.JSX.Element
21
+ useSettings: () => SettingsContextValue<T>
22
+ }
23
+
24
+ // Builds one {Context, Provider, useSettings} triple for a single settings shape T. Matches the
25
+ // initialValue/onChange/patch-based contract already duplicated by hand, near-identically, across
26
+ // this fleet's own settings Providers (feedback-press's sound + haptics, scroll-view's own settings,
27
+ // auto-paper's ThemeProvider, @tastic/core's orientation-lock context) — every one of those is this
28
+ // same ~30-line shape typed out separately. `defaults` seeds both the Context's own inert
29
+ // no-Provider-mounted fallback (settings read as `defaults`, `set` silently drops — the same
30
+ // "nothing crashes, it just never resolves" degradation every one of those packages already
31
+ // documents for a missing Provider) and the Provider's real initial state, patched by `initialValue`.
32
+ export function createSettingsContext<T extends object>(defaults: T): SettingsContextResult<T> {
33
+ const Context = createContext<SettingsContextValue<T>>({ settings: defaults, set: () => {} })
34
+
35
+ function Provider({ children, initialValue, onChange }: SettingsProviderProps<T>) {
36
+ const [settings, setSettings] = useState<T>(() => ({ ...defaults, ...initialValue }))
37
+
38
+ // Read via a ref rather than closed over directly, so `set`'s identity never changes just
39
+ // because the caller passed a fresh `onChange` closure this render — a consumer that memoizes
40
+ // off `set`'s stability (or hands it deep into a tree) doesn't re-render every time the app's
41
+ // own onChange callback is redefined.
42
+ const onChangeRef = useRef(onChange)
43
+ onChangeRef.current = onChange
44
+
45
+ const set = useCallback((patch: Partial<T>) => {
46
+ setSettings((prev) => {
47
+ const next = { ...prev, ...patch }
48
+ onChangeRef.current?.(next)
49
+ return next
50
+ })
51
+ }, [])
52
+
53
+ return <Context.Provider value={{ settings, set }}>{children}</Context.Provider>
54
+ }
55
+
56
+ function useSettings(): SettingsContextValue<T> {
57
+ return useContext(Context)
58
+ }
59
+
60
+ return { Context, Provider, useSettings }
61
+ }
62
+
63
+ // Thin convenience for the common single-field case — the {value, setValue} shape this fleet's own
64
+ // useOrientationLock/useSoundSettings/useHapticSettings hooks each hand-roll individually, for a
65
+ // caller that only cares about one field of a larger settings object rather than the whole
66
+ // patch-based contract. Not every consumer needs this — one juggling several fields at once (e.g.
67
+ // feedback-press's own sound/haptic sub-settings) is still better served calling useSettings()
68
+ // directly and patching as needed.
69
+ export function createSettingHook<T extends object, K extends keyof T>(useSettings: () => SettingsContextValue<T>, key: K) {
70
+ return function useSetting(): { value: T[K]; setValue: (value: T[K]) => void } {
71
+ const { settings, set } = useSettings()
72
+ const setValue = useCallback((value: T[K]) => set({ [key]: value } as unknown as Partial<T>), [set])
73
+ return { value: settings[key], setValue }
74
+ }
75
+ }
@@ -0,0 +1,113 @@
1
+ // Generalizes the Redux slice half of the same duplicated shape createSettingsContext already
2
+ // covers for the Context half. feedback-press's hapticSlice.ts/soundSlice.ts, scroll-view's
3
+ // scrollViewSlice.ts, and auto-paper's themeSlice.ts each hand-roll an identical createAction/
4
+ // .match()/if-chain reducer — the exact block below is copy-pasted verbatim across all four today.
5
+ // This factory reproduces every real behavior those four files need (including auto-paper's own
6
+ // wrinkles: a merging `initialize`, an overridable reducer factory, and per-field selectors) as
7
+ // plain config, not new logic.
8
+
9
+ export type SettingsAction<P> = { payload: P; type: string }
10
+
11
+ type ActionCreator<P> = {
12
+ (payload: P): SettingsAction<P>
13
+ type: string
14
+ match: (action: { type: string }) => action is SettingsAction<P>
15
+ }
16
+
17
+ type Capitalized<K extends PropertyKey> = K extends string ? Capitalize<K> : never
18
+
19
+ type FieldSetterActions<T, Fields extends keyof T> = {
20
+ [K in Fields as `set${Capitalized<K>}`]: ActionCreator<T[K]>
21
+ }
22
+
23
+ type FieldSelectors<T, Fields extends keyof T> = {
24
+ [K in Fields as `select${Capitalized<K>}`]: (state: T) => T[K]
25
+ }
26
+
27
+ export type SettingsReducer<T> = (state: T | undefined, action: { type: string }) => T
28
+
29
+ // 'replace' (the default, and what haptic/sound/scrollView all do): `initialize`'s payload becomes
30
+ // the entire next state, verbatim — matches how every real call site already dispatches it (the
31
+ // app's own onChange handler always hands back a complete settings object, never a partial one),
32
+ // so `initialize` takes the full T. 'merge' (only auto-paper's theme needs this): the payload is
33
+ // spread over the existing state instead, so `initialize` takes a Partial<T> — a caller can seed
34
+ // just part of it.
35
+ export type InitializeMode = 'replace' | 'merge'
36
+
37
+ type InitializePayload<T, Mode extends InitializeMode> = Mode extends 'merge' ? Partial<T> : T
38
+
39
+ export type CreateSettingsSliceOptions<T extends object, Mode extends InitializeMode, SetterFields extends keyof T, SelectorFields extends keyof T> = {
40
+ initialState: T
41
+ initializeMode?: Mode
42
+ // Which fields get a dedicated setX action (e.g. `vibrate` -> `setVibrate`). Defaults to every key
43
+ // of initialState — matches haptic/sound/theme, which each expose one setter per field.
44
+ // scrollView needs this passed as `[]` explicitly: it has 6 fields but zero per-field setters,
45
+ // relying on `initialize` alone.
46
+ fieldSetters?: SetterFields[]
47
+ // Which fields get a generated selectX(state) => state[field]. Defaults to none — only auto-paper's
48
+ // theme slice has these today.
49
+ selectors?: SelectorFields[]
50
+ }
51
+
52
+ export type SettingsSliceResult<T extends object, Mode extends InitializeMode, SetterFields extends keyof T, SelectorFields extends keyof T> = {
53
+ actions: { initialize: ActionCreator<InitializePayload<T, Mode>> } & FieldSetterActions<T, SetterFields>
54
+ reducer: SettingsReducer<T>
55
+ // The auto-paper wrinkle: a factory that lets a consumer override the default initial state at
56
+ // construction time (e.g. seeding a per-app default color), independent of the runtime `initialize`
57
+ // action. `reducer` above is just `createReducer()` with no override.
58
+ createReducer: (overrideInitialState?: Partial<T>) => SettingsReducer<T>
59
+ selectors: FieldSelectors<T, SelectorFields>
60
+ }
61
+
62
+ function capitalize<K extends PropertyKey>(key: K): Capitalized<K> {
63
+ const s = String(key)
64
+ return (s.charAt(0).toUpperCase() + s.slice(1)) as Capitalized<K>
65
+ }
66
+
67
+ function createActionCreator<P>(type: string): ActionCreator<P> {
68
+ const actionCreator = ((payload: P) => ({ payload, type })) as ActionCreator<P>
69
+ actionCreator.type = type
70
+ actionCreator.match = (action: { type: string }): action is SettingsAction<P> => action.type === type
71
+ return actionCreator
72
+ }
73
+
74
+ // `namespace` prefixes every action type (e.g. 'haptic/initialize', 'theme/setAppearance') so
75
+ // multiple slices combined in one store never collide — matches the fleet's existing convention
76
+ // exactly (hapticSlice's own action types are literally 'haptic/initialize' etc. today).
77
+ export function createSettingsSlice<T extends object, Mode extends InitializeMode = 'replace', SetterFields extends keyof T = keyof T, SelectorFields extends keyof T = never>(namespace: string, options: CreateSettingsSliceOptions<T, Mode, SetterFields, SelectorFields>): SettingsSliceResult<T, Mode, SetterFields, SelectorFields> {
78
+ const { initialState, selectors = [] } = options
79
+ const initializeMode: InitializeMode = options.initializeMode ?? 'replace'
80
+ const fieldSetters = options.fieldSetters ?? (Object.keys(initialState) as SetterFields[])
81
+
82
+ // Internally untyped (any payload) — the precise conditional typing based on `Mode` lives only
83
+ // in the exported return type below; the runtime logic itself doesn't need to know Mode at the
84
+ // type level, only the `initializeMode` string value it already has.
85
+ const initialize = createActionCreator<Partial<T> | T>(`${namespace}/initialize`)
86
+ const setterEntries = fieldSetters.map((field) => {
87
+ const actionName = `set${capitalize(field)}`
88
+ const creator = createActionCreator<T[typeof field]>(`${namespace}/${actionName}`)
89
+ return { actionName, field, creator }
90
+ })
91
+
92
+ const actions = {
93
+ initialize,
94
+ ...Object.fromEntries(setterEntries.map(({ actionName, creator }) => [actionName, creator]))
95
+ } as unknown as SettingsSliceResult<T, Mode, SetterFields, SelectorFields>['actions']
96
+
97
+ const reduce = (state: T, action: { type: string }): T => {
98
+ if (initialize.match(action)) return initializeMode === 'merge' ? { ...state, ...(action.payload as Partial<T>) } : (action.payload as T)
99
+ for (const { field, creator } of setterEntries) {
100
+ if (creator.match(action)) return { ...state, [field]: action.payload }
101
+ }
102
+ return state
103
+ }
104
+
105
+ function createReducer(overrideInitialState?: Partial<T>): SettingsReducer<T> {
106
+ const initial = { ...initialState, ...overrideInitialState }
107
+ return (state: T = initial, action: { type: string }): T => reduce(state, action)
108
+ }
109
+
110
+ const selectorsObj = Object.fromEntries(selectors.map((field) => [`select${capitalize(field)}`, (state: T) => state[field]])) as unknown as SettingsSliceResult<T, Mode, SetterFields, SelectorFields>['selectors']
111
+
112
+ return { actions, reducer: createReducer(), createReducer, selectors: selectorsObj }
113
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export type { ModuleConfigProviderProps, ModuleConfigResult } from './createModuleConfig'
2
+ export { createModuleConfig } from './createModuleConfig'
3
+ export type { SettingsContextResult, SettingsContextValue, SettingsProviderProps } from './createSettingsContext'
4
+ export { createSettingHook, createSettingsContext } from './createSettingsContext'
5
+ export type { CreateSettingsSliceOptions, InitializeMode, SettingsAction, SettingsReducer, SettingsSliceResult } from './createSettingsSlice'
6
+ export { createSettingsSlice } from './createSettingsSlice'
7
+ export type { OptionalModule } from './OptionalModule'