Add custom intervention modals
This commit is contained in:
@@ -3,15 +3,19 @@
|
||||
import Image from "next/image";
|
||||
import { memo } from "react";
|
||||
import ArrowBackIcon from "./arrow_back.svg";
|
||||
import ChevronRightIcon from "./chevron_right.svg";
|
||||
import ContentCopyIcon from "./content_copy.svg";
|
||||
import CsvIcon from "./csv.svg";
|
||||
import EditIcon from "./edit.svg";
|
||||
import ExclamationIcon from "./exclamation.svg";
|
||||
import ChevronRightIcon from "./chevron_right.svg";
|
||||
import CsvIcon from "./csv.svg";
|
||||
import ImageGlyphIcon from "./image.svg";
|
||||
import LogOutIcon from "./log_out.svg";
|
||||
import MailIcon from "./mail.svg";
|
||||
import MarkdownCopyIcon from "./markdown_copy.svg";
|
||||
import NumberIcon from "./number.svg";
|
||||
import PictureAsPdfIcon from "./picture_as_pdf.svg";
|
||||
import TagsIcon from "./tags.svg";
|
||||
import TextBlockIcon from "./text_block.svg";
|
||||
import WarningIcon from "./warning.svg";
|
||||
|
||||
export const ICON_NAME_OPTIONS = [
|
||||
@@ -21,10 +25,14 @@ export const ICON_NAME_OPTIONS = [
|
||||
"csv",
|
||||
"edit",
|
||||
"exclamation",
|
||||
"image",
|
||||
"log_out",
|
||||
"mail",
|
||||
"markdown_copy",
|
||||
"number",
|
||||
"picture_as_pdf",
|
||||
"tags",
|
||||
"text_block",
|
||||
"warning",
|
||||
] as const;
|
||||
|
||||
@@ -42,10 +50,14 @@ const iconMap: Record<IconName, SvgComponent> = {
|
||||
csv: CsvIcon,
|
||||
edit: EditIcon,
|
||||
exclamation: ExclamationIcon,
|
||||
image: ImageGlyphIcon,
|
||||
log_out: LogOutIcon,
|
||||
mail: MailIcon,
|
||||
markdown_copy: MarkdownCopyIcon,
|
||||
number: NumberIcon,
|
||||
picture_as_pdf: PictureAsPdfIcon,
|
||||
tags: TagsIcon,
|
||||
text_block: TextBlockIcon,
|
||||
warning: WarningIcon,
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_19787_11971)">
|
||||
<path d="M5 3.75H19C19.6858 3.75 20.25 4.31421 20.25 5V19C20.25 19.6858 19.6858 20.25 19 20.25H5C4.31421 20.25 3.75 19.6858 3.75 19V5C3.75 4.31421 4.31421 3.75 5 3.75Z" stroke="white" stroke-width="1.5"/>
|
||||
<path d="M10.7496 15.298L9.39281 13.7098C9.18897 13.4712 8.81826 13.4772 8.62221 13.7222L6.64988 16.1877C6.38797 16.515 6.62106 17 7.04031 17H16.9828C17.398 17 17.6323 16.5233 17.3787 16.1946L14.532 12.5045C14.334 12.2477 13.9477 12.2445 13.7454 12.498L11.5205 15.2852C11.3246 15.5306 10.9536 15.5368 10.7496 15.298Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_19787_11971">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 791 B |
@@ -0,0 +1,10 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_19787_11962)">
|
||||
<path d="M20.5 9.75L21 7.75H17L18 3.75H16L15 7.75H11L12 3.75H10L9 7.75H5L4.5 9.75H8.5L7.5 13.75H3.5L3 15.75H7L6 19.75H8L9 15.75H13L12 19.75H14L15 15.75H19L19.5 13.75H15.5L16.5 9.75H20.5ZM13.5 13.75H9.5L10.5 9.75H14.5L13.5 13.75Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_19787_11962">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 498 B |
@@ -0,0 +1,7 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.51299 10.462C7.32099 10.654 7.08332 10.75 6.79999 10.75C6.51665 10.75 6.27899 10.654 6.08699 10.462C5.89565 10.2707 5.79999 10.0333 5.79999 9.75C5.79999 9.46667 5.89565 9.229 6.08699 9.037C6.27899 8.84567 6.51665 8.75 6.79999 8.75C7.08332 8.75 7.32099 8.84567 7.51299 9.037C7.70432 9.229 7.79999 9.46667 7.79999 9.75C7.79999 10.0333 7.70432 10.2707 7.51299 10.462Z" fill="white"/>
|
||||
<path d="M12.8 14.75H6.79999C6.51665 14.75 6.27899 14.654 6.08699 14.462C5.89565 14.2707 5.79999 14.0333 5.79999 13.75C5.79999 13.4667 5.89565 13.229 6.08699 13.037C6.27899 12.8457 6.51665 12.75 6.79999 12.75H12.8C13.0833 12.75 13.321 12.8457 13.513 13.037C13.7043 13.229 13.8 13.4667 13.8 13.75C13.8 14.0333 13.7043 14.2707 13.513 14.462C13.321 14.654 13.0833 14.75 12.8 14.75Z" fill="white"/>
|
||||
<path d="M17.512 14.462C17.3207 14.654 17.0833 14.75 16.8 14.75C16.5167 14.75 16.2793 14.654 16.088 14.462C15.896 14.2707 15.8 14.0333 15.8 13.75C15.8 13.4667 15.896 13.229 16.088 13.037C16.2793 12.8457 16.5167 12.75 16.8 12.75C17.0833 12.75 17.3207 12.8457 17.512 13.037C17.704 13.229 17.8 13.4667 17.8 13.75C17.8 14.0333 17.704 14.2707 17.512 14.462Z" fill="white"/>
|
||||
<path d="M16.8 10.75H10.8C10.5167 10.75 10.2793 10.654 10.088 10.462C9.89599 10.2707 9.79999 10.0333 9.79999 9.75C9.79999 9.46667 9.89599 9.229 10.088 9.037C10.2793 8.84567 10.5167 8.75 10.8 8.75H16.8C17.0833 8.75 17.3207 8.84567 17.512 9.037C17.704 9.229 17.8 9.46667 17.8 9.75C17.8 10.0333 17.704 10.2707 17.512 10.462C17.3207 10.654 17.0833 10.75 16.8 10.75Z" fill="white"/>
|
||||
<path d="M3.875 4.5H19.875C20.2243 4.5 20.5052 4.61557 20.7578 4.86816C21.0095 5.11983 21.125 5.4003 21.125 5.75V17.75C21.125 18.0993 21.0092 18.3796 20.7578 18.6318C20.5053 18.8839 20.2247 19 19.875 19H3.875C3.5253 19 3.24483 18.8845 2.99316 18.6328C2.74057 18.3802 2.625 18.0993 2.625 17.75V5.75C2.625 5.40073 2.74099 5.1209 2.99316 4.86914L2.99414 4.86816C3.2459 4.61599 3.52573 4.5 3.875 4.5Z" stroke="white" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,6 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 20C3.45 20 2.97917 19.8042 2.5875 19.4125C2.19583 19.0208 2 18.55 2 18V6C2 5.45 2.19583 4.97917 2.5875 4.5875C2.97917 4.19583 3.45 4 4 4H20C20.55 4 21.0208 4.19583 21.4125 4.5875C21.8042 4.97917 22 5.45 22 6V18C22 18.55 21.8042 19.0208 21.4125 19.4125C21.0208 19.8042 20.55 20 20 20H4Z" stroke="white" stroke-width="1.5"/>
|
||||
<path d="M18 17H6C5.71667 17 5.47917 16.9042 5.2875 16.7125C5.09583 16.5208 5 16.2833 5 16C5 15.7167 5.09583 15.4792 5.2875 15.2875C5.47917 15.0958 5.71667 15 6 15H18C18.2833 15 18.5208 15.0958 18.7125 15.2875C18.9042 15.4792 19 15.7167 19 16C19 16.2833 18.9042 16.5208 18.7125 16.7125C18.5208 16.9042 18.2833 17 18 17Z" fill="white"/>
|
||||
<path d="M18 13H6C5.71667 13 5.47917 12.9042 5.2875 12.7125C5.09583 12.5208 5 12.2833 5 12C5 11.7167 5.09583 11.4792 5.2875 11.2875C5.47917 11.0958 5.71667 11 6 11H18C18.2833 11 18.5208 11.0958 18.7125 11.2875C18.9042 11.4792 19 11.7167 19 12C19 12.2833 18.9042 12.5208 18.7125 12.7125C18.5208 12.9042 18.2833 13 18 13Z" fill="white"/>
|
||||
<path d="M14 9H6C5.71667 9 5.47917 8.90417 5.2875 8.7125C5.09583 8.52083 5 8.28333 5 8C5 7.71667 5.09583 7.47917 5.2875 7.2875C5.47917 7.09583 5.71667 7 6 7H14C14.2833 7 14.5208 7.09583 14.7125 7.2875C14.9042 7.47917 15 7.71667 15 8C15 8.28333 14.9042 8.52083 14.7125 8.7125C14.5208 8.90417 14.2833 9 14 9Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
|
||||
/**
|
||||
* Figma: Community Rule System — **Vertical button** (`19787:10896`).
|
||||
*
|
||||
* Tile control: column layout, brand-primary border on transparent surface,
|
||||
* 32px icon slot + centered 14/18 medium label (label rendered by `children`).
|
||||
*/
|
||||
export interface VerticalProps {
|
||||
children: React.ReactNode;
|
||||
onClick?: (_event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
ariaLabel?: string;
|
||||
type?: "button" | "submit" | "reset";
|
||||
"data-testid"?: string;
|
||||
}
|
||||
|
||||
function VerticalComponent({
|
||||
children,
|
||||
onClick,
|
||||
className = "",
|
||||
disabled = false,
|
||||
ariaLabel,
|
||||
type = "button",
|
||||
"data-testid": dataTestId,
|
||||
}: VerticalProps) {
|
||||
const base =
|
||||
"box-border flex w-[90px] shrink-0 cursor-pointer flex-col items-center gap-[var(--spacing-scale-008)] rounded-[var(--spacing-scale-004)] border border-solid border-[var(--color-border-default-brand-primary)] bg-transparent px-[var(--spacing-scale-008)] py-[var(--spacing-scale-012)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-invert-primary)] disabled:cursor-not-allowed disabled:opacity-60";
|
||||
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
aria-label={ariaLabel}
|
||||
data-testid={dataTestId}
|
||||
className={`${base} ${className}`.trim()}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
VerticalComponent.displayName = "Vertical";
|
||||
|
||||
export default memo(VerticalComponent);
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./Vertical";
|
||||
export type { VerticalProps } from "./Vertical";
|
||||
@@ -32,6 +32,10 @@ const CardStackContainer = memo<CardStackProps>(
|
||||
headerLockupSize,
|
||||
toggleAlignment = "center",
|
||||
className = "",
|
||||
showAddCard = false,
|
||||
addCardLabel = "",
|
||||
addCardAriaLabel = "",
|
||||
onAddCard,
|
||||
}) => {
|
||||
const [internalExpanded, setInternalExpanded] = useState(false);
|
||||
const [internalSelectedIds, setInternalSelectedIds] = useState<string[]>(
|
||||
@@ -90,6 +94,10 @@ const CardStackContainer = memo<CardStackProps>(
|
||||
headerLockupSize={headerLockupSize}
|
||||
toggleAlignment={toggleAlignment}
|
||||
className={className}
|
||||
showAddCard={showAddCard}
|
||||
addCardLabel={addCardLabel}
|
||||
addCardAriaLabel={addCardAriaLabel}
|
||||
onAddCard={onAddCard}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ReactNode } from "react";
|
||||
import type { HeaderLockupSizeValue } from "../../type/HeaderLockup/HeaderLockup.types";
|
||||
|
||||
export interface CardStackItem {
|
||||
@@ -18,7 +19,7 @@ export interface CardStackProps {
|
||||
toggleLabel?: string;
|
||||
showLessLabel?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
description?: ReactNode;
|
||||
/** "default" = compact grid/column + expanded grid; "singleStack" = always one column, expand shows more in same stack */
|
||||
layout?: "default" | "singleStack";
|
||||
/**
|
||||
@@ -45,6 +46,11 @@ export interface CardStackProps {
|
||||
/** Alignment of the expand/collapse control in `singleStack` layout (Figma right-rail: end). */
|
||||
toggleAlignment?: "center" | "end";
|
||||
className?: string;
|
||||
/** Optional “Add” entry (e.g. custom method card wizard). */
|
||||
showAddCard?: boolean;
|
||||
addCardLabel?: string;
|
||||
addCardAriaLabel?: string;
|
||||
onAddCard?: () => void;
|
||||
}
|
||||
|
||||
export interface CardStackViewProps {
|
||||
@@ -57,7 +63,7 @@ export interface CardStackViewProps {
|
||||
toggleLabel: string;
|
||||
showLessLabel: string;
|
||||
title: string;
|
||||
description: string;
|
||||
description: ReactNode;
|
||||
layout: "default" | "singleStack";
|
||||
compactRecommendedLimit: number;
|
||||
compactCardIds: string[] | undefined;
|
||||
@@ -65,4 +71,8 @@ export interface CardStackViewProps {
|
||||
headerLockupSize: HeaderLockupSizeValue | undefined;
|
||||
toggleAlignment: "center" | "end";
|
||||
className: string;
|
||||
showAddCard: boolean;
|
||||
addCardLabel: string;
|
||||
addCardAriaLabel: string;
|
||||
onAddCard?: () => void;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,77 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import HeaderLockup from "../../type/HeaderLockup";
|
||||
import type { HeaderLockupSizeValue } from "../../type/HeaderLockup/HeaderLockup.types";
|
||||
import Selection from "../Selection";
|
||||
import type { CardStackViewProps } from "./CardStack.types";
|
||||
|
||||
function CardStackHeaderLockup({
|
||||
title,
|
||||
description,
|
||||
justification,
|
||||
size,
|
||||
wrapperClassName,
|
||||
}: {
|
||||
title: string;
|
||||
description: ReactNode;
|
||||
justification: "center" | "left";
|
||||
size: HeaderLockupSizeValue;
|
||||
wrapperClassName?: string;
|
||||
}) {
|
||||
if (!title && !description) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={wrapperClassName ?? "min-w-0"}>
|
||||
<HeaderLockup
|
||||
title={title}
|
||||
description={description}
|
||||
justification={justification}
|
||||
size={size}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CardStackAddCardButton({
|
||||
label,
|
||||
ariaLabel,
|
||||
onClick,
|
||||
}: {
|
||||
label: string;
|
||||
ariaLabel: string;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
aria-label={ariaLabel}
|
||||
className="flex min-h-[88px] w-full shrink-0 items-center justify-center rounded-[var(--measures-radius-medium,8px)] bg-[var(--color-surface-default-secondary)] font-inter text-base font-medium text-[var(--color-content-default-primary)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-invert-primary)]"
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden
|
||||
>
|
||||
<path
|
||||
d="M12 5v14M5 12h14"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
{label}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function CardStackView({
|
||||
cards,
|
||||
selectedIds,
|
||||
@@ -22,7 +90,19 @@ export function CardStackView({
|
||||
headerLockupSize,
|
||||
toggleAlignment,
|
||||
className,
|
||||
showAddCard,
|
||||
addCardLabel,
|
||||
addCardAriaLabel,
|
||||
onAddCard,
|
||||
}: CardStackViewProps) {
|
||||
const addTile =
|
||||
showAddCard && onAddCard && addCardLabel.length > 0 ? (
|
||||
<CardStackAddCardButton
|
||||
label={addCardLabel}
|
||||
ariaLabel={addCardAriaLabel || addCardLabel}
|
||||
onClick={onAddCard}
|
||||
/>
|
||||
) : null;
|
||||
const lockupSize = headerLockupSize ?? "L";
|
||||
const isSelected = (id: string) => selectedIds.includes(id);
|
||||
// Compact: explicit `compactCardIds` (caller-driven, used by create-flow
|
||||
@@ -47,16 +127,13 @@ export function CardStackView({
|
||||
const displayedCards = expanded ? cards : compactCards;
|
||||
return (
|
||||
<div className={`flex w-full flex-col gap-6 min-w-0 ${className}`}>
|
||||
{title || description ? (
|
||||
<div className="min-w-0 shrink-0">
|
||||
<HeaderLockup
|
||||
title={title}
|
||||
description={description}
|
||||
justification="center"
|
||||
size={lockupSize}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<CardStackHeaderLockup
|
||||
title={title}
|
||||
description={description}
|
||||
justification="center"
|
||||
size={lockupSize}
|
||||
wrapperClassName="min-w-0 shrink-0"
|
||||
/>
|
||||
<div className="flex w-full min-w-0 flex-col gap-2">
|
||||
{displayedCards.map((item) => (
|
||||
<Selection
|
||||
@@ -71,6 +148,7 @@ export function CardStackView({
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
/>
|
||||
))}
|
||||
{addTile}
|
||||
</div>
|
||||
{hasMore ? (
|
||||
<button
|
||||
@@ -89,16 +167,12 @@ export function CardStackView({
|
||||
|
||||
return (
|
||||
<div className={`flex w-full flex-col gap-6 min-w-0 ${className}`}>
|
||||
{title || description ? (
|
||||
<div className="min-w-0">
|
||||
<HeaderLockup
|
||||
title={title}
|
||||
description={description}
|
||||
justification="center"
|
||||
size={lockupSize}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<CardStackHeaderLockup
|
||||
title={title}
|
||||
description={description}
|
||||
justification="center"
|
||||
size={lockupSize}
|
||||
/>
|
||||
|
||||
{expanded ? (
|
||||
<div className="mx-auto grid w-full max-w-[min(100%,860px)] grid-cols-1 gap-x-4 gap-y-6 md:grid-cols-2">
|
||||
@@ -115,6 +189,9 @@ export function CardStackView({
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
/>
|
||||
))}
|
||||
{addTile ? (
|
||||
<div className="min-w-0 md:col-span-2">{addTile}</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : compactDesktopLayout === "pyramidFive" ? (
|
||||
<>
|
||||
@@ -133,6 +210,7 @@ export function CardStackView({
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
/>
|
||||
))}
|
||||
{addTile}
|
||||
</div>
|
||||
<div className="mx-auto hidden w-full max-w-[min(100%,860px)] md:block">
|
||||
{/*
|
||||
@@ -228,6 +306,11 @@ export function CardStackView({
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{addTile ? (
|
||||
<div className="mx-auto hidden w-full max-w-[min(100%,860px)] md:block">
|
||||
{addTile}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : compactDesktopLayout === "flexWrap" ? (
|
||||
<>
|
||||
@@ -246,6 +329,7 @@ export function CardStackView({
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
/>
|
||||
))}
|
||||
{addTile}
|
||||
</div>
|
||||
{/* md–lg: pyramid (2 + 1), each row centered; lg+: one centered row (not edge-to-edge in a 2-col grid) */}
|
||||
{compactCards.length === 3 ? (
|
||||
@@ -280,6 +364,9 @@ export function CardStackView({
|
||||
onClick={() => onCardSelect(compactCards[2].id)}
|
||||
/>
|
||||
</div>
|
||||
{addTile ? (
|
||||
<div className="flex w-full justify-center px-2">{addTile}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mx-auto hidden w-full max-w-[min(100%,860px)] flex-wrap justify-center gap-2 lg:flex">
|
||||
{compactCards.map((item) => (
|
||||
@@ -297,6 +384,11 @@ export function CardStackView({
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{addTile ? (
|
||||
<div className="mx-auto hidden w-full max-w-[min(100%,860px)] lg:flex lg:justify-center">
|
||||
<div className="w-full min-w-[281px] max-w-[574px]">{addTile}</div>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<div className="mx-auto hidden w-full max-w-[min(100%,860px)] flex-wrap justify-center gap-2 md:flex">
|
||||
@@ -318,6 +410,11 @@ export function CardStackView({
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{addTile ? (
|
||||
<div className="flex w-full min-w-0 shrink-0 justify-center md:w-[281px] md:max-w-[574px] md:flex-[1_1_100%]">
|
||||
{addTile}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -338,6 +435,7 @@ export function CardStackView({
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
/>
|
||||
))}
|
||||
{addTile}
|
||||
</div>
|
||||
{/* Compact 640+: 6-col grid so each card spans 2; second row centered (cols 2–3 and 4–5) */}
|
||||
<div className="hidden md:grid grid-cols-6 gap-x-4 gap-y-6 w-full">
|
||||
@@ -365,6 +463,9 @@ export function CardStackView({
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{addTile ? (
|
||||
<div className="col-span-6 min-w-0">{addTile}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { memo, useCallback, useMemo } from "react";
|
||||
import { useMessages } from "../../../contexts/MessagesContext";
|
||||
import { AddCustomFieldView } from "./AddCustomField.view";
|
||||
import type { AddCustomFieldProps, AddCustomFieldType } from "./AddCustomField.types";
|
||||
|
||||
/**
|
||||
* Figma: "Add Custom Field" control — Community Rule System (`20235:12994`).
|
||||
* Collapsed CTA expands to a 2×2 field-type picker (per-type modals deferred).
|
||||
*/
|
||||
const AddCustomFieldContainer = memo<AddCustomFieldProps>(
|
||||
({ active, onPressAdd, onSelectFieldType, className = "" }) => {
|
||||
const m = useMessages();
|
||||
const copy = m.create.customRule.customMethodCardWizard.addCustomField;
|
||||
|
||||
const fieldTypeLabels = useMemo(
|
||||
() => ({
|
||||
text: copy.fieldTypes.text,
|
||||
badges: copy.fieldTypes.badges,
|
||||
upload: copy.fieldTypes.upload,
|
||||
proportion: copy.fieldTypes.proportion,
|
||||
}),
|
||||
[copy.fieldTypes],
|
||||
);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(t: AddCustomFieldType) => {
|
||||
onSelectFieldType?.(t);
|
||||
},
|
||||
[onSelectFieldType],
|
||||
);
|
||||
|
||||
return (
|
||||
<AddCustomFieldView
|
||||
active={active}
|
||||
onPressAdd={onPressAdd}
|
||||
onSelectFieldType={handleSelect}
|
||||
ctaLabel={copy.cta}
|
||||
fieldTypeLabels={fieldTypeLabels}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
AddCustomFieldContainer.displayName = "AddCustomField";
|
||||
|
||||
export default AddCustomFieldContainer;
|
||||
@@ -0,0 +1,18 @@
|
||||
export type AddCustomFieldType = "text" | "badges" | "upload" | "proportion";
|
||||
|
||||
export interface AddCustomFieldProps {
|
||||
/** When true, show the 2×2 field-type grid; when false, show the primary CTA. */
|
||||
active: boolean;
|
||||
onPressAdd?: () => void;
|
||||
onSelectFieldType?: (type: AddCustomFieldType) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface AddCustomFieldViewProps {
|
||||
active: boolean;
|
||||
onPressAdd?: () => void;
|
||||
onSelectFieldType?: (type: AddCustomFieldType) => void;
|
||||
ctaLabel: string;
|
||||
fieldTypeLabels: Record<AddCustomFieldType, string>;
|
||||
className: string;
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import Icon, { type IconName } from "../../asset/icon";
|
||||
import Vertical from "../../buttons/Vertical";
|
||||
import type {
|
||||
AddCustomFieldType,
|
||||
AddCustomFieldViewProps,
|
||||
} from "./AddCustomField.types";
|
||||
|
||||
const FIELD_TYPE_ICONS: Record<AddCustomFieldType, IconName> = {
|
||||
text: "text_block",
|
||||
badges: "tags", // tag / chip list (filename: tags.svg)
|
||||
upload: "image", // image / file upload (filename: image.svg)
|
||||
proportion: "number", // numeric / proportion field (closest asset: number.svg)
|
||||
};
|
||||
|
||||
function FieldTypeButton({
|
||||
type,
|
||||
label,
|
||||
onSelect,
|
||||
}: {
|
||||
type: AddCustomFieldType;
|
||||
label: string;
|
||||
onSelect?: (t: AddCustomFieldType) => void;
|
||||
}) {
|
||||
return (
|
||||
<Vertical
|
||||
type="button"
|
||||
ariaLabel={label}
|
||||
onClick={() => onSelect?.(type)}
|
||||
>
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center">
|
||||
<Icon
|
||||
name={FIELD_TYPE_ICONS[type]}
|
||||
size={32}
|
||||
className="text-[var(--color-content-default-brand-primary,#fefcc9)]"
|
||||
/>
|
||||
</span>
|
||||
<span className="w-full text-center font-inter text-[14px] font-medium leading-[18px] text-[var(--color-content-default-brand-primary,#fefcc9)]">
|
||||
{label}
|
||||
</span>
|
||||
</Vertical>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stable block height for collapsed vs expanded so the Create dialog (`top-1/2 -translate-y-1/2`)
|
||||
* does not shrink and re-center when toggling `active`.
|
||||
*
|
||||
* - Collapsed CTA: `py-12` (48+48) + inner row (`py-3` + 20px icon/line) ≈ 140px border-box.
|
||||
* - Expanded: inner `p-4` (32) + Vertical tile (py 12+12, gap 8, 32px icon, 18px label) ≈ 114px — shorter without this floor.
|
||||
*/
|
||||
const ADD_CUSTOM_FIELD_SHELL_MIN_H_PX = 140;
|
||||
|
||||
function AddCustomFieldViewComponent({
|
||||
active,
|
||||
onPressAdd,
|
||||
onSelectFieldType,
|
||||
ctaLabel,
|
||||
fieldTypeLabels,
|
||||
className,
|
||||
}: AddCustomFieldViewProps) {
|
||||
const shellStyle = {
|
||||
minHeight: ADD_CUSTOM_FIELD_SHELL_MIN_H_PX,
|
||||
} as const;
|
||||
|
||||
if (!active) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onPressAdd}
|
||||
style={shellStyle}
|
||||
className={`flex w-full shrink-0 cursor-pointer items-center justify-center rounded-[var(--measures-radius-medium,8px)] bg-[var(--color-surface-default-secondary)] px-6 py-12 font-inter text-[16px] font-medium leading-5 text-[var(--color-content-default-primary)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-invert-primary)] ${className ?? ""}`.trim()}
|
||||
>
|
||||
<span className="flex items-center gap-[var(--spacing-scale-006)] rounded-full px-4 py-3">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden
|
||||
>
|
||||
<path
|
||||
d="M12 5v14M5 12h14"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
{ctaLabel}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const expandedShellClasses = ["flex w-full shrink-0 flex-col", className ?? ""]
|
||||
.join(" ")
|
||||
.trim();
|
||||
|
||||
return (
|
||||
<div className={expandedShellClasses} style={shellStyle}>
|
||||
<div className="flex w-full flex-col gap-3 rounded-[var(--measures-radius-medium,8px)] bg-[var(--color-surface-default-secondary)] p-4">
|
||||
<div className="flex w-full flex-row flex-nowrap justify-center gap-3 overflow-x-auto max-sm:justify-start">
|
||||
<FieldTypeButton
|
||||
type="text"
|
||||
label={fieldTypeLabels.text}
|
||||
onSelect={onSelectFieldType}
|
||||
/>
|
||||
<FieldTypeButton
|
||||
type="badges"
|
||||
label={fieldTypeLabels.badges}
|
||||
onSelect={onSelectFieldType}
|
||||
/>
|
||||
<FieldTypeButton
|
||||
type="upload"
|
||||
label={fieldTypeLabels.upload}
|
||||
onSelect={onSelectFieldType}
|
||||
/>
|
||||
<FieldTypeButton
|
||||
type="proportion"
|
||||
label={fieldTypeLabels.proportion}
|
||||
onSelect={onSelectFieldType}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const AddCustomFieldView = memo(AddCustomFieldViewComponent);
|
||||
AddCustomFieldView.displayName = "AddCustomFieldView";
|
||||
@@ -0,0 +1,5 @@
|
||||
export { default } from "./AddCustomField.container";
|
||||
export type {
|
||||
AddCustomFieldProps,
|
||||
AddCustomFieldType,
|
||||
} from "./AddCustomField.types";
|
||||
@@ -27,6 +27,7 @@ const CreateContainer = memo<CreateProps>(
|
||||
ariaLabel,
|
||||
ariaLabelledBy,
|
||||
backdropVariant = "default",
|
||||
stepper,
|
||||
}) => {
|
||||
const createRef = useRef<HTMLDivElement>(null);
|
||||
const overlayRef = useRef<HTMLDivElement>(null);
|
||||
@@ -58,6 +59,7 @@ const CreateContainer = memo<CreateProps>(
|
||||
createRef={createRef}
|
||||
overlayRef={overlayRef}
|
||||
backdropVariant={backdropVariant}
|
||||
stepper={stepper}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -35,6 +35,8 @@ export interface CreateProps {
|
||||
* @default "default"
|
||||
*/
|
||||
backdropVariant?: CreateModalBackdropVariant;
|
||||
/** Passed through to ModalFooter; set explicitly when step visibility must not infer from steps alone. */
|
||||
stepper?: boolean;
|
||||
}
|
||||
|
||||
export interface CreateViewProps {
|
||||
@@ -60,4 +62,5 @@ export interface CreateViewProps {
|
||||
createRef: RefObject<HTMLDivElement | null>;
|
||||
overlayRef: RefObject<HTMLDivElement | null>;
|
||||
backdropVariant: CreateModalBackdropVariant;
|
||||
stepper?: boolean;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ export function CreateView({
|
||||
createRef,
|
||||
overlayRef,
|
||||
backdropVariant,
|
||||
stepper,
|
||||
}: CreateViewProps) {
|
||||
return (
|
||||
<CreateModalFrameView
|
||||
@@ -70,6 +71,7 @@ export function CreateView({
|
||||
nextButtonDisabled={nextButtonDisabled}
|
||||
currentStep={currentStep}
|
||||
totalSteps={totalSteps}
|
||||
stepper={stepper}
|
||||
footerContent={footerContent}
|
||||
/>
|
||||
</CreateModalFrameView>
|
||||
|
||||
Reference in New Issue
Block a user