@reviewflow/ui 6.23.0 → 6.25.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 (39) hide show
  1. package/package.json +21 -6
  2. package/src/components/branch-diff/diff-font.ts +26 -0
  3. package/src/components/branch-diff/diff-themes.ts +53 -1
  4. package/src/components/branch-diff/reviewflow-diff-selection.ts +11 -0
  5. package/src/components/branch-diff/reviewflow-diff-themes.ts +2218 -0
  6. package/src/components/branch-diff/use-diff-settings.tsx +15 -3
  7. package/src/components/branch-diff/whitespace-button.tsx +2 -2
  8. package/src/components/diff-settings-section.tsx +189 -0
  9. package/src/components/diff-theme-preview.tsx +63 -0
  10. package/src/components/file-kind.ts +91 -0
  11. package/src/components/file-type-icon.tsx +70 -0
  12. package/src/components/pull-request-actions-review.tsx +2 -2
  13. package/src/components/pull-request-commits.tsx +2 -1
  14. package/src/components/pull-request-diff-annotations.tsx +8 -5
  15. package/src/components/pull-request-diff-impacts-summary.tsx +2 -1
  16. package/src/components/pull-request-diff-impacts.ts +1 -3
  17. package/src/components/pull-request-diff-jump.ts +59 -1
  18. package/src/components/pull-request-diff-overview-tree.ts +57 -0
  19. package/src/components/pull-request-diff-overview.tsx +489 -90
  20. package/src/components/pull-request-diff-tree-files.ts +64 -0
  21. package/src/components/pull-request-diff-tree.tsx +232 -0
  22. package/src/components/pull-request-diff-view.tsx +112 -28
  23. package/src/components/pull-request-diff-viewer.tsx +26 -6
  24. package/src/components/pull-request-files.tsx +6 -6
  25. package/src/components/pull-request-panel.tsx +23 -21
  26. package/src/components/pull-request-reactions.tsx +2 -2
  27. package/src/components/pull-request-revisions.tsx +2 -1
  28. package/src/components/pull-request-screen.tsx +37 -17
  29. package/src/components/pull-request-suggestion-actions.tsx +7 -6
  30. package/src/components/repository-attachment-banner.tsx +1 -1
  31. package/src/components/scroll-parent.ts +20 -0
  32. package/src/components/tooltip-icon-button.tsx +27 -0
  33. package/src/editor/code-editor.tsx +1 -0
  34. package/src/editor/code-token-colors.css +39 -0
  35. package/src/editor/codeHighlight.ts +2 -1
  36. package/src/editor/markdown-editor.css +1 -34
  37. package/src/editor/markdown-editor.tsx +1 -0
  38. package/src/index.ts +6 -6
  39. package/src/palette.ts +22 -0
@@ -1,5 +1,12 @@
1
1
  import { animationDurationsMs } from "alouette";
2
- import { createContext, use, useSyncExternalStore } from "react";
2
+ import type { RefObject } from "react";
3
+ import {
4
+ createContext,
5
+ use,
6
+ useEffect,
7
+ useState,
8
+ useSyncExternalStore,
9
+ } from "react";
3
10
 
4
11
  /** How long a file gone to stays outlined before the outline fades. */
5
12
  const highlightHoldMs = 1500;
@@ -20,6 +27,8 @@ export interface DiffFileElements {
20
27
  /** returns the unregistration */
21
28
  register: (path: string, element: HTMLElement) => () => void;
22
29
  elementOf: (path: string) => HTMLElement | undefined;
30
+ /** the file whose element spans the viewport's `y`; null in a gap or past the files */
31
+ fileAt: (y: number) => string | null;
23
32
  /** outlines the file's header in `color`, replacing any outline still showing */
24
33
  highlight: (path: string, color: string) => void;
25
34
  subscribe: (listener: () => void) => () => void;
@@ -43,6 +52,13 @@ export const createDiffFileElements = (): DiffFileElements => {
43
52
  };
44
53
  },
45
54
  elementOf: (path) => elements.get(path),
