@flamingo-stack/openframe-frontend-core 0.0.685 → 0.0.686-2463.7461.1

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 (174) hide show
  1. package/dist/{chunk-T6KWNJDW.js → chunk-22RSJ57Z.js} +470 -465
  2. package/dist/chunk-22RSJ57Z.js.map +1 -0
  3. package/dist/{chunk-65KTXRBY.js → chunk-2G2CB7ZZ.js} +6 -6
  4. package/dist/{chunk-K2HZG4RM.cjs → chunk-2OD2PPNO.cjs} +32 -32
  5. package/dist/{chunk-K2HZG4RM.cjs.map → chunk-2OD2PPNO.cjs.map} +1 -1
  6. package/dist/{chunk-HMXUOLZR.cjs → chunk-3DO76UAB.cjs} +20 -20
  7. package/dist/{chunk-HMXUOLZR.cjs.map → chunk-3DO76UAB.cjs.map} +1 -1
  8. package/dist/{chunk-XW7MUIW4.js → chunk-3NIZ2VLQ.js} +3 -3
  9. package/dist/{chunk-5LJZPJHX.cjs → chunk-4LBCUBVL.cjs} +59 -59
  10. package/dist/{chunk-5LJZPJHX.cjs.map → chunk-4LBCUBVL.cjs.map} +1 -1
  11. package/dist/{chunk-ZF4Z36G4.js → chunk-4RJQMOQW.js} +7 -7
  12. package/dist/{chunk-TJI5V5BR.cjs → chunk-AF4XOURN.cjs} +7 -7
  13. package/dist/{chunk-TJI5V5BR.cjs.map → chunk-AF4XOURN.cjs.map} +1 -1
  14. package/dist/{chunk-7XF63THA.js → chunk-ASBBGP36.js} +6 -6
  15. package/dist/{chunk-O7LOZW4R.js → chunk-C5TFPMOM.js} +5 -5
  16. package/dist/{chunk-CMQXPD5C.cjs → chunk-GETQTNRV.cjs} +28 -28
  17. package/dist/{chunk-CMQXPD5C.cjs.map → chunk-GETQTNRV.cjs.map} +1 -1
  18. package/dist/{chunk-TUG7IT6H.js → chunk-HLEXYTOE.js} +2 -2
  19. package/dist/{chunk-M4HYIXP6.cjs → chunk-IBUFFKAK.cjs} +399 -383
  20. package/dist/chunk-IBUFFKAK.cjs.map +1 -0
  21. package/dist/{chunk-GXV6UEVY.js → chunk-IELOPDTA.js} +3 -3
  22. package/dist/{chunk-T4DUTP5C.cjs → chunk-II5GVLTN.cjs} +5 -5
  23. package/dist/{chunk-T4DUTP5C.cjs.map → chunk-II5GVLTN.cjs.map} +1 -1
  24. package/dist/{chunk-B32YLOYQ.js → chunk-JBJZL5PI.js} +4 -4
  25. package/dist/{chunk-UT333EOT.js → chunk-LCOMNQLX.js} +4 -4
  26. package/dist/{chunk-VDPCK437.js → chunk-MHTB67ZH.js} +21 -5
  27. package/dist/{chunk-VDPCK437.js.map → chunk-MHTB67ZH.js.map} +1 -1
  28. package/dist/{chunk-EFBMNVSH.cjs → chunk-MZAZUSPQ.cjs} +15 -15
  29. package/dist/{chunk-EFBMNVSH.cjs.map → chunk-MZAZUSPQ.cjs.map} +1 -1
  30. package/dist/{chunk-C3B36OYZ.cjs → chunk-N3I53B5W.cjs} +3 -3
  31. package/dist/{chunk-C3B36OYZ.cjs.map → chunk-N3I53B5W.cjs.map} +1 -1
  32. package/dist/{chunk-WJK2EETW.js → chunk-NINO3PI7.js} +2 -2
  33. package/dist/{chunk-YCTVLN47.cjs → chunk-P4SBHO7U.cjs} +10 -10
  34. package/dist/{chunk-YCTVLN47.cjs.map → chunk-P4SBHO7U.cjs.map} +1 -1
  35. package/dist/{chunk-PSQKD6ZN.js → chunk-PG65AE3I.js} +3 -3
  36. package/dist/{chunk-DQOQ3KTP.cjs → chunk-QNWQQ4RT.cjs} +78 -73
  37. package/dist/chunk-QNWQQ4RT.cjs.map +1 -0
  38. package/dist/{chunk-BZ2XOEHW.js → chunk-QOH45QSE.js} +3 -3
  39. package/dist/{chunk-DWXWEGGO.cjs → chunk-QTXDXHMD.cjs} +22 -22
  40. package/dist/{chunk-DWXWEGGO.cjs.map → chunk-QTXDXHMD.cjs.map} +1 -1
  41. package/dist/{chunk-B25CF3PO.cjs → chunk-RBCMBRI7.cjs} +30 -30
  42. package/dist/{chunk-B25CF3PO.cjs.map → chunk-RBCMBRI7.cjs.map} +1 -1
  43. package/dist/{chunk-QHLJSB2J.cjs → chunk-RKH7DEDF.cjs} +12 -12
  44. package/dist/{chunk-QHLJSB2J.cjs.map → chunk-RKH7DEDF.cjs.map} +1 -1
  45. package/dist/{chunk-Y7ZYOQNB.js → chunk-RSMAVO2Z.js} +3 -3
  46. package/dist/{chunk-A3CSQB4A.cjs → chunk-SI77HF4U.cjs} +1247 -805
  47. package/dist/chunk-SI77HF4U.cjs.map +1 -0
  48. package/dist/{chunk-GHLGXFNU.js → chunk-U6JC3RS2.js} +3 -3
  49. package/dist/{chunk-B5N3AHPG.cjs → chunk-UBXEETWQ.cjs} +81 -81
  50. package/dist/{chunk-B5N3AHPG.cjs.map → chunk-UBXEETWQ.cjs.map} +1 -1
  51. package/dist/{chunk-N4Z6X5BV.js → chunk-WFH52OVQ.js} +5 -5
  52. package/dist/{chunk-U3IXG7TR.cjs → chunk-WJX3ZSOE.cjs} +33 -33
  53. package/dist/{chunk-U3IXG7TR.cjs.map → chunk-WJX3ZSOE.cjs.map} +1 -1
  54. package/dist/{chunk-XGX6JSEU.js → chunk-YPXXXX6R.js} +2086 -1644
  55. package/dist/chunk-YPXXXX6R.js.map +1 -0
  56. package/dist/{chunk-P2ASEPA2.cjs → chunk-ZS3YCJYY.cjs} +6 -6
  57. package/dist/{chunk-P2ASEPA2.cjs.map → chunk-ZS3YCJYY.cjs.map} +1 -1
  58. package/dist/components/case-studies/index.cjs +16 -16
  59. package/dist/components/case-studies/index.js +4 -4
  60. package/dist/components/chat/ai-assistant-info.d.ts.map +1 -1
  61. package/dist/components/chat/approval-request-message.d.ts.map +1 -1
  62. package/dist/components/chat/chat-appearance-context.d.ts +9 -0
  63. package/dist/components/chat/chat-appearance-context.d.ts.map +1 -0
  64. package/dist/components/chat/chat-input.d.ts.map +1 -1
  65. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  66. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  67. package/dist/components/chat/chat-nav-sidebar.d.ts +50 -0
  68. package/dist/components/chat/chat-nav-sidebar.d.ts.map +1 -0
  69. package/dist/components/chat/chat-top-navigation.d.ts +29 -0
  70. package/dist/components/chat/chat-top-navigation.d.ts.map +1 -0
  71. package/dist/components/chat/error-message-display.d.ts.map +1 -1
  72. package/dist/components/chat/index.cjs +21 -3
  73. package/dist/components/chat/index.cjs.map +1 -1
  74. package/dist/components/chat/index.d.ts +6 -0
  75. package/dist/components/chat/index.d.ts.map +1 -1
  76. package/dist/components/chat/index.js +20 -2
  77. package/dist/components/chat/msp-organization-card.d.ts +7 -1
  78. package/dist/components/chat/msp-organization-card.d.ts.map +1 -1
  79. package/dist/components/chat/onboarding-carousel.d.ts +29 -0
  80. package/dist/components/chat/onboarding-carousel.d.ts.map +1 -0
  81. package/dist/components/chat/system-event-message.d.ts +14 -0
  82. package/dist/components/chat/system-event-message.d.ts.map +1 -0
  83. package/dist/components/chat/types/chat.types.d.ts +13 -0
  84. package/dist/components/chat/types/chat.types.d.ts.map +1 -1
  85. package/dist/components/chat/types/component.types.d.ts +20 -1
  86. package/dist/components/chat/types/component.types.d.ts.map +1 -1
  87. package/dist/components/contact/index.cjs +5 -5
  88. package/dist/components/contact/index.js +4 -4
  89. package/dist/components/docs/index.cjs +8 -8
  90. package/dist/components/docs/index.js +7 -7
  91. package/dist/components/embeds/index.cjs +5 -5
  92. package/dist/components/embeds/index.js +4 -4
  93. package/dist/components/faq/index.cjs +5 -5
  94. package/dist/components/faq/index.js +4 -4
  95. package/dist/components/features/index.cjs +4 -4
  96. package/dist/components/features/index.js +3 -3
  97. package/dist/components/help-center-pages/index.cjs +91 -91
  98. package/dist/components/help-center-pages/index.js +15 -15
  99. package/dist/components/index.cjs +73 -55
  100. package/dist/components/index.cjs.map +1 -1
  101. package/dist/components/index.js +33 -15
  102. package/dist/components/index.js.map +1 -1
  103. package/dist/components/meeting-scheduler/index.cjs +41 -41
  104. package/dist/components/meeting-scheduler/index.js +3 -3
  105. package/dist/components/navigation/index.cjs +7 -7
  106. package/dist/components/navigation/index.js +6 -6
  107. package/dist/components/onboarding-guides/index.cjs +7 -7
  108. package/dist/components/onboarding-guides/index.js +6 -6
  109. package/dist/components/related-content/index.cjs +3 -3
  110. package/dist/components/related-content/index.js +2 -2
  111. package/dist/components/tickets/index.cjs +8 -8
  112. package/dist/components/tickets/index.js +7 -7
  113. package/dist/components/ui/avatar-stack.d.ts +5 -1
  114. package/dist/components/ui/avatar-stack.d.ts.map +1 -1
  115. package/dist/components/ui/index.cjs +22 -4
  116. package/dist/components/ui/index.cjs.map +1 -1
  117. package/dist/components/ui/index.js +21 -3
  118. package/dist/components/ui/reopen-ticket-request-modal.d.ts +4 -1
  119. package/dist/components/ui/reopen-ticket-request-modal.d.ts.map +1 -1
  120. package/dist/components/ui/tab-navigation.d.ts +7 -0
  121. package/dist/components/ui/tab-navigation.d.ts.map +1 -1
  122. package/dist/index.cjs +27 -9
  123. package/dist/index.cjs.map +1 -1
  124. package/dist/index.js +25 -7
  125. package/dist/index.js.map +1 -1
  126. package/package.json +1 -1
  127. package/src/components/chat/__tests__/chat-appearance.test.tsx +79 -0
  128. package/src/components/chat/__tests__/onboarding-carousel.test.tsx +56 -0
  129. package/src/components/chat/ai-assistant-info.tsx +7 -1
  130. package/src/components/chat/approval-request-message.tsx +6 -1
  131. package/src/components/chat/chat-appearance-context.tsx +15 -0
  132. package/src/components/chat/chat-input.tsx +26 -1
  133. package/src/components/chat/chat-message-enhanced.tsx +180 -142
  134. package/src/components/chat/chat-message-list.tsx +169 -164
  135. package/src/components/chat/chat-nav-sidebar.tsx +163 -0
  136. package/src/components/chat/chat-top-navigation.tsx +109 -0
  137. package/src/components/chat/error-message-display.tsx +21 -3
  138. package/src/components/chat/index.ts +6 -0
  139. package/src/components/chat/msp-organization-card.tsx +35 -0
  140. package/src/components/chat/onboarding-carousel.tsx +162 -0
  141. package/src/components/chat/system-event-message.tsx +54 -0
  142. package/src/components/chat/types/chat.types.ts +15 -0
  143. package/src/components/chat/types/component.types.ts +20 -0
  144. package/src/components/ui/__tests__/avatar-stack.test.tsx +25 -0
  145. package/src/components/ui/__tests__/tab-navigation.test.tsx +56 -2
  146. package/src/components/ui/avatar-stack.tsx +15 -5
  147. package/src/components/ui/reopen-ticket-request-modal.tsx +6 -2
  148. package/src/components/ui/tab-navigation.tsx +23 -0
  149. package/src/stories/FaeLayoutV2.stories.tsx +150 -0
  150. package/src/stories/FaeOnboardingV2.stories.tsx +68 -0
  151. package/src/stories/FaeThreadV2.stories.tsx +192 -0
  152. package/src/stories/fae-brand-decorator.tsx +28 -0
  153. package/src/styles/ods-colors.css +10 -0
  154. package/src/styles/ods-effects.css +13 -0
  155. package/dist/chunk-A3CSQB4A.cjs.map +0 -1
  156. package/dist/chunk-DQOQ3KTP.cjs.map +0 -1
  157. package/dist/chunk-M4HYIXP6.cjs.map +0 -1
  158. package/dist/chunk-T6KWNJDW.js.map +0 -1
  159. package/dist/chunk-XGX6JSEU.js.map +0 -1
  160. /package/dist/{chunk-65KTXRBY.js.map → chunk-2G2CB7ZZ.js.map} +0 -0
  161. /package/dist/{chunk-XW7MUIW4.js.map → chunk-3NIZ2VLQ.js.map} +0 -0
  162. /package/dist/{chunk-ZF4Z36G4.js.map → chunk-4RJQMOQW.js.map} +0 -0
  163. /package/dist/{chunk-7XF63THA.js.map → chunk-ASBBGP36.js.map} +0 -0
  164. /package/dist/{chunk-O7LOZW4R.js.map → chunk-C5TFPMOM.js.map} +0 -0
  165. /package/dist/{chunk-TUG7IT6H.js.map → chunk-HLEXYTOE.js.map} +0 -0
  166. /package/dist/{chunk-GXV6UEVY.js.map → chunk-IELOPDTA.js.map} +0 -0
  167. /package/dist/{chunk-B32YLOYQ.js.map → chunk-JBJZL5PI.js.map} +0 -0
  168. /package/dist/{chunk-UT333EOT.js.map → chunk-LCOMNQLX.js.map} +0 -0
  169. /package/dist/{chunk-WJK2EETW.js.map → chunk-NINO3PI7.js.map} +0 -0
  170. /package/dist/{chunk-PSQKD6ZN.js.map → chunk-PG65AE3I.js.map} +0 -0
  171. /package/dist/{chunk-BZ2XOEHW.js.map → chunk-QOH45QSE.js.map} +0 -0
  172. /package/dist/{chunk-Y7ZYOQNB.js.map → chunk-RSMAVO2Z.js.map} +0 -0
  173. /package/dist/{chunk-GHLGXFNU.js.map → chunk-U6JC3RS2.js.map} +0 -0
  174. /package/dist/{chunk-N4Z6X5BV.js.map → chunk-WFH52OVQ.js.map} +0 -0
