Add components for Act management and integrate Electron setup

This commit is contained in:
natreex
2025-11-16 11:00:04 -05:00
parent e192b6dcc2
commit 8167948881
97 changed files with 25378 additions and 3 deletions

View File

@@ -0,0 +1,20 @@
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faPlus} from "@fortawesome/free-solid-svg-icons";
import React from "react";
interface AddActionButtonProps {
callBackAction: () => Promise<void>;
}
export default function AddActionButton(
{
callBackAction
}: AddActionButtonProps) {
return (
<button
className={`group p-2 rounded-lg text-muted hover:text-primary hover:bg-primary/10 transition-colors`}
onClick={callBackAction}>
<FontAwesomeIcon icon={faPlus} className={'w-5 h-5 transition-transform group-hover:rotate-90'}/>
</button>
)
}

View File

@@ -0,0 +1,21 @@
import React from "react";
interface CancelButtonProps {
callBackFunction: () => void;
text?: string;
}
export default function CancelButton(
{
callBackFunction,
text = "Annuler"
}: CancelButtonProps) {
return (
<button
onClick={callBackFunction}
className="px-5 py-2.5 rounded-lg bg-secondary/50 text-text-primary border border-secondary/50 hover:bg-secondary hover:border-secondary hover:shadow-md transition-all duration-200 hover:scale-105 font-medium"
>
{text}
</button>
);
}

View File

