@alepha/react 0.11.11 → 0.11.12

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 (128) hide show
  1. package/README.md +1 -183
  2. package/dist/auth/index.browser.js +1460 -0
  3. package/dist/auth/index.browser.js.map +1 -0
  4. package/dist/auth/index.cjs +3647 -0
  5. package/dist/auth/index.cjs.map +1 -0
  6. package/dist/auth/index.d.cts +564 -0
  7. package/dist/auth/index.d.cts.map +1 -0
  8. package/dist/auth/index.d.ts +564 -0
  9. package/dist/auth/index.d.ts.map +1 -0
  10. package/dist/auth/index.js +3615 -0
  11. package/dist/auth/index.js.map +1 -0
  12. package/dist/{index.browser.js → core/index.browser.js} +36 -35
  13. package/dist/core/index.browser.js.map +1 -0
  14. package/dist/{index.cjs → core/index.cjs} +141 -140
  15. package/dist/core/index.cjs.map +1 -0
  16. package/dist/{index.d.cts → core/index.d.cts} +68 -68
  17. package/dist/core/index.d.cts.map +1 -0
  18. package/dist/{index.d.ts → core/index.d.ts} +68 -68
  19. package/dist/core/index.d.ts.map +1 -0
  20. package/dist/{index.js → core/index.js} +39 -38
  21. package/dist/core/index.js.map +1 -0
  22. package/dist/form/index.cjs +2054 -0
  23. package/dist/form/index.cjs.map +1 -0
  24. package/dist/form/index.d.cts +211 -0
  25. package/dist/form/index.d.cts.map +1 -0
  26. package/dist/form/index.d.ts +211 -0
  27. package/dist/form/index.d.ts.map +1 -0
  28. package/dist/form/index.js +2026 -0
  29. package/dist/form/index.js.map +1 -0
  30. package/dist/head/index.browser.js +1503 -0
  31. package/dist/head/index.browser.js.map +1 -0
  32. package/dist/head/index.cjs +1908 -0
  33. package/dist/head/index.cjs.map +1 -0
  34. package/dist/head/index.d.cts +595 -0
  35. package/dist/head/index.d.cts.map +1 -0
  36. package/dist/head/index.d.ts +601 -0
  37. package/dist/head/index.d.ts.map +1 -0
  38. package/dist/head/index.js +1880 -0
  39. package/dist/head/index.js.map +1 -0
  40. package/dist/i18n/index.cjs +1886 -0
  41. package/dist/i18n/index.cjs.map +1 -0
  42. package/dist/i18n/index.d.cts +168 -0
  43. package/dist/i18n/index.d.cts.map +1 -0
  44. package/dist/i18n/index.d.ts +168 -0
  45. package/dist/i18n/index.d.ts.map +1 -0
  46. package/dist/i18n/index.js +1857 -0
  47. package/dist/i18n/index.js.map +1 -0
  48. package/dist/websocket/index.cjs +1774 -0
  49. package/dist/websocket/index.cjs.map +1 -0
  50. package/dist/websocket/index.d.cts +118 -0
  51. package/dist/websocket/index.d.cts.map +1 -0
  52. package/dist/websocket/index.d.ts +118 -0
  53. package/dist/websocket/index.d.ts.map +1 -0
  54. package/dist/websocket/index.js +1750 -0
  55. package/dist/websocket/index.js.map +1 -0
  56. package/package.json +89 -67
  57. package/src/auth/descriptors/$auth.ts +436 -0
  58. package/src/auth/descriptors/$authApple.ts +8 -0
  59. package/src/auth/descriptors/$authGithub.ts +81 -0
  60. package/src/auth/descriptors/$authGoogle.ts +38 -0
  61. package/src/auth/errors/SessionExpiredError.ts +6 -0
  62. package/src/auth/hooks/useAuth.ts +31 -0
  63. package/src/auth/index.browser.ts +16 -0
  64. package/src/auth/index.shared.ts +3 -0
  65. package/src/auth/index.ts +47 -0
  66. package/src/auth/providers/ReactAuthProvider.ts +629 -0
  67. package/src/auth/schemas/tokenResponseSchema.ts +11 -0
  68. package/src/auth/schemas/tokensSchema.ts +21 -0
  69. package/src/auth/schemas/userinfoResponseSchema.ts +10 -0
  70. package/src/auth/services/ReactAuth.ts +124 -0
  71. package/src/{components → core/components}/ErrorViewer.tsx +3 -2
  72. package/src/{components → core/components}/NestedView.tsx +1 -1
  73. package/src/{contexts → core/contexts}/AlephaContext.ts +1 -1
  74. package/src/{descriptors → core/descriptors}/$page.ts +4 -4
  75. package/src/{hooks → core/hooks}/useAction.ts +1 -1
  76. package/src/{hooks → core/hooks}/useAlepha.ts +1 -1
  77. package/src/{hooks → core/hooks}/useClient.ts +1 -1
  78. package/src/{hooks → core/hooks}/useEvents.ts +1 -1
  79. package/src/{hooks → core/hooks}/useInject.ts +1 -1
  80. package/src/{hooks → core/hooks}/useQueryParams.ts +1 -1
  81. package/src/{hooks → core/hooks}/useRouterState.ts +1 -1
  82. package/src/{hooks → core/hooks}/useSchema.ts +3 -3
  83. package/src/{hooks → core/hooks}/useStore.ts +2 -2
  84. package/src/{index.browser.ts → core/index.browser.ts} +4 -4
  85. package/src/{index.ts → core/index.ts} +6 -6
  86. package/src/{providers → core/providers}/ReactBrowserProvider.ts +6 -6
  87. package/src/{providers → core/providers}/ReactBrowserRendererProvider.ts +2 -2
  88. package/src/{providers → core/providers}/ReactBrowserRouterProvider.ts +3 -3
  89. package/src/{providers → core/providers}/ReactPageProvider.ts +3 -3
  90. package/src/{providers → core/providers}/ReactServerProvider.ts +7 -7
  91. package/src/{services → core/services}/ReactPageServerService.ts +2 -2
  92. package/src/{services → core/services}/ReactPageService.ts +1 -1
  93. package/src/{services → core/services}/ReactRouter.ts +1 -1
  94. package/src/form/components/FormState.tsx +17 -0
  95. package/src/form/hooks/useForm.ts +47 -0
  96. package/src/form/hooks/useFormState.ts +130 -0
  97. package/src/form/index.ts +38 -0
  98. package/src/form/services/FormModel.ts +548 -0
  99. package/src/head/descriptors/$head.ts +25 -0
  100. package/src/head/hooks/useHead.ts +62 -0
  101. package/src/head/index.browser.ts +25 -0
  102. package/src/head/index.ts +47 -0
  103. package/src/head/interfaces/Head.ts +46 -0
  104. package/src/head/providers/BrowserHeadProvider.ts +105 -0
  105. package/src/head/providers/HeadProvider.ts +73 -0
  106. package/src/head/providers/ServerHeadProvider.ts +109 -0
  107. package/src/i18n/README.md +76 -0
  108. package/src/i18n/components/Localize.tsx +35 -0
  109. package/src/i18n/descriptors/$dictionary.ts +65 -0
  110. package/src/i18n/hooks/useI18n.ts +18 -0
  111. package/src/i18n/index.ts +34 -0
  112. package/src/i18n/providers/I18nProvider.ts +277 -0
  113. package/src/websocket/hooks/useRoom.tsx +223 -0
  114. package/src/websocket/index.ts +7 -0
  115. package/dist/index.browser.js.map +0 -1
  116. package/dist/index.cjs.map +0 -1
  117. package/dist/index.d.cts.map +0 -1
  118. package/dist/index.d.ts.map +0 -1
  119. package/dist/index.js.map +0 -1
  120. /package/src/{components → core/components}/ClientOnly.tsx +0 -0
  121. /package/src/{components → core/components}/ErrorBoundary.tsx +0 -0
  122. /package/src/{components → core/components}/Link.tsx +0 -0
  123. /package/src/{components → core/components}/NotFound.tsx +0 -0
  124. /package/src/{contexts → core/contexts}/RouterLayerContext.ts +0 -0
  125. /package/src/{errors → core/errors}/Redirection.ts +0 -0
  126. /package/src/{hooks → core/hooks}/useActive.ts +0 -0
  127. /package/src/{hooks → core/hooks}/useRouter.ts +0 -0
  128. /package/src/{index.shared.ts → core/index.shared.ts} +0 -0
