COORD: 44.21.90
OFFSET: +12.5°
SYS.READY
BUFFER: 99%
FOCAL_PT
BACK TO DEVLOG
KINDLING-PAGES

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

2026-01-04 // RAW LEARNING CAPTURE
PROJECTKINDLING-PAGES

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 view
  • a2b905f - 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 task

Added 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 checkboxes
  • dagre layout — 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 early

Added js-yaml dependency:

pnpm add js-yaml && pnpm add -D @types/js-yaml

Created 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 + title
  • icon — React Flow with compact 48x48 emoji nodes (Flyff-style)

Two data formats:

  • board.yaml — Structured YAML with task/status/icon/description
  • board.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).

LOG.ENTRY_END
ref:kindling-pages
RAW