diff --git a/web/packages/design/src/Flex/Flex.tsx b/web/packages/design/src/Flex/Flex.tsx index 0d7aa1a52bfab..9b48df5746b95 100644 --- a/web/packages/design/src/Flex/Flex.tsx +++ b/web/packages/design/src/Flex/Flex.tsx @@ -62,3 +62,45 @@ const Flex = styled(Box)` Flex.displayName = 'Flex'; export default Flex; + +/** + * Stack is a variant of Flex designed to distribute elements in a vertical space with consistent + * spacing using the gap property. If no gap is specified, it defaults to 1. + * + * It's possible to "split the stack" by setting `margin-top: auto;` on a specific child. That child + * and all children below it will be aligned to the bottom of the stack. + * + * Inspired by https://every-layout.dev/layouts/stack/. It follows the approach of styling the + * context, not the individual elements, to achieve desired spacing. + * + * @example + * + * + * + * + * + * + * + * + * + * + * + * + */ +export const Stack = styled(Flex).attrs({ + flexDirection: 'column', +})` + // Prevents children from shrinking, within a stack we pretty much never want that to happen. + // Individual children can override this. + & > * { + flex-shrink: 0; + } +`; +Stack.defaultProps = { + gap: 1, + // align-items: flex-start lets children keep their original size. Otherwise elements like buttons + // would occupy all available horizontal space instead of the minimal amount of space they need. + // + // This is set as a default prop, as in some cases it might be necessary to override align-items. + alignItems: 'flex-start', +}; diff --git a/web/packages/design/src/Flex/Stack.story.tsx b/web/packages/design/src/Flex/Stack.story.tsx new file mode 100644 index 0000000000000..64ff25584ee7c --- /dev/null +++ b/web/packages/design/src/Flex/Stack.story.tsx @@ -0,0 +1,70 @@ +/** + * Teleport + * Copyright (C) 2025 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import styled from 'styled-components'; + +import Box from 'design/Box'; +import { ButtonPrimary } from 'design/Button'; +import { P } from 'design/Text/Text'; + +import { Stack } from './Flex'; + +export default { + title: 'Design/Flex/Stack', +}; + +export const Basic = () => ( + + + + + {/* If no gap prop is given, a default gap of 1 is used. */} + + + Foo + + + + Bar + + + + Baz + + + + + +); + +export const MarginAuto = () => ( + +

+ margin-top: auto can be used to automatically align elements + after a certain child to the end of the stack. +

+ + + + + +
+); + +const Square = styled(Box).attrs({ width: '150px', height: '150px' })``; +const SmallSquare = styled(Box).attrs({ width: '50px', height: '50px' })``; diff --git a/web/packages/design/src/Flex/index.ts b/web/packages/design/src/Flex/index.ts index 12be5512d0a71..101ad1aad5492 100644 --- a/web/packages/design/src/Flex/index.ts +++ b/web/packages/design/src/Flex/index.ts @@ -18,6 +18,6 @@ import Flex from './Flex'; -export { type FlexProps } from './Flex'; +export { type FlexProps, Stack } from './Flex'; export default Flex; diff --git a/web/packages/design/src/index.ts b/web/packages/design/src/index.ts index 6428a7ac4cc75..23cc3f2236e4c 100644 --- a/web/packages/design/src/index.ts +++ b/web/packages/design/src/index.ts @@ -111,3 +111,4 @@ export { }; export type { TextAreaProps } from './TextArea'; export * from './keyframes'; +export { Stack } from './Flex';