@flamingo-stack/openframe-frontend-core 0.0.685-snapshot.20260929164409 → 0.0.685

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 (170) hide show
  1. package/dist/{chunk-DL67S7H4.cjs → chunk-5LJZPJHX.cjs} +59 -59
  2. package/dist/{chunk-DL67S7H4.cjs.map → chunk-5LJZPJHX.cjs.map} +1 -1
  3. package/dist/{chunk-IZ3WKG62.js → chunk-65KTXRBY.js} +6 -6
  4. package/dist/{chunk-IBKMIJ7Y.js → chunk-7XF63THA.js} +6 -6
  5. package/dist/{chunk-SI77HF4U.cjs → chunk-A3CSQB4A.cjs} +805 -1247
  6. package/dist/chunk-A3CSQB4A.cjs.map +1 -0
  7. package/dist/{chunk-5MUR43QD.cjs → chunk-B25CF3PO.cjs} +30 -30
  8. package/dist/{chunk-5MUR43QD.cjs.map → chunk-B25CF3PO.cjs.map} +1 -1
  9. package/dist/{chunk-32KBPEF7.js → chunk-B32YLOYQ.js} +4 -4
  10. package/dist/{chunk-OBLED7JY.cjs → chunk-B5N3AHPG.cjs} +81 -81
  11. package/dist/{chunk-OBLED7JY.cjs.map → chunk-B5N3AHPG.cjs.map} +1 -1
  12. package/dist/{chunk-QSRUYHTW.js → chunk-BZ2XOEHW.js} +3 -3
  13. package/dist/{chunk-XATA4OVO.cjs → chunk-C3B36OYZ.cjs} +3 -3
  14. package/dist/{chunk-XATA4OVO.cjs.map → chunk-C3B36OYZ.cjs.map} +1 -1
  15. package/dist/{chunk-V26TKJ4Z.cjs → chunk-CMQXPD5C.cjs} +28 -28
  16. package/dist/{chunk-V26TKJ4Z.cjs.map → chunk-CMQXPD5C.cjs.map} +1 -1
  17. package/dist/{chunk-QNWQQ4RT.cjs → chunk-DQOQ3KTP.cjs} +73 -78
  18. package/dist/chunk-DQOQ3KTP.cjs.map +1 -0
  19. package/dist/{chunk-5HVGOIAE.cjs → chunk-DWXWEGGO.cjs} +22 -22
  20. package/dist/{chunk-5HVGOIAE.cjs.map → chunk-DWXWEGGO.cjs.map} +1 -1
  21. package/dist/{chunk-6CMH53MP.cjs → chunk-EFBMNVSH.cjs} +15 -15
  22. package/dist/{chunk-6CMH53MP.cjs.map → chunk-EFBMNVSH.cjs.map} +1 -1
  23. package/dist/{chunk-DDFYKF3B.js → chunk-GHLGXFNU.js} +3 -3
  24. package/dist/{chunk-VDV7K5TD.js → chunk-GXV6UEVY.js} +3 -3
  25. package/dist/{chunk-AEAFQTVR.cjs → chunk-HMXUOLZR.cjs} +20 -20
  26. package/dist/{chunk-AEAFQTVR.cjs.map → chunk-HMXUOLZR.cjs.map} +1 -1
  27. package/dist/{chunk-2OD2PPNO.cjs → chunk-K2HZG4RM.cjs} +32 -32
  28. package/dist/{chunk-2OD2PPNO.cjs.map → chunk-K2HZG4RM.cjs.map} +1 -1
  29. package/dist/{chunk-JNDHXL2D.cjs → chunk-M4HYIXP6.cjs} +383 -384
  30. package/dist/chunk-M4HYIXP6.cjs.map +1 -0
  31. package/dist/{chunk-KTWJKCUB.js → chunk-N4Z6X5BV.js} +5 -5
  32. package/dist/{chunk-T2RHI676.js → chunk-O7LOZW4R.js} +5 -5
  33. package/dist/{chunk-ZHYYGIAI.cjs → chunk-P2ASEPA2.cjs} +6 -6
  34. package/dist/{chunk-ZHYYGIAI.cjs.map → chunk-P2ASEPA2.cjs.map} +1 -1
  35. package/dist/{chunk-CHRDLXXX.js → chunk-PSQKD6ZN.js} +3 -3
  36. package/dist/{chunk-GSJR33UP.cjs → chunk-QHLJSB2J.cjs} +12 -12
  37. package/dist/{chunk-GSJR33UP.cjs.map → chunk-QHLJSB2J.cjs.map} +1 -1
  38. package/dist/{chunk-CFL3EW4V.cjs → chunk-T4DUTP5C.cjs} +5 -5
  39. package/dist/{chunk-CFL3EW4V.cjs.map → chunk-T4DUTP5C.cjs.map} +1 -1
  40. package/dist/{chunk-22RSJ57Z.js → chunk-T6KWNJDW.js} +465 -470
  41. package/dist/chunk-T6KWNJDW.js.map +1 -0
  42. package/dist/{chunk-2FGFB2HY.cjs → chunk-TJI5V5BR.cjs} +7 -7
  43. package/dist/{chunk-2FGFB2HY.cjs.map → chunk-TJI5V5BR.cjs.map} +1 -1
  44. package/dist/{chunk-HLEXYTOE.js → chunk-TUG7IT6H.js} +2 -2
  45. package/dist/{chunk-DWCVA6XT.cjs → chunk-U3IXG7TR.cjs} +33 -33
  46. package/dist/{chunk-DWCVA6XT.cjs.map → chunk-U3IXG7TR.cjs.map} +1 -1
  47. package/dist/{chunk-WJ5XN4AL.js → chunk-UT333EOT.js} +4 -4
  48. package/dist/{chunk-OTV4W3YD.js → chunk-VDPCK437.js} +5 -6
  49. package/dist/{chunk-OTV4W3YD.js.map → chunk-VDPCK437.js.map} +1 -1
  50. package/dist/{chunk-5GK7TU2Q.js → chunk-WJK2EETW.js} +2 -2
  51. package/dist/{chunk-YPXXXX6R.js → chunk-XGX6JSEU.js} +1644 -2086
  52. package/dist/chunk-XGX6JSEU.js.map +1 -0
  53. package/dist/{chunk-XODE4YTG.js → chunk-XW7MUIW4.js} +3 -3
  54. package/dist/{chunk-N74MQ4HQ.js → chunk-Y7ZYOQNB.js} +3 -3
  55. package/dist/{chunk-2NUB5EMG.cjs → chunk-YCTVLN47.cjs} +10 -10
  56. package/dist/{chunk-2NUB5EMG.cjs.map → chunk-YCTVLN47.cjs.map} +1 -1
  57. package/dist/{chunk-TILC52ZF.js → chunk-ZF4Z36G4.js} +7 -7
  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-input.d.ts.map +1 -1
  63. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  64. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  65. package/dist/components/chat/error-message-display.d.ts.map +1 -1
  66. package/dist/components/chat/index.cjs +3 -21
  67. package/dist/components/chat/index.cjs.map +1 -1
  68. package/dist/components/chat/index.d.ts +0 -6
  69. package/dist/components/chat/index.d.ts.map +1 -1
  70. package/dist/components/chat/index.js +2 -20
  71. package/dist/components/chat/msp-organization-card.d.ts +1 -7
  72. package/dist/components/chat/msp-organization-card.d.ts.map +1 -1
  73. package/dist/components/chat/types/chat.types.d.ts +0 -13
  74. package/dist/components/chat/types/chat.types.d.ts.map +1 -1
  75. package/dist/components/chat/types/component.types.d.ts +1 -20
  76. package/dist/components/chat/types/component.types.d.ts.map +1 -1
  77. package/dist/components/contact/index.cjs +5 -5
  78. package/dist/components/contact/index.js +4 -4
  79. package/dist/components/docs/index.cjs +8 -8
  80. package/dist/components/docs/index.js +7 -7
  81. package/dist/components/embeds/index.cjs +5 -5
  82. package/dist/components/embeds/index.js +4 -4
  83. package/dist/components/faq/index.cjs +5 -5
  84. package/dist/components/faq/index.js +4 -4
  85. package/dist/components/features/index.cjs +4 -4
  86. package/dist/components/features/index.js +3 -3
  87. package/dist/components/help-center-pages/index.cjs +91 -91
  88. package/dist/components/help-center-pages/index.js +15 -15
  89. package/dist/components/index.cjs +55 -73
  90. package/dist/components/index.cjs.map +1 -1
  91. package/dist/components/index.js +15 -33
  92. package/dist/components/index.js.map +1 -1
  93. package/dist/components/meeting-scheduler/index.cjs +41 -41
  94. package/dist/components/meeting-scheduler/index.js +3 -3
  95. package/dist/components/navigation/index.cjs +7 -7
  96. package/dist/components/navigation/index.js +6 -6
  97. package/dist/components/onboarding-guides/index.cjs +7 -7
  98. package/dist/components/onboarding-guides/index.js +6 -6
  99. package/dist/components/related-content/index.cjs +3 -3
  100. package/dist/components/related-content/index.js +2 -2
  101. package/dist/components/tickets/index.cjs +8 -8
  102. package/dist/components/tickets/index.js +7 -7
  103. package/dist/components/ui/avatar-stack.d.ts +1 -5
  104. package/dist/components/ui/avatar-stack.d.ts.map +1 -1
  105. package/dist/components/ui/index.cjs +4 -22
  106. package/dist/components/ui/index.cjs.map +1 -1
  107. package/dist/components/ui/index.js +3 -21
  108. package/dist/components/ui/reopen-ticket-request-modal.d.ts +1 -4
  109. package/dist/components/ui/reopen-ticket-request-modal.d.ts.map +1 -1
  110. package/dist/index.cjs +9 -27
  111. package/dist/index.cjs.map +1 -1
  112. package/dist/index.js +7 -25
  113. package/dist/index.js.map +1 -1
  114. package/package.json +1 -1
  115. package/src/components/chat/ai-assistant-info.tsx +1 -7
  116. package/src/components/chat/approval-request-message.tsx +1 -6
  117. package/src/components/chat/chat-input.tsx +1 -26
  118. package/src/components/chat/chat-message-enhanced.tsx +142 -180
  119. package/src/components/chat/chat-message-list.tsx +164 -169
  120. package/src/components/chat/error-message-display.tsx +3 -21
  121. package/src/components/chat/index.ts +0 -6
  122. package/src/components/chat/msp-organization-card.tsx +0 -35
  123. package/src/components/chat/types/chat.types.ts +0 -15
  124. package/src/components/chat/types/component.types.ts +0 -20
  125. package/src/components/ui/avatar-stack.tsx +5 -15
  126. package/src/components/ui/reopen-ticket-request-modal.tsx +2 -6
  127. package/src/styles/ods-colors.css +0 -10
  128. package/src/styles/ods-effects.css +0 -13
  129. package/dist/chunk-22RSJ57Z.js.map +0 -1
  130. package/dist/chunk-JNDHXL2D.cjs.map +0 -1
  131. package/dist/chunk-QNWQQ4RT.cjs.map +0 -1
  132. package/dist/chunk-SI77HF4U.cjs.map +0 -1
  133. package/dist/chunk-YPXXXX6R.js.map +0 -1
  134. package/dist/components/chat/chat-appearance-context.d.ts +0 -9
  135. package/dist/components/chat/chat-appearance-context.d.ts.map +0 -1
  136. package/dist/components/chat/chat-nav-sidebar.d.ts +0 -50
  137. package/dist/components/chat/chat-nav-sidebar.d.ts.map +0 -1
  138. package/dist/components/chat/chat-top-navigation.d.ts +0 -29
  139. package/dist/components/chat/chat-top-navigation.d.ts.map +0 -1
  140. package/dist/components/chat/onboarding-carousel.d.ts +0 -29
  141. package/dist/components/chat/onboarding-carousel.d.ts.map +0 -1
  142. package/dist/components/chat/system-event-message.d.ts +0 -14
  143. package/dist/components/chat/system-event-message.d.ts.map +0 -1
  144. package/src/components/chat/__tests__/chat-appearance.test.tsx +0 -79
  145. package/src/components/chat/__tests__/onboarding-carousel.test.tsx +0 -56
  146. package/src/components/chat/chat-appearance-context.tsx +0 -15
  147. package/src/components/chat/chat-nav-sidebar.tsx +0 -163
  148. package/src/components/chat/chat-top-navigation.tsx +0 -109
  149. package/src/components/chat/onboarding-carousel.tsx +0 -162
  150. package/src/components/chat/system-event-message.tsx +0 -54
  151. package/src/components/ui/__tests__/avatar-stack.test.tsx +0 -25
  152. package/src/stories/FaeLayoutV2.stories.tsx +0 -150
  153. package/src/stories/FaeOnboardingV2.stories.tsx +0 -68
  154. package/src/stories/FaeThreadV2.stories.tsx +0 -192
  155. package/src/stories/fae-brand-decorator.tsx +0 -28
  156. /package/dist/{chunk-IZ3WKG62.js.map → chunk-65KTXRBY.js.map} +0 -0
  157. /package/dist/{chunk-IBKMIJ7Y.js.map → chunk-7XF63THA.js.map} +0 -0
  158. /package/dist/{chunk-32KBPEF7.js.map → chunk-B32YLOYQ.js.map} +0 -0
  159. /package/dist/{chunk-QSRUYHTW.js.map → chunk-BZ2XOEHW.js.map} +0 -0
  160. /package/dist/{chunk-DDFYKF3B.js.map → chunk-GHLGXFNU.js.map} +0 -0
  161. /package/dist/{chunk-VDV7K5TD.js.map → chunk-GXV6UEVY.js.map} +0 -0
  162. /package/dist/{chunk-KTWJKCUB.js.map → chunk-N4Z6X5BV.js.map} +0 -0
  163. /package/dist/{chunk-T2RHI676.js.map → chunk-O7LOZW4R.js.map} +0 -0
  164. /package/dist/{chunk-CHRDLXXX.js.map → chunk-PSQKD6ZN.js.map} +0 -0
  165. /package/dist/{chunk-HLEXYTOE.js.map → chunk-TUG7IT6H.js.map} +0 -0
  166. /package/dist/{chunk-WJ5XN4AL.js.map → chunk-UT333EOT.js.map} +0 -0
  167. /package/dist/{chunk-5GK7TU2Q.js.map → chunk-WJK2EETW.js.map} +0 -0
  168. /package/dist/{chunk-XODE4YTG.js.map → chunk-XW7MUIW4.js.map} +0 -0
  169. /package/dist/{chunk-N74MQ4HQ.js.map → chunk-Y7ZYOQNB.js.map} +0 -0
  170. /package/dist/{chunk-TILC52ZF.js.map → chunk-ZF4Z36G4.js.map} +0 -0
