@boostdev/design-system-components 2.1.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 +40 -0
- package/dist/client.cjs +303 -60
- package/dist/client.css +568 -527
- package/dist/client.d.cts +39 -1
- package/dist/client.d.ts +39 -1
- package/dist/client.js +309 -60
- package/dist/index.cjs +303 -60
- package/dist/index.css +568 -527
- package/dist/index.d.cts +39 -1
- package/dist/index.d.ts +39 -1
- package/dist/index.js +309 -60
- package/dist/web-components/index.d.ts +129 -1
- package/dist/web-components/index.js +311 -0
- package/package.json +1 -1
- 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/index.ts +2 -0
- package/src/web-components/index.ts +4 -0
- 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
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
|
|
2
|
+
import * as Stories from './Grid.stories';
|
|
3
|
+
|
|
4
|
+
<Meta of={Stories} />
|
|
5
|
+
|
|
6
|
+
# Grid
|
|
7
|
+
|
|
8
|
+
A responsive layout primitive built on the design-system-foundation grid tokens. Compose layouts from `Grid` (container) and `GridItem` (cells).
|
|
9
|
+
|
|
10
|
+
The foundation provides the breakpoint ladder (4 columns on mobile, 8 on tablet, 12 on desktop) and the responsive span tokens — `Grid` is a thin wrapper, not a reimplementation.
|
|
11
|
+
|
|
12
|
+
## When to use
|
|
13
|
+
|
|
14
|
+
- Page-level or content-block layouts that need to match the Figma grid
|
|
15
|
+
- Responsive multi-column layouts where column count should adapt per viewport
|
|
16
|
+
|
|
17
|
+
## Variants
|
|
18
|
+
|
|
19
|
+
| Variant | Use case |
|
|
20
|
+
| -------- | ----------------------------------------------------------------------------------------------------------- |
|
|
21
|
+
| `main` | Default. Content-block grid matching the Figma main grid. Often placed on `<main>` or large content blocks. |
|
|
22
|
+
| `page` | Single-column full-width grid. Use at the page root; stretches across any parent grid via `1 / -1`. |
|
|
23
|
+
| `funnel` | Narrow variant of `main` for funnels/forms. Capped at `--bds-container_max-width--narrow` (48rem). |
|
|
24
|
+
| `custom` | Explicit column count via the `columns` prop. Use sparingly — it is **not** responsive. |
|
|
25
|
+
|
|
26
|
+
## Responsive behavior
|
|
27
|
+
|
|
28
|
+
`main` and `funnel` inherit the foundation's column ladder automatically. Named `columnSpan` values adapt across breakpoints so you don't need media queries:
|
|
29
|
+
|
|
30
|
+
| Viewport | Total columns | `one-quarter` | `one-third` | `half` | `two-thirds` | `three-quarters` | `full` |
|
|
31
|
+
| ---------------------- | ------------- | ------------- | ----------- | ------ | ------------ | ---------------- | ------ |
|
|
32
|
+
| mobile (< 48rem) | 4 | 4 | 4 | 4 | 4 | 4 | 4 |
|
|
33
|
+
| tablet (≥ 48rem) | 8 | 4 | 4 | 4 | 4 | 8 | 8 |
|
|
34
|
+
| desktop (≥ 60rem) | 12 | 3 | 4 | 6 | 8 | 9 | 12 |
|
|
35
|
+
| wide (≥ 64rem / 86rem) | 12 | 3 | 4 | 6 | 8 | 9 | 12 |
|
|
36
|
+
|
|
37
|
+
Numeric `columnSpan={n}` is literal — it does not adapt. Use named values when you want responsive behavior.
|
|
38
|
+
|
|
39
|
+
## Grid props
|
|
40
|
+
|
|
41
|
+
<ArgTypes of={Stories} />
|
|
42
|
+
|
|
43
|
+
## GridItem props
|
|
44
|
+
|
|
45
|
+
| Prop | Type | Default | Description |
|
|
46
|
+
| ------------- | -------------------------------------------------------------------------------------------------------- | -------- | --------------------------------------------------------------------------------- |
|
|
47
|
+
| `columnSpan` | `'full'` <br /> \| `'three-quarters'` <br /> \| `'two-thirds'` <br /> \| `'half'` <br /> \| `'one-third'` <br /> \| `'one-quarter'` <br /> \| `'auto'` <br /> \| `number` | `'full'` | How many grid columns to span. Named values are responsive via the foundation. `'auto'` derives the span from the intrinsic aspect ratio of the first `<img>`/`<video>` descendant — see [Aspect-ratio auto-span](#aspect-ratio-auto-span). |
|
|
48
|
+
| `rowSpan` | `number` | — | How many rows to span. Ignored in masonry layouts. |
|
|
49
|
+
| `startColumn` | `number` | — | **Escape hatch.** Explicit start column. Overrides `columnSpan` responsive logic. |
|
|
50
|
+
| `endColumn` | `number` | — | **Escape hatch.** Explicit end column. Overrides `columnSpan` responsive logic. |
|
|
51
|
+
| `as` | `ElementType` | `'div'` | Render as a custom element/component. |
|
|
52
|
+
|
|
53
|
+
A `GridItemSpan` constant is exported for dot-access:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { GridItemSpan } from '@boostdev/design-system-components';
|
|
57
|
+
<GridItem columnSpan={GridItemSpan.threeQuarters}>…</GridItem>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Warning: startColumn / endColumn
|
|
61
|
+
|
|
62
|
+
These override the responsive span tokens and force fixed positioning. Only reach for them when you fully understand the layout implications. Prefer named `columnSpan` values.
|
|
63
|
+
|
|
64
|
+
## HTML attribute forwarding
|
|
65
|
+
|
|
66
|
+
All standard HTML attributes (e.g. `id`, `data-*`, `aria-*`, `role`) spread onto the root element.
|
|
67
|
+
|
|
68
|
+
## CSS variables
|
|
69
|
+
|
|
70
|
+
<table>
|
|
71
|
+
<thead>
|
|
72
|
+
<tr><th>Variable</th><th>Default</th><th>Description</th></tr>
|
|
73
|
+
</thead>
|
|
74
|
+
<tbody>
|
|
75
|
+
<tr><td>`--grid_gap`</td><td>`var(--bds-grid_gap)`</td><td>Gap between grid items.</td></tr>
|
|
76
|
+
<tr><td>`--grid_columns`</td><td>`4`</td><td>Column count for `variant="custom"` (set automatically when the `columns` prop is supplied).</td></tr>
|
|
77
|
+
</tbody>
|
|
78
|
+
</table>
|
|
79
|
+
|
|
80
|
+
## Foundation tokens referenced
|
|
81
|
+
|
|
82
|
+
- `--bds-grid_template-columns`, `--bds-grid_gap`, `--bds-grid_columns`
|
|
83
|
+
- `--bds-grid_span-100`, `--bds-grid_columns-75/66/50/33/25`
|
|
84
|
+
- `--bds-container_max-width`, `--bds-container_max-width--narrow`, `--bds-container_spacing-inline`
|
|
85
|
+
|
|
86
|
+
## Nesting
|
|
87
|
+
|
|
88
|
+
Grids can nest. A `variant="page"` inside another grid uses `grid-column: 1 / -1` to span the full parent, so nested page grids always occupy the full width of their container.
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
<Grid variant="main">
|
|
92
|
+
<GridItem columnSpan="full">
|
|
93
|
+
<Grid variant="page">…</Grid>
|
|
94
|
+
</GridItem>
|
|
95
|
+
</Grid>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Accessibility
|
|
99
|
+
|
|
100
|
+
Grid is purely presentational — it renders a `<div>` by default with no ARIA role. Use the `as` prop to render the appropriate landmark when the grid maps to one (`as="main"`, `as="section"`, etc.). The grid itself does not manage focus order; source order drives reading order and should match the visual flow.
|
|
101
|
+
|
|
102
|
+
## Framework equivalents
|
|
103
|
+
|
|
104
|
+
| Package | React | Web component |
|
|
105
|
+
| -------------------------------------- | -------------- | ------------------------- |
|
|
106
|
+
| `@boostdev/design-system-components` | `<Grid>` / `<GridItem>` | `<bds-grid>` / `<bds-grid-item>` |
|
|
107
|
+
|
|
108
|
+
## Masonry layout
|
|
109
|
+
|
|
110
|
+
Set `isMasonry` on any `Grid` to switch to a masonry (a.k.a. Pinterest-style) layout. Items flow into whichever track currently has the most room, so a column with a short item backfills under the tall item next to it — removing the ragged gaps that standard CSS Grid leaves when items have uneven heights.
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
<Grid isMasonry>
|
|
114
|
+
{items.map((i) => <GridItem columnSpan="one-quarter" key={i.id}>{i.content}</GridItem>)}
|
|
115
|
+
</Grid>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### When to use
|
|
119
|
+
|
|
120
|
+
- Image/card galleries with heterogenous heights (photos in mixed portrait/landscape, cards with variable text length).
|
|
121
|
+
- Pinterest / Unsplash / moodboard-style feeds.
|
|
122
|
+
|
|
123
|
+
### When NOT to use
|
|
124
|
+
|
|
125
|
+
- Layouts that require a stable **tab/reading order** that matches the visual order — masonry's `grid-auto-flow: dense` backfills items into earlier slots, which means DOM order can differ from visual top-to-bottom order. Keep source order meaningful regardless; don't rely on visual order for accessibility.
|
|
126
|
+
- Fixed-height cards in a uniform grid — standard grid is simpler, cheaper, and has no JS dependency.
|
|
127
|
+
- Lists where row alignment across columns matters (e.g. comparison tables).
|
|
128
|
+
|
|
129
|
+
### How it works
|
|
130
|
+
|
|
131
|
+
Masonry targets [CSS Grid Layout Module Level 3](https://www.w3.org/TR/css-grid-3/) ("grid lanes"):
|
|
132
|
+
|
|
133
|
+
- **Native path** — when the browser supports `display: grid-lanes` or `grid-template-rows: masonry`, CSS handles everything and the JS polyfill short-circuits. Currently Safari Technology Preview only; the polyfill runs in all other browsers.
|
|
134
|
+
- **Polyfill path** — row-span packing. The container's row tracks are collapsed to a 1 px auto-track with `grid-auto-flow: dense`, and each item is given a `grid-row-end: span ceil(height + gap)`. Items stay in **normal document flow** (no absolute positioning), so surrounding content — footers, scroll anchors, page margins — lays out correctly around the grid.
|
|
135
|
+
|
|
136
|
+
The polyfill re-runs on child size changes (`ResizeObserver` on each child) and child addition/removal (`MutationObserver` on the container). The container itself is not observed — setting row-spans changes the container block-size, which would loop back through the observer.
|
|
137
|
+
|
|
138
|
+
### Span behavior in masonry
|
|
139
|
+
|
|
140
|
+
- `columnSpan` works normally — items can span multiple columns. This is how you widen a hero or panorama item within a masonry feed.
|
|
141
|
+
- `rowSpan` is **ignored**. The stacking axis has no rows to span — each column flows freely.
|
|
142
|
+
- `startColumn` / `endColumn` still work for definite placement.
|
|
143
|
+
|
|
144
|
+
### Performance
|
|
145
|
+
|
|
146
|
+
- **Initial layout** — three passes (clear, batch-read all heights, batch-write all spans). Height reads and writes are each grouped to avoid O(N) forced reflows; the measured cost is one layout pass regardless of item count.
|
|
147
|
+
- **Re-layout** — on child resize or mutation the polyfill is scheduled via `requestAnimationFrame`, so bursts of observer callbacks coalesce into a single re-run per frame.
|
|
148
|
+
- **Ceil overshoot** — the 1 px row track means each item's span can overshoot by up to 1 px. Not visible in practice (row-gap is 0; the gap is baked into each item's span).
|
|
149
|
+
- **Long lists** — for galleries with hundreds of items, virtualisation is still the correct answer; masonry is orthogonal.
|
|
150
|
+
|
|
151
|
+
### SSR / hydration
|
|
152
|
+
|
|
153
|
+
Grid is client-only in masonry mode (uses `useRef` + `useLayoutEffect`). Before hydration there is no row-span overlay, so items fall back to one-row tracks. If you need a server-rendered placeholder, render a static `variant="custom"` grid of the same width on the server and swap to `isMasonry` after hydration.
|
|
154
|
+
|
|
155
|
+
### Accessibility
|
|
156
|
+
|
|
157
|
+
Masonry does not change semantics — the DOM stays in source order. Because `grid-auto-flow: dense` can pick any column for each item, **do not rely on visual order for screen-reader reading order**. Structure your content so it makes sense top-to-bottom in the DOM.
|
|
158
|
+
|
|
159
|
+
## Aspect-ratio auto-span
|
|
160
|
+
|
|
161
|
+
For galleries that mix portrait, landscape, and panoramic images, set `autoSpanMedia` on the `Grid` and each `GridItem` will derive its column span from the intrinsic aspect ratio of its first `<img>` or `<video>` descendant. The wide pano automatically gets more columns; the portrait stays a quarter.
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
<Grid isMasonry autoSpanMedia>
|
|
165
|
+
{photos.map((p) => (
|
|
166
|
+
<GridItem key={p.id}>
|
|
167
|
+
<img src={p.src} alt={p.alt} />
|
|
168
|
+
</GridItem>
|
|
169
|
+
))}
|
|
170
|
+
</Grid>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Individual items can opt in or out explicitly with `columnSpan="auto"` or a fixed span — `autoSpanMedia` is only the default for items that don't set `columnSpan` themselves.
|
|
174
|
+
|
|
175
|
+
### Thresholds
|
|
176
|
+
|
|
177
|
+
| Aspect ratio (w ÷ h) | Resolved span | Typical source |
|
|
178
|
+
| -------------------- | ------------------- | --------------------------------- |
|
|
179
|
+
| ≤ 1.5 | `'one-quarter'` | Portrait, square, 3:2 portrait |
|
|
180
|
+
| > 1.5 and ≤ 2.2 | `'half'` | 16:9, 3:2 landscape, 2:1 |
|
|
181
|
+
| > 2.2 and ≤ 3.5 | `'three-quarters'` | 21:9, 2.35:1, 2.5:1 (cinematic) |
|
|
182
|
+
| > 3.5 | `'full'` | 32:9, hard panoramic |
|
|
183
|
+
| invalid / unresolved | `'one-quarter'` | No media / metadata not yet ready |
|
|
184
|
+
|
|
185
|
+
Named-span responsive behavior still applies, so on mobile every item collapses to full-width (4-col grid) regardless of its resolved span.
|
|
186
|
+
|
|
187
|
+
### Loading behavior
|
|
188
|
+
|
|
189
|
+
Intrinsic dimensions are read from `img.naturalWidth/naturalHeight` and `video.videoWidth/videoHeight` — no layout is triggered. If metadata is not yet available when the component mounts, the item starts at `'one-quarter'` and re-resolves when the `load` (image) or `loadedmetadata` (video) event fires. Subsequent child mutations re-run the resolver.
|
|
190
|
+
|
|
191
|
+
### When NOT to use auto-span
|
|
192
|
+
|
|
193
|
+
- Items where span should reflect **content importance** (a featured card is wider because it's featured, not because its photo is wide) — set `columnSpan` explicitly.
|
|
194
|
+
- Non-media grids — a text-only card has no aspect ratio to read, so every item falls back to `'one-quarter'`. Use explicit spans instead.
|
|
195
|
+
- Galleries where all images share the same aspect — the thresholds won't differentiate them; a uniform explicit span is simpler.
|
|
196
|
+
|
|
197
|
+
### Performance
|
|
198
|
+
|
|
199
|
+
- One `querySelector('img, video')` + one property read per item per resolve pass.
|
|
200
|
+
- No layout thrashing: `naturalWidth` / `videoWidth` are cached properties that resolve once metadata is available.
|
|
201
|
+
- Event subscriptions are `{ once: true }` so there's no cleanup ceremony per item.
|
|
202
|
+
- Re-resolution cost is bounded by the number of items whose media changes, not the total item count.
|
|
203
|
+
|
|
204
|
+
## Migration from `@greenchoice/design-system`
|
|
205
|
+
|
|
206
|
+
| Greenchoice | BoostDev equivalent |
|
|
207
|
+
| ------------------------------- | -------------------------------------- |
|
|
208
|
+
| `<GdsGrid variant="main">` | `<Grid variant="main">` |
|
|
209
|
+
| `<GdsGridItem columnSpan="twoThird">` | `<GridItem columnSpan="two-thirds">` |
|
|
210
|
+
| `--gds-grid-span-*` | `--bds-grid_span-*` |
|
|
211
|
+
| `--gds-main-container--max-width` | `--bds-container_max-width` |
|
|
212
|
+
| `--gds-main-container--max-width--narrow` | `--bds-container_max-width--narrow` |
|
|
213
|
+
|
|
214
|
+
## Examples
|
|
215
|
+
|
|
216
|
+
### Main grid
|
|
217
|
+
<Canvas of={Stories.Main} />
|
|
218
|
+
|
|
219
|
+
### Page grid
|
|
220
|
+
<Canvas of={Stories.Page} />
|
|
221
|
+
|
|
222
|
+
### Funnel grid
|
|
223
|
+
<Canvas of={Stories.Funnel} />
|
|
224
|
+
|
|
225
|
+
### Custom grid
|
|
226
|
+
<Canvas of={Stories.Custom} />
|
|
227
|
+
|
|
228
|
+
### Centered
|
|
229
|
+
<Canvas of={Stories.Centered} />
|
|
230
|
+
|
|
231
|
+
### Spans
|
|
232
|
+
<Canvas of={Stories.Spans} />
|
|
233
|
+
|
|
234
|
+
### Start / end (escape hatch)
|
|
235
|
+
<Canvas of={Stories.StartEnd} />
|
|
236
|
+
|
|
237
|
+
### Complex layout
|
|
238
|
+
<Canvas of={Stories.ComplexLayout} />
|
|
239
|
+
|
|
240
|
+
### Masonry
|
|
241
|
+
<Canvas of={Stories.Masonry} />
|
|
242
|
+
|
|
243
|
+
### Masonry with spans
|
|
244
|
+
<Canvas of={Stories.MasonryWithSpans} />
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
@layer boostdev.component {
|
|
2
|
+
.grid {
|
|
3
|
+
display: grid;
|
|
4
|
+
gap: var(--grid_gap, var(--bds-grid_gap));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.grid.--main {
|
|
8
|
+
grid-template-columns: var(--bds-grid_template-columns);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.grid.--page {
|
|
12
|
+
grid-template-columns: 1fr;
|
|
13
|
+
grid-column: 1 / -1;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.grid.--funnel {
|
|
17
|
+
grid-template-columns: var(--bds-grid_template-columns);
|
|
18
|
+
max-inline-size: var(--bds-container_max-width--narrow);
|
|
19
|
+
margin-inline: auto;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.grid.--custom {
|
|
23
|
+
grid-template-columns: repeat(var(--grid_columns, 4), minmax(0, 1fr));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.grid.--centered {
|
|
27
|
+
margin-inline: auto;
|
|
28
|
+
max-inline-size: var(--bds-container_max-width);
|
|
29
|
+
padding-inline: var(--bds-container_spacing-inline);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.item {
|
|
33
|
+
min-inline-size: 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/*
|
|
37
|
+
* Masonry (CSS Grid Level 3 — https://www.w3.org/TR/css-grid-3/).
|
|
38
|
+
* Progressive enhancement: when a browser supports the native grid-lanes
|
|
39
|
+
* display value or the `masonry` keyword for grid-template-rows, it handles
|
|
40
|
+
* layout natively. Otherwise the JS polyfill in ./masonry.ts assigns each
|
|
41
|
+
* item a `grid-row: span N` based on its measured height — items stay in
|
|
42
|
+
* normal grid flow, so column tracks and spans resolve through CSS as usual.
|
|
43
|
+
*/
|
|
44
|
+
@supports (grid-template-rows: masonry) {
|
|
45
|
+
.grid.--masonry {
|
|
46
|
+
/* stylelint-disable-next-line declaration-property-value-no-unknown --
|
|
47
|
+
`masonry` is an experimental CSS Grid Level 3 value; stylelint's
|
|
48
|
+
built-in value list hasn't caught up. This rule is inside @supports
|
|
49
|
+
so non-supporting browsers never parse it. */
|
|
50
|
+
grid-template-rows: masonry;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@supports (display: grid-lanes) {
|
|
55
|
+
.grid.--masonry {
|
|
56
|
+
display: grid-lanes;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|