@reuters-graphics/graphics-components 3.0.20 → 3.0.21
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/dist/components/PhotoPack/PhotoPack.mdx +54 -2
- package/dist/components/PhotoPack/PhotoPack.stories.svelte +34 -2
- package/dist/components/PhotoPack/PhotoPack.svelte +8 -4
- package/dist/components/PhotoPack/PhotoPack.svelte.d.ts +1 -1
- package/dist/components/PhotoPack/utils.d.ts +28 -0
- package/dist/components/PhotoPack/utils.js +60 -0
- package/package.json +1 -1
|
@@ -8,9 +8,22 @@ import * as PhotoPackStories from './PhotoPack.stories.svelte';
|
|
|
8
8
|
|
|
9
9
|
The `PhotoPack` component makes simple photo grids with custom layouts at various breakpoints.
|
|
10
10
|
|
|
11
|
-
`images` are defined with their src, alt text, captions and an optional `maxHeight`, which ensures that
|
|
11
|
+
`images` are defined with their src, alt text, captions and an optional `maxHeight`, which ensures that an image is no taller than that height in any layout.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
```javascript
|
|
14
|
+
const images = [
|
|
15
|
+
{
|
|
16
|
+
src: 'https://...',
|
|
17
|
+
altText: 'Alt text',
|
|
18
|
+
caption: 'Lorem ipsum. REUTERS/Photog',
|
|
19
|
+
// Optional max-height of images across all layouts
|
|
20
|
+
maxHeight: 800,
|
|
21
|
+
},
|
|
22
|
+
// ...
|
|
23
|
+
];
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`layouts` optionally define how images are laid out at different breakpoints. You can customise the layouts and group images into `rows` above a certain `breakpoint` by specifying the number of images that should go in that row. For example:
|
|
14
27
|
|
|
15
28
|
```javascript
|
|
16
29
|
const layouts = [
|
|
@@ -23,6 +36,8 @@ const layouts = [
|
|
|
23
36
|
|
|
24
37
|
... tells the component that when the `PhotoPack` container is 450 pixels or wider, it should group the 4 images in 3 rows: 1 in the first, 2 in the second and 1 in the last.
|
|
25
38
|
|
|
39
|
+
If you don't specify any layouts, the component will use a default responsive layout based on the number of images in your pack.
|
|
40
|
+
|
|
26
41
|
You can define as many layouts for as many images as you like.
|
|
27
42
|
|
|
28
43
|
```svelte
|
|
@@ -123,3 +138,40 @@ gap: 10 # Optional; must be a number.
|
|
|
123
138
|
```
|
|
124
139
|
|
|
125
140
|
<Canvas of={PhotoPackStories.ArchieML} />
|
|
141
|
+
|
|
142
|
+
## Smart default layouts
|
|
143
|
+
|
|
144
|
+
If you don't specify the `layouts` prop, `PhotoPack` will automatically generate responsive layouts based on the number of images and the container width.
|
|
145
|
+
|
|
146
|
+
**How it works:**
|
|
147
|
+
|
|
148
|
+
- **Desktop** (1024px+): Number of images per row depends on container width:
|
|
149
|
+
- `normal`: max 2 per row
|
|
150
|
+
- `wide` / `wider`: max 3 per row
|
|
151
|
+
- `widest` / `fluid`: max 4 per row
|
|
152
|
+
- **Tablet** (768px+): Always max 2 per row
|
|
153
|
+
- **Mobile** (below 768px): 1 per row
|
|
154
|
+
|
|
155
|
+
The smart defaults use a **bottom-heavy distribution**, meaning earlier rows have fewer images (making them larger and more prominent), while later rows have more images.
|
|
156
|
+
|
|
157
|
+
**Examples:**
|
|
158
|
+
|
|
159
|
+
- 5 images, `wide` container, desktop: `[2, 3]` (2 in first row, 3 in second)
|
|
160
|
+
- 7 images, `widest` container, desktop: `[3, 4]` (3 in first row, 4 in second)
|
|
161
|
+
- 4 images, any container, desktop: `[2, 2]` (evenly distributed)
|
|
162
|
+
|
|
163
|
+
```svelte
|
|
164
|
+
<script>
|
|
165
|
+
import { PhotoPack } from '@reuters-graphics/graphics-components';
|
|
166
|
+
|
|
167
|
+
const images = [
|
|
168
|
+
{ src: `${assets}/image1.jpg`, altText: 'Photo 1', caption: 'Caption 1' },
|
|
169
|
+
{ src: `${assets}/image2.jpg`, altText: 'Photo 2', caption: 'Caption 2' },
|
|
170
|
+
{ src: `${assets}/image3.jpg`, altText: 'Photo 3', caption: 'Caption 3' },
|
|
171
|
+
{ src: `${assets}/image4.jpg`, altText: 'Photo 4', caption: 'Caption 4' },
|
|
172
|
+
];
|
|
173
|
+
</script>
|
|
174
|
+
|
|
175
|
+
<!-- No layouts prop = smart defaults! -->
|
|
176
|
+
<PhotoPack {images} width="wide" />
|
|
177
|
+
```
|
|
@@ -19,6 +19,12 @@
|
|
|
19
19
|
</script>
|
|
20
20
|
|
|
21
21
|
<script lang="ts">
|
|
22
|
+
import type { ComponentProps } from 'svelte';
|
|
23
|
+
|
|
24
|
+
type SmartDefaultsArgs = Omit<ComponentProps<typeof PhotoPack>, 'images'> & {
|
|
25
|
+
imageCount: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
22
28
|
const defaultImages = [
|
|
23
29
|
{
|
|
24
30
|
src: 'https://graphics.thomsonreuters.com/cdn/django-tools/media/graphics-gallery/galleries/world-cup-2022/spain-germany-11-27/2022-11-27T194630Z_544493697_UP1E.jpeg',
|
|
@@ -51,7 +57,7 @@
|
|
|
51
57
|
{ breakpoint: 750, rows: [1, 3] },
|
|
52
58
|
];
|
|
53
59
|
|
|
54
|
-
const
|
|
60
|
+
const allImages = [
|
|
55
61
|
{
|
|
56
62
|
src: 'https://graphics.thomsonreuters.com/cdn/django-tools/media/graphics-gallery/galleries/world-cup-2022/spain-germany-11-27/2022-11-27T194630Z_544493697_UP1E.jpeg',
|
|
57
63
|
caption:
|
|
@@ -88,7 +94,7 @@
|
|
|
88
94
|
width: 'wide' as const,
|
|
89
95
|
textWidth: 'normal' as const,
|
|
90
96
|
gap: Number('15'),
|
|
91
|
-
images:
|
|
97
|
+
images: allImages.slice(0, 5),
|
|
92
98
|
layouts: [
|
|
93
99
|
{ breakpoint: 750, rows: [2, 3] },
|
|
94
100
|
{ breakpoint: 450, rows: [1, 2, 2] },
|
|
@@ -106,3 +112,29 @@
|
|
|
106
112
|
}}
|
|
107
113
|
/>
|
|
108
114
|
<Story name="ArchieML" args={archieMLBlock} />
|
|
115
|
+
|
|
116
|
+
<Story
|
|
117
|
+
name="Smart layouts"
|
|
118
|
+
args={{
|
|
119
|
+
width: 'wide',
|
|
120
|
+
textWidth: 'normal',
|
|
121
|
+
// @ts-expect-error - imageCount is a custom arg for this story's template
|
|
122
|
+
imageCount: 4,
|
|
123
|
+
}}
|
|
124
|
+
argTypes={{
|
|
125
|
+
// @ts-expect-error - imageCount is a custom arg for this story's template
|
|
126
|
+
imageCount: {
|
|
127
|
+
control: { type: 'range', min: 2, max: 5, step: 1 },
|
|
128
|
+
description:
|
|
129
|
+
'Number of images to display (demonstrates smart default layouts)',
|
|
130
|
+
},
|
|
131
|
+
}}
|
|
132
|
+
>
|
|
133
|
+
{#snippet children(args)}
|
|
134
|
+
{@const { imageCount, ...photoPackProps } = args as SmartDefaultsArgs}
|
|
135
|
+
<PhotoPack
|
|
136
|
+
{...photoPackProps}
|
|
137
|
+
images={allImages.slice(0, imageCount || 4)}
|
|
138
|
+
/>
|
|
139
|
+
{/snippet}
|
|
140
|
+
</Story>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
// Utils
|
|
8
8
|
import { random4 } from '../../utils';
|
|
9
|
-
import { groupRows } from './utils';
|
|
9
|
+
import { groupRows, generateDefaultLayouts } from './utils';
|
|
10
10
|
|
|
11
11
|
// Types
|
|
12
12
|
export interface Image {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
/** Add an ID to target with SCSS. Should be unique from all other elements. */
|
|
34
34
|
id?: string;
|
|
35
35
|
/** Add a class to target with SCSS. */
|
|
36
|
-
class
|
|
36
|
+
class?: string;
|
|
37
37
|
/** Width of the component within the text well: 'normal' | 'wide' | 'wider' | 'widest' | 'fluid' */
|
|
38
38
|
width: ContainerWidth;
|
|
39
39
|
/** Set a different width for captions within the text well. For example, "normal" to keep captions inline with the rest of the text well.
|
|
@@ -60,10 +60,14 @@
|
|
|
60
60
|
*
|
|
61
61
|
* @NOTE - We can't use `sort` directly on the array because it mutates the original array; we can't update a state inside a derived expression: https://svelte.dev/docs/svelte/runtime-errors#Client-errors-state_unsafe_mutation
|
|
62
62
|
*
|
|
63
|
-
*
|
|
63
|
+
* We avoid `toSorted` because it's not supported on older iPhones. Instead, we create a shallow copy using the spread operator and then sort that copy.
|
|
64
|
+
*
|
|
65
|
+
* If no layouts are provided, we generate smart defaults based on the container width and number of images.
|
|
64
66
|
*/
|
|
65
67
|
let sortedLayouts = $derived(
|
|
66
|
-
layouts
|
|
68
|
+
layouts ?
|
|
69
|
+
[...layouts].sort((a, b) => (a.breakpoint < b.breakpoint ? 1 : -1))
|
|
70
|
+
: generateDefaultLayouts(images.length, width)
|
|
67
71
|
);
|
|
68
72
|
|
|
69
73
|
let layout = $derived(
|
|
@@ -19,7 +19,7 @@ interface Props {
|
|
|
19
19
|
/** Add an ID to target with SCSS. Should be unique from all other elements. */
|
|
20
20
|
id?: string;
|
|
21
21
|
/** Add a class to target with SCSS. */
|
|
22
|
-
class
|
|
22
|
+
class?: string;
|
|
23
23
|
/** Width of the component within the text well: 'normal' | 'wide' | 'wider' | 'widest' | 'fluid' */
|
|
24
24
|
width: ContainerWidth;
|
|
25
25
|
/** Set a different width for captions within the text well. For example, "normal" to keep captions inline with the rest of the text well.
|
|
@@ -1,2 +1,30 @@
|
|
|
1
1
|
import type { Image, Layout } from './PhotoPack.svelte';
|
|
2
|
+
export declare const DESKTOP_BREAKPOINT = 1024;
|
|
3
|
+
export declare const TABLET_BREAKPOINT = 768;
|
|
4
|
+
/**
|
|
5
|
+
* Generates a smart layout for a given number of images with bottom-heavy distribution.
|
|
6
|
+
* Avoids single-image rows by redistributing when necessary.
|
|
7
|
+
*
|
|
8
|
+
* @param imageCount - Total number of images
|
|
9
|
+
* @param maxPerRow - Maximum images per row
|
|
10
|
+
* @param breakpoint - Breakpoint threshold for this layout
|
|
11
|
+
* @returns Layout object with rows array
|
|
12
|
+
*/
|
|
13
|
+
export declare const generateSmartLayout: (imageCount: number, maxPerRow: number, breakpoint: number) => Layout;
|
|
14
|
+
type ContainerWidth = 'normal' | 'wide' | 'wider' | 'widest' | 'fluid';
|
|
15
|
+
/**
|
|
16
|
+
* Generates smart default layouts for desktop and tablet breakpoints.
|
|
17
|
+
* Mobile (below TABLET_BREAKPOINT) automatically shows 1 image per row.
|
|
18
|
+
*
|
|
19
|
+
* Max images per row by container width:
|
|
20
|
+
* - normal: 2
|
|
21
|
+
* - wide/wider: 3
|
|
22
|
+
* - widest/fluid: 4
|
|
23
|
+
*
|
|
24
|
+
* @param imageCount - Total number of images
|
|
25
|
+
* @param width - Container width setting
|
|
26
|
+
* @returns Array of 2 layouts [desktop, tablet]
|
|
27
|
+
*/
|
|
28
|
+
export declare const generateDefaultLayouts: (imageCount: number, width: ContainerWidth) => Layout[];
|
|
2
29
|
export declare const groupRows: (images: Image[], layout?: Layout) => Image[][];
|
|
30
|
+
export {};
|
|
@@ -1,3 +1,63 @@
|
|
|
1
|
+
// Breakpoint constants for smart default layouts
|
|
2
|
+
export const DESKTOP_BREAKPOINT = 1024;
|
|
3
|
+
export const TABLET_BREAKPOINT = 768;
|
|
4
|
+
/**
|
|
5
|
+
* Generates a smart layout for a given number of images with bottom-heavy distribution.
|
|
6
|
+
* Avoids single-image rows by redistributing when necessary.
|
|
7
|
+
*
|
|
8
|
+
* @param imageCount - Total number of images
|
|
9
|
+
* @param maxPerRow - Maximum images per row
|
|
10
|
+
* @param breakpoint - Breakpoint threshold for this layout
|
|
11
|
+
* @returns Layout object with rows array
|
|
12
|
+
*/
|
|
13
|
+
export const generateSmartLayout = (imageCount, maxPerRow, breakpoint) => {
|
|
14
|
+
// Handle edge cases
|
|
15
|
+
if (imageCount === 0)
|
|
16
|
+
return { breakpoint, rows: [] };
|
|
17
|
+
if (imageCount === 1)
|
|
18
|
+
return { breakpoint, rows: [1] };
|
|
19
|
+
const fullRows = Math.floor(imageCount / maxPerRow);
|
|
20
|
+
const remainder = imageCount % maxPerRow;
|
|
21
|
+
let rows = [];
|
|
22
|
+
if (remainder === 0) {
|
|
23
|
+
// Perfect division: all rows have maxPerRow
|
|
24
|
+
rows = Array(fullRows).fill(maxPerRow);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
// Bottom-heavy: smaller row at top, larger rows below
|
|
28
|
+
// This makes early images larger (fewer per row = bigger display size)
|
|
29
|
+
rows = [remainder, ...Array(fullRows).fill(maxPerRow)];
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
breakpoint,
|
|
33
|
+
rows,
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Generates smart default layouts for desktop and tablet breakpoints.
|
|
38
|
+
* Mobile (below TABLET_BREAKPOINT) automatically shows 1 image per row.
|
|
39
|
+
*
|
|
40
|
+
* Max images per row by container width:
|
|
41
|
+
* - normal: 2
|
|
42
|
+
* - wide/wider: 3
|
|
43
|
+
* - widest/fluid: 4
|
|
44
|
+
*
|
|
45
|
+
* @param imageCount - Total number of images
|
|
46
|
+
* @param width - Container width setting
|
|
47
|
+
* @returns Array of 2 layouts [desktop, tablet]
|
|
48
|
+
*/
|
|
49
|
+
export const generateDefaultLayouts = (imageCount, width) => {
|
|
50
|
+
// Map container width to max images per row for desktop
|
|
51
|
+
const desktopMaxPerRow = width === 'normal' ? 2
|
|
52
|
+
: width === 'widest' || width === 'fluid' ? 4
|
|
53
|
+
: 3;
|
|
54
|
+
// Tablet always uses max 2 per row
|
|
55
|
+
const tabletMaxPerRow = 2;
|
|
56
|
+
return [
|
|
57
|
+
generateSmartLayout(imageCount, desktopMaxPerRow, DESKTOP_BREAKPOINT),
|
|
58
|
+
generateSmartLayout(imageCount, tabletMaxPerRow, TABLET_BREAKPOINT),
|
|
59
|
+
];
|
|
60
|
+
};
|
|
1
61
|
export const groupRows = (images, layout) => {
|
|
2
62
|
// Default layout, one img per row
|
|
3
63
|
if (!layout)
|