// Copyright (c) Meta Platforms, Inc. and affiliates.
'use client';
declare const process: {env: {NODE_ENV?: string}};
/**
* @file Stepper.tsx
* @input Uses React, stylex, theme tokens, StepperContext
* @output Exports Stepper component and StepperProps
* @position Core container component; consumed by index.ts
*
* Besides the props it is given, this component tracks the `activeStep` it
* last rendered with and publishes it on the context. Steps need the distance
* and direction of a change to choreograph their connector fill; see the
* CONNECTOR FILL block in Step.tsx. A horizontal Stepper applies public layout
* styling to the frame that contains both the list and compact summary, while
* the semantic `
` remains the measured ref and DOM pass-through target.
*
* SYNC: When modified, update these files to stay in sync:
* - /packages/core/src/Stepper/Stepper.doc.mjs (props table, features, implementation notes)
* - /packages/core/src/Stepper/Stepper.test.tsx (tests for new/changed behavior)
* - /packages/core/src/Stepper/index.ts (exports if types change)
* - /apps/storybook/stories/Stepper.stories.tsx (storybook stories)
* - /packages/cli/assets/templates/blocks/components/Stepper/ (showcase blocks)
*/
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
import * as stylex from "../style-runtime.js";
import "../theme/tokens.stylex.js";
import { spacingVars } from "../theme/tokens.stylex.js";
import { mergeProps, rtlStyles } from "../utils/index.js";
import { observeResize } from "../utils/sharedResizeObserver.js";
import { useMergedRefs } from "../hooks/useMergedRefs.js";
import type { BaseProps } from "../BaseProps.js";
import { themeProps } from "../utils/index.js";
import { Icon } from "../Icon/index.js";
import { IconButton } from "../IconButton/index.js";
import { useTranslator } from "../i18n/index.js";
import { StepperContext, StepperInternalContext, type StepperContextValue, type StepperOrientation, type StepperIndicatorPosition, type StepperInternalContextValue, type StepperRegistrationOptions } from "./StepperContext.js";
/**
* Default width below which a step can no longer hold its own label. Under
* about this much a one-word label starts breaking mid-word and a two-word one
* stacks, which costs more vertical space than the row that replaces all of
* them — and reads worse, because every step pays for text only one of them
* needs now. Consumers can replace it through
* `horizontalOptions.minimumStepWidth` when their labels or layout need a
* different threshold.
*
* Applied per step rather than to the stepper, so where the collapse happens
* follows the count: four steps hold out to 448px, seven need 784px.
*/
const DEFAULT_MIN_STEP_WIDTH = 112;
const DEFAULT_COLLAPSED_VARIANT = 'withLabelAndControls';
export type StepperCollapsedVariant = 'withLabelAndControls' | 'withLabel' | 'hiddenLabel';
export interface StepperHorizontalOptions {
/**
* Minimum width in pixels allocated to each step before a horizontal Stepper
* collapses.
* @default 112
*/
minimumStepWidth: number;
/**
* Presentation used when the horizontal Stepper collapses.
* - `withLabelAndControls`: current-step label and navigation controls.
* - `withLabel`: current-step label without controls.
* - `hiddenLabel`: bare progress track with no compact row.
* `withLabelAndControls` renders its controls only when `onStepClick` is set.
* @default 'withLabelAndControls'
*/
collapsedVariant: StepperCollapsedVariant;
}
export interface StepperProps extends BaseProps {
/** Ref forwarded to the root element */
ref?: React.Ref;
/**
* Zero-based index of the active step.
*/
activeStep: number;
/**
* Step elements to render.
*/
children: ReactNode;
/**
* Layout direction of the stepper.
* @default 'horizontal'
*/
orientation?: StepperOrientation;
/**
* Called when a step is clicked, or when a compact summary control is used.
* Enables non-linear navigation. At compact horizontal widths, individual
* track nodes are presentational; when the collapsed variant includes
* summary controls, those controls skip disabled steps.
*/
onStepClick?: (index: number) => void;
/**
* Accessible label describing the set of steps. Defaults to a translated
* "Progress" when unset.
*/
label?: string;
/**
* Controls density (padding) of all steps.
* @default 'balanced'
*/
density?: 'compact' | 'balanced' | 'spacious';
/**
* Controls where each step's indicator sits relative to the connector track.
* - 'separated': indicator lives in the label row, distinct from the progress
* bar (the original Astryx layout).
* - 'on-track': indicator is slotted into the connector line as a node on the
* track (the on-track indicator design).
* @default 'separated'
*/
indicatorPosition?: StepperIndicatorPosition;
/**
* Options specific to the horizontal layout. `minimumStepWidth` controls the
* per-step collapse threshold; `collapsedVariant` selects a label with
* controls, a label alone, or a bare progress track.
* @default {minimumStepWidth: 112, collapsedVariant: 'withLabelAndControls'}
*/
horizontalOptions?: StepperHorizontalOptions;
}
const styles = {
root: {
"--step-connector-gap": "x5letbq",
k1xSpc: "x78zum5",
kzqmXN: "xh8yej3",
kH6xsr: "x3ct3a4",
kogj98: "x1ghz6dp",
kmVPX3: "x1717udv",
$$css: true as const
},
horizontal: {
kXwgrk: "x1q0g3np",
kGNEyG: "x1cy8zhl",
kOIVth: "xzye2dw",
$$css: true as const
},
vertical: {
kXwgrk: "xdt5ytf",
kOIVth: "xzye2dw",
$$css: true as const
},
horizontalOnTrack: {
kXwgrk: "x1q0g3np",
kGNEyG: "x1cy8zhl",
kOIVth: "xxhr3t",
$$css: true as const
},
verticalOnTrack: {
kXwgrk: "xdt5ytf",
kOIVth: "xxhr3t",
$$css: true as const
},
frame: {
"--step-connector-gap": "x152dxt4",
k1xSpc: "x78zum5",
kXwgrk: "xdt5ytf",
kOIVth: "xxhr3t",
kzqmXN: "xh8yej3",
$$css: true as const
}
};
/**
* A stepper component for multi-step workflows. Displays numbered steps
* with visual indicators for completed, active, and upcoming states.
*
* Each Step child must provide a `step` prop (zero-based index) so it
* can derive its state from the parent's activeStep. The on-track layout
* hides the leading connector on the first step and the trailing connector
* on the last step structurally, from each step's own `- ` position, so
* it works regardless of how the steps are grouped.
*
* Rendered as an ordered list (`
`/`- `) rather than a `nav`
* landmark: a stepper communicates *progress through a sequence*, not a
* set of site navigation links. The active step is marked with
* `aria-current="step"` (handled per-step) and the list carries an
* accessible `label`. This follows the WAI-ARIA pattern for steppers /
* progress sequences and avoids polluting the page's landmark map.
*
* @example
* ```
*
*
*
*
*
* ```
*/
export function Stepper({
activeStep,
children,
orientation = 'horizontal',
onStepClick,
label: labelFromProps,
density = 'balanced',
indicatorPosition = 'separated',
horizontalOptions,
xstyle,
className,
style,
ref,
...rest
}: StepperProps) {
const t = useTranslator();
const label = labelFromProps ?? t('@astryx.stepper.label');
const minimumStepWidth = horizontalOptions?.minimumStepWidth ?? DEFAULT_MIN_STEP_WIDTH;
const collapsedVariant = horizontalOptions?.collapsedVariant ?? DEFAULT_COLLAPSED_VARIANT;
// Dev-mode duplicate step index detection. Steps register on mount and
// deregister on unmount; a Map tracks count per index so we can warn when
// two Steps share the same `step` value (which breaks aria-current).
const stepCountsRef = useRef