@startsimpli/ui 0.1.0 → 0.1.3
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-27YUQBOE.mjs +3954 -0
- package/dist/chunk-27YUQBOE.mjs.map +1 -0
- package/dist/chunk-G2AM3DBU.mjs +1026 -0
- package/dist/chunk-G2AM3DBU.mjs.map +1 -0
- package/dist/chunk-G4XBXCFH.mjs +63 -0
- package/dist/chunk-G4XBXCFH.mjs.map +1 -0
- package/dist/chunk-LZOMFHX3.mjs +35 -0
- package/dist/chunk-LZOMFHX3.mjs.map +1 -0
- package/dist/chunk-QYXFLOO7.mjs +210 -0
- package/dist/chunk-QYXFLOO7.mjs.map +1 -0
- package/dist/components/index.d.mts +472 -0
- package/dist/components/index.d.ts +472 -0
- package/dist/components/index.js +5149 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/index.mjs +6 -0
- package/dist/components/index.mjs.map +1 -0
- package/dist/components/unified-table/index.d.mts +725 -0
- package/dist/components/unified-table/index.d.ts +725 -0
- package/dist/components/unified-table/index.js +4000 -0
- package/dist/components/unified-table/index.js.map +1 -0
- package/dist/components/unified-table/index.mjs +5 -0
- package/dist/components/unified-table/index.mjs.map +1 -0
- package/dist/index.d.mts +26 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +5448 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +12 -0
- package/dist/index.mjs.map +1 -0
- package/dist/theme/index.d.mts +20 -0
- package/dist/theme/index.d.ts +20 -0
- package/dist/theme/index.js +245 -0
- package/dist/theme/index.js.map +1 -0
- package/dist/theme/index.mjs +9 -0
- package/dist/theme/index.mjs.map +1 -0
- package/dist/utils/index.d.mts +38 -0
- package/dist/utils/index.d.ts +38 -0
- package/dist/utils/index.js +72 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/index.mjs +4 -0
- package/dist/utils/index.mjs.map +1 -0
- package/package.json +62 -21
- package/src/__mocks__/next/navigation.js +18 -0
- package/src/components/__tests__/safe-html.test.tsx +45 -0
- package/src/components/__tests__/states.test.tsx +94 -0
- package/src/components/__tests__/status-badge.test.tsx +101 -0
- package/src/components/__tests__/toast.test.tsx +124 -0
- package/src/components/badge/StatusBadge.tsx +55 -0
- package/src/components/badge/index.ts +2 -0
- package/src/components/dialog/BaseDialog.tsx +184 -0
- package/src/components/dialog/index.ts +8 -0
- package/src/components/index.ts +25 -0
- package/src/components/loading/DashboardSkeleton.tsx +27 -0
- package/src/components/loading/TableSkeleton.tsx +63 -0
- package/src/components/loading/index.ts +4 -0
- package/src/components/safe-html.tsx +18 -0
- package/src/components/states/EmptyState.tsx +48 -0
- package/src/components/states/ErrorState.tsx +76 -0
- package/src/components/states/index.ts +4 -0
- package/src/components/toast/Toaster.tsx +72 -0
- package/src/components/toast/index.ts +5 -0
- package/src/components/toast/use-notify.ts +45 -0
- package/src/components/toast/use-toast.ts +150 -0
- package/src/components/ui/api-error-boundary.tsx +64 -0
- package/src/components/ui/feature-gate.tsx +87 -0
- package/src/components/ui/index.ts +4 -0
- package/src/components/ui/page-loader.tsx +31 -0
- package/src/components/ui/query-provider.tsx +30 -0
- package/src/components/unified-table/components/Toolbar/StandardTableToolbar.tsx +1 -1
- package/src/components/unified-table/hooks/useFilters.ts +1 -0
- package/src/components/unified-table/hooks/usePagination.ts +1 -0
- package/src/components/unified-table/hooks/useSelection.ts +2 -1
- package/src/components/unified-table/hooks/useTableKeyboard.ts +2 -1
- package/src/components/unified-table/hooks/useTablePreferences.ts +1 -0
- package/src/components/unified-table/hooks/useTableState.ts +1 -0
- package/src/components/unified-table/hooks/useTableURL.test.tsx +1 -1
- package/src/components/unified-table/index.ts +4 -0
- package/src/components/wizard/StepIndicator.tsx +60 -0
- package/src/components/wizard/index.ts +2 -0
- package/src/theme/tailwind.config.d.ts +3 -0
- package/tailwind.preset.js +87 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { cn } from '../../lib/utils'
|
|
3
|
+
|
|
4
|
+
export interface DashboardSkeletonProps {
|
|
5
|
+
cards?: number
|
|
6
|
+
className?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function DashboardSkeleton({
|
|
10
|
+
cards = 4,
|
|
11
|
+
className,
|
|
12
|
+
}: DashboardSkeletonProps) {
|
|
13
|
+
return (
|
|
14
|
+
<div className={cn('grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4', className)}>
|
|
15
|
+
{Array.from({ length: cards }).map((_, i) => (
|
|
16
|
+
<div key={i} className="rounded-lg border bg-background p-6">
|
|
17
|
+
<div className="flex items-center justify-between mb-4">
|
|
18
|
+
<div className="h-4 w-24 animate-pulse rounded-md bg-gray-200" />
|
|
19
|
+
<div className="h-8 w-8 animate-pulse rounded-md bg-gray-200" />
|
|
20
|
+
</div>
|
|
21
|
+
<div className="h-8 w-20 animate-pulse rounded-md bg-gray-200 mb-2" />
|
|
22
|
+
<div className="h-3 w-32 animate-pulse rounded-md bg-gray-200" />
|
|
23
|
+
</div>
|
|
24
|
+
))}
|
|
25
|
+
</div>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { cn } from '../../lib/utils'
|
|
3
|
+
|
|
4
|
+
export interface TableSkeletonProps {
|
|
5
|
+
rows?: number
|
|
6
|
+
columns?: number
|
|
7
|
+
className?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function TableSkeleton({
|
|
11
|
+
rows = 5,
|
|
12
|
+
columns = 4,
|
|
13
|
+
className,
|
|
14
|
+
}: TableSkeletonProps) {
|
|
15
|
+
return (
|
|
16
|
+
<div className={cn('bg-background rounded-lg border', className)}>
|
|
17
|
+
{/* Header skeleton */}
|
|
18
|
+
<div className="p-4 border-b">
|
|
19
|
+
<div className="flex justify-between items-center gap-4">
|
|
20
|
+
<div className="h-10 w-full max-w-sm animate-pulse rounded-md bg-gray-200" />
|
|
21
|
+
<div className="flex gap-2">
|
|
22
|
+
<div className="h-10 w-24 animate-pulse rounded-md bg-gray-200" />
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
{/* Table body skeleton */}
|
|
28
|
+
<div className="overflow-x-auto">
|
|
29
|
+
<table className="w-full">
|
|
30
|
+
<thead className="border-b bg-muted/50">
|
|
31
|
+
<tr>
|
|
32
|
+
{Array.from({ length: columns }).map((_, i) => (
|
|
33
|
+
<th key={i} className="p-4 text-left">
|
|
34
|
+
<div className="h-4 w-24 animate-pulse rounded-md bg-gray-200" />
|
|
35
|
+
</th>
|
|
36
|
+
))}
|
|
37
|
+
</tr>
|
|
38
|
+
</thead>
|
|
39
|
+
<tbody>
|
|
40
|
+
{Array.from({ length: rows }).map((_, rowIndex) => (
|
|
41
|
+
<tr key={rowIndex} className="border-b last:border-b-0">
|
|
42
|
+
{Array.from({ length: columns }).map((_, colIndex) => (
|
|
43
|
+
<td key={colIndex} className="p-4">
|
|
44
|
+
<div className="h-4 w-full max-w-[200px] animate-pulse rounded-md bg-gray-200" />
|
|
45
|
+
</td>
|
|
46
|
+
))}
|
|
47
|
+
</tr>
|
|
48
|
+
))}
|
|
49
|
+
</tbody>
|
|
50
|
+
</table>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
{/* Pagination skeleton */}
|
|
54
|
+
<div className="p-4 border-t flex justify-between items-center">
|
|
55
|
+
<div className="h-4 w-40 animate-pulse rounded-md bg-gray-200" />
|
|
56
|
+
<div className="flex gap-2">
|
|
57
|
+
<div className="h-9 w-20 animate-pulse rounded-md bg-gray-200" />
|
|
58
|
+
<div className="h-9 w-20 animate-pulse rounded-md bg-gray-200" />
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import DOMPurify from 'isomorphic-dompurify';
|
|
2
|
+
import { type ElementType, type HTMLAttributes } from 'react';
|
|
3
|
+
|
|
4
|
+
interface SafeHtmlProps extends HTMLAttributes<HTMLElement> {
|
|
5
|
+
html: string;
|
|
6
|
+
as?: ElementType;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Renders user-supplied or external HTML safely by running it through DOMPurify.
|
|
11
|
+
* Strips script tags, event handlers, javascript: URLs, and other XSS vectors.
|
|
12
|
+
* Use this instead of dangerouslySetInnerHTML anywhere app content is rendered.
|
|
13
|
+
*/
|
|
14
|
+
export function SafeHtml({ html, as: Tag = 'div', ...props }: SafeHtmlProps) {
|
|
15
|
+
const sanitized = DOMPurify.sanitize(html, { ALLOW_DATA_ATTR: false });
|
|
16
|
+
// eslint-disable-next-line react/no-danger
|
|
17
|
+
return <Tag {...props} dangerouslySetInnerHTML={{ __html: sanitized }} />;
|
|
18
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { cn } from '../../lib/utils'
|
|
3
|
+
|
|
4
|
+
export interface EmptyStateAction {
|
|
5
|
+
label: string
|
|
6
|
+
onClick: () => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface EmptyStateProps {
|
|
10
|
+
title: string
|
|
11
|
+
description?: string
|
|
12
|
+
action?: EmptyStateAction
|
|
13
|
+
className?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function EmptyState({
|
|
17
|
+
title,
|
|
18
|
+
description,
|
|
19
|
+
action,
|
|
20
|
+
className,
|
|
21
|
+
}: EmptyStateProps) {
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
className={cn(
|
|
25
|
+
'flex flex-col items-center justify-center py-12 px-4 text-center',
|
|
26
|
+
className
|
|
27
|
+
)}
|
|
28
|
+
role="status"
|
|
29
|
+
aria-live="polite"
|
|
30
|
+
>
|
|
31
|
+
<h3 className="text-xl font-semibold text-foreground mb-3">{title}</h3>
|
|
32
|
+
{description && (
|
|
33
|
+
<p className="text-base text-muted-foreground mb-8 max-w-md leading-relaxed">
|
|
34
|
+
{description}
|
|
35
|
+
</p>
|
|
36
|
+
)}
|
|
37
|
+
{action && (
|
|
38
|
+
<button
|
|
39
|
+
onClick={action.onClick}
|
|
40
|
+
className="inline-flex items-center justify-center rounded-md text-sm font-medium bg-primary text-primary-foreground shadow hover:bg-primary/90 h-9 px-4 py-2 transition-colors"
|
|
41
|
+
aria-label={action.label}
|
|
42
|
+
>
|
|
43
|
+
{action.label}
|
|
44
|
+
</button>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { cn } from '../../lib/utils'
|
|
3
|
+
|
|
4
|
+
export interface ErrorStateProps {
|
|
5
|
+
title?: string
|
|
6
|
+
message: string
|
|
7
|
+
onRetry?: () => void
|
|
8
|
+
className?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ErrorState({
|
|
12
|
+
title = 'Something went wrong',
|
|
13
|
+
message,
|
|
14
|
+
onRetry,
|
|
15
|
+
className,
|
|
16
|
+
}: ErrorStateProps) {
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
className={cn(
|
|
20
|
+
'flex flex-col items-center justify-center py-12 px-4 text-center',
|
|
21
|
+
className
|
|
22
|
+
)}
|
|
23
|
+
role="alert"
|
|
24
|
+
aria-live="assertive"
|
|
25
|
+
>
|
|
26
|
+
<div
|
|
27
|
+
className="w-16 h-16 mb-4 rounded-full bg-red-50 flex items-center justify-center"
|
|
28
|
+
aria-hidden="true"
|
|
29
|
+
>
|
|
30
|
+
<svg
|
|
31
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
32
|
+
width="32"
|
|
33
|
+
height="32"
|
|
34
|
+
viewBox="0 0 24 24"
|
|
35
|
+
fill="none"
|
|
36
|
+
stroke="currentColor"
|
|
37
|
+
strokeWidth="2"
|
|
38
|
+
strokeLinecap="round"
|
|
39
|
+
strokeLinejoin="round"
|
|
40
|
+
className="text-red-500"
|
|
41
|
+
>
|
|
42
|
+
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" />
|
|
43
|
+
<line x1="12" y1="9" x2="12" y2="13" />
|
|
44
|
+
<line x1="12" y1="17" x2="12.01" y2="17" />
|
|
45
|
+
</svg>
|
|
46
|
+
</div>
|
|
47
|
+
<h3 className="text-lg font-semibold text-foreground mb-2">{title}</h3>
|
|
48
|
+
<p className="text-sm text-muted-foreground mb-6 max-w-sm">{message}</p>
|
|
49
|
+
{onRetry && (
|
|
50
|
+
<button
|
|
51
|
+
onClick={onRetry}
|
|
52
|
+
className="inline-flex items-center justify-center rounded-md text-sm font-medium border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground h-9 px-4 py-2 transition-colors"
|
|
53
|
+
aria-label="Retry loading content"
|
|
54
|
+
>
|
|
55
|
+
<svg
|
|
56
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
57
|
+
width="16"
|
|
58
|
+
height="16"
|
|
59
|
+
viewBox="0 0 24 24"
|
|
60
|
+
fill="none"
|
|
61
|
+
stroke="currentColor"
|
|
62
|
+
strokeWidth="2"
|
|
63
|
+
strokeLinecap="round"
|
|
64
|
+
strokeLinejoin="round"
|
|
65
|
+
className="mr-2"
|
|
66
|
+
aria-hidden="true"
|
|
67
|
+
>
|
|
68
|
+
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
|
|
69
|
+
<path d="M21 3v5h-5" />
|
|
70
|
+
</svg>
|
|
71
|
+
Try Again
|
|
72
|
+
</button>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { cn } from '../../lib/utils'
|
|
5
|
+
import { useToast, type ToastVariant } from './use-toast'
|
|
6
|
+
|
|
7
|
+
const variantClasses: Record<ToastVariant, string> = {
|
|
8
|
+
default: 'bg-background border-border text-foreground',
|
|
9
|
+
destructive: 'bg-destructive border-destructive text-destructive-foreground',
|
|
10
|
+
success: 'bg-green-50 border-green-200 text-green-900',
|
|
11
|
+
warning: 'bg-yellow-50 border-yellow-200 text-yellow-900',
|
|
12
|
+
info: 'bg-blue-50 border-blue-200 text-blue-900',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Toaster() {
|
|
16
|
+
const { toasts, dismiss } = useToast()
|
|
17
|
+
|
|
18
|
+
if (toasts.length === 0) return null
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
className="fixed bottom-4 right-4 z-[100] flex flex-col gap-2 w-full max-w-sm"
|
|
23
|
+
role="region"
|
|
24
|
+
aria-label="Notifications"
|
|
25
|
+
>
|
|
26
|
+
{toasts.map((t) => (
|
|
27
|
+
<div
|
|
28
|
+
key={t.id}
|
|
29
|
+
className={cn(
|
|
30
|
+
'rounded-lg border p-4 shadow-lg transition-all animate-in slide-in-from-bottom-2 fade-in-0',
|
|
31
|
+
variantClasses[t.variant ?? 'default']
|
|
32
|
+
)}
|
|
33
|
+
role="status"
|
|
34
|
+
aria-live="polite"
|
|
35
|
+
>
|
|
36
|
+
<div className="flex items-start justify-between gap-2">
|
|
37
|
+
<div className="flex-1">
|
|
38
|
+
{t.title && (
|
|
39
|
+
<div className="text-sm font-semibold">{t.title}</div>
|
|
40
|
+
)}
|
|
41
|
+
{t.description && (
|
|
42
|
+
<div className="text-sm opacity-90 mt-1">{t.description}</div>
|
|
43
|
+
)}
|
|
44
|
+
</div>
|
|
45
|
+
<button
|
|
46
|
+
onClick={() => dismiss(t.id)}
|
|
47
|
+
className="shrink-0 rounded-sm opacity-70 hover:opacity-100 transition-opacity"
|
|
48
|
+
aria-label="Dismiss notification"
|
|
49
|
+
>
|
|
50
|
+
<svg
|
|
51
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
52
|
+
width="16"
|
|
53
|
+
height="16"
|
|
54
|
+
viewBox="0 0 24 24"
|
|
55
|
+
fill="none"
|
|
56
|
+
stroke="currentColor"
|
|
57
|
+
strokeWidth="2"
|
|
58
|
+
strokeLinecap="round"
|
|
59
|
+
strokeLinejoin="round"
|
|
60
|
+
aria-hidden="true"
|
|
61
|
+
>
|
|
62
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
63
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
64
|
+
</svg>
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
{t.action && <div className="mt-2">{t.action}</div>}
|
|
68
|
+
</div>
|
|
69
|
+
))}
|
|
70
|
+
</div>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { toast } from './use-toast'
|
|
2
|
+
|
|
3
|
+
export interface NotifyOptions {
|
|
4
|
+
title?: string
|
|
5
|
+
description?: string
|
|
6
|
+
duration?: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function success(messageOrOptions: string | NotifyOptions) {
|
|
10
|
+
const opts =
|
|
11
|
+
typeof messageOrOptions === 'string'
|
|
12
|
+
? { title: messageOrOptions }
|
|
13
|
+
: messageOrOptions
|
|
14
|
+
return toast({ variant: 'success', ...opts })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function error(messageOrOptions: string | NotifyOptions) {
|
|
18
|
+
const opts =
|
|
19
|
+
typeof messageOrOptions === 'string'
|
|
20
|
+
? { title: messageOrOptions }
|
|
21
|
+
: messageOrOptions
|
|
22
|
+
return toast({ variant: 'destructive', ...opts })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function warning(messageOrOptions: string | NotifyOptions) {
|
|
26
|
+
const opts =
|
|
27
|
+
typeof messageOrOptions === 'string'
|
|
28
|
+
? { title: messageOrOptions }
|
|
29
|
+
: messageOrOptions
|
|
30
|
+
return toast({ variant: 'warning', ...opts })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function info(messageOrOptions: string | NotifyOptions) {
|
|
34
|
+
const opts =
|
|
35
|
+
typeof messageOrOptions === 'string'
|
|
36
|
+
? { title: messageOrOptions }
|
|
37
|
+
: messageOrOptions
|
|
38
|
+
return toast({ variant: 'info', ...opts })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const notify = { success, error, warning, info }
|
|
42
|
+
|
|
43
|
+
export function useNotify() {
|
|
44
|
+
return notify
|
|
45
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
|
|
3
|
+
const TOAST_LIMIT = 5
|
|
4
|
+
const TOAST_AUTO_DISMISS = 5000
|
|
5
|
+
|
|
6
|
+
export type ToastVariant = 'default' | 'destructive' | 'success' | 'warning' | 'info'
|
|
7
|
+
|
|
8
|
+
export interface ToasterToast {
|
|
9
|
+
id: string
|
|
10
|
+
title?: string
|
|
11
|
+
description?: string
|
|
12
|
+
action?: React.ReactNode
|
|
13
|
+
variant?: ToastVariant
|
|
14
|
+
duration?: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type Action =
|
|
18
|
+
| { type: 'ADD_TOAST'; toast: ToasterToast }
|
|
19
|
+
| { type: 'UPDATE_TOAST'; toast: Partial<ToasterToast> & { id: string } }
|
|
20
|
+
| { type: 'DISMISS_TOAST'; toastId?: string }
|
|
21
|
+
| { type: 'REMOVE_TOAST'; toastId?: string }
|
|
22
|
+
|
|
23
|
+
interface State {
|
|
24
|
+
toasts: ToasterToast[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
|
28
|
+
|
|
29
|
+
function addToRemoveQueue(toastId: string, delay: number) {
|
|
30
|
+
if (toastTimeouts.has(toastId)) {
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const timeout = setTimeout(() => {
|
|
35
|
+
toastTimeouts.delete(toastId)
|
|
36
|
+
dispatch({ type: 'REMOVE_TOAST', toastId })
|
|
37
|
+
}, delay)
|
|
38
|
+
|
|
39
|
+
toastTimeouts.set(toastId, timeout)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function reducer(state: State, action: Action): State {
|
|
43
|
+
switch (action.type) {
|
|
44
|
+
case 'ADD_TOAST':
|
|
45
|
+
return {
|
|
46
|
+
...state,
|
|
47
|
+
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
case 'UPDATE_TOAST':
|
|
51
|
+
return {
|
|
52
|
+
...state,
|
|
53
|
+
toasts: state.toasts.map((t) =>
|
|
54
|
+
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
|
55
|
+
),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
case 'DISMISS_TOAST': {
|
|
59
|
+
const { toastId } = action
|
|
60
|
+
|
|
61
|
+
if (toastId) {
|
|
62
|
+
addToRemoveQueue(toastId, 300)
|
|
63
|
+
} else {
|
|
64
|
+
state.toasts.forEach((t) => addToRemoveQueue(t.id, 300))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return state
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
case 'REMOVE_TOAST':
|
|
71
|
+
if (action.toastId === undefined) {
|
|
72
|
+
return { ...state, toasts: [] }
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
...state,
|
|
76
|
+
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const listeners: Array<(state: State) => void> = []
|
|
82
|
+
|
|
83
|
+
let memoryState: State = { toasts: [] }
|
|
84
|
+
|
|
85
|
+
function dispatch(action: Action) {
|
|
86
|
+
memoryState = reducer(memoryState, action)
|
|
87
|
+
listeners.forEach((listener) => listener(memoryState))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let count = 0
|
|
91
|
+
|
|
92
|
+
function genId() {
|
|
93
|
+
count = (count + 1) % Number.MAX_VALUE
|
|
94
|
+
return count.toString()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
type ToastInput = Omit<ToasterToast, 'id'>
|
|
98
|
+
|
|
99
|
+
function toast(props: ToastInput) {
|
|
100
|
+
const id = genId()
|
|
101
|
+
const duration = props.duration ?? TOAST_AUTO_DISMISS
|
|
102
|
+
|
|
103
|
+
dispatch({
|
|
104
|
+
type: 'ADD_TOAST',
|
|
105
|
+
toast: { ...props, id },
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// Auto-dismiss
|
|
109
|
+
if (duration > 0) {
|
|
110
|
+
addToRemoveQueue(id, duration)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const update = (updateProps: Partial<ToasterToast>) =>
|
|
114
|
+
dispatch({ type: 'UPDATE_TOAST', toast: { ...updateProps, id } })
|
|
115
|
+
|
|
116
|
+
const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id })
|
|
117
|
+
|
|
118
|
+
return { id, dismiss, update }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function clearAllToasts() {
|
|
122
|
+
// Clear all pending timeouts
|
|
123
|
+
toastTimeouts.forEach((timeout) => clearTimeout(timeout))
|
|
124
|
+
toastTimeouts.clear()
|
|
125
|
+
dispatch({ type: 'REMOVE_TOAST', toastId: undefined })
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function useToast() {
|
|
129
|
+
const [state, setState] = React.useState<State>(memoryState)
|
|
130
|
+
|
|
131
|
+
React.useEffect(() => {
|
|
132
|
+
listeners.push(setState)
|
|
133
|
+
return () => {
|
|
134
|
+
const index = listeners.indexOf(setState)
|
|
135
|
+
if (index > -1) {
|
|
136
|
+
listeners.splice(index, 1)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}, [state])
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
...state,
|
|
143
|
+
toast,
|
|
144
|
+
dismiss: (toastId?: string) =>
|
|
145
|
+
dispatch({ type: 'DISMISS_TOAST', toastId }),
|
|
146
|
+
clear: clearAllToasts,
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export { useToast, toast, clearAllToasts }
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { ErrorState } from "../states/ErrorState"
|
|
5
|
+
|
|
6
|
+
interface ApiErrorBoundaryState {
|
|
7
|
+
hasError: boolean
|
|
8
|
+
error: Error | null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ApiErrorBoundaryProps {
|
|
12
|
+
children: React.ReactNode
|
|
13
|
+
/** Custom fallback to render on error. Receives error and reset function. */
|
|
14
|
+
fallback?: (props: { error: Error; reset: () => void }) => React.ReactNode
|
|
15
|
+
/** Called when the error boundary catches an error. */
|
|
16
|
+
onError?: (error: Error, info: React.ErrorInfo) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Error boundary for API-driven page content.
|
|
21
|
+
*
|
|
22
|
+
* Catches unhandled errors from children and renders ErrorState with a retry button.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* <ApiErrorBoundary>
|
|
26
|
+
* <InvestorsTable />
|
|
27
|
+
* </ApiErrorBoundary>
|
|
28
|
+
*/
|
|
29
|
+
export class ApiErrorBoundary extends React.Component<
|
|
30
|
+
ApiErrorBoundaryProps,
|
|
31
|
+
ApiErrorBoundaryState
|
|
32
|
+
> {
|
|
33
|
+
constructor(props: ApiErrorBoundaryProps) {
|
|
34
|
+
super(props)
|
|
35
|
+
this.state = { hasError: false, error: null }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static getDerivedStateFromError(error: Error): ApiErrorBoundaryState {
|
|
39
|
+
return { hasError: true, error }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
|
43
|
+
this.props.onError?.(error, info)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
reset = () => {
|
|
47
|
+
this.setState({ hasError: false, error: null })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
render() {
|
|
51
|
+
if (this.state.hasError && this.state.error) {
|
|
52
|
+
if (this.props.fallback) {
|
|
53
|
+
return this.props.fallback({ error: this.state.error, reset: this.reset })
|
|
54
|
+
}
|
|
55
|
+
return (
|
|
56
|
+
<ErrorState
|
|
57
|
+
message={this.state.error.message || "An unexpected error occurred"}
|
|
58
|
+
onRetry={this.reset}
|
|
59
|
+
/>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
return this.props.children
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip"
|
|
5
|
+
import { cn } from "../../lib/utils"
|
|
6
|
+
|
|
7
|
+
export interface FeatureGateProps {
|
|
8
|
+
/** Whether the feature is available. When false, applies mode behavior. */
|
|
9
|
+
enabled: boolean
|
|
10
|
+
/**
|
|
11
|
+
* How to render when enabled=false:
|
|
12
|
+
* - 'disable' (default): renders children as disabled with a tooltip
|
|
13
|
+
* - 'hide': renders nothing
|
|
14
|
+
* - 'badge': renders children with a 'Coming soon' badge overlay
|
|
15
|
+
*/
|
|
16
|
+
mode?: "disable" | "hide" | "badge"
|
|
17
|
+
/** Tooltip text shown in 'disable' mode. Defaults to 'Coming soon'. */
|
|
18
|
+
tooltip?: string
|
|
19
|
+
children: React.ReactNode
|
|
20
|
+
className?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Gates a UI element behind a feature flag.
|
|
25
|
+
*
|
|
26
|
+
* Use this to wrap any button, link, or UI section that is not yet implemented.
|
|
27
|
+
* This prevents shipping clickable elements with no handlers.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // Disable a button with a tooltip
|
|
31
|
+
* <FeatureGate enabled={false}>
|
|
32
|
+
* <Button onClick={handleReply}>Reply</Button>
|
|
33
|
+
* </FeatureGate>
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* // Hide completely
|
|
37
|
+
* <FeatureGate enabled={false} mode="hide">
|
|
38
|
+
* <Button>Archive</Button>
|
|
39
|
+
* </FeatureGate>
|
|
40
|
+
*/
|
|
41
|
+
export function FeatureGate({
|
|
42
|
+
enabled,
|
|
43
|
+
mode = "disable",
|
|
44
|
+
tooltip = "Coming soon",
|
|
45
|
+
children,
|
|
46
|
+
className,
|
|
47
|
+
}: FeatureGateProps) {
|
|
48
|
+
if (enabled) {
|
|
49
|
+
return <>{children}</>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (mode === "hide") {
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (mode === "badge") {
|
|
57
|
+
return (
|
|
58
|
+
<div className={cn("relative inline-flex", className)}>
|
|
59
|
+
<div className="opacity-60 pointer-events-none select-none">{children}</div>
|
|
60
|
+
<span className="absolute -top-1.5 -right-1.5 rounded-full bg-muted text-muted-foreground text-[10px] font-medium px-1.5 py-0.5 leading-none border">
|
|
61
|
+
Soon
|
|
62
|
+
</span>
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// mode === 'disable' (default): wrap in tooltip, disable interaction
|
|
68
|
+
return (
|
|
69
|
+
<TooltipProvider>
|
|
70
|
+
<Tooltip>
|
|
71
|
+
<TooltipTrigger asChild>
|
|
72
|
+
<span
|
|
73
|
+
className={cn("inline-flex cursor-not-allowed", className)}
|
|
74
|
+
aria-disabled="true"
|
|
75
|
+
>
|
|
76
|
+
<span className="pointer-events-none opacity-50 select-none">
|
|
77
|
+
{children}
|
|
78
|
+
</span>
|
|
79
|
+
</span>
|
|
80
|
+
</TooltipTrigger>
|
|
81
|
+
<TooltipContent>
|
|
82
|
+
<p>{tooltip}</p>
|
|
83
|
+
</TooltipContent>
|
|
84
|
+
</Tooltip>
|
|
85
|
+
</TooltipProvider>
|
|
86
|
+
)
|
|
87
|
+
}
|