@expofp/debug 0.0.0-experimental.d269d30
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/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/lib/format-console-args.d.ts +16 -0
- package/dist/lib/format-console-args.js +95 -0
- package/dist/lib/init-debug.d.ts +2 -0
- package/dist/lib/init-debug.js +26 -0
- package/dist/lib/log-store.d.ts +11 -0
- package/dist/lib/log-store.js +22 -0
- package/dist/lib/settings/adopt-global-settings.d.ts +20 -0
- package/dist/lib/settings/adopt-global-settings.js +38 -0
- package/dist/lib/settings/editor-props.d.ts +7 -0
- package/dist/lib/settings/editor-props.js +1 -0
- package/dist/lib/settings/index.d.ts +7 -0
- package/dist/lib/settings/index.js +6 -0
- package/dist/lib/settings/register-boolean-setting.d.ts +8 -0
- package/dist/lib/settings/register-boolean-setting.js +13 -0
- package/dist/lib/settings/register-enum-setting.d.ts +9 -0
- package/dist/lib/settings/register-enum-setting.js +14 -0
- package/dist/lib/settings/register-setting.d.ts +18 -0
- package/dist/lib/settings/register-setting.js +66 -0
- package/dist/lib/settings/register-string-setting.d.ts +9 -0
- package/dist/lib/settings/register-string-setting.js +16 -0
- package/dist/lib/settings/reset-all-settings.d.ts +2 -0
- package/dist/lib/settings/reset-all-settings.js +4 -0
- package/dist/lib/settings/setting-registry.d.ts +4 -0
- package/dist/lib/settings/setting-registry.js +2 -0
- package/dist/lib/ui/boolean-editor.d.ts +3 -0
- package/dist/lib/ui/boolean-editor.js +6 -0
- package/dist/lib/ui/debug-overlay.d.ts +7 -0
- package/dist/lib/ui/debug-overlay.js +7 -0
- package/dist/lib/ui/debug-tabs.d.ts +6 -0
- package/dist/lib/ui/debug-tabs.js +6 -0
- package/dist/lib/ui/debug-ui.d.ts +7 -0
- package/dist/lib/ui/debug-ui.js +34 -0
- package/dist/lib/ui/enum-editor.d.ts +5 -0
- package/dist/lib/ui/enum-editor.js +16 -0
- package/dist/lib/ui/index.d.ts +2 -0
- package/dist/lib/ui/index.js +1 -0
- package/dist/lib/ui/log-tab.d.ts +2 -0
- package/dist/lib/ui/log-tab.js +46 -0
- package/dist/lib/ui/render-debug-ui.d.ts +2 -0
- package/dist/lib/ui/render-debug-ui.js +42 -0
- package/dist/lib/ui/settings-item.d.ts +6 -0
- package/dist/lib/ui/settings-item.js +5 -0
- package/dist/lib/ui/settings-list.d.ts +3 -0
- package/dist/lib/ui/settings-list.js +9 -0
- package/dist/lib/ui/settings-tab.d.ts +2 -0
- package/dist/lib/ui/settings-tab.js +13 -0
- package/dist/lib/ui/side-button.d.ts +5 -0
- package/dist/lib/ui/side-button.js +37 -0
- package/dist/lib/ui/string-editor.d.ts +5 -0
- package/dist/lib/ui/string-editor.js +7 -0
- package/dist/lib/use-log-buffer.d.ts +3 -0
- package/dist/lib/use-log-buffer.js +5 -0
- package/dist/lib/utils/add-debug-secret-listener.d.ts +2 -0
- package/dist/lib/utils/add-debug-secret-listener.js +38 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# @expofp/debug
|
|
2
|
+
|
|
3
|
+
Internal debug panel for the ExpoFP SDK. Bundled with `@expofp/floorplan` — no separate install.
|
|
4
|
+
|
|
5
|
+
## Open the panel
|
|
6
|
+
|
|
7
|
+
Type `dbg123` anywhere on the page (the 6 letters, in order, no modifiers). A Debug button appears on the right edge — click it to open the panel.
|
|
8
|
+
|
|
9
|
+
## Settings
|
|
10
|
+
|
|
11
|
+
- **Show Debug Button Overlay** — keeps the button visible across reloads.
|
|
12
|
+
- **Debug namespaces** — pattern for the [`debug`](https://www.npmjs.com/package/debug) npm library (e.g. `efp:*`). Applies immediately, no reload.
|
|
13
|
+
- Other
|
|
14
|
+
|
|
15
|
+
## Log
|
|
16
|
+
|
|
17
|
+
Captures `console.log` / `debug` / `info` / `warn` / `error` from page load — including styled output from the `debug` library — and renders it with `%c` colors preserved. Capped at 5000 entries.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react';
|
|
2
|
+
export type FormattedSegment = {
|
|
3
|
+
text: string;
|
|
4
|
+
style: CSSProperties;
|
|
5
|
+
};
|
|
6
|
+
export type FormattedArgs = {
|
|
7
|
+
segments: FormattedSegment[];
|
|
8
|
+
trailing: unknown[];
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Parse a console-style format string with `%c`/`%s`/`%d`/`%i`/`%f`/`%o`/`%O`/`%%`
|
|
12
|
+
* into styled text segments plus any trailing unconsumed args. Mirrors browser
|
|
13
|
+
* DevTools formatting so colored `console.log` output can be rendered in the UI.
|
|
14
|
+
*/
|
|
15
|
+
export declare function formatConsoleArgs(args: unknown[]): FormattedArgs;
|
|
16
|
+
//# sourceMappingURL=format-console-args.d.ts.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a console-style format string with `%c`/`%s`/`%d`/`%i`/`%f`/`%o`/`%O`/`%%`
|
|
3
|
+
* into styled text segments plus any trailing unconsumed args. Mirrors browser
|
|
4
|
+
* DevTools formatting so colored `console.log` output can be rendered in the UI.
|
|
5
|
+
*/
|
|
6
|
+
export function formatConsoleArgs(args) {
|
|
7
|
+
const fmt = args[0];
|
|
8
|
+
if (typeof fmt !== 'string' || !fmt.includes('%')) {
|
|
9
|
+
return { segments: [], trailing: args };
|
|
10
|
+
}
|
|
11
|
+
const segments = [];
|
|
12
|
+
let style = {};
|
|
13
|
+
let buf = '';
|
|
14
|
+
let argIdx = 1;
|
|
15
|
+
let i = 0;
|
|
16
|
+
const flush = () => {
|
|
17
|
+
if (buf.length > 0) {
|
|
18
|
+
segments.push({ text: buf, style });
|
|
19
|
+
buf = '';
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
while (i < fmt.length) {
|
|
23
|
+
const ch = fmt[i];
|
|
24
|
+
if (ch !== '%' || i === fmt.length - 1) {
|
|
25
|
+
buf += ch;
|
|
26
|
+
i++;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const directive = fmt[i + 1];
|
|
30
|
+
switch (directive) {
|
|
31
|
+
case 'c': {
|
|
32
|
+
flush();
|
|
33
|
+
const css = args[argIdx++];
|
|
34
|
+
style = typeof css === 'string' ? parseCss(css) : {};
|
|
35
|
+
i += 2;
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
case 's': {
|
|
39
|
+
buf += String(args[argIdx++] ?? '');
|
|
40
|
+
i += 2;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
case 'd':
|
|
44
|
+
case 'i': {
|
|
45
|
+
const n = Number(args[argIdx++]);
|
|
46
|
+
buf += Number.isNaN(n) ? 'NaN' : String(Math.trunc(n));
|
|
47
|
+
i += 2;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
case 'f': {
|
|
51
|
+
buf += String(Number(args[argIdx++]));
|
|
52
|
+
i += 2;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
case 'o':
|
|
56
|
+
case 'O': {
|
|
57
|
+
const v = args[argIdx++];
|
|
58
|
+
try {
|
|
59
|
+
buf += JSON.stringify(v);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
buf += String(v);
|
|
63
|
+
}
|
|
64
|
+
i += 2;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
case '%': {
|
|
68
|
+
buf += '%';
|
|
69
|
+
i += 2;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
default: {
|
|
73
|
+
buf += ch;
|
|
74
|
+
i++;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
flush();
|
|
79
|
+
return { segments, trailing: args.slice(argIdx) };
|
|
80
|
+
}
|
|
81
|
+
function parseCss(css) {
|
|
82
|
+
const out = {};
|
|
83
|
+
for (const decl of css.split(';')) {
|
|
84
|
+
const colonIdx = decl.indexOf(':');
|
|
85
|
+
if (colonIdx === -1)
|
|
86
|
+
continue;
|
|
87
|
+
const prop = decl.slice(0, colonIdx).trim();
|
|
88
|
+
const val = decl.slice(colonIdx + 1).trim();
|
|
89
|
+
if (!prop || !val)
|
|
90
|
+
continue;
|
|
91
|
+
const camel = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
92
|
+
out[camel] = val;
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { appendLog } from './log-store.js';
|
|
2
|
+
if (typeof console !== 'undefined') {
|
|
3
|
+
const wrap = (level, original) => (...args) => {
|
|
4
|
+
appendLog({ level, args, ts: Date.now() });
|
|
5
|
+
original(...args);
|
|
6
|
+
};
|
|
7
|
+
const originalLog = console.log.bind(console);
|
|
8
|
+
const originalDebug = console.debug.bind(console);
|
|
9
|
+
const originalInfo = console.info.bind(console);
|
|
10
|
+
const originalWarn = console.warn.bind(console);
|
|
11
|
+
const originalError = console.error.bind(console);
|
|
12
|
+
console.log = wrap('log', originalLog);
|
|
13
|
+
console.debug = wrap('debug', originalDebug);
|
|
14
|
+
console.info = wrap('info', originalInfo);
|
|
15
|
+
console.warn = wrap('warn', originalWarn);
|
|
16
|
+
console.error = wrap('error', originalError);
|
|
17
|
+
}
|
|
18
|
+
if (typeof window !== 'undefined' || typeof document !== 'undefined') {
|
|
19
|
+
import('./ui/index.js')
|
|
20
|
+
.then(({ renderDebugUi }) => {
|
|
21
|
+
renderDebugUi();
|
|
22
|
+
})
|
|
23
|
+
.catch(() => {
|
|
24
|
+
/* debug UI failed to load */
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type LogLevel = 'log' | 'debug' | 'info' | 'warn' | 'error';
|
|
2
|
+
export type LogEntry = {
|
|
3
|
+
level: LogLevel;
|
|
4
|
+
args: unknown[];
|
|
5
|
+
ts: number;
|
|
6
|
+
};
|
|
7
|
+
export declare function getLogSnapshot(): readonly LogEntry[];
|
|
8
|
+
export declare function subscribeLogStore(listener: () => void): () => void;
|
|
9
|
+
export declare function appendLog(entry: LogEntry): void;
|
|
10
|
+
export declare function clearLogs(): void;
|
|
11
|
+
//# sourceMappingURL=log-store.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const MAX = 5000;
|
|
2
|
+
let snapshot = [];
|
|
3
|
+
const listeners = new Set();
|
|
4
|
+
export function getLogSnapshot() {
|
|
5
|
+
return snapshot;
|
|
6
|
+
}
|
|
7
|
+
export function subscribeLogStore(listener) {
|
|
8
|
+
listeners.add(listener);
|
|
9
|
+
return () => {
|
|
10
|
+
listeners.delete(listener);
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function appendLog(entry) {
|
|
14
|
+
snapshot = snapshot.length >= MAX ? [...snapshot.slice(1), entry] : [...snapshot, entry];
|
|
15
|
+
listeners.forEach((l) => l());
|
|
16
|
+
}
|
|
17
|
+
export function clearLogs() {
|
|
18
|
+
if (snapshot.length === 0)
|
|
19
|
+
return;
|
|
20
|
+
snapshot = [];
|
|
21
|
+
listeners.forEach((l) => l());
|
|
22
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type GlobalSettingSchema = {
|
|
2
|
+
type: 'boolean';
|
|
3
|
+
default?: boolean;
|
|
4
|
+
} | {
|
|
5
|
+
type: 'string';
|
|
6
|
+
default?: string;
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
} | {
|
|
9
|
+
type: 'enum';
|
|
10
|
+
values: readonly string[];
|
|
11
|
+
default: string;
|
|
12
|
+
};
|
|
13
|
+
export type EfpDebugUi = Record<string, GlobalSettingSchema>;
|
|
14
|
+
declare global {
|
|
15
|
+
interface Window {
|
|
16
|
+
efpDebugUi?: EfpDebugUi;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export declare function adoptGlobalSettings(): void;
|
|
20
|
+
//# sourceMappingURL=adopt-global-settings.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { registerBooleanSetting } from './register-boolean-setting.js';
|
|
2
|
+
import { registerEnumSetting } from './register-enum-setting.js';
|
|
3
|
+
import { registerStringSetting } from './register-string-setting.js';
|
|
4
|
+
const adopted = new Set();
|
|
5
|
+
export function adoptGlobalSettings() {
|
|
6
|
+
if (typeof window === 'undefined')
|
|
7
|
+
return;
|
|
8
|
+
const settings = window.efpDebugUi;
|
|
9
|
+
if (!settings)
|
|
10
|
+
return;
|
|
11
|
+
for (const [key, schema] of Object.entries(settings)) {
|
|
12
|
+
if (adopted.has(key))
|
|
13
|
+
continue;
|
|
14
|
+
adopted.add(key);
|
|
15
|
+
switch (schema.type) {
|
|
16
|
+
case 'boolean':
|
|
17
|
+
registerBooleanSetting({
|
|
18
|
+
key,
|
|
19
|
+
default: schema.default,
|
|
20
|
+
});
|
|
21
|
+
break;
|
|
22
|
+
case 'string':
|
|
23
|
+
registerStringSetting({
|
|
24
|
+
key,
|
|
25
|
+
default: schema.default,
|
|
26
|
+
placeholder: schema.placeholder,
|
|
27
|
+
});
|
|
28
|
+
break;
|
|
29
|
+
case 'enum':
|
|
30
|
+
registerEnumSetting({
|
|
31
|
+
key,
|
|
32
|
+
values: schema.values,
|
|
33
|
+
default: schema.default,
|
|
34
|
+
});
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from './adopt-global-settings.js';
|
|
2
|
+
export * from './register-boolean-setting.js';
|
|
3
|
+
export * from './register-enum-setting.js';
|
|
4
|
+
export * from './register-setting.js';
|
|
5
|
+
export * from './register-string-setting.js';
|
|
6
|
+
export * from './reset-all-settings.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from './adopt-global-settings.js';
|
|
2
|
+
export * from './register-boolean-setting.js';
|
|
3
|
+
export * from './register-enum-setting.js';
|
|
4
|
+
export * from './register-setting.js';
|
|
5
|
+
export * from './register-string-setting.js';
|
|
6
|
+
export * from './reset-all-settings.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type DebugSetting } from './register-setting.js';
|
|
2
|
+
export interface RegisterBooleanSettingOptions {
|
|
3
|
+
key: string;
|
|
4
|
+
default?: boolean;
|
|
5
|
+
onChange?: (value: boolean) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function registerBooleanSetting(options: RegisterBooleanSettingOptions): DebugSetting<boolean>;
|
|
8
|
+
//# sourceMappingURL=register-boolean-setting.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { registerSetting } from './register-setting.js';
|
|
3
|
+
export function registerBooleanSetting(options) {
|
|
4
|
+
const BooleanEditorLazy = React.lazy(() => import('../ui/boolean-editor.js').then((mod) => ({
|
|
5
|
+
default: mod.BooleanEditor,
|
|
6
|
+
})));
|
|
7
|
+
return registerSetting({
|
|
8
|
+
key: options.key,
|
|
9
|
+
default: options.default ?? false,
|
|
10
|
+
Editor: BooleanEditorLazy,
|
|
11
|
+
onChange: options.onChange,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type DebugSetting } from './register-setting.js';
|
|
2
|
+
export interface RegisterEnumSettingOptions<T> {
|
|
3
|
+
key: string;
|
|
4
|
+
values: readonly T[];
|
|
5
|
+
default: T;
|
|
6
|
+
onChange?: (value: T) => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function registerEnumSetting<T>(options: RegisterEnumSettingOptions<T>): DebugSetting<T>;
|
|
9
|
+
//# sourceMappingURL=register-enum-setting.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { registerSetting } from './register-setting.js';
|
|
3
|
+
export function registerEnumSetting(options) {
|
|
4
|
+
const EnumEditorLazy = React.lazy(() => import('../ui/enum-editor.js').then((mod) => ({
|
|
5
|
+
default: mod.EnumEditor,
|
|
6
|
+
})));
|
|
7
|
+
return registerSetting({
|
|
8
|
+
key: options.key,
|
|
9
|
+
default: options.default,
|
|
10
|
+
Editor: EnumEditorLazy,
|
|
11
|
+
context: { values: options.values },
|
|
12
|
+
onChange: options.onChange,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { EditorProps } from './editor-props.js';
|
|
2
|
+
export interface DebugSetting<T> {
|
|
3
|
+
get: () => T;
|
|
4
|
+
set: (value: T) => void;
|
|
5
|
+
reset: () => void;
|
|
6
|
+
useState: () => [T, (value: T) => void];
|
|
7
|
+
}
|
|
8
|
+
export interface RegisterSettingOptions<T, K = void> {
|
|
9
|
+
key: string;
|
|
10
|
+
default: T;
|
|
11
|
+
Editor: React.FC<EditorProps<T, K>>;
|
|
12
|
+
context?: K;
|
|
13
|
+
onChange?: (value: T) => void;
|
|
14
|
+
serialize?: (value: T) => string;
|
|
15
|
+
deserialize?: (raw: string) => T;
|
|
16
|
+
}
|
|
17
|
+
export declare function registerSetting<T, K = void>(options: RegisterSettingOptions<T, K>): DebugSetting<T>;
|
|
18
|
+
//# sourceMappingURL=register-setting.d.ts.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { editors, resets } from './setting-registry.js';
|
|
4
|
+
const LOCAL_STORAGE_EVENT = 'efp-local-storage';
|
|
5
|
+
export function registerSetting(options) {
|
|
6
|
+
const { key, default: defaultValue, Editor, context, onChange, serialize = JSON.stringify, deserialize = (raw) => JSON.parse(raw), } = options;
|
|
7
|
+
const get = () => {
|
|
8
|
+
const raw = localStorage.getItem(key);
|
|
9
|
+
if (raw === null)
|
|
10
|
+
return defaultValue;
|
|
11
|
+
try {
|
|
12
|
+
return deserialize(raw);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return defaultValue;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const set = (value) => {
|
|
19
|
+
localStorage.setItem(key, serialize(value));
|
|
20
|
+
onChange?.(value);
|
|
21
|
+
emitLocalStorageChange(key);
|
|
22
|
+
};
|
|
23
|
+
const reset = () => {
|
|
24
|
+
localStorage.removeItem(key);
|
|
25
|
+
onChange?.(defaultValue);
|
|
26
|
+
emitLocalStorageChange(key);
|
|
27
|
+
};
|
|
28
|
+
resets.push(reset);
|
|
29
|
+
function useLocalStorageState() {
|
|
30
|
+
const [state, setState] = useState(get());
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const update = () => setState(get());
|
|
33
|
+
const onStorage = (e) => {
|
|
34
|
+
if (e.key === key)
|
|
35
|
+
update();
|
|
36
|
+
};
|
|
37
|
+
const onLocal = (e) => {
|
|
38
|
+
const { detail } = e;
|
|
39
|
+
if (detail?.key === key)
|
|
40
|
+
update();
|
|
41
|
+
};
|
|
42
|
+
window.addEventListener('storage', onStorage);
|
|
43
|
+
window.addEventListener(LOCAL_STORAGE_EVENT, onLocal);
|
|
44
|
+
return () => {
|
|
45
|
+
window.removeEventListener('storage', onStorage);
|
|
46
|
+
window.removeEventListener(LOCAL_STORAGE_EVENT, onLocal);
|
|
47
|
+
};
|
|
48
|
+
}, []);
|
|
49
|
+
return [state, set];
|
|
50
|
+
}
|
|
51
|
+
const EditorImpl = () => {
|
|
52
|
+
const [state, setState] = useLocalStorageState();
|
|
53
|
+
return _jsx(Editor, { label: key, value: state, context: context, onChange: setState });
|
|
54
|
+
};
|
|
55
|
+
EditorImpl.displayName = `DebugSetting(${key})`;
|
|
56
|
+
editors.push(EditorImpl);
|
|
57
|
+
return {
|
|
58
|
+
get,
|
|
59
|
+
set,
|
|
60
|
+
reset,
|
|
61
|
+
useState: useLocalStorageState,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function emitLocalStorageChange(key) {
|
|
65
|
+
window.dispatchEvent(new CustomEvent(LOCAL_STORAGE_EVENT, { detail: { key } }));
|
|
66
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type DebugSetting } from './register-setting.js';
|
|
2
|
+
export interface RegisterStringSettingOptions {
|
|
3
|
+
key: string;
|
|
4
|
+
default?: string;
|
|
5
|
+
placeholder?: string;
|
|
6
|
+
onChange?: (value: string) => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function registerStringSetting(options: RegisterStringSettingOptions): DebugSetting<string>;
|
|
9
|
+
//# sourceMappingURL=register-string-setting.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { registerSetting } from './register-setting.js';
|
|
3
|
+
export function registerStringSetting(options) {
|
|
4
|
+
const StringEditorLazy = React.lazy(() => import('../ui/string-editor.js').then((mod) => ({
|
|
5
|
+
default: mod.StringEditor,
|
|
6
|
+
})));
|
|
7
|
+
return registerSetting({
|
|
8
|
+
key: options.key,
|
|
9
|
+
default: options.default ?? '',
|
|
10
|
+
Editor: StringEditorLazy,
|
|
11
|
+
context: { placeholder: options.placeholder },
|
|
12
|
+
onChange: options.onChange,
|
|
13
|
+
serialize: (v) => v,
|
|
14
|
+
deserialize: (v) => v,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Flex, Switch } from '@radix-ui/themes';
|
|
3
|
+
import { SettingsItem } from './settings-item.js';
|
|
4
|
+
export const BooleanEditor = (props) => {
|
|
5
|
+
return (_jsx(SettingsItem, { label: props.label, children: _jsx(Flex, { children: _jsx(Switch, { checked: props.value, onCheckedChange: (checked) => props.onChange(checked) }) }) }));
|
|
6
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import '@radix-ui/themes/styles.css';
|
|
3
|
+
import { Box, Container, Theme } from '@radix-ui/themes';
|
|
4
|
+
import { DebugTabs } from './debug-tabs.js';
|
|
5
|
+
export const DebugOverlay = ({ onClose }) => {
|
|
6
|
+
return (_jsx(Theme, { appearance: "dark", children: _jsx(Box, { position: "fixed", top: "0", right: "0", width: "100vw", height: "100dvh", overflowY: "auto", style: { backgroundColor: 'black' }, children: _jsx(Container, { size: "4", px: "2", children: _jsx(DebugTabs, { onClose: onClose }) }) }) }));
|
|
7
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Cross1Icon } from '@radix-ui/react-icons';
|
|
3
|
+
import { Button, Tabs } from '@radix-ui/themes';
|
|
4
|
+
import { LogTab } from './log-tab.js';
|
|
5
|
+
import { SettingsTab } from './settings-tab.js';
|
|
6
|
+
export const DebugTabs = ({ onClose }) => (_jsxs(Tabs.Root, { defaultValue: "settingsTab", children: [_jsxs(Tabs.List, { children: [_jsx(Tabs.Trigger, { value: "settingsTab", children: "Settings" }), _jsx(Tabs.Trigger, { value: "logTab", children: "Log" }), _jsxs(Button, { ml: "auto", variant: "soft", mt: "3px", color: "gray", onClick: onClose, children: [_jsx(Cross1Icon, { width: "16", height: "16" }), " Close"] })] }), _jsx(Tabs.Content, { value: "settingsTab", children: _jsx(SettingsTab, {}) }), _jsx(Tabs.Content, { value: "logTab", children: _jsx(LogTab, {}) })] }));
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { lazy, Suspense, useEffect, useState } from 'react';
|
|
3
|
+
import { addDebugSecretListener } from '../utils/add-debug-secret-listener.js';
|
|
4
|
+
import { SideButton } from './side-button.js';
|
|
5
|
+
const LazyDebugOverlay = lazy(async () => ({
|
|
6
|
+
default: (await import('./debug-overlay.js')).DebugOverlay,
|
|
7
|
+
}));
|
|
8
|
+
const DEFAULT_HOST_SELECTOR = '.expofp-floorplan-default';
|
|
9
|
+
export const DebugUi = (props) => {
|
|
10
|
+
const [isOpen, setIsOpen] = useState(props.open);
|
|
11
|
+
const [debugButtonEnabled, setDebugButtonEnabled] = props.useDebugButtonState();
|
|
12
|
+
const selector = props.hostSelector ?? DEFAULT_HOST_SELECTOR;
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
return addDebugSecretListener(() => {
|
|
15
|
+
setDebugButtonEnabled(true);
|
|
16
|
+
setIsOpen(true);
|
|
17
|
+
});
|
|
18
|
+
}, []);
|
|
19
|
+
// set visibility of efp floorplan to none when debug ui is open
|
|
20
|
+
// otherwise, z-index issues occur
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const efpFloorplan = document.querySelector(selector);
|
|
23
|
+
if (efpFloorplan) {
|
|
24
|
+
efpFloorplan.style.visibility = isOpen ? 'hidden' : 'visible';
|
|
25
|
+
}
|
|
26
|
+
}, [isOpen, selector]);
|
|
27
|
+
if (isOpen) {
|
|
28
|
+
return (_jsx(Suspense, { fallback: null, children: _jsx(LazyDebugOverlay, { onClose: () => {
|
|
29
|
+
setIsOpen(false);
|
|
30
|
+
} }) }));
|
|
31
|
+
}
|
|
32
|
+
return debugButtonEnabled ? _jsx(SideButton, { onClick: () => setIsOpen(true) }) : null;
|
|
33
|
+
};
|
|
34
|
+
DebugUi.displayName = 'DebugUi';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Select } from '@radix-ui/themes';
|
|
3
|
+
import { SettingsItem } from './settings-item.js';
|
|
4
|
+
export function EnumEditor(props) {
|
|
5
|
+
const stringMode = props.context.values.every((v) => typeof v === 'string');
|
|
6
|
+
const stringify = stringMode ? (v) => v : (v) => JSON.stringify(v);
|
|
7
|
+
const parse = stringMode ? (v) => v : (v) => JSON.parse(v);
|
|
8
|
+
const value = stringify(props.value);
|
|
9
|
+
const values = props.context.values.map((v) => ({
|
|
10
|
+
value: stringify(v),
|
|
11
|
+
label: stringify(v),
|
|
12
|
+
}));
|
|
13
|
+
return (_jsx(SettingsItem, { label: props.label, children: _jsxs(Select.Root, { value: value, onValueChange: (value) => {
|
|
14
|
+
props.onChange(parse(value));
|
|
15
|
+
}, children: [_jsx(Select.Trigger, {}), _jsx(Select.Content, { children: values.map((v) => (_jsx(Select.Item, { value: v.value, children: v.label }, v.value))) })] }) }));
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { renderDebugUi } from './render-debug-ui.js';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { InfoCircledIcon } from '@radix-ui/react-icons';
|
|
3
|
+
import { Box, Button, Callout, Code, Flex } from '@radix-ui/themes';
|
|
4
|
+
import { formatConsoleArgs } from '../format-console-args.js';
|
|
5
|
+
import { clearLogs } from '../log-store.js';
|
|
6
|
+
import { useLogBuffer } from '../use-log-buffer.js';
|
|
7
|
+
export const LogTab = () => {
|
|
8
|
+
const logs = useLogBuffer();
|
|
9
|
+
return (_jsxs(Box, { pt: "20px", children: [_jsxs(Flex, { justify: "between", align: "center", mb: "3", children: [_jsxs(Box, { children: [logs.length, " ", logs.length === 1 ? 'entry' : 'entries'] }), _jsx(Button, { size: "1", variant: "soft", onClick: clearLogs, disabled: logs.length === 0, children: "Clear" })] }), logs.length === 0 ? (_jsxs(Callout.Root, { children: [_jsx(Callout.Icon, { children: _jsx(InfoCircledIcon, {}) }), _jsx(Callout.Text, { children: "No logs yet \u2014 console.log / info / warn / error calls will appear here." })] })) : (logs.map((entry, i) => _jsx(LogRow, { entry: entry }, i)))] }));
|
|
10
|
+
};
|
|
11
|
+
const levelColor = {
|
|
12
|
+
log: undefined,
|
|
13
|
+
debug: 'gray',
|
|
14
|
+
info: 'blue',
|
|
15
|
+
warn: 'amber',
|
|
16
|
+
error: 'red',
|
|
17
|
+
};
|
|
18
|
+
const LogRow = ({ entry }) => {
|
|
19
|
+
const { segments, trailing } = formatConsoleArgs(entry.args);
|
|
20
|
+
return (_jsxs(Box, { mb: "1", style: {
|
|
21
|
+
fontFamily: 'monospace',
|
|
22
|
+
fontSize: 12,
|
|
23
|
+
whiteSpace: 'pre-wrap',
|
|
24
|
+
wordBreak: 'break-word',
|
|
25
|
+
}, children: [_jsx(Code, { size: "1", variant: "ghost", children: formatTime(entry.ts) }), ' ', _jsxs(Code, { size: "1", variant: "ghost", color: levelColor[entry.level], children: ["[", entry.level, "]"] }), ' ', segments.length > 0
|
|
26
|
+
? renderFormatted(segments, trailing)
|
|
27
|
+
: entry.args.map((a, i) => (_jsxs("span", { children: [i > 0 ? ' ' : '', formatArg(a)] }, i)))] }));
|
|
28
|
+
};
|
|
29
|
+
function renderFormatted(segments, trailing) {
|
|
30
|
+
return (_jsxs(_Fragment, { children: [segments.map((s, i) => (_jsx("span", { style: s.style, children: s.text }, i))), trailing.map((a, i) => (_jsxs("span", { children: [" ", formatArg(a)] }, `t-${i}`)))] }));
|
|
31
|
+
}
|
|
32
|
+
function formatTime(ts) {
|
|
33
|
+
return new Date(ts).toISOString().slice(11, 23);
|
|
34
|
+
}
|
|
35
|
+
function formatArg(a) {
|
|
36
|
+
if (typeof a === 'string')
|
|
37
|
+
return a;
|
|
38
|
+
if (a instanceof Error)
|
|
39
|
+
return `${a.name}: ${a.message}`;
|
|
40
|
+
try {
|
|
41
|
+
return JSON.stringify(a);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return String(a);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import debug from 'debug';
|
|
3
|
+
import { registerBooleanSetting, registerStringSetting } from '../settings/index.js';
|
|
4
|
+
import { addDebugSecretListener } from '../utils/add-debug-secret-listener.js';
|
|
5
|
+
let rendered = false;
|
|
6
|
+
export function renderDebugUi() {
|
|
7
|
+
if (rendered) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
rendered = true;
|
|
11
|
+
const showDebugButtonSetting = registerBooleanSetting({
|
|
12
|
+
key: 'efp:show-debug-button',
|
|
13
|
+
});
|
|
14
|
+
registerStringSetting({
|
|
15
|
+
key: 'debug',
|
|
16
|
+
placeholder: 'e.g. efp:*',
|
|
17
|
+
onChange: (value) => {
|
|
18
|
+
if (value)
|
|
19
|
+
debug.enable(value);
|
|
20
|
+
else
|
|
21
|
+
debug.disable();
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
const render = async (open) => {
|
|
25
|
+
const { DebugUi } = await import('./debug-ui.js');
|
|
26
|
+
const { createRoot } = await import('react-dom/client');
|
|
27
|
+
const rootElement = document.createElement('div');
|
|
28
|
+
document.body.appendChild(rootElement);
|
|
29
|
+
const root = createRoot(rootElement);
|
|
30
|
+
root.render(_jsx(DebugUi, { useDebugButtonState: showDebugButtonSetting.useState, open: open }));
|
|
31
|
+
};
|
|
32
|
+
if (showDebugButtonSetting.get()) {
|
|
33
|
+
render(false);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
const unsubscribe = addDebugSecretListener(() => {
|
|
37
|
+
unsubscribe();
|
|
38
|
+
showDebugButtonSetting.set(true);
|
|
39
|
+
render(true);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Heading, Separator } from '@radix-ui/themes';
|
|
3
|
+
export const SettingsItem = ({ label, children }) => {
|
|
4
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Heading, { size: "4", children: label }), _jsx(Box, { mt: "10px", children: children })] }), _jsx(Separator, { size: "4", my: "20px" })] }));
|
|
5
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Skeleton } from '@radix-ui/themes';
|
|
3
|
+
import { Suspense } from 'react';
|
|
4
|
+
import { adoptGlobalSettings } from '../settings/adopt-global-settings.js';
|
|
5
|
+
import { editors } from '../settings/setting-registry.js';
|
|
6
|
+
export const SettingsList = () => {
|
|
7
|
+
adoptGlobalSettings();
|
|
8
|
+
return editors.map((Editor, index) => (_jsx(Suspense, { fallback: _jsx(Skeleton, { height: "60px" }), children: _jsx(Editor, {}) }, index)));
|
|
9
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { InfoCircledIcon } from '@radix-ui/react-icons';
|
|
3
|
+
import { AlertDialog, Box, Button, Callout, Flex } from '@radix-ui/themes';
|
|
4
|
+
import { resetAllSettings } from '../settings/reset-all-settings.js';
|
|
5
|
+
import { SettingsList } from './settings-list.js';
|
|
6
|
+
export const SettingsTab = () => {
|
|
7
|
+
return (_jsxs(Box, { pt: "20px", children: [_jsx(Box, { mb: "20px", children: _jsxs(Callout.Root, { children: [_jsx(Callout.Icon, { children: _jsx(InfoCircledIcon, {}) }), _jsx(Callout.Text, { children: "These settings will persist across page reloads in your browser." })] }) }), _jsx(SettingsList, {}), _jsxs(Flex, { gap: "3", mt: "40px", justify: "center", children: [_jsx(Button, { onClick: () => {
|
|
8
|
+
window.location.reload();
|
|
9
|
+
}, children: "Reload with the applied changes" }), _jsxs(AlertDialog.Root, { children: [_jsx(AlertDialog.Trigger, { children: _jsx(Button, { color: "red", children: "Reset all and reload" }) }), _jsxs(AlertDialog.Content, { maxWidth: "450px", children: [_jsx(AlertDialog.Title, { children: "Reset all and reload" }), _jsx(AlertDialog.Description, { size: "2", children: "This will reset all settings, disable debug mode, and reload the page." }), _jsxs(Flex, { gap: "3", mt: "4", justify: "end", children: [_jsx(AlertDialog.Cancel, { children: _jsx(Button, { variant: "soft", color: "gray", children: "Cancel" }) }), _jsx(AlertDialog.Action, { children: _jsx(Button, { variant: "solid", color: "red", onClick: () => {
|
|
10
|
+
resetAllSettings();
|
|
11
|
+
window.location.reload();
|
|
12
|
+
}, children: "Reset all and reload" }) })] })] })] })] })] }));
|
|
13
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
export const SideButton = ({ onClick }) => {
|
|
3
|
+
const size = 64;
|
|
4
|
+
const width = 20;
|
|
5
|
+
const radius = 14;
|
|
6
|
+
return (_jsx("button", { type: "button", style: {
|
|
7
|
+
position: 'fixed',
|
|
8
|
+
right: 0,
|
|
9
|
+
top: '50%',
|
|
10
|
+
transform: 'translateY(-50%)',
|
|
11
|
+
padding: 0,
|
|
12
|
+
border: 'none',
|
|
13
|
+
background: 'transparent',
|
|
14
|
+
cursor: 'pointer',
|
|
15
|
+
zIndex: 9999,
|
|
16
|
+
boxSizing: 'border-box',
|
|
17
|
+
}, onClick: onClick, children: _jsx("div", { style: {
|
|
18
|
+
width,
|
|
19
|
+
height: size,
|
|
20
|
+
paddingLeft: 4,
|
|
21
|
+
display: 'flex',
|
|
22
|
+
alignItems: 'center',
|
|
23
|
+
justifyContent: 'center',
|
|
24
|
+
background: 'var(--side-tab-bg, rgba(0,0,0,0.5))',
|
|
25
|
+
color: 'white',
|
|
26
|
+
borderRadius: `${radius}px 0 0 ${radius}px`,
|
|
27
|
+
fontSize: Math.round(size * 0.25),
|
|
28
|
+
lineHeight: 1,
|
|
29
|
+
userSelect: 'none',
|
|
30
|
+
cursor: 'pointer',
|
|
31
|
+
filter: 'grayscale(0.5)',
|
|
32
|
+
boxSizing: 'border-box',
|
|
33
|
+
}, children: _jsx(ChevronLeftSvg, { size: size * 0.7 }) }) }));
|
|
34
|
+
};
|
|
35
|
+
function ChevronLeftSvg({ size = 18, strokeWidth = 3, ...props }) {
|
|
36
|
+
return (_jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", ...props, children: _jsx("path", { fill: "none", d: "M14.5 5.5L8.5 12l6 6.5", stroke: "currentColor", strokeWidth: strokeWidth, strokeLinecap: "round", strokeLinejoin: "round" }) }));
|
|
37
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { TextField } from '@radix-ui/themes';
|
|
3
|
+
import { SettingsItem } from './settings-item.js';
|
|
4
|
+
export const StringEditor = (props) => {
|
|
5
|
+
return (_jsx(SettingsItem, { label: props.label, children: _jsx(TextField.Root, { placeholder: props.context?.placeholder, value: props.value, onChange: (e) => props.onChange(e.target.value), style: { minWidth: 220 } }) }));
|
|
6
|
+
};
|
|
7
|
+
StringEditor.displayName = 'StringEditor';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const SECRET_PHRASE = 'dbg123';
|
|
2
|
+
const callbacks = [];
|
|
3
|
+
let initialized = false;
|
|
4
|
+
let listenerRef = null;
|
|
5
|
+
export function addDebugSecretListener(callback) {
|
|
6
|
+
callbacks.push(callback);
|
|
7
|
+
if (!initialized) {
|
|
8
|
+
initialized = true;
|
|
9
|
+
let buffer = '';
|
|
10
|
+
function onKeyDown(e) {
|
|
11
|
+
// Ignore modifier keys
|
|
12
|
+
if (e.ctrlKey || e.metaKey || e.altKey)
|
|
13
|
+
return;
|
|
14
|
+
if (e.key.length !== 1)
|
|
15
|
+
return;
|
|
16
|
+
buffer += e.key.toLowerCase();
|
|
17
|
+
// Keep buffer length sane
|
|
18
|
+
buffer = buffer.slice(-SECRET_PHRASE.length);
|
|
19
|
+
if (buffer === SECRET_PHRASE) {
|
|
20
|
+
buffer = '';
|
|
21
|
+
callbacks.forEach((cb) => cb());
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
listenerRef = onKeyDown;
|
|
25
|
+
document.addEventListener('keydown', onKeyDown);
|
|
26
|
+
}
|
|
27
|
+
return () => {
|
|
28
|
+
const index = callbacks.indexOf(callback);
|
|
29
|
+
if (index !== -1) {
|
|
30
|
+
callbacks.splice(index, 1);
|
|
31
|
+
}
|
|
32
|
+
if (callbacks.length === 0 && listenerRef) {
|
|
33
|
+
document.removeEventListener('keydown', listenerRef);
|
|
34
|
+
listenerRef = null;
|
|
35
|
+
initialized = false;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@expofp/debug",
|
|
3
|
+
"version": "0.0.0-experimental.d269d30",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "ExpoFP SDK internal: debug utilities",
|
|
6
|
+
"homepage": "https://developer.expofp.com/",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"module": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
"./package.json": "./package.json",
|
|
16
|
+
".": {
|
|
17
|
+
"@expofp/source": "./src/index.ts",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.js",
|
|
20
|
+
"default": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"!**/*.tsbuildinfo",
|
|
26
|
+
"!**/*.map"
|
|
27
|
+
],
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@radix-ui/react-icons": "^1.3.2",
|
|
30
|
+
"@radix-ui/themes": "^3.2.1",
|
|
31
|
+
"debug": "^4.4.3",
|
|
32
|
+
"tslib": "^2.3.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"react": "^19.2.6",
|
|
36
|
+
"react-dom": "^19.2.6"
|
|
37
|
+
}
|
|
38
|
+
}
|