@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,560 @@
1
+ /**
2
+ * figma-export.ts
3
+ *
4
+ * Converts HTML elements to Figma clipboard format for pasting into Figma.
5
+ * Uses fig-kiwi's writeHTMLMessage for proper encoding with cached schema.
6
+ */
7
+
8
+ import { readHTMLMessage, writeHTMLMessage } from 'fig-kiwi';
9
+ import type { Schema } from 'kiwi-schema';
10
+
11
+ /**
12
+ * Cached schema from Figma paste (proper kiwi-schema Schema type)
13
+ */
14
+ let cachedFigKiwiSchema: Schema | null = null;
15
+
16
+ /**
17
+ * Cache the schema from a readHTMLMessage call
18
+ */
19
+ export function cacheFigKiwiSchema(schema: Schema): void {
20
+ cachedFigKiwiSchema = schema;
21
+ console.log('[figma-export] Cached fig-kiwi schema with', schema.definitions.length, 'definitions');
22
+ }
23
+
24
+ /**
25
+ * Get the cached fig-kiwi schema
26
+ */
27
+ export function getCachedFigKiwiSchema(): Schema | null {
28
+ return cachedFigKiwiSchema;
29
+ }
30
+
31
+ /**
32
+ * GUID generator
33
+ */
34
+ let guidCounter = 0;
35
+ const sessionID = Math.floor(Math.random() * 1000000000);
36
+
37
+ interface GUID {
38
+ sessionID: number;
39
+ localID: number;
40
+ }
41
+
42
+ function generateGuid(): GUID {
43
+ return {
44
+ sessionID,
45
+ localID: ++guidCounter,
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Reset GUID counter (call before each export operation)
51
+ */
52
+ function resetGuidCounter(): void {
53
+ guidCounter = 0;
54
+ }
55
+
56
+ /**
57
+ * Color type for Figma
58
+ */
59
+ interface Color {
60
+ r: number;
61
+ g: number;
62
+ b: number;
63
+ a: number;
64
+ }
65
+
66
+ /**
67
+ * Convert oklch to RGB
68
+ */
69
+ function oklchToRgb(l: number, c: number, h: number): { r: number; g: number; b: number } {
70
+ const hRad = h * Math.PI / 180;
71
+ const a = c * Math.cos(hRad);
72
+ const b = c * Math.sin(hRad);
73
+
74
+ const l_ = l + 0.3963377774 * a + 0.2158037573 * b;
75
+ const m_ = l - 0.1055613458 * a - 0.0638541728 * b;
76
+ const s_ = l - 0.0894841775 * a - 1.2914855480 * b;
77
+
78
+ const l3 = l_ * l_ * l_;
79
+ const m3 = m_ * m_ * m_;
80
+ const s3 = s_ * s_ * s_;
81
+
82
+ let r = +4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3;
83
+ let g = -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3;
84
+ let bVal = -0.0041960863 * l3 - 0.7034186147 * m3 + 1.7076147010 * s3;
85
+
86
+ r = Math.max(0, Math.min(1, r));
87
+ g = Math.max(0, Math.min(1, g));
88
+ bVal = Math.max(0, Math.min(1, bVal));
89
+
90
+ return { r, g, b: bVal };
91
+ }
92
+
93
+ /**
94
+ * Parse CSS color to Figma Color (0-1 range)
95
+ */
96
+ function parseCssColor(cssColor: string): Color | null {
97
+ if (!cssColor || cssColor === 'transparent' || cssColor === 'none') {
98
+ return null;
99
+ }
100
+
101
+ // Handle oklch
102
+ const oklchMatch = cssColor.match(/oklch\s*\(\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)\s*(?:\/\s*([\d.]+))?\s*\)/);
103
+ if (oklchMatch) {
104
+ const l = parseFloat(oklchMatch[1]);
105
+ const c = parseFloat(oklchMatch[2]);
106
+ const h = parseFloat(oklchMatch[3]);
107
+ const alpha = oklchMatch[4] !== undefined ? parseFloat(oklchMatch[4]) : 1;
108
+ const rgb = oklchToRgb(l, c, h);
109
+ return { ...rgb, a: alpha };
110
+ }
111
+
112
+ // Handle rgb/rgba
113
+ const rgbaMatch = cssColor.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/);
114
+ if (rgbaMatch) {
115
+ return {
116
+ r: parseInt(rgbaMatch[1]) / 255,
117
+ g: parseInt(rgbaMatch[2]) / 255,
118
+ b: parseInt(rgbaMatch[3]) / 255,
119
+ a: rgbaMatch[4] !== undefined ? parseFloat(rgbaMatch[4]) : 1,
120
+ };
121
+ }
122
+
123
+ // Handle hex colors
124
+ const hexMatch = cssColor.match(/^#([0-9a-f]{3,8})$/i);
125
+ if (hexMatch) {
126
+ const hex = hexMatch[1];
127
+ if (hex.length === 3) {
128
+ return {
129
+ r: parseInt(hex[0] + hex[0], 16) / 255,
130
+ g: parseInt(hex[1] + hex[1], 16) / 255,
131
+ b: parseInt(hex[2] + hex[2], 16) / 255,
132
+ a: 1,
133
+ };
134
+ } else if (hex.length === 6) {
135
+ return {
136
+ r: parseInt(hex.slice(0, 2), 16) / 255,
137
+ g: parseInt(hex.slice(2, 4), 16) / 255,
138
+ b: parseInt(hex.slice(4, 6), 16) / 255,
139
+ a: 1,
140
+ };
141
+ } else if (hex.length === 8) {
142
+ return {
143
+ r: parseInt(hex.slice(0, 2), 16) / 255,
144
+ g: parseInt(hex.slice(2, 4), 16) / 255,
145
+ b: parseInt(hex.slice(4, 6), 16) / 255,
146
+ a: parseInt(hex.slice(6, 8), 16) / 255,
147
+ };
148
+ }
149
+ }
150
+
151
+ // Named colors
152
+ const namedColors: Record<string, Color> = {
153
+ 'white': { r: 1, g: 1, b: 1, a: 1 },
154
+ 'black': { r: 0, g: 0, b: 0, a: 1 },
155
+ 'red': { r: 1, g: 0, b: 0, a: 1 },
156
+ 'green': { r: 0, g: 0.502, b: 0, a: 1 },
157
+ 'blue': { r: 0, g: 0, b: 1, a: 1 },
158
+ 'gray': { r: 0.502, g: 0.502, b: 0.502, a: 1 },
159
+ 'grey': { r: 0.502, g: 0.502, b: 0.502, a: 1 },
160
+ };
161
+
162
+ const lowerColor = cssColor.toLowerCase();
163
+ if (namedColors[lowerColor]) {
164
+ return namedColors[lowerColor];
165
+ }
166
+
167
+ console.warn('[figma-export] Could not parse color:', cssColor);
168
+ return null;
169
+ }
170
+
171
+ function parseCssNumeric(value: string): number {
172
+ if (!value) return 0;
173
+ const num = parseFloat(value);
174
+ return isNaN(num) ? 0 : num;
175
+ }
176
+
177
+ function detectFigmaType(el: HTMLElement): string {
178
+ const tagName = el.tagName.toLowerCase();
179
+ const figmaType = el.getAttribute('data-figma-type');
180
+
181
+ if (figmaType) return figmaType;
182
+
183
+ if (tagName === 'span' || tagName === 'p' || tagName === 'h1' || tagName === 'h2' || tagName === 'h3') {
184
+ return 'TEXT';
185
+ }
186
+
187
+ if (el.children.length > 0) return 'FRAME';
188
+
189
+ const style = getComputedStyle(el);
190
+ if (style.borderRadius === '50%' || style.borderRadius === '9999px') {
191
+ return 'ELLIPSE';
192
+ }
193
+
194
+ return 'RECTANGLE';
195
+ }
196
+
197
+ function getFillPaints(el: HTMLElement): Record<string, unknown>[] | undefined {
198
+ const style = getComputedStyle(el);
199
+ const paints: Record<string, unknown>[] = [];
200
+
201
+ const bgColor = style.backgroundColor;
202
+ if (bgColor && bgColor !== 'transparent' && bgColor !== 'rgba(0, 0, 0, 0)') {
203
+ const color = parseCssColor(bgColor);
204
+ if (color) {
205
+ paints.push({
206
+ type: 'SOLID',
207
+ visible: true,
208
+ opacity: 1,
209
+ color,
210
+ });
211
+ }
212
+ }
213
+
214
+ return paints.length > 0 ? paints : undefined;
215
+ }
216
+
217
+ function getStrokePaints(el: HTMLElement): { paints?: Record<string, unknown>[]; weight?: number } {
218
+ const style = getComputedStyle(el);
219
+ const borderColor = style.borderColor || style.borderTopColor;
220
+ const borderWidth = parseCssNumeric(style.borderWidth || style.borderTopWidth);
221
+
222
+ if (borderWidth > 0 && borderColor && borderColor !== 'transparent') {
223
+ const color = parseCssColor(borderColor);
224
+ if (color) {
225
+ return {
226
+ paints: [{
227
+ type: 'SOLID',
228
+ visible: true,
229
+ opacity: 1,
230
+ color,
231
+ }],
232
+ weight: borderWidth,
233
+ };
234
+ }
235
+ }
236
+
237
+ return {};
238
+ }
239
+
240
+ function getCornerRadius(el: HTMLElement): Record<string, number> {
241
+ const style = getComputedStyle(el);
242
+
243
+ const tl = parseCssNumeric(style.borderTopLeftRadius);
244
+ const tr = parseCssNumeric(style.borderTopRightRadius);
245
+ const br = parseCssNumeric(style.borderBottomRightRadius);
246
+ const bl = parseCssNumeric(style.borderBottomLeftRadius);
247
+
248
+ if (tl === tr && tr === br && br === bl) {
249
+ return tl > 0 ? { cornerRadius: tl } : {};
250
+ }
251
+
252
+ return {
253
+ rectangleTopLeftCornerRadius: tl,
254
+ rectangleTopRightCornerRadius: tr,
255
+ rectangleBottomRightCornerRadius: br,
256
+ rectangleBottomLeftCornerRadius: bl,
257
+ };
258
+ }
259
+
260
+ function getEffects(el: HTMLElement): Record<string, unknown>[] | undefined {
261
+ const style = getComputedStyle(el);
262
+ const boxShadow = style.boxShadow;
263
+
264
+ if (!boxShadow || boxShadow === 'none') return undefined;
265
+
266
+ const effects: Record<string, unknown>[] = [];
267
+
268
+ const shadowMatch = boxShadow.match(/(rgba?\([^)]+\)|#[0-9a-f]+)\s+([-\d.]+)px\s+([-\d.]+)px\s+([-\d.]+)px/i);
269
+ if (shadowMatch) {
270
+ const color = parseCssColor(shadowMatch[1]);
271
+ if (color) {
272
+ effects.push({
273
+ type: 'DROP_SHADOW',
274
+ visible: true,
275
+ radius: parseCssNumeric(shadowMatch[4]),
276
+ color,
277
+ offset: {
278
+ x: parseCssNumeric(shadowMatch[2]),
279
+ y: parseCssNumeric(shadowMatch[3]),
280
+ },
281
+ });
282
+ }
283
+ }
284
+
285
+ return effects.length > 0 ? effects : undefined;
286
+ }
287
+
288
+ function getTextProperties(el: HTMLElement): Record<string, unknown> {
289
+ const style = getComputedStyle(el);
290
+ const text = el.textContent || '';
291
+
292
+ if (!text.trim()) return {};
293
+
294
+ const fontSize = parseCssNumeric(style.fontSize);
295
+ const fontFamily = style.fontFamily.replace(/["']/g, '').split(',')[0].trim();
296
+ const fontWeight = style.fontWeight;
297
+
298
+ let fontStyle = 'Regular';
299
+ if (fontWeight === '700' || fontWeight === 'bold') fontStyle = 'Bold';
300
+ else if (fontWeight === '500' || fontWeight === '600') fontStyle = 'Medium';
301
+ else if (fontWeight === '300') fontStyle = 'Light';
302
+
303
+ let textAlignHorizontal: string | undefined;
304
+ switch (style.textAlign) {
305
+ case 'center': textAlignHorizontal = 'CENTER'; break;
306
+ case 'right': textAlignHorizontal = 'RIGHT'; break;
307
+ case 'justify': textAlignHorizontal = 'JUSTIFIED'; break;
308
+ default: textAlignHorizontal = 'LEFT';
309
+ }
310
+
311
+ return {
312
+ fontSize,
313
+ fontName: {
314
+ family: fontFamily,
315
+ style: fontStyle,
316
+ postscript: `${fontFamily}-${fontStyle}`,
317
+ },
318
+ textData: { characters: text },
319
+ textAlignHorizontal,
320
+ };
321
+ }
322
+
323
+ function getAutoLayoutProperties(el: HTMLElement): Record<string, unknown> {
324
+ const style = getComputedStyle(el);
325
+
326
+ if (style.display !== 'flex') return {};
327
+
328
+ const props: Record<string, unknown> = {};
329
+
330
+ props.stackMode = style.flexDirection === 'column' || style.flexDirection === 'column-reverse'
331
+ ? 'VERTICAL'
332
+ : 'HORIZONTAL';
333
+
334
+ const gap = parseCssNumeric(style.gap);
335
+ if (gap > 0) props.stackSpacing = gap;
336
+
337
+ const paddingTop = parseCssNumeric(style.paddingTop);
338
+ const paddingRight = parseCssNumeric(style.paddingRight);
339
+ const paddingBottom = parseCssNumeric(style.paddingBottom);
340
+ const paddingLeft = parseCssNumeric(style.paddingLeft);
341
+
342
+ if (paddingTop === paddingRight && paddingRight === paddingBottom && paddingBottom === paddingLeft) {
343
+ if (paddingTop > 0) props.stackPadding = paddingTop;
344
+ } else {
345
+ if (paddingTop > 0 || paddingBottom > 0) props.stackVerticalPadding = paddingTop;
346
+ if (paddingLeft > 0 || paddingRight > 0) props.stackHorizontalPadding = paddingLeft;
347
+ }
348
+
349
+ switch (style.justifyContent) {
350
+ case 'center': props.stackJustify = 'CENTER'; break;
351
+ case 'flex-end': props.stackJustify = 'MAX'; break;
352
+ case 'space-between': props.stackJustify = 'SPACE_EVENLY'; break;
353
+ default: props.stackJustify = 'MIN';
354
+ }
355
+
356
+ switch (style.alignItems) {
357
+ case 'center': props.stackCounterAlign = 'CENTER'; break;
358
+ case 'flex-end': props.stackCounterAlign = 'MAX'; break;
359
+ case 'stretch': props.stackCounterAlign = 'STRETCH'; break;
360
+ default: props.stackCounterAlign = 'MIN';
361
+ }
362
+
363
+ return props;
364
+ }
365
+
366
+ function elementToNodeChange(
367
+ el: HTMLElement,
368
+ parentGuid?: GUID,
369
+ index: number = 0
370
+ ): Record<string, unknown> {
371
+ const guid = generateGuid();
372
+ const style = getComputedStyle(el);
373
+ const rect = el.getBoundingClientRect();
374
+
375
+ let x = 0;
376
+ let y = 0;
377
+
378
+ if (el.parentElement) {
379
+ const parentRect = el.parentElement.getBoundingClientRect();
380
+ x = rect.left - parentRect.left;
381
+ y = rect.top - parentRect.top;
382
+ }
383
+
384
+ const type = detectFigmaType(el);
385
+
386
+ const nodeChange: Record<string, unknown> = {
387
+ guid,
388
+ phase: 'CREATED',
389
+ type,
390
+ name: el.getAttribute('data-name') || el.getAttribute('data-element-id') || type.toLowerCase(),
391
+ visible: style.display !== 'none' && style.visibility !== 'hidden',
392
+ size: {
393
+ x: rect.width,
394
+ y: rect.height,
395
+ },
396
+ transform: {
397
+ m00: 1,
398
+ m01: 0,
399
+ m02: x,
400
+ m10: 0,
401
+ m11: 1,
402
+ m12: y,
403
+ },
404
+ };
405
+
406
+ if (parentGuid) {
407
+ nodeChange.parentIndex = {
408
+ guid: parentGuid,
409
+ position: index.toString(),
410
+ };
411
+ }
412
+
413
+ const opacity = parseFloat(style.opacity);
414
+ if (opacity < 1) nodeChange.opacity = opacity;
415
+
416
+ const fills = getFillPaints(el);
417
+ if (fills) {
418
+ nodeChange.fillPaints = fills;
419
+ } else if (type === 'FRAME' || type === 'RECTANGLE') {
420
+ nodeChange.fillPaints = [{
421
+ type: 'SOLID',
422
+ visible: true,
423
+ opacity: 1,
424
+ color: { r: 1, g: 1, b: 1, a: 1 },
425
+ }];
426
+ }
427
+
428
+ const strokes = getStrokePaints(el);
429
+ if (strokes.paints) {
430
+ nodeChange.strokePaints = strokes.paints;
431
+ nodeChange.strokeWeight = strokes.weight;
432
+ }
433
+
434
+ Object.assign(nodeChange, getCornerRadius(el));
435
+
436
+ const effects = getEffects(el);
437
+ if (effects) nodeChange.effects = effects;
438
+
439
+ if (type === 'TEXT') {
440
+ Object.assign(nodeChange, getTextProperties(el));
441
+ const textColor = parseCssColor(style.color);
442
+ if (textColor) {
443
+ nodeChange.fillPaints = [{
444
+ type: 'SOLID',
445
+ visible: true,
446
+ opacity: 1,
447
+ color: textColor,
448
+ }];
449
+ }
450
+ }
451
+
452
+ if (type === 'FRAME') {
453
+ Object.assign(nodeChange, getAutoLayoutProperties(el));
454
+ }
455
+
456
+ return nodeChange;
457
+ }
458
+
459
+ function elementTreeToNodeChanges(
460
+ el: HTMLElement,
461
+ parentGuid?: GUID,
462
+ index: number = 0
463
+ ): Record<string, unknown>[] {
464
+ const nodes: Record<string, unknown>[] = [];
465
+
466
+ const nodeChange = elementToNodeChange(el, parentGuid, index);
467
+ nodes.push(nodeChange);
468
+
469
+ const children = Array.from(el.children).filter(
470
+ (child): child is HTMLElement => child instanceof HTMLElement
471
+ );
472
+
473
+ for (let i = 0; i < children.length; i++) {
474
+ const childNodes = elementTreeToNodeChanges(children[i], nodeChange.guid as GUID, i);
475
+ nodes.push(...childNodes);
476
+ }
477
+
478
+ return nodes;
479
+ }
480
+
481
+ /**
482
+ * Export HTML elements to Figma clipboard format
483
+ */
484
+ export async function copyElementsToFigma(elements: HTMLElement[]): Promise<{
485
+ success: boolean;
486
+ error?: string;
487
+ nodeCount?: number;
488
+ }> {
489
+ try {
490
+ // Check if schema is available
491
+ if (!cachedFigKiwiSchema) {
492
+ return {
493
+ success: false,
494
+ error: 'スキーマがキャッシュされていません。先にFigmaからペーストしてスキーマを取得してください。',
495
+ };
496
+ }
497
+
498
+ resetGuidCounter();
499
+
500
+ // Convert all elements to NodeChanges
501
+ const allNodes: Record<string, unknown>[] = [];
502
+
503
+ for (let i = 0; i < elements.length; i++) {
504
+ const nodes = elementTreeToNodeChanges(elements[i], undefined, i);
505
+ allNodes.push(...nodes);
506
+ }
507
+
508
+ console.log('[figma-export] Converted', allNodes.length, 'nodes');
509
+ console.log('[figma-export] First node:', JSON.stringify(allNodes[0], null, 2));
510
+
511
+ // Create message using fig-kiwi's expected format
512
+ const message = {
513
+ type: 'NODE_CHANGES',
514
+ sessionID: sessionID,
515
+ pasteID: Math.floor(Math.random() * 1000000000),
516
+ nodeChanges: allNodes,
517
+ };
518
+
519
+ // Use fig-kiwi's writeHTMLMessage with the cached schema
520
+ const clipboardHtml = writeHTMLMessage({
521
+ meta: {
522
+ fileKey: 'web-export',
523
+ pasteID: message.pasteID,
524
+ dataType: 'scene',
525
+ },
526
+ schema: cachedFigKiwiSchema,
527
+ message: message as any, // Type assertion needed due to fig-kiwi's strict types
528
+ });
529
+
530
+ console.log('[figma-export] Generated clipboard HTML length:', clipboardHtml.length);
531
+
532
+ // Copy to clipboard
533
+ await navigator.clipboard.write([
534
+ new ClipboardItem({
535
+ 'text/html': new Blob([clipboardHtml], { type: 'text/html' }),
536
+ 'text/plain': new Blob([clipboardHtml], { type: 'text/plain' }),
537
+ }),
538
+ ]);
539
+
540
+ console.log('[figma-export] Copied to clipboard successfully');
541
+
542
+ return {
543
+ success: true,
544
+ nodeCount: allNodes.length,
545
+ };
546
+ } catch (error) {
547
+ console.error('[figma-export] Failed to export:', error);
548
+ return {
549
+ success: false,
550
+ error: error instanceof Error ? error.message : 'Unknown error',
551
+ };
552
+ }
553
+ }
554
+
555
+ /**
556
+ * Check if Figma export is available (schema must be cached)
557
+ */
558
+ export function isFigmaExportAvailable(): boolean {
559
+ return cachedFigKiwiSchema !== null;
560
+ }