@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
@@ -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;