@flanksource/clicky-ui 0.2.16 → 0.2.17

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 (143) hide show
  1. package/dist/chat.cjs +16 -0
  2. package/dist/chat.cjs.map +1 -1
  3. package/dist/chat.js +17 -1
  4. package/dist/chat.js.map +1 -1
  5. package/dist/components/Combobox.cjs +19 -4
  6. package/dist/components/Combobox.cjs.map +1 -1
  7. package/dist/components/Combobox.d.ts.map +1 -1
  8. package/dist/components/Combobox.js +19 -4
  9. package/dist/components/Combobox.js.map +1 -1
  10. package/dist/components/FilterBar.cjs +40 -6
  11. package/dist/components/FilterBar.cjs.map +1 -1
  12. package/dist/components/FilterBar.d.ts +28 -0
  13. package/dist/components/FilterBar.d.ts.map +1 -1
  14. package/dist/components/FilterBar.js +40 -6
  15. package/dist/components/FilterBar.js.map +1 -1
  16. package/dist/components/JsonSchemaForm.cjs +26 -3
  17. package/dist/components/JsonSchemaForm.cjs.map +1 -1
  18. package/dist/components/JsonSchemaForm.d.ts.map +1 -1
  19. package/dist/components/JsonSchemaForm.js +26 -3
  20. package/dist/components/JsonSchemaForm.js.map +1 -1
  21. package/dist/components/json-schema-form-preferences.cjs +4 -0
  22. package/dist/components/json-schema-form-preferences.cjs.map +1 -1
  23. package/dist/components/json-schema-form-preferences.d.ts +2 -0
  24. package/dist/components/json-schema-form-preferences.d.ts.map +1 -1
  25. package/dist/components/json-schema-form-preferences.js +4 -0
  26. package/dist/components/json-schema-form-preferences.js.map +1 -1
  27. package/dist/components/json-schema-form-render.cjs +1 -1
  28. package/dist/components/json-schema-form-render.cjs.map +1 -1
  29. package/dist/components/json-schema-form-render.d.ts.map +1 -1
  30. package/dist/components/json-schema-form-render.js +2 -2
  31. package/dist/components/json-schema-form-render.js.map +1 -1
  32. package/dist/components/json-schema-form-types.d.ts +3 -2
  33. package/dist/components/json-schema-form-types.d.ts.map +1 -1
  34. package/dist/components/json-schema-form-utils.cjs +13 -0
  35. package/dist/components/json-schema-form-utils.cjs.map +1 -1
  36. package/dist/components/json-schema-form-utils.d.ts +2 -0
  37. package/dist/components/json-schema-form-utils.d.ts.map +1 -1
  38. package/dist/components/json-schema-form-utils.js +13 -0
  39. package/dist/components/json-schema-form-utils.js.map +1 -1
  40. package/dist/components/loading.cjs +19 -6
  41. package/dist/components/loading.cjs.map +1 -1
  42. package/dist/components/loading.d.ts.map +1 -1
  43. package/dist/components/loading.js +19 -6
  44. package/dist/components/loading.js.map +1 -1
  45. package/dist/data/Markdown.cjs +14 -40
  46. package/dist/data/Markdown.cjs.map +1 -1
  47. package/dist/data/Markdown.d.ts +3 -0
  48. package/dist/data/Markdown.d.ts.map +1 -1
  49. package/dist/data/Markdown.js +14 -40
  50. package/dist/data/Markdown.js.map +1 -1
  51. package/dist/data/TaskProgress.cjs +22 -0
  52. package/dist/data/TaskProgress.cjs.map +1 -1
  53. package/dist/data/TaskProgress.js +22 -0
  54. package/dist/data/TaskProgress.js.map +1 -1
  55. package/dist/data/TaskSnapshot.cjs.map +1 -1
  56. package/dist/data/TaskSnapshot.d.ts +5 -0
  57. package/dist/data/TaskSnapshot.d.ts.map +1 -1
  58. package/dist/data/TaskSnapshot.js.map +1 -1
  59. package/dist/data/Tree.cjs +32 -1
  60. package/dist/data/Tree.cjs.map +1 -1
  61. package/dist/data/Tree.d.ts +9 -1
  62. package/dist/data/Tree.d.ts.map +1 -1
  63. package/dist/data/Tree.js +32 -1
  64. package/dist/data/Tree.js.map +1 -1
  65. package/dist/data/chat/Attachment.cjs +79 -0
  66. package/dist/data/chat/Attachment.cjs.map +1 -0
  67. package/dist/data/chat/Attachment.d.ts +18 -0
  68. package/dist/data/chat/Attachment.d.ts.map +1 -0
  69. package/dist/data/chat/Attachment.js +79 -0
  70. package/dist/data/chat/Attachment.js.map +1 -0
  71. package/dist/data/chat/Chat.cjs +71 -14
  72. package/dist/data/chat/Chat.cjs.map +1 -1
  73. package/dist/data/chat/Chat.d.ts +30 -12
  74. package/dist/data/chat/Chat.d.ts.map +1 -1
  75. package/dist/data/chat/Chat.fixtures.d.ts +9 -0
  76. package/dist/data/chat/Chat.fixtures.d.ts.map +1 -1
  77. package/dist/data/chat/Chat.js +73 -16
  78. package/dist/data/chat/Chat.js.map +1 -1
  79. package/dist/data/chat/Conversation.cjs +32 -14
  80. package/dist/data/chat/Conversation.cjs.map +1 -1
  81. package/dist/data/chat/Conversation.d.ts +5 -3
  82. package/dist/data/chat/Conversation.d.ts.map +1 -1
  83. package/dist/data/chat/Conversation.js +34 -16
  84. package/dist/data/chat/Conversation.js.map +1 -1
  85. package/dist/data/chat/Message.cjs +73 -14
  86. package/dist/data/chat/Message.cjs.map +1 -1
  87. package/dist/data/chat/Message.d.ts +11 -4
  88. package/dist/data/chat/Message.d.ts.map +1 -1
  89. package/dist/data/chat/Message.js +75 -16
  90. package/dist/data/chat/Message.js.map +1 -1
  91. package/dist/data/chat/MessageActions.cjs +54 -0
  92. package/dist/data/chat/MessageActions.cjs.map +1 -0
  93. package/dist/data/chat/MessageActions.d.ts +11 -0
  94. package/dist/data/chat/MessageActions.d.ts.map +1 -0
  95. package/dist/data/chat/MessageActions.js +54 -0
  96. package/dist/data/chat/MessageActions.js.map +1 -0
  97. package/dist/data/chat/ModelSelector.cjs +43 -0
  98. package/dist/data/chat/ModelSelector.cjs.map +1 -0
  99. package/dist/data/chat/ModelSelector.d.ts +22 -0
  100. package/dist/data/chat/ModelSelector.d.ts.map +1 -0
  101. package/dist/data/chat/ModelSelector.js +43 -0
  102. package/dist/data/chat/ModelSelector.js.map +1 -0
  103. package/dist/data/chat/PromptInput.cjs +28 -15
  104. package/dist/data/chat/PromptInput.cjs.map +1 -1
  105. package/dist/data/chat/PromptInput.d.ts +14 -8
  106. package/dist/data/chat/PromptInput.d.ts.map +1 -1
  107. package/dist/data/chat/PromptInput.js +28 -15
  108. package/dist/data/chat/PromptInput.js.map +1 -1
  109. package/dist/data/chat/Reasoning.cjs +37 -0
  110. package/dist/data/chat/Reasoning.cjs.map +1 -0
  111. package/dist/data/chat/Reasoning.d.ts +11 -0
  112. package/dist/data/chat/Reasoning.d.ts.map +1 -0
  113. package/dist/data/chat/Reasoning.js +37 -0
  114. package/dist/data/chat/Reasoning.js.map +1 -0
  115. package/dist/data/chat/Suggestion.cjs +20 -0
  116. package/dist/data/chat/Suggestion.cjs.map +1 -0
  117. package/dist/data/chat/Suggestion.d.ts +10 -0
  118. package/dist/data/chat/Suggestion.d.ts.map +1 -0
  119. package/dist/data/chat/Suggestion.js +20 -0
  120. package/dist/data/chat/Suggestion.js.map +1 -0
  121. package/dist/data/chat/ToolCall.cjs +26 -3
  122. package/dist/data/chat/ToolCall.cjs.map +1 -1
  123. package/dist/data/chat/ToolCall.d.ts +5 -1
  124. package/dist/data/chat/ToolCall.d.ts.map +1 -1
  125. package/dist/data/chat/ToolCall.js +26 -3
  126. package/dist/data/chat/ToolCall.js.map +1 -1
  127. package/dist/data/chat/index.d.ts +7 -2
  128. package/dist/data/chat/index.d.ts.map +1 -1
  129. package/dist/data/chat/types.cjs +16 -0
  130. package/dist/data/chat/types.cjs.map +1 -1
  131. package/dist/data/chat/types.d.ts +29 -2
  132. package/dist/data/chat/types.d.ts.map +1 -1
  133. package/dist/data/chat/types.js +16 -0
  134. package/dist/data/chat/types.js.map +1 -1
  135. package/dist/data/version-info.cjs +3 -3
  136. package/dist/data/version-info.js +3 -3
  137. package/dist/hooks/use-task-run.cjs +33 -20
  138. package/dist/hooks/use-task-run.cjs.map +1 -1
  139. package/dist/hooks/use-task-run.d.ts.map +1 -1
  140. package/dist/hooks/use-task-run.js +33 -20
  141. package/dist/hooks/use-task-run.js.map +1 -1
  142. package/dist/styles.css +1 -1
  143. package/package.json +6 -1
@@ -92,6 +92,9 @@ function TaskRow({ task: t }) {
92
92
  const logs = t.logs ?? [];
93
93
  const hasLogs = logs.length > 0;
94
94
  const latestWarn = t.error ? void 0 : logs.filter((l) => l.level === "warn").at(-1);
95
+ const done = t.progress ?? 0;
96
+ const hasProgress = typeof t.maxValue === "number" && t.maxValue > 0 && done > 0;
97
+ const pct = hasProgress ? Math.round(done / t.maxValue * 100) : 0;
95
98
  return /* @__PURE__ */ jsxRuntime.jsxs(
96
99
  "div",
97
100
  {
@@ -112,6 +115,25 @@ function TaskRow({ task: t }) {
112
115
  ] }),
113
116
  t.duration && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: t.duration })
114
117
  ] }),
118
+ (t.description || hasProgress) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-0.5 flex items-center gap-2", children: [
119
+ t.description && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 truncate text-xs text-muted-foreground", children: t.description }),
120
+ hasProgress && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "shrink-0 text-xs tabular-nums text-muted-foreground", children: [
121
+ done,
122
+ "/",
123
+ t.maxValue,
124
+ " · ",
125
+ pct,
126
+ "%"
127
+ ] })
128
+ ] }),
129
+ hasProgress && t.status === "running" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-1", children: /* @__PURE__ */ jsxRuntime.jsx(
130
+ ProgressBar.ProgressBar,
131
+ {
132
+ segments: [{ count: done, color: "bg-blue-500", label: "done" }],
133
+ total: t.maxValue,
134
+ height: "h-1"
135
+ }
136
+ ) }),
115
137
  expanded && hasLogs && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-1 ml-1 max-h-48 space-y-0.5 overflow-y-auto border-l-2 pl-2", children: logs.map((l, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: utils.cn("text-xs", taskStatus.logLevelColor(l.level)), children: [
116
138
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mr-1 font-mono text-muted-foreground", children: l.level.substring(0, 3) }),
117
139
  l.message
