@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.
- package/dist/{chunk-T6KWNJDW.js → chunk-22RSJ57Z.js} +470 -465
- package/dist/chunk-22RSJ57Z.js.map +1 -0
- package/dist/{chunk-65KTXRBY.js → chunk-2G2CB7ZZ.js} +6 -6
- package/dist/{chunk-K2HZG4RM.cjs → chunk-2OD2PPNO.cjs} +32 -32
- package/dist/{chunk-K2HZG4RM.cjs.map → chunk-2OD2PPNO.cjs.map} +1 -1
- package/dist/{chunk-HMXUOLZR.cjs → chunk-3DO76UAB.cjs} +20 -20
- package/dist/{chunk-HMXUOLZR.cjs.map → chunk-3DO76UAB.cjs.map} +1 -1
- package/dist/{chunk-XW7MUIW4.js → chunk-3NIZ2VLQ.js} +3 -3
- package/dist/{chunk-5LJZPJHX.cjs → chunk-4LBCUBVL.cjs} +59 -59
- package/dist/{chunk-5LJZPJHX.cjs.map → chunk-4LBCUBVL.cjs.map} +1 -1
- package/dist/{chunk-ZF4Z36G4.js → chunk-4RJQMOQW.js} +7 -7
- package/dist/{chunk-TJI5V5BR.cjs → chunk-AF4XOURN.cjs} +7 -7
- package/dist/{chunk-TJI5V5BR.cjs.map → chunk-AF4XOURN.cjs.map} +1 -1
- package/dist/{chunk-7XF63THA.js → chunk-ASBBGP36.js} +6 -6
- package/dist/{chunk-O7LOZW4R.js → chunk-C5TFPMOM.js} +5 -5
- package/dist/{chunk-CMQXPD5C.cjs → chunk-GETQTNRV.cjs} +28 -28
- package/dist/{chunk-CMQXPD5C.cjs.map → chunk-GETQTNRV.cjs.map} +1 -1
- package/dist/{chunk-TUG7IT6H.js → chunk-HLEXYTOE.js} +2 -2
- package/dist/{chunk-M4HYIXP6.cjs → chunk-IBUFFKAK.cjs} +399 -383
- package/dist/chunk-IBUFFKAK.cjs.map +1 -0
- package/dist/{chunk-GXV6UEVY.js → chunk-IELOPDTA.js} +3 -3
- package/dist/{chunk-T4DUTP5C.cjs → chunk-II5GVLTN.cjs} +5 -5
- package/dist/{chunk-T4DUTP5C.cjs.map → chunk-II5GVLTN.cjs.map} +1 -1
- package/dist/{chunk-B32YLOYQ.js → chunk-JBJZL5PI.js} +4 -4
- package/dist/{chunk-UT333EOT.js → chunk-LCOMNQLX.js} +4 -4
- package/dist/{chunk-VDPCK437.js → chunk-MHTB67ZH.js} +21 -5
- package/dist/{chunk-VDPCK437.js.map → chunk-MHTB67ZH.js.map} +1 -1
- package/dist/{chunk-EFBMNVSH.cjs → chunk-MZAZUSPQ.cjs} +15 -15
- package/dist/{chunk-EFBMNVSH.cjs.map → chunk-MZAZUSPQ.cjs.map} +1 -1
- package/dist/{chunk-C3B36OYZ.cjs → chunk-N3I53B5W.cjs} +3 -3
- package/dist/{chunk-C3B36OYZ.cjs.map → chunk-N3I53B5W.cjs.map} +1 -1
- package/dist/{chunk-WJK2EETW.js → chunk-NINO3PI7.js} +2 -2
- package/dist/{chunk-YCTVLN47.cjs → chunk-P4SBHO7U.cjs} +10 -10
- package/dist/{chunk-YCTVLN47.cjs.map → chunk-P4SBHO7U.cjs.map} +1 -1
- package/dist/{chunk-PSQKD6ZN.js → chunk-PG65AE3I.js} +3 -3
- package/dist/{chunk-DQOQ3KTP.cjs → chunk-QNWQQ4RT.cjs} +78 -73
- package/dist/chunk-QNWQQ4RT.cjs.map +1 -0
- package/dist/{chunk-BZ2XOEHW.js → chunk-QOH45QSE.js} +3 -3
- package/dist/{chunk-DWXWEGGO.cjs → chunk-QTXDXHMD.cjs} +22 -22
- package/dist/{chunk-DWXWEGGO.cjs.map → chunk-QTXDXHMD.cjs.map} +1 -1
- package/dist/{chunk-B25CF3PO.cjs → chunk-RBCMBRI7.cjs} +30 -30
- package/dist/{chunk-B25CF3PO.cjs.map → chunk-RBCMBRI7.cjs.map} +1 -1
- package/dist/{chunk-QHLJSB2J.cjs → chunk-RKH7DEDF.cjs} +12 -12
- package/dist/{chunk-QHLJSB2J.cjs.map → chunk-RKH7DEDF.cjs.map} +1 -1
- package/dist/{chunk-Y7ZYOQNB.js → chunk-RSMAVO2Z.js} +3 -3
- package/dist/{chunk-A3CSQB4A.cjs → chunk-SI77HF4U.cjs} +1247 -805
- package/dist/chunk-SI77HF4U.cjs.map +1 -0
- package/dist/{chunk-GHLGXFNU.js → chunk-U6JC3RS2.js} +3 -3
- package/dist/{chunk-B5N3AHPG.cjs → chunk-UBXEETWQ.cjs} +81 -81
- package/dist/{chunk-B5N3AHPG.cjs.map → chunk-UBXEETWQ.cjs.map} +1 -1
- package/dist/{chunk-N4Z6X5BV.js → chunk-WFH52OVQ.js} +5 -5
- package/dist/{chunk-U3IXG7TR.cjs → chunk-WJX3ZSOE.cjs} +33 -33
- package/dist/{chunk-U3IXG7TR.cjs.map → chunk-WJX3ZSOE.cjs.map} +1 -1
- package/dist/{chunk-XGX6JSEU.js → chunk-YPXXXX6R.js} +2086 -1644
- package/dist/chunk-YPXXXX6R.js.map +1 -0
- package/dist/{chunk-P2ASEPA2.cjs → chunk-ZS3YCJYY.cjs} +6 -6
- package/dist/{chunk-P2ASEPA2.cjs.map → chunk-ZS3YCJYY.cjs.map} +1 -1
- package/dist/components/case-studies/index.cjs +16 -16
- package/dist/components/case-studies/index.js +4 -4
- package/dist/components/chat/ai-assistant-info.d.ts.map +1 -1
- package/dist/components/chat/approval-request-message.d.ts.map +1 -1
- package/dist/components/chat/chat-appearance-context.d.ts +9 -0
- package/dist/components/chat/chat-appearance-context.d.ts.map +1 -0
- package/dist/components/chat/chat-input.d.ts.map +1 -1
- package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
- package/dist/components/chat/chat-message-list.d.ts.map +1 -1
- package/dist/components/chat/chat-nav-sidebar.d.ts +50 -0
- package/dist/components/chat/chat-nav-sidebar.d.ts.map +1 -0
- package/dist/components/chat/chat-top-navigation.d.ts +29 -0
- package/dist/components/chat/chat-top-navigation.d.ts.map +1 -0
- package/dist/components/chat/error-message-display.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +21 -3
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.d.ts +6 -0
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +20 -2
- package/dist/components/chat/msp-organization-card.d.ts +7 -1
- package/dist/components/chat/msp-organization-card.d.ts.map +1 -1
- package/dist/components/chat/onboarding-carousel.d.ts +29 -0
- package/dist/components/chat/onboarding-carousel.d.ts.map +1 -0
- package/dist/components/chat/system-event-message.d.ts +14 -0
- package/dist/components/chat/system-event-message.d.ts.map +1 -0
- package/dist/components/chat/types/chat.types.d.ts +13 -0
- package/dist/components/chat/types/chat.types.d.ts.map +1 -1
- package/dist/components/chat/types/component.types.d.ts +20 -1
- package/dist/components/chat/types/component.types.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +5 -5
- package/dist/components/contact/index.js +4 -4
- package/dist/components/docs/index.cjs +8 -8
- package/dist/components/docs/index.js +7 -7
- package/dist/components/embeds/index.cjs +5 -5
- package/dist/components/embeds/index.js +4 -4
- package/dist/components/faq/index.cjs +5 -5
- package/dist/components/faq/index.js +4 -4
- package/dist/components/features/index.cjs +4 -4
- package/dist/components/features/index.js +3 -3
- package/dist/components/help-center-pages/index.cjs +91 -91
- package/dist/components/help-center-pages/index.js +15 -15
- package/dist/components/index.cjs +73 -55
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +33 -15
- package/dist/components/index.js.map +1 -1
- package/dist/components/meeting-scheduler/index.cjs +41 -41
- package/dist/components/meeting-scheduler/index.js +3 -3
- package/dist/components/navigation/index.cjs +7 -7
- package/dist/components/navigation/index.js +6 -6
- package/dist/components/onboarding-guides/index.cjs +7 -7
- package/dist/components/onboarding-guides/index.js +6 -6
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/tickets/index.cjs +8 -8
- package/dist/components/tickets/index.js +7 -7
- package/dist/components/ui/avatar-stack.d.ts +5 -1
- package/dist/components/ui/avatar-stack.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +22 -4
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +21 -3
- package/dist/components/ui/reopen-ticket-request-modal.d.ts +4 -1
- package/dist/components/ui/reopen-ticket-request-modal.d.ts.map +1 -1
- package/dist/components/ui/tab-navigation.d.ts +7 -0
- package/dist/components/ui/tab-navigation.d.ts.map +1 -1
- package/dist/index.cjs +27 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +25 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/__tests__/chat-appearance.test.tsx +79 -0
- package/src/components/chat/__tests__/onboarding-carousel.test.tsx +56 -0
- package/src/components/chat/ai-assistant-info.tsx +7 -1
- package/src/components/chat/approval-request-message.tsx +6 -1
- package/src/components/chat/chat-appearance-context.tsx +15 -0
- package/src/components/chat/chat-input.tsx +26 -1
- package/src/components/chat/chat-message-enhanced.tsx +180 -142
- package/src/components/chat/chat-message-list.tsx +169 -164
- package/src/components/chat/chat-nav-sidebar.tsx +163 -0
- package/src/components/chat/chat-top-navigation.tsx +109 -0
- package/src/components/chat/error-message-display.tsx +21 -3
- package/src/components/chat/index.ts +6 -0
- package/src/components/chat/msp-organization-card.tsx +35 -0
- package/src/components/chat/onboarding-carousel.tsx +162 -0
- package/src/components/chat/system-event-message.tsx +54 -0
- package/src/components/chat/types/chat.types.ts +15 -0
- package/src/components/chat/types/component.types.ts +20 -0
- package/src/components/ui/__tests__/avatar-stack.test.tsx +25 -0
- package/src/components/ui/__tests__/tab-navigation.test.tsx +56 -2
- package/src/components/ui/avatar-stack.tsx +15 -5
- package/src/components/ui/reopen-ticket-request-modal.tsx +6 -2
- package/src/components/ui/tab-navigation.tsx +23 -0
- package/src/stories/FaeLayoutV2.stories.tsx +150 -0
- package/src/stories/FaeOnboardingV2.stories.tsx +68 -0
- package/src/stories/FaeThreadV2.stories.tsx +192 -0
- package/src/stories/fae-brand-decorator.tsx +28 -0
- package/src/styles/ods-colors.css +10 -0
- package/src/styles/ods-effects.css +13 -0
- package/dist/chunk-A3CSQB4A.cjs.map +0 -1
- package/dist/chunk-DQOQ3KTP.cjs.map +0 -1
- package/dist/chunk-M4HYIXP6.cjs.map +0 -1
- package/dist/chunk-T6KWNJDW.js.map +0 -1
- package/dist/chunk-XGX6JSEU.js.map +0 -1
- /package/dist/{chunk-65KTXRBY.js.map → chunk-2G2CB7ZZ.js.map} +0 -0
- /package/dist/{chunk-XW7MUIW4.js.map → chunk-3NIZ2VLQ.js.map} +0 -0
- /package/dist/{chunk-ZF4Z36G4.js.map → chunk-4RJQMOQW.js.map} +0 -0
- /package/dist/{chunk-7XF63THA.js.map → chunk-ASBBGP36.js.map} +0 -0
- /package/dist/{chunk-O7LOZW4R.js.map → chunk-C5TFPMOM.js.map} +0 -0
- /package/dist/{chunk-TUG7IT6H.js.map → chunk-HLEXYTOE.js.map} +0 -0
- /package/dist/{chunk-GXV6UEVY.js.map → chunk-IELOPDTA.js.map} +0 -0
- /package/dist/{chunk-B32YLOYQ.js.map → chunk-JBJZL5PI.js.map} +0 -0
- /package/dist/{chunk-UT333EOT.js.map → chunk-LCOMNQLX.js.map} +0 -0
- /package/dist/{chunk-WJK2EETW.js.map → chunk-NINO3PI7.js.map} +0 -0
- /package/dist/{chunk-PSQKD6ZN.js.map → chunk-PG65AE3I.js.map} +0 -0
- /package/dist/{chunk-BZ2XOEHW.js.map → chunk-QOH45QSE.js.map} +0 -0
- /package/dist/{chunk-Y7ZYOQNB.js.map → chunk-RSMAVO2Z.js.map} +0 -0
- /package/dist/{chunk-GHLGXFNU.js.map → chunk-U6JC3RS2.js.map} +0 -0
- /package/dist/{chunk-N4Z6X5BV.js.map → chunk-WFH52OVQ.js.map} +0 -0
|
@@ -33,6 +33,10 @@ export interface AvatarStackProps {
|
|
|
33
33
|
* so the text beside every stack starts at the same x.
|
|
34
34
|
*/
|
|
35
35
|
slots?: number;
|
|
36
|
+
/** How many people the group has when `people` is only a sample of it (a
|
|
37
|
+
* server that returns the first few faces plus a count). The "+N" circle
|
|
38
|
+
* then counts everyone not shown. Defaults to `people.length`. */
|
|
39
|
+
total?: number;
|
|
36
40
|
/** Size bucket (also sizes the "+N" circle). `xs` (24px) is for
|
|
37
41
|
* dense card meta rows (chat compact cards, delivery rows). */
|
|
38
42
|
size?: AvatarStackSize;
|
|
@@ -63,22 +67,28 @@ export function AvatarStack({
|
|
|
63
67
|
people,
|
|
64
68
|
max = 3,
|
|
65
69
|
slots,
|
|
70
|
+
total,
|
|
66
71
|
size = 'md',
|
|
67
72
|
ringClassName = 'ring-ods-card',
|
|
68
73
|
label = 'Assignees',
|
|
69
74
|
className,
|
|
70
75
|
}: AvatarStackProps) {
|
|
71
76
|
if (people.length === 0) return null;
|
|
72
|
-
const
|
|
77
|
+
const groupSize = Math.max(total ?? people.length, people.length);
|
|
78
|
+
const faces = slots ? (groupSize > slots ? slots - 1 : slots) : max;
|
|
73
79
|
const visible = people.slice(0, faces);
|
|
74
80
|
const overflow = people.slice(faces);
|
|
81
|
+
const overflowCount = groupSize - visible.length;
|
|
82
|
+
const names = people.map(p => p.name).join(', ');
|
|
83
|
+
// Names beyond the sample are unknown here; say how many there are instead.
|
|
84
|
+
const unnamed = groupSize - people.length;
|
|
75
85
|
const compact = size === 'xs';
|
|
76
86
|
return (
|
|
77
87
|
<div
|
|
78
88
|
className={cn('flex items-center', slots && 'shrink-0', className)}
|
|
79
89
|
style={slots ? { width: SIZE_PX[size] + (slots - 1) * (SIZE_PX[size] - OVERLAP_PX[size]) } : undefined}
|
|
80
90
|
role="group"
|
|
81
|
-
aria-label={`${label}: ${
|
|
91
|
+
aria-label={`${label}: ${names}${unnamed > 0 ? ` and ${unnamed} more` : ''}`}
|
|
82
92
|
>
|
|
83
93
|
{visible.map((person, i) => (
|
|
84
94
|
<SquareAvatar
|
|
@@ -95,9 +105,9 @@ export function AvatarStack({
|
|
|
95
105
|
style={{ zIndex: visible.length - i }}
|
|
96
106
|
/>
|
|
97
107
|
))}
|
|
98
|
-
{
|
|
108
|
+
{overflowCount > 0 && (
|
|
99
109
|
<span
|
|
100
|
-
title={overflow.map(p => p.name).join(', ')}
|
|
110
|
+
title={overflow.length > 0 ? overflow.map(p => p.name).join(', ') : `${overflowCount} more`}
|
|
101
111
|
className={cn(
|
|
102
112
|
'relative z-0 flex shrink-0 items-center justify-center rounded-full bg-ods-bg text-ods-text-secondary ring-2',
|
|
103
113
|
ringClassName,
|
|
@@ -109,7 +119,7 @@ export function AvatarStack({
|
|
|
109
119
|
OVERFLOW_CIRCLE_SIZE[size],
|
|
110
120
|
)}
|
|
111
121
|
>
|
|
112
|
-
+{
|
|
122
|
+
+{overflowCount}
|
|
113
123
|
</span>
|
|
114
124
|
)}
|
|
115
125
|
</div>
|
|
@@ -28,6 +28,9 @@ export interface ReopenTicketRequestModalProps {
|
|
|
28
28
|
* omits `handoffToTechnician`.
|
|
29
29
|
*/
|
|
30
30
|
showHandoffOption?: boolean;
|
|
31
|
+
/** Opens with the handoff checkbox already ticked — the user reached the
|
|
32
|
+
* modal by asking for a technician, so that is the choice to confirm. */
|
|
33
|
+
defaultHandoff?: boolean;
|
|
31
34
|
/** Confirm in-flight: buttons lock and the modal refuses to close. */
|
|
32
35
|
isPending?: boolean;
|
|
33
36
|
onConfirm: (selection: ReopenTicketRequestSelection) => void;
|
|
@@ -45,11 +48,12 @@ export function ReopenTicketRequestModal({
|
|
|
45
48
|
isOpen,
|
|
46
49
|
onClose,
|
|
47
50
|
showHandoffOption = false,
|
|
51
|
+
defaultHandoff = false,
|
|
48
52
|
isPending = false,
|
|
49
53
|
onConfirm,
|
|
50
54
|
}: ReopenTicketRequestModalProps) {
|
|
51
55
|
const [reason, setReason] = useState('');
|
|
52
|
-
const [handoff, setHandoff] = useState(
|
|
56
|
+
const [handoff, setHandoff] = useState(defaultHandoff);
|
|
53
57
|
|
|
54
58
|
// Re-seed the inputs every time the modal opens. Adjusted while rendering,
|
|
55
59
|
// not from an effect: the modal does not unmount when it closes, so an effect
|
|
@@ -60,7 +64,7 @@ export function ReopenTicketRequestModal({
|
|
|
60
64
|
setWasOpen(isOpen);
|
|
61
65
|
if (isOpen) {
|
|
62
66
|
setReason('');
|
|
63
|
-
setHandoff(
|
|
67
|
+
setHandoff(defaultHandoff);
|
|
64
68
|
}
|
|
65
69
|
}
|
|
66
70
|
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from 'react';
|
|
15
15
|
import { useSearchParams, useRouter, usePathname } from '../../embed-shims/next-navigation';
|
|
16
16
|
import { cn } from '../../utils/cn';
|
|
17
|
+
import { Tag } from './tag';
|
|
17
18
|
|
|
18
19
|
export interface TabItem {
|
|
19
20
|
id: string;
|
|
@@ -43,6 +44,13 @@ export interface TabItem {
|
|
|
43
44
|
*/
|
|
44
45
|
component?: React.ComponentType<any>;
|
|
45
46
|
indicator?: 'success' | 'warning' | 'error';
|
|
47
|
+
/**
|
|
48
|
+
* A stamp drawn after the label, e.g. "Beta" on a tab that is shipped but
|
|
49
|
+
* still behind its flag. The same `warning` chip the navigation sidebar draws
|
|
50
|
+
* for `NavigationSidebarItem.badge`; the tab's accessible name becomes
|
|
51
|
+
* "Label (Badge)". Cased as read — the chip upper-cases it.
|
|
52
|
+
*/
|
|
53
|
+
badge?: string;
|
|
46
54
|
}
|
|
47
55
|
|
|
48
56
|
export interface TabNavigationUrlSyncOptions {
|
|
@@ -329,6 +337,7 @@ const TabBar = memo(function TabBarImpl({
|
|
|
329
337
|
ref={isActive ? activeTabRef : undefined}
|
|
330
338
|
type="button"
|
|
331
339
|
onClick={() => onTabChange(tab.id)}
|
|
340
|
+
aria-label={tab.badge ? `${tab.label} (${tab.badge})` : undefined}
|
|
332
341
|
className={cn(
|
|
333
342
|
'relative flex shrink-0 cursor-pointer items-center justify-center gap-[var(--spacing-system-xxs)] p-[var(--spacing-system-m)]',
|
|
334
343
|
// Named rather than `transition-all`: the only thing that moves
|
|
@@ -376,6 +385,20 @@ const TabBar = memo(function TabBarImpl({
|
|
|
376
385
|
>
|
|
377
386
|
{tab.label}
|
|
378
387
|
</span>
|
|
388
|
+
|
|
389
|
+
{tab.badge && (
|
|
390
|
+
// The chip is 32px against a 20/24px label line. The negative
|
|
391
|
+
// margin takes the difference back, so a stamped tab is exactly
|
|
392
|
+
// as tall as its neighbours at every breakpoint. No hover shade:
|
|
393
|
+
// the tab is the target, and the cursor passes through to it.
|
|
394
|
+
<Tag
|
|
395
|
+
as="span"
|
|
396
|
+
aria-hidden
|
|
397
|
+
label={tab.badge}
|
|
398
|
+
variant="warning"
|
|
399
|
+
className="pointer-events-none my-[calc((var(--font-line-space-h4-body)_-_2rem)/2)] ml-[var(--spacing-system-xxs)] shrink-0"
|
|
400
|
+
/>
|
|
401
|
+
)}
|
|
379
402
|
</button>
|
|
380
403
|
);
|
|
381
404
|
})}
|
|
@@ -0,0 +1,150 @@
|
|
|
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'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 } };
|
|
@@ -0,0 +1,68 @@
|
|
|
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 = {};
|
|
@@ -0,0 +1,192 @@
|
|
|
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' } };
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
);
|
|
@@ -407,6 +407,16 @@
|
|
|
407
407
|
--color-focus-ring: #00ff00;
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
+
/* ===================================================================== */
|
|
411
|
+
/* BRAND-TINTED SURFACE — the page background takes 2% of the accent. */
|
|
412
|
+
/* Figma's fae-background (#1a1719) and mingo-background (#171b1a) are */
|
|
413
|
+
/* exactly 2% of their brand over the base background. Resolved on the */
|
|
414
|
+
/* element, so it follows the theme and a runtime (tenant) accent. */
|
|
415
|
+
/* ===================================================================== */
|
|
416
|
+
[data-surface='brand-tinted'] {
|
|
417
|
+
--color-bg: color-mix(in srgb, var(--color-accent-primary) 2%, var(--ods-system-greys-background));
|
|
418
|
+
}
|
|
419
|
+
|
|
410
420
|
/* ===================================================================== */
|
|
411
421
|
/* PLATFORM OVERRIDES — brand/accent ONLY. */
|
|
412
422
|
/* Reference `--ods-*` so they follow the active theme; never touch */
|
|
@@ -51,3 +51,16 @@
|
|
|
51
51
|
.ods-shadow-glyph {
|
|
52
52
|
filter: var(--ods-shadow-glyph);
|
|
53
53
|
}
|
|
54
|
+
|
|
55
|
+
/* The brand glow in the bottom-right corner of a chat surface (Fae chat v2):
|
|
56
|
+
the accent at 4% fading out over the whole surface (radial fill centred on
|
|
57
|
+
the corner, radii 100% × 103% as in the design). Written
|
|
58
|
+
on the class, not as a :root variable, so the accent resolves on the element
|
|
59
|
+
and follows a platform or runtime (tenant) accent set below the root. */
|
|
60
|
+
.ods-glow-accent-corner {
|
|
61
|
+
background-image: radial-gradient(
|
|
62
|
+
100% 103% at 100% 100%,
|
|
63
|
+
color-mix(in srgb, var(--color-accent-primary) 4%, transparent) 0%,
|
|
64
|
+
color-mix(in srgb, var(--color-accent-primary) 0%, transparent) 100%
|
|
65
|
+
);
|
|
66
|
+
}
|