@flanksource/clicky-ui 0.3.45 → 0.3.46

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 (140) hide show
  1. package/README.md +22 -0
  2. package/dist/data/AccessMark.cjs +23 -0
  3. package/dist/data/AccessMark.cjs.map +1 -0
  4. package/dist/data/AccessMark.d.ts +11 -0
  5. package/dist/data/AccessMark.d.ts.map +1 -0
  6. package/dist/data/AccessMark.js +23 -0
  7. package/dist/data/AccessMark.js.map +1 -0
  8. package/dist/data/CodeDiff.cjs +116 -23
  9. package/dist/data/CodeDiff.cjs.map +1 -1
  10. package/dist/data/CodeDiff.d.ts +26 -0
  11. package/dist/data/CodeDiff.d.ts.map +1 -1
  12. package/dist/data/CodeDiff.js +116 -23
  13. package/dist/data/CodeDiff.js.map +1 -1
  14. package/dist/data/GraphDiagram.cjs +65 -13
  15. package/dist/data/GraphDiagram.cjs.map +1 -1
  16. package/dist/data/GraphDiagram.d.ts +9 -2
  17. package/dist/data/GraphDiagram.d.ts.map +1 -1
  18. package/dist/data/GraphDiagram.js +66 -14
  19. package/dist/data/GraphDiagram.js.map +1 -1
  20. package/dist/data/GraphDiagramGroups.cjs +100 -0
  21. package/dist/data/GraphDiagramGroups.cjs.map +1 -0
  22. package/dist/data/GraphDiagramGroups.d.ts +24 -0
  23. package/dist/data/GraphDiagramGroups.d.ts.map +1 -0
  24. package/dist/data/GraphDiagramGroups.js +100 -0
  25. package/dist/data/GraphDiagramGroups.js.map +1 -0
  26. package/dist/data/GraphDiagramNodes.cjs +17 -31
  27. package/dist/data/GraphDiagramNodes.cjs.map +1 -1
  28. package/dist/data/GraphDiagramNodes.d.ts +6 -13
  29. package/dist/data/GraphDiagramNodes.d.ts.map +1 -1
  30. package/dist/data/GraphDiagramNodes.js +17 -31
  31. package/dist/data/GraphDiagramNodes.js.map +1 -1
  32. package/dist/data/TreeNode.cjs +1 -1
  33. package/dist/data/TreeNode.cjs.map +1 -1
  34. package/dist/data/TreeNode.d.ts.map +1 -1
  35. package/dist/data/TreeNode.js +1 -1
  36. package/dist/data/TreeNode.js.map +1 -1
  37. package/dist/data/call-graph/CallGraph.cjs +50 -31
  38. package/dist/data/call-graph/CallGraph.cjs.map +1 -1
  39. package/dist/data/call-graph/CallGraph.d.ts +10 -1
  40. package/dist/data/call-graph/CallGraph.d.ts.map +1 -1
  41. package/dist/data/call-graph/CallGraph.js +52 -33
  42. package/dist/data/call-graph/CallGraph.js.map +1 -1
  43. package/dist/data/call-graph/CallGraphMembers.cjs +5 -4
  44. package/dist/data/call-graph/CallGraphMembers.cjs.map +1 -1
  45. package/dist/data/call-graph/CallGraphMembers.d.ts +1 -1
  46. package/dist/data/call-graph/CallGraphMembers.d.ts.map +1 -1
  47. package/dist/data/call-graph/CallGraphMembers.js +5 -4
  48. package/dist/data/call-graph/CallGraphMembers.js.map +1 -1
  49. package/dist/data/call-graph/CallGraphToolbar.cjs +17 -5
  50. package/dist/data/call-graph/CallGraphToolbar.cjs.map +1 -1
  51. package/dist/data/call-graph/CallGraphToolbar.d.ts +10 -0
  52. package/dist/data/call-graph/CallGraphToolbar.d.ts.map +1 -1
  53. package/dist/data/call-graph/CallGraphToolbar.js +18 -6
  54. package/dist/data/call-graph/CallGraphToolbar.js.map +1 -1
  55. package/dist/data/call-graph/call-graph-model.cjs +169 -18
  56. package/dist/data/call-graph/call-graph-model.cjs.map +1 -1
  57. package/dist/data/call-graph/call-graph-model.d.ts +40 -4
  58. package/dist/data/call-graph/call-graph-model.d.ts.map +1 -1
  59. package/dist/data/call-graph/call-graph-model.js +170 -19
  60. package/dist/data/call-graph/call-graph-model.js.map +1 -1
  61. package/dist/data/call-graph/call-graph-vocabulary.cjs +19 -1
  62. package/dist/data/call-graph/call-graph-vocabulary.cjs.map +1 -1
  63. package/dist/data/call-graph/call-graph-vocabulary.d.ts +4 -0
  64. package/dist/data/call-graph/call-graph-vocabulary.d.ts.map +1 -1
  65. package/dist/data/call-graph/call-graph-vocabulary.js +19 -1
  66. package/dist/data/call-graph/call-graph-vocabulary.js.map +1 -1
  67. package/dist/data/call-graph/call-graph.fixtures.d.ts.map +1 -1
  68. package/dist/data/call-graph/index.d.ts +2 -2
  69. package/dist/data/call-graph/index.d.ts.map +1 -1
  70. package/dist/data/call-graph/types.cjs.map +1 -1
  71. package/dist/data/call-graph/types.d.ts +7 -2
  72. package/dist/data/call-graph/types.d.ts.map +1 -1
  73. package/dist/data/call-graph/types.js.map +1 -1
  74. package/dist/data/data-access.cjs +8 -0
  75. package/dist/data/data-access.cjs.map +1 -0
  76. package/dist/data/data-access.d.ts +5 -0
  77. package/dist/data/data-access.d.ts.map +1 -0
  78. package/dist/data/data-access.js +8 -0
  79. package/dist/data/data-access.js.map +1 -0
  80. package/dist/data/git/GitChangeList.cjs +6 -2
  81. package/dist/data/git/GitChangeList.cjs.map +1 -1
  82. package/dist/data/git/GitChangeList.d.ts +6 -1
  83. package/dist/data/git/GitChangeList.d.ts.map +1 -1
  84. package/dist/data/git/GitChangeList.js +6 -2
  85. package/dist/data/git/GitChangeList.js.map +1 -1
  86. package/dist/data/graph-columns-layout.cjs +47 -16
  87. package/dist/data/graph-columns-layout.cjs.map +1 -1
  88. package/dist/data/graph-columns-layout.d.ts +34 -0
  89. package/dist/data/graph-columns-layout.d.ts.map +1 -1
  90. package/dist/data/graph-columns-layout.js +47 -16
  91. package/dist/data/graph-columns-layout.js.map +1 -1
  92. package/dist/data/graph-diagram-model.cjs +5 -0
  93. package/dist/data/graph-diagram-model.cjs.map +1 -1
  94. package/dist/data/graph-diagram-model.d.ts +19 -0
  95. package/dist/data/graph-diagram-model.d.ts.map +1 -1
  96. package/dist/data/graph-diagram-model.js +5 -0
  97. package/dist/data/graph-diagram-model.js.map +1 -1
  98. package/dist/data/graph-group-window.cjs +60 -0
  99. package/dist/data/graph-group-window.cjs.map +1 -0
  100. package/dist/data/graph-group-window.d.ts +33 -0
  101. package/dist/data/graph-group-window.d.ts.map +1 -0
  102. package/dist/data/graph-group-window.js +60 -0
  103. package/dist/data/graph-group-window.js.map +1 -0
  104. package/dist/data/use-group-windows.cjs +50 -0
  105. package/dist/data/use-group-windows.cjs.map +1 -0
  106. package/dist/data/use-group-windows.d.ts +27 -0
  107. package/dist/data/use-group-windows.d.ts.map +1 -0
  108. package/dist/data/use-group-windows.js +50 -0
  109. package/dist/data/use-group-windows.js.map +1 -0
  110. package/dist/data/version-info.cjs +3 -3
  111. package/dist/data/version-info.js +3 -3
  112. package/dist/data.cjs +10 -0
  113. package/dist/data.cjs.map +1 -1
  114. package/dist/data.d.ts +4 -2
  115. package/dist/data.d.ts.map +1 -1
  116. package/dist/data.js +13 -3
  117. package/dist/data.js.map +1 -1
  118. package/dist/hooks/event-hub.cjs +229 -0
  119. package/dist/hooks/event-hub.cjs.map +1 -0
  120. package/dist/hooks/event-hub.d.ts +26 -0
  121. package/dist/hooks/event-hub.d.ts.map +1 -0
  122. package/dist/hooks/event-hub.js +229 -0
  123. package/dist/hooks/event-hub.js.map +1 -0
  124. package/dist/hooks.cjs +2 -0
  125. package/dist/hooks.cjs.map +1 -1
  126. package/dist/hooks.d.ts +1 -0
  127. package/dist/hooks.d.ts.map +1 -1
  128. package/dist/hooks.js +2 -0
  129. package/dist/hooks.js.map +1 -1
  130. package/dist/index.cjs +12 -0
  131. package/dist/index.cjs.map +1 -1
  132. package/dist/index.js +15 -3
  133. package/dist/index.js.map +1 -1
  134. package/dist/overlay/modalStack.cjs +13 -6
  135. package/dist/overlay/modalStack.cjs.map +1 -1
  136. package/dist/overlay/modalStack.d.ts.map +1 -1
  137. package/dist/overlay/modalStack.js +13 -6
  138. package/dist/overlay/modalStack.js.map +1 -1
  139. package/dist/styles.css +1 -1
  140. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"GitChangeList.js","sources":["../../../src/data/git/GitChangeList.tsx"],"sourcesContent":["import { useMemo, useState, type KeyboardEvent, type ReactNode } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n UiChevronDown,\n UiChevronRight,\n UiClock,\n UiCode2,\n UiDiff,\n UiFile,\n UiFileCode,\n UiFilePlus,\n UiGitCommit,\n UiLinkExternal,\n UiRemove,\n} from \"../../icons\";\nimport type { IconComponent } from \"../../icons/types\";\nimport { CodeDiff } from \"../CodeDiff\";\nimport { languageFromPath } from \"../code-highlight\";\nimport { dateKey, formatFallbackTime, formatGitCommitDateHeader } from \"./git-dates\";\n\nexport type GitDiffPayload = {\n diff: string;\n truncated?: boolean;\n binary?: boolean;\n path?: string;\n commit?: string;\n};\n\nexport type GitDiffLoader<T> = (item: T) => Promise<GitDiffPayload>;\n\nexport type GitCommitItem = {\n id?: string;\n sha: string;\n shortSha?: string;\n title: string;\n body?: string;\n authorName?: string;\n authorLogin?: string;\n authorAvatarUrl?: string;\n committedAt?: string;\n additions?: number;\n deletions?: number;\n changedFiles?: number;\n href?: string;\n};\n\nexport type GitFileChangeStatus =\n | \"added\"\n | \"modified\"\n | \"deleted\"\n | \"renamed\"\n | \"copied\"\n | string;\n\nexport type GitFileChangeItem = {\n id?: string;\n path: string;\n previousPath?: string;\n status?: GitFileChangeStatus;\n additions?: number;\n deletions?: number;\n href?: string;\n binary?: boolean;\n};\n\ntype DiffState = {\n loading: boolean;\n payload: GitDiffPayload | null;\n error: string;\n};\n\nexport type GitDiffPanelProps = {\n loading?: boolean;\n payload?: GitDiffPayload | null;\n error?: string;\n emptyLabel?: string;\n className?: string;\n maxHeightClassName?: string;\n};\n\nexport type GitCommitListProps = {\n commits: GitCommitItem[];\n loadDiff?: GitDiffLoader<GitCommitItem>;\n renderTime?: (iso: string) => ReactNode;\n groupByDate?: boolean;\n emptyLabel?: string;\n className?: string;\n diffMaxHeightClassName?: string;\n};\n\nexport type GitFileListProps = {\n files: GitFileChangeItem[];\n loadDiff?: GitDiffLoader<GitFileChangeItem>;\n emptyLabel?: string;\n className?: string;\n diffMaxHeightClassName?: string;\n};\n\ntype CommitGroup = {\n key: string;\n label: string;\n commits: GitCommitItem[];\n};\n\nconst DIFF_INITIAL_STATE: DiffState = {\n loading: false,\n payload: null,\n error: \"\",\n};\n\nfunction messageFromError(error: unknown): string {\n if (error instanceof Error) return error.message;\n if (typeof error === \"string\") return error;\n return \"Failed to load diff\";\n}\n\nfunction isActivationKey(event: KeyboardEvent<HTMLDivElement>): boolean {\n return event.key === \"Enter\" || event.key === \" \";\n}\n\nfunction groupCommits(commits: GitCommitItem[], enabled: boolean): CommitGroup[] {\n if (!enabled) return [{ key: \"all\", label: \"\", commits }];\n const groups: CommitGroup[] = [];\n const indexByKey = new Map<string, number>();\n for (const commit of commits) {\n const key = dateKey(commit.committedAt);\n const existing = indexByKey.get(key);\n if (existing != null) {\n groups[existing]?.commits.push(commit);\n continue;\n }\n indexByKey.set(key, groups.length);\n groups.push({\n key,\n label: formatGitCommitDateHeader(commit.committedAt),\n commits: [commit],\n });\n }\n return groups;\n}\n\nfunction shortSha(commit: GitCommitItem): string {\n return commit.shortSha || commit.sha.slice(0, 7);\n}\n\nfunction displayAuthor(commit: GitCommitItem): string {\n return commit.authorLogin || commit.authorName || \"\";\n}\n\nfunction statText(additions?: number, deletions?: number): ReactNode {\n const adds = additions ?? 0;\n const dels = deletions ?? 0;\n if (adds === 0 && dels === 0) return null;\n return (\n <>\n <span aria-hidden>·</span>\n {adds > 0 && <span className=\"text-emerald-600 dark:text-emerald-400\">+{adds}</span>}\n {dels > 0 && <span className=\"text-rose-600 dark:text-rose-400\">-{dels}</span>}\n </>\n );\n}\n\nfunction useLazyDiff<T>(item: T, loader: GitDiffLoader<T> | undefined): [boolean, DiffState, () => void] {\n const [expanded, setExpanded] = useState(false);\n const [state, setState] = useState<DiffState>(DIFF_INITIAL_STATE);\n\n function toggle() {\n setExpanded(open => !open);\n if (!expanded && loader && !state.payload && !state.loading && !state.error) {\n setState({ loading: true, payload: null, error: \"\" });\n loader(item)\n .then(payload => setState({ loading: false, payload, error: \"\" }))\n .catch(error => setState({ loading: false, payload: null, error: messageFromError(error) }));\n }\n }\n\n return [expanded, state, toggle];\n}\n\nfunction Chevron({ expanded }: { expanded: boolean }) {\n const Icon = expanded ? UiChevronDown : UiChevronRight;\n return <Icon className=\"h-3.5 w-3.5 text-muted-foreground\" />;\n}\n\nfunction AvatarImage({ src, alt }: { src: string; alt: string }) {\n return (\n <img\n src={src}\n alt={alt}\n className=\"h-4 w-4 rounded-full border border-border object-cover\"\n loading=\"lazy\"\n />\n );\n}\n\nexport function GitDiffPanel({\n loading,\n payload,\n error,\n emptyLabel = \"No text diff available\",\n className,\n maxHeightClassName = \"max-h-[520px]\",\n}: GitDiffPanelProps) {\n if (loading) {\n return (\n <div className={cn(\"border-t border-border bg-muted/40 px-3 py-3 text-xs text-muted-foreground\", className)}>\n Loading diff...\n </div>\n );\n }\n\n if (error) {\n return (\n <div className={cn(\"border-t border-border bg-rose-50 px-3 py-3 text-xs text-rose-700 dark:bg-rose-950/30 dark:text-rose-300\", className)}>\n {error}\n </div>\n );\n }\n\n if (payload?.binary) {\n return (\n <div className={cn(\"border-t border-border bg-muted/40 px-3 py-3 text-xs text-muted-foreground\", className)}>\n Binary file not shown.\n </div>\n );\n }\n\n if (!payload?.diff) {\n return (\n <div className={cn(\"border-t border-border bg-muted/40 px-3 py-3 text-xs text-muted-foreground\", className)}>\n {emptyLabel}\n </div>\n );\n }\n\n const language = languageFromPath(payload.path);\n return (\n <div className={cn(\"border-t border-border\", className)}>\n {payload.truncated && (\n <div className=\"border-b border-border bg-amber-100/70 px-3 py-1.5 text-xs text-amber-800 [[data-theme=dark]_&]:bg-amber-950/40 [[data-theme=dark]_&]:text-amber-200\">\n Diff truncated.\n </div>\n )}\n <CodeDiff\n bare\n unified={payload.diff}\n className={cn(\"overflow-auto px-1 py-2\", maxHeightClassName)}\n {...(language ? { language } : {})}\n />\n </div>\n );\n}\n\nfunction CommitRow({\n commit,\n loadDiff,\n renderTime,\n diffMaxHeightClassName,\n}: {\n commit: GitCommitItem;\n loadDiff?: GitDiffLoader<GitCommitItem>;\n renderTime?: (iso: string) => ReactNode;\n diffMaxHeightClassName?: string;\n}) {\n const [expanded, diffState, toggle] = useLazyDiff(commit, loadDiff);\n const author = displayAuthor(commit);\n const canExpand = Boolean(loadDiff || commit.body);\n\n function onKeyDown(event: KeyboardEvent<HTMLDivElement>) {\n if (!canExpand || !isActivationKey(event)) return;\n event.preventDefault();\n toggle();\n }\n\n return (\n <div>\n <div\n role={canExpand ? \"button\" : undefined}\n tabIndex={canExpand ? 0 : undefined}\n className={cn(\n \"flex min-h-14 items-start gap-2 px-3 py-2 text-xs transition-colors\",\n canExpand && \"cursor-pointer hover:bg-muted/70\",\n )}\n onClick={canExpand ? toggle : undefined}\n onKeyDown={canExpand ? onKeyDown : undefined}\n >\n <span className=\"mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground\">\n <UiGitCommit className=\"h-3.5 w-3.5\" />\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate font-medium text-foreground\" title={commit.title}>\n {commit.title}\n </div>\n <div className=\"mt-1 flex flex-wrap items-center gap-1.5 text-muted-foreground\">\n {commit.authorAvatarUrl && (\n <AvatarImage src={commit.authorAvatarUrl} alt={author || \"Commit author\"} />\n )}\n {author && <span>{author}</span>}\n {commit.committedAt && (\n <>\n <span aria-hidden>·</span>\n <span className=\"inline-flex items-center gap-1\">\n <UiClock className=\"h-3 w-3\" />\n {renderTime ? renderTime(commit.committedAt) : formatFallbackTime(commit.committedAt)}\n </span>\n </>\n )}\n {statText(commit.additions, commit.deletions)}\n {(commit.changedFiles ?? 0) > 0 && (\n <>\n <span aria-hidden>·</span>\n <span>{commit.changedFiles} files</span>\n </>\n )}\n </div>\n </div>\n {commit.href ? (\n <a\n href={commit.href}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"mt-0.5 inline-flex shrink-0 items-center gap-1 font-mono text-cyan-600 hover:underline\"\n onClick={event => event.stopPropagation()}\n >\n {shortSha(commit)}\n <UiLinkExternal className=\"h-3 w-3\" />\n </a>\n ) : (\n <span className=\"mt-0.5 shrink-0 font-mono text-cyan-600\">{shortSha(commit)}</span>\n )}\n {canExpand && (\n <span className=\"mt-1 shrink-0\">\n <Chevron expanded={expanded} />\n </span>\n )}\n </div>\n {expanded && (\n <div className=\"border-t border-border bg-secondary/50\">\n {commit.body && (\n <pre className=\"mx-3 my-2 whitespace-pre-wrap rounded bg-muted px-3 py-2 font-mono text-[11px] leading-5 text-muted-foreground\">\n {commit.body}\n </pre>\n )}\n {loadDiff && (\n <GitDiffPanel\n loading={diffState.loading}\n payload={diffState.payload}\n error={diffState.error}\n {...(diffMaxHeightClassName ? { maxHeightClassName: diffMaxHeightClassName } : {})}\n />\n )}\n </div>\n )}\n </div>\n );\n}\n\nexport function GitCommitList({\n commits,\n loadDiff,\n renderTime,\n groupByDate = true,\n emptyLabel = \"No commits found\",\n className,\n diffMaxHeightClassName,\n}: GitCommitListProps) {\n const groups = useMemo(() => groupCommits(commits, groupByDate), [commits, groupByDate]);\n\n if (commits.length === 0) {\n return <div className={cn(\"py-4 text-xs text-muted-foreground\", className)}>{emptyLabel}</div>;\n }\n\n return (\n <div className={cn(\"overflow-hidden rounded-md border border-border\", className)}>\n {groups.map(group => (\n <section key={group.key} aria-label={group.label || \"Commits\"}>\n {group.label && (\n <div className=\"border-b border-border bg-muted/60 px-3 py-2 text-xs font-semibold text-foreground\">\n {group.label}\n </div>\n )}\n <div className=\"divide-y divide-border\">\n {group.commits.map(commit => (\n <CommitRow\n key={commit.id || commit.sha}\n commit={commit}\n {...(loadDiff ? { loadDiff } : {})}\n {...(renderTime ? { renderTime } : {})}\n {...(diffMaxHeightClassName ? { diffMaxHeightClassName } : {})}\n />\n ))}\n </div>\n </section>\n ))}\n </div>\n );\n}\n\ntype StatusView = {\n label: string;\n icon: IconComponent;\n iconClassName: string;\n};\n\nfunction normalizedStatus(status?: string): string {\n return (status || \"modified\").toLowerCase();\n}\n\nfunction statusView(status?: string): StatusView {\n switch (normalizedStatus(status)) {\n case \"added\":\n return { label: \"Added\", icon: UiFilePlus, iconClassName: \"text-emerald-600 dark:text-emerald-400\" };\n case \"deleted\":\n return { label: \"Deleted\", icon: UiRemove, iconClassName: \"text-rose-600 dark:text-rose-400\" };\n case \"renamed\":\n return { label: \"Renamed\", icon: UiFileCode, iconClassName: \"text-sky-600 dark:text-sky-400\" };\n case \"copied\":\n return { label: \"Copied\", icon: UiCode2, iconClassName: \"text-sky-600 dark:text-sky-400\" };\n default:\n return { label: \"Modified\", icon: UiFile, iconClassName: \"text-amber-600 dark:text-amber-400\" };\n }\n}\n\nfunction splitPath(path: string): { directory: string; basename: string } {\n const index = path.lastIndexOf(\"/\");\n if (index < 0) return { directory: \"\", basename: path };\n return { directory: `${path.slice(0, index)}/`, basename: path.slice(index + 1) };\n}\n\nfunction DiffBars({ additions = 0, deletions = 0 }: { additions?: number; deletions?: number }) {\n const total = additions + deletions;\n const maxBars = 5;\n const addBars = total > 0 ? Math.round((additions / total) * maxBars) : 0;\n const delBars = total > 0 ? maxBars - addBars : 0;\n return (\n <span className=\"inline-flex gap-px\" aria-hidden>\n {Array.from({ length: addBars }).map((_, index) => (\n <span key={`a${index}`} className=\"inline-block h-2 w-1.5 rounded-sm bg-emerald-500\" />\n ))}\n {Array.from({ length: delBars }).map((_, index) => (\n <span key={`d${index}`} className=\"inline-block h-2 w-1.5 rounded-sm bg-rose-500\" />\n ))}\n {total === 0 && <span className=\"inline-block h-2 w-1.5 rounded-sm bg-muted-foreground/30\" />}\n </span>\n );\n}\n\nfunction FileRow({\n file,\n loadDiff,\n diffMaxHeightClassName,\n}: {\n file: GitFileChangeItem;\n loadDiff?: GitDiffLoader<GitFileChangeItem>;\n diffMaxHeightClassName?: string;\n}) {\n const [expanded, diffState, toggle] = useLazyDiff(file, loadDiff);\n const status = statusView(file.status);\n const StatusIcon = status.icon;\n const path = splitPath(file.path);\n const canExpand = Boolean(loadDiff);\n\n function onKeyDown(event: KeyboardEvent<HTMLDivElement>) {\n if (!canExpand || !isActivationKey(event)) return;\n event.preventDefault();\n toggle();\n }\n\n return (\n <div>\n <div\n role={canExpand ? \"button\" : undefined}\n tabIndex={canExpand ? 0 : undefined}\n className={cn(\n \"flex min-h-11 items-center gap-2 px-3 py-2 text-xs transition-colors\",\n canExpand && \"cursor-pointer hover:bg-muted/70\",\n )}\n onClick={canExpand ? toggle : undefined}\n onKeyDown={canExpand ? onKeyDown : undefined}\n >\n <StatusIcon className={cn(\"h-4 w-4 shrink-0\", status.iconClassName)} />\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate font-mono text-foreground\" title={file.path}>\n {path.directory && <span className=\"text-muted-foreground\">{path.directory}</span>}\n <span>{path.basename}</span>\n </div>\n {file.previousPath && (\n <div className=\"mt-0.5 truncate font-mono text-[11px] text-muted-foreground\">\n from {file.previousPath}\n </div>\n )}\n </div>\n <span className=\"shrink-0 text-[11px] text-muted-foreground\">{status.label}</span>\n <span className=\"shrink-0 tabular-nums text-emerald-600 dark:text-emerald-400\">\n +{file.additions ?? 0}\n </span>\n <span className=\"shrink-0 tabular-nums text-rose-600 dark:text-rose-400\">\n -{file.deletions ?? 0}\n </span>\n <DiffBars\n {...(file.additions !== undefined ? { additions: file.additions } : {})}\n {...(file.deletions !== undefined ? { deletions: file.deletions } : {})}\n />\n {file.href && (\n <a\n href={file.href}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"shrink-0 text-muted-foreground hover:text-foreground\"\n onClick={event => event.stopPropagation()}\n aria-label={`Open ${file.path}`}\n >\n <UiLinkExternal className=\"h-3.5 w-3.5\" />\n </a>\n )}\n {canExpand && (\n <span className=\"shrink-0\">\n <Chevron expanded={expanded} />\n </span>\n )}\n </div>\n {expanded && (\n <GitDiffPanel\n loading={diffState.loading}\n payload={diffState.payload}\n error={diffState.error}\n {...(diffMaxHeightClassName ? { maxHeightClassName: diffMaxHeightClassName } : {})}\n />\n )}\n </div>\n );\n}\n\nexport function GitFileList({\n files,\n loadDiff,\n emptyLabel = \"No changed files found\",\n className,\n diffMaxHeightClassName,\n}: GitFileListProps) {\n if (files.length === 0) {\n return <div className={cn(\"py-4 text-xs text-muted-foreground\", className)}>{emptyLabel}</div>;\n }\n\n return (\n <div className={cn(\"overflow-hidden rounded-md border border-border\", className)}>\n <div className=\"divide-y divide-border\">\n {files.map(file => (\n <FileRow\n key={file.id || file.path}\n file={file}\n {...(loadDiff ? { loadDiff } : {})}\n {...(diffMaxHeightClassName ? { diffMaxHeightClassName } : {})}\n />\n ))}\n </div>\n </div>\n );\n}\n\nexport function GitChangedFilesSummary({\n files,\n additions,\n deletions,\n className,\n}: {\n files: number;\n additions: number;\n deletions: number;\n className?: string;\n}) {\n return (\n <div className={cn(\"flex items-center gap-2 text-xs text-muted-foreground\", className)}>\n <span className=\"inline-flex items-center gap-1 font-medium\">\n <UiDiff className=\"h-3.5 w-3.5\" />\n {files} file{files === 1 ? \"\" : \"s\"} changed\n </span>\n <span aria-hidden>·</span>\n <span className=\"text-emerald-600 dark:text-emerald-400\">+{additions}</span>\n <span className=\"text-rose-600 dark:text-rose-400\">-{deletions}</span>\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"GitChangeList.js","sources":["../../../src/data/git/GitChangeList.tsx"],"sourcesContent":["import { useMemo, useState, type KeyboardEvent, type ReactNode } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n UiChevronDown,\n UiChevronRight,\n UiClock,\n UiCode2,\n UiDiff,\n UiFile,\n UiFileCode,\n UiFilePlus,\n UiGitCommit,\n UiLinkExternal,\n UiRemove,\n} from \"../../icons\";\nimport type { IconComponent } from \"../../icons/types\";\nimport { CodeDiff, type DiffLineTarget, type DiffLineWidget } from \"../CodeDiff\";\nimport { languageFromPath } from \"../code-highlight\";\nimport { dateKey, formatFallbackTime, formatGitCommitDateHeader } from \"./git-dates\";\n\nexport type GitDiffPayload = {\n diff: string;\n truncated?: boolean;\n binary?: boolean;\n path?: string;\n commit?: string;\n};\n\nexport type GitDiffLoader<T> = (item: T) => Promise<GitDiffPayload>;\n\nexport type GitCommitItem = {\n id?: string;\n sha: string;\n shortSha?: string;\n title: string;\n body?: string;\n authorName?: string;\n authorLogin?: string;\n authorAvatarUrl?: string;\n committedAt?: string;\n additions?: number;\n deletions?: number;\n changedFiles?: number;\n href?: string;\n};\n\nexport type GitFileChangeStatus =\n | \"added\"\n | \"modified\"\n | \"deleted\"\n | \"renamed\"\n | \"copied\"\n | string;\n\nexport type GitFileChangeItem = {\n id?: string;\n path: string;\n previousPath?: string;\n status?: GitFileChangeStatus;\n additions?: number;\n deletions?: number;\n href?: string;\n binary?: boolean;\n};\n\ntype DiffState = {\n loading: boolean;\n payload: GitDiffPayload | null;\n error: string;\n};\n\nexport type GitDiffPanelProps = {\n loading?: boolean;\n payload?: GitDiffPayload | null;\n error?: string;\n emptyLabel?: string;\n className?: string;\n maxHeightClassName?: string;\n /** Forwarded to `CodeDiff`: shows a gutter \"+\" button per numbered line. */\n onLineAction?: (target: DiffLineTarget) => void;\n /** Forwarded to `CodeDiff`: rows rendered under the matching diff line. */\n lineWidgets?: DiffLineWidget[];\n};\n\nexport type GitCommitListProps = {\n commits: GitCommitItem[];\n loadDiff?: GitDiffLoader<GitCommitItem>;\n renderTime?: (iso: string) => ReactNode;\n groupByDate?: boolean;\n emptyLabel?: string;\n className?: string;\n diffMaxHeightClassName?: string;\n};\n\nexport type GitFileListProps = {\n files: GitFileChangeItem[];\n loadDiff?: GitDiffLoader<GitFileChangeItem>;\n emptyLabel?: string;\n className?: string;\n diffMaxHeightClassName?: string;\n};\n\ntype CommitGroup = {\n key: string;\n label: string;\n commits: GitCommitItem[];\n};\n\nconst DIFF_INITIAL_STATE: DiffState = {\n loading: false,\n payload: null,\n error: \"\",\n};\n\nfunction messageFromError(error: unknown): string {\n if (error instanceof Error) return error.message;\n if (typeof error === \"string\") return error;\n return \"Failed to load diff\";\n}\n\nfunction isActivationKey(event: KeyboardEvent<HTMLDivElement>): boolean {\n return event.key === \"Enter\" || event.key === \" \";\n}\n\nfunction groupCommits(commits: GitCommitItem[], enabled: boolean): CommitGroup[] {\n if (!enabled) return [{ key: \"all\", label: \"\", commits }];\n const groups: CommitGroup[] = [];\n const indexByKey = new Map<string, number>();\n for (const commit of commits) {\n const key = dateKey(commit.committedAt);\n const existing = indexByKey.get(key);\n if (existing != null) {\n groups[existing]?.commits.push(commit);\n continue;\n }\n indexByKey.set(key, groups.length);\n groups.push({\n key,\n label: formatGitCommitDateHeader(commit.committedAt),\n commits: [commit],\n });\n }\n return groups;\n}\n\nfunction shortSha(commit: GitCommitItem): string {\n return commit.shortSha || commit.sha.slice(0, 7);\n}\n\nfunction displayAuthor(commit: GitCommitItem): string {\n return commit.authorLogin || commit.authorName || \"\";\n}\n\nfunction statText(additions?: number, deletions?: number): ReactNode {\n const adds = additions ?? 0;\n const dels = deletions ?? 0;\n if (adds === 0 && dels === 0) return null;\n return (\n <>\n <span aria-hidden>·</span>\n {adds > 0 && <span className=\"text-emerald-600 dark:text-emerald-400\">+{adds}</span>}\n {dels > 0 && <span className=\"text-rose-600 dark:text-rose-400\">-{dels}</span>}\n </>\n );\n}\n\nfunction useLazyDiff<T>(item: T, loader: GitDiffLoader<T> | undefined): [boolean, DiffState, () => void] {\n const [expanded, setExpanded] = useState(false);\n const [state, setState] = useState<DiffState>(DIFF_INITIAL_STATE);\n\n function toggle() {\n setExpanded(open => !open);\n if (!expanded && loader && !state.payload && !state.loading && !state.error) {\n setState({ loading: true, payload: null, error: \"\" });\n loader(item)\n .then(payload => setState({ loading: false, payload, error: \"\" }))\n .catch(error => setState({ loading: false, payload: null, error: messageFromError(error) }));\n }\n }\n\n return [expanded, state, toggle];\n}\n\nfunction Chevron({ expanded }: { expanded: boolean }) {\n const Icon = expanded ? UiChevronDown : UiChevronRight;\n return <Icon className=\"h-3.5 w-3.5 text-muted-foreground\" />;\n}\n\nfunction AvatarImage({ src, alt }: { src: string; alt: string }) {\n return (\n <img\n src={src}\n alt={alt}\n className=\"h-4 w-4 rounded-full border border-border object-cover\"\n loading=\"lazy\"\n />\n );\n}\n\nexport function GitDiffPanel({\n loading,\n payload,\n error,\n emptyLabel = \"No text diff available\",\n className,\n maxHeightClassName = \"max-h-[520px]\",\n onLineAction,\n lineWidgets,\n}: GitDiffPanelProps) {\n if (loading) {\n return (\n <div className={cn(\"border-t border-border bg-muted/40 px-3 py-3 text-xs text-muted-foreground\", className)}>\n Loading diff...\n </div>\n );\n }\n\n if (error) {\n return (\n <div className={cn(\"border-t border-border bg-rose-50 px-3 py-3 text-xs text-rose-700 dark:bg-rose-950/30 dark:text-rose-300\", className)}>\n {error}\n </div>\n );\n }\n\n if (payload?.binary) {\n return (\n <div className={cn(\"border-t border-border bg-muted/40 px-3 py-3 text-xs text-muted-foreground\", className)}>\n Binary file not shown.\n </div>\n );\n }\n\n if (!payload?.diff) {\n return (\n <div className={cn(\"border-t border-border bg-muted/40 px-3 py-3 text-xs text-muted-foreground\", className)}>\n {emptyLabel}\n </div>\n );\n }\n\n const language = languageFromPath(payload.path);\n return (\n <div className={cn(\"border-t border-border\", className)}>\n {payload.truncated && (\n <div className=\"border-b border-border bg-amber-100/70 px-3 py-1.5 text-xs text-amber-800 [[data-theme=dark]_&]:bg-amber-950/40 [[data-theme=dark]_&]:text-amber-200\">\n Diff truncated.\n </div>\n )}\n <CodeDiff\n bare\n unified={payload.diff}\n className={cn(\"overflow-auto px-1 py-2\", maxHeightClassName)}\n {...(language ? { language } : {})}\n {...(onLineAction ? { onLineAction } : {})}\n {...(lineWidgets ? { lineWidgets } : {})}\n />\n </div>\n );\n}\n\nfunction CommitRow({\n commit,\n loadDiff,\n renderTime,\n diffMaxHeightClassName,\n}: {\n commit: GitCommitItem;\n loadDiff?: GitDiffLoader<GitCommitItem>;\n renderTime?: (iso: string) => ReactNode;\n diffMaxHeightClassName?: string;\n}) {\n const [expanded, diffState, toggle] = useLazyDiff(commit, loadDiff);\n const author = displayAuthor(commit);\n const canExpand = Boolean(loadDiff || commit.body);\n\n function onKeyDown(event: KeyboardEvent<HTMLDivElement>) {\n if (!canExpand || !isActivationKey(event)) return;\n event.preventDefault();\n toggle();\n }\n\n return (\n <div>\n <div\n role={canExpand ? \"button\" : undefined}\n tabIndex={canExpand ? 0 : undefined}\n className={cn(\n \"flex min-h-14 items-start gap-2 px-3 py-2 text-xs transition-colors\",\n canExpand && \"cursor-pointer hover:bg-muted/70\",\n )}\n onClick={canExpand ? toggle : undefined}\n onKeyDown={canExpand ? onKeyDown : undefined}\n >\n <span className=\"mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground\">\n <UiGitCommit className=\"h-3.5 w-3.5\" />\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate font-medium text-foreground\" title={commit.title}>\n {commit.title}\n </div>\n <div className=\"mt-1 flex flex-wrap items-center gap-1.5 text-muted-foreground\">\n {commit.authorAvatarUrl && (\n <AvatarImage src={commit.authorAvatarUrl} alt={author || \"Commit author\"} />\n )}\n {author && <span>{author}</span>}\n {commit.committedAt && (\n <>\n <span aria-hidden>·</span>\n <span className=\"inline-flex items-center gap-1\">\n <UiClock className=\"h-3 w-3\" />\n {renderTime ? renderTime(commit.committedAt) : formatFallbackTime(commit.committedAt)}\n </span>\n </>\n )}\n {statText(commit.additions, commit.deletions)}\n {(commit.changedFiles ?? 0) > 0 && (\n <>\n <span aria-hidden>·</span>\n <span>{commit.changedFiles} files</span>\n </>\n )}\n </div>\n </div>\n {commit.href ? (\n <a\n href={commit.href}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"mt-0.5 inline-flex shrink-0 items-center gap-1 font-mono text-cyan-600 hover:underline\"\n onClick={event => event.stopPropagation()}\n >\n {shortSha(commit)}\n <UiLinkExternal className=\"h-3 w-3\" />\n </a>\n ) : (\n <span className=\"mt-0.5 shrink-0 font-mono text-cyan-600\">{shortSha(commit)}</span>\n )}\n {canExpand && (\n <span className=\"mt-1 shrink-0\">\n <Chevron expanded={expanded} />\n </span>\n )}\n </div>\n {expanded && (\n <div className=\"border-t border-border bg-secondary/50\">\n {commit.body && (\n <pre className=\"mx-3 my-2 whitespace-pre-wrap rounded bg-muted px-3 py-2 font-mono text-[11px] leading-5 text-muted-foreground\">\n {commit.body}\n </pre>\n )}\n {loadDiff && (\n <GitDiffPanel\n loading={diffState.loading}\n payload={diffState.payload}\n error={diffState.error}\n {...(diffMaxHeightClassName ? { maxHeightClassName: diffMaxHeightClassName } : {})}\n />\n )}\n </div>\n )}\n </div>\n );\n}\n\nexport function GitCommitList({\n commits,\n loadDiff,\n renderTime,\n groupByDate = true,\n emptyLabel = \"No commits found\",\n className,\n diffMaxHeightClassName,\n}: GitCommitListProps) {\n const groups = useMemo(() => groupCommits(commits, groupByDate), [commits, groupByDate]);\n\n if (commits.length === 0) {\n return <div className={cn(\"py-4 text-xs text-muted-foreground\", className)}>{emptyLabel}</div>;\n }\n\n return (\n <div className={cn(\"overflow-hidden rounded-md border border-border\", className)}>\n {groups.map(group => (\n <section key={group.key} aria-label={group.label || \"Commits\"}>\n {group.label && (\n <div className=\"border-b border-border bg-muted/60 px-3 py-2 text-xs font-semibold text-foreground\">\n {group.label}\n </div>\n )}\n <div className=\"divide-y divide-border\">\n {group.commits.map(commit => (\n <CommitRow\n key={commit.id || commit.sha}\n commit={commit}\n {...(loadDiff ? { loadDiff } : {})}\n {...(renderTime ? { renderTime } : {})}\n {...(diffMaxHeightClassName ? { diffMaxHeightClassName } : {})}\n />\n ))}\n </div>\n </section>\n ))}\n </div>\n );\n}\n\ntype StatusView = {\n label: string;\n icon: IconComponent;\n iconClassName: string;\n};\n\nfunction normalizedStatus(status?: string): string {\n return (status || \"modified\").toLowerCase();\n}\n\nfunction statusView(status?: string): StatusView {\n switch (normalizedStatus(status)) {\n case \"added\":\n return { label: \"Added\", icon: UiFilePlus, iconClassName: \"text-emerald-600 dark:text-emerald-400\" };\n case \"deleted\":\n return { label: \"Deleted\", icon: UiRemove, iconClassName: \"text-rose-600 dark:text-rose-400\" };\n case \"renamed\":\n return { label: \"Renamed\", icon: UiFileCode, iconClassName: \"text-sky-600 dark:text-sky-400\" };\n case \"copied\":\n return { label: \"Copied\", icon: UiCode2, iconClassName: \"text-sky-600 dark:text-sky-400\" };\n default:\n return { label: \"Modified\", icon: UiFile, iconClassName: \"text-amber-600 dark:text-amber-400\" };\n }\n}\n\nfunction splitPath(path: string): { directory: string; basename: string } {\n const index = path.lastIndexOf(\"/\");\n if (index < 0) return { directory: \"\", basename: path };\n return { directory: `${path.slice(0, index)}/`, basename: path.slice(index + 1) };\n}\n\nfunction DiffBars({ additions = 0, deletions = 0 }: { additions?: number; deletions?: number }) {\n const total = additions + deletions;\n const maxBars = 5;\n const addBars = total > 0 ? Math.round((additions / total) * maxBars) : 0;\n const delBars = total > 0 ? maxBars - addBars : 0;\n return (\n <span className=\"inline-flex gap-px\" aria-hidden>\n {Array.from({ length: addBars }).map((_, index) => (\n <span key={`a${index}`} className=\"inline-block h-2 w-1.5 rounded-sm bg-emerald-500\" />\n ))}\n {Array.from({ length: delBars }).map((_, index) => (\n <span key={`d${index}`} className=\"inline-block h-2 w-1.5 rounded-sm bg-rose-500\" />\n ))}\n {total === 0 && <span className=\"inline-block h-2 w-1.5 rounded-sm bg-muted-foreground/30\" />}\n </span>\n );\n}\n\nfunction FileRow({\n file,\n loadDiff,\n diffMaxHeightClassName,\n}: {\n file: GitFileChangeItem;\n loadDiff?: GitDiffLoader<GitFileChangeItem>;\n diffMaxHeightClassName?: string;\n}) {\n const [expanded, diffState, toggle] = useLazyDiff(file, loadDiff);\n const status = statusView(file.status);\n const StatusIcon = status.icon;\n const path = splitPath(file.path);\n const canExpand = Boolean(loadDiff);\n\n function onKeyDown(event: KeyboardEvent<HTMLDivElement>) {\n if (!canExpand || !isActivationKey(event)) return;\n event.preventDefault();\n toggle();\n }\n\n return (\n <div>\n <div\n role={canExpand ? \"button\" : undefined}\n tabIndex={canExpand ? 0 : undefined}\n className={cn(\n \"flex min-h-11 items-center gap-2 px-3 py-2 text-xs transition-colors\",\n canExpand && \"cursor-pointer hover:bg-muted/70\",\n )}\n onClick={canExpand ? toggle : undefined}\n onKeyDown={canExpand ? onKeyDown : undefined}\n >\n <StatusIcon className={cn(\"h-4 w-4 shrink-0\", status.iconClassName)} />\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate font-mono text-foreground\" title={file.path}>\n {path.directory && <span className=\"text-muted-foreground\">{path.directory}</span>}\n <span>{path.basename}</span>\n </div>\n {file.previousPath && (\n <div className=\"mt-0.5 truncate font-mono text-[11px] text-muted-foreground\">\n from {file.previousPath}\n </div>\n )}\n </div>\n <span className=\"shrink-0 text-[11px] text-muted-foreground\">{status.label}</span>\n <span className=\"shrink-0 tabular-nums text-emerald-600 dark:text-emerald-400\">\n +{file.additions ?? 0}\n </span>\n <span className=\"shrink-0 tabular-nums text-rose-600 dark:text-rose-400\">\n -{file.deletions ?? 0}\n </span>\n <DiffBars\n {...(file.additions !== undefined ? { additions: file.additions } : {})}\n {...(file.deletions !== undefined ? { deletions: file.deletions } : {})}\n />\n {file.href && (\n <a\n href={file.href}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"shrink-0 text-muted-foreground hover:text-foreground\"\n onClick={event => event.stopPropagation()}\n aria-label={`Open ${file.path}`}\n >\n <UiLinkExternal className=\"h-3.5 w-3.5\" />\n </a>\n )}\n {canExpand && (\n <span className=\"shrink-0\">\n <Chevron expanded={expanded} />\n </span>\n )}\n </div>\n {expanded && (\n <GitDiffPanel\n loading={diffState.loading}\n payload={diffState.payload}\n error={diffState.error}\n {...(diffMaxHeightClassName ? { maxHeightClassName: diffMaxHeightClassName } : {})}\n />\n )}\n </div>\n );\n}\n\nexport function GitFileList({\n files,\n loadDiff,\n emptyLabel = \"No changed files found\",\n className,\n diffMaxHeightClassName,\n}: GitFileListProps) {\n if (files.length === 0) {\n return <div className={cn(\"py-4 text-xs text-muted-foreground\", className)}>{emptyLabel}</div>;\n }\n\n return (\n <div className={cn(\"overflow-hidden rounded-md border border-border\", className)}>\n <div className=\"divide-y divide-border\">\n {files.map(file => (\n <FileRow\n key={file.id || file.path}\n file={file}\n {...(loadDiff ? { loadDiff } : {})}\n {...(diffMaxHeightClassName ? { diffMaxHeightClassName } : {})}\n />\n ))}\n </div>\n </div>\n );\n}\n\nexport function GitChangedFilesSummary({\n files,\n additions,\n deletions,\n className,\n}: {\n files: number;\n additions: number;\n deletions: number;\n className?: string;\n}) {\n return (\n <div className={cn(\"flex items-center gap-2 text-xs text-muted-foreground\", className)}>\n <span className=\"inline-flex items-center gap-1 font-medium\">\n <UiDiff className=\"h-3.5 w-3.5\" />\n {files} file{files === 1 ? \"\" : \"s\"} changed\n </span>\n <span aria-hidden>·</span>\n <span className=\"text-emerald-600 dark:text-emerald-400\">+{additions}</span>\n <span className=\"text-rose-600 dark:text-rose-400\">-{deletions}</span>\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const graphEdgeRouting = require("./graph-edge-routing.cjs");
