diffity 0.1.0

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 (174) hide show
  1. package/.claude/settings.local.json +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +71 -0
  4. package/development.md +156 -0
  5. package/package.json +32 -0
  6. package/packages/cli/build.js +38 -0
  7. package/packages/cli/package.json +51 -0
  8. package/packages/cli/src/agent.ts +187 -0
  9. package/packages/cli/src/db.ts +58 -0
  10. package/packages/cli/src/index.ts +196 -0
  11. package/packages/cli/src/review-routes.ts +150 -0
  12. package/packages/cli/src/server.ts +370 -0
  13. package/packages/cli/src/session.ts +48 -0
  14. package/packages/cli/src/threads.ts +238 -0
  15. package/packages/cli/tsconfig.json +13 -0
  16. package/packages/git/package.json +24 -0
  17. package/packages/git/src/commits.ts +28 -0
  18. package/packages/git/src/diff.ts +97 -0
  19. package/packages/git/src/exec.ts +35 -0
  20. package/packages/git/src/index.ts +5 -0
  21. package/packages/git/src/repo.ts +63 -0
  22. package/packages/git/src/status.ts +9 -0
  23. package/packages/git/src/types.ts +12 -0
  24. package/packages/git/tsconfig.json +9 -0
  25. package/packages/parser/package.json +26 -0
  26. package/packages/parser/src/index.ts +12 -0
  27. package/packages/parser/src/parse.ts +299 -0
  28. package/packages/parser/src/types.ts +52 -0
  29. package/packages/parser/src/word-diff.ts +155 -0
  30. package/packages/parser/tests/fixtures/binary-deleted.diff +4 -0
  31. package/packages/parser/tests/fixtures/binary-file.diff +4 -0
  32. package/packages/parser/tests/fixtures/binary-modified.diff +3 -0
  33. package/packages/parser/tests/fixtures/copied-file.diff +12 -0
  34. package/packages/parser/tests/fixtures/deleted-file.diff +9 -0
  35. package/packages/parser/tests/fixtures/empty.diff +0 -0
  36. package/packages/parser/tests/fixtures/hunk-with-context.diff +12 -0
  37. package/packages/parser/tests/fixtures/mode-change-with-content.diff +10 -0
  38. package/packages/parser/tests/fixtures/mode-change.diff +3 -0
  39. package/packages/parser/tests/fixtures/multi-file.diff +22 -0
  40. package/packages/parser/tests/fixtures/new-file.diff +9 -0
  41. package/packages/parser/tests/fixtures/no-newline.diff +10 -0
  42. package/packages/parser/tests/fixtures/renamed-file.diff +12 -0
  43. package/packages/parser/tests/fixtures/single-file-additions.diff +11 -0
  44. package/packages/parser/tests/fixtures/single-file-deletions.diff +11 -0
  45. package/packages/parser/tests/fixtures/single-file-mixed.diff +15 -0
  46. package/packages/parser/tests/fixtures/single-file-multi-hunk.diff +22 -0
  47. package/packages/parser/tests/fixtures/spaces-in-path.diff +9 -0
  48. package/packages/parser/tests/fixtures/submodule.diff +7 -0
  49. package/packages/parser/tests/fixtures/unicode-content.diff +11 -0
  50. package/packages/parser/tests/parse.test.ts +312 -0
  51. package/packages/parser/tests/word-diff-integration.test.ts +52 -0
  52. package/packages/parser/tests/word-diff.test.ts +121 -0
  53. package/packages/parser/tsconfig.json +10 -0
  54. package/packages/skills/diffity-resolve/SKILL.md +55 -0
  55. package/packages/skills/diffity-review/SKILL.md +74 -0
  56. package/packages/skills/diffity-start/SKILL.md +25 -0
  57. package/packages/ui/index.html +13 -0
  58. package/packages/ui/package.json +35 -0
  59. package/packages/ui/public/brand.svg +12 -0
  60. package/packages/ui/public/favicon.svg +15 -0
  61. package/packages/ui/src/app.tsx +14 -0
  62. package/packages/ui/src/components/comment-bubble.tsx +78 -0
  63. package/packages/ui/src/components/comment-form-row.tsx +58 -0
  64. package/packages/ui/src/components/comment-form.tsx +78 -0
  65. package/packages/ui/src/components/comment-line-number.tsx +60 -0
  66. package/packages/ui/src/components/comment-thread.tsx +209 -0
  67. package/packages/ui/src/components/commit-list.tsx +100 -0
  68. package/packages/ui/src/components/dashboard.tsx +84 -0
  69. package/packages/ui/src/components/diff-line.tsx +90 -0
  70. package/packages/ui/src/components/diff-page.tsx +332 -0
  71. package/packages/ui/src/components/diff-stats.tsx +20 -0
  72. package/packages/ui/src/components/diff-view.tsx +278 -0
  73. package/packages/ui/src/components/expand-row.tsx +45 -0
  74. package/packages/ui/src/components/file-block.tsx +536 -0
  75. package/packages/ui/src/components/file-tree-item.tsx +84 -0
  76. package/packages/ui/src/components/file-tree.tsx +72 -0
  77. package/packages/ui/src/components/general-comments.tsx +174 -0
  78. package/packages/ui/src/components/hunk-block-split.tsx +357 -0
  79. package/packages/ui/src/components/hunk-block.tsx +161 -0
  80. package/packages/ui/src/components/hunk-header.tsx +144 -0
  81. package/packages/ui/src/components/hunk-with-gap.tsx +113 -0
  82. package/packages/ui/src/components/icons/arrow-down-icon.tsx +7 -0
  83. package/packages/ui/src/components/icons/arrow-up-icon.tsx +7 -0
  84. package/packages/ui/src/components/icons/check-circle-icon.tsx +8 -0
  85. package/packages/ui/src/components/icons/check-icon.tsx +9 -0
  86. package/packages/ui/src/components/icons/chevron-down-icon.tsx +11 -0
  87. package/packages/ui/src/components/icons/chevron-icon.tsx +20 -0
  88. package/packages/ui/src/components/icons/chevron-up-down-icon.tsx +7 -0
  89. package/packages/ui/src/components/icons/chevron-up-icon.tsx +11 -0
  90. package/packages/ui/src/components/icons/comment-icon.tsx +9 -0
  91. package/packages/ui/src/components/icons/copy-icon.tsx +10 -0
  92. package/packages/ui/src/components/icons/eye-icon.tsx +10 -0
  93. package/packages/ui/src/components/icons/eye-off-icon.tsx +12 -0
  94. package/packages/ui/src/components/icons/file-icon.tsx +7 -0
  95. package/packages/ui/src/components/icons/folder-icon.tsx +19 -0
  96. package/packages/ui/src/components/icons/git-branch-icon.tsx +13 -0
  97. package/packages/ui/src/components/icons/keyboard-icon.tsx +13 -0
  98. package/packages/ui/src/components/icons/moon-icon.tsx +9 -0
  99. package/packages/ui/src/components/icons/plus-icon.tsx +9 -0
  100. package/packages/ui/src/components/icons/search-icon.tsx +10 -0
  101. package/packages/ui/src/components/icons/sidebar-icon.tsx +10 -0
  102. package/packages/ui/src/components/icons/spinner.tsx +7 -0
  103. package/packages/ui/src/components/icons/split-view-icon.tsx +10 -0
  104. package/packages/ui/src/components/icons/sun-icon.tsx +17 -0
  105. package/packages/ui/src/components/icons/trash-icon.tsx +11 -0
  106. package/packages/ui/src/components/icons/undo-icon.tsx +9 -0
  107. package/packages/ui/src/components/icons/unified-view-icon.tsx +12 -0
  108. package/packages/ui/src/components/icons/x-icon.tsx +10 -0
  109. package/packages/ui/src/components/line-number-cell.tsx +18 -0
  110. package/packages/ui/src/components/markdown-content.tsx +139 -0
  111. package/packages/ui/src/components/orphaned-threads.tsx +80 -0
  112. package/packages/ui/src/components/overview-file-list.tsx +57 -0
  113. package/packages/ui/src/components/render-expansion-rows.tsx +47 -0
  114. package/packages/ui/src/components/shortcut-modal.tsx +93 -0
  115. package/packages/ui/src/components/sidebar.tsx +80 -0
  116. package/packages/ui/src/components/skeleton.tsx +9 -0
  117. package/packages/ui/src/components/stale-diff-banner.tsx +21 -0
  118. package/packages/ui/src/components/summary-bar.tsx +39 -0
  119. package/packages/ui/src/components/toolbar.tsx +246 -0
  120. package/packages/ui/src/components/ui/badge.tsx +17 -0
  121. package/packages/ui/src/components/ui/confirm-dialog.tsx +52 -0
  122. package/packages/ui/src/components/ui/icon-button.tsx +23 -0
  123. package/packages/ui/src/components/ui/status-badge.tsx +57 -0
  124. package/packages/ui/src/components/ui/thread-badge.tsx +35 -0
  125. package/packages/ui/src/components/word-diff.tsx +126 -0
  126. package/packages/ui/src/hooks/use-comment-actions.ts +97 -0
  127. package/packages/ui/src/hooks/use-commits.ts +12 -0
  128. package/packages/ui/src/hooks/use-copy.ts +18 -0
  129. package/packages/ui/src/hooks/use-diff-staleness.ts +58 -0
  130. package/packages/ui/src/hooks/use-diff.ts +12 -0
  131. package/packages/ui/src/hooks/use-highlighter.ts +190 -0
  132. package/packages/ui/src/hooks/use-info.ts +12 -0
  133. package/packages/ui/src/hooks/use-keyboard.ts +55 -0
  134. package/packages/ui/src/hooks/use-line-selection.ts +157 -0
  135. package/packages/ui/src/hooks/use-overview.ts +12 -0
  136. package/packages/ui/src/hooks/use-review-threads.ts +12 -0
  137. package/packages/ui/src/hooks/use-search-params.ts +26 -0
  138. package/packages/ui/src/hooks/use-theme.ts +34 -0
  139. package/packages/ui/src/hooks/use-thread-navigation.ts +43 -0
  140. package/packages/ui/src/lib/api.ts +232 -0
  141. package/packages/ui/src/lib/cn.ts +6 -0
  142. package/packages/ui/src/lib/context-expansion.ts +122 -0
  143. package/packages/ui/src/lib/diff-utils.ts +268 -0
  144. package/packages/ui/src/lib/dom-utils.ts +13 -0
  145. package/packages/ui/src/lib/file-tree.ts +122 -0
  146. package/packages/ui/src/lib/query-client.ts +10 -0
  147. package/packages/ui/src/lib/render-content.tsx +23 -0
  148. package/packages/ui/src/lib/syntax-token.ts +4 -0
  149. package/packages/ui/src/main.tsx +14 -0
  150. package/packages/ui/src/queries/commits.ts +9 -0
  151. package/packages/ui/src/queries/diff.ts +9 -0
  152. package/packages/ui/src/queries/file.ts +10 -0
  153. package/packages/ui/src/queries/info.ts +9 -0
  154. package/packages/ui/src/queries/overview.ts +9 -0
  155. package/packages/ui/src/styles/app.css +178 -0
  156. package/packages/ui/src/types/comment.ts +61 -0
  157. package/packages/ui/src/vite-env.d.ts +1 -0
  158. package/packages/ui/tests/context-expansion.test.ts +279 -0
  159. package/packages/ui/tests/diff-utils.test.ts +409 -0
  160. package/packages/ui/tsconfig.json +14 -0
  161. package/packages/ui/vite.config.ts +23 -0
  162. package/scripts/build-skills.ts +26 -0
  163. package/scripts/build.ts +15 -0
  164. package/scripts/dev.ts +32 -0
  165. package/scripts/lib/transformers/claude-code.ts +11 -0
  166. package/scripts/lib/transformers/codex.ts +17 -0
  167. package/scripts/lib/transformers/cursor.ts +17 -0
  168. package/scripts/lib/transformers/index.ts +3 -0
  169. package/scripts/lib/utils.ts +70 -0
  170. package/scripts/link-dev.ts +54 -0
  171. package/skills/diffity-resolve/SKILL.md +55 -0
  172. package/skills/diffity-review/SKILL.md +74 -0
  173. package/skills/diffity-start/SKILL.md +27 -0
  174. package/tsconfig.json +22 -0
