legend-state-dev-tools 0.0.2 → 0.0.4

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 DELETED
@@ -1,142 +0,0 @@
1
- # legend-state-dev-tools
2
-
3
- > **Early-stage project** -- I discovered Legend State recently and chose it for a project I was working on. It had everything I needed from a state manager, but I couldn't live without dev tools, so I built this. It appears to work, but it hasn't been thoroughly tested yet -- consider it a proof of concept for now.
4
-
5
- [![npm version](https://img.shields.io/npm/v/legend-state-dev-tools)](https://www.npmjs.com/package/legend-state-dev-tools)
6
- [![license](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
7
-
8
- Visual dev tools for Legend State v3 -- inspect and edit observable state in real time.
9
-
10
- <!-- screenshot -->
11
-
12
- ## Features
13
-
14
- - Real-time state tree view powered by `json-edit-react`
15
- - Inline editing of observable values
16
- - Multiple color themes (dark and light variants)
17
- - Draggable toolbar
18
- - Configurable panel positioning (left or right)
19
- - Read-only mode
20
- - Clean teardown via `destroy()`
21
-
22
- ## Installation
23
-
24
- ```bash
25
- npm install legend-state-dev-tools
26
- ```
27
-
28
- ```bash
29
- pnpm add legend-state-dev-tools
30
- ```
31
-
32
- ```bash
33
- yarn add legend-state-dev-tools
34
- ```
35
-
36
- ### Peer dependencies
37
-
38
- | Package | Version |
39
- |---------|---------|
40
- | `@legendapp/state` | `>= 3.0.0-beta.0` |
41
- | `react` | `>= 18.0.0` |
42
- | `react-dom` | `>= 18.0.0` |
43
-
44
- ## Quick Start
45
-
46
- ```ts
47
- import { observable } from '@legendapp/state';
48
- import { init } from 'legend-state-dev-tools';
49
- import 'legend-state-dev-tools/dist/styles.css';
50
-
51
- const state$ = observable({ count: 0, user: { name: 'Alice' } });
52
-
53
- const devtools = init(state$);
54
-
55
- // Later, to clean up:
56
- // devtools.destroy();
57
- ```
58
-
59
- ## API Reference
60
-
61
- ### `init(observable$, options?)`
62
-
63
- Mounts the dev tools UI and returns a handle for cleanup.
64
-
65
- **Parameters**
66
-
67
- | Parameter | Type | Description |
68
- |-----------|------|-------------|
69
- | `observable$` | `ObservableParam<any>` | The Legend State observable to inspect |
70
- | `options` | `DevToolsOptions` | Optional configuration (see below) |
71
-
72
- **Options**
73
-
74
- | Option | Type | Default | Description |
75
- |--------|------|---------|-------------|
76
- | `enabled` | `boolean` | `true` | Enable or disable the dev tools |
77
- | `readOnly` | `boolean` | `false` | Prevent editing of state values |
78
- | `theme` | `string` | `'githubDark'` | Color theme for the JSON editor |
79
- | `rootName` | `string` | `'state$'` | Label shown as the root node name |
80
- | `position` | `'left' \| 'right'` | `'right'` | Side of the screen for the panel |
81
-
82
- **Returns**
83
-
84
- ```ts
85
- { destroy: () => void }
86
- ```
87
-
88
- Call `destroy()` to unmount the toolbar, panel, and state subscription.
89
-
90
- ## Themes
91
-
92
- The following themes are available (provided by `json-edit-react`):
93
-
94
- | Theme key | Description |
95
- |-----------|-------------|
96
- | `githubDark` | GitHub dark (default) |
97
- | `githubLight` | GitHub light |
98
- | `monoDark` | Monochrome dark |
99
- | `monoLight` | Monochrome light |
100
-
101
- ## Example
102
-
103
- A working example is included in `examples/basic/`. To run it:
104
-
105
- ```bash
106
- npm install
107
- npm run dev
108
- ```
109
-
110
- This builds the core package and starts the Vite dev server for the example app.
111
-
112
- ## Development
113
-
114
- ```bash
115
- git clone <repo-url>
116
- cd legend-state-dev-tools
117
- npm install
118
- npm run build # build the core package
119
- npm run dev # build + start example dev server
120
- ```
121
-
122
- ## Architecture
123
-
124
- The project is a monorepo with the main package in `packages/core/` and examples in `examples/`.
125
-
126
- | Module | Path | Role |
127
- |--------|------|------|
128
- | `index` | `packages/core/src/index.ts` | Public API (`init`, options, lifecycle) |
129
- | `state-bridge` | `packages/core/src/state-bridge.ts` | Subscribes to observables, produces snapshots, writes back edits |
130
- | `json-editor-mount` | `packages/core/src/ui/json-editor-mount.tsx` | Mounts the `json-edit-react` editor with theme resolution |
131
- | `panel` | `packages/core/src/ui/panel.ts` | Slide-out panel DOM management |
132
- | `toolbar` | `packages/core/src/ui/toolbar.ts` | Draggable floating toolbar |
133
- | `template-engine` | `packages/core/src/ui/template-engine.ts` | Lightweight HTML templating (Eta) |
134
- | `styles` | `packages/core/src/styles.css` | Panel and toolbar CSS |
135
-
136
- ## Acknowledgments
137
-
138
- A huge thank you to [Carlos](https://github.com/CarlosNZ) for creating [`json-edit-react`](https://github.com/CarlosNZ/json-edit-react) -- the excellent React component that powers the state tree viewer and editor in this project. Without it, these dev tools simply wouldn't exist.
139
-
140
- ## License
141
-
142
- MIT
@@ -1,12 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>Legend State Dev Tools - Example</title>
7
- </head>
8
- <body>
9
- <div id="root"></div>
10
- <script type="module" src="/src/main.tsx"></script>
11
- </body>
12
- </html>
@@ -1,24 +0,0 @@
1
- {
2
- "name": "legend-state-dev-tools-example",
3
- "private": true,
4
- "version": "0.0.0",
5
- "type": "module",
6
- "scripts": {
7
- "dev": "vite",
8
- "build": "vite build"
9
- },
10
- "dependencies": {
11
- "@legendapp/state": "^3.0.0-beta.43",
12
- "react": "^18.3.0",
13
- "react-dom": "^18.3.0",
14
- "json-edit-react": "^1.16.0",
15
- "legend-state-dev-tools": "*"
16
- },
17
- "devDependencies": {
18
- "@types/react": "^18.2.0",
19
- "@types/react-dom": "^18.2.0",
20
- "@vitejs/plugin-react": "^4.3.0",
21
- "typescript": "^5.3.0",
22
- "vite": "^6.0.0"
23
- }
24
- }
@@ -1,50 +0,0 @@
1
- import React from 'react';
2
- import { observer } from '@legendapp/state/react';
3
- import { state$ } from './state';
4
-
5
- export const App = observer(function App() {
6
- // In Legend State v3, use .get() to subscribe to values
7
- const count = (state$.count as any).get();
8
- const userName = (state$.user.name as any).get();
9
-
10
- return (
11
- <div style={{ padding: 40, fontFamily: 'sans-serif', maxWidth: 600 }}>
12
- <h1>Legend State Dev Tools Demo</h1>
13
- <p>Open the dev tools panel using the floating button at the bottom-right.</p>
14
-
15
- <div style={{ marginTop: 24, padding: 16, background: '#f5f5f5', borderRadius: 8, color: '#1a1a1a' }}>
16
- <h2>Counter: {count}</h2>
17
- <button onClick={() => (state$.count as any).set((c: number) => c + 1)}>
18
- Increment
19
- </button>
20
- <button onClick={() => (state$.count as any).set(0)} style={{ marginLeft: 8 }}>
21
- Reset
22
- </button>
23
- </div>
24
-
25
- <div style={{ marginTop: 24, padding: 16, background: '#f5f5f5', borderRadius: 8, color: '#1a1a1a' }}>
26
- <h2>User: {userName}</h2>
27
- <input
28
- value={userName}
29
- onChange={(e) => (state$.user.name as any).set(e.target.value)}
30
- style={{ padding: '4px 8px', fontSize: 14 }}
31
- />
32
- </div>
33
-
34
- <div style={{ marginTop: 24, padding: 16, background: '#f5f5f5', borderRadius: 8, color: '#1a1a1a' }}>
35
- <h2>Todos</h2>
36
- <button
37
- onClick={() =>
38
- (state$.todos as any).push({
39
- id: Date.now(),
40
- text: `New todo ${Date.now()}`,
41
- done: false,
42
- })
43
- }
44
- >
45
- Add Todo
46
- </button>
47
- </div>
48
- </div>
49
- );
50
- });
@@ -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,6 +0,0 @@
1
- import { defineConfig } from 'vite';
2
- import react from '@vitejs/plugin-react';
3
-
4
- export default defineConfig({
5
- plugins: [react()],
6
- });
@@ -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,4 +0,0 @@
1
- declare module '*.eta' {
2
- const content: string;
3
- export default content;
4
- }
@@ -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
- }