@@ -0,0 +1,53 @@
import React, {Dispatch, SetStateAction} from "react";
interface CheckBoxProps {
isChecked: boolean;
setIsChecked: Dispatch<SetStateAction<boolean>>;
label: string;
description: string;
id: string;
}
export default function CheckBox(
{
isChecked,
setIsChecked,
label,
description,
id,
}: CheckBoxProps) {
return (
<div className="flex items-center group">
<div className="relative inline-block w-12 mr-3 align-middle select-none">
<input
type="checkbox"
id={id}
checked={isChecked}
onChange={() => setIsChecked(!isChecked)}
className="hidden"
/>
<label htmlFor={id}
className={`block overflow-hidden h-6 rounded-full cursor-pointer transition-all duration-200 border-2 shadow-sm hover:shadow-md ${
isChecked
? 'bg-primary border-primary shadow-primary/30'
: 'bg-secondary/50 border-secondary hover:bg-secondary'
}`}
>
<span
className={`absolute block h-5 w-5 rounded-full bg-white shadow-md transform transition-all duration-200 top-0.5 ${
isChecked ? 'right-0.5 scale-110' : 'left-0.5'
}`}/>
</label>
</div>
<div className="flex-1">
<label htmlFor={id}
className="text-text-primary text-sm font-medium cursor-pointer group-hover:text-primary transition-colors">
{label}
</label>
<p className="text-text-secondary text-xs mt-0.5 hidden md:block">
{description}
</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,40 @@
import React from "react";
type ButtonType = 'alert' | 'danger' | 'informatif' | 'success';
interface ConfirmButtonProps {
text: string;
callBackFunction?: () => void;
buttonType?: ButtonType;
}
export default function ConfirmButton(
{
text,
callBackFunction,
buttonType = 'success'
}: ConfirmButtonProps) {
function getButtonType(alertType: ButtonType): string {
switch (alertType) {
case 'alert':
return 'bg-warning';
case 'danger':
return 'bg-error';
case 'informatif':
return 'bg-info';
case 'success':
default:
return 'bg-success';
}
}
const applyType: string = getButtonType(buttonType);
return (
<button
onClick={callBackFunction}
className={`rounded-lg ${applyType} px-5 py-2.5 text-white font-semibold shadow-md hover:shadow-lg transition-all duration-200 hover:scale-105 focus:outline-none focus:ring-4 focus:ring-primary/20`}
>
{text}
</button>
)
}

View File

@@ -0,0 +1,21 @@
import React, {ChangeEvent} from "react";
interface DatePickerProps {
date: string;
setDate: (e: ChangeEvent<HTMLInputElement>) => void;
}
export default function DatePicker(
{
setDate,
date
}: DatePickerProps) {
return (
<input
type="date"
value={date}
onChange={setDate}
className="bg-secondary/50 text-text-primary px-4 py-2.5 rounded-xl border border-secondary/50 focus:border-primary focus:ring-4 focus:ring-primary/20 focus:bg-secondary hover:bg-secondary hover:border-secondary outline-none transition-all duration-200"
/>
)
}

View File

@@ -0,0 +1,71 @@
import React, {ChangeEvent, KeyboardEvent, useRef, useState} from "react";
import AddActionButton from "@/components/form/AddActionButton";
interface InlineAddInputProps {
value: string;
setValue: (value: string) => void;
numericalValue?: number;
setNumericalValue?: (value: number) => void;
placeholder: string;
onAdd: () => Promise<void>;
showNumericalInput?: boolean;
}
export default function InlineAddInput(
{
value,
setValue,
numericalValue,
setNumericalValue,
placeholder,
onAdd,
showNumericalInput = false
}: InlineAddInputProps) {
const [isAdding, setIsAdding] = useState<boolean>(false);
const listItemRef = useRef<HTMLLIElement>(null);
async function handleAdd(): Promise<void> {
await onAdd();
setIsAdding(false);
}
return (
<li
ref={listItemRef}
onBlur={(e: React.FocusEvent<HTMLLIElement, Element>): void => {
if (!listItemRef.current?.contains(e.relatedTarget)) {
setIsAdding(false);
}
}}
className="relative flex items-center gap-1 h-[44px] px-3 bg-secondary/30 rounded-xl border-2 border-dashed border-secondary/50 hover:border-primary/60 hover:bg-secondary/50 cursor-pointer transition-colors duration-200"
>
{showNumericalInput && numericalValue !== undefined && setNumericalValue && (
<input
className={`bg-secondary/50 text-primary text-sm px-1.5 py-0.5 rounded border border-secondary/50 transition-all duration-200 !outline-none !ring-0 !shadow-none focus-visible:!outline-none focus-visible:!ring-0 focus-visible:!shadow-none ${isAdding ? 'w-10 opacity-100' : 'w-0 opacity-0 px-0 border-0'}`}
type="number"
value={numericalValue}
onChange={(e: ChangeEvent<HTMLInputElement>) => setNumericalValue(parseInt(e.target.value))}
tabIndex={isAdding ? 0 : -1}
/>
)}
<input
onFocus={(): void => setIsAdding(true)}
onKeyUp={async (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
await handleAdd();
}
}}
className="flex-1 min-w-0 bg-transparent text-text-primary text-sm px-1 py-0.5 cursor-pointer focus:cursor-text placeholder:text-muted/60 transition-all duration-200 !outline-none !ring-0 !shadow-none focus-visible:!outline-none focus-visible:!ring-0 focus-visible:!shadow-none"
type="text"
onChange={(e: ChangeEvent<HTMLInputElement>) => setValue(e.target.value)}
value={value}
placeholder={placeholder}
/>
{isAdding && (
<div className="absolute right-1 opacity-100">
<AddActionButton callBackAction={handleAdd}/>
</div>
)}
</li>
);
}

View File

@@ -0,0 +1,92 @@
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import React from "react";
import {IconDefinition} from "@fortawesome/fontawesome-svg-core";
import {faPlus, faTrash} from "@fortawesome/free-solid-svg-icons";
interface InputFieldProps {
icon?: IconDefinition,
fieldName?: string,
input: React.ReactNode,
addButtonCallBack?: () => Promise<void>
removeButtonCallBack?: () => Promise<void>
isAddButtonDisabled?: boolean
action?: () => Promise<void>
actionLabel?: string
actionIcon?: IconDefinition
hint?: string,
}
export default function InputField(
{
fieldName,
icon,
input,
addButtonCallBack,
removeButtonCallBack,
isAddButtonDisabled,
action,
actionLabel,
actionIcon,
hint
}: InputFieldProps) {
return (
<div className={'flex flex-col'}>
<div className={'flex justify-between items-center mb-2 lg:mb-3 flex-wrap gap-2'}>
{
fieldName && (
<h3 className="text-text-primary text-xl font-[ADLaM Display] font-medium mb-2 flex items-center gap-2">
{
icon && <FontAwesomeIcon icon={icon} className="text-primary w-5 h-5"/>
}
{fieldName}
</h3>
)
}
{
action && (
<button
onClick={action}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs bg-secondary/50 rounded-lg text-primary hover:bg-secondary hover:shadow-md hover:scale-105 transition-all duration-200 border border-secondary/50 font-medium"
>
{
actionIcon && <FontAwesomeIcon icon={actionIcon} className={'w-3.5 h-3.5'}/>
}
{
actionLabel && <span>{actionLabel}</span>
}
</button>
)
}
{hint && (
<span
className="text-xs text-muted bg-secondary/30 px-3 py-1.5 rounded-lg border border-secondary/30">
{hint}
</span>
)}
</div>
<div className="flex justify-between items-center gap-2">
{input}
{
addButtonCallBack && (
<button
className="bg-primary text-text-primary w-9 h-9 rounded-full flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 hover:bg-primary-dark hover:shadow-lg hover:scale-110 shadow-md"
onClick={addButtonCallBack}
disabled={isAddButtonDisabled}>
<FontAwesomeIcon icon={faPlus} className="w-4 h-4"/>
</button>
)
}
{
removeButtonCallBack && (
<button
className="bg-error/90 hover:bg-error text-text-primary w-9 h-9 rounded-full flex items-center justify-center transition-all duration-200 hover:shadow-lg hover:scale-110 shadow-md"
onClick={removeButtonCallBack}
>
<FontAwesomeIcon icon={faTrash} className={'w-4 h-4'}/>
</button>
)
}
</div>
</div>
)
}

View File

@@ -0,0 +1,44 @@
import React, {ChangeEvent, Dispatch} from "react";
interface NumberInputProps {
value: number;
setValue: Dispatch<React.SetStateAction<number>>;
placeholder?: string;
readOnly?: boolean;
disabled?: boolean;
}
export default function NumberInput(
{
value,
setValue,
placeholder,
readOnly = false,
disabled = false
}: NumberInputProps
) {
function handleChange(e: ChangeEvent<HTMLInputElement>) {
const newValue: number = parseInt(e.target.value);
if (!isNaN(newValue)) {
setValue(newValue);
}
}
return (
<input
type="number"
value={value}
onChange={handleChange}
className={`w-full bg-secondary/50 text-text-primary px-4 py-2.5 rounded-xl border border-secondary/50
focus:border-primary focus:ring-4 focus:ring-primary/20 focus:bg-secondary
hover:bg-secondary hover:border-secondary
placeholder:text-muted/60
outline-none transition-all duration-200
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
${readOnly ? 'cursor-default' : ''}`}
placeholder={placeholder}
readOnly={readOnly}
disabled={disabled}
/>
)
}

View File

@@ -0,0 +1,62 @@
import {storyStates} from "@/lib/models/Story";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faBookOpen, faKeyboard, faMagicWandSparkles, faPalette, faPenNib} from "@fortawesome/free-solid-svg-icons";
import React, {Dispatch, SetStateAction} from "react";
export interface RadioBoxValue {
label: string;
value: number;
}
interface RadioBoxProps {
selected: number;
setSelected: Dispatch<SetStateAction<number>>;
name: string;
}
export default function RadioBox(
{
selected,
setSelected,
name
}: RadioBoxProps) {
return (
<div className="flex flex-wrap gap-2">
{storyStates.map((option: RadioBoxValue) => (
<div key={option.value} className="flex items-center">
<input
type="radio"
id={option.label}
name={name}
value={option.value}
checked={selected === option.value}
onChange={() => setSelected(option.value)}
className="hidden"
/>
<label
htmlFor={option.label}
className={`px-3 lg:px-4 py-2 lg:py-2.5 text-xs lg:text-sm font-medium rounded-xl cursor-pointer transition-all duration-200 flex items-center gap-2 ${
selected === option.value
? 'bg-primary text-text-primary shadow-lg shadow-primary/30 scale-105 border border-primary-dark'
: 'bg-secondary/50 text-muted hover:bg-secondary hover:text-text-primary hover:scale-105 border border-secondary/50 hover:border-secondary'
}`}
>
<FontAwesomeIcon
icon={
[
faPenNib,
faKeyboard,
faPalette,
faBookOpen,
faMagicWandSparkles
][option.value]
}
className={selected === option.value ? "text-text-primary w-5 h-5" : "text-muted w-5 h-5"}
/>
{option.label}
</label>
</div>
))}
</div>
)
}

