akanjs 2.2.4-rc.2 → 2.2.4-rc.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/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/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/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
|
@@ -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.3",
|
|
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
|
}
|
|
@@ -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
|
}
|
|
@@ -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-base-content/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;
|