Skip to content

Commit 54d7291

Browse files
naivefunclaude
andcommitted
Initial release of handtree v0.1.0
A React tree component for hand-crafted hierarchical interfaces. Unlike data-driven tree libraries, handtree lets you compose tree structures manually with full control over styling, behavior, and layout. Features: - Manual tree composition with TreeNode components - Rich content support (custom titles, details, styling) - Expandable/collapsible nodes with state management - Tree connecting lines and proper indentation - TypeScript support with full type definitions - Interactive demos with JSON Schema explorer example - GitHub Pages deployment for live examples Perfect for complex nested data that doesn't fit uniform schemas - like OpenAPI specifications, file systems, or custom navigation structures. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
0 parents  commit 54d7291

23 files changed

+6680
-0
lines changed

.claude/settings.local.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(pnpm add:*)",
5+
"Bash(npx tailwindcss init:*)",
6+
"Bash(pnpm exec tailwindcss init:*)",
7+
"Bash(mkdir:*)",
8+
"Bash(touch:*)",
9+
"Bash(pnpm remove:*)",
10+
"Bash(pnpm build:*)",
11+
"Bash(pnpm ladle:build:*)",
12+
"Bash(pnpm publish:*)",
13+
"Bash(git init:*)",
14+
"Bash(git add:*)"
15+
],
16+
"defaultMode": "acceptEdits"
17+
}
18+
}

.github/workflows/deploy-docs.yml

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
name: Deploy Ladle Examples to GitHub Pages
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
permissions:
10+
contents: read
11+
pages: write
12+
id-token: write
13+
14+
concurrency:
15+
group: "pages"
16+
cancel-in-progress: false
17+
18+
jobs:
19+
build:
20+
runs-on: ubuntu-latest
21+
22+
steps:
23+
- name: Checkout
24+
uses: actions/checkout@v4
25+
26+
- name: Setup pnpm
27+
uses: pnpm/action-setup@v4
28+
with:
29+
version: latest
30+
31+
- name: Setup Node.js
32+
uses: actions/setup-node@v4
33+
with:
34+
node-version: '18'
35+
cache: 'pnpm'
36+
37+
- name: Install dependencies
38+
run: pnpm install
39+
40+
- name: Build Ladle examples
41+
run: pnpm ladle:build
42+
43+
- name: Upload artifact
44+
uses: actions/upload-pages-artifact@v3
45+
with:
46+
path: 'build'
47+
48+
deploy:
49+
environment:
50+
name: github-pages
51+
url: ${{ steps.deployment.outputs.page_url }}
52+
runs-on: ubuntu-latest
53+
needs: build
54+
steps:
55+
- name: Deploy to GitHub Pages
56+
id: deployment
57+
uses: actions/deploy-pages@v4

.gitignore

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Dependencies
2+
node_modules/
3+
.pnpm-store/
4+
5+
# Build outputs
6+
dist/
7+
build/
8+
9+
# Development
10+
.DS_Store
11+
*.log
12+
*.tgz
13+
14+
# IDE
15+
.vscode/
16+
.idea/
17+
*.swp
18+
*.swo
19+
20+
# Environment
21+
.env
22+
.env.local
23+
.env.development.local
24+
.env.test.local
25+
.env.production.local
26+
27+
# Cache
28+
.cache/
29+
.temp/

.ladle/components.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import React from 'react'
2+
import '../src/styles/globals.css'
3+
4+
export const Provider = ({ children }: { children: React.ReactNode }) => {
5+
return (
6+
<div className="min-h-screen bg-white text-gray-900 dark:bg-gray-900 dark:text-white">
7+
{children}
8+
</div>
9+
)
10+
}

.ladle/config.mjs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/** @type {import('@ladle/react').UserConfig} */
2+
export default {
3+
stories: 'src/**/*.stories.{js,jsx,ts,tsx}',
4+
addons: {
5+
width: {
6+
enabled: true,
7+
options: {
8+
xsmall: 414,
9+
small: 640,
10+
medium: 768,
11+
large: 1024
12+
}
13+
},
14+
theme: {
15+
enabled: true,
16+
defaultState: 'light'
17+
}
18+
}
19+
}

.npmignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
src/
2+
.ladle/
3+
tsconfig.json
4+
tsup.config.ts
5+
*.stories.*
6+
.git/
7+
node_modules/
8+
.DS_Store

README.md

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
# handtree
2+
3+
A React tree component for **hand-crafted** hierarchical interfaces. Unlike data-driven tree libraries, handtree lets you compose tree structures manually with full control over styling, behavior, and layout.
4+
5+
**[📖 Live Examples & Documentation →](https://username.github.io/handtree/)**
6+
7+
## Why handtree?
8+
9+
Most tree components are data-driven - they work well when you can normalize your data into a homogeneous structure like `{id, children}`. But many real-world scenarios resist this normalization:
10+
11+
```tsx
12+
// Hard to normalize - different node types, mixed content
13+
const jsonSchema = {
14+
User: { type: 'object', required: true, properties: {
15+
id: { type: 'string', validation: /\d+/ },
16+
profile: { type: 'object', properties: {
17+
name: { type: 'string', maxLength: 50 },
18+
settings: { type: 'array', items: 'string' }
19+
}}
20+
}}
21+
}
22+
```
23+
24+
handtree is perfect when you need to:
25+
1. **Handle complex nested data structures** that don't fit a uniform schema
26+
2. **Different node content and behaviors** for different node types (objects vs arrays vs primitives, each with unique styling and interactions)
27+
28+
Instead of forcing your data into a generic tree format, handtree lets you craft each node type exactly as it should appear and behave.
29+
30+
## Installation
31+
32+
```bash
33+
npm install handtree
34+
# or
35+
pnpm add handtree
36+
# or
37+
yarn add handtree
38+
```
39+
40+
## How it works
41+
42+
The core pattern is using `TreeNode` as a **visual renderer** inside your own data-type-oriented components. You don't use `TreeNode` directly - instead, you create components that understand your specific data structure and use `TreeNode` to render the tree visualization.
43+
44+
```tsx
45+
// Your data-oriented component
46+
function JsonSchemaNode({ schema, level }) {
47+
return (
48+
<TreeNode
49+
level={level}
50+
expandable={schema.type === 'object'}
51+
title={<SchemaTitle data={schema} />}
52+
details={<SchemaDetails data={schema} />}
53+
>
54+
{schema.properties?.map(prop =>
55+
<JsonSchemaNode key={prop.name} schema={prop} level={level + 1} />
56+
)}
57+
</TreeNode>
58+
)
59+
}
60+
61+
// Your custom title component
62+
function SchemaTitle({ data }) {
63+
return (
64+
<span className="flex items-center gap-2">
65+
<span className="text-blue-600 font-bold">{data.name}</span>
66+
<span className="text-gray-500">:</span>
67+
<span style={{ color: getTypeColor(data.type) }}>{data.type}</span>
68+
{data.required && <span className="text-xs bg-red-100 text-red-800 px-1 rounded">required</span>}
69+
</span>
70+
)
71+
}
72+
73+
// Your custom details component
74+
function SchemaDetails({ data }) {
75+
return data.description ? (
76+
<div className="text-xs text-gray-600 italic py-1">
77+
{data.description}
78+
{data.validation && <code className="ml-2 bg-gray-100 px-1">{data.validation.toString()}</code>}
79+
</div>
80+
) : null
81+
}
82+
83+
// Usage
84+
<TreeContext.Provider value={{ indent: 24, ancestorLastTrail: [], classNames: {} }}>
85+
<JsonSchemaNode schema={mySchema} level={0} />
86+
</TreeContext.Provider>
87+
```
88+
89+
This way, `JsonSchemaNode` handles the business logic (what's expandable, how to render titles), while `TreeNode` handles the tree visualization (lines, indentation, expand/collapse UI).
90+
91+
## API Reference
92+
93+
### TreeContext
94+
95+
Provides configuration for the entire tree.
96+
97+
```tsx
98+
interface ITreeContext {
99+
indent: number // Indentation per level (px)
100+
ancestorLastTrail: boolean[] // Internal: tracks line drawing
101+
classNames: Record<string, string> // Custom CSS classes
102+
}
103+
```
104+
105+
### TreeNode
106+
107+
The visual renderer component. This handles tree UI concerns (indentation, connecting lines, expand/collapse icons) while you handle the data concerns in your wrapper component.
108+
109+
```tsx
110+
interface TreeNodeProps {
111+
level: number // Nesting level (0 = root)
112+
lastNode?: boolean // Is this the last sibling?
113+
title: React.ReactNode // Node content (your custom JSX)
114+
details?: React.ReactNode // Additional details below title
115+
children?: React.ReactNode // Child nodes (usually more of your wrapper components)
116+
expandable?: boolean // Can this node be expanded?
117+
expanded?: boolean // Is this node expanded?
118+
onToggleExpanded?: () => void // Expand/collapse handler
119+
}
120+
```
121+
122+
**Key responsibilities:**
123+
- **Visual structure**: Draws connecting lines, handles indentation
124+
- **Expand/collapse UI**: Shows icons and handles click interactions
125+
- **Layout**: Positions title, details, and children properly
126+
127+
**Your wrapper component handles:**
128+
- **Data interpretation**: What should be expandable? What's the title?
129+
- **Business logic**: State management, event handling
130+
- **Content rendering**: Custom styling, icons, badges, etc.
131+
132+
## Examples
133+
134+
### Interactive Tree
135+
136+
```tsx
137+
function InteractiveExample() {
138+
const [expanded, setExpanded] = useState({
139+
root: true,
140+
folder1: false,
141+
folder2: true
142+
})
143+
144+
const toggle = (id: string) => {
145+
setExpanded(prev => ({ ...prev, [id]: !prev[id] }))
146+
}
147+
148+
return (
149+
<TreeContext.Provider value={{ indent: 20, ancestorLastTrail: [], classNames: {} }}>
150+
<TreeNode
151+
level={0}
152+
expandable
153+
expanded={expanded.root}
154+
onToggleExpanded={() => toggle('root')}
155+
title={<span>📁 My Project</span>}
156+
>
157+
<TreeNode
158+
level={1}
159+
expandable
160+
expanded={expanded.folder1}
161+
onToggleExpanded={() => toggle('folder1')}
162+
title={<span>📁 src</span>}
163+
>
164+
<TreeNode level={2} title={<span>📄 index.ts</span>} />
165+
<TreeNode level={2} title={<span>📄 components.tsx</span>} lastNode />
166+
</TreeNode>
167+
<TreeNode
168+
level={1}
169+
title={<span>📄 package.json</span>}
170+
lastNode
171+
/>
172+
</TreeNode>
173+
</TreeContext.Provider>
174+
)
175+
}
176+
```
177+
178+
### Styled with Custom Content
179+
180+
```tsx
181+
function StyledExample() {
182+
return (
183+
<TreeContext.Provider value={{ indent: 30, ancestorLastTrail: [], classNames: {} }}>
184+
<TreeNode
185+
level={0}
186+
expandable
187+
expanded={true}
188+
title={
189+
<div className="flex items-center gap-2">
190+
<span className="font-bold text-blue-600">API</span>
191+
<span className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded">
192+
online
193+
</span>
194+
</div>
195+
}
196+
details={
197+
<div className="text-xs text-gray-500 italic">
198+
Production endpoint - handle with care
199+
</div>
200+
}
201+
>
202+
<TreeNode
203+
level={1}
204+
title={<span className="text-purple-600">/users</span>}
205+
details={<span className="text-xs">GET, POST</span>}
206+
/>
207+
<TreeNode
208+
level={1}
209+
title={<span className="text-purple-600">/auth</span>}
210+
details={<span className="text-xs">POST</span>}
211+
lastNode
212+
/>
213+
</TreeNode>
214+
</TreeContext.Provider>
215+
)
216+
}
217+
```
218+
219+
## Development
220+
221+
```bash
222+
# Install dependencies
223+
pnpm install
224+
225+
# Start development with hot reload
226+
pnpm dev
227+
228+
# View component demos
229+
pnpm ladle:serve
230+
231+
# Build for production
232+
pnpm build
233+
```
234+
235+
## Contributing
236+
237+
We welcome contributions! Please see our contributing guidelines for details.
238+
239+
## License
240+
241+
MIT © [Your Name]

0 commit comments

Comments
 (0)