@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
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from '@wordpress/element';
|
|
2
|
+
|
|
3
|
+
type DraggablePanelProps = {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export default function FloatingPanel({
|
|
8
|
+
children,
|
|
9
|
+
}: DraggablePanelProps & {
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
}) {
|
|
12
|
+
const panelRef = useRef(null);
|
|
13
|
+
const [position, setPosition] = useState({ x: 100, y: 100 });
|
|
14
|
+
const [dragging, setDragging] = useState(false);
|
|
15
|
+
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const handleMouseMove = (e) => {
|
|
19
|
+
if (dragging) {
|
|
20
|
+
setPosition({
|
|
21
|
+
x: e.clientX - offset.x,
|
|
22
|
+
y: e.clientY - offset.y,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const handleMouseUp = () => setDragging(false);
|
|
28
|
+
|
|
29
|
+
window.addEventListener('mousemove', handleMouseMove);
|
|
30
|
+
window.addEventListener('mouseup', handleMouseUp);
|
|
31
|
+
|
|
32
|
+
return () => {
|
|
33
|
+
window.removeEventListener('mousemove', handleMouseMove);
|
|
34
|
+
window.removeEventListener('mouseup', handleMouseUp);
|
|
35
|
+
};
|
|
36
|
+
}, [dragging, offset]);
|
|
37
|
+
|
|
38
|
+
const startDragging = (e) => {
|
|
39
|
+
const rect = panelRef.current.getBoundingClientRect();
|
|
40
|
+
setOffset({ x: e.clientX - rect.left, y: e.clientY - rect.top });
|
|
41
|
+
setDragging(true);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="bb_">
|
|
46
|
+
<div
|
|
47
|
+
ref={panelRef}
|
|
48
|
+
className="fixed bg-white shadow-xl rounded-2xl p-4 w-[400px] h-[550px] z-[9999]"
|
|
49
|
+
style={{ left: position.x, top: position.y }}
|
|
50
|
+
>
|
|
51
|
+
{children}
|
|
52
|
+
<div
|
|
53
|
+
className="absolute top-1 right-1 cursor-move w-5 h-5 bg-gray-300 rounded"
|
|
54
|
+
onMouseDown={startDragging}
|
|
55
|
+
></div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Wrap } from "./Wrap";
|
|
2
|
+
import { FocalPointPicker } from "@wordpress/components";
|
|
3
|
+
import { useEffect, useState } from "@wordpress/element";
|
|
4
|
+
|
|
5
|
+
type FocalPointControlProps = {
|
|
6
|
+
defaultValue: string;
|
|
7
|
+
onValueChange: (value: string) => void;
|
|
8
|
+
url?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const FocalPointControl: React.FC<FocalPointControlProps> = ({
|
|
12
|
+
defaultValue,
|
|
13
|
+
onValueChange,
|
|
14
|
+
url,
|
|
15
|
+
}) => {
|
|
16
|
+
const [focalPoint, setFocalPoint] = useState({
|
|
17
|
+
x: 0.5,
|
|
18
|
+
y: 0.5,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
onValueChange(
|
|
23
|
+
`[${(focalPoint.x * 100).toFixed(2)}%_${(focalPoint.y * 100).toFixed(
|
|
24
|
+
2
|
|
25
|
+
)}%]`
|
|
26
|
+
);
|
|
27
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
28
|
+
}, [focalPoint]);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
// default value is in the format of [x%_y%] parse to get the x and y values
|
|
32
|
+
if (defaultValue.includes("%")) {
|
|
33
|
+
const [x, y] = defaultValue
|
|
34
|
+
.replace("[", "")
|
|
35
|
+
.replace("%]", "")
|
|
36
|
+
.split("_")
|
|
37
|
+
.map((value: string) => parseFloat(value) / 100);
|
|
38
|
+
|
|
39
|
+
setFocalPoint({
|
|
40
|
+
x,
|
|
41
|
+
y,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}, [defaultValue]);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Wrap className="relative flex flex-col">
|
|
48
|
+
<FocalPointPicker
|
|
49
|
+
url={url}
|
|
50
|
+
value={focalPoint}
|
|
51
|
+
onDrag={(value) => setFocalPoint(value)}
|
|
52
|
+
onChange={(value) => setFocalPoint(value)}
|
|
53
|
+
/>
|
|
54
|
+
</Wrap>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default FocalPointControl;
|
package/src/Icon.tsx
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Wrap } from "./Wrap";
|
|
2
|
+
|
|
3
|
+
type IconProps = {
|
|
4
|
+
icon: React.FC<React.SVGProps<SVGSVGElement>> | null; // Type it as a React Functional Component
|
|
5
|
+
className?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const Icon = ({ icon: IconComponent, className = "" }: IconProps) => {
|
|
9
|
+
if (!IconComponent) return null;
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<Wrap className={`blockbite--icon`}>
|
|
13
|
+
<IconComponent className={className} />{" "}
|
|
14
|
+
{/* Render the functional component */}
|
|
15
|
+
</Wrap>
|
|
16
|
+
);
|
|
17
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Wrap } from "./Wrap";
|
|
2
|
+
import apiFetch from "@wordpress/api-fetch";
|
|
3
|
+
import { TextControl } from "@wordpress/components";
|
|
4
|
+
import { useEffect, useState } from "@wordpress/element";
|
|
5
|
+
import { __ } from "@wordpress/i18n";
|
|
6
|
+
|
|
7
|
+
export default function LinkPicker(props) {
|
|
8
|
+
const [activeKeyword, setActiveKeyword] = useState("");
|
|
9
|
+
const [links, setLinks] = useState<
|
|
10
|
+
Array<{ id: number; url: string; title: string; post_type: string }>
|
|
11
|
+
>([]);
|
|
12
|
+
const [activeLink, setActiveLink] = useState({
|
|
13
|
+
url: "",
|
|
14
|
+
title: "",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (activeKeyword === "") return;
|
|
19
|
+
setLinks(null);
|
|
20
|
+
apiFetch({
|
|
21
|
+
path: `/blockbite/v1/block-helpers/get-links/${activeKeyword}`,
|
|
22
|
+
}).then(
|
|
23
|
+
(
|
|
24
|
+
fetchedLinks: Array<{
|
|
25
|
+
id: number;
|
|
26
|
+
url: string;
|
|
27
|
+
title: string;
|
|
28
|
+
post_type: string;
|
|
29
|
+
}> | null
|
|
30
|
+
) => {
|
|
31
|
+
if (fetchedLinks?.length) {
|
|
32
|
+
setLinks([...fetchedLinks]);
|
|
33
|
+
} else {
|
|
34
|
+
setLinks([]);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
}, [activeKeyword]);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (activeLink.url !== "") {
|
|
42
|
+
props.parentCallback(activeLink);
|
|
43
|
+
}
|
|
44
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
45
|
+
}, [activeLink]);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Wrap className="blockbite--editor-linkwrap">
|
|
49
|
+
<TextControl
|
|
50
|
+
label={__("Search link", "blockbitelinks")}
|
|
51
|
+
value={activeKeyword}
|
|
52
|
+
placeholder="Example: About"
|
|
53
|
+
onChange={(value) => setActiveKeyword(value)}
|
|
54
|
+
help={__("Type a post, page, title", "blockbitelinks")}
|
|
55
|
+
/>
|
|
56
|
+
{activeKeyword ? (
|
|
57
|
+
<div className="blockbite--editor-linklist">
|
|
58
|
+
<LinkList
|
|
59
|
+
links={links}
|
|
60
|
+
onActiveLink={(link) => [
|
|
61
|
+
setActiveLink({ ...link }),
|
|
62
|
+
setActiveKeyword(""),
|
|
63
|
+
]}
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
) : null}
|
|
67
|
+
</Wrap>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
function LinkList({ links, onActiveLink }) {
|
|
71
|
+
const list = [];
|
|
72
|
+
if (links === null) {
|
|
73
|
+
return <p>Loading...</p>;
|
|
74
|
+
} else if (links.length === 0) {
|
|
75
|
+
return <p>No Results</p>;
|
|
76
|
+
}
|
|
77
|
+
// iterate over the links and show img with icon based on icon_url and icon
|
|
78
|
+
links.forEach((link) => {
|
|
79
|
+
list.push(
|
|
80
|
+
// add key
|
|
81
|
+
<Wrap key={link.id}>
|
|
82
|
+
<span
|
|
83
|
+
className="blockbite--editor-link"
|
|
84
|
+
onClick={() => onActiveLink(link)}
|
|
85
|
+
>
|
|
86
|
+
<span>{link.title}</span>
|
|
87
|
+
<span className="blockbite--preview-link">{link.url}</span>
|
|
88
|
+
<span className="blockbite--preview-link">{link.post_type}</span>
|
|
89
|
+
</span>
|
|
90
|
+
</Wrap>
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
return list;
|
|
94
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { MediaUpload, MediaUploadCheck } from '@wordpress/block-editor';
|
|
2
|
+
|
|
3
|
+
import { Button, PanelRow } from '@wordpress/components';
|
|
4
|
+
|
|
5
|
+
import { useEffect, useState } from '@wordpress/element';
|
|
6
|
+
import { __ } from '@wordpress/i18n';
|
|
7
|
+
|
|
8
|
+
export default function MediaPicker({ mediaProps, mediaCallback }) {
|
|
9
|
+
const allowedTypes = [
|
|
10
|
+
'image',
|
|
11
|
+
'video',
|
|
12
|
+
'image/svg+xml',
|
|
13
|
+
'svg',
|
|
14
|
+
'text/plain',
|
|
15
|
+
'application/json',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
// toggles
|
|
19
|
+
const [stateMedia, setStateMedia] = useState({
|
|
20
|
+
id: null,
|
|
21
|
+
url: '',
|
|
22
|
+
sizes: [],
|
|
23
|
+
alt: '',
|
|
24
|
+
type: '',
|
|
25
|
+
width: 0,
|
|
26
|
+
height: 0,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (mediaProps) {
|
|
31
|
+
setStateMedia({ ...mediaProps });
|
|
32
|
+
}
|
|
33
|
+
}, [mediaProps]);
|
|
34
|
+
|
|
35
|
+
// image handlers
|
|
36
|
+
const removeMedia = () => {
|
|
37
|
+
const mediaObj = {
|
|
38
|
+
mediaId: 0,
|
|
39
|
+
mediaUrl: '',
|
|
40
|
+
};
|
|
41
|
+
mediaCallback({ ...mediaObj });
|
|
42
|
+
};
|
|
43
|
+
// onselect media
|
|
44
|
+
const onSelectMedia = (media) => {
|
|
45
|
+
const safeSizes = {
|
|
46
|
+
thumbnail: null,
|
|
47
|
+
medium: null,
|
|
48
|
+
large: null,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Normalize Sizes
|
|
52
|
+
if (media?.sizes) {
|
|
53
|
+
Object.keys(media.sizes).map((key) => {
|
|
54
|
+
const sizeObject = media.sizes[key];
|
|
55
|
+
|
|
56
|
+
if (key === 'thumbnail') {
|
|
57
|
+
safeSizes.thumbnail = sizeObject.url;
|
|
58
|
+
} else if (sizeObject.width < 768 || sizeObject.height < 768) {
|
|
59
|
+
safeSizes.thumbnail = sizeObject.url;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (key === 'medium') {
|
|
63
|
+
safeSizes.medium = sizeObject.url;
|
|
64
|
+
} else if (
|
|
65
|
+
(sizeObject.width > 1024 && sizeObject.width < 1024) ||
|
|
66
|
+
(sizeObject.height < 1536 && sizeObject.height > 1536)
|
|
67
|
+
) {
|
|
68
|
+
safeSizes.medium = sizeObject.url;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (key === 'large') {
|
|
72
|
+
safeSizes.large = sizeObject.url;
|
|
73
|
+
} else if (sizeObject.width > 1536 || sizeObject.height > 1536) {
|
|
74
|
+
safeSizes.large = sizeObject.url;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return null;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// if type ends .lottie then it is a lottie file
|
|
82
|
+
if (media.url.endsWith('.json')) {
|
|
83
|
+
media.type = 'lottie';
|
|
84
|
+
}
|
|
85
|
+
// if type ends with .svg then it is an svg file
|
|
86
|
+
if (media.url.endsWith('.svg')) {
|
|
87
|
+
media.type = 'svg';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const mediaObj = {
|
|
91
|
+
id: media.id,
|
|
92
|
+
url: media.url,
|
|
93
|
+
sizes: safeSizes,
|
|
94
|
+
alt: media.alt,
|
|
95
|
+
type: media.type,
|
|
96
|
+
width: media.width,
|
|
97
|
+
height: media.height,
|
|
98
|
+
};
|
|
99
|
+
mediaCallback({ ...mediaObj });
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<PanelRow>
|
|
104
|
+
{stateMedia.id !== null && (
|
|
105
|
+
<MediaUploadCheck>
|
|
106
|
+
<MediaUpload
|
|
107
|
+
onSelect={onSelectMedia}
|
|
108
|
+
value={stateMedia.id}
|
|
109
|
+
allowedTypes={allowedTypes}
|
|
110
|
+
render={({ open }) => (
|
|
111
|
+
<Button
|
|
112
|
+
className={
|
|
113
|
+
stateMedia.id === 0
|
|
114
|
+
? 'editor-post-featured-image__toggle'
|
|
115
|
+
: 'editor-post-featured-image__preview'
|
|
116
|
+
}
|
|
117
|
+
onClick={open}
|
|
118
|
+
>
|
|
119
|
+
{stateMedia.id === 0 && __('Choose Media', 'blockbite')}
|
|
120
|
+
{stateMedia.id && stateMedia.type === 'image' ? (
|
|
121
|
+
<div className="blockbite--editor-visual-image">
|
|
122
|
+
<img
|
|
123
|
+
alt={
|
|
124
|
+
stateMedia.alt
|
|
125
|
+
? stateMedia.alt
|
|
126
|
+
: __('Image', 'blockbite')
|
|
127
|
+
}
|
|
128
|
+
src={stateMedia.url}
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
) : (
|
|
132
|
+
'Add media'
|
|
133
|
+
)}
|
|
134
|
+
</Button>
|
|
135
|
+
)}
|
|
136
|
+
/>
|
|
137
|
+
</MediaUploadCheck>
|
|
138
|
+
)}
|
|
139
|
+
{stateMedia.id !== 0 && (
|
|
140
|
+
<MediaUploadCheck>
|
|
141
|
+
<MediaUpload
|
|
142
|
+
title={__('Replace media', 'blockbite')}
|
|
143
|
+
value={stateMedia.id}
|
|
144
|
+
onSelect={onSelectMedia}
|
|
145
|
+
allowedTypes={allowedTypes}
|
|
146
|
+
render={({ open }) => (
|
|
147
|
+
<Button onClick={open}>{__('Replace media', 'blockbite')}</Button>
|
|
148
|
+
)}
|
|
149
|
+
/>
|
|
150
|
+
</MediaUploadCheck>
|
|
151
|
+
)}
|
|
152
|
+
{stateMedia.id !== 0 && (
|
|
153
|
+
<MediaUploadCheck>
|
|
154
|
+
<Button onClick={() => removeMedia()} isDestructive>
|
|
155
|
+
{__('Remove media', 'blockbite')}
|
|
156
|
+
</Button>
|
|
157
|
+
</MediaUploadCheck>
|
|
158
|
+
)}
|
|
159
|
+
</PanelRow>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import ButtonToggleGroup from "./ButtonToggle";
|
|
2
|
+
import { DropdownPicker } from "./DropdownPicker";
|
|
3
|
+
import { Popover } from "./Popover";
|
|
4
|
+
import RangeControl from "./RangeSlider";
|
|
5
|
+
import { TextControl } from "./TextControl";
|
|
6
|
+
import { Wrap } from "./Wrap";
|
|
7
|
+
import { useEffect, useState } from "@wordpress/element";
|
|
8
|
+
|
|
9
|
+
import ColumnSpacingIcon from "@blockbite/icons/dist/ColumnSpacing";
|
|
10
|
+
import DesktopIcon from "@blockbite/icons/dist/Desktop";
|
|
11
|
+
import GridIcon from "@blockbite/icons/dist/Grid";
|
|
12
|
+
import PercentageIcon from "@blockbite/icons/dist/Percentage";
|
|
13
|
+
import SliderIcon from "@blockbite/icons/dist/Slider";
|
|
14
|
+
import TailwindUnitIcon from "@blockbite/icons/dist/Tailwind";
|
|
15
|
+
import has from "lodash/has";
|
|
16
|
+
|
|
17
|
+
type MetricsControlProps = {
|
|
18
|
+
defaultUnit: string;
|
|
19
|
+
defaultValue: string;
|
|
20
|
+
units?:
|
|
21
|
+
| string[]
|
|
22
|
+
| "native"
|
|
23
|
+
| "percent"
|
|
24
|
+
| "grid"
|
|
25
|
+
| "arbitrary"
|
|
26
|
+
| "fluid"
|
|
27
|
+
| "screen"
|
|
28
|
+
| "all";
|
|
29
|
+
inputClassName?: string;
|
|
30
|
+
onValueChange: (value: string) => void;
|
|
31
|
+
onUnitChange: (unit: string) => void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const MetricsControl: React.FC<MetricsControlProps> = ({
|
|
35
|
+
defaultUnit,
|
|
36
|
+
defaultValue,
|
|
37
|
+
onValueChange,
|
|
38
|
+
onUnitChange,
|
|
39
|
+
inputClassName = "w-[75px]",
|
|
40
|
+
}) => {
|
|
41
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
42
|
+
const [currentOptions, setCurrentOptions] = useState<string[]>([]);
|
|
43
|
+
|
|
44
|
+
// Use local state for defaultUnit and defaultValue
|
|
45
|
+
const [unit, setUnit] = useState(defaultUnit);
|
|
46
|
+
const [value, setValue] = useState<string>("");
|
|
47
|
+
const [resetValue, setResetValue] = useState<string | number>(defaultValue);
|
|
48
|
+
|
|
49
|
+
// Set initial state from props
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (defaultUnit) {
|
|
52
|
+
setUnit(defaultUnit);
|
|
53
|
+
}
|
|
54
|
+
if (defaultValue) {
|
|
55
|
+
setValue(defaultValue.toString());
|
|
56
|
+
}
|
|
57
|
+
}, [defaultUnit, defaultValue]);
|
|
58
|
+
|
|
59
|
+
// Save last value after popover close to support "reset" functionality
|
|
60
|
+
// Only apply to arbitrary units
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!isVisible && unit === "arbitrary") {
|
|
63
|
+
setResetValue(value);
|
|
64
|
+
}
|
|
65
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
66
|
+
}, [isVisible]);
|
|
67
|
+
|
|
68
|
+
const showOptions = (u: string) => {
|
|
69
|
+
if (u !== "arbitrary" && has(bb.codex.units.spacing, u)) {
|
|
70
|
+
const options =
|
|
71
|
+
bb.codex.units.spacing[u as keyof typeof bb.codex.units.spacing];
|
|
72
|
+
setCurrentOptions(
|
|
73
|
+
Array.isArray(options) ? options : Object.keys(options)
|
|
74
|
+
);
|
|
75
|
+
} else {
|
|
76
|
+
setCurrentOptions([]);
|
|
77
|
+
}
|
|
78
|
+
setIsVisible(true);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const unitOptions = [
|
|
82
|
+
{
|
|
83
|
+
icon: <TailwindUnitIcon />,
|
|
84
|
+
label: "Tailwind CSS Units",
|
|
85
|
+
value: "native",
|
|
86
|
+
},
|
|
87
|
+
{ icon: <PercentageIcon />, label: "Percentage Units", value: "percent" },
|
|
88
|
+
{ icon: <GridIcon />, label: "Grid Units", value: "grid" },
|
|
89
|
+
{ icon: <DesktopIcon />, label: "Screen Units", value: "screen" },
|
|
90
|
+
{ icon: <SliderIcon />, label: "Pixel Units", value: "arbitrary" },
|
|
91
|
+
{ icon: <ColumnSpacingIcon />, label: "Fluid Units", value: "fluid" },
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<Wrap className="relative flex flex-col items-baseline">
|
|
96
|
+
<TextControl
|
|
97
|
+
inputClassName={inputClassName}
|
|
98
|
+
defaultValue={
|
|
99
|
+
// Remove the "b_" prefix for grid units
|
|
100
|
+
unit === "grid" ? value.replace("b_", "") : value
|
|
101
|
+
}
|
|
102
|
+
onClick={() => unit !== "arbitrary" && showOptions(unit)}
|
|
103
|
+
onChange={(newValue) => {
|
|
104
|
+
setValue(newValue);
|
|
105
|
+
onValueChange(newValue);
|
|
106
|
+
}}
|
|
107
|
+
readOnly={unit !== "arbitrary"}
|
|
108
|
+
>
|
|
109
|
+
<Popover
|
|
110
|
+
visible={isVisible}
|
|
111
|
+
position="bottom left"
|
|
112
|
+
className="w-[300px] bg-white shadow-sm"
|
|
113
|
+
onVisibleChange={setIsVisible}
|
|
114
|
+
>
|
|
115
|
+
{unit === "arbitrary" ? (
|
|
116
|
+
<RangeControl
|
|
117
|
+
defaultValue={value}
|
|
118
|
+
label="Pixel Value"
|
|
119
|
+
min={0}
|
|
120
|
+
max={100}
|
|
121
|
+
gridMode={true}
|
|
122
|
+
showTooltip={false}
|
|
123
|
+
allowReset={true}
|
|
124
|
+
resetFallbackValue={
|
|
125
|
+
isNaN(resetValue.toString() as any)
|
|
126
|
+
? 0
|
|
127
|
+
: Number(resetValue) / 16
|
|
128
|
+
}
|
|
129
|
+
onValueChange={(newValue: string) => {
|
|
130
|
+
setValue(newValue);
|
|
131
|
+
onValueChange(newValue);
|
|
132
|
+
}}
|
|
133
|
+
/>
|
|
134
|
+
) : (
|
|
135
|
+
<ButtonToggleGroup
|
|
136
|
+
className="mt-4"
|
|
137
|
+
options={currentOptions.map((option) => ({
|
|
138
|
+
value: option,
|
|
139
|
+
label:
|
|
140
|
+
unit === "grid"
|
|
141
|
+
? option.toString().replace("b_", "")
|
|
142
|
+
: option,
|
|
143
|
+
onClick: () => {
|
|
144
|
+
setValue(option);
|
|
145
|
+
onValueChange(option);
|
|
146
|
+
},
|
|
147
|
+
}))}
|
|
148
|
+
size="small"
|
|
149
|
+
defaultPressed={value?.toString() || ""}
|
|
150
|
+
onPressedChange={(newValue: string) => {
|
|
151
|
+
setValue(newValue);
|
|
152
|
+
onValueChange(newValue);
|
|
153
|
+
}}
|
|
154
|
+
/>
|
|
155
|
+
)}
|
|
156
|
+
</Popover>
|
|
157
|
+
|
|
158
|
+
<DropdownPicker
|
|
159
|
+
className="h-[32px]"
|
|
160
|
+
defaultValue={unit}
|
|
161
|
+
options={unitOptions}
|
|
162
|
+
onPressedChange={(selectedUnit) => {
|
|
163
|
+
if (selectedUnit === "reset") {
|
|
164
|
+
setValue("");
|
|
165
|
+
onValueChange("");
|
|
166
|
+
setIsVisible(false);
|
|
167
|
+
} else {
|
|
168
|
+
setUnit(selectedUnit);
|
|
169
|
+
onUnitChange(selectedUnit);
|
|
170
|
+
showOptions(selectedUnit);
|
|
171
|
+
}
|
|
172
|
+
}}
|
|
173
|
+
/>
|
|
174
|
+
</TextControl>
|
|
175
|
+
</Wrap>
|
|
176
|
+
);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export default MetricsControl;
|