Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions src/components/common/EntityBadge/EntityBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import React from "react";
import { cn } from "@/lib/utils";
import { X } from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";

/**
* Generic entity badge — base for MaterialBadge, RamoAtividadeBadge, etc.
*
* Pattern extracted in F1 Onda D (auditoria de duplicação): two near-identical
* 177-line components were merged into this generic component + thin wrappers.
*
* Behavior:
* - Optional color dot from `hexCode`.
* - Optional contextual label rendered before name with `groupSeparator`.
* - Optional `icon` rendered before color dot (e.g. for "Ramo de Atividade").
* - Optional remove button (`onRemove`).
* - Optional tooltip (auto-rendered when `groupLabel` or `productCount` set).
*/
export interface EntityBadgeProps {
/** Name shown as badge label (always rendered) */
name: string;
/** Optional contextual label (e.g. "Plásticos" for a material; "Hotel" for a ramo) */
groupLabel?: string;
/** Hex color for the leading dot. If null/undefined, dot is omitted. */
hexCode?: string | null;
/** Optional emoji or single-character icon shown before the dot */
icon?: string | null;
/** Visual size */
size?: "sm" | "md" | "lg";
/** Visual variant */
variant?: "default" | "outline" | "solid" | "ghost";
/** When true and `groupLabel` is set, renders `${groupLabel}${groupSeparator}${name}` */
showGroup?: boolean;
/** Separator between group label and name. Default: ": " */
groupSeparator?: string;
/** Click handler (full badge) */
onClick?: () => void;
/** Remove handler — when set, renders an `×` button */
onRemove?: () => void;
/** Extra classes merged via cn() */
className?: string;
/** Disable tooltip render even when context exists */
showTooltip?: boolean;
/** Number of products linked — appended to tooltip when present */
productCount?: number;
/** Per-size cap for the label (Tailwind max-width classes) */
truncateMaxWidth?: { sm: string; md: string; lg: string };
/** Tooltip content override — when not provided, builds from groupLabel + productCount */
tooltipContent?: React.ReactNode;
}

const DEFAULT_MAX_WIDTH = {
sm: "max-w-[100px]",
md: "max-w-[120px]",
lg: "max-w-[150px]",
};

export function EntityBadge({
name,
groupLabel,
hexCode,
icon,
size = "md",
variant = "default",
showGroup = false,
groupSeparator = ": ",
onClick,
onRemove,
className,
showTooltip = true,
productCount,
truncateMaxWidth = DEFAULT_MAX_WIDTH,
tooltipContent,
}: EntityBadgeProps) {
const sizeClasses = {
sm: "text-[11px] px-2 py-0.5 gap-1",
md: "text-xs px-2.5 py-1 gap-1.5",
lg: "text-sm px-3 py-1.5 gap-2",
};

const dotSizeClasses = {
sm: "w-1.5 h-1.5",
md: "w-2 h-2",
lg: "w-2.5 h-2.5",
};

const variantClasses = {
default: "bg-muted text-muted-foreground hover:bg-muted/80",
outline: "border border-border bg-transparent hover:bg-muted/50",
solid: "bg-foreground text-background hover:bg-foreground/90",
Comment on lines +94 to +95
ghost: "bg-transparent text-muted-foreground hover:bg-muted/50",
};

const displayText =
showGroup && groupLabel ? `${groupLabel}${groupSeparator}${name}` : name;

const badgeContent = (
<span
className={cn(
"inline-flex items-center rounded-full font-medium transition-colors",
sizeClasses[size],
variantClasses[variant],
onClick && "cursor-pointer",
className,
)}
onClick={onClick}
>
{icon && <span className="leading-none">{icon}</span>}
{hexCode && (
<span
className={cn("rounded-full shrink-0", dotSizeClasses[size])}
style={{ backgroundColor: hexCode }}
aria-hidden="true"
/>
)}
<span
className={cn(
"truncate",
size === "sm" && truncateMaxWidth.sm,
size === "md" && truncateMaxWidth.md,
Comment on lines +121 to +125
size === "lg" && truncateMaxWidth.lg,
)}
>
{displayText}
</span>
{onRemove && (
Comment on lines +121 to +131

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Render product counts in the badge again

When productCount is passed to MaterialBadge or RamoAtividadeBadge, the previous components rendered a small visible count pill next to the label for positive counts; the new shared content only renders the label and then the remove button, so the count is now hidden except inside the tooltip. That breaks the advertised API/visual compatibility for any badge using productCount to show product totals inline.

Useful? React with 👍 / 👎.

<button
type="button"
onClick={(e) => {
e.stopPropagation();
onRemove();
}}
className={cn(
"rounded-full hover:bg-foreground/10 transition-colors shrink-0",
size === "sm" && "p-0.5",
size === "md" && "p-0.5",
size === "lg" && "p-1",
)}
aria-label={`Remover ${name}`}
>
<X
className={cn(
size === "sm" && "w-2.5 h-2.5",
size === "md" && "w-3 h-3",
size === "lg" && "w-3.5 h-3.5",
)}
/>
</button>
)}
</span>
);

// Default tooltip text (when not overridden)
const defaultTooltip = (
<>
{groupLabel && <div className="font-medium">{groupLabel}</div>}
<div>{name}</div>
{productCount !== undefined && (
<div className="text-xs opacity-80 mt-1">
{productCount} produto{productCount !== 1 ? "s" : ""}
</div>
)}
</>
);

// With tooltip
if (showTooltip && (groupLabel || productCount !== undefined)) {
return (
Comment on lines +171 to +173
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>{badgeContent}</TooltipTrigger>
<TooltipContent>{tooltipContent ?? defaultTooltip}</TooltipContent>
Comment on lines +174 to +177
</Tooltip>
</TooltipProvider>
);
}

return badgeContent;
}
2 changes: 2 additions & 0 deletions src/components/common/EntityBadge/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { EntityBadge } from "./EntityBadge";
export type { EntityBadgeProps } from "./EntityBadge";
168 changes: 22 additions & 146 deletions src/components/materials/MaterialBadge.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import React from "react";
import { cn } from "@/lib/utils";
import { X } from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";

