@themodcraft/addon-providers 1.1.0
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/README.md +17 -0
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.js +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +81 -0
- package/dist/keyboard/index.d.ts +1 -0
- package/dist/keyboard/index.js +1 -0
- package/dist/nexus/index.d.ts +1 -0
- package/dist/nexus/index.js +1 -0
- package/dist/runtime/auth-provider.d.ts +39 -0
- package/dist/runtime/auth-provider.js +172 -0
- package/dist/runtime/config-provider.d.ts +13 -0
- package/dist/runtime/config-provider.js +11 -0
- package/dist/runtime/index.d.ts +6 -0
- package/dist/runtime/index.js +6 -0
- package/dist/runtime/keyboard-provider.d.ts +29 -0
- package/dist/runtime/keyboard-provider.js +106 -0
- package/dist/runtime/nexus-provider.d.ts +15 -0
- package/dist/runtime/nexus-provider.js +11 -0
- package/dist/runtime/theme-provider.d.ts +28 -0
- package/dist/runtime/theme-provider.js +131 -0
- package/dist/runtime/ui-overlay-provider.d.ts +14 -0
- package/dist/runtime/ui-overlay-provider.js +36 -0
- package/dist/theme/index.d.ts +1 -0
- package/dist/theme/index.js +1 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# `@themodcraft/addon-providers`
|
|
2
|
+
|
|
3
|
+
Provider presets and runtime wrappers for the UI stack.
|
|
4
|
+
|
|
5
|
+
Includes:
|
|
6
|
+
- Theme provider
|
|
7
|
+
- Auth provider
|
|
8
|
+
- Keyboard provider
|
|
9
|
+
- Nexus provider bundles
|
|
10
|
+
|
|
11
|
+
Install:
|
|
12
|
+
```bash
|
|
13
|
+
npm install @themodcraft/addon-providers
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
This package publishes compiled output only.
|
|
17
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createBetterAuthProviderPreset, type AuthProviderPreset, type BetterAuthProviderPresetOptions, } from "..";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createBetterAuthProviderPreset, } from "..";
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AuthProviderProps, KeyboardProviderProps, NexusProviderProps, ThemeProviderProps } from "./runtime";
|
|
2
|
+
import { type BetterAuthSessionLike } from "@themodcraft/addon-integrations/better-auth";
|
|
3
|
+
export * from "./runtime";
|
|
4
|
+
export type AuthProviderPreset = Omit<AuthProviderProps, "children">;
|
|
5
|
+
export type KeyboardProviderPreset = Omit<KeyboardProviderProps, "children">;
|
|
6
|
+
export type ThemeProviderPreset = Omit<ThemeProviderProps, "children">;
|
|
7
|
+
export type NexusProviderPreset = Omit<NexusProviderProps, "children">;
|
|
8
|
+
export interface BetterAuthProviderPresetOptions {
|
|
9
|
+
loginUrl?: string;
|
|
10
|
+
logoutUrl?: string;
|
|
11
|
+
onLogout?: () => void | Promise<void>;
|
|
12
|
+
session?: BetterAuthSessionLike | null;
|
|
13
|
+
settingsUrl?: string;
|
|
14
|
+
storageKey?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare const themodcraftV3ThemeProviderPreset: ThemeProviderPreset;
|
|
17
|
+
export declare const defaultKeyboardProviderPreset: KeyboardProviderPreset;
|
|
18
|
+
export declare function createBetterAuthProviderPreset({ loginUrl, logoutUrl, onLogout, session, settingsUrl, storageKey, }?: BetterAuthProviderPresetOptions): AuthProviderPreset;
|
|
19
|
+
export declare const themodcraftV3ProviderPreset: NexusProviderPreset;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { profileFromBetterAuthSession, } from "@themodcraft/addon-integrations/better-auth";
|
|
2
|
+
export * from "./runtime";
|
|
3
|
+
export const themodcraftV3ThemeProviderPreset = {
|
|
4
|
+
defaultMode: "system",
|
|
5
|
+
defaultThemeId: "themodcraft-v3",
|
|
6
|
+
storageKey: "tmc-nexus-theme",
|
|
7
|
+
themes: [
|
|
8
|
+
{
|
|
9
|
+
colorScheme: "dark light",
|
|
10
|
+
cssVariables: {
|
|
11
|
+
"--neutral-50": "#ffffff",
|
|
12
|
+
"--neutral-100": "#d9d9d9",
|
|
13
|
+
"--neutral-200": "#9e9e9e",
|
|
14
|
+
"--neutral-300": "#828282",
|
|
15
|
+
"--neutral-400": "#696969",
|
|
16
|
+
"--neutral-500": "#4a4a4a",
|
|
17
|
+
"--neutral-600": "#3c3c3c",
|
|
18
|
+
"--neutral-700": "#212121",
|
|
19
|
+
"--neutral-800": "#161616",
|
|
20
|
+
"--neutral-850": "#131313",
|
|
21
|
+
"--neutral-900": "#121212",
|
|
22
|
+
"--neutral-999": "#000000",
|
|
23
|
+
"--accent-green-500": "#30fa00",
|
|
24
|
+
"--accent-blue-500": "#005efe",
|
|
25
|
+
"--accent-cyan-400": "#00f2ff",
|
|
26
|
+
"--accent-cyan-500": "#00d2de",
|
|
27
|
+
"--primary-background": "#212121",
|
|
28
|
+
"--secondary-background": "#161616",
|
|
29
|
+
"--primary-text": "#ffffff",
|
|
30
|
+
"--secondary-text": "#828282",
|
|
31
|
+
"--tertiary-text": "#4a4a4a",
|
|
32
|
+
"--border-color": "#3c3c3c",
|
|
33
|
+
"--cta-color": "#30fa00",
|
|
34
|
+
"--link-color": "#005efe",
|
|
35
|
+
"--hover-overlay": "rgba(0, 0, 0, 0.49)",
|
|
36
|
+
"--border-radius": "15px",
|
|
37
|
+
"--border-radius-sm": "6px",
|
|
38
|
+
"--font-main": "Inter, system-ui, sans-serif",
|
|
39
|
+
"--font-mono": "JetBrains Mono, Fira Code, monospace",
|
|
40
|
+
},
|
|
41
|
+
id: "themodcraft-v3",
|
|
42
|
+
label: "TheModCraft V3",
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
export const defaultKeyboardProviderPreset = {
|
|
47
|
+
focusSelector: [
|
|
48
|
+
"a[href]",
|
|
49
|
+
"button:not([disabled])",
|
|
50
|
+
"input:not([disabled])",
|
|
51
|
+
"select:not([disabled])",
|
|
52
|
+
"textarea:not([disabled])",
|
|
53
|
+
"[tabindex]:not([tabindex='-1'])",
|
|
54
|
+
"[data-keyboard-focusable='true']",
|
|
55
|
+
].join(","),
|
|
56
|
+
shortcuts: [],
|
|
57
|
+
};
|
|
58
|
+
export function createBetterAuthProviderPreset({ loginUrl = "/login", logoutUrl = "/logout", onLogout, session, settingsUrl = "/account/settings", storageKey = "tmc-nexus-auth", } = {}) {
|
|
59
|
+
const user = profileFromBetterAuthSession(session);
|
|
60
|
+
return {
|
|
61
|
+
loginUrl,
|
|
62
|
+
logoutUrl,
|
|
63
|
+
onLogout,
|
|
64
|
+
profileImage: user?.image ?? null,
|
|
65
|
+
settingsUrl,
|
|
66
|
+
status: user ? "authenticated" : "anonymous",
|
|
67
|
+
storageKey,
|
|
68
|
+
user,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export const themodcraftV3ProviderPreset = {
|
|
72
|
+
auth: createBetterAuthProviderPreset(),
|
|
73
|
+
config: {
|
|
74
|
+
config: {
|
|
75
|
+
appName: "TMC Nexus",
|
|
76
|
+
cdnBaseUrl: "https://cdnui.themodcraft.net/api/files",
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
keyboard: defaultKeyboardProviderPreset,
|
|
80
|
+
theme: themodcraftV3ThemeProviderPreset,
|
|
81
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { defaultKeyboardProviderPreset, type KeyboardProviderPreset, } from "..";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { defaultKeyboardProviderPreset, } from "..";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { themodcraftV3ProviderPreset, type NexusProviderPreset, } from "..";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { themodcraftV3ProviderPreset, } from "..";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export interface AuthUserProfile {
|
|
3
|
+
email?: string | null;
|
|
4
|
+
id?: string | null;
|
|
5
|
+
image?: string | null;
|
|
6
|
+
name?: string | null;
|
|
7
|
+
username?: string | null;
|
|
8
|
+
}
|
|
9
|
+
export type AuthStatus = "anonymous" | "authenticated" | "loading";
|
|
10
|
+
export type AuthUserResolver = () => AuthUserProfile | null | Promise<AuthUserProfile | null>;
|
|
11
|
+
export type AuthProfileImageResolver = (user: AuthUserProfile | null) => string | null | Promise<string | null>;
|
|
12
|
+
export interface AuthProviderProps {
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
loginUrl?: string;
|
|
15
|
+
logoutUrl?: string;
|
|
16
|
+
onLogout?: () => void | Promise<void>;
|
|
17
|
+
profileImage?: string | null;
|
|
18
|
+
refreshOnMount?: boolean;
|
|
19
|
+
resolveProfileImage?: AuthProfileImageResolver;
|
|
20
|
+
resolveUser?: AuthUserResolver;
|
|
21
|
+
settingsUrl?: string;
|
|
22
|
+
status?: AuthStatus;
|
|
23
|
+
storageKey?: string;
|
|
24
|
+
user?: AuthUserProfile | null;
|
|
25
|
+
}
|
|
26
|
+
export interface AuthContextValue {
|
|
27
|
+
loginUrl?: string;
|
|
28
|
+
logout: () => Promise<void>;
|
|
29
|
+
logoutUrl?: string;
|
|
30
|
+
profileImage: string | null;
|
|
31
|
+
refreshUser: () => Promise<AuthUserProfile | null>;
|
|
32
|
+
setUser: (user: AuthUserProfile | null) => void;
|
|
33
|
+
settingsUrl?: string;
|
|
34
|
+
status: AuthStatus;
|
|
35
|
+
user: AuthUserProfile | null;
|
|
36
|
+
}
|
|
37
|
+
export declare function AuthProvider({ children, loginUrl, logoutUrl, onLogout, profileImage, refreshOnMount, resolveProfileImage, resolveUser, settingsUrl, status, storageKey, user: initialUser, }: AuthProviderProps): import("react").JSX.Element;
|
|
38
|
+
export declare function useAuth(): AuthContextValue;
|
|
39
|
+
export declare function useOptionalAuth(): AuthContextValue | null;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from "react";
|
|
4
|
+
const AuthContext = createContext(null);
|
|
5
|
+
const authEventName = "tmc:nexus-auth";
|
|
6
|
+
function statusFromUser(user, fallback) {
|
|
7
|
+
if (fallback) {
|
|
8
|
+
return fallback;
|
|
9
|
+
}
|
|
10
|
+
return user ? "authenticated" : "anonymous";
|
|
11
|
+
}
|
|
12
|
+
function readStoredAuthSnapshot(storageKey) {
|
|
13
|
+
if (!storageKey || typeof window === "undefined") {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const rawSnapshot = window.localStorage.getItem(storageKey);
|
|
18
|
+
return rawSnapshot ? JSON.parse(rawSnapshot) : null;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function writeStoredAuthSnapshot(storageKey, snapshot) {
|
|
25
|
+
if (!storageKey || typeof window === "undefined") {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (!snapshot) {
|
|
29
|
+
window.localStorage.removeItem(storageKey);
|
|
30
|
+
window.dispatchEvent(new CustomEvent(authEventName, { detail: null }));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
window.localStorage.setItem(storageKey, JSON.stringify(snapshot));
|
|
34
|
+
window.dispatchEvent(new CustomEvent(authEventName, { detail: snapshot }));
|
|
35
|
+
}
|
|
36
|
+
export function AuthProvider({ children, loginUrl, logoutUrl, onLogout, profileImage, refreshOnMount = true, resolveProfileImage, resolveUser, settingsUrl, status, storageKey = "tmc-nexus-auth", user: initialUser = null, }) {
|
|
37
|
+
const [user, setUserState] = useState(initialUser);
|
|
38
|
+
const [resolvedProfileImage, setResolvedProfileImage] = useState(profileImage ?? initialUser?.image ?? null);
|
|
39
|
+
const [resolvedStatus, setResolvedStatus] = useState(statusFromUser(initialUser, status));
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const storedSnapshot = readStoredAuthSnapshot(storageKey);
|
|
42
|
+
if (storedSnapshot) {
|
|
43
|
+
setUserState(storedSnapshot.user);
|
|
44
|
+
setResolvedProfileImage(storedSnapshot.profileImage);
|
|
45
|
+
setResolvedStatus(storedSnapshot.status);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
setUserState(initialUser);
|
|
49
|
+
setResolvedProfileImage(profileImage ?? initialUser?.image ?? null);
|
|
50
|
+
setResolvedStatus(statusFromUser(initialUser, status));
|
|
51
|
+
}, [initialUser, profileImage, status, storageKey]);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
function handleStorage(event) {
|
|
54
|
+
if (event.key !== storageKey) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const snapshot = readStoredAuthSnapshot(storageKey);
|
|
58
|
+
setUserState(snapshot?.user ?? null);
|
|
59
|
+
setResolvedProfileImage(snapshot?.profileImage ?? null);
|
|
60
|
+
setResolvedStatus(snapshot?.status ?? "anonymous");
|
|
61
|
+
}
|
|
62
|
+
function handleAuthEvent(event) {
|
|
63
|
+
const snapshot = event.detail;
|
|
64
|
+
setUserState(snapshot?.user ?? null);
|
|
65
|
+
setResolvedProfileImage(snapshot?.profileImage ?? null);
|
|
66
|
+
setResolvedStatus(snapshot?.status ?? "anonymous");
|
|
67
|
+
}
|
|
68
|
+
window.addEventListener("storage", handleStorage);
|
|
69
|
+
window.addEventListener(authEventName, handleAuthEvent);
|
|
70
|
+
return () => {
|
|
71
|
+
window.removeEventListener("storage", handleStorage);
|
|
72
|
+
window.removeEventListener(authEventName, handleAuthEvent);
|
|
73
|
+
};
|
|
74
|
+
}, [storageKey]);
|
|
75
|
+
const refreshUser = useCallback(async () => {
|
|
76
|
+
if (!resolveUser) {
|
|
77
|
+
return user;
|
|
78
|
+
}
|
|
79
|
+
setResolvedStatus("loading");
|
|
80
|
+
const nextUser = await resolveUser();
|
|
81
|
+
const nextProfileImage = resolveProfileImage
|
|
82
|
+
? await resolveProfileImage(nextUser)
|
|
83
|
+
: nextUser?.image ?? null;
|
|
84
|
+
setUserState(nextUser);
|
|
85
|
+
setResolvedProfileImage(nextProfileImage);
|
|
86
|
+
setResolvedStatus(nextUser ? "authenticated" : "anonymous");
|
|
87
|
+
writeStoredAuthSnapshot(storageKey, {
|
|
88
|
+
profileImage: nextProfileImage,
|
|
89
|
+
status: nextUser ? "authenticated" : "anonymous",
|
|
90
|
+
user: nextUser,
|
|
91
|
+
});
|
|
92
|
+
return nextUser;
|
|
93
|
+
}, [resolveProfileImage, resolveUser, storageKey, user]);
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!refreshOnMount || !resolveUser) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
let cancelled = false;
|
|
99
|
+
async function refresh() {
|
|
100
|
+
const nextUser = await resolveUser?.();
|
|
101
|
+
if (cancelled) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const nextProfileImage = resolveProfileImage
|
|
105
|
+
? await resolveProfileImage(nextUser ?? null)
|
|
106
|
+
: nextUser?.image ?? null;
|
|
107
|
+
if (!cancelled) {
|
|
108
|
+
setUserState(nextUser ?? null);
|
|
109
|
+
setResolvedProfileImage(nextProfileImage);
|
|
110
|
+
setResolvedStatus(nextUser ? "authenticated" : "anonymous");
|
|
111
|
+
writeStoredAuthSnapshot(storageKey, {
|
|
112
|
+
profileImage: nextProfileImage,
|
|
113
|
+
status: nextUser ? "authenticated" : "anonymous",
|
|
114
|
+
user: nextUser ?? null,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
refresh();
|
|
119
|
+
return () => {
|
|
120
|
+
cancelled = true;
|
|
121
|
+
};
|
|
122
|
+
}, [refreshOnMount, resolveProfileImage, resolveUser, storageKey]);
|
|
123
|
+
const setUser = useCallback((nextUser) => {
|
|
124
|
+
const nextProfileImage = nextUser?.image ?? null;
|
|
125
|
+
const nextStatus = nextUser ? "authenticated" : "anonymous";
|
|
126
|
+
setUserState(nextUser);
|
|
127
|
+
setResolvedProfileImage(nextProfileImage);
|
|
128
|
+
setResolvedStatus(nextStatus);
|
|
129
|
+
writeStoredAuthSnapshot(storageKey, {
|
|
130
|
+
profileImage: nextProfileImage,
|
|
131
|
+
status: nextStatus,
|
|
132
|
+
user: nextUser,
|
|
133
|
+
});
|
|
134
|
+
}, [storageKey]);
|
|
135
|
+
const logout = useCallback(async () => {
|
|
136
|
+
await onLogout?.();
|
|
137
|
+
setUser(null);
|
|
138
|
+
writeStoredAuthSnapshot(storageKey, null);
|
|
139
|
+
}, [onLogout, setUser, storageKey]);
|
|
140
|
+
const value = useMemo(() => ({
|
|
141
|
+
loginUrl,
|
|
142
|
+
logout,
|
|
143
|
+
logoutUrl,
|
|
144
|
+
profileImage: resolvedProfileImage,
|
|
145
|
+
refreshUser,
|
|
146
|
+
setUser,
|
|
147
|
+
settingsUrl,
|
|
148
|
+
status: resolvedStatus,
|
|
149
|
+
user,
|
|
150
|
+
}), [
|
|
151
|
+
loginUrl,
|
|
152
|
+
logout,
|
|
153
|
+
logoutUrl,
|
|
154
|
+
refreshUser,
|
|
155
|
+
resolvedProfileImage,
|
|
156
|
+
resolvedStatus,
|
|
157
|
+
setUser,
|
|
158
|
+
settingsUrl,
|
|
159
|
+
user,
|
|
160
|
+
]);
|
|
161
|
+
return _jsx(AuthContext.Provider, { value: value, children: children });
|
|
162
|
+
}
|
|
163
|
+
export function useAuth() {
|
|
164
|
+
const context = useContext(AuthContext);
|
|
165
|
+
if (!context) {
|
|
166
|
+
throw new Error("useAuth must be used inside AuthProvider.");
|
|
167
|
+
}
|
|
168
|
+
return context;
|
|
169
|
+
}
|
|
170
|
+
export function useOptionalAuth() {
|
|
171
|
+
return useContext(AuthContext);
|
|
172
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export interface TmcRuntimeConfig {
|
|
3
|
+
appName?: string;
|
|
4
|
+
assetBaseUrl?: string;
|
|
5
|
+
cdnBaseUrl?: string;
|
|
6
|
+
locale?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ConfigProviderProps {
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
config?: TmcRuntimeConfig;
|
|
11
|
+
}
|
|
12
|
+
export declare function ConfigProvider({ children, config }: ConfigProviderProps): import("react").JSX.Element;
|
|
13
|
+
export declare function useConfig(): TmcRuntimeConfig;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext, useMemo, } from "react";
|
|
4
|
+
const ConfigContext = createContext({});
|
|
5
|
+
export function ConfigProvider({ children, config = {} }) {
|
|
6
|
+
const value = useMemo(() => config, [config]);
|
|
7
|
+
return _jsx(ConfigContext.Provider, { value: value, children: children });
|
|
8
|
+
}
|
|
9
|
+
export function useConfig() {
|
|
10
|
+
return useContext(ConfigContext);
|
|
11
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type KeyboardEvent as ReactKeyboardEvent, type ReactNode } from "react";
|
|
2
|
+
export interface KeyboardShortcut {
|
|
3
|
+
allowInEditable?: boolean;
|
|
4
|
+
altKey?: boolean;
|
|
5
|
+
ctrlKey?: boolean;
|
|
6
|
+
enabled?: boolean;
|
|
7
|
+
handler: (event: KeyboardEvent) => void;
|
|
8
|
+
id: string;
|
|
9
|
+
key: string;
|
|
10
|
+
metaKey?: boolean;
|
|
11
|
+
preventDefault?: boolean;
|
|
12
|
+
shiftKey?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface KeyboardProviderProps {
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
focusSelector?: string;
|
|
17
|
+
shortcuts?: KeyboardShortcut[];
|
|
18
|
+
}
|
|
19
|
+
export interface KeyboardContextValue {
|
|
20
|
+
focusFirst: (selector?: string) => void;
|
|
21
|
+
focusNext: (selector?: string) => void;
|
|
22
|
+
focusPrevious: (selector?: string) => void;
|
|
23
|
+
registerShortcut: (shortcut: KeyboardShortcut) => () => void;
|
|
24
|
+
}
|
|
25
|
+
export declare function KeyboardProvider({ children, focusSelector, shortcuts, }: KeyboardProviderProps): import("react").JSX.Element;
|
|
26
|
+
export declare function useKeyboard(): KeyboardContextValue;
|
|
27
|
+
export declare function useOptionalKeyboard(): KeyboardContextValue | null;
|
|
28
|
+
export declare function useKeyboardShortcut(shortcut: KeyboardShortcut): void;
|
|
29
|
+
export declare function handleKeyboardClick(event: ReactKeyboardEvent, callback: () => void): void;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, } from "react";
|
|
4
|
+
const defaultFocusSelector = [
|
|
5
|
+
"a[href]",
|
|
6
|
+
"button:not([disabled])",
|
|
7
|
+
"input:not([disabled])",
|
|
8
|
+
"select:not([disabled])",
|
|
9
|
+
"textarea:not([disabled])",
|
|
10
|
+
"[tabindex]:not([tabindex='-1'])",
|
|
11
|
+
"[data-keyboard-focusable='true']",
|
|
12
|
+
].join(",");
|
|
13
|
+
const KeyboardContext = createContext(null);
|
|
14
|
+
function isEditableTarget(target) {
|
|
15
|
+
if (!(target instanceof HTMLElement)) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return (target.isContentEditable ||
|
|
19
|
+
target instanceof HTMLInputElement ||
|
|
20
|
+
target instanceof HTMLSelectElement ||
|
|
21
|
+
target instanceof HTMLTextAreaElement);
|
|
22
|
+
}
|
|
23
|
+
function matchesShortcut(event, shortcut) {
|
|
24
|
+
return (shortcut.enabled !== false &&
|
|
25
|
+
event.key.toLowerCase() === shortcut.key.toLowerCase() &&
|
|
26
|
+
Boolean(event.altKey) === Boolean(shortcut.altKey) &&
|
|
27
|
+
Boolean(event.ctrlKey) === Boolean(shortcut.ctrlKey) &&
|
|
28
|
+
Boolean(event.metaKey) === Boolean(shortcut.metaKey) &&
|
|
29
|
+
Boolean(event.shiftKey) === Boolean(shortcut.shiftKey));
|
|
30
|
+
}
|
|
31
|
+
function getFocusableElements(selector) {
|
|
32
|
+
return Array.from(document.querySelectorAll(selector)).filter((element) => {
|
|
33
|
+
const style = window.getComputedStyle(element);
|
|
34
|
+
return !element.hasAttribute("disabled") && style.display !== "none" && style.visibility !== "hidden";
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export function KeyboardProvider({ children, focusSelector = defaultFocusSelector, shortcuts = [], }) {
|
|
38
|
+
const shortcutsRef = useRef(new Map());
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
shortcutsRef.current = new Map(shortcuts.map((shortcut) => [shortcut.id, shortcut]));
|
|
41
|
+
}, [shortcuts]);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
function handleKeyDown(event) {
|
|
44
|
+
for (const shortcut of shortcutsRef.current.values()) {
|
|
45
|
+
if (!matchesShortcut(event, shortcut)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (!shortcut.allowInEditable && isEditableTarget(event.target)) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (shortcut.preventDefault ?? true) {
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
}
|
|
54
|
+
shortcut.handler(event);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
58
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
59
|
+
}, []);
|
|
60
|
+
const registerShortcut = useCallback((shortcut) => {
|
|
61
|
+
shortcutsRef.current.set(shortcut.id, shortcut);
|
|
62
|
+
return () => {
|
|
63
|
+
shortcutsRef.current.delete(shortcut.id);
|
|
64
|
+
};
|
|
65
|
+
}, []);
|
|
66
|
+
const focusByOffset = useCallback((offset, selector = focusSelector) => {
|
|
67
|
+
const elements = getFocusableElements(selector);
|
|
68
|
+
if (!elements.length) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const currentIndex = document.activeElement instanceof HTMLElement
|
|
72
|
+
? elements.indexOf(document.activeElement)
|
|
73
|
+
: -1;
|
|
74
|
+
const nextIndex = currentIndex === -1
|
|
75
|
+
? 0
|
|
76
|
+
: (currentIndex + offset + elements.length) % elements.length;
|
|
77
|
+
elements[nextIndex]?.focus();
|
|
78
|
+
}, [focusSelector]);
|
|
79
|
+
const value = useMemo(() => ({
|
|
80
|
+
focusFirst: (selector = focusSelector) => getFocusableElements(selector)[0]?.focus(),
|
|
81
|
+
focusNext: (selector = focusSelector) => focusByOffset(1, selector),
|
|
82
|
+
focusPrevious: (selector = focusSelector) => focusByOffset(-1, selector),
|
|
83
|
+
registerShortcut,
|
|
84
|
+
}), [focusByOffset, focusSelector, registerShortcut]);
|
|
85
|
+
return _jsx(KeyboardContext.Provider, { value: value, children: children });
|
|
86
|
+
}
|
|
87
|
+
export function useKeyboard() {
|
|
88
|
+
const context = useContext(KeyboardContext);
|
|
89
|
+
if (!context) {
|
|
90
|
+
throw new Error("useKeyboard must be used inside KeyboardProvider.");
|
|
91
|
+
}
|
|
92
|
+
return context;
|
|
93
|
+
}
|
|
94
|
+
export function useOptionalKeyboard() {
|
|
95
|
+
return useContext(KeyboardContext);
|
|
96
|
+
}
|
|
97
|
+
export function useKeyboardShortcut(shortcut) {
|
|
98
|
+
const keyboard = useKeyboard();
|
|
99
|
+
useEffect(() => keyboard.registerShortcut(shortcut), [keyboard, shortcut]);
|
|
100
|
+
}
|
|
101
|
+
export function handleKeyboardClick(event, callback) {
|
|
102
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
callback();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { type AuthProviderProps } from "./auth-provider";
|
|
3
|
+
import { type ConfigProviderProps } from "./config-provider";
|
|
4
|
+
import { type KeyboardProviderProps } from "./keyboard-provider";
|
|
5
|
+
import { type ThemeProviderProps } from "./theme-provider";
|
|
6
|
+
import { type UIOverlayProviderProps } from "./ui-overlay-provider";
|
|
7
|
+
export interface NexusProviderProps {
|
|
8
|
+
auth?: Omit<AuthProviderProps, "children">;
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
config?: Omit<ConfigProviderProps, "children">;
|
|
11
|
+
keyboard?: Omit<KeyboardProviderProps, "children">;
|
|
12
|
+
overlay?: Omit<UIOverlayProviderProps, "children">;
|
|
13
|
+
theme?: Omit<ThemeProviderProps, "children">;
|
|
14
|
+
}
|
|
15
|
+
export declare function NexusProvider({ auth, children, config, keyboard, overlay, theme, }: NexusProviderProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { LicenseGate, TMC_ENTITLEMENTS } from "@themodcraft/license-client";
|
|
4
|
+
import { AuthProvider, } from "./auth-provider";
|
|
5
|
+
import { ConfigProvider, } from "./config-provider";
|
|
6
|
+
import { KeyboardProvider, } from "./keyboard-provider";
|
|
7
|
+
import { ThemeProvider, } from "./theme-provider";
|
|
8
|
+
import { UIOverlayProvider, } from "./ui-overlay-provider";
|
|
9
|
+
export function NexusProvider({ auth, children, config, keyboard, overlay, theme, }) {
|
|
10
|
+
return (_jsx(LicenseGate, { required: TMC_ENTITLEMENTS.addonProviders, children: _jsx(ConfigProvider, { ...config, children: _jsx(ThemeProvider, { ...theme, children: _jsx(AuthProvider, { ...auth, children: _jsx(KeyboardProvider, { ...keyboard, children: _jsx(UIOverlayProvider, { ...overlay, children: children }) }) }) }) }) }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export type ThemeMode = "light" | "dark" | "system";
|
|
3
|
+
export type ThemeTokenValue = string | number;
|
|
4
|
+
export interface ThemeDefinition {
|
|
5
|
+
className?: string;
|
|
6
|
+
colorScheme?: string;
|
|
7
|
+
cssVariables?: Record<string, ThemeTokenValue>;
|
|
8
|
+
id: string;
|
|
9
|
+
label?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ThemeProviderProps {
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
defaultMode?: ThemeMode;
|
|
14
|
+
defaultThemeId?: string;
|
|
15
|
+
storageKey?: string;
|
|
16
|
+
themes?: ThemeDefinition[];
|
|
17
|
+
}
|
|
18
|
+
export interface ThemeContextValue {
|
|
19
|
+
activeTheme?: ThemeDefinition;
|
|
20
|
+
mode: ThemeMode;
|
|
21
|
+
setMode: (mode: ThemeMode) => void;
|
|
22
|
+
setThemeId: (themeId: string) => void;
|
|
23
|
+
themeId: string;
|
|
24
|
+
themes: ThemeDefinition[];
|
|
25
|
+
}
|
|
26
|
+
export declare function ThemeProvider({ children, defaultMode, defaultThemeId, storageKey, themes, }: ThemeProviderProps): import("react").JSX.Element;
|
|
27
|
+
export declare function useTheme(): ThemeContextValue;
|
|
28
|
+
export declare function useOptionalTheme(): ThemeContextValue | null;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from "react";
|
|
4
|
+
const ThemeContext = createContext(null);
|
|
5
|
+
const themeModeEventName = "tmc:nexus-theme-mode";
|
|
6
|
+
const themeIdEventName = "tmc:nexus-theme-id";
|
|
7
|
+
function toCssVariableName(tokenName) {
|
|
8
|
+
if (tokenName.startsWith("--")) {
|
|
9
|
+
return tokenName;
|
|
10
|
+
}
|
|
11
|
+
return `--tmc-${tokenName.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`).replace(/[_\s]+/g, "-")}`;
|
|
12
|
+
}
|
|
13
|
+
function setRootAttribute(name, value) {
|
|
14
|
+
if (!value) {
|
|
15
|
+
document.documentElement.removeAttribute(name);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
document.documentElement.setAttribute(name, value);
|
|
19
|
+
}
|
|
20
|
+
export function ThemeProvider({ children, defaultMode = "system", defaultThemeId, storageKey, themes = [], }) {
|
|
21
|
+
const initialThemeId = defaultThemeId ?? themes[0]?.id ?? "default";
|
|
22
|
+
const [themeId, setThemeIdState] = useState(initialThemeId);
|
|
23
|
+
const [mode, setModeState] = useState(defaultMode);
|
|
24
|
+
const activeTheme = themes.find((theme) => theme.id === themeId) ?? themes[0];
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!storageKey) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const storedThemeId = window.localStorage.getItem(`${storageKey}:theme`);
|
|
30
|
+
const storedMode = window.localStorage.getItem(`${storageKey}:mode`);
|
|
31
|
+
if (storedThemeId) {
|
|
32
|
+
setThemeIdState(storedThemeId);
|
|
33
|
+
}
|
|
34
|
+
if (storedMode === "light" || storedMode === "dark" || storedMode === "system") {
|
|
35
|
+
setModeState(storedMode);
|
|
36
|
+
}
|
|
37
|
+
}, [storageKey]);
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!storageKey) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
function handleStorage(event) {
|
|
43
|
+
if (event.key === `${storageKey}:theme` && event.newValue) {
|
|
44
|
+
setThemeIdState(event.newValue);
|
|
45
|
+
}
|
|
46
|
+
if (event.key === `${storageKey}:mode` &&
|
|
47
|
+
(event.newValue === "light" || event.newValue === "dark" || event.newValue === "system")) {
|
|
48
|
+
setModeState(event.newValue);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function handleThemeMode(event) {
|
|
52
|
+
const nextMode = event.detail?.mode;
|
|
53
|
+
if (nextMode === "light" || nextMode === "dark" || nextMode === "system") {
|
|
54
|
+
setModeState(nextMode);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function handleThemeId(event) {
|
|
58
|
+
const nextThemeId = event.detail?.themeId;
|
|
59
|
+
if (nextThemeId) {
|
|
60
|
+
setThemeIdState(nextThemeId);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
window.addEventListener("storage", handleStorage);
|
|
64
|
+
window.addEventListener(themeModeEventName, handleThemeMode);
|
|
65
|
+
window.addEventListener(themeIdEventName, handleThemeId);
|
|
66
|
+
return () => {
|
|
67
|
+
window.removeEventListener("storage", handleStorage);
|
|
68
|
+
window.removeEventListener(themeModeEventName, handleThemeMode);
|
|
69
|
+
window.removeEventListener(themeIdEventName, handleThemeId);
|
|
70
|
+
};
|
|
71
|
+
}, [storageKey]);
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
const root = document.documentElement;
|
|
74
|
+
const appliedVariableNames = [];
|
|
75
|
+
setRootAttribute("data-tmc-theme", activeTheme?.id);
|
|
76
|
+
setRootAttribute("data-tmc-theme-mode", mode);
|
|
77
|
+
if (activeTheme?.className) {
|
|
78
|
+
root.classList.add(activeTheme.className);
|
|
79
|
+
}
|
|
80
|
+
if (activeTheme?.colorScheme) {
|
|
81
|
+
root.style.colorScheme = activeTheme.colorScheme;
|
|
82
|
+
}
|
|
83
|
+
Object.entries(activeTheme?.cssVariables ?? {}).forEach(([name, value]) => {
|
|
84
|
+
const variableName = toCssVariableName(name);
|
|
85
|
+
appliedVariableNames.push(variableName);
|
|
86
|
+
root.style.setProperty(variableName, String(value));
|
|
87
|
+
});
|
|
88
|
+
return () => {
|
|
89
|
+
appliedVariableNames.forEach((name) => root.style.removeProperty(name));
|
|
90
|
+
if (activeTheme?.className) {
|
|
91
|
+
root.classList.remove(activeTheme.className);
|
|
92
|
+
}
|
|
93
|
+
if (activeTheme?.colorScheme) {
|
|
94
|
+
root.style.removeProperty("color-scheme");
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}, [activeTheme, mode]);
|
|
98
|
+
const setThemeId = useCallback((nextThemeId) => {
|
|
99
|
+
setThemeIdState(nextThemeId);
|
|
100
|
+
if (storageKey) {
|
|
101
|
+
window.localStorage.setItem(`${storageKey}:theme`, nextThemeId);
|
|
102
|
+
window.dispatchEvent(new CustomEvent(themeIdEventName, { detail: { themeId: nextThemeId } }));
|
|
103
|
+
}
|
|
104
|
+
}, [storageKey]);
|
|
105
|
+
const setMode = useCallback((nextMode) => {
|
|
106
|
+
setModeState(nextMode);
|
|
107
|
+
if (storageKey) {
|
|
108
|
+
window.localStorage.setItem(`${storageKey}:mode`, nextMode);
|
|
109
|
+
window.dispatchEvent(new CustomEvent(themeModeEventName, { detail: { mode: nextMode } }));
|
|
110
|
+
}
|
|
111
|
+
}, [storageKey]);
|
|
112
|
+
const value = useMemo(() => ({
|
|
113
|
+
activeTheme,
|
|
114
|
+
mode,
|
|
115
|
+
setMode,
|
|
116
|
+
setThemeId,
|
|
117
|
+
themeId,
|
|
118
|
+
themes,
|
|
119
|
+
}), [activeTheme, mode, setMode, setThemeId, themeId, themes]);
|
|
120
|
+
return _jsx(ThemeContext.Provider, { value: value, children: children });
|
|
121
|
+
}
|
|
122
|
+
export function useTheme() {
|
|
123
|
+
const context = useContext(ThemeContext);
|
|
124
|
+
if (!context) {
|
|
125
|
+
throw new Error("useTheme must be used inside ThemeProvider.");
|
|
126
|
+
}
|
|
127
|
+
return context;
|
|
128
|
+
}
|
|
129
|
+
export function useOptionalTheme() {
|
|
130
|
+
return useContext(ThemeContext);
|
|
131
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export interface UIOverlayProviderProps {
|
|
3
|
+
children: ReactNode;
|
|
4
|
+
}
|
|
5
|
+
export interface UIOverlayContextValue {
|
|
6
|
+
closeOverlay: (id: string) => void;
|
|
7
|
+
isOverlayOpen: (id: string) => boolean;
|
|
8
|
+
openOverlay: (id: string) => void;
|
|
9
|
+
openOverlayIds: string[];
|
|
10
|
+
toggleOverlay: (id: string) => void;
|
|
11
|
+
}
|
|
12
|
+
export declare function UIOverlayProvider({ children }: UIOverlayProviderProps): import("react").JSX.Element;
|
|
13
|
+
export declare function useUIOverlay(): UIOverlayContextValue;
|
|
14
|
+
export declare function useOptionalUIOverlay(): UIOverlayContextValue | null;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useCallback, useContext, useMemo, useState, } from "react";
|
|
4
|
+
const UIOverlayContext = createContext(null);
|
|
5
|
+
export function UIOverlayProvider({ children }) {
|
|
6
|
+
const [openOverlayIds, setOpenOverlayIds] = useState([]);
|
|
7
|
+
const openOverlay = useCallback((id) => {
|
|
8
|
+
setOpenOverlayIds((current) => (current.includes(id) ? current : [...current, id]));
|
|
9
|
+
}, []);
|
|
10
|
+
const closeOverlay = useCallback((id) => {
|
|
11
|
+
setOpenOverlayIds((current) => current.filter((currentId) => currentId !== id));
|
|
12
|
+
}, []);
|
|
13
|
+
const toggleOverlay = useCallback((id) => {
|
|
14
|
+
setOpenOverlayIds((current) => (current.includes(id)
|
|
15
|
+
? current.filter((currentId) => currentId !== id)
|
|
16
|
+
: [...current, id]));
|
|
17
|
+
}, []);
|
|
18
|
+
const value = useMemo(() => ({
|
|
19
|
+
closeOverlay,
|
|
20
|
+
isOverlayOpen: (id) => openOverlayIds.includes(id),
|
|
21
|
+
openOverlay,
|
|
22
|
+
openOverlayIds,
|
|
23
|
+
toggleOverlay,
|
|
24
|
+
}), [closeOverlay, openOverlay, openOverlayIds, toggleOverlay]);
|
|
25
|
+
return _jsx(UIOverlayContext.Provider, { value: value, children: children });
|
|
26
|
+
}
|
|
27
|
+
export function useUIOverlay() {
|
|
28
|
+
const context = useContext(UIOverlayContext);
|
|
29
|
+
if (!context) {
|
|
30
|
+
throw new Error("useUIOverlay must be used inside UIOverlayProvider.");
|
|
31
|
+
}
|
|
32
|
+
return context;
|
|
33
|
+
}
|
|
34
|
+
export function useOptionalUIOverlay() {
|
|
35
|
+
return useContext(UIOverlayContext);
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { themodcraftV3ThemeProviderPreset, type ThemeProviderPreset, } from "..";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { themodcraftV3ThemeProviderPreset, } from "..";
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@themodcraft/addon-providers",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"type": "module",
|
|
8
|
+
"files": ["dist"],
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./auth": {
|
|
15
|
+
"types": "./dist/auth/index.d.ts",
|
|
16
|
+
"default": "./dist/auth/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./keyboard": {
|
|
19
|
+
"types": "./dist/keyboard/index.d.ts",
|
|
20
|
+
"default": "./dist/keyboard/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./nexus": {
|
|
23
|
+
"types": "./dist/nexus/index.d.ts",
|
|
24
|
+
"default": "./dist/nexus/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./theme": {
|
|
27
|
+
"types": "./dist/theme/index.d.ts",
|
|
28
|
+
"default": "./dist/theme/index.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "node ../../tools/build-package.mjs",
|
|
33
|
+
"prepack": "npm run build",
|
|
34
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@themodcraft/addon-integrations": "1.1.0",
|
|
38
|
+
"@themodcraft/license-client": "1.1.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"react": "^19.0.0",
|
|
42
|
+
"react-dom": "^19.0.0"
|
|
43
|
+
}
|
|
44
|
+
}
|