93 lines
3.2 KiB
Vue
93 lines
3.2 KiB
Vue
<script setup lang="ts">
|
|
import { isRefVisible } from "../lib/filter";
|
|
import { refsUnder, type TreeNode } from "../lib/tree";
|
|
import type { FilterState, Ref, RefKind } from "../lib/types";
|
|
import EyeButton from "./EyeButton.vue";
|
|
import RefIcon from "./RefIcon.vue";
|
|
|
|
const props = defineProps<{
|
|
nodes: TreeNode[];
|
|
kind: RefKind;
|
|
depth: number;
|
|
filters: FilterState;
|
|
headRef: string | null;
|
|
/** Paths of folders the user closed. Shared by the whole tree. */
|
|
closed: Set<string>;
|
|
}>();
|
|
const emit = defineEmits<{ jump: [ref: Ref]; toggle: [names: string[]] }>();
|
|
|
|
const allHidden = (n: TreeNode) => refsUnder(n).every((r) => props.filters.hidden.includes(r.name));
|
|
const anyVisible = (n: TreeNode) => refsUnder(n).some((r) => isRefVisible(r.name, props.filters));
|
|
|
|
// Keyed by kind too, so a `backup` tag folder and a `backup` branch folder are separate.
|
|
const key = (n: TreeNode) => `${props.kind}:${n.path}`;
|
|
function toggleFolder(n: TreeNode) {
|
|
if (!props.closed.delete(key(n))) props.closed.add(key(n));
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<template v-for="n in nodes" :key="n.path">
|
|
<div
|
|
class="item"
|
|
:class="{ off: !anyVisible(n), head: n.ref?.name === headRef, folder: n.children.length }"
|
|
:style="{ paddingLeft: 10 + depth * 14 + 'px' }"
|
|
@click="n.ref ? emit('jump', n.ref) : toggleFolder(n)"
|
|
>
|
|
<span
|
|
v-if="n.children.length"
|
|
class="arrow"
|
|
:class="{ open: !closed.has(key(n)) }"
|
|
@click.stop="toggleFolder(n)"
|
|
>▸</span>
|
|
<span v-else class="arrow" />
|
|
<RefIcon v-if="n.ref" :kind="kind" />
|
|
<svg v-else class="folder-icon" viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
<path d="M1.5 4a1 1 0 0 1 1-1h3.5l1.5 1.5h6a1 1 0 0 1 1 1V12a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1z" />
|
|
</svg>
|
|
<span class="name" :title="n.ref?.name ?? n.path">{{ n.label }}</span>
|
|
<EyeButton
|
|
v-if="kind === 'tag' && filters.hideTags"
|
|
class="locked"
|
|
:shut="true"
|
|
disabled
|
|
title="all tags are hidden: use the eye next to Tags"
|
|
@click.stop
|
|
/>
|
|
<EyeButton v-else :shut="allHidden(n)" @click.stop="emit('toggle', refsUnder(n).map((r) => r.name))" />
|
|
</div>
|
|
<RefTree
|
|
v-if="n.children.length && !closed.has(key(n))"
|
|
:nodes="n.children"
|
|
:kind="kind"
|
|
:depth="depth + 1"
|
|
:filters="filters"
|
|
:head-ref="headRef"
|
|
:closed="closed"
|
|
@jump="emit('jump', $event)"
|
|
@toggle="emit('toggle', $event)"
|
|
/>
|
|
</template>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 2px 8px 2px 10px;
|
|
cursor: pointer;
|
|
color: var(--text);
|
|
}
|
|
.item:hover { background: var(--bg-hover); }
|
|
.item.off { color: var(--text-dim); opacity: 0.55; }
|
|
.item.head .name { font-weight: 600; color: #fff; }
|
|
.item.folder { color: #c5cae0; }
|
|
.folder-icon { flex: none; color: #ffd43b; opacity: 0.8; }
|
|
.arrow { width: 10px; flex: none; color: var(--text-dim); font-size: 10px; transition: transform 0.12s; }
|
|
.arrow.open { transform: rotate(90deg); }
|
|
.name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.item:hover :deep(.eye) { opacity: 1; }
|
|
.item :deep(.eye.locked) { opacity: 0.35; cursor: default; }
|
|
</style>
|