@agent-native/core-corpus 0.217.0 → 0.219.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/hubspot/SKILL.md +5 -3
  3. package/corpus/templates/analytics/CHANGELOG.md +2 -0
  4. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +38 -167
  5. package/corpus/templates/analytics/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
  6. package/corpus/templates/assets/CHANGELOG.md +6 -0
  7. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +51 -117
  8. package/corpus/templates/assets/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
  9. package/corpus/templates/brain/app/components/layout/Sidebar.tsx +34 -101
  10. package/corpus/templates/brain/app/routes/sources.tsx +1 -1
  11. package/corpus/templates/brain/vitest.config.ts +7 -0
  12. package/corpus/templates/calendar/CHANGELOG.md +6 -0
  13. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +1 -1
  14. package/corpus/templates/calendar/changelog/2026-10-16-booking-links-show-localized-visibility-badges.md +6 -0
  15. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +62 -101
  16. package/corpus/templates/clips/CHANGELOG.md +7 -20
  17. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +20 -77
  18. package/corpus/templates/clips/app/components/player/share-dialog.tsx +63 -90
  19. package/corpus/templates/clips/app/components/sharing/share-test-support.tsx +180 -0
  20. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +5 -584
  21. package/corpus/templates/clips/changelog/2026-10-16-share-dialogs-make-it-easier-to-see-who-has-access-and-manag.md +6 -0
  22. package/corpus/templates/clips/changelog/2026-10-21-share-panels-describe-general-access-in-a-sentence.md +6 -0
  23. package/corpus/templates/community/qr/package.json +1 -1
  24. package/corpus/templates/content/CHANGELOG.md +7 -0
  25. package/corpus/templates/content/app/components/editor/CommentStack.tsx +301 -0
  26. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +770 -179
  27. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +75 -27
  28. package/corpus/templates/content/app/components/editor/SuggestionText.tsx +18 -3
  29. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +89 -22
  30. package/corpus/templates/content/app/components/editor/block-field-comment-threads.ts +15 -0
  31. package/corpus/templates/content/app/components/editor/block-field-comments.tsx +5 -9
  32. package/corpus/templates/content/app/components/editor/comment-stacks.ts +143 -0
  33. package/corpus/templates/content/app/components/editor/extensions/SuggestionHighlight.ts +383 -34
  34. package/corpus/templates/content/app/components/editor/suggestions/draft-session.ts +31 -0
  35. package/corpus/templates/content/app/components/editor/suggestions/presentation-rebase.ts +82 -4
  36. package/corpus/templates/content/app/global.css +31 -0
  37. package/corpus/templates/content/app/i18n-data.ts +8 -0
  38. package/corpus/templates/content/changelog/2026-10-16-accepting-a-suggestion-on-a-page-that-changed-since-it-was-s.md +6 -0
  39. package/corpus/templates/content/changelog/2026-10-20-suggested-edits-that-add-remove-split-or-merge-paragraphs-li.md +6 -0
  40. package/corpus/templates/content/changelog/2026-10-22-blocks-with-several-comment-threads-fold-into-one-stack-in-t.md +6 -0
  41. package/corpus/templates/content/changelog/2026-10-22-opening-a-page-no-longer-saves-an-edit-again-after-another-o.md +6 -0
  42. package/corpus/templates/content/docs/product/capabilities/content.comment.page-owned.md +8 -1
  43. package/corpus/templates/content/shared/suggestion-diff.ts +23 -6
  44. package/corpus/templates/content/shared/suggestion-formatting.ts +44 -14
  45. package/corpus/templates/content/shared/suggestion-text.ts +20 -5
  46. package/corpus/templates/design/.generated/bridge/interact-links.generated.ts +8 -8
  47. package/corpus/templates/design/CHANGELOG.md +8 -0
  48. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +18 -1
  49. package/corpus/templates/design/app/components/design/bridge/interact-links.bridge.ts +8 -8
  50. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +15 -13
  51. package/corpus/templates/design/app/i18n/en-US.ts +1 -1
  52. package/corpus/templates/design/app/pages/DesignSystems.tsx +1 -1
  53. package/corpus/templates/design/app/pages/Index.tsx +18 -64
  54. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-modes.ts +15 -6
  55. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-screen-rendering.tsx +29 -5
  56. package/corpus/templates/design/app/pages/design-editor/prototype-navigation-transition.ts +423 -0
  57. package/corpus/templates/design/app/pages/design-editor/view/single-screen-canvas.tsx +35 -9
  58. package/corpus/templates/design/changelog/2026-10-16-design-pages-use-localized-visibility-badges-and-a-shared-ow.md +6 -0
  59. package/corpus/templates/design/changelog/2026-10-20-the-design-full-app-toggle-on-the-home-page-no-longer-hides-.md +6 -0
  60. package/corpus/templates/design/changelog/2026-10-22-prototype-links-can-play-code-authored-screen-transitions.md +6 -0
  61. package/corpus/templates/design/shared/motion-authored-css-import.ts +938 -7
  62. package/corpus/templates/design/shared/motion-html-source.ts +1689 -135
  63. package/corpus/templates/design/shared/motion-prototype-transition.ts +329 -0
  64. package/corpus/templates/design/shared/motion-react-source.ts +2154 -126
  65. package/corpus/templates/design/shared/motion-text-runtime-v1.fixture.txt +221 -0
  66. package/corpus/templates/dispatch/CHANGELOG.md +4 -0
  67. package/corpus/templates/dispatch/app/i18n-data.ts +75 -0
  68. package/corpus/templates/dispatch/changelog/2026-10-21-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
  69. package/corpus/templates/factory/app/components/layout/Sidebar.tsx +34 -100
  70. package/corpus/templates/forms/app/pages/FormsListPage.tsx +1 -1
  71. package/corpus/templates/plan/CHANGELOG.md +6 -0
  72. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +34 -109
  73. package/corpus/templates/plan/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
  74. package/corpus/templates/slides/CHANGELOG.md +1 -0
  75. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +1 -1
  76. package/corpus/templates/slides/app/components/design-system/DesignSystemCard.tsx +1 -1
  77. package/corpus/templates/slides/app/pages/Index.tsx +7 -2
  78. package/corpus/templates/slides/changelog/2026-10-16-deck-lists-use-the-shared-ownership-filter-and-visibility-ba.md +6 -0
  79. package/package.json +1 -1
  80. package/corpus/templates/chat/app/lib/sidebar-thread-state.ts +0 -29
  81. package/corpus/templates/slides/app/components/deck/DeckFilterMenu.tsx +0 -83
