This commit is contained in:
Stefan Pejcic
2024-11-07 19:03:37 +01:00
parent c6df945ed5
commit 09f9f9502d
2472 changed files with 620417 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
import React from "react";
type Props = {
children: string;
};
export const ApplyStyles = ({ children }: Props) => {
React.useEffect(() => {
const element = document.createElement("style");
element.innerHTML = children;
document.head.appendChild(element);
return () => {
document.head.removeChild(element);
};
}, [children]);
return null;
};

View File

@@ -0,0 +1,73 @@
import React from "react";
import { DevtoolsSelector } from "./devtools-selector";
import { DevtoolsIcon } from "./icons/devtools-icon";
import { SelectorButtonIcon } from "./icons/selector-button";
import { ApplyStyles } from "./apply-styles";
type Props = {
onClick?: () => void;
groupHover?: boolean;
onSelectorHighlight: (name: string) => void;
selectorActive: boolean;
setSelectorActive: React.Dispatch<React.SetStateAction<boolean>>;
};
export const DevtoolsPin = ({
onClick,
onSelectorHighlight,
selectorActive,
setSelectorActive,
}: Props) => {
return (
<div role="button" className="devtools-selector-pin-box" onClick={onClick}>
<DevtoolsIcon />
<DevtoolsSelector
style={{
position: "absolute",
top: 5,
right: 18,
width: "16px",
height: "16px",
}}
icon={
<SelectorButtonIcon
width={16}
height={16}
style={{ pointerEvents: "none" }}
/>
}
onHighlight={onSelectorHighlight}
active={selectorActive}
setActive={setSelectorActive}
/>
<ApplyStyles>
{
/* css */ `
.devtools-selector-pin-box {
z-index: 9999;
position: relative;
user-select: none;
-webkit-user-select: none;
background: none;
border: none;
padding: 0;
margin: 0;
appearance: none;
padding-right: 1px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: #6C7793;
transition: color 0.1s ease-in-out;
}
.devtools-selector-pin-box:hover {
color: #0FBDBD;
}
`
}
</ApplyStyles>
</div>
);
};

View File

@@ -0,0 +1,67 @@
import React from "react";
import { useSelector } from "src/utilities/use-selector";
import { ApplyStyles } from "./apply-styles";
import { SelectableElements } from "./selectable-elements";
type Props = {
active: boolean;
setActive: React.Dispatch<React.SetStateAction<boolean>>;
onHighlight: (name: string) => void;
icon?: React.ReactNode;
style?: React.CSSProperties;
};
export const DevtoolsSelector = ({
active,
setActive,
onHighlight,
icon,
style,
}: Props) => {
const { selectableElements } = useSelector(active);
const onSelect = (name: string) => {
onHighlight(name);
setActive(false);
};
return (
<div style={style}>
<div
role="button"
title="Element Selector"
className="refine-devtools-selector-button"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
(document?.activeElement as HTMLElement)?.blur();
setActive((active) => !active);
}}
>
{icon}
</div>
{active && (
<SelectableElements elements={selectableElements} onSelect={onSelect} />
)}
<ApplyStyles>
{
/* css */ `
.refine-devtools-selector-button {
padding: 0;
margin: 0;
height: 100%;
width: 100%;
transform: rotate(0deg);
transition: transform 0.2s ease-in-out;
line-height: 1;
}
.refine-devtools-selector-button:hover {
transform: rotate(180deg);
}
`
}
</ApplyStyles>
</div>
);
};

View File

@@ -0,0 +1,33 @@
import * as React from "react";
import type { SVGProps } from "react";
export const ArrowUnionIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={16}
height={16}
viewBox="0 0 16 16"
fill="none"
{...props}
>
<path
fill="#303450"
stroke="url(#arrow-union-icon)"
d="M.5 8.495V15.5h15V8.495a4.5 4.5 0 0 1-3.816-2.483L9.341 1.33c-.553-1.105-2.13-1.105-2.683 0L4.317 6.012A4.5 4.5 0 0 1 .5 8.495Z"
/>
<defs>
<linearGradient
id="arrow-union-icon"
x1={8}
x2={8}
y1={0}
y2={10}
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#474E6B" />
<stop offset={0.9} stopColor="#474E6B" />
<stop offset={0.901} stopColor="#474E6B" stopOpacity={0} />
</linearGradient>
</defs>
</svg>
);

View File

