@blockbite/ui 1.0.4 → 1.0.5

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 (48) hide show
  1. package/package.json +2 -2
  2. package/src/AutocompleteDropdown.tsx +109 -0
  3. package/src/Badge.tsx +21 -0
  4. package/src/BitePreview.tsx +88 -0
  5. package/src/Button.tsx +60 -0
  6. package/src/ButtonToggle.tsx +165 -0
  7. package/src/Chapter.tsx +22 -0
  8. package/src/ChapterDivider.tsx +25 -0
  9. package/src/Checkbox.tsx +30 -0
  10. package/src/DisappearingMessage.tsx +41 -0
  11. package/src/DropdownPicker.tsx +72 -0
  12. package/src/EmptyState.tsx +32 -0
  13. package/src/FloatingPanel.tsx +59 -0
  14. package/src/FocalPointControl.tsx +58 -0
  15. package/src/Icon.tsx +17 -0
  16. package/src/LinkPicker.tsx +94 -0
  17. package/src/MediaPicker.tsx +161 -0
  18. package/src/MetricsControl.tsx +179 -0
  19. package/src/Modal.tsx +171 -0
  20. package/src/NewWindowPortal.tsx +68 -0
  21. package/src/Notice.tsx +32 -0
  22. package/src/PasswordInput.tsx +53 -0
  23. package/src/Popover.tsx +68 -0
  24. package/src/RangeSlider.tsx +68 -0
  25. package/src/ResponsiveImage.tsx +42 -0
  26. package/src/ResponsiveVideo.tsx +20 -0
  27. package/src/ScrollList.tsx +24 -0
  28. package/src/SectionList.tsx +166 -0
  29. package/src/SelectControlWrapper.tsx +47 -0
  30. package/src/SingleBlockTypeAppender.tsx +37 -0
  31. package/src/SlideIn.tsx +34 -0
  32. package/src/Spinner.tsx +24 -0
  33. package/src/Tabs.tsx +102 -0
  34. package/src/Tag.tsx +44 -0
  35. package/src/TextControl.tsx +74 -0
  36. package/src/TextControlLabel.tsx +27 -0
  37. package/src/ToggleGroup.tsx +72 -0
  38. package/src/ToggleSwitch.tsx +37 -0
  39. package/src/Wrap.tsx +23 -0
  40. package/src/_dev/App.css +42 -0
  41. package/src/_dev/App.tsx +183 -0
  42. package/src/_dev/assets/react.svg +1 -0
  43. package/src/_dev/index.css +68 -0
  44. package/src/_dev/main.tsx +10 -0
  45. package/src/_dev/vite-env.d.ts +1 -0
  46. package/src/types/ui-fallbacks.d.ts +7 -0
  47. package/src/types.ts +4 -0
  48. package/src/ui.css +66 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockbite/ui",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Shared React UI components for Blockbite apps",
5
5
  "author": "Blockbite",
6
6
  "license": "MIT",
@@ -9,7 +9,7 @@
9
9
  "types": "src/index.d.ts",
10
10
  "module": "src/index.js",
11
11
  "files": [
12
- "src/components/"
12
+ "src/"
13
13
  ],
