@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,685 @@
1
+ /**
2
+ * スタイル関連のユーティリティ関数
3
+ */
4
+
5
+ import type { SelectedElementInfo, ImageFillMode } from '../types';
6
+ import { getArtboardScale } from './dom-utils';
7
+ import { extractValueFromTailwindClass } from './tailwind-utils';
8
+
9
+ /**
10
+ * RGB/RGBA色をHEXに変換
11
+ */
12
+ export function rgbToHex(rgb: string): string {
13
+ if (!rgb || rgb === 'transparent' || rgb === 'none') return '#000000';
14
+ if (rgb.startsWith('#')) return rgb;
15
+
16
+ const match = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
17
+ if (match) {
18
+ const r = parseInt(match[1]).toString(16).padStart(2, '0');
19
+ const g = parseInt(match[2]).toString(16).padStart(2, '0');
20
+ const b = parseInt(match[3]).toString(16).padStart(2, '0');
21
+ return `#${r}${g}${b}`;
22
+ }
23
+ return '#000000';
24
+ }
25
+
26
+ /**
27
+ * ボックスシャドウをパース
28
+ */
29
+ export function parseBoxShadow(boxShadow: string): {
30
+ hasShadow: boolean;
31
+ shadowX: number;
32
+ shadowY: number;
33
+ shadowBlur: number;
34
+ shadowSpread: number;
35
+ shadowColor: string;
36
+ } {
37
+ if (!boxShadow || boxShadow === 'none') {
38
+ return {
39
+ hasShadow: false,
40
+ shadowX: 0,
41
+ shadowY: 0,
42
+ shadowBlur: 0,
43
+ shadowSpread: 0,
44
+ shadowColor: 'rgba(0,0,0,0.25)',
45
+ };
46
+ }
47
+
48
+ let shadowX = 0, shadowY = 0, shadowBlur = 0, shadowSpread = 0;
49
+ let shadowColor = 'rgba(0,0,0,0.25)';
50
+
51
+ const match = boxShadow.match(/rgba?\([^)]+\)\s*([-\d.]+)px\s*([-\d.]+)px\s*([-\d.]+)px\s*([-\d.]+)?px?/);
52
+ if (match) {
53
+ shadowX = parseFloat(match[1]) || 0;
54
+ shadowY = parseFloat(match[2]) || 0;
55
+ shadowBlur = parseFloat(match[3]) || 0;
56
+ shadowSpread = parseFloat(match[4]) || 0;
57
+ }
58
+
59
+ const colorMatch = boxShadow.match(/rgba?\([^)]+\)/);
60
+ if (colorMatch) {
61
+ shadowColor = colorMatch[0];
62
+ }
63
+
64
+ return {
65
+ hasShadow: true,
66
+ shadowX,
67
+ shadowY,
68
+ shadowBlur,
69
+ shadowSpread,
70
+ shadowColor,
71
+ };
72
+ }
73
+
74
+ /**
75
+ * 画像フィルモードをパース
76
+ */
77
+ export function parseImageFillMode(
78
+ backgroundSize: string,
79
+ backgroundRepeat: string
80
+ ): ImageFillMode {
81
+ if (backgroundRepeat === 'repeat') return 'tile';
82
+ if (backgroundSize === 'contain') return 'fit';
83
+ if (backgroundSize === 'cover') return 'crop';
84
+ if (backgroundSize === '100% 100%') return 'fill';
85
+ return 'crop';
86
+ }
87
+
88
+ /**
89
+ * 背景位置のX座標をパース
90
+ */
91
+ export function parseBackgroundPositionX(backgroundPosition: string): number {
92
+ if (!backgroundPosition || backgroundPosition === 'center') return 50;
93
+ const parts = backgroundPosition.split(' ');
94
+ const xPart = parts[0];
95
+ if (xPart.endsWith('%')) return parseFloat(xPart) || 50;
96
+ if (xPart === 'left') return 0;
97
+ if (xPart === 'right') return 100;
98
+ if (xPart === 'center') return 50;
99
+ return 50;
100
+ }
101
+
102
+ /**
103
+ * 背景位置のY座標をパース
104
+ */
105
+ export function parseBackgroundPositionY(backgroundPosition: string): number {
106
+ if (!backgroundPosition || backgroundPosition === 'center') return 50;
107
+ const parts = backgroundPosition.split(' ');
108
+ const yPart = parts[1] || parts[0];
109
+ if (yPart.endsWith('%')) return parseFloat(yPart) || 50;
110
+ if (yPart === 'top') return 0;
111
+ if (yPart === 'bottom') return 100;
112
+ if (yPart === 'center') return 50;
113
+ return 50;
114
+ }
115
+
116
+ /**
117
+ * 背景スケールをパース
118
+ */
119
+ export function parseBackgroundScale(backgroundSize: string): number {
120
+ if (!backgroundSize || backgroundSize === 'cover' || backgroundSize === 'contain') return 100;
121
+ if (backgroundSize === '100% 100%') return 100;
122
+ const match = backgroundSize.match(/(\d+)%/);
123
+ if (match) return parseFloat(match[1]) || 100;
124
+ return 100;
125
+ }
126
+
127
+ /**
128
+ * filterプロパティから各フィルターの値を抽出
129
+ */
130
+ export function parseFilter(filter: string): {
131
+ blur: number;
132
+ brightness: number;
133
+ contrast: number;
134
+ grayscale: number;
135
+ saturate: number;
136
+ sepia: number;
137
+ hueRotate: number;
138
+ invert: number;
139
+ } {
140
+ const result = {
141
+ blur: 0,
142
+ brightness: 100,
143
+ contrast: 100,
144
+ grayscale: 0,
145
+ saturate: 100,
146
+ sepia: 0,
147
+ hueRotate: 0,
148
+ invert: 0,
149
+ };
150
+
151
+ if (!filter || filter === 'none') return result;
152
+
153
+ const safeParse = (str: string, defaultValue: number) => {
154
+ const val = parseFloat(str);
155
+ return isNaN(val) ? defaultValue : val;
156
+ };
157
+
158
+ // blur(Xpx)
159
+ const blurMatch = filter.match(/blur\((\d+(?:\.\d+)?)px\)/);
160
+ if (blurMatch) result.blur = safeParse(blurMatch[1], 0);
161
+
162
+ // brightness(X) or brightness(X%)
163
+ const brightnessMatch = filter.match(/brightness\((\d+(?:\.\d+)?)(%?)\)/);
164
+ if (brightnessMatch) {
165
+ const val = safeParse(brightnessMatch[1], 100);
166
+ result.brightness = brightnessMatch[2] === '%' ? val : val * 100;
167
+ }
168
+
169
+ // contrast(X) or contrast(X%)
170
+ const contrastMatch = filter.match(/contrast\((\d+(?:\.\d+)?)(%?)\)/);
171
+ if (contrastMatch) {
172
+ const val = safeParse(contrastMatch[1], 100);
173
+ result.contrast = contrastMatch[2] === '%' ? val : val * 100;
174
+ }
175
+
176
+ // grayscale(X) or grayscale(X%)
177
+ const grayscaleMatch = filter.match(/grayscale\((\d+(?:\.\d+)?)(%?)\)/);
178
+ if (grayscaleMatch) {
179
+ const val = safeParse(grayscaleMatch[1], 0);
180
+ result.grayscale = grayscaleMatch[2] === '%' ? val : val * 100;
181
+ }
182
+
183
+ // saturate(X) or saturate(X%)
184
+ const saturateMatch = filter.match(/saturate\((\d+(?:\.\d+)?)(%?)\)/);
185
+ if (saturateMatch) {
186
+ const val = safeParse(saturateMatch[1], 100);
187
+ result.saturate = saturateMatch[2] === '%' ? val : val * 100;
188
+ }
189
+
190
+ // sepia(X) or sepia(X%)
191
+ const sepiaMatch = filter.match(/sepia\((\d+(?:\.\d+)?)(%?)\)/);
192
+ if (sepiaMatch) {
193
+ const val = safeParse(sepiaMatch[1], 0);
194
+ result.sepia = sepiaMatch[2] === '%' ? val : val * 100;
195
+ }
196
+
197
+ // hue-rotate(Xdeg)
198
+ const hueRotateMatch = filter.match(/hue-rotate\((\d+(?:\.\d+)?)deg\)/);
199
+ if (hueRotateMatch) result.hueRotate = safeParse(hueRotateMatch[1], 0);
200
+
201
+ // invert(X) or invert(X%)
202
+ const invertMatch = filter.match(/invert\((\d+(?:\.\d+)?)(%?)\)/);
203
+ if (invertMatch) {
204
+ const val = safeParse(invertMatch[1], 0);
205
+ result.invert = invertMatch[2] === '%' ? val : val * 100;
206
+ }
207
+
208
+ return result;
209
+ }
210
+
211
+ /**
212
+ * backdrop-filterプロパティからblur値を抽出
213
+ */
214
+ export function parseBackdropFilter(backdropFilter: string): {
215
+ blur: number;
216
+ } {
217
+ const result = { blur: 0 };
218
+ if (!backdropFilter || backdropFilter === 'none') return result;
219
+
220
+ const blurMatch = backdropFilter.match(/blur\((\d+(?:\.\d+)?)px\)/);
221
+ if (blurMatch) result.blur = parseFloat(blurMatch[1]) || 0;
222
+
223
+ return result;
224
+ }
225
+
226
+ /**
227
+ * transformプロパティから回転とスケールを抽出
228
+ * ブラウザがmatrix形式で返す場合も対応
229
+ */
230
+ export function parseTransform(transform: string): {
231
+ rotation: number;
232
+ scaleX: number;
233
+ scaleY: number;
234
+ } {
235
+ const result = { rotation: 0, scaleX: 1, scaleY: 1 };
236
+ if (!transform || transform === 'none') return result;
237
+
238
+ // scale(X, Y) - まとめて指定(matrixより先にチェック)
239
+ const scaleMatch = transform.match(/scale\(\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\)/);
240
+ if (scaleMatch) {
241
+ result.scaleX = parseFloat(scaleMatch[1]);
242
+ result.scaleY = parseFloat(scaleMatch[2]);
243
+ }
244
+
245
+ // rotate(Xdeg)
246
+ const rotateMatch = transform.match(/rotate\(([-\d.]+)deg\)/);
247
+ if (rotateMatch) result.rotation = parseFloat(rotateMatch[1]) || 0;
248
+
249
+ // scaleX(X) - 単独
250
+ const scaleXMatch = transform.match(/scaleX\(([-\d.]+)\)/);
251
+ if (scaleXMatch) result.scaleX = parseFloat(scaleXMatch[1]);
252
+
253
+ // scaleY(X) - 単独
254
+ const scaleYMatch = transform.match(/scaleY\(([-\d.]+)\)/);
255
+ if (scaleYMatch) result.scaleY = parseFloat(scaleYMatch[1]);
256
+
257
+ // 上記でマッチしなかった場合のみmatrix形式をパース
258
+ if (!scaleMatch && !scaleXMatch && !scaleYMatch && !rotateMatch) {
259
+ // matrix(a, b, c, d, tx, ty) 形式をパース
260
+ const matrixMatch = transform.match(/matrix\(\s*([-\d.e]+)\s*,\s*([-\d.e]+)\s*,\s*([-\d.e]+)\s*,\s*([-\d.e]+)\s*,\s*([-\d.e]+)\s*,\s*([-\d.e]+)\s*\)/);
261
+ if (matrixMatch) {
262
+ const a = parseFloat(matrixMatch[1]);
263
+ const b = parseFloat(matrixMatch[2]);
264
+ const c = parseFloat(matrixMatch[3]);
265
+ const d = parseFloat(matrixMatch[4]);
266
+
267
+ // 反転のみのケース(回転なし): b === 0 && c === 0
268
+ if (Math.abs(b) < 0.001 && Math.abs(c) < 0.001) {
269
+ result.scaleX = a < 0 ? -1 : 1;
270
+ result.scaleY = d < 0 ? -1 : 1;
271
+ result.rotation = 0;
272
+ } else {
273
+ // 回転がある場合
274
+ const angle = Math.atan2(b, a);
275
+ result.rotation = Math.round(angle * (180 / Math.PI));
276
+
277
+ // スケールを計算
278
+ const det = a * d - b * c;
279
+ result.scaleX = det < 0 ? -1 : 1;
280
+ result.scaleY = 1;
281
+ }
282
+ }
283
+ }
284
+
285
+ return result;
286
+ }
287
+
288
+ /**
289
+ * paddingをパース(個別の値を抽出)
290
+ */
291
+ export function parsePadding(style: CSSStyleDeclaration): {
292
+ top: number;
293
+ right: number;
294
+ bottom: number;
295
+ left: number;
296
+ } {
297
+ return {
298
+ top: parseFloat(style.paddingTop) || 0,
299
+ right: parseFloat(style.paddingRight) || 0,
300
+ bottom: parseFloat(style.paddingBottom) || 0,
301
+ left: parseFloat(style.paddingLeft) || 0,
302
+ };
303
+ }
304
+
305
+ /**
306
+ * transformプロパティ文字列を生成
307
+ * scaleは明示的に1に戻す場合も含めて常に出力
308
+ */
309
+ export function buildTransformString(options: {
310
+ rotation?: number;
311
+ scaleX?: number;
312
+ scaleY?: number;
313
+ }): string {
314
+ const parts: string[] = [];
315
+ const scaleX = options.scaleX ?? 1;
316
+ const scaleY = options.scaleY ?? 1;
317
+
318
+ // 回転を先に適用
319
+ if (options.rotation !== undefined && options.rotation !== 0) {
320
+ parts.push(`rotate(${options.rotation}deg)`);
321
+ }
322
+
323
+ // スケールを常に適用(1に戻す場合も明示的に)
324
+ // これにより反転の解除が確実に動作する
325
+ parts.push(`scale(${scaleX}, ${scaleY})`);
326
+
327
+ return parts.join(' ');
328
+ }
329
+
330
+ /**
331
+ * フィルター値からfilterプロパティ文字列を生成
332
+ */
333
+ export function buildFilterString(filters: {
334
+ blur?: number;
335
+ brightness?: number;
336
+ contrast?: number;
337
+ grayscale?: number;
338
+ saturate?: number;
339
+ sepia?: number;
340
+ hueRotate?: number;
341
+ invert?: number;
342
+ }): string {
343
+ const parts: string[] = [];
344
+
345
+ if (filters.blur && filters.blur > 0) parts.push(`blur(${filters.blur}px)`);
346
+ if (filters.brightness !== undefined && filters.brightness !== 100) parts.push(`brightness(${filters.brightness}%)`);
347
+ if (filters.contrast !== undefined && filters.contrast !== 100) parts.push(`contrast(${filters.contrast}%)`);
348
+ if (filters.grayscale && filters.grayscale > 0) parts.push(`grayscale(${filters.grayscale}%)`);
349
+ if (filters.saturate !== undefined && filters.saturate !== 100) parts.push(`saturate(${filters.saturate}%)`);
350
+ if (filters.sepia && filters.sepia > 0) parts.push(`sepia(${filters.sepia}%)`);
351
+ if (filters.hueRotate && filters.hueRotate !== 0) parts.push(`hue-rotate(${filters.hueRotate}deg)`);
352
+ if (filters.invert && filters.invert > 0) parts.push(`invert(${filters.invert}%)`);
353
+
354
+ return parts.length > 0 ? parts.join(' ') : 'none';
355
+ }
356
+
357
+ /**
358
+ * 要素から選択要素情報を抽出
359
+ */
360
+ export function extractElementInfo(
361
+ element: HTMLElement,
362
+ iframeDoc: Document
363
+ ): SelectedElementInfo | null {
364
+ const style = iframeDoc.defaultView?.getComputedStyle(element);
365
+ if (!style) return null;
366
+
367
+ const rect = element.getBoundingClientRect();
368
+ // ズームスケールを取得(getBoundingClientRect()はスケール適用後の値を返すため)
369
+ const scale = getArtboardScale(iframeDoc);
370
+ const boxShadow = style.boxShadow;
371
+ const shadowInfo = parseBoxShadow(boxShadow);
372
+
373
+ // フィルターの抽出
374
+ const filterInfo = parseFilter(style.filter);
375
+ const backdropFilterInfo = parseBackdropFilter(style.backdropFilter);
376
+
377
+ // transformの抽出(インラインスタイルを優先、なければ計算済みスタイル)
378
+ // ブラウザはscale(1,1)をnoneに正規化することがあるため、インラインスタイルを先にチェック
379
+ const inlineTransform = element.style.transform;
380
+ const transformInfo = parseTransform(inlineTransform || style.transform);
381
+
382
+ // paddingの抽出
383
+ const paddingInfo = parsePadding(style);
384
+ const paddingLinked = paddingInfo.top === paddingInfo.right &&
385
+ paddingInfo.right === paddingInfo.bottom &&
386
+ paddingInfo.bottom === paddingInfo.left;
387
+
388
+ // width/heightがautoかどうかを判定(Tailwindクラスとインラインスタイルをチェック)
389
+ const inlineWidth = element.style.width;
390
+ const inlineHeight = element.style.height;
391
+ const tailwindWidth = extractValueFromTailwindClass(element.className, 'width');
392
+ const tailwindHeight = extractValueFromTailwindClass(element.className, 'height');
393
+ // Tailwindクラスまたはインラインスタイルで明示的に値が設定されていればautoではない
394
+ const widthAuto = !tailwindWidth && (inlineWidth === 'auto' || inlineWidth === '');
395
+ const heightAuto = !tailwindHeight && (inlineHeight === 'auto' || inlineHeight === '');
396
+
397
+ // 親要素情報の取得
398
+ let parentDisplay = 'block';
399
+ let parentFlexDirection = 'row';
400
+ if (element.parentElement) {
401
+ const parentStyle = iframeDoc.defaultView?.getComputedStyle(element.parentElement);
402
+ if (parentStyle) {
403
+ parentDisplay = parentStyle.display;
404
+ parentFlexDirection = parentStyle.flexDirection;
405
+ }
406
+ }
407
+
408
+ // リンク情報の取得(要素自体または最も近い<a>祖先)
409
+ let linkElement: HTMLAnchorElement | null = null;
410
+ if (element.tagName === 'A') {
411
+ linkElement = element as HTMLAnchorElement;
412
+ } else {
413
+ linkElement = element.closest('a');
414
+ }
415
+ const isLink = linkElement !== null;
416
+ const linkHref = linkElement?.getAttribute('href') || '';
417
+ const linkTarget = linkElement?.getAttribute('target') || '_self';
418
+ const linkTitle = linkElement?.getAttribute('title') || '';
419
+
420
+ // CSS変数参照の抽出(インラインスタイルからvar()参照を取得)
421
+ // getComputedStyleは変数を解決してしまうため、element.styleから直接取得する必要がある
422
+ const extractVarReference = (styleValue: string | undefined): string | undefined => {
423
+ if (styleValue && styleValue.includes('var(')) {
424
+ return styleValue;
425
+ }
426
+ return undefined;
427
+ };
428
+
429
+ // インラインスタイルの生値を抽出(単位付きの値を保持)
430
+ // getComputedStyleはpxに変換してしまうため、element.styleから直接取得する
431
+ // viewport単位はdata-original-*属性に保存されているため、そちらを優先する
432
+ const extractRawStyleValue = (styleValue: string | undefined, property?: string): string | undefined => {
433
+ // viewport単位が保存されている場合はそちらを優先
434
+ if (property) {
435
+ const kebabProperty = property.replace(/([A-Z])/g, '-$1').toLowerCase();
436
+ const dataAttr = `data-original-${kebabProperty}`;
437
+ const originalValue = element.getAttribute(dataAttr);
438
+ if (originalValue) {
439
+ return originalValue;
440
+ }
441
+ }
442
+
443
+ if (!styleValue || styleValue === '') {
444
+ return undefined;
445
+ }
446
+ // var(), %, em, rem, vw, vh などの特殊な単位を含む場合は生値を返す
447
+ if (styleValue.includes('var(') ||
448
+ styleValue.includes('%') ||
449
+ styleValue.includes('em') ||
450
+ styleValue.includes('rem') ||
451
+ styleValue.includes('vw') ||
452
+ styleValue.includes('vh') ||
453
+ styleValue === 'auto') {
454
+ return styleValue;
455
+ }
456
+ // pxの場合も生値を返す(一貫性のため)
457
+ return styleValue;
458
+ };
459
+
460
+ const rawWidth = extractRawStyleValue(element.style.width, 'width');
461
+ const rawHeight = extractRawStyleValue(element.style.height, 'height');
462
+ const rawPaddingTop = extractRawStyleValue(element.style.paddingTop, 'paddingTop');
463
+ const rawPaddingRight = extractRawStyleValue(element.style.paddingRight, 'paddingRight');
464
+ const rawPaddingBottom = extractRawStyleValue(element.style.paddingBottom, 'paddingBottom');
465
+ const rawPaddingLeft = extractRawStyleValue(element.style.paddingLeft, 'paddingLeft');
466
+ const rawMarginTop = extractRawStyleValue(element.style.marginTop, 'marginTop');
467
+ const rawMarginRight = extractRawStyleValue(element.style.marginRight, 'marginRight');
468
+ const rawMarginBottom = extractRawStyleValue(element.style.marginBottom, 'marginBottom');
469
+ const rawMarginLeft = extractRawStyleValue(element.style.marginLeft, 'marginLeft');
470
+ /**
471
+ * Tailwindクラス経由でテーマトークンが当たっている場合も「変数に束縛されている」として扱う。
472
+ * このテンプレートの色はほぼ bg-gg-green / text-ink-70 のようなクラスで塗られており、
473
+ * インラインの var() だけを見るとバインド表示がほとんど機能しない。
474
+ * クラス名 bg-<token> → --color-<token> が実在するときだけ var() として返す
475
+ * (text-[17px] や text-xs のような非トークンは実在チェックで自然に落ちる)。
476
+ */
477
+ const rootStyle = iframeDoc.documentElement
478
+ ? iframeDoc.defaultView?.getComputedStyle(iframeDoc.documentElement)
479
+ : null;
480
+ const themeVarFromClass = (prefix: string): string | undefined => {
481
+ if (!rootStyle) return undefined;
482
+ const classes = typeof element.className === 'string' ? element.className.split(/\s+/) : [];
483
+ for (const cls of classes) {
484
+ if (!cls.startsWith(prefix) || cls.includes(':') || cls.includes('[')) continue;
485
+ const token = cls.slice(prefix.length);
486
+ if (!token) continue;
487
+ const varName = `--color-${token}`;
488
+ if (rootStyle.getPropertyValue(varName).trim()) return `var(${varName})`;
489
+ }
490
+ return undefined;
491
+ };
492
+
493
+ const rawBackgroundColor =
494
+ extractVarReference(element.style.backgroundColor) ?? themeVarFromClass('bg-');
495
+ const rawColor = extractVarReference(element.style.color) ?? themeVarFromClass('text-');
496
+ const rawBorderColor =
497
+ extractVarReference(element.style.borderColor) ?? themeVarFromClass('border-');
498
+ // Additional raw values for unit-aware inputs
499
+ const rawBorderRadius = extractRawStyleValue(element.style.borderRadius, 'borderRadius');
500
+ const rawBorderTopLeftRadius = extractRawStyleValue(element.style.borderTopLeftRadius, 'borderTopLeftRadius');
501
+ const rawBorderTopRightRadius = extractRawStyleValue(element.style.borderTopRightRadius, 'borderTopRightRadius');
502
+ const rawBorderBottomLeftRadius = extractRawStyleValue(element.style.borderBottomLeftRadius, 'borderBottomLeftRadius');
503
+ const rawBorderBottomRightRadius = extractRawStyleValue(element.style.borderBottomRightRadius, 'borderBottomRightRadius');
504
+ const rawOpacity = extractVarReference(element.style.opacity);
505
+ const rawFontSize = extractRawStyleValue(element.style.fontSize, 'fontSize');
506
+ const rawLineHeight = extractRawStyleValue(element.style.lineHeight, 'lineHeight');
507
+ const rawLetterSpacing = extractRawStyleValue(element.style.letterSpacing, 'letterSpacing');
508
+ const rawBorderWidth = extractRawStyleValue(element.style.borderWidth, 'borderWidth');
509
+ const rawGap = extractRawStyleValue(element.style.gap, 'gap');
510
+ // Position values (left/top)
511
+ const rawLeft = extractRawStyleValue(element.style.left, 'left');
512
+ const rawTop = extractRawStyleValue(element.style.top, 'top');
513
+
514
+ return {
515
+ id: element.getAttribute('data-element-id') || '',
516
+ tagName: element.tagName,
517
+ text: element.textContent?.trim().substring(0, 50) || '',
518
+ fontSize: parseFloat(style.fontSize) || 16,
519
+ fontFamily: style.fontFamily.split(',')[0].replace(/['"]/g, '').trim(),
520
+ fontWeight: style.fontWeight,
521
+ lineHeight: style.lineHeight,
522
+ letterSpacing: style.letterSpacing,
523
+ textAlign: style.textAlign,
524
+ textDecoration: style.textDecoration,
525
+ fontStyle: style.fontStyle,
526
+ color: style.color,
527
+ x: parseInt(style.left) || 0,
528
+ y: parseInt(style.top) || 0,
529
+ width: rect.width / scale,
530
+ height: rect.height / scale,
531
+ widthAuto,
532
+ heightAuto,
533
+ rotation: transformInfo.rotation,
534
+ scaleX: transformInfo.scaleX,
535
+ scaleY: transformInfo.scaleY,
536
+ paddingTop: paddingInfo.top,
537
+ paddingRight: paddingInfo.right,
538
+ paddingBottom: paddingInfo.bottom,
539
+ paddingLeft: paddingInfo.left,
540
+ paddingLinked,
541
+ marginTop: parseFloat(style.marginTop) || 0,
542
+ marginRight: parseFloat(style.marginRight) || 0,
543
+ marginBottom: parseFloat(style.marginBottom) || 0,
544
+ marginLeft: parseFloat(style.marginLeft) || 0,
545
+ backgroundColor: style.backgroundColor,
546
+ backgroundImage: style.backgroundImage,
547
+ backgroundSize: style.backgroundSize,
548
+ backgroundPosition: style.backgroundPosition,
549
+ backgroundRepeat: style.backgroundRepeat,
550
+ opacity: parseFloat(style.opacity) || 1,
551
+ hasBackgroundImage: style.backgroundImage !== 'none' && style.backgroundImage !== '',
552
+ imageFillMode: parseImageFillMode(style.backgroundSize, style.backgroundRepeat),
553
+ imagePositionX: parseBackgroundPositionX(style.backgroundPosition),
554
+ imagePositionY: parseBackgroundPositionY(style.backgroundPosition),
555
+ imageScale: parseBackgroundScale(style.backgroundSize),
556
+ borderWidth: parseFloat(style.borderWidth) || 0,
557
+ borderColor: style.borderColor,
558
+ borderStyle: style.borderStyle,
559
+ borderRadius: parseFloat(style.borderRadius) || 0,
560
+ borderRadiusTopLeft: parseFloat(style.borderTopLeftRadius) || 0,
561
+ borderRadiusTopRight: parseFloat(style.borderTopRightRadius) || 0,
562
+ borderRadiusBottomRight: parseFloat(style.borderBottomRightRadius) || 0,
563
+ borderRadiusBottomLeft: parseFloat(style.borderBottomLeftRadius) || 0,
564
+ boxShadow,
565
+ ...shadowInfo,
566
+ // Filter effects
567
+ filter: style.filter,
568
+ mixBlendMode: style.mixBlendMode || 'normal',
569
+ filterBlur: filterInfo.blur,
570
+ filterBrightness: filterInfo.brightness,
571
+ filterContrast: filterInfo.contrast,
572
+ filterGrayscale: filterInfo.grayscale,
573
+ filterSaturate: filterInfo.saturate,
574
+ filterSepia: filterInfo.sepia,
575
+ filterHueRotate: filterInfo.hueRotate,
576
+ filterInvert: filterInfo.invert,
577
+ // Backdrop filter
578
+ backdropFilter: style.backdropFilter,
579
+ backdropBlur: backdropFilterInfo.blur,
580
+ // Overflow
581
+ overflow: style.overflow || 'visible',
582
+ transformOrigin: style.transformOrigin,
583
+ // Auto Layout (Flexbox)
584
+ display: style.display || 'block',
585
+ flexDirection: style.flexDirection || 'row',
586
+ flexWrap: style.flexWrap || 'nowrap',
587
+ justifyContent: style.justifyContent || 'flex-start',
588
+ alignItems: style.alignItems || 'stretch',
589
+ gap: parseFloat(style.gap) || 0,
590
+ // Child flex properties
591
+ flexGrow: parseFloat(style.flexGrow) || 0,
592
+ flexShrink: parseFloat(style.flexShrink) || 1,
593
+ flexBasis: style.flexBasis || 'auto',
594
+ alignSelf: style.alignSelf || 'auto',
595
+ // CSS Grid properties
596
+ gridTemplateColumns: style.gridTemplateColumns || 'none',
597
+ gridTemplateRows: style.gridTemplateRows || 'none',
598
+ gridGap: parseFloat(style.gap) || 0,
599
+ gridRowGap: parseFloat(style.rowGap) || 0,
600
+ gridColumnGap: parseFloat(style.columnGap) || 0,
601
+ gridAutoFlow: style.gridAutoFlow || 'row',
602
+ gridAutoRows: style.gridAutoRows || 'auto',
603
+ gridAutoColumns: style.gridAutoColumns || 'auto',
604
+ justifyItems: style.justifyItems || 'stretch',
605
+ alignContent: style.alignContent || 'stretch',
606
+ // Child grid properties
607
+ gridColumn: style.gridColumn || 'auto',
608
+ gridRow: style.gridRow || 'auto',
609
+ gridColumnStart: parseInt(style.gridColumnStart) || 0,
610
+ gridColumnEnd: parseInt(style.gridColumnEnd) || 0,
611
+ gridRowStart: parseInt(style.gridRowStart) || 0,
612
+ gridRowEnd: parseInt(style.gridRowEnd) || 0,
613
+ justifySelf: style.justifySelf || 'auto',
614
+ // Parent context
615
+ parentDisplay,
616
+ parentFlexDirection,
617
+ // Link properties
618
+ isLink,
619
+ imageSrc: element.tagName === 'IMG' ? (element as HTMLImageElement).getAttribute('src') || '' : undefined,
620
+ linkHref,
621
+ linkTarget,
622
+ linkTitle,
623
+ // CSS Variable references (raw inline style values if they contain var())
624
+ rawWidth,
625
+ rawHeight,
626
+ rawPaddingTop,
627
+ rawPaddingRight,
628
+ rawPaddingBottom,
629
+ rawPaddingLeft,
630
+ rawMarginTop,
631
+ rawMarginRight,
632
+ rawMarginBottom,
633
+ rawMarginLeft,
634
+ rawBackgroundColor,
635
+ rawColor,
636
+ rawBorderColor,
637
+ // Additional raw values
638
+ rawBorderRadius,
639
+ rawBorderTopLeftRadius,
640
+ rawBorderTopRightRadius,
641
+ rawBorderBottomLeftRadius,
642
+ rawBorderBottomRightRadius,
643
+ rawOpacity,
644
+ rawFontSize,
645
+ rawLineHeight,
646
+ rawLetterSpacing,
647
+ rawBorderWidth,
648
+ rawGap,
649
+ rawLeft,
650
+ rawTop,
651
+ };
652
+ }
653
+
654
+ /**
655
+ * フィルモードからbackgroundSizeを生成
656
+ */
657
+ export function getBackgroundSizeFromFillMode(fillMode: ImageFillMode): string {
658
+ switch (fillMode) {
659
+ case 'fill': return '100% 100%';
660
+ case 'fit': return 'contain';
661
+ case 'tile': return 'auto';
662
+ case 'crop':
663
+ default: return 'cover';
664
+ }
665
+ }
666
+
667
+ /**
668
+ * フィルモードからbackgroundRepeatを生成
669
+ */
670
+ export function getBackgroundRepeatFromFillMode(fillMode: ImageFillMode): string {
671
+ return fillMode === 'tile' ? 'repeat' : 'no-repeat';
672
+ }
673
+
674
+ /**
675
+ * シャドウスタイル文字列を生成
676
+ */
677
+ export function buildBoxShadow(
678
+ color: string,
679
+ x: number,
680
+ y: number,
681
+ blur: number,
682
+ spread: number
683
+ ): string {
684
+ return `${color} ${x}px ${y}px ${blur}px ${spread}px`;
685
+ }