@node-tree/react-core
v1.0.0
Published
Core React hooks and logic for VSCode-style tree explorer - no UI components
Readme
@node-tree/react-core
Bindings React para o tree headless do @node-tree/core (porta reativa do tree do
VSCode) + a regra de arquivos do @node-tree/explorer. Pure logic, sem componentes de
UI — você renderiza; este pacote projeta o estado e roteia a interação.
Arquitetura em 3 camadas
@node-tree/core (widget) + @node-tree/explorer (regra) ← fonte de estado (Emitters)
│
store createTreeStore: Emitter → useSyncExternalStore (snapshot estável, por slice)
controllers funções puras (teclado/ponteiro) — zero React/DOM, testáveis a seco
hooks finos: leem o store e ligam o DOM aos controllers- store —
useSyncExternalStorecom snapshot referencialmente estável. Slices independentes (visibleWindow,contentHeight,scrollTop,focus,selection): quem lê foco não re-renderiza quando só o scroll muda.useIsFocused(path)devolve um boolean → re-renderiza só os nós que mudaram (orenderIndexes([old,new])do VSCode). - controllers —
dispatchTreeKey(tecla → método do tree) eonNodeClick/onNodeDoubleClick/onNodeContextMenu(semântica de seleção). Puros. - hooks —
useVisibleWindow,useIsFocused,useKeyboardNavigation,useEventDelegation, etc. Sem lógica.
Drag & drop e renderização ficam na camada de UI (
@node-tree/react), não aqui.
Instalação
pnpm add @node-tree/react-core @node-tree/core @node-tree/explorer @firesystem/coreRequer React 18+ (usa useSyncExternalStore nativo).
Uso
import {
NodeTreeProvider,
useVisibleWindow,
useIsFocused,
useIsSelected,
useKeyboardNavigation,
useEventDelegation,
useTreeScroll,
} from "@node-tree/react-core";
function Explorer({ fs }) {
return (
<NodeTreeProvider fs={fs} rootPath="/">
<Tree />
</NodeTreeProvider>
);
}
function Tree() {
const { nodes } = useVisibleWindow();
const { handleKeyDown } = useKeyboardNavigation();
const { handleClick, handleDoubleClick, handleContextMenu } =
useEventDelegation();
const { scrollContainerProps, totalHeight } = useTreeScroll();
return (
<div tabIndex={0} onKeyDown={handleKeyDown} {...scrollContainerProps}>
<div style={{ height: totalHeight, position: "relative" }}>
{nodes.map(({ node, top }) => (
<Row
key={node.element.path}
entry={node.element}
top={top}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
onContextMenu={handleContextMenu}
/>
))}
</div>
</div>
);
}
function Row({ entry, top, ...handlers }) {
const focused = useIsFocused(entry.path);
const selected = useIsSelected(entry.path);
return (
<div
data-node-id={entry.path}
data-node-type={entry.type}
style={{ position: "absolute", top }}
aria-selected={selected}
data-focused={focused}
{...handlers}
>
{entry.name}
</div>
);
}API
Provider / factory
NodeTreeProvider— monta a stack (createNodeTree) e provê via context.createNodeTree(fs, options)— factory pura:{ tree, explorer, store, dispose }.useNodeTreeTree(),useNodeTreeExplorer(),useNodeTreeFileSystem(),useNodeTreeConfig(),useNodeTreeState(),useNodeTreeStore().
Hooks de leitura (useSyncExternalStore)
useVisibleWindow()— janela virtual{ nodes, startIndex, endIndex, totalCount, … }.useContentHeight(),useScrollTop(),useTreeScroll().useIsFocused(path),useIsSelected(path)— selector-por-nó (boolean).useFocusedPaths(),useSelectedPaths()— conjuntos completos.
Interação
useKeyboardNavigation()→{ handleKeyDown }.useEventDelegation()→{ handleClick, handleDoubleClick, handleContextMenu, findNodeFromElement }.useExplorerEvents(handlers?)+ especializados (useOperationEvents,useProgressEvents,useClipboardEvents,useHistoryEvents,useConfirmationEvents).
Controllers puros (testáveis sem React)
dispatchTreeKey(tree, event).onNodeClick(explorer, node, mods),onNodeDoubleClick,onNodeContextMenu.
Store
createTreeStore(tree, getId)→ slices{ subscribe, getSnapshot }.
Scripts
pnpm test # vitest
pnpm test:coverage # cobertura
pnpm typecheck # tsc --noEmit
pnpm build # tsup