mirror of
https://github.com/storytold/placeholder-artcraft.git
synced 2026-10-24 08:24:58 +00:00
mirror repo (WIP)
Need to remove secrets before entire repo can be made public.
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
export * from './lib/input';
|
||||
export * from './lib/InputVector';
|
||||
export * from './lib/NumberInput';
|
||||
@@ -0,0 +1,232 @@
|
||||
import React, { ChangeEvent, RefObject, useRef } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { DomLevels } from "@storyteller/common";
|
||||
|
||||
interface InputVectorProps {
|
||||
x: string;
|
||||
y: string;
|
||||
z: string;
|
||||
onChange: (v: Record<string, string>) => void;
|
||||
increment?: number;
|
||||
disabled?: boolean;
|
||||
disableHotkeyInput: (level: DomLevels) => void;
|
||||
enableHotkeyInput: (level: DomLevels) => void;
|
||||
}
|
||||
|
||||
export const InputVector = ({
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
onChange,
|
||||
increment = 0.1,
|
||||
disabled,
|
||||
disableHotkeyInput,
|
||||
enableHotkeyInput,
|
||||
}: InputVectorProps) => {
|
||||
const xRef = useRef<HTMLInputElement>(null);
|
||||
const yRef = useRef<HTMLInputElement>(null);
|
||||
const zRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const isDragging = useRef(false);
|
||||
const previousMousePosition = useRef(0);
|
||||
|
||||
const oldX = useRef("");
|
||||
const oldY = useRef("");
|
||||
const oldZ = useRef("");
|
||||
|
||||
oldX.current = x;
|
||||
oldY.current = y;
|
||||
oldZ.current = z;
|
||||
|
||||
const inputCommonClasses =
|
||||
"relative h-6 rounded-r-lg bg-black/25 p-2 text-sm text-white transition-all duration-100 ease-in-out outline-none -outline-offset-2 text-end w-full hover:cursor-e-resize hover:bg-black/40";
|
||||
|
||||
const wrapperCommonClasses =
|
||||
"relative flex items-center before:inline-block before:h-6 before:bg-brand-primary before:text-white before:rounded-l-lg before:w-1.5 before:text-center before:justify-center before:items-center before:font-semibold before:flex before:align-middle before:leading-8 text-xs";
|
||||
|
||||
const lockedCommonClasses =
|
||||
"hover:cursor-not-allowed hover:bg-brand-secondary";
|
||||
|
||||
function validateInput(newValue: string, oldValue: string) {
|
||||
if (newValue === "" || newValue === "-" || newValue === ".") {
|
||||
return newValue;
|
||||
}
|
||||
return /^-?\d*(\.\d{0,2})?$/.test(newValue) ? newValue : oldValue;
|
||||
}
|
||||
|
||||
function handleOnChange(event: ChangeEvent<HTMLInputElement>) {
|
||||
const newVector: Record<string, string> = {
|
||||
x: oldX.current,
|
||||
y: oldY.current,
|
||||
z: oldZ.current,
|
||||
};
|
||||
|
||||
newVector[event.target.name] = validateInput(
|
||||
event.target.value,
|
||||
newVector[event.target.name],
|
||||
);
|
||||
|
||||
onChange(newVector);
|
||||
}
|
||||
|
||||
const blurAllInputs = () => {
|
||||
xRef.current?.blur();
|
||||
yRef.current?.blur();
|
||||
zRef.current?.blur();
|
||||
};
|
||||
|
||||
// For dragging the input value to increment/decrement
|
||||
const handleMouseDown = (
|
||||
e: React.MouseEvent,
|
||||
ref: RefObject<HTMLInputElement|null>,
|
||||
) => {
|
||||
e.stopPropagation();
|
||||
blurAllInputs();
|
||||
previousMousePosition.current = e.clientX;
|
||||
isDragging.current = true;
|
||||
|
||||
const mouseMoveHandler = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
const currentMousePosition = e.clientX;
|
||||
const direction =
|
||||
currentMousePosition === previousMousePosition.current
|
||||
? 0
|
||||
: currentMousePosition > previousMousePosition.current
|
||||
? 1
|
||||
: -1;
|
||||
|
||||
previousMousePosition.current = currentMousePosition;
|
||||
|
||||
const newVector = {
|
||||
x: oldX.current,
|
||||
y: oldY.current,
|
||||
z: oldZ.current,
|
||||
} as Record<string, string>;
|
||||
|
||||
const newValue =
|
||||
parseFloat(newVector[ref.current?.name ?? ""]) + direction * increment;
|
||||
|
||||
newVector[ref.current?.name ?? ""] = (
|
||||
Math.round(newValue * 1000) / 1000
|
||||
).toString();
|
||||
|
||||
onChange(newVector);
|
||||
};
|
||||
|
||||
const mouseUpHandler = () => {
|
||||
document.removeEventListener("mousemove", mouseMoveHandler);
|
||||
document.removeEventListener("mouseup", mouseUpHandler);
|
||||
isDragging.current = false;
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", mouseMoveHandler);
|
||||
document.addEventListener("mouseup", mouseUpHandler);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
(e.currentTarget as HTMLInputElement).blur(); // Unfocus the input field on pressing Enter
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col justify-between gap-1.5">
|
||||
<span
|
||||
className={twMerge(
|
||||
wrapperCommonClasses,
|
||||
"before:bg-axis-x",
|
||||
disabled && "opacity-60",
|
||||
)}
|
||||
>
|
||||
<div className="absolute left-3.5 z-10 font-semibold">X</div>
|
||||
<input
|
||||
className={twMerge(
|
||||
inputCommonClasses,
|
||||
"focus:outline-axis-x",
|
||||
disabled && lockedCommonClasses,
|
||||
)}
|
||||
type="text"
|
||||
name="x"
|
||||
onChange={handleOnChange}
|
||||
ref={xRef}
|
||||
value={x}
|
||||
disabled={disabled}
|
||||
onFocus={() => {
|
||||
disableHotkeyInput(DomLevels.INPUT);
|
||||
}}
|
||||
onBlur={() => {
|
||||
enableHotkeyInput(DomLevels.INPUT);
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
handleMouseDown(e, xRef);
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className={twMerge(
|
||||
wrapperCommonClasses,
|
||||
"before:bg-axis-y",
|
||||
disabled && "opacity-60",
|
||||
)}
|
||||
>
|
||||
<div className="absolute left-3.5 z-10 font-semibold">Y</div>
|
||||
<input
|
||||
className={twMerge(
|
||||
inputCommonClasses,
|
||||
"focus:outline-axis-y",
|
||||
disabled && lockedCommonClasses,
|
||||
)}
|
||||
type="text"
|
||||
name="y"
|
||||
onChange={handleOnChange}
|
||||
ref={yRef}
|
||||
value={y}
|
||||
disabled={disabled}
|
||||
onFocus={() => {
|
||||
disableHotkeyInput(DomLevels.INPUT);
|
||||
}}
|
||||
onBlur={() => {
|
||||
enableHotkeyInput(DomLevels.INPUT);
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
handleMouseDown(e, yRef);
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className={twMerge(
|
||||
wrapperCommonClasses,
|
||||
"before:bg-axis-z",
|
||||
disabled && "opacity-60",
|
||||
)}
|
||||
>
|
||||
<div className="absolute left-3.5 z-10 font-semibold">Z</div>
|
||||
<input
|
||||
className={twMerge(
|
||||
inputCommonClasses,
|
||||
"focus:outline-axis-z",
|
||||
disabled && lockedCommonClasses,
|
||||
)}
|
||||
type="text"
|
||||
name="z"
|
||||
onChange={handleOnChange}
|
||||
ref={zRef}
|
||||
value={z}
|
||||
disabled={disabled}
|
||||
onFocus={() => {
|
||||
disableHotkeyInput(DomLevels.INPUT);
|
||||
}}
|
||||
onBlur={() => {
|
||||
enableHotkeyInput(DomLevels.INPUT);
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
handleMouseDown(e, zRef);
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Input } from "./input";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faChevronDown, faChevronUp } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
interface NumberInputProps {
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
}
|
||||
|
||||
export const NumberInput: React.FC<NumberInputProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const [inputValue, setInputValue] = useState<string>(value.toString());
|
||||
|
||||
useEffect(() => {
|
||||
setInputValue(Math.round(value * 100) / 100 + "");
|
||||
}, [value]);
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = event.target.value;
|
||||
if (
|
||||
newValue === "" ||
|
||||
(!isNaN(Number(newValue)) && Number(newValue) <= 100)
|
||||
) {
|
||||
setInputValue(newValue);
|
||||
const numericValue = parseFloat(newValue);
|
||||
if (newValue !== "") {
|
||||
onChange(Math.round(numericValue * 100) / 100);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
if (inputValue === "") {
|
||||
setInputValue(Math.round(value * 100) / 100 + "");
|
||||
}
|
||||
};
|
||||
|
||||
const handleIncrement = () => {
|
||||
const newValue = Math.min(Number(inputValue) + 1, 100);
|
||||
setInputValue(Math.round(newValue * 100) / 100 + "");
|
||||
onChange(Math.round(newValue * 100) / 100);
|
||||
};
|
||||
|
||||
const handleDecrement = () => {
|
||||
const newValue = Math.max(Number(inputValue) - 1, 0);
|
||||
setInputValue(Math.round(newValue * 100) / 100 + "");
|
||||
onChange(Math.round(newValue * 100) / 100);
|
||||
};
|
||||
|
||||
const incrementButtonStyle =
|
||||
"flex h-[15px] w-6 items-center justify-center text-[10px] text-white/80 bg-ui-controls hover:bg-ui-controls-button outline-none focus:outline-none transition-all active:outline-none";
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Input
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
inputClassName="w-[70px] h-[30px] rounded-md text-sm"
|
||||
/>
|
||||
|
||||
<div className="absolute right-0 top-0 flex h-[30px] flex-col rounded-r-md border-l border-white/15 bg-gray-800">
|
||||
<button
|
||||
className={twMerge(
|
||||
incrementButtonStyle,
|
||||
"rounded-tr-md border-b border-white/10",
|
||||
)}
|
||||
onClick={handleIncrement}
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronUp} />
|
||||
</button>
|
||||
<button
|
||||
className={twMerge(incrementButtonStyle, "rounded-br-md")}
|
||||
onClick={handleDecrement}
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronDown} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
/*
|
||||
* Replace this with your own classes
|
||||
*
|
||||
* e.g.
|
||||
* .container {
|
||||
* }
|
||||
*/
|
||||
@@ -0,0 +1,12 @@
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import Input from './input';
|
||||
|
||||
describe('Input', () => {
|
||||
|
||||
it('should render successfully', () => {
|
||||
const { baseElement } = render(<Input />);
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import React from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { IconDefinition } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Label } from "@storyteller/ui-label";
|
||||
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
inputClassName?: string;
|
||||
iconClassName?: string;
|
||||
label?: string;
|
||||
icon?: IconDefinition;
|
||||
isError?: boolean;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export const Input = React.forwardRef(
|
||||
(
|
||||
{
|
||||
label,
|
||||
icon,
|
||||
inputClassName,
|
||||
iconClassName,
|
||||
className,
|
||||
id,
|
||||
isError,
|
||||
onBlur,
|
||||
onFocus,
|
||||
errorMessage,
|
||||
...rest
|
||||
}: InputProps,
|
||||
ref: React.ForwardedRef<HTMLInputElement>
|
||||
) => {
|
||||
return (
|
||||
<div className={twMerge("flex flex-col", className)}>
|
||||
{label && <Label htmlFor={id ? id : label}>{label}</Label>}
|
||||
|
||||
<div className="relative w-full">
|
||||
{icon && (
|
||||
<FontAwesomeIcon
|
||||
icon={icon}
|
||||
className={twMerge("text-md absolute pl-3 pt-3", iconClassName)}
|
||||
/>
|
||||
)}
|
||||
<input
|
||||
ref={ref}
|
||||
id={id ? id : label ? label : undefined}
|
||||
className={twMerge(
|
||||
"h-10 w-full rounded-lg px-3 py-2.5 outline-none",
|
||||
"bg-ui-panel text-base-fg placeholder-base-fg/50",
|
||||
"border border-ui-panel-border transition-all duration-150 ease-in-out hover:border-primary/60 focus:border-primary focus:!outline-none",
|
||||
"disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:border-ui-panel-border",
|
||||
icon && "pl-10",
|
||||
isError && "outline-red focus:outline-red",
|
||||
inputClassName
|
||||
)}
|
||||
onFocus={(e: React.FocusEvent<HTMLInputElement>) => {
|
||||
if (onFocus) {
|
||||
onFocus(e);
|
||||
}
|
||||
}}
|
||||
onBlur={(e: React.FocusEvent<HTMLInputElement>) => {
|
||||
if (onBlur) {
|
||||
onBlur(e);
|
||||
}
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
{errorMessage && (
|
||||
<h6 className="absolute z-10 text-red">{errorMessage}</h6>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Input.displayName = "Input";
|
||||
|
||||
export default Input;
|
||||
Reference in New Issue
Block a user