@agent-native/toolkit 0.198.7-nightly-20260930213124 → 0.198.7

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 (50) hide show
  1. package/dist/app/chat/AgentPanel.d.ts +4 -3
  2. package/dist/app/chat/AgentPanel.d.ts.map +1 -1
  3. package/dist/app/chat/AgentPanel.js +14 -24
  4. package/dist/app/chat/AgentPanel.js.map +1 -1
  5. package/dist/app/chat/ConnectBuilderCard.d.ts.map +1 -1
  6. package/dist/app/chat/ConnectBuilderCard.js +2 -1
  7. package/dist/app/chat/ConnectBuilderCard.js.map +1 -1
  8. package/dist/app/chat/MultiTabAssistantChat.d.ts.map +1 -1
  9. package/dist/app/chat/MultiTabAssistantChat.js +21 -2
  10. package/dist/app/chat/MultiTabAssistantChat.js.map +1 -1
  11. package/dist/composer/ComposerContextMenu.d.ts +2 -2
  12. package/dist/composer/ComposerContextMenu.d.ts.map +1 -1
  13. package/dist/composer/ComposerContextMenu.js +54 -66
  14. package/dist/composer/ComposerContextMenu.js.map +1 -1
  15. package/dist/composer/ComposerPlusMenu.d.ts +2 -3
  16. package/dist/composer/ComposerPlusMenu.d.ts.map +1 -1
  17. package/dist/composer/ComposerPlusMenu.js +13 -278
  18. package/dist/composer/ComposerPlusMenu.js.map +1 -1
  19. package/dist/composer/TiptapComposer.d.ts.map +1 -1
  20. package/dist/composer/TiptapComposer.js +3 -12
  21. package/dist/composer/TiptapComposer.js.map +1 -1
  22. package/dist/composer/use-composer-panel-placement.d.ts +0 -1
  23. package/dist/composer/use-composer-panel-placement.d.ts.map +1 -1
  24. package/dist/composer/use-composer-panel-placement.js +4 -5
  25. package/dist/composer/use-composer-panel-placement.js.map +1 -1
  26. package/package.json +4 -4
  27. package/src/app/chat/AgentPanel.header.spec.ts +25 -27
  28. package/src/app/chat/AgentPanel.overlay.spec.tsx +14 -51
  29. package/src/app/chat/AgentPanel.tsx +15 -26
  30. package/src/app/chat/ConnectBuilderCard.spec.tsx +45 -0
  31. package/src/app/chat/ConnectBuilderCard.tsx +3 -2
  32. package/src/app/chat/MultiTabAssistantChat.spec.tsx +12 -0
  33. package/src/app/chat/MultiTabAssistantChat.tsx +26 -2
  34. package/src/app/chat/composer/use-agentkit-integration-menu.spec.tsx +4 -2
  35. package/src/composer/ComposerContextMenu.spec.tsx +68 -60
  36. package/src/composer/ComposerContextMenu.tsx +93 -110
  37. package/src/composer/ComposerContextPicker.spec.tsx +16 -14
  38. package/src/composer/ComposerContextPickerDialog.spec.tsx +8 -6
  39. package/src/composer/ComposerPlusMenu.parity.spec.tsx +37 -75
  40. package/src/composer/ComposerPlusMenu.tsx +26 -450
  41. package/src/composer/PromptComposer.context.spec.tsx +112 -18
  42. package/src/composer/TiptapComposer.spec.ts +15 -0
  43. package/src/composer/TiptapComposer.tsx +2 -12
  44. package/src/composer/use-composer-panel-placement.ts +7 -5
  45. package/dist/composer/asset-picker-url.d.ts +0 -10
  46. package/dist/composer/asset-picker-url.d.ts.map +0 -1
  47. package/dist/composer/asset-picker-url.js +0 -56
  48. package/dist/composer/asset-picker-url.js.map +0 -1
  49. package/src/composer/asset-picker-url.spec.ts +0 -56
  50. package/src/composer/asset-picker-url.ts +0 -72
@@ -1,14 +1,6 @@
1
1
  import { useComposerRuntime } from "@assistant-ui/react";
