@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.
Files changed (44) hide show
  1. package/dist/chunks/{LpAppShell-BLKZDLh8.js → LpAppShell-CV194os6.js} +2 -2
  2. package/dist/chunks/{LpAutocomplete-B9uoCs4t.js → LpAutocomplete-DIgXmtCA.js} +13 -5
  3. package/dist/chunks/{LpConfirmDialog-BA45MvRL.js → LpConfirmDialog-D6Rhge21.js} +1 -1
  4. package/dist/chunks/{LpDrawer-6n5Hd-iF.js → LpDrawer-BHzCy-Aa.js} +1 -1
  5. package/dist/chunks/{LpFileTree-D0DEpKof.js → LpFileTree-BQz8Rfgz.js} +49 -32
  6. package/dist/chunks/{LpFileTreeNode-3e71ywJV.js → LpFileTreeNode-DFg4PhOK.js} +10 -7
  7. package/dist/chunks/{LpLogViewer-BBgaY38i.js → LpLogViewer-BvzRZPjs.js} +2 -1
  8. package/dist/chunks/LpModal-d6SZpfBH.js +131 -0
  9. package/dist/chunks/{LpNotificationBell-B9VHks_D.js → LpNotificationBell-BtfRoL2i.js} +1 -1
  10. package/dist/chunks/{LpScrollArea-BtnZCCBT.js → LpScrollArea-De9DLpa1.js} +2 -2
  11. package/dist/chunks/{LpSidebar-tZ2z-PnM.js → LpSidebar-B11IDRQi.js} +2 -2
  12. package/dist/chunks/{LpSidebarNav-D-YvtwsH.js → LpSidebarNav-TIUNHZX-.js} +1 -1
  13. package/dist/chunks/{LpTable-CzHVcr3v.js → LpTable-CKGaUbEM.js} +1 -1
  14. package/dist/components/LpAppShell.vue.js +1 -1
  15. package/dist/components/LpAutocomplete.vue.js +1 -1
  16. package/dist/components/LpCommandPalette.vue.js +1 -1
  17. package/dist/components/LpConfirmDialog.vue.js +1 -1
  18. package/dist/components/LpDrawer.vue.js +1 -1
  19. package/dist/components/LpFileTree.vue.js +1 -1
  20. package/dist/components/LpFileTreeNode.vue.d.ts +3 -1
  21. package/dist/components/LpFileTreeNode.vue.js +1 -1
  22. package/dist/components/LpLogViewer.vue.js +1 -1
  23. package/dist/components/LpModal.vue.js +1 -1
  24. package/dist/components/LpNotificationBell.vue.js +1 -1
  25. package/dist/components/LpScrollArea.vue.d.ts +4 -2
  26. package/dist/components/LpScrollArea.vue.js +1 -1
  27. package/dist/components/LpSidebar.vue.js +1 -1
  28. package/dist/components/LpSidebarNav.vue.js +1 -1
  29. package/dist/components/LpTable.vue.js +1 -1
  30. package/dist/components/fileTree.d.ts +16 -0
  31. package/dist/components/fileTree.js +49 -0
  32. package/dist/index.d.ts +2 -2
  33. package/dist/index.js +13 -12
  34. package/package.json +1 -1
  35. package/src/components/LpAutocomplete.vue +19 -2
  36. package/src/components/LpFileTree.vue +67 -35
  37. package/src/components/LpFileTreeNode.vue +16 -9
  38. package/src/components/LpLogViewer.vue +4 -0
  39. package/src/components/LpModal.vue +11 -1
  40. package/src/components/LpScrollArea.vue +16 -11
  41. package/src/components/fileTree.ts +75 -0
  42. package/src/index.ts +2 -1
  43. package/src/tokens/tokens.css +16 -0
  44. 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,
@@ -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
- };