@flamingo-stack/openframe-frontend-core 0.0.225 → 0.0.227
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-2QURHTDV.js → chunk-CUBRHI6D.js} +2 -2
- package/dist/{chunk-NRJ53MIF.cjs → chunk-E3DB4D43.cjs} +9 -9
- package/dist/{chunk-NRJ53MIF.cjs.map → chunk-E3DB4D43.cjs.map} +1 -1
- package/dist/{chunk-3YTIV6C5.cjs → chunk-E3WJQQGW.cjs} +7 -7
- package/dist/{chunk-3YTIV6C5.cjs.map → chunk-E3WJQQGW.cjs.map} +1 -1
- package/dist/{chunk-SAJG77KY.js → chunk-EURLSRHW.js} +2 -2
- package/dist/{chunk-IWBDAGZQ.cjs → chunk-HO2VEMEX.cjs} +24 -24
- package/dist/{chunk-IWBDAGZQ.cjs.map → chunk-HO2VEMEX.cjs.map} +1 -1
- package/dist/{chunk-NHRVPMBU.js → chunk-K252FYE5.js} +3959 -3740
- package/dist/chunk-K252FYE5.js.map +1 -0
- package/dist/{chunk-5RELSRTS.cjs → chunk-LEM4FLCU.cjs} +10 -10
- package/dist/{chunk-5RELSRTS.cjs.map → chunk-LEM4FLCU.cjs.map} +1 -1
- package/dist/{chunk-HOQDRQ4M.cjs → chunk-QFC725WU.cjs} +746 -527
- package/dist/chunk-QFC725WU.cjs.map +1 -0
- package/dist/{chunk-RIEEPMJ6.js → chunk-X3KKG6IU.js} +2 -2
- package/dist/{chunk-NUOYDHUM.js → chunk-YE62NF6I.js} +2 -2
- package/dist/components/chat/approval-batch-message.d.ts +6 -0
- package/dist/components/chat/approval-batch-message.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +2 -2
- package/dist/components/chat/index.js +1 -1
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/features/index.cjs +10 -2
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.js +9 -1
- package/dist/components/features/notifications/approval-request-notification-tile.d.ts +13 -0
- package/dist/components/features/notifications/approval-request-notification-tile.d.ts.map +1 -0
- package/dist/components/features/notifications/index.d.ts +4 -1
- package/dist/components/features/notifications/index.d.ts.map +1 -1
- package/dist/components/features/notifications/notification-drawer.d.ts +3 -1
- package/dist/components/features/notifications/notification-drawer.d.ts.map +1 -1
- package/dist/components/features/notifications/notification-popups.d.ts +3 -1
- package/dist/components/features/notifications/notification-popups.d.ts.map +1 -1
- package/dist/components/features/notifications/notification-tile.d.ts +6 -1
- package/dist/components/features/notifications/notification-tile.d.ts.map +1 -1
- package/dist/components/features/notifications/notifications-context.d.ts +5 -2
- package/dist/components/features/notifications/notifications-context.d.ts.map +1 -1
- package/dist/components/features/notifications/types.d.ts +30 -0
- package/dist/components/features/notifications/types.d.ts.map +1 -1
- package/dist/components/index.cjs +87 -77
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +14 -4
- package/dist/components/index.js.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 +25 -25
- package/dist/components/onboarding-guides/index.js +3 -3
- package/dist/components/tickets/index.cjs +64 -64
- package/dist/components/tickets/index.js +4 -4
- package/dist/components/ui/color-preset-select.d.ts.map +1 -1
- package/dist/components/ui/color-swatch.d.ts +7 -0
- package/dist/components/ui/color-swatch.d.ts.map +1 -0
- package/dist/components/ui/data-table/data-table-body.d.ts +8 -1
- package/dist/components/ui/data-table/data-table-body.d.ts.map +1 -1
- package/dist/components/ui/data-table/data-table-row.d.ts +4 -2
- package/dist/components/ui/data-table/data-table-row.d.ts.map +1 -1
- package/dist/components/ui/data-table/index.d.ts +1 -1
- package/dist/components/ui/index.cjs +4 -2
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +3 -1
- package/dist/components/ui/ticket-status-config-row.d.ts.map +1 -1
- package/dist/components/ui/ticket-status-tag.d.ts.map +1 -1
- package/dist/index.cjs +12 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +11 -1
- package/package.json +1 -1
- package/src/components/chat/approval-batch-message.tsx +26 -15
- package/src/components/features/notifications/approval-request-notification-tile.tsx +118 -0
- package/src/components/features/notifications/index.ts +10 -0
- package/src/components/features/notifications/notification-drawer.tsx +28 -11
- package/src/components/features/notifications/notification-popups.tsx +33 -19
- package/src/components/features/notifications/notification-tile.tsx +34 -19
- package/src/components/features/notifications/notifications-context.tsx +7 -1
- package/src/components/features/notifications/types.ts +50 -0
- package/src/components/ui/color-preset-select.tsx +1 -10
- package/src/components/ui/color-swatch.tsx +17 -0
- package/src/components/ui/data-table/data-table-body.tsx +9 -0
- package/src/components/ui/data-table/data-table-row.tsx +24 -3
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/modal-v2.tsx +2 -2
- package/src/components/ui/ticket-status-config-row.tsx +78 -39
- package/src/components/ui/ticket-status-tag.tsx +2 -7
- package/src/stories/NotificationDrawer.stories.tsx +148 -1
- package/dist/chunk-HOQDRQ4M.cjs.map +0 -1
- package/dist/chunk-NHRVPMBU.js.map +0 -1
- /package/dist/{chunk-2QURHTDV.js.map → chunk-CUBRHI6D.js.map} +0 -0
- /package/dist/{chunk-SAJG77KY.js.map → chunk-EURLSRHW.js.map} +0 -0
- /package/dist/{chunk-RIEEPMJ6.js.map → chunk-X3KKG6IU.js.map} +0 -0
- /package/dist/{chunk-NUOYDHUM.js.map → chunk-YE62NF6I.js.map} +0 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState } from 'react'
|
|
4
|
+
import { cn } from '../../../utils/cn'
|
|
5
|
+
import { ApprovalBatchMessage } from '../../chat/approval-batch-message'
|
|
6
|
+
import { ExpandChevron } from '../../chat/expand-chevron'
|
|
7
|
+
import { useCollapsible } from '../../chat/hooks/use-collapsible'
|
|
8
|
+
import type { ApprovalBatchData, ChatApprovalStatus } from '../../chat/types'
|
|
9
|
+
import { NotificationTile } from './notification-tile'
|
|
10
|
+
import { getApprovalMeta } from './types'
|
|
11
|
+
import type { Notification } from './types'
|
|
12
|
+
|
|
13
|
+
export interface ApprovalRequestNotificationTileProps {
|
|
14
|
+
notification: Notification
|
|
15
|
+
onApprove: (approvalRequestId: string) => void | Promise<void>
|
|
16
|
+
onReject: (approvalRequestId: string) => void | Promise<void>
|
|
17
|
+
onComplete: (id: string) => void
|
|
18
|
+
onSettle?: (id: string) => void
|
|
19
|
+
liveDurationMs?: number
|
|
20
|
+
defaultExpanded?: boolean
|
|
21
|
+
className?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ApprovalRequestNotificationTile({
|
|
25
|
+
notification,
|
|
26
|
+
onApprove,
|
|
27
|
+
onReject,
|
|
28
|
+
onComplete,
|
|
29
|
+
onSettle,
|
|
30
|
+
liveDurationMs,
|
|
31
|
+
defaultExpanded = false,
|
|
32
|
+
className,
|
|
33
|
+
}: ApprovalRequestNotificationTileProps) {
|
|
34
|
+
const [expanded, setExpanded] = useState(defaultExpanded)
|
|
35
|
+
// Toggling the command section pins the tile so a live pop-up doesn't
|
|
36
|
+
// auto-dismiss out from under the user mid-read.
|
|
37
|
+
const [pinned, setPinned] = useState(defaultExpanded)
|
|
38
|
+
const [status, setStatus] = useState<ChatApprovalStatus>('pending')
|
|
39
|
+
|
|
40
|
+
const approval = getApprovalMeta(notification)
|
|
41
|
+
|
|
42
|
+
const batchData = useMemo<ApprovalBatchData | null>(() => {
|
|
43
|
+
if (!approval) return null
|
|
44
|
+
return {
|
|
45
|
+
approvalRequestId: approval.approvalRequestId,
|
|
46
|
+
approvalType: approval.approvalType ?? '',
|
|
47
|
+
toolCalls: approval.toolCalls.map((tc, index) => ({
|
|
48
|
+
toolExecutionRequestId: tc.toolExecutionRequestId ?? `${approval.approvalRequestId}:${index}`,
|
|
49
|
+
toolName: tc.toolName,
|
|
50
|
+
toolTitle: tc.toolTitle ?? undefined,
|
|
51
|
+
toolExplanation: tc.toolExplanation ?? undefined,
|
|
52
|
+
toolType: tc.toolType ?? undefined,
|
|
53
|
+
requiresApproval: tc.requiresApproval ?? true,
|
|
54
|
+
approvalType: tc.approvalType ?? null,
|
|
55
|
+
toolCallArguments: tc.toolCallArguments ?? null,
|
|
56
|
+
})),
|
|
57
|
+
}
|
|
58
|
+
}, [approval])
|
|
59
|
+
|
|
60
|
+
const { innerRef, containerStyle } = useCollapsible({ expanded })
|
|
61
|
+
|
|
62
|
+
if (!approval || !batchData) return null
|
|
63
|
+
|
|
64
|
+
const commandWord = batchData.toolCalls.length > 1 ? 'Commands' : 'Command'
|
|
65
|
+
const toggleLabel = `${expanded ? 'Hide' : 'Show'} ${commandWord}`
|
|
66
|
+
|
|
67
|
+
const handleApprove = async () => {
|
|
68
|
+
await onApprove(batchData.approvalRequestId)
|
|
69
|
+
setStatus('approved')
|
|
70
|
+
onComplete(notification.id)
|
|
71
|
+
}
|
|
72
|
+
const handleReject = async () => {
|
|
73
|
+
await onReject(batchData.approvalRequestId)
|
|
74
|
+
setStatus('rejected')
|
|
75
|
+
onComplete(notification.id)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<NotificationTile
|
|
80
|
+
notification={notification}
|
|
81
|
+
liveDurationMs={liveDurationMs}
|
|
82
|
+
onComplete={onComplete}
|
|
83
|
+
onSettle={onSettle}
|
|
84
|
+
className={className}
|
|
85
|
+
paused={pinned}
|
|
86
|
+
>
|
|
87
|
+
{/* Persistent collapse/expand control for the command section. */}
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
onClick={() => {
|
|
91
|
+
setExpanded(prev => !prev)
|
|
92
|
+
setPinned(true)
|
|
93
|
+
}}
|
|
94
|
+
aria-expanded={expanded}
|
|
95
|
+
className={cn(
|
|
96
|
+
'flex w-full items-center gap-[var(--spacing-system-xs)] bg-ods-card px-[var(--spacing-system-s)] py-[var(--spacing-system-xs)] text-left',
|
|
97
|
+
expanded && 'border-b border-ods-border',
|
|
98
|
+
)}
|
|
99
|
+
>
|
|
100
|
+
<span className="min-w-0 flex-1 text-h6 text-ods-text-primary">{toggleLabel}</span>
|
|
101
|
+
<ExpandChevron expanded={expanded} />
|
|
102
|
+
</button>
|
|
103
|
+
|
|
104
|
+
<div style={containerStyle}>
|
|
105
|
+
<div ref={innerRef}>
|
|
106
|
+
<ApprovalBatchMessage
|
|
107
|
+
data={batchData}
|
|
108
|
+
status={status}
|
|
109
|
+
onApprove={handleApprove}
|
|
110
|
+
onReject={handleReject}
|
|
111
|
+
maxBodyHeight="50vh"
|
|
112
|
+
className="mb-0 rounded-none border-0"
|
|
113
|
+
/>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</NotificationTile>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
@@ -10,9 +10,19 @@ export { NotificationTile } from './notification-tile'
|
|
|
10
10
|
export type { NotificationTileProps } from './notification-tile'
|
|
11
11
|
export { NotificationPopups } from './notification-popups'
|
|
12
12
|
export type { NotificationPopupsProps, NotificationPopupsPosition } from './notification-popups'
|
|
13
|
+
export { ApprovalRequestNotificationTile } from './approval-request-notification-tile'
|
|
14
|
+
export type { ApprovalRequestNotificationTileProps } from './approval-request-notification-tile'
|
|
15
|
+
export {
|
|
16
|
+
ADMIN_APPROVAL_REQUEST_CONTEXT_TYPE,
|
|
17
|
+
getApprovalMeta,
|
|
18
|
+
isApprovalNotification,
|
|
19
|
+
} from './types'
|
|
13
20
|
export type {
|
|
14
21
|
Notification,
|
|
15
22
|
NotificationVariant,
|
|
16
23
|
NotificationSeverity,
|
|
17
24
|
AddNotificationInput,
|
|
25
|
+
ApprovalNotificationMeta,
|
|
26
|
+
ApprovalToolCallMeta,
|
|
27
|
+
RenderNotificationTile,
|
|
18
28
|
} from './types'
|
|
@@ -10,15 +10,22 @@ import { Switch } from '../../ui/switch'
|
|
|
10
10
|
import { cn } from '../../../utils/cn'
|
|
11
11
|
import { useOptionalNotifications } from './notifications-context'
|
|
12
12
|
import { NotificationTile } from './notification-tile'
|
|
13
|
-
import type { Notification } from './types'
|
|
13
|
+
import type { Notification, RenderNotificationTile } from './types'
|
|
14
|
+
|
|
14
15
|
|
|
15
16
|
export interface NotificationDrawerProps {
|
|
16
17
|
className?: string
|
|
17
18
|
liveDurationMs?: number
|
|
18
19
|
loadMoreRootMargin?: string
|
|
20
|
+
renderTile?: RenderNotificationTile
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
export function NotificationDrawer({
|
|
23
|
+
export function NotificationDrawer({
|
|
24
|
+
className,
|
|
25
|
+
liveDurationMs,
|
|
26
|
+
loadMoreRootMargin = '200px',
|
|
27
|
+
renderTile: renderTileProp,
|
|
28
|
+
}: NotificationDrawerProps) {
|
|
22
29
|
const ctx = useOptionalNotifications()
|
|
23
30
|
if (!ctx) return null
|
|
24
31
|
|
|
@@ -37,8 +44,11 @@ export function NotificationDrawer({ className, liveDurationMs, loadMoreRootMarg
|
|
|
37
44
|
hasMore,
|
|
38
45
|
isLoadingMore,
|
|
39
46
|
loadMore,
|
|
47
|
+
renderTile: ctxRenderTile,
|
|
40
48
|
} = ctx
|
|
41
49
|
|
|
50
|
+
const renderTile = renderTileProp ?? ctxRenderTile
|
|
51
|
+
|
|
42
52
|
const unreadNotifications = notifications.filter((n) => !n.read)
|
|
43
53
|
|
|
44
54
|
return (
|
|
@@ -74,6 +84,7 @@ export function NotificationDrawer({ className, liveDurationMs, loadMoreRootMarg
|
|
|
74
84
|
isLoadingMore={isLoadingMore}
|
|
75
85
|
loadMore={loadMore}
|
|
76
86
|
loadMoreRootMargin={loadMoreRootMargin}
|
|
87
|
+
renderTile={renderTile}
|
|
77
88
|
/>
|
|
78
89
|
|
|
79
90
|
<div className="flex flex-col gap-[var(--spacing-system-xs)] px-[var(--spacing-system-m)] pb-[var(--spacing-system-m)]">
|
|
@@ -96,6 +107,7 @@ interface DrawerScrollListProps {
|
|
|
96
107
|
isLoadingMore: boolean
|
|
97
108
|
loadMore?: () => void
|
|
98
109
|
loadMoreRootMargin: string
|
|
110
|
+
renderTile?: RenderNotificationTile
|
|
99
111
|
}
|
|
100
112
|
|
|
101
113
|
function DrawerScrollList({
|
|
@@ -107,6 +119,7 @@ function DrawerScrollList({
|
|
|
107
119
|
isLoadingMore,
|
|
108
120
|
loadMore,
|
|
109
121
|
loadMoreRootMargin,
|
|
122
|
+
renderTile,
|
|
110
123
|
}: DrawerScrollListProps) {
|
|
111
124
|
const scrollRef = useRef<HTMLDivElement>(null)
|
|
112
125
|
const sentinelRef = useRef<HTMLDivElement>(null)
|
|
@@ -138,15 +151,19 @@ function DrawerScrollList({
|
|
|
138
151
|
<EmptyState />
|
|
139
152
|
) : (
|
|
140
153
|
<>
|
|
141
|
-
{unreadNotifications.map((n) =>
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
154
|
+
{unreadNotifications.map((n) => {
|
|
155
|
+
const custom = renderTile?.(n, { onComplete, onSettle, liveDurationMs })
|
|
156
|
+
if (custom) return <div key={n.id}>{custom}</div>
|
|
157
|
+
return (
|
|
158
|
+
<NotificationTile
|
|
159
|
+
key={n.id}
|
|
160
|
+
notification={n}
|
|
161
|
+
liveDurationMs={liveDurationMs}
|
|
162
|
+
onComplete={onComplete}
|
|
163
|
+
onSettle={onSettle}
|
|
164
|
+
/>
|
|
165
|
+
)
|
|
166
|
+
})}
|
|
150
167
|
{isLoadingMore && <DrawerLoadingTiles />}
|
|
151
168
|
{loadMore && hasMore && <div ref={sentinelRef} className="h-1" aria-hidden="true" />}
|
|
152
169
|
</>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import type { KeyboardEvent, MouseEvent } from 'react'
|
|
4
4
|
import { cn } from '../../../utils/cn'
|
|
5
|
+
import type { RenderNotificationTile } from './types'
|
|
5
6
|
import { useOptionalNotifications } from './notifications-context'
|
|
6
7
|
import { NotificationTile } from './notification-tile'
|
|
7
8
|
import type { Notification } from './types'
|
|
@@ -18,6 +19,7 @@ export interface NotificationPopupsProps {
|
|
|
18
19
|
maxVisible?: number
|
|
19
20
|
position?: NotificationPopupsPosition
|
|
20
21
|
hideWhenDrawerOpen?: boolean
|
|
22
|
+
renderTile?: RenderNotificationTile
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
const positionClasses: Record<NotificationPopupsPosition, string> = {
|
|
@@ -33,11 +35,13 @@ export function NotificationPopups({
|
|
|
33
35
|
maxVisible = 4,
|
|
34
36
|
position = 'top-right',
|
|
35
37
|
hideWhenDrawerOpen = true,
|
|
38
|
+
renderTile: renderTileProp,
|
|
36
39
|
}: NotificationPopupsProps) {
|
|
37
40
|
const ctx = useOptionalNotifications()
|
|
38
41
|
if (!ctx) return null
|
|
39
42
|
|
|
40
|
-
const { notifications, showPopups, isOpen, open, markRead, markSettled } = ctx
|
|
43
|
+
const { notifications, showPopups, isOpen, open, markRead, markSettled, renderTile: ctxRenderTile } = ctx
|
|
44
|
+
const renderTile = renderTileProp ?? ctxRenderTile
|
|
41
45
|
|
|
42
46
|
if (!showPopups) return null
|
|
43
47
|
if (hideWhenDrawerOpen && isOpen) return null
|
|
@@ -82,24 +86,34 @@ export function NotificationPopups({
|
|
|
82
86
|
className,
|
|
83
87
|
)}
|
|
84
88
|
>
|
|
85
|
-
{live.map((n) =>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
89
|
+
{live.map((n) => {
|
|
90
|
+
const custom = renderTile?.(n, { onComplete: markRead, onSettle: markSettled, liveDurationMs })
|
|
91
|
+
if (custom) {
|
|
92
|
+
return (
|
|
93
|
+
<div key={n.id} className="pointer-events-auto">
|
|
94
|
+
{custom}
|
|
95
|
+
</div>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
return (
|
|
99
|
+
// biome-ignore lint/a11y/useSemanticElements: nested interactive elements forbid <button>
|
|
100
|
+
<div
|
|
101
|
+
key={n.id}
|
|
102
|
+
role="button"
|
|
103
|
+
tabIndex={0}
|
|
104
|
+
onClick={handleBodyClick(n)}
|
|
105
|
+
onKeyDown={handleBodyKeyDown(n)}
|
|
106
|
+
className="pointer-events-auto cursor-pointer rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent"
|
|
107
|
+
>
|
|
108
|
+
<NotificationTile
|
|
109
|
+
notification={n}
|
|
110
|
+
liveDurationMs={liveDurationMs}
|
|
111
|
+
onComplete={markRead}
|
|
112
|
+
onSettle={markSettled}
|
|
113
|
+
/>
|
|
114
|
+
</div>
|
|
115
|
+
)
|
|
116
|
+
})}
|
|
103
117
|
</div>
|
|
104
118
|
)
|
|
105
119
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useMemo, useEffect} from 'react'
|
|
3
|
+
import { useMemo, useEffect, type ReactNode } from 'react'
|
|
4
4
|
import { CheckCircleIcon } from '../../icons-v2-generated/signs-and-symbols/check-circle-icon'
|
|
5
5
|
import { XmarkIcon } from '../../icons-v2-generated/signs-and-symbols/xmark-icon'
|
|
6
6
|
import { Button } from '../../ui/button/button'
|
|
@@ -14,6 +14,10 @@ export interface NotificationTileProps {
|
|
|
14
14
|
onComplete: (id: string) => void
|
|
15
15
|
onSettle?: (id: string) => void
|
|
16
16
|
className?: string
|
|
17
|
+
/** Extra content rendered below the header inside the same card (e.g. an approval command section). */
|
|
18
|
+
children?: ReactNode
|
|
19
|
+
/** Pin the tile: cancel the live auto-dismiss countdown (timer + progress bar) without settling it. */
|
|
20
|
+
paused?: boolean
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
export function NotificationTile({
|
|
@@ -22,20 +26,23 @@ export function NotificationTile({
|
|
|
22
26
|
onComplete,
|
|
23
27
|
onSettle,
|
|
24
28
|
className,
|
|
29
|
+
children,
|
|
30
|
+
paused = false,
|
|
25
31
|
}: NotificationTileProps) {
|
|
26
32
|
const { id, variant = 'default', title, description, createdAt, read, settled } = notification
|
|
27
33
|
|
|
28
34
|
const initialElapsed = useMemo(() => Date.now() - createdAt, [createdAt])
|
|
29
35
|
const isLive = !read && !settled && initialElapsed < liveDurationMs
|
|
36
|
+
const counting = isLive && !paused
|
|
30
37
|
|
|
31
38
|
useEffect(() => {
|
|
32
|
-
if (!
|
|
39
|
+
if (!counting) return
|
|
33
40
|
const remaining = Math.max(0, liveDurationMs - initialElapsed)
|
|
34
41
|
const timer = window.setTimeout(() => {
|
|
35
42
|
onSettle?.(id)
|
|
36
43
|
}, remaining)
|
|
37
44
|
return () => window.clearTimeout(timer)
|
|
38
|
-
}, [id,
|
|
45
|
+
}, [id, counting, initialElapsed, liveDurationMs, onSettle])
|
|
39
46
|
|
|
40
47
|
return (
|
|
41
48
|
<output
|
|
@@ -44,7 +51,12 @@ export function NotificationTile({
|
|
|
44
51
|
className,
|
|
45
52
|
)}
|
|
46
53
|
>
|
|
47
|
-
<div
|
|
54
|
+
<div
|
|
55
|
+
className={cn(
|
|
56
|
+
'relative flex items-center gap-[var(--spacing-system-xs)] p-[var(--spacing-system-s)]',
|
|
57
|
+
children ? 'border-b border-ods-border' : null,
|
|
58
|
+
)}
|
|
59
|
+
>
|
|
48
60
|
<div className="flex size-6 shrink-0 items-center justify-center">
|
|
49
61
|
<span className={cn('size-1.5 rounded-full', dotColorByVariant[variant])} />
|
|
50
62
|
</div>
|
|
@@ -64,14 +76,31 @@ export function NotificationTile({
|
|
|
64
76
|
aria-label="Mark notification complete"
|
|
65
77
|
tabIndex={isLive ? -1 : 0}
|
|
66
78
|
className={cn(
|
|
67
|
-
'shrink-0
|
|
79
|
+
'shrink-0 hover:text-ods-text-primary',
|
|
68
80
|
isLive && 'invisible',
|
|
69
81
|
)}
|
|
70
82
|
>
|
|
71
83
|
<CheckCircleIcon />
|
|
72
84
|
</Button>
|
|
85
|
+
|
|
86
|
+
{/* Progress sits at the header's bottom edge (above any children). */}
|
|
87
|
+
{counting ? (
|
|
88
|
+
<div
|
|
89
|
+
aria-hidden
|
|
90
|
+
className={cn(
|
|
91
|
+
'absolute inset-x-0 -bottom-px h-1 origin-left',
|
|
92
|
+
progressColorByVariant[variant],
|
|
93
|
+
)}
|
|
94
|
+
style={{
|
|
95
|
+
animation: `toast-progress ${liveDurationMs}ms linear forwards`,
|
|
96
|
+
animationDelay: `-${initialElapsed}ms`,
|
|
97
|
+
}}
|
|
98
|
+
/>
|
|
99
|
+
) : null}
|
|
73
100
|
</div>
|
|
74
101
|
|
|
102
|
+
{children}
|
|
103
|
+
|
|
75
104
|
{/* Live-only bare X overlay */}
|
|
76
105
|
<button
|
|
77
106
|
type="button"
|
|
@@ -85,20 +114,6 @@ export function NotificationTile({
|
|
|
85
114
|
>
|
|
86
115
|
<XmarkIcon size={16} />
|
|
87
116
|
</button>
|
|
88
|
-
|
|
89
|
-
{isLive ? (
|
|
90
|
-
<div
|
|
91
|
-
aria-hidden
|
|
92
|
-
className={cn(
|
|
93
|
-
'absolute inset-x-0 -bottom-px h-1 origin-left',
|
|
94
|
-
progressColorByVariant[variant],
|
|
95
|
-
)}
|
|
96
|
-
style={{
|
|
97
|
-
animation: `toast-progress ${liveDurationMs}ms linear forwards`,
|
|
98
|
-
animationDelay: `-${initialElapsed}ms`,
|
|
99
|
-
}}
|
|
100
|
-
/>
|
|
101
|
-
) : null}
|
|
102
117
|
</output>
|
|
103
118
|
)
|
|
104
119
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
-
import type { AddNotificationInput, Notification } from './types'
|
|
4
|
+
import type { AddNotificationInput, Notification, RenderNotificationTile } from './types'
|
|
5
5
|
|
|
6
6
|
interface NotificationsContextValue {
|
|
7
7
|
notifications: Notification[]
|
|
@@ -24,6 +24,7 @@ interface NotificationsContextValue {
|
|
|
24
24
|
hasMore: boolean
|
|
25
25
|
isLoadingMore: boolean
|
|
26
26
|
loadMore?: () => void
|
|
27
|
+
renderTile?: RenderNotificationTile
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const NotificationsContext = React.createContext<NotificationsContextValue | null>(null)
|
|
@@ -51,6 +52,8 @@ export interface NotificationsProviderProps {
|
|
|
51
52
|
hasMore?: boolean
|
|
52
53
|
isLoadingMore?: boolean
|
|
53
54
|
onLoadMore?: () => void
|
|
55
|
+
/** Override how individual tiles render (drawer + popups); falls back to `NotificationTile`. */
|
|
56
|
+
renderTile?: RenderNotificationTile
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
type Action =
|
|
@@ -140,6 +143,7 @@ export function NotificationsProvider({
|
|
|
140
143
|
hasMore = false,
|
|
141
144
|
isLoadingMore = false,
|
|
142
145
|
onLoadMore,
|
|
146
|
+
renderTile,
|
|
143
147
|
}: NotificationsProviderProps) {
|
|
144
148
|
const [notifications, dispatch] = React.useReducer(reducer, initialNotifications)
|
|
145
149
|
const [isOpen, setIsOpen] = React.useState(false)
|
|
@@ -244,6 +248,7 @@ export function NotificationsProvider({
|
|
|
244
248
|
hasMore,
|
|
245
249
|
isLoadingMore,
|
|
246
250
|
loadMore: onLoadMore,
|
|
251
|
+
renderTile,
|
|
247
252
|
}),
|
|
248
253
|
[
|
|
249
254
|
notifications,
|
|
@@ -266,6 +271,7 @@ export function NotificationsProvider({
|
|
|
266
271
|
hasMore,
|
|
267
272
|
isLoadingMore,
|
|
268
273
|
onLoadMore,
|
|
274
|
+
renderTile,
|
|
269
275
|
],
|
|
270
276
|
)
|
|
271
277
|
|
|
@@ -24,3 +24,53 @@ export type AddNotificationInput =
|
|
|
24
24
|
createdAt?: number
|
|
25
25
|
read?: boolean
|
|
26
26
|
}
|
|
27
|
+
|
|
28
|
+
/** Per-notification tile renderer; return a node to override the default `NotificationTile`, or `undefined` to fall back. */
|
|
29
|
+
export type RenderNotificationTile = (
|
|
30
|
+
notification: Notification,
|
|
31
|
+
helpers: { onComplete: (id: string) => void; onSettle: (id: string) => void; liveDurationMs?: number },
|
|
32
|
+
) => ReactNode
|
|
33
|
+
|
|
34
|
+
/** Discriminator carried in `notification.meta.contextType` for Mingo approval requests. */
|
|
35
|
+
export const ADMIN_APPROVAL_REQUEST_CONTEXT_TYPE = 'ADMIN_APPROVAL_REQUEST'
|
|
36
|
+
|
|
37
|
+
/** A single planned tool invocation awaiting approval. */
|
|
38
|
+
export interface ApprovalToolCallMeta {
|
|
39
|
+
toolExecutionRequestId?: string | null
|
|
40
|
+
toolName: string
|
|
41
|
+
toolTitle?: string | null
|
|
42
|
+
toolExplanation?: string | null
|
|
43
|
+
toolType?: string | null
|
|
44
|
+
requiresApproval?: boolean
|
|
45
|
+
approvalType?: string | null
|
|
46
|
+
toolCallArguments?: Record<string, unknown> | null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Approval payload stashed in `notification.meta` for an `ADMIN_APPROVAL_REQUEST` notification. */
|
|
50
|
+
export interface ApprovalNotificationMeta {
|
|
51
|
+
approvalRequestId: string
|
|
52
|
+
dialogId?: string | null
|
|
53
|
+
ticketId?: string | null
|
|
54
|
+
approvalType?: string | null
|
|
55
|
+
toolCalls: ApprovalToolCallMeta[]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function isApprovalNotification(notification: Notification): boolean {
|
|
59
|
+
return notification.meta?.contextType === ADMIN_APPROVAL_REQUEST_CONTEXT_TYPE
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Safely read the approval payload off a notification; returns null when absent or malformed. */
|
|
63
|
+
export function getApprovalMeta(notification: Notification): ApprovalNotificationMeta | null {
|
|
64
|
+
if (!isApprovalNotification(notification)) return null
|
|
65
|
+
const meta = notification.meta
|
|
66
|
+
const approvalRequestId = meta?.approvalRequestId
|
|
67
|
+
const toolCalls = meta?.toolCalls
|
|
68
|
+
if (typeof approvalRequestId !== 'string' || !Array.isArray(toolCalls)) return null
|
|
69
|
+
return {
|
|
70
|
+
approvalRequestId,
|
|
71
|
+
dialogId: (meta?.dialogId as string | undefined) ?? null,
|
|
72
|
+
ticketId: (meta?.ticketId as string | undefined) ?? null,
|
|
73
|
+
approvalType: (meta?.approvalType as string | undefined) ?? null,
|
|
74
|
+
toolCalls: toolCalls as ApprovalToolCallMeta[],
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -7,6 +7,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '../popover'
|
|
|
7
7
|
import { cn } from '../../utils/cn'
|
|
8
8
|
import { HEX_PATTERN, hexToRgb, hslToRgb, rgbToHex, rgbToHsl } from '../../utils/ods-color-utils'
|
|
9
9
|
import { ActionsMenuDropdown, type ActionsMenuGroup } from './actions-menu'
|
|
10
|
+
import { ColorSwatch } from './color-swatch'
|
|
10
11
|
import { Input } from './input'
|
|
11
12
|
import { InputTrigger } from './input-trigger'
|
|
12
13
|
|
|
@@ -35,16 +36,6 @@ export const TICKET_STATUS_COLOR_PRESETS: readonly ColorPreset[] = [
|
|
|
35
36
|
export const CUSTOM_PRESET_KEY = 'custom'
|
|
36
37
|
export const DEFAULT_CUSTOM_STATUS_COLOR = '#888888'
|
|
37
38
|
|
|
38
|
-
function ColorSwatch({ color, className }: { color: string; className?: string }) {
|
|
39
|
-
return (
|
|
40
|
-
<span
|
|
41
|
-
aria-hidden
|
|
42
|
-
className={cn('inline-block size-4 shrink-0 rounded-sm', className)}
|
|
43
|
-
style={{ backgroundColor: color }}
|
|
44
|
-
/>
|
|
45
|
-
)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
39
|
function TriggerChevron() {
|
|
49
40
|
return (
|
|
50
41
|
<Chevron02DownIcon
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { cn } from '../../utils/cn'
|
|
2
|
+
|
|
3
|
+
export interface ColorSwatchProps {
|
|
4
|
+
color: string
|
|
5
|
+
className?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Square color preview shared across color and status selectors. */
|
|
9
|
+
export function ColorSwatch({ color, className }: ColorSwatchProps) {
|
|
10
|
+
return (
|
|
11
|
+
<span
|
|
12
|
+
aria-hidden
|
|
13
|
+
className={cn('inline-block size-4 shrink-0 rounded-sm', className)}
|
|
14
|
+
style={{ backgroundColor: color }}
|
|
15
|
+
/>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import type { ReactNode } from 'react'
|
|
3
4
|
import { cn } from '../../../utils/cn'
|
|
4
5
|
import { useDataTableContext } from './data-table'
|
|
5
6
|
import { DataTableEmpty } from './data-table-empty'
|
|
@@ -39,6 +40,12 @@ export interface DataTableBodyProps<T = any> {
|
|
|
39
40
|
* Typically set to the same value as `skeletonRows`. Pass `0` to disable.
|
|
40
41
|
*/
|
|
41
42
|
minRows?: number
|
|
43
|
+
/**
|
|
44
|
+
* Render expandable content below a row, inside the same card. Return a node to
|
|
45
|
+
* attach it (the cells row gains a bottom divider), or null/undefined for none.
|
|
46
|
+
* Interactive content inside must carry `data-no-row-click`.
|
|
47
|
+
*/
|
|
48
|
+
renderSubRow?: (item: T) => ReactNode
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
/**
|
|
@@ -57,6 +64,7 @@ export function DataTableBody<T = any>({
|
|
|
57
64
|
onRowClick,
|
|
58
65
|
rowHref,
|
|
59
66
|
minRows,
|
|
67
|
+
renderSubRow,
|
|
60
68
|
}: DataTableBodyProps<T>) {
|
|
61
69
|
const table = useDataTableContext<T>()
|
|
62
70
|
const rows = table.getRowModel().rows
|
|
@@ -94,6 +102,7 @@ export function DataTableBody<T = any>({
|
|
|
94
102
|
href={href}
|
|
95
103
|
compact={compact}
|
|
96
104
|
className={cls}
|
|
105
|
+
subRow={renderSubRow?.(item)}
|
|
97
106
|
/>
|
|
98
107
|
)
|
|
99
108
|
})}
|
|
@@ -14,6 +14,8 @@ export interface DataTableRowProps<T> {
|
|
|
14
14
|
/** Dense row height. */
|
|
15
15
|
compact?: boolean
|
|
16
16
|
className?: string
|
|
17
|
+
/** Expandable content rendered below the cells, inside the same card. */
|
|
18
|
+
subRow?: ReactNode
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
/**
|
|
@@ -59,8 +61,13 @@ function DataTableRowImpl<T>({
|
|
|
59
61
|
href,
|
|
60
62
|
compact,
|
|
61
63
|
className,
|
|
64
|
+
subRow,
|
|
62
65
|
}: DataTableRowProps<T>) {
|
|
66
|
+
const hasSubRow = subRow != null && subRow !== false
|
|
67
|
+
// A sub-row carries its own interactive controls, so it must not live inside the
|
|
68
|
+
// row-level <Link>; when present, the link wraps only the cells.
|
|
63
69
|
const isLinkMode = Boolean(href) && !onClick
|
|
70
|
+
const isWholeCardLink = isLinkMode && !hasSubRow
|
|
64
71
|
const containerRef = useRef<HTMLElement | null>(null)
|
|
65
72
|
|
|
66
73
|
const handleClick = useCallback(
|
|
@@ -84,7 +91,8 @@ function DataTableRowImpl<T>({
|
|
|
84
91
|
|
|
85
92
|
const containerClassName = cn(
|
|
86
93
|
'block rounded-md bg-ods-card border border-ods-border overflow-hidden no-underline text-inherit',
|
|
87
|
-
|
|
94
|
+
// With a sub-row the link wraps only the cells, so keep the clickable affordance off the whole card.
|
|
95
|
+
(onClick || isWholeCardLink) && 'cursor-pointer hover:bg-ods-bg-active transition-colors',
|
|
88
96
|
className,
|
|
89
97
|
)
|
|
90
98
|
|
|
@@ -93,6 +101,7 @@ function DataTableRowImpl<T>({
|
|
|
93
101
|
className={cn(
|
|
94
102
|
'flex items-center gap-[var(--spacing-system-mf)] px-[var(--spacing-system-mf)]',
|
|
95
103
|
compact ? 'py-[var(--spacing-system-xsf)]' : `py-0 ${ROW_HEIGHT_DESKTOP}`,
|
|
104
|
+
hasSubRow && 'border-b border-ods-border',
|
|
96
105
|
)}
|
|
97
106
|
>
|
|
98
107
|
{row.getVisibleCells().map(cell => {
|
|
@@ -117,7 +126,7 @@ function DataTableRowImpl<T>({
|
|
|
117
126
|
</div>
|
|
118
127
|
)
|
|
119
128
|
|
|
120
|
-
if (
|
|
129
|
+
if (isWholeCardLink && href) {
|
|
121
130
|
return (
|
|
122
131
|
<Link
|
|
123
132
|
href={href}
|
|
@@ -137,7 +146,19 @@ function DataTableRowImpl<T>({
|
|
|
137
146
|
className={containerClassName}
|
|
138
147
|
onClick={onClick ? handleClick : undefined}
|
|
139
148
|
>
|
|
140
|
-
{
|
|
149
|
+
{isLinkMode && href ? (
|
|
150
|
+
<Link
|
|
151
|
+
href={href}
|
|
152
|
+
prefetch={false}
|
|
153
|
+
className="block no-underline text-inherit cursor-pointer hover:bg-ods-bg-active transition-colors"
|
|
154
|
+
onClick={handleClick}
|
|
155
|
+
>
|
|
156
|
+
{cells}
|
|
157
|
+
</Link>
|
|
158
|
+
) : (
|
|
159
|
+
cells
|
|
160
|
+
)}
|
|
161
|
+
{hasSubRow && subRow}
|
|
141
162
|
</div>
|
|
142
163
|
)
|
|
143
164
|
}
|
|
@@ -149,6 +149,7 @@ export * from './image-uploader'
|
|
|
149
149
|
// Ticket components
|
|
150
150
|
export * from './ticket-status-tag'
|
|
151
151
|
export * from './ticket-status-config-row'
|
|
152
|
+
export * from './color-swatch'
|
|
152
153
|
export * from './color-preset-select'
|
|
153
154
|
export * from './assignee-dropdown'
|
|
154
155
|
export * from './ticket-info-section'
|