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 {
|
||||
|
||||
Reference in New Issue
Block a user