/* Shared icons, FileIcon, GitPanel, FileTree — Vue 3 classic-script port */
const { ref, computed, onMounted, onUnmounted, watch, nextTick } = Vue;
/* ---- Icon component ---- */
/* Internal map: name → { vb, inner } */
const _iconMap = {
search: {
vb: "0 0 16 16",
inner: ``
},
branch: {
vb: "0 0 16 16",
inner: ``
},
close: {
vb: "0 0 12 12",
inner: ``
},
copy: {
vb: "0 0 16 16",
inner: ``
},
terminal: {
vb: "0 0 16 16",
inner: ``
},
spark: {
vb: "0 0 16 16",
inner: ``
},
file: {
vb: "0 0 16 16",
inner: ``
},
reveal: {
vb: "0 0 16 16",
inner: ``
},
diff: {
vb: "0 0 16 16",
inner: ``
},
plus: {
vb: "0 0 14 14",
inner: ``
},
minus: {
vb: "0 0 14 14",
inner: ``
},
check: {
vb: "0 0 14 14",
inner: ``
},
discard: {
vb: "0 0 16 16",
inner: ``
},
};
const Icon = {
props: {
name: { type: String, required: true },
w: { type: Number, default: 13 },
h: { type: Number, default: 13 },
},
setup(props) {
const entry = computed(() => _iconMap[props.name] || null);
const vb = computed(() => entry.value ? entry.value.vb : "0 0 16 16");
const inner = computed(() => entry.value ? entry.value.inner : "");
return { vb, inner };
},
template: ``,
};
/* ---- Chevron ---- */
const Chevron = {
props: {
open: { type: Boolean, default: false },
},
template: `
`,
};
/* ---- FolderIcon ---- */
const FolderIcon = {
props: {
open: { type: Boolean, default: false },
},
template: `
`,
};
/* ---- FileIcon ---- */
const FileIcon = {
props: {
path: { type: String, required: true },
},
setup(props) {
const ic = computed(() => HL.iconFor(props.path));
return { ic };
},
template: `
{{ ic.t }}
`,
};
/* ---- GitRow ---- */
const GitRow = {
props: {
c: { type: Object, required: true },
staged: { type: Boolean, required: true },
activePath: { type: String, default: null },
onOpen: { type: Function, required: true },
onContext: { type: Function, required: true },
onToggleStage: { type: Function, required: true },
},
components: { Icon, FileIcon },
setup(props) {
const name = computed(() => props.c.path.split("/").pop());
const dir = computed(() => props.c.path.split("/").slice(0, -1).join("/"));
return { name, dir };
},
template: `
onContext(e, { path: c.path, kind: 'git', staged })"
:title="c.path">
{{ c.status }}
{{ name }}
{{ dir }}/
+{{ c.add }}
-{{ c.del }}
`,
};
/* ---- GitPanel (multi-root fragment) ---- */
const GitPanel = {
props: {
changes: { type: Array, required: true },
staged: { type: Object, required: true }, /* Set */
committed: { type: Object, required: true }, /* Set */
commitMsg: { type: String, required: true },
setCommitMsg: { type: Function, required: true },
onStage: { type: Function, required: true },
onUnstage: { type: Function, required: true },
onStageAll: { type: Function, required: true },
onUnstageAll: { type: Function, required: true },
onCommit: { type: Function, required: true },
onOpen: { type: Function, required: true },
onContext: { type: Function, required: true },
activePath: { type: String, default: null },
},
components: { Icon, GitRow },
setup(props) {
const visible = computed(() => props.changes.filter((c) => !props.committed.has(c.path)));
const stagedList = computed(() => visible.value.filter((c) => props.staged.has(c.path)));
const changesList = computed(() => visible.value.filter((c) => !props.staged.has(c.path)));
const totals = computed(() =>
visible.value.reduce((a, c) => ({ add: a.add + c.add, del: a.del + c.del }), { add: 0, del: 0 })
);
const canCommit = computed(() => stagedList.value.length > 0 && props.commitMsg.trim().length > 0);
function handleKeyDown(e) {
if ((e.metaKey || e.ctrlKey) && e.key === "Enter" && canCommit.value) {
e.preventDefault();
props.onCommit();
}
}
return { visible, stagedList, changesList, totals, canCommit, handleKeyDown };
},
/* Vue 3 allows multi-root templates */
template: `
Source Control
{{ visible.length }}
No changes — working tree clean
Staged Changes {{ stagedList.length }}
Nothing staged — use + to stage a file
Changes {{ changesList.length }}
All changes staged
`,
};
/* ---- TreeNode (recursive — registered globally) ---- */
const TreeNode = {
name: "TreeNode",
props: {
node: { type: Object, required: true },
depth: { type: Number, required: true },
openDirs: { type: Object, required: true }, /* Set */
toggleDir: { type: Function, required: true },
onOpen: { type: Function, required: true },
onContext: { type: Function, required: true },
activePath: { type: String, default: null },
changeMap: { type: Object, required: true },
committed: { type: Object, default: null }, /* Set or null */
},
components: { Chevron, FolderIcon, FileIcon },
setup(props) {
const pad = computed(() => 10 + props.depth * 13);
const isOpen = computed(() => props.node.type === "dir" && (props.openDirs.has(props.node.path) || props.node.path === ""));
const status = computed(() => {
if (props.node.type === "dir") return null;
return (props.committed && props.committed.has(props.node.path)) ? null : props.changeMap[props.node.path];
});
return { pad, isOpen, status };
},
/* Multi-root fragment for the dir case */
template: `
onContext(e, { path: node.path, kind: 'dir' })">
{{ node.name }}
onContext(e, { path: node.path, kind: 'file' })"
:title="node.path">
{{ node.name }}
{{ status }}
`,
};
/* Self-reference: register TreeNode in itself for recursion */
TreeNode.components = Object.assign(TreeNode.components || {}, { TreeNode });
/* ---- FileTree (multi-root fragment) ---- */
const FileTree = {
props: {
tree: { type: Object, required: true },
openDirs: { type: Object, required: true }, /* Set */
toggleDir: { type: Function, required: true },
onOpen: { type: Function, required: true },
onContext: { type: Function, required: true },
activePath: { type: String, default: null },
changeMap: { type: Object, required: true },
committed: { type: Object, default: null }, /* Set or null */
},
components: { TreeNode },
template: `
Explorer
{{ tree.name }}
`,
};
/* ---- Global export ---- */
window.HelderComponents = Object.assign(window.HelderComponents || {}, {
Icon,
Chevron,
FolderIcon,
FileIcon,
GitRow,
GitPanel,
TreeNode,
FileTree,
});