diff --git a/packages/editor/src/editor/ContextMenu.tsx b/packages/editor/src/editor/ContextMenu.tsx index 063f355..cb3102b 100644 --- a/packages/editor/src/editor/ContextMenu.tsx +++ b/packages/editor/src/editor/ContextMenu.tsx @@ -13,7 +13,9 @@ export function ContextMenu(props: { state: ContextMenuState | null; selectionIds: string[]; selectionAllLocked: boolean; + selectionSomeLocked?: boolean; selectionAllHidden: boolean; + selectionSomeHidden?: boolean; hasAnyNodes?: boolean; onClose: () => void; onAddTextAt: (x: number, y: number) => void; @@ -160,7 +162,13 @@ export function ContextMenu(props: { /> { props.onToggleLockSelected?.(); @@ -168,7 +176,13 @@ export function ContextMenu(props: { }} /> { props.onToggleHideSelected?.(); diff --git a/packages/editor/src/editor/EditorApp.tsx b/packages/editor/src/editor/EditorApp.tsx index 0ce2277..e4bdea2 100644 --- a/packages/editor/src/editor/EditorApp.tsx +++ b/packages/editor/src/editor/EditorApp.tsx @@ -54,7 +54,10 @@ export function EditorApp() { }, [state.doc.screen.nodes, state.selection.ids]); const selectionAllLocked = selection.length > 0 && selection.every((n) => n.locked); + const selectionSomeLocked = selection.length > 0 && selection.some((n) => n.locked) && !selectionAllLocked; + const selectionAllHidden = selection.length > 0 && selection.every((n) => n.hidden); + const selectionSomeHidden = selection.length > 0 && selection.some((n) => n.hidden) && !selectionAllHidden; const bounds = useMemo( () => ({ w: state.doc.screen.width, h: state.doc.screen.height }), @@ -508,7 +511,9 @@ export function EditorApp() { state={ctxMenu} selectionIds={state.selection.ids} selectionAllLocked={selectionAllLocked} + selectionSomeLocked={selectionSomeLocked} selectionAllHidden={selectionAllHidden} + selectionSomeHidden={selectionSomeHidden} hasAnyNodes={state.doc.screen.nodes.length > 0} onClose={closeContextMenu} onAddTextAt={(x, y) => dispatch({ type: 'addTextAt', x, y })} diff --git a/packages/sdk/src/core/goview/convert.ts b/packages/sdk/src/core/goview/convert.ts index 995312c..d008379 100644 --- a/packages/sdk/src/core/goview/convert.ts +++ b/packages/sdk/src/core/goview/convert.ts @@ -236,6 +236,23 @@ function looksLikeTextOption(option: unknown): boolean { return false; } +function hasAnyKeyDeep(input: unknown, keys: string[], depth = 2): boolean { + if (!input || typeof input !== 'object') return false; + const obj = input as Record; + + for (const k of keys) { + if (k in obj) return true; + } + + if (depth <= 0) return false; + + for (const nestKey of ['style', 'config', 'option', 'options', 'props', 'dataset', 'data']) { + if (hasAnyKeyDeep(obj[nestKey], keys, depth - 1)) return true; + } + + return false; +} + function looksLikeIframeOption(option: unknown): boolean { if (!option) return false; @@ -248,14 +265,15 @@ function looksLikeIframeOption(option: unknown): boolean { if (trimmed.startsWith('<') && trimmed.includes('>')) return true; } - // Prefer explicit iframe-ish keys when option is an object. + // Prefer explicit iframe-ish keys when option is an object (including nested shapes). if (typeof option === 'object') { - const o = option as Record; - if ('iframeUrl' in o || 'iframeSrc' in o || 'embedUrl' in o || 'frameUrl' in o || 'frameSrc' in o) return true; - - // Some exports store raw HTML instead of a URL. - if ('srcdoc' in o || 'srcDoc' in o) return true; - if ('html' in o || 'htmlContent' in o || 'content' in o || 'template' in o) return true; + if ( + hasAnyKeyDeep(option, ['iframeUrl', 'iframeSrc', 'embedUrl', 'frameUrl', 'frameSrc'], 2) || + // Some exports store raw HTML instead of a URL. + hasAnyKeyDeep(option, ['srcdoc', 'srcDoc', 'html', 'htmlContent', 'content', 'template'], 2) + ) { + return true; + } } const url = pickUrlLike(option); @@ -284,31 +302,42 @@ function looksLikeVideoOption(option: unknown): boolean { // Avoid false positives: some TextCommon widgets carry link-like fields. if (looksLikeTextOption(option)) return false; - // Prefer explicit video-ish keys when option is an object. + // Prefer explicit video-ish keys when option is an object (including nested shapes). if (typeof option === 'object') { - const o = option as Record; if ( - 'videoUrl' in o || - 'videoSrc' in o || - 'playUrl' in o || - 'srcUrl' in o || - 'sourceUrl' in o || - 'liveUrl' in o || - 'streamUrl' in o || - 'mp4' in o || - 'm3u8' in o || - 'flv' in o || - 'hls' in o || - 'rtsp' in o || - // list-ish shapes - 'sources' in o || - 'sourceList' in o || - 'urlList' in o || - 'autoplay' in o || - 'autoPlay' in o || - 'isAutoPlay' in o || - 'poster' in o || - 'posterUrl' in o + hasAnyKeyDeep( + option, + [ + 'videoUrl', + 'videoSrc', + 'playUrl', + 'srcUrl', + 'sourceUrl', + 'liveUrl', + 'streamUrl', + 'mp4', + 'm3u8', + 'flv', + 'hls', + 'rtsp', + 'rtmp', + // list-ish shapes + 'sources', + 'sourceList', + 'urlList', + 'srcList', + 'playlist', + 'playList', + // playback flags + 'autoplay', + 'autoPlay', + 'isAutoPlay', + // common UI fields + 'poster', + 'posterUrl', + ], + 2, + ) ) { return true; }