@@ -0,0 +1,56 @@
1
+ import { fireEvent, render, screen } from '@testing-library/react';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import { OnboardingCarousel } from '../onboarding-carousel';
4
+
5
+ const STEPS = [
6
+ { id: 'a', icon: null, title: 'First' },
7
+ { id: 'b', icon: null, title: 'Second' },
8
+ { id: 'c', icon: null, title: 'Third' },
9
+ ];
10
+
11
+ const currentSlide = () =>
12
+ screen.getAllByRole('group', { hidden: true }).find(el => el.getAttribute('aria-hidden') === 'false');
13
+
14
+ describe('OnboardingCarousel', () => {
15
+ it('starts on the first step with Previous disabled', () => {
16
+ render(<OnboardingCarousel steps={STEPS} onComplete={vi.fn()} />);
17
+ expect(currentSlide()).toHaveTextContent('First');
18
+ expect(screen.getByRole('button', { name: 'Previous Step' })).toHaveAttribute('aria-disabled', 'true');
19
+ expect(screen.getByRole('button', { name: 'Next Step' })).toBeEnabled();
20
+ });
21
+
22
+ it('walks forward and back', () => {
23
+ render(<OnboardingCarousel steps={STEPS} onComplete={vi.fn()} />);
24
+ fireEvent.click(screen.getByRole('button', { name: 'Next Step' }));
25
+ expect(currentSlide()).toHaveTextContent('Second');
26
+ fireEvent.click(screen.getByRole('button', { name: 'Previous Step' }));
27
+ expect(currentSlide()).toHaveTextContent('First');
28
+ expect(screen.getByText('Step 1 of 3: First')).toHaveAttribute('aria-live', 'polite');
29
+ });
30
+
31
+ it('swaps Next for the complete action on the last step', () => {
32
+ const onComplete = vi.fn();
33
+ render(<OnboardingCarousel steps={STEPS} onComplete={onComplete} />);
34
+ fireEvent.click(screen.getByRole('button', { name: 'Go to step 3' }));
35
+ expect(screen.queryByRole('button', { name: 'Next Step' })).not.toBeInTheDocument();
36
+ fireEvent.click(screen.getByRole('button', { name: 'Start Chat' }));
37
+ expect(onComplete).toHaveBeenCalledTimes(1);
38
+ });
39
+
40
+ it('follows the arrow keys and stops at the ends', () => {
41
+ render(<OnboardingCarousel steps={STEPS} onComplete={vi.fn()} />);
42
+ const region = screen.getByRole('region', { name: 'Introduction' });
43
+ fireEvent.keyDown(region, { key: 'ArrowLeft' });
44
+ expect(currentSlide()).toHaveTextContent('First');
45
+ fireEvent.keyDown(region, { key: 'ArrowRight' });
46
+ fireEvent.keyDown(region, { key: 'ArrowRight' });
47
+ fireEvent.keyDown(region, { key: 'ArrowRight' });
48
+ expect(currentSlide()).toHaveTextContent('Third');
49
+ });
50
+
51
+ it('marks the current dot', () => {
52
+ render(<OnboardingCarousel steps={STEPS} onComplete={vi.fn()} />);
53
+ expect(screen.getByRole('button', { name: 'Go to step 1' })).toHaveAttribute('aria-current', 'step');
54
+ expect(screen.getByRole('button', { name: 'Go to step 2' })).not.toHaveAttribute('aria-current');
55
+ });
56
+ });
@@ -3,6 +3,8 @@
3
3
  import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';