55
+ fileAt: (y) => {
56
+ for (const [path, element] of elements) {
57
+ const { top, bottom } = element.getBoundingClientRect();
58
+ if (top <= y && y < bottom) return path;
59
+ }
60
+ return null;
61
+ },
46
62
  highlight: (path, color) => {
47
63
  for (const timer of timers) clearTimeout(timer);
48
64
  setHighlighted({ path, phase: "shown", color });
@@ -73,6 +89,48 @@ export const DiffFileElementsContext = createContext<DiffFileElements | null>(
73
89
  export const useDiffFileElements = (): DiffFileElements | null =>
74
90
  use(DiffFileElementsContext);
75
91
 
92
+ /**
93
+ * The file read at the moment: the one under the bottom edge of `bar`, the
94
+ * sticky toolbar the diff scrolls beneath. Measured once a frame while
95
+ * anything scrolls, on the web only; it keeps the last file found while the
96
+ * edge is over a gap between files, and is null until one is.
97
+ */
98
+ export const useFileInView = (
99
+ fileElements: DiffFileElements,
100
+ bar: RefObject<unknown>,
101
+ enabled: boolean,
102
+ ): string | null => {
103
+ const [path, setPath] = useState<string | null>(null);
104
+ useEffect(() => {
105
+ if (!enabled || typeof document === "undefined") return undefined;
106
+ let frame: number | null = null;
107
+ const measure = (): void => {
108
+ frame = null;
109
+ const element = bar.current;
110
+ if (!(element instanceof HTMLElement)) return;
111
+ const found = fileElements.fileAt(
112
+ element.getBoundingClientRect().bottom + 1,
113
+ );
114
+ if (found !== null) setPath(found);
115
+ };
116
+ const schedule = (): void => {
117
+ frame ??= requestAnimationFrame(measure);
118
+ };
119
+ schedule();
120
+ document.addEventListener("scroll", schedule, {
121
+ capture: true,
122
+ passive: true,
123
+ });
124
+ window.addEventListener("resize", schedule);
125
+ return () => {
126
+ if (frame !== null) cancelAnimationFrame(frame);
127
+ document.removeEventListener("scroll", schedule, { capture: true });
128
+ window.removeEventListener("resize", schedule);
129
+ };
130
+ }, [fileElements, bar, enabled]);
131
+ return enabled ? path : null;
132
+ };
133
+
76
134
  const noSubscription = (): (() => void) => () => {};
77
135
 
78
136
  /** Re-renders only the file whose highlight changes, not every file of the diff. */
@@ -0,0 +1,57 @@
1
+ export interface OverviewTreeRow {
2
+ /** `workspaceIdOf` or `fileIdOf`: a workspace and a file may share a path */
3
+ id: string;
4
+ kind: "file" | "workspace";
5
+ /** 1 for a workspace, 2 for its files, 3 for a test below its subject */
6
+ level: 1 | 2 | 3;
7
+ parent: string | null;
8
+ /** a workspace's rows are listed; false for a file */
9
+ expanded: boolean;
10
+ }
11
+
12
+ export const workspaceIdOf = (name: string): string => `workspace:${name}`;
13
+ export const fileIdOf = (path: string): string => `file:${path}`;
14
+
15
+ export type OverviewTreeAction =
16
+ | { type: "focus"; id: string }
17
+ | { type: "press"; id: string }
18
+ | { type: "toggle"; id: string };
19
+
20
+ /**
21
+ * What a key does on the focused row, as the WAI-ARIA tree pattern has it:
22
+ * up and down move a row, home and end to either end, right opens a closed
23
+ * workspace or enters an open one, left closes an open workspace or goes up to
24
+ * the parent, space presses the row. Enter is left to the pressable, which
25
+ * presses on it already. Null for any other key.
26
+ */
27
+ export const overviewTreeActionOf = (
28
+ rows: readonly OverviewTreeRow[],
29
+ focusedId: string,
30
+ key: string,
31
+ ): OverviewTreeAction | null => {
32
+ const index = rows.findIndex(({ id }) => id === focusedId);
33
+ const row = rows[index];
34
+ if (row === undefined) return null;
35
+ const focus = (
36
+ target: OverviewTreeRow | undefined,
37
+ ): OverviewTreeAction | null =>
38
+ target === undefined ? null : { type: "focus", id: target.id };
39
+ if (key === "ArrowDown") return focus(rows[index + 1]);
40
+ if (key === "ArrowUp") return focus(rows[index - 1]);
41
+ if (key === "Home") return focus(rows[0]);
42
+ if (key === "End") return focus(rows.at(-1));
43
+ if (key === " ") return { type: "press", id: row.id };
44
+ if (key === "ArrowRight") {
45
+ if (row.kind === "file") return null;
46
+ if (!row.expanded) return { type: "toggle", id: row.id };
47
+ const child = rows[index + 1];
48
+ return child?.parent === row.id ? focus(child) : null;
49
+ }
50
+ if (key === "ArrowLeft") {
51
+ if (row.kind === "workspace" && row.expanded) {
52
+ return { type: "toggle", id: row.id };
53
+ }
54
+ return row.parent === null ? null : { type: "focus", id: row.parent };
55
+ }
56
+ return null;
57
+ };