@djangocfg/ui-core 2.1.411 → 2.1.413

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 (52) hide show
  1. package/package.json +4 -4
  2. package/src/components/data/avatar-group/index.tsx +224 -0
  3. package/src/components/data/badge-overflow/index.tsx +259 -0
  4. package/src/components/data/circular-progress/index.tsx +358 -0
  5. package/src/components/data/relative-time-card/index.tsx +191 -0
  6. package/src/components/data/stat/index.tsx +140 -0
  7. package/src/components/data/status/index.tsx +80 -0
  8. package/src/components/effects/GlowBackground.tsx +9 -1
  9. package/src/components/effects/swap/index.tsx +289 -0
  10. package/src/components/feedback/banner/index.tsx +693 -0
  11. package/src/components/forms/checkbox-group/index.tsx +243 -0
  12. package/src/components/forms/editable/index.tsx +420 -0
  13. package/src/components/forms/input-otp/index.tsx +12 -3
  14. package/src/components/forms/mask-input/index.tsx +466 -0
  15. package/src/components/forms/otp/index.tsx +12 -8
  16. package/src/components/forms/segmented-input/index.tsx +319 -0
  17. package/src/components/forms/tags-input/index.tsx +896 -0
  18. package/src/components/forms/time-picker/index.tsx +285 -0
  19. package/src/components/index.ts +51 -0
  20. package/src/components/layout/key-value/index.tsx +884 -0
  21. package/src/components/layout/stack/index.tsx +349 -0
  22. package/src/components/navigation/context-menu/index.tsx +9 -6
  23. package/src/components/navigation/stepper/index.tsx +1307 -0
  24. package/src/components/select/multi-select-pro-async.tsx +11 -2
  25. package/src/components/select/multi-select-pro.tsx +11 -2
  26. package/src/components/select/select.tsx +13 -3
  27. package/src/components/specialized/presence/index.tsx +181 -0
  28. package/src/components/specialized/primitive/index.tsx +83 -0
  29. package/src/components/specialized/visually-hidden/index.tsx +19 -0
  30. package/src/components/specialized/visually-hidden-input/index.tsx +99 -0
  31. package/src/hooks/dom/index.ts +4 -0
  32. package/src/hooks/dom/useFormReset.ts +49 -0
  33. package/src/hooks/dom/useLayoutEffect.ts +16 -0
  34. package/src/hooks/dom/useSize.ts +57 -0
  35. package/src/hooks/state/index.ts +4 -0
  36. package/src/hooks/state/useCallbackRef.ts +25 -0
  37. package/src/hooks/state/usePrevious.ts +20 -0
  38. package/src/hooks/state/useStateMachine.ts +29 -0
  39. package/src/lib/compose-event-handlers.ts +22 -0
  40. package/src/lib/compose-refs.ts +65 -0
  41. package/src/lib/create-context.tsx +62 -0
  42. package/src/lib/get-element-ref.ts +33 -0
  43. package/src/lib/index.ts +5 -0
  44. package/src/lib/styles.ts +103 -0
  45. package/src/styles/README.md +43 -0
  46. package/src/styles/palette/utils.ts +15 -5
  47. package/src/styles/utilities/animations.css +135 -0
  48. package/src/styles/utilities/display.css +62 -0
  49. package/src/styles/utilities/glass.css +57 -0
  50. package/src/styles/utilities/marquee.css +69 -0
  51. package/src/styles/utilities/step.css +25 -0
  52. package/src/styles/utilities.css +6 -259
