Merge branch 'main' into sudev-scroller-lottie

This commit is contained in:
Sudev Kiyada 2026-01-12 16:10:33 +05:30 committed by GitHub
commit c80392742a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 238 additions and 60 deletions

View file

@ -12,3 +12,18 @@ jobs:
with: with:
node_version: '20' node_version: '20'
publish_docs: true publish_docs: true
notify-downstream:
needs: release
if: needs.release.outputs.published == 'true'
runs-on: ubuntu-latest
steps:
- name: Wait for npm propagation
run: sleep 30
- name: Dispatch to bluprint_graphics-kit
uses: peter-evans/repository-dispatch@v4
with:
token: ${{ secrets.REPO_PAT_TOKEN }}
repository: reuters-graphics/bluprint_graphics-kit
event-type: dependency-updated

View file

@ -1,5 +1,47 @@
# @reuters-graphics/graphics-components # @reuters-graphics/graphics-components
## 3.0.25
### Patch Changes
- 257f967: Updates svelte-fa version
## 3.0.24
### Patch Changes
- 0fce4cd: Removes dev from $app/environment
## 3.0.23
### Patch Changes
- 022d0dc: Test downstream notification workflow with updated reusable workflow
## 3.0.22
### Patch Changes
- bf550d5: Test downstream workflow notification system
## 3.0.21
### Patch Changes
- a2e6e8d: Fixes a bug in PhotoPack that on earlier iPhones would break. Also adds smarter default layouts based on the number of images in the pack and the max width of the PhotoPack.
## 3.0.20
### Patch Changes
- 6d5c152: Removes stray Google Analytics loading call so GA is only loaded via Google Tag Manager.
## 3.0.19
### Patch Changes
- 627f267: Enhances progress reactivity for ScrollerVideo
## 3.0.18 ## 3.0.18
### Patch Changes ### Patch Changes

View file

