legend-state-dev-tools 0.0.2 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +16 -1
- package/packages/core/dist/index.d.ts +17 -0
- package/packages/core/dist/index.js +28 -0
- package/packages/core/dist/index.js.map +1 -0
- package/packages/core/dist/index.mjs +349 -0
- package/packages/core/dist/index.mjs.map +1 -0
- package/examples/basic/index.html +0 -12
- package/examples/basic/package.json +0 -24
- package/examples/basic/src/App.tsx +0 -50
- package/examples/basic/src/main.tsx +0 -16
- package/examples/basic/src/state.ts +0 -17
- package/examples/basic/tsconfig.json +0 -15
- package/examples/basic/vite.config.ts +0 -6
- package/packages/core/package.json +0 -40
- package/packages/core/src/eta.d.ts +0 -4
- package/packages/core/src/index.ts +0 -130
- package/packages/core/src/state-bridge.ts +0 -56
- package/packages/core/src/ui/json-editor-mount.tsx +0 -144
- package/packages/core/src/ui/panel.ts +0 -96
- package/packages/core/src/ui/shared-utils.ts +0 -49
- package/packages/core/src/ui/template-engine.ts +0 -55
- package/packages/core/src/ui/templates/panel.eta +0 -12
- package/packages/core/src/ui/templates/toolbar.eta +0 -13
- package/packages/core/src/ui/toolbar.ts +0 -108
- package/packages/core/tsconfig.json +0 -9
- package/packages/core/vite.config.ts +0 -71
- package/tsconfig.json +0 -17
- /package/packages/core/{src → dist}/styles.css +0 -0
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { createRoot } from 'react-dom/client';
|
|
3
|
-
import { init } from 'legend-state-dev-tools';
|
|
4
|
-
import 'legend-state-dev-tools/dist/styles.css';
|
|
5
|
-
import { state$ } from './state';
|
|
6
|
-
import { App } from './App';
|
|
7
|
-
|
|
8
|
-
// Initialize dev tools
|
|
9
|
-
init(state$, {
|
|
10
|
-
rootName: 'state$',
|
|
11
|
-
theme: 'githubDark',
|
|
12
|
-
readOnly: false,
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
const root = createRoot(document.getElementById('root')!);
|
|
16
|
-
root.render(<App />);
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { observable } from '@legendapp/state';
|
|
2
|
-
|
|
3
|
-
export const state$ = observable({
|
|
4
|
-
count: 0,
|
|
5
|
-
user: {
|
|
6
|
-
name: 'Alice',
|
|
7
|
-
email: 'alice@example.com',
|
|
8
|
-
preferences: {
|
|
9
|
-
darkMode: true,
|
|
10
|
-
notifications: true,
|
|
11
|
-
},
|
|
12
|
-
},
|
|
13
|
-
todos: [
|
|
14
|
-
{ id: 1, text: 'Learn Legend State', done: true },
|
|
15
|
-
{ id: 2, text: 'Try dev tools', done: false },
|
|
16
|
-
],
|
|
17
|
-
});
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
-
"moduleResolution": "bundler",
|
|
7
|
-
"strict": true,
|
|
8
|
-
"skipLibCheck": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"jsx": "react-jsx",
|
|
11
|
-
"resolveJsonModule": true,
|
|
12
|
-
"isolatedModules": true
|
|
13
|
-
},
|
|
14
|
-
"include": ["src"]
|
|
15
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "legend-state-dev-tools",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"description": "Dev tools for Legend State v3 - view and edit observable state",
|
|
5
|
-
"main": "./dist/index.js",
|
|
6
|
-
"module": "./dist/index.mjs",
|
|
7
|
-
"types": "./dist/index.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"types": "./dist/index.d.ts",
|
|
11
|
-
"import": "./dist/index.mjs",
|
|
12
|
-
"require": "./dist/index.js",
|
|
13
|
-
"default": "./dist/index.mjs"
|
|
14
|
-
},
|
|
15
|
-
"./dist/styles.css": "./dist/styles.css"
|
|
16
|
-
},
|
|
17
|
-
"files": [
|
|
18
|
-
"dist"
|
|
19
|
-
],
|
|
20
|
-
"scripts": {
|
|
21
|
-
"build": "vite build",
|
|
22
|
-
"dev": "vite build --watch"
|
|
23
|
-
},
|
|
24
|
-
"devDependencies": {
|
|
25
|
-
"@types/react": "^18.2.0",
|
|
26
|
-
"@types/react-dom": "^18.2.0",
|
|
27
|
-
"typescript": "^5.3.0",
|
|
28
|
-
"vite": "^6.0.0",
|
|
29
|
-
"vite-plugin-dts": "^4.0.0"
|
|
30
|
-
},
|
|
31
|
-
"peerDependencies": {
|
|
32
|
-
"@legendapp/state": ">=3.0.0-beta.0",
|
|
33
|
-
"react": ">=18.0.0",
|
|
34
|
-
"react-dom": ">=18.0.0"
|
|
35
|
-
},
|
|
36
|
-
"dependencies": {
|
|
37
|
-
"eta": "^3.5.0",
|
|
38
|
-
"json-edit-react": "^1.16.0"
|
|
39
|
-
}
|
|
40
|
-
}
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import type { ObservableParam } from '@legendapp/state';
|
|
2
|
-
import { Toolbar } from './ui/toolbar';
|
|
3
|
-
import { Panel } from './ui/panel';
|
|
4
|
-
import { createStateBridge, type StateBridge } from './state-bridge';
|
|
5
|
-
import { mountJsonEditor, type JsonEditorBridge } from './ui/json-editor-mount';
|
|
6
|
-
import { createCleanup } from './ui/shared-utils';
|
|
7
|
-
|
|
8
|
-
export interface DevToolsOptions {
|
|
9
|
-
enabled?: boolean;
|
|
10
|
-
readOnly?: boolean;
|
|
11
|
-
theme?: string;
|
|
12
|
-
rootName?: string;
|
|
13
|
-
position?: 'left' | 'right';
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface DevTools {
|
|
17
|
-
destroy: () => void;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function init(
|
|
21
|
-
observable$: ObservableParam<any>,
|
|
22
|
-
options: DevToolsOptions = {}
|
|
23
|
-
): DevTools {
|
|
24
|
-
const {
|
|
25
|
-
enabled = true,
|
|
26
|
-
readOnly = false,
|
|
27
|
-
theme = 'githubDark',
|
|
28
|
-
rootName = 'state$',
|
|
29
|
-
position = 'right',
|
|
30
|
-
} = options;
|
|
31
|
-
|
|
32
|
-
if (!enabled) {
|
|
33
|
-
return { destroy: () => {} };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const cleanup = createCleanup();
|
|
37
|
-
let panel: Panel | null = null;
|
|
38
|
-
let toolbar: Toolbar | null = null;
|
|
39
|
-
let bridge: StateBridge | null = null;
|
|
40
|
-
let editorBridge: JsonEditorBridge | null = null;
|
|
41
|
-
|
|
42
|
-
// Create panel
|
|
43
|
-
panel = new Panel({
|
|
44
|
-
rootName,
|
|
45
|
-
readOnly,
|
|
46
|
-
position,
|
|
47
|
-
onClose: () => {
|
|
48
|
-
hidePanel();
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const showPanel = () => {
|
|
53
|
-
if (!panel) return;
|
|
54
|
-
panel.show();
|
|
55
|
-
toolbar?.setPanelVisible(true);
|
|
56
|
-
|
|
57
|
-
// Poll for editor root element (innerHTML may not be ready immediately)
|
|
58
|
-
const tryMount = (retries = 10) => {
|
|
59
|
-
const editorRoot = panel?.getEditorRoot();
|
|
60
|
-
if (!editorRoot) {
|
|
61
|
-
if (retries > 0) {
|
|
62
|
-
setTimeout(() => tryMount(retries - 1), 16);
|
|
63
|
-
} else {
|
|
64
|
-
console.warn('[Legend State DevTools] Could not find #lsdt-json-editor-root after retries');
|
|
65
|
-
}
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
if (editorBridge) return;
|
|
69
|
-
|
|
70
|
-
const initialData = bridge?.getSnapshot() ?? {};
|
|
71
|
-
|
|
72
|
-
editorBridge = mountJsonEditor(editorRoot, {
|
|
73
|
-
initialData,
|
|
74
|
-
onEdit: (newData: unknown) => {
|
|
75
|
-
bridge?.setData(newData);
|
|
76
|
-
},
|
|
77
|
-
readOnly,
|
|
78
|
-
theme,
|
|
79
|
-
rootName,
|
|
80
|
-
});
|
|
81
|
-
};
|
|
82
|
-
tryMount();
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
const hidePanel = () => {
|
|
86
|
-
if (editorBridge) {
|
|
87
|
-
editorBridge.destroy();
|
|
88
|
-
editorBridge = null;
|
|
89
|
-
}
|
|
90
|
-
panel?.hide();
|
|
91
|
-
toolbar?.setPanelVisible(false);
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
const togglePanel = () => {
|
|
95
|
-
if (panel?.isVisible()) {
|
|
96
|
-
hidePanel();
|
|
97
|
-
} else {
|
|
98
|
-
showPanel();
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
// Create toolbar
|
|
103
|
-
toolbar = new Toolbar({
|
|
104
|
-
onTogglePanel: togglePanel,
|
|
105
|
-
rootName,
|
|
106
|
-
});
|
|
107
|
-
toolbar.mount();
|
|
108
|
-
cleanup.add(() => toolbar?.unmount());
|
|
109
|
-
|
|
110
|
-
// Create state bridge
|
|
111
|
-
bridge = createStateBridge(observable$, {
|
|
112
|
-
onSnapshot: (snapshot) => {
|
|
113
|
-
editorBridge?.updateData(snapshot);
|
|
114
|
-
},
|
|
115
|
-
});
|
|
116
|
-
cleanup.add(() => bridge?.destroy());
|
|
117
|
-
cleanup.add(() => {
|
|
118
|
-
if (editorBridge) {
|
|
119
|
-
editorBridge.destroy();
|
|
120
|
-
editorBridge = null;
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
cleanup.add(() => panel?.unmount());
|
|
124
|
-
|
|
125
|
-
return {
|
|
126
|
-
destroy: () => {
|
|
127
|
-
cleanup.run();
|
|
128
|
-
},
|
|
129
|
-
};
|
|
130
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import type { ObservableParam } from '@legendapp/state';
|
|
2
|
-
|
|
3
|
-
export interface StateBridgeOptions {
|
|
4
|
-
onSnapshot: (snapshot: unknown) => void;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export interface StateBridge {
|
|
8
|
-
getSnapshot: () => unknown;
|
|
9
|
-
setData: (newData: unknown) => void;
|
|
10
|
-
destroy: () => void;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function createStateBridge(
|
|
14
|
-
observable$: ObservableParam<any>,
|
|
15
|
-
options: StateBridgeOptions
|
|
16
|
-
): StateBridge {
|
|
17
|
-
// Get initial snapshot
|
|
18
|
-
const getSnapshot = () => {
|
|
19
|
-
try {
|
|
20
|
-
return JSON.parse(JSON.stringify((observable$ as any).peek()));
|
|
21
|
-
} catch {
|
|
22
|
-
return undefined;
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
// Subscribe to changes using onChange
|
|
27
|
-
let dispose: (() => void) | null = null;
|
|
28
|
-
try {
|
|
29
|
-
dispose = (observable$ as any).onChange(
|
|
30
|
-
() => {
|
|
31
|
-
const snapshot = getSnapshot();
|
|
32
|
-
options.onSnapshot(snapshot);
|
|
33
|
-
},
|
|
34
|
-
{ trackingType: false }
|
|
35
|
-
);
|
|
36
|
-
} catch {
|
|
37
|
-
console.warn('[Legend State DevTools] Could not subscribe to observable changes via onChange');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return {
|
|
41
|
-
getSnapshot,
|
|
42
|
-
setData: (newData: unknown) => {
|
|
43
|
-
try {
|
|
44
|
-
(observable$ as any).set(newData);
|
|
45
|
-
} catch (e) {
|
|
46
|
-
console.error('[Legend State DevTools] Failed to set data:', e);
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
destroy: () => {
|
|
50
|
-
if (dispose) {
|
|
51
|
-
dispose();
|
|
52
|
-
dispose = null;
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
}
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import React, { Component, useEffect, useState, type ErrorInfo, type ReactNode } from 'react';
|
|
2
|
-
import { createRoot, type Root } from 'react-dom/client';
|
|
3
|
-
import { JsonEditor, githubDarkTheme, githubLightTheme, monoDarkTheme, monoLightTheme } from 'json-edit-react';
|
|
4
|
-
|
|
5
|
-
const themeMap: Record<string, object> = {
|
|
6
|
-
githubDark: githubDarkTheme,
|
|
7
|
-
githubLight: githubLightTheme,
|
|
8
|
-
monoDark: monoDarkTheme,
|
|
9
|
-
monoLight: monoLightTheme,
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
class ErrorBoundary extends Component<
|
|
13
|
-
{ children: ReactNode },
|
|
14
|
-
{ error: Error | null }
|
|
15
|
-
> {
|
|
16
|
-
state = { error: null as Error | null };
|
|
17
|
-
static getDerivedStateFromError(error: Error) {
|
|
18
|
-
return { error };
|
|
19
|
-
}
|
|
20
|
-
componentDidCatch(error: Error, info: ErrorInfo) {
|
|
21
|
-
console.error('[Legend State DevTools] React error:', error, info);
|
|
22
|
-
}
|
|
23
|
-
render() {
|
|
24
|
-
if (this.state.error) {
|
|
25
|
-
return React.createElement(
|
|
26
|
-
'pre',
|
|
27
|
-
{ style: { color: '#ff6b6b', padding: 16, fontSize: 12 } },
|
|
28
|
-
`DevTools Error: ${this.state.error.message}\n${this.state.error.stack}`
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
return this.props.children;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface JsonEditorWrapperProps {
|
|
36
|
-
data: unknown;
|
|
37
|
-
onEdit: (newData: unknown) => void;
|
|
38
|
-
readOnly: boolean;
|
|
39
|
-
theme: string;
|
|
40
|
-
rootName: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function JsonEditorWrapper({
|
|
44
|
-
data,
|
|
45
|
-
onEdit,
|
|
46
|
-
readOnly,
|
|
47
|
-
theme,
|
|
48
|
-
rootName,
|
|
49
|
-
}: JsonEditorWrapperProps) {
|
|
50
|
-
const resolvedTheme = themeMap[theme] ?? githubDarkTheme;
|
|
51
|
-
return (
|
|
52
|
-
<JsonEditor
|
|
53
|
-
data={data as Record<string, unknown>}
|
|
54
|
-
setData={onEdit as any}
|
|
55
|
-
rootName={rootName}
|
|
56
|
-
theme={resolvedTheme as any}
|
|
57
|
-
collapse={2}
|
|
58
|
-
restrictEdit={readOnly}
|
|
59
|
-
restrictDelete={readOnly}
|
|
60
|
-
restrictAdd={readOnly}
|
|
61
|
-
restrictTypeSelection={readOnly ? true : undefined}
|
|
62
|
-
/>
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface JsonEditorBridge {
|
|
67
|
-
updateData: (data: unknown) => void;
|
|
68
|
-
destroy: () => void;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Wrapper component that receives data via a callback registration
|
|
72
|
-
function JsonEditorBridgeWrapper(props: {
|
|
73
|
-
initialData: unknown;
|
|
74
|
-
onEdit: (newData: unknown) => void;
|
|
75
|
-
readOnly: boolean;
|
|
76
|
-
theme: string;
|
|
77
|
-
rootName: string;
|
|
78
|
-
registerUpdater: (updater: (data: unknown) => void) => void;
|
|
79
|
-
}) {
|
|
80
|
-
const [data, setData] = useState<unknown>(props.initialData);
|
|
81
|
-
|
|
82
|
-
useEffect(() => {
|
|
83
|
-
props.registerUpdater((newData: unknown) => {
|
|
84
|
-
setData(newData);
|
|
85
|
-
});
|
|
86
|
-
}, []);
|
|
87
|
-
|
|
88
|
-
const handleEdit = (newData: unknown) => {
|
|
89
|
-
setData(newData);
|
|
90
|
-
props.onEdit(newData);
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
return (
|
|
94
|
-
<JsonEditorWrapper
|
|
95
|
-
data={data}
|
|
96
|
-
onEdit={handleEdit}
|
|
97
|
-
readOnly={props.readOnly}
|
|
98
|
-
theme={props.theme}
|
|
99
|
-
rootName={props.rootName}
|
|
100
|
-
/>
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function mountJsonEditor(
|
|
105
|
-
container: HTMLElement,
|
|
106
|
-
options: {
|
|
107
|
-
initialData: unknown;
|
|
108
|
-
onEdit: (newData: unknown) => void;
|
|
109
|
-
readOnly: boolean;
|
|
110
|
-
theme: string;
|
|
111
|
-
rootName: string;
|
|
112
|
-
}
|
|
113
|
-
): JsonEditorBridge {
|
|
114
|
-
let root: Root | null = null;
|
|
115
|
-
let updaterFn: ((data: unknown) => void) | null = null;
|
|
116
|
-
|
|
117
|
-
root = createRoot(container);
|
|
118
|
-
root.render(
|
|
119
|
-
<ErrorBoundary>
|
|
120
|
-
<JsonEditorBridgeWrapper
|
|
121
|
-
initialData={options.initialData}
|
|
122
|
-
onEdit={options.onEdit}
|
|
123
|
-
readOnly={options.readOnly}
|
|
124
|
-
theme={options.theme}
|
|
125
|
-
rootName={options.rootName}
|
|
126
|
-
registerUpdater={(updater) => {
|
|
127
|
-
updaterFn = updater;
|
|
128
|
-
}}
|
|
129
|
-
/>
|
|
130
|
-
</ErrorBoundary>
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
return {
|
|
134
|
-
updateData: (data: unknown) => {
|
|
135
|
-
updaterFn?.(data);
|
|
136
|
-
},
|
|
137
|
-
destroy: () => {
|
|
138
|
-
if (root) {
|
|
139
|
-
root.unmount();
|
|
140
|
-
root = null;
|
|
141
|
-
}
|
|
142
|
-
},
|
|
143
|
-
};
|
|
144
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { renderPanel, type PanelData } from './template-engine';
|
|
2
|
-
|
|
3
|
-
export class Panel {
|
|
4
|
-
private container: HTMLElement | null = null;
|
|
5
|
-
private visible = false;
|
|
6
|
-
private rootName: string;
|
|
7
|
-
private readOnly: boolean;
|
|
8
|
-
private onClose?: () => void;
|
|
9
|
-
private position: 'left' | 'right';
|
|
10
|
-
|
|
11
|
-
constructor(options: {
|
|
12
|
-
rootName?: string;
|
|
13
|
-
readOnly?: boolean;
|
|
14
|
-
onClose?: () => void;
|
|
15
|
-
position?: 'left' | 'right';
|
|
16
|
-
} = {}) {
|
|
17
|
-
this.rootName = options.rootName || 'state$';
|
|
18
|
-
this.readOnly = options.readOnly || false;
|
|
19
|
-
this.onClose = options.onClose;
|
|
20
|
-
this.position = options.position || 'right';
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
public toggle(): void {
|
|
24
|
-
if (this.visible) {
|
|
25
|
-
this.hide();
|
|
26
|
-
} else {
|
|
27
|
-
this.show();
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
public show(): void {
|
|
32
|
-
this.visible = true;
|
|
33
|
-
|
|
34
|
-
if (!this.container) {
|
|
35
|
-
this.container = document.createElement('div');
|
|
36
|
-
this.container.id = 'lsdt-panel';
|
|
37
|
-
if (this.position === 'left') {
|
|
38
|
-
this.container.classList.add('lsdt-panel-left');
|
|
39
|
-
}
|
|
40
|
-
document.body.appendChild(this.container);
|
|
41
|
-
this.attachEventListeners();
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
this.render();
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
public hide(): void {
|
|
48
|
-
this.visible = false;
|
|
49
|
-
this.container?.remove();
|
|
50
|
-
this.container = null;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
public isVisible(): boolean {
|
|
54
|
-
return this.visible;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
public getEditorRoot(): HTMLElement | null {
|
|
58
|
-
return this.container?.querySelector('#lsdt-json-editor-root') || null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
private render(): void {
|
|
62
|
-
if (!this.container) return;
|
|
63
|
-
|
|
64
|
-
const data: PanelData = {
|
|
65
|
-
rootName: this.rootName,
|
|
66
|
-
readOnly: this.readOnly,
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
this.container.innerHTML = renderPanel(data);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
private attachEventListeners(): void {
|
|
73
|
-
if (!this.container) return;
|
|
74
|
-
this.container.addEventListener('click', this.handleClick);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
private handleClick = (e: Event): void => {
|
|
78
|
-
const target = e.target as HTMLElement;
|
|
79
|
-
const actionElement = target.closest('[data-action]');
|
|
80
|
-
if (!actionElement) return;
|
|
81
|
-
|
|
82
|
-
const action = actionElement.getAttribute('data-action');
|
|
83
|
-
if (action === 'close-panel') {
|
|
84
|
-
this.onClose?.();
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
public unmount(): void {
|
|
89
|
-
if (this.container) {
|
|
90
|
-
this.container.removeEventListener('click', this.handleClick);
|
|
91
|
-
this.container.remove();
|
|
92
|
-
this.container = null;
|
|
93
|
-
}
|
|
94
|
-
this.visible = false;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared UI utilities for Legend State Dev Tools
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
const STORAGE_PREFIX = 'lsdt';
|
|
6
|
-
|
|
7
|
-
export function escapeHtml(str: string): string {
|
|
8
|
-
const div = document.createElement('div');
|
|
9
|
-
div.textContent = str;
|
|
10
|
-
return div.innerHTML;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function getStoredBoolean(key: string, defaultValue: boolean): boolean {
|
|
14
|
-
const stored = localStorage.getItem(`${STORAGE_PREFIX}-${key}`);
|
|
15
|
-
if (stored === null) return defaultValue;
|
|
16
|
-
return stored === 'true';
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function setStoredBoolean(key: string, value: boolean): void {
|
|
20
|
-
localStorage.setItem(`${STORAGE_PREFIX}-${key}`, String(value));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function getStoredString(key: string, defaultValue: string): string {
|
|
24
|
-
return localStorage.getItem(`${STORAGE_PREFIX}-${key}`) || defaultValue;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function setStoredString(key: string, value: string): void {
|
|
28
|
-
localStorage.setItem(`${STORAGE_PREFIX}-${key}`, value);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function createCleanup(): {
|
|
32
|
-
add: (fn: () => void) => void;
|
|
33
|
-
run: () => void;
|
|
34
|
-
} {
|
|
35
|
-
const cleanupFns: (() => void)[] = [];
|
|
36
|
-
return {
|
|
37
|
-
add: (fn: () => void) => cleanupFns.push(fn),
|
|
38
|
-
run: () => {
|
|
39
|
-
cleanupFns.forEach((fn) => {
|
|
40
|
-
try {
|
|
41
|
-
fn();
|
|
42
|
-
} catch (e) {
|
|
43
|
-
console.error('[Legend State DevTools] Cleanup error:', e);
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
cleanupFns.length = 0;
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Eta template engine wrapper for Legend State Dev Tools
|
|
3
|
-
*/
|
|
4
|
-
import { Eta } from 'eta';
|
|
5
|
-
|
|
6
|
-
import toolbarTemplate from './templates/toolbar.eta';
|
|
7
|
-
import panelTemplate from './templates/panel.eta';
|
|
8
|
-
|
|
9
|
-
const eta = new Eta({
|
|
10
|
-
autoEscape: true,
|
|
11
|
-
autoTrim: false,
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
const templates: Record<string, string> = {
|
|
15
|
-
toolbar: toolbarTemplate,
|
|
16
|
-
panel: panelTemplate,
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export function renderTemplate<T extends Record<string, unknown>>(
|
|
20
|
-
name: string,
|
|
21
|
-
data: T
|
|
22
|
-
): string {
|
|
23
|
-
const template = templates[name];
|
|
24
|
-
if (!template) {
|
|
25
|
-
console.error(`[LSDT] Template not found: ${name}`);
|
|
26
|
-
return '';
|
|
27
|
-
}
|
|
28
|
-
try {
|
|
29
|
-
return eta.renderString(template, data);
|
|
30
|
-
} catch (error) {
|
|
31
|
-
console.error(`[LSDT] Error rendering template ${name}:`, error);
|
|
32
|
-
return '';
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface ToolbarData {
|
|
37
|
-
[key: string]: unknown;
|
|
38
|
-
isMinimized: boolean;
|
|
39
|
-
panelVisible: boolean;
|
|
40
|
-
rootName: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function renderToolbar(data: ToolbarData): string {
|
|
44
|
-
return renderTemplate('toolbar', data);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface PanelData {
|
|
48
|
-
[key: string]: unknown;
|
|
49
|
-
rootName: string;
|
|
50
|
-
readOnly: boolean;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function renderPanel(data: PanelData): string {
|
|
54
|
-
return renderTemplate('panel', data);
|
|
55
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
<div class="lsdt-panel-header">
|
|
2
|
-
<h3><%= it.rootName %></h3>
|
|
3
|
-
<div class="lsdt-panel-actions">
|
|
4
|
-
<% if (it.readOnly) { %>
|
|
5
|
-
<span class="lsdt-readonly-badge">Read-only</span>
|
|
6
|
-
<% } %>
|
|
7
|
-
<button class="lsdt-close-btn" data-action="close-panel" title="Close">×</button>
|
|
8
|
-
</div>
|
|
9
|
-
</div>
|
|
10
|
-
<div class="lsdt-panel-content">
|
|
11
|
-
<div id="lsdt-json-editor-root"></div>
|
|
12
|
-
</div>
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
<div class="lsdt-toolbar-header">
|
|
2
|
-
<div class="lsdt-toolbar-title">
|
|
3
|
-
<span class="lsdt-toolbar-indicator"></span>
|
|
4
|
-
Legend State
|
|
5
|
-
</div>
|
|
6
|
-
<button
|
|
7
|
-
class="lsdt-toggle-btn <%= it.panelVisible ? 'active' : '' %>"
|
|
8
|
-
data-action="toggle-panel"
|
|
9
|
-
title="<%= it.panelVisible ? 'Hide panel' : 'Show panel' %>"
|
|
10
|
-
>
|
|
11
|
-
<%= it.panelVisible ? 'Hide' : 'Show' %> <%= it.rootName %>
|
|
12
|
-
</button>
|
|
13
|
-
</div>
|