@cueloop/client 0.1.0-alpha.91 → 0.1.0-alpha.93

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 (40) hide show
  1. package/package.json +5 -5
  2. package/src/App.deleted-scroll.test.tsx +186 -0
  3. package/src/App.diff.test.tsx +3 -4
  4. package/src/App.test.tsx +1 -0
  5. package/src/App.toast.test.tsx +27 -1
  6. package/src/App.tsx +21 -12
  7. package/src/App.workbench.test.tsx +208 -2
  8. package/src/app-screens.tsx +34 -8
  9. package/src/components/AnnotatableFileView.test.tsx +48 -1
  10. package/src/components/AppShell.zoom.test.tsx +16 -3
  11. package/src/components/ChangesColumn.tsx +10 -4
  12. package/src/components/ChangesFileTree.test.tsx +122 -4
  13. package/src/components/DiffContentView.scroll.test.tsx +285 -1
  14. package/src/components/DiffContentView.tsx +214 -56
  15. package/src/components/EditorGrid.split.test.tsx +118 -0
  16. package/src/components/EditorGrid.stories.tsx +27 -0
  17. package/src/components/EditorGrid.tsx +170 -50
  18. package/src/components/EditorGrid.zoom.test.tsx +65 -1
  19. package/src/components/GridTabContent.actions.test.tsx +151 -0
  20. package/src/components/GridTabContent.stories.tsx +2 -2
  21. package/src/components/GridTabContent.tsx +5 -1
  22. package/src/components/ProjectTreeView.test.tsx +166 -8
  23. package/src/components/ProjectTreeView.tsx +53 -27
  24. package/src/components/ScrollArea.tsx +21 -2
  25. package/src/components/Toast.tsx +3 -1
  26. package/src/components/WelcomePlayground.test.tsx +28 -1
  27. package/src/components/__snapshots__/stories.test.tsx.snap +41 -21
  28. package/src/components/editor-grid.test.ts +87 -0
  29. package/src/components/editor-grid.ts +142 -6
  30. package/src/components/primitives/Tree.tsx +51 -15
  31. package/src/diff-syntax.test.ts +15 -2
  32. package/src/diff-syntax.ts +13 -5
  33. package/src/thread-controller.curate.test.ts +27 -0
  34. package/src/thread-controller.ts +4 -6
  35. package/src/use-annotation-surface.tsx +86 -19
  36. package/src/use-changes-workbench.ts +30 -18
  37. package/src/use-frame-measure.ts +40 -7
  38. package/src/use-settings-dialog.tsx +1 -13
  39. package/src/use-shared-keyboard.ts +55 -0
  40. package/src/use-terminal-virtualizer.ts +8 -2
@@ -1,14 +1,26 @@
1
- /** The Project tree navigates by keyboard when focused: j/k move the cursor, tab folds a folder or
1
+ /** The Project tree navigates by keyboard when focused: j/k move the cursor, Enter folds a folder or
2
2
  * opens a file. */
3
3
 
4
4
  import { test, expect } from "bun:test";
5
5
  import React from "react";
6
6
  import { testRender } from "@opentui/react/test-utils";
7
+ import { ScrollBoxRenderable, type Renderable } from "@opentui/core";
7
8
  import { ProjectTreeView } from "./ProjectTreeView";
8
- import { locateText, settle, waitForText } from "../test-support";
9
+ import { locateText, settle, waitForText, waitForTextGone } from "../test-support";
9
10
  import { DARK } from "../theme";
10
11
 
