@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.
Files changed (42) 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 +157 -43
  13. package/src/editor/components/EditorCanvas.tsx +1 -1
  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/useComponentEditMode.ts +93 -1
  34. package/src/editor/hooks/useIframeSetup.ts +10 -0
  35. package/src/editor/hooks/useMediaLibrary.ts +4 -5
  36. package/src/editor/hooks/useResizablePanel.ts +40 -11
  37. package/src/editor/parts.ts +321 -0
  38. package/src/editor/utils/dom-utils.ts +5 -4
  39. package/src/index.ts +25 -0
  40. package/src/io.ts +54 -0
  41. package/src/styles/skin.css +159 -0
  42. package/src/types/editor-components.ts +23 -1
@@ -21,6 +21,8 @@ import {
21
21
  } from "react";
22
22
  import { useEditorComponents } from "../EditorContext";
23
23
  import { InstanceOverrideSection } from "./property-panel/InstanceOverrideSection";
24
+ import { Component as PartIcon } from "lucide-react";
25
+ import { detachPart, partInfoOf, unlockPartDescendants } from "../parts";
24
26
  import { Button } from "../../components/ui/button";
25
27
  import { Input } from "../../components/ui/input";
26
28
  import { Label } from "../../components/ui/label";
@@ -813,6 +815,7 @@ export const EditorPropertyPanel = memo(function EditorPropertyPanel() {
813
815
  getIframeDoc,
814
816
  notifyIframeChange,
815
817
  iframeReady,
818
+ setSelectedElement,
816
819
  } = useEditorContext();
817
820
  const {
818
821
  updateElementStyle,
@@ -833,6 +836,8 @@ export const EditorPropertyPanel = memo(function EditorPropertyPanel() {
833
836
  resetAllOverrides,
834
837
  detachInstance,
835
838
  navigateToMasterComponent,
839
+ getPartDef,
840
+ updatePartFromElement,
836
841
  } = useEditorComponents();
837
842
 
838
843
  // Check if selected element is a component instance
@@ -841,6 +846,39 @@ export const EditorPropertyPanel = memo(function EditorPropertyPanel() {
841
846
  return getInstanceByDomId(selectedElement.id);
842
847
  }, [selectedElement?.id, getInstanceByDomId]);
843
848
 
849
+ // 部品(data-part)のインスタンスか(DOM の属性で判定)
850
+ const selectedPart = useMemo(() => {
851
+ if (!selectedElement?.id) return null;
852
+ const doc = getIframeDoc();
853
+ return partInfoOf(doc?.querySelector(`[data-element-id="${selectedElement.id}"]`));
854
+ }, [selectedElement, getIframeDoc]);
855
+ const selectedPartLabel = useMemo(() => {
856
+ if (!selectedPart) return "";
857
+ const def = getPartDef(selectedPart.id);
858
+ return `${def?.name || selectedPart.id} v${selectedPart.version}`;
859
+ }, [selectedPart, getPartDef]);
860
+ const handleUpdatePartFromPanel = useCallback(async () => {
861
+ if (!selectedElement?.id) return;
862
+ const doc = getIframeDoc();
863
+ const el = doc?.querySelector(`[data-element-id="${selectedElement.id}"]`) as HTMLElement | null;
864
+ if (!el) return;
865
+ const def = await updatePartFromElement(el);
866
+ if (def) notifyIframeChange(true);
867
+ }, [selectedElement?.id, getIframeDoc, updatePartFromElement, notifyIframeChange]);
868
+ const handleDetachPartFromPanel = useCallback(() => {
869
+ if (!selectedElement?.id) return;
870
+ const doc = getIframeDoc();
871
+ const el = doc?.querySelector(`[data-element-id="${selectedElement.id}"]`) as HTMLElement | null;
872
+ if (!doc || !el || !partInfoOf(el)) return;
873
+ detachPart(el);
874
+ unlockPartDescendants(el);
875
+ notifyIframeChange(true);
876
+ void import("../utils/style-utils").then(({ extractElementInfo }) => {
877
+ const info = extractElementInfo(el, doc);
878
+ if (info) setSelectedElement(info);
879
+ });
880
+ }, [selectedElement?.id, getIframeDoc, notifyIframeChange, setSelectedElement]);
881
+
844
882
  const selectedMaster = useMemo(() => {
845
883
  if (!selectedInstance) return null;
846
884
  return getMasterComponent(selectedInstance.masterComponentId);
@@ -882,15 +920,15 @@ export const EditorPropertyPanel = memo(function EditorPropertyPanel() {
882
920
 
883
921
  // リサイズ可能なパネル
884
922
  const { width, isDragging, resizeHandleProps } = useResizablePanel({
885
- initialWidth: 288, // w-72 = 18rem = 288px
886
- minWidth: 240,
923
+ initialWidth: 304,
924
+ minWidth: 280,
887
925
  maxWidth: 480,
888
926
  direction: 'left', // 左端をドラッグしてリサイズ
889
927
  storageKey: 'editor-property-panel-width',
890
928
  });
891
929
 
892
930
  const [openSections, setOpenSections] = useState<PanelSections>({
893
- position: true, // 位置セクション
931
+ position: editorMode !== "webpage", // 流し込みのページでは詳細操作として畳む
894
932
  layout: true, // レイアウトセクション
895
933
  appearance: true, // 外見セクション
896
934
  image: false, // 塗りに統合されたため非表示
@@ -1141,9 +1179,11 @@ export const EditorPropertyPanel = memo(function EditorPropertyPanel() {
1141
1179
  iframeReady={iframeReady}
1142
1180
  >
1143
1181
  <div
1182
+ data-property-panel
1144
1183
  className="flex-shrink-0 bg-[#2c2c2c] border-l border-[#444444] flex flex-col overflow-hidden relative"
1145
1184
  style={{ width: `${width}px` }}
1146
1185
  >
1186
+ <div className="ed-panel-heading">要素の編集</div>
1147
1187
  {/* リサイズハンドル */}
1148
1188
  <div {...resizeHandleProps} />
1149
1189
 
@@ -1164,11 +1204,12 @@ export const EditorPropertyPanel = memo(function EditorPropertyPanel() {
1164
1204
  </div>
1165
1205
  </ScrollArea>
1166
1206
  ) : (
1167
- <div className="flex-1 flex items-center justify-center p-4">
1168
- <div className="text-center text-gray-500 text-xs">
1169
- <MousePointer2 className="w-8 h-8 mx-auto mb-2 opacity-50" />
1170
- <p>要素を選択してください</p>
1171
- </div>
1207
+ <div className="ed-empty">
1208
+ <MousePointer2 className="h-8 w-8" />
1209
+ <strong>編集する要素を選択</strong>
1210
+ <p>紙面の文字や画像をクリックすると、色・大きさ・余白を調整できます。</p>
1211
+ <p>文字はダブルクリックで直接編集できます。</p>
1212
+ <span className="text-xs">Shift+クリックで複数選択</span>
1172
1213
  </div>
1173
1214
  )}
1174
1215
  </div>
@@ -1187,6 +1228,10 @@ export const EditorPropertyPanel = memo(function EditorPropertyPanel() {
1187
1228
  className="flex-shrink-0 bg-[#2c2c2c] border-l border-[#444444] flex flex-col overflow-hidden relative"
1188
1229
  style={{ width: `${width}px` }}
1189
1230
  >
1231
+ <div className="ed-panel-heading">
1232
+ <span>要素の編集</span>
1233
+ <span className="ml-auto truncate text-xs font-normal text-gray-400">{isMultiSelection ? `${selectedElementIds.length}個を選択` : selectedElement.text?.trim().slice(0, 28) || selectedElement.tagName.toLowerCase()}</span>
1234
+ </div>
1190
1235
  {/* リサイズハンドル */}
1191
1236
  <div {...resizeHandleProps} />
1192
1237
 
@@ -1240,6 +1285,37 @@ export const EditorPropertyPanel = memo(function EditorPropertyPanel() {
1240
1285
  />
1241
1286
  )}
1242
1287
 
1288
+ {/* 部品(data-part)のインスタンス */}
1289
+ {selectedPart && (
1290
+ <div className="space-y-2 rounded border border-[#444444] bg-[#2a2a2a] p-2 text-xs text-gray-300">
1291
+ <div className="flex items-center gap-2">
1292
+ <PartIcon className="h-3.5 w-3.5 text-purple-400" />
1293
+ <span className="truncate font-medium">部品: {selectedPartLabel}</span>
1294
+ </div>
1295
+ <p className="text-[10px] leading-snug text-gray-500">
1296
+ スロット(data-slot)の中だけ編集できます。「この姿で部品を更新」で定義が変わり、他ページへの反映は利用側(savePart)が行います。
1297
+ </p>
1298
+ <div className="flex gap-1">
1299
+ <Button
1300
+ size="sm"
1301
+ variant="outline"
1302
+ className="h-6 px-2 text-[10px]"
1303
+ onClick={() => void handleUpdatePartFromPanel()}
1304
+ >
1305
+ この姿で更新
1306
+ </Button>
1307
+ <Button
1308
+ size="sm"
1309
+ variant="outline"
1310
+ className="h-6 px-2 text-[10px]"
1311
+ onClick={handleDetachPartFromPanel}
1312
+ >
1313
+ 切り離す
1314
+ </Button>
1315
+ </div>
1316
+ </div>
1317
+ )}
1318
+
1243
1319
  {/* コンポーネントインスタンスセクション */}
1244
1320
  {selectedInstance && selectedMaster && (
1245
1321
  <InstanceOverrideSection
@@ -113,7 +113,7 @@ export interface LayerTreeItemProps {
113
113
  // Event handlers
114
114
  onToggleExpand: (nodeId: string) => void;
115
115
  onToggleVisibility: (nodeId: string) => void;
116
- onNodeClick: (e: React.MouseEvent, nodeId: string) => void;
116
+ onNodeClick: (e: React.MouseEvent | React.KeyboardEvent, nodeId: string) => void;
117
117
  /** 行ホバー開始(キャンバス側をハイライトする) */
118
118
  onRowMouseEnter: (nodeId: string) => void;
119
119
  /** 行ホバー終了 */
@@ -195,9 +195,30 @@ export const LayerTreeItem = memo(function LayerTreeItem({
195
195
  <ContextMenuTrigger asChild>
196
196
  <div
197
197
  ref={nodeRef}
198
- className={`flex items-center gap-1 py-0.5 px-1 rounded cursor-pointer text-xs group transition-colors ${
198
+ data-layer-row={node.id}
199
+ role="button"
200
+ tabIndex={0}
201
+ aria-label={`${label}(${node.tagName})`}
202
+ aria-pressed={isSelected}
203
+ aria-expanded={hasChildren ? isExpanded : undefined}
204
+ onKeyDown={(e) => {
205
+ if (e.target !== e.currentTarget) return;
206
+ if (e.key === 'Enter' || e.key === ' ') {
207
+ e.preventDefault(); onNodeClick(e, node.id);
208
+ } else if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
209
+ e.preventDefault(); e.stopPropagation();
210
+ if (hasChildren && isExpanded !== (e.key === 'ArrowRight')) onToggleExpand(node.id);
211
+ } else if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(e.key)) {
212
+ e.preventDefault(); e.stopPropagation();
213
+ const rows = Array.from(e.currentTarget.closest('.ed-layer-panel')?.querySelectorAll<HTMLElement>('[data-layer-row]') ?? []);
214
+ const index = rows.indexOf(e.currentTarget);
215
+ const next = e.key === 'Home' ? 0 : e.key === 'End' ? rows.length - 1 : index + (e.key === 'ArrowDown' ? 1 : -1);
216
+ rows[Math.max(0, Math.min(rows.length - 1, next))]?.focus();
217
+ }
218
+ }}
219
+ className={`flex items-center gap-1 min-h-8 py-1 px-1 rounded cursor-pointer text-xs group transition-colors ${
199
220
  isSelected
200
- ? 'bg-[#0d99ff] text-white'
221
+ ? 'ed-layer-selected bg-[#0d99ff]/15 text-[#4fb8ff]'
201
222
  : isHovered
202
223
  ? // キャンバス側のホバーと連動した薄いハイライト
203
224
  'text-gray-100 bg-[#37373d] ring-1 ring-inset ring-[#0d99ff]/50'
@@ -233,7 +254,7 @@ export const LayerTreeItem = memo(function LayerTreeItem({
233
254
  >
234
255
  {/* ドラッグハンドル */}
235
256
  <GripVertical
236
- className={`w-3 h-3 opacity-0 group-hover:opacity-100 cursor-grab ${
257
+ className={`w-3 h-3 opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 focus-visible:opacity-100 cursor-grab ${
237
258
  isSelected ? 'text-white' : 'text-gray-500'
238
259
  }`}
239
260
  />
@@ -241,6 +262,7 @@ export const LayerTreeItem = memo(function LayerTreeItem({
241
262
  {/* 展開/折りたたみ */}
242
263
  {hasChildren ? (
243
264
  <button
265
+ aria-label={isExpanded ? "子要素を折り畳む" : "子要素を表示"}
244
266
  onClick={(e) => {
245
267
  e.stopPropagation();
246
268
  onToggleExpand(node.id);
@@ -261,6 +283,7 @@ export const LayerTreeItem = memo(function LayerTreeItem({
261
283
 
262
284
  {/* 表示/非表示アイコン */}
263
285
  <button
286
+ aria-label={isHidden ? "要素を表示" : "要素を非表示"}
264
287
  onClick={(e) => {
265
288
  e.stopPropagation();
266
289
  onToggleVisibility(node.id);
@@ -271,7 +294,7 @@ export const LayerTreeItem = memo(function LayerTreeItem({
271
294
  : isSelected
272
295
  ? 'text-white'
273
296
  : 'text-gray-400'
274
- } opacity-0 group-hover:opacity-100 ${
297
+ } opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 focus-visible:opacity-100 ${
275
298
  isSelected ? 'hover:bg-[#0d99ff]' : 'hover:bg-gray-600'
276
299
  }`}
277
300
  >
@@ -198,7 +198,7 @@ export function ComponentPanel({
198
198
  {isDragging && <div className="fixed inset-0 z-50 cursor-col-resize" />}
199
199
 
200
200
  {/* Header */}
201
- <div className="flex items-center justify-between p-3 border-b border-[#444444]">
201
+ <div className="ed-panel-heading justify-between">
202
202
  <div className="flex items-center gap-2">
203
203
  <Component className="w-4 h-4 text-gray-400" />
204
204
  <span className="text-sm font-medium text-white">コンポーネント</span>
@@ -207,7 +207,7 @@ export function ComponentPanel({
207
207
  <Button
208
208
  variant="ghost"
209
209
  size="icon"
210
- className="h-6 w-6 text-gray-400 hover:text-white hover:bg-[#444444]"
210
+ className="h-8 w-8 text-gray-400 hover:text-white hover:bg-[#444444]"
211
211
  onClick={onClose}
212
212
  aria-label="コンポーネントパネルを閉じる"
213
213
  >
@@ -237,8 +237,9 @@ export function ComponentPanel({
237
237
  <p className="text-xs text-gray-500">
238
238
  {searchQuery
239
239
  ? '検索に一致するコンポーネントがありません'
240
- : 'コンポーネントがありません'}
240
+ : '登録されたコンポーネントはありません'}
241
241
  </p>
242
+ {!searchQuery && <p className="mt-2 px-4 text-xs text-gray-400">要素を右クリックして「コンポーネントを作成」から登録できます。</p>}
242
243
  </div>
243
244
  ) : (
244
245
  visibleCategories.map((category) => (