@multiplatform.one/core 7.0.0 → 7.2.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/package.json +16 -13
- package/src/app/CreateApp.tsx +25 -0
- package/src/app/CreateRootLayout.web.tsx +9 -0
- package/src/app/RuntimePublicConfigScript.spec.tsx +73 -0
- package/src/app/RuntimePublicConfigScript.tsx +54 -0
- package/src/app/TanstackDevtools.tsx +49 -5
- package/src/app/devtoolsTrigger.spec.ts +488 -0
- package/src/app/devtoolsTrigger.ts +385 -0
- package/types/app/CreateApp.d.ts +25 -0
- package/types/app/CreateApp.d.ts.map +1 -1
- package/types/app/CreateRootLayout.web.d.ts.map +1 -1
- package/types/app/RuntimePublicConfigScript.d.ts +35 -0
- package/types/app/RuntimePublicConfigScript.d.ts.map +1 -0
- package/types/app/TanstackDevtools.d.ts.map +1 -1
- package/types/app/devtoolsTrigger.d.ts +155 -0
- package/types/app/devtoolsTrigger.d.ts.map +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/core",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.2.0",
|
|
4
4
|
"description": "Core framework shell, provider composition, and layout factories for multiplatform.one",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"app",
|
|
@@ -42,15 +42,16 @@
|
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@tanstack/devtools-event-client": "^0.4.3",
|
|
45
|
+
"@tanstack/devtools-utils": "^0.4.0",
|
|
46
|
+
"@tanstack/pacer-devtools": "1.3.0",
|
|
45
47
|
"@tanstack/react-devtools": "^0.10.2",
|
|
46
48
|
"@tanstack/react-form-devtools": "0.2.22",
|
|
47
|
-
"@tanstack/react-pacer-devtools": "^0.7.0",
|
|
48
49
|
"react-cookie": "^8.1.2",
|
|
49
|
-
"@multiplatform.one/
|
|
50
|
-
"@multiplatform.one/logger": "7.
|
|
51
|
-
"@multiplatform.one/
|
|
52
|
-
"@multiplatform.one/
|
|
53
|
-
"@multiplatform.one/
|
|
50
|
+
"@multiplatform.one/platform": "7.2.0",
|
|
51
|
+
"@multiplatform.one/logger": "7.2.0",
|
|
52
|
+
"@multiplatform.one/i18n": "7.2.0",
|
|
53
|
+
"@multiplatform.one/theme": "7.2.0",
|
|
54
|
+
"@multiplatform.one/store": "7.2.0"
|
|
54
55
|
},
|
|
55
56
|
"devDependencies": {
|
|
56
57
|
"@sentry/react": "^10.51.0",
|
|
@@ -60,14 +61,16 @@
|
|
|
60
61
|
"i18next": "^25.10.10",
|
|
61
62
|
"one": "^1.16.5",
|
|
62
63
|
"react": "19.2.5",
|
|
64
|
+
"react-dom": "19.2.5",
|
|
63
65
|
"react-i18next": "^16.6.6",
|
|
64
66
|
"react-native": "0.83.2",
|
|
65
67
|
"react-native-gesture-handler": "~2.31.1",
|
|
66
68
|
"react-native-safe-area-context": "~5.7.0",
|
|
67
69
|
"tamagui": "2.7.6",
|
|
68
70
|
"typescript": "~5.9.3",
|
|
69
|
-
"
|
|
70
|
-
"@multiplatform.one/web3": "7.
|
|
71
|
+
"vitest": "^4.1.5",
|
|
72
|
+
"@multiplatform.one/web3": "7.2.0",
|
|
73
|
+
"@multiplatform.one/frappe": "7.2.0"
|
|
71
74
|
},
|
|
72
75
|
"peerDependencies": {
|
|
73
76
|
"@sentry/react": "^9.0.0",
|
|
@@ -83,10 +86,10 @@
|
|
|
83
86
|
"react-native-gesture-handler": ">=2.0.0",
|
|
84
87
|
"react-native-safe-area-context": ">=4.0.0",
|
|
85
88
|
"tamagui": "^2.0.0-rc",
|
|
86
|
-
"@multiplatform.one/frappe": "^7.
|
|
87
|
-
"@multiplatform.one/frappe-ui": "^7.
|
|
88
|
-
"@multiplatform.one/keycloak": "^7.
|
|
89
|
-
"@multiplatform.one/web3": "^7.
|
|
89
|
+
"@multiplatform.one/frappe": "^7.2.0",
|
|
90
|
+
"@multiplatform.one/frappe-ui": "^7.2.0",
|
|
91
|
+
"@multiplatform.one/keycloak": "^7.2.0",
|
|
92
|
+
"@multiplatform.one/web3": "^7.2.0"
|
|
90
93
|
},
|
|
91
94
|
"peerDependenciesMeta": {
|
|
92
95
|
"@multiplatform.one/web3": {
|
package/src/app/CreateApp.tsx
CHANGED
|
@@ -52,6 +52,15 @@ export function followPageOriginForLoopback(configured: string): string {
|
|
|
52
52
|
export interface TanstackConfig {
|
|
53
53
|
/** Show React Query devtools (web only, ignored on native and in Storybook). */
|
|
54
54
|
debug?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Shake the device to open the devtools shell (web only; requires
|
|
57
|
+
* `debug`). Defaults to true whenever the devtools mount. On iOS 13+
|
|
58
|
+
* WebKit the motion permission is requested once from the user's first
|
|
59
|
+
* tap; Android and desktop need no permission (desktop simply never
|
|
60
|
+
* fires devicemotion — use the Ctrl/Cmd+` shortcut there). Set false to
|
|
61
|
+
* keep the keyboard shortcut as the only trigger.
|
|
62
|
+
*/
|
|
63
|
+
shakeToOpen?: boolean;
|
|
55
64
|
/** Options forwarded to the QueryClient constructor. */
|
|
56
65
|
queryClientOptions?: Record<string, unknown>;
|
|
57
66
|
/**
|
|
@@ -70,6 +79,22 @@ export interface TanstackConfig {
|
|
|
70
79
|
* Passed automatically by `createApp()` from the theme config.
|
|
71
80
|
*/
|
|
72
81
|
tamaguiConfig?: unknown;
|
|
82
|
+
/**
|
|
83
|
+
* Theme COLOR names the devtools panel offers in its accent dropdown
|
|
84
|
+
* (e.g. `["", "blue", "green", "purple"]`; `""` is the base theme).
|
|
85
|
+
* The panel renders the dropdown only when this AND `onThemeColorChange`
|
|
86
|
+
* are both set, and only then does the panel's Randomize action touch the
|
|
87
|
+
* accent — otherwise the picker is inert.
|
|
88
|
+
*/
|
|
89
|
+
themeColors?: string[];
|
|
90
|
+
/** The theme color the app currently has applied (`""` for the base). */
|
|
91
|
+
currentThemeColor?: string;
|
|
92
|
+
/**
|
|
93
|
+
* Apply a theme color the user picked. The app owns this because a theme
|
|
94
|
+
* color is app state, not devtools state: the panel changes nothing on its
|
|
95
|
+
* own, it hands the choice back.
|
|
96
|
+
*/
|
|
97
|
+
onThemeColorChange?: (color: string) => void;
|
|
73
98
|
}
|
|
74
99
|
|
|
75
100
|
export interface KeycloakConfig {
|
|
@@ -8,6 +8,7 @@ import { config, isDev, platform } from "@multiplatform.one/platform";
|
|
|
8
8
|
import { createI18nConfig } from "@multiplatform.one/i18n";
|
|
9
9
|
import { logger } from "@multiplatform.one/logger";
|
|
10
10
|
import { storage } from "@multiplatform.one/store";
|
|
11
|
+
import { RuntimePublicConfigScript } from "./RuntimePublicConfigScript";
|
|
11
12
|
import type { CreateRootLayoutConfig, RootLoaderData } from "./rootLayoutShared";
|
|
12
13
|
|
|
13
14
|
export type {
|
|
@@ -142,6 +143,14 @@ export function createRootLayout(cfg: CreateRootLayoutConfig): ComponentType {
|
|
|
142
143
|
<html lang={htmlLang}>
|
|
143
144
|
<head>
|
|
144
145
|
<meta charSet="utf-8" />
|
|
146
|
+
{/* RUNTIME public config, first thing after charset so it is parsed
|
|
147
|
+
and executed before the deferred app bundle constructs Config.
|
|
148
|
+
Without it the browser has NO path to config at all: Config's env
|
|
149
|
+
lookup reads process.env, which does not exist there, so a
|
|
150
|
+
container env var (MARKETPLACE_API_URL, FRAPPE_ENABLED, …) could
|
|
151
|
+
never reach the SPA and the image had to be rebuilt per
|
|
152
|
+
environment. See RuntimePublicConfigScript. */}
|
|
153
|
+
<RuntimePublicConfigScript />
|
|
145
154
|
{interFonts && (
|
|
146
155
|
<>
|
|
147
156
|
<link
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { runtimePublicConfigKey } from "@multiplatform.one/platform";
|
|
5
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
6
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
import { RuntimePublicConfigScript } from "./RuntimePublicConfigScript";
|
|
8
|
+
|
|
9
|
+
// vitest runs this suite in the node environment, so `window` is undefined and
|
|
10
|
+
// `platform.isServer` is true — i.e. every render below is the SSR branch, the
|
|
11
|
+
// one that actually writes the served document.
|
|
12
|
+
function renderDocument(): string {
|
|
13
|
+
return renderToStaticMarkup(<RuntimePublicConfigScript />);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
vi.unstubAllEnvs();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("RuntimePublicConfigScript", () => {
|
|
21
|
+
it("emits the public payload into the SSR document", () => {
|
|
22
|
+
vi.stubEnv("VITE_MP_PUBLIC_CONFIG_KEYS", JSON.stringify(["MARKETPLACE_API_URL"]));
|
|
23
|
+
vi.stubEnv("MARKETPLACE_API_URL", "https://api.marketplace.example.org");
|
|
24
|
+
const html = renderDocument();
|
|
25
|
+
// The whole point of the seam: the VALUE the SSR process holds in its own
|
|
26
|
+
// env at request time is in the bytes the browser receives. Nothing about
|
|
27
|
+
// this depends on what the image was BUILT with.
|
|
28
|
+
expect(html).toContain("<script");
|
|
29
|
+
expect(html).toContain(runtimePublicConfigKey);
|
|
30
|
+
expect(html).toContain("https://api.marketplace.example.org");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("emits FRAPPE_ENABLED so the frappe provider can mount in the browser", () => {
|
|
34
|
+
// CreateApp.tsx: `cfg.enabled ?? config.get("FRAPPE_ENABLED") === "1"`.
|
|
35
|
+
vi.stubEnv("VITE_MP_PUBLIC_CONFIG_KEYS", JSON.stringify(["FRAPPE_ENABLED"]));
|
|
36
|
+
vi.stubEnv("FRAPPE_ENABLED", "1");
|
|
37
|
+
expect(renderDocument()).toContain('"FRAPPE_ENABLED":"1"');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("keeps a private key out of the SSR document", () => {
|
|
41
|
+
vi.stubEnv("VITE_MP_PUBLIC_CONFIG_KEYS", JSON.stringify(["MARKETPLACE_API_URL"]));
|
|
42
|
+
vi.stubEnv("MARKETPLACE_API_URL", "https://api.marketplace.example.org");
|
|
43
|
+
vi.stubEnv("SECRET", "hunter2");
|
|
44
|
+
vi.stubEnv("DATABASE_PASSWORD", "hunter2");
|
|
45
|
+
const html = renderDocument();
|
|
46
|
+
expect(html).not.toContain("SECRET");
|
|
47
|
+
expect(html).not.toContain("DATABASE_PASSWORD");
|
|
48
|
+
expect(html).not.toContain("hunter2");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("cannot be closed out of by a value that looks like markup", () => {
|
|
52
|
+
vi.stubEnv("VITE_MP_PUBLIC_CONFIG_KEYS", JSON.stringify(["BASE_URL"]));
|
|
53
|
+
vi.stubEnv("BASE_URL", "</script><script>alert(1)</script>");
|
|
54
|
+
const html = renderDocument();
|
|
55
|
+
expect(html).not.toContain("</script><script>");
|
|
56
|
+
expect(html).toContain("\\u003c");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("createRootLayout (web)", () => {
|
|
61
|
+
it("mounts RuntimePublicConfigScript in the document head", () => {
|
|
62
|
+
// The component only reaches a browser if the root layout renders it, and
|
|
63
|
+
// the root layout is a `one` route module that cannot be instantiated
|
|
64
|
+
// outside a router — so the mount is pinned at the source level. Delete
|
|
65
|
+
// the element from <head> and this reds.
|
|
66
|
+
const source = readFileSync(
|
|
67
|
+
join(dirname(fileURLToPath(import.meta.url)), "CreateRootLayout.web.tsx"),
|
|
68
|
+
"utf-8",
|
|
69
|
+
);
|
|
70
|
+
const head = source.slice(source.indexOf("<head>"), source.indexOf("</head>"));
|
|
71
|
+
expect(head).toContain("<RuntimePublicConfigScript />");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
platform,
|
|
3
|
+
runtimePublicConfigKey,
|
|
4
|
+
serializeRuntimePublicConfig,
|
|
5
|
+
} from "@multiplatform.one/platform";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Publishes the SSR-resolved PUBLIC config into the served document.
|
|
9
|
+
*
|
|
10
|
+
* Mounted in `<head>` by `createRootLayout` (web). Deliberately the SAME shape
|
|
11
|
+
* one uses for `__one_server_context__` — an inline `<script>` assigning a
|
|
12
|
+
* `globalThis` key, `async` + `href` so React 19 keys it stably, and
|
|
13
|
+
* `suppressHydrationWarning` because the client renders an EMPTY body for the
|
|
14
|
+
* already-executed script (one's `server/ServerContextScript` does exactly
|
|
15
|
+
* this). It is a sibling payload on the established seam, not a second
|
|
16
|
+
* transport.
|
|
17
|
+
*
|
|
18
|
+
* XSS: the injected string is NOT user content. It is built by
|
|
19
|
+
* `serializeRuntimePublicConfig`, which (a) walks the BUILD-TIME public key
|
|
20
|
+
* allowlist rather than any request-derived input, and (b) emits the values
|
|
21
|
+
* through a JSON serializer that escapes `<`, `>`, U+2028 and U+2029 — so the
|
|
22
|
+
* body cannot close its own `</script>` tag or terminate a JS statement. This
|
|
23
|
+
* is the same sanitizer shape one ships as `utils/htmlEscape.safeJsonStringify`
|
|
24
|
+
* for the server-context payload. React offers no other way to emit a script
|
|
25
|
+
* body: text children of `<script>` are HTML-escaped, which would corrupt the
|
|
26
|
+
* JSON quoting.
|
|
27
|
+
*
|
|
28
|
+
* Ordering: an inline classic script in `<head>` runs during document parse,
|
|
29
|
+
* while the app bundle is a deferred module script — so `globalThis` already
|
|
30
|
+
* carries the payload by the time `@multiplatform.one/platform` constructs its
|
|
31
|
+
* `Config`.
|
|
32
|
+
*
|
|
33
|
+
* On the browser the body is empty: `serializeRuntimePublicConfig()` reads
|
|
34
|
+
* `process.env`, which only exists on the server. Rendering it client-side
|
|
35
|
+
* would publish an EMPTY payload over the real one.
|
|
36
|
+
*/
|
|
37
|
+
export function RuntimePublicConfigScript() {
|
|
38
|
+
return (
|
|
39
|
+
<script
|
|
40
|
+
async
|
|
41
|
+
// @ts-expect-error `href` is not on React's ScriptHTMLAttributes — React 19
|
|
42
|
+
// keys hoisted scripts by it anyway, and `one`'s ServerContextScript (the
|
|
43
|
+
// sibling payload this deliberately mirrors) carries the identical
|
|
44
|
+
// suppression on the identical line. Copying the transport means copying
|
|
45
|
+
// this, not inventing a differently-keyed script.
|
|
46
|
+
href={runtimePublicConfigKey}
|
|
47
|
+
suppressHydrationWarning
|
|
48
|
+
dangerouslySetInnerHTML={{
|
|
49
|
+
__html: platform.isServer ? serializeRuntimePublicConfig() : "",
|
|
50
|
+
}}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
RuntimePublicConfigScript.displayName = "RuntimePublicConfigScript";
|
|
@@ -1,15 +1,36 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
|
|
2
|
+
// The `/production` subpaths carry the same implementations the main
|
|
3
|
+
// entries carry in development, but WITHOUT the NODE_ENV !== "development"
|
|
4
|
+
// no-op stubs the main entries dead-code themselves into in a production
|
|
5
|
+
// build. This whole file is lazy-loaded and gated on `tanstackConfig.debug`,
|
|
6
|
+
// so importing the real panels unconditionally is what lets a deployed
|
|
7
|
+
// build that opts in (debug: true) actually show devtools.
|
|
8
|
+
import { ReactQueryDevtools } from "@tanstack/react-query-devtools/production";
|
|
3
9
|
import { TanStackDevtools } from "@tanstack/react-devtools";
|
|
4
|
-
import { formDevtoolsPlugin } from "@tanstack/react-form-devtools";
|
|
5
|
-
import {
|
|
10
|
+
import { formDevtoolsPlugin } from "@tanstack/react-form-devtools/production";
|
|
11
|
+
import { createReactPanel, createReactPlugin } from "@tanstack/devtools-utils/react";
|
|
12
|
+
import { PacerDevtoolsCore } from "@tanstack/pacer-devtools/production";
|
|
6
13
|
import { ThemeDevtoolsPanel } from "@multiplatform.one/theme";
|
|
7
14
|
import { storage } from "@multiplatform.one/store";
|
|
8
15
|
import { useEffect, useState } from "react";
|
|
9
16
|
import type { TanstackConfig } from "./CreateApp";
|
|
17
|
+
import { devtoolsOpenHotkey, useShakeToOpen } from "./devtoolsTrigger";
|
|
10
18
|
|
|
11
19
|
const devtoolsSettingsKey = "tanstack_devtools_settings";
|
|
12
20
|
|
|
21
|
+
// @tanstack/react-pacer-devtools' /production entry is broken upstream
|
|
22
|
+
// (0.7.x): it re-exports its panel from ReactPacerDevtools.js, which pulls
|
|
23
|
+
// PacerDevtoolsCore from @tanstack/pacer-devtools' MAIN entry — the
|
|
24
|
+
// NODE_ENV-gated one — so its "production" plugin silently mounts the no-op
|
|
25
|
+
// core in a deployed bundle. Rebuild the plugin from the genuinely ungated
|
|
26
|
+
// /production core instead. devtoolsTrigger.spec.ts pins the resolved core
|
|
27
|
+
// as real (its unmount throws; the NoOp's is silent).
|
|
28
|
+
const [PacerDevtoolsPanel] = createReactPanel(PacerDevtoolsCore);
|
|
29
|
+
const [pacerDevtoolsPlugin] = createReactPlugin({
|
|
30
|
+
name: "TanStack Pacer",
|
|
31
|
+
Component: PacerDevtoolsPanel,
|
|
32
|
+
});
|
|
33
|
+
|
|
13
34
|
/**
|
|
14
35
|
* Sync the resolved color scheme into the TanStack Devtools settings
|
|
15
36
|
* stored via the shared storage adapter. The devtools shell reads this on mount and
|
|
@@ -36,7 +57,7 @@ export interface TanstackDevtoolsProps {
|
|
|
36
57
|
/**
|
|
37
58
|
* Lazily-loaded devtools shell. All heavy devtools dependencies
|
|
38
59
|
* (`@tanstack/react-devtools`, `@tanstack/react-query-devtools`,
|
|
39
|
-
* `@tanstack/react-form-devtools`, `@tanstack/
|
|
60
|
+
* `@tanstack/react-form-devtools`, `@tanstack/pacer-devtools`,
|
|
40
61
|
* and `ThemeDevtoolsPanel`) live here so they are code-split away
|
|
41
62
|
* from the production bundle.
|
|
42
63
|
*/
|
|
@@ -108,6 +129,11 @@ export default function TanstackDevtoolsWrapper({
|
|
|
108
129
|
|
|
109
130
|
const frappePlugin = useFrappePlugin();
|
|
110
131
|
|
|
132
|
+
// Shake-to-open (devicemotion) — the phone-friendly counterpart to the
|
|
133
|
+
// keyboard shortcut. Ships whenever the devtools mount unless the app
|
|
134
|
+
// opts out with `tanstack: { shakeToOpen: false }`.
|
|
135
|
+
useShakeToOpen(tanstackConfig.shakeToOpen !== false);
|
|
136
|
+
|
|
111
137
|
const plugins = [
|
|
112
138
|
{
|
|
113
139
|
id: "react-query",
|
|
@@ -119,11 +145,29 @@ export default function TanstackDevtoolsWrapper({
|
|
|
119
145
|
{
|
|
120
146
|
id: "theme",
|
|
121
147
|
name: "Theme",
|
|
148
|
+
// The panel opens focused on Theme, so a shake (or the hotkey) lands
|
|
149
|
+
// straight on the theme flipper — the whole point of the trigger is
|
|
150
|
+
// to check that everything is themable. `defaultOpen` seeds the tab
|
|
151
|
+
// only on first open, when no layout is persisted yet; once the user
|
|
152
|
+
// moves to another tab their choice is restored on later opens.
|
|
153
|
+
defaultOpen: true,
|
|
122
154
|
render: (_el: HTMLElement, props: { theme: "dark" | "light" }) => (
|
|
123
155
|
<ThemeDevtoolsPanel
|
|
124
156
|
useUserScheme={tanstackConfig.useUserScheme}
|
|
125
157
|
devtoolsTheme={props.theme}
|
|
126
158
|
tamaguiConfig={tanstackConfig.tamaguiConfig}
|
|
159
|
+
// The accent channel. `ThemeDevtoolsPanel` has taken these three
|
|
160
|
+
// props all along and core passed none of them, so the panel's
|
|
161
|
+
// accent dropdown never rendered and its Randomize action skipped
|
|
162
|
+
// the colour — a control that is present in the component and
|
|
163
|
+
// unreachable through the only wrapper that mounts it. They are
|
|
164
|
+
// forwarded, not invented here: a theme colour is app state, so the
|
|
165
|
+
// app supplies the list, the current value and the setter, exactly
|
|
166
|
+
// as it already supplies `useUserScheme`. Undefined stays undefined,
|
|
167
|
+
// so an app that wires nothing sees the panel it sees today.
|
|
168
|
+
themeColors={tanstackConfig.themeColors}
|
|
169
|
+
currentThemeColor={tanstackConfig.currentThemeColor}
|
|
170
|
+
onThemeColorChange={tanstackConfig.onThemeColorChange}
|
|
127
171
|
/>
|
|
128
172
|
),
|
|
129
173
|
},
|
|
@@ -136,7 +180,7 @@ export default function TanstackDevtoolsWrapper({
|
|
|
136
180
|
config={{
|
|
137
181
|
defaultOpen: false,
|
|
138
182
|
triggerHidden: true,
|
|
139
|
-
openHotkey: [
|
|
183
|
+
openHotkey: [...devtoolsOpenHotkey],
|
|
140
184
|
theme: resolvedScheme,
|
|
141
185
|
}}
|
|
142
186
|
plugins={plugins}
|