e.stopPropagation()}
onclick={() => onSelectNode(tree)}
oncontextmenu={(e) => { e.preventDefault(); e.stopPropagation(); onContextMenu(node, tree, parentTree, e.clientX, e.clientY); }}
draggable={true}
ondragstart={(e) => { e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", ""); e.currentTarget.classList.add("pcr-nettree-row--dragging"); e.currentTarget._dragTree = tree; e.currentTarget._dragParent = parentTree; }}
ondragend={(e) => { e.currentTarget.classList.remove("pcr-nettree-row--dragging"); }}
ondragover={(e) => {
const src = e.currentTarget.closest(".pcr-nettree-items")?.querySelector(".pcr-nettree-row--dragging");
if (!src || src._dragTree?.node === node) return;
e.preventDefault();
e.dataTransfer.dropEffect = "move";
const rect = e.currentTarget.getBoundingClientRect();
const y = e.clientY - rect.top;
const third = rect.height / 3;
e.currentTarget.closest(".pcr-nettree-items")?.querySelectorAll(".pcr-nettree-row").forEach(r => r.classList.remove("pcr-nettree-row--drop-before", "pcr-nettree-row--drop-after", "pcr-nettree-row--drop-onto"));
if (y < third) e.currentTarget.classList.add("pcr-nettree-row--drop-before");
else if (y > third * 2) e.currentTarget.classList.add("pcr-nettree-row--drop-after");
else e.currentTarget.classList.add("pcr-nettree-row--drop-onto");
}}
ondragleave={(e) => { e.currentTarget.classList.remove("pcr-nettree-row--drop-before", "pcr-nettree-row--drop-after", "pcr-nettree-row--drop-onto"); }}
ondrop={(e) => {
e.preventDefault();
const src = e.currentTarget.closest(".pcr-nettree-items")?.querySelector(".pcr-nettree-row--dragging");
if (!src || src._dragTree?.node === node) return;
const rect = e.currentTarget.getBoundingClientRect();
const y = e.clientY - rect.top;
const third = rect.height / 3;
const position = y < third ? "before" : y > third * 2 ? "after" : "into";
const sameParent = src._dragParent?.node === parentTree?.node;
// Drop-after on an expanded parent row means "the gap between parent and
// its first child" — file-explorer convention is prepend-as-first-child,
// not sibling-of-parent.
if (position === "after" && tree.hasChildren && !isCollapsed) {
onDragDrop("reparent", src._dragTree, tree, src._dragParent, parentTree, "first-child");
} else if (position !== "into" && sameParent && !isRoot) {
onDragDrop("reorder", src._dragTree, tree, src._dragParent, parentTree, position);
} else {
onDragDrop("reparent", src._dragTree, tree, src._dragParent, parentTree, position);
}
}}
>
{#if tree.hasChildren}
{isCollapsed ? "\u25B6" : "\u25BC"}
{/if}
{#if !isRoot && parentTree}
e.stopPropagation()}
onclick={handleIndicatorClick}
>
{#if parentMode === "switch"}
{isSelected ? "\u2705" : "\u2610"}
{:else}
{MODE_EMOJI[parentMode] || ""}
{/if}
{/if}
{#if isRenaming}
e.stopPropagation()}
onclick={(e) => e.stopPropagation()}
onblur={commitRename}
onkeydown={(e) => {
e.stopPropagation();
if (e.key === "Enter") { e.preventDefault(); commitRename(); }
else if (e.key === "Escape") { e.preventDefault(); cancelRename(); }
}}
/>
{:else}
{tree.title}
{/if}
{ e.stopPropagation(); onToggleLock(node); }}>{"\u{1F512}"}
{ e.stopPropagation(); onToggleDisable(node); }}>{"\u2298"}
{#if tree.hasChildren && !isCollapsed}
{#each tree.children as child}
{/each}
{#if tree.labels.length > 0}
l.index === switchIndex)}
onpointerdown={(e) => e.stopPropagation()}
onclick={() => {
// Always open the owning tab first — onLabelClick jumps the
// cursor in whatever editor is currently active, so the tab
// must be swapped before we dispatch the selection.
onSelectNode(tree);
const sel = tree.labels.find(l => l.index === switchIndex);
if (sel) onLabelClick(node, sel.index);
}}
oncontextmenu={(e) => { e.preventDefault(); e.stopPropagation(); onContextMenu(node, tree, parentTree, e.clientX, e.clientY); }}
>
e.stopPropagation()}
onclick={(e) => { e.stopPropagation(); onLabelClick(node, null, e.currentTarget); }}
>
{#if tree.labels.some(l => l.index === switchIndex)}
{"\u2705"}
{:else if mode === "switch" && switchIndex === 0}
{"\u274C"}
{:else}
{MODE_EMOJI[mode] || "\u{1F3B2}"}
{/if}
{#if tree.labels.some(l => l.index === switchIndex)}
{tree.labels.find(l => l.index === switchIndex)?.label}
{:else if mode === "switch" && switchIndex === 0}
None
{:else}
{tree.labels.length} options
{/if}
{/if}
{#each tree.wildcards as wc}
{@const keyName = wc.name.includes("/") ? wc.name.split("/").pop() : wc.name}
{@const hasRolled = wc.mode === "randomize" && wc.rolledLabel}
e.stopPropagation()}
onclick={() => onSelectNode(tree, wc.name)}
oncontextmenu={(e) => { e.preventDefault(); e.stopPropagation(); onSelectNode(tree, wc.name); }}>
e.stopPropagation()}
onclick={(e) => { e.stopPropagation(); onWildcardModeClick(node, wc.name, e.currentTarget); }}
>{wc.mode === "switch" && wc.index > 0 ? "\u2705" : MODE_EMOJI[wc.mode === "randomize" ? "roll" : wc.mode] || "\u{1F3B2}"}
0 ? MODE_COLORS.switch : wc.mode === "combine" ? MODE_COLORS.combine : wc.mode === "iterate" ? MODE_COLORS.iterate : wc.mode === "none" ? "#b0b0b0" : MODE_COLORS.roll}
>
{#if hasRolled}
{wc.rolledLabel}
{:else if wc.mode === "switch" && wc.label}
{keyName}: {wc.label}
{:else if wc.mode === "combine"}
{keyName} (all)
{:else if wc.mode === "none"}
{keyName} (off)
{:else}
{keyName}
{/if}
{/each}
{/if}