4
+ const GROUP_MEMBER_WINDOW = 10;
5
+ const GROUP_WINDOW_FOOTER_HEIGHT = 18;
4
6
  const DEFAULT_NODE_WIDTH = 168;
5
7
  const DEFAULT_NODE_HEIGHT = 60;
6
8
  const DEFAULT_COLUMN_GAP = 96;
@@ -20,6 +22,9 @@ function mustGet(map, key, what) {
20
22
  }
21
23
  function columnMetrics(options) {
22
24
  return {
25
+ records: options.records ?? [],
26
+ collapsed: options.collapsed ?? [],
27
+ recordHeaderHeight: options.recordHeaderHeight ?? 24,
23
28
  nodeWidth: options.nodeWidth ?? DEFAULT_NODE_WIDTH,
24
29
  nodeHeight: options.nodeHeight ?? DEFAULT_NODE_HEIGHT,
25
30
  columnGap: options.columnGap ?? DEFAULT_COLUMN_GAP,
@@ -63,18 +68,34 @@ function indexNeighbours(nodes, edges) {
63
68
  }
64
69
  return neighbours;
65
70
  }
66
- function runMetrics(group, nodes, m) {
67
- const compact = group !== void 0 && nodes.every((node) => node.compact === true);
68
- const pad = group === void 0 ? 0 : compact ? m.compactGroupPadding : m.groupPadding;
69
- const gap = compact ? m.compactRowGap : m.rowGap;
70
- const heightOf = (node) => node.compact ? m.compactNodeHeight : m.nodeHeight;
71
- const height = nodes.reduce((sum, node) => sum + heightOf(node), 0) + (nodes.length - 1) * gap + 2 * pad;
72
- return { pad, gap, heightOf, height };
71
+ function collapsedRun(group, nodes, m) {
72
+ if (nodes.length !== 1) throw new Error(`columnsLayout: collapsed group "${group}" holds ${nodes.length} nodes in a column; it draws one stand-in`);
73
+ return { pad: 0, gap: 0, heightOf: () => m.recordHeaderHeight, header: 0, height: m.recordHeaderHeight, collapsed: true };
74
+ }
75
+ function runMetrics(group, nodes, m, record) {
76
+ if (group !== void 0 && m.collapsed.includes(group)) return collapsedRun(group, nodes, m);
77
+ let stack;
78
+ if (record) {
79
+ const regular = nodes.find((node) => !node.compact);
80
+ if (regular) throw new Error(`columnsLayout: record group "${group}" holds non-compact node "${regular.id}"`);
81
+ stack = { pad: 0, gap: 0, heightOf: () => m.compactNodeHeight, header: m.recordHeaderHeight };
82
+ } else {
83
+ const compact = group !== void 0 && nodes.every((node) => node.compact === true);
84
+ const heightOf = (node) => node.compact ? m.compactNodeHeight : m.nodeHeight;
85
+ stack = { pad: group === void 0 ? 0 : compact ? m.compactGroupPadding : m.groupPadding, gap: compact ? m.compactRowGap : m.rowGap, heightOf, header: 0 };
86
+ }
87
+ const chrome = stack.header + 2 * stack.pad;
88
+ if (group === void 0 || nodes.length <= GROUP_MEMBER_WINDOW) {
89
+ const stacked = nodes.reduce((sum, node) => sum + stack.heightOf(node), 0) + (nodes.length - 1) * stack.gap;
90
+ return { ...stack, height: chrome + stacked, collapsed: false };
91
+ }
92
+ const view = GROUP_MEMBER_WINDOW * Math.max(...nodes.map(stack.heightOf)) + (GROUP_MEMBER_WINDOW - 1) * stack.gap;
93
+ return { ...stack, view, height: chrome + view + GROUP_WINDOW_FOOTER_HEIGHT, collapsed: false };
73
94
  }
74
95
  function columnHeight(column, m) {
75
96
  const runs = /* @__PURE__ */ new Map();
76
97
  for (const node of column) runs.set(node.group, [...runs.get(node.group) ?? [], node]);
77
- const stacked = [...runs].reduce((sum, [group, nodes]) => sum + runMetrics(group, nodes, m).height, 0);
98
+ const stacked = [...runs].reduce((sum, [group, nodes]) => sum + runMetrics(group, nodes, m, group !== void 0 && m.records.includes(group)).height, 0);
78
99
  return stacked + (runs.size - 1) * m.rowGap;
79
100
  }
80
101
  function orderColumn(column, groupRank, weight) {
@@ -99,24 +120,33 @@ function stackColumn(runs, origin, m, placed) {
99
120
  const boxes = [];
100
121
  let cursor = origin.top;
101
122
  for (const { group, nodes } of runs) {
102
- const { pad, gap, heightOf, height } = runMetrics(group, nodes, m);
103
- let top = cursor + pad;
123
+ const record = group !== void 0 && m.records.includes(group);
124
+ const run = runMetrics(group, nodes, m, record);
125
+ const first = cursor + run.pad + run.header;
126
+ const tops = [];
127
+ let top = first;
104
128
  for (const node of nodes) {
105
- placed.set(node.id, { x: origin.x, y: top + heightOf(node) / 2 });
106
- top += heightOf(node) + gap;
129
+ tops.push(top - first);
130
+ placed.set(node.id, { x: origin.x, y: top + run.heightOf(node) / 2 });
131
+ top += run.heightOf(node) + run.gap;
107
132
  }
108
133
  if (group !== void 0) {
134
+ const memberWindow = run.view === void 0 ? void 0 : { members: nodes.map((node) => node.id), tops, rows: GROUP_MEMBER_WINDOW, top: first, height: run.view, footerHeight: GROUP_WINDOW_FOOTER_HEIGHT };
109
135
  boxes.push({
110
136
  id: `${origin.level}:${group}`,
111
137
  group,
112
138
  level: origin.level,
113
- x: origin.x - m.nodeWidth / 2 - pad,
139
+ x: origin.x - m.nodeWidth / 2 - run.pad,
114
140
  y: cursor,
115
- width: m.nodeWidth + 2 * pad,
116
- height
141
+ width: m.nodeWidth + 2 * run.pad,
142
+ height: run.height,
143
+ record,
144
+ headerHeight: run.collapsed ? m.recordHeaderHeight : run.header,
145
+ ...memberWindow ? { window: memberWindow } : {},
146
+ ...run.collapsed ? { collapsed: true } : {}
117
147
  });
118
148
  }
119
- cursor += height + m.rowGap;
149
+ cursor += run.height + m.rowGap;
120
150
  }
121
151
  return boxes;
122
152
  }
@@ -228,5 +258,6 @@ function columnsLayout(nodes, edges, options = {}) {
228
258
  groups: [...boxes.values()].flat().map((box) => ({ ...box, x: box.x + room.left })).sort((a, b) => a.level - b.level || a.y - b.y)
229
259
  };
230
260
  }
261
+ exports.GROUP_MEMBER_WINDOW = GROUP_MEMBER_WINDOW;
231
262
  exports.columnsLayout = columnsLayout;
232
263
  //# sourceMappingURL=graph-columns-layout.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"graph-columns-layout.cjs","sources":["../../src/data/graph-columns-layout.ts"],"sourcesContent":["// Level-column layout for GraphDiagram: pure math, no React. Split from\n// graph-layout.ts, which owns the shared types and `ringLayout`.\n\nimport { loopOverhang } from \"./graph-edge-routing\";\nimport type { GraphLayoutEdge, GraphLayoutNode, GraphLayoutPosition, GraphLayoutResult } from \"./graph-layout\";\n\nexport interface ColumnsLayoutNode extends GraphLayoutNode {\n /** Column of the node. May be negative: the lowest level is the first column. */\n level: number;\n /** Nodes sharing a group stay contiguous within their column and are boxed together. */\n group?: string;\n /** Drawn at `compactNodeHeight`; a group of only compact nodes stacks them at the compact gap and padding. */\n compact?: boolean;\n}\n\nexport interface ColumnsLayoutOptions {\n /** Node bounding-box width in px. Defaults to 168. */\n nodeWidth?: number;\n /** Node bounding-box height in px. Defaults to 60. */\n nodeHeight?: number;\n /** Horizontal gap between the node boxes of adjacent columns in px. Defaults to 96. */\n columnGap?: number;\n /** Vertical gap between stacked nodes, and between group rectangles, in px. Defaults to 20. */\n rowGap?: number;\n /** Space between a group's nodes and its rectangle in px. Defaults to 12. */\n groupPadding?: number;\n /** Height of a compact node in px. Defaults to 24. */\n compactNodeHeight?: number;\n /** Vertical gap between the nodes of a group of only compact nodes in px. Defaults to 4. */\n compactRowGap?: number;\n /** Space between a group of only compact nodes and its rectangle in px. Defaults to 8. */\n compactGroupPadding?: number;\n /** Space reserved around the columns before the viewport edge. Defaults to 24. */\n padding?: number;\n /**\n * Re-ordering passes over all columns after the first placement, alternating direction; a pass\n * is kept only if it leaves fewer crossings. Defaults to 4. 0 keeps the first placement.\n */\n sweeps?: number;\n}\n\nexport interface GraphLayoutGroupBox {\n /** `${level}:${group}` — a group spanning several columns gets one rectangle per column. */\n id: string;\n group: string;\n level: number;\n /** Top-left corner, in the same coordinate space as the layout's `width`/`height`. */\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport interface ColumnsLayoutResult extends GraphLayoutResult {\n groups: GraphLayoutGroupBox[];\n}\n\ntype ColumnMetrics = Required<ColumnsLayoutOptions>;\n\ninterface GroupRun {\n group: string | undefined;\n nodes: ColumnsLayoutNode[];\n}\n\n/** One ordering of every column, stacked: the node centres and group rectangles it gives. */\ninterface Arrangement {\n order: Map<number, ColumnsLayoutNode[]>;\n placed: Map<string, GraphLayoutPosition>;\n boxes: Map<number, GraphLayoutGroupBox[]>;\n}\n\n/** What stays fixed while columns are re-ordered. */\ninterface ColumnsContext {\n m: ColumnMetrics;\n levels: number[];\n levelOf: Map<string, number>;\n groupRank: Map<string | undefined, number>;\n neighbours: Map<string, string[]>;\n edges: readonly GraphLayoutEdge[];\n /** Centre x and top y of a level's column. */\n origin: (level: number) => { x: number; top: number; level: number };\n}\n\nconst DEFAULT_NODE_WIDTH = 168;\nconst DEFAULT_NODE_HEIGHT = 60;\nconst DEFAULT_COLUMN_GAP = 96;\nconst DEFAULT_ROW_GAP = 20;\nconst DEFAULT_GROUP_PADDING = 12;\nconst DEFAULT_COMPACT_NODE_HEIGHT = 24;\nconst DEFAULT_COMPACT_ROW_GAP = 4;\nconst DEFAULT_COMPACT_GROUP_PADDING = 8;\nconst DEFAULT_PADDING = 24;\nconst DEFAULT_SWEEPS = 4;\n\nfunction mustGet<K, V>(map: ReadonlyMap<K, V>, key: K, what: string): V {\n const value = map.get(key);\n if (value === undefined) {\n throw new Error(`columnsLayout: no ${what} for \"${String(key)}\"`);\n }\n return value;\n}\n\nfunction columnMetrics(options: ColumnsLayoutOptions): ColumnMetrics {\n return {\n nodeWidth: options.nodeWidth ?? DEFAULT_NODE_WIDTH,\n nodeHeight: options.nodeHeight ?? DEFAULT_NODE_HEIGHT,\n columnGap: options.columnGap ?? DEFAULT_COLUMN_GAP,\n rowGap: options.rowGap ?? DEFAULT_ROW_GAP,\n groupPadding: options.groupPadding ?? DEFAULT_GROUP_PADDING,\n compactNodeHeight: options.compactNodeHeight ?? DEFAULT_COMPACT_NODE_HEIGHT,\n compactRowGap: options.compactRowGap ?? DEFAULT_COMPACT_ROW_GAP,\n compactGroupPadding: options.compactGroupPadding ?? DEFAULT_COMPACT_GROUP_PADDING,\n padding: options.padding ?? DEFAULT_PADDING,\n sweeps: options.sweeps ?? DEFAULT_SWEEPS,\n };\n}\n\nfunction indexColumns(nodes: readonly ColumnsLayoutNode[]) {\n const columns = new Map<number, ColumnsLayoutNode[]>();\n const groupRank = new Map<string | undefined, number>();\n for (const node of nodes) {\n if (!Number.isFinite(node.level)) {\n throw new Error(`columnsLayout: node \"${node.id}\" has no finite level (got ${node.level})`);\n }\n if (!groupRank.has(node.group)) groupRank.set(node.group, groupRank.size);\n const column = columns.get(node.level);\n if (column) {\n column.push(node);\n } else {\n columns.set(node.level, [node]);\n }\n }\n return { columns, groupRank };\n}\n\nfunction indexNeighbours(\n nodes: readonly ColumnsLayoutNode[],\n edges: readonly GraphLayoutEdge[],\n): Map<string, string[]> {\n const neighbours = new Map<string, string[]>(nodes.map((node) => [node.id, []]));\n for (const edge of edges) {\n const from = neighbours.get(edge.from);\n const to = neighbours.get(edge.to);\n if (!from || !to) {\n const missing = from ? edge.to : edge.from;\n throw new Error(`columnsLayout: edge \"${edge.id}\" references unknown node \"${missing}\"`);\n }\n from.push(edge.to);\n to.push(edge.from);\n }\n return neighbours;\n}\n\n/**\n * How a run of nodes stacks: each node's height, the gap between them and the padding inside the run's\n * rectangle. A grouped run of only compact nodes takes the compact gap and padding; any other run the\n * regular ones, whatever its order, so a column's height does not depend on how it is ordered.\n */\nfunction runMetrics(group: string | undefined, nodes: readonly ColumnsLayoutNode[], m: ColumnMetrics) {\n const compact = group !== undefined && nodes.every((node) => node.compact === true);\n const pad = group === undefined ? 0 : compact ? m.compactGroupPadding : m.groupPadding;\n const gap = compact ? m.compactRowGap : m.rowGap;\n const heightOf = (node: ColumnsLayoutNode) => (node.compact ? m.compactNodeHeight : m.nodeHeight);\n const height = nodes.reduce((sum, node) => sum + heightOf(node), 0) + (nodes.length - 1) * gap + 2 * pad;\n return { pad, gap, heightOf, height };\n}\n\nfunction columnHeight(column: readonly ColumnsLayoutNode[], m: ColumnMetrics): number {\n const runs = new Map<string | undefined, ColumnsLayoutNode[]>();\n for (const node of column) runs.set(node.group, [...(runs.get(node.group) ?? []), node]);\n const stacked = [...runs].reduce((sum, [group, nodes]) => sum + runMetrics(group, nodes, m).height, 0);\n return stacked + (runs.size - 1) * m.rowGap;\n}\n\n/** Groups in order of first appearance; within a group by ascending weight, the column's order on ties. */\nfunction orderColumn(\n column: readonly ColumnsLayoutNode[],\n groupRank: ReadonlyMap<string | undefined, number>,\n weight: (node: ColumnsLayoutNode) => number,\n): GroupRun[] {\n const runs = new Map<number, { node: ColumnsLayoutNode; index: number; weight: number }[]>();\n column.forEach((node, index) => {\n const rank = mustGet(groupRank, node.group, \"group rank\");\n const entry = { node, index, weight: weight(node) };\n const run = runs.get(rank);\n if (run) {\n run.push(entry);\n } else {\n runs.set(rank, [entry]);\n }\n });\n return [...runs.entries()]\n .sort(([a], [b]) => a - b)\n .map(([, run]) => {\n // Infinity - Infinity is NaN, which falls through to the column-order tiebreak.\n const nodes = run.sort((a, b) => a.weight - b.weight || a.index - b.index).map((entry) => entry.node);\n return { group: nodes[0]?.group, nodes };\n });\n}\n\nfunction stackColumn(\n runs: readonly GroupRun[],\n origin: { x: number; top: number; level: number },\n m: ColumnMetrics,\n placed: Map<string, GraphLayoutPosition>,\n): GraphLayoutGroupBox[] {\n const boxes: GraphLayoutGroupBox[] = [];\n let cursor = origin.top;\n for (const { group, nodes } of runs) {\n const { pad, gap, heightOf, height } = runMetrics(group, nodes, m);\n let top = cursor + pad;\n for (const node of nodes) {\n placed.set(node.id, { x: origin.x, y: top + heightOf(node) / 2 });\n top += heightOf(node) + gap;\n }\n if (group !== undefined) {\n boxes.push({\n id: `${origin.level}:${group}`,\n group,\n level: origin.level,\n x: origin.x - m.nodeWidth / 2 - pad,\n y: cursor,\n width: m.nodeWidth + 2 * pad,\n height,\n });\n }\n cursor += height + m.rowGap;\n }\n return boxes;\n}\n\nfunction meanY(ids: readonly string[], placed: ReadonlyMap<string, GraphLayoutPosition>): number {\n if (ids.length === 0) return Number.POSITIVE_INFINITY;\n return ids.reduce((sum, id) => sum + mustGet(placed, id, \"position\").y, 0) / ids.length;\n}\n\n/**\n * Levels in the order they are first laid out — the one nearest 0, then\n * outward on each side — each paired with the already-placed adjacent level\n * its nodes are ordered against.\n */\nfunction placementOrder(levels: readonly number[]): [level: number, reference: number | undefined][] {\n const anchor = levels.reduce((best, level) => (Math.abs(level) < Math.abs(best) ? level : best));\n const outward = (side: readonly number[]) =>\n side.map((level, index): [number, number] => [level, side[index - 1] ?? anchor]);\n return [\n [anchor, undefined],\n ...outward(levels.filter((level) => level > anchor)),\n ...outward(levels.filter((level) => level < anchor).reverse()),\n ];\n}\n\n/** Orders one column by the mean y of its nodes' neighbours in the `references` levels, and stacks it. */\nfunction placeColumn(level: number, references: readonly number[], ctx: ColumnsContext, into: Arrangement): void {\n const runs = orderColumn(mustGet(into.order, level, \"column\"), ctx.groupRank, (node) =>\n meanY(\n mustGet(ctx.neighbours, node.id, \"neighbours\").filter((id) =>\n references.includes(mustGet(ctx.levelOf, id, \"level\")),\n ),\n into.placed,\n ),\n );\n into.order.set(\n level,\n runs.flatMap((run) => run.nodes),\n );\n into.boxes.set(level, stackColumn(runs, ctx.origin(level), ctx.m, into.placed));\n}\n\n/** Pairs of edges between adjacent columns that cross, taking each edge as a straight line. */\nfunction countCrossings(ctx: ColumnsContext, placed: ReadonlyMap<string, GraphLayoutPosition>): number {\n const spans = new Map<number, [lower: number, upper: number][]>();\n for (const edge of ctx.edges) {\n const levels = [mustGet(ctx.levelOf, edge.from, \"level\"), mustGet(ctx.levelOf, edge.to, \"level\")] as const;\n const [lower, upper] = levels[0] < levels[1] ? [edge.from, edge.to] : [edge.to, edge.from];\n const index = ctx.levels.indexOf(Math.min(...levels));\n if (levels[0] === levels[1] || ctx.levels[index + 1] !== Math.max(...levels)) continue;\n const span: [number, number] = [mustGet(placed, lower, \"position\").y, mustGet(placed, upper, \"position\").y];\n spans.set(index, [...(spans.get(index) ?? []), span]);\n }\n let crossings = 0;\n for (const gap of spans.values()) {\n gap.forEach((a, index) => {\n crossings += gap.slice(index + 1).filter((b) => (a[0] - b[0]) * (a[1] - b[1]) < 0).length;\n });\n }\n return crossings;\n}\n\n/** A copy of `from` with every column re-ordered against both its neighbours, in the order given. */\nfunction sweep(from: Arrangement, sequence: readonly number[], ctx: ColumnsContext): Arrangement {\n const next: Arrangement = { order: new Map(from.order), placed: new Map(from.placed), boxes: new Map(from.boxes) };\n for (const level of sequence) {\n const index = ctx.levels.indexOf(level);\n const adjacent = [ctx.levels[index - 1], ctx.levels[index + 1]].filter((other): other is number => other !== undefined);\n placeColumn(level, adjacent, ctx, next);\n }\n return next;\n}\n\n/**\n * Orders the columns. The first pass places the level nearest 0 and works\n * outward, ordering each column against the one just placed. Then up to\n * `sweeps` passes re-order every column against both its neighbours, inward\n * then outward, and the arrangement with the fewest crossings is kept: the\n * first pass's unless a later one strictly beats it.\n */\nfunction arrange(columns: ReadonlyMap<number, ColumnsLayoutNode[]>, ctx: ColumnsContext): Arrangement {\n let current: Arrangement = { order: new Map(columns), placed: new Map(), boxes: new Map() };\n for (const [level, reference] of placementOrder(ctx.levels)) {\n placeColumn(level, reference === undefined ? [] : [reference], ctx, current);\n }\n let best = current;\n let fewest = countCrossings(ctx, best.placed);\n for (let pass = 0; pass < ctx.m.sweeps && fewest > 0; pass++) {\n current = sweep(current, pass % 2 === 0 ? [...ctx.levels].reverse() : ctx.levels, ctx);\n const crossings = countCrossings(ctx, current.placed);\n if (crossings < fewest) {\n best = current;\n fewest = crossings;\n }\n }\n return best;\n}\n\n/**\n * Places nodes in left-to-right columns by `level` (the lowest level first),\n * stacking each column top to bottom and centring it against the tallest one.\n *\n * Within a column, nodes of one group are contiguous, groups appearing in the\n * order they first occur in `nodes`. Inside a group nodes are ordered by the\n * mean y of their neighbours in the adjacent columns, so edges between columns\n * cross less (see `arrange`); nodes with no neighbour there go last, and ties\n * keep input order.\n *\n * Returns node centres plus one rectangle per (column, group). Extra width is\n * reserved on each side for the loops `routeEdges` draws outside the first and\n * last columns.\n */\nexport function columnsLayout(\n nodes: readonly ColumnsLayoutNode[],\n edges: readonly GraphLayoutEdge[],\n options: ColumnsLayoutOptions = {},\n): ColumnsLayoutResult {\n const m = columnMetrics(options);\n const { columns, groupRank } = indexColumns(nodes);\n const neighbours = indexNeighbours(nodes, edges);\n const levels = [...columns.keys()].sort((a, b) => a - b);\n if (levels.length === 0) {\n return { width: 2 * m.padding, height: 2 * m.padding, positions: {}, groups: [] };\n }\n\n const minLevel = Math.min(...levels);\n const tallest = Math.max(...[...columns.values()].map((column) => columnHeight(column, m)));\n const inset = m.padding + (nodes.some((node) => node.group !== undefined) ? m.groupPadding : 0);\n const { placed, boxes } = arrange(columns, {\n m,\n levels,\n levelOf: new Map(nodes.map((node) => [node.id, node.level])),\n groupRank,\n neighbours,\n edges,\n origin: (level) => ({\n x: inset + m.nodeWidth / 2 + (level - minLevel) * (m.nodeWidth + m.columnGap),\n top: m.padding + (tallest - columnHeight(mustGet(columns, level, \"column\"), m)) / 2,\n level,\n }),\n });\n\n const nodeHeights = Object.fromEntries(nodes.filter((node) => node.compact).map((node) => [node.id, m.compactNodeHeight]));\n const room = loopOverhang(edges, Object.fromEntries(placed), { ...m, nodeHeights });\n const span = (Math.max(...levels) - minLevel) * (m.nodeWidth + m.columnGap) + m.nodeWidth;\n return {\n width: room.left + 2 * inset + span + room.right,\n height: tallest + 2 * m.padding,\n positions: Object.fromEntries([...placed].map(([id, { x, y }]) => [id, { x: x + room.left, y }])),\n groups: [...boxes.values()]\n .flat()\n .map((box) => ({ ...box, x: box.x + room.left }))\n .sort((a, b) => a.level - b.level || a.y - b.y),\n };\n}\n"],"names":["loopOverhang"],"mappings":"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"}
1
+ {"version":3,"file":"graph-columns-layout.cjs","sources":["../../src/data/graph-columns-layout.ts"],"sourcesContent":["// Level-column layout for GraphDiagram: pure math, no React. Split from\n// graph-layout.ts, which owns the shared types and `ringLayout`.\n\nimport { loopOverhang } from \"./graph-edge-routing\";\nimport type { GraphLayoutEdge, GraphLayoutNode, GraphLayoutPosition, GraphLayoutResult } from \"./graph-layout\";\n\nexport interface ColumnsLayoutNode extends GraphLayoutNode {\n /** Column of the node. May be negative: the lowest level is the first column. */\n level: number;\n /** Nodes sharing a group stay contiguous within their column and are boxed together. */\n group?: string;\n /** Drawn at `compactNodeHeight`; a group of only compact nodes stacks them at the compact gap and padding. */\n compact?: boolean;\n}\n\n/** The most members a group shows at once in a column; a group with more scrolls them in a window this many rows tall. */\nexport const GROUP_MEMBER_WINDOW = 10;\n/** Height of the \"rows a–b of n\" footer under a windowed group's rows. */\nconst GROUP_WINDOW_FOOTER_HEIGHT = 18;\n\nexport interface ColumnsLayoutOptions {\n /** Group ids drawn as records with a header and flush compact rows. */\n records?: readonly string[];\n /**\n * Group ids drawn as their header alone, `recordHeaderHeight` tall. Each holds one node per column it\n * is drawn in, the stand-in its edges attach to, placed at the header's middle.\n */\n collapsed?: readonly string[];\n /** Height of a record's header in px. Defaults to 24. */\n recordHeaderHeight?: number;\n /** Node bounding-box width in px. Defaults to 168. */\n nodeWidth?: number;\n /** Node bounding-box height in px. Defaults to 60. */\n nodeHeight?: number;\n /** Horizontal gap between the node boxes of adjacent columns in px. Defaults to 96. */\n columnGap?: number;\n /** Vertical gap between stacked nodes, and between group rectangles, in px. Defaults to 20. */\n rowGap?: number;\n /** Space between a group's nodes and its rectangle in px. Defaults to 12. */\n groupPadding?: number;\n /** Height of a compact node in px. Defaults to 24. */\n compactNodeHeight?: number;\n /** Vertical gap between the nodes of a group of only compact nodes in px. Defaults to 4. */\n compactRowGap?: number;\n /** Space between a group of only compact nodes and its rectangle in px. Defaults to 8. */\n compactGroupPadding?: number;\n /** Space reserved around the columns before the viewport edge. Defaults to 24. */\n padding?: number;\n /**\n * Re-ordering passes over all columns after the first placement, alternating direction; a pass\n * is kept only if it leaves fewer crossings. Defaults to 4. 0 keeps the first placement.\n */\n sweeps?: number;\n}\n\nexport interface GraphLayoutGroupBox {\n /** `${level}:${group}` — a group spanning several columns gets one rectangle per column. */\n id: string;\n group: string;\n level: number;\n /** Top-left corner, in the same coordinate space as the layout's `width`/`height`. */\n x: number;\n y: number;\n width: number;\n height: number;\n record: boolean;\n headerHeight: number;\n /** Set when the group holds more members in this column than `GROUP_MEMBER_WINDOW`: they scroll in a window. */\n window?: GroupMemberWindow;\n /** Drawn as its header alone: see `ColumnsLayoutOptions.collapsed`. */\n collapsed?: true;\n}\n\n/**\n * The rows a group with more members than `GROUP_MEMBER_WINDOW` shows at once. The layout places every\n * member as if the group were drawn whole; a member shown from row `start` is drawn `tops[start]` higher.\n */\nexport interface GroupMemberWindow {\n /** The members in this column, top to bottom. */\n members: string[];\n /** Each member's top, from the first member's top. */\n tops: number[];\n /** How many rows show at once. */\n rows: number;\n /** Top of the area members are drawn in, and its height. */\n top: number;\n height: number;\n /** Height of the footer below that area, inside the group's rectangle. */\n footerHeight: number;\n}\n\nexport interface ColumnsLayoutResult extends GraphLayoutResult {\n groups: GraphLayoutGroupBox[];\n}\n\ntype ColumnMetrics = Required<ColumnsLayoutOptions>;\n\ninterface GroupRun {\n group: string | undefined;\n nodes: ColumnsLayoutNode[];\n}\n\n/** One ordering of every column, stacked: the node centres and group rectangles it gives. */\ninterface Arrangement {\n order: Map<number, ColumnsLayoutNode[]>;\n placed: Map<string, GraphLayoutPosition>;\n boxes: Map<number, GraphLayoutGroupBox[]>;\n}\n\n/** What stays fixed while columns are re-ordered. */\ninterface ColumnsContext {\n m: ColumnMetrics;\n levels: number[];\n levelOf: Map<string, number>;\n groupRank: Map<string | undefined, number>;\n neighbours: Map<string, string[]>;\n edges: readonly GraphLayoutEdge[];\n /** Centre x and top y of a level's column. */\n origin: (level: number) => { x: number; top: number; level: number };\n}\n\nconst DEFAULT_NODE_WIDTH = 168;\nconst DEFAULT_NODE_HEIGHT = 60;\nconst DEFAULT_COLUMN_GAP = 96;\nconst DEFAULT_ROW_GAP = 20;\nconst DEFAULT_GROUP_PADDING = 12;\nconst DEFAULT_COMPACT_NODE_HEIGHT = 24;\nconst DEFAULT_COMPACT_ROW_GAP = 4;\nconst DEFAULT_COMPACT_GROUP_PADDING = 8;\nconst DEFAULT_PADDING = 24;\nconst DEFAULT_SWEEPS = 4;\n\nfunction mustGet<K, V>(map: ReadonlyMap<K, V>, key: K, what: string): V {\n const value = map.get(key);\n if (value === undefined) {\n throw new Error(`columnsLayout: no ${what} for \"${String(key)}\"`);\n }\n return value;\n}\n\nfunction columnMetrics(options: ColumnsLayoutOptions): ColumnMetrics {\n return {\n records: options.records ?? [],\n collapsed: options.collapsed ?? [],\n recordHeaderHeight: options.recordHeaderHeight ?? 24,\n nodeWidth: options.nodeWidth ?? DEFAULT_NODE_WIDTH,\n nodeHeight: options.nodeHeight ?? DEFAULT_NODE_HEIGHT,\n columnGap: options.columnGap ?? DEFAULT_COLUMN_GAP,\n rowGap: options.rowGap ?? DEFAULT_ROW_GAP,\n groupPadding: options.groupPadding ?? DEFAULT_GROUP_PADDING,\n compactNodeHeight: options.compactNodeHeight ?? DEFAULT_COMPACT_NODE_HEIGHT,\n compactRowGap: options.compactRowGap ?? DEFAULT_COMPACT_ROW_GAP,\n compactGroupPadding: options.compactGroupPadding ?? DEFAULT_COMPACT_GROUP_PADDING,\n padding: options.padding ?? DEFAULT_PADDING,\n sweeps: options.sweeps ?? DEFAULT_SWEEPS,\n };\n}\n\nfunction indexColumns(nodes: readonly ColumnsLayoutNode[]) {\n const columns = new Map<number, ColumnsLayoutNode[]>();\n const groupRank = new Map<string | undefined, number>();\n for (const node of nodes) {\n if (!Number.isFinite(node.level)) {\n throw new Error(`columnsLayout: node \"${node.id}\" has no finite level (got ${node.level})`);\n }\n if (!groupRank.has(node.group)) groupRank.set(node.group, groupRank.size);\n const column = columns.get(node.level);\n if (column) {\n column.push(node);\n } else {\n columns.set(node.level, [node]);\n }\n }\n return { columns, groupRank };\n}\n\nfunction indexNeighbours(\n nodes: readonly ColumnsLayoutNode[],\n edges: readonly GraphLayoutEdge[],\n): Map<string, string[]> {\n const neighbours = new Map<string, string[]>(nodes.map((node) => [node.id, []]));\n for (const edge of edges) {\n const from = neighbours.get(edge.from);\n const to = neighbours.get(edge.to);\n if (!from || !to) {\n const missing = from ? edge.to : edge.from;\n throw new Error(`columnsLayout: edge \"${edge.id}\" references unknown node \"${missing}\"`);\n }\n from.push(edge.to);\n to.push(edge.from);\n }\n return neighbours;\n}\n\ninterface RunMetrics {\n pad: number;\n gap: number;\n heightOf: (node: ColumnsLayoutNode) => number;\n /** Room above the first node inside the run's rectangle: a record's header. */\n header: number;\n height: number;\n /** The height members are drawn in, when there are more of them than the window holds. */\n view?: number;\n collapsed: boolean;\n}\n\nfunction collapsedRun(group: string, nodes: readonly ColumnsLayoutNode[], m: ColumnMetrics): RunMetrics {\n if (nodes.length !== 1) throw new Error(`columnsLayout: collapsed group \"${group}\" holds ${nodes.length} nodes in a column; it draws one stand-in`);\n return { pad: 0, gap: 0, heightOf: () => m.recordHeaderHeight, header: 0, height: m.recordHeaderHeight, collapsed: true };\n}\n\n/**\n * How a run of nodes stacks: each node's height, the gap between them and the padding inside the run's\n * rectangle. A grouped run of only compact nodes takes the compact gap and padding; any other run the\n * regular ones, whatever its order, so a column's height does not depend on how it is ordered. A group\n * of more than `GROUP_MEMBER_WINDOW` members is only as tall as that many of its tallest rows, over a footer.\n */\nfunction runMetrics(group: string | undefined, nodes: readonly ColumnsLayoutNode[], m: ColumnMetrics, record: boolean): RunMetrics {\n if (group !== undefined && m.collapsed.includes(group)) return collapsedRun(group, nodes, m);\n let stack: Omit<RunMetrics, \"height\" | \"collapsed\">;\n if (record) {\n const regular = nodes.find((node) => !node.compact);\n if (regular) throw new Error(`columnsLayout: record group \"${group}\" holds non-compact node \"${regular.id}\"`);\n stack = { pad: 0, gap: 0, heightOf: () => m.compactNodeHeight, header: m.recordHeaderHeight };\n } else {\n const compact = group !== undefined && nodes.every((node) => node.compact === true);\n const heightOf = (node: ColumnsLayoutNode) => (node.compact ? m.compactNodeHeight : m.nodeHeight);\n stack = { pad: group === undefined ? 0 : compact ? m.compactGroupPadding : m.groupPadding, gap: compact ? m.compactRowGap : m.rowGap, heightOf, header: 0 };\n }\n const chrome = stack.header + 2 * stack.pad;\n if (group === undefined || nodes.length <= GROUP_MEMBER_WINDOW) {\n const stacked = nodes.reduce((sum, node) => sum + stack.heightOf(node), 0) + (nodes.length - 1) * stack.gap;\n return { ...stack, height: chrome + stacked, collapsed: false };\n }\n const view = GROUP_MEMBER_WINDOW * Math.max(...nodes.map(stack.heightOf)) + (GROUP_MEMBER_WINDOW - 1) * stack.gap;\n return { ...stack, view, height: chrome + view + GROUP_WINDOW_FOOTER_HEIGHT, collapsed: false };\n}\n\nfunction columnHeight(column: readonly ColumnsLayoutNode[], m: ColumnMetrics): number {\n const runs = new Map<string | undefined, ColumnsLayoutNode[]>();\n for (const node of column) runs.set(node.group, [...(runs.get(node.group) ?? []), node]);\n const stacked = [...runs].reduce((sum, [group, nodes]) => sum + runMetrics(group, nodes, m, group !== undefined && m.records.includes(group)).height, 0);\n return stacked + (runs.size - 1) * m.rowGap;\n}\n\n/** Groups in order of first appearance; within a group by ascending weight, the column's order on ties. */\nfunction orderColumn(\n column: readonly ColumnsLayoutNode[],\n groupRank: ReadonlyMap<string | undefined, number>,\n weight: (node: ColumnsLayoutNode) => number,\n): GroupRun[] {\n const runs = new Map<number, { node: ColumnsLayoutNode; index: number; weight: number }[]>();\n column.forEach((node, index) => {\n const rank = mustGet(groupRank, node.group, \"group rank\");\n const entry = { node, index, weight: weight(node) };\n const run = runs.get(rank);\n if (run) {\n run.push(entry);\n } else {\n runs.set(rank, [entry]);\n }\n });\n return [...runs.entries()]\n .sort(([a], [b]) => a - b)\n .map(([, run]) => {\n // Infinity - Infinity is NaN, which falls through to the column-order tiebreak.\n const nodes = run.sort((a, b) => a.weight - b.weight || a.index - b.index).map((entry) => entry.node);\n return { group: nodes[0]?.group, nodes };\n });\n}\n\nfunction stackColumn(\n runs: readonly GroupRun[],\n origin: { x: number; top: number; level: number },\n m: ColumnMetrics,\n placed: Map<string, GraphLayoutPosition>,\n): GraphLayoutGroupBox[] {\n const boxes: GraphLayoutGroupBox[] = [];\n let cursor = origin.top;\n for (const { group, nodes } of runs) {\n const record = group !== undefined && m.records.includes(group);\n const run = runMetrics(group, nodes, m, record);\n const first = cursor + run.pad + run.header;\n const tops: number[] = [];\n let top = first;\n for (const node of nodes) {\n tops.push(top - first);\n placed.set(node.id, { x: origin.x, y: top + run.heightOf(node) / 2 });\n top += run.heightOf(node) + run.gap;\n }\n if (group !== undefined) {\n const memberWindow: GroupMemberWindow | undefined = run.view === undefined ? undefined\n : { members: nodes.map((node) => node.id), tops, rows: GROUP_MEMBER_WINDOW, top: first, height: run.view, footerHeight: GROUP_WINDOW_FOOTER_HEIGHT };\n boxes.push({\n id: `${origin.level}:${group}`,\n group,\n level: origin.level,\n x: origin.x - m.nodeWidth / 2 - run.pad,\n y: cursor,\n width: m.nodeWidth + 2 * run.pad,\n height: run.height,\n record,\n headerHeight: run.collapsed ? m.recordHeaderHeight : run.header,\n ...(memberWindow ? { window: memberWindow } : {}),\n ...(run.collapsed ? { collapsed: true as const } : {}),\n });\n }\n cursor += run.height + m.rowGap;\n }\n return boxes;\n}\n\nfunction meanY(ids: readonly string[], placed: ReadonlyMap<string, GraphLayoutPosition>): number {\n if (ids.length === 0) return Number.POSITIVE_INFINITY;\n return ids.reduce((sum, id) => sum + mustGet(placed, id, \"position\").y, 0) / ids.length;\n}\n\n/**\n * Levels in the order they are first laid out — the one nearest 0, then\n * outward on each side — each paired with the already-placed adjacent level\n * its nodes are ordered against.\n */\nfunction placementOrder(levels: readonly number[]): [level: number, reference: number | undefined][] {\n const anchor = levels.reduce((best, level) => (Math.abs(level) < Math.abs(best) ? level : best));\n const outward = (side: readonly number[]) =>\n side.map((level, index): [number, number] => [level, side[index - 1] ?? anchor]);\n return [\n [anchor, undefined],\n ...outward(levels.filter((level) => level > anchor)),\n ...outward(levels.filter((level) => level < anchor).reverse()),\n ];\n}\n\n/** Orders one column by the mean y of its nodes' neighbours in the `references` levels, and stacks it. */\nfunction placeColumn(level: number, references: readonly number[], ctx: ColumnsContext, into: Arrangement): void {\n const runs = orderColumn(mustGet(into.order, level, \"column\"), ctx.groupRank, (node) =>\n meanY(\n mustGet(ctx.neighbours, node.id, \"neighbours\").filter((id) =>\n references.includes(mustGet(ctx.levelOf, id, \"level\")),\n ),\n into.placed,\n ),\n );\n into.order.set(\n level,\n runs.flatMap((run) => run.nodes),\n );\n into.boxes.set(level, stackColumn(runs, ctx.origin(level), ctx.m, into.placed));\n}\n\n/** Pairs of edges between adjacent columns that cross, taking each edge as a straight line. */\nfunction countCrossings(ctx: ColumnsContext, placed: ReadonlyMap<string, GraphLayoutPosition>): number {\n const spans = new Map<number, [lower: number, upper: number][]>();\n for (const edge of ctx.edges) {\n const levels = [mustGet(ctx.levelOf, edge.from, \"level\"), mustGet(ctx.levelOf, edge.to, \"level\")] as const;\n const [lower, upper] = levels[0] < levels[1] ? [edge.from, edge.to] : [edge.to, edge.from];\n const index = ctx.levels.indexOf(Math.min(...levels));\n if (levels[0] === levels[1] || ctx.levels[index + 1] !== Math.max(...levels)) continue;\n const span: [number, number] = [mustGet(placed, lower, \"position\").y, mustGet(placed, upper, \"position\").y];\n spans.set(index, [...(spans.get(index) ?? []), span]);\n }\n let crossings = 0;\n for (const gap of spans.values()) {\n gap.forEach((a, index) => {\n crossings += gap.slice(index + 1).filter((b) => (a[0] - b[0]) * (a[1] - b[1]) < 0).length;\n });\n }\n return crossings;\n}\n\n/** A copy of `from` with every column re-ordered against both its neighbours, in the order given. */\nfunction sweep(from: Arrangement, sequence: readonly number[], ctx: ColumnsContext): Arrangement {\n const next: Arrangement = { order: new Map(from.order), placed: new Map(from.placed), boxes: new Map(from.boxes) };\n for (const level of sequence) {\n const index = ctx.levels.indexOf(level);\n const adjacent = [ctx.levels[index - 1], ctx.levels[index + 1]].filter((other): other is number => other !== undefined);\n placeColumn(level, adjacent, ctx, next);\n }\n return next;\n}\n\n/**\n * Orders the columns. The first pass places the level nearest 0 and works\n * outward, ordering each column against the one just placed. Then up to\n * `sweeps` passes re-order every column against both its neighbours, inward\n * then outward, and the arrangement with the fewest crossings is kept: the\n * first pass's unless a later one strictly beats it.\n */\nfunction arrange(columns: ReadonlyMap<number, ColumnsLayoutNode[]>, ctx: ColumnsContext): Arrangement {\n let current: Arrangement = { order: new Map(columns), placed: new Map(), boxes: new Map() };\n for (const [level, reference] of placementOrder(ctx.levels)) {\n placeColumn(level, reference === undefined ? [] : [reference], ctx, current);\n }\n let best = current;\n let fewest = countCrossings(ctx, best.placed);\n for (let pass = 0; pass < ctx.m.sweeps && fewest > 0; pass++) {\n current = sweep(current, pass % 2 === 0 ? [...ctx.levels].reverse() : ctx.levels, ctx);\n const crossings = countCrossings(ctx, current.placed);\n if (crossings < fewest) {\n best = current;\n fewest = crossings;\n }\n }\n return best;\n}\n\n/**\n * Places nodes in left-to-right columns by `level` (the lowest level first),\n * stacking each column top to bottom and centring it against the tallest one.\n *\n * Within a column, nodes of one group are contiguous, groups appearing in the\n * order they first occur in `nodes`. Inside a group nodes are ordered by the\n * mean y of their neighbours in the adjacent columns, so edges between columns\n * cross less (see `arrange`); nodes with no neighbour there go last, and ties\n * keep input order.\n *\n * Returns node centres plus one rectangle per (column, group). Extra width is\n * reserved on each side for the loops `routeEdges` draws outside the first and\n * last columns.\n */\nexport function columnsLayout(\n nodes: readonly ColumnsLayoutNode[],\n edges: readonly GraphLayoutEdge[],\n options: ColumnsLayoutOptions = {},\n): ColumnsLayoutResult {\n const m = columnMetrics(options);\n const { columns, groupRank } = indexColumns(nodes);\n const neighbours = indexNeighbours(nodes, edges);\n const levels = [...columns.keys()].sort((a, b) => a - b);\n if (levels.length === 0) {\n return { width: 2 * m.padding, height: 2 * m.padding, positions: {}, groups: [] };\n }\n\n const minLevel = Math.min(...levels);\n const tallest = Math.max(...[...columns.values()].map((column) => columnHeight(column, m)));\n const inset = m.padding + (nodes.some((node) => node.group !== undefined) ? m.groupPadding : 0);\n const { placed, boxes } = arrange(columns, {\n m,\n levels,\n levelOf: new Map(nodes.map((node) => [node.id, node.level])),\n groupRank,\n neighbours,\n edges,\n origin: (level) => ({\n x: inset + m.nodeWidth / 2 + (level - minLevel) * (m.nodeWidth + m.columnGap),\n top: m.padding + (tallest - columnHeight(mustGet(columns, level, \"column\"), m)) / 2,\n level,\n }),\n });\n\n const nodeHeights = Object.fromEntries(nodes.filter((node) => node.compact).map((node) => [node.id, m.compactNodeHeight]));\n const room = loopOverhang(edges, Object.fromEntries(placed), { ...m, nodeHeights });\n const span = (Math.max(...levels) - minLevel) * (m.nodeWidth + m.columnGap) + m.nodeWidth;\n return {\n width: room.left + 2 * inset + span + room.right,\n height: tallest + 2 * m.padding,\n positions: Object.fromEntries([...placed].map(([id, { x, y }]) => [id, { x: x + room.left, y }])),\n groups: [...boxes.values()]\n .flat()\n .map((box) => ({ ...box, x: box.x + room.left }))\n .sort((a, b) => a.level - b.level || a.y - b.y),\n };\n}\n"],"names":["loopOverhang"],"mappings":"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"}
@@ -7,7 +7,18 @@ export interface ColumnsLayoutNode extends GraphLayoutNode {
7
7
  /** Drawn at `compactNodeHeight`; a group of only compact nodes stacks them at the compact gap and padding. */
8
8
  compact?: boolean;
9
9
  }
10
+ /** The most members a group shows at once in a column; a group with more scrolls them in a window this many rows tall. */
11
+ export declare const GROUP_MEMBER_WINDOW = 10;
10
12
  export interface ColumnsLayoutOptions {
13
+ /** Group ids drawn as records with a header and flush compact rows. */
14
+ records?: readonly string[];
15
+ /**
16
+ * Group ids drawn as their header alone, `recordHeaderHeight` tall. Each holds one node per column it
17
+ * is drawn in, the stand-in its edges attach to, placed at the header's middle.
18
+ */
19
+ collapsed?: readonly string[];
20
+ /** Height of a record's header in px. Defaults to 24. */
21
+ recordHeaderHeight?: number;
11
22
  /** Node bounding-box width in px. Defaults to 168. */
12
23
  nodeWidth?: number;
13
24
  /** Node bounding-box height in px. Defaults to 60. */
@@ -42,6 +53,29 @@ export interface GraphLayoutGroupBox {
42
53
  y: number;
43
54
  width: number;
44
55
  height: number;
56
+ record: boolean;
57
+ headerHeight: number;
58
+ /** Set when the group holds more members in this column than `GROUP_MEMBER_WINDOW`: they scroll in a window. */
59
+ window?: GroupMemberWindow;
60
+ /** Drawn as its header alone: see `ColumnsLayoutOptions.collapsed`. */
61
+ collapsed?: true;
62
+ }
63
+ /**
64
+ * The rows a group with more members than `GROUP_MEMBER_WINDOW` shows at once. The layout places every
65
+ * member as if the group were drawn whole; a member shown from row `start` is drawn `tops[start]` higher.
66
+ */
67
+ export interface GroupMemberWindow {
68
+ /** The members in this column, top to bottom. */
69
+ members: string[];
70
+ /** Each member's top, from the first member's top. */
71
+ tops: number[];
72
+ /** How many rows show at once. */
73
+ rows: number;
74
+ /** Top of the area members are drawn in, and its height. */
75
+ top: number;
76
+ height: number;
77
+ /** Height of the footer below that area, inside the group's rectangle. */
78
+ footerHeight: number;
45
79
  }
46
80
  export interface ColumnsLayoutResult extends GraphLayoutResult {
47
81
  groups: GraphLayoutGroupBox[];
@@ -1 +1 @@
1
- {"version":3,"file":"graph-columns-layout.d.ts","sourceRoot":"","sources":["../../src/data/graph-columns-layout.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAuB,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAE/G,MAAM,WAAW,iBAAkB,SAAQ,eAAe;IACxD,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAC;IACd,wFAAwF;IACxF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8GAA8G;IAC9G,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uFAAuF;IACvF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+FAA+F;IAC/F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sDAAsD;IACtD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,4FAA4F;IAC5F,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0FAA0F;IAC1F,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,4FAA4F;IAC5F,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,sFAAsF;IACtF,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,mBAAoB,SAAQ,iBAAiB;IAC5D,MAAM,EAAE,mBAAmB,EAAE,CAAC;CAC/B;AA8QD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,SAAS,iBAAiB,EAAE,EACnC,KAAK,EAAE,SAAS,eAAe,EAAE,EACjC,OAAO,GAAE,oBAAyB,GACjC,mBAAmB,CAsCrB"}
1
+ {"version":3,"file":"graph-columns-layout.d.ts","sourceRoot":"","sources":["../../src/data/graph-columns-layout.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAuB,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAE/G,MAAM,WAAW,iBAAkB,SAAQ,eAAe;IACxD,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAC;IACd,wFAAwF;IACxF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8GAA8G;IAC9G,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,0HAA0H;AAC1H,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAItC,MAAM,WAAW,oBAAoB;IACnC,uEAAuE;IACvE,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B;;;OAGG;IACH,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,yDAAyD;IACzD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uFAAuF;IACvF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+FAA+F;IAC/F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sDAAsD;IACtD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,4FAA4F;IAC5F,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0FAA0F;IAC1F,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,4FAA4F;IAC5F,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,sFAAsF;IACtF,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,gHAAgH;IAChH,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,uEAAuE;IACvE,SAAS,CAAC,EAAE,IAAI,CAAC;CAClB;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,iDAAiD;IACjD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,sDAAsD;IACtD,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,mBAAoB,SAAQ,iBAAiB;IAC5D,MAAM,EAAE,mBAAmB,EAAE,CAAC;CAC/B;AAyTD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,SAAS,iBAAiB,EAAE,EACnC,KAAK,EAAE,SAAS,eAAe,EAAE,EACjC,OAAO,GAAE,oBAAyB,GACjC,mBAAmB,CAsCrB"}
@@ -1,4 +1,6 @@
1
1
  import { loopOverhang } from "./graph-edge-routing.js";
2
+ const GROUP_MEMBER_WINDOW = 10;
3
+ const GROUP_WINDOW_FOOTER_HEIGHT = 18;
2
4
  const DEFAULT_NODE_WIDTH = 168;
3
5
  const DEFAULT_NODE_HEIGHT = 60;
4
6
  const DEFAULT_COLUMN_GAP = 96;
@@ -18,6 +20,9 @@ function mustGet(map, key, what) {
18
20
  }
19
21
  function columnMetrics(options) {
20
22
  return {
23
+ records: options.records ?? [],
24
+ collapsed: options.collapsed ?? [],
25
+ recordHeaderHeight: options.recordHeaderHeight ?? 24,
21
26
  nodeWidth: options.nodeWidth ?? DEFAULT_NODE_WIDTH,
22
27
  nodeHeight: options.nodeHeight ?? DEFAULT_NODE_HEIGHT,
23
28
  columnGap: options.columnGap ?? DEFAULT_COLUMN_GAP,
@@ -61,18 +66,34 @@ function indexNeighbours(nodes, edges) {
61
66
  }
62
67
  return neighbours;
63
68
  }
64
- function runMetrics(group, nodes, m) {
65
- const compact = group !== void 0 && nodes.every((node) => node.compact === true);
66
- const pad = group === void 0 ? 0 : compact ? m.compactGroupPadding : m.groupPadding;
67
- const gap = compact ? m.compactRowGap : m.rowGap;
68
- const heightOf = (node) => node.compact ? m.compactNodeHeight : m.nodeHeight;
69
- const height = nodes.reduce((sum, node) => sum + heightOf(node), 0) + (nodes.length - 1) * gap + 2 * pad;
70
- return { pad, gap, heightOf, height };
69
+ function collapsedRun(group, nodes, m) {
70
+ if (nodes.length !== 1) throw new Error(`columnsLayout: collapsed group "${group}" holds ${nodes.length} nodes in a column; it draws one stand-in`);
71
+ return { pad: 0, gap: 0, heightOf: () => m.recordHeaderHeight, header: 0, height: m.recordHeaderHeight, collapsed: true };
72
+ }
73
+ function runMetrics(group, nodes, m, record) {
74
+ if (group !== void 0 && m.collapsed.includes(group)) return collapsedRun(group, nodes, m);
75
+ let stack;
76
+ if (record) {
77
+ const regular = nodes.find((node) => !node.compact);
78
+ if (regular) throw new Error(`columnsLayout: record group "${group}" holds non-compact node "${regular.id}"`);
79
+ stack = { pad: 0, gap: 0, heightOf: () => m.compactNodeHeight, header: m.recordHeaderHeight };
80
+ } else {
81
+ const compact = group !== void 0 && nodes.every((node) => node.compact === true);
82
+ const heightOf = (node) => node.compact ? m.compactNodeHeight : m.nodeHeight;
83
+ stack = { pad: group === void 0 ? 0 : compact ? m.compactGroupPadding : m.groupPadding, gap: compact ? m.compactRowGap : m.rowGap, heightOf, header: 0 };
84
+ }
85
+ const chrome = stack.header + 2 * stack.pad;
86
+ if (group === void 0 || nodes.length <= GROUP_MEMBER_WINDOW) {
87
+ const stacked = nodes.reduce((sum, node) => sum + stack.heightOf(node), 0) + (nodes.length - 1) * stack.gap;
88
+ return { ...stack, height: chrome + stacked, collapsed: false };
89
+ }
90
+ const view = GROUP_MEMBER_WINDOW * Math.max(...nodes.map(stack.heightOf)) + (GROUP_MEMBER_WINDOW - 1) * stack.gap;
91
+ return { ...stack, view, height: chrome + view + GROUP_WINDOW_FOOTER_HEIGHT, collapsed: false };
71
92
  }
72
93
  function columnHeight(column, m) {
73
94
  const runs = /* @__PURE__ */ new Map();
74
95
  for (const node of column) runs.set(node.group, [...runs.get(node.group) ?? [], node]);
75
- const stacked = [...runs].reduce((sum, [group, nodes]) => sum + runMetrics(group, nodes, m).height, 0);
96
+ const stacked = [...runs].reduce((sum, [group, nodes]) => sum + runMetrics(group, nodes, m, group !== void 0 && m.records.includes(group)).height, 0);
76
97
  return stacked + (runs.size - 1) * m.rowGap;
77
98
  }
78
99
  function orderColumn(column, groupRank, weight) {
@@ -97,24 +118,33 @@ function stackColumn(runs, origin, m, placed) {
97
118
  const boxes = [];
98
119
  let cursor = origin.top;
99
120
  for (const { group, nodes } of runs) {
100
- const { pad, gap, heightOf, height } = runMetrics(group, nodes, m);
101
- let top = cursor + pad;
121
+ const record = group !== void 0 && m.records.includes(group);
122
+ const run = runMetrics(group, nodes, m, record);
123
+ const first = cursor + run.pad + run.header;
124
+ const tops = [];
125
+ let top = first;
102
126
  for (const node of nodes) {
103
- placed.set(node.id, { x: origin.x, y: top + heightOf(node) / 2 });
104
- top += heightOf(node) + gap;
127
+ tops.push(top - first);
128
+ placed.set(node.id, { x: origin.x, y: top + run.heightOf(node) / 2 });
129
+ top += run.heightOf(node) + run.gap;
105
130
  }
106
131
  if (group !== void 0) {
132
+ const memberWindow = run.view === void 0 ? void 0 : { members: nodes.map((node) => node.id), tops, rows: GROUP_MEMBER_WINDOW, top: first, height: run.view, footerHeight: GROUP_WINDOW_FOOTER_HEIGHT };
107
133
  boxes.push({
108
134
  id: `${origin.level}:${group}`,
109
135
  group,
110
136
  level: origin.level,
111
- x: origin.x - m.nodeWidth / 2 - pad,
137
+ x: origin.x - m.nodeWidth / 2 - run.pad,
112
138
  y: cursor,
113
- width: m.nodeWidth + 2 * pad,
114
- height
139
+ width: m.nodeWidth + 2 * run.pad,
140
+ height: run.height,
141
+ record,
142
+ headerHeight: run.collapsed ? m.recordHeaderHeight : run.header,
143
+ ...memberWindow ? { window: memberWindow } : {},
144
+ ...run.collapsed ? { collapsed: true } : {}
115
145
  });
116
146
  }
117
- cursor += height + m.rowGap;
147
+ cursor += run.height + m.rowGap;
118
148
  }
119
149
  return boxes;
120
150
  }
@@ -227,6 +257,7 @@ function columnsLayout(nodes, edges, options = {}) {
227
257
  };
228
258
  }
229
259
  export {
260
+ GROUP_MEMBER_WINDOW,
230
261
  columnsLayout
231
262
  };
232
263
  //# sourceMappingURL=graph-columns-layout.js.map