react-data-state 1.0.1

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/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # react-data-state
2
+
3
+ A lightweight React component to handle `loading`, `error`, and `empty` states with clean defaults and easy customization.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install react-data-state
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```tsx
14
+ import { DataState } from "react-data-state";
15
+
16
+ <DataState
17
+ loading={loading}
18
+ error={error}
19
+ data={users}
20
+ onRetry={refetchUsers}
21
+ emptyProps={{ message: "No users found" }}
22
+ >
23
+ {(data) => (
24
+ <ul>
25
+ {data.map((user) => (
26
+ <li key={user.id}>{user.name}</li>
27
+ ))}
28
+ </ul>
29
+ )}
30
+ </DataState>;
31
+ ```
32
+
33
+ ## Optional: Customize
34
+
35
+ ```tsx
36
+ <DataState
37
+ loading={loading} // define your loading state
38
+ error={error} // define your error state
39
+ data={users} // define your data state
40
+ onRetry={refetch} // define your retry function
41
+ loadingProps={{
42
+ color: "#22c55e", // color of the spinner
43
+ trackColor: "#1f2937", // color of the spinner track
44
+ size: 30, // size of the spinner in pixels
45
+ thickness: 3, // thickness of the spinner
46
+ speedMs: 650, // speed in milliseconds for one full rotation
47
+ }}
48
+ errorProps={{
49
+ bgColor: "#2a1010", // background color of the error message
50
+ borderColor: "#ef4444", // border color of the error message
51
+ color: "#fecaca", // text color of the error message
52
+ retryLabel: "Try again", // label for the retry button
53
+ buttonBgColor: "#ef4444", // background color of the retry button
54
+ buttonTextColor: "#111827", // text color of the retry button
55
+ }}
56
+ emptyProps={{
57
+ message: "No users found", // message to display when data is empty
58
+ icon: "🔎", // Support: emoji, image, inline svg, Font Awesome element and React-icons component type
59
+ color: "#94a3b8", // text color of the empty message
60
+ bgColor: "#0f172a", // background color of the empty message
61
+ }}
62
+ >
63
+ {(data) => (
64
+ // Render your data here, like example above ↑
65
+ )}
66
+ </DataState>
67
+ ```
68
+
69
+ ## Repo
70
+
71
+ GitHub: https://github.com/Bunheng-Dev/react-data-state
@@ -0,0 +1,81 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import React, { CSSProperties, ReactNode } from 'react';
3
+
4
+ type EmptyStateProps = {
5
+ message?: string;
6
+ icon?: React.ReactNode | React.ComponentType<{
7
+ style?: CSSProperties;
8
+ className?: string;
9
+ "aria-hidden"?: boolean;
10
+ }>;
11
+ color?: string;
12
+ iconColor?: string;
13
+ bgColor?: string;
14
+ minHeight?: number | string;
15
+ fontSize?: number;
16
+ iconSize?: number;
17
+ gap?: number;
18
+ style?: CSSProperties;
19
+ contentStyle?: CSSProperties;
20
+ iconContainerStyle?: CSSProperties;
21
+ iconStyle?: CSSProperties;
22
+ };
23
+ declare function EmptyState({ message, icon, color, iconColor, bgColor, minHeight, fontSize, iconSize, gap, style, contentStyle, iconContainerStyle, iconStyle, }: EmptyStateProps): react_jsx_runtime.JSX.Element;
24
+
25
+ type ErrorBoxProps = {
26
+ message?: string;
27
+ onRetry?: () => void;
28
+ bgColor?: string;
29
+ borderColor?: string;
30
+ color?: string;
31
+ borderRadius?: number;
32
+ padding?: number | string;
33
+ gap?: number;
34
+ retryLabel?: string;
35
+ buttonBgColor?: string;
36
+ buttonTextColor?: string;
37
+ buttonBorderColor?: string;
38
+ buttonBorderRadius?: number;
39
+ style?: CSSProperties;
40
+ buttonStyle?: CSSProperties;
41
+ };
42
+ declare function ErrorBox({ message, onRetry, bgColor, borderColor, color, borderRadius, padding, gap, retryLabel, buttonBgColor, buttonTextColor, buttonBorderColor, buttonBorderRadius, style, buttonStyle, }: ErrorBoxProps): react_jsx_runtime.JSX.Element;
43
+
44
+ type LoadingSpinnerProps = {
45
+ label?: string;
46
+ color?: string;
47
+ trackColor?: string;
48
+ size?: number;
49
+ thickness?: number;
50
+ minHeight?: number | string;
51
+ speedMs?: number;
52
+ style?: CSSProperties;
53
+ spinnerStyle?: CSSProperties;
54
+ };
55
+ declare function LoadingSpinner({ label, color, trackColor, size, thickness, minHeight, speedMs, style, spinnerStyle, }: LoadingSpinnerProps): react_jsx_runtime.JSX.Element;
56
+
57
+ type StateContext<T> = {
58
+ loading: boolean;
59
+ error: unknown;
60
+ data?: T;
61
+ errorMessage: string;
62
+ onRetry?: () => void;
63
+ };
64
+ type StateRenderer<T> = ReactNode | ((context: StateContext<T>) => ReactNode);
65
+ type DataStateProps<T> = {
66
+ loading?: boolean;
67
+ error?: unknown;
68
+ data?: T;
69
+ children: ((data: T) => ReactNode) | ReactNode;
70
+ loadingComponent?: StateRenderer<T>;
71
+ errorComponent?: StateRenderer<T>;
72
+ emptyComponent?: StateRenderer<T>;
73
+ loadingProps?: LoadingSpinnerProps;
74
+ errorProps?: ErrorBoxProps;
75
+ emptyProps?: EmptyStateProps;
76
+ onRetry?: () => void;
77
+ isEmpty?: (data: T) => boolean;
78
+ };
79
+ declare function DataState<T>({ loading, error, data, children, errorComponent, loadingComponent, emptyComponent, loadingProps, errorProps, emptyProps, onRetry, isEmpty, }: DataStateProps<T>): react_jsx_runtime.JSX.Element;
80
+
81
+ export { DataState, type DataStateProps, EmptyState, type EmptyStateProps, ErrorBox, type ErrorBoxProps, LoadingSpinner, type LoadingSpinnerProps };
@@ -0,0 +1,81 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import React, { CSSProperties, ReactNode } from 'react';
3
+
4
+ type EmptyStateProps = {
5
+ message?: string;
6
+ icon?: React.ReactNode | React.ComponentType<{
7
+ style?: CSSProperties;
8
+ className?: string;
9
+ "aria-hidden"?: boolean;
10
+ }>;
11
+ color?: string;
12
+ iconColor?: string;
13
+ bgColor?: string;
14
+ minHeight?: number | string;
15
+ fontSize?: number;
16
+ iconSize?: number;
17
+ gap?: number;
18
+ style?: CSSProperties;
19
+ contentStyle?: CSSProperties;
20
+ iconContainerStyle?: CSSProperties;
21
+ iconStyle?: CSSProperties;
22
+ };
23
+ declare function EmptyState({ message, icon, color, iconColor, bgColor, minHeight, fontSize, iconSize, gap, style, contentStyle, iconContainerStyle, iconStyle, }: EmptyStateProps): react_jsx_runtime.JSX.Element;
24
+
25
+ type ErrorBoxProps = {
26
+ message?: string;
27
+ onRetry?: () => void;
28
+ bgColor?: string;
29
+ borderColor?: string;
30
+ color?: string;
31
+ borderRadius?: number;
32
+ padding?: number | string;
33
+ gap?: number;
34
+ retryLabel?: string;
35
+ buttonBgColor?: string;
36
+ buttonTextColor?: string;
37
+ buttonBorderColor?: string;
38
+ buttonBorderRadius?: number;
39
+ style?: CSSProperties;
40
+ buttonStyle?: CSSProperties;
41
+ };
42
+ declare function ErrorBox({ message, onRetry, bgColor, borderColor, color, borderRadius, padding, gap, retryLabel, buttonBgColor, buttonTextColor, buttonBorderColor, buttonBorderRadius, style, buttonStyle, }: ErrorBoxProps): react_jsx_runtime.JSX.Element;
43
+
44
+ type LoadingSpinnerProps = {
45
+ label?: string;
46
+ color?: string;
47
+ trackColor?: string;
48
+ size?: number;
49
+ thickness?: number;
50
+ minHeight?: number | string;
51
+ speedMs?: number;
52
+ style?: CSSProperties;
53
+ spinnerStyle?: CSSProperties;
54
+ };
55
+ declare function LoadingSpinner({ label, color, trackColor, size, thickness, minHeight, speedMs, style, spinnerStyle, }: LoadingSpinnerProps): react_jsx_runtime.JSX.Element;
56
+
57
+ type StateContext<T> = {
58
+ loading: boolean;
59
+ error: unknown;
60
+ data?: T;
61
+ errorMessage: string;
62
+ onRetry?: () => void;
63
+ };
64
+ type StateRenderer<T> = ReactNode | ((context: StateContext<T>) => ReactNode);
65
+ type DataStateProps<T> = {
66
+ loading?: boolean;
67
+ error?: unknown;
68
+ data?: T;
69
+ children: ((data: T) => ReactNode) | ReactNode;
70
+ loadingComponent?: StateRenderer<T>;
71
+ errorComponent?: StateRenderer<T>;
72
+ emptyComponent?: StateRenderer<T>;
73
+ loadingProps?: LoadingSpinnerProps;
74
+ errorProps?: ErrorBoxProps;
75
+ emptyProps?: EmptyStateProps;
76
+ onRetry?: () => void;
77
+ isEmpty?: (data: T) => boolean;
78
+ };
79
+ declare function DataState<T>({ loading, error, data, children, errorComponent, loadingComponent, emptyComponent, loadingProps, errorProps, emptyProps, onRetry, isEmpty, }: DataStateProps<T>): react_jsx_runtime.JSX.Element;
80
+
81
+ export { DataState, type DataStateProps, EmptyState, type EmptyStateProps, ErrorBox, type ErrorBoxProps, LoadingSpinner, type LoadingSpinnerProps };
package/dist/index.js ADDED
@@ -0,0 +1,310 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ DataState: () => DataState,
34
+ EmptyState: () => EmptyState,
35
+ ErrorBox: () => ErrorBox,
36
+ LoadingSpinner: () => LoadingSpinner
37
+ });
38
+ module.exports = __toCommonJS(index_exports);
39
+
40
+ // src/components/EmptyState.tsx
41
+ var import_react = __toESM(require("react"));
42
+ var import_jsx_runtime = require("react/jsx-runtime");
43
+ var baseContainerStyle = {
44
+ minHeight: 120,
45
+ padding: 12,
46
+ display: "flex",
47
+ alignItems: "center",
48
+ justifyContent: "center",
49
+ textAlign: "center",
50
+ color: "#6b7280"
51
+ };
52
+ var baseContentStyle = {
53
+ display: "inline-flex",
54
+ alignItems: "center",
55
+ gap: 8,
56
+ fontSize: 15
57
+ };
58
+ function EmptyState({
59
+ message = "No data found",
60
+ icon = "\u{1F5ED}",
61
+ color = "#6b7280",
62
+ iconColor,
63
+ bgColor,
64
+ minHeight = 120,
65
+ fontSize = 15,
66
+ iconSize,
67
+ gap = 8,
68
+ style,
69
+ contentStyle,
70
+ iconContainerStyle,
71
+ iconStyle
72
+ }) {
73
+ const containerStyle = {
74
+ ...baseContainerStyle,
75
+ minHeight,
76
+ color,
77
+ backgroundColor: bgColor,
78
+ ...style
79
+ };
80
+ const resolvedContentStyle = {
81
+ ...baseContentStyle,
82
+ gap,
83
+ fontSize,
84
+ ...contentStyle
85
+ };
86
+ const resolvedIconContainerStyle = {
87
+ display: "inline-flex",
88
+ alignItems: "center",
89
+ justifyContent: "center",
90
+ color: iconColor ?? color,
91
+ ...iconSize ? { width: iconSize, height: iconSize, fontSize: iconSize } : null,
92
+ ...iconContainerStyle
93
+ };
94
+ const resolvedIconStyle = {
95
+ ...iconSize ? { width: iconSize, height: iconSize } : null,
96
+ ...iconColor ? { color: iconColor } : null,
97
+ ...iconStyle
98
+ };
99
+ const renderIcon = () => {
100
+ if (icon === null || icon === void 0 || icon === false) return null;
101
+ if (typeof icon === "function") {
102
+ const IconComponent = icon;
103
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", style: resolvedIconContainerStyle, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IconComponent, { "aria-hidden": true, style: resolvedIconStyle }) });
104
+ }
105
+ if (import_react.default.isValidElement(icon)) {
106
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", style: resolvedIconContainerStyle, children: icon });
107
+ }
108
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", style: resolvedIconContainerStyle, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: resolvedIconStyle, children: icon }) });
109
+ };
110
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: containerStyle, role: "status", "aria-live": "polite", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: resolvedContentStyle, children: [
111
+ renderIcon(),
112
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: message })
113
+ ] }) });
114
+ }
115
+
116
+ // src/components/ErrorBox.tsx
117
+ var import_jsx_runtime2 = require("react/jsx-runtime");
118
+ var baseBoxStyle = {
119
+ backgroundColor: "#fef2f2",
120
+ border: "1px solid #fecaca",
121
+ borderRadius: 10,
122
+ color: "#991b1b",
123
+ padding: "14px 16px",
124
+ display: "flex",
125
+ flexDirection: "column",
126
+ gap: 12
127
+ };
128
+ var baseRetryStyle = {
129
+ alignSelf: "center",
130
+ backgroundColor: "#b91c1c",
131
+ color: "#ffffff",
132
+ border: 0,
133
+ borderRadius: 8,
134
+ padding: "8px 12px",
135
+ fontSize: 14,
136
+ lineHeight: 1.1,
137
+ cursor: "pointer"
138
+ };
139
+ function ErrorBox({
140
+ message = "Something went wrong",
141
+ onRetry,
142
+ bgColor = "#fef2f2",
143
+ borderColor = "#fecaca",
144
+ color = "#991b1b",
145
+ borderRadius = 10,
146
+ padding = "14px 16px",
147
+ gap = 12,
148
+ retryLabel = "Retry",
149
+ buttonBgColor = "#b91c1c",
150
+ buttonTextColor = "#ffffff",
151
+ buttonBorderColor = "transparent",
152
+ buttonBorderRadius = 8,
153
+ style,
154
+ buttonStyle
155
+ }) {
156
+ const boxStyle = {
157
+ ...baseBoxStyle,
158
+ backgroundColor: bgColor,
159
+ border: `1px solid ${borderColor}`,
160
+ color,
161
+ borderRadius,
162
+ padding,
163
+ gap,
164
+ ...style
165
+ };
166
+ const retryStyle = {
167
+ ...baseRetryStyle,
168
+ backgroundColor: buttonBgColor,
169
+ color: buttonTextColor,
170
+ border: `1px solid ${buttonBorderColor}`,
171
+ borderRadius: buttonBorderRadius,
172
+ ...buttonStyle
173
+ };
174
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: boxStyle, role: "alert", "aria-live": "assertive", children: [
175
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { children: message }),
176
+ onRetry ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", onClick: onRetry, style: retryStyle, children: retryLabel }) : null
177
+ ] });
178
+ }
179
+
180
+ // src/components/LoadingSpinner.tsx
181
+ var import_jsx_runtime3 = require("react/jsx-runtime");
182
+ var baseContainerStyle2 = {
183
+ minHeight: 120,
184
+ position: "relative",
185
+ display: "flex",
186
+ alignItems: "center",
187
+ justifyContent: "center"
188
+ };
189
+ function LoadingSpinner({
190
+ label = "Loading",
191
+ color = "#2563eb",
192
+ trackColor = "#d1d5db",
193
+ size = 28,
194
+ thickness = 3,
195
+ minHeight = 120,
196
+ speedMs = 750,
197
+ style,
198
+ spinnerStyle
199
+ }) {
200
+ const containerStyle = {
201
+ ...baseContainerStyle2,
202
+ minHeight,
203
+ ...style
204
+ };
205
+ const resolvedSpinnerStyle = {
206
+ width: size,
207
+ height: size,
208
+ borderRadius: "50%",
209
+ border: `${thickness}px solid ${trackColor}`,
210
+ borderTopColor: color,
211
+ animation: `rds-spin ${speedMs}ms linear infinite`,
212
+ ...spinnerStyle
213
+ };
214
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: containerStyle, "aria-busy": "true", "aria-live": "polite", role: "status", children: [
215
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("style", { children: "@keyframes rds-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }" }),
216
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: resolvedSpinnerStyle, "aria-hidden": "true" }),
217
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
218
+ "span",
219
+ {
220
+ style: {
221
+ position: "absolute",
222
+ width: 1,
223
+ height: 1,
224
+ padding: 0,
225
+ margin: -1,
226
+ overflow: "hidden",
227
+ clip: "rect(0, 0, 0, 0)",
228
+ whiteSpace: "nowrap",
229
+ border: 0
230
+ },
231
+ children: label
232
+ }
233
+ )
234
+ ] });
235
+ }
236
+
237
+ // src/DataState.tsx
238
+ var import_jsx_runtime4 = require("react/jsx-runtime");
239
+ var transitionStyle = {
240
+ animation: "rds-fade-in 180ms ease-out"
241
+ };
242
+ function StateTransition({ children }) {
243
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: transitionStyle, children: [
244
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("style", { children: "@keyframes rds-fade-in { from { opacity: 0; transform: translateY(2px); } to { opacity: 1; transform: translateY(0); } }" }),
245
+ children
246
+ ] });
247
+ }
248
+ function resolveStateRenderer(renderer, context) {
249
+ if (renderer === void 0) return void 0;
250
+ return typeof renderer === "function" ? renderer(context) : renderer;
251
+ }
252
+ function getErrorMessage(error) {
253
+ if (error && typeof error === "object" && "message" in error) {
254
+ const maybeMessage = error.message;
255
+ if (typeof maybeMessage === "string" && maybeMessage.trim().length > 0) return maybeMessage;
256
+ }
257
+ if (typeof error === "string" && error.trim().length > 0) return error;
258
+ return "Something went wrong";
259
+ }
260
+ function DataState({
261
+ loading,
262
+ error,
263
+ data,
264
+ children,
265
+ errorComponent,
266
+ loadingComponent,
267
+ emptyComponent,
268
+ loadingProps,
269
+ errorProps,
270
+ emptyProps,
271
+ onRetry,
272
+ isEmpty
273
+ }) {
274
+ const errorMessage = getErrorMessage(error);
275
+ const context = {
276
+ loading: Boolean(loading),
277
+ error,
278
+ data,
279
+ errorMessage,
280
+ onRetry
281
+ };
282
+ if (loading) {
283
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(StateTransition, { children: resolveStateRenderer(loadingComponent, context) ?? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(LoadingSpinner, { ...loadingProps }) }, "loading");
284
+ }
285
+ if (error) {
286
+ const resolvedErrorProps = {
287
+ ...errorProps,
288
+ message: errorProps?.message ?? errorMessage,
289
+ onRetry: errorProps?.onRetry ?? onRetry
290
+ };
291
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(StateTransition, { children: resolveStateRenderer(errorComponent, context) ?? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ErrorBox, { ...resolvedErrorProps }) }, "error");
292
+ }
293
+ const hasNoData = data === void 0 || data === null;
294
+ const emptyByDefault = !hasNoData && Array.isArray(data) && data.length === 0;
295
+ const empty = hasNoData || !hasNoData && (isEmpty ? isEmpty(data) : emptyByDefault);
296
+ if (empty) {
297
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(StateTransition, { children: resolveStateRenderer(emptyComponent, context) ?? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(EmptyState, { ...emptyProps }) }, "empty");
298
+ }
299
+ if (typeof children === "function") {
300
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children: children(data) });
301
+ }
302
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children });
303
+ }
304
+ // Annotate the CommonJS export names for ESM import in node:
305
+ 0 && (module.exports = {
306
+ DataState,
307
+ EmptyState,
308
+ ErrorBox,
309
+ LoadingSpinner
310
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,270 @@
1
+ // src/components/EmptyState.tsx
2
+ import React from "react";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ var baseContainerStyle = {
5
+ minHeight: 120,
6
+ padding: 12,
7
+ display: "flex",
8
+ alignItems: "center",
9
+ justifyContent: "center",
10
+ textAlign: "center",
11
+ color: "#6b7280"
12
+ };
13
+ var baseContentStyle = {
14
+ display: "inline-flex",
15
+ alignItems: "center",
16
+ gap: 8,
17
+ fontSize: 15
18
+ };
19
+ function EmptyState({
20
+ message = "No data found",
21
+ icon = "\u{1F5ED}",
22
+ color = "#6b7280",
23
+ iconColor,
24
+ bgColor,
25
+ minHeight = 120,
26
+ fontSize = 15,
27
+ iconSize,
28
+ gap = 8,
29
+ style,
30
+ contentStyle,
31
+ iconContainerStyle,
32
+ iconStyle
33
+ }) {
34
+ const containerStyle = {
35
+ ...baseContainerStyle,
36
+ minHeight,
37
+ color,
38
+ backgroundColor: bgColor,
39
+ ...style
40
+ };
41
+ const resolvedContentStyle = {
42
+ ...baseContentStyle,
43
+ gap,
44
+ fontSize,
45
+ ...contentStyle
46
+ };
47
+ const resolvedIconContainerStyle = {
48
+ display: "inline-flex",
49
+ alignItems: "center",
50
+ justifyContent: "center",
51
+ color: iconColor ?? color,
52
+ ...iconSize ? { width: iconSize, height: iconSize, fontSize: iconSize } : null,
53
+ ...iconContainerStyle
54
+ };
55
+ const resolvedIconStyle = {
56
+ ...iconSize ? { width: iconSize, height: iconSize } : null,
57
+ ...iconColor ? { color: iconColor } : null,
58
+ ...iconStyle
59
+ };
60
+ const renderIcon = () => {
61
+ if (icon === null || icon === void 0 || icon === false) return null;
62
+ if (typeof icon === "function") {
63
+ const IconComponent = icon;
64
+ return /* @__PURE__ */ jsx("span", { "aria-hidden": "true", style: resolvedIconContainerStyle, children: /* @__PURE__ */ jsx(IconComponent, { "aria-hidden": true, style: resolvedIconStyle }) });
65
+ }
66
+ if (React.isValidElement(icon)) {
67
+ return /* @__PURE__ */ jsx("span", { "aria-hidden": "true", style: resolvedIconContainerStyle, children: icon });
68
+ }
69
+ return /* @__PURE__ */ jsx("span", { "aria-hidden": "true", style: resolvedIconContainerStyle, children: /* @__PURE__ */ jsx("span", { style: resolvedIconStyle, children: icon }) });
70
+ };
71
+ return /* @__PURE__ */ jsx("div", { style: containerStyle, role: "status", "aria-live": "polite", children: /* @__PURE__ */ jsxs("div", { style: resolvedContentStyle, children: [
72
+ renderIcon(),
73
+ /* @__PURE__ */ jsx("span", { children: message })
74
+ ] }) });
75
+ }
76
+
77
+ // src/components/ErrorBox.tsx
78
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
79
+ var baseBoxStyle = {
80
+ backgroundColor: "#fef2f2",
81
+ border: "1px solid #fecaca",
82
+ borderRadius: 10,
83
+ color: "#991b1b",
84
+ padding: "14px 16px",
85
+ display: "flex",
86
+ flexDirection: "column",
87
+ gap: 12
88
+ };
89
+ var baseRetryStyle = {
90
+ alignSelf: "center",
91
+ backgroundColor: "#b91c1c",
92
+ color: "#ffffff",
93
+ border: 0,
94
+ borderRadius: 8,
95
+ padding: "8px 12px",
96
+ fontSize: 14,
97
+ lineHeight: 1.1,
98
+ cursor: "pointer"
99
+ };
100
+ function ErrorBox({
101
+ message = "Something went wrong",
102
+ onRetry,
103
+ bgColor = "#fef2f2",
104
+ borderColor = "#fecaca",
105
+ color = "#991b1b",
106
+ borderRadius = 10,
107
+ padding = "14px 16px",
108
+ gap = 12,
109
+ retryLabel = "Retry",
110
+ buttonBgColor = "#b91c1c",
111
+ buttonTextColor = "#ffffff",
112
+ buttonBorderColor = "transparent",
113
+ buttonBorderRadius = 8,
114
+ style,
115
+ buttonStyle
116
+ }) {
117
+ const boxStyle = {
118
+ ...baseBoxStyle,
119
+ backgroundColor: bgColor,
120
+ border: `1px solid ${borderColor}`,
121
+ color,
122
+ borderRadius,
123
+ padding,
124
+ gap,
125
+ ...style
126
+ };
127
+ const retryStyle = {
128
+ ...baseRetryStyle,
129
+ backgroundColor: buttonBgColor,
130
+ color: buttonTextColor,
131
+ border: `1px solid ${buttonBorderColor}`,
132
+ borderRadius: buttonBorderRadius,
133
+ ...buttonStyle
134
+ };
135
+ return /* @__PURE__ */ jsxs2("div", { style: boxStyle, role: "alert", "aria-live": "assertive", children: [
136
+ /* @__PURE__ */ jsx2("div", { children: message }),
137
+ onRetry ? /* @__PURE__ */ jsx2("button", { type: "button", onClick: onRetry, style: retryStyle, children: retryLabel }) : null
138
+ ] });
139
+ }
140
+
141
+ // src/components/LoadingSpinner.tsx
142
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
143
+ var baseContainerStyle2 = {
144
+ minHeight: 120,
145
+ position: "relative",
146
+ display: "flex",
147
+ alignItems: "center",
148
+ justifyContent: "center"
149
+ };
150
+ function LoadingSpinner({
151
+ label = "Loading",
152
+ color = "#2563eb",
153
+ trackColor = "#d1d5db",
154
+ size = 28,
155
+ thickness = 3,
156
+ minHeight = 120,
157
+ speedMs = 750,
158
+ style,
159
+ spinnerStyle
160
+ }) {
161
+ const containerStyle = {
162
+ ...baseContainerStyle2,
163
+ minHeight,
164
+ ...style
165
+ };
166
+ const resolvedSpinnerStyle = {
167
+ width: size,
168
+ height: size,
169
+ borderRadius: "50%",
170
+ border: `${thickness}px solid ${trackColor}`,
171
+ borderTopColor: color,
172
+ animation: `rds-spin ${speedMs}ms linear infinite`,
173
+ ...spinnerStyle
174
+ };
175
+ return /* @__PURE__ */ jsxs3("div", { style: containerStyle, "aria-busy": "true", "aria-live": "polite", role: "status", children: [
176
+ /* @__PURE__ */ jsx3("style", { children: "@keyframes rds-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }" }),
177
+ /* @__PURE__ */ jsx3("div", { style: resolvedSpinnerStyle, "aria-hidden": "true" }),
178
+ /* @__PURE__ */ jsx3(
179
+ "span",
180
+ {
181
+ style: {
182
+ position: "absolute",
183
+ width: 1,
184
+ height: 1,
185
+ padding: 0,
186
+ margin: -1,
187
+ overflow: "hidden",
188
+ clip: "rect(0, 0, 0, 0)",
189
+ whiteSpace: "nowrap",
190
+ border: 0
191
+ },
192
+ children: label
193
+ }
194
+ )
195
+ ] });
196
+ }
197
+
198
+ // src/DataState.tsx
199
+ import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
200
+ var transitionStyle = {
201
+ animation: "rds-fade-in 180ms ease-out"
202
+ };
203
+ function StateTransition({ children }) {
204
+ return /* @__PURE__ */ jsxs4("div", { style: transitionStyle, children: [
205
+ /* @__PURE__ */ jsx4("style", { children: "@keyframes rds-fade-in { from { opacity: 0; transform: translateY(2px); } to { opacity: 1; transform: translateY(0); } }" }),
206
+ children
207
+ ] });
208
+ }
209
+ function resolveStateRenderer(renderer, context) {
210
+ if (renderer === void 0) return void 0;
211
+ return typeof renderer === "function" ? renderer(context) : renderer;
212
+ }
213
+ function getErrorMessage(error) {
214
+ if (error && typeof error === "object" && "message" in error) {
215
+ const maybeMessage = error.message;
216
+ if (typeof maybeMessage === "string" && maybeMessage.trim().length > 0) return maybeMessage;
217
+ }
218
+ if (typeof error === "string" && error.trim().length > 0) return error;
219
+ return "Something went wrong";
220
+ }
221
+ function DataState({
222
+ loading,
223
+ error,
224
+ data,
225
+ children,
226
+ errorComponent,
227
+ loadingComponent,
228
+ emptyComponent,
229
+ loadingProps,
230
+ errorProps,
231
+ emptyProps,
232
+ onRetry,
233
+ isEmpty
234
+ }) {
235
+ const errorMessage = getErrorMessage(error);
236
+ const context = {
237
+ loading: Boolean(loading),
238
+ error,
239
+ data,
240
+ errorMessage,
241
+ onRetry
242
+ };
243
+ if (loading) {
244
+ return /* @__PURE__ */ jsx4(StateTransition, { children: resolveStateRenderer(loadingComponent, context) ?? /* @__PURE__ */ jsx4(LoadingSpinner, { ...loadingProps }) }, "loading");
245
+ }
246
+ if (error) {
247
+ const resolvedErrorProps = {
248
+ ...errorProps,
249
+ message: errorProps?.message ?? errorMessage,
250
+ onRetry: errorProps?.onRetry ?? onRetry
251
+ };
252
+ return /* @__PURE__ */ jsx4(StateTransition, { children: resolveStateRenderer(errorComponent, context) ?? /* @__PURE__ */ jsx4(ErrorBox, { ...resolvedErrorProps }) }, "error");
253
+ }
254
+ const hasNoData = data === void 0 || data === null;
255
+ const emptyByDefault = !hasNoData && Array.isArray(data) && data.length === 0;
256
+ const empty = hasNoData || !hasNoData && (isEmpty ? isEmpty(data) : emptyByDefault);
257
+ if (empty) {
258
+ return /* @__PURE__ */ jsx4(StateTransition, { children: resolveStateRenderer(emptyComponent, context) ?? /* @__PURE__ */ jsx4(EmptyState, { ...emptyProps }) }, "empty");
259
+ }
260
+ if (typeof children === "function") {
261
+ return /* @__PURE__ */ jsx4(Fragment, { children: children(data) });
262
+ }
263
+ return /* @__PURE__ */ jsx4(Fragment, { children });
264
+ }
265
+ export {
266
+ DataState,
267
+ EmptyState,
268
+ ErrorBox,
269
+ LoadingSpinner
270
+ };
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "react-data-state",
3
+ "version": "1.0.1",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/Bunheng-Dev/react-data-state.git"
7
+ },
8
+ "bugs": {
9
+ "url": "https://github.com/Bunheng-Dev/react-data-state/issues"
10
+ },
11
+ "homepage": "https://github.com/Bunheng-Dev/react-data-state#readme",
12
+ "main": "dist/index.js",
13
+ "module": "dist/index.mjs",
14
+ "types": "dist/index.d.ts",
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup src/index.ts --format cjs,esm --dts",
20
+ "dev": "tsup src/index.ts --watch"
21
+ },
22
+ "peerDependencies": {
23
+ "react": ">=18"
24
+ }
25
+ }