11
- test("tab expands a folder, then j moves down and tab opens the file", async () => {
12
+ function findById(node: Renderable, id: string): Renderable | undefined {
13
+ if (node.id === id) return node;
14
+ for (const child of node.getChildren()) {
15
+ const found = findById(child, id);
16
+
17
+ if (found) return found;
18
+ }
19
+
20
+ return undefined;
21
+ }
22
+
23
+ test("Enter expands a folder, then j moves down and Enter opens the file", async () => {
12
24
  const opened: string[] = [];
13
25
  const setup = await testRender(
14
26
  <ProjectTreeView
@@ -22,14 +34,14 @@ test("tab expands a folder, then j moves down and tab opens the file", async ()
22
34
  await settle(setup);
23
35
  await waitForText(setup, "src");
24
36
 
25
- // cursor starts on the "src" folder; tab expands it
26
- setup.mockInput.pressKey("TAB");
37
+ // cursor starts on the "src" folder; Enter expands it
38
+ setup.mockInput.pressKey("RETURN");
27
39
  await waitForText(setup, "a.ts");
28
40
 
29
- // j moves onto the file, tab opens it
41
+ // j moves onto the file, Enter opens it
30
42
  setup.mockInput.pressKey("j");
31
43
  await settle(setup);
32
- setup.mockInput.pressKey("TAB");
44
+ setup.mockInput.pressKey("RETURN");
33
45
  await settle(setup);
34
46
 
35
47
  expect(opened).toEqual(["src/a.ts"]);
@@ -37,6 +49,91 @@ test("tab expands a folder, then j moves down and tab opens the file", async ()
37
49
  setup.renderer.destroy();
38
50
  });
39
51
 
52
+ test("keyboard navigation reveals the selected project row", async () => {
53
+ const paths = Array.from(
54
+ { length: 35 },
55
+ (_, index) => `file-${String(index).padStart(2, "0")}.ts`,
56
+ );
57
+ const setup = await testRender(
58
+ <ProjectTreeView loadFiles={async () => paths} onSelectFile={() => {}} focused theme={DARK} />,
59
+ { width: 28, height: 8 },
60
+ );
61
+
62
+ await waitForText(setup, "file-00.ts");
63
+ const scrollbox = findById(setup.renderer.root, "tree-scroll");
64
+
65
+ if (!(scrollbox instanceof ScrollBoxRenderable)) throw new Error("tree scrollbox missing");
66
+ const scrollSteps: number[] = [];
67
+
68
+ for (const [index, key] of Array.from({ length: 15 }, () => "j").entries()) {
69
+ const before = scrollbox.scrollTop;
70
+
71
+ setup.mockInput.pressKey(key);
72
+ // eslint-disable-next-line no-await-in-loop
73
+ await settle(setup);
74
+ scrollSteps.push(scrollbox.scrollTop - before);
75
+ const row = scrollbox.content.findDescendantById(
76
+ `tree-row-file-${String(index + 1).padStart(2, "0")}.ts`,
77
+ );
78
+
79
+ expect(row?.y).toBeGreaterThanOrEqual(scrollbox.viewport.y);
80
+ expect(row?.y).toBeLessThan(scrollbox.viewport.y + scrollbox.viewport.height);
81
+ }
82
+
83
+ expect(scrollbox.scrollTop).toBeGreaterThan(0);
84
+ expect(Math.max(...scrollSteps)).toBe(1);
85
+ expect(setup.captureCharFrame()).toContain("file-15.ts");
86
+ setup.renderer.destroy();
87
+ });
88
+
89
+ test("project wheel scrolling accelerates during a gesture and resets after a pause", async () => {
90
+ const paths = Array.from(
91
+ { length: 35 },
92
+ (_, index) => `file-${String(index).padStart(2, "0")}.ts`,
93
+ );
94
+ const setup = await testRender(
95
+ <ProjectTreeView loadFiles={async () => paths} onSelectFile={() => {}} theme={DARK} />,
96
+ { width: 28, height: 8 },
97
+ );
98
+
99
+ await waitForText(setup, "file-00.ts");
100
+ const scrollbox = findById(setup.renderer.root, "tree-scroll");
101
+
102
+ if (!(scrollbox instanceof ScrollBoxRenderable)) throw new Error("tree scrollbox missing");
103
+ const acceleration = scrollbox.scrollAcceleration;
104
+ const first = acceleration.tick(1000);
105
+ const burst = Array.from({ length: 10 }, (_, index) => acceleration.tick(1016 + index * 16));
106
+ const tail = acceleration.tick(1400);
107
+
108
+ expect(first).toBe(1);
109
+ expect(Math.max(...burst)).toBeGreaterThan(first);
110
+ expect(tail).toBe(1);
111
+ acceleration.reset();
112
+ await setup.mockMouse.scroll(10, 4, "down");
113
+ await settle(setup);
114
+ expect(scrollbox.scrollTop).toBeGreaterThan(0);
115
+ setup.renderer.destroy();
116
+ });
117
+
118
+ test("long project filenames stay on one row without an ellipsis", async () => {
119
+ const path = "src/ChangesFileTree.test.tsx";
120
+ const setup = await testRender(
121
+ <ProjectTreeView loadFiles={async () => [path]} onSelectFile={() => {}} focused theme={DARK} />,
122
+ { width: 22, height: 8 },
123
+ );
124
+
125
+ await waitForText(setup, "src");
126
+ setup.mockInput.pressKey("RETURN");
127
+ await settle(setup);
128
+ const frame = setup.captureCharFrame();
129
+ const row = findById(setup.renderer.root, `tree-row-${path}`);
130
+
131
+ expect(row?.height).toBe(1);
132
+ expect(frame).not.toContain("…");
133
+ expect(frame).toContain("ChangesFile");
134
+ setup.renderer.destroy();
135
+ });
136
+
40
137
  test("without focus the keyboard does nothing", async () => {
41
138
  const opened: string[] = [];
42
139
  const setup = await testRender(
@@ -50,7 +147,7 @@ test("without focus the keyboard does nothing", async () => {
50
147
  await settle(setup);
51
148
  await waitForText(setup, "src");
52
149
 
53
- setup.mockInput.pressKey("TAB");
150
+ setup.mockInput.pressKey("RETURN");
54
151
  await settle(setup);
55
152
  expect(setup.captureCharFrame()).not.toContain("a.ts");
56
153
  expect(opened).toEqual([]);
@@ -73,3 +170,64 @@ test("the empty project hint stays centered in the panel", async () => {
73
170
  expect(position.row).toBeLessThanOrEqual(6);
74
171
  setup.renderer.destroy();
75
172
  });
173
+
174
+ test("the visible tree removes deleted files without remounting", async () => {
175
+ let files = ["README.md", "gone.ts"];
176
+ const setup = await testRender(
177
+ <ProjectTreeView loadFiles={async () => files} onSelectFile={() => {}} theme={DARK} />,
178
+ { width: 40, height: 12 },
179
+ );
180
+
181
+ await waitForText(setup, "gone.ts");
182
+ files = ["README.md"];
183
+ await waitForTextGone(setup, "gone.ts");
184
+ expect(setup.captureCharFrame()).toContain("README.md");
185
+ setup.renderer.destroy();
186
+ });
187
+
188
+ test("refresh keeps the selected file when an earlier file disappears", async () => {
189
+ let files = ["a.ts", "b.ts", "c.ts"];
190
+ const opened: string[] = [];
191
+ const setup = await testRender(
192
+ <ProjectTreeView
193
+ loadFiles={async () => files}
194
+ onSelectFile={(path) => opened.push(path)}
195
+ focused
196
+ theme={DARK}
197
+ />,
198
+ { width: 40, height: 12 },
199
+ );
200
+
201
+ await waitForText(setup, "b.ts");
202
+ setup.mockInput.pressKey("j");
203
+ await settle(setup);
204
+ files = ["b.ts", "c.ts"];
205
+ await waitForTextGone(setup, "a.ts");
206
+ setup.mockInput.pressKey("RETURN");
207
+ await settle(setup);
208
+
209
+ expect(opened).toEqual(["b.ts"]);
210
+ setup.renderer.destroy();
211
+ });
212
+
213
+ test("a double-click requests a persistent file tab", async () => {
214
+ const opened: { path: string; persistent: boolean }[] = [];
215
+ const setup = await testRender(
216
+ <ProjectTreeView
217
+ loadFiles={async () => ["README.md"]}
218
+ onSelectFile={(path, persistent) => opened.push({ path, persistent: persistent === true })}
219
+ theme={DARK}
220
+ />,
221
+ { width: 40, height: 12 },
222
+ );
223
+
224
+ await waitForText(setup, "README.md");
225
+ const file = locateText(setup, "README.md");
226
+ await setup.mockMouse.doubleClick(file.column, file.row);
227
+ expect(opened).toEqual([
228
+ { path: "README.md", persistent: false },
229
+ { path: "README.md", persistent: false },
230
+ { path: "README.md", persistent: true },
231
+ ]);
232
+ setup.renderer.destroy();
233
+ });
@@ -3,7 +3,7 @@
3
3
  // on the session id so switching sessions remounts it and never shows the previous repository's paths.
4
4
 
5
5
  import { ScrollArea } from "./ScrollArea";
6
- import React, { useEffect, useMemo, useRef, useState } from "react";
6
+ import React, { useEffect, useEffectEvent, useMemo, useState } from "react";
7
7
  import { useKeyboard } from "@opentui/react";
8
8
  import type { Theme } from "../theme";
9
9
  import { useComponentTheme } from "./theme-context";
@@ -11,10 +11,12 @@ import { Tree } from "./primitives/Tree";
11
11
  import { flattenTree } from "./primitives/tree-model";
12
12
  import { buildPathTree } from "./file-tree";
13
13
 
14
+ const PROJECT_FILES_REFRESH_MS = 2000;
15
+
14
16
  export interface ProjectTreeViewProps {
15
17
  loadFiles: () => Promise<string[]>;
16
- onSelectFile: (path: string) => void;
17
- /** The pane owns the keyboard: j/k and arrows move the cursor, tab/enter open or fold. */
18
+ onSelectFile: (path: string, persistent?: boolean) => void;
19
+ /** The pane owns the keyboard: j/k and arrows move the cursor; Enter opens or folds. */
18
20
  focused?: boolean;
19
21
  theme?: Theme;
20
22
  }
@@ -28,14 +30,14 @@ export function ProjectTreeView({
28
30
  const tokens = useComponentTheme(theme);
29
31
  const [paths, setPaths] = useState<readonly string[] | null>(null);
30
32
  const [expandedIds, setExpandedIds] = useState<ReadonlySet<string>>(new Set());
31
- const [cursor, setCursor] = useState(0);
33
+ const [cursorId, setCursorId] = useState<string | null>(null);
32
34
  const nodes = useMemo(() => (paths ? buildPathTree(paths) : []), [paths]);
33
35
  const rows = useMemo(() => flattenTree(nodes, { expandedIds }), [nodes, expandedIds]);
34
- const cursorIndex = Math.min(cursor, Math.max(0, rows.length - 1));
35
- const loadRef = useRef(loadFiles);
36
- useEffect(() => {
37
- loadRef.current = loadFiles;
38
- });
36
+ const cursorIndex = Math.max(
37
+ 0,
38
+ rows.findIndex((row) => row.id === cursorId),
39
+ );
40
+ const loadLatestFiles = useEffectEvent(loadFiles);
39
41
 
40
42
  const toggle = (id: string): void =>
41
43
  setExpandedIds((current) => {
@@ -50,29 +52,49 @@ export function ProjectTreeView({
50
52
  useKeyboard((key) => {
51
53
  if (!focused || rows.length === 0) return;
52
54
  if (key.name === "j" || key.name === "down")
53
- return setCursor(Math.min(cursorIndex + 1, rows.length - 1));
54
- if (key.name === "k" || key.name === "up") return setCursor(Math.max(cursorIndex - 1, 0));
55
+ return setCursorId(rows[Math.min(cursorIndex + 1, rows.length - 1)]!.id);
56
+ if (key.name === "k" || key.name === "up")
57
+ return setCursorId(rows[Math.max(cursorIndex - 1, 0)]!.id);
55
58
  const row = rows[cursorIndex];
56
59
  if (!row) return;
57
- if (key.name === "tab" || key.name === "return" || key.name === "enter")
58
- return row.isFolder ? toggle(row.id) : onSelectFile(row.id);
60
+ if (key.name === "return" || key.name === "enter")
61
+ return row.isFolder ? toggle(row.id) : onSelectFile(row.id, true);
59
62
  if (key.name === "l" && row.isFolder && !expandedIds.has(row.id)) return toggle(row.id);
60
63
  if (key.name === "h" && row.isFolder && expandedIds.has(row.id)) return toggle(row.id);
61
64
  });
62
65
 
63
66
  useEffect(() => {
64
- let alive = true;
65
- void loadRef.current().then(
66
- (files) => {
67
- if (alive) setPaths(files);
68
- },
69
- () => {
70
- if (alive) setPaths([]);
71
- },
72
- );
67
+ let cancelled = false;
68
+ let requestInFlight = false;
69
+
70
+ const refreshFiles = async (): Promise<void> => {
71
+ if (requestInFlight) return;
72
+ requestInFlight = true;
73
+
74
+ try {
75
+ const files = await loadLatestFiles();
76
+
77
+ if (!cancelled) {
78
+ setPaths((previous) =>
79
+ previous?.length === files.length &&
80
+ previous.every((path, index) => path === files[index])
81
+ ? previous
82
+ : files,
83
+ );
84
+ }
85
+ } catch {
86
+ if (!cancelled) setPaths((previous) => previous ?? []);
87
+ } finally {
88
+ requestInFlight = false;
89
+ }
90
+ };
91
+
92
+ void refreshFiles();
93
+ const timer = setInterval(() => void refreshFiles(), PROJECT_FILES_REFRESH_MS);
73
94
 
74
95
  return () => {
75
- alive = false;
96
+ cancelled = true;
97
+ clearInterval(timer);
76
98
  };
77
99
  }, []);
78
100
 
@@ -106,17 +128,21 @@ export function ProjectTreeView({
106
128
  }
107
129
 
108
130
  return (
109
- <ScrollArea>
131
+ <ScrollArea
132
+ id="tree-scroll"
133
+ revealId={focused ? rows[cursorIndex]?.id : undefined}
134
+ gestureWheel
135
+ >
110
136
  <Tree
111
137
  nodes={nodes}
112
138
  expandedIds={expandedIds}
113
139
  selectedId={focused ? rows[cursorIndex]?.id : undefined}
140
+ singleLine
114
141
  onSelect={(id) => {
115
- const index = rows.findIndex((row) => row.id === id);
116
-
117
- if (index >= 0) setCursor(index);
142
+ setCursorId(id);
118
143
  onSelectFile(id);
119
144
  }}
145
+ onDoubleSelect={(id) => onSelectFile(id, true)}
120
146
  onToggle={toggle}
121
147
  theme={theme}
122
148
  />
@@ -4,8 +4,8 @@
4
4
  * Pass scrollRef to keep scrollTo / scrollChildIntoView on the underlying ScrollBox.
5
5
  */
6
6
 
7
- import React, { useRef, type ReactNode, type RefObject } from "react";
8
- import type { ScrollBoxRenderable } from "@opentui/core";
7
+ import React, { useLayoutEffect, useMemo, useRef, type ReactNode, type RefObject } from "react";
8
+ import { MacOSScrollAccel, type ScrollBoxRenderable } from "@opentui/core";
9
9
  import type { Theme } from "../theme";
10
10
  import { OverlayScrollbar } from "./OverlayScrollbar";
11
11
 
@@ -14,6 +14,11 @@ export interface ScrollAreaProps {
14
14
  focused?: boolean;
15
15
  theme?: Theme;
16
16
  scrollRef?: RefObject<ScrollBoxRenderable | null>;
17
+ id?: string;
18
+ /** Reveal this mounted row when keyboard selection changes. */
19
+ revealId?: string;
20
+ /** Use gesture acceleration for a scrollable file tree. */
21
+ gestureWheel?: boolean;
17
22
  }
18
23
 
19
24
  export function ScrollArea({
@@ -21,19 +26,33 @@ export function ScrollArea({
21
26
  focused = false,
22
27
  theme,
23
28
  scrollRef,
29
+ id,
30
+ revealId,
31
+ gestureWheel = false,
24
32
  }: ScrollAreaProps): React.ReactNode {
25
33
  const innerRef = useRef<ScrollBoxRenderable | null>(null);
34
+ const wheelAcceleration = useMemo(
35
+ () => (gestureWheel ? new MacOSScrollAccel({ A: 0.4, tau: 4, maxMultiplier: 3 }) : undefined),
36
+ [gestureWheel],
37
+ );
26
38
  const attach = (node: ScrollBoxRenderable | null): void => {
27
39
  innerRef.current = node;
28
40
  if (scrollRef) scrollRef.current = node;
29
41
  };
30
42
 
43
+ useLayoutEffect(() => {
44
+ if (revealId === undefined) return;
45
+ innerRef.current?.scrollChildIntoView(`tree-row-${revealId}`);
46
+ }, [revealId]);
47
+
31
48
  return (
32
49
  <box style={{ flexGrow: 1, flexDirection: "row" }}>
33
50
  <scrollbox
51
+ id={id}
34
52
  ref={attach}
35
53
  style={{ flexGrow: 1 }}
36
54
  focused={focused}
55
+ scrollAcceleration={wheelAcceleration}
37
56
  verticalScrollbarOptions={{ visible: false }}
38
57
  >
39
58
  {children}
@@ -7,13 +7,14 @@ import { Dialog } from "./primitives/Dialog";
7
7
  export interface ToastProps {
8
8
  title?: string;
9
9
  body: string;
10
+ onDismiss?: () => void;
10
11
  theme?: Theme;
11
12
  }
12
13
 
13
14
  /** A toast never spans the screen: a long message wraps inside this, like the settings dialog. */
14
15
  const TOAST_MAX_WIDTH = 64;
15
16
 
16
- export function Toast({ title, body, theme }: ToastProps): React.ReactNode {
17
+ export function Toast({ title, body, onDismiss, theme }: ToastProps): React.ReactNode {
17
18
  const tokens = useComponentTheme(theme);
18
19
  const { width: terminalWidth, height: terminalHeight } = useTerminalDimensions();
19
20
  const width = Math.min(
@@ -34,6 +35,7 @@ export function Toast({ title, body, theme }: ToastProps): React.ReactNode {
34
35
  width={width}
35
36
  height={height}
36
37
  background={tokens.elevated}
38
+ onDismiss={onDismiss}
37
39
  theme={theme}
38
40
  >
39
41
  <box
@@ -10,7 +10,7 @@ import React from "react";
10
10
  import { WelcomePlayground } from "./WelcomePlayground";
11
11
  import { DARK } from "../theme";
12
12
  import { CLIENT_VERSION } from "../version";
13
- import { dragText, typeText, pressKey, waitForText } from "../test-support";
13
+ import { dragText, locateText, press, typeText, pressKey, waitForText } from "../test-support";
14
14
 
15
15
  describe("WelcomePlayground", () => {
16
16
  test("the copy prompts the select-and-type and the slash gestures", async () => {
@@ -46,4 +46,31 @@ describe("WelcomePlayground", () => {
46
46
 
47
47
  setup.renderer.destroy();
48
48
  });
49
+
50
+ test("the caret marker remains visible when Up crosses an empty line", async () => {
51
+ const setup = await testRender(<WelcomePlayground quickActions={[]} theme={DARK} />, {
52
+ width: 100,
53
+ height: 28,
54
+ });
55
+
56
+ await waitForText(setup, "Start");
57
+ const start = locateText(setup, "Start");
58
+
59
+ await setup.mockMouse.click(start.column, start.row);
60
+ await press(setup, "escape");
61
+
62
+ const markerRows = () =>
63
+ setup
64
+ .captureCharFrame()
65
+ .split("\n")
66
+ .flatMap((line, row) => (line.includes("▎") ? [row] : []));
67
+
68
+ expect(markerRows()).toEqual([start.row]);
69
+ await press(setup, "up");
70
+ expect(markerRows()).toEqual([start.row - 1]);
71
+ await press(setup, "up");
72
+ expect(markerRows()).toEqual([start.row - 2]);
73
+
74
+ setup.renderer.destroy();
75
+ });
49
76
  });
@@ -181,12 +181,12 @@ exports[`stories catalog Chrome/Breadcrumb/SessionHeader renders, snapshots, and
181
181
  exports[`stories catalog Surfaces/ChangesColumn/Changed renders, snapshots, and carries its colors: Surfaces/ChangesColumn/Changed 1`] = `
182
182
  "│  packages
183
183
  │  client/src
184
- │  App.tsx M
184
+ │  App.tsx M
185
185
  │  components
186
- │  ChangesColumn.tsx A
186
+ │  ChangesColumn.tsx A
187
187
  │  daemon/src
188
- │  thread-review.ts D
189
- │  README.md M
188
+ │  thread-review.ts D
189
+ │  README.md M
190
190
  │
191
191
  │
192
192
  │
@@ -555,7 +555,7 @@ exports[`stories catalog Chrome/DiscussionMarkerRail/Dots renders, snapshots, an
555
555
  `;
556
556
 
557
557
  exports[`stories catalog Surfaces/EditorGrid/OneGroup renders, snapshots, and carries its colors: Surfaces/EditorGrid/OneGroup 1`] = `
558
- " Changes │ App.tsx │ ⛶
558
+ " Changes │ App.tsx │ split ⛶
559
559
  ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
560
560
 
561
561
  the whole diff
@@ -575,22 +575,42 @@ exports[`stories catalog Surfaces/EditorGrid/OneGroup renders, snapshots, and ca
575
575
  `;
576
576
 
577
577
  exports[`stories catalog Surfaces/EditorGrid/SplitGroups renders, snapshots, and carries its colors: Surfaces/EditorGrid/SplitGroups 1`] = `
578
- " plugin.json │ split ⛶ │ plugin.json │ split ⛶
579
- ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾│‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
580
- │
581
- .claude-plugin/plugin.json │ .claude-plugin/plugin.json
582
- │
583
- │
584
- │
585
- │
586
- │
587
- │
588
- │
589
- │
590
- │
591
- │
592
- │
593
- │
578
+ " Changes │ split │ plugin.json │ split ⛶
579
+ ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾│‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
580
+ │
581
+ the whole diff │ .claude-plugin/plugin.json
582
+ │
583
+ │
584
+ │
585
+ │
586
+ │
587
+ │
588
+ │
589
+ │
590
+ │
591
+ │
592
+ │
593
+ │
594
+ "
595
+ `;
596
+
597
+ exports[`stories catalog Surfaces/EditorGrid/WholeCellSizing renders, snapshots, and carries its colors: Surfaces/EditorGrid/WholeCellSizing 1`] = `
598
+ "Whole-cell split sizing
599
+ 100 cells = 49 left + 1 divider + 50 right
600
+ Fix the leading width; let the last group take the remainder.
601
+ Changes │ split │ plugin.json │ split ⛶
602
+ ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾│‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
603
+ │
604
+ the whole diff │ .claude-plugin/plugin.json
605
+ │
606
+ │
607
+ │
608
+ │
609
+ │
610
+ │
611
+ │
612
+ │
613
+ │
594
614
  "
595
615
  `;
596
616