@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.
Files changed (91) hide show
  1. package/dist/{chunk-2QURHTDV.js → chunk-CUBRHI6D.js} +2 -2
  2. package/dist/{chunk-NRJ53MIF.cjs → chunk-E3DB4D43.cjs} +9 -9
  3. package/dist/{chunk-NRJ53MIF.cjs.map → chunk-E3DB4D43.cjs.map} +1 -1
  4. package/dist/{chunk-3YTIV6C5.cjs → chunk-E3WJQQGW.cjs} +7 -7
  5. package/dist/{chunk-3YTIV6C5.cjs.map → chunk-E3WJQQGW.cjs.map} +1 -1
  6. package/dist/{chunk-SAJG77KY.js → chunk-EURLSRHW.js} +2 -2
  7. package/dist/{chunk-IWBDAGZQ.cjs → chunk-HO2VEMEX.cjs} +24 -24
  8. package/dist/{chunk-IWBDAGZQ.cjs.map → chunk-HO2VEMEX.cjs.map} +1 -1
  9. package/dist/{chunk-NHRVPMBU.js → chunk-K252FYE5.js} +3959 -3740
  10. package/dist/chunk-K252FYE5.js.map +1 -0
  11. package/dist/{chunk-5RELSRTS.cjs → chunk-LEM4FLCU.cjs} +10 -10
  12. package/dist/{chunk-5RELSRTS.cjs.map → chunk-LEM4FLCU.cjs.map} +1 -1
  13. package/dist/{chunk-HOQDRQ4M.cjs → chunk-QFC725WU.cjs} +746 -527
  14. package/dist/chunk-QFC725WU.cjs.map +1 -0
  15. package/dist/{chunk-RIEEPMJ6.js → chunk-X3KKG6IU.js} +2 -2
  16. package/dist/{chunk-NUOYDHUM.js → chunk-YE62NF6I.js} +2 -2
  17. package/dist/components/chat/approval-batch-message.d.ts +6 -0
  18. package/dist/components/chat/approval-batch-message.d.ts.map +1 -1
  19. package/dist/components/chat/index.cjs +2 -2
  20. package/dist/components/chat/index.js +1 -1
  21. package/dist/components/contact/index.cjs +3 -3
  22. package/dist/components/contact/index.js +2 -2
  23. package/dist/components/features/index.cjs +10 -2
  24. package/dist/components/features/index.cjs.map +1 -1
  25. package/dist/components/features/index.js +9 -1
  26. package/dist/components/features/notifications/approval-request-notification-tile.d.ts +13 -0
  27. package/dist/components/features/notifications/approval-request-notification-tile.d.ts.map +1 -0
  28. package/dist/components/features/notifications/index.d.ts +4 -1
  29. package/dist/components/features/notifications/index.d.ts.map +1 -1
  30. package/dist/components/features/notifications/notification-drawer.d.ts +3 -1
  31. package/dist/components/features/notifications/notification-drawer.d.ts.map +1 -1
  32. package/dist/components/features/notifications/notification-popups.d.ts +3 -1
  33. package/dist/components/features/notifications/notification-popups.d.ts.map +1 -1
  34. package/dist/components/features/notifications/notification-tile.d.ts +6 -1
  35. package/dist/components/features/notifications/notification-tile.d.ts.map +1 -1
  36. package/dist/components/features/notifications/notifications-context.d.ts +5 -2
  37. package/dist/components/features/notifications/notifications-context.d.ts.map +1 -1
  38. package/dist/components/features/notifications/types.d.ts +30 -0
  39. package/dist/components/features/notifications/types.d.ts.map +1 -1
  40. package/dist/components/index.cjs +87 -77
  41. package/dist/components/index.cjs.map +1 -1
  42. package/dist/components/index.js +14 -4
  43. package/dist/components/index.js.map +1 -1
  44. package/dist/components/navigation/index.cjs +2 -2
  45. package/dist/components/navigation/index.js +1 -1
  46. package/dist/components/onboarding-guides/index.cjs +25 -25
  47. package/dist/components/onboarding-guides/index.js +3 -3
  48. package/dist/components/tickets/index.cjs +64 -64
  49. package/dist/components/tickets/index.js +4 -4
  50. package/dist/components/ui/color-preset-select.d.ts.map +1 -1
  51. package/dist/components/ui/color-swatch.d.ts +7 -0
  52. package/dist/components/ui/color-swatch.d.ts.map +1 -0
  53. package/dist/components/ui/data-table/data-table-body.d.ts +8 -1
  54. package/dist/components/ui/data-table/data-table-body.d.ts.map +1 -1
  55. package/dist/components/ui/data-table/data-table-row.d.ts +4 -2
  56. package/dist/components/ui/data-table/data-table-row.d.ts.map +1 -1
  57. package/dist/components/ui/data-table/index.d.ts +1 -1
  58. package/dist/components/ui/index.cjs +4 -2
  59. package/dist/components/ui/index.cjs.map +1 -1
  60. package/dist/components/ui/index.d.ts +1 -0
  61. package/dist/components/ui/index.d.ts.map +1 -1
  62. package/dist/components/ui/index.js +3 -1
  63. package/dist/components/ui/ticket-status-config-row.d.ts.map +1 -1
  64. package/dist/components/ui/ticket-status-tag.d.ts.map +1 -1
  65. package/dist/index.cjs +12 -2
  66. package/dist/index.cjs.map +1 -1
  67. package/dist/index.js +11 -1
  68. package/package.json +1 -1
  69. package/src/components/chat/approval-batch-message.tsx +26 -15
  70. package/src/components/features/notifications/approval-request-notification-tile.tsx +118 -0
  71. package/src/components/features/notifications/index.ts +10 -0
  72. package/src/components/features/notifications/notification-drawer.tsx +28 -11
  73. package/src/components/features/notifications/notification-popups.tsx +33 -19
  74. package/src/components/features/notifications/notification-tile.tsx +34 -19
  75. package/src/components/features/notifications/notifications-context.tsx +7 -1
  76. package/src/components/features/notifications/types.ts +50 -0
  77. package/src/components/ui/color-preset-select.tsx +1 -10
  78. package/src/components/ui/color-swatch.tsx +17 -0
  79. package/src/components/ui/data-table/data-table-body.tsx +9 -0
  80. package/src/components/ui/data-table/data-table-row.tsx +24 -3
  81. package/src/components/ui/index.ts +1 -0
  82. package/src/components/ui/modal-v2.tsx +2 -2
  83. package/src/components/ui/ticket-status-config-row.tsx +78 -39
  84. package/src/components/ui/ticket-status-tag.tsx +2 -7
  85. package/src/stories/NotificationDrawer.stories.tsx +148 -1
  86. package/dist/chunk-HOQDRQ4M.cjs.map +0 -1
  87. package/dist/chunk-NHRVPMBU.js.map +0 -1
  88. /package/dist/{chunk-2QURHTDV.js.map → chunk-CUBRHI6D.js.map} +0 -0
  89. /package/dist/{chunk-SAJG77KY.js.map → chunk-EURLSRHW.js.map} +0 -0
  90. /package/dist/{chunk-RIEEPMJ6.js.map → chunk-X3KKG6IU.js.map} +0 -0
  91. /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({ className, liveDurationMs, loadMoreRootMargin = '200px' }: NotificationDrawerProps) {
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
- <NotificationTile
143
- key={n.id}
144
- notification={n}
145
- liveDurationMs={liveDurationMs}
146
- onComplete={onComplete}
147
- onSettle={onSettle}
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
- // biome-ignore lint/a11y/useSemanticElements: nested interactive elements forbid <button>
87
- <div
88
- key={n.id}
89
- role="button"
90
- tabIndex={0}
91
- onClick={handleBodyClick(n)}
92
- onKeyDown={handleBodyKeyDown(n)}
93
- className="pointer-events-auto cursor-pointer rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent"
94
- >
95
- <NotificationTile
96
- notification={n}
97
- liveDurationMs={liveDurationMs}
98
- onComplete={markRead}
99
- onSettle={markSettled}
100
- />
101
- </div>
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 (!isLive) return
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, isLive, initialElapsed, liveDurationMs, onSettle])
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 className="flex items-center gap-[var(--spacing-system-xs)] p-[var(--spacing-system-s)]">
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 text-ods-text-secondary hover:text-ods-text-primary',
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
- (onClick || isLinkMode) && 'cursor-pointer hover:bg-ods-bg-active transition-colors',
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 (isLinkMode && href) {
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
- {cells}
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'