@flamingo-stack/openframe-frontend-core 0.0.222 → 0.0.223

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 (183) hide show
  1. package/dist/{chunk-AIKPR6BE.js → chunk-4VHWSAQR.js} +6069 -5235
  2. package/dist/chunk-4VHWSAQR.js.map +1 -0
  3. package/dist/{chunk-GMNE7P7V.cjs → chunk-4WBVMIKQ.cjs} +26 -26
  4. package/dist/{chunk-GMNE7P7V.cjs.map → chunk-4WBVMIKQ.cjs.map} +1 -1
  5. package/dist/{chunk-QYRV6MKX.js → chunk-53EA4QPZ.js} +13 -7
  6. package/dist/chunk-53EA4QPZ.js.map +1 -0
  7. package/dist/{chunk-4JOWZLEL.cjs → chunk-7TKYFZ3L.cjs} +7 -7
  8. package/dist/{chunk-4JOWZLEL.cjs.map → chunk-7TKYFZ3L.cjs.map} +1 -1
  9. package/dist/{chunk-IZA5WF44.cjs → chunk-ABBU7QOM.cjs} +12 -12
  10. package/dist/{chunk-IZA5WF44.cjs.map → chunk-ABBU7QOM.cjs.map} +1 -1
  11. package/dist/{chunk-KM4KK2A6.cjs → chunk-ENDUMC7Z.cjs} +2558 -1724
  12. package/dist/chunk-ENDUMC7Z.cjs.map +1 -0
  13. package/dist/{chunk-CCSEXVI4.js → chunk-GYYGA24C.js} +2 -2
  14. package/dist/{chunk-RMOHKAHA.js → chunk-ISWWP2BH.js} +3 -3
  15. package/dist/{chunk-QNYH3WUU.cjs → chunk-JUWPBMQM.cjs} +13 -7
  16. package/dist/chunk-JUWPBMQM.cjs.map +1 -0
  17. package/dist/{chunk-A2SFHYVF.cjs → chunk-KXLTWANQ.cjs} +10 -10
  18. package/dist/{chunk-A2SFHYVF.cjs.map → chunk-KXLTWANQ.cjs.map} +1 -1
  19. package/dist/{chunk-ND24XSVT.js → chunk-TA6Z2AKD.js} +3 -3
  20. package/dist/chunk-TA6Z2AKD.js.map +1 -0
  21. package/dist/{chunk-HRIAZQ5D.js → chunk-WOKB6RJ7.js} +3 -3
  22. package/dist/components/chat/chat-archive-page.d.ts.map +1 -1
  23. package/dist/components/chat/chat-composer.d.ts.map +1 -1
  24. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  25. package/dist/components/chat/chat-message-skeleton.d.ts +19 -4
  26. package/dist/components/chat/chat-message-skeleton.d.ts.map +1 -1
  27. package/dist/components/chat/chat-panel-header-mobile.d.ts +24 -0
  28. package/dist/components/chat/chat-panel-header-mobile.d.ts.map +1 -0
  29. package/dist/components/chat/chat-panel-header.d.ts +6 -0
  30. package/dist/components/chat/chat-panel-header.d.ts.map +1 -1
  31. package/dist/components/chat/chat-quick-action-row.d.ts +31 -0
  32. package/dist/components/chat/chat-quick-action-row.d.ts.map +1 -0
  33. package/dist/components/chat/embeddable-chat.d.ts +27 -0
  34. package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
  35. package/dist/components/chat/entity-cards/dispatch.d.ts +14 -0
  36. package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
  37. package/dist/components/chat/entity-cards/github-activity-card.d.ts +10 -1
  38. package/dist/components/chat/entity-cards/github-activity-card.d.ts.map +1 -1
  39. package/dist/components/chat/guide-welcome.d.ts.map +1 -1
  40. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +9 -0
  41. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  42. package/dist/components/chat/hooks/use-slash-commands.d.ts +26 -0
  43. package/dist/components/chat/hooks/use-slash-commands.d.ts.map +1 -1
  44. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +12 -1
  45. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
  46. package/dist/components/chat/hooks/use-unified-chat.d.ts +12 -0
  47. package/dist/components/chat/hooks/use-unified-chat.d.ts.map +1 -1
  48. package/dist/components/chat/index.cjs +9 -3
  49. package/dist/components/chat/index.cjs.map +1 -1
  50. package/dist/components/chat/index.js +8 -2
  51. package/dist/components/chat/mingo-chat-history.d.ts +10 -0
  52. package/dist/components/chat/mingo-chat-history.d.ts.map +1 -1
  53. package/dist/components/chat/mingo-info-card.d.ts +63 -0
  54. package/dist/components/chat/mingo-info-card.d.ts.map +1 -0
  55. package/dist/components/chat/mingo-welcome.d.ts +14 -1
  56. package/dist/components/chat/mingo-welcome.d.ts.map +1 -1
  57. package/dist/components/chat/types/chat.types.d.ts +6 -0
  58. package/dist/components/chat/types/chat.types.d.ts.map +1 -1
  59. package/dist/components/chat/types/unified-chat-state.types.d.ts +30 -0
  60. package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
  61. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  62. package/dist/components/contact/index.cjs +4 -4
  63. package/dist/components/contact/index.js +3 -3
  64. package/dist/components/features/index.cjs +3 -3
  65. package/dist/components/features/index.js +2 -2
  66. package/dist/components/index.cjs +110 -104
  67. package/dist/components/index.cjs.map +1 -1
  68. package/dist/components/index.js +36 -30
  69. package/dist/components/index.js.map +1 -1
  70. package/dist/components/navigation/app-layout-drawer.d.ts +7 -0
  71. package/dist/components/navigation/app-layout-drawer.d.ts.map +1 -1
  72. package/dist/components/navigation/index.cjs +3 -3
  73. package/dist/components/navigation/index.js +2 -2
  74. package/dist/components/onboarding-guides/index.cjs +26 -26
  75. package/dist/components/onboarding-guides/index.js +4 -4
  76. package/dist/components/tickets/index.cjs +67 -67
  77. package/dist/components/tickets/index.js +5 -5
  78. package/dist/components/ui/actions-menu.d.ts +9 -0
  79. package/dist/components/ui/actions-menu.d.ts.map +1 -1
  80. package/dist/components/ui/dropdown-button.d.ts +2 -2
  81. package/dist/components/ui/dropdown-button.d.ts.map +1 -1
  82. package/dist/components/ui/index.cjs +9 -3
  83. package/dist/components/ui/index.cjs.map +1 -1
  84. package/dist/components/ui/index.js +8 -2
  85. package/dist/components/ui/modal-v2.d.ts.map +1 -1
  86. package/dist/components/ui/more-actions-menu.d.ts +13 -0
  87. package/dist/components/ui/more-actions-menu.d.ts.map +1 -1
  88. package/dist/components/ui/square-avatar.d.ts +4 -0
  89. package/dist/components/ui/square-avatar.d.ts.map +1 -1
  90. package/dist/components/ui/textarea.d.ts.map +1 -1
  91. package/dist/hooks/index.cjs +2 -2
  92. package/dist/hooks/index.js +1 -1
  93. package/dist/hooks/ui/use-auto-limit-tags.d.ts +7 -1
  94. package/dist/hooks/ui/use-auto-limit-tags.d.ts.map +1 -1
  95. package/dist/index.cjs +9 -3
  96. package/dist/index.cjs.map +1 -1
  97. package/dist/index.js +8 -2
  98. package/package.json +1 -1
  99. package/src/components/chat/chat-archive-page.tsx +32 -7
  100. package/src/components/chat/chat-composer.tsx +6 -2
  101. package/src/components/chat/chat-message-enhanced.tsx +24 -7
  102. package/src/components/chat/chat-message-list.tsx +1 -1
  103. package/src/components/chat/chat-message-skeleton.tsx +60 -60
  104. package/src/components/chat/chat-panel-header-mobile.tsx +94 -0
  105. package/src/components/chat/chat-panel-header.tsx +78 -50
  106. package/src/components/chat/chat-quick-action-row.tsx +146 -0
  107. package/src/components/chat/embeddable-chat.tsx +140 -45
  108. package/src/components/chat/entity-cards/dispatch.tsx +770 -232
  109. package/src/components/chat/entity-cards/github-activity-card.tsx +9 -3
  110. package/src/components/chat/guide-welcome.tsx +11 -8
  111. package/src/components/chat/hooks/use-nats-chat-adapter.ts +80 -7
  112. package/src/components/chat/hooks/use-slash-commands.ts +39 -0
  113. package/src/components/chat/hooks/use-sse-chat-adapter.ts +49 -45
  114. package/src/components/chat/hooks/use-unified-chat.ts +31 -8
  115. package/src/components/chat/mingo-chat-history.tsx +118 -36
  116. package/src/components/chat/mingo-info-card.tsx +214 -0
  117. package/src/components/chat/mingo-welcome.tsx +89 -36
  118. package/src/components/chat/types/chat.types.ts +4 -0
  119. package/src/components/chat/types/unified-chat-state.types.ts +35 -0
  120. package/src/components/chat/utils/process-historical-messages.ts +8 -0
  121. package/src/components/content-loading-container.tsx +1 -1
  122. package/src/components/empty-state.tsx +3 -3
  123. package/src/components/faq-accordion.tsx +2 -2
  124. package/src/components/features/changelog-manager.tsx +2 -2
  125. package/src/components/features/highlight-config-section.tsx +2 -2
  126. package/src/components/features/og-editor-preview.tsx +3 -3
  127. package/src/components/features/release-media-manager.tsx +2 -2
  128. package/src/components/loading/card-skeleton.tsx +3 -3
  129. package/src/components/loading/device-card-skeleton.tsx +1 -1
  130. package/src/components/loading/margin-report-skeleton.tsx +2 -2
  131. package/src/components/loading/organization-card-skeleton.tsx +1 -1
  132. package/src/components/loading/organization-icon-skeleton.tsx +1 -1
  133. package/src/components/loading/page-layout-skeleton.tsx +9 -9
  134. package/src/components/loading/users-grid-skeleton.tsx +2 -2
  135. package/src/components/logs-list.tsx +1 -1
  136. package/src/components/media-carousel.tsx +1 -1
  137. package/src/components/navigation/app-layout-drawer.tsx +70 -6
  138. package/src/components/open-source-features.tsx +2 -2
  139. package/src/components/ui/actions-menu.tsx +36 -13
  140. package/src/components/ui/allowed-domains-input.tsx +3 -3
  141. package/src/components/ui/cursor-pagination.tsx +1 -1
  142. package/src/components/ui/dropdown-button.tsx +6 -6
  143. package/src/components/ui/info-row.tsx +2 -2
  144. package/src/components/ui/modal-v2.tsx +8 -1
  145. package/src/components/ui/more-actions-menu.tsx +13 -0
  146. package/src/components/ui/square-avatar.tsx +6 -1
  147. package/src/components/ui/tags-input.tsx +1 -1
  148. package/src/components/ui/textarea.tsx +18 -4
  149. package/src/components/vendor-icon.tsx +1 -1
  150. package/src/components/vendor-tag.tsx +1 -1
  151. package/src/components/why-it-matters.tsx +1 -1
  152. package/src/hooks/ui/use-auto-limit-tags.ts +22 -8
  153. package/src/stories/ActionsMenu.stories.tsx +256 -0
  154. package/src/stories/AppLayoutSidebar.stories.tsx +6 -0
  155. package/src/stories/Board.stories.tsx +1 -1
  156. package/src/stories/ChatMingoInfoCards.stories.tsx +748 -0
  157. package/src/stories/ChatTicketList.stories.tsx +1 -1
  158. package/src/stories/DataTable.stories.tsx +17 -13
  159. package/src/stories/EmbeddableChat.stories.tsx +233 -1
  160. package/src/stories/MingoInfoCard.stories.tsx +193 -0
  161. package/src/stories/MingoOnboardingCard.stories.tsx +1 -1
  162. package/src/stories/Modal.stories.tsx +1 -0
  163. package/src/stories/NotificationDrawer.stories.tsx +14 -3
  164. package/src/stories/PageActions.stories.tsx +4 -4
  165. package/src/stories/PageContainer.stories.tsx +5 -5
  166. package/src/stories/PageLayout.stories.tsx +1 -1
  167. package/src/stories/TabSelector.stories.tsx +2 -2
  168. package/src/stories/Tag.stories.tsx +10 -5
  169. package/src/stories/TicketInfoSection.stories.tsx +23 -11
  170. package/src/stories/Toaster.stories.tsx +5 -5
  171. package/src/stories/__fixtures__/chat-card-decorator.tsx +44 -0
  172. package/dist/chunk-AIKPR6BE.js.map +0 -1
  173. package/dist/chunk-KM4KK2A6.cjs.map +0 -1
  174. package/dist/chunk-ND24XSVT.js.map +0 -1
  175. package/dist/chunk-QNYH3WUU.cjs.map +0 -1
  176. package/dist/chunk-QYRV6MKX.js.map +0 -1
  177. package/src/stories/.TicketDescriptionViewer.stories.md +0 -39
  178. package/src/stories/.TicketKnowledgeBaseList.stories.md +0 -36
  179. package/src/stories/TicketDescriptionViewer.stories.tsx +0 -70
  180. package/src/stories/TicketKnowledgeBaseList.stories.tsx +0 -51
  181. /package/dist/{chunk-CCSEXVI4.js.map → chunk-GYYGA24C.js.map} +0 -0
  182. /package/dist/{chunk-RMOHKAHA.js.map → chunk-ISWWP2BH.js.map} +0 -0
  183. /package/dist/{chunk-HRIAZQ5D.js.map → chunk-WOKB6RJ7.js.map} +0 -0
