diff --git a/app/components-preview/page.tsx b/app/components-preview/page.tsx index f7ba62f..7b23a5e 100644 --- a/app/components-preview/page.tsx +++ b/app/components-preview/page.tsx @@ -474,7 +474,7 @@ export default function ComponentsPreview() {

Default palette -

+
{/* Inverse palette - on white background */} -
+

Inverse palette (on white background)

@@ -643,8 +643,8 @@ export default function ComponentsPreview() { logoUrl="http://localhost:3845/assets/d2513a6ab56f2b2927e8a7c442c06326e7a29541.png" logoAlt="Mutual Aid Mondays" onClick={() => console.log("Card clicked: Mutual Aid Mondays")} - /> -
+ /> +
{/* Collapsed State - Medium */} @@ -663,16 +663,16 @@ export default function ComponentsPreview() { logoUrl="http://localhost:3845/assets/d2513a6ab56f2b2927e8a7c442c06326e7a29541.png" logoAlt="Mutual Aid Mondays" onClick={() => console.log("Card clicked: Mutual Aid Mondays")} - /> + />
{/* Expanded State - Large */} -
-

+
+

Expanded State - Large (L) -

-
+

+
console.log("Card clicked: Mutual Aid Mondays")} - /> -
+ /> +
{/* Expanded State - Medium */} @@ -706,14 +706,14 @@ export default function ComponentsPreview() { categories={ruleCardCategories} onClick={() => console.log("Card clicked: Mutual Aid Mondays")} /> - - + + {/* Different Background Colors */} -
-

+
+

Different Background Colors -

+

} onClick={() => console.log("Consensus selected")} - /> -
-
+ /> + +
{/* Logo Fallback */} @@ -769,8 +769,8 @@ export default function ComponentsPreview() { className="w-[525px]" communityInitials="CE" onClick={() => console.log("Community Example selected")} - /> - + /> + {/* MultiSelect Component */} diff --git a/app/components/RuleCard/RuleCard.types.ts b/app/components/RuleCard/RuleCard.types.ts index f7bda7f..3ebe9ce 100644 --- a/app/components/RuleCard/RuleCard.types.ts +++ b/app/components/RuleCard/RuleCard.types.ts @@ -17,7 +17,7 @@ export interface RuleCardProps { className?: string; onClick?: () => void; expanded?: boolean; - size?: "L" | "M" | "l" | "m"; + size?: "XS" | "S" | "M" | "L" | "xs" | "s" | "m" | "l"; categories?: Category[]; logoUrl?: string; logoAlt?: string; @@ -33,7 +33,7 @@ export interface RuleCardViewProps { onClick: () => void; onKeyDown: (_event: React.KeyboardEvent) => void; expanded: boolean; - size: "L" | "M"; + size: "XS" | "S" | "M" | "L"; categories?: Category[]; logoUrl?: string; logoAlt?: string; diff --git a/app/components/RuleCard/RuleCard.view.tsx b/app/components/RuleCard/RuleCard.view.tsx index ce7689b..4e637e9 100644 --- a/app/components/RuleCard/RuleCard.view.tsx +++ b/app/components/RuleCard/RuleCard.view.tsx @@ -25,33 +25,67 @@ export function RuleCardView({ // Size-based styling const isLarge = size === "L"; + const isMedium = size === "M"; + const isSmall = size === "S"; + const isExtraSmall = size === "XS"; // Card dimensions - fixed width for expanded states (568px for L, 398px for M per Figma) - const cardPadding = isLarge ? "p-[24px]" : "p-[16px]"; + // XS and S don't have fixed widths when expanded + const cardPadding = isLarge || isSmall + ? "p-[24px]" + : isMedium + ? "p-[16px]" + : "pb-[24px] pt-[12px] px-[12px]"; // XS: asymmetric padding const cardGap = expanded ? "gap-[16px]" - : isLarge ? "gap-[10px]" : "gap-[12px]"; + : isLarge + ? "gap-[10px]" + : isMedium + ? "gap-[12px]" + : "gap-[18px]"; // XS and S: 18px gap const cardWidth = expanded ? isLarge ? "w-[568px]" - : "w-[398px]" + : isMedium + ? "w-[398px]" + : "" // XS and S: no fixed width : ""; // Logo/Icon dimensions - const logoSize = isLarge ? 103 : 56; + // For S: 80px container with 12px padding = 56px icon area + // For XS: 40px container with 16px padding = 8px icon area (very small, but matches Figma) + const logoSize = isLarge + ? 103 + : isMedium + ? 56 + : isSmall + ? 56 // S: 80px container - 12px padding * 2 = 56px icon + : 8; // XS: 40px container - 16px padding * 2 = 8px icon const logoContainerClass = isLarge ? "size-[103px]" - : "size-[56px]"; + : isMedium + ? "size-[56px]" + : isSmall + ? "size-[80px]" // S: 80px container + : "size-[40px]"; // XS: 40px container // Title typography const titleClass = isLarge ? "font-bricolage-grotesque font-extrabold text-[36px] leading-[44px]" - : "font-bricolage-grotesque font-bold text-[24px] leading-[32px]"; + : isMedium + ? "font-bricolage-grotesque font-bold text-[24px] leading-[32px]" + : isSmall + ? "font-bricolage-grotesque font-bold text-[28px] leading-[36px]" // S: 28px, bold, Bricolage + : "font-inter font-bold text-[20px] leading-[28px]"; // XS: 20px, bold, Inter // Description typography const descriptionClass = isLarge ? "font-inter font-medium text-[18px] leading-[24px]" - : "font-inter font-medium text-[14px] leading-[16px]"; + : isMedium + ? "font-inter font-medium text-[14px] leading-[16px]" + : isSmall + ? "font-inter font-medium text-[14px] leading-[16px]" // S: 14px, medium, Inter + : "font-inter font-medium text-[12px] leading-[14px]"; // XS: 12px, medium, Inter // Render logo/icon const renderLogo = () => { @@ -59,29 +93,31 @@ export function RuleCardView({ // Check if it's a localhost URL or external URL that needs regular img tag const isLocalhost = logoUrl.startsWith("http://localhost") || logoUrl.startsWith("https://localhost"); + const containerClass = `${logoContainerClass} relative rounded-full overflow-hidden mix-blend-luminosity ${isSmall ? "p-[12px]" : isExtraSmall ? "p-[16px]" : ""}`; + if (isLocalhost) { return ( -
+
{/* eslint-disable-next-line @next/next/no-img-element */} {logoAlt
); } return ( -
+
{logoAlt
); @@ -89,16 +125,23 @@ export function RuleCardView({ if (icon) { return ( -
+
{icon}
); } if (communityInitials) { + const initialsSize = isLarge + ? "text-[36px]" + : isMedium + ? "text-[24px]" + : isSmall + ? "text-[20px]" + : "text-[16px]"; return (
- + {communityInitials}
@@ -111,7 +154,7 @@ export function RuleCardView({ return (
{/* Outermost container with bottom border - taller to match Figma */} -
+
{/* Logo/Icon - fixed width/height, vertically centered, does not touch bottom */} {renderLogo() && ( -
+
{renderLogo()}
)} - {/* 16px spacing */} -
+ {/* Spacing between icon and title */} + {!isSmall && !isExtraSmall &&
} {/* Container with no padding and left border - extends full height to touch bottom */} {title && ( -
+
{/* Inner container for header text with padding */} -
+

- {title} -

+ {title} +
)} @@ -175,7 +218,7 @@ export function RuleCardView({
)} {/* Footer: Description */} - {description && ( + {description && (

{description} @@ -188,8 +231,8 @@ export function RuleCardView({ description && (

- {description} -

+ {description} +

) )} diff --git a/lib/propNormalization.ts b/lib/propNormalization.ts index 0c5d6de..9ebd610 100644 --- a/lib/propNormalization.ts +++ b/lib/propNormalization.ts @@ -519,10 +519,10 @@ export function normalizeSmallMediumLargeSize( export function normalizeRuleCardSize( value: string | undefined, defaultValue: "L" = "L" -): "L" | "M" { +): "XS" | "S" | "M" | "L" { if (!value) return defaultValue; const normalized = value.toUpperCase(); - if (normalized === "L" || normalized === "M") { + if (normalized === "XS" || normalized === "S" || normalized === "M" || normalized === "L") { return normalized; } return defaultValue; diff --git a/stories/RuleCard.stories.js b/stories/RuleCard.stories.js index a88b827..074fc19 100644 --- a/stories/RuleCard.stories.js +++ b/stories/RuleCard.stories.js @@ -39,7 +39,7 @@ export default { }, size: { control: { type: "select" }, - options: ["L", "M", "l", "m"], + options: ["XS", "S", "M", "L", "xs", "s", "m", "l"], description: "Size variant of the card", }, onClick: { action: "clicked" }, @@ -186,6 +186,44 @@ export const SizeMedium = { }, }; +export const SizeSmall = { + args: { + title: "Consensus clusters", + description: + "Units called Circles have the ability to decide and act on matters in their domains, which their members agree on through a Council.", + backgroundColor: "bg-[var(--color-surface-default-brand-lime)]", + expanded: false, + size: "S", + icon: ( + Sociocracy + ), + }, +}; + +export const SizeExtraSmall = { + args: { + title: "Consensus clusters", + description: + "Units called Circles have the ability to decide and act on matters in their domains, which their members agree on through a Council.", + backgroundColor: "bg-[var(--color-surface-default-brand-lime)]", + expanded: false, + size: "XS", + icon: ( + Sociocracy + ), + }, +}; + export const ExpandedMedium = { args: { title: "Mutual Aid Mondays",