Add hints
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "vectrace",
|
"name": "vectrace",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "vectrace",
|
"name": "vectrace",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@stylistic/eslint-plugin": "^5.10.0",
|
"@stylistic/eslint-plugin": "^5.10.0",
|
||||||
"@types/lodash": "^4.17.24",
|
"@types/lodash": "^4.17.24",
|
||||||
|
|||||||
+54
-54
@@ -1,56 +1,56 @@
|
|||||||
{
|
{
|
||||||
"name": "vectrace",
|
"name": "vectrace",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.1.0",
|
"version": "1.1.q",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Terncode",
|
"name": "Terncode",
|
||||||
"email": "[email protected]",
|
"email": "[email protected]",
|
||||||
"url": "https://terncode.dev"
|
"url": "https://terncode.dev"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host",
|
"dev": "vite --host",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"lint-fix": "eslint --fix \"src/**/*.{js,mjs,cjs,ts,jsx,tsx}\"",
|
"lint-fix": "eslint --fix \"src/**/*.{js,mjs,cjs,ts,jsx,tsx}\"",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@stylistic/eslint-plugin": "^5.10.0",
|
"@stylistic/eslint-plugin": "^5.10.0",
|
||||||
"@types/lodash": "^4.17.24",
|
"@types/lodash": "^4.17.24",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"fast-xml-builder": "^1.1.5",
|
"fast-xml-builder": "^1.1.5",
|
||||||
"fast-xml-parser": "^5.7.1",
|
"fast-xml-parser": "^5.7.1",
|
||||||
"imagetracerjs": "^1.2.6",
|
"imagetracerjs": "^1.2.6",
|
||||||
"jspdf": "^4.2.1",
|
"jspdf": "^4.2.1",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"ldrs": "^1.1.9",
|
"ldrs": "^1.1.9",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
"lodash": "^4.18.1",
|
"lodash": "^4.18.1",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"polygon-clipping": "^0.15.7",
|
"polygon-clipping": "^0.15.7",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-icons": "^5.6.0",
|
"react-icons": "^5.6.0",
|
||||||
"styled-components": "^6.4.0",
|
"styled-components": "^6.4.0",
|
||||||
"svg.js": "^2.7.1",
|
"svg.js": "^2.7.1",
|
||||||
"svgson": "^5.3.1",
|
"svgson": "^5.3.1",
|
||||||
"uuid": "^14.0.0"
|
"uuid": "^14.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.4",
|
"@eslint/js": "^9.39.4",
|
||||||
"@types/node": "^24.12.2",
|
"@types/node": "^24.12.2",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"eslint": "^9.39.4",
|
"eslint": "^9.39.4",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"eslint-plugin-styled-components-a11y": "^2.2.1",
|
"eslint-plugin-styled-components-a11y": "^2.2.1",
|
||||||
"globals": "^17.4.0",
|
"globals": "^17.4.0",
|
||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
"typescript-eslint": "^8.58.0",
|
"typescript-eslint": "^8.58.0",
|
||||||
"vite": "^8.0.4"
|
"vite": "^8.0.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+116
-100
@@ -4,6 +4,7 @@ import { Button } from "../styles";
|
|||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { createPdf, createSVGDoc, downloadBlob, downloadZip } from "../utils/download";
|
import { createPdf, createSVGDoc, downloadBlob, downloadZip } from "../utils/download";
|
||||||
import type { ImageEditor } from "../handler/image-editor";
|
import type { ImageEditor } from "../handler/image-editor";
|
||||||
|
import { Hint } from "./tooltip";
|
||||||
|
|
||||||
const Btn = styled(Button)`
|
const Btn = styled(Button)`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -39,117 +40,132 @@ export function ImportExportButtons({ handler, selected }: CanvasHandlerProps &
|
|||||||
<Box $width={boxSize} $height={boxSize}>
|
<Box $width={boxSize} $height={boxSize}>
|
||||||
<Column>
|
<Column>
|
||||||
<Row>
|
<Row>
|
||||||
<Btn
|
<Hint hint={"Download all project assets as a ZIP archive"}>
|
||||||
onClick={() => {
|
<Btn
|
||||||
downloadZip(handler);
|
onClick={() => {
|
||||||
}}
|
downloadZip(handler);
|
||||||
>
|
}}
|
||||||
<FaFileZipper />
|
>
|
||||||
ZIP
|
<FaFileZipper />
|
||||||
</Btn>
|
ZIP
|
||||||
<Btn
|
</Btn>
|
||||||
onClick={() => {
|
</Hint>
|
||||||
createPdf(handler, true).save(`${handler.projectName}.pdf`);
|
<Hint hint={"Combine all assets into a single PDF"}>
|
||||||
}}
|
<Btn
|
||||||
>
|
onClick={() => {
|
||||||
<FaFilePdf />
|
createPdf(handler, true).save(`${handler.projectName}.pdf`);
|
||||||
PDF
|
}}
|
||||||
</Btn>
|
>
|
||||||
<Btn
|
<FaFilePdf />
|
||||||
onClick={async () => {
|
PDF
|
||||||
downloadBlob(await createSVGDoc(handler), `${handler.projectName}.svg`);
|
</Btn>
|
||||||
}}
|
</Hint>
|
||||||
>
|
<Hint hint={"Merge all SVG paths into one .svg file"}>
|
||||||
<FaDrawPolygon />
|
<Btn
|
||||||
SVG
|
onClick={async () => {
|
||||||
</Btn>
|
downloadBlob(await createSVGDoc(handler), `${handler.projectName}.svg`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaDrawPolygon />
|
||||||
|
SVG
|
||||||
|
</Btn>
|
||||||
|
</Hint>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<Btn
|
<Hint hint={"Download selected layer as PNG"}>
|
||||||
disabled={selected.length !== 1}
|
<Btn
|
||||||
onClick={() => {
|
disabled={selected.length !== 1}
|
||||||
const v = handler.imagesRenders.find((e) => e.id === selected[0].id);
|
onClick={() => {
|
||||||
if (v) {
|
const v = handler.imagesRenders.find((e) => e.id === selected[0].id);
|
||||||
const canvas = document.createElement("canvas");
|
if (v) {
|
||||||
canvas.width = v.image.naturalWidth * v.scale;
|
const canvas = document.createElement("canvas");
|
||||||
canvas.height = v.image.naturalHeight * v.scale;
|
canvas.width = v.image.naturalWidth * v.scale;
|
||||||
const ctx = canvas.getContext("2d")!;
|
canvas.height = v.image.naturalHeight * v.scale;
|
||||||
ctx.drawImage(v.image, 0, 0, canvas.width, canvas.height);
|
const ctx = canvas.getContext("2d")!;
|
||||||
canvas.toBlob((blob) => {
|
ctx.drawImage(v.image, 0, 0, canvas.width, canvas.height);
|
||||||
if (blob) {
|
canvas.toBlob((blob) => {
|
||||||
downloadBlob(blob, `${v.name}.png`);
|
if (blob) {
|
||||||
} else {
|
downloadBlob(blob, `${v.name}.png`);
|
||||||
handler.popup.alert("Error", "Cannot export image");
|
} else {
|
||||||
}
|
handler.popup.alert("Error", "Cannot export image");
|
||||||
}, "image/png");
|
}
|
||||||
} else {
|
}, "image/png");
|
||||||
handler.popup.alert("Error", "Image not found");
|
} else {
|
||||||
}
|
handler.popup.alert("Error", "Image not found");
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FaImage />
|
|
||||||
Image
|
|
||||||
</Btn>
|
|
||||||
<Btn
|
|
||||||
disabled={selected.length !== 1}
|
|
||||||
onClick={() => {
|
|
||||||
const v = handler.imagesRenders.find((e) => e.id === selected[0].id);
|
|
||||||
if (v) {
|
|
||||||
if (v.svg.scale !== v.scale || v.svg.dirty) {
|
|
||||||
handler.redrawSvg(v.id);
|
|
||||||
}
|
}
|
||||||
downloadBlob(new Blob([v.svg.data], { type: "image/svg+xml" }), `${v.name}.svg`);
|
}}
|
||||||
} else {
|
>
|
||||||
handler.popup.alert("Error", "Image not found");
|
<FaImage />
|
||||||
}
|
Image
|
||||||
}}
|
</Btn>
|
||||||
>
|
</Hint>
|
||||||
{" "}
|
<Hint hint={"Download SVG path for selected layer"}>
|
||||||
<FaDrawPolygon />
|
<Btn
|
||||||
SVG
|
disabled={selected.length !== 1}
|
||||||
</Btn>
|
onClick={() => {
|
||||||
|
const v = handler.imagesRenders.find((e) => e.id === selected[0].id);
|
||||||
|
if (v) {
|
||||||
|
if (v.svg.scale !== v.scale || v.svg.dirty) {
|
||||||
|
handler.redrawSvg(v.id);
|
||||||
|
}
|
||||||
|
downloadBlob(new Blob([v.svg.data], { type: "image/svg+xml" }), `${v.name}.svg`);
|
||||||
|
} else {
|
||||||
|
handler.popup.alert("Error", "Image not found");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaDrawPolygon />
|
||||||
|
SVG
|
||||||
|
</Btn>
|
||||||
|
</Hint>
|
||||||
</Row>
|
</Row>
|
||||||
</Column>
|
</Column>
|
||||||
</Box>
|
</Box>
|
||||||
<Box $width={boxSize} $height={boxSize}>
|
<Box $width={boxSize} $height={boxSize}>
|
||||||
<Column>
|
<Column>
|
||||||
<Row>
|
<Row>
|
||||||
<Btn
|
<Hint hint={"Save entire project to a .ppd file"}>
|
||||||
onClick={() => {
|
<Btn
|
||||||
handler.exportProject();
|
onClick={() => {
|
||||||
}}
|
handler.exportProject();
|
||||||
>
|
}}
|
||||||
<FaFileExport />
|
>
|
||||||
Export
|
<FaFileExport />
|
||||||
</Btn>
|
Export
|
||||||
<Btn
|
</Btn>
|
||||||
onClick={() => {
|
</Hint>
|
||||||
handler.importProject();
|
<Hint hint={"Load a project from a .ppd file"}>
|
||||||
}}
|
<Btn
|
||||||
>
|
onClick={() => {
|
||||||
<FaFileImport />
|
handler.importProject();
|
||||||
Import
|
}}
|
||||||
</Btn>
|
>
|
||||||
|
<FaFileImport />
|
||||||
|
Import
|
||||||
|
</Btn>
|
||||||
|
</Hint>
|
||||||
</Row>
|
</Row>
|
||||||
<Btn
|
<Hint hint={"Permanently wipe the canvas — cannot be undone"}>
|
||||||
style={{ backgroundColor: "#ff000073" }}
|
<Btn
|
||||||
disabled={selected.length !== 1}
|
style={{ backgroundColor: "#ff000073" }}
|
||||||
onClick={async () => {
|
disabled={selected.length !== 1}
|
||||||
{
|
onClick={async () => {
|
||||||
if (
|
{
|
||||||
await handler.popup.confirm(
|
if (
|
||||||
"Clear",
|
await handler.popup.confirm(
|
||||||
"Are you sure you want to clear project? Any unsaved changes will be discarded",
|
"Clear",
|
||||||
)
|
"Are you sure you want to clear project? Any unsaved changes will be discarded",
|
||||||
) {
|
)
|
||||||
handler.clear();
|
) {
|
||||||
|
handler.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<FaBroom />
|
||||||
<FaBroom />
|
Clear
|
||||||
Clear
|
</Btn>
|
||||||
</Btn>
|
</Hint>
|
||||||
</Column>
|
</Column>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { calculateMathExpression, canEvaluateMathExpression } from "../calc";
|
|||||||
const StyledInput = styled.input<{ $isValid: boolean | null }>`
|
const StyledInput = styled.input<{ $isValid: boolean | null }>`
|
||||||
padding: 1px;
|
padding: 1px;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
height: 15px;
|
||||||
width: 80px;
|
width: 80px;
|
||||||
border: 1px solid ${({ $isValid }) => ($isValid === null ? "#888" : $isValid ? "#ffffff" : "#ef4444")};
|
border: 1px solid ${({ $isValid }) => ($isValid === null ? "#888" : $isValid ? "#ffffff" : "#ef4444")};
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import { VerticalSlider } from "./vertical-slider";
|
|||||||
import { TRANSPARENCY_THRESHOLD_DEFAULT } from "../svg-tracer";
|
import { TRANSPARENCY_THRESHOLD_DEFAULT } from "../svg-tracer";
|
||||||
import type { VectraceHandler } from "../handler/vectrace-handler";
|
import type { VectraceHandler } from "../handler/vectrace-handler";
|
||||||
import { clamp } from "lodash";
|
import { clamp } from "lodash";
|
||||||
|
import { Hint } from "./tooltip";
|
||||||
|
|
||||||
const Table = styled.table`
|
const Table = styled.table`
|
||||||
margin: 2px;
|
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ const LayerPanel = styled.div`
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100px;
|
width: 100px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
height: 100px;
|
height: 97px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ToggleButton = styled.button<{ $enabled: boolean }>`
|
const ToggleButton = styled.button<{ $enabled: boolean }>`
|
||||||
@@ -67,60 +67,89 @@ export function SvgTracerOptions({ selected, handler }: { selected: ImageEditor[
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<WrapperRow>
|
<WrapperRow>
|
||||||
<VerticalSlider
|
<Hint
|
||||||
max={255}
|
hint={
|
||||||
min={0}
|
"Transparency cutoff — pixels more transparent than this value are ignored when tracing. " +
|
||||||
value={options.transparencyThreshold ?? TRANSPARENCY_THRESHOLD_DEFAULT}
|
"Lower = only opaque pixels traced, Higher = transparent pixels included in the path."
|
||||||
onLabelClick={async () => {
|
}
|
||||||
const value = await handler.popup.prompt(
|
>
|
||||||
"Transparency threshold",
|
<VerticalSlider
|
||||||
"Enter transparency threshold between 0-255",
|
max={255}
|
||||||
(options.transparencyThreshold ?? TRANSPARENCY_THRESHOLD_DEFAULT).toString(),
|
min={0}
|
||||||
);
|
value={options.transparencyThreshold ?? TRANSPARENCY_THRESHOLD_DEFAULT}
|
||||||
if (value) {
|
onLabelClick={async () => {
|
||||||
const int = parseInt(value, 10);
|
const value = await handler.popup.prompt(
|
||||||
if (!isNaN(int)) {
|
"Transparency threshold",
|
||||||
setValue("transparencyThreshold", clamp(int, 0, 255));
|
"Enter transparency threshold between 0-255",
|
||||||
|
(options.transparencyThreshold ?? TRANSPARENCY_THRESHOLD_DEFAULT).toString(),
|
||||||
|
);
|
||||||
|
if (value) {
|
||||||
|
const int = parseInt(value, 10);
|
||||||
|
if (!isNaN(int)) {
|
||||||
|
setValue("transparencyThreshold", clamp(int, 0, 255));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}}
|
||||||
}}
|
onChange={(value) => {
|
||||||
onChange={(value) => {
|
setValue("transparencyThreshold", value);
|
||||||
setValue("transparencyThreshold", value);
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
</Hint>
|
||||||
<WrapperColumn>
|
<WrapperColumn>
|
||||||
<Button
|
<Button
|
||||||
$active={!updated}
|
$active={!updated}
|
||||||
title="Draw"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
redraw();
|
redraw();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FaBezierCurve />
|
<span>
|
||||||
|
<span>Redraw</span>
|
||||||
|
<FaBezierCurve style={{ paddingLeft: 10 }} />
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
<WrapperRow>
|
<WrapperRow>
|
||||||
<Table>
|
<Table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<NumericInputWithLabelTable
|
<Hint
|
||||||
name="Ltres"
|
hint={
|
||||||
value={options.ltres}
|
"Line threshold — how closely straight lines must follow the original shape. Lower = more accurate, more points."
|
||||||
onChange={(value) => setValue("ltres", value)}
|
}
|
||||||
/>
|
>
|
||||||
<NumericInputWithLabelTable
|
<NumericInputWithLabelTable
|
||||||
name="Qtres"
|
name="Ltres"
|
||||||
value={options.qtres}
|
value={options.ltres}
|
||||||
onChange={(value) => setValue("qtres", value)}
|
onChange={(value) => setValue("ltres", value)}
|
||||||
/>
|
/>
|
||||||
<NumericInputWithLabelTable
|
</Hint>
|
||||||
name="Path omit"
|
<Hint
|
||||||
value={options.pathomit}
|
hint={
|
||||||
onChange={(value) => setValue("pathomit", value)}
|
"Curve threshold — how closely curves must follow the original shape. Lower = smoother curves, more points."
|
||||||
/>
|
}
|
||||||
<NumericInputWithLabelTable
|
>
|
||||||
name="Round corners"
|
<NumericInputWithLabelTable
|
||||||
value={options.roundcoords}
|
name="Qtres"
|
||||||
onChange={(value) => setValue("roundcoords", value)}
|
value={options.qtres}
|
||||||
/>
|
onChange={(value) => setValue("qtres", value)}
|
||||||
|
/>
|
||||||
|
</Hint>
|
||||||
|
<Hint
|
||||||
|
hint={
|
||||||
|
"Ignore tiny paths smaller than this many pixels. Higher = cleaner output, but small details get dropped."
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<NumericInputWithLabelTable
|
||||||
|
name="Path omit"
|
||||||
|
value={options.pathomit}
|
||||||
|
onChange={(value) => setValue("pathomit", value)}
|
||||||
|
/>
|
||||||
|
</Hint>
|
||||||
|
<Hint hint={"Decimal precision of path coordinates. Lower = smaller file, slightly less accurate."}>
|
||||||
|
<NumericInputWithLabelTable
|
||||||
|
name="Round corners"
|
||||||
|
value={options.roundcoords}
|
||||||
|
onChange={(value) => setValue("roundcoords", value)}
|
||||||
|
/>
|
||||||
|
</Hint>
|
||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
<LayerPanel>
|
<LayerPanel>
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { useState, useRef, useEffect, type ReactNode } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
|
||||||
|
interface HintProps {
|
||||||
|
hint: ReactNode;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Hint({ hint, children }: HintProps) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [pos, setPos] = useState({ top: 0, left: 0 });
|
||||||
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const updatePos = () => {
|
||||||
|
if (!wrapperRef.current?.children[0]) return;
|
||||||
|
const rect = wrapperRef.current.children[0].getBoundingClientRect();
|
||||||
|
setPos({
|
||||||
|
top: rect.bottom,
|
||||||
|
left: rect.left + rect.width / 2,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open || !tooltipRef.current) return;
|
||||||
|
const tip = tooltipRef.current.getBoundingClientRect();
|
||||||
|
const margin = 8;
|
||||||
|
|
||||||
|
setPos((prev) => ({
|
||||||
|
top: tip.top,
|
||||||
|
left: Math.min(Math.max(prev.left, tip.width / 2 + margin), window.innerWidth - tip.width / 2 - margin),
|
||||||
|
}));
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={wrapperRef}
|
||||||
|
style={{ display: "contents" }}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
updatePos();
|
||||||
|
setOpen(true);
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{open &&
|
||||||
|
createPortal(
|
||||||
|
<div
|
||||||
|
ref={tooltipRef}
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
top: pos.top,
|
||||||
|
left: pos.left,
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
background: "#141414",
|
||||||
|
border: "1px solid #4af626",
|
||||||
|
color: "#fff",
|
||||||
|
padding: "6px 10px",
|
||||||
|
fontSize: "12px",
|
||||||
|
maxWidth: "260px",
|
||||||
|
pointerEvents: "none",
|
||||||
|
zIndex: 9999,
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{hint}
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -116,7 +116,7 @@ export function VerticalSlider({ min = 0, max = 100, step = 1, onLabelClick, val
|
|||||||
onLabelClick?.();
|
onLabelClick?.();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{value}
|
{Math.floor((value / max) * 100)}%
|
||||||
</ValueLabel>
|
</ValueLabel>
|
||||||
<Track min={min} max={max} step={step} value={value} $fillPercent={fillPercent} onChange={handleChange} />
|
<Track min={min} max={max} step={step} value={value} $fillPercent={fillPercent} onChange={handleChange} />
|
||||||
</SliderWrapper>
|
</SliderWrapper>
|
||||||
|
|||||||
Reference in New Issue
Block a user