npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@darksnow-ui/node-tree-headless

v0.1.13

Published

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.

Readme

@node-tree/headless

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.

🌟 Características Principais

  • 🎯 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

📦 Instalação

pnpm add @node-tree/headless @firesystem/core

🚀 Quick Start

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();

🏗️ Arquitetura

Fluxo de Dados

Eventos (teclado/mouse/API)
           ↓
      Controller (validações, hooks)
           ↓
       Services
       /      \
  FileSystem  NavigationService
      ↓            ↓
   (persist)  Estado Zustand
               (reativo)

Princípios Fundamentais

  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

🔧 Serviços

NodeStateService

Gerencia o estado interno dos nós (Map de nodes, indexação).

interface INodeStateService {
  getNode(nodeId: string): Node | undefined;
  getNodes(): Map<string, Node>;
  setNodes(nodes: NodeItem[]): void;
  addOrUpdateNode(node: Node): void;
  removeNode(nodeId: string): void;
  isNodeAncestorOf(ancestorId: string, nodeId: string): boolean;
}

NodeOperationService

Coordena operações com persistência no FileSystem.

interface INodeOperationService {
  createNode(parentId: string, label: string, expandable?: boolean): Promise<boolean>;
  updateNode(nodeId: string, updates: Partial<Node>): Promise<boolean>;
  deleteNodes(nodeIds: string[]): Promise<boolean>;
  moveNodes(nodeIds: string[], targetId?: string): Promise<boolean>;
  renameNode(nodeId: string, newName: string): Promise<boolean>;
}

NavigationService

Controla navegação, seleção e expansão (estado efêmero).

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;
}

DOMQueryService

Utilitários para interação com DOM (quando usado com UI).

interface IDOMQueryService {
  getNodeElementRect(nodeId: string): DOMRect | null;
  getContextMenuPosition(nodeId: string): { x: number; y: number };
  scrollNodeIntoView(nodeId: string): void;
}

🎮 Controller

O controller é a interface pública principal, coordenando todas as operações.

Operações Básicas

// 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);

Hooks Extensíveis

class MyController extends NodeTreeController {
  // Pedir nome ao criar nó
  protected async getNewNodeName(expandable?: boolean): Promise<string | null> {
    return prompt(`Nome do ${expandable ? 'diretório' : 'arquivo'}:`);
  }
  
  // Validar nome
  protected async validateNodeName(parentId: string, name: string): Promise<boolean> {
    return !name.includes('/') && name.length > 0;
  }
  
  // Confirmar exclusão
  protected async confirmDelete(nodeIds: string[]): Promise<boolean> {
    return confirm(`Deletar ${nodeIds.length} itens?`);
  }
  
  // Validar antes de mover
  protected async beforeMove(nodeIds: string[], targetId: string): Promise<boolean> {
    // Suas validações customizadas
    return true;
  }
  
  // Ação ao ativar arquivo (Enter)
  protected async handleFileActivation(node: Node): Promise<void> {
    console.log('Arquivo ativado:', node.label);
  }
  
  // Ação ao abrir arquivo (double click)
  protected async handleFileOpen(node: Node): Promise<void> {
    window.open(`/editor/${node.id}`);
  }
}

🔄 Sistema de Eventos

O sistema emite eventos tipados para todas as operações importantes.

// 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();

🔌 Customização

Substituir Serviços

// 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
  }
});

Estender Serviços Nativos

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
  }
});

Controller Customizado

class MyTreeController extends NodeTreeController {
  // Sobrescrever métodos
  async createNode(parentId?: string, label?: string): Promise<boolean> {
    // Validações customizadas
    if (!await this.canUserCreateNode()) {
      return false;
    }
    
    return super.createNode(parentId, label);
  }
  
  // Adicionar novos métodos
  async batchImport(files: File[]): Promise<void> {
    for (const file of files) {
      await this.createNode("root", file.name, false);
    }
  }
}

const context = createNodeTreeContext({
  fileSystem,
  controller: MyTreeController
});

💾 Persistência com FileSystem

O @firesystem/core permite diferentes backends de armazenamento:

// 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
  └── ...

Sincronização Automática

O NodeOperationService observa mudanças no FileSystem e sincroniza automaticamente com o estado Zustand:

// 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

🧪 Testes

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");
  });
});

🎯 Casos de Uso

1. File Explorer

class FileExplorerController extends NodeTreeController {
  protected async handleFileOpen(node: Node): Promise<void> {
    if (node.metadata?.mimeType?.startsWith('image/')) {
      this.openImageViewer(node);
    } else {
      this.openTextEditor(node);
    }
  }
}

2. Configurações Hierárquicas

class SettingsController extends NodeTreeController {
  protected async validateNodeName(parentId: string, name: string): Promise<boolean> {
    // Validar formato de chave de configuração
    return /^[a-z][a-zA-Z0-9.]*$/.test(name);
  }
}

3. Estrutura Organizacional

class OrgChartController extends NodeTreeController {
  protected async beforeMove(nodeIds: string[], targetId: string): Promise<boolean> {
    // Validar hierarquia organizacional
    return this.validateOrgStructure(nodeIds, targetId);
  }
}

📚 Referências

📄 Licença

MIT