@leavepulse/ui 0.14.10 → 0.15.0
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/{LpFileTree-0Ko_uj8q.js → LpFileTree-D0DEpKof.js} +7 -3
- package/dist/chunks/{LpFileTreeNode-D3lNEP-1.js → LpFileTreeNode-3e71ywJV.js} +21 -10
- package/dist/components/LpFileTree.vue.js +1 -1
- package/dist/components/LpFileTreeNode.vue.js +1 -1
- package/dist/components/fileTree.d.ts +9 -0
- package/dist/components/fileTree.js +17 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -2
- package/package.json +3 -2
- package/src/components/LpFileTree.vue +6 -0
- package/src/components/LpFileTreeNode.vue +21 -0
- package/src/components/fileTree.ts +25 -0
- package/src/index.ts +1 -0
|
@@ -1,7 +1,7 @@
|
|
|
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
2
|
import { subtreeIds, subtreeSize, formatSize, sortNodes, ancestorIds, checkStateOf } from "../components/fileTree.js";
|
|
3
3
|
import { _ as _sfc_main$1 } from "./LpEmptyState-CrfyRJUG.js";
|
|
4
|
-
import { _ as _sfc_main$3 } from "./LpFileTreeNode-
|
|
4
|
+
import { _ as _sfc_main$3 } from "./LpFileTreeNode-3e71ywJV.js";
|
|
5
5
|
import { _ as _sfc_main$2 } from "./LpScrollArea-BtnZCCBT.js";
|
|
6
6
|
const _hoisted_1 = {
|
|
7
7
|
key: 0,
|
|
@@ -9,7 +9,7 @@ const _hoisted_1 = {
|
|
|
9
9
|
};
|
|
10
10
|
const _hoisted_2 = {
|
|
11
11
|
key: 2,
|
|
12
|
-
class: "flex min-h-0 flex-col"
|
|
12
|
+
class: "flex h-full min-h-0 flex-col"
|
|
13
13
|
};
|
|
14
14
|
const _hoisted_3 = ["aria-multiselectable"];
|
|
15
15
|
const _hoisted_4 = {
|
|
@@ -195,6 +195,10 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
195
195
|
}
|
|
196
196
|
function onSelect(node) {
|
|
197
197
|
if (node.disabled) return;
|
|
198
|
+
if (props.selected === node.id) {
|
|
199
|
+
emit("update:selected", "");
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
198
202
|
emit("update:selected", node.id);
|
|
199
203
|
emit("select", node);
|
|
200
204
|
if (node.kind === "file") emit("open", node);
|
|
@@ -318,7 +322,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
318
322
|
ref: root,
|
|
319
323
|
role: "tree",
|
|
320
324
|
"aria-multiselectable": __props.checkable,
|
|
321
|
-
class: "flex flex-col",
|
|
325
|
+
class: "flex min-w-0 flex-col",
|
|
322
326
|
onKeydown
|
|
323
327
|
}, [
|
|
324
328
|
(openBlock(true), createElementBlock(Fragment, null, renderList(roots.value, (node) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
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, checkStateOf, subtreeSize, formatSize, formatModified, fileIcon } from "../components/fileTree.js";
|
|
2
|
+
import { sortNodes, checkStateOf, subtreeSize, formatSize, checkedCount, 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";
|
|
@@ -9,20 +9,24 @@ const _hoisted_3 = { class: "flex size-4 shrink-0 items-center justify-center" }
|
|
|
9
9
|
const _hoisted_4 = { class: "min-w-0 flex-1 truncate" };
|
|
10
10
|
const _hoisted_5 = {
|
|
11
11
|
key: 1,
|
|
12
|
-
class: "shrink-0 text-
|
|
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: "shrink-0 text-xs
|
|
16
|
+
class: "shrink-0 text-xs text-muted"
|
|
17
17
|
};
|
|
18
18
|
const _hoisted_7 = {
|
|
19
19
|
key: 3,
|
|
20
|
-
class: "
|
|
20
|
+
class: "shrink-0 text-xs tabular-nums text-muted/80"
|
|
21
21
|
};
|
|
22
22
|
const _hoisted_8 = {
|
|
23
|
+
key: 4,
|
|
24
|
+
class: "w-16 shrink-0 text-right text-xs tabular-nums text-muted"
|
|
25
|
+
};
|
|
26
|
+
const _hoisted_9 = {
|
|
23
27
|
key: 0,
|
|
24
28
|
role: "group",
|
|
25
|
-
class: "flex flex-col overflow-hidden"
|
|
29
|
+
class: "flex min-w-0 flex-col overflow-hidden"
|
|
26
30
|
};
|
|
27
31
|
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
28
32
|
__name: "LpFileTreeNode",
|
|
@@ -63,6 +67,11 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
63
67
|
const bytes = subtreeSize(props.node);
|
|
64
68
|
return bytes === void 0 ? "" : formatSize(bytes);
|
|
65
69
|
});
|
|
70
|
+
const countLabel = computed(() => {
|
|
71
|
+
if (!props.checkable || !isDir.value) return "";
|
|
72
|
+
const { checked, total } = checkedCount(props.node, props.checked);
|
|
73
|
+
return total && checked && checked < total ? `${checked} / ${total}` : "";
|
|
74
|
+
});
|
|
66
75
|
const modifiedLabel = computed(
|
|
67
76
|
() => props.showModified && props.node.modified !== void 0 ? formatModified(props.node.modified) : ""
|
|
68
77
|
);
|
|
@@ -104,6 +113,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
104
113
|
const _component_LpFileTreeNode = resolveComponent("LpFileTreeNode", true);
|
|
105
114
|
return openBlock(), createElementBlock("li", {
|
|
106
115
|
role: "treeitem",
|
|
116
|
+
class: "min-w-0",
|
|
107
117
|
"aria-level": __props.depth + 1,
|
|
108
118
|
"aria-selected": isSelected.value,
|
|
109
119
|
"aria-expanded": canExpand.value ? isOpen.value : void 0,
|
|
@@ -116,7 +126,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
116
126
|
createElementVNode("div", {
|
|
117
127
|
"data-node-id": __props.node.id,
|
|
118
128
|
tabindex: isFocused.value ? 0 : -1,
|
|
119
|
-
class: normalizeClass(["group/row relative flex cursor-pointer select-none items-center gap-1.5 rounded-control py-1 pr-2 text-sm outline-none transition-colors duration-[var(--duration-fast)] focus-visible:ring-2 focus-visible:ring-ring", [
|
|
129
|
+
class: normalizeClass(["group/row relative flex min-w-0 cursor-pointer select-none items-center gap-1.5 rounded-control py-1 pr-2 text-sm outline-none transition-colors duration-[var(--duration-fast)] focus-visible:ring-2 focus-visible:ring-ring", [
|
|
120
130
|
__props.node.disabled ? "cursor-not-allowed text-muted/50" : isSelected.value ? "bg-brand-soft text-ink" : "text-muted hover:bg-surface-soft hover:text-ink"
|
|
121
131
|
]]),
|
|
122
132
|
style: normalizeStyle({ paddingLeft: `${__props.depth * 14 + 6}px` }),
|
|
@@ -161,9 +171,10 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
161
171
|
createTextVNode(toDisplayString(__props.node.name), 1)
|
|
162
172
|
])
|
|
163
173
|
]),
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
174
|
+
countLabel.value ? (openBlock(), createElementBlock("span", _hoisted_5, toDisplayString(countLabel.value), 1)) : createCommentVNode("", true),
|
|
175
|
+
__props.node.meta ? (openBlock(), createElementBlock("span", _hoisted_6, toDisplayString(__props.node.meta), 1)) : createCommentVNode("", true),
|
|
176
|
+
modifiedLabel.value ? (openBlock(), createElementBlock("span", _hoisted_7, toDisplayString(modifiedLabel.value), 1)) : createCommentVNode("", true),
|
|
177
|
+
sizeLabel.value ? (openBlock(), createElementBlock("span", _hoisted_8, toDisplayString(sizeLabel.value), 1)) : createCommentVNode("", true)
|
|
167
178
|
], 14, _hoisted_2)
|
|
168
179
|
]),
|
|
169
180
|
_: 3
|
|
@@ -174,7 +185,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
174
185
|
onLeave: onBranchLeave
|
|
175
186
|
}, {
|
|
176
187
|
default: withCtx(() => [
|
|
177
|
-
isOpen.value && hasChildren.value ? (openBlock(), createElementBlock("ul",
|
|
188
|
+
isOpen.value && hasChildren.value ? (openBlock(), createElementBlock("ul", _hoisted_9, [
|
|
178
189
|
(openBlock(true), createElementBlock(Fragment, null, renderList(children.value, (child, i) => {
|
|
179
190
|
return openBlock(), createBlock(_component_LpFileTreeNode, {
|
|
180
191
|
key: child.id,
|
|
@@ -50,5 +50,14 @@ export type CheckState = "checked" | "unchecked" | "indeterminate";
|
|
|
50
50
|
* ticked as a whole — which is exactly what a backup picker needs.
|
|
51
51
|
*/
|
|
52
52
|
export declare function checkStateOf(node: FileNode, checked: ReadonlySet<string>): CheckState;
|
|
53
|
+
/**
|
|
54
|
+
* How much of a directory's subtree is ticked, counting FILES only (folders are
|
|
55
|
+
* containers, not payload). Lets a row say "3 / 12" so a half-filled checkbox
|
|
56
|
+
* isn't the only clue that a folder is partially selected.
|
|
57
|
+
*/
|
|
58
|
+
export declare function checkedCount(node: FileNode, checked: ReadonlySet<string>): {
|
|
59
|
+
checked: number;
|
|
60
|
+
total: number;
|
|
61
|
+
};
|
|
53
62
|
/** Ids of every ancestor directory of `id`, so callers can reveal a deep node. */
|
|
54
63
|
export declare function ancestorIds(nodes: FileNode[], id: string): string[];
|
|
@@ -128,6 +128,22 @@ function checkStateOf(node, checked) {
|
|
|
128
128
|
if (all) return "checked";
|
|
129
129
|
return any ? "indeterminate" : "unchecked";
|
|
130
130
|
}
|
|
131
|
+
function checkedCount(node, checked) {
|
|
132
|
+
let count = 0;
|
|
133
|
+
let total = 0;
|
|
134
|
+
function walk(list) {
|
|
135
|
+
for (const child of list) {
|
|
136
|
+
if (child.kind === "file") {
|
|
137
|
+
total++;
|
|
138
|
+
if (checked.has(child.id)) count++;
|
|
139
|
+
} else if (child.children?.length) {
|
|
140
|
+
walk(child.children);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
walk(node.children ?? []);
|
|
145
|
+
return { checked: count, total };
|
|
146
|
+
}
|
|
131
147
|
function ancestorIds(nodes, id) {
|
|
132
148
|
const path = [];
|
|
133
149
|
function walk(list, trail) {
|
|
@@ -146,6 +162,7 @@ function ancestorIds(nodes, id) {
|
|
|
146
162
|
export {
|
|
147
163
|
ancestorIds,
|
|
148
164
|
checkStateOf,
|
|
165
|
+
checkedCount,
|
|
149
166
|
fileIcon,
|
|
150
167
|
formatModified,
|
|
151
168
|
formatSize,
|
package/dist/index.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ 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
30
|
export type { CheckState, FileNode } from './components/fileTree';
|
|
31
|
-
export { ancestorIds, checkStateOf, fileIcon, formatModified, formatSize, sortNodes, subtreeIds, subtreeSize, } from './components/fileTree';
|
|
31
|
+
export { ancestorIds, checkedCount, checkStateOf, 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
|
@@ -19,8 +19,8 @@ import { _ as _17 } from "./chunks/LpDivider-CUX46KR0.js";
|
|
|
19
19
|
import { _ as _18 } from "./chunks/LpDrawer-6n5Hd-iF.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, fileIcon, formatModified, formatSize, sortNodes, subtreeIds, subtreeSize } from "./components/fileTree.js";
|
|
22
|
+
import { _ as _21 } from "./chunks/LpFileTree-D0DEpKof.js";
|
|
23
|
+
import { ancestorIds, checkStateOf, checkedCount, 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";
|
|
@@ -168,6 +168,7 @@ export {
|
|
|
168
168
|
blockTitle,
|
|
169
169
|
e as bootstrapTheme,
|
|
170
170
|
checkStateOf,
|
|
171
|
+
checkedCount,
|
|
171
172
|
countBlocks,
|
|
172
173
|
countLeaves,
|
|
173
174
|
f as dark,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leavepulse/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "LeavePulse token-driven Vue component kit.",
|
|
6
6
|
"repository": {
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"./dist/*": "./dist/*"
|
|
32
32
|
},
|
|
33
33
|
"publishConfig": {
|
|
34
|
-
"registry": "https://registry.npmjs.org"
|
|
34
|
+
"registry": "https://registry.npmjs.org",
|
|
35
|
+
"access": "public"
|
|
35
36
|
},
|
|
36
37
|
"sideEffects": [
|
|
37
38
|
"**/*.css",
|
|
@@ -321,6 +321,12 @@ function onToggle(node: FileNode) {
|
|
|
321
321
|
|
|
322
322
|
function onSelect(node: FileNode) {
|
|
323
323
|
if (node.disabled) return
|
|
324
|
+
// Clicking the highlighted row again clears it — a selection you can't undo
|
|
325
|
+
// without picking something else is a trap.
|
|
326
|
+
if (props.selected === node.id) {
|
|
327
|
+
emit("update:selected", "")
|
|
328
|
+
return
|
|
329
|
+
}
|
|
324
330
|
emit("update:selected", node.id)
|
|
325
331
|
emit("select", node)
|
|
326
332
|
if (node.kind === "file") emit("open", node)
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { computed } from "vue"
|
|
13
13
|
import {
|
|
14
|
+
checkedCount,
|
|
14
15
|
checkStateOf,
|
|
15
16
|
fileIcon,
|
|
16
17
|
formatModified,
|
|
@@ -82,6 +83,17 @@ const sizeLabel = computed(() => {
|
|
|
82
83
|
return bytes === undefined ? "" : formatSize(bytes)
|
|
83
84
|
})
|
|
84
85
|
|
|
86
|
+
/**
|
|
87
|
+
* "3 / 12" on a directory whose subtree is partly ticked. Only shown while it
|
|
88
|
+
* actually differs from the whole — a fully ticked or untouched folder reads
|
|
89
|
+
* fine from the checkbox alone, and a count on every row would be noise.
|
|
90
|
+
*/
|
|
91
|
+
const countLabel = computed(() => {
|
|
92
|
+
if (!props.checkable || !isDir.value) return ""
|
|
93
|
+
const { checked, total } = checkedCount(props.node, props.checked)
|
|
94
|
+
return total && checked && checked < total ? `${checked} / ${total}` : ""
|
|
95
|
+
})
|
|
96
|
+
|
|
85
97
|
const modifiedLabel = computed(() =>
|
|
86
98
|
props.showModified && props.node.modified !== undefined
|
|
87
99
|
? formatModified(props.node.modified)
|
|
@@ -207,6 +219,15 @@ function onActivate() {
|
|
|
207
219
|
</slot>
|
|
208
220
|
</span>
|
|
209
221
|
|
|
222
|
+
<!-- Partial-selection counter: says outright how much of the folder is
|
|
223
|
+
ticked, which a half-state checkbox alone doesn't convey. -->
|
|
224
|
+
<span
|
|
225
|
+
v-if="countLabel"
|
|
226
|
+
class="shrink-0 rounded-pill bg-brand-soft px-1.5 py-0.5 text-[11px] font-medium tabular-nums text-brand"
|
|
227
|
+
>
|
|
228
|
+
{{ countLabel }}
|
|
229
|
+
</span>
|
|
230
|
+
|
|
210
231
|
<!-- Metadata columns: tabular figures so sizes line up down the tree. -->
|
|
211
232
|
<span v-if="node.meta" class="shrink-0 text-xs text-muted">{{ node.meta }}</span>
|
|
212
233
|
<span
|
|
@@ -196,6 +196,31 @@ export function checkStateOf(node: FileNode, checked: ReadonlySet<string>): Chec
|
|
|
196
196
|
return any ? "indeterminate" : "unchecked"
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
/**
|
|
200
|
+
* How much of a directory's subtree is ticked, counting FILES only (folders are
|
|
201
|
+
* containers, not payload). Lets a row say "3 / 12" so a half-filled checkbox
|
|
202
|
+
* isn't the only clue that a folder is partially selected.
|
|
203
|
+
*/
|
|
204
|
+
export function checkedCount(
|
|
205
|
+
node: FileNode,
|
|
206
|
+
checked: ReadonlySet<string>,
|
|
207
|
+
): { checked: number; total: number } {
|
|
208
|
+
let count = 0
|
|
209
|
+
let total = 0
|
|
210
|
+
function walk(list: FileNode[]) {
|
|
211
|
+
for (const child of list) {
|
|
212
|
+
if (child.kind === "file") {
|
|
213
|
+
total++
|
|
214
|
+
if (checked.has(child.id)) count++
|
|
215
|
+
} else if (child.children?.length) {
|
|
216
|
+
walk(child.children)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
walk(node.children ?? [])
|
|
221
|
+
return { checked: count, total }
|
|
222
|
+
}
|
|
223
|
+
|
|
199
224
|
/** Ids of every ancestor directory of `id`, so callers can reveal a deep node. */
|
|
200
225
|
export function ancestorIds(nodes: FileNode[], id: string): string[] {
|
|
201
226
|
const path: string[] = []
|