akanjs 2.2.7 → 2.2.9

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +11 -2
  2. package/client/clientRuntime.ts +3 -0
  3. package/client/csrTypes.ts +1 -0
  4. package/client/makePageProto.tsx +5 -2
  5. package/client/translator.ts +7 -4
  6. package/common/fileUpload.ts +1 -1
  7. package/common/index.ts +5 -1
  8. package/constant/getDefault.ts +1 -1
  9. package/dictionary/base.dictionary.ts +0 -1
  10. package/fetch/client/fetchClient.ts +21 -24
  11. package/fetch/client/wsClient.ts +8 -0
  12. package/fetch/serializer/fetch.serializer.ts +1 -0
  13. package/package.json +1 -5
  14. package/server/hmr/devHmrController.ts +1 -0
  15. package/server/routeTreeBuilder.ts +1 -0
  16. package/server/ssrFromRscRenderer.tsx +34 -12
  17. package/server/ssrTypes.ts +5 -6
  18. package/service/base.service.ts +0 -4
  19. package/service/injectInfo.ts +49 -12
  20. package/service/predefinedAdaptor/cache.adaptor.ts +13 -0
  21. package/service/predefinedAdaptor/database.adaptor.ts +74 -16
  22. package/service/predefinedAdaptor/solidCache.adaptor.ts +23 -0
  23. package/signal/base.signal.ts +0 -5
  24. package/signal/serializer/fetch.serializer.ts +1 -0
  25. package/signal/types.ts +3 -0
  26. package/store/action.ts +15 -3
  27. package/store/storeInstance.ts +50 -3
  28. package/types/client/csrTypes.d.ts +1 -0
  29. package/types/client/translator.d.ts +1 -0
  30. package/types/common/fileUpload.d.ts +1 -1
  31. package/types/common/index.d.ts +1 -1
  32. package/types/server/ssrTypes.d.ts +5 -6
  33. package/types/service/base.service.d.ts +0 -1
  34. package/types/service/injectInfo.d.ts +8 -2
  35. package/types/service/predefinedAdaptor/cache.adaptor.d.ts +6 -0
  36. package/types/service/predefinedAdaptor/database.adaptor.d.ts +3 -1
  37. package/types/service/predefinedAdaptor/solidCache.adaptor.d.ts +3 -0
  38. package/types/signal/base.signal.d.ts +0 -3
  39. package/types/signal/types.d.ts +3 -0
  40. package/types/ui/Dialog/Modal.d.ts +1 -1
  41. package/types/ui/Dialog/index.d.ts +1 -1
  42. package/types/ui/Modal.d.ts +1 -12
  43. package/types/ui/System/CSR.d.ts +2 -2
  44. package/types/ui/System/Client.d.ts +5 -4
  45. package/types/ui/System/Common.d.ts +2 -0
  46. package/types/ui/System/SSR.d.ts +2 -2
  47. package/ui/Dialog/Modal.tsx +181 -70
  48. package/ui/Dialog/Provider.tsx +3 -6
  49. package/ui/Modal.tsx +0 -44
  50. package/ui/System/CSR.tsx +9 -1
  51. package/ui/System/Client.tsx +27 -62
  52. package/ui/System/Common.tsx +2 -0
  53. package/ui/System/SSR.tsx +9 -1
  54. package/webkit/bootCsr.tsx +1 -0
