@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,722 @@
1
+ /**
2
+ * figma-kiwi-decoder.ts
3
+ *
4
+ * Custom Figma kiwi decoder that handles newer schema versions.
5
+ * Implements full kiwi binary format parsing with extended primitive types.
6
+ */
7
+
8
+ import * as pako from 'pako';
9
+ import { cacheSchema } from './figma-kiwi-encoder';
10
+
11
+ // Extended types array
12
+ const EXTENDED_TYPES = [
13
+ 'bool', // -1 -> 0
14
+ 'byte', // -2 -> 1
15
+ 'int', // -3 -> 2
16
+ 'uint', // -4 -> 3
17
+ 'float', // -5 -> 4
18
+ 'string', // -6 -> 5
19
+ 'int64', // -7 -> 6
20
+ 'uint64', // -8 -> 7
21
+ 'double', // -9 -> 8
22
+ 'bytes', // -10 -> 9
23
+ 'type10', // -11 -> 10
24
+ 'type11', // -12 -> 11
25
+ 'type12', // -13 -> 12
26
+ 'type13', // -14 -> 13
27
+ 'type14', // -15 -> 14
28
+ 'type15', // -16 -> 15
29
+ ];
30
+
31
+ const KINDS = ['ENUM', 'STRUCT', 'MESSAGE'];
32
+
33
+ interface Field {
34
+ name: string;
35
+ line: number;
36
+ column: number;
37
+ type: string | number | null;
38
+ isArray: boolean;
39
+ isDeprecated: boolean;
40
+ value: number;
41
+ }
42
+
43
+ interface Definition {
44
+ name: string;
45
+ line: number;
46
+ column: number;
47
+ kind: string;
48
+ fields: Field[];
49
+ }
50
+
51
+ interface Schema {
52
+ package: null;
53
+ definitions: Definition[];
54
+ }
55
+
56
+ /**
57
+ * ByteBuffer for reading binary data (matching kiwi-schema's implementation)
58
+ */
59
+ class ByteBuffer {
60
+ private data: Uint8Array;
61
+ private index: number = 0;
62
+
63
+ constructor(data: Uint8Array | ArrayBuffer) {
64
+ this.data = data instanceof Uint8Array ? data : new Uint8Array(data);
65
+ }
66
+
67
+ readByte(): number {
68
+ if (this.index >= this.data.length) {
69
+ throw new Error('Read past end of buffer');
70
+ }
71
+ return this.data[this.index++];
72
+ }
73
+
74
+ readVarUint(): number {
75
+ let value = 0;
76
+ let shift = 0;
77
+ let byte: number;
78
+ do {
79
+ byte = this.readByte();
80
+ value |= (byte & 0x7F) << shift;
81
+ shift += 7;
82
+ } while (byte & 0x80);
83
+ return value >>> 0;
84
+ }
85
+
86
+ readVarInt(): number {
87
+ const value = this.readVarUint();
88
+ return (value & 1) ? ~(value >>> 1) : (value >>> 1);
89
+ }
90
+
91
+ /**
92
+ * Read null-terminated UTF-8 string (kiwi format)
93
+ */
94
+ readString(): string {
95
+ let result = '';
96
+ while (true) {
97
+ let codePoint: number;
98
+ // Decode UTF-8
99
+ const a = this.readByte();
100
+ if (a < 0xC0) {
101
+ codePoint = a;
102
+ } else {
103
+ const b = this.readByte();
104
+ if (a < 0xE0) {
105
+ codePoint = ((a & 0x1F) << 6) | (b & 0x3F);
106
+ } else {
107
+ const c = this.readByte();
108
+ if (a < 0xF0) {
109
+ codePoint = ((a & 0x0F) << 12) | ((b & 0x3F) << 6) | (c & 0x3F);
110
+ } else {
111
+ const d = this.readByte();
112
+ codePoint = ((a & 0x07) << 18) | ((b & 0x3F) << 12) | ((c & 0x3F) << 6) | (d & 0x3F);
113
+ }
114
+ }
115
+ }
116
+ // Strings are null-terminated
117
+ if (codePoint === 0) {
118
+ break;
119
+ }
120
+ // Encode UTF-16
121
+ if (codePoint < 0x10000) {
122
+ result += String.fromCharCode(codePoint);
123
+ } else {
124
+ codePoint -= 0x10000;
125
+ result += String.fromCharCode((codePoint >> 10) + 0xD800, (codePoint & ((1 << 10) - 1)) + 0xDC00);
126
+ }
127
+ }
128
+ return result;
129
+ }
130
+
131
+ get position(): number {
132
+ return this.index;
133
+ }
134
+
135
+ get remaining(): number {
136
+ return this.data.length - this.index;
137
+ }
138
+
139
+ /**
140
+ * Read length-prefixed byte array
141
+ */
142
+ readByteArray(): Uint8Array {
143
+ const length = this.readVarUint();
144
+ const start = this.index;
145
+ const end = start + length;
146
+ if (end > this.data.length) {
147
+ throw new Error('Read array out of bounds');
148
+ }
149
+ this.index = end;
150
+ const result = new Uint8Array(length);
151
+ result.set(this.data.subarray(start, end));
152
+ return result;
153
+ }
154
+
155
+ /**
156
+ * Read variable-length float (kiwi optimization format)
157
+ */
158
+ readVarFloat(): number {
159
+ const index = this.index;
160
+ const data = this.data;
161
+ const length = data.length;
162
+
163
+ // Optimization: use a single byte to store zero
164
+ if (index + 1 > length) {
165
+ throw new Error('Index out of bounds');
166
+ }
167
+ const first = data[index];
168
+ if (first === 0) {
169
+ this.index = index + 1;
170
+ return 0;
171
+ }
172
+
173
+ // Endian-independent 32-bit read
174
+ if (index + 4 > length) {
175
+ throw new Error('Index out of bounds');
176
+ }
177
+ let bits = first | (data[index + 1] << 8) | (data[index + 2] << 16) | (data[index + 3] << 24);
178
+ this.index = index + 4;
179
+
180
+ // Move the exponent back into place
181
+ bits = (bits << 23) | (bits >>> 9);
182
+
183
+ // Reinterpret as a floating-point number
184
+ const int32 = new Int32Array(1);
185
+ const float32 = new Float32Array(int32.buffer);
186
+ int32[0] = bits;
187
+ return float32[0];
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Custom implementation of decodeBinarySchema with extended type support
193
+ */
194
+ function customDecodeBinarySchema(buffer: Uint8Array, types: string[]): Schema {
195
+ const bb = new ByteBuffer(buffer);
196
+ const definitionCount = bb.readVarUint();
197
+ const definitions: Definition[] = [];
198
+
199
+ console.log('[figma-kiwi-decoder] Custom decode: definition count =', definitionCount);
200
+
201
+ // Read in the schema
202
+ for (let i = 0; i < definitionCount; i++) {
203
+ const definitionName = bb.readString();
204
+ const kind = bb.readByte();
205
+ const fieldCount = bb.readVarUint();
206
+ const fields: Field[] = [];
207
+
208
+ for (let j = 0; j < fieldCount; j++) {
209
+ const fieldName = bb.readString();
210
+ const type = bb.readVarInt();
211
+ const isArray = !!(bb.readByte() & 1);
212
+ const value = bb.readVarUint();
213
+
214
+ fields.push({
215
+ name: fieldName,
216
+ line: 0,
217
+ column: 0,
218
+ type: KINDS[kind] === 'ENUM' ? null : type,
219
+ isArray,
220
+ isDeprecated: false,
221
+ value,
222
+ });
223
+ }
224
+
225
+ definitions.push({
226
+ name: definitionName,
227
+ line: 0,
228
+ column: 0,
229
+ kind: KINDS[kind] || 'MESSAGE',
230
+ fields,
231
+ });
232
+ }
233
+
234
+ // Bind type names afterwards
235
+ for (let i = 0; i < definitionCount; i++) {
236
+ const fields = definitions[i].fields;
237
+ for (let j = 0; j < fields.length; j++) {
238
+ const field = fields[j];
239
+ const type = field.type;
240
+ if (type !== null && typeof type === 'number') {
241
+ if (type < 0) {
242
+ const typeIndex = ~type;
243
+ if (typeIndex >= types.length) {
244
+ // Use a placeholder for unknown types
245
+ field.type = `unknown_type_${typeIndex}`;
246
+ } else {
247
+ field.type = types[typeIndex];
248
+ }
249
+ } else {
250
+ if (type >= definitions.length) {
251
+ field.type = `ref_${type}`;
252
+ } else {
253
+ field.type = definitions[type].name;
254
+ }
255
+ }
256
+ }
257
+ }
258
+ }
259
+
260
+ return {
261
+ package: null,
262
+ definitions,
263
+ };
264
+ }
265
+
266
+ /**
267
+ * Parse Figma archive format (fig-kiwi header)
268
+ */
269
+ function parseArchive(buffer: Uint8Array): { schema: Uint8Array; data: Uint8Array } {
270
+ const prelude = 'fig-kiwi';
271
+ const preludeBytes = new TextEncoder().encode(prelude);
272
+
273
+ // Check prelude
274
+ const actualPrelude = new TextDecoder().decode(buffer.slice(0, preludeBytes.length));
275
+ if (actualPrelude !== prelude) {
276
+ throw new Error(`Invalid fig-kiwi prelude: got "${actualPrelude}"`);
277
+ }
278
+
279
+ let offset = preludeBytes.length;
280
+
281
+ // Read version (4 bytes, little endian)
282
+ const view = new DataView(buffer.buffer, buffer.byteOffset);
283
+ const version = view.getUint32(offset, true);
284
+ console.log('[figma-kiwi-decoder] Archive version:', version);
285
+ offset += 4;
286
+
287
+ const files: Uint8Array[] = [];
288
+
289
+ // Read chunks
290
+ while (offset + 4 <= buffer.length) {
291
+ const size = view.getUint32(offset, true);
292
+ offset += 4;
293
+
294
+ if (offset + size > buffer.length) {
295
+ files.push(buffer.slice(offset));
296
+ break;
297
+ }
298
+
299
+ files.push(buffer.slice(offset, offset + size));
300
+ offset += size;
301
+ }
302
+
303
+ if (files.length < 2) {
304
+ throw new Error(`Invalid archive: expected at least 2 chunks, got ${files.length}`);
305
+ }
306
+
307
+ return {
308
+ schema: files[0],
309
+ data: files[1],
310
+ };
311
+ }
312
+
313
+ /**
314
+ * Parse Figma clipboard HTML and extract data using custom decoder
315
+ */
316
+ export interface FigmaClipboardData {
317
+ meta: {
318
+ fileKey: string;
319
+ pasteID: number;
320
+ dataType: string;
321
+ };
322
+ message: Record<string, unknown>;
323
+ schema: Schema;
324
+ }
325
+
326
+ export function parseFigmaClipboardToJson(html: string): FigmaClipboardData {
327
+ // Extract metadata
328
+ const metaMatch = html.match(/\(figmeta\)([A-Za-z0-9+/=]+)\(\/figmeta\)/);
329
+ if (!metaMatch) {
330
+ throw new Error('Figma metadata not found');
331
+ }
332
+ const meta = JSON.parse(atob(metaMatch[1]));
333
+ console.log('[figma-kiwi-decoder] Meta:', meta);
334
+
335
+ // Extract binary data
336
+ const dataMatch = html.match(/\(figma\)([A-Za-z0-9+/=]+)\(\/figma\)/);
337
+ if (!dataMatch) {
338
+ throw new Error('Figma data not found');
339
+ }
340
+
341
+ // Decode base64 to Uint8Array
342
+ const binaryString = atob(dataMatch[1]);
343
+ const bytes = new Uint8Array(binaryString.length);
344
+ for (let i = 0; i < binaryString.length; i++) {
345
+ bytes[i] = binaryString.charCodeAt(i);
346
+ }
347
+ console.log('[figma-kiwi-decoder] Binary data size:', bytes.length);
348
+
349
+ // Parse archive
350
+ const archive = parseArchive(bytes);
351
+ console.log('[figma-kiwi-decoder] Schema chunk size:', archive.schema.length);
352
+ console.log('[figma-kiwi-decoder] Data chunk size:', archive.data.length);
353
+
354
+ // Decompress schema and data
355
+ const schemaDecompressed = pako.inflateRaw(archive.schema);
356
+ const dataDecompressed = pako.inflateRaw(archive.data);
357
+ console.log('[figma-kiwi-decoder] Schema decompressed size:', schemaDecompressed.length);
358
+ console.log('[figma-kiwi-decoder] Data decompressed size:', dataDecompressed.length);
359
+
360
+ // Decode schema with extended types
361
+ const schema = customDecodeBinarySchema(schemaDecompressed, EXTENDED_TYPES);
362
+ console.log('[figma-kiwi-decoder] Schema definitions:', schema.definitions.length);
363
+
364
+ // Cache schema for later use in encoding (Figma export)
365
+ cacheSchema(schema, archive.schema);
366
+
367
+ // Export raw schema for bundling (one-time capture)
368
+ if (typeof window !== 'undefined') {
369
+ const schemaBase64 = btoa(String.fromCharCode(...archive.schema));
370
+ console.log('[figma-kiwi-decoder] RAW SCHEMA BASE64 (copy this for bundling):');
371
+ console.log(schemaBase64);
372
+ }
373
+
374
+ // Find Message definition
375
+ const messageDefIndex = schema.definitions.findIndex(d => d.name === 'Message');
376
+ if (messageDefIndex === -1) {
377
+ throw new Error('Message definition not found in schema');
378
+ }
379
+ console.log('[figma-kiwi-decoder] Message definition found at index:', messageDefIndex);
380
+
381
+ // Find and log NodeChange definition to see available fields
382
+ const nodeChangeDef = schema.definitions.find(d => d.name === 'NodeChange');
383
+ if (nodeChangeDef) {
384
+ console.log('[figma-kiwi-decoder] NodeChange fields:',
385
+ nodeChangeDef.fields.map(f => `${f.name}(${f.value})`).join(', '));
386
+ // Look for parent-related fields
387
+ const parentFields = nodeChangeDef.fields.filter(f =>
388
+ f.name.toLowerCase().includes('parent') || f.name.toLowerCase().includes('index'));
389
+ if (parentFields.length > 0) {
390
+ console.log('[figma-kiwi-decoder] Parent-related fields:', parentFields.map(f => f.name));
391
+ }
392
+ }
393
+
394
+ // Log Paint-related definitions to see what fields are available
395
+ const paintDefs = schema.definitions.filter(d =>
396
+ d.name.toLowerCase().includes('paint') || d.name.toLowerCase().includes('image'));
397
+ for (const def of paintDefs) {
398
+ console.log(`[figma-kiwi-decoder] ${def.name} (${def.kind}): ${def.fields.map(f => `${f.name}:${f.type}(${f.value})`).join(', ')}`);
399
+ }
400
+
401
+ // Log PaintType enum if it exists
402
+ const paintTypeEnum = schema.definitions.find(d => d.name === 'PaintType');
403
+ if (paintTypeEnum) {
404
+ console.log('[figma-kiwi-decoder] PaintType enum values:', paintTypeEnum.fields.map(f => `${f.name}=${f.value}`).join(', '));
405
+ }
406
+
407
+ // Compile schema and decode message using our custom implementation
408
+ const message = decodeMessage(dataDecompressed, schema);
409
+
410
+ console.log('[figma-kiwi-decoder] Message decoded, type:', message.type);
411
+
412
+ return { meta, message, schema };
413
+ }
414
+
415
+ /**
416
+ * Decode a message using the compiled schema
417
+ */
418
+ function decodeMessage(buffer: Uint8Array, schema: Schema): Record<string, unknown> {
419
+ const bb = new ByteBuffer(buffer);
420
+
421
+ // Build definition map
422
+ const defMap = new Map<string, Definition>();
423
+ for (const def of schema.definitions) {
424
+ defMap.set(def.name, def);
425
+ }
426
+
427
+ // Find Message definition
428
+ const messageDef = defMap.get('Message');
429
+ if (!messageDef) {
430
+ throw new Error('Message definition not found');
431
+ }
432
+
433
+ const result = decodeStruct(bb, messageDef, defMap);
434
+ // Message should always be a struct, not an enum
435
+ if (typeof result === 'string') {
436
+ throw new Error('Message definition is unexpectedly an enum');
437
+ }
438
+ return result;
439
+ }
440
+
441
+ /**
442
+ * Decode a value based on its type
443
+ */
444
+ function decodeValue(
445
+ bb: ByteBuffer,
446
+ typeName: string | null,
447
+ isArray: boolean,
448
+ defMap: Map<string, Definition>
449
+ ): unknown {
450
+ if (isArray) {
451
+ const length = bb.readVarUint();
452
+ const arr: unknown[] = [];
453
+ for (let i = 0; i < length; i++) {
454
+ arr.push(decodeValue(bb, typeName, false, defMap));
455
+ }
456
+ return arr;
457
+ }
458
+
459
+ switch (typeName) {
460
+ case 'bool':
461
+ return bb.readByte() !== 0;
462
+ case 'byte':
463
+ return bb.readByte();
464
+ case 'int':
465
+ case 'int64':
466
+ return bb.readVarInt();
467
+ case 'uint':
468
+ case 'uint64':
469
+ return bb.readVarUint();
470
+ case 'float':
471
+ return bb.readVarFloat();
472
+ case 'double':
473
+ return readDouble(bb);
474
+ case 'string':
475
+ return bb.readString();
476
+ case 'bytes':
477
+ return bb.readByteArray();
478
+ default:
479
+ // Check if it's a reference to another definition
480
+ if (typeName && defMap.has(typeName)) {
481
+ const def = defMap.get(typeName)!;
482
+ // Handle ENUM types - resolve to name
483
+ if (def.kind === 'ENUM') {
484
+ const enumValue = bb.readVarUint();
485
+ // Find the enum field with this value
486
+ for (const field of def.fields) {
487
+ if (field.value === enumValue) {
488
+ return field.name;
489
+ }
490
+ }
491
+ // Return raw value if no matching field found
492
+ return `UNKNOWN_ENUM_${enumValue}`;
493
+ }
494
+ return decodeStruct(bb, def, defMap);
495
+ }
496
+ // Unknown type - try to skip or return null
497
+ console.warn(`[figma-kiwi-decoder] Unknown type: ${typeName}`);
498
+ return null;
499
+ }
500
+ }
501
+
502
+ /**
503
+ * Read a double from the buffer (8 bytes, little endian)
504
+ */
505
+ function readDouble(bb: ByteBuffer): number {
506
+ const bytes = new Uint8Array(8);
507
+ for (let i = 0; i < 8; i++) {
508
+ bytes[i] = bb.readByte();
509
+ }
510
+ return new DataView(bytes.buffer).getFloat64(0, true);
511
+ }
512
+
513
+ /**
514
+ * Decode a struct or message
515
+ */
516
+ function decodeStruct(
517
+ bb: ByteBuffer,
518
+ def: Definition,
519
+ defMap: Map<string, Definition>
520
+ ): Record<string, unknown> | string {
521
+ const result: Record<string, unknown> = {};
522
+
523
+ // ENUMs should be handled in decodeValue, but handle here as fallback
524
+ if (def.kind === 'ENUM') {
525
+ const enumValue = bb.readVarUint();
526
+ for (const field of def.fields) {
527
+ if (field.value === enumValue) {
528
+ return field.name;
529
+ }
530
+ }
531
+ return `UNKNOWN_ENUM_${enumValue}`;
532
+ }
533
+
534
+ if (def.kind === 'STRUCT') {
535
+ for (const field of def.fields) {
536
+ try {
537
+ result[field.name] = decodeValue(bb, field.type as string, field.isArray, defMap);
538
+ } catch (e) {
539
+ console.warn(`[figma-kiwi-decoder] Error decoding struct field ${field.name}:`, e);
540
+ result[field.name] = null;
541
+ }
542
+ }
543
+ return result;
544
+ }
545
+
546
+ // MESSAGE type - fields are optional with field IDs
547
+ if (def.kind === 'MESSAGE') {
548
+ const fieldMap = new Map<number, Field>();
549
+ for (const field of def.fields) {
550
+ fieldMap.set(field.value, field);
551
+ }
552
+
553
+ while (bb.remaining > 0) {
554
+ const fieldId = bb.readVarUint();
555
+ if (fieldId === 0) break;
556
+
557
+ const field = fieldMap.get(fieldId);
558
+ if (field) {
559
+ try {
560
+ result[field.name] = decodeValue(bb, field.type as string, field.isArray, defMap);
561
+ } catch (e) {
562
+ console.warn(`[figma-kiwi-decoder] Error decoding message field ${field.name}:`, e);
563
+ break;
564
+ }
565
+ } else {
566
+ // Unknown field - we can't skip it properly without knowing the type
567
+ console.warn(`[figma-kiwi-decoder] Unknown field ID: ${fieldId} in ${def.name}`);
568
+ break;
569
+ }
570
+ }
571
+ return result;
572
+ }
573
+
574
+ return result;
575
+ }
576
+
577
+ /**
578
+ * Convert Figma message to simplified node structure
579
+ */
580
+ export interface FigmaNode {
581
+ guid?: { sessionID: number; localID: number };
582
+ parentIndex?: { guid?: { sessionID: number; localID: number }; position?: string };
583
+ type?: string;
584
+ name?: string;
585
+ visible?: boolean;
586
+ locked?: boolean;
587
+ opacity?: number;
588
+ blendMode?: string;
589
+ size?: { x: number; y: number };
590
+ transform?: { m00: number; m01: number; m02: number; m10: number; m11: number; m12: number };
591
+
592
+ // Fill & Stroke
593
+ fillPaints?: unknown[];
594
+ strokePaints?: unknown[];
595
+ strokeWeight?: number;
596
+ strokeAlign?: string;
597
+ strokeCap?: string;
598
+ strokeJoin?: string;
599
+ dashPattern?: number[];
600
+
601
+ // Individual border weights
602
+ borderTopWeight?: number;
603
+ borderRightWeight?: number;
604
+ borderBottomWeight?: number;
605
+ borderLeftWeight?: number;
606
+ borderStrokeWeightsIndependent?: boolean;
607
+
608
+ // Corner radius
609
+ cornerRadius?: number;
610
+ rectangleCornerRadii?: number[];
611
+ rectangleTopLeftCornerRadius?: number;
612
+ rectangleTopRightCornerRadius?: number;
613
+ rectangleBottomLeftCornerRadius?: number;
614
+ rectangleBottomRightCornerRadius?: number;
615
+
616
+ // Effects
617
+ effects?: unknown[];
618
+
619
+ // Text properties
620
+ fontSize?: number;
621
+ fontName?: { family: string; style: string; postscript: string };
622
+ textData?: { characters: string };
623
+ textAlignHorizontal?: string;
624
+ textAlignVertical?: string;
625
+ lineHeight?: { value: number; units: string };
626
+ letterSpacing?: { value: number; units: string };
627
+ textCase?: string;
628
+ textDecoration?: string;
629
+ paragraphIndent?: number;
630
+ paragraphSpacing?: number;
631
+ textTruncation?: string;
632
+ maxLines?: number;
633
+
634
+ // Auto-layout properties
635
+ stackMode?: string;
636
+ stackSpacing?: number;
637
+ stackPadding?: number;
638
+ stackHorizontalPadding?: number;
639
+ stackVerticalPadding?: number;
640
+ stackPaddingRight?: number;
641
+ stackPaddingBottom?: number;
642
+ stackPrimaryAlignItems?: string;
643
+ stackCounterAlignItems?: string;
644
+ stackPrimarySizing?: string;
645
+ stackCounterSizing?: string;
646
+ stackChildPrimaryGrow?: number;
647
+ stackChildAlignSelf?: string;
648
+ stackPositioning?: string;
649
+ stackReverseZIndex?: boolean;
650
+ stackWrap?: string;
651
+
652
+ // Constraints
653
+ horizontalConstraint?: string;
654
+ verticalConstraint?: string;
655
+
656
+ // Clipping
657
+ clipsContent?: boolean;
658
+ frameMaskDisabled?: boolean;
659
+
660
+ // Other
661
+ count?: number;
662
+ [key: string]: unknown;
663
+ }
664
+
665
+ export function extractNodes(message: Record<string, unknown>): FigmaNode[] {
666
+ const nodes: FigmaNode[] = [];
667
+
668
+ // Try to extract node changes from message
669
+ const nodeChanges = message.nodeChanges as unknown[];
670
+ if (Array.isArray(nodeChanges)) {
671
+ console.log('[figma-kiwi-decoder] === NODE CHANGES DEBUG ===');
672
+ for (let i = 0; i < nodeChanges.length; i++) {
673
+ const change = nodeChanges[i];
674
+ if (change && typeof change === 'object') {
675
+ const nodeObj = change as Record<string, unknown>;
676
+ // Log all keys to see what fields are available
677
+ console.log(`[figma-kiwi-decoder] Node ${i}: ${nodeObj.type} "${nodeObj.name}"`);
678
+ console.log(`[figma-kiwi-decoder] Keys: ${Object.keys(nodeObj).join(', ')}`);
679
+ // Look for parent-related fields - parentIndex has nested guid structure
680
+ if ('parentIndex' in nodeObj && nodeObj.parentIndex) {
681
+ const pi = nodeObj.parentIndex as { guid?: { sessionID?: number; localID?: number } };
682
+ if (pi.guid) {
683
+ console.log(`[figma-kiwi-decoder] parentIndex.guid: ${pi.guid.sessionID}:${pi.guid.localID}`);
684
+ }
685
+ }
686
+ if ('guid' in nodeObj && nodeObj.guid) {
687
+ const g = nodeObj.guid as { sessionID?: number; localID?: number };
688
+ console.log(`[figma-kiwi-decoder] guid: ${g.sessionID}:${g.localID}`);
689
+ }
690
+ // Log auto-layout properties if present
691
+ if ('stackMode' in nodeObj) {
692
+ console.log(`[figma-kiwi-decoder] auto-layout: mode=${nodeObj.stackMode} spacing=${nodeObj.stackSpacing} padding=${nodeObj.stackPadding}`);
693
+ }
694
+ // Log text properties if present
695
+ if ('textAlignHorizontal' in nodeObj || 'lineHeight' in nodeObj) {
696
+ const lh = nodeObj.lineHeight as { value?: number; units?: string } | undefined;
697
+ const ls = nodeObj.letterSpacing as { value?: number; units?: string } | undefined;
698
+ console.log(`[figma-kiwi-decoder] text: align=${nodeObj.textAlignHorizontal} lineHeight=${lh?.value}${lh?.units} letterSpacing=${ls?.value}${ls?.units}`);
699
+ }
700
+ // Log fill paints details for debugging image detection
701
+ if ('fillPaints' in nodeObj && Array.isArray(nodeObj.fillPaints)) {
702
+ for (let j = 0; j < nodeObj.fillPaints.length; j++) {
703
+ const paint = nodeObj.fillPaints[j] as Record<string, unknown>;
704
+ const paintKeys = Object.keys(paint);
705
+ console.log(`[figma-kiwi-decoder] fillPaint[${j}]: type=${paint.type} keys=[${paintKeys.join(', ')}]`);
706
+ // Log all values for IMAGE type or unknown types
707
+ if (paint.type === 'IMAGE' || paintKeys.includes('imageHash') || paintKeys.includes('imageRef')) {
708
+ console.log(`[figma-kiwi-decoder] IMAGE paint detail:`, JSON.stringify(paint, (_, v) => {
709
+ if (v instanceof Uint8Array) return `<Uint8Array(${v.length})>`;
710
+ return v;
711
+ }, 2).substring(0, 500));
712
+ }
713
+ }
714
+ }
715
+ nodes.push(nodeObj as FigmaNode);
716
+ }
717
+ }
718
+ console.log('[figma-kiwi-decoder] === END NODE CHANGES DEBUG ===');
719
+ }
720
+
721
+ return nodes;
722
+ }