@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.
Files changed (219) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +52 -0
  3. package/README.md +143 -0
  4. package/dist/editor.css +2 -0
  5. package/package.json +94 -0
  6. package/src/components/DeckSlideRender.tsx +21 -0
  7. package/src/components/SaveNote.tsx +21 -0
  8. package/src/components/auth/AuthProvider.tsx +44 -0
  9. package/src/components/ui/accordion.tsx +57 -0
  10. package/src/components/ui/alert-dialog.tsx +141 -0
  11. package/src/components/ui/badge.tsx +36 -0
  12. package/src/components/ui/button.tsx +56 -0
  13. package/src/components/ui/card.tsx +76 -0
  14. package/src/components/ui/checkbox.tsx +30 -0
  15. package/src/components/ui/collapsible.tsx +11 -0
  16. package/src/components/ui/context-menu.tsx +200 -0
  17. package/src/components/ui/dialog.tsx +122 -0
  18. package/src/components/ui/dropdown-menu.tsx +201 -0
  19. package/src/components/ui/input.tsx +22 -0
  20. package/src/components/ui/label.tsx +26 -0
  21. package/src/components/ui/popover.tsx +33 -0
  22. package/src/components/ui/scroll-area.tsx +48 -0
  23. package/src/components/ui/scrubbable-label.tsx +95 -0
  24. package/src/components/ui/select.tsx +159 -0
  25. package/src/components/ui/separator.tsx +31 -0
  26. package/src/components/ui/slider.tsx +28 -0
  27. package/src/components/ui/switch.tsx +29 -0
  28. package/src/components/ui/tabs.tsx +55 -0
  29. package/src/components/ui/textarea.tsx +22 -0
  30. package/src/components/ui/tooltip.tsx +32 -0
  31. package/src/components/viewer/useDeck.ts +58 -0
  32. package/src/editor/EditorContext.tsx +632 -0
  33. package/src/editor/EditorToolbar.tsx +446 -0
  34. package/src/editor/FrontendVisualEditor.tsx +2595 -0
  35. package/src/editor/autosave.ts +34 -0
  36. package/src/editor/components/AiPromptPopover.tsx +378 -0
  37. package/src/editor/components/BreakpointGuides.tsx +323 -0
  38. package/src/editor/components/BreakpointSelector.tsx +180 -0
  39. package/src/editor/components/ComponentPropertyEditor.tsx +426 -0
  40. package/src/editor/components/CssEditorDialog.tsx +168 -0
  41. package/src/editor/components/EditorCanvas.tsx +987 -0
  42. package/src/editor/components/EditorContextMenu.tsx +515 -0
  43. package/src/editor/components/EditorFooter.tsx +81 -0
  44. package/src/editor/components/EditorHeader.tsx +524 -0
  45. package/src/editor/components/EditorLayerPanel.tsx +1197 -0
  46. package/src/editor/components/EditorPropertyPanel.tsx +2996 -0
  47. package/src/editor/components/EditorSidebar.tsx +226 -0
  48. package/src/editor/components/FigmaColorPicker.tsx +1577 -0
  49. package/src/editor/components/HtmlEditorDialog.tsx +71 -0
  50. package/src/editor/components/HtmlImportDialog.tsx +346 -0
  51. package/src/editor/components/JsEditorDialog.tsx +175 -0
  52. package/src/editor/components/LayerTreeItem.tsx +494 -0
  53. package/src/editor/components/MasterComponentEditor.tsx +846 -0
  54. package/src/editor/components/MediaLibraryDialog.tsx +447 -0
  55. package/src/editor/components/PageSettingsDialog.tsx +776 -0
  56. package/src/editor/components/VariablePicker.tsx +355 -0
  57. package/src/editor/components/VariablesDialog.tsx +564 -0
  58. package/src/editor/components/VariantEditor.tsx +687 -0
  59. package/src/editor/components/VariantMatrix.tsx +507 -0
  60. package/src/editor/components/component-panel/CategoryAccordion.tsx +139 -0
  61. package/src/editor/components/component-panel/ComponentGrid.tsx +51 -0
  62. package/src/editor/components/component-panel/ComponentItem.tsx +368 -0
  63. package/src/editor/components/component-panel/ComponentPanel.tsx +275 -0
  64. package/src/editor/components/component-panel/ComponentSearch.tsx +82 -0
  65. package/src/editor/components/component-panel/VariantList.tsx +260 -0
  66. package/src/editor/components/component-panel/index.ts +12 -0
  67. package/src/editor/components/index.ts +48 -0
  68. package/src/editor/components/multi-page/CanvasZoomControls.tsx +71 -0
  69. package/src/editor/components/multi-page/InfiniteCanvas.tsx +95 -0
  70. package/src/editor/components/multi-page/MultiPageCanvasView.tsx +1404 -0
  71. package/src/editor/components/multi-page/PageFrameOverlay.tsx +76 -0
  72. package/src/editor/components/multi-page/PageLabel.tsx +34 -0
  73. package/src/editor/components/multi-page/PageLivePreview.tsx +242 -0
  74. package/src/editor/components/multi-page/PageThumbnail.tsx +59 -0
  75. package/src/editor/components/multi-page/index.ts +7 -0
  76. package/src/editor/components/ppt/PptChrome.tsx +2508 -0
  77. package/src/editor/components/ppt/PptComments.tsx +589 -0
  78. package/src/editor/components/ppt/PptDesignProposals.tsx +275 -0
  79. package/src/editor/components/ppt/PptFormatPane.tsx +320 -0
  80. package/src/editor/components/ppt/PptNotes.tsx +246 -0
  81. package/src/editor/components/property-panel/AlignmentPanel.tsx +110 -0
  82. package/src/editor/components/property-panel/AutoLayoutPanel.tsx +615 -0
  83. package/src/editor/components/property-panel/BorderSection.tsx +104 -0
  84. package/src/editor/components/property-panel/CompactNumberInput.tsx +237 -0
  85. package/src/editor/components/property-panel/CompactSizeInput.tsx +148 -0
  86. package/src/editor/components/property-panel/EffectSection.tsx +157 -0
  87. package/src/editor/components/property-panel/FillSection.tsx +69 -0
  88. package/src/editor/components/property-panel/GoogleFontPicker.tsx +192 -0
  89. package/src/editor/components/property-panel/ImageSection.tsx +206 -0
  90. package/src/editor/components/property-panel/ImgSrcSection.tsx +125 -0
  91. package/src/editor/components/property-panel/InstanceOverrideSection.tsx +413 -0
  92. package/src/editor/components/property-panel/LayoutSection.tsx +105 -0
  93. package/src/editor/components/property-panel/LinkSection.tsx +103 -0
  94. package/src/editor/components/property-panel/PropertyVariablePicker.tsx +311 -0
  95. package/src/editor/components/property-panel/ScalePanel.tsx +212 -0
  96. package/src/editor/components/property-panel/TypographySection.tsx +230 -0
  97. package/src/editor/components/property-panel/UnitAwareNumberInput.tsx +238 -0
  98. package/src/editor/components/property-panel/VariableAwareColorInput.tsx +477 -0
  99. package/src/editor/components/property-panel/VariableAwareInput.tsx +606 -0
  100. package/src/editor/components/property-panel/VariableAwareSizeInput.tsx +588 -0
  101. package/src/editor/components/property-panel/VariableAwareUnitInput.tsx +546 -0
  102. package/src/editor/components/property-panel/index.ts +24 -0
  103. package/src/editor/components/property-panel/unit-utils.ts +261 -0
  104. package/src/editor/components/shell/EditorTopBar.tsx +392 -0
  105. package/src/editor/components/shell/LeftPanel.tsx +47 -0
  106. package/src/editor/components/shell/PagesPanel.tsx +118 -0
  107. package/src/editor/components/variables-panel/ColorValueCell.tsx +165 -0
  108. package/src/editor/components/variables-panel/TextValueCell.tsx +116 -0
  109. package/src/editor/components/variables-panel/VariablesPanel.tsx +518 -0
  110. package/src/editor/components/variables-panel/index.ts +7 -0
  111. package/src/editor/constants.ts +468 -0
  112. package/src/editor/contexts/EditorArtboardContext.tsx +229 -0
  113. package/src/editor/contexts/EditorComponentsContext.tsx +1320 -0
  114. package/src/editor/contexts/EditorDocumentContext.tsx +113 -0
  115. package/src/editor/contexts/EditorHistoryContext.tsx +516 -0
  116. package/src/editor/contexts/EditorRefsContext.tsx +204 -0
  117. package/src/editor/contexts/EditorSelectionContext.tsx +54 -0
  118. package/src/editor/contexts/EditorToolContext.tsx +58 -0
  119. package/src/editor/contexts/EditorUIStateContext.tsx +122 -0
  120. package/src/editor/contexts/EditorVariablesContext.tsx +397 -0
  121. package/src/editor/contexts/EditorViewContext.tsx +101 -0
  122. package/src/editor/contexts/MultiPageCanvasContext.tsx +498 -0
  123. package/src/editor/contexts/index.ts +17 -0
  124. package/src/editor/editor-skin.css +122 -0
  125. package/src/editor/hooks/index.ts +38 -0
  126. package/src/editor/hooks/useAiReplace.ts +397 -0
  127. package/src/editor/hooks/useAltMeasure.ts +67 -0
  128. package/src/editor/hooks/useBrowserZoomPrevention.ts +121 -0
  129. package/src/editor/hooks/useCanvasControls.ts +1222 -0
  130. package/src/editor/hooks/useComponentEditMode.ts +658 -0
  131. package/src/editor/hooks/useComponentInstances.ts +515 -0
  132. package/src/editor/hooks/useContextMenuHandler.ts +157 -0
  133. package/src/editor/hooks/useCoordinateTransform.ts +207 -0
  134. package/src/editor/hooks/useDragResize.ts +1317 -0
  135. package/src/editor/hooks/useDrawingMode.ts +472 -0
  136. package/src/editor/hooks/useEditorColors.ts +144 -0
  137. package/src/editor/hooks/useEditorMessages.ts +108 -0
  138. package/src/editor/hooks/useElementActions.ts +1754 -0
  139. package/src/editor/hooks/useElementSelection.ts +1573 -0
  140. package/src/editor/hooks/useFocusManagement.ts +249 -0
  141. package/src/editor/hooks/useGoogleFonts.ts +305 -0
  142. package/src/editor/hooks/useIframeInitializer.ts +584 -0
  143. package/src/editor/hooks/useIframeSetup.ts +360 -0
  144. package/src/editor/hooks/useImageUpload.ts +270 -0
  145. package/src/editor/hooks/useInfiniteCanvas.ts +301 -0
  146. package/src/editor/hooks/useKeyboardShortcuts.ts +205 -0
  147. package/src/editor/hooks/useMarqueeSelection.ts +388 -0
  148. package/src/editor/hooks/useMediaLibrary.ts +129 -0
  149. package/src/editor/hooks/usePageSettingsManager.ts +739 -0
  150. package/src/editor/hooks/useResizablePanel.ts +163 -0
  151. package/src/editor/hooks/useRichPaste.ts +255 -0
  152. package/src/editor/hooks/useTouchGestures.ts +197 -0
  153. package/src/editor/index.ts +27 -0
  154. package/src/editor/types/page-settings.ts +513 -0
  155. package/src/editor/types.ts +382 -0
  156. package/src/editor/utils/align-elements.ts +169 -0
  157. package/src/editor/utils/api-json.ts +21 -0
  158. package/src/editor/utils/aspect-lock.ts +50 -0
  159. package/src/editor/utils/component-renderer.ts +702 -0
  160. package/src/editor/utils/component-sync.ts +845 -0
  161. package/src/editor/utils/crop-mode.ts +265 -0
  162. package/src/editor/utils/dom-utils.ts +2238 -0
  163. package/src/editor/utils/element-effects.ts +218 -0
  164. package/src/editor/utils/eyedropper.ts +35 -0
  165. package/src/editor/utils/figma-export.ts +560 -0
  166. package/src/editor/utils/figma-kiwi-decoder.ts +722 -0
  167. package/src/editor/utils/figma-kiwi-encoder.ts +583 -0
  168. package/src/editor/utils/figma-paste.ts +2262 -0
  169. package/src/editor/utils/flex-reorder.ts +231 -0
  170. package/src/editor/utils/flex-utils.ts +1230 -0
  171. package/src/editor/utils/geometry.ts +232 -0
  172. package/src/editor/utils/grid-layout.ts +210 -0
  173. package/src/editor/utils/html-utils.ts +470 -0
  174. package/src/editor/utils/index.ts +11 -0
  175. package/src/editor/utils/ink-style.ts +57 -0
  176. package/src/editor/utils/inline-format.ts +236 -0
  177. package/src/editor/utils/measure-distance.ts +120 -0
  178. package/src/editor/utils/override-apply.ts +526 -0
  179. package/src/editor/utils/override-detection.ts +613 -0
  180. package/src/editor/utils/paste-processors.ts +609 -0
  181. package/src/editor/utils/paste-sanitizer.ts +244 -0
  182. package/src/editor/utils/restore-flow.ts +188 -0
  183. package/src/editor/utils/shape-library.ts +171 -0
  184. package/src/editor/utils/slide-root.ts +29 -0
  185. package/src/editor/utils/smart-guides.ts +218 -0
  186. package/src/editor/utils/style-utils.ts +685 -0
  187. package/src/editor/utils/table-edit.ts +151 -0
  188. package/src/editor/utils/tailwind-mappings.ts +778 -0
  189. package/src/editor/utils/tailwind-utils.ts +804 -0
  190. package/src/editor/utils/text-highlight.ts +216 -0
  191. package/src/editor/utils/theme-to-variables.ts +341 -0
  192. package/src/editor/utils/variant-resolver.ts +456 -0
  193. package/src/editor/utils/viewport-utils.ts +295 -0
  194. package/src/hooks/useEditorHistory.ts +195 -0
  195. package/src/hooks/useEditorShortcuts.ts +260 -0
  196. package/src/index.ts +40 -0
  197. package/src/io.ts +134 -0
  198. package/src/lib/agent/slide-agent/types.ts +547 -0
  199. package/src/lib/agent/website-agent/types.ts +805 -0
  200. package/src/lib/api/auth-fetch.ts +58 -0
  201. package/src/lib/deck.ts +69 -0
  202. package/src/lib/export.ts +47 -0
  203. package/src/lib/firebase/config.ts +29 -0
  204. package/src/lib/firebase/css-variables.ts +378 -0
  205. package/src/lib/firebase/editor-components.ts +677 -0
  206. package/src/lib/firebase/storage.ts +469 -0
  207. package/src/lib/utils.ts +6 -0
  208. package/src/styles/editor.css +52 -0
  209. package/src/styles/skin.css +122 -0
  210. package/src/types/css-variables.ts +645 -0
  211. package/src/types/editor-components.ts +944 -0
  212. package/src/types/editor.ts +420 -0
  213. package/src/types/page-master.ts +197 -0
  214. package/src/types/page.ts +419 -0
  215. package/src/types/slide.ts +402 -0
  216. package/src/types/website-theme.ts +788 -0
  217. package/src/types/website.ts +374 -0
  218. package/src/vendor/firebase-firestore.ts +13 -0
  219. package/src/vendor/firebase-functions.ts +17 -0
