@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.
- package/LICENSE +21 -0
- package/NOTICE +52 -0
- package/README.md +143 -0
- package/dist/editor.css +2 -0
- package/package.json +94 -0
- package/src/components/DeckSlideRender.tsx +21 -0
- package/src/components/SaveNote.tsx +21 -0
- package/src/components/auth/AuthProvider.tsx +44 -0
- package/src/components/ui/accordion.tsx +57 -0
- package/src/components/ui/alert-dialog.tsx +141 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/button.tsx +56 -0
- package/src/components/ui/card.tsx +76 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/collapsible.tsx +11 -0
- package/src/components/ui/context-menu.tsx +200 -0
- package/src/components/ui/dialog.tsx +122 -0
- package/src/components/ui/dropdown-menu.tsx +201 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/popover.tsx +33 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/scrubbable-label.tsx +95 -0
- package/src/components/ui/select.tsx +159 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/slider.tsx +28 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/textarea.tsx +22 -0
- package/src/components/ui/tooltip.tsx +32 -0
- package/src/components/viewer/useDeck.ts +58 -0
- package/src/editor/EditorContext.tsx +632 -0
- package/src/editor/EditorToolbar.tsx +446 -0
- package/src/editor/FrontendVisualEditor.tsx +2595 -0
- package/src/editor/autosave.ts +34 -0
- package/src/editor/components/AiPromptPopover.tsx +378 -0
- package/src/editor/components/BreakpointGuides.tsx +323 -0
- package/src/editor/components/BreakpointSelector.tsx +180 -0
- package/src/editor/components/ComponentPropertyEditor.tsx +426 -0
- package/src/editor/components/CssEditorDialog.tsx +168 -0
- package/src/editor/components/EditorCanvas.tsx +987 -0
- package/src/editor/components/EditorContextMenu.tsx +515 -0
- package/src/editor/components/EditorFooter.tsx +81 -0
- package/src/editor/components/EditorHeader.tsx +524 -0
- package/src/editor/components/EditorLayerPanel.tsx +1197 -0
- package/src/editor/components/EditorPropertyPanel.tsx +2996 -0
- package/src/editor/components/EditorSidebar.tsx +226 -0
- package/src/editor/components/FigmaColorPicker.tsx +1577 -0
- package/src/editor/components/HtmlEditorDialog.tsx +71 -0
- package/src/editor/components/HtmlImportDialog.tsx +346 -0
- package/src/editor/components/JsEditorDialog.tsx +175 -0
- package/src/editor/components/LayerTreeItem.tsx +494 -0
- package/src/editor/components/MasterComponentEditor.tsx +846 -0
- package/src/editor/components/MediaLibraryDialog.tsx +447 -0
- package/src/editor/components/PageSettingsDialog.tsx +776 -0
- package/src/editor/components/VariablePicker.tsx +355 -0
- package/src/editor/components/VariablesDialog.tsx +564 -0
- package/src/editor/components/VariantEditor.tsx +687 -0
- package/src/editor/components/VariantMatrix.tsx +507 -0
- package/src/editor/components/component-panel/CategoryAccordion.tsx +139 -0
- package/src/editor/components/component-panel/ComponentGrid.tsx +51 -0
- package/src/editor/components/component-panel/ComponentItem.tsx +368 -0
- package/src/editor/components/component-panel/ComponentPanel.tsx +275 -0
- package/src/editor/components/component-panel/ComponentSearch.tsx +82 -0
- package/src/editor/components/component-panel/VariantList.tsx +260 -0
- package/src/editor/components/component-panel/index.ts +12 -0
- package/src/editor/components/index.ts +48 -0
- package/src/editor/components/multi-page/CanvasZoomControls.tsx +71 -0
- package/src/editor/components/multi-page/InfiniteCanvas.tsx +95 -0
- package/src/editor/components/multi-page/MultiPageCanvasView.tsx +1404 -0
- package/src/editor/components/multi-page/PageFrameOverlay.tsx +76 -0
- package/src/editor/components/multi-page/PageLabel.tsx +34 -0
- package/src/editor/components/multi-page/PageLivePreview.tsx +242 -0
- package/src/editor/components/multi-page/PageThumbnail.tsx +59 -0
- package/src/editor/components/multi-page/index.ts +7 -0
- package/src/editor/components/ppt/PptChrome.tsx +2508 -0
- package/src/editor/components/ppt/PptComments.tsx +589 -0
- package/src/editor/components/ppt/PptDesignProposals.tsx +275 -0
- package/src/editor/components/ppt/PptFormatPane.tsx +320 -0
- package/src/editor/components/ppt/PptNotes.tsx +246 -0
- package/src/editor/components/property-panel/AlignmentPanel.tsx +110 -0
- package/src/editor/components/property-panel/AutoLayoutPanel.tsx +615 -0
- package/src/editor/components/property-panel/BorderSection.tsx +104 -0
- package/src/editor/components/property-panel/CompactNumberInput.tsx +237 -0
- package/src/editor/components/property-panel/CompactSizeInput.tsx +148 -0
- package/src/editor/components/property-panel/EffectSection.tsx +157 -0
- package/src/editor/components/property-panel/FillSection.tsx +69 -0
- package/src/editor/components/property-panel/GoogleFontPicker.tsx +192 -0
- package/src/editor/components/property-panel/ImageSection.tsx +206 -0
- package/src/editor/components/property-panel/ImgSrcSection.tsx +125 -0
- package/src/editor/components/property-panel/InstanceOverrideSection.tsx +413 -0
- package/src/editor/components/property-panel/LayoutSection.tsx +105 -0
- package/src/editor/components/property-panel/LinkSection.tsx +103 -0
- package/src/editor/components/property-panel/PropertyVariablePicker.tsx +311 -0
- package/src/editor/components/property-panel/ScalePanel.tsx +212 -0
- package/src/editor/components/property-panel/TypographySection.tsx +230 -0
- package/src/editor/components/property-panel/UnitAwareNumberInput.tsx +238 -0
- package/src/editor/components/property-panel/VariableAwareColorInput.tsx +477 -0
- package/src/editor/components/property-panel/VariableAwareInput.tsx +606 -0
- package/src/editor/components/property-panel/VariableAwareSizeInput.tsx +588 -0
- package/src/editor/components/property-panel/VariableAwareUnitInput.tsx +546 -0
- package/src/editor/components/property-panel/index.ts +24 -0
- package/src/editor/components/property-panel/unit-utils.ts +261 -0
- package/src/editor/components/shell/EditorTopBar.tsx +392 -0
- package/src/editor/components/shell/LeftPanel.tsx +47 -0
- package/src/editor/components/shell/PagesPanel.tsx +118 -0
- package/src/editor/components/variables-panel/ColorValueCell.tsx +165 -0
- package/src/editor/components/variables-panel/TextValueCell.tsx +116 -0
- package/src/editor/components/variables-panel/VariablesPanel.tsx +518 -0
- package/src/editor/components/variables-panel/index.ts +7 -0
- package/src/editor/constants.ts +468 -0
- package/src/editor/contexts/EditorArtboardContext.tsx +229 -0
- package/src/editor/contexts/EditorComponentsContext.tsx +1320 -0
- package/src/editor/contexts/EditorDocumentContext.tsx +113 -0
- package/src/editor/contexts/EditorHistoryContext.tsx +516 -0
- package/src/editor/contexts/EditorRefsContext.tsx +204 -0
- package/src/editor/contexts/EditorSelectionContext.tsx +54 -0
- package/src/editor/contexts/EditorToolContext.tsx +58 -0
- package/src/editor/contexts/EditorUIStateContext.tsx +122 -0
- package/src/editor/contexts/EditorVariablesContext.tsx +397 -0
- package/src/editor/contexts/EditorViewContext.tsx +101 -0
- package/src/editor/contexts/MultiPageCanvasContext.tsx +498 -0
- package/src/editor/contexts/index.ts +17 -0
- package/src/editor/editor-skin.css +122 -0
- package/src/editor/hooks/index.ts +38 -0
- package/src/editor/hooks/useAiReplace.ts +397 -0
- package/src/editor/hooks/useAltMeasure.ts +67 -0
- package/src/editor/hooks/useBrowserZoomPrevention.ts +121 -0
- package/src/editor/hooks/useCanvasControls.ts +1222 -0
- package/src/editor/hooks/useComponentEditMode.ts +658 -0
- package/src/editor/hooks/useComponentInstances.ts +515 -0
- package/src/editor/hooks/useContextMenuHandler.ts +157 -0
- package/src/editor/hooks/useCoordinateTransform.ts +207 -0
- package/src/editor/hooks/useDragResize.ts +1317 -0
- package/src/editor/hooks/useDrawingMode.ts +472 -0
- package/src/editor/hooks/useEditorColors.ts +144 -0
- package/src/editor/hooks/useEditorMessages.ts +108 -0
- package/src/editor/hooks/useElementActions.ts +1754 -0
- package/src/editor/hooks/useElementSelection.ts +1573 -0
- package/src/editor/hooks/useFocusManagement.ts +249 -0
- package/src/editor/hooks/useGoogleFonts.ts +305 -0
- package/src/editor/hooks/useIframeInitializer.ts +584 -0
- package/src/editor/hooks/useIframeSetup.ts +360 -0
- package/src/editor/hooks/useImageUpload.ts +270 -0
- package/src/editor/hooks/useInfiniteCanvas.ts +301 -0
- package/src/editor/hooks/useKeyboardShortcuts.ts +205 -0
- package/src/editor/hooks/useMarqueeSelection.ts +388 -0
- package/src/editor/hooks/useMediaLibrary.ts +129 -0
- package/src/editor/hooks/usePageSettingsManager.ts +739 -0
- package/src/editor/hooks/useResizablePanel.ts +163 -0
- package/src/editor/hooks/useRichPaste.ts +255 -0
- package/src/editor/hooks/useTouchGestures.ts +197 -0
- package/src/editor/index.ts +27 -0
- package/src/editor/types/page-settings.ts +513 -0
- package/src/editor/types.ts +382 -0
- package/src/editor/utils/align-elements.ts +169 -0
- package/src/editor/utils/api-json.ts +21 -0
- package/src/editor/utils/aspect-lock.ts +50 -0
- package/src/editor/utils/component-renderer.ts +702 -0
- package/src/editor/utils/component-sync.ts +845 -0
- package/src/editor/utils/crop-mode.ts +265 -0
- package/src/editor/utils/dom-utils.ts +2238 -0
- package/src/editor/utils/element-effects.ts +218 -0
- package/src/editor/utils/eyedropper.ts +35 -0
- package/src/editor/utils/figma-export.ts +560 -0
- package/src/editor/utils/figma-kiwi-decoder.ts +722 -0
- package/src/editor/utils/figma-kiwi-encoder.ts +583 -0
- package/src/editor/utils/figma-paste.ts +2262 -0
- package/src/editor/utils/flex-reorder.ts +231 -0
- package/src/editor/utils/flex-utils.ts +1230 -0
- package/src/editor/utils/geometry.ts +232 -0
- package/src/editor/utils/grid-layout.ts +210 -0
- package/src/editor/utils/html-utils.ts +470 -0
- package/src/editor/utils/index.ts +11 -0
- package/src/editor/utils/ink-style.ts +57 -0
- package/src/editor/utils/inline-format.ts +236 -0
- package/src/editor/utils/measure-distance.ts +120 -0
- package/src/editor/utils/override-apply.ts +526 -0
- package/src/editor/utils/override-detection.ts +613 -0
- package/src/editor/utils/paste-processors.ts +609 -0
- package/src/editor/utils/paste-sanitizer.ts +244 -0
- package/src/editor/utils/restore-flow.ts +188 -0
- package/src/editor/utils/shape-library.ts +171 -0
- package/src/editor/utils/slide-root.ts +29 -0
- package/src/editor/utils/smart-guides.ts +218 -0
- package/src/editor/utils/style-utils.ts +685 -0
- package/src/editor/utils/table-edit.ts +151 -0
- package/src/editor/utils/tailwind-mappings.ts +778 -0
- package/src/editor/utils/tailwind-utils.ts +804 -0
- package/src/editor/utils/text-highlight.ts +216 -0
- package/src/editor/utils/theme-to-variables.ts +341 -0
- package/src/editor/utils/variant-resolver.ts +456 -0
- package/src/editor/utils/viewport-utils.ts +295 -0
- package/src/hooks/useEditorHistory.ts +195 -0
- package/src/hooks/useEditorShortcuts.ts +260 -0
- package/src/index.ts +40 -0
- package/src/io.ts +134 -0
- package/src/lib/agent/slide-agent/types.ts +547 -0
- package/src/lib/agent/website-agent/types.ts +805 -0
- package/src/lib/api/auth-fetch.ts +58 -0
- package/src/lib/deck.ts +69 -0
- package/src/lib/export.ts +47 -0
- package/src/lib/firebase/config.ts +29 -0
- package/src/lib/firebase/css-variables.ts +378 -0
- package/src/lib/firebase/editor-components.ts +677 -0
- package/src/lib/firebase/storage.ts +469 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/editor.css +52 -0
- package/src/styles/skin.css +122 -0
- package/src/types/css-variables.ts +645 -0
- package/src/types/editor-components.ts +944 -0
- package/src/types/editor.ts +420 -0
- package/src/types/page-master.ts +197 -0
- package/src/types/page.ts +419 -0
- package/src/types/slide.ts +402 -0
- package/src/types/website-theme.ts +788 -0
- package/src/types/website.ts +374 -0
- package/src/vendor/firebase-firestore.ts +13 -0
- 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
|
+
}
|