diff --git a/packages/editor/src/editor/Canvas.tsx b/packages/editor/src/editor/Canvas.tsx index fa1ba1f..b728169 100644 --- a/packages/editor/src/editor/Canvas.tsx +++ b/packages/editor/src/editor/Canvas.tsx @@ -425,7 +425,6 @@ export function Canvas(props: CanvasProps) { onPointerDown={(e) => { e.preventDefault(); e.stopPropagation(); - if (node.locked) return; // Right-click selection is handled by the contextmenu handler. // Important: don't collapse a multi-selection on pointerdown. @@ -437,6 +436,12 @@ export function Canvas(props: CanvasProps) { return; } + // Locked nodes should still be selectable, but not movable. + if (node.locked) { + props.onSelectSingle(node.id); + return; + } + props.onSelectSingle(node.id); props.onBeginMove(e); }} @@ -531,6 +536,8 @@ function NodeView(props: { color: '#fff', boxSizing: 'border-box', background: 'rgba(255,255,255,0.02)', + opacity: node.hidden ? 0.28 : 1, + borderStyle: node.hidden ? 'dashed' : 'solid', }} > {node.type === 'text' ? ( @@ -648,7 +655,7 @@ function NodeView(props: { ) : null} - {props.selected && } + {props.selected && !node.locked && !node.hidden && } ); } diff --git a/packages/editor/src/editor/store.ts b/packages/editor/src/editor/store.ts index f9b84d7..3c30ee2 100644 --- a/packages/editor/src/editor/store.ts +++ b/packages/editor/src/editor/store.ts @@ -65,6 +65,24 @@ interface DragSession { snapshot: Map; } +interface PanSession { + startScreenX: number; + startScreenY: number; + startPanX: number; + startPanY: number; +} + +interface BoxSelectSession { + additive: boolean; + baseIds: string[]; +} + +type InternalEditorState = EditorState & { + __pan?: PanSession; + __boxSelect?: BoxSelectSession; + __drag?: DragSession; +}; + function historyPush(state: EditorState): EditorState { return { ...state, @@ -123,6 +141,7 @@ export function createInitialState(): EditorState { } export function editorReducer(state: EditorState, action: EditorAction): EditorState { + const s = state as InternalEditorState; switch (action.type) { case 'keyboard': return { ...state, keyboard: { ctrl: action.ctrl, space: action.space } }; @@ -359,7 +378,8 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS nodes: state.doc.screen.nodes.map((n) => (ids.has(n.id) ? { ...n, hidden: shouldHide } : n)), }, }, - selection: shouldHide ? { ids: [] } : state.selection, + // Keep selection so "Show" can be toggled back immediately via context menu. + selection: state.selection, }; } @@ -433,12 +453,12 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS startPanX: state.canvas.panX, startPanY: state.canvas.panY, }, - } as EditorState & { __pan: { startScreenX: number; startScreenY: number; startPanX: number; startPanY: number } }; + } as EditorState; } case 'updatePan': { if (!state.canvas.isPanning) return state; - const pan = (state as EditorState & { __pan?: { startScreenX: number; startScreenY: number; startPanX: number; startPanY: number } }).__pan; + const pan = s.__pan; if (!pan) return state; const scale = state.canvas.scale; const dx = (action.current.screenX - pan.startScreenX) / scale; @@ -456,14 +476,14 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS case 'endPan': { if (!state.canvas.isPanning) return state; // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { __pan: _pan, ...rest } = state as EditorState & { __pan?: unknown }; + const { __pan: _pan, ...rest } = s; return { ...rest, canvas: { ...state.canvas, isPanning: false, }, - }; + } as EditorState; } case 'selectSingle': @@ -501,7 +521,7 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS }, selection: { ids: baseIds }, __boxSelect: { additive: action.additive, baseIds }, - } as EditorState & { __boxSelect: { additive: boolean; baseIds: string[] } }; + } as EditorState; } case 'updateBoxSelect': { @@ -522,7 +542,7 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS if (rectContains(box, node.rect)) selected.push(node.id); } - const boxState = (state as EditorState & { __boxSelect?: { additive: boolean; baseIds: string[] } }).__boxSelect; + const boxState = s.__boxSelect; const ids = boxState?.additive ? Array.from(new Set([...(boxState.baseIds ?? []), ...selected])) : selected; return { @@ -544,7 +564,7 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS case 'endBoxSelect': { if (!state.canvas.isBoxSelecting) return state; // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { __boxSelect: _box, ...rest } = state as EditorState & { __boxSelect?: unknown }; + const { __boxSelect: _box, ...rest } = s; return { ...rest, canvas: { @@ -552,7 +572,7 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS isBoxSelecting: false, mouse: { ...state.canvas.mouse, offsetX: 0, offsetY: 0, offsetStartX: 0, offsetStartY: 0 }, }, - }; + } as EditorState; } case 'beginMove': { @@ -584,11 +604,11 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS }, }, __drag: drag, - } as EditorState & { __drag: DragSession }; + } as EditorState; } case 'updateMove': { - const drag = (state as EditorState & { __drag?: DragSession }).__drag; + const drag = s.__drag; if (!state.canvas.isDragging || !drag || drag.kind !== 'move') return state; const scale = state.canvas.scale; const dx = (action.current.screenX - drag.startScreenX) / scale; @@ -636,10 +656,9 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS } case 'endMove': { - const s = state as EditorState & { __drag?: DragSession }; const drag = s.__drag; // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { __drag: _drag, ...rest } = s as EditorState & { __drag?: unknown }; + const { __drag: _drag, ...rest } = s; if (!drag || drag.kind !== 'move') { return { ...rest, canvas: { ...state.canvas, isDragging: false, guides: { xs: [], ys: [] } } }; @@ -678,11 +697,11 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS ...next, canvas: { ...next.canvas, isDragging: true, guides: { xs: [], ys: [] } }, __drag: drag, - } as EditorState & { __drag: DragSession }; + } as EditorState; } case 'updateResize': { - const drag = (state as EditorState & { __drag?: DragSession }).__drag; + const drag = s.__drag; if (!state.canvas.isDragging || !drag || drag.kind !== 'resize' || !drag.targetId || !drag.handle) return state; const scale = state.canvas.scale; @@ -742,10 +761,9 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS } case 'endResize': { - const s = state as EditorState & { __drag?: DragSession }; const drag = s.__drag; // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { __drag: _drag, ...rest } = s as EditorState & { __drag?: unknown }; + const { __drag: _drag, ...rest } = s; if (!drag || drag.kind !== 'resize') { return { ...rest, canvas: { ...state.canvas, isDragging: false, guides: { xs: [], ys: [] } } }; diff --git a/packages/sdk/src/core/goview/convert.ts b/packages/sdk/src/core/goview/convert.ts index 03a513d..dc4bf67 100644 --- a/packages/sdk/src/core/goview/convert.ts +++ b/packages/sdk/src/core/goview/convert.ts @@ -180,6 +180,47 @@ function toNumber(v: unknown, fallback: number): number { return fallback; } +function toMaybeNumber(v: unknown): number | undefined { + if (typeof v === 'number' && Number.isFinite(v)) return v; + if (typeof v === 'string') { + const n = Number(v); + if (Number.isFinite(n)) return n; + } + return undefined; +} + +function pickNumberLike(input: unknown, keys: string[], maxDepth = 2): number | undefined { + return pickNumberLikeInner(input, keys, maxDepth); +} + +function pickNumberLikeInner(input: unknown, keys: string[], depth: number): number | undefined { + if (!input) return undefined; + if (typeof input !== 'object') return toMaybeNumber(input); + + const obj = input as Record; + + for (const key of keys) { + const v = toMaybeNumber(obj[key]); + if (v !== undefined) return v; + } + + if (depth <= 0) return undefined; + + for (const key of ['style', 'config', 'option', 'options', 'props', 'dataset', 'data']) { + const nested = pickNumberLikeInner(obj[key], keys, depth - 1); + if (nested !== undefined) return nested; + } + + return undefined; +} + +function pickSizeLike(option: unknown): { w?: number; h?: number } { + // goView-ish variants use different keys and sometimes nest them under style/config. + const w = pickNumberLike(option, ['width', 'w']); + const h = pickNumberLike(option, ['height', 'h']); + return { w, h }; +} + export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewStorageLike): Screen { // goView exports vary a lot; attempt a few common nesting shapes. const root = input as unknown as Record; @@ -232,18 +273,21 @@ export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewSt for (const raw of componentList) { const c = unwrapComponent(raw); + const option = optionOf(c); + const optSize = pickSizeLike(option); const rect = c.attr ? { x: toNumber((c.attr as unknown as Record).x, 0), y: toNumber((c.attr as unknown as Record).y, 0), - w: toNumber((c.attr as unknown as Record).w, 320), - h: toNumber((c.attr as unknown as Record).h, 60), + // Prefer explicit attr sizing, but fall back to option sizing when missing. + w: toNumber((c.attr as unknown as Record).w, optSize.w ?? 320), + h: toNumber((c.attr as unknown as Record).h, optSize.h ?? 60), } - : { x: 0, y: 0, w: 320, h: 60 }; + : { x: 0, y: 0, w: optSize.w ?? 320, h: optSize.h ?? 60 }; if (isTextCommon(c)) { - const props = convertGoViewTextOptionToNodeProps(optionOf(c) as GoViewTextOption); + const props = convertGoViewTextOptionToNodeProps(option as GoViewTextOption); nodes.push({ id: c.id ?? `import_text_${Math.random().toString(16).slice(2)}`, type: 'text', @@ -257,7 +301,7 @@ export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewSt } if (isImage(c)) { - const props = convertGoViewImageOptionToNodeProps(optionOf(c) as GoViewImageOption); + const props = convertGoViewImageOptionToNodeProps(option as GoViewImageOption); nodes.push({ id: c.id ?? `import_image_${Math.random().toString(16).slice(2)}`, type: 'image', @@ -270,8 +314,6 @@ export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewSt continue; } - const option = optionOf(c); - if (isIframe(c) || looksLikeIframeOption(option)) { const props = convertGoViewIframeOptionToNodeProps(option as GoViewIframeOption); nodes.push({ diff --git a/packages/sdk/src/core/widgets/iframe.ts b/packages/sdk/src/core/widgets/iframe.ts index d3953b6..d00eddb 100644 --- a/packages/sdk/src/core/widgets/iframe.ts +++ b/packages/sdk/src/core/widgets/iframe.ts @@ -22,10 +22,41 @@ function pickSrc(option: GoViewIframeOption): string { return pickUrlLike(option) || pickUrlLike(option.dataset) || pickUrlLike(option.src) || pickUrlLike(option.url); } +function toMaybeNumber(v: unknown): number | undefined { + if (typeof v === 'number' && Number.isFinite(v)) return v; + if (typeof v === 'string') { + const n = Number(v); + if (Number.isFinite(n)) return n; + } + return undefined; +} + +function pickFromNested(input: unknown, picker: (obj: Record) => T | undefined, depth: number): T | undefined { + if (!input || typeof input !== 'object') return undefined; + const obj = input as Record; + + const direct = picker(obj); + if (direct !== undefined) return direct; + if (depth <= 0) return undefined; + + for (const key of ['style', 'config', 'option', 'options', 'props', 'dataset', 'data']) { + const nested = pickFromNested(obj[key], picker, depth - 1); + if (nested !== undefined) return nested; + } + + return undefined; +} + +function pickBorderRadius(option: GoViewIframeOption): number | undefined { + const direct = toMaybeNumber(option.borderRadius); + if (direct !== undefined) return direct; + return pickFromNested(option, (obj) => toMaybeNumber(obj.borderRadius ?? obj.radius ?? obj.r), 2); +} + export function convertGoViewIframeOptionToNodeProps(option: GoViewIframeOption): IframeWidgetNode['props'] { return { src: pickSrc(option), - borderRadius: option.borderRadius, + borderRadius: pickBorderRadius(option), }; } diff --git a/packages/sdk/src/core/widgets/video.ts b/packages/sdk/src/core/widgets/video.ts index 4a69e0d..770b828 100644 --- a/packages/sdk/src/core/widgets/video.ts +++ b/packages/sdk/src/core/widgets/video.ts @@ -12,6 +12,8 @@ export interface GoViewVideoOption { loop?: boolean; muted?: boolean; + autoplay?: boolean; + autoPlay?: boolean; fit?: unknown; objectFit?: unknown; @@ -35,8 +37,73 @@ function asString(v: unknown): string { return ''; } +function toMaybeBoolean(v: unknown): boolean | undefined { + if (typeof v === 'boolean') return v; + if (typeof v === 'number') return v !== 0; + if (typeof v === 'string') { + const s = v.trim().toLowerCase(); + if (s === 'true' || s === '1') return true; + if (s === 'false' || s === '0') return false; + } + return undefined; +} + +function toMaybeNumber(v: unknown): number | undefined { + if (typeof v === 'number' && Number.isFinite(v)) return v; + if (typeof v === 'string') { + const n = Number(v); + if (Number.isFinite(n)) return n; + } + return undefined; +} + +function pickFromNested(input: unknown, picker: (obj: Record) => T | undefined, depth: number): T | undefined { + if (!input || typeof input !== 'object') return undefined; + const obj = input as Record; + + const direct = picker(obj); + if (direct !== undefined) return direct; + if (depth <= 0) return undefined; + + for (const key of ['style', 'config', 'option', 'options', 'props', 'dataset', 'data']) { + const nested = pickFromNested(obj[key], picker, depth - 1); + if (nested !== undefined) return nested; + } + + return undefined; +} + +function pickBorderRadius(option: GoViewVideoOption): number | undefined { + const direct = toMaybeNumber(option.borderRadius); + if (direct !== undefined) return direct; + + return pickFromNested(option, (obj) => toMaybeNumber(obj.borderRadius ?? obj.radius ?? obj.r), 2); +} + +function pickBooleanLike(option: GoViewVideoOption, keys: string[]): boolean | undefined { + return pickFromNested( + option, + (obj) => { + for (const key of keys) { + const v = toMaybeBoolean(obj[key]); + if (v !== undefined) return v; + } + return undefined; + }, + 2, + ); +} + +function pickFitFromNested(option: GoViewVideoOption): string { + const direct = asString(option.fit) || asString(option.objectFit); + if (direct) return direct; + + const nested = pickFromNested(option, (obj) => asString(obj.fit) || asString(obj.objectFit), 2); + return nested ?? ''; +} + function pickFit(option: GoViewVideoOption): VideoWidgetNode['props']['fit'] | undefined { - const raw = asString(option.fit) || asString(option.objectFit); + const raw = pickFitFromNested(option); if (!raw) return undefined; // normalize common variants @@ -51,10 +118,10 @@ function pickFit(option: GoViewVideoOption): VideoWidgetNode['props']['fit'] | u export function convertGoViewVideoOptionToNodeProps(option: GoViewVideoOption): VideoWidgetNode['props'] { return { src: pickSrc(option), - loop: option.loop, - muted: option.muted, + loop: pickBooleanLike(option, ['loop', 'isLoop']), + muted: pickBooleanLike(option, ['muted', 'isMuted', 'mute']), fit: pickFit(option), - borderRadius: option.borderRadius, + borderRadius: pickBorderRadius(option), }; }