diff-changes.tsx 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. import { createMemo, For, Match, Show, Switch } from "solid-js"
  2. export function DiffChanges(props: {
  3. class?: string
  4. changes: { additions: number; deletions: number } | { additions: number; deletions: number }[]
  5. variant?: "default" | "bars"
  6. }) {
  7. const variant = () => props.variant ?? "default"
  8. const additions = createMemo(() =>
  9. Array.isArray(props.changes)
  10. ? props.changes.reduce((acc, diff) => acc + (diff.additions ?? 0), 0)
  11. : props.changes.additions,
  12. )
  13. const deletions = createMemo(() =>
  14. Array.isArray(props.changes)
  15. ? props.changes.reduce((acc, diff) => acc + (diff.deletions ?? 0), 0)
  16. : props.changes.deletions,
  17. )
  18. const total = createMemo(() => (additions() ?? 0) + (deletions() ?? 0))
  19. const blockCounts = createMemo(() => {
  20. const TOTAL_BLOCKS = 5
  21. const adds = additions() ?? 0
  22. const dels = deletions() ?? 0
  23. if (adds === 0 && dels === 0) {
  24. return { added: 0, deleted: 0, neutral: TOTAL_BLOCKS }
  25. }
  26. const total = adds + dels
  27. if (total < 5) {
  28. const added = adds > 0 ? 1 : 0
  29. const deleted = dels > 0 ? 1 : 0
  30. const neutral = TOTAL_BLOCKS - added - deleted
  31. return { added, deleted, neutral }
  32. }
  33. const ratio = adds > dels ? adds / dels : dels / adds
  34. let BLOCKS_FOR_COLORS = TOTAL_BLOCKS
  35. if (total < 20) {
  36. BLOCKS_FOR_COLORS = TOTAL_BLOCKS - 1
  37. } else if (ratio < 4) {
  38. BLOCKS_FOR_COLORS = TOTAL_BLOCKS - 1
  39. }
  40. const percentAdded = adds / total
  41. const percentDeleted = dels / total
  42. const added_raw = percentAdded * BLOCKS_FOR_COLORS
  43. const deleted_raw = percentDeleted * BLOCKS_FOR_COLORS
  44. let added = adds > 0 ? Math.max(1, Math.round(added_raw)) : 0
  45. let deleted = dels > 0 ? Math.max(1, Math.round(deleted_raw)) : 0
  46. // Cap bars based on actual change magnitude
  47. if (adds > 0 && adds <= 5) added = Math.min(added, 1)
  48. if (adds > 5 && adds <= 10) added = Math.min(added, 2)
  49. if (dels > 0 && dels <= 5) deleted = Math.min(deleted, 1)
  50. if (dels > 5 && dels <= 10) deleted = Math.min(deleted, 2)
  51. let total_allocated = added + deleted
  52. if (total_allocated > BLOCKS_FOR_COLORS) {
  53. if (added_raw > deleted_raw) {
  54. added = BLOCKS_FOR_COLORS - deleted
  55. } else {
  56. deleted = BLOCKS_FOR_COLORS - added
  57. }
  58. total_allocated = added + deleted
  59. }
  60. const neutral = Math.max(0, TOTAL_BLOCKS - total_allocated)
  61. return { added, deleted, neutral }
  62. })
  63. const ADD_COLOR = "var(--icon-diff-add-base)"
  64. const DELETE_COLOR = "var(--icon-diff-delete-base)"
  65. const NEUTRAL_COLOR = "var(--icon-weak-base)"
  66. const visibleBlocks = createMemo(() => {
  67. const counts = blockCounts()
  68. const blocks = [
  69. ...Array(counts.added).fill(ADD_COLOR),
  70. ...Array(counts.deleted).fill(DELETE_COLOR),
  71. ...Array(counts.neutral).fill(NEUTRAL_COLOR),
  72. ]
  73. return blocks.slice(0, 5)
  74. })
  75. return (
  76. <Show when={variant() === "default" ? total() > 0 : true}>
  77. <div data-component="diff-changes" data-variant={variant()} classList={{ [props.class ?? ""]: true }}>
  78. <Switch>
  79. <Match when={variant() === "bars"}>
  80. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 12" fill="none">
  81. <g>
  82. <For each={visibleBlocks()}>
  83. {(color, i) => <rect x={i() * 4} width="2" height="12" rx="1" fill={color} />}
  84. </For>
  85. </g>
  86. </svg>
  87. </Match>
  88. <Match when={variant() === "default"}>
  89. <span data-slot="diff-changes-additions">{`+${additions()}`}</span>
  90. <span data-slot="diff-changes-deletions">{`-${deletions()}`}</span>
  91. </Match>
  92. </Switch>
  93. </div>
  94. </Show>
  95. )
  96. }