@leavepulse/ui 0.15.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/{LpAppShell-BLKZDLh8.js → LpAppShell-CV194os6.js} +2 -2
- package/dist/chunks/{LpAutocomplete-B9uoCs4t.js → LpAutocomplete-DIgXmtCA.js} +13 -5
- package/dist/chunks/{LpConfirmDialog-BA45MvRL.js → LpConfirmDialog-WHkymac5.js} +1 -1
- package/dist/chunks/{LpDrawer-6n5Hd-iF.js → LpDrawer-BHzCy-Aa.js} +1 -1
- package/dist/chunks/{LpFileTree-D0DEpKof.js → LpFileTree-BQz8Rfgz.js} +49 -32
- package/dist/chunks/{LpFileTreeNode-3e71ywJV.js → LpFileTreeNode-DFg4PhOK.js} +10 -7
- package/dist/chunks/{LpLogViewer-BBgaY38i.js → LpLogViewer-BvzRZPjs.js} +2 -1
- package/dist/chunks/{LpModal-B9CVVC5U.js → LpModal-DpBHQpbU.js} +1 -1
- package/dist/chunks/{LpNotificationBell-B9VHks_D.js → LpNotificationBell-BtfRoL2i.js} +1 -1
- package/dist/chunks/{LpScrollArea-BtnZCCBT.js → LpScrollArea-De9DLpa1.js} +2 -2
- package/dist/chunks/{LpSidebar-tZ2z-PnM.js → LpSidebar-B11IDRQi.js} +2 -2
- package/dist/chunks/{LpSidebarNav-D-YvtwsH.js → LpSidebarNav-TIUNHZX-.js} +1 -1
- package/dist/chunks/{LpTable-CzHVcr3v.js → LpTable-CKGaUbEM.js} +1 -1
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpAutocomplete.vue.js +1 -1
- package/dist/components/LpCommandPalette.vue.js +1 -1
- package/dist/components/LpConfirmDialog.vue.js +1 -1
- package/dist/components/LpDrawer.vue.js +1 -1
- package/dist/components/LpFileTree.vue.js +1 -1
- package/dist/components/LpFileTreeNode.vue.d.ts +3 -1
- package/dist/components/LpFileTreeNode.vue.js +1 -1
- package/dist/components/LpLogViewer.vue.js +1 -1
- package/dist/components/LpModal.vue.js +1 -1
- package/dist/components/LpNotificationBell.vue.js +1 -1
- package/dist/components/LpScrollArea.vue.d.ts +4 -2
- package/dist/components/LpScrollArea.vue.js +1 -1
- package/dist/components/LpSidebar.vue.js +1 -1
- package/dist/components/LpSidebarNav.vue.js +1 -1
- package/dist/components/LpTable.vue.js +1 -1
- package/dist/components/fileTree.d.ts +16 -0
- package/dist/components/fileTree.js +49 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +13 -12
- package/package.json +1 -1
- package/src/components/LpAutocomplete.vue +19 -2
- package/src/components/LpFileTree.vue +67 -35
- package/src/components/LpFileTreeNode.vue +16 -9
- package/src/components/LpLogViewer.vue +4 -0
- package/src/components/LpScrollArea.vue +16 -11
- package/src/components/fileTree.ts +75 -0
- package/src/index.ts +2 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { defineComponent, computed, watch, openBlock, createElementBlock, createVNode, createSlots, withCtx, renderSlot, normalizeProps, guardReactiveProps, createElementVNode, normalizeClass, toDisplayString, createCommentVNode, createBlock } from "vue";
|
|
2
2
|
import { _ as _sfc_main$2 } from "./LpButton-5VXXCjwF.js";
|
|
3
3
|
import { _ as _sfc_main$3 } from "./LpIcon-CCnX5_2j.js";
|
|
4
|
-
import { _ as _sfc_main$4 } from "./LpScrollArea-
|
|
5
|
-
import { _ as _sfc_main$1 } from "./LpSidebar-
|
|
4
|
+
import { _ as _sfc_main$4 } from "./LpScrollArea-De9DLpa1.js";
|
|
5
|
+
import { _ as _sfc_main$1 } from "./LpSidebar-B11IDRQi.js";
|
|
6
6
|
const _hoisted_1 = { class: "flex h-dvh overflow-hidden bg-surface text-ink" };
|
|
7
7
|
const _hoisted_2 = { class: "flex min-w-0 flex-1 flex-col" };
|
|
8
8
|
const _hoisted_3 = { class: "flex h-16 shrink-0 items-center gap-3 border-b border-line bg-surface px-4 md:px-6" };
|
|
@@ -50,9 +50,10 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
50
50
|
(o) => typeof o === "string" ? { value: o } : o
|
|
51
51
|
)
|
|
52
52
|
);
|
|
53
|
+
const typing = ref(false);
|
|
53
54
|
const filtered = computed(() => {
|
|
54
55
|
const q = text.value.trim().toLowerCase();
|
|
55
|
-
if (!props.filter || !q) return items.value;
|
|
56
|
+
if (!props.filter || !typing.value || !q) return items.value;
|
|
56
57
|
return items.value.filter((o) => {
|
|
57
58
|
const hay = `${o.label ?? o.value} ${o.description ?? ""}`.toLowerCase();
|
|
58
59
|
return hay.includes(q);
|
|
@@ -72,23 +73,30 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
72
73
|
md: "h-(--size-control-md) text-sm",
|
|
73
74
|
lg: "h-(--size-control-lg) text-sm"
|
|
74
75
|
};
|
|
76
|
+
function onInput(event) {
|
|
77
|
+
typing.value = true;
|
|
78
|
+
text.value = event.target.value;
|
|
79
|
+
}
|
|
75
80
|
function onFocus() {
|
|
81
|
+
typing.value = false;
|
|
76
82
|
focused.value = true;
|
|
77
83
|
if (canOpen.value) open.value = true;
|
|
78
84
|
}
|
|
79
85
|
function choose(opt) {
|
|
86
|
+
typing.value = false;
|
|
80
87
|
emit("update:modelValue", opt.value);
|
|
81
88
|
emit("select", opt.value);
|
|
82
89
|
open.value = false;
|
|
83
90
|
}
|
|
84
91
|
function clear() {
|
|
92
|
+
typing.value = false;
|
|
85
93
|
emit("update:modelValue", "");
|
|
86
94
|
open.value = false;
|
|
87
95
|
}
|
|
88
96
|
return (_ctx, _cache) => {
|
|
89
97
|
return openBlock(), createBlock(unref(ComboboxRoot), {
|
|
90
98
|
open: open.value,
|
|
91
|
-
"onUpdate:open": _cache[
|
|
99
|
+
"onUpdate:open": _cache[2] || (_cache[2] = ($event) => open.value = $event),
|
|
92
100
|
"ignore-filter": true,
|
|
93
101
|
"reset-search-term-on-blur": false,
|
|
94
102
|
"reset-search-term-on-select": false,
|
|
@@ -118,9 +126,9 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
118
126
|
class: "min-w-0 flex-1 bg-transparent outline-none placeholder:text-muted",
|
|
119
127
|
onBeforeinput: unref(onBeforeInput),
|
|
120
128
|
onPaste: unref(onPaste),
|
|
121
|
-
onInput
|
|
129
|
+
onInput,
|
|
122
130
|
onFocus,
|
|
123
|
-
onBlur: _cache[
|
|
131
|
+
onBlur: _cache[0] || (_cache[0] = ($event) => focused.value = false)
|
|
124
132
|
}, null, 8, ["model-value", "placeholder", "disabled", "aria-invalid", "onBeforeinput", "onPaste"]),
|
|
125
133
|
__props.clearable && hasValue.value ? (openBlock(), createElementBlock("button", {
|
|
126
134
|
key: 1,
|
|
@@ -144,7 +152,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
144
152
|
position: "popper",
|
|
145
153
|
"side-offset": 6,
|
|
146
154
|
class: normalizeClass([unref(POPOVER_PANEL), "z-(--z-popover) max-h-72 w-(--reka-combobox-trigger-width) overflow-hidden"]),
|
|
147
|
-
onOpenAutoFocus: _cache[
|
|
155
|
+
onOpenAutoFocus: _cache[1] || (_cache[1] = withModifiers(() => {
|
|
148
156
|
}, ["prevent"]))
|
|
149
157
|
}, {
|
|
150
158
|
default: withCtx(() => [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineComponent, openBlock, createBlock, withCtx, renderSlot, createVNode, createTextVNode, toDisplayString } from "vue";
|
|
2
2
|
import { _ as _sfc_main$2 } from "./LpButton-5VXXCjwF.js";
|
|
3
|
-
import { _ as _sfc_main$1 } from "./LpModal-
|
|
3
|
+
import { _ as _sfc_main$1 } from "./LpModal-DpBHQpbU.js";
|
|
4
4
|
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
5
5
|
__name: "LpConfirmDialog",
|
|
6
6
|
props: {
|
|
@@ -3,7 +3,7 @@ import { usePointerSwipe } from "@vueuse/core";
|
|
|
3
3
|
import { DrawerRoot, DrawerPortal, DrawerOverlay, DrawerContent, DrawerHandle, DrawerTitle, DrawerDescription, DrawerClose } from "vaul-vue";
|
|
4
4
|
import { CLOSE_ICON } from "../components/dropdown.js";
|
|
5
5
|
import { _ as _sfc_main$1 } from "./LpIcon-CCnX5_2j.js";
|
|
6
|
-
import { _ as _sfc_main$2 } from "./LpScrollArea-
|
|
6
|
+
import { _ as _sfc_main$2 } from "./LpScrollArea-De9DLpa1.js";
|
|
7
7
|
const _hoisted_1 = { class: "text-base font-semibold text-ink" };
|
|
8
8
|
const _hoisted_2 = { class: "flex flex-col gap-1" };
|
|
9
9
|
const _hoisted_3 = ["data-vaul-no-drag"];
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { defineComponent, ref, watch, computed, openBlock, createElementBlock, Fragment, renderList, normalizeStyle, createBlock, createVNode, withCtx, createElementVNode, renderSlot, mergeProps, normalizeProps, guardReactiveProps, createTextVNode, toDisplayString, createCommentVNode } from "vue";
|
|
2
|
-
import {
|
|
2
|
+
import { formatSize, sortNodes, computeStats, subtreeIds, ancestorIds } from "../components/fileTree.js";
|
|
3
3
|
import { _ as _sfc_main$1 } from "./LpEmptyState-CrfyRJUG.js";
|
|
4
|
-
import { _ as _sfc_main$3 } from "./LpFileTreeNode-
|
|
5
|
-
import { _ as _sfc_main$2 } from "./LpScrollArea-
|
|
4
|
+
import { _ as _sfc_main$3 } from "./LpFileTreeNode-DFg4PhOK.js";
|
|
5
|
+
import { _ as _sfc_main$2 } from "./LpScrollArea-De9DLpa1.js";
|
|
6
6
|
const _hoisted_1 = {
|
|
7
7
|
key: 0,
|
|
8
8
|
class: "flex flex-col gap-1"
|
|
@@ -53,43 +53,56 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
53
53
|
if (ids) checkedIds.value = new Set(ids);
|
|
54
54
|
}
|
|
55
55
|
);
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
56
|
+
const index = computed(() => {
|
|
57
|
+
const byId2 = /* @__PURE__ */ new Map();
|
|
58
|
+
const parentOfId = /* @__PURE__ */ new Map();
|
|
59
|
+
function walk(list, parent) {
|
|
59
60
|
for (const node of list) {
|
|
60
|
-
|
|
61
|
-
if (
|
|
61
|
+
byId2.set(node.id, node);
|
|
62
|
+
if (parent !== void 0) parentOfId.set(node.id, parent);
|
|
63
|
+
if (node.children) walk(node.children, node.id);
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
walk(props.nodes);
|
|
65
|
-
return
|
|
67
|
+
return { byId: byId2, parentOfId };
|
|
66
68
|
});
|
|
69
|
+
const byId = computed(() => index.value.byId);
|
|
67
70
|
const summary = computed(() => {
|
|
68
71
|
let files = 0;
|
|
69
72
|
let dirs = 0;
|
|
70
73
|
let bytes = 0;
|
|
71
74
|
let sized = false;
|
|
72
|
-
|
|
73
|
-
const node
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (size !== void 0) {
|
|
81
|
-
bytes += size;
|
|
75
|
+
function walk(list) {
|
|
76
|
+
for (const node of list) {
|
|
77
|
+
const stat = stats.value.get(node.id);
|
|
78
|
+
if (!stat || stat.state === "unchecked") continue;
|
|
79
|
+
if (node.kind === "file") {
|
|
80
|
+
files++;
|
|
81
|
+
if (node.size !== void 0) {
|
|
82
|
+
bytes += node.size;
|
|
82
83
|
sized = true;
|
|
83
84
|
}
|
|
85
|
+
continue;
|
|
84
86
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
bytes += node.size;
|
|
87
|
+
dirs++;
|
|
88
|
+
if (stat.state === "checked" && stat.size !== void 0) {
|
|
89
|
+
bytes += stat.size;
|
|
89
90
|
sized = true;
|
|
91
|
+
files += stat.total;
|
|
92
|
+
countDirs(node.children ?? []);
|
|
93
|
+
continue;
|
|
90
94
|
}
|
|
95
|
+
walk(node.children ?? []);
|
|
91
96
|
}
|
|
92
97
|
}
|
|
98
|
+
function countDirs(list) {
|
|
99
|
+
for (const node of list) {
|
|
100
|
+
if (node.kind !== "dir") continue;
|
|
101
|
+
dirs++;
|
|
102
|
+
countDirs(node.children ?? []);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
walk(props.nodes);
|
|
93
106
|
return {
|
|
94
107
|
files,
|
|
95
108
|
dirs,
|
|
@@ -111,12 +124,15 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
111
124
|
if (value) next.add(id);
|
|
112
125
|
else next.delete(id);
|
|
113
126
|
}
|
|
114
|
-
|
|
127
|
+
let ancestorId = index.value.parentOfId.get(node.id);
|
|
128
|
+
while (ancestorId !== void 0) {
|
|
115
129
|
const ancestor = byId.value.get(ancestorId);
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
130
|
+
if (ancestor?.children?.length) {
|
|
131
|
+
const all = ancestor.children.every((c) => c.disabled || next.has(c.id));
|
|
132
|
+
if (all) next.add(ancestorId);
|
|
133
|
+
else next.delete(ancestorId);
|
|
134
|
+
}
|
|
135
|
+
ancestorId = index.value.parentOfId.get(ancestorId);
|
|
120
136
|
}
|
|
121
137
|
setChecked(next);
|
|
122
138
|
}
|
|
@@ -130,6 +146,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
130
146
|
}
|
|
131
147
|
const requested = /* @__PURE__ */ new Set();
|
|
132
148
|
const roots = computed(() => props.sort ? sortNodes(props.nodes) : props.nodes);
|
|
149
|
+
const stats = computed(() => computeStats(props.nodes, checkedIds.value));
|
|
133
150
|
const visible = computed(() => {
|
|
134
151
|
const out = [];
|
|
135
152
|
function walk(list) {
|
|
@@ -204,8 +221,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
204
221
|
if (node.kind === "file") emit("open", node);
|
|
205
222
|
}
|
|
206
223
|
function parentOf(id) {
|
|
207
|
-
const
|
|
208
|
-
const parentId = trail.at(-1);
|
|
224
|
+
const parentId = index.value.parentOfId.get(id);
|
|
209
225
|
return parentId ? visible.value.find((n) => n.id === parentId) : void 0;
|
|
210
226
|
}
|
|
211
227
|
function move(delta) {
|
|
@@ -260,7 +276,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
260
276
|
if (!node) return;
|
|
261
277
|
e.preventDefault();
|
|
262
278
|
if (props.checkable) {
|
|
263
|
-
onCheck(node,
|
|
279
|
+
onCheck(node, stats.value.get(node.id)?.state !== "checked");
|
|
264
280
|
break;
|
|
265
281
|
}
|
|
266
282
|
onSelect(node);
|
|
@@ -338,6 +354,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
338
354
|
"icon-size": __props.iconSize,
|
|
339
355
|
checkable: __props.checkable,
|
|
340
356
|
checked: checkedIds.value,
|
|
357
|
+
stats: stats.value,
|
|
341
358
|
"show-size": __props.showSize,
|
|
342
359
|
"show-modified": __props.showModified,
|
|
343
360
|
onSelect,
|
|
@@ -349,7 +366,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
349
366
|
renderSlot(_ctx.$slots, "row", mergeProps({ ref_for: true }, slotProps))
|
|
350
367
|
]),
|
|
351
368
|
_: 3
|
|
352
|
-
}, 8, ["node", "expanded", "loading", "selected-id", "focused-id", "sort", "icon-size", "checkable", "checked", "show-size", "show-modified"]);
|
|
369
|
+
}, 8, ["node", "expanded", "loading", "selected-id", "focused-id", "sort", "icon-size", "checkable", "checked", "stats", "show-size", "show-modified"]);
|
|
353
370
|
}), 128))
|
|
354
371
|
], 40, _hoisted_3)
|
|
355
372
|
]),
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { defineComponent, computed, resolveComponent, openBlock, createElementBlock, createVNode, withCtx, createElementVNode, withModifiers, normalizeStyle, normalizeClass, createBlock, createCommentVNode, unref, renderSlot, createTextVNode, toDisplayString, Transition, Fragment, renderList, mergeProps } from "vue";
|
|
2
|
-
import { sortNodes,
|
|
2
|
+
import { sortNodes, formatSize, formatModified, fileIcon } from "../components/fileTree.js";
|
|
3
3
|
import { _ as _sfc_main$3 } from "./LpCheckbox-BO0zuuPh.js";
|
|
4
4
|
import { _ as _sfc_main$1 } from "./LpContextMenu-D0mzXqA7.js";
|
|
5
5
|
import { _ as _sfc_main$2 } from "./LpIcon-CCnX5_2j.js";
|
|
6
6
|
const _hoisted_1 = ["aria-level", "aria-selected", "aria-expanded", "aria-disabled"];
|
|
7
7
|
const _hoisted_2 = ["data-node-id", "tabindex"];
|
|
8
8
|
const _hoisted_3 = { class: "flex size-4 shrink-0 items-center justify-center" };
|
|
9
|
-
const _hoisted_4 = { class: "min-w-0 flex-1 truncate" };
|
|
9
|
+
const _hoisted_4 = { class: "min-w-0 flex-1 shrink-[2] truncate" };
|
|
10
10
|
const _hoisted_5 = {
|
|
11
11
|
key: 1,
|
|
12
12
|
class: "shrink-0 rounded-pill bg-brand-soft px-1.5 py-0.5 text-[11px] font-medium tabular-nums text-brand"
|
|
13
13
|
};
|
|
14
14
|
const _hoisted_6 = {
|
|
15
15
|
key: 2,
|
|
16
|
-
class: "
|
|
16
|
+
class: "min-w-0 shrink truncate text-xs text-muted"
|
|
17
17
|
};
|
|
18
18
|
const _hoisted_7 = {
|
|
19
19
|
key: 3,
|
|
@@ -41,6 +41,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
41
41
|
iconSize: {},
|
|
42
42
|
checkable: { type: Boolean },
|
|
43
43
|
checked: {},
|
|
44
|
+
stats: {},
|
|
44
45
|
showSize: { type: Boolean },
|
|
45
46
|
showModified: { type: Boolean }
|
|
46
47
|
},
|
|
@@ -59,17 +60,18 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
59
60
|
const list = props.node.children ?? [];
|
|
60
61
|
return props.sort ? sortNodes(list) : list;
|
|
61
62
|
});
|
|
63
|
+
const stat = computed(() => props.stats.get(props.node.id));
|
|
62
64
|
const checkState = computed(
|
|
63
|
-
() => props.checkable ?
|
|
65
|
+
() => props.checkable ? stat.value?.state ?? "unchecked" : "unchecked"
|
|
64
66
|
);
|
|
65
67
|
const sizeLabel = computed(() => {
|
|
66
68
|
if (!props.showSize) return "";
|
|
67
|
-
const bytes =
|
|
69
|
+
const bytes = stat.value?.size;
|
|
68
70
|
return bytes === void 0 ? "" : formatSize(bytes);
|
|
69
71
|
});
|
|
70
72
|
const countLabel = computed(() => {
|
|
71
73
|
if (!props.checkable || !isDir.value) return "";
|
|
72
|
-
const { checked, total } =
|
|
74
|
+
const { checked = 0, total = 0 } = stat.value ?? {};
|
|
73
75
|
return total && checked && checked < total ? `${checked} / ${total}` : "";
|
|
74
76
|
});
|
|
75
77
|
const modifiedLabel = computed(
|
|
@@ -201,6 +203,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
201
203
|
"icon-size": __props.iconSize,
|
|
202
204
|
checkable: __props.checkable,
|
|
203
205
|
checked: __props.checked,
|
|
206
|
+
stats: __props.stats,
|
|
204
207
|
"show-size": __props.showSize,
|
|
205
208
|
"show-modified": __props.showModified,
|
|
206
209
|
onSelect: _cache[2] || (_cache[2] = ($event) => emit("select", $event)),
|
|
@@ -212,7 +215,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
212
215
|
renderSlot(_ctx.$slots, "row", mergeProps({ ref_for: true }, slotProps))
|
|
213
216
|
]),
|
|
214
217
|
_: 3
|
|
215
|
-
}, 8, ["style", "node", "depth", "expanded", "loading", "selected-id", "focused-id", "sort", "icon-size", "checkable", "checked", "show-size", "show-modified"]);
|
|
218
|
+
}, 8, ["style", "node", "depth", "expanded", "loading", "selected-id", "focused-id", "sort", "icon-size", "checkable", "checked", "stats", "show-size", "show-modified"]);
|
|
216
219
|
}), 128))
|
|
217
220
|
])) : createCommentVNode("", true)
|
|
218
221
|
]),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineComponent, computed, ref, watch, nextTick, onMounted, onBeforeUnmount, openBlock, createElementBlock, createVNode, normalizeStyle, withCtx, createElementVNode, toDisplayString, createCommentVNode, Fragment, createBlock, resolveDynamicComponent, TransitionGroup, mergeProps, renderList, normalizeClass, createTextVNode, Transition } from "vue";
|
|
2
2
|
import { _ as _sfc_main$3 } from "./LpContextMenu-D0mzXqA7.js";
|
|
3
3
|
import { _ as _sfc_main$2 } from "./LpIcon-CCnX5_2j.js";
|
|
4
|
-
import { _ as _sfc_main$1 } from "./LpScrollArea-
|
|
4
|
+
import { _ as _sfc_main$1 } from "./LpScrollArea-De9DLpa1.js";
|
|
5
5
|
const _hoisted_1 = { class: "relative overflow-hidden rounded-card border border-line bg-surface font-mono text-xs leading-relaxed" };
|
|
6
6
|
const _hoisted_2 = {
|
|
7
7
|
key: 0,
|
|
@@ -354,6 +354,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
354
354
|
ref_key: "scrollRef",
|
|
355
355
|
ref: scrollRef,
|
|
356
356
|
fade: "",
|
|
357
|
+
instant: "",
|
|
357
358
|
"content-class": __props.wrap ? "" : "min-w-max",
|
|
358
359
|
style: normalizeStyle({ height: __props.height }),
|
|
359
360
|
onScroll
|
|
@@ -2,7 +2,7 @@ import { defineComponent, computed, useSlots, openBlock, createBlock, unref, wit
|
|
|
2
2
|
import { DialogRoot, DialogPortal, DialogOverlay, DialogContent, DialogTitle, DialogDescription, DialogClose } from "reka-ui";
|
|
3
3
|
import { CLOSE_ICON } from "../components/dropdown.js";
|
|
4
4
|
import { _ as _sfc_main$1 } from "./LpIcon-CCnX5_2j.js";
|
|
5
|
-
import { _ as _sfc_main$2 } from "./LpScrollArea-
|
|
5
|
+
import { _ as _sfc_main$2 } from "./LpScrollArea-De9DLpa1.js";
|
|
6
6
|
const _hoisted_1 = {
|
|
7
7
|
key: 0,
|
|
8
8
|
class: "flex shrink-0 items-start justify-between gap-4 p-5 pb-3"
|
|
@@ -5,7 +5,7 @@ import { _ as _sfc_main$4 } from "./LpContextMenu-D0mzXqA7.js";
|
|
|
5
5
|
import { _ as _sfc_main$2 } from "./LpEmptyState-CrfyRJUG.js";
|
|
6
6
|
import { _ as _sfc_main$5 } from "./LpIcon-CCnX5_2j.js";
|
|
7
7
|
import { _ as _sfc_main$1 } from "./LpPopover-8268UmlC.js";
|
|
8
|
-
import { _ as _sfc_main$3 } from "./LpScrollArea-
|
|
8
|
+
import { _ as _sfc_main$3 } from "./LpScrollArea-De9DLpa1.js";
|
|
9
9
|
const _hoisted_1 = { class: "flex items-center justify-between gap-2 px-3 py-2.5" };
|
|
10
10
|
const _hoisted_2 = { class: "text-sm font-semibold" };
|
|
11
11
|
const _hoisted_3 = {
|
|
@@ -5,7 +5,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
5
5
|
__name: "LpScrollArea",
|
|
6
6
|
props: {
|
|
7
7
|
fade: { type: Boolean },
|
|
8
|
-
|
|
8
|
+
instant: { type: Boolean, default: false },
|
|
9
9
|
contentClass: {},
|
|
10
10
|
barInsetTop: {}
|
|
11
11
|
},
|
|
@@ -47,7 +47,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
47
47
|
ref_key: "viewportRef",
|
|
48
48
|
ref: viewportRef,
|
|
49
49
|
class: normalizeClass(["size-full min-w-0 [&>div]:!block [&>div]:!min-w-0", [
|
|
50
|
-
__props.
|
|
50
|
+
__props.instant ? "" : "scroll-smooth motion-reduce:scroll-auto",
|
|
51
51
|
__props.fade ? "[mask-image:linear-gradient(to_bottom,transparent_0,black_14px,black_calc(100%-14px),transparent_100%)]" : ""
|
|
52
52
|
]]),
|
|
53
53
|
onScrollPassive: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("scroll", $event))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineComponent, computed, useId, openBlock, createElementBlock, Fragment, createElementVNode, mergeProps, normalizeClass, renderSlot, createCommentVNode, createVNode, createSlots, withCtx, normalizeProps, guardReactiveProps, createBlock } from "vue";
|
|
2
|
-
import { _ as _sfc_main$2 } from "./LpDrawer-
|
|
3
|
-
import { _ as _sfc_main$1 } from "./LpSidebarNav-
|
|
2
|
+
import { _ as _sfc_main$2 } from "./LpDrawer-BHzCy-Aa.js";
|
|
3
|
+
import { _ as _sfc_main$1 } from "./LpSidebarNav-TIUNHZX-.js";
|
|
4
4
|
const _hoisted_1 = {
|
|
5
5
|
key: 1,
|
|
6
6
|
class: "mt-2 flex shrink-0 flex-col gap-2"
|
|
@@ -4,7 +4,7 @@ import { usePillTransition } from "../composables/usePillTransition.js";
|
|
|
4
4
|
import { _ as _sfc_main$4 } from "./LpBadge-CXzBPnwO.js";
|
|
5
5
|
import { _ as _sfc_main$2 } from "./LpContextMenu-D0mzXqA7.js";
|
|
6
6
|
import { _ as _sfc_main$3 } from "./LpIcon-CCnX5_2j.js";
|
|
7
|
-
import { _ as _sfc_main$1 } from "./LpScrollArea-
|
|
7
|
+
import { _ as _sfc_main$1 } from "./LpScrollArea-De9DLpa1.js";
|
|
8
8
|
const _hoisted_1 = {
|
|
9
9
|
key: 0,
|
|
10
10
|
class: "flex min-h-0 flex-1 flex-col gap-4"
|
|
@@ -2,7 +2,7 @@ import { defineComponent, computed, ref, watch, onBeforeUnmount, openBlock, crea
|
|
|
2
2
|
import { _ as _sfc_main$2 } from "./LpCheckbox-BO0zuuPh.js";
|
|
3
3
|
import { _ as _sfc_main$4 } from "./LpContextMenu-D0mzXqA7.js";
|
|
4
4
|
import { _ as _sfc_main$3 } from "./LpIcon-CCnX5_2j.js";
|
|
5
|
-
import { _ as _sfc_main$1 } from "./LpScrollArea-
|
|
5
|
+
import { _ as _sfc_main$1 } from "./LpScrollArea-De9DLpa1.js";
|
|
6
6
|
const _hoisted_1 = { class: "w-full border-collapse text-sm" };
|
|
7
7
|
const _hoisted_2 = { class: "border-b border-line bg-surface-soft" };
|
|
8
8
|
const _hoisted_3 = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineComponent, ref, computed, watch, nextTick, onMounted, onBeforeUnmount, openBlock, createBlock, unref, withCtx, createVNode, withModifiers, createTextVNode, createElementVNode, withDirectives, withKeys, vModelText, createElementBlock, toDisplayString, createCommentVNode, Fragment, renderList, normalizeClass } from "vue";
|
|
2
2
|
import { DialogRoot, DialogPortal, DialogOverlay, DialogContent, DialogTitle, DialogDescription } from "reka-ui";
|
|
3
3
|
import { _ as _sfc_main$1 } from "../chunks/LpIcon-CCnX5_2j.js";
|
|
4
|
-
import { _ as _sfc_main$2 } from "../chunks/LpScrollArea-
|
|
4
|
+
import { _ as _sfc_main$2 } from "../chunks/LpScrollArea-De9DLpa1.js";
|
|
5
5
|
import '../assets/LpCommandPalette-D315B47-.css';const _hoisted_1 = { class: "flex shrink-0 items-center gap-2.5 border-b border-line px-4" };
|
|
6
6
|
const _hoisted_2 = ["placeholder", "onKeydown"];
|
|
7
7
|
const _hoisted_3 = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FileNode } from './fileTree';
|
|
1
|
+
import { FileNode, NodeStats } from './fileTree';
|
|
2
2
|
type __VLS_Props = {
|
|
3
3
|
node: FileNode;
|
|
4
4
|
/** Nesting depth, 0 at the root — drives the indent guide and aria-level. */
|
|
@@ -13,6 +13,8 @@ type __VLS_Props = {
|
|
|
13
13
|
/** Render a tri-state checkbox per row (multi-select mode). */
|
|
14
14
|
checkable: boolean;
|
|
15
15
|
checked: Set<string>;
|
|
16
|
+
/** Per-node state, derived once by the root for the whole tree. */
|
|
17
|
+
stats: Map<string, NodeStats>;
|
|
16
18
|
showSize: boolean;
|
|
17
19
|
showModified: boolean;
|
|
18
20
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
type __VLS_Props = {
|
|
2
2
|
fade?: boolean;
|
|
3
|
-
|
|
3
|
+
instant?: boolean;
|
|
4
4
|
contentClass?: string;
|
|
5
5
|
barInsetTop?: string;
|
|
6
6
|
};
|
|
@@ -14,7 +14,9 @@ declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {
|
|
|
14
14
|
scroll: (event: Event) => any;
|
|
15
15
|
}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
16
16
|
onScroll?: ((event: Event) => any) | undefined;
|
|
17
|
-
}>, {
|
|
17
|
+
}>, {
|
|
18
|
+
instant: boolean;
|
|
19
|
+
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
|
|
18
20
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
19
21
|
declare const _default: typeof __VLS_export;
|
|
20
22
|
export default _default;
|
|
@@ -59,5 +59,21 @@ export declare function checkedCount(node: FileNode, checked: ReadonlySet<string
|
|
|
59
59
|
checked: number;
|
|
60
60
|
total: number;
|
|
61
61
|
};
|
|
62
|
+
/** Everything a row needs to render, precomputed once per node. */
|
|
63
|
+
export interface NodeStats {
|
|
64
|
+
state: CheckState;
|
|
65
|
+
/** Ticked files in the subtree, and how many there are in total. */
|
|
66
|
+
checked: number;
|
|
67
|
+
total: number;
|
|
68
|
+
/** Rolled-up subtree size, or undefined when nothing carries one. */
|
|
69
|
+
size?: number;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Derive every row's state in ONE post-order pass over the tree, keyed by node
|
|
73
|
+
* id. Calling `checkStateOf`/`subtreeSize`/`checkedCount` per row instead makes
|
|
74
|
+
* each row walk its own subtree, which is quadratic on a deep tree and shows up
|
|
75
|
+
* as a stutter the moment a directory holds a few thousand files.
|
|
76
|
+
*/
|
|
77
|
+
export declare function computeStats(nodes: FileNode[], checked: ReadonlySet<string>): Map<string, NodeStats>;
|
|
62
78
|
/** Ids of every ancestor directory of `id`, so callers can reveal a deep node. */
|
|
63
79
|
export declare function ancestorIds(nodes: FileNode[], id: string): string[];
|
|
@@ -144,6 +144,54 @@ function checkedCount(node, checked) {
|
|
|
144
144
|
walk(node.children ?? []);
|
|
145
145
|
return { checked: count, total };
|
|
146
146
|
}
|
|
147
|
+
function computeStats(nodes, checked) {
|
|
148
|
+
const stats = /* @__PURE__ */ new Map();
|
|
149
|
+
function visit(node) {
|
|
150
|
+
if (node.kind === "file" || !node.children?.length) {
|
|
151
|
+
const isChecked = checked.has(node.id);
|
|
152
|
+
const self2 = {
|
|
153
|
+
state: isChecked ? "checked" : "unchecked",
|
|
154
|
+
checked: node.kind === "file" && isChecked ? 1 : 0,
|
|
155
|
+
total: node.kind === "file" ? 1 : 0,
|
|
156
|
+
size: node.size
|
|
157
|
+
};
|
|
158
|
+
stats.set(node.id, self2);
|
|
159
|
+
return self2;
|
|
160
|
+
}
|
|
161
|
+
let all = true;
|
|
162
|
+
let any = false;
|
|
163
|
+
let count = 0;
|
|
164
|
+
let total = 0;
|
|
165
|
+
let bytes = 0;
|
|
166
|
+
let sized = node.size !== void 0;
|
|
167
|
+
for (const child of node.children) {
|
|
168
|
+
const childStats = visit(child);
|
|
169
|
+
if (childStats.state === "checked") any = true;
|
|
170
|
+
else if (childStats.state === "indeterminate") {
|
|
171
|
+
any = true;
|
|
172
|
+
all = false;
|
|
173
|
+
} else all = false;
|
|
174
|
+
count += childStats.checked;
|
|
175
|
+
total += childStats.total;
|
|
176
|
+
if (childStats.size !== void 0) {
|
|
177
|
+
bytes += childStats.size;
|
|
178
|
+
sized = true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const self = {
|
|
182
|
+
state: all ? "checked" : any ? "indeterminate" : "unchecked",
|
|
183
|
+
checked: count,
|
|
184
|
+
total,
|
|
185
|
+
// An explicit size on the directory wins: a server-side listing usually
|
|
186
|
+
// knows the real figure without the children being loaded.
|
|
187
|
+
size: node.size ?? (sized ? bytes : void 0)
|
|
188
|
+
};
|
|
189
|
+
stats.set(node.id, self);
|
|
190
|
+
return self;
|
|
191
|
+
}
|
|
192
|
+
for (const node of nodes) visit(node);
|
|
193
|
+
return stats;
|
|
194
|
+
}
|
|
147
195
|
function ancestorIds(nodes, id) {
|
|
148
196
|
const path = [];
|
|
149
197
|
function walk(list, trail) {
|
|
@@ -163,6 +211,7 @@ export {
|
|
|
163
211
|
ancestorIds,
|
|
164
212
|
checkStateOf,
|
|
165
213
|
checkedCount,
|
|
214
|
+
computeStats,
|
|
166
215
|
fileIcon,
|
|
167
216
|
formatModified,
|
|
168
217
|
formatSize,
|
package/dist/index.d.ts
CHANGED
|
@@ -27,8 +27,8 @@ export type { MenuItem } from './components/LpDropdownMenu.vue';
|
|
|
27
27
|
export { default as LpEmptyState } from './components/LpEmptyState.vue';
|
|
28
28
|
export { default as LpFileTree } from './components/LpFileTree.vue';
|
|
29
29
|
export type { FileTreeSummary } from './components/LpFileTree.vue';
|
|
30
|
-
export type { CheckState, FileNode } from './components/fileTree';
|
|
31
|
-
export { ancestorIds, checkedCount, checkStateOf, fileIcon, formatModified, formatSize, sortNodes, subtreeIds, subtreeSize, } from './components/fileTree';
|
|
30
|
+
export type { CheckState, FileNode, NodeStats } from './components/fileTree';
|
|
31
|
+
export { ancestorIds, checkedCount, checkStateOf, computeStats, fileIcon, formatModified, formatSize, sortNodes, subtreeIds, subtreeSize, } from './components/fileTree';
|
|
32
32
|
export { default as LpFormField } from './components/LpFormField.vue';
|
|
33
33
|
export { default as LpIcon } from './components/LpIcon.vue';
|
|
34
34
|
export { default as LpInfraNode } from './components/LpInfraNode.vue';
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { _ } from "./chunks/LayoutCanvas-Dd20boUG.js";
|
|
2
2
|
import { _ as _2 } from "./chunks/LpAlert-BAYHDSS1.js";
|
|
3
|
-
import { _ as _3 } from "./chunks/LpAppShell-
|
|
4
|
-
import { _ as _4 } from "./chunks/LpAutocomplete-
|
|
3
|
+
import { _ as _3 } from "./chunks/LpAppShell-CV194os6.js";
|
|
4
|
+
import { _ as _4 } from "./chunks/LpAutocomplete-DIgXmtCA.js";
|
|
5
5
|
import { _ as _5 } from "./chunks/LpAvatar-B1Ez0ZAx.js";
|
|
6
6
|
import { _ as _6 } from "./chunks/LpBadge-CXzBPnwO.js";
|
|
7
7
|
import { _ as _7 } from "./chunks/LpBreadcrumbs-B7Aqatzw.js";
|
|
@@ -11,24 +11,24 @@ import { _ as _10 } from "./chunks/LpCard-BmrTvWOe.js";
|
|
|
11
11
|
import { _ as _11 } from "./chunks/LpCheckbox-BO0zuuPh.js";
|
|
12
12
|
import { _ as _12 } from "./chunks/LpCodeBlock-CSiOSPs8.js";
|
|
13
13
|
import { default as default2 } from "./components/LpCommandPalette.vue.js";
|
|
14
|
-
import { _ as _13 } from "./chunks/LpConfirmDialog-
|
|
14
|
+
import { _ as _13 } from "./chunks/LpConfirmDialog-WHkymac5.js";
|
|
15
15
|
import { _ as _14 } from "./chunks/LpContextMenu-D0mzXqA7.js";
|
|
16
16
|
import { _ as _15 } from "./chunks/LpDatePicker-dY2q4dr4.js";
|
|
17
17
|
import { _ as _16 } from "./chunks/LpDisclosure-DP6yfgfM.js";
|
|
18
18
|
import { _ as _17 } from "./chunks/LpDivider-CUX46KR0.js";
|
|
19
|
-
import { _ as _18 } from "./chunks/LpDrawer-
|
|
19
|
+
import { _ as _18 } from "./chunks/LpDrawer-BHzCy-Aa.js";
|
|
20
20
|
import { _ as _19 } from "./chunks/LpDropdownMenu-DJpoqRCb.js";
|
|
21
21
|
import { _ as _20 } from "./chunks/LpEmptyState-CrfyRJUG.js";
|
|
22
|
-
import { _ as _21 } from "./chunks/LpFileTree-
|
|
23
|
-
import { ancestorIds, checkStateOf, checkedCount, fileIcon, formatModified, formatSize, sortNodes, subtreeIds, subtreeSize } from "./components/fileTree.js";
|
|
22
|
+
import { _ as _21 } from "./chunks/LpFileTree-BQz8Rfgz.js";
|
|
23
|
+
import { ancestorIds, checkStateOf, checkedCount, computeStats, fileIcon, formatModified, formatSize, sortNodes, subtreeIds, subtreeSize } from "./components/fileTree.js";
|
|
24
24
|
import { _ as _22 } from "./chunks/LpFormField-Bn-ZwC6z.js";
|
|
25
25
|
import { _ as _23 } from "./chunks/LpIcon-CCnX5_2j.js";
|
|
26
26
|
import { _ as _24 } from "./chunks/LpInfraNode-C_PHcthC.js";
|
|
27
27
|
import { _ as _25 } from "./chunks/LpInput-BauZRyzm.js";
|
|
28
28
|
import { _ as _26 } from "./chunks/LpLink-DHwOEp4e.js";
|
|
29
|
-
import { _ as _27 } from "./chunks/LpLogViewer-
|
|
30
|
-
import { _ as _28 } from "./chunks/LpModal-
|
|
31
|
-
import { _ as _29 } from "./chunks/LpNotificationBell-
|
|
29
|
+
import { _ as _27 } from "./chunks/LpLogViewer-BvzRZPjs.js";
|
|
30
|
+
import { _ as _28 } from "./chunks/LpModal-DpBHQpbU.js";
|
|
31
|
+
import { _ as _29 } from "./chunks/LpNotificationBell-BtfRoL2i.js";
|
|
32
32
|
import { _ as _30 } from "./chunks/LpNumberField-C5ar-OwE.js";
|
|
33
33
|
import { _ as _31 } from "./chunks/LpOtpInput-DqaT0Z75.js";
|
|
34
34
|
import { _ as _32 } from "./chunks/LpPagination-CeGuNe_q.js";
|
|
@@ -39,17 +39,17 @@ import { _ as _35 } from "./chunks/LpPopover-8268UmlC.js";
|
|
|
39
39
|
import { _ as _36 } from "./chunks/LpProgress-BhI0IbjC.js";
|
|
40
40
|
import { _ as _37 } from "./chunks/LpRadio-CL-pI0_O.js";
|
|
41
41
|
import { _ as _38 } from "./chunks/LpRadioGroup-B4yDTmi6.js";
|
|
42
|
-
import { _ as _39 } from "./chunks/LpScrollArea-
|
|
42
|
+
import { _ as _39 } from "./chunks/LpScrollArea-De9DLpa1.js";
|
|
43
43
|
import { _ as _40 } from "./chunks/LpSegmented-BmpanyAo.js";
|
|
44
44
|
import { _ as _41 } from "./chunks/LpSelect-DznhPEWX.js";
|
|
45
45
|
import { _ as _42 } from "./chunks/LpServiceNode-Dc-Ut5RS.js";
|
|
46
|
-
import { _ as _43 } from "./chunks/LpSidebar-
|
|
46
|
+
import { _ as _43 } from "./chunks/LpSidebar-B11IDRQi.js";
|
|
47
47
|
import { _ as _44 } from "./chunks/LpSkeleton-eSFIo4kD.js";
|
|
48
48
|
import { _ as _45 } from "./chunks/LpSlider-B1SF3pRV.js";
|
|
49
49
|
import { _ as _46 } from "./chunks/LpStat-CdbEFOvn.js";
|
|
50
50
|
import { _ as _47 } from "./chunks/LpStepper-5dFTrW21.js";
|
|
51
51
|
import { _ as _48 } from "./chunks/LpSwitch-v6fnccSM.js";
|
|
52
|
-
import { _ as _49 } from "./chunks/LpTable-
|
|
52
|
+
import { _ as _49 } from "./chunks/LpTable-CKGaUbEM.js";
|
|
53
53
|
import { _ as _50 } from "./chunks/LpTableOfContents-CjvxPMpU.js";
|
|
54
54
|
import { _ as _51 } from "./chunks/LpTabs-D7aTVPOb.js";
|
|
55
55
|
import { _ as _52 } from "./chunks/LpTextarea-CRAkhemZ.js";
|
|
@@ -169,6 +169,7 @@ export {
|
|
|
169
169
|
e as bootstrapTheme,
|
|
170
170
|
checkStateOf,
|
|
171
171
|
checkedCount,
|
|
172
|
+
computeStats,
|
|
172
173
|
countBlocks,
|
|
173
174
|
countLeaves,
|
|
174
175
|
f as dark,
|
package/package.json
CHANGED
|
@@ -97,9 +97,16 @@ const items = computed<AutocompleteOption[]>(() =>
|
|
|
97
97
|
),
|
|
98
98
|
)
|
|
99
99
|
|
|
100
|
+
// The field's text IS the value here, so a chosen option would otherwise filter
|
|
101
|
+
// the list down to itself: reopening the field showed a single row, and the
|
|
102
|
+
// other options only came back after erasing what was already selected. The
|
|
103
|
+
// filter therefore applies to what the user TYPES, and stops applying as soon
|
|
104
|
+
// as a value is picked (or the field is reopened untouched).
|
|
105
|
+
const typing = ref(false)
|
|
106
|
+
|
|
100
107
|
const filtered = computed(() => {
|
|
101
108
|
const q = text.value.trim().toLowerCase()
|
|
102
|
-
if (!props.filter || !q) return items.value
|
|
109
|
+
if (!props.filter || !typing.value || !q) return items.value
|
|
103
110
|
return items.value.filter((o) => {
|
|
104
111
|
const hay = `${o.label ?? o.value} ${o.description ?? ""}`.toLowerCase()
|
|
105
112
|
return hay.includes(q)
|
|
@@ -136,18 +143,28 @@ const anchorSize = {
|
|
|
136
143
|
lg: "h-(--size-control-lg) text-sm",
|
|
137
144
|
}
|
|
138
145
|
|
|
146
|
+
function onInput(event: Event) {
|
|
147
|
+
typing.value = true
|
|
148
|
+
text.value = (event.target as HTMLInputElement).value
|
|
149
|
+
}
|
|
150
|
+
|
|
139
151
|
function onFocus() {
|
|
152
|
+
// Arriving at a field that already holds a value shows every option, not just
|
|
153
|
+
// the one it currently holds.
|
|
154
|
+
typing.value = false
|
|
140
155
|
focused.value = true
|
|
141
156
|
if (canOpen.value) open.value = true
|
|
142
157
|
}
|
|
143
158
|
|
|
144
159
|
function choose(opt: AutocompleteOption) {
|
|
160
|
+
typing.value = false
|
|
145
161
|
emit("update:modelValue", opt.value)
|
|
146
162
|
emit("select", opt.value)
|
|
147
163
|
open.value = false
|
|
148
164
|
}
|
|
149
165
|
|
|
150
166
|
function clear() {
|
|
167
|
+
typing.value = false
|
|
151
168
|
emit("update:modelValue", "")
|
|
152
169
|
open.value = false
|
|
153
170
|
}
|
|
@@ -188,7 +205,7 @@ function clear() {
|
|
|
188
205
|
class="min-w-0 flex-1 bg-transparent outline-none placeholder:text-muted"
|
|
189
206
|
@beforeinput="onBeforeInput"
|
|
190
207
|
@paste="onPaste"
|
|
191
|
-
@input="
|
|
208
|
+
@input="onInput"
|
|
192
209
|
@focus="onFocus"
|
|
193
210
|
@blur="focused = false"
|
|
194
211
|
/>
|
|
@@ -21,11 +21,10 @@
|
|
|
21
21
|
import { computed, ref, watch } from "vue"
|
|
22
22
|
import {
|
|
23
23
|
ancestorIds,
|
|
24
|
-
|
|
24
|
+
computeStats,
|
|
25
25
|
formatSize,
|
|
26
26
|
sortNodes,
|
|
27
27
|
subtreeIds,
|
|
28
|
-
subtreeSize,
|
|
29
28
|
type FileNode,
|
|
30
29
|
} from "./fileTree"
|
|
31
30
|
import LpEmptyState from "./LpEmptyState.vue"
|
|
@@ -135,19 +134,27 @@ watch(
|
|
|
135
134
|
},
|
|
136
135
|
)
|
|
137
136
|
|
|
138
|
-
/**
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
137
|
+
/**
|
|
138
|
+
* Index every node by id, plus each node's parent id, in one pass per data
|
|
139
|
+
* change. The parent map turns "reconcile my ancestors" into a walk up the
|
|
140
|
+
* chain instead of a search through the whole tree on every tick.
|
|
141
|
+
*/
|
|
142
|
+
const index = computed(() => {
|
|
143
|
+
const byId = new Map<string, FileNode>()
|
|
144
|
+
const parentOfId = new Map<string, string>()
|
|
145
|
+
function walk(list: FileNode[], parent?: string) {
|
|
142
146
|
for (const node of list) {
|
|
143
|
-
|
|
144
|
-
if (
|
|
147
|
+
byId.set(node.id, node)
|
|
148
|
+
if (parent !== undefined) parentOfId.set(node.id, parent)
|
|
149
|
+
if (node.children) walk(node.children, node.id)
|
|
145
150
|
}
|
|
146
151
|
}
|
|
147
152
|
walk(props.nodes)
|
|
148
|
-
return
|
|
153
|
+
return { byId, parentOfId }
|
|
149
154
|
})
|
|
150
155
|
|
|
156
|
+
const byId = computed(() => index.value.byId)
|
|
157
|
+
|
|
151
158
|
/**
|
|
152
159
|
* What the ticked set adds up to. Directories are counted but not sized — their
|
|
153
160
|
* bytes arrive through the files inside them, so summing both would double-count.
|
|
@@ -159,30 +166,47 @@ const summary = computed<FileTreeSummary>(() => {
|
|
|
159
166
|
let dirs = 0
|
|
160
167
|
let bytes = 0
|
|
161
168
|
let sized = false
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
if (size !== undefined) {
|
|
174
|
-
bytes += size
|
|
169
|
+
|
|
170
|
+
// Walk the tree, not the ticked set: a fully ticked directory can be counted
|
|
171
|
+
// whole and its subtree skipped, which keeps this linear in what's selected
|
|
172
|
+
// rather than in the size of the instance.
|
|
173
|
+
function walk(list: FileNode[]) {
|
|
174
|
+
for (const node of list) {
|
|
175
|
+
const stat = stats.value.get(node.id)
|
|
176
|
+
if (!stat || stat.state === "unchecked") continue
|
|
177
|
+
|
|
178
|
+
if (node.kind === "file") {
|
|
179
|
+
files++
|
|
180
|
+
if (node.size !== undefined) {
|
|
181
|
+
bytes += node.size
|
|
175
182
|
sized = true
|
|
176
183
|
}
|
|
184
|
+
continue
|
|
177
185
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (
|
|
181
|
-
|
|
186
|
+
|
|
187
|
+
dirs++
|
|
188
|
+
if (stat.state === "checked" && stat.size !== undefined) {
|
|
189
|
+
// Whole directory: take its rolled-up size and don't descend.
|
|
190
|
+
bytes += stat.size
|
|
182
191
|
sized = true
|
|
192
|
+
files += stat.total
|
|
193
|
+
countDirs(node.children ?? [])
|
|
194
|
+
continue
|
|
183
195
|
}
|
|
196
|
+
walk(node.children ?? [])
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// A fully ticked directory still contributes its nested folders to the count.
|
|
201
|
+
function countDirs(list: FileNode[]) {
|
|
202
|
+
for (const node of list) {
|
|
203
|
+
if (node.kind !== "dir") continue
|
|
204
|
+
dirs++
|
|
205
|
+
countDirs(node.children ?? [])
|
|
184
206
|
}
|
|
185
207
|
}
|
|
208
|
+
|
|
209
|
+
walk(props.nodes)
|
|
186
210
|
return {
|
|
187
211
|
files,
|
|
188
212
|
dirs,
|
|
@@ -212,13 +236,17 @@ function onCheck(node: FileNode, value: boolean) {
|
|
|
212
236
|
if (value) next.add(id)
|
|
213
237
|
else next.delete(id)
|
|
214
238
|
}
|
|
215
|
-
// Walk
|
|
216
|
-
|
|
239
|
+
// Walk straight up the parent chain, re-deriving each ancestor from its
|
|
240
|
+
// children — nearest first, so each one sees the level below already settled.
|
|
241
|
+
let ancestorId = index.value.parentOfId.get(node.id)
|
|
242
|
+
while (ancestorId !== undefined) {
|
|
217
243
|
const ancestor = byId.value.get(ancestorId)
|
|
218
|
-
if (
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
244
|
+
if (ancestor?.children?.length) {
|
|
245
|
+
const all = ancestor.children.every((c) => c.disabled || next.has(c.id))
|
|
246
|
+
if (all) next.add(ancestorId)
|
|
247
|
+
else next.delete(ancestorId)
|
|
248
|
+
}
|
|
249
|
+
ancestorId = index.value.parentOfId.get(ancestorId)
|
|
222
250
|
}
|
|
223
251
|
setChecked(next)
|
|
224
252
|
}
|
|
@@ -240,6 +268,10 @@ const requested = new Set<string>()
|
|
|
240
268
|
|
|
241
269
|
const roots = computed(() => (props.sort ? sortNodes(props.nodes) : props.nodes))
|
|
242
270
|
|
|
271
|
+
// One pass for the whole tree; every row reads its entry by id instead of
|
|
272
|
+
// walking its own subtree on each render.
|
|
273
|
+
const stats = computed(() => computeStats(props.nodes, checkedIds.value))
|
|
274
|
+
|
|
243
275
|
/** Rows currently visible, in render order — the axis the arrow keys walk. */
|
|
244
276
|
const visible = computed(() => {
|
|
245
277
|
const out: FileNode[] = []
|
|
@@ -334,8 +366,7 @@ function onSelect(node: FileNode) {
|
|
|
334
366
|
|
|
335
367
|
/** Nearest expanded ancestor of `id`, or undefined at the top level. */
|
|
336
368
|
function parentOf(id: string): FileNode | undefined {
|
|
337
|
-
const
|
|
338
|
-
const parentId = trail.at(-1)
|
|
369
|
+
const parentId = index.value.parentOfId.get(id)
|
|
339
370
|
return parentId ? visible.value.find((n) => n.id === parentId) : undefined
|
|
340
371
|
}
|
|
341
372
|
|
|
@@ -397,7 +428,7 @@ function onKeydown(e: KeyboardEvent) {
|
|
|
397
428
|
// In checkable mode Space is the tick — the standard tree behaviour for a
|
|
398
429
|
// multi-select tree — and Enter stays "open/activate".
|
|
399
430
|
if (props.checkable) {
|
|
400
|
-
onCheck(node,
|
|
431
|
+
onCheck(node, stats.value.get(node.id)?.state !== "checked")
|
|
401
432
|
break
|
|
402
433
|
}
|
|
403
434
|
onSelect(node)
|
|
@@ -487,6 +518,7 @@ defineExpose({ reveal, expandAll, collapseAll, checkAll, clearChecked, summary }
|
|
|
487
518
|
:icon-size="iconSize"
|
|
488
519
|
:checkable="checkable"
|
|
489
520
|
:checked="checkedIds"
|
|
521
|
+
:stats="stats"
|
|
490
522
|
:show-size="showSize"
|
|
491
523
|
:show-modified="showModified"
|
|
492
524
|
@select="onSelect"
|
|
@@ -11,14 +11,12 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { computed } from "vue"
|
|
13
13
|
import {
|
|
14
|
-
checkedCount,
|
|
15
|
-
checkStateOf,
|
|
16
14
|
fileIcon,
|
|
17
15
|
formatModified,
|
|
18
16
|
formatSize,
|
|
19
17
|
sortNodes,
|
|
20
|
-
subtreeSize,
|
|
21
18
|
type FileNode,
|
|
19
|
+
type NodeStats,
|
|
22
20
|
} from "./fileTree"
|
|
23
21
|
import LpCheckbox from "./LpCheckbox.vue"
|
|
24
22
|
import LpContextMenu from "./LpContextMenu.vue"
|
|
@@ -38,6 +36,8 @@ const props = defineProps<{
|
|
|
38
36
|
/** Render a tri-state checkbox per row (multi-select mode). */
|
|
39
37
|
checkable: boolean
|
|
40
38
|
checked: Set<string>
|
|
39
|
+
/** Per-node state, derived once by the root for the whole tree. */
|
|
40
|
+
stats: Map<string, NodeStats>
|
|
41
41
|
showSize: boolean
|
|
42
42
|
showModified: boolean
|
|
43
43
|
}>()
|
|
@@ -71,15 +71,17 @@ const children = computed(() => {
|
|
|
71
71
|
return props.sort ? sortNodes(list) : list
|
|
72
72
|
})
|
|
73
73
|
|
|
74
|
+
const stat = computed(() => props.stats.get(props.node.id))
|
|
75
|
+
|
|
74
76
|
const checkState = computed(() =>
|
|
75
|
-
props.checkable ?
|
|
77
|
+
props.checkable ? (stat.value?.state ?? "unchecked") : "unchecked",
|
|
76
78
|
)
|
|
77
79
|
|
|
78
80
|
// A directory shows the rolled-up size of what's under it, so a backup picker
|
|
79
81
|
// can answer "how much is this folder" without expanding it.
|
|
80
82
|
const sizeLabel = computed(() => {
|
|
81
83
|
if (!props.showSize) return ""
|
|
82
|
-
const bytes =
|
|
84
|
+
const bytes = stat.value?.size
|
|
83
85
|
return bytes === undefined ? "" : formatSize(bytes)
|
|
84
86
|
})
|
|
85
87
|
|
|
@@ -90,7 +92,7 @@ const sizeLabel = computed(() => {
|
|
|
90
92
|
*/
|
|
91
93
|
const countLabel = computed(() => {
|
|
92
94
|
if (!props.checkable || !isDir.value) return ""
|
|
93
|
-
const { checked, total } =
|
|
95
|
+
const { checked = 0, total = 0 } = stat.value ?? {}
|
|
94
96
|
return total && checked && checked < total ? `${checked} / ${total}` : ""
|
|
95
97
|
})
|
|
96
98
|
|
|
@@ -213,7 +215,7 @@ function onActivate() {
|
|
|
213
215
|
:class="node.disabled ? '' : isDir ? 'text-brand' : 'text-muted'"
|
|
214
216
|
/>
|
|
215
217
|
|
|
216
|
-
<span class="min-w-0 flex-1 truncate">
|
|
218
|
+
<span class="min-w-0 flex-1 shrink-[2] truncate">
|
|
217
219
|
<slot name="row" :node="node" :depth="depth" :expanded="isOpen" :selected="isSelected">
|
|
218
220
|
{{ node.name }}
|
|
219
221
|
</slot>
|
|
@@ -228,8 +230,12 @@ function onActivate() {
|
|
|
228
230
|
{{ countLabel }}
|
|
229
231
|
</span>
|
|
230
232
|
|
|
231
|
-
<!-- Metadata columns: tabular figures so sizes line up down the tree.
|
|
232
|
-
|
|
233
|
+
<!-- Metadata columns: tabular figures so sizes line up down the tree.
|
|
234
|
+
`meta` is free text, so it truncates rather than pushing the size
|
|
235
|
+
column off the row. -->
|
|
236
|
+
<span v-if="node.meta" class="min-w-0 shrink truncate text-xs text-muted">
|
|
237
|
+
{{ node.meta }}
|
|
238
|
+
</span>
|
|
233
239
|
<span
|
|
234
240
|
v-if="modifiedLabel"
|
|
235
241
|
class="shrink-0 text-xs tabular-nums text-muted/80"
|
|
@@ -268,6 +274,7 @@ function onActivate() {
|
|
|
268
274
|
:icon-size="iconSize"
|
|
269
275
|
:checkable="checkable"
|
|
270
276
|
:checked="checked"
|
|
277
|
+
:stats="stats"
|
|
271
278
|
:show-size="showSize"
|
|
272
279
|
:show-modified="showModified"
|
|
273
280
|
@select="emit('select', $event)"
|
|
@@ -551,9 +551,13 @@ const transitionProps = {
|
|
|
551
551
|
<div
|
|
552
552
|
class="relative overflow-hidden rounded-card border border-line bg-surface font-mono text-xs leading-relaxed"
|
|
553
553
|
>
|
|
554
|
+
<!-- `instant`: this viewer decides for itself when a jump animates (see
|
|
555
|
+
rideToBottom) and restores scrollTop directly after prepending older
|
|
556
|
+
lines. A CSS smooth scroll would override both. -->
|
|
554
557
|
<LpScrollArea
|
|
555
558
|
ref="scrollRef"
|
|
556
559
|
fade
|
|
560
|
+
instant
|
|
557
561
|
:content-class="wrap ? '' : 'min-w-max'"
|
|
558
562
|
:style="{ height }"
|
|
559
563
|
@scroll="onScroll"
|
|
@@ -20,16 +20,21 @@ import {
|
|
|
20
20
|
// component root, since items live inside reka's viewport.
|
|
21
21
|
// `barInsetTop` (any CSS length) pulls the vertical scrollbar down by that much,
|
|
22
22
|
// so it doesn't run under a sticky header pinned at the top of the viewport.
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
// Wheel and keyboard scrolling ease by default. `instant` opts back out, and
|
|
24
|
+
// any consumer that DRIVES the scroll itself needs it: CSS scroll-behaviour
|
|
25
|
+
// also governs programmatic scrolling, overriding `scrollTo({behavior:"auto"})`
|
|
26
|
+
// and animating even a plain `scrollTop = n`. A log tail pinning itself to the
|
|
27
|
+
// newest line, or restoring a position after prepending rows, has to land
|
|
28
|
+
// instantly — see LpLogViewer.
|
|
29
|
+
const props = withDefaults(
|
|
30
|
+
defineProps<{
|
|
31
|
+
fade?: boolean
|
|
32
|
+
instant?: boolean
|
|
33
|
+
contentClass?: string
|
|
34
|
+
barInsetTop?: string
|
|
35
|
+
}>(),
|
|
36
|
+
{ instant: false },
|
|
37
|
+
)
|
|
33
38
|
|
|
34
39
|
// Re-emit the native scroll event of the underlying viewport so consumers that
|
|
35
40
|
// drive scroll-following (e.g. log tails) can track position. The listener is
|
|
@@ -91,7 +96,7 @@ const barFade =
|
|
|
91
96
|
ref="viewportRef"
|
|
92
97
|
class="size-full min-w-0 [&>div]:!block [&>div]:!min-w-0"
|
|
93
98
|
:class="[
|
|
94
|
-
|
|
99
|
+
instant ? '' : 'scroll-smooth motion-reduce:scroll-auto',
|
|
95
100
|
fade ? '[mask-image:linear-gradient(to_bottom,transparent_0,black_14px,black_calc(100%-14px),transparent_100%)]' : '',
|
|
96
101
|
]"
|
|
97
102
|
@scroll.passive="$emit('scroll', $event)"
|
|
@@ -221,6 +221,81 @@ export function checkedCount(
|
|
|
221
221
|
return { checked: count, total }
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
/** Everything a row needs to render, precomputed once per node. */
|
|
225
|
+
export interface NodeStats {
|
|
226
|
+
state: CheckState
|
|
227
|
+
/** Ticked files in the subtree, and how many there are in total. */
|
|
228
|
+
checked: number
|
|
229
|
+
total: number
|
|
230
|
+
/** Rolled-up subtree size, or undefined when nothing carries one. */
|
|
231
|
+
size?: number
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Derive every row's state in ONE post-order pass over the tree, keyed by node
|
|
236
|
+
* id. Calling `checkStateOf`/`subtreeSize`/`checkedCount` per row instead makes
|
|
237
|
+
* each row walk its own subtree, which is quadratic on a deep tree and shows up
|
|
238
|
+
* as a stutter the moment a directory holds a few thousand files.
|
|
239
|
+
*/
|
|
240
|
+
export function computeStats(
|
|
241
|
+
nodes: FileNode[],
|
|
242
|
+
checked: ReadonlySet<string>,
|
|
243
|
+
): Map<string, NodeStats> {
|
|
244
|
+
const stats = new Map<string, NodeStats>()
|
|
245
|
+
|
|
246
|
+
function visit(node: FileNode): NodeStats {
|
|
247
|
+
if (node.kind === "file" || !node.children?.length) {
|
|
248
|
+
// A leaf, or a directory whose children aren't loaded: fall back to its
|
|
249
|
+
// own membership so an unexpanded folder can still be ticked whole.
|
|
250
|
+
const isChecked = checked.has(node.id)
|
|
251
|
+
const self: NodeStats = {
|
|
252
|
+
state: isChecked ? "checked" : "unchecked",
|
|
253
|
+
checked: node.kind === "file" && isChecked ? 1 : 0,
|
|
254
|
+
total: node.kind === "file" ? 1 : 0,
|
|
255
|
+
size: node.size,
|
|
256
|
+
}
|
|
257
|
+
stats.set(node.id, self)
|
|
258
|
+
return self
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let all = true
|
|
262
|
+
let any = false
|
|
263
|
+
let count = 0
|
|
264
|
+
let total = 0
|
|
265
|
+
let bytes = 0
|
|
266
|
+
let sized = node.size !== undefined
|
|
267
|
+
|
|
268
|
+
for (const child of node.children) {
|
|
269
|
+
const childStats = visit(child)
|
|
270
|
+
if (childStats.state === "checked") any = true
|
|
271
|
+
else if (childStats.state === "indeterminate") {
|
|
272
|
+
any = true
|
|
273
|
+
all = false
|
|
274
|
+
} else all = false
|
|
275
|
+
count += childStats.checked
|
|
276
|
+
total += childStats.total
|
|
277
|
+
if (childStats.size !== undefined) {
|
|
278
|
+
bytes += childStats.size
|
|
279
|
+
sized = true
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const self: NodeStats = {
|
|
284
|
+
state: all ? "checked" : any ? "indeterminate" : "unchecked",
|
|
285
|
+
checked: count,
|
|
286
|
+
total,
|
|
287
|
+
// An explicit size on the directory wins: a server-side listing usually
|
|
288
|
+
// knows the real figure without the children being loaded.
|
|
289
|
+
size: node.size ?? (sized ? bytes : undefined),
|
|
290
|
+
}
|
|
291
|
+
stats.set(node.id, self)
|
|
292
|
+
return self
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
for (const node of nodes) visit(node)
|
|
296
|
+
return stats
|
|
297
|
+
}
|
|
298
|
+
|
|
224
299
|
/** Ids of every ancestor directory of `id`, so callers can reveal a deep node. */
|
|
225
300
|
export function ancestorIds(nodes: FileNode[], id: string): string[] {
|
|
226
301
|
const path: string[] = []
|
package/src/index.ts
CHANGED
|
@@ -27,11 +27,12 @@ export type { MenuItem } from "./components/LpDropdownMenu.vue"
|
|
|
27
27
|
export { default as LpEmptyState } from "./components/LpEmptyState.vue"
|
|
28
28
|
export { default as LpFileTree } from "./components/LpFileTree.vue"
|
|
29
29
|
export type { FileTreeSummary } from "./components/LpFileTree.vue"
|
|
30
|
-
export type { CheckState, FileNode } from "./components/fileTree"
|
|
30
|
+
export type { CheckState, FileNode, NodeStats } from "./components/fileTree"
|
|
31
31
|
export {
|
|
32
32
|
ancestorIds,
|
|
33
33
|
checkedCount,
|
|
34
34
|
checkStateOf,
|
|
35
|
+
computeStats,
|
|
35
36
|
fileIcon,
|
|
36
37
|
formatModified,
|
|
37
38
|
formatSize,
|