@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.
Files changed (58) hide show
  1. package/README.md +17 -0
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.js +2 -0
  4. package/dist/lib/format-console-args.d.ts +16 -0
  5. package/dist/lib/format-console-args.js +95 -0
  6. package/dist/lib/init-debug.d.ts +2 -0
  7. package/dist/lib/init-debug.js +26 -0
  8. package/dist/lib/log-store.d.ts +11 -0
  9. package/dist/lib/log-store.js +22 -0
  10. package/dist/lib/settings/adopt-global-settings.d.ts +20 -0
  11. package/dist/lib/settings/adopt-global-settings.js +38 -0
  12. package/dist/lib/settings/editor-props.d.ts +7 -0
  13. package/dist/lib/settings/editor-props.js +1 -0
  14. package/dist/lib/settings/index.d.ts +7 -0
  15. package/dist/lib/settings/index.js +6 -0
  16. package/dist/lib/settings/register-boolean-setting.d.ts +8 -0
  17. package/dist/lib/settings/register-boolean-setting.js +13 -0
  18. package/dist/lib/settings/register-enum-setting.d.ts +9 -0
  19. package/dist/lib/settings/register-enum-setting.js +14 -0
  20. package/dist/lib/settings/register-setting.d.ts +18 -0
  21. package/dist/lib/settings/register-setting.js +66 -0
  22. package/dist/lib/settings/register-string-setting.d.ts +9 -0
  23. package/dist/lib/settings/register-string-setting.js +16 -0
  24. package/dist/lib/settings/reset-all-settings.d.ts +2 -0
  25. package/dist/lib/settings/reset-all-settings.js +4 -0
  26. package/dist/lib/settings/setting-registry.d.ts +4 -0
  27. package/dist/lib/settings/setting-registry.js +2 -0
  28. package/dist/lib/ui/boolean-editor.d.ts +3 -0
  29. package/dist/lib/ui/boolean-editor.js +6 -0
  30. package/dist/lib/ui/debug-overlay.d.ts +7 -0
  31. package/dist/lib/ui/debug-overlay.js +7 -0
  32. package/dist/lib/ui/debug-tabs.d.ts +6 -0
  33. package/dist/lib/ui/debug-tabs.js +6 -0
  34. package/dist/lib/ui/debug-ui.d.ts +7 -0
  35. package/dist/lib/ui/debug-ui.js +34 -0
  36. package/dist/lib/ui/enum-editor.d.ts +5 -0
  37. package/dist/lib/ui/enum-editor.js +16 -0
  38. package/dist/lib/ui/index.d.ts +2 -0
  39. package/dist/lib/ui/index.js +1 -0
  40. package/dist/lib/ui/log-tab.d.ts +2 -0
  41. package/dist/lib/ui/log-tab.js +46 -0
  42. package/dist/lib/ui/render-debug-ui.d.ts +2 -0
  43. package/dist/lib/ui/render-debug-ui.js +42 -0
  44. package/dist/lib/ui/settings-item.d.ts +6 -0
  45. package/dist/lib/ui/settings-item.js +5 -0
  46. package/dist/lib/ui/settings-list.d.ts +3 -0
  47. package/dist/lib/ui/settings-list.js +9 -0
  48. package/dist/lib/ui/settings-tab.d.ts +2 -0
  49. package/dist/lib/ui/settings-tab.js +13 -0
  50. package/dist/lib/ui/side-button.d.ts +5 -0
  51. package/dist/lib/ui/side-button.js +37 -0
  52. package/dist/lib/ui/string-editor.d.ts +5 -0
  53. package/dist/lib/ui/string-editor.js +7 -0
  54. package/dist/lib/use-log-buffer.d.ts +3 -0
  55. package/dist/lib/use-log-buffer.js +5 -0
  56. package/dist/lib/utils/add-debug-secret-listener.d.ts +2 -0
  57. package/dist/lib/utils/add-debug-secret-listener.js +38 -0
  58. 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.