4
4
  import { cn } from '../../utils/cn';
5
5
  import { formatTime } from '../../utils/format-date';
6
+ import { useChatAppearance } from './chat-appearance-context';
7
+ import { CHAT_APPEARANCE } from './types/chat.types';
6
8
 
7
9
  export interface AiAssistantInfoProps extends HTMLAttributes<HTMLDivElement> {
8
10
  /** Rendered inside the standard 48px framed box (typically a 24px icon). */
@@ -34,11 +36,15 @@ export interface AiAssistantInfoProps extends HTMLAttributes<HTMLDivElement> {
34
36
  */
35
37
  const AiAssistantInfo = forwardRef<HTMLDivElement, AiAssistantInfoProps>(
36
38
  ({ className, icon, leading, title, body, timestamp, ...props }, ref) => {
39
+ // v2: on the page surface, content centered against the 48px icon, and
40
+ // the thread (not the card) owns the space below it.
41
+ const isV2 = useChatAppearance() === CHAT_APPEARANCE.V2;
37
42
  return (
38
43
  <div
39
44
  ref={ref}
40
45
  className={cn(
41
- 'mb-[var(--spacing-system-xsf)] flex items-start gap-[var(--spacing-system-s)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-s)]',
46
+ 'flex gap-[var(--spacing-system-s)] rounded-md border border-ods-border p-[var(--spacing-system-s)]',
47
+ isV2 ? 'items-center bg-ods-bg' : 'mb-[var(--spacing-system-xsf)] items-start bg-ods-card',
42
48
  className,
43
49
  )}
44
50
  {...props}
@@ -6,7 +6,9 @@ import { cn } from '../../utils/cn';
6
6
  import { Button } from '../ui/button';
7
7
  import { Tag } from '../ui/tag';
8
8
  import { ApprovalStatusTag } from './approval-batch-message';
9
+ import { useChatAppearance } from './chat-appearance-context';
9
10
  import type { ApprovalRequestMessageProps } from './types';
11
+ import { CHAT_APPEARANCE } from './types/chat.types';
10
12
  import type { ApprovalRequestField } from './types/message.types';
11
13
 
12
14
  /**
@@ -77,6 +79,7 @@ const ApprovalRequestMessage = forwardRef<HTMLDivElement, ApprovalRequestMessage
77
79
  ref,
78
80
  ) => {
79
81
  const [isProcessing, setIsProcessing] = useState(false);
82
+ const isV2 = useChatAppearance() === CHAT_APPEARANCE.V2;
80
83
 
81
84
  const handleApprove = async () => {
82
85
  setIsProcessing(true);
@@ -104,7 +107,9 @@ const ApprovalRequestMessage = forwardRef<HTMLDivElement, ApprovalRequestMessage
104
107
  <div
105
108
  ref={ref}
106
109
  className={cn(
107
- 'mb-[var(--spacing-system-xsf)] flex flex-col gap-[var(--spacing-system-mf)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-mf)]',
110
+ 'flex flex-col gap-[var(--spacing-system-mf)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-mf)]',
111
+ // v2: the thread owns the space below the card.
112
+ !isV2 && 'mb-[var(--spacing-system-xsf)]',
108
113
  className,
109
114
  )}
110
115
  {...props}
@@ -0,0 +1,15 @@
1
+ 'use client';
2
+
3
+ import { createContext, useContext } from 'react';
4
+ import { CHAT_APPEARANCE, type ChatAppearance } from './types/chat.types';
5
+
6
+ /**
7
+ * Thread-wide `ChatAppearance`. `ChatMessageList` provides it, so the blocks a
8
+ * message renders (error, lifecycle receipts) follow the thread without every
9
+ * segment component growing an `appearance` prop.
10
+ */
11
+ export const ChatAppearanceContext = createContext<ChatAppearance>(CHAT_APPEARANCE.CLASSIC);
12
+
13
+ export function useChatAppearance(): ChatAppearance {
14
+ return useContext(ChatAppearanceContext);
15
+ }
@@ -16,10 +16,12 @@ import {
16
16
  import { renderToStaticMarkup } from 'react-dom/server';
17
17
  import { cn } from '../../utils/cn';
18
18
  import { Send01Icon, StopCircleIcon } from '../icons-v2-generated';
19
+ import { AvatarStack } from '../ui/avatar-stack';
19
20
  import { Tag } from '../ui/tag';
20
21
  import { ChatTypingIndicator } from './chat-typing-indicator';
21
22
  import { SlashCommandSuggestions } from './slash-command-suggestions';
22
23
  import type { ChatInputProps, ChatInputRef, MentionMeta, SlashCommandSummary } from './types';
24
+ import { CHAT_APPEARANCE } from './types/chat.types';
23
25
 
24
26
  /** SHARED with `lib/config/slash-commands-config.ts` AND the chat-route slash
25
27
  * dispatch parser. Keep all three in sync. */
@@ -164,6 +166,9 @@ const ChatInput = forwardRef<ChatInputRef, ChatInputProps>((allProps, ref) => {
164
166
  onStop,
165
167
  sending = false,
166
168
  awaitingResponse = false,
169
+ awaitingTeam,
170
+ awaitingTeamCount,
171
+ appearance = CHAT_APPEARANCE.CLASSIC,
167
172
  placeholder = 'Enter your Request...',
168
173
  reserveAvatarOffset: _reserveAvatarOffset,
169
174
  disabled = false,
@@ -560,11 +565,31 @@ const ChatInput = forwardRef<ChatInputRef, ChatInputProps>((allProps, ref) => {
560
565
  className,
561
566
  )}
562
567
  >
563
- {awaitingResponse ? (
568
+ {awaitingResponse && appearance === CHAT_APPEARANCE.CLASSIC ? (
564
569
  <div className="relative flex items-center justify-center gap-[var(--spacing-system-xs)] rounded-md border border-ods-border bg-ods-card px-[var(--spacing-system-s)] py-[var(--spacing-system-s)] transition-colors">
565
570
  <ChatTypingIndicator size="sm" dotClassName="bg-ods-text-primary" />
566
571
  <p className="text-ods-text-secondary text-h4">Waiting for Technician Response</p>
567
572
  </div>
573
+ ) : awaitingResponse ? (
574
+ // Hand-off bar (fae chat v2): who has the conversation now, and that a
575
+ // reply is on its way. Same 48px height as the editor row it replaces,
576
+ // so the thread above does not jump when the chat is handed off.
577
+ <div
578
+ role="status"
579
+ className="flex min-h-11 w-full items-center justify-center gap-[var(--spacing-system-xs)] rounded-md border border-ods-border bg-ods-bg p-[var(--spacing-system-sf)] md:min-h-12"
580
+ >
581
+ {awaitingTeam && awaitingTeam.length > 0 && (
582
+ <AvatarStack
583
+ people={awaitingTeam}
584
+ total={awaitingTeamCount}
585
+ size="xs"
586
+ ringClassName="ring-ods-bg"
587
+ label="Technicians"
588
+ />
589
+ )}
590
+ <p className="min-w-0 truncate text-ods-text-primary text-h4">Handed off to your technical support team</p>
591
+ <ChatTypingIndicator size="sm" dotClassName="bg-ods-text-secondary" />
592
+ </div>
568
593
  ) : (
569
594
  <div className="relative">
570
595
  <SlashCommandSuggestions
@@ -11,6 +11,7 @@ import { SquareAvatar } from '../ui/square-avatar';
11
11
  import { ApprovalBatchMessage } from './approval-batch-message';
12
12
  import { ApprovalRequestMessage } from './approval-request-message';
13
13
  import { AskDisplay } from './ask-display';
14
+ import { ChatAppearanceContext, useChatAppearance } from './chat-appearance-context';
14
15
  import { ChatContextChipStrip } from './chat-context-picker';
15
16
  import type { ChatRef } from './chat-ref.types';
16
17
  import { ContextCompactionDisplay } from './context-compaction-display';
@@ -20,11 +21,13 @@ import { EscalationOfferMessage } from './escalation-offer-message';
20
21
  import { remarkCardLinks } from './remark-card-links';
21
22
  import { remarkMentionChips } from './remark-mention-chips';
22
23
  import { remarkStripCitations } from './remark-strip-citations';
24
+ import { SystemEventMessage } from './system-event-message';
23
25
  import { ThinkingDisplay } from './thinking-display';
24
26
  import { TicketEscalatedMessage } from './ticket-escalated-message';
25
27
  import { TicketEventMessage } from './ticket-event-message';
26
28
  import { ToolExecutionDisplay } from './tool-execution-display';
27
29
  import type { AskSegment, MessageSegment, MessageContent, ChatMessageEnhancedProps } from './types';
30
+ import { CHAT_APPEARANCE } from './types/chat.types';
28
31
 
29
32
  /** Inline `@marker:id` mention token in the message body (sibling of the
30
33
  * `[card://]` grammar) — used to filter out items rendered inline from the
@@ -94,6 +97,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
94
97
  showAvatar = true,
95
98
  assistantType,
96
99
  approvalVariant,
100
+ appearance: appearanceProp,
97
101
  authorType: authorTypeProp,
98
102
  assistantIcon,
99
103
  contextItems,
@@ -110,6 +114,9 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
110
114
  ) => {
111
115
  const isUser = role === 'user';
112
116
  const isError = role === 'error';
117
+ const inheritedAppearance = useChatAppearance();
118
+ const appearance = appearanceProp ?? inheritedAppearance;
119
+ const isV2 = appearance === CHAT_APPEARANCE.V2;
113
120
  const authorType = authorTypeProp ?? (isUser ? 'user' : assistantType === 'mingo' ? 'mingo' : 'fae');
114
121
 
115
122
  // Inline-card rendering uses a HOST-PROVIDED `renderEntityCard` function
@@ -518,7 +525,8 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
518
525
  // (passing pre-joined initials like "PS" would collapse to one letter,
519
526
  // since getFirstLastInitials treats it as a single word).
520
527
  fallback: displayName,
521
- size: 'sm' as const,
528
+ // v2 (fae chat v2): a 24px face that sits on the name's line height.
529
+ size: isV2 ? ('xs' as const) : ('sm' as const),
522
530
  variant: 'round' as const,
523
531
  // User avatar: compact 20×20 with 2px padding and a subtle gray fill
524
532
  // (`bg-ods-card`) so the `border-ods-border` ring stays visible — the
@@ -536,9 +544,32 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
536
544
 
537
545
  const isSystem = authorType === 'system';
538
546
 
547
+ // v2 draws a system line (e.g. a technician joining) as an in-thread
548
+ // receipt card rather than as an author row with no body.
549
+ if (isV2 && isSystem && name) {
550
+ return (
551
+ <div ref={ref} className={cn('relative py-[calc(var(--spacing-system-m)/2)]', className)} {...props}>
552
+ <ChatAppearanceContext.Provider value={appearance}>
553
+ <SystemEventMessage text={name} timestamp={timestamp} />
554
+ </ChatAppearanceContext.Provider>
555
+ </div>
556
+ );
557
+ }
558
+
539
559
  return (
540
- <div ref={ref} className={cn('relative py-[var(--spacing-system-s)]', className)} {...props}>
541
- {/* Message Content — full panel width.
560
+ <ChatAppearanceContext.Provider value={appearance}>
561
+ <div
562
+ ref={ref}
563
+ className={cn(
564
+ 'relative',
565
+ // v2 spaces messages 16px apart (spacing-system-m), split across the
566
+ // two neighbours' padding so blocks outside a row keep the rhythm.
567
+ isV2 ? 'py-[calc(var(--spacing-system-m)/2)]' : 'py-[var(--spacing-system-s)]',
568
+ className,
569
+ )}
570
+ {...props}
571
+ >
572
+ {/* Message Content — full panel width.
542
573
  Avatar is INLINE in the name row below (2025-2026 chat
543
574
  pattern — Claude.ai, ChatGPT, Gemini, Perplexity).
544
575
  Legacy hanging-avatar layout (`absolute -left-16`) wasted
@@ -552,8 +583,8 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
552
583
  layout per drawer-resize frame in Safari / the desktop WKWebView
553
584
  vs ~9ms as grid (Chrome: ~2ms either way). Same gap and stretch,
554
585
  no margin collapsing — visually identical. */}
555
- <div className="grid min-w-0 grid-cols-1 gap-[var(--spacing-system-xxs)]">
556
- {/* Avatar + Name + Timestamp Row.
586
+ <div className="grid min-w-0 grid-cols-1 gap-[var(--spacing-system-xxs)]">
587
+ {/* Avatar + Name + Timestamp Row.
557
588
  Sizing rationale (per design-token measurements):
558
589
  - Name uses `text-h3` = 14px mobile / 18px desktop.
559
590
  - Avatar uses `SquareAvatar size="sm"` = 32px — the
@@ -569,155 +600,161 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
569
600
  inside via `flex items-center justify-center` — they
570
601
  should be sized at ~50-60% of the wrapper (h-4 w-4 =
571
602
  16px works well for a 32px circle). */}
572
- <div className="flex items-center gap-[var(--spacing-system-xs)]">
573
- {/* Avatar rules:
603
+ <div
604
+ className={cn(
605
+ 'flex items-center',
606
+ isV2 ? 'gap-[var(--spacing-system-xxs)]' : 'gap-[var(--spacing-system-xs)]',
607
+ )}
608
+ >
609
+ {/* Avatar rules:
574
610
  - Assistant/Fae always show an avatar — host brand icon when no
575
611
  image is supplied, else the filled SquareAvatar.
576
612
  - User shows the SquareAvatar ONLY when an avatar image actually
577
613
  arrived. With no user avatar we hide the block entirely (just
578
614
  the name), instead of an initials placeholder. TEMPORARY —
579
615
  restore the user placeholder when user avatars ship. */}
580
- {showAvatar &&
581
- !isSystem &&
582
- !(isUser && !avatar) &&
583
- (!isUser && assistantIcon && !avatar ? (
584
- // Host-supplied brand icon (e.g. Mingo): render it directly,
585
- // no filled pill — the icon carries its own brand accent.
586
- <div className="flex flex-shrink-0 items-center justify-center">{assistantIcon}</div>
587
- ) : (
588
- <SquareAvatar {...avatarProps} />
589
- ))}
590
- <span
591
- className={cn(
592
- 'flex-1 !font-mono !font-medium text-h3',
593
- authorType === 'system'
594
- ? 'text-ods-open-yellow'
595
- : authorType === 'admin'
616
+ {showAvatar &&
617
+ !isSystem &&
618
+ !(isUser && !avatar) &&
619
+ (!isUser && assistantIcon && !avatar ? (
620
+ // Host-supplied brand icon (e.g. Mingo): render it directly,
621
+ // no filled pill — the icon carries its own brand accent.
622
+ <div className="flex flex-shrink-0 items-center justify-center">{assistantIcon}</div>
623
+ ) : (
624
+ <SquareAvatar {...avatarProps} />
625
+ ))}
626
+ <span
627
+ className={cn(
628
+ 'flex-1 !font-mono !font-medium text-h3',
629
+ authorType === 'system'
596
630
  ? 'text-ods-open-yellow'
597
- : authorType === 'mingo'
598
- ? 'text-ods-flamingo-cyan'
599
- : authorType === 'fae'
600
- ? 'text-ods-flamingo-pink'
601
- : 'text-ods-text-secondary',
602
- )}
603
- >
604
- {name || (isUser ? 'User' : assistantType === 'mingo' ? 'Mingo' : 'Fae')}
605
- {!isSystem && ':'}
606
- </span>
607
- {timestamp && (
608
- <span className="shrink-0 whitespace-nowrap text-ods-text-secondary text-h6">
609
- {formatMessageTimestamp(timestamp)}
631
+ : authorType === 'admin'
632
+ ? 'text-ods-open-yellow'
633
+ : authorType === 'mingo'
634
+ ? 'text-ods-flamingo-cyan'
635
+ : authorType === 'fae'
636
+ ? 'text-ods-flamingo-pink'
637
+ : 'text-ods-text-secondary',
638
+ )}
639
+ >
640
+ {name || (isUser ? 'User' : assistantType === 'mingo' ? 'Mingo' : 'Fae')}
641
+ {!isSystem && !isV2 && ':'}
610
642
  </span>
611
- )}
612
- </div>
613
-
614
- {/* Message segments — hidden for system messages without content */}
615
- {(!isSystem || segments.length > 0) && (
616
- <div className="grid grid-cols-1 gap-2">
617
- {segments.map((segment, index) => {
618
- // The engine's streaming path (atomic-block memoization +
619
- // fence tail-completion + aria-live) applies ONLY to the
620
- // actively streaming segment: last segment of a message that
621
- // is still typing. On completion `isTyping` flips false and
622
- // the engine does one authoritative whole-document parse.
623
- const segmentIsStreaming = index === segments.length - 1 && !!isTyping;
624
- if (segment.type === 'text') {
625
- return (
626
- <div
627
- key={index}
628
- className={cn(
629
- 'w-full min-w-0 break-words text-h4',
630
- isError ? 'text-ods-error' : 'text-ods-text-primary',
631
- )}
632
- >
633
- {renderSegmentBody(index, segment.text, segmentIsStreaming)}
634
- </div>
635
- );
636
- } else if (segment.type === 'ask') {
637
- // Only the run's head draws — the tail segments are pages of
638
- // the card already rendered above them.
639
- const run = askRuns.get(index);
640
- if (!run) return null;
641
- return <AskDisplay key={index} cards={run} onSelect={onAskSelect} />;
642
- } else if (segment.type === 'tool_execution') {
643
- return (
644
- <ToolExecutionDisplay
645
- key={index}
646
- message={segment.data}
647
- assistantType={assistantType}
648
- variant={approvalVariant}
649
- />
650
- );
651
- } else if (segment.type === 'approval_request') {
652
- return (
653
- <ApprovalRequestMessage
654
- key={index}
655
- data={segment.data}
656
- status={segment.status}
657
- resolvedByName={segment.resolvedByName}
658
- onApprove={segment.onApprove}
659
- onReject={segment.onReject}
660
- assistantType={assistantType}
661
- variant={approvalVariant}
662
- />
663
- );
664
- } else if (segment.type === 'approval_batch') {
665
- return (
666
- <ApprovalBatchMessage
667
- key={index}
668
- data={segment.data}
669
- status={segment.status}
670
- resolvedByName={segment.resolvedByName}
671
- onApprove={segment.onApprove}
672
- onReject={segment.onReject}
673
- assistantType={assistantType}
674
- variant={approvalVariant}
675
- />
676
- );
677
- } else if (segment.type === 'escalation_offer') {
678
- return (
679
- <EscalationOfferMessage
680
- key={index}
681
- data={segment.data}
682
- status={segment.status}
683
- resolvedByName={segment.resolvedByName}
684
- onApprove={segment.onApprove}
685
- onReject={segment.onReject}
686
- />
687
- );
688
- } else if (segment.type === 'ticket_escalated') {
689
- return <TicketEscalatedMessage key={index} data={segment.data} timestamp={timestamp} />;
690
- } else if (segment.type === 'ticket_event') {
691
- // The card's own event time; the bubble timestamp is the
692
- // turn's FIRST row and lags every later lifecycle event.
693
- return (
694
- <TicketEventMessage key={index} data={segment.data} timestamp={segment.occurredAt ?? timestamp} />
695
- );
696
- } else if (segment.type === 'error') {
697
- return <ErrorMessageDisplay key={index} title={segment.title} details={segment.details} />;
698
- } else if (segment.type === 'context_compaction') {
699
- return <ContextCompactionDisplay key={index} status={segment.status} />;
700
- } else if (segment.type === 'thinking') {
701
- const isStreaming = index === segments.length - 1 && isTyping;
702
- return <ThinkingDisplay key={index} text={segment.text} isStreaming={isStreaming} />;
703
- }
704
- return null;
705
- })}
643
+ {timestamp && (
644
+ <span className="shrink-0 whitespace-nowrap text-ods-text-secondary text-h6">
645
+ {formatMessageTimestamp(timestamp)}
646
+ </span>
647
+ )}
706
648
  </div>
707
- )}
708
649
 
709
- {/* Attached entity-context chips (user bubbles). Read-only — no
650
+ {/* Message segments — hidden for system messages without content */}
651
+ {(!isSystem || segments.length > 0) && (
652
+ <div className="grid grid-cols-1 gap-2">
653
+ {segments.map((segment, index) => {
654
+ // The engine's streaming path (atomic-block memoization +
655
+ // fence tail-completion + aria-live) applies ONLY to the
656
+ // actively streaming segment: last segment of a message that
657
+ // is still typing. On completion `isTyping` flips false and
658
+ // the engine does one authoritative whole-document parse.
659
+ const segmentIsStreaming = index === segments.length - 1 && !!isTyping;
660
+ if (segment.type === 'text') {
661
+ return (
662
+ <div
663
+ key={index}
664
+ className={cn(
665
+ 'w-full min-w-0 break-words text-h4',
666
+ isError ? 'text-ods-error' : 'text-ods-text-primary',
667
+ )}
668
+ >
669
+ {renderSegmentBody(index, segment.text, segmentIsStreaming)}
670
+ </div>
671
+ );
672
+ } else if (segment.type === 'ask') {
673
+ // Only the run's head draws — the tail segments are pages of
674
+ // the card already rendered above them.
675
+ const run = askRuns.get(index);
676
+ if (!run) return null;
677
+ return <AskDisplay key={index} cards={run} onSelect={onAskSelect} />;
678
+ } else if (segment.type === 'tool_execution') {
679
+ return (
680
+ <ToolExecutionDisplay
681
+ key={index}
682
+ message={segment.data}
683
+ assistantType={assistantType}
684
+ variant={approvalVariant}
685
+ />
686
+ );
687
+ } else if (segment.type === 'approval_request') {
688
+ return (
689
+ <ApprovalRequestMessage
690
+ key={index}
691
+ data={segment.data}
692
+ status={segment.status}
693
+ resolvedByName={segment.resolvedByName}
694
+ onApprove={segment.onApprove}
695
+ onReject={segment.onReject}
696
+ assistantType={assistantType}
697
+ variant={approvalVariant}
698
+ />
699
+ );
700
+ } else if (segment.type === 'approval_batch') {
701
+ return (
702
+ <ApprovalBatchMessage
703
+ key={index}
704
+ data={segment.data}
705
+ status={segment.status}
706
+ resolvedByName={segment.resolvedByName}
707
+ onApprove={segment.onApprove}
708
+ onReject={segment.onReject}
709
+ assistantType={assistantType}
710
+ variant={approvalVariant}
711
+ />
712
+ );
713
+ } else if (segment.type === 'escalation_offer') {
714
+ return (
715
+ <EscalationOfferMessage
716
+ key={index}
717
+ data={segment.data}
718
+ status={segment.status}
719
+ resolvedByName={segment.resolvedByName}
720
+ onApprove={segment.onApprove}
721
+ onReject={segment.onReject}
722
+ />
723
+ );
724
+ } else if (segment.type === 'ticket_escalated') {
725
+ return <TicketEscalatedMessage key={index} data={segment.data} timestamp={timestamp} />;
726
+ } else if (segment.type === 'ticket_event') {
727
+ // The card's own event time; the bubble timestamp is the
728
+ // turn's FIRST row and lags every later lifecycle event.
729
+ return (
730
+ <TicketEventMessage key={index} data={segment.data} timestamp={segment.occurredAt ?? timestamp} />
731
+ );
732
+ } else if (segment.type === 'error') {
733
+ return <ErrorMessageDisplay key={index} title={segment.title} details={segment.details} />;
734
+ } else if (segment.type === 'context_compaction') {
735
+ return <ContextCompactionDisplay key={index} status={segment.status} />;
736
+ } else if (segment.type === 'thinking') {
737
+ const isStreaming = index === segments.length - 1 && isTyping;
738
+ return <ThinkingDisplay key={index} text={segment.text} isStreaming={isStreaming} />;
739
+ }
740
+ return null;
741
+ })}
742
+ </div>
743
+ )}
744
+
745
+ {/* Attached entity-context chips (user bubbles). Read-only — no
710
746
  remove affordance once the message is sent (Figma 31:28709). */}
711
- {stripContextItems && stripContextItems.length > 0 && (
712
- <ChatContextChipStrip
713
- items={stripContextItems}
714
- resolveIcon={resolveContextIcon}
715
- renderItem={renderContextItem}
716
- className="mt-2"
717
- />
718
- )}
747
+ {stripContextItems && stripContextItems.length > 0 && (
748
+ <ChatContextChipStrip
749
+ items={stripContextItems}
750
+ resolveIcon={resolveContextIcon}
751
+ renderItem={renderContextItem}
752
+ className="mt-2"
753
+ />
754
+ )}
755
+ </div>
719
756
  </div>
720
- </div>
757
+ </ChatAppearanceContext.Provider>
721
758
  );
722
759
  },
723
760
  );
@@ -735,6 +772,7 @@ const MemoizedChatMessageEnhanced = memo(ChatMessageEnhanced, (prevProps, nextPr
735
772
  prevProps.showAvatar === nextProps.showAvatar &&
736
773
  prevProps.assistantType === nextProps.assistantType &&
737
774
  prevProps.approvalVariant === nextProps.approvalVariant &&
775
+ prevProps.appearance === nextProps.appearance &&
738
776
  prevProps.authorType === nextProps.authorType &&
739
777
  prevProps.assistantIcon === nextProps.assistantIcon &&
740
778
  prevProps.className === nextProps.className &&