@@ -0,0 +1,35 @@
import * as React from "react";
import type { SVGProps } from "react";
export const DevtoolsIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={157}
height={25}
viewBox="0 0 157 25"
fill="none"
{...props}
>
<g>
<path fill="#1D1E30" d="M17 1h123v24H17z" />
<path
fill="#1D1E30"
d="M6.265 9.205A12 12 0 0 1 17.649 1H25v24H1L6.265 9.205ZM150.735 9.205A12 12 0 0 0 139.351 1H132v24h24l-5.265-15.795Z"
/>
<path
fill="currentColor"
d="M25 14.333A1.333 1.333 0 1 1 25 17a1.333 1.333 0 0 1 0-2.667Z"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M23.211 20.578a4 4 0 0 0 3.578 0l4-2A4 4 0 0 0 33 15v-4a4 4 0 0 0-2.211-3.578l-4-2a4 4 0 0 0-3.578 0l-4 2A4 4 0 0 0 17 11v4a4 4 0 0 0 2.211 3.578l4 2Zm-.878-4.911a2.667 2.667 0 0 0 5.334 0v-5.334a2.667 2.667 0 0 0-5.334 0v5.334Z"
clipRule="evenodd"
/>
<path
fill="#CFD7E2"
d="M42.152 17a.287.287 0 0 1-.192-.072.287.287 0 0 1-.072-.192V9.032c0-.072.024-.132.072-.18a.264.264 0 0 1 .192-.084h4.2c.288 0 .56.056.816.168a2.135 2.135 0 0 1 1.14 1.128c.112.256.168.532.168.828v3.984c0 .296-.056.572-.168.828a2.135 2.135 0 0 1-1.14 1.128 2.014 2.014 0 0 1-.816.168h-4.2Zm1.38-1.644h2.82a.455.455 0 0 0 .336-.132.497.497 0 0 0 .132-.348v-3.984a.455.455 0 0 0-.132-.336.436.436 0 0 0-.336-.144h-2.82v4.944Zm13.18-5.196a.244.244 0 0 1-.253.252h-4.44v1.656h4.02c.072 0 .132.024.18.072a.227.227 0 0 1 .084.18v1.128a.264.264 0 0 1-.084.192.244.244 0 0 1-.18.072h-4.02v1.644h4.44c.072 0 .132.028.18.084a.244.244 0 0 1 .072.18v1.116a.287.287 0 0 1-.072.192.244.244 0 0 1-.18.072h-5.832a.244.244 0 0 1-.18-.072.287.287 0 0 1-.072-.192V9.032c0-.072.024-.132.072-.18a.227.227 0 0 1 .18-.084h5.832c.072 0 .132.028.18.084a.244.244 0 0 1 .072.18v1.128ZM63.014 17h-2.232a.387.387 0 0 1-.216-.072.356.356 0 0 1-.144-.168l-1.716-4.296a.853.853 0 0 1-.072-.24 1.783 1.783 0 0 1-.024-.264V9.032c0-.072.024-.132.072-.18a.227.227 0 0 1 .18-.084h1.128c.072 0 .132.028.18.084a.227.227 0 0 1 .084.18v2.616c0 .072.008.156.024.252s.04.176.072.24l1.284 3.216h.528l1.284-3.216a.853.853 0 0 0 .072-.24c.016-.096.024-.18.024-.252V9.032c0-.072.024-.132.072-.18a.264.264 0 0 1 .192-.084h1.128c.072 0 .132.028.18.084a.244.244 0 0 1 .072.18v2.928c0 .072-.008.16-.024.264a.853.853 0 0 1-.072.24l-1.716 4.296a.356.356 0 0 1-.144.168.387.387 0 0 1-.216.072ZM73.29 8.768c.072 0 .132.028.18.084a.227.227 0 0 1 .084.18v1.128a.227.227 0 0 1-.084.18.244.244 0 0 1-.18.072h-2.208v6.324a.264.264 0 0 1-.084.192.244.244 0 0 1-.18.072H69.69a.244.244 0 0 1-.18-.072.287.287 0 0 1-.072-.192v-6.324H67.23a.287.287 0 0 1-.192-.072.244.244 0 0 1-.072-.18V9.032c0-.072.024-.132.072-.18a.264.264 0 0 1 .192-.084h6.06Zm6.507.012c.296 0 .572.056.828.168a2.171 2.171 0 0 1 1.128 1.128c.112.256.168.528.168.816v3.996c0 .288-.056.56-.168.816a2.171 2.171 0 0 1-1.128 1.128 2.043 2.043 0 0 1-.828.168h-2.34c-.296 0-.572-.056-.828-.168a2.171 2.171 0 0 1-1.128-1.128 2.014 2.014 0 0 1-.168-.816v-3.996c0-.288.056-.56.168-.816a2.171 2.171 0 0 1 1.128-1.128c.256-.112.532-.168.828-.168h2.34Zm.48 2.112a.436.436 0 0 0-.144-.336.455.455 0 0 0-.336-.132h-2.34a.497.497 0 0 0-.348.132.455.455 0 0 0-.132.336v3.996c0 .136.044.248.132.336a.497.497 0 0 0 .348.132h2.34a.455.455 0 0 0 .336-.132.436.436 0 0 0 .144-.336v-3.996Zm7.888-2.112c.295 0 .572.056.828.168a2.171 2.171 0 0 1 1.128 1.128c.112.256.168.528.168.816v3.996c0 .288-.056.56-.168.816a2.171 2.171 0 0 1-1.128 1.128 2.043 2.043 0 0 1-.828.168h-2.34c-.297 0-.573-.056-.829-.168a2.171 2.171 0 0 1-1.127-1.128 2.014 2.014 0 0 1-.168-.816v-3.996c0-.288.056-.56.168-.816a2.171 2.171 0 0 1 1.127-1.128c.257-.112.532-.168.829-.168h2.34Zm.48 2.112a.436.436 0 0 0-.144-.336.455.455 0 0 0-.337-.132h-2.34a.497.497 0 0 0-.347.132.455.455 0 0 0-.133.336v3.996c0 .136.044.248.133.336a.497.497 0 0 0 .347.132h2.34a.455.455 0 0 0 .337-.132.436.436 0 0 0 .143-.336v-3.996ZM98.294 17H92.68a.287.287 0 0 1-.192-.072.287.287 0 0 1-.072-.192V9.032c0-.072.024-.132.072-.18a.264.264 0 0 1 .192-.084h1.116c.072 0 .132.028.18.084a.227.227 0 0 1 .084.18v6.324h4.236c.072 0 .132.028.18.084a.244.244 0 0 1 .072.18v1.116a.287.287 0 0 1-.072.192.244.244 0 0 1-.18.072Zm7.336-5.76a.287.287 0 0 1-.192-.072.287.287 0 0 1-.072-.192v-.084a.455.455 0 0 0-.132-.336.436.436 0 0 0-.336-.144h-2.352a.46.46 0 0 0-.336.144.455.455 0 0 0-.132.336v.696c0 .136.044.252.132.348a.482.482 0 0 0 .336.132h2.352c.288 0 .56.056.816.168a2.171 2.171 0 0 1 1.128 1.128c.112.256.168.528.168.816v.696c0 .296-.056.572-.168.828a2.171 2.171 0 0 1-1.128 1.128 2.014 2.014 0 0 1-.816.168h-2.352c-.288 0-.56-.056-.816-.168a2.171 2.171 0 0 1-1.128-1.128 2.043 2.043 0 0 1-.168-.828v-.084c0-.072.024-.132.072-.18a.264.264 0 0 1 .192-.084h1.116c.072 0 .132.028.18.084a.227.227 0 0 1 .084.18v.084c0 .136.044.252.132.348a.482.482 0 0 0 .336.132h2.352a.455.455 0 0 0 .336-.132.497.497 0 0 0 .132-.348v-.696a.455.455 0 0 0-.132-.336.455.455 0 0 0-.336-.132h-2.352c-.288 0-.56-.056-.816-.168a2.171 2.171 0 0 1-1.128-1.128 2.099 2.099 0 0 1-.168-.828v-.696c0-.296.056-.572.168-.828a2.171 2.171 0 0 1 1.128-1.128c.256-.112.528-.168.816-.168h2.352c.288 0 .56.056.816.168a2.171 2.171 0 0 1 1.128 1.128c.112.256.168.532.168.828v.084a.287.287 0 0 1-.072.192.244.244 0 0 1-.18.072h-1.128Z"
/>
</g>
</svg>
);