@@ -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-black/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;
package/ui/System/CSR.tsx CHANGED
@@ -45,6 +45,7 @@ const CSRProvider = ({
45
45
  fonts,
46
46
  layoutStyle = "web",
47
47
  reconnect = getEnv().operationMode === "local",
48
+ wsConnect = true,
48
49
  of,
49
50
  }: CSRProviderProps) => {
50
51
  return (
@@ -72,7 +73,14 @@ const CSRProvider = ({
72
73
  </Client.Wrapper>
73
74
  <Client.Inner />
74
75
  <CSRInner />
75
- <Client.Bridge lang={lang} env={env} theme={theme} prefix={prefix} gaTrackingId={gaTrackingId} />
76
+ <Client.Bridge
77
+ lang={lang}
78
+ env={env}
79
+ theme={theme}
80
+ prefix={prefix}
81
+ gaTrackingId={gaTrackingId}
82
+ wsConnect={wsConnect}
83
+ />
76
84
  <CSRBridge lang={lang} prefix={prefix} />
77
85
  </>
78
86
  )}
@@ -5,6 +5,7 @@ import {
5
5
  clsx,
6
6
  Device,
7
7
  defaultPageState,
8
+ fetch,
8
9
  getPathInfo,
9
10
  initAuth,
10
11
  type Location,
@@ -55,8 +56,10 @@ export const ClientWrapper = ({
55
56
  reconnect = true,
56
57
  }: ClientWrapperProps) => {
57
58
 
58
- if (dictionary) Translator.seed(lang, dictionary);
59
- if (getEnv().renderMode === "ssr") {
59
+ if (dictionary) {
60
+ Translator.seed(lang, dictionary);
61
+
62
+ if (typeof window !== "undefined") Translator.setActiveLocale(lang);
60
63
  }
61
64
  useLayoutEffect(() => {
62
65
  Logger.rawLog(logo);
@@ -71,8 +74,7 @@ export const ClientWrapper = ({
71
74
  };
72
75
  Client.Wrapper = ClientWrapper;
73
76
 
74
- interface ClientPathWrapperProps
75
- extends Omit<HTMLAttributes<HTMLDivElement>, "style"> {
77
+ interface ClientPathWrapperProps extends Omit<HTMLAttributes<HTMLDivElement>, "style"> {
76
78
  bind?: () => HTMLAttributes<HTMLDivElement>;
77
79
  wrapperRef?: RefObject<HTMLDivElement | null> | null;
78
80
  pageType?: "current" | "prev" | "cached";
@@ -93,18 +95,12 @@ export const ClientPathWrapper = ({
93
95
  layoutStyle = "web",
94
96
  ...props
95
97
  }: ClientPathWrapperProps) => {
96
- const href =
97
- location?.href ??
98
- (typeof window !== "undefined" ? window.location.href : "");
99
- const hash =
100
- location?.hash ??
101
- (typeof window !== "undefined" ? window.location.hash : "");
98
+ const href = location?.href ?? (typeof window !== "undefined" ? window.location.href : "");
99
+ const hash = location?.hash ?? (typeof window !== "undefined" ? window.location.hash : "");
102
100
  const pathname = location?.pathname ?? "/";
103
101
  const params = location?.params ?? {};
104
102
  const searchParams = location?.searchParams ?? {};
105
- const search =
106
- location?.search ??
107
- (typeof window !== "undefined" ? window.location.search : "");
103
+ const search = location?.search ?? (typeof window !== "undefined" ? window.location.search : "");
108
104
  const lang = params.lang;
109
105
  const firstPath = pathname.split("/")[2];
110
106
  const pathRoute: PathRoute = location?.pathRoute ?? {
@@ -137,9 +133,7 @@ export const ClientPathWrapper = ({
137
133
  }}
138
134
  >
139
135
  <animated.div
140
- {...(bind && pathRoute.pageState.gesture && gestureEnabled
141
- ? bind()
142
- : {})}
136
+ {...(bind && pathRoute.pageState.gesture && gestureEnabled ? bind() : {})}
143
137
  className={clsx("group/path", className)}
144
138
  ref={wrapperRef}
145
139
  {...props}
@@ -159,15 +153,10 @@ interface ClientBridgeProps {
159
153
  theme?: AkanTheme;
160
154
  prefix?: string;
161
155
  gaTrackingId?: string;
156
+ wsConnect?: boolean;
162
157
  }
163
158
 
164
- export const ClientBridge = ({
165
- env,
166
- lang,
167
- theme,
168
- prefix,
169
- gaTrackingId,
170
- }: ClientBridgeProps) => {
159
+ export const ClientBridge = ({ env, lang, theme, prefix, gaTrackingId, wsConnect = true }: ClientBridgeProps) => {
171
160
  const uiOperation = st.use.uiOperation();
172
161
  const pathname = st.use.pathname();
173
162
  const params = st.use.params();
@@ -187,6 +176,11 @@ export const ClientBridge = ({
187
176
  }, 2000);
188
177
  }, []);
189
178
 
179
+ useEffect(() => {
180
+ if (!wsConnect) return;
181
+ (fetch.instance as { connect: () => void }).connect();
182
+ }, [wsConnect]);
183
+
190
184
  useEffect(() => {
191
185
  if (getThemeCookie() !== undefined) return;
192
186
  applyThemePolicy(theme ?? "system");
@@ -234,26 +228,18 @@ function applyThemePolicy(theme: AkanTheme): void {
234
228
  }
235
229
  if (theme === "system") {
236
230
  const dark = window.matchMedia("(prefers-color-scheme: dark)").matches;
237
- document.documentElement.setAttribute(
238
- "data-theme",
239
- dark ? "dark" : "light",
240
- );
231
+ document.documentElement.setAttribute("data-theme", dark ? "dark" : "light");
241
232
  return;
242
233
  }
243
234
  document.documentElement.setAttribute("data-theme", theme);
244
235
  }
245
236
 
246
- function buildSearchParams(
247
- entries: Iterable<[string, string]>,
248
- ): Record<string, string | string[]> {
237
+ function buildSearchParams(entries: Iterable<[string, string]>): Record<string, string | string[]> {
249
238
  const params: Record<string, string | string[]> = {};
250
239
  for (const [key, value] of entries) {
251
240
  const current = params[key];
252
241
  if (current === undefined) params[key] = value;
253
- else
254
- params[key] = Array.isArray(current)
255
- ? [...current, value]
256
- : [current, value];
242
+ else params[key] = Array.isArray(current) ? [...current, value] : [current, value];
257
243
  }
258
244
  return params;
259
245
  }
@@ -273,10 +259,7 @@ interface ClientSsrBridgeProps {
273
259
  lang: string;
274
260
  prefix?: string;
275
261
  }
276
- export const ClientSsrBridge = ({
277
- lang,
278
- prefix = "",
279
- }: ClientSsrBridgeProps) => {
262
+ export const ClientSsrBridge = ({ lang, prefix = "" }: ClientSsrBridgeProps) => {
280
263
  useEffect(() => {
281
264
  const visiblePrefix = getEnv().operationMode === "local" ? prefix : "";
282
265
  const navigateRscWithFallback = (
@@ -290,19 +273,13 @@ export const ClientSsrBridge = ({
290
273
  return;
291
274
  }
292
275
  void navigation.catch((error) => {
293
- Logger.warn(
294
- `RSC navigation failed, falling back to document navigation: ${String(error)}`,
295
- );
276
+ Logger.warn(`RSC navigation failed, falling back to document navigation: ${String(error)}`);
296
277
  fallback();
297
278
  });
298
279
  };
299
280
  const syncHref = (href: string) => {
300
281
  const url = new URL(href, window.location.origin);
301
- const { path } = getPathInfo(
302
- `${url.pathname}${url.search}${url.hash}`,
303
- lang,
304
- visiblePrefix,
305
- );
282
+ const { path } = getPathInfo(`${url.pathname}${url.search}${url.hash}`, lang, visiblePrefix);
306
283
  const searchParams = buildSearchParams(url.searchParams.entries());
307
284
  st.set({ pathname: url.pathname, path, searchParams });
308
285
  };
@@ -314,17 +291,11 @@ export const ClientSsrBridge = ({
314
291
  router: {
315
292
  push: (href, routeOptions) => {
316
293
  syncHref(href);
317
- navigateRscWithFallback(href, routeOptions, () =>
318
- window.location.assign(href),
319
- );
294
+ navigateRscWithFallback(href, routeOptions, () => window.location.assign(href));
320
295
  },
321
296
  replace: (href, routeOptions) => {
322
297
  syncHref(href);
323
- navigateRscWithFallback(
324
- href,
325
- { ...routeOptions, replace: true },
326
- () => window.location.replace(href),
327
- );
298
+ navigateRscWithFallback(href, { ...routeOptions, replace: true }, () => window.location.replace(href));
328
299
  },
329
300
  back: () => {
330
301
  window.history.back();
@@ -346,14 +317,8 @@ export const ClientSsrBridge = ({
346
317
  const visiblePrefix = getEnv().operationMode === "local" ? prefix : "";
347
318
  const sync = () => {
348
319
  const { pathname, search, hash } = window.location;
349
- const { path } = getPathInfo(
350
- `${pathname}${search}${hash}`,
351
- lang,
352
- visiblePrefix,
353
- );
354
- const searchParams = buildSearchParams(
355
- new URLSearchParams(search).entries(),
356
- );
320
+ const { path } = getPathInfo(`${pathname}${search}${hash}`, lang, visiblePrefix);
321
+ const searchParams = buildSearchParams(new URLSearchParams(search).entries());
357
322
  st.set({ pathname: window.location.pathname, path, searchParams });
358
323
  };
359
324
  sync();
@@ -30,6 +30,8 @@ export interface ProviderProps {
30
30
  layoutStyle?: "mobile" | "web";
31
31
  /** Enable reconnect helper. Defaults to local operation mode in CSR. */
32
32
  reconnect?: boolean;
33
+ /** Connect the client WebSocket runtime after the browser loads. */
34
+ wsConnect?: boolean;
33
35
  /** Active-locale dictionary injected by the server (SSR only) to seed the client Translator. */
34
36
  dictionary?: Record<string, Record<string, unknown>>;
35
37
  /**
package/ui/System/SSR.tsx CHANGED
@@ -29,6 +29,7 @@ const SSRProvider = ({
29
29
  fonts,
30
30
  layoutStyle = "web",
31
31
  reconnect = getEnv().operationMode === "local",
32
+ wsConnect = true,
32
33
  dictionary,
33
34
  allDictionary,
34
35
  of,
@@ -65,7 +66,14 @@ const SSRProvider = ({
65
66
  <ClientInner />
66
67
  </Suspense>
67
68
  <Suspense key="client-bridge" fallback={null}>
68
- <ClientBridge key="bridge" env={env} theme={theme} prefix={prefix} gaTrackingId={gaTrackingId} />
69
+ <ClientBridge
70
+ key="bridge"
71
+ env={env}
72
+ theme={theme}
73
+ prefix={prefix}
74
+ gaTrackingId={gaTrackingId}
75
+ wsConnect={wsConnect}
76
+ />
69
77
  <ClientSsrBridge key="ssr-bridge" lang={lang} prefix={prefix} />
70
78
  </Suspense>
71
79
  </ClientWrapper>
@@ -293,6 +293,7 @@ function validateRouteModuleExports(key: string, mod: RouteModule) {
293
293
  "manifest",
294
294
  "theme",
295
295
  "reconnect",
296
+ "wsConnect",
296
297
  "layoutStyle",
297
298
  "gaTrackingId",
298
299
  "Loading",