@agent-native/core 0.84.57 → 0.84.59

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 (68) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +19 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/action.ts +87 -2
  5. package/corpus/core/src/agent/production-agent.ts +24 -46
  6. package/corpus/core/src/client/AssistantChat.tsx +69 -12
  7. package/corpus/core/src/client/MultiTabAssistantChat.tsx +1 -1
  8. package/corpus/core/src/client/conversation/use-near-bottom-autoscroll.ts +45 -2
  9. package/corpus/core/src/coding-tools/run-code.ts +1 -0
  10. package/corpus/core/src/integrations/webhook-handler.ts +10 -9
  11. package/corpus/core/src/server/action-discovery.ts +3 -0
  12. package/corpus/core/src/server/credential-provider.ts +11 -6
  13. package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +29 -0
  14. package/corpus/templates/analytics/server/plugins/agent-chat.ts +9 -1
  15. package/corpus/templates/design/actions/list-design-extensions.ts +2 -2
  16. package/corpus/templates/design/app/components/design/CodeWorkbenchHost.tsx +11 -89
  17. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +314 -181
  18. package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +92 -134
  19. package/corpus/templates/design/app/components/design/EditPanel.tsx +93 -19
  20. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +18 -26
  21. package/corpus/templates/design/app/components/design/TokensPanel.tsx +220 -192
  22. package/corpus/templates/design/app/components/design/code-workbench-theme.ts +150 -0
  23. package/corpus/templates/design/app/i18n/zh-TW.ts +5 -7
  24. package/corpus/templates/design/app/i18n-data.ts +61 -77
  25. package/corpus/templates/design/app/lib/design-import.ts +4 -1
  26. package/corpus/templates/design/app/pages/DesignEditor.tsx +97 -48
  27. package/corpus/templates/design/changelog/2026-07-02-design-inspector-and-canvas-controls-stay-consistent-when-se.md +6 -0
  28. package/corpus/templates/design/changelog/2026-07-02-fixed-a-crash-when-opening-the-code-editor.md +6 -0
  29. package/corpus/templates/design/changelog/2026-07-02-fixed-motion-drawer-open-transition.md +6 -0
  30. package/corpus/templates/design/changelog/2026-07-02-improved-design-assets-panel.md +6 -0
  31. package/dist/action.d.ts +8 -0
  32. package/dist/action.d.ts.map +1 -1
  33. package/dist/action.js +79 -2
  34. package/dist/action.js.map +1 -1
  35. package/dist/agent/production-agent.d.ts +10 -11
  36. package/dist/agent/production-agent.d.ts.map +1 -1
  37. package/dist/agent/production-agent.js +21 -44
  38. package/dist/agent/production-agent.js.map +1 -1
  39. package/dist/client/AssistantChat.d.ts +1 -0
  40. package/dist/client/AssistantChat.d.ts.map +1 -1
  41. package/dist/client/AssistantChat.js +61 -11
  42. package/dist/client/AssistantChat.js.map +1 -1
  43. package/dist/client/MultiTabAssistantChat.js +1 -1
  44. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  45. package/dist/client/conversation/use-near-bottom-autoscroll.d.ts.map +1 -1
  46. package/dist/client/conversation/use-near-bottom-autoscroll.js +43 -2
  47. package/dist/client/conversation/use-near-bottom-autoscroll.js.map +1 -1
  48. package/dist/coding-tools/run-code.d.ts.map +1 -1
  49. package/dist/coding-tools/run-code.js +1 -0
  50. package/dist/coding-tools/run-code.js.map +1 -1
  51. package/dist/collab/awareness.d.ts +2 -2
  52. package/dist/collab/awareness.d.ts.map +1 -1
  53. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  54. package/dist/integrations/webhook-handler.d.ts +1 -0
  55. package/dist/integrations/webhook-handler.d.ts.map +1 -1
  56. package/dist/integrations/webhook-handler.js +9 -9
  57. package/dist/integrations/webhook-handler.js.map +1 -1
  58. package/dist/notifications/routes.d.ts +3 -3
  59. package/dist/observability/routes.d.ts +4 -4
  60. package/dist/server/action-discovery.d.ts.map +1 -1
  61. package/dist/server/action-discovery.js +3 -0
  62. package/dist/server/action-discovery.js.map +1 -1
  63. package/dist/server/credential-provider.d.ts +4 -0
  64. package/dist/server/credential-provider.d.ts.map +1 -1
  65. package/dist/server/credential-provider.js +13 -7
  66. package/dist/server/credential-provider.js.map +1 -1
  67. package/dist/server/transcribe-voice.d.ts +1 -1
  68. package/package.json +1 -1
