@growgroup/visual-editor 0.1.5 → 0.2.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 +59 -2
- package/dist/editor.css +1 -1
- package/package.json +7 -3
- package/src/components/ui/alert-dialog.tsx +18 -4
- package/src/components/ui/context-menu.tsx +18 -4
- package/src/components/ui/dialog.tsx +19 -5
- package/src/components/ui/dropdown-menu.tsx +18 -4
- package/src/components/ui/popover.tsx +10 -2
- package/src/components/ui/select.tsx +10 -2
- package/src/components/ui/tooltip.tsx +10 -2
- package/src/editor/EditorToolbar.tsx +10 -20
- package/src/editor/FrontendVisualEditor.tsx +189 -43
- package/src/editor/components/EditorCanvas.tsx +8 -3
- package/src/editor/components/EditorContextMenu.tsx +53 -71
- package/src/editor/components/EditorFooter.tsx +36 -70
- package/src/editor/components/EditorHeader.tsx +37 -20
- package/src/editor/components/EditorLayerPanel.tsx +7 -9
- package/src/editor/components/EditorPropertyPanel.tsx +84 -8
- package/src/editor/components/LayerTreeItem.tsx +28 -5
- package/src/editor/components/component-panel/ComponentPanel.tsx +4 -3
- package/src/editor/components/ppt/PptChrome.tsx +156 -344
- package/src/editor/components/ppt/PptComments.tsx +135 -87
- package/src/editor/components/ppt/PptDesignProposals.tsx +21 -26
- package/src/editor/components/ppt/PptNotes.tsx +15 -15
- package/src/editor/components/shell/CanvasAppearance.tsx +19 -0
- package/src/editor/components/shell/ConfirmDialog.tsx +15 -0
- package/src/editor/components/shell/EditorTopBar.tsx +29 -68
- package/src/editor/components/shell/LeftPanel.tsx +20 -94
- package/src/editor/components/shell/PagesPanel.tsx +4 -2
- package/src/editor/contexts/EditorAppearanceContext.tsx +44 -0
- package/src/editor/contexts/EditorComponentsContext.tsx +278 -16
- package/src/editor/editor-skin.css +2 -122
- package/src/editor/hooks/useCanvasControls.ts +3 -2
- package/src/editor/hooks/useComponentEditMode.ts +95 -1
- package/src/editor/hooks/useDragResize.ts +86 -6
- package/src/editor/hooks/useDrawingMode.ts +4 -2
- package/src/editor/hooks/useElementActions.ts +60 -1
- package/src/editor/hooks/useElementSelection.ts +35 -8
- package/src/editor/hooks/useIframeSetup.ts +10 -0
- package/src/editor/hooks/useKeyboardShortcuts.ts +12 -1
- package/src/editor/hooks/useMediaLibrary.ts +4 -5
- package/src/editor/hooks/useResizablePanel.ts +40 -11
- package/src/editor/parts.ts +321 -0
- package/src/editor/utils/dom-utils.ts +135 -15
- package/src/editor/utils/flex-reorder.ts +268 -43
- package/src/editor/utils/geometry.ts +4 -3
- package/src/editor/utils/html-utils.ts +10 -5
- package/src/hooks/useEditorShortcuts.ts +39 -2
- package/src/index.ts +25 -0
- package/src/io.ts +54 -0
- package/src/styles/skin.css +159 -0
- package/src/types/editor-components.ts +23 -1
|
@@ -43,6 +43,76 @@ import {
|
|
|
43
43
|
deleteComponentInstance,
|
|
44
44
|
getPageComponentInstances,
|
|
45
45
|
} from '../../lib/firebase/editor-components';
|
|
46
|
+
import { io } from '../../io';
|
|
47
|
+
import type { EditorPartDef, EditorPartsLibrary } from '../../io';
|
|
48
|
+
import {
|
|
49
|
+
materializePart,
|
|
50
|
+
partDefFromElement,
|
|
51
|
+
partInfoOf,
|
|
52
|
+
stampAsInstance,
|
|
53
|
+
stripEditorAttrs,
|
|
54
|
+
toPartId,
|
|
55
|
+
} from '../parts';
|
|
56
|
+
|
|
57
|
+
// ============================================================
|
|
58
|
+
// Parts (HTML template) helpers
|
|
59
|
+
// ============================================================
|
|
60
|
+
|
|
61
|
+
/** 分類の無い部品が入る既定のカテゴリ */
|
|
62
|
+
const DEFAULT_PART_CATEGORY = 'parts';
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 部品の定義(HTML)を、既存のパネル・ドラッグ経路が読める MasterComponent に写す。
|
|
66
|
+
* id は部品 id をそのまま使う(ドロップ時の componentId がそのまま部品 id になる)。
|
|
67
|
+
*/
|
|
68
|
+
function masterFromPartDef(part: EditorPartDef, parser: DOMParser, websiteId: string): MasterComponent | null {
|
|
69
|
+
const doc = parser.parseFromString(`<!doctype html><body>${part.html}</body>`, 'text/html');
|
|
70
|
+
const root = doc.body.firstElementChild as HTMLElement | null;
|
|
71
|
+
if (!root) return null;
|
|
72
|
+
const rootElement = htmlElementToComponentElement(root);
|
|
73
|
+
const now = new Date().toISOString();
|
|
74
|
+
const variant: ComponentVariant = { id: 'default', name: 'Default', rootElement, isDefault: true };
|
|
75
|
+
return {
|
|
76
|
+
id: part.id,
|
|
77
|
+
name: part.name || part.id,
|
|
78
|
+
description: part.description,
|
|
79
|
+
categoryId: part.category || DEFAULT_PART_CATEGORY,
|
|
80
|
+
tags: [],
|
|
81
|
+
variants: [variant],
|
|
82
|
+
defaultVariantId: 'default',
|
|
83
|
+
exposedProperties: [],
|
|
84
|
+
createdAt: now,
|
|
85
|
+
updatedAt: now,
|
|
86
|
+
websiteId,
|
|
87
|
+
version: part.version,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 部品の分類をパネルのカテゴリにする。宣言された順 → 宣言に無い分類の順 */
|
|
92
|
+
function categoriesFromParts(
|
|
93
|
+
lib: EditorPartsLibrary,
|
|
94
|
+
masters: Map<string, MasterComponent>,
|
|
95
|
+
): ComponentLibraryCategory[] {
|
|
96
|
+
const ids = new Map<string, string[]>();
|
|
97
|
+
for (const m of masters.values()) {
|
|
98
|
+
if (!ids.has(m.categoryId)) ids.set(m.categoryId, []);
|
|
99
|
+
ids.get(m.categoryId)!.push(m.id);
|
|
100
|
+
}
|
|
101
|
+
const cats: ComponentLibraryCategory[] = (lib.categories ?? []).map((c, i) => ({
|
|
102
|
+
id: c.id,
|
|
103
|
+
name: c.name,
|
|
104
|
+
description: c.description,
|
|
105
|
+
componentIds: ids.get(c.id) ?? [],
|
|
106
|
+
order: i + 1,
|
|
107
|
+
}));
|
|
108
|
+
const known = new Set(cats.map((c) => c.id));
|
|
109
|
+
let order = cats.length;
|
|
110
|
+
for (const [id, componentIds] of ids) {
|
|
111
|
+
if (known.has(id)) continue;
|
|
112
|
+
cats.push({ id, name: id === DEFAULT_PART_CATEGORY ? '部品' : id, componentIds, order: ++order });
|
|
113
|
+
}
|
|
114
|
+
return cats;
|
|
115
|
+
}
|
|
46
116
|
|
|
47
117
|
// ============================================================
|
|
48
118
|
// Context Creation
|
|
@@ -424,6 +494,10 @@ export function EditorComponentsProvider({
|
|
|
424
494
|
const [error, setError] = useState<string | null>(null);
|
|
425
495
|
const [pendingNavigationTarget, setPendingNavigationTarget] = useState<string | null>(null);
|
|
426
496
|
|
|
497
|
+
// 部品(HTML template)モード。io.loadParts が渡されていれば、JSON コンポーネントではなく部品を使う
|
|
498
|
+
const partsMode = typeof io().loadParts === 'function';
|
|
499
|
+
const [partDefs, setPartDefs] = useState<Map<string, EditorPartDef>>(new Map());
|
|
500
|
+
|
|
427
501
|
// ============================================================
|
|
428
502
|
// Navigation
|
|
429
503
|
// ============================================================
|
|
@@ -508,6 +582,30 @@ export function EditorComponentsProvider({
|
|
|
508
582
|
`[EditorComponentsContext] Loading components for website: ${targetWebsiteId}`
|
|
509
583
|
);
|
|
510
584
|
|
|
585
|
+
// 部品モード: 利用側の loadParts から HTML の定義を読む(localStorage は見ない)
|
|
586
|
+
const loadParts = io().loadParts;
|
|
587
|
+
if (loadParts) {
|
|
588
|
+
const lib = await loadParts();
|
|
589
|
+
const parser = new DOMParser();
|
|
590
|
+
const defs = new Map<string, EditorPartDef>();
|
|
591
|
+
const componentsMap = new Map<string, MasterComponent>();
|
|
592
|
+
for (const part of lib.parts) {
|
|
593
|
+
const master = masterFromPartDef(part, parser, targetWebsiteId);
|
|
594
|
+
if (!master) {
|
|
595
|
+
console.warn(`[EditorComponentsContext] 部品 ${part.id} の HTML にルート要素がありません`);
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
defs.set(part.id, part);
|
|
599
|
+
componentsMap.set(master.id, master);
|
|
600
|
+
}
|
|
601
|
+
setPartDefs(defs);
|
|
602
|
+
setMasterComponents(componentsMap);
|
|
603
|
+
setComponentLibrary(categoriesFromParts(lib, componentsMap));
|
|
604
|
+
setComponentInstances(new Map());
|
|
605
|
+
console.log(`[EditorComponentsContext] Loaded ${defs.size} parts`);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
511
609
|
// Load master components from Firestore
|
|
512
610
|
const components = await getAllMasterComponents(targetWebsiteId);
|
|
513
611
|
console.log(`[EditorComponentsContext] Loaded ${components.length} components`);
|
|
@@ -550,22 +648,25 @@ export function EditorComponentsProvider({
|
|
|
550
648
|
// Initialize on mount or when websiteId changes
|
|
551
649
|
const loadedWebsiteIdRef = useRef<string | null>(null);
|
|
552
650
|
useEffect(() => {
|
|
651
|
+
// 部品モードは websiteId が無くても読む(部品の置き場は利用側が知っている)
|
|
652
|
+
const key = websiteId || (partsMode ? '__parts__' : '');
|
|
553
653
|
// Only load if websiteId exists and is different from the last loaded one
|
|
554
|
-
if (
|
|
555
|
-
loadedWebsiteIdRef.current =
|
|
556
|
-
loadComponents(
|
|
557
|
-
} else if (!
|
|
654
|
+
if (key && key !== loadedWebsiteIdRef.current) {
|
|
655
|
+
loadedWebsiteIdRef.current = key;
|
|
656
|
+
loadComponents(key);
|
|
657
|
+
} else if (!key) {
|
|
558
658
|
// Reset to default categories if no websiteId
|
|
559
659
|
setComponentLibrary(defaultCategories);
|
|
560
660
|
setMasterComponents(new Map());
|
|
561
661
|
loadedWebsiteIdRef.current = null;
|
|
562
662
|
}
|
|
563
|
-
}, [websiteId, loadComponents, defaultCategories]);
|
|
663
|
+
}, [websiteId, partsMode, loadComponents, defaultCategories]);
|
|
564
664
|
|
|
565
665
|
// Load instances when pageId changes
|
|
566
666
|
const loadedPageIdRef = useRef<string | null>(null);
|
|
567
667
|
useEffect(() => {
|
|
568
|
-
|
|
668
|
+
// 部品モードではインスタンスを別に持たない(ページの HTML そのものがインスタンス)
|
|
669
|
+
if (partsMode || !websiteId || !pageId) {
|
|
569
670
|
setComponentInstances(new Map());
|
|
570
671
|
loadedPageIdRef.current = null;
|
|
571
672
|
return;
|
|
@@ -597,7 +698,7 @@ export function EditorComponentsProvider({
|
|
|
597
698
|
};
|
|
598
699
|
|
|
599
700
|
loadInstances();
|
|
600
|
-
}, [websiteId, pageId]);
|
|
701
|
+
}, [websiteId, pageId, partsMode]);
|
|
601
702
|
|
|
602
703
|
// ============================================================
|
|
603
704
|
// Master Component Operations
|
|
@@ -650,7 +751,7 @@ export function EditorComponentsProvider({
|
|
|
650
751
|
);
|
|
651
752
|
|
|
652
753
|
// Save to Firestore asynchronously
|
|
653
|
-
if (websiteId) {
|
|
754
|
+
if (websiteId && !partsMode) {
|
|
654
755
|
saveToFirestore(websiteId, masterComponent).catch((err) => {
|
|
655
756
|
console.error('[EditorComponentsContext] Failed to save component to Firestore:', err);
|
|
656
757
|
setError('Failed to save component');
|
|
@@ -662,7 +763,116 @@ export function EditorComponentsProvider({
|
|
|
662
763
|
);
|
|
663
764
|
return masterComponent;
|
|
664
765
|
},
|
|
665
|
-
[websiteId]
|
|
766
|
+
[websiteId, partsMode]
|
|
767
|
+
);
|
|
768
|
+
|
|
769
|
+
// ============================================================
|
|
770
|
+
// Parts (HTML template) Operations
|
|
771
|
+
// ============================================================
|
|
772
|
+
|
|
773
|
+
const getPartDef = useCallback(
|
|
774
|
+
(id: string): EditorPartDef | null => partDefs.get(id) ?? null,
|
|
775
|
+
[partDefs]
|
|
776
|
+
);
|
|
777
|
+
|
|
778
|
+
const materializePartInstance = useCallback(
|
|
779
|
+
(id: string, doc: Document): HTMLElement | null => {
|
|
780
|
+
const def = partDefs.get(id);
|
|
781
|
+
return def ? materializePart(def, doc) : null;
|
|
782
|
+
},
|
|
783
|
+
[partDefs]
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
/** 定義を状態に入れる(新規・更新の両方)。パネルの分類も揃える */
|
|
787
|
+
const upsertPartDef = useCallback((def: EditorPartDef) => {
|
|
788
|
+
const master = masterFromPartDef(def, new DOMParser(), websiteId || '');
|
|
789
|
+
if (!master) return;
|
|
790
|
+
setPartDefs((prev) => {
|
|
791
|
+
const next = new Map(prev);
|
|
792
|
+
next.set(def.id, def);
|
|
793
|
+
return next;
|
|
794
|
+
});
|
|
795
|
+
setMasterComponents((prev) => {
|
|
796
|
+
const next = new Map(prev);
|
|
797
|
+
next.set(master.id, master);
|
|
798
|
+
return next;
|
|
799
|
+
});
|
|
800
|
+
setComponentLibrary((prev) => {
|
|
801
|
+
const categoryId = master.categoryId;
|
|
802
|
+
const cleared = prev.map((cat) => ({
|
|
803
|
+
...cat,
|
|
804
|
+
componentIds: cat.componentIds.filter((cid) => cid !== master.id),
|
|
805
|
+
}));
|
|
806
|
+
const found = cleared.find((cat) => cat.id === categoryId);
|
|
807
|
+
if (found) {
|
|
808
|
+
return cleared.map((cat) =>
|
|
809
|
+
cat.id === categoryId ? { ...cat, componentIds: [...cat.componentIds, master.id] } : cat
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
return [
|
|
813
|
+
...cleared,
|
|
814
|
+
{
|
|
815
|
+
id: categoryId,
|
|
816
|
+
name: categoryId === DEFAULT_PART_CATEGORY ? '部品' : categoryId,
|
|
817
|
+
componentIds: [master.id],
|
|
818
|
+
order: cleared.length + 1,
|
|
819
|
+
},
|
|
820
|
+
];
|
|
821
|
+
});
|
|
822
|
+
}, [websiteId]);
|
|
823
|
+
|
|
824
|
+
const savePartFromElement = useCallback(
|
|
825
|
+
async (
|
|
826
|
+
el: HTMLElement,
|
|
827
|
+
meta: { name: string; category?: string; description?: string }
|
|
828
|
+
): Promise<EditorPartDef> => {
|
|
829
|
+
const save = io().savePart;
|
|
830
|
+
let id = toPartId(meta.name);
|
|
831
|
+
// 同名の部品があれば番号を足す(既存の定義を黙って上書きしない)
|
|
832
|
+
if (partDefs.has(id)) {
|
|
833
|
+
let n = 2;
|
|
834
|
+
while (partDefs.has(`${id}-${n}`)) n++;
|
|
835
|
+
id = `${id}-${n}`;
|
|
836
|
+
}
|
|
837
|
+
const def = partDefFromElement(
|
|
838
|
+
el,
|
|
839
|
+
{
|
|
840
|
+
id,
|
|
841
|
+
name: meta.name,
|
|
842
|
+
category: meta.category || DEFAULT_PART_CATEGORY,
|
|
843
|
+
description: meta.description,
|
|
844
|
+
version: 1,
|
|
845
|
+
},
|
|
846
|
+
{ inferSlots: true },
|
|
847
|
+
);
|
|
848
|
+
const saved = (save ? await save(def) : undefined) ?? def;
|
|
849
|
+
upsertPartDef(saved);
|
|
850
|
+
stampAsInstance(el, saved, el.ownerDocument);
|
|
851
|
+
return saved;
|
|
852
|
+
},
|
|
853
|
+
[partDefs, upsertPartDef]
|
|
854
|
+
);
|
|
855
|
+
|
|
856
|
+
const updatePartFromElement = useCallback(
|
|
857
|
+
async (el: HTMLElement): Promise<EditorPartDef | null> => {
|
|
858
|
+
const info = partInfoOf(el);
|
|
859
|
+
if (!info) return null;
|
|
860
|
+
const current = partDefs.get(info.id);
|
|
861
|
+
if (!current) return null;
|
|
862
|
+
const next = partDefFromElement(el, {
|
|
863
|
+
id: current.id,
|
|
864
|
+
name: current.name,
|
|
865
|
+
category: current.category,
|
|
866
|
+
description: current.description,
|
|
867
|
+
version: current.version + 1,
|
|
868
|
+
});
|
|
869
|
+
const save = io().savePart;
|
|
870
|
+
const saved = (save ? await save(next) : undefined) ?? next;
|
|
871
|
+
upsertPartDef(saved);
|
|
872
|
+
el.setAttribute('data-part-v', String(saved.version));
|
|
873
|
+
return saved;
|
|
874
|
+
},
|
|
875
|
+
[partDefs, upsertPartDef]
|
|
666
876
|
);
|
|
667
877
|
|
|
668
878
|
const updateMasterComponent = useCallback(
|
|
@@ -687,7 +897,7 @@ export function EditorComponentsProvider({
|
|
|
687
897
|
next.set(id, updatedComponent);
|
|
688
898
|
|
|
689
899
|
// Save to Firestore asynchronously
|
|
690
|
-
if (websiteId) {
|
|
900
|
+
if (websiteId && !partsMode) {
|
|
691
901
|
updateMasterComponentInFirestore(websiteId, id, updates).catch((err) => {
|
|
692
902
|
console.error('[EditorComponentsContext] Failed to update component in Firestore:', err);
|
|
693
903
|
setError('Failed to save component changes');
|
|
@@ -696,8 +906,38 @@ export function EditorComponentsProvider({
|
|
|
696
906
|
|
|
697
907
|
return next;
|
|
698
908
|
});
|
|
909
|
+
|
|
910
|
+
// 部品モード: 名前・説明・分類・(エディタで直した)構造を定義に写して利用側へ保存する
|
|
911
|
+
if (partsMode) {
|
|
912
|
+
const current = partDefs.get(id);
|
|
913
|
+
if (current) {
|
|
914
|
+
let html = current.html;
|
|
915
|
+
const rootElement = updates.variants?.[0]?.rootElement;
|
|
916
|
+
if (rootElement) {
|
|
917
|
+
const rendered = componentElementToHtml(rootElement, document);
|
|
918
|
+
stripEditorAttrs(rendered);
|
|
919
|
+
html = rendered.outerHTML;
|
|
920
|
+
}
|
|
921
|
+
const next: EditorPartDef = {
|
|
922
|
+
...current,
|
|
923
|
+
name: updates.name ?? current.name,
|
|
924
|
+
description: updates.description ?? current.description,
|
|
925
|
+
category: updates.categoryId ?? current.category,
|
|
926
|
+
html,
|
|
927
|
+
};
|
|
928
|
+
setPartDefs((prev) => {
|
|
929
|
+
const m = new Map(prev);
|
|
930
|
+
m.set(id, next);
|
|
931
|
+
return m;
|
|
932
|
+
});
|
|
933
|
+
io().savePart?.(next).catch((err) => {
|
|
934
|
+
console.error('[EditorComponentsContext] 部品の保存に失敗:', err);
|
|
935
|
+
setError('部品の保存に失敗しました');
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
}
|
|
699
939
|
},
|
|
700
|
-
[websiteId]
|
|
940
|
+
[websiteId, partsMode, partDefs]
|
|
701
941
|
);
|
|
702
942
|
|
|
703
943
|
const deleteMasterComponent = useCallback((id: string) => {
|
|
@@ -727,14 +967,24 @@ export function EditorComponentsProvider({
|
|
|
727
967
|
});
|
|
728
968
|
|
|
729
969
|
// Delete from Firestore asynchronously
|
|
730
|
-
if (websiteId) {
|
|
970
|
+
if (websiteId && !partsMode) {
|
|
731
971
|
deleteFromFirestore(websiteId, id).catch((err) => {
|
|
732
972
|
console.error('[EditorComponentsContext] Failed to delete component from Firestore:', err);
|
|
733
973
|
});
|
|
734
974
|
}
|
|
975
|
+
if (partsMode) {
|
|
976
|
+
setPartDefs((prev) => {
|
|
977
|
+
const next = new Map(prev);
|
|
978
|
+
next.delete(id);
|
|
979
|
+
return next;
|
|
980
|
+
});
|
|
981
|
+
io().deletePart?.(id).catch((err) => {
|
|
982
|
+
console.error('[EditorComponentsContext] 部品の削除に失敗:', err);
|
|
983
|
+
});
|
|
984
|
+
}
|
|
735
985
|
|
|
736
986
|
console.log(`[EditorComponentsContext] Deleted master component: ${id}`);
|
|
737
|
-
}, [websiteId]);
|
|
987
|
+
}, [websiteId, partsMode]);
|
|
738
988
|
|
|
739
989
|
const getMasterComponent = useCallback(
|
|
740
990
|
(id: string): MasterComponent | null => {
|
|
@@ -798,8 +1048,8 @@ export function EditorComponentsProvider({
|
|
|
798
1048
|
return next;
|
|
799
1049
|
});
|
|
800
1050
|
|
|
801
|
-
// Save to Firestore asynchronously
|
|
802
|
-
if (websiteId && effectivePageId) {
|
|
1051
|
+
// Save to Firestore asynchronously(部品モードではインスタンスを別に持たない)
|
|
1052
|
+
if (websiteId && effectivePageId && !partsMode) {
|
|
803
1053
|
saveComponentInstance(websiteId, effectivePageId, instance).catch((err) => {
|
|
804
1054
|
console.error('[EditorComponentsContext] Failed to save instance to Firestore:', err);
|
|
805
1055
|
setError('Failed to save component instance');
|
|
@@ -811,7 +1061,7 @@ export function EditorComponentsProvider({
|
|
|
811
1061
|
);
|
|
812
1062
|
return instance;
|
|
813
1063
|
},
|
|
814
|
-
[masterComponents, websiteId, pageId]
|
|
1064
|
+
[masterComponents, websiteId, pageId, partsMode]
|
|
815
1065
|
);
|
|
816
1066
|
|
|
817
1067
|
const updateInstance = useCallback(
|
|
@@ -1280,6 +1530,13 @@ export function EditorComponentsProvider({
|
|
|
1280
1530
|
resolveInstance,
|
|
1281
1531
|
navigateToMasterComponent,
|
|
1282
1532
|
clearNavigationRequest,
|
|
1533
|
+
|
|
1534
|
+
// Parts
|
|
1535
|
+
partsMode,
|
|
1536
|
+
getPartDef,
|
|
1537
|
+
materializePartInstance,
|
|
1538
|
+
savePartFromElement,
|
|
1539
|
+
updatePartFromElement,
|
|
1283
1540
|
}),
|
|
1284
1541
|
[
|
|
1285
1542
|
masterComponents,
|
|
@@ -1307,6 +1564,11 @@ export function EditorComponentsProvider({
|
|
|
1307
1564
|
resolveInstance,
|
|
1308
1565
|
navigateToMasterComponent,
|
|
1309
1566
|
clearNavigationRequest,
|
|
1567
|
+
partsMode,
|
|
1568
|
+
getPartDef,
|
|
1569
|
+
materializePartInstance,
|
|
1570
|
+
savePartFromElement,
|
|
1571
|
+
updatePartFromElement,
|
|
1310
1572
|
]
|
|
1311
1573
|
);
|
|
1312
1574
|
|
|
@@ -1,122 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* 移植元のエディタはVS Code風のグレーが47ファイルに直書きされている。
|
|
5
|
-
* 全ファイルを書き換えるとマージが破綻するので、エディタのルートに `.gg-editor-skin` を付け、
|
|
6
|
-
* その配下でだけ元のクラスが指す色を差し替える。
|
|
7
|
-
*
|
|
8
|
-
* 意匠はFigmaデスクトップ(ダーク)の実測値:
|
|
9
|
-
* キャンバス=#1e1e1e / パネル=#2c2c2c / 入力=#383838 / 罫線=white 10% / アクセント=#0d99ff
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
.gg-editor-skin {
|
|
13
|
-
--ed-bg: #1e1e1e; /* キャンバスの地(Figmaのカンバス) */
|
|
14
|
-
--ed-panel: #2c2c2c; /* パネル・ツールバーの面 */
|
|
15
|
-
--ed-panel-raised: #2c2c2c; /* Figmaはパネルとカードが同面 */
|
|
16
|
-
--ed-surface: #383838; /* 入力・ボタンの面 */
|
|
17
|
-
--ed-surface-hover: #444444;
|
|
18
|
-
--ed-line: rgba(255, 255, 255, 0.1);
|
|
19
|
-
--ed-line-strong: rgba(255, 255, 255, 0.16);
|
|
20
|
-
--ed-accent: #0d99ff; /* Figmaブルー */
|
|
21
|
-
--ed-accent-strong: #0d99ff;
|
|
22
|
-
--ed-accent-hover: #0c8ce9;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/* ── 地・面 ───────────────────────────────────────── */
|
|
26
|
-
.gg-editor-skin .bg-\[\#1e1e1e\] {
|
|
27
|
-
background-color: var(--ed-bg);
|
|
28
|
-
}
|
|
29
|
-
.gg-editor-skin .bg-\[\#2c2c2c\],
|
|
30
|
-
.gg-editor-skin .bg-\[\#2a2a2a\] {
|
|
31
|
-
background-color: var(--ed-panel);
|
|
32
|
-
}
|
|
33
|
-
.gg-editor-skin .bg-\[\#383838\] {
|
|
34
|
-
background-color: var(--ed-surface);
|
|
35
|
-
}
|
|
36
|
-
.gg-editor-skin .bg-\[\#444444\],
|
|
37
|
-
.gg-editor-skin .bg-\[\#4a4a4a\],
|
|
38
|
-
.gg-editor-skin .bg-\[\#5d5d5d\] {
|
|
39
|
-
background-color: var(--ed-surface-hover);
|
|
40
|
-
}
|
|
41
|
-
.gg-editor-skin .hover\:bg-\[\#444444\]:hover,
|
|
42
|
-
.gg-editor-skin .hover\:bg-\[\#383838\]:hover {
|
|
43
|
-
background-color: rgba(255, 255, 255, 0.08);
|
|
44
|
-
}
|
|
45
|
-
.gg-editor-skin .hover\:bg-\[\#4a4a4a\]:hover {
|
|
46
|
-
background-color: var(--ed-surface-hover);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/* ── 罫線 ─────────────────────────────────────────── */
|
|
50
|
-
.gg-editor-skin .border-\[\#444444\],
|
|
51
|
-
.gg-editor-skin .border-\[\#383838\],
|
|
52
|
-
.gg-editor-skin .border-\[\#2c2c2c\] {
|
|
53
|
-
border-color: var(--ed-line);
|
|
54
|
-
}
|
|
55
|
-
.gg-editor-skin .border-\[\#4a4a4a\],
|
|
56
|
-
.gg-editor-skin .border-\[\#5d5d5d\] {
|
|
57
|
-
border-color: var(--ed-line-strong);
|
|
58
|
-
}
|
|
59
|
-
.gg-editor-skin .hover\:border-\[\#4a4a4a\]:hover,
|
|
60
|
-
.gg-editor-skin .hover\:border-\[\#5d5d5d\]:hover {
|
|
61
|
-
border-color: rgba(13, 153, 255, 0.6);
|
|
62
|
-
}
|
|
63
|
-
/* 区切り線として使われている塗り */
|
|
64
|
-
.gg-editor-skin .w-px.bg-\[\#444444\],
|
|
65
|
-
.gg-editor-skin .h-px.bg-\[\#444444\] {
|
|
66
|
-
background-color: var(--ed-line);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/* ── アクセント(選択・保存・共有ボタン等) ─────────── */
|
|
70
|
-
.gg-editor-skin .bg-\[\#0d99ff\],
|
|
71
|
-
.gg-editor-skin .bg-blue-500,
|
|
72
|
-
.gg-editor-skin .bg-blue-600 {
|
|
73
|
-
background-color: var(--ed-accent-strong);
|
|
74
|
-
}
|
|
75
|
-
.gg-editor-skin .hover\:bg-\[\#0c8ce9\]:hover,
|
|
76
|
-
.gg-editor-skin .hover\:bg-blue-600:hover,
|
|
77
|
-
.gg-editor-skin .hover\:bg-blue-700:hover {
|
|
78
|
-
background-color: var(--ed-accent-hover);
|
|
79
|
-
}
|
|
80
|
-
.gg-editor-skin .text-\[\#0d99ff\],
|
|
81
|
-
.gg-editor-skin .text-\[\#4fb8ff\],
|
|
82
|
-
.gg-editor-skin .text-blue-400,
|
|
83
|
-
.gg-editor-skin .text-blue-500 {
|
|
84
|
-
color: var(--ed-accent);
|
|
85
|
-
}
|
|
86
|
-
.gg-editor-skin .border-\[\#0d99ff\],
|
|
87
|
-
.gg-editor-skin .border-blue-500 {
|
|
88
|
-
border-color: var(--ed-accent);
|
|
89
|
-
}
|
|
90
|
-
.gg-editor-skin .ring-\[\#0d99ff\],
|
|
91
|
-
.gg-editor-skin .ring-blue-500 {
|
|
92
|
-
--tw-ring-color: var(--ed-accent);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/* ── 文字色(Figmaの白/70%/45%) ────────────────────── */
|
|
96
|
-
.gg-editor-skin .text-gray-300 {
|
|
97
|
-
color: rgba(255, 255, 255, 0.85);
|
|
98
|
-
}
|
|
99
|
-
.gg-editor-skin .text-gray-400 {
|
|
100
|
-
color: rgba(255, 255, 255, 0.7);
|
|
101
|
-
}
|
|
102
|
-
.gg-editor-skin .text-gray-500 {
|
|
103
|
-
color: rgba(255, 255, 255, 0.45);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/* ── 角丸・影(Figmaのメニュー影) ─────────────────── */
|
|
107
|
-
.gg-editor-skin .rounded {
|
|
108
|
-
border-radius: 0.375rem;
|
|
109
|
-
}
|
|
110
|
-
.gg-editor-skin .shadow-lg,
|
|
111
|
-
.gg-editor-skin .shadow-xl,
|
|
112
|
-
.gg-editor-skin .shadow-2xl {
|
|
113
|
-
box-shadow: 0 5px 17px rgba(0, 0, 0, 0.35), 0 2px 7px rgba(0, 0, 0, 0.15);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/* ── フォーカスリング(Figmaブルー) ────────────────── */
|
|
117
|
-
.gg-editor-skin input:focus-visible,
|
|
118
|
-
.gg-editor-skin button:focus-visible,
|
|
119
|
-
.gg-editor-skin select:focus-visible {
|
|
120
|
-
outline: 2px solid var(--ed-accent);
|
|
121
|
-
outline-offset: 1px;
|
|
122
|
-
}
|
|
1
|
+
/* 配色の正本は src/styles/skin.css。dist/editor.css にビルドして同梱する。 */
|
|
2
|
+
@import "../styles/skin.css";
|
|
@@ -527,10 +527,11 @@ export function useCanvasControls() {
|
|
|
527
527
|
if (!container) return;
|
|
528
528
|
container.scrollTo({
|
|
529
529
|
left: (container.scrollWidth - container.clientWidth) / 2,
|
|
530
|
-
|
|
530
|
+
// webpage は上端固定(#canvas-scroll-area が flex-start)。先頭から見せる
|
|
531
|
+
top: editorMode === 'webpage' ? 0 : (container.scrollHeight - container.clientHeight) / 2,
|
|
531
532
|
behavior: 'instant',
|
|
532
533
|
});
|
|
533
|
-
}, [containerRef]);
|
|
534
|
+
}, [containerRef, editorMode]);
|
|
534
535
|
|
|
535
536
|
// ズームフィット計算
|
|
536
537
|
const calculateFitZoom = useCallback(() => {
|