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.
@@ -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
- }
@@ -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">&times;</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>