/* 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: `
{{ 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
{{ PROJECT.branch }} +{{ totals.add }}{{ ' ' }} -{{ totals.del }}
`, }; /* ---- 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: `
{{ 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, });