@@ -0,0 +1,358 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Slot } from "@radix-ui/react-slot"
5
+
6
+ import { cn } from "../../../lib/utils"
7
+
8
+ const CIRCULAR_PROGRESS_NAME = "CircularProgress"
9
+ const INDICATOR_NAME = "CircularProgressIndicator"
10
+ const TRACK_NAME = "CircularProgressTrack"
11
+ const RANGE_NAME = "CircularProgressRange"
12
+ const VALUE_TEXT_NAME = "CircularProgressValueText"
13
+
14
+ const DEFAULT_MAX = 100
15
+
16
+ type ProgressState = "indeterminate" | "complete" | "loading"
17
+
18
+ function getProgressState(
19
+ value: number | undefined | null,
20
+ maxValue: number,
21
+ ): ProgressState {
22
+ return value == null
23
+ ? "indeterminate"
24
+ : value === maxValue
25
+ ? "complete"
26
+ : "loading"
27
+ }
28
+
29
+ function getIsValidNumber(value: unknown): value is number {
30
+ return typeof value === "number" && Number.isFinite(value)
31
+ }
32
+
33
+ function getIsValidMaxNumber(max: unknown): max is number {
34
+ return getIsValidNumber(max) && max > 0
35
+ }
36
+
37
+ function getIsValidValueNumber(
38
+ value: unknown,
39
+ min: number,
40
+ max: number,
41
+ ): value is number {
42
+ return getIsValidNumber(value) && value <= max && value >= min
43
+ }
44
+
45
+ function getDefaultValueText(value: number, min: number, max: number): string {
46
+ const percentage = max === min ? 100 : ((value - min) / (max - min)) * 100
47
+ return `${Math.round(percentage)}%`
48
+ }
49
+
50
+ function getInvalidValueError(
51
+ propValue: string,
52
+ componentName: string,
53
+ ): string {
54
+ return `Invalid prop \`value\` of value \`${propValue}\` supplied to \`${componentName}\`. The \`value\` prop must be a number between \`min\` and \`max\` (inclusive), or \`null\`/\`undefined\` for indeterminate progress. The value will be clamped to the valid range.`
55
+ }
56
+
57
+ function getInvalidMaxError(propValue: string, componentName: string): string {
58
+ return `Invalid prop \`max\` of value \`${propValue}\` supplied to \`${componentName}\`. Only numbers greater than 0 are valid. Defaulting to ${DEFAULT_MAX}.`
59
+ }
60
+
61
+ interface CircularProgressContextValue {
62
+ value: number | null
63
+ valueText: string | undefined
64
+ max: number
65
+ min: number
66
+ state: ProgressState
67
+ radius: number
68
+ thickness: number
69
+ size: number
70
+ center: number
71
+ circumference: number
72
+ percentage: number | null
73
+ valueTextId?: string
74
+ }
75
+
76
+ const CircularProgressContext =
77
+ React.createContext<CircularProgressContextValue | null>(null)
78
+
79
+ function useCircularProgressContext(consumerName: string) {
80
+ const context = React.useContext(CircularProgressContext)
81
+ if (!context) {
82
+ throw new Error(
83
+ `\`${consumerName}\` must be used within \`${CIRCULAR_PROGRESS_NAME}\``,
84
+ )
85
+ }
86
+ return context
87
+ }
88
+
89
+ interface CircularProgressProps extends React.ComponentProps<"div"> {
90
+ value?: number | null | undefined
91
+ getValueText?(value: number, min: number, max: number): string
92
+ min?: number
93
+ max?: number
94
+ size?: number
95
+ thickness?: number
96
+ label?: string
97
+ asChild?: boolean
98
+ }
99
+
100
+ function CircularProgress(props: CircularProgressProps) {
101
+ const {
102
+ value: valueProp = null,
103
+ getValueText = getDefaultValueText,
104
+ min: minProp = 0,
105
+ max: maxProp,
106
+ size = 48,
107
+ thickness = 4,
108
+ label,
109
+ asChild,
110
+ className,
111
+ children,
112
+ ...rootProps
113
+ } = props
114
+
115
+ if ((maxProp || maxProp === 0) && !getIsValidMaxNumber(maxProp)) {
116
+ if (process.env.NODE_ENV !== "production") {
117
+ console.error(getInvalidMaxError(`${maxProp}`, CIRCULAR_PROGRESS_NAME))
118
+ }
119
+ }
120
+
121
+ const rawMax = getIsValidMaxNumber(maxProp) ? maxProp : DEFAULT_MAX
122
+ const min = getIsValidNumber(minProp) ? minProp : 0
123
+ const max = rawMax <= min ? min + 1 : rawMax
124
+
125
+ if (process.env.NODE_ENV !== "production" && thickness >= size) {
126
+ console.warn(
127
+ `CircularProgress: thickness (${thickness}) should be less than size (${size}) for proper rendering.`,
128
+ )
129
+ }
130
+
131
+ if (valueProp !== null && !getIsValidValueNumber(valueProp, min, max)) {
132
+ if (process.env.NODE_ENV !== "production") {
133
+ console.error(
134
+ getInvalidValueError(`${valueProp}`, CIRCULAR_PROGRESS_NAME),
135
+ )
136
+ }
137
+ }
138
+
139
+ const value = getIsValidValueNumber(valueProp, min, max)
140
+ ? valueProp
141
+ : getIsValidNumber(valueProp) && valueProp > max
142
+ ? max
143
+ : getIsValidNumber(valueProp) && valueProp < min
144
+ ? min
145
+ : null
146
+
147
+ const valueText = getIsValidNumber(value)
148
+ ? getValueText(value, min, max)
149
+ : undefined
150
+ const state = getProgressState(value, max)
151
+ const radius = Math.max(0, (size - thickness) / 2)
152
+ const center = size / 2
153
+ const circumference = 2 * Math.PI * radius
154
+
155
+ const percentage = getIsValidNumber(value)
156
+ ? max === min
157
+ ? 1
158
+ : (value - min) / (max - min)
159
+ : null
160
+
161
+ const labelId = React.useId()
162
+ const valueTextId = React.useId()
163
+
164
+ const contextValue = React.useMemo<CircularProgressContextValue>(
165
+ () => ({
166
+ value,
167
+ valueText,
168
+ max,
169
+ min,
170
+ state,
171
+ radius,
172
+ thickness,
173
+ size,
174
+ center,
175
+ circumference,
176
+ percentage,
177
+ valueTextId,
178
+ }),
179
+ [
180
+ value,
181
+ valueText,
182
+ max,
183
+ min,
184
+ state,
185
+ radius,
186
+ thickness,
187
+ size,
188
+ center,
189
+ circumference,
190
+ percentage,
191
+ valueTextId,
192
+ ],
193
+ )
194
+
195
+ const RootPrimitive = asChild ? Slot : "div"
196
+
197
+ return (
198
+ <CircularProgressContext.Provider value={contextValue}>
199
+ <RootPrimitive
200
+ role="progressbar"
201
+ aria-describedby={valueText ? valueTextId : undefined}
202
+ aria-labelledby={label ? labelId : undefined}
203
+ aria-valuemax={max}
204
+ aria-valuemin={min}
205
+ aria-valuenow={getIsValidNumber(value) ? value : undefined}
206
+ aria-valuetext={valueText}
207
+ data-state={state}
208
+ data-value={value ?? undefined}
209
+ data-max={max}
210
+ data-min={min}
211
+ data-percentage={percentage}
212
+ {...rootProps}
213
+ className={cn(
214
+ "relative inline-flex w-fit items-center justify-center",
215
+ className,
216
+ )}
217
+ >
218
+ {children}
219
+ {label && <div id={labelId}>{label}</div>}
220
+ </RootPrimitive>
221
+ </CircularProgressContext.Provider>
222
+ )
223
+ }
224
+
225
+ function CircularProgressIndicator(props: React.ComponentProps<"svg">) {
226
+ const { className, ...indicatorProps } = props
227
+
228
+ const context = useCircularProgressContext(INDICATOR_NAME)
229
+
230
+ return (
231
+ <svg
232
+ aria-hidden="true"
233
+ focusable="false"
234
+ viewBox={`0 0 ${context.size} ${context.size}`}
235
+ data-state={context.state}
236
+ data-value={context.value ?? undefined}
237
+ data-max={context.max}
238
+ data-min={context.min}
239
+ data-percentage={context.percentage}
240
+ width={context.size}
241
+ height={context.size}
242
+ {...indicatorProps}
243
+ className={cn("-rotate-90 transform", className)}
244
+ />
245
+ )
246
+ }
247
+
248
+ CircularProgressIndicator.displayName = INDICATOR_NAME
249
+
250
+ function CircularProgressTrack(props: React.ComponentProps<"circle">) {
251
+ const { className, ...trackProps } = props
252
+
253
+ const context = useCircularProgressContext(TRACK_NAME)
254
+
255
+ return (
256
+ <circle
257
+ data-state={context.state}
258
+ cx={context.center}
259
+ cy={context.center}
260
+ r={context.radius}
261
+ fill="none"
262
+ stroke="currentColor"
263
+ strokeWidth={context.thickness}
264
+ strokeLinecap="round"
265
+ vectorEffect="non-scaling-stroke"
266
+ {...trackProps}
267
+ className={cn("text-muted-foreground/20", className)}
268
+ />
269
+ )
270
+ }
271
+
272
+ function CircularProgressRange(props: React.ComponentProps<"circle">) {
273
+ const { className, ...rangeProps } = props
274
+
275
+ const context = useCircularProgressContext(RANGE_NAME)
276
+
277
+ const strokeDasharray = context.circumference
278
+ const strokeDashoffset =
279
+ context.state === "indeterminate"
280
+ ? context.circumference * 0.75
281
+ : context.percentage !== null
282
+ ? context.circumference - context.percentage * context.circumference
283
+ : context.circumference
284
+
285
+ return (
286
+ <circle
287
+ data-state={context.state}
288
+ data-value={context.value ?? undefined}
289
+ data-max={context.max}
290
+ data-min={context.min}
291
+ cx={context.center}
292
+ cy={context.center}
293
+ r={context.radius}
294
+ fill="none"
295
+ stroke="currentColor"
296
+ strokeWidth={context.thickness}
297
+ strokeLinecap="round"
298
+ strokeDasharray={strokeDasharray}
299
+ strokeDashoffset={strokeDashoffset}
300
+ vectorEffect="non-scaling-stroke"
301
+ {...rangeProps}
302
+ className={cn(
303
+ "origin-center text-primary transition-all duration-300 ease-in-out",
304
+ context.state === "indeterminate" &&
305
+ "motion-reduce:animate-none motion-safe:[animation:var(--animate-spin-around)]",
306
+ className,
307
+ )}
308
+ />
309
+ )
310
+ }
311
+
312
+ interface CircularProgressValueTextProps extends React.ComponentProps<"span"> {
313
+ asChild?: boolean
314
+ }
315
+
316
+ function CircularProgressValueText(props: CircularProgressValueTextProps) {
317
+ const { asChild, className, children, ...valueTextProps } = props
318
+
319
+ const context = useCircularProgressContext(VALUE_TEXT_NAME)
320
+
321
+ const ValueTextPrimitive = asChild ? Slot : "span"
322
+
323
+ return (
324
+ <ValueTextPrimitive
325
+ id={context.valueTextId}
326
+ data-state={context.state}
327
+ {...valueTextProps}
328
+ className={cn(
329
+ "absolute inset-0 flex items-center justify-center font-medium text-sm",
330
+ className,
331
+ )}
332
+ >
333
+ {children ?? context.valueText}
334
+ </ValueTextPrimitive>
335
+ )
336
+ }
337
+
338
+ function CircularProgressCombined(props: CircularProgressProps) {
339
+ return (
340
+ <CircularProgress {...props}>
341
+ <CircularProgressIndicator>
342
+ <CircularProgressTrack />
343
+ <CircularProgressRange />
344
+ </CircularProgressIndicator>
345
+ <CircularProgressValueText />
346
+ </CircularProgress>
347
+ )
348
+ }
349
+
350
+ export {
351
+ CircularProgress,
352
+ CircularProgressCombined,
353
+ CircularProgressIndicator,
354
+ CircularProgressRange,
355
+ CircularProgressTrack,
356
+ CircularProgressValueText,
357
+ }
358
+ export type { CircularProgressProps }
@@ -0,0 +1,191 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import moment from "moment";
5
+ import { cva, type VariantProps } from "class-variance-authority";
6
+ import { cn } from "../../../lib/utils";
7
+
8
+ const relativeTimeCardVariants = cva(
9
+ "inline-flex items-center gap-2 rounded-md px-2.5 py-1 text-sm font-medium",
10
+ {
11
+ variants: {
12
+ variant: {
13
+ default: "bg-muted text-muted-foreground",
14
+ subtle: "bg-transparent text-muted-foreground",
15
+ outline: "border border-border text-foreground",
16
+ accent: "bg-accent text-accent-foreground",
17
+ },
18
+ size: {
19
+ default: "text-sm px-2.5 py-1",
20
+ sm: "text-xs px-2 py-0.5",
21
+ lg: "text-base px-3 py-1.5",
22
+ },
23
+ },
24
+ defaultVariants: {
25
+ variant: "default",
26
+ size: "default",
27
+ },
28
+ }
29
+ );
30
+
31
+ interface RelativeTimeCardProps
32
+ extends React.HTMLAttributes<HTMLSpanElement>,
33
+ VariantProps<typeof relativeTimeCardVariants> {
34
+ /** Date value — accepts Date, string in any format, number (timestamp), or Moment */
35
+ date: Date | string | number | moment.Moment;
36
+ /** Input format hint if date is a string (e.g. "DD.MM.YYYY HH:mm"). Auto-detected if omitted. */
37
+ inputFormat?: string;
38
+ /** Display format: relative | auto | datetime | custom */
39
+ format?: "relative" | "auto" | "datetime" | "custom";
40
+ /** Custom output format string (moment format). Used when format="custom" */
41
+ outputFormat?: string;
42
+ /** Update interval in ms for relative format. @default 60000 */
43
+ updateInterval?: number;
44
+ /** Text before the time */
45
+ prefix?: string;
46
+ /** Text after the time */
47
+ suffix?: string;
48
+ /** Locale for display. @default "en" */
49
+ locale?: string;
50
+ /** Use UTC for all calculations. @default true */
51
+ utc?: boolean;
52
+ }
53
+
54
+ function parseDate(
55
+ value: Date | string | number | moment.Moment,
56
+ inputFormat?: string,
57
+ useUtc: boolean = true
58
+ ): moment.Moment {
59
+ if (moment.isMoment(value)) {
60
+ return useUtc ? value.utc() : value.local();
61
+ }
62
+
63
+ const m = useUtc ? moment.utc : moment;
64
+
65
+ if (value instanceof Date) {
66
+ return m(value);
67
+ }
68
+
69
+ if (typeof value === "number") {
70
+ return m(value);
71
+ }
72
+
73
+ if (typeof value === "string") {
74
+ if (inputFormat) {
75
+ return m(value, inputFormat, true);
76
+ }
77
+ return m(value);
78
+ }
79
+
80
+ return m();
81
+ }
82
+
83
+ function getRelativeTime(date: moment.Moment, locale: string = "en"): string {
84
+ moment.locale(locale);
85
+ return date.fromNow();
86
+ }
87
+
88
+ function getAutoFormat(date: moment.Moment, locale: string = "en"): string {
89
+ moment.locale(locale);
90
+ const now = moment();
91
+ const diffHours = now.diff(date, "hours");
92
+ const diffDays = now.diff(date, "days");
93
+
94
+ if (diffHours < 24) {
95
+ return date.format("HH:mm");
96
+ }
97
+ if (diffDays < 7) {
98
+ return date.format("ddd");
99
+ }
100
+ if (diffDays < 365) {
101
+ return date.format("MMM D");
102
+ }
103
+ return date.format("MMM D, YYYY");
104
+ }
105
+
106
+ function getDateTimeFormat(date: moment.Moment, locale: string = "en"): string {
107
+ moment.locale(locale);
108
+ return date.format("MMM D, YYYY HH:mm");
109
+ }
110
+
111
+ function formatDate(
112
+ date: moment.Moment,
113
+ format: "relative" | "auto" | "datetime" | "custom",
114
+ locale: string,
115
+ outputFormat?: string
116
+ ): string {
117
+ switch (format) {
118
+ case "relative":
119
+ return getRelativeTime(date, locale);
120
+ case "auto":
121
+ return getAutoFormat(date, locale);
122
+ case "datetime":
123
+ return getDateTimeFormat(date, locale);
124
+ case "custom":
125
+ moment.locale(locale);
126
+ return date.format(outputFormat || "YYYY-MM-DD HH:mm");
127
+ }
128
+ }
129
+
130
+ const RelativeTimeCard = React.forwardRef<
131
+ HTMLTimeElement,
132
+ RelativeTimeCardProps
133
+ >(
134
+ (
135
+ {
136
+ date,
137
+ inputFormat,
138
+ format = "relative",
139
+ outputFormat,
140
+ updateInterval = 60000,
141
+ prefix,
142
+ suffix,
143
+ locale = "en",
144
+ utc = true,
145
+ className,
146
+ variant,
147
+ size,
148
+ ...props
149
+ },
150
+ ref
151
+ ) => {
152
+ const dateObj = React.useMemo(
153
+ () => parseDate(date, inputFormat, utc),
154
+ [date, inputFormat, utc]
155
+ );
156
+
157
+ const [display, setDisplay] = React.useState(() =>
158
+ formatDate(dateObj, format, locale, outputFormat)
159
+ );
160
+
161
+ React.useEffect(() => {
162
+ setDisplay(formatDate(dateObj, format, locale, outputFormat));
163
+ if (format !== "relative") return;
164
+ const id = setInterval(() => {
165
+ setDisplay(formatDate(dateObj, format, locale, outputFormat));
166
+ }, updateInterval);
167
+ return () => clearInterval(id);
168
+ }, [dateObj, format, locale, outputFormat, updateInterval]);
169
+
170
+ const isoString = dateObj.toISOString();
171
+ const title = dateObj.format("LLLL");
172
+
173
+ return (
174
+ <time
175
+ ref={ref}
176
+ dateTime={isoString}
177
+ className={cn(relativeTimeCardVariants({ variant, size }), className)}
178
+ title={title}
179
+ {...props}
180
+ >
181
+ {prefix}
182
+ {display}
183
+ {suffix}
184
+ </time>
185
+ );
186
+ }
187
+ );
188
+ RelativeTimeCard.displayName = "RelativeTimeCard";
189
+
190
+ export { RelativeTimeCard, relativeTimeCardVariants };
191
+ export type { RelativeTimeCardProps };
@@ -0,0 +1,140 @@
1
+ import { cva, type VariantProps } from "class-variance-authority"
2
+ import * as React from "react"
3
+
4
+ import { cn } from "../../../lib/utils"
5
+ import { Separator } from "../../layout/separator"
6
+
7
+ function Stat({ className, ...props }: React.ComponentProps<"div">) {
8
+ return (
9
+ <div
10
+ data-slot="stat"
11
+ className={cn(
12
+ "grid grid-cols-[1fr_auto] gap-x-4 gap-y-1 rounded-lg border bg-card p-4 text-card-foreground shadow-sm",
13
+ "**:data-[slot=stat-label]:col-span-1 **:data-[slot=stat-value]:col-span-1",
14
+ "**:data-[slot=stat-indicator]:col-start-2 **:data-[slot=stat-indicator]:row-span-2 **:data-[slot=stat-indicator]:row-start-1 **:data-[slot=stat-indicator]:self-start",
15
+ "**:data-[slot=stat-description]:col-span-2 **:data-[slot=stat-separator]:col-span-2 **:data-[slot=stat-trend]:col-span-2",
16
+ className,
17
+ )}
18
+ {...props}
19
+ />
20
+ )
21
+ }
22
+
23
+ function StatLabel({ className, ...props }: React.ComponentProps<"div">) {
24
+ return (
25
+ <div
26
+ data-slot="stat-label"
27
+ className={cn("font-medium text-muted-foreground text-sm", className)}
28
+ {...props}
29
+ />
30
+ )
31
+ }
32
+
33
+ const statIndicatorVariants = cva(
34
+ "flex shrink-0 items-center justify-center [&_svg]:pointer-events-none",
35
+ {
36
+ variants: {
37
+ variant: {
38
+ default: "text-muted-foreground [&_svg:not([class*='size-'])]:size-5",
39
+ icon: "size-8 rounded-md border [&_svg:not([class*='size-'])]:size-3.5",
40
+ badge:
41
+ "h-6 min-w-6 rounded-sm border px-1.5 font-medium text-xs [&_svg:not([class*='size-'])]:size-3",
42
+ action:
43
+ "size-8 cursor-pointer rounded-md transition-colors hover:bg-muted/50 [&_svg:not([class*='size-'])]:size-4",
44
+ },
45
+ color: {
46
+ default: "bg-muted text-muted-foreground",
47
+ success:
48
+ "border-green-500/20 bg-green-500/10 text-green-600 dark:text-green-400",
49
+ info: "border-blue-500/20 bg-blue-500/10 text-blue-600 dark:text-blue-400",
50
+ warning:
51
+ "border-orange-500/20 bg-orange-500/10 text-orange-600 dark:text-orange-400",
52
+ error: "border-destructive/20 bg-destructive/10 text-destructive",
53
+ },
54
+ },
55
+ defaultVariants: {
56
+ variant: "default",
57
+ color: "default",
58
+ },
59
+ },
60
+ )
61
+
62
+ interface StatIndicatorProps
63
+ extends Omit<React.ComponentProps<"div">, "color">,
64
+ VariantProps<typeof statIndicatorVariants> {}
65
+
66
+ function StatIndicator({
67
+ className,
68
+ variant = "default",
69
+ color = "default",
70
+ ...props
71
+ }: StatIndicatorProps) {
72
+ return (
73
+ <div
74
+ data-slot="stat-indicator"
75
+ data-variant={variant}
76
+ data-color={color}
77
+ className={cn(statIndicatorVariants({ variant, color, className }))}
78
+ {...props}
79
+ />
80
+ )
81
+ }
82
+
83
+ function StatValue({ className, ...props }: React.ComponentProps<"div">) {
84
+ return (
85
+ <div
86
+ data-slot="stat-value"
87
+ className={cn("font-semibold text-2xl tracking-tight", className)}
88
+ {...props}
89
+ />
90
+ )
91
+ }
92
+
93
+ function StatTrend({
94
+ className,
95
+ trend,
96
+ ...props
97
+ }: React.ComponentProps<"div"> & { trend?: "up" | "down" | "neutral" }) {
98
+ return (
99
+ <div
100
+ data-slot="stat-trend"
101
+ data-trend={trend}
102
+ className={cn(
103
+ "inline-flex items-center gap-1 font-medium text-xs [&_svg:not([class*='size-'])]:size-3 [&_svg]:pointer-events-none [&_svg]:shrink-0",
104
+ {
105
+ "text-green-600 dark:text-green-400": trend === "up",
106
+ "text-red-600 dark:text-red-400": trend === "down",
107
+ "text-muted-foreground": trend === "neutral" || !trend,
108
+ },
109
+ className,
110
+ )}
111
+ {...props}
112
+ />
113
+ )
114
+ }
115
+
116
+ function StatSeparator({ ...props }: React.ComponentProps<typeof Separator>) {
117
+ return <Separator data-slot="stat-separator" className="my-2" {...props} />
118
+ }
119
+
120
+ function StatDescription({ className, ...props }: React.ComponentProps<"div">) {
121
+ return (
122
+ <div
123
+ data-slot="stat-description"
124
+ className={cn("text-muted-foreground text-xs", className)}
125
+ {...props}
126
+ />
127
+ )
128
+ }
129
+
130
+ export {
131
+ Stat,
132
+ StatDescription,
133
+ StatIndicator,
134
+ StatLabel,
135
+ StatSeparator,
136
+ StatTrend,
137
+ StatValue,
138
+ statIndicatorVariants,
139
+ }
140
+ export type { StatIndicatorProps }