feat(npcs): free rotate overlays and drop session zoom tools

Drag the top-edge handle to rotate the whole frame with the avatar; remove unused zoom controls from the session NPC window.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-07-21 14:22:46 +08:00
parent 1829191410
commit 0cab7ce7ca
7 changed files with 217 additions and 111 deletions
+6 -74
View File
@@ -12,32 +12,6 @@ import { useAssetUrl } from '../shared/useAssetImageUrl';
import styles from './NpcsApp.module.css';
function ZoomInIcon() {
return (
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden>
<circle cx="10.5" cy="10.5" r="6.25" fill="none" stroke="currentColor" strokeWidth="1.8" />
<path d="M15.2 15.2 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
<path
d="M10.5 7.8v5.4M7.8 10.5h5.4"
fill="none"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
/>
</svg>
);
}
function ZoomOutIcon() {
return (
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden>
<circle cx="10.5" cy="10.5" r="6.25" fill="none" stroke="currentColor" strokeWidth="1.8" />
<path d="M15.2 15.2 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
<path d="M7.8 10.5h5.4" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
</svg>
);
}
/** Убрать пустые ветки групп (удобно при поиске). */
function pruneEmptyGroupNodes(nodes: NpcGroupTreeNode[]): NpcGroupTreeNode[] {
const out: NpcGroupTreeNode[] = [];
@@ -155,7 +129,6 @@ export function NpcsApp() {
const activeIds = overlay?.activeNpcIds ?? [];
const hasActive = activeIds.length > 0;
const zoomTool = overlay?.zoomTool ?? null;
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
@@ -164,16 +137,12 @@ export function NpcsApp() {
void overlayApi.dispatch({ kind: 'hide' });
return;
}
if (zoomTool) {
void overlayApi.dispatch({ kind: 'zoomTool.set', tool: null });
return;
}
window.close();
}
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [hasActive, zoomTool, overlayApi]);
}, [hasActive, overlayApi]);
const npcs = useMemo(() => session?.project?.npcs ?? [], [session?.project?.npcs]);
const npcGroups = useMemo(() => session?.project?.npcGroups ?? [], [session?.project?.npcGroups]);
@@ -245,39 +214,9 @@ export function NpcsApp() {
return (
<div className={styles.page}>
<div className={styles.toolbar}>
<div className={styles.toolbarRow}>
<Button
variant={zoomTool === 'zoomIn' ? 'primary' : 'ghost'}
iconOnly
title={t('npcs.zoomIn')}
ariaLabel={t('npcs.zoomIn')}
tooltipPlacement="bottom"
onClick={() => {
void overlayApi.dispatch({
kind: 'zoomTool.set',
tool: zoomTool === 'zoomIn' ? null : 'zoomIn',
});
}}
>
<ZoomInIcon />
</Button>
<Button
variant={zoomTool === 'zoomOut' ? 'primary' : 'ghost'}
iconOnly
title={t('npcs.zoomOut')}
ariaLabel={t('npcs.zoomOut')}
tooltipPlacement="bottom"
onClick={() => {
void overlayApi.dispatch({
kind: 'zoomTool.set',
tool: zoomTool === 'zoomOut' ? null : 'zoomOut',
});
}}
>
<ZoomOutIcon />
</Button>
{hasActive ? (
{hasActive ? (
<div className={styles.toolbar}>
<div className={styles.toolbarRow}>
<Button
title={t('npcs.closeOverlay')}
ariaLabel={t('npcs.closeOverlay')}
@@ -288,16 +227,9 @@ export function NpcsApp() {
>
{t('npcs.closeOverlay')}
</Button>
) : null}
</div>
</div>
<div className={styles.toolbarHint}>
{zoomTool === 'zoomIn'
? t('npcs.zoomInHint')
: zoomTool === 'zoomOut'
? t('npcs.zoomOutHint')
: t('npcs.zoomIdleHint')}
</div>
</div>
) : null}
<div className={styles.body}>
<div className={styles.detail}>