@@ -0,0 +1,278 @@
1
+ import { useMemo, useRef, useState, useCallback, useImperativeHandle, useEffect } from 'react';
2
+ import { useVirtualizer } from '@tanstack/react-virtual';
3
+ import type { ParsedDiff } from '@diffity/parser';
4
+ import { FileBlock, LARGE_DIFF_LINE_THRESHOLD } from './file-block';
5
+ import { GeneralComments } from './general-comments';
6
+ import { useHighlighter } from '../hooks/use-highlighter';
7
+ import { type ViewMode, getFilePath } from '../lib/diff-utils';
8
+ import type { CommentThread, LineSelection } from '../types/comment';
9
+ import type { CommentActions } from '../hooks/use-comment-actions';
10
+
11
+ function flashThreadElement(element: Element) {
12
+ element.classList.remove('flash-thread');
13
+ void (element as HTMLElement).offsetWidth;
14
+ element.classList.add('flash-thread');
15
+ }
16
+
17
+ export interface DiffViewHandle {
18
+ scrollToFile: (path: string) => void;
19
+ scrollToThread: (threadId: string, filePath: string) => void;
20
+ }
21
+
22
+ const VIRTUALIZER_OVERSCAN = 3;
23
+ const FILE_HEADER_HEIGHT = 56;
24
+ const EMPTY_CONTENT_HEIGHT = 100;
25
+ const LINE_HEIGHT = 24;
26
+ const HUNK_HEADER_HEIGHT = 32;
27
+ const FILE_BLOCK_PADDING = 16;
28
+
29
+ interface DiffViewProps {
30
+ diff: ParsedDiff;
31
+ viewMode: ViewMode;
32
+ theme: 'light' | 'dark';
33
+ collapsedFiles: Set<string>;
34
+ onToggleCollapse: (path: string) => void;
35
+ reviewedFiles: Set<string>;
36
+ onReviewedChange: (path: string, reviewed: boolean) => void;
37
+ onActiveFileChange?: (path: string) => void;
38
+ scrollRef?: React.RefCallback<HTMLElement>;
39
+ handle?: React.Ref<DiffViewHandle>;
40
+ baseRef?: string;
41
+ canRevert?: boolean;
42
+ onRevert?: () => void;
43
+ threads: CommentThread[];
44
+ commentsEnabled: boolean;
45
+ commentActions: CommentActions;
46
+ onAddThread: CommentActions['addThread'];
47
+ pendingSelection: LineSelection | null;
48
+ onPendingSelectionChange: (selection: LineSelection | null) => void;
49
+ }
50
+
51
+ function estimateFileHeight(file: { hunks: { lines: { length: number } }[]; isBinary: boolean }, collapsed: boolean): number {
52
+ if (collapsed) {
53
+ return FILE_HEADER_HEIGHT;
54
+ }
55
+ if (file.isBinary || file.hunks.length === 0) {
56
+ return EMPTY_CONTENT_HEIGHT;
57
+ }
58
+ let lineCount = 0;
59
+ for (const hunk of file.hunks) {
60
+ lineCount += hunk.lines.length;
61
+ }
62
+ if (lineCount >= LARGE_DIFF_LINE_THRESHOLD) {
63
+ return EMPTY_CONTENT_HEIGHT;
64
+ }
65
+ return FILE_HEADER_HEIGHT + lineCount * LINE_HEIGHT + file.hunks.length * HUNK_HEADER_HEIGHT + FILE_BLOCK_PADDING;
66
+ }
67
+
68
+ export function DiffView(props: DiffViewProps) {
69
+ const {
70
+ diff, viewMode, theme, collapsedFiles, onToggleCollapse,
71
+ reviewedFiles, onReviewedChange, onActiveFileChange, scrollRef,
72
+ handle, baseRef, canRevert, onRevert,
73
+ threads, commentsEnabled, commentActions, onAddThread,
74
+ pendingSelection, onPendingSelectionChange,
75
+ } = props;
76
+ const { highlight } = useHighlighter();
77
+ const scrollElementRef = useRef<HTMLElement>(null);
78
+
79
+ const highlighters = useMemo(() => {
80
+ const map = new Map<string, (code: string) => ReturnType<typeof highlight>>();
81
+ for (const file of diff.files) {
82
+ const filePath = getFilePath(file);
83
+ map.set(filePath, (code: string) => highlight(code, filePath, theme));
84
+ }
85
+ return map;
86
+ }, [diff, highlight, theme]);
87
+
88
+ const virtualizer = useVirtualizer({
89
+ count: diff.files.length,
90
+ getScrollElement: () => scrollElementRef.current,
91
+ estimateSize: (index) => estimateFileHeight(diff.files[index], collapsedFiles.has(getFilePath(diff.files[index]))),
92
+ overscan: VIRTUALIZER_OVERSCAN,
93
+ });
94
+
95
+ const scrollTargetRef = useRef<string | null>(null);
96
+ const [highlightedFile, setHighlightedFile] = useState<string | null>(null);
97
+
98
+ const [pendingThreadScroll, setPendingThreadScroll] = useState<string | null>(null);
99
+
100
+ useImperativeHandle(handle, () => ({
101
+ scrollToFile: (path: string) => {
102
+ const index = diff.files.findIndex((f) => getFilePath(f) === path);
103
+ if (index >= 0) {
104
+ scrollTargetRef.current = path;
105
+ setHighlightedFile(path);
106
+ virtualizer.scrollToIndex(index, { align: 'start' });
107
+ }
108
+ },
109
+ scrollToThread: (threadId: string, filePath: string) => {
110
+ const element = document.querySelector(`[data-thread-id="${threadId}"]`);
111
+ if (element) {
112
+ element.scrollIntoView({ behavior: 'instant', block: 'center' });
113
+ flashThreadElement(element);
114
+ return;
115
+ }
116
+
117
+ const index = diff.files.findIndex((f) => getFilePath(f) === filePath);
118
+ if (index >= 0) {
119
+ scrollTargetRef.current = filePath;
120
+ virtualizer.scrollToIndex(index, { align: 'start' });
121
+ setPendingThreadScroll(threadId);
122
+ }
123
+ },
124
+ }), [diff.files, virtualizer]);
125
+
126
+ useEffect(() => {
127
+ if (!pendingThreadScroll) {
128
+ return;
129
+ }
130
+
131
+ const scrollEl = scrollElementRef.current;
132
+ if (!scrollEl) {
133
+ return;
134
+ }
135
+
136
+ const threadId = pendingThreadScroll;
137
+
138
+ const tryScroll = () => {
139
+ const element = document.querySelector(`[data-thread-id="${threadId}"]`);
140
+ if (element) {
141
+ setPendingThreadScroll(null);
142
+ element.scrollIntoView({ behavior: 'instant', block: 'center' });
143
+ flashThreadElement(element);
144
+ return true;
145
+ }
146
+ return false;
147
+ };
148
+
149
+ if (tryScroll()) {
150
+ return;
151
+ }
152
+
153
+ const observer = new MutationObserver(() => {
154
+ if (tryScroll()) {
155
+ observer.disconnect();
156
+ }
157
+ });
158
+
159
+ observer.observe(scrollEl, { childList: true, subtree: true });
160
+
161
+ const timeout = setTimeout(() => {
162
+ setPendingThreadScroll(null);
163
+ observer.disconnect();
164
+ }, 2000);
165
+
166
+ return () => {
167
+ observer.disconnect();
168
+ clearTimeout(timeout);
169
+ };
170
+ }, [pendingThreadScroll]);
171
+
172
+ const getTopVisibleFile = useCallback((): string | null => {
173
+ const visibleItems = virtualizer.getVirtualItems();
174
+ if (visibleItems.length === 0) {
175
+ return null;
176
+ }
177
+
178
+ const scrollEl = scrollElementRef.current;
179
+ if (!scrollEl) {
180
+ return null;
181
+ }
182
+
183
+ const scrollTop = scrollEl.scrollTop;
184
+ for (const item of visibleItems) {
185
+ if (item.end > scrollTop) {
186
+ return getFilePath(diff.files[item.index]);
187
+ }
188
+ }
189
+
190
+ return getFilePath(diff.files[visibleItems[0].index]);
191
+ }, [virtualizer, diff.files]);
192
+
193
+ const handleScroll = useCallback(() => {
194
+ if (!onActiveFileChange) {
195
+ return;
196
+ }
197
+
198
+ const topFile = getTopVisibleFile();
199
+ if (!topFile) {
200
+ return;
201
+ }
202
+
203
+ if (scrollTargetRef.current) {
204
+ if (topFile === scrollTargetRef.current) {
205
+ scrollTargetRef.current = null;
206
+ }
207
+ return;
208
+ }
209
+
210
+ onActiveFileChange(topFile);
211
+ }, [getTopVisibleFile, onActiveFileChange]);
212
+
213
+ const items = virtualizer.getVirtualItems();
214
+ const [paddingTop, paddingBottom] = items.length > 0
215
+ ? [
216
+ items[0].start,
217
+ virtualizer.getTotalSize() - items[items.length - 1].end,
218
+ ]
219
+ : [0, 0];
220
+
221
+ return (
222
+ <main
223
+ ref={(node) => {
224
+ scrollElementRef.current = node;
225
+ if (scrollRef) {
226
+ scrollRef(node);
227
+ }
228
+ }}
229
+ onScroll={handleScroll}
230
+ className="flex-1 overflow-y-auto pb-12"
231
+ >
232
+ {commentsEnabled && (
233
+ <GeneralComments
234
+ threads={threads}
235
+ commentActions={commentActions}
236
+ />
237
+ )}
238
+ <div className="py-2" style={{ paddingTop, paddingBottom }}>
239
+ {items.map((virtualItem) => {
240
+ const file = diff.files[virtualItem.index];
241
+ const filePath = getFilePath(file);
242
+ return (
243
+ <div
244
+ key={filePath + '-' + virtualItem.index}
245
+ data-index={virtualItem.index}
246
+ ref={virtualizer.measureElement}
247
+ >
248
+ <FileBlock
249
+ highlighted={highlightedFile === filePath}
250
+ onHighlightEnd={() => {
251
+ if (highlightedFile === filePath) {
252
+ setHighlightedFile(null);
253
+ }
254
+ }}
255
+ file={file}
256
+ viewMode={viewMode}
257
+ collapsed={collapsedFiles.has(filePath)}
258
+ onToggleCollapse={onToggleCollapse}
259
+ reviewed={reviewedFiles.has(filePath)}
260
+ onReviewedChange={onReviewedChange}
261
+ highlightLine={highlighters.get(filePath)}
262
+ baseRef={baseRef}
263
+ canRevert={canRevert}
264
+ onRevert={onRevert}
265
+ threads={threads}
266
+ commentsEnabled={commentsEnabled}
267
+ commentActions={commentActions}
268
+ onAddThread={onAddThread}
269
+ pendingSelection={pendingSelection}
270
+ onPendingSelectionChange={onPendingSelectionChange}
271
+ />
272
+ </div>
273
+ );
274
+ })}
275
+ </div>
276
+ </main>
277
+ );
278
+ }
@@ -0,0 +1,45 @@
1
+ import { ArrowUpIcon } from './icons/arrow-up-icon';
2
+ import { ArrowDownIcon } from './icons/arrow-down-icon';
3
+ import { Spinner } from './icons/spinner';
4
+
5
+ interface ExpandRowProps {
6
+ position: 'top' | 'bottom';
7
+ remainingLines: number;
8
+ loading: boolean;
9
+ onExpand: (dir: 'up' | 'down' | 'all') => void;
10
+ }
11
+
12
+ const gutterCell = 'w-[25px] min-w-[25px] bg-diff-hunk-bg border-r border-border-muted p-0';
13
+ const expandBtn = 'flex items-center justify-center w-full h-[18px] cursor-pointer text-diff-hunk-text/70 hover:text-diff-hunk-text transition-colors';
14
+ const expandRowClass = 'bg-diff-hunk-bg';
15
+
16
+ export function ExpandRow(props: ExpandRowProps) {
17
+ const { position, remainingLines, loading, onExpand } = props;
18
+
19
+ if (remainingLines <= 0) {
20
+ return null;
21
+ }
22
+
23
+ return (
24
+ <tr className={expandRowClass}>
25
+ {loading ? (
26
+ <td className={gutterCell}>
27
+ <div className="flex items-center justify-center h-[18px]">
28
+ <Spinner />
29
+ </div>
30
+ </td>
31
+ ) : (
32
+ <td className={gutterCell}>
33
+ <button
34
+ className={expandBtn}
35
+ onClick={() => onExpand('down')}
36
+ title={`Expand ${Math.min(remainingLines, 20)} lines`}
37
+ >
38
+ {position === 'bottom' ? <ArrowDownIcon /> : <ArrowUpIcon />}
39
+ </button>
40
+ </td>
41
+ )}
42
+ <td colSpan={3} />
43
+ </tr>
44
+ );
45
+ }