@flamingo-stack/openframe-frontend-core 0.0.389 → 0.0.390-snapshot.20260709094846
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-3JQUNKWY.cjs → chunk-33PP6VR3.cjs} +15 -15
- package/dist/{chunk-3JQUNKWY.cjs.map → chunk-33PP6VR3.cjs.map} +1 -1
- package/dist/{chunk-CQXHHVMR.js → chunk-4HV6RIDE.js} +2 -2
- package/dist/{chunk-LFLFQ422.js → chunk-BY337N4T.js} +2 -2
- package/dist/{chunk-M6ODI2F5.js → chunk-CAEKZPSH.js} +2 -2
- package/dist/{chunk-JSFHTIKA.js → chunk-CPWR2BJG.js} +2 -2
- package/dist/{chunk-Q4YGG3CL.cjs → chunk-DAYWQZIW.cjs} +31 -31
- package/dist/{chunk-Q4YGG3CL.cjs.map → chunk-DAYWQZIW.cjs.map} +1 -1
- package/dist/{chunk-44Q3IXZM.js → chunk-GM5M4L7O.js} +2 -2
- package/dist/{chunk-OBKCQ7LA.cjs → chunk-GWJSQEQO.cjs} +62 -62
- package/dist/{chunk-OBKCQ7LA.cjs.map → chunk-GWJSQEQO.cjs.map} +1 -1
- package/dist/{chunk-BJPQJNSC.js → chunk-HPTHEA2A.js} +4 -4
- package/dist/{chunk-WIMXB5EU.js → chunk-KWLSVTDN.js} +4 -4
- package/dist/{chunk-7MKU2EB4.cjs → chunk-LC4VIFII.cjs} +26 -26
- package/dist/{chunk-7MKU2EB4.cjs.map → chunk-LC4VIFII.cjs.map} +1 -1
- package/dist/{chunk-KA4WXCN3.js → chunk-QHKU6473.js} +4 -4
- package/dist/{chunk-MPOP47ME.cjs → chunk-QQQW4DUI.cjs} +12 -12
- package/dist/{chunk-MPOP47ME.cjs.map → chunk-QQQW4DUI.cjs.map} +1 -1
- package/dist/{chunk-NULFIYK2.js → chunk-QXD4OZCN.js} +2 -2
- package/dist/{chunk-ZFZAHLI4.cjs → chunk-R62LSFOU.cjs} +7 -7
- package/dist/{chunk-ZFZAHLI4.cjs.map → chunk-R62LSFOU.cjs.map} +1 -1
- package/dist/{chunk-4OLIDOLO.js → chunk-SJYXTTUB.js} +2 -2
- package/dist/{chunk-EHTE3CTT.cjs → chunk-TXCE75UB.cjs} +37 -37
- package/dist/{chunk-EHTE3CTT.cjs.map → chunk-TXCE75UB.cjs.map} +1 -1
- package/dist/{chunk-2JCSORCJ.cjs → chunk-USUNV54R.cjs} +26 -26
- package/dist/{chunk-2JCSORCJ.cjs.map → chunk-USUNV54R.cjs.map} +1 -1
- package/dist/{chunk-IDWKP5H5.js → chunk-VIZUVGIY.js} +1936 -1852
- package/dist/chunk-VIZUVGIY.js.map +1 -0
- package/dist/{chunk-VXJBE5NX.cjs → chunk-VY3N2RPS.cjs} +59 -59
- package/dist/{chunk-VXJBE5NX.cjs.map → chunk-VY3N2RPS.cjs.map} +1 -1
- package/dist/{chunk-GNB5ERZP.cjs → chunk-Y4S6X7IV.cjs} +1574 -1490
- package/dist/chunk-Y4S6X7IV.cjs.map +1 -0
- package/dist/{chunk-MIEODAAV.cjs → chunk-YKQE7DDT.cjs} +9 -9
- package/dist/{chunk-MIEODAAV.cjs.map → chunk-YKQE7DDT.cjs.map} +1 -1
- package/dist/{chunk-XGOX5UQ2.js → chunk-YU2RPRI4.js} +4 -4
- package/dist/{chunk-TQZLC3BQ.cjs → chunk-ZHYT2HYT.cjs} +5 -5
- package/dist/{chunk-TQZLC3BQ.cjs.map → chunk-ZHYT2HYT.cjs.map} +1 -1
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- package/dist/components/chat/approval-batch-message.d.ts +13 -4
- package/dist/components/chat/approval-batch-message.d.ts.map +1 -1
- package/dist/components/chat/approval-request-message.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/index.cjs +4 -2
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +3 -1
- 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 +21 -3
- package/dist/components/chat/types/component.types.d.ts.map +1 -1
- package/dist/components/chat/types/message.types.d.ts +3 -0
- package/dist/components/chat/types/message.types.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/docs/index.cjs +5 -5
- package/dist/components/docs/index.js +4 -4
- package/dist/components/embeds/index.cjs +3 -3
- package/dist/components/embeds/index.js +2 -2
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/auth/accept-invitation-form.d.ts +2 -2
- package/dist/components/features/auth/accept-invitation-form.d.ts.map +1 -1
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/features/notifications/notification-drawer.d.ts.map +1 -1
- package/dist/components/features/time-tracker/time-tracker-header-button.d.ts +5 -2
- package/dist/components/features/time-tracker/time-tracker-header-button.d.ts.map +1 -1
- package/dist/components/help-center-pages/index.cjs +20 -20
- package/dist/components/help-center-pages/index.js +11 -11
- package/dist/components/index.cjs +122 -120
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +11 -9
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/app-header.d.ts.map +1 -1
- package/dist/components/navigation/app-layout-context.d.ts +34 -0
- package/dist/components/navigation/app-layout-context.d.ts.map +1 -0
- package/dist/components/navigation/app-layout-drawer.d.ts.map +1 -1
- package/dist/components/navigation/app-layout.d.ts +2 -21
- package/dist/components/navigation/app-layout.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/tickets/index.cjs +5 -5
- package/dist/components/tickets/index.js +4 -4
- package/dist/components/ui/index.cjs +4 -2
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +3 -1
- package/dist/index.cjs +4 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3 -1
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/approval-batch-message.tsx +104 -88
- package/src/components/chat/approval-request-message.tsx +57 -3
- package/src/components/chat/chat-message-enhanced.tsx +5 -1
- package/src/components/chat/chat-message-list.tsx +3 -0
- package/src/components/chat/types/chat.types.ts +17 -0
- package/src/components/chat/types/component.types.ts +21 -3
- package/src/components/chat/types/message.types.ts +3 -0
- package/src/components/features/auth/accept-invitation-form.tsx +4 -4
- package/src/components/features/notifications/notification-drawer.tsx +72 -43
- package/src/components/features/time-tracker/time-tracker-header-button.tsx +46 -6
- package/src/components/navigation/app-header.tsx +13 -9
- package/src/components/navigation/app-layout-context.ts +46 -0
- package/src/components/navigation/app-layout-drawer.tsx +21 -11
- package/src/components/navigation/app-layout.tsx +17 -39
- package/src/stories/ApprovalBatchMessageClient.stories.tsx +20 -62
- package/dist/chunk-GNB5ERZP.cjs.map +0 -1
- package/dist/chunk-IDWKP5H5.js.map +0 -1
- /package/dist/{chunk-CQXHHVMR.js.map → chunk-4HV6RIDE.js.map} +0 -0
- /package/dist/{chunk-LFLFQ422.js.map → chunk-BY337N4T.js.map} +0 -0
- /package/dist/{chunk-M6ODI2F5.js.map → chunk-CAEKZPSH.js.map} +0 -0
- /package/dist/{chunk-JSFHTIKA.js.map → chunk-CPWR2BJG.js.map} +0 -0
- /package/dist/{chunk-44Q3IXZM.js.map → chunk-GM5M4L7O.js.map} +0 -0
- /package/dist/{chunk-BJPQJNSC.js.map → chunk-HPTHEA2A.js.map} +0 -0
- /package/dist/{chunk-WIMXB5EU.js.map → chunk-KWLSVTDN.js.map} +0 -0
- /package/dist/{chunk-KA4WXCN3.js.map → chunk-QHKU6473.js.map} +0 -0
- /package/dist/{chunk-NULFIYK2.js.map → chunk-QXD4OZCN.js.map} +0 -0
- /package/dist/{chunk-4OLIDOLO.js.map → chunk-SJYXTTUB.js.map} +0 -0
- /package/dist/{chunk-XGOX5UQ2.js.map → chunk-YU2RPRI4.js.map} +0 -0
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import { useEffect, useRef } from 'react'
|
|
4
4
|
import { BellOffIcon } from '../../icons-v2-generated/interface/bell-off-icon'
|
|
5
5
|
import { ClockHistoryIcon, ArrowRightUpIcon } from '../../icons-v2-generated'
|
|
6
|
+
import { useAppLayoutDrawerContainer } from '../../navigation/app-layout-context'
|
|
7
|
+
import { AppLayoutDrawer, AppLayoutDrawerContent } from '../../navigation/app-layout-drawer'
|
|
6
8
|
import { SplitButton } from '../../ui/button'
|
|
7
9
|
import { Drawer, DrawerContent, DrawerTitle } from '../../ui/drawer'
|
|
8
10
|
import { Switch } from '../../ui/switch'
|
|
@@ -27,6 +29,10 @@ export function NotificationDrawer({
|
|
|
27
29
|
renderTile: renderTileProp,
|
|
28
30
|
}: NotificationDrawerProps) {
|
|
29
31
|
const ctx = useOptionalNotifications()
|
|
32
|
+
// Inside AppLayout the drawer renders in-layout (dims and covers only the
|
|
33
|
+
// main content area — header and sidebar stay interactive). Outside of it
|
|
34
|
+
// (Storybook, non-AppLayout hosts) it falls back to the viewport Drawer.
|
|
35
|
+
const layoutContainer = useAppLayoutDrawerContainer()
|
|
30
36
|
|
|
31
37
|
if (!ctx) return null
|
|
32
38
|
|
|
@@ -56,6 +62,71 @@ export function NotificationDrawer({
|
|
|
56
62
|
|
|
57
63
|
const unreadNotifications = notifications.filter((n) => !n.read)
|
|
58
64
|
|
|
65
|
+
const content = (
|
|
66
|
+
<>
|
|
67
|
+
<div className="px-[var(--spacing-system-m)] pt-[var(--spacing-system-m)]">
|
|
68
|
+
<DrawerTitle
|
|
69
|
+
hideClose
|
|
70
|
+
className="truncate"
|
|
71
|
+
actions={
|
|
72
|
+
<button
|
|
73
|
+
type="button"
|
|
74
|
+
disabled={unreadCount === 0}
|
|
75
|
+
onClick={markAllRead}
|
|
76
|
+
className="self-center text-h6 text-ods-text-secondary underline transition-colors hover:text-ods-text-primary disabled:opacity-40"
|
|
77
|
+
>
|
|
78
|
+
Mark All Complete
|
|
79
|
+
</button>
|
|
80
|
+
}
|
|
81
|
+
>
|
|
82
|
+
New Notifications
|
|
83
|
+
</DrawerTitle>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<DrawerScrollList
|
|
87
|
+
unreadNotifications={unreadNotifications}
|
|
88
|
+
liveDurationMs={liveDurationMs}
|
|
89
|
+
onComplete={markRead}
|
|
90
|
+
onSettle={markSettled}
|
|
91
|
+
hasMore={hasMore}
|
|
92
|
+
isLoadingMore={isLoadingMore}
|
|
93
|
+
loadMore={loadMore}
|
|
94
|
+
loadMoreRootMargin={loadMoreRootMargin}
|
|
95
|
+
renderTile={renderTile}
|
|
96
|
+
/>
|
|
97
|
+
|
|
98
|
+
<div className="flex flex-col gap-[var(--spacing-system-xs)] px-[var(--spacing-system-m)] pb-[var(--spacing-system-m)]">
|
|
99
|
+
<div className="overflow-hidden rounded-md border border-ods-border bg-ods-card">
|
|
100
|
+
<ShowNotificationsToggleRow checked={showPopups} onChange={setShowPopups} />
|
|
101
|
+
{desktopPopupsConfigured && (
|
|
102
|
+
<DesktopNotificationsToggleRow checked={showDesktopPopups} onChange={setShowDesktopPopups} />
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
<NotificationsHistoryButton
|
|
106
|
+
onClick={onHistoryClick ? () => { onHistoryClick(); close() } : undefined}
|
|
107
|
+
historyHref={historyHref}
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
</>
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if (layoutContainer) {
|
|
114
|
+
return (
|
|
115
|
+
<AppLayoutDrawer open={isOpen} onOpenChange={(v) => (v ? open() : close())}>
|
|
116
|
+
<AppLayoutDrawerContent
|
|
117
|
+
side="right"
|
|
118
|
+
aria-describedby={undefined}
|
|
119
|
+
// md matches the content's default mobileBreakpoint: below it the
|
|
120
|
+
// panel is forced full-bleed (w-full), so the fixed width must not
|
|
121
|
+
// apply there or the panel would detach from the right edge.
|
|
122
|
+
className={cn('md:w-[26rem] gap-[var(--spacing-system-m)] p-[var(--spacing-system-zero)]', className)}
|
|
123
|
+
>
|
|
124
|
+
{content}
|
|
125
|
+
</AppLayoutDrawerContent>
|
|
126
|
+
</AppLayoutDrawer>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
59
130
|
return (
|
|
60
131
|
<Drawer open={isOpen} onOpenChange={(v) => (v ? open() : close())}>
|
|
61
132
|
<DrawerContent
|
|
@@ -67,49 +138,7 @@ export function NotificationDrawer({
|
|
|
67
138
|
className,
|
|
68
139
|
)}
|
|
69
140
|
>
|
|
70
|
-
|
|
71
|
-
<DrawerTitle
|
|
72
|
-
hideClose
|
|
73
|
-
className="truncate"
|
|
74
|
-
actions={
|
|
75
|
-
<button
|
|
76
|
-
type="button"
|
|
77
|
-
disabled={unreadCount === 0}
|
|
78
|
-
onClick={markAllRead}
|
|
79
|
-
className="self-center text-h6 text-ods-text-secondary underline transition-colors hover:text-ods-text-primary disabled:opacity-40"
|
|
80
|
-
>
|
|
81
|
-
Complete All
|
|
82
|
-
</button>
|
|
83
|
-
}
|
|
84
|
-
>
|
|
85
|
-
New Notifications
|
|
86
|
-
</DrawerTitle>
|
|
87
|
-
</div>
|
|
88
|
-
|
|
89
|
-
<DrawerScrollList
|
|
90
|
-
unreadNotifications={unreadNotifications}
|
|
91
|
-
liveDurationMs={liveDurationMs}
|
|
92
|
-
onComplete={markRead}
|
|
93
|
-
onSettle={markSettled}
|
|
94
|
-
hasMore={hasMore}
|
|
95
|
-
isLoadingMore={isLoadingMore}
|
|
96
|
-
loadMore={loadMore}
|
|
97
|
-
loadMoreRootMargin={loadMoreRootMargin}
|
|
98
|
-
renderTile={renderTile}
|
|
99
|
-
/>
|
|
100
|
-
|
|
101
|
-
<div className="flex flex-col gap-[var(--spacing-system-xs)] px-[var(--spacing-system-m)] pb-[var(--spacing-system-m)]">
|
|
102
|
-
<div className="overflow-hidden rounded-md border border-ods-border bg-ods-card">
|
|
103
|
-
<ShowNotificationsToggleRow checked={showPopups} onChange={setShowPopups} />
|
|
104
|
-
{desktopPopupsConfigured && (
|
|
105
|
-
<DesktopNotificationsToggleRow checked={showDesktopPopups} onChange={setShowDesktopPopups} />
|
|
106
|
-
)}
|
|
107
|
-
</div>
|
|
108
|
-
<NotificationsHistoryButton
|
|
109
|
-
onClick={onHistoryClick ? () => { onHistoryClick(); close() } : undefined}
|
|
110
|
-
historyHref={historyHref}
|
|
111
|
-
/>
|
|
112
|
-
</div>
|
|
141
|
+
{content}
|
|
113
142
|
</DrawerContent>
|
|
114
143
|
</Drawer>
|
|
115
144
|
)
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import * as PopoverPrimitive from '@radix-ui/react-popover'
|
|
4
|
+
import { useEffect, useRef } from 'react'
|
|
4
5
|
import { cn } from '../../../utils/cn'
|
|
5
6
|
import { ClockHistoryIcon } from '../../icons-v2-generated/date-and-time/clock-history-icon'
|
|
7
|
+
import {
|
|
8
|
+
type AppLayoutDrawerHandle,
|
|
9
|
+
useAppLayoutDrawerContainer,
|
|
10
|
+
useAppLayoutDrawerCoordination,
|
|
11
|
+
} from '../../navigation/app-layout-context'
|
|
6
12
|
import { HeaderButton } from '../../navigation/header-button'
|
|
7
13
|
import { OVERLAY_BACKDROP_CLASS } from '../../ui/drawer'
|
|
8
14
|
import { TimeTrackerPanel } from './time-tracker-panel'
|
|
@@ -19,8 +25,11 @@ export interface TimeTrackerHeaderButtonProps {
|
|
|
19
25
|
* Header affordance for the time tracker. Renders nothing unless wrapped in a
|
|
20
26
|
* `<TimeTrackerProvider>`. The trigger is the standard `HeaderButton`; when a
|
|
21
27
|
* session is active it also shows the live elapsed time. The popup is a Radix
|
|
22
|
-
* Popover anchored under the button (not a modal/drawer)
|
|
23
|
-
* dim
|
|
28
|
+
* Popover anchored under the button (not a modal/drawer). Inside AppLayout the
|
|
29
|
+
* dim backdrop covers only the main content area (header and sidebar stay
|
|
30
|
+
* interactive) and the popup joins the layout's panel coordination — opening
|
|
31
|
+
* it closes any open in-layout drawer and vice versa. Outside AppLayout it
|
|
32
|
+
* falls back to a viewport-wide backdrop.
|
|
24
33
|
*/
|
|
25
34
|
export function TimeTrackerHeaderButton({ className, disabled }: TimeTrackerHeaderButtonProps) {
|
|
26
35
|
const ctx = useOptionalTimeTracker()
|
|
@@ -29,10 +38,38 @@ export function TimeTrackerHeaderButton({ className, disabled }: TimeTrackerHead
|
|
|
29
38
|
runningSince: ctx?.runningSince,
|
|
30
39
|
accumulatedMs: ctx?.accumulatedMs,
|
|
31
40
|
})
|
|
41
|
+
const layoutContainer = useAppLayoutDrawerContainer()
|
|
42
|
+
const coordination = useAppLayoutDrawerCoordination()
|
|
43
|
+
|
|
44
|
+
const isOpen = ctx?.isOpen ?? false
|
|
45
|
+
const isOpenRef = useRef(isOpen)
|
|
46
|
+
isOpenRef.current = isOpen
|
|
47
|
+
const closeRef = useRef(ctx?.close)
|
|
48
|
+
closeRef.current = ctx?.close
|
|
49
|
+
// Stable handle: the same object must be registered AND passed as `self`
|
|
50
|
+
// to notifyDrawerDidOpen so the coordinator can skip it when closing the
|
|
51
|
+
// other panels.
|
|
52
|
+
const selfHandleRef = useRef<AppLayoutDrawerHandle | null>(null)
|
|
53
|
+
if (!selfHandleRef.current) {
|
|
54
|
+
selfHandleRef.current = {
|
|
55
|
+
close: () => {
|
|
56
|
+
if (isOpenRef.current) closeRef.current?.()
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (isOpen) coordination?.notifyDrawerDidOpen(selfHandleRef.current ?? undefined)
|
|
63
|
+
}, [isOpen, coordination])
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (!selfHandleRef.current) return
|
|
67
|
+
return coordination?.registerDrawer(selfHandleRef.current)
|
|
68
|
+
}, [coordination])
|
|
32
69
|
|
|
33
70
|
if (!ctx) return null
|
|
34
71
|
|
|
35
|
-
const {
|
|
72
|
+
const { open, close, status } = ctx
|
|
36
73
|
const isPaused = status === 'paused'
|
|
37
74
|
const isActive = status === 'tracking' || isPaused
|
|
38
75
|
|
|
@@ -72,13 +109,16 @@ export function TimeTrackerHeaderButton({ className, disabled }: TimeTrackerHead
|
|
|
72
109
|
</PopoverPrimitive.Trigger>
|
|
73
110
|
{/* Popovers have no built-in overlay — portal a dim backdrop behind the
|
|
74
111
|
panel. Clicks land on it (outside Content), so Radix still closes the
|
|
75
|
-
popover. Own Portal: Radix's Portal slots a single child.
|
|
76
|
-
|
|
112
|
+
popover. Own Portal: Radix's Portal slots a single child. Inside
|
|
113
|
+
AppLayout the backdrop portals into the main-area container (absolute,
|
|
114
|
+
AppLayoutDrawer-overlay tier) so header/sidebar stay undimmed and
|
|
115
|
+
interactive; standalone it dims the whole viewport. */}
|
|
116
|
+
<PopoverPrimitive.Portal container={layoutContainer ?? undefined}>
|
|
77
117
|
<div
|
|
78
118
|
aria-hidden="true"
|
|
79
119
|
data-state={isOpen ? 'open' : 'closed'}
|
|
80
120
|
className={cn(
|
|
81
|
-
'fixed inset-0 z-[1299]',
|
|
121
|
+
layoutContainer ? 'absolute inset-0 z-[102]' : 'fixed inset-0 z-[1299]',
|
|
82
122
|
OVERLAY_BACKDROP_CLASS,
|
|
83
123
|
'data-[state=open]:animate-in data-[state=open]:fade-in-0',
|
|
84
124
|
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0',
|
|
@@ -251,16 +251,20 @@ function NotificationsHeaderButton({
|
|
|
251
251
|
return (
|
|
252
252
|
<HeaderButton
|
|
253
253
|
icon={
|
|
254
|
-
|
|
255
|
-
<
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
254
|
+
isActive ? (
|
|
255
|
+
<XmarkIcon className="w-4 h-4 md:w-6 md:h-6" />
|
|
256
|
+
) : (
|
|
257
|
+
<div className="relative w-4 h-4 md:w-6 md:h-6">
|
|
258
|
+
<BellIcon className="w-full h-full" />
|
|
259
|
+
{hasUnread && (
|
|
260
|
+
<span
|
|
261
|
+
className="absolute top-0 right-0 bg-ods-warning rounded-full w-1.5 h-1.5 md:w-2 md:h-2"
|
|
262
|
+
/>
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
)
|
|
262
266
|
}
|
|
263
|
-
aria-label=
|
|
267
|
+
aria-label={isActive ? 'Close notifications' : 'Notifications'}
|
|
264
268
|
onClick={onClick}
|
|
265
269
|
isActive={isActive}
|
|
266
270
|
disabled={disabled || !onClick}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext } from 'react'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Container element that wraps `<main>` and serves as the portal target for
|
|
7
|
+
* `AppLayoutDrawer`. Drawers rendered into this container sit on top of the
|
|
8
|
+
* main content area only — the sidebar and header remain visible and
|
|
9
|
+
* interactive. Null when AppLayout hasn't mounted (or when used outside of it).
|
|
10
|
+
*
|
|
11
|
+
* Lives in its own module (not app-layout.tsx) so in-layout panels
|
|
12
|
+
* (AppLayoutDrawer, NotificationDrawer, TimeTrackerHeaderButton) can consume
|
|
13
|
+
* these contexts without importing AppLayout itself — AppLayout renders those
|
|
14
|
+
* panels, so importing it back would create a module cycle.
|
|
15
|
+
*/
|
|
16
|
+
export const AppLayoutDrawerContainerContext = createContext<HTMLElement | null>(null)
|
|
17
|
+
|
|
18
|
+
export function useAppLayoutDrawerContainer(): HTMLElement | null {
|
|
19
|
+
return useContext(AppLayoutDrawerContainerContext)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Coordination between AppLayout's in-layout panels (`AppLayoutDrawer`, the
|
|
24
|
+
* time-tracker popup) and the mobile burger menu. At most ONE of them may be
|
|
25
|
+
* open at a time — each dims the main area, so stacking them reads as broken:
|
|
26
|
+
* - a panel that opens calls `notifyDrawerDidOpen(self)` so the layout
|
|
27
|
+
* closes the burger menu AND every other registered panel;
|
|
28
|
+
* - each panel registers a close handle so opening the menu via the burger
|
|
29
|
+
* button (or another panel) closes it. Null outside of AppLayout.
|
|
30
|
+
*/
|
|
31
|
+
export interface AppLayoutDrawerHandle {
|
|
32
|
+
close: () => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AppLayoutDrawerCoordination {
|
|
36
|
+
/** `self` is skipped when closing the other registered panels. */
|
|
37
|
+
notifyDrawerDidOpen: (self?: AppLayoutDrawerHandle) => void
|
|
38
|
+
/** Returns an unregister cleanup. */
|
|
39
|
+
registerDrawer: (handle: AppLayoutDrawerHandle) => () => void
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const AppLayoutDrawerCoordinationContext = createContext<AppLayoutDrawerCoordination | null>(null)
|
|
43
|
+
|
|
44
|
+
export function useAppLayoutDrawerCoordination(): AppLayoutDrawerCoordination | null {
|
|
45
|
+
return useContext(AppLayoutDrawerCoordinationContext)
|
|
46
|
+
}
|
|
@@ -14,7 +14,11 @@ import {
|
|
|
14
14
|
OVERLAY_BACKDROP_CLASS,
|
|
15
15
|
type DrawerSide,
|
|
16
16
|
} from "../ui/drawer"
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
type AppLayoutDrawerHandle,
|
|
19
|
+
useAppLayoutDrawerContainer,
|
|
20
|
+
useAppLayoutDrawerCoordination,
|
|
21
|
+
} from "./app-layout-context"
|
|
18
22
|
|
|
19
23
|
/**
|
|
20
24
|
* AppLayoutDrawer is a Drawer variant that renders **inside** AppLayout's main
|
|
@@ -68,26 +72,32 @@ const AppLayoutDrawerRoot = ({
|
|
|
68
72
|
[isControlled, onOpenChange],
|
|
69
73
|
)
|
|
70
74
|
|
|
71
|
-
// Coordinate with AppLayout's
|
|
72
|
-
// opening
|
|
73
|
-
// drawer via the registered close handle. Refs keep the
|
|
74
|
-
//
|
|
75
|
+
// Coordinate with AppLayout's other in-layout panels and the mobile burger
|
|
76
|
+
// menu: opening this drawer closes them, and opening any of them closes
|
|
77
|
+
// this drawer via the registered close handle. Refs keep the handle
|
|
78
|
+
// identity stable across renders — the same object must be registered AND
|
|
79
|
+
// passed as `self` so the coordinator can skip it when closing the rest.
|
|
75
80
|
const coordination = useAppLayoutDrawerCoordination()
|
|
76
81
|
const openRef = React.useRef(open)
|
|
77
82
|
openRef.current = open
|
|
78
83
|
const handleOpenChangeRef = React.useRef(handleOpenChange)
|
|
79
84
|
handleOpenChangeRef.current = handleOpenChange
|
|
85
|
+
const selfHandleRef = React.useRef<AppLayoutDrawerHandle | null>(null)
|
|
86
|
+
if (!selfHandleRef.current) {
|
|
87
|
+
selfHandleRef.current = {
|
|
88
|
+
close: () => {
|
|
89
|
+
if (openRef.current) handleOpenChangeRef.current(false)
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
}
|
|
80
93
|
|
|
81
94
|
React.useEffect(() => {
|
|
82
|
-
if (open) coordination?.notifyDrawerDidOpen()
|
|
95
|
+
if (open) coordination?.notifyDrawerDidOpen(selfHandleRef.current ?? undefined)
|
|
83
96
|
}, [open, coordination])
|
|
84
97
|
|
|
85
98
|
React.useEffect(() => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (openRef.current) handleOpenChangeRef.current(false)
|
|
89
|
-
},
|
|
90
|
-
})
|
|
99
|
+
if (!selfHandleRef.current) return
|
|
100
|
+
return coordination?.registerDrawer(selfHandleRef.current)
|
|
91
101
|
}, [coordination])
|
|
92
102
|
|
|
93
103
|
return (
|
|
@@ -1,50 +1,21 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { Suspense, useCallback, useMemo, useRef, useState } from 'react'
|
|
4
4
|
import { NavigationSidebarConfig } from '../../types/navigation'
|
|
5
5
|
import { cn } from '../../utils'
|
|
6
6
|
import { NotificationDrawer } from '../features/notifications/notification-drawer'
|
|
7
7
|
import { AppHeader, AppHeaderProps } from './app-header'
|
|
8
|
+
import {
|
|
9
|
+
AppLayoutDrawerContainerContext,
|
|
10
|
+
type AppLayoutDrawerCoordination,
|
|
11
|
+
AppLayoutDrawerCoordinationContext,
|
|
12
|
+
type AppLayoutDrawerHandle,
|
|
13
|
+
} from './app-layout-context'
|
|
8
14
|
import { MobileBurgerMenu, MobileBurgerMenuProps } from './mobile-burger-menu'
|
|
9
15
|
import { NavigationSidebar } from './navigation-sidebar'
|
|
10
16
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
* `AppLayoutDrawer`. Drawers rendered into this container sit on top of the
|
|
14
|
-
* main content area only — the sidebar and header remain visible and
|
|
15
|
-
* interactive. Null when AppLayout hasn't mounted (or when used outside of it).
|
|
16
|
-
*/
|
|
17
|
-
const AppLayoutDrawerContainerContext = createContext<HTMLElement | null>(null)
|
|
18
|
-
|
|
19
|
-
export function useAppLayoutDrawerContainer(): HTMLElement | null {
|
|
20
|
-
return useContext(AppLayoutDrawerContainerContext)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Two-way coordination between AppLayout's mobile burger menu and in-layout
|
|
25
|
-
* drawers (`AppLayoutDrawer`), which render above the menu (z-[103] vs
|
|
26
|
-
* z-[101]) — the two must never be open at the same time:
|
|
27
|
-
* - a drawer that opens calls `notifyDrawerDidOpen` so the layout closes
|
|
28
|
-
* the menu (otherwise it would resurface once the drawer closes);
|
|
29
|
-
* - each drawer registers a close handle so opening the menu via the
|
|
30
|
-
* burger button closes any open drawer instead of hiding the menu
|
|
31
|
-
* underneath it. Null outside of AppLayout.
|
|
32
|
-
*/
|
|
33
|
-
export interface AppLayoutDrawerHandle {
|
|
34
|
-
close: () => void
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface AppLayoutDrawerCoordination {
|
|
38
|
-
notifyDrawerDidOpen: () => void
|
|
39
|
-
/** Returns an unregister cleanup. */
|
|
40
|
-
registerDrawer: (handle: AppLayoutDrawerHandle) => () => void
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const AppLayoutDrawerCoordinationContext = createContext<AppLayoutDrawerCoordination | null>(null)
|
|
44
|
-
|
|
45
|
-
export function useAppLayoutDrawerCoordination(): AppLayoutDrawerCoordination | null {
|
|
46
|
-
return useContext(AppLayoutDrawerCoordinationContext)
|
|
47
|
-
}
|
|
17
|
+
export { useAppLayoutDrawerContainer, useAppLayoutDrawerCoordination } from './app-layout-context'
|
|
18
|
+
export type { AppLayoutDrawerHandle } from './app-layout-context'
|
|
48
19
|
|
|
49
20
|
export interface AppLayoutProps {
|
|
50
21
|
children: React.ReactNode
|
|
@@ -113,7 +84,14 @@ export function AppLayout({
|
|
|
113
84
|
}, [])
|
|
114
85
|
|
|
115
86
|
const drawerCoordination = useMemo<AppLayoutDrawerCoordination>(() => ({
|
|
116
|
-
notifyDrawerDidOpen: () =>
|
|
87
|
+
notifyDrawerDidOpen: (self) => {
|
|
88
|
+
setMobileMenuOpen(false)
|
|
89
|
+
// Only one in-layout panel may be open at a time — each dims the main
|
|
90
|
+
// area, so stacking them reads as broken.
|
|
91
|
+
for (const handle of drawerHandlesRef.current) {
|
|
92
|
+
if (handle !== self) handle.close()
|
|
93
|
+
}
|
|
94
|
+
},
|
|
117
95
|
registerDrawer: (handle) => {
|
|
118
96
|
drawerHandlesRef.current.add(handle)
|
|
119
97
|
return () => drawerHandlesRef.current.delete(handle)
|
|
@@ -4,34 +4,31 @@ import type React from "react";
|
|
|
4
4
|
import { ApprovalBatchMessage } from "../components/chat/approval-batch-message";
|
|
5
5
|
import type {
|
|
6
6
|
ApprovalBatchData,
|
|
7
|
-
ApprovalBatchExecutionState,
|
|
8
7
|
PendingToolCallData,
|
|
9
8
|
} from "../components/chat/types";
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
|
-
* CLIENT (Fae) variant of the
|
|
11
|
+
* CLIENT (end-user Fae desktop app) variant of the approval block — Figma
|
|
12
|
+
* node 203-11947 ("fae-approval-block").
|
|
13
13
|
*
|
|
14
|
-
* Same component and props as the Admin card, driven by `
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* Same component and props as the Admin card, driven by `variant="client"`.
|
|
15
|
+
* The end client sees ONLY the BE-generated title plus the Approve/Reject
|
|
16
|
+
* buttons, or the full-text resolved pill ("Approved by Michael Johnson").
|
|
17
|
+
* Commands, scripts, expandable args/results and execution icons are never
|
|
18
|
+
* rendered in this variant.
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
const LONG_COMMAND = `Get-CimInstance Win32_Process |
|
|
21
22
|
Select-Object ProcessId, Name, Path, CommandLine |
|
|
22
23
|
Where-Object { $_.Path -like 'C:\\Users\\Public\\*' }`;
|
|
23
24
|
|
|
24
|
-
const RESULT = `pid | name | path | cmdline
|
|
25
|
-
4821 | suspicious.exe | C:\\Users\\Public\\suspicious.exe | suspicious.exe --hidden --connect 185.220.101.45`;
|
|
26
|
-
|
|
27
25
|
const REQUEST_ID = "req-1";
|
|
28
26
|
|
|
29
27
|
const toolCall: PendingToolCallData = {
|
|
30
28
|
toolExecutionRequestId: REQUEST_ID,
|
|
31
29
|
toolName: "run_script",
|
|
32
30
|
toolTitle: "Run Script",
|
|
33
|
-
//
|
|
34
|
-
// the raw command moves into the expanded body (Figma 1972-6100).
|
|
31
|
+
// BE-generated human-readable title — the ONLY content the client sees.
|
|
35
32
|
toolExplanation:
|
|
36
33
|
"Collects today's system, application, and security event logs and exports them to a CSV file.",
|
|
37
34
|
toolType: "OPENFRAME_RMM",
|
|
@@ -44,39 +41,12 @@ const toolCall: PendingToolCallData = {
|
|
|
44
41
|
},
|
|
45
42
|
};
|
|
46
43
|
|
|
47
|
-
const makeData = (
|
|
48
|
-
executions?: Record<string, ApprovalBatchExecutionState>,
|
|
49
|
-
): ApprovalBatchData => ({
|
|
44
|
+
const makeData = (): ApprovalBatchData => ({
|
|
50
45
|
approvalRequestId: "batch-1",
|
|
51
46
|
approvalType: "CLIENT",
|
|
52
47
|
toolCalls: [toolCall],
|
|
53
|
-
executions,
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const executingData = makeData({ [REQUEST_ID]: { status: "executing" } });
|
|
57
|
-
const doneData = makeData({
|
|
58
|
-
[REQUEST_ID]: { status: "done", result: RESULT, success: true },
|
|
59
48
|
});
|
|
60
49
|
|
|
61
|
-
/**
|
|
62
|
-
* The assistant's message text that precedes the command block in the real
|
|
63
|
-
* chat (Figma "Command description text."). It is NOT part of the component —
|
|
64
|
-
* shown here only so the story matches the Figma frame.
|
|
65
|
-
*/
|
|
66
|
-
const CommandHeading = () => (
|
|
67
|
-
<p className="text-h4 text-ods-text-primary mb-[var(--spacing-system-xxs)]">
|
|
68
|
-
Command description text.
|
|
69
|
-
</p>
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
const constrainedDecorator =
|
|
73
|
-
(width?: number) => (Story: React.ComponentType) => (
|
|
74
|
-
<div style={{ maxWidth: width ?? 400, background: "var(--color-bg)" }}>
|
|
75
|
-
<CommandHeading />
|
|
76
|
-
<Story />
|
|
77
|
-
</div>
|
|
78
|
-
);
|
|
79
|
-
|
|
80
50
|
const plainDecorator =
|
|
81
51
|
(width?: number) => (Story: React.ComponentType) => (
|
|
82
52
|
<div style={{ maxWidth: width ?? 400, background: "var(--color-bg)" }}>
|
|
@@ -88,22 +58,22 @@ const meta = {
|
|
|
88
58
|
title: "Chat/Client/ApprovalBatchMessage",
|
|
89
59
|
component: ApprovalBatchMessage,
|
|
90
60
|
tags: ["autodocs"],
|
|
91
|
-
args: {
|
|
61
|
+
args: { variant: "client" },
|
|
92
62
|
parameters: {
|
|
93
63
|
docs: {
|
|
94
64
|
description: {
|
|
95
65
|
component:
|
|
96
|
-
"CLIENT (Fae)
|
|
66
|
+
"CLIENT (Fae end-user) approval block — title-only card with Approve/Reject or a full-text status pill. Commands/scripts are never shown. Gated by variant='client' (Figma 203-11947).",
|
|
97
67
|
},
|
|
98
68
|
},
|
|
99
69
|
},
|
|
100
|
-
decorators: [
|
|
70
|
+
decorators: [plainDecorator()],
|
|
101
71
|
} satisfies Meta<typeof ApprovalBatchMessage>;
|
|
102
72
|
|
|
103
73
|
export default meta;
|
|
104
74
|
type Story = StoryObj<typeof meta>;
|
|
105
75
|
|
|
106
|
-
/** Pending — Approve / Reject buttons.
|
|
76
|
+
/** Pending — title + Approve / Reject buttons. No expandable command. */
|
|
107
77
|
export const Pending: Story = {
|
|
108
78
|
args: {
|
|
109
79
|
data: makeData(),
|
|
@@ -113,19 +83,10 @@ export const Pending: Story = {
|
|
|
113
83
|
},
|
|
114
84
|
};
|
|
115
85
|
|
|
116
|
-
/** Approved
|
|
117
|
-
export const
|
|
86
|
+
/** Approved — full-text green status pill. */
|
|
87
|
+
export const Approved: Story = {
|
|
118
88
|
args: {
|
|
119
|
-
data:
|
|
120
|
-
status: "approved",
|
|
121
|
-
resolvedByName: "Michael Johnson",
|
|
122
|
-
},
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
/** Approved and finished — green check + expandable Result block. */
|
|
126
|
-
export const ApprovedDone: Story = {
|
|
127
|
-
args: {
|
|
128
|
-
data: doneData,
|
|
89
|
+
data: makeData(),
|
|
129
90
|
status: "approved",
|
|
130
91
|
resolvedByName: "Michael Johnson",
|
|
131
92
|
},
|
|
@@ -150,25 +111,22 @@ export const Cancelled: Story = {
|
|
|
150
111
|
};
|
|
151
112
|
|
|
152
113
|
/**
|
|
153
|
-
* All states side by side — mirrors the Figma CLIENT
|
|
114
|
+
* All states side by side — mirrors the Figma CLIENT approval-block frames
|
|
115
|
+
* (1-6575 / 1-6621 / 1-6637 / 1-6653).
|
|
154
116
|
*/
|
|
155
117
|
export const AllStates: Story = {
|
|
156
118
|
decorators: [plainDecorator(420)],
|
|
157
119
|
render: () => {
|
|
158
120
|
const states = [
|
|
159
121
|
{ data: makeData(), status: "pending" as const, onApprove: () => {}, onReject: () => {} },
|
|
160
|
-
{ data:
|
|
161
|
-
{ data: doneData, status: "approved" as const, resolvedByName: "Michael Johnson" },
|
|
122
|
+
{ data: makeData(), status: "approved" as const, resolvedByName: "Michael Johnson" },
|
|
162
123
|
{ data: makeData(), status: "rejected" as const, resolvedByName: "Michael Johnson" },
|
|
163
124
|
{ data: makeData(), status: "cancelled" as const, resolvedByName: "Michael Johnson" },
|
|
164
125
|
];
|
|
165
126
|
return (
|
|
166
127
|
<div style={{ display: "flex", flexDirection: "column", gap: 32 }}>
|
|
167
128
|
{states.map((s, i) => (
|
|
168
|
-
<
|
|
169
|
-
<CommandHeading />
|
|
170
|
-
<ApprovalBatchMessage assistantType="fae" {...s} />
|
|
171
|
-
</div>
|
|
129
|
+
<ApprovalBatchMessage key={i} variant="client" {...s} />
|
|
172
130
|
))}
|
|
173
131
|
</div>
|
|
174
132
|
);
|