import { EntityBadge } from "@/components/common/EntityBadge";

/**
* Badge for displaying a Material (Plástico, Metal, Tecido, etc).
*
* Thin wrapper around `EntityBadge` — see common/EntityBadge for the actual
* implementation. API kept identical for backwards compatibility with all
* existing callers.
*/
interface MaterialBadgeProps {
name: string;
groupName?: string;
Expand Down Expand Up @@ -35,143 +34,20 @@ export function MaterialBadge({
showTooltip = true,
productCount,
}: MaterialBadgeProps) {
const sizeClasses = {
sm: "text-[11px] px-2 py-0.5 gap-1",
md: "text-xs px-2.5 py-1 gap-1.5",
lg: "text-sm px-3 py-1.5 gap-2",
};

const colorDotSizes = {
sm: "w-2 h-2",
md: "w-2.5 h-2.5",
lg: "w-3 h-3",
};

const variantClasses = {
default: "bg-muted/60 text-muted-foreground hover:bg-muted",
outline: "border border-border bg-background text-foreground hover:bg-muted/50",
solid: "bg-primary/15 text-primary border border-primary/20 hover:bg-primary/25",
ghost: "bg-transparent text-muted-foreground hover:bg-muted/50",
};

const displayText = showGroup && groupName ? `${groupName}: ${name}` : name;

const badgeContent = (
<span
className={cn(
"inline-flex items-center rounded-full font-medium transition-all duration-200",
sizeClasses[size],
variantClasses[variant],
onClick && "cursor-pointer",
onRemove && "pr-1",
className
)}
onClick={onClick}
>

{/* Texto */}
<span className={cn(
"truncate",
size === "sm" && "max-w-[100px]",
size === "md" && "max-w-[120px]",
size === "lg" && "max-w-[150px]"
)}>
{displayText}
</span>

{/* Contador de produtos */}
{productCount !== undefined && productCount > 0 && (
<span className={cn(
"rounded-full bg-background/80 text-muted-foreground font-normal",
size === "sm" && "text-[9px] px-1",
size === "md" && "text-[10px] px-1.5",
size === "lg" && "text-xs px-2"
)}>
{productCount}
</span>
)}

{/* Botão remover */}
{onRemove && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onRemove();
}}
className={cn(
"rounded-full transition-all duration-150 hover:bg-destructive/20 hover:text-destructive",
size === "sm" && "p-0.5 ml-0.5",
size === "md" && "p-0.5 ml-1",
size === "lg" && "p-1 ml-1"
)}
>
<X className={cn(
size === "sm" && "w-2.5 h-2.5",
size === "md" && "w-3 h-3",
size === "lg" && "w-3.5 h-3.5"
)} />
</button>
)}
</span>
);

// Com tooltip
if (showTooltip && (groupName || productCount)) {
return (
<TooltipProvider delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
{badgeContent}
</TooltipTrigger>
<TooltipContent
side="top"
className="text-xs"
>
<div className="flex flex-col gap-0.5">
{groupName && (
<span className="font-medium">{groupName}</span>
)}
<span>{name}</span>
{productCount !== undefined && (
<span className="text-muted-foreground">
{productCount} produto{productCount !== 1 ? 's' : ''}
</span>
)}
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}

return badgeContent;
}

// Variante compacta para listas
export function CompactMaterialBadge({
name,
hexCode,
isSelected,
onClick,
}: {
name: string;
hexCode?: string | null;
isSelected?: boolean;
onClick?: () => void;
}) {
return (
<button
type="button"
<EntityBadge
name={name}
groupLabel={groupName}
hexCode={hexCode}
size={size}
variant={variant}
showGroup={showGroup}
groupSeparator=": "
onClick={onClick}
className={cn(
"inline-flex items-center gap-1.5 text-xs px-2 py-1 rounded-md transition-all duration-150",
isSelected
? "bg-primary/15 text-primary font-medium ring-1 ring-primary/30"
: "bg-muted/50 text-muted-foreground hover:bg-muted hover:text-foreground"
)}
>
<span className="truncate max-w-[80px]">{name}</span>
</button>
onRemove={onRemove}
className={className}
showTooltip={showTooltip}
productCount={productCount}
/>
);
}
Loading
Loading