File size: 6,717 Bytes
a414867
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
"use client";

import { ArrowDown, ArrowUp, BookOpen, ChevronLeft, ChevronRight, Crosshair, Pause, Rows3 } from "lucide-react";
import { Button } from "@/components/ui/Button";
import { getTreeNavigationState } from "@/lib/tree/navigation";
import { visibleNodeIdsForStep } from "@/lib/tree/playback";
import type { TrajectoryTree } from "@/lib/tree/types";
import { cn } from "@/lib/utils";

export type CompactNavigatorFocusMode = "node" | "floor";

interface CompactTreeNavigatorProps {
  tree: TrajectoryTree | null;
  revealCount: number;
  selectedNodeId: string | null;
  autoReadEnabled: boolean;
  onNavigate: (nodeId: string, mode: CompactNavigatorFocusMode) => void;
  onToggleAutoRead: () => void;
}

function shortLabel(value: string) {
  const trimmed = value.trim();
  if (trimmed.length <= 46) return trimmed;
  return `${trimmed.slice(0, 43)}...`;
}

export function CompactTreeNavigator({
  tree,
  revealCount,
  selectedNodeId,
  autoReadEnabled,
  onNavigate,
  onToggleAutoRead
}: CompactTreeNavigatorProps) {
  if (!tree || tree.nodes.length === 0) return null;

  const visibleIds = visibleNodeIdsForStep(tree.nodes, revealCount);
  const navigation = getTreeNavigationState(tree.nodes, visibleIds, selectedNodeId);
  const current = navigation.currentNode;
  if (!current) return null;

  const currentFloorPosition = navigation.floors.indexOf(navigation.currentDepth) + 1;
  const totalFloors = navigation.floors.length;

  return (
    <div className="pointer-events-none absolute bottom-5 left-1/2 z-20 w-[min(calc(100%-2rem),900px)] -translate-x-1/2">
      <div className="pointer-events-auto rounded-xl border border-paper-300 bg-white/86 p-2.5 shadow-panel backdrop-blur-xl">
        <div className="flex flex-col gap-2 lg:flex-row lg:items-center">
          <div className="flex min-w-0 flex-1 items-center gap-2">
            <div className="flex items-center gap-1 rounded-lg border border-paper-300 bg-paper-50 p-1">
              <Button
                size="icon"
                variant="ghost"
                disabled={!navigation.previousNode}
                title="View previous node"
                onClick={() => navigation.previousNode && onNavigate(navigation.previousNode.id, "node")}
              >
                <ChevronLeft className="h-4 w-4" />
              </Button>
              <Button
                size="icon"
                variant="ghost"
                disabled={!navigation.nextNode}
                title="View next node"
                onClick={() => navigation.nextNode && onNavigate(navigation.nextNode.id, "node")}
              >
                <ChevronRight className="h-4 w-4" />
              </Button>
            </div>

            <div className="min-w-0 flex-1">
              <div className="flex items-center gap-2">
                <span className="rounded-md bg-claude-500 px-2 py-1 font-mono text-[11px] font-semibold text-white">
                  {current.id}
                </span>
                <span className="truncate text-xs font-semibold text-ink-700">{current.kind}</span>
                <span className="hidden text-xs text-ink-400 sm:inline">
                  {navigation.currentIndex + 1}/{navigation.orderedNodes.length}
                </span>
              </div>
              <div className="mt-1 truncate text-xs text-ink-500">{shortLabel(current.goal || "Untitled node")}</div>
            </div>
          </div>

          <div className="flex flex-wrap items-center gap-2 lg:justify-end">
            <div className="flex items-center gap-1 rounded-lg border border-paper-300 bg-paper-50 p-1">
              <Button
                size="icon"
                variant="ghost"
                disabled={!navigation.previousFloorNode}
                title="View previous floor"
                onClick={() => navigation.previousFloorNode && onNavigate(navigation.previousFloorNode.id, "floor")}
              >
                <ArrowUp className="h-4 w-4" />
              </Button>
              <div className="flex h-9 items-center gap-1 px-2 text-xs font-medium text-ink-600">
                <Rows3 className="h-4 w-4 text-claude-500" />
                {currentFloorPosition}/{totalFloors}
              </div>
              <Button
                size="icon"
                variant="ghost"
                disabled={!navigation.nextFloorNode}
                title="View next floor"
                onClick={() => navigation.nextFloorNode && onNavigate(navigation.nextFloorNode.id, "floor")}
              >
                <ArrowDown className="h-4 w-4" />
              </Button>
            </div>

            <div className="flex items-center gap-1 rounded-lg border border-paper-300 bg-white/82 p-1 shadow-sm">
              <Button
                size="sm"
                variant="ghost"
                title="Recenter current node"
                onClick={() => onNavigate(current.id, "node")}
              >
                <Crosshair className="h-4 w-4" />
                Focus
              </Button>
              <Button
                className="min-w-[104px]"
                size="sm"
                variant={autoReadEnabled ? "primary" : "secondary"}
                title={autoReadEnabled ? "Pause auto read" : "Auto read notes"}
                onClick={onToggleAutoRead}
              >
                {autoReadEnabled ? <Pause className="h-4 w-4" /> : <BookOpen className="h-4 w-4" />}
                {autoReadEnabled ? "Pause" : "Auto read"}
              </Button>
            </div>
          </div>
        </div>

        {navigation.sameFloorNodes.length > 1 && (
          <div className="mt-2 flex items-center gap-2 border-t border-paper-200 pt-2">
            <span className="shrink-0 text-[11px] font-semibold uppercase text-ink-400">Neighbors</span>
            <div className="flex min-w-0 flex-1 gap-1 overflow-x-auto scrollbar-thin">
              {navigation.sameFloorNodes.map((node) => (
                <button
                  key={node.id}
                  type="button"
                  className={cn(
                    "shrink-0 rounded-full border px-2.5 py-1 text-xs font-semibold transition",
                    node.id === current.id
                      ? "border-claude-300 bg-claude-50 text-claude-700"
                      : "border-paper-300 bg-white/70 text-ink-500 hover:border-claude-300 hover:text-claude-700"
                  )}
                  title={node.goal || node.id}
                  onClick={() => onNavigate(node.id, "floor")}
                >
                  {node.id}
                </button>
              ))}
            </div>
          </div>
        )}
      </div>
    </div>
  );
}