Completed template

This commit is contained in:
adilallo
2026-03-02 22:12:50 -07:00
parent d811b87b12
commit 3e3d2881f5
103 changed files with 1410 additions and 622 deletions
+1 -1
View File
@@ -189,7 +189,7 @@ export default async function BlogPostPage({ params }: PageProps) {
url: "https://communityrule.com", url: "https://communityrule.com",
logo: { logo: {
"@type": "ImageObject", "@type": "ImageObject",
url: "https://communityrule.com/assets/Logo.svg", url: "https://communityrule.com/assets/logo/Logo.svg",
}, },
}, },
datePublished: post.frontmatter.date, datePublished: post.frontmatter.date,
+10 -4
View File
@@ -12,12 +12,15 @@ const LogoWall = dynamic(() => import("../components/sections/LogoWall"), {
ssr: true, ssr: true,
}); });
const NumberedCards = dynamic(() => import("../components/sections/NumberedCards"), { const NumberedCards = dynamic(
() => import("../components/sections/NumberedCards"),
{
loading: () => ( loading: () => (
<section className="py-[var(--spacing-scale-032)] min-h-[300px]" /> <section className="py-[var(--spacing-scale-032)] min-h-[300px]" />
), ),
ssr: true, ssr: true,
}); },
);
const RuleStack = dynamic(() => import("../components/sections/RuleStack"), { const RuleStack = dynamic(() => import("../components/sections/RuleStack"), {
loading: () => ( loading: () => (
@@ -26,12 +29,15 @@ const RuleStack = dynamic(() => import("../components/sections/RuleStack"), {
ssr: true, ssr: true,
}); });
const FeatureGrid = dynamic(() => import("../components/sections/FeatureGrid"), { const FeatureGrid = dynamic(
() => import("../components/sections/FeatureGrid"),
{
loading: () => ( loading: () => (
<section className="py-[var(--spacing-scale-032)] min-h-[500px]" /> <section className="py-[var(--spacing-scale-032)] min-h-[500px]" />
), ),
ssr: true, ssr: true,
}); },
);
const QuoteBlock = dynamic(() => import("../components/sections/QuoteBlock"), { const QuoteBlock = dynamic(() => import("../components/sections/QuoteBlock"), {
loading: () => ( loading: () => (
@@ -1,4 +1,10 @@
export type ContextMenuItemSizeValue = "small" | "medium" | "large" | "Small" | "Medium" | "Large"; export type ContextMenuItemSizeValue =
| "small"
| "medium"
| "large"
| "Small"
| "Medium"
| "Large";
export interface ContextMenuItemProps extends React.HTMLAttributes<HTMLDivElement> { export interface ContextMenuItemProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode; children?: React.ReactNode;
+10 -3
View File
@@ -8,7 +8,8 @@ export type IconName = "exclamation";
/** SVG import may be a React component or a module object { default: Component } (e.g. with Turbopack) */ /** SVG import may be a React component or a module object { default: Component } (e.g. with Turbopack) */
const iconMap: Record< const iconMap: Record<
IconName, IconName,
React.ComponentType<React.SVGProps<SVGSVGElement>> | { default: React.ComponentType<React.SVGProps<SVGSVGElement>> } | React.ComponentType<React.SVGProps<SVGSVGElement>>
| { default: React.ComponentType<React.SVGProps<SVGSVGElement>> }
> = { > = {
exclamation: ExclamationIcon, exclamation: ExclamationIcon,
}; };
@@ -31,8 +32,14 @@ function IconComponent({
if (!SvgModule) return null; if (!SvgModule) return null;
// Turbopack/bundler may expose SVG as { default: Component } instead of the component directly // Turbopack/bundler may expose SVG as { default: Component } instead of the component directly
const Svg = const Svg =
typeof SvgModule === "object" && SvgModule !== null && "default" in SvgModule typeof SvgModule === "object" &&
? (SvgModule as { default: React.ComponentType<React.SVGProps<SVGSVGElement>> }).default SvgModule !== null &&
"default" in SvgModule
? (
SvgModule as {
default: React.ComponentType<React.SVGProps<SVGSVGElement>>;
}
).default
: (SvgModule as React.ComponentType<React.SVGProps<SVGSVGElement>>); : (SvgModule as React.ComponentType<React.SVGProps<SVGSVGElement>>);
if (typeof Svg !== "function") return null; if (typeof Svg !== "function") return null;
return ( return (
+1
View File
@@ -1,2 +1,3 @@
export { default as Icon } from "./Icon"; export { default as Icon } from "./Icon";
export type { IconName, IconProps } from "./Icon"; export type { IconName, IconProps } from "./Icon";
export { default as Logo } from "./logo";
+134
View File
@@ -0,0 +1,134 @@
import { memo } from "react";
import Link from "next/link";
import { getAssetPath, ASSETS } from "../../../../lib/assetUtils";
interface LogoProps {
size?:
| "default"
| "footer"
| "createFlow"
| "topNavFolderTop"
| "topNavHeader";
/**
* Visual style: default (dark on light) or inverse (e.g. black/white on teal).
* @default "default"
*/
palette?: "default" | "inverse";
/**
* Whether to show the "CommunityRule" wordmark.
* @default true
*/
wordmark?: boolean;
}
interface SizeConfig {
containerHeight: string;
gap: string;
textSize: string;
lineHeight: string;
iconSize: string;
}
const Logo = memo<LogoProps>(
({ size = "default", palette = "default", wordmark = true }) => {
// Size configurations
const sizes: Record<string, SizeConfig> = {
default: {
containerHeight: "h-[41px]",
gap: "gap-[8.28px]",
textSize: "text-[21.97px]",
lineHeight: "leading-[27.05px]",
iconSize: "w-[27.05px] h-[27.05px]",
},
footer: {
containerHeight:
"h-[41px] sm:h-[calc(40px*1.37)] lg:h-[calc(40px*2.05)]",
gap: "gap-[8.28px] sm:gap-[calc(8px*1.37)] lg:gap-[calc(8px*2.05)]",
textSize:
"text-[21.97px] sm:text-[calc(21.97px*1.37)] lg:text-[calc(21.97px*2.05)]",
lineHeight:
"leading-[27.05px] sm:leading-[calc(27.05px*1.37)] lg:leading-[calc(27.05px*2.05)]",
iconSize:
"w-[27.05px] h-[27.05px] sm:w-[calc(27.05px*1.37)] sm:h-[calc(27.05px*1.37)] lg:w-[calc(27.05px*2.05)] lg:h-[calc(27.05px*2.05)]",
},
createFlow: {
containerHeight: "h-[30px] md:h-[41px]",
gap: "gap-[6px] md:gap-[8.28px]",
textSize: "text-[16.48px] md:text-[21.97px]",
lineHeight: "leading-[20.28px] md:leading-[27.05px]",
iconSize: "w-[20.28px] h-[20.28px] md:w-[27.05px] md:h-[27.05px]",
},
topNavFolderTop: {
containerHeight:
"h-[14.11px] sm:h-[21.06px] md:h-[32.24px] lg:h-[28px] xl:h-[36px]",
gap: "gap-0 sm:gap-[3.19px] md:gap-[4.89px] lg:gap-[6.55px] xl:gap-[8.64px]",
textSize:
"text-[11.57px] sm:text-[11.69px] md:text-[17.89px] lg:text-[21.97px] xl:text-[29.01px]",
lineHeight:
"leading-[14.24px] sm:leading-[14.39px] md:leading-[22.02px] lg:leading-[27.05px] xl:leading-[35.7px]",
iconSize:
"w-[14.11px] h-[14.11px] sm:w-[14.39px] sm:h-[14.39px] md:w-[22.02px] md:h-[22.02px] lg:w-[27.05px] lg:h-[27.05px] xl:w-[35.7px] xl:h-[35.7px]",
},
topNavHeader: {
containerHeight:
"h-[20.85px] sm:h-[20.85px] md:h-[17.91px] lg:h-[28px] xl:h-[34px]",
gap: "gap-0 sm:gap-[4.21px] md:gap-[6.51px] lg:gap-[6.55px] xl:gap-[8.19px]",
textSize:
"text-[11.57px] sm:text-[11.57px] md:text-[17.89px] lg:text-[21.97px] xl:text-[27.47px]",
lineHeight:
"leading-[14.24px] sm:leading-[14.24px] md:leading-[22.02px] lg:leading-[27.05px] xl:leading-[33.81px]",
iconSize:
"w-[14.24px] h-[14.24px] sm:w-[14.24px] sm:h-[14.24px] md:w-[22.02px] md:h-[22.02px] lg:w-[27.05px] lg:h-[27.05px] xl:w-[33.81px] xl:h-[33.81px]",
},
};
const config = sizes[size || "default"] || sizes.default;
const isInverse = palette === "inverse";
const textColorClass = isInverse
? "text-[var(--color-content-invert-primary)]"
: "text-[var(--color-content-default-primary)]";
const wordmarkVisibilityClass =
size === "topNavFolderTop" || size === "topNavHeader"
? wordmark
? "hidden sm:block"
: "hidden"
: wordmark
? ""
: "hidden";
return (
<Link href="/" className="block" aria-label="CommunityRule Logo">
<div
className={`flex items-center ${config.containerHeight} ${
wordmark ? config.gap : ""
} transition-all duration-200 ease-in-out hover:scale-[1.02] cursor-pointer`}
>
{/* Logo Text - responsive visibility for topNav sizes */}
<div
className={`font-bricolage-grotesque ${textColorClass} ${config.textSize} ${config.lineHeight} font-normal tracking-[0px] transition-colors duration-200 ${wordmarkVisibilityClass}`}
aria-label="CommunityRule"
>
CommunityRule
</div>
{/* Vector Icon */}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={getAssetPath(ASSETS.LOGO)}
alt="CommunityRule Logo Icon"
width={27.05}
height={27.05}
className={`flex-shrink-0 ${config.iconSize} transition-all duration-200 ${
isInverse ? "brightness-0" : ""
}`}
aria-hidden="true"
/>
</div>
</Link>
);
},
);
Logo.displayName = "Logo";
export default Logo;
+1
View File
@@ -0,0 +1 @@
export { default } from "./Logo";
+6 -9
View File
@@ -109,15 +109,11 @@ const Button = memo<ButtonProps>(
const variant = getVariantFromTypeAndPalette(buttonType, buttonPalette); const variant = getVariantFromTypeAndPalette(buttonType, buttonPalette);
const sizeStyles: Record<string, string> = { const sizeStyles: Record<string, string> = {
xsmall: xsmall: "p-[var(--spacing-scale-004)] gap-[var(--spacing-scale-002)]",
"p-[var(--spacing-scale-004)] gap-[var(--spacing-scale-002)]", small: "p-[var(--spacing-scale-008)] gap-[var(--spacing-scale-002)]",
small:
"p-[var(--spacing-scale-008)] gap-[var(--spacing-scale-002)]",
medium: "p-[var(--spacing-scale-010)] gap-[var(--spacing-scale-004)]", medium: "p-[var(--spacing-scale-010)] gap-[var(--spacing-scale-004)]",
large: large: "p-[var(--spacing-scale-012)] gap-[var(--spacing-scale-006)]",
"p-[var(--spacing-scale-012)] gap-[var(--spacing-scale-006)]", xlarge: "p-[var(--spacing-scale-016)] gap-[var(--spacing-scale-008)]",
xlarge:
"p-[var(--spacing-scale-016)] gap-[var(--spacing-scale-008)]",
}; };
const fontStyles: Record<string, string> = { const fontStyles: Record<string, string> = {
@@ -135,7 +131,8 @@ const Button = memo<ButtonProps>(
"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:bg-[var(--color-surface-default-primary)] focus:text-[var(--color-content-default-brand-primary)] focus:outline-none focus:border-transparent focus:shadow-[0_0_0px_2px_var(--color-border-invert-primary),0_0_0px_4px_var(--color-border-default-primary)] focus: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)] hover:scale-[1.02] focus:bg-[var(--color-surface-default-primary)] focus:text-[var(--color-content-default-brand-primary)] focus:outline-none focus:border-transparent focus:shadow-[0_0_0px_2px_var(--color-border-invert-primary),0_0_0px_4px_var(--color-border-default-primary)] focus: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",
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:bg-transparent focus:text-[var(--color-content-default-primary)] focus:outline-none focus:border-[1.5px] focus:border-[var(--color-border-invert-primary)] focus:shadow-[0_0_0px_2px_var(--color-border-default-primary),0_0_0px_4px_var(--color-border-invert-primary)] focus: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)] hover:scale-[1.02] focus:bg-transparent focus:text-[var(--color-content-default-primary)] focus:outline-none focus:border-[1.5px] focus:border-[var(--color-border-invert-primary)] focus:shadow-[0_0_0px_2px_var(--color-border-default-primary),0_0_0px_4px_var(--color-border-invert-primary)] focus: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",
"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:bg-transparent focus:text-[var(--color-content-invert-primary)] focus:outline-none focus:border-[1.5px] focus:border-[var(--color-border-default-primary)] focus:shadow-[0_0_0px_2px_var(--color-border-invert-primary),0_0_0px_4px_var(--color-border-default-primary)] focus: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", "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:bg-transparent focus:text-[var(--color-content-invert-primary)] focus:outline-none focus:border-[1.5px] focus:border-[var(--color-border-default-primary)] focus:shadow-[0_0_0px_2px_var(--color-border-invert-primary),0_0_0px_4px_var(--color-border-default-primary)] focus: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",
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:bg-transparent focus:text-[var(--color-content-default-brand-primary)] focus:outline-none focus:border-transparent focus:shadow-[0_0_0px_2px_var(--color-border-default-primary),0_0_0px_4px_var(--color-border-invert-primary)] focus: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 hover:scale-[1.02] focus:bg-transparent focus:text-[var(--color-content-default-brand-primary)] focus:outline-none focus:border-transparent focus:shadow-[0_0_0px_2px_var(--color-border-default-primary),0_0_0px_4px_var(--color-border-invert-primary)] focus: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",
"ghost-inverse": "ghost-inverse":
+19 -14
View File
@@ -29,7 +29,8 @@ interface NumberCardProps {
const NumberCard = memo<NumberCardProps>(({ number, text, size: sizeProp }) => { const NumberCard = memo<NumberCardProps>(({ number, text, size: sizeProp }) => {
// Base classes common to all sizes // Base classes common to all sizes
const baseClasses = "bg-[var(--color-surface-inverse-primary)] rounded-[12px] shadow-lg"; const baseClasses =
"bg-[var(--color-surface-inverse-primary)] rounded-[12px] shadow-lg";
// If size prop is provided, use explicit size classes // If size prop is provided, use explicit size classes
// Otherwise, use responsive breakpoints for backward compatibility // Otherwise, use responsive breakpoints for backward compatibility
@@ -40,16 +41,22 @@ const NumberCard = memo<NumberCardProps>(({ number, text, size: sizeProp }) => {
const sizeClasses = { const sizeClasses = {
Small: "flex flex-col items-end justify-center gap-4 p-5 relative", Small: "flex flex-col items-end justify-center gap-4 p-5 relative",
Medium: "flex flex-row items-center gap-8 p-8 relative", Medium: "flex flex-row items-center gap-8 p-8 relative",
Large: "flex flex-col items-start justify-end gap-[22px] h-[238px] p-8 relative", Large:
XLarge: "flex flex-col items-start justify-end gap-[22px] h-[238px] p-8 relative", "flex flex-col items-start justify-end gap-[22px] h-[238px] p-8 relative",
XLarge:
"flex flex-col items-start justify-end gap-[22px] h-[238px] p-8 relative",
}; };
// Text size classes // Text size classes
const textClasses = { const textClasses = {
Small: "font-bricolage-grotesque font-medium text-[24px] leading-[32px] text-[#141414]", Small:
Medium: "font-bricolage-grotesque font-medium text-[24px] leading-[24px] text-[#141414]", "font-bricolage-grotesque font-medium text-[24px] leading-[32px] text-[#141414]",
Large: "font-bricolage-grotesque font-medium text-[24px] leading-[24px] text-[#141414]", Medium:
XLarge: "font-bricolage-grotesque font-medium text-[32px] leading-[32px] text-[#141414]", "font-bricolage-grotesque font-medium text-[24px] leading-[24px] text-[#141414]",
Large:
"font-bricolage-grotesque font-medium text-[24px] leading-[24px] text-[#141414]",
XLarge:
"font-bricolage-grotesque font-medium text-[32px] leading-[32px] text-[#141414]",
}; };
// Section number wrapper classes - Small doesn't need a wrapper // Section number wrapper classes - Small doesn't need a wrapper
@@ -76,9 +83,7 @@ const NumberCard = memo<NumberCardProps>(({ number, text, size: sizeProp }) => {
<SectionNumber number={number} /> <SectionNumber number={number} />
{/* Card Content */} {/* Card Content */}
<p className={textClasses[size]}> <p className={textClasses[size]}>{text}</p>
{text}
</p>
</div> </div>
); );
} }
@@ -92,9 +97,7 @@ const NumberCard = memo<NumberCardProps>(({ number, text, size: sizeProp }) => {
{/* Card Content */} {/* Card Content */}
<div className={contentClasses[size]}> <div className={contentClasses[size]}>
<p className={textClasses[size]}> <p className={textClasses[size]}>{text}</p>
{text}
</p>
</div> </div>
</div> </div>
); );
@@ -103,7 +106,9 @@ const NumberCard = memo<NumberCardProps>(({ number, text, size: sizeProp }) => {
// Responsive breakpoints for backward compatibility (matches original behavior) // Responsive breakpoints for backward compatibility (matches original behavior)
// Maps to: Small (mobile) -> Medium (sm) -> Large (lg) -> XLarge (xl) // Maps to: Small (mobile) -> Medium (sm) -> Large (lg) -> XLarge (xl)
return ( return (
<div className={`${baseClasses} flex flex-col gap-4 p-5 sm:flex-row sm:gap-8 sm:p-8 sm:items-center lg:flex-col lg:gap-[22px] lg:items-start lg:justify-end lg:p-8 lg:relative lg:h-[238px]`}> <div
className={`${baseClasses} flex flex-col gap-4 p-5 sm:flex-row sm:gap-8 sm:p-8 sm:items-center lg:flex-col lg:gap-[22px] lg:items-start lg:justify-end lg:p-8 lg:relative lg:h-[238px]`}
>
{/* Section Number - Responsive positioning */} {/* Section Number - Responsive positioning */}
<div className="flex justify-end items-end sm:justify-start sm:flex-shrink-0 lg:absolute lg:top-8 lg:right-8"> <div className="flex justify-end items-end sm:justify-start sm:flex-shrink-0 lg:absolute lg:top-8 lg:right-8">
<SectionNumber number={number} /> <SectionNumber number={number} />
@@ -5,7 +5,11 @@ export interface Category {
chipOptions: ChipOption[]; chipOptions: ChipOption[];
onChipClick?: (categoryName: string, chipId: string) => void; onChipClick?: (categoryName: string, chipId: string) => void;
onAddClick?: (categoryName: string) => void; onAddClick?: (categoryName: string) => void;
onCustomChipConfirm?: (categoryName: string, chipId: string, value: string) => void; onCustomChipConfirm?: (
categoryName: string,
chipId: string,
value: string,
) => void;
onCustomChipClose?: (categoryName: string, chipId: string) => void; onCustomChipClose?: (categoryName: string, chipId: string) => void;
} }
+47 -21
View File
@@ -31,7 +31,12 @@ export function RuleCardView({
// Card dimensions - use CSS classes from className if provided, otherwise use size-based logic // Card dimensions - use CSS classes from className if provided, otherwise use size-based logic
// Check if className already has padding/gap classes // Check if className already has padding/gap classes
const hasResponsivePadding = className?.includes("p-[") || className?.includes("px-[") || className?.includes("py-[") || className?.includes("pt-[") || className?.includes("pb-["); const hasResponsivePadding =
className?.includes("p-[") ||
className?.includes("px-[") ||
className?.includes("py-[") ||
className?.includes("pt-[") ||
className?.includes("pb-[");
const hasResponsiveGap = className?.includes("gap-["); const hasResponsiveGap = className?.includes("gap-[");
const cardPadding = hasResponsivePadding const cardPadding = hasResponsivePadding
@@ -90,7 +95,9 @@ export function RuleCardView({
const renderLogo = () => { const renderLogo = () => {
if (logoUrl) { if (logoUrl) {
// Check if it's a localhost URL or external URL that needs regular img tag // 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 isLocalhost =
logoUrl.startsWith("http://localhost") ||
logoUrl.startsWith("https://localhost");
const containerClass = `${logoContainerClass} relative rounded-full overflow-hidden mix-blend-luminosity max-[639px]:p-[16px] min-[640px]:max-[1023px]:p-[12px]`; const containerClass = `${logoContainerClass} relative rounded-full overflow-hidden mix-blend-luminosity max-[639px]:p-[16px] min-[640px]:max-[1023px]:p-[12px]`;
@@ -124,7 +131,9 @@ export function RuleCardView({
if (icon) { if (icon) {
return ( return (
<div className={`${logoContainerClass} flex items-center justify-center max-[639px]:p-[16px] min-[640px]:max-[1023px]:p-[12px]`}> <div
className={`${logoContainerClass} flex items-center justify-center max-[639px]:p-[16px] min-[640px]:max-[1023px]:p-[12px]`}
>
{icon} {icon}
</div> </div>
); );
@@ -138,8 +147,12 @@ export function RuleCardView({
min-[1440px]:text-[36px] min-[1440px]:text-[36px]
`; `;
return ( return (
<div className={`${logoContainerClass} rounded-full bg-[var(--color-surface-default-primary)] flex items-center justify-center`}> <div
<span className={`${initialsSize} font-bricolage-grotesque font-bold text-[var(--color-content-default-primary,white)]`}> className={`${logoContainerClass} rounded-full bg-[var(--color-surface-default-primary)] flex items-center justify-center`}
>
<span
className={`${initialsSize} font-bricolage-grotesque font-bold text-[var(--color-content-default-primary,white)]`}
>
{communityInitials} {communityInitials}
</span> </span>
</div> </div>
@@ -149,7 +162,6 @@ export function RuleCardView({
return null; return null;
}; };
// Border radius - use CSS classes if provided via className, otherwise use size-based logic // Border radius - use CSS classes if provided via className, otherwise use size-based logic
const borderRadiusClass = className?.includes("rounded-") const borderRadiusClass = className?.includes("rounded-")
? "" // If className already has border radius, don't add size-based one ? "" // If className already has border radius, don't add size-based one
@@ -170,46 +182,58 @@ export function RuleCardView({
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
> >
{/* Outermost container with bottom border - taller to match Figma */} {/* Outermost container with bottom border - taller to match Figma */}
<div className={` <div
className={`
border-b border-black border-solid flex items-center relative shrink-0 w-full border-b border-black border-solid flex items-center relative shrink-0 w-full
max-[639px]:h-[72px] max-[639px]:h-[72px]
min-[640px]:max-[1023px]:h-[80px] min-[640px]:max-[1023px]:h-[80px]
min-[1024px]:max-[1439px]:h-[88px] min-[1024px]:max-[1439px]:h-[88px]
min-[1440px]:h-[136px] min-[1440px]:h-[136px]
`}> `}
>
{/* Logo/Icon - fixed width/height, vertically centered, does not touch bottom */} {/* Logo/Icon - fixed width/height, vertically centered, does not touch bottom */}
{renderLogo() && ( {renderLogo() && (
<div className={` <div
className={`
flex items-center justify-center shrink-0 flex items-center justify-center shrink-0
max-[639px]:w-[72px] max-[639px]:h-[72px] max-[639px]:border-r max-[639px]:border-black max-[639px]:border-solid max-[639px]:w-[72px] max-[639px]:h-[72px] max-[639px]:border-r max-[639px]:border-black max-[639px]:border-solid
min-[640px]:max-[1023px]:w-[80px] min-[640px]:max-[1023px]:h-[80px] min-[640px]:max-[1023px]:border-r min-[640px]:max-[1023px]:border-black min-[640px]:max-[1023px]:border-solid min-[640px]:max-[1023px]:w-[80px] min-[640px]:max-[1023px]:h-[80px] min-[640px]:max-[1023px]:border-r min-[640px]:max-[1023px]:border-black min-[640px]:max-[1023px]:border-solid
min-[1024px]:max-[1439px]:w-[56px] min-[1024px]:max-[1439px]:h-[56px] min-[1024px]:max-[1439px]:w-[56px] min-[1024px]:max-[1439px]:h-[56px]
min-[1440px]:w-[103px] min-[1440px]:h-[103px] min-[1440px]:w-[103px] min-[1440px]:h-[103px]
`}> `}
>
{renderLogo()} {renderLogo()}
</div> </div>
)} )}
{/* Spacing between icon and title */} {/* Spacing between icon and title */}
<div className=" <div
className="
max-[1023px]:hidden max-[1023px]:hidden
min-[1024px]:w-[16px] min-[1024px]:shrink-0 min-[1024px]:w-[16px] min-[1024px]:shrink-0
" /> "
/>
{/* Container with no padding and left border - extends full height to touch bottom */} {/* Container with no padding and left border - extends full height to touch bottom */}
{title && ( {title && (
<div className={` <div
className={`
flex-1 min-w-0 h-full flex flex-1 min-w-0 h-full flex
max-[1023px]:border-0 max-[1023px]:border-0
min-[1024px]:border-l min-[1024px]:border-black min-[1024px]:border-solid min-[1024px]:border-l min-[1024px]:border-black min-[1024px]:border-solid
`}> `}
>
{/* Inner container for header text with padding */} {/* Inner container for header text with padding */}
<div className={` <div
className={`
flex items-center justify-center w-full flex items-center justify-center w-full
max-[639px]:pl-[8px] max-[639px]:py-[8px] max-[639px]:pl-[8px] max-[639px]:py-[8px]
min-[640px]:max-[1023px]:pl-[12px] min-[640px]:max-[1023px]:py-[12px] min-[640px]:max-[1023px]:pl-[12px] min-[640px]:max-[1023px]:py-[12px]
min-[1024px]:max-[1439px]:px-[16px] min-[1024px]:max-[1439px]:py-[12px] min-[1024px]:max-[1439px]:px-[16px] min-[1024px]:max-[1439px]:py-[12px]
min-[1440px]:px-[16px] min-[1440px]:py-[24px] min-[1440px]:px-[16px] min-[1440px]:py-[24px]
`}> `}
<h3 className={`${titleClass} text-black overflow-hidden text-ellipsis w-full`}> >
<h3
className={`${titleClass} text-black overflow-hidden text-ellipsis w-full`}
>
{title} {title}
</h3> </h3>
</div> </div>
@@ -237,7 +261,11 @@ export function RuleCardView({
category.onAddClick?.(category.name); category.onAddClick?.(category.name);
}} }}
onCustomChipConfirm={(chipId, value) => { onCustomChipConfirm={(chipId, value) => {
category.onCustomChipConfirm?.(category.name, chipId, value); category.onCustomChipConfirm?.(
category.name,
chipId,
value,
);
}} }}
onCustomChipClose={(chipId) => { onCustomChipClose={(chipId) => {
category.onCustomChipClose?.(category.name, chipId); category.onCustomChipClose?.(category.name, chipId);
@@ -252,9 +280,7 @@ export function RuleCardView({
{/* Footer: Description */} {/* Footer: Description */}
{description && ( {description && (
<div className="border-t border-black border-solid pt-[16px] relative shrink-0 w-full"> <div className="border-t border-black border-solid pt-[16px] relative shrink-0 w-full">
<p className={`${descriptionClass} text-black`}> <p className={`${descriptionClass} text-black`}>{description}</p>
{description}
</p>
</div> </div>
)} )}
</> </>
@@ -1,6 +1,10 @@
import type { BlogPost } from "../../../../lib/content"; import type { BlogPost } from "../../../../lib/content";
export type ContentContainerSizeValue = "xs" | "responsive" | "Xs" | "Responsive"; export type ContentContainerSizeValue =
| "xs"
| "responsive"
| "Xs"
| "Responsive";
export interface ContentContainerProps { export interface ContentContainerProps {
post: BlogPost; post: BlogPost;
@@ -1,6 +1,10 @@
import type { BlogPost } from "../../../../lib/content"; import type { BlogPost } from "../../../../lib/content";
export type ContentThumbnailTemplateVariantValue = "vertical" | "horizontal" | "Vertical" | "Horizontal"; export type ContentThumbnailTemplateVariantValue =
| "vertical"
| "horizontal"
| "Vertical"
| "Horizontal";
export interface ContentThumbnailTemplateProps { export interface ContentThumbnailTemplateProps {
post: BlogPost; post: BlogPost;
@@ -4,7 +4,10 @@ import { memo } from "react";
import { useComponentId } from "../../../hooks"; import { useComponentId } from "../../../hooks";
import { CheckboxView } from "./Checkbox.view"; import { CheckboxView } from "./Checkbox.view";
import type { CheckboxProps } from "./Checkbox.types"; import type { CheckboxProps } from "./Checkbox.types";
import { normalizeMode, normalizeState } from "../../../../lib/propNormalization"; import {
normalizeMode,
normalizeState,
} from "../../../../lib/propNormalization";
const CheckboxContainer = memo<CheckboxProps>( const CheckboxContainer = memo<CheckboxProps>(
({ ({
@@ -43,7 +46,9 @@ const CheckboxContainer = memo<CheckboxProps>(
transition-all transition-all
duration-200 duration-200
ease-in-out ease-in-out
`.trim().replace(/\s+/g, " "); `
.trim()
.replace(/\s+/g, " ");
// Get box styles based on state and checked status per Figma designs // Get box styles based on state and checked status per Figma designs
const getBoxStyles = (): string => { const getBoxStyles = (): string => {
@@ -22,7 +22,9 @@ const CheckboxGroupContainer = ({
const groupId = name || `checkbox-group-${generatedId}`; const groupId = name || `checkbox-group-${generatedId}`;
// Internal state to track checked values (only used if value prop is not provided) // Internal state to track checked values (only used if value prop is not provided)
const [internalCheckedValues, setInternalCheckedValues] = useState<string[]>([]); const [internalCheckedValues, setInternalCheckedValues] = useState<string[]>(
[],
);
// Use controlled value if provided, otherwise use internal state // Use controlled value if provided, otherwise use internal state
const checkedValues = value !== undefined ? value : internalCheckedValues; const checkedValues = value !== undefined ? value : internalCheckedValues;
@@ -23,10 +23,7 @@ export function CheckboxGroupView({
// If there's subtext, render checkbox without label and handle layout separately // If there's subtext, render checkbox without label and handle layout separately
if (option.subtext) { if (option.subtext) {
return ( return (
<div <div key={option.value} className="flex gap-[8px] items-start">
key={option.value}
className="flex gap-[8px] items-start"
>
<Checkbox <Checkbox
checked={isChecked} checked={isChecked}
mode={mode} mode={mode}
@@ -41,7 +41,10 @@ const ChipContainer = memo<ChipProps>(
} }
}, [isCustom]); }, [isCustom]);
const handleCheck = (value: string, event: React.MouseEvent<HTMLButtonElement>) => { const handleCheck = (
value: string,
event: React.MouseEvent<HTMLButtonElement>,
) => {
if (onCheck && value.trim()) { if (onCheck && value.trim()) {
onCheck(value.trim(), event); onCheck(value.trim(), event);
// Reset input after successful check // Reset input after successful check
@@ -63,7 +66,10 @@ const ChipContainer = memo<ChipProps>(
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => { const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter" && inputValue.trim() && onCheck) { if (event.key === "Enter" && inputValue.trim() && onCheck) {
event.preventDefault(); event.preventDefault();
handleCheck(inputValue.trim(), event as unknown as React.MouseEvent<HTMLButtonElement>); handleCheck(
inputValue.trim(),
event as unknown as React.MouseEvent<HTMLButtonElement>,
);
} else if (event.key === "Escape" && onClose) { } else if (event.key === "Escape" && onClose) {
event.preventDefault(); event.preventDefault();
handleClose(event as unknown as React.MouseEvent<HTMLButtonElement>); handleClose(event as unknown as React.MouseEvent<HTMLButtonElement>);
@@ -95,4 +101,3 @@ const ChipContainer = memo<ChipProps>(
ChipContainer.displayName = "Chip"; ChipContainer.displayName = "Chip";
export default ChipContainer; export default ChipContainer;
@@ -68,4 +68,3 @@ export interface ChipViewProps {
inputRef?: React.RefObject<HTMLInputElement>; inputRef?: React.RefObject<HTMLInputElement>;
ariaLabel?: string; ariaLabel?: string;
} }
+23 -30
View File
@@ -43,31 +43,25 @@ function ChipView({
// Use consistent border width to prevent layout shift // Use consistent border width to prevent layout shift
const borderWidth = isSmall ? "border-[1.25px]" : "border-2"; const borderWidth = isSmall ? "border-[1.25px]" : "border-2";
let background = "bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))]"; let background =
let border = "bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))]";
`${borderWidth} border-[var(--color-border-default-tertiary,#464646)]`; let border = `${borderWidth} border-[var(--color-border-default-tertiary,#464646)]`;
let textColor = let textColor =
"text-[color:var(--color-content-default-brand-primary,#fefcc9)]"; "text-[color:var(--color-content-default-brand-primary,#fefcc9)]";
if (isDefault) { if (isDefault) {
if (state === "custom") { if (state === "custom") {
background = background = "bg-[var(--color-surface-default-secondary,#141414)]"; // dark background for custom
"bg-[var(--color-surface-default-secondary,#141414)]"; // dark background for custom
border = "border-none"; border = "border-none";
textColor = textColor = "text-[color:var(--color-content-default-tertiary,#b4b4b4)]";
"text-[color:var(--color-content-default-tertiary,#b4b4b4)]";
} else if (state === "disabled") { } else if (state === "disabled") {
background = background = "bg-[var(--color-surface-default-secondary,#141414)]"; // dark background
"bg-[var(--color-surface-default-secondary,#141414)]"; // dark background
border = "border-none"; border = "border-none";
textColor = textColor = "text-[color:var(--color-content-default-tertiary,#b4b4b4)]";
"text-[color:var(--color-content-default-tertiary,#b4b4b4)]";
} else if (isSelected) { } else if (isSelected) {
background = background = "bg-[var(--color-surface-inverse-brandaccent,#fdfaa8)]"; // yellow selected
"bg-[var(--color-surface-inverse-brandaccent,#fdfaa8)]"; // yellow selected
border = `${borderWidth} border-[var(--color-border-default-brand-primary,#fdfaa8)]`; border = `${borderWidth} border-[var(--color-border-default-brand-primary,#fdfaa8)]`;
textColor = textColor = "text-[color:var(--color-content-inverse-primary,black)]";
"text-[color:var(--color-content-inverse-primary,black)]";
} else { } else {
// Unselected default // Unselected default
background = background =
@@ -78,24 +72,20 @@ function ChipView({
} }
} else if (isInverse) { } else if (isInverse) {
if (state === "disabled") { if (state === "disabled") {
background = background = "bg-[var(--color-surface-inverse-tertiary,#d2d2d2)]";
"bg-[var(--color-surface-inverse-tertiary,#d2d2d2)]";
border = "border-none"; border = "border-none";
textColor = textColor = "text-[color:var(--color-content-inverse-primary,black)]";
"text-[color:var(--color-content-inverse-primary,black)]";
} else if (isSelected) { } else if (isSelected) {
background = background =
"bg-[var(--color-surface-default-semi-opaque,rgba(0,0,0,0.05))]"; "bg-[var(--color-surface-default-semi-opaque,rgba(0,0,0,0.05))]";
border = `${borderWidth} border-[var(--color-border-default-primary,#141414)]`; border = `${borderWidth} border-[var(--color-border-default-primary,#141414)]`;
textColor = textColor = "text-[color:var(--color-content-inverse-primary,black)]";
"text-[color:var(--color-content-inverse-primary,black)]";
} else { } else {
// Unselected / custom inverse // Unselected / custom inverse
background = background =
"bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))]"; "bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))]";
border = `${borderWidth} border-[var(--color-border-default-primary,#141414)]`; border = `${borderWidth} border-[var(--color-border-default-primary,#141414)]`;
textColor = textColor = "text-[color:var(--color-content-inverse-primary,black)]";
"text-[color:var(--color-content-inverse-primary,black)]";
} }
} }
@@ -134,7 +124,9 @@ function ChipView({
.filter(Boolean) .filter(Boolean)
.join(" "); .join(" ");
const handleClick = (event: React.MouseEvent<HTMLButtonElement | HTMLDivElement>) => { const handleClick = (
event: React.MouseEvent<HTMLButtonElement | HTMLDivElement>,
) => {
if (isDisabled) { if (isDisabled) {
event.preventDefault(); event.preventDefault();
return; return;
@@ -162,7 +154,9 @@ function ChipView({
}} }}
{...sharedA11y} {...sharedA11y}
> >
<div className={`flex items-center ${isSmall ? "gap-[8px]" : "gap-[12px]"}`}> <div
className={`flex items-center ${isSmall ? "gap-[8px]" : "gap-[12px]"}`}
>
{/* Check button */} {/* Check button */}
{onCheck && ( {onCheck && (
<button <button
@@ -208,7 +202,9 @@ function ChipView({
placeholder="Type to add" placeholder="Type to add"
className="bg-transparent border-none outline-none flex-1 min-w-0 font-inter font-normal text-[color:var(--color-content-default-tertiary,#b4b4b4)] placeholder:text-[color:var(--color-content-default-tertiary,#b4b4b4)]" className="bg-transparent border-none outline-none flex-1 min-w-0 font-inter font-normal text-[color:var(--color-content-default-tertiary,#b4b4b4)] placeholder:text-[color:var(--color-content-default-tertiary,#b4b4b4)]"
style={{ style={{
fontSize: isSmall ? "var(--sizing-300,12px)" : "var(--sizing-400,16px)", fontSize: isSmall
? "var(--sizing-300,12px)"
: "var(--sizing-400,16px)",
lineHeight: isSmall ? "16px" : "24px", lineHeight: isSmall ? "16px" : "24px",
}} }}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
@@ -259,9 +255,7 @@ function ChipView({
onClick={handleClick} onClick={handleClick}
{...sharedA11y} {...sharedA11y}
> >
<span className="flex items-center justify-center"> <span className="flex items-center justify-center">{label}</span>
{label}
</span>
{onRemove && !isDisabled && ( {onRemove && !isDisabled && (
<button <button
type="button" type="button"
@@ -284,4 +278,3 @@ function ChipView({
ChipView.displayName = "ChipView"; ChipView.displayName = "ChipView";
export default memo(ChipView); export default memo(ChipView);
-1
View File
@@ -1,3 +1,2 @@
export { default } from "./Chip.container"; export { default } from "./Chip.container";
export type { ChipProps } from "./Chip.types"; export type { ChipProps } from "./Chip.types";
@@ -3,7 +3,10 @@
import { memo } from "react"; import { memo } from "react";
import MultiSelectView from "./MultiSelect.view"; import MultiSelectView from "./MultiSelect.view";
import type { MultiSelectProps } from "./MultiSelect.types"; import type { MultiSelectProps } from "./MultiSelect.types";
import { normalizeMultiSelectSize, normalizeChipPalette } from "../../../../lib/propNormalization"; import {
normalizeMultiSelectSize,
normalizeChipPalette,
} from "../../../../lib/propNormalization";
const MultiSelectContainer = memo<MultiSelectProps>( const MultiSelectContainer = memo<MultiSelectProps>(
({ ({
@@ -1,4 +1,7 @@
import type { ChipStateValue, ChipPaletteValue } from "../../../../lib/propNormalization"; import type {
ChipStateValue,
ChipPaletteValue,
} from "../../../../lib/propNormalization";
export interface ChipOption { export interface ChipOption {
id: string; id: string;
@@ -31,7 +31,9 @@ function MultiSelectView({
const chipSize = isSmall ? "S" : "M"; const chipSize = isSmall ? "S" : "M";
return ( return (
<div className={`flex flex-col ${isSmall ? "gap-[var(--measures-spacing-200,8px)]" : "gap-[var(--measures-spacing-300,12px)]"} items-start relative w-full ${className}`}> <div
className={`flex flex-col ${isSmall ? "gap-[var(--measures-spacing-200,8px)]" : "gap-[var(--measures-spacing-300,12px)]"} items-start relative w-full ${className}`}
>
{/* Label using InputLabel component */} {/* Label using InputLabel component */}
{formHeader && label && ( {formHeader && label && (
<InputLabel <InputLabel
@@ -45,7 +47,9 @@ function MultiSelectView({
)} )}
{/* Chips container */} {/* Chips container */}
<div className={`flex flex-wrap ${gapClass} items-center relative shrink-0 w-full`}> <div
className={`flex flex-wrap ${gapClass} items-center relative shrink-0 w-full`}
>
{options.map((option) => ( {options.map((option) => (
<Chip <Chip
key={option.id} key={option.id}
@@ -110,7 +114,9 @@ function MultiSelectView({
</svg> </svg>
{/* Text - only show if addButtonText is provided */} {/* Text - only show if addButtonText is provided */}
{addButtonText && ( {addButtonText && (
<span className={`font-inter font-medium ${isSmall ? "text-[length:var(--sizing-300,12px)] leading-[14px]" : "text-[length:var(--sizing-400,16px)] leading-[20px]"} ${isInverse ? "text-[color:var(--color-content-inverse-primary,black)]" : "text-[color:var(--color-content-default-brand-primary,#fefcc9)]"}`}> <span
className={`font-inter font-medium ${isSmall ? "text-[length:var(--sizing-300,12px)] leading-[14px]" : "text-[length:var(--sizing-400,16px)] leading-[20px]"} ${isInverse ? "text-[color:var(--color-content-inverse-primary,black)]" : "text-[color:var(--color-content-default-brand-primary,#fefcc9)]"}`}
>
{addButtonText} {addButtonText}
</span> </span>
)} )}
@@ -3,7 +3,10 @@
import { memo, useCallback, useId } from "react"; import { memo, useCallback, useId } from "react";
import { RadioButtonView } from "./RadioButton.view"; import { RadioButtonView } from "./RadioButton.view";
import type { RadioButtonProps } from "./RadioButton.types"; import type { RadioButtonProps } from "./RadioButton.types";
import { normalizeMode, normalizeState } from "../../../../lib/propNormalization"; import {
normalizeMode,
normalizeState,
} from "../../../../lib/propNormalization";
const RadioButtonContainer = ({ const RadioButtonContainer = ({
checked = false, checked = false,
@@ -42,7 +45,9 @@ const RadioButtonContainer = ({
duration-200 duration-200
ease-in-out ease-in-out
p-[4px] p-[4px]
`.trim().replace(/\s+/g, " "); `
.trim()
.replace(/\s+/g, " ");
// Get box styles based on mode and checked status per Figma designs // Get box styles based on mode and checked status per Figma designs
const getBoxStyles = (): string => { const getBoxStyles = (): string => {
@@ -75,7 +80,8 @@ const RadioButtonContainer = ({
: "border-[var(--color-border-invert-primary,white)]"; : "border-[var(--color-border-invert-primary,white)]";
// Hover border: inverse brand primary for both selected and unselected per Figma // Hover border: inverse brand primary for both selected and unselected per Figma
const hoverBorder = "hover:border-[var(--color-border-invert-brand-primary,#6c6701)]"; const hoverBorder =
"hover:border-[var(--color-border-invert-brand-primary,#6c6701)]";
// Focus border: when focused and checked, border should be white per Figma // Focus border: when focused and checked, border should be white per Figma
const focusBorder = checked const focusBorder = checked
@@ -3,7 +3,10 @@
import { memo, useCallback, useId } from "react"; import { memo, useCallback, useId } from "react";
import { RadioGroupView } from "./RadioGroup.view"; import { RadioGroupView } from "./RadioGroup.view";
import type { RadioGroupProps } from "./RadioGroup.types"; import type { RadioGroupProps } from "./RadioGroup.types";
import { normalizeMode, normalizeState } from "../../../../lib/propNormalization"; import {
normalizeMode,
normalizeState,
} from "../../../../lib/propNormalization";
const RadioGroupContainer = ({ const RadioGroupContainer = ({
name, name,
@@ -19,7 +22,8 @@ const RadioGroupContainer = ({
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase) // Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const mode = normalizeMode(modeProp); const mode = normalizeMode(modeProp);
// Normalize state, but handle "With Subtext" separately (it's represented by options with subtext) // Normalize state, but handle "With Subtext" separately (it's represented by options with subtext)
const state = typeof stateProp === "string" && const state =
typeof stateProp === "string" &&
(stateProp.toLowerCase() === "with subtext" || stateProp === "With Subtext") (stateProp.toLowerCase() === "with subtext" || stateProp === "With Subtext")
? "default" // "With Subtext" is handled via RadioOption.subtext, use default state ? "default" // "With Subtext" is handled via RadioOption.subtext, use default state
: normalizeState(stateProp); : normalizeState(stateProp);
@@ -24,10 +24,7 @@ export function RadioGroupView({
// If there's subtext, render radio button without label and handle layout separately // If there's subtext, render radio button without label and handle layout separately
if (option.subtext) { if (option.subtext) {
return ( return (
<div <div key={option.value} className="flex gap-[8px] items-start">
key={option.value}
className="flex gap-[8px] items-start"
>
<RadioButton <RadioButton
checked={isSelected} checked={isSelected}
mode={mode} mode={mode}
@@ -16,7 +16,11 @@ import React, {
import { useClickOutside } from "../../../hooks"; import { useClickOutside } from "../../../hooks";
import { SelectInputView } from "./SelectInput.view"; import { SelectInputView } from "./SelectInput.view";
import type { SelectInputProps } from "./SelectInput.types"; import type { SelectInputProps } from "./SelectInput.types";
import { normalizeState, normalizeSmallMediumLargeSize, normalizeLabelVariant } from "../../../../lib/propNormalization"; import {
normalizeState,
normalizeSmallMediumLargeSize,
normalizeLabelVariant,
} from "../../../../lib/propNormalization";
const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>( const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
( (
@@ -46,7 +50,8 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
ref, ref,
) => { ) => {
// Determine if label should be shown // Determine if label should be shown
const shouldShowLabel = showLabel !== undefined ? showLabel : (labelText !== undefined); const shouldShowLabel =
showLabel !== undefined ? showLabel : labelText !== undefined;
// Normalize state - handle "state5" as disabled // Normalize state - handle "state5" as disabled
let normalizedState = externalStateProp; let normalizedState = externalStateProp;
@@ -57,8 +62,12 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase) // Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
// Note: labelVariant and size are normalized for future use but not yet implemented in the view // Note: labelVariant and size are normalized for future use but not yet implemented in the view
const _labelVariant = labelVariantProp ? normalizeLabelVariant(labelVariantProp) : undefined; const _labelVariant = labelVariantProp
const _size = sizeProp ? normalizeSmallMediumLargeSize(sizeProp) : undefined; ? normalizeLabelVariant(labelVariantProp)
: undefined;
const _size = sizeProp
? normalizeSmallMediumLargeSize(sizeProp)
: undefined;
// Mark as intentionally unused for future implementation // Mark as intentionally unused for future implementation
void _labelVariant; void _labelVariant;
void _size; void _size;
@@ -73,11 +82,14 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
// Internal state management: track if focused and how (mouse vs keyboard) // Internal state management: track if focused and how (mouse vs keyboard)
const [isFocused, setIsFocused] = useState(false); const [isFocused, setIsFocused] = useState(false);
const [focusMethod, setFocusMethod] = useState<"mouse" | "keyboard" | null>(null); const [focusMethod, setFocusMethod] = useState<"mouse" | "keyboard" | null>(
null,
);
const wasMouseDownRef = useRef(false); const wasMouseDownRef = useRef(false);
// Determine if we should auto-manage focus (only when state is "default" or undefined) // Determine if we should auto-manage focus (only when state is "default" or undefined)
const shouldAutoManageFocus = externalState === "default" || externalState === undefined; const shouldAutoManageFocus =
externalState === "default" || externalState === undefined;
// Sync internal state with external value prop // Sync internal state with external value prop
useEffect(() => { useEffect(() => {
@@ -7,8 +7,18 @@ export interface SelectOptionData {
import type { StateValue } from "../../../../lib/propNormalization"; import type { StateValue } from "../../../../lib/propNormalization";
export type SelectInputLabelVariantValue = "default" | "horizontal" | "Default" | "Horizontal"; export type SelectInputLabelVariantValue =
export type SelectInputSizeValue = "small" | "medium" | "large" | "Small" | "Medium" | "Large"; | "default"
| "horizontal"
| "Default"
| "Horizontal";
export type SelectInputSizeValue =
| "small"
| "medium"
| "large"
| "Small"
| "Medium"
| "Large";
export interface SelectInputProps { export interface SelectInputProps {
id?: string; id?: string;
@@ -1,4 +1,10 @@
export type SelectOptionSizeValue = "small" | "medium" | "large" | "Small" | "Medium" | "Large"; export type SelectOptionSizeValue =
| "small"
| "medium"
| "large"
| "Small"
| "Medium"
| "Large";
export interface SelectOptionProps { export interface SelectOptionProps {
children?: React.ReactNode; children?: React.ReactNode;
@@ -4,7 +4,12 @@ import { memo, forwardRef } from "react";
import { useComponentId, useFormField } from "../../../hooks"; import { useComponentId, useFormField } from "../../../hooks";
import { TextAreaView } from "./TextArea.view"; import { TextAreaView } from "./TextArea.view";
import type { TextAreaProps } from "./TextArea.types"; import type { TextAreaProps } from "./TextArea.types";
import { normalizeInputState, normalizeSmallMediumLargeSize, normalizeLabelVariant, normalizeTextAreaAppearance } from "../../../../lib/propNormalization"; import {
normalizeInputState,
normalizeSmallMediumLargeSize,
normalizeLabelVariant,
normalizeTextAreaAppearance,
} from "../../../../lib/propNormalization";
const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>( const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>(
( (
@@ -1,9 +1,23 @@
import type { InputStateValue } from "../../../../lib/propNormalization"; import type { InputStateValue } from "../../../../lib/propNormalization";
export type TextAreaSizeValue = "small" | "medium" | "large" | "Small" | "Medium" | "Large"; export type TextAreaSizeValue =
export type TextAreaLabelVariantValue = "default" | "horizontal" | "Default" | "Horizontal"; | "small"
| "medium"
| "large"
| "Small"
| "Medium"
| "Large";
export type TextAreaLabelVariantValue =
| "default"
| "horizontal"
| "Default"
| "Horizontal";
export type TextAreaAppearanceValue = "default" | "embedded" | "Default" | "Embedded"; export type TextAreaAppearanceValue =
| "default"
| "embedded"
| "Default"
| "Embedded";
export interface TextAreaProps extends Omit< export interface TextAreaProps extends Omit<
React.TextareaHTMLAttributes<HTMLTextAreaElement>, React.TextareaHTMLAttributes<HTMLTextAreaElement>,
@@ -4,7 +4,10 @@ import { memo, forwardRef, useState, useRef } from "react";
import { useComponentId, useFormField } from "../../../hooks"; import { useComponentId, useFormField } from "../../../hooks";
import { TextInputView } from "./TextInput.view"; import { TextInputView } from "./TextInput.view";
import type { TextInputProps } from "./TextInput.types"; import type { TextInputProps } from "./TextInput.types";
import { normalizeInputState, normalizeTextInputSize } from "../../../../lib/propNormalization"; import {
normalizeInputState,
normalizeTextInputSize,
} from "../../../../lib/propNormalization";
const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>( const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
( (
@@ -39,12 +42,15 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
// Internal state management: track if focused and how (mouse vs keyboard) // Internal state management: track if focused and how (mouse vs keyboard)
const [isFocused, setIsFocused] = useState(false); const [isFocused, setIsFocused] = useState(false);
const [focusMethod, setFocusMethod] = useState<"mouse" | "keyboard" | null>(null); const [focusMethod, setFocusMethod] = useState<"mouse" | "keyboard" | null>(
null,
);
const wasMouseDownRef = useRef(false); const wasMouseDownRef = useRef(false);
// Determine if we should auto-manage focus (only when state is "default" or undefined) // Determine if we should auto-manage focus (only when state is "default" or undefined)
// If state is "active", "hover", or "focus", respect it and don't override // If state is "active", "hover", or "focus", respect it and don't override
const shouldAutoManageFocus = externalState === "default" || externalState === undefined; const shouldAutoManageFocus =
externalState === "default" || externalState === undefined;
// Determine actual state: // Determine actual state:
// - Active: when clicked (mouse focus) // - Active: when clicked (mouse focus)
@@ -62,7 +68,8 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
const isFilled = Boolean(value && value.trim().length > 0); const isFilled = Boolean(value && value.trim().length > 0);
// Size styles based on inputSize prop // Size styles based on inputSize prop
const sizeStyles = inputSize === "small" const sizeStyles =
inputSize === "small"
? { ? {
input: "h-[32px] px-[10px] py-[6px] text-[14px]", input: "h-[32px] px-[10px] py-[6px] text-[14px]",
label: "text-[12px] leading-[16px] font-medium", label: "text-[12px] leading-[16px] font-medium",
@@ -167,7 +174,9 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
: "text-[var(--color-content-default-tertiary,#b4b4b4)]"; : "text-[var(--color-content-default-tertiary,#b4b4b4)]";
// Form field handlers with disabled state handling // Form field handlers with disabled state handling
const { handleChange, handleBlur } = useFormField<HTMLInputElement>(disabled, { const { handleChange, handleBlur } = useFormField<HTMLInputElement>(
disabled,
{
onChange, onChange,
onBlur: (e) => { onBlur: (e) => {
if (shouldAutoManageFocus) { if (shouldAutoManageFocus) {
@@ -177,7 +186,8 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
} }
onBlur?.(e); onBlur?.(e);
}, },
}); },
);
// Handle mouse down to detect mouse clicks // Handle mouse down to detect mouse clicks
const handleMouseDown = () => { const handleMouseDown = () => {
@@ -3,7 +3,10 @@
import { memo, useCallback, useId, forwardRef } from "react"; import { memo, useCallback, useId, forwardRef } from "react";
import { ToggleGroupView } from "./ToggleGroup.view"; import { ToggleGroupView } from "./ToggleGroup.view";
import type { ToggleGroupProps } from "./ToggleGroup.types"; import type { ToggleGroupProps } from "./ToggleGroup.types";
import { normalizeToggleState, normalizeToggleGroupPosition } from "../../../../lib/propNormalization"; import {
normalizeToggleState,
normalizeToggleGroupPosition,
} from "../../../../lib/propNormalization";
const ToggleGroupContainer = memo( const ToggleGroupContainer = memo(
forwardRef<HTMLButtonElement, ToggleGroupProps>((props, _ref) => { forwardRef<HTMLButtonElement, ToggleGroupProps>((props, _ref) => {
@@ -1,6 +1,12 @@
import type { StateValue } from "../../../../lib/propNormalization"; import type { StateValue } from "../../../../lib/propNormalization";
export type ToggleGroupPositionValue = "left" | "middle" | "right" | "Left" | "Middle" | "Right"; export type ToggleGroupPositionValue =
| "left"
| "middle"
| "right"
| "Left"
| "Middle"
| "Right";
export interface ToggleGroupProps extends Omit< export interface ToggleGroupProps extends Omit<
React.ButtonHTMLAttributes<HTMLButtonElement>, React.ButtonHTMLAttributes<HTMLButtonElement>,
@@ -5,13 +5,7 @@ import UploadView from "./Upload.view";
import type { UploadProps } from "./Upload.types"; import type { UploadProps } from "./Upload.types";
const UploadContainer = memo<UploadProps>( const UploadContainer = memo<UploadProps>(
({ ({ active = true, label, showHelpIcon = true, onClick, className = "" }) => {
active = true,
label,
showHelpIcon = true,
onClick,
className = "",
}) => {
return ( return (
<UploadView <UploadView
active={active} active={active}
@@ -33,7 +33,9 @@ function UploadView({
: "text-[color:var(--color-content-invert-tertiary,#2d2d2d)]"; : "text-[color:var(--color-content-invert-tertiary,#2d2d2d)]";
return ( return (
<div className={`flex flex-col gap-[var(--measures-spacing-300,12px)] items-start relative w-full ${className}`}> <div
className={`flex flex-col gap-[var(--measures-spacing-300,12px)] items-start relative w-full ${className}`}
>
{/* Label using InputLabel component */} {/* Label using InputLabel component */}
{label && ( {label && (
<InputLabel <InputLabel
@@ -92,13 +94,17 @@ function UploadView({
</svg> </svg>
</div> </div>
{/* Button text */} {/* Button text */}
<div className={`flex flex-col font-inter font-medium justify-center leading-[0] relative shrink-0 text-[length:var(--sizing-400,16px)] whitespace-nowrap ${buttonTextColor}`}> <div
className={`flex flex-col font-inter font-medium justify-center leading-[0] relative shrink-0 text-[length:var(--sizing-400,16px)] whitespace-nowrap ${buttonTextColor}`}
>
<p className="leading-[20px]">Upload</p> <p className="leading-[20px]">Upload</p>
</div> </div>
</button> </button>
{/* Description text */} {/* Description text */}
<div className={`flex flex-[1_0_0] flex-col font-inter font-normal h-[32px] justify-center leading-[0] min-h-px min-w-px relative text-[length:var(--sizing-350,14px)] ${descriptionTextColor}`}> <div
className={`flex flex-[1_0_0] flex-col font-inter font-normal h-[32px] justify-center leading-[0] min-h-px min-w-px relative text-[length:var(--sizing-350,14px)] ${descriptionTextColor}`}
>
<p className="leading-[20px] whitespace-pre-wrap"> <p className="leading-[20px] whitespace-pre-wrap">
Add images, PDFs, and other files to the policy Add images, PDFs, and other files to the policy
</p> </p>
+11 -2
View File
@@ -1,7 +1,15 @@
import { memo } from "react"; import { memo } from "react";
import { normalizeSize } from "../../../lib/propNormalization"; import { normalizeSize } from "../../../lib/propNormalization";
export type AvatarSizeValue = "small" | "medium" | "large" | "xlarge" | "Small" | "Medium" | "Large" | "XLarge"; export type AvatarSizeValue =
| "small"
| "medium"
| "large"
| "xlarge"
| "Small"
| "Medium"
| "Large"
| "XLarge";
interface AvatarProps extends React.ImgHTMLAttributes<HTMLImageElement> { interface AvatarProps extends React.ImgHTMLAttributes<HTMLImageElement> {
src: string; src: string;
@@ -19,7 +27,8 @@ const Avatar = memo<AvatarProps>(
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase) // Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const size = normalizeSize(sizeProp, "small"); const size = normalizeSize(sizeProp, "small");
const sizeStyles: Record<string, string> = { const sizeStyles: Record<string, string> = {
small: "w-[var(--spacing-scale-016)] h-[var(--spacing-scale-016)] border-[1.5px] border-[#FFFFFF4D] border-solid", small:
"w-[var(--spacing-scale-016)] h-[var(--spacing-scale-016)] border-[1.5px] border-[#FFFFFF4D] border-solid",
medium: "w-[var(--spacing-scale-018)] h-[var(--spacing-scale-018)]", medium: "w-[var(--spacing-scale-018)] h-[var(--spacing-scale-018)]",
large: "w-[var(--spacing-scale-024)] h-[var(--spacing-scale-024)]", large: "w-[var(--spacing-scale-024)] h-[var(--spacing-scale-024)]",
xlarge: "w-[var(--spacing-scale-032)] h-[var(--spacing-scale-032)]", xlarge: "w-[var(--spacing-scale-032)] h-[var(--spacing-scale-032)]",
-115
View File
@@ -1,115 +0,0 @@
import { memo } from "react";
import Link from "next/link";
import { getAssetPath, ASSETS } from "../../../lib/assetUtils";
interface LogoProps {
size?:
| "default"
| "footer"
| "createFlow"
| "topNavFolderTop"
| "topNavHeader";
showText?: boolean;
}
interface SizeConfig {
containerHeight: string;
gap: string;
textSize: string;
lineHeight: string;
iconSize: string;
}
const Logo = memo<LogoProps>(({ size = "default", showText = true }) => {
// Size configurations
const sizes: Record<string, SizeConfig> = {
default: {
containerHeight: "h-[41px]",
gap: "gap-[8.28px]",
textSize: "text-[21.97px]",
lineHeight: "leading-[27.05px]",
iconSize: "w-[27.05px] h-[27.05px]",
},
footer: {
containerHeight: "h-[41px] sm:h-[calc(40px*1.37)] lg:h-[calc(40px*2.05)]",
gap: "gap-[8.28px] sm:gap-[calc(8px*1.37)] lg:gap-[calc(8px*2.05)]",
textSize: "text-[21.97px] sm:text-[calc(21.97px*1.37)] lg:text-[calc(21.97px*2.05)]",
lineHeight: "leading-[27.05px] sm:leading-[calc(27.05px*1.37)] lg:leading-[calc(27.05px*2.05)]",
iconSize: "w-[27.05px] h-[27.05px] sm:w-[calc(27.05px*1.37)] sm:h-[calc(27.05px*1.37)] lg:w-[calc(27.05px*2.05)] lg:h-[calc(27.05px*2.05)]",
},
createFlow: {
containerHeight: "h-[30px] md:h-[41px]",
gap: "gap-[6px] md:gap-[8.28px]",
textSize: "text-[16.48px] md:text-[21.97px]",
lineHeight: "leading-[20.28px] md:leading-[27.05px]",
iconSize: "w-[20.28px] h-[20.28px] md:w-[27.05px] md:h-[27.05px]",
},
topNavFolderTop: {
containerHeight: "h-[14.11px] sm:h-[21.06px] md:h-[32.24px] lg:h-[28px] xl:h-[36px]",
gap: "gap-0 sm:gap-[3.19px] md:gap-[4.89px] lg:gap-[6.55px] xl:gap-[8.64px]",
textSize: "text-[11.57px] sm:text-[11.69px] md:text-[17.89px] lg:text-[21.97px] xl:text-[29.01px]",
lineHeight: "leading-[14.24px] sm:leading-[14.39px] md:leading-[22.02px] lg:leading-[27.05px] xl:leading-[35.7px]",
iconSize: "w-[14.11px] h-[14.11px] sm:w-[14.39px] sm:h-[14.39px] md:w-[22.02px] md:h-[22.02px] lg:w-[27.05px] lg:h-[27.05px] xl:w-[35.7px] xl:h-[35.7px]",
},
topNavHeader: {
containerHeight: "h-[20.85px] sm:h-[20.85px] md:h-[17.91px] lg:h-[28px] xl:h-[34px]",
gap: "gap-0 sm:gap-[4.21px] md:gap-[6.51px] lg:gap-[6.55px] xl:gap-[8.19px]",
textSize: "text-[11.57px] sm:text-[11.57px] md:text-[17.89px] lg:text-[21.97px] xl:text-[27.47px]",
lineHeight: "leading-[14.24px] sm:leading-[14.24px] md:leading-[22.02px] lg:leading-[27.05px] xl:leading-[33.81px]",
iconSize: "w-[14.24px] h-[14.24px] sm:w-[14.24px] sm:h-[14.24px] md:w-[22.02px] md:h-[22.02px] lg:w-[27.05px] lg:h-[27.05px] xl:w-[33.81px] xl:h-[33.81px]",
},
};
const config = sizes[size || "default"] || sizes.default;
return (
<Link href="/" className="block" aria-label="CommunityRule Logo">
<div
className={`flex items-center ${config.containerHeight} ${
showText ? config.gap : ""
} transition-all duration-200 ease-in-out hover:scale-[1.02] cursor-pointer`}
>
{/* Logo Text - responsive visibility for topNav sizes */}
<div
className={`font-bricolage-grotesque ${
size === "topNavFolderTop"
? "text-[var(--color-content-inverse-primary)]"
: "text-[var(--color-content-default-primary)]"
} ${config.textSize} ${
config.lineHeight
} font-normal tracking-[0px] transition-colors duration-200 ${
size === "topNavFolderTop" || size === "topNavHeader"
? showText
? "hidden sm:block"
: "hidden"
: showText
? ""
: "hidden"
}`}
aria-label="CommunityRule"
>
CommunityRule
</div>
{/* Vector Icon */}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={getAssetPath(ASSETS.LOGO)}
alt="CommunityRule Logo Icon"
width={27.05}
height={27.05}
className={`flex-shrink-0 ${
config.iconSize
} transition-all duration-200 ${
size === "topNavFolderTop" ? "filter brightness-0" : ""
}`}
aria-hidden="true"
/>
</div>
</Link>
);
});
Logo.displayName = "Logo";
export default Logo;
@@ -3,7 +3,10 @@
import { memo } from "react"; import { memo } from "react";
import { AlertView } from "./Alert.view"; import { AlertView } from "./Alert.view";
import type { AlertProps } from "./Alert.types"; import type { AlertProps } from "./Alert.types";
import { normalizeAlertStatus, normalizeAlertType } from "../../../../lib/propNormalization"; import {
normalizeAlertStatus,
normalizeAlertType,
} from "../../../../lib/propNormalization";
const AlertContainer = memo<AlertProps>( const AlertContainer = memo<AlertProps>(
({ ({
+1 -1
View File
@@ -56,7 +56,7 @@ export function CreateView({
{/* Header: custom headerContent (when provided) or default title/description */} {/* Header: custom headerContent (when provided) or default title/description */}
{headerContent !== undefined ? ( {headerContent !== undefined ? (
<div className="shrink-0">{headerContent}</div> <div className="shrink-0">{headerContent}</div>
) : (title || description) ? ( ) : title || description ? (
<div className="bg-[var(--color-surface-default-primary)] px-[24px] py-[12px] shrink-0"> <div className="bg-[var(--color-surface-default-primary)] px-[24px] py-[12px] shrink-0">
<ContentLockup <ContentLockup
title={title} title={title}
@@ -6,7 +6,13 @@ import type { TooltipProps } from "./Tooltip.types";
import { normalizeTooltipPosition } from "../../../../lib/propNormalization"; import { normalizeTooltipPosition } from "../../../../lib/propNormalization";
const TooltipContainer = memo<TooltipProps>( const TooltipContainer = memo<TooltipProps>(
({ children, text, position: positionProp = "top", className = "", disabled = false }) => { ({
children,
text,
position: positionProp = "top",
className = "",
disabled = false,
}) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase) // Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const position = normalizeTooltipPosition(positionProp); const position = normalizeTooltipPosition(positionProp);
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
+2 -2
View File
@@ -3,7 +3,7 @@
import { memo } from "react"; import { memo } from "react";
import { useTranslation } from "../../contexts/MessagesContext"; import { useTranslation } from "../../contexts/MessagesContext";
import Link from "next/link"; import Link from "next/link";
import Logo from "../icons/Logo"; import Logo from "../asset/logo";
import Separator from "../utility/Separator"; import Separator from "../utility/Separator";
import { getAssetPath, ASSETS } from "../../../lib/assetUtils"; import { getAssetPath, ASSETS } from "../../../lib/assetUtils";
@@ -40,7 +40,7 @@ const Footer = memo(() => {
lg:gap-[var(--spacing-measures-spacing-060,60px)]" lg:gap-[var(--spacing-measures-spacing-060,60px)]"
> >
{/* Logo */} {/* Logo */}
<Logo size="footer" /> <Logo size="footer" wordmark />
{/* Content section */} {/* Content section */}
<div className="flex flex-col items-start w-full gap-[var(--spacing-measures-spacing-048,48px)] sm:flex-row sm:justify-between sm:gap-0"> <div className="flex flex-col items-start w-full gap-[var(--spacing-measures-spacing-048,48px)] sm:flex-row sm:justify-between sm:gap-0">
@@ -32,11 +32,17 @@ const MenuBarItemContainer = memo<MenuBarItemProps>(
"X Small" | "Small" | "Medium" | "Large" | "X Large", "X Small" | "Small" | "Medium" | "Large" | "X Large",
string string
> = { > = {
"X Small": reducedPadding ? "px-[var(--spacing-scale-004)] py-[var(--spacing-scale-002)]" : "px-[var(--spacing-scale-004)] py-[var(--spacing-scale-002)]", "X Small": reducedPadding
? "px-[var(--spacing-scale-004)] py-[var(--spacing-scale-002)]"
: "px-[var(--spacing-scale-004)] py-[var(--spacing-scale-002)]",
Small: "px-[var(--spacing-scale-004)] py-[var(--spacing-scale-002)]", Small: "px-[var(--spacing-scale-004)] py-[var(--spacing-scale-002)]",
Medium: reducedPadding ? "px-[var(--spacing-scale-002)] py-[var(--spacing-scale-008)] h-[32px]" : "px-[var(--spacing-scale-008)] py-[var(--spacing-scale-008)] h-[32px]", Medium: reducedPadding
Large: "px-[var(--spacing-scale-016)] py-[var(--spacing-scale-016)] h-[44px]", ? "px-[var(--spacing-scale-002)] py-[var(--spacing-scale-008)] h-[32px]"
"X Large": "px-[var(--spacing-scale-016)] py-[var(--spacing-scale-016)] h-[44px]", : "px-[var(--spacing-scale-008)] py-[var(--spacing-scale-008)] h-[32px]",
Large:
"px-[var(--spacing-scale-016)] py-[var(--spacing-scale-016)] h-[44px]",
"X Large":
"px-[var(--spacing-scale-016)] py-[var(--spacing-scale-016)] h-[44px]",
}; };
// Text styles based on Figma specifications // Text styles based on Figma specifications
@@ -46,21 +52,16 @@ const MenuBarItemContainer = memo<MenuBarItemProps>(
> = { > = {
"X Small": "X Small":
"font-inter text-[10px] leading-[12px] font-medium tracking-[0%]", "font-inter text-[10px] leading-[12px] font-medium tracking-[0%]",
Small: Small: "font-inter text-[12px] leading-[14px] font-medium tracking-[0%]",
"font-inter text-[12px] leading-[14px] font-medium tracking-[0%]", Medium: "font-inter text-[12px] leading-[14px] font-medium tracking-[0%]",
Medium: Large: "font-inter text-[16px] leading-[20px] font-medium tracking-[0%]",
"font-inter text-[12px] leading-[14px] font-medium tracking-[0%]",
Large:
"font-inter text-[16px] leading-[20px] font-medium tracking-[0%]",
"X Large": "X Large":
"font-inter text-[24px] leading-[28px] font-normal tracking-[0%]", "font-inter text-[24px] leading-[28px] font-normal tracking-[0%]",
}; };
// State styles for Default mode (yellow text on dark background) // State styles for Default mode (yellow text on dark background)
const defaultModeStyles: Record< const defaultModeStyles: Record<"default" | "hover" | "selected", string> =
"default" | "hover" | "selected", {
string
> = {
default: default:
"bg-transparent text-[var(--color-content-default-brand-primary,#fefcc9)] hover:bg-[var(--color-gray-800)] hover:text-[var(--color-content-default-brand-primary,#fefcc9)]", "bg-transparent text-[var(--color-content-default-brand-primary,#fefcc9)] hover:bg-[var(--color-gray-800)] hover:text-[var(--color-content-default-brand-primary,#fefcc9)]",
hover: hover:
@@ -70,10 +71,8 @@ const MenuBarItemContainer = memo<MenuBarItemProps>(
}; };
// State styles for Inverse mode (black text on yellow background) // State styles for Inverse mode (black text on yellow background)
const inverseModeStyles: Record< const inverseModeStyles: Record<"default" | "hover" | "selected", string> =
"default" | "hover" | "selected", {
string
> = {
default: default:
"bg-transparent text-[var(--color-content-inverse-primary,black)] hover:bg-[var(--color-surface-brand-accent,#4d4a00)] hover:text-[var(--color-content-inverse-primary,black)]", "bg-transparent text-[var(--color-content-inverse-primary,black)] hover:bg-[var(--color-surface-brand-accent,#4d4a00)] hover:text-[var(--color-content-inverse-primary,black)]",
hover: hover:
@@ -5,14 +5,9 @@ export type MenuBarItemSizeValue =
| "Large" | "Large"
| "X Large"; | "X Large";
export type MenuBarItemStateValue = export type MenuBarItemStateValue = "default" | "hover" | "selected";
| "default"
| "hover"
| "selected";
export type MenuBarItemModeValue = export type MenuBarItemModeValue = "default" | "inverse";
| "default"
| "inverse";
export interface MenuBarItemProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> { export interface MenuBarItemProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
href?: string; href?: string;
@@ -3,7 +3,10 @@
import { memo } from "react"; import { memo } from "react";
import NavigationItemView from "./NavigationItem.view"; import NavigationItemView from "./NavigationItem.view";
import type { NavigationItemProps } from "./NavigationItem.types"; import type { NavigationItemProps } from "./NavigationItem.types";
import { normalizeNavigationItemVariant, normalizeNavigationItemSize } from "../../../../lib/propNormalization"; import {
normalizeNavigationItemVariant,
normalizeNavigationItemSize,
} from "../../../../lib/propNormalization";
const NavigationItemContainer = memo<NavigationItemProps>( const NavigationItemContainer = memo<NavigationItemProps>(
({ ({
@@ -1,5 +1,9 @@
export type NavigationItemVariantValue = "default" | "Default"; export type NavigationItemVariantValue = "default" | "Default";
export type NavigationItemSizeValue = "default" | "xsmall" | "Default" | "XSmall"; export type NavigationItemSizeValue =
| "default"
| "xsmall"
| "Default"
| "XSmall";
export interface NavigationItemProps extends Omit< export interface NavigationItemProps extends Omit<
React.AnchorHTMLAttributes<HTMLAnchorElement>, React.AnchorHTMLAttributes<HTMLAnchorElement>,
@@ -7,7 +7,7 @@ import { getAssetPath } from "../../../../lib/assetUtils";
import MenuBar from "../MenuBar"; import MenuBar from "../MenuBar";
import type { TopNavViewProps } from "./TopNav.types"; import type { TopNavViewProps } from "./TopNav.types";
import Logo from "../../icons/Logo"; import Logo from "../../asset/logo";
function TopNavView({ function TopNavView({
folderTop, folderTop,
@@ -44,7 +44,11 @@ function TopNavView({
{/* Header Tab - Yellow tab container with decorative Union images */} {/* Header Tab - Yellow tab container with decorative Union images */}
<div className="HeaderTab header-breakpoint-transition relative bg-[var(--color-surface-inverse-brand-primary)] rounded-tl-[var(--radius-measures-radius-medium)] rounded-tr-[var(--radius-measures-radius-medium)] sm:rounded-t-[var(--radius-measures-radius-xlarge)] md:rounded-t-[var(--radius-measures-radius-xlarge)] lg:rounded-t-[var(--radius-measures-radius-xlarge)] xl:rounded-t-[var(--radius-measures-radius-xlarge)] pl-[var(--spacing-scale-012)] pr-[var(--spacing-scale-048)] h-[var(--spacing-scale-040)] sm:pl-[var(--spacing-scale-012)] sm:h-[52px] sm:pr-[var(--spacing-scale-006)] md:h-[52px] md:pl-[var(--spacing-scale-024)] md:pr-[var(--spacing-scale-012)] lg:h-[52px] lg:pl-[var(--spacing-scale-024)] lg:pr-[var(--spacing-scale-048)] xl:h-[64px] xl:pl-[var(--spacing-scale-032)] xl:pr-[var(--spacing-scale-120)] md:gap-[var(--spacing-scale-032)] flex-1 min-w-0 min-w-[197px] sm:min-w-0 sm:mr-[var(--spacing-scale-008)] md:mr-[185px] lg:mr-[var(--spacing-scale-024)] xl:mr-[var(--spacing-scale-032)] flex items-center self-end"> <div className="HeaderTab header-breakpoint-transition relative bg-[var(--color-surface-inverse-brand-primary)] rounded-tl-[var(--radius-measures-radius-medium)] rounded-tr-[var(--radius-measures-radius-medium)] sm:rounded-t-[var(--radius-measures-radius-xlarge)] md:rounded-t-[var(--radius-measures-radius-xlarge)] lg:rounded-t-[var(--radius-measures-radius-xlarge)] xl:rounded-t-[var(--radius-measures-radius-xlarge)] pl-[var(--spacing-scale-012)] pr-[var(--spacing-scale-048)] h-[var(--spacing-scale-040)] sm:pl-[var(--spacing-scale-012)] sm:h-[52px] sm:pr-[var(--spacing-scale-006)] md:h-[52px] md:pl-[var(--spacing-scale-024)] md:pr-[var(--spacing-scale-012)] lg:h-[52px] lg:pl-[var(--spacing-scale-024)] lg:pr-[var(--spacing-scale-048)] xl:h-[64px] xl:pl-[var(--spacing-scale-032)] xl:pr-[var(--spacing-scale-120)] md:gap-[var(--spacing-scale-032)] flex-1 min-w-0 min-w-[197px] sm:min-w-0 sm:mr-[var(--spacing-scale-008)] md:mr-[185px] lg:mr-[var(--spacing-scale-024)] xl:mr-[var(--spacing-scale-032)] flex items-center self-end">
{/* Logo - Consistent left positioning within HeaderTab */} {/* Logo - Consistent left positioning within HeaderTab */}
<Logo size={logoSize} showText={true} /> <Logo
size={logoSize}
wordmark
palette={folderTop ? "inverse" : "default"}
/>
{/* XSmall menu bar - positioned next to logo */} {/* XSmall menu bar - positioned next to logo */}
<div className="block sm:hidden -me-[2px]"> <div className="block sm:hidden -me-[2px]">
@@ -90,7 +94,9 @@ function TopNavView({
{/* 640-1023px (md: breakpoint): MenuBar Small */} {/* 640-1023px (md: breakpoint): MenuBar Small */}
<div className="hidden md:block lg:hidden"> <div className="hidden md:block lg:hidden">
<MenuBar size="Small">{renderNavigationItems("homeMd")}</MenuBar> <MenuBar size="Small">
{renderNavigationItems("homeMd")}
</MenuBar>
</div> </div>
{/* 1024-1440px (lg: breakpoint): MenuBar Large */} {/* 1024-1440px (lg: breakpoint): MenuBar Large */}
@@ -161,7 +167,11 @@ function TopNavView({
aria-label={t("ariaLabels.mainNavigation")} aria-label={t("ariaLabels.mainNavigation")}
> >
{/* Logo - Consistent left positioning across all breakpoints */} {/* Logo - Consistent left positioning across all breakpoints */}
<Logo size={logoSize} showText={true} /> <Logo
size={logoSize}
wordmark
palette={folderTop ? "inverse" : "default"}
/>
{/* Navigation Links - Consistent center positioning */} {/* Navigation Links - Consistent center positioning */}
<div className="flex items-center flex-1 justify-end sm:flex-none sm:justify-center"> <div className="flex items-center flex-1 justify-end sm:flex-none sm:justify-center">
@@ -193,7 +203,9 @@ function TopNavView({
</div> </div>
<div className="hidden xl:block" data-testid="nav-xl"> <div className="hidden xl:block" data-testid="nav-xl">
<MenuBar size="X Large">{renderNavigationItems("xlarge")}</MenuBar> <MenuBar size="X Large">
{renderNavigationItems("xlarge")}
</MenuBar>
</div> </div>
</div> </div>
@@ -44,7 +44,9 @@ const AskOrganizerContainer = memo<AskOrganizerProps>(
onContactClick, onContactClick,
}) => { }) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase) // Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const variant = normalizeAskOrganizerVariant(variantProp) as AskOrganizerVariant; const variant = normalizeAskOrganizerVariant(
variantProp,
) as AskOrganizerVariant;
const t = useTranslation(); const t = useTranslation();
const defaultButtonText = buttonText ?? t("askOrganizer.buttonText"); const defaultButtonText = buttonText ?? t("askOrganizer.buttonText");
const defaultButtonHref = buttonHref ?? t("askOrganizer.buttonHref"); const defaultButtonHref = buttonHref ?? t("askOrganizer.buttonHref");
@@ -0,0 +1,16 @@
export interface CommunityRuleDocumentEntry {
title: string;
body: string;
}
export interface CommunityRuleDocumentSection {
categoryName: string;
entries: CommunityRuleDocumentEntry[];
}
export interface CommunityRuleDocumentProps {
sections: CommunityRuleDocumentSection[];
className?: string;
/** When true, wrap in white background with left teal bar (small breakpoint). */
useCardStyle?: boolean;
}
@@ -0,0 +1,65 @@
"use client";
import { memo } from "react";
import type { CommunityRuleDocumentProps } from "./CommunityRuleDocument.types";
const SECTION_GAP = "var(--measures-spacing-1200, 64px)";
const TEAL_BG = "var(--color-teal-teal50, #c9fef9)";
const SECTION_LINE_COLOR = "var(--color-border-default-tertiary, #464646)";
function CommunityRuleDocumentView({
sections,
className = "",
useCardStyle = false,
}: CommunityRuleDocumentProps) {
const rootClass = useCardStyle
? `rounded-[12px] bg-white pl-3 border-l-4 ${className}`
: className;
const rootStyle = useCardStyle ? { borderLeftColor: TEAL_BG } : undefined;
const sectionLineStyle = useCardStyle
? undefined
: { borderLeftColor: SECTION_LINE_COLOR };
return (
<div
className={`flex flex-col min-w-0 ${rootClass}`}
style={{ gap: SECTION_GAP, ...rootStyle }}
>
{sections.map((section, sectionIndex) => (
<div
key={sectionIndex}
className={`flex flex-col min-w-0 ${!useCardStyle ? "border-l pl-3" : ""}`}
style={sectionLineStyle}
>
{/* Section content: line runs full height of this block via border-left */}
<div className="flex flex-1 flex-col gap-4 min-w-0">
<p className="font-inter font-medium text-[16px] leading-[20px] text-[var(--color-content-invert-secondary,#1f1f1f)] shrink-0">
{section.categoryName}
</p>
<div className="flex flex-col min-w-0" style={{ gap: "24px" }}>
{section.entries.map((entry, entryIndex) => (
<div
key={entryIndex}
className="flex flex-col min-w-0"
style={{ gap: "6px" }}
>
<p className="font-inter font-bold text-[20px] leading-[28px] text-[var(--color-content-invert-primary)] shrink-0">
{entry.title}
</p>
<p className="font-inter font-normal text-[14px] leading-[20px] text-[var(--color-content-invert-primary)] shrink-0">
{entry.body}
</p>
</div>
))}
</div>
</div>
</div>
))}
</div>
);
}
CommunityRuleDocumentView.displayName = "CommunityRuleDocumentView";
export default memo(CommunityRuleDocumentView);
@@ -0,0 +1,2 @@
export { default } from "./CommunityRuleDocument.view";
export type { CommunityRuleDocumentProps } from "./CommunityRuleDocument.types";
+5 -1
View File
@@ -3,7 +3,11 @@
import { memo } from "react"; import { memo } from "react";
import { normalizeSectionHeaderVariant } from "../../../lib/propNormalization"; import { normalizeSectionHeaderVariant } from "../../../lib/propNormalization";
export type SectionHeaderVariantValue = "default" | "multi-line" | "Default" | "Multi-Line"; export type SectionHeaderVariantValue =
| "default"
| "multi-line"
| "Default"
| "Multi-Line";
interface SectionHeaderProps { interface SectionHeaderProps {
title: string; title: string;
@@ -3,7 +3,10 @@
import { memo } from "react"; import { memo } from "react";
import ContentLockupView from "./ContentLockup.view"; import ContentLockupView from "./ContentLockup.view";
import type { ContentLockupProps, VariantStyle } from "./ContentLockup.types"; import type { ContentLockupProps, VariantStyle } from "./ContentLockup.types";
import { normalizeContentLockupVariant, normalizeAlignment } from "../../../../lib/propNormalization"; import {
normalizeContentLockupVariant,
normalizeAlignment,
} from "../../../../lib/propNormalization";
const ContentLockupContainer = memo<ContentLockupProps>( const ContentLockupContainer = memo<ContentLockupProps>(
({ ({
@@ -96,19 +96,32 @@ function ContentLockupView({
<div className="flex justify-start"> <div className="flex justify-start">
{/* Small button for xsm and sm breakpoints */} {/* Small button for xsm and sm breakpoints */}
<div className="block md:hidden"> <div className="block md:hidden">
<Button buttonType="filled" palette={variant === "hero" ? "default" : "inverse"} size="small"> <Button
buttonType="filled"
palette={variant === "hero" ? "default" : "inverse"}
size="small"
>
{ctaText} {ctaText}
</Button> </Button>
</div> </div>
{/* Large button for md and lg breakpoints */} {/* Large button for md and lg breakpoints */}
<div className="hidden md:block xl:hidden"> <div className="hidden md:block xl:hidden">
<Button buttonType="filled" palette={variant === "hero" ? "default" : "inverse"} size="large" className={buttonClassName}> <Button
buttonType="filled"
palette={variant === "hero" ? "default" : "inverse"}
size="large"
className={buttonClassName}
>
{ctaText} {ctaText}
</Button> </Button>
</div> </div>
{/* XLarge button for xl breakpoint */} {/* XLarge button for xl breakpoint */}
<div className="hidden xl:block"> <div className="hidden xl:block">
<Button buttonType="filled" palette={variant === "hero" ? "default" : "inverse"} size="xlarge"> <Button
buttonType="filled"
palette={variant === "hero" ? "default" : "inverse"}
size="xlarge"
>
{ctaText} {ctaText}
</Button> </Button>
</div> </div>
@@ -6,6 +6,7 @@ import type { HeaderLockupProps } from "./HeaderLockup.types";
import { import {
normalizeHeaderLockupJustification, normalizeHeaderLockupJustification,
normalizeHeaderLockupSize, normalizeHeaderLockupSize,
normalizeHeaderLockupPalette,
} from "../../../../lib/propNormalization"; } from "../../../../lib/propNormalization";
const HeaderLockupContainer = memo<HeaderLockupProps>( const HeaderLockupContainer = memo<HeaderLockupProps>(
@@ -14,10 +15,12 @@ const HeaderLockupContainer = memo<HeaderLockupProps>(
description, description,
justification: justificationProp = "left", justification: justificationProp = "left",
size: sizeProp = "L", size: sizeProp = "L",
palette: paletteProp = "default",
}) => { }) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase) // Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const justification = normalizeHeaderLockupJustification(justificationProp); const justification = normalizeHeaderLockupJustification(justificationProp);
const size = normalizeHeaderLockupSize(sizeProp); const size = normalizeHeaderLockupSize(sizeProp);
const palette = normalizeHeaderLockupPalette(paletteProp);
return ( return (
<HeaderLockupView <HeaderLockupView
@@ -25,6 +28,7 @@ const HeaderLockupContainer = memo<HeaderLockupProps>(
description={description} description={description}
justification={justification} justification={justification}
size={size} size={size}
palette={palette}
/> />
); );
}, },
@@ -1,5 +1,14 @@
export type HeaderLockupJustificationValue = "left" | "center" | "Left" | "Center"; export type HeaderLockupJustificationValue =
| "left"
| "center"
| "Left"
| "Center";
export type HeaderLockupSizeValue = "L" | "M" | "l" | "m"; export type HeaderLockupSizeValue = "L" | "M" | "l" | "m";
export type HeaderLockupPaletteValue =
| "default"
| "inverse"
| "Default"
| "Inverse";
export interface HeaderLockupProps { export interface HeaderLockupProps {
/** /**
@@ -20,6 +29,11 @@ export interface HeaderLockupProps {
* Figma uses PascalCase, codebase uses lowercase - both are supported. * Figma uses PascalCase, codebase uses lowercase - both are supported.
*/ */
size?: HeaderLockupSizeValue; size?: HeaderLockupSizeValue;
/**
* Palette. Default = light text (dark bg); Inverse = dark text (light bg).
* Accepts both PascalCase (Figma) and lowercase (codebase).
*/
palette?: HeaderLockupPaletteValue;
} }
export interface HeaderLockupViewProps { export interface HeaderLockupViewProps {
@@ -27,4 +41,5 @@ export interface HeaderLockupViewProps {
description?: string; description?: string;
justification: "left" | "center"; justification: "left" | "center";
size: "L" | "M"; size: "L" | "M";
palette: "default" | "inverse";
} }
@@ -8,9 +8,18 @@ function HeaderLockupView({
description, description,
justification, justification,
size, size,
palette,
}: HeaderLockupViewProps) { }: HeaderLockupViewProps) {
const isL = size === "L"; const isL = size === "L";
const isLeft = justification === "left"; const isLeft = justification === "left";
const isInverse = palette === "inverse";
const titleColorClass = isInverse
? "text-[var(--color-content-invert-primary)]"
: "text-[var(--color-content-default-primary,white)]";
const descriptionColorClass = isInverse
? "text-[#2d2d2d]"
: "text-[var(--color-content-default-tertiary,#b4b4b4)]";
return ( return (
<div <div
@@ -21,7 +30,7 @@ function HeaderLockupView({
{/* Title */} {/* Title */}
<div className="flex items-center relative shrink-0 w-full"> <div className="flex items-center relative shrink-0 w-full">
<h1 <h1
className={`flex-[1_0_0] min-h-px min-w-px overflow-hidden relative text-[var(--color-content-default-primary,white)] text-ellipsis whitespace-pre-wrap ${ className={`flex-[1_0_0] min-h-px min-w-px overflow-hidden relative ${titleColorClass} text-ellipsis whitespace-pre-wrap ${
isLeft ? "text-left" : "text-center" isLeft ? "text-left" : "text-center"
} ${ } ${
isL isL
@@ -36,12 +45,10 @@ function HeaderLockupView({
{/* Description */} {/* Description */}
{description && ( {description && (
<p <p
className={`font-inter font-normal max-w-[640px] overflow-hidden relative shrink-0 text-[var(--color-content-default-tertiary,#b4b4b4)] text-ellipsis w-full whitespace-pre-wrap ${ className={`font-inter font-normal max-w-[640px] overflow-hidden relative shrink-0 ${descriptionColorClass} text-ellipsis w-full whitespace-pre-wrap ${
isLeft ? "" : "text-center" isLeft ? "" : "text-center"
} ${ } ${
isL isL ? "text-[18px] leading-[1.3]" : "text-[14px] leading-[20px]"
? "text-[18px] leading-[1.3]"
: "text-[14px] leading-[20px]"
}`} }`}
> >
{description} {description}
+9 -1
View File
@@ -1,7 +1,15 @@
import { memo } from "react"; import { memo } from "react";
import { normalizeSize } from "../../../lib/propNormalization"; import { normalizeSize } from "../../../lib/propNormalization";
export type AvatarContainerSizeValue = "small" | "medium" | "large" | "xlarge" | "Small" | "Medium" | "Large" | "XLarge"; export type AvatarContainerSizeValue =
| "small"
| "medium"
| "large"
| "xlarge"
| "Small"
| "Medium"
| "Large"
| "XLarge";
interface AvatarContainerProps extends React.HTMLAttributes<HTMLDivElement> { interface AvatarContainerProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode; children?: React.ReactNode;
@@ -24,7 +24,9 @@ const CardStackContainer = memo<CardStackProps>(
className = "", className = "",
}) => { }) => {
const [internalExpanded, setInternalExpanded] = useState(false); const [internalExpanded, setInternalExpanded] = useState(false);
const [internalSelectedIds, setInternalSelectedIds] = useState<string[]>([]); const [internalSelectedIds, setInternalSelectedIds] = useState<string[]>(
[],
);
const expanded = const expanded =
controlledExpanded !== undefined ? controlledExpanded : internalExpanded; controlledExpanded !== undefined ? controlledExpanded : internalExpanded;
@@ -41,7 +43,9 @@ const CardStackContainer = memo<CardStackProps>(
controlledSelectedIds !== undefined controlledSelectedIds !== undefined
? controlledSelectedIds ? controlledSelectedIds
: controlledSelectedId !== undefined : controlledSelectedId !== undefined
? (controlledSelectedId ? [controlledSelectedId] : []) ? controlledSelectedId
? [controlledSelectedId]
: []
: internalSelectedIds; : internalSelectedIds;
const handleCardSelect = useCallback( const handleCardSelect = useCallback(
@@ -36,9 +36,7 @@ export function CreateFlowFooterView({
</Button> </Button>
{/* Second Button - Right */} {/* Second Button - Right */}
{secondButton && ( {secondButton && <div className="flex-shrink-0">{secondButton}</div>}
<div className="flex-shrink-0">{secondButton}</div>
)}
</div> </div>
</footer> </footer>
); );
@@ -15,6 +15,7 @@ const CreateFlowTopNavContainer = memo<CreateFlowTopNavProps>(
onExport, onExport,
onEdit, onEdit,
onExit, onExit,
buttonPalette,
className = "", className = "",
}) => { }) => {
const router = useRouter(); const router = useRouter();
@@ -38,6 +39,7 @@ const CreateFlowTopNavContainer = memo<CreateFlowTopNavProps>(
onExport={onExport} onExport={onExport}
onEdit={onEdit} onEdit={onEdit}
onExit={handleExit} onExit={handleExit}
buttonPalette={buttonPalette}
className={className} className={className}
/> />
); );
@@ -42,6 +42,11 @@ export interface CreateFlowTopNavProps {
* Callback when Exit/Save & Exit button is clicked * Callback when Exit/Save & Exit button is clicked
*/ */
onExit?: () => void; onExit?: () => void;
/**
* Palette for nav buttons (e.g. "inverse" on completed page to match teal background)
* @default "default"
*/
buttonPalette?: "default" | "inverse";
/** /**
* Additional CSS classes * Additional CSS classes
*/ */
@@ -1,4 +1,4 @@
import Logo from "../../icons/Logo"; import Logo from "../../asset/logo";
import Button from "../../buttons/Button"; import Button from "../../buttons/Button";
import type { CreateFlowTopNavProps } from "./CreateFlowTopNav.types"; import type { CreateFlowTopNavProps } from "./CreateFlowTopNav.types";
@@ -11,6 +11,7 @@ export function CreateFlowTopNavView({
onExport, onExport,
onEdit, onEdit,
onExit, onExit,
buttonPalette = "default",
className = "", className = "",
}: CreateFlowTopNavProps) { }: CreateFlowTopNavProps) {
const exitButtonText = loggedIn ? "Save & Exit" : "Exit"; const exitButtonText = loggedIn ? "Save & Exit" : "Exit";
@@ -27,14 +28,14 @@ export function CreateFlowTopNavView({
aria-label="Create Flow Navigation" aria-label="Create Flow Navigation"
> >
{/* Logo - Left */} {/* Logo - Left */}
<Logo size="createFlow" showText={true} /> <Logo size="createFlow" wordmark palette={buttonPalette} />
{/* Button Group - Right */} {/* Button Group - Right */}
<div className="flex items-center gap-[var(--spacing-scale-012,12px)]"> <div className="flex items-center gap-[var(--spacing-scale-012,12px)]">
{hasShare && ( {hasShare && (
<Button <Button
buttonType="outline" buttonType="outline"
palette="default" palette={buttonPalette}
size="xsmall" size="xsmall"
onClick={onShare} onClick={onShare}
ariaLabel="Share" ariaLabel="Share"
@@ -47,7 +48,7 @@ export function CreateFlowTopNavView({
{hasExport && ( {hasExport && (
<Button <Button
buttonType="outline" buttonType="outline"
palette="default" palette={buttonPalette}
size="xsmall" size="xsmall"
onClick={onExport} onClick={onExport}
ariaLabel="Export" ariaLabel="Export"
@@ -74,7 +75,7 @@ export function CreateFlowTopNavView({
{hasEdit && ( {hasEdit && (
<Button <Button
buttonType="outline" buttonType="outline"
palette="default" palette={buttonPalette}
size="xsmall" size="xsmall"
onClick={onEdit} onClick={onEdit}
ariaLabel="Edit" ariaLabel="Edit"
@@ -86,7 +87,7 @@ export function CreateFlowTopNavView({
<Button <Button
buttonType="outline" buttonType="outline"
palette="default" palette={buttonPalette}
size="xsmall" size="xsmall"
onClick={onExit} onClick={onExit}
ariaLabel={exitButtonText} ariaLabel={exitButtonText}
@@ -24,7 +24,9 @@ export interface DecisionMakingSidebarViewProps {
messageBoxTitle: string; messageBoxTitle: string;
messageBoxItems: InfoMessageBoxItem[]; messageBoxItems: InfoMessageBoxItem[];
messageBoxCheckedIds: string[] | undefined; messageBoxCheckedIds: string[] | undefined;
onMessageBoxCheckboxChange: ((id: string, checked: boolean) => void) | undefined; onMessageBoxCheckboxChange:
| ((id: string, checked: boolean) => void)
| undefined;
size: "L" | "M"; size: "L" | "M";
justification: "left" | "center"; justification: "left" | "center";
className: string; className: string;
@@ -53,9 +53,7 @@ function DecisionMakingSidebarView({
className={`font-inter font-normal max-w-[640px] overflow-hidden relative shrink-0 text-[var(--color-content-default-tertiary,#b4b4b4)] text-ellipsis w-full whitespace-pre-wrap ${ className={`font-inter font-normal max-w-[640px] overflow-hidden relative shrink-0 text-[var(--color-content-default-tertiary,#b4b4b4)] text-ellipsis w-full whitespace-pre-wrap ${
isLeft ? "" : "text-center" isLeft ? "" : "text-center"
} ${ } ${
isL isL ? "text-[18px] leading-[1.3]" : "text-[14px] leading-[20px]"
? "text-[18px] leading-[1.3]"
: "text-[14px] leading-[20px]"
}`} }`}
> >
{description} {description}
@@ -1,2 +1,5 @@
export { default } from "./InfoMessageBox.container"; export { default } from "./InfoMessageBox.container";
export type { InfoMessageBoxProps, InfoMessageBoxItem } from "./InfoMessageBox.types"; export type {
InfoMessageBoxProps,
InfoMessageBoxItem,
} from "./InfoMessageBox.types";
@@ -1,5 +1,9 @@
export type InputLabelSizeValue = "S" | "M" | "s" | "m"; export type InputLabelSizeValue = "S" | "M" | "s" | "m";
export type InputLabelPaletteValue = "Default" | "Inverse" | "default" | "inverse"; export type InputLabelPaletteValue =
| "Default"
| "Inverse"
| "default"
| "inverse";
export interface InputLabelProps { export interface InputLabelProps {
/** /**
@@ -34,7 +34,8 @@ function InputLabelView({
? "text-[color:var(--color-content-inverse-secondary,#1f1f1f)]" ? "text-[color:var(--color-content-inverse-secondary,#1f1f1f)]"
: "text-[color:var(--color-content-default-secondary,#d2d2d2)]"; : "text-[color:var(--color-content-default-secondary,#d2d2d2)]";
const helperTextColor = "text-[color:var(--color-content-default-tertiary,#b4b4b4)]"; const helperTextColor =
"text-[color:var(--color-content-default-tertiary,#b4b4b4)]";
// Layout: S uses flex-wrap with baseline, M uses flex with center // Layout: S uses flex-wrap with baseline, M uses flex with center
const containerClass = isSmall const containerClass = isSmall
@@ -27,9 +27,10 @@ export function ModalFooterView({
// Determine if stepper should be shown // Determine if stepper should be shown
// Defaults to true if currentStep and totalSteps are provided, unless explicitly set to false // Defaults to true if currentStep and totalSteps are provided, unless explicitly set to false
const shouldShowStepper = stepperProp !== undefined const shouldShowStepper =
stepperProp !== undefined
? stepperProp ? stepperProp
: (currentStep !== undefined && totalSteps !== undefined); : currentStep !== undefined && totalSteps !== undefined;
return ( return (
<div <div
@@ -38,7 +39,12 @@ export function ModalFooterView({
{/* Back Button - Absolutely positioned bottom left */} {/* Back Button - Absolutely positioned bottom left */}
{showBackButton && ( {showBackButton && (
<div className="absolute left-[16px] top-[12px]"> <div className="absolute left-[16px] top-[12px]">
<Button buttonType="outline" palette="default" size="medium" onClick={onBack}> <Button
buttonType="outline"
palette="default"
size="medium"
onClick={onBack}
>
{defaultBackText} {defaultBackText}
</Button> </Button>
</div> </div>
+2 -4
View File
@@ -9,16 +9,14 @@ const DEFAULT_LABELS: Record<TagProps["variant"], string> = {
selected: "SELECTED", selected: "SELECTED",
}; };
const TagContainer = memo<TagProps>( const TagContainer = memo<TagProps>(({ variant, children, className = "" }) => {
({ variant, children, className = "" }) => {
const content = children ?? DEFAULT_LABELS[variant]; const content = children ?? DEFAULT_LABELS[variant];
return ( return (
<TagView variant={variant} className={className}> <TagView variant={variant} className={className}>
{content} {content}
</TagView> </TagView>
); );
}, });
);
TagContainer.displayName = "Tag"; TagContainer.displayName = "Tag";
+196
View File
@@ -0,0 +1,196 @@
"use client";
import { useState, useEffect } from "react";
import { useMediaQuery } from "../../hooks/useMediaQuery";
import HeaderLockup from "../../components/type/HeaderLockup";
import CommunityRuleDocument from "../../components/sections/CommunityRuleDocument";
import type { CommunityRuleDocumentSection } from "../../components/sections/CommunityRuleDocument/CommunityRuleDocument.types";
import Alert from "../../components/modals/Alert";
const TITLE = "Mutual Aid Mondays";
const DESCRIPTION =
"Mutual Aid Monday is a grassroots community in Denver, founded in November 2020 by Kelsang Virya, dedicated to supporting neighbors experiencing homelessness.";
const TOAST_TITLE = "This is what folks see when you share your CommunityRule";
const TOAST_DESCRIPTION =
"Your group can use this document as an operating manual.";
const SOLIDARITY_BODY =
"Food Not Bombs is not a charity. It is a project of solidarity. Charity is vertical. It moves from those who have to those who have not and maintains the hierarchy between them. Solidarity is horizontal. It moves between equals who recognize that our liberation is bound together. We do not help the poor. We share resources among community members because access to food is a human right rather than a privilege of wealth.";
/** Static sections for the completed Community Rule document (placeholder data). */
const COMPLETED_RULE_SECTIONS: CommunityRuleDocumentSection[] = [
{
categoryName: "Values",
entries: [
{ title: "Solidarity Forever", body: SOLIDARITY_BODY },
{
title: "Shared Leadership",
body: "We operate without bosses or managers. This does not mean we are disorganized. It means we are self-organized. Authority in this chapter is temporary and task-specific rather than permanent or personal. We believe the people doing the work should make the decisions about that work. By distributing responsibility we prevent burnout and ensure the movement survives beyond any single leader.",
},
{
title: "Organizing Offline",
body: "We use digital tools to coordinate but we build power in the physical world. An algorithm cannot cook a meal and a group chat cannot look someone in the eye. We prioritize face-to-face connection and resist the pull of digital metrics.",
},
{
title: "Circular Food Systems",
body: "We intervene in the ecological crisis by addressing food waste and food recovery. We redirect surplus food to where it is needed and model a circular economy at the scale of our communities.",
},
],
},
{
categoryName: "Communication",
entries: [
{
title: "Signal",
body: "We use Signal for sensitive coordination. Encrypted messaging helps protect our members and our plans from surveillance.",
},
],
},
{
categoryName: "Membership",
entries: [
{
title: "Open Admission",
body: "Anyone who shares our values and is willing to contribute is welcome. We do not require applications or approval processes for general participation.",
},
],
},
{
categoryName: "Decision-making",
entries: [
{
title: "Lazy Consensus",
body: "We use lazy consensus for most decisions: proposals move forward unless someone raises a blocking concern. This keeps us moving without requiring everyone to approve every detail.",
},
{
title: "Modified Consensus",
body: "For larger or more consequential decisions we use modified consensus, with clear timelines and a fallback to a supermajority vote if needed.",
},
],
},
{
categoryName: "Conflict management",
entries: [
{
title: "Code of Conduct",
body: "We have a code of conduct that sets expectations for behavior and outlines how we address harm.",
},
{
title: "Restorative Justice",
body: "When conflict arises we prioritize restoration and learning over punishment. We use facilitated circles and other restorative practices where appropriate.",
},
],
},
];
/**
* Completed create flow page.
* Figma: 20907-213286 (main), 18002-28017 (toast).
*/
export default function CompletedPage() {
const [isMounted, setIsMounted] = useState(false);
const [toastDismissed, setToastDismissed] = useState(false);
const isMdOrLarger = useMediaQuery("(min-width: 640px)");
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional: defer layout breakpoint until after mount to prevent flash
setIsMounted(true);
}, []);
const showDesktopLayout = !isMounted || isMdOrLarger;
if (showDesktopLayout) {
return (
<div className="flex h-full min-h-0 w-full flex-1 flex-col overflow-hidden">
<div className="flex min-h-0 flex-1 overflow-hidden bg-[var(--color-teal-teal50,#c9fef9)] px-5 md:px-12">
<div className="grid h-full max-w-[1280px] grid-cols-2 shrink-0 gap-[var(--measures-spacing-1200,48px)] min-h-0 min-w-0 w-full">
{/* Left column: community title + header, centered, does not scroll */}
<div className="flex min-w-0 flex-col justify-center overflow-hidden py-8">
<HeaderLockup
title={TITLE}
description={DESCRIPTION}
justification="left"
size="L"
palette="inverse"
/>
</div>
{/* Right column: Community Rule document — this column scrolls independently; padding inside scroll so content isn't clipped */}
<div className="scrollbar-hide relative flex min-h-0 min-w-0 flex-col overflow-x-hidden overflow-y-auto">
{/* Soft fade at top: gradient wash only (no blur) so no sharp cutoff line */}
<div
className="sticky top-0 z-10 h-5 shrink-0 pointer-events-none bg-gradient-to-b from-[var(--color-teal-teal50,#c9fef9)]/55 from-0% via-[var(--color-teal-teal50,#c9fef9)]/20 via-50% to-transparent"
aria-hidden
/>
<div className="py-8 min-w-0">
<CommunityRuleDocument
sections={COMPLETED_RULE_SECTIONS}
className="min-w-0"
/>
</div>
</div>
</div>
</div>
{!toastDismissed && (
<div
className="fixed bottom-0 left-0 right-0 z-10 w-full"
role="status"
aria-live="polite"
>
<Alert
type="toast"
status="default"
title={TOAST_TITLE}
description={TOAST_DESCRIPTION}
hasLeadingIcon
hasBodyText
onClose={() => setToastDismissed(true)}
className="w-full"
/>
</div>
)}
</div>
);
}
return (
<>
<div className="w-full flex flex-col items-center px-5 min-w-0 bg-[var(--color-teal-teal50,#c9fef9)] py-8">
<div className="flex flex-col gap-4 w-full max-w-[639px]">
<HeaderLockup
title={TITLE}
description={DESCRIPTION}
justification="left"
size="M"
palette="inverse"
/>
<CommunityRuleDocument
sections={COMPLETED_RULE_SECTIONS}
useCardStyle
className="w-full p-4"
/>
</div>
</div>
{!toastDismissed && (
<div
className="fixed bottom-0 left-0 right-0 z-10 w-full"
role="status"
aria-live="polite"
>
<Alert
type="toast"
status="default"
title={TOAST_TITLE}
description={TOAST_DESCRIPTION}
hasLeadingIcon
hasBodyText
onClose={() => setToastDismissed(true)}
className="w-full"
/>
</div>
)}
</>
);
}
+1 -3
View File
@@ -25,9 +25,7 @@ export function CreateFlowProvider({
initialStep = null, initialStep = null,
}: CreateFlowProviderProps) { }: CreateFlowProviderProps) {
const [state, setState] = useState<CreateFlowState>({}); const [state, setState] = useState<CreateFlowState>({});
const [currentStep] = useState<CreateFlowStep | null>( const [currentStep] = useState<CreateFlowStep | null>(initialStep);
initialStep,
);
const updateState = (updates: Partial<CreateFlowState>) => { const updateState = (updates: Partial<CreateFlowState>) => {
setState((prevState) => ({ setState((prevState) => ({
+2 -4
View File
@@ -32,9 +32,7 @@ const FINAL_REVIEW_CATEGORIES: Category[] = [
}, },
{ {
name: "Membership", name: "Membership",
chipOptions: [ chipOptions: [{ id: "m1", label: "Open Admission", state: "unselected" }],
{ id: "m1", label: "Open Admission", state: "unselected" },
],
}, },
{ {
name: "Decision-making", name: "Decision-making",
@@ -70,7 +68,7 @@ export default function FinalReviewPage() {
if (showDesktopLayout) { if (showDesktopLayout) {
return ( return (
<div className="w-full max-w-[1280px] shrink-0 px-5 md:px-16"> <div className="w-full max-w-[1280px] shrink-0 px-5 md:px-12">
<div className="flex w-full flex-col gap-4 min-w-0 sm:grid sm:grid-cols-2 sm:gap-[var(--measures-spacing-1200,48px)]"> <div className="flex w-full flex-col gap-4 min-w-0 sm:grid sm:grid-cols-2 sm:gap-[var(--measures-spacing-1200,48px)]">
<div className="min-w-0 flex flex-col justify-center"> <div className="min-w-0 flex flex-col justify-center">
<HeaderLockup <HeaderLockup
+33 -5
View File
@@ -59,7 +59,10 @@ function CreateFlowLayoutContent({ children }: { children: ReactNode }) {
const previousStep = getPreviousStep(); const previousStep = getPreviousStep();
const handleNext = () => { const handleNext = () => {
if (typeof document !== "undefined" && document.activeElement instanceof HTMLElement) { if (
typeof document !== "undefined" &&
document.activeElement instanceof HTMLElement
) {
document.activeElement.blur(); document.activeElement.blur();
} }
if (nextStep) { if (nextStep) {
@@ -68,7 +71,10 @@ function CreateFlowLayoutContent({ children }: { children: ReactNode }) {
}; };
const handleBack = () => { const handleBack = () => {
if (typeof document !== "undefined" && document.activeElement instanceof HTMLElement) { if (
typeof document !== "undefined" &&
document.activeElement instanceof HTMLElement
) {
document.activeElement.blur(); document.activeElement.blur();
} }
if (previousStep) { if (previousStep) {
@@ -76,12 +82,33 @@ function CreateFlowLayoutContent({ children }: { children: ReactNode }) {
} }
}; };
const isCompletedStep = currentStep === "completed";
return ( return (
<div className="min-h-screen bg-black flex flex-col"> <div
<CreateFlowTopNav /> className={`bg-black flex flex-col ${isCompletedStep ? "h-screen overflow-hidden" : "min-h-screen"}`}
<main className="flex-1 flex items-center justify-center overflow-auto"> >
<CreateFlowTopNav
hasShare={isCompletedStep}
hasExport={isCompletedStep}
hasEdit={isCompletedStep}
loggedIn={isCompletedStep}
onEdit={
isCompletedStep
? () => router.push("/create/final-review")
: undefined
}
buttonPalette={isCompletedStep ? "inverse" : undefined}
className={
isCompletedStep ? "!bg-[var(--color-teal-teal50,#c9fef9)]" : undefined
}
/>
<main
className={`flex-1 flex min-h-0 justify-center ${isCompletedStep ? "items-stretch overflow-hidden" : "items-center overflow-auto"}`}
>
{children} {children}
</main> </main>
{!isCompletedStep && (
<CreateFlowFooter <CreateFlowFooter
secondButton={ secondButton={
nextStep ? ( nextStep ? (
@@ -100,6 +127,7 @@ function CreateFlowLayoutContent({ children }: { children: ReactNode }) {
} }
onBackClick={previousStep ? handleBack : undefined} onBackClick={previousStep ? handleBack : undefined}
/> />
)}
</div> </div>
); );
} }
+18 -9
View File
@@ -44,9 +44,12 @@ export default function SelectPage() {
setCommunitySizeOptions((prev) => setCommunitySizeOptions((prev) =>
prev.map((opt) => prev.map((opt) =>
opt.id === chipId opt.id === chipId
? { ...opt, state: opt.state === "Selected" ? "Unselected" : "Selected" } ? {
: opt ...opt,
) state: opt.state === "Selected" ? "Unselected" : "Selected",
}
: opt,
),
); );
}; };
@@ -54,9 +57,12 @@ export default function SelectPage() {
setOrganizationTypeOptions((prev) => setOrganizationTypeOptions((prev) =>
prev.map((opt) => prev.map((opt) =>
opt.id === chipId opt.id === chipId
? { ...opt, state: opt.state === "Selected" ? "Unselected" : "Selected" } ? {
: opt ...opt,
) state: opt.state === "Selected" ? "Unselected" : "Selected",
}
: opt,
),
); );
}; };
@@ -64,9 +70,12 @@ export default function SelectPage() {
setGovernanceStyleOptions((prev) => setGovernanceStyleOptions((prev) =>
prev.map((opt) => prev.map((opt) =>
opt.id === chipId opt.id === chipId
? { ...opt, state: opt.state === "Selected" ? "Unselected" : "Selected" } ? {
: opt ...opt,
) state: opt.state === "Selected" ? "Unselected" : "Selected",
}
: opt,
),
); );
}; };
+7 -7
View File
@@ -18,31 +18,31 @@
/* Design system scrollbar (Figma node 20612-36521): dark track + thumb with states */ /* Design system scrollbar (Figma node 20612-36521): dark track + thumb with states */
.scrollbar-design { .scrollbar-design {
scrollbar-width: thin; /* Firefox: narrow scrollbar */ scrollbar-width: thin; /* Firefox: narrow scrollbar */
scrollbar-color: #545B64 #292D32; /* Firefox: thumb track */ scrollbar-color: #545b64 #292d32; /* Firefox: thumb track */
} }
.scrollbar-design::-webkit-scrollbar { .scrollbar-design::-webkit-scrollbar {
width: 16px; width: 16px;
height: 16px; height: 16px;
} }
.scrollbar-design::-webkit-scrollbar-track { .scrollbar-design::-webkit-scrollbar-track {
background: #292D32; background: #292d32;
} }
.scrollbar-design::-webkit-scrollbar-thumb { .scrollbar-design::-webkit-scrollbar-thumb {
background: #545B64; background: #545b64;
border-radius: 4px; border-radius: 4px;
border: 4px solid #292D32; /* visual padding: thumb appears 8px within 16px track */ border: 4px solid #292d32; /* visual padding: thumb appears 8px within 16px track */
background-clip: padding-box; background-clip: padding-box;
} }
.scrollbar-design::-webkit-scrollbar-thumb:hover { .scrollbar-design::-webkit-scrollbar-thumb:hover {
background: #787F8A; background: #787f8a;
border-width: 2px; /* hover: thumb expands to 12px */ border-width: 2px; /* hover: thumb expands to 12px */
} }
.scrollbar-design::-webkit-scrollbar-thumb:active { .scrollbar-design::-webkit-scrollbar-thumb:active {
background: #3F434C; background: #3f434c;
border-width: 2px; border-width: 2px;
} }
.scrollbar-design::-webkit-scrollbar-corner { .scrollbar-design::-webkit-scrollbar-corner {
background: #292D32; background: #292d32;
} }
@theme inline { @theme inline {
+1 -1
View File
@@ -30,7 +30,7 @@ export function getAssetPath(assetPath: string): string {
*/ */
export const ASSETS = { export const ASSETS = {
// Logo // Logo
LOGO: "assets/Logo.svg", LOGO: "assets/logo/Logo.svg",
// Avatars // Avatars
AVATAR_1: "assets/Avatar_1.png", AVATAR_1: "assets/Avatar_1.png",
+74 -44
View File
@@ -11,7 +11,7 @@
*/ */
export function normalizeMode( export function normalizeMode(
value: string | undefined, value: string | undefined,
defaultValue: "standard" | "inverse" = "standard" defaultValue: "standard" | "inverse" = "standard",
): "standard" | "inverse" { ): "standard" | "inverse" {
if (!value) return defaultValue; if (!value) return defaultValue;
const normalized = value.toLowerCase(); const normalized = value.toLowerCase();
@@ -26,7 +26,7 @@ export function normalizeMode(
*/ */
export function normalizeState( export function normalizeState(
value: string | undefined, value: string | undefined,
defaultValue: "default" | "hover" | "focus" | "selected" = "default" defaultValue: "default" | "hover" | "focus" | "selected" = "default",
): "default" | "hover" | "focus" | "selected" { ): "default" | "hover" | "focus" | "selected" {
if (!value) return defaultValue; if (!value) return defaultValue;
const normalized = value.toLowerCase(); const normalized = value.toLowerCase();
@@ -46,7 +46,7 @@ export function normalizeState(
*/ */
export function normalizeInputState( export function normalizeInputState(
value: string | undefined, value: string | undefined,
defaultValue: "default" | "active" | "hover" | "focus" = "default" defaultValue: "default" | "active" | "hover" | "focus" = "default",
): "default" | "active" | "hover" | "focus" { ): "default" | "active" | "hover" | "focus" {
if (!value) return defaultValue; if (!value) return defaultValue;
const normalized = value.toLowerCase(); const normalized = value.toLowerCase();
@@ -66,7 +66,7 @@ export function normalizeInputState(
*/ */
export function normalizeToggleState( export function normalizeToggleState(
value: string | undefined, value: string | undefined,
defaultValue: "default" | "hover" | "focus" | "selected" = "default" defaultValue: "default" | "hover" | "focus" | "selected" = "default",
): "default" | "hover" | "focus" | "selected" { ): "default" | "hover" | "focus" | "selected" {
if (!value) return defaultValue; if (!value) return defaultValue;
const normalized = value.toLowerCase(); const normalized = value.toLowerCase();
@@ -112,13 +112,12 @@ export type InputStateValue =
| "Hover" | "Hover"
| "Focus"; | "Focus";
/** /**
* Normalize button size prop values * Normalize button size prop values
*/ */
export function normalizeSize( export function normalizeSize(
value: string | undefined, value: string | undefined,
defaultValue: "xsmall" | "small" | "medium" | "large" | "xlarge" = "xsmall" defaultValue: "xsmall" | "small" | "medium" | "large" | "xlarge" = "xsmall",
): "xsmall" | "small" | "medium" | "large" | "xlarge" { ): "xsmall" | "small" | "medium" | "large" | "xlarge" {
if (!value) return defaultValue; if (!value) return defaultValue;
const normalized = value.toLowerCase(); const normalized = value.toLowerCase();
@@ -134,7 +133,7 @@ export function normalizeSize(
*/ */
export function normalizeAlertStatus( export function normalizeAlertStatus(
value: string | undefined, value: string | undefined,
defaultValue: "default" = "default" defaultValue: "default" = "default",
): "default" | "positive" | "warning" | "danger" { ): "default" | "positive" | "warning" | "danger" {
if (!value) return defaultValue; if (!value) return defaultValue;
const normalized = value.toLowerCase(); const normalized = value.toLowerCase();
@@ -150,7 +149,7 @@ export function normalizeAlertStatus(
*/ */
export function normalizeAlertType( export function normalizeAlertType(
value: string | undefined, value: string | undefined,
defaultValue: "toast" = "toast" defaultValue: "toast" = "toast",
): "toast" | "banner" { ): "toast" | "banner" {
if (!value) return defaultValue; if (!value) return defaultValue;
const normalized = value.toLowerCase(); const normalized = value.toLowerCase();
@@ -166,7 +165,7 @@ export function normalizeAlertType(
*/ */
export function normalizeTooltipPosition( export function normalizeTooltipPosition(
value: string | undefined, value: string | undefined,
defaultValue: "top" = "top" defaultValue: "top" = "top",
): "top" | "bottom" { ): "top" | "bottom" {
if (!value) return defaultValue; if (!value) return defaultValue;
const normalized = value.toLowerCase(); const normalized = value.toLowerCase();
@@ -199,7 +198,12 @@ export type SizeValue =
*/ */
export function normalizeMenuBarSize( export function normalizeMenuBarSize(
value: string | undefined, value: string | undefined,
defaultValue: "X Small" | "Small" | "Medium" | "Large" | "X Large" = "X Small" defaultValue:
| "X Small"
| "Small"
| "Medium"
| "Large"
| "X Large" = "X Small",
): "X Small" | "Small" | "Medium" | "Large" | "X Large" { ): "X Small" | "Small" | "Medium" | "Large" | "X Large" {
if (!value) return defaultValue; if (!value) return defaultValue;
if ( if (
@@ -219,7 +223,7 @@ export function normalizeMenuBarSize(
*/ */
export function normalizeNavigationItemVariant( export function normalizeNavigationItemVariant(
value: string | undefined, value: string | undefined,
defaultValue: "default" = "default" defaultValue: "default" = "default",
): "default" { ): "default" {
if (!value) return defaultValue; if (!value) return defaultValue;
const normalized = value.toLowerCase(); const normalized = value.toLowerCase();
@@ -234,7 +238,7 @@ export function normalizeNavigationItemVariant(
*/ */
export function normalizeNavigationItemSize( export function normalizeNavigationItemSize(
value: string | undefined, value: string | undefined,
defaultValue: "default" = "default" defaultValue: "default" = "default",
): "default" | "xsmall" { ): "default" | "xsmall" {
if (!value) return defaultValue; if (!value) return defaultValue;
const normalized = value.toLowerCase(); const normalized = value.toLowerCase();
@@ -250,7 +254,7 @@ export function normalizeNavigationItemSize(
*/ */
export function normalizeContentLockupVariant( export function normalizeContentLockupVariant(
value: string | undefined, value: string | undefined,
defaultValue: "hero" = "hero" defaultValue: "hero" = "hero",
): "hero" | "feature" | "learn" | "ask" | "ask-inverse" | "modal" { ): "hero" | "feature" | "learn" | "ask" | "ask-inverse" | "modal" {
if (!value) return defaultValue; if (!value) return defaultValue;
const normalized = value.toLowerCase(); const normalized = value.toLowerCase();
@@ -266,7 +270,7 @@ export function normalizeContentLockupVariant(
*/ */
export function normalizeAlignment( export function normalizeAlignment(
value: string | undefined, value: string | undefined,
defaultValue: "center" = "center" defaultValue: "center" = "center",
): "center" | "left" { ): "center" | "left" {
if (!value) return defaultValue; if (!value) return defaultValue;
const normalized = value.toLowerCase(); const normalized = value.toLowerCase();
@@ -282,7 +286,7 @@ export function normalizeAlignment(
*/ */
export function normalizeNumberedListSize( export function normalizeNumberedListSize(
value: string | undefined, value: string | undefined,
defaultValue: "M" = "M" defaultValue: "M" = "M",
): "M" | "S" { ): "M" | "S" {
if (!value) return defaultValue; if (!value) return defaultValue;
const normalized = value.toUpperCase(); const normalized = value.toUpperCase();
@@ -297,7 +301,7 @@ export function normalizeNumberedListSize(
*/ */
export function normalizeHeaderLockupJustification( export function normalizeHeaderLockupJustification(
value: string | undefined, value: string | undefined,
defaultValue: "left" = "left" defaultValue: "left" = "left",
): "left" | "center" { ): "left" | "center" {
if (!value) return defaultValue; if (!value) return defaultValue;
const normalized = value.toLowerCase(); const normalized = value.toLowerCase();
@@ -312,7 +316,7 @@ export function normalizeHeaderLockupJustification(
*/ */
export function normalizeHeaderLockupSize( export function normalizeHeaderLockupSize(
value: string | undefined, value: string | undefined,
defaultValue: "L" = "L" defaultValue: "L" = "L",
): "L" | "M" { ): "L" | "M" {
if (!value) return defaultValue; if (!value) return defaultValue;
const normalized = value.toUpperCase(); const normalized = value.toUpperCase();
@@ -322,12 +326,27 @@ export function normalizeHeaderLockupSize(
return defaultValue; return defaultValue;
} }
/**
* Normalize header lockup palette prop values (Default/Inverse -> default/inverse)
*/
export function normalizeHeaderLockupPalette(
value: string | undefined,
defaultValue: "default" = "default",
): "default" | "inverse" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
if (normalized === "default" || normalized === "inverse") {
return normalized;
}
return defaultValue;
}
/** /**
* Normalize text input size prop values * Normalize text input size prop values
*/ */
export function normalizeTextInputSize( export function normalizeTextInputSize(
value: string | undefined, value: string | undefined,
defaultValue: "medium" = "medium" defaultValue: "medium" = "medium",
): "small" | "medium" { ): "small" | "medium" {
if (!value) return defaultValue; if (!value) return defaultValue;
const normalized = value.toLowerCase(); const normalized = value.toLowerCase();
@@ -342,7 +361,7 @@ export function normalizeTextInputSize(
*/ */
export function normalizeContentContainerSize( export function normalizeContentContainerSize(
value: string | undefined, value: string | undefined,
defaultValue: "responsive" = "responsive" defaultValue: "responsive" = "responsive",
): "xs" | "responsive" { ): "xs" | "responsive" {
if (!value) return defaultValue; if (!value) return defaultValue;
const normalized = value.toLowerCase(); const normalized = value.toLowerCase();
@@ -358,7 +377,7 @@ export function normalizeContentContainerSize(
*/ */
export function normalizeContentThumbnailVariant( export function normalizeContentThumbnailVariant(
value: string | undefined, value: string | undefined,
defaultValue: "vertical" = "vertical" defaultValue: "vertical" = "vertical",
): "vertical" | "horizontal" { ): "vertical" | "horizontal" {
if (!value) return defaultValue; if (!value) return defaultValue;
const normalized = value.toLowerCase(); const normalized = value.toLowerCase();
@@ -374,7 +393,7 @@ export function normalizeContentThumbnailVariant(
*/ */
export function normalizeSectionHeaderVariant( export function normalizeSectionHeaderVariant(
value: string | undefined, value: string | undefined,
defaultValue: "default" = "default" defaultValue: "default" = "default",
): "default" | "multi-line" { ): "default" | "multi-line" {
if (!value) return defaultValue; if (!value) return defaultValue;
const normalized = value.toLowerCase(); const normalized = value.toLowerCase();
@@ -390,7 +409,7 @@ export function normalizeSectionHeaderVariant(
*/ */
export function normalizeQuoteBlockVariant( export function normalizeQuoteBlockVariant(
value: string | undefined, value: string | undefined,
defaultValue: "standard" = "standard" defaultValue: "standard" = "standard",
): "compact" | "standard" | "extended" { ): "compact" | "standard" | "extended" {
if (!value) return defaultValue; if (!value) return defaultValue;
const normalized = value.toLowerCase(); const normalized = value.toLowerCase();
@@ -406,11 +425,16 @@ export function normalizeQuoteBlockVariant(
*/ */
export function normalizeNumberCardSize( export function normalizeNumberCardSize(
value: string | undefined, value: string | undefined,
defaultValue: "Medium" = "Medium" defaultValue: "Medium" = "Medium",
): "Small" | "Medium" | "Large" | "XLarge" { ): "Small" | "Medium" | "Large" | "XLarge" {
if (!value) return defaultValue; if (!value) return defaultValue;
// Check if already PascalCase // Check if already PascalCase
if (value === "Small" || value === "Medium" || value === "Large" || value === "XLarge") { if (
value === "Small" ||
value === "Medium" ||
value === "Large" ||
value === "XLarge"
) {
return value; return value;
} }
// Normalize lowercase to PascalCase // Normalize lowercase to PascalCase
@@ -427,7 +451,7 @@ export function normalizeNumberCardSize(
*/ */
export function normalizeAskOrganizerVariant( export function normalizeAskOrganizerVariant(
value: string | undefined, value: string | undefined,
defaultValue: "centered" = "centered" defaultValue: "centered" = "centered",
): "centered" | "left-aligned" | "compact" | "inverse" { ): "centered" | "left-aligned" | "compact" | "inverse" {
if (!value) return defaultValue; if (!value) return defaultValue;
const normalized = value.toLowerCase(); const normalized = value.toLowerCase();
@@ -443,7 +467,7 @@ export function normalizeAskOrganizerVariant(
*/ */
export function normalizeContextMenuItemSize( export function normalizeContextMenuItemSize(
value: string | undefined, value: string | undefined,
defaultValue: "medium" = "medium" defaultValue: "medium" = "medium",
): "small" | "medium" | "large" { ): "small" | "medium" | "large" {
if (!value) return defaultValue; if (!value) return defaultValue;
const normalized = value.toLowerCase(); const normalized = value.toLowerCase();
@@ -459,7 +483,7 @@ export function normalizeContextMenuItemSize(
*/ */
export function normalizeImagePlaceholderColor( export function normalizeImagePlaceholderColor(
value: string | undefined, value: string | undefined,
defaultValue: "blue" = "blue" defaultValue: "blue" = "blue",
): "blue" | "green" | "purple" | "red" | "orange" | "teal" { ): "blue" | "green" | "purple" | "red" | "orange" | "teal" {
if (!value) return defaultValue; if (!value) return defaultValue;
const normalized = value.toLowerCase(); const normalized = value.toLowerCase();
@@ -475,7 +499,7 @@ export function normalizeImagePlaceholderColor(
*/ */
export function normalizeToggleGroupPosition( export function normalizeToggleGroupPosition(
value: string | undefined, value: string | undefined,
defaultValue: "left" = "left" defaultValue: "left" = "left",
): "left" | "middle" | "right" { ): "left" | "middle" | "right" {
if (!value) return defaultValue; if (!value) return defaultValue;
const normalized = value.toLowerCase(); const normalized = value.toLowerCase();
@@ -491,7 +515,7 @@ export function normalizeToggleGroupPosition(
*/ */
export function normalizeLabelVariant( export function normalizeLabelVariant(
value: string | undefined, value: string | undefined,
defaultValue: "default" = "default" defaultValue: "default" = "default",
): "default" | "horizontal" { ): "default" | "horizontal" {
if (!value) return defaultValue; if (!value) return defaultValue;
const normalized = value.toLowerCase(); const normalized = value.toLowerCase();
@@ -507,7 +531,7 @@ export function normalizeLabelVariant(
*/ */
export function normalizeTextAreaAppearance( export function normalizeTextAreaAppearance(
value: string | undefined, value: string | undefined,
defaultValue: "default" = "default" defaultValue: "default" = "default",
): "default" | "embedded" { ): "default" | "embedded" {
if (!value) return defaultValue; if (!value) return defaultValue;
const n = value.toLowerCase(); const n = value.toLowerCase();
@@ -519,7 +543,7 @@ export function normalizeTextAreaAppearance(
*/ */
export function normalizeSmallMediumLargeSize( export function normalizeSmallMediumLargeSize(
value: string | undefined, value: string | undefined,
defaultValue: "medium" = "medium" defaultValue: "medium" = "medium",
): "small" | "medium" | "large" { ): "small" | "medium" | "large" {
if (!value) return defaultValue; if (!value) return defaultValue;
const normalized = value.toLowerCase(); const normalized = value.toLowerCase();
@@ -535,11 +559,16 @@ export function normalizeSmallMediumLargeSize(
*/ */
export function normalizeRuleCardSize( export function normalizeRuleCardSize(
value: string | undefined, value: string | undefined,
defaultValue: "L" = "L" defaultValue: "L" = "L",
): "XS" | "S" | "M" | "L" { ): "XS" | "S" | "M" | "L" {
if (!value) return defaultValue; if (!value) return defaultValue;
const normalized = value.toUpperCase(); const normalized = value.toUpperCase();
if (normalized === "XS" || normalized === "S" || normalized === "M" || normalized === "L") { if (
normalized === "XS" ||
normalized === "S" ||
normalized === "M" ||
normalized === "L"
) {
return normalized; return normalized;
} }
return defaultValue; return defaultValue;
@@ -561,11 +590,7 @@ export type ChipStateValue =
/** /**
* Type helper for case-insensitive Chip palette prop * Type helper for case-insensitive Chip palette prop
*/ */
export type ChipPaletteValue = export type ChipPaletteValue = "default" | "inverse" | "Default" | "Inverse";
| "default"
| "inverse"
| "Default"
| "Inverse";
/** /**
* Type helper for case-insensitive Chip size prop * Type helper for case-insensitive Chip size prop
@@ -673,7 +698,7 @@ export function normalizeInputLabelPalette(
*/ */
export function normalizeMenuBarItemState( export function normalizeMenuBarItemState(
value: string | undefined, value: string | undefined,
defaultValue: "default" | "hover" | "selected" = "default" defaultValue: "default" | "hover" | "selected" = "default",
): "default" | "hover" | "selected" { ): "default" | "hover" | "selected" {
if (!value) return defaultValue; if (!value) return defaultValue;
if (value === "default" || value === "hover" || value === "selected") { if (value === "default" || value === "hover" || value === "selected") {
@@ -689,7 +714,7 @@ export function normalizeMenuBarItemState(
*/ */
export function normalizeMenuBarItemMode( export function normalizeMenuBarItemMode(
value: string | undefined, value: string | undefined,
defaultValue: "default" | "inverse" = "default" defaultValue: "default" | "inverse" = "default",
): "default" | "inverse" { ): "default" | "inverse" {
if (!value) return defaultValue; if (!value) return defaultValue;
if (value === "default" || value === "inverse") { if (value === "default" || value === "inverse") {
@@ -704,7 +729,12 @@ export function normalizeMenuBarItemMode(
*/ */
export function normalizeMenuBarItemSize( export function normalizeMenuBarItemSize(
value: string | undefined, value: string | undefined,
defaultValue: "X Small" | "Small" | "Medium" | "Large" | "X Large" = "X Small" defaultValue:
| "X Small"
| "Small"
| "Medium"
| "Large"
| "X Large" = "X Small",
): "X Small" | "Small" | "Medium" | "Large" | "X Large" { ): "X Small" | "Small" | "Medium" | "Large" | "X Large" {
if (!value) return defaultValue; if (!value) return defaultValue;
if ( if (
@@ -724,7 +754,7 @@ export function normalizeMenuBarItemSize(
*/ */
export function normalizeButtonType( export function normalizeButtonType(
value: string | undefined, value: string | undefined,
defaultValue: "filled" = "filled" defaultValue: "filled" = "filled",
): "filled" | "outline" | "ghost" | "danger" { ): "filled" | "outline" | "ghost" | "danger" {
if (!value) return defaultValue; if (!value) return defaultValue;
const normalized = value.toLowerCase(); const normalized = value.toLowerCase();
@@ -740,7 +770,7 @@ export function normalizeButtonType(
*/ */
export function normalizeButtonPalette( export function normalizeButtonPalette(
value: string | undefined, value: string | undefined,
defaultValue: "default" = "default" defaultValue: "default" = "default",
): "default" | "inverse" { ): "default" | "inverse" {
if (!value) return defaultValue; if (!value) return defaultValue;
const normalized = value.toLowerCase(); const normalized = value.toLowerCase();
@@ -759,7 +789,7 @@ export function normalizeButtonPalette(
*/ */
export function normalizeButtonState( export function normalizeButtonState(
value: string | undefined, value: string | undefined,
defaultValue: "default" = "default" defaultValue: "default" = "default",
): "default" | "focus" | "active" | "hover" | "disabled" { ): "default" | "focus" | "active" | "hover" | "disabled" {
if (!value) return defaultValue; if (!value) return defaultValue;
const normalized = value.toLowerCase(); const normalized = value.toLowerCase();

Before

Width:  |  Height:  |  Size: 793 B

After

Width:  |  Height:  |  Size: 793 B

+9 -3
View File
@@ -179,7 +179,9 @@ export const AllVariants = {
</div> </div>
<div> <div>
<h3 className="text-white font-semibold mb-3">Filled Inverse Variant</h3> <h3 className="text-white font-semibold mb-3">
Filled Inverse Variant
</h3>
<div className="space-x-4"> <div className="space-x-4">
<Button buttonType="filled" palette="inverse" size="xsmall"> <Button buttonType="filled" palette="inverse" size="xsmall">
XSmall XSmall
@@ -221,7 +223,9 @@ export const AllVariants = {
</div> </div>
<div> <div>
<h3 className="text-white font-semibold mb-3">Outline Inverse Variant</h3> <h3 className="text-white font-semibold mb-3">
Outline Inverse Variant
</h3>
<div className="space-x-4"> <div className="space-x-4">
<Button buttonType="outline" palette="inverse" size="xsmall"> <Button buttonType="outline" palette="inverse" size="xsmall">
XSmall XSmall
@@ -305,7 +309,9 @@ export const AllVariants = {
</div> </div>
<div> <div>
<h3 className="text-white font-semibold mb-3">Danger Inverse Variant</h3> <h3 className="text-white font-semibold mb-3">
Danger Inverse Variant
</h3>
<div className="space-x-4"> <div className="space-x-4">
<Button buttonType="danger" palette="inverse" size="xsmall"> <Button buttonType="danger" palette="inverse" size="xsmall">
XSmall XSmall
+6 -2
View File
@@ -273,7 +273,9 @@ export const StateComparison = {
<Button disabled>Disabled</Button> <Button disabled>Disabled</Button>
</div> </div>
<div className="flex flex-wrap gap-4 items-center"> <div className="flex flex-wrap gap-4 items-center">
<Button buttonType="filled" palette="default">Home Default</Button> <Button buttonType="filled" palette="default">
Home Default
</Button>
<Button buttonType="filled" palette="default" disabled> <Button buttonType="filled" palette="default" disabled>
Home Disabled Home Disabled
</Button> </Button>
@@ -341,7 +343,9 @@ export const EdgeCases = {
<div className="flex flex-wrap gap-4 items-center"> <div className="flex flex-wrap gap-4 items-center">
<Button>Normal</Button> <Button>Normal</Button>
<Button disabled>Disabled</Button> <Button disabled>Disabled</Button>
<Button buttonType="filled" palette="default">Home</Button> <Button buttonType="filled" palette="default">
Home
</Button>
<Button buttonType="filled" palette="default" disabled> <Button buttonType="filled" palette="default" disabled>
Home Disabled Home Disabled
</Button> </Button>
+3 -6
View File
@@ -46,8 +46,7 @@ export default {
export const Default = { export const Default = {
args: { args: {
label: "Label", label: "Label",
supportText: supportText: "Members vote to resolve a dispute democratically.",
"Members vote to resolve a dispute democratically.",
recommended: true, recommended: true,
selected: false, selected: false,
orientation: "horizontal", orientation: "horizontal",
@@ -69,8 +68,7 @@ export const HorizontalRecommended = {
export const HorizontalSelected = { export const HorizontalSelected = {
args: { args: {
label: "Label", label: "Label",
supportText: supportText: "Members vote to resolve a dispute democratically.",
"Members vote to resolve a dispute democratically.",
recommended: false, recommended: false,
selected: true, selected: true,
orientation: "horizontal", orientation: "horizontal",
@@ -157,8 +155,7 @@ export const AllVariants = {
parameters: { parameters: {
docs: { docs: {
description: { description: {
story: story: "All four variants: horizontal/vertical × recommended/selected.",
"All four variants: horizontal/vertical × recommended/selected.",
}, },
}, },
}, },
+27 -15
View File
@@ -75,7 +75,8 @@ export const Expanded = {
backgroundColor: "bg-[#b7d9d5]", backgroundColor: "bg-[#b7d9d5]",
expanded: true, expanded: true,
size: "L", size: "L",
logoUrl: "http://localhost:3845/assets/d2513a6ab56f2b2927e8a7c442c06326e7a29541.png", logoUrl:
"http://localhost:3845/assets/d2513a6ab56f2b2927e8a7c442c06326e7a29541.png",
logoAlt: "Mutual Aid Mondays", logoAlt: "Mutual Aid Mondays",
categories: [ categories: [
{ {
@@ -96,9 +97,7 @@ export const Expanded = {
}, },
{ {
name: "Communication", name: "Communication",
chipOptions: [ chipOptions: [{ id: "comm-1", label: "Signal", state: "Unselected" }],
{ id: "comm-1", label: "Signal", state: "Unselected" },
],
onChipClick: (categoryName, chipId) => { onChipClick: (categoryName, chipId) => {
console.log(`Chip clicked: ${categoryName} - ${chipId}`); console.log(`Chip clicked: ${categoryName} - ${chipId}`);
}, },
@@ -122,7 +121,11 @@ export const Expanded = {
name: "Decision-making", name: "Decision-making",
chipOptions: [ chipOptions: [
{ id: "decision-1", label: "Lazy Consensus", state: "Unselected" }, { id: "decision-1", label: "Lazy Consensus", state: "Unselected" },
{ id: "decision-2", label: "Modified Consensus", state: "Unselected" }, {
id: "decision-2",
label: "Modified Consensus",
state: "Unselected",
},
], ],
onChipClick: (categoryName, chipId) => { onChipClick: (categoryName, chipId) => {
console.log(`Chip clicked: ${categoryName} - ${chipId}`); console.log(`Chip clicked: ${categoryName} - ${chipId}`);
@@ -135,7 +138,11 @@ export const Expanded = {
name: "Conflict management", name: "Conflict management",
chipOptions: [ chipOptions: [
{ id: "conflict-1", label: "Code of Conduct", state: "Unselected" }, { id: "conflict-1", label: "Code of Conduct", state: "Unselected" },
{ id: "conflict-2", label: "Restorative Justice", state: "Unselected" }, {
id: "conflict-2",
label: "Restorative Justice",
state: "Unselected",
},
], ],
onChipClick: (categoryName, chipId) => { onChipClick: (categoryName, chipId) => {
console.log(`Chip clicked: ${categoryName} - ${chipId}`); console.log(`Chip clicked: ${categoryName} - ${chipId}`);
@@ -232,7 +239,8 @@ export const ExpandedMedium = {
backgroundColor: "bg-[#b7d9d5]", backgroundColor: "bg-[#b7d9d5]",
expanded: true, expanded: true,
size: "M", size: "M",
logoUrl: "http://localhost:3845/assets/d2513a6ab56f2b2927e8a7c442c06326e7a29541.png", logoUrl:
"http://localhost:3845/assets/d2513a6ab56f2b2927e8a7c442c06326e7a29541.png",
logoAlt: "Mutual Aid Mondays", logoAlt: "Mutual Aid Mondays",
categories: [ categories: [
{ {
@@ -247,9 +255,7 @@ export const ExpandedMedium = {
}, },
{ {
name: "Communication", name: "Communication",
chipOptions: [ chipOptions: [{ id: "comm-1", label: "Signal", state: "Unselected" }],
{ id: "comm-1", label: "Signal", state: "Unselected" },
],
}, },
{ {
name: "Membership", name: "Membership",
@@ -261,14 +267,22 @@ export const ExpandedMedium = {
name: "Decision-making", name: "Decision-making",
chipOptions: [ chipOptions: [
{ id: "decision-1", label: "Lazy Consensus", state: "Unselected" }, { id: "decision-1", label: "Lazy Consensus", state: "Unselected" },
{ id: "decision-2", label: "Modified Consensus", state: "Unselected" }, {
id: "decision-2",
label: "Modified Consensus",
state: "Unselected",
},
], ],
}, },
{ {
name: "Conflict management", name: "Conflict management",
chipOptions: [ chipOptions: [
{ id: "conflict-1", label: "Code of Conduct", state: "Unselected" }, { id: "conflict-1", label: "Code of Conduct", state: "Unselected" },
{ id: "conflict-2", label: "Restorative Justice", state: "Unselected" }, {
id: "conflict-2",
label: "Restorative Justice",
state: "Unselected",
},
], ],
}, },
], ],
@@ -393,9 +407,7 @@ export const InteractiveStates = {
}, },
{ {
name: "Communication", name: "Communication",
chipOptions: [ chipOptions: [{ id: "comm-1", label: "Signal", state: "Unselected" }],
{ id: "comm-1", label: "Signal", state: "Unselected" },
],
onChipClick: (categoryName, chipId) => { onChipClick: (categoryName, chipId) => {
console.log(`Chip clicked: ${categoryName} - ${chipId}`); console.log(`Chip clicked: ${categoryName} - ${chipId}`);
}, },
+12 -5
View File
@@ -47,12 +47,14 @@ export default {
mode: { mode: {
control: "select", control: "select",
options: ["standard", "inverse", "Standard", "Inverse"], options: ["standard", "inverse", "Standard", "Inverse"],
description: "Visual mode of the checkbox (case-insensitive: accepts both lowercase and PascalCase)", description:
"Visual mode of the checkbox (case-insensitive: accepts both lowercase and PascalCase)",
}, },
state: { state: {
control: "select", control: "select",
options: ["default", "hover", "focus", "Default", "Hover", "Focus"], options: ["default", "hover", "focus", "Default", "Hover", "Focus"],
description: "Interaction state for static display (case-insensitive: accepts both lowercase and PascalCase)", description:
"Interaction state for static display (case-insensitive: accepts both lowercase and PascalCase)",
}, },
disabled: { disabled: {
control: "boolean", control: "boolean",
@@ -215,9 +217,12 @@ export const FigmaPascalCase = () => {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<h3 className="text-lg font-semibold mb-4 text-white">Figma PascalCase Props (Standard/Inverse)</h3> <h3 className="text-lg font-semibold mb-4 text-white">
Figma PascalCase Props (Standard/Inverse)
</h3>
<p className="text-sm text-gray-400 mb-4"> <p className="text-sm text-gray-400 mb-4">
These components accept both PascalCase (from Figma) and lowercase (from codebase) prop values. These components accept both PascalCase (from Figma) and lowercase
(from codebase) prop values.
</p> </p>
<div className="space-y-4"> <div className="space-y-4">
<Checkbox <Checkbox
@@ -237,7 +242,9 @@ export const FigmaPascalCase = () => {
</div> </div>
</div> </div>
<div> <div>
<h3 className="text-lg font-semibold mb-4 text-white">Mixed Case (backward compatibility)</h3> <h3 className="text-lg font-semibold mb-4 text-white">
Mixed Case (backward compatibility)
</h3>
<div className="space-y-4"> <div className="space-y-4">
<Checkbox <Checkbox
label="Standard mode (lowercase) - still works" label="Standard mode (lowercase) - still works"
+34 -10
View File
@@ -22,12 +22,23 @@ export default {
mode: { mode: {
control: "select", control: "select",
options: ["standard", "inverse", "Standard", "Inverse"], options: ["standard", "inverse", "Standard", "Inverse"],
description: "Visual mode of the radio button (case-insensitive: accepts both lowercase and PascalCase)", description:
"Visual mode of the radio button (case-insensitive: accepts both lowercase and PascalCase)",
}, },
state: { state: {
control: "select", control: "select",
options: ["default", "hover", "focus", "selected", "Default", "Hover", "Focus", "Selected"], options: [
description: "Interaction state for static display (case-insensitive: accepts both lowercase and PascalCase)", "default",
"hover",
"focus",
"selected",
"Default",
"Hover",
"Focus",
"Selected",
],
description:
"Interaction state for static display (case-insensitive: accepts both lowercase and PascalCase)",
}, },
disabled: { disabled: {
control: "boolean", control: "boolean",
@@ -188,7 +199,9 @@ export const StandardAllStates = () => {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<h3 className="text-lg font-semibold mb-4 text-white">Standard Mode - Unselected</h3> <h3 className="text-lg font-semibold mb-4 text-white">
Standard Mode - Unselected
</h3>
<div className="space-y-4"> <div className="space-y-4">
<RadioButton <RadioButton
label="Unselected (default, hover, focus)" label="Unselected (default, hover, focus)"
@@ -200,7 +213,9 @@ export const StandardAllStates = () => {
</div> </div>
<div> <div>
<h3 className="text-lg font-semibold mb-4 text-white">Standard Mode - Selected</h3> <h3 className="text-lg font-semibold mb-4 text-white">
Standard Mode - Selected
</h3>
<div className="space-y-4"> <div className="space-y-4">
<RadioButton <RadioButton
label="Selected (default, hover, focus)" label="Selected (default, hover, focus)"
@@ -222,7 +237,9 @@ export const InverseAllStates = () => {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<h3 className="text-lg font-semibold mb-4 text-white">Inverse Mode - Unselected</h3> <h3 className="text-lg font-semibold mb-4 text-white">
Inverse Mode - Unselected
</h3>
<div className="space-y-4"> <div className="space-y-4">
<RadioButton <RadioButton
label="Unselected (default, hover, focus)" label="Unselected (default, hover, focus)"
@@ -234,7 +251,9 @@ export const InverseAllStates = () => {
</div> </div>
<div> <div>
<h3 className="text-lg font-semibold mb-4 text-white">Inverse Mode - Selected</h3> <h3 className="text-lg font-semibold mb-4 text-white">
Inverse Mode - Selected
</h3>
<div className="space-y-4"> <div className="space-y-4">
<RadioButton <RadioButton
label="Selected (default, hover, focus)" label="Selected (default, hover, focus)"
@@ -256,9 +275,12 @@ export const FigmaPascalCase = () => {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<h3 className="text-lg font-semibold mb-4 text-white">Figma PascalCase Props (Standard/Inverse)</h3> <h3 className="text-lg font-semibold mb-4 text-white">
Figma PascalCase Props (Standard/Inverse)
</h3>
<p className="text-sm text-gray-400 mb-4"> <p className="text-sm text-gray-400 mb-4">
These components accept both PascalCase (from Figma) and lowercase (from codebase) prop values. These components accept both PascalCase (from Figma) and lowercase
(from codebase) prop values.
</p> </p>
<div className="space-y-4"> <div className="space-y-4">
<RadioButton <RadioButton
@@ -278,7 +300,9 @@ export const FigmaPascalCase = () => {
</div> </div>
</div> </div>
<div> <div>
<h3 className="text-lg font-semibold mb-4 text-white">Mixed Case (backward compatibility)</h3> <h3 className="text-lg font-semibold mb-4 text-white">
Mixed Case (backward compatibility)
</h3>
<div className="space-y-4"> <div className="space-y-4">
<RadioButton <RadioButton
label="Standard mode (lowercase) - still works" label="Standard mode (lowercase) - still works"
+2 -1
View File
@@ -10,7 +10,8 @@ export default {
argTypes: { argTypes: {
propSwitch: { propSwitch: {
control: "boolean", control: "boolean",
description: "Whether the switch is checked (on) or not (off) (Figma prop)", description:
"Whether the switch is checked (on) or not (off) (Figma prop)",
}, },
state: { state: {
control: "select", control: "select",
+2 -1
View File
@@ -74,7 +74,8 @@ export const Embedded = Template.bind({});
Embedded.args = { Embedded.args = {
label: "Section content", label: "Section content",
placeholder: "Enter text...", placeholder: "Enter text...",
value: "Embedded appearance used in create-flow modals: borderless, darker grey block.", value:
"Embedded appearance used in create-flow modals: borderless, darker grey block.",
appearance: "embedded", appearance: "embedded",
size: "large", size: "large",
rows: 4, rows: 4,
+4 -2
View File
@@ -51,7 +51,8 @@ Active.args = {
Active.parameters = { Active.parameters = {
docs: { docs: {
description: { description: {
story: "Upload component in active state with white button and black text.", story:
"Upload component in active state with white button and black text.",
}, },
}, },
}; };
@@ -66,7 +67,8 @@ Inactive.args = {
Inactive.parameters = { Inactive.parameters = {
docs: { docs: {
description: { description: {
story: "Upload component in inactive state with dark button and gray text.", story:
"Upload component in inactive state with dark button and gray text.",
}, },
}, },
}; };
+43 -12
View File
@@ -1,4 +1,4 @@
import Logo from "../../app/components/icons/Logo"; import Logo from "../../app/components/asset/logo";
export default { export default {
title: "Components/Icons/Logo", title: "Components/Icons/Logo",
@@ -24,9 +24,15 @@ export default {
], ],
description: "The size variant of the logo", description: "The size variant of the logo",
}, },
showText: { palette: {
control: { type: "select" },
options: ["default", "inverse"],
description:
"Visual style: default (dark on light) or inverse (e.g. on teal)",
},
wordmark: {
control: { type: "boolean" }, control: { type: "boolean" },
description: "Whether to show the text portion of the logo", description: "Whether to show the CommunityRule wordmark",
}, },
}, },
tags: ["autodocs"], tags: ["autodocs"],
@@ -35,13 +41,13 @@ export default {
export const Default = { export const Default = {
args: { args: {
size: "default", size: "default",
showText: true, wordmark: true,
}, },
}; };
export const Sizes = { export const Sizes = {
args: { args: {
showText: true, wordmark: true,
}, },
render: (args) => ( render: (args) => (
<div className="space-y-6"> <div className="space-y-6">
@@ -76,7 +82,7 @@ export const Sizes = {
export const IconOnly = { export const IconOnly = {
args: { args: {
size: "default", size: "default",
showText: false, wordmark: false,
}, },
render: (args) => ( render: (args) => (
<div className="space-y-6"> <div className="space-y-6">
@@ -123,11 +129,11 @@ export const TopNavContext = {
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<span className="text-white text-sm w-32">FolderTop:</span> <span className="text-white text-sm w-32">FolderTop:</span>
<Logo size="topNavFolderTop" showText={true} /> <Logo size="topNavFolderTop" wordmark palette="inverse" />
</div> </div>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<span className="text-white text-sm w-32">Header:</span> <span className="text-white text-sm w-32">Header:</span>
<Logo size="topNavHeader" showText={true} /> <Logo size="topNavHeader" wordmark />
</div> </div>
</div> </div>
</div> </div>
@@ -148,13 +154,11 @@ export const CreateFlowContext = {
render: () => ( render: () => (
<div className="min-h-screen bg-black p-8"> <div className="min-h-screen bg-black p-8">
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto">
<h2 className="text-white font-semibold mb-6"> <h2 className="text-white font-semibold mb-6">Create Flow Context</h2>
Create Flow Context
</h2>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<span className="text-white text-sm w-32">CreateFlow:</span> <span className="text-white text-sm w-32">CreateFlow:</span>
<Logo size="createFlow" showText={true} /> <Logo size="createFlow" wordmark />
</div> </div>
</div> </div>
</div> </div>
@@ -169,3 +173,30 @@ export const CreateFlowContext = {
}, },
}, },
}; };
export const CreateFlowCompletedInverse = {
args: {},
render: () => (
<div
className="min-h-screen p-8"
style={{ background: "var(--color-teal-teal50, #c9fef9)" }}
>
<div className="max-w-4xl mx-auto">
<h2 className="font-semibold mb-6 text-[var(--color-content-invert-primary)]">
Completed page (inverse on teal)
</h2>
<div className="space-y-4">
<Logo size="createFlow" wordmark palette="inverse" />
</div>
</div>
</div>
),
parameters: {
docs: {
description: {
story:
"Same size as CreateFlowTopNav with inverse palette, as used on the completed page.",
},
},
},
};
+2 -1
View File
@@ -139,7 +139,8 @@ WithCustomHeader.args = {
children: ( children: (
<div className="space-y-4"> <div className="space-y-4">
<p className="text-[var(--color-content-default-primary)]"> <p className="text-[var(--color-content-default-primary)]">
When headerContent is provided, the default title and description are not shown. When headerContent is provided, the default title and description are
not shown.
</p> </p>
</div> </div>
), ),
+10 -5
View File
@@ -136,9 +136,15 @@ export const AllModes = {
<div> <div>
<h3 className="text-white font-semibold mb-3">Default Mode</h3> <h3 className="text-white font-semibold mb-3">Default Mode</h3>
<div className="space-x-4"> <div className="space-x-4">
<MenuBarItem size="X Small" mode="default">X Small</MenuBarItem> <MenuBarItem size="X Small" mode="default">
<MenuBarItem size="Large" mode="default">Large</MenuBarItem> X Small
<MenuBarItem size="X Large" mode="default">X Large</MenuBarItem> </MenuBarItem>
<MenuBarItem size="Large" mode="default">
Large
</MenuBarItem>
<MenuBarItem size="X Large" mode="default">
X Large
</MenuBarItem>
</div> </div>
</div> </div>
@@ -173,8 +179,7 @@ export const AllModes = {
parameters: { parameters: {
docs: { docs: {
description: { description: {
story: story: "Complete overview of all menu item modes, sizes, and states.",
"Complete overview of all menu item modes, sizes, and states.",
}, },
}, },
}, },
+6 -4
View File
@@ -15,11 +15,13 @@ export default {
argTypes: { argTypes: {
folderTop: { folderTop: {
control: "boolean", control: "boolean",
description: "When true, renders the home page variant with yellow tab container. When false, renders the standard header variant.", description:
"When true, renders the home page variant with yellow tab container. When false, renders the standard header variant.",
}, },
loggedIn: { loggedIn: {
control: "boolean", control: "boolean",
description: "Whether the user is logged in (affects displayed elements).", description:
"Whether the user is logged in (affects displayed elements).",
}, },
profile: { profile: {
control: "boolean", control: "boolean",
@@ -123,8 +125,8 @@ export const StandardInPageContext = {
</h1> </h1>
<p className="text-white mb-4"> <p className="text-white mb-4">
This demonstrates how the standard header looks in a realistic page This demonstrates how the standard header looks in a realistic page
context. The header maintains its responsive behavior while providing context. The header maintains its responsive behavior while
navigation for the page content. providing navigation for the page content.
</p> </p>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{[1, 2, 3, 4, 5, 6].map((i) => ( {[1, 2, 3, 4, 5, 6].map((i) => (
+1 -2
View File
@@ -44,8 +44,7 @@ export const SizeM = {
export const CenterJustified = { export const CenterJustified = {
args: { args: {
title: "How should conflicts be resolved?", title: "How should conflicts be resolved?",
description: description: "You can also combine or add new approaches to the list",
"You can also combine or add new approaches to the list",
justification: "center", justification: "center",
size: "L", size: "L",
}, },
+2 -1
View File
@@ -20,7 +20,8 @@ export default {
}, },
secondButton: { secondButton: {
control: false, control: false,
description: "The second button (typically Next) to display on the right side", description:
"The second button (typically Next) to display on the right side",
}, },
}, },
tags: ["autodocs"], tags: ["autodocs"],
+72
View File
@@ -0,0 +1,72 @@
import React from "react";
import { describe, it, expect } from "vitest";
import { renderWithProviders as render, screen } from "../utils/test-utils";
import "@testing-library/jest-dom/vitest";
import CompletedPage from "../../app/create/completed/page";
describe("CompletedPage", () => {
it("renders without crashing", () => {
render(<CompletedPage />);
expect(screen.getByRole("heading", { level: 1 })).toBeInTheDocument();
});
it("renders HeaderLockup with expected title", () => {
render(<CompletedPage />);
expect(
screen.getByRole("heading", {
name: "Mutual Aid Mondays",
}),
).toBeInTheDocument();
});
it("renders HeaderLockup with expected description", () => {
render(<CompletedPage />);
expect(
screen.getByText(
/Mutual Aid Monday is a grassroots community in Denver, founded in November 2020 by Kelsang Virya, dedicated to supporting neighbors experiencing homelessness./i,
),
).toBeInTheDocument();
});
it("renders Community Rule document with section labels", () => {
render(<CompletedPage />);
expect(screen.getByText("Values")).toBeInTheDocument();
expect(screen.getByText("Communication")).toBeInTheDocument();
expect(screen.getByText("Membership")).toBeInTheDocument();
expect(screen.getByText("Decision-making")).toBeInTheDocument();
expect(screen.getByText("Conflict management")).toBeInTheDocument();
});
it("renders document entry titles", () => {
render(<CompletedPage />);
expect(screen.getByText("Solidarity Forever")).toBeInTheDocument();
expect(screen.getByText("Shared Leadership")).toBeInTheDocument();
expect(screen.getByText("Organizing Offline")).toBeInTheDocument();
expect(screen.getByText("Circular Food Systems")).toBeInTheDocument();
});
it("renders toast alert when page loads", () => {
render(<CompletedPage />);
expect(
screen.getByText(
"This is what folks see when you share your CommunityRule",
),
).toBeInTheDocument();
expect(
screen.getByText(
"Your group can use this document as an operating manual.",
),
).toBeInTheDocument();
});
it("renders toast with role status", () => {
render(<CompletedPage />);
const statusRegions = screen.getAllByRole("status");
expect(statusRegions.length).toBeGreaterThanOrEqual(1);
expect(
statusRegions.some((el) =>
el.textContent?.includes("This is what folks see when you share"),
),
).toBe(true);
});
});
+12 -3
View File
@@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { render, screen } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest"; import { describe, it, expect } from "vitest";
import Logo from "../../app/components/icons/Logo"; import Logo from "../../app/components/asset/logo";
import { import {
componentTestSuite, componentTestSuite,
ComponentTestSuiteConfig, ComponentTestSuiteConfig,
@@ -45,13 +45,22 @@ describe("Logo (behavioral tests)", () => {
expect(screen.getByText("CommunityRule")).toBeInTheDocument(); expect(screen.getByText("CommunityRule")).toBeInTheDocument();
}); });
it("hides text when showText is false", () => { it("hides wordmark when wordmark is false", () => {
const { container } = render(<Logo showText={false} />); const { container } = render(<Logo wordmark={false} />);
const textElement = container.querySelector(".hidden"); const textElement = container.querySelector(".hidden");
expect(textElement).toBeInTheDocument(); expect(textElement).toBeInTheDocument();
expect(screen.getByAltText("CommunityRule Logo Icon")).toBeInTheDocument(); expect(screen.getByAltText("CommunityRule Logo Icon")).toBeInTheDocument();
}); });
it("applies inverse palette styling when palette is inverse", () => {
render(<Logo palette="inverse" />);
const link = screen.getByRole("link");
const textEl = link.querySelector(".font-bricolage-grotesque");
const img = link.querySelector("img");
expect(textEl).toHaveClass("text-[var(--color-content-invert-primary)]");
expect(img).toHaveClass("brightness-0");
});
it("renders with different size variants", () => { it("renders with different size variants", () => {
const { rerender } = render(<Logo size="default" />); const { rerender } = render(<Logo size="default" />);
expect(screen.getByRole("link")).toBeInTheDocument(); expect(screen.getByRole("link")).toBeInTheDocument();
+1 -3
View File
@@ -17,9 +17,7 @@ vi.mock("next/dynamic", () => {
function DynamicWrapper(props) { function DynamicWrapper(props) {
const [Component, setComponent] = React.useState(null); const [Component, setComponent] = React.useState(null);
React.useEffect(() => { React.useEffect(() => {
importFn().then((mod) => importFn().then((mod) => setComponent(() => mod.default || mod));
setComponent(() => mod.default || mod),
);
}, []); }, []);
if (!Component) { if (!Component) {
return options?.loading ? options.loading() : null; return options?.loading ? options.loading() : null;