@@ -0,0 +1,277 @@
1
+ import { $hook, $inject, Alepha, TypeBoxError, TypeProvider, t } from "alepha";
2
+ import { type DateTime, DateTimeProvider } from "alepha/datetime";
3
+ import { $logger } from "alepha/logger";
4
+ import { $cookie } from "alepha/server/cookies";
5
+ import type { ServiceDictionary } from "../hooks/useI18n.ts";
6
+
7
+ export class I18nProvider<
8
+ S extends object,
9
+ K extends keyof ServiceDictionary<S>,
10
+ > {
11
+ protected logger = $logger();
12
+ protected alepha = $inject(Alepha);
13
+ protected dateTimeProvider = $inject(DateTimeProvider);
14
+
15
+ protected cookie = $cookie({
16
+ name: "lang",
17
+ schema: t.text(),
18
+ });
19
+
20
+ public readonly registry: Array<{
21
+ name: string;
22
+ lang: string;
23
+ loader: () => Promise<Record<string, string>>;
24
+ translations: Record<string, string>;
25
+ }> = [];
26
+
27
+ options = {
28
+ fallbackLang: "en",
29
+ };
30
+
31
+ public dateFormat: { format: (value: Date) => string } =
32
+ new Intl.DateTimeFormat(this.lang);
33
+
34
+ public numberFormat: { format: (value: number) => string } =
35
+ new Intl.NumberFormat(this.lang);
36
+
37
+ public get languages() {
38
+ const languages = new Set<string>();
39
+
40
+ for (const item of this.registry) {
41
+ languages.add(item.lang);
42
+ }
43
+ languages.add(this.options.fallbackLang);
44
+
45
+ return Array.from(languages);
46
+ }
47
+
48
+ constructor() {
49
+ this.refreshLocale();
50
+ }
51
+
52
+ protected readonly onRender = $hook({
53
+ on: "server:onRequest",
54
+ priority: "last",
55
+ handler: async ({ request }) => {
56
+ this.alepha.state.set("alepha.react.i18n.lang", this.cookie.get(request));
57
+ },
58
+ });
59
+
60
+ protected readonly onStart = $hook({
61
+ on: "start",
62
+ handler: async () => {
63
+ if (this.alepha.isBrowser()) {
64
+ // get cookie lang
65
+ const cookieLang = this.cookie.get();
66
+ if (cookieLang) {
67
+ this.alepha.state.set("alepha.react.i18n.lang", cookieLang);
68
+ }
69
+
70
+ for (const item of this.registry) {
71
+ if (
72
+ item.lang === this.lang ||
73
+ item.lang === this.options.fallbackLang
74
+ ) {
75
+ item.translations = await item.loader();
76
+ }
77
+ }
78
+ return;
79
+ }
80
+
81
+ for (const item of this.registry) {
82
+ item.translations = await item.loader();
83
+ }
84
+ },
85
+ });
86
+
87
+ protected refreshLocale() {
88
+ this.numberFormat = new Intl.NumberFormat(this.lang);
89
+ this.dateFormat = new Intl.DateTimeFormat(this.lang);
90
+ this.dateTimeProvider.setLocale(this.lang);
91
+ TypeProvider.setLocale(this.lang);
92
+ }
93
+
94
+ public setLang = async (lang: string) => {
95
+ if (this.alepha.isBrowser()) {
96
+ for (const item of this.registry) {
97
+ if (lang === item.lang) {
98
+ if (Object.keys(item.translations).length > 0) {
99
+ continue; // already loaded
100
+ }
101
+ item.translations = await item.loader();
102
+ }
103
+ }
104
+ this.cookie.set(lang);
105
+ }
106
+
107
+ this.alepha.state.set("alepha.react.i18n.lang", lang);
108
+ this.refreshLocale();
109
+ };
110
+
111
+ protected readonly mutate = $hook({
112
+ on: "state:mutate",
113
+ handler: async ({ key, value }) => {
114
+ if (key === "alepha.react.i18n.lang" && this.alepha.isBrowser()) {
115
+ let hasChanged = false;
116
+ for (const item of this.registry) {
117
+ if (value === item.lang) {
118
+ if (Object.keys(item.translations).length > 0) {
119
+ continue; // already loaded
120
+ }
121
+ item.translations = await item.loader();
122
+ hasChanged = true;
123
+ }
124
+ }
125
+
126
+ this.refreshLocale();
127
+
128
+ if (hasChanged) {
129
+ this.alepha.state.set("alepha.react.i18n.lang", value);
130
+ }
131
+ }
132
+ },
133
+ });
134
+
135
+ public get lang(): string {
136
+ return (
137
+ this.alepha.state.get("alepha.react.i18n.lang") ||
138
+ this.options.fallbackLang
139
+ );
140
+ }
141
+
142
+ public translate = (key: string, args: string[] = []) => {
143
+ for (const item of this.registry) {
144
+ if (item.lang === this.lang) {
145
+ if (item.translations[key]) {
146
+ return this.render(item.translations[key], args); // append lang for fallback
147
+ } else {
148
+ break;
149
+ }
150
+ }
151
+ }
152
+
153
+ for (const item of this.registry) {
154
+ if (item.lang === this.options.fallbackLang) {
155
+ if (item.translations[key]) {
156
+ return this.render(item.translations[key], args); // append lang for fallback
157
+ } else {
158
+ break;
159
+ }
160
+ }
161
+ }
162
+
163
+ return key; // fallback to the key itself if not found
164
+ };
165
+
166
+ public readonly l = (
167
+ value: I18nLocalizeType,
168
+ options: I18nLocalizeOptions = {},
169
+ ) => {
170
+ // Handle numbers
171
+ if (typeof value === "number") {
172
+ if (options.number) {
173
+ return new Intl.NumberFormat(this.lang, options.number).format(value);
174
+ }
175
+ return this.numberFormat.format(value);
176
+ }
177
+
178
+ // Handle dates
179
+ if (
180
+ value instanceof Date ||
181
+ this.dateTimeProvider.isDateTime(value) ||
182
+ (typeof value === "string" && options.date)
183
+ ) {
184
+ // convert to DateTime with locale applied
185
+ let dt = this.dateTimeProvider.of(value);
186
+
187
+ // apply timezone if specified
188
+ if (options.timezone) {
189
+ dt = dt.tz(options.timezone);
190
+ }
191
+
192
+ // format using dayjs format string
193
+ if (typeof options.date === "string") {
194
+ if (options.date === "fromNow") {
195
+ return dt.locale(this.lang).fromNow();
196
+ }
197
+ return dt.locale(this.lang).format(options.date);
198
+ }
199
+
200
+ // format using Intl.DateTimeFormatOptions
201
+ if (options.date) {
202
+ return new Intl.DateTimeFormat(
203
+ this.lang,
204
+ options.timezone
205
+ ? { ...options.date, timeZone: options.timezone }
206
+ : options.date,
207
+ ).format(dt.toDate());
208
+ }
209
+
210
+ // default formatting with timezone
211
+ if (options.timezone) {
212
+ return new Intl.DateTimeFormat(this.lang, {
213
+ timeZone: options.timezone,
214
+ }).format(dt.toDate());
215
+ }
216
+
217
+ // default formatting
218
+ return this.dateFormat.format(dt.toDate());
219
+ }
220
+
221
+ // handle TypeBox errors
222
+ if (value instanceof TypeBoxError) {
223
+ return TypeProvider.translateError(value, this.lang);
224
+ }
225
+
226
+ // return string values as-is
227
+ return value;
228
+ };
229
+
230
+ public readonly tr = (
231
+ key: keyof ServiceDictionary<S>[K],
232
+ options: {
233
+ args?: string[];
234
+ default?: string;
235
+ } = {},
236
+ ) => {
237
+ const translation = this.translate(key as string, options.args || []);
238
+ if (translation === key && options.default) {
239
+ return options.default;
240
+ }
241
+ return translation;
242
+ };
243
+
244
+ protected render(item: string, args: string[]): string {
245
+ let result = item;
246
+ for (let i = 0; i < args.length; i++) {
247
+ result = result.replace(`$${i + 1}`, args[i]);
248
+ }
249
+ return result;
250
+ }
251
+ }
252
+
253
+ export type I18nLocalizeType = string | number | Date | DateTime | TypeBoxError;
254
+
255
+ export interface I18nLocalizeOptions {
256
+ /**
257
+ * Options for number formatting (when value is a number)
258
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat
259
+ */
260
+ number?: Intl.NumberFormatOptions;
261
+ /**
262
+ * Options for date formatting (when value is a Date or DateTime)
263
+ * Can be:
264
+ * - A dayjs format string (e.g., "LLL", "YYYY-MM-DD", "dddd, MMMM D YYYY")
265
+ * - "fromNow" for relative time (e.g., "2 hours ago")
266
+ * - Intl.DateTimeFormatOptions for native formatting
267
+ * @see https://day.js.org/docs/en/display/format
268
+ * @see https://day.js.org/docs/en/display/from-now
269
+ */
270
+ date?: string | "fromNow" | Intl.DateTimeFormatOptions;
271
+ /**
272
+ * Timezone to display dates in (when value is a Date or DateTime)
273
+ * Uses IANA timezone names (e.g., "America/New_York", "Europe/Paris", "Asia/Tokyo")
274
+ * @see https://day.js.org/docs/en/timezone/timezone
275
+ */
276
+ timezone?: string;
277
+ }
@@ -0,0 +1,223 @@
1
+ import { useInject } from "@alepha/react";
2
+ import type { Static } from "alepha";
3
+ import type { ChannelDescriptor, TWSObject } from "alepha/websocket";
4
+ import { WebSocketClient } from "alepha/websocket";
5
+ import { useEffect, useRef, useState } from "react";
6
+
7
+ /**
8
+ * UseRoom options
9
+ */
10
+ export interface UseRoomOptions<
11
+ TClient extends TWSObject,
12
+ TServer extends TWSObject,
13
+ > {
14
+ /**
15
+ * Room ID to connect to
16
+ */
17
+ roomId: string;
18
+
19
+ /**
20
+ * Channel descriptor defining the schemas
21
+ */
22
+ channel: ChannelDescriptor<TClient, TServer>;
23
+
24
+ /**
25
+ * Handler for incoming messages from the server
26
+ */
27
+ handler: (message: Static<TClient>) => void;
28
+
29
+ /**
30
+ * Optional WebSocket URL override
31
+ * Defaults to auto-detected URL based on window.location
32
+ */
33
+ url?: string;
34
+
35
+ /**
36
+ * Enable automatic reconnection on disconnect
37
+ * @default true
38
+ */
39
+ autoReconnect?: boolean;
40
+
41
+ /**
42
+ * Reconnection interval in milliseconds
43
+ * @default 3000
44
+ */
45
+ reconnectInterval?: number;
46
+
47
+ /**
48
+ * Maximum reconnection attempts (-1 for infinite)
49
+ * @default 10
50
+ */
51
+ maxReconnectAttempts?: number;
52
+
53
+ /**
54
+ * Called when connection is established
55
+ */
56
+ onConnect?: () => void;
57
+
58
+ /**
59
+ * Called when connection is closed
60
+ */
61
+ onDisconnect?: () => void;
62
+
63
+ /**
64
+ * Called on connection error
65
+ */
66
+ onError?: (error: Error) => void;
67
+ }
68
+
69
+ /**
70
+ * UseRoom return value
71
+ */
72
+ export interface UseRoomReturn<TServer extends TWSObject> {
73
+ /**
74
+ * Send a message to the server
75
+ */
76
+ send: (message: Static<TServer>) => Promise<void>;
77
+
78
+ /**
79
+ * Whether the connection is established
80
+ */
81
+ isConnected: boolean;
82
+
83
+ /**
84
+ * Whether the connection is in progress
85
+ */
86
+ isConnecting: boolean;
87
+
88
+ /**
89
+ * Whether there was an error
90
+ */
91
+ isError: boolean;
92
+
93
+ /**
94
+ * The error object if any
95
+ */
96
+ error?: Error;
97
+
98
+ /**
99
+ * Manually reconnect
100
+ */
101
+ reconnect: () => void;
102
+
103
+ /**
104
+ * Manually disconnect
105
+ */
106
+ disconnect: () => void;
107
+ }
108
+
109
+ /**
110
+ * React hook for WebSocket room communication
111
+ *
112
+ * Provides automatic connection management, reconnection, and type-safe messaging
113
+ * for WebSocket rooms using the injected WebSocketClient service.
114
+ *
115
+ * Multiple useRoom hooks on the same channel will share a single WebSocket connection.
116
+ *
117
+ * @example
118
+ * ```tsx
119
+ * const chat = useRoom({
120
+ * roomId: "room-123",
121
+ * channel: chatChannel,
122
+ * handler: (message) => {
123
+ * if (message.type === "append") {
124
+ * setMessages(prev => [...prev, message]);
125
+ * }
126
+ * }
127
+ * }, [roomId]);
128
+ *
129
+ * const sendMessage = async () => {
130
+ * await chat.send({
131
+ * content: "Hello, world!"
132
+ * });
133
+ * };
134
+ * ```
135
+ */
136
+ export const useRoom = <TClient extends TWSObject, TServer extends TWSObject>(
137
+ options: UseRoomOptions<TClient, TServer>,
138
+ deps: unknown[],
139
+ ): UseRoomReturn<TServer> => {
140
+ const webSocketClient = useInject(WebSocketClient);
141
+ const unsubscribeRef = useRef<(() => void) | null>(null);
142
+
143
+ const [isConnected, setIsConnected] = useState(false);
144
+ const [isConnecting, setIsConnecting] = useState(false);
145
+ const [isError, setIsError] = useState(false);
146
+ const [error, setError] = useState<Error | undefined>(undefined);
147
+
148
+ const {
149
+ roomId,
150
+ channel,
151
+ handler,
152
+ url,
153
+ autoReconnect,
154
+ reconnectInterval,
155
+ maxReconnectAttempts,
156
+ onConnect,
157
+ onDisconnect,
158
+ onError,
159
+ } = options;
160
+
161
+ useEffect(() => {
162
+ // Subscribe to room
163
+ const unsubscribe = webSocketClient.subscribe(roomId, channel, handler, {
164
+ url,
165
+ autoReconnect,
166
+ reconnectInterval,
167
+ maxReconnectAttempts,
168
+ onConnect: () => {
169
+ setIsConnected(true);
170
+ setIsConnecting(false);
171
+ setIsError(false);
172
+ setError(undefined);
173
+ onConnect?.();
174
+ },
175
+ onDisconnect: () => {
176
+ setIsConnected(false);
177
+ setIsConnecting(false);
178
+ onDisconnect?.();
179
+ },
180
+ onError: (err) => {
181
+ setIsError(true);
182
+ setError(err);
183
+ setIsConnecting(false);
184
+ onError?.(err);
185
+ },
186
+ });
187
+
188
+ unsubscribeRef.current = unsubscribe;
189
+
190
+ // Get initial state from connection
191
+ const connection = webSocketClient.getConnection(channel);
192
+ if (connection) {
193
+ setIsConnected(connection.isConnected);
194
+ setIsConnecting(connection.isConnecting);
195
+ setIsError(connection.isError);
196
+ setError(connection.error);
197
+ }
198
+
199
+ // Cleanup on unmount or deps change
200
+ return () => {
201
+ unsubscribe();
202
+ unsubscribeRef.current = null;
203
+ };
204
+ // eslint-disable-next-line react-hooks/exhaustive-deps
205
+ }, deps);
206
+
207
+ return {
208
+ send: async (message: Static<TServer>) => {
209
+ await webSocketClient.send(roomId, channel, message);
210
+ },
211
+ isConnected,
212
+ isConnecting,
213
+ isError,
214
+ error,
215
+ reconnect: () => {
216
+ const connection = webSocketClient.getConnection(channel);
217
+ connection?.reconnect();
218
+ },
219
+ disconnect: () => {
220
+ unsubscribeRef.current?.();
221
+ },
222
+ };
223
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * alepha/react/websocket
3
+ *
4
+ * React hooks for real-time WebSocket communication
5
+ */
6
+
7
+ export * from "./hooks/useRoom.tsx";