@growgroup/visual-editor 0.3.0 → 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 +4 -0
- package/dist/editor.css +1 -1
- package/package.json +3 -2
- package/src/editor/EditorContext.tsx +8 -0
- package/src/editor/FrontendVisualEditor.tsx +14 -1
- package/src/editor/components/EditorCanvas.tsx +14 -0
- package/src/editor/components/EditorContextMenu.tsx +12 -3
- package/src/editor/components/EditorLayerPanel.tsx +28 -2
- package/src/editor/components/EditorPropertyPanel.tsx +54 -25
- package/src/editor/components/multi-page/MultiPageCanvasView.tsx +66 -16
- package/src/editor/components/multi-page/PageFramePreview.tsx +35 -2
- package/src/editor/constants.ts +28 -0
- package/src/editor/contexts/EditorArtboardContext.tsx +52 -1
- package/src/editor/contexts/EditorComponentsContext.tsx +17 -4
- package/src/editor/contexts/MultiPageCanvasContext.tsx +187 -34
- package/src/editor/utils/dom-utils.ts +16 -0
- package/src/editor/utils/html-utils.ts +19 -6
- package/src/index.ts +5 -0
- package/src/styles/skin.css +57 -3
- package/src/version.ts +9 -0
|
@@ -34,7 +34,7 @@ import React, {
|
|
|
34
34
|
useState,
|
|
35
35
|
useSyncExternalStore,
|
|
36
36
|
} from 'react';
|
|
37
|
-
import { useEditorArtboard } from './EditorArtboardContext';
|
|
37
|
+
import { useEditorArtboard, computeContentDepths, type DocumentAttributes } from './EditorArtboardContext';
|
|
38
38
|
import { useEditorTool } from './EditorToolContext';
|
|
39
39
|
import { useEditorView } from './EditorViewContext';
|
|
40
40
|
import { SLIDE_HEIGHT, SLIDE_WIDTH, WEBPAGE_WIDTH } from '../constants';
|
|
@@ -82,7 +82,7 @@ const WRAP_AT: Record<EditorMode, (count: number) => number> = {
|
|
|
82
82
|
export interface PageFrame {
|
|
83
83
|
id: string;
|
|
84
84
|
title: string;
|
|
85
|
-
/** 本文。null = まだ読んでいない */
|
|
85
|
+
/** 本文。null = まだ読んでいない(または読み直す) */
|
|
86
86
|
html: string | null;
|
|
87
87
|
/** 本文を読んでいる途中 */
|
|
88
88
|
loading: boolean;
|
|
@@ -93,6 +93,19 @@ export interface PageFrame {
|
|
|
93
93
|
measured: boolean;
|
|
94
94
|
/** 未保存の変更がある(生きているページだけ立つ) */
|
|
95
95
|
isDirty: boolean;
|
|
96
|
+
/** 親ページ(contentList[].parentId。一覧に無い親は null 扱い) */
|
|
97
|
+
parentId: string | null;
|
|
98
|
+
/** 階層の深さ(ルート = 0) */
|
|
99
|
+
depth: number;
|
|
100
|
+
/** 編集しない表示の URL(contentList[].href) */
|
|
101
|
+
href?: string;
|
|
102
|
+
/** 利用側が進める本文の版(contentList[].revision)。変わったら読み直す */
|
|
103
|
+
revision: number;
|
|
104
|
+
/**
|
|
105
|
+
* 本文が古い(部品の反映などで外から変わった)。読み直すときは contentList[].thumbnailHtml
|
|
106
|
+
* ではなく io.loadContent から取る(thumbnailHtml は利用側が最初に渡した姿のままなので)
|
|
107
|
+
*/
|
|
108
|
+
stale: boolean;
|
|
96
109
|
}
|
|
97
110
|
|
|
98
111
|
/** レイアウト済み(位置付き)のフレーム */
|
|
@@ -101,6 +114,9 @@ export interface PageFrameLayout extends PageFrame {
|
|
|
101
114
|
index: number;
|
|
102
115
|
}
|
|
103
116
|
|
|
117
|
+
/** フレームの並べ方。tree = 階層(parentId)のツリー、grid = 行で折り返す */
|
|
118
|
+
export type CanvasLayoutKind = 'grid' | 'tree';
|
|
119
|
+
|
|
104
120
|
export interface CanvasViewState {
|
|
105
121
|
canvasOffset: { x: number; y: number };
|
|
106
122
|
canvasZoom: number;
|
|
@@ -129,13 +145,19 @@ export interface MultiPageCanvasContextValue {
|
|
|
129
145
|
// ---- ページフレーム
|
|
130
146
|
pages: PageFrameLayout[];
|
|
131
147
|
bounds: CanvasBounds;
|
|
148
|
+
/** 並べ方(contentList に階層があれば tree) */
|
|
149
|
+
layout: CanvasLayoutKind;
|
|
132
150
|
getPage: (id: string) => PageFrameLayout | undefined;
|
|
133
|
-
updatePageFrame: (
|
|
134
|
-
id: string,
|
|
135
|
-
updates: Partial<Pick<PageFrame, 'title' | 'html' | 'size' | 'isDirty' | 'measured' | 'loading' | 'error'>>,
|
|
136
|
-
) => void;
|
|
151
|
+
updatePageFrame: (id: string, updates: Partial<Omit<PageFrame, 'id'>>) => void;
|
|
137
152
|
/** 本文を差し替える(保存後・読み込み後) */
|
|
138
153
|
setPageHtml: (id: string, html: string) => void;
|
|
154
|
+
/**
|
|
155
|
+
* 見るだけの紙面の本文を捨てて読み直させる(部品の定義を更新して他ページが変わったとき等)。
|
|
156
|
+
* except のページ(編集中のページ)は触らない。読み直す手段(io.loadContent)が無ければ何もしない
|
|
157
|
+
*/
|
|
158
|
+
invalidatePages: (options?: { except?: readonly (string | null | undefined)[] }) => void;
|
|
159
|
+
/** 文書(`<html>`)に付ける属性(利用側の documentAttributes)。紙面にも読み直さずに反映する */
|
|
160
|
+
documentAttributes: DocumentAttributes;
|
|
139
161
|
/**
|
|
140
162
|
* 高さの実測を反映する。source が 'preview' のときは、生きているページには効かない
|
|
141
163
|
* (エディタの実測が正。見るだけの紙面はフォントや画像の読み込み前に測ることがある)
|
|
@@ -241,16 +263,19 @@ export function useCanvasViewStateOptional(): CanvasViewState | null {
|
|
|
241
263
|
// レイアウト
|
|
242
264
|
// ============================================================
|
|
243
265
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
266
|
+
function boundsOf(pages: PageFrameLayout[]): CanvasBounds {
|
|
267
|
+
return pages.length
|
|
268
|
+
? {
|
|
269
|
+
minX: Math.min(...pages.map((p) => p.position.x)),
|
|
270
|
+
minY: Math.min(...pages.map((p) => p.position.y)),
|
|
271
|
+
maxX: Math.max(...pages.map((p) => p.position.x + p.size.width)),
|
|
272
|
+
maxY: Math.max(...pages.map((p) => p.position.y + p.size.height)),
|
|
273
|
+
}
|
|
274
|
+
: { minX: 0, minY: 0, maxX: 0, maxY: 0 };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** 行で折り返す並べ方(階層が無いとき)。行の高さはその行で一番高いフレームに合わせる */
|
|
278
|
+
function layoutGrid(frames: PageFrame[], gap: { x: number; y: number }, perRow: number): PageFrameLayout[] {
|
|
254
279
|
const pages: PageFrameLayout[] = [];
|
|
255
280
|
let x = 0;
|
|
256
281
|
let y = 0;
|
|
@@ -265,15 +290,101 @@ export function layoutFrames(
|
|
|
265
290
|
x += frame.size.width + gap.x;
|
|
266
291
|
rowHeight = Math.max(rowHeight, frame.size.height);
|
|
267
292
|
});
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
293
|
+
return pages;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* 階層(parentId)のツリーで並べる(サイトマップの図と同じ姿)。
|
|
298
|
+
* - 深さごとに行を作り、行の高さはその深さで一番高いフレームに合わせる
|
|
299
|
+
* - 親は子の並び(部分木)の中央の上に置く。部分木の幅 = max(自分の幅, 子の部分木の幅の和 + 間隔)
|
|
300
|
+
* - 親が一覧に無い・循環しているものはルートとして左から順に置く
|
|
301
|
+
* 一覧の順(index)は変えない(フレーム名の番号・読み込みの順番待ちに使う)
|
|
302
|
+
*/
|
|
303
|
+
function layoutTree(frames: PageFrame[], gap: { x: number; y: number }): PageFrameLayout[] {
|
|
304
|
+
const byId = new Map(frames.map((f) => [f.id, f] as const));
|
|
305
|
+
const children = new Map<string, PageFrame[]>();
|
|
306
|
+
const roots: PageFrame[] = [];
|
|
307
|
+
// 循環の検出: 親をたどってルートに着くか(着かないものはルート扱い)
|
|
308
|
+
const reachesRoot = (f: PageFrame): boolean => {
|
|
309
|
+
const seen = new Set<string>([f.id]);
|
|
310
|
+
let cur: PageFrame | undefined = f;
|
|
311
|
+
while (cur?.parentId && byId.has(cur.parentId)) {
|
|
312
|
+
if (seen.has(cur.parentId)) return false;
|
|
313
|
+
seen.add(cur.parentId);
|
|
314
|
+
cur = byId.get(cur.parentId);
|
|
315
|
+
}
|
|
316
|
+
return true;
|
|
317
|
+
};
|
|
318
|
+
frames.forEach((f) => {
|
|
319
|
+
const parent = f.parentId && f.parentId !== f.id && byId.has(f.parentId) && reachesRoot(f) ? f.parentId : null;
|
|
320
|
+
if (parent) {
|
|
321
|
+
const list = children.get(parent) ?? [];
|
|
322
|
+
list.push(f);
|
|
323
|
+
children.set(parent, list);
|
|
324
|
+
} else {
|
|
325
|
+
roots.push(f);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
const subtreeWidth = new Map<string, number>();
|
|
329
|
+
const widthOf = (f: PageFrame): number => {
|
|
330
|
+
const cached = subtreeWidth.get(f.id);
|
|
331
|
+
if (cached != null) return cached;
|
|
332
|
+
const kids = children.get(f.id) ?? [];
|
|
333
|
+
const kidsWidth = kids.reduce((sum, k, i) => sum + widthOf(k) + (i > 0 ? gap.x : 0), 0);
|
|
334
|
+
const w = Math.max(f.size.width, kidsWidth);
|
|
335
|
+
subtreeWidth.set(f.id, w);
|
|
336
|
+
return w;
|
|
337
|
+
};
|
|
338
|
+
// 深さと行の高さ
|
|
339
|
+
const depthOf = new Map<string, number>();
|
|
340
|
+
const rowHeight: number[] = [];
|
|
341
|
+
const walk = (f: PageFrame, d: number) => {
|
|
342
|
+
depthOf.set(f.id, d);
|
|
343
|
+
rowHeight[d] = Math.max(rowHeight[d] ?? 0, f.size.height);
|
|
344
|
+
(children.get(f.id) ?? []).forEach((k) => walk(k, d + 1));
|
|
345
|
+
};
|
|
346
|
+
roots.forEach((r) => walk(r, 0));
|
|
347
|
+
const rowY: number[] = [];
|
|
348
|
+
rowHeight.forEach((h, d) => {
|
|
349
|
+
rowY[d] = d === 0 ? 0 : rowY[d - 1] + rowHeight[d - 1] + gap.y;
|
|
350
|
+
});
|
|
351
|
+
const position = new Map<string, { x: number; y: number }>();
|
|
352
|
+
const place = (f: PageFrame, x0: number) => {
|
|
353
|
+
const d = depthOf.get(f.id) ?? 0;
|
|
354
|
+
const w = widthOf(f);
|
|
355
|
+
position.set(f.id, { x: x0 + (w - f.size.width) / 2, y: rowY[d] ?? 0 });
|
|
356
|
+
const kids = children.get(f.id) ?? [];
|
|
357
|
+
const kidsWidth = kids.reduce((sum, k, i) => sum + widthOf(k) + (i > 0 ? gap.x : 0), 0);
|
|
358
|
+
let cx = x0 + (w - kidsWidth) / 2;
|
|
359
|
+
kids.forEach((k) => {
|
|
360
|
+
place(k, cx);
|
|
361
|
+
cx += widthOf(k) + gap.x;
|
|
362
|
+
});
|
|
363
|
+
};
|
|
364
|
+
let x = 0;
|
|
365
|
+
roots.forEach((r) => {
|
|
366
|
+
place(r, x);
|
|
367
|
+
x += widthOf(r) + gap.x;
|
|
368
|
+
});
|
|
369
|
+
return frames.map((f, index) => {
|
|
370
|
+
const p = position.get(f.id) ?? { x: 0, y: 0 };
|
|
371
|
+
return { ...f, index, depth: depthOf.get(f.id) ?? 0, position: { x: Math.round(p.x), y: Math.round(p.y) } };
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* 寸法から位置を決める。contentList に階層(parentId)があればツリー、無ければ行で折り返す。
|
|
377
|
+
* 位置を状態に持たないので、高さが実測で変わっても流れ直すだけで済む
|
|
378
|
+
*/
|
|
379
|
+
export function layoutFrames(
|
|
380
|
+
frames: PageFrame[],
|
|
381
|
+
editorMode: EditorMode,
|
|
382
|
+
): { pages: PageFrameLayout[]; bounds: CanvasBounds; layout: CanvasLayoutKind } {
|
|
383
|
+
const gap = GAP[editorMode];
|
|
384
|
+
const ids = new Set(frames.map((f) => f.id));
|
|
385
|
+
const hasHierarchy = frames.some((f) => f.parentId && f.parentId !== f.id && ids.has(f.parentId));
|
|
386
|
+
const pages = hasHierarchy ? layoutTree(frames, gap) : layoutGrid(frames, gap, WRAP_AT[editorMode](frames.length));
|
|
387
|
+
return { pages, bounds: boundsOf(pages), layout: hasHierarchy ? 'tree' : 'grid' };
|
|
277
388
|
}
|
|
278
389
|
|
|
279
390
|
function fitView(
|
|
@@ -361,19 +472,36 @@ export function MultiPageCanvasProvider({ children, enabled, storageKey }: Multi
|
|
|
361
472
|
framesMapRef.current = framesMap;
|
|
362
473
|
|
|
363
474
|
// contentList からフレームを同期(既存の実測・本文は保つ)
|
|
475
|
+
const currentContentIdRef = useRef(artboard.currentContentId);
|
|
476
|
+
currentContentIdRef.current = artboard.currentContentId;
|
|
364
477
|
useEffect(() => {
|
|
365
478
|
if (!enabled) return;
|
|
479
|
+
const ids = new Set(artboard.contentList.map((c) => c.id));
|
|
480
|
+
const depths = computeContentDepths(artboard.contentList);
|
|
366
481
|
setFramesMap((prev) => {
|
|
367
482
|
const next = new Map<string, PageFrame>();
|
|
368
483
|
let changed = prev.size !== artboard.contentList.length;
|
|
369
484
|
artboard.contentList.forEach((content) => {
|
|
370
485
|
const existing = prev.get(content.id);
|
|
486
|
+
const parentId = content.parentId && ids.has(content.parentId) && content.parentId !== content.id ? content.parentId : null;
|
|
487
|
+
const depth = depths.get(content.id) ?? 0;
|
|
488
|
+
const revision = content.revision ?? 0;
|
|
489
|
+
const href = content.href;
|
|
371
490
|
if (existing) {
|
|
372
491
|
const title = content.title || existing.title;
|
|
373
|
-
const html = existing.html ?? content.thumbnailHtml ?? null;
|
|
374
492
|
const width = frameWidth;
|
|
375
|
-
|
|
376
|
-
|
|
493
|
+
// 利用側が版を進めた = 本文が外から変わった。編集中のページは触らず(編集中の本文が正)、
|
|
494
|
+
// 他のページは本文を捨てて読み直させる(紙面は次の本文が来るまで前の姿のまま)
|
|
495
|
+
const bumped = revision !== existing.revision;
|
|
496
|
+
const isLive = content.id === currentContentIdRef.current;
|
|
497
|
+
const html = bumped && !isLive ? null : existing.html ?? content.thumbnailHtml ?? null;
|
|
498
|
+
const stale = bumped && !isLive ? true : existing.stale;
|
|
499
|
+
if (
|
|
500
|
+
title !== existing.title || html !== existing.html || width !== existing.size.width ||
|
|
501
|
+
parentId !== existing.parentId || depth !== existing.depth || href !== existing.href ||
|
|
502
|
+
revision !== existing.revision || stale !== existing.stale
|
|
503
|
+
) changed = true;
|
|
504
|
+
next.set(content.id, { ...existing, title, html, stale, parentId, depth, href, revision, size: { width, height: existing.size.height } });
|
|
377
505
|
} else {
|
|
378
506
|
changed = true;
|
|
379
507
|
next.set(content.id, {
|
|
@@ -385,6 +513,11 @@ export function MultiPageCanvasProvider({ children, enabled, storageKey }: Multi
|
|
|
385
513
|
size: { width: frameWidth, height: defaultHeight },
|
|
386
514
|
measured: editorMode === 'slide',
|
|
387
515
|
isDirty: false,
|
|
516
|
+
parentId,
|
|
517
|
+
depth,
|
|
518
|
+
href,
|
|
519
|
+
revision,
|
|
520
|
+
stale: false,
|
|
388
521
|
});
|
|
389
522
|
}
|
|
390
523
|
});
|
|
@@ -392,7 +525,7 @@ export function MultiPageCanvasProvider({ children, enabled, storageKey }: Multi
|
|
|
392
525
|
});
|
|
393
526
|
}, [enabled, artboard.contentList, frameWidth, defaultHeight, editorMode]);
|
|
394
527
|
|
|
395
|
-
const { pages, bounds } = useMemo(() => {
|
|
528
|
+
const { pages, bounds, layout } = useMemo(() => {
|
|
396
529
|
const order = artboard.contentList.map((c) => c.id);
|
|
397
530
|
const frames = order.map((id) => framesMap.get(id)).filter((f): f is PageFrame => !!f);
|
|
398
531
|
return layoutFrames(frames, editorMode);
|
|
@@ -428,10 +561,26 @@ export function MultiPageCanvasProvider({ children, enabled, storageKey }: Multi
|
|
|
428
561
|
}, []);
|
|
429
562
|
|
|
430
563
|
const setPageHtml = useCallback(
|
|
431
|
-
(id: string, html: string) => updatePageFrame(id, { html, loading: false, error: null }),
|
|
564
|
+
(id: string, html: string) => updatePageFrame(id, { html, loading: false, error: null, stale: false }),
|
|
432
565
|
[updatePageFrame],
|
|
433
566
|
);
|
|
434
567
|
|
|
568
|
+
// 見るだけの紙面の本文を捨てて読み直させる(部品の反映など、外からファイルが変わったとき)
|
|
569
|
+
const invalidatePages = useCallback<MultiPageCanvasContextValue['invalidatePages']>((options) => {
|
|
570
|
+
if (!io().loadContent) return; // 読み直す手段が無い(thumbnailHtml は最初の姿のままなので読み直しにならない)
|
|
571
|
+
const except = new Set((options?.except ?? []).filter((id): id is string => !!id));
|
|
572
|
+
setFramesMap((prev) => {
|
|
573
|
+
let changed = false;
|
|
574
|
+
const next = new Map(prev);
|
|
575
|
+
prev.forEach((frame, id) => {
|
|
576
|
+
if (except.has(id) || frame.html == null) return;
|
|
577
|
+
next.set(id, { ...frame, html: null, loading: false, error: null, stale: true });
|
|
578
|
+
changed = true;
|
|
579
|
+
});
|
|
580
|
+
return changed ? next : prev;
|
|
581
|
+
});
|
|
582
|
+
}, []);
|
|
583
|
+
|
|
435
584
|
// ---- ビュー状態(外部ストア。ズーム・パンで Context の値は変えない)
|
|
436
585
|
const persisted = useMemo(() => readPersistedView(storageKey), [storageKey]);
|
|
437
586
|
const [viewStore] = useState<CanvasViewStore>(() =>
|
|
@@ -669,12 +818,13 @@ export function MultiPageCanvasProvider({ children, enabled, storageKey }: Multi
|
|
|
669
818
|
async (id) => {
|
|
670
819
|
const frame = framesMapRef.current.get(id);
|
|
671
820
|
if (!frame || frame.html != null || loadingRef.current.has(id)) return;
|
|
821
|
+
const loader = io().loadContent;
|
|
822
|
+
// 最初は利用側が渡した本文(thumbnailHtml)で足りる。外から変わった(stale)あとは loadContent で読み直す
|
|
672
823
|
const fromList = artboard.contentList.find((c) => c.id === id)?.thumbnailHtml;
|
|
673
|
-
if (fromList != null) {
|
|
824
|
+
if (fromList != null && !(frame.stale && loader)) {
|
|
674
825
|
setPageHtml(id, fromList);
|
|
675
826
|
return;
|
|
676
827
|
}
|
|
677
|
-
const loader = io().loadContent;
|
|
678
828
|
if (!loader) return;
|
|
679
829
|
loadingRef.current.add(id);
|
|
680
830
|
updatePageFrame(id, { loading: true, error: null });
|
|
@@ -786,9 +936,12 @@ export function MultiPageCanvasProvider({ children, enabled, storageKey }: Multi
|
|
|
786
936
|
editorMode,
|
|
787
937
|
pages,
|
|
788
938
|
bounds,
|
|
939
|
+
layout,
|
|
789
940
|
getPage,
|
|
790
941
|
updatePageFrame,
|
|
791
942
|
setPageHtml,
|
|
943
|
+
invalidatePages,
|
|
944
|
+
documentAttributes: artboard.documentAttributes,
|
|
792
945
|
setPageHeight,
|
|
793
946
|
ensurePageHtml,
|
|
794
947
|
previewStyles,
|
|
@@ -817,8 +970,8 @@ export function MultiPageCanvasProvider({ children, enabled, storageKey }: Multi
|
|
|
817
970
|
releasePreviewSlot,
|
|
818
971
|
}),
|
|
819
972
|
[
|
|
820
|
-
enabled, editorMode, pages, bounds, getPage, updatePageFrame, setPageHtml,
|
|
821
|
-
previewStyles, viewStore, setCanvasOffset, setCanvasZoom, setView, zoomAt, zoomTo, zoomIn, zoomOut, zoomToFit, zoomToPage,
|
|
973
|
+
enabled, editorMode, pages, bounds, layout, getPage, updatePageFrame, setPageHtml, invalidatePages, artboard.documentAttributes,
|
|
974
|
+
setPageHeight, ensurePageHtml, previewStyles, viewStore, setCanvasOffset, setCanvasZoom, setView, zoomAt, zoomTo, zoomIn, zoomOut, zoomToFit, zoomToPage,
|
|
822
975
|
zoomToActual, revealPage, activatePage, activatingPageId, registerContainer, isInteracting, markInteracting,
|
|
823
976
|
persisted, rulersVisible, toggleRulers, requestPreviewSlot, releasePreviewSlot,
|
|
824
977
|
],
|
|
@@ -1301,6 +1301,22 @@ function drawSelectionBox(
|
|
|
1301
1301
|
selectionBox.className =
|
|
1302
1302
|
mode === 'member' ? 'selection-box selection-member' : 'selection-box';
|
|
1303
1303
|
selectionBox.setAttribute('data-for-element', elementId);
|
|
1304
|
+
// 部品(data-part)のインスタンスは枠を紫にして、名前と版を枠の上に出す(部品だと一目で分かる)。
|
|
1305
|
+
// 部品の中のスロットを選んでいるときも、どの部品の中かを出す
|
|
1306
|
+
const partRoot = element.hasAttribute('data-part') ? element : (element.parentElement?.closest('[data-part]') as HTMLElement | null) ?? null;
|
|
1307
|
+
if (partRoot) {
|
|
1308
|
+
const isRoot = partRoot === element;
|
|
1309
|
+
selectionBox.classList.add(isRoot ? 'selection-part' : 'selection-in-part');
|
|
1310
|
+
if (mode !== 'member') {
|
|
1311
|
+
const badge = iframeDoc.createElement('div');
|
|
1312
|
+
badge.className = 'part-badge';
|
|
1313
|
+
const slot = isRoot ? null : (element.closest('[data-slot]') as HTMLElement | null);
|
|
1314
|
+
const slotName = slot && partRoot.contains(slot) && slot !== partRoot ? slot.getAttribute('data-slot') : null;
|
|
1315
|
+
const version = partRoot.getAttribute('data-part-v');
|
|
1316
|
+
badge.textContent = `${isRoot ? '部品' : '部品の中'} ${partRoot.getAttribute('data-part') ?? ''}${version ? ` v${version}` : ''}${slotName ? ` › ${slotName}` : ''}`;
|
|
1317
|
+
selectionBox.appendChild(badge);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1304
1320
|
selectionBox.style.cssText = `
|
|
1305
1321
|
left: ${relativeLeft}px;
|
|
1306
1322
|
top: ${relativeTop}px;
|
|
@@ -185,9 +185,14 @@ export function generatePreviewHtml(
|
|
|
185
185
|
.join('\n ');
|
|
186
186
|
const width = editorMode === 'webpage' ? (artboardWidth ?? WEBPAGE_WIDTH) : SLIDE_WIDTH;
|
|
187
187
|
const heightRule = editorMode === 'webpage' ? '' : `height: ${SLIDE_HEIGHT}px; overflow: hidden;`;
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
|
|
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, '');
|
|
191
196
|
return `<!DOCTYPE html>
|
|
192
197
|
<html lang="ja">
|
|
193
198
|
<head>${stripScripts(buildCommonHead(content))}
|
|
@@ -234,6 +239,17 @@ export function generateEditableHtml(
|
|
|
234
239
|
<head>${buildCommonHead(content)}
|
|
235
240
|
<style>
|
|
236
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
|
+
}
|
|
237
253
|
html, body {
|
|
238
254
|
margin: 0;
|
|
239
255
|
padding: 0;
|
|
@@ -241,9 +257,6 @@ export function generateEditableHtml(
|
|
|
241
257
|
height: 100%;
|
|
242
258
|
overflow: hidden;
|
|
243
259
|
background-color: ${CANVAS_BG_COLOR};
|
|
244
|
-
/* iframe内のエディタUI(パンくず・ラベル等)用。
|
|
245
|
-
スライド本体は #artboard 内のルートが font-sans(游ゴシック)を持つので影響しない */
|
|
246
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Hiragino Sans', 'Noto Sans JP', Meiryo, sans-serif;
|
|
247
260
|
}
|
|
248
261
|
|
|
249
262
|
/* スクロール可能なキャンバスコンテナ */
|
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/styles/skin.css
CHANGED
|
@@ -258,9 +258,46 @@
|
|
|
258
258
|
.gg-editor-ui .ed-resize-handle:hover, .gg-editor-ui .ed-resize-handle:focus-visible { background-color: var(--ed-accent); }
|
|
259
259
|
.gg-editor-ui .ed-layer-selected { box-shadow: inset 3px 0 var(--ed-accent); }
|
|
260
260
|
.gg-editor-ui .ed-layer-selected :is(.text-white, .text-blue-100, .text-blue-200) { color: var(--ed-accent); }
|
|
261
|
-
|
|
262
|
-
|
|
261
|
+
/* ── 右のプロパティパネル(Figma UI3 の寸法に寄せる) ──────────────
|
|
262
|
+
節は 1px の罫線で区切り、見出しは 13px/600・高さ 40px、中身は左右 16px。
|
|
263
|
+
入力は高さ 32px・角丸 6px・面の色(枠線は無し。ホバーで枠、フォーカスでアクセント)。
|
|
264
|
+
ラベルは 11px の補助色。数値の小さな入力(十字のパディング等)は 28px */
|
|
263
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; }
|
|
264
301
|
/* ポータル自身も配色スコープ。利用側にToasterがある場合もその設定を奪わない。 */
|
|
265
302
|
.gg-editor-ui[data-editor-surface="popup"], .gg-editor-ui[data-editor-surface="dialog"], .gg-editor-ui[data-editor-surface="tooltip"] {
|
|
266
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;
|
|
@@ -290,11 +327,28 @@
|
|
|
290
327
|
/* ── マルチフレームのキャンバス(Figma 風) ──────────────
|
|
291
328
|
フレーム名とリングは画面座標に置く(倍率に関係なく同じ太さ・同じ文字サイズ)。
|
|
292
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%; }
|
|
293
331
|
.gg-editor-ui .ed-frame-label {
|
|
294
|
-
|
|
332
|
+
height: 20px; min-width: 0; display: inline-flex; align-items: center; gap: 6px;
|
|
295
333
|
padding: 0 6px 0 4px; border-radius: 4px; font-size: 11px; line-height: 1; white-space: nowrap;
|
|
296
334
|
color: var(--ed-muted); pointer-events: auto; cursor: pointer; overflow: hidden; text-overflow: ellipsis;
|
|
297
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); }
|
|
298
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); }
|
|
299
353
|
.gg-editor-ui .ed-frame-label-active, .gg-editor-ui .ed-frame-label-active:hover { color: var(--ed-accent); font-weight: 600; }
|
|
300
354
|
.gg-editor-ui .ed-frame-label-index { font-variant-numeric: tabular-nums; opacity: 0.7; }
|
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';
|