Uma biblioteca headless (sem interface visual) para gerenciamento de árvores de dados reativos, com persistência via @firesystem/core. Fornece toda a lógica de negócio para operações em árvores hierárquicas, permitindo implementações customizadas de UI.
npm install @darksnow-ui/node-tree-headlessUma biblioteca headless (sem interface visual) para gerenciamento de árvores de dados reativos, com persistência via @firesystem/core. Fornece toda a lógica de negócio para operações em árvores hierárquicas, permitindo implementações customizadas de UI.
- 🎯 Arquitetura em Camadas: Controller → Services → FileSystem → Estado Reativo
- 🔄 Estado Reativo: Sincronização automática entre FileSystem e Zustand
- 🔌 Totalmente Extensível: Substitua qualquer serviço ou controller
- 💾 Persistência Automática: Via @firesystem/core (IndexedDB, Memory, etc)
- 🎮 Controle Programático: API rica para todas operações
- ⚡ Performance: Indexação inteligente e otimizações de renderização
- 🧩 Framework Agnostic: Use com React, Vue, Angular ou vanilla JS
``bash`
pnpm add @node-tree/headless @firesystem/core
`typescript
import { createNodeTreeContext } from "@node-tree/headless";
import { IndexedDBFileSystem } from "@firesystem/indexeddb";
// 1. Criar sistema de arquivos para persistência
const fileSystem = new IndexedDBFileSystem({ dbName: "my-tree" });
// 2. Criar contexto da árvore
const context = createNodeTreeContext({
fileSystem,
initialNodes: [
{ id: "root", label: "My Project", expandable: true },
{ id: "src", label: "src", parentId: "root", expandable: true },
{ id: "file1", label: "index.ts", parentId: "src" }
]
});
// 3. Usar o controller para operações
const controller = context.controller;
// Criar arquivo
await controller.createNode("src", "new-file.ts", false);
// Navegar
controller.navigate("down");
controller.toggleExpand();
// Selecionar
controller.selectAll();
controller.clearSelection();
`
``
Eventos (teclado/mouse/API)
↓
Controller (validações, hooks)
↓
Services
/ \
FileSystem NavigationService
↓ ↓
(persist) Estado Zustand
(reativo)
1. Controller como Interface Única: Toda operação passa pelo controller
2. Services com Responsabilidade Única: Cada serviço tem um propósito específico
3. FileSystem como Fonte da Verdade: Dados persistentes sempre no FileSystem
4. Estado Reativo ao FileSystem: Zustand sincroniza automaticamente
5. Navegação Modifica Apenas Estado Efêmero: Cursor, seleção, expansão
`typescript`
interface INodeStateService {
getNode(nodeId: string): Node | undefined;
getNodes(): Map
setNodes(nodes: NodeItem[]): void;
addOrUpdateNode(node: Node): void;
removeNode(nodeId: string): void;
isNodeAncestorOf(ancestorId: string, nodeId: string): boolean;
}
`typescript`
interface INodeOperationService {
createNode(parentId: string, label: string, expandable?: boolean): Promise
updateNode(nodeId: string, updates: Partial
deleteNodes(nodeIds: string[]): Promise
moveNodes(nodeIds: string[], targetId?: string): Promise
renameNode(nodeId: string, newName: string): Promise
}
`typescript`
interface INavigationService {
// Cursor
setCursor(nodeId: string): void;
moveCursorNext(): void;
moveCursorPrev(): void;
moveCursorParent(): void;
// Expansão
expandNode(nodeId: string): void;
collapseNode(nodeId: string): void;
toggleDir(nodeId: string): void;
// Seleção
selectNode(nodeId: string): void;
toggleSelectNode(nodeId: string): void;
selectAll(): void;
clearSelection(): void;
}
`typescript`
interface IDOMQueryService {
getNodeElementRect(nodeId: string): DOMRect | null;
getContextMenuPosition(nodeId: string): { x: number; y: number };
scrollNodeIntoView(nodeId: string): void;
}
O controller é a interface pública principal, coordenando todas as operações.
`typescript
// Navegação
controller.navigate("up" | "down" | "left" | "right" | "start" | "end");
controller.setCursor(nodeId);
// Expansão
controller.toggleExpand(nodeId?);
controller.expandAll();
controller.collapseAll();
// Seleção
controller.toggleSelect(nodeId?);
controller.selectAll();
controller.clearSelection();
// CRUD
await controller.createNode(parentId?, label?, expandable?);
await controller.deleteNodes(nodeIds?);
await controller.renameNode(nodeId?, newName?);
await controller.moveNodes(nodeIds, targetId);
// Clipboard
controller.copy(nodeIds?);
controller.cut(nodeIds?);
await controller.paste(targetId?);
// Ações contextuais
await controller.handleEnter(nodeId?);
await controller.handleDoubleClick(nodeId);
await controller.handleContextMenu(nodeId, x, y);
`
`typescriptNome do ${expandable ? 'diretório' : 'arquivo'}:
class MyController extends NodeTreeController {
// Pedir nome ao criar nó
protected async getNewNodeName(expandable?: boolean): Promise
return prompt();Deletar ${nodeIds.length} itens?
}
// Validar nome
protected async validateNodeName(parentId: string, name: string): Promise
return !name.includes('/') && name.length > 0;
}
// Confirmar exclusão
protected async confirmDelete(nodeIds: string[]): Promise
return confirm();/editor/${node.id}
}
// Validar antes de mover
protected async beforeMove(nodeIds: string[], targetId: string): Promise
// Suas validações customizadas
return true;
}
// Ação ao ativar arquivo (Enter)
protected async handleFileActivation(node: Node): Promise
console.log('Arquivo ativado:', node.label);
}
// Ação ao abrir arquivo (double click)
protected async handleFileOpen(node: Node): Promise
window.open();`
}
}
O sistema emite eventos tipados para todas as operações importantes.
`typescript
// Tipos de eventos disponíveis
type NodeTreeEventMap = {
// Lifecycle
"tree:initializing": undefined;
"tree:initialized": undefined;
"tree:destroying": undefined;
// Navegação
"navigation:start": { direction: string };
"cursor:moved": { from?: string; to?: string; direction?: string };
// Nós
"node:creating": { parentId: string; label: string; expandable?: boolean };
"node:created": { parentId: string; label: string };
"node:renaming": { nodeId: string; oldName: string; newName: string };
"node:renamed": { nodeId: string; oldName: string; newName: string };
"nodes:deleting": { nodeIds: string[] };
"nodes:deleted": { nodeIds: string[] };
"nodes:moving": { nodeIds: string[]; targetId: string };
"nodes:moved": { nodeIds: string[]; targetId: string };
// Expansão
"node:expanding": { nodeId: string };
"node:expanded": { nodeId: string };
"node:collapsing": { nodeId: string };
"node:collapsed": { nodeId: string };
// Seleção
"selection:changed": { added: string[]; removed: string[]; total: number };
"selection:toggled": { nodeId: string; selected: boolean };
"selection:all": { count: number };
"selection:cleared": undefined;
// Arquivos
"file:activated": { node: Node };
"file:open": { node: Node };
"file:dropOnFile": { sourceNodes: Node[]; targetNode: Node };
// Clipboard
"clipboard:copied": { nodeIds: string[]; count: number };
"clipboard:cut": { nodeIds: string[]; count: number };
"clipboard:pasted": { nodeIds: string[]; targetId: string };
// Menu contexto
"contextmenu:open": { node: Node; nodeType: string; position: { x: number; y: number } };
// Erros
"error": { error: Error; operation: string; context?: any };
"warning": { message: string; operation: string; context?: any };
// FileSystem sync
"fileSystem:sync": { nodes: Node[] };
};
// Ouvir eventos
context.services.events.on("node:created", ({ parentId, label }) => {
console.log(Novo nó criado: ${label} em ${parentId});
});
// Remover listener
const disposable = context.services.events.on("cursor:moved", handler);
disposable.dispose();
`
`typescript
// 1. Implementar interface do serviço
class MyNodeStateService implements INodeStateService {
getNode(nodeId: string): Node | undefined {
// Sua implementação customizada
}
// ... outros métodos
}
// 2. Passar na criação do contexto
const context = createNodeTreeContext({
fileSystem,
services: {
nodeState: MyNodeStateService, // Classe
// ou
nodeState: new MyNodeStateService(), // Instância
}
});
`
`typescript
class MyNavigationService extends NavigationService {
setCursor(nodeId: string): void {
// Adicionar comportamento customizado
console.log('Cursor mudou para:', nodeId);
// Chamar implementação original
super.setCursor(nodeId);
}
}
const context = createNodeTreeContext({
fileSystem,
services: {
navigation: MyNavigationService
}
});
`
`typescript
class MyTreeController extends NodeTreeController {
// Sobrescrever métodos
async createNode(parentId?: string, label?: string): Promise
// Validações customizadas
if (!await this.canUserCreateNode()) {
return false;
}
return super.createNode(parentId, label);
}
// Adicionar novos métodos
async batchImport(files: File[]): Promise
for (const file of files) {
await this.createNode("root", file.name, false);
}
}
}
const context = createNodeTreeContext({
fileSystem,
controller: MyTreeController
});
`
O @firesystem/core permite diferentes backends de armazenamento:
`typescript
// IndexedDB (browser)
import { IndexedDBFileSystem } from "@firesystem/indexeddb";
const fs = new IndexedDBFileSystem({ dbName: "my-app" });
// Memory (testes)
import { MemoryFileSystem } from "@firesystem/memory";
const fs = new MemoryFileSystem();
// Estrutura de arquivos
/nodes.json # Índice com todos os nós
/nodes/
├── node-id1.json # Dados do nó 1
├── node-id2.json # Dados do nó 2
└── ...
`
O NodeOperationService observa mudanças no FileSystem e sincroniza automaticamente com o estado Zustand:
`typescript
// Mudanças externas no FileSystem são refletidas no estado
await fileSystem.writeFile("/nodes.json", updatedNodes);
// Estado Zustand é atualizado automaticamente
// Operações via controller atualizam FileSystem e estado
await controller.createNode("root", "new-file.ts");
// Tanto FileSystem quanto Zustand são atualizados
`
`typescript
import { createNodeTreeContext } from "@node-tree/headless";
import { MemoryFileSystem } from "@firesystem/memory";
describe("My Tree Feature", () => {
let context;
beforeEach(() => {
const fileSystem = new MemoryFileSystem();
context = createNodeTreeContext({ fileSystem });
});
it("should create nodes", async () => {
const controller = context.controller;
await controller.createNode("", "root", true);
const nodes = controller.getNodes();
expect(nodes).toHaveLength(1);
expect(nodes[0].label).toBe("root");
});
});
`
typescript
class FileExplorerController extends NodeTreeController {
protected async handleFileOpen(node: Node): Promise {
if (node.metadata?.mimeType?.startsWith('image/')) {
this.openImageViewer(node);
} else {
this.openTextEditor(node);
}
}
}
`$3
`typescript
class SettingsController extends NodeTreeController {
protected async validateNodeName(parentId: string, name: string): Promise {
// Validar formato de chave de configuração
return /^[a-z][a-zA-Z0-9.]*$/.test(name);
}
}
`$3
`typescript
class OrgChartController extends NodeTreeController {
protected async beforeMove(nodeIds: string[], targetId: string): Promise {
// Validar hierarquia organizacional
return this.validateOrgStructure(nodeIds, targetId);
}
}
``- Arquitetura de Serviços
- Padrões Avançados do Controller
- Guia de Testes
- @firesystem/core Documentation
MIT