- 01.Starting Point
- 02.Phase 1: Directory Mode Routing
- 03.Phase 2: First Board Attempt — CSS Columns
- 04.Now
- 05.Done
- 06.Phase 3: Pivot to React Flow
- 07.Phase 4: Flyff-Style Icon Nodes
- 08.Phase 5: YAML Format
- 09.Phase 6: Kanban Columns
- 10.Phase 7: Layout Width Fix
- 11.The Hooks Order Bug
- 12.Where We Landed
- 13.Takeaways
Board Visualization Exploration
Kindling Pages v0.6.0 had single-file `.project.md` support. Goal: add directory mode (`.project/` with multiple files) and a `/board` route for project status tracking. User wanted something like Lin
Starting Point
Kindling Pages v0.6.0 had single-file .project.md support. Goal: add directory mode (.project/ with multiple files) and a /board route for project status tracking. User wanted something like Linear's board view that Claude could easily update.
Commits from this session:
e60fc1b- feat: add directory mode with board visualization (v0.7.0)0cb7d61- feat: add YAML board format and kanban viewa2b905f- fix: resolve hooks order lint error, update changelog
Phase 1: Directory Mode Routing
First challenge: detect whether we're in "file mode" (single .project.md) or "directory mode" (.project/ folder with index.md, board.md, etc.).
Created getContentMode() in src/lib/mdx-utils.ts:
export interface ContentMode {
type: 'directory' | 'file';
basePath: string;
}
export const getContentMode = cache((): ContentMode | null => {
// Docker directory mode: /content/index.md exists
if (fs.existsSync('/content/index.md')) {
return { type: 'directory', basePath: '/content' };
}
// ... fallback checks for local paths
});Key decision: Docker paths (/content/) take precedence over local paths (content/). This matters because in production, content is volume-mounted.
Replaced src/app/page.tsx with src/app/[[...slug]]/page.tsx (optional catch-all route). This handles both / and /board in one file.
Gotcha: After deleting page.tsx, TypeScript still errored because .next directory had stale type references. Fix: rm -rf .next && pnpm typecheck.
Phase 2: First Board Attempt — CSS Columns
Initial approach: parse markdown with ## Now, ## Next, ## Done sections, render as CSS grid columns.
## Now
- [ ] Active task
## Done
- [x] Completed taskAdded to globals.css:
.board-layout {
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}Problem: Columns wrapped on narrow viewports. User showed screenshot: "three columns! but they wrap instead of looking like linear".
Phase 3: Pivot to React Flow
User shared inspiration images from Flyff Skill Simulator — compact icon nodes, colored borders, dotted edges. React Flow was already in the project (for tsx flow blocks), so we pivoted.
Created src/components/mdx/board-flow.tsx with:
parseMarkdownBoard()— extracts tasks from markdown checkboxesdagrelayout — auto-positions nodes left-to-right- Status-based styling: green=done, yellow=in-progress, gray=pending
const statusStyles: Record<Task['status'], React.CSSProperties> = {
done: { background: '#f0fdf4', borderColor: '#22c55e' },
in_progress: { background: '#fefce8', borderColor: '#eab308' },
pending: { background: '#f8fafc', borderColor: '#cbd5e1', opacity: 0.7 },
};This worked but nodes were large text boxes (180x60px). User wanted compact icon nodes like Flyff.
Phase 4: Flyff-Style Icon Nodes
Redesigned to 48x48px icon nodes with custom React Flow node component:
const IconNode = memo(({ data }: NodeProps<Node<IconNodeData>>) => {
return (
<div style={{
width: 48,
height: 48,
border: `3px solid ${borderColor}`,
borderRadius: 8,
}}>
<span>{icon}</span>
{/* Corner badge for done */}
{data.status === 'done' && (
<span style={{ position: 'absolute', top: -4, right: -4 }}>✓</span>
)}
</div>
);
});Added emoji parsing from task text:
const emojiRegex = /^(\p{Emoji_Presentation}|\p{Emoji}\uFE0F?)\s*/u;So - [x] 📊 Recharts blocks extracts 📊 as the icon and "Recharts blocks" as the label.
Phase 5: YAML Format
User wanted richer task data (descriptions, not just titles). Markdown inline syntax gets messy. Pivoted to YAML:
- task: Fix auth bug
status: done
icon: 🔧
description: Session tokens were expiring too earlyAdded js-yaml dependency:
pnpm add js-yaml && pnpm add -D @types/js-yamlCreated getBoardYaml() in mdx-utils to detect board.yaml:
export const getBoardYaml = cache((): string | null => {
const yamlPath = path.join(mode.basePath, 'board.yaml');
if (fs.existsSync(yamlPath)) {
return fs.readFileSync(yamlPath, 'utf8');
}
return null;
});Page now checks for YAML first, falls back to markdown board.md.
Phase 6: Kanban Columns
After seeing the React Flow visualization, user wanted to try classic Kanban columns instead. Added third variant:
function KanbanBoard({ tasks }: { tasks: Task[] }) {
return (
<div className="grid grid-cols-3 gap-4">
<KanbanColumn title="Now" tasks={inProgress} status="in_progress" />
<KanbanColumn title="Next" tasks={pending} status="pending" />
<KanbanColumn title="Done" tasks={done} status="done" />
</div>
);
}Each column shows:
- Colored header with status dot and task count
- Cards with icon, title, description
- Left border color matching status
User asked to swap column order to Now | Next | Done (active work first).
Phase 7: Layout Width Fix
Screenshot showed board squished into narrow max-w-3xl container. Fixed by making board page use wider layout:
<div className={`mx-auto px-6 relative ${isBoard ? 'max-w-6xl' : 'max-w-3xl'}`}>Also reduced header size and padding for board view.
The Hooks Order Bug
During cleanup, Biome caught a React hooks violation:
src/components/mdx/board-flow.tsx:421:42 lint/correctness/useHookAtTopLevel
× This hook is being called conditionally
The problem: early return for kanban variant before React Flow hooks:
// BAD - hooks after conditional return
export function BoardFlow({ variant }) {
if (variant === 'kanban') {
return <KanbanBoard />; // early return
}
const [nodes] = useNodesState(initialNodes); // hook after return!
}Fix: Split into separate components:
function FlowBoard({ content, height, variant, format }) {
// All hooks here
const { initialNodes, initialEdges } = useMemo(() => { ... });
const [nodes] = useNodesState(initialNodes);
return <ReactFlow ... />;
}
export function BoardFlow({ variant, ...props }) {
if (variant === 'kanban') {
return <KanbanBoard tasks={...} />;
}
return <FlowBoard {...props} />; // delegate to hook-containing component
}Where We Landed
Three board visualization variants:
kanban— Classic 3-column board (Now | Next | Done)card— React Flow with 160px cards showing icon + titleicon— React Flow with compact 48x48 emoji nodes (Flyff-style)
Two data formats:
board.yaml— Structured YAML with task/status/icon/descriptionboard.md— Markdown with checkboxes and emoji prefixes (legacy)
File structure:
content/
├── index.md # Pitch page (frontmatter lives here)
├── board.yaml # Structured task data
└── board.md # Fallback markdown format
Takeaways
React Flow custom nodes: Must register in nodeTypes object and use type: 'customName' in node data. Handles need explicit positioning.
Hooks order matters: Can't have hooks after conditional returns. Solution: extract the hook-using code into a separate component.
YAML vs inline metadata: For Claude-editable structured data, YAML beats inline markdown syntax. Each field on its own line = easy grep/edit.
Dagre layout: nodesep = horizontal gap, ranksep = gap between ranks (columns in LR layout). Smaller values = denser graph.
Emoji regex: Unicode property escapes work well: /\p{Emoji_Presentation}/u. Watch out for variation selectors (\uFE0F).