@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,295 @@
1
+ /**
2
+ * ビューポート単位のユーティリティ
3
+ *
4
+ * iframe内でvw/vh/vmin/vmaxを使用する際、ブラウザのビューポートではなく
5
+ * iframeの寸法を基準に計算するためのユーティリティ
6
+ */
7
+
8
+ // ビューポート単位のパターン
9
+ const VIEWPORT_UNIT_REGEX = /^(-?\d*\.?\d+)(vw|vh|vmin|vmax)$/;
10
+ const VIEWPORT_UNIT_IN_CALC_REGEX = /([\d.]+)(vw|vh|vmin|vmax)/g;
11
+
12
+ // ビューポート単位を含むプロパティ
13
+ const VIEWPORT_SENSITIVE_PROPERTIES = [
14
+ 'width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight',
15
+ 'top', 'left', 'right', 'bottom',
16
+ 'marginTop', 'marginRight', 'marginBottom', 'marginLeft',
17
+ 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft',
18
+ 'fontSize', 'lineHeight', 'letterSpacing',
19
+ 'gap', 'rowGap', 'columnGap',
20
+ 'borderWidth', 'borderRadius',
21
+ 'borderTopLeftRadius', 'borderTopRightRadius',
22
+ 'borderBottomLeftRadius', 'borderBottomRightRadius',
23
+ ];
24
+
25
+ /**
26
+ * 値がビューポート単位を含むかチェック
27
+ */
28
+ export function hasViewportUnit(value: string): boolean {
29
+ if (!value) return false;
30
+ return VIEWPORT_UNIT_REGEX.test(value) || VIEWPORT_UNIT_IN_CALC_REGEX.test(value);
31
+ }
32
+
33
+ /**
34
+ * ビューポート単位をpxに変換
35
+ */
36
+ export function convertViewportToPx(
37
+ value: string,
38
+ iframeWidth: number,
39
+ iframeHeight: number
40
+ ): string {
41
+ if (!value || !hasViewportUnit(value)) {
42
+ return value;
43
+ }
44
+
45
+ const vmin = Math.min(iframeWidth, iframeHeight);
46
+ const vmax = Math.max(iframeWidth, iframeHeight);
47
+
48
+ // 単純なビューポート単位 (e.g., "50vw")
49
+ const simpleMatch = value.match(VIEWPORT_UNIT_REGEX);
50
+ if (simpleMatch) {
51
+ const num = parseFloat(simpleMatch[1]);
52
+ const unit = simpleMatch[2];
53
+ let px: number;
54
+
55
+ switch (unit) {
56
+ case 'vw':
57
+ px = (num * iframeWidth) / 100;
58
+ break;
59
+ case 'vh':
60
+ px = (num * iframeHeight) / 100;
61
+ break;
62
+ case 'vmin':
63
+ px = (num * vmin) / 100;
64
+ break;
65
+ case 'vmax':
66
+ px = (num * vmax) / 100;
67
+ break;
68
+ default:
69
+ return value;
70
+ }
71
+
72
+ return `${Math.round(px * 100) / 100}px`;
73
+ }
74
+
75
+ // calc()内のビューポート単位を変換
76
+ return value.replace(VIEWPORT_UNIT_IN_CALC_REGEX, (match, num, unit) => {
77
+ const numVal = parseFloat(num);
78
+ let px: number;
79
+
80
+ switch (unit) {
81
+ case 'vw':
82
+ px = (numVal * iframeWidth) / 100;
83
+ break;
84
+ case 'vh':
85
+ px = (numVal * iframeHeight) / 100;
86
+ break;
87
+ case 'vmin':
88
+ px = (numVal * vmin) / 100;
89
+ break;
90
+ case 'vmax':
91
+ px = (numVal * vmax) / 100;
92
+ break;
93
+ default:
94
+ return match;
95
+ }
96
+
97
+ return `${Math.round(px * 100) / 100}px`;
98
+ });
99
+ }
100
+
101
+ /**
102
+ * pxをビューポート単位に変換
103
+ */
104
+ export function convertPxToViewport(
105
+ pxValue: number,
106
+ unit: 'vw' | 'vh' | 'vmin' | 'vmax',
107
+ iframeWidth: number,
108
+ iframeHeight: number
109
+ ): number {
110
+ const vmin = Math.min(iframeWidth, iframeHeight);
111
+ const vmax = Math.max(iframeWidth, iframeHeight);
112
+
113
+ switch (unit) {
114
+ case 'vw':
115
+ return Math.round((pxValue / iframeWidth) * 100 * 100) / 100;
116
+ case 'vh':
117
+ return Math.round((pxValue / iframeHeight) * 100 * 100) / 100;
118
+ case 'vmin':
119
+ return Math.round((pxValue / vmin) * 100 * 100) / 100;
120
+ case 'vmax':
121
+ return Math.round((pxValue / vmax) * 100 * 100) / 100;
122
+ default:
123
+ return pxValue;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * data-original-* 属性名を生成
129
+ */
130
+ export function getOriginalAttrName(cssProperty: string): string {
131
+ // camelCase to kebab-case
132
+ const kebab = cssProperty.replace(/([A-Z])/g, '-$1').toLowerCase();
133
+ return `data-original-${kebab}`;
134
+ }
135
+
136
+ /**
137
+ * 要素にビューポート単位の値を適用
138
+ * - 元の値をdata属性に保存
139
+ * - 変換したpx値をstyleに適用
140
+ */
141
+ export function applyViewportValue(
142
+ element: HTMLElement,
143
+ property: string,
144
+ value: string,
145
+ iframeWidth: number,
146
+ iframeHeight: number
147
+ ): void {
148
+ if (hasViewportUnit(value)) {
149
+ // 元の値を保存
150
+ const attrName = getOriginalAttrName(property);
151
+ element.setAttribute(attrName, value);
152
+
153
+ // pxに変換して適用
154
+ const pxValue = convertViewportToPx(value, iframeWidth, iframeHeight);
155
+ element.style.setProperty(
156
+ property.replace(/([A-Z])/g, '-$1').toLowerCase(),
157
+ pxValue
158
+ );
159
+ } else {
160
+ // ビューポート単位でない場合は元の属性を削除
161
+ const attrName = getOriginalAttrName(property);
162
+ element.removeAttribute(attrName);
163
+
164
+ // そのまま適用
165
+ element.style.setProperty(
166
+ property.replace(/([A-Z])/g, '-$1').toLowerCase(),
167
+ value
168
+ );
169
+ }
170
+ }
171
+
172
+ /**
173
+ * 要素から元のビューポート単位値を取得
174
+ */
175
+ export function getOriginalViewportValue(
176
+ element: HTMLElement,
177
+ property: string
178
+ ): string | null {
179
+ const attrName = getOriginalAttrName(property);
180
+ return element.getAttribute(attrName);
181
+ }
182
+
183
+ /**
184
+ * HTML保存前に元のビューポート単位値を復元
185
+ */
186
+ export function restoreViewportUnitsForSave(doc: Document): void {
187
+ const elements = doc.querySelectorAll('[data-original-width], [data-original-height], [data-original-top], [data-original-left], [data-original-right], [data-original-bottom], [data-original-margin-top], [data-original-margin-right], [data-original-margin-bottom], [data-original-margin-left], [data-original-padding-top], [data-original-padding-right], [data-original-padding-bottom], [data-original-padding-left], [data-original-font-size], [data-original-gap], [data-original-border-width], [data-original-border-radius], [data-original-min-width], [data-original-min-height], [data-original-max-width], [data-original-max-height]');
188
+
189
+ elements.forEach((el) => {
190
+ const htmlEl = el as HTMLElement;
191
+ const attrs = Array.from(htmlEl.attributes);
192
+
193
+ attrs.forEach((attr) => {
194
+ if (attr.name.startsWith('data-original-')) {
195
+ const cssProperty = attr.name
196
+ .replace('data-original-', '')
197
+ .replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
198
+
199
+ // 元の値をstyleに復元
200
+ htmlEl.style.setProperty(
201
+ attr.name.replace('data-original-', ''),
202
+ attr.value
203
+ );
204
+
205
+ // data属性を削除(保存するHTMLをクリーンに)
206
+ htmlEl.removeAttribute(attr.name);
207
+ }
208
+ });
209
+ });
210
+ }
211
+
212
+ /**
213
+ * 保存用HTMLを生成(ビューポート単位を復元)
214
+ */
215
+ export function prepareHtmlForSave(artboard: HTMLElement): string {
216
+ // クローンを作成して元のDOMを変更しない
217
+ const clone = artboard.cloneNode(true) as HTMLElement;
218
+
219
+ // data-original-* 属性から元の値を復元
220
+ const elementsWithOriginal = clone.querySelectorAll('*');
221
+ elementsWithOriginal.forEach((el) => {
222
+ const htmlEl = el as HTMLElement;
223
+ const attrs = Array.from(htmlEl.attributes);
224
+
225
+ attrs.forEach((attr) => {
226
+ if (attr.name.startsWith('data-original-')) {
227
+ const cssProperty = attr.name.replace('data-original-', '');
228
+
229
+ // 元の値をstyleに復元
230
+ htmlEl.style.setProperty(cssProperty, attr.value);
231
+
232
+ // data属性を削除
233
+ htmlEl.removeAttribute(attr.name);
234
+ }
235
+ });
236
+ });
237
+
238
+ return clone.innerHTML;
239
+ }
240
+
241
+ /**
242
+ * iframe内の全要素のビューポート単位を再計算
243
+ * (iframeサイズ変更時に呼び出す)
244
+ */
245
+ export function recalculateViewportUnits(
246
+ doc: Document,
247
+ iframeWidth: number,
248
+ iframeHeight: number
249
+ ): void {
250
+ const elements = doc.querySelectorAll('*');
251
+
252
+ elements.forEach((el) => {
253
+ const htmlEl = el as HTMLElement;
254
+ const attrs = Array.from(htmlEl.attributes);
255
+
256
+ attrs.forEach((attr) => {
257
+ if (attr.name.startsWith('data-original-')) {
258
+ const cssProperty = attr.name.replace('data-original-', '');
259
+ const originalValue = attr.value;
260
+
261
+ // px値を再計算して適用
262
+ const pxValue = convertViewportToPx(originalValue, iframeWidth, iframeHeight);
263
+ htmlEl.style.setProperty(cssProperty, pxValue);
264
+ }
265
+ });
266
+ });
267
+ }
268
+
269
+ /**
270
+ * スタイルオブジェクトをビューポート対応形式に変換
271
+ */
272
+ export function processStylesForViewport(
273
+ styles: Record<string, string>,
274
+ iframeWidth: number,
275
+ iframeHeight: number
276
+ ): {
277
+ appliedStyles: Record<string, string>;
278
+ originalValues: Record<string, string>;
279
+ } {
280
+ const appliedStyles: Record<string, string> = {};
281
+ const originalValues: Record<string, string> = {};
282
+
283
+ Object.entries(styles).forEach(([property, value]) => {
284
+ if (hasViewportUnit(value)) {
285
+ // 元の値を保存
286
+ originalValues[property] = value;
287
+ // pxに変換
288
+ appliedStyles[property] = convertViewportToPx(value, iframeWidth, iframeHeight);
289
+ } else {
290
+ appliedStyles[property] = value;
291
+ }
292
+ });
293
+
294
+ return { appliedStyles, originalValues };
295
+ }
@@ -0,0 +1,195 @@
1
+ 'use client';
2
+
3
+ import { useState, useCallback, useRef, useEffect } from 'react';
4
+ import { HistoryState, MAX_HISTORY_SIZE, HISTORY_DEBOUNCE_MS } from '../types/editor';
5
+
6
+ interface UseEditorHistoryOptions {
7
+ maxHistorySize?: number;
8
+ debounceMs?: number;
9
+ }
10
+
11
+ interface UseEditorHistoryReturn {
12
+ html: string;
13
+ setHtml: (html: string) => void;
14
+ pushState: (html: string, selectedId?: string | null) => void;
15
+ undo: () => void;
16
+ redo: () => void;
17
+ canUndo: boolean;
18
+ canRedo: boolean;
19
+ clearHistory: () => void;
20
+ currentState: HistoryState;
21
+ }
22
+
23
+ export function useEditorHistory(
24
+ initialHtml: string,
25
+ options: UseEditorHistoryOptions = {}
26
+ ): UseEditorHistoryReturn {
27
+ const {
28
+ maxHistorySize = MAX_HISTORY_SIZE,
29
+ debounceMs = HISTORY_DEBOUNCE_MS,
30
+ } = options;
31
+
32
+ // History stacks
33
+ const [past, setPast] = useState<HistoryState[]>([]);
34
+ const [present, setPresent] = useState<HistoryState>({
35
+ html: initialHtml,
36
+ selectedElementId: null,
37
+ timestamp: Date.now(),
38
+ });
39
+ const [future, setFuture] = useState<HistoryState[]>([]);
40
+
41
+ // Debounce timer ref
42
+ const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
43
+ const pendingStateRef = useRef<HistoryState | null>(null);
44
+
45
+ // Reset history when initial HTML changes significantly (e.g., loading new slide)
46
+ useEffect(() => {
47
+ // Only reset if the initial HTML is significantly different
48
+ // This prevents resetting during normal edits
49
+ if (past.length === 0 && future.length === 0 && present.html !== initialHtml) {
50
+ setPresent({
51
+ html: initialHtml,
52
+ selectedElementId: null,
53
+ timestamp: Date.now(),
54
+ });
55
+ }
56
+ }, [initialHtml]);
57
+
58
+ // Push new state to history with debouncing
59
+ const pushState = useCallback((html: string, selectedId?: string | null) => {
60
+ // If HTML hasn't changed, don't push
61
+ if (html === present.html) return;
62
+
63
+ const newState: HistoryState = {
64
+ html,
65
+ selectedElementId: selectedId ?? present.selectedElementId,
66
+ timestamp: Date.now(),
67
+ };
68
+
69
+ // Clear any existing debounce timer
70
+ if (debounceTimerRef.current) {
71
+ clearTimeout(debounceTimerRef.current);
72
+ }
73
+
74
+ // Store pending state
75
+ pendingStateRef.current = newState;
76
+
77
+ // Debounce the history push
78
+ debounceTimerRef.current = setTimeout(() => {
79
+ if (pendingStateRef.current) {
80
+ setPast(prev => {
81
+ const newPast = [...prev, present];
82
+ // Trim history if it exceeds max size
83
+ if (newPast.length > maxHistorySize) {
84
+ return newPast.slice(newPast.length - maxHistorySize);
85
+ }
86
+ return newPast;
87
+ });
88
+ setPresent(pendingStateRef.current);
89
+ setFuture([]); // Clear redo stack on new action
90
+ pendingStateRef.current = null;
91
+ }
92
+ }, debounceMs);
93
+ }, [present, maxHistorySize, debounceMs]);
94
+
95
+ // Immediate push (bypasses debouncing) - useful for discrete actions
96
+ const pushStateImmediate = useCallback((html: string, selectedId?: string | null) => {
97
+ // Clear any pending debounced state
98
+ if (debounceTimerRef.current) {
99
+ clearTimeout(debounceTimerRef.current);
100
+ debounceTimerRef.current = null;
101
+ }
102
+ pendingStateRef.current = null;
103
+
104
+ if (html === present.html) return;
105
+
106
+ const newState: HistoryState = {
107
+ html,
108
+ selectedElementId: selectedId ?? present.selectedElementId,
109
+ timestamp: Date.now(),
110
+ };
111
+
112
+ setPast(prev => {
113
+ const newPast = [...prev, present];
114
+ if (newPast.length > maxHistorySize) {
115
+ return newPast.slice(newPast.length - maxHistorySize);
116
+ }
117
+ return newPast;
118
+ });
119
+ setPresent(newState);
120
+ setFuture([]);
121
+ }, [present, maxHistorySize]);
122
+
123
+ // Undo action
124
+ const undo = useCallback(() => {
125
+ // Flush any pending state first
126
+ if (debounceTimerRef.current) {
127
+ clearTimeout(debounceTimerRef.current);
128
+ debounceTimerRef.current = null;
129
+ }
130
+
131
+ if (past.length === 0) return;
132
+
133
+ const previous = past[past.length - 1];
134
+ const newPast = past.slice(0, -1);
135
+
136
+ setPast(newPast);
137
+ setPresent(previous);
138
+ setFuture([present, ...future]);
139
+ }, [past, present, future]);
140
+
141
+ // Redo action
142
+ const redo = useCallback(() => {
143
+ if (future.length === 0) return;
144
+
145
+ const next = future[0];
146
+ const newFuture = future.slice(1);
147
+
148
+ setPast([...past, present]);
149
+ setPresent(next);
150
+ setFuture(newFuture);
151
+ }, [past, present, future]);
152
+
153
+ // Clear all history
154
+ const clearHistory = useCallback(() => {
155
+ if (debounceTimerRef.current) {
156
+ clearTimeout(debounceTimerRef.current);
157
+ debounceTimerRef.current = null;
158
+ }
159
+ pendingStateRef.current = null;
160
+ setPast([]);
161
+ setFuture([]);
162
+ }, []);
163
+
164
+ // Set HTML without pushing to history (for external updates)
165
+ const setHtml = useCallback((html: string) => {
166
+ setPresent(prev => ({
167
+ ...prev,
168
+ html,
169
+ timestamp: Date.now(),
170
+ }));
171
+ }, []);
172
+
173
+ // Cleanup on unmount
174
+ useEffect(() => {
175
+ return () => {
176
+ if (debounceTimerRef.current) {
177
+ clearTimeout(debounceTimerRef.current);
178
+ }
179
+ };
180
+ }, []);
181
+
182
+ return {
183
+ html: present.html,
184
+ setHtml,
185
+ pushState: pushStateImmediate, // Use immediate for discrete actions
186
+ undo,
187
+ redo,
188
+ canUndo: past.length > 0,
189
+ canRedo: future.length > 0,
190
+ clearHistory,
191
+ currentState: present,
192
+ };
193
+ }
194
+
195
+ export default useEditorHistory;