@@ -1 +1 @@
1
- {"version":3,"file":"TaskProgress.cjs","sources":["../../src/data/TaskProgress.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { ProgressBar } from \"./ProgressBar\";\nimport type { LogEntry, TaskSnapshot } from \"./TaskSnapshot\";\nimport {\n bucketTasks,\n logLevelColor,\n taskSegments,\n taskStatusBg,\n taskStatusColor,\n taskStatusIcon,\n} from \"./task-status\";\n\n// TaskProgress renders one or more clicky task runs (groups) and their child\n// tasks: a segmented progress bar per group plus collapsible per-task rows with\n// status icon, duration, error, and expandable logs. Presentational only —\n// callers feed it snapshots from useTaskRun (SSE) or any other source.\n\n// Iconify web component, loaded by the host app (same as clicky's task UI). The\n// intrinsic element keeps TaskProgress free of clicky-ui's icon registry.\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace JSX {\n interface IntrinsicElements {\n \"iconify-icon\": { icon: string; class?: string };\n }\n }\n}\n\nconst MAX_COMPLETED = 5;\nconst MAX_PENDING = 3;\n\nexport interface TaskProgressProps {\n snapshots: TaskSnapshot[];\n title?: string;\n /** Tighter spacing for embedding in a panel. */\n compact?: boolean;\n className?: string;\n}\n\nexport function TaskProgress({ snapshots, title, compact, className }: TaskProgressProps) {\n const groups = snapshots.filter((s) => s.type === \"group\");\n const tasks = snapshots.filter((s) => s.type === \"task\");\n\n if (groups.length === 0) {\n return (\n <div className={cn(\"text-sm text-muted-foreground\", className)}>\n {title ? `${title}: ` : \"\"}No tasks yet.\n </div>\n );\n }\n\n return (\n <div className={cn(compact ? \"space-y-2\" : \"space-y-3\", className)}>\n {title && <h2 className=\"text-sm font-semibold\">{title}</h2>}\n {groups.map((g) => (\n <TaskGroupCard\n key={g.groupId || g.id}\n group={g}\n tasks={tasks.filter((t) => t.groupId === g.groupId || t.group === g.id)}\n compact={compact}\n />\n ))}\n </div>\n );\n}\n\nfunction isFailedOrWarn(t: TaskSnapshot): boolean {\n return t.status === \"failed\" || t.status === \"FAIL\" || t.status === \"ERR\" || t.status === \"warning\";\n}\n\nfunction TaskGroupCard({\n group: g,\n tasks,\n compact,\n}: {\n group: TaskSnapshot;\n tasks: TaskSnapshot[];\n compact: boolean | undefined;\n}) {\n const [showAll, setShowAll] = useState(false);\n const total = g.total ?? tasks.length;\n const counts = bucketTasks(tasks);\n const done = counts.ok + counts.warn + counts.fail;\n const progress = total > 0 ? `${done}/${total}` : \"\";\n\n const running = tasks.filter((t) => t.status === \"running\");\n const pending = tasks.filter((t) => t.status === \"pending\");\n const completed = tasks.filter((t) => t.status !== \"running\" && t.status !== \"pending\");\n const alwaysShow = completed.filter(isFailedOrWarn);\n const collapsible = completed.filter((t) => !isFailedOrWarn(t));\n\n const hiddenCompleted = collapsible.length - MAX_COMPLETED;\n const collapseCompleted = !showAll && hiddenCompleted > 0;\n const visibleSuccess = collapseCompleted ? collapsible.slice(-MAX_COMPLETED) : collapsible;\n\n const hiddenPending = pending.length - MAX_PENDING;\n const collapsePending = !showAll && hiddenPending > 0;\n const visiblePending = collapsePending ? pending.slice(0, MAX_PENDING) : pending;\n\n const isTerminal = g.status !== \"running\" && g.status !== \"pending\";\n\n return (\n <div className={cn(\"rounded-lg border bg-card\", compact ? \"p-3\" : \"p-4\")}>\n <div className=\"mb-3 flex items-center justify-between\">\n <h3 className=\"flex items-center gap-1.5 text-sm font-semibold\">\n <iconify-icon icon={taskStatusIcon(g.status)} class={taskStatusColor(g.status)} />\n <span>{g.name}</span>\n {progress && <span className=\"text-xs text-muted-foreground\">{progress}</span>}\n {g.kind && (\n <span className=\"rounded-full bg-muted px-1.5 py-0 text-[10px] text-muted-foreground\">\n {g.kind}\n </span>\n )}\n </h3>\n {isTerminal && (\n <span\n className={cn(\n \"rounded-full px-2 py-0.5 text-xs\",\n taskStatusColor(g.status),\n taskStatusBg(g.status),\n )}\n >\n {g.status}\n </span>\n )}\n </div>\n\n {total > 0 && (\n <div className=\"mb-3\">\n <ProgressBar segments={taskSegments(counts)} total={total} height=\"h-1.5\" />\n </div>\n )}\n\n {hiddenCompleted > 0 || hiddenPending > 0 ? (\n <button\n type=\"button\"\n className=\"border-b py-1.5 text-xs text-muted-foreground hover:text-foreground\"\n onClick={() => setShowAll((v) => !v)}\n >\n {showAll\n ? \"▲ collapse\"\n : `... ${(collapseCompleted ? hiddenCompleted : 0) + (collapsePending ? hiddenPending : 0)} more tasks`}\n </button>\n ) : null}\n\n {[...visibleSuccess, ...alwaysShow, ...running, ...visiblePending].map((t) => (\n <TaskRow key={t.id} task={t} />\n ))}\n </div>\n );\n}\n\nfunction TaskRow({ task: t }: { task: TaskSnapshot }) {\n const [expanded, setExpanded] = useState(false);\n const logs: LogEntry[] = t.logs ?? [];\n const hasLogs = logs.length > 0;\n // Promote the latest warning message inline so a `warning` row shows its\n // reason without expanding. Suppressed when an error is already shown.\n const latestWarn = t.error ? undefined : logs.filter((l) => l.level === \"warn\").at(-1);\n\n return (\n <div\n className={cn(\n \"flex items-start gap-3 border-b py-2 last:border-0\",\n hasLogs && \"-mx-1 cursor-pointer rounded px-1 hover:bg-muted/50\",\n )}\n onClick={hasLogs ? () => setExpanded((v) => !v) : undefined}\n >\n <iconify-icon icon={taskStatusIcon(t.status)} class={cn(taskStatusColor(t.status), \"mt-0.5 text-lg\")} />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"flex min-w-0 items-center gap-1.5 text-sm font-medium\">\n <span className=\"truncate\">{t.name}</span>\n {t.error && <span className=\"truncate text-xs font-normal text-red-500\">{t.error}</span>}\n {latestWarn && (\n <span className={cn(\"truncate text-xs font-normal\", logLevelColor(\"warn\"))}>\n {latestWarn.message}\n </span>\n )}\n {hasLogs && (\n <span className=\"inline-flex shrink-0 items-center rounded-full bg-muted px-1.5 text-[10px] font-medium text-muted-foreground\">\n {logs.length}\n </span>\n )}\n </span>\n {t.duration && <span className=\"shrink-0 text-xs text-muted-foreground\">{t.duration}</span>}\n </div>\n {expanded && hasLogs && (\n <div className=\"mt-1 ml-1 max-h-48 space-y-0.5 overflow-y-auto border-l-2 pl-2\">\n {logs.map((l, i) => (\n <div key={i} className={cn(\"text-xs\", logLevelColor(l.level))}>\n <span className=\"mr-1 font-mono text-muted-foreground\">{l.level.substring(0, 3)}</span>\n {l.message}\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"names":["cn","jsxs","jsx","useState","bucketTasks","taskStatusIcon","taskStatusColor","taskStatusBg","ProgressBar","taskSegments","logLevelColor"],"mappings":"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"}
1
+ {"version":3,"file":"TaskProgress.cjs","sources":["../../src/data/TaskProgress.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { ProgressBar } from \"./ProgressBar\";\nimport type { LogEntry, TaskSnapshot } from \"./TaskSnapshot\";\nimport {\n bucketTasks,\n logLevelColor,\n taskSegments,\n taskStatusBg,\n taskStatusColor,\n taskStatusIcon,\n} from \"./task-status\";\n\n// TaskProgress renders one or more clicky task runs (groups) and their child\n// tasks: a segmented progress bar per group plus collapsible per-task rows with\n// status icon, duration, error, and expandable logs. Presentational only —\n// callers feed it snapshots from useTaskRun (SSE) or any other source.\n\n// Iconify web component, loaded by the host app (same as clicky's task UI). The\n// intrinsic element keeps TaskProgress free of clicky-ui's icon registry.\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace JSX {\n interface IntrinsicElements {\n \"iconify-icon\": { icon: string; class?: string };\n }\n }\n}\n\nconst MAX_COMPLETED = 5;\nconst MAX_PENDING = 3;\n\nexport interface TaskProgressProps {\n snapshots: TaskSnapshot[];\n title?: string;\n /** Tighter spacing for embedding in a panel. */\n compact?: boolean;\n className?: string;\n}\n\nexport function TaskProgress({ snapshots, title, compact, className }: TaskProgressProps) {\n const groups = snapshots.filter((s) => s.type === \"group\");\n const tasks = snapshots.filter((s) => s.type === \"task\");\n\n if (groups.length === 0) {\n return (\n <div className={cn(\"text-sm text-muted-foreground\", className)}>\n {title ? `${title}: ` : \"\"}No tasks yet.\n </div>\n );\n }\n\n return (\n <div className={cn(compact ? \"space-y-2\" : \"space-y-3\", className)}>\n {title && <h2 className=\"text-sm font-semibold\">{title}</h2>}\n {groups.map((g) => (\n <TaskGroupCard\n key={g.groupId || g.id}\n group={g}\n tasks={tasks.filter((t) => t.groupId === g.groupId || t.group === g.id)}\n compact={compact}\n />\n ))}\n </div>\n );\n}\n\nfunction isFailedOrWarn(t: TaskSnapshot): boolean {\n return t.status === \"failed\" || t.status === \"FAIL\" || t.status === \"ERR\" || t.status === \"warning\";\n}\n\nfunction TaskGroupCard({\n group: g,\n tasks,\n compact,\n}: {\n group: TaskSnapshot;\n tasks: TaskSnapshot[];\n compact: boolean | undefined;\n}) {\n const [showAll, setShowAll] = useState(false);\n const total = g.total ?? tasks.length;\n const counts = bucketTasks(tasks);\n const done = counts.ok + counts.warn + counts.fail;\n const progress = total > 0 ? `${done}/${total}` : \"\";\n\n const running = tasks.filter((t) => t.status === \"running\");\n const pending = tasks.filter((t) => t.status === \"pending\");\n const completed = tasks.filter((t) => t.status !== \"running\" && t.status !== \"pending\");\n const alwaysShow = completed.filter(isFailedOrWarn);\n const collapsible = completed.filter((t) => !isFailedOrWarn(t));\n\n const hiddenCompleted = collapsible.length - MAX_COMPLETED;\n const collapseCompleted = !showAll && hiddenCompleted > 0;\n const visibleSuccess = collapseCompleted ? collapsible.slice(-MAX_COMPLETED) : collapsible;\n\n const hiddenPending = pending.length - MAX_PENDING;\n const collapsePending = !showAll && hiddenPending > 0;\n const visiblePending = collapsePending ? pending.slice(0, MAX_PENDING) : pending;\n\n const isTerminal = g.status !== \"running\" && g.status !== \"pending\";\n\n return (\n <div className={cn(\"rounded-lg border bg-card\", compact ? \"p-3\" : \"p-4\")}>\n <div className=\"mb-3 flex items-center justify-between\">\n <h3 className=\"flex items-center gap-1.5 text-sm font-semibold\">\n <iconify-icon icon={taskStatusIcon(g.status)} class={taskStatusColor(g.status)} />\n <span>{g.name}</span>\n {progress && <span className=\"text-xs text-muted-foreground\">{progress}</span>}\n {g.kind && (\n <span className=\"rounded-full bg-muted px-1.5 py-0 text-[10px] text-muted-foreground\">\n {g.kind}\n </span>\n )}\n </h3>\n {isTerminal && (\n <span\n className={cn(\n \"rounded-full px-2 py-0.5 text-xs\",\n taskStatusColor(g.status),\n taskStatusBg(g.status),\n )}\n >\n {g.status}\n </span>\n )}\n </div>\n\n {total > 0 && (\n <div className=\"mb-3\">\n <ProgressBar segments={taskSegments(counts)} total={total} height=\"h-1.5\" />\n </div>\n )}\n\n {hiddenCompleted > 0 || hiddenPending > 0 ? (\n <button\n type=\"button\"\n className=\"border-b py-1.5 text-xs text-muted-foreground hover:text-foreground\"\n onClick={() => setShowAll((v) => !v)}\n >\n {showAll\n ? \"▲ collapse\"\n : `... ${(collapseCompleted ? hiddenCompleted : 0) + (collapsePending ? hiddenPending : 0)} more tasks`}\n </button>\n ) : null}\n\n {[...visibleSuccess, ...alwaysShow, ...running, ...visiblePending].map((t) => (\n <TaskRow key={t.id} task={t} />\n ))}\n </div>\n );\n}\n\nfunction TaskRow({ task: t }: { task: TaskSnapshot }) {\n const [expanded, setExpanded] = useState(false);\n const logs: LogEntry[] = t.logs ?? [];\n const hasLogs = logs.length > 0;\n // Promote the latest warning message inline so a `warning` row shows its\n // reason without expanding. Suppressed when an error is already shown.\n const latestWarn = t.error ? undefined : logs.filter((l) => l.level === \"warn\").at(-1);\n // Bounded per-task progress (e.g. records emitted in a phase): render an x/y\n // count, a percentage, and a thin bar while the task runs.\n const done = t.progress ?? 0;\n // Only surface bounded progress once work has actually started — an untouched\n // task should not render a \"0/100 · 0%\" placeholder.\n const hasProgress = typeof t.maxValue === \"number\" && t.maxValue > 0 && done > 0;\n const pct = hasProgress ? Math.round((done / (t.maxValue as number)) * 100) : 0;\n\n return (\n <div\n className={cn(\n \"flex items-start gap-3 border-b py-2 last:border-0\",\n hasLogs && \"-mx-1 cursor-pointer rounded px-1 hover:bg-muted/50\",\n )}\n onClick={hasLogs ? () => setExpanded((v) => !v) : undefined}\n >\n <iconify-icon icon={taskStatusIcon(t.status)} class={cn(taskStatusColor(t.status), \"mt-0.5 text-lg\")} />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"flex min-w-0 items-center gap-1.5 text-sm font-medium\">\n <span className=\"truncate\">{t.name}</span>\n {t.error && <span className=\"truncate text-xs font-normal text-red-500\">{t.error}</span>}\n {latestWarn && (\n <span className={cn(\"truncate text-xs font-normal\", logLevelColor(\"warn\"))}>\n {latestWarn.message}\n </span>\n )}\n {hasLogs && (\n <span className=\"inline-flex shrink-0 items-center rounded-full bg-muted px-1.5 text-[10px] font-medium text-muted-foreground\">\n {logs.length}\n </span>\n )}\n </span>\n {t.duration && <span className=\"shrink-0 text-xs text-muted-foreground\">{t.duration}</span>}\n </div>\n {(t.description || hasProgress) && (\n <div className=\"mt-0.5 flex items-center gap-2\">\n {t.description && (\n <span className=\"min-w-0 flex-1 truncate text-xs text-muted-foreground\">{t.description}</span>\n )}\n {hasProgress && (\n <span className=\"shrink-0 text-xs tabular-nums text-muted-foreground\">\n {done}/{t.maxValue} · {pct}%\n </span>\n )}\n </div>\n )}\n {hasProgress && t.status === \"running\" && (\n <div className=\"mt-1\">\n <ProgressBar\n segments={[{ count: done, color: \"bg-blue-500\", label: \"done\" }]}\n total={t.maxValue as number}\n height=\"h-1\"\n />\n </div>\n )}\n {expanded && hasLogs && (\n <div className=\"mt-1 ml-1 max-h-48 space-y-0.5 overflow-y-auto border-l-2 pl-2\">\n {logs.map((l, i) => (\n <div key={i} className={cn(\"text-xs\", logLevelColor(l.level))}>\n <span className=\"mr-1 font-mono text-muted-foreground\">{l.level.substring(0, 3)}</span>\n {l.message}\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"names":["cn","jsxs","jsx","useState","bucketTasks","taskStatusIcon","taskStatusColor","taskStatusBg","ProgressBar","taskSegments","logLevelColor"],"mappings":"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"}
@@ -90,6 +90,9 @@ function TaskRow({ task: t }) {
90
90
  const logs = t.logs ?? [];
91
91
  const hasLogs = logs.length > 0;
92
92
  const latestWarn = t.error ? void 0 : logs.filter((l) => l.level === "warn").at(-1);
93
+ const done = t.progress ?? 0;
94
+ const hasProgress = typeof t.maxValue === "number" && t.maxValue > 0 && done > 0;
95
+ const pct = hasProgress ? Math.round(done / t.maxValue * 100) : 0;
93
96
  return /* @__PURE__ */ jsxs(
94
97
  "div",
95
98
  {
@@ -110,6 +113,25 @@ function TaskRow({ task: t }) {
110
113
  ] }),
111
114
  t.duration && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: t.duration })
112
115
  ] }),