@@ -0,0 +1,3 @@
1
+ import './lib/init-debug.js';
2
+ export * from './lib/settings/index.js';
3
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import './lib/init-debug.js';
2
+ export * from './lib/settings/index.js';
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=init-debug.d.ts.map
@@ -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,7 @@
1
+ export type EditorProps<T, K = void> = {
2
+ label: string;
3
+ value: T;
4
+ context: K;
5
+ onChange: (value: T) => void;
6
+ };
7
+ //# sourceMappingURL=editor-props.d.ts.map
@@ -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,2 @@
1
+ export declare function resetAllSettings(): void;
2
+ //# sourceMappingURL=reset-all-settings.d.ts.map
@@ -0,0 +1,4 @@
1
+ import { resets } from './setting-registry.js';
2
+ export function resetAllSettings() {
3
+ resets.forEach((reset) => reset());
4
+ }
@@ -0,0 +1,4 @@
1
+ import type { FC } from 'react';
2
+ export declare const editors: FC[];
3
+ export declare const resets: (() => void)[];
4
+ //# sourceMappingURL=setting-registry.d.ts.map
@@ -0,0 +1,2 @@
1
+ export const editors = [];
2
+ export const resets = [];
@@ -0,0 +1,3 @@
1
+ import type { EditorProps } from '../settings/editor-props.js';
2
+ export declare const BooleanEditor: React.FC<EditorProps<boolean>>;
3
+ //# sourceMappingURL=boolean-editor.d.ts.map
@@ -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 '@radix-ui/themes/styles.css';
2
+ interface DebugPanelProps {
3
+ onClose?: () => void;
4
+ }
5
+ export declare const DebugOverlay: ({ onClose }: DebugPanelProps) => import("react/jsx-runtime").JSX.Element;
6
+ export {};
7
+ //# sourceMappingURL=debug-overlay.d.ts.map
@@ -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
+ interface DebugTabsProps {
2
+ onClose?: () => void;
3
+ }
4
+ export declare const DebugTabs: ({ onClose }: DebugTabsProps) => import("react/jsx-runtime").JSX.Element;
5
+ export {};
6
+ //# sourceMappingURL=debug-tabs.d.ts.map
@@ -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,7 @@
1
+ import type { FC } from 'react';
2
+ export declare const DebugUi: FC<{
3
+ open: boolean;
4
+ hostSelector?: string;
5
+ useDebugButtonState: () => [boolean, (n: boolean) => void];
6
+ }>;
7
+ //# sourceMappingURL=debug-ui.d.ts.map
@@ -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,5 @@
1
+ import type { EditorProps } from '../settings/editor-props.js';
2
+ export declare function EnumEditor<T>(props: EditorProps<T, {
3
+ values: readonly T[];
4
+ }>): import("react/jsx-runtime").JSX.Element;
5
+ //# sourceMappingURL=enum-editor.d.ts.map
@@ -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,2 @@
1
+ export { renderDebugUi } from './render-debug-ui.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ export { renderDebugUi } from './render-debug-ui.js';
@@ -0,0 +1,2 @@
1
+ export declare const LogTab: () => import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=log-tab.d.ts.map
@@ -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,2 @@
1
+ export declare function renderDebugUi(): void;
2
+ //# sourceMappingURL=render-debug-ui.d.ts.map
@@ -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,6 @@
1
+ interface SettingsItemProps extends React.PropsWithChildren {
2
+ label: string;
3
+ }
4
+ export declare const SettingsItem: ({ label, children }: SettingsItemProps) => import("react/jsx-runtime").JSX.Element;
5
+ export {};
6
+ //# sourceMappingURL=settings-item.d.ts.map
@@ -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,3 @@
1
+ import type { FC } from 'react';
2
+ export declare const SettingsList: FC;
3
+ //# sourceMappingURL=settings-list.d.ts.map
@@ -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,2 @@
1
+ export declare const SettingsTab: () => import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=settings-tab.d.ts.map
@@ -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,5 @@
1
+ import type { FC } from 'react';
2
+ export declare const SideButton: FC<{
3
+ onClick?: () => void;
4
+ }>;
5
+ //# sourceMappingURL=side-button.d.ts.map
@@ -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,5 @@
1
+ import type { EditorProps } from '../settings/editor-props.js';
2
+ export declare const StringEditor: React.FC<EditorProps<string, {
3
+ placeholder?: string;
4
+ }>>;
5
+ //# sourceMappingURL=string-editor.d.ts.map
@@ -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,3 @@
1
+ import { type LogEntry } from './log-store.js';
2
+ export declare function useLogBuffer(): readonly LogEntry[];
3
+ //# sourceMappingURL=use-log-buffer.d.ts.map
@@ -0,0 +1,5 @@
1
+ import { useSyncExternalStore } from 'react';
2
+ import { getLogSnapshot, subscribeLogStore } from './log-store.js';
3
+ export function useLogBuffer() {
4
+ return useSyncExternalStore(subscribeLogStore, getLogSnapshot, getLogSnapshot);
5
+ }
@@ -0,0 +1,2 @@
1
+ export declare function addDebugSecretListener(callback: () => void): () => void;
2
+ //# sourceMappingURL=add-debug-secret-listener.d.ts.map
@@ -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
+ }