smart-catalog
v1.0.4
Published
An unopinionated, composable React tree/catalog component with drag-and-drop, inline rename, open/close control, and built-in undo/redo history.
Readme
Smart Catalog
An unopinionated, composable React tree/catalog component with drag-and-drop, inline rename, open/close control, and built-in undo/redo history.
Features
- Nested tree structure support
- Open/close (expand/collapse) state with a context
- Inline rename with your custom editor
- Undo/redo history with an imperative ref API
- Pluggable callbacks for move/create/rename and full-tree change events
Installation
npm install smart-catalogPeer dependencies:
- react >= 19
- react-dom >= 19
The library also uses:
- @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities
- react-icons (only for the default arrow icon in
Toggle)
Basic Concepts
- Tree node shape (
CatalogItm):
type ID = string | number
interface CatalogItm {
id: ID
title: string
order: number
children?: CatalogItm[]
}- Component: default export
Catalog - Composition API:
Catalog.Group,Catalog.Title,Catalog.Toggle,Catalog.Handle - Hooks for customization:
useCatalogItem,useCatalogTitle - Context-managed state for open nodes and drag-and-drop
Quick Start
import React, { useRef } from 'react'
import Catalog, { type CatalogHandler } from 'smart-catalog'
const initialTree = [
{ id: 1, title: 'Root', order: 0, children: [
{ id: 2, title: 'Child A', order: 0 },
{ id: 3, title: 'Child B', order: 1 },
]},
]
export default function Example() {
const ref = useRef<CatalogHandler>(null)
return (
<div>
<div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
<button onClick={() => ref.current?.undo()}>Undo</button>
<button onClick={() => ref.current?.redo()}>Redo</button>
<button onClick={() => ref.current?.collapseToggle()}>Toggle All</button>
</div>
<Catalog
ref={ref}
tree={initialTree}
defaultOpen={[1]}
onMove={(id, parentId, order) => {
// Persist move if needed
}}
onRename={(id, name) => {
// Persist rename if needed
}}
onChange={(nextTree) => {
// Entire tree changed (dnd/create/delete/rename)
}}
/>
</div>
)
}The default rendering includes a toggle, a title (double-click to edit), and a drag handle. You can fully customize the item layout by providing the item prop (see below).
Custom Item Rendering
Provide a React node via item to control how each item renders. Use the composition API and hooks to wire up behavior:
import Catalog, { useCatalogItem, useCatalogTitle } from 'smart-catalog'
function MyItem() {
const { hasChildren } = useCatalogItem()
const { title } = useCatalogTitle() // when inside Title's editor provider
return (
<Catalog.Group style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
<Catalog.Toggle />
<Catalog.Title />
</div>
<Catalog.Handle />
</Catalog.Group>
)
}
<Catalog tree={initialTree} item={<MyItem />} />To enable inline editing, pass an editor to Catalog.Title. You can provide either a React node or a render function. The library ships a minimal input component at CatalogItem/Input which is used in the default view:
import Input from 'smart-catalog/lib/src/components/CatalogItem/Input' // or copy your own input
<Catalog.Title editor={<Input />} />When an editor node is provided, double-click on the title area toggles edit mode. If you pass a render function, you’ll receive (title, setTitle) => ReactNode and can build a fully custom editor.
Imperative API (Ref)
Attach a ref with type CatalogHandler to call history and UI helpers:
undo()/redo()– history navigation for tree editscollapseToggle()– collapse all vs open all (for nodes that have children)- Also exposes item operations used internally:
handleCreate,handleRename,handleRemove
import { useRef } from 'react'
import Catalog, { type CatalogHandler } from 'smart-catalog'
const ref = useRef<CatalogHandler>(null)
<Catalog ref={ref} tree={initialTree} />
// Later
ref.current?.undo()
ref.current?.redo()
ref.current?.collapseToggle()Props
Catalog props:
tree: CatalogItm[]– the initial treedefaultOpen?: ID[]– node ids that start openeditem: ReactNode– optional custom item view; if omitted, a default layout is usedonMove?: (id: ID, parent: ID | undefined, order: number) => void– called on drag-and-drop reorderonRename?: (id: ID, name: string) => void– called on renameonChange?: (tree: CatalogItm[]) => void– called whenever the tree changesonOpenAll?: (status: boolean, waiting: boolean) => void– reflects “all opened” statusonChangeHistory?: (status: { undo: boolean; redo: boolean }) => void– history availability
Hooks
useCatalogItem()– item-level context:{ id, status, hasChildren, title, setTitle, parentId, createItem, removeItem }useCatalogTitle()– available within Title’s editor provider:{ title, setTitle }
Keyboard and Accessibility
Togglesupports Space key to open/close when focusedTitleenters edit mode on double-click wheneditoris providedHandleserves as the drag handle;@dnd-kitattributes and listeners are attached
Styling
The library does not impose styles beyond basic inline layout. Use className and style on Group, Title, Toggle, and Handle to style as needed.
TypeScript
- Types are exported:
CatalogHandler,HistoryStatus - Node ids can be
string | number
Build
npm run buildLicense
ISC