View File

@@ -0,0 +1,27 @@
import React from "react";
export const ResizeHandleIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg
width={10}
height={26}
viewBox="0 0 10 26"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<rect x={0.5} y={0.5} width={9} height={25} rx={4.5} fill="#1D1E30" />
<path
d="M7 5C7 6.10457 6.10457 7 5 7C3.89543 7 3 6.10457 3 5C3 3.89543 3.89543 3 5 3C6.10457 3 7 3.89543 7 5Z"
fill="#303450"
/>
<path
d="M7 13C7 14.1046 6.10457 15 5 15C3.89543 15 3 14.1046 3 13C3 11.8954 3.89543 11 5 11C6.10457 11 7 11.8954 7 13Z"
fill="#303450"
/>
<path
d="M7 21C7 22.1046 6.10457 23 5 23C3.89543 23 3 22.1046 3 21C3 19.8954 3.89543 19 5 19C6.10457 19 7 19.8954 7 21Z"
fill="#303450"
/>
<rect x={0.5} y={0.5} width={9} height={25} rx={4.5} stroke="#303450" />
</svg>
);

View File

@@ -0,0 +1,37 @@
import React from "react";
export const SelectorButtonIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={16}
height={16}
viewBox="0 0 16 16"
fill="none"
{...props}
>
<path
fill="#0FBDBD"
fillRule="evenodd"
d="M9 1a1 1 0 0 0-2 0v2.1A5.006 5.006 0 0 0 3.1 7H1a1 1 0 0 0 0 2h2.1A5.006 5.006 0 0 0 7 12.9V15a1 1 0 1 0 2 0v-2.1A5.006 5.006 0 0 0 12.9 9H15a1 1 0 1 0 0-2h-2.1A5.006 5.006 0 0 0 9 3.1V1Zm2 7a3 3 0 1 0-6 0 3 3 0 0 0 6 0Z"
clipRule="evenodd"
/>
</svg>
);
export const SelectorIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={16}
height={16}
viewBox="0 0 16 16"
fill="none"
{...props}
>
<path
fill="#14141F"
fillRule="evenodd"
d="M9 1a1 1 0 0 0-2 0v2.1A5.006 5.006 0 0 0 3.1 7H1a1 1 0 0 0 0 2h2.1A5.006 5.006 0 0 0 7 12.9V15a1 1 0 1 0 2 0v-2.1A5.006 5.006 0 0 0 12.9 9H15a1 1 0 1 0 0-2h-2.1A5.006 5.006 0 0 0 9 3.1V1Zm2 7a3 3 0 1 0-6 0 3 3 0 0 0 6 0Z"
clipRule="evenodd"
/>
</svg>
);

View File