@@ -1,162 +0,0 @@
1
- 'use client';
2
-
3
- import { type CSSProperties, type KeyboardEvent, type ReactNode, useState } from 'react';
4
- import { cn } from '../../utils/cn';
5
- import { Button } from '../ui/button';
6
-
7
- export interface OnboardingCarouselStep {
8
- id: string;
9
- /** Glyph in the 64px tile above the title. Pass it sized to 28px. */
10
- icon: ReactNode;
11
- title: ReactNode;
12
- description?: ReactNode;
13
- }
14
-
15
- export interface OnboardingCarouselProps {
16
- steps: OnboardingCarouselStep[];
17
- /** The last step's primary action. */
18
- onComplete: () => void;
19
- /** Label of the last step's primary action. */
20
- completeLabel?: string;
21
- previousLabel?: string;
22
- nextLabel?: string;
23
- /** Accessible name of the carousel region. */
24
- label?: string;
25
- className?: string;
26
- }
27
-
28
- /**
29
- * Step-by-step introduction: one card at a time in the middle, its neighbours
30
- * peeking in from the sides under an edge fade, dots below, Previous / Next
31
- * underneath. The last step swaps Next for `completeLabel`. There is no skip:
32
- * the design walks every step. Fills the height its parent gives it: the cards
33
- * stretch, the dots and buttons keep their size.
34
- */
35
- export function OnboardingCarousel({
36
- steps,
37
- onComplete,
38
- completeLabel = 'Start Chat',
39
- previousLabel = 'Previous Step',
40
- nextLabel = 'Next Step',
41
- label = 'Introduction',
42
- className,
43
- }: OnboardingCarouselProps) {
44
- const [index, setIndex] = useState(0);
45
- const lastIndex = steps.length - 1;
46
- const isFirst = index === 0;
47
- const isLast = index === lastIndex;
48
-
49
- const goTo = (next: number) => setIndex(Math.min(Math.max(next, 0), lastIndex));
50
-
51
- const handleKeyDown = (event: KeyboardEvent<HTMLElement>) => {
52
- if (event.key === 'ArrowRight') goTo(index + 1);
53
- else if (event.key === 'ArrowLeft') goTo(index - 1);
54
- else return;
55
- event.preventDefault();
56
- };
57
-
58
- // Card width and the gap between cards drive both the card size and the
59
- // track offset, so they live in one place. Percentages resolve against the
60
- // viewport width in both uses (the track is as wide as the viewport).
61
- const trackStyle = {
62
- '--onboarding-card-w': 'min(600px, calc(100% - 2 * var(--spacing-system-xl)))',
63
- '--onboarding-gap': 'var(--spacing-system-lf)',
64
- transform: `translateX(calc(${-index} * (var(--onboarding-card-w) + var(--onboarding-gap))))`,
65
- } as CSSProperties;
66
-
67
- // The design's large steps (card padding, dots to buttons) are 80px at the
68
- // app's 1024px default width, where `--spacing-system-xxl` is still 48px:
69
- // spelled as twice the fixed 40px step.
70
- return (
71
- <section
72
- aria-roledescription="carousel"
73
- aria-label={label}
74
- onKeyDown={handleKeyDown}
75
- className={cn(
76
- 'flex min-h-0 w-full flex-1 flex-col items-center gap-[calc(2*var(--spacing-system-xlf))]',
77
- className,
78
- )}
79
- >
80
- <div className="flex min-h-0 w-full flex-1 flex-col items-center gap-[var(--spacing-system-xs)]">
81
- <div className="relative flex min-h-0 w-full flex-1 overflow-hidden">
82
- <div
83
- className="flex w-full gap-[var(--onboarding-gap)] transition-transform duration-300 ease-out motion-reduce:transition-none"
84
- style={trackStyle}
85
- >
86
- {steps.map((step, i) => {
87
- const isCurrent = i === index;
88
- return (
89
- <div
90
- key={step.id}
91
- role="group"
92
- aria-roledescription="slide"
93
- aria-label={`${i + 1} of ${steps.length}`}
94
- aria-hidden={!isCurrent}
95
- inert={!isCurrent || undefined}
96
- className={cn(
97
- 'flex w-[var(--onboarding-card-w)] shrink-0 flex-col items-center justify-center gap-[var(--spacing-system-m)] rounded-md border border-ods-border bg-ods-card px-[var(--spacing-system-xl)] py-[calc(2*var(--spacing-system-xlf))] text-center',
98
- i === 0 && 'ml-[calc((100%-var(--onboarding-card-w))/2)]',
99
- )}
100
- >
101
- <span className="flex size-16 shrink-0 items-center justify-center rounded-md border border-ods-border bg-ods-bg text-ods-accent">
102
- {step.icon}
103
- </span>
104
- <div className="flex flex-col gap-[var(--spacing-system-l)] text-ods-text-primary [word-break:break-word]">
105
- <h2 className="text-h2">{step.title}</h2>
106
- {step.description && <p className="whitespace-pre-line text-h6">{step.description}</p>}
107
- </div>
108
- </div>
109
- );
110
- })}
111
- </div>
112
- <div className="pointer-events-none absolute inset-y-0 left-0 w-[120px] bg-gradient-to-r from-ods-bg to-transparent" />
113
- <div className="pointer-events-none absolute inset-y-0 right-0 w-[120px] bg-gradient-to-l from-ods-bg to-transparent" />
114
- </div>
115
-
116
- {/* The slides only toggle visibility, which is not announced. */}
117
- <p className="sr-only" aria-live="polite">
118
- Step {index + 1} of {steps.length}: {steps[index]?.title}
119
- </p>
120
-
121
- <div className="flex items-center" role="group" aria-label="Choose step">
122
- {steps.map((step, i) => {
123
- const isCurrent = i === index;
124
- return (
125
- <Button
126
- key={step.id}
127
- variant="transparent"
128
- aria-current={isCurrent ? 'step' : undefined}
129
- aria-label={`Go to step ${i + 1}`}
130
- onClick={() => goTo(i)}
131
- className="flex size-6 shrink-0 items-center justify-center rounded-full p-0 md:h-6"
132
- >
133
- <span
134
- className={cn(
135
- 'size-1.5 rounded-full transition-colors duration-150',
136
- isCurrent ? 'bg-ods-accent' : 'bg-ods-text-secondary',
137
- )}
138
- />
139
- </Button>
140
- );
141
- })}
142
- </div>
143
- </div>
144
-
145
- <div className="flex flex-wrap items-center justify-center gap-[var(--spacing-system-m)]">
146
- {/* aria-disabled, not disabled: stepping back onto the first card must
147
- not drop keyboard focus from the button the user just pressed. */}
148
- <Button
149
- variant="outline"
150
- className="w-[200px]"
151
- aria-disabled={isFirst || undefined}
152
- onClick={() => goTo(index - 1)}
153
- >
154
- {previousLabel}
155
- </Button>
156
- <Button variant="accent" className="w-[200px]" onClick={isLast ? onComplete : () => goTo(index + 1)}>
157
- {isLast ? completeLabel : nextLabel}
158
- </Button>
159
- </div>
160
- </section>
161
- );
162
- }
@@ -1,54 +0,0 @@
1
- 'use client';
2
-
3
- import { forwardRef, type HTMLAttributes } from 'react';
4
- import { InfoCircleIcon } from '../icons-v2-generated';
5
- import { SquareAvatar } from '../ui/square-avatar';
6
- import { AiAssistantInfo } from './ai-assistant-info';
7
-
8
- // The chat backend announces a technician taking the chat over (or handing it
9
- // back) as a plain system line: "{name} joined the chat" / "{name} left the
10
- // chat". The copy is fixed server-side, so these two shapes are the contract.
11
- const JOINED = /^(.+) joined the chat$/;
12
- const LEFT = /^(.+) left the chat$/;
13
-
14
- export interface SystemEventMessageProps extends HTMLAttributes<HTMLDivElement> {
15
- /** The system line as the backend sent it. */
16
- text: string;
17
- timestamp?: Date;
18
- }
19
-
20
- /**
21
- * A system line in the fae chat v2 thread (`ai-assistant-info`,
22
- * type=direct-chat): a technician joining becomes "Technician Joined" with
23
- * their initials; any other line keeps its own wording under an info icon.
24
- */
25
- const SystemEventMessage = forwardRef<HTMLDivElement, SystemEventMessageProps>(({ text, timestamp, ...props }, ref) => {
26
- const joined = JOINED.exec(text)?.[1];
27
- if (joined) {
28
- return (
29
- <AiAssistantInfo
30
- ref={ref}
31
- leading={<SquareAvatar alt={joined} fallback={joined} size="lg" variant="round" className="shrink-0" />}
32
- title="Technician Joined"
33
- body={`You're now chatting with ${joined}.`}
34
- timestamp={timestamp}
35
- {...props}
36
- />
37
- );
38
- }
39
- const left = LEFT.exec(text)?.[1];
40
- return (
41
- <AiAssistantInfo
42
- ref={ref}
43
- icon={<InfoCircleIcon className="size-6 text-ods-accent" />}
44
- title={left ? 'Technician Left' : text}
45
- body={left ? `${left} left the conversation.` : undefined}
46
- timestamp={timestamp}
47
- {...props}
48
- />
49
- );
50
- });
51
-
52
- SystemEventMessage.displayName = 'SystemEventMessage';
53
-
54
- export { SystemEventMessage };
@@ -1,25 +0,0 @@
1
- import { render, screen } from '@testing-library/react';
2
- import { describe, expect, it } from 'vitest';
3
- import { AvatarStack } from '../avatar-stack';
4
-
5
- const people = [{ name: 'Ada Lin' }, { name: 'Roman Smith' }, { name: 'Michael Johnson' }];
6
-
7
- describe('AvatarStack', () => {
8
- it('counts the overflow from the people it gets', () => {
9
- render(<AvatarStack people={[...people, { name: 'Kim Park' }]} max={3} label="Technicians" />);
10
- expect(screen.getByText('+1')).toHaveAttribute('title', 'Kim Park');
11
- });
12
-
13
- it('counts everyone not shown when the people are a sample of a larger group', () => {
14
- render(<AvatarStack people={people} total={7} max={3} label="Technicians" />);
15
- expect(screen.getByText('+4')).toHaveAttribute('title', '4 more');
16
- expect(screen.getByRole('group')).toHaveAccessibleName(
17
- 'Technicians: Ada Lin, Roman Smith, Michael Johnson and 4 more',
18
- );
19
- });
20
-
21
- it('shows no overflow when the sample is the whole group', () => {
22
- render(<AvatarStack people={people} total={3} max={3} label="Technicians" />);
23
- expect(screen.queryByText(/^\+/)).not.toBeInTheDocument();
24
- });
25
- });
@@ -1,150 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/nextjs-vite';
2
- import { useState } from 'react';
3
-
4
- import { ChatInput } from '../components/chat/chat-input';
5
- import { ChatListEmptyState } from '../components/chat/chat-list-empty-state';
6
- import { ChatMessageList } from '../components/chat/chat-message-list';
7
- import { ChatListItem, ChatNavSidebar } from '../components/chat/chat-nav-sidebar';
8
- import { ChatTopNavigation } from '../components/chat/chat-top-navigation';
9
- import { MspOrganizationCard } from '../components/chat/msp-organization-card';
10
- import type { Message } from '../components/chat/types/message.types';
11
- import { OpenFrameLogo } from '../components/icons';
12
- import {
13
- ChatPlusIcon,
14
- ChatQuestionIcon,
15
- ChatsIcon,
16
- CheckCircleIcon,
17
- ClockHistoryIcon,
18
- DotsLoaderIcon,
19
- HourglassClockIcon,
20
- MessageIndicatorIcon,
21
- UserCheckIcon,
22
- } from '../components/icons-v2-generated';
23
- import { Button } from '../components/ui/button';
24
- import { withFaeBrand } from './fae-brand-decorator';
25
-
26
- const at = (minute: number) => new Date(2026, 8, 29, 14, minute);
27
-
28
- const THREAD: Message[] = [
29
- {
30
- id: 'u1',
31
- role: 'user',
32
- name: 'John Smith',
33
- content: 'My laptop has been running really slowly lately',
34
- timestamp: at(47),
35
- },
36
- {
37
- id: 'a1',
38
- role: 'assistant',
39
- name: 'Fae',
40
- assistantType: 'fae',
41
- content: "I'll run a quick diagnostic to identify what's causing the performance issues.",
42
- timestamp: at(47),
43
- },
44
- ];
45
-
46
- const CHATS = [
47
- { id: '1', title: 'Slow Laptop', mark: <DotsLoaderIcon className="text-ods-accent" /> },
48
- { id: '2', title: 'Forgot my password' },
49
- { id: '3', title: 'Teams mic not working', mark: <HourglassClockIcon className="text-ods-accent" /> },
50
- { id: '4', title: 'Email not syncing on phone', mark: <MessageIndicatorIcon className="text-ods-accent" /> },
51
- { id: '5', title: 'Blue screen on startup', resolved: true },
52
- ];
53
-
54
- function Layout({ withChats, withThread }: { withChats: boolean; withThread: boolean }) {
55
- const [sidebarOpen, setSidebarOpen] = useState(true);
56
- return (
57
- <div data-surface="brand-tinted" className="flex h-[800px] w-[1024px] border-t border-ods-border bg-ods-bg">
58
- {sidebarOpen && (
59
- <ChatNavSidebar
60
- logo={
61
- <span className="flex items-center gap-[var(--spacing-system-xxs)] text-ods-text-primary">
62
- <OpenFrameLogo
63
- className="size-6"
64
- lowerPathColor="var(--ods-open-yellow-base)"
65
- upperPathColor="var(--color-text-primary)"
66
- />
67
- <span className="text-wordmark">OpenFrame</span>
68
- </span>
69
- }
70
- items={[
71
- { id: 'new', label: 'Start New Chat', icon: <ChatPlusIcon />, active: !withThread },
72
- { id: 'archive', label: 'Chat Archive', icon: <ClockHistoryIcon /> },
73
- { id: 'guide', label: 'What to ask Fae?', icon: <ChatQuestionIcon /> },
74
- ]}
75
- sectionLabel="Your Current Chats"
76
- footer={<MspOrganizationCard variant="footer" name="TechFlow Solutions" onOpenWebsite={() => undefined} />}
77
- >
78
- {withChats ? (
79
- CHATS.map((chat, i) => (
80
- <ChatListItem
81
- key={chat.id}
82
- title={chat.title}
83
- subtitle="Ticket Number: 1932"
84
- active={withThread && i === 0}
85
- leading={chat.resolved ? <CheckCircleIcon className="text-ods-success" /> : undefined}
86
- trailing={chat.mark}
87
- />
88
- ))
89
- ) : (
90
- <ChatListEmptyState
91
- icon={<ChatsIcon size={24} />}
92
- title="No Current Chats"
93
- description="Your conversations will show here"
94
- />
95
- )}
96
- </ChatNavSidebar>
97
- )}
98
- <div className="flex min-w-0 flex-1 flex-col">
99
- <ChatTopNavigation
100
- name="Fae"
101
- subtitle="Your AI Assistant"
102
- sidebarOpen={sidebarOpen}
103
- onToggleSidebar={() => setSidebarOpen(open => !open)}
104
- onOpenSettings={() => undefined}
105
- actions={
106
- <Button variant="outline" size="small" leftIcon={<UserCheckIcon />}>
107
- Request a Technician
108
- </Button>
109
- }
110
- />
111
- <main className="ods-glow-accent-corner flex min-h-0 flex-1 flex-col gap-[var(--spacing-system-m)] px-[var(--spacing-system-m)] pb-[var(--spacing-system-m)] pt-[var(--spacing-system-mf)]">
112
- {withThread ? (
113
- <ChatMessageList
114
- messages={THREAD}
115
- assistantType="fae"
116
- approvalVariant="client"
117
- appearance="v2"
118
- fullWidth
119
- contentClassName="px-0"
120
- autoScroll={false}
121
- />
122
- ) : (
123
- <div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-[var(--spacing-system-l)]">
124
- <div className="flex flex-col gap-[var(--spacing-system-xsf)] text-center">
125
- <h1 className="text-ods-text-primary text-h2">Hi, I&apos;m your IT assistant</h1>
126
- <p className="text-ods-text-secondary text-h4">
127
- Having trouble with your computer, email, Wi-Fi, or software? Just describe it.
128
- </p>
129
- </div>
130
- </div>
131
- )}
132
- <ChatInput fullWidth appearance="v2" placeholder="Enter your request here..." />
133
- </main>
134
- </div>
135
- </div>
136
- );
137
- }
138
-
139
- const meta: Meta<typeof Layout> = {
140
- decorators: [withFaeBrand],
141
- title: 'Chat/Fae Layout v2',
142
- component: Layout,
143
- parameters: { layout: 'fullscreen' },
144
- };
145
-
146
- export default meta;
147
- type Story = StoryObj<typeof Layout>;
148
-
149
- export const StartEmpty: Story = { args: { withChats: false, withThread: false } };
150
- export const ChatOpen: Story = { args: { withChats: true, withThread: true } };
@@ -1,68 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/nextjs-vite';
2
-
3
- import { OnboardingCarousel, type OnboardingCarouselStep } from '../components/chat/onboarding-carousel';
4
- import { PoweredByFlamingo } from '../components/features/auth/auth-branding';
5
- import { OpenFrameLogo } from '../components/icons';
6
- import { ClockCheckIcon, MessagesIcon, UserCheckIcon } from '../components/icons-v2-generated';
7
- import { SquareAvatar } from '../components/ui/square-avatar';
8
- import { withFaeBrand } from './fae-brand-decorator';
9
-
10
- const STEPS: OnboardingCarouselStep[] = [
11
- {
12
- id: 'meet',
13
- icon: <SquareAvatar variant="round" sizePx={28} fallback="Fae" />,
14
- title: 'Meet Your AI IT Assistant',
15
- description: 'Fixes what it can right away,\nand hands off the rest to your technicians.',
16
- },
17
- {
18
- id: 'type',
19
- icon: <MessagesIcon size={28} />,
20
- title: "Type what's going on.",
21
- description:
22
- "Your assistant troubleshoots first.\nAnything it can't solve goes to your technician with full context.",
23
- },
24
- {
25
- id: 'human',
26
- icon: <UserCheckIcon size={28} />,
27
- title: 'Need a human?',
28
- description: 'Skip the chat and create a ticket.\nIt goes straight to your technicians.',
29
- },
30
- {
31
- id: 'anytime',
32
- icon: <ClockCheckIcon size={28} />,
33
- title: 'Ask anything, anytime.',
34
- description: 'No hold music, no queue.\nJust immediate help.',
35
- },
36
- ];
37
-
38
- function Screen() {
39
- return (
40
- <div className="flex h-[800px] w-[1024px] flex-col items-center gap-[calc(2*var(--spacing-system-xlf))] bg-ods-bg px-[calc(2*var(--spacing-system-xlf))] py-[var(--spacing-system-xl)]">
41
- <span className="flex items-center gap-[var(--spacing-system-xxs)] text-ods-text-primary">
42
- <OpenFrameLogo
43
- className="size-6"
44
- lowerPathColor="var(--ods-open-yellow-base)"
45
- upperPathColor="var(--color-text-primary)"
46
- />
47
- <span className="text-wordmark">OpenFrame</span>
48
- </span>
49
- {/* The side cards run to the window edge, past the screen padding. */}
50
- <div className="-mx-[calc(2*var(--spacing-system-xlf))] flex min-h-0 flex-1 self-stretch">
51
- <OnboardingCarousel steps={STEPS} onComplete={() => undefined} />
52
- </div>
53
- <PoweredByFlamingo />
54
- </div>
55
- );
56
- }
57
-
58
- const meta: Meta<typeof Screen> = {
59
- decorators: [withFaeBrand],
60
- title: 'Chat/Fae Onboarding v2',
61
- component: Screen,
62
- parameters: { layout: 'fullscreen' },
63
- };
64
-
65
- export default meta;
66
- type Story = StoryObj<typeof Screen>;
67
-
68
- export const Introduction: Story = {};
@@ -1,192 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/nextjs-vite';
2
-
3
- import { ChatInput } from '../components/chat/chat-input';
4
- import { ChatMessageList } from '../components/chat/chat-message-list';
5
- import type { ChatAppearance } from '../components/chat/types/chat.types';
6
- import type { Message } from '../components/chat/types/message.types';
7
- import { withFaeBrand } from './fae-brand-decorator';
8
-
9
- const at = (minute: number) => new Date(2026, 8, 29, 14, minute);
10
-
11
- // The "Admin joins" frame of the fae chat v2 escalation flow: a user request,
12
- // an escalation offer the user approved, the hand-off and join receipts, then
13
- // the technician's reply.
14
- const ESCALATION_THREAD: Message[] = [
15
- {
16
- id: 'u1',
17
- role: 'user',
18
- name: 'John Smith',
19
- content: 'My laptop has been running really slowly lately',
20
- timestamp: at(47),
21
- },
22
- {
23
- id: 'a1',
24
- role: 'assistant',
25
- name: 'Fae',
26
- assistantType: 'fae',
27
- content: 'Sorry, I can’t help with this problem',
28
- timestamp: at(47),
29
- },
30
- { id: 'u2', role: 'user', name: 'John Smith', content: 'call the admin', timestamp: at(47) },
31
- {
32
- id: 'a2',
33
- role: 'assistant',
34
- name: 'Fae',
35
- assistantType: 'fae',
36
- timestamp: at(47),
37
- content: [
38
- {
39
- type: 'escalation_offer',
40
- status: 'approved',
41
- resolvedByName: 'User',
42
- data: {
43
- offerId: 'offer-1',
44
- text: 'Having issues with the AI assistant? This ticket can be handed off to a technician. Fae will no longer respond in this conversation. A technician will review the ticket and reply when available.',
45
- },
46
- },
47
- { type: 'ticket_escalated', data: { ticketId: 't-1', reason: 'MANUAL' } },
48
- {
49
- type: 'error',
50
- title: 'AI response error',
51
- details: 'Something went wrong. Please try again.',
52
- },
53
- ],
54
- },
55
- {
56
- id: 's1',
57
- role: 'user',
58
- authorType: 'system',
59
- name: 'Michael Johnson joined the chat',
60
- content: '',
61
- timestamp: at(48),
62
- },
63
- {
64
- id: 't1',
65
- role: 'assistant',
66
- name: 'Michael Johnson',
67
- authorType: 'admin',
68
- content: 'I’ll do it faster',
69
- timestamp: at(48),
70
- },
71
- { id: 'u3', role: 'user', name: 'John Smith', content: 'Cool!', timestamp: at(48) },
72
- ];
73
-
74
- const APPROVAL_THREAD: Message[] = [
75
- {
76
- id: 'u1',
77
- role: 'user',
78
- name: 'John Smith',
79
- content: 'My laptop has been running really slowly lately',
80
- timestamp: at(47),
81
- },
82
- {
83
- id: 'a1',
84
- role: 'assistant',
85
- name: 'Fae',
86
- assistantType: 'fae',
87
- content:
88
- 'I found that your C: drive is 89% full. I can clean up temporary files to free some space - may I proceed with clearing C:/temp?',
89
- timestamp: at(47),
90
- },
91
- {
92
- id: 'a2',
93
- role: 'assistant',
94
- name: 'Fae',
95
- assistantType: 'fae',
96
- timestamp: at(47),
97
- content: [
98
- {
99
- type: 'approval_request',
100
- status: 'pending',
101
- data: {
102
- requestId: 'req-1',
103
- command: 'Remove-Item C:\\Temp\\* -Recurse',
104
- explanation: 'Clears temporary files in C:\\Temp to free disk space.',
105
- },
106
- onApprove: () => undefined,
107
- onReject: () => undefined,
108
- },
109
- ],
110
- },
111
- ];
112
-
113
- function Thread({
114
- messages,
115
- appearance,
116
- handedOff,
117
- }: {
118
- messages: Message[];
119
- appearance: ChatAppearance;
120
- handedOff?: boolean;
121
- }) {
122
- return (
123
- <div className="flex h-[720px] w-[728px] flex-col bg-ods-bg">
124
- <ChatMessageList
125
- messages={messages}
126
- assistantType="fae"
127
- approvalVariant="client"
128
- appearance={appearance}
129
- fullWidth
130
- autoScroll={false}
131
- />
132
- <div className="px-[var(--spacing-system-m)] pb-[var(--spacing-system-m)]">
133
- <ChatInput
134
- fullWidth
135
- placeholder="Enter your request here..."
136
- awaitingResponse={handedOff}
137
- appearance={appearance}
138
- awaitingTeam={[
139
- { key: '1', name: 'Roman Smith' },
140
- { key: '2', name: 'Ada Lin' },
141
- { key: '3', name: 'Michael Johnson' },
142
- ]}
143
- />
144
- </div>
145
- </div>
146
- );
147
- }
148
-
149
- const meta: Meta<typeof Thread> = {
150
- decorators: [withFaeBrand],
151
- title: 'Chat/Fae Thread v2',
152
- component: Thread,
153
- parameters: { layout: 'centered' },
154
- };
155
-
156
- export default meta;
157
- type Story = StoryObj<typeof Thread>;
158
-
159
- export const EscalationV2: Story = { args: { messages: ESCALATION_THREAD, appearance: 'v2', handedOff: true } };
160
- export const EscalationClassic: Story = {
161
- args: { messages: ESCALATION_THREAD, appearance: 'classic', handedOff: true },
162
- };
163
-
164
- const approvalCard = (id: string, status: 'approved' | 'rejected' | 'cancelled', resolvedByName: string) => ({
165
- type: 'approval_request' as const,
166
- status,
167
- resolvedByName,
168
- data: {
169
- requestId: id,
170
- command: 'Remove-Item C:\\Temp\\* -Recurse',
171
- explanation: 'Clears temporary files in C:\\Temp.',
172
- },
173
- });
174
-
175
- const APPROVAL_STATES_THREAD: Message[] = [
176
- {
177
- id: 'a1',
178
- role: 'assistant',
179
- name: 'Fae',
180
- assistantType: 'fae',
181
- timestamp: at(47),
182
- content: [
183
- approvalCard('r1', 'approved', 'Michael Johnson'),
184
- approvalCard('r2', 'rejected', 'Michael Johnson'),
185
- approvalCard('r3', 'cancelled', 'Michael Johnson'),
186
- ],
187
- },
188
- ];
189
-
190
- export const ApprovalV2: Story = { args: { messages: APPROVAL_THREAD, appearance: 'v2' } };
191
-
192
- export const ApprovalStatesV2: Story = { args: { messages: APPROVAL_STATES_THREAD, appearance: 'v2' } };
@@ -1,28 +0,0 @@
1
- import type { Decorator } from '@storybook/nextjs-vite';
2
- import { useLayoutEffect, type ReactNode } from 'react';
3
-
4
- /**
5
- * Renders a story in the Fae client's default brand. The chat app sets
6
- * `data-app-type="flamingo"` on <html> before first paint, which points the
7
- * accent tokens at flamingo pink; without it Storybook falls back to the
8
- * OpenFrame yellow and a Fae screen shows two brand colours at once. Restores
9
- * the previous value so the next story is unaffected.
10
- */
11
- function FaeBrand({ children }: { children: ReactNode }) {
12
- useLayoutEffect(() => {
13
- const root = document.documentElement;
14
- const previous = root.getAttribute('data-app-type');
15
- root.setAttribute('data-app-type', 'flamingo');
16
- return () => {
17
- if (previous === null) root.removeAttribute('data-app-type');
18
- else root.setAttribute('data-app-type', previous);
19
- };
20
- }, []);
21
- return <>{children}</>;
22
- }
23
-
24
- export const withFaeBrand: Decorator = Story => (
25
- <FaeBrand>
26
- <Story />
27
- </FaeBrand>
28
- );