npm package: @node-tree/state
Description: Reactive state management for virtual file systems
Author: Anderson D. Rosa
Latest version: 0.0.1

# @node-tree/state

Reactive state management for virtual file systems. This package provides a complete tree state solution that mirrors any VFS implementation from `@firesystem/core`, with built-in support for navigation, selection, and expansion states.

## Features

- 🎯 **VFS Mirroring** - Automatically syncs with any firesystem implementation
- 🚀 **Framework Agnostic** - Works with Zustand, MobX, or vanilla JS
- 📁 **Tree Operations** - Navigation, selection, expansion with keyboard-friendly APIs
- 👀 **Real-time Sync** - Auto-updates when files change via watch system
- 🔍 **Smart Queries** - Efficient getters for visible nodes, children, parents
- ✅ **Fully Typed** - Complete TypeScript support
- 🧪 **Well Tested** - Comprehensive unit and integration tests

## Installation

```bash
npm install @node-tree/state @firesystem/core
# or
pnpm add @node-tree/state @firesystem/core
# or
yarn add @node-tree/state @firesystem/core
```

Choose your preferred state management library:
```bash
# For Zustand (recommended)
npm install zustand

# For MobX
npm install mobx mobx-react-lite
```

## Quick Start

### With Zustand (Recommended)

```typescript
import { createZustandTreeStore } from "@node-tree/state";
import { MemoryFileSystem } from "@firesystem/memory";

// Create store with optional file system
const fs = new MemoryFileSystem();
const useTreeStore = createZustandTreeStore(fs);

// Use in React component
function FileExplorer() {
  const { 
    visibleNodes, 
    cursor, 
    expandNode, 
    collapseNode,
    moveCursorDown,
    moveCursorUp 
  } = useTreeStore();
  
  const nodes = useTreeStore(state => state.getVisibleNodes());
  
  return (
    <div>
      {nodes.map(node => (
        <div 
          key={node.id}
          style={{ paddingLeft: node.level * 20 }}
          className={cursor === node.id ? 'selected' : ''}
        >
          {node.type === 'directory' && (
            <button onClick={() => toggleNode(node.id)}>
              {isExpanded(node.id) ? '▼' : '▶'}
            </button>
          )}
          {node.name}
        </div>
      ))}
    </div>
  );
}
```

### With MobX

```typescript
import { createMobXTreeStore } from "@node-tree/state";
import { observer } from "mobx-react-lite";

const treeStore = createMobXTreeStore(fs);

const FileExplorer = observer(() => {
  const nodes = treeStore.getVisibleNodes();
  
  return (
    <div>
      {nodes.map(node => (
        <div key={node.id}>
          {node.name}
        </div>
      ))}
    </div>
  );
});
```

### Vanilla JavaScript

```typescript
import { TreeStateCore } from "@node-tree/state";

const treeState = new TreeStateCore();

// Load from file entries
treeState.loadFromFs([
  { path: "/src", name: "src", type: "directory" },
  { path: "/src/index.ts", name: "index.ts", type: "file" }
]);

// Navigate
treeState.moveCursorDown();
treeState.expandNode("src");

// Query
const visibleNodes = treeState.getVisibleNodes();
```

## Core Concepts

### Tree Node Structure

```typescript
interface TreeNode {
  id: string;              // Unique identifier
  path: string;            // Full path
  name: string;            // File/directory name
  type: "file" | "directory";
  level: number;           // Depth in tree
  parentId?: string;       // Parent node ID
  expanded?: boolean;      // For directories
  // ... other FileEntry properties
}
```

### Navigation Actions

```typescript
// Cursor movement
store.moveCursorUp();
store.moveCursorDown();
store.moveCursorToFirst();
store.moveCursorToLast();
store.setCursor(nodeId);

// Expansion
store.expandNode(nodeId);
store.collapseNode(nodeId);
store.toggleNode(nodeId);
store.expandAll();
store.collapseAll();
store.expandPath("/src/components"); // Expands all parents

// Selection
store.selectNode(nodeId);
store.deselectNode(nodeId);
store.toggleSelection(nodeId);
store.selectAll();
store.clearSelection();
store.selectRange(fromId, toId); // Select visible nodes in range
```

### Queries

```typescript
// Get nodes
const node = store.getNode(nodeId);
const node = store.getNodeByPath("/src/index.ts");
const children = store.getChildren(nodeId);
const parent = store.getParent(nodeId);
const siblings = store.getSiblings(nodeId);

// Get collections
const visible = store.getVisibleNodes();
const selected = store.getSelectedNodes();
const cursor = store.getCursorNode();

// Navigation helpers
const next = store.getNextVisible(nodeId);
const prev = store.getPreviousVisible(nodeId);

// State checks
const expanded = store.isExpanded(nodeId);
const selected = store.isSelected(nodeId);
const visible = store.isVisible(nodeId);
const hasKids = store.hasChildren(nodeId);
```

## File System Integration

The state automatically syncs with the file system when provided:

```typescript
const store = createZustandTreeStore(fs);

// Watches all changes
fs.writeFile("/new-file.txt", "content");
// State auto-updates

fs.mkdir("/new-folder");
// State auto-updates

fs.deleteFile("/old-file.txt");
// State auto-updates

// Manual sync for specific events
store.syncWithFs({
  type: "created",
  path: "/manual-file.txt"
});

// Load from entries
const entries = await fs.readDir("/");
store.loadFromFs(entries);
```

## Advanced Usage

### Custom Selectors (Zustand)

