feat: site institucional completo com design system - Implementa\u00e7\u00e3o do site institucional da Aggios com design system completo incluindo gradientes, tipografia, componentes e se\u00e7\u00f5es de recursos, pre\u00e7os e CTA

This commit is contained in:
Erik Silva
2025-12-07 02:19:08 -03:00
parent 53f240a4da
commit bf6707e746
90 changed files with 25927 additions and 1 deletions

View File

@@ -0,0 +1,71 @@
"use client";
import { ButtonHTMLAttributes, forwardRef } from "react";
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "outline" | "ghost";
size?: "sm" | "md" | "lg";
isLoading?: boolean;
leftIcon?: string;
rightIcon?: string;
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
children,
variant = "primary",
size = "md",
isLoading = false,
leftIcon,
rightIcon,
className = "",
disabled,
...props
},
ref
) => {
const baseStyles =
"inline-flex items-center justify-center font-medium rounded-[6px] transition-opacity focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#FF3A05] disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer";
const variants = {
primary: "text-white hover:opacity-90 active:opacity-80",
secondary:
"bg-[#E5E5E5] dark:bg-gray-700 text-[#000000] dark:text-white hover:opacity-90 active:opacity-80",
outline:
"border border-[#E5E5E5] dark:border-gray-600 text-[#000000] dark:text-white hover:bg-[#E5E5E5]/10 dark:hover:bg-gray-700/50 active:bg-[#E5E5E5]/20 dark:active:bg-gray-700",
ghost: "text-[#000000] dark:text-white hover:bg-[#E5E5E5]/20 dark:hover:bg-gray-700/30 active:bg-[#E5E5E5]/30 dark:active:bg-gray-700/50",
};
const sizes = {
sm: "h-9 px-3 text-[13px]",
md: "h-10 px-4 text-[14px]",
lg: "h-12 px-6 text-[14px]",
};
return (
<button
ref={ref}
className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`}
style={variant === 'primary' ? { background: 'var(--gradient)' } : undefined}
disabled={disabled || isLoading}
{...props}
>
{isLoading && (
<i className="ri-loader-4-line animate-spin mr-2 text-[20px]" />
)}
{!isLoading && leftIcon && (
<i className={`${leftIcon} mr-2 text-[20px]`} />
)}
{children}
{!isLoading && rightIcon && (
<i className={`${rightIcon} ml-2 text-[20px]`} />
)}
</button>
);
}
);
Button.displayName = "Button";
export default Button;

View File

@@ -0,0 +1,69 @@
"use client";
import { InputHTMLAttributes, forwardRef, useState } from "react";
interface CheckboxProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string | React.ReactNode;
error?: string;
}
const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
({ label, error, className = "", onChange, checked: controlledChecked, ...props }, ref) => {
const [isChecked, setIsChecked] = useState(controlledChecked || false);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setIsChecked(e.target.checked);
if (onChange) {
onChange(e);
}
};
const checked = controlledChecked !== undefined ? controlledChecked : isChecked;
return (
<div className="w-full">
<label className="flex items-start gap-3 cursor-pointer group">
<div className="relative flex items-center justify-center mt-0.5">
<input
ref={ref}
type="checkbox"
className={`
appearance-none w-[18px] h-[18px] border rounded-sm
border-[#E5E5E5] dark:border-gray-600 bg-white dark:bg-gray-700
checked:border-[#FF3A05]
focus:outline-none focus:border-[#FF3A05]
transition-colors cursor-pointer
${className}
`}
style={{
background: checked ? 'var(--gradient)' : undefined,
}}
checked={checked}
onChange={handleChange}
{...props}
/>
<i
className={`ri-check-line absolute text-white text-[14px] pointer-events-none transition-opacity ${checked ? 'opacity-100' : 'opacity-0'
}`}
/>
</div>
{label && (
<span className="text-[14px] text-[#000000] dark:text-white select-none">
{label}
</span>
)}
</label>
{error && (
<p className="mt-1 text-[13px] text-[#FF3A05] flex items-center gap-1">
<i className="ri-error-warning-line" />
{error}
</p>
)}
</div>
);
}
);
Checkbox.displayName = "Checkbox";
export default Checkbox;

View File

@@ -0,0 +1,105 @@
"use client";
import { InputHTMLAttributes, forwardRef, useState } from "react";
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
helperText?: string;
leftIcon?: string;
rightIcon?: string;
onRightIconClick?: () => void;
}
const Input = forwardRef<HTMLInputElement, InputProps>(
(
{
label,
error,
helperText,
leftIcon,
rightIcon,
onRightIconClick,
className = "",
type,
...props
},
ref
) => {
const [showPassword, setShowPassword] = useState(false);
const isPassword = type === "password";
const inputType = isPassword ? (showPassword ? "text" : "password") : type;
return (
<div className="w-full">
{label && (
<label className="block text-[13px] font-semibold text-[#000000] dark:text-white mb-2">
{label}
{props.required && <span className="text-[#FF3A05] ml-1">*</span>}
</label>
)}
<div className="relative">
{leftIcon && (
<i
className={`${leftIcon} absolute left-3.5 top-1/2 -translate-y-1/2 text-[#7D7D7D] dark:text-gray-400 text-[20px]`}
/>
)}
<input
ref={ref}
type={inputType}
className={`
w-full px-3.5 py-3 text-[14px] font-normal
border rounded-md bg-white dark:bg-gray-700 dark:text-white
placeholder:text-[#7D7D7D] dark:placeholder:text-gray-400
transition-all
${leftIcon ? "pl-11" : ""}
${isPassword || rightIcon ? "pr-11" : ""}
${error
? "border-[#FF3A05]"
: "border-[#E5E5E5] dark:border-gray-600 focus:border-[#FF3A05]"
}
outline-none ring-0 focus:ring-0 shadow-none focus:shadow-none
disabled:bg-[#E5E5E5]/30 disabled:cursor-not-allowed
${className}
`}
{...props}
/>
{isPassword && (
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3.5 top-1/2 -translate-y-1/2 text-[#7D7D7D] hover:text-[#000000] transition-colors cursor-pointer"
>
<i
className={`${showPassword ? "ri-eye-off-line" : "ri-eye-line"} text-[20px]`}
/>
</button>
)}
{!isPassword && rightIcon && (
<button
type="button"
onClick={onRightIconClick}
className="absolute right-3.5 top-1/2 -translate-y-1/2 text-[#7D7D7D] hover:text-[#000000] transition-colors cursor-pointer"
>
<i className={`${rightIcon} text-[20px]`} />
</button>
)}
</div>
{error && (
<p className="mt-1 text-[13px] text-[#FF3A05] flex items-center gap-1">
<i className="ri-error-warning-line" />
{error}
</p>
)}
{helperText && !error && (
<p className="mt-1 text-[13px] text-[#7D7D7D]">{helperText}</p>
)}
</div>
);
}
);
Input.displayName = "Input";
export default Input;

View File

@@ -0,0 +1,211 @@
"use client";
import { SelectHTMLAttributes, forwardRef, useState, useRef, useEffect } from "react";
interface SelectOption {
value: string;
label: string;
}
interface SearchableSelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, 'onChange'> {
label?: string;
error?: string;
helperText?: string;
leftIcon?: string;
options: SelectOption[];
placeholder?: string;
onChange?: (value: string) => void;
value?: string;
}
const SearchableSelect = forwardRef<HTMLSelectElement, SearchableSelectProps>(
(
{
label,
error,
helperText,
leftIcon,
options,
placeholder,
className = "",
onChange,
value,
required,
...props
},
ref
) => {
const [isOpen, setIsOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [selectedOption, setSelectedOption] = useState<SelectOption | null>(
options.find(opt => opt.value === value) || null
);
const containerRef = useRef<HTMLDivElement>(null);
const searchInputRef = useRef<HTMLInputElement>(null);
const filteredOptions = options.filter(option =>
option.label.toLowerCase().includes(searchTerm.toLowerCase())
);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
useEffect(() => {
if (isOpen && searchInputRef.current) {
searchInputRef.current.focus();
}
}, [isOpen]);
useEffect(() => {
if (value) {
const option = options.find(opt => opt.value === value);
if (option) {
setSelectedOption(option);
}
}
}, [value, options]);
const handleSelect = (option: SelectOption) => {
setSelectedOption(option);
setIsOpen(false);
setSearchTerm("");
if (onChange) {
onChange(option.value);
}
};
return (
<div className="w-full">
{/* Hidden select for form compatibility */}
<select
ref={ref}
value={selectedOption?.value || ""}
onChange={(e) => {
const option = options.find(opt => opt.value === e.target.value);
if (option) handleSelect(option);
}}
className="hidden"
required={required}
{...props}
>
<option value="" disabled>
{placeholder || "Selecione uma opção"}
</option>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
{label && (
<label className="block text-[13px] font-semibold text-[#000000] mb-2">
{label}
{required && <span className="text-[#FF3A05] ml-1">*</span>}
</label>
)}
<div ref={containerRef} className="relative">
{leftIcon && (
<i
className={`${leftIcon} absolute left-3.5 top-1/2 -translate-y-1/2 text-[#7D7D7D] text-[20px] pointer-events-none z-10`}
/>
)}
{/* Custom trigger */}
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className={`
w-full px-3.5 py-3 text-[14px] font-normal
border rounded-md bg-white
text-[#000000] text-left
transition-all
cursor-pointer
${leftIcon ? "pl-11" : ""}
pr-11
${error
? "border-[#FF3A05]"
: "border-[#E5E5E5] focus:border-[#FF3A05]"
}
outline-none ring-0 focus:ring-0 shadow-none focus:shadow-none
${className}
`}
>
{selectedOption ? selectedOption.label : (
<span className="text-[#7D7D7D]">{placeholder || "Selecione uma opção"}</span>
)}
</button>
<i className={`ri-arrow-${isOpen ? 'up' : 'down'}-s-line absolute right-3.5 top-1/2 -translate-y-1/2 text-[#7D7D7D] text-[20px] pointer-events-none transition-transform`} />
{/* Dropdown */}
{isOpen && (
<div className="absolute z-50 w-full mt-2 bg-white border border-[#E5E5E5] rounded-md shadow-lg max-h-[300px] overflow-hidden">
{/* Search input */}
<div className="p-2 border-b border-[#E5E5E5]">
<div className="relative">
<i className="ri-search-line absolute left-3 top-1/2 -translate-y-1/2 text-[#7D7D7D] text-[16px]" />
<input
ref={searchInputRef}
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Buscar..."
className="w-full pl-9 pr-3 py-2 text-[14px] border border-[#E5E5E5] rounded-md outline-none focus:border-[#FF3A05] shadow-none"
/>
</div>
</div>
{/* Options list */}
<div className="overflow-y-auto max-h-60">
{filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<button
key={option.value}
type="button"
onClick={() => handleSelect(option)}
className={`
w-full px-4 py-2.5 text-left text-[14px] transition-colors
hover:bg-[#FDFDFC] cursor-pointer
${selectedOption?.value === option.value ? 'bg-[#FF3A05]/10 text-[#FF3A05] font-medium' : 'text-[#000000]'}
`}
>
{option.label}
</button>
))
) : (
<div className="px-4 py-8 text-center text-[#7D7D7D] text-[14px]">
Nenhum resultado encontrado
</div>
)}
</div>
</div>
)}
</div>
{helperText && !error && (
<p className="mt-1.5 text-[12px] text-[#7D7D7D]">{helperText}</p>
)}
{error && (
<p className="mt-1 text-[13px] text-[#FF3A05] flex items-center gap-1">
<i className="ri-error-warning-line" />
{error}
</p>
)}
</div>
);
}
);
SearchableSelect.displayName = "SearchableSelect";
export default SearchableSelect;