@@ -0,0 +1,283 @@
import React from "react";
import type { Placement } from "src/interfaces/placement";
import {
getDefaultPanelSize,
getMaxPanelHeight,
getMaxPanelWidth,
getPanelPosition,
getPanelToggleTransforms,
MIN_PANEL_HEIGHT,
MIN_PANEL_WIDTH,
roundToEven,
} from "src/utilities";
import { ResizeHandleIcon } from "./icons/resize-handle-icon";
type Props = {
placement: Placement;
defaultWidth?: number;
minWidth?: number;
maxWidth?: number;
defaultHeight?: number;
minHeight?: number;
maxHeight?: number;
children: ({ resizing }: { resizing: string | null }) => React.ReactNode;
onResize?: (width: number, height: number) => void;
visible?: boolean;
};
export const ResizablePane = ({ placement, visible, children }: Props) => {
const [hover, setHover] = React.useState(false);
const [resizing, setResizing] = React.useState<
"lx" | "rx" | "ty" | "by" | null
>(null);
const [resizePosition, setResizePosition] = React.useState<{
x: number;
y: number;
} | null>(null);
const [panelSize, setPanelSize] = React.useState<
Record<"width" | "height", number>
>(() => {
const defaultSize = getDefaultPanelSize(placement);
return {
width: roundToEven(defaultSize.width),
height: roundToEven(defaultSize.height),
};
});
React.useEffect(() => {
const handleResize = () => {
setPanelSize((p) => {
const defaultSize = getDefaultPanelSize(placement, p);
return {
width: roundToEven(defaultSize.width),
height: roundToEven(defaultSize.height),
};
});
};
handleResize();
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, [placement]);
React.useEffect(() => {
const handleMouseUp = () => {
setResizing(null);
};
if (resizing !== null) {
window.addEventListener("mouseup", handleMouseUp);
return () => {
window.removeEventListener("mouseup", handleMouseUp);
};
}
return;
}, [resizing]);
React.useEffect(() => {
const currentCursor = document.body.style.cursor;
if (resizing?.includes("x")) {
document.body.style.cursor = "col-resize";
} else if (resizing?.includes("y")) {
document.body.style.cursor = "row-resize";
}
return () => {
document.body.style.cursor = currentCursor;
};
}, [resizing]);
React.useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (resizing?.[1] === "x") {
const diff = e.clientX - (resizePosition?.x ?? e.clientX);
const newWidth =
panelSize.width + (resizing === "lx" ? -diff : diff) * 2;
setPanelSize((p) => ({
...p,
width: roundToEven(
Math.min(
getMaxPanelWidth(placement),
Math.max(MIN_PANEL_WIDTH, newWidth),
),
),
}));
} else if (resizing?.[1] === "y") {
const diff = e.clientY - (resizePosition?.y ?? e.clientY);
const newHeight =
panelSize.height + (resizing === "ty" ? -diff : diff) * 1;
setPanelSize((p) => ({
...p,
height: roundToEven(
Math.min(
getMaxPanelHeight(placement),
Math.max(MIN_PANEL_HEIGHT, newHeight),
),
),
}));
}
};
if (resizing !== null) {
window.addEventListener("mousemove", handleMouseMove);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
};
}
return;
}, [resizing, placement]);
return (
<div
style={{
position: "absolute",
borderRadius: "8px",
boxShadow: "0 0 10px rgba(0, 0, 0, 0.5)",
border: "1px solid rgba(0, 0, 0, 0.5)",
transitionProperty: "transform, opacity",
transitionTimingFunction: "ease-in-out",
transitionDuration: "0.2s",
...getPanelPosition(placement),
opacity: visible ? 1 : 0,
transform: `${
getPanelPosition(placement).transform
} ${getPanelToggleTransforms(visible ?? false)}`,
...panelSize,
}}
onMouseEnter={() => {
setHover(true);
}}
onMouseLeave={() => {
setHover(false);
}}
>
{children({ resizing })}
{/* */}
<React.Fragment>
<div
style={{
position: "absolute",
left: 0,
top: "50%",
width: "10px",
height: "26px",
transform: "translateY(-13px) translateX(-5px)",
cursor: "col-resize",
transition: "opacity ease-in-out 0.2s",
pointerEvents: hover || resizing ? "auto" : "none",
opacity: hover || resizing ? 1 : 0,
}}
onMouseDown={(event) => {
setResizing("lx");
setResizePosition({
x: event.clientX,
y: event.clientY,
});
event.preventDefault();
}}
>
<ResizeHandleIcon />
</div>
<div
style={{
position: "absolute",
right: 0,
top: "50%",
width: "10px",
height: "26px",
transform: "translateY(-13px) translateX(5px)",
cursor: "col-resize",
transition: "opacity ease-in-out 0.2s",
pointerEvents: hover || resizing ? "auto" : "none",
opacity: hover || resizing ? 1 : 0,
}}
onMouseDown={(event) => {
setResizing("rx");
setResizePosition({
x: event.clientX,
y: event.clientY,
});
event.preventDefault();
}}
>
<ResizeHandleIcon />
</div>
<div
style={{
position: "absolute",
left: "50%",
top: 0,
width: "26px",
height: "10px",
transform: "translateY(-5px) translateX(-13px)",
cursor: "row-resize",
transition: "opacity ease-in-out 0.2s",
pointerEvents: hover || resizing ? "auto" : "none",
opacity: hover || resizing ? 1 : 0,
}}
onMouseDown={(event) => {
setResizing("ty");
setResizePosition({
x: event.clientX,
y: event.clientY,
});
event.preventDefault();
}}
>
<ResizeHandleIcon
style={{
transform: "rotate(90deg)",
transformOrigin: "13px 13px",
}}
/>
</div>
<div
style={{
position: "absolute",
left: "50%",
bottom: 0,
width: "26px",
height: "10px",
transform: "translateY(5px) translateX(-13px)",
cursor: "row-resize",
transition: "opacity ease-in-out 0.2s",
pointerEvents: hover || resizing ? "auto" : "none",
opacity: hover || resizing ? 1 : 0,
}}
onMouseDown={(event) => {
setResizing("by");
setResizePosition({
x: event.clientX,
y: event.clientY,
});
event.preventDefault();
}}
>
<ResizeHandleIcon
style={{
transform: "rotate(90deg)",
transformOrigin: "13px 13px",
}}
/>
</div>
</React.Fragment>
</div>
);
};

View File