@@ -39,7 +39,6 @@ import { resolveSourceRowCTA, resolveSourceIcon, sourceRowCtxFromRuntime } from
39
39
  import { resolveHrefForRuntime } from '../utils/chat-nav-resolution'
40
40
  import {
41
41
  computeIsNewTab,
42
- newTabAnchorAttrs,
43
42
  buildAnchorProps,
44
43
  } from '../utils/nav-anchor-props'
45
44
  import { safeHref } from '../utils/compact-card-classes'
@@ -47,52 +46,68 @@ import { useChatPanel } from '../chat-panel-context'
47
46
  import { getSourceLabel } from '../utils/source-icons'
48
47
  import { SourceActionButton } from '../source-action-button'
49
48
  import { NavLinkAnchorViaRuntime } from '../nav-link-anchor-via-runtime'
50
- import { DEFAULT_PROGRAM_CONFIGS } from './program-card-defaults'
51
49
  import { defaultBuildProductReleaseCardProps } from './product-release-card-defaults'
52
50
  import { ChatVideoEntityCard } from './chat-video-entity-card'
53
51
  import { BlockCard } from './block-card'
54
- import { BlogCard, BlogCardSkeleton } from './blog-card'
55
- import { CaseStudyCard, CaseStudyCardSkeleton } from './case-study-card'
52
+ import { BlogCardSkeleton } from './blog-card'
53
+ import { CaseStudyCardSkeleton } from './case-study-card'
54
+ import { CustomerInterviewCardSkeleton } from './customer-interview-card'
56
55
  import {
57
- CustomerInterviewCard,
58
- CustomerInterviewCardSkeleton,
59
- } from './customer-interview-card'
60
- import {
61
- ProductReleaseCard,
62
56
  ProductReleaseCardSkeleton,
63
57
  type ProductReleaseCardProps,
64
58
  } from './product-release-card'
59
+ import { ProgramCardSkeleton } from './program-card'
60
+ import { InvestorUpdateCardSkeleton } from './investor-update-card'
61
+ import { OnboardingGuideCardSkeleton } from './onboarding-guide-card'
62
+ import { CampaignCardAdminSkeleton } from './campaign-card-admin'
63
+ import { RoadmapCardSkeleton } from './roadmap-card'
64
+ import { TaskTypeIcon } from './task-type-icon'
65
65
  import {
66
- ProgramCard,
67
- ProgramCardSkeleton,
68
- type ProgramCardProps,
69
- } from './program-card'
70
- import {
71
- InvestorUpdateCard,
72
- InvestorUpdateCardSkeleton,
73
- } from './investor-update-card'
74
- import {
75
- OnboardingGuideCard,
76
- OnboardingGuideCardSkeleton,
77
- } from './onboarding-guide-card'
78
- import {
79
- CampaignCardAdmin,
80
- CampaignCardAdminSkeleton,
81
- } from './campaign-card-admin'
82
- import { RoadmapCard, RoadmapCardSkeleton } from './roadmap-card'
83
- import {
84
- GitHubActivityCard,
85
- type GitHubActivityCardAnchorProps,
66
+ parseGithubTitle,
67
+ formatActivityId,
68
+ kindLabel as githubKindLabel,
69
+ reviewStateLabel,
86
70
  } from './github-activity-card'
