diff --git a/packages/cli/assets/templates/pages/detail-page/page.tsx b/packages/cli/assets/templates/pages/detail-page/page.tsx index e94d4531f73..91d20d5d692 100644 --- a/packages/cli/assets/templates/pages/detail-page/page.tsx +++ b/packages/cli/assets/templates/pages/detail-page/page.tsx @@ -44,15 +44,12 @@ import { import type {CSSProperties} from 'react'; // The only custom CSS in this template is small optical-alignment negative -// margins: the tab row's block dock has no prop (see tabsRow) and List has no -// "bleed to container edge" prop (#2626). Everything else uses props. Plain -// inline styles — no StyleX compiler required. +// margins: LayoutHeader/TabList have no edge-dock prop (#2622) and List +// has no "bleed to container edge" prop (#2626). Everything else uses props. +// Plain inline styles — no StyleX compiler required. -// Dock the tab bar on the header's bottom edge so the active-tab underline -// meets the header divider. TabList's `isFullBleed` handles the inline bleed; -// the block-end dock is still hand-tuned because the tab row sits inside -// nested flex wrappers (StackItem > HStack), where no component prop can -// reach the header's padding edge (a LayoutHeader docking slot would). +// Bleed the tab bar to the header's content edges so the active-tab underline +// meets the header divider. No edge-dock prop on TabList (#2622). const tabsRow: CSSProperties = { marginBottom: -16, marginTop: 12, diff --git a/packages/core/src/TabList/Tab.tsx b/packages/core/src/TabList/Tab.tsx index dfa5fc593b0..eeccdfe69e3 100644 --- a/packages/core/src/TabList/Tab.tsx +++ b/packages/core/src/TabList/Tab.tsx @@ -105,7 +105,18 @@ const styles = stylex.create({ alignItems: 'center', justifyContent: 'center', gap: spacingVars['--spacing-1'], - paddingInline: spacingVars['--spacing-3'], + // The edge stop of a full-bleed strip pads back in by the container's + // padding, so its label still lands on the container's content inset. + '--_tab-inset-start': { + default: spacingVars['--spacing-3'], + ':first-child': `var(--_tab-edge-inset-start, ${spacingVars['--spacing-3']})`, + }, + '--_tab-inset-end': { + default: spacingVars['--spacing-3'], + ':last-child': `var(--_tab-edge-inset-end, ${spacingVars['--spacing-3']})`, + }, + paddingInlineStart: 'var(--_tab-inset-start)', + paddingInlineEnd: 'var(--_tab-inset-end)', backgroundColor: 'transparent', borderWidth: 0, borderStyle: 'none', @@ -153,8 +164,8 @@ const styles = stylex.create({ // with a bottom divider — that ancestor sets `--_tab-indicator-bottom` // to drop the indicator onto the rail beneath the reserved gap. bottom: 'var(--_tab-indicator-bottom, -1px)', - insetInlineStart: spacingVars['--spacing-3'], - insetInlineEnd: spacingVars['--spacing-3'], + insetInlineStart: `var(--_tab-inset-start, ${spacingVars['--spacing-3']})`, + insetInlineEnd: `var(--_tab-inset-end, ${spacingVars['--spacing-3']})`, height: '2px', borderRadius: radiusVars['--radius-full'], pointerEvents: 'none', diff --git a/packages/core/src/TabList/TabList.tsx b/packages/core/src/TabList/TabList.tsx index ab2d927803c..3cf93bb2b64 100644 --- a/packages/core/src/TabList/TabList.tsx +++ b/packages/core/src/TabList/TabList.tsx @@ -113,18 +113,18 @@ export interface TabListProps extends Omit, 'onChange'> { */ hasDivider?: boolean; /** - * Makes the tab strip escape its parent's container padding, extending it - * out to the container's content edges. Reads the `--container-padding-*` - * custom properties that padded Layout containers (LayoutHeader, Card, - * Section, LayoutContent, ...) set and cancels them with negative margins. - * Use it to stretch a tab bar to a header's content edges so a - * `hasDivider` underline spans the full content width, replacing the - * negative-margin CSS that case otherwise requires. + * Bleeds the strip out to its container's content edges. * - * Matches Divider's `isFullBleed`: only the inline (start/end) edges - * bleed. Block-edge docking stays with the surrounding layout — the - * padded container's padding can't be reliably detected from here, so - * cancelling it block-wise would pull the strip into its siblings. + * Two things move, and both are computed from the `--container-padding-*` + * custom properties a padded Layout container publishes. The strip's box is + * pulled out through the container's inline padding, so a `hasDivider` + * underline — or the header's own divider, when the strip is docked on it — + * spans the full content width. The first and last stop then pad that same + * amount back in, so their labels still land on the container's content + * inset rather than 4px short of it. + * + * Outside a padded container the properties are unset and the fallbacks + * leave the strip exactly as it was. * @default false */ isFullBleed?: boolean; @@ -232,21 +232,21 @@ const styles = stylex.create({ paddingBlockEnd: spacingVars['--spacing-1'], '--_tab-indicator-bottom': `calc(-1 * (${spacingVars['--spacing-1']} + ${borderVars['--border-width']}))`, }, - // Cancel the nearest padded Layout container's inline padding so the strip - // reaches its content edges. Same `--container-padding-*` mechanism as - // Divider's horizontal `isFullBleed`, and like it inline-only: the - // `--container-padding-block-*` vars inherit through any wrapper, but - // first/last-child checks only see the strip's own parent, so a block-wise - // cancel would fire whenever the strip is alone in a wrapper and pull it - // into its siblings. The one addition over Divider is `maxWidth: 'none'`, - // needed because the `nav` base sets `maxWidth: '100%'`, which would - // otherwise clamp the widened box back inside the container's padding. fullBleed: { + // Half one: the box. Cancel the container's inline padding so the strip + // reaches its content edges. Same spelling as Divider's `isFullBleed`. marginInlineStart: 'calc(-1 * var(--container-padding-inline-start, 0px))', marginInlineEnd: 'calc(-1 * var(--container-padding-inline-end, 0px))', + // The `nav` base clamps to 100%, which would pull the widened box back. maxWidth: 'none', width: 'calc(100% + var(--container-padding-inline-start, 0px) + var(--container-padding-inline-end, 0px))', + // Half two: the content. Publish the amount the edge stops pad back in. + // `max(…, --spacing-2)` is Table's `containerEdgeStyles` clamp: a stop + // never insets less than 8px, and an unpadded container (the properties + // are unset) falls through to the tab's own inset, changing nothing. + '--_tab-edge-inset-start': `max(var(--container-padding-inline-start, ${spacingVars['--spacing-3']}), ${spacingVars['--spacing-2']})`, + '--_tab-edge-inset-end': `max(var(--container-padding-inline-end, ${spacingVars['--spacing-3']}), ${spacingVars['--spacing-2']})`, }, strip: { display: 'flex', diff --git a/packages/core/src/TabList/TabMenu.tsx b/packages/core/src/TabList/TabMenu.tsx index e2cb390a855..6e2dd4851ef 100644 --- a/packages/core/src/TabList/TabMenu.tsx +++ b/packages/core/src/TabList/TabMenu.tsx @@ -78,7 +78,18 @@ const styles = stylex.create({ alignItems: 'center', justifyContent: 'center', gap: spacingVars['--spacing-1'], - paddingInline: spacingVars['--spacing-3'], + // Mirrors Tab: the edge stop of a full-bleed strip pads back in by the + // container's padding. + '--_tab-inset-start': { + default: spacingVars['--spacing-3'], + ':first-child': `var(--_tab-edge-inset-start, ${spacingVars['--spacing-3']})`, + }, + '--_tab-inset-end': { + default: spacingVars['--spacing-3'], + ':last-child': `var(--_tab-edge-inset-end, ${spacingVars['--spacing-3']})`, + }, + paddingInlineStart: 'var(--_tab-inset-start)', + paddingInlineEnd: 'var(--_tab-inset-end)', backgroundColor: 'transparent', borderWidth: 0, borderStyle: 'none', @@ -124,8 +135,8 @@ const styles = stylex.create({ // drops onto the divider rail when an ancestor (TabList `hasDivider` or a // Toolbar with a bottom divider) sets `--_tab-indicator-bottom`. bottom: 'var(--_tab-indicator-bottom, -1px)', - insetInlineStart: spacingVars['--spacing-3'], - insetInlineEnd: spacingVars['--spacing-3'], + insetInlineStart: `var(--_tab-inset-start, ${spacingVars['--spacing-3']})`, + insetInlineEnd: `var(--_tab-inset-end, ${spacingVars['--spacing-3']})`, height: '2px', borderRadius: radiusVars['--radius-full'], pointerEvents: 'none',