refactor(editor): improve selection parity and exhaustiveness

This commit is contained in:
clawdbot 2026-01-27 23:12:47 +08:00
parent 38119cbe55
commit 0df4e9a704

View File

@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { Screen, WidgetNode } from '@astralview/sdk'; import type { Screen, WidgetNode } from '@astralview/sdk';
import { assertNever } from '@astralview/sdk';
import { Button, Space, Typography } from 'antd'; import { Button, Space, Typography } from 'antd';
import type { ResizeHandle } from './types'; import type { ResizeHandle } from './types';
import { rectFromPoints } from './geometry'; import { rectFromPoints } from './geometry';
@ -232,7 +233,7 @@ export function Canvas(props: CanvasProps) {
const p = clientToWorld(e.clientX, e.clientY); const p = clientToWorld(e.clientX, e.clientY);
if (!p) return; if (!p) return;
const additive = e.ctrlKey || e.metaKey; const additive = e.ctrlKey || e.metaKey || e.shiftKey;
if (!additive) props.onSelectSingle(undefined); if (!additive) props.onSelectSingle(undefined);
props.onBeginBoxSelect(e, p.x, p.y, additive); props.onBeginBoxSelect(e, p.x, p.y, additive);
setBox(rectFromPoints({ x: p.x, y: p.y }, { x: p.x, y: p.y })); setBox(rectFromPoints({ x: p.x, y: p.y }, { x: p.x, y: p.y }));
@ -555,120 +556,137 @@ function NodeView(props: {
borderStyle: node.hidden ? 'dashed' : 'solid', borderStyle: node.hidden ? 'dashed' : 'solid',
}} }}
> >
{node.type === 'text' ? ( {(() => {
<div switch (node.type) {
style={{ case 'text':
width: '100%', return (
height: '100%', <div
display: 'flex', style={{
alignItems: 'center', width: '100%',
justifyContent: height: '100%',
node.props.textAlign === 'left' display: 'flex',
? 'flex-start' alignItems: 'center',
: node.props.textAlign === 'right' justifyContent:
? 'flex-end' node.props.textAlign === 'left'
: 'center', ? 'flex-start'
backgroundColor: node.props.backgroundColor ?? 'transparent', : node.props.textAlign === 'right'
borderStyle: 'solid', ? 'flex-end'
borderWidth: `${node.props.borderWidth ?? 0}px`, : 'center',
borderColor: node.props.borderColor ?? 'transparent', backgroundColor: node.props.backgroundColor ?? 'transparent',
borderRadius: `${node.props.borderRadius ?? 0}px`, borderStyle: 'solid',
boxSizing: 'border-box', borderWidth: `${node.props.borderWidth ?? 0}px`,
padding: `${node.props.paddingY ?? 0}px ${node.props.paddingX ?? 0}px`, borderColor: node.props.borderColor ?? 'transparent',
overflow: 'hidden', borderRadius: `${node.props.borderRadius ?? 0}px`,
}} boxSizing: 'border-box',
> padding: `${node.props.paddingY ?? 0}px ${node.props.paddingX ?? 0}px`,
<span overflow: 'hidden',
style={{ }}
whiteSpace: 'pre-wrap', >
fontSize: node.props.fontSize ?? 24, <span
color: node.props.color ?? '#fff', style={{
fontWeight: node.props.fontWeight ?? 400, whiteSpace: 'pre-wrap',
letterSpacing: `${node.props.letterSpacing ?? 0}px`, fontSize: node.props.fontSize ?? 24,
writingMode: node.props.writingMode as unknown as React.CSSProperties['writingMode'], color: node.props.color ?? '#fff',
cursor: node.props.link ? 'pointer' : 'default', fontWeight: node.props.fontWeight ?? 400,
}} letterSpacing: `${node.props.letterSpacing ?? 0}px`,
onClick={() => { writingMode: node.props.writingMode as unknown as React.CSSProperties['writingMode'],
if (!node.props.link) return; cursor: node.props.link ? 'pointer' : 'default',
const head = node.props.linkHead ?? 'http://'; }}
window.open(`${head}${node.props.link}`); onClick={() => {
}} if (!node.props.link) return;
> const head = node.props.linkHead ?? 'http://';
{node.props.text} window.open(`${head}${node.props.link}`);
</span> }}
</div> >
) : node.type === 'image' ? ( {node.props.text}
<div </span>
style={{ </div>
width: '100%', );
height: '100%',
overflow: 'hidden', case 'image':
borderRadius: `${node.props.borderRadius ?? 0}px`, return (
}} <div
> style={{
<img width: '100%',
src={node.props.src} height: '100%',
alt="" overflow: 'hidden',
style={{ borderRadius: `${node.props.borderRadius ?? 0}px`,
width: '100%', }}
height: '100%', >
objectFit: node.props.fit ?? 'contain', <img
display: 'block', src={node.props.src}
// Editor parity: allow selecting/dragging the widget even when clicking the media. alt=""
pointerEvents: 'none', style={{
}} width: '100%',
/> height: '100%',
</div> objectFit: node.props.fit ?? 'contain',
) : node.type === 'iframe' ? ( display: 'block',
<div // Editor parity: allow selecting/dragging the widget even when clicking the media.
style={{ pointerEvents: 'none',
width: '100%', }}
height: '100%', />
overflow: 'hidden', </div>
borderRadius: `${node.props.borderRadius ?? 0}px`, );
}}
> case 'iframe':
<iframe return (
src={node.props.src} <div
width={rect.w} style={{
height={rect.h} width: '100%',
style={{ height: '100%',
border: 0, overflow: 'hidden',
display: 'block', borderRadius: `${node.props.borderRadius ?? 0}px`,
// Editor parity: iframes steal pointer events; disable so selection/context menu works. }}
pointerEvents: 'none', >
}} <iframe
title={node.id} src={node.props.src}
/> width={rect.w}
</div> height={rect.h}
) : node.type === 'video' ? ( style={{
<div border: 0,
style={{ display: 'block',
width: '100%', // Editor parity: iframes steal pointer events; disable so selection/context menu works.
height: '100%', pointerEvents: 'none',
overflow: 'hidden', }}
borderRadius: `${node.props.borderRadius ?? 0}px`, title={node.id}
}} />
> </div>
<video );
src={node.props.src}
width={rect.w} case 'video':
height={rect.h} return (
autoPlay <div
playsInline style={{
loop={node.props.loop ?? false} width: '100%',
muted={node.props.muted ?? false} height: '100%',
style={{ overflow: 'hidden',
display: 'block', borderRadius: `${node.props.borderRadius ?? 0}px`,
width: '100%', }}
height: '100%', >
objectFit: node.props.fit ?? 'contain', <video
// Editor parity: allow selecting/dragging even when clicking the video surface. src={node.props.src}
pointerEvents: 'none', width={rect.w}
}} height={rect.h}
/> autoPlay
</div> playsInline
) : null} loop={node.props.loop ?? false}
muted={node.props.muted ?? false}
style={{
display: 'block',
width: '100%',
height: '100%',
objectFit: node.props.fit ?? 'contain',
// Editor parity: allow selecting/dragging even when clicking the video surface.
pointerEvents: 'none',
}}
/>
</div>
);
default:
return assertNever(node);
}
})()}
{props.selected && !node.locked && !node.hidden && <ResizeHandles onPointerDown={props.onResizePointerDown} />} {props.selected && !node.locked && !node.hidden && <ResizeHandles onPointerDown={props.onResizePointerDown} />}
</div> </div>