diff --git a/src/lib/spacing.tsx b/src/lib/spacing.tsx
index 780d9a9472..3af60cdc1a 100644
--- a/src/lib/spacing.tsx
+++ b/src/lib/spacing.tsx
@@ -1,5 +1,5 @@
import { Children, HTMLAttributes, HTMLProps, ReactNode } from 'react';
-import styled from 'styled-components';
+import styled, { css } from 'styled-components';
import { Box, BoxComponentProps } from './components/box/Box';
export const spacing = {
@@ -33,39 +33,89 @@ export const spacing = {
f40: '40px',
};
-const HSeparator = styled.div`
- background: ${(props) => props.theme.border};
+// The two separator treatments. A row Stack draws a full-height rule between
+// its children; a column Stack draws a short dash. They are different shapes,
+// not one shape on two axes, so a single element cannot serve both by CSS alone
+// — which is why `stackBelow` below restyles the element rather than relying on
+// the flip.
+const ruleSeparator = css`
width: 1px;
align-self: stretch;
flex-shrink: 0;
- margin: ${spacing.r12} 0px;
+ margin: ${spacing.r12} 0;
`;
-const VSeparator = styled.div`
- height: 1px;
+const dashSeparator = css`
width: ${spacing.r24};
+ height: 1px;
+ align-self: auto;
+ margin: 0;
+`;
+
+// One element for both directions. `Stack` used to pick between two components
+// at render time, which meant a CSS direction change left the separators
+// pointing the wrong way — CSS can restyle an element but cannot swap which one
+// React rendered.
+const Separator = styled.div<{
+ $direction: 'vertical' | 'horizontal';
+ $stackBelow?: number;
+}>`
background: ${(props) => props.theme.border};
+ ${(props) =>
+ props.$direction === 'horizontal' ? ruleSeparator : dashSeparator}
+
+ ${(props) =>
+ props.$direction === 'horizontal' &&
+ props.$stackBelow !== undefined &&
+ css`
+ @container responsive (max-width: ${props.$stackBelow}px) {
+ ${dashSeparator}
+ }
+ `}
`;
-const Separator = ({ type }: { type?: 'vertical' | 'horizontal' }) => {
- return (
- <>
- {type === 'horizontal' && }
- {type === 'vertical' && }
- >
- );
-};
+// Layout lives here rather than on `Box`'s props so the container query below
+// isn't fighting styled-system output for specificity.
+const StackBox = styled(Box)<{
+ $direction: 'vertical' | 'horizontal';
+ $stackBelow?: number;
+}>`
+ display: flex;
+ flex-direction: ${(props) =>
+ props.$direction === 'horizontal' ? 'row' : 'column'};
+ align-items: ${(props) =>
+ props.$direction === 'horizontal' ? 'center' : 'normal'};
+
+ ${(props) =>
+ props.$direction === 'horizontal' &&
+ props.$stackBelow !== undefined &&
+ css`
+ @container responsive (max-width: ${props.$stackBelow}px) {
+ flex-direction: column;
+ align-items: normal;
+ }
+ `}
+`;
export const Stack = ({
gap,
direction,
withSeparators,
+ stackBelow,
children,
...rest
}: {
gap?: keyof typeof spacing;
direction?: 'vertical' | 'horizontal';
withSeparators?: boolean;
+ /**
+ * Below this container width (px) a horizontal Stack becomes vertical, and
+ * its separators become the vertical treatment along with it. Requires an
+ * ancestor that establishes the `responsive` container — ``.
+ * Without one the query never matches and the Stack stays horizontal.
+ * Ignored when `direction="vertical"`.
+ */
+ stackBelow?: number;
children: ReactNode[];
container?: boolean;
} & HTMLAttributes) => {
@@ -75,10 +125,9 @@ export const Stack = ({
const numberOfChildren = Children.count(children);
return (
-
@@ -87,12 +136,12 @@ export const Stack = ({
<>
{node}
{withSeparators && nodeIndex + 1 !== numberOfChildren && (
-
+
)}
>
);
})}
-
+
);
};
diff --git a/stories/spacing.stories.tsx b/stories/spacing.stories.tsx
index 16e1ab04df..38cdd0bcca 100644
--- a/stories/spacing.stories.tsx
+++ b/stories/spacing.stories.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { useTheme } from 'styled-components';
+import styled, { useTheme } from 'styled-components';
import { Icon } from '../src/lib/components/icon/Icon.component';
import {
EmphaseText,
@@ -113,6 +113,64 @@ export const StackStory = {
},
};
+// `stackBelow` queries the `responsive` container, so an ancestor has to
+// establish one. `` does this in app code.
+const ResizableContainer = styled.div`
+ container-type: inline-size;
+ container-name: responsive;
+ resize: horizontal;
+ overflow: auto;
+ min-width: 240px;
+ max-width: 100%;
+ width: 700px;
+ background: ${(props) => props.theme.backgroundLevel2};
+ padding: ${spacing.r16};
+`;
+
+export const StackBelowStory = {
+ name: 'stackBelow — direction follows the container width',
+ render: ({}) => (
+ <>
+ Drag the bottom-right handle below 500px
+
+ One prop. The Stack flips from row to column in CSS only — no re-render
+ — and the separators switch from the row treatment (a full-height rule)
+ to the column treatment (a short dash) along with it.
+
+
+
+
+
+
+ 12
+ Accounts
+
+
+
+
+
+ 148
+ Buckets
+
+
+
+
+
+ 3
+ Endpoints
+
+
+
+
+ >
+ ),
+};
+
export const WrapStory = {
render: ({}) => {
const theme = useTheme();