@@ -0,0 +1,469 @@
1
+ import { io } from '../../io';
2
+ /**
3
+ * ローカル永続化スタブ。オリジナルは Firebase 実装(Firebase Storage)。
4
+ *
5
+ * バックエンドが存在しないため、アップロード系の関数はファイルを転送せず
6
+ * FileReader / Base64 で作った data URL をそのまま storageUrl として返す。
7
+ * 削除系(deletePageResource / deleteFigmaComponentImages)は no-op。
8
+ *
9
+ * 定数(SUPPORTED_FILE_TYPES / MAX_FILE_SIZE / MAX_IMAGE_SIZE /
10
+ * SUPPORTED_IMAGE_TYPES / CSS_JS_SIZE_THRESHOLD)とユーティリティ
11
+ * (validateFile / formatFileSize / getFileTypeIcon / getFileTypeFromMime /
12
+ * estimateImageSize)はオリジナルのロジックをそのまま移植している。
13
+ *
14
+ * export 名・引数・戻り値型はオリジナルと同一(呼び出し側を変更しないため)。
15
+ */
16
+
17
+ /**
18
+ * [移植版] data URL を dev サーバーへ送り、public/media/uploads/ に実ファイルとして保存する。
19
+ * 失敗した場合は data URL をそのまま返す(オフライン/ビルド時のフォールバック)。
20
+ */
21
+ async function persistDataUrl(dataUrl: string, fileName: string): Promise<string> {
22
+ try {
23
+ const res = await fetch('/__upload', {
24
+ method: 'POST',
25
+ headers: { 'Content-Type': 'application/json' },
26
+ body: JSON.stringify({ name: fileName, dataUrl }),
27
+ });
28
+ if (!res.ok) return dataUrl;
29
+ const data = (await res.json()) as { url?: string };
30
+ if (!data.url) return dataUrl;
31
+ // エディタのiframeは blob: なので、ルート相対パス(/media/...)を解決できず
32
+ // アップロード直後の画像がリンク切れに見える(右パネルは親文書なので表示される)。
33
+ // 絶対URLで返す。保存時に slide-html.ts の fromEditorUrls が相対へ戻すので、
34
+ // 永続化されるHTMLに localhost は残らない。
35
+ if (data.url.startsWith('/') && typeof window !== 'undefined') {
36
+ return new URL(data.url, window.location.origin).href;
37
+ }
38
+ return data.url;
39
+ } catch {
40
+ return dataUrl;
41
+ }
42
+ }
43
+
44
+ import type { PageResourceUpload } from '../../types/page';
45
+
46
+ // ============================================
47
+ // 内部ヘルパー
48
+ // ============================================
49
+
50
+ /**
51
+ * Blob / File を data URL(base64)に変換
52
+ */
53
+ function blobToDataUrl(blob: Blob): Promise<string> {
54
+ return new Promise((resolve, reject) => {
55
+ if (typeof FileReader === 'undefined') {
56
+ reject(new Error('FileReader が利用できない環境です。'));
57
+ return;
58
+ }
59
+
60
+ const reader = new FileReader();
61
+ reader.onload = () => {
62
+ if (typeof reader.result === 'string') {
63
+ resolve(reader.result);
64
+ } else {
65
+ reject(new Error('ファイルの読み込みに失敗しました。'));
66
+ }
67
+ };
68
+ reader.onerror = () => reject(new Error('ファイルの読み込みに失敗しました。'));
69
+ reader.readAsDataURL(blob);
70
+ });
71
+ }
72
+
73
+ /**
74
+ * Base64 文字列を Blob に変換
75
+ */
76
+ function base64ToBlob(base64Data: string, mimeType: string): Blob {
77
+ const binaryString = atob(base64Data);
78
+ const bytes = new Uint8Array(binaryString.length);
79
+ for (let i = 0; i < binaryString.length; i++) {
80
+ bytes[i] = binaryString.charCodeAt(i);
81
+ }
82
+ return new Blob([bytes], { type: mimeType });
83
+ }
84
+
85
+ // サポートされているファイルタイプ
86
+ export const SUPPORTED_FILE_TYPES = {
87
+ 'application/pdf': 'pdf',
88
+ 'text/csv': 'csv',
89
+ 'text/plain': 'txt',
90
+ 'text/markdown': 'md',
91
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'pptx',
92
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx',
93
+ } as const;
94
+
95
+ export const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
96
+
97
+ // ファイルバリデーション
98
+ export function validateFile(file: File): { isValid: boolean; error?: string } {
99
+ // ファイルサイズチェック
100
+ if (file.size > MAX_FILE_SIZE) {
101
+ return {
102
+ isValid: false,
103
+ error: `ファイルサイズが大きすぎます。${MAX_FILE_SIZE / (1024 * 1024)}MB以下にしてください。`
104
+ };
105
+ }
106
+
107
+ // ファイルタイプチェック
108
+ const allowedTypes = Object.keys(SUPPORTED_FILE_TYPES);
109
+ if (!allowedTypes.includes(file.type)) {
110
+ return {
111
+ isValid: false,
112
+ error: 'サポートされていないファイル形式です。PDF、CSV、TXT、MD、PPTX、XLSXファイルのみアップロード可能です。'
113
+ };
114
+ }
115
+
116
+ return { isValid: true };
117
+ }
118
+
119
+ // ファイルアップロード(アップロードせず data URL を返す)
120
+ export async function uploadPageResource(
121
+ projectId: string,
122
+ pageId: string,
123
+ file: File,
124
+ uploadData: Omit<PageResourceUpload, 'file'>
125
+ ): Promise<{ storageUrl: string; storagePath: string }> {
126
+ try {
127
+ void uploadData; // オリジナルは customMetadata に使用(ローカルでは保持しない)
128
+
129
+ // ファイルバリデーション
130
+ const validation = validateFile(file);
131
+ if (!validation.isValid) {
132
+ throw new Error(validation.error);
133
+ }
134
+
135
+ // ファイル名を生成(重複を避けるためにタイムスタンプを追加)
136
+ const timestamp = Date.now();
137
+ const sanitizedFileName = file.name.replace(/[^a-zA-Z0-9.-]/g, '_');
138
+ const fileName = `${timestamp}_${sanitizedFileName}`;
139
+
140
+ // Storage path 相当の識別子を生成(互換のため同じ形式を維持)
141
+ const storagePath = `projects/${projectId}/pages/${pageId}/resources/${fileName}`;
142
+
143
+ // data URL 化した上で dev サーバーへ送り、実ファイルとして保存する
144
+ const dataUrl = await blobToDataUrl(file);
145
+ const storageUrl = await persistDataUrl(dataUrl, fileName);
146
+
147
+ return {
148
+ storageUrl,
149
+ storagePath
150
+ };
151
+ } catch (error) {
152
+ console.error('File upload error:', error);
153
+
154
+ if (error instanceof Error) {
155
+ throw error;
156
+ }
157
+
158
+ throw new Error('ファイルのアップロードに失敗しました。');
159
+ }
160
+ }
161
+
162
+ // ファイル削除(ローカルではリモート実体が無いため no-op)
163
+ export async function deletePageResource(storagePath: string): Promise<void> {
164
+ console.log('[storage] deletePageResource is a no-op in local mode:', storagePath);
165
+ }
166
+
167
+ // ファイル情報取得(ローカルではメタデータを保持しないためスタブを返す)
168
+ export async function getFileMetadata(storagePath: string) {
169
+ const now = new Date().toISOString();
170
+ return {
171
+ name: storagePath.split('/').pop() ?? storagePath,
172
+ fullPath: storagePath,
173
+ bucket: 'local',
174
+ size: 0,
175
+ contentType: undefined as string | undefined,
176
+ timeCreated: now,
177
+ updated: now,
178
+ customMetadata: {} as Record<string, string>,
179
+ };
180
+ }
181
+
182
+ // ファイルサイズのフォーマット
183
+ export function formatFileSize(bytes: number): string {
184
+ if (bytes === 0) return '0 Bytes';
185
+
186
+ const k = 1024;
187
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
188
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
189
+
190
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
191
+ }
192
+
193
+ // ファイルタイプからアイコンを取得
194
+ export function getFileTypeIcon(fileType: string): string {
195
+ switch (fileType) {
196
+ case 'pdf':
197
+ return '📄';
198
+ case 'csv':
199
+ return '📊';
200
+ case 'txt':
201
+ case 'md':
202
+ return '📝';
203
+ case 'pptx':
204
+ return '📺';
205
+ case 'xlsx':
206
+ return '📋';
207
+ default:
208
+ return '📎';
209
+ }
210
+ }
211
+
212
+ // MIMEタイプからファイルタイプを取得
213
+ export function getFileTypeFromMime(mimeType: string): string {
214
+ return SUPPORTED_FILE_TYPES[mimeType as keyof typeof SUPPORTED_FILE_TYPES] || 'unknown';
215
+ }
216
+
217
+ // Figmaプラグインからの画像データ(アップロードせず data URL を返す)
218
+ export async function uploadFigmaImage(
219
+ componentId: string,
220
+ imageData: {
221
+ base64: string;
222
+ format: 'PNG' | 'JPG' | 'SVG';
223
+ scale: number;
224
+ capturedAt: string;
225
+ },
226
+ metadata: {
227
+ figmaFileId: string;
228
+ figmaNodeId: string;
229
+ layerName: string;
230
+ createdBy: string;
231
+ }
232
+ ): Promise<{ storageUrl: string; storagePath: string }> {
233
+ try {
234
+ const mimeType = `image/${imageData.format.toLowerCase()}`;
235
+
236
+ // ファイル名を生成
237
+ const timestamp = Date.now();
238
+ const sanitizedLayerName = metadata.layerName.replace(/[^a-zA-Z0-9.-]/g, '_');
239
+ const fileName = `${timestamp}_${sanitizedLayerName}_scale${imageData.scale}x.${imageData.format.toLowerCase()}`;
240
+
241
+ // Storage path 相当の識別子を生成(互換のため同じ形式を維持)
242
+ const storagePath = `components/${componentId}/figma-images/${fileName}`;
243
+
244
+ // アップロードの代わりに data URL を組み立てる
245
+ const storageUrl = imageData.base64.startsWith('data:')
246
+ ? imageData.base64
247
+ : `data:${mimeType};base64,${imageData.base64}`;
248
+
249
+ return {
250
+ storageUrl,
251
+ storagePath
252
+ };
253
+ } catch (error) {
254
+ console.error('❌ Figma image upload error:', error);
255
+
256
+ if (error instanceof Error) {
257
+ throw error;
258
+ }
259
+
260
+ throw new Error('Figma画像のアップロードに失敗しました。');
261
+ }
262
+ }
263
+
264
+ // コンポーネントの画像ファイルを削除(ローカルでは no-op)
265
+ export async function deleteFigmaComponentImages(componentId: string): Promise<void> {
266
+ const basePath = `components/${componentId}/figma-images/`;
267
+ console.log(`[storage] deleteFigmaComponentImages is a no-op in local mode: ${basePath}`);
268
+ }
269
+
270
+ // 画像ファイルサイズの推定(Base64から)
271
+ export function estimateImageSize(base64: string): number {
272
+ // Base64の実際のバイト数を推定(padding考慮)
273
+ const padding = base64.endsWith('==') ? 2 : base64.endsWith('=') ? 1 : 0;
274
+ return Math.round((base64.length * 0.75) - padding);
275
+ }
276
+
277
+ // 画像の最大サイズ(エディタ用)
278
+ export const MAX_IMAGE_SIZE = 5 * 1024 * 1024; // 5MB
279
+
280
+ // サポートされている画像タイプ
281
+ export const SUPPORTED_IMAGE_TYPES = [
282
+ 'image/jpeg',
283
+ 'image/png',
284
+ 'image/gif',
285
+ 'image/webp',
286
+ 'image/svg+xml',
287
+ ];
288
+
289
+ /**
290
+ * エディタ用の画像アップロード
291
+ * アップロードは行わず、data URL をそのまま storageUrl として返す
292
+ */
293
+ export async function uploadEditorImage(
294
+ file: File | Blob,
295
+ options: {
296
+ presentationId?: string;
297
+ slideId?: string;
298
+ fileName?: string;
299
+ } = {}
300
+ ): Promise<{ storageUrl: string; storagePath: string; width?: number; height?: number }> {
301
+ // [パッケージ化での変更] 利用側が保存先を持っているならそれを使う。
302
+ // 無ければ従来どおり data URL のまま埋め込む(バックエンド不要で動く)。
303
+ const provided = io().uploadImage;
304
+ if (provided) {
305
+ const r = await provided(file, { fileName: options.fileName });
306
+ return { storageUrl: r.url, storagePath: r.url, width: r.width, height: r.height };
307
+ }
308
+
309
+ try {
310
+ // ファイルサイズチェック
311
+ if (file.size > MAX_IMAGE_SIZE) {
312
+ throw new Error(`画像サイズが大きすぎます。${MAX_IMAGE_SIZE / (1024 * 1024)}MB以下にしてください。`);
313
+ }
314
+
315
+ // ファイルタイプチェック
316
+ if (!SUPPORTED_IMAGE_TYPES.includes(file.type)) {
317
+ throw new Error('サポートされていない画像形式です。JPEG、PNG、GIF、WebP、SVGがサポートされています。');
318
+ }
319
+
320
+ // ファイル名を生成
321
+ const timestamp = Date.now();
322
+ const extension = file.type.split('/')[1].replace('svg+xml', 'svg');
323
+ const fileName = options.fileName
324
+ ? `${timestamp}_${options.fileName.replace(/[^a-zA-Z0-9.-]/g, '_')}`
325
+ : `${timestamp}_image.${extension}`;
326
+
327
+ // Storage path 相当の識別子を生成(互換のため同じ形式を維持)
328
+ let storagePath: string;
329
+ if (options.presentationId && options.slideId) {
330
+ storagePath = `presentations/${options.presentationId}/slides/${options.slideId}/images/${fileName}`;
331
+ } else if (options.presentationId) {
332
+ storagePath = `presentations/${options.presentationId}/images/${fileName}`;
333
+ } else {
334
+ storagePath = `editor/images/${fileName}`;
335
+ }
336
+
337
+ // data URL 化した上で dev サーバーへ送り、実ファイルとして保存する
338
+ const dataUrl = await blobToDataUrl(file);
339
+ const storageUrl = await persistDataUrl(dataUrl, fileName);
340
+
341
+ // 画像サイズを取得(ブラウザ環境の場合)
342
+ let width: number | undefined;
343
+ let height: number | undefined;
344
+
345
+ if (typeof window !== 'undefined' && file.type.startsWith('image/') && file.type !== 'image/svg+xml') {
346
+ try {
347
+ const dimensions = await getImageDimensions(file);
348
+ width = dimensions.width;
349
+ height = dimensions.height;
350
+ } catch (e) {
351
+ console.warn('画像サイズの取得に失敗:', e);
352
+ }
353
+ }
354
+
355
+ return {
356
+ storageUrl,
357
+ storagePath,
358
+ width,
359
+ height,
360
+ };
361
+ } catch (error) {
362
+ console.error('Editor image upload error:', error);
363
+
364
+ if (error instanceof Error) {
365
+ throw error;
366
+ }
367
+
368
+ throw new Error('画像のアップロードに失敗しました。');
369
+ }
370
+ }
371
+
372
+ /**
373
+ * 画像のサイズを取得
374
+ */
375
+ function getImageDimensions(file: File | Blob): Promise<{ width: number; height: number }> {
376
+ return new Promise((resolve, reject) => {
377
+ const url = URL.createObjectURL(file);
378
+ const img = new Image();
379
+
380
+ img.onload = () => {
381
+ URL.revokeObjectURL(url);
382
+ resolve({ width: img.width, height: img.height });
383
+ };
384
+
385
+ img.onerror = () => {
386
+ URL.revokeObjectURL(url);
387
+ reject(new Error('画像の読み込みに失敗しました'));
388
+ };
389
+
390
+ img.src = url;
391
+ });
392
+ }
393
+
394
+ /**
395
+ * Base64データから画像をアップロード
396
+ */
397
+ export async function uploadEditorImageFromBase64(
398
+ base64Data: string,
399
+ mimeType: string,
400
+ options: {
401
+ presentationId?: string;
402
+ slideId?: string;
403
+ fileName?: string;
404
+ } = {}
405
+ ): Promise<{ storageUrl: string; storagePath: string; width?: number; height?: number }> {
406
+ // Base64データをBlobに変換
407
+ const blob = base64ToBlob(base64Data, mimeType);
408
+
409
+ return uploadEditorImage(blob, options);
410
+ }
411
+
412
+ /**
413
+ * DataURLから画像をアップロード
414
+ */
415
+ export async function uploadEditorImageFromDataUrl(
416
+ dataUrl: string,
417
+ options: {
418
+ presentationId?: string;
419
+ slideId?: string;
420
+ fileName?: string;
421
+ } = {}
422
+ ): Promise<{ storageUrl: string; storagePath: string; width?: number; height?: number }> {
423
+ // DataURLをパース
424
+ const matches = dataUrl.match(/^data:([^;]+);base64,(.+)$/);
425
+ if (!matches) {
426
+ throw new Error('無効なDataURL形式です。');
427
+ }
428
+
429
+ const mimeType = matches[1];
430
+ const base64Data = matches[2];
431
+
432
+ return uploadEditorImageFromBase64(base64Data, mimeType, options);
433
+ }
434
+
435
+ // ===== CSS/JS Storage Functions =====
436
+
437
+ /**
438
+ * 大きなCSS/JSはFirestoreに直接保存できないため、オリジナルではStorageに保存していた
439
+ */
440
+ export const CSS_JS_SIZE_THRESHOLD = 100 * 1024; // 100KB以上はStorageに保存
441
+
442
+ export interface CssJsStorageResult {
443
+ /** Storage URL (Storageに保存した場合) */
444
+ storageUrl?: string;
445
+ /** Storage path (Storageに保存した場合) */
446
+ storagePath?: string;
447
+ /** 直接保存する場合のコンテンツ (小さい場合) */
448
+ content?: string;
449
+ /** コンテンツサイズ */
450
+ size: number;
451
+ /** Storageに保存されたかどうか */
452
+ isStoredInStorage: boolean;
453
+ }
454
+
455
+ /**
456
+ * URL(data URL含む)からCSS/JSコンテンツを取得
457
+ */
458
+ export async function fetchCssJsContent(storageUrl: string): Promise<string> {
459
+ try {
460
+ const response = await fetch(storageUrl);
461
+ if (!response.ok) {
462
+ throw new Error(`Failed to fetch content: ${response.status}`);
463
+ }
464
+ return await response.text();
465
+ } catch (error) {
466
+ console.error('[fetchCssJsContent] Failed to fetch:', error);
467
+ throw error;
468
+ }
469
+ }
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * エディタUIのスタイル。**このファイルをビルドした dist/editor.css を配る。**
3
+ *
4
+ * 【なぜビルド済みで配るのか】
5
+ * エディタのUIは Tailwind のクラスで書かれている。ソースのまま配ると、
6
+ * 利用側の Tailwind がこのパッケージを走査してコンパイルする必要があり、
7
+ * 利用側のバージョン(v3 / v4)に依存してしまう。
8
+ * ここで完結してCSSにしてしまえば、利用側のTailwindは何でもよい。
9
+ *
10
+ * 【preflight(リセット)を入れない】
11
+ * 利用側にも Tailwind のリセットが入っている。二重に当てると
12
+ * 利用側のページの見た目が変わる。theme と utilities だけを取り込む。
13
+ */
14
+ @import "tailwindcss/theme.css";
15
+ @import "tailwindcss/utilities.css";
16
+
17
+ /* このパッケージのソースだけを走査する */
18
+ @source "../**/*.{ts,tsx}";
19
+
20
+ /* エディタUIが参照するトークン(shadcn/ui 由来)。
21
+ 利用側の設定に依存しないよう、ここで閉じる */
22
+ @theme {
23
+ --color-background: #ffffff;
24
+ --color-foreground: #0a0a0a;
25
+ --color-card: #ffffff;
26
+ --color-card-foreground: #0a0a0a;
27
+ --color-popover: #ffffff;
28
+ --color-popover-foreground: #0a0a0a;
29
+ --color-primary: #171717;
30
+ --color-primary-foreground: #fafafa;
31
+ --color-secondary: #f5f5f5;
32
+ --color-secondary-foreground: #171717;
33
+ --color-muted: #f5f5f5;
34
+ --color-muted-foreground: #737373;
35
+ --color-accent: #f5f5f5;
36
+ --color-accent-foreground: #171717;
37
+ --color-destructive: #ef4444;
38
+ --color-destructive-foreground: #fafafa;
39
+ --color-border: #e5e5e5;
40
+ --color-input: #e5e5e5;
41
+ --color-ring: #0a0a0a;
42
+ --color-gg-green: #316a40;
43
+ --color-ink: #2e2e2e;
44
+ --color-mark-green: #cfe6d6;
45
+ --radius: 0.5rem;
46
+ }
47
+
48
+ /* エディタの配色スキン(.gg-editor-skin 配下でだけ効く) */
49
+ @import "./skin.css";
50
+
51
+ /* アイコン挿入(Material Symbols)。読み込めないと豆腐になる */
52
+ @import url("https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0&display=block");
@@ -0,0 +1,122 @@
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
+ }