@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,583 @@
1
+ /**
2
+ * figma-kiwi-encoder.ts
3
+ *
4
+ * Encodes data into Figma's kiwi binary format for clipboard export.
5
+ * This is the reverse of figma-kiwi-decoder.ts
6
+ */
7
+
8
+ import * as pako from 'pako';
9
+
10
+ /**
11
+ * WriteBuffer for encoding binary data in kiwi format
12
+ */
13
+ export class WriteBuffer {
14
+ private chunks: Uint8Array[] = [];
15
+ private currentChunk: Uint8Array;
16
+ private index: number = 0;
17
+ private static CHUNK_SIZE = 4096;
18
+
19
+ constructor() {
20
+ this.currentChunk = new Uint8Array(WriteBuffer.CHUNK_SIZE);
21
+ }
22
+
23
+ private ensureCapacity(bytes: number): void {
24
+ if (this.index + bytes > this.currentChunk.length) {
25
+ // Save current chunk and allocate new one
26
+ this.chunks.push(this.currentChunk.slice(0, this.index));
27
+ this.currentChunk = new Uint8Array(Math.max(WriteBuffer.CHUNK_SIZE, bytes));
28
+ this.index = 0;
29
+ }
30
+ }
31
+
32
+ writeByte(value: number): void {
33
+ this.ensureCapacity(1);
34
+ this.currentChunk[this.index++] = value & 0xFF;
35
+ }
36
+
37
+ writeVarUint(value: number): void {
38
+ value = value >>> 0; // Convert to unsigned 32-bit
39
+ while (value > 0x7F) {
40
+ this.writeByte((value & 0x7F) | 0x80);
41
+ value >>>= 7;
42
+ }
43
+ this.writeByte(value);
44
+ }
45
+
46
+ writeVarInt(value: number): void {
47
+ // Zig-zag encoding
48
+ this.writeVarUint(value < 0 ? (~value << 1) | 1 : value << 1);
49
+ }
50
+
51
+ /**
52
+ * Write null-terminated UTF-8 string (kiwi format)
53
+ */
54
+ writeString(value: string): void {
55
+ for (let i = 0; i < value.length; i++) {
56
+ let codePoint = value.charCodeAt(i);
57
+
58
+ // Handle surrogate pairs
59
+ if (codePoint >= 0xD800 && codePoint <= 0xDBFF && i + 1 < value.length) {
60
+ const low = value.charCodeAt(i + 1);
61
+ if (low >= 0xDC00 && low <= 0xDFFF) {
62
+ codePoint = 0x10000 + ((codePoint - 0xD800) << 10) + (low - 0xDC00);
63
+ i++;
64
+ }
65
+ }
66
+
67
+ // Encode UTF-8
68
+ if (codePoint < 0x80) {
69
+ this.writeByte(codePoint);
70
+ } else if (codePoint < 0x800) {
71
+ this.writeByte(0xC0 | (codePoint >> 6));
72
+ this.writeByte(0x80 | (codePoint & 0x3F));
73
+ } else if (codePoint < 0x10000) {
74
+ this.writeByte(0xE0 | (codePoint >> 12));
75
+ this.writeByte(0x80 | ((codePoint >> 6) & 0x3F));
76
+ this.writeByte(0x80 | (codePoint & 0x3F));
77
+ } else {
78
+ this.writeByte(0xF0 | (codePoint >> 18));
79
+ this.writeByte(0x80 | ((codePoint >> 12) & 0x3F));
80
+ this.writeByte(0x80 | ((codePoint >> 6) & 0x3F));
81
+ this.writeByte(0x80 | (codePoint & 0x3F));
82
+ }
83
+ }
84
+ // Null terminator
85
+ this.writeByte(0);
86
+ }
87
+
88
+ /**
89
+ * Write variable-length float (kiwi optimization format)
90
+ */
91
+ writeVarFloat(value: number): void {
92
+ if (value === 0) {
93
+ this.writeByte(0);
94
+ return;
95
+ }
96
+
97
+ // Reinterpret as 32-bit integer
98
+ const float32 = new Float32Array(1);
99
+ const int32 = new Int32Array(float32.buffer);
100
+ float32[0] = value;
101
+ let bits = int32[0];
102
+
103
+ // Rotate exponent to low bits for better compression
104
+ bits = (bits >>> 23) | (bits << 9);
105
+
106
+ // Write 4 bytes little-endian
107
+ this.ensureCapacity(4);
108
+ this.currentChunk[this.index++] = bits & 0xFF;
109
+ this.currentChunk[this.index++] = (bits >> 8) & 0xFF;
110
+ this.currentChunk[this.index++] = (bits >> 16) & 0xFF;
111
+ this.currentChunk[this.index++] = (bits >> 24) & 0xFF;
112
+ }
113
+
114
+ /**
115
+ * Write length-prefixed byte array
116
+ */
117
+ writeByteArray(value: Uint8Array): void {
118
+ this.writeVarUint(value.length);
119
+ this.ensureCapacity(value.length);
120
+ this.currentChunk.set(value, this.index);
121
+ this.index += value.length;
122
+ }
123
+
124
+ /**
125
+ * Write a double (8 bytes, little endian)
126
+ */
127
+ writeDouble(value: number): void {
128
+ const float64 = new Float64Array(1);
129
+ float64[0] = value;
130
+ const bytes = new Uint8Array(float64.buffer);
131
+ this.ensureCapacity(8);
132
+ for (let i = 0; i < 8; i++) {
133
+ this.currentChunk[this.index++] = bytes[i];
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Get the final buffer
139
+ */
140
+ toUint8Array(): Uint8Array {
141
+ // Combine all chunks
142
+ const totalLength = this.chunks.reduce((sum, c) => sum + c.length, 0) + this.index;
143
+ const result = new Uint8Array(totalLength);
144
+ let offset = 0;
145
+ for (const chunk of this.chunks) {
146
+ result.set(chunk, offset);
147
+ offset += chunk.length;
148
+ }
149
+ result.set(this.currentChunk.slice(0, this.index), offset);
150
+ return result;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Schema definition types (matching decoder)
156
+ */
157
+ interface Field {
158
+ name: string;
159
+ type: string | number | null;
160
+ isArray: boolean;
161
+ value: number; // field ID for MESSAGE types
162
+ }
163
+
164
+ interface Definition {
165
+ name: string;
166
+ kind: string; // 'ENUM' | 'STRUCT' | 'MESSAGE'
167
+ fields: Field[];
168
+ }
169
+
170
+ export interface Schema {
171
+ definitions: Definition[];
172
+ }
173
+
174
+ /**
175
+ * Bundled default schema (captured from Figma paste)
176
+ * This is the raw compressed schema bytes as base64
177
+ * TODO: Replace with actual schema after capturing from Figma paste
178
+ */
179
+ const BUNDLED_SCHEMA_BASE64: string | null = null; // Will be set after capture
180
+
181
+ /**
182
+ * Cached schema from previous Figma paste operation
183
+ */
184
+ let cachedSchema: Schema | null = null;
185
+ let cachedSchemaRaw: Uint8Array | null = null;
186
+ let bundledSchemaInitialized = false;
187
+
188
+ /**
189
+ * Cache the schema from a decode operation for later use in encoding
190
+ */
191
+ export function cacheSchema(schema: Schema, rawSchemaBytes?: Uint8Array): void {
192
+ cachedSchema = schema;
193
+ if (rawSchemaBytes) {
194
+ cachedSchemaRaw = rawSchemaBytes;
195
+ }
196
+ console.log('[figma-kiwi-encoder] Schema cached with', schema.definitions.length, 'definitions');
197
+ }
198
+
199
+ /**
200
+ * Get the cached schema
201
+ */
202
+ export function getCachedSchema(): Schema | null {
203
+ return cachedSchema;
204
+ }
205
+
206
+ /**
207
+ * Get the cached raw schema bytes
208
+ */
209
+ export function getCachedSchemaRaw(): Uint8Array | null {
210
+ return cachedSchemaRaw;
211
+ }
212
+
213
+ /**
214
+ * Encode a value based on its type
215
+ */
216
+ function encodeValue(
217
+ bb: WriteBuffer,
218
+ value: unknown,
219
+ typeName: string | null,
220
+ isArray: boolean,
221
+ defMap: Map<string, Definition>
222
+ ): void {
223
+ if (isArray) {
224
+ const arr = value as unknown[];
225
+ bb.writeVarUint(arr.length);
226
+ for (const item of arr) {
227
+ encodeValue(bb, item, typeName, false, defMap);
228
+ }
229
+ return;
230
+ }
231
+
232
+ switch (typeName) {
233
+ case 'bool':
234
+ bb.writeByte((value as boolean) ? 1 : 0);
235
+ break;
236
+ case 'byte':
237
+ bb.writeByte(value as number);
238
+ break;
239
+ case 'int':
240
+ case 'int64':
241
+ bb.writeVarInt(value as number);
242
+ break;
243
+ case 'uint':
244
+ case 'uint64':
245
+ bb.writeVarUint(value as number);
246
+ break;
247
+ case 'float':
248
+ bb.writeVarFloat(value as number);
249
+ break;
250
+ case 'double':
251
+ bb.writeDouble(value as number);
252
+ break;
253
+ case 'string':
254
+ bb.writeString(value as string);
255
+ break;
256
+ case 'bytes':
257
+ bb.writeByteArray(value as Uint8Array);
258
+ break;
259
+ default:
260
+ // Check if it's a reference to another definition
261
+ if (typeName && defMap.has(typeName)) {
262
+ const def = defMap.get(typeName)!;
263
+ if (def.kind === 'ENUM') {
264
+ // Find the enum value by name
265
+ const enumName = value as string;
266
+ for (const field of def.fields) {
267
+ if (field.name === enumName) {
268
+ bb.writeVarUint(field.value);
269
+ return;
270
+ }
271
+ }
272
+ // Default to 0 if not found
273
+ console.warn(`[figma-kiwi-encoder] Unknown enum value: ${enumName} in ${typeName}`);
274
+ bb.writeVarUint(0);
275
+ } else {
276
+ encodeStruct(bb, value as Record<string, unknown>, def, defMap);
277
+ }
278
+ } else {
279
+ console.warn(`[figma-kiwi-encoder] Unknown type: ${typeName}`);
280
+ }
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Encode a struct or message
286
+ */
287
+ function encodeStruct(
288
+ bb: WriteBuffer,
289
+ obj: Record<string, unknown>,
290
+ def: Definition,
291
+ defMap: Map<string, Definition>
292
+ ): void {
293
+ if (def.kind === 'STRUCT') {
294
+ // STRUCT: all fields in order
295
+ for (const field of def.fields) {
296
+ const value = obj[field.name];
297
+ if (value !== undefined && value !== null) {
298
+ encodeValue(bb, value, field.type as string, field.isArray, defMap);
299
+ } else {
300
+ // Write default value
301
+ encodeDefaultValue(bb, field.type as string, field.isArray, defMap);
302
+ }
303
+ }
304
+ } else if (def.kind === 'MESSAGE') {
305
+ // MESSAGE: optional fields with field IDs
306
+ for (const field of def.fields) {
307
+ const value = obj[field.name];
308
+ if (value !== undefined && value !== null) {
309
+ bb.writeVarUint(field.value); // field ID
310
+ encodeValue(bb, value, field.type as string, field.isArray, defMap);
311
+ }
312
+ }
313
+ // End of message marker
314
+ bb.writeVarUint(0);
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Encode default value for a type
320
+ */
321
+ function encodeDefaultValue(
322
+ bb: WriteBuffer,
323
+ typeName: string,
324
+ isArray: boolean,
325
+ defMap: Map<string, Definition>
326
+ ): void {
327
+ if (isArray) {
328
+ bb.writeVarUint(0); // empty array
329
+ return;
330
+ }
331
+
332
+ switch (typeName) {
333
+ case 'bool':
334
+ bb.writeByte(0);
335
+ break;
336
+ case 'byte':
337
+ bb.writeByte(0);
338
+ break;
339
+ case 'int':
340
+ case 'int64':
341
+ bb.writeVarInt(0);
342
+ break;
343
+ case 'uint':
344
+ case 'uint64':
345
+ bb.writeVarUint(0);
346
+ break;
347
+ case 'float':
348
+ bb.writeVarFloat(0);
349
+ break;
350
+ case 'double':
351
+ bb.writeDouble(0);
352
+ break;
353
+ case 'string':
354
+ bb.writeString('');
355
+ break;
356
+ case 'bytes':
357
+ bb.writeByteArray(new Uint8Array(0));
358
+ break;
359
+ default:
360
+ if (typeName && defMap.has(typeName)) {
361
+ const def = defMap.get(typeName)!;
362
+ if (def.kind === 'ENUM') {
363
+ bb.writeVarUint(0);
364
+ } else if (def.kind === 'MESSAGE') {
365
+ bb.writeVarUint(0); // empty message
366
+ } else {
367
+ encodeStruct(bb, {}, def, defMap);
368
+ }
369
+ }
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Encode a Message with nodeChanges
375
+ */
376
+ export function encodeMessage(
377
+ nodeChanges: Record<string, unknown>[],
378
+ schema: Schema
379
+ ): Uint8Array {
380
+ const bb = new WriteBuffer();
381
+
382
+ // Build definition map
383
+ const defMap = new Map<string, Definition>();
384
+ for (const def of schema.definitions) {
385
+ defMap.set(def.name, def);
386
+ }
387
+
388
+ // Find Message definition
389
+ const messageDef = defMap.get('Message');
390
+ if (!messageDef) {
391
+ throw new Error('Message definition not found in schema');
392
+ }
393
+
394
+ // Create message object
395
+ const sessionID = Math.floor(Math.random() * 1000000000);
396
+ const pasteID = Math.floor(Math.random() * 1000000000);
397
+
398
+ const message: Record<string, unknown> = {
399
+ type: 'NODE_CHANGES',
400
+ sessionID: sessionID,
401
+ pasteID: pasteID,
402
+ nodeChanges: nodeChanges,
403
+ };
404
+
405
+ console.log('[figma-kiwi-encoder] Message:', { type: 'NODE_CHANGES', sessionID, pasteID, nodeChangesCount: nodeChanges.length });
406
+
407
+ // Encode the message
408
+ encodeStruct(bb, message, messageDef, defMap);
409
+
410
+ return bb.toUint8Array();
411
+ }
412
+
413
+ /**
414
+ * Create fig-kiwi archive format
415
+ */
416
+ export function createArchive(schemaCompressed: Uint8Array, dataCompressed: Uint8Array): Uint8Array {
417
+ const prelude = new TextEncoder().encode('fig-kiwi');
418
+ const version = 15; // Figma's current version
419
+
420
+ // Calculate total size
421
+ const totalSize = prelude.length + 4 + 4 + schemaCompressed.length + 4 + dataCompressed.length;
422
+ const result = new Uint8Array(totalSize);
423
+ const view = new DataView(result.buffer);
424
+
425
+ let offset = 0;
426
+
427
+ // Write prelude
428
+ result.set(prelude, offset);
429
+ offset += prelude.length;
430
+
431
+ // Write version (4 bytes, little endian)
432
+ view.setUint32(offset, version, true);
433
+ offset += 4;
434
+
435
+ // Write schema chunk
436
+ view.setUint32(offset, schemaCompressed.length, true);
437
+ offset += 4;
438
+ result.set(schemaCompressed, offset);
439
+ offset += schemaCompressed.length;
440
+
441
+ // Write data chunk
442
+ view.setUint32(offset, dataCompressed.length, true);
443
+ offset += 4;
444
+ result.set(dataCompressed, offset);
445
+ offset += dataCompressed.length;
446
+
447
+ return result;
448
+ }
449
+
450
+ /**
451
+ * Encode schema to binary format
452
+ */
453
+ export function encodeBinarySchema(schema: Schema): Uint8Array {
454
+ const bb = new WriteBuffer();
455
+
456
+ // Write definition count
457
+ bb.writeVarUint(schema.definitions.length);
458
+
459
+ // Kind mapping
460
+ const kindToIndex: Record<string, number> = {
461
+ 'ENUM': 0,
462
+ 'STRUCT': 1,
463
+ 'MESSAGE': 2,
464
+ };
465
+
466
+ // Build name to index map for type references
467
+ const nameToIndex = new Map<string, number>();
468
+ schema.definitions.forEach((def, index) => {
469
+ nameToIndex.set(def.name, index);
470
+ });
471
+
472
+ // Extended types mapping (reverse)
473
+ const typeNameToIndex: Record<string, number> = {
474
+ 'bool': -1,
475
+ 'byte': -2,
476
+ 'int': -3,
477
+ 'uint': -4,
478
+ 'float': -5,
479
+ 'string': -6,
480
+ 'int64': -7,
481
+ 'uint64': -8,
482
+ 'double': -9,
483
+ 'bytes': -10,
484
+ };
485
+
486
+ // Write each definition
487
+ for (const def of schema.definitions) {
488
+ bb.writeString(def.name);
489
+ bb.writeByte(kindToIndex[def.kind] ?? 2);
490
+ bb.writeVarUint(def.fields.length);
491
+
492
+ for (const field of def.fields) {
493
+ bb.writeString(field.name);
494
+
495
+ // Encode type
496
+ let typeIndex: number;
497
+ if (field.type === null) {
498
+ typeIndex = 0; // For ENUM fields
499
+ } else if (typeof field.type === 'string') {
500
+ if (typeNameToIndex[field.type] !== undefined) {
501
+ typeIndex = typeNameToIndex[field.type];
502
+ } else if (nameToIndex.has(field.type)) {
503
+ typeIndex = nameToIndex.get(field.type)!;
504
+ } else {
505
+ console.warn(`[figma-kiwi-encoder] Unknown type reference: ${field.type}`);
506
+ typeIndex = 0;
507
+ }
508
+ } else {
509
+ typeIndex = field.type;
510
+ }
511
+ bb.writeVarInt(typeIndex);
512
+
513
+ // isArray flag
514
+ bb.writeByte(field.isArray ? 1 : 0);
515
+
516
+ // field value/ID
517
+ bb.writeVarUint(field.value);
518
+ }
519
+ }
520
+
521
+ return bb.toUint8Array();
522
+ }
523
+
524
+ /**
525
+ * Generate Figma clipboard HTML
526
+ */
527
+ export function generateFigmaClipboardHtml(
528
+ nodeChanges: Record<string, unknown>[],
529
+ fileKey: string = 'export',
530
+ schema?: Schema,
531
+ previewText?: string
532
+ ): string {
533
+ const schemaToUse = schema || cachedSchema;
534
+ if (!schemaToUse) {
535
+ throw new Error('No schema available. Please paste from Figma first to cache the schema.');
536
+ }
537
+
538
+ // Encode message
539
+ const messageData = encodeMessage(nodeChanges, schemaToUse);
540
+ console.log('[figma-kiwi-encoder] Encoded message size:', messageData.length);
541
+
542
+ // Compress data
543
+ const dataCompressed = pako.deflateRaw(messageData);
544
+ console.log('[figma-kiwi-encoder] Compressed data size:', dataCompressed.length);
545
+
546
+ // Get or encode schema
547
+ let schemaCompressed: Uint8Array;
548
+ if (cachedSchemaRaw) {
549
+ // Re-use cached raw schema (already in correct format)
550
+ schemaCompressed = cachedSchemaRaw;
551
+ console.log('[figma-kiwi-encoder] Using cached schema bytes');
552
+ } else {
553
+ // Encode schema
554
+ const schemaData = encodeBinarySchema(schemaToUse);
555
+ schemaCompressed = pako.deflateRaw(schemaData);
556
+ console.log('[figma-kiwi-encoder] Encoded schema size:', schemaCompressed.length);
557
+ }
558
+
559
+ // Create archive
560
+ const archive = createArchive(schemaCompressed, dataCompressed);
561
+ console.log('[figma-kiwi-encoder] Archive size:', archive.length);
562
+
563
+ // Encode to base64
564
+ const binaryString = Array.from(archive).map(b => String.fromCharCode(b)).join('');
565
+ const base64Data = btoa(binaryString);
566
+
567
+ // Create metadata
568
+ const meta = {
569
+ fileKey: fileKey,
570
+ pasteID: Math.floor(Math.random() * 1000000000),
571
+ dataType: 'scene',
572
+ };
573
+ const base64Meta = btoa(JSON.stringify(meta));
574
+
575
+ // Generate HTML with optional preview text (like Figma does)
576
+ const previewSpan = previewText
577
+ ? `<span style="white-space: pre-wrap;">${previewText}</span>`
578
+ : '';
579
+ const html = `<meta charset="utf-8"><span data-metadata="<!--(figmeta)${base64Meta}(/figmeta)-->"></span><span data-buffer="<!--(figma)${base64Data}(/figma)-->"></span>${previewSpan}`;
580
+
581
+ console.log('[figma-kiwi-encoder] Generated clipboard HTML length:', html.length);
582
+ return html;
583
+ }