@fluid-app/portal-sdk 0.1.498 → 0.1.499

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 (64) hide show
  1. package/dist/{ActivityScreen-BHNUUl5N.mjs → ActivityScreen-8n5MUTqb.mjs} +72 -13
  2. package/dist/ActivityScreen-8n5MUTqb.mjs.map +1 -0
  3. package/dist/{ActivityScreen-CI8YhKBs.cjs → ActivityScreen-BtJZDBL8.cjs} +2 -2
  4. package/dist/{ActivityScreen-wpyJhHPK.cjs → ActivityScreen-DqNFQ0L-.cjs} +72 -13
  5. package/dist/ActivityScreen-DqNFQ0L-.cjs.map +1 -0
  6. package/dist/{FluidProvider-BG7D2WE_.mjs → FluidProvider-BY-IWBBl.mjs} +2 -2
  7. package/dist/{FluidProvider-BG7D2WE_.mjs.map → FluidProvider-BY-IWBBl.mjs.map} +1 -1
  8. package/dist/{FluidProvider-BMwY9-iq.cjs → FluidProvider-uiAG1e1c.cjs} +2 -2
  9. package/dist/{FluidProvider-BMwY9-iq.cjs.map → FluidProvider-uiAG1e1c.cjs.map} +1 -1
  10. package/dist/{MessagingScreen-DxeNig66.mjs → MessagingScreen-6yKtIQrt.mjs} +5 -5
  11. package/dist/{MessagingScreen-CPtQwSDv.mjs → MessagingScreen-BA0Bsjhu.mjs} +2 -2
  12. package/dist/{MessagingScreen-CPtQwSDv.mjs.map → MessagingScreen-BA0Bsjhu.mjs.map} +1 -1
  13. package/dist/{MessagingScreen-_OoquxUK.cjs → MessagingScreen-BzEMYknm.cjs} +5 -5
  14. package/dist/{MessagingScreen-F4wILtII.cjs → MessagingScreen-CHcWfgsC.cjs} +2 -2
  15. package/dist/{MessagingScreen-F4wILtII.cjs.map → MessagingScreen-CHcWfgsC.cjs.map} +1 -1
  16. package/dist/{OrdersScreen-DcT_LmrE.cjs → OrdersScreen-B4TQJMrX.cjs} +5 -5
  17. package/dist/{OrdersScreen-E2OXymPQ.mjs → OrdersScreen-ClQpYrU8.mjs} +5 -5
  18. package/dist/{OrdersScreen-C5SI6kEM.cjs → OrdersScreen-JCTiIxCr.cjs} +3 -3
  19. package/dist/{OrdersScreen-C5SI6kEM.cjs.map → OrdersScreen-JCTiIxCr.cjs.map} +1 -1
  20. package/dist/{OrdersScreen-Cm7aNQ6W.mjs → OrdersScreen-LaaKMS1l.mjs} +3 -3
  21. package/dist/{OrdersScreen-Cm7aNQ6W.mjs.map → OrdersScreen-LaaKMS1l.mjs.map} +1 -1
  22. package/dist/{ProfileScreen-CgrFxz1m.mjs → ProfileScreen-BQADmCT1.mjs} +6 -6
  23. package/dist/{ProfileScreen-CqhTJvXs.cjs → ProfileScreen-Bb2UCqei.cjs} +3 -3
  24. package/dist/{ProfileScreen-CqhTJvXs.cjs.map → ProfileScreen-Bb2UCqei.cjs.map} +1 -1
  25. package/dist/{ProfileScreen-CQY8BnZm.mjs → ProfileScreen-CPKQamjq.mjs} +3 -3
  26. package/dist/{ProfileScreen-CQY8BnZm.mjs.map → ProfileScreen-CPKQamjq.mjs.map} +1 -1
  27. package/dist/{ProfileScreen-B7xaDVqM.cjs → ProfileScreen-PtQeT_Tx.cjs} +6 -6
  28. package/dist/{RecentActivityWidget-DemnPXZR.cjs → RecentActivityWidget-BQXZR4_G.cjs} +3 -7
  29. package/dist/RecentActivityWidget-BQXZR4_G.cjs.map +1 -0
  30. package/dist/{RecentActivityWidget-CJcOQPUf.cjs → RecentActivityWidget-CSZeB4Pp.cjs} +2 -2
  31. package/dist/{RecentActivityWidget-BIgJS_kr.mjs → RecentActivityWidget-DLtQZmo3.mjs} +3 -7
  32. package/dist/RecentActivityWidget-DLtQZmo3.mjs.map +1 -0
  33. package/dist/{SubscriptionsScreen-CbvJj7kc.mjs → SubscriptionsScreen-Bdb0863a.mjs} +6 -6
  34. package/dist/{SubscriptionsScreen-DDhm_w13.cjs → SubscriptionsScreen-BuKop-5w.cjs} +6 -6
  35. package/dist/{SubscriptionsScreen-ChWqn480.mjs → SubscriptionsScreen-YrSM9ea-.mjs} +4 -4
  36. package/dist/{SubscriptionsScreen-ChWqn480.mjs.map → SubscriptionsScreen-YrSM9ea-.mjs.map} +1 -1
  37. package/dist/{SubscriptionsScreen-ljALMAX-.cjs → SubscriptionsScreen-bLbgcoWU.cjs} +4 -4
  38. package/dist/{SubscriptionsScreen-ljALMAX-.cjs.map → SubscriptionsScreen-bLbgcoWU.cjs.map} +1 -1
  39. package/dist/{activity-presentation-CUBIlrYQ.mjs → activity-presentation-DN7WG2W8.mjs} +20 -2
  40. package/dist/{activity-presentation-CUBIlrYQ.mjs.map → activity-presentation-DN7WG2W8.mjs.map} +1 -1
  41. package/dist/{activity-presentation-DbBDc0oA.cjs → activity-presentation-OSfAUOoY.cjs} +25 -1
  42. package/dist/{activity-presentation-DbBDc0oA.cjs.map → activity-presentation-OSfAUOoY.cjs.map} +1 -1
  43. package/dist/{build-widget-registry-DSu1FrDF.cjs → build-widget-registry-BQLi4JqV.cjs} +3 -3
  44. package/dist/{build-widget-registry-DSu1FrDF.cjs.map → build-widget-registry-BQLi4JqV.cjs.map} +1 -1
  45. package/dist/{build-widget-registry-Du6Ssx7x.mjs → build-widget-registry-C-WhTBEe.mjs} +3 -3
  46. package/dist/{build-widget-registry-Du6Ssx7x.mjs.map → build-widget-registry-C-WhTBEe.mjs.map} +1 -1
  47. package/dist/index.cjs +20 -20
  48. package/dist/index.d.cts.map +1 -1
  49. package/dist/index.d.mts.map +1 -1
  50. package/dist/index.mjs +20 -20
  51. package/dist/styles/remote-widget-shadow.css +2 -2
  52. package/dist/{use-portal-card-add-3ds-flow-66g15igj.mjs → use-portal-card-add-3ds-flow-Cu9yqnIn.mjs} +3 -3
  53. package/dist/{use-portal-card-add-3ds-flow-66g15igj.mjs.map → use-portal-card-add-3ds-flow-Cu9yqnIn.mjs.map} +1 -1
  54. package/dist/{use-portal-card-add-3ds-flow-LyyuhE0C.cjs → use-portal-card-add-3ds-flow-fFNO6XU3.cjs} +3 -3
  55. package/dist/{use-portal-card-add-3ds-flow-LyyuhE0C.cjs.map → use-portal-card-add-3ds-flow-fFNO6XU3.cjs.map} +1 -1
  56. package/dist/utils/build-remote-widget-plugin-manifests.cjs +3 -3
  57. package/dist/utils/build-remote-widget-plugin-manifests.mjs +3 -3
  58. package/dist/utils/build-widget-registry.cjs +3 -3
  59. package/dist/utils/build-widget-registry.mjs +3 -3
  60. package/package.json +18 -18
  61. package/dist/ActivityScreen-BHNUUl5N.mjs.map +0 -1
  62. package/dist/ActivityScreen-wpyJhHPK.cjs.map +0 -1
  63. package/dist/RecentActivityWidget-BIgJS_kr.mjs.map +0 -1
  64. package/dist/RecentActivityWidget-DemnPXZR.cjs.map +0 -1
@@ -1,7 +1,7 @@
1
1
  import { G as DropdownMenuContent, H as Label, J as DropdownMenuRadioGroup, K as DropdownMenuItem, N as Input, Tn as Button, U as DropdownMenu, Vn as __exportAll, W as DropdownMenuCheckboxItem, X as DropdownMenuSeparator, Y as DropdownMenuRadioItem, _ as Skeleton, _n as BreadcrumbLink, bn as BreadcrumbSeparator, et as DropdownMenuTrigger, gn as BreadcrumbItem, hn as Breadcrumb, l as TabsList, s as Tabs, u as TabsTrigger, vn as BreadcrumbList, yn as BreadcrumbPage } from "./src-BfINy9nC.mjs";
2
2
  import { r as useShellTranslation } from "./shell-translation-api-context-CCQ7vCA6.mjs";
3
3
  import { n as useWidgetsApi } from "./widgets-api-context-6escG2w8.mjs";
4
- import { c as getActivityIcon, i as formatActivityDateHeader, l as slugsForCategories, n as EmptyActivityIcon, o as formatActivityTime, r as describeActor, s as getActivityDateKey, t as ACTIVITY_CATEGORY_KEYS } from "./activity-presentation-CUBIlrYQ.mjs";
4
+ import { c as getActivityIcon, i as formatActivityDateHeader, l as initialsForActor, n as EmptyActivityIcon, o as formatActivityTime, r as describeActor, s as getActivityDateKey, t as ACTIVITY_CATEGORY_KEYS, u as slugsForCategories } from "./activity-presentation-DN7WG2W8.mjs";
5
5
  import { n as useAppNavigation, r as useOptionalAppNavigation } from "./AppNavigationContext-PCbmG3nH.mjs";
6
6
  import { n as useOptionalNavigationParent } from "./use-navigation-parent-CNtK6-bI.mjs";
7
7
  import { n as ScreenHeaderBreadcrumbs } from "./ScreenHeaderContext-DahYFA_m.mjs";
@@ -428,14 +428,25 @@ function isReachable(slug, items) {
428
428
  return navSlugs(items).some((navSlug) => findSystemSlugByPrefix(navSlug) === base);
429
429
  }
430
430
  /**
431
+ * Whether a link to `to` would actually go somewhere, for callers that style the
432
+ * affordance themselves. Shared with PortalDetailLink rather than reimplemented,
433
+ * since contacts and messaging are opt-in: the same slug links in one portal and
434
+ * not in another, and a name painted as a link that does nothing is worse than a
435
+ * plain one.
436
+ */
437
+ function usePortalDetailReachable(to) {
438
+ const navigation = useOptionalAppNavigation();
439
+ if (!to || !navigation) return false;
440
+ return isReachable(to, navigation.navItems);
441
+ }
442
+ /**
431
443
  * Wraps its children in a portal link, or leaves them alone when the link would
432
444
  * not resolve — no shell to navigate within, which is the case in tests and the
433
445
  * builder's preview, or a screen this portal does not offer.
434
446
  */