2
- import { IconArrowLeft, IconLoader2, IconX } from "@tabler/icons-react";
3
- import React, {
4
- useCallback,
5
- useEffect,
6
- useId,
7
- useMemo,
8
- useRef,
9
- useState,
10
- } from "react";
11
- import { createPortal } from "react-dom";
2
+ import { IconArrowLeft, IconLoader2 } from "@tabler/icons-react";
3
+ import { useCallback, useEffect, useId, useRef, useState } from "react";
12
4
 
13
5
  import { Button } from "../ui/button.js";
14
6
  import { DropdownMenuGroup, DropdownMenuItem } from "../ui/dropdown-menu.js";
@@ -16,11 +8,6 @@ import { Input } from "../ui/input.js";
16
8
  import { Label } from "../ui/label.js";
17
9
  import { Textarea } from "../ui/textarea.js";
18
10
  import { cn } from "../utils.js";
19
- import {
20
- createAssetPickerHandoffId,
21
- isExternalAssetPickerUrl,
22
- standaloneAssetPickerUrl,
23
- } from "./asset-picker-url.js";
24
11
  import { formatAttachmentError } from "./attachment-accept.js";
25
12
  import {
26
13
  ComposerContextMenu,
@@ -51,137 +38,12 @@ interface ComposerPlusMenuProps extends Omit<
51
38
 
52
39
  type View = "menu" | "skill-upload";
53
40
 
54
- const DEFAULT_ASSETS_PICKER_URL = "https://assets.agent-native.com/picker";
55
- const EMBED_PROTOCOL = "agent-native.embed";
56
- const EMBED_VERSION = 1;
57
-
58
41
  export function isExtensionComposerMenuEnabled(
59
42
  extensionTools?: boolean,
60
43
  ): boolean {
61
44
  return extensionTools === true;
62
45
  }
63
46
 
64
- interface EmbedEnvelope<TPayload = unknown> {
65
- protocol?: string;
66
- version?: number;
67
- type?: string;
68
- name?: string;
69
- payload?: TPayload;
70
- }
71
-
72
- interface AssetPickerPayload {
73
- assetId?: unknown;
74
- handoffId?: unknown;
75
- url?: unknown;
76
- previewUrl?: unknown;
77
- downloadUrl?: unknown;
78
- embedUrl?: unknown;
79
- altText?: unknown;
80
- title?: unknown;
81
- prompt?: unknown;
82
- mediaType?: unknown;
83
- libraryId?: unknown;
84
- }
85
-
86
- function assetPickerUrl() {
87
- const env =
88
- (import.meta as ImportMeta & { env?: Record<string, string | undefined> })
89
- .env ?? {};
90
- return env.VITE_AGENT_NATIVE_ASSETS_PICKER_URL || DEFAULT_ASSETS_PICKER_URL;
91
- }
92
-
93
- function withEmbeddedParams(url: string): string {
94
- try {
95
- const parsed = new URL(url, window.location.href);
96
- parsed.searchParams.set("embedded", "1");
97
- parsed.searchParams.set("mediaType", "image");
98
- return parsed.toString();
99
- } catch {
100
- const separator = url.includes("?") ? "&" : "?";
101
- return `${url}${separator}embedded=1&mediaType=image`;
102
- }
103
- }
104
-
105
- function assetPickerOrigin(url: string): string | null {
106
- try {
107
- return new URL(url, window.location.href).origin;
108
- } catch {
109
- return null;
110
- }
111
- }
112
-
113
- function embedEnvelope(
114
- type: "message" | "ready",
115
- options: { name?: string; payload?: unknown } = {},
116
- ): EmbedEnvelope {
117
- return {
118
- protocol: EMBED_PROTOCOL,
119
- version: EMBED_VERSION,
120
- type,
121
- ...options,
122
- };
123
- }
124
-
125
- function isEmbedEnvelope(value: unknown): value is EmbedEnvelope {
126
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
127
- const candidate = value as EmbedEnvelope;
128
- return (
129
- candidate.protocol === EMBED_PROTOCOL &&
130
- candidate.version === EMBED_VERSION &&
131
- typeof candidate.type === "string"
132
- );
133
- }
134
-
135
- function assetString(value: unknown): string | null {
136
- return typeof value === "string" && value.trim() ? value.trim() : null;
137
- }
138
-
139
- function assetImageSource(payload: unknown): string | null {
140
- if (!payload || typeof payload !== "object") return null;
141
- const asset = payload as AssetPickerPayload;
142
- return (
143
- assetString(asset.url) ??
144
- assetString(asset.previewUrl) ??
145
- assetString(asset.downloadUrl) ??
146
- assetString(asset.embedUrl)
147
- );
148
- }
149
-
150
- function assetTitle(
151
- payload: unknown,
152
- url: string,
153
- generatedImageLabel: string,
154
- ): string {
155
- if (payload && typeof payload === "object") {
156
- const title = assetString((payload as AssetPickerPayload).title);
157
- if (title) return title;
158
- const prompt = assetString((payload as AssetPickerPayload).prompt);
159
- if (prompt) return prompt.slice(0, 80);
160
- }
161
- try {
162
- const name = new URL(url).pathname.split("/").filter(Boolean).pop();
163
- return name ? decodeURIComponent(name) : generatedImageLabel;
164
- } catch {
165
- return generatedImageLabel;
166
- }
167
- }
168
-
169
- function assetContext(payload: unknown, url: string): string {
170
- const lines = [`Image URL: ${url}`];
171
- if (payload && typeof payload === "object") {
172
- const asset = payload as AssetPickerPayload;
173
- const assetId = assetString(asset.assetId);
174
- const libraryId = assetString(asset.libraryId);
175
- const prompt = assetString(asset.prompt);
176
- const altText = assetString(asset.altText);
177
- if (assetId) lines.push(`Asset ID: ${assetId}`);
178
- if (libraryId) lines.push(`Library ID: ${libraryId}`);
179
- if (prompt) lines.push(`Prompt: ${prompt}`);
180
- if (altText) lines.push(`Alt text: ${altText}`);
181
- }
182
- return lines.join("\n");
183
- }
184
-
185
47
  function slugifyName(value: string): string {
186
48
  return (
187
49
  value
@@ -218,7 +80,6 @@ export function useComposerDefaultActions({
218
80
  const adapters = useComposerRuntimeAdapters();
219
81
  const t = adapters.translate!;
220
82
  const resources = adapters.resources!;
221
- const [assetsPickerOpen, setAssetsPickerOpen] = useState(false);
222
83
  const [mcpDialogOpen, setMcpDialogOpen] = useState(false);
223
84
  const overlayOpen = useRef(false);
224
85
  const showMcpIntegrations = resources.isMcpIntegrationAvailable!();
@@ -253,7 +114,6 @@ export function useComposerDefaultActions({
253
114
  useEffect(() => {
254
115
  if (!disabled) return;
255
116
  cancelSkillWork();
256
- setAssetsPickerOpen(false);
257
117
  setMcpDialogOpen(false);
258
118
  overlayOpen.current = false;
259
119
  }, [disabled, cancelSkillWork]);
@@ -365,21 +225,6 @@ export function useComposerDefaultActions({
365
225
  };
366
226
 
367
227
  const items: ComposerContextMenuItem[] = [
368
- {
369
- id: "generate-image",
370
- label: t("agentChat.composer.menu.generateImage", {
371
- defaultValue: "Generate Image",
372
- }),
373
- keywords: [
374
- t("agentChat.composer.menu.generateImageDescription", {
375
- defaultValue: "Open the Assets image picker",
376
- }),
377
- ],
378
- onSelect: () => {
379
- overlayOpen.current = true;
380
- setAssetsPickerOpen(true);
381
- },
382
- },
383
228
  {
384
229
  id: "schedule-task",
385
230
  label: t("agentChat.composer.menu.scheduleTask", {
@@ -626,14 +471,6 @@ export function useComposerDefaultActions({
626
471
  onCreateMcpServer={(args: unknown) => createMcp.mutateAsync(args)}
627
472
  />
628
473
  ) : null}
629
- <AssetsPickerModal
630
- open={assetsPickerOpen}
631
- onOpenChange={(open) => {
632
- overlayOpen.current = open;
633
- setAssetsPickerOpen(open);
634
- if (!open) onRestoreFocus?.();
635
- }}
636
- />
637
474
  </>
638
475
  ),
639
476
  };
@@ -642,14 +479,23 @@ export function useComposerDefaultActions({
642
479
  function ComposerFullContextMenu(props: ComposerPlusMenuProps) {
643
480
  const defaults = useComposerDefaultActions(props);
644
481
  const runtime = useComposerRuntime();
482
+ const mergedItems = mergeComposerMenuItems(
483
+ defaults.items,
484
+ props.contextMenuItems ?? [],
485
+ );
486
+ const defaultActionIds = new Set(defaults.items.map((item) => item.id));
487
+ const menuActionItems = mergedItems.filter((item) =>
488
+ defaultActionIds.has(item.id),
489
+ );
490
+ const contextItems = mergedItems.filter(
491
+ (item) => !defaultActionIds.has(item.id),
492
+ );
645
493
  return (
646
494
  <>
647
495
  <ComposerContextMenu
648
496
  {...props}
649
- items={mergeComposerMenuItems(
650
- defaults.items,
651
- props.contextMenuItems ?? [],
652
- )}
497
+ items={contextItems}
498
+ menuActionItems={menuActionItems}
653
499
  addAttachment={
654
500
  props.attachmentsEnabled === false
655
501
  ? undefined
@@ -706,14 +552,20 @@ export function ComposerPlusMenu({
706
552
  },
707
553
  ]
708
554
  : [];
555
+ const mergedTerminalItems =
556
+ mode === "terminal"
557
+ ? mergeComposerMenuItems(terminalItems, contextMenuItems)
558
+ : contextMenuItems;
559
+ const terminalActionIds = new Set(terminalItems.map((item) => item.id));
709
560
  return (
710
561
  <ComposerContextMenu
711
562
  {...props}
712
- items={
713
- mode === "terminal"
714
- ? mergeComposerMenuItems(terminalItems, contextMenuItems)
715
- : contextMenuItems
716
- }
563
+ items={mergedTerminalItems.filter(
564
+ (item) => !terminalActionIds.has(item.id),
565
+ )}
566
+ menuActionItems={mergedTerminalItems.filter((item) =>
567
+ terminalActionIds.has(item.id),
568
+ )}
717
569
  addAttachment={
718
570
  mode === "upload-only" && attachmentsEnabled
719
571
  ? (props.addAttachment ?? runtime.addAttachment)
@@ -725,279 +577,3 @@ export function ComposerPlusMenu({
725
577
  />
726
578
  );
727
579
  }
728
-
729
- function AssetsPickerModal({
730
- open,
731
- onOpenChange,
732
- }: {
733
- open: boolean;
734
- onOpenChange: (open: boolean) => void;
735
- }) {
736
- const adapters = useComposerRuntimeAdapters();
737
- const t = adapters.translate!;
738
- const iframeRef = useRef<HTMLIFrameElement | null>(null);
739
- const standaloneWindowRef = useRef<Window | null>(null);
740
- const [pickerReady, setPickerReady] = useState(false);
741
- const [standaloneHandoffId, setStandaloneHandoffId] = useState<string | null>(
742
- null,
743
- );
744
- const sourceUrl = useMemo(() => assetPickerUrl(), []);
745
- const externalPicker = useMemo(
746
- () =>
747
- typeof window !== "undefined" &&
748
- isExternalAssetPickerUrl(sourceUrl, window.location.origin),
749
- [sourceUrl],
750
- );
751
- const standaloneUrl = useMemo(
752
- () =>
753
- standaloneAssetPickerUrl(
754
- sourceUrl,
755
- typeof window !== "undefined" ? window.location.href : undefined,
756
- {
757
- handoffId: standaloneHandoffId ?? undefined,
758
- returnOrigin:
759
- typeof window !== "undefined" ? window.location.origin : undefined,
760
- },
761
- ),
762
- [sourceUrl, standaloneHandoffId],
763
- );
764
- const iframeUrl = useMemo(() => withEmbeddedParams(sourceUrl), [sourceUrl]);
765
- const targetOrigin = useMemo(() => assetPickerOrigin(iframeUrl), [iframeUrl]);
766
- const configurePicker = useCallback(() => {
767
- if (!targetOrigin) return;
768
- iframeRef.current?.contentWindow?.postMessage(
769
- embedEnvelope("message", {
770
- name: "configure",
771
- payload: { mediaType: "image", count: 3 },
772
- }),
773
- targetOrigin,
774
- );
775
- }, [targetOrigin]);
776
-
777
- useEffect(() => {
778
- if (open) {
779
- setPickerReady(false);
780
- if (externalPicker) {
781
- standaloneWindowRef.current = null;
782
- setStandaloneHandoffId(createAssetPickerHandoffId());
783
- } else {
784
- setStandaloneHandoffId(null);
785
- }
786
- return;
787
- }
788
- if (!standaloneWindowRef.current) setStandaloneHandoffId(null);
789
- }, [externalPicker, iframeUrl, open]);
790
-
791
- useEffect(() => {
792
- if (
793
- !targetOrigin ||
794
- (!open && !standaloneWindowRef.current) ||
795
- (externalPicker && !standaloneHandoffId)
796
- )
797
- return;
798
-
799
- const handleMessage = (event: MessageEvent) => {
800
- const expectedSource = externalPicker
801
- ? standaloneWindowRef.current
802
- : iframeRef.current?.contentWindow;
803
- if (!expectedSource || event.source !== expectedSource) return;
804
- if (event.origin !== targetOrigin) return;
805
- if (!isEmbedEnvelope(event.data)) return;
806
-
807
- if (event.data.type === "ready") {
808
- setPickerReady(true);
809
- configurePicker();
810
- return;
811
- }
812
-
813
- if (event.data.type !== "message") return;
814
- if (externalPicker) {
815
- const payload = event.data.payload;
816
- const handoffId =
817
- payload && typeof payload === "object"
818
- ? assetString((payload as AssetPickerPayload).handoffId)
819
- : null;
820
- if (handoffId !== standaloneHandoffId) return;
821
- }
822
- if (event.data.name === "close") {
823
- onOpenChange(false);
824
- return;
825
- }
826
- if (
827
- event.data.name !== "chooseImage" &&
828
- event.data.name !== "chooseAsset"
829
- )
830
- return;
831
-
832
- const url = assetImageSource(event.data.payload);
833
- if (!url) return;
834
- const title = assetTitle(
835
- event.data.payload,
836
- url,
837
- t("agentChat.composer.assets.generatedImage", {
838
- defaultValue: "Generated image",
839
- }),
840
- );
841
- const assetId =
842
- event.data.payload && typeof event.data.payload === "object"
843
- ? assetString((event.data.payload as AssetPickerPayload).assetId)
844
- : null;
845
- adapters.agentChat!.setContextItem!({
846
- key: `asset-image:${assetId ?? url}`,
847
- title: t("agentChat.composer.assets.contextTitle", {
848
- title,
849
- defaultValue: `Image: ${title}`,
850
- }),
851
- context: assetContext(event.data.payload, url),
852
- });
853
- standaloneWindowRef.current = null;
854
- setStandaloneHandoffId(null);
855
- onOpenChange(false);
856
- };
857
-
858
- const handleKey = (event: KeyboardEvent) => {
859
- if (event.key === "Escape") onOpenChange(false);
860
- };
861
-
862
- window.addEventListener("message", handleMessage);
863
- window.addEventListener("keydown", handleKey);
864
- return () => {
865
- window.removeEventListener("message", handleMessage);
866
- window.removeEventListener("keydown", handleKey);
867
- };
868
- }, [
869
- configurePicker,
870
- adapters,
871
- externalPicker,
872
- onOpenChange,
873
- open,
874
- standaloneHandoffId,
875
- t,
876
- targetOrigin,
877
- ]);
878
-
879
- const openStandalonePicker = useCallback(
880
- (event: React.MouseEvent<HTMLAnchorElement>) => {
881
- if (!standaloneHandoffId) return;
882
- event.preventDefault();
883
- const pickerWindow = window.open(standaloneUrl, "_blank");
884
- if (!pickerWindow) return;
885
- standaloneWindowRef.current = pickerWindow;
886
- onOpenChange(false);
887
- },
888
- [onOpenChange, standaloneHandoffId, standaloneUrl],
889
- );
890
-
891
- if (!open || typeof document === "undefined") return null;
892
-
893
- return createPortal(
894
- <div
895
- className="fixed inset-0 z-[280] flex items-center justify-center bg-black/50 p-3"
896
- role="dialog"
897
- aria-modal="true"
898
- aria-labelledby="composer-assets-picker-title"
899
- onMouseDown={(event) => {
900
- if (event.target === event.currentTarget) onOpenChange(false);
901
- }}
902
- >
903
- <div className="flex h-[min(86vh,760px)] w-[min(96vw,1040px)] flex-col overflow-hidden rounded-xl border border-border bg-background shadow-2xl">
904
- <div className="flex h-12 shrink-0 items-center justify-between border-b border-border px-4">
905
- <div
906
- id="composer-assets-picker-title"
907
- className="text-sm font-medium text-foreground"
908
- >
909
- {t("agentChat.composer.assets.generateImage", {
910
- defaultValue: "Generate image",
911
- })}
912
- </div>
913
- <button
914
- type="button"
915
- onClick={() => onOpenChange(false)}
916
- className="flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground"
917
- aria-label={t("agentChat.composer.assets.closePicker", {
918
- defaultValue: "Close image picker",
919
- })}
920
- >
921
- <IconX className="h-4 w-4" />
922
- </button>
923
- </div>
924
- {externalPicker ? (
925
- <div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-3 p-8 text-center">
926
- <div className="max-w-md text-sm text-muted-foreground">
927
- {t("agentChat.composer.assets.openSecurely", {
928
- defaultValue:
929
- "Open Assets in a new tab to sign in and choose an image securely.",
930
- })}
931
- </div>
932
- <a
933
- href={standaloneUrl}
934
- target="_blank"
935
- onClick={openStandalonePicker}
936
- className="inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground hover:bg-primary/90"
937
- >
938
- {t("agentChat.composer.assets.openPicker", {
939
- defaultValue: "Open Assets image picker",
940
- })}
941
- </a>
942
- </div>
943
- ) : targetOrigin ? (
944
- <div className="relative min-h-0 flex-1 overflow-hidden bg-background">
945
- {!pickerReady && <AssetsPickerLoadingSkeleton />}
946
- <iframe
947
- ref={iframeRef}
948
- src={iframeUrl}
949
- title={t("agentChat.composer.assets.pickerTitle", {
950
- defaultValue: "Assets image picker",
951
- })}
952
- className={cn(
953
- "absolute inset-0 h-full w-full border-0 bg-background transition-opacity duration-150",
954
- pickerReady ? "opacity-100" : "pointer-events-none opacity-0",
955
- )}
956
- sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-downloads"
957
- allow="clipboard-read; clipboard-write; microphone; fullscreen"
958
- referrerPolicy="strict-origin-when-cross-origin"
959
- onLoad={() => {
960
- configurePicker();
961
- setPickerReady(true);
962
- }}
963
- />
964
- </div>
965
- ) : (
966
- <div className="flex min-h-0 flex-1 items-center justify-center p-8 text-center text-sm text-muted-foreground">
967
- {t("agentChat.composer.assets.invalidUrl", {
968
- defaultValue: "The configured image picker URL is not valid.",
969
- })}
970
- </div>
971
- )}
972
- </div>
973
- </div>,
974
- document.body,
975
- );
976
- }
977
-
978
- function AssetsPickerLoadingSkeleton() {
979
- const t = useComposerRuntimeAdapters().translate!;
980
- return (
981
- <div
982
- className="absolute inset-0 flex flex-col gap-5 p-5"
983
- role="status"
984
- aria-label={t("agentChat.composer.assets.loadingPicker", {
985
- defaultValue: "Loading Assets picker",
986
- })}
987
- >
988
- <div className="flex items-center gap-3">
989
- <div className="h-9 flex-1 animate-pulse rounded-md bg-muted" />
990
- <div className="h-9 w-24 animate-pulse rounded-md bg-muted" />
991
- </div>
992
- <div className="grid min-h-0 flex-1 grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4">
993
- {Array.from({ length: 8 }).map((_, index) => (
994
- <div key={index} className="flex min-w-0 flex-col gap-2">
995
- <div className="aspect-square w-full animate-pulse rounded-lg bg-muted" />
996
- <div className="h-3 w-3/4 animate-pulse rounded bg-muted" />
997
- <div className="h-3 w-1/2 animate-pulse rounded bg-muted" />
998
- </div>
999
- ))}
1000
- </div>
1001
- </div>
1002
- );
1003
- }