mirror of
https://github.com/makeplane/plane.git
synced 2026-08-29 10:08:51 +02:00
chore: remove unused @plane/ui exports (#9701)
* chore: remove unused @plane/ui exports Drop barrel symbols and source that nothing in the monorepo imported, and drop @radix-ui/react-scroll-area with them. Keep Button source internal for InputColorPicker. * chore: format leftover dropdown comboButton indent
This commit is contained in:
committed by
GitHub
parent
3fadd7ac58
commit
0495678f7c
@@ -186,10 +186,7 @@ export const WorkItemStateDropdownBase = observer(function WorkItemStateDropdown
|
|||||||
<span className="flex-grow truncate text-left">{selectedState?.name ?? t("state")}</span>
|
<span className="flex-grow truncate text-left">{selectedState?.name ?? t("state")}</span>
|
||||||
)}
|
)}
|
||||||
{dropdownArrow && (
|
{dropdownArrow && (
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||||
className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -188,10 +188,7 @@ export const WorkItemStateDropdownBase = observer(function WorkItemStateDropdown
|
|||||||
<span className="flex-grow truncate text-left">{selectedState?.name ?? t("state")}</span>
|
<span className="flex-grow truncate text-left">{selectedState?.name ?? t("state")}</span>
|
||||||
)}
|
)}
|
||||||
{dropdownArrow && (
|
{dropdownArrow && (
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||||
className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -36,7 +36,6 @@
|
|||||||
"@plane/types": "workspace:*",
|
"@plane/types": "workspace:*",
|
||||||
"@plane/utils": "workspace:*",
|
"@plane/utils": "workspace:*",
|
||||||
"@popperjs/core": "catalog:",
|
"@popperjs/core": "catalog:",
|
||||||
"@radix-ui/react-scroll-area": "catalog:",
|
|
||||||
"clsx": "catalog:",
|
"clsx": "catalog:",
|
||||||
"lodash-es": "catalog:",
|
"lodash-es": "catalog:",
|
||||||
"lucide-react": "catalog:",
|
"lucide-react": "catalog:",
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* See the LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import { cn } from "@plane/utils";
|
|
||||||
import { AuthInput } from "./auth-input";
|
|
||||||
|
|
||||||
export type TAuthConfirmPasswordInputProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
|
||||||
password: string;
|
|
||||||
label?: string;
|
|
||||||
error?: string;
|
|
||||||
showPasswordToggle?: boolean;
|
|
||||||
containerClassName?: string;
|
|
||||||
labelClassName?: string;
|
|
||||||
errorClassName?: string;
|
|
||||||
onPasswordMatchChange?: (matches: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function AuthConfirmPasswordInput({
|
|
||||||
password,
|
|
||||||
label = "Confirm Password",
|
|
||||||
error,
|
|
||||||
showPasswordToggle = true,
|
|
||||||
containerClassName = "",
|
|
||||||
errorClassName = "",
|
|
||||||
className = "",
|
|
||||||
value = "",
|
|
||||||
onChange,
|
|
||||||
onPasswordMatchChange,
|
|
||||||
...props
|
|
||||||
}: TAuthConfirmPasswordInputProps) {
|
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
|
||||||
|
|
||||||
const confirmPassword = value as string;
|
|
||||||
const passwordsMatch = password === confirmPassword && password.length > 0;
|
|
||||||
const showMatchError =
|
|
||||||
confirmPassword.length > 0 && !passwordsMatch && (!isFocused || confirmPassword.length >= password.length);
|
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const newConfirmPassword = e.target.value;
|
|
||||||
onChange?.(e);
|
|
||||||
onPasswordMatchChange?.(password === newConfirmPassword && password.length > 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFocus = () => {
|
|
||||||
setIsFocused(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBlur = () => {
|
|
||||||
setIsFocused(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getError = () => {
|
|
||||||
if (error) return error;
|
|
||||||
if (showMatchError) return "Passwords don't match";
|
|
||||||
return "";
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn("space-y-2", containerClassName)}>
|
|
||||||
<AuthInput
|
|
||||||
{...props}
|
|
||||||
type="password"
|
|
||||||
label={label}
|
|
||||||
error={getError()}
|
|
||||||
showPasswordToggle={showPasswordToggle}
|
|
||||||
errorClassName={errorClassName}
|
|
||||||
className={className}
|
|
||||||
value={value}
|
|
||||||
onChange={handleChange}
|
|
||||||
onFocus={handleFocus}
|
|
||||||
onBlur={handleBlur}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
{confirmPassword && passwordsMatch && <p className="text-13 text-success-primary">Passwords match</p>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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 "../utils";
|
|
||||||
|
|
||||||
export interface AuthForgotPasswordProps {
|
|
||||||
onForgotPassword?: () => void;
|
|
||||||
className?: string;
|
|
||||||
text?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AuthForgotPassword({
|
|
||||||
onForgotPassword,
|
|
||||||
className = "",
|
|
||||||
text = "Forgot your password?",
|
|
||||||
disabled = false,
|
|
||||||
}: AuthForgotPasswordProps) {
|
|
||||||
const handleClick = (e: React.MouseEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!disabled && onForgotPassword) {
|
|
||||||
onForgotPassword();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleClick}
|
|
||||||
disabled={disabled}
|
|
||||||
className={cn(
|
|
||||||
"text-13 text-accent-primary transition-colors duration-200 hover:text-accent-secondary",
|
|
||||||
{
|
|
||||||
"cursor-not-allowed opacity-50": disabled,
|
|
||||||
"cursor-pointer": !disabled,
|
|
||||||
},
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* See the LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { useState, useMemo } from "react";
|
|
||||||
import { E_PASSWORD_STRENGTH } from "@plane/constants";
|
|
||||||
import { Button } from "../button/button";
|
|
||||||
import { Spinner } from "../spinners/circular-spinner";
|
|
||||||
import { cn } from "../utils";
|
|
||||||
import { AuthConfirmPasswordInput } from "./auth-confirm-password-input";
|
|
||||||
import { AuthForgotPassword } from "./auth-forgot-password";
|
|
||||||
import { AuthInput } from "./auth-input";
|
|
||||||
import { AuthPasswordInput } from "./auth-password-input";
|
|
||||||
|
|
||||||
export type AuthMode = "sign-in" | "sign-up";
|
|
||||||
|
|
||||||
export interface AuthFormData {
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
confirmPassword?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AuthFormProps {
|
|
||||||
mode: AuthMode;
|
|
||||||
initialData?: Partial<AuthFormData>;
|
|
||||||
onSubmit?: (data: AuthFormData) => void;
|
|
||||||
onForgotPassword?: () => void;
|
|
||||||
onModeChange?: (mode: AuthMode) => void;
|
|
||||||
loading?: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
className?: string;
|
|
||||||
showForgotPassword?: boolean;
|
|
||||||
showPasswordStrength?: boolean;
|
|
||||||
emailError?: string;
|
|
||||||
passwordError?: string;
|
|
||||||
confirmPasswordError?: string;
|
|
||||||
submitButtonText?: string;
|
|
||||||
alternateModeText?: string;
|
|
||||||
alternateModeButtonText?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AuthForm({
|
|
||||||
mode,
|
|
||||||
initialData = {},
|
|
||||||
onSubmit,
|
|
||||||
onForgotPassword,
|
|
||||||
onModeChange,
|
|
||||||
loading = false,
|
|
||||||
disabled = false,
|
|
||||||
className = "",
|
|
||||||
showForgotPassword = true,
|
|
||||||
showPasswordStrength = true,
|
|
||||||
emailError,
|
|
||||||
passwordError,
|
|
||||||
confirmPasswordError,
|
|
||||||
submitButtonText,
|
|
||||||
alternateModeText,
|
|
||||||
alternateModeButtonText,
|
|
||||||
}: AuthFormProps) {
|
|
||||||
const [formData, setFormData] = useState<AuthFormData>({
|
|
||||||
email: initialData.email || "",
|
|
||||||
password: initialData.password || "",
|
|
||||||
confirmPassword: initialData.confirmPassword || "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const [passwordStrength, setPasswordStrength] = useState<E_PASSWORD_STRENGTH>(E_PASSWORD_STRENGTH.EMPTY);
|
|
||||||
const [_passwordsMatch, setPasswordsMatch] = useState(false);
|
|
||||||
|
|
||||||
const handleInputChange = (field: keyof AuthFormData) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setFormData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[field]: e.target.value,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePasswordChange = (password: string) => {
|
|
||||||
setFormData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
password,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePasswordStrengthChange = (strength: E_PASSWORD_STRENGTH) => {
|
|
||||||
setPasswordStrength(strength);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleConfirmPasswordChange = (matches: boolean) => {
|
|
||||||
setPasswordsMatch(matches);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (onSubmit && isFormValid) {
|
|
||||||
onSubmit(formData);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleModeChange = () => {
|
|
||||||
const newMode = mode === "sign-in" ? "sign-up" : "sign-in";
|
|
||||||
onModeChange?.(newMode);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isFormValid = useMemo(() => {
|
|
||||||
const hasEmail = formData.email.length > 0;
|
|
||||||
const hasPassword = formData.password.length > 0;
|
|
||||||
|
|
||||||
if (mode === "sign-in") {
|
|
||||||
return hasEmail && hasPassword && !loading && !disabled;
|
|
||||||
} else {
|
|
||||||
const isPasswordStrong = passwordStrength === E_PASSWORD_STRENGTH.STRENGTH_VALID;
|
|
||||||
const passwordsMatch = formData.password === formData.confirmPassword && formData.password.length > 0;
|
|
||||||
return hasEmail && hasPassword && isPasswordStrong && passwordsMatch && !loading && !disabled;
|
|
||||||
}
|
|
||||||
}, [mode, formData, passwordStrength, loading, disabled]);
|
|
||||||
|
|
||||||
const getSubmitButtonText = () => {
|
|
||||||
if (submitButtonText) return submitButtonText;
|
|
||||||
return mode === "sign-in" ? "Sign In" : "Create Account";
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAlternateModeText = () => {
|
|
||||||
if (alternateModeText) return alternateModeText;
|
|
||||||
return mode === "sign-in" ? "Don't have an account?" : "Already have an account?";
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAlternateModeButtonText = () => {
|
|
||||||
if (alternateModeButtonText) return alternateModeButtonText;
|
|
||||||
return mode === "sign-in" ? "Sign Up" : "Sign In";
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit} className={cn("space-y-4", className)}>
|
|
||||||
{/* Email Input */}
|
|
||||||
<AuthInput
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
label="Email"
|
|
||||||
value={formData.email}
|
|
||||||
onChange={handleInputChange("email")}
|
|
||||||
placeholder="name@company.com"
|
|
||||||
error={emailError}
|
|
||||||
disabled={disabled}
|
|
||||||
// autoComplete="email"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Password Input */}
|
|
||||||
<AuthPasswordInput
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
label={mode === "sign-in" ? "Password" : "Set a password"}
|
|
||||||
value={formData.password}
|
|
||||||
onChange={handleInputChange("password")}
|
|
||||||
onPasswordChange={handlePasswordChange}
|
|
||||||
onPasswordStrengthChange={handlePasswordStrengthChange}
|
|
||||||
placeholder="Enter password"
|
|
||||||
error={passwordError}
|
|
||||||
showPasswordStrength={showPasswordStrength && mode === "sign-up"}
|
|
||||||
disabled={disabled}
|
|
||||||
// autoComplete={mode === "sign-in" ? "current-password" : "new-password"}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Confirm Password Input (Sign Up Only) */}
|
|
||||||
{mode === "sign-up" && (
|
|
||||||
<AuthConfirmPasswordInput
|
|
||||||
id="confirmPassword"
|
|
||||||
name="confirmPassword"
|
|
||||||
password={formData.password}
|
|
||||||
value={formData.confirmPassword}
|
|
||||||
onChange={handleInputChange("confirmPassword")}
|
|
||||||
onPasswordMatchChange={handleConfirmPasswordChange}
|
|
||||||
error={confirmPasswordError}
|
|
||||||
disabled={disabled}
|
|
||||||
// autoComplete="new-password"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Forgot Password Link (Sign In Only) */}
|
|
||||||
{mode === "sign-in" && showForgotPassword && (
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<AuthForgotPassword onForgotPassword={onForgotPassword} disabled={disabled} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Submit Button */}
|
|
||||||
<div className="space-y-2.5">
|
|
||||||
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={!isFormValid} loading={loading}>
|
|
||||||
{loading ? <Spinner height="20px" width="20px" /> : getSubmitButtonText()}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* Alternate Mode Button */}
|
|
||||||
{onModeChange && (
|
|
||||||
<div className="text-center">
|
|
||||||
<span className="text-13 text-tertiary">{getAlternateModeText()}</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleModeChange}
|
|
||||||
className="ml-1 text-13 text-accent-primary transition-colors duration-200 hover:text-accent-secondary"
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
{getAlternateModeButtonText()}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* See the LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Eye, EyeOff } from "lucide-react";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import { Input } from "../form-fields/input";
|
|
||||||
import { cn } from "../utils";
|
|
||||||
|
|
||||||
export type TAuthInputProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
|
||||||
label?: string;
|
|
||||||
error?: string;
|
|
||||||
showPasswordToggle?: boolean;
|
|
||||||
errorClassName?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const baseContainerClassName = "flex flex-col gap-1.5";
|
|
||||||
|
|
||||||
export function AuthInput({
|
|
||||||
label,
|
|
||||||
error,
|
|
||||||
showPasswordToggle = false,
|
|
||||||
errorClassName = "",
|
|
||||||
className = "",
|
|
||||||
type = "text",
|
|
||||||
autoComplete = "off",
|
|
||||||
...props
|
|
||||||
}: TAuthInputProps) {
|
|
||||||
const { id } = props;
|
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
|
||||||
const isPasswordType = type === "password";
|
|
||||||
|
|
||||||
const inputType = isPasswordType && showPasswordToggle && showPassword ? "text" : type;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn(baseContainerClassName)}>
|
|
||||||
{label && (
|
|
||||||
<label htmlFor={id} className={cn("text-13 font-semibold text-tertiary")}>
|
|
||||||
{label}
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
<div className={cn("relative flex items-center rounded-md border border-strong px-3 py-2 transition-all")}>
|
|
||||||
<Input
|
|
||||||
{...props}
|
|
||||||
type={inputType}
|
|
||||||
autoComplete={autoComplete}
|
|
||||||
className={cn(
|
|
||||||
"h-6 w-full rounded-md border-none p-0 disable-autofill-style placeholder:text-14 placeholder:text-placeholder",
|
|
||||||
{
|
|
||||||
"border-danger-strong": error,
|
|
||||||
},
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{showPasswordToggle && isPasswordType && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
|
||||||
className="absolute right-3 h-5 w-5 stroke-placeholder hover:cursor-pointer"
|
|
||||||
>
|
|
||||||
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && <p className={cn("text-13 text-danger-primary", errorClassName)}>{error}</p>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* See the LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import type { E_PASSWORD_STRENGTH } from "@plane/constants";
|
|
||||||
import { cn, getPasswordStrength } from "@plane/utils";
|
|
||||||
import { PasswordStrengthIndicator } from "../form-fields/password/indicator";
|
|
||||||
import { AuthInput } from "./auth-input";
|
|
||||||
|
|
||||||
export type TAuthPasswordInputProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
|
||||||
label?: string;
|
|
||||||
error?: string;
|
|
||||||
showPasswordStrength?: boolean;
|
|
||||||
showPasswordToggle?: boolean;
|
|
||||||
containerClassName?: string;
|
|
||||||
errorClassName?: string;
|
|
||||||
onPasswordChange?: (password: string) => void;
|
|
||||||
onPasswordStrengthChange?: (strength: E_PASSWORD_STRENGTH) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function AuthPasswordInput({
|
|
||||||
label = "Password",
|
|
||||||
error,
|
|
||||||
showPasswordStrength = true,
|
|
||||||
showPasswordToggle = true,
|
|
||||||
containerClassName = "",
|
|
||||||
errorClassName = "",
|
|
||||||
className = "",
|
|
||||||
value = "",
|
|
||||||
onChange,
|
|
||||||
onPasswordChange,
|
|
||||||
onPasswordStrengthChange,
|
|
||||||
...props
|
|
||||||
}: TAuthPasswordInputProps) {
|
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const newPassword = e.target.value;
|
|
||||||
onChange?.(e);
|
|
||||||
onPasswordChange?.(newPassword);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFocus = () => {
|
|
||||||
setIsFocused(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBlur = () => {
|
|
||||||
setIsFocused(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const passwordStrength = getPasswordStrength(value as string);
|
|
||||||
|
|
||||||
// Notify parent of strength change
|
|
||||||
React.useEffect(() => {
|
|
||||||
onPasswordStrengthChange?.(passwordStrength);
|
|
||||||
}, [passwordStrength, onPasswordStrengthChange]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn("space-y-2", containerClassName)}>
|
|
||||||
<AuthInput
|
|
||||||
{...props}
|
|
||||||
type="password"
|
|
||||||
label={label}
|
|
||||||
error={error}
|
|
||||||
showPasswordToggle={showPasswordToggle}
|
|
||||||
errorClassName={errorClassName}
|
|
||||||
className={className}
|
|
||||||
value={value}
|
|
||||||
onChange={handleChange}
|
|
||||||
onFocus={handleFocus}
|
|
||||||
onBlur={handleBlur}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
{showPasswordStrength && value && isFocused && (
|
|
||||||
<PasswordStrengthIndicator password={value as string} showCriteria />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* See the LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export { AuthForm } from "./auth-form";
|
|
||||||
export { AuthInput } from "./auth-input";
|
|
||||||
export { AuthPasswordInput } from "./auth-password-input";
|
|
||||||
export { AuthConfirmPasswordInput } from "./auth-confirm-password-input";
|
|
||||||
export { AuthForgotPassword } from "./auth-forgot-password";
|
|
||||||
|
|
||||||
export type { AuthFormProps, AuthFormData, AuthMode } from "./auth-form";
|
|
||||||
export type { TAuthInputProps } from "./auth-input";
|
|
||||||
export type { TAuthPasswordInputProps } from "./auth-password-input";
|
|
||||||
export type { TAuthConfirmPasswordInputProps } from "./auth-confirm-password-input";
|
|
||||||
export type { AuthForgotPasswordProps } from "./auth-forgot-password";
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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";
|
|
||||||
import type { TBadgeVariant, TBadgeSizes } from "./helper";
|
|
||||||
import { getIconStyling, getBadgeStyling } from "./helper";
|
|
||||||
|
|
||||||
export interface BadgeProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
||||||
variant?: TBadgeVariant;
|
|
||||||
size?: TBadgeSizes;
|
|
||||||
className?: string;
|
|
||||||
loading?: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
appendIcon?: any;
|
|
||||||
prependIcon?: any;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Badge = React.forwardRef(function Badge(props: BadgeProps, ref: React.ForwardedRef<HTMLButtonElement>) {
|
|
||||||
const {
|
|
||||||
variant = "primary",
|
|
||||||
size = "md",
|
|
||||||
className = "",
|
|
||||||
type = "button",
|
|
||||||
loading = false,
|
|
||||||
disabled = false,
|
|
||||||
prependIcon = null,
|
|
||||||
appendIcon = null,
|
|
||||||
children,
|
|
||||||
...rest
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const buttonStyle = getBadgeStyling(variant, size, disabled || loading);
|
|
||||||
const buttonIconStyle = getIconStyling(size);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button ref={ref} type={type} className={cn(buttonStyle, className)} disabled={disabled || loading} {...rest}>
|
|
||||||
{prependIcon && <div className={buttonIconStyle}>{React.cloneElement(prependIcon, { strokeWidth: 2 })}</div>}
|
|
||||||
{children}
|
|
||||||
{appendIcon && <div className={buttonIconStyle}>{React.cloneElement(appendIcon, { strokeWidth: 2 })}</div>}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
Badge.displayName = "plane-ui-badge";
|
|
||||||
|
|
||||||
export { Badge };
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* See the LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type TBadgeVariant =
|
|
||||||
| "primary"
|
|
||||||
| "accent-primary"
|
|
||||||
| "outline-primary"
|
|
||||||
| "neutral"
|
|
||||||
| "accent-neutral"
|
|
||||||
| "outline-neutral"
|
|
||||||
| "success"
|
|
||||||
| "accent-success"
|
|
||||||
| "outline-success"
|
|
||||||
| "warning"
|
|
||||||
| "accent-warning"
|
|
||||||
| "outline-warning"
|
|
||||||
| "destructive"
|
|
||||||
| "accent-destructive"
|
|
||||||
| "outline-destructive";
|
|
||||||
|
|
||||||
export type TBadgeSizes = "sm" | "md" | "lg" | "xl";
|
|
||||||
|
|
||||||
export interface IBadgeStyling {
|
|
||||||
[key: string]: {
|
|
||||||
default: string;
|
|
||||||
hover: string;
|
|
||||||
disabled: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: convert them to objects instead of enums
|
|
||||||
enum badgeSizeStyling {
|
|
||||||
sm = `px-2.5 py-1 font-medium text-11 rounded-sm flex items-center gap-1.5 whitespace-nowrap transition-all justify-center inline`,
|
|
||||||
md = `px-4 py-1.5 font-medium text-13 rounded-sm flex items-center gap-1.5 whitespace-nowrap transition-all justify-center inline`,
|
|
||||||
lg = `px-4 py-2 font-medium text-13 rounded-sm flex items-center gap-1.5 whitespace-nowrap transition-all justify-center inline`,
|
|
||||||
xl = `px-5 py-3 font-medium text-13 rounded-sm flex items-center gap-1.5 whitespace-nowrap transition-all justify-center inline`,
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: convert them to objects instead of enums
|
|
||||||
enum badgeIconStyling {
|
|
||||||
sm = "h-3 w-3 flex justify-center items-center overflow-hidden flex-shrink-0",
|
|
||||||
md = "h-3.5 w-3.5 flex justify-center items-center overflow-hidden flex-shrink-0",
|
|
||||||
lg = "h-4 w-4 flex justify-center items-center overflow-hidden flex-shrink-0",
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values
|
|
||||||
xl = "h-4 w-4 flex justify-center items-center overflow-hidden flex-shrink-0",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const badgeStyling: IBadgeStyling = {
|
|
||||||
primary: {
|
|
||||||
default: `text-on-color bg-accent-primary`,
|
|
||||||
hover: `hover:bg-accent-primary/80`,
|
|
||||||
disabled: `cursor-not-allowed !bg-custom-primary-60 hover:bg-custom-primary-60`,
|
|
||||||
},
|
|
||||||
"accent-primary": {
|
|
||||||
default: `bg-accent-subtle text-accent-primary`,
|
|
||||||
hover: `hover:bg-custom-primary-20 hover:text-accent-secondary`,
|
|
||||||
disabled: `cursor-not-allowed !text-accent-primary/60`,
|
|
||||||
},
|
|
||||||
"outline-primary": {
|
|
||||||
default: `text-accent-primary bg-surface-1 border border-accent-strong`,
|
|
||||||
hover: `hover:border-accent-strong-80 hover:bg-accent-subtle`,
|
|
||||||
disabled: `cursor-not-allowed !text-accent-primary/60 !border-accent-strong-60 `,
|
|
||||||
},
|
|
||||||
|
|
||||||
neutral: {
|
|
||||||
default: `text-custom-background-100 bg-layer-1 border border-subtle`,
|
|
||||||
hover: `hover:bg-layer-1`,
|
|
||||||
disabled: `cursor-not-allowed bg-subtle-1 !text-placeholder`,
|
|
||||||
},
|
|
||||||
"accent-neutral": {
|
|
||||||
default: `text-secondary bg-layer-1`,
|
|
||||||
hover: `hover:bg-subtle-1 hover:text-primary`,
|
|
||||||
disabled: `cursor-not-allowed !text-placeholder`,
|
|
||||||
},
|
|
||||||
"outline-neutral": {
|
|
||||||
default: `text-secondary bg-surface-1 border border-subtle`,
|
|
||||||
hover: `hover:text-primary hover:bg-subtle-1`,
|
|
||||||
disabled: `cursor-not-allowed !text-placeholder`,
|
|
||||||
},
|
|
||||||
|
|
||||||
success: {
|
|
||||||
default: `text-on-color bg-green-500`,
|
|
||||||
hover: `hover:bg-green-600`,
|
|
||||||
disabled: `cursor-not-allowed !bg-green-300`,
|
|
||||||
},
|
|
||||||
"accent-success": {
|
|
||||||
default: `text-success-primary bg-green-50`,
|
|
||||||
hover: `hover:bg-green-100 hover:text-success-primary`,
|
|
||||||
disabled: `cursor-not-allowed text-success-secondary!`,
|
|
||||||
},
|
|
||||||
"outline-success": {
|
|
||||||
default: `text-success-primary bg-surface-1 border border-success-strong`,
|
|
||||||
hover: `hover:text-success-primary hover:bg-green-50`,
|
|
||||||
disabled: `cursor-not-allowed text-success-secondary! border-success-subtle`,
|
|
||||||
},
|
|
||||||
|
|
||||||
warning: {
|
|
||||||
default: `text-on-color bg-amber-500`,
|
|
||||||
hover: `hover:bg-amber-600`,
|
|
||||||
disabled: `cursor-not-allowed !bg-amber-300`,
|
|
||||||
},
|
|
||||||
"accent-warning": {
|
|
||||||
default: `text-amber-500 bg-amber-50`,
|
|
||||||
hover: `hover:bg-amber-100 hover:text-amber-600`,
|
|
||||||
disabled: `cursor-not-allowed !text-amber-300`,
|
|
||||||
},
|
|
||||||
"outline-warning": {
|
|
||||||
default: `text-amber-500 bg-surface-1 border border-amber-500`,
|
|
||||||
hover: `hover:text-amber-600 hover:bg-amber-50`,
|
|
||||||
disabled: `cursor-not-allowed !text-amber-300 border-amber-300`,
|
|
||||||
},
|
|
||||||
|
|
||||||
destructive: {
|
|
||||||
default: `text-on-color bg-red-500`,
|
|
||||||
hover: `hover:bg-red-600`,
|
|
||||||
disabled: `cursor-not-allowed !bg-red-300`,
|
|
||||||
},
|
|
||||||
"accent-destructive": {
|
|
||||||
default: `text-danger-primary bg-red-50`,
|
|
||||||
hover: `hover:bg-red-100 hover:text-danger-primary`,
|
|
||||||
disabled: `cursor-not-allowed text-danger-secondary!`,
|
|
||||||
},
|
|
||||||
"outline-destructive": {
|
|
||||||
default: `text-danger-primary bg-surface-1 border border-danger-strong`,
|
|
||||||
hover: `hover:text-danger-primary hover:bg-red-50`,
|
|
||||||
disabled: `cursor-not-allowed text-danger-secondary! border-danger-subtle`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBadgeStyling = (variant: TBadgeVariant, size: TBadgeSizes, disabled: boolean = false): string => {
|
|
||||||
let tempVariant: string = ``;
|
|
||||||
const currentVariant = badgeStyling[variant];
|
|
||||||
|
|
||||||
tempVariant = `${currentVariant.default} ${disabled ? currentVariant.disabled : currentVariant.hover}`;
|
|
||||||
|
|
||||||
let tempSize: string = ``;
|
|
||||||
if (size) tempSize = badgeSizeStyling[size];
|
|
||||||
return `${tempVariant} ${tempSize}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getIconStyling = (size: TBadgeSizes): string => {
|
|
||||||
let icon: string = ``;
|
|
||||||
if (size) icon = badgeIconStyling[size];
|
|
||||||
return icon;
|
|
||||||
};
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* See the LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from "./badge";
|
|
||||||
@@ -5,11 +5,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Meta, StoryObj } from "@storybook/react";
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
import { Home, Settings, Briefcase, GridIcon, Layers2, FileIcon } from "lucide-react";
|
import { Home, Settings } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { ContrastIcon, EpicIcon, LayersIcon } from "@plane/propel/icons";
|
|
||||||
import { Breadcrumbs } from "./breadcrumbs";
|
import { Breadcrumbs } from "./breadcrumbs";
|
||||||
import { BreadcrumbNavigationDropdown } from "./navigation-dropdown";
|
|
||||||
|
|
||||||
const meta: Meta<typeof Breadcrumbs> = {
|
const meta: Meta<typeof Breadcrumbs> = {
|
||||||
title: "UI/Breadcrumbs",
|
title: "UI/Breadcrumbs",
|
||||||
@@ -87,138 +85,13 @@ export const SingleItem: Story = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithNavigationDropdown: Story = {
|
export const WithIcons: Story = {
|
||||||
args: {
|
|
||||||
children: [
|
|
||||||
<Breadcrumbs.Item key="home" component={<BreadcrumbBlock href="/" label="Home" />} />,
|
|
||||||
<Breadcrumbs.Item
|
|
||||||
key="projects"
|
|
||||||
component={
|
|
||||||
<BreadcrumbNavigationDropdown
|
|
||||||
selectedItemKey="project-1"
|
|
||||||
navigationItems={[
|
|
||||||
{
|
|
||||||
key: "project-1",
|
|
||||||
title: "Project Alpha",
|
|
||||||
|
|
||||||
action: () => console.log("Project Alpha selected"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "project-2",
|
|
||||||
title: "Project Beta",
|
|
||||||
|
|
||||||
action: () => console.log("Project Beta selected"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "project-3",
|
|
||||||
title: "Project Gamma",
|
|
||||||
|
|
||||||
action: () => console.log("Project Gamma selected"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
showSeparator={false}
|
|
||||||
/>,
|
|
||||||
<Breadcrumbs.Item key="settings" component={<BreadcrumbBlock href="/settings" label="Settings" />} />,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WithNavigationDropdownAndIcons: Story = {
|
|
||||||
args: {
|
args: {
|
||||||
children: [
|
children: [
|
||||||
<Breadcrumbs.Item
|
<Breadcrumbs.Item
|
||||||
key="home"
|
key="home"
|
||||||
component={<BreadcrumbBlock href="/" label="Home" icon={<Home className="size-3.5" />} />}
|
component={<BreadcrumbBlock href="/" label="Home" icon={<Home className="size-3.5" />} />}
|
||||||
/>,
|
/>,
|
||||||
<Breadcrumbs.Item
|
|
||||||
key="projects"
|
|
||||||
component={
|
|
||||||
<BreadcrumbNavigationDropdown
|
|
||||||
selectedItemKey="project-1"
|
|
||||||
navigationItems={[
|
|
||||||
{
|
|
||||||
key: "project-1",
|
|
||||||
title: "Project Alpha",
|
|
||||||
icon: Briefcase,
|
|
||||||
|
|
||||||
action: () => console.log("Project Alpha selected"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "project-2",
|
|
||||||
title: "Project Beta",
|
|
||||||
icon: Briefcase,
|
|
||||||
|
|
||||||
// disabled: true,
|
|
||||||
action: () => console.log("Project Beta selected"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "project-3",
|
|
||||||
title: "Project Gamma",
|
|
||||||
icon: Briefcase,
|
|
||||||
|
|
||||||
action: () => console.log("Project Gamma selected"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
showSeparator={false}
|
|
||||||
/>,
|
|
||||||
<Breadcrumbs.Item
|
|
||||||
key="features"
|
|
||||||
component={
|
|
||||||
<BreadcrumbNavigationDropdown
|
|
||||||
selectedItemKey="feature-1"
|
|
||||||
navigationItems={[
|
|
||||||
{
|
|
||||||
key: "feature-1",
|
|
||||||
title: "Epics",
|
|
||||||
icon: EpicIcon,
|
|
||||||
|
|
||||||
action: () => console.log("Feature Alpha selected"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "feature-2",
|
|
||||||
title: "Work items",
|
|
||||||
icon: LayersIcon,
|
|
||||||
|
|
||||||
// disabled: true,
|
|
||||||
action: () => console.log("Feature Beta selected"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "feature-3",
|
|
||||||
title: "Cycles",
|
|
||||||
icon: ContrastIcon,
|
|
||||||
|
|
||||||
action: () => console.log("Feature Gamma selected"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "feature-3",
|
|
||||||
title: "Modules",
|
|
||||||
icon: GridIcon,
|
|
||||||
|
|
||||||
action: () => console.log("Feature Gamma selected"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "feature-3",
|
|
||||||
title: "Views",
|
|
||||||
icon: Layers2,
|
|
||||||
|
|
||||||
action: () => console.log("Feature Gamma selected"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "feature-3",
|
|
||||||
title: "Pages",
|
|
||||||
icon: FileIcon,
|
|
||||||
|
|
||||||
action: () => console.log("Feature Gamma selected"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
showSeparator={false}
|
|
||||||
/>,
|
|
||||||
<Breadcrumbs.Item
|
<Breadcrumbs.Item
|
||||||
key="settings"
|
key="settings"
|
||||||
component={<BreadcrumbBlock href="/settings" label="Settings" icon={<Settings className="size-3.5" />} />}
|
component={<BreadcrumbBlock href="/settings" label="Settings" icon={<Settings className="size-3.5" />} />}
|
||||||
|
|||||||
@@ -5,5 +5,4 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./breadcrumbs";
|
export * from "./breadcrumbs";
|
||||||
export * from "./navigation-dropdown";
|
|
||||||
export * from "./navigation-search-dropdown";
|
export * from "./navigation-search-dropdown";
|
||||||
|
|||||||
@@ -1,142 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* See the LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { CheckIcon } from "lucide-react";
|
|
||||||
import * as React from "react";
|
|
||||||
// ui
|
|
||||||
import { Tooltip } from "@plane/propel/tooltip";
|
|
||||||
import type { TContextMenuItem } from "../dropdowns";
|
|
||||||
import { CustomMenu } from "../dropdowns";
|
|
||||||
import { cn } from "../utils";
|
|
||||||
import { Breadcrumbs } from "./breadcrumbs";
|
|
||||||
|
|
||||||
type TBreadcrumbNavigationDropdownProps = {
|
|
||||||
selectedItemKey: string;
|
|
||||||
navigationItems: TContextMenuItem[];
|
|
||||||
navigationDisabled?: boolean;
|
|
||||||
handleOnClick?: () => void;
|
|
||||||
isLast?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function BreadcrumbNavigationDropdown(props: TBreadcrumbNavigationDropdownProps) {
|
|
||||||
const { selectedItemKey, navigationItems, navigationDisabled = false, handleOnClick, isLast = false } = props;
|
|
||||||
const [isOpen, setIsOpen] = React.useState(false);
|
|
||||||
// derived values
|
|
||||||
const selectedItem = navigationItems.find((item) => item.key === selectedItemKey);
|
|
||||||
const selectedItemIcon = selectedItem?.icon ? (
|
|
||||||
<selectedItem.icon className={cn("size-4", selectedItem.iconClassName)} />
|
|
||||||
) : undefined;
|
|
||||||
|
|
||||||
// if no selected item, return null
|
|
||||||
if (!selectedItem) return null;
|
|
||||||
|
|
||||||
function NavigationButton() {
|
|
||||||
return (
|
|
||||||
<Tooltip tooltipContent={selectedItem?.title} position="bottom" disabled={isOpen}>
|
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
if (!isLast) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
handleOnClick?.();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={cn(
|
|
||||||
"group flex h-full cursor-pointer items-center gap-2 rounded-sm rounded-r-none px-1.5 py-1 text-13 font-medium text-tertiary",
|
|
||||||
{
|
|
||||||
"hover:bg-layer-1 hover:text-primary": !isLast,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex text-tertiary @4xl:hidden">...</div>
|
|
||||||
<div className="hidden items-center gap-2 @4xl:flex">
|
|
||||||
{selectedItemIcon && <Breadcrumbs.Icon>{selectedItemIcon}</Breadcrumbs.Icon>}
|
|
||||||
<Breadcrumbs.Label>{selectedItem?.title}</Breadcrumbs.Label>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (navigationDisabled) {
|
|
||||||
return <NavigationButton />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CustomMenu
|
|
||||||
customButton={
|
|
||||||
<>
|
|
||||||
<NavigationButton />
|
|
||||||
<Breadcrumbs.Separator
|
|
||||||
className={cn("rounded-r-sm", {
|
|
||||||
"bg-layer-1": isOpen && !isLast,
|
|
||||||
"hover:bg-layer-1": !isLast,
|
|
||||||
})}
|
|
||||||
containerClassName="p-0"
|
|
||||||
iconClassName={cn("group-hover:rotate-90 hover:text-primary", {
|
|
||||||
"text-primary": isOpen,
|
|
||||||
"rotate-90": isOpen || isLast,
|
|
||||||
})}
|
|
||||||
showDivider={!isLast}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
placement="bottom-start"
|
|
||||||
className="h-full rounded-sm"
|
|
||||||
customButtonClassName={cn(
|
|
||||||
"group flex h-full cursor-pointer items-center gap-0.5 rounded-sm outline-none hover:bg-surface-2",
|
|
||||||
{
|
|
||||||
"bg-surface-2": isOpen,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
closeOnSelect
|
|
||||||
menuButtonOnClick={() => {
|
|
||||||
setIsOpen(!isOpen);
|
|
||||||
}}
|
|
||||||
onMenuClose={() => {
|
|
||||||
setIsOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{navigationItems.map((item) => {
|
|
||||||
if (item.shouldRender === false) return null;
|
|
||||||
return (
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
key={item.key}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
if (item.key === selectedItemKey) return;
|
|
||||||
item.action();
|
|
||||||
}}
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-2",
|
|
||||||
{
|
|
||||||
"text-placeholder": item.disabled,
|
|
||||||
},
|
|
||||||
item.className
|
|
||||||
)}
|
|
||||||
disabled={item.disabled}
|
|
||||||
>
|
|
||||||
{item.icon && <item.icon className={cn("size-4 flex-shrink-0", item.iconClassName)} />}
|
|
||||||
<div className="w-full">
|
|
||||||
<h5>{item.title}</h5>
|
|
||||||
{item.description && (
|
|
||||||
<p
|
|
||||||
className={cn("whitespace-pre-line text-tertiary", {
|
|
||||||
"text-placeholder": item.disabled,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{item.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{item.key === selectedItemKey && <CheckIcon className="size-3.5 flex-shrink-0" />}
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</CustomMenu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,4 @@
|
|||||||
* See the LICENSE file for details.
|
* See the LICENSE file for details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./button";
|
|
||||||
export * from "./helper";
|
|
||||||
export * from "./toggle-switch";
|
export * from "./toggle-switch";
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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";
|
|
||||||
|
|
||||||
interface ColorPickerProps {
|
|
||||||
value: string;
|
|
||||||
onChange: (color: string) => void;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ColorPicker(props: ColorPickerProps) {
|
|
||||||
const { value, onChange, className = "" } = props;
|
|
||||||
// refs
|
|
||||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
// handlers
|
|
||||||
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
inputRef.current?.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative flex items-center justify-center">
|
|
||||||
<button
|
|
||||||
className={`size-4 cursor-pointer rounded-full conical-gradient ${className}`}
|
|
||||||
onClick={handleOnClick}
|
|
||||||
aria-label="Open color picker"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
ref={inputRef}
|
|
||||||
type="color"
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => onChange(e.target.value)}
|
|
||||||
className="invisible absolute inset-0 size-4"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* See the LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from "./color-picker";
|
|
||||||
@@ -1,928 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* See the LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
Activity,
|
|
||||||
Airplay,
|
|
||||||
AlertCircle,
|
|
||||||
AlertOctagon,
|
|
||||||
AlertTriangle,
|
|
||||||
AlignCenter,
|
|
||||||
AlignJustify,
|
|
||||||
AlignLeft,
|
|
||||||
AlignRight,
|
|
||||||
Anchor,
|
|
||||||
Aperture,
|
|
||||||
Archive,
|
|
||||||
ArrowDown,
|
|
||||||
ArrowLeft,
|
|
||||||
ArrowRight,
|
|
||||||
ArrowUp,
|
|
||||||
AtSign,
|
|
||||||
Award,
|
|
||||||
BarChart,
|
|
||||||
BarChart2,
|
|
||||||
Battery,
|
|
||||||
BatteryCharging,
|
|
||||||
Bell,
|
|
||||||
BellOff,
|
|
||||||
Book,
|
|
||||||
Bookmark,
|
|
||||||
BookOpen,
|
|
||||||
Box,
|
|
||||||
Briefcase,
|
|
||||||
Calendar,
|
|
||||||
Camera,
|
|
||||||
CameraOff,
|
|
||||||
Cast,
|
|
||||||
CheckCircle,
|
|
||||||
CheckSquare,
|
|
||||||
Clipboard,
|
|
||||||
Clock,
|
|
||||||
Cloud,
|
|
||||||
CloudDrizzle,
|
|
||||||
CloudLightning,
|
|
||||||
CloudOff,
|
|
||||||
CloudRain,
|
|
||||||
CloudSnow,
|
|
||||||
Code,
|
|
||||||
Codepen,
|
|
||||||
Codesandbox,
|
|
||||||
Coffee,
|
|
||||||
Columns,
|
|
||||||
Command,
|
|
||||||
Compass,
|
|
||||||
CornerDownLeft,
|
|
||||||
CornerDownRight,
|
|
||||||
CornerLeftDown,
|
|
||||||
CornerLeftUp,
|
|
||||||
CornerRightDown,
|
|
||||||
CornerRightUp,
|
|
||||||
CornerUpLeft,
|
|
||||||
CornerUpRight,
|
|
||||||
Cpu,
|
|
||||||
CreditCard,
|
|
||||||
Crop,
|
|
||||||
Crosshair,
|
|
||||||
Database,
|
|
||||||
Delete,
|
|
||||||
Disc,
|
|
||||||
Divide,
|
|
||||||
DivideCircle,
|
|
||||||
DivideSquare,
|
|
||||||
DollarSign,
|
|
||||||
Download,
|
|
||||||
DownloadCloud,
|
|
||||||
Dribbble,
|
|
||||||
Droplet,
|
|
||||||
Edit,
|
|
||||||
Edit2,
|
|
||||||
Edit3,
|
|
||||||
Eye,
|
|
||||||
EyeOff,
|
|
||||||
Facebook,
|
|
||||||
FastForward,
|
|
||||||
Feather,
|
|
||||||
Figma,
|
|
||||||
File,
|
|
||||||
FileMinus,
|
|
||||||
FilePlus,
|
|
||||||
FileText,
|
|
||||||
Film,
|
|
||||||
Filter,
|
|
||||||
Flag,
|
|
||||||
Folder,
|
|
||||||
FolderMinus,
|
|
||||||
FolderPlus,
|
|
||||||
Framer,
|
|
||||||
Frown,
|
|
||||||
Gift,
|
|
||||||
GitBranch,
|
|
||||||
GitCommit,
|
|
||||||
GitMerge,
|
|
||||||
GitPullRequest,
|
|
||||||
Github,
|
|
||||||
Gitlab,
|
|
||||||
Grid,
|
|
||||||
HardDrive,
|
|
||||||
Hash,
|
|
||||||
Headphones,
|
|
||||||
Heart,
|
|
||||||
HelpCircle,
|
|
||||||
Hexagon,
|
|
||||||
Home,
|
|
||||||
Image,
|
|
||||||
Inbox,
|
|
||||||
Info,
|
|
||||||
Instagram,
|
|
||||||
Italic,
|
|
||||||
Key,
|
|
||||||
Layers,
|
|
||||||
Layout,
|
|
||||||
LifeBuoy,
|
|
||||||
Linkedin,
|
|
||||||
List,
|
|
||||||
Loader,
|
|
||||||
LogIn,
|
|
||||||
LogOut,
|
|
||||||
Mail,
|
|
||||||
Map,
|
|
||||||
MapPin,
|
|
||||||
Maximize,
|
|
||||||
Maximize2,
|
|
||||||
Meh,
|
|
||||||
Menu,
|
|
||||||
MessageCircle,
|
|
||||||
MessageSquare,
|
|
||||||
Mic,
|
|
||||||
MicOff,
|
|
||||||
Minimize,
|
|
||||||
Minimize2,
|
|
||||||
Minus,
|
|
||||||
MinusCircle,
|
|
||||||
MinusSquare,
|
|
||||||
CircleChevronDown,
|
|
||||||
UsersRound,
|
|
||||||
ToggleLeft,
|
|
||||||
User,
|
|
||||||
} from "lucide-react";
|
|
||||||
import {
|
|
||||||
LinkIcon,
|
|
||||||
CopyIcon,
|
|
||||||
GlobeIcon,
|
|
||||||
LockIcon,
|
|
||||||
NewTabIcon,
|
|
||||||
CheckIcon,
|
|
||||||
SearchIcon,
|
|
||||||
ChevronDownIcon,
|
|
||||||
ChevronLeftIcon,
|
|
||||||
ChevronRightIcon,
|
|
||||||
ChevronUpIcon,
|
|
||||||
} from "@plane/propel/icons";
|
|
||||||
|
|
||||||
export const MATERIAL_ICONS_LIST = [
|
|
||||||
{
|
|
||||||
name: "search",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "home",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "menu",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "close",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "settings",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "done",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "check_circle",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "favorite",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "add",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "delete",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "arrow_back",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "star",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "logout",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "add_circle",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "cancel",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "arrow_drop_down",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "more_vert",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "check",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "check_box",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "toggle_on",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "open_in_new",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "refresh",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "login",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "radio_button_unchecked",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "more_horiz",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "apps",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "radio_button_checked",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "download",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "remove",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "toggle_off",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "bolt",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "arrow_upward",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "filter_list",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "delete_forever",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "autorenew",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "key",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "sort",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "sync",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "add_box",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "block",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "restart_alt",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "menu_open",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "shopping_cart_checkout",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "expand_circle_down",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "backspace",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "undo",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "done_all",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "do_not_disturb_on",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "open_in_full",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "double_arrow",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "sync_alt",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "zoom_in",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "done_outline",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "drag_indicator",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "fullscreen",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "star_half",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "settings_accessibility",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "reply",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "exit_to_app",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unfold_more",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "library_add",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "cached",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "select_check_box",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "terminal",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "change_circle",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "disabled_by_default",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "swap_horiz",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "swap_vert",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "app_registration",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "download_for_offline",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "close_fullscreen",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "file_open",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "minimize",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "open_with",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "dataset",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "add_task",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "start",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "keyboard_voice",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "create_new_folder",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "forward",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "download",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "settings_applications",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "compare_arrows",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "redo",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "zoom_out",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "publish",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "html",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "token",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "switch_access_shortcut",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "fullscreen_exit",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "sort_by_alpha",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "delete_sweep",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "indeterminate_check_box",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "view_timeline",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "settings_backup_restore",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "arrow_drop_down_circle",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "assistant_navigation",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "sync_problem",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "clear_all",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "density_medium",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "heart_plus",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "filter_alt_off",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "expand",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "subdirectory_arrow_right",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "download_done",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "arrow_outward",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "123",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "swipe_left",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "auto_mode",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "saved_search",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "place_item",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "system_update_alt",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "javascript",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "search_off",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "output",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "select_all",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "fit_screen",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "swipe_up",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "dynamic_form",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "hide_source",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "swipe_right",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "switch_access_shortcut_add",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "browse_gallery",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "css",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "density_small",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "assistant_direction",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "check_small",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "youtube_searched_for",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "move_up",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "swap_horizontal_circle",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "data_thresholding",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "install_mobile",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "move_down",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "dataset_linked",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "keyboard_command_key",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "view_kanban",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "swipe_down",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "key_off",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "transcribe",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "send_time_extension",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "swipe_down_alt",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "swipe_left_alt",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "swipe_right_alt",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "swipe_up_alt",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "keyboard_option_key",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "cycle",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "rebase",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "rebase_edit",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty_dashboard",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "magic_exchange",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "acute",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "point_scan",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "step_into",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "cheer",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "emoticon",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "explosion",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "water_bottle",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "weather_hail",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "syringe",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "pill",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "genetics",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "allergy",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "medical_mask",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "body_fat",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "barefoot",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "infrared",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "wrist",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "metabolism",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "conditions",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "taunt",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "altitude",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tibia",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "footprint",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "eyeglasses",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "man_3",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "woman_2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "rheumatology",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tornado",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "landslide",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "foggy",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "severe_cold",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tsunami",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "vape_free",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "sign_language",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "emoji_symbols",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "clear_night",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "emoji_food_beverage",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "hive",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "thunderstorm",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "communication",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "rocket",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "pets",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "public",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "quiz",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "mood",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "gavel",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "eco",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "diamond",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "forest",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "rainy",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "skull",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const LUCIDE_ICONS_LIST = [
|
|
||||||
{ name: "Activity", element: Activity },
|
|
||||||
{ name: "Airplay", element: Airplay },
|
|
||||||
{ name: "AlertCircle", element: AlertCircle },
|
|
||||||
{ name: "AlertOctagon", element: AlertOctagon },
|
|
||||||
{ name: "AlertTriangle", element: AlertTriangle },
|
|
||||||
{ name: "AlignCenter", element: AlignCenter },
|
|
||||||
{ name: "AlignJustify", element: AlignJustify },
|
|
||||||
{ name: "AlignLeft", element: AlignLeft },
|
|
||||||
{ name: "AlignRight", element: AlignRight },
|
|
||||||
{ name: "Anchor", element: Anchor },
|
|
||||||
{ name: "Aperture", element: Aperture },
|
|
||||||
{ name: "Archive", element: Archive },
|
|
||||||
{ name: "ArrowDown", element: ArrowDown },
|
|
||||||
{ name: "ArrowLeft", element: ArrowLeft },
|
|
||||||
{ name: "ArrowRight", element: ArrowRight },
|
|
||||||
{ name: "ArrowUp", element: ArrowUp },
|
|
||||||
{ name: "AtSign", element: AtSign },
|
|
||||||
{ name: "Award", element: Award },
|
|
||||||
{ name: "BarChart", element: BarChart },
|
|
||||||
{ name: "BarChart2", element: BarChart2 },
|
|
||||||
{ name: "Battery", element: Battery },
|
|
||||||
{ name: "BatteryCharging", element: BatteryCharging },
|
|
||||||
{ name: "Bell", element: Bell },
|
|
||||||
{ name: "BellOff", element: BellOff },
|
|
||||||
{ name: "Book", element: Book },
|
|
||||||
{ name: "Bookmark", element: Bookmark },
|
|
||||||
{ name: "BookOpen", element: BookOpen },
|
|
||||||
{ name: "Box", element: Box },
|
|
||||||
{ name: "Briefcase", element: Briefcase },
|
|
||||||
{ name: "Calendar", element: Calendar },
|
|
||||||
{ name: "Camera", element: Camera },
|
|
||||||
{ name: "CameraOff", element: CameraOff },
|
|
||||||
{ name: "Cast", element: Cast },
|
|
||||||
{ name: "CircleChevronDown", element: CircleChevronDown },
|
|
||||||
{ name: "Check", element: CheckIcon },
|
|
||||||
{ name: "CheckCircle", element: CheckCircle },
|
|
||||||
{ name: "CheckSquare", element: CheckSquare },
|
|
||||||
{ name: "ChevronDown", element: ChevronDownIcon },
|
|
||||||
{ name: "ChevronLeft", element: ChevronLeftIcon },
|
|
||||||
{ name: "ChevronRight", element: ChevronRightIcon },
|
|
||||||
{ name: "ChevronUp", element: ChevronUpIcon },
|
|
||||||
{ name: "Clipboard", element: Clipboard },
|
|
||||||
{ name: "Clock", element: Clock },
|
|
||||||
{ name: "Cloud", element: Cloud },
|
|
||||||
{ name: "CloudDrizzle", element: CloudDrizzle },
|
|
||||||
{ name: "CloudLightning", element: CloudLightning },
|
|
||||||
{ name: "CloudOff", element: CloudOff },
|
|
||||||
{ name: "CloudRain", element: CloudRain },
|
|
||||||
{ name: "CloudSnow", element: CloudSnow },
|
|
||||||
{ name: "Code", element: Code },
|
|
||||||
{ name: "Codepen", element: Codepen },
|
|
||||||
{ name: "Codesandbox", element: Codesandbox },
|
|
||||||
{ name: "Coffee", element: Coffee },
|
|
||||||
{ name: "Columns", element: Columns },
|
|
||||||
{ name: "Command", element: Command },
|
|
||||||
{ name: "Compass", element: Compass },
|
|
||||||
{ name: "Copy", element: CopyIcon },
|
|
||||||
{ name: "CornerDownLeft", element: CornerDownLeft },
|
|
||||||
{ name: "CornerDownRight", element: CornerDownRight },
|
|
||||||
{ name: "CornerLeftDown", element: CornerLeftDown },
|
|
||||||
{ name: "CornerLeftUp", element: CornerLeftUp },
|
|
||||||
{ name: "CornerRightDown", element: CornerRightDown },
|
|
||||||
{ name: "CornerRightUp", element: CornerRightUp },
|
|
||||||
{ name: "CornerUpLeft", element: CornerUpLeft },
|
|
||||||
{ name: "CornerUpRight", element: CornerUpRight },
|
|
||||||
{ name: "Cpu", element: Cpu },
|
|
||||||
{ name: "CreditCard", element: CreditCard },
|
|
||||||
{ name: "Crop", element: Crop },
|
|
||||||
{ name: "Crosshair", element: Crosshair },
|
|
||||||
{ name: "Database", element: Database },
|
|
||||||
{ name: "Delete", element: Delete },
|
|
||||||
{ name: "Disc", element: Disc },
|
|
||||||
{ name: "Divide", element: Divide },
|
|
||||||
{ name: "DivideCircle", element: DivideCircle },
|
|
||||||
{ name: "DivideSquare", element: DivideSquare },
|
|
||||||
{ name: "DollarSign", element: DollarSign },
|
|
||||||
{ name: "Download", element: Download },
|
|
||||||
{ name: "DownloadCloud", element: DownloadCloud },
|
|
||||||
{ name: "Dribbble", element: Dribbble },
|
|
||||||
{ name: "Droplet", element: Droplet },
|
|
||||||
{ name: "Edit", element: Edit },
|
|
||||||
{ name: "Edit2", element: Edit2 },
|
|
||||||
{ name: "Edit3", element: Edit3 },
|
|
||||||
{ name: "ExternalLink", element: NewTabIcon },
|
|
||||||
{ name: "Eye", element: Eye },
|
|
||||||
{ name: "EyeOff", element: EyeOff },
|
|
||||||
{ name: "Facebook", element: Facebook },
|
|
||||||
{ name: "FastForward", element: FastForward },
|
|
||||||
{ name: "Feather", element: Feather },
|
|
||||||
{ name: "Figma", element: Figma },
|
|
||||||
{ name: "File", element: File },
|
|
||||||
{ name: "FileMinus", element: FileMinus },
|
|
||||||
{ name: "FilePlus", element: FilePlus },
|
|
||||||
{ name: "FileText", element: FileText },
|
|
||||||
{ name: "Film", element: Film },
|
|
||||||
{ name: "Filter", element: Filter },
|
|
||||||
{ name: "Flag", element: Flag },
|
|
||||||
{ name: "Folder", element: Folder },
|
|
||||||
{ name: "FolderMinus", element: FolderMinus },
|
|
||||||
{ name: "FolderPlus", element: FolderPlus },
|
|
||||||
{ name: "Framer", element: Framer },
|
|
||||||
{ name: "Frown", element: Frown },
|
|
||||||
{ name: "Gift", element: Gift },
|
|
||||||
{ name: "GitBranch", element: GitBranch },
|
|
||||||
{ name: "GitCommit", element: GitCommit },
|
|
||||||
{ name: "GitMerge", element: GitMerge },
|
|
||||||
{ name: "GitPullRequest", element: GitPullRequest },
|
|
||||||
{ name: "Github", element: Github },
|
|
||||||
{ name: "Gitlab", element: Gitlab },
|
|
||||||
{ name: "Globe", element: GlobeIcon },
|
|
||||||
{ name: "Grid", element: Grid },
|
|
||||||
{ name: "HardDrive", element: HardDrive },
|
|
||||||
{ name: "Hash", element: Hash },
|
|
||||||
{ name: "Headphones", element: Headphones },
|
|
||||||
{ name: "Heart", element: Heart },
|
|
||||||
{ name: "HelpCircle", element: HelpCircle },
|
|
||||||
{ name: "Hexagon", element: Hexagon },
|
|
||||||
{ name: "Home", element: Home },
|
|
||||||
{ name: "Image", element: Image },
|
|
||||||
{ name: "Inbox", element: Inbox },
|
|
||||||
{ name: "Info", element: Info },
|
|
||||||
{ name: "Instagram", element: Instagram },
|
|
||||||
{ name: "Italic", element: Italic },
|
|
||||||
{ name: "Key", element: Key },
|
|
||||||
{ name: "Layers", element: Layers },
|
|
||||||
{ name: "Layout", element: Layout },
|
|
||||||
{ name: "LifeBuoy", element: LifeBuoy },
|
|
||||||
{ name: "Link", element: LinkIcon },
|
|
||||||
{ name: "Link2", element: LinkIcon },
|
|
||||||
{ name: "Linkedin", element: Linkedin },
|
|
||||||
{ name: "List", element: List },
|
|
||||||
{ name: "Loader", element: Loader },
|
|
||||||
{ name: "Lock", element: LockIcon },
|
|
||||||
{ name: "LogIn", element: LogIn },
|
|
||||||
{ name: "LogOut", element: LogOut },
|
|
||||||
{ name: "Mail", element: Mail },
|
|
||||||
{ name: "Map", element: Map },
|
|
||||||
{ name: "MapPin", element: MapPin },
|
|
||||||
{ name: "Maximize", element: Maximize },
|
|
||||||
{ name: "Maximize2", element: Maximize2 },
|
|
||||||
{ name: "Meh", element: Meh },
|
|
||||||
{ name: "Menu", element: Menu },
|
|
||||||
{ name: "MessageCircle", element: MessageCircle },
|
|
||||||
{ name: "MessageSquare", element: MessageSquare },
|
|
||||||
{ name: "Mic", element: Mic },
|
|
||||||
{ name: "MicOff", element: MicOff },
|
|
||||||
{ name: "Minimize", element: Minimize },
|
|
||||||
{ name: "Minimize2", element: Minimize2 },
|
|
||||||
{ name: "Minus", element: Minus },
|
|
||||||
{ name: "MinusCircle", element: MinusCircle },
|
|
||||||
{ name: "MinusSquare", element: MinusSquare },
|
|
||||||
{ name: "Search", element: SearchIcon },
|
|
||||||
{ name: "ToggleLeft", element: ToggleLeft },
|
|
||||||
{ name: "User", element: User },
|
|
||||||
{ name: "UsersRound", element: UsersRound },
|
|
||||||
];
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* See the LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from "./icons";
|
|
||||||
@@ -4,6 +4,4 @@
|
|||||||
* See the LICENSE file for details.
|
* See the LICENSE file for details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./common";
|
|
||||||
export * from "./multi-select";
|
|
||||||
export * from "./single-select";
|
export * from "./single-select";
|
||||||
|
|||||||
@@ -1,172 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* See the LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Combobox } from "@headlessui/react";
|
|
||||||
import { sortBy } from "lodash-es";
|
|
||||||
import React, { useMemo, useRef, useState } from "react";
|
|
||||||
import { usePopper } from "react-popper";
|
|
||||||
// plane imports
|
|
||||||
import { useOutsideClickDetector } from "@plane/hooks";
|
|
||||||
// local imports
|
|
||||||
import { useDropdownKeyPressed } from "../hooks/use-dropdown-key-pressed";
|
|
||||||
import { cn } from "../utils";
|
|
||||||
import { DropdownButton } from "./common";
|
|
||||||
import { DropdownOptions } from "./common/options";
|
|
||||||
import type { IMultiSelectDropdown } from "./dropdown";
|
|
||||||
|
|
||||||
export function MultiSelectDropdown(props: IMultiSelectDropdown) {
|
|
||||||
const {
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
options,
|
|
||||||
onOpen,
|
|
||||||
onClose,
|
|
||||||
containerClassName,
|
|
||||||
tabIndex,
|
|
||||||
placement,
|
|
||||||
disabled,
|
|
||||||
buttonContent,
|
|
||||||
buttonContainerClassName,
|
|
||||||
buttonClassName,
|
|
||||||
disableSearch,
|
|
||||||
inputPlaceholder,
|
|
||||||
inputClassName,
|
|
||||||
inputIcon,
|
|
||||||
inputContainerClassName,
|
|
||||||
keyExtractor,
|
|
||||||
optionsContainerClassName,
|
|
||||||
queryArray,
|
|
||||||
sortByKey,
|
|
||||||
firstItem,
|
|
||||||
renderItem,
|
|
||||||
loader = false,
|
|
||||||
disableSorting,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
// states
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [query, setQuery] = useState("");
|
|
||||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
|
||||||
// refs
|
|
||||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
// popper-js refs
|
|
||||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
|
||||||
|
|
||||||
// popper-js init
|
|
||||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
|
||||||
placement: placement ?? "bottom-start",
|
|
||||||
modifiers: [
|
|
||||||
{
|
|
||||||
name: "preventOverflow",
|
|
||||||
options: {
|
|
||||||
padding: 12,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
// handlers
|
|
||||||
const toggleDropdown = () => {
|
|
||||||
if (!isOpen) onOpen?.();
|
|
||||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
|
||||||
if (isOpen) onClose?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
toggleDropdown();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
if (!isOpen) return;
|
|
||||||
setIsOpen(false);
|
|
||||||
onClose?.();
|
|
||||||
setQuery?.("");
|
|
||||||
};
|
|
||||||
|
|
||||||
// options
|
|
||||||
const sortedOptions = useMemo(() => {
|
|
||||||
if (!options) return undefined;
|
|
||||||
|
|
||||||
const filteredOptions = queryArray
|
|
||||||
? (options || []).filter((options) => {
|
|
||||||
const queryString = queryArray.map((query) => options.data[query]).join(" ");
|
|
||||||
return queryString.toLowerCase().includes(query.toLowerCase());
|
|
||||||
})
|
|
||||||
: options;
|
|
||||||
|
|
||||||
if (disableSorting) return filteredOptions;
|
|
||||||
|
|
||||||
return sortBy(filteredOptions, [
|
|
||||||
(option) => firstItem && firstItem(option.data[option.value]),
|
|
||||||
(option) => !(value ?? []).includes(option.data[option.value]),
|
|
||||||
() => sortByKey && sortByKey.toLowerCase(),
|
|
||||||
]);
|
|
||||||
}, [query, options]);
|
|
||||||
|
|
||||||
// hooks
|
|
||||||
const handleKeyDown = useDropdownKeyPressed(toggleDropdown, handleClose);
|
|
||||||
|
|
||||||
useOutsideClickDetector(dropdownRef, handleClose);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Combobox
|
|
||||||
as="div"
|
|
||||||
ref={dropdownRef}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
className={cn(
|
|
||||||
"h-full",
|
|
||||||
typeof containerClassName === "function" ? containerClassName(isOpen) : containerClassName
|
|
||||||
)}
|
|
||||||
tabIndex={tabIndex}
|
|
||||||
multiple
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
<DropdownButton
|
|
||||||
value={value}
|
|
||||||
isOpen={isOpen}
|
|
||||||
setReferenceElement={setReferenceElement}
|
|
||||||
handleOnClick={handleOnClick}
|
|
||||||
buttonContent={buttonContent}
|
|
||||||
buttonClassName={buttonClassName}
|
|
||||||
buttonContainerClassName={buttonContainerClassName}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
{isOpen && (
|
|
||||||
<Combobox.Options as="ul" className="fixed z-10" static>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"my-1 w-48 rounded-sm border-[0.5px] border-strong bg-surface-1 px-2 py-2.5 text-11 shadow-raised-200 focus:outline-none",
|
|
||||||
optionsContainerClassName
|
|
||||||
)}
|
|
||||||
ref={setPopperElement}
|
|
||||||
style={styles.popper}
|
|
||||||
{...attributes.popper}
|
|
||||||
>
|
|
||||||
<DropdownOptions
|
|
||||||
isOpen={isOpen}
|
|
||||||
query={query}
|
|
||||||
setQuery={setQuery}
|
|
||||||
inputIcon={inputIcon}
|
|
||||||
inputPlaceholder={inputPlaceholder}
|
|
||||||
inputClassName={inputClassName}
|
|
||||||
inputContainerClassName={inputContainerClassName}
|
|
||||||
disableSearch={disableSearch}
|
|
||||||
keyExtractor={keyExtractor}
|
|
||||||
options={sortedOptions}
|
|
||||||
value={value}
|
|
||||||
renderItem={renderItem}
|
|
||||||
loader={loader}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Combobox.Options>
|
|
||||||
)}
|
|
||||||
</Combobox>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -64,10 +64,6 @@ const ComboDropDown = forwardRef(function ComboDropDown(props: Props, ref) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const ComboOptions = Combobox.Options;
|
|
||||||
const ComboOption = Combobox.Option;
|
|
||||||
const ComboInput = Combobox.Input;
|
|
||||||
|
|
||||||
ComboDropDown.displayName = "ComboDropDown";
|
ComboDropDown.displayName = "ComboDropDown";
|
||||||
|
|
||||||
export { ComboDropDown, ComboOptions, ComboOption, ComboInput };
|
export { ComboDropDown };
|
||||||
|
|||||||
@@ -8,5 +8,4 @@ export * from "./input";
|
|||||||
export * from "./textarea";
|
export * from "./textarea";
|
||||||
export * from "./input-color-picker";
|
export * from "./input-color-picker";
|
||||||
export * from "./checkbox";
|
export * from "./checkbox";
|
||||||
export * from "./root";
|
|
||||||
export * from "./password";
|
export * from "./password";
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import * as ColorPicker from "react-color";
|
|||||||
import type { ColorResult } from "react-color";
|
import type { ColorResult } from "react-color";
|
||||||
import { usePopper } from "react-popper";
|
import { usePopper } from "react-popper";
|
||||||
// helpers
|
// helpers
|
||||||
import { Button } from "../button";
|
import { Button } from "../button/button";
|
||||||
import { cn } from "../utils";
|
import { cn } from "../utils";
|
||||||
// components
|
// components
|
||||||
import { Input } from "./input";
|
import { Input } from "./input";
|
||||||
|
|||||||
@@ -5,5 +5,4 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./indicator";
|
export * from "./indicator";
|
||||||
export * from "./helper";
|
|
||||||
export * from "./password-input";
|
export * from "./password-input";
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -5,13 +5,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./avatar";
|
export * from "./avatar";
|
||||||
export * from "./badge";
|
|
||||||
export * from "./breadcrumbs";
|
export * from "./breadcrumbs";
|
||||||
export * from "./button";
|
export * from "./button";
|
||||||
export * from "./card";
|
export * from "./card";
|
||||||
export * from "./collapsible";
|
export * from "./collapsible";
|
||||||
export * from "./color-picker";
|
|
||||||
export * from "./constants";
|
|
||||||
export * from "./content-wrapper";
|
export * from "./content-wrapper";
|
||||||
export * from "./control-link";
|
export * from "./control-link";
|
||||||
export * from "./drag-handle";
|
export * from "./drag-handle";
|
||||||
@@ -27,13 +24,10 @@ export * from "./modals";
|
|||||||
export * from "./popovers";
|
export * from "./popovers";
|
||||||
export * from "./progress";
|
export * from "./progress";
|
||||||
export * from "./row";
|
export * from "./row";
|
||||||
export * from "./scroll-area";
|
|
||||||
export * from "./sortable";
|
export * from "./sortable";
|
||||||
export * from "./spinners";
|
export * from "./spinners";
|
||||||
export * from "./tables";
|
export * from "./tables";
|
||||||
export * from "./tabs";
|
|
||||||
export * from "./tag";
|
export * from "./tag";
|
||||||
export * from "./tooltip";
|
export * from "./tooltip";
|
||||||
export * from "./typography";
|
|
||||||
export * from "./utils";
|
export * from "./utils";
|
||||||
export * from "./oauth";
|
export * from "./oauth";
|
||||||
|
|||||||
@@ -4,7 +4,5 @@
|
|||||||
* See the LICENSE file for details.
|
* See the LICENSE file for details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./radial-progress";
|
|
||||||
export * from "./progress-bar";
|
|
||||||
export * from "./linear-progress-indicator";
|
export * from "./linear-progress-indicator";
|
||||||
export * from "./circular-progress-indicator";
|
export * from "./circular-progress-indicator";
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
maxValue?: number;
|
|
||||||
value?: number;
|
|
||||||
radius?: number;
|
|
||||||
strokeWidth?: number;
|
|
||||||
activeStrokeColor?: string;
|
|
||||||
inactiveStrokeColor?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ProgressBar({
|
|
||||||
maxValue = 0,
|
|
||||||
value = 0,
|
|
||||||
radius = 8,
|
|
||||||
strokeWidth = 2,
|
|
||||||
activeStrokeColor = "#3e98c7",
|
|
||||||
inactiveStrokeColor = "#ddd",
|
|
||||||
}: Props) {
|
|
||||||
// PIE Calc Fn
|
|
||||||
const generatePie = (value: any) => {
|
|
||||||
const x = radius - Math.cos((2 * Math.PI) / (100 / value)) * radius;
|
|
||||||
const y = radius + Math.sin((2 * Math.PI) / (100 / value)) * radius;
|
|
||||||
const long = value <= 50 ? 0 : 1;
|
|
||||||
const d = `M${radius} ${radius} L${radius} ${0} A${radius} ${radius} 0 ${long} 1 ${y} ${x} Z`;
|
|
||||||
|
|
||||||
return d;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---- PIE Area Calc --------
|
|
||||||
const calculatePieValue = (numberOfBars: any) => {
|
|
||||||
const angle = 360 / numberOfBars;
|
|
||||||
const pieValue = Math.floor(angle / 4);
|
|
||||||
return pieValue < 1 ? 1 : Math.floor(angle / 4);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---- PIE Render Fn --------
|
|
||||||
const renderPie = (i: any) => {
|
|
||||||
const DIRECTION = -1;
|
|
||||||
// Rotation Calc
|
|
||||||
const primaryRotationAngle = (maxValue - 1) * (360 / maxValue);
|
|
||||||
const rotationAngle = -1 * DIRECTION * primaryRotationAngle + i * DIRECTION * primaryRotationAngle;
|
|
||||||
const rotationTransformation = `rotate(${rotationAngle}, ${radius}, ${radius})`;
|
|
||||||
const pieValue = calculatePieValue(maxValue);
|
|
||||||
const dValue = generatePie(pieValue);
|
|
||||||
const fillColor = value > 0 && i <= value ? activeStrokeColor : inactiveStrokeColor;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<path
|
|
||||||
style={{ opacity: i === 0 ? 0 : 1 }}
|
|
||||||
key={i}
|
|
||||||
d={dValue}
|
|
||||||
fill={fillColor}
|
|
||||||
transform={rotationTransformation}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// combining the Pies
|
|
||||||
const renderOuterCircle = () => [...Array(maxValue + 1)].map((e, i) => renderPie(i));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<svg width={radius * 2} height={radius * 2}>
|
|
||||||
{renderOuterCircle()}
|
|
||||||
<circle r={radius - strokeWidth} cx={radius} cy={radius} className="progress-bar" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* See the LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
|
||||||
|
|
||||||
interface IRadialProgressBar {
|
|
||||||
progress: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function RadialProgressBar(props: IRadialProgressBar) {
|
|
||||||
const { progress } = props;
|
|
||||||
const [circumference, setCircumference] = useState(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const radius = 40;
|
|
||||||
const circumference = 2 * Math.PI * radius;
|
|
||||||
setCircumference(circumference);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const progressOffset = ((100 - progress) / 100) * circumference;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative h-4 w-4">
|
|
||||||
<svg className="absolute top-0 left-0" viewBox="0 0 100 100">
|
|
||||||
<circle
|
|
||||||
className={"stroke-current opacity-10"}
|
|
||||||
cx="50"
|
|
||||||
cy="50"
|
|
||||||
r="40"
|
|
||||||
strokeWidth="12"
|
|
||||||
fill="none"
|
|
||||||
strokeDasharray={`${circumference} ${circumference}`}
|
|
||||||
/>
|
|
||||||
<circle
|
|
||||||
className={`stroke-current`}
|
|
||||||
cx="50"
|
|
||||||
cy="50"
|
|
||||||
r="40"
|
|
||||||
strokeWidth="12"
|
|
||||||
fill="none"
|
|
||||||
strokeDasharray={`${circumference} ${circumference}`}
|
|
||||||
strokeDashoffset={progressOffset}
|
|
||||||
transform="rotate(-90 50 50)"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* See the LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as RadixScrollArea from "@radix-ui/react-scroll-area";
|
|
||||||
import React from "react";
|
|
||||||
import { cn } from "./utils";
|
|
||||||
|
|
||||||
type TScrollAreaProps = {
|
|
||||||
type?: "auto" | "always" | "scroll" | "hover";
|
|
||||||
className?: string;
|
|
||||||
scrollHideDelay?: number;
|
|
||||||
size?: "sm" | "md" | "lg";
|
|
||||||
children: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
const sizeStyles = {
|
|
||||||
sm: "p-[0.112rem] data-[orientation=vertical]:w-2.5 data-[orientation=horizontal]:h-2.5",
|
|
||||||
md: "p-[0.152rem] data-[orientation=vertical]:w-3 data-[orientation=horizontal]:h-3",
|
|
||||||
lg: "p-[0.225rem] data-[orientation=vertical]:w-4 data-[orientation=horizontal]:h-4",
|
|
||||||
};
|
|
||||||
|
|
||||||
const thumbSizeStyles = {
|
|
||||||
sm: "before:absolute before:left-1/2 before:top-1/2 before:size-full before:min-h-11 before:min-w-11 before:-translate-x-1/2 before:-translate-y-1/2",
|
|
||||||
md: "before:absolute before:left-1/2 before:top-1/2 before:size-full before:min-h-14 before:min-w-14 before:-translate-x-1/2 before:-translate-y-1/2",
|
|
||||||
lg: "before:absolute before:left-1/2 before:top-1/2 before:size-full before:min-h-17 before:min-w-17 before:-translate-x-1/2 before:-translate-y-1/2",
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ScrollArea(props: TScrollAreaProps) {
|
|
||||||
const { type = "always", className = "", scrollHideDelay = 600, size = "md", children } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RadixScrollArea.Root
|
|
||||||
type={type}
|
|
||||||
className={cn("group overflow-hidden", className)}
|
|
||||||
scrollHideDelay={scrollHideDelay}
|
|
||||||
>
|
|
||||||
<RadixScrollArea.Viewport className="size-full">{children}</RadixScrollArea.Viewport>
|
|
||||||
<RadixScrollArea.Scrollbar
|
|
||||||
className={cn(
|
|
||||||
"group/track flex touch-none bg-transparent transition-colors duration-150 ease-out select-none",
|
|
||||||
sizeStyles[size]
|
|
||||||
)}
|
|
||||||
orientation="vertical"
|
|
||||||
>
|
|
||||||
<RadixScrollArea.Thumb
|
|
||||||
className={cn(
|
|
||||||
"relative flex-1 rounded-[10px] bg-scrollbar-thumb group-hover:bg-scrollbar-thumb-hover group-hover/track:bg-scrollbar-thumb-hover group-active/track:bg-scrollbar-thumb-active",
|
|
||||||
thumbSizeStyles[size]
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</RadixScrollArea.Scrollbar>
|
|
||||||
<RadixScrollArea.Scrollbar
|
|
||||||
className={cn(
|
|
||||||
"group/track flex touch-none bg-transparent transition-colors duration-150 ease-out select-none",
|
|
||||||
sizeStyles[size]
|
|
||||||
)}
|
|
||||||
orientation="horizontal"
|
|
||||||
>
|
|
||||||
<RadixScrollArea.Thumb
|
|
||||||
className={cn(
|
|
||||||
"relative flex-1 rounded-[10px] bg-scrollbar-thumb group-hover:bg-scrollbar-thumb-hover group-hover/track:bg-scrollbar-thumb-hover group-active/track:bg-scrollbar-thumb-active",
|
|
||||||
thumbSizeStyles[size]
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</RadixScrollArea.Scrollbar>
|
|
||||||
</RadixScrollArea.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -5,4 +5,3 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./sortable";
|
export * from "./sortable";
|
||||||
export * from "./draggable";
|
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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";
|
|
||||||
|
|
||||||
interface ICircularBarSpinner extends React.SVGAttributes<SVGElement> {
|
|
||||||
height?: string;
|
|
||||||
width?: string;
|
|
||||||
className?: string | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CircularBarSpinner({ height = "16px", width = "16px", className = "" }: ICircularBarSpinner) {
|
|
||||||
return (
|
|
||||||
<div role="status">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width={width} height={height} viewBox="0 0 24 24" className={className}>
|
|
||||||
<g>
|
|
||||||
<rect width={2} height={5} x={11} y={1} fill="currentColor" opacity={0.14} />
|
|
||||||
<rect width={2} height={5} x={11} y={1} fill="currentColor" opacity={0.29} transform="rotate(30 12 12)" />
|
|
||||||
<rect width={2} height={5} x={11} y={1} fill="currentColor" opacity={0.43} transform="rotate(60 12 12)" />
|
|
||||||
<rect width={2} height={5} x={11} y={1} fill="currentColor" opacity={0.57} transform="rotate(90 12 12)" />
|
|
||||||
<rect width={2} height={5} x={11} y={1} fill="currentColor" opacity={0.71} transform="rotate(120 12 12)" />
|
|
||||||
<rect width={2} height={5} x={11} y={1} fill="currentColor" opacity={0.86} transform="rotate(150 12 12)" />
|
|
||||||
<rect width={2} height={5} x={11} y={1} fill="currentColor" transform="rotate(180 12 12)" />
|
|
||||||
<animateTransform
|
|
||||||
attributeName="transform"
|
|
||||||
calcMode="discrete"
|
|
||||||
dur="0.75s"
|
|
||||||
repeatCount="indefinite"
|
|
||||||
type="rotate"
|
|
||||||
values="0 12 12;30 12 12;60 12 12;90 12 12;120 12 12;150 12 12;180 12 12;210 12 12;240 12 12;270 12 12;300 12 12;330 12 12;360 12 12"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -5,4 +5,3 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./circular-spinner";
|
export * from "./circular-spinner";
|
||||||
export * from "./circular-bar-spinner";
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* See the LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from "./tabs";
|
|
||||||
export * from "./tab-list";
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* See the LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Tab } from "@headlessui/react";
|
|
||||||
import type { LucideProps } from "lucide-react";
|
|
||||||
import type { FC } from "react";
|
|
||||||
import React, { Fragment } from "react";
|
|
||||||
// helpers
|
|
||||||
import { cn } from "../utils/classname";
|
|
||||||
|
|
||||||
export type TabListItem = {
|
|
||||||
key: string;
|
|
||||||
icon?: FC<LucideProps>;
|
|
||||||
label?: React.ReactNode;
|
|
||||||
disabled?: boolean;
|
|
||||||
onClick?: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
type TTabListProps = {
|
|
||||||
tabs: TabListItem[];
|
|
||||||
tabListClassName?: string;
|
|
||||||
tabClassName?: string;
|
|
||||||
size?: "sm" | "md" | "lg";
|
|
||||||
selectedTab?: string;
|
|
||||||
autoWrap?: boolean;
|
|
||||||
onTabChange?: (key: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function TabList({ autoWrap = true, ...props }: TTabListProps) {
|
|
||||||
return autoWrap ? (
|
|
||||||
<Tab.Group as={Fragment}>
|
|
||||||
<TabListInner {...props} />
|
|
||||||
</Tab.Group>
|
|
||||||
) : (
|
|
||||||
<TabListInner {...props} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TabListInner({ tabs, tabListClassName, tabClassName, size = "md", selectedTab, onTabChange }: TTabListProps) {
|
|
||||||
return (
|
|
||||||
<Tab.List
|
|
||||||
as="div"
|
|
||||||
className={cn(
|
|
||||||
"flex w-full min-w-fit items-center justify-between gap-1.5 rounded-md bg-layer-1 p-0.5 text-13",
|
|
||||||
tabListClassName
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{tabs.map((tab) => (
|
|
||||||
<Tab
|
|
||||||
className={({ selected }) =>
|
|
||||||
cn(
|
|
||||||
"flex w-full min-w-fit cursor-pointer items-center justify-center rounded-sm p-1 font-medium text-primary transition-all outline-none focus:outline-none",
|
|
||||||
(selectedTab ? selectedTab === tab.key : selected)
|
|
||||||
? "shadow-sm bg-layer-transparent-active text-primary"
|
|
||||||
: tab.disabled
|
|
||||||
? "cursor-not-allowed text-placeholder"
|
|
||||||
: "text-placeholder hover:bg-layer-transparent-hover hover:text-tertiary",
|
|
||||||
{
|
|
||||||
"text-11": size === "sm",
|
|
||||||
"text-13": size === "md",
|
|
||||||
"text-14": size === "lg",
|
|
||||||
},
|
|
||||||
tabClassName
|
|
||||||
)
|
|
||||||
}
|
|
||||||
key={tab.key}
|
|
||||||
onClick={() => {
|
|
||||||
if (!tab.disabled) {
|
|
||||||
onTabChange?.(tab.key);
|
|
||||||
tab.onClick?.();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={tab.disabled}
|
|
||||||
>
|
|
||||||
{tab.icon && (
|
|
||||||
<tab.icon className={cn({ "size-3": size === "sm", "size-4": size === "md", "size-5": size === "lg" })} />
|
|
||||||
)}
|
|
||||||
{tab.label}
|
|
||||||
</Tab>
|
|
||||||
))}
|
|
||||||
</Tab.List>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* See the LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Tab } from "@headlessui/react";
|
|
||||||
import React, { Fragment, useEffect, useState } from "react";
|
|
||||||
// helpers
|
|
||||||
import { useLocalStorage } from "@plane/hooks";
|
|
||||||
import { cn } from "../utils";
|
|
||||||
// types
|
|
||||||
import type { TabListItem } from "./tab-list";
|
|
||||||
import { TabList } from "./tab-list";
|
|
||||||
|
|
||||||
export type TabContent = {
|
|
||||||
content: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TabItem = TabListItem & TabContent;
|
|
||||||
|
|
||||||
type TTabsProps = {
|
|
||||||
tabs: TabItem[];
|
|
||||||
storageKey?: string;
|
|
||||||
actions?: React.ReactNode;
|
|
||||||
defaultTab?: string;
|
|
||||||
containerClassName?: string;
|
|
||||||
tabListContainerClassName?: string;
|
|
||||||
tabListClassName?: string;
|
|
||||||
tabClassName?: string;
|
|
||||||
tabPanelClassName?: string;
|
|
||||||
size?: "sm" | "md" | "lg";
|
|
||||||
storeInLocalStorage?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function Tabs(props: TTabsProps) {
|
|
||||||
const {
|
|
||||||
tabs,
|
|
||||||
storageKey,
|
|
||||||
actions,
|
|
||||||
defaultTab = tabs[0]?.key,
|
|
||||||
containerClassName = "",
|
|
||||||
tabListContainerClassName = "",
|
|
||||||
tabListClassName = "",
|
|
||||||
tabClassName = "",
|
|
||||||
tabPanelClassName = "",
|
|
||||||
size = "md",
|
|
||||||
storeInLocalStorage = true,
|
|
||||||
} = props;
|
|
||||||
// local storage
|
|
||||||
const { storedValue, setValue } = useLocalStorage(
|
|
||||||
storeInLocalStorage && storageKey ? `tab-${storageKey}` : `tab-${tabs[0]?.key}`,
|
|
||||||
defaultTab
|
|
||||||
);
|
|
||||||
// state
|
|
||||||
const [selectedTab, setSelectedTab] = useState(storedValue ?? defaultTab);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (storeInLocalStorage) {
|
|
||||||
setValue(selectedTab);
|
|
||||||
}
|
|
||||||
}, [selectedTab, setValue, storeInLocalStorage, storageKey]);
|
|
||||||
|
|
||||||
const currentTabIndex = (tabKey: string): number => tabs.findIndex((tab) => tab.key === tabKey);
|
|
||||||
|
|
||||||
const handleTabChange = (key: string) => {
|
|
||||||
setSelectedTab(key);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-full w-full flex-col">
|
|
||||||
<Tab.Group as={Fragment} defaultIndex={currentTabIndex(selectedTab)}>
|
|
||||||
<div className={cn("flex h-full w-full flex-col gap-2", containerClassName)}>
|
|
||||||
<div className={cn("flex w-full items-center gap-4", tabListContainerClassName)}>
|
|
||||||
<TabList
|
|
||||||
tabs={tabs}
|
|
||||||
tabListClassName={tabListClassName}
|
|
||||||
tabClassName={tabClassName}
|
|
||||||
size={size}
|
|
||||||
onTabChange={handleTabChange}
|
|
||||||
/>
|
|
||||||
{actions && <div className="flex-grow">{actions}</div>}
|
|
||||||
</div>
|
|
||||||
<Tab.Panels as={Fragment}>
|
|
||||||
{tabs.map((tab) => (
|
|
||||||
<Tab.Panel key={tab.key} as="div" className={cn("relative outline-none", tabPanelClassName)}>
|
|
||||||
{tab.content}
|
|
||||||
</Tab.Panel>
|
|
||||||
))}
|
|
||||||
</Tab.Panels>
|
|
||||||
</div>
|
|
||||||
</Tab.Group>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* See the LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from "./sub-heading";
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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 "../utils";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
children: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
noMargin?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
function SubHeading({ children, className, noMargin }: Props) {
|
|
||||||
return (
|
|
||||||
<h3 className={cn("block text-18 leading-7 font-medium text-secondary", !noMargin && "mb-2", className)}>
|
|
||||||
{children}
|
|
||||||
</h3>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { SubHeading };
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* See the LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { LUCIDE_ICONS_LIST } from "..";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a random icon name from the LUCIDE_ICONS_LIST array
|
|
||||||
*/
|
|
||||||
export const getRandomIconName = (): string =>
|
|
||||||
LUCIDE_ICONS_LIST[Math.floor(Math.random() * LUCIDE_ICONS_LIST.length)].name;
|
|
||||||
@@ -5,4 +5,3 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./classname";
|
export * from "./classname";
|
||||||
export * from "./icons";
|
|
||||||
|
|||||||
56
pnpm-lock.yaml
generated
56
pnpm-lock.yaml
generated
@@ -75,9 +75,6 @@ catalogs:
|
|||||||
'@popperjs/core':
|
'@popperjs/core':
|
||||||
specifier: ^2.11.8
|
specifier: ^2.11.8
|
||||||
version: 2.11.8
|
version: 2.11.8
|
||||||
'@radix-ui/react-scroll-area':
|
|
||||||
specifier: ^1.2.3
|
|
||||||
version: 1.2.10
|
|
||||||
'@react-pdf/renderer':
|
'@react-pdf/renderer':
|
||||||
specifier: ^4.8.1
|
specifier: ^4.8.1
|
||||||
version: 4.8.1
|
version: 4.8.1
|
||||||
@@ -1772,9 +1769,6 @@ importers:
|
|||||||
'@popperjs/core':
|
'@popperjs/core':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 2.11.8
|
version: 2.11.8
|
||||||
'@radix-ui/react-scroll-area':
|
|
||||||
specifier: 'catalog:'
|
|
||||||
version: 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
|
||||||
clsx:
|
clsx:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
@@ -4007,9 +4001,6 @@ packages:
|
|||||||
'@quansync/fs@1.0.0':
|
'@quansync/fs@1.0.0':
|
||||||
resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==}
|
resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==}
|
||||||
|
|
||||||
'@radix-ui/number@1.1.1':
|
|
||||||
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
|
|
||||||
|
|
||||||
'@radix-ui/primitive@1.1.3':
|
'@radix-ui/primitive@1.1.3':
|
||||||
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
|
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
|
||||||
|
|
||||||
@@ -4044,15 +4035,6 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-direction@1.1.1':
|
|
||||||
resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': 19.2.17
|
|
||||||
react: 19.2.8
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@radix-ui/react-dismissable-layer@1.1.11':
|
'@radix-ui/react-dismissable-layer@1.1.11':
|
||||||
resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==}
|
resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -4136,19 +4118,6 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-scroll-area@1.2.10':
|
|
||||||
resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': 19.2.17
|
|
||||||
'@types/react-dom': 19.2.3
|
|
||||||
react: 19.2.8
|
|
||||||
react-dom: 19.2.8
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
'@types/react-dom':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@radix-ui/react-slot@1.2.3':
|
'@radix-ui/react-slot@1.2.3':
|
||||||
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
|
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -11548,8 +11517,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
quansync: 1.0.0
|
quansync: 1.0.0
|
||||||
|
|
||||||
'@radix-ui/number@1.1.1': {}
|
|
||||||
|
|
||||||
'@radix-ui/primitive@1.1.3': {}
|
'@radix-ui/primitive@1.1.3': {}
|
||||||
|
|
||||||
'@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.17)(react@19.2.8)':
|
'@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.17)(react@19.2.8)':
|
||||||
@@ -11586,12 +11553,6 @@ snapshots:
|
|||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-direction@1.1.1(@types/react@19.2.17)(react@19.2.8)':
|
|
||||||
dependencies:
|
|
||||||
react: 19.2.8
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/react': 19.2.17
|
|
||||||
|
|
||||||
'@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
|
'@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.3
|
'@radix-ui/primitive': 1.1.3
|
||||||
@@ -11658,23 +11619,6 @@ snapshots:
|
|||||||
'@types/react': 19.2.17
|
'@types/react': 19.2.17
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
||||||
|
|
||||||
'@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
|
|
||||||
dependencies:
|
|
||||||
'@radix-ui/number': 1.1.1
|
|
||||||
'@radix-ui/primitive': 1.1.3
|
|
||||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.17)(react@19.2.8)
|
|
||||||
'@radix-ui/react-context': 1.1.2(@types/react@19.2.17)(react@19.2.8)
|
|
||||||
'@radix-ui/react-direction': 1.1.1(@types/react@19.2.17)(react@19.2.8)
|
|
||||||
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
|
||||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
|
||||||
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.17)(react@19.2.8)
|
|
||||||
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.17)(react@19.2.8)
|
|
||||||
react: 19.2.8
|
|
||||||
react-dom: 19.2.8(react@19.2.8)
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/react': 19.2.17
|
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
|
||||||
|
|
||||||
'@radix-ui/react-slot@1.2.3(@types/react@19.2.17)(react@19.2.8)':
|
'@radix-ui/react-slot@1.2.3(@types/react@19.2.17)(react@19.2.8)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.17)(react@19.2.8)
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.17)(react@19.2.8)
|
||||||
|
|||||||
Reference in New Issue
Block a user