87
- import { SlackMessageCard } from './slack-message-card'
88
- import { HubspotTicketCard } from './hubspot-ticket-card'
89
- import { DataRoomDocCard } from './data-room-doc-card'
71
+ import { MingoInfoCard, type MingoInfoCardStatus } from '../mingo-info-card'
72
+ import type { ActionsMenuGroup } from '../../ui/actions-menu'
73
+ import { MingoIcon } from '../../icons'
74
+ import { EyeIcon } from '../../icons-v2-generated/interface/eye-icon'
75
+ import { ArrowRightUpIcon } from '../../icons-v2-generated/arrows/arrow-right-up-icon'
76
+ import { TagIcon } from '../../icons-v2-generated/shopping/tag-icon'
77
+ import { SlackLogoGreyIcon } from '../../icons-v2-generated/brand-logos/slack-logo-grey-icon'
78
+ import { FileContentIcon } from '../../icons-v2-generated/documents/file-content-icon'
79
+ import { ChartBar01VerIcon } from '../../icons-v2-generated/charts/chart-bar-01-ver-icon'
80
+ import { ChartPieIcon } from '../../icons-v2-generated/charts/chart-pie-icon'
81
+ import { MoneyBillDollarIcon } from '../../icons-v2-generated/finance/money-bill-dollar-icon'
82
+ import { BankIcon } from '../../icons-v2-generated/finance/bank-icon'
83
+ import { CoinsExchangeCurrencyIcon } from '../../icons-v2-generated/finance/coins-exchange-currency-icon'
84
+ import { NewspaperIcon } from '../../icons-v2-generated/documents/newspaper-icon'
85
+ import { TrophyIcon } from '../../icons-v2-generated/sport/trophy-icon'
86
+ import { MicrophoneIcon } from '../../icons-v2-generated/household/microphone-icon'
87
+ import { Rocket02Icon } from '../../icons-v2-generated/vehicles-and-delivery/rocket-02-icon'
88
+ import { TruckFastIcon } from '../../icons-v2-generated/vehicles-and-delivery/truck-fast-icon'
89
+ import { PresentationBarIcon } from '../../icons-v2-generated/charts/presentation-bar-icon'
90
+ import { PresentationLineIcon } from '../../icons-v2-generated/charts/presentation-line-icon'
91
+ import { CalendarIcon } from '../../icons-v2-generated/date-and-time/calendar-icon'
92
+ import { CompassIcon } from '../../icons-v2-generated/map-and-travel/compass-icon'
93
+ import { MapIcon } from '../../icons-v2-generated/map-and-travel/map-icon'
94
+ import { CheckSquareIcon } from '../../icons-v2-generated/signs-and-symbols/check-square-icon'
95
+ import { Megaphone01Icon } from '../../icons-v2-generated/shopping/megaphone-01-icon'
96
+ import { getStatusColorScheme } from '../utils/agent-status-message'
97
+ import { formatDateShort } from '../../../utils/date-formatters'
98
+ import { formatInvestorUpdatePeriod } from '../types/entities/investor-update'
99
+ import { CodingCommitIcon } from '../../icons-v2-generated/coding/coding-commit-icon'
100
+ import { CodingPullRequestIcon } from '../../icons-v2-generated/coding/coding-pull-request-icon'
101
+ import { CodeIcon } from '../../icons-v2-generated/coding/code-icon'
90
102
  import {
91
- GenericEntityCard,
92
- type GenericEntityCardAnchorProps,
93
- } from './generic-entity-card'
103
+ formatDateUTC as formatDate,
104
+ formatDurationCompact,
105
+ formatTimeWithTimezone,
106
+ formatDurationFromRange,
107
+ } from '../../../utils/format'
108
+ import type { PrReviewState } from '../types/entities/github-activity'
94
109
  import type { GitHubActivityKind } from '../types/entities/github-activity'
95
- import type { BaseProgramItem, ProgramConfig } from '../types/entities/program-types'
110
+ import type { ProgramConfig } from '../types/entities/program-types'
96
111
 
97
112
  // =============================================================================
98
113
  // Public option / extras shape
@@ -143,6 +158,11 @@ export interface ChatCardRenderOptions {
143
158
  * whatever `ChatCardNavWrap` + `handleChatNavClick` decide at click
144
159
  * time. */
145
160
  isNewTab: boolean
161
+ /** Host-provided "Ask Mingo"/"Display" affordance for this card, resolved
162
+ * once by `ChatCardLoader`. Each wrapper threads it into its "⋯" menu via
163
+ * `cardMenuGroups`. Undefined when the host supplied no `onDiscuss`/
164
+ * `onDisplay` (then the menu shows only "Open in new tab"). */
165
+ discuss?: CardDiscussAction
146
166
  }
147
167
 
148
168
  // =============================================================================
