@leavepulse/ui 0.15.0 → 0.15.2
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-D6Rhge21.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-d6SZpfBH.js +131 -0
- 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/LpModal.vue +11 -1
- package/src/components/LpScrollArea.vue +16 -11
- package/src/components/fileTree.ts +75 -0
- package/src/index.ts +2 -1
- package/src/tokens/tokens.css +16 -0
- package/dist/chunks/LpModal-B9CVVC5U.js +0 -128
|
@@ -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,
|
package/src/tokens/tokens.css
CHANGED
|
@@ -147,6 +147,8 @@
|
|
|
147
147
|
* scaling it re-runs that layout every frame — the content visibly squashed and
|
|
148
148
|
* the animation stuttered. A short rise reads as arriving just as well.
|
|
149
149
|
*/
|
|
150
|
+
/* For panels that translate off their own centre (the command palette pins its
|
|
151
|
+
left edge with -translate-x-1/2). */
|
|
150
152
|
@keyframes pop-in {
|
|
151
153
|
from {
|
|
152
154
|
opacity: 0;
|
|
@@ -159,6 +161,20 @@
|
|
|
159
161
|
transform: translate(-50%, calc(-50% + 5px));
|
|
160
162
|
}
|
|
161
163
|
}
|
|
164
|
+
/* For panels a flex/grid parent already centres, so the animation only has to
|
|
165
|
+
supply the rise — no -50% to preserve. LpModal uses these. */
|
|
166
|
+
@keyframes rise-in {
|
|
167
|
+
from {
|
|
168
|
+
opacity: 0;
|
|
169
|
+
transform: translateY(10px);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
@keyframes rise-out {
|
|
173
|
+
to {
|
|
174
|
+
opacity: 0;
|
|
175
|
+
transform: translateY(5px);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
162
178
|
/*
|
|
163
179
|
* Popover enter/exit. The -in/-out pair drifts DOWN from the trigger (content
|
|
164
180
|
* placed below it). When the popper flips ABOVE the trigger (data-side=top),
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { defineComponent, computed, useSlots, openBlock, createBlock, unref, withCtx, createVNode, normalizeStyle, normalizeClass, createElementBlock, createElementVNode, renderSlot, createTextVNode, toDisplayString, createCommentVNode } from "vue";
|
|
2
|
-
import { DialogRoot, DialogPortal, DialogOverlay, DialogContent, DialogTitle, DialogDescription, DialogClose } from "reka-ui";
|
|
3
|
-
import { CLOSE_ICON } from "../components/dropdown.js";
|
|
4
|
-
import { _ as _sfc_main$1 } from "./LpIcon-CCnX5_2j.js";
|
|
5
|
-
import { _ as _sfc_main$2 } from "./LpScrollArea-BtnZCCBT.js";
|
|
6
|
-
const _hoisted_1 = {
|
|
7
|
-
key: 0,
|
|
8
|
-
class: "flex shrink-0 items-start justify-between gap-4 p-5 pb-3"
|
|
9
|
-
};
|
|
10
|
-
const _hoisted_2 = { class: "flex flex-col gap-1" };
|
|
11
|
-
const _hoisted_3 = {
|
|
12
|
-
key: 3,
|
|
13
|
-
class: "flex shrink-0 justify-end gap-2 p-5 pt-4"
|
|
14
|
-
};
|
|
15
|
-
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
16
|
-
__name: "LpModal",
|
|
17
|
-
props: {
|
|
18
|
-
open: { type: Boolean },
|
|
19
|
-
title: {},
|
|
20
|
-
description: {},
|
|
21
|
-
size: { default: "md" },
|
|
22
|
-
width: {},
|
|
23
|
-
fillBody: { type: Boolean }
|
|
24
|
-
},
|
|
25
|
-
emits: ["update:open"],
|
|
26
|
-
setup(__props) {
|
|
27
|
-
const props = __props;
|
|
28
|
-
const widthClass = computed(() => {
|
|
29
|
-
if (props.width) return "";
|
|
30
|
-
return {
|
|
31
|
-
sm: "w-[min(92vw,24rem)]",
|
|
32
|
-
md: "w-[min(92vw,28rem)]",
|
|
33
|
-
lg: "w-[min(92vw,34rem)]",
|
|
34
|
-
xl: "w-[min(92vw,42rem)]",
|
|
35
|
-
"2xl": "w-[min(94vw,56rem)]",
|
|
36
|
-
"3xl": "w-[min(95vw,72rem)]",
|
|
37
|
-
full: "w-[96vw]"
|
|
38
|
-
}[props.size];
|
|
39
|
-
});
|
|
40
|
-
const slots = useSlots();
|
|
41
|
-
const bodyPad = computed(
|
|
42
|
-
() => [
|
|
43
|
-
"px-5",
|
|
44
|
-
props.title || slots.title ? "" : "pt-5",
|
|
45
|
-
slots.footer ? "" : "pb-5"
|
|
46
|
-
].filter(Boolean).join(" ")
|
|
47
|
-
);
|
|
48
|
-
return (_ctx, _cache) => {
|
|
49
|
-
return openBlock(), createBlock(unref(DialogRoot), {
|
|
50
|
-
open: __props.open,
|
|
51
|
-
"onUpdate:open": _cache[0] || (_cache[0] = (v) => _ctx.$emit("update:open", v))
|
|
52
|
-
}, {
|
|
53
|
-
default: withCtx(() => [
|
|
54
|
-
createVNode(unref(DialogPortal), null, {
|
|
55
|
-
default: withCtx(() => [
|
|
56
|
-
createVNode(unref(DialogOverlay), { class: "fixed inset-0 z-(--z-overlay) bg-black/50 backdrop-blur-sm data-[state=open]:animate-[fade-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[fade-out_120ms_ease]" }),
|
|
57
|
-
createVNode(unref(DialogContent), {
|
|
58
|
-
class: normalizeClass(["fixed left-1/2 top-1/2 z-(--z-modal) flex max-h-[min(90vh,calc(100dvh-2rem))] -translate-x-1/2 -translate-y-1/2 flex-col rounded-card border border-line bg-surface-raised shadow-panel outline-none data-[state=open]:animate-[pop-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[pop-out_120ms_cubic-bezier(0.4,0,1,1)]", widthClass.value]),
|
|
59
|
-
style: normalizeStyle(__props.width ? { width: __props.width } : void 0)
|
|
60
|
-
}, {
|
|
61
|
-
default: withCtx(() => [
|
|
62
|
-
__props.title || _ctx.$slots.title ? (openBlock(), createElementBlock("header", _hoisted_1, [
|
|
63
|
-
createElementVNode("div", _hoisted_2, [
|
|
64
|
-
createVNode(unref(DialogTitle), { class: "text-base font-semibold text-ink" }, {
|
|
65
|
-
default: withCtx(() => [
|
|
66
|
-
renderSlot(_ctx.$slots, "title", {}, () => [
|
|
67
|
-
createTextVNode(toDisplayString(__props.title), 1)
|
|
68
|
-
])
|
|
69
|
-
]),
|
|
70
|
-
_: 3
|
|
71
|
-
}),
|
|
72
|
-
__props.description ? (openBlock(), createBlock(unref(DialogDescription), {
|
|
73
|
-
key: 0,
|
|
74
|
-
class: "text-sm text-muted"
|
|
75
|
-
}, {
|
|
76
|
-
default: withCtx(() => [
|
|
77
|
-
createTextVNode(toDisplayString(__props.description), 1)
|
|
78
|
-
]),
|
|
79
|
-
_: 1
|
|
80
|
-
})) : createCommentVNode("", true)
|
|
81
|
-
]),
|
|
82
|
-
createVNode(unref(DialogClose), {
|
|
83
|
-
class: "group flex shrink-0 items-center rounded-md p-1 text-muted outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink focus-visible:ring-2 focus-visible:ring-ring",
|
|
84
|
-
"aria-label": "Close"
|
|
85
|
-
}, {
|
|
86
|
-
default: withCtx(() => [
|
|
87
|
-
createVNode(_sfc_main$1, {
|
|
88
|
-
name: "lucide:x",
|
|
89
|
-
size: 18,
|
|
90
|
-
class: normalizeClass(unref(CLOSE_ICON))
|
|
91
|
-
}, null, 8, ["class"])
|
|
92
|
-
]),
|
|
93
|
-
_: 1
|
|
94
|
-
})
|
|
95
|
-
])) : createCommentVNode("", true),
|
|
96
|
-
__props.fillBody ? (openBlock(), createElementBlock("div", {
|
|
97
|
-
key: 1,
|
|
98
|
-
class: normalizeClass(["flex min-h-0 flex-1 flex-col overflow-hidden text-sm text-ink/90", bodyPad.value])
|
|
99
|
-
}, [
|
|
100
|
-
renderSlot(_ctx.$slots, "default")
|
|
101
|
-
], 2)) : (openBlock(), createBlock(_sfc_main$2, {
|
|
102
|
-
key: 2,
|
|
103
|
-
class: "min-h-0 flex-1 text-sm text-ink/90",
|
|
104
|
-
"content-class": bodyPad.value
|
|
105
|
-
}, {
|
|
106
|
-
default: withCtx(() => [
|
|
107
|
-
renderSlot(_ctx.$slots, "default")
|
|
108
|
-
]),
|
|
109
|
-
_: 3
|
|
110
|
-
}, 8, ["content-class"])),
|
|
111
|
-
_ctx.$slots.footer ? (openBlock(), createElementBlock("footer", _hoisted_3, [
|
|
112
|
-
renderSlot(_ctx.$slots, "footer")
|
|
113
|
-
])) : createCommentVNode("", true)
|
|
114
|
-
]),
|
|
115
|
-
_: 3
|
|
116
|
-
}, 8, ["class", "style"])
|
|
117
|
-
]),
|
|
118
|
-
_: 3
|
|
119
|
-
})
|
|
120
|
-
]),
|
|
121
|
-
_: 3
|
|
122
|
-
}, 8, ["open"]);
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
export {
|
|
127
|
-
_sfc_main as _
|
|
128
|
-
};
|