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",
logo: {
"@type": "ImageObject",
url: "https://communityrule.com/assets/Logo.svg",
url: "https://communityrule.com/assets/logo/Logo.svg",
},
},
datePublished: post.frontmatter.date,
+18 -12
View File
@@ -12,12 +12,15 @@ const LogoWall = dynamic(() => import("../components/sections/LogoWall"), {
ssr: true,
});
const NumberedCards = dynamic(() => import("../components/sections/NumberedCards"), {
loading: () => (
<section className="py-[var(--spacing-scale-032)] min-h-[300px]" />
),
ssr: true,
});
const NumberedCards = dynamic(
() => import("../components/sections/NumberedCards"),
{
loading: () => (
<section className="py-[var(--spacing-scale-032)] min-h-[300px]" />
),
ssr: true,
},
);
const RuleStack = dynamic(() => import("../components/sections/RuleStack"), {
loading: () => (
@@ -26,12 +29,15 @@ const RuleStack = dynamic(() => import("../components/sections/RuleStack"), {
ssr: true,
});
const FeatureGrid = dynamic(() => import("../components/sections/FeatureGrid"), {
loading: () => (
<section className="py-[var(--spacing-scale-032)] min-h-[500px]" />
),
ssr: true,
});
const FeatureGrid = dynamic(
() => import("../components/sections/FeatureGrid"),
{
loading: () => (
<section className="py-[var(--spacing-scale-032)] min-h-[500px]" />
),
ssr: true,
},
);
const QuoteBlock = dynamic(() => import("../components/sections/QuoteBlock"), {
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> {
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) */
const iconMap: Record<
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,
};
@@ -31,8 +32,14 @@ function IconComponent({
if (!SvgModule) return null;
// Turbopack/bundler may expose SVG as { default: Component } instead of the component directly
const Svg =
typeof SvgModule === "object" && SvgModule !== null && "default" in SvgModule
? (SvgModule as { default: React.ComponentType<React.SVGProps<SVGSVGElement>> }).default
typeof SvgModule === "object" &&
SvgModule !== null &&
"default" in SvgModule
? (
SvgModule as {
default: React.ComponentType<React.SVGProps<SVGSVGElement>>;
}
).default
: (SvgModule as React.ComponentType<React.SVGProps<SVGSVGElement>>);
if (typeof Svg !== "function") return null;
return (
+1
View File
@@ -1,2 +1,3 @@
export { default as Icon } 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 sizeStyles: Record<string, string> = {
xsmall:
"p-[var(--spacing-scale-004)] gap-[var(--spacing-scale-002)]",
small:
"p-[var(--spacing-scale-008)] gap-[var(--spacing-scale-002)]",
xsmall: "p-[var(--spacing-scale-004)] 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)]",
large:
"p-[var(--spacing-scale-012)] gap-[var(--spacing-scale-006)]",
xlarge:
"p-[var(--spacing-scale-016)] gap-[var(--spacing-scale-008)]",
large: "p-[var(--spacing-scale-012)] gap-[var(--spacing-scale-006)]",
xlarge: "p-[var(--spacing-scale-016)] gap-[var(--spacing-scale-008)]",
};
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",
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",
"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:
"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":
+20 -15
View File
@@ -29,7 +29,8 @@ interface NumberCardProps {
const NumberCard = memo<NumberCardProps>(({ number, text, size: sizeProp }) => {
// 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
// Otherwise, use responsive breakpoints for backward compatibility
@@ -40,16 +41,22 @@ const NumberCard = memo<NumberCardProps>(({ number, text, size: sizeProp }) => {
const sizeClasses = {
Small: "flex flex-col items-end justify-center gap-4 p-5 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",
XLarge: "flex flex-col items-start justify-end gap-[22px] h-[238px] p-8 relative",
Large:
"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
const textClasses = {
Small: "font-bricolage-grotesque font-medium text-[24px] leading-[32px] text-[#141414]",
Medium: "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]",
Small:
"font-bricolage-grotesque font-medium text-[24px] leading-[32px] text-[#141414]",
Medium:
"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
@@ -74,11 +81,9 @@ const NumberCard = memo<NumberCardProps>(({ number, text, size: sizeProp }) => {
<div className={`${baseClasses} ${sizeClasses[size]}`}>
{/* Section Number - Direct child for Small */}
<SectionNumber number={number} />
{/* Card Content */}
<p className={textClasses[size]}>
{text}
</p>
<p className={textClasses[size]}>{text}</p>
</div>
);
}
@@ -92,9 +97,7 @@ const NumberCard = memo<NumberCardProps>(({ number, text, size: sizeProp }) => {
{/* Card Content */}
<div className={contentClasses[size]}>
<p className={textClasses[size]}>
{text}
</p>
<p className={textClasses[size]}>{text}</p>
</div>
</div>
);
@@ -103,7 +106,9 @@ const NumberCard = memo<NumberCardProps>(({ number, text, size: sizeProp }) => {
// Responsive breakpoints for backward compatibility (matches original behavior)
// Maps to: Small (mobile) -> Medium (sm) -> Large (lg) -> XLarge (xl)
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 */}
<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} />
@@ -5,7 +5,11 @@ export interface Category {
chipOptions: ChipOption[];
onChipClick?: (categoryName: string, chipId: 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;
}
+82 -56
View File
@@ -28,34 +28,39 @@ export function RuleCardView({
const isMedium = size === "M";
const isSmall = size === "S";
const isExtraSmall = size === "XS";
// Card dimensions - use CSS classes from className if provided, otherwise use size-based logic
// 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 cardPadding = hasResponsivePadding
? "" // If className has responsive padding, don't add size-based padding
: isLarge || isSmall
? "p-[24px]"
: isMedium
? "p-[16px]"
: "pb-[24px] pt-[12px] px-[12px]"; // XS: asymmetric padding
? "p-[24px]"
: isMedium
? "p-[16px]"
: "pb-[24px] pt-[12px] px-[12px]"; // XS: asymmetric padding
const cardGap = expanded
? "gap-[16px]"
: hasResponsiveGap
? "" // If className has responsive gap, don't add size-based gap
: isLarge
? "gap-[10px]"
: isMedium
? "gap-[12px]"
: "gap-[18px]"; // XS and S: 18px gap
? "" // If className has responsive gap, don't add size-based gap
: isLarge
? "gap-[10px]"
: isMedium
? "gap-[12px]"
: "gap-[18px]"; // XS and S: 18px gap
const cardWidth = expanded
? isLarge
? "w-[568px]"
: isMedium
? "w-[398px]"
: "" // XS and S: no fixed width
? "w-[398px]"
: "" // XS and S: no fixed width
: "";
// Logo/Icon dimensions - use CSS responsive classes
@@ -81,19 +86,21 @@ export function RuleCardView({
const descriptionClass = isLarge
? "font-inter font-medium text-[18px] leading-[24px]"
: isMedium
? "font-inter font-medium text-[14px] leading-[16px]"
: isSmall
? "font-inter font-medium text-[14px] leading-[16px]" // S: 14px, medium, Inter
: "font-inter font-medium text-[12px] leading-[14px]"; // XS: 12px, medium, Inter
? "font-inter font-medium text-[14px] leading-[16px]"
: isSmall
? "font-inter font-medium text-[14px] leading-[16px]" // S: 14px, medium, Inter
: "font-inter font-medium text-[12px] leading-[14px]"; // XS: 12px, medium, Inter
// Render logo/icon
const renderLogo = () => {
if (logoUrl) {
// 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]`;
if (isLocalhost) {
return (
<div className={containerClass}>
@@ -108,7 +115,7 @@ export function RuleCardView({
</div>
);
}
return (
<div className={containerClass}>
<Image
@@ -121,15 +128,17 @@ export function RuleCardView({
</div>
);
}
if (icon) {
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}
</div>
);
}
if (communityInitials) {
const initialsSize = `
max-[639px]:text-[16px]
@@ -138,26 +147,29 @@ export function RuleCardView({
min-[1440px]:text-[36px]
`;
return (
<div 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)]`}>
<div
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}
</span>
</div>
);
}
return null;
};
// 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
: isExtraSmall
? "rounded-[var(--measures-radius-200,8px)]"
: isSmall
? "rounded-[var(--measures-radius-300,12px)]"
: "rounded-[var(--radius-measures-radius-small)]";
: isExtraSmall
? "rounded-[var(--measures-radius-200,8px)]"
: isSmall
? "rounded-[var(--measures-radius-300,12px)]"
: "rounded-[var(--radius-measures-radius-small)]";
return (
<div
@@ -170,48 +182,60 @@ export function RuleCardView({
onKeyDown={onKeyDown}
>
{/* 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
max-[639px]:h-[72px]
min-[640px]:max-[1023px]:h-[80px]
min-[1024px]:max-[1439px]:h-[88px]
min-[1440px]:h-[136px]
`}>
`}
>
{/* Logo/Icon - fixed width/height, vertically centered, does not touch bottom */}
{renderLogo() && (
<div className={`
<div
className={`
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
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-[1440px]:w-[103px] min-[1440px]:h-[103px]
`}>
`}
>
{renderLogo()}
</div>
)}
{/* Spacing between icon and title */}
<div className="
<div
className="
max-[1023px]:hidden
min-[1024px]:w-[16px] min-[1024px]:shrink-0
" />
"
/>
{/* Container with no padding and left border - extends full height to touch bottom */}
{title && (
<div className={`
<div
className={`
flex-1 min-w-0 h-full flex
max-[1023px]:border-0
min-[1024px]:border-l min-[1024px]:border-black min-[1024px]:border-solid
`}>
`}
>
{/* Inner container for header text with padding */}
<div className={`
<div
className={`
flex items-center justify-center w-full
max-[639px]:pl-[8px] max-[639px]:py-[8px]
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-[1440px]:px-[16px] min-[1440px]:py-[24px]
`}>
<h3 className={`${titleClass} text-black overflow-hidden text-ellipsis w-full`}>
{title}
</h3>
`}
>
<h3
className={`${titleClass} text-black overflow-hidden text-ellipsis w-full`}
>
{title}
</h3>
</div>
</div>
)}
@@ -237,7 +261,11 @@ export function RuleCardView({
category.onAddClick?.(category.name);
}}
onCustomChipConfirm={(chipId, value) => {
category.onCustomChipConfirm?.(category.name, chipId, value);
category.onCustomChipConfirm?.(
category.name,
chipId,
value,
);
}}
onCustomChipClose={(chipId) => {
category.onCustomChipClose?.(category.name, chipId);
@@ -250,11 +278,9 @@ export function RuleCardView({
</div>
)}
{/* Footer: Description */}
{description && (
{description && (
<div className="border-t border-black border-solid pt-[16px] relative shrink-0 w-full">
<p className={`${descriptionClass} text-black`}>
{description}
</p>
<p className={`${descriptionClass} text-black`}>{description}</p>
</div>
)}
</>
@@ -263,8 +289,8 @@ export function RuleCardView({
description && (
<div className="flex items-center justify-center relative shrink-0 w-full">
<p className={`${descriptionClass} text-black flex-1`}>
{description}
</p>
{description}
</p>
</div>
)
)}
@@ -1,6 +1,10 @@
import type { BlogPost } from "../../../../lib/content";
export type ContentContainerSizeValue = "xs" | "responsive" | "Xs" | "Responsive";
export type ContentContainerSizeValue =
| "xs"
| "responsive"
| "Xs"
| "Responsive";
export interface ContentContainerProps {
post: BlogPost;
@@ -1,6 +1,10 @@
import type { BlogPost } from "../../../../lib/content";
export type ContentThumbnailTemplateVariantValue = "vertical" | "horizontal" | "Vertical" | "Horizontal";
export type ContentThumbnailTemplateVariantValue =
| "vertical"
| "horizontal"
| "Vertical"
| "Horizontal";
export interface ContentThumbnailTemplateProps {
post: BlogPost;
@@ -4,7 +4,10 @@ import { memo } from "react";
import { useComponentId } from "../../../hooks";
import { CheckboxView } from "./Checkbox.view";
import type { CheckboxProps } from "./Checkbox.types";
import { normalizeMode, normalizeState } from "../../../../lib/propNormalization";
import {
normalizeMode,
normalizeState,
} from "../../../../lib/propNormalization";
const CheckboxContainer = memo<CheckboxProps>(
({
@@ -24,7 +27,7 @@ const CheckboxContainer = memo<CheckboxProps>(
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const mode = normalizeMode(modeProp);
const state = normalizeState(stateProp);
const isInverse = mode === "inverse";
const isStandard = mode === "standard";
@@ -43,7 +46,9 @@ const CheckboxContainer = memo<CheckboxProps>(
transition-all
duration-200
ease-in-out
`.trim().replace(/\s+/g, " ");
`
.trim()
.replace(/\s+/g, " ");
// Get box styles based on state and checked status per Figma designs
const getBoxStyles = (): string => {
@@ -22,8 +22,10 @@ const CheckboxGroupContainer = ({
const groupId = name || `checkbox-group-${generatedId}`;
// 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
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 (option.subtext) {
return (
<div
key={option.value}
className="flex gap-[8px] items-start"
>
<div key={option.value} className="flex gap-[8px] items-start">
<Checkbox
checked={isChecked}
mode={mode}
@@ -41,7 +41,10 @@ const ChipContainer = memo<ChipProps>(
}
}, [isCustom]);
const handleCheck = (value: string, event: React.MouseEvent<HTMLButtonElement>) => {
const handleCheck = (
value: string,
event: React.MouseEvent<HTMLButtonElement>,
) => {
if (onCheck && value.trim()) {
onCheck(value.trim(), event);
// Reset input after successful check
@@ -63,7 +66,10 @@ const ChipContainer = memo<ChipProps>(
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter" && inputValue.trim() && onCheck) {
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) {
event.preventDefault();
handleClose(event as unknown as React.MouseEvent<HTMLButtonElement>);
@@ -95,4 +101,3 @@ const ChipContainer = memo<ChipProps>(
ChipContainer.displayName = "Chip";
export default ChipContainer;
@@ -68,4 +68,3 @@ export interface ChipViewProps {
inputRef?: React.RefObject<HTMLInputElement>;
ariaLabel?: string;
}
+24 -31
View File
@@ -42,32 +42,26 @@ function ChipView({
// Palette + state styling based on Figma examples
// Use consistent border width to prevent layout shift
const borderWidth = isSmall ? "border-[1.25px]" : "border-2";
let background = "bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))]";
let border =
`${borderWidth} border-[var(--color-border-default-tertiary,#464646)]`;
let background =
"bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))]";
let border = `${borderWidth} border-[var(--color-border-default-tertiary,#464646)]`;
let textColor =
"text-[color:var(--color-content-default-brand-primary,#fefcc9)]";
if (isDefault) {
if (state === "custom") {
background =
"bg-[var(--color-surface-default-secondary,#141414)]"; // dark background for custom
background = "bg-[var(--color-surface-default-secondary,#141414)]"; // dark background for custom
border = "border-none";
textColor =
"text-[color:var(--color-content-default-tertiary,#b4b4b4)]";
textColor = "text-[color:var(--color-content-default-tertiary,#b4b4b4)]";
} else if (state === "disabled") {
background =
"bg-[var(--color-surface-default-secondary,#141414)]"; // dark background
background = "bg-[var(--color-surface-default-secondary,#141414)]"; // dark background
border = "border-none";
textColor =
"text-[color:var(--color-content-default-tertiary,#b4b4b4)]";
textColor = "text-[color:var(--color-content-default-tertiary,#b4b4b4)]";
} else if (isSelected) {
background =
"bg-[var(--color-surface-inverse-brandaccent,#fdfaa8)]"; // yellow selected
background = "bg-[var(--color-surface-inverse-brandaccent,#fdfaa8)]"; // yellow selected
border = `${borderWidth} border-[var(--color-border-default-brand-primary,#fdfaa8)]`;
textColor =
"text-[color:var(--color-content-inverse-primary,black)]";
textColor = "text-[color:var(--color-content-inverse-primary,black)]";
} else {
// Unselected default
background =
@@ -78,24 +72,20 @@ function ChipView({
}
} else if (isInverse) {
if (state === "disabled") {
background =
"bg-[var(--color-surface-inverse-tertiary,#d2d2d2)]";
background = "bg-[var(--color-surface-inverse-tertiary,#d2d2d2)]";
border = "border-none";
textColor =
"text-[color:var(--color-content-inverse-primary,black)]";
textColor = "text-[color:var(--color-content-inverse-primary,black)]";
} else if (isSelected) {
background =
"bg-[var(--color-surface-default-semi-opaque,rgba(0,0,0,0.05))]";
border = `${borderWidth} border-[var(--color-border-default-primary,#141414)]`;
textColor =
"text-[color:var(--color-content-inverse-primary,black)]";
textColor = "text-[color:var(--color-content-inverse-primary,black)]";
} else {
// Unselected / custom inverse
background =
"bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))]";
border = `${borderWidth} border-[var(--color-border-default-primary,#141414)]`;
textColor =
"text-[color:var(--color-content-inverse-primary,black)]";
textColor = "text-[color:var(--color-content-inverse-primary,black)]";
}
}
@@ -134,7 +124,9 @@ function ChipView({
.filter(Boolean)
.join(" ");
const handleClick = (event: React.MouseEvent<HTMLButtonElement | HTMLDivElement>) => {
const handleClick = (
event: React.MouseEvent<HTMLButtonElement | HTMLDivElement>,
) => {
if (isDisabled) {
event.preventDefault();
return;
@@ -162,7 +154,9 @@ function ChipView({
}}
{...sharedA11y}
>
<div className={`flex items-center ${isSmall ? "gap-[8px]" : "gap-[12px]"}`}>
<div
className={`flex items-center ${isSmall ? "gap-[8px]" : "gap-[12px]"}`}
>
{/* Check button */}
{onCheck && (
<button
@@ -208,7 +202,9 @@ function ChipView({
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)]"
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",
}}
onClick={(e) => e.stopPropagation()}
@@ -259,9 +255,7 @@ function ChipView({
onClick={handleClick}
{...sharedA11y}
>
<span className="flex items-center justify-center">
{label}
</span>
<span className="flex items-center justify-center">{label}</span>
{onRemove && !isDisabled && (
<button
type="button"
@@ -284,4 +278,3 @@ function ChipView({
ChipView.displayName = "ChipView";
export default memo(ChipView);
-1
View File
@@ -1,3 +1,2 @@
export { default } from "./Chip.container";
export type { ChipProps } from "./Chip.types";
@@ -3,7 +3,10 @@
import { memo } from "react";
import MultiSelectView from "./MultiSelect.view";
import type { MultiSelectProps } from "./MultiSelect.types";
import { normalizeMultiSelectSize, normalizeChipPalette } from "../../../../lib/propNormalization";
import {
normalizeMultiSelectSize,
normalizeChipPalette,
} from "../../../../lib/propNormalization";
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 {
id: string;
@@ -31,7 +31,9 @@ function MultiSelectView({
const chipSize = isSmall ? "S" : "M";
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 */}
{formHeader && label && (
<InputLabel
@@ -45,7 +47,9 @@ function MultiSelectView({
)}
{/* 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) => (
<Chip
key={option.id}
@@ -110,7 +114,9 @@ function MultiSelectView({
</svg>
{/* Text - only show if addButtonText is provided */}
{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}
</span>
)}
@@ -3,7 +3,10 @@
import { memo, useCallback, useId } from "react";
import { RadioButtonView } from "./RadioButton.view";
import type { RadioButtonProps } from "./RadioButton.types";
import { normalizeMode, normalizeState } from "../../../../lib/propNormalization";
import {
normalizeMode,
normalizeState,
} from "../../../../lib/propNormalization";
const RadioButtonContainer = ({
checked = false,
@@ -22,10 +25,10 @@ const RadioButtonContainer = ({
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const mode = normalizeMode(modeProp);
const state = normalizeState(stateProp);
// If state is "selected", it means checked in Figma terms
const normalizedState = state === "selected" || checked ? "selected" : state;
const isInverse = mode === "inverse";
const isStandard = mode === "standard";
@@ -42,7 +45,9 @@ const RadioButtonContainer = ({
duration-200
ease-in-out
p-[4px]
`.trim().replace(/\s+/g, " ");
`
.trim()
.replace(/\s+/g, " ");
// Get box styles based on mode and checked status per Figma designs
const getBoxStyles = (): string => {
@@ -55,12 +60,12 @@ const RadioButtonContainer = ({
const defaultBorder = checked
? "border-[var(--color-border-default-brand-primary,#fdfaa8)]"
: "border-[var(--color-border-default-tertiary,#464646)]";
// When focused and checked, border should be invert tertiary (#2d2d2d) per Figma
const focusBorder = checked
? "focus:border-[var(--color-content-invert-tertiary,#2d2d2d)]"
: "focus:border-[var(--color-border-default-tertiary,#464646)]";
return `${baseBox} bg-[var(--color-surface-default-primary)] border border-solid ${defaultBorder} hover:border-[var(--color-border-default-brand-primary,#fdfaa8)] ${focusBorder} focus:shadow-[0px_0px_0px_2px_var(--color-border-invert-primary,white),0px_0px_0px_4px_var(--color-border-default-primary,#141414)] focus:outline-none`;
}
@@ -73,15 +78,16 @@ const RadioButtonContainer = ({
const defaultBorder = checked
? "border-[var(--color-border-default-brand-primary,#fdfaa8)]"
: "border-[var(--color-border-invert-primary,white)]";
// 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
const focusBorder = checked
? "focus:border-[var(--color-border-invert-primary,white)]"
: "focus:border-[var(--color-border-invert-primary,white)]";
return `${baseBox} bg-transparent border border-solid ${defaultBorder} ${hoverBorder} ${focusBorder} focus:shadow-[0px_0px_0px_2px_var(--color-border-default-primary,#141414),0px_0px_0px_4px_var(--color-border-invert-primary,white)] focus:outline-none`;
}
@@ -41,8 +41,8 @@ export function RadioButtonView({
checked && mode === "standard"
? "bg-[var(--color-content-default-brand-primary,#fefcc9)] group-hover:!bg-[#333000]"
: checked && mode === "inverse"
? "bg-[var(--color-content-default-primary,#000000)]"
: "bg-transparent"
? "bg-[var(--color-content-default-primary,#000000)]"
: "bg-transparent"
}`}
/>
</span>
@@ -3,7 +3,10 @@
import { memo, useCallback, useId } from "react";
import { RadioGroupView } from "./RadioGroup.view";
import type { RadioGroupProps } from "./RadioGroup.types";
import { normalizeMode, normalizeState } from "../../../../lib/propNormalization";
import {
normalizeMode,
normalizeState,
} from "../../../../lib/propNormalization";
const RadioGroupContainer = ({
name,
@@ -19,10 +22,11 @@ const RadioGroupContainer = ({
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const mode = normalizeMode(modeProp);
// 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")
? "default" // "With Subtext" is handled via RadioOption.subtext, use default state
: normalizeState(stateProp);
? "default" // "With Subtext" is handled via RadioOption.subtext, use default state
: normalizeState(stateProp);
// Generate unique ID for accessibility if not provided
const generatedId = useId();
const groupId = name || `radio-group-${generatedId}`;
@@ -24,10 +24,7 @@ export function RadioGroupView({
// If there's subtext, render radio button without label and handle layout separately
if (option.subtext) {
return (
<div
key={option.value}
className="flex gap-[8px] items-start"
>
<div key={option.value} className="flex gap-[8px] items-start">
<RadioButton
checked={isSelected}
mode={mode}
@@ -16,7 +16,11 @@ import React, {
import { useClickOutside } from "../../../hooks";
import { SelectInputView } from "./SelectInput.view";
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>(
(
@@ -46,23 +50,28 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
ref,
) => {
// 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
let normalizedState = externalStateProp;
if (normalizedState === "state5" || normalizedState === "State5") {
normalizedState = "default"; // Map to default, disabled prop handles the disabled state
}
const externalState = normalizeState(normalizedState);
// 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
const _labelVariant = labelVariantProp ? normalizeLabelVariant(labelVariantProp) : undefined;
const _size = sizeProp ? normalizeSmallMediumLargeSize(sizeProp) : undefined;
const _labelVariant = labelVariantProp
? normalizeLabelVariant(labelVariantProp)
: undefined;
const _size = sizeProp
? normalizeSmallMediumLargeSize(sizeProp)
: undefined;
// Mark as intentionally unused for future implementation
void _labelVariant;
void _size;
const generatedId = useId();
const selectId = id || `select-input-${generatedId}`;
const labelId = `${selectId}-label`;
@@ -73,11 +82,14 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
// Internal state management: track if focused and how (mouse vs keyboard)
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);
// 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
useEffect(() => {
@@ -7,8 +7,18 @@ export interface SelectOptionData {
import type { StateValue } from "../../../../lib/propNormalization";
export type SelectInputLabelVariantValue = "default" | "horizontal" | "Default" | "Horizontal";
export type SelectInputSizeValue = "small" | "medium" | "large" | "Small" | "Medium" | "Large";
export type SelectInputLabelVariantValue =
| "default"
| "horizontal"
| "Default"
| "Horizontal";
export type SelectInputSizeValue =
| "small"
| "medium"
| "large"
| "Small"
| "Medium"
| "Large";
export interface SelectInputProps {
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 {
children?: React.ReactNode;
@@ -17,7 +17,7 @@ const SwitchContainer = memo(
className = "",
...rest
} = props;
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const state = normalizeState(stateProp);
@@ -4,7 +4,12 @@ import { memo, forwardRef } from "react";
import { useComponentId, useFormField } from "../../../hooks";
import { TextAreaView } from "./TextArea.view";
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>(
(
@@ -1,9 +1,23 @@
import type { InputStateValue } from "../../../../lib/propNormalization";
export type TextAreaSizeValue = "small" | "medium" | "large" | "Small" | "Medium" | "Large";
export type TextAreaLabelVariantValue = "default" | "horizontal" | "Default" | "Horizontal";
export type TextAreaSizeValue =
| "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<
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
@@ -4,7 +4,10 @@ import { memo, forwardRef, useState, useRef } from "react";
import { useComponentId, useFormField } from "../../../hooks";
import { TextInputView } from "./TextInput.view";
import type { TextInputProps } from "./TextInput.types";
import { normalizeInputState, normalizeTextInputSize } from "../../../../lib/propNormalization";
import {
normalizeInputState,
normalizeTextInputSize,
} from "../../../../lib/propNormalization";
const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
(
@@ -33,18 +36,21 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const externalState = normalizeInputState(externalStateProp);
const inputSize = normalizeTextInputSize(inputSizeProp);
// Generate unique ID for accessibility if not provided
const { id: inputId, labelId } = useComponentId("text-input", id);
// Internal state management: track if focused and how (mouse vs keyboard)
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);
// 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
const shouldAutoManageFocus = externalState === "default" || externalState === undefined;
const shouldAutoManageFocus =
externalState === "default" || externalState === undefined;
// Determine actual state:
// - Active: when clicked (mouse focus)
@@ -62,19 +68,20 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
const isFilled = Boolean(value && value.trim().length > 0);
// Size styles based on inputSize prop
const sizeStyles = inputSize === "small"
? {
input: "h-[32px] px-[10px] py-[6px] text-[14px]",
label: "text-[12px] leading-[16px] font-medium",
container: "gap-[6px]",
radius: "var(--measures-radius-200,8px)",
}
: {
input: "h-[40px] px-[12px] py-[8px] text-[16px]",
label: "text-[14px] leading-[20px] font-medium",
container: "gap-[8px]",
radius: "var(--measures-radius-200,8px)",
};
const sizeStyles =
inputSize === "small"
? {
input: "h-[32px] px-[10px] py-[6px] text-[14px]",
label: "text-[12px] leading-[16px] font-medium",
container: "gap-[6px]",
radius: "var(--measures-radius-200,8px)",
}
: {
input: "h-[40px] px-[12px] py-[8px] text-[16px]",
label: "text-[14px] leading-[20px] font-medium",
container: "gap-[8px]",
radius: "var(--measures-radius-200,8px)",
};
// State styles based on Figma designs
const getStateStyles = (): {
@@ -167,17 +174,20 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
: "text-[var(--color-content-default-tertiary,#b4b4b4)]";
// Form field handlers with disabled state handling
const { handleChange, handleBlur } = useFormField<HTMLInputElement>(disabled, {
onChange,
onBlur: (e) => {
if (shouldAutoManageFocus) {
setIsFocused(false);
setFocusMethod(null);
wasMouseDownRef.current = false;
}
onBlur?.(e);
const { handleChange, handleBlur } = useFormField<HTMLInputElement>(
disabled,
{
onChange,
onBlur: (e) => {
if (shouldAutoManageFocus) {
setIsFocused(false);
setFocusMethod(null);
wasMouseDownRef.current = false;
}
onBlur?.(e);
},
},
});
);
// Handle mouse down to detect mouse clicks
const handleMouseDown = () => {
@@ -189,19 +199,19 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
// Custom focus handler to detect mouse vs keyboard
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
if (disabled) return;
// Detect if focus came from keyboard (Tab) or mouse (click)
// If mouseDown was detected before focus, it's a mouse click (active)
// Otherwise, it's keyboard navigation (focus)
const method = wasMouseDownRef.current ? "mouse" : "keyboard";
if (shouldAutoManageFocus) {
setIsFocused(true);
setFocusMethod(method);
// Reset mouse down flag after focus is processed
wasMouseDownRef.current = false;
}
onFocus?.(e);
};
@@ -3,7 +3,10 @@
import { memo, useCallback, useId, forwardRef } from "react";
import { ToggleGroupView } from "./ToggleGroup.view";
import type { ToggleGroupProps } from "./ToggleGroup.types";
import { normalizeToggleState, normalizeToggleGroupPosition } from "../../../../lib/propNormalization";
import {
normalizeToggleState,
normalizeToggleGroupPosition,
} from "../../../../lib/propNormalization";
const ToggleGroupContainer = memo(
forwardRef<HTMLButtonElement, ToggleGroupProps>((props, _ref) => {
@@ -19,7 +22,7 @@ const ToggleGroupContainer = memo(
onBlur,
...rest
} = props;
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const position = normalizeToggleGroupPosition(positionProp);
const state = normalizeToggleState(stateProp);
@@ -1,6 +1,12 @@
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<
React.ButtonHTMLAttributes<HTMLButtonElement>,
@@ -5,13 +5,7 @@ import UploadView from "./Upload.view";
import type { UploadProps } from "./Upload.types";
const UploadContainer = memo<UploadProps>(
({
active = true,
label,
showHelpIcon = true,
onClick,
className = "",
}) => {
({ active = true, label, showHelpIcon = true, onClick, className = "" }) => {
return (
<UploadView
active={active}
@@ -33,7 +33,9 @@ function UploadView({
: "text-[color:var(--color-content-invert-tertiary,#2d2d2d)]";
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 && (
<InputLabel
@@ -92,13 +94,17 @@ function UploadView({
</svg>
</div>
{/* 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>
</div>
</button>
{/* 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">
Add images, PDFs, and other files to the policy
</p>
+11 -2
View File
@@ -1,7 +1,15 @@
import { memo } from "react";
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> {
src: string;
@@ -19,7 +27,8 @@ const Avatar = memo<AvatarProps>(
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const size = normalizeSize(sizeProp, "small");
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)]",
large: "w-[var(--spacing-scale-024)] h-[var(--spacing-scale-024)]",
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 { AlertView } from "./Alert.view";
import type { AlertProps } from "./Alert.types";
import { normalizeAlertStatus, normalizeAlertType } from "../../../../lib/propNormalization";
import {
normalizeAlertStatus,
normalizeAlertType,
} from "../../../../lib/propNormalization";
const AlertContainer = memo<AlertProps>(
({
+1 -1
View File
@@ -56,7 +56,7 @@ export function CreateView({
{/* Header: custom headerContent (when provided) or default title/description */}
{headerContent !== undefined ? (
<div className="shrink-0">{headerContent}</div>
) : (title || description) ? (
) : title || description ? (
<div className="bg-[var(--color-surface-default-primary)] px-[24px] py-[12px] shrink-0">
<ContentLockup
title={title}
@@ -6,7 +6,13 @@ import type { TooltipProps } from "./Tooltip.types";
import { normalizeTooltipPosition } from "../../../../lib/propNormalization";
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)
const position = normalizeTooltipPosition(positionProp);
const [isVisible, setIsVisible] = useState(false);
+2 -2
View File
@@ -3,7 +3,7 @@
import { memo } from "react";
import { useTranslation } from "../../contexts/MessagesContext";
import Link from "next/link";
import Logo from "../icons/Logo";
import Logo from "../asset/logo";
import Separator from "../utility/Separator";
import { getAssetPath, ASSETS } from "../../../lib/assetUtils";
@@ -40,7 +40,7 @@ const Footer = memo(() => {
lg:gap-[var(--spacing-measures-spacing-060,60px)]"
>
{/* Logo */}
<Logo size="footer" />
<Logo size="footer" wordmark />
{/* 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">
+1 -1
View File
@@ -25,7 +25,7 @@ const MenuBar = memo<MenuBarProps>(
({ children, className = "", size: sizeProp = "X Small", ...props }) => {
const size = normalizeMenuBarSize(sizeProp);
const t = useTranslation("menuBar");
// Size styles based on Figma specifications
const sizeStyles: Record<
"X Small" | "Small" | "Medium" | "Large" | "X Large",
@@ -32,11 +32,17 @@ const MenuBarItemContainer = memo<MenuBarItemProps>(
"X Small" | "Small" | "Medium" | "Large" | "X Large",
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)]",
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]",
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]",
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]",
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
@@ -46,41 +52,34 @@ const MenuBarItemContainer = memo<MenuBarItemProps>(
> = {
"X Small":
"font-inter text-[10px] leading-[12px] font-medium tracking-[0%]",
Small:
"font-inter text-[12px] leading-[14px] font-medium tracking-[0%]",
Medium:
"font-inter text-[12px] leading-[14px] font-medium tracking-[0%]",
Large:
"font-inter text-[16px] leading-[20px] font-medium tracking-[0%]",
Small: "font-inter text-[12px] leading-[14px] font-medium tracking-[0%]",
Medium: "font-inter text-[12px] leading-[14px] font-medium tracking-[0%]",
Large: "font-inter text-[16px] leading-[20px] font-medium tracking-[0%]",
"X Large":
"font-inter text-[24px] leading-[28px] font-normal tracking-[0%]",
};
// State styles for Default mode (yellow text on dark background)
const defaultModeStyles: Record<
"default" | "hover" | "selected",
string
> = {
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)]",
hover:
"bg-[var(--color-gray-800)] text-[var(--color-content-default-brand-primary,#fefcc9)]",
selected:
"border border-[var(--color-border-default-brand-primary,#fdfaa8)] text-[var(--color-content-default-brand-primary,#fefcc9)] bg-transparent hover:bg-[var(--color-gray-800)]",
};
const defaultModeStyles: Record<"default" | "hover" | "selected", string> =
{
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)]",
hover:
"bg-[var(--color-gray-800)] text-[var(--color-content-default-brand-primary,#fefcc9)]",
selected:
"border border-[var(--color-border-default-brand-primary,#fdfaa8)] text-[var(--color-content-default-brand-primary,#fefcc9)] bg-transparent hover:bg-[var(--color-gray-800)]",
};
// State styles for Inverse mode (black text on yellow background)
const inverseModeStyles: Record<
"default" | "hover" | "selected",
string
> = {
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)]",
hover:
"bg-[var(--color-surface-brand-accent,#4d4a00)] text-[var(--color-content-inverse-primary,black)]",
selected:
"border border-[var(--color-border-default-primary,#141414)] text-[var(--color-content-inverse-primary,black)] bg-transparent hover:bg-[var(--color-surface-brand-accent,#4d4a00)]",
};
const inverseModeStyles: Record<"default" | "hover" | "selected", string> =
{
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)]",
hover:
"bg-[var(--color-surface-brand-accent,#4d4a00)] text-[var(--color-content-inverse-primary,black)]",
selected:
"border border-[var(--color-border-default-primary,#141414)] text-[var(--color-content-inverse-primary,black)] bg-transparent hover:bg-[var(--color-surface-brand-accent,#4d4a00)]",
};
// Get state styles based on mode
const stateStyles =
@@ -5,14 +5,9 @@ export type MenuBarItemSizeValue =
| "Large"
| "X Large";
export type MenuBarItemStateValue =
| "default"
| "hover"
| "selected";
export type MenuBarItemStateValue = "default" | "hover" | "selected";
export type MenuBarItemModeValue =
| "default"
| "inverse";
export type MenuBarItemModeValue = "default" | "inverse";
export interface MenuBarItemProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
href?: string;
@@ -3,7 +3,10 @@
import { memo } from "react";
import NavigationItemView from "./NavigationItem.view";
import type { NavigationItemProps } from "./NavigationItem.types";
import { normalizeNavigationItemVariant, normalizeNavigationItemSize } from "../../../../lib/propNormalization";
import {
normalizeNavigationItemVariant,
normalizeNavigationItemSize,
} from "../../../../lib/propNormalization";
const NavigationItemContainer = memo<NavigationItemProps>(
({
@@ -1,5 +1,9 @@
export type NavigationItemVariantValue = "default" | "Default";
export type NavigationItemSizeValue = "default" | "xsmall" | "Default" | "XSmall";
export type NavigationItemSizeValue =
| "default"
| "xsmall"
| "Default"
| "XSmall";
export interface NavigationItemProps extends Omit<
React.AnchorHTMLAttributes<HTMLAnchorElement>,
@@ -7,7 +7,7 @@ import { getAssetPath } from "../../../../lib/assetUtils";
import MenuBar from "../MenuBar";
import type { TopNavViewProps } from "./TopNav.types";
import Logo from "../../icons/Logo";
import Logo from "../../asset/logo";
function TopNavView({
folderTop,
@@ -44,7 +44,11 @@ function TopNavView({
{/* 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">
{/* 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 */}
<div className="block sm:hidden -me-[2px]">
@@ -90,7 +94,9 @@ function TopNavView({
{/* 640-1023px (md: breakpoint): MenuBar Small */}
<div className="hidden md:block lg:hidden">
<MenuBar size="Small">{renderNavigationItems("homeMd")}</MenuBar>
<MenuBar size="Small">
{renderNavigationItems("homeMd")}
</MenuBar>
</div>
{/* 1024-1440px (lg: breakpoint): MenuBar Large */}
@@ -161,7 +167,11 @@ function TopNavView({
aria-label={t("ariaLabels.mainNavigation")}
>
{/* 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 */}
<div className="flex items-center flex-1 justify-end sm:flex-none sm:justify-center">
@@ -193,7 +203,9 @@ function TopNavView({
</div>
<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>
@@ -44,7 +44,9 @@ const AskOrganizerContainer = memo<AskOrganizerProps>(
onContactClick,
}) => {
// 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 defaultButtonText = buttonText ?? t("askOrganizer.buttonText");
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 { normalizeSectionHeaderVariant } from "../../../lib/propNormalization";
export type SectionHeaderVariantValue = "default" | "multi-line" | "Default" | "Multi-Line";
export type SectionHeaderVariantValue =
| "default"
| "multi-line"
| "Default"
| "Multi-Line";
interface SectionHeaderProps {
title: string;
@@ -3,7 +3,10 @@
import { memo } from "react";
import ContentLockupView from "./ContentLockup.view";
import type { ContentLockupProps, VariantStyle } from "./ContentLockup.types";
import { normalizeContentLockupVariant, normalizeAlignment } from "../../../../lib/propNormalization";
import {
normalizeContentLockupVariant,
normalizeAlignment,
} from "../../../../lib/propNormalization";
const ContentLockupContainer = memo<ContentLockupProps>(
({
@@ -96,19 +96,32 @@ function ContentLockupView({
<div className="flex justify-start">
{/* Small button for xsm and sm breakpoints */}
<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}
</Button>
</div>
{/* Large button for md and lg breakpoints */}
<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}
</Button>
</div>
{/* XLarge button for xl breakpoint */}
<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}
</Button>
</div>
@@ -6,6 +6,7 @@ import type { HeaderLockupProps } from "./HeaderLockup.types";
import {
normalizeHeaderLockupJustification,
normalizeHeaderLockupSize,
normalizeHeaderLockupPalette,
} from "../../../../lib/propNormalization";
const HeaderLockupContainer = memo<HeaderLockupProps>(
@@ -14,10 +15,12 @@ const HeaderLockupContainer = memo<HeaderLockupProps>(
description,
justification: justificationProp = "left",
size: sizeProp = "L",
palette: paletteProp = "default",
}) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const justification = normalizeHeaderLockupJustification(justificationProp);
const size = normalizeHeaderLockupSize(sizeProp);
const palette = normalizeHeaderLockupPalette(paletteProp);
return (
<HeaderLockupView
@@ -25,6 +28,7 @@ const HeaderLockupContainer = memo<HeaderLockupProps>(
description={description}
justification={justification}
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 HeaderLockupPaletteValue =
| "default"
| "inverse"
| "Default"
| "Inverse";
export interface HeaderLockupProps {
/**
@@ -20,6 +29,11 @@ export interface HeaderLockupProps {
* Figma uses PascalCase, codebase uses lowercase - both are supported.
*/
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 {
@@ -27,4 +41,5 @@ export interface HeaderLockupViewProps {
description?: string;
justification: "left" | "center";
size: "L" | "M";
palette: "default" | "inverse";
}
@@ -8,9 +8,18 @@ function HeaderLockupView({
description,
justification,
size,
palette,
}: HeaderLockupViewProps) {
const isL = size === "L";
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 (
<div
@@ -21,7 +30,7 @@ function HeaderLockupView({
{/* Title */}
<div className="flex items-center relative shrink-0 w-full">
<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"
} ${
isL
@@ -36,12 +45,10 @@ function HeaderLockupView({
{/* Description */}
{description && (
<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"
} ${
isL
? "text-[18px] leading-[1.3]"
: "text-[14px] leading-[20px]"
isL ? "text-[18px] leading-[1.3]" : "text-[14px] leading-[20px]"
}`}
>
{description}
+9 -1
View File
@@ -1,7 +1,15 @@
import { memo } from "react";
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> {
children?: React.ReactNode;
@@ -24,7 +24,9 @@ const CardStackContainer = memo<CardStackProps>(
className = "",
}) => {
const [internalExpanded, setInternalExpanded] = useState(false);
const [internalSelectedIds, setInternalSelectedIds] = useState<string[]>([]);
const [internalSelectedIds, setInternalSelectedIds] = useState<string[]>(
[],
);
const expanded =
controlledExpanded !== undefined ? controlledExpanded : internalExpanded;
@@ -41,7 +43,9 @@ const CardStackContainer = memo<CardStackProps>(
controlledSelectedIds !== undefined
? controlledSelectedIds
: controlledSelectedId !== undefined
? (controlledSelectedId ? [controlledSelectedId] : [])
? controlledSelectedId
? [controlledSelectedId]
: []
: internalSelectedIds;
const handleCardSelect = useCallback(
@@ -36,9 +36,7 @@ export function CreateFlowFooterView({
</Button>
{/* Second Button - Right */}
{secondButton && (
<div className="flex-shrink-0">{secondButton}</div>
)}
{secondButton && <div className="flex-shrink-0">{secondButton}</div>}
</div>
</footer>
);
@@ -15,6 +15,7 @@ const CreateFlowTopNavContainer = memo<CreateFlowTopNavProps>(
onExport,
onEdit,
onExit,
buttonPalette,
className = "",
}) => {
const router = useRouter();
@@ -38,6 +39,7 @@ const CreateFlowTopNavContainer = memo<CreateFlowTopNavProps>(
onExport={onExport}
onEdit={onEdit}
onExit={handleExit}
buttonPalette={buttonPalette}
className={className}
/>
);
@@ -1,6 +1,6 @@
/**
* Type definitions for CreateFlowTopNav component
*
*
* Top navigation bar for the create rule flow.
* Includes logo and action buttons (Share, Export, Edit, Exit).
*/
@@ -42,6 +42,11 @@ export interface CreateFlowTopNavProps {
* Callback when Exit/Save & Exit button is clicked
*/
onExit?: () => void;
/**
* Palette for nav buttons (e.g. "inverse" on completed page to match teal background)
* @default "default"
*/
buttonPalette?: "default" | "inverse";
/**
* Additional CSS classes
*/
@@ -1,4 +1,4 @@
import Logo from "../../icons/Logo";
import Logo from "../../asset/logo";
import Button from "../../buttons/Button";
import type { CreateFlowTopNavProps } from "./CreateFlowTopNav.types";
@@ -11,6 +11,7 @@ export function CreateFlowTopNavView({
onExport,
onEdit,
onExit,
buttonPalette = "default",
className = "",
}: CreateFlowTopNavProps) {
const exitButtonText = loggedIn ? "Save & Exit" : "Exit";
@@ -27,14 +28,14 @@ export function CreateFlowTopNavView({
aria-label="Create Flow Navigation"
>
{/* Logo - Left */}
<Logo size="createFlow" showText={true} />
<Logo size="createFlow" wordmark palette={buttonPalette} />
{/* Button Group - Right */}
<div className="flex items-center gap-[var(--spacing-scale-012,12px)]">
{hasShare && (
<Button
buttonType="outline"
palette="default"
palette={buttonPalette}
size="xsmall"
onClick={onShare}
ariaLabel="Share"
@@ -47,7 +48,7 @@ export function CreateFlowTopNavView({
{hasExport && (
<Button
buttonType="outline"
palette="default"
palette={buttonPalette}
size="xsmall"
onClick={onExport}
ariaLabel="Export"
@@ -74,7 +75,7 @@ export function CreateFlowTopNavView({
{hasEdit && (
<Button
buttonType="outline"
palette="default"
palette={buttonPalette}
size="xsmall"
onClick={onEdit}
ariaLabel="Edit"
@@ -86,7 +87,7 @@ export function CreateFlowTopNavView({
<Button
buttonType="outline"
palette="default"
palette={buttonPalette}
size="xsmall"
onClick={onExit}
ariaLabel={exitButtonText}
@@ -24,7 +24,9 @@ export interface DecisionMakingSidebarViewProps {
messageBoxTitle: string;
messageBoxItems: InfoMessageBoxItem[];
messageBoxCheckedIds: string[] | undefined;
onMessageBoxCheckboxChange: ((id: string, checked: boolean) => void) | undefined;
onMessageBoxCheckboxChange:
| ((id: string, checked: boolean) => void)
| undefined;
size: "L" | "M";
justification: "left" | "center";
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 ${
isLeft ? "" : "text-center"
} ${
isL
? "text-[18px] leading-[1.3]"
: "text-[14px] leading-[20px]"
isL ? "text-[18px] leading-[1.3]" : "text-[14px] leading-[20px]"
}`}
>
{description}
@@ -1,2 +1,5 @@
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 InputLabelPaletteValue = "Default" | "Inverse" | "default" | "inverse";
export type InputLabelPaletteValue =
| "Default"
| "Inverse"
| "default"
| "inverse";
export interface InputLabelProps {
/**
@@ -34,7 +34,8 @@ function InputLabelView({
? "text-[color:var(--color-content-inverse-secondary,#1f1f1f)]"
: "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
const containerClass = isSmall
@@ -24,12 +24,13 @@ export function ModalFooterView({
// Use localized defaults if text not provided
const defaultBackText = backButtonText || t("buttons.back");
const defaultNextText = nextButtonText || t("buttons.next");
// Determine if stepper should be shown
// Defaults to true if currentStep and totalSteps are provided, unless explicitly set to false
const shouldShowStepper = stepperProp !== undefined
? stepperProp
: (currentStep !== undefined && totalSteps !== undefined);
const shouldShowStepper =
stepperProp !== undefined
? stepperProp
: currentStep !== undefined && totalSteps !== undefined;
return (
<div
@@ -38,7 +39,12 @@ export function ModalFooterView({
{/* Back Button - Absolutely positioned bottom left */}
{showBackButton && (
<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}
</Button>
</div>
+8 -10
View File
@@ -9,16 +9,14 @@ const DEFAULT_LABELS: Record<TagProps["variant"], string> = {
selected: "SELECTED",
};
const TagContainer = memo<TagProps>(
({ variant, children, className = "" }) => {
const content = children ?? DEFAULT_LABELS[variant];
return (
<TagView variant={variant} className={className}>
{content}
</TagView>
);
},
);
const TagContainer = memo<TagProps>(({ variant, children, className = "" }) => {
const content = children ?? DEFAULT_LABELS[variant];
return (
<TagView variant={variant} className={className}>
{content}
</TagView>
);
});
TagContainer.displayName = "Tag";
+1 -1
View File
@@ -25,7 +25,7 @@ const VALID_STEPS: CreateFlowStep[] = [
/**
* Dynamic route handler for create flow steps
*
*
* Handles all flow steps via dynamic routing: /create/[step]
* Validates step exists and renders appropriate template (placeholder for now)
*/
+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>
)}
</>
);
}
+3 -5
View File
@@ -16,7 +16,7 @@ interface CreateFlowProviderProps {
/**
* Provider component for Create Flow state management
*
*
* This is a basic implementation that will be expanded in CR-56
* with full navigation logic, state persistence, and validation.
*/
@@ -25,9 +25,7 @@ export function CreateFlowProvider({
initialStep = null,
}: CreateFlowProviderProps) {
const [state, setState] = useState<CreateFlowState>({});
const [currentStep] = useState<CreateFlowStep | null>(
initialStep,
);
const [currentStep] = useState<CreateFlowStep | null>(initialStep);
const updateState = (updates: Partial<CreateFlowState>) => {
setState((prevState) => ({
@@ -51,7 +49,7 @@ export function CreateFlowProvider({
/**
* Hook to access Create Flow context
*
*
* @throws Error if used outside CreateFlowProvider
* @returns CreateFlowContextValue
*/
+2 -4
View File
@@ -32,9 +32,7 @@ const FINAL_REVIEW_CATEGORIES: Category[] = [
},
{
name: "Membership",
chipOptions: [
{ id: "m1", label: "Open Admission", state: "unselected" },
],
chipOptions: [{ id: "m1", label: "Open Admission", state: "unselected" }],
},
{
name: "Decision-making",
@@ -70,7 +68,7 @@ export default function FinalReviewPage() {
if (showDesktopLayout) {
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="min-w-0 flex flex-col justify-center">
<HeaderLockup
+1 -1
View File
@@ -6,7 +6,7 @@ import NumberedList from "../../components/type/NumberedList";
/**
* Informational page for the create flow
*
*
* Displays information about the create flow process using HeaderLockup and NumberedList components.
* Responsive sizing: uses L/M for HeaderLockup and M/S for NumberedList based on 640px breakpoint.
*/
+52 -24
View File
@@ -10,7 +10,7 @@ import type { CreateFlowStep } from "./types";
/**
* Layout for the Create Rule Flow
*
*
* Provides a full-screen layout without the root layout's TopNav/Footer.
* This layout wraps all create flow pages and provides the CreateFlowContext.
* Includes the create flow-specific TopNav and Footer components.
@@ -59,7 +59,10 @@ function CreateFlowLayoutContent({ children }: { children: ReactNode }) {
const previousStep = getPreviousStep();
const handleNext = () => {
if (typeof document !== "undefined" && document.activeElement instanceof HTMLElement) {
if (
typeof document !== "undefined" &&
document.activeElement instanceof HTMLElement
) {
document.activeElement.blur();
}
if (nextStep) {
@@ -68,7 +71,10 @@ function CreateFlowLayoutContent({ children }: { children: ReactNode }) {
};
const handleBack = () => {
if (typeof document !== "undefined" && document.activeElement instanceof HTMLElement) {
if (
typeof document !== "undefined" &&
document.activeElement instanceof HTMLElement
) {
document.activeElement.blur();
}
if (previousStep) {
@@ -76,30 +82,52 @@ function CreateFlowLayoutContent({ children }: { children: ReactNode }) {
}
};
const isCompletedStep = currentStep === "completed";
return (
<div className="min-h-screen bg-black flex flex-col">
<CreateFlowTopNav />
<main className="flex-1 flex items-center justify-center overflow-auto">
<div
className={`bg-black flex flex-col ${isCompletedStep ? "h-screen overflow-hidden" : "min-h-screen"}`}
>
<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}
</main>
<CreateFlowFooter
secondButton={
nextStep ? (
<Button
buttonType="filled"
palette="default"
size="xsmall"
className="md:!text-[14px] md:!leading-[16px] !text-[12px] !leading-[14px] !px-[var(--spacing-measures-spacing-200,8px)] md:!px-[var(--spacing-measures-spacing-250,10px)] !py-[var(--spacing-measures-spacing-200,8px)] md:!py-[var(--spacing-measures-spacing-250,10px)]"
onClick={handleNext}
>
{currentStep === "final-review"
? "Finalize CommunityRule"
: "Next"}
</Button>
) : null
}
onBackClick={previousStep ? handleBack : undefined}
/>
{!isCompletedStep && (
<CreateFlowFooter
secondButton={
nextStep ? (
<Button
buttonType="filled"
palette="default"
size="xsmall"
className="md:!text-[14px] md:!leading-[16px] !text-[12px] !leading-[14px] !px-[var(--spacing-measures-spacing-200,8px)] md:!px-[var(--spacing-measures-spacing-250,10px)] !py-[var(--spacing-measures-spacing-200,8px)] md:!py-[var(--spacing-measures-spacing-250,10px)]"
onClick={handleNext}
>
{currentStep === "final-review"
? "Finalize CommunityRule"
: "Next"}
</Button>
) : null
}
onBackClick={previousStep ? handleBack : undefined}
/>
)}
</div>
);
}
+19 -10
View File
@@ -7,7 +7,7 @@ import MultiSelect from "../../components/controls/MultiSelect";
/**
* Select page for the create flow
*
*
* Displays selection options using HeaderLockup and MultiSelect components.
* Responsive layout: two-column at 640px+, single column below 640px.
* Responsive sizing: uses L/M for HeaderLockup and S for MultiSelect based on 640px breakpoint.
@@ -44,9 +44,12 @@ export default function SelectPage() {
setCommunitySizeOptions((prev) =>
prev.map((opt) =>
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) =>
prev.map((opt) =>
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) =>
prev.map((opt) =>
opt.id === chipId
? { ...opt, state: opt.state === "Selected" ? "Unselected" : "Selected" }
: opt
)
? {
...opt,
state: opt.state === "Selected" ? "Unselected" : "Selected",
}
: opt,
),
);
};
+1 -1
View File
@@ -7,7 +7,7 @@ import TextInput from "../../components/controls/TextInput";
/**
* Text page for the create flow
*
*
* Displays a text input field for user input using HeaderLockup and TextInput components.
* Responsive sizing: uses L/M for HeaderLockup and medium/small for TextInput based on 640px breakpoint.
*/
+7 -7
View File
@@ -18,31 +18,31 @@
/* Design system scrollbar (Figma node 20612-36521): dark track + thumb with states */
.scrollbar-design {
scrollbar-width: thin; /* Firefox: narrow scrollbar */
scrollbar-color: #545B64 #292D32; /* Firefox: thumb track */
scrollbar-color: #545b64 #292d32; /* Firefox: thumb track */
}
.scrollbar-design::-webkit-scrollbar {
width: 16px;
height: 16px;
}
.scrollbar-design::-webkit-scrollbar-track {
background: #292D32;
background: #292d32;
}
.scrollbar-design::-webkit-scrollbar-thumb {
background: #545B64;
background: #545b64;
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;
}
.scrollbar-design::-webkit-scrollbar-thumb:hover {
background: #787F8A;
background: #787f8a;
border-width: 2px; /* hover: thumb expands to 12px */
}
.scrollbar-design::-webkit-scrollbar-thumb:active {
background: #3F434C;
background: #3f434c;
border-width: 2px;
}
.scrollbar-design::-webkit-scrollbar-corner {
background: #292D32;
background: #292d32;
}
@theme inline {
+1 -1
View File
@@ -30,7 +30,7 @@ export function getAssetPath(assetPath: string): string {
*/
export const ASSETS = {
// Logo
LOGO: "assets/Logo.svg",
LOGO: "assets/logo/Logo.svg",
// Avatars
AVATAR_1: "assets/Avatar_1.png",
+76 -46
View File
@@ -1,7 +1,7 @@
/**
* Utility functions for normalizing component props to match Figma specifications
* while maintaining backward compatibility with existing lowercase usage.
*
*
* Figma uses PascalCase (e.g., "Standard", "Inverse") but codebase uses lowercase.
* These helpers accept both formats and normalize to lowercase for internal use.
*/
@@ -11,7 +11,7 @@
*/
export function normalizeMode(
value: string | undefined,
defaultValue: "standard" | "inverse" = "standard"
defaultValue: "standard" | "inverse" = "standard",
): "standard" | "inverse" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
@@ -26,7 +26,7 @@ export function normalizeMode(
*/
export function normalizeState(
value: string | undefined,
defaultValue: "default" | "hover" | "focus" | "selected" = "default"
defaultValue: "default" | "hover" | "focus" | "selected" = "default",
): "default" | "hover" | "focus" | "selected" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
@@ -46,7 +46,7 @@ export function normalizeState(
*/
export function normalizeInputState(
value: string | undefined,
defaultValue: "default" | "active" | "hover" | "focus" = "default"
defaultValue: "default" | "active" | "hover" | "focus" = "default",
): "default" | "active" | "hover" | "focus" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
@@ -66,7 +66,7 @@ export function normalizeInputState(
*/
export function normalizeToggleState(
value: string | undefined,
defaultValue: "default" | "hover" | "focus" | "selected" = "default"
defaultValue: "default" | "hover" | "focus" | "selected" = "default",
): "default" | "hover" | "focus" | "selected" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
@@ -112,13 +112,12 @@ export type InputStateValue =
| "Hover"
| "Focus";
/**
* Normalize button size prop values
*/
export function normalizeSize(
value: string | undefined,
defaultValue: "xsmall" | "small" | "medium" | "large" | "xlarge" = "xsmall"
defaultValue: "xsmall" | "small" | "medium" | "large" | "xlarge" = "xsmall",
): "xsmall" | "small" | "medium" | "large" | "xlarge" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
@@ -134,7 +133,7 @@ export function normalizeSize(
*/
export function normalizeAlertStatus(
value: string | undefined,
defaultValue: "default" = "default"
defaultValue: "default" = "default",
): "default" | "positive" | "warning" | "danger" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
@@ -150,7 +149,7 @@ export function normalizeAlertStatus(
*/
export function normalizeAlertType(
value: string | undefined,
defaultValue: "toast" = "toast"
defaultValue: "toast" = "toast",
): "toast" | "banner" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
@@ -166,7 +165,7 @@ export function normalizeAlertType(
*/
export function normalizeTooltipPosition(
value: string | undefined,
defaultValue: "top" = "top"
defaultValue: "top" = "top",
): "top" | "bottom" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
@@ -199,7 +198,12 @@ export type SizeValue =
*/
export function normalizeMenuBarSize(
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" {
if (!value) return defaultValue;
if (
@@ -219,7 +223,7 @@ export function normalizeMenuBarSize(
*/
export function normalizeNavigationItemVariant(
value: string | undefined,
defaultValue: "default" = "default"
defaultValue: "default" = "default",
): "default" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
@@ -234,7 +238,7 @@ export function normalizeNavigationItemVariant(
*/
export function normalizeNavigationItemSize(
value: string | undefined,
defaultValue: "default" = "default"
defaultValue: "default" = "default",
): "default" | "xsmall" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
@@ -250,7 +254,7 @@ export function normalizeNavigationItemSize(
*/
export function normalizeContentLockupVariant(
value: string | undefined,
defaultValue: "hero" = "hero"
defaultValue: "hero" = "hero",
): "hero" | "feature" | "learn" | "ask" | "ask-inverse" | "modal" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
@@ -266,7 +270,7 @@ export function normalizeContentLockupVariant(
*/
export function normalizeAlignment(
value: string | undefined,
defaultValue: "center" = "center"
defaultValue: "center" = "center",
): "center" | "left" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
@@ -282,7 +286,7 @@ export function normalizeAlignment(
*/
export function normalizeNumberedListSize(
value: string | undefined,
defaultValue: "M" = "M"
defaultValue: "M" = "M",
): "M" | "S" {
if (!value) return defaultValue;
const normalized = value.toUpperCase();
@@ -297,7 +301,7 @@ export function normalizeNumberedListSize(
*/
export function normalizeHeaderLockupJustification(
value: string | undefined,
defaultValue: "left" = "left"
defaultValue: "left" = "left",
): "left" | "center" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
@@ -312,7 +316,7 @@ export function normalizeHeaderLockupJustification(
*/
export function normalizeHeaderLockupSize(
value: string | undefined,
defaultValue: "L" = "L"
defaultValue: "L" = "L",
): "L" | "M" {
if (!value) return defaultValue;
const normalized = value.toUpperCase();
@@ -322,12 +326,27 @@ export function normalizeHeaderLockupSize(
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
*/
export function normalizeTextInputSize(
value: string | undefined,
defaultValue: "medium" = "medium"
defaultValue: "medium" = "medium",
): "small" | "medium" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
@@ -342,7 +361,7 @@ export function normalizeTextInputSize(
*/
export function normalizeContentContainerSize(
value: string | undefined,
defaultValue: "responsive" = "responsive"
defaultValue: "responsive" = "responsive",
): "xs" | "responsive" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
@@ -358,7 +377,7 @@ export function normalizeContentContainerSize(
*/
export function normalizeContentThumbnailVariant(
value: string | undefined,
defaultValue: "vertical" = "vertical"
defaultValue: "vertical" = "vertical",
): "vertical" | "horizontal" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
@@ -374,7 +393,7 @@ export function normalizeContentThumbnailVariant(
*/
export function normalizeSectionHeaderVariant(
value: string | undefined,
defaultValue: "default" = "default"
defaultValue: "default" = "default",
): "default" | "multi-line" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
@@ -390,7 +409,7 @@ export function normalizeSectionHeaderVariant(
*/
export function normalizeQuoteBlockVariant(
value: string | undefined,
defaultValue: "standard" = "standard"
defaultValue: "standard" = "standard",
): "compact" | "standard" | "extended" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
@@ -406,11 +425,16 @@ export function normalizeQuoteBlockVariant(
*/
export function normalizeNumberCardSize(
value: string | undefined,
defaultValue: "Medium" = "Medium"
defaultValue: "Medium" = "Medium",
): "Small" | "Medium" | "Large" | "XLarge" {
if (!value) return defaultValue;
// Check if already PascalCase
if (value === "Small" || value === "Medium" || value === "Large" || value === "XLarge") {
if (
value === "Small" ||
value === "Medium" ||
value === "Large" ||
value === "XLarge"
) {
return value;
}
// Normalize lowercase to PascalCase
@@ -427,7 +451,7 @@ export function normalizeNumberCardSize(
*/
export function normalizeAskOrganizerVariant(
value: string | undefined,
defaultValue: "centered" = "centered"
defaultValue: "centered" = "centered",
): "centered" | "left-aligned" | "compact" | "inverse" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
@@ -443,7 +467,7 @@ export function normalizeAskOrganizerVariant(
*/
export function normalizeContextMenuItemSize(
value: string | undefined,
defaultValue: "medium" = "medium"
defaultValue: "medium" = "medium",
): "small" | "medium" | "large" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
@@ -459,7 +483,7 @@ export function normalizeContextMenuItemSize(
*/
export function normalizeImagePlaceholderColor(
value: string | undefined,
defaultValue: "blue" = "blue"
defaultValue: "blue" = "blue",
): "blue" | "green" | "purple" | "red" | "orange" | "teal" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
@@ -475,7 +499,7 @@ export function normalizeImagePlaceholderColor(
*/
export function normalizeToggleGroupPosition(
value: string | undefined,
defaultValue: "left" = "left"
defaultValue: "left" = "left",
): "left" | "middle" | "right" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
@@ -491,7 +515,7 @@ export function normalizeToggleGroupPosition(
*/
export function normalizeLabelVariant(
value: string | undefined,
defaultValue: "default" = "default"
defaultValue: "default" = "default",
): "default" | "horizontal" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
@@ -507,7 +531,7 @@ export function normalizeLabelVariant(
*/
export function normalizeTextAreaAppearance(
value: string | undefined,
defaultValue: "default" = "default"
defaultValue: "default" = "default",
): "default" | "embedded" {
if (!value) return defaultValue;
const n = value.toLowerCase();
@@ -519,7 +543,7 @@ export function normalizeTextAreaAppearance(
*/
export function normalizeSmallMediumLargeSize(
value: string | undefined,
defaultValue: "medium" = "medium"
defaultValue: "medium" = "medium",
): "small" | "medium" | "large" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
@@ -535,11 +559,16 @@ export function normalizeSmallMediumLargeSize(
*/
export function normalizeRuleCardSize(
value: string | undefined,
defaultValue: "L" = "L"
defaultValue: "L" = "L",
): "XS" | "S" | "M" | "L" {
if (!value) return defaultValue;
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 defaultValue;
@@ -561,11 +590,7 @@ export type ChipStateValue =
/**
* Type helper for case-insensitive Chip palette prop
*/
export type ChipPaletteValue =
| "default"
| "inverse"
| "Default"
| "Inverse";
export type ChipPaletteValue = "default" | "inverse" | "Default" | "Inverse";
/**
* Type helper for case-insensitive Chip size prop
@@ -673,7 +698,7 @@ export function normalizeInputLabelPalette(
*/
export function normalizeMenuBarItemState(
value: string | undefined,
defaultValue: "default" | "hover" | "selected" = "default"
defaultValue: "default" | "hover" | "selected" = "default",
): "default" | "hover" | "selected" {
if (!value) return defaultValue;
if (value === "default" || value === "hover" || value === "selected") {
@@ -689,7 +714,7 @@ export function normalizeMenuBarItemState(
*/
export function normalizeMenuBarItemMode(
value: string | undefined,
defaultValue: "default" | "inverse" = "default"
defaultValue: "default" | "inverse" = "default",
): "default" | "inverse" {
if (!value) return defaultValue;
if (value === "default" || value === "inverse") {
@@ -704,7 +729,12 @@ export function normalizeMenuBarItemMode(
*/
export function normalizeMenuBarItemSize(
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" {
if (!value) return defaultValue;
if (
@@ -724,7 +754,7 @@ export function normalizeMenuBarItemSize(
*/
export function normalizeButtonType(
value: string | undefined,
defaultValue: "filled" = "filled"
defaultValue: "filled" = "filled",
): "filled" | "outline" | "ghost" | "danger" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
@@ -740,7 +770,7 @@ export function normalizeButtonType(
*/
export function normalizeButtonPalette(
value: string | undefined,
defaultValue: "default" = "default"
defaultValue: "default" = "default",
): "default" | "inverse" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
@@ -759,7 +789,7 @@ export function normalizeButtonPalette(
*/
export function normalizeButtonState(
value: string | undefined,
defaultValue: "default" = "default"
defaultValue: "default" = "default",
): "default" | "focus" | "active" | "hover" | "disabled" {
if (!value) return defaultValue;
const normalized = value.toLowerCase();
@@ -806,4 +836,4 @@ export type ButtonStateValue =
| "Focus"
| "Active"
| "Hover"
| "Disabled";
| "Disabled";

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>
<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">
<Button buttonType="filled" palette="inverse" size="xsmall">
XSmall
@@ -221,7 +223,9 @@ export const AllVariants = {
</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">
<Button buttonType="outline" palette="inverse" size="xsmall">
XSmall
@@ -305,7 +309,9 @@ export const AllVariants = {
</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">
<Button buttonType="danger" palette="inverse" size="xsmall">
XSmall
+6 -2
View File
@@ -273,7 +273,9 @@ export const StateComparison = {
<Button disabled>Disabled</Button>
</div>
<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>
Home Disabled
</Button>
@@ -341,7 +343,9 @@ export const EdgeCases = {
<div className="flex flex-wrap gap-4 items-center">
<Button>Normal</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>
Home Disabled
</Button>
+3 -6
View File
@@ -46,8 +46,7 @@ export default {
export const Default = {
args: {
label: "Label",
supportText:
"Members vote to resolve a dispute democratically.",
supportText: "Members vote to resolve a dispute democratically.",
recommended: true,
selected: false,
orientation: "horizontal",
@@ -69,8 +68,7 @@ export const HorizontalRecommended = {
export const HorizontalSelected = {
args: {
label: "Label",
supportText:
"Members vote to resolve a dispute democratically.",
supportText: "Members vote to resolve a dispute democratically.",
recommended: false,
selected: true,
orientation: "horizontal",
@@ -157,8 +155,7 @@ export const AllVariants = {
parameters: {
docs: {
description: {
story:
"All four variants: horizontal/vertical × recommended/selected.",
story: "All four variants: horizontal/vertical × recommended/selected.",
},
},
},
+27 -15
View File
@@ -75,7 +75,8 @@ export const Expanded = {
backgroundColor: "bg-[#b7d9d5]",
expanded: true,
size: "L",
logoUrl: "http://localhost:3845/assets/d2513a6ab56f2b2927e8a7c442c06326e7a29541.png",
logoUrl:
"http://localhost:3845/assets/d2513a6ab56f2b2927e8a7c442c06326e7a29541.png",
logoAlt: "Mutual Aid Mondays",
categories: [
{
@@ -96,9 +97,7 @@ export const Expanded = {
},
{
name: "Communication",
chipOptions: [
{ id: "comm-1", label: "Signal", state: "Unselected" },
],
chipOptions: [{ id: "comm-1", label: "Signal", state: "Unselected" }],
onChipClick: (categoryName, chipId) => {
console.log(`Chip clicked: ${categoryName} - ${chipId}`);
},
@@ -122,7 +121,11 @@ export const Expanded = {
name: "Decision-making",
chipOptions: [
{ 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) => {
console.log(`Chip clicked: ${categoryName} - ${chipId}`);
@@ -135,7 +138,11 @@ export const Expanded = {
name: "Conflict management",
chipOptions: [
{ 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) => {
console.log(`Chip clicked: ${categoryName} - ${chipId}`);
@@ -232,7 +239,8 @@ export const ExpandedMedium = {
backgroundColor: "bg-[#b7d9d5]",
expanded: true,
size: "M",
logoUrl: "http://localhost:3845/assets/d2513a6ab56f2b2927e8a7c442c06326e7a29541.png",
logoUrl:
"http://localhost:3845/assets/d2513a6ab56f2b2927e8a7c442c06326e7a29541.png",
logoAlt: "Mutual Aid Mondays",
categories: [
{
@@ -247,9 +255,7 @@ export const ExpandedMedium = {
},
{
name: "Communication",
chipOptions: [
{ id: "comm-1", label: "Signal", state: "Unselected" },
],
chipOptions: [{ id: "comm-1", label: "Signal", state: "Unselected" }],
},
{
name: "Membership",
@@ -261,14 +267,22 @@ export const ExpandedMedium = {
name: "Decision-making",
chipOptions: [
{ 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",
chipOptions: [
{ 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",
chipOptions: [
{ id: "comm-1", label: "Signal", state: "Unselected" },
],
chipOptions: [{ id: "comm-1", label: "Signal", state: "Unselected" }],
onChipClick: (categoryName, chipId) => {
console.log(`Chip clicked: ${categoryName} - ${chipId}`);
},
+12 -5
View File
@@ -47,12 +47,14 @@ export default {
mode: {
control: "select",
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: {
control: "select",
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: {
control: "boolean",
@@ -215,9 +217,12 @@ export const FigmaPascalCase = () => {
return (
<div className="space-y-6">
<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">
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>
<div className="space-y-4">
<Checkbox
@@ -237,7 +242,9 @@ export const FigmaPascalCase = () => {
</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">
<Checkbox
label="Standard mode (lowercase) - still works"
+34 -10
View File
@@ -22,12 +22,23 @@ export default {
mode: {
control: "select",
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: {
control: "select",
options: ["default", "hover", "focus", "selected", "Default", "Hover", "Focus", "Selected"],
description: "Interaction state for static display (case-insensitive: accepts both lowercase and PascalCase)",
options: [
"default",
"hover",
"focus",
"selected",
"Default",
"Hover",
"Focus",
"Selected",
],
description:
"Interaction state for static display (case-insensitive: accepts both lowercase and PascalCase)",
},
disabled: {
control: "boolean",
@@ -188,7 +199,9 @@ export const StandardAllStates = () => {
return (
<div className="space-y-6">
<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">
<RadioButton
label="Unselected (default, hover, focus)"
@@ -200,7 +213,9 @@ export const StandardAllStates = () => {
</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">
<RadioButton
label="Selected (default, hover, focus)"
@@ -222,7 +237,9 @@ export const InverseAllStates = () => {
return (
<div className="space-y-6">
<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">
<RadioButton
label="Unselected (default, hover, focus)"
@@ -234,7 +251,9 @@ export const InverseAllStates = () => {
</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">
<RadioButton
label="Selected (default, hover, focus)"
@@ -256,9 +275,12 @@ export const FigmaPascalCase = () => {
return (
<div className="space-y-6">
<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">
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>
<div className="space-y-4">
<RadioButton
@@ -278,7 +300,9 @@ export const FigmaPascalCase = () => {
</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">
<RadioButton
label="Standard mode (lowercase) - still works"
+2 -1
View File
@@ -10,7 +10,8 @@ export default {
argTypes: {
propSwitch: {
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: {
control: "select",
+2 -1
View File
@@ -74,7 +74,8 @@ export const Embedded = Template.bind({});
Embedded.args = {
label: "Section content",
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",
size: "large",
rows: 4,
+4 -2
View File
@@ -51,7 +51,8 @@ Active.args = {
Active.parameters = {
docs: {
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 = {
docs: {
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 {
title: "Components/Icons/Logo",
@@ -24,9 +24,15 @@ export default {
],
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" },
description: "Whether to show the text portion of the logo",
description: "Whether to show the CommunityRule wordmark",
},
},
tags: ["autodocs"],
@@ -35,13 +41,13 @@ export default {
export const Default = {
args: {
size: "default",
showText: true,
wordmark: true,
},
};
export const Sizes = {
args: {
showText: true,
wordmark: true,
},
render: (args) => (
<div className="space-y-6">
@@ -76,7 +82,7 @@ export const Sizes = {
export const IconOnly = {
args: {
size: "default",
showText: false,
wordmark: false,
},
render: (args) => (
<div className="space-y-6">
@@ -123,11 +129,11 @@ export const TopNavContext = {
<div className="space-y-4">
<div className="flex items-center space-x-4">
<span className="text-white text-sm w-32">FolderTop:</span>
<Logo size="topNavFolderTop" showText={true} />
<Logo size="topNavFolderTop" wordmark palette="inverse" />
</div>
<div className="flex items-center space-x-4">
<span className="text-white text-sm w-32">Header:</span>
<Logo size="topNavHeader" showText={true} />
<Logo size="topNavHeader" wordmark />
</div>
</div>
</div>
@@ -148,13 +154,11 @@ export const CreateFlowContext = {
render: () => (
<div className="min-h-screen bg-black p-8">
<div className="max-w-4xl mx-auto">
<h2 className="text-white font-semibold mb-6">
Create Flow Context
</h2>
<h2 className="text-white font-semibold mb-6">Create Flow Context</h2>
<div className="space-y-4">
<div className="flex items-center space-x-4">
<span className="text-white text-sm w-32">CreateFlow:</span>
<Logo size="createFlow" showText={true} />
<Logo size="createFlow" wordmark />
</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: (
<div className="space-y-4">
<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>
</div>
),
+10 -5
View File
@@ -136,9 +136,15 @@ export const AllModes = {
<div>
<h3 className="text-white font-semibold mb-3">Default Mode</h3>
<div className="space-x-4">
<MenuBarItem size="X Small" mode="default">X Small</MenuBarItem>
<MenuBarItem size="Large" mode="default">Large</MenuBarItem>
<MenuBarItem size="X Large" mode="default">X Large</MenuBarItem>
<MenuBarItem size="X Small" mode="default">
X Small
</MenuBarItem>
<MenuBarItem size="Large" mode="default">
Large
</MenuBarItem>
<MenuBarItem size="X Large" mode="default">
X Large
</MenuBarItem>
</div>
</div>
@@ -173,8 +179,7 @@ export const AllModes = {
parameters: {
docs: {
description: {
story:
"Complete overview of all menu item modes, sizes, and states.",
story: "Complete overview of all menu item modes, sizes, and states.",
},
},
},
+6 -4
View File
@@ -15,11 +15,13 @@ export default {
argTypes: {
folderTop: {
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: {
control: "boolean",
description: "Whether the user is logged in (affects displayed elements).",
description:
"Whether the user is logged in (affects displayed elements).",
},
profile: {
control: "boolean",
@@ -123,8 +125,8 @@ export const StandardInPageContext = {
</h1>
<p className="text-white mb-4">
This demonstrates how the standard header looks in a realistic page
context. The header maintains its responsive behavior while providing
navigation for the page content.
context. The header maintains its responsive behavior while
providing navigation for the page content.
</p>
<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
View File
@@ -44,8 +44,7 @@ export const SizeM = {
export const CenterJustified = {
args: {
title: "How should conflicts be resolved?",
description:
"You can also combine or add new approaches to the list",
description: "You can also combine or add new approaches to the list",
justification: "center",
size: "L",
},
+2 -1
View File
@@ -20,7 +20,8 @@ export default {
},
secondButton: {
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"],

Some files were not shown because too many files have changed in this diff Show More