akanjs 2.2.4-rc.2 → 2.2.4-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/constant/getDefault.ts +1 -1
- package/fetch/client/fetchClient.ts +1 -7
- package/package.json +1 -5
- package/service/injectInfo.ts +46 -10
- package/service/predefinedAdaptor/cache.adaptor.ts +13 -0
- package/service/predefinedAdaptor/database.adaptor.ts +59 -13
- package/service/predefinedAdaptor/solidCache.adaptor.ts +23 -0
- package/types/service/injectInfo.d.ts +6 -0
- package/types/service/predefinedAdaptor/cache.adaptor.d.ts +6 -0
- package/types/service/predefinedAdaptor/database.adaptor.d.ts +3 -1
- package/types/service/predefinedAdaptor/solidCache.adaptor.d.ts +3 -0
- package/types/ui/Dialog/Modal.d.ts +1 -1
- package/types/ui/Dialog/index.d.ts +1 -1
- package/types/ui/Modal.d.ts +1 -12
- package/ui/Dialog/Modal.tsx +181 -70
- package/ui/Dialog/Provider.tsx +3 -6
- package/ui/Modal.tsx +0 -44
package/constant/getDefault.ts
CHANGED
|
@@ -6,7 +6,7 @@ export const getDefault = <T>(fieldObj: FieldObject): DefaultOf<T> => {
|
|
|
6
6
|
const result: Record<string, unknown> = {};
|
|
7
7
|
for (const [key, field] of Object.entries(fieldObj)) {
|
|
8
8
|
if (field.fieldType === "hidden") result[key] = null;
|
|
9
|
-
else if (field.default) {
|
|
9
|
+
else if (field.default !== undefined && field.default !== null) {
|
|
10
10
|
if (typeof field.default === "function") result[key] = (field.default as () => object)();
|
|
11
11
|
else result[key] = field.default as object;
|
|
12
12
|
} else if (field.isArray) result[key] = [];
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { DataList, getEnv, PrimitiveRegistry, type PromiseOrObject } from "akanjs/base";
|
|
2
|
-
import {
|
|
3
|
-
capitalize,
|
|
4
|
-
type FetchPolicy,
|
|
5
|
-
fileUploadContract,
|
|
6
|
-
Logger,
|
|
7
|
-
resolveFileUploadCapability,
|
|
8
|
-
} from "akanjs/common";
|
|
2
|
+
import { capitalize, type FetchPolicy, fileUploadContract, Logger, resolveFileUploadCapability } from "akanjs/common";
|
|
9
3
|
import { type BaseInsight, type BaseObject, ConstantRegistry, deserialize, serialize } from "akanjs/constant";
|
|
10
4
|
import type {
|
|
11
5
|
DatabaseSignal,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akanjs",
|
|
3
|
-
"version": "2.2.4-rc.
|
|
3
|
+
"version": "2.2.4-rc.4",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -175,7 +175,6 @@
|
|
|
175
175
|
"@capgo/capacitor-updater": "^8.46.1",
|
|
176
176
|
"@libsql/client": "^0.17.3",
|
|
177
177
|
"@playwright/test": "^1.60.0",
|
|
178
|
-
"@radix-ui/react-dialog": "^1.1.15",
|
|
179
178
|
"@react-spring/web": "^10.1.0",
|
|
180
179
|
"@use-gesture/react": "^10.3.1",
|
|
181
180
|
"bullmq": "^5.76.10",
|
|
@@ -250,9 +249,6 @@
|
|
|
250
249
|
"@playwright/test": {
|
|
251
250
|
"optional": true
|
|
252
251
|
},
|
|
253
|
-
"@radix-ui/react-dialog": {
|
|
254
|
-
"optional": true
|
|
255
|
-
},
|
|
256
252
|
"@react-spring/web": {
|
|
257
253
|
"optional": true
|
|
258
254
|
},
|
package/service/injectInfo.ts
CHANGED
|
@@ -254,20 +254,47 @@ export class InjectInfo<
|
|
|
254
254
|
enumerable: true,
|
|
255
255
|
});
|
|
256
256
|
} else if (injectInfo.isMap) {
|
|
257
|
+
const topic = `akan:memory:${injectInfo.parentRefName}`;
|
|
258
|
+
const getter = injectInfo.get as unknown as (value: unknown) => unknown;
|
|
259
|
+
const setter = injectInfo.set as unknown as (value: unknown) => string | number | Buffer;
|
|
260
|
+
const get = async (key: string) => {
|
|
261
|
+
const value = await cacheAdaptor.hget(topic, propKey, key);
|
|
262
|
+
return value === undefined || value === null ? undefined : getter(value);
|
|
263
|
+
};
|
|
264
|
+
const set = async (key: string, value: unknown) => {
|
|
265
|
+
const setValue = setter(value);
|
|
266
|
+
await cacheAdaptor.hset(topic, propKey, key, setValue);
|
|
267
|
+
};
|
|
257
268
|
Object.defineProperty(instance, propKey, {
|
|
258
269
|
value: {
|
|
259
|
-
get
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
270
|
+
get,
|
|
271
|
+
set,
|
|
272
|
+
delete: async (key: string) => {
|
|
273
|
+
await cacheAdaptor.hdelete(topic, propKey, key);
|
|
263
274
|
},
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
await
|
|
275
|
+
getOrInsert: async (key: string, value: unknown) => {
|
|
276
|
+
const existingValue = await get(key);
|
|
277
|
+
if (existingValue !== undefined) return existingValue;
|
|
278
|
+
await set(key, value);
|
|
279
|
+
return value;
|
|
268
280
|
},
|
|
269
|
-
|
|
270
|
-
await
|
|
281
|
+
getOrInsertComputed: async (key: string, compute: (key: string) => unknown | Promise<unknown>) => {
|
|
282
|
+
const existingValue = await get(key);
|
|
283
|
+
if (existingValue !== undefined) return existingValue;
|
|
284
|
+
const value = await compute(key);
|
|
285
|
+
await set(key, value);
|
|
286
|
+
return value;
|
|
287
|
+
},
|
|
288
|
+
keys: async () => await cacheAdaptor.hkeys(topic, propKey),
|
|
289
|
+
entries: async () => {
|
|
290
|
+
const entries = await cacheAdaptor.hentries(topic, propKey);
|
|
291
|
+
return entries.map(([key, value]) => [key, getter(value)]);
|
|
292
|
+
},
|
|
293
|
+
forEach: async (callback: (value: unknown, key: string) => void | Promise<void>) => {
|
|
294
|
+
for (const [key, value] of await cacheAdaptor.hentries(topic, propKey)) await callback(getter(value), key);
|
|
295
|
+
},
|
|
296
|
+
clear: async () => {
|
|
297
|
+
await cacheAdaptor.hclear(topic, propKey);
|
|
271
298
|
},
|
|
272
299
|
},
|
|
273
300
|
});
|
|
@@ -360,6 +387,15 @@ export const injectionBuilder = (parentRefName: string) => ({
|
|
|
360
387
|
get: (key: string) => Promise<MapFieldValue | undefined>;
|
|
361
388
|
set: (key: string, value: MapFieldValue) => Promise<void>;
|
|
362
389
|
delete: (key: string) => Promise<void>;
|
|
390
|
+
getOrInsert: (key: string, value: MapFieldValue) => Promise<MapFieldValue>;
|
|
391
|
+
getOrInsertComputed: (
|
|
392
|
+
key: string,
|
|
393
|
+
compute: (key: string) => MapFieldValue | Promise<MapFieldValue>,
|
|
394
|
+
) => Promise<MapFieldValue>;
|
|
395
|
+
keys: () => Promise<string[]>;
|
|
396
|
+
entries: () => Promise<[string, MapFieldValue][]>;
|
|
397
|
+
forEach: (callback: (value: MapFieldValue, key: string) => void | Promise<void>) => Promise<void>;
|
|
398
|
+
clear: () => Promise<void>;
|
|
363
399
|
}
|
|
364
400
|
: { get: () => Promise<UseValue>; set: (value: UseValue) => Promise<void>; delete: () => Promise<void> },
|
|
365
401
|
never,
|
|
@@ -16,6 +16,9 @@ export interface CacheAdaptor {
|
|
|
16
16
|
): Promise<void>;
|
|
17
17
|
hget<T extends string | number | Buffer>(topic: string, key: string, subKey: string): Promise<T | undefined>;
|
|
18
18
|
hdelete(topic: string, key: string, subKey: string): Promise<void>;
|
|
19
|
+
hkeys(topic: string, key: string): Promise<string[]>;
|
|
20
|
+
hentries<T extends string | number | Buffer>(topic: string, key: string): Promise<[string, T][]>;
|
|
21
|
+
hclear(topic: string, key: string): Promise<void>;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
interface RedisEnv extends BaseEnv {
|
|
@@ -96,6 +99,16 @@ export class RedisCache
|
|
|
96
99
|
async hdelete(topic: string, key: string, subKey: string): Promise<void> {
|
|
97
100
|
await this.redis.hdel(`${topic}:${key}`, subKey);
|
|
98
101
|
}
|
|
102
|
+
async hkeys(topic: string, key: string): Promise<string[]> {
|
|
103
|
+
return await this.redis.hkeys(`${topic}:${key}`);
|
|
104
|
+
}
|
|
105
|
+
async hentries<T extends string | number | Buffer>(topic: string, key: string): Promise<[string, T][]> {
|
|
106
|
+
const values = await this.redis.hgetall(`${topic}:${key}`);
|
|
107
|
+
return Object.entries(values) as [string, T][];
|
|
108
|
+
}
|
|
109
|
+
async hclear(topic: string, key: string): Promise<void> {
|
|
110
|
+
await this.redis.del(`${topic}:${key}`);
|
|
111
|
+
}
|
|
99
112
|
getClient(): Redis {
|
|
100
113
|
return this.redis;
|
|
101
114
|
}
|
|
@@ -4,7 +4,7 @@ import { mkdir } from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import type { InArgs, InValue, Client as LibsqlClient } from "@libsql/client";
|
|
6
6
|
import { type BaseEnv, dayjs, FIELD_META, type PromiseOrObject } from "akanjs/base";
|
|
7
|
-
import type
|
|
7
|
+
import { type ConstantModel, getDefault } from "akanjs/constant";
|
|
8
8
|
import {
|
|
9
9
|
createDocumentId,
|
|
10
10
|
type DatabaseModel,
|
|
@@ -773,6 +773,9 @@ export class SqliteDocumentStore {
|
|
|
773
773
|
} else {
|
|
774
774
|
doc[key] = value;
|
|
775
775
|
}
|
|
776
|
+
if ((props.isClass as boolean) && (props.isScalar as boolean) && doc[key] !== undefined && doc[key] !== null) {
|
|
777
|
+
doc[key] = this.applyNestedDefaults(doc[key], props);
|
|
778
|
+
}
|
|
776
779
|
if (props.enum && doc[key] !== undefined && doc[key] !== null) {
|
|
777
780
|
const values = Array.isArray(doc[key]) ? doc[key] : [doc[key]];
|
|
778
781
|
const fieldEnum = props.enum as { has: (value: unknown) => boolean } | undefined;
|
|
@@ -958,12 +961,28 @@ export class SqliteDocumentStore {
|
|
|
958
961
|
|
|
959
962
|
private decodeDocumentPayload(payload: Record<string, unknown>) {
|
|
960
963
|
const fields = this.database.doc[FIELD_META] as unknown as FieldMap;
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
964
|
+
const result: Record<string, unknown> = {};
|
|
965
|
+
for (const [key, fieldMeta] of Object.entries(fields)) {
|
|
966
|
+
if (BASE_COLUMNS.has(key)) continue;
|
|
967
|
+
const props = fieldMeta.getProps();
|
|
968
|
+
const value = payload[key];
|
|
969
|
+
if (value === undefined) {
|
|
970
|
+
const def = props.default;
|
|
971
|
+
if (def !== undefined && def !== null) {
|
|
972
|
+
result[key] = typeof def === "function" ? (def as (data: unknown) => unknown)(payload) : def;
|
|
973
|
+
} else if (props.nullable) {
|
|
974
|
+
result[key] = null;
|
|
975
|
+
}
|
|
976
|
+
} else {
|
|
977
|
+
result[key] = this.decodeFieldValue(value, props);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
981
|
+
if (key in result || BASE_COLUMNS.has(key)) continue;
|
|
982
|
+
const props = fields[key]?.getProps?.();
|
|
983
|
+
result[key] = props ? this.decodeFieldValue(value, props) : value;
|
|
984
|
+
}
|
|
985
|
+
return result;
|
|
967
986
|
}
|
|
968
987
|
|
|
969
988
|
private decodeFieldValue(value: unknown, props: Record<string, unknown>): unknown {
|
|
@@ -991,12 +1010,39 @@ export class SqliteDocumentStore {
|
|
|
991
1010
|
if (!props.isClass || !props.isScalar) return value;
|
|
992
1011
|
const scalarFields = (props.modelRef as { [FIELD_META]?: FieldMap } | undefined)?.[FIELD_META];
|
|
993
1012
|
if (!scalarFields) return value;
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1013
|
+
const source = value as Record<string, unknown>;
|
|
1014
|
+
const defaults = getDefault(scalarFields as never) as Record<string, unknown>;
|
|
1015
|
+
const result: Record<string, unknown> = {};
|
|
1016
|
+
for (const [key, fieldMeta] of Object.entries(scalarFields)) {
|
|
1017
|
+
const nestedProps = fieldMeta.getProps();
|
|
1018
|
+
const nested = source[key];
|
|
1019
|
+
result[key] = nested === undefined ? defaults[key] : this.decodeFieldValue(nested, nestedProps);
|
|
1020
|
+
}
|
|
1021
|
+
for (const [key, nested] of Object.entries(source)) {
|
|
1022
|
+
if (!(key in result)) result[key] = nested;
|
|
1023
|
+
}
|
|
1024
|
+
return result;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
private applyNestedDefaults(value: unknown, props: Record<string, unknown>): unknown {
|
|
1028
|
+
if (value === undefined || value === null) return value;
|
|
1029
|
+
if (!props.isClass || !props.isScalar) return value;
|
|
1030
|
+
if (Array.isArray(value)) return value.map((item) => this.fillScalarDefaults(item, props));
|
|
1031
|
+
return this.fillScalarDefaults(value, props);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
private fillScalarDefaults(value: unknown, props: Record<string, unknown>): unknown {
|
|
1035
|
+
if (!value || typeof value !== "object") return value;
|
|
1036
|
+
const scalarFields = (props.modelRef as { [FIELD_META]?: FieldMap } | undefined)?.[FIELD_META];
|
|
1037
|
+
if (!scalarFields) return value;
|
|
1038
|
+
const defaults = getDefault(scalarFields as never) as Record<string, unknown>;
|
|
1039
|
+
const result = { ...(value as Record<string, unknown>) };
|
|
1040
|
+
for (const [key, fieldMeta] of Object.entries(scalarFields)) {
|
|
1041
|
+
const nestedProps = fieldMeta.getProps();
|
|
1042
|
+
if (result[key] === undefined) result[key] = defaults[key];
|
|
1043
|
+
else result[key] = this.applyNestedDefaults(result[key], nestedProps);
|
|
1044
|
+
}
|
|
1045
|
+
return result;
|
|
1000
1046
|
}
|
|
1001
1047
|
|
|
1002
1048
|
hydrate(data: DocumentRecord, originalData: DocumentRecord = data) {
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "./solidSqlite";
|
|
15
15
|
|
|
16
16
|
type CacheRow = { value: string | Buffer | null; valueType: SolidValueType; expiresAt: number | null };
|
|
17
|
+
type CacheEntryRow = CacheRow & { subKey: string };
|
|
17
18
|
|
|
18
19
|
export class SolidCache
|
|
19
20
|
extends adapt("solidCache", ({ env }) => ({
|
|
@@ -135,6 +136,28 @@ export class SolidCache
|
|
|
135
136
|
.run(topic, key, subKey);
|
|
136
137
|
}
|
|
137
138
|
|
|
139
|
+
async hkeys(topic: string, key: string): Promise<string[]> {
|
|
140
|
+
this.#cleanup();
|
|
141
|
+
const rows = this.#db
|
|
142
|
+
.query(`SELECT "subKey" FROM "_akan_solid_cache_hash" WHERE "topic" = ? AND "key" = ? ORDER BY "subKey" ASC`)
|
|
143
|
+
.all(topic, key) as { subKey: string }[];
|
|
144
|
+
return rows.map((row) => row.subKey);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async hentries<T extends string | number | Buffer>(topic: string, key: string): Promise<[string, T][]> {
|
|
148
|
+
this.#cleanup();
|
|
149
|
+
const rows = this.#db
|
|
150
|
+
.query(
|
|
151
|
+
`SELECT "subKey", "value", "valueType", "expiresAt" FROM "_akan_solid_cache_hash" WHERE "topic" = ? AND "key" = ? ORDER BY "subKey" ASC`,
|
|
152
|
+
)
|
|
153
|
+
.all(topic, key) as CacheEntryRow[];
|
|
154
|
+
return rows.map((row) => [row.subKey, decodeSolidValue<T>(row.valueType, row.value) as T]);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async hclear(topic: string, key: string): Promise<void> {
|
|
158
|
+
this.#db.query(`DELETE FROM "_akan_solid_cache_hash" WHERE "topic" = ? AND "key" = ?`).run(topic, key);
|
|
159
|
+
}
|
|
160
|
+
|
|
138
161
|
#cleanup() {
|
|
139
162
|
const now = Date.now();
|
|
140
163
|
this.#db.query(`DELETE FROM "_akan_solid_cache" WHERE "expiresAt" IS NOT NULL AND "expiresAt" <= ?`).run(now);
|
|
@@ -79,6 +79,12 @@ export declare const injectionBuilder: (parentRefName: string) => {
|
|
|
79
79
|
get: (key: string) => Promise<(never extends GetFn ? FieldToValue<MapValue> : ReturnType<GetFn>) | undefined>;
|
|
80
80
|
set: (key: string, value: never extends GetFn ? FieldToValue<MapValue> : ReturnType<GetFn>) => Promise<void>;
|
|
81
81
|
delete: (key: string) => Promise<void>;
|
|
82
|
+
getOrInsert: (key: string, value: never extends GetFn ? FieldToValue<MapValue> : ReturnType<GetFn>) => Promise<never extends GetFn ? FieldToValue<MapValue> : ReturnType<GetFn>>;
|
|
83
|
+
getOrInsertComputed: (key: string, compute: (key: string) => (never extends GetFn ? FieldToValue<MapValue> : ReturnType<GetFn>) | Promise<never extends GetFn ? FieldToValue<MapValue> : ReturnType<GetFn>>) => Promise<never extends GetFn ? FieldToValue<MapValue> : ReturnType<GetFn>>;
|
|
84
|
+
keys: () => Promise<string[]>;
|
|
85
|
+
entries: () => Promise<[string, never extends GetFn ? FieldToValue<MapValue> : ReturnType<GetFn>][]>;
|
|
86
|
+
forEach: (callback: (value: never extends GetFn ? FieldToValue<MapValue> : ReturnType<GetFn>, key: string) => void | Promise<void>) => Promise<void>;
|
|
87
|
+
clear: () => Promise<void>;
|
|
82
88
|
} : {
|
|
83
89
|
get: () => Promise<(DefaultValue extends never ? true : false) extends true ? (never extends GetFn ? GetFieldValue<ValueRef, ExplicitType, MapValue> : ReturnType<GetFn>) | null : never extends GetFn ? GetFieldValue<ValueRef, ExplicitType, MapValue> : ReturnType<GetFn>>;
|
|
84
90
|
set: (value: (DefaultValue extends never ? true : false) extends true ? (never extends GetFn ? GetFieldValue<ValueRef, ExplicitType, MapValue> : ReturnType<GetFn>) | null : never extends GetFn ? GetFieldValue<ValueRef, ExplicitType, MapValue> : ReturnType<GetFn>) => Promise<void>;
|
|
@@ -12,6 +12,9 @@ export interface CacheAdaptor {
|
|
|
12
12
|
}): Promise<void>;
|
|
13
13
|
hget<T extends string | number | Buffer>(topic: string, key: string, subKey: string): Promise<T | undefined>;
|
|
14
14
|
hdelete(topic: string, key: string, subKey: string): Promise<void>;
|
|
15
|
+
hkeys(topic: string, key: string): Promise<string[]>;
|
|
16
|
+
hentries<T extends string | number | Buffer>(topic: string, key: string): Promise<[string, T][]>;
|
|
17
|
+
hclear(topic: string, key: string): Promise<void>;
|
|
15
18
|
}
|
|
16
19
|
interface RedisEnv extends BaseEnv {
|
|
17
20
|
redis?: {
|
|
@@ -34,6 +37,9 @@ export declare class RedisCache extends RedisCache_base implements CacheAdaptor
|
|
|
34
37
|
}): Promise<void>;
|
|
35
38
|
hget<T extends string | number | Buffer>(topic: string, key: string, subKey: string): Promise<T | undefined>;
|
|
36
39
|
hdelete(topic: string, key: string, subKey: string): Promise<void>;
|
|
40
|
+
hkeys(topic: string, key: string): Promise<string[]>;
|
|
41
|
+
hentries<T extends string | number | Buffer>(topic: string, key: string): Promise<[string, T][]>;
|
|
42
|
+
hclear(topic: string, key: string): Promise<void>;
|
|
37
43
|
getClient(): Redis;
|
|
38
44
|
onDestroy(): Promise<void>;
|
|
39
45
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
2
|
import type { Client as LibsqlClient } from "@libsql/client";
|
|
3
3
|
import { type BaseEnv, type PromiseOrObject } from "akanjs/base";
|
|
4
|
-
import type
|
|
4
|
+
import { type ConstantModel } from "akanjs/constant";
|
|
5
5
|
import { type DatabaseModel, type DocumentQuery, type DocumentSchema, type DocumentUpdate, type DocumentUpdateOptions, type SchemaOf } from "akanjs/document";
|
|
6
6
|
import type { Sql } from "postgres";
|
|
7
7
|
export interface SqliteDatabaseConfig {
|
|
@@ -256,6 +256,8 @@ export declare class SqliteDocumentStore {
|
|
|
256
256
|
private decodeFieldValue;
|
|
257
257
|
private decodeMapValue;
|
|
258
258
|
private decodeNestedValue;
|
|
259
|
+
private applyNestedDefaults;
|
|
260
|
+
private fillScalarDefaults;
|
|
259
261
|
hydrate(data: DocumentRecord, originalData?: DocumentRecord): any;
|
|
260
262
|
private runHooks;
|
|
261
263
|
private insertStmt;
|
|
@@ -18,5 +18,8 @@ export declare class SolidCache extends SolidCache_base implements CacheAdaptor
|
|
|
18
18
|
}): Promise<void>;
|
|
19
19
|
hget<T extends string | number | Buffer>(topic: string, key: string, subKey: string): Promise<T | undefined>;
|
|
20
20
|
hdelete(topic: string, key: string, subKey: string): Promise<void>;
|
|
21
|
+
hkeys(topic: string, key: string): Promise<string[]>;
|
|
22
|
+
hentries<T extends string | number | Buffer>(topic: string, key: string): Promise<[string, T][]>;
|
|
23
|
+
hclear(topic: string, key: string): Promise<void>;
|
|
21
24
|
}
|
|
22
25
|
export {};
|
|
@@ -6,4 +6,4 @@ export interface ModalProps {
|
|
|
6
6
|
children?: ReactNode;
|
|
7
7
|
onCancel?: () => void;
|
|
8
8
|
}
|
|
9
|
-
export declare const Modal: ({ className, bodyClassName, confirmClose, children, onCancel }: ModalProps) => import("react
|
|
9
|
+
export declare const Modal: ({ className, bodyClassName, confirmClose, children, onCancel }: ModalProps) => import("react").ReactPortal | null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ProviderProps } from "./Provider.d.ts";
|
|
2
2
|
export declare const Dialog: {
|
|
3
3
|
({ children, ...props }: ProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
4
|
-
Modal: ({ className, bodyClassName, confirmClose, children, onCancel }: import("./Modal.d.ts").ModalProps) => import("react
|
|
4
|
+
Modal: ({ className, bodyClassName, confirmClose, children, onCancel }: import("./Modal.d.ts").ModalProps) => import("react").ReactPortal | null;
|
|
5
5
|
Title: ({ children }: import("./Title.d.ts").TitleProps) => null;
|
|
6
6
|
Action: ({ children }: import("./Action.d.ts").ActionProps) => null;
|
|
7
7
|
Trigger: ({ className, children }: import("./Trigger.d.ts").TriggerProps) => import("react/jsx-runtime").JSX.Element;
|
package/types/ui/Modal.d.ts
CHANGED
|
@@ -16,15 +16,4 @@ export interface ModalProps {
|
|
|
16
16
|
/** Ask for close confirmation before dismissing. */
|
|
17
17
|
confirmClose?: boolean;
|
|
18
18
|
}
|
|
19
|
-
export declare const Modal: {
|
|
20
|
-
({ className, title, action, open, onCancel, bodyClassName, children, confirmClose, }: ModalProps): import("react/jsx-runtime").JSX.Element;
|
|
21
|
-
Window: ({ open, onCancel, title, children }: WindowProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
22
|
-
};
|
|
23
|
-
interface WindowProps {
|
|
24
|
-
open: boolean;
|
|
25
|
-
onCancel: () => void;
|
|
26
|
-
title: ReactNode;
|
|
27
|
-
children: ReactNode;
|
|
28
|
-
}
|
|
29
|
-
export declare const Window: ({ open, onCancel, title, children }: WindowProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
30
|
-
export {};
|
|
19
|
+
export declare const Modal: ({ className, title, action, open, onCancel, bodyClassName, children, confirmClose, }: ModalProps) => import("react/jsx-runtime").JSX.Element;
|
package/ui/Dialog/Modal.tsx
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import * as Dialog from "@radix-ui/react-dialog";
|
|
3
2
|
import { useDrag } from "@use-gesture/react";
|
|
4
3
|
import { clsx, usePage } from "akanjs/client";
|
|
5
4
|
import { animated } from "akanjs/ui";
|
|
6
|
-
import { type ReactNode, useContext, useEffect, useRef, useState } from "react";
|
|
5
|
+
import { type ReactNode, useCallback, useContext, useEffect, useId, useRef, useState } from "react";
|
|
6
|
+
import { createPortal } from "react-dom";
|
|
7
7
|
import { BiX } from "react-icons/bi";
|
|
8
8
|
import { config, useSpring } from "react-spring";
|
|
9
9
|
|
|
@@ -11,6 +11,8 @@ import { DialogContext } from "./context";
|
|
|
11
11
|
|
|
12
12
|
const MODAL_MARGIN = 0;
|
|
13
13
|
const OPACITY = { START: 0, END: 1 };
|
|
14
|
+
let bodyScrollLockCount = 0;
|
|
15
|
+
let previousBodyOverflow = "";
|
|
14
16
|
|
|
15
17
|
const interpolate = (o: number, i: number, t: number) => {
|
|
16
18
|
return o + (i - o) * t;
|
|
@@ -25,41 +27,80 @@ export interface ModalProps {
|
|
|
25
27
|
}
|
|
26
28
|
export const Modal = ({ className, bodyClassName, confirmClose, children, onCancel }: ModalProps) => {
|
|
27
29
|
const { open, setOpen, title, action } = useContext(DialogContext);
|
|
28
|
-
const openRef = useRef<boolean>(open);
|
|
29
30
|
const { l } = usePage();
|
|
30
31
|
const ref = useRef<HTMLDivElement>(null);
|
|
32
|
+
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
33
|
+
const closingRef = useRef(false);
|
|
34
|
+
const focusedElementRef = useRef<HTMLElement | null>(null);
|
|
35
|
+
const titleId = useId();
|
|
36
|
+
const contentId = useId();
|
|
31
37
|
const [{ translate }, api] = useSpring(() => ({ translate: 1 }));
|
|
38
|
+
const [portalElement, setPortalElement] = useState<HTMLElement | null>(null);
|
|
39
|
+
const [isMounted, setIsMounted] = useState(open);
|
|
32
40
|
const [showBackground, setShowBackground] = useState(false);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
|
|
42
|
+
const openModal = useCallback(
|
|
43
|
+
async ({ canceled }: { canceled?: boolean } = {}) => {
|
|
44
|
+
closingRef.current = false;
|
|
45
|
+
setIsMounted(true);
|
|
46
|
+
if (closeTimerRef.current) clearTimeout(closeTimerRef.current);
|
|
47
|
+
closeTimerRef.current = setTimeout(() => {
|
|
48
|
+
setShowBackground(true);
|
|
49
|
+
}, 100);
|
|
50
|
+
await Promise.all(api.start({ translate: 0, immediate: false, config: canceled ? config.wobbly : config.stiff }));
|
|
51
|
+
},
|
|
52
|
+
[api],
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const closeModal = useCallback(
|
|
56
|
+
async ({
|
|
57
|
+
velocity = 0,
|
|
58
|
+
confirmClose,
|
|
59
|
+
notifyCancel = true,
|
|
60
|
+
}: {
|
|
61
|
+
velocity?: number;
|
|
62
|
+
confirmClose?: boolean;
|
|
63
|
+
notifyCancel?: boolean;
|
|
64
|
+
}) => {
|
|
65
|
+
if (closingRef.current) return;
|
|
66
|
+
if (confirmClose && !window.confirm(l("base.confirmClose"))) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
closingRef.current = true;
|
|
71
|
+
if (closeTimerRef.current) clearTimeout(closeTimerRef.current);
|
|
72
|
+
closeTimerRef.current = setTimeout(() => {
|
|
73
|
+
setShowBackground(false);
|
|
74
|
+
}, 100);
|
|
75
|
+
await Promise.all(api.start({ translate: 1, immediate: false, config: { ...config.stiff, velocity } }));
|
|
76
|
+
setIsMounted(false);
|
|
77
|
+
setOpen(false);
|
|
78
|
+
if (notifyCancel) onCancel?.();
|
|
79
|
+
closingRef.current = false;
|
|
80
|
+
},
|
|
81
|
+
[api, l, onCancel, setOpen],
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const requestClose = useCallback(
|
|
85
|
+
(options?: { velocity?: number }) => {
|
|
86
|
+
void closeModal({ velocity: options?.velocity, confirmClose });
|
|
87
|
+
},
|
|
88
|
+
[closeModal, confirmClose],
|
|
89
|
+
);
|
|
90
|
+
|
|
50
91
|
const bind = useDrag(
|
|
51
92
|
({ last, velocity: [, vy], direction: [, dy], offset: [, oy], movement: [, my], cancel, canceled }) => {
|
|
52
93
|
if (!ref.current) return;
|
|
53
|
-
const height = (ref.current.clientHeight || MODAL_MARGIN) - MODAL_MARGIN;
|
|
94
|
+
const height = Math.max((ref.current.clientHeight || MODAL_MARGIN) - MODAL_MARGIN, 1);
|
|
54
95
|
if (my > 70) cancel();
|
|
55
96
|
if (last) {
|
|
56
|
-
if (my > height * 0.5 || (vy > 0.5 && dy > 0))
|
|
57
|
-
void closeModal({ velocity: vy / height, confirmClose: confirmClose });
|
|
97
|
+
if (my > height * 0.5 || (vy > 0.5 && dy > 0)) requestClose({ velocity: vy / height });
|
|
58
98
|
else void openModal({ canceled });
|
|
59
99
|
} else void api.start({ translate: oy / height, immediate: true });
|
|
60
100
|
},
|
|
61
101
|
{ from: () => [0, translate.get()], filterTaps: true, bounds: { top: 0 }, rubberband: true },
|
|
62
102
|
);
|
|
103
|
+
|
|
63
104
|
const opacity = translate.to((t) => {
|
|
64
105
|
return interpolate(OPACITY.END, OPACITY.START, t);
|
|
65
106
|
});
|
|
@@ -68,33 +109,104 @@ export const Modal = ({ className, bodyClassName, confirmClose, children, onCanc
|
|
|
68
109
|
});
|
|
69
110
|
|
|
70
111
|
useEffect(() => {
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
112
|
+
if (typeof document === "undefined") return;
|
|
113
|
+
setPortalElement(document.body);
|
|
114
|
+
}, []);
|
|
115
|
+
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (open) {
|
|
118
|
+
void openModal();
|
|
119
|
+
}
|
|
120
|
+
}, [open, openModal]);
|
|
121
|
+
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (!open && isMounted) {
|
|
124
|
+
void closeModal({ notifyCancel: false });
|
|
125
|
+
}
|
|
126
|
+
}, [closeModal, isMounted, open]);
|
|
127
|
+
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
if (!isMounted || typeof document === "undefined") return;
|
|
130
|
+
|
|
131
|
+
bodyScrollLockCount += 1;
|
|
132
|
+
if (bodyScrollLockCount === 1) {
|
|
133
|
+
previousBodyOverflow = document.body.style.overflow;
|
|
134
|
+
document.body.style.overflow = "hidden";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return () => {
|
|
138
|
+
bodyScrollLockCount -= 1;
|
|
139
|
+
if (bodyScrollLockCount === 0) {
|
|
140
|
+
document.body.style.overflow = previousBodyOverflow;
|
|
141
|
+
previousBodyOverflow = "";
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}, [isMounted]);
|
|
145
|
+
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
if (!isMounted || !portalElement || typeof document === "undefined") return;
|
|
148
|
+
|
|
149
|
+
focusedElementRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
150
|
+
queueMicrotask(() => {
|
|
151
|
+
ref.current?.focus();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return () => {
|
|
155
|
+
if (focusedElementRef.current && document.contains(focusedElementRef.current)) {
|
|
156
|
+
focusedElementRef.current.focus();
|
|
157
|
+
}
|
|
158
|
+
focusedElementRef.current = null;
|
|
159
|
+
};
|
|
160
|
+
}, [isMounted, portalElement]);
|
|
161
|
+
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
if (!isMounted) return;
|
|
164
|
+
|
|
165
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
166
|
+
if (event.key !== "Escape") return;
|
|
167
|
+
event.preventDefault();
|
|
168
|
+
requestClose();
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
window.addEventListener("keydown", onKeyDown);
|
|
172
|
+
return () => {
|
|
173
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
174
|
+
};
|
|
175
|
+
}, [isMounted, requestClose]);
|
|
176
|
+
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
return () => {
|
|
179
|
+
if (closeTimerRef.current) clearTimeout(closeTimerRef.current);
|
|
180
|
+
};
|
|
181
|
+
}, []);
|
|
182
|
+
|
|
183
|
+
if (!isMounted || !portalElement) return null;
|
|
184
|
+
|
|
185
|
+
return createPortal(
|
|
186
|
+
<>
|
|
187
|
+
<div
|
|
188
|
+
className={clsx("fixed inset-0 z-10", showBackground && "animate-fadeIn bg-black/50 backdrop-blur-md")}
|
|
189
|
+
onClick={(event) => {
|
|
190
|
+
if (event.target !== event.currentTarget) return;
|
|
191
|
+
requestClose();
|
|
82
192
|
}}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
<div className={"fixed inset-0 z-10 bg-base-content/50 backdrop-blur-md data-[state=open]:animate-fadeIn"} />
|
|
86
|
-
) : null}
|
|
87
|
-
</Dialog.Overlay>
|
|
88
|
-
<Dialog.Content
|
|
89
|
-
className="fixed top-1/2 left-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center justify-center"
|
|
90
|
-
asChild
|
|
91
|
-
forceMount
|
|
92
|
-
>
|
|
193
|
+
/>
|
|
194
|
+
<div className="fixed top-1/2 left-1/2 z-10 flex -translate-x-1/2 -translate-y-1/2 items-center justify-center">
|
|
93
195
|
<div className="z-10">
|
|
94
|
-
<animated.div
|
|
196
|
+
<animated.div
|
|
197
|
+
ref={ref}
|
|
198
|
+
style={{ translateY, opacity }}
|
|
199
|
+
role="dialog"
|
|
200
|
+
aria-modal="true"
|
|
201
|
+
aria-labelledby={title ? titleId : undefined}
|
|
202
|
+
aria-describedby={contentId}
|
|
203
|
+
tabIndex={-1}
|
|
204
|
+
>
|
|
95
205
|
<button
|
|
206
|
+
type="button"
|
|
207
|
+
aria-label="Close"
|
|
96
208
|
className="btn btn-circle btn-sm absolute top-[-16px] right-0 z-20 md:top-[-40px]"
|
|
97
|
-
onClick={() =>
|
|
209
|
+
onClick={() => requestClose()}
|
|
98
210
|
>
|
|
99
211
|
<BiX className="text-3xl" />
|
|
100
212
|
</button>
|
|
@@ -104,34 +216,33 @@ export const Modal = ({ className, bodyClassName, confirmClose, children, onCanc
|
|
|
104
216
|
className,
|
|
105
217
|
)}
|
|
106
218
|
>
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
</div>
|
|
118
|
-
</animated.div>
|
|
119
|
-
</Dialog.Title>
|
|
120
|
-
<Dialog.Description asChild>
|
|
121
|
-
<div
|
|
122
|
-
className={clsx(
|
|
123
|
-
"scrollbar-none relative m-2 flex size-full min-w-[90vw] overflow-x-hidden overflow-y-scroll border-base-content/30 border-t-[0.1px] p-4 sm:p-4 md:min-w-[384px] md:px-8 lg:min-w-[576px] xl:min-w-[768px]",
|
|
124
|
-
bodyClassName,
|
|
125
|
-
)}
|
|
126
|
-
>
|
|
127
|
-
{children}
|
|
219
|
+
<animated.div
|
|
220
|
+
{...bind()}
|
|
221
|
+
id={titleId}
|
|
222
|
+
className="relative z-10 flex w-full animate-fadeIn cursor-pointer touch-pan-y flex-col items-center justify-center px-4 pt-1"
|
|
223
|
+
>
|
|
224
|
+
<div className="flex w-full cursor-pointer items-center justify-center pt-1 opacity-50">
|
|
225
|
+
<div className="h-1 w-24 rounded-full bg-gray-500" />
|
|
226
|
+
</div>
|
|
227
|
+
<div className="flex w-full items-center justify-start">
|
|
228
|
+
<div className="w-full text-start font-bold text-lg">{title}</div>
|
|
128
229
|
</div>
|
|
129
|
-
</
|
|
230
|
+
</animated.div>
|
|
231
|
+
<div
|
|
232
|
+
id={contentId}
|
|
233
|
+
className={clsx(
|
|
234
|
+
"scrollbar-none relative m-2 flex size-full min-w-[90vw] overflow-x-hidden overflow-y-scroll border-base-content/30 border-t-[0.1px] p-4 sm:p-4 md:min-w-[384px] md:px-8 lg:min-w-[576px] xl:min-w-[768px]",
|
|
235
|
+
bodyClassName,
|
|
236
|
+
)}
|
|
237
|
+
>
|
|
238
|
+
{children}
|
|
239
|
+
</div>
|
|
130
240
|
{action ? <div className="w-full">{action}</div> : null}
|
|
131
241
|
</div>
|
|
132
242
|
</animated.div>
|
|
133
243
|
</div>
|
|
134
|
-
</
|
|
135
|
-
|
|
244
|
+
</div>
|
|
245
|
+
</>,
|
|
246
|
+
portalElement,
|
|
136
247
|
);
|
|
137
248
|
};
|
package/ui/Dialog/Provider.tsx
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import * as Dialog from "@radix-ui/react-dialog";
|
|
3
2
|
import { clsx } from "akanjs/client";
|
|
4
3
|
import { type ReactNode, useEffect, useState } from "react";
|
|
5
4
|
|
|
@@ -23,11 +22,9 @@ export const Provider = ({ className, defaultOpen = false, open = defaultOpen, c
|
|
|
23
22
|
}, [open]);
|
|
24
23
|
return (
|
|
25
24
|
<DialogContext.Provider value={{ open: openState, setOpen: setOpenState, title, setTitle, action, setAction }}>
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
</div>
|
|
30
|
-
</Dialog.Root>
|
|
25
|
+
<div data-open={openState} className={clsx("group/dialog", className)}>
|
|
26
|
+
{children}
|
|
27
|
+
</div>
|
|
31
28
|
</DialogContext.Provider>
|
|
32
29
|
);
|
|
33
30
|
};
|
package/ui/Modal.tsx
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import * as RadixDialog from "@radix-ui/react-dialog";
|
|
3
2
|
import type { ReactNode } from "react";
|
|
4
|
-
import { BiX } from "react-icons/bi";
|
|
5
3
|
|
|
6
4
|
import { Dialog } from "./Dialog";
|
|
7
5
|
|
|
@@ -43,45 +41,3 @@ export const Modal = ({
|
|
|
43
41
|
</Dialog>
|
|
44
42
|
);
|
|
45
43
|
};
|
|
46
|
-
|
|
47
|
-
interface WindowProps {
|
|
48
|
-
open: boolean;
|
|
49
|
-
onCancel: () => void;
|
|
50
|
-
title: ReactNode;
|
|
51
|
-
children: ReactNode;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export const Window = ({ open, onCancel, title, children }: WindowProps) => {
|
|
55
|
-
if (!open) return null;
|
|
56
|
-
|
|
57
|
-
return (
|
|
58
|
-
<RadixDialog.Root open={open}>
|
|
59
|
-
<RadixDialog.Portal>
|
|
60
|
-
<RadixDialog.Overlay className="fixed inset-0 bg-black/40" />
|
|
61
|
-
<RadixDialog.Content
|
|
62
|
-
className="fixed top-1/2 left-1/2 z-[2] w-[90%] min-w-auto -translate-x-1/2 -translate-y-1/2 animate-fadeIn rounded-[10px] border-[3px] border-black text-black backdrop-blur-lg md:w-fit"
|
|
63
|
-
style={{
|
|
64
|
-
background: `rgba(255, 255, 255, 0.3)`,
|
|
65
|
-
width: "406px",
|
|
66
|
-
}}
|
|
67
|
-
>
|
|
68
|
-
<RadixDialog.Title className="height-[36px] relative overflow-hidden rounded-t-[6px] border-black border-b-2 bg-white/60 text-center">
|
|
69
|
-
<div className="m-0 text-[22px]">{title}</div>
|
|
70
|
-
<RadixDialog.Close
|
|
71
|
-
onClick={() => {
|
|
72
|
-
onCancel();
|
|
73
|
-
}}
|
|
74
|
-
className="absolute top-0 right-0 flex h-[34px] w-[40px] cursor-pointer items-center justify-center border-black border-l-2"
|
|
75
|
-
>
|
|
76
|
-
<BiX className="text-[32px]" />
|
|
77
|
-
</RadixDialog.Close>
|
|
78
|
-
</RadixDialog.Title>
|
|
79
|
-
<RadixDialog.Description className="overflow-y-hidden rounded-b-[10px] p-2">
|
|
80
|
-
{children}
|
|
81
|
-
</RadixDialog.Description>
|
|
82
|
-
</RadixDialog.Content>
|
|
83
|
-
</RadixDialog.Portal>
|
|
84
|
-
</RadixDialog.Root>
|
|
85
|
-
);
|
|
86
|
-
};
|
|
87
|
-
Modal.Window = Window;
|