116
+ (t.description || hasProgress) && /* @__PURE__ */ jsxs("div", { className: "mt-0.5 flex items-center gap-2", children: [
117
+ t.description && /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-xs text-muted-foreground", children: t.description }),
118
+ hasProgress && /* @__PURE__ */ jsxs("span", { className: "shrink-0 text-xs tabular-nums text-muted-foreground", children: [
119
+ done,
120
+ "/",
121
+ t.maxValue,
122
+ " · ",
123
+ pct,
124
+ "%"
125
+ ] })
126
+ ] }),
127
+ hasProgress && t.status === "running" && /* @__PURE__ */ jsx("div", { className: "mt-1", children: /* @__PURE__ */ jsx(
128
+ ProgressBar,
129
+ {
130
+ segments: [{ count: done, color: "bg-blue-500", label: "done" }],
131
+ total: t.maxValue,
132
+ height: "h-1"
133
+ }
134
+ ) }),
113
135
  expanded && hasLogs && /* @__PURE__ */ jsx("div", { className: "mt-1 ml-1 max-h-48 space-y-0.5 overflow-y-auto border-l-2 pl-2", children: logs.map((l, i) => /* @__PURE__ */ jsxs("div", { className: cn("text-xs", logLevelColor(l.level)), children: [
114
136
  /* @__PURE__ */ jsx("span", { className: "mr-1 font-mono text-muted-foreground", children: l.level.substring(0, 3) }),
115
137
  l.message
@@ -1 +1 @@
1
- {"version":3,"file":"TaskProgress.js","sources":["../../src/data/TaskProgress.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { ProgressBar } from \"./ProgressBar\";\nimport type { LogEntry, TaskSnapshot } from \"./TaskSnapshot\";\nimport {\n bucketTasks,\n logLevelColor,\n taskSegments,\n taskStatusBg,\n taskStatusColor,\n taskStatusIcon,\n} from \"./task-status\";\n\n// TaskProgress renders one or more clicky task runs (groups) and their child\n// tasks: a segmented progress bar per group plus collapsible per-task rows with\n// status icon, duration, error, and expandable logs. Presentational only —\n// callers feed it snapshots from useTaskRun (SSE) or any other source.\n\n// Iconify web component, loaded by the host app (same as clicky's task UI). The\n// intrinsic element keeps TaskProgress free of clicky-ui's icon registry.\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace JSX {\n interface IntrinsicElements {\n \"iconify-icon\": { icon: string; class?: string };\n }\n }\n}\n\nconst MAX_COMPLETED = 5;\nconst MAX_PENDING = 3;\n\nexport interface TaskProgressProps {\n snapshots: TaskSnapshot[];\n title?: string;\n /** Tighter spacing for embedding in a panel. */\n compact?: boolean;\n className?: string;\n}\n\nexport function TaskProgress({ snapshots, title, compact, className }: TaskProgressProps) {\n const groups = snapshots.filter((s) => s.type === \"group\");\n const tasks = snapshots.filter((s) => s.type === \"task\");\n\n if (groups.length === 0) {\n return (\n <div className={cn(\"text-sm text-muted-foreground\", className)}>\n {title ? `${title}: ` : \"\"}No tasks yet.\n </div>\n );\n }\n\n return (\n <div className={cn(compact ? \"space-y-2\" : \"space-y-3\", className)}>\n {title && <h2 className=\"text-sm font-semibold\">{title}</h2>}\n {groups.map((g) => (\n <TaskGroupCard\n key={g.groupId || g.id}\n group={g}\n tasks={tasks.filter((t) => t.groupId === g.groupId || t.group === g.id)}\n compact={compact}\n />\n ))}\n </div>\n );\n}\n\nfunction isFailedOrWarn(t: TaskSnapshot): boolean {\n return t.status === \"failed\" || t.status === \"FAIL\" || t.status === \"ERR\" || t.status === \"warning\";\n}\n\nfunction TaskGroupCard({\n group: g,\n tasks,\n compact,\n}: {\n group: TaskSnapshot;\n tasks: TaskSnapshot[];\n compact: boolean | undefined;\n}) {\n const [showAll, setShowAll] = useState(false);\n const total = g.total ?? tasks.length;\n const counts = bucketTasks(tasks);\n const done = counts.ok + counts.warn + counts.fail;\n const progress = total > 0 ? `${done}/${total}` : \"\";\n\n const running = tasks.filter((t) => t.status === \"running\");\n const pending = tasks.filter((t) => t.status === \"pending\");\n const completed = tasks.filter((t) => t.status !== \"running\" && t.status !== \"pending\");\n const alwaysShow = completed.filter(isFailedOrWarn);\n const collapsible = completed.filter((t) => !isFailedOrWarn(t));\n\n const hiddenCompleted = collapsible.length - MAX_COMPLETED;\n const collapseCompleted = !showAll && hiddenCompleted > 0;\n const visibleSuccess = collapseCompleted ? collapsible.slice(-MAX_COMPLETED) : collapsible;\n\n const hiddenPending = pending.length - MAX_PENDING;\n const collapsePending = !showAll && hiddenPending > 0;\n const visiblePending = collapsePending ? pending.slice(0, MAX_PENDING) : pending;\n\n const isTerminal = g.status !== \"running\" && g.status !== \"pending\";\n\n return (\n <div className={cn(\"rounded-lg border bg-card\", compact ? \"p-3\" : \"p-4\")}>\n <div className=\"mb-3 flex items-center justify-between\">\n <h3 className=\"flex items-center gap-1.5 text-sm font-semibold\">\n <iconify-icon icon={taskStatusIcon(g.status)} class={taskStatusColor(g.status)} />\n <span>{g.name}</span>\n {progress && <span className=\"text-xs text-muted-foreground\">{progress}</span>}\n {g.kind && (\n <span className=\"rounded-full bg-muted px-1.5 py-0 text-[10px] text-muted-foreground\">\n {g.kind}\n </span>\n )}\n </h3>\n {isTerminal && (\n <span\n className={cn(\n \"rounded-full px-2 py-0.5 text-xs\",\n taskStatusColor(g.status),\n taskStatusBg(g.status),\n )}\n >\n {g.status}\n </span>\n )}\n </div>\n\n {total > 0 && (\n <div className=\"mb-3\">\n <ProgressBar segments={taskSegments(counts)} total={total} height=\"h-1.5\" />\n </div>\n )}\n\n {hiddenCompleted > 0 || hiddenPending > 0 ? (\n <button\n type=\"button\"\n className=\"border-b py-1.5 text-xs text-muted-foreground hover:text-foreground\"\n onClick={() => setShowAll((v) => !v)}\n >\n {showAll\n ? \"▲ collapse\"\n : `... ${(collapseCompleted ? hiddenCompleted : 0) + (collapsePending ? hiddenPending : 0)} more tasks`}\n </button>\n ) : null}\n\n {[...visibleSuccess, ...alwaysShow, ...running, ...visiblePending].map((t) => (\n <TaskRow key={t.id} task={t} />\n ))}\n </div>\n );\n}\n\nfunction TaskRow({ task: t }: { task: TaskSnapshot }) {\n const [expanded, setExpanded] = useState(false);\n const logs: LogEntry[] = t.logs ?? [];\n const hasLogs = logs.length > 0;\n // Promote the latest warning message inline so a `warning` row shows its\n // reason without expanding. Suppressed when an error is already shown.\n const latestWarn = t.error ? undefined : logs.filter((l) => l.level === \"warn\").at(-1);\n\n return (\n <div\n className={cn(\n \"flex items-start gap-3 border-b py-2 last:border-0\",\n hasLogs && \"-mx-1 cursor-pointer rounded px-1 hover:bg-muted/50\",\n )}\n onClick={hasLogs ? () => setExpanded((v) => !v) : undefined}\n >\n <iconify-icon icon={taskStatusIcon(t.status)} class={cn(taskStatusColor(t.status), \"mt-0.5 text-lg\")} />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"flex min-w-0 items-center gap-1.5 text-sm font-medium\">\n <span className=\"truncate\">{t.name}</span>\n {t.error && <span className=\"truncate text-xs font-normal text-red-500\">{t.error}</span>}\n {latestWarn && (\n <span className={cn(\"truncate text-xs font-normal\", logLevelColor(\"warn\"))}>\n {latestWarn.message}\n </span>\n )}\n {hasLogs && (\n <span className=\"inline-flex shrink-0 items-center rounded-full bg-muted px-1.5 text-[10px] font-medium text-muted-foreground\">\n {logs.length}\n </span>\n )}\n </span>\n {t.duration && <span className=\"shrink-0 text-xs text-muted-foreground\">{t.duration}</span>}\n </div>\n {expanded && hasLogs && (\n <div className=\"mt-1 ml-1 max-h-48 space-y-0.5 overflow-y-auto border-l-2 pl-2\">\n {logs.map((l, i) => (\n <div key={i} className={cn(\"text-xs\", logLevelColor(l.level))}>\n <span className=\"mr-1 font-mono text-muted-foreground\">{l.level.substring(0, 3)}</span>\n {l.message}\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"TaskProgress.js","sources":["../../src/data/TaskProgress.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { ProgressBar } from \"./ProgressBar\";\nimport type { LogEntry, TaskSnapshot } from \"./TaskSnapshot\";\nimport {\n bucketTasks,\n logLevelColor,\n taskSegments,\n taskStatusBg,\n taskStatusColor,\n taskStatusIcon,\n} from \"./task-status\";\n\n// TaskProgress renders one or more clicky task runs (groups) and their child\n// tasks: a segmented progress bar per group plus collapsible per-task rows with\n// status icon, duration, error, and expandable logs. Presentational only —\n// callers feed it snapshots from useTaskRun (SSE) or any other source.\n\n// Iconify web component, loaded by the host app (same as clicky's task UI). The\n// intrinsic element keeps TaskProgress free of clicky-ui's icon registry.\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace JSX {\n interface IntrinsicElements {\n \"iconify-icon\": { icon: string; class?: string };\n }\n }\n}\n\nconst MAX_COMPLETED = 5;\nconst MAX_PENDING = 3;\n\nexport interface TaskProgressProps {\n snapshots: TaskSnapshot[];\n title?: string;\n /** Tighter spacing for embedding in a panel. */\n compact?: boolean;\n className?: string;\n}\n\nexport function TaskProgress({ snapshots, title, compact, className }: TaskProgressProps) {\n const groups = snapshots.filter((s) => s.type === \"group\");\n const tasks = snapshots.filter((s) => s.type === \"task\");\n\n if (groups.length === 0) {\n return (\n <div className={cn(\"text-sm text-muted-foreground\", className)}>\n {title ? `${title}: ` : \"\"}No tasks yet.\n </div>\n );\n }\n\n return (\n <div className={cn(compact ? \"space-y-2\" : \"space-y-3\", className)}>\n {title && <h2 className=\"text-sm font-semibold\">{title}</h2>}\n {groups.map((g) => (\n <TaskGroupCard\n key={g.groupId || g.id}\n group={g}\n tasks={tasks.filter((t) => t.groupId === g.groupId || t.group === g.id)}\n compact={compact}\n />\n ))}\n </div>\n );\n}\n\nfunction isFailedOrWarn(t: TaskSnapshot): boolean {\n return t.status === \"failed\" || t.status === \"FAIL\" || t.status === \"ERR\" || t.status === \"warning\";\n}\n\nfunction TaskGroupCard({\n group: g,\n tasks,\n compact,\n}: {\n group: TaskSnapshot;\n tasks: TaskSnapshot[];\n compact: boolean | undefined;\n}) {\n const [showAll, setShowAll] = useState(false);\n const total = g.total ?? tasks.length;\n const counts = bucketTasks(tasks);\n const done = counts.ok + counts.warn + counts.fail;\n const progress = total > 0 ? `${done}/${total}` : \"\";\n\n const running = tasks.filter((t) => t.status === \"running\");\n const pending = tasks.filter((t) => t.status === \"pending\");\n const completed = tasks.filter((t) => t.status !== \"running\" && t.status !== \"pending\");\n const alwaysShow = completed.filter(isFailedOrWarn);\n const collapsible = completed.filter((t) => !isFailedOrWarn(t));\n\n const hiddenCompleted = collapsible.length - MAX_COMPLETED;\n const collapseCompleted = !showAll && hiddenCompleted > 0;\n const visibleSuccess = collapseCompleted ? collapsible.slice(-MAX_COMPLETED) : collapsible;\n\n const hiddenPending = pending.length - MAX_PENDING;\n const collapsePending = !showAll && hiddenPending > 0;\n const visiblePending = collapsePending ? pending.slice(0, MAX_PENDING) : pending;\n\n const isTerminal = g.status !== \"running\" && g.status !== \"pending\";\n\n return (\n <div className={cn(\"rounded-lg border bg-card\", compact ? \"p-3\" : \"p-4\")}>\n <div className=\"mb-3 flex items-center justify-between\">\n <h3 className=\"flex items-center gap-1.5 text-sm font-semibold\">\n <iconify-icon icon={taskStatusIcon(g.status)} class={taskStatusColor(g.status)} />\n <span>{g.name}</span>\n {progress && <span className=\"text-xs text-muted-foreground\">{progress}</span>}\n {g.kind && (\n <span className=\"rounded-full bg-muted px-1.5 py-0 text-[10px] text-muted-foreground\">\n {g.kind}\n </span>\n )}\n </h3>\n {isTerminal && (\n <span\n className={cn(\n \"rounded-full px-2 py-0.5 text-xs\",\n taskStatusColor(g.status),\n taskStatusBg(g.status),\n )}\n >\n {g.status}\n </span>\n )}\n </div>\n\n {total > 0 && (\n <div className=\"mb-3\">\n <ProgressBar segments={taskSegments(counts)} total={total} height=\"h-1.5\" />\n </div>\n )}\n\n {hiddenCompleted > 0 || hiddenPending > 0 ? (\n <button\n type=\"button\"\n className=\"border-b py-1.5 text-xs text-muted-foreground hover:text-foreground\"\n onClick={() => setShowAll((v) => !v)}\n >\n {showAll\n ? \"▲ collapse\"\n : `... ${(collapseCompleted ? hiddenCompleted : 0) + (collapsePending ? hiddenPending : 0)} more tasks`}\n </button>\n ) : null}\n\n {[...visibleSuccess, ...alwaysShow, ...running, ...visiblePending].map((t) => (\n <TaskRow key={t.id} task={t} />\n ))}\n </div>\n );\n}\n\nfunction TaskRow({ task: t }: { task: TaskSnapshot }) {\n const [expanded, setExpanded] = useState(false);\n const logs: LogEntry[] = t.logs ?? [];\n const hasLogs = logs.length > 0;\n // Promote the latest warning message inline so a `warning` row shows its\n // reason without expanding. Suppressed when an error is already shown.\n const latestWarn = t.error ? undefined : logs.filter((l) => l.level === \"warn\").at(-1);\n // Bounded per-task progress (e.g. records emitted in a phase): render an x/y\n // count, a percentage, and a thin bar while the task runs.\n const done = t.progress ?? 0;\n // Only surface bounded progress once work has actually started — an untouched\n // task should not render a \"0/100 · 0%\" placeholder.\n const hasProgress = typeof t.maxValue === \"number\" && t.maxValue > 0 && done > 0;\n const pct = hasProgress ? Math.round((done / (t.maxValue as number)) * 100) : 0;\n\n return (\n <div\n className={cn(\n \"flex items-start gap-3 border-b py-2 last:border-0\",\n hasLogs && \"-mx-1 cursor-pointer rounded px-1 hover:bg-muted/50\",\n )}\n onClick={hasLogs ? () => setExpanded((v) => !v) : undefined}\n >\n <iconify-icon icon={taskStatusIcon(t.status)} class={cn(taskStatusColor(t.status), \"mt-0.5 text-lg\")} />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"flex min-w-0 items-center gap-1.5 text-sm font-medium\">\n <span className=\"truncate\">{t.name}</span>\n {t.error && <span className=\"truncate text-xs font-normal text-red-500\">{t.error}</span>}\n {latestWarn && (\n <span className={cn(\"truncate text-xs font-normal\", logLevelColor(\"warn\"))}>\n {latestWarn.message}\n </span>\n )}\n {hasLogs && (\n <span className=\"inline-flex shrink-0 items-center rounded-full bg-muted px-1.5 text-[10px] font-medium text-muted-foreground\">\n {logs.length}\n </span>\n )}\n </span>\n {t.duration && <span className=\"shrink-0 text-xs text-muted-foreground\">{t.duration}</span>}\n </div>\n {(t.description || hasProgress) && (\n <div className=\"mt-0.5 flex items-center gap-2\">\n {t.description && (\n <span className=\"min-w-0 flex-1 truncate text-xs text-muted-foreground\">{t.description}</span>\n )}\n {hasProgress && (\n <span className=\"shrink-0 text-xs tabular-nums text-muted-foreground\">\n {done}/{t.maxValue} · {pct}%\n </span>\n )}\n </div>\n )}\n {hasProgress && t.status === \"running\" && (\n <div className=\"mt-1\">\n <ProgressBar\n segments={[{ count: done, color: \"bg-blue-500\", label: \"done\" }]}\n total={t.maxValue as number}\n height=\"h-1\"\n />\n </div>\n )}\n {expanded && hasLogs && (\n <div className=\"mt-1 ml-1 max-h-48 space-y-0.5 overflow-y-auto border-l-2 pl-2\">\n {logs.map((l, i) => (\n <div key={i} className={cn(\"text-xs\", logLevelColor(l.level))}>\n <span className=\"mr-1 font-mono text-muted-foreground\">{l.level.substring(0, 3)}</span>\n {l.message}\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"TaskSnapshot.cjs","sources":["../../src/data/TaskSnapshot.ts"],"sourcesContent":["// TaskSnapshot mirrors the Go `task.TaskSnapshot` wire format emitted by clicky's\n// JSONHandler / SSEHandler (github.com/flanksource/clicky/task/snapshot.go). Field\n// names match the JSON tags exactly. This module is generic — it carries no\n// application-specific concepts.\n\nexport interface LogEntry {\n level: string;\n message: string;\n}\n\nexport interface TaskSnapshot {\n id: string;\n name: string;\n /** \"task\" or \"group\". */\n type: \"task\" | \"group\";\n /** Parent group NAME (groups: empty). */\n group?: string;\n status: string;\n duration?: string;\n error?: string;\n /** Latest log line. */\n message?: string;\n logs?: LogEntry[];\n /** Group aggregates. */\n total?: number;\n completed?: number;\n failed?: number;\n running?: number;\n /** Stable parent-run id (tasks) or the run's own id (groups). */\n groupId?: string;\n kind?: string;\n labels?: Record<string, string>;\n owner?: string;\n /** RFC3339. */\n startedAt?: string;\n /** RFC3339. */\n finishedAt?: string;\n}\n\n// TaskRunMeta mirrors the Go `task.RunMeta` listing summary returned by\n// GET {prefix}/tasks. Named TaskRunMeta to avoid colliding with the unrelated\n// diagnostics `RunMeta` already exported by this package.\nexport interface TaskRunMeta {\n id: string;\n name: string;\n kind?: string;\n labels?: Record<string, string>;\n owner?: string;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n total: number;\n completed: number;\n failed: number;\n running: number;\n}\n\n// Terminal run/group statuses (not running/pending). Used to decide when an SSE\n// stream is done or when polling should stop.\nconst TERMINAL_STATUSES = new Set([\n \"success\",\n \"failed\",\n \"warning\",\n \"canceled\",\n \"PASS\",\n \"FAIL\",\n \"ERR\",\n \"SKIP\",\n]);\n\nexport function isTerminalStatus(status: string | undefined): boolean {\n return !!status && TERMINAL_STATUSES.has(status);\n}\n\n// allGroupsTerminal reports whether every group snapshot has reached a terminal\n// status — i.e. the run(s) are complete and an SSE stream / poll can stop.\nexport function allGroupsTerminal(snapshots: TaskSnapshot[]): boolean {\n const groups = snapshots.filter((s) => s.type === \"group\");\n if (groups.length === 0) return false;\n return groups.every((g) => isTerminalStatus(g.status));\n}\n"],"names":[],"mappings":";;AA2DA,MAAM,wCAAwB,IAAI;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAEM,SAAS,iBAAiB,QAAqC;AACpE,SAAO,CAAC,CAAC,UAAU,kBAAkB,IAAI,MAAM;AACjD;AAIO,SAAS,kBAAkB,WAAoC;AACpE,QAAM,SAAS,UAAU,OAAO,CAAC,MAAM,EAAE,SAAS,OAAO;AACzD,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,SAAO,OAAO,MAAM,CAAC,MAAM,iBAAiB,EAAE,MAAM,CAAC;AACvD;;;"}
1
+ {"version":3,"file":"TaskSnapshot.cjs","sources":["../../src/data/TaskSnapshot.ts"],"sourcesContent":["// TaskSnapshot mirrors the Go `task.TaskSnapshot` wire format emitted by clicky's\n// JSONHandler / SSEHandler (github.com/flanksource/clicky/task/snapshot.go). Field\n// names match the JSON tags exactly. This module is generic — it carries no\n// application-specific concepts.\n\nexport interface LogEntry {\n level: string;\n message: string;\n}\n\nexport interface TaskSnapshot {\n id: string;\n name: string;\n /** \"task\" or \"group\". */\n type: \"task\" | \"group\";\n /** Parent group NAME (groups: empty). */\n group?: string;\n status: string;\n duration?: string;\n error?: string;\n /** Latest log line. */\n message?: string;\n logs?: LogEntry[];\n /** Group aggregates. */\n total?: number;\n completed?: number;\n failed?: number;\n running?: number;\n /** Per-task (type === \"task\") live stage label and bounded progress. */\n description?: string;\n progress?: number;\n /** Progress denominator; 0/undefined means the task has no bounded progress. */\n maxValue?: number;\n /** Stable parent-run id (tasks) or the run's own id (groups). */\n groupId?: string;\n kind?: string;\n labels?: Record<string, string>;\n owner?: string;\n /** RFC3339. */\n startedAt?: string;\n /** RFC3339. */\n finishedAt?: string;\n}\n\n// TaskRunMeta mirrors the Go `task.RunMeta` listing summary returned by\n// GET {prefix}/tasks. Named TaskRunMeta to avoid colliding with the unrelated\n// diagnostics `RunMeta` already exported by this package.\nexport interface TaskRunMeta {\n id: string;\n name: string;\n kind?: string;\n labels?: Record<string, string>;\n owner?: string;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n total: number;\n completed: number;\n failed: number;\n running: number;\n}\n\n// Terminal run/group statuses (not running/pending). Used to decide when an SSE\n// stream is done or when polling should stop.\nconst TERMINAL_STATUSES = new Set([\n \"success\",\n \"failed\",\n \"warning\",\n \"canceled\",\n \"PASS\",\n \"FAIL\",\n \"ERR\",\n \"SKIP\",\n]);\n\nexport function isTerminalStatus(status: string | undefined): boolean {\n return !!status && TERMINAL_STATUSES.has(status);\n}\n\n// allGroupsTerminal reports whether every group snapshot has reached a terminal\n// status — i.e. the run(s) are complete and an SSE stream / poll can stop.\nexport function allGroupsTerminal(snapshots: TaskSnapshot[]): boolean {\n const groups = snapshots.filter((s) => s.type === \"group\");\n if (groups.length === 0) return false;\n return groups.every((g) => isTerminalStatus(g.status));\n}\n"],"names":[],"mappings":";;AAgEA,MAAM,wCAAwB,IAAI;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAEM,SAAS,iBAAiB,QAAqC;AACpE,SAAO,CAAC,CAAC,UAAU,kBAAkB,IAAI,MAAM;AACjD;AAIO,SAAS,kBAAkB,WAAoC;AACpE,QAAM,SAAS,UAAU,OAAO,CAAC,MAAM,EAAE,SAAS,OAAO;AACzD,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,SAAO,OAAO,MAAM,CAAC,MAAM,iBAAiB,EAAE,MAAM,CAAC;AACvD;;;"}
@@ -20,6 +20,11 @@ export interface TaskSnapshot {
20
20
  completed?: number;
21
21
  failed?: number;
22
22
  running?: number;
23
+ /** Per-task (type === "task") live stage label and bounded progress. */
24
+ description?: string;
25
+ progress?: number;
26
+ /** Progress denominator; 0/undefined means the task has no bounded progress. */
27
+ maxValue?: number;
23
28
  /** Stable parent-run id (tasks) or the run's own id (groups). */
24
29
  groupId?: string;
25
30
  kind?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"TaskSnapshot.d.ts","sourceRoot":"","sources":["../../src/data/TaskSnapshot.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,yBAAyB;IACzB,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IACvB,yCAAyC;IACzC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uBAAuB;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC;IAClB,wBAAwB;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iEAAiE;IACjE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAKD,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAeD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAEpE;AAID,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,YAAY,EAAE,GAAG,OAAO,CAIpE"}
1
+ {"version":3,"file":"TaskSnapshot.d.ts","sourceRoot":"","sources":["../../src/data/TaskSnapshot.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,yBAAyB;IACzB,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IACvB,yCAAyC;IACzC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uBAAuB;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC;IAClB,wBAAwB;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wEAAwE;IACxE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAKD,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAeD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAEpE;AAID,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,YAAY,EAAE,GAAG,OAAO,CAIpE"}
@@ -1 +1 @@
1
- {"version":3,"file":"TaskSnapshot.js","sources":["../../src/data/TaskSnapshot.ts"],"sourcesContent":["// TaskSnapshot mirrors the Go `task.TaskSnapshot` wire format emitted by clicky's\n// JSONHandler / SSEHandler (github.com/flanksource/clicky/task/snapshot.go). Field\n// names match the JSON tags exactly. This module is generic — it carries no\n// application-specific concepts.\n\nexport interface LogEntry {\n level: string;\n message: string;\n}\n\nexport interface TaskSnapshot {\n id: string;\n name: string;\n /** \"task\" or \"group\". */\n type: \"task\" | \"group\";\n /** Parent group NAME (groups: empty). */\n group?: string;\n status: string;\n duration?: string;\n error?: string;\n /** Latest log line. */\n message?: string;\n logs?: LogEntry[];\n /** Group aggregates. */\n total?: number;\n completed?: number;\n failed?: number;\n running?: number;\n /** Stable parent-run id (tasks) or the run's own id (groups). */\n groupId?: string;\n kind?: string;\n labels?: Record<string, string>;\n owner?: string;\n /** RFC3339. */\n startedAt?: string;\n /** RFC3339. */\n finishedAt?: string;\n}\n\n// TaskRunMeta mirrors the Go `task.RunMeta` listing summary returned by\n// GET {prefix}/tasks. Named TaskRunMeta to avoid colliding with the unrelated\n// diagnostics `RunMeta` already exported by this package.\nexport interface TaskRunMeta {\n id: string;\n name: string;\n kind?: string;\n labels?: Record<string, string>;\n owner?: string;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n total: number;\n completed: number;\n failed: number;\n running: number;\n}\n\n// Terminal run/group statuses (not running/pending). Used to decide when an SSE\n// stream is done or when polling should stop.\nconst TERMINAL_STATUSES = new Set([\n \"success\",\n \"failed\",\n \"warning\",\n \"canceled\",\n \"PASS\",\n \"FAIL\",\n \"ERR\",\n \"SKIP\",\n]);\n\nexport function isTerminalStatus(status: string | undefined): boolean {\n return !!status && TERMINAL_STATUSES.has(status);\n}\n\n// allGroupsTerminal reports whether every group snapshot has reached a terminal\n// status — i.e. the run(s) are complete and an SSE stream / poll can stop.\nexport function allGroupsTerminal(snapshots: TaskSnapshot[]): boolean {\n const groups = snapshots.filter((s) => s.type === \"group\");\n if (groups.length === 0) return false;\n return groups.every((g) => isTerminalStatus(g.status));\n}\n"],"names":[],"mappings":"AA2DA,MAAM,wCAAwB,IAAI;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAEM,SAAS,iBAAiB,QAAqC;AACpE,SAAO,CAAC,CAAC,UAAU,kBAAkB,IAAI,MAAM;AACjD;AAIO,SAAS,kBAAkB,WAAoC;AACpE,QAAM,SAAS,UAAU,OAAO,CAAC,MAAM,EAAE,SAAS,OAAO;AACzD,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,SAAO,OAAO,MAAM,CAAC,MAAM,iBAAiB,EAAE,MAAM,CAAC;AACvD;"}
1
+ {"version":3,"file":"TaskSnapshot.js","sources":["../../src/data/TaskSnapshot.ts"],"sourcesContent":["// TaskSnapshot mirrors the Go `task.TaskSnapshot` wire format emitted by clicky's\n// JSONHandler / SSEHandler (github.com/flanksource/clicky/task/snapshot.go). Field\n// names match the JSON tags exactly. This module is generic — it carries no\n// application-specific concepts.\n\nexport interface LogEntry {\n level: string;\n message: string;\n}\n\nexport interface TaskSnapshot {\n id: string;\n name: string;\n /** \"task\" or \"group\". */\n type: \"task\" | \"group\";\n /** Parent group NAME (groups: empty). */\n group?: string;\n status: string;\n duration?: string;\n error?: string;\n /** Latest log line. */\n message?: string;\n logs?: LogEntry[];\n /** Group aggregates. */\n total?: number;\n completed?: number;\n failed?: number;\n running?: number;\n /** Per-task (type === \"task\") live stage label and bounded progress. */\n description?: string;\n progress?: number;\n /** Progress denominator; 0/undefined means the task has no bounded progress. */\n maxValue?: number;\n /** Stable parent-run id (tasks) or the run's own id (groups). */\n groupId?: string;\n kind?: string;\n labels?: Record<string, string>;\n owner?: string;\n /** RFC3339. */\n startedAt?: string;\n /** RFC3339. */\n finishedAt?: string;\n}\n\n// TaskRunMeta mirrors the Go `task.RunMeta` listing summary returned by\n// GET {prefix}/tasks. Named TaskRunMeta to avoid colliding with the unrelated\n// diagnostics `RunMeta` already exported by this package.\nexport interface TaskRunMeta {\n id: string;\n name: string;\n kind?: string;\n labels?: Record<string, string>;\n owner?: string;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n total: number;\n completed: number;\n failed: number;\n running: number;\n}\n\n// Terminal run/group statuses (not running/pending). Used to decide when an SSE\n// stream is done or when polling should stop.\nconst TERMINAL_STATUSES = new Set([\n \"success\",\n \"failed\",\n \"warning\",\n \"canceled\",\n \"PASS\",\n \"FAIL\",\n \"ERR\",\n \"SKIP\",\n]);\n\nexport function isTerminalStatus(status: string | undefined): boolean {\n return !!status && TERMINAL_STATUSES.has(status);\n}\n\n// allGroupsTerminal reports whether every group snapshot has reached a terminal\n// status — i.e. the run(s) are complete and an SSE stream / poll can stop.\nexport function allGroupsTerminal(snapshots: TaskSnapshot[]): boolean {\n const groups = snapshots.filter((s) => s.type === \"group\");\n if (groups.length === 0) return false;\n return groups.every((g) => isTerminalStatus(g.status));\n}\n"],"names":[],"mappings":"AAgEA,MAAM,wCAAwB,IAAI;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAEM,SAAS,iBAAiB,QAAqC;AACpE,SAAO,CAAC,CAAC,UAAU,kBAAkB,IAAI,MAAM;AACjD;AAIO,SAAS,kBAAkB,WAAoC;AACpE,QAAM,SAAS,UAAU,OAAO,CAAC,MAAM,EAAE,SAAS,OAAO;AACzD,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,SAAO,OAAO,MAAM,CAAC,MAAM,iBAAiB,EAAE,MAAM,CAAC;AACvD;"}
@@ -9,6 +9,29 @@ const UiSearch = require("../icons/components/UiSearch.cjs");
9
9
  const UiClose = require("../icons/components/UiClose.cjs");
10
10
  const UiExpandAll = require("../icons/components/UiExpandAll.cjs");
11
11
  const UiCollapseAll = require("../icons/components/UiCollapseAll.cjs");
12
+ function ancestorPathKeys(roots, getChildren, getKey, target) {
13
+ const targetKey = getKey(target);
14
+ const path = [];
15
+ const visit = (node) => {
16
+ if (getKey(node) === targetKey) return true;
17
+ for (const child of getChildren(node) ?? []) {
18
+ if (visit(child)) {
19
+ path.push(getKey(node));
20
+ return true;
21
+ }
22
+ }
23
+ return false;
24
+ };
25
+ for (const root of roots) {
26
+ if (visit(root)) return path.length ? new Set(path) : null;
27
+ }
28
+ return null;
29
+ }
30
+ function mergeKeySets(a, b) {
31
+ if (!a) return b;
32
+ if (!b) return a;
33
+ return /* @__PURE__ */ new Set([...a, ...b]);
34
+ }
12
35
  function countTreeEdges(roots, getChildren) {
13
36
  let total = 0;
14
37
  const stack = [...roots];
@@ -106,6 +129,7 @@ function Tree({
106
129
  onExpandAllChange,
107
130
  toolbarClassName,
108
131
  getSearchText,
132
+ revealSelected = false,
109
133
  ...nodeProps
110
134
  }) {
111
135
  const [internalExpandAll, setInternalExpandAll] = react.useState(null);
@@ -139,7 +163,14 @@ function Tree({
139
163
  );
140
164
  const treeRoots = filteredTree.roots;
141
165
  const filteredChildren = filteredTree.filteredChildren;
142
- const forcedOpenKeys = filteredTree.forcedOpenKeys;
166
+ const revealKeys = react.useMemo(
167
+ () => revealSelected && nodeProps.selected ? ancestorPathKeys(roots, nodeProps.getChildren, nodeProps.getKey, nodeProps.selected) : null,
168
+ [revealSelected, roots, nodeProps.getChildren, nodeProps.getKey, nodeProps.selected]
169
+ );
170
+ const forcedOpenKeys = react.useMemo(
171
+ () => mergeKeySets(filteredTree.forcedOpenKeys, revealKeys),
172
+ [filteredTree.forcedOpenKeys, revealKeys]
173
+ );
143
174
  const effectiveGetChildren = filteredChildren ? (node) => filteredChildren.get(nodeProps.getKey(node)) ?? [] : nodeProps.getChildren;
144
175
  const effectiveExpandAll = expandAll;
145
176
  const effectiveEmpty = activeFilter.trim() ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-3 py-4 text-sm text-muted-foreground", children: "No matching tree nodes." }) : empty;
@@ -1 +1 @@
1
- {"version":3,"file":"Tree.cjs","sources":["../../src/data/Tree.tsx"],"sourcesContent":["import { useMemo, useState, type ReactNode } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Icon } from \"./Icon\";\nimport { UiClose, UiCollapseAll, UiExpandAll, UiSearch } from \"../icons\";\nimport { TreeNode, type TreeNodeProps } from \"./TreeNode\";\n\nexport type TreeProps<T> = Omit<\n TreeNodeProps<T>,\n \"node\" | \"depth\" | \"expandAll\" | \"forcedOpenKeys\"\n> & {\n /** Root nodes to render. */\n roots: T[];\n /** Empty-state content when no roots are provided. */\n empty?: ReactNode;\n /** Classes applied to the tree root. */\n className?: string;\n /**\n * Render the Expand all / Collapse all toolbar above the tree. Enabled by\n * default. Set to false to suppress when the tree is tiny or the controls\n * are owned externally.\n */\n showControls?: boolean;\n /**\n * Externally controlled expand-all state. When provided together with\n * `onExpandAllChange`, the tree is controlled; otherwise it manages the\n * toolbar state internally.\n */\n expandAll?: boolean | null;\n /** Called when the toolbar expand-all state changes. */\n onExpandAllChange?: (next: boolean | null) => void;\n /** Classes applied to the search/control toolbar. */\n toolbarClassName?: string;\n /**\n * Override the text the filter matches against for a given node.\n * Defaults to a recursive walk of the node's own fields (excluding\n * its `children` and any secondary children), which works for\n * plain-shape trees but leaks text from nested domain objects —\n * e.g. a SQL Table's Field rows are marked secondary, yet the\n * parent record still carries `record.fields[].field` strings that\n * the default walker would collect. Callers that need precise\n * control over search behaviour should return only the text they\n * want surfaced (the node's own display label / id / type).\n */\n getSearchText?: (node: T) => string;\n};\n\n// Edge counting, search-text collection and filtering all walk the tree through\n// the synchronous `getChildren` only. A lazy node (one declared via\n// `hasMoreChildren` whose children have not been fetched yet) therefore\n// contributes no edges and no search text until the operator expands it — so it\n// is invisible to \"expand all\" and to the filter while still collapsed. This is\n// intentional: we never trigger network loads from bulk/search operations.\nfunction countTreeEdges<T>(roots: T[], getChildren: TreeProps<T>[\"getChildren\"]) {\n let total = 0;\n const stack = [...roots];\n\n while (stack.length > 0) {\n const node = stack.pop()!;\n const children = getChildren(node) ?? [];\n total += children.length;\n stack.push(...children);\n }\n\n return total;\n}\n\nfunction collectTreeSearchText(\n value: unknown,\n excluded = new Set<unknown>(),\n seen = new Set<unknown>(),\n): string {\n if (value == null || excluded.has(value)) return \"\";\n\n if (\n typeof value === \"string\" ||\n typeof value === \"number\" ||\n typeof value === \"boolean\" ||\n typeof value === \"bigint\"\n ) {\n return String(value);\n }\n\n if (typeof value !== \"object\") return \"\";\n if (seen.has(value)) return \"\";\n seen.add(value);\n\n if (Array.isArray(value)) {\n return value\n .map((entry) => collectTreeSearchText(entry, excluded, seen))\n .filter(Boolean)\n .join(\" \");\n }\n\n const preferredKeys = [\"label\", \"name\", \"title\", \"text\", \"plain\", \"id\", \"content\"];\n const record = value as Record<string, unknown>;\n const chunks: string[] = [];\n\n for (const key of preferredKeys) {\n if (!(key in record)) continue;\n const text = collectTreeSearchText(record[key], excluded, seen);\n if (text) chunks.push(text);\n }\n\n for (const [key, entry] of Object.entries(record)) {\n if (key === \"children\" || preferredKeys.includes(key)) continue;\n const text = collectTreeSearchText(entry, excluded, seen);\n if (text) chunks.push(text);\n }\n\n return chunks.join(\" \");\n}\n\nfunction nodeSearchText<T>(node: T, children: ReturnType<TreeProps<T>[\"getChildren\"]>) {\n const excluded = new Set<unknown>();\n if (children) excluded.add(children);\n return collectTreeSearchText(node, excluded).trim().toLowerCase();\n}\n\nfunction filterTreeRoots<T>(\n roots: T[],\n getChildren: TreeProps<T>[\"getChildren\"],\n getKey: TreeProps<T>[\"getKey\"],\n query: string,\n isSecondary?: (node: T) => boolean,\n getSearchText?: (node: T) => string,\n) {\n const normalizedQuery = query.trim().toLowerCase();\n if (!normalizedQuery) {\n return {\n roots,\n filteredChildren: null as Map<string | number, T[]> | null,\n forcedOpenKeys: null as Set<string | number> | null,\n };\n }\n\n const filteredChildren = new Map<string | number, T[]>();\n const forcedOpenKeys = new Set<string | number>();\n\n function matchText(node: T, children: T[]): string {\n // Honour the caller-supplied text extractor first — it's the\n // only way to get precise control over what the filter sees when\n // the node carries domain data whose fields would otherwise leak\n // into the default walker.\n if (getSearchText) return getSearchText(node).trim().toLowerCase();\n // Match-text skips secondary children so e.g. a Table's search\n // text only draws from the Table itself, not from its Field\n // rows. The Table is primary, so its own text still counts.\n const searchChildren = children.filter((c) => !isSecondary?.(c));\n return nodeSearchText(node, searchChildren);\n }\n\n function visit(node: T): boolean {\n // A secondary node never drives a filter match — it's\n // effectively invisible to search. Still rendered when the\n // caller opens the parent manually. This is what lets a 28k-row\n // field tree stay searchable at the record level without \"field\n // name contains 'x'\" matches spamming the results.\n if (isSecondary?.(node)) return false;\n\n const key = getKey(node);\n const children = getChildren(node) ?? [];\n const childrenAllSecondary =\n children.length > 0 && children.every((child) => isSecondary?.(child));\n const matches = matchText(node, children).includes(normalizedQuery);\n const visibleChildren: T[] = [];\n\n for (const child of children) {\n if (visit(child)) visibleChildren.push(child);\n }\n\n if (matches) {\n if (children.length > 0) {\n filteredChildren.set(key, children);\n if (!childrenAllSecondary) forcedOpenKeys.add(key);\n }\n return true;\n }\n\n if (visibleChildren.length > 0) {\n filteredChildren.set(key, visibleChildren);\n forcedOpenKeys.add(key);\n return true;\n }\n\n return false;\n }\n\n return {\n roots: roots.filter((root) => visit(root)),\n filteredChildren,\n forcedOpenKeys,\n };\n}\n\nexport function Tree<T>({\n roots,\n empty,\n className,\n showControls = true,\n expandAll: controlledExpandAll,\n onExpandAllChange,\n toolbarClassName,\n getSearchText,\n ...nodeProps\n}: TreeProps<T>) {\n const [internalExpandAll, setInternalExpandAll] = useState<boolean | null>(null);\n const [filterQuery, setFilterQuery] = useState(\"\");\n const basePaddingPx = nodeProps.basePaddingPx ?? 8;\n const isControlled = onExpandAllChange !== undefined;\n const expandAll = isControlled ? (controlledExpandAll ?? null) : internalExpandAll;\n const totalEdges = useMemo(\n () => countTreeEdges(roots, nodeProps.getChildren),\n [roots, nodeProps.getChildren],\n );\n const showFilter = totalEdges > 20;\n const activeFilter = showFilter ? filterQuery : \"\";\n const filteredTree = useMemo(\n () =>\n filterTreeRoots(\n roots,\n nodeProps.getChildren,\n nodeProps.getKey,\n activeFilter,\n nodeProps.isSecondary,\n getSearchText,\n ),\n [\n roots,\n nodeProps.getChildren,\n nodeProps.getKey,\n activeFilter,\n nodeProps.isSecondary,\n getSearchText,\n ],\n );\n const treeRoots = filteredTree.roots;\n const filteredChildren = filteredTree.filteredChildren;\n const forcedOpenKeys = filteredTree.forcedOpenKeys;\n const effectiveGetChildren = filteredChildren\n ? (node: T) => filteredChildren.get(nodeProps.getKey(node)) ?? []\n : nodeProps.getChildren;\n const effectiveExpandAll = expandAll;\n const effectiveEmpty = activeFilter.trim() ? (\n <div className=\"px-3 py-4 text-sm text-muted-foreground\">No matching tree nodes.</div>\n ) : (\n empty\n );\n const showControlButtons = showControls && roots.length > 0;\n const showVirtualRow = showFilter || showControlButtons;\n const setExpandAll = (next: boolean | null) => {\n if (isControlled) onExpandAllChange?.(next);\n else setInternalExpandAll(next);\n };\n\n if (treeRoots.length === 0 && !showVirtualRow) return <>{effectiveEmpty ?? null}</>;\n\n return (\n <div className={cn(\"flex flex-col min-h-0\", className)}>\n <div role=\"tree\" className=\"min-h-0 flex-1 overflow-auto\">\n {showVirtualRow && (\n <div\n role=\"presentation\"\n className={cn(\n \"sticky top-0 z-10 flex items-center gap-1.5 border-b border-border/70 bg-background/95 py-1 pr-2 text-sm backdrop-blur supports-[backdrop-filter]:bg-background/80\",\n toolbarClassName,\n )}\n style={{ paddingLeft: `${basePaddingPx}px` }}\n >\n {showFilter ? (\n <Icon icon={UiSearch} className=\"w-3 shrink-0 text-xs text-muted-foreground\" />\n ) : (\n <span className=\"w-3 shrink-0\" aria-hidden />\n )}\n\n {showFilter ? (\n <label className=\"flex min-w-0 flex-1 items-center gap-2 rounded-md border border-border bg-background px-2 py-1 text-xs text-foreground shadow-sm\">\n <input\n type=\"search\"\n value={filterQuery}\n onChange={(event) => setFilterQuery(event.target.value)}\n placeholder=\"Filter tree\"\n className=\"h-5 w-full border-0 bg-transparent p-0 text-xs outline-none placeholder:text-muted-foreground\"\n aria-label=\"Filter tree nodes\"\n />\n {filterQuery && (\n <button\n type=\"button\"\n onClick={() => setFilterQuery(\"\")}\n className=\"inline-flex items-center rounded p-0.5 text-muted-foreground hover:bg-accent hover:text-accent-foreground\"\n aria-label=\"Clear tree filter\"\n title=\"Clear tree filter\"\n >\n <Icon icon={UiClose} className=\"text-xs\" />\n </button>\n )}\n </label>\n ) : (\n <span className=\"flex-1\" />\n )}\n\n {showControlButtons && (\n <div className=\"ml-auto flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => setExpandAll(true)}\n aria-pressed={effectiveExpandAll === true}\n aria-label=\"Expand all\"\n title=\"Expand all\"\n disabled={activeFilter.trim().length > 0}\n className={cn(\n \"inline-flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:cursor-not-allowed disabled:opacity-50\",\n effectiveExpandAll === true && \"bg-accent text-accent-foreground\",\n )}\n >\n <Icon icon={UiExpandAll} className=\"text-sm\" />\n </button>\n <button\n type=\"button\"\n onClick={() => setExpandAll(false)}\n aria-pressed={effectiveExpandAll === false}\n aria-label=\"Collapse all\"\n title=\"Collapse all\"\n disabled={activeFilter.trim().length > 0}\n className={cn(\n \"inline-flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:cursor-not-allowed disabled:opacity-50\",\n effectiveExpandAll === false && \"bg-accent text-accent-foreground\",\n )}\n >\n <Icon icon={UiCollapseAll} className=\"text-sm\" />\n </button>\n </div>\n )}\n </div>\n )}\n\n {treeRoots.length === 0 ? (\n (effectiveEmpty ?? null)\n ) : (\n <>\n {treeRoots.map((root) => (\n <TreeNode<T>\n {...nodeProps}\n key={nodeProps.getKey(root)}\n node={root}\n expandAll={effectiveExpandAll}\n forcedOpenKeys={forcedOpenKeys}\n getChildren={effectiveGetChildren}\n />\n ))}\n </>\n )}\n </div>\n </div>\n );\n}\n"],"names":["useState","useMemo","jsx","Fragment","cn","jsxs","Icon","UiSearch","UiClose","UiExpandAll","UiCollapseAll","createElement","TreeNode"],"mappings":"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"}
1
+ {"version":3,"file":"Tree.cjs","sources":["../../src/data/Tree.tsx"],"sourcesContent":["import { useMemo, useState, type ReactNode } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Icon } from \"./Icon\";\nimport { UiClose, UiCollapseAll, UiExpandAll, UiSearch } from \"../icons\";\nimport { TreeNode, type TreeNodeProps } from \"./TreeNode\";\n\nexport type TreeProps<T> = Omit<\n TreeNodeProps<T>,\n \"node\" | \"depth\" | \"expandAll\" | \"forcedOpenKeys\"\n> & {\n /** Root nodes to render. */\n roots: T[];\n /** Empty-state content when no roots are provided. */\n empty?: ReactNode;\n /** Classes applied to the tree root. */\n className?: string;\n /**\n * Render the Expand all / Collapse all toolbar above the tree. Enabled by\n * default. Set to false to suppress when the tree is tiny or the controls\n * are owned externally.\n */\n showControls?: boolean;\n /**\n * Externally controlled expand-all state. When provided together with\n * `onExpandAllChange`, the tree is controlled; otherwise it manages the\n * toolbar state internally.\n */\n expandAll?: boolean | null;\n /** Called when the toolbar expand-all state changes. */\n onExpandAllChange?: (next: boolean | null) => void;\n /** Classes applied to the search/control toolbar. */\n toolbarClassName?: string;\n /**\n * Override the text the filter matches against for a given node.\n * Defaults to a recursive walk of the node's own fields (excluding\n * its `children` and any secondary children), which works for\n * plain-shape trees but leaks text from nested domain objects —\n * e.g. a SQL Table's Field rows are marked secondary, yet the\n * parent record still carries `record.fields[].field` strings that\n * the default walker would collect. Callers that need precise\n * control over search behaviour should return only the text they\n * want surfaced (the node's own display label / id / type).\n */\n getSearchText?: (node: T) => string;\n /**\n * Force-open the ancestors of the `selected` node so it is always revealed,\n * even when its branch was collapsed (a deep-link, a programmatic selection\n * from outside the tree, or simply a folder the user never opened). While a\n * node is selected its ancestors cannot be collapsed. Off by default; other\n * nodes keep their normal expand state. Requires `selected` to be set.\n */\n revealSelected?: boolean;\n};\n\n// ancestorPathKeys returns the keys of every ancestor on the path from a root\n// down to `target` (excluding target itself — opening the ancestors is what\n// makes target render). Matches by getKey so it is robust to object identity\n// across roots rebuilds. Returns null when target is not found.\nfunction ancestorPathKeys<T>(\n roots: T[],\n getChildren: TreeProps<T>[\"getChildren\"],\n getKey: TreeProps<T>[\"getKey\"],\n target: T,\n): Set<string | number> | null {\n const targetKey = getKey(target);\n const path: (string | number)[] = [];\n const visit = (node: T): boolean => {\n if (getKey(node) === targetKey) return true;\n for (const child of getChildren(node) ?? []) {\n if (visit(child)) {\n path.push(getKey(node));\n return true;\n }\n }\n return false;\n };\n for (const root of roots) {\n if (visit(root)) return path.length ? new Set(path) : null;\n }\n return null;\n}\n\n// mergeKeySets unions two optional key sets, preserving the null fast-path when\n// both are empty so consumers without a filter or reveal pass null downstream.\nfunction mergeKeySets(\n a: Set<string | number> | null,\n b: Set<string | number> | null,\n): Set<string | number> | null {\n if (!a) return b;\n if (!b) return a;\n return new Set([...a, ...b]);\n}\n\n// Edge counting, search-text collection and filtering all walk the tree through\n// the synchronous `getChildren` only. A lazy node (one declared via\n// `hasMoreChildren` whose children have not been fetched yet) therefore\n// contributes no edges and no search text until the operator expands it — so it\n// is invisible to \"expand all\" and to the filter while still collapsed. This is\n// intentional: we never trigger network loads from bulk/search operations.\nfunction countTreeEdges<T>(roots: T[], getChildren: TreeProps<T>[\"getChildren\"]) {\n let total = 0;\n const stack = [...roots];\n\n while (stack.length > 0) {\n const node = stack.pop()!;\n const children = getChildren(node) ?? [];\n total += children.length;\n stack.push(...children);\n }\n\n return total;\n}\n\nfunction collectTreeSearchText(\n value: unknown,\n excluded = new Set<unknown>(),\n seen = new Set<unknown>(),\n): string {\n if (value == null || excluded.has(value)) return \"\";\n\n if (\n typeof value === \"string\" ||\n typeof value === \"number\" ||\n typeof value === \"boolean\" ||\n typeof value === \"bigint\"\n ) {\n return String(value);\n }\n\n if (typeof value !== \"object\") return \"\";\n if (seen.has(value)) return \"\";\n seen.add(value);\n\n if (Array.isArray(value)) {\n return value\n .map((entry) => collectTreeSearchText(entry, excluded, seen))\n .filter(Boolean)\n .join(\" \");\n }\n\n const preferredKeys = [\"label\", \"name\", \"title\", \"text\", \"plain\", \"id\", \"content\"];\n const record = value as Record<string, unknown>;\n const chunks: string[] = [];\n\n for (const key of preferredKeys) {\n if (!(key in record)) continue;\n const text = collectTreeSearchText(record[key], excluded, seen);\n if (text) chunks.push(text);\n }\n\n for (const [key, entry] of Object.entries(record)) {\n if (key === \"children\" || preferredKeys.includes(key)) continue;\n const text = collectTreeSearchText(entry, excluded, seen);\n if (text) chunks.push(text);\n }\n\n return chunks.join(\" \");\n}\n\nfunction nodeSearchText<T>(node: T, children: ReturnType<TreeProps<T>[\"getChildren\"]>) {\n const excluded = new Set<unknown>();\n if (children) excluded.add(children);\n return collectTreeSearchText(node, excluded).trim().toLowerCase();\n}\n\nfunction filterTreeRoots<T>(\n roots: T[],\n getChildren: TreeProps<T>[\"getChildren\"],\n getKey: TreeProps<T>[\"getKey\"],\n query: string,\n isSecondary?: (node: T) => boolean,\n getSearchText?: (node: T) => string,\n) {\n const normalizedQuery = query.trim().toLowerCase();\n if (!normalizedQuery) {\n return {\n roots,\n filteredChildren: null as Map<string | number, T[]> | null,\n forcedOpenKeys: null as Set<string | number> | null,\n };\n }\n\n const filteredChildren = new Map<string | number, T[]>();\n const forcedOpenKeys = new Set<string | number>();\n\n function matchText(node: T, children: T[]): string {\n // Honour the caller-supplied text extractor first — it's the\n // only way to get precise control over what the filter sees when\n // the node carries domain data whose fields would otherwise leak\n // into the default walker.\n if (getSearchText) return getSearchText(node).trim().toLowerCase();\n // Match-text skips secondary children so e.g. a Table's search\n // text only draws from the Table itself, not from its Field\n // rows. The Table is primary, so its own text still counts.\n const searchChildren = children.filter((c) => !isSecondary?.(c));\n return nodeSearchText(node, searchChildren);\n }\n\n function visit(node: T): boolean {\n // A secondary node never drives a filter match — it's\n // effectively invisible to search. Still rendered when the\n // caller opens the parent manually. This is what lets a 28k-row\n // field tree stay searchable at the record level without \"field\n // name contains 'x'\" matches spamming the results.\n if (isSecondary?.(node)) return false;\n\n const key = getKey(node);\n const children = getChildren(node) ?? [];\n const childrenAllSecondary =\n children.length > 0 && children.every((child) => isSecondary?.(child));\n const matches = matchText(node, children).includes(normalizedQuery);\n const visibleChildren: T[] = [];\n\n for (const child of children) {\n if (visit(child)) visibleChildren.push(child);\n }\n\n if (matches) {\n if (children.length > 0) {\n filteredChildren.set(key, children);\n if (!childrenAllSecondary) forcedOpenKeys.add(key);\n }\n return true;\n }\n\n if (visibleChildren.length > 0) {\n filteredChildren.set(key, visibleChildren);\n forcedOpenKeys.add(key);\n return true;\n }\n\n return false;\n }\n\n return {\n roots: roots.filter((root) => visit(root)),\n filteredChildren,\n forcedOpenKeys,\n };\n}\n\nexport function Tree<T>({\n roots,\n empty,\n className,\n showControls = true,\n expandAll: controlledExpandAll,\n onExpandAllChange,\n toolbarClassName,\n getSearchText,\n revealSelected = false,\n ...nodeProps\n}: TreeProps<T>) {\n const [internalExpandAll, setInternalExpandAll] = useState<boolean | null>(null);\n const [filterQuery, setFilterQuery] = useState(\"\");\n const basePaddingPx = nodeProps.basePaddingPx ?? 8;\n const isControlled = onExpandAllChange !== undefined;\n const expandAll = isControlled ? (controlledExpandAll ?? null) : internalExpandAll;\n const totalEdges = useMemo(\n () => countTreeEdges(roots, nodeProps.getChildren),\n [roots, nodeProps.getChildren],\n );\n const showFilter = totalEdges > 20;\n const activeFilter = showFilter ? filterQuery : \"\";\n const filteredTree = useMemo(\n () =>\n filterTreeRoots(\n roots,\n nodeProps.getChildren,\n nodeProps.getKey,\n activeFilter,\n nodeProps.isSecondary,\n getSearchText,\n ),\n [\n roots,\n nodeProps.getChildren,\n nodeProps.getKey,\n activeFilter,\n nodeProps.isSecondary,\n getSearchText,\n ],\n );\n const treeRoots = filteredTree.roots;\n const filteredChildren = filteredTree.filteredChildren;\n // Force-open the selected node's ancestors when revealSelected is set, unioned\n // with the filter's own forced-open keys. Recomputed when the selection moves\n // so the tree follows it without a remount.\n const revealKeys = useMemo(\n () =>\n revealSelected && nodeProps.selected\n ? ancestorPathKeys(roots, nodeProps.getChildren, nodeProps.getKey, nodeProps.selected)\n : null,\n [revealSelected, roots, nodeProps.getChildren, nodeProps.getKey, nodeProps.selected],\n );\n const forcedOpenKeys = useMemo(\n () => mergeKeySets(filteredTree.forcedOpenKeys, revealKeys),\n [filteredTree.forcedOpenKeys, revealKeys],\n );\n const effectiveGetChildren = filteredChildren\n ? (node: T) => filteredChildren.get(nodeProps.getKey(node)) ?? []\n : nodeProps.getChildren;\n const effectiveExpandAll = expandAll;\n const effectiveEmpty = activeFilter.trim() ? (\n <div className=\"px-3 py-4 text-sm text-muted-foreground\">No matching tree nodes.</div>\n ) : (\n empty\n );\n const showControlButtons = showControls && roots.length > 0;\n const showVirtualRow = showFilter || showControlButtons;\n const setExpandAll = (next: boolean | null) => {\n if (isControlled) onExpandAllChange?.(next);\n else setInternalExpandAll(next);\n };\n\n if (treeRoots.length === 0 && !showVirtualRow) return <>{effectiveEmpty ?? null}</>;\n\n return (\n <div className={cn(\"flex flex-col min-h-0\", className)}>\n <div role=\"tree\" className=\"min-h-0 flex-1 overflow-auto\">\n {showVirtualRow && (\n <div\n role=\"presentation\"\n className={cn(\n \"sticky top-0 z-10 flex items-center gap-1.5 border-b border-border/70 bg-background/95 py-1 pr-2 text-sm backdrop-blur supports-[backdrop-filter]:bg-background/80\",\n toolbarClassName,\n )}\n style={{ paddingLeft: `${basePaddingPx}px` }}\n >\n {showFilter ? (\n <Icon icon={UiSearch} className=\"w-3 shrink-0 text-xs text-muted-foreground\" />\n ) : (\n <span className=\"w-3 shrink-0\" aria-hidden />\n )}\n\n {showFilter ? (\n <label className=\"flex min-w-0 flex-1 items-center gap-2 rounded-md border border-border bg-background px-2 py-1 text-xs text-foreground shadow-sm\">\n <input\n type=\"search\"\n value={filterQuery}\n onChange={(event) => setFilterQuery(event.target.value)}\n placeholder=\"Filter tree\"\n className=\"h-5 w-full border-0 bg-transparent p-0 text-xs outline-none placeholder:text-muted-foreground\"\n aria-label=\"Filter tree nodes\"\n />\n {filterQuery && (\n <button\n type=\"button\"\n onClick={() => setFilterQuery(\"\")}\n className=\"inline-flex items-center rounded p-0.5 text-muted-foreground hover:bg-accent hover:text-accent-foreground\"\n aria-label=\"Clear tree filter\"\n title=\"Clear tree filter\"\n >\n <Icon icon={UiClose} className=\"text-xs\" />\n </button>\n )}\n </label>\n ) : (\n <span className=\"flex-1\" />\n )}\n\n {showControlButtons && (\n <div className=\"ml-auto flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => setExpandAll(true)}\n aria-pressed={effectiveExpandAll === true}\n aria-label=\"Expand all\"\n title=\"Expand all\"\n disabled={activeFilter.trim().length > 0}\n className={cn(\n \"inline-flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:cursor-not-allowed disabled:opacity-50\",\n effectiveExpandAll === true && \"bg-accent text-accent-foreground\",\n )}\n >\n <Icon icon={UiExpandAll} className=\"text-sm\" />\n </button>\n <button\n type=\"button\"\n onClick={() => setExpandAll(false)}\n aria-pressed={effectiveExpandAll === false}\n aria-label=\"Collapse all\"\n title=\"Collapse all\"\n disabled={activeFilter.trim().length > 0}\n className={cn(\n \"inline-flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:cursor-not-allowed disabled:opacity-50\",\n effectiveExpandAll === false && \"bg-accent text-accent-foreground\",\n )}\n >\n <Icon icon={UiCollapseAll} className=\"text-sm\" />\n </button>\n </div>\n )}\n </div>\n )}\n\n {treeRoots.length === 0 ? (\n (effectiveEmpty ?? null)\n ) : (\n <>\n {treeRoots.map((root) => (\n <TreeNode<T>\n {...nodeProps}\n key={nodeProps.getKey(root)}\n node={root}\n expandAll={effectiveExpandAll}\n forcedOpenKeys={forcedOpenKeys}\n getChildren={effectiveGetChildren}\n />\n ))}\n </>\n )}\n </div>\n </div>\n );\n}\n"],"names":["useState","useMemo","jsx","Fragment","cn","jsxs","Icon","UiSearch","UiClose","UiExpandAll","UiCollapseAll","createElement","TreeNode"],"mappings":"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"}
@@ -35,6 +35,14 @@ export type TreeProps<T> = Omit<TreeNodeProps<T>, "node" | "depth" | "expandAll"
35
35
  * want surfaced (the node's own display label / id / type).
36
36
  */
37
37
  getSearchText?: (node: T) => string;
38
+ /**
39
+ * Force-open the ancestors of the `selected` node so it is always revealed,
40
+ * even when its branch was collapsed (a deep-link, a programmatic selection
41
+ * from outside the tree, or simply a folder the user never opened). While a
42
+ * node is selected its ancestors cannot be collapsed. Off by default; other
43
+ * nodes keep their normal expand state. Requires `selected` to be set.
44
+ */
45
+ revealSelected?: boolean;
38
46
  };
39
- export declare function Tree<T>({ roots, empty, className, showControls, expandAll: controlledExpandAll, onExpandAllChange, toolbarClassName, getSearchText, ...nodeProps }: TreeProps<T>): import("react/jsx-runtime").JSX.Element;
47
+ export declare function Tree<T>({ roots, empty, className, showControls, expandAll: controlledExpandAll, onExpandAllChange, toolbarClassName, getSearchText, revealSelected, ...nodeProps }: TreeProps<T>): import("react/jsx-runtime").JSX.Element;
40
48
  //# sourceMappingURL=Tree.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Tree.d.ts","sourceRoot":"","sources":["../../src/data/Tree.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAI1D,OAAO,EAAY,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAE1D,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI,IAAI,CAC7B,aAAa,CAAC,CAAC,CAAC,EAChB,MAAM,GAAG,OAAO,GAAG,WAAW,GAAG,gBAAgB,CAClD,GAAG;IACF,4BAA4B;IAC5B,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,sDAAsD;IACtD,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAC3B,wDAAwD;IACxD,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,KAAK,IAAI,CAAC;IACnD,qDAAqD;IACrD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,CAAC;CACrC,CAAC;AAsJF,wBAAgB,IAAI,CAAC,CAAC,EAAE,EACtB,KAAK,EACL,KAAK,EACL,SAAS,EACT,YAAmB,EACnB,SAAS,EAAE,mBAAmB,EAC9B,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,GAAG,SAAS,EACb,EAAE,SAAS,CAAC,CAAC,CAAC,2CAsJd"}
1
+ {"version":3,"file":"Tree.d.ts","sourceRoot":"","sources":["../../src/data/Tree.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAI1D,OAAO,EAAY,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAE1D,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI,IAAI,CAC7B,aAAa,CAAC,CAAC,CAAC,EAChB,MAAM,GAAG,OAAO,GAAG,WAAW,GAAG,gBAAgB,CAClD,GAAG;IACF,4BAA4B;IAC5B,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,sDAAsD;IACtD,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAC3B,wDAAwD;IACxD,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,KAAK,IAAI,CAAC;IACnD,qDAAqD;IACrD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,CAAC;IACpC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AA6LF,wBAAgB,IAAI,CAAC,CAAC,EAAE,EACtB,KAAK,EACL,KAAK,EACL,SAAS,EACT,YAAmB,EACnB,SAAS,EAAE,mBAAmB,EAC9B,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,cAAsB,EACtB,GAAG,SAAS,EACb,EAAE,SAAS,CAAC,CAAC,CAAC,2CAmKd"}
package/dist/data/Tree.js CHANGED
@@ -7,6 +7,29 @@ import { UiSearch } from "../icons/components/UiSearch.js";
7
7
  import { UiClose } from "../icons/components/UiClose.js";
8
8
  import { UiExpandAll } from "../icons/components/UiExpandAll.js";
9
9
  import { UiCollapseAll } from "../icons/components/UiCollapseAll.js";
10
+ function ancestorPathKeys(roots, getChildren, getKey, target) {
11
+ const targetKey = getKey(target);
12
+ const path = [];
13
+ const visit = (node) => {
14
+ if (getKey(node) === targetKey) return true;
15
+ for (const child of getChildren(node) ?? []) {
16
+ if (visit(child)) {
17
+ path.push(getKey(node));
18
+ return true;
19
+ }
20
+ }
21
+ return false;
22
+ };
23
+ for (const root of roots) {
24
+ if (visit(root)) return path.length ? new Set(path) : null;
25
+ }
26
+ return null;
27
+ }
28
+ function mergeKeySets(a, b) {
29
+ if (!a) return b;
30
+ if (!b) return a;
31
+ return /* @__PURE__ */ new Set([...a, ...b]);
32
+ }
10
33
  function countTreeEdges(roots, getChildren) {
11
34
  let total = 0;
12
35
  const stack = [...roots];
@@ -104,6 +127,7 @@ function Tree({
104
127
  onExpandAllChange,
105
128
  toolbarClassName,
106
129
  getSearchText,
130
+ revealSelected = false,
107
131
  ...nodeProps
108
132
  }) {
109
133
  const [internalExpandAll, setInternalExpandAll] = useState(null);
@@ -137,7 +161,14 @@ function Tree({
137
161
  );
138
162
  const treeRoots = filteredTree.roots;
139
163
  const filteredChildren = filteredTree.filteredChildren;
140
- const forcedOpenKeys = filteredTree.forcedOpenKeys;
164
+ const revealKeys = useMemo(
165
+ () => revealSelected && nodeProps.selected ? ancestorPathKeys(roots, nodeProps.getChildren, nodeProps.getKey, nodeProps.selected) : null,
166
+ [revealSelected, roots, nodeProps.getChildren, nodeProps.getKey, nodeProps.selected]
167
+ );
168
+ const forcedOpenKeys = useMemo(
169
+ () => mergeKeySets(filteredTree.forcedOpenKeys, revealKeys),
170
+ [filteredTree.forcedOpenKeys, revealKeys]
171
+ );
141
172
  const effectiveGetChildren = filteredChildren ? (node) => filteredChildren.get(nodeProps.getKey(node)) ?? [] : nodeProps.getChildren;
142
173
  const effectiveExpandAll = expandAll;
143
174
  const effectiveEmpty = activeFilter.trim() ? /* @__PURE__ */ jsx("div", { className: "px-3 py-4 text-sm text-muted-foreground", children: "No matching tree nodes." }) : empty;