@mbao01/common 0.9.0 → 0.9.2
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/types/components/IconContainer/IconContainer.d.ts +2 -0
- package/dist/types/components/IconContainer/constants.d.ts +15 -0
- package/dist/types/components/IconContainer/index.d.ts +1 -0
- package/dist/types/components/IconContainer/types.d.ts +3 -0
- package/dist/types/components/KPICard/KPICard.d.ts +1 -1
- package/dist/types/components/KPICard/KPICardSkeleton.d.ts +11 -0
- package/dist/types/components/KPICard/index.d.ts +2 -0
- package/dist/types/components/KPICard/types.d.ts +2 -8
- package/dist/types/components/WidgetShell/WidgetShell.d.ts +20 -1
- package/dist/types/components/WidgetShell/WidgetShellSkeleton.d.ts +11 -0
- package/dist/types/components/WidgetShell/index.d.ts +3 -1
- package/dist/types/components/WidgetShell/types.d.ts +2 -11
- package/dist/types/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/components/IconContainer/IconContainer.tsx +44 -0
- package/src/components/IconContainer/constants.ts +112 -0
- package/src/components/IconContainer/index.ts +1 -0
- package/src/components/IconContainer/types.ts +5 -0
- package/src/components/KPICard/KPICard.tsx +9 -45
- package/src/components/KPICard/KPICardSkeleton.tsx +42 -0
- package/src/components/KPICard/index.ts +2 -0
- package/src/components/KPICard/types.ts +2 -8
- package/src/components/WidgetShell/WidgetShell.tsx +72 -70
- package/src/components/WidgetShell/WidgetShellSkeleton.tsx +47 -0
- package/src/components/WidgetShell/index.ts +3 -1
- package/src/components/WidgetShell/types.ts +2 -12
- package/src/index.ts +1 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const getIconRaisedContainerClasses: (props?: ({
|
|
2
|
+
variant?: "accent" | "default" | "error" | "ghost" | "info" | "neutral" | "primary" | "secondary" | "success" | "warning" | null | undefined;
|
|
3
|
+
size?: "xs" | "sm" | "md" | "lg" | "xl" | null | undefined;
|
|
4
|
+
shadow?: "sm" | "md" | "lg" | "xl" | "none" | null | undefined;
|
|
5
|
+
raised?: boolean | null | undefined;
|
|
6
|
+
} & import('class-variance-authority/types').ClassProp) | undefined) => string;
|
|
7
|
+
export declare const getIconContainerClasses: (props?: ({
|
|
8
|
+
variant?: "accent" | "default" | "error" | "ghost" | "info" | "neutral" | "primary" | "secondary" | "success" | "warning" | null | undefined;
|
|
9
|
+
size?: "xs" | "sm" | "md" | "lg" | "xl" | null | undefined;
|
|
10
|
+
shape?: "rounded" | "circle" | "square" | null | undefined;
|
|
11
|
+
shadow?: "sm" | "md" | "lg" | "xl" | "none" | null | undefined;
|
|
12
|
+
outline?: boolean | null | undefined;
|
|
13
|
+
soft?: boolean | null | undefined;
|
|
14
|
+
raised?: boolean | null | undefined;
|
|
15
|
+
} & import('class-variance-authority/types').ClassProp) | undefined) => string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { IconContainer } from './IconContainer';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { KPICardProps } from './types';
|
|
2
2
|
declare const KPICard: {
|
|
3
|
-
({ title, value, change, description,
|
|
3
|
+
({ title, value, change, description, chart, icon, className, ...props }: KPICardProps): import("react/jsx-runtime").JSX.Element;
|
|
4
4
|
displayName: string;
|
|
5
5
|
};
|
|
6
6
|
export { KPICard };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type KPICardSkeletonProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
2
|
+
/** Whether to show the icon placeholder */
|
|
3
|
+
icon?: boolean;
|
|
4
|
+
/** Whether to show the chart placeholder */
|
|
5
|
+
chart?: boolean;
|
|
6
|
+
};
|
|
7
|
+
declare const KPICardSkeleton: {
|
|
8
|
+
({ icon, chart, className, ...props }: KPICardSkeletonProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
displayName: string;
|
|
10
|
+
};
|
|
11
|
+
export { KPICardSkeleton };
|
|
@@ -8,14 +8,8 @@ export type KPICardProps = HTMLAttributes<HTMLDivElement> & {
|
|
|
8
8
|
change?: number;
|
|
9
9
|
/** Description text below value */
|
|
10
10
|
description?: string;
|
|
11
|
-
/** Sparkline
|
|
12
|
-
|
|
13
|
-
/** Sparkline color */
|
|
14
|
-
sparklineColor?: string;
|
|
15
|
-
/** Whether sparkline is filled */
|
|
16
|
-
sparklineFilled?: boolean;
|
|
11
|
+
/** Inline chart element (e.g. Sparkline, MiniBarChart, MiniAreaChart, MiniDonutChart, MiniStackedBar) */
|
|
12
|
+
chart?: ReactNode;
|
|
17
13
|
/** Icon to show in the card */
|
|
18
14
|
icon?: ReactNode;
|
|
19
|
-
/** Loading state */
|
|
20
|
-
loading?: boolean;
|
|
21
15
|
};
|
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
import { WidgetShellProps } from './types';
|
|
2
2
|
declare const WidgetShell: {
|
|
3
|
-
({
|
|
3
|
+
({ title, description, action, children, className, ...props }: WidgetShellProps): import("react/jsx-runtime").JSX.Element;
|
|
4
4
|
displayName: string;
|
|
5
|
+
Empty: {
|
|
6
|
+
({ children }: {
|
|
7
|
+
children?: React.ReactNode;
|
|
8
|
+
}): string | number | bigint | boolean | Iterable<import('react').ReactNode> | Promise<string | number | bigint | boolean | import('react').ReactPortal | import('react').ReactElement<unknown, string | import('react').JSXElementConstructor<any>> | Iterable<import('react').ReactNode> | null | undefined> | import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
displayName: string;
|
|
10
|
+
};
|
|
11
|
+
Error: {
|
|
12
|
+
({ children, onRetry, }: {
|
|
13
|
+
children?: React.ReactNode;
|
|
14
|
+
onRetry?: () => void;
|
|
15
|
+
}): string | number | bigint | boolean | Iterable<import('react').ReactNode> | Promise<string | number | bigint | boolean | import('react').ReactPortal | import('react').ReactElement<unknown, string | import('react').JSXElementConstructor<any>> | Iterable<import('react').ReactNode> | null | undefined> | import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
displayName: string;
|
|
17
|
+
};
|
|
18
|
+
Loading: {
|
|
19
|
+
({ lines }: {
|
|
20
|
+
lines?: number;
|
|
21
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
22
|
+
displayName: string;
|
|
23
|
+
};
|
|
5
24
|
};
|
|
6
25
|
export { WidgetShell };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type WidgetShellSkeletonProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
2
|
+
/** Number of skeleton lines to show */
|
|
3
|
+
lines?: number;
|
|
4
|
+
/** Whether to show the header skeleton */
|
|
5
|
+
header?: boolean;
|
|
6
|
+
};
|
|
7
|
+
declare const WidgetShellSkeleton: {
|
|
8
|
+
({ lines, header, className, ...props }: WidgetShellSkeletonProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
displayName: string;
|
|
10
|
+
};
|
|
11
|
+
export { WidgetShellSkeleton };
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { WidgetShell } from './WidgetShell';
|
|
2
|
-
export
|
|
2
|
+
export { WidgetShellSkeleton } from './WidgetShellSkeleton';
|
|
3
|
+
export type { WidgetShellProps } from './types';
|
|
4
|
+
export type { WidgetShellSkeletonProps } from './WidgetShellSkeleton';
|
|
@@ -1,20 +1,11 @@
|
|
|
1
1
|
import { HTMLAttributes, ReactNode } from 'react';
|
|
2
|
-
export type WidgetShellState = "loading" | "error" | "empty" | "ready";
|
|
3
2
|
export type WidgetShellProps = HTMLAttributes<HTMLDivElement> & {
|
|
4
|
-
/** Current state of the widget */
|
|
5
|
-
state?: WidgetShellState;
|
|
6
3
|
/** Widget title */
|
|
7
4
|
title?: ReactNode;
|
|
8
5
|
/** Widget description */
|
|
9
6
|
description?: ReactNode;
|
|
7
|
+
/** Content rendered in the top-right of the header (e.g. actions, icons, badges) */
|
|
8
|
+
action?: ReactNode;
|
|
10
9
|
/** Content to show when state is "ready" */
|
|
11
10
|
children: ReactNode;
|
|
12
|
-
/** Custom content for error state */
|
|
13
|
-
errorContent?: ReactNode;
|
|
14
|
-
/** Custom content for empty state */
|
|
15
|
-
emptyContent?: ReactNode;
|
|
16
|
-
/** Number of skeleton lines to show in loading state */
|
|
17
|
-
skeletonLines?: number;
|
|
18
|
-
/** Callback when retry is clicked in error state */
|
|
19
|
-
onRetry?: () => void;
|
|
20
11
|
};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export * from './components/Heading';
|
|
|
21
21
|
export * from './components/Paragraph';
|
|
22
22
|
/** utility & atoms */
|
|
23
23
|
export * from './components/AspectRatio';
|
|
24
|
+
export * from './components/IconContainer';
|
|
24
25
|
export * from './components/Image';
|
|
25
26
|
export * from './components/Spinner';
|
|
26
27
|
/** data display */
|
package/package.json
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { cn } from "../../utilities";
|
|
2
|
+
import { getIconContainerClasses, getIconRaisedContainerClasses } from "./constants";
|
|
3
|
+
import { type IconContainerProps } from "./types";
|
|
4
|
+
|
|
5
|
+
export const IconContainer = ({
|
|
6
|
+
variant,
|
|
7
|
+
size,
|
|
8
|
+
shape,
|
|
9
|
+
shadow,
|
|
10
|
+
outline,
|
|
11
|
+
soft,
|
|
12
|
+
raised,
|
|
13
|
+
className,
|
|
14
|
+
children,
|
|
15
|
+
...props
|
|
16
|
+
}: IconContainerProps) => {
|
|
17
|
+
const content = raised ? (
|
|
18
|
+
<div className={getIconRaisedContainerClasses({ size, variant, shadow, raised })}>
|
|
19
|
+
{children}
|
|
20
|
+
</div>
|
|
21
|
+
) : (
|
|
22
|
+
children
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div
|
|
27
|
+
className={cn(
|
|
28
|
+
getIconContainerClasses({
|
|
29
|
+
size,
|
|
30
|
+
soft,
|
|
31
|
+
shape,
|
|
32
|
+
variant,
|
|
33
|
+
outline,
|
|
34
|
+
raised,
|
|
35
|
+
shadow: raised ? "none" : shadow,
|
|
36
|
+
}),
|
|
37
|
+
className
|
|
38
|
+
)}
|
|
39
|
+
{...props}
|
|
40
|
+
>
|
|
41
|
+
{content}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { cva } from "../../libs";
|
|
2
|
+
import { createVariants } from "../../utilities";
|
|
3
|
+
|
|
4
|
+
const variants = createVariants({
|
|
5
|
+
variant: {
|
|
6
|
+
accent: "bg-accent text-accent-content",
|
|
7
|
+
default: "bg-base-200 text-base-content",
|
|
8
|
+
error: "bg-error text-error-content",
|
|
9
|
+
ghost: "bg-ghost text-ghost-content",
|
|
10
|
+
info: "bg-info text-info-content",
|
|
11
|
+
neutral: "bg-neutral text-neutral-content",
|
|
12
|
+
primary: "bg-primary text-primary-content",
|
|
13
|
+
secondary: "bg-secondary text-secondary-content",
|
|
14
|
+
success: "bg-success text-success-content",
|
|
15
|
+
warning: "bg-warning text-warning-content",
|
|
16
|
+
},
|
|
17
|
+
size: {
|
|
18
|
+
xs: "size-6 text-xs",
|
|
19
|
+
sm: "size-8 text-sm",
|
|
20
|
+
md: "size-10 text-base",
|
|
21
|
+
lg: "size-12 text-lg",
|
|
22
|
+
xl: "size-16 text-2xl",
|
|
23
|
+
},
|
|
24
|
+
shape: {
|
|
25
|
+
circle: "rounded-full",
|
|
26
|
+
square: "rounded-none",
|
|
27
|
+
rounded: "rounded-lg",
|
|
28
|
+
},
|
|
29
|
+
shadow: {
|
|
30
|
+
none: "",
|
|
31
|
+
sm: "shadow-sm",
|
|
32
|
+
md: "shadow-md",
|
|
33
|
+
lg: "shadow-lg",
|
|
34
|
+
xl: "shadow-xl",
|
|
35
|
+
},
|
|
36
|
+
outline: {
|
|
37
|
+
true: "bg-transparent ring-2 ring-offset-2 ring-offset-base-100",
|
|
38
|
+
},
|
|
39
|
+
soft: {
|
|
40
|
+
true: "",
|
|
41
|
+
},
|
|
42
|
+
raised: {
|
|
43
|
+
true: "ring-3 ring-base-200 bg-base-200 [&>div]:bg-base-100 [&>div]:size-[inherit] [&>div]:rounded-[inherit]",
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export const getIconRaisedContainerClasses = cva(
|
|
48
|
+
"inline-flex items-center justify-center shrink-0 [&>svg]:size-[1em]",
|
|
49
|
+
{
|
|
50
|
+
variants: createVariants({
|
|
51
|
+
variant: {
|
|
52
|
+
accent: "bg-accent text-accent",
|
|
53
|
+
default: "bg-base-200 text-base",
|
|
54
|
+
error: "bg-error text-error",
|
|
55
|
+
ghost: "bg-ghost text-ghost",
|
|
56
|
+
info: "bg-info text-info",
|
|
57
|
+
neutral: "bg-neutral text-neutral",
|
|
58
|
+
primary: "bg-primary text-primary",
|
|
59
|
+
secondary: "bg-secondary text-secondary",
|
|
60
|
+
success: "bg-success text-success",
|
|
61
|
+
warning: "bg-warning text-warning",
|
|
62
|
+
},
|
|
63
|
+
size: variants.size,
|
|
64
|
+
shadow: variants.shadow,
|
|
65
|
+
raised: {
|
|
66
|
+
true: "",
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
defaultVariants: {
|
|
70
|
+
variant: "default",
|
|
71
|
+
size: "md",
|
|
72
|
+
shadow: "md",
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
export const getIconContainerClasses = cva(
|
|
78
|
+
"inline-flex items-center justify-center shrink-0 [&>svg]:size-[1em]",
|
|
79
|
+
{
|
|
80
|
+
variants,
|
|
81
|
+
compoundVariants: [
|
|
82
|
+
// soft: pastel bg with colored icon
|
|
83
|
+
{ variant: "accent", soft: true, class: "bg-accent/10 text-accent" },
|
|
84
|
+
{ variant: "default", soft: true, class: "bg-base-200/50 text-base-content" },
|
|
85
|
+
{ variant: "error", soft: true, class: "bg-error/10 text-error" },
|
|
86
|
+
{ variant: "ghost", soft: true, class: "bg-base-100 text-base-content" },
|
|
87
|
+
{ variant: "info", soft: true, class: "bg-info/10 text-info" },
|
|
88
|
+
{ variant: "neutral", soft: true, class: "bg-neutral/10 text-neutral" },
|
|
89
|
+
{ variant: "primary", soft: true, class: "bg-primary/10 text-primary" },
|
|
90
|
+
{ variant: "secondary", soft: true, class: "bg-secondary/10 text-secondary" },
|
|
91
|
+
{ variant: "success", soft: true, class: "bg-success/10 text-success" },
|
|
92
|
+
{ variant: "warning", soft: true, class: "bg-warning/10 text-warning" },
|
|
93
|
+
// outline: variant dictates ring color and icon color
|
|
94
|
+
{ variant: "accent", outline: true, class: "ring-accent text-accent" },
|
|
95
|
+
{ variant: "default", outline: true, class: "ring-base-300 text-base-content" },
|
|
96
|
+
{ variant: "error", outline: true, class: "ring-error text-error" },
|
|
97
|
+
{ variant: "ghost", outline: true, class: "ring-base-300 text-base-content" },
|
|
98
|
+
{ variant: "info", outline: true, class: "ring-info text-info" },
|
|
99
|
+
{ variant: "neutral", outline: true, class: "ring-neutral text-neutral" },
|
|
100
|
+
{ variant: "primary", outline: true, class: "ring-primary text-primary" },
|
|
101
|
+
{ variant: "secondary", outline: true, class: "ring-secondary text-secondary" },
|
|
102
|
+
{ variant: "success", outline: true, class: "ring-success text-success" },
|
|
103
|
+
{ variant: "warning", outline: true, class: "ring-warning text-warning" },
|
|
104
|
+
],
|
|
105
|
+
defaultVariants: {
|
|
106
|
+
variant: "default",
|
|
107
|
+
size: "md",
|
|
108
|
+
shape: "circle",
|
|
109
|
+
shadow: "md",
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { IconContainer } from "./IconContainer";
|
|
@@ -1,80 +1,44 @@
|
|
|
1
|
+
import type { KPICardProps } from "./types";
|
|
1
2
|
import { cn } from "../../utilities";
|
|
2
|
-
import { Sparkline } from "../Sparkline";
|
|
3
3
|
import { TrendBadge } from "../TrendBadge";
|
|
4
|
-
import type { KPICardProps } from "./types";
|
|
5
4
|
|
|
6
5
|
const KPICard = ({
|
|
7
6
|
title,
|
|
8
7
|
value,
|
|
9
8
|
change,
|
|
10
9
|
description,
|
|
11
|
-
|
|
12
|
-
sparklineColor,
|
|
13
|
-
sparklineFilled = true,
|
|
10
|
+
chart,
|
|
14
11
|
icon,
|
|
15
|
-
loading = false,
|
|
16
12
|
className,
|
|
17
13
|
...props
|
|
18
14
|
}: KPICardProps) => {
|
|
19
|
-
if (loading) {
|
|
20
|
-
return (
|
|
21
|
-
<div
|
|
22
|
-
className={cn(
|
|
23
|
-
"rounded-lg border bg-base-100 p-4 shadow-sm transition-shadow duration-300 hover:shadow-md",
|
|
24
|
-
className
|
|
25
|
-
)}
|
|
26
|
-
{...props}
|
|
27
|
-
>
|
|
28
|
-
<div className="flex items-center justify-between">
|
|
29
|
-
<span className="skeleton h-4 w-24 rounded" />
|
|
30
|
-
{icon && <span className="skeleton size-8 rounded-md" />}
|
|
31
|
-
</div>
|
|
32
|
-
<div className="mt-3 flex items-end justify-between gap-4">
|
|
33
|
-
<div className="flex flex-col gap-1.5">
|
|
34
|
-
<span className="skeleton h-7 w-28 rounded" />
|
|
35
|
-
<span className="skeleton h-3.5 w-20 rounded" />
|
|
36
|
-
</div>
|
|
37
|
-
<span className="skeleton h-8 w-20 rounded" />
|
|
38
|
-
</div>
|
|
39
|
-
</div>
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
15
|
return (
|
|
44
16
|
<div
|
|
45
17
|
className={cn(
|
|
46
|
-
"rounded-lg border bg-base-100 p-4 shadow-sm transition-shadow duration-300 hover:shadow-md",
|
|
18
|
+
"w-full @container/kpicard overflow-hidden rounded-lg border bg-base-100 p-4 shadow-sm transition-shadow duration-300 hover:shadow-md",
|
|
47
19
|
className
|
|
48
20
|
)}
|
|
49
21
|
{...props}
|
|
50
22
|
>
|
|
51
23
|
<div className="flex items-center justify-between">
|
|
52
|
-
<span className="text-sm font-medium text-base-content/60">{title}</span>
|
|
24
|
+
<span className="min-w-0 truncate text-sm font-medium text-base-content/60">{title}</span>
|
|
53
25
|
{icon && (
|
|
54
|
-
<div className="flex size-8 items-center justify-center rounded-md bg-base-200/50 text-base-content/60">
|
|
26
|
+
<div className="flex size-8 shrink-0 items-center justify-center rounded-md bg-base-200/50 text-base-content/60">
|
|
55
27
|
{icon}
|
|
56
28
|
</div>
|
|
57
29
|
)}
|
|
58
30
|
</div>
|
|
59
31
|
<div className="mt-3 flex items-end justify-between gap-4">
|
|
60
|
-
<div className="flex flex-col gap-1">
|
|
61
|
-
<span className="text-2xl font-bold tracking-tight">{value}</span>
|
|
32
|
+
<div className="min-w-0 flex flex-col gap-1">
|
|
33
|
+
<span className="truncate text-2xl font-bold tracking-tight">{value}</span>
|
|
62
34
|
<div className="flex items-center gap-2">
|
|
63
35
|
{change !== undefined && <TrendBadge value={change} size="xs" />}
|
|
64
36
|
{description && (
|
|
65
|
-
<span className="text-xs text-base-content/50">{description}</span>
|
|
37
|
+
<span className="truncate text-xs text-base-content/50">{description}</span>
|
|
66
38
|
)}
|
|
67
39
|
</div>
|
|
68
40
|
</div>
|
|
69
|
-
{
|
|
70
|
-
<Sparkline
|
|
71
|
-
data={sparklineData}
|
|
72
|
-
color={sparklineColor ?? (change !== undefined && change >= 0 ? "oklch(0.7 0.2 150)" : "oklch(0.65 0.25 25)")}
|
|
73
|
-
filled={sparklineFilled}
|
|
74
|
-
width={80}
|
|
75
|
-
height={32}
|
|
76
|
-
/>
|
|
77
|
-
)}
|
|
41
|
+
{chart && <div className="shrink-0 @max-[160px]/kpicard:hidden">{chart}</div>}
|
|
78
42
|
</div>
|
|
79
43
|
</div>
|
|
80
44
|
);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { cn } from "../../utilities";
|
|
2
|
+
import { Skeleton } from "../Skeleton";
|
|
3
|
+
|
|
4
|
+
export type KPICardSkeletonProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
5
|
+
/** Whether to show the icon placeholder */
|
|
6
|
+
icon?: boolean;
|
|
7
|
+
/** Whether to show the chart placeholder */
|
|
8
|
+
chart?: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const KPICardSkeleton = ({
|
|
12
|
+
icon = true,
|
|
13
|
+
chart = true,
|
|
14
|
+
className,
|
|
15
|
+
...props
|
|
16
|
+
}: KPICardSkeletonProps) => {
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
className={cn(
|
|
20
|
+
"w-full overflow-hidden rounded-lg border bg-base-100 p-4 shadow-sm",
|
|
21
|
+
className
|
|
22
|
+
)}
|
|
23
|
+
{...props}
|
|
24
|
+
>
|
|
25
|
+
<div className="flex items-center justify-between">
|
|
26
|
+
<Skeleton className="h-4 w-24 rounded" />
|
|
27
|
+
{icon && <Skeleton className="size-8 rounded-md" />}
|
|
28
|
+
</div>
|
|
29
|
+
<div className="mt-4 flex items-end justify-between gap-4">
|
|
30
|
+
<div className="flex flex-col gap-1.5">
|
|
31
|
+
<Skeleton className="h-7 w-28 rounded" />
|
|
32
|
+
<Skeleton className="h-3.5 w-20 rounded" />
|
|
33
|
+
</div>
|
|
34
|
+
{chart && <Skeleton className="h-8 w-20 rounded" />}
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
KPICardSkeleton.displayName = "KPICardSkeleton";
|
|
41
|
+
|
|
42
|
+
export { KPICardSkeleton };
|
|
@@ -9,14 +9,8 @@ export type KPICardProps = HTMLAttributes<HTMLDivElement> & {
|
|
|
9
9
|
change?: number;
|
|
10
10
|
/** Description text below value */
|
|
11
11
|
description?: string;
|
|
12
|
-
/** Sparkline
|
|
13
|
-
|
|
14
|
-
/** Sparkline color */
|
|
15
|
-
sparklineColor?: string;
|
|
16
|
-
/** Whether sparkline is filled */
|
|
17
|
-
sparklineFilled?: boolean;
|
|
12
|
+
/** Inline chart element (e.g. Sparkline, MiniBarChart, MiniAreaChart, MiniDonutChart, MiniStackedBar) */
|
|
13
|
+
chart?: ReactNode;
|
|
18
14
|
/** Icon to show in the card */
|
|
19
15
|
icon?: ReactNode;
|
|
20
|
-
/** Loading state */
|
|
21
|
-
loading?: boolean;
|
|
22
16
|
};
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import { AlertCircleIcon, InboxIcon, RefreshCwIcon } from "lucide-react";
|
|
2
|
-
import { cn } from "../../utilities";
|
|
3
2
|
import type { WidgetShellProps } from "./types";
|
|
3
|
+
import { cn } from "../../utilities";
|
|
4
|
+
import { Button } from "../Button";
|
|
4
5
|
|
|
5
6
|
const WidgetShell = ({
|
|
6
|
-
state = "ready",
|
|
7
7
|
title,
|
|
8
8
|
description,
|
|
9
|
+
action,
|
|
9
10
|
children,
|
|
10
11
|
className,
|
|
11
|
-
errorContent,
|
|
12
|
-
emptyContent,
|
|
13
|
-
skeletonLines = 3,
|
|
14
|
-
onRetry,
|
|
15
12
|
...props
|
|
16
13
|
}: WidgetShellProps) => {
|
|
17
14
|
return (
|
|
@@ -22,80 +19,85 @@ const WidgetShell = ({
|
|
|
22
19
|
)}
|
|
23
20
|
{...props}
|
|
24
21
|
>
|
|
25
|
-
{(title || description) && (
|
|
26
|
-
<div className="border-b px-4 py-3">
|
|
27
|
-
|
|
28
|
-
<h3 className="text-sm font-semibold">
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
title
|
|
33
|
-
)}
|
|
34
|
-
</h3>
|
|
35
|
-
)}
|
|
36
|
-
{description && (
|
|
37
|
-
<p className="mt-0.5 text-xs text-base-content/60">
|
|
38
|
-
{state === "loading" ? (
|
|
39
|
-
<span className="skeleton inline-block h-3 w-48 rounded" />
|
|
40
|
-
) : (
|
|
41
|
-
description
|
|
42
|
-
)}
|
|
43
|
-
</p>
|
|
44
|
-
)}
|
|
22
|
+
{(title || description || action) && (
|
|
23
|
+
<div className="flex items-start justify-between border-b px-4 py-3">
|
|
24
|
+
<div className="min-w-0">
|
|
25
|
+
{title && <h3 className="text-sm font-semibold">{title}</h3>}
|
|
26
|
+
{description && <p className="mt-0.5 text-xs text-base-content/60">{description}</p>}
|
|
27
|
+
</div>
|
|
28
|
+
{action && <div className="shrink-0">{action}</div>}
|
|
45
29
|
</div>
|
|
46
30
|
)}
|
|
47
31
|
|
|
48
|
-
<div className="p-4">
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
className="skeleton h-4 rounded"
|
|
55
|
-
style={{ width: `${100 - i * 15}%` }}
|
|
56
|
-
/>
|
|
57
|
-
))}
|
|
58
|
-
</div>
|
|
59
|
-
)}
|
|
32
|
+
<div className="p-4">{children}</div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
WidgetShell.displayName = "WidgetShell";
|
|
60
38
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
>
|
|
75
|
-
<RefreshCwIcon className="size-3" />
|
|
76
|
-
Retry
|
|
77
|
-
</button>
|
|
78
|
-
)}
|
|
79
|
-
</div>
|
|
80
|
-
))}
|
|
39
|
+
const WidgetShellEmpty = ({ children }: { children?: React.ReactNode }) => {
|
|
40
|
+
return (
|
|
41
|
+
children ?? (
|
|
42
|
+
<div className="flex flex-col items-center justify-center gap-3 py-6 text-center">
|
|
43
|
+
<InboxIcon className="size-8 text-base-content/30" />
|
|
44
|
+
<div>
|
|
45
|
+
<p className="text-sm font-medium">No data</p>
|
|
46
|
+
<p className="text-xs text-base-content/60">Nothing to display yet</p>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
)
|
|
50
|
+
);
|
|
51
|
+
};
|
|
81
52
|
|
|
82
|
-
|
|
83
|
-
(emptyContent ?? (
|
|
84
|
-
<div className="flex flex-col items-center justify-center gap-3 py-6 text-center">
|
|
85
|
-
<InboxIcon className="size-8 text-base-content/30" />
|
|
86
|
-
<div>
|
|
87
|
-
<p className="text-sm font-medium">No data</p>
|
|
88
|
-
<p className="text-xs text-base-content/60">Nothing to display yet</p>
|
|
89
|
-
</div>
|
|
90
|
-
</div>
|
|
91
|
-
))}
|
|
53
|
+
WidgetShellEmpty.displayName = "WidgetShellEmpty";
|
|
92
54
|
|
|
93
|
-
|
|
55
|
+
const WidgetShellError = ({
|
|
56
|
+
children,
|
|
57
|
+
onRetry,
|
|
58
|
+
}: {
|
|
59
|
+
children?: React.ReactNode;
|
|
60
|
+
onRetry?: () => void;
|
|
61
|
+
}) => {
|
|
62
|
+
return (
|
|
63
|
+
children ?? (
|
|
64
|
+
<div className="flex flex-col items-center justify-center gap-3 py-6 text-center">
|
|
65
|
+
<AlertCircleIcon className="size-8 text-error/60" />
|
|
66
|
+
<div>
|
|
67
|
+
<p className="text-sm font-medium">Something went wrong</p>
|
|
68
|
+
<p className="text-xs text-base-content/60">Failed to load data</p>
|
|
69
|
+
</div>
|
|
70
|
+
{onRetry && (
|
|
71
|
+
<Button
|
|
72
|
+
type="button"
|
|
73
|
+
onClick={onRetry}
|
|
74
|
+
className="inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-xs font-medium transition-colors duration-200 hover:bg-base-200"
|
|
75
|
+
>
|
|
76
|
+
<RefreshCwIcon className="size-3 shrink-0" />
|
|
77
|
+
Retry
|
|
78
|
+
</Button>
|
|
79
|
+
)}
|
|
94
80
|
</div>
|
|
81
|
+
)
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
WidgetShellError.displayName = "WidgetShellError";
|
|
86
|
+
|
|
87
|
+
const WidgetShellLoading = ({ lines = 4 }: { lines?: number }) => {
|
|
88
|
+
return (
|
|
89
|
+
<div className="flex flex-col gap-3" role="status" aria-label="Loading">
|
|
90
|
+
{Array.from({ length: lines }, (_, i) => (
|
|
91
|
+
<span key={i} className="skeleton h-4 rounded" style={{ width: `${100 - i * 15}%` }} />
|
|
92
|
+
))}
|
|
95
93
|
</div>
|
|
96
94
|
);
|
|
97
95
|
};
|
|
98
96
|
|
|
99
|
-
|
|
97
|
+
WidgetShellLoading.displayName = "WidgetShellLoading";
|
|
98
|
+
|
|
99
|
+
WidgetShell.Empty = WidgetShellEmpty;
|
|
100
|
+
WidgetShell.Error = WidgetShellError;
|
|
101
|
+
WidgetShell.Loading = WidgetShellLoading;
|
|
100
102
|
|
|
101
103
|
export { WidgetShell };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { cn } from "../../utilities";
|
|
2
|
+
|
|
3
|
+
export type WidgetShellSkeletonProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
4
|
+
/** Number of skeleton lines to show */
|
|
5
|
+
lines?: number;
|
|
6
|
+
/** Whether to show the header skeleton */
|
|
7
|
+
header?: boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const WidgetShellSkeleton = ({
|
|
11
|
+
lines = 3,
|
|
12
|
+
header = true,
|
|
13
|
+
className,
|
|
14
|
+
...props
|
|
15
|
+
}: WidgetShellSkeletonProps) => {
|
|
16
|
+
return (
|
|
17
|
+
<div
|
|
18
|
+
className={cn(
|
|
19
|
+
"rounded-lg border bg-base-100 shadow-sm",
|
|
20
|
+
className
|
|
21
|
+
)}
|
|
22
|
+
{...props}
|
|
23
|
+
>
|
|
24
|
+
{header && (
|
|
25
|
+
<div className="border-b px-4 py-3">
|
|
26
|
+
<span className="skeleton inline-block h-4 w-32 rounded" />
|
|
27
|
+
<span className="skeleton mt-1.5 inline-block h-3 w-48 rounded" />
|
|
28
|
+
</div>
|
|
29
|
+
)}
|
|
30
|
+
<div className="p-4">
|
|
31
|
+
<div className="flex flex-col gap-3" role="status" aria-label="Loading">
|
|
32
|
+
{Array.from({ length: lines }, (_, i) => (
|
|
33
|
+
<span
|
|
34
|
+
key={i}
|
|
35
|
+
className="skeleton h-4 rounded"
|
|
36
|
+
style={{ width: `${100 - i * 15}%` }}
|
|
37
|
+
/>
|
|
38
|
+
))}
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
WidgetShellSkeleton.displayName = "WidgetShellSkeleton";
|
|
46
|
+
|
|
47
|
+
export { WidgetShellSkeleton };
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { WidgetShell } from "./WidgetShell";
|
|
2
|
-
export
|
|
2
|
+
export { WidgetShellSkeleton } from "./WidgetShellSkeleton";
|
|
3
|
+
export type { WidgetShellProps } from "./types";
|
|
4
|
+
export type { WidgetShellSkeletonProps } from "./WidgetShellSkeleton";
|
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
2
|
|
|
3
|
-
export type WidgetShellState = "loading" | "error" | "empty" | "ready";
|
|
4
|
-
|
|
5
3
|
export type WidgetShellProps = HTMLAttributes<HTMLDivElement> & {
|
|
6
|
-
/** Current state of the widget */
|
|
7
|
-
state?: WidgetShellState;
|
|
8
4
|
/** Widget title */
|
|
9
5
|
title?: ReactNode;
|
|
10
6
|
/** Widget description */
|
|
11
7
|
description?: ReactNode;
|
|
8
|
+
/** Content rendered in the top-right of the header (e.g. actions, icons, badges) */
|
|
9
|
+
action?: ReactNode;
|
|
12
10
|
/** Content to show when state is "ready" */
|
|
13
11
|
children: ReactNode;
|
|
14
|
-
/** Custom content for error state */
|
|
15
|
-
errorContent?: ReactNode;
|
|
16
|
-
/** Custom content for empty state */
|
|
17
|
-
emptyContent?: ReactNode;
|
|
18
|
-
/** Number of skeleton lines to show in loading state */
|
|
19
|
-
skeletonLines?: number;
|
|
20
|
-
/** Callback when retry is clicked in error state */
|
|
21
|
-
onRetry?: () => void;
|
|
22
12
|
};
|
package/src/index.ts
CHANGED