@multiplatform.one/core 7.1.0 → 7.2.1
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 +14 -13
- package/src/app/CreateApp.tsx +25 -0
- package/src/app/TanstackDevtools.tsx +52 -5
- package/src/app/devtoolsTrigger.spec.ts +560 -0
- package/src/app/devtoolsTrigger.ts +429 -0
- package/types/app/CreateApp.d.ts +25 -0
- package/types/app/CreateApp.d.ts.map +1 -1
- package/types/app/TanstackDevtools.d.ts.map +1 -1
- package/types/app/devtoolsTrigger.d.ts +179 -0
- package/types/app/devtoolsTrigger.d.ts.map +1 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { EventClient } from "@tanstack/devtools-event-client";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Keyboard trigger
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Keyboard shortcut that opens the devtools shell: Cmd+Shift+` on Mac,
|
|
10
|
+
* Ctrl+Shift+` elsewhere.
|
|
11
|
+
*
|
|
12
|
+
* Why this combo, and why `~`: unshifted Ctrl/Cmd+` is macOS "cycle
|
|
13
|
+
* windows" and never reaches the page. Shift+Backquote is the physical
|
|
14
|
+
* key the ticket names. `@tanstack/react-devtools` matches an exact
|
|
15
|
+
* `e.key` sequence via `@solid-primitives/keyboard`, and Shift+Backquote
|
|
16
|
+
* produces `e.key === "~"` on US layouts — so the TanStack config must
|
|
17
|
+
* be Shift + "~", not unshifted "`". The capture-phase listener in
|
|
18
|
+
* `useDevtoolsKeyboardTrigger` matches `e.code === "Backquote"` so
|
|
19
|
+
* non-US layouts (where Shift+Backquote is ¬ or `) still fire.
|
|
20
|
+
*
|
|
21
|
+
* Exported so tests pin the binding.
|
|
22
|
+
*/
|
|
23
|
+
export const devtoolsOpenHotkey = ["CtrlOrMeta", "Shift", "~"] as const;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* True when the event is the documented debugger shortcut. Prefers the
|
|
27
|
+
* physical Backquote key so it does not depend on keyboard layout;
|
|
28
|
+
* `e.key` of "`" or "~" is accepted as a fallback when `code` is absent
|
|
29
|
+
* (jsdom / synthetic events).
|
|
30
|
+
*/
|
|
31
|
+
export function isDevtoolsOpenHotkeyEvent(event: KeyboardEvent): boolean {
|
|
32
|
+
if (!(event.metaKey || event.ctrlKey) || !event.shiftKey || event.altKey) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (event.code === "Backquote") return true;
|
|
36
|
+
return event.key === "`" || event.key === "~";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Capture-phase window listener that opens the shell on the documented
|
|
41
|
+
* shortcut. Capture runs before app-level keydown handlers, so a page
|
|
42
|
+
* that stopPropagation's cannot swallow it. preventDefault +
|
|
43
|
+
* stopImmediatePropagation keep TanStack's own createShortcut from
|
|
44
|
+
* also firing (which would toggle the panel shut again).
|
|
45
|
+
*/
|
|
46
|
+
export function useDevtoolsKeyboardTrigger(enabled: boolean) {
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!enabled || typeof window === "undefined") return;
|
|
49
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
50
|
+
if (!isDevtoolsOpenHotkeyEvent(event)) return;
|
|
51
|
+
event.preventDefault();
|
|
52
|
+
event.stopImmediatePropagation();
|
|
53
|
+
openDevtoolsShell();
|
|
54
|
+
};
|
|
55
|
+
window.addEventListener("keydown", onKeyDown, { capture: true });
|
|
56
|
+
return () => window.removeEventListener("keydown", onKeyDown, { capture: true });
|
|
57
|
+
}, [enabled]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Programmatic open — the same event the devtools shell's own trigger emits
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Plugin id of the TanStack Devtools shell's internal event client
|
|
66
|
+
* (`devtoolsEventClient` in `@tanstack/devtools-client`). The shell
|
|
67
|
+
* subscribes to `<pluginId>:trigger-toggled` and opens/closes on the
|
|
68
|
+
* payload's `isOpen`. The spec asserts both strings
|
|
69
|
+
* against the installed packages so an upstream rename fails the build
|
|
70
|
+
* instead of silently breaking shake-to-open.
|
|
71
|
+
*/
|
|
72
|
+
export const devtoolsShellPluginId = "tanstack-devtools-core";
|
|
73
|
+
|
|
74
|
+
/** Event suffix the shell listens on for open/close state changes. */
|
|
75
|
+
export const devtoolsTriggerToggledEvent = "trigger-toggled";
|
|
76
|
+
|
|
77
|
+
interface DevtoolsShellEventMap {
|
|
78
|
+
"trigger-toggled": { isOpen: boolean };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let shellEventClient: EventClient<DevtoolsShellEventMap> | undefined;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Open the TanStack Devtools shell programmatically by emitting the same
|
|
85
|
+
* bus event the shell's own (hidden) trigger button uses. Open-only by
|
|
86
|
+
* design: a continued shake must not immediately re-close the panel —
|
|
87
|
+
* closing stays on the close button / Escape / the keyboard shortcut.
|
|
88
|
+
* The event client queues emits until the bus connects, so an early shake
|
|
89
|
+
* racing the shell mount is still delivered.
|
|
90
|
+
*/
|
|
91
|
+
export function openDevtoolsShell() {
|
|
92
|
+
shellEventClient ??= new EventClient<DevtoolsShellEventMap>({
|
|
93
|
+
pluginId: devtoolsShellPluginId,
|
|
94
|
+
});
|
|
95
|
+
shellEventClient.emit(devtoolsTriggerToggledEvent, { isOpen: true });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Shake detection — pure state machine over devicemotion samples
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
const gravityMs2 = 9.81;
|
|
103
|
+
|
|
104
|
+
/** Defaults: deliberate shakes peak 15–25 m/s²; walking/bumps stay under ~8. */
|
|
105
|
+
export const shakeDefaults = {
|
|
106
|
+
/** m/s² a sample must exceed to count as a shake peak. */
|
|
107
|
+
threshold: 12,
|
|
108
|
+
/** Minimum ms between counted peaks (one swing = one peak, not many samples). */
|
|
109
|
+
minPeakGapMs: 80,
|
|
110
|
+
/** Peaks required inside the rolling window to fire. */
|
|
111
|
+
peakCount: 3,
|
|
112
|
+
/** Rolling window (ms) the peaks must land in. */
|
|
113
|
+
windowMs: 1000,
|
|
114
|
+
/** Dead time (ms) after firing before the detector re-arms. */
|
|
115
|
+
cooldownMs: 2000,
|
|
116
|
+
} as const;
|
|
117
|
+
|
|
118
|
+
interface MotionAxes {
|
|
119
|
+
x?: number | null;
|
|
120
|
+
y?: number | null;
|
|
121
|
+
z?: number | null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Structural subset of DeviceMotionEvent the detector needs (testable in node). */
|
|
125
|
+
export interface MotionEventLike {
|
|
126
|
+
acceleration?: MotionAxes | null;
|
|
127
|
+
accelerationIncludingGravity?: MotionAxes | null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Acceleration magnitude (m/s²) of a devicemotion sample. Prefers the
|
|
132
|
+
* gravity-excluded reading; falls back to the including-gravity reading
|
|
133
|
+
* minus standard gravity. Returns null when the event carries no data
|
|
134
|
+
* (some desktop browsers fire devicemotion with all-null axes).
|
|
135
|
+
*/
|
|
136
|
+
export function motionMagnitude(event: MotionEventLike): number | null {
|
|
137
|
+
const a = event.acceleration;
|
|
138
|
+
if (a && (a.x != null || a.y != null || a.z != null)) {
|
|
139
|
+
return Math.hypot(a.x ?? 0, a.y ?? 0, a.z ?? 0);
|
|
140
|
+
}
|
|
141
|
+
const g = event.accelerationIncludingGravity;
|
|
142
|
+
if (g && (g.x != null || g.y != null || g.z != null)) {
|
|
143
|
+
return Math.abs(Math.hypot(g.x ?? 0, g.y ?? 0, g.z ?? 0) - gravityMs2);
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface ShakeDetectorOptions {
|
|
149
|
+
onShake: () => void;
|
|
150
|
+
threshold?: number;
|
|
151
|
+
minPeakGapMs?: number;
|
|
152
|
+
peakCount?: number;
|
|
153
|
+
windowMs?: number;
|
|
154
|
+
cooldownMs?: number;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface ShakeDetector {
|
|
158
|
+
/** Feed a devicemotion sample. `now` is injectable for tests. */
|
|
159
|
+
handleMotion: (event: MotionEventLike, now?: number) => void;
|
|
160
|
+
/** Drop all peaks and cooldown state. */
|
|
161
|
+
reset: () => void;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Debounced shake detector: fires `onShake` when `peakCount` peaks above
|
|
166
|
+
* `threshold` (each at least `minPeakGapMs` apart) land inside a rolling
|
|
167
|
+
* `windowMs`, then goes dead for `cooldownMs` before re-arming.
|
|
168
|
+
*/
|
|
169
|
+
export function createShakeDetector({
|
|
170
|
+
onShake,
|
|
171
|
+
threshold = shakeDefaults.threshold,
|
|
172
|
+
minPeakGapMs = shakeDefaults.minPeakGapMs,
|
|
173
|
+
peakCount = shakeDefaults.peakCount,
|
|
174
|
+
windowMs = shakeDefaults.windowMs,
|
|
175
|
+
cooldownMs = shakeDefaults.cooldownMs,
|
|
176
|
+
}: ShakeDetectorOptions): ShakeDetector {
|
|
177
|
+
let peaks: number[] = [];
|
|
178
|
+
let lastPeakAt = Number.NEGATIVE_INFINITY;
|
|
179
|
+
let cooldownUntil = Number.NEGATIVE_INFINITY;
|
|
180
|
+
|
|
181
|
+
function reset() {
|
|
182
|
+
peaks = [];
|
|
183
|
+
lastPeakAt = Number.NEGATIVE_INFINITY;
|
|
184
|
+
cooldownUntil = Number.NEGATIVE_INFINITY;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function handleMotion(event: MotionEventLike, now: number = Date.now()) {
|
|
188
|
+
if (now < cooldownUntil) return;
|
|
189
|
+
const magnitude = motionMagnitude(event);
|
|
190
|
+
if (magnitude == null || magnitude <= threshold) return;
|
|
191
|
+
if (now - lastPeakAt < minPeakGapMs) return;
|
|
192
|
+
lastPeakAt = now;
|
|
193
|
+
peaks = peaks.filter((at) => now - at <= windowMs);
|
|
194
|
+
peaks.push(now);
|
|
195
|
+
if (peaks.length < peakCount) return;
|
|
196
|
+
peaks = [];
|
|
197
|
+
lastPeakAt = Number.NEGATIVE_INFINITY;
|
|
198
|
+
cooldownUntil = now + cooldownMs;
|
|
199
|
+
onShake();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return { handleMotion, reset };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// Motion permission — iOS 13+ WebKit gesture-gated grant flow
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
export type MotionPermissionState = "granted" | "awaiting-gesture" | "requesting" | "denied";
|
|
210
|
+
|
|
211
|
+
export interface MotionPermissionFlow {
|
|
212
|
+
readonly state: MotionPermissionState;
|
|
213
|
+
/**
|
|
214
|
+
* Call from a user gesture (pointerup/click). Resolves once the
|
|
215
|
+
* permission settles; no-ops unless state is "awaiting-gesture".
|
|
216
|
+
*/
|
|
217
|
+
handleGesture: () => Promise<void>;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Permission state machine. Platforms without
|
|
222
|
+
* `DeviceMotionEvent.requestPermission` (Android, desktop) grant
|
|
223
|
+
* immediately; iOS WebKit waits for a user gesture, asks once, and lands
|
|
224
|
+
* on granted or denied. A denial is terminal for this page load — Safari
|
|
225
|
+
* remembers the answer per site, so later loads resolve silently.
|
|
226
|
+
*/
|
|
227
|
+
export function createMotionPermissionFlow({
|
|
228
|
+
requestPermission,
|
|
229
|
+
onGranted,
|
|
230
|
+
onDenied,
|
|
231
|
+
}: {
|
|
232
|
+
requestPermission?: () => Promise<string>;
|
|
233
|
+
onGranted: () => void;
|
|
234
|
+
onDenied?: () => void;
|
|
235
|
+
}): MotionPermissionFlow {
|
|
236
|
+
let state: MotionPermissionState = requestPermission ? "awaiting-gesture" : "granted";
|
|
237
|
+
if (state === "granted") onGranted();
|
|
238
|
+
|
|
239
|
+
async function handleGesture() {
|
|
240
|
+
if (state !== "awaiting-gesture" || !requestPermission) return;
|
|
241
|
+
state = "requesting";
|
|
242
|
+
let result: string;
|
|
243
|
+
try {
|
|
244
|
+
result = await requestPermission();
|
|
245
|
+
} catch {
|
|
246
|
+
// requestPermission rejects when called outside a user gesture or in
|
|
247
|
+
// an insecure context — treat as denied for this load.
|
|
248
|
+
result = "denied";
|
|
249
|
+
}
|
|
250
|
+
if (result === "granted") {
|
|
251
|
+
state = "granted";
|
|
252
|
+
onGranted();
|
|
253
|
+
} else {
|
|
254
|
+
state = "denied";
|
|
255
|
+
onDenied?.();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
get state() {
|
|
261
|
+
return state;
|
|
262
|
+
},
|
|
263
|
+
handleGesture,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// Motion arming — a DELIBERATE, app-driven grant (no arbitrary tap grab)
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
interface DeviceMotionEventConstructorLike {
|
|
272
|
+
requestPermission?: () => Promise<string>;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function motionEventCtor(): DeviceMotionEventConstructorLike | undefined {
|
|
276
|
+
return (globalThis as { DeviceMotionEvent?: DeviceMotionEventConstructorLike }).DeviceMotionEvent;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* True only on the platform that gates devicemotion behind a user-gesture
|
|
281
|
+
* grant: iOS 13+ WebKit exposes `DeviceMotionEvent.requestPermission`.
|
|
282
|
+
* Android and desktop return false — they deliver motion events without a
|
|
283
|
+
* prompt, so shake-to-open arms itself there and no trigger UI is needed.
|
|
284
|
+
*/
|
|
285
|
+
export function isMotionPermissionRequired(): boolean {
|
|
286
|
+
return typeof motionEventCtor()?.requestPermission === "function";
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The window event an app dispatches from a DELIBERATE user gesture to arm
|
|
291
|
+
* shake-to-open on iOS (e.g. the SHC console's triple-tap on the build
|
|
292
|
+
* number in Settings › Estate). It is a window CustomEvent rather than an
|
|
293
|
+
* imported function on purpose: the trigger lives in a feature package that
|
|
294
|
+
* does not — and must not — depend on this core build. `dispatchEvent` is
|
|
295
|
+
* synchronous, so the `requestPermission()` this fires runs INSIDE the
|
|
296
|
+
* originating gesture's call stack, which is the whole reason iOS accepts
|
|
297
|
+
* it. `armDevtoolsMotion()` dispatches it for callers that CAN import core.
|
|
298
|
+
*/
|
|
299
|
+
export const devtoolsArmMotionEvent = "mpo:devtools-arm-motion";
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* The window event core dispatches back once an arm attempt settles, so a
|
|
303
|
+
* trigger UI can report the honest outcome. `detail.granted` is true only
|
|
304
|
+
* when motion is now live.
|
|
305
|
+
*/
|
|
306
|
+
export const devtoolsMotionArmedEvent = "mpo:devtools-motion-armed";
|
|
307
|
+
|
|
308
|
+
// The grant is a one-shot module latch plus a set of attachers (one per
|
|
309
|
+
// mounted `useShakeToOpen`). Once granted, a shell that mounts LATER — a
|
|
310
|
+
// colour-scheme change remounts it under a new key — attaches immediately
|
|
311
|
+
// instead of waiting for a second arm gesture.
|
|
312
|
+
let motionGranted = false;
|
|
313
|
+
const motionAttachers = new Set<() => void>();
|
|
314
|
+
|
|
315
|
+
function registerMotionAttacher(attach: () => void): () => void {
|
|
316
|
+
if (motionGranted) {
|
|
317
|
+
attach();
|
|
318
|
+
return () => {};
|
|
319
|
+
}
|
|
320
|
+
motionAttachers.add(attach);
|
|
321
|
+
return () => {
|
|
322
|
+
motionAttachers.delete(attach);
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function grantAndAttachAll() {
|
|
327
|
+
motionGranted = true;
|
|
328
|
+
// Safe to iterate directly: `attach` adds a devicemotion listener and
|
|
329
|
+
// never mutates the set, and a shell mounting mid-grant sees
|
|
330
|
+
// motionGranted === true and attaches without registering.
|
|
331
|
+
for (const attach of motionAttachers) attach();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Test seam: forget the grant and drop attachers between cases. */
|
|
335
|
+
export function __resetMotionArming() {
|
|
336
|
+
motionGranted = false;
|
|
337
|
+
motionAttachers.clear();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Request motion permission from within a user gesture and, on grant,
|
|
342
|
+
* attach every waiting shake listener. Idempotent once granted. Resolves
|
|
343
|
+
* to the settled permission state; a denial is terminal for this page load
|
|
344
|
+
* (Safari remembers it per site). Broadcast the outcome on
|
|
345
|
+
* `devtoolsMotionArmedEvent` so a trigger UI can report it.
|
|
346
|
+
*/
|
|
347
|
+
export async function requestMotionPermissionFromGesture(): Promise<MotionPermissionState> {
|
|
348
|
+
if (motionGranted) return "granted";
|
|
349
|
+
const ctor = motionEventCtor();
|
|
350
|
+
const flow = createMotionPermissionFlow({
|
|
351
|
+
// Invoke as a static method on the constructor — a detached reference
|
|
352
|
+
// throws in WebKit.
|
|
353
|
+
requestPermission:
|
|
354
|
+
typeof ctor?.requestPermission === "function" ? () => ctor.requestPermission!() : undefined,
|
|
355
|
+
onGranted: grantAndAttachAll,
|
|
356
|
+
});
|
|
357
|
+
// Non-iOS: the flow granted (and attached) synchronously in its
|
|
358
|
+
// constructor — nothing to await.
|
|
359
|
+
if (flow.state !== "granted") await flow.handleGesture();
|
|
360
|
+
if (typeof window !== "undefined") {
|
|
361
|
+
window.dispatchEvent(
|
|
362
|
+
new CustomEvent(devtoolsMotionArmedEvent, {
|
|
363
|
+
detail: { granted: flow.state === "granted" },
|
|
364
|
+
}),
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
return flow.state;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Arm shake-to-open programmatically. For callers that CAN import core
|
|
372
|
+
* (the app layer); a feature package instead dispatches
|
|
373
|
+
* `devtoolsArmMotionEvent` directly. Must be called from a user gesture on
|
|
374
|
+
* iOS. No-op on the server.
|
|
375
|
+
*/
|
|
376
|
+
export function armDevtoolsMotion(): void {
|
|
377
|
+
if (typeof window === "undefined") return;
|
|
378
|
+
window.dispatchEvent(new Event(devtoolsArmMotionEvent));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ---------------------------------------------------------------------------
|
|
382
|
+
// React wiring
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Shake-to-open for the devtools shell. Attaches a `devicemotion` listener
|
|
387
|
+
* and opens the shell on a detected shake. Desktop browsers deliver no
|
|
388
|
+
* devicemotion events, so the listener is inert there — the keyboard
|
|
389
|
+
* shortcut is the desktop path.
|
|
390
|
+
*
|
|
391
|
+
* iOS 13+ gates devicemotion behind a permission prompt that can ONLY be
|
|
392
|
+
* requested from a user gesture, and the first shake cannot self-grant. So
|
|
393
|
+
* on iOS the listener attaches only after the app arms it — by dispatching
|
|
394
|
+
* `devtoolsArmMotionEvent` from a deliberate gesture (the console wires the
|
|
395
|
+
* build-number triple-tap). Android/desktop attach immediately. SSR-safe;
|
|
396
|
+
* every listener is torn down on unmount.
|
|
397
|
+
*/
|
|
398
|
+
export function useShakeToOpen(enabled: boolean, onShake: () => void = openDevtoolsShell) {
|
|
399
|
+
useEffect(() => {
|
|
400
|
+
if (!enabled || typeof window === "undefined") return;
|
|
401
|
+
|
|
402
|
+
const detector = createShakeDetector({ onShake });
|
|
403
|
+
const motionListener = (event: Event) =>
|
|
404
|
+
detector.handleMotion(event as unknown as MotionEventLike);
|
|
405
|
+
const attach = () => window.addEventListener("devicemotion", motionListener, { passive: true });
|
|
406
|
+
const detach = () => window.removeEventListener("devicemotion", motionListener);
|
|
407
|
+
|
|
408
|
+
if (!isMotionPermissionRequired()) {
|
|
409
|
+
// Android / desktop: no grant needed — listen right away.
|
|
410
|
+
attach();
|
|
411
|
+
return detach;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// iOS: attach now if a prior deliberate arm already granted; otherwise
|
|
415
|
+
// wait for the app's arm event (dispatched from a user gesture, so the
|
|
416
|
+
// permission request it fires stays inside that gesture's stack).
|
|
417
|
+
const removeAttacher = registerMotionAttacher(attach);
|
|
418
|
+
const armListener = () => {
|
|
419
|
+
void requestMotionPermissionFromGesture();
|
|
420
|
+
};
|
|
421
|
+
window.addEventListener(devtoolsArmMotionEvent, armListener);
|
|
422
|
+
|
|
423
|
+
return () => {
|
|
424
|
+
detach();
|
|
425
|
+
removeAttacher();
|
|
426
|
+
window.removeEventListener(devtoolsArmMotionEvent, armListener);
|
|
427
|
+
};
|
|
428
|
+
}, [enabled, onShake]);
|
|
429
|
+
}
|
package/types/app/CreateApp.d.ts
CHANGED
|
@@ -15,6 +15,15 @@ export declare function followPageOriginForLoopback(configured: string): string;
|
|
|
15
15
|
export interface TanstackConfig {
|
|
16
16
|
/** Show React Query devtools (web only, ignored on native and in Storybook). */
|
|
17
17
|
debug?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Shake the device to open the devtools shell (web only; requires
|
|
20
|
+
* `debug`). Defaults to true whenever the devtools mount. On iOS 13+
|
|
21
|
+
* WebKit the motion permission is requested once from the user's first
|
|
22
|
+
* tap; Android and desktop need no permission (desktop simply never
|
|
23
|
+
* fires devicemotion — use Cmd/Ctrl+Shift+` there). Set false to
|
|
24
|
+
* keep the keyboard shortcut as the only trigger.
|
|
25
|
+
*/
|
|
26
|
+
shakeToOpen?: boolean;
|
|
18
27
|
/** Options forwarded to the QueryClient constructor. */
|
|
19
28
|
queryClientOptions?: Record<string, unknown>;
|
|
20
29
|
/**
|
|
@@ -33,6 +42,22 @@ export interface TanstackConfig {
|
|
|
33
42
|
* Passed automatically by `createApp()` from the theme config.
|
|
34
43
|
*/
|
|
35
44
|
tamaguiConfig?: unknown;
|
|
45
|
+
/**
|
|
46
|
+
* Theme COLOR names the devtools panel offers in its accent dropdown
|
|
47
|
+
* (e.g. `["", "blue", "green", "purple"]`; `""` is the base theme).
|
|
48
|
+
* The panel renders the dropdown only when this AND `onThemeColorChange`
|
|
49
|
+
* are both set, and only then does the panel's Randomize action touch the
|
|
50
|
+
* accent — otherwise the picker is inert.
|
|
51
|
+
*/
|
|
52
|
+
themeColors?: string[];
|
|
53
|
+
/** The theme color the app currently has applied (`""` for the base). */
|
|
54
|
+
currentThemeColor?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Apply a theme color the user picked. The app owns this because a theme
|
|
57
|
+
* color is app state, not devtools state: the panel changes nothing on its
|
|
58
|
+
* own, it hands the choice back.
|
|
59
|
+
*/
|
|
60
|
+
onThemeColorChange?: (color: string) => void;
|
|
36
61
|
}
|
|
37
62
|
export interface KeycloakConfig {
|
|
38
63
|
/** Force-enable or disable. When omitted, reads KEYCLOAK_ENABLED from env config. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CreateApp.d.ts","sourceRoot":"","sources":["../../src/app/CreateApp.tsx"],"names":[],"mappings":"AAAA,OAAc,EAKZ,KAAK,aAAa,EAClB,KAAK,iBAAiB,EAEvB,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAU5D,MAAM,MAAM,iBAAiB,GAAG,aAAa,CAAC,iBAAiB,CAAC,CAAC;AAIjE;;;;;;;;;GASG;AACH,wBAAgB,2BAA2B,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAWtE;AAMD,MAAM,WAAW,cAAc;IAC7B,gFAAgF;IAChF,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,wDAAwD;IACxD,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7C;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM;QACpB,OAAO,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAC;QACrC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC;QACxB,GAAG,EAAE,CAAC,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,KAAK,IAAI,CAAC;KAC/C,CAAC;IACF;;;;;OAKG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"CreateApp.d.ts","sourceRoot":"","sources":["../../src/app/CreateApp.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TanstackDevtools.d.ts","sourceRoot":"","sources":["../../src/app/TanstackDevtools.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"TanstackDevtools.d.ts","sourceRoot":"","sources":["../../src/app/TanstackDevtools.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAoClD,MAAM,WAAW,qBAAqB;IACpC,cAAc,EAAE,OAAO,GAAG,MAAM,CAAC;IACjC,cAAc,EAAE,cAAc,CAAC;CAChC;AAkED,MAAM,CAAC,OAAO,UAAU,uBAAuB,CAAC,EAC9C,cAAc,EACd,cAAc,GACf,EAAE,qBAAqB,2CAoEvB"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard shortcut that opens the devtools shell: Cmd+Shift+` on Mac,
|
|
3
|
+
* Ctrl+Shift+` elsewhere.
|
|
4
|
+
*
|
|
5
|
+
* Why this combo, and why `~`: unshifted Ctrl/Cmd+` is macOS "cycle
|
|
6
|
+
* windows" and never reaches the page. Shift+Backquote is the physical
|
|
7
|
+
* key the ticket names. `@tanstack/react-devtools` matches an exact
|
|
8
|
+
* `e.key` sequence via `@solid-primitives/keyboard`, and Shift+Backquote
|
|
9
|
+
* produces `e.key === "~"` on US layouts — so the TanStack config must
|
|
10
|
+
* be Shift + "~", not unshifted "`". The capture-phase listener in
|
|
11
|
+
* `useDevtoolsKeyboardTrigger` matches `e.code === "Backquote"` so
|
|
12
|
+
* non-US layouts (where Shift+Backquote is ¬ or `) still fire.
|
|
13
|
+
*
|
|
14
|
+
* Exported so tests pin the binding.
|
|
15
|
+
*/
|
|
16
|
+
export declare const devtoolsOpenHotkey: readonly ["CtrlOrMeta", "Shift", "~"];
|
|
17
|
+
/**
|
|
18
|
+
* True when the event is the documented debugger shortcut. Prefers the
|
|
19
|
+
* physical Backquote key so it does not depend on keyboard layout;
|
|
20
|
+
* `e.key` of "`" or "~" is accepted as a fallback when `code` is absent
|
|
21
|
+
* (jsdom / synthetic events).
|
|
22
|
+
*/
|
|
23
|
+
export declare function isDevtoolsOpenHotkeyEvent(event: KeyboardEvent): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Capture-phase window listener that opens the shell on the documented
|
|
26
|
+
* shortcut. Capture runs before app-level keydown handlers, so a page
|
|
27
|
+
* that stopPropagation's cannot swallow it. preventDefault +
|
|
28
|
+
* stopImmediatePropagation keep TanStack's own createShortcut from
|
|
29
|
+
* also firing (which would toggle the panel shut again).
|
|
30
|
+
*/
|
|
31
|
+
export declare function useDevtoolsKeyboardTrigger(enabled: boolean): void;
|
|
32
|
+
/**
|
|
33
|
+
* Plugin id of the TanStack Devtools shell's internal event client
|
|
34
|
+
* (`devtoolsEventClient` in `@tanstack/devtools-client`). The shell
|
|
35
|
+
* subscribes to `<pluginId>:trigger-toggled` and opens/closes on the
|
|
36
|
+
* payload's `isOpen`. The spec asserts both strings
|
|
37
|
+
* against the installed packages so an upstream rename fails the build
|
|
38
|
+
* instead of silently breaking shake-to-open.
|
|
39
|
+
*/
|
|
40
|
+
export declare const devtoolsShellPluginId = "tanstack-devtools-core";
|
|
41
|
+
/** Event suffix the shell listens on for open/close state changes. */
|
|
42
|
+
export declare const devtoolsTriggerToggledEvent = "trigger-toggled";
|
|
43
|
+
/**
|
|
44
|
+
* Open the TanStack Devtools shell programmatically by emitting the same
|
|
45
|
+
* bus event the shell's own (hidden) trigger button uses. Open-only by
|
|
46
|
+
* design: a continued shake must not immediately re-close the panel —
|
|
47
|
+
* closing stays on the close button / Escape / the keyboard shortcut.
|
|
48
|
+
* The event client queues emits until the bus connects, so an early shake
|
|
49
|
+
* racing the shell mount is still delivered.
|
|
50
|
+
*/
|
|
51
|
+
export declare function openDevtoolsShell(): void;
|
|
52
|
+
/** Defaults: deliberate shakes peak 15–25 m/s²; walking/bumps stay under ~8. */
|
|
53
|
+
export declare const shakeDefaults: {
|
|
54
|
+
/** m/s² a sample must exceed to count as a shake peak. */
|
|
55
|
+
readonly threshold: 12;
|
|
56
|
+
/** Minimum ms between counted peaks (one swing = one peak, not many samples). */
|
|
57
|
+
readonly minPeakGapMs: 80;
|
|
58
|
+
/** Peaks required inside the rolling window to fire. */
|
|
59
|
+
readonly peakCount: 3;
|
|
60
|
+
/** Rolling window (ms) the peaks must land in. */
|
|
61
|
+
readonly windowMs: 1000;
|
|
62
|
+
/** Dead time (ms) after firing before the detector re-arms. */
|
|
63
|
+
readonly cooldownMs: 2000;
|
|
64
|
+
};
|
|
65
|
+
interface MotionAxes {
|
|
66
|
+
x?: number | null;
|
|
67
|
+
y?: number | null;
|
|
68
|
+
z?: number | null;
|
|
69
|
+
}
|
|
70
|
+
/** Structural subset of DeviceMotionEvent the detector needs (testable in node). */
|
|
71
|
+
export interface MotionEventLike {
|
|
72
|
+
acceleration?: MotionAxes | null;
|
|
73
|
+
accelerationIncludingGravity?: MotionAxes | null;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Acceleration magnitude (m/s²) of a devicemotion sample. Prefers the
|
|
77
|
+
* gravity-excluded reading; falls back to the including-gravity reading
|
|
78
|
+
* minus standard gravity. Returns null when the event carries no data
|
|
79
|
+
* (some desktop browsers fire devicemotion with all-null axes).
|
|
80
|
+
*/
|
|
81
|
+
export declare function motionMagnitude(event: MotionEventLike): number | null;
|
|
82
|
+
export interface ShakeDetectorOptions {
|
|
83
|
+
onShake: () => void;
|
|
84
|
+
threshold?: number;
|
|
85
|
+
minPeakGapMs?: number;
|
|
86
|
+
peakCount?: number;
|
|
87
|
+
windowMs?: number;
|
|
88
|
+
cooldownMs?: number;
|
|
89
|
+
}
|
|
90
|
+
export interface ShakeDetector {
|
|
91
|
+
/** Feed a devicemotion sample. `now` is injectable for tests. */
|
|
92
|
+
handleMotion: (event: MotionEventLike, now?: number) => void;
|
|
93
|
+
/** Drop all peaks and cooldown state. */
|
|
94
|
+
reset: () => void;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Debounced shake detector: fires `onShake` when `peakCount` peaks above
|
|
98
|
+
* `threshold` (each at least `minPeakGapMs` apart) land inside a rolling
|
|
99
|
+
* `windowMs`, then goes dead for `cooldownMs` before re-arming.
|
|
100
|
+
*/
|
|
101
|
+
export declare function createShakeDetector({ onShake, threshold, minPeakGapMs, peakCount, windowMs, cooldownMs, }: ShakeDetectorOptions): ShakeDetector;
|
|
102
|
+
export type MotionPermissionState = "granted" | "awaiting-gesture" | "requesting" | "denied";
|
|
103
|
+
export interface MotionPermissionFlow {
|
|
104
|
+
readonly state: MotionPermissionState;
|
|
105
|
+
/**
|
|
106
|
+
* Call from a user gesture (pointerup/click). Resolves once the
|
|
107
|
+
* permission settles; no-ops unless state is "awaiting-gesture".
|
|
108
|
+
*/
|
|
109
|
+
handleGesture: () => Promise<void>;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Permission state machine. Platforms without
|
|
113
|
+
* `DeviceMotionEvent.requestPermission` (Android, desktop) grant
|
|
114
|
+
* immediately; iOS WebKit waits for a user gesture, asks once, and lands
|
|
115
|
+
* on granted or denied. A denial is terminal for this page load — Safari
|
|
116
|
+
* remembers the answer per site, so later loads resolve silently.
|
|
117
|
+
*/
|
|
118
|
+
export declare function createMotionPermissionFlow({ requestPermission, onGranted, onDenied, }: {
|
|
119
|
+
requestPermission?: () => Promise<string>;
|
|
120
|
+
onGranted: () => void;
|
|
121
|
+
onDenied?: () => void;
|
|
122
|
+
}): MotionPermissionFlow;
|
|
123
|
+
/**
|
|
124
|
+
* True only on the platform that gates devicemotion behind a user-gesture
|
|
125
|
+
* grant: iOS 13+ WebKit exposes `DeviceMotionEvent.requestPermission`.
|
|
126
|
+
* Android and desktop return false — they deliver motion events without a
|
|
127
|
+
* prompt, so shake-to-open arms itself there and no trigger UI is needed.
|
|
128
|
+
*/
|
|
129
|
+
export declare function isMotionPermissionRequired(): boolean;
|
|
130
|
+
/**
|
|
131
|
+
* The window event an app dispatches from a DELIBERATE user gesture to arm
|
|
132
|
+
* shake-to-open on iOS (e.g. the SHC console's triple-tap on the build
|
|
133
|
+
* number in Settings › Estate). It is a window CustomEvent rather than an
|
|
134
|
+
* imported function on purpose: the trigger lives in a feature package that
|
|
135
|
+
* does not — and must not — depend on this core build. `dispatchEvent` is
|
|
136
|
+
* synchronous, so the `requestPermission()` this fires runs INSIDE the
|
|
137
|
+
* originating gesture's call stack, which is the whole reason iOS accepts
|
|
138
|
+
* it. `armDevtoolsMotion()` dispatches it for callers that CAN import core.
|
|
139
|
+
*/
|
|
140
|
+
export declare const devtoolsArmMotionEvent = "mpo:devtools-arm-motion";
|
|
141
|
+
/**
|
|
142
|
+
* The window event core dispatches back once an arm attempt settles, so a
|
|
143
|
+
* trigger UI can report the honest outcome. `detail.granted` is true only
|
|
144
|
+
* when motion is now live.
|
|
145
|
+
*/
|
|
146
|
+
export declare const devtoolsMotionArmedEvent = "mpo:devtools-motion-armed";
|
|
147
|
+
/** Test seam: forget the grant and drop attachers between cases. */
|
|
148
|
+
export declare function __resetMotionArming(): void;
|
|
149
|
+
/**
|
|
150
|
+
* Request motion permission from within a user gesture and, on grant,
|
|
151
|
+
* attach every waiting shake listener. Idempotent once granted. Resolves
|
|
152
|
+
* to the settled permission state; a denial is terminal for this page load
|
|
153
|
+
* (Safari remembers it per site). Broadcast the outcome on
|
|
154
|
+
* `devtoolsMotionArmedEvent` so a trigger UI can report it.
|
|
155
|
+
*/
|
|
156
|
+
export declare function requestMotionPermissionFromGesture(): Promise<MotionPermissionState>;
|
|
157
|
+
/**
|
|
158
|
+
* Arm shake-to-open programmatically. For callers that CAN import core
|
|
159
|
+
* (the app layer); a feature package instead dispatches
|
|
160
|
+
* `devtoolsArmMotionEvent` directly. Must be called from a user gesture on
|
|
161
|
+
* iOS. No-op on the server.
|
|
162
|
+
*/
|
|
163
|
+
export declare function armDevtoolsMotion(): void;
|
|
164
|
+
/**
|
|
165
|
+
* Shake-to-open for the devtools shell. Attaches a `devicemotion` listener
|
|
166
|
+
* and opens the shell on a detected shake. Desktop browsers deliver no
|
|
167
|
+
* devicemotion events, so the listener is inert there — the keyboard
|
|
168
|
+
* shortcut is the desktop path.
|
|
169
|
+
*
|
|
170
|
+
* iOS 13+ gates devicemotion behind a permission prompt that can ONLY be
|
|
171
|
+
* requested from a user gesture, and the first shake cannot self-grant. So
|
|
172
|
+
* on iOS the listener attaches only after the app arms it — by dispatching
|
|
173
|
+
* `devtoolsArmMotionEvent` from a deliberate gesture (the console wires the
|
|
174
|
+
* build-number triple-tap). Android/desktop attach immediately. SSR-safe;
|
|
175
|
+
* every listener is torn down on unmount.
|
|
176
|
+
*/
|
|
177
|
+
export declare function useShakeToOpen(enabled: boolean, onShake?: () => void): void;
|
|
178
|
+
export {};
|
|
179
|
+
//# sourceMappingURL=devtoolsTrigger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devtoolsTrigger.d.ts","sourceRoot":"","sources":["../../src/app/devtoolsTrigger.ts"],"names":[],"mappings":"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"}
|