diff --git a/src/components/EffortPanel/EffortPanel.tsx b/src/components/EffortPanel/EffortPanel.tsx index 15328cd2d..eff563199 100644 --- a/src/components/EffortPanel/EffortPanel.tsx +++ b/src/components/EffortPanel/EffortPanel.tsx @@ -14,12 +14,28 @@ import { import { executeEffort } from '../../commands/effort/effort.js'; import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'; import { useSetAppState } from '../../state/AppState.js'; +import { useRippleFrame } from './useRippleFrame.js'; +import { type Overlay, computeRippleLine, mergeLayers } from './rippleAnimation.js'; // 每档固定宽度,Ink Box 自动对齐。PANEL_WIDTH = SEGMENT * 6。 const SEGMENT = 12; const PANEL_WIDTH = SEGMENT * PANEL_POSITIONS.length; const SUBLABEL_ULTRACODE = 'xhigh + workflows'; +// 波纹震源坐标(相对波纹区域坐标系,y=0 是档位名行)。 +// ultracode 字符在 SEGMENT*5=60 起始段内居中(9 字符 in 12 列 → 偏移 1.5 → 1), +// 中心列 ≈ 60 + 1 + 4 = 65。 +const RIPPLE_SOURCE_X = SEGMENT * 5 + 5; +const RIPPLE_SOURCE_Y = 0; + +/** + * 计算某段 idx 内居中文字的起始列。 + * 'ultracode' = 9 字符 in SEGMENT=12 → offset = floor((12-9)/2) = 1。 + */ +function segmentTextStartX(idx: number, textLen: number): number { + return SEGMENT * idx + Math.max(0, Math.floor((SEGMENT - textLen) / 2)); +} + type Props = { appStateEffort: EffortValue | undefined; onDone: (message: string) => void; @@ -36,6 +52,9 @@ export function EffortPanel({ appStateEffort, onDone }: Props): React.ReactNode const [cursor, setCursor] = React.useState(initialCursor); const [done, setDone] = React.useState(false); + const rippleActive = cursor === 'ultracode'; + const [rippleRef, time] = useRippleFrame(rippleActive); + const handleConfirm = React.useCallback(() => { if (done) return; setDone(true); @@ -70,19 +89,49 @@ export function EffortPanel({ appStateEffort, onDone }: Props): React.ReactNode const envActive = envOverride !== null && envOverride !== undefined; const envRaw = process.env.CLAUDE_CODE_EFFORT_LEVEL; + // 波纹行渲染:返回 merge 后的单字符串。 + const renderRippleLine = React.useCallback( + (relY: number, overlays: Overlay[]): string => { + const ripple = computeRippleLine({ + y: relY + RIPPLE_SOURCE_Y, + width: PANEL_WIDTH, + time, + sourceX: RIPPLE_SOURCE_X, + sourceY: RIPPLE_SOURCE_Y, + }); + return mergeLayers(ripple, overlays); + }, + [time], + ); + return ( - + Effort {envActive && {`⚠ CLAUDE_CODE_EFFORT_LEVEL=${envRaw} overrides this session`}} + {rippleActive ? ( + + ) : ( + + )} + + ←/→ adjust · Enter confirm · Esc cancel + + + ); +} + +// ---- 普通模式(无波纹)---- + +function PlainContent({ cursor }: { cursor: PanelPosition }): React.ReactNode { + return ( + <> Faster Smarter - {/* 分隔线 */} {'─'.repeat(PANEL_WIDTH)} - {/* ▲ 行:每段独立居中,与下方档位文字严格对齐 */} {PANEL_POSITIONS.map(p => ( @@ -92,7 +141,6 @@ export function EffortPanel({ appStateEffort, onDone }: Props): React.ReactNode ))} - {/* 档位名:选中 suggestion + bold,未选中 subtle */} {PANEL_POSITIONS.map(p => ( @@ -102,16 +150,78 @@ export function EffortPanel({ appStateEffort, onDone }: Props): React.ReactNode ))} - {/* ultracode 副标签:右对齐到最末段 */} {SUBLABEL_ULTRACODE} - - ←/→ adjust · Enter confirm · Esc cancel - - + + ); +} + +// ---- 波纹模式(cursor === 'ultracode')---- + +type RippleContentProps = { + time: number; + renderLine: (relY: number, overlays: Overlay[]) => string; + cursor: PanelPosition; +}; + +function RippleContent({ renderLine }: RippleContentProps): React.ReactNode { + // 各档位名 overlay(基于段中心对齐) + const labelOverlays: Overlay[] = PANEL_POSITIONS.map((p, idx) => ({ + text: p, + x: segmentTextStartX(idx, p.length), + })); + + // ▲ overlay:放在 ultracode 段中心 + const cursorIdx = PANEL_POSITIONS.indexOf('ultracode'); + const cursorOverlay: Overlay = { + text: '▲', + x: segmentTextStartX(cursorIdx, 1), + }; + + // 副标签 overlay:放在 ultracode 段中心 + const sublabelOverlay: Overlay = { + text: SUBLABEL_ULTRACODE, + x: segmentTextStartX(cursorIdx, SUBLABEL_ULTRACODE.length), + }; + + // Faster / Smarter overlay + const fasterOverlay: Overlay = { text: 'Faster', x: 0 }; + const smarterOverlay: Overlay = { + text: 'Smarter', + x: PANEL_WIDTH - 'Smarter'.length, + }; + + // 分隔线 overlay + const separatorOverlay: Overlay = { + text: '─'.repeat(PANEL_WIDTH), + x: 0, + }; + + // 各行 y 坐标(相对震源 RIPPLE_SOURCE_Y = 档位名行) + // y=-3: Faster/Smarter + // y=-2: 分隔线 + // y=-1: ▲ + // y=0: 档位名(震源) + // y=1: 副标签 + const fasterLine = renderLine(-3, [fasterOverlay, smarterOverlay]); + const separatorLine = renderLine(-2, [separatorOverlay]); + const cursorLine = renderLine(-1, [cursorOverlay]); + const labelLine = renderLine(0, labelOverlays); + const sublabelLine = renderLine(1, [sublabelOverlay]); + + return ( + <> + {fasterLine} + {separatorLine} + {cursorLine} + + {labelLine} + + {sublabelLine} + ); }