alambre 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -1
- package/dist/components/base/SafeAreaView.d.ts +1 -1
- package/dist/components/base/SafeAreaView.js +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/modules/api/api-provider.d.ts +20 -0
- package/dist/modules/api/api-provider.js +124 -0
- package/dist/modules/api/api-types.d.ts +33 -0
- package/dist/modules/api/api-types.js +1 -0
- package/dist/modules/api/api-utils.d.ts +3 -0
- package/dist/modules/api/api-utils.js +23 -0
- package/dist/modules/api/index.d.ts +2 -0
- package/dist/modules/api/index.js +1 -0
- package/dist/modules/base-module.d.ts +12 -0
- package/dist/modules/base-module.js +33 -0
- package/dist/modules/config/config-module.d.ts +10 -0
- package/dist/modules/config/config-module.js +25 -0
- package/dist/modules/config/config-provider.d.ts +7 -0
- package/dist/modules/config/config-provider.js +17 -0
- package/dist/modules/config/config-types.d.ts +7 -0
- package/dist/modules/config/config-types.js +1 -0
- package/dist/modules/config/index.d.ts +3 -0
- package/dist/modules/config/index.js +2 -0
- package/dist/modules/index.d.ts +9 -0
- package/dist/modules/index.js +8 -0
- package/dist/modules/lang/index.d.ts +3 -0
- package/dist/modules/lang/index.js +2 -0
- package/dist/modules/lang/lang-module.d.ts +11 -0
- package/dist/modules/lang/lang-module.js +27 -0
- package/dist/modules/lang/lang-provider.d.ts +7 -0
- package/dist/modules/lang/lang-provider.js +18 -0
- package/dist/modules/lang/lang-types.d.ts +9 -0
- package/dist/modules/lang/lang-types.js +1 -0
- package/dist/modules/module-provider.d.ts +12 -0
- package/dist/modules/module-provider.js +42 -0
- package/dist/modules/navigator/classes/navigator-body.d.ts +3 -0
- package/dist/modules/navigator/classes/navigator-body.js +126 -0
- package/dist/modules/navigator/index.d.ts +3 -0
- package/dist/modules/navigator/index.js +2 -0
- package/dist/modules/navigator/navigator-provider.d.ts +3 -0
- package/dist/modules/navigator/navigator-provider.js +59 -0
- package/dist/modules/navigator/navigator-types.d.ts +19 -0
- package/dist/modules/navigator/navigator-types.js +1 -0
- package/dist/modules/theme/index.d.ts +3 -0
- package/dist/modules/theme/index.js +2 -0
- package/dist/modules/theme/theme-module.d.ts +13 -0
- package/dist/modules/theme/theme-module.js +29 -0
- package/dist/modules/theme/theme-provider.d.ts +7 -0
- package/dist/modules/theme/theme-provider.js +19 -0
- package/dist/modules/theme/theme-types.d.ts +10 -0
- package/dist/modules/theme/theme-types.js +1 -0
- package/dist/modules/toast/index.d.ts +4 -0
- package/dist/modules/toast/index.js +3 -0
- package/dist/modules/toast/toast-body.d.ts +3 -0
- package/dist/modules/toast/toast-body.js +123 -0
- package/dist/modules/toast/toast-context.d.ts +3 -0
- package/dist/modules/toast/toast-context.js +2 -0
- package/dist/modules/toast/toast-provider.d.ts +3 -0
- package/dist/modules/toast/toast-provider.js +12 -0
- package/dist/modules/toast/toast-state.d.ts +7 -0
- package/dist/modules/toast/toast-state.js +86 -0
- package/dist/modules/toast/toast-types.d.ts +59 -0
- package/dist/modules/toast/toast-types.js +1 -0
- package/dist/modules/toast/use-toast.d.ts +2 -0
- package/dist/modules/toast/use-toast.js +8 -0
- package/dist/providers/api-provider/create-api-provider.d.ts +51 -0
- package/dist/providers/api-provider/create-api-provider.js +146 -0
- package/dist/providers/api-provider/index.d.ts +2 -0
- package/dist/providers/api-provider/index.js +1 -0
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.js +1 -0
- package/dist/providers/toast-provider/provider.d.ts +1 -1
- package/dist/providers/toast-provider/provider.js +7 -3
- package/dist/providers/toast-provider/toast-body.d.ts +1 -1
- package/dist/providers/toast-provider/toast-body.js +42 -28
- package/dist/providers/toast-provider/types.d.ts +22 -5
- package/dist/providers/toast-provider/value.d.ts +5 -1
- package/dist/providers/toast-provider/value.js +68 -18
- package/dist/types/navigator.d.ts +2 -0
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -1 +1,104 @@
|
|
|
1
|
-
# alambre
|
|
1
|
+
# alambre
|
|
2
|
+
|
|
3
|
+
`alambre` is a React Native utility library focused on reusable modules, small UI helpers, and navigation/testing playgrounds.
|
|
4
|
+
|
|
5
|
+
This repository includes a working test app (`tests/`) where modules and components are exercised in real screens.
|
|
6
|
+
|
|
7
|
+
## Current Scope
|
|
8
|
+
|
|
9
|
+
- Module factories to keep feature logic encapsulated:
|
|
10
|
+
- `createConfigProvider`
|
|
11
|
+
- `createLangProvider`
|
|
12
|
+
- `createLightDarkProvider`
|
|
13
|
+
- `createApiProvider` (powered by TanStack Query)
|
|
14
|
+
- `NavigatorProvider`
|
|
15
|
+
- `ToastProvider`
|
|
16
|
+
- Navigation test flow with nested pages (`home`, `configuration`, `advance`, `play`, `toast`)
|
|
17
|
+
- Toast provider integration with queue and custom toast component support
|
|
18
|
+
- Theme switching (light/dark) with persisted storage
|
|
19
|
+
- API playground in tests:
|
|
20
|
+
- typed route catalog
|
|
21
|
+
- generated query/mutation hooks from route definitions
|
|
22
|
+
- route execution demo and structured response rendering
|
|
23
|
+
|
|
24
|
+
## Project Structure (high level)
|
|
25
|
+
|
|
26
|
+
- `src/modules/`: module system and domain modules
|
|
27
|
+
- `src/components/`: base UI components and animations
|
|
28
|
+
- `tests/providers/`: test providers for config/lang/theme/api/toast
|
|
29
|
+
- `tests/views/`: test screens grouped by domain
|
|
30
|
+
- `tests/navigator/`: test app navigation config and root test screen
|
|
31
|
+
- `App.tsx`: wires providers and test screens together
|
|
32
|
+
|
|
33
|
+
## Module Architecture
|
|
34
|
+
|
|
35
|
+
The project uses a class-based module architecture that separates state logic from React rendering.
|
|
36
|
+
|
|
37
|
+
### Core pieces
|
|
38
|
+
|
|
39
|
+
- `BaseModule<TState>`:
|
|
40
|
+
- Owns module state.
|
|
41
|
+
- Exposes `getState()` for reads.
|
|
42
|
+
- Exposes `subscribe()` to notify observers on state changes.
|
|
43
|
+
- Exposes protected `setState()` to update state and broadcast updates.
|
|
44
|
+
- Supports optional async initialization via `init()`.
|
|
45
|
+
- Supports cleanup via `dispose()`.
|
|
46
|
+
- `createModuleProvider(...)`:
|
|
47
|
+
- Adapts a `BaseModule` instance to React Context.
|
|
48
|
+
- Creates one module instance per provider lifecycle.
|
|
49
|
+
- Subscribes React state to module updates.
|
|
50
|
+
- Runs `module.init()` on mount.
|
|
51
|
+
- Calls `unsubscribe()` and `module.dispose()` on unmount.
|
|
52
|
+
|
|
53
|
+
### Data flow
|
|
54
|
+
|
|
55
|
+
1. A provider creates one module instance.
|
|
56
|
+
2. React initializes local state with `module.getState()`.
|
|
57
|
+
3. Provider subscribes with `module.subscribe(setState)`.
|
|
58
|
+
4. Module methods call `setState(...)`.
|
|
59
|
+
5. All subscribers are notified and React re-renders.
|
|
60
|
+
6. On unmount, provider unsubscribes and disposes module resources.
|
|
61
|
+
|
|
62
|
+
This keeps state transitions centralized in classes while preserving React's declarative rendering.
|
|
63
|
+
|
|
64
|
+
## Domain Modules
|
|
65
|
+
|
|
66
|
+
- `config`:
|
|
67
|
+
- Persists and updates simple string config fields.
|
|
68
|
+
- Exposes `config` and `saveField`.
|
|
69
|
+
- `lang`:
|
|
70
|
+
- Persists selected language key.
|
|
71
|
+
- Exposes `lang`, `currentLang`, and `changeLang`.
|
|
72
|
+
- `theme`:
|
|
73
|
+
- Persists current theme name (`light` or `dark`).
|
|
74
|
+
- Exposes `theme`, `currThemeName`, `toggleTheme`, and `setThemeName`.
|
|
75
|
+
- `api`:
|
|
76
|
+
- Generates typed routes and query/mutation hooks.
|
|
77
|
+
- Wraps TanStack Query setup and request helpers.
|
|
78
|
+
- `navigator`:
|
|
79
|
+
- Stores page stack and scroll state.
|
|
80
|
+
- Exposes navigation actions and current page metadata.
|
|
81
|
+
- `toast`:
|
|
82
|
+
- Handles toast queue, timers, and lifecycle.
|
|
83
|
+
- Renders a required client `ToastComponent`.
|
|
84
|
+
|
|
85
|
+
## Quick Start
|
|
86
|
+
|
|
87
|
+
Install dependencies:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npm install
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Run the test app:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npm run android
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Other useful scripts:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
npm run lint
|
|
103
|
+
npm run build
|
|
104
|
+
```
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { DefaultReact } from '../../types';
|
|
2
|
-
declare const _default: ({ children, ...props }: DefaultReact.SafeAreaViewProps) => import("react").JSX.Element;
|
|
2
|
+
declare const _default: ({ children, style, ...props }: DefaultReact.SafeAreaViewProps) => import("react").JSX.Element;
|
|
3
3
|
export default _default;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
2
|
-
export default ({ children, ...props }) => {
|
|
3
|
-
return (<SafeAreaView {...props}>
|
|
2
|
+
export default ({ children, style, ...props }) => {
|
|
3
|
+
return (<SafeAreaView style={[{ flex: 1 }, style]} {...props}>
|
|
4
4
|
{children}
|
|
5
5
|
</SafeAreaView>);
|
|
6
6
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export { SafeAreaView, SubTitle, Text, Title, View, } from './components/base';
|
|
2
2
|
export type { DefaultReact, AnimadoTypes, ActionTypes, NavigatorTypes, } from './types';
|
|
3
|
-
export { createLightDarkProvider, createConfigProvider, createLangProvider, NavigatorProvider, useNavigator, ToastProvider, useToast, } from './
|
|
3
|
+
export { createLightDarkProvider, createConfigProvider, createLangProvider, createApiProvider, NavigatorProvider, useNavigator, ToastProvider, useToast, } from './modules';
|
|
4
4
|
export * as AlambreIcons from './components/icons';
|
|
5
5
|
export * as Buttons from './components/base/input/buttons';
|
|
6
6
|
export { default as AnimadoView } from './components/animations/animado/AnimadoView';
|
|
7
7
|
export { default as AnimadoText } from './components/animations/animado/AnimadoText';
|
|
8
8
|
export { default as ToggleLightDark } from './components/animations/ToggleLightDark';
|
|
9
|
-
export {
|
|
9
|
+
export { NavigatorBody } from './modules/navigator';
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Base Components
|
|
2
2
|
export { SafeAreaView, SubTitle, Text, Title, View, } from './components/base';
|
|
3
|
-
//
|
|
4
|
-
export { createLightDarkProvider, createConfigProvider, createLangProvider, NavigatorProvider, useNavigator, ToastProvider, useToast, } from './
|
|
3
|
+
// Modules
|
|
4
|
+
export { createLightDarkProvider, createConfigProvider, createLangProvider, createApiProvider, NavigatorProvider, useNavigator, ToastProvider, useToast, } from './modules';
|
|
5
5
|
// Icons
|
|
6
6
|
export * as AlambreIcons from './components/icons';
|
|
7
7
|
// Buttons.
|
|
@@ -10,4 +10,4 @@ export * as Buttons from './components/base/input/buttons';
|
|
|
10
10
|
export { default as AnimadoView } from './components/animations/animado/AnimadoView';
|
|
11
11
|
export { default as AnimadoText } from './components/animations/animado/AnimadoText';
|
|
12
12
|
export { default as ToggleLightDark } from './components/animations/ToggleLightDark';
|
|
13
|
-
export {
|
|
13
|
+
export { NavigatorBody } from './modules/navigator';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type QueryKey, type UseMutationOptions, type UseMutationResult, type UseQueryOptions, type UseQueryResult } from '@tanstack/react-query';
|
|
3
|
+
import type { ApiContextValue, ApiEndpoint, CreateApiProviderOptions, RouteCatalog } from './api-types';
|
|
4
|
+
declare const createApiProvider: ({ baseUrl, getHeaders, createQueryClient, }: CreateApiProviderOptions) => {
|
|
5
|
+
ApiProvider: ({ children }: {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
}) => import("react").JSX.Element;
|
|
8
|
+
useApi: () => ApiContextValue;
|
|
9
|
+
createRoute: <TResponse, TVariables = void>(route: ApiEndpoint<TResponse, TVariables>) => ApiEndpoint<TResponse, TVariables>;
|
|
10
|
+
createRoutes: <TRoutes extends RouteCatalog>(routes: TRoutes) => TRoutes;
|
|
11
|
+
createQueryHook: <TResponse, TVariables = void>(endpoint: ApiEndpoint<TResponse, TVariables>) => (variables: TVariables, options?: Omit<UseQueryOptions<TResponse, Error, TResponse, QueryKey>, "queryKey" | "queryFn">) => UseQueryResult<TResponse, Error>;
|
|
12
|
+
createQueryHookFromRoute: <TResponse, TVariables = void>(route: ApiEndpoint<TResponse, TVariables>) => (variables: TVariables, options?: Omit<UseQueryOptions<TResponse, Error, TResponse, readonly unknown[]>, "queryKey" | "queryFn"> | undefined) => UseQueryResult<TResponse, Error>;
|
|
13
|
+
createMutationHook: <TResponse, TVariables>(endpoint: ApiEndpoint<TResponse, TVariables>, settings?: {
|
|
14
|
+
invalidateKeys?: QueryKey[];
|
|
15
|
+
}) => (options?: Omit<UseMutationOptions<TResponse, Error, TVariables>, "mutationFn">) => UseMutationResult<TResponse, Error, TVariables>;
|
|
16
|
+
createMutationHookFromRoute: <TResponse, TVariables>(route: ApiEndpoint<TResponse, TVariables>, settings?: {
|
|
17
|
+
invalidateKeys?: QueryKey[];
|
|
18
|
+
}) => (options?: Omit<UseMutationOptions<TResponse, Error, TVariables, unknown>, "mutationFn"> | undefined) => UseMutationResult<TResponse, Error, TVariables>;
|
|
19
|
+
};
|
|
20
|
+
export default createApiProvider;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/* global fetch */
|
|
2
|
+
import { createContext, useContext, useMemo, } from 'react';
|
|
3
|
+
import { QueryClient, QueryClientProvider, useMutation, useQuery, useQueryClient, } from '@tanstack/react-query';
|
|
4
|
+
import { buildUrl, resolveValue } from './api-utils';
|
|
5
|
+
const createApiProvider = ({ baseUrl, getHeaders, createQueryClient, }) => {
|
|
6
|
+
const ApiContext = createContext(undefined);
|
|
7
|
+
const ApiProvider = ({ children }) => {
|
|
8
|
+
const queryClient = useMemo(() => { var _a; return (_a = createQueryClient === null || createQueryClient === void 0 ? void 0 : createQueryClient()) !== null && _a !== void 0 ? _a : new QueryClient(); }, []);
|
|
9
|
+
const request = async ({ method, path, query, body, headers, transform, }) => {
|
|
10
|
+
const authHeaders = await (getHeaders === null || getHeaders === void 0 ? void 0 : getHeaders());
|
|
11
|
+
const url = buildUrl(baseUrl, path, query);
|
|
12
|
+
const response = await fetch(url, {
|
|
13
|
+
method,
|
|
14
|
+
headers: {
|
|
15
|
+
Accept: 'application/json',
|
|
16
|
+
'Content-Type': 'application/json',
|
|
17
|
+
...authHeaders,
|
|
18
|
+
...headers,
|
|
19
|
+
},
|
|
20
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
21
|
+
});
|
|
22
|
+
const rawText = await response.text();
|
|
23
|
+
let rawData = undefined;
|
|
24
|
+
if (rawText.length) {
|
|
25
|
+
try {
|
|
26
|
+
rawData = JSON.parse(rawText);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
rawData = rawText;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
const message = typeof rawData === 'object' && rawData && 'message' in rawData
|
|
34
|
+
? String(rawData.message)
|
|
35
|
+
: `Request failed (${response.status})`;
|
|
36
|
+
throw new Error(message);
|
|
37
|
+
}
|
|
38
|
+
if (transform)
|
|
39
|
+
return transform(rawData);
|
|
40
|
+
return rawData;
|
|
41
|
+
};
|
|
42
|
+
return (<QueryClientProvider client={queryClient}>
|
|
43
|
+
<ApiContext.Provider value={{ request }}>
|
|
44
|
+
{children}
|
|
45
|
+
</ApiContext.Provider>
|
|
46
|
+
</QueryClientProvider>);
|
|
47
|
+
};
|
|
48
|
+
const useApi = () => {
|
|
49
|
+
const ctx = useContext(ApiContext);
|
|
50
|
+
if (!ctx) {
|
|
51
|
+
throw new Error('useApi must be used within ApiProvider');
|
|
52
|
+
}
|
|
53
|
+
return ctx;
|
|
54
|
+
};
|
|
55
|
+
const createQueryHook = (endpoint) => {
|
|
56
|
+
return (variables, options) => {
|
|
57
|
+
const { request } = useApi();
|
|
58
|
+
const queryKey = resolveValue(typeof endpoint.key === 'function'
|
|
59
|
+
? () => endpoint.key(variables)
|
|
60
|
+
: endpoint.key);
|
|
61
|
+
return useQuery({
|
|
62
|
+
queryKey,
|
|
63
|
+
queryFn: () => {
|
|
64
|
+
var _a, _b, _c, _d;
|
|
65
|
+
return request({
|
|
66
|
+
method: (_a = endpoint.method) !== null && _a !== void 0 ? _a : 'GET',
|
|
67
|
+
path: typeof endpoint.path === 'function'
|
|
68
|
+
? endpoint.path(variables)
|
|
69
|
+
: endpoint.path,
|
|
70
|
+
query: (_b = endpoint.query) === null || _b === void 0 ? void 0 : _b.call(endpoint, variables),
|
|
71
|
+
body: (_c = endpoint.body) === null || _c === void 0 ? void 0 : _c.call(endpoint, variables),
|
|
72
|
+
headers: (_d = endpoint.headers) === null || _d === void 0 ? void 0 : _d.call(endpoint, variables),
|
|
73
|
+
transform: endpoint.transform,
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
...options,
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
const createRoute = (route) => route;
|
|
81
|
+
const createRoutes = (routes) => routes;
|
|
82
|
+
const createQueryHookFromRoute = (route) => createQueryHook(route);
|
|
83
|
+
const createMutationHook = (endpoint, settings) => {
|
|
84
|
+
return (options) => {
|
|
85
|
+
const { request } = useApi();
|
|
86
|
+
const queryClient = useQueryClient();
|
|
87
|
+
return useMutation({
|
|
88
|
+
mutationFn: (variables) => {
|
|
89
|
+
var _a, _b, _c, _d;
|
|
90
|
+
return request({
|
|
91
|
+
method: (_a = endpoint.method) !== null && _a !== void 0 ? _a : 'POST',
|
|
92
|
+
path: typeof endpoint.path === 'function'
|
|
93
|
+
? endpoint.path(variables)
|
|
94
|
+
: endpoint.path,
|
|
95
|
+
query: (_b = endpoint.query) === null || _b === void 0 ? void 0 : _b.call(endpoint, variables),
|
|
96
|
+
body: (_c = endpoint.body) === null || _c === void 0 ? void 0 : _c.call(endpoint, variables),
|
|
97
|
+
headers: (_d = endpoint.headers) === null || _d === void 0 ? void 0 : _d.call(endpoint, variables),
|
|
98
|
+
transform: endpoint.transform,
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
...options,
|
|
102
|
+
onSuccess: async (data, variables, onMutateResult, context) => {
|
|
103
|
+
var _a, _b;
|
|
104
|
+
if ((_a = settings === null || settings === void 0 ? void 0 : settings.invalidateKeys) === null || _a === void 0 ? void 0 : _a.length) {
|
|
105
|
+
await Promise.all(settings.invalidateKeys.map((key) => queryClient.invalidateQueries({ queryKey: key })));
|
|
106
|
+
}
|
|
107
|
+
await ((_b = options === null || options === void 0 ? void 0 : options.onSuccess) === null || _b === void 0 ? void 0 : _b.call(options, data, variables, onMutateResult, context));
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
const createMutationHookFromRoute = (route, settings) => createMutationHook(route, settings);
|
|
113
|
+
return {
|
|
114
|
+
ApiProvider,
|
|
115
|
+
useApi,
|
|
116
|
+
createRoute,
|
|
117
|
+
createRoutes,
|
|
118
|
+
createQueryHook,
|
|
119
|
+
createQueryHookFromRoute,
|
|
120
|
+
createMutationHook,
|
|
121
|
+
createMutationHookFromRoute,
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
export default createApiProvider;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { QueryClient, QueryKey } from '@tanstack/react-query';
|
|
2
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
3
|
+
export type QueryParams = Record<string, string | number | boolean | null | undefined>;
|
|
4
|
+
export type RequestHeaders = Record<string, string>;
|
|
5
|
+
export type ApiRequestInput = {
|
|
6
|
+
method: HttpMethod;
|
|
7
|
+
path: string;
|
|
8
|
+
query?: QueryParams;
|
|
9
|
+
body?: unknown;
|
|
10
|
+
headers?: RequestHeaders;
|
|
11
|
+
};
|
|
12
|
+
export type CreateApiProviderOptions = {
|
|
13
|
+
baseUrl: string;
|
|
14
|
+
getHeaders?: () => Promise<RequestHeaders | undefined> | RequestHeaders | undefined;
|
|
15
|
+
createQueryClient?: () => QueryClient;
|
|
16
|
+
};
|
|
17
|
+
export type ApiEndpoint<TResponse, TVariables = void> = {
|
|
18
|
+
key: QueryKey | ((variables: TVariables) => QueryKey);
|
|
19
|
+
method?: HttpMethod;
|
|
20
|
+
path: string | ((variables: TVariables) => string);
|
|
21
|
+
query?: (variables: TVariables) => QueryParams | undefined;
|
|
22
|
+
body?: (variables: TVariables) => unknown;
|
|
23
|
+
headers?: (variables: TVariables) => RequestHeaders | undefined;
|
|
24
|
+
transform?: (raw: unknown) => TResponse;
|
|
25
|
+
};
|
|
26
|
+
export type InferRouteResponse<TRoute> = TRoute extends ApiEndpoint<infer TResponse, unknown> ? TResponse : never;
|
|
27
|
+
export type InferRouteVariables<TRoute> = TRoute extends ApiEndpoint<unknown, infer TVariables> ? TVariables : never;
|
|
28
|
+
export type RouteCatalog = Record<string, ApiEndpoint<unknown, never>>;
|
|
29
|
+
export type ApiContextValue = {
|
|
30
|
+
request: <TResponse>(input: ApiRequestInput & {
|
|
31
|
+
transform?: (raw: unknown) => TResponse;
|
|
32
|
+
}) => Promise<TResponse>;
|
|
33
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const buildUrl = (baseUrl, path, query) => {
|
|
2
|
+
const normalizedBase = baseUrl.endsWith('/')
|
|
3
|
+
? baseUrl.slice(0, -1)
|
|
4
|
+
: baseUrl;
|
|
5
|
+
const normalizedPath = path.startsWith('/')
|
|
6
|
+
? path
|
|
7
|
+
: `/${path}`;
|
|
8
|
+
if (!query)
|
|
9
|
+
return `${normalizedBase}${normalizedPath}`;
|
|
10
|
+
const queryString = Object.entries(query)
|
|
11
|
+
.filter(([, value]) => value !== undefined && value !== null)
|
|
12
|
+
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
|
|
13
|
+
.join('&');
|
|
14
|
+
if (!queryString)
|
|
15
|
+
return `${normalizedBase}${normalizedPath}`;
|
|
16
|
+
return `${normalizedBase}${normalizedPath}?${queryString}`;
|
|
17
|
+
};
|
|
18
|
+
export const resolveValue = (value) => {
|
|
19
|
+
if (typeof value === 'function') {
|
|
20
|
+
return value();
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as createApiProvider } from './api-provider';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type Listener<TState> = (nextState: TState) => void;
|
|
2
|
+
export default class BaseModule<TState> {
|
|
3
|
+
private state;
|
|
4
|
+
private readonly listeners;
|
|
5
|
+
constructor(initialState: TState);
|
|
6
|
+
getState(): TState;
|
|
7
|
+
subscribe(listener: Listener<TState>): () => void;
|
|
8
|
+
protected setState(nextState: TState | ((prevState: TState) => TState)): void;
|
|
9
|
+
init(): Promise<void>;
|
|
10
|
+
dispose(): void;
|
|
11
|
+
}
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export default class BaseModule {
|
|
2
|
+
constructor(initialState) {
|
|
3
|
+
// Track all subscribers that react to state changes.
|
|
4
|
+
this.listeners = new Set();
|
|
5
|
+
this.state = initialState;
|
|
6
|
+
}
|
|
7
|
+
getState() {
|
|
8
|
+
return this.state;
|
|
9
|
+
}
|
|
10
|
+
subscribe(listener) {
|
|
11
|
+
// Register subscriber and return an unsubscribe function.
|
|
12
|
+
this.listeners.add(listener);
|
|
13
|
+
return () => {
|
|
14
|
+
this.listeners.delete(listener);
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
setState(nextState) {
|
|
18
|
+
// Support direct values and updater functions.
|
|
19
|
+
this.state = typeof nextState === 'function'
|
|
20
|
+
? nextState(this.state)
|
|
21
|
+
: nextState;
|
|
22
|
+
// Notify every subscriber with the latest state snapshot.
|
|
23
|
+
this.listeners.forEach((listener) => listener(this.state));
|
|
24
|
+
}
|
|
25
|
+
async init() {
|
|
26
|
+
// Allow subclasses to override async initialization logic.
|
|
27
|
+
return Promise.resolve();
|
|
28
|
+
}
|
|
29
|
+
dispose() {
|
|
30
|
+
// Release subscribers to avoid leaks when provider unmounts.
|
|
31
|
+
this.listeners.clear();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ActionTypes } from '../../types';
|
|
2
|
+
import BaseModule from '../base-module';
|
|
3
|
+
import type { ConfigState } from './config-types';
|
|
4
|
+
export default class ConfigModule<T extends Record<string, string>> extends BaseModule<ConfigState<T>> {
|
|
5
|
+
private readonly baseConfig;
|
|
6
|
+
private readonly storage;
|
|
7
|
+
constructor(baseConfig: T, storage: ActionTypes.Storage);
|
|
8
|
+
init(): Promise<void>;
|
|
9
|
+
saveField(field: keyof T, value: string): Promise<void>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import BaseModule from '../base-module';
|
|
2
|
+
export default class ConfigModule extends BaseModule {
|
|
3
|
+
constructor(baseConfig, storage) {
|
|
4
|
+
super({ config: baseConfig });
|
|
5
|
+
this.baseConfig = baseConfig;
|
|
6
|
+
this.storage = storage;
|
|
7
|
+
}
|
|
8
|
+
async init() {
|
|
9
|
+
const nextConfig = { ...this.baseConfig };
|
|
10
|
+
for (const key of Object.keys(this.baseConfig)) {
|
|
11
|
+
const saved = await this.storage.get(key);
|
|
12
|
+
nextConfig[key] = (saved !== null && saved !== void 0 ? saved : this.baseConfig[key]);
|
|
13
|
+
}
|
|
14
|
+
this.setState({ config: nextConfig });
|
|
15
|
+
}
|
|
16
|
+
async saveField(field, value) {
|
|
17
|
+
this.setState((prev) => ({
|
|
18
|
+
config: {
|
|
19
|
+
...prev.config,
|
|
20
|
+
[field]: value,
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
await this.storage.set(field, value);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ActionTypes } from '../../types';
|
|
2
|
+
import type { UseConfigType } from './config-types';
|
|
3
|
+
declare const createConfigProvider: <T extends Record<string, string>>(baseConfig: T, storage: ActionTypes.Storage) => {
|
|
4
|
+
ConfigProvider: ({ children, ...props }: import("../../types/default-react").ViewProps) => import("react").JSX.Element;
|
|
5
|
+
useConfig: () => UseConfigType<T>;
|
|
6
|
+
};
|
|
7
|
+
export default createConfigProvider;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import createModuleProvider from '../module-provider';
|
|
2
|
+
import ConfigModule from './config-module';
|
|
3
|
+
const createConfigProvider = (baseConfig, storage) => {
|
|
4
|
+
const { Provider, useValue, } = createModuleProvider({
|
|
5
|
+
createModule: () => new ConfigModule(baseConfig, storage),
|
|
6
|
+
mapContextValue: (module, state) => ({
|
|
7
|
+
config: state.config,
|
|
8
|
+
saveField: module.saveField.bind(module),
|
|
9
|
+
}),
|
|
10
|
+
missingProviderError: 'useConfig must be used within ConfigProvider',
|
|
11
|
+
});
|
|
12
|
+
return {
|
|
13
|
+
ConfigProvider: Provider,
|
|
14
|
+
useConfig: useValue,
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export default createConfigProvider;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { default as BaseModule } from './base-module';
|
|
2
|
+
export { default as createModuleProvider } from './module-provider';
|
|
3
|
+
export { default as createLightDarkProvider } from './theme/theme-provider';
|
|
4
|
+
export { default as createConfigProvider } from './config/config-provider';
|
|
5
|
+
export { default as createLangProvider } from './lang/lang-provider';
|
|
6
|
+
export { createApiProvider } from './api';
|
|
7
|
+
export { NavigatorProvider, useNavigator } from './navigator';
|
|
8
|
+
export { ToastProvider, useToast, DefaultToastBody } from './toast';
|
|
9
|
+
export type { ToastComponentProps } from './toast/toast-types';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { default as BaseModule } from './base-module';
|
|
2
|
+
export { default as createModuleProvider } from './module-provider';
|
|
3
|
+
export { default as createLightDarkProvider } from './theme/theme-provider';
|
|
4
|
+
export { default as createConfigProvider } from './config/config-provider';
|
|
5
|
+
export { default as createLangProvider } from './lang/lang-provider';
|
|
6
|
+
export { createApiProvider } from './api';
|
|
7
|
+
export { NavigatorProvider, useNavigator } from './navigator';
|
|
8
|
+
export { ToastProvider, useToast, DefaultToastBody } from './toast';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ActionTypes } from '../../types';
|
|
2
|
+
import BaseModule from '../base-module';
|
|
3
|
+
import type { Langs, LangState } from './lang-types';
|
|
4
|
+
export default class LangModule<T extends Langs> extends BaseModule<LangState<T>> {
|
|
5
|
+
private readonly langs;
|
|
6
|
+
private readonly storage;
|
|
7
|
+
private readonly firstLangKey;
|
|
8
|
+
constructor(langs: T, storage: ActionTypes.Storage);
|
|
9
|
+
init(): Promise<void>;
|
|
10
|
+
changeLang(lang: keyof T): Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import BaseModule from '../base-module';
|
|
2
|
+
const langKey = 'currentLanguage';
|
|
3
|
+
export default class LangModule extends BaseModule {
|
|
4
|
+
constructor(langs, storage) {
|
|
5
|
+
const firstLangKey = Object.keys(langs).shift();
|
|
6
|
+
if (!firstLangKey) {
|
|
7
|
+
throw new Error('createLangProvider requires at least one language');
|
|
8
|
+
}
|
|
9
|
+
super({ currentLang: firstLangKey });
|
|
10
|
+
this.langs = langs;
|
|
11
|
+
this.storage = storage;
|
|
12
|
+
this.firstLangKey = firstLangKey;
|
|
13
|
+
}
|
|
14
|
+
async init() {
|
|
15
|
+
const stored = await this.storage.get(langKey);
|
|
16
|
+
if (!stored)
|
|
17
|
+
return;
|
|
18
|
+
if (!(stored in this.langs))
|
|
19
|
+
return;
|
|
20
|
+
this.setState({ currentLang: stored });
|
|
21
|
+
}
|
|
22
|
+
async changeLang(lang) {
|
|
23
|
+
const target = this.langs[lang] ? lang : this.firstLangKey;
|
|
24
|
+
this.setState({ currentLang: target });
|
|
25
|
+
await this.storage.set(langKey, String(target));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ActionTypes } from '../../types';
|
|
2
|
+
import type { Langs, UseLangType } from './lang-types';
|
|
3
|
+
declare const createLangProvider: <T extends Langs>(langs: T, storage: ActionTypes.Storage) => {
|
|
4
|
+
LangProvider: ({ children, ...props }: import("../../types/default-react").ViewProps) => import("react").JSX.Element;
|
|
5
|
+
useLang: () => UseLangType<T>;
|
|
6
|
+
};
|
|
7
|
+
export default createLangProvider;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import createModuleProvider from '../module-provider';
|
|
2
|
+
import LangModule from './lang-module';
|
|
3
|
+
const createLangProvider = (langs, storage) => {
|
|
4
|
+
const { Provider, useValue, } = createModuleProvider({
|
|
5
|
+
createModule: () => new LangModule(langs, storage),
|
|
6
|
+
mapContextValue: (module, state) => ({
|
|
7
|
+
lang: langs[state.currentLang],
|
|
8
|
+
currentLang: state.currentLang,
|
|
9
|
+
changeLang: module.changeLang.bind(module),
|
|
10
|
+
}),
|
|
11
|
+
missingProviderError: 'useLang must be used within a LangProvider',
|
|
12
|
+
});
|
|
13
|
+
return {
|
|
14
|
+
LangProvider: Provider,
|
|
15
|
+
useLang: useValue,
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export default createLangProvider;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type Langs = Record<string, object>;
|
|
2
|
+
export type LangState<T extends Langs> = {
|
|
3
|
+
currentLang: keyof T;
|
|
4
|
+
};
|
|
5
|
+
export type UseLangType<T extends Langs> = {
|
|
6
|
+
lang: T[keyof T];
|
|
7
|
+
currentLang: keyof T;
|
|
8
|
+
changeLang: (newLangKey: keyof T) => Promise<void>;
|
|
9
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DefaultReact } from '../types';
|
|
2
|
+
import type BaseModule from './base-module';
|
|
3
|
+
type CreateModuleProviderOptions<TState, TModule extends BaseModule<TState>, TContextValue> = {
|
|
4
|
+
createModule: () => TModule;
|
|
5
|
+
mapContextValue: (module: TModule, state: TState) => TContextValue;
|
|
6
|
+
missingProviderError: string;
|
|
7
|
+
};
|
|
8
|
+
declare const _default: <TState, TModule extends BaseModule<TState>, TContextValue>({ createModule, mapContextValue, missingProviderError, }: CreateModuleProviderOptions<TState, TModule, TContextValue>) => {
|
|
9
|
+
Provider: ({ children, ...props }: DefaultReact.ViewProps) => import("react").JSX.Element;
|
|
10
|
+
useValue: () => NonNullable<TContextValue>;
|
|
11
|
+
};
|
|
12
|
+
export default _default;
|