@ -1,6 +1,6 @@
{ {
"name": "@reuters-graphics/graphics-components", "name": "@reuters-graphics/graphics-components",
"version": "3.0.18", "version": "3.0.25",
"type": "module", "type": "module",
"private": false, "private": false,
"homepage": "https://reuters-graphics.github.io/graphics-components", "homepage": "https://reuters-graphics.github.io/graphics-components",
@ -109,7 +109,7 @@
"pym.js": "^1.3.2", "pym.js": "^1.3.2",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"storybook-addon-rtl": "^1.1.0", "storybook-addon-rtl": "^1.1.0",
"svelte-fa": "^4.0.3", "svelte-fa": "^4.0.4",
"svelte-intersection-observer": "^1.0.0", "svelte-intersection-observer": "^1.0.0",
"ua-parser-js": "^2.0.3", "ua-parser-js": "^2.0.3",
"vitest": "^3.2.4" "vitest": "^3.2.4"

View file

@ -48,8 +48,8 @@ importers:
specifier: ^1.1.0 specifier: ^1.1.0
version: 1.1.0 version: 1.1.0
svelte-fa: svelte-fa:
specifier: ^4.0.3 specifier: ^4.0.4
version: 4.0.3(svelte@5.28.1) version: 4.0.4(svelte@5.28.1)
svelte-intersection-observer: svelte-intersection-observer:
specifier: ^1.0.0 specifier: ^1.0.0
version: 1.0.0 version: 1.0.0
@ -3800,8 +3800,8 @@ packages:
svelte: svelte:
optional: true optional: true
svelte-fa@4.0.3: svelte-fa@4.0.4:
resolution: {integrity: sha512-saZ8yACM0k9Aexey+2NXU1W0MBosU5lBsRgqFCJKM+Taw7d0HyimPaPAjmvY/Xkyi3UwEYL/Sdu1IZJv/p0Flw==} resolution: {integrity: sha512-85BomCGkTrH8kPDGvb8JrVwq9CqR9foprbKjxemP4Dtg3zPR7OXj5hD0xVYK0C+UCzFI1zooLoK/ndIX6aYXAw==}
peerDependencies: peerDependencies:
svelte: ^4.0.0 || ^5.0.0 svelte: ^4.0.0 || ^5.0.0
@ -8564,7 +8564,7 @@ snapshots:
optionalDependencies: optionalDependencies:
svelte: 5.28.1 svelte: 5.28.1
svelte-fa@4.0.3(svelte@5.28.1): svelte-fa@4.0.4(svelte@5.28.1):
dependencies: dependencies:
svelte: 5.28.1 svelte: 5.28.1

View file

@ -1,6 +1,3 @@
// Reuters Google Tag ID
const GOOGLE_TAG_ID = 'G-WBSR7WLTGD';
export default () => { export default () => {
try { try {
window.dataLayer = window.dataLayer || []; window.dataLayer = window.dataLayer || [];
@ -11,10 +8,6 @@ export default () => {
window.dataLayer.push(arguments); window.dataLayer.push(arguments);
}; };
window.gtag('js', new Date()); window.gtag('js', new Date());
// config event registers a pageview by default
window.gtag('config', GOOGLE_TAG_ID, {
send_page_view: false,
});
registerPageview(); registerPageview();
} }
} catch (e) { } catch (e) {

View file

@ -8,9 +8,22 @@ import * as PhotoPackStories from './PhotoPack.stories.svelte';
The `PhotoPack` component makes simple photo grids with custom layouts at various breakpoints. The `PhotoPack` component makes simple photo grids with custom layouts at various breakpoints.
`images` are defined with their src, alt text, captions and an optional `maxHeight`, which ensures that the images are no taller than that height in any layout. `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.
`layouts` describe how images will be laid out at different breakpoints. The default layout is one photo per row, stacked vertically -- i.e. mobile layout. 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: ```javascript
const images = [
{
src: 'https://...',
altText: 'Alt text',
caption: 'Lorem ipsum. REUTERS/Photog',
// Optional max-height of images across all layouts
maxHeight: 800,
},
// ...
];
```
`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:
```javascript ```javascript
const layouts = [ const layouts = [
@ -23,6 +36,8 @@ const layouts = [
... 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. ... 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.
If you don't specify any layouts, the component will use a default responsive layout based on the number of images in your pack.
You can define as many layouts for as many images as you like. You can define as many layouts for as many images as you like.
```svelte ```svelte
@ -123,3 +138,40 @@ gap: 10 # Optional; must be a number.
``` ```
<Canvas of={PhotoPackStories.ArchieML} /> <Canvas of={PhotoPackStories.ArchieML} />
## Smart default layouts
If you don't specify the `layouts` prop, `PhotoPack` will automatically generate responsive layouts based on the number of images and the container width.
**How it works:**
- **Desktop** (1024px+): Number of images per row depends on container width:
- `normal`: max 2 per row
- `wide` / `wider`: max 3 per row
- `widest` / `fluid`: max 4 per row
- **Tablet** (768px+): Always max 2 per row
- **Mobile** (below 768px): 1 per row
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.
**Examples:**
- 5 images, `wide` container, desktop: `[2, 3]` (2 in first row, 3 in second)
- 7 images, `widest` container, desktop: `[3, 4]` (3 in first row, 4 in second)
- 4 images, any container, desktop: `[2, 2]` (evenly distributed)
```svelte
<script>
import { PhotoPack } from '@reuters-graphics/graphics-components';
const images = [
{ src: `${assets}/image1.jpg`, altText: 'Photo 1', caption: 'Caption 1' },
{ src: `${assets}/image2.jpg`, altText: 'Photo 2', caption: 'Caption 2' },
{ src: `${assets}/image3.jpg`, altText: 'Photo 3', caption: 'Caption 3' },
{ src: `${assets}/image4.jpg`, altText: 'Photo 4', caption: 'Caption 4' },
];
</script>
<!-- No layouts prop = smart defaults! -->
<PhotoPack {images} width="wide" />
```

View file

@ -19,6 +19,12 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import type { ComponentProps } from 'svelte';
type SmartDefaultsArgs = Omit<ComponentProps<typeof PhotoPack>, 'images'> & {
imageCount: number;
};
const defaultImages = [ const defaultImages = [
{ {
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', 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 @@
{ breakpoint: 750, rows: [1, 3] }, { breakpoint: 750, rows: [1, 3] },
]; ];
const archieMLImages = [ const allImages = [
{ {
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', 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',
caption: caption:
@ -88,7 +94,7 @@
width: 'wide' as const, width: 'wide' as const,
textWidth: 'normal' as const, textWidth: 'normal' as const,
gap: Number('15'), gap: Number('15'),
images: archieMLImages, images: allImages.slice(0, 5),
layouts: [ layouts: [
{ breakpoint: 750, rows: [2, 3] }, { breakpoint: 750, rows: [2, 3] },
{ breakpoint: 450, rows: [1, 2, 2] }, { breakpoint: 450, rows: [1, 2, 2] },
@ -106,3 +112,29 @@
}} }}
/> />
<Story name="ArchieML" args={archieMLBlock} /> <Story name="ArchieML" args={archieMLBlock} />
<Story
name="Smart layouts"
args={{
width: 'wide',
textWidth: 'normal',
// @ts-expect-error - imageCount is a custom arg for this story's template
imageCount: 4,
}}
argTypes={{
// @ts-expect-error - imageCount is a custom arg for this story's template
imageCount: {
control: { type: 'range', min: 2, max: 5, step: 1 },
description:
'Number of images to display (demonstrates smart default layouts)',
},
}}
>
{#snippet children(args)}
{@const { imageCount, ...photoPackProps } = args as SmartDefaultsArgs}
<PhotoPack
{...photoPackProps}
images={allImages.slice(0, imageCount || 4)}
/>
{/snippet}
</Story>

View file

@ -6,7 +6,7 @@
// Utils // Utils
import { random4 } from '../../utils'; import { random4 } from '../../utils';
import { groupRows } from './utils'; import { groupRows, generateDefaultLayouts } from './utils';
// Types // Types
export interface Image { export interface Image {
@ -33,7 +33,7 @@
/** Add an ID to target with SCSS. Should be unique from all other elements. */ /** Add an ID to target with SCSS. Should be unique from all other elements. */
id?: string; id?: string;
/** Add a class to target with SCSS. */ /** Add a class to target with SCSS. */
class: string; class?: string;
/** Width of the component within the text well: 'normal' | 'wide' | 'wider' | 'widest' | 'fluid' */ /** Width of the component within the text well: 'normal' | 'wide' | 'wider' | 'widest' | 'fluid' */
width: ContainerWidth; width: ContainerWidth;
/** Set a different width for captions within the text well. For example, "normal" to keep captions inline with the rest of the text well. /** 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 @@
* *
* @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 * @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
* *
* So, we need to use `toSorted` instead. * 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.
*
* If no layouts are provided, we generate smart defaults based on the container width and number of images.
*/ */
let sortedLayouts = $derived( let sortedLayouts = $derived(
layouts?.toSorted((a, b) => (a.breakpoint < b.breakpoint ? 1 : -1)) layouts ?
[...layouts].sort((a, b) => (a.breakpoint < b.breakpoint ? 1 : -1))
: generateDefaultLayouts(images.length, width)
); );
let layout = $derived( let layout = $derived(

View file

@ -1,5 +1,81 @@
import type { Image, Layout } from './PhotoPack.svelte'; import type { Image, Layout } from './PhotoPack.svelte';
// Breakpoint constants for smart default layouts
export const DESKTOP_BREAKPOINT = 1024;
export const TABLET_BREAKPOINT = 768;
/**
* Generates a smart layout for a given number of images with bottom-heavy distribution.
* Avoids single-image rows by redistributing when necessary.
*
* @param imageCount - Total number of images
* @param maxPerRow - Maximum images per row
* @param breakpoint - Breakpoint threshold for this layout
* @returns Layout object with rows array
*/
export const generateSmartLayout = (
imageCount: number,
maxPerRow: number,
breakpoint: number
): Layout => {
// Handle edge cases
if (imageCount === 0) return { breakpoint, rows: [] };
if (imageCount === 1) return { breakpoint, rows: [1] };
const fullRows = Math.floor(imageCount / maxPerRow);
const remainder = imageCount % maxPerRow;
let rows: number[] = [];
if (remainder === 0) {
// Perfect division: all rows have maxPerRow
rows = Array(fullRows).fill(maxPerRow);
} else {
// Bottom-heavy: smaller row at top, larger rows below
// This makes early images larger (fewer per row = bigger display size)
rows = [remainder, ...Array(fullRows).fill(maxPerRow)];
}
return {
breakpoint,
rows,
};
};
type ContainerWidth = 'normal' | 'wide' | 'wider' | 'widest' | 'fluid';
/**
* Generates smart default layouts for desktop and tablet breakpoints.
* Mobile (below TABLET_BREAKPOINT) automatically shows 1 image per row.
*
* Max images per row by container width:
* - normal: 2
* - wide/wider: 3
* - widest/fluid: 4
*
* @param imageCount - Total number of images
* @param width - Container width setting
* @returns Array of 2 layouts [desktop, tablet]
*/
export const generateDefaultLayouts = (
imageCount: number,
width: ContainerWidth
): Layout[] => {
// Map container width to max images per row for desktop
const desktopMaxPerRow =
width === 'normal' ? 2
: width === 'widest' || width === 'fluid' ? 4
: 3;
// Tablet always uses max 2 per row
const tabletMaxPerRow = 2;
return [
generateSmartLayout(imageCount, desktopMaxPerRow, DESKTOP_BREAKPOINT),
generateSmartLayout(imageCount, tabletMaxPerRow, TABLET_BREAKPOINT),
];
};
export const groupRows = (images: Image[], layout?: Layout) => { export const groupRows = (images: Image[], layout?: Layout) => {
// Default layout, one img per row // Default layout, one img per row
if (!layout) return images.map((img) => [img]); if (!layout) return images.map((img) => [img]);

View file

@ -4,7 +4,6 @@
import Debug from './Debug.svelte'; import Debug from './Debug.svelte';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import { setContext } from 'svelte'; import { setContext } from 'svelte';
import { dev } from '$app/environment';
import { Tween } from 'svelte/motion'; import { Tween } from 'svelte/motion';
interface Props { interface Props {
@ -202,8 +201,7 @@
) { ) {
scrollerVideo.setVideoPercentage(videoPercentage, { scrollerVideo.setVideoPercentage(videoPercentage, {
jump: false, jump: false,
easing: (t) => t, transitionSpeed: restProps.transitionSpeed || 8,
transitionSpeed: restProps.transitionSpeed,
}); });
} }
} }
@ -233,7 +231,7 @@
<!-- renders Debug component and children foregrounds --> <!-- renders Debug component and children foregrounds -->
{#snippet supportingElements()} {#snippet supportingElements()}
{#if scrollerVideo} {#if scrollerVideo}
{#if showDebugInfo && dev} {#if showDebugInfo}
<div class="debug-info"> <div class="debug-info">
<Debug componentState={scrollerVideo.componentState} /> <Debug componentState={scrollerVideo.componentState} />
</div> </div>

View file

@ -63,40 +63,6 @@
<style lang="scss"> <style lang="scss">
@use '../../../scss/mixins' as mixins; @use '../../../scss/mixins' as mixins;
// svelte-scroller-background
#progress-bar {
background-color: rgba(0, 0, 0, 0.8);
position: absolute;
z-index: 4;
right: 0;
padding: 1rem;
top: 0;
progress {
height: 6px;
background-color: #ff000044; /* Background color of the entire bar */
margin: 0;
}
progress::-webkit-progress-value {
background-color: white;
border-radius: 10px;
}
progress::-webkit-progress-bar {
background-color: #444444;
border-radius: 10px;
}
p {
font-family: var(--theme-font-family-sans-serif);
color: white;
font-size: var(--theme-font-size-xs);
padding: 0;
margin: 0;
}
}
.step-foreground-container { .step-foreground-container {
height: 100lvh; height: 100lvh;
width: 50%; width: 50%;