Files
qiming/qiming-vite-plugin-design-mode/packages/client-react/src/DesignModeContext.tsx
2026-06-01 13:43:09 +08:00

1075 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, {
createContext,
useContext,
useState,
useCallback,
useEffect,
} from 'react';
import { twMerge } from 'tailwind-merge';
import {
DesignModeMessage,
ParentToIframeMessage,
IframeToParentMessage,
BridgeConfig,
ElementInfo,
SourceInfo,
ToggleDesignModeMessage,
UpdateStyleMessage,
UpdateContentMessage,
BatchUpdateMessage,
HeartbeatMessage,
HealthCheckMessage,
HealthCheckResponseMessage,
} from '@xagi/design-mode-shared/messages';
import { bridge, messageValidator } from './bridge';
import { AttributeNames } from '@xagi/design-mode-shared/attributeNames';
import { isPureStaticText } from '@xagi/design-mode-shared/elementUtils';
import { extractSourceInfo as extractSourceInfoFromAttributes } from '@xagi/design-mode-shared/sourceInfo';
import { resolveSourceInfo } from '@xagi/design-mode-shared/sourceInfoResolver';
export interface Modification {
id: string;
element: string;
type: 'class' | 'style';
oldValue: string;
newValue: string;
timestamp: number;
}
export interface DesignModeConfig {
enabled?: boolean;
iframeMode?: {
enabled: boolean;
hideUI: boolean;
enableSelection: boolean;
enableDirectEdit: boolean;
};
batchUpdate?: {
enabled: boolean;
debounceMs: number;
};
bridge?: Partial<BridgeConfig>;
}
interface DesignModeContextType {
// State
isDesignMode: boolean;
selectedElement: HTMLElement | null;
modifications: Modification[];
isConnected: boolean;
bridgeStatus: 'connected' | 'disconnected' | 'connecting' | 'error';
// Config
config: DesignModeConfig;
// Actions
toggleDesignMode: () => void;
selectElement: (element: HTMLElement | null) => void;
modifyElementClass: (element: HTMLElement, newClass: string) => Promise<void>;
updateElementContent: (
element: HTMLElement,
newContent: string
) => Promise<void>;
batchUpdateElements: (
updates: Array<{
element: HTMLElement;
type: 'style' | 'content';
newValue: string;
originalValue?: string;
}>
) => Promise<void>;
resetModifications: () => void;
// Bridge helpers
sendMessage: <T extends DesignModeMessage>(message: T) => Promise<void>;
sendMessageWithResponse: <
T extends DesignModeMessage,
R extends DesignModeMessage,
>(
message: T,
responseType: R['type']
) => Promise<R>;
healthCheck: () => Promise<any>;
}
const DesignModeContext = createContext<DesignModeContextType | undefined>(
undefined
);
export const DesignModeProvider: React.FC<{
children: React.ReactNode;
config?: DesignModeConfig;
}> = ({ children, config: userConfig = {} }) => {
// Defaults
const defaultConfig: DesignModeConfig = {
enabled: true,
iframeMode: {
enabled: true,
hideUI: false,
enableSelection: true,
enableDirectEdit: true,
},
batchUpdate: {
enabled: true,
debounceMs: 300,
},
bridge: {
timeout: 10000,
retryAttempts: 3,
heartbeatInterval: 30000,
debug: process.env.NODE_ENV === 'development',
},
...userConfig,
};
// State
const [isDesignMode, setIsDesignMode] = useState(false);
const [selectedElement, setSelectedElement] = useState<HTMLElement | null>(
null
);
const [modifications, setModifications] = useState<Modification[]>([]);
const [isConnected, setIsConnected] = useState(false);
const [bridgeStatus, setBridgeStatus] = useState<
'connected' | 'disconnected' | 'connecting' | 'error'
>('connecting');
const [config] = useState<DesignModeConfig>(defaultConfig);
// Batch debounce state
const [batchUpdateTimer, setBatchUpdateTimer] =
useState<NodeJS.Timeout | null>(null);
const [pendingBatchUpdates, setPendingBatchUpdates] = useState<
Array<{
element: HTMLElement;
type: 'style' | 'content';
newValue: string;
originalValue?: string;
}>
>([]);
/**
* Wire iframe bridge listeners
*/
useEffect(() => {
// Bridge setup
if (config.iframeMode?.enabled) {
setBridgeStatus('connecting');
// Poll isConnected
const connectionCheck = setInterval(() => {
const connected = bridge.isConnected();
setIsConnected(connected);
setBridgeStatus(connected ? 'connected' : 'disconnected');
}, 1000);
// Subscriptions
const unsubscribeHandlers: (() => void)[] = [];
// TOGGLE_DESIGN_MODE
unsubscribeHandlers.push(
bridge.on<ToggleDesignModeMessage>('TOGGLE_DESIGN_MODE', message => {
const newState = message.enabled;
setIsDesignMode(newState);
// Echo DESIGN_MODE_CHANGED
if (window.self !== window.top) {
sendToParent({
type: 'DESIGN_MODE_CHANGED',
enabled: newState,
requestId: message.requestId,
timestamp: Date.now(),
});
}
})
);
// UPDATE_STYLE
unsubscribeHandlers.push(
bridge.on<UpdateStyleMessage>('UPDATE_STYLE', async message => {
await handleExternalStyleUpdate(message);
})
);
// UPDATE_CONTENT
unsubscribeHandlers.push(
bridge.on<UpdateContentMessage>('UPDATE_CONTENT', async message => {
await handleExternalContentUpdate(message);
})
);
// BATCH_UPDATE
unsubscribeHandlers.push(
bridge.on<BatchUpdateMessage>('BATCH_UPDATE', async message => {
await handleExternalBatchUpdate(message);
})
);
// HEARTBEAT
unsubscribeHandlers.push(
bridge.on<HeartbeatMessage>('HEARTBEAT', _ => {
// Echo HEARTBEAT
bridge.send({
type: 'HEARTBEAT',
payload: { timestamp: Date.now() },
timestamp: Date.now(),
});
})
);
// HEALTH_CHECK
unsubscribeHandlers.push(
bridge.on<HealthCheckMessage>('HEALTH_CHECK', async message => {
const healthStatus = await bridge.healthCheck();
// Use a type assertion or construct the object carefully to match HealthCheckResponseMessage
const response: HealthCheckResponseMessage = {
type: 'HEALTH_CHECK_RESPONSE',
payload: {
status: healthStatus.status === 'healthy' ? 'healthy' : 'unhealthy',
version: '2.0.0',
uptime: Date.now() - ((window as any).__startTime || 0),
},
requestId: message.requestId || '', // Provide requestId
timestamp: Date.now(),
};
bridge.send(response);
})
);
// Initial health probe
const initialHealthCheck = setTimeout(async () => {
try {
const health = await bridge.healthCheck();
setBridgeStatus(health.status === 'healthy' ? 'connected' : 'error');
} catch (error) {
setBridgeStatus('error');
}
}, 1000);
return () => {
clearInterval(connectionCheck);
clearTimeout(initialHealthCheck);
unsubscribeHandlers.forEach(unsubscribe => unsubscribe());
};
}
}, [config]);
/**
* postMessage when iframe + connected
*/
const sendToParent = useCallback(
(message: IframeToParentMessage) => {
if (!config.iframeMode?.enabled) {
return;
}
// 优先走 bridgebridge 未连通或发送失败时兜底直发 parent避免关键回执丢失。
if (bridge.isConnected()) {
bridge.send(message).catch(error => {
if (window.self !== window.top) {
window.parent.postMessage(message, '*');
}
});
return;
}
if (window.self !== window.top) {
window.parent.postMessage(message, '*');
}
},
[config.iframeMode?.enabled]
);
/**
* findElementBySourceInfo
*/
const findElementBySourceInfo = useCallback(
(sourceInfo: SourceInfo): HTMLElement | null => {
// 1) element-id
if (sourceInfo.elementId) {
const element = document.querySelector(`[${AttributeNames.elementId}="${sourceInfo.elementId}"]`);
if (element) return element as HTMLElement;
}
// 2) legacy file/line/column attrs
const selector = `[${AttributeNames.file}="${sourceInfo.fileName}"][${AttributeNames.line}="${sourceInfo.lineNumber}"][${AttributeNames.column}="${sourceInfo.columnNumber}"]`;
const element = document.querySelector(selector);
if (element) return element as HTMLElement;
// 3) children-source
const childrenSourceValue = `${sourceInfo.fileName}:${sourceInfo.lineNumber}:${sourceInfo.columnNumber}`;
const elementByChildrenSource = document.querySelector(`[${AttributeNames.childrenSource}="${childrenSourceValue}"]`);
if (elementByChildrenSource) return elementByChildrenSource as HTMLElement;
// 4) scan all -info
const allElements = document.querySelectorAll(`[${AttributeNames.info}]`);
for (let i = 0; i < allElements.length; i++) {
const el = allElements[i] as HTMLElement;
try {
const infoStr = el.getAttribute(AttributeNames.info);
if (infoStr) {
const info = JSON.parse(infoStr);
if (
info.fileName === sourceInfo.fileName &&
info.lineNumber === sourceInfo.lineNumber &&
info.columnNumber === sourceInfo.columnNumber
) {
return el;
}
}
} catch (e) {
// ignore parse error
}
}
return null;
},
[]
);
/**
* Parent-driven style patch
*/
const handleExternalStyleUpdate = useCallback(
async (message: UpdateStyleMessage) => {
if (!config.iframeMode?.enabled) return;
const updateMessage = message;
const { sourceInfo, newClass } = updateMessage.payload;
try {
// validate
const validation = messageValidator.validate(updateMessage);
if (!validation.isValid) {
console.error(
'[DesignMode] Invalid style update message:',
validation.errors
);
return;
}
// resolve element
const element = findElementBySourceInfo(sourceInfo);
if (!element) {
console.error(
'[DesignMode] Element not found for sourceInfo:',
sourceInfo
);
sendToParent({
type: 'ERROR',
payload: {
code: 'ELEMENT_NOT_FOUND',
message: `Element not found: ${sourceInfo.fileName}:${sourceInfo.lineNumber}:${sourceInfo.columnNumber}`,
details: { sourceInfo },
},
timestamp: Date.now(),
});
return;
}
const oldClass = element.className;
// Same element-id → list sync
const elementId = element.getAttribute(AttributeNames.elementId);
let relatedElements: HTMLElement[] = [element];
if (elementId) {
// query all [element-id]
const allElementsWithId = Array.from(
document.querySelectorAll(`[${AttributeNames.elementId}]`)
) as HTMLElement[];
relatedElements = allElementsWithId.filter(el => {
const elId = el.getAttribute(AttributeNames.elementId);
return elId === elementId;
});
} else {
console.warn('[DesignMode] Element missing element-id attribute, only updating current element');
}
// Apply class to all peers
relatedElements.forEach(el => {
el.setAttribute('data-ignore-mutation', 'true');
el.className = newClass;
// Use setTimeout to ensure MutationObserver sees the attribute
setTimeout(() => {
el.removeAttribute('data-ignore-mutation');
}, 0);
});
// Refresh selection ref
if (selectedElement === element) {
setSelectedElement(element);
}
// (persist via API — commented)
// try {
// const response = await fetch('/__appdev_design_mode/update', {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// },
// body: JSON.stringify({
// filePath: sourceInfo.fileName,
// line: sourceInfo.lineNumber,
// column: sourceInfo.columnNumber,
// newValue: newClass,
// type: 'style',
// originalValue: oldClass,
// }),
// });
// if (!response.ok) {
// throw new Error('Failed to update source');
// }
// } catch (error) {
// console.error('[DesignMode] Error updating source:', error);
// throw error;
// }
// STYLE_UPDATED
sendToParent({
type: 'STYLE_UPDATED',
payload: {
sourceInfo,
oldClass,
newClass,
},
timestamp: Date.now(),
});
} catch (error) {
console.error(
'[DesignMode] Error handling external style update:',
error
);
// ERROR
sendToParent({
type: 'ERROR',
payload: {
code: 'STYLE_UPDATE_FAILED',
message: error instanceof Error ? error.message : 'Unknown error',
details: { sourceInfo: updateMessage.payload.sourceInfo },
},
timestamp: Date.now(),
});
}
},
[
selectedElement,
config.iframeMode?.enabled,
sendToParent,
setSelectedElement,
findElementBySourceInfo
]
);
/**
* Parent-driven content patch
*/
const handleExternalContentUpdate = useCallback(
async (message: UpdateContentMessage) => {
if (!config.iframeMode?.enabled) return;
const updateMessage = message;
const { sourceInfo, newContent } = updateMessage.payload;
try {
// validate
const validation = messageValidator.validate(updateMessage);
if (!validation.isValid) {
console.error(
'[DesignMode] Invalid content update message:',
validation.errors
);
return;
}
// resolve element
const element = findElementBySourceInfo(sourceInfo);
if (!element) {
console.error(
'[DesignMode] Element not found for sourceInfo:',
sourceInfo
);
sendToParent({
type: 'ERROR',
payload: {
code: 'ELEMENT_NOT_FOUND',
message: `Element not found: ${sourceInfo.fileName}:${sourceInfo.lineNumber}:${sourceInfo.columnNumber}`,
details: { sourceInfo },
},
timestamp: Date.now(),
});
return;
}
const originalContent = element.innerText || element.textContent || '';
// Same element-id → list sync
const elementId = element.getAttribute(AttributeNames.elementId);
let relatedElements: HTMLElement[] = [element];
if (elementId) {
// query all [element-id]
const allElementsWithId = Array.from(
document.querySelectorAll(`[${AttributeNames.elementId}]`)
) as HTMLElement[];
relatedElements = allElementsWithId.filter(el => {
const elId = el.getAttribute(AttributeNames.elementId);
return elId === elementId;
});
} else {
console.warn('[DesignMode] Element missing element-id attribute, only updating current element');
}
// Apply text to all peers (list sync)
relatedElements.forEach(el => {
el.setAttribute('data-ignore-mutation', 'true');
el.innerText = newContent;
// Use setTimeout to ensure MutationObserver sees the attribute
setTimeout(() => {
el.removeAttribute('data-ignore-mutation');
}, 0);
});
// Refresh selection ref
if (selectedElement === element) {
setSelectedElement(element);
}
// persist flag (commented)
// if (updateMessage.payload.persist !== false) {
// // Persist via API using extractSourceInfo (commented)
// const elementSourceInfo = extractSourceInfo(element);
// if (elementSourceInfo) {
// try {
// const response = await fetch('/__appdev_design_mode/update', {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// },
// body: JSON.stringify({
// filePath: elementSourceInfo.fileName,
// line: elementSourceInfo.lineNumber,
// column: elementSourceInfo.columnNumber,
// newValue: newContent,
// type: 'content',
// originalValue: originalContent,
// }),
// });
// if (!response.ok) {
// throw new Error('Failed to update source');
// }
// } catch (error) {
// console.error('[DesignMode] Error updating source:', error);
// throw error;
// }
// }
// }
// STYLE_UPDATED
sendToParent({
type: 'CONTENT_UPDATED_CALLBACK',
payload: {
sourceInfo,
oldValue: originalContent,
newValue: newContent,
},
timestamp: Date.now(),
});
} catch (error) {
console.error(
'[DesignMode] Error handling external content update:',
error
);
// ERROR
sendToParent({
type: 'ERROR',
payload: {
code: 'CONTENT_UPDATE_FAILED',
message: error instanceof Error ? error.message : 'Unknown error',
details: { sourceInfo: updateMessage.payload.sourceInfo },
},
timestamp: Date.now(),
});
}
},
[
selectedElement,
config.iframeMode?.enabled,
sendToParent,
setSelectedElement,
findElementBySourceInfo
]
);
/**
* updateSource (HTTP)
*/
const updateSource = useCallback(
async (
element: HTMLElement,
newValue: string,
type: 'style' | 'content',
originalValue?: string
) => {
const sourceInfo = extractSourceInfo(element);
if (!sourceInfo) {
throw new Error('Element does not have source mapping data');
}
try {
const response = await fetch('/__appdev_design_mode/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
filePath: sourceInfo.fileName,
line: sourceInfo.lineNumber,
column: sourceInfo.columnNumber,
newValue,
type,
originalValue,
}),
});
if (!response.ok) {
throw new Error('Failed to update source');
}
} catch (error) {
console.error('[DesignMode] Error updating source:', error);
throw error;
}
},
[]
);
/**
* Parent BATCH_UPDATE
*/
const handleExternalBatchUpdate = useCallback(
async (message: BatchUpdateMessage) => {
const updateMessage = message;
const { updates } = updateMessage.payload;
try {
// validate
const validation = messageValidator.validate(updateMessage);
if (!validation.isValid) {
console.error(
'[DesignMode] Invalid batch update message:',
validation.errors
);
return;
}
// Promise.allSettled items
const results = await Promise.allSettled(
updates.map(async (update: any) => {
const element = findElementBySourceInfo(update.sourceInfo);
if (!element) {
throw new Error(
`Element not found: ${update.sourceInfo.fileName}:${update.sourceInfo.lineNumber}`
);
}
if (update.type === 'style') {
element.setAttribute('data-ignore-mutation', 'true');
element.className = update.newValue;
setTimeout(() => element.removeAttribute('data-ignore-mutation'), 0);
} else if (update.type === 'content') {
element.setAttribute('data-ignore-mutation', 'true');
element.innerText = update.newValue;
setTimeout(() => element.removeAttribute('data-ignore-mutation'), 0);
}
await updateSource(
element,
update.newValue,
update.type,
update.originalValue
);
return { success: true, sourceInfo: update.sourceInfo };
})
);
} catch (error) {
console.error('[DesignMode] Error handling batch update:', error);
// ERROR
sendToParent({
type: 'ERROR',
payload: {
code: 'BATCH_UPDATE_FAILED',
message: error instanceof Error ? error.message : 'Unknown error',
details: { updatesCount: updates?.length || 0 },
},
timestamp: Date.now(),
});
}
},
[findElementBySourceInfo, sendToParent, updateSource]
);
/**
* selectElement
*/
const selectElement = useCallback(
async (element: HTMLElement | null) => {
if (element && (element.hasAttribute(AttributeNames.staticContent)
|| element.hasAttribute(AttributeNames.staticClass))) {
setSelectedElement(element);
} else {
setSelectedElement(null);
}
// iframe: ELEMENT_SELECTED
if (element && config.iframeMode?.enabled) {
// resolveSourceInfo for usage site
const sourceInfo = resolveSourceInfo(element);
// console.log('[DesignModeContext] selectElement - resolveSourceInfo:', sourceInfo);
if (sourceInfo) {
// Static text if attr + pure text
const hasStaticContentAttr = element.hasAttribute(AttributeNames.staticContent);
const isActuallyPureText = isPureStaticText(element);
const isStaticText = hasStaticContentAttr && isActuallyPureText;
// static-class attr
const isStaticClass = element.hasAttribute(AttributeNames.staticClass);
let textContent = '';
if (isStaticText) {
textContent = element.textContent || element.innerText || '';
} else {
textContent = element.innerText || element.textContent || '';
}
const elementInfo: ElementInfo = {
tagName: element.tagName.toLowerCase(),
className: element.className,
textContent: textContent,
sourceInfo,
isStaticText: isStaticText || false,
isStaticClass: isStaticClass,
};
sendToParent({
type: 'ELEMENT_SELECTED',
payload: { elementInfo },
timestamp: Date.now(),
});
} else {
console.warn(
`[DesignMode] Element selected but could not resolve source info:`,
element
);
}
} else if (!element && config.iframeMode?.enabled) {
sendToParent({
type: 'ELEMENT_DESELECTED',
timestamp: Date.now(),
});
}
},
[config.iframeMode?.enabled]
);
/**
* toggleDesignMode
*/
const toggleDesignMode = useCallback(() => {
setIsDesignMode(prev => {
const next = !prev;
if (!next) {
setSelectedElement(null);
}
return next;
});
}, []);
/**
* Local extractSourceInfo helper
*/
const extractSourceInfo = useCallback(
(element: HTMLElement): SourceInfo | null => {
return extractSourceInfoFromAttributes(element);
},
[]
);
/**
* modifyElementClass
*/
const modifyElementClass = useCallback(
async (element: HTMLElement, newClass: string) => {
const oldClasses = element.className;
const mergedClasses = twMerge(oldClasses, newClass);
// twMerge + DOM
element.className = mergedClasses;
// PATCH /update
await updateSource(element, mergedClasses, 'style', oldClasses);
// modifications list
const modification: Modification = {
id: Date.now().toString(),
element: element.id || 'unknown',
type: 'class',
oldValue: oldClasses,
newValue: mergedClasses,
timestamp: Date.now(),
};
setModifications(prev => [modification, ...prev]);
// STYLE_UPDATED to parent
if (config.iframeMode?.enabled) {
const sourceInfo = extractSourceInfo(element);
if (sourceInfo) {
sendToParent({
type: 'STYLE_UPDATED',
payload: {
sourceInfo,
oldClass: oldClasses,
newClass: mergedClasses,
},
timestamp: Date.now(),
});
}
}
},
[updateSource, config.iframeMode?.enabled]
);
/**
* updateElementContent
*/
const updateElementContent = useCallback(
async (element: HTMLElement, newContent: string) => {
const sourceInfo = extractSourceInfo(element);
const originalContent = element.innerText;
// twMerge + DOM
element.innerText = newContent;
// PATCH /update
await updateSource(element, newContent, 'content', originalContent);
// STYLE_UPDATED to parent
if (config.iframeMode?.enabled) {
const sourceInfo = extractSourceInfo(element);
if (sourceInfo) {
sendToParent({
type: 'CONTENT_UPDATED',
payload: {
sourceInfo,
oldValue: originalContent,
newValue: newContent,
},
timestamp: Date.now(),
});
}
}
},
[updateSource, config.iframeMode?.enabled]
);
/**
* batchUpdateElements
*/
const batchUpdateElements = useCallback(
async (
updates: Array<{
element: HTMLElement;
type: 'style' | 'content';
newValue: string;
originalValue?: string;
}>
) => {
if (!config.batchUpdate?.enabled) {
// Sequential fallback
await Promise.all(
updates.map(update => {
if (update.type === 'style') {
return modifyElementClass(update.element, update.newValue);
} else {
return updateElementContent(update.element, update.newValue);
}
})
);
return;
}
// Debounced queue
const newUpdates = [...pendingBatchUpdates, ...updates];
setPendingBatchUpdates(newUpdates);
// Reset debounce timer
if (batchUpdateTimer) {
clearTimeout(batchUpdateTimer);
}
// Schedule flush
const timer = setTimeout(async () => {
try {
// Build payload
const batchUpdateItems = newUpdates.map(update => {
const sourceInfo = extractSourceInfo(update.element);
if (!sourceInfo) {
throw new Error('Element missing source mapping');
}
return {
type: update.type,
sourceInfo,
newValue: update.newValue,
originalValue: update.originalValue,
};
});
// iframe: bridge BATCH_UPDATE
if (config.iframeMode?.enabled) {
await bridge.send({
type: 'BATCH_UPDATE',
payload: { updates: batchUpdateItems },
timestamp: Date.now(),
});
} else {
// top: fetch batch endpoint
await fetch('/__appdev_design_mode/batch-update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
updates: batchUpdateItems,
}),
});
}
// Clear queue
setPendingBatchUpdates([]);
} catch (error) {
console.error('[DesignMode] Batch update failed:', error);
setPendingBatchUpdates([]);
throw error;
}
}, config.batchUpdate.debounceMs);
setBatchUpdateTimer(timer);
},
[
config.batchUpdate,
pendingBatchUpdates,
batchUpdateTimer,
config.iframeMode?.enabled,
modifyElementClass,
updateElementContent,
]
);
/**
* resetModifications
*/
const resetModifications = useCallback(() => {
window.location.reload();
}, []);
/**
* postMessage when iframe + connected
*/
const sendMessage = useCallback(
async <T extends DesignModeMessage>(message: T) => {
if (config.iframeMode?.enabled) {
await bridge.send(message);
}
},
[config.iframeMode?.enabled]
);
/**
* sendMessageWithResponse
*/
const sendMessageWithResponse = useCallback(
async <T extends DesignModeMessage, R extends DesignModeMessage>(
message: T,
responseType: R['type']
): Promise<R> => {
if (config.iframeMode?.enabled) {
return await bridge.sendWithResponse(message, responseType);
}
throw new Error('Iframe mode is not enabled');
},
[config.iframeMode?.enabled]
);
/**
* healthCheck
*/
const healthCheck = useCallback(async () => {
if (config.iframeMode?.enabled) {
return await bridge.healthCheck();
}
return { status: 'healthy', details: { mode: 'standalone' } };
}, [config.iframeMode?.enabled]);
return (
<DesignModeContext.Provider
value={{
// State
isDesignMode,
selectedElement,
modifications,
isConnected,
bridgeStatus,
// Config
config,
// Actions
toggleDesignMode,
selectElement,
modifyElementClass,
updateElementContent,
batchUpdateElements,
resetModifications,
// Bridge helpers
sendMessage,
sendMessageWithResponse,
healthCheck,
}}
>
{children}
</DesignModeContext.Provider>
);
};
export const useDesignMode = () => {
const context = useContext(DesignModeContext);
if (context === undefined) {
throw new Error('useDesignMode must be used within a DesignModeProvider');
}
return context;
};