@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,261 @@
1
+ /**
2
+ * CSS単位関連のユーティリティ関数
3
+ */
4
+
5
+ // サポートする単位の定義
6
+ export const CSS_UNITS = ['px', '%', 'em', 'rem', 'vw', 'vh'] as const;
7
+ export type CSSUnit = (typeof CSS_UNITS)[number];
8
+
9
+ // サイズ用単位(auto含む)
10
+ export const SIZE_UNITS = ['px', '%', 'vw', 'vh', 'auto'] as const;
11
+ export type SizeUnit = (typeof SIZE_UNITS)[number];
12
+
13
+ // フォントサイズ用単位
14
+ export const FONT_SIZE_UNITS = ['px', 'em', 'rem', '%'] as const;
15
+ export type FontSizeUnit = (typeof FONT_SIZE_UNITS)[number];
16
+
17
+ // スペーシング用単位(gap, padding, margin)
18
+ export const SPACING_UNITS = ['px', 'em', 'rem', '%'] as const;
19
+ export type SpacingUnit = (typeof SPACING_UNITS)[number];
20
+
21
+ // Border Radius用単位
22
+ export const BORDER_RADIUS_UNITS = ['px', '%', 'em'] as const;
23
+ export type BorderRadiusUnit = (typeof BORDER_RADIUS_UNITS)[number];
24
+
25
+ // Line Height用単位 (空文字は unitless を表す)
26
+ export const LINE_HEIGHT_UNITS = ['', 'em', 'px', '%'] as const;
27
+ export type LineHeightUnit = (typeof LINE_HEIGHT_UNITS)[number];
28
+
29
+ // Letter Spacing用単位
30
+ export const LETTER_SPACING_UNITS = ['em', 'px'] as const;
31
+ export type LetterSpacingUnit = (typeof LETTER_SPACING_UNITS)[number];
32
+
33
+ // Border Width用単位
34
+ export const BORDER_WIDTH_UNITS = ['px', 'em'] as const;
35
+ export type BorderWidthUnit = (typeof BORDER_WIDTH_UNITS)[number];
36
+
37
+ // Position用単位 (left, top, right, bottom)
38
+ export const POSITION_UNITS = ['px', '%', 'vw', 'vh'] as const;
39
+ export type PositionUnit = (typeof POSITION_UNITS)[number];
40
+
41
+ // 値と単位のパース結果
42
+ export interface ParsedValue {
43
+ numericValue: number;
44
+ unit: string;
45
+ isSpecial: boolean; // auto, inherit, var(...) などの特殊値
46
+ originalValue: string;
47
+ }
48
+
49
+ /**
50
+ * CSS値を数値と単位にパースする
51
+ * @param value CSS値文字列 (例: "16px", "1.5em", "100%", "auto", "var(--spacing)")
52
+ * @param defaultUnit デフォルト単位 (パース失敗時に使用)
53
+ * @returns パース結果
54
+ */
55
+ export function parseValueWithUnit(
56
+ value: string | number | undefined,
57
+ defaultUnit: string = 'px'
58
+ ): ParsedValue {
59
+ // undefined または空の場合
60
+ if (value === undefined || value === '') {
61
+ return {
62
+ numericValue: 0,
63
+ unit: defaultUnit,
64
+ isSpecial: false,
65
+ originalValue: '',
66
+ };
67
+ }
68
+
69
+ // 数値の場合
70
+ if (typeof value === 'number') {
71
+ return {
72
+ numericValue: value,
73
+ unit: defaultUnit,
74
+ isSpecial: false,
75
+ originalValue: `${value}${defaultUnit}`,
76
+ };
77
+ }
78
+
79
+ const trimmedValue = value.trim();
80
+
81
+ // 特殊値のチェック (auto, inherit, initial, unset, var(...))
82
+ if (
83
+ trimmedValue === 'auto' ||
84
+ trimmedValue === 'inherit' ||
85
+ trimmedValue === 'initial' ||
86
+ trimmedValue === 'unset' ||
87
+ trimmedValue.startsWith('var(')
88
+ ) {
89
+ return {
90
+ numericValue: 0,
91
+ unit: defaultUnit,
92
+ isSpecial: true,
93
+ originalValue: trimmedValue,
94
+ };
95
+ }
96
+
97
+ // 数値+単位のパターンをマッチ
98
+ const match = trimmedValue.match(/^(-?\d*\.?\d+)(px|%|em|rem|vw|vh)?$/);
99
+ if (match) {
100
+ return {
101
+ numericValue: parseFloat(match[1]),
102
+ unit: match[2] || defaultUnit,
103
+ isSpecial: false,
104
+ originalValue: trimmedValue,
105
+ };
106
+ }
107
+
108
+ // パース失敗時はデフォルト値を返す
109
+ return {
110
+ numericValue: 0,
111
+ unit: defaultUnit,
112
+ isSpecial: false,
113
+ originalValue: trimmedValue,
114
+ };
115
+ }
116
+
117
+ /**
118
+ * 数値と単位を結合してCSS値を生成
119
+ * @param numericValue 数値
120
+ * @param unit 単位
121
+ * @returns CSS値文字列
122
+ */
123
+ export function formatValueWithUnit(numericValue: number, unit: string): string {
124
+ // autoの場合は単位なし
125
+ if (unit === 'auto') {
126
+ return 'auto';
127
+ }
128
+ return `${numericValue}${unit}`;
129
+ }
130
+
131
+ /**
132
+ * px値をパースして数値を返す(後方互換性用)
133
+ * @param value CSS値文字列
134
+ * @returns 数値
135
+ */
136
+ export function parsePxValue(value: string | number | undefined): number {
137
+ const parsed = parseValueWithUnit(value, 'px');
138
+ return parsed.numericValue;
139
+ }
140
+
141
+ /**
142
+ * 単位変換のコンテキスト情報
143
+ */
144
+ export interface UnitConversionContext {
145
+ /** ビューポート幅 (px) */
146
+ viewportWidth?: number;
147
+ /** ビューポート高さ (px) */
148
+ viewportHeight?: number;
149
+ /** 親要素の幅 (px) */
150
+ parentWidth?: number;
151
+ /** 親要素の高さ (px) */
152
+ parentHeight?: number;
153
+ /** 要素のフォントサイズ (px) */
154
+ fontSize?: number;
155
+ /** ルートフォントサイズ (px) - 通常は16 */
156
+ rootFontSize?: number;
157
+ /** 変換対象のプロパティ (width/height/other) */
158
+ property?: 'width' | 'height' | 'other';
159
+ }
160
+
161
+ /**
162
+ * 値を一度pxに変換する
163
+ */
164
+ function toPx(
165
+ value: number,
166
+ fromUnit: string,
167
+ context: UnitConversionContext
168
+ ): number {
169
+ const {
170
+ viewportWidth = 1920,
171
+ viewportHeight = 1080,
172
+ parentWidth = 1920,
173
+ parentHeight = 1080,
174
+ fontSize = 16,
175
+ rootFontSize = 16,
176
+ property = 'width',
177
+ } = context;
178
+
179
+ switch (fromUnit) {
180
+ case 'px':
181
+ case '': // unitless
182
+ return value;
183
+ case 'vw':
184
+ return (value * viewportWidth) / 100;
185
+ case 'vh':
186
+ return (value * viewportHeight) / 100;
187
+ case '%':
188
+ return property === 'height'
189
+ ? (value * parentHeight) / 100
190
+ : (value * parentWidth) / 100;
191
+ case 'em':
192
+ return value * fontSize;
193
+ case 'rem':
194
+ return value * rootFontSize;
195
+ default:
196
+ return value;
197
+ }
198
+ }
199
+
200
+ /**
201
+ * pxから目的の単位に変換する
202
+ */
203
+ function fromPx(
204
+ pxValue: number,
205
+ toUnit: string,
206
+ context: UnitConversionContext
207
+ ): number {
208
+ const {
209
+ viewportWidth = 1920,
210
+ viewportHeight = 1080,
211
+ parentWidth = 1920,
212
+ parentHeight = 1080,
213
+ fontSize = 16,
214
+ rootFontSize = 16,
215
+ property = 'width',
216
+ } = context;
217
+
218
+ switch (toUnit) {
219
+ case 'px':
220
+ return Math.round(pxValue * 100) / 100;
221
+ case '': // unitless (line-height等)
222
+ return Math.round((pxValue / fontSize) * 100) / 100;
223
+ case 'vw':
224
+ return Math.round((pxValue / viewportWidth) * 100 * 100) / 100;
225
+ case 'vh':
226
+ return Math.round((pxValue / viewportHeight) * 100 * 100) / 100;
227
+ case '%':
228
+ const parentDim = property === 'height' ? parentHeight : parentWidth;
229
+ return Math.round((pxValue / parentDim) * 100 * 100) / 100;
230
+ case 'em':
231
+ return Math.round((pxValue / fontSize) * 100) / 100;
232
+ case 'rem':
233
+ return Math.round((pxValue / rootFontSize) * 100) / 100;
234
+ default:
235
+ return pxValue;
236
+ }
237
+ }
238
+
239
+ /**
240
+ * 単位を変換する際の値調整
241
+ * @param value 現在の数値
242
+ * @param fromUnit 変換元の単位
243
+ * @param toUnit 変換先の単位
244
+ * @param context 変換コンテキスト(ビューポートサイズ、親要素サイズ等)
245
+ * @returns 変換後の数値
246
+ */
247
+ export function convertUnit(
248
+ value: number,
249
+ fromUnit: string,
250
+ toUnit: string,
251
+ context: UnitConversionContext = {}
252
+ ): number {
253
+ // 同じ単位なら変換不要
254
+ if (fromUnit === toUnit) {
255
+ return value;
256
+ }
257
+
258
+ // 一度pxに変換してから目的の単位に変換
259
+ const pxValue = toPx(value, fromUnit, context);
260
+ return fromPx(pxValue, toUnit, context);
261
+ }
@@ -0,0 +1,392 @@
1
+ "use client";
2
+
3
+ /**
4
+ * エディタ共通の最上段バー。
5
+ *
6
+ * Figma風UIとPowerPoint風UIで「殻」は違っても、最上段でやることは同じ:
7
+ * 一覧へ戻る / UI切替 | デッキタイトル | 自動保存の状態 / 保存 / プレビュー / 共有 / 閉じる
8
+ * 同じ操作が2つのUIで別の場所・別の並びにあると、UIを切り替えるたびに探し直しになる。
9
+ * ここに1本化して、variant では**配色だけ**を変える。
10
+ *
11
+ * 【設計の約束】ここは見た目の殻。保存は effectiveSave、書き出しは lib/export、
12
+ * 閉じるは FVE の handleClose を呼ぶだけで、ロジックを二重に持たない。
13
+ * UI固有の操作(PPT: 元に戻す/検索/テーマ, Figma: ズーム/…メニュー)は
14
+ * leftExtra / rightExtra / menuExtra のスロットで受け取り、この並びを崩さない。
15
+ */
16
+
17
+ import { useState } from 'react';
18
+ import {
19
+ DropdownMenu,
20
+ DropdownMenuContent,
21
+ DropdownMenuItem,
22
+ DropdownMenuTrigger,
23
+ } from '../../../components/ui/dropdown-menu';
24
+ import { ArrowLeft, ChevronDown, Download, FileText, Loader2, PenTool, Play, Save, X } from 'lucide-react';
25
+ import { useEditorContext } from '../../EditorContext';
26
+ import { getCleanHtml } from '../../utils/html-utils';
27
+ import { startExport, waitForExport, downloadExport, type ExportFormat } from '../../../lib/export';
28
+ import { can } from '../../../io';
29
+
30
+ export type EditorSaveStatus = 'saved' | 'dirty' | 'saving' | 'error';
31
+
32
+ /**
33
+ * 配色。PowerPoint風は PPT_PALETTES[theme] をそのまま渡してもらう
34
+ * (PptChrome から import すると循環参照になるため、値は呼び出し側から受け取る)。
35
+ */
36
+ export type TopBarPalette = {
37
+ chrome: string;
38
+ text: string;
39
+ sub: string;
40
+ border: string;
41
+ hover: string;
42
+ activeBg: string;
43
+ control: string;
44
+ disabled: string;
45
+ };
46
+
47
+ /** Figma風の配色(editor-skin.css のパネル色に合わせる) */
48
+ const FIGMA_PALETTE: TopBarPalette = {
49
+ chrome: '#2c2c2c',
50
+ text: '#ffffff',
51
+ sub: '#9ca3af',
52
+ border: '#444444',
53
+ hover: '#444444',
54
+ activeBg: '#3a3a3a',
55
+ control: '#1e1e1e',
56
+ disabled: '#5f5f5f',
57
+ };
58
+
59
+ /** 共有ボタンの色だけはUIの「顔」なのでテーマごとに持つ */
60
+ const SHARE_COLOR = {
61
+ figma: { bg: '#0d99ff', fg: '#ffffff' },
62
+ ppt: { bg: '#E8912D', fg: '#33230a' },
63
+ } as const;
64
+
65
+ export interface EditorTopBarProps {
66
+ variant: 'figma' | 'ppt';
67
+ /** PowerPoint風の配色。省略時はFigma風の配色 */
68
+ palette?: TopBarPalette;
69
+ /** 中央に出すデッキ(スライド)のタイトル */
70
+ title?: string;
71
+ /** 編集中スライドの番号(1始まり)。プレビューで開くページにも使う */
72
+ pageNumber?: number;
73
+ saveStatus?: EditorSaveStatus;
74
+ /** 保存の実体。FVEの effectiveSave をそのまま渡す */
75
+ onSave: (html: string) => Promise<void> | void;
76
+ /** ページ設定(CSS/JS等)も併せて保存する場合に渡す */
77
+ onSaveSettings?: () => Promise<void>;
78
+ onClose: () => void;
79
+ /** マルチページキャンバスからの編集。閉じる=「キャンバスに戻る」になる */
80
+ isCanvasEditing?: boolean;
81
+ /** もう一方のUIへ切り替える */
82
+ onSwitchUi?: () => void;
83
+ /** 一覧へ戻る/UI切替の右。UI固有の操作(PPT: 元に戻す・やり直し) */
84
+ leftExtra?: React.ReactNode;
85
+ /** 保存状態の左。UI固有の操作(Figma: ズーム / PPT: 検索・テーマ) */
86
+ rightExtra?: React.ReactNode;
87
+ /** 共有と閉じるの間。UI固有のメニュー(Figma: …) */
88
+ menuExtra?: React.ReactNode;
89
+ /**
90
+ * 「一覧へ戻る」を出すか。
91
+ * 一覧が無い使い方(1ページずつ編集する等)では出さない。
92
+ */
93
+ showBackToList?: boolean;
94
+ }
95
+
96
+ export function EditorTopBar({
97
+ variant,
98
+ palette,
99
+ title,
100
+ pageNumber,
101
+ saveStatus = 'saved',
102
+ onSave,
103
+ onSaveSettings,
104
+ onClose,
105
+ isCanvasEditing,
106
+ onSwitchUi,
107
+ leftExtra,
108
+ rightExtra,
109
+ menuExtra,
110
+ showBackToList = true,
111
+ }: EditorTopBarProps) {
112
+ const pal = palette ?? FIGMA_PALETTE;
113
+ const share = SHARE_COLOR[variant];
114
+ const { getIframeDoc, html, saving, hasChanges } = useEditorContext();
115
+ const [exporting, setExporting] = useState<string | null>(null);
116
+
117
+ /**
118
+ * 手動保存。自動保存と同じ入口(effectiveSave)を通すので、
119
+ * 保存中フラグ・書き戻し・状態表示はすべて共通の1本で動く。
120
+ */
121
+ const handleSave = async () => {
122
+ try {
123
+ const doc = getIframeDoc();
124
+ // 保存してもエディタは閉じない。保存は作業の中断ではない
125
+ await onSave(doc ? getCleanHtml(doc) : html);
126
+ if (onSaveSettings) await onSaveSettings();
127
+ } catch (e) {
128
+ console.error('Save error:', e);
129
+ }
130
+ };
131
+
132
+ /** プレビュー = 表示専用(?clean)で今のページを別タブに開く */
133
+ const handlePreview = () => {
134
+ const page = pageNumber ?? 1;
135
+ window.open(`${window.location.origin}${window.location.pathname}#/${page}?clean`, '_blank');
136
+ };
137
+
138
+ /** 共有 = デッキ全体をPDF/PPTXへ書き出してダウンロード */
139
+ const runExport = async (format: ExportFormat, label: string) => {
140
+ setExporting(label);
141
+ try {
142
+ const jobId = await startExport(format);
143
+ const st = await waitForExport(jobId, () => undefined);
144
+ downloadExport(st);
145
+ } catch (e) {
146
+ window.alert(`書き出しに失敗しました: ${String(e).slice(0, 120)}`);
147
+ } finally {
148
+ setExporting(null);
149
+ }
150
+ };
151
+
152
+ const iconBtn = 'flex h-7 w-7 shrink-0 items-center justify-center rounded transition-colors disabled:opacity-40';
153
+ const textBtn = 'flex h-7 shrink-0 items-center gap-1.5 rounded-md border px-2.5 text-[11px] font-medium transition-colors';
154
+ const hoverIn = (e: React.MouseEvent<HTMLElement>) => {
155
+ if (!(e.currentTarget as HTMLButtonElement).disabled) e.currentTarget.style.backgroundColor = pal.hover;
156
+ };
157
+ const hoverOut = (e: React.MouseEvent<HTMLElement>) => {
158
+ e.currentTarget.style.backgroundColor = '';
159
+ };
160
+
161
+ return (
162
+ <div
163
+ data-editor-topbar={variant}
164
+ className={`flex shrink-0 items-center gap-2 border-b px-3 ${variant === 'ppt' ? 'h-10' : 'h-11'}`}
165
+ style={{ backgroundColor: pal.chrome, borderColor: pal.border, color: pal.text }}
166
+ >
167
+ {/* 左: 一覧へ戻る / UI切替 / UI固有の操作 */}
168
+ {showBackToList && (
169
+ <button
170
+ data-topbar="back"
171
+ onClick={onClose}
172
+ title="保存して一覧へ戻る"
173
+ className={textBtn}
174
+ style={{ borderColor: pal.border, color: pal.sub }}
175
+ onMouseEnter={hoverIn}
176
+ onMouseLeave={hoverOut}
177
+ >
178
+ <ArrowLeft className="h-3.5 w-3.5" />
179
+ 一覧へ
180
+ </button>
181
+ )}
182
+ {onSwitchUi && (
183
+ <button
184
+ data-topbar="switch-ui"
185
+ onClick={onSwitchUi}
186
+ title={
187
+ variant === 'ppt'
188
+ ? 'Figma風UIに切り替え(編集エンジンは共通)'
189
+ : 'PowerPoint風UIに切り替え(編集エンジンは共通)'
190
+ }
191
+ className={textBtn}
192
+ style={{ borderColor: pal.border, color: pal.sub }}
193
+ onMouseEnter={hoverIn}
194
+ onMouseLeave={hoverOut}
195
+ >
196
+ {variant === 'ppt' ? (
197
+ <PenTool className="h-3.5 w-3.5" />
198
+ ) : (
199
+ <span className="flex h-3.5 w-3.5 items-center justify-center rounded-[2px] bg-[#C43E1C] text-[9px] font-bold leading-none text-white">
200
+ P
201
+ </span>
202
+ )}
203
+ {variant === 'ppt' ? 'Figma風' : 'PPT風'}
204
+ </button>
205
+ )}
206
+ {leftExtra}
207
+
208
+ {/* 中央: デッキタイトル + スライド番号 */}
209
+ <div className="flex min-w-0 flex-1 items-center justify-center gap-2">
210
+ {pageNumber != null && (
211
+ <span className="shrink-0 font-mono text-[11px] tabular-nums" style={{ color: pal.sub }}>
212
+ {String(pageNumber).padStart(3, '0')}
213
+ </span>
214
+ )}
215
+ <span data-topbar="title" className="truncate text-[12.5px] font-semibold" style={{ color: pal.text }}>
216
+ {title || 'ビジュアルエディタ'}
217
+ </span>
218
+ </div>
219
+
220
+ {/* 右: UI固有の操作 / 自動保存 / 保存 / プレビュー / 共有 / …/ 閉じる */}
221
+ <div className="flex shrink-0 items-center gap-1.5">
222
+ {rightExtra}
223
+ <SaveStatusPill variant={variant} status={saveStatus} pal={pal} />
224
+ <button
225
+ data-topbar="save"
226
+ onClick={() => void handleSave()}
227
+ disabled={saving || !hasChanges}
228
+ title={saving ? '保存中…' : hasChanges ? '今すぐ保存' : '保存済みです(変更があると自動でも保存されます)'}
229
+ className={iconBtn}
230
+ style={{ color: saving || !hasChanges ? pal.disabled : pal.text }}
231
+ onMouseEnter={hoverIn}
232
+ onMouseLeave={hoverOut}
233
+ >
234
+ <Save className="h-4 w-4" />
235
+ </button>
236
+ {/* 書き出しは利用側の処理が要る。渡されていなければ出さない
237
+ (押すと失敗するボタンを残さないため) */}
238
+ {can("exportDeck") && (
239
+ <>
240
+ <button
241
+ data-topbar="preview"
242
+ onClick={handlePreview}
243
+ title="プレビュー(表示専用で別タブに開く)"
244
+ className={iconBtn}
245
+ style={{ color: pal.text }}
246
+ onMouseEnter={hoverIn}
247
+ onMouseLeave={hoverOut}
248
+ >
249
+ <Play className="h-4 w-4" />
250
+ </button>
251
+ <DropdownMenu>
252
+ <DropdownMenuTrigger asChild>
253
+ <button
254
+ data-topbar="share"
255
+ title="書き出して共有"
256
+ className="flex h-7 shrink-0 items-center gap-1 rounded-md px-2.5 text-[11px] font-semibold transition-opacity hover:opacity-90"
257
+ style={{ backgroundColor: share.bg, color: share.fg }}
258
+ >
259
+ {exporting && <Loader2 className="h-3 w-3 animate-spin" />}
260
+ {exporting ? `${exporting}を書き出し中…` : '共有'}
261
+ <ChevronDown className="h-3 w-3" />
262
+ </button>
263
+ </DropdownMenuTrigger>
264
+ <DropdownMenuContent
265
+ align="end"
266
+ data-topbar-menu="share"
267
+ className="w-56"
268
+ style={{ backgroundColor: pal.control, borderColor: pal.border, color: pal.text }}
269
+ >
270
+ <DropdownMenuItem
271
+ onClick={() => void runExport('pdf', 'PDF')}
272
+ className="cursor-pointer gap-2 focus:bg-white/10"
273
+ style={{ color: pal.text }}
274
+ >
275
+ <FileText className="h-4 w-4" />
276
+ PDF を書き出し
277
+ </DropdownMenuItem>
278
+ <DropdownMenuItem
279
+ onClick={() => void runExport('pptx', 'PPTX')}
280
+ className="cursor-pointer gap-2 focus:bg-white/10"
281
+ style={{ color: pal.text }}
282
+ >
283
+ <Download className="h-4 w-4" />
284
+ PowerPoint (画像貼り込み)
285
+ </DropdownMenuItem>
286
+ <DropdownMenuItem
287
+ onClick={() => void runExport('pptx-edit', 'PPTX')}
288
+ className="cursor-pointer gap-2 focus:bg-white/10"
289
+ style={{ color: pal.text }}
290
+ >
291
+ <Download className="h-4 w-4" />
292
+ PowerPoint (編集できる)
293
+ </DropdownMenuItem>
294
+ </DropdownMenuContent>
295
+ </DropdownMenu>
296
+ </>
297
+ )}
298
+ {menuExtra}
299
+ {isCanvasEditing ? (
300
+ <button
301
+ data-topbar="close"
302
+ onClick={onClose}
303
+ title="キャンバスに戻る"
304
+ className={textBtn}
305
+ style={{ borderColor: pal.border, color: pal.sub }}
306
+ onMouseEnter={hoverIn}
307
+ onMouseLeave={hoverOut}
308
+ >
309
+ <ArrowLeft className="h-3.5 w-3.5" />
310
+ キャンバスに戻る
311
+ </button>
312
+ ) : (
313
+ <button
314
+ data-topbar="close"
315
+ onClick={onClose}
316
+ title="エディタを閉じて一覧へ戻る"
317
+ className={iconBtn}
318
+ style={{ color: pal.sub }}
319
+ onMouseEnter={hoverIn}
320
+ onMouseLeave={hoverOut}
321
+ >
322
+ <X className="h-4 w-4" />
323
+ </button>
324
+ )}
325
+ </div>
326
+ </div>
327
+ );
328
+ }
329
+
330
+ /**
331
+ * 自動保存の状態表示。押せるものではないので、色と文言だけで状態を示す。
332
+ * 自動保存は常時オンなので「未保存」は「このあと保存される」の意味。
333
+ */
334
+ function SaveStatusPill({
335
+ variant,
336
+ status,
337
+ pal,
338
+ }: {
339
+ variant: 'figma' | 'ppt';
340
+ status: EditorSaveStatus;
341
+ pal: TopBarPalette;
342
+ }) {
343
+ const note = {
344
+ saved: '保存済み',
345
+ dirty: '保存待ち',
346
+ saving: '保存中…',
347
+ error: '保存できません',
348
+ }[status];
349
+
350
+ if (variant === 'ppt') {
351
+ // PowerPoint実機の「自動保存」ピル(トグルは無く、ノブが進み具合を示す)
352
+ const on = status !== 'error';
353
+ return (
354
+ <span
355
+ data-topbar="save-status"
356
+ data-save-status={status}
357
+ className="flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px]"
358
+ style={{ backgroundColor: pal.hover, color: status === 'error' ? '#ff6b6b' : pal.sub }}
359
+ title={`自動保存: ${note}(編集の手が止まると保存します)`}
360
+ >
361
+ 自動保存
362
+ <span
363
+ className="relative inline-block h-3 w-6 rounded-full"
364
+ style={{ backgroundColor: on ? '#C43E1C' : '#8a3a3a' }}
365
+ >
366
+ <span
367
+ className={`absolute right-0.5 top-0.5 h-2 w-2 rounded-full bg-white ${status === 'saving' ? 'animate-pulse' : ''}`}
368
+ />
369
+ </span>
370
+ {note}
371
+ </span>
372
+ );
373
+ }
374
+
375
+ const view = {
376
+ saved: { dot: 'bg-emerald-400', box: 'bg-emerald-400/10 text-emerald-300 ring-emerald-400/25', title: '自動保存済みです' },
377
+ dirty: { dot: 'bg-amber-400', box: 'bg-amber-400/15 text-amber-300 ring-amber-400/30', title: '変更が止まると自動で保存されます' },
378
+ saving: { dot: 'bg-sky-400 animate-pulse', box: 'bg-sky-400/15 text-sky-300 ring-sky-400/30', title: '保存しています' },
379
+ error: { dot: 'bg-rose-400', box: 'bg-rose-500/15 text-rose-300 ring-rose-400/30', title: '自動保存に失敗しました。保存ボタンで再試行してください' },
380
+ }[status];
381
+ return (
382
+ <span
383
+ data-topbar="save-status"
384
+ data-save-status={status}
385
+ title={view.title}
386
+ className={`inline-flex shrink-0 items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold tracking-wide ring-1 ring-inset ${view.box}`}
387
+ >
388
+ <span className={`h-1.5 w-1.5 rounded-full ${view.dot}`} />
389
+ {status === 'dirty' ? '未保存' : note}
390
+ </span>
391
+ );
392
+ }
@@ -0,0 +1,47 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Figma風UIの左パネル(2段構成)。
5
+ * 上: ページ切替(PagesPanel。スライドのプレビュー付き)
6
+ * 下: 現在ページのレイヤーツリー(EditorLayerPanel hideSlideList)
7
+ *
8
+ * 幅はこのラッパーが1本のハンドルで持つ。段ごとに幅・ハンドルを持たせると
9
+ * 上下で幅が食い違ったり、リサイズが二重になったりするため。
10
+ */
11
+
12
+ import { useEditorContext } from '../../EditorContext';
13
+ import { useResizablePanel } from '../../hooks/useResizablePanel';
14
+ import { EditorLayerPanel } from '../EditorLayerPanel';
15
+ import { PagesPanel } from './PagesPanel';
16
+ import { can } from '../../../io';
17
+
18
+ export function LeftPanel({ page }: { page: number }) {
19
+ // 一覧のサムネイルは利用側が描く(io.renderContent)。描けないなら段ごと出さない。
20
+ // Webページの用途ではページ切替を利用側のUIが持つことが多い
21
+ const { editorMode } = useEditorContext();
22
+ const showPages = can('renderContent') && editorMode !== 'webpage';
23
+ const { width, isDragging, resizeHandleProps } = useResizablePanel({
24
+ initialWidth: 256,
25
+ minWidth: 180,
26
+ maxWidth: 480,
27
+ direction: 'right',
28
+ storageKey: 'gg-editor:left-panel-width',
29
+ });
30
+
31
+ return (
32
+ <div
33
+ className="relative flex shrink-0 flex-col overflow-hidden"
34
+ style={{ width: `${width}px` }}
35
+ >
36
+ {showPages && <PagesPanel page={page} />}
37
+ {/* 残りの高さいっぱいにレイヤーツリー(自前でスクロールする) */}
38
+ <div className="flex min-h-0 flex-1">
39
+ <EditorLayerPanel hideSlideList />
40
+ </div>
41
+
42
+ {/* リサイズハンドル(全高。上下どちらの段からでも掴める) */}
43
+ <div {...resizeHandleProps} />
44
+ {isDragging && <div className="fixed inset-0 z-50 cursor-col-resize" />}
45
+ </div>
46
+ );
47
+ }