@growgroup/visual-editor 0.2.2 → 0.3.1
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 +41 -1
- package/dist/editor.css +1 -1
- package/package.json +3 -2
- package/src/editor/EditorContext.tsx +15 -3
- package/src/editor/FrontendVisualEditor.tsx +168 -55
- package/src/editor/autosave.ts +32 -0
- package/src/editor/components/EditorCanvas.tsx +107 -8
- package/src/editor/components/EditorContextMenu.tsx +12 -3
- package/src/editor/components/EditorHeader.tsx +47 -12
- package/src/editor/components/EditorLayerPanel.tsx +28 -2
- package/src/editor/components/EditorPropertyPanel.tsx +56 -25
- package/src/editor/components/multi-page/CanvasRulers.tsx +166 -0
- package/src/editor/components/multi-page/MultiPageCanvasView.tsx +345 -1345
- package/src/editor/components/multi-page/PageFramePreview.tsx +197 -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/constants.ts +28 -0
- package/src/editor/contexts/EditorArtboardContext.tsx +60 -3
- package/src/editor/contexts/EditorComponentsContext.tsx +17 -4
- package/src/editor/contexts/MultiPageCanvasContext.tsx +859 -354
- 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 +23 -1
- package/src/editor/utils/embedded-canvas-bridge.ts +155 -0
- package/src/editor/utils/html-utils.ts +102 -14
- package/src/index.ts +5 -0
- package/src/io.ts +20 -0
- package/src/styles/skin.css +122 -36
- package/src/version.ts +9 -0
- 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
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 埋め込み(マルチフレームのキャンバス)のとき、生きているエディタの iframe で起きた
|
|
3
|
+
* 「キャンバス全体に向けた操作」を親へ転送する。
|
|
4
|
+
*
|
|
5
|
+
* iframe の中で起きたホイール・キー・中ボタンは親には届かない。等倍の単独表示では
|
|
6
|
+
* iframe 自身が #canvas-container をスクロールして受けていたが、埋め込みでは
|
|
7
|
+
* 倍率も位置も外側(useInfiniteCanvas)が持つので、そのまま外へ流す。
|
|
8
|
+
* 座標は iframe の座標のまま送る(受け手が送り元 iframe の実測矩形で親の座標へ直す)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const ZOOM_KEYS = new Set(['=', '+', '-', '_', ';']);
|
|
12
|
+
|
|
13
|
+
function isTypingTarget(target: EventTarget | null): boolean {
|
|
14
|
+
const el = target as HTMLElement | null;
|
|
15
|
+
if (!el || typeof el.closest !== 'function') return false;
|
|
16
|
+
return !!el.closest('input, textarea, select, [contenteditable="true"], [role="textbox"]');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function setupEmbeddedCanvasBridge(iframeDoc: Document, iframe: HTMLIFrameElement): () => void {
|
|
20
|
+
const parent = iframeDoc.defaultView?.parent;
|
|
21
|
+
if (!parent) return () => undefined;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* iframe の座標 → 親の client 座標。
|
|
25
|
+
* このリスナーは親の realm で作られた関数なので postMessage の e.source は親自身になり、
|
|
26
|
+
* 受け手が送り元の iframe を特定できない。送る前にここで親の座標へ直す(coords: 'parent')
|
|
27
|
+
*/
|
|
28
|
+
const toParent = (x: number, y: number) => {
|
|
29
|
+
const rect = iframe.getBoundingClientRect();
|
|
30
|
+
const scale = rect.width / (iframe.clientWidth || rect.width) || 1;
|
|
31
|
+
return { clientX: rect.left + x * scale, clientY: rect.top + y * scale };
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// ---- ホイール(すべて外へ。iframe の中にはスクロールする物が無い)
|
|
35
|
+
const handleWheel = (e: WheelEvent) => {
|
|
36
|
+
e.preventDefault();
|
|
37
|
+
e.stopPropagation();
|
|
38
|
+
parent.postMessage(
|
|
39
|
+
{
|
|
40
|
+
type: 'EMBEDDED_WHEEL',
|
|
41
|
+
coords: 'parent',
|
|
42
|
+
deltaX: e.deltaMode === 1 ? e.deltaX * 16 : e.deltaMode === 2 ? e.deltaX * 100 : e.deltaX,
|
|
43
|
+
deltaY: e.deltaMode === 1 ? e.deltaY * 16 : e.deltaMode === 2 ? e.deltaY * 100 : e.deltaY,
|
|
44
|
+
...toParent(e.clientX, e.clientY),
|
|
45
|
+
ctrlKey: e.ctrlKey,
|
|
46
|
+
metaKey: e.metaKey,
|
|
47
|
+
shiftKey: e.shiftKey,
|
|
48
|
+
},
|
|
49
|
+
'*',
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// ---- Safari の gesture(ピンチ)はブラウザ拡大を止めるだけ
|
|
54
|
+
const handleGesture = (e: Event) => {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
e.stopPropagation();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// ---- 2 本指タッチ = ピンチ
|
|
60
|
+
let lastDistance: number | null = null;
|
|
61
|
+
const distance = (t: TouchList) => (t.length < 2 ? 0 : Math.hypot(t[1].clientX - t[0].clientX, t[1].clientY - t[0].clientY));
|
|
62
|
+
const handleTouchStart = (e: TouchEvent) => {
|
|
63
|
+
if (e.touches.length >= 2) {
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
lastDistance = distance(e.touches);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const handleTouchMove = (e: TouchEvent) => {
|
|
69
|
+
if (e.touches.length < 2) return;
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
if (lastDistance == null) return;
|
|
72
|
+
const current = distance(e.touches);
|
|
73
|
+
const center = toParent((e.touches[0].clientX + e.touches[1].clientX) / 2, (e.touches[0].clientY + e.touches[1].clientY) / 2);
|
|
74
|
+
parent.postMessage(
|
|
75
|
+
{
|
|
76
|
+
type: 'EMBEDDED_PINCH',
|
|
77
|
+
coords: 'parent',
|
|
78
|
+
previousDistance: lastDistance,
|
|
79
|
+
currentDistance: current,
|
|
80
|
+
centerX: center.clientX,
|
|
81
|
+
centerY: center.clientY,
|
|
82
|
+
},
|
|
83
|
+
'*',
|
|
84
|
+
);
|
|
85
|
+
lastDistance = current;
|
|
86
|
+
};
|
|
87
|
+
const handleTouchEnd = (e: TouchEvent) => {
|
|
88
|
+
if (e.touches.length < 2) lastDistance = null;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// ---- キー: Space(パン)、Cmd+/- (ズーム)、Shift+0/1/2(100%・全体・このページ)、Shift+R(定規)
|
|
92
|
+
const forwardKey = (kind: 'down' | 'up') => (e: KeyboardEvent) => {
|
|
93
|
+
const cmd = e.metaKey || e.ctrlKey;
|
|
94
|
+
const typing = isTypingTarget(e.target);
|
|
95
|
+
const isSpace = e.code === 'Space';
|
|
96
|
+
const isZoomKey = cmd && ZOOM_KEYS.has(e.key);
|
|
97
|
+
const isShiftView = !cmd && e.shiftKey && !e.altKey && (['0', '1', '2'].includes(e.key) || ['Digit0', 'Digit1', 'Digit2', 'KeyR'].includes(e.code) || e.key.toLowerCase() === 'r');
|
|
98
|
+
if (!isSpace && !isZoomKey && !isShiftView) return;
|
|
99
|
+
if (typing) return;
|
|
100
|
+
if (isSpace && e.repeat) {
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (kind === 'down') e.preventDefault();
|
|
105
|
+
parent.postMessage(
|
|
106
|
+
{
|
|
107
|
+
type: 'EMBEDDED_KEY',
|
|
108
|
+
kind,
|
|
109
|
+
key: e.key,
|
|
110
|
+
code: e.code,
|
|
111
|
+
metaKey: e.metaKey,
|
|
112
|
+
ctrlKey: e.ctrlKey,
|
|
113
|
+
shiftKey: e.shiftKey,
|
|
114
|
+
altKey: e.altKey,
|
|
115
|
+
},
|
|
116
|
+
'*',
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
const handleKeyDown = forwardKey('down');
|
|
120
|
+
const handleKeyUp = forwardKey('up');
|
|
121
|
+
|
|
122
|
+
// ---- 中ボタン = パンの開始(以降のマウス移動は親が受ける: 親が iframe の pointer-events を切る)
|
|
123
|
+
const handleMouseDown = (e: MouseEvent) => {
|
|
124
|
+
if (e.button !== 1) return;
|
|
125
|
+
e.preventDefault();
|
|
126
|
+
parent.postMessage({ type: 'EMBEDDED_PAN_START', coords: 'parent', ...toParent(e.clientX, e.clientY) }, '*');
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const options: AddEventListenerOptions = { passive: false, capture: true };
|
|
130
|
+
iframeDoc.addEventListener('wheel', handleWheel, options);
|
|
131
|
+
iframeDoc.addEventListener('gesturestart', handleGesture, options);
|
|
132
|
+
iframeDoc.addEventListener('gesturechange', handleGesture, options);
|
|
133
|
+
iframeDoc.addEventListener('gestureend', handleGesture, options);
|
|
134
|
+
iframeDoc.addEventListener('touchstart', handleTouchStart, options);
|
|
135
|
+
iframeDoc.addEventListener('touchmove', handleTouchMove, options);
|
|
136
|
+
iframeDoc.addEventListener('touchend', handleTouchEnd, options);
|
|
137
|
+
iframeDoc.addEventListener('touchcancel', handleTouchEnd, options);
|
|
138
|
+
iframeDoc.addEventListener('keydown', handleKeyDown, options);
|
|
139
|
+
iframeDoc.addEventListener('keyup', handleKeyUp, options);
|
|
140
|
+
iframeDoc.addEventListener('mousedown', handleMouseDown, options);
|
|
141
|
+
|
|
142
|
+
return () => {
|
|
143
|
+
iframeDoc.removeEventListener('wheel', handleWheel, options);
|
|
144
|
+
iframeDoc.removeEventListener('gesturestart', handleGesture, options);
|
|
145
|
+
iframeDoc.removeEventListener('gesturechange', handleGesture, options);
|
|
146
|
+
iframeDoc.removeEventListener('gestureend', handleGesture, options);
|
|
147
|
+
iframeDoc.removeEventListener('touchstart', handleTouchStart, options);
|
|
148
|
+
iframeDoc.removeEventListener('touchmove', handleTouchMove, options);
|
|
149
|
+
iframeDoc.removeEventListener('touchend', handleTouchEnd, options);
|
|
150
|
+
iframeDoc.removeEventListener('touchcancel', handleTouchEnd, options);
|
|
151
|
+
iframeDoc.removeEventListener('keydown', handleKeyDown, options);
|
|
152
|
+
iframeDoc.removeEventListener('keyup', handleKeyUp, options);
|
|
153
|
+
iframeDoc.removeEventListener('mousedown', handleMouseDown, options);
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { SLIDE_WIDTH, SLIDE_HEIGHT, WEBPAGE_WIDTH, WEBPAGE_MIN_HEIGHT } from '../constants';
|
|
6
|
+
import type { PreviewStyle } from '../../io';
|
|
6
7
|
import type { EditorMode } from '../EditorContext';
|
|
7
8
|
import { prepareHtmlForSave } from './viewport-utils';
|
|
8
9
|
import { classifySaveAndRevert } from './dom-utils';
|
|
@@ -133,21 +134,29 @@ ${collectHostThemeTokens()}
|
|
|
133
134
|
* - #artboard-wrapper: transform適用対象(ズーム/パン)
|
|
134
135
|
* - #artboard: 白背景、コンテンツ配置エリア
|
|
135
136
|
*/
|
|
136
|
-
export
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
137
|
+
export interface EditableHtmlOptions {
|
|
138
|
+
/**
|
|
139
|
+
* マルチフレームのキャンバスに埋め込む姿。
|
|
140
|
+
* 倍率は外側(親)の CSS transform が持つので、iframe の中は等倍・スクロール無しにする。
|
|
141
|
+
* #canvas-container 等の id は残す(数十か所の getElementById を壊さないため)
|
|
142
|
+
*/
|
|
143
|
+
embedded?: boolean;
|
|
144
|
+
/** webpage の版面の幅。省略時は WEBPAGE_WIDTH(EditorCanvas が後から viewportWidth で上書きする) */
|
|
145
|
+
artboardWidth?: number;
|
|
146
|
+
}
|
|
140
147
|
|
|
148
|
+
/**
|
|
149
|
+
* 紙面の外側で共通に読む <head> の中身(Tailwind の注入判定・フォント)。
|
|
150
|
+
* 編集用(generateEditableHtml)と見るだけ(generatePreviewHtml)で同じ判定を使う。
|
|
151
|
+
* 別々に持つと、片方だけ Tailwind を二重注入して見え方が食い違う
|
|
152
|
+
*/
|
|
153
|
+
function buildCommonHead(content: string): string {
|
|
141
154
|
// コンテンツにTailwind CSSが含まれていない場合は注入
|
|
142
155
|
// 外部スタイルがある場合はセーフモードを使用(既存スタイルを保護)
|
|
143
156
|
const needsTailwind = !hasTailwindCss(content);
|
|
144
157
|
const useSafeMode = needsTailwind && hasExternalStyles(content);
|
|
145
158
|
const tailwindScript = needsTailwind ? getTailwindScriptHtml(useSafeMode) : '';
|
|
146
|
-
|
|
147
159
|
return `
|
|
148
|
-
<!DOCTYPE html>
|
|
149
|
-
<html lang="ja">
|
|
150
|
-
<head>
|
|
151
160
|
<meta charset="UTF-8">
|
|
152
161
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
153
162
|
${tailwindScript}
|
|
@@ -156,9 +165,91 @@ export function generateEditableHtml(content: string, editorMode: EditorMode = '
|
|
|
156
165
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100;200;300;400;500;600;700;800;900&display=swap" rel="stylesheet">
|
|
157
166
|
<!-- スライドの欧文(font-en)。読み込まないと游ゴシックで代替され、
|
|
158
167
|
数字・フッターの幅がビューアと10px以上ずれる(検証で実測) -->
|
|
159
|
-
<link href="https://fonts.googleapis.com/css2?family=Jost:wght@300;400;500;600;700&display=swap" rel="stylesheet"
|
|
168
|
+
<link href="https://fonts.googleapis.com/css2?family=Jost:wght@300;400;500;600;700&display=swap" rel="stylesheet">`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 見るだけの紙面(マルチフレームのキャンバスで、編集していないページの姿)。
|
|
173
|
+
* 編集用と同じ head を使い、body は等倍・白地・スクロール無し。
|
|
174
|
+
* 編集用の属性は付けない(選択も編集も、このページを開いてから)
|
|
175
|
+
*/
|
|
176
|
+
export function generatePreviewHtml(
|
|
177
|
+
content: string,
|
|
178
|
+
editorMode: EditorMode = 'slide',
|
|
179
|
+
artboardWidth?: number,
|
|
180
|
+
styles: PreviewStyle[] = [],
|
|
181
|
+
): string {
|
|
182
|
+
// 利用側が渡したスタイル(コンパイル済み Tailwind など)。head の共通部分の後、紙面の基本 CSS の前
|
|
183
|
+
const extraStyles = styles
|
|
184
|
+
.map((s) => ('href' in s ? `<link rel="stylesheet" href="${s.href.replace(/"/g, '"')}">` : `<style data-preview-style>${s.css.replace(/<\/style/gi, '<\\/style')}</style>`))
|
|
185
|
+
.join('\n ');
|
|
186
|
+
const width = editorMode === 'webpage' ? (artboardWidth ?? WEBPAGE_WIDTH) : SLIDE_WIDTH;
|
|
187
|
+
const heightRule = editorMode === 'webpage' ? '' : `height: ${SLIDE_HEIGHT}px; overflow: hidden;`;
|
|
188
|
+
// 見るだけの紙面では script を動かさない(ブラウザ版 Tailwind の JIT やページの script は要らない)。
|
|
189
|
+
// <script> と、勝手に発火しうるインラインハンドラ(onload / onerror 等)を本文からも head からも外す。
|
|
190
|
+
// iframe に sandbox は付けない: sandbox で script を禁じると、ページへ script を差し込む
|
|
191
|
+
// ブラウザ拡張(React DevTools 等)が 1 枚ごとに「Blocked script execution」をコンソールへ出す(実測 26 枚 = 26 件)
|
|
192
|
+
const stripScripts = (html: string) =>
|
|
193
|
+
html
|
|
194
|
+
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
195
|
+
.replace(/\son(?:load|error|abort|click|dblclick|mouse\w*|pointer\w*|touch\w*|key\w*|focus\w*|blur|change|input|submit|reset|scroll|wheel|animation\w*|transition\w*|toggle|play\w*|pause|ended|canplay\w*|loaded\w*)\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, '');
|
|
196
|
+
return `<!DOCTYPE html>
|
|
197
|
+
<html lang="ja">
|
|
198
|
+
<head>${stripScripts(buildCommonHead(content))}
|
|
199
|
+
${extraStyles}
|
|
200
|
+
<style>
|
|
201
|
+
* { box-sizing: border-box; }
|
|
202
|
+
html, body { margin: 0; padding: 0; background: transparent; overflow: hidden; }
|
|
203
|
+
body { width: ${width}px; }
|
|
204
|
+
#artboard { position: relative; width: ${width}px; ${heightRule} background: white; }
|
|
205
|
+
/* 見るだけなので、リンク・入力を触れなくする */
|
|
206
|
+
#artboard a, #artboard button, #artboard input, #artboard textarea, #artboard select { pointer-events: none; }
|
|
207
|
+
</style>
|
|
208
|
+
</head>
|
|
209
|
+
<body data-editor-mode="${editorMode}" data-preview="1">
|
|
210
|
+
<div id="artboard">${stripScripts(content)}</div>
|
|
211
|
+
</body>
|
|
212
|
+
</html>`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function generateEditableHtml(
|
|
216
|
+
content: string,
|
|
217
|
+
editorMode: EditorMode = 'slide',
|
|
218
|
+
options: EditableHtmlOptions = {},
|
|
219
|
+
): string {
|
|
220
|
+
const embedded = !!options.embedded;
|
|
221
|
+
const artboardWidth = editorMode === 'webpage' ? (options.artboardWidth ?? WEBPAGE_WIDTH) : SLIDE_WIDTH;
|
|
222
|
+
const artboardHeight = editorMode === 'webpage' ? WEBPAGE_MIN_HEIGHT : SLIDE_HEIGHT;
|
|
223
|
+
const artboardHeightStyle = editorMode === 'webpage' ? 'min-height' : 'height';
|
|
224
|
+
|
|
225
|
+
// 埋め込み: 倍率もスクロールも外側が持つ。中は「紙面がそのまま置いてある」だけの姿にする。
|
|
226
|
+
// id は残し(getElementById の呼び出しを壊さない)、役割だけ CSS で外す
|
|
227
|
+
const embeddedCss = embedded
|
|
228
|
+
? `
|
|
229
|
+
html, body { overflow: visible; height: auto; background: transparent; }
|
|
230
|
+
#canvas-container { position: static; overflow: visible; background: transparent; }
|
|
231
|
+
#canvas-scroll-area { display: block; padding: 0 !important; width: auto !important; height: auto !important; min-width: 0 !important; min-height: 0 !important; }
|
|
232
|
+
#artboard-wrapper { transform: none !important; visibility: visible; }
|
|
233
|
+
#artboard { box-shadow: none; ${editorMode === 'webpage' ? 'min-height: 200px;' : ''} }`
|
|
234
|
+
: '';
|
|
235
|
+
|
|
236
|
+
return `
|
|
237
|
+
<!DOCTYPE html>
|
|
238
|
+
<html lang="ja">
|
|
239
|
+
<head>${buildCommonHead(content)}
|
|
160
240
|
<style>
|
|
161
241
|
* { box-sizing: border-box; }
|
|
242
|
+
/* 既定の書体はカスケードレイヤーに入れて置く(レイヤーに入れた宣言は、レイヤー外の宣言と、
|
|
243
|
+
後から宣言されたレイヤーの両方に負ける)。ページ側の CSS が html { font-family } を
|
|
244
|
+
素で書いていても、Tailwind v4 のように @layer base に書いていても、そちらが勝つ。
|
|
245
|
+
以前は html, body に直接書いていたため、構成ラフの @layer base の html { font-family: "Noto Sans JP" } が
|
|
246
|
+
負けて、エディタだけシステム書体で表示されていた(実測。:where() でも @layer には勝ってしまう)。
|
|
247
|
+
スライド本体は #artboard 内のルートが font-sans を持つのでどちらでも影響しない */
|
|
248
|
+
@layer gg-editor-defaults {
|
|
249
|
+
html {
|
|
250
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Hiragino Sans', 'Noto Sans JP', Meiryo, sans-serif;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
162
253
|
html, body {
|
|
163
254
|
margin: 0;
|
|
164
255
|
padding: 0;
|
|
@@ -166,9 +257,6 @@ export function generateEditableHtml(content: string, editorMode: EditorMode = '
|
|
|
166
257
|
height: 100%;
|
|
167
258
|
overflow: hidden;
|
|
168
259
|
background-color: ${CANVAS_BG_COLOR};
|
|
169
|
-
/* iframe内のエディタUI(パンくず・ラベル等)用。
|
|
170
|
-
スライド本体は #artboard 内のルートが font-sans(游ゴシック)を持つので影響しない */
|
|
171
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Hiragino Sans', 'Noto Sans JP', Meiryo, sans-serif;
|
|
172
260
|
}
|
|
173
261
|
|
|
174
262
|
/* スクロール可能なキャンバスコンテナ */
|
|
@@ -232,10 +320,10 @@ export function generateEditableHtml(content: string, editorMode: EditorMode = '
|
|
|
232
320
|
body.panning #artboard,
|
|
233
321
|
body.panning [data-editable="true"] {
|
|
234
322
|
pointer-events: none !important;
|
|
235
|
-
}
|
|
323
|
+
}${embeddedCss}
|
|
236
324
|
</style>
|
|
237
325
|
</head>
|
|
238
|
-
<body tabindex="-1" data-editor-mode="${editorMode}">
|
|
326
|
+
<body tabindex="-1" data-editor-mode="${editorMode}"${embedded ? ' data-embedded="1"' : ''}>
|
|
239
327
|
<div id="canvas-container">
|
|
240
328
|
<div id="canvas-scroll-area">
|
|
241
329
|
<div id="artboard-wrapper">
|
package/src/index.ts
CHANGED
|
@@ -22,11 +22,16 @@ export type { FrontendVisualEditorProps } from "./editor/FrontendVisualEditor";
|
|
|
22
22
|
|
|
23
23
|
export { EditorProvider, useEditorContext } from "./editor/EditorContext";
|
|
24
24
|
export type { ContentListItem, EditorMode } from "./editor/EditorContext";
|
|
25
|
+
export type { DocumentAttributes } from "./editor/contexts/EditorArtboardContext";
|
|
26
|
+
export { computeContentDepths } from "./editor/contexts/EditorArtboardContext";
|
|
25
27
|
|
|
26
28
|
export { WEBPAGE_WIDTH, WEBPAGE_MIN_HEIGHT, SLIDE_WIDTH, SLIDE_HEIGHT, BREAKPOINT_PRESETS } from "./editor/constants";
|
|
27
29
|
|
|
28
30
|
export { registerAutoSaveFlush, flushAutoSave } from "./editor/autosave";
|
|
29
31
|
|
|
32
|
+
/** パッケージの版。利用側が dev サーバーの束ねたコードと node_modules の食い違い(再起動漏れ)を検出するのに使う */
|
|
33
|
+
export { EDITOR_VERSION } from "./version";
|
|
34
|
+
|
|
30
35
|
/**
|
|
31
36
|
* コメントの一覧(カンバン)。エディタとは別の画面に単独で置ける。
|
|
32
37
|
*
|
package/src/io.ts
CHANGED
|
@@ -143,6 +143,23 @@ export type EditorIO = {
|
|
|
143
143
|
/** 保存結果の通知。無ければ何もしない */
|
|
144
144
|
notifySave?: (info: { page: number }) => void;
|
|
145
145
|
|
|
146
|
+
/**
|
|
147
|
+
* 1件の本文(HTML)を取る。マルチフレームのキャンバス(enableMultiPageCanvas)で、
|
|
148
|
+
* 隣のページへ移るとき・見えてきたページを描くときに呼ばれる。
|
|
149
|
+
* id は contentList の id。無ければ contentList[].thumbnailHtml を使い、
|
|
150
|
+
* それも無ければ(parentId がある利用側では)API から取る
|
|
151
|
+
*/
|
|
152
|
+
loadContent?: (id: string) => Promise<string>;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 見るだけの紙面(マルチフレームのキャンバスで、編集していないページ)に足すスタイル。
|
|
156
|
+
* 紙面は script を動かさない(sandbox)ので、本文が Tailwind のブラウザ版 JIT に
|
|
157
|
+
* 頼っている利用側は、コンパイル済み CSS の URL か CSS 文字列をここで渡す
|
|
158
|
+
* (提案書のテンプレートは自分のページの stylesheet を渡す)。
|
|
159
|
+
* 本文に CSS が入っている利用側(構成ラフ)は不要
|
|
160
|
+
*/
|
|
161
|
+
previewStyles?: () => PreviewStyle[] | Promise<PreviewStyle[]>;
|
|
162
|
+
|
|
146
163
|
/**
|
|
147
164
|
* 部品(HTML の template)の一覧。渡すと部品パネルはこれを使い、
|
|
148
165
|
* ブラウザ内(localStorage)の JSON コンポーネントは読まない。
|
|
@@ -186,3 +203,6 @@ export function notProvided(what: string): Error {
|
|
|
186
203
|
}
|
|
187
204
|
|
|
188
205
|
export const EMPTY_DECK: EditorDeck = { version: 0, title: "", slides: [] };
|
|
206
|
+
|
|
207
|
+
/** 見るだけの紙面(マルチフレームのキャンバス)に足すスタイル(URL か CSS 文字列)。io.previewStyles が返す */
|
|
208
|
+
export type PreviewStyle = { href: string } | { css: string };
|
package/src/styles/skin.css
CHANGED
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
`@layer base` を自分の指令と誤認して「@tailwind base が無い」とビルドを止める。
|
|
127
127
|
独自名の層なら素通しされ、層なしの utilities が上書きする関係も保てる。 */
|
|
128
128
|
@layer gg-editor-defaults {
|
|
129
|
-
.gg-editor-ui { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Hiragino Sans", Meiryo, sans-serif; font-size:
|
|
129
|
+
.gg-editor-ui { font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Hiragino Sans", Meiryo, sans-serif; font-size: 12px; line-height: 1.5; text-align: left; }
|
|
130
130
|
.gg-editor-ui, .gg-editor-ui :where(*, *::before, *::after) { box-sizing: border-box; border-width: 0; border-style: solid; }
|
|
131
131
|
.gg-editor-ui :where(button, input, textarea, select) { font: inherit; color: inherit; margin: 0; }
|
|
132
132
|
.gg-editor-ui :where(button) { background: transparent; cursor: pointer; }
|
|
@@ -137,25 +137,29 @@
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
.gg-editor-ui {
|
|
140
|
+
/* Figma(UI3 ライト)の実測値に寄せる:
|
|
141
|
+
カンバス #F5F5F5 / パネル #FFFFFF / 罫線 #E6E6E6 / 文字 #1E1E1E / 補助 #6B6B6B / アクセント #0D99FF */
|
|
140
142
|
color-scheme: light;
|
|
141
|
-
--ed-bg: #
|
|
143
|
+
--ed-bg: #f5f5f5;
|
|
144
|
+
--ed-canvas: #f5f5f5;
|
|
142
145
|
--ed-panel: #ffffff;
|
|
143
146
|
--ed-panel-raised: #ffffff;
|
|
144
|
-
--ed-surface: #
|
|
145
|
-
--ed-surface-hover: #
|
|
146
|
-
--ed-text: #
|
|
147
|
-
--ed-muted: #
|
|
148
|
-
--ed-line: #
|
|
149
|
-
--ed-line-strong: #
|
|
150
|
-
--ed-accent: #
|
|
151
|
-
--ed-accent-strong: #
|
|
152
|
-
--ed-accent-hover: #
|
|
153
|
-
--ed-accent-soft: #
|
|
154
|
-
--ed-success: #
|
|
155
|
-
--ed-warning: #
|
|
156
|
-
--ed-danger: #
|
|
157
|
-
--ed-
|
|
158
|
-
--ed-
|
|
147
|
+
--ed-surface: #f5f5f5;
|
|
148
|
+
--ed-surface-hover: #e6e6e6;
|
|
149
|
+
--ed-text: #1e1e1e;
|
|
150
|
+
--ed-muted: #6b6b6b;
|
|
151
|
+
--ed-line: #e6e6e6;
|
|
152
|
+
--ed-line-strong: #b3b3b3;
|
|
153
|
+
--ed-accent: #0d99ff;
|
|
154
|
+
--ed-accent-strong: #0d99ff;
|
|
155
|
+
--ed-accent-hover: #0c8ce9;
|
|
156
|
+
--ed-accent-soft: #e5f4ff;
|
|
157
|
+
--ed-success: #14ae5c;
|
|
158
|
+
--ed-warning: #8f5a00;
|
|
159
|
+
--ed-danger: #dc3412;
|
|
160
|
+
--ed-frame-edge: rgba(0, 0, 0, 0.08);
|
|
161
|
+
--ed-shadow: 0 5px 17px rgba(0, 0, 0, 0.2), 0 2px 7px rgba(0, 0, 0, 0.15);
|
|
162
|
+
--ed-radius: 6px;
|
|
159
163
|
--ed-focus: 0 0 0 2px var(--ed-panel), 0 0 0 4px var(--ed-accent);
|
|
160
164
|
--color-background: var(--ed-panel);
|
|
161
165
|
--color-foreground: var(--ed-text);
|
|
@@ -180,24 +184,27 @@
|
|
|
180
184
|
scrollbar-color: var(--ed-line-strong) transparent;
|
|
181
185
|
}
|
|
182
186
|
.gg-editor-ui[data-editor-theme="dark"] {
|
|
187
|
+
/* Figma(UI3 ダーク): カンバス #1E1E1E / パネル #2C2C2C / 入力 #383838 / 罫線 white 10% / アクセント #0D99FF */
|
|
183
188
|
color-scheme: dark;
|
|
184
|
-
--ed-bg: #
|
|
185
|
-
--ed-
|
|
186
|
-
--ed-panel
|
|
187
|
-
--ed-
|
|
188
|
-
--ed-surface
|
|
189
|
-
--ed-
|
|
190
|
-
--ed-
|
|
191
|
-
--ed-
|
|
192
|
-
--ed-line
|
|
193
|
-
--ed-
|
|
194
|
-
--ed-accent
|
|
195
|
-
--ed-accent-
|
|
196
|
-
--ed-accent-
|
|
197
|
-
--ed-
|
|
198
|
-
--ed-
|
|
199
|
-
--ed-
|
|
200
|
-
--ed-
|
|
189
|
+
--ed-bg: #1e1e1e;
|
|
190
|
+
--ed-canvas: #1e1e1e;
|
|
191
|
+
--ed-panel: #2c2c2c;
|
|
192
|
+
--ed-panel-raised: #333333;
|
|
193
|
+
--ed-surface: #383838;
|
|
194
|
+
--ed-surface-hover: #444444;
|
|
195
|
+
--ed-text: #ffffff;
|
|
196
|
+
--ed-muted: #b3b3b3;
|
|
197
|
+
--ed-line: #444444;
|
|
198
|
+
--ed-line-strong: #5c5c5c;
|
|
199
|
+
--ed-accent: #4fb8ff;
|
|
200
|
+
--ed-accent-strong: #0d99ff;
|
|
201
|
+
--ed-accent-hover: #0c8ce9;
|
|
202
|
+
--ed-accent-soft: #244761;
|
|
203
|
+
--ed-success: #85e0a3;
|
|
204
|
+
--ed-warning: #ffcd29;
|
|
205
|
+
--ed-danger: #fca397;
|
|
206
|
+
--ed-frame-edge: rgba(255, 255, 255, 0.12);
|
|
207
|
+
--ed-shadow: 0 5px 17px rgba(0, 0, 0, 0.45), 0 2px 7px rgba(0, 0, 0, 0.3);
|
|
201
208
|
}
|
|
202
209
|
/* 古い固定色の部品も同じトークンへ移行。塗り見本・紙面のコンテンツは対象外。 */
|
|
203
210
|
.gg-editor-ui :is(.text-white, .text-gray-100, .text-gray-200, .text-gray-300):not([data-editor-preview] *) { color: var(--ed-text); }
|
|
@@ -251,9 +258,46 @@
|
|
|
251
258
|
.gg-editor-ui .ed-resize-handle:hover, .gg-editor-ui .ed-resize-handle:focus-visible { background-color: var(--ed-accent); }
|
|
252
259
|
.gg-editor-ui .ed-layer-selected { box-shadow: inset 3px 0 var(--ed-accent); }
|
|
253
260
|
.gg-editor-ui .ed-layer-selected :is(.text-white, .text-blue-100, .text-blue-200) { color: var(--ed-accent); }
|
|
254
|
-
|
|
255
|
-
|
|
261
|
+
/* ── 右のプロパティパネル(Figma UI3 の寸法に寄せる) ──────────────
|
|
262
|
+
節は 1px の罫線で区切り、見出しは 13px/600・高さ 40px、中身は左右 16px。
|
|
263
|
+
入力は高さ 32px・角丸 6px・面の色(枠線は無し。ホバーで枠、フォーカスでアクセント)。
|
|
264
|
+
ラベルは 11px の補助色。数値の小さな入力(十字のパディング等)は 28px */
|
|
256
265
|
.gg-editor-ui [data-property-panel] .ed-panel-heading { position: sticky; top: 0; z-index: 1; }
|
|
266
|
+
.gg-editor-ui [data-property-panel] .p-3 { padding: 0; }
|
|
267
|
+
.gg-editor-ui [data-property-panel] .p-3 > * { padding-left: 16px; padding-right: 16px; }
|
|
268
|
+
.gg-editor-ui [data-property-panel] .p-3 > .mb-3 { min-height: 44px; margin-bottom: 0; }
|
|
269
|
+
.gg-editor-ui [data-property-panel] .p-3 > [data-state] { border-top: 1px solid var(--ed-line); }
|
|
270
|
+
.gg-editor-ui [data-property-panel] .p-3 > .ed-part-card { margin: 0 16px 12px; padding: 10px 12px; }
|
|
271
|
+
.gg-editor-ui [data-property-panel] [data-state] > button[data-state] {
|
|
272
|
+
min-height: 40px; padding: 0; border-top: 0; font-size: 13px; font-weight: 600; color: var(--ed-text); border-radius: 0;
|
|
273
|
+
}
|
|
274
|
+
.gg-editor-ui [data-property-panel] [data-state] > button[data-state]:hover { background: transparent; color: var(--ed-text); }
|
|
275
|
+
.gg-editor-ui [data-property-panel] [data-state] > button[data-state] > span:first-child > svg { width: 14px; height: 14px; color: var(--ed-muted); }
|
|
276
|
+
.gg-editor-ui [data-property-panel] [data-state] > button[data-state] > svg:last-child { width: 14px; height: 14px; color: var(--ed-muted); }
|
|
277
|
+
.gg-editor-ui [data-property-panel] [data-state] > div[data-state="open"] { padding-bottom: 16px; }
|
|
278
|
+
.gg-editor-ui [data-property-panel] label, .gg-editor-ui [data-property-panel] .ed-prop-label { font-size: 11px; line-height: 1.4; color: var(--ed-muted); font-weight: 500; }
|
|
279
|
+
.gg-editor-ui [data-property-panel] :is(input:not([type="color"]):not([type="checkbox"]):not([type="range"]), select, [role="combobox"]):not(.group *) {
|
|
280
|
+
min-height: 32px; height: 32px; border-radius: 6px; background: var(--ed-surface); border: 1px solid transparent !important;
|
|
281
|
+
color: var(--ed-text); font-size: 12px; padding-left: 8px; padding-right: 8px; box-shadow: none;
|
|
282
|
+
}
|
|
283
|
+
.gg-editor-ui [data-property-panel] :is(input:not([type="color"]):not([type="checkbox"]):not([type="range"]), select, [role="combobox"]):not(.group *):hover { border-color: var(--ed-line-strong) !important; }
|
|
284
|
+
.gg-editor-ui [data-property-panel] :is(input:not([type="color"]):not([type="checkbox"]):not([type="range"]), select, [role="combobox"]):not(.group *):focus-visible,
|
|
285
|
+
.gg-editor-ui [data-property-panel] :is(input:not([type="color"]):not([type="checkbox"]):not([type="range"]), select, [role="combobox"]):not(.group *):focus-within { border-color: var(--ed-accent) !important; outline: none; box-shadow: none; }
|
|
286
|
+
/* 数値のスクラブ入力(CompactNumberInput。器 .group が枠) */
|
|
287
|
+
.gg-editor-ui [data-property-panel] .group.h-5 { height: 28px; min-width: 40px; border-radius: 6px; background: var(--ed-surface); border-color: transparent; }
|
|
288
|
+
.gg-editor-ui [data-property-panel] .group.h-5:hover { border-color: var(--ed-line-strong); }
|
|
289
|
+
.gg-editor-ui [data-property-panel] .group.h-5:focus-within { border-color: var(--ed-accent); }
|
|
290
|
+
.gg-editor-ui [data-property-panel] .group.h-5 input { font-size: 12px; color: var(--ed-text); }
|
|
291
|
+
.gg-editor-ui [data-property-panel] .group.h-5 input:focus { box-shadow: none; }
|
|
292
|
+
/* 配置・書式のトグル(h-7 w-7)は 32px の角丸 6px。押下は面の色で示す */
|
|
293
|
+
.gg-editor-ui [data-property-panel] button.h-7.w-7, .gg-editor-ui [data-property-panel] button.h-7 { height: 32px; min-width: 32px; border-radius: 6px; }
|
|
294
|
+
.gg-editor-ui [data-property-panel] button.h-7.w-7 svg { width: 14px; height: 14px; }
|
|
295
|
+
.gg-editor-ui [data-property-panel] button[data-state]:not([role="checkbox"]):not([role="switch"]) { min-height: 32px; }
|
|
296
|
+
.gg-editor-ui [data-property-panel] button[role="checkbox"] { width: 16px; height: 16px; min-height: 16px; border-radius: 4px; border: 1px solid var(--ed-line-strong); background: var(--ed-surface); }
|
|
297
|
+
.gg-editor-ui [data-property-panel] button[role="checkbox"][data-state="checked"] { background: var(--ed-accent-strong); border-color: var(--ed-accent-strong); color: #fff; }
|
|
298
|
+
.gg-editor-ui [data-property-panel] input[type="color"] { width: 28px; height: 28px; border-radius: 6px; border: 1px solid var(--ed-line-strong); padding: 0; }
|
|
299
|
+
.gg-editor-ui [data-property-panel] .grid.grid-cols-2 { gap: 8px; }
|
|
300
|
+
.gg-editor-ui [data-property-panel] .space-y-2 > * + *, .gg-editor-ui [data-property-panel] .space-y-3 > * + * { margin-top: 10px; }
|
|
257
301
|
/* ポータル自身も配色スコープ。利用側にToasterがある場合もその設定を奪わない。 */
|
|
258
302
|
.gg-editor-ui[data-editor-surface="popup"], .gg-editor-ui[data-editor-surface="dialog"], .gg-editor-ui[data-editor-surface="tooltip"] {
|
|
259
303
|
color: var(--ed-text); background-color: var(--ed-panel-raised); border-color: var(--ed-line); border-radius: 12px; box-shadow: var(--ed-shadow); z-index: 10000;
|
|
@@ -279,3 +323,45 @@
|
|
|
279
323
|
|
|
280
324
|
/* 入力の識別に必要な境界は、レイアウトの薄い区切り線とは分ける。 */
|
|
281
325
|
.gg-editor-ui :is(input:not([type="color"]), textarea, select):not([data-editor-preview] *) { border-color: var(--ed-line-strong) !important; }
|
|
326
|
+
|
|
327
|
+
/* ── マルチフレームのキャンバス(Figma 風) ──────────────
|
|
328
|
+
フレーム名とリングは画面座標に置く(倍率に関係なく同じ太さ・同じ文字サイズ)。
|
|
329
|
+
見るだけの紙面の骨組み(スケルトン)だけは転写層の中にあるので、フレームの寸法に対する % で描く */
|
|
330
|
+
.gg-editor-ui .ed-frame-label-row { position: absolute; left: 0; height: 20px; display: inline-flex; align-items: center; gap: 2px; max-width: 100%; }
|
|
331
|
+
.gg-editor-ui .ed-frame-label {
|
|
332
|
+
height: 20px; min-width: 0; display: inline-flex; align-items: center; gap: 6px;
|
|
333
|
+
padding: 0 6px 0 4px; border-radius: 4px; font-size: 11px; line-height: 1; white-space: nowrap;
|
|
334
|
+
color: var(--ed-muted); pointer-events: auto; cursor: pointer; overflow: hidden; text-overflow: ellipsis;
|
|
335
|
+
}
|
|
336
|
+
/* 別タブで開く(編集しない表示)。名前の右。ホバー・編集中のときだけ見せる */
|
|
337
|
+
.gg-editor-ui .ed-frame-label-link {
|
|
338
|
+
display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 18px; height: 18px;
|
|
339
|
+
border-radius: 4px; color: var(--ed-muted); pointer-events: auto; opacity: 0; transition: opacity .12s;
|
|
340
|
+
}
|
|
341
|
+
.gg-editor-ui .ed-frame-label-link svg { width: 12px; height: 12px; }
|
|
342
|
+
.gg-editor-ui .ed-frame-label-row:hover .ed-frame-label-link, .gg-editor-ui .ed-frame-label-active + .ed-frame-label-link, .gg-editor-ui .ed-frame-label-link:focus-visible { opacity: 1; }
|
|
343
|
+
.gg-editor-ui .ed-frame-label-link:hover { color: var(--ed-text); background: var(--ed-panel); box-shadow: 0 0 0 1px var(--ed-line); }
|
|
344
|
+
/* 階層の線(ツリー配置)。転写層の中にあるが、線の太さは non-scaling-stroke で画面上 1.5px */
|
|
345
|
+
.gg-editor-ui .ed-frame-connectors { position: absolute; left: 0; top: 0; pointer-events: none; overflow: visible; }
|
|
346
|
+
.gg-editor-ui .ed-frame-connectors path { stroke: var(--ed-line-strong); stroke-width: 1.5px; stroke-linejoin: round; }
|
|
347
|
+
/* 部品(data-part)のカード。紙面の紫の選択枠と同じ色 */
|
|
348
|
+
.gg-editor-ui .ed-part-card { border: 1px solid color-mix(in srgb, #7b61ff 45%, var(--ed-line)); background: color-mix(in srgb, #7b61ff 8%, var(--ed-panel)); color: var(--ed-text); }
|
|
349
|
+
.gg-editor-ui .ed-part-accent { color: #7b61ff; }
|
|
350
|
+
.gg-editor-ui[data-editor-theme="dark"] .ed-part-accent { color: #a99cff; }
|
|
351
|
+
.gg-editor-ui .ed-part-note { color: var(--ed-muted); }
|
|
352
|
+
.gg-editor-ui .ed-frame-label:hover { color: var(--ed-text); background: var(--ed-panel); box-shadow: 0 0 0 1px var(--ed-line); }
|
|
353
|
+
.gg-editor-ui .ed-frame-label-active, .gg-editor-ui .ed-frame-label-active:hover { color: var(--ed-accent); font-weight: 600; }
|
|
354
|
+
.gg-editor-ui .ed-frame-label-index { font-variant-numeric: tabular-nums; opacity: 0.7; }
|
|
355
|
+
.gg-editor-ui .ed-frame-label-title { overflow: hidden; text-overflow: ellipsis; }
|
|
356
|
+
.gg-editor-ui .ed-frame-label-dirty { width: 6px; height: 6px; border-radius: 50%; background: var(--ed-warning); flex-shrink: 0; }
|
|
357
|
+
.gg-editor-ui .ed-frame-label-loading { width: 9px; height: 9px; border: 1.5px solid var(--ed-line-strong); border-top-color: var(--ed-accent); border-radius: 50%; animation: ed-spin 0.8s linear infinite; flex-shrink: 0; }
|
|
358
|
+
@keyframes ed-spin { to { transform: rotate(360deg); } }
|
|
359
|
+
.gg-editor-ui .ed-frame-ring { position: absolute; left: 0; top: 0; pointer-events: none; border-radius: 1px; }
|
|
360
|
+
.gg-editor-ui .ed-canvas-marquee { position: absolute; z-index: 20; pointer-events: none; border: 1px solid var(--ed-accent); background: color-mix(in srgb, var(--ed-accent) 12%, transparent); }
|
|
361
|
+
.gg-editor-ui .ed-frame-ring-active { box-shadow: 0 0 0 1.5px var(--ed-accent-strong); }
|
|
362
|
+
.gg-editor-ui .ed-frame-ring-hover { box-shadow: 0 0 0 1px var(--ed-accent-strong); opacity: 0.55; }
|
|
363
|
+
.gg-editor-ui .ed-frame-skeleton { position: absolute; inset: 0; padding: 6% 8%; display: flex; flex-direction: column; gap: 3%; background: #fff; }
|
|
364
|
+
.gg-editor-ui .ed-frame-skeleton-bar { display: block; height: 2.5%; min-height: 8px; border-radius: 999px; background: #eceff3; }
|
|
365
|
+
.gg-editor-ui .ed-frame-skeleton-note { font-size: 40px; color: #9aa3b2; }
|
|
366
|
+
/* 定規の下に来る部分は透けないように */
|
|
367
|
+
.gg-editor-ui [data-canvas-rulers] canvas { display: block; }
|
package/src/version.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* パッケージの版(package.json の version と同じ値。scripts/check-version.mjs が一致を検査する)。
|
|
3
|
+
*
|
|
4
|
+
* 利用側が「いま動いているエディタの版」を知るために使う。dev サーバー(Vite)は起動時に
|
|
5
|
+
* node_modules を束ねるので、パッケージを更新しても再起動するまで古いコードが動く。
|
|
6
|
+
* 利用側はこの値と node_modules の package.json を見比べて、食い違っていれば
|
|
7
|
+
* 「再起動してください」と出せる(構成ラフのテンプレートが行っている)
|
|
8
|
+
*/
|
|
9
|
+
export const EDITOR_VERSION = '0.3.1';
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { memo, useCallback, useRef } from 'react';
|
|
4
|
-
import { Minus, Plus, Maximize } from 'lucide-react';
|
|
5
|
-
import { Button } from '../../../components/ui/button';
|
|
6
|
-
import { useMultiPageCanvas } from '../../contexts/MultiPageCanvasContext';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* ズームコントロール(右下固定)
|
|
10
|
-
* - ズームイン/アウトボタン
|
|
11
|
-
* - ズーム値表示
|
|
12
|
-
* - フィットボタン (全ページを表示)
|
|
13
|
-
*/
|
|
14
|
-
export const CanvasZoomControls = memo(function CanvasZoomControls() {
|
|
15
|
-
const { viewState, zoomIn, zoomOut, zoomToFit } = useMultiPageCanvas();
|
|
16
|
-
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
17
|
-
|
|
18
|
-
const handleFitToView = useCallback(() => {
|
|
19
|
-
// 親のInfiniteCanvasコンテナサイズを取得
|
|
20
|
-
const canvas = containerRef.current?.closest('[data-infinite-canvas]');
|
|
21
|
-
if (canvas) {
|
|
22
|
-
const rect = canvas.getBoundingClientRect();
|
|
23
|
-
zoomToFit(rect.width, rect.height);
|
|
24
|
-
}
|
|
25
|
-
}, [zoomToFit]);
|
|
26
|
-
|
|
27
|
-
const zoomPercent = Math.round(viewState.canvasZoom * 100);
|
|
28
|
-
|
|
29
|
-
return (
|
|
30
|
-
<div
|
|
31
|
-
ref={containerRef}
|
|
32
|
-
className="absolute bottom-4 right-4 z-10 flex items-center gap-1 bg-[#2a2a2a] border border-[#404040] rounded-lg px-1 py-1 shadow-lg"
|
|
33
|
-
>
|
|
34
|
-
<Button
|
|
35
|
-
variant="ghost"
|
|
36
|
-
size="sm"
|
|
37
|
-
className="h-7 w-7 p-0 text-gray-400 hover:text-white hover:bg-[#404040]"
|
|
38
|
-
onClick={zoomOut}
|
|
39
|
-
title="ズームアウト"
|
|
40
|
-
>
|
|
41
|
-
<Minus className="w-3.5 h-3.5" />
|
|
42
|
-
</Button>
|
|
43
|
-
|
|
44
|
-
<span className="text-xs text-gray-300 min-w-[40px] text-center tabular-nums select-none">
|
|
45
|
-
{zoomPercent}%
|
|
46
|
-
</span>
|
|
47
|
-
|
|
48
|
-
<Button
|
|
49
|
-
variant="ghost"
|
|
50
|
-
size="sm"
|
|
51
|
-
className="h-7 w-7 p-0 text-gray-400 hover:text-white hover:bg-[#404040]"
|
|
52
|
-
onClick={zoomIn}
|
|
53
|
-
title="ズームイン"
|
|
54
|
-
>
|
|
55
|
-
<Plus className="w-3.5 h-3.5" />
|
|
56
|
-
</Button>
|
|
57
|
-
|
|
58
|
-
<div className="w-px h-4 bg-[#404040] mx-0.5" />
|
|
59
|
-
|
|
60
|
-
<Button
|
|
61
|
-
variant="ghost"
|
|
62
|
-
size="sm"
|
|
63
|
-
className="h-7 w-7 p-0 text-gray-400 hover:text-white hover:bg-[#404040]"
|
|
64
|
-
onClick={handleFitToView}
|
|
65
|
-
title="全体にフィット (Ctrl+0)"
|
|
66
|
-
>
|
|
67
|
-
<Maximize className="w-3.5 h-3.5" />
|
|
68
|
-
</Button>
|
|
69
|
-
</div>
|
|
70
|
-
);
|
|
71
|
-
});
|