@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
|
@@ -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:
|
|
886
|
-
minWidth:
|
|
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:
|
|
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="
|
|
1168
|
-
<
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
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
|
-
|
|
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-
|
|
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="
|
|
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-
|
|
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) => (
|