14
14
  "scripts": {
15
15
  "dev": "vite",
@@ -0,0 +1,109 @@
1
+ import ChevronDownIcon from "@blockbite/icons/dist/ChevronDown";
2
+ import type { OptionProps } from "./types";
3
+ import { ButtonToggle } from "./ButtonToggle";
4
+ import { Wrap } from "./Wrap";
5
+ import { Button, Dropdown, TextControl } from "@wordpress/components";
6
+ import { useEffect, useState } from "@wordpress/element";
7
+ import classNames from "classnames";
8
+
9
+ interface OptionPanelDropdownProps {
10
+ defaultValue: string;
11
+ options: { label: string; value: string }[];
12
+ onPressedChange: (value: string) => void;
13
+ swatch?: boolean;
14
+ }
15
+
16
+ export default function OptionPanelDropdown({
17
+ defaultValue,
18
+ options,
19
+ swatch,
20
+ onPressedChange,
21
+ }: OptionPanelDropdownProps) {
22
+ const [activeKeyword, setActiveKeyword] = useState("");
23
+ const [filteredOptions, setFilteredOptions] = useState<OptionProps[]>([]);
24
+
25
+ useEffect(() => {
26
+ setFilteredOptions(
27
+ options.filter((option: OptionProps) =>
28
+ option.label.toLowerCase().includes(activeKeyword.toLowerCase())
29
+ )
30
+ );
31
+ }, [activeKeyword, options]);
32
+
33
+ useEffect(() => {
34
+ setActiveKeyword("");
35
+ setFilteredOptions(options);
36
+ }, [defaultValue, options]);
37
+
38
+ return (
39
+ <Dropdown
40
+ className="option-panel-dropdown"
41
+ contentClassName="option-panel-dropdown-content"
42
+ popoverProps={{ placement: "bottom-start" }}
43
+ renderToggle={({ isOpen, onToggle }) => (
44
+ <Wrap important>
45
+ <Button
46
+ variant="secondary"
47
+ size="small"
48
+ onClick={onToggle}
49
+ aria-expanded={isOpen}
50
+ >
51
+ <div className="flex items-center gap-1 !bg-transparent !p-0 !text-[11px] !text-current">
52
+ {swatch && (
53
+ <div
54
+ className={classNames(
55
+ `h-3 w-3 rounded-full bg-${defaultValue}`
56
+ )}
57
+ />
58
+ )}
59
+ <span>{defaultValue || "Select option…"}</span>
60
+ <ChevronDownIcon />
61
+ </div>
62
+ </Button>
63
+ </Wrap>
64
+ )}
65
+ renderContent={() => (
66
+ <Wrap important>
67
+ <div className="w-52">
68
+ <TextControl
69
+ label="Search options"
70
+ value={activeKeyword}
71
+ onChange={(value) => setActiveKeyword(value)}
72
+ autoComplete="off"
73
+ />
74
+ <div className="grid grid-cols-2 gap-1 !bg-transparent !p-0 !pt-2">
75
+ {filteredOptions.length === 0 && (
76
+ <div className="!text-gray-medium col-span-2 pb-2 text-center !text-[11px]">
77
+ No options found.
78
+ </div>
79
+ )}
80
+ {filteredOptions.map((option: OptionProps, index: Number) => (
81
+ <ButtonToggle
82
+ key={`ButtonToggle__${option.value}___${index}`}
83
+ className={classNames({
84
+ "bg-primary": option.value,
85
+ })}
86
+ size="small"
87
+ value={option.value.toString()}
88
+ defaultPressed={defaultValue}
89
+ onPressedChange={(value: string) => {
90
+ onPressedChange(value ? option.value : "");
91
+ }}
92
+ >
93
+ {swatch && (
94
+ <div
95
+ className={classNames(
96
+ `mr-3 h-3 w-3 rounded-full bg-${option.value}`
97
+ )}
98
+ />
99
+ )}
100
+ {option.label}
101
+ </ButtonToggle>
102
+ ))}
103
+ </div>
104
+ </div>
105
+ </Wrap>
106
+ )}
107
+ />
108
+ );
109
+ }
package/src/Badge.tsx ADDED
@@ -0,0 +1,21 @@
1
+ import classNames from 'classnames';
2
+
3
+ type BadgeProps = {
4
+ children: React.ReactNode;
5
+ className?: string;
6
+ onClick?: () => void;
7
+ };
8
+
9
+ export const Badge = ({ children, className, onClick }: BadgeProps) => {
10
+ return (
11
+ <div
12
+ onClick={onClick}
13
+ className={classNames(
14
+ className,
15
+ 'inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800'
16
+ )}
17
+ >
18
+ {children}
19
+ </div>
20
+ );
21
+ };
@@ -0,0 +1,88 @@
1
+ import { createPortal, useEffect, useRef, useState } from '@wordpress/element';
2
+
3
+ const BitePreview = ({ htmlContent, cssContent, frontendAssets }) => {
4
+ const iframeRef = useRef(null);
5
+ const [iframeBody, setIframeBody] = useState(null);
6
+
7
+ useEffect(() => {
8
+ const iframe = iframeRef.current;
9
+
10
+ if (iframe) {
11
+ iframe.onload = () => {
12
+ const iframeDocument =
13
+ iframe.contentDocument || iframe.contentWindow.document;
14
+
15
+ if (iframeDocument) {
16
+ setIframeBody(iframeDocument.body); // Set the iframe body for portal
17
+
18
+ // Inject CSS content directly
19
+ const styleTag = iframeDocument.createElement('style');
20
+ styleTag.innerHTML = cssContent;
21
+ iframeDocument.head.appendChild(styleTag);
22
+
23
+ // Adopt or Create Frontend Assets
24
+ frontendAssets.forEach(({ type, id, url }) => {
25
+ const existingElement = document.getElementById(id);
26
+
27
+ if (existingElement) {
28
+ // If the asset exists in the parent, move it to the iframe
29
+ const adoptedElement = document.adoptNode(
30
+ existingElement.cloneNode(true)
31
+ );
32
+ iframeDocument.head.appendChild(adoptedElement);
33
+ } else {
34
+ // Create a new script or style if not found
35
+ const newElement = iframeDocument.createElement(
36
+ type === 'script' ? 'script' : 'link'
37
+ );
38
+ newElement.id = id;
39
+
40
+ if (type === 'script') {
41
+ newElement.src = url;
42
+ newElement.async = true;
43
+ } else {
44
+ newElement.rel = 'stylesheet';
45
+ newElement.href = url;
46
+ }
47
+
48
+ iframeDocument.head.appendChild(newElement);
49
+ }
50
+ });
51
+ }
52
+ };
53
+ }
54
+
55
+ return () => {
56
+ if (iframe) {
57
+ iframe.onload = null; // Clean up event listener
58
+ }
59
+ };
60
+ }, [htmlContent, cssContent, frontendAssets]);
61
+
62
+ return (
63
+ <div
64
+ className="render-preview-container"
65
+ style={{ width: '100%', minHeight: '100%' }}
66
+ >
67
+ <iframe
68
+ ref={iframeRef}
69
+ title="Preview"
70
+ className="editor-styles-wrapper"
71
+ width="100%"
72
+ height="100%"
73
+ />
74
+ {iframeBody &&
75
+ createPortal(
76
+ <div className="b_">
77
+ <div
78
+ className="b_utils"
79
+ dangerouslySetInnerHTML={{ __html: htmlContent }}
80
+ />
81
+ </div>,
82
+ iframeBody
83
+ )}
84
+ </div>
85
+ );
86
+ };
87
+
88
+ export default BitePreview;
package/src/Button.tsx ADDED
@@ -0,0 +1,60 @@
1
+ import { Icon } from "./Icon";
2
+ import { Button as WordpressButton } from "@wordpress/components";
3
+ import classNames from "classnames";
4
+
5
+ type ButtonProps = {
6
+ children?: React.ReactNode;
7
+ asChild?: boolean;
8
+ className?: string;
9
+ display?: "icon" | "icon-lg" | "label" | "auto" | "" | null;
10
+ onClick?: () => void;
11
+ label?: string;
12
+ size?: "small" | "default" | "compact";
13
+ variant?: "primary" | "secondary" | "link" | "primary" | "tertiary";
14
+ icon?: any;
15
+ disabled?: boolean;
16
+ };
17
+
18
+ export const Button = ({
19
+ children,
20
+ size = "default",
21
+ label,
22
+ className,
23
+ onClick,
24
+ variant = "primary",
25
+ display = "auto",
26
+ icon,
27
+ disabled = false,
28
+ }: ButtonProps) => {
29
+ const isIconDisplay = display === "icon" || display === "icon-lg";
30
+
31
+ return (
32
+ <WordpressButton
33
+ size={size}
34
+ variant={variant}
35
+ label={label}
36
+ showTooltip={true}
37
+ disabled={disabled}
38
+ className={classNames(
39
+ className,
40
+ "blockbite-ui__button",
41
+ "flex items-center justify-center gap-1",
42
+ { "is-primary": variant === "primary" },
43
+ { "is-secondary": variant === "secondary" },
44
+ { "is-link": variant === "link" },
45
+ { "is-tertiary": variant === "tertiary" },
46
+ { "is-icon": display === "icon" }
47
+ )}
48
+ onClick={onClick}
49
+ >
50
+ {icon && (
51
+ <Icon
52
+ icon={icon}
53
+ className={classNames({ "h-4 w-4": display === "icon-lg" })}
54
+ />
55
+ )}
56
+ {!isIconDisplay ? children : null}
57
+ {label && !children && !isIconDisplay ? <span>{label}</span> : null}
58
+ </WordpressButton>
59
+ );
60
+ };
@@ -0,0 +1,165 @@
1
+ import { Icon } from "./Icon";
2
+ import { Wrap } from "./Wrap";
3
+ import { Button as WordpressButton } from "@wordpress/components";
4
+ import { memo, useCallback, useEffect, useState } from "@wordpress/element";
5
+ import classNames from "classnames";
6
+
7
+ type ButtonToggleProps = {
8
+ children?: React.ReactNode;
9
+ className?: string;
10
+ value: string;
11
+ defaultPressed: string;
12
+ variant?: "primary" | "secondary";
13
+ size?: "small" | "default" | "compact";
14
+ icon?: any;
15
+ display?: "icon" | "label" | "" | null;
16
+ onPressedChange: (value: string) => void;
17
+ label?: string;
18
+ };
19
+
20
+ type ButtonToggleGroupOptionProp = {
21
+ value: string;
22
+ label: string;
23
+ tooltip?: string;
24
+ icon?: any;
25
+ children?: React.ReactNode;
26
+ };
27
+
28
+ type ButtonToggleGroupProps = {
29
+ className?: string;
30
+ options: ButtonToggleGroupOptionProp[];
31
+ defaultPressed?: string;
32
+ toggle?: boolean;
33
+ size?: "small" | "default" | "compact";
34
+ tabs?: boolean;
35
+ display?: "icon" | "label" | "" | null;
36
+ variant?: "primary" | "secondary";
37
+ stretch?: boolean;
38
+ icon?: any;
39
+ onPressedChange?: (value: string) => void;
40
+ };
41
+
42
+ const ButtonToggle: React.FC<ButtonToggleProps> = memo(
43
+ ({
44
+ children,
45
+ className,
46
+ value,
47
+ variant = "secondary",
48
+ defaultPressed,
49
+ onPressedChange,
50
+ icon,
51
+ size = "compact",
52
+ display = "auto",
53
+ label,
54
+ }) => {
55
+ const [isPressed, setIsPressed] = useState(false);
56
+
57
+ useEffect(() => {
58
+ setIsPressed(defaultPressed === value ? true : false);
59
+ // eslint-disable-next-line react-hooks/exhaustive-deps
60
+ }, [defaultPressed]);
61
+
62
+ const handleClick = useCallback(() => {
63
+ setIsPressed((prev) => !prev);
64
+ onPressedChange(value);
65
+ // eslint-disable-next-line react-hooks/exhaustive-deps
66
+ }, [isPressed, onPressedChange]);
67
+
68
+ return (
69
+ <WordpressButton
70
+ aria-label={label}
71
+ className={classNames(className, "blockbite-ui__button--default")}
72
+ value={value}
73
+ size={size}
74
+ label={label}
75
+ variant={variant}
76
+ showTooltip={true}
77
+ isPressed={isPressed}
78
+ onClick={handleClick}
79
+ >
80
+ {icon && <Icon icon={icon} />}
81
+ {display !== "icon" ? children : null}
82
+ {label && !children && display !== "icon" ? <span>{label}</span> : null}
83
+ </WordpressButton>
84
+ );
85
+ }
86
+ );
87
+
88
+ const ButtonToggleGroup: React.FC<ButtonToggleGroupProps> = memo(
89
+ ({
90
+ className,
91
+ defaultPressed = "",
92
+ toggle = true,
93
+ display = "auto",
94
+ options,
95
+ size = "compact",
96
+ tabs = false,
97
+ variant = "secondary",
98
+ stretch = false,
99
+ onPressedChange,
100
+ }) => {
101
+ const [isPressed, setIsPressed] = useState<string>(defaultPressed);
102
+
103
+ useEffect(() => {
104
+ setIsPressed(defaultPressed);
105
+ }, [defaultPressed]);
106
+
107
+ const handleButtonClick = useCallback(
108
+ (value: string) => {
109
+ const newValue = toggle && isPressed === value ? "" : value;
110
+ setIsPressed(newValue);
111
+ onPressedChange?.(newValue);
112
+ },
113
+ [isPressed, onPressedChange, toggle]
114
+ );
115
+
116
+ const renderContent = (option: ButtonToggleGroupOptionProp) => {
117
+ if (display === "icon" && option?.icon) {
118
+ return <Icon icon={option.icon} />;
119
+ } else if (display === "label") {
120
+ return <span>{option.label}</span>;
121
+ }
122
+ return (
123
+ <span className="flex items-center justify-center gap-1">
124
+ {option.icon && <Icon icon={option.icon} />}
125
+ <span>{option.label}</span>
126
+ </span>
127
+ );
128
+ };
129
+
130
+ return (
131
+ <Wrap
132
+ className={classNames(
133
+ "blockbite-ui__button-group flex flex-wrap gap-1",
134
+ className,
135
+ tabs ? "blockbite-ui__button-group--tabs" : ""
136
+ )}
137
+ >
138
+ {options.map((option, index) => (
139
+ <WordpressButton
140
+ key={`ButtonToggleGroup__${option.value}__${option.label}__${index}`}
141
+ className={classNames("blockbite-ui__button--default", {
142
+ grow: stretch,
143
+ "justify-center": stretch,
144
+ })}
145
+ // tooltip
146
+ aria-label={option.label}
147
+ showTooltip={true}
148
+ value={option.value}
149
+ size={size}
150
+ label={option?.tooltip || option.label}
151
+ variant={variant}
152
+ isPressed={isPressed === option.value}
153
+ onClick={() => handleButtonClick(option.value)}
154
+ >
155
+ {renderContent(option)}
156
+ {option.children && option.children}
157
+ </WordpressButton>
158
+ ))}
159
+ </Wrap>
160
+ );
161
+ }
162
+ );
163
+
164
+ export default ButtonToggleGroup;
165
+ export { ButtonToggle, ButtonToggleGroup };
@@ -0,0 +1,22 @@
1
+ import { Wrap } from "./Wrap";
2
+ import classNames from "classnames";
3
+
4
+ type ChapterProps = {
5
+ children?: React.ReactNode;
6
+ className?: string;
7
+ title?: string;
8
+ };
9
+
10
+ export const Chapter = ({ children, title }: ChapterProps) => {
11
+ return (
12
+ <Wrap
13
+ className={classNames(
14
+ "text-gray-medium my-2 flex items-center gap-1 text-[12px] font-medium",
15
+ classNames
16
+ )}
17
+ >
18
+ {title && <Wrap className="font-medium">{title}</Wrap>}
19
+ {children}
20
+ </Wrap>
21
+ );
22
+ };
@@ -0,0 +1,25 @@
1
+ import classNames from 'classnames';
2
+
3
+ type ChapterDividerProps = {
4
+ id?: string;
5
+ title?: string;
6
+ className?: string;
7
+ help?: string;
8
+ };
9
+
10
+ export const ChapterDivider = ({
11
+ id = '',
12
+ title,
13
+ className,
14
+ help,
15
+ }: ChapterDividerProps) => {
16
+ return (
17
+ <div {...(id ? { id } : null)} className={classNames('mb-4', className)}>
18
+ <div className="flex w-full flex-wrap items-center gap-2">
19
+ <small className="shrink-1 text-[12px]">{title}</small>
20
+ <span className="h-[1px] w-full bg-easy"></span>
21
+ </div>
22
+ {help && <small className="w-full shrink-0 text-[12px]">{help}</small>}
23
+ </div>
24
+ );
25
+ };
@@ -0,0 +1,30 @@
1
+ import { Wrap } from "./Wrap";
2
+ import { CheckboxControl } from "@wordpress/components";
3
+ import { useEffect, useState } from "@wordpress/element";
4
+
5
+ type CheckboxProps = {
6
+ id: string;
7
+ label?: string;
8
+ help?: string;
9
+ defaultChecked?: boolean;
10
+ onCheckedChange: (checked: boolean, id: string) => void;
11
+ };
12
+
13
+ export const Checkbox = ({ label, help, defaultChecked }: CheckboxProps) => {
14
+ const [isChecked, setChecked] = useState(defaultChecked);
15
+
16
+ useEffect(() => {
17
+ setChecked(defaultChecked);
18
+ }, [defaultChecked]);
19
+
20
+ return (
21
+ <Wrap className="blockbite-ui__checkbox mx-1 flex items-center gap-2">
22
+ <CheckboxControl
23
+ label={label}
24
+ help={help}
25
+ checked={isChecked}
26
+ onChange={setChecked}
27
+ />
28
+ </Wrap>
29
+ );
30
+ };
@@ -0,0 +1,41 @@
1
+ import { useEffect, useRef, useState } from '@wordpress/element';
2
+
3
+ type DisappearingMessageProps = {
4
+ duration: number;
5
+ children: React.ReactNode;
6
+ trigger: boolean;
7
+ };
8
+
9
+ export const DisappearingMessage = ({
10
+ duration,
11
+ children,
12
+ trigger,
13
+ }: DisappearingMessageProps) => {
14
+ const [alert, setAlert] = useState(false);
15
+ const isMountingRef = useRef(false);
16
+
17
+ useEffect(() => {
18
+ isMountingRef.current = true;
19
+ }, []);
20
+
21
+ useEffect(() => {
22
+ let timeoutId = null;
23
+
24
+ // Only run on subsequent renders
25
+ if (trigger && !isMountingRef.current) {
26
+ setAlert(true);
27
+ timeoutId = setTimeout(() => {
28
+ setAlert(false);
29
+ }, duration);
30
+ } else {
31
+ isMountingRef.current = false;
32
+ }
33
+
34
+ // Cleanup timeout on unmount or when `trigger` changes
35
+ return () => {
36
+ clearTimeout(timeoutId);
37
+ };
38
+ }, [trigger, duration]);
39
+
40
+ return alert && <>{children}</>;
41
+ };
@@ -0,0 +1,72 @@
1
+ import ChevronDownIcon from "@blockbite/icons/dist/ChevronDown";
2
+ import { Icon } from "./Icon";
3
+ import { DropdownMenu } from "@wordpress/components";
4
+ import { useEffect, useState } from "@wordpress/element";
5
+ import classNames from "classnames";
6
+
7
+ type DropdownPickerProps = {
8
+ label?: string;
9
+ className?: string;
10
+ defaultValue: string;
11
+ defaultIcon?: any;
12
+ options: {
13
+ icon?: React.ReactElement;
14
+ label: string;
15
+ subtitle?: string;
16
+ value: string;
17
+ }[];
18
+ onPressedChange: (value: string | null) => void; // Updated to allow `null` for reset
19
+ };
20
+
21
+ export const DropdownPicker = ({
22
+ label,
23
+ className,
24
+ defaultValue,
25
+ defaultIcon = ChevronDownIcon,
26
+ onPressedChange,
27
+ options,
28
+ }: DropdownPickerProps) => {
29
+ const [currentOption, setCurrentOption] = useState<string | null>(null);
30
+
31
+ useEffect(() => {
32
+ setCurrentOption(defaultValue);
33
+ }, [defaultValue]);
34
+
35
+ const allOptions = [
36
+ ...options.map((option) => ({
37
+ icon: option.icon,
38
+ label: option.label,
39
+ title: option.label,
40
+ value: option.value,
41
+ onClick: () => {
42
+ setCurrentOption(option.value);
43
+ onPressedChange(option.value);
44
+ },
45
+ })),
46
+ {
47
+ icon: <Icon icon={defaultIcon} />,
48
+ title: "Reset",
49
+ value: "reset",
50
+ onClick: () => {
51
+ setCurrentOption("reset");
52
+ onPressedChange("reset");
53
+ },
54
+ },
55
+ ];
56
+
57
+ return (
58
+ <DropdownMenu
59
+ controls={allOptions}
60
+ className={classNames(
61
+ "blockbite-ui__dropdown-picker border-primary border",
62
+ className
63
+ )}
64
+ icon={
65
+ options.find((option) => option.value === currentOption)?.icon || (
66
+ <Icon icon={defaultIcon} />
67
+ )
68
+ }
69
+ label={label || "Select"}
70
+ />
71
+ );
72
+ };
@@ -0,0 +1,32 @@
1
+ type Props = {
2
+ icon?: JSX.Element;
3
+ title: string;
4
+ description: string;
5
+ children?: React.ReactNode;
6
+ [key: string]: any;
7
+ };
8
+
9
+ export default function EmptyState({
10
+ icon,
11
+ title,
12
+ description,
13
+ children = null,
14
+ ...rest
15
+ }: Props) {
16
+ return (
17
+ <div {...rest}>
18
+ <div className="flex h-full w-full flex-col items-center justify-center text-center !font-sans">
19
+ <div className="max-w-sm">
20
+ {icon && <div className="mx-auto !text-gray-400">{icon}</div>}
21
+ <h3 className="text-gray-medium mt-2 !font-sans text-sm font-medium">
22
+ {title}
23
+ </h3>
24
+ <p className="mt-1 !font-sans text-sm !text-gray-500">
25
+ {description}
26
+ </p>
27
+ {children && <div className="mt-4">{children}</div>}
28
+ </div>
29
+ </div>
30
+ </div>
31
+ );
32
+ }