```typescript
import { treeSelectors } from "@node-tree/state";

// Use predefined selectors
const visibleNodes = useTreeStore(treeSelectors.visibleNodes);
const cursorNode = useTreeStore(treeSelectors.cursorNode);
const children = useTreeStore(treeSelectors.childrenOf("src"));

// Create custom selectors
const fileCount = useTreeStore(state => 
  Array.from(state.nodes.values()).filter(n => n.type === "file").length
);
```

### Keyboard Navigation Example

```typescript
function useKeyboardNavigation(store) {
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      switch (e.key) {
        case "ArrowUp":
          e.preventDefault();
          store.moveCursorUp();
          break;
        case "ArrowDown":
          e.preventDefault();
          store.moveCursorDown();
          break;
        case "ArrowRight":
          const cursor = store.getCursorNode();
          if (cursor?.type === "directory") {
            store.expandNode(cursor.id);
          }
          break;
        case "ArrowLeft":
          const current = store.getCursorNode();
          if (current?.type === "directory" && store.isExpanded(current.id)) {
            store.collapseNode(current.id);
          } else if (current?.parentId) {
            store.setCursor(current.parentId);
          }
          break;
        case " ":
          e.preventDefault();
          const node = store.getCursorNode();
          if (node) store.toggleSelection(node.id);
          break;
      }
    };
    
    window.addEventListener("keydown", handleKeyDown);
    return () => window.removeEventListener("keydown", handleKeyDown);
  }, [store]);
}
```

### Performance Tips

1. **Use selectors** for computed values instead of filtering in components
2. **Batch operations** when making multiple state changes
3. **Virtualize** long lists of visible nodes (use `@tanstack/react-virtual` or similar)
4. **Debounce** rapid file system changes if needed
5. **Memoize** expensive computations in React components
6. **Use `shallow` equality** in Zustand selectors for better performance

## API Reference

### TreeStore Interface

```typescript
interface TreeStore {
  // State
  nodes: Map<string, TreeNode>;
  rootIds: string[];
  cursor?: string;
  selectedIds: Set<string>;
  expandedIds: Set<string>;
  visibleIds: string[];
  
  // All methods listed above...
}
```

### Store Creation Options

```typescript
// Zustand
const store = createZustandTreeStore(fs?: IFileSystem);

// MobX  
const store = createMobXTreeStore(fs?: IFileSystem);

// Vanilla (no reactivity)
const store = new TreeStateCore();
```

## React Integration Examples

### Creating a File Explorer Component

```typescript
// FileExplorer.tsx
import { useTreeStore } from './store';
import { useKeyboardNavigation } from './hooks';

export function FileExplorer() {
  const visibleNodes = useTreeStore(state => state.getVisibleNodes());
  const cursor = useTreeStore(state => state.cursor);
  const { expandNode, collapseNode, setCursor } = useTreeStore();
  
  useKeyboardNavigation();
  
  return (
    <div className="file-explorer">
      {visibleNodes.map(node => (
        <FileNode
          key={node.id}
          node={node}
          isCursor={cursor === node.id}
          onToggle={() => node.type === 'directory' && toggleNode(node.id)}
          onClick={() => setCursor(node.id)}
        />
      ))}
    </div>
  );
}
```

### Using with Zustand DevTools

```typescript
import { devtools } from 'zustand/middleware';

const useTreeStore = create<TreeStore>()(
  devtools(
    subscribeWithSelector((set, get) => ({
      // ... your store
    })),
    { name: 'tree-store' }
  )
);
```

## Common Patterns

### Lazy Loading Directories

```typescript
// Only load directory contents when expanded
const handleExpand = async (nodeId: string) => {
  const node = store.getNode(nodeId);
  if (node?.type === 'directory' && !node.children?.length) {
    const children = await fs.readDir(node.path);
    // Update store with new children
  }
  store.expandNode(nodeId);
};
```

### Search Implementation

```typescript
// Add search functionality
const searchNodes = (query: string) => {
  const results: TreeNode[] = [];
  for (const node of store.nodes.values()) {
    if (node.name.toLowerCase().includes(query.toLowerCase())) {
      results.push(node);
      // Expand parent path to show result
      store.expandPath(node.path);
    }
  }
  return results;
};
```

## Troubleshooting

### Issue: State not updating in React component
**Solution**: Make sure you're using the store correctly:
```typescript
// ❌ Wrong - not reactive
const nodes = store.getVisibleNodes();

// ✅ Correct - reactive subscription
const nodes = useTreeStore(state => state.getVisibleNodes());
```

### Issue: Too many re-renders
**Solution**: Use selectors to minimize updates:
```typescript
// ❌ Causes re-render on any state change
const state = useTreeStore();

// ✅ Only re-renders when visible nodes change
const visibleNodes = useTreeStore(state => state.getVisibleNodes());
```

### Issue: File system changes not reflecting
**Solution**: Ensure filesystem has watch support:
```typescript
// Check if watch is working
const watcher = fs.watch('**', (event) => {
  console.log('File changed:', event);
});
```

### Issue: Performance with large directories
**Solution**: Implement virtualization:
```typescript
import { useVirtualizer } from '@tanstack/react-virtual';

// Only render visible items
const virtualizer = useVirtualizer({
  count: visibleNodes.length,
  getScrollElement: () => parentRef.current,
  estimateSize: () => 24,
});
```

## Contributing

See [ARCHITECTURE.md](./ARCHITECTURE.md) for detailed technical documentation.

## License

MIT