@@ -0,0 +1,262 @@
import React from "react";
import debounce from "lodash/debounce";
import { createPortal } from "react-dom";
import { ApplyStyles } from "./apply-styles";
import { SelectorIcon } from "./icons/selector-button";
const MIN_SIZE = 22;
const getPosition = (element: HTMLElement, document: Document) => {
const { top, left, width, height } = element.getBoundingClientRect();
const { scrollLeft, scrollTop } = document.documentElement;
const positionLeft = left + scrollLeft - Math.max(0, MIN_SIZE - width) / 2;
const positionTop = top + scrollTop - Math.max(0, MIN_SIZE - height) / 2;
return {
left: positionLeft,
top: positionTop,
width: Math.max(MIN_SIZE, width),
height: Math.max(MIN_SIZE, height),
};
};
const SelectableElement = ({
element,
name,
onSelect,
}: {
element: HTMLElement;
name: string;
onSelect: (name: string) => void;
}) => {
const [position] = React.useState(() => getPosition(element, document));
const elementRef = React.useRef<HTMLButtonElement | null>(null);
React.useEffect(() => {
// use scroll event listener
const onScroll = debounce(
() => {
const nextPos = getPosition(element, document);
(["left", "top", "width", "height"] as const).forEach((prop) => {
elementRef.current?.style.setProperty(prop, `${nextPos[prop]}px`);
});
elementRef.current?.style.setProperty("opacity", "1");
},
200,
{
leading: false,
trailing: true,
},
);
const opacityOnScroll = debounce(
() => {
elementRef.current?.style.setProperty("opacity", "0");
},
200,
{
leading: true,
trailing: false,
},
);
document.addEventListener("scroll", onScroll);
document.addEventListener("scroll", opacityOnScroll);
return () => {
document.removeEventListener("scroll", onScroll);
document.removeEventListener("scroll", opacityOnScroll);
};
}, [element]);
const placement = React.useMemo(() => {
const tooltipBaseSize = { width: 22, height: 22 };
const nameWidth = name.length * 7.5;
const tooltipSize = {
width: tooltipBaseSize.width + nameWidth,
height: tooltipBaseSize.height,
};
const gap = 4;
// outside top start
if (
position.top - tooltipSize.height > 0 &&
position.left + tooltipSize.width < window.innerWidth &&
position.width > tooltipSize.width
) {
return { left: gap / 2, top: tooltipSize.height * -1 - gap };
}
// inside top start
if (
position.height >= tooltipSize.height * 1.5 &&
position.width >= tooltipSize.width * 1.5
) {
return { left: 0 + gap, top: 0 + gap };
}
// outside left start
if (position.left - tooltipSize.width > 0) {
return { right: position.width + gap, top: 0 - 1 };
}
// outside right start
if (
position.left + position.width + tooltipSize.width <
window.innerWidth
) {
return { left: position.width + gap, top: 0 - 1 };
}
// outside bottom start
if (
position.top + position.height + tooltipSize.height <
document.documentElement.scrollHeight
) {
return { left: 0 - 1, top: position.height + gap };
}
return { left: 0, top: 0 };
}, [position, name.length]);
return (
<button
type="button"
className="selector-xray-box"
onClick={(event) => {
event?.preventDefault();
event?.stopPropagation();
onSelect(name);
}}
ref={elementRef}
style={{
position: "absolute",
...position,
}}
>
<div
style={{
position: "absolute",
...placement,
}}
className="selector-xray-info"
>
<span className="selector-xray-info-icon">
<SelectorIcon
width={12}
height={12}
style={{ pointerEvents: "none" }}
/>
</span>
<span className="selector-xray-info-title">{` ${name}`}</span>
</div>
</button>
);
};
export const SelectableElements = ({
elements,
onSelect,
}: {
elements: Array<{ element: HTMLElement; name: string }>;
onSelect: (name: string) => void;
}) => {
const [selectorBoxRoot, setSelectorBoxRoot] =
React.useState<HTMLElement | null>(null);
React.useEffect(() => {
if (!selectorBoxRoot) {
const element = document.createElement("div");
element.id = "selector-box-root";
document.body.appendChild(element);
setSelectorBoxRoot(element);
return () => {
document.body.removeChild(element);
setSelectorBoxRoot(null);
};
}
return () => 0;
}, []);
if (!selectorBoxRoot) return null;
return (
<>
{createPortal(
elements.map((element, idx) => (
<SelectableElement
key={`selector-element-${idx}-${element.name}`}
{...element}
onSelect={onSelect}
/>
)),
selectorBoxRoot,
)}
<ApplyStyles>
{
/* css */ `
.selector-xray-box {
display: flex;
margin: 0;
padding: 0;
appearance: none;
z-index: 9999;
border: 2px dashed #47EBEB;
border-radius: 6px;
background: rgba(71, 235, 235, 0.01);
transition: opacity 0.2s ease-in-out;
cursor: crosshair;
}
.selector-xray-info {
display: flex;
justify-content: center;
align-items: center;
z-index: 10;
padding: 3px 0;
min-width: 22px;
height: 22px;
color: #14141F;
background: #47EBEB;
font-size: 12px;
line-height: 16px;
font-family: monospace;
border-radius: 11px;
}
.selector-xray-info-icon {
display: flex;
min-width: 22px;
justify-content: center;
align-items: center;
flex-shrink: 0;
}
.selector-xray-info-title {
display: block;
max-width: 0;
overflow: hidden;
transition-property: max-width, padding-right;
transition-duration: 0.2s;
transition-timing-function: ease-in-out;
transition-delay: 0.1s;
}
.selector-xray-box:hover .selector-xray-info-title {
max-width: 200px;
padding-right: 8px;
}
.selector-xray-box:hover .selector-xray-info-title {
z-index: 90;
}
`
}
</ApplyStyles>
</>
);
};

