@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.
Files changed (75) hide show
  1. package/dist/{chunk-J6O57C7S.js → chunk-ALCUDWKJ.js} +3356 -3173
  2. package/dist/chunk-ALCUDWKJ.js.map +1 -0
  3. package/dist/{chunk-Z6MKG53Z.js → chunk-DATYABIL.js} +2 -2
  4. package/dist/{chunk-YV55JZAD.cjs → chunk-FEIRH7ER.cjs} +25 -25
  5. package/dist/{chunk-YV55JZAD.cjs.map → chunk-FEIRH7ER.cjs.map} +1 -1
  6. package/dist/{chunk-XSK72QRR.js → chunk-HQGGXT2T.js} +2 -2
  7. package/dist/{chunk-CI5XE4LB.cjs → chunk-NM7J44WP.cjs} +30 -30
  8. package/dist/{chunk-CI5XE4LB.cjs.map → chunk-NM7J44WP.cjs.map} +1 -1
  9. package/dist/{chunk-O6UA475M.cjs → chunk-QDA2XVFP.cjs} +1086 -903
  10. package/dist/chunk-QDA2XVFP.cjs.map +1 -0
  11. package/dist/{chunk-GJVSLS2R.js → chunk-TOQH27ZP.js} +2 -2
  12. package/dist/{chunk-TPZI7NBN.cjs → chunk-TZEHP5IW.cjs} +11 -11
  13. package/dist/{chunk-TPZI7NBN.cjs.map → chunk-TZEHP5IW.cjs.map} +1 -1
  14. package/dist/components/chat/approval-batch-message.d.ts +8 -0
  15. package/dist/components/chat/approval-batch-message.d.ts.map +1 -1
  16. package/dist/components/chat/index.cjs +2 -2
  17. package/dist/components/chat/index.js +1 -1
  18. package/dist/components/chat/types/chat.types.d.ts +1 -0
  19. package/dist/components/chat/types/chat.types.d.ts.map +1 -1
  20. package/dist/components/contact/index.cjs +3 -3
  21. package/dist/components/contact/index.js +2 -2
  22. package/dist/components/features/index.cjs +6 -2
  23. package/dist/components/features/index.cjs.map +1 -1
  24. package/dist/components/features/index.js +5 -1
  25. package/dist/components/features/notifications/approval-request-notification-tile.d.ts.map +1 -1
  26. package/dist/components/features/notifications/index.d.ts +1 -1
  27. package/dist/components/features/notifications/index.d.ts.map +1 -1
  28. package/dist/components/features/notifications/types.d.ts +9 -0
  29. package/dist/components/features/notifications/types.d.ts.map +1 -1
  30. package/dist/components/index.cjs +65 -47
  31. package/dist/components/index.cjs.map +1 -1
  32. package/dist/components/index.js +21 -3
  33. package/dist/components/index.js.map +1 -1
  34. package/dist/components/navigation/index.cjs +2 -2
  35. package/dist/components/navigation/index.js +1 -1
  36. package/dist/components/related-content/index.cjs +3 -3
  37. package/dist/components/related-content/index.js +2 -2
  38. package/dist/components/tickets/index.cjs +53 -53
  39. package/dist/components/tickets/index.js +3 -3
  40. package/dist/components/ui/index.cjs +16 -2
  41. package/dist/components/ui/index.cjs.map +1 -1
  42. package/dist/components/ui/index.d.ts +1 -0
  43. package/dist/components/ui/index.d.ts.map +1 -1
  44. package/dist/components/ui/index.js +15 -1
  45. package/dist/components/ui/no-data/index.d.ts +5 -0
  46. package/dist/components/ui/no-data/index.d.ts.map +1 -0
  47. package/dist/components/ui/no-data/no-data-actions.d.ts +34 -0
  48. package/dist/components/ui/no-data/no-data-actions.d.ts.map +1 -0
  49. package/dist/components/ui/no-data/no-data-message.d.ts +17 -0
  50. package/dist/components/ui/no-data/no-data-message.d.ts.map +1 -0
  51. package/dist/components/ui/no-data/no-data-styles.d.ts +8 -0
  52. package/dist/components/ui/no-data/no-data-styles.d.ts.map +1 -0
  53. package/dist/components/ui/no-data/no-data.d.ts +33 -0
  54. package/dist/components/ui/no-data/no-data.d.ts.map +1 -0
  55. package/dist/index.cjs +20 -2
  56. package/dist/index.cjs.map +1 -1
  57. package/dist/index.js +19 -1
  58. package/package.json +1 -1
  59. package/src/components/chat/approval-batch-message.tsx +32 -19
  60. package/src/components/chat/types/chat.types.ts +1 -0
  61. package/src/components/features/notifications/approval-request-notification-tile.tsx +23 -26
  62. package/src/components/features/notifications/index.ts +2 -0
  63. package/src/components/features/notifications/types.ts +39 -0
  64. package/src/components/ui/index.ts +1 -0
  65. package/src/components/ui/no-data/index.ts +4 -0
  66. package/src/components/ui/no-data/no-data-actions.tsx +102 -0
  67. package/src/components/ui/no-data/no-data-message.tsx +43 -0
  68. package/src/components/ui/no-data/no-data-styles.ts +36 -0
  69. package/src/components/ui/no-data/no-data.tsx +84 -0
  70. package/src/stories/EmptyState.stories.tsx +207 -0
  71. package/dist/chunk-J6O57C7S.js.map +0 -1
  72. package/dist/chunk-O6UA475M.cjs.map +0 -1
  73. /package/dist/{chunk-Z6MKG53Z.js.map → chunk-DATYABIL.js.map} +0 -0
  74. /package/dist/{chunk-XSK72QRR.js.map → chunk-HQGGXT2T.js.map} +0 -0
  75. /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 { ApprovalBatchData, ChatApprovalStatus } from '../../chat/types'
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
- const [status, setStatus] = useState<ChatApprovalStatus>('pending')
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<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])
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
- await onApprove(batchData.approvalRequestId)
69
- setStatus('approved')
70
- onComplete(notification.id)
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
- await onReject(batchData.approvalRequestId)
74
- setStatus('rejected')
75
- onComplete(notification.id)
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,4 @@
1
+ export * from "./no-data"
2
+ export * from "./no-data-message"
3
+ export * from "./no-data-actions"
4
+ export * from "./no-data-styles"
@@ -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
+ };