@agent-native/core 0.84.30 → 0.84.34
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +24 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/session-replay.ts +5 -0
- package/corpus/core/src/client/use-action.ts +5 -1
- package/corpus/core/src/server/ssr-handler.ts +12 -0
- package/corpus/core/src/vite/action-types-plugin.ts +6 -2
- package/corpus/core/src/vite/client.ts +152 -17
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +13 -1
- package/corpus/templates/analytics/changelog/2026-07-01-session-replays-keep-inlined-css-without-live-resource-loa.md +6 -0
- package/corpus/templates/design/AGENTS.md +18 -0
- package/corpus/templates/design/actions/apply-source-edit.ts +87 -0
- package/corpus/templates/design/actions/import-design-source.ts +158 -0
- package/corpus/templates/design/actions/list-source-files.ts +52 -0
- package/corpus/templates/design/actions/navigate.ts +4 -2
- package/corpus/templates/design/actions/preview-source-edit.ts +85 -0
- package/corpus/templates/design/actions/read-source-file.ts +55 -0
- package/corpus/templates/design/actions/resolve-selection-source.ts +101 -0
- package/corpus/templates/design/actions/view-screen.ts +43 -1
- package/corpus/templates/design/app/components/design/CodeWorkbenchHost.tsx +630 -0
- package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +519 -0
- package/corpus/templates/design/app/hooks/use-navigation-state.ts +32 -6
- package/corpus/templates/design/app/i18n/zh-TW.ts +40 -0
- package/corpus/templates/design/app/i18n-data.ts +508 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +171 -9
- package/corpus/templates/design/changelog/2026-07-01-design-can-import-figma-paste-fig-files-and-standalone-html.md +6 -0
- package/corpus/templates/design/changelog/2026-07-01-design-code-workspace.md +6 -0
- package/corpus/templates/design/package.json +3 -0
- package/corpus/templates/design/server/handlers/import-design-file.ts +168 -0
- package/corpus/templates/design/server/lib/figma-import/clipboard.ts +122 -0
- package/corpus/templates/design/server/lib/figma-import/decode.ts +455 -0
- package/corpus/templates/design/server/lib/figma-import/processor.ts +116 -0
- package/corpus/templates/design/server/lib/figma-import/render-html.ts +397 -0
- package/corpus/templates/design/server/lib/figma-import/types.ts +60 -0
- package/corpus/templates/design/server/lib/import-design-files.ts +314 -0
- package/corpus/templates/design/server/routes/api/import-design-file.post.ts +1 -0
- package/corpus/templates/design/server/source-workspace.ts +215 -0
- package/corpus/templates/design/shared/design-source-capabilities.ts +5 -5
- package/corpus/templates/design/shared/source-workspace.ts +149 -0
- package/dist/client/session-replay.d.ts +1 -0
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +2 -0
- package/dist/client/session-replay.js.map +1 -1
- package/dist/client/use-action.d.ts +5 -1
- package/dist/client/use-action.d.ts.map +1 -1
- package/dist/client/use-action.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/ssr-handler.d.ts.map +1 -1
- package/dist/server/ssr-handler.js +7 -0
- package/dist/server/ssr-handler.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/vite/action-types-plugin.d.ts.map +1 -1
- package/dist/vite/action-types-plugin.js +6 -2
- package/dist/vite/action-types-plugin.js.map +1 -1
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +132 -11
- package/dist/vite/client.js.map +1 -1
- package/package.json +1 -1
|
@@ -111,6 +111,7 @@ import {
|
|
|
111
111
|
IconDownload,
|
|
112
112
|
IconClipboard,
|
|
113
113
|
IconFileExport,
|
|
114
|
+
IconFileImport,
|
|
114
115
|
IconPlayerPlay,
|
|
115
116
|
IconDeviceFloppy,
|
|
116
117
|
IconRocket,
|
|
@@ -149,6 +150,10 @@ import {
|
|
|
149
150
|
CanvasContextMenu,
|
|
150
151
|
type CanvasContextMenuHandle,
|
|
151
152
|
} from "@/components/design/CanvasContextMenu";
|
|
153
|
+
import {
|
|
154
|
+
CodeWorkbenchHost,
|
|
155
|
+
type CodeWorkbenchActiveFile,
|
|
156
|
+
} from "@/components/design/CodeWorkbenchHost";
|
|
152
157
|
import {
|
|
153
158
|
DesignCanvas,
|
|
154
159
|
type IframeContextMenuPayload,
|
|
@@ -161,6 +166,7 @@ import {
|
|
|
161
166
|
DesignExtensionsPanel,
|
|
162
167
|
type DesignExtensionSlotContext,
|
|
163
168
|
} from "@/components/design/DesignExtensionsPanel";
|
|
169
|
+
import { DesignImportPanel } from "@/components/design/DesignImportPanel";
|
|
164
170
|
import {
|
|
165
171
|
EditPanel,
|
|
166
172
|
type InspectCodeData,
|
|
@@ -550,18 +556,34 @@ export function getDesignEditorStateUrlSearch(args: {
|
|
|
550
556
|
currentSearch: string;
|
|
551
557
|
viewMode: "single" | "overview";
|
|
552
558
|
screenId?: string | null;
|
|
559
|
+
codeFileId?: string | null;
|
|
560
|
+
codeFilename?: string | null;
|
|
553
561
|
selectionId?: string | null;
|
|
562
|
+
leftPanel?: DesignLeftPanel | null;
|
|
554
563
|
zoom?: number | null;
|
|
555
564
|
}) {
|
|
556
565
|
const params = new URLSearchParams(args.currentSearch);
|
|
557
566
|
params.set("view", args.viewMode);
|
|
567
|
+
if (args.leftPanel && args.leftPanel !== "file") {
|
|
568
|
+
params.set("panel", args.leftPanel);
|
|
569
|
+
} else {
|
|
570
|
+
params.delete("panel");
|
|
571
|
+
}
|
|
558
572
|
if (args.screenId) {
|
|
559
573
|
params.set("screen", args.screenId);
|
|
560
574
|
} else {
|
|
561
575
|
params.delete("screen");
|
|
562
576
|
}
|
|
563
|
-
|
|
564
|
-
|
|
577
|
+
if (args.leftPanel === "code" && args.codeFileId) {
|
|
578
|
+
params.set("fileId", args.codeFileId);
|
|
579
|
+
} else {
|
|
580
|
+
params.delete("fileId");
|
|
581
|
+
}
|
|
582
|
+
if (args.leftPanel === "code" && !args.codeFileId && args.codeFilename) {
|
|
583
|
+
params.set("filename", args.codeFilename);
|
|
584
|
+
} else {
|
|
585
|
+
params.delete("filename");
|
|
586
|
+
}
|
|
565
587
|
if (args.selectionId) {
|
|
566
588
|
params.set("selection", args.selectionId);
|
|
567
589
|
} else {
|
|
@@ -3495,13 +3517,22 @@ function AgentNativeMenuMark({ className }: { className?: string }) {
|
|
|
3495
3517
|
);
|
|
3496
3518
|
}
|
|
3497
3519
|
|
|
3498
|
-
type DesignLeftPanel =
|
|
3520
|
+
type DesignLeftPanel =
|
|
3521
|
+
| "file"
|
|
3522
|
+
| "agent"
|
|
3523
|
+
| "assets"
|
|
3524
|
+
| "tools"
|
|
3525
|
+
| "tokens"
|
|
3526
|
+
| "import"
|
|
3527
|
+
| "code";
|
|
3499
3528
|
|
|
3500
3529
|
const INITIAL_GENERATION_DISABLED_LEFT_PANELS = new Set<DesignLeftPanel>([
|
|
3501
3530
|
"file",
|
|
3502
3531
|
"assets",
|
|
3503
3532
|
"tools",
|
|
3504
3533
|
"tokens",
|
|
3534
|
+
"import",
|
|
3535
|
+
"code",
|
|
3505
3536
|
]);
|
|
3506
3537
|
|
|
3507
3538
|
function normalizeDesignLeftPanel(value: unknown): DesignLeftPanel | undefined {
|
|
@@ -3510,7 +3541,9 @@ function normalizeDesignLeftPanel(value: unknown): DesignLeftPanel | undefined {
|
|
|
3510
3541
|
value === "agent" ||
|
|
3511
3542
|
value === "assets" ||
|
|
3512
3543
|
value === "tools" ||
|
|
3513
|
-
value === "tokens"
|
|
3544
|
+
value === "tokens" ||
|
|
3545
|
+
value === "import" ||
|
|
3546
|
+
value === "code"
|
|
3514
3547
|
? value
|
|
3515
3548
|
: undefined;
|
|
3516
3549
|
}
|
|
@@ -3553,6 +3586,11 @@ function DesignWorkspaceRail({
|
|
|
3553
3586
|
label: t("designEditor.leftRail.assets"),
|
|
3554
3587
|
icon: <IconPhoto className="size-[15px]" />,
|
|
3555
3588
|
},
|
|
3589
|
+
{
|
|
3590
|
+
panel: "import",
|
|
3591
|
+
label: t("designEditor.leftRail.import"),
|
|
3592
|
+
icon: <IconFileImport className="size-[15px]" />,
|
|
3593
|
+
},
|
|
3556
3594
|
{
|
|
3557
3595
|
panel: "tools",
|
|
3558
3596
|
label: t("designEditor.leftRail.tools"),
|
|
@@ -3564,6 +3602,11 @@ function DesignWorkspaceRail({
|
|
|
3564
3602
|
icon: <IconAssembly className="size-[15px]" />,
|
|
3565
3603
|
},
|
|
3566
3604
|
];
|
|
3605
|
+
const codeItem = {
|
|
3606
|
+
panel: "code" as const,
|
|
3607
|
+
label: "Code" /* i18n-ignore */,
|
|
3608
|
+
icon: <IconCode className="size-[15px]" />,
|
|
3609
|
+
};
|
|
3567
3610
|
|
|
3568
3611
|
return (
|
|
3569
3612
|
<nav
|
|
@@ -3622,6 +3665,51 @@ function DesignWorkspaceRail({
|
|
|
3622
3665
|
</Tooltip>
|
|
3623
3666
|
);
|
|
3624
3667
|
})}
|
|
3668
|
+
<div className="mt-auto flex w-full flex-col items-center border-t border-border/70 pt-3">
|
|
3669
|
+
<Tooltip>
|
|
3670
|
+
<TooltipTrigger asChild>
|
|
3671
|
+
<button
|
|
3672
|
+
type="button"
|
|
3673
|
+
aria-label={codeItem.label}
|
|
3674
|
+
aria-disabled={disabledPanels?.has(codeItem.panel) || undefined}
|
|
3675
|
+
aria-current={
|
|
3676
|
+
activePanel === codeItem.panel ? "page" : undefined
|
|
3677
|
+
}
|
|
3678
|
+
tabIndex={disabledPanels?.has(codeItem.panel) ? -1 : undefined}
|
|
3679
|
+
onClick={(event) => {
|
|
3680
|
+
if (disabledPanels?.has(codeItem.panel)) {
|
|
3681
|
+
event.preventDefault();
|
|
3682
|
+
return;
|
|
3683
|
+
}
|
|
3684
|
+
onPanelChange(codeItem.panel);
|
|
3685
|
+
}}
|
|
3686
|
+
className={cn(
|
|
3687
|
+
"group flex w-12 cursor-pointer flex-col items-center justify-start gap-1 rounded-none text-[10px] font-[450] leading-none text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]",
|
|
3688
|
+
disabledPanels?.has(codeItem.panel) &&
|
|
3689
|
+
"cursor-default opacity-35 hover:text-muted-foreground",
|
|
3690
|
+
activePanel === codeItem.panel && "text-foreground",
|
|
3691
|
+
)}
|
|
3692
|
+
>
|
|
3693
|
+
<span
|
|
3694
|
+
className={cn(
|
|
3695
|
+
"flex size-8 items-center justify-center rounded-lg transition-colors",
|
|
3696
|
+
activePanel === codeItem.panel
|
|
3697
|
+
? "bg-[var(--design-editor-selection-color)] text-[var(--design-editor-accent-color)]"
|
|
3698
|
+
: "text-muted-foreground group-hover:bg-[var(--design-editor-layer-hover-color)] group-hover:text-foreground",
|
|
3699
|
+
disabledPanels?.has(codeItem.panel) &&
|
|
3700
|
+
"group-hover:bg-transparent group-hover:text-muted-foreground",
|
|
3701
|
+
)}
|
|
3702
|
+
>
|
|
3703
|
+
{codeItem.icon}
|
|
3704
|
+
</span>
|
|
3705
|
+
<span className="max-w-full truncate leading-none">
|
|
3706
|
+
{codeItem.label}
|
|
3707
|
+
</span>
|
|
3708
|
+
</button>
|
|
3709
|
+
</TooltipTrigger>
|
|
3710
|
+
<TooltipContent side="right">{codeItem.label}</TooltipContent>
|
|
3711
|
+
</Tooltip>
|
|
3712
|
+
</div>
|
|
3625
3713
|
</div>
|
|
3626
3714
|
{onMotionToggle ? (
|
|
3627
3715
|
<div className="mt-4 flex w-full flex-col items-center border-t border-border/70 pt-3">
|
|
@@ -4608,6 +4696,8 @@ export default function DesignEditor() {
|
|
|
4608
4696
|
useState<InspectorTab>("design");
|
|
4609
4697
|
const [activeLeftPanel, setActiveLeftPanel] =
|
|
4610
4698
|
useState<DesignLeftPanel>("file");
|
|
4699
|
+
const [activeCodeFile, setActiveCodeFile] =
|
|
4700
|
+
useState<CodeWorkbenchActiveFile | null>(null);
|
|
4611
4701
|
const initialSearchCommandAppliedForIdRef = useRef<string | null>(null);
|
|
4612
4702
|
const initialUrlSelectionHydratedForIdRef = useRef<string | null>(null);
|
|
4613
4703
|
const [leftSidebarWidth, setLeftSidebarWidth] = useState(256);
|
|
@@ -4896,11 +4986,17 @@ export default function DesignEditor() {
|
|
|
4896
4986
|
event.stopPropagation();
|
|
4897
4987
|
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
4898
4988
|
const startX = event.clientX;
|
|
4899
|
-
const
|
|
4989
|
+
const codePanelOpen = side === "left" && activeLeftPanel === "code";
|
|
4990
|
+
const startWidth =
|
|
4991
|
+
side === "left"
|
|
4992
|
+
? codePanelOpen
|
|
4993
|
+
? Math.max(leftSidebarWidth, 640)
|
|
4994
|
+
: Math.min(leftSidebarWidth, 420)
|
|
4995
|
+
: rightSidebarWidth;
|
|
4900
4996
|
const setWidth =
|
|
4901
4997
|
side === "left" ? setLeftSidebarWidth : setRightSidebarWidth;
|
|
4902
|
-
const minWidth = side === "left" ? 220 : 240;
|
|
4903
|
-
const maxWidth = side === "left" ? 420 : 390;
|
|
4998
|
+
const minWidth = side === "left" ? (codePanelOpen ? 520 : 220) : 240;
|
|
4999
|
+
const maxWidth = side === "left" ? (codePanelOpen ? 860 : 420) : 390;
|
|
4904
5000
|
const previousCursor = document.body.style.cursor;
|
|
4905
5001
|
const previousUserSelect = document.body.style.userSelect;
|
|
4906
5002
|
const dragShield = document.createElement("div");
|
|
@@ -4939,7 +5035,7 @@ export default function DesignEditor() {
|
|
|
4939
5035
|
window.addEventListener("pointerup", cleanup);
|
|
4940
5036
|
window.addEventListener("pointercancel", cleanup);
|
|
4941
5037
|
},
|
|
4942
|
-
[leftSidebarWidth, rightSidebarWidth],
|
|
5038
|
+
[activeLeftPanel, leftSidebarWidth, rightSidebarWidth],
|
|
4943
5039
|
);
|
|
4944
5040
|
// Undo/redo state driven by Y.UndoManager
|
|
4945
5041
|
const [canUndo, setCanUndo] = useState(false);
|
|
@@ -9134,6 +9230,14 @@ export default function DesignEditor() {
|
|
|
9134
9230
|
activeTool,
|
|
9135
9231
|
inspectorTab: activeInspectorTab,
|
|
9136
9232
|
leftPanel: activeLeftPanel,
|
|
9233
|
+
codeWorkspace: {
|
|
9234
|
+
open: activeLeftPanel === "code",
|
|
9235
|
+
backendKind: activeCodeFile?.backendKind ?? "virtual-inline",
|
|
9236
|
+
activePath: activeCodeFile?.path ?? null,
|
|
9237
|
+
activeFileId: activeCodeFile?.fileId ?? null,
|
|
9238
|
+
dirty: activeCodeFile?.dirty ?? false,
|
|
9239
|
+
versionHash: activeCodeFile?.versionHash ?? null,
|
|
9240
|
+
},
|
|
9137
9241
|
// §8 DesignNavigationState additions — dock + breakpoint context
|
|
9138
9242
|
dock: { kind: "motion" as const, open: motionDockOpen },
|
|
9139
9243
|
motion: {
|
|
@@ -9202,6 +9306,7 @@ export default function DesignEditor() {
|
|
|
9202
9306
|
activeTool: selection.activeTool,
|
|
9203
9307
|
inspectorTab: selection.inspectorTab,
|
|
9204
9308
|
leftPanel: selection.leftPanel,
|
|
9309
|
+
codeWorkspace: selection.codeWorkspace,
|
|
9205
9310
|
dock: selection.dock,
|
|
9206
9311
|
motion: selection.motion,
|
|
9207
9312
|
breakpoint: selection.breakpoint,
|
|
@@ -9243,6 +9348,7 @@ export default function DesignEditor() {
|
|
|
9243
9348
|
activeTool,
|
|
9244
9349
|
activeInspectorTab,
|
|
9245
9350
|
activeLeftPanel,
|
|
9351
|
+
activeCodeFile,
|
|
9246
9352
|
overviewSelectedScreenIds,
|
|
9247
9353
|
viewMode,
|
|
9248
9354
|
zoom,
|
|
@@ -14352,6 +14458,9 @@ ${serializedHtml}
|
|
|
14352
14458
|
currentSearch: location.search,
|
|
14353
14459
|
viewMode,
|
|
14354
14460
|
screenId: activeFile?.id ?? activeFileId,
|
|
14461
|
+
leftPanel: activeLeftPanel,
|
|
14462
|
+
codeFileId: activeLeftPanel === "code" ? activeCodeFile?.fileId : null,
|
|
14463
|
+
codeFilename: activeLeftPanel === "code" ? activeCodeFile?.path : null,
|
|
14355
14464
|
selectionId:
|
|
14356
14465
|
selectedUrlSelectionId ??
|
|
14357
14466
|
(preserveInitialRouteSelection ? initialRouteSelectionId : null),
|
|
@@ -14369,6 +14478,9 @@ ${serializedHtml}
|
|
|
14369
14478
|
}, [
|
|
14370
14479
|
activeFile?.id,
|
|
14371
14480
|
activeFileId,
|
|
14481
|
+
activeCodeFile?.fileId,
|
|
14482
|
+
activeCodeFile?.path,
|
|
14483
|
+
activeLeftPanel,
|
|
14372
14484
|
codeLayerOwnerByNodeId.size,
|
|
14373
14485
|
files,
|
|
14374
14486
|
id,
|
|
@@ -16287,7 +16399,14 @@ ${serializedHtml}
|
|
|
16287
16399
|
</div>
|
|
16288
16400
|
);
|
|
16289
16401
|
|
|
16290
|
-
const leftContentWidth =
|
|
16402
|
+
const leftContentWidth =
|
|
16403
|
+
activeLeftPanel === "code"
|
|
16404
|
+
? Math.max(leftSidebarWidth, 640)
|
|
16405
|
+
: Math.max(Math.min(leftSidebarWidth, 420), 320);
|
|
16406
|
+
const routeCodeFileId =
|
|
16407
|
+
activeLeftPanel === "code" ? searchParams.get("fileId") : null;
|
|
16408
|
+
const routeCodeFilename =
|
|
16409
|
+
activeLeftPanel === "code" ? searchParams.get("filename") : null;
|
|
16291
16410
|
return (
|
|
16292
16411
|
// h-full not flex-1: the parent <main> uses overflow-y-auto, not flex,
|
|
16293
16412
|
// so flex-1 on the child doesn't resolve to the available height. h-full
|
|
@@ -16765,6 +16884,23 @@ ${serializedHtml}
|
|
|
16765
16884
|
/>
|
|
16766
16885
|
)}
|
|
16767
16886
|
</div>
|
|
16887
|
+
<div
|
|
16888
|
+
className={cn(
|
|
16889
|
+
"min-h-0 flex-1 flex-col overflow-hidden",
|
|
16890
|
+
activeLeftPanel === "import" ? "flex" : "hidden",
|
|
16891
|
+
)}
|
|
16892
|
+
>
|
|
16893
|
+
{canEditDesign ? (
|
|
16894
|
+
<DesignImportPanel context={designExtensionContext} />
|
|
16895
|
+
) : (
|
|
16896
|
+
<ReadOnlyEditorPanel
|
|
16897
|
+
title={"Import requires editor access" /* i18n-ignore */}
|
|
16898
|
+
description={
|
|
16899
|
+
"Ask an owner for edit access before importing files into this design." /* i18n-ignore */
|
|
16900
|
+
}
|
|
16901
|
+
/>
|
|
16902
|
+
)}
|
|
16903
|
+
</div>
|
|
16768
16904
|
<div
|
|
16769
16905
|
className={cn(
|
|
16770
16906
|
"min-h-0 flex-1 flex-col overflow-hidden",
|
|
@@ -16815,6 +16951,32 @@ ${serializedHtml}
|
|
|
16815
16951
|
/>
|
|
16816
16952
|
)}
|
|
16817
16953
|
</div>
|
|
16954
|
+
<div
|
|
16955
|
+
className={cn(
|
|
16956
|
+
"min-h-0 flex-1 flex-col overflow-hidden",
|
|
16957
|
+
activeLeftPanel === "code" ? "flex" : "hidden",
|
|
16958
|
+
)}
|
|
16959
|
+
>
|
|
16960
|
+
{id ? (
|
|
16961
|
+
<CodeWorkbenchHost
|
|
16962
|
+
designId={id}
|
|
16963
|
+
activeFileId={
|
|
16964
|
+
activeLeftPanel === "code"
|
|
16965
|
+
? routeCodeFileId
|
|
16966
|
+
: (activeFile?.id ?? activeFileId)
|
|
16967
|
+
}
|
|
16968
|
+
activeFilename={
|
|
16969
|
+
activeLeftPanel === "code"
|
|
16970
|
+
? routeCodeFilename
|
|
16971
|
+
: activeFile?.filename
|
|
16972
|
+
}
|
|
16973
|
+
selectedNodeId={selectedElementLayerId}
|
|
16974
|
+
selectedSelector={selectedCanvasSelector}
|
|
16975
|
+
canEdit={canEditDesign}
|
|
16976
|
+
onActiveFileChange={setActiveCodeFile}
|
|
16977
|
+
/>
|
|
16978
|
+
) : null}
|
|
16979
|
+
</div>
|
|
16818
16980
|
</div>
|
|
16819
16981
|
<div
|
|
16820
16982
|
role="separator"
|
|
@@ -24,12 +24,15 @@
|
|
|
24
24
|
"@paper-design/shaders-react": "0.0.76",
|
|
25
25
|
"@tabler/icons-react": "catalog:",
|
|
26
26
|
"drizzle-orm": "^0.45.2",
|
|
27
|
+
"fzstd": "^0.1.1",
|
|
27
28
|
"h3": "^2.0.1-rc.20",
|
|
28
29
|
"html2canvas": "^1.4.1",
|
|
29
30
|
"isbot": "^5",
|
|
30
31
|
"jspdf": "^4.2.1",
|
|
31
32
|
"jszip": "^3.10.1",
|
|
33
|
+
"kiwi-schema": "^0.5.0",
|
|
32
34
|
"nanoid": "^5.1.9",
|
|
35
|
+
"pako": "^3.0.0",
|
|
33
36
|
"pdf-parse": "^2.4.5",
|
|
34
37
|
"yjs": "^13.6.31",
|
|
35
38
|
"zod": "^4.3.6"
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { getSession } from "@agent-native/core/server";
|
|
4
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
5
|
+
import {
|
|
6
|
+
defineEventHandler,
|
|
7
|
+
getQuery,
|
|
8
|
+
getRequestHeader,
|
|
9
|
+
readMultipartFormData,
|
|
10
|
+
setResponseStatus,
|
|
11
|
+
} from "h3";
|
|
12
|
+
|
|
13
|
+
import { isFigKiwiBuffer, isZipBuffer } from "../lib/figma-import/decode.js";
|
|
14
|
+
import { importFigmaBuffer } from "../lib/figma-import/processor.js";
|
|
15
|
+
import {
|
|
16
|
+
normalizeImportedHtmlDocument,
|
|
17
|
+
saveImportedDesignFiles,
|
|
18
|
+
type ImportedDesignFile,
|
|
19
|
+
} from "../lib/import-design-files.js";
|
|
20
|
+
|
|
21
|
+
const MAX_HTML_BYTES = 2 * 1024 * 1024;
|
|
22
|
+
const MAX_FIG_BYTES = 50 * 1024 * 1024;
|
|
23
|
+
const TOTAL_BODY_LIMIT = MAX_FIG_BYTES + 1024 * 1024;
|
|
24
|
+
|
|
25
|
+
function fieldText(
|
|
26
|
+
parts: Awaited<ReturnType<typeof readMultipartFormData>>,
|
|
27
|
+
name: string,
|
|
28
|
+
) {
|
|
29
|
+
const part = parts?.find((candidate) => candidate.name === name);
|
|
30
|
+
return part?.data
|
|
31
|
+
? Buffer.from(part.data).toString("utf8").trim()
|
|
32
|
+
: undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function statusForError(message: string): number {
|
|
36
|
+
if (/unauthorized/i.test(message)) return 401;
|
|
37
|
+
if (/access|permission|not allowed/i.test(message)) return 403;
|
|
38
|
+
if (/too large|max/i.test(message)) return 413;
|
|
39
|
+
return 400;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const importDesignFile = defineEventHandler(async (event) => {
|
|
43
|
+
const session = await getSession(event).catch(() => null);
|
|
44
|
+
if (!session?.email) {
|
|
45
|
+
setResponseStatus(event, 401);
|
|
46
|
+
return { error: "Unauthorized" };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const rawContentLength = getRequestHeader(event, "content-length");
|
|
50
|
+
const contentLength = Number(rawContentLength);
|
|
51
|
+
if (!rawContentLength || !Number.isFinite(contentLength)) {
|
|
52
|
+
setResponseStatus(event, 411);
|
|
53
|
+
return { error: "Content-Length header is required" };
|
|
54
|
+
}
|
|
55
|
+
if (contentLength > TOTAL_BODY_LIMIT) {
|
|
56
|
+
setResponseStatus(event, 413);
|
|
57
|
+
return { error: "Request body too large" };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const query = getQuery(event);
|
|
62
|
+
const queryDesignId =
|
|
63
|
+
typeof query.designId === "string" ? query.designId.trim() : undefined;
|
|
64
|
+
let accessChecked = false;
|
|
65
|
+
if (queryDesignId) {
|
|
66
|
+
await assertAccess("design", queryDesignId, "editor");
|
|
67
|
+
accessChecked = true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const parts = await readMultipartFormData(event);
|
|
71
|
+
const bodyDesignId = fieldText(parts, "designId");
|
|
72
|
+
if (queryDesignId && bodyDesignId && bodyDesignId !== queryDesignId) {
|
|
73
|
+
setResponseStatus(event, 400);
|
|
74
|
+
return { error: "Mismatched designId" };
|
|
75
|
+
}
|
|
76
|
+
const designId = queryDesignId ?? bodyDesignId;
|
|
77
|
+
const filePart = parts?.find((part) => part.name === "file" && part.data);
|
|
78
|
+
if (!designId) {
|
|
79
|
+
setResponseStatus(event, 400);
|
|
80
|
+
return { error: "Missing designId" };
|
|
81
|
+
}
|
|
82
|
+
if (!accessChecked) {
|
|
83
|
+
await assertAccess("design", designId, "editor");
|
|
84
|
+
}
|
|
85
|
+
if (!filePart?.data) {
|
|
86
|
+
setResponseStatus(event, 400);
|
|
87
|
+
return { error: "No file uploaded" };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const originalName = filePart.filename || "import";
|
|
91
|
+
const ext = path.extname(originalName).toLowerCase();
|
|
92
|
+
const data = Buffer.from(filePart.data);
|
|
93
|
+
|
|
94
|
+
if (ext === ".html" || ext === ".htm") {
|
|
95
|
+
if (data.length > MAX_HTML_BYTES) {
|
|
96
|
+
throw new Error("HTML file is too large (max 2 MB).");
|
|
97
|
+
}
|
|
98
|
+
const saved = await saveImportedDesignFiles({
|
|
99
|
+
designId,
|
|
100
|
+
sourceType: "html-upload",
|
|
101
|
+
files: [
|
|
102
|
+
{
|
|
103
|
+
filename: originalName,
|
|
104
|
+
fileType: "html",
|
|
105
|
+
content: normalizeImportedHtmlDocument(
|
|
106
|
+
data.toString("utf8"),
|
|
107
|
+
"uploaded HTML file",
|
|
108
|
+
),
|
|
109
|
+
source: { sourceType: "html-upload", originalName },
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
importKind: "html",
|
|
115
|
+
...saved,
|
|
116
|
+
stats: { sourceKind: "html-upload", frameCount: saved.files.length },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (ext !== ".fig") {
|
|
121
|
+
throw new Error("Unsupported file type. Upload .fig, .html, or .htm.");
|
|
122
|
+
}
|
|
123
|
+
if (data.length > MAX_FIG_BYTES) {
|
|
124
|
+
throw new Error("Figma file is too large (max 50 MB).");
|
|
125
|
+
}
|
|
126
|
+
if (!isFigKiwiBuffer(data) && !isZipBuffer(data)) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
"Figma file contents do not match .fig or fig-kiwi format.",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const imported = await importFigmaBuffer({
|
|
133
|
+
buffer: data,
|
|
134
|
+
filename: originalName,
|
|
135
|
+
sourceKind: "fig-file",
|
|
136
|
+
});
|
|
137
|
+
const files: ImportedDesignFile[] = imported.files.map((file) => ({
|
|
138
|
+
filename: file.filename,
|
|
139
|
+
fileType: "html",
|
|
140
|
+
content: file.content,
|
|
141
|
+
source: { ...file.source, originalName },
|
|
142
|
+
preferredFrame: {
|
|
143
|
+
title:
|
|
144
|
+
typeof file.source?.frameName === "string"
|
|
145
|
+
? file.source.frameName
|
|
146
|
+
: undefined,
|
|
147
|
+
width: file.width,
|
|
148
|
+
height: file.height,
|
|
149
|
+
},
|
|
150
|
+
}));
|
|
151
|
+
const saved = await saveImportedDesignFiles({
|
|
152
|
+
designId,
|
|
153
|
+
sourceType: "fig-file",
|
|
154
|
+
files,
|
|
155
|
+
warnings: imported.warnings,
|
|
156
|
+
});
|
|
157
|
+
return {
|
|
158
|
+
importKind: "fig",
|
|
159
|
+
...saved,
|
|
160
|
+
stats: imported.stats,
|
|
161
|
+
};
|
|
162
|
+
} catch (error) {
|
|
163
|
+
const message =
|
|
164
|
+
error instanceof Error ? error.message : "File import failed.";
|
|
165
|
+
setResponseStatus(event, statusForError(message));
|
|
166
|
+
return { error: message };
|
|
167
|
+
}
|
|
168
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { FigmaClipboardMeta, ParsedFigmaClipboardHtml } from "./types.js";
|
|
2
|
+
|
|
3
|
+
const MAX_CLIPBOARD_HTML_BYTES = 2 * 1024 * 1024;
|
|
4
|
+
const MAX_DECODED_BUFFER_BYTES = 25 * 1024 * 1024;
|
|
5
|
+
const FIG_KIWI_MAGIC = Buffer.from("fig-kiwi", "utf8");
|
|
6
|
+
|
|
7
|
+
function byteLength(value: string): number {
|
|
8
|
+
return Buffer.byteLength(value, "utf8");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function stripWrapper(value: string, marker: "figmeta" | "figma"): string {
|
|
12
|
+
return value
|
|
13
|
+
.trim()
|
|
14
|
+
.replace(new RegExp(`^<!--\\(${marker}\\)`, "i"), "")
|
|
15
|
+
.replace(new RegExp(`\\(/${marker}\\)-->$`, "i"), "")
|
|
16
|
+
.trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function decodeBase64(value: string, label: string): Buffer {
|
|
20
|
+
const normalized = value.replace(/\s+/g, "");
|
|
21
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(normalized)) {
|
|
22
|
+
throw new Error(`${label} is not valid base64.`);
|
|
23
|
+
}
|
|
24
|
+
const decoded = Buffer.from(normalized, "base64");
|
|
25
|
+
const roundTrip = decoded.toString("base64").replace(/=+$/, "");
|
|
26
|
+
if (roundTrip !== normalized.replace(/=+$/, "")) {
|
|
27
|
+
throw new Error(`${label} is malformed base64.`);
|
|
28
|
+
}
|
|
29
|
+
return decoded;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function extractDataAttribute(html: string, attribute: string): string | null {
|
|
33
|
+
const pattern = new RegExp(`${attribute}\\s*=\\s*(["'])([\\s\\S]*?)\\1`, "i");
|
|
34
|
+
const match = pattern.exec(html);
|
|
35
|
+
return match?.[2] ?? null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function removeFigmaHiddenSpans(html: string): string {
|
|
39
|
+
return html
|
|
40
|
+
.replace(
|
|
41
|
+
/<span\b[^>]*\bdata-metadata\s*=\s*(["'])[\s\S]*?\1[^>]*>\s*<\/span>/gi,
|
|
42
|
+
"",
|
|
43
|
+
)
|
|
44
|
+
.replace(
|
|
45
|
+
/<span\b[^>]*\bdata-buffer\s*=\s*(["'])[\s\S]*?\1[^>]*>\s*<\/span>/gi,
|
|
46
|
+
"",
|
|
47
|
+
)
|
|
48
|
+
.replace(/<meta\b[^>]*charset[^>]*>/gi, "")
|
|
49
|
+
.trim();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeMeta(value: unknown): FigmaClipboardMeta {
|
|
53
|
+
const raw =
|
|
54
|
+
value && typeof value === "object" && !Array.isArray(value)
|
|
55
|
+
? (value as Record<string, unknown>)
|
|
56
|
+
: {};
|
|
57
|
+
const selectedNodeData =
|
|
58
|
+
typeof raw.selectedNodeData === "string" ? raw.selectedNodeData : undefined;
|
|
59
|
+
const selectedNodeId = selectedNodeData?.split("|")[0]?.trim() || undefined;
|
|
60
|
+
return {
|
|
61
|
+
fileKey: typeof raw.fileKey === "string" ? raw.fileKey : undefined,
|
|
62
|
+
pasteID:
|
|
63
|
+
typeof raw.pasteID === "string" || typeof raw.pasteID === "number"
|
|
64
|
+
? raw.pasteID
|
|
65
|
+
: undefined,
|
|
66
|
+
dataType: typeof raw.dataType === "string" ? raw.dataType : undefined,
|
|
67
|
+
environment:
|
|
68
|
+
typeof raw.environment === "string" ? raw.environment : undefined,
|
|
69
|
+
selectedNodeData,
|
|
70
|
+
selectedNodeId,
|
|
71
|
+
raw,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function looksLikeFigmaClipboardHtml(html: string): boolean {
|
|
76
|
+
return /\(figmeta\)|\(figma\)|data-metadata=|data-buffer=/i.test(html);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function parseFigmaClipboardHtml(
|
|
80
|
+
html: string,
|
|
81
|
+
): ParsedFigmaClipboardHtml {
|
|
82
|
+
if (byteLength(html) > MAX_CLIPBOARD_HTML_BYTES) {
|
|
83
|
+
throw new Error("Clipboard HTML is too large to import (max 2 MB).");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const metadataAttr = extractDataAttribute(html, "data-metadata");
|
|
87
|
+
const bufferAttr = extractDataAttribute(html, "data-buffer");
|
|
88
|
+
const fallbackHtml = removeFigmaHiddenSpans(html);
|
|
89
|
+
|
|
90
|
+
let meta: FigmaClipboardMeta | null = null;
|
|
91
|
+
if (metadataAttr) {
|
|
92
|
+
const metadataBase64 = stripWrapper(metadataAttr, "figmeta");
|
|
93
|
+
const metadataJson = decodeBase64(
|
|
94
|
+
metadataBase64,
|
|
95
|
+
"Figma metadata",
|
|
96
|
+
).toString("utf8");
|
|
97
|
+
meta = normalizeMeta(JSON.parse(metadataJson));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let buffer: Buffer | undefined;
|
|
101
|
+
if (bufferAttr) {
|
|
102
|
+
const bufferBase64 = stripWrapper(bufferAttr, "figma");
|
|
103
|
+
buffer = decodeBase64(bufferBase64, "Figma buffer");
|
|
104
|
+
if (buffer.length > MAX_DECODED_BUFFER_BYTES) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
"Figma clipboard buffer is too large to import (max 25 MB).",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const hasFigmaBuffer =
|
|
112
|
+
!!buffer &&
|
|
113
|
+
buffer.length >= FIG_KIWI_MAGIC.length &&
|
|
114
|
+
buffer.subarray(0, FIG_KIWI_MAGIC.length).equals(FIG_KIWI_MAGIC);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
meta,
|
|
118
|
+
buffer,
|
|
119
|
+
hasFigmaBuffer,
|
|
120
|
+
fallbackHtml: fallbackHtml || undefined,
|
|
121
|
+
};
|
|
122
|
+
}
|