9
packages/devtools/src/define.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
declare const __DEV_CONDITION__: string;
declare const __IMPORT_META_KEY__: any;
declare const __PROCESS_KEY__: any;
declare const __PROCESS_ENV_REFINE_DEVTOOLS_PORT_KEY__: any;
declare const __PROCESS_ENV_NEXT_PUBLIC_REFINE_DEVTOOLS_PORT_KEY__: any;
declare const __PROCESS_ENV_REACT_APP_REFINE_DEVTOOLS_PORT_KEY__: any;
declare const __IMPORT_META_ENV_REFINE_DEVTOOLS_PORT_KEY__: any;
declare const __IMPORT_META_ENV_VITE_REFINE_DEVTOOLS_PORT_KEY__: any;

View File

@@ -0,0 +1,2 @@
export { DevtoolsPanel } from "./panel.js";
export { DevtoolsProvider } from "./provider.js";

View File

@@ -0,0 +1 @@
export type Placement = "bottom" | "left" | "right" | "top";

View File

@@ -0,0 +1,172 @@
import React from "react";
import { DevtoolsPin } from "./components/devtools-pin";
import { ResizablePane } from "./components/resizable-pane";
import {
DevToolsContext,
DevtoolsEvent,
send,
} from "@refinedev/devtools-shared";
import type { Placement } from "./interfaces/placement";
const MAX_IFRAME_WAIT_TIME = 1500;
export const DevtoolsPanel =
__DEV_CONDITION__ !== "development"
? () => null
: () => {
const [browser, setBrowser] = React.useState<boolean>(false);
const [visible, setVisible] = React.useState(false);
const [placement] = React.useState<Placement>("bottom");
const { httpUrl, ws } = React.useContext(DevToolsContext);
const [width, setWidth] = React.useState<number>(0);
const [selectorActive, setSelectorActive] = React.useState(false);
const [iframeStatus, setIframeStatus] = React.useState<
"loading" | "loaded" | "failed"
>("loading");
const onSelectorHighlight = React.useCallback(
(name: string) => {
if (ws) {
send(ws, DevtoolsEvent.DEVTOOLS_HIGHLIGHT_IN_MONITOR, {
name,
});
}
setVisible(true);
},
[ws],
);
React.useEffect(() => {
if (selectorActive) {
setVisible(false);
}
}, [selectorActive]);
React.useEffect(() => {
if (typeof window !== "undefined") {
setBrowser(true);
}
}, []);
React.useEffect(() => {
if (browser) {
// set width by window size dynamically
setWidth(window.innerWidth);
const onResize = () => {
setWidth(window.innerWidth);
};
window.addEventListener("resize", onResize);
return () => {
window.removeEventListener("resize", onResize);
};
}
return () => undefined;
}, [browser]);
React.useEffect(() => {
if (iframeStatus !== "loaded") {
const onMessage = (event: MessageEvent) => {
if (event.data.type === "refine-devtools-iframe-loaded") {
setIframeStatus("loaded");
}
};
window.addEventListener("message", onMessage);
return () => {
window.removeEventListener("message", onMessage);
};
}
return () => 0;
}, []);
React.useEffect(() => {
let timeout: number;
if (iframeStatus === "loading") {
timeout = window.setTimeout(() => {
setIframeStatus("failed");
if (timeout) {
clearInterval(timeout);
}
}, MAX_IFRAME_WAIT_TIME);
}
return () => {
if (typeof timeout !== "undefined") {
clearInterval(timeout);
}
};
}, [iframeStatus]);
if (!browser) {
return null;
}
return (
<div
style={{
position: "fixed",
left: `${Math.round(width / 2)}px`,
transform: "translateX(-50%)",
bottom: 0,
zIndex: 99999,
}}
>
<DevtoolsPin
onClick={() => {
setVisible((v) => !v);
setSelectorActive(false);
}}
onSelectorHighlight={onSelectorHighlight}
selectorActive={selectorActive}
setSelectorActive={setSelectorActive}
/>
<ResizablePane visible={visible} placement={placement}>
{({ resizing }) => (
<iframe
allow="clipboard-write;"
src={httpUrl}
srcDoc={
httpUrl
? iframeStatus === "failed"
? failedConnectionContent
: undefined
: missingUrlContent
}
style={{
width: "100%",
height: "100%",
border: "none",
borderRadius: "7px",
pointerEvents: resizing ? "none" : "auto",
background: "#14141F",
}}
/>
)}
</ResizablePane>
</div>
);
};
const missingUrlContent = `
<html style="height:100%;padding:0;margin:0;background:#14141F;">
<body style="background:#14141F;display:flex;justify-content:center;height:100%;padding:24px;margin:0;align-items:center;box-sizing:border-box;">
<h1 style="font-family:ui-monospace,monospace;font-weight:400;color:#CFD7E2;text-align:center;font-size:24px;">Could not connect to the devtools server.</h1>
</body>
</html>
`;
const failedConnectionContent = `
<html style="height:100%;padding:0;margin:0;background:#14141F;">
<body style="background:#14141F;display:flex;flex-direction:column;justify-content:center;height:100%;padding:24px;margin:0;align-items:center;box-sizing:border-box;">
<h1 style="max-width:480px;min-width:480px;font-family:ui-monospace,monospace;font-weight:400;color:#CFD7E2;text-align:left;font-size:24px;margin-bottom:12px;line-height:24px;">Devtools Server is unreachable.</h1>
<p style="max-width:480px;font-family:ui-monospace,monospace;font-weight:400;color:#6C7793;text-align:left;font-size:16px;line-height:32px;">Please make sure Refine Devtools is running and <code style="background:#303450;color:#A3ADC2;padding:3px 6px;border-radius:4px;">&lt;DevtoolsProvider /&gt;</code> has valid <code style="background:#303450;color:#A3ADC2;padding:3px 6px;border-radius:4px;">url</code> prop. Environment variables may not always be available in browser depending on your project setup.</p>
</body>
</html>
`;

