@rnzeus/atlas 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/dist/README.md +21 -0
- package/dist/config/internal.d.ts +1 -0
- package/dist/config/internal.js +1 -0
- package/dist/config/navigation/index.d.ts +1 -0
- package/dist/config/navigation/index.js +1 -0
- package/dist/config/navigation/types.d.ts +55 -0
- package/dist/config/navigation/types.js +1 -0
- package/dist/config/navigation.types.d.ts +25 -0
- package/dist/config/navigation.types.js +1 -0
- package/dist/hooks/README.md +11 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/internal.d.ts +1 -0
- package/dist/hooks/internal.js +1 -0
- package/dist/hooks/use-path-navigation/README.md +66 -0
- package/dist/hooks/use-path-navigation/hook.d.ts +119 -0
- package/dist/hooks/use-path-navigation/hook.js +52 -0
- package/dist/hooks/use-path-navigation/index.d.ts +1 -0
- package/dist/hooks/use-path-navigation/index.js +1 -0
- package/dist/services/README.md +17 -0
- package/dist/services/index.d.ts +4 -0
- package/dist/services/index.js +3 -0
- package/dist/services/internal.d.ts +1 -0
- package/dist/services/internal.js +1 -0
- package/dist/services/navigation/README.md +131 -0
- package/dist/services/navigation/api.d.ts +5 -0
- package/dist/services/navigation/api.js +11 -0
- package/dist/services/navigation/chain-builder.d.ts +16 -0
- package/dist/services/navigation/chain-builder.js +31 -0
- package/dist/services/navigation/index.d.ts +3 -0
- package/dist/services/navigation/index.js +3 -0
- package/dist/services/navigation/service.d.ts +55 -0
- package/dist/services/navigation/service.js +536 -0
- package/dist/services/navigation/types.d.ts +13 -0
- package/dist/services/navigation/types.js +1 -0
- package/dist/services/navigation/zod-adapter.d.ts +22 -0
- package/dist/services/navigation/zod-adapter.js +36 -0
- package/dist/services/transport/README.md +79 -0
- package/dist/services/transport/build-query.d.ts +2 -0
- package/dist/services/transport/build-query.js +3 -0
- package/dist/services/transport/create-transport-methods.d.ts +8 -0
- package/dist/services/transport/create-transport-methods.js +34 -0
- package/dist/services/transport/error-exception.d.ts +8 -0
- package/dist/services/transport/error-exception.js +16 -0
- package/dist/services/transport/index.d.ts +3 -0
- package/dist/services/transport/index.js +2 -0
- package/dist/services/transport/read-response-body.d.ts +3 -0
- package/dist/services/transport/read-response-body.js +27 -0
- package/dist/services/transport/service.d.ts +22 -0
- package/dist/services/transport/service.js +102 -0
- package/dist/services/transport/types.d.ts +4 -0
- package/dist/services/transport/types.js +1 -0
- package/dist/ui/README.md +17 -0
- package/dist/ui/debug-dock/README.md +58 -0
- package/dist/ui/debug-dock/debug-dock.d.ts +13 -0
- package/dist/ui/debug-dock/debug-dock.js +20 -0
- package/dist/ui/debug-dock/index.d.ts +2 -0
- package/dist/ui/debug-dock/index.js +1 -0
- package/dist/ui/debug-dock/styles.d.ts +8 -0
- package/dist/ui/debug-dock/styles.js +25 -0
- package/dist/ui/debug-dock/use-debug-dock.d.ts +20 -0
- package/dist/ui/debug-dock/use-debug-dock.js +71 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +2 -0
- package/dist/ui/toast/README.md +62 -0
- package/dist/ui/toast/context.d.ts +7 -0
- package/dist/ui/toast/context.js +6 -0
- package/dist/ui/toast/index.d.ts +5 -0
- package/dist/ui/toast/index.js +2 -0
- package/dist/ui/toast/toast-plate/README.md +38 -0
- package/dist/ui/toast/toast-plate/index.d.ts +2 -0
- package/dist/ui/toast/toast-plate/index.js +1 -0
- package/dist/ui/toast/toast-plate/toast-plate.d.ts +9 -0
- package/dist/ui/toast/toast-plate/toast-plate.js +11 -0
- package/dist/ui/toast/toast-plate/use-toast-plate.d.ts +4 -0
- package/dist/ui/toast/toast-plate/use-toast-plate.js +33 -0
- package/dist/ui/toast/toast-provider.d.ts +10 -0
- package/dist/ui/toast/toast-provider.js +53 -0
- package/dist/ui/toast/toast-viewport/README.md +38 -0
- package/dist/ui/toast/toast-viewport/index.d.ts +2 -0
- package/dist/ui/toast/toast-viewport/index.js +1 -0
- package/dist/ui/toast/toast-viewport/styles.d.ts +5 -0
- package/dist/ui/toast/toast-viewport/styles.js +10 -0
- package/dist/ui/toast/toast-viewport/toast-viewport.d.ts +10 -0
- package/dist/ui/toast/toast-viewport/toast-viewport.js +13 -0
- package/dist/ui/toast/types.d.ts +25 -0
- package/dist/ui/toast/types.js +1 -0
- package/dist/ui/toast/use-toast.d.ts +1 -0
- package/dist/ui/toast/use-toast.js +5 -0
- package/dist/utils/README.md +23 -0
- package/dist/utils/create-typography/README.md +71 -0
- package/dist/utils/create-typography/helper.d.ts +23 -0
- package/dist/utils/create-typography/helper.js +72 -0
- package/dist/utils/create-typography/index.d.ts +1 -0
- package/dist/utils/create-typography/index.js +1 -0
- package/dist/utils/define-route/README.md +50 -0
- package/dist/utils/define-route/helper.d.ts +2 -0
- package/dist/utils/define-route/helper.js +1 -0
- package/dist/utils/define-route/index.d.ts +1 -0
- package/dist/utils/define-route/index.js +1 -0
- package/dist/utils/display-name/README.md +46 -0
- package/dist/utils/display-name/helper.d.ts +2 -0
- package/dist/utils/display-name/helper.js +91 -0
- package/dist/utils/display-name/index.d.ts +1 -0
- package/dist/utils/display-name/index.js +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/internal.d.ts +3 -0
- package/dist/utils/internal.js +3 -0
- package/package.json +74 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# `@rnzeus/atlas/services/transport`
|
|
2
|
+
|
|
3
|
+
← [`@rnzeus/atlas/services`](../README.md)
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
Base `fetch` transport with query params, default headers, response parsing, and normalized errors. Provides `get/post/put/delete` helpers on top of a single request pipeline.
|
|
8
|
+
|
|
9
|
+
## Import
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import {Transport} from '@rnzeus/atlas/services';
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## API (public surface)
|
|
16
|
+
|
|
17
|
+
- `class Transport` (abstract)
|
|
18
|
+
- **required**: `protected get baseURL(): string`
|
|
19
|
+
- **required**: `protected formatErrorMessage(data: unknown, status: number): string`
|
|
20
|
+
- **optional**: `protected notify(message: string): void` (default: no-op)
|
|
21
|
+
- `headers: HeadersInit_` (get/set, merges with defaults)
|
|
22
|
+
- `get<T>(url, params?, options?)`
|
|
23
|
+
- `post<T>(url, body, options?)`
|
|
24
|
+
- `put<T>(url, body, options?)`
|
|
25
|
+
- `delete<T>(url, params?, options?)`
|
|
26
|
+
- `class TransportErrorException extends Error`
|
|
27
|
+
- `status: number`
|
|
28
|
+
- `data: unknown`
|
|
29
|
+
- `isServerError()`
|
|
30
|
+
- `isClientError()`
|
|
31
|
+
- `type TransportOptions`
|
|
32
|
+
- `RequestInit` + `headers?: HeadersInit_` + `responseType?: 'arrayBuffer' | 'json'`
|
|
33
|
+
|
|
34
|
+
## Usage (minimal)
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import {Transport} from '@rnzeus/atlas/services';
|
|
38
|
+
|
|
39
|
+
class ApiTransport extends Transport {
|
|
40
|
+
protected get baseURL() {
|
|
41
|
+
return 'https://api.example.com/';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
protected formatErrorMessage(data: unknown, status: number) {
|
|
45
|
+
const msg = typeof data === 'object' && data && (data as any).message;
|
|
46
|
+
return msg ? String(msg) : `HTTP ${status}`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const api = new ApiTransport();
|
|
51
|
+
api.headers = {Authorization: 'Bearer token'};
|
|
52
|
+
|
|
53
|
+
const user = await api.get<User>('/v1/user', {id: '42'});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Notes
|
|
57
|
+
|
|
58
|
+
- **Relative vs absolute URLs**: relative paths are resolved against `baseURL`; absolute (`http(s)://`) are used as-is.
|
|
59
|
+
- **Trailing slash**: the final resolved URL has a trailing `/` removed.
|
|
60
|
+
- **Query params**: `params` are serialized with `qs` using `arrayFormat: 'brackets'`.
|
|
61
|
+
- **Headers**: default headers are merged with per-request headers; `Content-Type` is removed automatically for `FormData`.
|
|
62
|
+
- **Response parsing**:
|
|
63
|
+
- `options.responseType === 'arrayBuffer'` forces `arrayBuffer()`
|
|
64
|
+
- otherwise parses by `content-type` (`json`, `formData`, `text`), with a JSON→text fallback
|
|
65
|
+
- **Errors**:
|
|
66
|
+
- non-OK responses throw `TransportErrorException(status, message, data)`
|
|
67
|
+
- 5xx triggers `notify(message)` before throwing
|
|
68
|
+
- `AbortError` is rethrown as-is
|
|
69
|
+
- unknown errors are wrapped into `TransportErrorException(0, message)`
|
|
70
|
+
|
|
71
|
+
## Sources
|
|
72
|
+
|
|
73
|
+
- [`service`](./service.js)
|
|
74
|
+
- [`types`](./types.js)
|
|
75
|
+
- [`error-exception`](./error-exception.js)
|
|
76
|
+
- [`create-transport-methods`](./create-transport-methods.js)
|
|
77
|
+
- [`read-response-body`](./read-response-body.js)
|
|
78
|
+
- [`build-query`](./build-query.js)
|
|
79
|
+
- [`index`](./index.js)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { TransportOptions } from './types';
|
|
2
|
+
declare const createTransportMethods: (requestFn: <T>(url: string, options: TransportOptions, paramsOrBody?: Record<string, unknown> | null) => Promise<T>) => {
|
|
3
|
+
get<T>(url: string, params?: Record<string, unknown> | null, options?: TransportOptions): Promise<T>;
|
|
4
|
+
post<T>(url: string, body: unknown, options?: TransportOptions): Promise<T>;
|
|
5
|
+
put<T>(url: string, body: unknown, options?: TransportOptions): Promise<T>;
|
|
6
|
+
delete<T>(url: string, params?: Record<string, unknown> | null, options?: TransportOptions): Promise<T>;
|
|
7
|
+
};
|
|
8
|
+
export default createTransportMethods;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
function isBodyFormData(v) {
|
|
2
|
+
return typeof FormData !== 'undefined' && v instanceof FormData;
|
|
3
|
+
}
|
|
4
|
+
function buildRequestInitWithBody(options, method, body) {
|
|
5
|
+
const init = { ...options, method };
|
|
6
|
+
if (body === undefined || body === null)
|
|
7
|
+
return init;
|
|
8
|
+
if (isBodyFormData(body)) {
|
|
9
|
+
init.body = body;
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
init.body = JSON.stringify(body);
|
|
13
|
+
}
|
|
14
|
+
return init;
|
|
15
|
+
}
|
|
16
|
+
const createTransportMethods = (requestFn) => {
|
|
17
|
+
return {
|
|
18
|
+
get(url, params = null, options = {}) {
|
|
19
|
+
return requestFn(url, { ...options, method: 'GET' }, params);
|
|
20
|
+
},
|
|
21
|
+
post(url, body, options = {}) {
|
|
22
|
+
const init = buildRequestInitWithBody(options, 'POST', body);
|
|
23
|
+
return requestFn(url, init);
|
|
24
|
+
},
|
|
25
|
+
put(url, body, options = {}) {
|
|
26
|
+
const init = buildRequestInitWithBody(options, 'PUT', body);
|
|
27
|
+
return requestFn(url, init);
|
|
28
|
+
},
|
|
29
|
+
delete(url, params = null, options = {}) {
|
|
30
|
+
return requestFn(url, { ...options, method: 'DELETE' }, params);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
export default createTransportMethods;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
declare class TransportErrorException extends Error {
|
|
2
|
+
readonly status: number;
|
|
3
|
+
readonly data: unknown;
|
|
4
|
+
constructor(status: number, message: string, data?: unknown);
|
|
5
|
+
isServerError(): boolean;
|
|
6
|
+
isClientError(): boolean;
|
|
7
|
+
}
|
|
8
|
+
export default TransportErrorException;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class TransportErrorException extends Error {
|
|
2
|
+
constructor(status, message, data) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = 'TransportErrorException';
|
|
5
|
+
this.status = status;
|
|
6
|
+
this.data = data;
|
|
7
|
+
Object.setPrototypeOf(this, TransportErrorException.prototype);
|
|
8
|
+
}
|
|
9
|
+
isServerError() {
|
|
10
|
+
return this.status >= 500;
|
|
11
|
+
}
|
|
12
|
+
isClientError() {
|
|
13
|
+
return this.status >= 400 && this.status < 500;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export default TransportErrorException;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const readTransportResponseBody = async (response, options = {}, contentType) => {
|
|
2
|
+
try {
|
|
3
|
+
if (options.responseType === 'arrayBuffer') {
|
|
4
|
+
return await response.arrayBuffer();
|
|
5
|
+
}
|
|
6
|
+
if (contentType?.includes('application/json')) {
|
|
7
|
+
return await response.json();
|
|
8
|
+
}
|
|
9
|
+
if (contentType?.includes('multipart/form-data')) {
|
|
10
|
+
return await response.formData();
|
|
11
|
+
}
|
|
12
|
+
if (contentType?.includes('text/')) {
|
|
13
|
+
return await response.text();
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
return await response.json();
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return await response.text();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
console.error('Error reading response body:', error);
|
|
24
|
+
return await response.text();
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
export default readTransportResponseBody;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { TransportOptions } from './types';
|
|
2
|
+
export default abstract class Transport {
|
|
3
|
+
private headersDefault;
|
|
4
|
+
protected abstract get baseURL(): string;
|
|
5
|
+
protected abstract formatErrorMessage(data: unknown, status: number): string;
|
|
6
|
+
protected notify(_message: string): void;
|
|
7
|
+
get headers(): HeadersInit_;
|
|
8
|
+
set headers(next: HeadersInit_);
|
|
9
|
+
protected request<T>(url: string, options?: TransportOptions, params?: Record<string, unknown> | null): Promise<T>;
|
|
10
|
+
protected transportMethods: {
|
|
11
|
+
get<T>(url: string, params?: Record<string, unknown> | null, options?: TransportOptions): Promise<T>;
|
|
12
|
+
post<T>(url: string, body: unknown, options?: TransportOptions): Promise<T>;
|
|
13
|
+
put<T>(url: string, body: unknown, options?: TransportOptions): Promise<T>;
|
|
14
|
+
delete<T>(url: string, params?: Record<string, unknown> | null, options?: TransportOptions): Promise<T>;
|
|
15
|
+
};
|
|
16
|
+
get: <T>(url: string, params?: Record<string, unknown> | null, options?: TransportOptions) => Promise<T>;
|
|
17
|
+
post: <T>(url: string, body: unknown, options?: TransportOptions) => Promise<T>;
|
|
18
|
+
put: <T>(url: string, body: unknown, options?: TransportOptions) => Promise<T>;
|
|
19
|
+
delete: <T>(url: string, params?: Record<string, unknown> | null, options?: TransportOptions) => Promise<T>;
|
|
20
|
+
private headersToRecord;
|
|
21
|
+
private withoutContentType;
|
|
22
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import buildTransportQuery from './build-query';
|
|
2
|
+
import createTransportMethods from './create-transport-methods';
|
|
3
|
+
import TransportErrorException from './error-exception';
|
|
4
|
+
import readTransportResponseBody from './read-response-body';
|
|
5
|
+
export default class Transport {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.headersDefault = {
|
|
8
|
+
'Content-Type': 'application/json',
|
|
9
|
+
Accept: 'application/json',
|
|
10
|
+
};
|
|
11
|
+
this.transportMethods = createTransportMethods(this.request.bind(this));
|
|
12
|
+
this.get = this.transportMethods.get.bind(this.transportMethods);
|
|
13
|
+
this.post = this.transportMethods.post.bind(this.transportMethods);
|
|
14
|
+
this.put = this.transportMethods.put.bind(this.transportMethods);
|
|
15
|
+
this.delete = this.transportMethods.delete.bind(this.transportMethods);
|
|
16
|
+
}
|
|
17
|
+
notify(_message) {
|
|
18
|
+
/* no-op */
|
|
19
|
+
}
|
|
20
|
+
get headers() {
|
|
21
|
+
return this.headersDefault;
|
|
22
|
+
}
|
|
23
|
+
set headers(next) {
|
|
24
|
+
this.headersDefault = {
|
|
25
|
+
...this.headersDefault,
|
|
26
|
+
...this.headersToRecord(next),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
async request(url, options = {}, params) {
|
|
30
|
+
const search = buildTransportQuery(params);
|
|
31
|
+
const urlWithQuery = `${url}${search ? `?${search}` : ''}`;
|
|
32
|
+
const optionHeaders = this.headersToRecord(options.headers);
|
|
33
|
+
let mergedHeaders = {
|
|
34
|
+
...this.headersDefault,
|
|
35
|
+
...optionHeaders,
|
|
36
|
+
};
|
|
37
|
+
if (options.body instanceof FormData) {
|
|
38
|
+
mergedHeaders = this.withoutContentType(mergedHeaders);
|
|
39
|
+
}
|
|
40
|
+
const isAbsolute = urlWithQuery.startsWith('http://') || urlWithQuery.startsWith('https://');
|
|
41
|
+
const base = this.baseURL;
|
|
42
|
+
let finalUrl = isAbsolute ? urlWithQuery : new URL(urlWithQuery, base).toString();
|
|
43
|
+
if (finalUrl.endsWith('/'))
|
|
44
|
+
finalUrl = finalUrl.slice(0, -1);
|
|
45
|
+
try {
|
|
46
|
+
const response = await fetch(finalUrl, { ...options, headers: mergedHeaders });
|
|
47
|
+
const contentType = response.headers.get('content-type') ?? undefined;
|
|
48
|
+
const responseData = await readTransportResponseBody(response, options, contentType);
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
const message = this.formatErrorMessage(responseData, response.status);
|
|
51
|
+
const transportErrorException = new TransportErrorException(response.status, message, responseData);
|
|
52
|
+
if (transportErrorException.isServerError()) {
|
|
53
|
+
this.notify(message);
|
|
54
|
+
}
|
|
55
|
+
throw transportErrorException;
|
|
56
|
+
}
|
|
57
|
+
return responseData;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
61
|
+
console.warn('⚠️ [Transport] Fetch aborted:', url);
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
if (error instanceof TransportErrorException) {
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
console.error('❌ [Transport] Fetch error:', error);
|
|
68
|
+
throw new TransportErrorException(0, error instanceof Error ? error.message : 'Unknown error');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
headersToRecord(input) {
|
|
72
|
+
if (!input)
|
|
73
|
+
return {};
|
|
74
|
+
if (typeof Headers !== 'undefined' && input instanceof Headers) {
|
|
75
|
+
const out = {};
|
|
76
|
+
input.forEach((v, k) => (out[k] = v));
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
if (Array.isArray(input)) {
|
|
80
|
+
const out = {};
|
|
81
|
+
for (const pair of input) {
|
|
82
|
+
if (Array.isArray(pair) && typeof pair[0] === 'string' && typeof pair[1] === 'string') {
|
|
83
|
+
out[pair[0]] = pair[1];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
if (typeof input === 'object') {
|
|
89
|
+
const out = {};
|
|
90
|
+
for (const [k, v] of Object.entries(input)) {
|
|
91
|
+
if (typeof k === 'string' && typeof v === 'string')
|
|
92
|
+
out[k] = v;
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
return {};
|
|
97
|
+
}
|
|
98
|
+
withoutContentType(h) {
|
|
99
|
+
const { ['Content-Type']: _omit, ...rest } = h;
|
|
100
|
+
return rest;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# `@rnzeus/atlas/ui`
|
|
2
|
+
|
|
3
|
+
← [`@rnzeus/atlas`](../README.md)
|
|
4
|
+
|
|
5
|
+
## Documentation
|
|
6
|
+
|
|
7
|
+
- **[`toast`](./toast/README.md)** — toast notifications: mount `ToastProvider` once near the app root, then call `useToast().show()` from screens or hooks. Provider owns the queue, viewport, and plate animations.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import {ToastProvider, useToast} from '@rnzeus/atlas/ui';
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- **[`debug-dock`](./debug-dock/README.md)** — a dev-oriented floating strip docked to the right edge with a draggable peek drawer; wrap debug tools or secondary actions and disable in production with `enabled={false}` or conditional render.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import {DebugDock} from '@rnzeus/atlas/ui';
|
|
17
|
+
```
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# `@rnzeus/atlas/ui/debug-dock`
|
|
2
|
+
|
|
3
|
+
← [`@rnzeus/atlas/ui`](../README.md)
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
Floating debug UI anchored to the top-right: a slim vertical drawer handle and a horizontal strip for shortcuts or dev-only controls. Uses Reanimated and Gesture Handler for vertical drag (reposition) and tap-to-toggle expand/peek along the X axis.
|
|
8
|
+
|
|
9
|
+
## Import
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import {DebugDock} from '@rnzeus/atlas/ui';
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## API (public surface)
|
|
16
|
+
|
|
17
|
+
- `DebugDock` — layout wrapper component.
|
|
18
|
+
- `DebugDockProps`:
|
|
19
|
+
- `children` — main strip content (laid out in a row).
|
|
20
|
+
- `enabled` — default `true`; when `false`, the component renders nothing.
|
|
21
|
+
- `height` — strip height in px (default `60`).
|
|
22
|
+
- `drawerWidth` — width of the left handle area in px (default `20`).
|
|
23
|
+
- `drawerPeek` — how many px of the dock stay visible when “closed” on the X axis; defaults to `drawerWidth + 3`.
|
|
24
|
+
- `drawerContent` — optional node for the handle (default is a chevron `Text`).
|
|
25
|
+
|
|
26
|
+
## Usage (minimal)
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import type {ReactNode} from 'react';
|
|
30
|
+
import {DebugDock} from '@rnzeus/atlas/ui';
|
|
31
|
+
import {Text} from 'react-native';
|
|
32
|
+
|
|
33
|
+
export function AppWithDebugShell({children}: {children: ReactNode}) {
|
|
34
|
+
const showDock = __DEV__;
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<>
|
|
38
|
+
{children}
|
|
39
|
+
<DebugDock enabled={showDock} drawerContent={<Text style={{color: '#fff'}}>⋮</Text>}>
|
|
40
|
+
<Text style={{color: '#fff'}}>Open menu</Text>
|
|
41
|
+
</DebugDock>
|
|
42
|
+
</>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Notes
|
|
48
|
+
|
|
49
|
+
- Positioning is `absolute` at the top; vertical position respects device height and safe-area insets via `useDebugDock` (internal).
|
|
50
|
+
- Depends on `react-native-reanimated`, `react-native-gesture-handler`, and `react-native-safe-area-context` (peer expectations match a typical RN app with gesture + safe area already set up).
|
|
51
|
+
- Intended for **development / internal builds**; avoid shipping `enabled={true}` in production or gate with `__DEV__` / build flags.
|
|
52
|
+
|
|
53
|
+
## Sources
|
|
54
|
+
|
|
55
|
+
- [`index`](./index.js)
|
|
56
|
+
- [`debug-dock`](./debug-dock.js)
|
|
57
|
+
- [`use-debug-dock`](./use-debug-dock.js)
|
|
58
|
+
- [`styles`](./styles.js)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { JSX, ReactNode } from 'react';
|
|
2
|
+
export type DebugDockProps = {
|
|
3
|
+
children: ReactNode;
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
height?: number;
|
|
6
|
+
drawerWidth?: number;
|
|
7
|
+
drawerPeek?: number;
|
|
8
|
+
drawerContent?: ReactNode;
|
|
9
|
+
};
|
|
10
|
+
export declare function DebugDock({ children, enabled, height, drawerWidth, drawerPeek, drawerContent, }: DebugDockProps): JSX.Element | null;
|
|
11
|
+
export declare namespace DebugDock {
|
|
12
|
+
var displayName: string;
|
|
13
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { GestureDetector } from 'react-native-gesture-handler';
|
|
4
|
+
import Animated from 'react-native-reanimated';
|
|
5
|
+
import { Text, View } from 'react-native';
|
|
6
|
+
import { displayName } from '../../utils';
|
|
7
|
+
import { styles } from './styles';
|
|
8
|
+
import { useDebugDock } from './use-debug-dock';
|
|
9
|
+
export function DebugDock({ children, enabled = true, height = 60, drawerWidth = 20, drawerPeek, drawerContent, }) {
|
|
10
|
+
const resolvedDrawerPeek = useMemo(() => drawerPeek ?? drawerWidth + 3, [drawerPeek, drawerWidth]);
|
|
11
|
+
const { containerAnimatedStyle, panGesture, onContainerLayout } = useDebugDock({
|
|
12
|
+
height,
|
|
13
|
+
drawerWidth,
|
|
14
|
+
drawerPeek: resolvedDrawerPeek,
|
|
15
|
+
});
|
|
16
|
+
if (!enabled)
|
|
17
|
+
return null;
|
|
18
|
+
return (_jsxs(Animated.View, { onLayout: onContainerLayout, style: [styles.container, { height }, containerAnimatedStyle], children: [_jsx(GestureDetector, { gesture: panGesture, children: _jsx(View, { style: [styles.drawer, { width: drawerWidth }], children: drawerContent ?? _jsx(Text, { style: styles.drawerIcon, children: "\u276E" }) }) }), _jsx(View, { style: styles.content, children: children })] }));
|
|
19
|
+
}
|
|
20
|
+
DebugDock.displayName = displayName('ui', 'debug-dock');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DebugDock } from './debug-dock';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { TextStyle, ViewStyle } from 'react-native';
|
|
2
|
+
export type DebugDockStyles = {
|
|
3
|
+
readonly container: ViewStyle;
|
|
4
|
+
readonly drawer: ViewStyle;
|
|
5
|
+
readonly drawerIcon: TextStyle;
|
|
6
|
+
readonly content: ViewStyle;
|
|
7
|
+
};
|
|
8
|
+
export declare const styles: DebugDockStyles;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native';
|
|
2
|
+
export const styles = StyleSheet.create({
|
|
3
|
+
container: {
|
|
4
|
+
alignItems: 'center',
|
|
5
|
+
backgroundColor: '#444444',
|
|
6
|
+
borderBottomLeftRadius: 10,
|
|
7
|
+
borderTopLeftRadius: 10,
|
|
8
|
+
flexDirection: 'row',
|
|
9
|
+
height: 60,
|
|
10
|
+
justifyContent: 'center',
|
|
11
|
+
paddingRight: 20,
|
|
12
|
+
position: 'absolute',
|
|
13
|
+
top: 0,
|
|
14
|
+
},
|
|
15
|
+
content: { flexDirection: 'row', gap: 20 },
|
|
16
|
+
drawer: {
|
|
17
|
+
alignItems: 'center',
|
|
18
|
+
borderBottomLeftRadius: 10,
|
|
19
|
+
borderTopLeftRadius: 10,
|
|
20
|
+
height: '100%',
|
|
21
|
+
justifyContent: 'center',
|
|
22
|
+
width: 20,
|
|
23
|
+
},
|
|
24
|
+
drawerIcon: { color: 'white', fontSize: 30 },
|
|
25
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { LayoutChangeEvent } from 'react-native';
|
|
2
|
+
export type UseDebugDockOptions = {
|
|
3
|
+
height: number;
|
|
4
|
+
drawerWidth: number;
|
|
5
|
+
drawerPeek: number;
|
|
6
|
+
};
|
|
7
|
+
export declare function useDebugDock({ height, drawerWidth, drawerPeek }: UseDebugDockOptions): {
|
|
8
|
+
containerAnimatedStyle: {
|
|
9
|
+
left: number;
|
|
10
|
+
transform: ({
|
|
11
|
+
translateY: number;
|
|
12
|
+
translateX?: undefined;
|
|
13
|
+
} | {
|
|
14
|
+
translateX: number;
|
|
15
|
+
translateY?: undefined;
|
|
16
|
+
})[];
|
|
17
|
+
};
|
|
18
|
+
panGesture: import("react-native-gesture-handler/lib/typescript/handlers/gestures/panGesture").PanGesture;
|
|
19
|
+
onContainerLayout: (event: LayoutChangeEvent) => void;
|
|
20
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { useCallback, useMemo } from 'react';
|
|
2
|
+
import { Gesture } from 'react-native-gesture-handler';
|
|
3
|
+
import { cancelAnimation, interpolate, ReduceMotion, useAnimatedStyle, useSharedValue, withDecay, withTiming, } from 'react-native-reanimated';
|
|
4
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
5
|
+
import { useWindowDimensions } from 'react-native';
|
|
6
|
+
export function useDebugDock({ height, drawerWidth, drawerPeek }) {
|
|
7
|
+
const insets = useSafeAreaInsets();
|
|
8
|
+
const { height: windowHeight, width: windowWidth } = useWindowDimensions();
|
|
9
|
+
const containerWidth = useSharedValue(0);
|
|
10
|
+
const dockLevel = useSharedValue(0);
|
|
11
|
+
const isDragging = useSharedValue(false);
|
|
12
|
+
const translateY = useSharedValue(Math.max(insets.top, windowHeight - height - insets.bottom));
|
|
13
|
+
const clampY = useMemo(() => {
|
|
14
|
+
const min = insets.top;
|
|
15
|
+
const max = Math.max(insets.top, windowHeight - height - insets.bottom);
|
|
16
|
+
return { min, max };
|
|
17
|
+
}, [height, insets.bottom, insets.top, windowHeight]);
|
|
18
|
+
const onContainerLayout = useCallback((event) => {
|
|
19
|
+
const width = event.nativeEvent.layout.width;
|
|
20
|
+
if (width <= 0)
|
|
21
|
+
return;
|
|
22
|
+
containerWidth.value = width;
|
|
23
|
+
if (dockLevel.value === 0) {
|
|
24
|
+
dockLevel.value = 1;
|
|
25
|
+
}
|
|
26
|
+
}, [containerWidth, dockLevel]);
|
|
27
|
+
const panGesture = useMemo(() => {
|
|
28
|
+
return Gesture.Pan()
|
|
29
|
+
.onTouchesUp(() => {
|
|
30
|
+
'worklet';
|
|
31
|
+
if (isDragging.value)
|
|
32
|
+
return;
|
|
33
|
+
if (dockLevel.value === 0)
|
|
34
|
+
return;
|
|
35
|
+
dockLevel.value = withTiming(dockLevel.value === 1 ? 2 : 1, { duration: 300 });
|
|
36
|
+
})
|
|
37
|
+
.onStart(() => {
|
|
38
|
+
isDragging.value = true;
|
|
39
|
+
cancelAnimation(translateY);
|
|
40
|
+
})
|
|
41
|
+
.onChange((event) => {
|
|
42
|
+
translateY.value = Math.min(Math.max(Math.abs(translateY.value) + event.changeY, clampY.min), clampY.max);
|
|
43
|
+
})
|
|
44
|
+
.onFinalize((event) => {
|
|
45
|
+
translateY.value = withDecay({
|
|
46
|
+
velocity: event.velocityY,
|
|
47
|
+
velocityFactor: 0.25,
|
|
48
|
+
reduceMotion: ReduceMotion.System,
|
|
49
|
+
clamp: [clampY.min, clampY.max],
|
|
50
|
+
}, () => {
|
|
51
|
+
isDragging.value = false;
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}, [clampY.max, clampY.min]);
|
|
55
|
+
const containerAnimatedStyle = useAnimatedStyle(() => {
|
|
56
|
+
const openX = -containerWidth.value;
|
|
57
|
+
const peekX = -(drawerPeek > 0 ? drawerPeek : drawerWidth);
|
|
58
|
+
return {
|
|
59
|
+
left: windowWidth,
|
|
60
|
+
transform: [
|
|
61
|
+
{ translateY: translateY.value },
|
|
62
|
+
{ translateX: interpolate(dockLevel.value, [0, 1, 2], [0, peekX, openX]) },
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
}, [drawerPeek, drawerWidth, windowWidth]);
|
|
66
|
+
return {
|
|
67
|
+
containerAnimatedStyle,
|
|
68
|
+
panGesture,
|
|
69
|
+
onContainerLayout,
|
|
70
|
+
};
|
|
71
|
+
}
|
package/dist/ui/index.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# `@rnzeus/atlas/ui/toast`
|
|
2
|
+
|
|
3
|
+
← [`@rnzeus/atlas/ui`](../README.md)
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
Toast system for React Native: global toast API (`useToast`), provider-managed queue, and animated viewport/plates.
|
|
8
|
+
|
|
9
|
+
## Import
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import {ToastProvider, useToast} from '@rnzeus/atlas/ui';
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## API (public surface)
|
|
16
|
+
|
|
17
|
+
- `ToastProvider`
|
|
18
|
+
- `useToast() -> { show, dismiss, clear }`
|
|
19
|
+
|
|
20
|
+
## Usage (minimal)
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import {ToastProvider, useToast} from '@rnzeus/atlas/ui';
|
|
24
|
+
|
|
25
|
+
function SaveButton() {
|
|
26
|
+
const toast = useToast();
|
|
27
|
+
return (
|
|
28
|
+
<Button
|
|
29
|
+
title="Save"
|
|
30
|
+
onPress={() =>
|
|
31
|
+
toast.show({
|
|
32
|
+
render: () => <Text>Saved</Text>,
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function App() {
|
|
40
|
+
return (
|
|
41
|
+
<ToastProvider>
|
|
42
|
+
<SaveButton />
|
|
43
|
+
</ToastProvider>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Notes
|
|
49
|
+
|
|
50
|
+
- `ToastProvider` renders `ToastViewport` internally; usually you only mount the provider once near app root.
|
|
51
|
+
- `show()` supports dedupe via `uuid` and limits concurrent toasts by `maxToasts`.
|
|
52
|
+
- `autoHide` defaults to `true`; default `visibilityTime` is `3000`.
|
|
53
|
+
|
|
54
|
+
## Sources
|
|
55
|
+
|
|
56
|
+
- [`index`](./index.js)
|
|
57
|
+
- [`toast-provider`](./toast-provider.js)
|
|
58
|
+
- [`context`](./context.js)
|
|
59
|
+
- [`use-toast`](./use-toast.js)
|
|
60
|
+
- [`types`](./types.js)
|
|
61
|
+
- [`toast-viewport`](./toast-viewport/toast-viewport.js)
|
|
62
|
+
- [`toast-plate`](./toast-plate/toast-plate.js)
|