@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.
- package/dist/{ActivityScreen-BHNUUl5N.mjs → ActivityScreen-8n5MUTqb.mjs} +72 -13
- package/dist/ActivityScreen-8n5MUTqb.mjs.map +1 -0
- package/dist/{ActivityScreen-CI8YhKBs.cjs → ActivityScreen-BtJZDBL8.cjs} +2 -2
- package/dist/{ActivityScreen-wpyJhHPK.cjs → ActivityScreen-DqNFQ0L-.cjs} +72 -13
- package/dist/ActivityScreen-DqNFQ0L-.cjs.map +1 -0
- package/dist/{FluidProvider-BG7D2WE_.mjs → FluidProvider-BY-IWBBl.mjs} +2 -2
- package/dist/{FluidProvider-BG7D2WE_.mjs.map → FluidProvider-BY-IWBBl.mjs.map} +1 -1
- package/dist/{FluidProvider-BMwY9-iq.cjs → FluidProvider-uiAG1e1c.cjs} +2 -2
- package/dist/{FluidProvider-BMwY9-iq.cjs.map → FluidProvider-uiAG1e1c.cjs.map} +1 -1
- package/dist/{MessagingScreen-DxeNig66.mjs → MessagingScreen-6yKtIQrt.mjs} +5 -5
- package/dist/{MessagingScreen-CPtQwSDv.mjs → MessagingScreen-BA0Bsjhu.mjs} +2 -2
- package/dist/{MessagingScreen-CPtQwSDv.mjs.map → MessagingScreen-BA0Bsjhu.mjs.map} +1 -1
- package/dist/{MessagingScreen-_OoquxUK.cjs → MessagingScreen-BzEMYknm.cjs} +5 -5
- package/dist/{MessagingScreen-F4wILtII.cjs → MessagingScreen-CHcWfgsC.cjs} +2 -2
- package/dist/{MessagingScreen-F4wILtII.cjs.map → MessagingScreen-CHcWfgsC.cjs.map} +1 -1
- package/dist/{OrdersScreen-DcT_LmrE.cjs → OrdersScreen-B4TQJMrX.cjs} +5 -5
- package/dist/{OrdersScreen-E2OXymPQ.mjs → OrdersScreen-ClQpYrU8.mjs} +5 -5
- package/dist/{OrdersScreen-C5SI6kEM.cjs → OrdersScreen-JCTiIxCr.cjs} +3 -3
- package/dist/{OrdersScreen-C5SI6kEM.cjs.map → OrdersScreen-JCTiIxCr.cjs.map} +1 -1
- package/dist/{OrdersScreen-Cm7aNQ6W.mjs → OrdersScreen-LaaKMS1l.mjs} +3 -3
- package/dist/{OrdersScreen-Cm7aNQ6W.mjs.map → OrdersScreen-LaaKMS1l.mjs.map} +1 -1
- package/dist/{ProfileScreen-CgrFxz1m.mjs → ProfileScreen-BQADmCT1.mjs} +6 -6
- package/dist/{ProfileScreen-CqhTJvXs.cjs → ProfileScreen-Bb2UCqei.cjs} +3 -3
- package/dist/{ProfileScreen-CqhTJvXs.cjs.map → ProfileScreen-Bb2UCqei.cjs.map} +1 -1
- package/dist/{ProfileScreen-CQY8BnZm.mjs → ProfileScreen-CPKQamjq.mjs} +3 -3
- package/dist/{ProfileScreen-CQY8BnZm.mjs.map → ProfileScreen-CPKQamjq.mjs.map} +1 -1
- package/dist/{ProfileScreen-B7xaDVqM.cjs → ProfileScreen-PtQeT_Tx.cjs} +6 -6
- package/dist/{RecentActivityWidget-DemnPXZR.cjs → RecentActivityWidget-BQXZR4_G.cjs} +3 -7
- package/dist/RecentActivityWidget-BQXZR4_G.cjs.map +1 -0
- package/dist/{RecentActivityWidget-CJcOQPUf.cjs → RecentActivityWidget-CSZeB4Pp.cjs} +2 -2
- package/dist/{RecentActivityWidget-BIgJS_kr.mjs → RecentActivityWidget-DLtQZmo3.mjs} +3 -7
- package/dist/RecentActivityWidget-DLtQZmo3.mjs.map +1 -0
- package/dist/{SubscriptionsScreen-CbvJj7kc.mjs → SubscriptionsScreen-Bdb0863a.mjs} +6 -6
- package/dist/{SubscriptionsScreen-DDhm_w13.cjs → SubscriptionsScreen-BuKop-5w.cjs} +6 -6
- package/dist/{SubscriptionsScreen-ChWqn480.mjs → SubscriptionsScreen-YrSM9ea-.mjs} +4 -4
- package/dist/{SubscriptionsScreen-ChWqn480.mjs.map → SubscriptionsScreen-YrSM9ea-.mjs.map} +1 -1
- package/dist/{SubscriptionsScreen-ljALMAX-.cjs → SubscriptionsScreen-bLbgcoWU.cjs} +4 -4
- package/dist/{SubscriptionsScreen-ljALMAX-.cjs.map → SubscriptionsScreen-bLbgcoWU.cjs.map} +1 -1
- package/dist/{activity-presentation-CUBIlrYQ.mjs → activity-presentation-DN7WG2W8.mjs} +20 -2
- package/dist/{activity-presentation-CUBIlrYQ.mjs.map → activity-presentation-DN7WG2W8.mjs.map} +1 -1
- package/dist/{activity-presentation-DbBDc0oA.cjs → activity-presentation-OSfAUOoY.cjs} +25 -1
- package/dist/{activity-presentation-DbBDc0oA.cjs.map → activity-presentation-OSfAUOoY.cjs.map} +1 -1
- package/dist/{build-widget-registry-DSu1FrDF.cjs → build-widget-registry-BQLi4JqV.cjs} +3 -3
- package/dist/{build-widget-registry-DSu1FrDF.cjs.map → build-widget-registry-BQLi4JqV.cjs.map} +1 -1
- package/dist/{build-widget-registry-Du6Ssx7x.mjs → build-widget-registry-C-WhTBEe.mjs} +3 -3
- package/dist/{build-widget-registry-Du6Ssx7x.mjs.map → build-widget-registry-C-WhTBEe.mjs.map} +1 -1
- package/dist/index.cjs +20 -20
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +20 -20
- package/dist/styles/remote-widget-shadow.css +2 -2
- package/dist/{use-portal-card-add-3ds-flow-66g15igj.mjs → use-portal-card-add-3ds-flow-Cu9yqnIn.mjs} +3 -3
- package/dist/{use-portal-card-add-3ds-flow-66g15igj.mjs.map → use-portal-card-add-3ds-flow-Cu9yqnIn.mjs.map} +1 -1
- package/dist/{use-portal-card-add-3ds-flow-LyyuhE0C.cjs → use-portal-card-add-3ds-flow-fFNO6XU3.cjs} +3 -3
- package/dist/{use-portal-card-add-3ds-flow-LyyuhE0C.cjs.map → use-portal-card-add-3ds-flow-fFNO6XU3.cjs.map} +1 -1
- package/dist/utils/build-remote-widget-plugin-manifests.cjs +3 -3
- package/dist/utils/build-remote-widget-plugin-manifests.mjs +3 -3
- package/dist/utils/build-widget-registry.cjs +3 -3
- package/dist/utils/build-widget-registry.mjs +3 -3
- package/package.json +18 -18
- package/dist/ActivityScreen-BHNUUl5N.mjs.map +0 -1
- package/dist/ActivityScreen-wpyJhHPK.cjs.map +0 -1
- package/dist/RecentActivityWidget-BIgJS_kr.mjs.map +0 -1
- package/dist/RecentActivityWidget-DemnPXZR.cjs.map +0 -1
|
@@ -183,6 +183,24 @@ function describeActor(actor, t) {
|
|
|
183
183
|
default: return describeUnhandledActor(actor, t);
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* First and last initial of whoever a row is about, empty when there is no person
|
|
188
|
+
* to show. First and last rather than the first two, matching the contacts list
|
|
189
|
+
* and the profile menu, so one contact does not get two different monograms.
|
|
190
|
+
*
|
|
191
|
+
* Reads the name off the actor rather than a rendered label: a contact the server
|
|
192
|
+
* sent no name labels as the translated word "Contact", and its first letter
|
|
193
|
+
* would read as a real person's initial in every one of the 34 locales.
|
|
194
|
+
*/
|
|
195
|
+
function initialsForActor(actor) {
|
|
196
|
+
if (actor.type !== "contact") return "";
|
|
197
|
+
const name = actor.name?.trim() ?? "";
|
|
198
|
+
if (name === "" || name.includes("@")) return "";
|
|
199
|
+
const letters = name.split(/\s+/).map((word) => [...word][0] ?? "").filter((char) => /\p{L}/u.test(char));
|
|
200
|
+
const first = letters[0];
|
|
201
|
+
if (first === void 0) return "";
|
|
202
|
+
return (first + (letters.length > 1 ? letters[letters.length - 1] : "")).toUpperCase();
|
|
203
|
+
}
|
|
186
204
|
function describeUnhandledActor(actor, t) {
|
|
187
205
|
return {
|
|
188
206
|
label: t("activity_actor_unknown"),
|
|
@@ -237,6 +255,6 @@ function formatActivityRelativeTime(timestamp, now, locale = "en-US") {
|
|
|
237
255
|
});
|
|
238
256
|
}
|
|
239
257
|
//#endregion
|
|
240
|
-
export { formatActivityRelativeTime as a, getActivityIcon as c, formatActivityDateHeader as i,
|
|
258
|
+
export { formatActivityRelativeTime as a, getActivityIcon as c, formatActivityDateHeader as i, initialsForActor as l, EmptyActivityIcon as n, formatActivityTime as o, describeActor as r, getActivityDateKey as s, ACTIVITY_CATEGORY_KEYS as t, slugsForCategories as u };
|
|
241
259
|
|
|
242
|
-
//# sourceMappingURL=activity-presentation-
|
|
260
|
+
//# sourceMappingURL=activity-presentation-DN7WG2W8.mjs.map
|
package/dist/{activity-presentation-CUBIlrYQ.mjs.map → activity-presentation-DN7WG2W8.mjs.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"activity-presentation-CUBIlrYQ.mjs","names":[],"sources":["../../core/src/activity-slug.ts","../../core/src/activity-presentation.ts"],"sourcesContent":["import type { ActivitySlug } from \"./widgets-api-types\";\n\n// Typing as `Record<ActivitySlug, true>` makes TS reject this object when a new\n// value is added to `ActivitySlug` and not registered here, so the guard stays\n// exhaustive without a separate test.\nconst KNOWN_ACTIVITY_SLUGS: Record<ActivitySlug, true> = {\n abandoned_cart: true,\n announcements: true,\n cart_item_removed: true,\n cart_items_added: true,\n comment_like: true,\n comment_reply: true,\n direct_message: true,\n emoji_reaction: true,\n enrollment: true,\n external_order: true,\n fantasy_point: true,\n imported: true,\n media_views: true,\n new_lead: true,\n order_placed: true,\n page_views: true,\n page_views_contact: true,\n tasks: true,\n upcoming_event: true,\n video: true,\n video_complete: true,\n video_complete_contact: true,\n video_contact: true,\n video_started: true,\n message_received: true,\n message_sent: true,\n new_cart_items_added: true,\n smart_link_clicked: true,\n review_left: true,\n};\n\n/**\n * Narrows a server slug to one this client knows how to present.\n *\n * This is a presentation lookup, never a filter: the server's slug set grows\n * independently, and an unrecognized slug still renders with generic treatment.\n */\nexport function isActivitySlug(slug: string): slug is ActivitySlug {\n return Object.hasOwn(KNOWN_ACTIVITY_SLUGS, slug);\n}\n","import {\n AudioWaveform,\n Bell,\n Calendar,\n CheckSquare,\n Eye,\n MessageCircle,\n Play,\n ShoppingCart,\n SmilePlus,\n Star,\n Trophy,\n User,\n UserPlus,\n type LucideIcon,\n} from \"lucide-react\";\nimport type { TranslationApi } from \"@fluid-app/i18n/translation-api\";\nimport { isActivitySlug } from \"./activity-slug\";\nimport type { ShellDict } from \"./shell-translation-dictionary\";\nimport type { ActivityActor, ActivitySlug } from \"./widgets-api-types\";\n\n/**\n * The shell translator, passed in rather than read from context, so the pieces\n * below a screen stay context-free and the widget can share them.\n */\nexport type ShellT = TranslationApi<ShellDict>[\"t\"];\n\n/**\n * Presentation shared by the Recent Activity widget and the Activity screen.\n * Both surfaces must agree — the widget's count is a promise about what the\n * screen will show — so the icon map and date bucketing live here rather than\n * being duplicated per renderer.\n */\n\nconst ACTIVITY_ICON_MAP: Record<ActivitySlug, LucideIcon> = {\n order_placed: ShoppingCart,\n external_order: ShoppingCart,\n abandoned_cart: ShoppingCart,\n cart_items_added: ShoppingCart,\n cart_item_removed: ShoppingCart,\n new_cart_items_added: ShoppingCart,\n direct_message: MessageCircle,\n comment_reply: MessageCircle,\n comment_like: MessageCircle,\n message_received: MessageCircle,\n message_sent: MessageCircle,\n video: Play,\n video_started: Play,\n video_complete: Play,\n video_contact: Play,\n video_complete_contact: Play,\n media_views: Play,\n new_lead: UserPlus,\n enrollment: UserPlus,\n imported: UserPlus,\n page_views_contact: UserPlus,\n smart_link_clicked: UserPlus,\n page_views: Eye,\n upcoming_event: Calendar,\n review_left: Star,\n emoji_reaction: SmilePlus,\n tasks: CheckSquare,\n announcements: Bell,\n fantasy_point: Trophy,\n};\n\n/**\n * Slugs grouped into the categories a rep filters by. Reps think in \"orders\"\n * and \"videos\", not in the ~30 slugs the platform writes, and each category\n * expands to a server-side slug filter so the results stay honest across\n * pagination.\n *\n * A slug missing from every category is still shown; it just cannot be\n * filtered to on its own.\n */\nexport const ACTIVITY_CATEGORIES = {\n orders: [\"order_placed\", \"external_order\"],\n carts: [\n \"abandoned_cart\",\n \"cart_items_added\",\n \"new_cart_items_added\",\n \"cart_item_removed\",\n ],\n messages: [\n \"direct_message\",\n \"message_received\",\n \"message_sent\",\n \"comment_reply\",\n \"comment_like\",\n ],\n videos: [\n \"video\",\n \"video_started\",\n \"video_complete\",\n \"video_contact\",\n \"video_complete_contact\",\n \"media_views\",\n ],\n pageViews: [\"page_views\", \"page_views_contact\", \"smart_link_clicked\"],\n leads: [\"new_lead\", \"enrollment\", \"imported\"],\n reviews: [\"review_left\"],\n reactions: [\"emoji_reaction\"],\n // Every category has to be able to return a row, since a filter is just another\n // view of the same types the feed renders cards for. Two are absent for that\n // reason: tasks, upcoming_event and announcements are written to notifications\n // rather than activities, and fantasy_point has no writer at all. Offering\n // either would have been a filter that always came back empty.\n} as const satisfies Record<string, readonly ActivitySlug[]>;\n\nexport type ActivityCategory = keyof typeof ACTIVITY_CATEGORIES;\n\nexport const ACTIVITY_CATEGORY_KEYS = Object.keys(\n ACTIVITY_CATEGORIES,\n) as ActivityCategory[];\n\n/** Flattens selected categories into the slug list the API filter takes. */\nexport function slugsForCategories(\n categories: readonly ActivityCategory[],\n): string[] {\n return categories.flatMap((category) => [...ACTIVITY_CATEGORIES[category]]);\n}\n\n/** Stand-in for a slug this client has no specific treatment for. */\nexport const GenericActivityIcon: LucideIcon = User;\n\n/** Icon for the empty feed, exported so widget and screen match. */\nexport const EmptyActivityIcon: LucideIcon = AudioWaveform;\n\n/**\n * Icon for a raw server slug. Unrecognized slugs get the generic icon rather\n * than being hidden — the server's slug set grows independently of this client.\n */\nexport function getActivityIcon(slug: string): LucideIcon {\n return isActivitySlug(slug) ? ACTIVITY_ICON_MAP[slug] : GenericActivityIcon;\n}\n\n/** Local-time YYYY-MM-DD, the key activities are grouped into days by. */\nexport function getActivityDateKey(timestamp: string): string {\n const date = new Date(timestamp);\n if (!Number.isFinite(date.getTime())) return timestamp;\n\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, \"0\");\n const day = String(date.getDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n\n/** Labels only the caller can supply, because only it has a translator. */\n/**\n * How to label and style whoever an activity is about.\n *\n * Composed here rather than read off `userName`, which the server writes in\n * English. `muted` is what makes an anonymous visitor read differently from an\n * identified contact, the distinction mobile draws with a different avatar.\n */\nexport function describeActor(\n actor: ActivityActor,\n t: ShellT,\n): { label: string; muted: boolean } {\n switch (actor.type) {\n case \"contact\":\n return {\n label: actor.name?.trim() || t(\"activity_actor_contact\"),\n muted: false,\n };\n case \"visitor\": {\n // City and state go in as separate values rather than a joined string. The\n // separator and their order are part of the sentence, and both vary: ja and\n // zh use a full-width comma with no space, ar and he need RTL ordering, and\n // several locales put the region first. Joining in TS gives a translator no\n // handle on any of it.\n const city = actor.city?.trim();\n const state = actor.state?.trim();\n\n if (city && state) {\n return {\n label: t(\"activity_actor_visitor_from_city_state\", { city, state }),\n muted: true,\n };\n }\n\n const single = city || state;\n\n return {\n label: single\n ? t(\"activity_actor_visitor_from_city\", { city: single })\n : t(\"activity_actor_visitor\"),\n muted: true,\n };\n }\n case \"unknown\":\n return { label: t(\"activity_actor_unknown\"), muted: true };\n default:\n // ActivityActor is closed and local, so a variant added without a case here\n // should fail the typecheck rather than quietly read as unknown.\n return describeUnhandledActor(actor, t);\n }\n}\n\nfunction describeUnhandledActor(\n actor: never,\n t: ShellT,\n): { label: string; muted: boolean } {\n void actor;\n\n return { label: t(\"activity_actor_unknown\"), muted: true };\n}\n\nexport type ActivityDayLabels = {\n today: string;\n /**\n * Omit to head yesterday with its date instead.\n *\n * The activity screen does, because it reserves Yesterday for the timeframe\n * filter: seeing it as a group heading as well made one word mean two things.\n * The widget still passes it.\n */\n yesterday?: string | undefined;\n};\n\n/**\n * Heading for a day group: the caller's label for today, the same for yesterday\n * when it supplies one, and otherwise a localized month and day.\n */\nexport function formatActivityDateHeader(\n timestamp: string,\n nowMs: number,\n labels: ActivityDayLabels,\n locale = \"en-US\",\n): string {\n const date = new Date(timestamp);\n if (!Number.isFinite(date.getTime())) return timestamp;\n\n const now = new Date(nowMs);\n const yesterday = new Date(now);\n yesterday.setDate(now.getDate() - 1);\n\n const dateKey = getActivityDateKey(timestamp);\n if (dateKey === getActivityDateKey(now.toISOString())) return labels.today;\n if (\n labels.yesterday &&\n dateKey === getActivityDateKey(yesterday.toISOString())\n ) {\n return labels.yesterday;\n }\n\n return date.toLocaleDateString(locale, { month: \"long\", day: \"numeric\" });\n}\n\n/** Clock time for an activity row, e.g. \"4:12 PM\". */\nexport function formatActivityTime(\n timestamp: string,\n locale = \"en-US\",\n): string {\n return new Date(timestamp).toLocaleTimeString(locale, {\n hour: \"numeric\",\n minute: \"2-digit\",\n hour12: true,\n });\n}\n\n/**\n * Compact age, e.g. \"now\", \"5m\", \"3h\", \"2d\", then a date past a week.\n *\n * The units are unlocalized single letters; only the widget uses this, and its\n * copy is not translated yet.\n */\nexport function formatActivityRelativeTime(\n timestamp: string,\n now: number,\n locale = \"en-US\",\n): string {\n const then = new Date(timestamp).getTime();\n const diffSec = Math.max(0, Math.round((now - then) / 1000));\n\n if (diffSec < 60) return \"now\";\n const diffMin = Math.floor(diffSec / 60);\n if (diffMin < 60) return `${diffMin}m`;\n const diffHr = Math.floor(diffMin / 60);\n if (diffHr < 24) return `${diffHr}h`;\n const diffDay = Math.floor(diffHr / 24);\n if (diffDay < 7) return `${diffDay}d`;\n\n return new Date(timestamp).toLocaleDateString(locale, {\n month: \"short\",\n day: \"numeric\",\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"activity-presentation-DN7WG2W8.mjs","names":[],"sources":["../../core/src/activity-slug.ts","../../core/src/activity-presentation.ts"],"sourcesContent":["import type { ActivitySlug } from \"./widgets-api-types\";\n\n// Typing as `Record<ActivitySlug, true>` makes TS reject this object when a new\n// value is added to `ActivitySlug` and not registered here, so the guard stays\n// exhaustive without a separate test.\nconst KNOWN_ACTIVITY_SLUGS: Record<ActivitySlug, true> = {\n abandoned_cart: true,\n announcements: true,\n cart_item_removed: true,\n cart_items_added: true,\n comment_like: true,\n comment_reply: true,\n direct_message: true,\n emoji_reaction: true,\n enrollment: true,\n external_order: true,\n fantasy_point: true,\n imported: true,\n media_views: true,\n new_lead: true,\n order_placed: true,\n page_views: true,\n page_views_contact: true,\n tasks: true,\n upcoming_event: true,\n video: true,\n video_complete: true,\n video_complete_contact: true,\n video_contact: true,\n video_started: true,\n message_received: true,\n message_sent: true,\n new_cart_items_added: true,\n smart_link_clicked: true,\n review_left: true,\n};\n\n/**\n * Narrows a server slug to one this client knows how to present.\n *\n * This is a presentation lookup, never a filter: the server's slug set grows\n * independently, and an unrecognized slug still renders with generic treatment.\n */\nexport function isActivitySlug(slug: string): slug is ActivitySlug {\n return Object.hasOwn(KNOWN_ACTIVITY_SLUGS, slug);\n}\n","import {\n AudioWaveform,\n Bell,\n Calendar,\n CheckSquare,\n Eye,\n MessageCircle,\n Play,\n ShoppingCart,\n SmilePlus,\n Star,\n Trophy,\n User,\n UserPlus,\n type LucideIcon,\n} from \"lucide-react\";\nimport type { TranslationApi } from \"@fluid-app/i18n/translation-api\";\nimport { isActivitySlug } from \"./activity-slug\";\nimport type { ShellDict } from \"./shell-translation-dictionary\";\nimport type { ActivityActor, ActivitySlug } from \"./widgets-api-types\";\n\n/**\n * The shell translator, passed in rather than read from context, so the pieces\n * below a screen stay context-free and the widget can share them.\n */\nexport type ShellT = TranslationApi<ShellDict>[\"t\"];\n\n/**\n * Presentation shared by the Recent Activity widget and the Activity screen.\n * Both surfaces must agree — the widget's count is a promise about what the\n * screen will show — so the icon map and date bucketing live here rather than\n * being duplicated per renderer.\n */\n\nconst ACTIVITY_ICON_MAP: Record<ActivitySlug, LucideIcon> = {\n order_placed: ShoppingCart,\n external_order: ShoppingCart,\n abandoned_cart: ShoppingCart,\n cart_items_added: ShoppingCart,\n cart_item_removed: ShoppingCart,\n new_cart_items_added: ShoppingCart,\n direct_message: MessageCircle,\n comment_reply: MessageCircle,\n comment_like: MessageCircle,\n message_received: MessageCircle,\n message_sent: MessageCircle,\n video: Play,\n video_started: Play,\n video_complete: Play,\n video_contact: Play,\n video_complete_contact: Play,\n media_views: Play,\n new_lead: UserPlus,\n enrollment: UserPlus,\n imported: UserPlus,\n page_views_contact: UserPlus,\n smart_link_clicked: UserPlus,\n page_views: Eye,\n upcoming_event: Calendar,\n review_left: Star,\n emoji_reaction: SmilePlus,\n tasks: CheckSquare,\n announcements: Bell,\n fantasy_point: Trophy,\n};\n\n/**\n * Slugs grouped into the categories a rep filters by. Reps think in \"orders\"\n * and \"videos\", not in the ~30 slugs the platform writes, and each category\n * expands to a server-side slug filter so the results stay honest across\n * pagination.\n *\n * A slug missing from every category is still shown; it just cannot be\n * filtered to on its own.\n */\nexport const ACTIVITY_CATEGORIES = {\n orders: [\"order_placed\", \"external_order\"],\n carts: [\n \"abandoned_cart\",\n \"cart_items_added\",\n \"new_cart_items_added\",\n \"cart_item_removed\",\n ],\n messages: [\n \"direct_message\",\n \"message_received\",\n \"message_sent\",\n \"comment_reply\",\n \"comment_like\",\n ],\n videos: [\n \"video\",\n \"video_started\",\n \"video_complete\",\n \"video_contact\",\n \"video_complete_contact\",\n \"media_views\",\n ],\n pageViews: [\"page_views\", \"page_views_contact\", \"smart_link_clicked\"],\n leads: [\"new_lead\", \"enrollment\", \"imported\"],\n reviews: [\"review_left\"],\n reactions: [\"emoji_reaction\"],\n // Every category has to be able to return a row, since a filter is just another\n // view of the same types the feed renders cards for. Two are absent for that\n // reason: tasks, upcoming_event and announcements are written to notifications\n // rather than activities, and fantasy_point has no writer at all. Offering\n // either would have been a filter that always came back empty.\n} as const satisfies Record<string, readonly ActivitySlug[]>;\n\nexport type ActivityCategory = keyof typeof ACTIVITY_CATEGORIES;\n\nexport const ACTIVITY_CATEGORY_KEYS = Object.keys(\n ACTIVITY_CATEGORIES,\n) as ActivityCategory[];\n\n/** Flattens selected categories into the slug list the API filter takes. */\nexport function slugsForCategories(\n categories: readonly ActivityCategory[],\n): string[] {\n return categories.flatMap((category) => [...ACTIVITY_CATEGORIES[category]]);\n}\n\n/** Stand-in for a slug this client has no specific treatment for. */\nexport const GenericActivityIcon: LucideIcon = User;\n\n/** Icon for the empty feed, exported so widget and screen match. */\nexport const EmptyActivityIcon: LucideIcon = AudioWaveform;\n\n/**\n * Icon for a raw server slug. Unrecognized slugs get the generic icon rather\n * than being hidden — the server's slug set grows independently of this client.\n */\nexport function getActivityIcon(slug: string): LucideIcon {\n return isActivitySlug(slug) ? ACTIVITY_ICON_MAP[slug] : GenericActivityIcon;\n}\n\n/** Local-time YYYY-MM-DD, the key activities are grouped into days by. */\nexport function getActivityDateKey(timestamp: string): string {\n const date = new Date(timestamp);\n if (!Number.isFinite(date.getTime())) return timestamp;\n\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, \"0\");\n const day = String(date.getDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n\n/** Labels only the caller can supply, because only it has a translator. */\n/**\n * How to label and style whoever an activity is about.\n *\n * Composed here rather than read off `userName`, which the server writes in\n * English. `muted` is what makes an anonymous visitor read differently from an\n * identified contact, the distinction mobile draws with a different avatar.\n */\nexport function describeActor(\n actor: ActivityActor,\n t: ShellT,\n): { label: string; muted: boolean } {\n switch (actor.type) {\n case \"contact\":\n return {\n label: actor.name?.trim() || t(\"activity_actor_contact\"),\n muted: false,\n };\n case \"visitor\": {\n // City and state go in as separate values rather than a joined string. The\n // separator and their order are part of the sentence, and both vary: ja and\n // zh use a full-width comma with no space, ar and he need RTL ordering, and\n // several locales put the region first. Joining in TS gives a translator no\n // handle on any of it.\n const city = actor.city?.trim();\n const state = actor.state?.trim();\n\n if (city && state) {\n return {\n label: t(\"activity_actor_visitor_from_city_state\", { city, state }),\n muted: true,\n };\n }\n\n const single = city || state;\n\n return {\n label: single\n ? t(\"activity_actor_visitor_from_city\", { city: single })\n : t(\"activity_actor_visitor\"),\n muted: true,\n };\n }\n case \"unknown\":\n return { label: t(\"activity_actor_unknown\"), muted: true };\n default:\n // ActivityActor is closed and local, so a variant added without a case here\n // should fail the typecheck rather than quietly read as unknown.\n return describeUnhandledActor(actor, t);\n }\n}\n\n/**\n * First and last initial of whoever a row is about, empty when there is no person\n * to show. First and last rather than the first two, matching the contacts list\n * and the profile menu, so one contact does not get two different monograms.\n *\n * Reads the name off the actor rather than a rendered label: a contact the server\n * sent no name labels as the translated word \"Contact\", and its first letter\n * would read as a real person's initial in every one of the 34 locales.\n */\nexport function initialsForActor(actor: ActivityActor): string {\n if (actor.type !== \"contact\") return \"\";\n\n const name = actor.name?.trim() ?? \"\";\n\n // A contact with no name falls back to their email or phone, and neither makes a\n // monogram. An address is the case worth naming: \"jane@example.com\" starts with a\n // letter, so it would otherwise pass for a first name.\n if (name === \"\" || name.includes(\"@\")) return \"\";\n\n // Spread rather than charAt, so a name starting outside the BMP keeps its first\n // character whole instead of half a surrogate pair. Letters only, so a phone\n // number does not put a plus sign in the circle where a face goes.\n const letters = name\n .split(/\\s+/)\n .map((word) => [...word][0] ?? \"\")\n .filter((char) => /\\p{L}/u.test(char));\n\n const first = letters[0];\n if (first === undefined) return \"\";\n\n const last = letters.length > 1 ? letters[letters.length - 1] : \"\";\n\n return (first + last).toUpperCase();\n}\n\nfunction describeUnhandledActor(\n actor: never,\n t: ShellT,\n): { label: string; muted: boolean } {\n void actor;\n\n return { label: t(\"activity_actor_unknown\"), muted: true };\n}\n\nexport type ActivityDayLabels = {\n today: string;\n /**\n * Omit to head yesterday with its date instead.\n *\n * The activity screen does, because it reserves Yesterday for the timeframe\n * filter: seeing it as a group heading as well made one word mean two things.\n * The widget still passes it.\n */\n yesterday?: string | undefined;\n};\n\n/**\n * Heading for a day group: the caller's label for today, the same for yesterday\n * when it supplies one, and otherwise a localized month and day.\n */\nexport function formatActivityDateHeader(\n timestamp: string,\n nowMs: number,\n labels: ActivityDayLabels,\n locale = \"en-US\",\n): string {\n const date = new Date(timestamp);\n if (!Number.isFinite(date.getTime())) return timestamp;\n\n const now = new Date(nowMs);\n const yesterday = new Date(now);\n yesterday.setDate(now.getDate() - 1);\n\n const dateKey = getActivityDateKey(timestamp);\n if (dateKey === getActivityDateKey(now.toISOString())) return labels.today;\n if (\n labels.yesterday &&\n dateKey === getActivityDateKey(yesterday.toISOString())\n ) {\n return labels.yesterday;\n }\n\n return date.toLocaleDateString(locale, { month: \"long\", day: \"numeric\" });\n}\n\n/** Clock time for an activity row, e.g. \"4:12 PM\". */\nexport function formatActivityTime(\n timestamp: string,\n locale = \"en-US\",\n): string {\n return new Date(timestamp).toLocaleTimeString(locale, {\n hour: \"numeric\",\n minute: \"2-digit\",\n hour12: true,\n });\n}\n\n/**\n * Compact age, e.g. \"now\", \"5m\", \"3h\", \"2d\", then a date past a week.\n *\n * The units are unlocalized single letters; only the widget uses this, and its\n * copy is not translated yet.\n */\nexport function formatActivityRelativeTime(\n timestamp: string,\n now: number,\n locale = \"en-US\",\n): string {\n const then = new Date(timestamp).getTime();\n const diffSec = Math.max(0, Math.round((now - then) / 1000));\n\n if (diffSec < 60) return \"now\";\n const diffMin = Math.floor(diffSec / 60);\n if (diffMin < 60) return `${diffMin}m`;\n const diffHr = Math.floor(diffMin / 60);\n if (diffHr < 24) return `${diffHr}h`;\n const diffDay = Math.floor(diffHr / 24);\n if (diffDay < 7) return `${diffDay}d`;\n\n return new Date(timestamp).toLocaleDateString(locale, {\n month: \"short\",\n day: \"numeric\",\n });\n}\n"],"mappings":"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"}
|
|
@@ -184,6 +184,24 @@ function describeActor(actor, t) {
|
|
|
184
184
|
default: return describeUnhandledActor(actor, t);
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* First and last initial of whoever a row is about, empty when there is no person
|
|
189
|
+
* to show. First and last rather than the first two, matching the contacts list
|
|
190
|
+
* and the profile menu, so one contact does not get two different monograms.
|
|
191
|
+
*
|
|
192
|
+
* Reads the name off the actor rather than a rendered label: a contact the server
|
|
193
|
+
* sent no name labels as the translated word "Contact", and its first letter
|
|
194
|
+
* would read as a real person's initial in every one of the 34 locales.
|
|
195
|
+
*/
|
|
196
|
+
function initialsForActor(actor) {
|
|
197
|
+
if (actor.type !== "contact") return "";
|
|
198
|
+
const name = actor.name?.trim() ?? "";
|
|
199
|
+
if (name === "" || name.includes("@")) return "";
|
|
200
|
+
const letters = name.split(/\s+/).map((word) => [...word][0] ?? "").filter((char) => /\p{L}/u.test(char));
|
|
201
|
+
const first = letters[0];
|
|
202
|
+
if (first === void 0) return "";
|
|
203
|
+
return (first + (letters.length > 1 ? letters[letters.length - 1] : "")).toUpperCase();
|
|
204
|
+
}
|
|
187
205
|
function describeUnhandledActor(actor, t) {
|
|
188
206
|
return {
|
|
189
207
|
label: t("activity_actor_unknown"),
|
|
@@ -286,6 +304,12 @@ Object.defineProperty(exports, "getActivityIcon", {
|
|
|
286
304
|
return getActivityIcon;
|
|
287
305
|
}
|
|
288
306
|
});
|
|
307
|
+
Object.defineProperty(exports, "initialsForActor", {
|
|
308
|
+
enumerable: true,
|
|
309
|
+
get: function() {
|
|
310
|
+
return initialsForActor;
|
|
311
|
+
}
|
|
312
|
+
});
|
|
289
313
|
Object.defineProperty(exports, "slugsForCategories", {
|
|
290
314
|
enumerable: true,
|
|
291
315
|
get: function() {
|
|
@@ -293,4 +317,4 @@ Object.defineProperty(exports, "slugsForCategories", {
|
|
|
293
317
|
}
|
|
294
318
|
});
|
|
295
319
|
|
|
296
|
-
//# sourceMappingURL=activity-presentation-
|
|
320
|
+
//# sourceMappingURL=activity-presentation-OSfAUOoY.cjs.map
|
package/dist/{activity-presentation-DbBDc0oA.cjs.map → activity-presentation-OSfAUOoY.cjs.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"activity-presentation-DbBDc0oA.cjs","names":["ShoppingCart","MessageCircle","Play","UserPlus","Eye","Calendar","Star","SmilePlus","CheckSquare","Bell","Trophy","User","AudioWaveform"],"sources":["../../core/src/activity-slug.ts","../../core/src/activity-presentation.ts"],"sourcesContent":["import type { ActivitySlug } from \"./widgets-api-types\";\n\n// Typing as `Record<ActivitySlug, true>` makes TS reject this object when a new\n// value is added to `ActivitySlug` and not registered here, so the guard stays\n// exhaustive without a separate test.\nconst KNOWN_ACTIVITY_SLUGS: Record<ActivitySlug, true> = {\n abandoned_cart: true,\n announcements: true,\n cart_item_removed: true,\n cart_items_added: true,\n comment_like: true,\n comment_reply: true,\n direct_message: true,\n emoji_reaction: true,\n enrollment: true,\n external_order: true,\n fantasy_point: true,\n imported: true,\n media_views: true,\n new_lead: true,\n order_placed: true,\n page_views: true,\n page_views_contact: true,\n tasks: true,\n upcoming_event: true,\n video: true,\n video_complete: true,\n video_complete_contact: true,\n video_contact: true,\n video_started: true,\n message_received: true,\n message_sent: true,\n new_cart_items_added: true,\n smart_link_clicked: true,\n review_left: true,\n};\n\n/**\n * Narrows a server slug to one this client knows how to present.\n *\n * This is a presentation lookup, never a filter: the server's slug set grows\n * independently, and an unrecognized slug still renders with generic treatment.\n */\nexport function isActivitySlug(slug: string): slug is ActivitySlug {\n return Object.hasOwn(KNOWN_ACTIVITY_SLUGS, slug);\n}\n","import {\n AudioWaveform,\n Bell,\n Calendar,\n CheckSquare,\n Eye,\n MessageCircle,\n Play,\n ShoppingCart,\n SmilePlus,\n Star,\n Trophy,\n User,\n UserPlus,\n type LucideIcon,\n} from \"lucide-react\";\nimport type { TranslationApi } from \"@fluid-app/i18n/translation-api\";\nimport { isActivitySlug } from \"./activity-slug\";\nimport type { ShellDict } from \"./shell-translation-dictionary\";\nimport type { ActivityActor, ActivitySlug } from \"./widgets-api-types\";\n\n/**\n * The shell translator, passed in rather than read from context, so the pieces\n * below a screen stay context-free and the widget can share them.\n */\nexport type ShellT = TranslationApi<ShellDict>[\"t\"];\n\n/**\n * Presentation shared by the Recent Activity widget and the Activity screen.\n * Both surfaces must agree — the widget's count is a promise about what the\n * screen will show — so the icon map and date bucketing live here rather than\n * being duplicated per renderer.\n */\n\nconst ACTIVITY_ICON_MAP: Record<ActivitySlug, LucideIcon> = {\n order_placed: ShoppingCart,\n external_order: ShoppingCart,\n abandoned_cart: ShoppingCart,\n cart_items_added: ShoppingCart,\n cart_item_removed: ShoppingCart,\n new_cart_items_added: ShoppingCart,\n direct_message: MessageCircle,\n comment_reply: MessageCircle,\n comment_like: MessageCircle,\n message_received: MessageCircle,\n message_sent: MessageCircle,\n video: Play,\n video_started: Play,\n video_complete: Play,\n video_contact: Play,\n video_complete_contact: Play,\n media_views: Play,\n new_lead: UserPlus,\n enrollment: UserPlus,\n imported: UserPlus,\n page_views_contact: UserPlus,\n smart_link_clicked: UserPlus,\n page_views: Eye,\n upcoming_event: Calendar,\n review_left: Star,\n emoji_reaction: SmilePlus,\n tasks: CheckSquare,\n announcements: Bell,\n fantasy_point: Trophy,\n};\n\n/**\n * Slugs grouped into the categories a rep filters by. Reps think in \"orders\"\n * and \"videos\", not in the ~30 slugs the platform writes, and each category\n * expands to a server-side slug filter so the results stay honest across\n * pagination.\n *\n * A slug missing from every category is still shown; it just cannot be\n * filtered to on its own.\n */\nexport const ACTIVITY_CATEGORIES = {\n orders: [\"order_placed\", \"external_order\"],\n carts: [\n \"abandoned_cart\",\n \"cart_items_added\",\n \"new_cart_items_added\",\n \"cart_item_removed\",\n ],\n messages: [\n \"direct_message\",\n \"message_received\",\n \"message_sent\",\n \"comment_reply\",\n \"comment_like\",\n ],\n videos: [\n \"video\",\n \"video_started\",\n \"video_complete\",\n \"video_contact\",\n \"video_complete_contact\",\n \"media_views\",\n ],\n pageViews: [\"page_views\", \"page_views_contact\", \"smart_link_clicked\"],\n leads: [\"new_lead\", \"enrollment\", \"imported\"],\n reviews: [\"review_left\"],\n reactions: [\"emoji_reaction\"],\n // Every category has to be able to return a row, since a filter is just another\n // view of the same types the feed renders cards for. Two are absent for that\n // reason: tasks, upcoming_event and announcements are written to notifications\n // rather than activities, and fantasy_point has no writer at all. Offering\n // either would have been a filter that always came back empty.\n} as const satisfies Record<string, readonly ActivitySlug[]>;\n\nexport type ActivityCategory = keyof typeof ACTIVITY_CATEGORIES;\n\nexport const ACTIVITY_CATEGORY_KEYS = Object.keys(\n ACTIVITY_CATEGORIES,\n) as ActivityCategory[];\n\n/** Flattens selected categories into the slug list the API filter takes. */\nexport function slugsForCategories(\n categories: readonly ActivityCategory[],\n): string[] {\n return categories.flatMap((category) => [...ACTIVITY_CATEGORIES[category]]);\n}\n\n/** Stand-in for a slug this client has no specific treatment for. */\nexport const GenericActivityIcon: LucideIcon = User;\n\n/** Icon for the empty feed, exported so widget and screen match. */\nexport const EmptyActivityIcon: LucideIcon = AudioWaveform;\n\n/**\n * Icon for a raw server slug. Unrecognized slugs get the generic icon rather\n * than being hidden — the server's slug set grows independently of this client.\n */\nexport function getActivityIcon(slug: string): LucideIcon {\n return isActivitySlug(slug) ? ACTIVITY_ICON_MAP[slug] : GenericActivityIcon;\n}\n\n/** Local-time YYYY-MM-DD, the key activities are grouped into days by. */\nexport function getActivityDateKey(timestamp: string): string {\n const date = new Date(timestamp);\n if (!Number.isFinite(date.getTime())) return timestamp;\n\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, \"0\");\n const day = String(date.getDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n\n/** Labels only the caller can supply, because only it has a translator. */\n/**\n * How to label and style whoever an activity is about.\n *\n * Composed here rather than read off `userName`, which the server writes in\n * English. `muted` is what makes an anonymous visitor read differently from an\n * identified contact, the distinction mobile draws with a different avatar.\n */\nexport function describeActor(\n actor: ActivityActor,\n t: ShellT,\n): { label: string; muted: boolean } {\n switch (actor.type) {\n case \"contact\":\n return {\n label: actor.name?.trim() || t(\"activity_actor_contact\"),\n muted: false,\n };\n case \"visitor\": {\n // City and state go in as separate values rather than a joined string. The\n // separator and their order are part of the sentence, and both vary: ja and\n // zh use a full-width comma with no space, ar and he need RTL ordering, and\n // several locales put the region first. Joining in TS gives a translator no\n // handle on any of it.\n const city = actor.city?.trim();\n const state = actor.state?.trim();\n\n if (city && state) {\n return {\n label: t(\"activity_actor_visitor_from_city_state\", { city, state }),\n muted: true,\n };\n }\n\n const single = city || state;\n\n return {\n label: single\n ? t(\"activity_actor_visitor_from_city\", { city: single })\n : t(\"activity_actor_visitor\"),\n muted: true,\n };\n }\n case \"unknown\":\n return { label: t(\"activity_actor_unknown\"), muted: true };\n default:\n // ActivityActor is closed and local, so a variant added without a case here\n // should fail the typecheck rather than quietly read as unknown.\n return describeUnhandledActor(actor, t);\n }\n}\n\nfunction describeUnhandledActor(\n actor: never,\n t: ShellT,\n): { label: string; muted: boolean } {\n void actor;\n\n return { label: t(\"activity_actor_unknown\"), muted: true };\n}\n\nexport type ActivityDayLabels = {\n today: string;\n /**\n * Omit to head yesterday with its date instead.\n *\n * The activity screen does, because it reserves Yesterday for the timeframe\n * filter: seeing it as a group heading as well made one word mean two things.\n * The widget still passes it.\n */\n yesterday?: string | undefined;\n};\n\n/**\n * Heading for a day group: the caller's label for today, the same for yesterday\n * when it supplies one, and otherwise a localized month and day.\n */\nexport function formatActivityDateHeader(\n timestamp: string,\n nowMs: number,\n labels: ActivityDayLabels,\n locale = \"en-US\",\n): string {\n const date = new Date(timestamp);\n if (!Number.isFinite(date.getTime())) return timestamp;\n\n const now = new Date(nowMs);\n const yesterday = new Date(now);\n yesterday.setDate(now.getDate() - 1);\n\n const dateKey = getActivityDateKey(timestamp);\n if (dateKey === getActivityDateKey(now.toISOString())) return labels.today;\n if (\n labels.yesterday &&\n dateKey === getActivityDateKey(yesterday.toISOString())\n ) {\n return labels.yesterday;\n }\n\n return date.toLocaleDateString(locale, { month: \"long\", day: \"numeric\" });\n}\n\n/** Clock time for an activity row, e.g. \"4:12 PM\". */\nexport function formatActivityTime(\n timestamp: string,\n locale = \"en-US\",\n): string {\n return new Date(timestamp).toLocaleTimeString(locale, {\n hour: \"numeric\",\n minute: \"2-digit\",\n hour12: true,\n });\n}\n\n/**\n * Compact age, e.g. \"now\", \"5m\", \"3h\", \"2d\", then a date past a week.\n *\n * The units are unlocalized single letters; only the widget uses this, and its\n * copy is not translated yet.\n */\nexport function formatActivityRelativeTime(\n timestamp: string,\n now: number,\n locale = \"en-US\",\n): string {\n const then = new Date(timestamp).getTime();\n const diffSec = Math.max(0, Math.round((now - then) / 1000));\n\n if (diffSec < 60) return \"now\";\n const diffMin = Math.floor(diffSec / 60);\n if (diffMin < 60) return `${diffMin}m`;\n const diffHr = Math.floor(diffMin / 60);\n if (diffHr < 24) return `${diffHr}h`;\n const diffDay = Math.floor(diffHr / 24);\n if (diffDay < 7) return `${diffDay}d`;\n\n return new Date(timestamp).toLocaleDateString(locale, {\n month: \"short\",\n day: \"numeric\",\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"activity-presentation-OSfAUOoY.cjs","names":["ShoppingCart","MessageCircle","Play","UserPlus","Eye","Calendar","Star","SmilePlus","CheckSquare","Bell","Trophy","User","AudioWaveform"],"sources":["../../core/src/activity-slug.ts","../../core/src/activity-presentation.ts"],"sourcesContent":["import type { ActivitySlug } from \"./widgets-api-types\";\n\n// Typing as `Record<ActivitySlug, true>` makes TS reject this object when a new\n// value is added to `ActivitySlug` and not registered here, so the guard stays\n// exhaustive without a separate test.\nconst KNOWN_ACTIVITY_SLUGS: Record<ActivitySlug, true> = {\n abandoned_cart: true,\n announcements: true,\n cart_item_removed: true,\n cart_items_added: true,\n comment_like: true,\n comment_reply: true,\n direct_message: true,\n emoji_reaction: true,\n enrollment: true,\n external_order: true,\n fantasy_point: true,\n imported: true,\n media_views: true,\n new_lead: true,\n order_placed: true,\n page_views: true,\n page_views_contact: true,\n tasks: true,\n upcoming_event: true,\n video: true,\n video_complete: true,\n video_complete_contact: true,\n video_contact: true,\n video_started: true,\n message_received: true,\n message_sent: true,\n new_cart_items_added: true,\n smart_link_clicked: true,\n review_left: true,\n};\n\n/**\n * Narrows a server slug to one this client knows how to present.\n *\n * This is a presentation lookup, never a filter: the server's slug set grows\n * independently, and an unrecognized slug still renders with generic treatment.\n */\nexport function isActivitySlug(slug: string): slug is ActivitySlug {\n return Object.hasOwn(KNOWN_ACTIVITY_SLUGS, slug);\n}\n","import {\n AudioWaveform,\n Bell,\n Calendar,\n CheckSquare,\n Eye,\n MessageCircle,\n Play,\n ShoppingCart,\n SmilePlus,\n Star,\n Trophy,\n User,\n UserPlus,\n type LucideIcon,\n} from \"lucide-react\";\nimport type { TranslationApi } from \"@fluid-app/i18n/translation-api\";\nimport { isActivitySlug } from \"./activity-slug\";\nimport type { ShellDict } from \"./shell-translation-dictionary\";\nimport type { ActivityActor, ActivitySlug } from \"./widgets-api-types\";\n\n/**\n * The shell translator, passed in rather than read from context, so the pieces\n * below a screen stay context-free and the widget can share them.\n */\nexport type ShellT = TranslationApi<ShellDict>[\"t\"];\n\n/**\n * Presentation shared by the Recent Activity widget and the Activity screen.\n * Both surfaces must agree — the widget's count is a promise about what the\n * screen will show — so the icon map and date bucketing live here rather than\n * being duplicated per renderer.\n */\n\nconst ACTIVITY_ICON_MAP: Record<ActivitySlug, LucideIcon> = {\n order_placed: ShoppingCart,\n external_order: ShoppingCart,\n abandoned_cart: ShoppingCart,\n cart_items_added: ShoppingCart,\n cart_item_removed: ShoppingCart,\n new_cart_items_added: ShoppingCart,\n direct_message: MessageCircle,\n comment_reply: MessageCircle,\n comment_like: MessageCircle,\n message_received: MessageCircle,\n message_sent: MessageCircle,\n video: Play,\n video_started: Play,\n video_complete: Play,\n video_contact: Play,\n video_complete_contact: Play,\n media_views: Play,\n new_lead: UserPlus,\n enrollment: UserPlus,\n imported: UserPlus,\n page_views_contact: UserPlus,\n smart_link_clicked: UserPlus,\n page_views: Eye,\n upcoming_event: Calendar,\n review_left: Star,\n emoji_reaction: SmilePlus,\n tasks: CheckSquare,\n announcements: Bell,\n fantasy_point: Trophy,\n};\n\n/**\n * Slugs grouped into the categories a rep filters by. Reps think in \"orders\"\n * and \"videos\", not in the ~30 slugs the platform writes, and each category\n * expands to a server-side slug filter so the results stay honest across\n * pagination.\n *\n * A slug missing from every category is still shown; it just cannot be\n * filtered to on its own.\n */\nexport const ACTIVITY_CATEGORIES = {\n orders: [\"order_placed\", \"external_order\"],\n carts: [\n \"abandoned_cart\",\n \"cart_items_added\",\n \"new_cart_items_added\",\n \"cart_item_removed\",\n ],\n messages: [\n \"direct_message\",\n \"message_received\",\n \"message_sent\",\n \"comment_reply\",\n \"comment_like\",\n ],\n videos: [\n \"video\",\n \"video_started\",\n \"video_complete\",\n \"video_contact\",\n \"video_complete_contact\",\n \"media_views\",\n ],\n pageViews: [\"page_views\", \"page_views_contact\", \"smart_link_clicked\"],\n leads: [\"new_lead\", \"enrollment\", \"imported\"],\n reviews: [\"review_left\"],\n reactions: [\"emoji_reaction\"],\n // Every category has to be able to return a row, since a filter is just another\n // view of the same types the feed renders cards for. Two are absent for that\n // reason: tasks, upcoming_event and announcements are written to notifications\n // rather than activities, and fantasy_point has no writer at all. Offering\n // either would have been a filter that always came back empty.\n} as const satisfies Record<string, readonly ActivitySlug[]>;\n\nexport type ActivityCategory = keyof typeof ACTIVITY_CATEGORIES;\n\nexport const ACTIVITY_CATEGORY_KEYS = Object.keys(\n ACTIVITY_CATEGORIES,\n) as ActivityCategory[];\n\n/** Flattens selected categories into the slug list the API filter takes. */\nexport function slugsForCategories(\n categories: readonly ActivityCategory[],\n): string[] {\n return categories.flatMap((category) => [...ACTIVITY_CATEGORIES[category]]);\n}\n\n/** Stand-in for a slug this client has no specific treatment for. */\nexport const GenericActivityIcon: LucideIcon = User;\n\n/** Icon for the empty feed, exported so widget and screen match. */\nexport const EmptyActivityIcon: LucideIcon = AudioWaveform;\n\n/**\n * Icon for a raw server slug. Unrecognized slugs get the generic icon rather\n * than being hidden — the server's slug set grows independently of this client.\n */\nexport function getActivityIcon(slug: string): LucideIcon {\n return isActivitySlug(slug) ? ACTIVITY_ICON_MAP[slug] : GenericActivityIcon;\n}\n\n/** Local-time YYYY-MM-DD, the key activities are grouped into days by. */\nexport function getActivityDateKey(timestamp: string): string {\n const date = new Date(timestamp);\n if (!Number.isFinite(date.getTime())) return timestamp;\n\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, \"0\");\n const day = String(date.getDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n\n/** Labels only the caller can supply, because only it has a translator. */\n/**\n * How to label and style whoever an activity is about.\n *\n * Composed here rather than read off `userName`, which the server writes in\n * English. `muted` is what makes an anonymous visitor read differently from an\n * identified contact, the distinction mobile draws with a different avatar.\n */\nexport function describeActor(\n actor: ActivityActor,\n t: ShellT,\n): { label: string; muted: boolean } {\n switch (actor.type) {\n case \"contact\":\n return {\n label: actor.name?.trim() || t(\"activity_actor_contact\"),\n muted: false,\n };\n case \"visitor\": {\n // City and state go in as separate values rather than a joined string. The\n // separator and their order are part of the sentence, and both vary: ja and\n // zh use a full-width comma with no space, ar and he need RTL ordering, and\n // several locales put the region first. Joining in TS gives a translator no\n // handle on any of it.\n const city = actor.city?.trim();\n const state = actor.state?.trim();\n\n if (city && state) {\n return {\n label: t(\"activity_actor_visitor_from_city_state\", { city, state }),\n muted: true,\n };\n }\n\n const single = city || state;\n\n return {\n label: single\n ? t(\"activity_actor_visitor_from_city\", { city: single })\n : t(\"activity_actor_visitor\"),\n muted: true,\n };\n }\n case \"unknown\":\n return { label: t(\"activity_actor_unknown\"), muted: true };\n default:\n // ActivityActor is closed and local, so a variant added without a case here\n // should fail the typecheck rather than quietly read as unknown.\n return describeUnhandledActor(actor, t);\n }\n}\n\n/**\n * First and last initial of whoever a row is about, empty when there is no person\n * to show. First and last rather than the first two, matching the contacts list\n * and the profile menu, so one contact does not get two different monograms.\n *\n * Reads the name off the actor rather than a rendered label: a contact the server\n * sent no name labels as the translated word \"Contact\", and its first letter\n * would read as a real person's initial in every one of the 34 locales.\n */\nexport function initialsForActor(actor: ActivityActor): string {\n if (actor.type !== \"contact\") return \"\";\n\n const name = actor.name?.trim() ?? \"\";\n\n // A contact with no name falls back to their email or phone, and neither makes a\n // monogram. An address is the case worth naming: \"jane@example.com\" starts with a\n // letter, so it would otherwise pass for a first name.\n if (name === \"\" || name.includes(\"@\")) return \"\";\n\n // Spread rather than charAt, so a name starting outside the BMP keeps its first\n // character whole instead of half a surrogate pair. Letters only, so a phone\n // number does not put a plus sign in the circle where a face goes.\n const letters = name\n .split(/\\s+/)\n .map((word) => [...word][0] ?? \"\")\n .filter((char) => /\\p{L}/u.test(char));\n\n const first = letters[0];\n if (first === undefined) return \"\";\n\n const last = letters.length > 1 ? letters[letters.length - 1] : \"\";\n\n return (first + last).toUpperCase();\n}\n\nfunction describeUnhandledActor(\n actor: never,\n t: ShellT,\n): { label: string; muted: boolean } {\n void actor;\n\n return { label: t(\"activity_actor_unknown\"), muted: true };\n}\n\nexport type ActivityDayLabels = {\n today: string;\n /**\n * Omit to head yesterday with its date instead.\n *\n * The activity screen does, because it reserves Yesterday for the timeframe\n * filter: seeing it as a group heading as well made one word mean two things.\n * The widget still passes it.\n */\n yesterday?: string | undefined;\n};\n\n/**\n * Heading for a day group: the caller's label for today, the same for yesterday\n * when it supplies one, and otherwise a localized month and day.\n */\nexport function formatActivityDateHeader(\n timestamp: string,\n nowMs: number,\n labels: ActivityDayLabels,\n locale = \"en-US\",\n): string {\n const date = new Date(timestamp);\n if (!Number.isFinite(date.getTime())) return timestamp;\n\n const now = new Date(nowMs);\n const yesterday = new Date(now);\n yesterday.setDate(now.getDate() - 1);\n\n const dateKey = getActivityDateKey(timestamp);\n if (dateKey === getActivityDateKey(now.toISOString())) return labels.today;\n if (\n labels.yesterday &&\n dateKey === getActivityDateKey(yesterday.toISOString())\n ) {\n return labels.yesterday;\n }\n\n return date.toLocaleDateString(locale, { month: \"long\", day: \"numeric\" });\n}\n\n/** Clock time for an activity row, e.g. \"4:12 PM\". */\nexport function formatActivityTime(\n timestamp: string,\n locale = \"en-US\",\n): string {\n return new Date(timestamp).toLocaleTimeString(locale, {\n hour: \"numeric\",\n minute: \"2-digit\",\n hour12: true,\n });\n}\n\n/**\n * Compact age, e.g. \"now\", \"5m\", \"3h\", \"2d\", then a date past a week.\n *\n * The units are unlocalized single letters; only the widget uses this, and its\n * copy is not translated yet.\n */\nexport function formatActivityRelativeTime(\n timestamp: string,\n now: number,\n locale = \"en-US\",\n): string {\n const then = new Date(timestamp).getTime();\n const diffSec = Math.max(0, Math.round((now - then) / 1000));\n\n if (diffSec < 60) return \"now\";\n const diffMin = Math.floor(diffSec / 60);\n if (diffMin < 60) return `${diffMin}m`;\n const diffHr = Math.floor(diffMin / 60);\n if (diffHr < 24) return `${diffHr}h`;\n const diffDay = Math.floor(diffHr / 24);\n if (diffDay < 7) return `${diffDay}d`;\n\n return new Date(timestamp).toLocaleDateString(locale, {\n month: \"short\",\n day: \"numeric\",\n });\n}\n"],"mappings":"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"}
|
|
@@ -32,7 +32,7 @@ const require_MySiteWidget = require("./MySiteWidget-B96f7y26.cjs");
|
|
|
32
32
|
const require_NestedWidget = require("./NestedWidget-DrKZhr9R.cjs");
|
|
33
33
|
const require_QuoteWidget = require("./QuoteWidget-DvBhcY9_.cjs");
|
|
34
34
|
const require_QuickShareWidget = require("./QuickShareWidget-CFv7r5k8.cjs");
|
|
35
|
-
const require_RecentActivityWidget = require("./RecentActivityWidget-
|
|
35
|
+
const require_RecentActivityWidget = require("./RecentActivityWidget-BQXZR4_G.cjs");
|
|
36
36
|
const require_SeparatorWidget = require("./SeparatorWidget-CMWb2k-L.cjs");
|
|
37
37
|
const require_SpacerWidget = require("./SpacerWidget-C76DS5vR.cjs");
|
|
38
38
|
const require_TableWidget = require("./TableWidget-Dx7juyPW.cjs");
|
|
@@ -2130,7 +2130,7 @@ const widgetPropertySchemas = {
|
|
|
2130
2130
|
QuoteWidget: () => Promise.resolve().then(() => require("./QuoteWidget-DvBhcY9_.cjs")).then((n) => n.QuoteWidget_exports).then((m) => m.quoteWidgetPropertySchema),
|
|
2131
2131
|
QuickLinksWidget: () => Promise.resolve().then(() => require("./QuickLinksWidget-Cp96gGJB.cjs")).then((m) => m.quickLinksWidgetPropertySchema),
|
|
2132
2132
|
QuickShareWidget: () => Promise.resolve().then(() => require("./QuickShareWidget-C0fQmQf7.cjs")).then((m) => m.quickShareWidgetPropertySchema),
|
|
2133
|
-
RecentActivityWidget: () => Promise.resolve().then(() => require("./RecentActivityWidget-
|
|
2133
|
+
RecentActivityWidget: () => Promise.resolve().then(() => require("./RecentActivityWidget-CSZeB4Pp.cjs")).then((m) => m.recentActivityWidgetPropertySchema),
|
|
2134
2134
|
SeparatorWidget: () => Promise.resolve().then(() => require("./SeparatorWidget-CMWb2k-L.cjs")).then((n) => n.SeparatorWidget_exports).then((m) => m.separatorWidgetPropertySchema),
|
|
2135
2135
|
ShopWidget: () => Promise.resolve().then(() => require("./ShopWidget-BSBboUU5.cjs")).then((m) => m.shopWidgetPropertySchema),
|
|
2136
2136
|
SpacerWidget: () => Promise.resolve().then(() => require("./SpacerWidget-C76DS5vR.cjs")).then((n) => n.SpacerWidget_exports).then((m) => m.spacerWidgetPropertySchema),
|
|
@@ -25435,4 +25435,4 @@ Object.defineProperty(exports, "wrapRemoteDomRoot", {
|
|
|
25435
25435
|
}
|
|
25436
25436
|
});
|
|
25437
25437
|
|
|
25438
|
-
//# sourceMappingURL=build-widget-registry-
|
|
25438
|
+
//# sourceMappingURL=build-widget-registry-BQLi4JqV.cjs.map
|