Standardize create-flow option steps on one question-and-options layout and Selection sizes.
Method cards, chips, and the right rail now share the two-column shell; last options clear the footer fade, See all stays under the stack, and the left heading no longer jumps when the list expands. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
2fc7fbf818
commit
24a9615c0c
26 files changed
+354
-245
No files matched your search
@@ -9,10 +9,11 @@ const DEFAULT_TOGGLE_LABEL = "See all communication approaches";
|
||||
|
||||
/**
|
||||
* Figma: "Utility / CardStack"; canonical code under `cards/`.
|
||||
* Selectable stack of cards with
|
||||
* Selectable stack of {@link Selection} options (`size` `m` tiles or `l` rows) with
|
||||
* an optional "see all"/"show less" expand toggle.
|
||||
* Expanded default layout matches Flow — Expanded Card Stack (`20768:14820`):
|
||||
* 281×142 CardSelection tiles in two columns.
|
||||
* Create-flow method steps use `layout="singleStack"` (size `l`) in the shared
|
||||
* question-and-options shell. Expanded default layout still matches Flow —
|
||||
* Expanded Card Stack (`20768:14820`): 281×142 size-`m` tiles in two columns.
|
||||
*/
|
||||
const CardStackContainer = memo<CardStackProps>(
|
||||
({
|
||||
|
||||
@@ -43,7 +43,10 @@ export interface CardStackProps {
|
||||
compactDesktopLayout?: "grid" | "flexWrap" | "pyramidFive";
|
||||
/** Optional title/description lockup size (create-flow passes `md`-matched `L`/`M`). Defaults to `L`. */
|
||||
headerLockupSize?: HeaderLockupSizeValue;
|
||||
/** Alignment of the expand/collapse control in `singleStack` layout (Figma right-rail: end). */
|
||||
/**
|
||||
* Alignment of the expand/collapse control in `singleStack` layout.
|
||||
* Figma Flow — Compact Card Stack, Expanded Card Stack, and Right Rail center the control.
|
||||
*/
|
||||
toggleAlignment?: "center" | "end";
|
||||
className?: string;
|
||||
/** Optional “Add” entry (e.g. custom method card wizard). */
|
||||
|
||||
@@ -126,11 +126,15 @@ export function CardStackView({
|
||||
.slice(0, compactRecommendedLimit);
|
||||
})();
|
||||
|
||||
// Single stack: always one column; expand reveals more in same stack (scrollable)
|
||||
// Single stack: always one column; expand reveals more in the same stack.
|
||||
// At lg+ the cards scroll inside this column and the See all / Show less
|
||||
// control stays pinned below (Figma Flow — Right Rail).
|
||||
if (layout === "singleStack") {
|
||||
const displayedCards = expanded ? cards : compactCards;
|
||||
return (
|
||||
<div className={`flex w-full flex-col gap-6 min-w-0 ${className}`}>
|
||||
<div
|
||||
className={`flex w-full min-w-0 flex-col gap-6 lg:h-full lg:max-h-full lg:min-h-0 lg:flex-1 lg:overflow-hidden ${className}`}
|
||||
>
|
||||
<CardStackHeaderLockup
|
||||
title={title}
|
||||
description={description}
|
||||
@@ -138,7 +142,7 @@ export function CardStackView({
|
||||
size={lockupSize}
|
||||
wrapperClassName="min-w-0 shrink-0"
|
||||
/>
|
||||
<div className="flex w-full min-w-0 flex-col gap-2">
|
||||
<div className="flex w-full min-w-0 flex-col gap-2 lg:min-h-0 lg:flex-1 lg:overflow-y-auto scrollbar-design">
|
||||
{displayedCards.map((item) => (
|
||||
<Selection
|
||||
key={item.id}
|
||||
@@ -147,7 +151,7 @@ export function CardStackView({
|
||||
supportText={item.supportText}
|
||||
recommended={item.recommended ?? false}
|
||||
selected={isSelected(item.id)}
|
||||
orientation="vertical"
|
||||
size="l"
|
||||
showInfoIcon={false}
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
/>
|
||||
@@ -158,7 +162,7 @@ export function CardStackView({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleExpand}
|
||||
className={`text-medium-paragraph text-[var(--color-gray-000)] underline hover:opacity-90 focus:outline-none cursor-pointer ${
|
||||
className={`shrink-0 text-medium-paragraph text-[var(--color-gray-000)] underline hover:opacity-90 focus:outline-none cursor-pointer ${
|
||||
toggleAlignment === "end" ? "self-end" : "self-center"
|
||||
}`}
|
||||
>
|
||||
@@ -189,7 +193,7 @@ export function CardStackView({
|
||||
supportText={item.supportText}
|
||||
recommended={item.recommended ?? false}
|
||||
selected={isSelected(item.id)}
|
||||
orientation="horizontal"
|
||||
size="m"
|
||||
showInfoIcon={false}
|
||||
className="min-h-[142px]"
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
@@ -206,7 +210,7 @@ export function CardStackView({
|
||||
supportText={item.supportText}
|
||||
recommended={item.recommended ?? false}
|
||||
selected={isSelected(item.id)}
|
||||
orientation="horizontal"
|
||||
size="m"
|
||||
showInfoIcon={false}
|
||||
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
@@ -230,7 +234,7 @@ export function CardStackView({
|
||||
supportText={item.supportText}
|
||||
recommended={item.recommended ?? false}
|
||||
selected={isSelected(item.id)}
|
||||
orientation="horizontal"
|
||||
size="m"
|
||||
showInfoIcon={false}
|
||||
className="min-h-[142px]"
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
@@ -254,7 +258,7 @@ export function CardStackView({
|
||||
supportText={item.supportText}
|
||||
recommended={item.recommended ?? false}
|
||||
selected={isSelected(item.id)}
|
||||
orientation="horizontal"
|
||||
size="m"
|
||||
showInfoIcon={false}
|
||||
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
@@ -273,7 +277,7 @@ export function CardStackView({
|
||||
supportText={item.supportText}
|
||||
recommended={item.recommended ?? false}
|
||||
selected={isSelected(item.id)}
|
||||
orientation="horizontal"
|
||||
size="m"
|
||||
showInfoIcon={false}
|
||||
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
@@ -292,7 +296,7 @@ export function CardStackView({
|
||||
supportText={item.supportText}
|
||||
recommended={item.recommended ?? false}
|
||||
selected={isSelected(item.id)}
|
||||
orientation="horizontal"
|
||||
size="m"
|
||||
showInfoIcon={false}
|
||||
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
@@ -308,7 +312,7 @@ export function CardStackView({
|
||||
supportText={item.supportText}
|
||||
recommended={item.recommended ?? false}
|
||||
selected={isSelected(item.id)}
|
||||
orientation="horizontal"
|
||||
size="m"
|
||||
showInfoIcon={false}
|
||||
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
@@ -323,7 +327,7 @@ export function CardStackView({
|
||||
supportText={compactCards[4].supportText}
|
||||
recommended={compactCards[4].recommended ?? false}
|
||||
selected={isSelected(compactCards[4].id)}
|
||||
orientation="horizontal"
|
||||
size="m"
|
||||
showInfoIcon={false}
|
||||
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
||||
onClick={() => onCardSelect(compactCards[4].id)}
|
||||
@@ -349,7 +353,7 @@ export function CardStackView({
|
||||
supportText={item.supportText}
|
||||
recommended={item.recommended ?? false}
|
||||
selected={isSelected(item.id)}
|
||||
orientation="horizontal"
|
||||
size="m"
|
||||
showInfoIcon={false}
|
||||
className="min-h-[142px]"
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
@@ -370,7 +374,7 @@ export function CardStackView({
|
||||
supportText={item.supportText}
|
||||
recommended={item.recommended ?? false}
|
||||
selected={isSelected(item.id)}
|
||||
orientation="horizontal"
|
||||
size="m"
|
||||
showInfoIcon={false}
|
||||
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
@@ -384,7 +388,7 @@ export function CardStackView({
|
||||
supportText={compactCards[2].supportText}
|
||||
recommended={compactCards[2].recommended ?? false}
|
||||
selected={isSelected(compactCards[2].id)}
|
||||
orientation="horizontal"
|
||||
size="m"
|
||||
showInfoIcon={false}
|
||||
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
||||
onClick={() => onCardSelect(compactCards[2].id)}
|
||||
@@ -403,7 +407,7 @@ export function CardStackView({
|
||||
supportText={item.supportText}
|
||||
recommended={item.recommended ?? false}
|
||||
selected={isSelected(item.id)}
|
||||
orientation="horizontal"
|
||||
size="m"
|
||||
showInfoIcon={false}
|
||||
className={FIGMA_CARD_SELECTION_TILE_CLASS}
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
@@ -429,7 +433,7 @@ export function CardStackView({
|
||||
supportText={item.supportText}
|
||||
recommended={item.recommended ?? false}
|
||||
selected={isSelected(item.id)}
|
||||
orientation="horizontal"
|
||||
size="m"
|
||||
showInfoIcon={false}
|
||||
className="h-[142px] min-h-[142px] max-h-[142px] w-full max-w-[281px]"
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
@@ -456,7 +460,7 @@ export function CardStackView({
|
||||
supportText={item.supportText}
|
||||
recommended={item.recommended ?? false}
|
||||
selected={isSelected(item.id)}
|
||||
orientation="vertical"
|
||||
size="l"
|
||||
showInfoIcon={false}
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
/>
|
||||
@@ -482,7 +486,7 @@ export function CardStackView({
|
||||
supportText={item.supportText}
|
||||
recommended={item.recommended ?? false}
|
||||
selected={isSelected(item.id)}
|
||||
orientation="horizontal"
|
||||
size="m"
|
||||
showInfoIcon={false}
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
/>
|
||||
|
||||
@@ -2,11 +2,22 @@
|
||||
|
||||
import { memo } from "react";
|
||||
import { SelectionView } from "./Selection.view";
|
||||
import type { SelectionProps } from "./Selection.types";
|
||||
import type { SelectionProps, SelectionSizeValue } from "./Selection.types";
|
||||
|
||||
function resolveSelectionSize(
|
||||
size: SelectionProps["size"],
|
||||
orientation: SelectionProps["orientation"],
|
||||
): SelectionSizeValue {
|
||||
if (size) return size;
|
||||
if (orientation === "vertical") return "l";
|
||||
return "m";
|
||||
}
|
||||
|
||||
/**
|
||||
* Figma: "Card / CardSelection" — stacked tile e.g. `16775:28762` (recommended + label + supportText).
|
||||
* `orientation="horizontal"` selects that vertical stack; `"vertical"` is label + tag on the right (CardStack `singleStack` / right-rail). Helper `?` stays off until tooltip behavior ships.
|
||||
* Size `s` is Control / Chip; `m` is the compact tile (`orientation="horizontal"`); `l` is the
|
||||
* full-width row (`orientation="vertical"`, CardStack `singleStack` / right-rail). Helper `?`
|
||||
* stays off until tooltip behavior ships.
|
||||
*/
|
||||
const SelectionContainer = memo<SelectionProps>(
|
||||
({
|
||||
@@ -14,12 +25,15 @@ const SelectionContainer = memo<SelectionProps>(
|
||||
supportText = "",
|
||||
recommended = false,
|
||||
selected = false,
|
||||
orientation = "horizontal",
|
||||
size: sizeProp,
|
||||
orientation,
|
||||
showInfoIcon = false,
|
||||
id,
|
||||
className = "",
|
||||
onClick,
|
||||
}) => {
|
||||
const size = resolveSelectionSize(sizeProp, orientation);
|
||||
|
||||
const handleClick = () => {
|
||||
if (onClick) onClick();
|
||||
};
|
||||
@@ -37,7 +51,7 @@ const SelectionContainer = memo<SelectionProps>(
|
||||
supportText={supportText}
|
||||
recommended={recommended}
|
||||
selected={selected}
|
||||
orientation={orientation}
|
||||
size={size}
|
||||
showInfoIcon={showInfoIcon}
|
||||
id={id}
|
||||
className={className}
|
||||
|
||||
@@ -1,9 +1,28 @@
|
||||
import type {
|
||||
SelectionOrientationValue,
|
||||
SelectionSizeValue,
|
||||
} from "../../../../lib/propNormalization";
|
||||
|
||||
export type { SelectionOrientationValue, SelectionSizeValue };
|
||||
|
||||
export interface SelectionProps {
|
||||
label: string;
|
||||
supportText?: string;
|
||||
recommended?: boolean;
|
||||
selected?: boolean;
|
||||
orientation: "horizontal" | "vertical";
|
||||
/**
|
||||
* Size of the selectable option:
|
||||
* - `s` — chip (label only)
|
||||
* - `m` — compact tile (label + support text stacked)
|
||||
* - `l` — full-width row (label + support text, tag at the end)
|
||||
*
|
||||
* When omitted, `orientation` maps `horizontal` → `m` and `vertical` → `l`.
|
||||
*/
|
||||
size?: SelectionSizeValue;
|
||||
/**
|
||||
* Legacy layout flag. Prefer `size`. `horizontal` is size `m`; `vertical` is size `l`.
|
||||
*/
|
||||
orientation?: SelectionOrientationValue;
|
||||
/** Off in product until tooltip behavior ships. */
|
||||
showInfoIcon?: boolean;
|
||||
/** Optional id for the root (e.g. `data-card-id` for focus after modal close). */
|
||||
@@ -17,7 +36,7 @@ export interface SelectionViewProps {
|
||||
supportText: string;
|
||||
recommended: boolean;
|
||||
selected: boolean;
|
||||
orientation: "horizontal" | "vertical";
|
||||
size: SelectionSizeValue;
|
||||
showInfoIcon: boolean;
|
||||
id: string | undefined;
|
||||
className: string;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import Chip from "../../controls/Chip";
|
||||
import Tag from "../../utility/Tag";
|
||||
import type { SelectionViewProps } from "./Selection.types";
|
||||
|
||||
@@ -26,34 +27,54 @@ function SelectionTag({
|
||||
return null;
|
||||
}
|
||||
|
||||
function optionAriaLabel(label: string, supportText: string): string {
|
||||
return supportText ? `${label}: ${supportText}` : label;
|
||||
}
|
||||
|
||||
export function SelectionView({
|
||||
label,
|
||||
supportText,
|
||||
recommended,
|
||||
selected,
|
||||
orientation,
|
||||
size,
|
||||
showInfoIcon,
|
||||
id: selectionId,
|
||||
className,
|
||||
onClick,
|
||||
onKeyDown,
|
||||
}: SelectionViewProps) {
|
||||
const ariaLabel = optionAriaLabel(label, supportText);
|
||||
|
||||
if (size === "s") {
|
||||
return (
|
||||
<Chip
|
||||
label={label}
|
||||
state={selected ? "selected" : "unselected"}
|
||||
size="m"
|
||||
ariaLabel={ariaLabel}
|
||||
className={className}
|
||||
onClick={() => onClick()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const borderClass = "border border-[var(--color-border-default-primary)]";
|
||||
const selectedBorder = selected
|
||||
? "outline outline-4 outline-dashed outline-black outline-offset-[-4px]"
|
||||
: "";
|
||||
const cardIdProp = selectionId ? { "data-card-id": selectionId } : {};
|
||||
|
||||
// Figma: "Card / CardSelection" vertical stack — node `16775:28762` (dev).
|
||||
// Prop `orientation="horizontal"` is this stacked layout (historical naming).
|
||||
if (orientation === "horizontal") {
|
||||
if (size === "m") {
|
||||
const baseClasses = `select-none rounded-[var(--measures-radius-200,8px)] bg-[var(--color-gray-000)] px-4 py-3 transition-[border-color,box-shadow,outline] duration-200 cursor-pointer ${borderClass} ${selectedBorder} ${className}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
{...(selectionId ? { "data-card-id": selectionId } : {})}
|
||||
{...cardIdProp}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={supportText ? `${label}: ${supportText}` : label}
|
||||
aria-label={ariaLabel}
|
||||
aria-pressed={selected}
|
||||
data-selection-size="m"
|
||||
className={`${baseClasses} flex min-h-0 w-full flex-col items-start justify-center gap-1`}
|
||||
onClick={onClick}
|
||||
onKeyDown={onKeyDown}
|
||||
@@ -75,10 +96,12 @@ export function SelectionView({
|
||||
|
||||
return (
|
||||
<div
|
||||
{...(selectionId ? { "data-card-id": selectionId } : {})}
|
||||
{...cardIdProp}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={supportText ? `${label}: ${supportText}` : label}
|
||||
aria-label={ariaLabel}
|
||||
aria-pressed={selected}
|
||||
data-selection-size="l"
|
||||
className={`${baseClasses} flex flex-row items-center justify-between gap-4`}
|
||||
onClick={onClick}
|
||||
onKeyDown={onKeyDown}
|
||||
|
||||
Reference in new issue
Block a user