View File

@@ -0,0 +1,24 @@
import React from "react";
import { DevToolsContextProvider } from "@refinedev/devtools-shared";
import { getDevtoolsUrlFromEnv } from "./utilities/get-devtools-url-from-env";
type Props = React.PropsWithChildren<{
/**
* Devtools URL to connect to the server. This will also be used for the WebSocket connections and serving the Devtools UI.
* By default, it will use the `REFINE_DEVTOOLS_PORT` environment variable to construct to URL or use `5001` as the default port.
* If you're using `refine dev` command, it will try to automatically set the environment variable for you and use it.
* If environment variable is not working for you, you can manually set the URL as a string or a tuple of `[httpUrl: string, wsUrl: string]`.
*/
url?: string | [httpUrl: string, wsUrl: string];
}>;
export const DevtoolsProvider =
__DEV_CONDITION__ !== "development"
? ({ children }: Props) => children as any
: ({ children, url = getDevtoolsUrlFromEnv() }: Props) => {
return (
<DevToolsContextProvider url={url}>
{children}
</DevToolsContextProvider>
);
};

View File

@@ -0,0 +1,20 @@
const DEFAULT_DEVTOOLS_PORT = 5001;
export const getDevtoolsUrlFromEnv = () => {
const PORT_FROM_ENV =
typeof __PROCESS_KEY__ !== "undefined" && "env" in __PROCESS_KEY__
? __PROCESS_ENV_REFINE_DEVTOOLS_PORT_KEY__ ||
__PROCESS_ENV_NEXT_PUBLIC_REFINE_DEVTOOLS_PORT_KEY__ ||
__PROCESS_ENV_REACT_APP_REFINE_DEVTOOLS_PORT_KEY__
: typeof __IMPORT_META_KEY__ !== "undefined" && __IMPORT_META_KEY__.env
? __IMPORT_META_ENV_REFINE_DEVTOOLS_PORT_KEY__ ||
__IMPORT_META_ENV_VITE_REFINE_DEVTOOLS_PORT_KEY__
: null;
const port = PORT_FROM_ENV || DEFAULT_DEVTOOLS_PORT;
return [`http://localhost:${port}`, `ws://localhost:${port}`] as [
httpUrl: string,
wsUrl: string,
];
};

View File

@@ -0,0 +1,123 @@
import type { Placement } from "src/interfaces/placement";
export const getPanelToggleTransforms = (visible: boolean) => {
return visible ? "scaleX(1) translateY(0)" : "scaleX(0) translateY(25vw)";
};
export const SIZE = 50;
export const BUFFER = 10;
const PREFERRED_DEFAULT_WIDTH = () =>
typeof window !== "undefined" ? window.innerWidth * 0.7 : 1440 * 0.7; // 70% of window width
const PREFERRED_DEFAULT_HEIGHT = () =>
typeof window !== "undefined" ? window.innerHeight * 0.7 : 900 * 0.7; // 70% of window height
export const MIN_PANEL_WIDTH = 640;
export const MIN_PANEL_HEIGHT = 360;
export const getPinButtonTransform = (hover?: boolean) => {
return `translateY(${hover ? "0" : "50%"})`;
};
export const getPanelPosition = (placement: Placement) => {
switch (placement) {
case "left":
return {
left: `calc(${SIZE}px + ${BUFFER}px)`,
top: "50%",
transform: "translateY(-50%)",
};
case "right":
return {
right: `calc(${SIZE}px + ${BUFFER}px)`,
top: "50%",
transform: "translateY(-50%)",
};
case "top":
return {
left: "50%",
top: `calc(${SIZE}px + ${BUFFER}px)`,
transform: "translateX(-50%)",
};
case "bottom":
return {
left: "50%",
bottom: `calc(${SIZE}px + ${BUFFER}px)`,
transform: "translateX(-50%)",
};
}
};
export const getMaxPanelWidth = (placement: Placement) => {
switch (placement) {
case "left":
case "right":
return (
-BUFFER -
SIZE -
BUFFER +
(typeof window !== "undefined" ? window.innerWidth : 1440) -
BUFFER
);
case "top":
case "bottom":
return (
-BUFFER +
(typeof window !== "undefined" ? window.innerWidth : 1440) -
BUFFER
);
}
};
export const getMaxPanelHeight = (placement: Placement) => {
switch (placement) {
case "left":
case "right":
return (
-BUFFER +
(typeof window !== "undefined" ? window.innerHeight : 900) -
BUFFER
);
case "top":
case "bottom":
return (
-BUFFER -
SIZE -
BUFFER +
(typeof window !== "undefined" ? window.innerHeight : 900) -
BUFFER
);
}
};
export const getDefaultPanelSize = (
placement: Placement,
preferredSize?: { width: number; height: number },
): { width: number; height: number } => {
const defaultPreferred = {
width: PREFERRED_DEFAULT_WIDTH(),
height: PREFERRED_DEFAULT_HEIGHT(),
};
const maxPanelWidth = getMaxPanelWidth(placement);
const maxPanelHeight = getMaxPanelHeight(placement);
const width = Math.min(
maxPanelWidth,
(preferredSize ?? defaultPreferred).width,
);
const height = Math.min(
maxPanelHeight,
(preferredSize ?? defaultPreferred).height,
);
return {
width: width,
height: height,
};
};
export const roundToEven = (num: number) => {
const rounded = Math.round(num);
return rounded % 2 === 0 ? rounded : rounded + 1;
};