@@ -19,6 +19,7 @@ import {
19
19
  IconChevronDown,
20
20
  IconExternalLink,
21
21
  IconLock,
22
+ IconLayoutGrid,
22
23
  IconMessageCircle,
23
24
  IconPalette,
24
25
  IconPhoto,
@@ -34,6 +35,7 @@ import {
34
35
  useRef,
35
36
  useState,
36
37
  type FormEvent,
38
+ type DragEvent as ReactDragEvent,
37
39
  } from "react";
38
40
  import { toast } from "sonner";
39
41
 
@@ -150,6 +152,7 @@ type FirstPartyRow = {
150
152
 
151
153
  const DEFAULT_ASSETS_PICKER_URL =
152
154
  "https://assets.agent-native.com/library?__an_picker=1&mediaType=image&layout=vertical";
155
+ const SHOW_FIGMA_ASSET_TAB = true;
153
156
 
154
157
  interface PickedAssetPayload {
155
158
  url?: unknown;
@@ -368,11 +371,11 @@ function ToolFilterMenu<T extends string>({
368
371
  type="button"
369
372
  variant="outline"
370
373
  size="sm"
371
- className="h-8 cursor-pointer gap-1.5 rounded-md bg-transparent px-2.5 text-sm font-medium"
374
+ className="h-7 cursor-pointer gap-1 rounded-md bg-transparent px-2 !text-[11px] font-medium"
372
375
  >
373
- <IconAdjustmentsHorizontal className="size-4 text-muted-foreground" />
376
+ <IconAdjustmentsHorizontal className="size-3 text-muted-foreground" />
374
377
  <span>{triggerLabel}</span>
375
- <IconChevronDown className="size-3.5 text-muted-foreground" />
378
+ <IconChevronDown className="size-3 text-muted-foreground" />
376
379
  </Button>
377
380
  </DropdownMenuTrigger>
378
381
  <DropdownMenuContent align="start" className="min-w-36">
@@ -451,7 +454,12 @@ const NATIVE_ASSET_CATEGORY_LABELS: Record<
451
454
  };
452
455
 
453
456
  export function AssetLibraryPanel({ context }: AssetLibraryPanelProps) {
457
+ const panelRef = useRef<HTMLDivElement | null>(null);
454
458
  const [assetPickerReady, setAssetPickerReady] = useState(false);
459
+ const [nativeSearchQuery, setNativeSearchQuery] = useState("");
460
+ const [draggedNativeAsset, setDraggedNativeAsset] =
461
+ useState<DesignNativeAsset | null>(null);
462
+ const [dropOverlayLeft, setDropOverlayLeft] = useState<number | null>(null);
455
463
  const [figmaFileInput, setFigmaFileInput] = useState("");
456
464
  const [figmaQueryInput, setFigmaQueryInput] = useState("");
457
465
  const [figmaRequest, setFigmaRequest] = useState<{
@@ -476,6 +484,24 @@ export function AssetLibraryPanel({ context }: AssetLibraryPanelProps) {
476
484
  : undefined,
477
485
  { enabled: Boolean(figmaRequest) },
478
486
  );
487
+ const filteredNativeAssets = useMemo(() => {
488
+ const assets = nativeAssets.data?.assets ?? [];
489
+ const query = nativeSearchQuery.trim().toLowerCase();
490
+ if (!query) return assets;
491
+ return assets.filter((asset) =>
492
+ [
493
+ asset.title,
494
+ asset.description,
495
+ asset.componentName,
496
+ asset.category,
497
+ NATIVE_ASSET_CATEGORY_LABELS[asset.category],
498
+ ]
499
+ .join(" ")
500
+ .toLowerCase()
501
+ .includes(query),
502
+ );
503
+ }, [nativeAssets.data?.assets, nativeSearchQuery]);
504
+ const canInsertNativeAssets = Boolean(context.activeFileId);
479
505
  const notifyAssetInserted = useCallback(
480
506
  (result: unknown, title: string) => {
481
507
  if (!result || typeof result !== "object") return;
@@ -553,23 +579,68 @@ export function AssetLibraryPanel({ context }: AssetLibraryPanelProps) {
553
579
  [context.designId, context.activeFileId, insertAsset, notifyAssetInserted],
554
580
  );
555
581
 
556
- const handleInsertNativeAsset = (asset: DesignNativeAsset) => {
557
- insertNativeAsset.mutate(
558
- {
559
- kind: asset.kind,
560
- designId: context.designId || undefined,
561
- fileId: context.activeFileId || undefined,
562
- },
563
- {
564
- onSuccess: (result) => {
565
- notifyAssetInserted(result, asset.title);
566
- toast.success(`${asset.title} inserted into design.`);
582
+ const handleInsertNativeAsset = useCallback(
583
+ (asset: DesignNativeAsset) => {
584
+ if (!context.activeFileId) {
585
+ toast.error("Open a design screen first to insert assets.");
586
+ return;
587
+ }
588
+ insertNativeAsset.mutate(
589
+ {
590
+ kind: asset.kind,
591
+ designId: context.designId || undefined,
592
+ fileId: context.activeFileId || undefined,
567
593
  },
568
- onError: (error) => {
569
- toast.error(error.message || "Failed to insert native asset.");
594
+ {
595
+ onSuccess: (result) => {
596
+ notifyAssetInserted(result, asset.title);
597
+ toast.success(`${asset.title} inserted into design.`);
598
+ },
599
+ onError: (error) => {
600
+ toast.error(error.message || "Failed to insert Design asset.");
601
+ },
570
602
  },
571
- },
572
- );
603
+ );
604
+ },
605
+ [
606
+ context.activeFileId,
607
+ context.designId,
608
+ insertNativeAsset,
609
+ notifyAssetInserted,
610
+ ],
611
+ );
612
+
613
+ const clearNativeAssetDrag = useCallback(() => {
614
+ setDraggedNativeAsset(null);
615
+ setDropOverlayLeft(null);
616
+ }, []);
617
+
618
+ useEffect(() => {
619
+ if (!draggedNativeAsset) return;
620
+ window.addEventListener("dragend", clearNativeAssetDrag);
621
+ return () => window.removeEventListener("dragend", clearNativeAssetDrag);
622
+ }, [clearNativeAssetDrag, draggedNativeAsset]);
623
+
624
+ const handleNativeAssetDragStart = (
625
+ event: ReactDragEvent<HTMLDivElement>,
626
+ asset: DesignNativeAsset,
627
+ ) => {
628
+ if (!canInsertNativeAssets || insertNativeAsset.isPending) {
629
+ event.preventDefault();
630
+ return;
631
+ }
632
+ setDraggedNativeAsset(asset);
633
+ setDropOverlayLeft(panelRef.current?.getBoundingClientRect().right ?? 0);
634
+ event.dataTransfer.effectAllowed = "copy";
635
+ event.dataTransfer.setData("text/plain", asset.title);
636
+ };
637
+
638
+ const handleNativeAssetDrop = (event: ReactDragEvent<HTMLDivElement>) => {
639
+ event.preventDefault();
640
+ event.stopPropagation();
641
+ if (!draggedNativeAsset) return;
642
+ handleInsertNativeAsset(draggedNativeAsset);
643
+ clearNativeAssetDrag();
573
644
  };
574
645
 
575
646
  const handleBrowseFigma = (event: FormEvent<HTMLFormElement>) => {
@@ -612,22 +683,32 @@ export function AssetLibraryPanel({ context }: AssetLibraryPanelProps) {
612
683
  };
613
684
 
614
685
  return (
615
- <div className="flex min-h-0 flex-1 flex-col">
686
+ <div ref={panelRef} className="flex min-h-0 flex-1 flex-col">
616
687
  <Tabs defaultValue="native" className="flex min-h-0 flex-1 flex-col">
617
688
  <div className="border-b border-border/60 px-2 py-1.5">
618
- <TabsList className="grid h-7 w-full grid-cols-3 rounded-md p-0.5">
689
+ <TabsList
690
+ className={cn(
691
+ "grid h-7 w-full rounded-md p-0.5",
692
+ SHOW_FIGMA_ASSET_TAB ? "grid-cols-3" : "grid-cols-2",
693
+ )}
694
+ >
619
695
  <TabsTrigger value="native" className="h-6 gap-1 px-2 !text-[11px]">
620
- <IconAssembly className="size-3" />
621
- Native
696
+ <IconLayoutGrid className="size-3" />
697
+ Design
622
698
  </TabsTrigger>
623
699
  <TabsTrigger value="media" className="h-6 gap-1 px-2 !text-[11px]">
624
700
  <IconPhoto className="size-3" />
625
701
  Media
626
702
  </TabsTrigger>
627
- <TabsTrigger value="figma" className="h-6 gap-1 px-2 !text-[11px]">
628
- <IconBrandFigma className="size-3" />
629
- Figma
630
- </TabsTrigger>
703
+ {SHOW_FIGMA_ASSET_TAB ? (
704
+ <TabsTrigger
705
+ value="figma"
706
+ className="h-6 gap-1 px-2 !text-[11px]"
707
+ >
708
+ <IconBrandFigma className="size-3" />
709
+ Figma
710
+ </TabsTrigger>
711
+ ) : null}
631
712
  </TabsList>
632
713
  </div>
633
714
 
@@ -650,35 +731,67 @@ export function AssetLibraryPanel({ context }: AssetLibraryPanelProps) {
650
731
  )}
651
732
 
652
733
  {!nativeAssets.isLoading && nativeAssets.data && (
653
- <div className="space-y-1.5">
654
- {nativeAssets.data.assets.map((asset) => (
655
- <button
656
- key={asset.kind}
657
- type="button"
658
- disabled={
659
- !context.activeFileId || insertNativeAsset.isPending
660
- }
661
- onClick={() => handleInsertNativeAsset(asset)}
662
- className="group flex w-full cursor-pointer items-start gap-2 rounded-md border border-border bg-background px-2 py-2 text-left transition hover:border-primary/50 hover:bg-accent/30 disabled:cursor-not-allowed disabled:opacity-55"
663
- >
664
- <span className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md border border-border/70 bg-muted/50 text-muted-foreground group-hover:text-foreground">
665
- <IconAssembly className="size-4" />
666
- </span>
667
- <span className="min-w-0 flex-1">
668
- <span className="flex items-center gap-1.5">
669
- <span className="truncate !text-[11px] font-medium leading-tight text-foreground">
670
- {asset.title}
734
+ <div className="space-y-2">
735
+ <p className="text-[10px] leading-snug text-muted-foreground">
736
+ Drag Design elements into the canvas to add them to the current
737
+ screen.
738
+ </p>
739
+ <div className="relative">
740
+ <IconSearch className="pointer-events-none absolute left-2 top-1/2 size-3 -translate-y-1/2 text-muted-foreground" />
741
+ <Input
742
+ value={nativeSearchQuery}
743
+ onChange={(event) => setNativeSearchQuery(event.target.value)}
744
+ placeholder="Search Design assets"
745
+ className="h-7 bg-muted/50 pl-7 !text-[11px] shadow-none md:!text-[11px]"
746
+ />
747
+ </div>
748
+ {filteredNativeAssets.length === 0 ? (
749
+ <p className="rounded border border-border bg-muted/30 px-2 py-1.5 text-[10px] leading-snug text-muted-foreground">
750
+ No Design assets match this search.
751
+ </p>
752
+ ) : (
753
+ <div role="list" className="space-y-1.5">
754
+ {filteredNativeAssets.map((asset) => (
755
+ <div
756
+ key={asset.kind}
757
+ role="listitem"
758
+ aria-disabled={
759
+ !canInsertNativeAssets || insertNativeAsset.isPending
760
+ }
761
+ draggable={
762
+ canInsertNativeAssets && !insertNativeAsset.isPending
763
+ }
764
+ onDragStart={(event) =>
765
+ handleNativeAssetDragStart(event, asset)
766
+ }
767
+ onDragEnd={clearNativeAssetDrag}
768
+ className={cn(
769
+ "group flex w-full items-start gap-2 rounded-md border border-border bg-background px-2 py-2 text-left transition hover:border-primary/50 hover:bg-accent/30",
770
+ canInsertNativeAssets && !insertNativeAsset.isPending
771
+ ? "cursor-grab active:cursor-grabbing"
772
+ : "cursor-not-allowed opacity-55",
773
+ )}
774
+ >
775
+ <span className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md border border-border/70 bg-muted/50 text-muted-foreground group-hover:text-foreground">
776
+ <IconLayoutGrid className="size-4" />
671
777
  </span>
672
- <span className="shrink-0 rounded border border-border/70 px-1 py-0.5 text-[9px] leading-none text-muted-foreground">
673
- {NATIVE_ASSET_CATEGORY_LABELS[asset.category]}
778
+ <span className="min-w-0 flex-1">
779
+ <span className="flex items-center gap-1.5">
780
+ <span className="truncate !text-[11px] font-medium leading-tight text-foreground">
781
+ {asset.title}
782
+ </span>
783
+ <span className="shrink-0 rounded border border-border/70 px-1 py-0.5 text-[9px] leading-none text-muted-foreground">
784
+ {NATIVE_ASSET_CATEGORY_LABELS[asset.category]}
785
+ </span>
786
+ </span>
787
+ <span className="mt-1 line-clamp-2 text-[10px] leading-snug text-muted-foreground">
788
+ {asset.description}
789
+ </span>
674
790
  </span>
675
- </span>
676
- <span className="mt-1 line-clamp-2 text-[10px] leading-snug text-muted-foreground">
677
- {asset.description}
678
- </span>
679
- </span>
680
- </button>
681
- ))}
791
+ </div>
792
+ ))}
793
+ </div>
794
+ )}
682
795
  </div>
683
796
  )}
684
797
  </TabsContent>
@@ -704,136 +817,156 @@ export function AssetLibraryPanel({ context }: AssetLibraryPanelProps) {
704
817
  </div>
705
818
  </TabsContent>
706
819
 
707
- <TabsContent
708
- value="figma"
709
- className="design-inspector-scroll m-0 min-h-0 flex-1 overflow-y-auto p-2.5"
710
- >
711
- <form className="space-y-1.5" onSubmit={handleBrowseFigma}>
712
- <Input
713
- value={figmaFileInput}
714
- onChange={(event) => setFigmaFileInput(event.target.value)}
715
- placeholder="Figma file URL or key"
716
- className="h-7 !text-[11px] md:!text-[11px]"
717
- />
718
- <div className="flex gap-1.5">
820
+ {SHOW_FIGMA_ASSET_TAB ? (
821
+ <TabsContent
822
+ value="figma"
823
+ className="design-inspector-scroll m-0 min-h-0 flex-1 overflow-y-auto p-2.5"
824
+ >
825
+ <form className="space-y-1.5" onSubmit={handleBrowseFigma}>
719
826
  <Input
720
- value={figmaQueryInput}
721
- onChange={(event) => setFigmaQueryInput(event.target.value)}
722
- placeholder="Search components"
723
- className="h-7 min-w-0 flex-1 !text-[11px] md:!text-[11px]"
827
+ value={figmaFileInput}
828
+ onChange={(event) => setFigmaFileInput(event.target.value)}
829
+ placeholder="Figma file URL or key"
830
+ className="h-7 !text-[11px] md:!text-[11px]"
724
831
  />
725
- <Button
726
- type="submit"
727
- size="sm"
728
- className="h-7 cursor-pointer gap-1 px-2 !text-[11px]"
729
- disabled={!figmaFileInput.trim() || figmaAssets.isFetching}
730
- >
731
- <IconSearch className="size-3" />
732
- {figmaAssets.isFetching ? "Loading" : "Browse"}
733
- </Button>
734
- </div>
735
- </form>
736
-
737
- {figmaAssets.isError && (
738
- <p className="mt-2 rounded border border-destructive/30 bg-destructive/5 px-2 py-1.5 text-[10px] leading-snug text-destructive">
739
- {figmaAssets.error.message}
740
- </p>
741
- )}
742
-
743
- {figmaAssets.isFetching && (
744
- <div className="mt-2 grid grid-cols-2 gap-1.5">
745
- {Array.from({ length: 4 }).map((_, index) => (
746
- <Skeleton key={index} className="aspect-square rounded-md" />
747
- ))}
748
- </div>
749
- )}
832
+ <div className="flex gap-1.5">
833
+ <Input
834
+ value={figmaQueryInput}
835
+ onChange={(event) => setFigmaQueryInput(event.target.value)}
836
+ placeholder="Search components"
837
+ className="h-7 min-w-0 flex-1 !text-[11px] md:!text-[11px]"
838
+ />
839
+ <Button
840
+ type="submit"
841
+ size="sm"
842
+ className="h-7 cursor-pointer gap-1 px-2 !text-[11px]"
843
+ disabled={!figmaFileInput.trim() || figmaAssets.isFetching}
844
+ >
845
+ <IconSearch className="size-3" />
846
+ {figmaAssets.isFetching ? "Loading" : "Browse"}
847
+ </Button>
848
+ </div>
849
+ </form>
750
850
 
751
- {!figmaAssets.isFetching &&
752
- figmaAssets.data &&
753
- figmaAssets.data.assets.length === 0 && (
754
- <p className="mt-2 rounded border border-border bg-muted/30 px-2 py-1.5 text-[10px] leading-snug text-muted-foreground">
755
- No components matched this Figma file and search.
851
+ {figmaAssets.isError && (
852
+ <p className="mt-2 rounded border border-destructive/30 bg-destructive/5 px-2 py-1.5 text-[10px] leading-snug text-destructive">
853
+ {figmaAssets.error.message}
756
854
  </p>
757
855
  )}
758
856
 
759
- {!figmaAssets.isFetching &&
760
- figmaAssets.data &&
761
- figmaAssets.data.assets.length > 0 && (
762
- <div className="mt-2 space-y-1.5">
763
- <div className="flex items-center justify-between gap-2 text-[10px] text-muted-foreground">
764
- <span>
765
- {figmaAssets.data.returned} of {figmaAssets.data.total}
766
- </span>
767
- <span className="truncate">{figmaAssets.data.fileKey}</span>
768
- </div>
769
- <div className="grid grid-cols-2 gap-1.5">
770
- {figmaAssets.data.assets.map((asset) => {
771
- const preview = asset.thumbnailUrl ?? asset.renderUrl;
772
- const canInsert = Boolean(
773
- context.activeFileId &&
774
- (asset.insertUrl ??
775
- asset.renderUrl ??
776
- asset.thumbnailUrl),
777
- );
778
- return (
779
- <div
780
- key={asset.id}
781
- className="overflow-hidden rounded-md border border-border bg-background"
782
- >
783
- <button
784
- type="button"
785
- className="block aspect-square w-full cursor-pointer bg-muted/30"
786
- disabled={!canInsert || insertFigmaAsset.isPending}
787
- onClick={() => handleInsertFigmaAsset(asset)}
857
+ {figmaAssets.isFetching && (
858
+ <div className="mt-2 grid grid-cols-2 gap-1.5">
859
+ {Array.from({ length: 4 }).map((_, index) => (
860
+ <Skeleton key={index} className="aspect-square rounded-md" />
861
+ ))}
862
+ </div>
863
+ )}
864
+
865
+ {!figmaAssets.isFetching &&
866
+ figmaAssets.data &&
867
+ figmaAssets.data.assets.length === 0 && (
868
+ <p className="mt-2 rounded border border-border bg-muted/30 px-2 py-1.5 text-[10px] leading-snug text-muted-foreground">
869
+ No components matched this Figma file and search.
870
+ </p>
871
+ )}
872
+
873
+ {!figmaAssets.isFetching &&
874
+ figmaAssets.data &&
875
+ figmaAssets.data.assets.length > 0 && (
876
+ <div className="mt-2 space-y-1.5">
877
+ <div className="flex items-center justify-between gap-2 text-[10px] text-muted-foreground">
878
+ <span>
879
+ {figmaAssets.data.returned} of {figmaAssets.data.total}
880
+ </span>
881
+ <span className="truncate">{figmaAssets.data.fileKey}</span>
882
+ </div>
883
+ <div className="grid grid-cols-2 gap-1.5">
884
+ {figmaAssets.data.assets.map((asset) => {
885
+ const preview = asset.thumbnailUrl ?? asset.renderUrl;
886
+ const canInsert = Boolean(
887
+ context.activeFileId &&
888
+ (asset.insertUrl ??
889
+ asset.renderUrl ??
890
+ asset.thumbnailUrl),
891
+ );
892
+ return (
893
+ <div
894
+ key={asset.id}
895
+ className="overflow-hidden rounded-md border border-border bg-background"
788
896
  >
789
- {preview ? (
790
- <img
791
- src={preview}
792
- alt={asset.name}
793
- className="size-full object-contain p-2"
794
- loading="lazy"
795
- />
796
- ) : (
797
- <span className="flex size-full items-center justify-center text-muted-foreground">
798
- <IconAssembly className="size-5" />
799
- </span>
800
- )}
801
- </button>
802
- <div className="border-t border-border/70 px-1.5 py-1">
803
- <div className="flex items-center gap-1">
804
- <span className="min-w-0 flex-1 truncate text-[10px] font-medium leading-tight">
805
- {asset.name}
806
- </span>
807
- {asset.sourceUrl && (
808
- <a
809
- href={asset.sourceUrl}
810
- target="_blank"
811
- rel="noreferrer"
812
- className="shrink-0 text-muted-foreground hover:text-foreground"
813
- aria-label="Open in Figma"
814
- >
815
- <IconExternalLink className="size-3" />
816
- </a>
897
+ <button
898
+ type="button"
899
+ className="block aspect-square w-full cursor-pointer bg-muted/30"
900
+ disabled={!canInsert || insertFigmaAsset.isPending}
901
+ onClick={() => handleInsertFigmaAsset(asset)}
902
+ >
903
+ {preview ? (
904
+ <img
905
+ src={preview}
906
+ alt={asset.name}
907
+ className="size-full object-contain p-2"
908
+ loading="lazy"
909
+ />
910
+ ) : (
911
+ <span className="flex size-full items-center justify-center text-muted-foreground">
912
+ <IconLayoutGrid className="size-5" />
913
+ </span>
817
914
  )}
915
+ </button>
916
+ <div className="border-t border-border/70 px-1.5 py-1">
917
+ <div className="flex items-center gap-1">
918
+ <span className="min-w-0 flex-1 truncate text-[10px] font-medium leading-tight">
919
+ {asset.name}
920
+ </span>
921
+ {asset.sourceUrl && (
922
+ <a
923
+ href={asset.sourceUrl}
924
+ target="_blank"
925
+ rel="noreferrer"
926
+ className="shrink-0 text-muted-foreground hover:text-foreground"
927
+ aria-label="Open in Figma"
928
+ >
929
+ <IconExternalLink className="size-3" />
930
+ </a>
931
+ )}
932
+ </div>
933
+ <p className="mt-0.5 truncate text-[9px] text-muted-foreground">
934
+ {asset.kind === "component_set"
935
+ ? "Component set"
936
+ : "Component"}
937
+ {asset.containingFrame?.name
938
+ ? ` · ${asset.containingFrame.name}`
939
+ : ""}
940
+ </p>
818
941
  </div>
819
- <p className="mt-0.5 truncate text-[9px] text-muted-foreground">
820
- {asset.kind === "component_set"
821
- ? "Component set"
822
- : "Component"}
823
- {asset.containingFrame?.name
824
- ? ` · ${asset.containingFrame.name}`
825
- : ""}
826
- </p>
827
942
  </div>
828
- </div>
829
- );
830
- })}
943
+ );
944
+ })}
945
+ </div>
831
946
  </div>
832
- </div>
833
- )}
834
- </TabsContent>
947
+ )}
948
+ </TabsContent>
949
+ ) : null}
835
950
  </Tabs>
836
951
 
952
+ {draggedNativeAsset && dropOverlayLeft !== null ? (
953
+ <div
954
+ className="fixed bottom-0 right-0 top-0 z-[120] cursor-copy"
955
+ style={{ left: dropOverlayLeft }}
956
+ onDragOver={(event) => {
957
+ event.preventDefault();
958
+ event.dataTransfer.dropEffect = "copy";
959
+ }}
960
+ onDrop={handleNativeAssetDrop}
961
+ >
962
+ <div className="pointer-events-none absolute inset-3 flex items-center justify-center rounded-xl border border-dashed border-primary/50 bg-primary/5 text-primary shadow-[inset_0_0_0_1px_rgba(255,255,255,0.18)]">
963
+ <div className="rounded-md border border-border bg-background/95 px-3 py-2 text-xs font-medium shadow-sm">
964
+ Drop to add {draggedNativeAsset.title} to this design
965
+ </div>
966
+ </div>
967
+ </div>
968
+ ) : null}
969
+
837
970
  {!context.activeFileId && (
838
971
  <p className="border-t border-border/60 px-2.5 py-1.5 text-[10px] text-amber-600 dark:text-amber-400">
839
972
  Open a design screen first to insert assets.
@@ -1310,8 +1443,8 @@ export function DesignExtensionsPanel({
1310
1443
 
1311
1444
  return (
1312
1445
  <div className={cn("flex min-h-0 flex-1 flex-col", className)}>
1313
- <div className="flex h-16 shrink-0 items-center gap-3 border-b border-border/60 px-4">
1314
- <h3 className="min-w-0 flex-1 truncate text-xl font-semibold tracking-tight text-foreground">
1446
+ <div className="flex min-h-8 shrink-0 items-center gap-2 border-b border-border/60 px-3">
1447
+ <h3 className="min-w-0 flex-1 truncate text-xs font-semibold text-foreground">
1315
1448
  {title ?? t("designEditor.extensions")}
1316
1449
  </h3>
1317
1450
  <CreateExtensionPopover
@@ -1321,18 +1454,18 @@ export function DesignExtensionsPanel({
1321
1454
  />
1322
1455
  </div>
1323
1456
 
1324
- <div className="design-inspector-scroll min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pb-4 pt-4">
1325
- <div className="relative mb-4">
1326
- <IconSearch className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
1457
+ <div className="design-inspector-scroll min-h-0 flex-1 overflow-y-auto overscroll-contain px-3 pb-4 pt-3">
1458
+ <div className="relative mb-3">
1459
+ <IconSearch className="pointer-events-none absolute left-2 top-1/2 size-3 -translate-y-1/2 text-muted-foreground" />
1327
1460
  <Input
1328
1461
  value={searchQuery}
1329
1462
  onChange={(event) => setSearchQuery(event.target.value)}
1330
1463
  placeholder="Search all tools"
1331
- className="h-9 border-0 bg-muted/70 pl-8 text-sm shadow-none focus-visible:ring-1"
1464
+ className="h-7 border-0 bg-muted/70 pl-7 !text-[11px] shadow-none focus-visible:ring-1 md:!text-[11px]"
1332
1465
  />
1333
1466
  </div>
1334
1467
 
1335
- <div className="mb-5 flex flex-wrap gap-2">
1468
+ <div className="mb-4 flex flex-wrap gap-1.5">
1336
1469
  <ToolFilterMenu
1337
1470
  label="Source"
1338
1471
  value={sourceFilter}
@@ -1367,7 +1500,7 @@ export function DesignExtensionsPanel({
1367
1500
  <>
1368
1501
  {visibleInstalls.length > 0 ? (
1369
1502
  <div className="mb-6">
1370
- <p className="mb-3 text-sm font-medium text-muted-foreground">
1503
+ <p className="mb-2 text-xs font-medium text-muted-foreground">
1371
1504
  Recents
1372
1505
  </p>
1373
1506
  <div className="space-y-2">
@@ -1387,7 +1520,7 @@ export function DesignExtensionsPanel({
1387
1520
 
1388
1521
  {visibleFirstPartyRows.length > 0 ? (
1389
1522
  <div className="mb-6">
1390
- <p className="mb-3 text-sm font-medium text-muted-foreground">
1523
+ <p className="mb-2 text-xs font-medium text-muted-foreground">
1391
1524
  Suggested
1392
1525
  </p>
1393
1526
  <div className="space-y-1">
@@ -1411,7 +1544,7 @@ export function DesignExtensionsPanel({
1411
1544
 
1412
1545
  {visibleInstallable.length > 0 ? (
1413
1546
  <div className="border-t border-border/60 pt-4">
1414
- <p className="mb-3 text-sm font-medium text-muted-foreground">
1547
+ <p className="mb-2 text-xs font-medium text-muted-foreground">
1415
1548
  Available
1416
1549
  </p>
1417
1550
  <div className="space-y-1">
@@ -1498,7 +1631,7 @@ function CreateExtensionPopover({
1498
1631
  type="button"
1499
1632
  variant="outline"
1500
1633
  size="sm"
1501
- className="h-8 cursor-pointer rounded-md bg-transparent px-3 text-sm font-medium"
1634
+ className="h-6 cursor-pointer rounded-md bg-transparent px-2 !text-[11px] font-medium"
1502
1635
  aria-label={t("designEditor.addExtension")}
1503
1636
  >
1504
1637
  Create