@@ -1,9 +1,15 @@
1
1
  import { appPath } from "@agent-native/core/client/api-path";
2
- import {
3
- useActionQuery,
4
- useActionMutation,
5
- } from "@agent-native/core/client/hooks";
2
+ import { useActionMutation } from "@agent-native/core/client/hooks";
6
3
  import { useT } from "@agent-native/core/client/i18n";
4
+ import {
5
+ useShareButtonController,
6
+ type ShareButtonController,
7
+ } from "@agent-native/core/client/sharing/useShareButtonController";
8
+ import {
9
+ ShareGeneralAccessRow,
10
+ SharePeopleInvite,
11
+ SharePeopleList,
12
+ } from "@agent-native/toolkit/app/sharing";
7
13
  import {
8
14
  useCallback,
9
15
  useEffect,
@@ -16,15 +22,8 @@ import { toast } from "sonner";
16
22
 
17
23
  import {
18
24
  CopyButton,
19
- GeneralAccessSelect,
20
- InvitePeopleField,
21
- PeopleAccessSection,
22
25
  ShareSectionLabel,
23
26
  nestedLayerDismissGuards,
24
- useResourceVisibilityMutation,
25
- type SharesQuery,
26
- type SharesResponse,
27
- type Visibility,
28
27
  } from "@/components/sharing/share-ui";
29
28
  import { Button } from "@/components/ui/button";
30
29
  import {
@@ -53,6 +52,7 @@ export function ShareMeetingPopover({
53
52
  <PopoverTrigger asChild>{children}</PopoverTrigger>
54
53
  <PopoverContent
55
54
  align="end"
55
+ onOpenAutoFocus={(event) => event.preventDefault()}
56
56
  {...nestedLayerDismissGuards()}
57
57
  className="max-h-[calc(100vh-1rem)] w-[440px] max-w-[calc(100vw-1rem)] overflow-y-auto border-border p-0"
58
58
  >
@@ -80,32 +80,18 @@ function ShareMeetingContent({
80
80
  [meetingId],
81
81
  );
82
82
 
83
- const sharesQuery = useActionQuery<SharesResponse>("list-resource-shares", {
83
+ const controller = useShareButtonController({
84
84
  resourceType: "meeting",
85
85
  resourceId: meetingId,
86
+ defaultOpen: true,
86
87
  });
87
88
 
88
- const data = sharesQuery.data;
89
- const canManage = data?.role === "owner" || data?.role === "admin";
90
- const visibility: Visibility =
91
- (data?.visibility as Visibility | null) ?? "private";
92
- const { setResourceVisibility, isPending } = useResourceVisibilityMutation(
93
- "meeting",
94
- meetingId,
95
- sharesQuery,
96
- );
97
- const visibilityPending = isPending || sharesQuery.isLoading;
98
-
99
89
  return (
100
90
  <div className="min-w-0 px-4 py-3">
101
91
  <LinkTab
102
92
  meetingId={meetingId}
103
93
  shareUrl={shareUrl}
104
- sharesQuery={sharesQuery}
105
- canManage={canManage}
106
- visibility={visibility}
107
- visibilityPending={visibilityPending}
108
- onVisibilityChange={setResourceVisibility}
94
+ controller={controller}
109
95
  shareTranscript={shareTranscript}
110
96
  transcriptReady={transcriptReady}
111
97
  />
@@ -116,24 +102,13 @@ function ShareMeetingContent({
116
102
  function LinkTab({
117
103
  meetingId,
118
104
  shareUrl,
119
- sharesQuery,
120
- canManage,
121
- visibility,
122
- visibilityPending,
123
- onVisibilityChange,
105
+ controller,
124
106
  shareTranscript,
125
107
  transcriptReady,
126
108
  }: {
127
109
  meetingId: string;
128
110
  shareUrl: string;
129
- sharesQuery: SharesQuery;
130
- canManage: boolean;
131
- visibility: Visibility;
132
- visibilityPending: boolean;
133
- onVisibilityChange: (
134
- next: Visibility,
135
- options?: { onSuccess?: () => void },
136
- ) => void;
111
+ controller: ShareButtonController;
137
112
  shareTranscript: boolean;
138
113
  transcriptReady: boolean;
139
114
  }) {
@@ -143,7 +118,7 @@ function LinkTab({
143
118
  { id: string; shareTranscript: boolean }
144
119
  >("update-meeting");
145
120
  const [includeTranscript, setIncludeTranscript] = useState(shareTranscript);
146
- const data = sharesQuery.data;
121
+ const { sharesQuery, data, canManage, visibility } = controller;
147
122
  const isPublic = visibility === "public";
148
123
  const sharesLoaded = data?.visibility != null;
149
124
  const createAgentLink = useActionMutation("create-agent-resource-link");
@@ -216,45 +191,13 @@ function LinkTab({
216
191
 
217
192
  return (
218
193
  <div className="space-y-3">
219
- {canManage ? (
220
- <InvitePeopleField
221
- resourceType="meeting"
222
- resourceId={meetingId}
223
- sharesQuery={sharesQuery}
224
- onError={(err) =>
225
- toast.error(
226
- err instanceof Error
227
- ? err.message
228
- : t("clipsFinalRaw.inviteFailed"),
229
- )
230
- }
231
- />
232
- ) : null}
194
+ <SharePeopleInvite controller={controller} />
233
195
 
234
196
  <div className="space-y-2">
235
197
  <ShareSectionLabel>{t("shareUi.whoHasAccess")}</ShareSectionLabel>
236
198
  <div className="flex flex-col gap-1">
237
- <PeopleAccessSection
238
- resourceType="meeting"
239
- resourceId={meetingId}
240
- sharesQuery={sharesQuery}
241
- canManage={canManage}
242
- onError={(err, action) =>
243
- toast.error(
244
- err instanceof Error
245
- ? err.message
246
- : action === "permission"
247
- ? t("clipsFinalRaw.permissionUpdateFailed")
248
- : t("clipsFinalRaw.removePersonFailed"),
249
- )
250
- }
251
- />
252
- <GeneralAccessSelect
253
- visibility={visibility}
254
- canManage={canManage}
255
- isPending={visibilityPending}
256
- onChange={onVisibilityChange}
257
- />
199
+ <SharePeopleList controller={controller} />
200
+ <ShareGeneralAccessRow controller={controller} sentenceTrigger />
258
201
  </div>
259
202
  </div>
260
203
 
@@ -1,11 +1,16 @@
1
1
  import { trackEvent } from "@agent-native/core/client/analytics";
2
2
  import { appPath } from "@agent-native/core/client/api-path";
3
- import {
4
- useActionMutation,
5
- useActionQuery,
6
- useSession,
7
- } from "@agent-native/core/client/hooks";
3
+ import { useActionMutation, useSession } from "@agent-native/core/client/hooks";
8
4
  import { useT } from "@agent-native/core/client/i18n";
5
+ import {
6
+ useShareButtonController,
7
+ type ShareButtonController,
8
+ } from "@agent-native/core/client/sharing/useShareButtonController";
9
+ import {
10
+ ShareGeneralAccessRow,
11
+ SharePeopleInvite,
12
+ SharePeopleList,
13
+ } from "@agent-native/toolkit/app/sharing";
9
14
  import { writeClipboardText } from "@agent-native/toolkit/clipboard";
10
15
  import {
11
16
  AgentDestinationActions,
@@ -41,14 +46,8 @@ import {
41
46
  } from "@/components/agent-destination-logos";
42
47
  import {
43
48
  CopyButton,
44
- GeneralAccessSelect,
45
- InvitePeopleField,
46
- PeopleAccessSection,
47
49
  ShareSectionLabel,
48
50
  nestedLayerDismissGuards,
49
- useResourceVisibilityMutation,
50
- type SharesQuery,
51
- type SharesResponse,
52
51
  type Visibility,
53
52
  } from "@/components/sharing/share-ui";
54
53
  import { Button } from "@/components/ui/button";
@@ -326,19 +325,22 @@ function ShareRecordingContent({
326
325
  setShareMode("people");
327
326
  }, [recordingId]);
328
327
 
329
- const sharesQuery = useActionQuery<SharesResponse>(
330
- "list-resource-shares",
331
- { resourceType: "recording", resourceId: recordingId },
332
- { enabled: !viewerReshareOnly },
333
- );
334
-
335
- const data = viewerReshareOnly ? undefined : sharesQuery.data;
328
+ const controller = useShareButtonController({
329
+ resourceType: "recording",
330
+ resourceId: recordingId,
331
+ defaultOpen: !viewerReshareOnly,
332
+ shareUrl: absoluteAppUrl(`/r/${recordingId}`),
333
+ });
334
+ const sharesQuery = controller.sharesQuery;
335
+ const data = viewerReshareOnly ? undefined : controller.data;
336
336
  const role = data?.role ?? initialRole;
337
- const canManage = role === "owner" || role === "admin";
337
+ const canManage = !viewerReshareOnly && controller.canManage;
338
338
  const canViewShares =
339
339
  role === "owner" || role === "admin" || role === "editor";
340
340
  const visibility =
341
- (data?.visibility as Visibility | null | undefined) ??
341
+ (viewerReshareOnly
342
+ ? null
343
+ : (controller.visibilityOverride ?? data?.visibility)) ??
342
344
  initialVisibility ??
343
345
  null;
344
346
  const canEmbed = canViewShares || visibility === "public";
@@ -355,9 +357,8 @@ function ShareRecordingContent({
355
357
  ownerViaId,
356
358
  );
357
359
 
358
- const { setResourceVisibility, isPending: visibilityMutationPending } =
359
- useResourceVisibilityMutation("recording", recordingId, sharesQuery);
360
- const visibilityPending = visibilityMutationPending || sharesQuery.isLoading;
360
+ const visibilityPending =
361
+ controller.visibilityTransition.submitting || sharesQuery.isLoading;
361
362
  const sharesLoaded = visibility !== null;
362
363
  const viewTitle =
363
364
  view === "social"
@@ -368,11 +369,8 @@ function ShareRecordingContent({
368
369
  const peopleTab = (
369
370
  <PeopleTab
370
371
  recordingId={recordingId}
371
- sharesQuery={sharesQuery}
372
+ controller={controller}
372
373
  visibility={visibility}
373
- visibilityPending={visibilityPending}
374
- onVisibilityChange={setResourceVisibility}
375
- canManage={canManage}
376
374
  hasPassword={passwordProtected}
377
375
  expiresAt={currentExpiry}
378
376
  onPasswordChange={setPasswordProtected}
@@ -462,7 +460,7 @@ function ShareRecordingContent({
462
460
  ) : (
463
461
  <ClipsEmbedConfigurator
464
462
  recordingId={recordingId}
465
- sharesQuery={sharesQuery}
463
+ controller={controller}
466
464
  visibility={visibility}
467
465
  canManage={canManage}
468
466
  ownerViaId={ownerViaId}
@@ -498,11 +496,8 @@ function ShareOptionRow({
498
496
 
499
497
  function PeopleTab({
500
498
  recordingId,
501
- sharesQuery,
499
+ controller,
502
500
  visibility,
503
- visibilityPending,
504
- onVisibilityChange,
505
- canManage,
506
501
  hasPassword,
507
502
  expiresAt,
508
503
  onPasswordChange,
@@ -514,14 +509,8 @@ function PeopleTab({
514
509
  onOpenEmbed,
515
510
  }: {
516
511
  recordingId: string;
517
- sharesQuery: SharesQuery;
512
+ controller: ShareButtonController;
518
513
  visibility: Visibility | null;
519
- visibilityPending: boolean;
520
- onVisibilityChange: (
521
- next: Visibility,
522
- options?: { onSuccess?: () => void },
523
- ) => void;
524
- canManage: boolean;
525
514
  hasPassword?: boolean;
526
515
  expiresAt: string | null;
527
516
  onPasswordChange: (hasPassword: boolean) => void;
@@ -556,55 +545,37 @@ function PeopleTab({
556
545
  return (
557
546
  <div className="flex flex-col gap-3">
558
547
  <div className="flex flex-col gap-3">
559
- {/* `share-resource` requires owner/admin, so anyone else would only
560
- get a rejected submission. */}
561
- {canManage ? (
562
- <InvitePeopleField
563
- resourceType="recording"
564
- resourceId={recordingId}
565
- resourceUrl={absoluteAppUrl(`/r/${recordingId}`)}
566
- sharesQuery={sharesQuery}
567
- onError={(err) =>
568
- toast.error(
569
- err instanceof Error
570
- ? err.message
571
- : t("clipsFinalRaw.inviteFailed"),
572
- )
573
- }
574
- />
575
- ) : null}
548
+ <SharePeopleInvite
549
+ controller={controller}
550
+ roleCopy={{
551
+ commenter: {
552
+ label: t("shareUi.recordingCommenter.label"),
553
+ description: t("shareUi.recordingCommenter.description"),
554
+ },
555
+ }}
556
+ />
576
557
 
577
558
  {canViewShares ? (
578
559
  visibility ? (
579
560
  <div className="flex flex-col gap-1.5">
580
561
  <ShareSectionLabel>{t("shareUi.whoHasAccess")}</ShareSectionLabel>
581
562
  <div className="flex flex-col gap-0.5">
582
- <PeopleAccessSection
583
- resourceType="recording"
584
- resourceId={recordingId}
585
- sharesQuery={sharesQuery}
586
- canManage={canManage}
587
- roleCopy={{
588
- commenter: {
589
- label: t("shareUi.recordingCommenter.label"),
590
- description: t("shareUi.recordingCommenter.description"),
591
- },
592
- }}
593
- onError={(err, action) =>
594
- toast.error(
595
- err instanceof Error
596
- ? err.message
597
- : action === "permission"
598
- ? t("clipsFinalRaw.permissionUpdateFailed")
599
- : t("clipsFinalRaw.removePersonFailed"),
600
- )
601
- }
602
- />
603
- <GeneralAccessSelect
604
- visibility={visibility}
605
- canManage={canManage}
606
- isPending={visibilityPending}
607
- onChange={onVisibilityChange}
563
+ <div className="max-h-72 overflow-y-auto">
564
+ <SharePeopleList
565
+ controller={controller}
566
+ roleCopy={{
567
+ commenter: {
568
+ label: t("shareUi.recordingCommenter.label"),
569
+ description: t(
570
+ "shareUi.recordingCommenter.description",
571
+ ),
572
+ },
573
+ }}
574
+ />
575
+ </div>
576
+ <ShareGeneralAccessRow
577
+ controller={{ ...controller, visibility }}
578
+ sentenceTrigger
608
579
  />
609
580
  <RecordingAccessControls
610
581
  recordingId={recordingId}
@@ -1133,13 +1104,13 @@ function SocialTab({
1133
1104
 
1134
1105
  function ClipsEmbedConfigurator({
1135
1106
  recordingId,
1136
- sharesQuery,
1107
+ controller,
1137
1108
  visibility,
1138
1109
  canManage,
1139
1110
  ownerViaId,
1140
1111
  }: {
1141
1112
  recordingId: string;
1142
- sharesQuery: SharesQuery;
1113
+ controller: ShareButtonController;
1143
1114
  visibility: Visibility | null;
1144
1115
  canManage: boolean;
1145
1116
  ownerViaId?: string;
@@ -1156,12 +1127,8 @@ function ClipsEmbedConfigurator({
1156
1127
  const visibilityLabel = visibility
1157
1128
  ? t(`shareUi.visibility.${visibility}.label`)
1158
1129
  : "";
1159
- const { setResourceVisibility, isPending } = useResourceVisibilityMutation(
1160
- "recording",
1161
- recordingId,
1162
- sharesQuery,
1163
- );
1164
- const makePublic = () => setResourceVisibility("public");
1130
+ const isPending = controller.visibilityTransition.submitting;
1131
+ const makePublic = () => controller.handleVisibility("public");
1165
1132
 
1166
1133
  const src = useMemo(() => {
1167
1134
  const params: string[] = [];
@@ -1219,6 +1186,12 @@ function ClipsEmbedConfigurator({
1219
1186
  </div>
1220
1187
  ) : null}
1221
1188
 
1189
+ {controller.shareError ? (
1190
+ <div role="alert" className="text-xs text-destructive">
1191
+ {controller.shareError}
1192
+ </div>
1193
+ ) : null}
1194
+
1222
1195
  <CopyButton value={code} className="w-full" variant="default">
1223
1196
  {t("shareDialog.copyEmbedCode")}
1224
1197
  </CopyButton>
@@ -0,0 +1,180 @@
1
+ import type { ShareButtonSharesResponse } from "@agent-native/core/client/sharing/useShareButtonController";
2
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
3
+ import React, { act, type ReactNode } from "react";
4
+ import { createRoot } from "react-dom/client";
5
+ import { afterEach, beforeEach, expect, vi } from "vitest";
6
+
7
+ import { TooltipProvider } from "@/components/ui/tooltip";
8
+
9
+ const mocks = vi.hoisted(() => ({
10
+ data: undefined as ShareButtonSharesResponse | undefined,
11
+ query: vi.fn(),
12
+ refetch: vi.fn(async () => undefined),
13
+ visibility: vi.fn(),
14
+ share: vi.fn(),
15
+ unshare: vi.fn(),
16
+ update: vi.fn(),
17
+ createLink: vi.fn(async () => ({
18
+ contextUrl: "https://clips.example.test/scoped-agent",
19
+ })),
20
+ clipboard: vi.fn(async (_value: string) => true),
21
+ track: vi.fn(),
22
+ }));
23
+ function mutation(name: string) {
24
+ if (name === "set-resource-visibility")
25
+ return { mutateAsync: mocks.visibility };
26
+ if (name === "share-resource") return { mutate: mocks.share };
27
+ if (name === "unshare-resource") return { mutate: mocks.unshare };
28
+ if (name.includes("agent-link") || name === "create-agent-resource-link")
29
+ return { mutateAsync: mocks.createLink };
30
+ return { mutate: mocks.update };
31
+ }
32
+ vi.mock("@agent-native/core/client/use-action", () => ({
33
+ useActionQuery: (...args: unknown[]) => {
34
+ mocks.query(...args);
35
+ return { data: mocks.data, refetch: mocks.refetch, isLoading: false };
36
+ },
37
+ useActionMutation: (name: string) => mutation(name),
38
+ }));
39
+ vi.mock("@agent-native/core/client/hooks", () => ({
40
+ useActionMutation: (name: string) => mutation(name),
41
+ useSession: () => ({ session: { userId: "test-owner" } }),
42
+ }));
43
+ vi.mock("@agent-native/core/client/org/hooks", () => ({
44
+ useOrg: () => ({ data: null }),
45
+ }));
46
+ vi.mock("@agent-native/core/client/analytics", () => ({
47
+ trackEvent: mocks.track,
48
+ }));
49
+ vi.mock("@agent-native/core/client/i18n", () => ({
50
+ useT: () => (key: string, options?: { defaultValue?: string }) =>
51
+ options?.defaultValue ?? key,
52
+ }));
53
+ vi.mock("@agent-native/toolkit/clipboard", () => ({
54
+ writeClipboardText: mocks.clipboard,
55
+ }));
56
+ vi.mock("@/components/ui/dialog", () => ({
57
+ Dialog: ({ open, children }: { open: boolean; children: ReactNode }) =>
58
+ open ? children : null,
59
+ DialogContent: ({ children }: { children: ReactNode }) => (
60
+ <div>{children}</div>
61
+ ),
62
+ DialogTitle: ({ children }: { children: ReactNode }) => <h2>{children}</h2>,
63
+ }));
64
+ vi.mock("@/components/ui/popover", () => ({
65
+ Popover: ({ children }: { children: ReactNode }) => children,
66
+ PopoverContent: ({ children }: { children: ReactNode }) => (
67
+ <div>{children}</div>
68
+ ),
69
+ PopoverTrigger: ({ children }: { children: ReactNode }) => children,
70
+ PopoverAnchor: ({ children }: { children: ReactNode }) => children,
71
+ }));
72
+
73
+ export function shareTestHarness() {
74
+ let root: ReturnType<typeof createRoot>;
75
+ let container: HTMLDivElement;
76
+ let client: QueryClient;
77
+ beforeEach(() => {
78
+ vi.clearAllMocks();
79
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
80
+ vi.stubGlobal(
81
+ "fetch",
82
+ vi.fn(async () => ({ ok: true, json: async () => ({ members: [] }) })),
83
+ );
84
+ Element.prototype.scrollIntoView = vi.fn();
85
+ Element.prototype.hasPointerCapture = vi.fn(() => false);
86
+ Element.prototype.setPointerCapture = vi.fn();
87
+ Element.prototype.releasePointerCapture = vi.fn();
88
+ mocks.data = {
89
+ ownerEmail: "owner@example.test",
90
+ orgId: "org-test",
91
+ role: "owner",
92
+ visibility: "private",
93
+ shares: [
94
+ {
95
+ id: "share-test",
96
+ principalType: "user",
97
+ principalId: "viewer@example.test",
98
+ role: "viewer",
99
+ },
100
+ ],
101
+ };
102
+ mocks.visibility
103
+ .mockReset()
104
+ .mockImplementation(async ({ visibility }: { visibility: string }) => ({
105
+ visibility,
106
+ }));
107
+ mocks.clipboard.mockResolvedValue(true);
108
+ client = new QueryClient({
109
+ defaultOptions: {
110
+ queries: { retry: false },
111
+ mutations: { retry: false },
112
+ },
113
+ });
114
+ container = document.createElement("div");
115
+ document.body.appendChild(container);
116
+ root = createRoot(container);
117
+ });
118
+ afterEach(() => {
119
+ act(() => root.unmount());
120
+ client.clear();
121
+ container.remove();
122
+ vi.unstubAllGlobals();
123
+ });
124
+ return {
125
+ async render(children: ReactNode) {
126
+ await act(async () =>
127
+ root.render(
128
+ <QueryClientProvider client={client}>
129
+ <TooltipProvider>{children}</TooltipProvider>
130
+ </QueryClientProvider>,
131
+ ),
132
+ );
133
+ },
134
+ get client() {
135
+ return client;
136
+ },
137
+ };
138
+ }
139
+ export function button(label: string, scope: ParentNode = document.body) {
140
+ const result = Array.from(
141
+ scope.querySelectorAll<HTMLButtonElement>("button"),
142
+ ).find(
143
+ (el) =>
144
+ el.textContent?.trim() === label ||
145
+ el.getAttribute("aria-label") === label,
146
+ );
147
+ expect(result, `button ${label}`).toBeDefined();
148
+ return result!;
149
+ }
150
+ export async function click(element: HTMLElement) {
151
+ await act(async () => {
152
+ element.dispatchEvent(
153
+ new MouseEvent("mousedown", { bubbles: true, button: 0 }),
154
+ );
155
+ element.click();
156
+ });
157
+ }
158
+ export async function input(element: HTMLInputElement, value: string) {
159
+ await act(async () => {
160
+ Object.getOwnPropertyDescriptor(
161
+ HTMLInputElement.prototype,
162
+ "value",
163
+ )!.set!.call(element, value);
164
+ element.dispatchEvent(new Event("input", { bubbles: true }));
165
+ });
166
+ }
167
+ export async function select(trigger: HTMLElement, label: string) {
168
+ await act(async () =>
169
+ trigger.dispatchEvent(
170
+ new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }),
171
+ ),
172
+ );
173
+ const option = Array.from(
174
+ document.querySelectorAll<HTMLElement>('[role="option"]'),
175
+ ).find((el) => el.textContent?.includes(label));
176
+ expect(option, `option ${label}`).toBeDefined();
177
+ await click(option!);
178
+ }
179
+
180
+ export { mocks };