435
447
  function PortalDetailLink({ to, className, children }) {
436
- const navigation = useOptionalAppNavigation();
437
- if (!to || !navigation) return /* @__PURE__ */ jsx(Fragment$1, { children });
438
- if (!isReachable(to, navigation.navItems)) return /* @__PURE__ */ jsx(Fragment$1, { children });
448
+ const reachable = usePortalDetailReachable(to);
449
+ if (!to || !reachable) return /* @__PURE__ */ jsx(Fragment$1, { children });
439
450
  return /* @__PURE__ */ jsx(AppLink, {
440
451
  to,
441
452
  className,
@@ -873,11 +884,61 @@ const SPINE_CLASS = {
873
884
  };
874
885
  const ROW_STAGGER_MS = 40;
875
886
  const MAX_STAGGERED_ROWS = 8;
887
+ /**
888
+ * Photo, then initials, then the type icon, with the type kept as a corner badge
889
+ * whenever a face takes the circle. The widget does the same, and without it a
890
+ * cart, an order and a page view all look alike once the rows have faces.
891
+ *
892
+ * Keyed on the failed URL rather than a flag: rows survive a refetch, so a photo
893
+ * that 404s once would otherwise never be retried after the contact replaces it.
894
+ * Decorative throughout, since the row already names the actor and the event.
895
+ */
896
+ function ActorBadge({ avatarUrl, initials, Icon }) {
897
+ const [failedUrl, setFailedUrl] = useState(null);
898
+ if (avatarUrl && failedUrl !== avatarUrl) return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("img", {
899
+ src: avatarUrl,
900
+ alt: "",
901
+ width: 32,
902
+ height: 32,
903
+ loading: "lazy",
904
+ onError: () => setFailedUrl(avatarUrl),
905
+ className: "ring-border size-8 rounded-full object-cover ring-1"
906
+ }), /* @__PURE__ */ jsx(TypeBadge, { Icon })] });
907
+ if (initials) return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
908
+ "aria-hidden": "true",
909
+ className: "ring-border bg-muted text-foreground flex size-8 items-center justify-center rounded-full text-[11px] font-semibold tracking-wide ring-1",
910
+ children: initials
911
+ }), /* @__PURE__ */ jsx(TypeBadge, { Icon })] });
912
+ return /* @__PURE__ */ jsx("span", {
913
+ "aria-hidden": "true",
914
+ className: "ring-border bg-muted text-foreground flex size-8 items-center justify-center rounded-full ring-1",
915
+ children: /* @__PURE__ */ jsx(Icon, { className: "size-3.5" })
916
+ });
917
+ }
918
+ /** Sits opposite the unread dot, so the two never cover each other. */
919
+ function TypeBadge({ Icon }) {
920
+ return /* @__PURE__ */ jsx("span", {
921
+ "aria-hidden": "true",
922
+ className: "bg-muted text-foreground ring-background absolute -right-0.5 -bottom-0.5 flex size-3.5 items-center justify-center rounded-full ring-2",
923
+ children: /* @__PURE__ */ jsx(Icon, { className: "size-2" })
924
+ });
925
+ }
926
+ /**
927
+ * A name only wears the link colour when it really opens: an underline on hover is
928
+ * no affordance on a phone, and colouring a name that goes nowhere is worse than
929
+ * leaving it plain. A visitor stays muted either way, having no page to open.
930
+ */
931
+ function actorNameClass(muted, opensContact) {
932
+ if (muted) return "text-muted-foreground italic";
933
+ return opensContact ? "text-primary font-medium" : "text-foreground/80 font-medium";
934
+ }
876
935
  function ActivityRow({ activity, spine, index, locale, t, wasUnread }) {
877
936
  const Icon = getActivityIcon(activity.slug);
878
937
  const isUnread = wasUnread || activity.readAt === null;
879
938
  const actor = describeActor(activity.actor, t);
880
939
  const headline = activity.title ?? activity.activityType;
940
+ const contactPath = activity.actor.type === "contact" && activity.actor.contactId ? `${CONTACTS_SLUG}/${activity.actor.contactId}` : null;
941
+ const opensContact = usePortalDetailReachable(contactPath);
881
942
  return /* @__PURE__ */ jsxs("li", {
882
943
  className: "animate-in fade-in slide-in-from-bottom-2 relative flex gap-3 pb-6 duration-300 motion-reduce:animate-none",
883
944
  style: { animationDelay: `${Math.min(index, MAX_STAGGERED_ROWS) * ROW_STAGGER_MS}ms` },
@@ -888,12 +949,10 @@ function ActivityRow({ activity, spine, index, locale, t, wasUnread }) {
888
949
  className: `border-primary/40 absolute left-1/2 -translate-x-1/2 border-l-2 border-dashed ${SPINE_CLASS[spine]}`
889
950
  }), /* @__PURE__ */ jsxs("span", {
890
951
  className: "bg-background relative z-10 flex size-8 items-center justify-center rounded-full",
891
- children: [/* @__PURE__ */ jsx("span", {
892
- className: "ring-border bg-muted text-foreground flex size-8 items-center justify-center rounded-full ring-1",
893
- children: /* @__PURE__ */ jsx(Icon, {
894
- className: "size-3.5",
895
- "aria-hidden": "true"
896
- })
952
+ children: [/* @__PURE__ */ jsx(ActorBadge, {
953
+ avatarUrl: activity.avatarUrl,
954
+ initials: initialsForActor(activity.actor),
955
+ Icon
897
956
  }), isUnread && /* @__PURE__ */ jsx("span", {
898
957
  className: "bg-primary border-background absolute -top-0.5 -right-0.5 size-2.5 rounded-full border-2",
899
958
  "aria-label": t("activity_unread")
@@ -916,10 +975,10 @@ function ActivityRow({ activity, spine, index, locale, t, wasUnread }) {
916
975
  /* @__PURE__ */ jsxs("p", {
917
976
  className: "text-muted-foreground mt-0.5 truncate text-xs",
918
977
  children: [/* @__PURE__ */ jsx(PortalDetailLink, {
919
- to: activity.actor.type === "contact" && activity.actor.contactId ? `${CONTACTS_SLUG}/${activity.actor.contactId}` : null,
978
+ to: contactPath,
920
979
  className: "hover:underline",
921
980
  children: /* @__PURE__ */ jsx("span", {
922
- className: actor.muted ? "text-muted-foreground italic" : "text-foreground/80 font-medium",
981
+ className: actorNameClass(actor.muted, opensContact),
923
982
  children: actor.label
924
983
  })
925
984
  }), activity.description && activity.description !== headline && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
@@ -1208,4 +1267,4 @@ const activityScreenPropertySchema = {
1208
1267
  //#endregion
1209
1268
  export { OPT_IN_SYSTEM_SCREEN_SLUGS as a, AppLink as i, ActivityScreen_exports as n, findSystemSlugByPrefix as o, activityScreenPropertySchema as r, ActivityScreen as t };
1210
1269
 
1211
- //# sourceMappingURL=ActivityScreen-BHNUUl5N.mjs.map
1270
+ //# sourceMappingURL=ActivityScreen-8n5MUTqb.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ActivityScreen-8n5MUTqb.mjs","names":[],"sources":["../src/shell/system-screen-slugs.ts","../src/screens/activity/use-infinite-activities.ts","../src/screens/activity/use-mark-activities-read-on-open.ts","../src/screens/activity/activity-feed-helpers.ts","../src/shell/AppLink.tsx","../src/screens/activity/activity-detail-links.tsx","../src/screens/activity/ActivityDetailCard.tsx","../src/screens/activity/PortalActivityFeed.tsx","../src/screens/ActivityScreen.tsx"],"sourcesContent":["/**\n * The system screen slugs, without the screens. system-screen-map.ts pairs\n * these with lazy-loaded components, so importing it from anything a screen\n * renders is a cycle — a component that links across screens (activity cards,\n * for one) reads this leaf instead. The map's keys are typed off this list, so\n * the two cannot drift.\n */\nexport const SYSTEM_SCREEN_SLUGS = [\n \"profile\",\n \"account\",\n \"orders\",\n \"subscriptions\",\n \"messages\",\n \"contacts\",\n \"shop\",\n \"customers\",\n \"my-site\",\n \"share/products\",\n \"share/product\",\n \"share/media\",\n \"share/playlists\",\n \"share/playlist\",\n \"share/files\",\n \"share/pages\",\n \"share/page\",\n \"app-download\",\n \"activity\",\n] as const;\n\nexport type SystemScreenSlug = (typeof SYSTEM_SCREEN_SLUGS)[number];\n\n/**\n * Slugs of the screens a portal must opt into by adding them to its active\n * navigation; every other system screen renders regardless. The map derives\n * OPT_IN_SYSTEM_SCREENS from this set, so the two gates stay one decision.\n */\nexport const OPT_IN_SYSTEM_SCREEN_SLUGS: ReadonlySet<SystemScreenSlug> =\n new Set([\"messages\", \"contacts\"] as const);\n\n/** Pre-sorted for greedy prefix matching (longest first). */\nconst SORTED_SCREEN_SLUGS = [...SYSTEM_SCREEN_SLUGS].sort(\n (a, b) => b.length - a.length,\n);\n\n/** Prefix-match a slug against the system slugs, longest match first. */\nexport function findSystemSlugByPrefix(\n slug: string,\n): SystemScreenSlug | undefined {\n for (const key of SORTED_SCREEN_SLUGS) {\n if (slug === key || slug.startsWith(key + \"/\")) {\n return key;\n }\n }\n return undefined;\n}\n","import {\n useInfiniteQuery,\n type InfiniteData,\n type UseInfiniteQueryResult,\n} from \"@tanstack/react-query\";\nimport { useWidgetsApi } from \"@fluid-app/portal-core/widgets-api-context\";\nimport type {\n ActivitiesPage,\n ActivityActorFilter,\n FetchActivitiesArgs,\n} from \"@fluid-app/portal-core/widgets-api\";\n\n/**\n * Matches the API's default page size. Kept explicit so the request the screen\n * makes is the one the server was tuned for.\n */\nexport const ACTIVITY_PAGE_SIZE = 25;\n\n/**\n * Prefix shared by every filter combination of this feed. Anything that changes\n * the feed refreshes this rather than one key, so pages cached under a filter the\n * member is not currently looking at do not come back stale.\n */\nexport const ACTIVITY_FEED_QUERY_ROOT = [\"portal-sdk\", \"activities\"] as const;\n\nexport type UseInfiniteActivitiesParams = {\n /** Rolling window in days; omit for all time. */\n daysAgo?: number;\n /** Activity slugs to filter to; omit or empty for every type. */\n slugs?: string[];\n /** Free-text term; omit or blank for no search. */\n query?: string;\n /** Earliest whole day, `YYYY-MM-DD`. */\n from?: string;\n /** Latest whole day, `YYYY-MM-DD`. */\n to?: string;\n /** IANA zone `from` and `to` are days in; omit to leave the server on UTC. */\n timeZone?: string;\n /** Restrict to contacts or to visitors; omit for both. */\n actorType?: ActivityActorFilter;\n};\n\n/**\n * Cursor-paginated activity feed for the signed-in member.\n *\n * The query key includes a sorted slug list so re-picking the same filters in a\n * different order reuses the cached pages instead of refetching.\n */\nexport function useInfiniteActivities(\n params: UseInfiniteActivitiesParams = {},\n): UseInfiniteQueryResult<InfiniteData<ActivitiesPage>, Error> {\n const widgetsApi = useWidgetsApi();\n const slugs = params.slugs?.length ? [...params.slugs].sort() : undefined;\n\n return useInfiniteQuery({\n queryKey: [\n ...ACTIVITY_FEED_QUERY_ROOT,\n \"infinite\",\n params.daysAgo ?? null,\n slugs?.join(\",\") ?? null,\n params.query?.trim() || null,\n params.from ?? null,\n params.to ?? null,\n // In the key because it changes which rows the same from/to select.\n params.timeZone ?? null,\n params.actorType ?? null,\n ] as const,\n queryFn: ({ pageParam, signal }) => {\n // Built key by key: exactOptionalPropertyTypes rejects an explicit\n // undefined for these optional fields.\n const args: FetchActivitiesArgs = { limit: ACTIVITY_PAGE_SIZE };\n if (params.daysAgo != null) args.daysAgo = params.daysAgo;\n if (slugs) args.slugs = slugs;\n // Trimmed, so trailing whitespace does not become a second cache entry for\n // the same search.\n const query = params.query?.trim();\n if (query) args.query = query;\n if (params.from != null) args.from = params.from;\n if (params.to != null) args.to = params.to;\n if (params.timeZone != null) args.timeZone = params.timeZone;\n if (params.actorType != null) args.actorType = params.actorType;\n if (pageParam != null) args.cursor = pageParam;\n return widgetsApi.fetchActivities(args, signal);\n },\n getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,\n initialPageParam: undefined as string | undefined,\n });\n}\n","import { useEffect, useRef } from \"react\";\nimport {\n useMutation,\n useQueryClient,\n type InfiniteData,\n} from \"@tanstack/react-query\";\nimport { useWidgetsApi } from \"@fluid-app/portal-core/widgets-api-context\";\nimport type { ActivitiesPage } from \"@fluid-app/portal-core/widgets-api\";\nimport { ACTIVITY_FEED_QUERY_ROOT } from \"./use-infinite-activities\";\n\n/**\n * Stamps the cached rows read instead of invalidating: the endpoint marked the\n * whole feed, and a refetch would cancel any fetchNextPage in flight. Every\n * filter combination is stamped so pages cached under another filter do not\n * come back unread. The timestamp is the client's; only its null-ness is read.\n */\nfunction markCachedRowsRead(\n queryClient: ReturnType<typeof useQueryClient>,\n): void {\n const readAt = new Date().toISOString();\n\n queryClient.setQueriesData<InfiniteData<ActivitiesPage>>(\n { queryKey: ACTIVITY_FEED_QUERY_ROOT },\n (cached) =>\n cached && {\n ...cached,\n pages: cached.pages.map((page) => ({\n ...page,\n // Zero the count too so reopening the screen sees nothing unread and\n // skips a repeat write.\n ...(page.unreadCount !== undefined && { unreadCount: 0 }),\n activities: page.activities.map((activity) =>\n activity.readAt === null ? { ...activity, readAt } : activity,\n ),\n })),\n },\n );\n}\n\n/**\n * Marks the whole feed read once the screen has shown it; skipped when the\n * feed reports nothing unread. Waits for `feedLoaded` because firing on mount\n * races the screen's first fetch and the rows that arrive would still show as\n * unread.\n */\nexport function useMarkActivitiesReadOnOpen(\n feedLoaded: boolean,\n unreadCount: number | undefined,\n): void {\n const widgetsApi = useWidgetsApi();\n const queryClient = useQueryClient();\n\n const { mutate } = useMutation({\n mutationFn: () => widgetsApi.markActivitiesRead(),\n onSuccess: (result) => {\n // Only an explicit already-read means the cache needs no update; an\n // adapter that cannot tell still stamps.\n if (result.outcome === \"already-read\") return;\n\n markCachedRowsRead(queryClient);\n },\n // console.error rather than a reporter: this package takes no Sentry\n // dependency; the host app's console integration forwards this.\n onError: (error) => {\n console.error(\"[ActivityFeed] failed to mark activities read:\", error);\n },\n });\n\n // The ref holds this to one write across StrictMode's dev remount. Only a\n // reported zero skips the write: undefined means the adapter could not say,\n // and treating it as zero would leave rows unread.\n const hasNothingToMark = unreadCount === 0;\n\n const hasMarked = useRef(false);\n useEffect(() => {\n if (!feedLoaded || hasNothingToMark || hasMarked.current) return;\n hasMarked.current = true;\n mutate();\n }, [feedLoaded, hasNothingToMark, mutate]);\n}\n","import {\n describeActor,\n formatActivityDateHeader,\n getActivityDateKey,\n type ActivityDayLabels,\n} from \"@fluid-app/portal-core/activity-presentation\";\nimport type { Activity } from \"@fluid-app/portal-core/widgets-api-types\";\nimport type { ShellDict } from \"@fluid-app/portal-core/shell-translation-dictionary\";\n\nexport type ActivityTimeframe =\n | \"all\"\n | \"today\"\n | \"yesterday\"\n | \"last7\"\n | \"custom\";\n\nexport const TIMEFRAMES = [\n { value: \"all\", labelKey: \"activity_timeframe_all\" },\n { value: \"today\", labelKey: \"activity_timeframe_today\" },\n { value: \"yesterday\", labelKey: \"activity_timeframe_yesterday\" },\n { value: \"last7\", labelKey: \"activity_timeframe_7\" },\n { value: \"custom\", labelKey: \"activity_timeframe_custom\" },\n] as const satisfies ReadonlyArray<{\n value: ActivityTimeframe;\n labelKey: keyof ShellDict;\n}>;\n\n/**\n * Timeframes that need no explicit date range. Only the portal-tenant API\n * takes from/to bounds; the legacy adapter behind fluid-admin's portal preview\n * serves only days_ago, so it offers just these two.\n */\nconst RANGE_FREE_TIMEFRAMES: ReadonlyArray<ActivityTimeframe> = [\n \"all\",\n \"last7\",\n];\n\n/**\n * The timeframe options to offer for a given adapter. Exported for direct\n * testing: Radix mounts menu contents only when opened, so tests cannot read\n * the options from the DOM.\n */\nexport function timeframesFor(\n supportsRange: boolean,\n): ReadonlyArray<(typeof TIMEFRAMES)[number]> {\n if (supportsRange) return TIMEFRAMES;\n\n return TIMEFRAMES.filter((frame) =>\n RANGE_FREE_TIMEFRAMES.includes(frame.value),\n );\n}\n\n/**\n * The server rejects terms shorter than this, so the feed holds the request\n * back instead of round-tripping to a 422.\n */\nexport const MIN_SEARCH_LENGTH = 2;\n\n/**\n * The server rejects queries longer than this. Applies to the whole string,\n * unlike the minimum, which is per term.\n */\nexport const MAX_SEARCH_LENGTH = 100;\n\nexport const SEARCH_DEBOUNCE_MS = 300;\n/** A resolved date window, as the `YYYY-MM-DD` strings the API takes. */\nexport type ActivityDateRange = { from?: string; to?: string };\n\n/**\n * What a timeframe resolves to on the wire: either a rolling window in days or\n * an explicit range. Never both, which the server rejects.\n */\nexport type ActivityWindow = ActivityDateRange & { daysAgo?: number };\n\n/**\n * Stays a rolling days_ago window: the legacy adapter serves days_ago but not\n * explicit date ranges, so folding this into from/to bounds would break it\n * there.\n */\nconst LAST_7_DAYS_AGO = 7;\n\n/**\n * The local calendar day `at` falls on, as `YYYY-MM-DD`. Built from local date\n * fields rather than sliced off an ISO string (that gives the UTC day), so it\n * matches `getActivityDateKey`, which the day headers group by.\n */\nexport function localDay(at: Date): string {\n const year = at.getFullYear();\n const month = String(at.getMonth() + 1).padStart(2, \"0\");\n const day = String(at.getDate()).padStart(2, \"0\");\n\n return `${year}-${month}-${day}`;\n}\n\n/**\n * The viewer's IANA zone, for the server to read `from` and `to` in.\n *\n * Undefined when the environment cannot name it, which leaves the server on UTC\n * rather than guessing. An offset would not do: the offset for a past date is not\n * necessarily today's across a daylight-saving change.\n */\nexport function viewerTimeZone(): string | undefined {\n return Intl.DateTimeFormat().resolvedOptions().timeZone || undefined;\n}\n\n/**\n * The next local midnight strictly after `atMs`, so a caller that reschedules\n * on the result always moves forward instead of firing in a loop.\n */\nexport function nextLocalMidnight(atMs: number): number {\n const at = new Date(atMs);\n\n return new Date(\n at.getFullYear(),\n at.getMonth(),\n at.getDate() + 1,\n 0,\n 0,\n 0,\n 0,\n ).getTime();\n}\n\n/**\n * Turn a chosen timeframe into the window to request.\n *\n * A single day needs explicit bounds, since `days_ago` has no upper bound and so\n * cannot say \"yesterday\". `custom` passes the caller's own bounds through, half-open\n * ones included.\n */\nexport function resolveTimeframe(\n timeframe: ActivityTimeframe,\n nowMs: number,\n custom: ActivityDateRange = {},\n): ActivityWindow {\n const dayMs = 24 * 60 * 60 * 1000;\n const today = localDay(new Date(nowMs));\n\n switch (timeframe) {\n case \"today\":\n return { from: today, to: today };\n case \"yesterday\": {\n const yesterday = localDay(new Date(nowMs - dayMs));\n return { from: yesterday, to: yesterday };\n }\n case \"last7\":\n return { daysAgo: LAST_7_DAYS_AGO };\n case \"custom\": {\n // The date inputs allow typing an inverted pair, which the server\n // rejects. Collapse it to a single day rather than dropping both bounds,\n // so the window never silently widens to all time while the menu still\n // says Custom range.\n if (custom.from && custom.to && custom.to < custom.from) {\n return { from: custom.from, to: custom.from };\n }\n\n return {\n ...(custom.from ? { from: custom.from } : {}),\n ...(custom.to ? { to: custom.to } : {}),\n };\n }\n case \"all\":\n return {};\n default:\n return assertNoUnhandledTimeframe(timeframe);\n }\n}\n\n/** A new ActivityTimeframe has to be handled above rather than mean \"no filter\". */\nfunction assertNoUnhandledTimeframe(timeframe: never): ActivityWindow {\n void timeframe;\n\n return {};\n}\n\n/**\n * Whether the query fits the server's limits. The minimum applies to each\n * whitespace-separated term, while the maximum applies to the whole string,\n * so one short word makes the whole query invalid.\n */\nexport function isSearchable(\n query: string,\n minTermLength: number,\n maxQueryLength: number,\n): boolean {\n if (query.length > maxQueryLength) return false;\n\n const terms = query.split(/\\s+/).filter(Boolean);\n\n return (\n terms.length > 0 && terms.every((term) => term.length >= minTermLength)\n );\n}\n\nexport type ActivityDayGroup = {\n /** Local date key, stable across renders — used as the React key. */\n key: string;\n label: string;\n activities: Activity[];\n};\n\n/**\n * Buckets a chronological (newest-first) feed into day groups, preserving the\n * server's order within and between groups.\n *\n * Grouping keys off the local date derived from each timestamp rather than any\n * server-formatted string, so a rep sees days as their own clock reports them.\n */\nexport function groupActivitiesByDay(\n activities: readonly Activity[],\n nowMs: number,\n labels: ActivityDayLabels,\n locale?: string,\n): ActivityDayGroup[] {\n const groups: ActivityDayGroup[] = [];\n const byKey = new Map<string, ActivityDayGroup>();\n\n for (const activity of activities) {\n const key = getActivityDateKey(activity.timestamp);\n const existing = byKey.get(key);\n if (existing) {\n existing.activities.push(activity);\n continue;\n }\n\n const group: ActivityDayGroup = {\n key,\n label: formatActivityDateHeader(\n activity.timestamp,\n nowMs,\n labels,\n locale,\n ),\n activities: [activity],\n };\n byKey.set(key, group);\n groups.push(group);\n }\n\n return groups;\n}\n\n/**\n * Where a row sits in the feed, which decides how much of the timeline spine it\n * draws: the first row starts the line below its avatar, the last row stops at\n * its avatar, and a lone row draws none.\n */\nexport type SpinePosition = \"only\" | \"first\" | \"middle\" | \"last\";\n\n/**\n * Callers pass a feed-wide index and total, not per-day ones: the spine is one\n * continuous line through the date headers, and counting per day broke it at\n * every group boundary.\n */\nexport function spinePositionAt(index: number, total: number): SpinePosition {\n if (total <= 1) return \"only\";\n if (index === 0) return \"first\";\n if (index === total - 1) return \"last\";\n return \"middle\";\n}\n\n/**\n * How many rows precede each day group, so a row can be placed against the whole\n * feed. Index i is the running total before group i.\n */\nexport function rowOffsetsByGroup(\n groups: ReadonlyArray<ActivityDayGroup>,\n): number[] {\n let running = 0;\n\n return groups.map((group) => {\n const offset = running;\n running += group.activities.length;\n return offset;\n });\n}\n\nexport { describeActor };\n\n/**\n * Moving one bound past the other carries the other with it.\n *\n * `min` and `max` on a date input only mark it invalid; they do not stop a typed\n * value. Collapsing to a single day keeps every edit a range the server accepts.\n */\nexport function withFrom(\n range: ActivityDateRange,\n from: string,\n): ActivityDateRange {\n const to = range.to && from && range.to < from ? from : range.to;\n\n return { ...(from && { from }), ...(to && { to }) };\n}\n\nexport function withTo(\n range: ActivityDateRange,\n to: string,\n): ActivityDateRange {\n const from = range.from && to && to < range.from ? to : range.from;\n\n return { ...(from && { from }), ...(to && { to }) };\n}\n","import React, {\n forwardRef,\n type AnchorHTMLAttributes,\n type MouseEvent,\n} from \"react\";\nimport { useAppNavigation } from \"./AppNavigationContext\";\n\nexport interface AppLinkProps extends Omit<\n AnchorHTMLAttributes<HTMLAnchorElement>,\n \"href\"\n> {\n /** Slug to navigate to (e.g. \"contacts/123\") */\n to: string;\n}\n\n/**\n * SPA-aware link that renders a real `<a href>` for accessibility\n * (right-click, ctrl+click, screen readers) but intercepts normal\n * clicks for client-side navigation.\n */\nexport const AppLink: React.ForwardRefExoticComponent<\n AppLinkProps & React.RefAttributes<HTMLAnchorElement>\n> = forwardRef<HTMLAnchorElement, AppLinkProps>(function AppLink(\n { to, onClick, children, ...rest },\n ref,\n) {\n const { navigate, buildHref } = useAppNavigation();\n\n const handleClick = (e: MouseEvent<HTMLAnchorElement>) => {\n // Allow custom onClick handlers to run first\n onClick?.(e);\n if (e.defaultPrevented) return;\n\n // Skip SPA navigation for modifier keys, non-primary button, or external target\n if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;\n if (e.button !== 0) return;\n if (rest.target && rest.target !== \"_self\") return;\n\n e.preventDefault();\n navigate(to);\n };\n\n return (\n <a ref={ref} href={buildHref(to)} onClick={handleClick} {...rest}>\n {children}\n </a>\n );\n});\n","import type { JSX, ReactNode } from \"react\";\nimport type { ActivityDetails } from \"@fluid-app/portal-core/widgets-api-types\";\nimport type { NavigationItem } from \"../../types/navigation\";\nimport { AppLink } from \"../../shell/AppLink\";\nimport { useOptionalAppNavigation } from \"../../shell/AppNavigationContext\";\nimport {\n OPT_IN_SYSTEM_SCREEN_SLUGS,\n findSystemSlugByPrefix,\n} from \"../../shell/system-screen-slugs\";\nimport { buildMessagingSlugFromRoute } from \"../messaging-route-state\";\n\n/**\n * The portal screen that owns each kind of record a payload can point at.\n *\n * Several details name their subject polymorphically, and the type decides where it\n * opens. A Share can also point at an EnrollmentPack, Promotion, UserCompany or\n * Simulator; none has a portal screen, so anything missing here does not link.\n */\nconst SCREEN_FOR_RECORD: Record<string, string> = {\n Product: \"shop\",\n Medium: \"share/media\",\n Library: \"share/playlists\",\n};\n\n/** Base slug of the messaging screen. See the `message` case for why not \"messaging\". */\nconst MESSAGING_SLUG = \"messages\";\n\n/** Base slug of the contacts screen, for linking a row's actor. */\nexport const CONTACTS_SLUG = \"contacts\";\n\nfunction slugForRecord(\n recordType: string | undefined,\n recordId: number | undefined,\n): string | null {\n if (!recordType || !recordId) return null;\n\n const screen = SCREEN_FOR_RECORD[recordType];\n\n return screen ? `${screen}/${recordId}` : null;\n}\n\n/**\n * Where a card can send a rep inside the portal, or null when nowhere can.\n *\n * Contacts are linked from the row's actor instead. Orders are the one subject with a\n * screen we cannot open: PortalTenant::OrderFinder scopes to the viewing member's own\n * customer, so a contact's order is not theirs to open.\n */\nexport function portalDetailSlug(details: ActivityDetails): string | null {\n switch (details.type) {\n // Both name their subject outright rather than polymorphically.\n case \"cart\":\n return slugForRecord(\"Product\", details.productId);\n case \"video\":\n return slugForRecord(\"Medium\", details.mediumId);\n\n // Built rather than written out, because messaging owns the shape and encodes\n // each segment. The base is \"messages\", the slug SYSTEM_SLUG_SCREEN_MAP routes to\n // MessagingScreen and the one real portals carry in their nav. Its page template\n // registers \"messaging\" instead, which routes nowhere.\n case \"message\":\n return details.conversationId && details.messageId\n ? buildMessagingSlugFromRoute(MESSAGING_SLUG, {\n view: \"message\",\n conversationId: details.conversationId,\n messageId: String(details.messageId),\n })\n : null;\n\n case \"pageView\":\n return slugForRecord(details.resourceType, details.resourceId);\n case \"review\":\n return slugForRecord(details.reviewableType, details.reviewableId);\n case \"reaction\":\n return slugForRecord(details.reactableType, details.reactableId);\n case \"comment\":\n return slugForRecord(details.relateableType, details.relateableId);\n\n default:\n return null;\n }\n}\n\nfunction navSlugs(items: readonly NavigationItem[]): string[] {\n return items.flatMap((item) => [\n ...(item.slug ? [item.slug] : []),\n ...navSlugs(item.children ?? []),\n ]);\n}\n\n/**\n * Whether this portal would render the slug, mirroring PageRouter so a link never\n * offers a Page Not Found.\n *\n * Messaging and contacts are opt-in there, rendering only when the tenant's nav\n * carries a slug for them. Every other system screen renders regardless.\n */\nfunction isReachable(slug: string, items: readonly NavigationItem[]): boolean {\n const base = findSystemSlugByPrefix(slug);\n if (!base) return false;\n if (!OPT_IN_SYSTEM_SCREEN_SLUGS.has(base)) return true;\n\n return navSlugs(items).some(\n (navSlug) => findSystemSlugByPrefix(navSlug) === base,\n );\n}\n\n/**\n * Whether a link to `to` would actually go somewhere, for callers that style the\n * affordance themselves. Shared with PortalDetailLink rather than reimplemented,\n * since contacts and messaging are opt-in: the same slug links in one portal and\n * not in another, and a name painted as a link that does nothing is worse than a\n * plain one.\n */\nexport function usePortalDetailReachable(to: string | null): boolean {\n const navigation = useOptionalAppNavigation();\n\n if (!to || !navigation) return false;\n\n return isReachable(to, navigation.navItems);\n}\n\n/**\n * Wraps its children in a portal link, or leaves them alone when the link would\n * not resolve — no shell to navigate within, which is the case in tests and the\n * builder's preview, or a screen this portal does not offer.\n */\nexport function PortalDetailLink({\n to,\n className,\n children,\n}: {\n to: string | null;\n className?: string;\n children: ReactNode;\n}): JSX.Element {\n const reachable = usePortalDetailReachable(to);\n\n if (!to || !reachable) return <>{children}</>;\n\n return (\n <AppLink to={to} className={className}>\n {children}\n </AppLink>\n );\n}\n","import type { JSX } from \"react\";\nimport { useState } from \"react\";\nimport { ImageOff, Star } from \"lucide-react\";\nimport type { ActivityDetails } from \"@fluid-app/portal-core/widgets-api-types\";\nimport type { ShellT } from \"./activity-translation\";\nimport { PortalDetailLink, portalDetailSlug } from \"./activity-detail-links\";\n\ntype ActivityDetailCardProps = {\n details: ActivityDetails;\n t: ShellT;\n};\n\n/**\n * The rich body under an activity row, one shape per detail type.\n *\n * Every field is optional — the server strips blanks before storing a detail —\n * so each branch renders only what arrived and returns null when nothing\n * meaningful is left. The row above it already carries who and when.\n */\nexport function ActivityDetailCard({\n details,\n t,\n}: ActivityDetailCardProps): JSX.Element | null {\n switch (details.type) {\n case \"order\": {\n const total = details.orderTotalInCurrency ?? details.orderTotal;\n if (details.orderItemName || details.imageUrl) {\n const extraItems = (details.orderItemCount ?? 0) - 1;\n const subtitle = [\n total,\n extraItems > 0\n ? t(\"activity_order_more_items\", { count: extraItems })\n : null,\n ]\n .filter(Boolean)\n .join(\" · \");\n return (\n <MediaCard\n title={details.orderItemName}\n imageUrl={details.imageUrl}\n subtitle={subtitle || undefined}\n />\n );\n }\n return total ? (\n <MetaCard label={t(\"activity_order_total\")} value={total} />\n ) : null;\n }\n\n case \"video\":\n return details.mediumTitle || details.imageUrl ? (\n <PortalDetailLink to={portalDetailSlug(details)} className=\"block\">\n <MediaCard title={details.mediumTitle} imageUrl={details.imageUrl} />\n </PortalDetailLink>\n ) : null;\n\n case \"cart\":\n return details.productName || details.imageUrl ? (\n <PortalDetailLink to={portalDetailSlug(details)} className=\"block\">\n <MediaCard title={details.productName} imageUrl={details.imageUrl} />\n </PortalDetailLink>\n ) : null;\n\n // Prefer the in-portal product page over the public URL when both resolve:\n // it keeps the rep in the portal on a page they can act on.\n case \"pageView\": {\n const to = portalDetailSlug(details);\n const label = details.resourceName ?? details.url;\n\n if (to && label) {\n return (\n <PortalDetailLink to={to} className={LINK_CLASS}>\n {label}\n </PortalDetailLink>\n );\n }\n\n return details.url ? (\n <PageLink label={label ?? details.url} url={details.url} />\n ) : null;\n }\n\n // No card: the row's own sentence already carries the lead's type, reason,\n // and interest.\n case \"contactLead\":\n return null;\n\n case \"comment\":\n return details.body ? (\n <PortalDetailLink to={portalDetailSlug(details)} className=\"block\">\n <QuoteCard body={details.body} caption={details.relateableName} />\n </PortalDetailLink>\n ) : null;\n\n case \"review\":\n return details.rating != null || details.body ? (\n <PortalDetailLink to={portalDetailSlug(details)} className=\"block\">\n <ReviewCard\n rating={details.rating}\n body={details.body}\n caption={details.reviewableName}\n t={t}\n />\n </PortalDetailLink>\n ) : null;\n\n case \"reaction\":\n return details.icon ? (\n <PortalDetailLink to={portalDetailSlug(details)} className=\"block\">\n <ReactionCard icon={details.icon} caption={details.reactableTitle} />\n </PortalDetailLink>\n ) : null;\n\n // A subject means the message was an email; it is the only visible\n // difference from an SMS.\n case \"message\":\n return details.body || details.subject ? (\n <PortalDetailLink to={portalDetailSlug(details)} className=\"block\">\n <QuoteCard\n heading={details.body ? details.subject : undefined}\n body={details.body ?? details.subject ?? \"\"}\n />\n </PortalDetailLink>\n ) : null;\n\n default:\n return null;\n }\n}\n\nconst CARD_CLASS =\n \"border-border bg-card mt-2 flex items-center gap-3 rounded-lg border p-2\";\n\nfunction MediaCard({\n title,\n imageUrl,\n subtitle,\n}: {\n title: string | undefined;\n imageUrl: string | undefined;\n subtitle?: string;\n}): JSX.Element {\n return (\n <div className={CARD_CLASS}>\n <Thumbnail src={imageUrl} alt={title ?? \"\"} />\n <div className=\"min-w-0 flex-1\">\n {title && (\n <p className=\"text-foreground truncate text-sm font-semibold\">\n {title}\n </p>\n )}\n {subtitle && (\n <p className=\"text-muted-foreground truncate text-xs\">{subtitle}</p>\n )}\n </div>\n </div>\n );\n}\n\nfunction Thumbnail({\n src,\n alt,\n}: {\n src: string | undefined;\n alt: string;\n}): JSX.Element {\n const [failed, setFailed] = useState(false);\n\n if (!src || failed) {\n return (\n <div\n aria-hidden=\"true\"\n className=\"bg-muted text-muted-foreground flex h-[60px] w-[115px] shrink-0 items-center justify-center rounded\"\n >\n <ImageOff className=\"size-4\" />\n </div>\n );\n }\n\n return (\n <img\n src={src}\n alt={alt}\n loading=\"lazy\"\n onError={() => setFailed(true)}\n className=\"bg-muted h-[60px] w-[115px] shrink-0 rounded object-cover object-top\"\n />\n );\n}\n\nfunction MetaCard({\n label,\n value,\n}: {\n label: string;\n value: string;\n}): JSX.Element {\n return (\n <div className={CARD_CLASS}>\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-muted-foreground text-xs font-medium\">{label}</p>\n <p className=\"text-foreground truncate text-sm font-semibold\">\n {value}\n </p>\n </div>\n </div>\n );\n}\n\nconst LINK_CLASS = \"text-primary mt-1 block truncate text-sm hover:underline\";\n\nfunction PageLink({ label, url }: { label: string; url: string }): JSX.Element {\n return (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noreferrer noopener\"\n className={LINK_CLASS}\n >\n {label}\n </a>\n );\n}\n\nfunction QuoteCard({\n heading,\n body,\n caption,\n}: {\n heading?: string | undefined;\n body: string;\n caption?: string | undefined;\n}): JSX.Element {\n return (\n <div className={`${CARD_CLASS} flex-col items-stretch gap-1`}>\n {heading && (\n <p className=\"text-foreground truncate text-xs font-semibold\">\n {heading}\n </p>\n )}\n {/* whitespace-pre-line: an SMS body arrives with real newlines; the\n server only flattens email bodies. */}\n <p className=\"text-foreground line-clamp-3 text-sm leading-relaxed whitespace-pre-line\">\n {body}\n </p>\n {caption && <p className=\"text-muted-foreground text-xs\">{caption}</p>}\n </div>\n );\n}\n\nfunction ReviewCard({\n rating,\n body,\n caption,\n t,\n}: {\n rating: number | undefined;\n body: string | undefined;\n caption: string | undefined;\n t: ShellT;\n}): JSX.Element {\n return (\n <div className={`${CARD_CLASS} flex-col items-stretch gap-1`}>\n {rating != null && (\n <div\n className=\"flex items-center gap-0.5\"\n aria-label={t(\"activity_rating\", { rating })}\n >\n {[1, 2, 3, 4, 5].map((star) => (\n <Star\n key={star}\n aria-hidden=\"true\"\n className={\n star <= rating\n ? \"fill-primary text-primary size-3.5\"\n : \"text-muted-foreground size-3.5\"\n }\n />\n ))}\n </div>\n )}\n {body && (\n <p className=\"text-foreground line-clamp-3 text-sm leading-relaxed\">\n {body}\n </p>\n )}\n {caption && <p className=\"text-muted-foreground text-xs\">{caption}</p>}\n </div>\n );\n}\n\nfunction ReactionCard({\n icon,\n caption,\n}: {\n icon: string;\n caption: string | undefined;\n}): JSX.Element {\n return (\n <div className={CARD_CLASS}>\n <span className=\"text-xl leading-none\" aria-hidden=\"true\">\n {icon}\n </span>\n {caption && (\n <p className=\"text-foreground min-w-0 flex-1 truncate text-sm font-semibold\">\n {caption}\n </p>\n )}\n </div>\n );\n}\n","import { type JSX, useEffect, useId, useMemo, useState, useRef } from \"react\";\nimport {\n AlertCircle,\n Calendar,\n Filter,\n ChevronDown,\n Search,\n X,\n type LucideIcon,\n} from \"lucide-react\";\nimport {\n Breadcrumb,\n BreadcrumbItem,\n BreadcrumbLink,\n BreadcrumbList,\n BreadcrumbPage,\n BreadcrumbSeparator,\n Button,\n DropdownMenu,\n DropdownMenuCheckboxItem,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuRadioGroup,\n DropdownMenuRadioItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n Tabs,\n TabsList,\n TabsTrigger,\n Input,\n Label,\n Skeleton,\n} from \"@fluid-app/ui-primitives\";\nimport { ScreenHeaderBreadcrumbs } from \"@fluid-app/portal-react/shell/ScreenHeaderContext\";\nimport { useOptionalNavigationParent } from \"../../shell/use-navigation-parent\";\nimport { useShellTranslation } from \"@fluid-app/portal-core/shell-translation-api-context\";\nimport { useWidgetsApi } from \"@fluid-app/portal-core/widgets-api-context\";\nimport {\n ACTIVITY_CATEGORY_KEYS,\n EmptyActivityIcon,\n formatActivityTime,\n getActivityIcon,\n initialsForActor,\n slugsForCategories,\n type ActivityCategory,\n} from \"@fluid-app/portal-core/activity-presentation\";\nimport type { Activity } from \"@fluid-app/portal-core/widgets-api-types\";\nimport type { ShellDict } from \"@fluid-app/portal-core/shell-translation-dictionary\";\nimport { useInfiniteActivities } from \"./use-infinite-activities\";\nimport { useMarkActivitiesReadOnOpen } from \"./use-mark-activities-read-on-open\";\nimport {\n MAX_SEARCH_LENGTH,\n MIN_SEARCH_LENGTH,\n SEARCH_DEBOUNCE_MS,\n TIMEFRAMES,\n describeActor,\n groupActivitiesByDay,\n isSearchable,\n nextLocalMidnight,\n localDay,\n resolveTimeframe,\n rowOffsetsByGroup,\n spinePositionAt,\n timeframesFor,\n viewerTimeZone,\n type ActivityDateRange,\n type ActivityTimeframe,\n type SpinePosition,\n withFrom,\n withTo,\n} from \"./activity-feed-helpers\";\nimport type { ActivityActorFilter } from \"@fluid-app/portal-core/widgets-api\";\nimport { ActivityDetailCard } from \"./ActivityDetailCard\";\nimport {\n CONTACTS_SLUG,\n PortalDetailLink,\n usePortalDetailReachable,\n} from \"./activity-detail-links\";\nimport type { ShellT } from \"./activity-translation\";\n\n/**\n * Reps land here from the Recent Activity widget, whose count covers the last\n * 7 days, so the feed opens on the same window that count described.\n */\nconst DEFAULT_TIMEFRAME: ActivityTimeframe = \"last7\";\n\n/** `all` means the filter is off; it is never sent to the API. */\ntype ActorFilter = \"all\" | ActivityActorFilter;\n\nconst ACTOR_FILTERS = [\n { value: \"all\", labelKey: \"activity_actor_filter_all\" },\n { value: \"contact\", labelKey: \"activity_actor_filter_contacts\" },\n { value: \"visitor\", labelKey: \"activity_actor_filter_visitors\" },\n] as const satisfies ReadonlyArray<{\n value: ActorFilter;\n labelKey: keyof ShellDict;\n}>;\n\nconst CATEGORY_LABEL_KEYS = {\n orders: \"activity_category_orders\",\n carts: \"activity_category_carts\",\n messages: \"activity_category_messages\",\n videos: \"activity_category_videos\",\n pageViews: \"activity_category_page_views\",\n leads: \"activity_category_leads\",\n reviews: \"activity_category_reviews\",\n reactions: \"activity_category_reactions\",\n} as const satisfies Record<ActivityCategory, string>;\n\nexport function PortalActivityFeed(): JSX.Element {\n const { t, locale } = useShellTranslation();\n // Not every adapter supports every filter — asking for one it refuses\n // throws, which this screen renders as its generic error state. Only offer\n // controls the port advertises.\n const { dateRange: supportsRange, actorType: supportsActor } =\n useWidgetsApi().activityFilterSupport;\n const [timeframe, setTimeframe] =\n useState<ActivityTimeframe>(DEFAULT_TIMEFRAME);\n const [customRange, setCustomRange] = useState<ActivityDateRange>({});\n const [categories, setCategories] = useState<ActivityCategory[]>([]);\n const [actorFilter, setActorFilter] = useState<ActorFilter>(\"all\");\n\n // The input is committed to searchTerm on a debounce so a request doesn't\n // fire per keystroke.\n const [searchInput, setSearchInput] = useState(\"\");\n const navigationParent = useOptionalNavigationParent();\n // Ids that arrived unread this visit. The screen marks everything read on\n // load, so a dot driven by live readAt would disappear as soon as that\n // request lands; this snapshot keeps the dots for the visit. A remount\n // starts clean.\n const firstSeenUnread = useRef<Set<number>>(new Set());\n const [searchTerm, setSearchTerm] = useState(\"\");\n\n const slugs = useMemo(() => slugsForCategories(categories), [categories]);\n const timeframes = useMemo(\n () => timeframesFor(supportsRange),\n [supportsRange],\n );\n\n // Which day counts as \"Today\", for the group headers and the bounds relative\n // timeframes resolve to. Advanced at each local midnight rather than ticked:\n // the grouping memo stays stable, but a feed left open overnight doesn't\n // keep filtering yesterday under a Today label.\n const [nowMs, setNowMs] = useState(() => Date.now());\n\n useEffect(() => {\n const midnight = nextLocalMidnight(nowMs);\n const timer = setTimeout(\n () => setNowMs(midnight),\n Math.max(midnight - Date.now(), 0),\n );\n\n return () => clearTimeout(timer);\n }, [nowMs]);\n\n const range = useMemo(\n () => resolveTimeframe(timeframe, nowMs, customRange),\n [timeframe, nowMs, customRange],\n );\n\n // Read once so a mid-session zone change can't churn the query key and look\n // like a new filter.\n const [timeZone] = useState(viewerTimeZone);\n\n const trimmedInput = searchInput.trim();\n const searchReady = isSearchable(\n trimmedInput,\n MIN_SEARCH_LENGTH,\n MAX_SEARCH_LENGTH,\n );\n const searchInvalid = trimmedInput.length > 0 && !searchReady;\n\n // An invalid term is never sent — the 422 would render as the error state.\n // It also never clears the applied search: mid-edit, the current results\n // stay put instead of snapping back to the unfiltered feed.\n useEffect(() => {\n if (searchInvalid) return;\n\n const next = searchReady ? trimmedInput : \"\";\n if (next === searchTerm) return;\n\n const timer = setTimeout(() => setSearchTerm(next), SEARCH_DEBOUNCE_MS);\n return () => clearTimeout(timer);\n }, [trimmedInput, searchReady, searchInvalid, searchTerm]);\n\n const {\n data,\n isLoading,\n isError,\n isSuccess,\n refetch,\n fetchNextPage,\n hasNextPage,\n isFetchingNextPage,\n } = useInfiniteActivities({\n ...(slugs.length > 0 && { slugs }),\n ...(searchTerm && { query: searchTerm }),\n ...(range.daysAgo != null && { daysAgo: range.daysAgo }),\n ...(range.from && { from: range.from }),\n ...(range.to && { to: range.to }),\n // The zone the from/to days are in — without it the server reads them as\n // UTC days, which may not match the local days the headers group by.\n ...((range.from || range.to) && timeZone ? { timeZone } : {}),\n ...(actorFilter !== \"all\" && { actorType: actorFilter }),\n });\n\n // Gated on the query succeeding: marking read before the first page lands\n // cannot clear the dots (see the hook).\n useMarkActivitiesReadOnOpen(isSuccess, data?.pages[0]?.unreadCount);\n\n const activities = useMemo(\n () => data?.pages.flatMap((page) => page.activities) ?? [],\n [data],\n );\n // Every page reports the same total for a given query; the first is enough.\n const totalCount = data?.pages[0]?.totalCount;\n // During render rather than in an effect: the mark-read effect would stamp\n // readAt before an effect here got to see the rows unread. Safe because\n // Set.add is idempotent.\n for (const activity of activities) {\n if (activity.readAt === null) firstSeenUnread.current.add(activity.id);\n }\n\n const dayLabels = useMemo(\n () => ({\n today: t(\"activity_date_today\"),\n yesterday: t(\"activity_date_yesterday\"),\n }),\n [t],\n );\n const groups = useMemo(\n () => groupActivitiesByDay(activities, nowMs, dayLabels, locale),\n [activities, nowMs, dayLabels, locale],\n );\n const rowOffsets = useMemo(() => rowOffsetsByGroup(groups), [groups]);\n const totalRows = activities.length;\n\n // Based on the resolved window, not the picked timeframe: \"Custom range\"\n // with no dates narrows nothing, and the default 7-day window is not a\n // filter to clear — an empty feed there just means no recent activity.\n const isFiltered =\n categories.length > 0 ||\n range.from != null ||\n range.to != null ||\n actorFilter !== \"all\" ||\n searchTerm.length > 0;\n\n // Seeded with today rather than left empty: Safari paints a focused empty\n // date input with today's segments, indistinguishable from a committed value.\n const pickTimeframe = (next: ActivityTimeframe) => {\n if (next === \"custom\" && !customRange.from && !customRange.to) {\n const today = localDay(new Date(nowMs));\n setCustomRange({ from: today, to: today });\n }\n setTimeframe(next);\n };\n\n const clearFilters = () => {\n setCategories([]);\n setTimeframe(DEFAULT_TIMEFRAME);\n setCustomRange({});\n setActorFilter(\"all\");\n setSearchInput(\"\");\n setSearchTerm(\"\");\n };\n\n const toggleCategory = (category: ActivityCategory) => {\n setCategories((current) =>\n current.includes(category)\n ? current.filter((entry) => entry !== category)\n : [...current, category],\n );\n };\n\n return (\n <>\n <ScreenHeaderBreadcrumbs>\n <Breadcrumb>\n <BreadcrumbList className=\"text-lg\">\n {/* This screen isn't in the nav, so this crumb is the only in-app\n way back. */}\n {navigationParent && (\n <>\n <BreadcrumbItem>\n <BreadcrumbLink\n href=\"#\"\n onClick={(event) => {\n event.preventDefault();\n navigationParent.onClick();\n }}\n >\n {navigationParent.label}\n </BreadcrumbLink>\n </BreadcrumbItem>\n <BreadcrumbSeparator />\n </>\n )}\n <BreadcrumbItem>\n <BreadcrumbPage className=\"font-semibold\">\n {t(\"activity_title\")}\n </BreadcrumbPage>\n </BreadcrumbItem>\n </BreadcrumbList>\n </Breadcrumb>\n </ScreenHeaderBreadcrumbs>\n\n <div className=\"relative mx-auto max-w-4xl px-5 pt-8 pb-20 md:px-8\">\n <div>\n <h1 className=\"text-foreground truncate text-xl font-bold tracking-tight md:text-2xl\">\n {t(\"activity_title\")}\n </h1>\n {/* Only shown for a windowed query — the API cannot count all time. */}\n {totalCount != null && (\n <p className=\"text-muted-foreground mt-0.5 text-xs\">\n {t(\"activity_count\", { count: totalCount })}\n </p>\n )}\n </div>\n\n <div className=\"mt-5 flex items-center gap-3\">\n {supportsActor && (\n <ActorTabs value={actorFilter} onChange={setActorFilter} t={t} />\n )}\n <div className=\"ml-auto flex items-center gap-2\">\n <SearchField\n value={searchInput}\n onChange={setSearchInput}\n invalid={searchInvalid}\n t={t}\n />\n <TimeframeMenu\n value={timeframe}\n options={timeframes}\n onChange={pickTimeframe}\n range={customRange}\n onRangeChange={setCustomRange}\n t={t}\n />\n <CategoryMenu\n selected={categories}\n onToggle={toggleCategory}\n onClear={() => setCategories([])}\n t={t}\n />\n </div>\n </div>\n\n {isLoading ? (\n <FeedSkeleton />\n ) : isError ? (\n <ErrorState onRetry={() => void refetch()} t={t} />\n ) : activities.length === 0 ? (\n // Don't claim nothing matches while more pages remain: the legacy\n // adapter filters slugs in memory, so a page can come back empty\n // with more still to load.\n hasNextPage ? null : isFiltered ? (\n <NoResultsState onClear={clearFilters} t={t} />\n ) : (\n <EmptyState t={t} />\n )\n ) : (\n <div className=\"mt-6\">\n {groups.map((group, groupIndex) => (\n <section key={group.key}>\n <header className=\"text-muted-foreground py-3 pl-11 text-xs font-semibold\">\n {group.label}\n </header>\n <ul className=\"list-none\">\n {group.activities.map((activity, index) => (\n <ActivityRow\n key={activity.id}\n activity={activity}\n wasUnread={firstSeenUnread.current.has(activity.id)}\n /* Counted across the feed, not the day: the timeline is one\n line broken only by a date header. */\n spine={spinePositionAt(\n (rowOffsets[groupIndex] ?? 0) + index,\n totalRows,\n )}\n index={index}\n locale={locale}\n t={t}\n />\n ))}\n </ul>\n </section>\n ))}\n </div>\n )}\n\n {hasNextPage && (\n <div className=\"mt-3 flex justify-center\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => void fetchNextPage()}\n disabled={isFetchingNextPage}\n >\n {isFetchingNextPage\n ? t(\"activity_loading\")\n : t(\"activity_load_more\")}\n </Button>\n </div>\n )}\n </div>\n </>\n );\n}\n\n// ─── row ──────────────────────────────────────────────────────────────────\n\n// Drawn per row so the spine stops at the first and last avatar. -bottom-6\n// rather than bottom-0: the spine resolves against the row's content box,\n// which excludes the pb-6, so bottom-0 left a gap before each next icon. last\n// is a short stub since nothing follows it.\nconst SPINE_CLASS: Record<SpinePosition, string> = {\n only: \"hidden\",\n first: \"top-9 -bottom-6\",\n middle: \"top-0 -bottom-6\",\n last: \"top-0 h-4\",\n};\n\n// Rows fade in staggered top to bottom; the delay is capped so a full page\n// isn't still animating seconds after it arrives.\nconst ROW_STAGGER_MS = 40;\nconst MAX_STAGGERED_ROWS = 8;\n\n/**\n * Photo, then initials, then the type icon, with the type kept as a corner badge\n * whenever a face takes the circle. The widget does the same, and without it a\n * cart, an order and a page view all look alike once the rows have faces.\n *\n * Keyed on the failed URL rather than a flag: rows survive a refetch, so a photo\n * that 404s once would otherwise never be retried after the contact replaces it.\n * Decorative throughout, since the row already names the actor and the event.\n */\nfunction ActorBadge({\n avatarUrl,\n initials,\n Icon,\n}: {\n avatarUrl: string | null;\n initials: string;\n Icon: LucideIcon;\n}): JSX.Element {\n const [failedUrl, setFailedUrl] = useState<string | null>(null);\n\n if (avatarUrl && failedUrl !== avatarUrl) {\n return (\n <>\n <img\n src={avatarUrl}\n alt=\"\"\n width={32}\n height={32}\n loading=\"lazy\"\n onError={() => setFailedUrl(avatarUrl)}\n className=\"ring-border size-8 rounded-full object-cover ring-1\"\n />\n <TypeBadge Icon={Icon} />\n </>\n );\n }\n\n if (initials) {\n return (\n <>\n <span\n aria-hidden=\"true\"\n className=\"ring-border bg-muted text-foreground flex size-8 items-center justify-center rounded-full text-[11px] font-semibold tracking-wide ring-1\"\n >\n {initials}\n </span>\n <TypeBadge Icon={Icon} />\n </>\n );\n }\n\n return (\n <span\n aria-hidden=\"true\"\n className=\"ring-border bg-muted text-foreground flex size-8 items-center justify-center rounded-full ring-1\"\n >\n <Icon className=\"size-3.5\" />\n </span>\n );\n}\n\n/** Sits opposite the unread dot, so the two never cover each other. */\nfunction TypeBadge({ Icon }: { Icon: LucideIcon }): JSX.Element {\n return (\n <span\n aria-hidden=\"true\"\n className=\"bg-muted text-foreground ring-background absolute -right-0.5 -bottom-0.5 flex size-3.5 items-center justify-center rounded-full ring-2\"\n >\n <Icon className=\"size-2\" />\n </span>\n );\n}\n\n/**\n * A name only wears the link colour when it really opens: an underline on hover is\n * no affordance on a phone, and colouring a name that goes nowhere is worse than\n * leaving it plain. A visitor stays muted either way, having no page to open.\n */\nfunction actorNameClass(muted: boolean, opensContact: boolean): string {\n if (muted) return \"text-muted-foreground italic\";\n\n return opensContact\n ? \"text-primary font-medium\"\n : \"text-foreground/80 font-medium\";\n}\n\nfunction ActivityRow({\n activity,\n spine,\n index,\n locale,\n t,\n wasUnread,\n}: {\n activity: Activity;\n spine: SpinePosition;\n index: number;\n locale: string;\n t: ShellT;\n wasUnread: boolean;\n}): JSX.Element {\n const Icon = getActivityIcon(activity.slug);\n const isUnread = wasUnread || activity.readAt === null;\n const actor = describeActor(activity.actor, t);\n const headline = activity.title ?? activity.activityType;\n // Only an identified contact has a page to open, and only in a portal that\n // carries the contacts screen.\n const contactPath =\n activity.actor.type === \"contact\" && activity.actor.contactId\n ? `${CONTACTS_SLUG}/${activity.actor.contactId}`\n : null;\n const opensContact = usePortalDetailReachable(contactPath);\n\n return (\n <li\n className=\"animate-in fade-in slide-in-from-bottom-2 relative flex gap-3 pb-6 duration-300 motion-reduce:animate-none\"\n style={{\n animationDelay: `${Math.min(index, MAX_STAGGERED_ROWS) * ROW_STAGGER_MS}ms`,\n }}\n >\n <div className=\"relative flex w-8 shrink-0 justify-center\">\n <span\n aria-hidden=\"true\"\n className={`border-primary/40 absolute left-1/2 -translate-x-1/2 border-l-2 border-dashed ${SPINE_CLASS[spine]}`}\n />\n {/* Opaque background so the circle knocks a hole in the spine. */}\n <span className=\"bg-background relative z-10 flex size-8 items-center justify-center rounded-full\">\n <ActorBadge\n avatarUrl={activity.avatarUrl}\n initials={initialsForActor(activity.actor)}\n Icon={Icon}\n />\n {isUnread && (\n <span\n className=\"bg-primary border-background absolute -top-0.5 -right-0.5 size-2.5 rounded-full border-2\"\n aria-label={t(\"activity_unread\")}\n />\n )}\n </span>\n </div>\n\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-baseline gap-2\">\n <p className=\"text-foreground min-w-0 flex-1 truncate text-sm font-semibold\">\n {headline}\n </p>\n <time\n dateTime={activity.timestamp}\n className=\"text-muted-foreground shrink-0 text-xs tabular-nums\"\n >\n {formatActivityTime(activity.timestamp, locale)}\n </time>\n </div>\n\n <p className=\"text-muted-foreground mt-0.5 truncate text-xs\">\n <PortalDetailLink to={contactPath} className=\"hover:underline\">\n <span className={actorNameClass(actor.muted, opensContact)}>\n {actor.label}\n </span>\n </PortalDetailLink>\n {/* description, not targetName: the server builds targetName as\n description-or-title for the one-line home widget, so it usually\n repeats the headline. Some producers copy the headline into\n description too, so exact repeats are dropped. */}\n {activity.description && activity.description !== headline && (\n <>\n <span className=\"mx-1\">·</span>\n <span>{activity.description}</span>\n </>\n )}\n </p>\n\n {activity.details && (\n <ActivityDetailCard details={activity.details} t={t} />\n )}\n </div>\n </li>\n );\n}\n\n// ─── filters ──────────────────────────────────────────────────────────────\n\n/**\n * Radix hands radio groups back a bare string. Narrowing through the option\n * list means an unknown value is a no-op instead of an invalid request.\n */\nfunction pickOption<T extends string>(\n options: ReadonlyArray<{ value: T }>,\n candidate: string,\n): T | null {\n return options.find((option) => option.value === candidate)?.value ?? null;\n}\n\nfunction TimeframeMenu({\n value,\n options,\n onChange,\n range,\n onRangeChange,\n t,\n}: {\n value: ActivityTimeframe;\n // Element type borrowed from TIMEFRAMES so labelKey stays the narrow literal\n // union. Widening it to keyof ShellDict pulls in keys that take interpolation,\n // and then `t` demands an argument these labels do not have.\n options: ReadonlyArray<(typeof TIMEFRAMES)[number]>;\n onChange: (next: ActivityTimeframe) => void;\n range: ActivityDateRange;\n onRangeChange: (next: ActivityDateRange) => void;\n t: ShellT;\n}): JSX.Element {\n const active = options.find((frame) => frame.value === value);\n const fromId = useId();\n const toId = useId();\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"outline\" size=\"sm\" className=\"gap-1.5\">\n <Calendar\n className=\"text-muted-foreground size-3.5\"\n aria-hidden=\"true\"\n />\n {t(active?.labelKey ?? \"activity_timeframe_all\")}\n <ChevronDown className=\"size-3\" aria-hidden=\"true\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuRadioGroup\n value={value}\n onValueChange={(next) => {\n const picked = pickOption(options, next);\n if (picked) onChange(picked);\n }}\n >\n {options.map((frame) => (\n <DropdownMenuRadioItem key={frame.value} value={frame.value}>\n {t(frame.labelKey)}\n </DropdownMenuRadioItem>\n ))}\n </DropdownMenuRadioGroup>\n\n {value === \"custom\" &&\n options.some((frame) => frame.value === \"custom\") && (\n <>\n <DropdownMenuSeparator />\n <div\n className=\"flex flex-col gap-2 p-2\"\n // Radix routes single characters in a menu into typeahead, which\n // moves focus to a matching item mid-edit, and swallows Tab. Escape\n // still closes, from a capture-phase listener this cannot reach.\n onKeyDown={(event) => event.stopPropagation()}\n >\n <Label className=\"text-xs\" htmlFor={fromId}>\n {t(\"activity_range_from\")}\n </Label>\n <Input\n id={fromId}\n type=\"date\"\n value={range.from ?? \"\"}\n max={range.to ?? undefined}\n onChange={(event) =>\n onRangeChange(withFrom(range, event.target.value))\n }\n />\n <Label className=\"text-xs\" htmlFor={toId}>\n {t(\"activity_range_to\")}\n </Label>\n <Input\n id={toId}\n type=\"date\"\n value={range.to ?? \"\"}\n // The server refuses an inverted range, so the picker will not\n // offer one.\n min={range.from ?? undefined}\n onChange={(event) =>\n onRangeChange(withTo(range, event.target.value))\n }\n />\n </div>\n </>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n\nfunction ActorTabs({\n value,\n onChange,\n t,\n}: {\n value: ActorFilter;\n onChange: (next: ActorFilter) => void;\n t: ShellT;\n}): JSX.Element {\n return (\n <Tabs\n value={value}\n onValueChange={(next) => {\n const picked = pickOption(ACTOR_FILTERS, next);\n if (picked) onChange(picked);\n }}\n >\n <TabsList aria-label={t(\"activity_filter_actor\")}>\n {ACTOR_FILTERS.map((filter) => (\n <TabsTrigger key={filter.value} value={filter.value}>\n {t(filter.labelKey)}\n </TabsTrigger>\n ))}\n </TabsList>\n </Tabs>\n );\n}\n\nfunction SearchField({\n value,\n onChange,\n invalid,\n t,\n}: {\n value: string;\n onChange: (next: string) => void;\n invalid: boolean;\n t: ShellT;\n}): JSX.Element {\n const hintId = useId();\n\n return (\n <div className=\"relative\">\n <Search\n aria-hidden=\"true\"\n className=\"text-muted-foreground pointer-events-none absolute top-1/2 left-2 size-3.5 -translate-y-1/2\"\n />\n <Input\n type=\"search\"\n value={value}\n onChange={(event) => onChange(event.target.value)}\n placeholder={t(\"activity_search_placeholder\")}\n aria-label={t(\"activity_search_placeholder\")}\n maxLength={MAX_SEARCH_LENGTH}\n aria-describedby={invalid ? hintId : undefined}\n // WebKit draws its own clear control for type=\"search\", which would sit\n // beside the button below as a second way to do the same thing.\n className=\"h-8 w-44 pl-7 text-xs [&::-webkit-search-cancel-button]:appearance-none\"\n />\n {value && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t(\"activity_search_clear\")}\n onClick={() => onChange(\"\")}\n className=\"absolute top-1/2 right-0 size-8 -translate-y-1/2 px-0\"\n >\n <X className=\"size-3.5\" aria-hidden=\"true\" />\n </Button>\n )}\n {invalid && (\n <p\n id={hintId}\n className=\"text-muted-foreground absolute top-full left-0 mt-1 text-[11px]\"\n >\n {t(\"activity_search_too_short\", { count: MIN_SEARCH_LENGTH })}\n </p>\n )}\n </div>\n );\n}\n\nfunction CategoryMenu({\n selected,\n onToggle,\n onClear,\n t,\n}: {\n selected: readonly ActivityCategory[];\n onToggle: (category: ActivityCategory) => void;\n onClear: () => void;\n t: ShellT;\n}): JSX.Element {\n const label =\n selected.length === 0\n ? t(\"activity_filter_all_types\")\n : t(\"activity_filter_types\");\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"gap-1.5\"\n aria-label={label}\n >\n <Filter className=\"size-3.5\" aria-hidden=\"true\" />\n {selected.length > 0 && (\n <span className=\"text-muted-foreground tabular-nums\">\n {selected.length}\n </span>\n )}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"w-56\">\n {ACTIVITY_CATEGORY_KEYS.map((category) => (\n <DropdownMenuCheckboxItem\n key={category}\n checked={selected.includes(category)}\n onCheckedChange={() => onToggle(category)}\n // Keeps the menu open so several types can be picked in one visit.\n onSelect={(event) => event.preventDefault()}\n >\n {t(CATEGORY_LABEL_KEYS[category])}\n </DropdownMenuCheckboxItem>\n ))}\n {selected.length > 0 && (\n <>\n <DropdownMenuSeparator />\n {/* An action, not a toggle: a checkbox item here would announce a\n never-checked checkbox to screen readers. */}\n <DropdownMenuItem onSelect={onClear}>\n {t(\"activity_clear_filters\")}\n </DropdownMenuItem>\n </>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n\n// ─── states ───────────────────────────────────────────────────────────────\n\nfunction FeedSkeleton(): JSX.Element {\n return (\n <div className=\"mt-6 space-y-6\" aria-hidden=\"true\">\n {[0, 1, 2, 3, 4].map((row) => (\n <div key={row} className=\"flex gap-3\">\n <Skeleton className=\"size-8 shrink-0 rounded-full\" />\n <div className=\"flex-1 space-y-2\">\n <Skeleton className=\"h-3.5 w-1/3\" />\n <Skeleton className=\"h-3 w-2/3\" />\n </div>\n </div>\n ))}\n </div>\n );\n}\n\nfunction EmptyState({ t }: { t: ShellT }): JSX.Element {\n return (\n <div className=\"ring-border mt-6 flex flex-col items-center gap-2 rounded-2xl px-6 py-14 text-center ring-1\">\n <EmptyActivityIcon\n className=\"text-muted-foreground size-6\"\n aria-hidden=\"true\"\n />\n <p className=\"text-foreground text-sm font-semibold\">\n {t(\"activity_empty_title\")}\n </p>\n <p className=\"text-muted-foreground max-w-sm text-xs\">\n {t(\"activity_empty_description\")}\n </p>\n </div>\n );\n}\n\nfunction NoResultsState({\n onClear,\n t,\n}: {\n onClear: () => void;\n t: ShellT;\n}): JSX.Element {\n return (\n <div className=\"ring-border mt-6 flex flex-col items-center gap-2 rounded-2xl px-6 py-14 text-center ring-1\">\n <Filter className=\"text-muted-foreground size-6\" aria-hidden=\"true\" />\n <p className=\"text-foreground text-sm font-semibold\">\n {t(\"activity_no_results_title\")}\n </p>\n <p className=\"text-muted-foreground max-w-sm text-xs\">\n {t(\"activity_no_results_description\")}\n </p>\n <Button variant=\"outline\" size=\"sm\" className=\"mt-2\" onClick={onClear}>\n {t(\"activity_clear_filters\")}\n </Button>\n </div>\n );\n}\n\nfunction ErrorState({\n onRetry,\n t,\n}: {\n onRetry: () => void;\n t: ShellT;\n}): JSX.Element {\n return (\n <div className=\"ring-border mt-6 flex flex-col items-center gap-2 rounded-2xl px-6 py-14 text-center ring-1\">\n <AlertCircle className=\"text-destructive size-6\" aria-hidden=\"true\" />\n <p className=\"text-foreground text-sm font-semibold\">\n {t(\"activity_error_title\")}\n </p>\n <Button variant=\"outline\" size=\"sm\" className=\"mt-2\" onClick={onRetry}>\n {t(\"activity_error_retry\")}\n </Button>\n </div>\n );\n}\n","import type { ComponentProps } from \"react\";\nimport type {\n BackgroundValue,\n BorderRadiusOptions,\n ColorOptions,\n PaddingOptions,\n} from \"../types\";\nimport type { WidgetPropertySchema } from \"../registries/property-schema-types\";\nimport { PortalActivityFeed } from \"./activity/PortalActivityFeed\";\n\ntype ActivityScreenProps = ComponentProps<\"div\"> & {\n background?: BackgroundValue;\n textColor?: ColorOptions;\n accentColor?: ColorOptions;\n padding?: PaddingOptions;\n borderRadius?: BorderRadiusOptions;\n};\n\n/**\n * The rep's own activity feed, and the destination the Recent Activity widget\n * clicks into. Needs no API provider of its own: WidgetsApi is mounted app-wide\n * by FluidProvider.\n */\nexport function ActivityScreen({\n /* eslint-disable @typescript-eslint/no-unused-vars -- destructured to exclude from divProps spread */\n background,\n textColor,\n accentColor,\n padding,\n borderRadius,\n /* eslint-enable @typescript-eslint/no-unused-vars */\n ...divProps\n}: ActivityScreenProps): React.JSX.Element {\n return (\n <div {...divProps}>\n <PortalActivityFeed />\n </div>\n );\n}\n\nexport const activityScreenPropertySchema: WidgetPropertySchema = {\n widgetType: \"ActivityScreen\",\n displayName: \"Activity Screen\",\n tabsConfig: [{ id: \"styling\", label: \"Styling\" }],\n fields: [],\n} as const satisfies WidgetPropertySchema;\n"],"mappings":"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"}
@@ -3,8 +3,8 @@ require("./translation-api-context-factory-mbsJk2UU.cjs");
3
3
  require("./shell-translation-api-context-BxZ1W6Eb.cjs");
4
4
  require("./widgets-api-context-CdgvPvNT.cjs");
5
5
  require("./src-DPBCDigp.cjs");
6
- require("./activity-presentation-DbBDc0oA.cjs");
6
+ require("./activity-presentation-OSfAUOoY.cjs");
7
7
  require("./ScreenHeaderContext-Df1DvpEW.cjs");
8
- const require_ActivityScreen = require("./ActivityScreen-wpyJhHPK.cjs");
8
+ const require_ActivityScreen = require("./ActivityScreen-DqNFQ0L-.cjs");
9
9
  exports.ActivityScreen = require_ActivityScreen.ActivityScreen;
10
10
  exports.activityScreenPropertySchema = require_ActivityScreen.activityScreenPropertySchema;
@@ -2,7 +2,7 @@ const require_chunk = require("./chunk-9hOWP6kD.cjs");
2
2
  const require_shell_translation_api_context = require("./shell-translation-api-context-BxZ1W6Eb.cjs");
3
3
  const require_widgets_api_context = require("./widgets-api-context-CdgvPvNT.cjs");
4
4
  const require_src = require("./src-DPBCDigp.cjs");
5
- const require_activity_presentation = require("./activity-presentation-DbBDc0oA.cjs");
5
+ const require_activity_presentation = require("./activity-presentation-OSfAUOoY.cjs");
6
6
  const require_AppNavigationContext = require("./AppNavigationContext-C6FeqL-k.cjs");
7
7
  const require_use_navigation_parent = require("./use-navigation-parent-BMFPIGdi.cjs");
8
8
  const require_ScreenHeaderContext = require("./ScreenHeaderContext-Df1DvpEW.cjs");
@@ -430,14 +430,25 @@ function isReachable(slug, items) {
430
430
  return navSlugs(items).some((navSlug) => findSystemSlugByPrefix(navSlug) === base);
431
431
  }
432
432
  /**
433
+ * Whether a link to `to` would actually go somewhere, for callers that style the
434
+ * affordance themselves. Shared with PortalDetailLink rather than reimplemented,
435
+ * since contacts and messaging are opt-in: the same slug links in one portal and
436
+ * not in another, and a name painted as a link that does nothing is worse than a
437
+ * plain one.
438
+ */
439
+ function usePortalDetailReachable(to) {
440
+ const navigation = require_AppNavigationContext.useOptionalAppNavigation();
441
+ if (!to || !navigation) return false;
442
+ return isReachable(to, navigation.navItems);
443
+ }
444
+ /**
433
445
  * Wraps its children in a portal link, or leaves them alone when the link would
434
446
  * not resolve — no shell to navigate within, which is the case in tests and the
435
447
  * builder's preview, or a screen this portal does not offer.
436
448
  */
437
449
  function PortalDetailLink({ to, className, children }) {
438
- const navigation = require_AppNavigationContext.useOptionalAppNavigation();
439
- if (!to || !navigation) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children });
440
- if (!isReachable(to, navigation.navItems)) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children });
450
+ const reachable = usePortalDetailReachable(to);
451
+ if (!to || !reachable) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children });
441
452
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppLink, {
442
453
  to,
443
454
  className,
@@ -875,11 +886,61 @@ const SPINE_CLASS = {
875
886
  };
876
887
  const ROW_STAGGER_MS = 40;
877
888
  const MAX_STAGGERED_ROWS = 8;
889
+ /**
890
+ * Photo, then initials, then the type icon, with the type kept as a corner badge
891
+ * whenever a face takes the circle. The widget does the same, and without it a
892
+ * cart, an order and a page view all look alike once the rows have faces.
893
+ *
894
+ * Keyed on the failed URL rather than a flag: rows survive a refetch, so a photo
895
+ * that 404s once would otherwise never be retried after the contact replaces it.
896
+ * Decorative throughout, since the row already names the actor and the event.
897
+ */
898
+ function ActorBadge({ avatarUrl, initials, Icon }) {
899
+ const [failedUrl, setFailedUrl] = (0, react.useState)(null);
900
+ if (avatarUrl && failedUrl !== avatarUrl) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
901
+ src: avatarUrl,
902
+ alt: "",
903
+ width: 32,
904
+ height: 32,
905
+ loading: "lazy",
906
+ onError: () => setFailedUrl(avatarUrl),
907
+ className: "ring-border size-8 rounded-full object-cover ring-1"
908
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TypeBadge, { Icon })] });
909
+ if (initials) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
910
+ "aria-hidden": "true",
911
+ className: "ring-border bg-muted text-foreground flex size-8 items-center justify-center rounded-full text-[11px] font-semibold tracking-wide ring-1",
912
+ children: initials
913
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TypeBadge, { Icon })] });
914
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
915
+ "aria-hidden": "true",
916
+ className: "ring-border bg-muted text-foreground flex size-8 items-center justify-center rounded-full ring-1",
917
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: "size-3.5" })
918
+ });
919
+ }
920
+ /** Sits opposite the unread dot, so the two never cover each other. */
921
+ function TypeBadge({ Icon }) {
922
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
923
+ "aria-hidden": "true",
924
+ className: "bg-muted text-foreground ring-background absolute -right-0.5 -bottom-0.5 flex size-3.5 items-center justify-center rounded-full ring-2",
925
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: "size-2" })
926
+ });
927
+ }
928
+ /**
929
+ * A name only wears the link colour when it really opens: an underline on hover is
930
+ * no affordance on a phone, and colouring a name that goes nowhere is worse than
931
+ * leaving it plain. A visitor stays muted either way, having no page to open.
932
+ */
933
+ function actorNameClass(muted, opensContact) {
934
+ if (muted) return "text-muted-foreground italic";
935
+ return opensContact ? "text-primary font-medium" : "text-foreground/80 font-medium";
936
+ }
878
937
  function ActivityRow({ activity, spine, index, locale, t, wasUnread }) {
879
938
  const Icon = require_activity_presentation.getActivityIcon(activity.slug);
880
939
  const isUnread = wasUnread || activity.readAt === null;
881
940
  const actor = require_activity_presentation.describeActor(activity.actor, t);
882
941
  const headline = activity.title ?? activity.activityType;
942
+ const contactPath = activity.actor.type === "contact" && activity.actor.contactId ? `${CONTACTS_SLUG}/${activity.actor.contactId}` : null;
943
+ const opensContact = usePortalDetailReachable(contactPath);
883
944
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
884
945
  className: "animate-in fade-in slide-in-from-bottom-2 relative flex gap-3 pb-6 duration-300 motion-reduce:animate-none",
885
946
  style: { animationDelay: `${Math.min(index, MAX_STAGGERED_ROWS) * ROW_STAGGER_MS}ms` },
@@ -890,12 +951,10 @@ function ActivityRow({ activity, spine, index, locale, t, wasUnread }) {
890
951
  className: `border-primary/40 absolute left-1/2 -translate-x-1/2 border-l-2 border-dashed ${SPINE_CLASS[spine]}`
891
952
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
892
953
  className: "bg-background relative z-10 flex size-8 items-center justify-center rounded-full",
893
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
894
- className: "ring-border bg-muted text-foreground flex size-8 items-center justify-center rounded-full ring-1",
895
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
896
- className: "size-3.5",
897
- "aria-hidden": "true"
898
- })
954
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActorBadge, {
955
+ avatarUrl: activity.avatarUrl,
956
+ initials: require_activity_presentation.initialsForActor(activity.actor),
957
+ Icon
899
958
  }), isUnread && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
