improvement(spacing): let Stack flip direction from CSS via stackBelow (CUI-38) - #1182
Conversation
Hello jeanmarcmilletscality,My role is to assist you with the merge of this Available options
Available commands
Status report is not available. |
Waiting for approvalThe following approvals are needed before I can proceed with the merge:
Peer approvals must include at least 1 approval from the following list: |
2dfb90d to
f5e87ef
Compare
f5e87ef to
6010197
Compare
Waiting for approvalThe following approvals are needed before I can proceed with the merge:
Peer approvals must include at least 1 approval from the following list: |
…w (CUI-38)
Stack picked its separator element from the direction prop at render time,
injecting HSeparator (a vertical rule) or VSeparator (a 24px horizontal dash).
A CSS flex-direction flip — how container-query-driven responsive layouts work —
left the separators pointing the wrong way, blocking any consuming
application that needs a row to collapse into a column as its container
narrows.
An earlier revision made the row separator self-orienting, so that a CSS flip
turned it into a full-width rule. Design rejected that: a column Stack must keep
its short dash. The two treatments are different shapes rather than one shape on
two axes, and CSS cannot query a parent's flex-direction, so the component has
to emit the query itself.
Both treatments now live in one element as two css blocks, and Stack gains
stackBelow: below that container width a horizontal Stack flips its direction,
its align-items and its separators together. Consumers set one number instead of
hand-writing a container query and fighting styled-system for specificity.
This follows the convention already used by Button (iconOnly={number}) and Form
(STACK_BELOW): a numeric breakpoint the component turns into an
@container responsive query. Without an ancestor establishing that container the
query never matches and the Stack stays horizontal, the same graceful no-op as
iconOnly.
Layout moved off Box's styled-system props onto a styled(Box) wrapper so the
query does not have to out-specify them.
Verified by rendering old and new Stack side by side and diffing the computed
declarations per element: in both default directions the container and separator
are identical, except that the column separator now also states align-self: auto
and margin: 0 — the initial values it must restore when the query swaps
treatments. The separators' text nodes are dropped; both treatments set
explicit dimensions, so they render the same without them.
stackBelow is opt-in, so no existing call site changes. No public API removal:
Separator was never exported.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
6010197 to
131de48
Compare
Waiting for approvalThe following approvals are needed before I can proceed with the merge:
Peer approvals must include at least 1 approval from the following list: |
Waiting for approvalThe following approvals are needed before I can proceed with the merge:
Peer approvals must include at least 1 approval from the following list: |
|
/approve |
In the queueThe changeset has received all authorizations and has been added to the The changeset will be merged in:
There is no action required on your side. You will be notified here once IMPORTANT Please do not attempt to modify this pull request.
If you need this pull request to be removed from the queue, please contact a The following options are set: approve |
|
I have successfully merged the changeset of this pull request
Please check the status of the associated issue CUI-38. Goodbye jeanmarcmilletscality. |
TL;DR —
StackgainsstackBelow={px}: below that container width a horizontal Stack becomes vertical, and its separators switch to the vertical treatment along with it. Nothing renders differently today — the prop is opt-in.Context / Why
Stackpicks its separator element from thedirectionprop, at render time. A CSSflex-directionflip — which is how container-query-driven responsive layouts work — leaves the separators pointing the wrong way. A consuming application hits this the moment it needs a row of columns to collapse into a stack as its container narrows — the row cannot flip, so the content clips silently instead. CUI-38🧩 Approach
This PR replaces an earlier revision that made the row separator self-orienting. That version used
flex-basisfor the main axis andalign-selffor the cross axis, so a CSS flip turned the rule into a full-width one. Design rejected it: a columnStackmust keep its short dash. The two treatments are different shapes, not one shape on two axes — and CSS cannot query a parent'sflex-direction— so the component has to emit the query itself.Both treatments now live in one element as two
cssblocks, andStackgainsstackBelow:One element is what makes this possible at all: CSS can restyle an element, but it cannot swap which element React rendered.
This follows a convention the library already has — a numeric breakpoint the component turns into an
@container responsivequery:ButtoniconOnly={number}FormSTACK_BELOWStackstackBelow={number}(new)Without an ancestor establishing that container (
<Box container>), the query never matches and theStackstays horizontal — the same graceful no-op asiconOnly.Layout moved off
Box's styled-system props onto astyled(Box)wrapper so the container query doesn't have to out-specify them. This is what removes the&&specificity hack a consumer previously needed.Verified no-op in both default directions
Rendered the old and new
Stackside by side withServerStyleSheet, then diffed the computed declarations per element (not the raw stylesheet, which differs only in class bookkeeping and rule splitting):horizontalverticalalign-self: autoandmargin: 0addedThose two additions are the CSS initial values for a flex item, so the computed style is unchanged. They exist only so the shared
dashSeparatorblock resets whatruleSeparatorsets when the container query swaps treatments.The
text nodes are dropped from both separators. Each treatment sets explicit dimensions, so neither depends on content for its size.Why the vertical dash had to stay — beyond design's ruling, a full-width rule in a page
Formduplicates the header's ownborder-bottom: 1px solid theme.border(Form.component.tsx:102): same weight, same colour, near-same width, so a section separator becomes indistinguishable from the form title's rule.📷 Screenshots
🔧 Usage
No existing
Stackusage changes. What changes is how a consumer opts into a responsive direction — before, they hand-wrote the query and foughtBoxfor specificity:Before — what the earlier revision of this PR required:
After — from the story added in this PR:
Two notes for consumers:
responsivecontainer —<Box container>. Without one the query never matches and the Stack stays horizontal.stackBelowis ignored whendirection="vertical". Flipping a vertical Stack to a row in CSS is still unsupported — pre-existing behaviour, unchanged here.🔍 Review focus
src/lib/spacing.tsx › StackBox— layout (display,flex-direction,align-items) moved offBox's styled-system props onto astyled(Box)wrapper. This is the change most able to affect existing call sites: everyStackin every repo now gets its layout from a different class. The computed-declaration diff above is the evidence it's a no-op;gapstill comes fromBox, so the two classes now split what one used to emit.src/lib/spacing.tsx › Separator—ruleSeparator/dashSeparatormust stay mutually resetting.dashSeparatorrestoresalign-selfandmarginprecisely becauseruleSeparatorsets them; dropping either reset silently breaks the flipped state, and container queries don't evaluate in jsdom so no unit test will catch it.src/lib/spacing.tsx › Stack—stackBelowis additive andSeparatorwas never exported, so no public API is removed.🧪 How to test
npm run storybook→ Components/Styling/Spacing Utils › stackBelow — direction follows the container width.development/1.0.page-form,tab-form,form-with-accordion) — section separators should be unchanged short dashes, still clearly subordinate to the form title's rule.🚧 Follow-up
@scality/core-uiversion bump once this is released; the integration details live in the consuming application's own ticket.Stackstill can't be flipped to a row in CSS. No consumer needs it today. Not yet ticketed.Wrapis deliberately untouched — it does not wrap despite its name, deferred pending a usage audit. Not yet ticketed.🔗 References
IconWrapperdeforming into an ellipse), merged; released in 0.228.0.What changed
src/lib/spacing.tsx—HSeparator/VSeparator/Separator({ type })collapse into oneSeparatorstyled component carrying both treatments asruleSeparatoranddashSeparatorcssblocks. A newStackBox = styled(Box)holds the layout so the container query doesn't fight styled-system for specificity.StackgainsstackBelow?: numberand passes it, withdirection, to both as transient props (verified not to reach the DOM).stories/spacing.stories.tsx— replaces the earlierContainerQueryDirectionFlipstory withStackBelowStory, which uses the prop instead of a hand-written query. Its container setscontainer-name: responsiveexplicitly, documenting the requirement.Deliberately not in this PR:
Wrap(out of scope per CUI-38), any restyle of existing vertical separators, and anyFormchange.tsc --noEmitclean,npm run buildclean,npm run lintclean, suite green.