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.
@@ -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.2",
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
  },
@@ -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: async (key: string) => {
260
- const getter = injectInfo.get as unknown as (value: unknown) => unknown;
261
- const value = await cacheAdaptor.hget(`akan:memory:${injectInfo.parentRefName}`, propKey, key);
262
- return value === null ? undefined : getter(value);
270
+ get,
271
+ set,
272
+ delete: async (key: string) => {
273
+ await cacheAdaptor.hdelete(topic, propKey, key);
263
274
  },
264
- set: async (key: string, value: unknown) => {
265
- const setter = injectInfo.set as unknown as (value: unknown) => string | number | Buffer;
266
- const setValue = setter(value);
267
- await cacheAdaptor.hset(`akan:memory:${injectInfo.parentRefName}`, propKey, key, setValue);
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
- delete: async (key: string) => {
270
- await cacheAdaptor.hdelete(`akan:memory:${injectInfo.parentRefName}`, propKey, key);
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/jsx-runtime").JSX.Element;
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/jsx-runtime").JSX.Element;
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;
@@ -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;
@@ -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
- const openModal = async ({ canceled }: { canceled?: boolean } = {}) => {
34
- setTimeout(() => {
35
- setShowBackground(true);
36
- }, 100);
37
- await Promise.all(api.start({ translate: 0, immediate: false, config: canceled ? config.wobbly : config.stiff }));
38
- };
39
- const closeModal = async ({ velocity = 0, confirmClose }: { velocity?: number; confirmClose?: boolean }) => {
40
- if (confirmClose && !window.confirm(l("base.confirmClose"))) {
41
- return;
42
- }
43
- setTimeout(() => {
44
- setShowBackground(false);
45
- }, 100);
46
- await Promise.all(api.start({ translate: 1, immediate: false, config: { ...config.stiff, velocity } }));
47
- setOpen(false);
48
- onCancel?.();
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 (openRef.current === open) return;
72
- openRef.current = open;
73
- if (open) void openModal();
74
- else void closeModal({});
75
- }, [open]);
76
-
77
- return (
78
- <Dialog.Portal>
79
- <Dialog.Overlay
80
- onClick={() => {
81
- void closeModal({ confirmClose });
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
- {showBackground ? (
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 ref={ref} style={{ translateY, opacity }}>
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={() => void closeModal({ confirmClose })}
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
- <Dialog.Title asChild>
108
- <animated.div
109
- {...bind()}
110
- className="relative z-10 flex w-full animate-fadeIn cursor-pointer touch-pan-y flex-col items-center justify-center px-4 pt-1"
111
- >
112
- <div className="flex w-full cursor-pointer items-center justify-center pt-1 opacity-50">
113
- <div className="h-1 w-24 rounded-full bg-gray-500" />
114
- </div>
115
- <div className="flex w-full items-center justify-start">
116
- <div className="w-full text-start font-bold text-lg">{title}</div>
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
- </Dialog.Description>
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
- </Dialog.Content>
135
- </Dialog.Portal>
244
+ </div>
245
+ </>,
246
+ portalElement,
136
247
  );
137
248
  };
@@ -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
- <Dialog.Root open={openState}>
27
- <div data-open={openState} className={clsx("group/dialog", className)}>
28
- {children}
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;