900
959
  className: "bg-primary border-background absolute -top-0.5 -right-0.5 size-2.5 rounded-full border-2",
901
960
  "aria-label": t("activity_unread")
@@ -918,10 +977,10 @@ function ActivityRow({ activity, spine, index, locale, t, wasUnread }) {
918
977
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
919
978
  className: "text-muted-foreground mt-0.5 truncate text-xs",
920
979
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PortalDetailLink, {
921
- to: activity.actor.type === "contact" && activity.actor.contactId ? `${CONTACTS_SLUG}/${activity.actor.contactId}` : null,
980
+ to: contactPath,
922
981
  className: "hover:underline",
923
982
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
924
- className: actor.muted ? "text-muted-foreground italic" : "text-foreground/80 font-medium",
983
+ className: actorNameClass(actor.muted, opensContact),
925
984
  children: actor.label
926
985
  })
927
986
  }), activity.description && activity.description !== headline && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -1235,4 +1294,4 @@ Object.defineProperty(exports, "findSystemSlugByPrefix", {
1235
1294
  }
1236
1295
  });
1237
1296
 
1238
- //# sourceMappingURL=ActivityScreen-wpyJhHPK.cjs.map
1297
+ //# sourceMappingURL=ActivityScreen-DqNFQ0L-.cjs.map