@leavepulse/ui 0.14.10 → 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.
Files changed (41) 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-WHkymac5.js} +1 -1
  4. package/dist/chunks/{LpDrawer-6n5Hd-iF.js → LpDrawer-BHzCy-Aa.js} +1 -1
  5. package/dist/chunks/{LpFileTree-0Ko_uj8q.js → LpFileTree-BQz8Rfgz.js} +55 -34
  6. package/dist/chunks/{LpFileTreeNode-D3lNEP-1.js → LpFileTreeNode-DFg4PhOK.js} +28 -14
  7. package/dist/chunks/{LpLogViewer-BBgaY38i.js → LpLogViewer-BvzRZPjs.js} +2 -1
  8. package/dist/chunks/{LpModal-B9CVVC5U.js → LpModal-DpBHQpbU.js} +1 -1
  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 +25 -0
  31. package/dist/components/fileTree.js +66 -0
  32. package/dist/index.d.ts +2 -2
  33. package/dist/index.js +14 -12
  34. package/package.json +3 -2
  35. package/src/components/LpAutocomplete.vue +19 -2
  36. package/src/components/LpFileTree.vue +73 -35
  37. package/src/components/LpFileTreeNode.vue +35 -7
  38. package/src/components/LpLogViewer.vue +4 -0
  39. package/src/components/LpScrollArea.vue +16 -11
  40. package/src/components/fileTree.ts +100 -0
  41. package/src/index.ts +3 -1
@@ -196,6 +196,106 @@ 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
+
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
+
199
299
  /** Ids of every ancestor directory of `id`, so callers can reveal a deep node. */
200
300
  export function ancestorIds(nodes: FileNode[], id: string): string[] {
201
301
  const path: string[] = []
package/src/index.ts CHANGED
@@ -27,10 +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
+ checkedCount,
33
34
  checkStateOf,
35
+ computeStats,
34
36
  fileIcon,
35
37
  formatModified,
36
38
  formatSize,