@boostdev/design-system-components 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -1
- package/README.md +42 -1
- package/dist/client.cjs +305 -60
- package/dist/client.css +577 -523
- package/dist/client.d.cts +42 -2
- package/dist/client.d.ts +42 -2
- package/dist/client.js +311 -60
- package/dist/index.cjs +305 -60
- package/dist/index.css +577 -523
- package/dist/index.d.cts +42 -2
- package/dist/index.d.ts +42 -2
- package/dist/index.js +311 -60
- package/dist/native/index.cjs +5 -2
- package/dist/native/index.d.cts +3 -1
- package/dist/native/index.d.ts +3 -1
- package/dist/native/index.js +5 -2
- package/dist/web-components/{chunk-3REOIRDW.js → chunk-N3TN6WCH.js} +26 -1
- package/dist/web-components/index.d.ts +129 -1
- package/dist/web-components/index.js +312 -1
- package/dist/web-components/interaction/bds-button.d.ts +7 -0
- package/dist/web-components/interaction/bds-button.js +1 -1
- package/package.json +1 -1
- package/src/components/interaction/Button/Button.mdx +81 -36
- package/src/components/interaction/Button/Button.module.css +24 -0
- package/src/components/interaction/Button/Button.native.mdx +31 -12
- package/src/components/interaction/Button/Button.native.spec.tsx +20 -0
- package/src/components/interaction/Button/Button.native.stories.tsx +110 -9
- package/src/components/interaction/Button/Button.native.tsx +13 -4
- package/src/components/interaction/Button/Button.spec.tsx +16 -0
- package/src/components/interaction/Button/Button.stories.tsx +134 -16
- package/src/components/interaction/Button/Button.tsx +4 -0
- package/src/components/layout/Grid/Grid.mdx +244 -0
- package/src/components/layout/Grid/Grid.module.css +59 -0
- package/src/components/layout/Grid/Grid.spec.tsx +401 -0
- package/src/components/layout/Grid/Grid.stories.tsx +160 -0
- package/src/components/layout/Grid/Grid.tsx +85 -0
- package/src/components/layout/Grid/GridItem.tsx +150 -0
- package/src/components/layout/Grid/autoSpan.ts +77 -0
- package/src/components/layout/Grid/index.ts +4 -0
- package/src/components/layout/Grid/masonry.ts +134 -0
- package/src/components/layout/IconWrapper/IconWrapper.stories.tsx +46 -14
- package/src/index.ts +2 -0
- package/src/web-components/index.ts +4 -0
- package/src/web-components/interaction/BdsButton.mdx +46 -14
- package/src/web-components/interaction/BdsButton.stories.tsx +171 -19
- package/src/web-components/interaction/bds-button.spec.ts +35 -0
- package/src/web-components/interaction/bds-button.ts +28 -1
- package/src/web-components/layout/BdsGrid.mdx +210 -0
- package/src/web-components/layout/BdsGrid.stories.tsx +209 -0
- package/src/web-components/layout/BdsGridItem.mdx +52 -0
- package/src/web-components/layout/BdsGridItem.stories.tsx +72 -0
- package/src/web-components/layout/bds-grid-item.spec.ts +102 -0
- package/src/web-components/layout/bds-grid-item.ts +177 -0
- package/src/web-components/layout/bds-grid.spec.ts +62 -0
- package/src/web-components/layout/bds-grid.ts +184 -0
|
@@ -1,56 +1,188 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { type ReactNode } from 'react';
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
3
|
import type { ButtonVariant, ButtonSize } from './bds-button';
|
|
4
4
|
import '../index'; // auto-registers all custom elements
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
const CheckIcon = (
|
|
7
|
+
<svg
|
|
8
|
+
slot="icon-start"
|
|
9
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
10
|
+
viewBox="0 0 24 24"
|
|
11
|
+
width="1em"
|
|
12
|
+
height="1em"
|
|
13
|
+
aria-hidden
|
|
14
|
+
fill="currentColor"
|
|
15
|
+
>
|
|
16
|
+
<path d="M12 0a12 12 0 1 0 12 12A12 12 0 0 0 12 0Zm6.93 8.2-6.85 9.29a1 1 0 0 1-1.43.19l-4.89-3.91a1 1 0 0 1-.15-1.41A1 1 0 0 1 7 12.21l4.08 3.26L17.32 7a1 1 0 0 1 1.39-.21 1 1 0 0 1 .22 1.41Z" />
|
|
17
|
+
</svg>
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const ArrowRightStart = <span slot="icon-start" aria-hidden>→</span>;
|
|
21
|
+
const ArrowRightEnd = <span slot="icon-end" aria-hidden>→</span>;
|
|
22
|
+
const ArrowLeftStart = <span slot="icon-start" aria-hidden>←</span>;
|
|
23
|
+
|
|
24
|
+
// Icon for icon-only buttons — goes into the default slot (no slot attribute)
|
|
25
|
+
const CheckIconDefault = (
|
|
26
|
+
<svg
|
|
27
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
28
|
+
viewBox="0 0 24 24"
|
|
29
|
+
width="1em"
|
|
30
|
+
height="1em"
|
|
31
|
+
aria-hidden
|
|
32
|
+
fill="currentColor"
|
|
33
|
+
>
|
|
34
|
+
<path d="M12 0a12 12 0 1 0 12 12A12 12 0 0 0 12 0Zm6.93 8.2-6.85 9.29a1 1 0 0 1-1.43.19l-4.89-3.91a1 1 0 0 1-.15-1.41A1 1 0 0 1 7 12.21l4.08 3.26L17.32 7a1 1 0 0 1 1.39-.21 1 1 0 0 1 .22 1.41Z" />
|
|
35
|
+
</svg>
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const iconStartMap = {
|
|
39
|
+
none: undefined,
|
|
40
|
+
check: CheckIcon,
|
|
41
|
+
arrowRight: ArrowRightStart,
|
|
42
|
+
arrowLeft: ArrowLeftStart,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const iconEndMap = {
|
|
46
|
+
none: undefined,
|
|
47
|
+
arrowRight: ArrowRightEnd,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type IconStartKey = keyof typeof iconStartMap;
|
|
51
|
+
type IconEndKey = keyof typeof iconEndMap;
|
|
52
|
+
|
|
53
|
+
// React wrapper translating typed props into the custom element's attributes/slots
|
|
54
|
+
interface BdsButtonProps {
|
|
55
|
+
variant?: ButtonVariant;
|
|
56
|
+
size?: ButtonSize;
|
|
57
|
+
disabled?: boolean;
|
|
58
|
+
href?: string;
|
|
59
|
+
hasPulse?: boolean;
|
|
60
|
+
isIconOnly?: boolean;
|
|
61
|
+
'aria-label'?: string;
|
|
62
|
+
iconStart?: ReactNode;
|
|
63
|
+
iconEnd?: ReactNode;
|
|
64
|
+
children?: ReactNode;
|
|
65
|
+
}
|
|
66
|
+
|
|
7
67
|
function BdsButton({
|
|
8
68
|
variant = 'default',
|
|
9
69
|
size = 'medium',
|
|
10
70
|
disabled,
|
|
11
71
|
href,
|
|
12
72
|
hasPulse,
|
|
73
|
+
isIconOnly,
|
|
74
|
+
'aria-label': ariaLabel,
|
|
75
|
+
iconStart,
|
|
76
|
+
iconEnd,
|
|
13
77
|
children,
|
|
14
|
-
}: {
|
|
15
|
-
variant?: ButtonVariant;
|
|
16
|
-
size?: ButtonSize;
|
|
17
|
-
disabled?: boolean;
|
|
18
|
-
href?: string;
|
|
19
|
-
hasPulse?: boolean;
|
|
20
|
-
children?: React.ReactNode;
|
|
21
|
-
}) {
|
|
78
|
+
}: BdsButtonProps) {
|
|
22
79
|
return React.createElement(
|
|
23
80
|
'bds-button',
|
|
24
|
-
{
|
|
81
|
+
{
|
|
82
|
+
variant,
|
|
83
|
+
size,
|
|
84
|
+
disabled: disabled || undefined,
|
|
85
|
+
href,
|
|
86
|
+
'has-pulse': hasPulse || undefined,
|
|
87
|
+
'is-icon-only': isIconOnly || undefined,
|
|
88
|
+
'aria-label': ariaLabel,
|
|
89
|
+
},
|
|
90
|
+
iconStart,
|
|
25
91
|
children,
|
|
92
|
+
iconEnd,
|
|
26
93
|
);
|
|
27
94
|
}
|
|
28
95
|
|
|
29
|
-
|
|
96
|
+
type BdsButtonStoryArgs = Omit<BdsButtonProps, 'iconStart' | 'iconEnd'> & {
|
|
97
|
+
iconStart?: IconStartKey;
|
|
98
|
+
iconEnd?: IconEndKey;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const meta: Meta<BdsButtonStoryArgs> = {
|
|
30
102
|
title: 'Web Components/Interaction/Button',
|
|
31
103
|
component: BdsButton,
|
|
32
104
|
tags: ['!stable', 'experimental'],
|
|
33
105
|
parameters: { layout: 'centered' },
|
|
34
106
|
argTypes: {
|
|
35
|
-
variant: {
|
|
36
|
-
|
|
107
|
+
variant: {
|
|
108
|
+
control: 'select',
|
|
109
|
+
options: ['default', 'outline'],
|
|
110
|
+
description: '`default` is filled; `outline` is transparent with a border.',
|
|
111
|
+
table: { defaultValue: { summary: 'default' } },
|
|
112
|
+
},
|
|
113
|
+
size: {
|
|
114
|
+
control: 'select',
|
|
115
|
+
options: ['small', 'medium', 'large'],
|
|
116
|
+
description: 'Height + horizontal padding.',
|
|
117
|
+
table: { defaultValue: { summary: 'medium' } },
|
|
118
|
+
},
|
|
119
|
+
iconStart: {
|
|
120
|
+
control: 'select',
|
|
121
|
+
options: Object.keys(iconStartMap) as IconStartKey[],
|
|
122
|
+
mapping: iconStartMap,
|
|
123
|
+
description: 'Icon rendered in the `icon-start` slot.',
|
|
124
|
+
},
|
|
125
|
+
iconEnd: {
|
|
126
|
+
control: 'select',
|
|
127
|
+
options: Object.keys(iconEndMap) as IconEndKey[],
|
|
128
|
+
mapping: iconEndMap,
|
|
129
|
+
description: 'Icon rendered in the `icon-end` slot.',
|
|
130
|
+
},
|
|
131
|
+
isIconOnly: {
|
|
132
|
+
control: 'boolean',
|
|
133
|
+
description: 'Forces 1:1 aspect ratio with minimal xs padding. Composable with any `variant`. Requires `aria-label`.',
|
|
134
|
+
table: { defaultValue: { summary: 'false' } },
|
|
135
|
+
},
|
|
136
|
+
hasPulse: {
|
|
137
|
+
control: 'boolean',
|
|
138
|
+
description: 'Adds a pulsing animation for CTA emphasis.',
|
|
139
|
+
table: { defaultValue: { summary: 'false' } },
|
|
140
|
+
},
|
|
141
|
+
disabled: { control: 'boolean' },
|
|
142
|
+
href: { control: 'text', description: 'When set, renders as `<a>` with button styling.' },
|
|
143
|
+
'aria-label': { control: 'text', description: 'Required for icon-only buttons.' },
|
|
37
144
|
},
|
|
38
|
-
|
|
145
|
+
args: { variant: 'default', size: 'medium' },
|
|
146
|
+
};
|
|
39
147
|
|
|
40
148
|
export default meta;
|
|
41
|
-
type Story = StoryObj<
|
|
149
|
+
type Story = StoryObj<BdsButtonStoryArgs>;
|
|
42
150
|
|
|
43
|
-
export const
|
|
44
|
-
|
|
151
|
+
export const Playground: Story = {
|
|
152
|
+
args: { children: 'Button', iconStart: 'none', iconEnd: 'none' },
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export const Default: Story = { args: { children: 'Default' } };
|
|
156
|
+
export const Outline: Story = { args: { children: 'Outline', variant: 'outline' } };
|
|
45
157
|
export const Small: Story = { args: { children: 'Small', size: 'small' } };
|
|
46
158
|
export const Medium: Story = { args: { children: 'Medium', size: 'medium' } };
|
|
47
159
|
export const Large: Story = { args: { children: 'Large', size: 'large' } };
|
|
48
160
|
export const WithPulse: Story = { args: { children: 'Pulsing', hasPulse: true } };
|
|
49
161
|
export const Disabled: Story = { args: { children: 'Disabled', disabled: true } };
|
|
162
|
+
export const WithIconStart: Story = { args: { children: 'Confirm', iconStart: 'check' } };
|
|
163
|
+
export const WithIconEnd: Story = { args: { children: 'Continue', iconEnd: 'arrowRight' } };
|
|
164
|
+
|
|
165
|
+
export const IconOnly: Story = {
|
|
166
|
+
args: {
|
|
167
|
+
isIconOnly: true,
|
|
168
|
+
'aria-label': 'Confirm',
|
|
169
|
+
children: CheckIconDefault,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export const IconOnlyOutline: Story = {
|
|
174
|
+
args: {
|
|
175
|
+
variant: 'outline',
|
|
176
|
+
isIconOnly: true,
|
|
177
|
+
'aria-label': 'Confirm',
|
|
178
|
+
children: CheckIconDefault,
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
|
|
50
182
|
export const AllVariants: Story = {
|
|
51
183
|
render: () => (
|
|
52
184
|
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap', alignItems: 'center' }}>
|
|
53
|
-
{(['default', '
|
|
185
|
+
{(['default', 'outline'] as const).flatMap(v =>
|
|
54
186
|
(['small', 'medium', 'large'] as const).map(s => (
|
|
55
187
|
<BdsButton key={`${v}-${s}`} variant={v} size={s}>{v} {s}</BdsButton>
|
|
56
188
|
)),
|
|
@@ -58,3 +190,23 @@ export const AllVariants: Story = {
|
|
|
58
190
|
</div>
|
|
59
191
|
),
|
|
60
192
|
};
|
|
193
|
+
|
|
194
|
+
export const AllIconOnly: Story = {
|
|
195
|
+
render: () => (
|
|
196
|
+
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap', alignItems: 'center' }}>
|
|
197
|
+
{(['default', 'outline'] as const).flatMap(v =>
|
|
198
|
+
(['small', 'medium', 'large'] as const).map(s => (
|
|
199
|
+
<BdsButton
|
|
200
|
+
key={`${v}-${s}`}
|
|
201
|
+
variant={v}
|
|
202
|
+
size={s}
|
|
203
|
+
isIconOnly
|
|
204
|
+
aria-label={`${v} ${s}`}
|
|
205
|
+
>
|
|
206
|
+
{CheckIconDefault}
|
|
207
|
+
</BdsButton>
|
|
208
|
+
)),
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
),
|
|
212
|
+
};
|
|
@@ -92,4 +92,39 @@ describe('bds-button', () => {
|
|
|
92
92
|
expect(a!.getAttribute('rel')).toBe('noopener');
|
|
93
93
|
cleanup(el);
|
|
94
94
|
});
|
|
95
|
+
|
|
96
|
+
it('applies is-icon-only class when is-icon-only is set', async () => {
|
|
97
|
+
const el = await fixture('<bds-button is-icon-only aria-label="Confirm"><svg></svg></bds-button>');
|
|
98
|
+
const btn = el.shadowRoot!.querySelector('button');
|
|
99
|
+
expect(btn!.className).toContain('is-icon-only');
|
|
100
|
+
cleanup(el);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('does not apply is-icon-only class by default', async () => {
|
|
104
|
+
const el = await fixture('<bds-button>Click</bds-button>');
|
|
105
|
+
const btn = el.shadowRoot!.querySelector('button');
|
|
106
|
+
expect(btn!.className).not.toContain('is-icon-only');
|
|
107
|
+
cleanup(el);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('reflects the is-icon-only attribute on the host', async () => {
|
|
111
|
+
const el = await fixture('<bds-button is-icon-only aria-label="Confirm"></bds-button>');
|
|
112
|
+
expect(el.hasAttribute('is-icon-only')).toBe(true);
|
|
113
|
+
cleanup(el);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('composes is-icon-only with outline variant', async () => {
|
|
117
|
+
const el = await fixture('<bds-button variant="outline" is-icon-only aria-label="Confirm"></bds-button>');
|
|
118
|
+
const btn = el.shadowRoot!.querySelector('button');
|
|
119
|
+
expect(btn!.className).toContain('outline');
|
|
120
|
+
expect(btn!.className).toContain('is-icon-only');
|
|
121
|
+
cleanup(el);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('forwards aria-label to the inner button', async () => {
|
|
125
|
+
const el = await fixture('<bds-button is-icon-only aria-label="Close"></bds-button>');
|
|
126
|
+
const btn = el.shadowRoot!.querySelector('button');
|
|
127
|
+
expect(btn!.getAttribute('aria-label')).toBe('Close');
|
|
128
|
+
cleanup(el);
|
|
129
|
+
});
|
|
95
130
|
});
|
|
@@ -14,6 +14,7 @@ export type ButtonSize = 'small' | 'medium' | 'large';
|
|
|
14
14
|
* target — forwarded to <a> when href is set
|
|
15
15
|
* rel — forwarded to <a> when href is set
|
|
16
16
|
* has-pulse — boolean, enables the pulse ring animation
|
|
17
|
+
* is-icon-only — boolean, forces a 1:1 aspect ratio with minimal xs padding (requires aria-label)
|
|
17
18
|
* type — "button" (default) | "submit" | "reset"
|
|
18
19
|
*
|
|
19
20
|
* Slots:
|
|
@@ -114,6 +115,23 @@ export class BdsButton extends LitElement {
|
|
|
114
115
|
animation: pulse 3s infinite;
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
/* Icon-only: square (1:1), equal xs padding on both axes — composes with
|
|
119
|
+
any variant/size. box-sizing: border-box keeps the outer box at
|
|
120
|
+
--button_height; without it, all: unset reverts to content-box and
|
|
121
|
+
padding-block would stretch the button past its declared height.
|
|
122
|
+
Slotted child fills the content area (button height minus the two
|
|
123
|
+
padding sides) so SVG icons scale with button size. */
|
|
124
|
+
.button.is-icon-only {
|
|
125
|
+
aspect-ratio: 1;
|
|
126
|
+
box-sizing: border-box;
|
|
127
|
+
padding: var(--bds-space_xs);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
:host([is-icon-only]) ::slotted(*) {
|
|
131
|
+
inline-size: 100%;
|
|
132
|
+
block-size: 100%;
|
|
133
|
+
}
|
|
134
|
+
|
|
117
135
|
/* SVG icon colour */
|
|
118
136
|
.button svg {
|
|
119
137
|
--icon__stroke: currentcolor;
|
|
@@ -141,6 +159,12 @@ export class BdsButton extends LitElement {
|
|
|
141
159
|
transition: var(--bds-animation_transition);
|
|
142
160
|
}
|
|
143
161
|
|
|
162
|
+
/* Icon-only: zero out icon slot margins so icon centres */
|
|
163
|
+
.button.is-icon-only .icon-start.has-content,
|
|
164
|
+
.button.is-icon-only .icon-end.has-content {
|
|
165
|
+
margin-inline: 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
144
168
|
/* Hover icon animation */
|
|
145
169
|
@media (hover: hover) and (pointer: fine) {
|
|
146
170
|
.button:hover .icon-start svg,
|
|
@@ -189,6 +213,7 @@ export class BdsButton extends LitElement {
|
|
|
189
213
|
target: { type: String },
|
|
190
214
|
rel: { type: String },
|
|
191
215
|
hasPulse: { type: Boolean, attribute: 'has-pulse', reflect: true },
|
|
216
|
+
isIconOnly: { type: Boolean, attribute: 'is-icon-only', reflect: true },
|
|
192
217
|
type: { type: String },
|
|
193
218
|
ariaLabel: { type: String, attribute: 'aria-label' },
|
|
194
219
|
// Internal state
|
|
@@ -205,6 +230,7 @@ export class BdsButton extends LitElement {
|
|
|
205
230
|
declare target: string | undefined;
|
|
206
231
|
declare rel: string | undefined;
|
|
207
232
|
declare hasPulse: boolean;
|
|
233
|
+
declare isIconOnly: boolean;
|
|
208
234
|
declare type: 'button' | 'submit' | 'reset';
|
|
209
235
|
declare ariaLabel: string | null;
|
|
210
236
|
declare private _iconStartFilled: boolean;
|
|
@@ -216,6 +242,7 @@ export class BdsButton extends LitElement {
|
|
|
216
242
|
this.size = 'medium';
|
|
217
243
|
this.disabled = false;
|
|
218
244
|
this.hasPulse = false;
|
|
245
|
+
this.isIconOnly = false;
|
|
219
246
|
this.type = 'button';
|
|
220
247
|
this.ariaLabel = null;
|
|
221
248
|
this._iconStartFilled = false;
|
|
@@ -233,7 +260,7 @@ export class BdsButton extends LitElement {
|
|
|
233
260
|
}
|
|
234
261
|
|
|
235
262
|
private get _classes() {
|
|
236
|
-
return `button ${this.variant} ${this.size}${this.hasPulse ? ' has-pulse' : ''}`;
|
|
263
|
+
return `button ${this.variant} ${this.size}${this.hasPulse ? ' has-pulse' : ''}${this.isIconOnly ? ' is-icon-only' : ''}`;
|
|
237
264
|
}
|
|
238
265
|
|
|
239
266
|
private _handleAnchorClick(e: MouseEvent) {
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
|
|
2
|
+
import * as Stories from './BdsGrid.stories';
|
|
3
|
+
|
|
4
|
+
<Meta of={Stories} />
|
|
5
|
+
|
|
6
|
+
# <bds-grid>
|
|
7
|
+
|
|
8
|
+
Framework-agnostic Grid layout custom element. Built on the `@boostdev/design-system-foundation` grid tokens.
|
|
9
|
+
|
|
10
|
+
> **Status: experimental** — API may change before stable release.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
import '@boostdev/design-system-components/web-components';
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Variants
|
|
19
|
+
|
|
20
|
+
| Variant | Use case |
|
|
21
|
+
| -------- | ---------------------------------------------------------------------------------------- |
|
|
22
|
+
| `main` | Default. Responsive content-block grid matching the Figma main grid. |
|
|
23
|
+
| `page` | Single-column full-width grid. Use at page root. |
|
|
24
|
+
| `funnel` | Narrow variant of `main`; capped at `--bds-container_max-width--narrow` (48rem). |
|
|
25
|
+
| `custom` | Explicit column count via the `columns` attribute. Not responsive — use sparingly. |
|
|
26
|
+
|
|
27
|
+
## Responsive behavior
|
|
28
|
+
|
|
29
|
+
`main` and `funnel` inherit the foundation's column ladder automatically. Named `column-span` values adapt across breakpoints so no media queries are required in consumer code:
|
|
30
|
+
|
|
31
|
+
| Viewport | Total columns | `one-quarter` | `one-third` | `half` | `two-thirds` | `three-quarters` | `full` |
|
|
32
|
+
| ----------------- | ------------- | ------------- | ----------- | ------ | ------------ | ---------------- | ------ |
|
|
33
|
+
| mobile (< 48rem) | 4 | 4 | 4 | 4 | 4 | 4 | 4 |
|
|
34
|
+
| tablet (≥ 48rem) | 8 | 4 | 4 | 4 | 4 | 8 | 8 |
|
|
35
|
+
| desktop (≥ 60rem) | 12 | 3 | 4 | 6 | 8 | 9 | 12 |
|
|
36
|
+
|
|
37
|
+
Numeric `column-span="n"` is literal — it does not adapt. Use named values for responsive behavior.
|
|
38
|
+
|
|
39
|
+
## Props
|
|
40
|
+
|
|
41
|
+
<ArgTypes of={Stories} />
|
|
42
|
+
|
|
43
|
+
## Attributes
|
|
44
|
+
|
|
45
|
+
| Attribute | Type | Default | Description |
|
|
46
|
+
| ----------------- | ---------------------------------------------- | -------- | -------------------------------------------------- |
|
|
47
|
+
| `variant` | `"main" \| "page" \| "funnel" \| "custom"` | `"main"` | Grid type |
|
|
48
|
+
| `columns` | number | — | Column count (only for `variant="custom"`) |
|
|
49
|
+
| `is-centered` | boolean | `false` | Caps max-width via `--bds-container_max-width` |
|
|
50
|
+
| `is-masonry` | boolean | `false` | Enables masonry layout (CSS Grid Level 3 "grid-lanes"). Uses native CSS when supported, otherwise runs a JS polyfill. |
|
|
51
|
+
| `auto-span-media` | boolean | `false` | Child `<bds-grid-item>`s without an explicit `column-span` resolve their span from the intrinsic aspect ratio of their first `<img>`/`<video>` descendant. See [Aspect-ratio auto-span](#aspect-ratio-auto-span). |
|
|
52
|
+
|
|
53
|
+
## Slots
|
|
54
|
+
|
|
55
|
+
| Slot | Description |
|
|
56
|
+
| ----------- | -------------------------------------------- |
|
|
57
|
+
| `(default)` | Grid content; typically `<bds-grid-item>` children |
|
|
58
|
+
|
|
59
|
+
## CSS custom properties
|
|
60
|
+
|
|
61
|
+
| Property | Default | Description |
|
|
62
|
+
| ----------------- | ------------------------- | ------------------------------------------ |
|
|
63
|
+
| `--grid_gap` | `var(--bds-grid_gap)` | Gap between grid items |
|
|
64
|
+
| `--grid_columns` | `4` | Column count for `variant="custom"` |
|
|
65
|
+
|
|
66
|
+
## Usage in plain HTML
|
|
67
|
+
|
|
68
|
+
```html
|
|
69
|
+
<bds-grid variant="main">
|
|
70
|
+
<bds-grid-item column-span="half">A</bds-grid-item>
|
|
71
|
+
<bds-grid-item column-span="half">B</bds-grid-item>
|
|
72
|
+
</bds-grid>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Nesting
|
|
76
|
+
|
|
77
|
+
Grids can nest. A `variant="page"` inside another grid uses `grid-column: 1 / -1` so nested page grids always occupy the full width of their container.
|
|
78
|
+
|
|
79
|
+
```html
|
|
80
|
+
<bds-grid variant="main">
|
|
81
|
+
<bds-grid-item column-span="full">
|
|
82
|
+
<bds-grid variant="page">…</bds-grid>
|
|
83
|
+
</bds-grid-item>
|
|
84
|
+
</bds-grid>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Accessibility
|
|
88
|
+
|
|
89
|
+
`<bds-grid>` is purely presentational — no implicit ARIA role. Wrap in a landmark (`<main>`, `<section>`) if one applies. The grid does not manage focus order; source order drives reading order and should match the visual flow.
|
|
90
|
+
|
|
91
|
+
## Masonry layout
|
|
92
|
+
|
|
93
|
+
Set `is-masonry` on any `<bds-grid>` to enable masonry (a.k.a. Pinterest-style) layout. Items flow into whichever track currently has the most room, removing the ragged gaps a standard grid leaves when items have uneven heights.
|
|
94
|
+
|
|
95
|
+
```html
|
|
96
|
+
<bds-grid is-masonry>
|
|
97
|
+
<bds-grid-item column-span="one-quarter">…</bds-grid-item>
|
|
98
|
+
<bds-grid-item column-span="one-quarter">…</bds-grid-item>
|
|
99
|
+
</bds-grid>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### When to use
|
|
103
|
+
|
|
104
|
+
- Image/card galleries with heterogenous heights.
|
|
105
|
+
- Feeds/moodboards where uniform-height tiles would look wrong.
|
|
106
|
+
|
|
107
|
+
### When NOT to use
|
|
108
|
+
|
|
109
|
+
- Layouts where the visual order must match reading/tab order. `grid-auto-flow: dense` can backfill an item into an earlier visual slot than its DOM position suggests. Keep DOM order meaningful.
|
|
110
|
+
- Fixed-height cards — a standard grid is simpler and has no JS dependency.
|
|
111
|
+
- Row-aligned layouts (comparison tables).
|
|
112
|
+
|
|
113
|
+
### How it works
|
|
114
|
+
|
|
115
|
+
Targets [CSS Grid Layout Module Level 3](https://www.w3.org/TR/css-grid-3/) ("grid lanes"):
|
|
116
|
+
|
|
117
|
+
- **Native path** — when the browser supports `display: grid-lanes` or `grid-template-rows: masonry`, CSS handles everything and the polyfill short-circuits.
|
|
118
|
+
- **Polyfill path** — row-span packing: the container collapses to a 1 px row track with `grid-auto-flow: dense`, and each item is given `grid-row-end: span ceil(height + gap)`. Items stay in **normal flow** (no absolute positioning), so surrounding content lays out correctly around the grid.
|
|
119
|
+
|
|
120
|
+
Re-runs on child size changes (`ResizeObserver` on each child) and child addition/removal (`MutationObserver` on the host). The container itself is not observed to avoid a feedback loop — setting row-spans changes the container's block-size.
|
|
121
|
+
|
|
122
|
+
### Span behavior in masonry
|
|
123
|
+
|
|
124
|
+
- `column-span` works normally — wider items span more columns. Use this to make a hero or panorama stand out.
|
|
125
|
+
- `row-span` is **ignored**. The stacking axis has no rows to span.
|
|
126
|
+
- `start-column` / `end-column` still apply for definite placement.
|
|
127
|
+
|
|
128
|
+
### Performance
|
|
129
|
+
|
|
130
|
+
- Initial layout is one measure pass + one write pass (heights are batched to avoid per-item reflows).
|
|
131
|
+
- Re-layout is scheduled via `requestAnimationFrame` so bursts of observer callbacks coalesce into a single run per frame.
|
|
132
|
+
- Row-gap is baked into each item's span, so ceil-rounding only introduces sub-pixel overshoot per item — not visible in practice.
|
|
133
|
+
|
|
134
|
+
### Accessibility
|
|
135
|
+
|
|
136
|
+
Masonry does not change semantics — DOM order is preserved. Because visual order can differ from DOM order, **do not rely on visual order for screen-reader reading order**. Structure content so it makes sense top-to-bottom in the DOM.
|
|
137
|
+
|
|
138
|
+
## Aspect-ratio auto-span
|
|
139
|
+
|
|
140
|
+
For galleries mixing portrait, landscape, and panoramic media, set `auto-span-media` on `<bds-grid>` and each `<bds-grid-item>` without a `column-span` attribute will derive its span from the intrinsic aspect ratio of its first `<img>` or `<video>` descendant.
|
|
141
|
+
|
|
142
|
+
```html
|
|
143
|
+
<bds-grid is-masonry auto-span-media>
|
|
144
|
+
<bds-grid-item><img src="/portrait.jpg" alt=""></bds-grid-item>
|
|
145
|
+
<bds-grid-item><img src="/landscape.jpg" alt=""></bds-grid-item>
|
|
146
|
+
<bds-grid-item><img src="/panorama.jpg" alt=""></bds-grid-item>
|
|
147
|
+
</bds-grid>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Individual items can opt out with an explicit `column-span` (e.g. `column-span="full"` for a hero that shouldn't be auto-sized) or opt in explicitly with `column-span="auto"` when the parent grid doesn't set `auto-span-media`.
|
|
151
|
+
|
|
152
|
+
### Thresholds
|
|
153
|
+
|
|
154
|
+
| Aspect ratio (w ÷ h) | Resolved span | Typical source |
|
|
155
|
+
| -------------------- | ------------------- | --------------------------------- |
|
|
156
|
+
| ≤ 1.5 | `'one-quarter'` | Portrait, square, 3:2 portrait |
|
|
157
|
+
| > 1.5 and ≤ 2.2 | `'half'` | 16:9, 3:2 landscape, 2:1 |
|
|
158
|
+
| > 2.2 and ≤ 3.5 | `'three-quarters'` | 21:9, 2.35:1, 2.5:1 (cinematic) |
|
|
159
|
+
| > 3.5 | `'full'` | 32:9, hard panoramic |
|
|
160
|
+
| invalid / unresolved | `'one-quarter'` | No media / metadata not yet ready |
|
|
161
|
+
|
|
162
|
+
Named-span responsive behavior still applies — on mobile every item collapses to full-width regardless of its resolved span.
|
|
163
|
+
|
|
164
|
+
### Loading behavior
|
|
165
|
+
|
|
166
|
+
Dimensions are read from `img.naturalWidth/naturalHeight` and `video.videoWidth/videoHeight` — no layout is triggered. If metadata is not yet available when the element upgrades, the item starts at `'one-quarter'` and re-resolves when the `load` (image) or `loadedmetadata` (video) event fires. Subsequent slot mutations re-run the resolver.
|
|
167
|
+
|
|
168
|
+
### When NOT to use auto-span
|
|
169
|
+
|
|
170
|
+
- Items whose span reflects **content importance** rather than media shape — set `column-span` explicitly.
|
|
171
|
+
- Non-media grids — every item falls back to `'one-quarter'` because there's no aspect to read.
|
|
172
|
+
- Galleries where all images share the same aspect — the thresholds won't differentiate them; a uniform explicit span is simpler.
|
|
173
|
+
|
|
174
|
+
## Framework equivalents
|
|
175
|
+
|
|
176
|
+
| React | Web component |
|
|
177
|
+
| ----------------------- | -------------------------- |
|
|
178
|
+
| `<Grid>` / `<GridItem>` | `<bds-grid>` / `<bds-grid-item>` |
|
|
179
|
+
|
|
180
|
+
## Examples
|
|
181
|
+
|
|
182
|
+
### Main grid
|
|
183
|
+
<Canvas of={Stories.Main} />
|
|
184
|
+
|
|
185
|
+
### Page grid
|
|
186
|
+
<Canvas of={Stories.Page} />
|
|
187
|
+
|
|
188
|
+
### Funnel
|
|
189
|
+
<Canvas of={Stories.Funnel} />
|
|
190
|
+
|
|
191
|
+
### Custom
|
|
192
|
+
<Canvas of={Stories.Custom} />
|
|
193
|
+
|
|
194
|
+
### Centered
|
|
195
|
+
<Canvas of={Stories.Centered} />
|
|
196
|
+
|
|
197
|
+
### Spans
|
|
198
|
+
<Canvas of={Stories.Spans} />
|
|
199
|
+
|
|
200
|
+
### Start / end (escape hatch)
|
|
201
|
+
<Canvas of={Stories.StartEnd} />
|
|
202
|
+
|
|
203
|
+
### Complex layout
|
|
204
|
+
<Canvas of={Stories.ComplexLayout} />
|
|
205
|
+
|
|
206
|
+
### Masonry
|
|
207
|
+
<Canvas of={Stories.Masonry} />
|
|
208
|
+
|
|
209
|
+
### Masonry with spans
|
|
210
|
+
<Canvas of={Stories.MasonryWithSpans} />
|