learn and share notes on atproto (wip) 馃
malfestio.stormlightlabs.org/
readability
solid
axum
atproto
srs
1import { createSignal, For, Show, splitProps } from "solid-js";
2import type { Component, JSX } from "solid-js";
3
4export type TreeNode = { id: string; label: string; icon?: JSX.Element; children?: TreeNode[] };
5
6type TreeViewProps = { nodes: TreeNode[]; onSelect?: (node: TreeNode) => void; class?: string };
7
8type TreeNodeItemProps = { node: TreeNode; level: number; onSelect?: (node: TreeNode) => void };
9
10const ChevronIcon: Component<{ expanded: boolean }> = (props) => (
11 <svg
12 class={`w-4 h-4 transition-transform duration-200 ${props.expanded ? "rotate-90" : ""}`}
13 fill="none"
14 viewBox="0 0 24 24"
15 stroke="currentColor">
16 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
17 </svg>
18);
19
20const TreeNodeItem: Component<TreeNodeItemProps> = (props) => {
21 const [expanded, setExpanded] = createSignal(false);
22 const hasChildren = () => props.node.children && props.node.children.length > 0;
23
24 const handleKeyDown = (e: KeyboardEvent) => {
25 if (e.key === "Enter" || e.key === " ") {
26 e.preventDefault();
27 if (hasChildren()) {
28 setExpanded(!expanded());
29 }
30 props.onSelect?.(props.node);
31 } else if (e.key === "ArrowRight" && hasChildren() && !expanded()) {
32 setExpanded(true);
33 } else if (e.key === "ArrowLeft" && expanded()) {
34 setExpanded(false);
35 }
36 };
37
38 return (
39 <li role="treeitem" aria-expanded={hasChildren() ? expanded() : undefined}>
40 <div
41 class="flex items-center gap-1 px-2 py-1.5 hover:bg-gray-800 cursor-pointer text-gray-300 hover:text-white transition-colors rounded"
42 style={{ "padding-left": `${props.level * 16 + 8}px` }}
43 onClick={() => {
44 if (hasChildren()) setExpanded(!expanded());
45 props.onSelect?.(props.node);
46 }}
47 onKeyDown={handleKeyDown}
48 tabIndex={0}
49 role="button">
50 <span class="w-4 h-4 flex items-center justify-center text-gray-500">
51 <Show when={hasChildren()} fallback={<span class="w-4" />}>
52 <ChevronIcon expanded={expanded()} />
53 </Show>
54 </span>
55 <Show when={props.node.icon}>
56 <span class="w-4 h-4 flex items-center justify-center">{props.node.icon}</span>
57 </Show>
58 <span class="text-sm truncate">{props.node.label}</span>
59 </div>
60 <Show when={expanded() && hasChildren()}>
61 <ul role="group" class="border-l border-gray-800 ml-4">
62 <For each={props.node.children}>
63 {(child) => <TreeNodeItem node={child} level={props.level + 1} onSelect={props.onSelect} />}
64 </For>
65 </ul>
66 </Show>
67 </li>
68 );
69};
70
71export const TreeView: Component<TreeViewProps> = (props) => {
72 const [local, others] = splitProps(props, ["nodes", "onSelect", "class"]);
73
74 return (
75 <ul role="tree" class={`text-sm ${local.class || ""}`} {...others}>
76 <For each={local.nodes}>{(node) => <TreeNodeItem node={node} level={0} onSelect={local.onSelect} />}</For>
77 </ul>
78 );
79};