@brutalcomponent/react
Version:
Brutalist React components
734 lines (729 loc) • 24.7 kB
JavaScript
;
var chunkZERAQT5C_js = require('./chunk-ZERAQT5C.js');
var chunk7YHHVG7W_js = require('./chunk-7YHHVG7W.js');
var chunk7T4KDGYW_js = require('./chunk-7T4KDGYW.js');
var React2 = require('react');
var clsx = require('clsx');
var fa = require('react-icons/fa');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
var React2__default = /*#__PURE__*/_interopDefault(React2);
/**
* @brutalcomponent/react
* (c) David Heffler (https://dvh.sh)
* Licensed under MIT
*/
var ButtonGroup = ({
children,
className,
direction = "horizontal",
attached = true
}) => {
const childrenArray = Array.isArray(children) ? children : [children];
const validChildren = childrenArray.filter(
(child) => React2__default.default.isValidElement(child)
);
return /* @__PURE__ */ React2__default.default.createElement(
"div",
{
className: clsx.clsx(
"inline-flex",
direction === "horizontal" ? attached ? "-space-x-px" : "gap-2" : "flex-col",
direction === "vertical" && (attached ? "-space-y-px" : "gap-2"),
className
)
},
validChildren.map((child, index) => {
if (child.type === chunkZERAQT5C_js.Button) {
const childCount = validChildren.length;
if (!attached) {
return React2__default.default.cloneElement(child, { key: child.key || index });
}
const typedChild = child;
return React2__default.default.cloneElement(typedChild, {
key: child.key || index,
className: clsx.clsx(
typedChild.props.className,
direction === "horizontal" && [
index === 0 && "rounded-r-none",
index === childCount - 1 && "rounded-l-none",
index !== 0 && index !== childCount - 1 && "rounded-none"
],
direction === "vertical" && [
index === 0 && "rounded-b-none",
index === childCount - 1 && "rounded-t-none",
index !== 0 && index !== childCount - 1 && "rounded-none"
]
)
});
}
return React2__default.default.cloneElement(child, { key: child.key || index });
})
);
};
var Input = React2__default.default.forwardRef(
({
label,
error,
hint,
leftIcon: LeftIcon,
rightIcon: RightIcon,
brutal = true,
size = "md",
variant = "default",
required = false,
showPasswordToggle = false,
accentColor = "brutal-pink",
className,
disabled,
type = "text",
...props
}, ref) => {
const [showPassword, setShowPassword] = React2.useState(false);
const [isFocused, setIsFocused] = React2.useState(false);
const sizeClasses = chunk7T4KDGYW_js.getSizeClasses(size);
chunk7T4KDGYW_js.getAccentClasses(accentColor);
const isPassword = type === "password";
const actualType = isPassword && showPassword ? "text" : type;
const hasLeftIcon = LeftIcon;
const hasRightIcon = RightIcon || isPassword && showPasswordToggle;
return /* @__PURE__ */ React2__default.default.createElement(
"div",
{
className: "w-full",
style: {
"--accent-color": accentColor.startsWith("#") ? accentColor : `var(--brutal-${accentColor.replace("brutal-", "")})`
}
},
label && /* @__PURE__ */ React2__default.default.createElement(
"label",
{
className: chunk7T4KDGYW_js.cn(
"block mb-2 font-black uppercase tracking-wider text-brutal-black",
sizeClasses.text === "text-xs" ? "text-xs" : "text-sm",
required && "after:content-['*'] after:ml-1 after:text-accent"
)
},
label
),
/* @__PURE__ */ React2__default.default.createElement("div", { className: "relative" }, hasLeftIcon && /* @__PURE__ */ React2__default.default.createElement(
"div",
{
className: chunk7T4KDGYW_js.cn(
"absolute top-1/2 -translate-y-1/2 pointer-events-none",
size === "xs" ? "left-2" : size === "sm" ? "left-2.5" : "left-3"
)
},
/* @__PURE__ */ React2__default.default.createElement(
chunk7YHHVG7W_js.Icon,
{
icon: LeftIcon,
size: size === "xs" ? "xs" : size === "sm" ? "sm" : "md",
className: "text-brutal-gray-500"
}
)
), /* @__PURE__ */ React2__default.default.createElement(
"input",
{
ref,
type: actualType,
className: chunk7T4KDGYW_js.cn(
// Base styling
"w-full font-mono transition-all duration-200",
"bg-brutal-white text-brutal-black placeholder:text-brutal-gray-500",
// Size classes
sizeClasses.padding,
sizeClasses.text,
// Icon spacing
hasLeftIcon && (size === "xs" ? "pl-8" : size === "sm" ? "pl-9" : "pl-12"),
hasRightIcon && (size === "xs" ? "pr-8" : size === "sm" ? "pr-9" : "pr-12"),
// Brutal styling
brutal && [
sizeClasses.border,
"border-brutal-black",
"focus:shadow-brutal focus:-translate-x-0.5 focus:-translate-y-0.5",
"hover:shadow-brutal-sm hover:-translate-x-0.25 hover:-translate-y-0.25"
],
!brutal && [
"border-2 border-brutal-gray-300 rounded-md",
"focus:border-accent focus:ring-2 focus:ring-accent/20"
],
// Variant styling
variant === "ghost" && [
"bg-transparent border-transparent",
"focus:bg-brutal-white focus:border-brutal-black"
],
variant === "filled" && "bg-brutal-gray-100",
// States
error && "border-accent",
disabled && [
"opacity-50 cursor-not-allowed bg-brutal-gray-100",
"hover:transform-none hover:shadow-none"
],
isFocused && brutal && "shadow-brutal -translate-x-0.5 -translate-y-0.5",
// Focus styles
"focus:outline-none",
className
),
disabled,
required,
onFocus: (e) => {
setIsFocused(true);
props.onFocus?.(e);
},
onBlur: (e) => {
setIsFocused(false);
props.onBlur?.(e);
},
"aria-invalid": error ? "true" : "false",
"aria-describedby": error ? `${props.id || "input"}-error` : hint ? `${props.id || "input"}-hint` : void 0,
...props
}
), hasRightIcon && /* @__PURE__ */ React2__default.default.createElement(
"div",
{
className: chunk7T4KDGYW_js.cn(
"absolute top-1/2 -translate-y-1/2",
size === "xs" ? "right-2" : size === "sm" ? "right-2.5" : "right-3"
)
},
isPassword && showPasswordToggle ? /* @__PURE__ */ React2__default.default.createElement(
"button",
{
type: "button",
onClick: () => setShowPassword(!showPassword),
className: chunk7T4KDGYW_js.cn(
"p-1 transition-colors duration-200",
"hover:bg-brutal-gray-100 rounded",
"focus:outline-none focus:bg-brutal-gray-100"
),
"aria-label": showPassword ? "Hide password" : "Show password",
tabIndex: -1
},
/* @__PURE__ */ React2__default.default.createElement(
chunk7YHHVG7W_js.Icon,
{
icon: showPassword ? fa.FaEyeSlash : fa.FaEye,
size: size === "xs" ? "xs" : size === "sm" ? "sm" : "md",
className: "text-brutal-gray-500 hover:text-brutal-black"
}
)
) : RightIcon ? /* @__PURE__ */ React2__default.default.createElement(
chunk7YHHVG7W_js.Icon,
{
icon: RightIcon,
size: size === "xs" ? "xs" : size === "sm" ? "sm" : "md",
className: "text-brutal-gray-500"
}
) : null
)),
hint && !error && /* @__PURE__ */ React2__default.default.createElement(
"p",
{
id: `${props.id || "input"}-hint`,
className: chunk7T4KDGYW_js.cn(
"mt-2 text-brutal-gray-600 font-mono",
sizeClasses.text === "text-xs" ? "text-xs" : "text-sm"
)
},
hint
),
error && /* @__PURE__ */ React2__default.default.createElement(
"p",
{
id: `${props.id || "input"}-error`,
className: chunk7T4KDGYW_js.cn(
"mt-2 font-black uppercase tracking-wider text-accent",
sizeClasses.text === "text-xs" ? "text-xs" : "text-sm"
),
role: "alert"
},
error
)
);
}
);
Input.displayName = "Input";
var Select = React2__default.default.forwardRef(
({
label,
error,
hint,
options,
brutal = true,
size = "md",
variant = "default",
required = false,
placeholder = "Select an option",
accentColor = "brutal-pink",
className,
disabled,
...props
}, ref) => {
const [isFocused, setIsFocused] = React2.useState(false);
const sizeClasses = chunk7T4KDGYW_js.getSizeClasses(size);
chunk7T4KDGYW_js.getAccentClasses(accentColor);
return /* @__PURE__ */ React2__default.default.createElement(
"div",
{
className: "w-full",
style: {
"--accent-color": accentColor.startsWith("#") ? accentColor : `var(--brutal-${accentColor.replace("brutal-", "")})`
}
},
label && /* @__PURE__ */ React2__default.default.createElement(
"label",
{
className: chunk7T4KDGYW_js.cn(
"block mb-2 font-black uppercase tracking-wider text-brutal-black",
sizeClasses.text === "text-xs" ? "text-xs" : "text-sm",
required && "after:content-['*'] after:ml-1 after:text-accent"
)
},
label
),
/* @__PURE__ */ React2__default.default.createElement("div", { className: "relative" }, /* @__PURE__ */ React2__default.default.createElement(
"select",
{
ref,
className: chunk7T4KDGYW_js.cn(
// Base styling
"w-full font-mono appearance-none cursor-pointer",
"bg-brutal-white text-brutal-black transition-all duration-200",
// Size classes
sizeClasses.padding,
sizeClasses.text,
// Icon spacing (for dropdown arrow)
size === "xs" ? "pr-8" : size === "sm" ? "pr-9" : "pr-12",
// Brutal styling
brutal && [
sizeClasses.border,
"border-brutal-black",
"focus:shadow-brutal focus:-translate-x-0.5 focus:-translate-y-0.5",
"hover:shadow-brutal-sm hover:-translate-x-0.25 hover:-translate-y-0.25"
],
!brutal && [
"border-2 border-brutal-gray-300 rounded-md",
"focus:border-accent focus:ring-2 focus:ring-accent/20"
],
// Variant styling
variant === "ghost" && [
"bg-transparent border-transparent",
"focus:bg-brutal-white focus:border-brutal-black"
],
variant === "filled" && "bg-brutal-gray-100",
// States
error && "border-accent",
disabled && [
"opacity-50 cursor-not-allowed bg-brutal-gray-100",
"hover:transform-none hover:shadow-none"
],
isFocused && brutal && "shadow-brutal -translate-x-0.5 -translate-y-0.5",
// Focus styles
"focus:outline-none",
className
),
disabled,
required,
onFocus: (e) => {
setIsFocused(true);
props.onFocus?.(e);
},
onBlur: (e) => {
setIsFocused(false);
props.onBlur?.(e);
},
"aria-invalid": error ? "true" : "false",
"aria-describedby": error ? `${props.id || "select"}-error` : hint ? `${props.id || "select"}-hint` : void 0,
...props
},
placeholder && /* @__PURE__ */ React2__default.default.createElement("option", { value: "", disabled: true }, placeholder),
options.map((option) => /* @__PURE__ */ React2__default.default.createElement(
"option",
{
key: option.value,
value: option.value,
disabled: option.disabled
},
option.label
))
), /* @__PURE__ */ React2__default.default.createElement(
"div",
{
className: chunk7T4KDGYW_js.cn(
"absolute top-1/2 -translate-y-1/2 pointer-events-none",
size === "xs" ? "right-2" : size === "sm" ? "right-2.5" : "right-3"
)
},
/* @__PURE__ */ React2__default.default.createElement(
chunk7YHHVG7W_js.Icon,
{
icon: fa.FaChevronDown,
size: size === "xs" ? "xs" : size === "sm" ? "sm" : "md",
className: chunk7T4KDGYW_js.cn(
"transition-all duration-200",
isFocused ? "text-accent rotate-180" : "text-brutal-gray-500",
disabled && "opacity-50"
)
}
)
)),
hint && !error && /* @__PURE__ */ React2__default.default.createElement(
"p",
{
id: `${props.id || "select"}-hint`,
className: chunk7T4KDGYW_js.cn(
"mt-2 text-brutal-gray-600 font-mono",
sizeClasses.text === "text-xs" ? "text-xs" : "text-sm"
)
},
hint
),
error && /* @__PURE__ */ React2__default.default.createElement(
"p",
{
id: `${props.id || "select"}-error`,
className: chunk7T4KDGYW_js.cn(
"mt-2 font-black uppercase tracking-wider text-accent",
sizeClasses.text === "text-xs" ? "text-xs" : "text-sm"
),
role: "alert"
},
error
)
);
}
);
Select.displayName = "Select";
var Textarea = React2__default.default.forwardRef(
({
label,
error,
hint,
brutal = true,
size = "md",
variant = "default",
required = false,
autoResize = false,
showCharacterCount = false,
maxLength,
accentColor = "brutal-pink",
className,
disabled,
rows = 4,
...props
}, ref) => {
const [isFocused, setIsFocused] = React2.useState(false);
const [characterCount, setCharacterCount] = React2.useState(0);
const textareaRef = React2.useRef(null);
const sizeClasses = chunk7T4KDGYW_js.getSizeClasses(size);
React2.useEffect(() => {
if (autoResize && textareaRef.current) {
const textarea = textareaRef.current;
textarea.style.height = "auto";
textarea.style.height = `${textarea.scrollHeight}px`;
}
}, [props.value, autoResize]);
const handleChange = (e) => {
setCharacterCount(e.target.value.length);
props.onChange?.(e);
if (autoResize && textareaRef.current) {
const textarea = textareaRef.current;
textarea.style.height = "auto";
textarea.style.height = `${textarea.scrollHeight}px`;
}
};
const combinedRef = (node) => {
if (textareaRef.current !== node) {
textareaRef.current = node;
}
if (typeof ref === "function") {
ref(node);
} else if (ref) {
ref.current = node;
}
};
const isNearLimit = maxLength && characterCount > maxLength * 0.8;
const isOverLimit = maxLength && characterCount > maxLength;
return /* @__PURE__ */ React2__default.default.createElement(
"div",
{
className: "w-full",
style: {
"--accent-color": accentColor.startsWith("#") ? accentColor : `var(--brutal-${accentColor.replace("brutal-", "")})`
}
},
label && /* @__PURE__ */ React2__default.default.createElement(
"label",
{
className: chunk7T4KDGYW_js.cn(
"block mb-2 font-black uppercase tracking-wider text-brutal-black",
sizeClasses.text === "text-xs" ? "text-xs" : "text-sm",
required && "after:content-['*'] after:ml-1 after:text-accent"
)
},
label
),
/* @__PURE__ */ React2__default.default.createElement(
"textarea",
{
ref: combinedRef,
rows: autoResize ? 1 : rows,
maxLength,
className: chunk7T4KDGYW_js.cn(
// Base styling
"w-full font-mono transition-all duration-200 resize-none",
"bg-brutal-white text-brutal-black placeholder:text-brutal-gray-500",
// Size classes
sizeClasses.padding,
sizeClasses.text,
// Brutal styling
brutal && [
sizeClasses.border,
"border-brutal-black",
"focus:shadow-brutal focus:-translate-x-0.5 focus:-translate-y-0.5",
"hover:shadow-brutal-sm hover:-translate-x-0.25 hover:-translate-y-0.25"
],
!brutal && [
"border-2 border-brutal-gray-300 rounded-md",
"focus:border-accent focus:ring-2 focus:ring-accent/20"
],
// Variant styling
variant === "ghost" && [
"bg-transparent border-transparent",
"focus:bg-brutal-white focus:border-brutal-black"
],
variant === "filled" && "bg-brutal-gray-100",
// States
error && "border-accent",
isOverLimit && "border-brutal-coral",
disabled && [
"opacity-50 cursor-not-allowed bg-brutal-gray-100",
"hover:transform-none hover:shadow-none"
],
isFocused && brutal && "shadow-brutal -translate-x-0.5 -translate-y-0.5",
// Resize behavior
!autoResize && "resize-y",
// Focus styles
"focus:outline-none",
className
),
disabled,
required,
onFocus: (e) => {
setIsFocused(true);
props.onFocus?.(e);
},
onBlur: (e) => {
setIsFocused(false);
props.onBlur?.(e);
},
onChange: handleChange,
"aria-invalid": error ? "true" : "false",
"aria-describedby": error ? `${props.id || "textarea"}-error` : hint ? `${props.id || "textarea"}-hint` : void 0,
...props
}
),
/* @__PURE__ */ React2__default.default.createElement("div", { className: "flex justify-between items-start mt-2" }, /* @__PURE__ */ React2__default.default.createElement("div", { className: "flex-1" }, hint && !error && /* @__PURE__ */ React2__default.default.createElement(
"p",
{
id: `${props.id || "textarea"}-hint`,
className: chunk7T4KDGYW_js.cn(
"text-brutal-gray-600 font-mono",
sizeClasses.text === "text-xs" ? "text-xs" : "text-sm"
)
},
hint
), error && /* @__PURE__ */ React2__default.default.createElement(
"p",
{
id: `${props.id || "textarea"}-error`,
className: chunk7T4KDGYW_js.cn(
"font-black uppercase tracking-wider text-accent",
sizeClasses.text === "text-xs" ? "text-xs" : "text-sm"
),
role: "alert"
},
error
)), showCharacterCount && maxLength && /* @__PURE__ */ React2__default.default.createElement(
"p",
{
className: chunk7T4KDGYW_js.cn(
"font-mono ml-4 flex-shrink-0",
sizeClasses.text === "text-xs" ? "text-xs" : "text-sm",
isOverLimit ? "text-brutal-coral font-bold" : isNearLimit ? "text-brutal-yellow font-bold" : "text-brutal-gray-500"
)
},
characterCount,
"/",
maxLength
))
);
}
);
Textarea.displayName = "Textarea";
var InputSkeleton = ({
label = false,
brutal = true,
size = "md",
className
}) => {
const sizeClasses = chunk7T4KDGYW_js.getSizeClasses(size);
return /* @__PURE__ */ React2__default.default.createElement("div", { className: chunk7T4KDGYW_js.cn("animate-pulse", className) }, label && /* @__PURE__ */ React2__default.default.createElement(
"div",
{
className: chunk7T4KDGYW_js.cn(
"h-4 bg-brutal-gray-300 rounded w-20 mb-2",
sizeClasses.text === "text-xs" && "h-3"
)
}
), /* @__PURE__ */ React2__default.default.createElement(
"div",
{
className: chunk7T4KDGYW_js.cn(
"w-full bg-brutal-gray-200",
sizeClasses.padding,
brutal && sizeClasses.border && "border-brutal-gray-300",
!brutal && "border-2 border-brutal-gray-300 rounded-md"
)
},
/* @__PURE__ */ React2__default.default.createElement(
"div",
{
className: chunk7T4KDGYW_js.cn(
"h-4 bg-brutal-gray-300 rounded w-1/2",
sizeClasses.text === "text-xs" && "h-3"
)
}
)
));
};
var SelectSkeleton = (props) => /* @__PURE__ */ React2__default.default.createElement(InputSkeleton, { ...props });
var TextareaSkeleton = ({
label = false,
brutal = true,
size = "md",
rows = 4,
className
}) => {
const sizeClasses = chunk7T4KDGYW_js.getSizeClasses(size);
return /* @__PURE__ */ React2__default.default.createElement("div", { className: chunk7T4KDGYW_js.cn("animate-pulse", className) }, label && /* @__PURE__ */ React2__default.default.createElement(
"div",
{
className: chunk7T4KDGYW_js.cn(
"h-4 bg-brutal-gray-300 rounded w-20 mb-2",
sizeClasses.text === "text-xs" && "h-3"
)
}
), /* @__PURE__ */ React2__default.default.createElement(
"div",
{
className: chunk7T4KDGYW_js.cn(
"w-full bg-brutal-gray-200 space-y-2",
sizeClasses.padding,
brutal && sizeClasses.border && "border-brutal-gray-300",
!brutal && "border-2 border-brutal-gray-300 rounded-md"
),
style: { minHeight: `${rows * 1.5}rem` }
},
Array.from({ length: Math.min(rows, 3) }).map((_, i) => /* @__PURE__ */ React2__default.default.createElement(
"div",
{
key: i,
className: chunk7T4KDGYW_js.cn(
"h-4 bg-brutal-gray-300 rounded",
i === Math.min(rows, 3) - 1 && "w-2/3"
// Last line shorter
)
}
))
));
};
/**
* @file src/components/core/Button/ButtonGroup.tsx
* @author David (https://dvh.sh)
* @license MIT
*
* @created Thu Sep 11 2025
* @updated Fri Sep 12 2025
*
* @description
* Group buttons together
*/
/**
* @file src/components/core/Button/index.ts
* @author David (https://dvh.sh)
* @license MIT
*
* @created Thu Sep 11 2025
* @updated Fri Sep 12 2025
*
* @description
* Button components barrel export
*/
/**
* @file src/components/core/Input/Input.tsx
* @author David (https://dvh.sh)
* @license MIT
*
* @created Thu Sep 11 2025
* @updated Sat Sep 13 2025
*
* @description
* Brutal input field with optional icons, validation, and multiple variants
* @client
*/
/**
* @file src/components/core/Input/Select.tsx
* @author David (https://dvh.sh)
* @license MIT
*
* @created Thu Sep 11 2025
* @updated Sat Sep 13 2025
*
* @description
* Brutal select dropdown with enhanced styling and functionality
* @client
*/
/**
* @file src/components/core/Input/Textarea.tsx
* @author David (https://dvh.sh)
* @license MIT
*
* @created Sat Sep 13 2025
* @updated Sat Sep 13 2025
*
* @description
* Brutal textarea component with auto-resize and character counting
* @client
*/
/**
* @file src/components/core/Input/InputSkeleton.tsx
* @author David (https://dvh.sh)
* @license MIT
*
* @created Sat Sep 13 2025
* @updated Sat Sep 13 2025
*
* @description
* Skeleton loaders for Input components
*/
/**
* @file src/components/core/Input/index.ts
* @author David (https://dvh.sh)
* @license MIT
*
* @created Thu Sep 11 2025
* @updated Sat Sep 13 2025
*
* @description
* Input components barrel export
*/
exports.ButtonGroup = ButtonGroup;
exports.Input = Input;
exports.InputSkeleton = InputSkeleton;
exports.Select = Select;
exports.SelectSkeleton = SelectSkeleton;
exports.Textarea = Textarea;
exports.TextareaSkeleton = TextareaSkeleton;
//# sourceMappingURL=chunk-7GZ4CFXP.js.map
//# sourceMappingURL=chunk-7GZ4CFXP.js.map