@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.
Files changed (52) hide show
  1. package/README.md +59 -2
  2. package/dist/editor.css +1 -1
  3. package/package.json +7 -3
  4. package/src/components/ui/alert-dialog.tsx +18 -4
  5. package/src/components/ui/context-menu.tsx +18 -4
  6. package/src/components/ui/dialog.tsx +19 -5
  7. package/src/components/ui/dropdown-menu.tsx +18 -4
  8. package/src/components/ui/popover.tsx +10 -2
  9. package/src/components/ui/select.tsx +10 -2
  10. package/src/components/ui/tooltip.tsx +10 -2
  11. package/src/editor/EditorToolbar.tsx +10 -20
  12. package/src/editor/FrontendVisualEditor.tsx +189 -43
  13. package/src/editor/components/EditorCanvas.tsx +8 -3
  14. package/src/editor/components/EditorContextMenu.tsx +53 -71
  15. package/src/editor/components/EditorFooter.tsx +36 -70
  16. package/src/editor/components/EditorHeader.tsx +37 -20
  17. package/src/editor/components/EditorLayerPanel.tsx +7 -9
  18. package/src/editor/components/EditorPropertyPanel.tsx +84 -8
  19. package/src/editor/components/LayerTreeItem.tsx +28 -5
  20. package/src/editor/components/component-panel/ComponentPanel.tsx +4 -3
  21. package/src/editor/components/ppt/PptChrome.tsx +156 -344
  22. package/src/editor/components/ppt/PptComments.tsx +135 -87
  23. package/src/editor/components/ppt/PptDesignProposals.tsx +21 -26
  24. package/src/editor/components/ppt/PptNotes.tsx +15 -15
  25. package/src/editor/components/shell/CanvasAppearance.tsx +19 -0
  26. package/src/editor/components/shell/ConfirmDialog.tsx +15 -0
  27. package/src/editor/components/shell/EditorTopBar.tsx +29 -68
  28. package/src/editor/components/shell/LeftPanel.tsx +20 -94
  29. package/src/editor/components/shell/PagesPanel.tsx +4 -2
  30. package/src/editor/contexts/EditorAppearanceContext.tsx +44 -0
  31. package/src/editor/contexts/EditorComponentsContext.tsx +278 -16
  32. package/src/editor/editor-skin.css +2 -122
  33. package/src/editor/hooks/useCanvasControls.ts +3 -2
  34. package/src/editor/hooks/useComponentEditMode.ts +95 -1
  35. package/src/editor/hooks/useDragResize.ts +86 -6
  36. package/src/editor/hooks/useDrawingMode.ts +4 -2
  37. package/src/editor/hooks/useElementActions.ts +60 -1
  38. package/src/editor/hooks/useElementSelection.ts +35 -8
  39. package/src/editor/hooks/useIframeSetup.ts +10 -0
  40. package/src/editor/hooks/useKeyboardShortcuts.ts +12 -1
  41. package/src/editor/hooks/useMediaLibrary.ts +4 -5
  42. package/src/editor/hooks/useResizablePanel.ts +40 -11
  43. package/src/editor/parts.ts +321 -0
  44. package/src/editor/utils/dom-utils.ts +135 -15
  45. package/src/editor/utils/flex-reorder.ts +268 -43
  46. package/src/editor/utils/geometry.ts +4 -3
  47. package/src/editor/utils/html-utils.ts +10 -5
  48. package/src/hooks/useEditorShortcuts.ts +39 -2
  49. package/src/index.ts +25 -0
  50. package/src/io.ts +54 -0
  51. package/src/styles/skin.css +159 -0
  52. 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 (websiteId && websiteId !== loadedWebsiteIdRef.current) {
555
- loadedWebsiteIdRef.current = websiteId;
556
- loadComponents(websiteId);
557
- } else if (!websiteId) {
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
- if (!websiteId || !pageId) {
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
- * [移植時の追加] エディタの配色スキン。配色の正本はこの1ファイル。
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
- top: (container.scrollHeight - container.clientHeight) / 2,
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(() => {