@flamingo-stack/openframe-frontend-core 0.0.268 → 0.0.269
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-J6O57C7S.js → chunk-ALCUDWKJ.js} +3356 -3173
- package/dist/chunk-ALCUDWKJ.js.map +1 -0
- package/dist/{chunk-Z6MKG53Z.js → chunk-DATYABIL.js} +2 -2
- package/dist/{chunk-YV55JZAD.cjs → chunk-FEIRH7ER.cjs} +25 -25
- package/dist/{chunk-YV55JZAD.cjs.map → chunk-FEIRH7ER.cjs.map} +1 -1
- package/dist/{chunk-XSK72QRR.js → chunk-HQGGXT2T.js} +2 -2
- package/dist/{chunk-CI5XE4LB.cjs → chunk-NM7J44WP.cjs} +30 -30
- package/dist/{chunk-CI5XE4LB.cjs.map → chunk-NM7J44WP.cjs.map} +1 -1
- package/dist/{chunk-O6UA475M.cjs → chunk-QDA2XVFP.cjs} +1086 -903
- package/dist/chunk-QDA2XVFP.cjs.map +1 -0
- package/dist/{chunk-GJVSLS2R.js → chunk-TOQH27ZP.js} +2 -2
- package/dist/{chunk-TPZI7NBN.cjs → chunk-TZEHP5IW.cjs} +11 -11
- package/dist/{chunk-TPZI7NBN.cjs.map → chunk-TZEHP5IW.cjs.map} +1 -1
- package/dist/components/chat/approval-batch-message.d.ts +8 -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/chat/types/chat.types.d.ts +1 -0
- package/dist/components/chat/types/chat.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/features/index.cjs +6 -2
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.js +5 -1
- package/dist/components/features/notifications/approval-request-notification-tile.d.ts.map +1 -1
- package/dist/components/features/notifications/index.d.ts +1 -1
- package/dist/components/features/notifications/index.d.ts.map +1 -1
- package/dist/components/features/notifications/types.d.ts +9 -0
- package/dist/components/features/notifications/types.d.ts.map +1 -1
- package/dist/components/index.cjs +65 -47
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +21 -3
- 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/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/tickets/index.cjs +53 -53
- package/dist/components/tickets/index.js +3 -3
- package/dist/components/ui/index.cjs +16 -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 +15 -1
- package/dist/components/ui/no-data/index.d.ts +5 -0
- package/dist/components/ui/no-data/index.d.ts.map +1 -0
- package/dist/components/ui/no-data/no-data-actions.d.ts +34 -0
- package/dist/components/ui/no-data/no-data-actions.d.ts.map +1 -0
- package/dist/components/ui/no-data/no-data-message.d.ts +17 -0
- package/dist/components/ui/no-data/no-data-message.d.ts.map +1 -0
- package/dist/components/ui/no-data/no-data-styles.d.ts +8 -0
- package/dist/components/ui/no-data/no-data-styles.d.ts.map +1 -0
- package/dist/components/ui/no-data/no-data.d.ts +33 -0
- package/dist/components/ui/no-data/no-data.d.ts.map +1 -0
- package/dist/index.cjs +20 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +19 -1
- package/package.json +1 -1
- package/src/components/chat/approval-batch-message.tsx +32 -19
- package/src/components/chat/types/chat.types.ts +1 -0
- package/src/components/features/notifications/approval-request-notification-tile.tsx +23 -26
- package/src/components/features/notifications/index.ts +2 -0
- package/src/components/features/notifications/types.ts +39 -0
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/no-data/index.ts +4 -0
- package/src/components/ui/no-data/no-data-actions.tsx +102 -0
- package/src/components/ui/no-data/no-data-message.tsx +43 -0
- package/src/components/ui/no-data/no-data-styles.ts +36 -0
- package/src/components/ui/no-data/no-data.tsx +84 -0
- package/src/stories/EmptyState.stories.tsx +207 -0
- package/dist/chunk-J6O57C7S.js.map +0 -1
- package/dist/chunk-O6UA475M.cjs.map +0 -1
- /package/dist/{chunk-Z6MKG53Z.js.map → chunk-DATYABIL.js.map} +0 -0
- /package/dist/{chunk-XSK72QRR.js.map → chunk-HQGGXT2T.js.map} +0 -0
- /package/dist/{chunk-GJVSLS2R.js.map → chunk-TOQH27ZP.js.map} +0 -0
|
@@ -5,9 +5,9 @@ import { cn } from '../../../utils/cn'
|
|
|
5
5
|
import { ApprovalBatchMessage } from '../../chat/approval-batch-message'
|
|
6
6
|
import { ExpandChevron } from '../../chat/expand-chevron'
|
|
7
7
|
import { useCollapsible } from '../../chat/hooks/use-collapsible'
|
|
8
|
-
import type {
|
|
8
|
+
import type { ChatApprovalStatus } from '../../chat/types'
|
|
9
9
|
import { NotificationTile } from './notification-tile'
|
|
10
|
-
import { getApprovalMeta } from './types'
|
|
10
|
+
import { approvalMetaToBatchData, getApprovalMeta, resolutionToStatus } from './types'
|
|
11
11
|
import type { Notification } from './types'
|
|
12
12
|
|
|
13
13
|
export interface ApprovalRequestNotificationTileProps {
|
|
@@ -35,27 +35,14 @@ export function ApprovalRequestNotificationTile({
|
|
|
35
35
|
// Toggling the command section pins the tile so a live pop-up doesn't
|
|
36
36
|
// auto-dismiss out from under the user mid-read.
|
|
37
37
|
const [pinned, setPinned] = useState(defaultExpanded)
|
|
38
|
-
|
|
38
|
+
// Optimistic status for this user's own click; backend resolution (below) wins once it arrives.
|
|
39
|
+
const [localStatus, setLocalStatus] = useState<ChatApprovalStatus | null>(null)
|
|
39
40
|
|
|
40
41
|
const approval = getApprovalMeta(notification)
|
|
42
|
+
const resolvedStatus = resolutionToStatus(approval?.resolution)
|
|
43
|
+
const status: ChatApprovalStatus = resolvedStatus !== 'pending' ? resolvedStatus : (localStatus ?? 'pending')
|
|
41
44
|
|
|
42
|
-
const batchData = useMemo
|
|
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])
|
|
45
|
+
const batchData = useMemo(() => (approval ? approvalMetaToBatchData(approval) : null), [approval])
|
|
59
46
|
|
|
60
47
|
const { innerRef, containerStyle } = useCollapsible({ expanded })
|
|
61
48
|
|
|
@@ -65,14 +52,22 @@ export function ApprovalRequestNotificationTile({
|
|
|
65
52
|
const toggleLabel = `${expanded ? 'Hide' : 'Show'} ${commandWord}`
|
|
66
53
|
|
|
67
54
|
const handleApprove = async () => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
55
|
+
setLocalStatus('approved')
|
|
56
|
+
try {
|
|
57
|
+
await onApprove(batchData.approvalRequestId)
|
|
58
|
+
onComplete(notification.id)
|
|
59
|
+
} catch {
|
|
60
|
+
setLocalStatus(null) // roll back the optimistic flip; the request failed
|
|
61
|
+
}
|
|
71
62
|
}
|
|
72
63
|
const handleReject = async () => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
64
|
+
setLocalStatus('rejected')
|
|
65
|
+
try {
|
|
66
|
+
await onReject(batchData.approvalRequestId)
|
|
67
|
+
onComplete(notification.id)
|
|
68
|
+
} catch {
|
|
69
|
+
setLocalStatus(null)
|
|
70
|
+
}
|
|
76
71
|
}
|
|
77
72
|
|
|
78
73
|
return (
|
|
@@ -106,6 +101,8 @@ export function ApprovalRequestNotificationTile({
|
|
|
106
101
|
<ApprovalBatchMessage
|
|
107
102
|
data={batchData}
|
|
108
103
|
status={status}
|
|
104
|
+
resolvedByName={approval.resolvedByName}
|
|
105
|
+
showExecutionStatus={false}
|
|
109
106
|
onApprove={handleApprove}
|
|
110
107
|
onReject={handleReject}
|
|
111
108
|
maxBodyHeight="50vh"
|
|
@@ -14,8 +14,10 @@ export { ApprovalRequestNotificationTile } from './approval-request-notification
|
|
|
14
14
|
export type { ApprovalRequestNotificationTileProps } from './approval-request-notification-tile'
|
|
15
15
|
export {
|
|
16
16
|
ADMIN_APPROVAL_REQUEST_CONTEXT_TYPE,
|
|
17
|
+
approvalMetaToBatchData,
|
|
17
18
|
getApprovalMeta,
|
|
18
19
|
isApprovalNotification,
|
|
20
|
+
resolutionToStatus,
|
|
19
21
|
} from './types'
|
|
20
22
|
export type {
|
|
21
23
|
Notification,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from 'react'
|
|
2
|
+
import { APPROVAL_STATUS, type ApprovalBatchData, type ChatApprovalStatus } from '../../chat/types'
|
|
2
3
|
|
|
3
4
|
export type NotificationVariant = 'default' | 'success' | 'warning' | 'error' | 'info'
|
|
4
5
|
|
|
@@ -52,6 +53,10 @@ export interface ApprovalNotificationMeta {
|
|
|
52
53
|
dialogId?: string | null
|
|
53
54
|
ticketId?: string | null
|
|
54
55
|
approvalType?: string | null
|
|
56
|
+
/** Backend `ApprovalResolution`: PENDING | APPROVED | REJECTED | CANCELLED. Null until resolved. */
|
|
57
|
+
resolution?: string | null
|
|
58
|
+
/** Display name of the user who resolved the request; null until resolved (or for system actions). */
|
|
59
|
+
resolvedByName?: string | null
|
|
55
60
|
toolCalls: ApprovalToolCallMeta[]
|
|
56
61
|
}
|
|
57
62
|
|
|
@@ -71,6 +76,40 @@ export function getApprovalMeta(notification: Notification): ApprovalNotificatio
|
|
|
71
76
|
dialogId: (meta?.dialogId as string | undefined) ?? null,
|
|
72
77
|
ticketId: (meta?.ticketId as string | undefined) ?? null,
|
|
73
78
|
approvalType: (meta?.approvalType as string | undefined) ?? null,
|
|
79
|
+
resolution: (meta?.resolution as string | undefined) ?? null,
|
|
80
|
+
resolvedByName: (meta?.resolvedByName as string | undefined) ?? null,
|
|
74
81
|
toolCalls: toolCalls as ApprovalToolCallMeta[],
|
|
75
82
|
}
|
|
76
83
|
}
|
|
84
|
+
|
|
85
|
+
/** Map the backend `ApprovalResolution` carried on a notification to a tile/batch status. */
|
|
86
|
+
export function resolutionToStatus(resolution: unknown): ChatApprovalStatus {
|
|
87
|
+
switch (typeof resolution === 'string' ? resolution.toUpperCase() : undefined) {
|
|
88
|
+
case 'APPROVED':
|
|
89
|
+
return APPROVAL_STATUS.APPROVED
|
|
90
|
+
case 'REJECTED':
|
|
91
|
+
return APPROVAL_STATUS.REJECTED
|
|
92
|
+
case 'CANCELLED':
|
|
93
|
+
return APPROVAL_STATUS.CANCELLED
|
|
94
|
+
default:
|
|
95
|
+
return APPROVAL_STATUS.PENDING
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Build the `ApprovalBatchMessage` data payload from an approval notification's meta. */
|
|
100
|
+
export function approvalMetaToBatchData(approval: ApprovalNotificationMeta): ApprovalBatchData {
|
|
101
|
+
return {
|
|
102
|
+
approvalRequestId: approval.approvalRequestId,
|
|
103
|
+
approvalType: approval.approvalType ?? '',
|
|
104
|
+
toolCalls: approval.toolCalls.map((tc, index) => ({
|
|
105
|
+
toolExecutionRequestId: tc.toolExecutionRequestId ?? `${approval.approvalRequestId}:${index}`,
|
|
106
|
+
toolName: tc.toolName,
|
|
107
|
+
toolTitle: tc.toolTitle ?? undefined,
|
|
108
|
+
toolExplanation: tc.toolExplanation ?? undefined,
|
|
109
|
+
toolType: tc.toolType ?? undefined,
|
|
110
|
+
requiresApproval: tc.requiresApproval ?? true,
|
|
111
|
+
approvalType: tc.approvalType ?? null,
|
|
112
|
+
toolCallArguments: tc.toolCallArguments ?? null,
|
|
113
|
+
})),
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -18,6 +18,7 @@ export * from './input-trigger'
|
|
|
18
18
|
export * from './interactive-card'
|
|
19
19
|
export * from './label'
|
|
20
20
|
export * from './media-type-selector'
|
|
21
|
+
export * from './no-data'
|
|
21
22
|
export * from './page-loader'
|
|
22
23
|
export * from './progress-bar'
|
|
23
24
|
export * from './radio-group'
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../../utils/cn"
|
|
4
|
+
import {
|
|
5
|
+
noDataActionInteractiveClasses,
|
|
6
|
+
noDataActionsVariants,
|
|
7
|
+
noDataIconClasses,
|
|
8
|
+
type NoDataActionsVariant,
|
|
9
|
+
} from "./no-data-styles"
|
|
10
|
+
|
|
11
|
+
interface NoDataActionProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "title"> {
|
|
12
|
+
/** Leading glyph, sized to 16px (mobile) / 24px (md+). Optional. */
|
|
13
|
+
icon?: React.ReactNode
|
|
14
|
+
/** Short label describing the block. */
|
|
15
|
+
label?: React.ReactNode
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A single clickable block: a leading icon with a short label, laid out
|
|
20
|
+
* icon-beside-label on mobile and icon-above-label on larger screens. Tints on
|
|
21
|
+
* hover, shows a focus ring for keyboard users, and grays out when disabled.
|
|
22
|
+
* The outer border, rounded corners and dividers come from the surrounding
|
|
23
|
+
* group, so on its own it has no frame.
|
|
24
|
+
*/
|
|
25
|
+
const NoDataAction = React.forwardRef<HTMLButtonElement, NoDataActionProps>(
|
|
26
|
+
function NoDataAction({ icon, label, children, className, disabled, type, ...props }, ref) {
|
|
27
|
+
return (
|
|
28
|
+
<button
|
|
29
|
+
ref={ref}
|
|
30
|
+
type={type ?? "button"}
|
|
31
|
+
disabled={disabled}
|
|
32
|
+
className={cn(
|
|
33
|
+
"flex flex-1 items-center gap-[var(--spacing-system-s)] p-[var(--spacing-system-m)] text-left text-ods-text-secondary",
|
|
34
|
+
"md:flex-col md:items-start md:gap-[var(--spacing-system-xs)]",
|
|
35
|
+
noDataActionInteractiveClasses,
|
|
36
|
+
className,
|
|
37
|
+
)}
|
|
38
|
+
{...props}
|
|
39
|
+
>
|
|
40
|
+
{icon && <span className={noDataIconClasses}>{icon}</span>}
|
|
41
|
+
<span className="min-w-0 flex-1 break-words text-h6 md:w-full md:flex-none">
|
|
42
|
+
{label ?? children}
|
|
43
|
+
</span>
|
|
44
|
+
</button>
|
|
45
|
+
)
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
interface NoDataActionsProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
50
|
+
/**
|
|
51
|
+
* The blocks to render. Count is dynamic — any number works. When omitted,
|
|
52
|
+
* `children` are rendered instead.
|
|
53
|
+
*/
|
|
54
|
+
actions?: NoDataActionProps[]
|
|
55
|
+
/** Surface treatment of the group panel. */
|
|
56
|
+
variant?: NoDataActionsVariant
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A group of clickable blocks sharing one bordered, card-colored panel. Renders
|
|
61
|
+
* as a horizontal row with vertical dividers on larger screens and a vertical
|
|
62
|
+
* stack with horizontal dividers on mobile, switching automatically. Only the
|
|
63
|
+
* outermost blocks have rounded corners.
|
|
64
|
+
*/
|
|
65
|
+
const NoDataActions = React.forwardRef<HTMLDivElement, NoDataActionsProps>(
|
|
66
|
+
function NoDataActions({ actions, variant = "outline", className, children, ...props }, ref) {
|
|
67
|
+
const blocks = actions
|
|
68
|
+
? actions.map((action, index) => <NoDataAction key={index} {...action} />)
|
|
69
|
+
: React.Children.toArray(children)
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div
|
|
73
|
+
ref={ref}
|
|
74
|
+
className={cn(
|
|
75
|
+
noDataActionsVariants({ variant }),
|
|
76
|
+
"flex w-full flex-col md:w-auto md:flex-row",
|
|
77
|
+
className,
|
|
78
|
+
)}
|
|
79
|
+
{...props}
|
|
80
|
+
>
|
|
81
|
+
{blocks.map((block, index) => (
|
|
82
|
+
<React.Fragment key={index}>
|
|
83
|
+
{index > 0 && (
|
|
84
|
+
<div
|
|
85
|
+
aria-hidden="true"
|
|
86
|
+
className="shrink-0 self-stretch border-t border-ods-border md:border-l md:border-t-0"
|
|
87
|
+
/>
|
|
88
|
+
)}
|
|
89
|
+
{block}
|
|
90
|
+
</React.Fragment>
|
|
91
|
+
))}
|
|
92
|
+
</div>
|
|
93
|
+
)
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
export {
|
|
98
|
+
NoDataAction,
|
|
99
|
+
NoDataActions,
|
|
100
|
+
type NoDataActionProps,
|
|
101
|
+
type NoDataActionsProps,
|
|
102
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../../utils/cn"
|
|
4
|
+
import { noDataIconClasses } from "./no-data-styles"
|
|
5
|
+
|
|
6
|
+
interface NoDataMessageProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
|
|
7
|
+
/** Leading glyph, sized to 16px (mobile) / 24px (md+). Optional. */
|
|
8
|
+
icon?: React.ReactNode
|
|
9
|
+
/** Primary line (body size). */
|
|
10
|
+
title?: React.ReactNode
|
|
11
|
+
/** Secondary line (caption size). */
|
|
12
|
+
description?: React.ReactNode
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A transparent, centered stack of an icon, a title and a description, in muted
|
|
17
|
+
* gray. Any piece is optional, so it also covers "icon + title", "title only",
|
|
18
|
+
* and similar reduced forms.
|
|
19
|
+
*/
|
|
20
|
+
const NoDataMessage = React.forwardRef<HTMLDivElement, NoDataMessageProps>(
|
|
21
|
+
function NoDataMessage({ icon, title, description, className, ...props }, ref) {
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
ref={ref}
|
|
25
|
+
className={cn(
|
|
26
|
+
"flex flex-col items-center gap-[var(--spacing-system-l)] text-center text-ods-text-secondary",
|
|
27
|
+
className,
|
|
28
|
+
)}
|
|
29
|
+
{...props}
|
|
30
|
+
>
|
|
31
|
+
{icon && <span className={noDataIconClasses}>{icon}</span>}
|
|
32
|
+
{(title || description) && (
|
|
33
|
+
<div className="flex flex-col">
|
|
34
|
+
{title && <p className="text-h4 break-words">{title}</p>}
|
|
35
|
+
{description && <p className="text-h6 break-words">{description}</p>}
|
|
36
|
+
</div>
|
|
37
|
+
)}
|
|
38
|
+
</div>
|
|
39
|
+
)
|
|
40
|
+
},
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
export { NoDataMessage, type NoDataMessageProps }
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Shared style atoms for the NoData (empty-state) family. Mirrors the approach
|
|
2
|
+
// used by `button-styles.ts`: a single source of truth for the icon scale, the
|
|
3
|
+
// block-group surface and the interactive/disabled treatment, expressed as
|
|
4
|
+
// reusable class atoms so new looks can be added without touching the markup.
|
|
5
|
+
|
|
6
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
7
|
+
|
|
8
|
+
// Leading-icon glyph scale: 16px on mobile, 24px on md+. Color is intentionally
|
|
9
|
+
// omitted so the glyph inherits the current text color (and dims with it when
|
|
10
|
+
// the block is disabled).
|
|
11
|
+
export const noDataIconClasses =
|
|
12
|
+
"inline-flex shrink-0 items-center justify-center [&_svg]:h-4 [&_svg]:w-4 md:[&_svg]:h-6 md:[&_svg]:w-6"
|
|
13
|
+
|
|
14
|
+
// Surface for the action-block group container: a card-colored panel with a
|
|
15
|
+
// border and rounded corners. `overflow-hidden` keeps only the outer blocks
|
|
16
|
+
// round — inner blocks stay square.
|
|
17
|
+
export const noDataActionsVariants = cva("overflow-hidden rounded-md", {
|
|
18
|
+
variants: {
|
|
19
|
+
variant: {
|
|
20
|
+
outline: "bg-ods-card border border-ods-border",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
defaultVariants: { variant: "outline" },
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export type NoDataActionsVariant = NonNullable<
|
|
27
|
+
VariantProps<typeof noDataActionsVariants>["variant"]
|
|
28
|
+
>
|
|
29
|
+
|
|
30
|
+
// Interactive treatment for a clickable block: hover/active surface tints, a
|
|
31
|
+
// keyboard focus ring, and a disabled state that grays the text and stops
|
|
32
|
+
// pointer interaction — matching the disabled outline button.
|
|
33
|
+
export const noDataActionInteractiveClasses =
|
|
34
|
+
"cursor-pointer transition-colors hover:bg-ods-bg-hover active:bg-ods-bg-active " +
|
|
35
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-focus focus-visible:ring-inset " +
|
|
36
|
+
"disabled:cursor-default disabled:pointer-events-none disabled:text-ods-text-disabled"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../../utils/cn"
|
|
4
|
+
import { Button, type ButtonProps } from "../button"
|
|
5
|
+
import { NoDataMessage } from "./no-data-message"
|
|
6
|
+
import { NoDataActions, type NoDataActionProps } from "./no-data-actions"
|
|
7
|
+
|
|
8
|
+
interface NoDataProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
|
|
9
|
+
// --- Message ---
|
|
10
|
+
/** Leading glyph, sized to 16px (mobile) / 24px (md+). */
|
|
11
|
+
icon?: React.ReactNode
|
|
12
|
+
/** Primary line. */
|
|
13
|
+
title?: React.ReactNode
|
|
14
|
+
/** Secondary line. */
|
|
15
|
+
description?: React.ReactNode
|
|
16
|
+
|
|
17
|
+
// --- Clickable blocks ---
|
|
18
|
+
/** Clickable blocks. Dynamic count. Hidden when empty/omitted. */
|
|
19
|
+
actions?: NoDataActionProps[]
|
|
20
|
+
|
|
21
|
+
// --- Footer button ---
|
|
22
|
+
/** Footer button label. When set (and `button` is not), renders the button. */
|
|
23
|
+
buttonLabel?: React.ReactNode
|
|
24
|
+
/** Leading icon for the footer button. */
|
|
25
|
+
buttonIcon?: React.ReactNode
|
|
26
|
+
/** Click handler for the footer button. */
|
|
27
|
+
onButtonClick?: ButtonProps["onClick"]
|
|
28
|
+
/** Extra props forwarded to the footer button (variant, href, loading, …). */
|
|
29
|
+
buttonProps?: Partial<ButtonProps>
|
|
30
|
+
/** Fully custom footer node; overrides the `buttonLabel`/`buttonIcon` path. */
|
|
31
|
+
button?: React.ReactNode
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A centered empty-state panel: a muted icon/title/description message, an
|
|
36
|
+
* optional group of clickable blocks, and an optional footer button, stacked
|
|
37
|
+
* vertically with breakpoint-aware spacing. Each region renders only when its
|
|
38
|
+
* inputs are present, so the same component covers a bare message, a message
|
|
39
|
+
* with a button, the full layout, and everything in between.
|
|
40
|
+
*/
|
|
41
|
+
const NoData = React.forwardRef<HTMLDivElement, NoDataProps>(function NoData(
|
|
42
|
+
{
|
|
43
|
+
icon,
|
|
44
|
+
title,
|
|
45
|
+
description,
|
|
46
|
+
actions,
|
|
47
|
+
buttonLabel,
|
|
48
|
+
buttonIcon,
|
|
49
|
+
onButtonClick,
|
|
50
|
+
buttonProps,
|
|
51
|
+
button,
|
|
52
|
+
className,
|
|
53
|
+
...props
|
|
54
|
+
},
|
|
55
|
+
ref,
|
|
56
|
+
) {
|
|
57
|
+
const hasMessage = !!(icon || title || description)
|
|
58
|
+
const hasActions = !!actions?.length
|
|
59
|
+
|
|
60
|
+
const footerButton =
|
|
61
|
+
button ??
|
|
62
|
+
(buttonLabel != null ? (
|
|
63
|
+
<Button variant="outline" leftIcon={buttonIcon} onClick={onButtonClick} {...buttonProps}>
|
|
64
|
+
{buttonLabel}
|
|
65
|
+
</Button>
|
|
66
|
+
) : null)
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div
|
|
70
|
+
ref={ref}
|
|
71
|
+
className={cn(
|
|
72
|
+
"flex w-full flex-col items-center justify-center gap-[var(--spacing-system-l)] p-[var(--spacing-system-l)]",
|
|
73
|
+
className,
|
|
74
|
+
)}
|
|
75
|
+
{...props}
|
|
76
|
+
>
|
|
77
|
+
{hasMessage && <NoDataMessage icon={icon} title={title} description={description} />}
|
|
78
|
+
{hasActions && <NoDataActions actions={actions} />}
|
|
79
|
+
{footerButton}
|
|
80
|
+
</div>
|
|
81
|
+
)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
export { NoData, type NoDataProps }
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
|
|
2
|
+
import { Database, FileText, Inbox, Plus, Search, Settings, Sparkles, Upload, Zap } from 'lucide-react';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Button } from '../components/ui/button';
|
|
5
|
+
import { NoData, NoDataAction, NoDataActions, NoDataMessage } from '../components/ui/no-data';
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
title: 'UI/EmptyState',
|
|
9
|
+
component: NoData,
|
|
10
|
+
argTypes: {
|
|
11
|
+
icon: { control: false },
|
|
12
|
+
actions: { control: false },
|
|
13
|
+
button: { control: false },
|
|
14
|
+
buttonIcon: { control: false },
|
|
15
|
+
onButtonClick: { control: false },
|
|
16
|
+
buttonProps: { control: false },
|
|
17
|
+
title: { control: 'text' },
|
|
18
|
+
description: { control: 'text' },
|
|
19
|
+
buttonLabel: { control: 'text' },
|
|
20
|
+
},
|
|
21
|
+
} satisfies Meta<typeof NoData>;
|
|
22
|
+
|
|
23
|
+
export default meta;
|
|
24
|
+
type Story = StoryObj<typeof meta>;
|
|
25
|
+
|
|
26
|
+
const noop = () => {};
|
|
27
|
+
|
|
28
|
+
const defaultActions = [
|
|
29
|
+
{ icon: <Zap />, label: 'Feature Description', onClick: noop },
|
|
30
|
+
{ icon: <Sparkles />, label: 'Feature Description', onClick: noop },
|
|
31
|
+
{ icon: <Database />, label: 'Feature Description', onClick: noop },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// === Full composition ===
|
|
35
|
+
|
|
36
|
+
export const Default: Story = {
|
|
37
|
+
args: {
|
|
38
|
+
icon: <Inbox />,
|
|
39
|
+
title: 'No data available',
|
|
40
|
+
description: 'Start by adding an item',
|
|
41
|
+
actions: defaultActions,
|
|
42
|
+
buttonLabel: 'Ask Mingo about Item',
|
|
43
|
+
buttonIcon: <Sparkles />,
|
|
44
|
+
onButtonClick: noop,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// === Reduced message ===
|
|
49
|
+
|
|
50
|
+
export const MessageTitleOnly: Story = {
|
|
51
|
+
args: {
|
|
52
|
+
icon: <Search />,
|
|
53
|
+
title: 'No results found',
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// === Without one region ===
|
|
58
|
+
|
|
59
|
+
export const WithoutActions: Story = {
|
|
60
|
+
args: {
|
|
61
|
+
icon: <Inbox />,
|
|
62
|
+
title: 'No data available',
|
|
63
|
+
description: 'Start by adding an item',
|
|
64
|
+
buttonLabel: 'Add item',
|
|
65
|
+
buttonIcon: <Plus />,
|
|
66
|
+
onButtonClick: noop,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const WithoutButton: Story = {
|
|
71
|
+
args: {
|
|
72
|
+
icon: <Inbox />,
|
|
73
|
+
title: 'No data available',
|
|
74
|
+
description: 'Start by adding an item',
|
|
75
|
+
actions: defaultActions,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// === Dynamic block count ===
|
|
80
|
+
|
|
81
|
+
export const TwoActions: Story = {
|
|
82
|
+
args: {
|
|
83
|
+
icon: <Database />,
|
|
84
|
+
title: 'No data available',
|
|
85
|
+
description: 'Start by adding an item',
|
|
86
|
+
actions: [
|
|
87
|
+
{ icon: <Zap />, label: 'Feature Description', onClick: noop },
|
|
88
|
+
{ icon: <Sparkles />, label: 'Feature Description', onClick: noop },
|
|
89
|
+
],
|
|
90
|
+
buttonLabel: 'Ask Mingo about Item',
|
|
91
|
+
buttonIcon: <Sparkles />,
|
|
92
|
+
onButtonClick: noop,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const FourActions: Story = {
|
|
97
|
+
args: {
|
|
98
|
+
icon: <Database />,
|
|
99
|
+
title: 'No data available',
|
|
100
|
+
description: 'Start by adding an item',
|
|
101
|
+
actions: [
|
|
102
|
+
{ icon: <Zap />, label: 'Real-time sync', onClick: noop },
|
|
103
|
+
{ icon: <Sparkles />, label: 'Smart suggestions', onClick: noop },
|
|
104
|
+
{ icon: <Database />, label: 'Unlimited storage', onClick: noop },
|
|
105
|
+
{ icon: <FileText />, label: 'Export anywhere', onClick: noop },
|
|
106
|
+
],
|
|
107
|
+
buttonLabel: 'Ask Mingo about Item',
|
|
108
|
+
buttonIcon: <Sparkles />,
|
|
109
|
+
onButtonClick: noop,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// === Disabled blocks ===
|
|
114
|
+
|
|
115
|
+
export const DisabledActions: Story = {
|
|
116
|
+
args: {
|
|
117
|
+
icon: <Inbox />,
|
|
118
|
+
title: 'No data available',
|
|
119
|
+
description: 'Some actions are unavailable',
|
|
120
|
+
actions: [
|
|
121
|
+
{ icon: <Zap />, label: 'Available', onClick: noop },
|
|
122
|
+
{ icon: <Sparkles />, label: 'Disabled', onClick: noop, disabled: true },
|
|
123
|
+
{ icon: <Database />, label: 'Disabled', onClick: noop, disabled: true },
|
|
124
|
+
],
|
|
125
|
+
buttonLabel: 'Ask Mingo about Item',
|
|
126
|
+
buttonIcon: <Sparkles />,
|
|
127
|
+
onButtonClick: noop,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// === Footer button variations ===
|
|
132
|
+
|
|
133
|
+
export const ButtonAsLink: Story = {
|
|
134
|
+
args: {
|
|
135
|
+
icon: <Upload />,
|
|
136
|
+
title: 'No data available',
|
|
137
|
+
description: 'Import your first dataset',
|
|
138
|
+
buttonLabel: 'View docs',
|
|
139
|
+
buttonIcon: <FileText />,
|
|
140
|
+
buttonProps: { href: '/docs' },
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export const CustomButton: Story = {
|
|
145
|
+
args: {
|
|
146
|
+
icon: <Settings />,
|
|
147
|
+
title: 'No data available',
|
|
148
|
+
description: 'Configure a source to get started',
|
|
149
|
+
// `button` is the escape hatch — pass any node.
|
|
150
|
+
button: (
|
|
151
|
+
<div className="flex gap-2">
|
|
152
|
+
<Button variant="outline" leftIcon={<Plus />}>Connect source</Button>
|
|
153
|
+
<Button variant="transparent" leftIcon={<FileText />}>View docs</Button>
|
|
154
|
+
</div>
|
|
155
|
+
),
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// === Standalone parts ===
|
|
160
|
+
|
|
161
|
+
export const StandaloneActions: Story = {
|
|
162
|
+
args: { title: '' },
|
|
163
|
+
render: () => (
|
|
164
|
+
<NoDataActions>
|
|
165
|
+
<NoDataAction icon={<Zap />} label="Clickable block" onClick={noop} />
|
|
166
|
+
<NoDataAction icon={<Sparkles />} label="Clickable block" onClick={noop} />
|
|
167
|
+
<NoDataAction icon={<Database />} label="Disabled block" onClick={noop} disabled />
|
|
168
|
+
</NoDataActions>
|
|
169
|
+
),
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export const StandaloneMessage: Story = {
|
|
173
|
+
args: { title: '' },
|
|
174
|
+
render: () => (
|
|
175
|
+
<NoDataMessage
|
|
176
|
+
icon={<Search />}
|
|
177
|
+
title="No results found"
|
|
178
|
+
description="Try adjusting your filters"
|
|
179
|
+
/>
|
|
180
|
+
),
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// === Showcase ===
|
|
184
|
+
|
|
185
|
+
export const Showcase: Story = {
|
|
186
|
+
args: { title: '' },
|
|
187
|
+
render: () => (
|
|
188
|
+
<div className="flex flex-col items-stretch gap-12">
|
|
189
|
+
<NoData
|
|
190
|
+
icon={<Inbox />}
|
|
191
|
+
title="No data available"
|
|
192
|
+
description="Start by adding an item"
|
|
193
|
+
actions={defaultActions}
|
|
194
|
+
buttonLabel="Ask Mingo about Item"
|
|
195
|
+
buttonIcon={<Sparkles />}
|
|
196
|
+
onButtonClick={noop}
|
|
197
|
+
/>
|
|
198
|
+
<NoData icon={<Search />} title="No results found" description="Try a different search" />
|
|
199
|
+
<NoData
|
|
200
|
+
icon={<Database />}
|
|
201
|
+
title="No data available"
|
|
202
|
+
description="Start by adding an item"
|
|
203
|
+
actions={defaultActions}
|
|
204
|
+
/>
|
|
205
|
+
</div>
|
|
206
|
+
),
|
|
207
|
+
};
|