Completed template
This commit is contained in:
@@ -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
@@ -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;
|
||||
|
||||
@@ -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,2 +1,3 @@
|
||||
export { default as Icon } from "./Icon";
|
||||
export type { IconName, IconProps } from "./Icon";
|
||||
export { default as Logo } from "./logo";
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./Logo";
|
||||
@@ -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":
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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)]",
|
||||
|
||||
@@ -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>(
|
||||
({
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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";
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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)
|
||||
*/
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 |
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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}`);
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
|
||||
@@ -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.",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user