Skip to content

Commit ee451b3

Browse files
authored
Storybook cleanup (#44)
2 parents 15c1f69 + 1ca36ba commit ee451b3

5 files changed

Lines changed: 235 additions & 185 deletions

File tree

lib/src/stories/AppBar.stories.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ const DEFAULT_SHELLS = [
77
{ name: 'fish', path: '/usr/bin/fish' },
88
];
99

10+
function wait(ms: number) {
11+
return new Promise((resolve) => setTimeout(resolve, ms));
12+
}
13+
14+
async function openShellSelector({ canvasElement }: { canvasElement: HTMLElement }) {
15+
await wait(100);
16+
const shellButton = Array.from(canvasElement.querySelectorAll<HTMLButtonElement>('button[aria-haspopup="menu"]'))
17+
.find((button) => DEFAULT_SHELLS.some((shell) => button.textContent?.includes(shell.name)));
18+
shellButton?.click();
19+
await wait(100);
20+
}
21+
1022
function AppBarStory(props: React.ComponentProps<typeof AppBar>) {
1123
return (
1224
<div style={{ width: '100%' }}>
@@ -32,6 +44,7 @@ export const SingleShell: Story = {
3244
args: {
3345
shells: [{ name: 'bash', path: '/bin/bash' }],
3446
},
47+
play: openShellSelector,
3548
};
3649

3750
export const ManyShells: Story = {
@@ -44,4 +57,5 @@ export const ManyShells: Story = {
4457
{ name: 'nu', path: '/usr/bin/nu' },
4558
],
4659
},
60+
play: openShellSelector,
4761
};
Lines changed: 129 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,147 @@
1-
import { useState, useRef, useEffect } from 'react';
1+
import { useEffect, useRef } from 'react';
22
import type { Meta, StoryObj } from '@storybook/react';
3-
import type { WallMode } from '../components/Wall';
4-
import { MarchingAntsRect } from '../components/Wall';
3+
import '@xterm/xterm/css/xterm.css';
4+
import { SelectionOverlay } from '../components/SelectionOverlay';
5+
import {
6+
focusSession,
7+
getOrCreateTerminal,
8+
getTerminalOverlayDims,
9+
mountElement,
10+
refitSession,
11+
unmountElement,
12+
} from '../lib/terminal-registry';
13+
import { flattenScenario, SCENARIO_LS_OUTPUT } from '../lib/platform';
14+
import {
15+
setHintToken,
16+
setSelection,
17+
type Selection,
18+
type TokenHint,
19+
} from '../lib/mouse-selection';
20+
import { TERMINAL_BOTTOM_RADIUS_CLASS } from '../components/design';
521

6-
function SelectionOverlayDemo({ initialMode = 'command' as WallMode }) {
7-
const [mode, setMode] = useState<WallMode>(initialMode);
8-
const containerRef = useRef<HTMLDivElement>(null);
9-
const [size, setSize] = useState({ width: 484, height: 284 });
22+
function SelectionOverlayStory({
23+
id,
24+
selection,
25+
hintToken = null,
26+
}: {
27+
id: string;
28+
selection: Omit<Selection, 'startedInScrollback'>;
29+
hintToken?: TokenHint | null;
30+
}) {
31+
const terminalHostRef = useRef<HTMLDivElement>(null);
1032

1133
useEffect(() => {
12-
const el = containerRef.current;
13-
if (!el) return;
14-
const ro = new ResizeObserver(([entry]) => {
15-
setSize({ width: entry.contentRect.width - 16, height: entry.contentRect.height - 16 });
16-
});
17-
ro.observe(el);
18-
return () => ro.disconnect();
19-
}, []);
20-
21-
const color = getComputedStyle(document.documentElement).getPropertyValue('--color-header-active-bg').trim() || '#094771';
22-
23-
const overlayStyle: React.CSSProperties = {
24-
position: 'absolute',
25-
inset: 8,
26-
borderRadius: '0.5rem',
27-
pointerEvents: 'none',
28-
transition: 'border 150ms, box-shadow 150ms',
29-
};
30-
31-
if (mode === 'passthrough') {
32-
overlayStyle.border = `2px solid ${color}`;
33-
overlayStyle.boxShadow = `0 0 15px color-mix(in srgb, ${color} 30%, transparent)`;
34-
}
34+
const terminalHost = terminalHostRef.current;
35+
if (!terminalHost) return;
36+
37+
getOrCreateTerminal(id);
38+
mountElement(id, terminalHost);
39+
40+
const observer = new ResizeObserver(() => refitSession(id));
41+
observer.observe(terminalHost);
42+
43+
return () => {
44+
observer.disconnect();
45+
unmountElement(id);
46+
};
47+
}, [id]);
48+
49+
useEffect(() => {
50+
focusSession(id, true);
51+
}, [id]);
52+
53+
useEffect(() => {
54+
let cancelled = false;
55+
let timer: ReturnType<typeof setTimeout>;
56+
57+
const applySelection = () => {
58+
if (cancelled) return;
59+
const dims = getTerminalOverlayDims(id);
60+
if (!dims || dims.cellHeight === 0) {
61+
timer = setTimeout(applySelection, 50);
62+
return;
63+
}
64+
65+
setSelection(id, { ...selection, startedInScrollback: false });
66+
setHintToken(id, hintToken);
67+
};
68+
69+
timer = setTimeout(applySelection, 100);
70+
return () => {
71+
cancelled = true;
72+
clearTimeout(timer);
73+
setSelection(id, null);
74+
setHintToken(id, null);
75+
};
76+
}, [id, selection, hintToken]);
3577

3678
return (
37-
<div ref={containerRef} style={{ width: 500, height: 300 }} className="relative bg-app-bg">
38-
{/* Simulated terminal content */}
39-
<div className="p-4 font-mono text-sm text-terminal-fg">
40-
<div>user@mouseterm:~$ ls -la</div>
41-
<div>total 48</div>
42-
<div>drwxr-xr-x 12 user staff 384 Mar 16 10:30 .</div>
43-
</div>
44-
{/* Selection overlay */}
45-
{mode === 'command' ? (
46-
<div style={{ position: 'absolute', inset: 8, pointerEvents: 'none' }}>
47-
<MarchingAntsRect width={size.width} height={size.height} isDoor={false} color={color} />
48-
</div>
49-
) : (
50-
<div style={overlayStyle} />
51-
)}
52-
{/* Mode toggle */}
53-
<div className="absolute bottom-2 right-2 flex gap-2">
54-
<button
55-
className={`px-3 py-1 rounded text-sm font-mono ${mode === 'passthrough' ? 'bg-header-active-bg text-header-active-fg' : 'bg-header-inactive-bg text-header-inactive-fg'}`}
56-
onClick={() => setMode('passthrough')}
57-
>passthrough</button>
58-
<button
59-
className={`px-3 py-1 rounded text-sm font-mono ${mode === 'command' ? 'bg-header-active-bg text-header-active-fg' : 'bg-header-inactive-bg text-header-inactive-fg'}`}
60-
onClick={() => setMode('command')}
61-
>command</button>
62-
</div>
79+
<div
80+
className={`relative bg-terminal-bg ${TERMINAL_BOTTOM_RADIUS_CLASS}`}
81+
style={{ width: 620, height: 340 }}
82+
>
83+
<div ref={terminalHostRef} className="h-full w-full" />
84+
<SelectionOverlay terminalId={id} />
6385
</div>
6486
);
6587
}
6688

67-
const meta: Meta<typeof SelectionOverlayDemo> = {
89+
const meta: Meta<typeof SelectionOverlayStory> = {
6890
title: 'Components/SelectionOverlay',
69-
component: SelectionOverlayDemo,
91+
component: SelectionOverlayStory,
92+
parameters: {
93+
fakePty: { scenario: flattenScenario(SCENARIO_LS_OUTPUT) },
94+
},
7095
};
7196

7297
export default meta;
73-
type Story = StoryObj<typeof SelectionOverlayDemo>;
98+
type Story = StoryObj<typeof SelectionOverlayStory>;
99+
100+
export const LinewiseDrag: Story = {
101+
args: {
102+
id: 'selection-overlay-linewise-drag',
103+
selection: {
104+
startRow: 2,
105+
startCol: 5,
106+
endRow: 6,
107+
endCol: 24,
108+
shape: 'linewise',
109+
dragging: true,
110+
},
111+
},
112+
};
74113

75-
export const CommandMode: Story = {
76-
args: { initialMode: 'command' },
114+
export const BlockDrag: Story = {
115+
args: {
116+
id: 'selection-overlay-block-drag',
117+
selection: {
118+
startRow: 2,
119+
startCol: 6,
120+
endRow: 5,
121+
endCol: 26,
122+
shape: 'block',
123+
dragging: true,
124+
},
125+
},
77126
};
78127

79-
export const PassthroughMode: Story = {
80-
args: { initialMode: 'passthrough' },
128+
export const SmartPathHint: Story = {
129+
args: {
130+
id: 'selection-overlay-smart-path-hint',
131+
selection: {
132+
startRow: 2,
133+
startCol: 5,
134+
endRow: 6,
135+
endCol: 24,
136+
shape: 'linewise',
137+
dragging: true,
138+
},
139+
hintToken: {
140+
kind: 'path',
141+
row: 8,
142+
startCol: 35,
143+
endCol: 38,
144+
text: 'src',
145+
},
146+
},
81147
};

lib/src/stories/TerminalPaneHeader.stories.tsx

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,6 @@ async function openAlertRightClickDialog() {
9292
await wait(100);
9393
}
9494

95-
async function clickTodoPill() {
96-
await wait(100);
97-
const todoButton = document.querySelector<HTMLButtonElement>(`[data-session-todo-for="${SESSION_ID}"]`);
98-
todoButton?.click();
99-
await wait(100);
100-
}
101-
10295
const meta: Meta<typeof TabStory> = {
10396
title: 'Components/TerminalPaneHeader',
10497
component: TabStory,
@@ -179,14 +172,6 @@ export const AlertRightClickDialog: Story = {
179172
play: openAlertRightClickDialog,
180173
};
181174

182-
export const TodoClickToDismiss: Story = {
183-
parameters: primedState({
184-
status: 'NOTHING_TO_SHOW',
185-
todo: true,
186-
}),
187-
play: clickTodoPill,
188-
};
189-
190175
export const TodoOnly: Story = {
191176
parameters: primedState({
192177
status: 'ALERT_DISABLED',

lib/src/stories/UpdateBanner.stories.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Meta, StoryObj } from '@storybook/react';
22
import { UpdateBanner, type UpdateBannerState } from '../../../standalone/src/UpdateBanner';
33

4-
function UpdateBannerStory({ state }: { state: UpdateBannerState }) {
4+
function UpdateBannerStory({ state, expectedNullReason }: { state: UpdateBannerState; expectedNullReason?: string }) {
55
return (
66
<div className="bg-app-bg" style={{ width: '100%' }}>
77
<UpdateBanner
@@ -10,6 +10,11 @@ function UpdateBannerStory({ state }: { state: UpdateBannerState }) {
1010
onOpenChangelog={() => console.log('Open changelog')}
1111
onOpenDebug={() => console.log('Open debug')}
1212
/>
13+
{expectedNullReason ? (
14+
<div className="inline-flex border border-dashed border-border bg-surface-raised px-2 py-1 font-mono text-xs text-muted">
15+
Expected empty banner: {expectedNullReason}
16+
</div>
17+
) : null}
1318
</div>
1419
);
1520
}
@@ -43,18 +48,14 @@ export const PostUpdateFailure: Story = {
4348
export const Idle: Story = {
4449
args: {
4550
state: { status: 'idle' },
51+
expectedNullReason: 'idle has no update notice to show.',
4652
},
4753
};
4854

4955
export const Dismissed: Story = {
5056
args: {
5157
state: { status: 'dismissed' },
52-
},
53-
};
54-
55-
export const LongVersionString: Story = {
56-
args: {
57-
state: { status: 'downloaded', version: '1.23.456-beta.7+build.2025.04.10' },
58+
expectedNullReason: 'the user has already dismissed this notice.',
5859
},
5960
};
6061

0 commit comments

Comments
 (0)