Skip to content

Commit ac6e2b5

Browse files
authored
Fix confirm close dialog (#14)
2 parents 50fef9f + 2a32566 commit ac6e2b5

File tree

4 files changed

+42
-24
lines changed

4 files changed

+42
-24
lines changed

docs/specs/layout.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ All handled in a single capture-phase `keydown` listener on `window`. Every hand
167167

168168
### Kill confirmation
169169

170-
Pressing `x` shows a pane-centered semi-transparent overlay (`KillConfirmOverlay``KillConfirmCard`) with a random uppercase letter (A-Z, excluding X). Typing that letter confirms the kill (destroys session, removes pane). Escape cancels.
170+
Pressing `x` (or clicking the kill button) enters command mode and shows a pane-centered semi-transparent overlay (`KillConfirmOverlay``KillConfirmCard`) with a random uppercase letter (A-Z, excluding X). Typing that letter confirms the kill (destroys session, removes pane). Cancel with Escape key, clicking the `[ESC] to cancel` button, or clicking another panel. Any other key triggers a shake animation (400ms `shake-x` keyframe) then auto-dismisses the confirmation.
171171

172172
## Selection overlay
173173

lib/src/components/Pond.tsx

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ function toDetachedItem(item: PersistedDetachedItem): DetachedItem {
7676
interface ConfirmKill {
7777
id: string;
7878
char: string;
79+
shaking?: boolean;
7980
}
8081

8182
export type PondMode = 'command' | 'passthrough';
@@ -887,24 +888,25 @@ function SelectionOverlay({ apiRef, selectedId, selectedType, mode }: {
887888

888889
// --- Kill confirmation overlay ---
889890

890-
function KillConfirmCard({ char }: { char: string }) {
891+
export function KillConfirmCard({ char, onCancel, shaking }: { char: string; onCancel?: () => void; shaking?: boolean }) {
891892
return (
892-
<div className="bg-surface-raised border border-error/30 px-6 py-4 rounded-lg text-center shadow-lg">
893-
<h2 className="text-sm font-bold mb-2 text-foreground">Kill Session?</h2>
893+
<div className={`bg-surface-raised border border-error/30 px-6 py-4 rounded-lg text-center shadow-lg${shaking ? ' motion-safe:animate-shake-x' : ''}`}>
894+
<h2 className="text-base font-bold mb-3 text-foreground">Kill Session?</h2>
894895
<div className="bg-black py-2 px-6 rounded border border-border inline-block mb-2">
895-
<span className="text-2xl font-black text-error">{char}</span>
896+
<span className="text-xl font-bold text-error">{char}</span>
896897
</div>
897-
<div className="text-[9px] text-muted uppercase tracking-widest leading-relaxed">
898+
<div className="text-xs text-muted uppercase tracking-widest leading-relaxed">
898899
<div>[{char}] to confirm</div>
899-
<div>[ESC] to cancel</div>
900+
<button type="button" onClick={onCancel} className="uppercase hover:text-foreground transition-colors cursor-pointer">[ESC] to cancel</button>
900901
</div>
901902
</div>
902903
);
903904
}
904905

905-
function KillConfirmOverlay({ confirmKill, panelElements }: {
906+
function KillConfirmOverlay({ confirmKill, panelElements, onCancel }: {
906907
confirmKill: ConfirmKill;
907908
panelElements: Map<string, HTMLElement>;
909+
onCancel: () => void;
908910
}) {
909911
const [rect, setRect] = useState<{ top: number; left: number; width: number; height: number } | null>(null);
910912

@@ -930,15 +932,15 @@ function KillConfirmOverlay({ confirmKill, panelElements }: {
930932
style={{ position: 'fixed', top: rect.top, left: rect.left, width: rect.width, height: rect.height, zIndex: 100 }}
931933
className="flex items-center justify-center bg-surface/50 rounded"
932934
>
933-
<KillConfirmCard char={confirmKill.char} />
935+
<KillConfirmCard char={confirmKill.char} onCancel={onCancel} shaking={confirmKill.shaking} />
934936
</div>
935937
);
936938
}
937939

938940
// Fallback: centered in viewport
939941
return (
940942
<div className="fixed inset-0 bg-surface/50 z-[100] flex items-center justify-center">
941-
<KillConfirmCard char={confirmKill.char} />
943+
<KillConfirmCard char={confirmKill.char} onCancel={onCancel} shaking={confirmKill.shaking} />
942944
</div>
943945
);
944946
}
@@ -998,6 +1000,7 @@ export function Pond({
9981000

9991001
// UI state
10001002
const [confirmKill, setConfirmKill] = useState<ConfirmKill | null>(null);
1003+
useEffect(() => { if (!confirmKill) { clearTimeout(shakeTimerRef.current!); } }, [confirmKill]);
10011004
const [renamingPaneId, setRenamingPaneId] = useState<string | null>(null);
10021005
const [detached, setDetached] = useState<DetachedItem[]>(() => (initialDetached ?? []).map(toDetachedItem));
10031006
const [zoomed, setZoomed] = useState(false);
@@ -1022,6 +1025,7 @@ export function Pond({
10221025
confirmKillRef.current = confirmKill;
10231026
const renamingRef = useRef(renamingPaneId);
10241027
renamingRef.current = renamingPaneId;
1028+
const shakeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
10251029
const sessionSaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
10261030
const sessionSavePromiseRef = useRef<Promise<void> | null>(null);
10271031

@@ -1410,8 +1414,14 @@ export function Pond({
14101414
} else {
14111415
setSelectedId(null);
14121416
}
1417+
setConfirmKill(null);
1418+
return;
1419+
}
1420+
// Wrong key — shake then dismiss
1421+
if (!ck.shaking) {
1422+
setConfirmKill({ ...ck, shaking: true });
1423+
shakeTimerRef.current = setTimeout(() => setConfirmKill(null), 400);
14131424
}
1414-
setConfirmKill(null);
14151425
return;
14161426
}
14171427

@@ -1745,6 +1755,7 @@ export function Pond({
17451755

17461756
const pondActions: PondActions = useMemo(() => ({
17471757
onKill: (id: string) => {
1758+
exitTerminalMode();
17481759
const char = randomKillChar();
17491760
setConfirmKill({ id, char });
17501761
},
@@ -1775,6 +1786,7 @@ export function Pond({
17751786
}
17761787
},
17771788
onClickPanel: (id: string) => {
1789+
setConfirmKill(null);
17781790
enterTerminalMode(id);
17791791
},
17801792
onStartRename: (id: string) => {
@@ -1790,7 +1802,7 @@ export function Pond({
17901802
onCancelRename: () => {
17911803
setRenamingPaneId(null);
17921804
},
1793-
}), [addSplitPanel, detachPanel, enterTerminalMode]);
1805+
}), [addSplitPanel, detachPanel, enterTerminalMode, exitTerminalMode]);
17941806
const pondActionsRef = useRef(pondActions);
17951807
pondActionsRef.current = pondActions;
17961808

@@ -1827,6 +1839,7 @@ export function Pond({
18271839
<KillConfirmOverlay
18281840
confirmKill={confirmKill}
18291841
panelElements={panelElements}
1842+
onCancel={() => setConfirmKill(null)}
18301843
/>
18311844
)}
18321845

lib/src/stories/KillModal.stories.tsx

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,17 @@
11
import type { Meta, StoryObj } from '@storybook/react';
2+
import { KillConfirmCard } from '../components/Pond';
23

3-
function KillModal({ char = 'G' }: { char?: string }) {
4+
function KillModal({ char = 'G', onCancel, shaking }: { char?: string; onCancel?: () => void; shaking?: boolean }) {
45
return (
56
<div className="relative bg-surface" style={{ width: 600, height: 400 }}>
67
{/* Simulated terminal content behind the overlay */}
7-
<div className="p-4 font-mono text-[11px] text-terminal-fg">
8+
<div className="p-4 font-mono text-xs text-terminal-fg">
89
<div>user@mouseterm:~$ npm run build</div>
910
<div className="text-muted">Building project...</div>
1011
</div>
1112
{/* Kill confirmation overlay — positioned over the pane */}
1213
<div className="absolute inset-0 flex items-center justify-center bg-black/50 rounded">
13-
<div className="bg-surface-raised border border-error/30 px-6 py-4 rounded-lg text-center shadow-lg">
14-
<h2 className="text-sm font-bold mb-2 text-foreground">Kill Session?</h2>
15-
<div className="bg-black py-2 px-6 rounded border border-border inline-block mb-2">
16-
<span className="text-2xl font-black text-error">{char}</span>
17-
</div>
18-
<div className="text-[9px] text-muted uppercase tracking-widest leading-relaxed">
19-
<div>[{char}] to confirm</div>
20-
<div>[ESC] to cancel</div>
21-
</div>
22-
</div>
14+
<KillConfirmCard char={char} onCancel={onCancel} shaking={shaking} />
2315
</div>
2416
</div>
2517
);
@@ -43,3 +35,7 @@ export const Default: Story = {
4335
export const RandomChar: Story = {
4436
args: { char: 'W' },
4537
};
38+
39+
export const Shaking: Story = {
40+
args: { char: 'G', shaking: true },
41+
};

lib/src/theme.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868

6969
/* Animation */
7070
--animate-alarm-dot: alarm-dot 2s ease-in-out infinite;
71+
--animate-shake-x: shake-x 400ms ease-out;
7172
}
7273

7374
/* --- Light mode fallback defaults ---
@@ -136,3 +137,11 @@ body.vscode-light {
136137
0%, 100% { opacity: 1; }
137138
50% { opacity: 0.4; }
138139
}
140+
141+
@keyframes shake-x {
142+
0%, 100% { translate: 0; }
143+
20% { translate: -6px; }
144+
40% { translate: 5px; }
145+
60% { translate: -3px; }
146+
80% { translate: 2px; }
147+
}

0 commit comments

Comments
 (0)