View File

@@ -0,0 +1,157 @@
import {
getElementFromFiber,
getFiberFromElement,
getFirstFiberHasName,
getFirstStateNodeFiber,
getNameFromFiber,
getParentOfFiber,
} from "@aliemir/dom-to-fiber-utils";
type Fiber = Exclude<ReturnType<typeof getFiberFromElement>, null>;
export type SelectableElement = {
element: HTMLElement;
name: string;
};
const getChildOfFiber = (fiber: Fiber | null) => {
if (!fiber) {
return null;
}
return fiber.child;
};
const getFirstHTMLElementFromFiberByChild = (fiber: Fiber | null) => {
let child = fiber;
while (child) {
const element = getElementFromFiber(child);
if (element && element instanceof HTMLElement) {
return element;
}
child = getChildOfFiber(child) as Fiber;
}
return null;
};
const getFirstHTMLElementFromFiberByParent = (fiber: Fiber | null) => {
let parent = fiber;
while (parent) {
const element = getElementFromFiber(parent);
if (element && element instanceof HTMLElement) {
return element;
}
parent = getParentOfFiber(parent) as Fiber;
}
return null;
};
const getFirstHTMLElementFromFiber = (
fiber: Fiber | null,
): [element: HTMLElement, "child" | "parent" | "body"] => {
let element = getFirstHTMLElementFromFiberByChild(fiber);
if (element) {
return [element, "child"];
}
element = getFirstHTMLElementFromFiberByParent(fiber);
if (element) {
return [element, "parent"];
}
return [document.body, "body"];
};
const selectFiber = (start: Fiber | null, activeTraceItems: string[]) => {
let fiber = start;
let firstParentOfNodeWithName: Fiber | null = null;
let fiberWithStateNode: Fiber | null = null;
let acceptedName = false;
while (!acceptedName && fiber) {
// Get the first fiber node that has a name (look up the tree)
firstParentOfNodeWithName = getFirstFiberHasName(fiber);
// Get the first fiber node that has a state node (look up the tree)
fiberWithStateNode = getFirstStateNodeFiber(firstParentOfNodeWithName);
acceptedName = activeTraceItems.includes(
getNameFromFiber(firstParentOfNodeWithName) ?? "",
);
if (!acceptedName) {
fiber = getParentOfFiber(fiber);
}
}
if (fiberWithStateNode && firstParentOfNodeWithName) {
return {
stateNode: fiberWithStateNode,
nameFiber: firstParentOfNodeWithName,
};
}
return {
stateNode: null,
nameFiber: null,
};
};
export const filterInvisibleNodes = (nodes: SelectableElement[]) => {
return nodes.filter(
(item) => item.element.offsetWidth > 0 && item.element.offsetHeight > 0,
);
};
export const getUniqueNodes = (nodes: SelectableElement[]) => {
const uniques: SelectableElement[] = [];
nodes.forEach((node) => {
const isElementExist = uniques.find(
(item) => item.element === node.element && item.name === node.name,
);
if (!isElementExist) {
uniques.push(node);
}
});
return uniques;
};
export const traverseDom = (
node: HTMLElement | null,
activeTraceItems: string[],
): SelectableElement[] => {
if (!node) {
return [];
}
const items: SelectableElement[] = [];
const fiber = getFiberFromElement(node);
const targetFiber = selectFiber(fiber, activeTraceItems);
if (targetFiber.nameFiber) {
const [element] = getFirstHTMLElementFromFiber(targetFiber.nameFiber);
const name = getNameFromFiber(targetFiber.nameFiber);
if (element && name) {
items.push({
element,
name,
});
}
}
for (let i = 0; i < node?.children?.length ?? 0; i++) {
items.push(
...traverseDom(node.children[i] as HTMLElement, activeTraceItems),
);
}
return items;
};

View File

@@ -0,0 +1,42 @@
import React from "react";
import { DevToolsContext } from "@refinedev/devtools-shared";
import {
filterInvisibleNodes,
getUniqueNodes,
traverseDom,
type SelectableElement,
} from "./selector-helpers";
export const useSelector = (active: boolean) => {
const { httpUrl } = React.useContext(DevToolsContext);
const [selectableElements, setSelectableElements] = React.useState<
SelectableElement[]
>([]);
const fetchTraceItems = React.useCallback(async () => {
const response = await fetch(`${httpUrl}/api/unique-trace-items`);
const data = await response.json();
return data.data as string[];
}, [httpUrl]);
const prepareSelector = React.useCallback(async () => {
const fetchedTraceItems = await fetchTraceItems();
const traversedNodes = traverseDom(document.body, fetchedTraceItems);
const filterInvisible = filterInvisibleNodes(traversedNodes);
const uniqueNodes = getUniqueNodes(filterInvisible);
setSelectableElements(uniqueNodes);
}, [fetchTraceItems]);
React.useEffect(() => {
if (active) {
prepareSelector();
}
}, [active, prepareSelector]);
return {
selectableElements,
};
};