This commit is contained in:
DCCONSTRUCTIONS
2026-04-18 18:39:25 +03:00
commit 3ba092b60c
4944 changed files with 497564 additions and 0 deletions
@@ -0,0 +1,96 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import * as React from "react";
// helpers
import { cn } from "../utils";
export interface CheckboxProps extends React.InputHTMLAttributes<HTMLInputElement> {
containerClassName?: string;
iconClassName?: string;
indeterminate?: boolean;
}
const Checkbox = React.forwardRef(function Checkbox(props: CheckboxProps, ref: React.ForwardedRef<HTMLInputElement>) {
const {
id,
name,
checked,
indeterminate = false,
disabled,
containerClassName,
iconClassName,
className,
...rest
} = props;
return (
<div className={cn("relative flex flex-shrink-0 gap-2", containerClassName)}>
<input
id={id}
ref={ref}
type="checkbox"
name={name}
checked={checked}
className={cn(
"size-4 shrink-0 cursor-pointer appearance-none rounded-[3px] border focus:outline-1 focus:outline-offset-4 focus:outline-accent-strong",
{
"cursor-not-allowed border-subtle bg-layer-1": disabled,
"border-strong bg-transparent hover:border-strong-1": !disabled,
"border-accent-strong-40 hover:border-accent-strong-40 bg-accent-primary hover:bg-accent-primary/80":
!disabled && (checked || indeterminate),
"border-none": checked,
},
className
)}
disabled={disabled}
{...rest}
/>
<svg
className={cn(
"pointer-events-none absolute top-1/2 left-1/2 hidden size-4 -translate-x-1/2 -translate-y-1/2 p-0.5 text-on-color outline-none",
{
block: checked,
"text-placeholder opacity-40": disabled,
},
iconClassName
)}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="20 6 9 17 4 12" />
</svg>
<svg
className={cn(
"pointer-events-none absolute top-1/2 left-1/2 hidden size-4 -translate-x-1/2 -translate-y-1/2 stroke-white p-0.5 outline-none",
{
"stroke-placeholder opacity-40": disabled,
block: indeterminate && !checked,
},
iconClassName
)}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 8"
fill="none"
stroke="currentColor"
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M5.75 4H2.25" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
);
});
Checkbox.displayName = "form-checkbox-field";
export { Checkbox };
@@ -0,0 +1,12 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
export * from "./input";
export * from "./textarea";
export * from "./input-color-picker";
export * from "./checkbox";
export * from "./root";
export * from "./password";
@@ -0,0 +1,111 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { Popover, Transition } from "@headlessui/react";
import * as React from "react";
import * as ColorPicker from "react-color";
import type { ColorResult } from "react-color";
import { usePopper } from "react-popper";
// helpers
import { Button } from "../button";
import { cn } from "../utils";
// components
import { Input } from "./input";
export interface InputColorPickerProps {
hasError: boolean;
value: string | undefined;
onChange: (value: string) => void;
name: string;
className?: string;
style?: React.CSSProperties;
placeholder: string;
}
export function InputColorPicker(props: InputColorPickerProps) {
const { value, hasError, onChange, name, className, style, placeholder } = props;
const [referenceElement, setReferenceElement] = React.useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = React.useState<HTMLDivElement | null>(null);
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: "auto",
});
const handleColorChange = (newColor: ColorResult) => {
const { hex } = newColor;
onChange(hex);
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e.target.value);
};
return (
<div className="relative">
<Input
id={name}
name={name}
type="text"
value={value}
onChange={handleInputChange}
hasError={hasError}
placeholder={placeholder}
className={cn("border-[0.5px] border-subtle", className)}
style={style}
/>
<Popover as="div" className="absolute top-1/2 right-1 z-10 -translate-y-1/2">
{() => (
<>
<Popover.Button as={React.Fragment}>
<Button ref={setReferenceElement} variant="neutral-primary" className="border-none !bg-transparent">
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="lucide lucide-palette"
>
<circle cx="13.5" cy="6.5" r=".5" />
<circle cx="17.5" cy="10.5" r=".5" />
<circle cx="8.5" cy="7.5" r=".5" />
<circle cx="6.5" cy="12.5" r=".5" />
<path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z" />
</svg>
</Button>
</Popover.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel>
<div
className="z-10 overflow-hidden rounded-sm border border-subtle bg-surface-1 shadow-raised-200"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<ColorPicker.SketchPicker color={value} onChange={handleColorChange} />
</div>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
</div>
);
}
@@ -0,0 +1,59 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import * as React from "react";
// helpers
import { cn } from "../utils";
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
mode?: "primary" | "transparent" | "true-transparent";
inputSize?: "xs" | "sm" | "md";
hasError?: boolean;
className?: string;
}
const Input = React.forwardRef(function Input(props: InputProps, ref: React.ForwardedRef<HTMLInputElement>) {
const {
id,
type,
name,
mode = "primary",
inputSize = "sm",
hasError = false,
className = "",
autoComplete = "off",
...rest
} = props;
return (
<input
id={id}
ref={ref}
type={type}
name={name}
className={cn(
"placeholder-tertiary block rounded-md border-subtle-1 bg-layer-2 text-13 focus:outline-none",
{
"rounded-md border-[0.5px]": mode === "primary",
"rounded-sm border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-accent-strong":
mode === "transparent",
"rounded-sm border-none bg-transparent ring-0": mode === "true-transparent",
"border-danger-strong": hasError,
"px-1.5 py-1": inputSize === "xs",
"px-3 py-2": inputSize === "sm",
"p-3": inputSize === "md",
},
className
)}
autoComplete={autoComplete}
{...rest}
/>
);
});
Input.displayName = "form-input-field";
export { Input };
@@ -0,0 +1,71 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { E_PASSWORD_STRENGTH } from "@plane/constants";
export interface StrengthInfo {
message: string;
textColor: string;
activeFragments: number;
}
/**
* Get strength information including message, color, and active fragments
*/
export const getStrengthInfo = (strength: E_PASSWORD_STRENGTH): StrengthInfo => {
switch (strength) {
case E_PASSWORD_STRENGTH.EMPTY:
return {
message: "Please enter your password",
textColor: "text-primary",
activeFragments: 0,
};
case E_PASSWORD_STRENGTH.LENGTH_NOT_VALID:
return {
message: "Password is too short",
textColor: "text-danger-primary",
activeFragments: 1,
};
case E_PASSWORD_STRENGTH.STRENGTH_NOT_VALID:
return {
message: "Password does not meet security requirements",
textColor: "text-orange-500",
activeFragments: 2,
};
case E_PASSWORD_STRENGTH.STRENGTH_VALID:
return {
message: "Password meets security requirements",
textColor: "text-success-primary",
activeFragments: 3,
};
default:
return {
message: "Please enter your password",
textColor: "text-primary",
activeFragments: 0,
};
}
};
/**
* Get fragment color based on position and active state
*/
export const getFragmentColor = (fragmentIndex: number, activeFragments: number): string => {
if (fragmentIndex >= activeFragments) {
return "bg-layer-1";
}
switch (activeFragments) {
case 1:
return "bg-danger-primary";
case 2:
return "bg-orange-500";
case 3:
return "bg-success-primary";
default:
return "bg-layer-1";
}
};
@@ -0,0 +1,9 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
export * from "./indicator";
export * from "./helper";
export * from "./password-input";
@@ -0,0 +1,81 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { CircleCheck } from "lucide-react";
import React from "react";
import { E_PASSWORD_STRENGTH } from "@plane/constants";
import { cn, getPasswordStrength, getPasswordCriteria } from "@plane/utils";
import { getStrengthInfo, getFragmentColor } from "./helper";
export interface PasswordStrengthIndicatorProps {
password: string;
showCriteria?: boolean;
isFocused?: boolean;
}
export function PasswordStrengthIndicator({
password,
showCriteria = true,
isFocused = false,
}: PasswordStrengthIndicatorProps) {
const strength = getPasswordStrength(password);
const criteria = getPasswordCriteria(password);
const strengthInfo = getStrengthInfo(strength);
const isPasswordMeterVisible = isFocused ? true : strength === E_PASSWORD_STRENGTH.STRENGTH_VALID ? false : true;
if ((!password && !showCriteria) || !isPasswordMeterVisible) {
return null;
}
return (
<div className={cn("space-y-3")}>
{/* Strength Indicator */}
<div className="space-y-2">
<div className="flex w-full gap-1 transition-all duration-300 ease-linear">
{[0, 1, 2].map((fragmentIndex) => (
<div
key={fragmentIndex}
className={cn(
"h-1 flex-1 rounded-xs transition-all duration-300 ease-in-out",
getFragmentColor(fragmentIndex, strengthInfo.activeFragments)
)}
/>
))}
</div>
{/* Strength Message */}
{password && <p className={cn("!text-13 font-medium", strengthInfo.textColor)}>{strengthInfo.message}</p>}
</div>
{/* Criteria list */}
{showCriteria && (
<div className="flex flex-wrap gap-2">
{criteria.map((criterion) => (
<div key={criterion.key} className="flex items-center gap-1.5">
<div className="flex items-center justify-center p-0.5">
<CircleCheck
className={cn("h-3 w-3 flex-shrink-0", {
"text-success-primary": criterion.isValid,
"text-primary": !criterion.isValid,
})}
/>
</div>
<span
className={cn("!text-11", {
"text-success-primary": criterion.isValid,
"text-primary": !criterion.isValid,
})}
>
{criterion.label}
</span>
</div>
))}
</div>
)}
</div>
);
}
@@ -0,0 +1,78 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { Eye, EyeClosed } from "lucide-react";
import { useState } from "react";
import { Tooltip } from "@plane/propel/tooltip";
import { cn } from "@plane/utils";
type TPasswordInputProps = {
id: string;
value: string;
onChange: (value: string) => void;
placeholder?: string;
className?: string;
showToggle?: boolean;
error?: boolean;
autoComplete?: React.HTMLInputAutoCompleteAttribute;
};
export function PasswordInput({
id,
value,
onChange,
placeholder = "Enter your password",
className,
showToggle = true,
error = false,
autoComplete = "off",
}: TPasswordInputProps) {
const [showPassword, setShowPassword] = useState(false);
return (
<div className="relative">
<input
id={id}
type={showPassword ? "text" : "password"}
value={value}
onChange={(e) => onChange(e.target.value)}
className={cn(
"w-full rounded-md border bg-surface-1 px-3 py-2 pr-10 text-secondary transition-all duration-200 placeholder:text-placeholder focus:border-transparent focus:ring-2 focus:ring-accent-strong focus:outline-none",
{
"border-strong": !error,
"border-danger-strong": error,
},
className
)}
placeholder={placeholder}
autoComplete={autoComplete}
/>
{showToggle && (
<Tooltip tooltipContent={showPassword ? "Hide password" : "Show password"} position="top">
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-secondary transition-colors duration-200 hover:text-primary"
>
<div className="relative h-4 w-4">
<Eye
className={cn(
"absolute inset-0 h-4 w-4 transition-all duration-300 ease-in-out",
showPassword ? "scale-75 rotate-12 opacity-0" : "scale-100 rotate-0 opacity-100"
)}
/>
<EyeClosed
className={cn(
"absolute inset-0 h-4 w-4 transition-all duration-300 ease-in-out",
showPassword ? "scale-100 rotate-0 opacity-100" : "scale-75 -rotate-12 opacity-0"
)}
/>
</div>
</button>
</Tooltip>
)}
</div>
);
}
@@ -0,0 +1,68 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import React from "react";
import { cn } from "@plane/utils";
// Reusable Label Component
interface LabelProps {
htmlFor: string;
children: React.ReactNode;
className?: string;
}
export function Label({ htmlFor, children, className }: LabelProps) {
return (
<label htmlFor={htmlFor} className={cn("block text-13 font-medium text-primary", className)}>
{children}
</label>
);
}
// Reusable Form Field Component
interface FormFieldProps {
label: string;
htmlFor: string;
children: React.ReactNode;
className?: string;
optional?: boolean;
}
export function FormField({ label, htmlFor, children, className, optional = false }: FormFieldProps) {
return (
<div className={cn("flex flex-col gap-1.5", className)}>
<Label htmlFor={htmlFor}>
{label}
{optional && <span className="text-13 text-placeholder"> (optional)</span>}
</Label>
{children}
</div>
);
}
// Reusable Validation Message Component
interface ValidationMessageProps {
type: "error" | "success";
message: string;
className?: string;
}
export function ValidationMessage({ type, message, className }: ValidationMessageProps) {
return (
<p
className={cn(
"text-13",
{
"text-danger-primary": type === "error",
"text-success-primary": type === "success",
},
className
)}
>
{message}
</p>
);
}
@@ -0,0 +1,67 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import React, { useRef } from "react";
// helpers
import { useAutoResizeTextArea } from "../hooks/use-auto-resize-textarea";
import { cn } from "../utils";
// hooks
export interface TextAreaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
mode?: "primary" | "transparent" | "true-transparent";
textAreaSize?: "xs" | "sm" | "md";
hasError?: boolean;
className?: string;
}
const TextArea = React.forwardRef(function TextArea(
props: TextAreaProps,
ref: React.ForwardedRef<HTMLTextAreaElement>
) {
const {
id,
name,
value = "",
mode = "primary",
textAreaSize = "sm",
hasError = false,
className = "",
...rest
} = props;
// refs
const textAreaRef = useRef<any>(ref);
// auto re-size
useAutoResizeTextArea(textAreaRef, value);
return (
<textarea
id={id}
name={name}
ref={textAreaRef}
value={value}
className={cn(
"no-scrollbar w-full bg-layer-2 placeholder-(--text-color-placeholder) outline-none",
{
"rounded-md border-[0.5px] border-subtle-1": mode === "primary",
"focus:ring-theme rounded-sm border-none bg-transparent ring-0 transition-all focus:ring-1":
mode === "transparent",
"rounded-sm border-none bg-transparent ring-0": mode === "true-transparent",
"px-1.5 py-1": textAreaSize === "xs",
"px-3 py-2": textAreaSize === "sm",
"p-3": textAreaSize === "md",
"border-danger-strong": hasError,
"bg-danger-subtle": hasError && mode === "primary",
},
className
)}
{...rest}
/>
);
});
TextArea.displayName = "TextArea";
export { TextArea };