feat(ui): story tab with tree and prose log
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import type { StoryTreeNodeView } from '../state/viewModel';
|
||||
|
||||
interface StoryTreeProps {
|
||||
nodes: StoryTreeNodeView[];
|
||||
selectedId: string | null;
|
||||
onSelect: (id: string) => void;
|
||||
}
|
||||
|
||||
interface StoryTreeNodeProps {
|
||||
node: StoryTreeNodeView;
|
||||
depth: number;
|
||||
selectedId: string | null;
|
||||
onSelect: (id: string) => void;
|
||||
}
|
||||
|
||||
function StoryTreeNode({ node, depth, selectedId, onSelect }: StoryTreeNodeProps) {
|
||||
const isSelected = node.id === selectedId;
|
||||
|
||||
let labelClasses =
|
||||
'w-full cursor-pointer rounded px-2 py-1 text-left text-sm transition-colors duration-150';
|
||||
|
||||
if (isSelected) {
|
||||
labelClasses += ' bg-amber-500/15 border border-amber-500/40 text-amber-200';
|
||||
} else if (node.active) {
|
||||
labelClasses += ' text-amber-400 hover:bg-slate-800/60 border border-transparent';
|
||||
} else if (!node.seen) {
|
||||
labelClasses += ' text-slate-600 hover:bg-slate-800/40 border border-transparent';
|
||||
} else {
|
||||
labelClasses += ' text-slate-300 hover:bg-slate-800/60 border border-transparent';
|
||||
}
|
||||
|
||||
return (
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
className={labelClasses}
|
||||
style={{ marginLeft: `${depth * 1}rem` }}
|
||||
onClick={() => onSelect(node.id)}
|
||||
aria-pressed={isSelected}
|
||||
>
|
||||
<span className="flex items-center gap-1.5">
|
||||
{node.active && (
|
||||
<span className="inline-block h-1.5 w-1.5 shrink-0 rounded-full bg-amber-400" />
|
||||
)}
|
||||
{node.label}
|
||||
</span>
|
||||
</button>
|
||||
{node.children.length > 0 && (
|
||||
<ul className="mt-0.5 space-y-0.5">
|
||||
{node.children.map((child) => (
|
||||
<StoryTreeNode
|
||||
key={child.id}
|
||||
node={child}
|
||||
depth={depth + 1}
|
||||
selectedId={selectedId}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export function StoryTree({ nodes, selectedId, onSelect }: StoryTreeProps) {
|
||||
if (nodes.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<p className="text-sm text-slate-500 italic">No story branches yet…</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-y-auto rounded-xl border border-slate-800/60 bg-slate-900/40 p-4 shadow-inner">
|
||||
<h2 className="mb-3 text-xs font-semibold tracking-wider text-slate-400 uppercase">
|
||||
Story Tree
|
||||
</h2>
|
||||
<ul className="space-y-0.5">
|
||||
{nodes.map((node) => (
|
||||
<StoryTreeNode
|
||||
key={node.id}
|
||||
node={node}
|
||||
depth={0}
|
||||
selectedId={selectedId}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user