@syscore/ui-library 1.25.0 → 2.0.0
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/client/components/ui/app-bar.tsx +299 -0
- package/client/components/ui/banner.tsx +108 -0
- package/client/components/ui/bottom-navigation.tsx +223 -0
- package/client/components/ui/card.tsx +108 -26
- package/client/components/ui/date-picker.tsx +188 -0
- package/client/components/ui/empty-state.tsx +197 -0
- package/client/components/ui/file-upload.tsx +214 -0
- package/client/components/ui/footer.tsx +179 -0
- package/client/components/ui/layout.tsx +381 -0
- package/client/components/ui/sidebar.tsx +11 -5
- package/client/components/ui/stepper.tsx +148 -0
- package/client/components/ui/system-bar.tsx +119 -0
- package/client/components/ui/timeline.tsx +181 -0
- package/client/global.css +2304 -41
- package/client/ui/AppBar/app-bar.stories.tsx +280 -0
- package/client/ui/Banner/banner.stories.tsx +238 -0
- package/client/ui/BottomNavigation/bottom-navigation.stories.tsx +285 -0
- package/client/ui/Card.stories.tsx +285 -168
- package/client/ui/DatePicker/DatePicker.stories.tsx +293 -0
- package/client/ui/EmptyState/empty-state.stories.tsx +251 -0
- package/client/ui/FileUpload/FileUpload.stories.tsx +218 -0
- package/client/ui/Footer/footer.stories.tsx +194 -0
- package/client/ui/Layout.stories.tsx +1481 -0
- package/client/ui/Stepper/Stepper.stories.tsx +344 -0
- package/client/ui/SystemBar/system-bar.stories.tsx +179 -0
- package/client/ui/Timeline/timeline.stories.tsx +404 -0
- package/dist/index.cjs.js +1 -1
- package/dist/index.d.ts +219 -0
- package/dist/index.es.js +1504 -99
- package/package.json +1 -1
|
@@ -1,18 +1,37 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
2
3
|
|
|
3
4
|
import { cn } from "@/lib/utils";
|
|
4
5
|
import { Text } from "./typography";
|
|
5
6
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
const cardVariants = cva("card", {
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: "card--default",
|
|
11
|
+
outlined: "card--outlined",
|
|
12
|
+
elevated: "card--elevated",
|
|
13
|
+
filled: "card--filled",
|
|
14
|
+
interactive: "card--interactive",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
defaultVariants: {
|
|
18
|
+
variant: "default",
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
interface CardProps
|
|
23
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
24
|
+
VariantProps<typeof cardVariants> {}
|
|
25
|
+
|
|
26
|
+
const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
|
27
|
+
({ className, variant, ...props }, ref) => (
|
|
28
|
+
<div
|
|
29
|
+
ref={ref}
|
|
30
|
+
className={cn(cardVariants({ variant }), className)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
),
|
|
34
|
+
);
|
|
16
35
|
Card.displayName = "Card";
|
|
17
36
|
|
|
18
37
|
const CardHeader = React.forwardRef<
|
|
@@ -71,6 +90,65 @@ const CardFooter = React.forwardRef<
|
|
|
71
90
|
));
|
|
72
91
|
CardFooter.displayName = "CardFooter";
|
|
73
92
|
|
|
93
|
+
const CardMedia = React.forwardRef<
|
|
94
|
+
HTMLDivElement,
|
|
95
|
+
React.HTMLAttributes<HTMLDivElement> & { src?: string; alt?: string; height?: number }
|
|
96
|
+
>(({ className, src, alt = "", height = 200, children, ...props }, ref) => (
|
|
97
|
+
<div ref={ref} className={cn("card-media", className)} style={{ height }} {...props}>
|
|
98
|
+
{src ? (
|
|
99
|
+
<img src={src} alt={alt} className="card-media__img" />
|
|
100
|
+
) : (
|
|
101
|
+
children
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
));
|
|
105
|
+
CardMedia.displayName = "CardMedia";
|
|
106
|
+
|
|
107
|
+
interface CardBadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
108
|
+
color?: "default" | "success" | "warning" | "error" | "info";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const CardBadge = React.forwardRef<HTMLSpanElement, CardBadgeProps>(
|
|
112
|
+
({ className, color = "default", ...props }, ref) => (
|
|
113
|
+
<span
|
|
114
|
+
ref={ref}
|
|
115
|
+
className={cn("card-badge", `card-badge--${color}`, className)}
|
|
116
|
+
{...props}
|
|
117
|
+
/>
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
CardBadge.displayName = "CardBadge";
|
|
121
|
+
|
|
122
|
+
const CardActions = React.forwardRef<
|
|
123
|
+
HTMLDivElement,
|
|
124
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
125
|
+
>(({ className, ...props }, ref) => (
|
|
126
|
+
<div ref={ref} className={cn("card-actions", className)} {...props} />
|
|
127
|
+
));
|
|
128
|
+
CardActions.displayName = "CardActions";
|
|
129
|
+
|
|
130
|
+
interface CardMetricProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
131
|
+
value: string | number;
|
|
132
|
+
label: string;
|
|
133
|
+
trend?: "up" | "down" | "neutral";
|
|
134
|
+
trendValue?: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const CardMetric = React.forwardRef<HTMLDivElement, CardMetricProps>(
|
|
138
|
+
({ className, value, label, trend, trendValue, ...props }, ref) => (
|
|
139
|
+
<div ref={ref} className={cn("card-metric", className)} {...props}>
|
|
140
|
+
<span className="card-metric__value">{value}</span>
|
|
141
|
+
<span className="card-metric__label">{label}</span>
|
|
142
|
+
{trend && trendValue && (
|
|
143
|
+
<span className={cn("card-metric__trend", `card-metric__trend--${trend}`)}>
|
|
144
|
+
{trend === "up" ? "↑" : trend === "down" ? "↓" : "→"} {trendValue}
|
|
145
|
+
</span>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
),
|
|
149
|
+
);
|
|
150
|
+
CardMetric.displayName = "CardMetric";
|
|
151
|
+
|
|
74
152
|
interface CardWithIconProps {
|
|
75
153
|
icon?: React.ComponentType<React.SVGProps<SVGSVGElement> | { className?: string }>;
|
|
76
154
|
title: string;
|
|
@@ -78,22 +156,22 @@ interface CardWithIconProps {
|
|
|
78
156
|
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
|
79
157
|
}
|
|
80
158
|
|
|
81
|
-
const CardWithIcon = React.forwardRef<
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
)
|
|
159
|
+
const CardWithIcon = React.forwardRef<HTMLDivElement, CardWithIconProps>(
|
|
160
|
+
({ icon: Icon, title, description, onClick }, ref) => (
|
|
161
|
+
<Card
|
|
162
|
+
ref={ref}
|
|
163
|
+
variant="interactive"
|
|
164
|
+
className="card-with-icon"
|
|
165
|
+
onClick={onClick}
|
|
166
|
+
>
|
|
167
|
+
<div className="card-with-icon__header">
|
|
168
|
+
{Icon && <Icon className="card-with-icon__icon" />}
|
|
169
|
+
<Text as="h3" variant="overline-large">{title}</Text>
|
|
170
|
+
</div>
|
|
171
|
+
<Text as="p" variant="body-base">{description}</Text>
|
|
172
|
+
</Card>
|
|
173
|
+
),
|
|
174
|
+
);
|
|
97
175
|
CardWithIcon.displayName = "CardWithIcon";
|
|
98
176
|
|
|
99
177
|
export {
|
|
@@ -103,5 +181,9 @@ export {
|
|
|
103
181
|
CardTitle,
|
|
104
182
|
CardDescription,
|
|
105
183
|
CardContent,
|
|
184
|
+
CardMedia,
|
|
185
|
+
CardBadge,
|
|
186
|
+
CardActions,
|
|
187
|
+
CardMetric,
|
|
106
188
|
CardWithIcon,
|
|
107
189
|
};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { format } from "date-fns";
|
|
3
|
+
import { Calendar } from "@/components/ui/calendar";
|
|
4
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
export interface DatePickerProps {
|
|
8
|
+
value?: Date;
|
|
9
|
+
onChange?: (date: Date | undefined) => void;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
disabledDates?: (date: Date) => boolean;
|
|
13
|
+
fromDate?: Date;
|
|
14
|
+
toDate?: Date;
|
|
15
|
+
dateFormat?: string;
|
|
16
|
+
className?: string;
|
|
17
|
+
align?: "start" | "center" | "end";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
|
|
21
|
+
(
|
|
22
|
+
{
|
|
23
|
+
value,
|
|
24
|
+
onChange,
|
|
25
|
+
placeholder = "Select date",
|
|
26
|
+
disabled = false,
|
|
27
|
+
disabledDates,
|
|
28
|
+
fromDate,
|
|
29
|
+
toDate,
|
|
30
|
+
dateFormat = "MMM d, yyyy",
|
|
31
|
+
className,
|
|
32
|
+
align = "start",
|
|
33
|
+
},
|
|
34
|
+
ref,
|
|
35
|
+
) => {
|
|
36
|
+
const [open, setOpen] = React.useState(false);
|
|
37
|
+
|
|
38
|
+
const handleSelect = (date: Date | undefined) => {
|
|
39
|
+
onChange?.(date);
|
|
40
|
+
setOpen(false);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
45
|
+
<PopoverTrigger asChild>
|
|
46
|
+
<button
|
|
47
|
+
ref={ref}
|
|
48
|
+
type="button"
|
|
49
|
+
disabled={disabled}
|
|
50
|
+
aria-haspopup="dialog"
|
|
51
|
+
aria-expanded={open}
|
|
52
|
+
className={cn(
|
|
53
|
+
"date-picker-trigger",
|
|
54
|
+
!value && "date-picker-trigger--placeholder",
|
|
55
|
+
disabled && "date-picker-trigger--disabled",
|
|
56
|
+
className,
|
|
57
|
+
)}
|
|
58
|
+
>
|
|
59
|
+
<span className="date-picker-icon" aria-hidden="true">
|
|
60
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
61
|
+
<rect x="1" y="3" width="14" height="12" rx="2" stroke="currentColor" strokeWidth="1.5" />
|
|
62
|
+
<path d="M1 7h14" stroke="currentColor" strokeWidth="1.5" />
|
|
63
|
+
<path d="M5 1v4M11 1v4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
64
|
+
</svg>
|
|
65
|
+
</span>
|
|
66
|
+
<span className="date-picker-value">
|
|
67
|
+
{value ? format(value, dateFormat) : placeholder}
|
|
68
|
+
</span>
|
|
69
|
+
<span className="date-picker-chevron" aria-hidden="true">
|
|
70
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
|
71
|
+
<path d="M2 4l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
72
|
+
</svg>
|
|
73
|
+
</span>
|
|
74
|
+
</button>
|
|
75
|
+
</PopoverTrigger>
|
|
76
|
+
<PopoverContent
|
|
77
|
+
className="date-picker-popover"
|
|
78
|
+
align={align}
|
|
79
|
+
sideOffset={6}
|
|
80
|
+
>
|
|
81
|
+
<Calendar
|
|
82
|
+
mode="single"
|
|
83
|
+
selected={value}
|
|
84
|
+
onSelect={handleSelect}
|
|
85
|
+
disabled={disabledDates}
|
|
86
|
+
fromDate={fromDate}
|
|
87
|
+
toDate={toDate}
|
|
88
|
+
initialFocus
|
|
89
|
+
/>
|
|
90
|
+
</PopoverContent>
|
|
91
|
+
</Popover>
|
|
92
|
+
);
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
DatePicker.displayName = "DatePicker";
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// DateRangePicker
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
export interface DateRange {
|
|
102
|
+
from: Date | undefined;
|
|
103
|
+
to?: Date | undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface DateRangePickerProps {
|
|
107
|
+
value?: DateRange;
|
|
108
|
+
onChange?: (range: DateRange | undefined) => void;
|
|
109
|
+
placeholder?: string;
|
|
110
|
+
disabled?: boolean;
|
|
111
|
+
dateFormat?: string;
|
|
112
|
+
className?: string;
|
|
113
|
+
align?: "start" | "center" | "end";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePickerProps>(
|
|
117
|
+
(
|
|
118
|
+
{
|
|
119
|
+
value,
|
|
120
|
+
onChange,
|
|
121
|
+
placeholder = "Select date range",
|
|
122
|
+
disabled = false,
|
|
123
|
+
dateFormat = "MMM d, yyyy",
|
|
124
|
+
className,
|
|
125
|
+
align = "start",
|
|
126
|
+
},
|
|
127
|
+
ref,
|
|
128
|
+
) => {
|
|
129
|
+
const [open, setOpen] = React.useState(false);
|
|
130
|
+
|
|
131
|
+
const label = React.useMemo(() => {
|
|
132
|
+
if (!value?.from) return null;
|
|
133
|
+
if (!value.to) return format(value.from, dateFormat);
|
|
134
|
+
return `${format(value.from, dateFormat)} — ${format(value.to, dateFormat)}`;
|
|
135
|
+
}, [value, dateFormat]);
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
139
|
+
<PopoverTrigger asChild>
|
|
140
|
+
<button
|
|
141
|
+
ref={ref}
|
|
142
|
+
type="button"
|
|
143
|
+
disabled={disabled}
|
|
144
|
+
aria-haspopup="dialog"
|
|
145
|
+
aria-expanded={open}
|
|
146
|
+
className={cn(
|
|
147
|
+
"date-picker-trigger",
|
|
148
|
+
!label && "date-picker-trigger--placeholder",
|
|
149
|
+
disabled && "date-picker-trigger--disabled",
|
|
150
|
+
className,
|
|
151
|
+
)}
|
|
152
|
+
>
|
|
153
|
+
<span className="date-picker-icon" aria-hidden="true">
|
|
154
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
155
|
+
<rect x="1" y="3" width="14" height="12" rx="2" stroke="currentColor" strokeWidth="1.5" />
|
|
156
|
+
<path d="M1 7h14" stroke="currentColor" strokeWidth="1.5" />
|
|
157
|
+
<path d="M5 1v4M11 1v4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
158
|
+
</svg>
|
|
159
|
+
</span>
|
|
160
|
+
<span className="date-picker-value">{label ?? placeholder}</span>
|
|
161
|
+
<span className="date-picker-chevron" aria-hidden="true">
|
|
162
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
|
163
|
+
<path d="M2 4l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
164
|
+
</svg>
|
|
165
|
+
</span>
|
|
166
|
+
</button>
|
|
167
|
+
</PopoverTrigger>
|
|
168
|
+
<PopoverContent
|
|
169
|
+
className="date-picker-popover"
|
|
170
|
+
align={align}
|
|
171
|
+
sideOffset={6}
|
|
172
|
+
>
|
|
173
|
+
<Calendar
|
|
174
|
+
mode="range"
|
|
175
|
+
selected={value}
|
|
176
|
+
onSelect={onChange as (range: DateRange | undefined) => void}
|
|
177
|
+
numberOfMonths={2}
|
|
178
|
+
initialFocus
|
|
179
|
+
/>
|
|
180
|
+
</PopoverContent>
|
|
181
|
+
</Popover>
|
|
182
|
+
);
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
DateRangePicker.displayName = "DateRangePicker";
|
|
187
|
+
|
|
188
|
+
export { DatePicker, DateRangePicker };
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
// ─── EmptyState Root ──────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export interface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
/**
|
|
8
|
+
* Large display headline (e.g. "Whoops, 404" or "No Messages Yet").
|
|
9
|
+
* Rendered above the title. Ignored when `headline` slot is used.
|
|
10
|
+
*/
|
|
11
|
+
headline?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Main title below the headline.
|
|
14
|
+
* Ignored when `title` slot is used.
|
|
15
|
+
*/
|
|
16
|
+
title?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Subtitle rendered between title and body text.
|
|
19
|
+
* Ignored when `subtitle` slot is used.
|
|
20
|
+
*/
|
|
21
|
+
subtitle?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Body copy below the title.
|
|
24
|
+
* Ignored when `text` slot is used.
|
|
25
|
+
*/
|
|
26
|
+
text?: string;
|
|
27
|
+
/**
|
|
28
|
+
* URL for an illustration / image rendered above the headline.
|
|
29
|
+
* Ignored when `media` slot is used.
|
|
30
|
+
*/
|
|
31
|
+
image?: string;
|
|
32
|
+
/** Alt text for the image */
|
|
33
|
+
imageAlt?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Icon rendered in the media area when no `image` is provided.
|
|
36
|
+
* Pass any React node (SVG, lucide icon, etc.).
|
|
37
|
+
* Ignored when `media` slot is used.
|
|
38
|
+
*/
|
|
39
|
+
icon?: React.ReactNode;
|
|
40
|
+
/**
|
|
41
|
+
* Label for the primary action button.
|
|
42
|
+
* When provided an `<EmptyStateAction>` button is rendered automatically.
|
|
43
|
+
* Ignored when `actions` slot is used.
|
|
44
|
+
*/
|
|
45
|
+
actionText?: string;
|
|
46
|
+
/** Fires when the auto-rendered action button is clicked */
|
|
47
|
+
onClickAction?: () => void;
|
|
48
|
+
|
|
49
|
+
// ── Slot overrides ──────────────────────────────────────────────────────
|
|
50
|
+
/** Replaces the media area (image / icon) */
|
|
51
|
+
mediaSlot?: React.ReactNode;
|
|
52
|
+
/** Replaces the headline */
|
|
53
|
+
headlineSlot?: React.ReactNode;
|
|
54
|
+
/** Replaces the title */
|
|
55
|
+
titleSlot?: React.ReactNode;
|
|
56
|
+
/** Replaces the subtitle */
|
|
57
|
+
subtitleSlot?: React.ReactNode;
|
|
58
|
+
/** Replaces the body text */
|
|
59
|
+
textSlot?: React.ReactNode;
|
|
60
|
+
/** Replaces the actions area */
|
|
61
|
+
actionsSlot?: React.ReactNode;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(
|
|
65
|
+
(
|
|
66
|
+
{
|
|
67
|
+
className,
|
|
68
|
+
children,
|
|
69
|
+
headline,
|
|
70
|
+
title,
|
|
71
|
+
subtitle,
|
|
72
|
+
text,
|
|
73
|
+
image,
|
|
74
|
+
imageAlt = "",
|
|
75
|
+
icon,
|
|
76
|
+
actionText,
|
|
77
|
+
onClickAction,
|
|
78
|
+
mediaSlot,
|
|
79
|
+
headlineSlot,
|
|
80
|
+
titleSlot,
|
|
81
|
+
subtitleSlot,
|
|
82
|
+
textSlot,
|
|
83
|
+
actionsSlot,
|
|
84
|
+
...props
|
|
85
|
+
},
|
|
86
|
+
ref
|
|
87
|
+
) => {
|
|
88
|
+
const hasMedia = mediaSlot || image || icon;
|
|
89
|
+
const hasHeadline = headlineSlot || headline;
|
|
90
|
+
const hasTitle = titleSlot || title;
|
|
91
|
+
const hasSubtitle = subtitleSlot || subtitle;
|
|
92
|
+
const hasText = textSlot || text;
|
|
93
|
+
const hasActions = actionsSlot || actionText;
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div
|
|
97
|
+
ref={ref}
|
|
98
|
+
className={cn("empty-state", className)}
|
|
99
|
+
{...props}
|
|
100
|
+
>
|
|
101
|
+
{/* Media: image or icon */}
|
|
102
|
+
{hasMedia && (
|
|
103
|
+
<div className="empty-state-media">
|
|
104
|
+
{mediaSlot ?? (
|
|
105
|
+
image ? (
|
|
106
|
+
<img
|
|
107
|
+
src={image}
|
|
108
|
+
alt={imageAlt}
|
|
109
|
+
className="empty-state-image"
|
|
110
|
+
/>
|
|
111
|
+
) : (
|
|
112
|
+
<span className="empty-state-icon">{icon}</span>
|
|
113
|
+
)
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{/* Headline */}
|
|
119
|
+
{hasHeadline && (
|
|
120
|
+
<div className="empty-state-headline">
|
|
121
|
+
{headlineSlot ?? headline}
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
|
|
125
|
+
{/* Title */}
|
|
126
|
+
{hasTitle && (
|
|
127
|
+
<div className="empty-state-title">
|
|
128
|
+
{titleSlot ?? title}
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
{/* Subtitle */}
|
|
133
|
+
{hasSubtitle && (
|
|
134
|
+
<div className="empty-state-subtitle">
|
|
135
|
+
{subtitleSlot ?? subtitle}
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
{/* Body text */}
|
|
140
|
+
{hasText && (
|
|
141
|
+
<div className="empty-state-text">
|
|
142
|
+
{textSlot ?? text}
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
|
+
|
|
146
|
+
{/* Default slot (custom content — e.g. a card grid) */}
|
|
147
|
+
{children && (
|
|
148
|
+
<div className="empty-state-default">{children}</div>
|
|
149
|
+
)}
|
|
150
|
+
|
|
151
|
+
{/* Actions */}
|
|
152
|
+
{hasActions && (
|
|
153
|
+
<div className="empty-state-actions">
|
|
154
|
+
{actionsSlot ?? (
|
|
155
|
+
<EmptyStateAction onClick={onClickAction}>
|
|
156
|
+
{actionText}
|
|
157
|
+
</EmptyStateAction>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
EmptyState.displayName = "EmptyState";
|
|
166
|
+
|
|
167
|
+
// ─── EmptyStateAction ─────────────────────────────────────────────────────────
|
|
168
|
+
// Pre-styled CTA button. Use inside `actionsSlot` for custom actions,
|
|
169
|
+
// or let `EmptyState` auto-render one via the `actionText` prop.
|
|
170
|
+
|
|
171
|
+
export interface EmptyStateActionProps
|
|
172
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
173
|
+
/** Visual variant */
|
|
174
|
+
variant?: "default" | "primary" | "outline";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const EmptyStateAction = React.forwardRef<
|
|
178
|
+
HTMLButtonElement,
|
|
179
|
+
EmptyStateActionProps
|
|
180
|
+
>(({ className, variant = "default", ...props }, ref) => (
|
|
181
|
+
<button
|
|
182
|
+
ref={ref}
|
|
183
|
+
type="button"
|
|
184
|
+
className={cn(
|
|
185
|
+
"empty-state-action-btn",
|
|
186
|
+
variant === "primary" && "empty-state-action-btn--primary",
|
|
187
|
+
variant === "outline" && "empty-state-action-btn--outline",
|
|
188
|
+
className
|
|
189
|
+
)}
|
|
190
|
+
{...props}
|
|
191
|
+
/>
|
|
192
|
+
));
|
|
193
|
+
EmptyStateAction.displayName = "EmptyStateAction";
|
|
194
|
+
|
|
195
|
+
// ─── Exports ─────────────────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
export { EmptyState, EmptyStateAction };
|