@syscore/ui-library 1.27.3 → 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.
@@ -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 Card = React.forwardRef<
7
- HTMLDivElement,
8
- React.HTMLAttributes<HTMLDivElement>
9
- >(({ className, ...props }, ref) => (
10
- <div
11
- ref={ref}
12
- className={cn("card", className)}
13
- {...props}
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
- HTMLDivElement,
83
- CardWithIconProps
84
- >(({ icon: Icon, title, description, onClick }, ref) => (
85
- <Card
86
- ref={ref}
87
- className="card-with-icon"
88
- onClick={onClick}
89
- >
90
- <div className="card-with-icon__header">
91
- {Icon && <Icon className="card-with-icon__icon" />}
92
- <Text as="h3" variant="overline-large">{title}</Text>
93
- </div>
94
- <Text as="p" variant="body-base">{description}</Text>
95
- </Card>
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 };