@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,804 @@
1
+ /**
2
+ * Tailwind CSS 変換ユーティリティ
3
+ *
4
+ * CSSプロパティ値をTailwindクラスに変換し、要素に適用する
5
+ */
6
+
7
+ import { twMerge } from 'tailwind-merge';
8
+ import {
9
+ TAILWIND_CONFLICT_GROUPS,
10
+ ENUM_MAPPINGS,
11
+ NUMERIC_PREFIXES,
12
+ COLOR_PREFIXES,
13
+ STYLE_ONLY_PROPERTIES,
14
+ SPECIAL_VALUES,
15
+ } from './tailwind-mappings';
16
+
17
+ /**
18
+ * 要素のクラス名を安全に取得(SVG要素対応)
19
+ * SVG要素のclassNameはSVGAnimatedStringオブジェクトなので、
20
+ * getAttribute('class')を使用して文字列として取得する
21
+ */
22
+ function getElementClassName(element: Element): string {
23
+ return element.getAttribute('class') || '';
24
+ }
25
+
26
+ /**
27
+ * 要素のクラス名を安全に設定(SVG要素対応)
28
+ */
29
+ function setElementClassName(element: Element, className: string): void {
30
+ if (className) {
31
+ element.setAttribute('class', className);
32
+ } else {
33
+ element.removeAttribute('class');
34
+ }
35
+ }
36
+
37
+ /**
38
+ * 変換結果の型
39
+ */
40
+ export interface ConversionResult {
41
+ /** 追加するTailwindクラス(nullの場合はstyle属性を使用) */
42
+ className: string | null;
43
+ /** style属性に適用する値(nullの場合はTailwindクラスを使用) */
44
+ styleValue: string | null;
45
+ }
46
+
47
+ /**
48
+ * CSSプロパティ値をTailwindクラスに変換
49
+ *
50
+ * @param property CSSプロパティ名(camelCase)
51
+ * @param value CSSプロパティ値
52
+ * @returns 変換結果(className または styleValue のいずれか)
53
+ */
54
+ export function cssToTailwindClass(
55
+ property: string,
56
+ value: string
57
+ ): ConversionResult {
58
+ // 空値の場合はスキップ
59
+ if (!value || value === '') {
60
+ return { className: null, styleValue: null };
61
+ }
62
+
63
+ // CSS変数参照はstyle属性に直接設定
64
+ if (value.startsWith('var(')) {
65
+ // DEBUG: var() を検出した時のログ
66
+ console.log('[DEBUG tailwind-utils] cssToTailwindClass detected var():', {
67
+ property,
68
+ value,
69
+ result: { className: null, styleValue: value },
70
+ });
71
+ return { className: null, styleValue: value };
72
+ }
73
+
74
+ // style属性のみのプロパティはフォールバック
75
+ if (STYLE_ONLY_PROPERTIES.includes(property)) {
76
+ return { className: null, styleValue: value };
77
+ }
78
+
79
+ // 1. 特殊値マッピングをチェック(auto, 100%, 50%など)
80
+ const specialMap = SPECIAL_VALUES[property];
81
+ if (specialMap && specialMap[value]) {
82
+ return { className: specialMap[value], styleValue: null };
83
+ }
84
+
85
+ // 2. 列挙型マッピングをチェック
86
+ const enumMap = ENUM_MAPPINGS[property];
87
+ if (enumMap && enumMap[value]) {
88
+ return { className: enumMap[value], styleValue: null };
89
+ }
90
+
91
+ // 3. 色プロパティの処理
92
+ const colorPrefix = COLOR_PREFIXES[property];
93
+ if (colorPrefix) {
94
+ return convertColorValue(colorPrefix, value);
95
+ }
96
+
97
+ // 4. 数値プレフィックスの処理
98
+ const numericPrefix = NUMERIC_PREFIXES[property];
99
+ if (numericPrefix) {
100
+ return convertNumericValue(numericPrefix, value);
101
+ }
102
+
103
+ // 5. 特殊なプロパティの処理
104
+ switch (property) {
105
+ case 'fontFamily':
106
+ return convertFontFamily(value);
107
+
108
+ case 'flexGrow':
109
+ return {
110
+ className: value === '1' || value === '1' ? 'grow' : 'grow-0',
111
+ styleValue: null,
112
+ };
113
+
114
+ case 'flexShrink':
115
+ return {
116
+ className: value === '1' || value === '1' ? 'shrink' : 'shrink-0',
117
+ styleValue: null,
118
+ };
119
+
120
+ case 'gridTemplateColumns':
121
+ return convertGridTemplate('grid-cols', value);
122
+
123
+ case 'gridTemplateRows':
124
+ return convertGridTemplate('grid-rows', value);
125
+
126
+ default:
127
+ // マッピングなし - style属性にフォールバック
128
+ return { className: null, styleValue: value };
129
+ }
130
+ }
131
+
132
+ /**
133
+ * 色値をTailwindクラスに変換
134
+ */
135
+ function convertColorValue(prefix: string, value: string): ConversionResult {
136
+ // transparent
137
+ if (value === 'transparent') {
138
+ return { className: `${prefix}-transparent`, styleValue: null };
139
+ }
140
+
141
+ // currentColor
142
+ if (value === 'currentColor' || value === 'currentcolor') {
143
+ return { className: `${prefix}-current`, styleValue: null };
144
+ }
145
+
146
+ // inherit
147
+ if (value === 'inherit') {
148
+ return { className: `${prefix}-inherit`, styleValue: null };
149
+ }
150
+
151
+ // HEX色はそのまま任意値として使用
152
+ if (value.startsWith('#')) {
153
+ return { className: `${prefix}-[${value}]`, styleValue: null };
154
+ }
155
+
156
+ // RGB/RGBA/HSL/HSLA
157
+ if (
158
+ value.startsWith('rgb') ||
159
+ value.startsWith('hsl') ||
160
+ value.startsWith('oklch') ||
161
+ value.startsWith('oklab')
162
+ ) {
163
+ // スペースをアンダースコアに置換してTailwindクラスとして使用
164
+ const sanitized = value.replace(/\s+/g, '_').replace(/,/g, '_');
165
+ return { className: `${prefix}-[${sanitized}]`, styleValue: null };
166
+ }
167
+
168
+ // 名前付きカラー(CSS named colors)はstyle属性にフォールバック
169
+ // Tailwindの標準色と一致しない可能性があるため
170
+ return { className: null, styleValue: value };
171
+ }
172
+
173
+ /**
174
+ * 数値値をTailwindクラスに変換
175
+ */
176
+ function convertNumericValue(prefix: string, value: string): ConversionResult {
177
+ // 0は特別扱い
178
+ if (value === '0' || value === '0px') {
179
+ return { className: `${prefix}-0`, styleValue: null };
180
+ }
181
+
182
+ // 負の値
183
+ const isNegative = value.startsWith('-');
184
+ const absValue = isNegative ? value.slice(1) : value;
185
+
186
+ // px値、rem値、%値、vh/vw値などはそのまま任意値として使用
187
+ const classValue = isNegative ? `-${prefix}-[${absValue}]` : `${prefix}-[${value}]`;
188
+ return { className: classValue, styleValue: null };
189
+ }
190
+
191
+ /**
192
+ * font-familyをTailwindクラスに変換
193
+ */
194
+ function convertFontFamily(value: string): ConversionResult {
195
+ // クォートを除去し、スペースをアンダースコアに置換
196
+ const sanitized = value
197
+ .replace(/["']/g, '')
198
+ .replace(/,\s*/g, ',')
199
+ .replace(/\s+/g, '_');
200
+
201
+ return { className: `font-[${sanitized}]`, styleValue: null };
202
+ }
203
+
204
+ /**
205
+ * grid-template-columns/rowsをTailwindクラスに変換
206
+ */
207
+ function convertGridTemplate(
208
+ prefix: string,
209
+ value: string
210
+ ): ConversionResult {
211
+ // repeat(n, 1fr) パターン
212
+ const repeatMatch = value.match(/^repeat\((\d+),\s*1fr\)$/);
213
+ if (repeatMatch) {
214
+ return { className: `${prefix}-${repeatMatch[1]}`, styleValue: null };
215
+ }
216
+
217
+ // none
218
+ if (value === 'none') {
219
+ return { className: `${prefix}-none`, styleValue: null };
220
+ }
221
+
222
+ // subgrid
223
+ if (value === 'subgrid') {
224
+ return { className: `${prefix}-subgrid`, styleValue: null };
225
+ }
226
+
227
+ // その他は任意値
228
+ const sanitized = value.replace(/\s+/g, '_');
229
+ return { className: `${prefix}-[${sanitized}]`, styleValue: null };
230
+ }
231
+
232
+ /**
233
+ * テキストクラスがフォントサイズクラスかどうかを判定
234
+ * text-xs, text-sm, text-base, text-lg, text-xl, text-2xl, text-[12px] など
235
+ */
236
+ function isTextSizeClass(cls: string): boolean {
237
+ if (!cls.startsWith('text-')) return false;
238
+
239
+ // Tailwind標準のフォントサイズクラス
240
+ const sizeKeywords = [
241
+ 'text-xs', 'text-sm', 'text-base', 'text-lg', 'text-xl',
242
+ 'text-2xl', 'text-3xl', 'text-4xl', 'text-5xl', 'text-6xl',
243
+ 'text-7xl', 'text-8xl', 'text-9xl',
244
+ ];
245
+ if (sizeKeywords.includes(cls)) return true;
246
+
247
+ // 任意値のサイズクラス: text-[12px], text-[1.5rem], text-[2em] など
248
+ const arbitraryMatch = cls.match(/^text-\[([^\]]+)\]$/);
249
+ if (arbitraryMatch) {
250
+ const value = arbitraryMatch[1];
251
+ // 数値で始まり、単位(px, rem, em, pt, vw, vh, %)で終わるか、純粋な数値
252
+ return /^-?\d/.test(value) && /(?:px|rem|em|pt|vw|vh|%|ch|ex|cap|ic|lh|rlh|vi|vb|vmin|vmax|cqw|cqh|cqi|cqb|cqmin|cqmax)?$/.test(value);
253
+ }
254
+
255
+ return false;
256
+ }
257
+
258
+ /**
259
+ * テキストクラスがテキストカラークラスかどうかを判定
260
+ * text-red-500, text-[#ff0000], text-inherit, text-transparent など
261
+ */
262
+ function isTextColorClass(cls: string): boolean {
263
+ if (!cls.startsWith('text-')) return false;
264
+
265
+ // サイズクラスではない text- クラスはカラークラスとみなす
266
+ if (isTextSizeClass(cls)) return false;
267
+
268
+ // 明示的なカラーキーワード
269
+ if (cls === 'text-inherit' || cls === 'text-current' || cls === 'text-transparent') return true;
270
+
271
+ // 任意値のカラークラス: text-[#ff0000], text-[rgb(...)], text-[hsl(...)] など
272
+ const arbitraryMatch = cls.match(/^text-\[([^\]]+)\]$/);
273
+ if (arbitraryMatch) {
274
+ const value = arbitraryMatch[1];
275
+ // #で始まる(HEX)、rgb/hsl/oklch等で始まる、または var(--) で始まる
276
+ return value.startsWith('#') ||
277
+ value.startsWith('rgb') ||
278
+ value.startsWith('hsl') ||
279
+ value.startsWith('oklch') ||
280
+ value.startsWith('oklab') ||
281
+ value.startsWith('var(');
282
+ }
283
+
284
+ // Tailwindの標準カラークラス(text-red-500, text-blue-700など)
285
+ // これらは text-{color}-{shade} のパターンに一致
286
+ const colorMatch = cls.match(/^text-([a-z]+)-(\d+)$/);
287
+ if (colorMatch) return true;
288
+
289
+ // text-white, text-black などの単純なカラー名
290
+ const simpleColors = ['text-white', 'text-black', 'text-slate', 'text-gray', 'text-zinc', 'text-neutral', 'text-stone', 'text-red', 'text-orange', 'text-amber', 'text-yellow', 'text-lime', 'text-green', 'text-emerald', 'text-teal', 'text-cyan', 'text-sky', 'text-blue', 'text-indigo', 'text-violet', 'text-purple', 'text-fuchsia', 'text-pink', 'text-rose'];
291
+ if (simpleColors.includes(cls)) return true;
292
+
293
+ return false;
294
+ }
295
+
296
+ /**
297
+ * 競合するクラスを要素から削除
298
+ *
299
+ * @param element 対象要素
300
+ * @param property 更新するCSSプロパティ名
301
+ */
302
+ export function removeConflictingClasses(
303
+ element: HTMLElement,
304
+ property: string
305
+ ): void {
306
+ const conflictPrefixes = TAILWIND_CONFLICT_GROUPS[property];
307
+
308
+ // fontSize と color は特別処理(twMergeに任せるため空配列だが、
309
+ // 同種のクラスのみ削除する特別なロジックを適用)
310
+ if (property === 'fontSize') {
311
+ const currentClasses = getElementClassName(element).split(/\s+/).filter(Boolean);
312
+ const filteredClasses = currentClasses.filter(cls => !isTextSizeClass(cls));
313
+ setElementClassName(element, filteredClasses.join(' '));
314
+ return;
315
+ }
316
+
317
+ if (property === 'color') {
318
+ const currentClasses = getElementClassName(element).split(/\s+/).filter(Boolean);
319
+ const filteredClasses = currentClasses.filter(cls => !isTextColorClass(cls));
320
+ setElementClassName(element, filteredClasses.join(' '));
321
+ return;
322
+ }
323
+
324
+ if (!conflictPrefixes || conflictPrefixes.length === 0) return;
325
+
326
+ const currentClasses = getElementClassName(element).split(/\s+/).filter(Boolean);
327
+ const filteredClasses = currentClasses.filter((cls) => {
328
+ // 各競合プレフィックスに対してチェック
329
+ return !conflictPrefixes.some((prefix) => {
330
+ // 完全一致(position: static, relativeなど)
331
+ if (cls === prefix) return true;
332
+ // プレフィックス一致(w-, h-, bg-など)
333
+ if (prefix.endsWith('-') && cls.startsWith(prefix)) return true;
334
+ // 負の値のプレフィックス一致(-top-[10px]など)
335
+ if (prefix.endsWith('-') && cls.startsWith(`-${prefix}`)) return true;
336
+ // font-[ で始まるパターン(font-family用)
337
+ if (prefix === 'font-[' && cls.startsWith('font-[')) return true;
338
+ return false;
339
+ });
340
+ });
341
+
342
+ setElementClassName(element, filteredClasses.join(' '));
343
+ }
344
+
345
+ /**
346
+ * 指定されたCSSプロパティのstyle属性をクリア
347
+ *
348
+ * @param element 対象要素
349
+ * @param property CSSプロパティ名
350
+ */
351
+ export function clearInlineStyle(
352
+ element: HTMLElement,
353
+ property: string
354
+ ): void {
355
+ // style属性から該当プロパティを削除
356
+ element.style.removeProperty(
357
+ property.replace(/([A-Z])/g, '-$1').toLowerCase()
358
+ );
359
+ }
360
+
361
+ /**
362
+ * 任意値クラス(arbitrary value)かどうかを判定
363
+ * 例: w-[100px], text-[185px], bg-[#ff0000]
364
+ */
365
+ function isArbitraryValueClass(className: string): boolean {
366
+ return className.includes('[') && className.includes(']');
367
+ }
368
+
369
+ /**
370
+ * レイアウト関連のプロパティかどうかを判定
371
+ * これらのプロパティは、Tailwind Browserのコンパイル遅延や
372
+ * 既存CSSルールとの競合を避けるため、インラインスタイルも併用する
373
+ */
374
+ const LAYOUT_CRITICAL_PROPERTIES = new Set([
375
+ 'display',
376
+ 'flexDirection',
377
+ 'flexWrap',
378
+ 'justifyContent',
379
+ 'alignItems',
380
+ 'alignContent',
381
+ 'gap',
382
+ 'rowGap',
383
+ 'columnGap',
384
+ 'gridTemplateColumns',
385
+ 'gridTemplateRows',
386
+ 'gridAutoFlow',
387
+ 'justifyItems',
388
+ ]);
389
+
390
+ /**
391
+ * Tailwindスタイルを要素に適用
392
+ *
393
+ * 1. 競合するTailwindクラスを削除
394
+ * 2. 新しいTailwindクラスを追加
395
+ * 3. style属性が必要な場合は適用
396
+ * 4. 任意値クラスの場合は、インラインスタイルもフォールバックとして設定
397
+ * (Tailwind Browser v4のJITコンパイルが遅れる場合の対策)
398
+ *
399
+ * @param element 対象要素
400
+ * @param styles 適用するスタイル(CSSプロパティ名: 値)
401
+ */
402
+ /**
403
+ * エディタが選択・ドラッグ等の状態管理に付けるクラス。
404
+ * デザインのクラスではないので、Tailwindの重複解決(twMerge)に混ぜてはいけない。
405
+ */
406
+ const EDITOR_MANAGED_CLASSES = new Set([
407
+ 'selected',
408
+ 'dragging',
409
+ 'editing',
410
+ 'rotating',
411
+ 'panning',
412
+ 'hover-preview',
413
+ 'marquee-hover',
414
+ 'marquee-active',
415
+ 'text-editable-hover',
416
+ 'drag-ghost',
417
+ ]);
418
+
419
+ export function applyTailwindStyles(
420
+ element: HTMLElement,
421
+ styles: Record<string, string>
422
+ ): void {
423
+ // DEBUG: applyTailwindStyles 開始時のログ
424
+ console.log('[DEBUG tailwind-utils] applyTailwindStyles called:', {
425
+ elementId: element.id,
426
+ elementTagName: element.tagName,
427
+ inputStyles: styles,
428
+ currentClassName: element.getAttribute('class'),
429
+ currentInlineStyle: element.getAttribute('style'),
430
+ });
431
+
432
+ const classesToAdd: string[] = [];
433
+ const stylesToApply: Record<string, string> = {};
434
+ const propertiesToClear: string[] = [];
435
+ // 任意値クラス用のフォールバックスタイル
436
+ const arbitraryFallbackStyles: Record<string, string> = {};
437
+
438
+ Object.entries(styles).forEach(([property, value]) => {
439
+ // 競合クラスを削除
440
+ removeConflictingClasses(element, property);
441
+
442
+ // 変換
443
+ const { className, styleValue } = cssToTailwindClass(property, value);
444
+
445
+ if (className) {
446
+ classesToAdd.push(className);
447
+
448
+ // 以下の場合はインラインスタイルもフォールバックとして設定:
449
+ // 1. 任意値クラス(Tailwind Browser v4が動的クラスをコンパイルするまでの間)
450
+ // 2. レイアウト関連プロパティ(既存CSSルールとの競合を避けるため)
451
+ if (isArbitraryValueClass(className) || LAYOUT_CRITICAL_PROPERTIES.has(property)) {
452
+ arbitraryFallbackStyles[property] = value;
453
+ } else {
454
+ // 通常のTailwindクラスの場合のみ、インラインスタイルをクリア
455
+ propertiesToClear.push(property);
456
+ }
457
+ }
458
+
459
+ if (styleValue) {
460
+ stylesToApply[property] = styleValue;
461
+ }
462
+ });
463
+
464
+ // 既存のstyle属性をクリア(通常のTailwindクラスで管理するプロパティのみ)
465
+ propertiesToClear.forEach((prop) => {
466
+ clearInlineStyle(element, prop);
467
+ });
468
+
469
+ // クラスを適用(tailwind-mergeで重複解決)
470
+ if (classesToAdd.length > 0) {
471
+ // エディタが状態管理に付けるクラスは twMerge に見せない。
472
+ // tailwind-merge は `text-editable-hover` を文字色クラス(text-{color})と解釈し、
473
+ // 既存の text-white/85 などを「同グループの古い指定」として削除してしまう
474
+ // (実害: テキストをドラッグしただけで文字色クラスが消えて色が変わった)。
475
+ // 退避してからマージし、最後に付け直す。
476
+ const parts = getElementClassName(element).split(/\s+/).filter(Boolean);
477
+ const editorClasses = parts.filter((c) => EDITOR_MANAGED_CLASSES.has(c));
478
+ const designClasses = parts.filter((c) => !EDITOR_MANAGED_CLASSES.has(c));
479
+ const merged = twMerge(designClasses.join(' '), ...classesToAdd);
480
+ setElementClassName(element, [merged, ...editorClasses].join(' ').trim());
481
+ }
482
+
483
+ // style属性を適用(STYLE_ONLY_PROPERTIESからの値)
484
+ Object.entries(stylesToApply).forEach(([prop, val]) => {
485
+ (element.style as any)[prop] = val;
486
+ });
487
+
488
+ // 任意値クラス用のフォールバックスタイルを適用
489
+ // これにより、Tailwind Browserがコンパイルする前でもスタイルが表示される
490
+ Object.entries(arbitraryFallbackStyles).forEach(([prop, val]) => {
491
+ (element.style as any)[prop] = val;
492
+ });
493
+
494
+ // DEBUG: applyTailwindStyles 最終適用結果のログ
495
+ console.log('[DEBUG tailwind-utils] applyTailwindStyles completed:', {
496
+ elementId: element.id,
497
+ classesToAdd,
498
+ stylesToApply,
499
+ arbitraryFallbackStyles,
500
+ propertiesToClear,
501
+ finalClassName: element.getAttribute('class'),
502
+ finalInlineStyle: element.getAttribute('style'),
503
+ });
504
+
505
+ // Tailwind Browserに再コンパイルをリクエスト(利用可能な場合)
506
+ triggerTailwindRecompile(element);
507
+ }
508
+
509
+ /**
510
+ * Tailwind Browserに再コンパイルをトリガー
511
+ * iframe内のTailwind Browserインスタンスに変更を通知
512
+ */
513
+ function triggerTailwindRecompile(element: HTMLElement): void {
514
+ try {
515
+ const doc = element.ownerDocument;
516
+ const win = doc?.defaultView as Window & {
517
+ tailwindcss?: { refresh?: () => void };
518
+ __twind?: { observe?: (el: Element) => void };
519
+ };
520
+
521
+ if (win?.tailwindcss?.refresh) {
522
+ // Tailwind CSS Browser v4の場合
523
+ win.tailwindcss.refresh();
524
+ } else if (win?.__twind?.observe) {
525
+ // Twindの場合(互換性のため)
526
+ win.__twind.observe(element);
527
+ }
528
+ } catch (e) {
529
+ // 再コンパイルに失敗しても、フォールバックスタイルが適用されているので問題なし
530
+ }
531
+ }
532
+
533
+ /**
534
+ * Tailwindクラスから値を抽出(PropertyPanel用)
535
+ *
536
+ * @param className 要素のclassName
537
+ * @param property CSSプロパティ名
538
+ * @returns 抽出された値(見つからない場合はnull)
539
+ */
540
+ export function extractValueFromTailwindClass(
541
+ className: unknown,
542
+ property: string
543
+ ): string | null {
544
+ if (!className) return null;
545
+
546
+ // classNameの型に応じて文字列を取得
547
+ // - string: そのまま使用
548
+ // - SVGAnimatedString: baseValプロパティ
549
+ // - DOMTokenList: valueプロパティ
550
+ // - その他: String()で変換
551
+ let classNameStr: string;
552
+ if (typeof className === 'string') {
553
+ classNameStr = className;
554
+ } else if (typeof className === 'object' && className !== null) {
555
+ // SVGAnimatedString (baseVal) または DOMTokenList (value) をチェック
556
+ const obj = className as { baseVal?: string; value?: string };
557
+ classNameStr = obj.baseVal ?? obj.value ?? String(className);
558
+ } else {
559
+ classNameStr = String(className);
560
+ }
561
+
562
+ if (!classNameStr || classNameStr === '[object Object]') return null;
563
+
564
+ const classes = classNameStr.split(/\s+/);
565
+
566
+ // 1. 特殊値マッピングから逆引き
567
+ const specialMap = SPECIAL_VALUES[property];
568
+ if (specialMap) {
569
+ for (const [value, tailwindClass] of Object.entries(specialMap)) {
570
+ if (classes.includes(tailwindClass)) {
571
+ return value;
572
+ }
573
+ }
574
+ }
575
+
576
+ // 2. 列挙型マッピングから逆引き
577
+ const enumMap = ENUM_MAPPINGS[property];
578
+ if (enumMap) {
579
+ for (const [value, tailwindClass] of Object.entries(enumMap)) {
580
+ if (classes.includes(tailwindClass)) {
581
+ return value;
582
+ }
583
+ }
584
+ }
585
+
586
+ // 3. 色プレフィックスの場合
587
+ const colorPrefix = COLOR_PREFIXES[property];
588
+ if (colorPrefix) {
589
+ const colorClass = classes.find((cls) => {
590
+ return (
591
+ cls.startsWith(`${colorPrefix}-[`) ||
592
+ cls === `${colorPrefix}-transparent` ||
593
+ cls === `${colorPrefix}-current` ||
594
+ cls === `${colorPrefix}-inherit`
595
+ );
596
+ });
597
+
598
+ if (colorClass) {
599
+ if (colorClass === `${colorPrefix}-transparent`) return 'transparent';
600
+ if (colorClass === `${colorPrefix}-current`) return 'currentColor';
601
+ if (colorClass === `${colorPrefix}-inherit`) return 'inherit';
602
+
603
+ // 任意値から抽出
604
+ const match = colorClass.match(/\[([^\]]+)\]/);
605
+ if (match) {
606
+ // アンダースコアをスペースに戻す
607
+ return match[1].replace(/_/g, ' ');
608
+ }
609
+ }
610
+ return null;
611
+ }
612
+
613
+ // 4. 数値プレフィックスの場合
614
+ const numericPrefix = NUMERIC_PREFIXES[property];
615
+ if (numericPrefix) {
616
+ // 負の値も含めて検索
617
+ const numericClass = classes.find((cls) => {
618
+ return (
619
+ cls.startsWith(`${numericPrefix}-`) ||
620
+ cls.startsWith(`-${numericPrefix}-`)
621
+ );
622
+ });
623
+
624
+ if (numericClass) {
625
+ // 特殊値(auto, full, 1/2など)のチェック
626
+ if (numericClass.endsWith('-auto')) return 'auto';
627
+ if (numericClass.endsWith('-full')) return '100%';
628
+ if (numericClass.includes('-1/2')) return '50%';
629
+ if (numericClass.includes('-1/3')) return '33.333333%';
630
+ if (numericClass.includes('-2/3')) return '66.666667%';
631
+ if (numericClass.includes('-1/4')) return '25%';
632
+ if (numericClass.includes('-3/4')) return '75%';
633
+ if (numericClass.endsWith('-fit')) return 'fit-content';
634
+ if (numericClass.endsWith('-min')) return 'min-content';
635
+ if (numericClass.endsWith('-max')) return 'max-content';
636
+ if (numericClass.endsWith('-screen')) {
637
+ if (property === 'width') return '100vw';
638
+ if (property === 'height') return '100vh';
639
+ }
640
+ if (numericClass.endsWith('-dvh')) return '100dvh';
641
+
642
+ // 0
643
+ if (numericClass === `${numericPrefix}-0`) return '0';
644
+
645
+ // 任意値から抽出
646
+ const match = numericClass.match(/\[([^\]]+)\]/);
647
+ if (match) {
648
+ // 負の値の場合はマイナス記号を付ける
649
+ const isNegative = numericClass.startsWith('-');
650
+ return isNegative ? `-${match[1]}` : match[1];
651
+ }
652
+ }
653
+ return null;
654
+ }
655
+
656
+ // 5. 特殊なプロパティ
657
+ switch (property) {
658
+ case 'fontFamily': {
659
+ const fontClass = classes.find((cls) => cls.startsWith('font-['));
660
+ if (fontClass) {
661
+ const match = fontClass.match(/font-\[([^\]]+)\]/);
662
+ if (match) {
663
+ // アンダースコアをスペースに戻す
664
+ return match[1].replace(/_/g, ' ');
665
+ }
666
+ }
667
+ return null;
668
+ }
669
+
670
+ case 'flexGrow': {
671
+ if (classes.includes('grow')) return '1';
672
+ if (classes.includes('grow-0')) return '0';
673
+ return null;
674
+ }
675
+
676
+ case 'flexShrink': {
677
+ if (classes.includes('shrink')) return '1';
678
+ if (classes.includes('shrink-0')) return '0';
679
+ return null;
680
+ }
681
+
682
+ case 'gridTemplateColumns': {
683
+ const colClass = classes.find((cls) => cls.startsWith('grid-cols-'));
684
+ if (colClass) {
685
+ if (colClass === 'grid-cols-none') return 'none';
686
+ if (colClass === 'grid-cols-subgrid') return 'subgrid';
687
+ const match = colClass.match(/grid-cols-(\d+)/);
688
+ if (match) return `repeat(${match[1]}, 1fr)`;
689
+ const arbitraryMatch = colClass.match(/grid-cols-\[([^\]]+)\]/);
690
+ if (arbitraryMatch) return arbitraryMatch[1].replace(/_/g, ' ');
691
+ }
692
+ return null;
693
+ }
694
+
695
+ case 'gridTemplateRows': {
696
+ const rowClass = classes.find((cls) => cls.startsWith('grid-rows-'));
697
+ if (rowClass) {
698
+ if (rowClass === 'grid-rows-none') return 'none';
699
+ if (rowClass === 'grid-rows-subgrid') return 'subgrid';
700
+ const match = rowClass.match(/grid-rows-(\d+)/);
701
+ if (match) return `repeat(${match[1]}, 1fr)`;
702
+ const arbitraryMatch = rowClass.match(/grid-rows-\[([^\]]+)\]/);
703
+ if (arbitraryMatch) return arbitraryMatch[1].replace(/_/g, ' ');
704
+ }
705
+ return null;
706
+ }
707
+
708
+ default:
709
+ return null;
710
+ }
711
+ }
712
+
713
+ /**
714
+ * 要素のインラインスタイルをTailwindクラスに変換
715
+ *
716
+ * ドラッグやリサイズ中はパフォーマンスのためインラインスタイルを使用し、
717
+ * 操作終了時にこの関数を呼び出してTailwindクラスに変換する
718
+ *
719
+ * @param element 対象要素
720
+ * @param properties 変換するCSSプロパティ名の配列(指定しない場合は主要プロパティ全て)
721
+ */
722
+ export function convertInlineStylesToTailwind(
723
+ element: HTMLElement,
724
+ properties?: string[]
725
+ ): void {
726
+ // 変換対象のプロパティ(デフォルト)
727
+ const targetProperties = properties || [
728
+ 'width',
729
+ 'height',
730
+ 'minWidth',
731
+ 'maxWidth',
732
+ 'minHeight',
733
+ 'maxHeight',
734
+ 'top',
735
+ 'right',
736
+ 'bottom',
737
+ 'left',
738
+ 'padding',
739
+ 'paddingTop',
740
+ 'paddingRight',
741
+ 'paddingBottom',
742
+ 'paddingLeft',
743
+ 'margin',
744
+ 'marginTop',
745
+ 'marginRight',
746
+ 'marginBottom',
747
+ 'marginLeft',
748
+ 'gap',
749
+ 'borderRadius',
750
+ 'borderWidth',
751
+ ];
752
+
753
+ const stylesToConvert: Record<string, string> = {};
754
+
755
+ // インラインスタイルから値を抽出
756
+ targetProperties.forEach((prop) => {
757
+ // camelCaseからkebab-caseに変換
758
+ const kebabProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
759
+ const value = element.style.getPropertyValue(kebabProp);
760
+
761
+ if (value && value !== '') {
762
+ stylesToConvert[prop] = value;
763
+ }
764
+ });
765
+
766
+ // Tailwindクラスに変換して適用
767
+ if (Object.keys(stylesToConvert).length > 0) {
768
+ applyTailwindStyles(element, stylesToConvert);
769
+ }
770
+ }
771
+
772
+ /**
773
+ * 要素のスタイル情報を取得(Tailwindクラス + computedStyle)
774
+ *
775
+ * @param element 対象要素
776
+ * @param property CSSプロパティ名
777
+ * @returns スタイル値(Tailwindクラスから抽出を優先、なければcomputedStyle)
778
+ */
779
+ export function getElementStyleValue(
780
+ element: HTMLElement,
781
+ property: string
782
+ ): string {
783
+ // 1. Tailwindクラスから抽出を試みる
784
+ const tailwindValue = extractValueFromTailwindClass(
785
+ getElementClassName(element),
786
+ property
787
+ );
788
+ if (tailwindValue !== null) {
789
+ return tailwindValue;
790
+ }
791
+
792
+ // 2. inline styleから取得
793
+
794
+ const inlineValue = (element.style as any)[property];
795
+ if (inlineValue) {
796
+ return inlineValue;
797
+ }
798
+
799
+ // 3. computedStyleから取得
800
+ const computed = window.getComputedStyle(element);
801
+ return computed.getPropertyValue(
802
+ property.replace(/([A-Z])/g, '-$1').toLowerCase()
803
+ );
804
+ }