@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.
- package/package.json +2 -2
- package/src/AutocompleteDropdown.tsx +109 -0
- package/src/Badge.tsx +21 -0
- package/src/BitePreview.tsx +88 -0
- package/src/Button.tsx +60 -0
- package/src/ButtonToggle.tsx +165 -0
- package/src/Chapter.tsx +22 -0
- package/src/ChapterDivider.tsx +25 -0
- package/src/Checkbox.tsx +30 -0
- package/src/DisappearingMessage.tsx +41 -0
- package/src/DropdownPicker.tsx +72 -0
- package/src/EmptyState.tsx +32 -0
- package/src/FloatingPanel.tsx +59 -0
- package/src/FocalPointControl.tsx +58 -0
- package/src/Icon.tsx +17 -0
- package/src/LinkPicker.tsx +94 -0
- package/src/MediaPicker.tsx +161 -0
- package/src/MetricsControl.tsx +179 -0
- package/src/Modal.tsx +171 -0
- package/src/NewWindowPortal.tsx +68 -0
- package/src/Notice.tsx +32 -0
- package/src/PasswordInput.tsx +53 -0
- package/src/Popover.tsx +68 -0
- package/src/RangeSlider.tsx +68 -0
- package/src/ResponsiveImage.tsx +42 -0
- package/src/ResponsiveVideo.tsx +20 -0
- package/src/ScrollList.tsx +24 -0
- package/src/SectionList.tsx +166 -0
- package/src/SelectControlWrapper.tsx +47 -0
- package/src/SingleBlockTypeAppender.tsx +37 -0
- package/src/SlideIn.tsx +34 -0
- package/src/Spinner.tsx +24 -0
- package/src/Tabs.tsx +102 -0
- package/src/Tag.tsx +44 -0
- package/src/TextControl.tsx +74 -0
- package/src/TextControlLabel.tsx +27 -0
- package/src/ToggleGroup.tsx +72 -0
- package/src/ToggleSwitch.tsx +37 -0
- package/src/Wrap.tsx +23 -0
- package/src/_dev/App.css +42 -0
- package/src/_dev/App.tsx +183 -0
- package/src/_dev/assets/react.svg +1 -0
- package/src/_dev/index.css +68 -0
- package/src/_dev/main.tsx +10 -0
- package/src/_dev/vite-env.d.ts +1 -0
- package/src/types/ui-fallbacks.d.ts +7 -0
- package/src/types.ts +4 -0
- 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.
|
|
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/
|
|
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 };
|
package/src/Chapter.tsx
ADDED
|
@@ -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
|
+
};
|
package/src/Checkbox.tsx
ADDED
|
@@ -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
|
+
}
|