@@ -155,40 +175,157 @@ export interface ChatCardRenderOptions {
155
175
  // close-on-nav gate.
156
176
  // =============================================================================
157
177
 
178
+ /** Leading glyph per GitHub activity kind. */
179
+ function githubKindIcon(kind: GitHubActivityKind) {
180
+ switch (kind) {
181
+ case 'pull_request':
182
+ return <CodingPullRequestIcon size={24} />
183
+ case 'pr_review':
184
+ return <CodeIcon size={24} />
185
+ case 'commit':
186
+ default:
187
+ return <CodingCommitIcon size={24} />
188
+ }
189
+ }
190
+
191
+ /** PR review state → status-pill colour. */
192
+ function reviewStateToVariant(state: PrReviewState): MingoInfoCardStatus['variant'] {
193
+ switch (state) {
194
+ case 'APPROVED':
195
+ return 'success'
196
+ case 'CHANGES_REQUESTED':
197
+ return 'error'
198
+ case 'PENDING':
199
+ return 'warning'
200
+ case 'COMMENTED':
201
+ case 'DISMISSED':
202
+ default:
203
+ return 'grey'
204
+ }
205
+ }
206
+
158
207
  function GitHubChatCard({
159
208
  chatRef,
160
209
  kind,
161
210
  isNewTab,
211
+ discuss,
162
212
  }: {
163
213
  chatRef: ChatRef
164
214
  kind: GitHubActivityKind
165
215
  isNewTab: boolean
216
+ discuss?: CardDiscussAction
166
217
  }) {
167
- const anchorProps: GitHubActivityCardAnchorProps | undefined = buildAnchorProps(
168
- chatRef.url,
169
- isNewTab,
170
- )
218
+ // Reuse the card's own title parser so the display title + review state
219
+ // read identically here and in the (legacy) GitHubActivityCard.
220
+ const { display, reviewState } = parseGithubTitle(chatRef.title, kind)
221
+ const title =
222
+ kind === 'pr_review'
223
+ ? display.replace(/^by\s+/i, '').trim() || 'Reviewer'
224
+ : display
225
+
226
+ // Review state pill for reviews; otherwise the neutral activity-type pill.
227
+ const status: MingoInfoCardStatus =
228
+ kind === 'pr_review' && reviewState
229
+ ? { label: reviewStateLabel(reviewState), variant: reviewStateToVariant(reviewState) }
230
+ : { label: githubKindLabel(kind), variant: 'grey' }
231
+
232
+ // Description: "<id> · <date>" (short SHA / #num, then updated date).
233
+ const idLabel = formatActivityId(chatRef.id, kind)
234
+ const dateText = formatDate(chatRef.date ?? null, { fallback: '', timezone: 'local' })
235
+ const description = [idLabel, dateText].filter(Boolean).join(' · ') || undefined
236
+
171
237
  return (
172
- <GitHubActivityCard
173
- item={{
174
- id: chatRef.id,
175
- title: chatRef.title,
176
- url: chatRef.url ?? null,
177
- dateUpdated: chatRef.date ?? null,
178
- kind,
179
- }}
180
- variant="compact"
181
- anchorProps={anchorProps}
238
+ <MingoInfoCard
239
+ title={title}
240
+ description={description}
241
+ icon={githubKindIcon(kind)}
242
+ status={status}
243
+ anchorProps={buildAnchorProps(chatRef.url, isNewTab)}
244
+ menuGroups={cardMenuGroups(chatRef.url, discuss)}
245
+ menuAriaLabel="Activity actions"
182
246
  />
183
247
  )
184
248
  }
185
249
 
250
+ /** The host-provided "Ask Mingo" / "Display" affordance, resolved once per
251
+ * card in `ChatCardLoader` and threaded through `renderOpts.discuss`. Mirrors
252
+ * the legacy `ChatCardWithDiscuss` source-row button, relocated into the "⋯"
253
+ * menu. */
254
+ export interface CardDiscussAction {
255
+ label: string
256
+ icon: React.ReactNode
257
+ run: () => void
258
+ }
259
+
260
+ /** Overflow-menu groups for a card (Figma `7740:55075`): the optional "Ask
261
+ * Mingo"/"Display" item (when the host supplied a handler) followed by "Open
262
+ * Details" — whose main row navigates to the card (same tab) and whose trailing
263
+ * ↗ side-button opens it in a new tab. Returns `undefined` when neither applies
264
+ * so the "⋯" button is omitted entirely. The href is already absolute
265
+ * (ChatCardLoader pre-resolves it). */
266
+ function cardMenuGroups(
267
+ href: string | null | undefined,
268
+ discuss?: CardDiscussAction,
269
+ /** Per-entity override for the "Open Details" row — e.g. HubSpot tickets use
270
+ * `{ label: 'Open Ticket Details', icon: <HubSpot logo> }` so the row reads
271
+ * in the entity's own terms and mirrors the card's leading icon. Defaults to
272
+ * a generic "Open Details" + eye glyph. */
273
+ openDetails?: { label: string; icon: React.ReactNode },
274
+ ) {
275
+ const items: ActionsMenuGroup['items'] = []
276
+ if (discuss) {
277
+ items.push({
278
+ id: 'discuss',
279
+ label: discuss.label,
280
+ icon: discuss.icon,
281
+ onClick: discuss.run,
282
+ })
283
+ }
284
+ if (href) {
285
+ items.push({
286
+ id: 'open-details',
287
+ label: openDetails?.label ?? 'Open Details',
288
+ icon: openDetails?.icon ?? <EyeIcon size={20} />,
289
+ // Main row → open the entity (same tab, soft Link nav).
290
+ href,
291
+ // Trailing 40px ↗ side-button → open in a new tab.
292
+ iconAction: {
293
+ icon: <ArrowRightUpIcon size={20} />,
294
+ 'aria-label': 'Open in new tab',
295
+ href,
296
+ openInNewTab: true,
297
+ },
298
+ })
299
+ }
300
+ return items.length ? [{ items }] : undefined
301
+ }
302
+
303
+ /** Title-case a status/priority token ("WAITING_ON_US" → "Waiting on us"). */
304
+ function formatStatusToken(token: string | undefined): string | undefined {
305
+ if (!token) return undefined
306
+ const lower = token.toLowerCase().replace(/_/g, ' ')
307
+ return lower.charAt(0).toUpperCase() + lower.slice(1)
308
+ }
309
+
310
+ /** HubSpot ticket status → `MingoInfoCard` status pill (Closed→green,
311
+ * Open→amber, anything else→neutral grey). */
312
+ function hubspotStatusToVariant(
313
+ status: string | undefined,
314
+ ): MingoInfoCardStatus['variant'] {
315
+ const s = (status || '').toUpperCase()
316
+ if (s === 'CLOSED') return 'success'
317
+ if (s === 'OPEN') return 'warning'
318
+ return 'grey'
319
+ }
320
+
186
321
  function HubspotTicketChatCard({
187
322
  chatRef,
188
323
  isNewTab,
324
+ discuss,
189
325
  }: {
190
326
  chatRef: ChatRef
191
327
  isNewTab: boolean
328
+ discuss?: CardDiscussAction
192
329
  }) {
193
330
  const status =
194
331
  typeof chatRef.metadata?.status === 'string' ? (chatRef.metadata.status as string) : undefined
@@ -196,72 +333,69 @@ function HubspotTicketChatCard({
196
333
  typeof chatRef.metadata?.statusLabel === 'string'
197
334
  ? (chatRef.metadata.statusLabel as string)
198
335
  : undefined
199
- const priority =
200
- typeof chatRef.metadata?.priority === 'string'
201
- ? (chatRef.metadata.priority as string)
202
- : undefined
203
- const customerCompany =
204
- typeof chatRef.metadata?.customerCompany === 'string'
205
- ? (chatRef.metadata.customerCompany as string)
206
- : undefined
207
- const customerEmail =
208
- typeof chatRef.metadata?.customerEmail === 'string'
209
- ? (chatRef.metadata.customerEmail as string)
210
- : undefined
336
+ const statusText = statusLabel ?? formatStatusToken(status)
211
337
  return (
212
- <HubspotTicketCard
213
- item={{
214
- id: chatRef.id,
215
- title: chatRef.title,
216
- preview: chatRef.preview,
217
- status,
218
- statusLabel,
219
- priority,
220
- customerCompany,
221
- customerEmail,
222
- url: chatRef.url ?? null,
223
- dateUpdated: chatRef.date ?? null,
224
- }}
225
- variant="compact"
338
+ <MingoInfoCard
339
+ title={chatRef.title}
340
+ description={chatRef.preview ?? undefined}
341
+ icon={<TagIcon size={24} />}
342
+ status={
343
+ statusText
344
+ ? { label: statusText, variant: hubspotStatusToVariant(status) }
345
+ : undefined
346
+ }
226
347
  anchorProps={buildAnchorProps(chatRef.url, isNewTab)}
348
+ menuGroups={cardMenuGroups(chatRef.url, discuss, {
349
+ label: 'Open Ticket Details',
350
+ icon: <TagIcon size={20} />,
351
+ })}
352
+ menuAriaLabel="Ticket actions"
227
353
  />
228
354
  )
229
355
  }
230
356
 
231
- function SlackChatCard({ chatRef, isNewTab }: { chatRef: ChatRef; isNewTab: boolean }) {
357
+ function SlackChatCard({
358
+ chatRef,
359
+ isNewTab,
360
+ discuss,
361
+ }: {
362
+ chatRef: ChatRef
363
+ isNewTab: boolean
364
+ discuss?: CardDiscussAction
365
+ }) {
232
366
  const channelName =
233
367
  typeof chatRef.metadata?.channelName === 'string'
234
- ? (chatRef.metadata.channelName as string)
368
+ ? (chatRef.metadata.channelName as string).trim()
235
369
  : undefined
370
+ // Prefix the channel (with a leading "#") onto the author title, e.g.
371
+ // "Pavlo Shylo · #general".
372
+ const channelPretty = channelName
373
+ ? channelName.startsWith('#')
374
+ ? channelName
375
+ : `#${channelName}`
376
+ : undefined
377
+ const title = channelPretty ? `${chatRef.title} · ${channelPretty}` : chatRef.title
236
378
  return (
237
- <SlackMessageCard
238
- item={{
239
- id: chatRef.id,
240
- title: chatRef.title,
241
- preview: chatRef.preview,
242
- url: chatRef.url ?? null,
243
- dateUpdated: chatRef.date ?? null,
244
- channel: channelName,
245
- }}
246
- variant="compact"
379
+ <MingoInfoCard
380
+ title={title}
381
+ description={chatRef.preview ?? undefined}
382
+ icon={<SlackLogoGreyIcon size={24} />}
247
383
  anchorProps={buildAnchorProps(chatRef.url, isNewTab)}
384
+ menuGroups={cardMenuGroups(chatRef.url, discuss)}
385
+ menuAriaLabel="Message actions"
248
386
  />
249
387
  )
250
388
  }
251
389
 
252
390
  function DataRoomDocChatCard({
253
391
  chatRef,
254
- baseRoute,
255
- chipBasePlatform,
256
392
  isNewTab,
393
+ discuss,
257
394
  }: {
258
395
  chatRef: ChatRef
259
- baseRoute?: string
260
- chipBasePlatform?: string
261
396
  isNewTab: boolean
397
+ discuss?: CardDiscussAction
262
398
  }) {
263
- const path =
264
- typeof chatRef.metadata?.path === 'string' ? (chatRef.metadata.path as string) : undefined
265
399
  // Provenance label. NEVER fall back to "Data room" — that would
266
400
  // falsely label non-data-room content (openframe-docs, etc.) as
267
401
  // private investor material when `sourceRepo` is missing or
@@ -269,58 +403,41 @@ function DataRoomDocChatCard({
269
403
  // can't be misled about sensitivity / scope.
270
404
  const badgeText = chatRef.sourceRepo ? getSourceLabel(chatRef.sourceRepo) : 'Document'
271
405
  return (
272
- <DataRoomDocCard
273
- item={{
274
- id: chatRef.id,
275
- title: chatRef.title,
276
- preview: chatRef.preview ?? undefined,
277
- path,
278
- url: chatRef.url ?? null,
279
- sourceRepo: chatRef.sourceRepo ?? null,
280
- baseRoute,
281
- chipBasePlatform,
282
- }}
283
- badgeText={badgeText}
406
+ <MingoInfoCard
407
+ title={chatRef.title}
408
+ description={chatRef.preview ?? undefined}
409
+ icon={<FileContentIcon size={24} />}
410
+ status={{ label: badgeText, variant: 'grey' }}
284
411
  anchorProps={buildAnchorProps(chatRef.url, isNewTab)}
412
+ menuGroups={cardMenuGroups(chatRef.url, discuss)}
413
+ menuAriaLabel="Document actions"
285
414
  />
286
415
  )
287
416
  }
288
417
 
289
418
  function GenericFinancialChatCard({
290
419
  chatRef,
291
- badge,
292
- scheme,
420
+ icon,
293
421
  isNewTab,
422
+ discuss,
294
423
  }: {
295
424
  chatRef: ChatRef
296
- badge: string
297
- scheme: 'cyan' | 'warning' | 'success' | 'error' | 'default'
425
+ icon: React.ReactNode
298
426
  isNewTab: boolean
427
+ discuss?: CardDiscussAction
299
428
  }) {
300
- const facts = Array.isArray(chatRef.metadata?.facts)
301
- ? (chatRef.metadata!.facts as Array<{ label: string; value: string }>)
302
- : null
303
429
  const subtitle =
304
430
  typeof chatRef.metadata?.subtitle === 'string'
305
431
  ? (chatRef.metadata.subtitle as string)
306
- : null
307
- const anchorProps: GenericEntityCardAnchorProps | undefined = buildAnchorProps(
308
- chatRef.url,
309
- isNewTab,
310
- )
432
+ : undefined
311
433
  return (
312
- <GenericEntityCard
313
- item={{
314
- id: chatRef.id,
315
- title: chatRef.title,
316
- subtitle,
317
- preview: chatRef.preview ?? null,
318
- url: chatRef.url ?? null,
319
- badge: { text: badge, scheme },
320
- facts,
321
- dateUpdated: chatRef.date ?? null,
322
- }}
323
- anchorProps={anchorProps}
434
+ <MingoInfoCard
435
+ title={chatRef.title}
436
+ description={subtitle}
437
+ icon={icon}
438
+ anchorProps={buildAnchorProps(chatRef.url, isNewTab)}
439
+ menuGroups={cardMenuGroups(chatRef.url, discuss)}
440
+ menuAriaLabel="Document actions"
324
441
  />
325
442
  )
326
443
  }
@@ -394,21 +511,29 @@ function ProductReleaseChatCard({
394
511
  chatRef,
395
512
  buildProps,
396
513
  isNewTab,
514
+ discuss,
397
515
  }: {
398
516
  item: any
399
517
  chatRef: ChatRef
400
518
  buildProps: NonNullable<ChatCardDispatchExtras['buildProductReleaseCardProps']>
401
519
  isNewTab: boolean
520
+ discuss?: CardDiscussAction
402
521
  }) {
403
522
  const releaseProps = buildProps(item)
523
+ const status: MingoInfoCardStatus = item?.version
524
+ ? { label: String(item.version), variant: 'grey' }
525
+ : { label: 'Release', variant: 'grey' }
404
526
  return (
405
- <ProductReleaseCard
406
- size="sm"
407
- title={item.title}
408
- summary={item.summary}
409
- version={item.version}
410
- {...releaseProps}
411
- anchorProps={buildAnchorProps(chatRef.url, isNewTab)}
527
+ <EntityMingoCard
528
+ title={item?.title ?? ''}
529
+ description={item?.summary || releaseProps.formattedDate || undefined}
530
+ cover={releaseProps.coverImage || undefined}
531
+ fallbackIcon={<Rocket02Icon size={24} />}
532
+ status={status}
533
+ chatRef={chatRef}
534
+ isNewTab={isNewTab}
535
+ discuss={discuss}
536
+ menuAriaLabel="Release actions"
412
537
  />
413
538
  )
414
539
  }
@@ -422,15 +547,391 @@ function CampaignChatCard({
422
547
  item,
423
548
  chatRef,
424
549
  isNewTab,
550
+ discuss,
551
+ }: {
552
+ item: any
553
+ chatRef: ChatRef
554
+ isNewTab: boolean
555
+ discuss?: CardDiscussAction
556
+ }) {
557
+ // Mirror CampaignCardAdmin's compact meta — "date · N goals · Marketing
558
+ // campaign" — instead of a status pill (the original showed no status badge).
559
+ const goalsCount = Array.isArray(item?.goals) ? item.goals.length : 0
560
+ const meta = [
561
+ item?.start_date ? formatDateShort(item.start_date) : null,
562
+ goalsCount > 0 ? `${goalsCount} goal${goalsCount !== 1 ? 's' : ''}` : null,
563
+ 'Marketing campaign',
564
+ ]
565
+ .filter(Boolean)
566
+ .join(' · ')
567
+ return (
568
+ <EntityMingoCard
569
+ title={item?.name ?? ''}
570
+ description={meta || item?.description || undefined}
571
+ fallbackIcon={<Megaphone01Icon size={24} />}
572
+ chatRef={chatRef}
573
+ isNewTab={isNewTab}
574
+ discuss={discuss}
575
+ menuAriaLabel="Campaign actions"
576
+ />
577
+ )
578
+ }
579
+
580
+ /**
581
+ * Shared `MingoInfoCard` shell for fetch-mode entity cards. `cover` renders as
582
+ * the 40px image when present; otherwise `fallbackIcon` shows. Anchor + menu +
583
+ * alt are wired uniformly so each registry render only computes the per-type
584
+ * title / description / cover / icon / status.
585
+ */
586
+ function EntityMingoCard({
587
+ title,
588
+ description,
589
+ cover,
590
+ fallbackIcon,
591
+ status,
592
+ chatRef,
593
+ isNewTab,
594
+ menuAriaLabel,
595
+ discuss,
596
+ }: {
597
+ title: React.ReactNode
598
+ description?: React.ReactNode
599
+ cover?: string
600
+ fallbackIcon: React.ReactNode
601
+ status?: MingoInfoCardStatus
602
+ chatRef: ChatRef
603
+ isNewTab: boolean
604
+ menuAriaLabel: string
605
+ discuss?: CardDiscussAction
606
+ }) {
607
+ return (
608
+ <MingoInfoCard
609
+ title={title}
610
+ description={description}
611
+ imageSrc={cover}
612
+ imageAlt={typeof title === 'string' ? title : undefined}
613
+ icon={fallbackIcon}
614
+ status={status}
615
+ anchorProps={buildAnchorProps(chatRef.url, isNewTab)}
616
+ menuGroups={cardMenuGroups(chatRef.url, discuss)}
617
+ menuAriaLabel={menuAriaLabel}
618
+ />
619
+ )
620
+ }
621
+
622
+ /** First non-empty cover URL: entity image → OG placeholder → none. */
623
+ function entityCover(image: unknown, ogPlaceholder?: string | null): string | undefined {
624
+ return (typeof image === 'string' && image) || ogPlaceholder || undefined
625
+ }
626
+
627
+ /** Free-text status string → coloured status pill, reusing the shared
628
+ * `getStatusColorScheme` mapping (cyan/default collapse to neutral grey). */
629
+ function statusPill(status: unknown): MingoInfoCardStatus | undefined {
630
+ const s = typeof status === 'string' ? status.trim() : ''
631
+ if (!s) return undefined
632
+ const scheme = getStatusColorScheme(s)
633
+ const variant: MingoInfoCardStatus['variant'] =
634
+ scheme === 'success' || scheme === 'error' || scheme === 'warning' ? scheme : 'grey'
635
+ return { label: s, variant }
636
+ }
637
+
638
+ /** Blog post → image card. Cover is `featured_image`, falling back to the
639
+ * host's OG placeholder, then to a newspaper glyph when neither exists. */
640
+ function BlogChatCard({
641
+ item,
642
+ chatRef,
643
+ isNewTab,
644
+ ogPlaceholder,
645
+ hasEmbeddedVideo,
646
+ discuss,
425
647
  }: {
426
648
  item: any
427
649
  chatRef: ChatRef
428
650
  isNewTab: boolean
651
+ ogPlaceholder?: string | null
652
+ hasEmbeddedVideo?: boolean
653
+ discuss?: CardDiscussAction
429
654
  }) {
655
+ const category = Array.isArray(item?.categories)
656
+ ? (item.categories.find((c: { name?: string }) => c && c.name)?.name as string | undefined)
657
+ : undefined
658
+ // A post with embedded video surfaces a yellow "Video" pill (mirrors the
659
+ // original BlogCard's compact video badge + the Figma `7741:26583` sample);
660
+ // otherwise the first category.
661
+ const status: MingoInfoCardStatus | undefined = hasEmbeddedVideo
662
+ ? { label: 'Video', variant: 'primary' }
663
+ : category
664
+ ? { label: category, variant: 'grey' }
665
+ : undefined
430
666
  return (
431
- <CampaignCardAdmin
432
- campaign={item}
433
- anchorProps={buildAnchorProps(chatRef.url, isNewTab)!}
667
+ <EntityMingoCard
668
+ title={item?.title ?? ''}
669
+ description={item?.summary ?? undefined}
670
+ cover={entityCover(item?.featured_image, ogPlaceholder)}
671
+ fallbackIcon={<NewspaperIcon size={24} />}
672
+ status={status}
673
+ chatRef={chatRef}
674
+ isNewTab={isNewTab}
675
+ discuss={discuss}
676
+ menuAriaLabel="Article actions"
677
+ />
678
+ )
679
+ }
680
+
681
+ /** Case study → trophy icon + "Case study" pill. */
682
+ function CaseStudyChatCard({
683
+ item,
684
+ chatRef,
685
+ isNewTab,
686
+ ogPlaceholder,
687
+ discuss,
688
+ }: {
689
+ item: any
690
+ chatRef: ChatRef
691
+ isNewTab: boolean
692
+ ogPlaceholder?: string | null
693
+ discuss?: CardDiscussAction
694
+ }) {
695
+ const meta = [item?.msp?.name, item?.user?.full_name].filter(Boolean).join(' · ')
696
+ return (
697
+ <EntityMingoCard
698
+ title={item?.title ?? ''}
699
+ description={item?.summary || meta || undefined}
700
+ cover={entityCover(item?.featured_image, ogPlaceholder)}
701
+ fallbackIcon={<TrophyIcon size={24} />}
702
+ status={{ label: 'Case study', variant: 'grey' }}
703
+ chatRef={chatRef}
704
+ isNewTab={isNewTab}
705
+ discuss={discuss}
706
+ menuAriaLabel="Case study actions"
707
+ />
708
+ )
709
+ }
710
+
711
+ /** Customer interview → microphone icon + "Interview" pill. */
712
+ function CustomerInterviewChatCard({
713
+ item,
714
+ chatRef,
715
+ isNewTab,
716
+ ogPlaceholder,
717
+ discuss,
718
+ }: {
719
+ item: any
720
+ chatRef: ChatRef
721
+ isNewTab: boolean
722
+ ogPlaceholder?: string | null
723
+ discuss?: CardDiscussAction
724
+ }) {
725
+ const meta = [item?.user?.full_name, item?.msp?.name].filter(Boolean).join(' · ')
726
+ return (
727
+ <EntityMingoCard
728
+ title={item?.title ?? ''}
729
+ // Original compact body reads `video_summary` (NOT `summary`); fall back
730
+ // to the "user · msp" meta line when absent.
731
+ description={item?.video_summary || meta || undefined}
732
+ cover={entityCover(item?.featured_image, ogPlaceholder)}
733
+ fallbackIcon={<MicrophoneIcon size={24} />}
734
+ status={{ label: 'Interview', variant: 'grey' }}
735
+ chatRef={chatRef}
736
+ isNewTab={isNewTab}
737
+ discuss={discuss}
738
+ menuAriaLabel="Interview actions"
739
+ />
740
+ )
741
+ }
742
+
743
+ /** Investor update → presentation icon + "Investor update" pill. Title falls
744
+ * back to "Update #N" when the row has no explicit title. */
745
+ function InvestorUpdateChatCard({
746
+ item,
747
+ chatRef,
748
+ isNewTab,
749
+ ogPlaceholder,
750
+ discuss,
751
+ }: {
752
+ item: any
753
+ chatRef: ChatRef
754
+ isNewTab: boolean
755
+ ogPlaceholder?: string | null
756
+ discuss?: CardDiscussAction
757
+ }) {
758
+ const title = item?.title || `Update #${item?.update_number ?? '?'}`
759
+ return (
760
+ <EntityMingoCard
761
+ title={title}
762
+ // Original compact body reads `strategic_update || content` (NOT
763
+ // `summary`); fall back to the period text.
764
+ description={
765
+ item?.strategic_update ||
766
+ item?.content ||
767
+ formatInvestorUpdatePeriod(item?.period_start, item?.period_end) ||
768
+ undefined
769
+ }
770
+ cover={entityCover(item?.featured_image, ogPlaceholder)}
771
+ fallbackIcon={<PresentationLineIcon size={24} />}
772
+ status={{ label: 'Investor update', variant: 'grey' }}
773
+ chatRef={chatRef}
774
+ isNewTab={isNewTab}
775
+ discuss={discuss}
776
+ menuAriaLabel="Update actions"
777
+ />
778
+ )
779
+ }
780
+
781
+ /** Onboarding guide → compass icon + "Guide" pill. Cover prefers the featured
782
+ * image, then the video thumbnail / OG image. */
783
+ function OnboardingGuideChatCard({
784
+ item,
785
+ chatRef,
786
+ isNewTab,
787
+ ogPlaceholder,
788
+ discuss,
789
+ }: {
790
+ item: any
791
+ chatRef: ChatRef
792
+ isNewTab: boolean
793
+ ogPlaceholder?: string | null
794
+ discuss?: CardDiscussAction
795
+ }) {
796
+ const cover =
797
+ entityCover(item?.featured_image) ??
798
+ entityCover(item?.main_video_thumbnail) ??
799
+ entityCover(item?.og_image_url, ogPlaceholder)
800
+ return (
801
+ <EntityMingoCard
802
+ title={item?.title ?? ''}
803
+ description={item?.summary ?? item?.description ?? undefined}
804
+ cover={cover}
805
+ fallbackIcon={<CompassIcon size={24} />}
806
+ status={{ label: 'Guide', variant: 'grey' }}
807
+ chatRef={chatRef}
808
+ isNewTab={isNewTab}
809
+ discuss={discuss}
810
+ menuAriaLabel="Guide actions"
811
+ />
812
+ )
813
+ }
814
+
815
+ /** Program (podcast / webinar / event) → per-type icon + type pill. Cover is
816
+ * the program `cover_url`. */
817
+ function ProgramChatCard({
818
+ item,
819
+ chatRef,
820
+ isNewTab,
821
+ configKey,
822
+ label,
823
+ ogPlaceholder,
824
+ discuss,
825
+ }: {
826
+ item: any
827
+ chatRef: ChatRef
828
+ isNewTab: boolean
829
+ configKey: 'podcast' | 'webinar' | 'event'
830
+ label: string
831
+ ogPlaceholder?: string | null
832
+ discuss?: CardDiscussAction
833
+ }) {
834
+ const icon =
835
+ configKey === 'webinar' ? (
836
+ <PresentationBarIcon size={24} />
837
+ ) : configKey === 'event' ? (
838
+ <CalendarIcon size={24} />
839
+ ) : (
840
+ <MicrophoneIcon size={24} />
841
+ )
842
+
843
+ // Rich meta line mirroring ProgramCard's compact subtitle: "date · <typeMeta>"
844
+ // where typeMeta is podcast duration / event location / webinar time·duration.
845
+ // The type label itself already lives in the status pill, so it's omitted here.
846
+ const isScheduled = item?.status === 'scheduled'
847
+ let typeMeta: string | undefined
848
+ if (
849
+ configKey === 'podcast' &&
850
+ typeof item?.duration_seconds === 'number' &&
851
+ item.duration_seconds > 0 &&
852
+ !isScheduled
853
+ ) {
854
+ typeMeta = formatDurationCompact(item.duration_seconds)
855
+ } else if (
856
+ configKey === 'event' &&
857
+ typeof item?.location_name === 'string' &&
858
+ item.location_name.trim().length > 0
859
+ ) {
860
+ typeMeta = item.location_name
861
+ } else if (configKey === 'webinar' && item?.start_at) {
862
+ const time = formatTimeWithTimezone(item.start_at, null)
863
+ const dur = formatDurationFromRange(item.start_at, item.end_at)
864
+ typeMeta = dur ? `${time} · ${dur}` : time
865
+ }
866
+ const itemDate = formatDate(item?.date ?? null, { fallback: '', timezone: 'local' })
867
+ const meta = [itemDate, typeMeta].filter(Boolean).join(' · ')
868
+
869
+ return (
870
+ <EntityMingoCard
871
+ title={item?.title ?? ''}
872
+ description={meta || item?.description || undefined}
873
+ cover={entityCover(item?.cover_url, ogPlaceholder)}
874
+ fallbackIcon={icon}
875
+ status={{ label, variant: 'grey' }}
876
+ chatRef={chatRef}
877
+ isNewTab={isNewTab}
878
+ discuss={discuss}
879
+ menuAriaLabel={`${label} actions`}
880
+ />
881
+ )
882
+ }
883
+
884
+ /**
885
+ * Roadmap / delivery / task → coloured status pill, no cover image. Icon
886
+ * priority mirrors `RoadmapCard`:
887
+ * 1. task-type glyph for internal tasks (and logo-less typed items)
888
+ * 2. the server-provided integration logo (`item.icon`, ClickUp, …),
889
+ * shown contained inside the icon box
890
+ * 3. a per-type default glyph
891
+ */
892
+ function RoadmapChatCard({
893
+ item,
894
+ chatRef,
895
+ isNewTab,
896
+ cardType,
897
+ discuss,
898
+ }: {
899
+ item: any
900
+ chatRef: ChatRef
901
+ isNewTab: boolean
902
+ cardType: 'roadmap_item' | 'delivery_item' | 'internal_task'
903
+ discuss?: CardDiscussAction
904
+ }) {
905
+ const logoUrl =
906
+ typeof item?.icon === 'string' && item.icon.startsWith('http') ? item.icon : undefined
907
+ const useTypeIcon = cardType === 'internal_task' || (!logoUrl && item?.customItemId != null)
908
+ const defaultIcon =
909
+ cardType === 'delivery_item' ? (
910
+ <TruckFastIcon size={24} />
911
+ ) : cardType === 'internal_task' ? (
912
+ <CheckSquareIcon size={24} />
913
+ ) : (
914
+ <MapIcon size={24} />
915
+ )
916
+ const icon = useTypeIcon ? (
917
+ <TaskTypeIcon customItemId={item?.customItemId} className="size-6" />
918
+ ) : logoUrl ? (
919
+ // Contained (not full-bleed) so the integration logo keeps its aspect
920
+ // ratio inside the bordered icon box — matches RoadmapCard.
921
+ <img src={logoUrl} alt="" className="size-6 object-contain" />
922
+ ) : (
923
+ defaultIcon
924
+ )
925
+ return (
926
+ <EntityMingoCard
927
+ title={item?.title ?? ''}
928
+ description={item?.description ?? undefined}
929
+ fallbackIcon={icon}
930
+ status={statusPill(item?.status)}
931
+ chatRef={chatRef}
932
+ isNewTab={isNewTab}
933
+ discuss={discuss}
934
+ menuAriaLabel="Roadmap actions"
434
935
  />
435
936
  )
436
937
  }
@@ -452,6 +953,9 @@ type ChatCardRegistryEntry =
452
953
  label: string
453
954
  contentRefType: string
454
955
  displayAction?: boolean
956
+ /** Render the card alone — skip the `ChatCardWithDiscuss` "Ask" +
957
+ * provenance source row (mirrors the no-fetch `bareInline`). */
958
+ bareInline?: boolean
455
959
  skeleton: () => React.ReactNode
456
960
  render: (item: any, chatRef: ChatRef, opts: ChatCardRenderOptions) => React.ReactNode
457
961
  /** Optional post-fetch URL synthesizer. When `chatRef.url` is null
@@ -466,32 +970,32 @@ type ChatCardRegistryEntry =
466
970
  fallbackHref?: (item: any) => string | null
467
971
  }
468
972
 
469
- type FinancialBadgeScheme = 'cyan' | 'warning' | 'success' | 'error' | 'default'
470
- interface FinancialBadgeConfig {
973
+ interface FinancialCardConfig {
471
974
  label: string
472
- badge: string
473
- scheme?: FinancialBadgeScheme
975
+ /** Per-type leading glyph — carries the type signal now that the
976
+ * badge pill is dropped. */
977
+ icon: () => React.ReactNode
474
978
  }
475
- const FINANCIAL_CARD_BADGES: Record<string, FinancialBadgeConfig> = {
476
- financial_kpi: { label: 'Financial KPI', badge: 'KPI', scheme: 'cyan' },
477
- cap_table: { label: 'Cap table entry', badge: 'Cap table', scheme: 'warning' },
478
- profit_loss: { label: 'P&L period', badge: 'P&L' },
479
- balance_sheet: { label: 'Balance sheet', badge: 'Balance sheet' },
480
- cash_flow: { label: 'Cash flow', badge: 'Cash flow' },
979
+ const FINANCIAL_CARD_CONFIGS: Record<string, FinancialCardConfig> = {
980
+ financial_kpi: { label: 'Financial KPI', icon: () => <ChartBar01VerIcon size={24} /> },
981
+ cap_table: { label: 'Cap table entry', icon: () => <ChartPieIcon size={24} /> },
982
+ profit_loss: { label: 'P&L period', icon: () => <MoneyBillDollarIcon size={24} /> },
983
+ balance_sheet: { label: 'Balance sheet', icon: () => <BankIcon size={24} /> },
984
+ cash_flow: { label: 'Cash flow', icon: () => <CoinsExchangeCurrencyIcon size={24} /> },
481
985
  }
482
986
  function financialRegistryEntries(): Record<string, ChatCardRegistryEntry> {
483
987
  const out: Record<string, ChatCardRegistryEntry> = {}
484
- for (const [docType, cfg] of Object.entries(FINANCIAL_CARD_BADGES)) {
485
- const scheme = cfg.scheme ?? 'default'
988
+ for (const [docType, cfg] of Object.entries(FINANCIAL_CARD_CONFIGS)) {
486
989
  out[docType] = {
487
990
  mode: 'no-fetch',
488
991
  label: cfg.label,
992
+ bareInline: true,
489
993
  render: (chatRef, opts) => (
490
994
  <GenericFinancialChatCard
491
995
  chatRef={chatRef}
492
- badge={cfg.badge}
493
- scheme={scheme}
996
+ icon={cfg.icon()}
494
997
  isNewTab={opts.isNewTab}
998
+ discuss={opts.discuss}
495
999
  />
496
1000
  ),
497
1001
  }
@@ -517,8 +1021,10 @@ function githubRegistryEntries(): Record<string, ChatCardRegistryEntry> {
517
1021
  out[docType] = {
518
1022
  mode: 'no-fetch',
519
1023
  label: cfg.label,
1024
+ bareInline: true,
520
1025
  render: (chatRef, opts) => (
521
- <GitHubChatCard chatRef={chatRef} kind={cfg.kind} isNewTab={opts.isNewTab} />
1026
+ <GitHubChatCard chatRef={chatRef} kind={cfg.kind} isNewTab={opts.isNewTab}
1027
+ discuss={opts.discuss} />
522
1028
  ),
523
1029
  }
524
1030
  }
@@ -543,25 +1049,19 @@ function programRegistryEntries(): Record<string, ChatCardRegistryEntry> {
543
1049
  mode: 'fetch',
544
1050
  label: cfg.label,
545
1051
  contentRefType: cfg.contentRefType,
1052
+ bareInline: true,
546
1053
  skeleton: () => <ProgramCardSkeleton size="sm" />,
547
- render: (item, chatRef, opts) => {
548
- // Embedder-provided config wins; lib default fills in otherwise
549
- // so the compact card renders even without `extras`. See
550
- // `program-card-defaults.ts` for the rationale.
551
- const config =
552
- opts?.extras?.programConfigs?.[cfg.configKey] ??
553
- DEFAULT_PROGRAM_CONFIGS[cfg.configKey]
554
- return (
555
- <ProgramCard
556
- config={config}
557
- item={item as BaseProgramItem}
558
- size="sm"
559
- href={chatRef.url ?? ''}
560
- targetPlatform={chatRef.targetPlatform ?? null}
561
- {...newTabAnchorAttrs(opts.isNewTab)}
562
- />
563
- )
564
- },
1054
+ render: (item, chatRef, opts) => (
1055
+ <ProgramChatCard
1056
+ item={item}
1057
+ chatRef={chatRef}
1058
+ isNewTab={opts.isNewTab}
1059
+ discuss={opts.discuss}
1060
+ configKey={cfg.configKey}
1061
+ label={cfg.configKey.charAt(0).toUpperCase() + cfg.configKey.slice(1)}
1062
+ ogPlaceholder={opts?.extras?.buildOgPlaceholderUrl?.(item?.title ?? '') ?? null}
1063
+ />
1064
+ ),
565
1065
  }
566
1066
  }
567
1067
  return out
@@ -593,16 +1093,14 @@ function roadmapRegistryEntries(): Record<string, ChatCardRegistryEntry> {
593
1093
  mode: 'fetch',
594
1094
  label: cfg.label,
595
1095
  contentRefType: cfg.contentRefType,
1096
+ bareInline: true,
596
1097
  skeleton: () => <RoadmapCardSkeleton size="sm" />,
597
1098
  render: (item, chatRef, opts) => (
598
- <RoadmapCard
1099
+ <RoadmapChatCard
599
1100
  item={item}
600
- href={chatRef.url ?? ''}
601
- targetPlatform={chatRef.targetPlatform ?? null}
602
- {...newTabAnchorAttrs(opts.isNewTab)}
603
- userVote={null}
604
- onVote={() => {}}
605
- size="sm"
1101
+ chatRef={chatRef}
1102
+ isNewTab={opts.isNewTab}
1103
+ discuss={opts.discuss}
606
1104
  cardType={cfg.cardType}
607
1105
  />
608
1106
  ),
@@ -617,53 +1115,56 @@ const CHAT_CARD_REGISTRY: Record<string, ChatCardRegistryEntry> = {
617
1115
  slack_message: {
618
1116
  mode: 'no-fetch',
619
1117
  label: 'Slack message',
1118
+ bareInline: true,
620
1119
  render: (chatRef, opts) => (
621
- <SlackChatCard chatRef={chatRef} isNewTab={opts.isNewTab} />
1120
+ <SlackChatCard chatRef={chatRef} isNewTab={opts.isNewTab}
1121
+ discuss={opts.discuss} />
622
1122
  ),
623
1123
  },
624
1124
  hubspot_ticket: {
625
1125
  mode: 'no-fetch',
626
1126
  label: 'HubSpot ticket',
1127
+ // `MingoInfoCard` is the whole card — no "Ask"/provenance source row.
1128
+ bareInline: true,
627
1129
  render: (chatRef, opts) => (
628
- <HubspotTicketChatCard chatRef={chatRef} isNewTab={opts.isNewTab} />
1130
+ <HubspotTicketChatCard chatRef={chatRef} isNewTab={opts.isNewTab}
1131
+ discuss={opts.discuss} />
629
1132
  ),
630
1133
  },
631
1134
  hubspot_ticket_anon: {
632
1135
  mode: 'no-fetch',
633
1136
  label: 'HubSpot ticket (anon)',
1137
+ bareInline: true,
634
1138
  render: (chatRef, opts) => (
635
- <HubspotTicketChatCard chatRef={chatRef} isNewTab={opts.isNewTab} />
1139
+ <HubspotTicketChatCard chatRef={chatRef} isNewTab={opts.isNewTab}
1140
+ discuss={opts.discuss} />
636
1141
  ),
637
1142
  },
638
1143
  hubspot_ticket_self: {
639
1144
  mode: 'no-fetch',
640
1145
  label: 'HubSpot ticket (self)',
1146
+ bareInline: true,
641
1147
  render: (chatRef, opts) => (
642
- <HubspotTicketChatCard chatRef={chatRef} isNewTab={opts.isNewTab} />
1148
+ <HubspotTicketChatCard chatRef={chatRef} isNewTab={opts.isNewTab}
1149
+ discuss={opts.discuss} />
643
1150
  ),
644
1151
  },
645
1152
  data_room_doc: {
646
1153
  mode: 'no-fetch',
647
1154
  label: 'Data-room doc',
1155
+ bareInline: true,
648
1156
  render: (chatRef, opts) => (
649
- <DataRoomDocChatCard
650
- chatRef={chatRef}
651
- baseRoute={opts?.baseRoute}
652
- chipBasePlatform={opts?.chipBasePlatform}
653
- isNewTab={opts.isNewTab}
654
- />
1157
+ <DataRoomDocChatCard chatRef={chatRef} isNewTab={opts.isNewTab}
1158
+ discuss={opts.discuss} />
655
1159
  ),
656
1160
  },
657
1161
  markdown: {
658
1162
  mode: 'no-fetch',
659
1163
  label: 'Doc page (markdown)',
1164
+ bareInline: true,
660
1165
  render: (chatRef, opts) => (
661
- <DataRoomDocChatCard
662
- chatRef={chatRef}
663
- baseRoute={opts?.baseRoute}
664
- chipBasePlatform={opts?.chipBasePlatform}
665
- isNewTab={opts.isNewTab}
666
- />
1166
+ <DataRoomDocChatCard chatRef={chatRef} isNewTab={opts.isNewTab}
1167
+ discuss={opts.discuss} />
667
1168
  ),
668
1169
  },
669
1170
  video: {
@@ -679,15 +1180,15 @@ const CHAT_CARD_REGISTRY: Record<string, ChatCardRegistryEntry> = {
679
1180
  mode: 'fetch',
680
1181
  label: 'Blog post',
681
1182
  contentRefType: 'blog_post_existing',
1183
+ bareInline: true,
682
1184
  skeleton: () => <BlogCardSkeleton size="sm" />,
683
1185
  render: (item, chatRef, opts) => (
684
- <BlogCard
685
- post={item}
686
- size="sm"
687
- href={chatRef.url ?? ''}
688
- targetPlatform={chatRef.targetPlatform ?? null}
689
- placeholderUrl={opts?.extras?.buildOgPlaceholderUrl?.(item?.title ?? '') ?? null}
690
- {...newTabAnchorAttrs(opts.isNewTab)}
1186
+ <BlogChatCard
1187
+ item={item}
1188
+ chatRef={chatRef}
1189
+ isNewTab={opts.isNewTab}
1190
+ discuss={opts.discuss}
1191
+ ogPlaceholder={opts?.extras?.buildOgPlaceholderUrl?.(item?.title ?? '') ?? null}
691
1192
  hasEmbeddedVideo={chatRef.metadata?.hasEmbeddedVideo === true}
692
1193
  />
693
1194
  ),
@@ -696,15 +1197,15 @@ const CHAT_CARD_REGISTRY: Record<string, ChatCardRegistryEntry> = {
696
1197
  mode: 'fetch',
697
1198
  label: 'Case study',
698
1199
  contentRefType: 'case_study',
1200
+ bareInline: true,
699
1201
  skeleton: () => <CaseStudyCardSkeleton size="sm" />,
700
1202
  render: (item, chatRef, opts) => (
701
- <CaseStudyCard
702
- study={item}
703
- size="sm"
704
- href={chatRef.url ?? ''}
705
- targetPlatform={chatRef.targetPlatform ?? null}
706
- placeholderUrl={opts?.extras?.buildOgPlaceholderUrl?.(item?.title ?? '') ?? null}
707
- {...newTabAnchorAttrs(opts.isNewTab)}
1203
+ <CaseStudyChatCard
1204
+ item={item}
1205
+ chatRef={chatRef}
1206
+ isNewTab={opts.isNewTab}
1207
+ discuss={opts.discuss}
1208
+ ogPlaceholder={opts?.extras?.buildOgPlaceholderUrl?.(item?.title ?? '') ?? null}
708
1209
  />
709
1210
  ),
710
1211
  },
@@ -712,15 +1213,15 @@ const CHAT_CARD_REGISTRY: Record<string, ChatCardRegistryEntry> = {
712
1213
  mode: 'fetch',
713
1214
  label: 'Customer interview',
714
1215
  contentRefType: 'customer_interview',
1216
+ bareInline: true,
715
1217
  skeleton: () => <CustomerInterviewCardSkeleton size="sm" />,
716
1218
  render: (item, chatRef, opts) => (
717
- <CustomerInterviewCard
718
- interview={item}
719
- size="sm"
720
- href={chatRef.url ?? ''}
721
- targetPlatform={chatRef.targetPlatform ?? null}
722
- placeholderUrl={opts?.extras?.buildOgPlaceholderUrl?.(item?.title ?? '') ?? null}
723
- {...newTabAnchorAttrs(opts.isNewTab)}
1219
+ <CustomerInterviewChatCard
1220
+ item={item}
1221
+ chatRef={chatRef}
1222
+ isNewTab={opts.isNewTab}
1223
+ discuss={opts.discuss}
1224
+ ogPlaceholder={opts?.extras?.buildOgPlaceholderUrl?.(item?.title ?? '') ?? null}
724
1225
  />
725
1226
  ),
726
1227
  },
@@ -728,6 +1229,7 @@ const CHAT_CARD_REGISTRY: Record<string, ChatCardRegistryEntry> = {
728
1229
  mode: 'fetch',
729
1230
  label: 'Product release',
730
1231
  contentRefType: 'product_release',
1232
+ bareInline: true,
731
1233
  skeleton: () => <ProductReleaseCardSkeleton size="sm" />,
732
1234
  render: (item, chatRef, opts) => {
733
1235
  // Embedder-provided builder wins; lib default fills in otherwise
@@ -744,6 +1246,7 @@ const CHAT_CARD_REGISTRY: Record<string, ChatCardRegistryEntry> = {
744
1246
  chatRef={chatRef}
745
1247
  buildProps={builder}
746
1248
  isNewTab={opts.isNewTab}
1249
+ discuss={opts.discuss}
747
1250
  />
748
1251
  )
749
1252
  },
@@ -753,15 +1256,15 @@ const CHAT_CARD_REGISTRY: Record<string, ChatCardRegistryEntry> = {
753
1256
  mode: 'fetch',
754
1257
  label: 'Investor update',
755
1258
  contentRefType: 'investor_update',
1259
+ bareInline: true,
756
1260
  skeleton: () => <InvestorUpdateCardSkeleton size="sm" />,
757
1261
  render: (item, chatRef, opts) => (
758
- <InvestorUpdateCard
759
- update={item}
760
- size="sm"
761
- href={chatRef.url ?? ''}
762
- targetPlatform={chatRef.targetPlatform ?? null}
763
- placeholderUrl={opts?.extras?.buildOgPlaceholderUrl?.(item?.title ?? '') ?? null}
764
- {...newTabAnchorAttrs(opts.isNewTab)}
1262
+ <InvestorUpdateChatCard
1263
+ item={item}
1264
+ chatRef={chatRef}
1265
+ isNewTab={opts.isNewTab}
1266
+ discuss={opts.discuss}
1267
+ ogPlaceholder={opts?.extras?.buildOgPlaceholderUrl?.(item?.title ?? '') ?? null}
765
1268
  />
766
1269
  ),
767
1270
  },
@@ -769,16 +1272,17 @@ const CHAT_CARD_REGISTRY: Record<string, ChatCardRegistryEntry> = {
769
1272
  mode: 'fetch',
770
1273
  label: 'Onboarding guide',
771
1274
  contentRefType: 'onboarding_guide',
1275
+ bareInline: true,
1276
+ // "Display" affordance (verbatim guide body) instead of "Ask".
772
1277
  displayAction: true,
773
1278
  skeleton: () => <OnboardingGuideCardSkeleton size="sm" />,
774
1279
  render: (item, chatRef, opts) => (
775
- <OnboardingGuideCard
776
- guide={item}
777
- size="sm"
778
- href={chatRef.url ?? ''}
779
- targetPlatform={chatRef.targetPlatform ?? null}
780
- placeholderUrl={opts?.extras?.buildOgPlaceholderUrl?.(item?.title ?? '') ?? null}
781
- {...newTabAnchorAttrs(opts.isNewTab)}
1280
+ <OnboardingGuideChatCard
1281
+ item={item}
1282
+ chatRef={chatRef}
1283
+ isNewTab={opts.isNewTab}
1284
+ discuss={opts.discuss}
1285
+ ogPlaceholder={opts?.extras?.buildOgPlaceholderUrl?.(item?.title ?? '') ?? null}
782
1286
  />
783
1287
  ),
784
1288
  },
@@ -786,6 +1290,7 @@ const CHAT_CARD_REGISTRY: Record<string, ChatCardRegistryEntry> = {
786
1290
  mode: 'fetch',
787
1291
  label: 'Marketing campaign',
788
1292
  contentRefType: 'marketing_campaign',
1293
+ bareInline: true,
789
1294
  skeleton: () => <CampaignCardAdminSkeleton />,
790
1295
  // No public viewer — synthesize the hub-internal admin URL post-fetch
791
1296
  // so the wrapper + isNewTab computation see the actual destination.
@@ -796,6 +1301,7 @@ const CHAT_CARD_REGISTRY: Record<string, ChatCardRegistryEntry> = {
796
1301
  item={item}
797
1302
  chatRef={chatRef}
798
1303
  isNewTab={opts.isNewTab}
1304
+ discuss={opts.discuss}
799
1305
  />
800
1306
  ),
801
1307
  },
@@ -992,7 +1498,36 @@ export function ChatCardLoader({
992
1498
  // via `renderOpts.isNewTab` so the inner `<a>` agrees with the
993
1499
  // runtime nav decision in `ChatCardNavWrap` + `handleChatNavClick`.
994
1500
  const isNewTab = computeIsNewTab(runtime, finalChatRef.url, finalChatRef.targetPlatform ?? null)
995
- const renderOpts: ChatCardRenderOptions = { baseRoute, chipBasePlatform, extras, isNewTab }
1501
+
1502
+ // Resolve the "Ask Mingo"/"Display" affordance ONCE — same precedence as the
1503
+ // legacy `ChatCardWithDiscuss` (displayAction + onDisplay → "Display",
1504
+ // otherwise onDiscuss → "Ask Mingo"). Threaded into each card's "⋯" menu.
1505
+ const useDisplay = !!entry.displayAction && !!onDisplay
1506
+ const discussFn = useDisplay ? onDisplay : onDiscuss
1507
+ const discuss: CardDiscussAction | undefined = discussFn
1508
+ ? {
1509
+ label: useDisplay ? 'Display' : 'Ask Mingo',
1510
+ icon: useDisplay ? (
1511
+ <FileContentIcon size={20} />
1512
+ ) : (
1513
+ <MingoIcon
1514
+ className="size-5"
1515
+ color="white"
1516
+ eyesColor="var(--ods-flamingo-cyan-base)"
1517
+ cornerColor="var(--ods-flamingo-cyan-base)"
1518
+ />
1519
+ ),
1520
+ run: () => discussFn(finalChatRef),
1521
+ }
1522
+ : undefined
1523
+
1524
+ const renderOpts: ChatCardRenderOptions = {
1525
+ baseRoute,
1526
+ chipBasePlatform,
1527
+ extras,
1528
+ isNewTab,
1529
+ discuss,
1530
+ }
996
1531
 
997
1532
  // Wrap EVERY rendered card with ChatCardNavWrap so the inner anchor's
998
1533
  // primary click routes through the chat runtime (same handler as the
@@ -1051,6 +1586,9 @@ export function ChatCardLoader({
1051
1586
  }
1052
1587
  if (isLoading) return <>{entry.skeleton()}</>
1053
1588
  if (!item) return null
1589
+ if (entry.bareInline) {
1590
+ return <>{navWrap(entry.render(item, finalChatRef, renderOpts))}</>
1591
+ }
1054
1592
  return (
1055
1593
  <ChatCardWithDiscuss
1056
1594
  chatRef={finalChatRef}