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),
};
}