@growgroup/visual-editor 0.1.6 → 0.2.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 +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 +157 -43
- package/src/editor/components/EditorCanvas.tsx +1 -1
- 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/useComponentEditMode.ts +93 -1
- package/src/editor/hooks/useIframeSetup.ts +10 -0
- 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 +5 -4
- 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";
|
|
@@ -4,6 +4,7 @@ import { htmlElementToComponentElement } from '../contexts/EditorComponentsConte
|
|
|
4
4
|
import { renderComponentElement } from '../utils/component-renderer';
|
|
5
5
|
import { propagateMasterChanges } from '../utils/component-sync';
|
|
6
6
|
import { buildDomTree } from '../utils/dom-utils';
|
|
7
|
+
import { applyLockInside, detachPart, partInfoOf, unlockPartDescendants } from '../parts';
|
|
7
8
|
import { toast } from 'sonner';
|
|
8
9
|
import type { MasterComponent, ComponentInstance } from '../../types/editor-components';
|
|
9
10
|
|
|
@@ -37,6 +38,10 @@ interface UseComponentEditModeReturn {
|
|
|
37
38
|
selectedElementInstance: ComponentInstance | null;
|
|
38
39
|
isComponentInstance: boolean;
|
|
39
40
|
hasOverrides: boolean;
|
|
41
|
+
/** 選択要素が部品(data-part)のインスタンスか */
|
|
42
|
+
isPartInstance: boolean;
|
|
43
|
+
/** 「名前 vN」。インスタンスでなければ空 */
|
|
44
|
+
partLabel: string;
|
|
40
45
|
// Handlers
|
|
41
46
|
handleCreateComponent: () => void;
|
|
42
47
|
handleConfirmCreateComponent: () => Promise<void>;
|
|
@@ -49,6 +54,10 @@ interface UseComponentEditModeReturn {
|
|
|
49
54
|
handleDetachInstance: () => void;
|
|
50
55
|
handleResetOverrides: () => Promise<void>;
|
|
51
56
|
handlePushOverridesToMain: () => void;
|
|
57
|
+
/** 部品から切り離す(出自の記録を外して普通の HTML にする) */
|
|
58
|
+
handleDetachPart: () => void;
|
|
59
|
+
/** インスタンスの今の姿で定義を更新する(版 +1) */
|
|
60
|
+
handleUpdatePart: () => Promise<void>;
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
/**
|
|
@@ -86,6 +95,10 @@ export function useComponentEditMode({
|
|
|
86
95
|
deleteMasterComponent,
|
|
87
96
|
pendingNavigationTarget,
|
|
88
97
|
clearNavigationRequest,
|
|
98
|
+
partsMode,
|
|
99
|
+
getPartDef,
|
|
100
|
+
savePartFromElement,
|
|
101
|
+
updatePartFromElement,
|
|
89
102
|
} = useEditorComponents();
|
|
90
103
|
|
|
91
104
|
// コンポーネントパネル状態
|
|
@@ -128,6 +141,20 @@ export function useComponentEditMode({
|
|
|
128
141
|
return selectedElementInstance.overrides.length > 0;
|
|
129
142
|
}, [selectedElementInstance]);
|
|
130
143
|
|
|
144
|
+
// 部品(data-part)のインスタンスか。状態は持たず、その時点の DOM の属性で決める
|
|
145
|
+
const selectedPart = useMemo(() => {
|
|
146
|
+
if (!selectedElement?.id) return null;
|
|
147
|
+
const doc = getIframeDoc();
|
|
148
|
+
const el = doc?.querySelector(`[data-element-id="${selectedElement.id}"]`);
|
|
149
|
+
return partInfoOf(el);
|
|
150
|
+
}, [selectedElement, getIframeDoc]);
|
|
151
|
+
const isPartInstance = !!selectedPart;
|
|
152
|
+
const partLabel = useMemo(() => {
|
|
153
|
+
if (!selectedPart) return '';
|
|
154
|
+
const def = getPartDef(selectedPart.id);
|
|
155
|
+
return `${def?.name || selectedPart.id} v${selectedPart.version}`;
|
|
156
|
+
}, [selectedPart, getPartDef]);
|
|
157
|
+
|
|
131
158
|
// Create component from selected element
|
|
132
159
|
const handleCreateComponent = useCallback(() => {
|
|
133
160
|
if (!selectedElement?.id || !iframeRef.current) return;
|
|
@@ -149,6 +176,29 @@ export function useComponentEditMode({
|
|
|
149
176
|
const element = iframeDoc.querySelector(`[data-element-id="${selectedElement.id}"]`) as HTMLElement;
|
|
150
177
|
if (!element) return;
|
|
151
178
|
|
|
179
|
+
// 部品モード: 選択要素を HTML の定義として利用側へ保存し、その場でインスタンスにする
|
|
180
|
+
if (partsMode) {
|
|
181
|
+
try {
|
|
182
|
+
const def = await savePartFromElement(element, {
|
|
183
|
+
name: newComponentName.trim(),
|
|
184
|
+
category: newComponentCategory || undefined,
|
|
185
|
+
});
|
|
186
|
+
applyLockInside(element);
|
|
187
|
+
notifyIframeChange(true);
|
|
188
|
+
setCreateComponentDialogOpen(false);
|
|
189
|
+
setNewComponentName('');
|
|
190
|
+
setNewComponentCategory('');
|
|
191
|
+
closeContextMenu();
|
|
192
|
+
toast.success(`部品「${def.name ?? def.id}」として保存しました`, {
|
|
193
|
+
description: `parts/${def.id}.html。スロット(data-slot)の外は編集できなくなります(切り離すと戻ります)`,
|
|
194
|
+
});
|
|
195
|
+
} catch (error) {
|
|
196
|
+
console.error('Failed to save part:', error);
|
|
197
|
+
toast.error('部品の保存に失敗しました');
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
152
202
|
try {
|
|
153
203
|
// Debug: Log the original element
|
|
154
204
|
console.log('[handleConfirmCreateComponent] Original element:', {
|
|
@@ -243,7 +293,7 @@ export function useComponentEditMode({
|
|
|
243
293
|
console.error('Failed to create component:', error);
|
|
244
294
|
toast.error('コンポーネントの作成に失敗しました');
|
|
245
295
|
}
|
|
246
|
-
}, [selectedElement?.id, iframeRef, newComponentName, newComponentCategory, createMasterComponent, createInstance, contentId, notifyIframeChange, closeContextMenu, setSelectedElement, setSelectedElementIds]);
|
|
296
|
+
}, [selectedElement?.id, iframeRef, newComponentName, newComponentCategory, createMasterComponent, createInstance, contentId, notifyIframeChange, closeContextMenu, setSelectedElement, setSelectedElementIds, partsMode, savePartFromElement]);
|
|
247
297
|
|
|
248
298
|
// Open master component editor
|
|
249
299
|
const handleEditMasterComponent = useCallback((masterId: string) => {
|
|
@@ -617,6 +667,44 @@ export function useComponentEditMode({
|
|
|
617
667
|
toast.info('この機能は現在開発中です');
|
|
618
668
|
}, [selectedElementInstance, closeContextMenu]);
|
|
619
669
|
|
|
670
|
+
// 部品から切り離す: 出自の記録(data-part)だけ外し、ロックされていた子孫を編集できるようにする
|
|
671
|
+
const handleDetachPart = useCallback(() => {
|
|
672
|
+
if (!selectedElement?.id) return;
|
|
673
|
+
const doc = getIframeDoc();
|
|
674
|
+
const el = doc?.querySelector(`[data-element-id="${selectedElement.id}"]`) as HTMLElement | null;
|
|
675
|
+
if (!doc || !el || !partInfoOf(el)) return;
|
|
676
|
+
detachPart(el);
|
|
677
|
+
unlockPartDescendants(el);
|
|
678
|
+
notifyIframeChange(true);
|
|
679
|
+
// 部品バッジを消すため、選択情報を作り直す
|
|
680
|
+
void import('../utils/style-utils').then(({ extractElementInfo }) => {
|
|
681
|
+
const info = extractElementInfo(el, doc);
|
|
682
|
+
if (info) setSelectedElement(info);
|
|
683
|
+
});
|
|
684
|
+
closeContextMenu();
|
|
685
|
+
toast.success('部品から切り離しました', {
|
|
686
|
+
description: 'この要素は普通の HTML になり、部品の更新の対象から外れます',
|
|
687
|
+
});
|
|
688
|
+
}, [selectedElement?.id, getIframeDoc, notifyIframeChange, setSelectedElement, closeContextMenu]);
|
|
689
|
+
|
|
690
|
+
// インスタンスの今の姿で定義を更新する(版 +1)。他ページへの反映は利用側(io.savePart の実装)が行う
|
|
691
|
+
const handleUpdatePart = useCallback(async () => {
|
|
692
|
+
if (!selectedElement?.id) return;
|
|
693
|
+
const doc = getIframeDoc();
|
|
694
|
+
const el = doc?.querySelector(`[data-element-id="${selectedElement.id}"]`) as HTMLElement | null;
|
|
695
|
+
if (!el) return;
|
|
696
|
+
try {
|
|
697
|
+
const def = await updatePartFromElement(el);
|
|
698
|
+
if (!def) return;
|
|
699
|
+
notifyIframeChange(true);
|
|
700
|
+
closeContextMenu();
|
|
701
|
+
toast.success(`部品「${def.name ?? def.id}」を v${def.version} に更新しました`);
|
|
702
|
+
} catch (error) {
|
|
703
|
+
console.error('Failed to update part:', error);
|
|
704
|
+
toast.error('部品の更新に失敗しました');
|
|
705
|
+
}
|
|
706
|
+
}, [selectedElement?.id, getIframeDoc, updatePartFromElement, notifyIframeChange, closeContextMenu]);
|
|
707
|
+
|
|
620
708
|
return {
|
|
621
709
|
// Panel state
|
|
622
710
|
isComponentPanelOpen,
|
|
@@ -642,6 +730,8 @@ export function useComponentEditMode({
|
|
|
642
730
|
selectedElementInstance,
|
|
643
731
|
isComponentInstance,
|
|
644
732
|
hasOverrides,
|
|
733
|
+
isPartInstance,
|
|
734
|
+
partLabel,
|
|
645
735
|
// Handlers
|
|
646
736
|
handleCreateComponent,
|
|
647
737
|
handleConfirmCreateComponent,
|
|
@@ -654,5 +744,7 @@ export function useComponentEditMode({
|
|
|
654
744
|
handleDetachInstance,
|
|
655
745
|
handleResetOverrides,
|
|
656
746
|
handlePushOverridesToMain,
|
|
747
|
+
handleDetachPart,
|
|
748
|
+
handleUpdatePart,
|
|
657
749
|
};
|
|
658
750
|
}
|