Component cleanup

This commit is contained in:
adilallo
2026-04-29 07:20:16 -06:00
parent 252848eba9
commit e6127f1a3f
267 changed files with 2087 additions and 2196 deletions
@@ -0,0 +1,28 @@
/** Labeled paragraph group (Figma “Text” stacks under Membership / Decision-making, etc.). */
export interface CommunityRuleLabeledBlock {
label: string;
body: string;
}
export interface CommunityRuleEntry {
title: string;
/** Plain text; split on blank lines into paragraphs when rendering. */
body: string;
/**
* When set, rendered as Figma-style label + body stacks. If non-empty, takes
* precedence over {@link body} for main content (body may be empty).
*/
blocks?: CommunityRuleLabeledBlock[];
}
export interface CommunityRuleSection {
categoryName: string;
entries: CommunityRuleEntry[];
}
export interface CommunityRuleProps {
sections: CommunityRuleSection[];
className?: string;
/** When true, wrap in white background with left teal bar (small breakpoint). */
useCardStyle?: boolean;
}
@@ -0,0 +1,56 @@
"use client";
import { memo } from "react";
import Section from "../Section";
import TextBlock from "../TextBlock";
import type { CommunityRuleProps } from "./CommunityRule.types";
/**
* Figma: **Sections** canvas, “Community Rule” frame (16489:4507). Composes
* **`Section`** (22002:30757) + **`TextBlock`** (22001:29793); canonical code under **`type/CommunityRule`**.
*/
const SECTION_GAP = "var(--measures-spacing-1200, 64px)";
const TEAL_BG = "var(--color-teal-teal50, #c9fef9)";
function CommunityRuleView({
sections,
className = "",
useCardStyle = false,
}: CommunityRuleProps) {
const rootClass = useCardStyle
? `rounded-[12px] bg-white pl-3 border-l-4 ${className}`
: className;
const rootStyle = useCardStyle ? { borderLeftColor: TEAL_BG } : undefined;
return (
<div
className={`flex flex-col min-w-0 ${rootClass}`}
style={{ gap: SECTION_GAP, ...rootStyle }}
>
{sections.map((ruleSection, sectionIndex) => (
<Section
key={sectionIndex}
categoryName={ruleSection.categoryName}
showRail={!useCardStyle}
>
{ruleSection.entries.map((entry, entryIndex) => {
const hasBlocks = Boolean(entry.blocks?.length);
return (
<TextBlock
key={entryIndex}
title={entry.title}
body={hasBlocks ? undefined : entry.body}
rows={hasBlocks ? entry.blocks : undefined}
/>
);
})}
</Section>
))}
</div>
);
}
CommunityRuleView.displayName = "CommunityRuleView";
export default memo(CommunityRuleView);
@@ -0,0 +1,7 @@
export { default } from "./CommunityRule.view";
export type {
CommunityRuleProps,
CommunityRuleLabeledBlock,
CommunityRuleEntry,
CommunityRuleSection,
} from "./CommunityRule.types";
@@ -46,7 +46,8 @@ function HeaderLockupView({
{/* Description */}
{description != null &&
!(typeof description === "string" && description.length === 0) && (
!(typeof description === "string" && description.length === 0) &&
(typeof description === "string" ? (
<p
className={`font-inter font-normal max-w-[640px] overflow-hidden relative shrink-0 ${descriptionColorClass} text-ellipsis w-full whitespace-pre-wrap ${
isLeft ? "" : "text-center"
@@ -56,7 +57,17 @@ function HeaderLockupView({
>
{description}
</p>
)}
) : (
<div
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]"
}`}
>
{description}
</div>
))}
</div>
);
}
@@ -0,0 +1,41 @@
"use client";
import { memo } from "react";
import InputLabelView from "./InputLabel.view";
import type { InputLabelProps } from "./InputLabel.types";
/**
* Figma: "Utility / InputLabel"; canonical code under `type/`.
* Reusable form-input label with
* optional asterisk, help icon, and helper text.
*/
const InputLabelContainer = memo<InputLabelProps>(
({
label,
helpIcon = false,
asterisk = false,
helperText = false,
size: sizeProp = "s",
palette: paletteProp = "default",
className = "",
}) => {
const size = sizeProp;
const palette = paletteProp;
return (
<InputLabelView
label={label}
helpIcon={helpIcon}
asterisk={asterisk}
helperText={helperText}
size={size}
palette={palette}
className={className}
/>
);
},
);
InputLabelContainer.displayName = "InputLabel";
export default InputLabelContainer;
@@ -0,0 +1,42 @@
export type InputLabelSizeValue = "s" | "m";
export type InputLabelPaletteValue = "default" | "inverse";
export interface InputLabelProps {
/**
* The label text to display
*/
label: string;
/**
* Show help icon next to label
*/
helpIcon?: boolean;
/**
* Show asterisk (*) to indicate required field
*/
asterisk?: boolean;
/**
* Helper text to display on the right side.
* If boolean true, shows "Optional text".
* If string, shows the provided text.
*/
helperText?: boolean | string;
/**
* Size variant: "s" (small) or "m" (medium)
*/
size?: InputLabelSizeValue;
/**
* Palette variant: "default" or "inverse"
*/
palette?: InputLabelPaletteValue;
className?: string;
}
export interface InputLabelViewProps {
label: string;
helpIcon: boolean;
asterisk: boolean;
helperText: boolean | string;
size: "s" | "m";
palette: "default" | "inverse";
className: string;
}
@@ -0,0 +1,108 @@
"use client";
import { memo } from "react";
import { getAssetPath, ASSETS } from "../../../../lib/assetUtils";
import type { InputLabelViewProps } from "./InputLabel.types";
function InputLabelView({
label,
helpIcon,
asterisk,
helperText,
size,
palette,
className = "",
}: InputLabelViewProps) {
const isSmall = size === "s";
const isInverse = palette === "inverse";
// Size-based typography
const labelTextSize = isSmall
? "text-[length:var(--sizing-350,14px)] leading-[20px]"
: "text-[length:var(--sizing-400,16px)] leading-[24px]";
const helperTextSize = isSmall
? "text-[length:var(--measures-sizing-250,10px)] leading-[var(--measures-spacing-350,14px)]"
: "text-[length:var(--sizing-300,12px)] leading-[16px]";
const asteriskSize = isSmall
? "text-[length:var(--measures-sizing-250,10px)] leading-[var(--measures-spacing-300,12px)]"
: "text-[length:var(--measures-spacing-300,12px)] leading-[var(--measures-spacing-300,12px)]";
// Palette-based colors
const labelColor = isInverse
? "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)]";
// Layout: S uses flex-wrap with baseline, M uses flex with center
const containerClass = isSmall
? "flex flex-wrap gap-[var(--measures-spacing-200,4px_8px)] items-baseline pr-[var(--measures-spacing-100,4px)] relative w-full"
: "flex gap-[4px] items-center relative w-full";
const labelContainerClass = isSmall
? "flex gap-[var(--measures-spacing-050,2px)] items-center relative shrink-0"
: "flex gap-[var(--measures-spacing-100,4px)] items-center relative shrink-0";
const helpIconSize = isSmall ? "size-[12px]" : "size-[16px]";
// Help icon color filter based on palette
// Default: Light yellow (#f6f06f / rgba(246, 240, 111, 1)) - SVG already has this color
// Inverse: Dark yellow (#8c8505 / rgba(140, 133, 5, 1))
// For default, no filter needed as SVG already has the correct yellow
// For inverse, darken the yellow
const helpIconFilter = isInverse
? "brightness(0.57) saturate(100%)" // Dark yellow (#8c8505) - darken the existing yellow
: undefined; // No filter for default - use SVG's native yellow color
return (
<div className={`${containerClass} ${className}`}>
<div className={labelContainerClass}>
<div className="flex gap-px items-start relative shrink-0">
<p
className={`font-inter font-normal ${labelTextSize} ${labelColor} relative shrink-0`}
>
{label}
</p>
{asterisk && (
<p
className={`font-inter font-medium ${asteriskSize} relative shrink-0 text-[color:var(--color-content-default-negative-primary,#ea4845)]`}
>
*
</p>
)}
</div>
{helpIcon && (
<div className={`relative shrink-0 ${helpIconSize}`}>
{/* eslint-disable-next-line @next/next/no-img-element -- icon from asset path */}
<img
src={getAssetPath(ASSETS.ICON_HELP)}
alt="Help"
className="block max-w-none size-full"
style={
helpIconFilter
? {
filter: helpIconFilter,
}
: undefined
}
/>
</div>
)}
</div>
{helperText && (
<p
className={`flex-[1_0_0] font-inter font-normal ${helperTextSize} min-h-px min-w-px relative ${helperTextColor} text-right`}
>
{typeof helperText === "string" ? helperText : "Optional text"}
</p>
)}
</div>
);
}
InputLabelView.displayName = "InputLabelView";
export default memo(InputLabelView);
+3
View File
@@ -0,0 +1,3 @@
import InputLabel from "./InputLabel.container";
export default InputLabel;
@@ -0,0 +1,10 @@
import type { ReactNode } from "react";
export interface SectionProps {
/** Category label (16px medium, invert secondary). */
categoryName: string;
/** When true, renders the 12px-gapped vertical rail like Figma Section (22002:30757). */
showRail?: boolean;
children: ReactNode;
className?: string;
}
@@ -0,0 +1,55 @@
"use client";
import { memo } from "react";
import type { SectionProps } from "./Section.types";
/**
* Figma: **Section** (22002:30757) — vertical rail + category + stacked content
* (typically **TextBlock** children inside Community Rule).
*/
const SECTION_LINE_COLOR = "var(--color-border-default-tertiary, #464646)";
const CATEGORY_CLASS =
"font-inter font-medium text-[16px] leading-[20px] text-[var(--color-content-invert-secondary,#1f1f1f)] shrink-0 w-full min-w-0";
function SectionView({
categoryName,
showRail = true,
children,
className = "",
}: SectionProps) {
const inner = (
<div className="flex min-w-0 flex-1 flex-col gap-4" style={{ gap: "16px" }}>
<p className={CATEGORY_CLASS}>{categoryName}</p>
<div className="flex min-w-0 flex-col gap-6">{children}</div>
</div>
);
if (!showRail) {
return (
<div className={`flex min-w-0 flex-col ${className}`.trim()} data-name="Section">
{inner}
</div>
);
}
return (
<div
className={`flex min-w-0 items-stretch gap-3 ${className}`.trim()}
style={{ gap: "12px" }}
data-name="Section"
>
<div
className="w-px shrink-0 self-stretch"
style={{ backgroundColor: SECTION_LINE_COLOR }}
aria-hidden
/>
{inner}
</div>
);
}
SectionView.displayName = "SectionView";
export default memo(SectionView);
+2
View File
@@ -0,0 +1,2 @@
export { default } from "./Section.view";
export type { SectionProps } from "./Section.types";
@@ -0,0 +1,92 @@
"use client";
import { memo } from "react";
import type { SectionHeaderVariantValue } from "../../../../lib/propNormalization";
interface SectionHeaderProps {
title: string;
subtitle: string;
titleLg?: string;
variant?: SectionHeaderVariantValue;
/** When set with `variant="multi-line"`, large screens show three title lines (Figma SectionCardSteps). */
stackedDesktopLines?: readonly [string, string, string];
}
/**
* Figma: **Type / SectionHeader** ([17411:10981](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=17411-10981)).
* Responsive title + subtitle lockup: stacked on small viewports, split row from `lg` up.
*/
const SectionHeader = memo<SectionHeaderProps>(
({
title,
subtitle,
titleLg,
variant: variantProp = "default",
stackedDesktopLines,
}) => {
const variant = variantProp;
const useStackedDesktop =
variant === "multi-line" && stackedDesktopLines != null;
const rowAlignClasses =
variant === "multi-line"
? "lg:flex-row lg:justify-between lg:items-center xl:gap-[var(--spacing-scale-024)]"
: "lg:flex-row lg:justify-between lg:items-start xl:gap-[var(--spacing-scale-024)]";
return (
<div
className={`flex flex-col gap-[var(--spacing-scale-004)] w-full ${rowAlignClasses}`}
>
{/* Title — left column at lg+ */}
<div
className={
variant === "multi-line"
? "lg:w-[50%] lg:h-[var(--spacing-scale-120)] lg:flex lg:items-center xl:w-[50%] xl:h-[156px] xl:flex xl:items-center"
: "lg:w-[369px] lg:h-[var(--spacing-scale-120)] lg:flex lg:items-center xl:w-[452px] xl:h-[156px] xl:flex xl:items-center"
}
>
<h2
className={
variant === "multi-line"
? "font-bricolage-grotesque font-bold text-[28px] leading-[36px] md:text-[32px] md:leading-[40px] lg:w-[410px] lg:text-left xl:text-[40px] xl:leading-[52px] text-[var(--color-content-default-primary)]"
: "font-bricolage-grotesque font-bold text-[28px] leading-[36px] sm:text-[32px] sm:leading-[40px] lg:text-[32px] lg:leading-[40px] lg:w-[369px] lg:pr-[var(--spacing-scale-096)] xl:text-[40px] xl:leading-[52px] xl:w-[452px] xl:pr-[var(--spacing-scale-096)] text-[var(--color-content-default-primary)]"
}
>
<span className="block lg:hidden">{title}</span>
{useStackedDesktop ? (
<span className="hidden lg:block">
<span className="block">{stackedDesktopLines[0]}</span>
<span className="block">{stackedDesktopLines[1]}</span>
<span className="block">{stackedDesktopLines[2]}</span>
</span>
) : (
<span className="hidden lg:block">{titleLg || title}</span>
)}
</h2>
</div>
{/* Subtitle — right column at lg+ (Figma X Large / Large / stacked small) */}
<div
className={
variant === "multi-line"
? "lg:w-[50%] lg:h-[var(--spacing-scale-120)] lg:flex lg:items-center lg:justify-end lg:ml-[var(--spacing-scale-016)] xl:ml-0 xl:w-[50%] xl:h-[156px] xl:flex xl:items-center xl:justify-end"
: "lg:w-[928px] lg:h-[var(--spacing-scale-120)] lg:flex lg:items-center lg:justify-end xl:h-[156px] xl:flex xl:items-center xl:justify-end"
}
>
<p
className={
variant === "multi-line"
? "font-inter font-normal text-[14px] leading-[20px] md:text-[18px] md:leading-[130%] xl:text-[24px] xl:leading-[32px] text-[var(--color-content-default-tertiary)] lg:text-right"
: "font-inter font-normal text-[18px] leading-[130%] sm:text-[18px] sm:leading-[32px] lg:text-[24px] lg:leading-[32px] xl:text-[32px] xl:leading-[40px] xl:text-right text-[#484848] sm:text-[var(--color-content-default-tertiary)] lg:text-[var(--color-content-default-tertiary)] xl:text-[var(--color-content-default-tertiary)] tracking-[0px]"
}
>
{subtitle}
</p>
</div>
</div>
);
},
);
SectionHeader.displayName = "SectionHeader";
export default SectionHeader;
@@ -0,0 +1,2 @@
export { default } from "./SectionHeader";
export type { SectionHeaderVariantValue } from "../../../../lib/propNormalization";
@@ -0,0 +1,11 @@
import type { CommunityRuleLabeledBlock } from "../CommunityRule/CommunityRule.types";
export interface TextBlockProps {
/** Figma X Small/Heading — entry title (20px bold, 28px line). */
title: string;
/** Plain copy; blank-line splits become paragraphs when `rows` is absent. */
body?: string;
/** Figma labeled stacks (14px medium label + 14px body). Overrides plain `body` when non-empty. */
rows?: CommunityRuleLabeledBlock[];
className?: string;
}
@@ -0,0 +1,71 @@
"use client";
import { memo } from "react";
import type { TextBlockProps } from "./TextBlock.types";
/**
* Figma: Utility / **Community Rule / Text Block** (22001:29793).
* Title + body paragraphs and/or labeled rows (12px between stacks, 8px label→body).
*/
const ENTRY_TITLE_CLASS =
"font-inter font-bold text-[20px] leading-[28px] text-[var(--color-content-invert-primary)] shrink-0";
const ROW_LABEL_CLASS =
"font-inter font-medium text-[14px] leading-[18px] text-[var(--color-content-invert-primary)] shrink-0";
const PARAGRAPH_CLASS =
"font-inter font-normal text-[14px] leading-[20px] text-[var(--color-content-invert-primary)] shrink-0";
function bodyToParagraphs(body: string): string[] {
return body
.split(/\n\n/)
.map((s) => s.trim())
.filter((s) => s.length > 0);
}
function ParagraphGroup({ text }: { text: string }) {
const paragraphs = bodyToParagraphs(text);
if (paragraphs.length === 0) return null;
return (
<div className="flex min-w-0 flex-col gap-2">
{paragraphs.map((p, i) => (
<p key={i} className={`${PARAGRAPH_CLASS} whitespace-pre-wrap`}>
{p}
</p>
))}
</div>
);
}
function TextBlockView({
title,
body = "",
rows,
className = "",
}: TextBlockProps) {
const hasRows = rows && rows.length > 0;
return (
<div
className={`flex min-w-0 flex-col gap-2 ${className}`.trim()}
data-name="TextBlock"
>
<p className={`${ENTRY_TITLE_CLASS} w-full min-w-0`}>{title}</p>
<div className="flex min-w-0 flex-col gap-3">
{hasRows
? rows!.map((row, i) => (
<div key={i} className="flex min-w-0 flex-col gap-2">
<p className={ROW_LABEL_CLASS}>{row.label}</p>
<ParagraphGroup text={row.body} />
</div>
))
: body.trim().length > 0 && <ParagraphGroup text={body} />}
</div>
</div>
);
}
TextBlockView.displayName = "TextBlockView";
export default memo(TextBlockView);
+2
View File
@@ -0,0 +1,2 @@
export { default } from "./TextBlock.view";
export type { TextBlockProps } from "./TextBlock.types";