View File

@@ -0,0 +1,89 @@
"use client";
import { SelectHTMLAttributes, forwardRef } from "react";
interface SelectOption {
value: string;
label: string;
}
interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
label?: string;
error?: string;
helperText?: string;
leftIcon?: string;
options: SelectOption[];
placeholder?: string;
}
const Select = forwardRef<HTMLSelectElement, SelectProps>(
(
{
label,
error,
helperText,
leftIcon,
options,
placeholder,
className = "",
...props
},
ref
) => {
return (
<div className="w-full">
{label && (
<label className="block text-[13px] font-semibold text-[#000000] mb-2">
{label}
{props.required && <span className="text-[#FF3A05] ml-1">*</span>}
</label>
)}
<div className="relative">
{leftIcon && (
<i
className={`${leftIcon} absolute left-3.5 top-1/2 -translate-y-1/2 text-[#7D7D7D] text-[20px] pointer-events-none z-10`}
/>
)}
<select
ref={ref}
className={`
w-full px-3.5 py-3 text-[14px] font-normal
border rounded-md bg-white
text-[#000000]
transition-all appearance-none
cursor-pointer
${leftIcon ? "pl-11" : ""}
pr-11
${error
? "border-[#FF3A05]"
: "border-[#E5E5E5] focus:border-[#FF3A05]"
}
outline-none ring-0 focus:ring-0 shadow-none focus:shadow-none
disabled:bg-[#F5F5F5] disabled:cursor-not-allowed
${className}
`}
{...props}
>
<option value="" disabled>
{placeholder || "Selecione uma opção"}
</option>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
<i className="ri-arrow-down-s-line absolute right-3.5 top-1/2 -translate-y-1/2 text-[#7D7D7D] text-[20px] pointer-events-none" />
</div>
{helperText && !error && (
<p className="mt-1.5 text-[12px] text-[#7D7D7D]">{helperText}</p>
)}
{error && <p className="mt-1.5 text-[12px] text-[#FF3A05]">{error}</p>}
</div>
);
}
);
Select.displayName = "Select";
export default Select;

View File

@@ -0,0 +1,5 @@
export { default as Button } from "./Button";
export { default as Input } from "./Input";
export { default as Checkbox } from "./Checkbox";
export { default as Select } from "./Select";
export { default as SearchableSelect } from "./SearchableSelect";