View File

@@ -0,0 +1,51 @@
import React from "react";
interface RadioOption {
value: string;
label: string;
}
interface RadioGroupProps {
name: string;
value: string;
onChange: (value: string) => void;
options: RadioOption[];
className?: string;
}
export default function RadioGroup(
{
name,
value,
onChange,
options,
className = ""
}: RadioGroupProps) {
return (
<div className={`flex flex-wrap gap-3 ${className}`}>
{options.map((option: RadioOption) => (
<div key={option.value} className="flex items-center">
<input
type="radio"
id={`${name}-${option.value}`}
name={name}
value={option.value}
checked={value === option.value}
onChange={() => onChange(option.value)}
className="hidden"
/>
<label
htmlFor={`${name}-${option.value}`}
className={`px-4 py-2 rounded-lg cursor-pointer transition-all duration-200 text-sm font-medium border ${
value === option.value
? 'bg-primary/20 text-primary border-primary/40 shadow-md'
: 'bg-secondary/30 text-text-primary border-secondary/50 hover:bg-secondary hover:border-secondary hover:scale-105'
}`}
>
{option.label}
</label>
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,62 @@
import React, {ChangeEvent} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {IconDefinition} from "@fortawesome/fontawesome-svg-core";
import {SelectBoxProps} from "@/shared/interface";
interface SearchInputWithSelectProps {
selectValue: string;
setSelectValue: (value: string) => void;
selectOptions: SelectBoxProps[];
inputValue: string;
setInputValue: (value: string) => void;
inputPlaceholder?: string;
searchIcon: IconDefinition;
onSearch: () => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
}
export default function SearchInputWithSelect(
{
selectValue,
setSelectValue,
selectOptions,
inputValue,
setInputValue,
inputPlaceholder,
searchIcon,
onSearch,
onKeyDown
}: SearchInputWithSelectProps) {
return (
<div className="flex items-center space-x-3">
<select
value={selectValue}
onChange={(e: ChangeEvent<HTMLSelectElement>) => setSelectValue(e.target.value)}
className="bg-secondary/50 text-text-primary px-4 py-2.5 rounded-xl border border-secondary/50 focus:border-primary focus:ring-4 focus:ring-primary/20 focus:bg-secondary hover:bg-secondary hover:border-secondary outline-none transition-all duration-200 font-medium"
>
{selectOptions.map((option: SelectBoxProps) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
<div className="flex-1 relative">
<input
type="text"
value={inputValue}
onChange={(e: ChangeEvent<HTMLInputElement>) => setInputValue(e.target.value)}
placeholder={inputPlaceholder}
className="w-full bg-secondary/50 text-text-primary px-4 py-2.5 rounded-xl border border-secondary/50 focus:border-primary focus:ring-4 focus:ring-primary/20 focus:bg-secondary hover:bg-secondary hover:border-secondary placeholder:text-muted/60 outline-none transition-all duration-200 pr-12"
onKeyDown={onKeyDown}
/>
<button
onClick={onSearch}
className="absolute right-0 top-0 h-full px-4 text-primary hover:text-primary-light hover:scale-110 transition-all duration-200"
>
<FontAwesomeIcon icon={searchIcon} className="w-5 h-5"/>
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,43 @@
import React, {ChangeEvent} from "react";
import {SelectBoxProps} from "@/shared/interface";
export interface SelectBoxFormProps {
onChangeCallBack: (event: ChangeEvent<HTMLSelectElement>) => void,
data: SelectBoxProps[],
defaultValue: string | null | undefined,
placeholder?: string,
disabled?: boolean
}
export default function SelectBox(
{
onChangeCallBack,
data,
defaultValue,
placeholder,
disabled
}: SelectBoxFormProps) {
return (
<select
onChange={onChangeCallBack}
disabled={disabled}
key={defaultValue || 'placeholder'}
defaultValue={defaultValue || '0'}
className={`w-full text-text-primary bg-secondary/50 hover:bg-secondary px-4 py-2.5 rounded-xl
border border-secondary/50
focus:border-primary focus:ring-4 focus:ring-primary/20 focus:bg-secondary
hover:border-secondary
outline-none transition-all duration-200 cursor-pointer
${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
>
{placeholder && <option value={'0'}>{placeholder}</option>}
{
data.map((item: SelectBoxProps) => (
<option key={item.value} value={item.value} className="bg-tertiary text-text-primary">
{item.label}
</option>
))
}
</select>
)
}

View File

@@ -0,0 +1,55 @@
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import React from "react";
import {IconDefinition} from "@fortawesome/fontawesome-svg-core";
import {faSpinner} from "@fortawesome/free-solid-svg-icons";
interface SubmitButtonWLoadingProps {
callBackAction: () => Promise<void> | void;
isLoading: boolean;
text: string;
loadingText: string;
icon?: IconDefinition;
}
export default function SubmitButtonWLoading(
{
callBackAction,
isLoading,
icon,
text,
loadingText
}: SubmitButtonWLoadingProps) {
return (
<button
onClick={callBackAction}
disabled={isLoading}
className={`group py-2.5 px-5 rounded-lg font-semibold transition-all flex items-center justify-center gap-2 relative overflow-hidden ${
isLoading
? 'bg-secondary cursor-not-allowed opacity-75'
: 'bg-secondary/80 hover:bg-secondary shadow-md hover:shadow-lg hover:shadow-primary/20 hover:scale-105 border border-secondary/50 hover:border-primary/30'
}`}
>
<span
className={`flex items-center gap-2 transition-all duration-200 ${isLoading ? 'opacity-0' : 'opacity-100'} text-primary`}>
{
icon &&
<FontAwesomeIcon icon={icon} className={'w-4 h-4 transition-transform group-hover:scale-110'}/>
}
<span className="text-sm">{text}</span>
</span>
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-secondary/50 backdrop-blur-sm">
<FontAwesomeIcon icon={faSpinner} className="w-4 h-4 text-primary animate-spin"/>
<span className="ml-3 text-primary text-sm font-medium">
<span className="hidden sm:inline">
{loadingText}
</span>
<span className="sm:hidden">
{loadingText}
</span>
</span>
</div>
)}
</button>
)
}

View File

@@ -0,0 +1,79 @@
import {SelectBoxProps} from "@/shared/interface";
import React, {ChangeEvent, Dispatch, SetStateAction} from "react";
import InputField from "@/components/form/InputField";
import TextInput from "@/components/form/TextInput";
interface SuggestFieldInputProps {
inputFieldName: string;
inputFieldIcon?: any;
searchTags: string;
tagued: string[];
handleTagSearch: (e: ChangeEvent<HTMLInputElement>) => void;
handleAddTag: (characterId: string) => void;
handleRemoveTag: (characterId: string) => void;
filteredTags: () => SelectBoxProps[];
showTagSuggestions: boolean;
setShowTagSuggestions: Dispatch<SetStateAction<boolean>>;
getTagLabel: (id: string) => string;
}
export default function SuggestFieldInput(
{
inputFieldName,
inputFieldIcon,
searchTags,
tagued,
handleTagSearch,
handleAddTag,
handleRemoveTag,
filteredTags,
showTagSuggestions,
setShowTagSuggestions,
getTagLabel
}: SuggestFieldInputProps) {
return (
<div className="bg-secondary/20 rounded-xl p-5 shadow-inner border border-secondary/30">
<InputField fieldName={inputFieldName} icon={inputFieldIcon} input={
<div className="w-full mb-3 relative">
<TextInput value={searchTags} setValue={handleTagSearch}
onFocus={() => setShowTagSuggestions(searchTags.trim().length > 0)}
placeholder="Rechercher et ajouter..."/>
{showTagSuggestions && filteredTags().length > 0 && (
<div
className="absolute top-full left-0 right-0 z-10 mt-2 bg-tertiary border border-secondary/50 rounded-xl shadow-2xl max-h-48 overflow-y-auto backdrop-blur-sm">
{filteredTags().map((character: SelectBoxProps) => (
<button
key={character.value}
className="w-full text-left px-4 py-2.5 hover:bg-secondary/70 text-text-primary transition-all hover:pl-5 first:rounded-t-xl last:rounded-b-xl font-medium"
onClick={() => handleAddTag(character.value)}
>
{character.label}
</button>
))}
</div>
)}
</div>
}/>
<div className="flex flex-wrap gap-2">
{tagued.length === 0 ? (
<p className="text-text-secondary text-sm italic">Aucun élément ajouté</p>
) : (
tagued.map((tag: string) => (
<div
key={tag}
className="group bg-primary/90 text-white rounded-full px-4 py-1.5 text-sm font-medium flex items-center gap-2 shadow-md hover:shadow-lg hover:scale-105 transition-all border border-primary-dark"
>
<span>{getTagLabel(tag)}</span>
<button
onClick={() => handleRemoveTag(tag)}
className="w-5 h-5 flex items-center justify-center rounded-full hover:bg-white/20 transition-all group-hover:scale-110 text-base font-bold"
>
×
</button>
</div>
))
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,39 @@
import React, {ChangeEvent} from "react";
interface TextInputProps {
value: string;
setValue?: (e: ChangeEvent<HTMLInputElement>) => void;
placeholder?: string;
readOnly?: boolean;
disabled?: boolean;
onFocus?: () => void;
}
export default function TextInput(
{
value,
setValue,
placeholder,
readOnly = false,
disabled = false,
onFocus
}: TextInputProps) {
return (
<input
type="text"
value={value}
onChange={setValue}
readOnly={readOnly}
disabled={disabled}
placeholder={placeholder}
onFocus={onFocus}
className={`w-full bg-secondary/50 text-text-primary px-4 py-2.5 rounded-xl border border-secondary/50
focus:border-primary focus:ring-4 focus:ring-primary/20 focus:bg-secondary
hover:bg-secondary hover:border-secondary
placeholder:text-muted/60
outline-none transition-all duration-200
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
${readOnly ? 'cursor-default' : ''}`}
/>
)
}

View File

@@ -0,0 +1,117 @@
import React, {ChangeEvent, useEffect, useState} from "react";
interface TextAreaInputProps {
value: string;
setValue: (e: ChangeEvent<HTMLTextAreaElement>) => void;
placeholder: string;
maxLength?: number;
}
export default function TextAreaInput(
{
value,
setValue,
placeholder,
maxLength
}: TextAreaInputProps) {
const [prevLength, setPrevLength] = useState(value.length);
const [isGrowing, setIsGrowing] = useState(false);
useEffect(() => {
if (value.length > prevLength) {
setIsGrowing(true);
setTimeout(() => setIsGrowing(false), 200);
}
setPrevLength(value.length);
}, [value.length, prevLength]);
const getProgressPercentage = () => {
if (!maxLength) return 0;
return Math.min((value.length / maxLength) * 100, 100);
};
const getStatusStyles = () => {
if (!maxLength) return {};
const percentage = getProgressPercentage();
if (percentage >= 100) return {
textColor: 'text-error',
bgColor: 'bg-error/10',
borderColor: 'border-error/30',
progressColor: 'bg-error'
};
if (percentage >= 90) return {
textColor: 'text-warning',
bgColor: 'bg-warning/10',
borderColor: 'border-warning/30',
progressColor: 'bg-warning'
};
if (percentage >= 75) return {
textColor: 'text-warning',
bgColor: 'bg-warning/10',
borderColor: 'border-warning/30',
progressColor: 'bg-warning'
};
return {
textColor: 'text-success',
bgColor: 'bg-success/10',
borderColor: 'border-success/30',
progressColor: 'bg-success'
};
};
const styles = getStatusStyles();
return (
<div className="flex-grow flex-col flex h-full">
<textarea
value={value}
onChange={setValue}
placeholder={placeholder}
rows={3}
className={`w-full flex-grow text-text-primary p-3 lg:p-4 rounded-xl border-2 outline-none resize-none transition-all duration-300 placeholder:text-muted/60 ${
maxLength && value.length >= maxLength
? 'border-error focus:ring-4 focus:ring-error/20 bg-error/10 hover:bg-error/15'
: 'bg-secondary/50 border-secondary/50 focus:ring-4 focus:ring-primary/20 focus:border-primary focus:bg-secondary hover:bg-secondary hover:border-secondary'
}`}
style={{height: '100%', minHeight: '200px'}}
/>
{maxLength && (
<div className="flex items-center justify-end gap-3 mt-3">
{/* Compteur avec effet de croissance */}
<div className={`flex items-center gap-3 px-4 py-2 rounded-lg border transition-all duration-300 ${
isGrowing ? 'scale-110 shadow-lg' : 'scale-100'
} ${styles.bgColor} ${styles.borderColor}`}>
{/* Progress bar visible */}
<div className="flex items-center gap-2">
<span className="text-xs text-muted font-medium">Progression</span>
<div className="w-20 h-2 bg-secondary/50 rounded-full overflow-hidden shadow-inner">
<div
className={`h-full rounded-full transition-all duration-500 ease-out ${styles.progressColor} shadow-md`}
style={{width: `${getProgressPercentage()}%`}}
></div>
</div>
</div>
{/* Compteur de caractères */}
<div className="flex items-center gap-2">
<div className={`text-sm font-semibold transition-all duration-200 ${
isGrowing ? 'scale-125' : 'scale-100'
} ${styles.textColor}`}>
{value.length}
<span className="text-muted mx-1">/</span>
<span className="text-text-secondary">{maxLength}</span>
</div>
<span className="text-xs text-muted font-medium">caractères</span>
</div>
</div>
</div>
)}
</div>
);
}