mirror of
https://github.com/stefanpejcic/openpanel
synced 2025-06-26 18:28:26 +00:00
packages
This commit is contained in:
19
packages/devtools/src/components/apply-styles.tsx
Normal file
19
packages/devtools/src/components/apply-styles.tsx
Normal 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;
|
||||
};
|
||||
73
packages/devtools/src/components/devtools-pin.tsx
Normal file
73
packages/devtools/src/components/devtools-pin.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
67
packages/devtools/src/components/devtools-selector.tsx
Normal file
67
packages/devtools/src/components/devtools-selector.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
33
packages/devtools/src/components/icons/arrow-union-icon.tsx
Normal file
33
packages/devtools/src/components/icons/arrow-union-icon.tsx
Normal 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>
|
||||
);
|
||||
35
packages/devtools/src/components/icons/devtools-icon.tsx
Normal file
35
packages/devtools/src/components/icons/devtools-icon.tsx
Normal 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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
37
packages/devtools/src/components/icons/selector-button.tsx
Normal file
37
packages/devtools/src/components/icons/selector-button.tsx
Normal 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>
|
||||
);
|
||||
283
packages/devtools/src/components/resizable-pane.tsx
Normal file
283
packages/devtools/src/components/resizable-pane.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
262
packages/devtools/src/components/selectable-elements.tsx
Normal file
262
packages/devtools/src/components/selectable-elements.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user