@growgroup/visual-editor 0.1.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/LICENSE +21 -0
- package/NOTICE +52 -0
- package/README.md +143 -0
- package/dist/editor.css +2 -0
- package/package.json +94 -0
- package/src/components/DeckSlideRender.tsx +21 -0
- package/src/components/SaveNote.tsx +21 -0
- package/src/components/auth/AuthProvider.tsx +44 -0
- package/src/components/ui/accordion.tsx +57 -0
- package/src/components/ui/alert-dialog.tsx +141 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/button.tsx +56 -0
- package/src/components/ui/card.tsx +76 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/collapsible.tsx +11 -0
- package/src/components/ui/context-menu.tsx +200 -0
- package/src/components/ui/dialog.tsx +122 -0
- package/src/components/ui/dropdown-menu.tsx +201 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/popover.tsx +33 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/scrubbable-label.tsx +95 -0
- package/src/components/ui/select.tsx +159 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/slider.tsx +28 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/textarea.tsx +22 -0
- package/src/components/ui/tooltip.tsx +32 -0
- package/src/components/viewer/useDeck.ts +58 -0
- package/src/editor/EditorContext.tsx +632 -0
- package/src/editor/EditorToolbar.tsx +446 -0
- package/src/editor/FrontendVisualEditor.tsx +2595 -0
- package/src/editor/autosave.ts +34 -0
- package/src/editor/components/AiPromptPopover.tsx +378 -0
- package/src/editor/components/BreakpointGuides.tsx +323 -0
- package/src/editor/components/BreakpointSelector.tsx +180 -0
- package/src/editor/components/ComponentPropertyEditor.tsx +426 -0
- package/src/editor/components/CssEditorDialog.tsx +168 -0
- package/src/editor/components/EditorCanvas.tsx +987 -0
- package/src/editor/components/EditorContextMenu.tsx +515 -0
- package/src/editor/components/EditorFooter.tsx +81 -0
- package/src/editor/components/EditorHeader.tsx +524 -0
- package/src/editor/components/EditorLayerPanel.tsx +1197 -0
- package/src/editor/components/EditorPropertyPanel.tsx +2996 -0
- package/src/editor/components/EditorSidebar.tsx +226 -0
- package/src/editor/components/FigmaColorPicker.tsx +1577 -0
- package/src/editor/components/HtmlEditorDialog.tsx +71 -0
- package/src/editor/components/HtmlImportDialog.tsx +346 -0
- package/src/editor/components/JsEditorDialog.tsx +175 -0
- package/src/editor/components/LayerTreeItem.tsx +494 -0
- package/src/editor/components/MasterComponentEditor.tsx +846 -0
- package/src/editor/components/MediaLibraryDialog.tsx +447 -0
- package/src/editor/components/PageSettingsDialog.tsx +776 -0
- package/src/editor/components/VariablePicker.tsx +355 -0
- package/src/editor/components/VariablesDialog.tsx +564 -0
- package/src/editor/components/VariantEditor.tsx +687 -0
- package/src/editor/components/VariantMatrix.tsx +507 -0
- package/src/editor/components/component-panel/CategoryAccordion.tsx +139 -0
- package/src/editor/components/component-panel/ComponentGrid.tsx +51 -0
- package/src/editor/components/component-panel/ComponentItem.tsx +368 -0
- package/src/editor/components/component-panel/ComponentPanel.tsx +275 -0
- package/src/editor/components/component-panel/ComponentSearch.tsx +82 -0
- package/src/editor/components/component-panel/VariantList.tsx +260 -0
- package/src/editor/components/component-panel/index.ts +12 -0
- package/src/editor/components/index.ts +48 -0
- package/src/editor/components/multi-page/CanvasZoomControls.tsx +71 -0
- package/src/editor/components/multi-page/InfiniteCanvas.tsx +95 -0
- package/src/editor/components/multi-page/MultiPageCanvasView.tsx +1404 -0
- package/src/editor/components/multi-page/PageFrameOverlay.tsx +76 -0
- package/src/editor/components/multi-page/PageLabel.tsx +34 -0
- package/src/editor/components/multi-page/PageLivePreview.tsx +242 -0
- package/src/editor/components/multi-page/PageThumbnail.tsx +59 -0
- package/src/editor/components/multi-page/index.ts +7 -0
- package/src/editor/components/ppt/PptChrome.tsx +2508 -0
- package/src/editor/components/ppt/PptComments.tsx +589 -0
- package/src/editor/components/ppt/PptDesignProposals.tsx +275 -0
- package/src/editor/components/ppt/PptFormatPane.tsx +320 -0
- package/src/editor/components/ppt/PptNotes.tsx +246 -0
- package/src/editor/components/property-panel/AlignmentPanel.tsx +110 -0
- package/src/editor/components/property-panel/AutoLayoutPanel.tsx +615 -0
- package/src/editor/components/property-panel/BorderSection.tsx +104 -0
- package/src/editor/components/property-panel/CompactNumberInput.tsx +237 -0
- package/src/editor/components/property-panel/CompactSizeInput.tsx +148 -0
- package/src/editor/components/property-panel/EffectSection.tsx +157 -0
- package/src/editor/components/property-panel/FillSection.tsx +69 -0
- package/src/editor/components/property-panel/GoogleFontPicker.tsx +192 -0
- package/src/editor/components/property-panel/ImageSection.tsx +206 -0
- package/src/editor/components/property-panel/ImgSrcSection.tsx +125 -0
- package/src/editor/components/property-panel/InstanceOverrideSection.tsx +413 -0
- package/src/editor/components/property-panel/LayoutSection.tsx +105 -0
- package/src/editor/components/property-panel/LinkSection.tsx +103 -0
- package/src/editor/components/property-panel/PropertyVariablePicker.tsx +311 -0
- package/src/editor/components/property-panel/ScalePanel.tsx +212 -0
- package/src/editor/components/property-panel/TypographySection.tsx +230 -0
- package/src/editor/components/property-panel/UnitAwareNumberInput.tsx +238 -0
- package/src/editor/components/property-panel/VariableAwareColorInput.tsx +477 -0
- package/src/editor/components/property-panel/VariableAwareInput.tsx +606 -0
- package/src/editor/components/property-panel/VariableAwareSizeInput.tsx +588 -0
- package/src/editor/components/property-panel/VariableAwareUnitInput.tsx +546 -0
- package/src/editor/components/property-panel/index.ts +24 -0
- package/src/editor/components/property-panel/unit-utils.ts +261 -0
- package/src/editor/components/shell/EditorTopBar.tsx +392 -0
- package/src/editor/components/shell/LeftPanel.tsx +47 -0
- package/src/editor/components/shell/PagesPanel.tsx +118 -0
- package/src/editor/components/variables-panel/ColorValueCell.tsx +165 -0
- package/src/editor/components/variables-panel/TextValueCell.tsx +116 -0
- package/src/editor/components/variables-panel/VariablesPanel.tsx +518 -0
- package/src/editor/components/variables-panel/index.ts +7 -0
- package/src/editor/constants.ts +468 -0
- package/src/editor/contexts/EditorArtboardContext.tsx +229 -0
- package/src/editor/contexts/EditorComponentsContext.tsx +1320 -0
- package/src/editor/contexts/EditorDocumentContext.tsx +113 -0
- package/src/editor/contexts/EditorHistoryContext.tsx +516 -0
- package/src/editor/contexts/EditorRefsContext.tsx +204 -0
- package/src/editor/contexts/EditorSelectionContext.tsx +54 -0
- package/src/editor/contexts/EditorToolContext.tsx +58 -0
- package/src/editor/contexts/EditorUIStateContext.tsx +122 -0
- package/src/editor/contexts/EditorVariablesContext.tsx +397 -0
- package/src/editor/contexts/EditorViewContext.tsx +101 -0
- package/src/editor/contexts/MultiPageCanvasContext.tsx +498 -0
- package/src/editor/contexts/index.ts +17 -0
- package/src/editor/editor-skin.css +122 -0
- package/src/editor/hooks/index.ts +38 -0
- package/src/editor/hooks/useAiReplace.ts +397 -0
- package/src/editor/hooks/useAltMeasure.ts +67 -0
- package/src/editor/hooks/useBrowserZoomPrevention.ts +121 -0
- package/src/editor/hooks/useCanvasControls.ts +1222 -0
- package/src/editor/hooks/useComponentEditMode.ts +658 -0
- package/src/editor/hooks/useComponentInstances.ts +515 -0
- package/src/editor/hooks/useContextMenuHandler.ts +157 -0
- package/src/editor/hooks/useCoordinateTransform.ts +207 -0
- package/src/editor/hooks/useDragResize.ts +1317 -0
- package/src/editor/hooks/useDrawingMode.ts +472 -0
- package/src/editor/hooks/useEditorColors.ts +144 -0
- package/src/editor/hooks/useEditorMessages.ts +108 -0
- package/src/editor/hooks/useElementActions.ts +1754 -0
- package/src/editor/hooks/useElementSelection.ts +1573 -0
- package/src/editor/hooks/useFocusManagement.ts +249 -0
- package/src/editor/hooks/useGoogleFonts.ts +305 -0
- package/src/editor/hooks/useIframeInitializer.ts +584 -0
- package/src/editor/hooks/useIframeSetup.ts +360 -0
- package/src/editor/hooks/useImageUpload.ts +270 -0
- package/src/editor/hooks/useInfiniteCanvas.ts +301 -0
- package/src/editor/hooks/useKeyboardShortcuts.ts +205 -0
- package/src/editor/hooks/useMarqueeSelection.ts +388 -0
- package/src/editor/hooks/useMediaLibrary.ts +129 -0
- package/src/editor/hooks/usePageSettingsManager.ts +739 -0
- package/src/editor/hooks/useResizablePanel.ts +163 -0
- package/src/editor/hooks/useRichPaste.ts +255 -0
- package/src/editor/hooks/useTouchGestures.ts +197 -0
- package/src/editor/index.ts +27 -0
- package/src/editor/types/page-settings.ts +513 -0
- package/src/editor/types.ts +382 -0
- package/src/editor/utils/align-elements.ts +169 -0
- package/src/editor/utils/api-json.ts +21 -0
- package/src/editor/utils/aspect-lock.ts +50 -0
- package/src/editor/utils/component-renderer.ts +702 -0
- package/src/editor/utils/component-sync.ts +845 -0
- package/src/editor/utils/crop-mode.ts +265 -0
- package/src/editor/utils/dom-utils.ts +2238 -0
- package/src/editor/utils/element-effects.ts +218 -0
- package/src/editor/utils/eyedropper.ts +35 -0
- package/src/editor/utils/figma-export.ts +560 -0
- package/src/editor/utils/figma-kiwi-decoder.ts +722 -0
- package/src/editor/utils/figma-kiwi-encoder.ts +583 -0
- package/src/editor/utils/figma-paste.ts +2262 -0
- package/src/editor/utils/flex-reorder.ts +231 -0
- package/src/editor/utils/flex-utils.ts +1230 -0
- package/src/editor/utils/geometry.ts +232 -0
- package/src/editor/utils/grid-layout.ts +210 -0
- package/src/editor/utils/html-utils.ts +470 -0
- package/src/editor/utils/index.ts +11 -0
- package/src/editor/utils/ink-style.ts +57 -0
- package/src/editor/utils/inline-format.ts +236 -0
- package/src/editor/utils/measure-distance.ts +120 -0
- package/src/editor/utils/override-apply.ts +526 -0
- package/src/editor/utils/override-detection.ts +613 -0
- package/src/editor/utils/paste-processors.ts +609 -0
- package/src/editor/utils/paste-sanitizer.ts +244 -0
- package/src/editor/utils/restore-flow.ts +188 -0
- package/src/editor/utils/shape-library.ts +171 -0
- package/src/editor/utils/slide-root.ts +29 -0
- package/src/editor/utils/smart-guides.ts +218 -0
- package/src/editor/utils/style-utils.ts +685 -0
- package/src/editor/utils/table-edit.ts +151 -0
- package/src/editor/utils/tailwind-mappings.ts +778 -0
- package/src/editor/utils/tailwind-utils.ts +804 -0
- package/src/editor/utils/text-highlight.ts +216 -0
- package/src/editor/utils/theme-to-variables.ts +341 -0
- package/src/editor/utils/variant-resolver.ts +456 -0
- package/src/editor/utils/viewport-utils.ts +295 -0
- package/src/hooks/useEditorHistory.ts +195 -0
- package/src/hooks/useEditorShortcuts.ts +260 -0
- package/src/index.ts +40 -0
- package/src/io.ts +134 -0
- package/src/lib/agent/slide-agent/types.ts +547 -0
- package/src/lib/agent/website-agent/types.ts +805 -0
- package/src/lib/api/auth-fetch.ts +58 -0
- package/src/lib/deck.ts +69 -0
- package/src/lib/export.ts +47 -0
- package/src/lib/firebase/config.ts +29 -0
- package/src/lib/firebase/css-variables.ts +378 -0
- package/src/lib/firebase/editor-components.ts +677 -0
- package/src/lib/firebase/storage.ts +469 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/editor.css +52 -0
- package/src/styles/skin.css +122 -0
- package/src/types/css-variables.ts +645 -0
- package/src/types/editor-components.ts +944 -0
- package/src/types/editor.ts +420 -0
- package/src/types/page-master.ts +197 -0
- package/src/types/page.ts +419 -0
- package/src/types/slide.ts +402 -0
- package/src/types/website-theme.ts +788 -0
- package/src/types/website.ts +374 -0
- package/src/vendor/firebase-firestore.ts +13 -0
- package/src/vendor/firebase-functions.ts +17 -0
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ローカル永続化スタブ。オリジナルは Firebase 実装(Firestore)。
|
|
3
|
+
*
|
|
4
|
+
* MasterComponent / ComponentInstance の CRUD をバックエンド無しで提供する。
|
|
5
|
+
*
|
|
6
|
+
* オリジナルの Firestore スキーマ:
|
|
7
|
+
* - /websites/{websiteId}/componentLibrary/master/components/{componentId} -> MasterComponent
|
|
8
|
+
* - /websites/{websiteId}/pages/{pageId}/componentInstances/{instanceId} -> ComponentInstance
|
|
9
|
+
*
|
|
10
|
+
* 本実装の localStorage スキーマ:
|
|
11
|
+
* - `gg-editor:masterComponents` -> { [websiteId]: { [componentId]: doc } }
|
|
12
|
+
* - `gg-editor:componentInstances` -> { [websiteId]: { [pageId]: { [instanceId]: doc } } }
|
|
13
|
+
*
|
|
14
|
+
* export 名・引数・戻り値型はオリジナルと同一(呼び出し側を変更しないため)。
|
|
15
|
+
* 日時は Firestore Timestamp ではなく ISO 8601 文字列で保持する。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
MasterComponent,
|
|
20
|
+
ComponentInstance,
|
|
21
|
+
ComponentVariant,
|
|
22
|
+
ComponentOverride,
|
|
23
|
+
ExposedProperty,
|
|
24
|
+
} from '../../types/editor-components';
|
|
25
|
+
|
|
26
|
+
// ============================================================
|
|
27
|
+
// localStorage Helpers
|
|
28
|
+
// ============================================================
|
|
29
|
+
|
|
30
|
+
const MASTER_COMPONENTS_KEY = 'gg-editor:masterComponents';
|
|
31
|
+
const COMPONENT_INSTANCES_KEY = 'gg-editor:componentInstances';
|
|
32
|
+
|
|
33
|
+
type StoredDoc = Record<string, unknown>;
|
|
34
|
+
/** { [websiteId]: { [componentId]: doc } } */
|
|
35
|
+
type MasterStore = Record<string, Record<string, StoredDoc>>;
|
|
36
|
+
/** { [websiteId]: { [pageId]: { [instanceId]: doc } } } */
|
|
37
|
+
type InstanceStore = Record<string, Record<string, Record<string, StoredDoc>>>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* localStorage が利用可能かどうか(SSR / 制限環境対策)
|
|
41
|
+
*/
|
|
42
|
+
function getStore(): Storage | null {
|
|
43
|
+
try {
|
|
44
|
+
if (typeof window === 'undefined' || !window.localStorage) return null;
|
|
45
|
+
return window.localStorage;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readStore<T extends object>(key: string): T {
|
|
52
|
+
const store = getStore();
|
|
53
|
+
if (!store) return {} as T;
|
|
54
|
+
|
|
55
|
+
const raw = store.getItem(key);
|
|
56
|
+
if (!raw) return {} as T;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(raw);
|
|
60
|
+
if (!parsed || typeof parsed !== 'object') return {} as T;
|
|
61
|
+
return parsed as T;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error(`Error parsing localStorage key "${key}":`, error);
|
|
64
|
+
return {} as T;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function writeStore(key: string, value: object): void {
|
|
69
|
+
const store = getStore();
|
|
70
|
+
if (!store) return;
|
|
71
|
+
store.setItem(key, JSON.stringify(value));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readMasterStore(): MasterStore {
|
|
75
|
+
return readStore<MasterStore>(MASTER_COMPONENTS_KEY);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function writeMasterStore(store: MasterStore): void {
|
|
79
|
+
writeStore(MASTER_COMPONENTS_KEY, store);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readInstanceStore(): InstanceStore {
|
|
83
|
+
return readStore<InstanceStore>(COMPONENT_INSTANCES_KEY);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function writeInstanceStore(store: InstanceStore): void {
|
|
87
|
+
writeStore(COMPONENT_INSTANCES_KEY, store);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ============================================================
|
|
91
|
+
// Helper Functions
|
|
92
|
+
// ============================================================
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Remove undefined values from an object recursively.
|
|
96
|
+
* (オリジナルは Firestore が undefined を受け付けないための処理。
|
|
97
|
+
* JSON.stringify でも undefined は落ちるが、挙動を揃えるため移植している)
|
|
98
|
+
*/
|
|
99
|
+
function removeUndefinedValues<T extends Record<string, unknown>>(obj: T): T {
|
|
100
|
+
const result: Record<string, unknown> = {};
|
|
101
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
102
|
+
if (value === undefined) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (
|
|
106
|
+
value !== null &&
|
|
107
|
+
typeof value === 'object' &&
|
|
108
|
+
!Array.isArray(value) &&
|
|
109
|
+
!(value instanceof Date)
|
|
110
|
+
) {
|
|
111
|
+
const cleaned = removeUndefinedValues(value as Record<string, unknown>);
|
|
112
|
+
result[key] = cleaned;
|
|
113
|
+
} else if (Array.isArray(value)) {
|
|
114
|
+
result[key] = value
|
|
115
|
+
.map((item) =>
|
|
116
|
+
item !== null &&
|
|
117
|
+
typeof item === 'object' &&
|
|
118
|
+
!Array.isArray(item) &&
|
|
119
|
+
!(item instanceof Date)
|
|
120
|
+
? removeUndefinedValues(item as Record<string, unknown>)
|
|
121
|
+
: item
|
|
122
|
+
)
|
|
123
|
+
.filter((item) => item !== undefined);
|
|
124
|
+
} else {
|
|
125
|
+
result[key] = value;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return result as T;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Convert stored timestamp value to Date.
|
|
133
|
+
*/
|
|
134
|
+
function timestampToDate(timestamp: unknown): Date {
|
|
135
|
+
if (timestamp instanceof Date) return timestamp;
|
|
136
|
+
if (typeof timestamp === 'string' || typeof timestamp === 'number') {
|
|
137
|
+
const date = new Date(timestamp);
|
|
138
|
+
return Number.isNaN(date.getTime()) ? new Date() : date;
|
|
139
|
+
}
|
|
140
|
+
if (timestamp && typeof timestamp === 'object' && 'toDate' in timestamp) {
|
|
141
|
+
try {
|
|
142
|
+
return (timestamp as { toDate: () => Date }).toDate();
|
|
143
|
+
} catch {
|
|
144
|
+
return new Date();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return new Date();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Convert Date or ISO string to ISO string for storage.
|
|
152
|
+
*/
|
|
153
|
+
function dateToISOString(date: Date | string | undefined): string {
|
|
154
|
+
if (!date) return new Date().toISOString();
|
|
155
|
+
if (typeof date === 'string') return date;
|
|
156
|
+
return date.toISOString();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Convert stored document data to MasterComponent.
|
|
161
|
+
*/
|
|
162
|
+
function docToMasterComponent(
|
|
163
|
+
id: string,
|
|
164
|
+
data: Record<string, unknown>
|
|
165
|
+
): MasterComponent {
|
|
166
|
+
return {
|
|
167
|
+
// 保存時に載せた追加フィールド(rootElement / variantProperties / figmaData 等)を維持
|
|
168
|
+
...(data as Partial<MasterComponent>),
|
|
169
|
+
id,
|
|
170
|
+
name: data.name as string,
|
|
171
|
+
description: data.description as string | undefined,
|
|
172
|
+
categoryId: data.categoryId as string,
|
|
173
|
+
tags: (data.tags as string[]) || [],
|
|
174
|
+
variants: (data.variants as ComponentVariant[]) || [],
|
|
175
|
+
defaultVariantId: data.defaultVariantId as string,
|
|
176
|
+
exposedProperties: (data.exposedProperties as ExposedProperty[]) || [],
|
|
177
|
+
createdAt: dateToISOString(
|
|
178
|
+
data.createdAt
|
|
179
|
+
? timestampToDate(data.createdAt)
|
|
180
|
+
: undefined
|
|
181
|
+
),
|
|
182
|
+
updatedAt: dateToISOString(
|
|
183
|
+
data.updatedAt
|
|
184
|
+
? timestampToDate(data.updatedAt)
|
|
185
|
+
: undefined
|
|
186
|
+
),
|
|
187
|
+
createdBy: data.createdBy as string | undefined,
|
|
188
|
+
websiteId: data.websiteId as string,
|
|
189
|
+
thumbnail: data.thumbnail as string | undefined,
|
|
190
|
+
version: (data.version as number) || 1,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Convert stored document data to ComponentInstance.
|
|
196
|
+
*/
|
|
197
|
+
function docToComponentInstance(
|
|
198
|
+
id: string,
|
|
199
|
+
data: Record<string, unknown>
|
|
200
|
+
): ComponentInstance {
|
|
201
|
+
return {
|
|
202
|
+
// 保存時に載せた追加フィールド(position / size / name 等)を維持
|
|
203
|
+
...(data as Partial<ComponentInstance>),
|
|
204
|
+
id,
|
|
205
|
+
masterComponentId: data.masterComponentId as string,
|
|
206
|
+
variantId: data.variantId as string,
|
|
207
|
+
domElementId: data.domElementId as string,
|
|
208
|
+
overrides: (data.overrides as ComponentOverride[]) || [],
|
|
209
|
+
propertyValues:
|
|
210
|
+
(data.propertyValues as Record<string, string | number | boolean>) || {},
|
|
211
|
+
isDetached: (data.isDetached as boolean) || false,
|
|
212
|
+
pageId: data.pageId as string | undefined,
|
|
213
|
+
createdAt: dateToISOString(
|
|
214
|
+
data.createdAt
|
|
215
|
+
? timestampToDate(data.createdAt)
|
|
216
|
+
: undefined
|
|
217
|
+
),
|
|
218
|
+
updatedAt: dateToISOString(
|
|
219
|
+
data.updatedAt
|
|
220
|
+
? timestampToDate(data.updatedAt)
|
|
221
|
+
: undefined
|
|
222
|
+
),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Prepare MasterComponent for storage.
|
|
228
|
+
*/
|
|
229
|
+
function masterComponentToDoc(
|
|
230
|
+
component: MasterComponent
|
|
231
|
+
): Record<string, unknown> {
|
|
232
|
+
const { id: _id, ...rest } = component;
|
|
233
|
+
void _id; // id はキーとして保持するのでドキュメント本体には含めない
|
|
234
|
+
|
|
235
|
+
const doc: Record<string, unknown> = {
|
|
236
|
+
...rest,
|
|
237
|
+
name: component.name,
|
|
238
|
+
categoryId: component.categoryId,
|
|
239
|
+
tags: component.tags,
|
|
240
|
+
variants: component.variants,
|
|
241
|
+
defaultVariantId: component.defaultVariantId,
|
|
242
|
+
exposedProperties: component.exposedProperties,
|
|
243
|
+
createdAt: component.createdAt,
|
|
244
|
+
updatedAt: new Date().toISOString(),
|
|
245
|
+
websiteId: component.websiteId,
|
|
246
|
+
version: component.version,
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
if (component.description) doc.description = component.description;
|
|
250
|
+
if (component.createdBy) doc.createdBy = component.createdBy;
|
|
251
|
+
if (component.thumbnail) doc.thumbnail = component.thumbnail;
|
|
252
|
+
|
|
253
|
+
return removeUndefinedValues(doc);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Prepare ComponentInstance for storage.
|
|
258
|
+
*/
|
|
259
|
+
function componentInstanceToDoc(
|
|
260
|
+
instance: ComponentInstance
|
|
261
|
+
): Record<string, unknown> {
|
|
262
|
+
const { id: _id, ...rest } = instance;
|
|
263
|
+
void _id; // id はキーとして保持するのでドキュメント本体には含めない
|
|
264
|
+
|
|
265
|
+
const doc: Record<string, unknown> = {
|
|
266
|
+
...rest,
|
|
267
|
+
masterComponentId: instance.masterComponentId,
|
|
268
|
+
variantId: instance.variantId,
|
|
269
|
+
domElementId: instance.domElementId,
|
|
270
|
+
overrides: instance.overrides,
|
|
271
|
+
propertyValues: instance.propertyValues,
|
|
272
|
+
isDetached: instance.isDetached,
|
|
273
|
+
createdAt: instance.createdAt,
|
|
274
|
+
updatedAt: new Date().toISOString(),
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
if (instance.pageId) doc.pageId = instance.pageId;
|
|
278
|
+
|
|
279
|
+
return removeUndefinedValues(doc);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ============================================================
|
|
283
|
+
// Master Component Operations
|
|
284
|
+
// ============================================================
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Save a master component to localStorage.
|
|
288
|
+
*/
|
|
289
|
+
export async function saveMasterComponent(
|
|
290
|
+
websiteId: string,
|
|
291
|
+
component: MasterComponent
|
|
292
|
+
): Promise<void> {
|
|
293
|
+
try {
|
|
294
|
+
const store = readMasterStore();
|
|
295
|
+
const websiteComponents = store[websiteId] ?? {};
|
|
296
|
+
websiteComponents[component.id] = masterComponentToDoc(component);
|
|
297
|
+
store[websiteId] = websiteComponents;
|
|
298
|
+
writeMasterStore(store);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
console.error('Error saving master component:', error);
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Get a master component by ID.
|
|
307
|
+
*/
|
|
308
|
+
export async function getMasterComponent(
|
|
309
|
+
websiteId: string,
|
|
310
|
+
componentId: string
|
|
311
|
+
): Promise<MasterComponent | null> {
|
|
312
|
+
try {
|
|
313
|
+
const store = readMasterStore();
|
|
314
|
+
const data = store[websiteId]?.[componentId];
|
|
315
|
+
|
|
316
|
+
if (!data) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return docToMasterComponent(componentId, data);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
console.error('Error getting master component:', error);
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Get all master components for a website.
|
|
329
|
+
*/
|
|
330
|
+
export async function getAllMasterComponents(
|
|
331
|
+
websiteId: string
|
|
332
|
+
): Promise<MasterComponent[]> {
|
|
333
|
+
try {
|
|
334
|
+
const store = readMasterStore();
|
|
335
|
+
const websiteComponents = store[websiteId] ?? {};
|
|
336
|
+
|
|
337
|
+
return Object.entries(websiteComponents).map(([componentId, data]) =>
|
|
338
|
+
docToMasterComponent(componentId, data)
|
|
339
|
+
);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
console.error('Error getting all master components:', error);
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Update a master component.
|
|
348
|
+
*/
|
|
349
|
+
export async function updateMasterComponent(
|
|
350
|
+
websiteId: string,
|
|
351
|
+
componentId: string,
|
|
352
|
+
updates: Partial<MasterComponent>
|
|
353
|
+
): Promise<void> {
|
|
354
|
+
try {
|
|
355
|
+
const store = readMasterStore();
|
|
356
|
+
const websiteComponents = store[websiteId] ?? {};
|
|
357
|
+
const existing = websiteComponents[componentId];
|
|
358
|
+
|
|
359
|
+
if (!existing) {
|
|
360
|
+
console.warn(
|
|
361
|
+
`[editor-components] Master component not found, creating: ${websiteId}/${componentId}`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const updateData = removeUndefinedValues({
|
|
366
|
+
...updates,
|
|
367
|
+
updatedAt: new Date().toISOString(),
|
|
368
|
+
version: (updates.version ?? 1) + 1,
|
|
369
|
+
} as Record<string, unknown>);
|
|
370
|
+
|
|
371
|
+
// Remove id from updates (should not be stored in the document)
|
|
372
|
+
delete updateData.id;
|
|
373
|
+
|
|
374
|
+
websiteComponents[componentId] = { ...(existing ?? {}), ...updateData };
|
|
375
|
+
store[websiteId] = websiteComponents;
|
|
376
|
+
writeMasterStore(store);
|
|
377
|
+
} catch (error) {
|
|
378
|
+
console.error('Error updating master component:', error);
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Delete a master component.
|
|
385
|
+
*/
|
|
386
|
+
export async function deleteMasterComponent(
|
|
387
|
+
websiteId: string,
|
|
388
|
+
componentId: string
|
|
389
|
+
): Promise<void> {
|
|
390
|
+
try {
|
|
391
|
+
const store = readMasterStore();
|
|
392
|
+
const websiteComponents = store[websiteId];
|
|
393
|
+
if (!websiteComponents) return;
|
|
394
|
+
|
|
395
|
+
delete websiteComponents[componentId];
|
|
396
|
+
store[websiteId] = websiteComponents;
|
|
397
|
+
writeMasterStore(store);
|
|
398
|
+
} catch (error) {
|
|
399
|
+
console.error('Error deleting master component:', error);
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ============================================================
|
|
405
|
+
// Component Instance Operations
|
|
406
|
+
// ============================================================
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Save a component instance to localStorage.
|
|
410
|
+
*/
|
|
411
|
+
export async function saveComponentInstance(
|
|
412
|
+
websiteId: string,
|
|
413
|
+
pageId: string,
|
|
414
|
+
instance: ComponentInstance
|
|
415
|
+
): Promise<void> {
|
|
416
|
+
try {
|
|
417
|
+
const store = readInstanceStore();
|
|
418
|
+
const pages = store[websiteId] ?? {};
|
|
419
|
+
const instances = pages[pageId] ?? {};
|
|
420
|
+
|
|
421
|
+
instances[instance.id] = componentInstanceToDoc(instance);
|
|
422
|
+
pages[pageId] = instances;
|
|
423
|
+
store[websiteId] = pages;
|
|
424
|
+
writeInstanceStore(store);
|
|
425
|
+
} catch (error) {
|
|
426
|
+
console.error('Error saving component instance:', error);
|
|
427
|
+
throw error;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Get a component instance by ID.
|
|
433
|
+
*/
|
|
434
|
+
export async function getComponentInstance(
|
|
435
|
+
websiteId: string,
|
|
436
|
+
pageId: string,
|
|
437
|
+
instanceId: string
|
|
438
|
+
): Promise<ComponentInstance | null> {
|
|
439
|
+
try {
|
|
440
|
+
const store = readInstanceStore();
|
|
441
|
+
const data = store[websiteId]?.[pageId]?.[instanceId];
|
|
442
|
+
|
|
443
|
+
if (!data) {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return docToComponentInstance(instanceId, data);
|
|
448
|
+
} catch (error) {
|
|
449
|
+
console.error('Error getting component instance:', error);
|
|
450
|
+
throw error;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Get all component instances for a page.
|
|
456
|
+
*/
|
|
457
|
+
export async function getPageComponentInstances(
|
|
458
|
+
websiteId: string,
|
|
459
|
+
pageId: string
|
|
460
|
+
): Promise<ComponentInstance[]> {
|
|
461
|
+
try {
|
|
462
|
+
const store = readInstanceStore();
|
|
463
|
+
const instances = store[websiteId]?.[pageId] ?? {};
|
|
464
|
+
|
|
465
|
+
return Object.entries(instances).map(([instanceId, data]) =>
|
|
466
|
+
docToComponentInstance(instanceId, data)
|
|
467
|
+
);
|
|
468
|
+
} catch (error) {
|
|
469
|
+
console.error('Error getting page component instances:', error);
|
|
470
|
+
throw error;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Update a component instance.
|
|
476
|
+
*/
|
|
477
|
+
export async function updateComponentInstance(
|
|
478
|
+
websiteId: string,
|
|
479
|
+
pageId: string,
|
|
480
|
+
instanceId: string,
|
|
481
|
+
updates: Partial<ComponentInstance>
|
|
482
|
+
): Promise<void> {
|
|
483
|
+
try {
|
|
484
|
+
const store = readInstanceStore();
|
|
485
|
+
const pages = store[websiteId] ?? {};
|
|
486
|
+
const instances = pages[pageId] ?? {};
|
|
487
|
+
const existing = instances[instanceId];
|
|
488
|
+
|
|
489
|
+
if (!existing) {
|
|
490
|
+
console.warn(
|
|
491
|
+
`[editor-components] Component instance not found, creating: ${websiteId}/${pageId}/${instanceId}`
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const updateData = removeUndefinedValues({
|
|
496
|
+
...updates,
|
|
497
|
+
updatedAt: new Date().toISOString(),
|
|
498
|
+
} as Record<string, unknown>);
|
|
499
|
+
|
|
500
|
+
// Remove id from updates (should not be stored in the document)
|
|
501
|
+
delete updateData.id;
|
|
502
|
+
|
|
503
|
+
instances[instanceId] = { ...(existing ?? {}), ...updateData };
|
|
504
|
+
pages[pageId] = instances;
|
|
505
|
+
store[websiteId] = pages;
|
|
506
|
+
writeInstanceStore(store);
|
|
507
|
+
} catch (error) {
|
|
508
|
+
console.error('Error updating component instance:', error);
|
|
509
|
+
throw error;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Delete a component instance.
|
|
515
|
+
*/
|
|
516
|
+
export async function deleteComponentInstance(
|
|
517
|
+
websiteId: string,
|
|
518
|
+
pageId: string,
|
|
519
|
+
instanceId: string
|
|
520
|
+
): Promise<void> {
|
|
521
|
+
try {
|
|
522
|
+
const store = readInstanceStore();
|
|
523
|
+
const instances = store[websiteId]?.[pageId];
|
|
524
|
+
if (!instances) return;
|
|
525
|
+
|
|
526
|
+
delete instances[instanceId];
|
|
527
|
+
writeInstanceStore(store);
|
|
528
|
+
} catch (error) {
|
|
529
|
+
console.error('Error deleting component instance:', error);
|
|
530
|
+
throw error;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Get all instances of a master component across all pages.
|
|
536
|
+
* (オリジナルは Firestore の collectionGroup クエリ。ここでは全ページを走査する)
|
|
537
|
+
*/
|
|
538
|
+
export async function getInstancesByMasterComponent(
|
|
539
|
+
websiteId: string,
|
|
540
|
+
masterComponentId: string
|
|
541
|
+
): Promise<ComponentInstance[]> {
|
|
542
|
+
try {
|
|
543
|
+
const store = readInstanceStore();
|
|
544
|
+
const pages = store[websiteId] ?? {};
|
|
545
|
+
|
|
546
|
+
const instances: ComponentInstance[] = [];
|
|
547
|
+
for (const [pageId, pageInstances] of Object.entries(pages)) {
|
|
548
|
+
for (const [instanceId, data] of Object.entries(pageInstances)) {
|
|
549
|
+
if (data.masterComponentId !== masterComponentId) continue;
|
|
550
|
+
instances.push(
|
|
551
|
+
docToComponentInstance(instanceId, { pageId, ...data })
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return instances;
|
|
557
|
+
} catch (error) {
|
|
558
|
+
console.error('Error getting instances by master component:', error);
|
|
559
|
+
throw error;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// ============================================================
|
|
564
|
+
// Batch Operations
|
|
565
|
+
// ============================================================
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Batch update multiple component instances across pages.
|
|
569
|
+
*/
|
|
570
|
+
export async function batchUpdateInstances(
|
|
571
|
+
websiteId: string,
|
|
572
|
+
updates: Array<{
|
|
573
|
+
pageId: string;
|
|
574
|
+
instanceId: string;
|
|
575
|
+
data: Partial<ComponentInstance>;
|
|
576
|
+
}>
|
|
577
|
+
): Promise<void> {
|
|
578
|
+
try {
|
|
579
|
+
const store = readInstanceStore();
|
|
580
|
+
const pages = store[websiteId] ?? {};
|
|
581
|
+
const now = new Date().toISOString();
|
|
582
|
+
|
|
583
|
+
for (const update of updates) {
|
|
584
|
+
const instances = pages[update.pageId] ?? {};
|
|
585
|
+
const existing = instances[update.instanceId];
|
|
586
|
+
|
|
587
|
+
const updateData = removeUndefinedValues({
|
|
588
|
+
...update.data,
|
|
589
|
+
updatedAt: now,
|
|
590
|
+
} as Record<string, unknown>);
|
|
591
|
+
|
|
592
|
+
// Remove id from updates
|
|
593
|
+
delete updateData.id;
|
|
594
|
+
|
|
595
|
+
instances[update.instanceId] = { ...(existing ?? {}), ...updateData };
|
|
596
|
+
pages[update.pageId] = instances;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
store[websiteId] = pages;
|
|
600
|
+
writeInstanceStore(store);
|
|
601
|
+
} catch (error) {
|
|
602
|
+
console.error('Error batch updating instances:', error);
|
|
603
|
+
throw error;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Batch delete multiple component instances.
|
|
609
|
+
*/
|
|
610
|
+
export async function batchDeleteInstances(
|
|
611
|
+
websiteId: string,
|
|
612
|
+
instances: Array<{ pageId: string; instanceId: string }>
|
|
613
|
+
): Promise<void> {
|
|
614
|
+
try {
|
|
615
|
+
const store = readInstanceStore();
|
|
616
|
+
const pages = store[websiteId];
|
|
617
|
+
if (!pages) return;
|
|
618
|
+
|
|
619
|
+
for (const instance of instances) {
|
|
620
|
+
const pageInstances = pages[instance.pageId];
|
|
621
|
+
if (!pageInstances) continue;
|
|
622
|
+
delete pageInstances[instance.instanceId];
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
writeInstanceStore(store);
|
|
626
|
+
} catch (error) {
|
|
627
|
+
console.error('Error batch deleting instances:', error);
|
|
628
|
+
throw error;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Propagate master component changes to all non-detached instances.
|
|
634
|
+
* Updates the variant structure for all instances using the specified master component.
|
|
635
|
+
*/
|
|
636
|
+
export async function propagateMasterChanges(
|
|
637
|
+
websiteId: string,
|
|
638
|
+
masterComponentId: string,
|
|
639
|
+
newVersion: number
|
|
640
|
+
): Promise<number> {
|
|
641
|
+
try {
|
|
642
|
+
void newVersion; // オリジナル同様、現状はバージョン番号を直接書き込まない
|
|
643
|
+
|
|
644
|
+
const instances = await getInstancesByMasterComponent(
|
|
645
|
+
websiteId,
|
|
646
|
+
masterComponentId
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
// Filter to only non-detached instances
|
|
650
|
+
const linkedInstances = instances.filter((inst) => !inst.isDetached);
|
|
651
|
+
|
|
652
|
+
if (linkedInstances.length === 0) {
|
|
653
|
+
return 0;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Batch update all linked instances
|
|
657
|
+
const updates = linkedInstances.map((instance) => ({
|
|
658
|
+
pageId: instance.pageId || '',
|
|
659
|
+
instanceId: instance.id,
|
|
660
|
+
data: {
|
|
661
|
+
updatedAt: new Date().toISOString(),
|
|
662
|
+
},
|
|
663
|
+
}));
|
|
664
|
+
|
|
665
|
+
// Filter out instances without pageId
|
|
666
|
+
const validUpdates = updates.filter((u) => u.pageId);
|
|
667
|
+
|
|
668
|
+
if (validUpdates.length > 0) {
|
|
669
|
+
await batchUpdateInstances(websiteId, validUpdates);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return linkedInstances.length;
|
|
673
|
+
} catch (error) {
|
|
674
|
+
console.error('Error propagating master changes:', error);
|
|
675
|
+
throw error;
|
|
676
|
+
}
|
|
677
|
+
}
|