@growgroup/visual-editor 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -1
- package/dist/editor.css +1 -1
- package/package.json +1 -1
- package/src/editor/EditorContext.tsx +7 -3
- package/src/editor/FrontendVisualEditor.tsx +154 -54
- package/src/editor/autosave.ts +32 -0
- package/src/editor/components/EditorCanvas.tsx +93 -8
- package/src/editor/components/EditorHeader.tsx +47 -12
- package/src/editor/components/EditorPropertyPanel.tsx +2 -0
- package/src/editor/components/multi-page/CanvasRulers.tsx +166 -0
- package/src/editor/components/multi-page/MultiPageCanvasView.tsx +295 -1345
- package/src/editor/components/multi-page/PageFramePreview.tsx +164 -0
- package/src/editor/components/multi-page/index.ts +2 -6
- package/src/editor/components/ppt/PptComments.tsx +8 -4
- package/src/editor/components/shell/CanvasAppearance.tsx +3 -1
- package/src/editor/components/shell/PagesPanel.tsx +8 -0
- package/src/editor/contexts/EditorArtboardContext.tsx +8 -2
- package/src/editor/contexts/MultiPageCanvasContext.tsx +710 -358
- package/src/editor/hooks/useCanvasControls.ts +1 -1
- package/src/editor/hooks/useCanvasMarquee.ts +196 -0
- package/src/editor/hooks/useContextMenuHandler.ts +5 -14
- package/src/editor/hooks/useCoordinateTransform.ts +11 -8
- package/src/editor/hooks/useDrawingMode.ts +1 -0
- package/src/editor/hooks/useGoogleFonts.ts +6 -2
- package/src/editor/hooks/useInfiniteCanvas.ts +228 -220
- package/src/editor/hooks/useMarqueeSelection.ts +2 -2
- package/src/editor/utils/dom-utils.ts +7 -1
- package/src/editor/utils/embedded-canvas-bridge.ts +155 -0
- package/src/editor/utils/html-utils.ts +86 -11
- package/src/io.ts +20 -0
- package/src/styles/skin.css +66 -34
- package/src/editor/components/multi-page/CanvasZoomControls.tsx +0 -71
- package/src/editor/components/multi-page/InfiniteCanvas.tsx +0 -95
- package/src/editor/components/multi-page/PageFrameOverlay.tsx +0 -76
- package/src/editor/components/multi-page/PageLabel.tsx +0 -34
- package/src/editor/components/multi-page/PageLivePreview.tsx +0 -242
- package/src/editor/components/multi-page/PageThumbnail.tsx +0 -59
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { memo, useCallback } from 'react';
|
|
4
|
-
import { cn } from '../../../lib/utils';
|
|
5
|
-
import type { PageFrame } from '../../contexts/MultiPageCanvasContext';
|
|
6
|
-
import { PageLabel } from './PageLabel';
|
|
7
|
-
import { PageThumbnail } from './PageThumbnail';
|
|
8
|
-
|
|
9
|
-
interface PageFrameOverlayProps {
|
|
10
|
-
page: PageFrame;
|
|
11
|
-
isActive?: boolean;
|
|
12
|
-
onDoubleClick?: (pageId: string) => void;
|
|
13
|
-
onClick?: (pageId: string) => void;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* ページフレームオーバーレイ: 無限キャンバス上の各ページ表示
|
|
18
|
-
* - ページ名ラベル(Figma風、上部)
|
|
19
|
-
* - サムネイルプレビュー
|
|
20
|
-
* - ホバー時のリング表示
|
|
21
|
-
* - ダブルクリックでページフォーカス(Phase 2で完全実装)
|
|
22
|
-
*/
|
|
23
|
-
export const PageFrameOverlay = memo(function PageFrameOverlay({
|
|
24
|
-
page,
|
|
25
|
-
isActive = false,
|
|
26
|
-
onDoubleClick,
|
|
27
|
-
onClick,
|
|
28
|
-
}: PageFrameOverlayProps) {
|
|
29
|
-
const handleDoubleClick = useCallback((e: React.MouseEvent) => {
|
|
30
|
-
e.stopPropagation();
|
|
31
|
-
onDoubleClick?.(page.id);
|
|
32
|
-
}, [onDoubleClick, page.id]);
|
|
33
|
-
|
|
34
|
-
const handleClick = useCallback((e: React.MouseEvent) => {
|
|
35
|
-
e.stopPropagation();
|
|
36
|
-
onClick?.(page.id);
|
|
37
|
-
}, [onClick, page.id]);
|
|
38
|
-
|
|
39
|
-
return (
|
|
40
|
-
<div
|
|
41
|
-
className="absolute cursor-pointer group"
|
|
42
|
-
style={{
|
|
43
|
-
left: page.position.x,
|
|
44
|
-
top: page.position.y,
|
|
45
|
-
width: page.size.width,
|
|
46
|
-
height: page.size.height,
|
|
47
|
-
}}
|
|
48
|
-
onDoubleClick={handleDoubleClick}
|
|
49
|
-
onClick={handleClick}
|
|
50
|
-
>
|
|
51
|
-
{/* ページ名ラベル */}
|
|
52
|
-
<PageLabel
|
|
53
|
-
title={page.title}
|
|
54
|
-
isDirty={page.isDirty}
|
|
55
|
-
isActive={isActive}
|
|
56
|
-
/>
|
|
57
|
-
|
|
58
|
-
{/* サムネイルコンテナ */}
|
|
59
|
-
<div
|
|
60
|
-
className={cn(
|
|
61
|
-
'w-full h-full bg-white rounded-sm shadow-lg overflow-hidden',
|
|
62
|
-
'ring-2 transition-all duration-150',
|
|
63
|
-
isActive
|
|
64
|
-
? 'ring-[#0d99ff] shadow-blue-500/20'
|
|
65
|
-
: 'ring-transparent group-hover:ring-[#0d99ff]/50'
|
|
66
|
-
)}
|
|
67
|
-
>
|
|
68
|
-
<PageThumbnail
|
|
69
|
-
html={page.thumbnailHtml}
|
|
70
|
-
dataUrl={page.thumbnailDataUrl}
|
|
71
|
-
size={page.size}
|
|
72
|
-
/>
|
|
73
|
-
</div>
|
|
74
|
-
</div>
|
|
75
|
-
);
|
|
76
|
-
});
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { memo } from 'react';
|
|
4
|
-
import { cn } from '../../../lib/utils';
|
|
5
|
-
|
|
6
|
-
interface PageLabelProps {
|
|
7
|
-
title: string;
|
|
8
|
-
isDirty?: boolean;
|
|
9
|
-
isActive?: boolean;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* ページ名ラベル(Figma風: フレーム上部に表示)
|
|
14
|
-
*/
|
|
15
|
-
export const PageLabel = memo(function PageLabel({
|
|
16
|
-
title,
|
|
17
|
-
isDirty = false,
|
|
18
|
-
isActive = false,
|
|
19
|
-
}: PageLabelProps) {
|
|
20
|
-
return (
|
|
21
|
-
<div
|
|
22
|
-
className={cn(
|
|
23
|
-
'absolute -top-7 left-0 px-1.5 py-0.5 text-[11px] font-medium truncate max-w-[200px]',
|
|
24
|
-
'select-none pointer-events-none',
|
|
25
|
-
'text-gray-400'
|
|
26
|
-
)}
|
|
27
|
-
>
|
|
28
|
-
{title}
|
|
29
|
-
{isDirty && (
|
|
30
|
-
<span className="ml-1 text-orange-400">*</span>
|
|
31
|
-
)}
|
|
32
|
-
</div>
|
|
33
|
-
);
|
|
34
|
-
});
|
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { memo, useRef, useEffect } from 'react';
|
|
4
|
-
|
|
5
|
-
interface PageLivePreviewProps {
|
|
6
|
-
html: string;
|
|
7
|
-
pageId: string;
|
|
8
|
-
/** iframe読み込み完了時にiframe要素を親に通知 */
|
|
9
|
-
onIframeLoad?: (pageId: string, iframe: HTMLIFrameElement) => void;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Multi-page canvas用のシンプルHTML生成
|
|
14
|
-
* body > #artboard > content のフラット構造
|
|
15
|
-
*/
|
|
16
|
-
function generateCanvasPageHtml(content: string): string {
|
|
17
|
-
const baseUrl = typeof window !== 'undefined' ? window.location.origin : '';
|
|
18
|
-
const scriptUrl = baseUrl
|
|
19
|
-
? `${baseUrl}/vendor/tailwindcss-browser.js`
|
|
20
|
-
: 'https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4';
|
|
21
|
-
|
|
22
|
-
return `<!DOCTYPE html>
|
|
23
|
-
<html lang="ja">
|
|
24
|
-
<head>
|
|
25
|
-
<meta charset="UTF-8">
|
|
26
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
|
|
27
|
-
<script src="${scriptUrl}"></script>
|
|
28
|
-
<style type="text/tailwindcss">
|
|
29
|
-
@layer base {}
|
|
30
|
-
@theme { --font-sans: 'Noto Sans JP', system-ui, sans-serif; }
|
|
31
|
-
</style>
|
|
32
|
-
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100..900&display=swap" rel="stylesheet">
|
|
33
|
-
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
|
34
|
-
<link href="https://fonts.googleapis.com/icon?family=Material+Icons+Outlined" rel="stylesheet">
|
|
35
|
-
<style>
|
|
36
|
-
* { box-sizing: border-box; }
|
|
37
|
-
html { margin: 0; padding: 0; touch-action: none; }
|
|
38
|
-
body { margin: 0; padding: 0; width: 100%; background: white; font-family: 'Noto Sans JP', system-ui, sans-serif; overflow: hidden; touch-action: none; }
|
|
39
|
-
#artboard { position: relative; }
|
|
40
|
-
</style>
|
|
41
|
-
</head>
|
|
42
|
-
<body>
|
|
43
|
-
<div id="artboard">${content}</div>
|
|
44
|
-
</body>
|
|
45
|
-
</html>`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* 全ページ同時編集可能なライブiframeプレビュー
|
|
50
|
-
* - シンプルHTML構造(ラッパーなし)
|
|
51
|
-
* - コンテンツ高さ自動測定 → 親にpostMessage
|
|
52
|
-
* - 編集ロジックは親(MultiPageCanvasView)が管理
|
|
53
|
-
*/
|
|
54
|
-
export const PageLivePreview = memo(function PageLivePreview({
|
|
55
|
-
html,
|
|
56
|
-
pageId,
|
|
57
|
-
onIframeLoad,
|
|
58
|
-
}: PageLivePreviewProps) {
|
|
59
|
-
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
60
|
-
const isLoadedRef = useRef(false);
|
|
61
|
-
|
|
62
|
-
// HTML初回読み込み(一度だけ)
|
|
63
|
-
// 初回ロード後はDOM操作で直接編集されるため、html prop変更でのiframe再読込は不要
|
|
64
|
-
useEffect(() => {
|
|
65
|
-
const iframe = iframeRef.current;
|
|
66
|
-
if (!iframe) return;
|
|
67
|
-
if (!html?.trim()) return;
|
|
68
|
-
if (isLoadedRef.current) return;
|
|
69
|
-
|
|
70
|
-
isLoadedRef.current = true;
|
|
71
|
-
const fullHtml = generateCanvasPageHtml(html);
|
|
72
|
-
const blob = new Blob([fullHtml], { type: 'text/html' });
|
|
73
|
-
const url = URL.createObjectURL(blob);
|
|
74
|
-
iframe.src = url;
|
|
75
|
-
|
|
76
|
-
return () => URL.revokeObjectURL(url);
|
|
77
|
-
}, [html]);
|
|
78
|
-
|
|
79
|
-
// 最小限のスクリプト注入(ID付与・高さ測定・イベント転送のみ)
|
|
80
|
-
useEffect(() => {
|
|
81
|
-
const iframe = iframeRef.current;
|
|
82
|
-
if (!iframe) return;
|
|
83
|
-
|
|
84
|
-
const injectMinimalScripts = (doc: Document) => {
|
|
85
|
-
if (doc.querySelector('[data-canvas-init]')) return;
|
|
86
|
-
|
|
87
|
-
const script = doc.createElement('script');
|
|
88
|
-
script.setAttribute('data-canvas-init', 'true');
|
|
89
|
-
script.textContent = `(function(){
|
|
90
|
-
var PAGE_ID = ${JSON.stringify(pageId)};
|
|
91
|
-
var artboard = document.getElementById('artboard');
|
|
92
|
-
if (!artboard) return;
|
|
93
|
-
|
|
94
|
-
// === Assign data-element-id + data-editable to ALL elements ===
|
|
95
|
-
var idCounter = 0;
|
|
96
|
-
function assignElementIds(parent, isArtboardChild) {
|
|
97
|
-
for (var i = 0; i < parent.children.length; i++) {
|
|
98
|
-
var child = parent.children[i];
|
|
99
|
-
if (child.nodeType !== 1) continue;
|
|
100
|
-
if (child.tagName === 'SCRIPT' || child.tagName === 'STYLE') continue;
|
|
101
|
-
if (!child.getAttribute('data-element-id')) {
|
|
102
|
-
child.setAttribute('data-element-id', 'el-' + (idCounter++));
|
|
103
|
-
}
|
|
104
|
-
// artboard直下の子要素にdata-editableを付与(レイアウトモード変換で必要)
|
|
105
|
-
if (isArtboardChild && !child.hasAttribute('data-editable')) {
|
|
106
|
-
child.setAttribute('data-editable', 'true');
|
|
107
|
-
}
|
|
108
|
-
assignElementIds(child, false);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
assignElementIds(artboard, true);
|
|
112
|
-
|
|
113
|
-
// === Height measurement (one-shot) ===
|
|
114
|
-
var heightReported = false;
|
|
115
|
-
function reportHeight() {
|
|
116
|
-
if (heightReported) return;
|
|
117
|
-
var h = artboard.scrollHeight || document.body.scrollHeight;
|
|
118
|
-
if (h > 0) {
|
|
119
|
-
h = Math.min(h, 8000);
|
|
120
|
-
heightReported = true;
|
|
121
|
-
window.parent.postMessage({ type: 'PAGE_CONTENT_HEIGHT', pageId: PAGE_ID, height: h }, '*');
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
setTimeout(reportHeight, 500);
|
|
125
|
-
setTimeout(reportHeight, 1500);
|
|
126
|
-
|
|
127
|
-
// === Disable trackpad/keyboard zoom natively ===
|
|
128
|
-
var preventZoom = function(e) {
|
|
129
|
-
if (e.ctrlKey || e.metaKey) {
|
|
130
|
-
e.preventDefault();
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
window.addEventListener('wheel', preventZoom, { passive: false, capture: true });
|
|
134
|
-
document.addEventListener('wheel', preventZoom, { passive: false, capture: true });
|
|
135
|
-
|
|
136
|
-
var preventShortcuts = function(e) {
|
|
137
|
-
if ((e.ctrlKey || e.metaKey) && (e.key === '+' || e.key === '=' || e.key === '-' || e.key === '0')) {
|
|
138
|
-
e.preventDefault();
|
|
139
|
-
}
|
|
140
|
-
};
|
|
141
|
-
window.addEventListener('keydown', preventShortcuts, { passive: false, capture: true });
|
|
142
|
-
document.addEventListener('keydown', preventShortcuts, { passive: false, capture: true });
|
|
143
|
-
|
|
144
|
-
// === Prevent Safari gesture event native behavior ===
|
|
145
|
-
var preventGesture = function(e) {
|
|
146
|
-
e.preventDefault();
|
|
147
|
-
};
|
|
148
|
-
window.addEventListener('gesturestart', preventGesture, { passive: false, capture: true });
|
|
149
|
-
window.addEventListener('gesturechange', preventGesture, { passive: false, capture: true });
|
|
150
|
-
window.addEventListener('gestureend', preventGesture, { passive: false, capture: true });
|
|
151
|
-
document.addEventListener('gesturestart', preventGesture, { passive: false, capture: true });
|
|
152
|
-
document.addEventListener('gesturechange', preventGesture, { passive: false, capture: true });
|
|
153
|
-
document.addEventListener('gestureend', preventGesture, { passive: false, capture: true });
|
|
154
|
-
|
|
155
|
-
// === Wheel forwarding ===
|
|
156
|
-
window.addEventListener('wheel', function(e) {
|
|
157
|
-
window.parent.postMessage({
|
|
158
|
-
type: 'PAGE_PREVIEW_WHEEL',
|
|
159
|
-
deltaX: e.deltaX, deltaY: e.deltaY,
|
|
160
|
-
ctrlKey: e.ctrlKey, metaKey: e.metaKey
|
|
161
|
-
}, '*');
|
|
162
|
-
}, { passive: false });
|
|
163
|
-
|
|
164
|
-
// === Touch forwarding for pinch zoom ===
|
|
165
|
-
var lastTouches = null;
|
|
166
|
-
document.addEventListener('touchstart', function(e) {
|
|
167
|
-
if (e.touches.length >= 2) {
|
|
168
|
-
e.preventDefault();
|
|
169
|
-
lastTouches = [];
|
|
170
|
-
for (var i = 0; i < e.touches.length; i++) {
|
|
171
|
-
lastTouches.push({ clientX: e.touches[i].clientX, clientY: e.touches[i].clientY });
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}, { passive: false });
|
|
175
|
-
|
|
176
|
-
document.addEventListener('touchmove', function(e) {
|
|
177
|
-
if (e.touches.length >= 2) {
|
|
178
|
-
e.preventDefault();
|
|
179
|
-
var touches = [];
|
|
180
|
-
for (var i = 0; i < e.touches.length; i++) {
|
|
181
|
-
touches.push({ clientX: e.touches[i].clientX, clientY: e.touches[i].clientY });
|
|
182
|
-
}
|
|
183
|
-
window.parent.postMessage({
|
|
184
|
-
type: 'PAGE_PREVIEW_TOUCH',
|
|
185
|
-
touchType: 'move',
|
|
186
|
-
touches: touches,
|
|
187
|
-
prevTouches: lastTouches
|
|
188
|
-
}, '*');
|
|
189
|
-
lastTouches = touches;
|
|
190
|
-
}
|
|
191
|
-
}, { passive: false });
|
|
192
|
-
|
|
193
|
-
document.addEventListener('touchend', function() {
|
|
194
|
-
lastTouches = null;
|
|
195
|
-
});
|
|
196
|
-
document.addEventListener('touchcancel', function() {
|
|
197
|
-
lastTouches = null;
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
// === Prevent link navigation ===
|
|
201
|
-
document.addEventListener('click', function(e) {
|
|
202
|
-
var a = e.target.closest('a');
|
|
203
|
-
if (a) e.preventDefault();
|
|
204
|
-
}, true);
|
|
205
|
-
})();`;
|
|
206
|
-
doc.body.appendChild(script);
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
try {
|
|
210
|
-
const doc = iframe.contentDocument;
|
|
211
|
-
if (doc && doc.readyState === 'complete' && doc.body) injectMinimalScripts(doc);
|
|
212
|
-
} catch { /* cross-origin */ }
|
|
213
|
-
|
|
214
|
-
const handleLoad = () => {
|
|
215
|
-
try {
|
|
216
|
-
const doc = iframe.contentDocument;
|
|
217
|
-
if (doc) injectMinimalScripts(doc);
|
|
218
|
-
} catch { /* cross-origin */ }
|
|
219
|
-
if (onIframeLoad && iframe) onIframeLoad(pageId, iframe);
|
|
220
|
-
};
|
|
221
|
-
iframe.addEventListener('load', handleLoad);
|
|
222
|
-
return () => iframe.removeEventListener('load', handleLoad);
|
|
223
|
-
}, [pageId, onIframeLoad]);
|
|
224
|
-
|
|
225
|
-
if (!html?.trim()) {
|
|
226
|
-
return (
|
|
227
|
-
<div className="w-full h-full flex items-center justify-center bg-gray-100">
|
|
228
|
-
<span className="text-gray-400 text-sm">Empty Page</span>
|
|
229
|
-
</div>
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return (
|
|
234
|
-
<iframe
|
|
235
|
-
ref={iframeRef}
|
|
236
|
-
className="w-full h-full border-0"
|
|
237
|
-
style={{ touchAction: 'none', overscrollBehavior: 'none' }}
|
|
238
|
-
sandbox="allow-same-origin allow-scripts"
|
|
239
|
-
title={`Page ${pageId}`}
|
|
240
|
-
/>
|
|
241
|
-
);
|
|
242
|
-
});
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { memo } from 'react';
|
|
4
|
-
import { WEBPAGE_WIDTH, WEBPAGE_MIN_HEIGHT } from '../../contexts/MultiPageCanvasContext';
|
|
5
|
-
|
|
6
|
-
interface PageThumbnailProps {
|
|
7
|
-
html: string;
|
|
8
|
-
dataUrl: string | null;
|
|
9
|
-
size: { width: number; height: number };
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* ページサムネイル: CSS scaleで縮小描画
|
|
14
|
-
* dataUrl(高品質キャプチャ)がある場合はそれを表示、
|
|
15
|
-
* なければHTMLをdangerouslySetInnerHTMLで描画
|
|
16
|
-
*/
|
|
17
|
-
export const PageThumbnail = memo(function PageThumbnail({
|
|
18
|
-
html,
|
|
19
|
-
dataUrl,
|
|
20
|
-
size,
|
|
21
|
-
}: PageThumbnailProps) {
|
|
22
|
-
// 高品質サムネイルがあればそれを表示
|
|
23
|
-
if (dataUrl) {
|
|
24
|
-
return (
|
|
25
|
-
<img
|
|
26
|
-
src={dataUrl}
|
|
27
|
-
alt=""
|
|
28
|
-
className="w-full h-full object-cover object-top"
|
|
29
|
-
draggable={false}
|
|
30
|
-
/>
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// HTMLが空の場合はプレースホルダー
|
|
35
|
-
if (!html?.trim()) {
|
|
36
|
-
return (
|
|
37
|
-
<div className="w-full h-full flex items-center justify-center bg-gray-100">
|
|
38
|
-
<span className="text-gray-400 text-sm">Empty Page</span>
|
|
39
|
-
</div>
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// HTMLをCSS scaleで縮小描画
|
|
44
|
-
const scale = size.width / WEBPAGE_WIDTH;
|
|
45
|
-
|
|
46
|
-
return (
|
|
47
|
-
<div className="w-full h-full overflow-hidden">
|
|
48
|
-
<div
|
|
49
|
-
className="origin-top-left pointer-events-none"
|
|
50
|
-
style={{
|
|
51
|
-
width: WEBPAGE_WIDTH,
|
|
52
|
-
height: WEBPAGE_MIN_HEIGHT,
|
|
53
|
-
transform: `scale(${scale})`,
|
|
54
|
-
}}
|
|
55
|
-
dangerouslySetInnerHTML={{ __html: html }}
|
|
56
|
-
/>
|
|
57
|
-
</div>
|
|
58
|
-
);
|
|
59
|
-
});
|