Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/specs/layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ All handled in a single capture-phase `keydown` listener on `window`. Every hand

### Kill confirmation

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.
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.

## Selection overlay

Expand Down
35 changes: 24 additions & 11 deletions lib/src/components/Pond.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ function toDetachedItem(item: PersistedDetachedItem): DetachedItem {
interface ConfirmKill {
id: string;
char: string;
shaking?: boolean;
}

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

// --- Kill confirmation overlay ---

function KillConfirmCard({ char }: { char: string }) {
export function KillConfirmCard({ char, onCancel, shaking }: { char: string; onCancel?: () => void; shaking?: boolean }) {
return (
<div className="bg-surface-raised border border-error/30 px-6 py-4 rounded-lg text-center shadow-lg">
<h2 className="text-sm font-bold mb-2 text-foreground">Kill Session?</h2>
<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' : ''}`}>
<h2 className="text-base font-bold mb-3 text-foreground">Kill Session?</h2>
<div className="bg-black py-2 px-6 rounded border border-border inline-block mb-2">
<span className="text-2xl font-black text-error">{char}</span>
<span className="text-xl font-bold text-error">{char}</span>
</div>
<div className="text-[9px] text-muted uppercase tracking-widest leading-relaxed">
<div className="text-xs text-muted uppercase tracking-widest leading-relaxed">
<div>[{char}] to confirm</div>
<div>[ESC] to cancel</div>
<button type="button" onClick={onCancel} className="uppercase hover:text-foreground transition-colors cursor-pointer">[ESC] to cancel</button>
</div>
</div>
);
}

function KillConfirmOverlay({ confirmKill, panelElements }: {
function KillConfirmOverlay({ confirmKill, panelElements, onCancel }: {
confirmKill: ConfirmKill;
panelElements: Map<string, HTMLElement>;
onCancel: () => void;
}) {
const [rect, setRect] = useState<{ top: number; left: number; width: number; height: number } | null>(null);

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

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

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

Expand Down Expand Up @@ -1410,8 +1414,14 @@ export function Pond({
} else {
setSelectedId(null);
}
setConfirmKill(null);
return;
}
// Wrong key — shake then dismiss
if (!ck.shaking) {
setConfirmKill({ ...ck, shaking: true });
shakeTimerRef.current = setTimeout(() => setConfirmKill(null), 400);
}
setConfirmKill(null);
return;
}

Expand Down Expand Up @@ -1745,6 +1755,7 @@ export function Pond({

const pondActions: PondActions = useMemo(() => ({
onKill: (id: string) => {
exitTerminalMode();
const char = randomKillChar();
setConfirmKill({ id, char });
},
Expand Down Expand Up @@ -1775,6 +1786,7 @@ export function Pond({
}
},
onClickPanel: (id: string) => {
setConfirmKill(null);
enterTerminalMode(id);
},
onStartRename: (id: string) => {
Expand All @@ -1790,7 +1802,7 @@ export function Pond({
onCancelRename: () => {
setRenamingPaneId(null);
},
}), [addSplitPanel, detachPanel, enterTerminalMode]);
}), [addSplitPanel, detachPanel, enterTerminalMode, exitTerminalMode]);
const pondActionsRef = useRef(pondActions);
pondActionsRef.current = pondActions;

Expand Down Expand Up @@ -1827,6 +1839,7 @@ export function Pond({
<KillConfirmOverlay
confirmKill={confirmKill}
panelElements={panelElements}
onCancel={() => setConfirmKill(null)}
/>
)}

Expand Down
20 changes: 8 additions & 12 deletions lib/src/stories/KillModal.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,17 @@
import type { Meta, StoryObj } from '@storybook/react';
import { KillConfirmCard } from '../components/Pond';

function KillModal({ char = 'G' }: { char?: string }) {
function KillModal({ char = 'G', onCancel, shaking }: { char?: string; onCancel?: () => void; shaking?: boolean }) {
return (
<div className="relative bg-surface" style={{ width: 600, height: 400 }}>
{/* Simulated terminal content behind the overlay */}
<div className="p-4 font-mono text-[11px] text-terminal-fg">
<div className="p-4 font-mono text-xs text-terminal-fg">
<div>user@mouseterm:~$ npm run build</div>
<div className="text-muted">Building project...</div>
</div>
{/* Kill confirmation overlay — positioned over the pane */}
<div className="absolute inset-0 flex items-center justify-center bg-black/50 rounded">
<div className="bg-surface-raised border border-error/30 px-6 py-4 rounded-lg text-center shadow-lg">
<h2 className="text-sm font-bold mb-2 text-foreground">Kill Session?</h2>
<div className="bg-black py-2 px-6 rounded border border-border inline-block mb-2">
<span className="text-2xl font-black text-error">{char}</span>
</div>
<div className="text-[9px] text-muted uppercase tracking-widest leading-relaxed">
<div>[{char}] to confirm</div>
<div>[ESC] to cancel</div>
</div>
</div>
<KillConfirmCard char={char} onCancel={onCancel} shaking={shaking} />
</div>
</div>
);
Expand All @@ -43,3 +35,7 @@ export const Default: Story = {
export const RandomChar: Story = {
args: { char: 'W' },
};

export const Shaking: Story = {
args: { char: 'G', shaking: true },
};
9 changes: 9 additions & 0 deletions lib/src/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@

/* Animation */
--animate-alarm-dot: alarm-dot 2s ease-in-out infinite;
--animate-shake-x: shake-x 400ms ease-out;
}

/* --- Light mode fallback defaults ---
Expand Down Expand Up @@ -136,3 +137,11 @@ body.vscode-light {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}

@keyframes shake-x {
0%, 100% { translate: 0; }
20% { translate: -6px; }
40% { translate: 5px; }
60% { translate: -3px; }
80% { translate: 2px; }
}
Loading