@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/src/Modal.tsx ADDED
@@ -0,0 +1,171 @@
1
+ import {
2
+ Children,
3
+ createContext,
4
+ isValidElement,
5
+ useContext,
6
+ useEffect,
7
+ useState,
8
+ } from '@wordpress/element';
9
+
10
+ import { Modal as WordpressModal } from '@wordpress/components';
11
+ import classNames from 'classnames';
12
+
13
+ type ModalContextType = {
14
+ closeModal: () => void;
15
+ openModal: () => void;
16
+ };
17
+
18
+ const ModalContext = createContext<ModalContextType | null>(null);
19
+
20
+ export const useModalContext = () => {
21
+ const context = useContext(ModalContext);
22
+ if (!context) {
23
+ throw new Error('useModalContext must be used within a ModalProvider');
24
+ }
25
+ return context;
26
+ };
27
+
28
+ type ModalProps = {
29
+ children: React.ReactElement | React.ReactElement[];
30
+ defaultOpen: boolean;
31
+ onOpenChange: (checked: boolean) => void;
32
+ title: string;
33
+ className?: string;
34
+ };
35
+
36
+ const Modal: React.FC<ModalProps> = ({
37
+ children,
38
+ defaultOpen,
39
+ onOpenChange,
40
+ title,
41
+ className,
42
+ }) => {
43
+ const [isOpen, setOpen] = useState(false);
44
+
45
+ // Sync `isOpen` with `defaultOpen` when `defaultOpen` changes
46
+ useEffect(() => {
47
+ setOpen(defaultOpen);
48
+ }, [defaultOpen]);
49
+
50
+ const openModal = () => setOpen(true);
51
+ const closeModal = () => setOpen(false);
52
+
53
+ useEffect(() => {
54
+ onOpenChange(isOpen);
55
+ }, [isOpen, onOpenChange]);
56
+
57
+ return (
58
+ <ModalContext.Provider value={{ closeModal, openModal }}>
59
+ {/* Render MOdalTrigger */}
60
+ {Children.map(children, (child) => {
61
+ return isValidElement(child) && child.type === ModalTrigger
62
+ ? child
63
+ : null;
64
+ })}
65
+ {isOpen && (
66
+ <WordpressModal
67
+ className={classNames('blockbite-ui__modal bb_', className)}
68
+ onRequestClose={closeModal}
69
+ title={title}
70
+ >
71
+ <div className="relative">
72
+ {/* Render ModalHeader */}
73
+ {Children.map(children, (child) => {
74
+ return isValidElement(child) && child.type === ModalHeader
75
+ ? child
76
+ : null;
77
+ })}
78
+ {/* Render ModalContent */}
79
+ {Children.map(children, (child) => {
80
+ return isValidElement(child) && child.type === ModalContent
81
+ ? child
82
+ : null;
83
+ })}
84
+ {/* Render ModalFooter */}
85
+ {Children.map(children, (child) => {
86
+ return isValidElement(child) && child.type === ModalFooter
87
+ ? child
88
+ : null;
89
+ })}
90
+ </div>
91
+ </WordpressModal>
92
+ )}
93
+ </ModalContext.Provider>
94
+ );
95
+ };
96
+
97
+ // Header component for modal
98
+ const ModalHeader: React.FC<{
99
+ children: React.ReactNode;
100
+ className?: string;
101
+ }> = ({ children, className = '', ...props }) => (
102
+ <div
103
+ className={classNames(
104
+ 'flex flex-col space-y-1.5 text-center sm:text-left',
105
+ className
106
+ )}
107
+ {...props}
108
+ >
109
+ {children}
110
+ </div>
111
+ );
112
+
113
+ // Footer component for modal
114
+ const ModalFooter: React.FC<{
115
+ children: React.ReactNode;
116
+ className?: string;
117
+ }> = ({ children, className = '', ...props }) => (
118
+ <div
119
+ className={classNames(
120
+ 'border-gray-light fixed bottom-0 left-0 right-0 flex h-[4rem] flex-row items-center justify-end space-x-2 border-t bg-white px-4',
121
+ className
122
+ )}
123
+ {...props}
124
+ >
125
+ {children}
126
+ </div>
127
+ );
128
+
129
+ // Description component for modal
130
+ const ModalContent: React.FC<{
131
+ children: React.ReactNode;
132
+ className?: string;
133
+ }> = ({ children, className = '', ...props }) => (
134
+ <div
135
+ className={classNames('text-gray-medium px-4 pb-4 text-sm', className)}
136
+ {...props}
137
+ >
138
+ {children}
139
+ </div>
140
+ );
141
+
142
+ const ModalClose: React.FC<{
143
+ children: React.ReactNode;
144
+ }> = ({ children }) => {
145
+ const { closeModal } = useModalContext();
146
+ return (
147
+ <div onClick={closeModal} className="cursor-pointer">
148
+ {children}
149
+ </div>
150
+ );
151
+ };
152
+
153
+ const ModalTrigger: React.FC<{
154
+ children: React.ReactNode;
155
+ }> = ({ children }) => {
156
+ const { openModal } = useModalContext();
157
+ return (
158
+ <div onClick={openModal} className="cursor-pointer">
159
+ {children}
160
+ </div>
161
+ );
162
+ };
163
+
164
+ export {
165
+ Modal,
166
+ ModalClose,
167
+ ModalContent,
168
+ ModalFooter,
169
+ ModalHeader,
170
+ ModalTrigger,
171
+ };
@@ -0,0 +1,68 @@
1
+ import { createPortal, useEffect, useMemo } from '@wordpress/element';
2
+
3
+ function copyStyles(sourceDoc, targetDoc) {
4
+ Array.from(sourceDoc.styleSheets).forEach((styleSheet: any) => {
5
+ try {
6
+ if (styleSheet.cssRules) {
7
+ const newStyleEl = targetDoc.createElement('style');
8
+ Array.from(styleSheet.cssRules).forEach((cssRule: any) => {
9
+ newStyleEl.appendChild(targetDoc.createTextNode(cssRule.cssText));
10
+ });
11
+ targetDoc.head.appendChild(newStyleEl);
12
+ } else if (styleSheet.href) {
13
+ const newLinkEl = targetDoc.createElement('link');
14
+ newLinkEl.rel = 'stylesheet';
15
+ newLinkEl.href = styleSheet.href;
16
+ targetDoc.head.appendChild(newLinkEl);
17
+ }
18
+ } catch (e) {
19
+ if (styleSheet.href) {
20
+ const newLinkEl = targetDoc.createElement('link');
21
+ newLinkEl.rel = 'stylesheet';
22
+ newLinkEl.href = styleSheet.href;
23
+ targetDoc.head.appendChild(newLinkEl);
24
+ } else {
25
+ // eslint-disable-next-line no-console
26
+ console.warn('Error copying styles:', e);
27
+ }
28
+ }
29
+ });
30
+ }
31
+
32
+ type Props = {
33
+ windowInstance: Window;
34
+ onClose: () => void;
35
+ children: React.ReactNode;
36
+ };
37
+
38
+ export const NewWindowPortal = ({
39
+ windowInstance,
40
+ onClose,
41
+ children,
42
+ }: Props) => {
43
+ const containerEl = useMemo(() => document.createElement('div'), []);
44
+
45
+ useEffect(() => {
46
+ const win = windowInstance;
47
+
48
+ win.document.body.innerHTML = '';
49
+ win.document.body.appendChild(containerEl);
50
+ win.document.body.classList.add('bb_');
51
+ copyStyles(document, win.document);
52
+ win.document.title = document.title || 'Blockbite Editor';
53
+
54
+ const handleBeforeUnload = () => {
55
+ setTimeout(onClose, 100);
56
+ };
57
+
58
+ win.addEventListener('beforeunload', handleBeforeUnload);
59
+
60
+ return () => {
61
+ win.removeEventListener('beforeunload', handleBeforeUnload);
62
+ };
63
+ // eslint-disable-next-line react-hooks/exhaustive-deps
64
+ }, [windowInstance]);
65
+
66
+ // Typecasting: https://github.com/vercel/next.js/discussions/64753
67
+ return createPortal(children as any, containerEl);
68
+ };
package/src/Notice.tsx ADDED
@@ -0,0 +1,32 @@
1
+ import { Wrap } from "./Wrap";
2
+ import { Notice as WordpressNotice } from "@wordpress/components";
3
+ import { useState } from "@wordpress/element";
4
+ import classNames from "classnames";
5
+
6
+ type NoticeProps = {
7
+ children: React.ReactNode;
8
+ className?: string;
9
+ status?: "success" | "error" | "warning" | "info";
10
+ };
11
+
12
+ export const Notice = ({
13
+ children,
14
+ status = "success",
15
+ className,
16
+ }: NoticeProps) => {
17
+ const [showNotice, setShowNotice] = useState(true);
18
+
19
+ return (
20
+ <Wrap onClick={() => setShowNotice(false)}>
21
+ {showNotice && (
22
+ <WordpressNotice
23
+ status={status}
24
+ className={classNames(className)}
25
+ onRemove={() => setShowNotice(false)}
26
+ >
27
+ {children}
28
+ </WordpressNotice>
29
+ )}
30
+ </Wrap>
31
+ );
32
+ };
@@ -0,0 +1,53 @@
1
+ import {
2
+ Button,
3
+ __experimentalInputControl as InputControl,
4
+ __experimentalInputControlSuffixWrapper as InputControlSuffixWrapper,
5
+ } from '@wordpress/components';
6
+ import { useState } from '@wordpress/element';
7
+ import { Path, SVG } from '@wordpress/primitives';
8
+
9
+ type PasswordInputProps = {
10
+ label: string;
11
+ value: string;
12
+ onChange: (value: string) => void;
13
+ [key: string]: any;
14
+ };
15
+
16
+ export const PasswordInput = ({
17
+ label,
18
+ value,
19
+ onChange,
20
+ ...rest
21
+ }: PasswordInputProps) => {
22
+ const [showPassword, setShowPassword] = useState(false);
23
+
24
+ return (
25
+ <InputControl
26
+ label={label}
27
+ value={value}
28
+ type={showPassword ? 'text' : 'password'}
29
+ suffix={
30
+ <InputControlSuffixWrapper variant="control">
31
+ <Button
32
+ icon={
33
+ <SVG viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
34
+ <Path
35
+ d={
36
+ showPassword
37
+ ? 'M3.99961 13C4.67043 13.3354 4.6703 13.3357 4.67017 13.3359L4.67298 13.3305C4.67621 13.3242 4.68184 13.3135 4.68988 13.2985C4.70595 13.2686 4.7316 13.2218 4.76695 13.1608C4.8377 13.0385 4.94692 12.8592 5.09541 12.6419C5.39312 12.2062 5.84436 11.624 6.45435 11.0431C7.67308 9.88241 9.49719 8.75 11.9996 8.75C14.502 8.75 16.3261 9.88241 17.5449 11.0431C18.1549 11.624 18.6061 12.2062 18.9038 12.6419C19.0523 12.8592 19.1615 13.0385 19.2323 13.1608C19.2676 13.2218 19.2933 13.2686 19.3093 13.2985C19.3174 13.3135 19.323 13.3242 19.3262 13.3305L19.3291 13.3359C19.3289 13.3357 19.3288 13.3354 19.9996 13C20.6704 12.6646 20.6703 12.6643 20.6701 12.664L20.6697 12.6632L20.6688 12.6614L20.6662 12.6563L20.6583 12.6408C20.6517 12.6282 20.6427 12.6108 20.631 12.5892C20.6078 12.5459 20.5744 12.4852 20.5306 12.4096C20.4432 12.2584 20.3141 12.0471 20.1423 11.7956C19.7994 11.2938 19.2819 10.626 18.5794 9.9569C17.1731 8.61759 14.9972 7.25 11.9996 7.25C9.00203 7.25 6.82614 8.61759 5.41987 9.9569C4.71736 10.626 4.19984 11.2938 3.85694 11.7956C3.68511 12.0471 3.55605 12.2584 3.4686 12.4096C3.42484 12.4852 3.39142 12.5459 3.36818 12.5892C3.35656 12.6108 3.34748 12.6282 3.34092 12.6408L3.33297 12.6563L3.33041 12.6614L3.32948 12.6632L3.32911 12.664C3.32894 12.6643 3.32879 12.6646 3.99961 13ZM11.9996 16C13.9326 16 15.4996 14.433 15.4996 12.5C15.4996 10.567 13.9326 9 11.9996 9C10.0666 9 8.49961 10.567 8.49961 12.5C8.49961 14.433 10.0666 16 11.9996 16Z'
38
+ : 'M20.7 12.7s0-.1-.1-.2c0-.2-.2-.4-.4-.6-.3-.5-.9-1.2-1.6-1.8-.7-.6-1.5-1.3-2.6-1.8l-.6 1.4c.9.4 1.6 1 2.1 1.5.6.6 1.1 1.2 1.4 1.6.1.2.3.4.3.5v.1l.7-.3.7-.3Zm-5.2-9.3-1.8 4c-.5-.1-1.1-.2-1.7-.2-3 0-5.2 1.4-6.6 2.7-.7.7-1.2 1.3-1.6 1.8-.2.3-.3.5-.4.6 0 0 0 .1-.1.2s0 0 .7.3l.7.3V13c0-.1.2-.3.3-.5.3-.4.7-1 1.4-1.6 1.2-1.2 3-2.3 5.5-2.3H13v.3c-.4 0-.8-.1-1.1-.1-1.9 0-3.5 1.6-3.5 3.5s.6 2.3 1.6 2.9l-2 4.4.9.4 7.6-16.2-.9-.4Zm-3 12.6c1.7-.2 3-1.7 3-3.5s-.2-1.4-.6-1.9L12.4 16Z'
39
+ }
40
+ />
41
+ </SVG>
42
+ }
43
+ label="Show password"
44
+ onClick={() => setShowPassword((prev) => !prev)}
45
+ size="small"
46
+ />
47
+ </InputControlSuffixWrapper>
48
+ }
49
+ onChange={onChange}
50
+ {...rest}
51
+ />
52
+ );
53
+ };
@@ -0,0 +1,68 @@
1
+ import Cross2Icon from '@blockbite/icons/dist/Cross2';
2
+ import { Button, Popover as WordpressPopover } from '@wordpress/components';
3
+ import { useEffect, useState } from '@wordpress/element';
4
+
5
+ type PopoverProps = {
6
+ children: React.ReactNode;
7
+ className?: string;
8
+ defaultValue?: string;
9
+ value?: string;
10
+ position?: any;
11
+ visible?: boolean;
12
+ onClick?: () => void;
13
+ onVisibleChange?: (value: boolean) => void;
14
+ };
15
+
16
+ export const Popover: React.FC<PopoverProps> = ({
17
+ children,
18
+ className,
19
+ position,
20
+ visible,
21
+ onClick,
22
+ onVisibleChange,
23
+ }) => {
24
+ const [isVisible, setIsVisible] = useState<boolean>(!!visible);
25
+
26
+ useEffect(() => {
27
+ if (visible !== undefined) {
28
+ setIsVisible(visible);
29
+ }
30
+ }, [visible]);
31
+
32
+ const toggleVisible = () => {
33
+ const newValue = !isVisible;
34
+ setIsVisible(newValue);
35
+ onVisibleChange && onVisibleChange(newValue);
36
+ };
37
+
38
+ return (
39
+ <>
40
+ {isVisible && (
41
+ <WordpressPopover
42
+ position={position}
43
+ className="blockbite-ui__popover bb_"
44
+ onClick={onClick}
45
+ onFocusOutside={() => {
46
+ setIsVisible(false);
47
+ onVisibleChange && onVisibleChange(false);
48
+ }}
49
+ >
50
+ <div className={className}>
51
+ <div className="relative h-full w-full p-4">
52
+ <Button
53
+ onClick={toggleVisible}
54
+ size="small"
55
+ className="close-button absolute right-0 top-0"
56
+ >
57
+ <Cross2Icon />
58
+ </Button>
59
+ {children}
60
+ </div>
61
+ </div>
62
+ </WordpressPopover>
63
+ )}
64
+ </>
65
+ );
66
+ };
67
+
68
+ export default Popover;
@@ -0,0 +1,68 @@
1
+ import { Wrap } from "./Wrap";
2
+ import { useEffect, useState } from "@wordpress/element";
3
+
4
+ import { RangeControl as WordpressRangeControl } from "@wordpress/components";
5
+
6
+ export type RangeControlType = {
7
+ defaultValue: string;
8
+ label: string;
9
+ min: number;
10
+ max: number;
11
+ withInputField?: boolean;
12
+ onValueChange: (value: string) => void;
13
+ gridMode?: boolean;
14
+ showTooltip?: boolean;
15
+ [key: string]: any;
16
+ };
17
+
18
+ const RangeControl: React.FC<RangeControlType> = ({
19
+ defaultValue,
20
+ label,
21
+ min = 0,
22
+ max = 2000,
23
+ withInputField = false,
24
+ onValueChange,
25
+ gridMode = false,
26
+ showTooltip = false,
27
+ ...props
28
+ }) => {
29
+ const [resetFallbackValue] = useState(0);
30
+ const [rangeValue, setRangeValue] = useState<number>(0);
31
+
32
+ useEffect(() => {
33
+ setRangeValue(Math.round(parseInt(defaultValue) / (gridMode ? 16 : 1)));
34
+ // eslint-disable-next-line react-hooks/exhaustive-deps
35
+ }, [defaultValue]);
36
+ /**
37
+ * Set the range slider values
38
+ * @param modifier
39
+ */
40
+
41
+ // convert back to arbitrary unit
42
+ function handleRangeUpdate(value: number) {
43
+ const gridValue = value * (gridMode ? 16 : 1);
44
+ onValueChange(gridValue.toString());
45
+ }
46
+
47
+ return (
48
+ <Wrap className="flex min-w-[240px] flex-col">
49
+ <WordpressRangeControl
50
+ label={label}
51
+ value={rangeValue}
52
+ min={min}
53
+ max={max}
54
+ showTooltip={showTooltip}
55
+ withInputField={withInputField}
56
+ onChange={(value) => {
57
+ setRangeValue(value);
58
+ handleRangeUpdate(value);
59
+ }}
60
+ resetFallbackValue={resetFallbackValue}
61
+ {...props}
62
+ />
63
+ <span>{gridMode ? `${rangeValue * 16}px` : null} </span>
64
+ </Wrap>
65
+ );
66
+ };
67
+
68
+ export default RangeControl;
@@ -0,0 +1,42 @@
1
+ export default function ResponsiveImage(props) {
2
+ const { media, mediaClass } = props;
3
+ const { url, alt, sizes } = media;
4
+
5
+ if (sizes === undefined) {
6
+ return <img src={url} alt={alt} className={`bf_media ${mediaClass}`} />;
7
+ }
8
+
9
+ const { thumbnail, medium, large } = sizes;
10
+ const thumbnailImg = thumbnail || url;
11
+ const mediumImg = medium || url;
12
+ const largeImg = large || url;
13
+
14
+ return (
15
+ <picture className="bf_responsive-media">
16
+ {thumbnailImg ? (
17
+ <source
18
+ media="(max-width: 768px)"
19
+ srcSet={thumbnailImg}
20
+ className={`bf_media ${mediaClass}`}
21
+ />
22
+ ) : null}
23
+ {mediumImg ? (
24
+ <source
25
+ media="(min-width: 1024px)"
26
+ srcSet={mediumImg}
27
+ className={`bf_media ${mediaClass}`}
28
+ />
29
+ ) : null}
30
+ {largeImg ? (
31
+ <source
32
+ media="(min-width: 1536px)"
33
+ srcSet={largeImg}
34
+ className={`bf_media ${mediaClass}`}
35
+ />
36
+ ) : null}
37
+ {mediumImg ? (
38
+ <img src={mediumImg} alt={alt} className={`bf_media ${mediaClass}`} />
39
+ ) : null}
40
+ </picture>
41
+ );
42
+ }
@@ -0,0 +1,20 @@
1
+ export default function ResponsiveVideo(props: any) {
2
+ const { media, mediaClass } = props;
3
+ const { url } = media;
4
+
5
+ const videoUrl = url;
6
+
7
+ return (
8
+ <video
9
+ className={`bf_responsive-media ${mediaClass}`}
10
+ preload="none"
11
+ playsInline
12
+ controls
13
+ autoPlay
14
+ muted
15
+ loop
16
+ >
17
+ <source src={videoUrl} type="video/mp4" />
18
+ </video>
19
+ );
20
+ }
@@ -0,0 +1,24 @@
1
+ import { Wrap } from "./Wrap";
2
+ import classNames from "classnames";
3
+
4
+ type ScrollListProps = {
5
+ children: React.ReactNode;
6
+ className?: string;
7
+ height?: "small" | "medium" | "large";
8
+ };
9
+
10
+ export const ScrollList: React.FC<ScrollListProps> = ({
11
+ children,
12
+ className = "",
13
+ height = "large",
14
+ }) => (
15
+ <Wrap
16
+ className={classNames("scrollbar relative overflow-y-scroll", className, {
17
+ "h-[400px]": height === "large",
18
+ "h-[300px]": height === "medium",
19
+ "h-[200px]": height === "small",
20
+ })}
21
+ >
22
+ {children}
23
+ </Wrap>
24
+ );