Skip to content

Commit 51a35fa

Browse files
Merge pull request #70 from jeffreylauwers/feature/17-stack-layout
feat(Stack): Stack layout component met space-row varianten (#17)
2 parents e657179 + ea7d6b9 commit 51a35fa

10 files changed

Lines changed: 388 additions & 0 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* Stack Layout Component
3+
* Applies consistent vertical spacing between direct child elements using flexbox + gap.
4+
*
5+
* Usage:
6+
* <!-- Default (md = 8px) -->
7+
* <div class="dsn-stack">
8+
* <div>...</div>
9+
* <div>...</div>
10+
* </div>
11+
*
12+
* <!-- Explicit space variant -->
13+
* <div class="dsn-stack dsn-stack--space-lg">
14+
* <div>...</div>
15+
* <div>...</div>
16+
* </div>
17+
*/
18+
19+
.dsn-stack {
20+
display: flex;
21+
flex-direction: column;
22+
gap: var(--dsn-stack-space, var(--dsn-space-row-md));
23+
}
24+
25+
/* Space variant modifiers */
26+
.dsn-stack--space-sm {
27+
--dsn-stack-space: var(--dsn-space-row-sm);
28+
}
29+
.dsn-stack--space-md {
30+
--dsn-stack-space: var(--dsn-space-row-md);
31+
}
32+
.dsn-stack--space-lg {
33+
--dsn-stack-space: var(--dsn-space-row-lg);
34+
}
35+
.dsn-stack--space-xl {
36+
--dsn-stack-space: var(--dsn-space-row-xl);
37+
}
38+
.dsn-stack--space-2xl {
39+
--dsn-stack-space: var(--dsn-space-row-2xl);
40+
}
41+
.dsn-stack--space-3xl {
42+
--dsn-stack-space: var(--dsn-space-row-3xl);
43+
}
44+
.dsn-stack--space-4xl {
45+
--dsn-stack-space: var(--dsn-space-row-4xl);
46+
}
47+
.dsn-stack--space-5xl {
48+
--dsn-stack-space: var(--dsn-space-row-5xl);
49+
}
50+
.dsn-stack--space-6xl {
51+
--dsn-stack-space: var(--dsn-space-row-6xl);
52+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* Stack Component Styles for React
3+
* Re-exports the base Stack styles from components-html
4+
*/
5+
6+
@import '../../../components-html/src/stack/stack.css';
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { render, screen } from '@testing-library/react';
3+
import { createRef } from 'react';
4+
import { Stack } from './Stack';
5+
6+
describe('Stack', () => {
7+
it('renders children', () => {
8+
render(
9+
<Stack>
10+
<div>Item 1</div>
11+
<div>Item 2</div>
12+
</Stack>
13+
);
14+
expect(screen.getByText('Item 1')).toBeInTheDocument();
15+
expect(screen.getByText('Item 2')).toBeInTheDocument();
16+
});
17+
18+
it('renders as a <div> element', () => {
19+
const { container } = render(<Stack />);
20+
expect(container.firstChild?.nodeName).toBe('DIV');
21+
});
22+
23+
it('always has base dsn-stack class', () => {
24+
const { container } = render(<Stack />);
25+
expect(container.firstChild).toHaveClass('dsn-stack');
26+
});
27+
28+
it('does not add space modifier class for default (md)', () => {
29+
const { container } = render(<Stack />);
30+
expect(container.firstChild).not.toHaveClass('dsn-stack--space-md');
31+
});
32+
33+
it('does not add space modifier class for explicit md', () => {
34+
const { container } = render(<Stack space="md" />);
35+
expect(container.firstChild).not.toHaveClass('dsn-stack--space-md');
36+
});
37+
38+
it.each(['sm', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl', '6xl'] as const)(
39+
'applies space modifier class for %s',
40+
(space) => {
41+
const { container } = render(<Stack space={space} />);
42+
expect(container.firstChild).toHaveClass(`dsn-stack--space-${space}`);
43+
}
44+
);
45+
46+
it('applies custom className', () => {
47+
const { container } = render(<Stack className="custom" />);
48+
expect(container.firstChild).toHaveClass('dsn-stack');
49+
expect(container.firstChild).toHaveClass('custom');
50+
});
51+
52+
it('forwards ref to the div element', () => {
53+
const ref = createRef<HTMLDivElement>();
54+
const { container } = render(<Stack ref={ref} />);
55+
expect(ref.current).toBe(container.firstChild);
56+
});
57+
58+
it('passes additional HTML attributes', () => {
59+
const { container } = render(<Stack data-testid="stack" role="list" />);
60+
expect(container.firstChild).toHaveAttribute('data-testid', 'stack');
61+
expect(container.firstChild).toHaveAttribute('role', 'list');
62+
});
63+
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import React from 'react';
2+
import { classNames } from '@dsn/core';
3+
import './Stack.css';
4+
5+
export type StackSpace =
6+
| 'sm'
7+
| 'md'
8+
| 'lg'
9+
| 'xl'
10+
| '2xl'
11+
| '3xl'
12+
| '4xl'
13+
| '5xl'
14+
| '6xl';
15+
16+
export interface StackProps extends React.HTMLAttributes<HTMLDivElement> {
17+
/**
18+
* Verticale ruimte tussen directe child-elementen
19+
* @default 'md'
20+
*/
21+
space?: StackSpace;
22+
23+
/**
24+
* Child-elementen om verticaal te stapelen
25+
*/
26+
children?: React.ReactNode;
27+
}
28+
29+
/**
30+
* Stack layout component
31+
* Brengt consistente verticale ruimte aan tussen directe child-elementen via flexbox + gap.
32+
*
33+
* @example
34+
* ```tsx
35+
* // Default (md = 8px)
36+
* <Stack>
37+
* <Paragraph>Eerste paragraaf</Paragraph>
38+
* <Paragraph>Tweede paragraaf</Paragraph>
39+
* </Stack>
40+
*
41+
* // Grote ruimte tussen secties
42+
* <Stack space="3xl">
43+
* <section>...</section>
44+
* <section>...</section>
45+
* </Stack>
46+
* ```
47+
*/
48+
export const Stack = React.forwardRef<HTMLDivElement, StackProps>(
49+
({ className, space = 'md', children, ...props }, ref) => {
50+
const classes = classNames(
51+
'dsn-stack',
52+
space !== 'md' && `dsn-stack--space-${space}`,
53+
className
54+
);
55+
56+
return (
57+
<div ref={ref} className={classes} {...props}>
58+
{children}
59+
</div>
60+
);
61+
}
62+
);
63+
64+
Stack.displayName = 'Stack';
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { Stack } from './Stack';
2+
export type { StackProps, StackSpace } from './Stack';

packages/components-react/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
* Import components like: import { Button, TextInput } from '@dsn/components-react'
66
*/
77

8+
// Layout Components
9+
export * from './Stack';
10+
811
// Layout & Typography
912
export * from './Heading';
1013
export * from './Paragraph';

packages/storybook/.storybook/main.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ configureSort({
88
storyOrder: {
99
introduction: null,
1010
foundations: null,
11+
'layout-components': {
12+
'*': { docs: null },
13+
},
1114
components: {
1215
'*': { docs: null },
1316
},
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Stack
2+
3+
Layout primitief dat consistente verticale ruimte aanbrengt tussen gestapelde elementen.
4+
5+
## Doel
6+
7+
Stack past automatisch verticale ruimte toe tussen alle directe child-elementen via `display: flex; flex-direction: column; gap`. Je schrijft nooit zelf `margin` of `padding` voor tussenruimte — de Stack regelt dat.
8+
9+
Negen space-varianten — gebaseerd op de globale `--dsn-space-row-*` tokens — geven je controle over hoe ver de elementen uit elkaar staan.
10+
11+
<!-- VOORBEELD -->
12+
13+
## Use when
14+
15+
- Elementen verticaal stapelen met consistente tussenruimte (kopteksten, paragrafen, formuliervelden, secties).
16+
- De tussenruimte uniform moet zijn voor alle children, zonder uitzonderingen per child.
17+
- Je een layout-laag wilt scheiden van de inhoud zelf.
18+
19+
## Don't use when
20+
21+
- De ruimte tussen elementen sterk verschilt per child — gebruik dan losse `margin-block` per element.
22+
- Je horizontale layout nodig hebt — gebruik flexbox of grid rechtstreeks.
23+
- Slechts één child aanwezig is — Stack voegt dan geen zichtbare ruimte toe.
24+
25+
## Best practices
26+
27+
- **Kies de kleinst passende variant**: voor formuliervelden `sm``md`, voor secties op een pagina `3xl``4xl`, voor grote paginaovergangen `5xl``6xl`.
28+
- **Nest Stack-componenten** voor complexe layouts: een buitenste Stack met `4xl` voor paginasecties, binnenste Stacks met `md` voor formuliervelden.
29+
- **Gebruik Stack niet als vervanging voor semantische HTML**: de Stack rendert altijd als `<div>`. Geef children de juiste semantische elementen mee.
30+
31+
## Accessibility
32+
33+
Stack is een puur visueel layout-hulpmiddel. Het voegt geen ARIA-rollen, labels of andere toegankelijkheidsattributen toe. De semantiek van de pagina zit volledig in de children.
34+
35+
## Design tokens
36+
37+
| Token | Beschrijving |
38+
| --------------------- | ------------------------------------------------------------------------------- |
39+
| `--dsn-stack-space` | Interne CSS custom property — bepaalt de `gap` waarde. Overschrijfbaar via CSS. |
40+
| `--dsn-space-row-sm` | 4px — gebruikt door `.dsn-stack--space-sm` |
41+
| `--dsn-space-row-md` | 8px — standaardwaarde (fallback van `--dsn-stack-space`) |
42+
| `--dsn-space-row-lg` | 12px — gebruikt door `.dsn-stack--space-lg` |
43+
| `--dsn-space-row-xl` | 16px — gebruikt door `.dsn-stack--space-xl` |
44+
| `--dsn-space-row-2xl` | 20px — gebruikt door `.dsn-stack--space-2xl` |
45+
| `--dsn-space-row-3xl` | 24px — gebruikt door `.dsn-stack--space-3xl` |
46+
| `--dsn-space-row-4xl` | 32px — gebruikt door `.dsn-stack--space-4xl` |
47+
| `--dsn-space-row-5xl` | 64px — gebruikt door `.dsn-stack--space-5xl` |
48+
| `--dsn-space-row-6xl` | 160px — gebruikt door `.dsn-stack--space-6xl` |
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Meta, Story, Controls, Markdown } from '@storybook/blocks';
2+
import * as StackStories from './Stack.stories';
3+
import docs from './Stack.docs.md?raw';
4+
import { PreviewFrame, CodeTabs } from './components';
5+
6+
export const [intro, rest] = docs.split('<!-- VOORBEELD -->');
7+
8+
<Meta of={StackStories} />
9+
10+
<Markdown>{intro}</Markdown>
11+
12+
## Voorbeeld
13+
14+
<PreviewFrame>
15+
<Story of={StackStories.Default} />
16+
</PreviewFrame>
17+
18+
<CodeTabs
19+
of={StackStories.Default}
20+
html={`<div class="dsn-stack">
21+
<div>...</div>
22+
<div>...</div>
23+
<div>...</div>
24+
</div>`}
25+
/>
26+
27+
<Controls of={StackStories.Default} />
28+
29+
<Markdown>{rest}</Markdown>

0 commit comments

Comments
 (0)