Adjust button hover

This commit is contained in:
adilallo
2026-08-15 11:12:41 -06:00
parent 79b5166fa3
commit 79bdb97c69
19 changed files with 82 additions and 92 deletions
@@ -88,7 +88,7 @@ function CustomMethodCardWizardViewComponent({
/> />
) : null} ) : null}
{!fieldTypeModal && wizardStep === 3 ? ( {!fieldTypeModal && wizardStep === 3 ? (
<div className="flex w-full flex-col gap-4"> <div className="flex w-full flex-col gap-4 pt-1">
{draftFieldBlocks.length > 0 ? ( {draftFieldBlocks.length > 0 ? (
<CustomMethodCardWizardBlocksList <CustomMethodCardWizardBlocksList
blocks={draftFieldBlocks} blocks={draftFieldBlocks}
@@ -40,19 +40,27 @@ function CustomMethodCardWizardBlocksListViewComponent({
onDragEnd, onDragEnd,
}: CustomMethodCardWizardBlocksListViewProps) { }: CustomMethodCardWizardBlocksListViewProps) {
return ( return (
<ul className="flex list-none flex-col gap-2 p-0" aria-label={listLabel}> <ul
className="flex list-none flex-col gap-2 p-0 pt-1"
aria-label={listLabel}
>
{blocks.map((block, index) => { {blocks.map((block, index) => {
const kind = block.kind as AddCustomFieldType; const kind = block.kind as AddCustomFieldType;
const typeLabel = fieldTypeLabels[kind]; const typeLabel = fieldTypeLabels[kind];
const isOver = overIndex === index && draggingIndex !== index; const title = block.blockTitle.trim() || typeLabel;
const isDragging = draggingIndex === index;
const isDropTarget = overIndex === index && draggingIndex !== index;
return ( return (
<li <li
key={block.id} key={block.id}
className={`flex min-h-[52px] items-stretch gap-2 rounded-[var(--measures-radius-medium,8px)] border border-[var(--color-border-default-primary)] bg-[var(--color-surface-default-secondary)] pl-1 pr-3 py-2 transition-shadow ${ aria-grabbed={isDragging ? true : undefined}
isOver className={`group flex min-h-[52px] items-stretch gap-2 rounded-[var(--measures-radius-medium,8px)] border-2 bg-[var(--color-surface-default-secondary)] py-2 pl-1 pr-3 transition-[border-color,box-shadow,opacity] ${
? "ring-2 ring-[var(--color-border-invert-primary)] ring-offset-2 ring-offset-[var(--color-surface-default-primary)]" isDropTarget
: "" ? "relative z-10 border-[var(--color-border-invert-primary)]"
} ${draggingIndex === index ? "opacity-60" : ""}`} : isDragging
? "relative z-20 border-[var(--color-border-default-primary)] opacity-60 shadow-[0_4px_12px_rgba(0,0,0,0.12)]"
: "border-[var(--color-border-default-primary)] hover:border-[var(--color-content-default-secondary)]"
}`}
onDragOver={onDragOver(index)} onDragOver={onDragOver(index)}
onDrop={onDrop(index)} onDrop={onDrop(index)}
> >
@@ -61,8 +69,8 @@ function CustomMethodCardWizardBlocksListViewComponent({
draggable draggable
onDragStart={onDragStart(index)} onDragStart={onDragStart(index)}
onDragEnd={onDragEnd} onDragEnd={onDragEnd}
className="flex shrink-0 cursor-grab touch-manipulation items-center justify-center rounded-[var(--measures-radius-200,8px)] border-0 bg-transparent px-1 text-[var(--color-content-default-secondary)] active:cursor-grabbing focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-invert-primary)]"
aria-label={dragHandleAriaLabel} aria-label={dragHandleAriaLabel}
className="flex shrink-0 cursor-grab touch-manipulation items-center self-stretch rounded-[var(--measures-radius-200,8px)] border-0 bg-transparent px-1 text-[var(--color-content-default-secondary)] transition-colors active:cursor-grabbing focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--color-border-invert-primary)] group-hover:text-[var(--color-content-default-primary)]"
> >
<DragHandleGlyph /> <DragHandleGlyph />
</button> </button>
@@ -75,7 +83,7 @@ function CustomMethodCardWizardBlocksListViewComponent({
</span> </span>
<div className="flex min-w-0 flex-1 flex-col justify-center gap-0.5"> <div className="flex min-w-0 flex-1 flex-col justify-center gap-0.5">
<span className="truncate font-inter text-[14px] font-medium leading-[18px] text-[var(--color-content-default-primary)]"> <span className="truncate font-inter text-[14px] font-medium leading-[18px] text-[var(--color-content-default-primary)]">
{block.blockTitle.trim() || typeLabel} {title}
</span> </span>
<span className="font-inter text-[12px] leading-4 text-[var(--color-content-default-secondary)]"> <span className="font-inter text-[12px] leading-4 text-[var(--color-content-default-secondary)]">
{typeLabel} {typeLabel}
+2 -2
View File
@@ -38,8 +38,8 @@ const CASE_STUDY_TILE_RADIUS_CLASS = "rounded-[23.093px]";
const CASE_STUDY_LINK_CLASS = [ const CASE_STUDY_LINK_CLASS = [
CASE_STUDY_TILE_RADIUS_CLASS, CASE_STUDY_TILE_RADIUS_CLASS,
"block shrink-0 cursor-pointer outline-none transition-transform duration-200", "block shrink-0 cursor-pointer outline-none transition-[opacity,transform] duration-200",
"hover:scale-[1.02] hover:opacity-95", "hover:opacity-95",
"focus-visible:ring-2 focus-visible:ring-[var(--color-border-default-brand-primary)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-surface-default-primary)]", "focus-visible:ring-2 focus-visible:ring-[var(--color-border-default-brand-primary)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-surface-default-primary)]",
"active:scale-[0.98]", "active:scale-[0.98]",
].join(" "); ].join(" ");
+4 -4
View File
@@ -102,15 +102,15 @@ const Logo = memo<LogoProps>(
: "hidden"; : "hidden";
return ( return (
<Link href="/" className="block" aria-label={t("logoAlt")}> <Link href="/" className="group block" aria-label={t("logoAlt")}>
<div <div
className={`flex items-center ${config.containerHeight} ${ className={`flex items-center ${config.containerHeight} ${
wordmark ? config.gap : "" wordmark ? config.gap : ""
} transition-all duration-200 ease-in-out hover:scale-[1.02] cursor-pointer`} } cursor-pointer`}
> >
{/* Logo Text - responsive visibility for topNav sizes */} {/* Logo Text - responsive visibility for topNav sizes */}
<div <div
className={`font-bricolage-grotesque ${textColorClass} ${config.textSize} ${config.lineHeight} font-normal tracking-[0px] transition-colors duration-200 ${wordmarkVisibilityClass}`} className={`font-bricolage-grotesque ${textColorClass} ${config.textSize} ${config.lineHeight} font-normal tracking-[0px] transition-opacity duration-200 ease-out group-hover:opacity-80 ${wordmarkVisibilityClass}`}
aria-label={t("logoText")} aria-label={t("logoText")}
> >
{t("logoText")} {t("logoText")}
@@ -123,7 +123,7 @@ const Logo = memo<LogoProps>(
alt={t("logoAlt")} alt={t("logoAlt")}
width={27.05} width={27.05}
height={27.05} height={27.05}
className={`flex-shrink-0 ${config.iconSize} transition-all duration-200 ${ className={`flex-shrink-0 ${config.iconSize} origin-center transition-transform duration-200 ease-out group-hover:scale-110 ${
isInverse ? "brightness-0" : "" isInverse ? "brightness-0" : ""
}`} }`}
aria-hidden="true" aria-hidden="true"
+14 -33
View File
@@ -115,48 +115,29 @@ const Button = memo<ButtonProps>(
const variantStyles: Record<string, string> = { const variantStyles: Record<string, string> = {
filled: filled:
"bg-[var(--color-surface-inverse-primary)] text-[var(--color-content-inverse-primary)] border-[1.5px] border-transparent hover:bg-[var(--color-surface-inverse-primary)] hover:text-[var(--color-content-invert-brand-primary)] hover:border-[var(--color-border-invert-brand-primary)] hover:scale-[1.02] focus-visible:bg-[var(--color-surface-inverse-primary)] focus-visible:text-[var(--color-content-invert-brand-primary)] focus-visible:outline-none focus-visible:border-transparent focus-visible:shadow-[0_0_0px_2px_var(--color-border-default-primary),0_0_0px_4px_var(--color-border-invert-primary)] focus-visible:scale-[1.02] active:bg-[var(--color-surface-invert-brand-primary)] active:text-[var(--color-content-invert-primary)] active:border-[var(--color-border-invert-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-invert-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100", "bg-[var(--color-surface-inverse-primary)] text-[var(--color-content-inverse-primary)] border-[1.5px] border-transparent hover:bg-[var(--color-surface-inverse-primary)] hover:text-[var(--color-content-invert-brand-primary)] hover:border-[var(--color-border-invert-brand-primary)] focus-visible:bg-[var(--color-surface-inverse-primary)] focus-visible:text-[var(--color-content-invert-brand-primary)] focus-visible:outline-none focus-visible:border-transparent focus-visible:shadow-[0_0_0px_2px_var(--color-border-default-primary),0_0_0px_4px_var(--color-border-invert-primary)] active:bg-[var(--color-surface-invert-brand-primary)] active:text-[var(--color-content-invert-primary)] active:border-[var(--color-border-invert-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-invert-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:active:scale-100",
"filled-inverse": "filled-inverse":
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border-[1.5px] border-transparent hover:bg-[var(--color-surface-default-primary)] hover:text-[var(--color-content-default-brand-primary)] hover:border-[var(--color-border-default-brand-primary)] hover:scale-[1.02] focus-visible:bg-[var(--color-surface-default-primary)] focus-visible:text-[var(--color-content-default-brand-primary)] focus-visible:outline-none focus-visible:border-transparent focus-visible:shadow-[0_0_0px_2px_var(--color-border-invert-primary),0_0_0px_4px_var(--color-border-default-primary)] focus-visible:scale-[1.02] active:bg-[var(--color-surface-default-brand-primary)] active:text-[var(--color-content-default-primary)] active:border-[var(--color-border-default-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-invert-secondary)] disabled:text-[var(--color-content-default-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100", "bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border-[1.5px] border-transparent hover:bg-[var(--color-surface-default-primary)] hover:text-[var(--color-content-default-brand-primary)] hover:border-[var(--color-border-default-brand-primary)] focus-visible:bg-[var(--color-surface-default-primary)] focus-visible:text-[var(--color-content-default-brand-primary)] focus-visible:outline-none focus-visible:border-transparent focus-visible:shadow-[0_0_0px_2px_var(--color-border-invert-primary),0_0_0px_4px_var(--color-border-default-primary)] active:bg-[var(--color-surface-default-brand-primary)] active:text-[var(--color-content-default-primary)] active:border-[var(--color-border-default-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-invert-secondary)] disabled:text-[var(--color-content-default-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:active:scale-100",
outline: outline:
"bg-transparent text-[var(--color-content-default-primary)] border-[1.5px] border-[var(--color-border-invert-primary)] hover:bg-transparent hover:text-[var(--color-content-default-brand-primary)] hover:border-[1.5px] hover:border-[var(--color-border-default-brand-primary)] hover:scale-[1.02] focus-visible:bg-transparent focus-visible:text-[var(--color-content-default-primary)] focus-visible:outline-none focus-visible:border-[1.5px] focus-visible:border-[var(--color-border-invert-primary)] focus-visible:shadow-[0_0_0px_2px_var(--color-border-default-primary),0_0_0px_4px_var(--color-border-invert-primary)] focus-visible:scale-[1.02] active:bg-transparent active:text-[var(--color-content-default-primary)] active:border-[1.5px] active:border-[var(--color-border-default-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-invert-tertiary)] disabled:border-[1.5px] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100", "bg-transparent text-[var(--color-content-default-primary)] border-[1.5px] border-[var(--color-border-invert-primary)] hover:bg-transparent hover:text-[var(--color-content-default-brand-primary)] hover:border-[1.5px] hover:border-[var(--color-border-default-brand-primary)] focus-visible:bg-transparent focus-visible:text-[var(--color-content-default-primary)] focus-visible:outline-none focus-visible:border-[1.5px] focus-visible:border-[var(--color-border-invert-primary)] focus-visible:shadow-[0_0_0px_2px_var(--color-border-default-primary),0_0_0px_4px_var(--color-border-invert-primary)] active:bg-transparent active:text-[var(--color-content-default-primary)] active:border-[1.5px] active:border-[var(--color-border-default-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-invert-tertiary)] disabled:border-[1.5px] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:active:scale-100",
"outline-inverse": "outline-inverse":
"bg-transparent text-[var(--color-content-invert-primary)] border-[1.5px] border-[var(--color-border-default-primary)] hover:bg-transparent hover:text-[var(--color-content-invert-brand-primary)] hover:border-[1.5px] hover:border-[var(--color-border-invert-brand-primary)] hover:scale-[1.02] focus-visible:bg-transparent focus-visible:text-[var(--color-content-invert-primary)] focus-visible:outline-none focus-visible:border-[1.5px] focus-visible:border-[var(--color-border-default-primary)] focus-visible:shadow-[0_0_0px_2px_var(--color-border-invert-primary),0_0_0px_4px_var(--color-border-default-primary)] focus-visible:scale-[1.02] active:bg-transparent active:text-[var(--color-content-invert-primary)] active:border-[1.5px] active:border-[var(--color-border-invert-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-invert-secondary)] disabled:text-[var(--color-content-default-tertiary)] disabled:border-[1.5px] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100", "bg-transparent text-[var(--color-content-invert-primary)] border-[1.5px] border-[var(--color-border-default-primary)] hover:bg-transparent hover:text-[var(--color-content-invert-brand-primary)] hover:border-[1.5px] hover:border-[var(--color-border-invert-brand-primary)] focus-visible:bg-transparent focus-visible:text-[var(--color-content-invert-primary)] focus-visible:outline-none focus-visible:border-[1.5px] focus-visible:border-[var(--color-border-default-primary)] focus-visible:shadow-[0_0_0px_2px_var(--color-border-invert-primary),0_0_0px_4px_var(--color-border-default-primary)] active:bg-transparent active:text-[var(--color-content-invert-primary)] active:border-[1.5px] active:border-[var(--color-border-invert-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-invert-secondary)] disabled:text-[var(--color-content-default-tertiary)] disabled:border-[1.5px] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:active:scale-100",
ghost: ghost:
"bg-transparent text-[var(--color-content-default-brand-primary)] border-[1.5px] border-transparent hover:bg-transparent hover:text-[var(--color-content-default-primary)] hover:border-transparent hover:scale-[1.02] focus-visible:bg-transparent focus-visible:text-[var(--color-content-default-brand-primary)] focus-visible:outline-none focus-visible:border-transparent focus-visible:shadow-[0_0_0px_2px_var(--color-border-default-primary),0_0_0px_4px_var(--color-border-invert-primary)] focus-visible:scale-[1.02] active:bg-transparent active:text-[var(--color-content-default-primary)] active:border-[var(--color-border-default-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-invert-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100", "bg-transparent text-[var(--color-content-default-brand-primary)] border-[1.5px] border-transparent hover:bg-transparent hover:text-[var(--color-content-default-primary)] hover:border-transparent focus-visible:bg-transparent focus-visible:text-[var(--color-content-default-brand-primary)] focus-visible:outline-none focus-visible:border-transparent focus-visible:shadow-[0_0_0px_2px_var(--color-border-default-primary),0_0_0px_4px_var(--color-border-invert-primary)] active:bg-transparent active:text-[var(--color-content-default-primary)] active:border-[var(--color-border-default-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-invert-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:active:scale-100",
"ghost-inverse": "ghost-inverse":
"bg-transparent text-[var(--color-content-invert-primary)] border-[1.5px] border-transparent hover:bg-transparent hover:text-[var(--color-content-invert-primary)] hover:border-transparent hover:scale-[1.02] focus-visible:bg-transparent focus-visible:text-[var(--color-content-invert-brand-primary)] focus-visible:outline-none focus-visible:border-transparent focus-visible:shadow-[0_0_0px_2px_var(--color-border-invert-primary),0_0_0px_4px_var(--color-border-default-primary)] focus-visible:scale-[1.02] active:bg-[var(--color-surface-invert-brand-primary)] active:text-[var(--color-content-invert-primary)] active:border-[var(--color-border-invert-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-invert-secondary)] disabled:text-[var(--color-content-default-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100", "bg-transparent text-[var(--color-content-invert-primary)] border-[1.5px] border-transparent hover:bg-transparent hover:text-[var(--color-content-invert-primary)] hover:border-transparent focus-visible:bg-transparent focus-visible:text-[var(--color-content-invert-brand-primary)] focus-visible:outline-none focus-visible:border-transparent focus-visible:shadow-[0_0_0px_2px_var(--color-border-invert-primary),0_0_0px_4px_var(--color-border-default-primary)] active:bg-[var(--color-surface-invert-brand-primary)] active:text-[var(--color-content-invert-primary)] active:border-[var(--color-border-invert-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-invert-secondary)] disabled:text-[var(--color-content-default-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:active:scale-100",
danger: danger:
"bg-transparent text-[var(--color-border-default-negative-primary)] border border-[var(--color-border-default-negative-primary)] hover:bg-[var(--color-surface-invert-negative-secondary)] hover:text-[var(--color-border-default-negative-primary)] hover:border-[var(--color-border-default-negative-primary)] hover:scale-[1.02] focus-visible:bg-transparent focus-visible:text-[var(--color-border-default-negative-primary)] focus-visible:outline-none focus-visible:border-[var(--color-border-default-negative-primary)] focus-visible:shadow-[0_0_0px_2px_var(--color-border-default-primary),0_0_0px_4px_var(--color-border-invert-primary)] focus-visible:scale-[1.02] active:bg-[var(--color-surface-invert-negative-primary)] active:text-[var(--color-content-invert-negative-primary)] active:border-[1.5px] active:border-[var(--color-border-default-negative-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-inverse-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100", "bg-transparent text-[var(--color-border-default-negative-primary)] border border-[var(--color-border-default-negative-primary)] hover:bg-[var(--color-surface-invert-negative-secondary)] hover:text-[var(--color-border-default-negative-primary)] hover:border-[var(--color-border-default-negative-primary)] focus-visible:bg-transparent focus-visible:text-[var(--color-border-default-negative-primary)] focus-visible:outline-none focus-visible:border-[var(--color-border-default-negative-primary)] focus-visible:shadow-[0_0_0px_2px_var(--color-border-default-primary),0_0_0px_4px_var(--color-border-invert-primary)] active:bg-[var(--color-surface-invert-negative-primary)] active:text-[var(--color-content-invert-negative-primary)] active:border-[1.5px] active:border-[var(--color-border-default-negative-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-inverse-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:active:scale-100",
"danger-inverse": "danger-inverse":
"bg-transparent text-[var(--color-content-invert-negative-primary)] border border-[var(--color-border-invert-negative-primary)] hover:bg-transparent hover:text-[var(--color-content-invert-negative-primary)] hover:border-[var(--color-border-invert-negative-primary)] hover:scale-[1.02] focus-visible:bg-transparent focus-visible:text-[var(--color-content-invert-negative-primary)] focus-visible:outline-none focus-visible:border-[var(--color-border-invert-negative-primary)] focus-visible:shadow-[0_0_0px_2px_var(--color-border-invert-primary),0_0_0px_4px_var(--color-border-default-primary)] focus-visible:scale-[1.02] active:bg-[var(--color-surface-default-negative-primary)] active:text-[var(--color-content-default-primary)] active:border-[1.5px] active:border-[var(--color-border-default-negative-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-inverse-secondary)] disabled:text-[var(--color-content-default-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100", "bg-transparent text-[var(--color-content-invert-negative-primary)] border border-[var(--color-border-invert-negative-primary)] hover:bg-transparent hover:text-[var(--color-content-invert-negative-primary)] hover:border-[var(--color-border-invert-negative-primary)] focus-visible:bg-transparent focus-visible:text-[var(--color-content-invert-negative-primary)] focus-visible:outline-none focus-visible:border-[var(--color-border-invert-negative-primary)] focus-visible:shadow-[0_0_0px_2px_var(--color-border-invert-primary),0_0_0px_4px_var(--color-border-default-primary)] active:bg-[var(--color-surface-default-negative-primary)] active:text-[var(--color-content-default-primary)] active:border-[1.5px] active:border-[var(--color-border-default-negative-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-inverse-secondary)] disabled:text-[var(--color-content-default-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:active:scale-100",
}; };
const hoverOutlineStyles: Record<string, string> = { // Hover/focus: color + border only — no scale and no extra outline (those
xsmall: "hover:outline-1", // enlarge the label). Press: active:scale-[0.98]. Same rule for nav items
small: "hover:outline-1", // and cards: color, opacity, or shadow on hover; scale only on press.
medium: "hover:outline-1", // State prop is informational for Figma alignment; CSS pseudo-classes own
large: "hover:outline-2", // the actual interaction states.
xlarge: "hover:outline-[2.5px]", const baseStyles = `inline-flex items-center justify-start box-border whitespace-nowrap shrink-0 touch-manipulation [-webkit-tap-highlight-color:transparent] ${sizeStyles[size]} rounded-[var(--radius-measures-radius-full)] ${fontStyles[size]} transition-[transform,color,background-color,border-color,box-shadow] duration-150 ease-out cursor-pointer ${variantStyles[variant]}`;
};
// Only apply outline styles to filled variant, not filled-inverse, outline, outline-inverse, ghost, ghost-inverse, danger, or danger-inverse
const outlineStyles =
variant === "filled-inverse" ||
variant === "outline" ||
variant === "outline-inverse" ||
variant === "ghost" ||
variant === "ghost-inverse" ||
variant === "danger" ||
variant === "danger-inverse"
? ""
: hoverOutlineStyles[size];
// Apply state-based styles if state prop is provided (overrides default hover/focus/active)
// Note: State prop is informational for Figma alignment - actual state is handled by CSS pseudo-classes
// For now, we maintain existing behavior and state prop is for documentation/alignment purposes
const baseStyles = `inline-flex items-center justify-start box-border whitespace-nowrap shrink-0 touch-manipulation [-webkit-tap-highlight-color:transparent] ${sizeStyles[size]} rounded-[var(--radius-measures-radius-full)] ${fontStyles[size]} transition-[transform,color,background-color,border-color,box-shadow,outline-color] duration-150 ease-out cursor-pointer ${variantStyles[variant]} ${outlineStyles}`;
const combinedStyles = `${baseStyles} ${className}`; const combinedStyles = `${baseStyles} ${className}`;
const sharedA11y = { const sharedA11y = {
+1 -1
View File
@@ -13,7 +13,7 @@ export function IconView({
onKeyDown, onKeyDown,
}: IconViewProps) { }: IconViewProps) {
const interactionClass = interactive const interactionClass = interactive
? "cursor-pointer transition-all duration-200 hover:scale-[1.02] hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-content-default-brand-primary)] focus:ring-offset-2" ? "cursor-pointer transition-shadow duration-200 hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-content-default-brand-primary)] focus:ring-offset-2"
: "cursor-default"; : "cursor-default";
return ( return (
+2 -2
View File
@@ -51,7 +51,7 @@ const MiniContainer = memo<MiniProps>(
...baseProps, ...baseProps,
href, href,
className: className:
"block focus:outline-none focus:ring-2 focus:ring-[var(--color-surface-default-brand-royal)] focus:ring-offset-2 rounded-[var(--radius-measures-radius-xlarge)] transition-all duration-200 hover:scale-[1.02]", "block focus:outline-none focus:ring-2 focus:ring-[var(--color-surface-default-brand-royal)] focus:ring-offset-2 rounded-[var(--radius-measures-radius-xlarge)]",
tabIndex: 0, tabIndex: 0,
}, },
}; };
@@ -64,7 +64,7 @@ const MiniContainer = memo<MiniProps>(
...baseProps, ...baseProps,
onClick, onClick,
className: className:
"block w-full text-left focus:outline-none focus:ring-2 focus:ring-[var(--color-surface-default-brand-royal)] focus:ring-offset-2 rounded-[var(--radius-measures-radius-xlarge)] transition-all duration-200 hover:scale-[1.02]", "block w-full text-left focus:outline-none focus:ring-2 focus:ring-[var(--color-surface-default-brand-royal)] focus:ring-offset-2 rounded-[var(--radius-measures-radius-xlarge)]",
tabIndex: 0, tabIndex: 0,
onKeyDown: (e: React.KeyboardEvent<HTMLButtonElement>) => { onKeyDown: (e: React.KeyboardEvent<HTMLButtonElement>) => {
if (e.key === "Enter" || e.key === " ") { if (e.key === "Enter" || e.key === " ") {
+1 -1
View File
@@ -31,7 +31,7 @@ function MiniView({
const panelClass = featureGridShell const panelClass = featureGridShell
? `h-[138px] shrink-0 rounded-[var(--measures-radius-400,16px)] px-[24px] py-[32px] ${backgroundColor} flex items-center justify-center` ? `h-[138px] shrink-0 rounded-[var(--measures-radius-400,16px)] px-[24px] py-[32px] ${backgroundColor} flex items-center justify-center`
: `flex-1 rounded-[var(--radius-measures-radius-xlarge)] border border-[1px] py-[var(--spacing-scale-032)] px-[var(--spacing-scale-024)] ${backgroundColor} flex items-center justify-center transition-all duration-200 hover:scale-[1.02] hover:shadow-lg`; : `flex-1 rounded-[var(--radius-measures-radius-xlarge)] border border-[1px] py-[var(--spacing-scale-032)] px-[var(--spacing-scale-024)] ${backgroundColor} flex items-center justify-center transition-shadow duration-200 hover:shadow-lg`;
const imageClass = featureGridShell const imageClass = featureGridShell
? `max-h-[48px] max-w-[56px] w-auto h-auto object-contain${panelImageClassName ? ` ${panelImageClassName}` : ""}` ? `max-h-[48px] max-w-[56px] w-auto h-auto object-contain${panelImageClassName ? ` ${panelImageClassName}` : ""}`
@@ -17,7 +17,7 @@ function ContentThumbnailTemplateView({
return ( return (
<Link <Link
href={`/blog/${post.slug}`} href={`/blog/${post.slug}`}
className={`group block w-full transition-transform duration-200 hover:scale-[1.02] ${className}`} className={`group block w-full ${className}`}
> >
<div className="relative aspect-[320/225.5] w-full overflow-hidden smd:aspect-[260/390]"> <div className="relative aspect-[320/225.5] w-full overflow-hidden smd:aspect-[260/390]">
<div className="absolute inset-0 z-0"> <div className="absolute inset-0 z-0">
@@ -49,7 +49,7 @@ function ContentThumbnailTemplateView({
return ( return (
<Link <Link
href={`/blog/${post.slug}`} href={`/blog/${post.slug}`}
className={`group block transition-transform duration-200 hover:scale-[1.02] ${className}`} className={`group block ${className}`}
> >
<div className="relative h-[390px] w-[260px] overflow-hidden"> <div className="relative h-[390px] w-[260px] overflow-hidden">
<div className="absolute inset-0 z-0"> <div className="absolute inset-0 z-0">
@@ -71,7 +71,7 @@ function ContentThumbnailTemplateView({
return ( return (
<Link <Link
href={`/blog/${post.slug}`} href={`/blog/${post.slug}`}
className={`group block w-full transition-transform duration-200 hover:scale-[1.02] ${className}`} className={`group block w-full ${className}`}
> >
<div className="relative aspect-[260/390] w-full overflow-hidden"> <div className="relative aspect-[260/390] w-full overflow-hidden">
<div className="absolute inset-0 z-0"> <div className="absolute inset-0 z-0">
@@ -92,10 +92,7 @@ function ContentThumbnailTemplateView({
if (sizing === "fixed") { if (sizing === "fixed") {
return ( return (
<Link <Link href={`/blog/${post.slug}`} className={`group block ${className}`}>
href={`/blog/${post.slug}`}
className={`group block transition-transform duration-200 hover:scale-[1.02] ${className}`}
>
<div className="relative h-[225.5px] w-[320px] overflow-hidden"> <div className="relative h-[225.5px] w-[320px] overflow-hidden">
<div className="absolute inset-0 z-0"> <div className="absolute inset-0 z-0">
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
@@ -116,7 +113,7 @@ function ContentThumbnailTemplateView({
return ( return (
<Link <Link
href={`/blog/${post.slug}`} href={`/blog/${post.slug}`}
className={`group block w-full transition-transform duration-200 hover:scale-[1.02] ${className}`} className={`group block w-full ${className}`}
> >
<div className="relative aspect-[320/225.5] w-full overflow-hidden"> <div className="relative aspect-[320/225.5] w-full overflow-hidden">
<div className="absolute inset-0 z-0"> <div className="absolute inset-0 z-0">
+2 -2
View File
@@ -109,7 +109,7 @@ function ChipView({
focus-visible:ring-[var(--color-border-default-primary,#141414)] focus-visible:ring-[var(--color-border-default-primary,#141414)]
focus-visible:ring-offset-2 focus-visible:ring-offset-2
focus-visible:ring-offset-transparent focus-visible:ring-offset-transparent
transition-[background,border-color,color,box-shadow,transform] transition-[background,border-color,color,box-shadow]
duration-200 duration-200
ease-in-out ease-in-out
` `
@@ -118,7 +118,7 @@ function ChipView({
const stateClasses = isDisabled const stateClasses = isDisabled
? "cursor-not-allowed opacity-60" ? "cursor-not-allowed opacity-60"
: "cursor-pointer hover:scale-[1.02]"; : "cursor-pointer";
const combinedClasses = [ const combinedClasses = [
baseClasses, baseClasses,
@@ -4,7 +4,7 @@ import { memo } from "react";
import type { IncrementerViewProps } from "./Incrementer.types"; import type { IncrementerViewProps } from "./Incrementer.types";
const STEP_BUTTON_CLASSES = const STEP_BUTTON_CLASSES =
"bg-[var(--color-surface-default-secondary,#141414)] text-[var(--color-content-default-primary,#fff)] inline-flex shrink-0 items-center justify-center overflow-clip rounded-[var(--measures-radius-full,9999px)] px-[var(--space-200,8px)] py-[var(--measures-spacing-150,6px)] transition-[background,color,transform] duration-200 ease-in-out hover:scale-[1.02] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-invert-primary,#fff)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-surface-default-primary,#000)] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100"; "bg-[var(--color-surface-default-secondary,#141414)] text-[var(--color-content-default-primary,#fff)] inline-flex shrink-0 items-center justify-center overflow-clip rounded-[var(--measures-radius-full,9999px)] px-[var(--space-200,8px)] py-[var(--measures-spacing-150,6px)] transition-[background,color,opacity] duration-200 ease-in-out hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-invert-primary,#fff)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-surface-default-primary,#000)] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:opacity-50";
function MinusIcon() { function MinusIcon() {
return ( return (
@@ -90,7 +90,7 @@ const MenuItemContainer = memo<MenuItemProps>(
// Interactive styles // Interactive styles
const interactiveStyles = const interactiveStyles =
"hover:scale-[1.02] active:scale-[0.98] focus:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 disabled:active:scale-100"; "active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed disabled:active:scale-100";
// Disabled styles // Disabled styles
const disabledStyles = disabled const disabledStyles = disabled
@@ -23,16 +23,14 @@ function LogoWallView({
{displayLogos.map((logo, index) => ( {displayLogos.map((logo, index) => (
<div <div
key={index} key={index}
className={`flex items-center justify-center transition-opacity duration-500 hover:opacity-100 ${ className={`flex items-center justify-center ${
logo.order || "" logo.order || ""
}`} }`}
> >
<Image <Image
src={logo.src} src={logo.src}
alt={logo.alt} alt={logo.alt}
className={`${ className={`${logo.size || "h-8"} w-auto object-contain`}
logo.size || "h-8"
} w-auto object-contain transition-transform duration-500 hover:scale-105`}
priority={index < 2} // Prioritize first 2 logos for above-the-fold loading priority={index < 2} // Prioritize first 2 logos for above-the-fold loading
unoptimized // Skip optimization for local images unoptimized // Skip optimization for local images
width={0} width={0}
+1
View File
@@ -30,6 +30,7 @@ Quick map from the Figma file **Community Rule System** (`agv0VBLiBlcnSAaiAORgPR
## Button conventions (Figma “Button” canvas) ## Button conventions (Figma “Button” canvas)
- **`buttons/Button/`** — **`Button.tsx`** + **`index.tsx`**. Variants (**filled**, **outline**, **ghost**, **danger**) and **`palette`** / **`size`** align with **`lib/propNormalization`**. **`href`** renders an `<a>` when not disabled. - **`buttons/Button/`** — **`Button.tsx`** + **`index.tsx`**. Variants (**filled**, **outline**, **ghost**, **danger**) and **`palette`** / **`size`** align with **`lib/propNormalization`**. **`href`** renders an `<a>` when not disabled.
- **Interaction:** hover and focus change color/border (or opacity/shadow on cards) — they must not scale the control, because that enlarges the label. Reserve `active:scale-[0.98]` for press. Same rule for nav items (`MenuItem`) and cards (`Mini`, `Icon`, article thumbnails).
- **`buttons/InlineTextButton/`** — tertiary inline **`<button>`** (underline, inherited typography) for in-paragraph controls—**not** primary actions. - **`buttons/InlineTextButton/`** — tertiary inline **`<button>`** (underline, inherited typography) for in-paragraph controls—**not** primary actions.
## Control conventions (Figma [“Control”](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=5944-58611) canvas) ## Control conventions (Figma [“Control”](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=5944-58611) canvas)
+7 -4
View File
@@ -225,10 +225,13 @@ maintenance window — after the final Cloudron backup (§5 phase 3 step
([CR-101](https://linear.app/community-rule/issue/CR-101/backend-decommission-legacy-communityrule-lamp-app)) ([CR-101](https://linear.app/community-rule/issue/CR-101/backend-decommission-legacy-communityrule-lamp-app))
for operator recovery if manual lookup from the export is ever needed. for operator recovery if manual lookup from the export is ever needed.
**User discoverability:** link to the archive repo (or a release **No app-side surface.** The archive lives in the repo only. It exists
download) from the new app — footer, help page, or a static for posterity and operator lookup, not end-user self-service — there is
`/legacy-archive` page — so users looking for pre-cutover rules can no footer link, help-page entry, or `/legacy-archive` page in the new
find it without knowing Gitea exists. app, and none is required to close
[CR-102](https://linear.app/community-rule/issue/CR-102). Operators
answering a "where did my rule go?" question point at the Gitea repo
directly.
## 7. Old vs new deltas ## 7. Old vs new deltas
+1 -5
View File
@@ -20,10 +20,6 @@ export default {
## Animations & Transitions ## Animations & Transitions
- **Fade-in Effect**: Logos fade in from opacity 0 to 60% after component mounts (500ms transition) - **Fade-in Effect**: Logos fade in from opacity 0 to 60% after component mounts (500ms transition)
- **Hover Interactions**: Individual logos respond to hover with:
- Opacity change from 60% to 100%
- Scale transform (105% zoom)
- 500ms smooth transitions for all effects
- **Loading States**: Progressive loading with fallback timer for reliable display - **Loading States**: Progressive loading with fallback timer for reliable display
## Props ## Props
@@ -74,7 +70,7 @@ export const Default = {
docs: { docs: {
description: { description: {
story: story:
"Default LogoWall with all partner logos. Displays in a 3×2 grid on mobile, 2×3 grid on small screens, single row on medium screens, and larger sizes on large screens. Features smooth fade-in animations and hover interactions.", "Default LogoWall with all partner logos. Displays in a 3×2 grid on mobile, 2×3 grid on small screens, single row on medium screens, and larger sizes on large screens. Features a fade-in on mount.",
}, },
}, },
}, },
+3
View File
@@ -61,6 +61,9 @@ describe("Button (behavioral tests)", () => {
expect(button.className).toContain("duration-150"); expect(button.className).toContain("duration-150");
expect(button.className).not.toContain("duration-500"); expect(button.className).not.toContain("duration-500");
expect(button.className).toContain("active:scale-[0.98]"); expect(button.className).toContain("active:scale-[0.98]");
expect(button.className).not.toContain("hover:scale-");
expect(button.className).not.toContain("focus-visible:scale-");
expect(button.className).not.toContain("hover:outline-");
}); });
it("renders as a link when href is provided", () => { it("renders as a link when href is provided", () => {
+10
View File
@@ -36,6 +36,16 @@ describe("Logo (behavioral tests)", () => {
expect(logo).toHaveAttribute("aria-label", "CommunityRule Logo"); expect(logo).toHaveAttribute("aria-label", "CommunityRule Logo");
}); });
it("animates the mark on hover without scaling the wordmark", () => {
render(<Logo />);
const logo = screen.getByRole("link", { name: /communityrule logo/i });
const wordmark = screen.getByText("CommunityRule");
const mark = logo.querySelector("img");
expect(logo).toHaveClass("group");
expect(wordmark.className).not.toContain("scale-");
expect(mark).toHaveClass("group-hover:scale-110");
});
it("renders logo icon", () => { it("renders logo icon", () => {
render(<Logo />); render(<Logo />);
expect(screen.getByAltText("CommunityRule Logo")).toBeInTheDocument(); expect(screen.getByAltText("CommunityRule Logo")).toBeInTheDocument();
+7 -14
View File
@@ -92,13 +92,15 @@ describe("LogoWall Component", () => {
expect(screen.getByAltText("Food Not Bombs")).toBeInTheDocument(); expect(screen.getByAltText("Food Not Bombs")).toBeInTheDocument();
}); });
test("applies hover effects", () => { test("does not apply hover effects on partner logos", () => {
render(<LogoWall />); render(<LogoWall />);
const logoContainers = document.querySelectorAll( expect(
'[class*="hover:opacity-100"]', document.querySelectorAll('[class*="hover:opacity"]').length,
); ).toBe(0);
expect(logoContainers.length).toBeGreaterThan(0); expect(
document.querySelectorAll('[class*="hover:scale"]').length,
).toBe(0);
}); });
test("renders with proper semantic structure", () => { test("renders with proper semantic structure", () => {
@@ -142,13 +144,4 @@ describe("LogoWall Component", () => {
expect(foodNotBombsLogo).toBeInTheDocument(); expect(foodNotBombsLogo).toBeInTheDocument();
expect(startCoopLogo).toBeInTheDocument(); expect(startCoopLogo).toBeInTheDocument();
}); });
test("applies scale effect on hover", () => {
render(<LogoWall />);
const logos = screen.getAllByRole("img");
logos.forEach((logo) => {
expect(logo).toHaveClass("hover:scale-105");
});
});
}); });