migrated, added docs, adds assets prop

This commit is contained in:
MinamiFunakoshiTR 2025-03-20 15:17:52 -07:00
parent f9cce65f66
commit 81aedad0b6
Failed to extract signature
8 changed files with 275 additions and 212 deletions

View file

@ -48,3 +48,22 @@ export interface ScrollerStep {
*/
foregroundProps?: object;
}
export interface PhotoCarouselImage {
/**
* Image source
*/
src: string;
/**
* Image alt text
*/
altText: string;
/** Optional caption */
caption?: string;
/** Optional credit */
credit?: string;
/** Optional object-fit rule */
objectFit?: string;
/** Optional object-position rule */
objectPosition?: string;
}

View file

@ -30,10 +30,10 @@ Venison shoulder *ham hock ham leberkas*. Flank beef ribs fatback, jerky meatbal
## Using with ArchieML docs
With the graphics kit, you'll likely get your text value from an ArchieML doc.
With the Graphics Kit, you'll likely get your text value from an ArchieML doc...
```yaml
# Archie ML doc
# ArchieML doc
[blocks]
type: text
@ -49,6 +49,11 @@ text: Bacon ipsum ...
```svelte
<!-- App.svelte -->
<script>
import { BodyText } from '@reuters-graphics/graphics-components';
import content from '$locales/en/content.json';
</script>
{#each content.blocks as block}
{#if block.type === 'text'}
<BodyText text={block.text} />

View file

@ -0,0 +1,125 @@
import { Meta, Canvas } from '@storybook/blocks';
import * as PhotoCarouselStories from './PhotoCarousel.stories.svelte';
<Meta of={PhotoCarouselStories} />
# PhotoCarousel
The `PhotoCarousel` component creates a simple and accessible photo carousel with built-in lazy-loading and mobile swipe.
```svelte
<script>
import { PhotoCarousel } from '@reuters-graphics/graphics-components';
const photos = [
{
src: 'https://.../myImage.jpg',
altText: 'A picture of...',
caption: 'My caption...', // Optional
credit: 'REUTERS/Jane Doe', // Optional
objectFit: 'contain', // Optional
objectPosition: '50% 50%', // Optional
},
];
</script>
<PhotoCarousel {photos} />
```
<Canvas of={PhotoCarouselStories.Demo} />
## Using with ArchieML docs
With the Graphics Kit, you'll likely get your text value from an ArchieML doc...
```yaml
# ArchieML doc
[blocks]
type: photo-carousel
# List of photo data
[.photos]
# Photo 1
src: 'images/myImage.jpg', # The source path can be a URL or a local path
altText: 'A picture of...',
caption: 'My caption...', // Optional
credit: 'REUTERS/Jane Doe', // Optional
objectFit: 'contain', // Optional
objectPosition: '50% 50%', // Optional
# Photo 2
src: 'images/myImage2.jpg',
altText: 'A picture of...',
caption: 'My caption...', // Optional
credit: 'REUTERS/Jane Doe', // Optional
objectFit: 'contain', // Optional
objectPosition: '50% 50%', // Optional
...
[]
[]
```
... which you'll parse out of a ArchieML block object before passing to the `PhotoCarousel` component.
> **Important❗:** If you're using the Graphics Kit and your photos are saved locally in `src/statics` instead of being on the web, pass the Svelte-kit `assets` string to `PhotoCarousel` to get the correct path. You can't mix and match, though -- all photos must be either local or web-hosted.
```svelte
<!-- App.svelte -->
<script>
import { PhotoCarousel } from '@reuters-graphics/graphics-components';
import { assets } from '$app/paths'; // 👈 If using in the Graphics Kit...
import content from '$locales/en/content.json';
</script>
{#each content.blocks as block}
{#if block.type === 'photo-carousel'}
<!-- Pass `assets` if using photos saved in your Graphics Kit project folder -->
<PhotoCarousel {block.photos} {assets} />
{/if}
{/each}
```
<Canvas of={PhotoCarouselStories.Demo} />
## Custom text
To customise the credit and/or caption style, use the `credit` and `caption` [snippets](https://svelte.dev/docs/svelte/snippet) and pass `photo` as an argument.
```svelte
<PhotoCarousel {photos}>
<!-- Pass `photo` and use the `photo.credit` string in the HTML -->
{#snippet credit(photo)}
<p class="custom-credit">{photo.credit}</p>
{/snippet}
<!-- Pass `photo` and use the `photo.caption` string in the HTML -->
{#snippet caption(photo)}
<p class="custom-caption">{photo.caption}</p>
{/snippet}
</PhotoCarousel>
<!-- Customise credit and caption styles -->
<style lang="scss">
p {
position: absolute;
color: white;
background-color: rgba(0, 0, 0, 0.6);
font-family: sans-serif;
font-size: 0.8rem;
padding: 0 5px;
&.custom-credit {
top: 0;
right: 0;
}
&.custom-caption {
bottom: 5px;
left: 0;
}
}
</style>
```
<Canvas of={PhotoCarouselStories.CustomText} />

View file

@ -1,68 +1,41 @@
<script module lang="ts">
// @ts-ignore raw
import componentDocs from './stories/docs/component.md?raw';
// @ts-ignore raw
import customDocs from './stories/docs/withCustom.md?raw';
import { defineMeta } from '@storybook/addon-svelte-csf';
import PhotoCarousel from './PhotoCarousel.svelte';
import { withComponentDocs, withStoryDocs } from '$docs/utils/withParams.js';
export const meta = {
const { Story } = defineMeta({
title: 'Components/Multimedia/PhotoCarousel',
component: PhotoCarousel,
...withComponentDocs(componentDocs),
argTypes: {
width: {
control: 'select',
options: ['normal', 'wide', 'wider', 'widest', 'fluid'],
},
},
};
});
</script>
<script>
import { Template, Story } from '@storybook/addon-svelte-csf';
import photosJson from './stories/photos.json';
import photosJson from './demo/photos.json';
// import { PhotoCarouselImage } from '../@types/global';
const photos = photosJson.map((p) => ({ ...p, altText: p.caption }));
</script>
<Template >
{#snippet children({ args })}
<PhotoCarousel {...args} />
{/snippet}
</Template>
<Story
name="Default"
args="{{
width: 'wider',
name="Demo"
args={{
photos,
}}"
}}
/>
<Story
name="Custom credits and captions"
args="{{
width: 'wider',
photos,
}}"
{...withStoryDocs(customDocs)}
>
<PhotoCarousel
{...{
width: 'wider',
photos,
}}
>
{#snippet credit({ credit })}
<p class="custom-credit" >{credit}</p>
{/snippet}
{#snippet caption({ caption })}
<p class="custom-caption" >{caption}</p>
{/snippet}
<Story name="Custom text" exportName="CustomText">
<PhotoCarousel {photos}>
{#snippet credit(photo)}
<p class="custom-credit">{photo.credit}</p>
{/snippet}
{#snippet caption(photo)}
<p class="custom-caption">{photo.caption}</p>
{/snippet}
</PhotoCarousel>
</Story>

View file

@ -1,108 +1,90 @@
<!-- @migration-task Error while migrating Svelte code: Cannot set properties of undefined (setting 'next') -->
<!-- @component `PhotoCarousel` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-multimedia-photocarousel--docs) -->
<script lang="ts">
type ContainerWidth = 'normal' | 'wide' | 'wider' | 'widest' | 'fluid';
/** Width of the component within the text well. */
export let width: ContainerWidth = 'wider';
/**
* Set a different width for captions within the text well, for example,
* "normal" to keep captions inline with the rest of the text well.
* Can't ever be wider than `width`.
* @type {string}
*/
export let textWidth: ContainerWidth = 'normal';
/** Add an ID to target with SCSS. */
export let id: string = '';
/** Add a class to target with SCSS. */
let cls: string = '';
export { cls as class };
interface Image {
/**
* Image src
* @required
*/
src: string;
/**
* Image alt-text
* @required
*/
altText: string;
/** Optional caption */
caption?: string;
/** Optional credit */
credit?: string;
/** Optional object-fit rule */
objectFit?: string;
/** Optional object-position rule */
objectPosition?: string;
}
/**
* Array of photos.
* @required
*/
export let photos: Image[] = [];
/**
* Max height of the carousel
*/
export let maxHeight: number = 660;
type ObjectFit = 'cover' | 'contain';
/**
* Default Image object-fit style, either `cover` or `contain`.
*/
export let defaultImageObjectFit: ObjectFit = 'cover';
/**
* Default image object-position style, e.g., `center center` or `50% 50%`.
*/
export let defaultImageObjectPosition: string = 'center center';
/**
* ARIA label for the carousel.
* @required
*/
export let carouselAriaLabel: string = 'Photo gallery';
/**
* Set height of the carousel as a ratio of its width
* as long as its below whatever you set in `maxHeight`.
*/
export let heightRatio: number = 0.68;
/**
* Number of images to preload ahead of the active image.
*/
export let preloadImages: number = 1;
import Block from '../Block/Block.svelte';
import { Splide, SplideSlide, SplideTrack } from '@splidejs/svelte-splide';
// Utils
import '@splidejs/svelte-splide/css/core';
// @ts-ignore no types
import { fly } from 'svelte/transition';
import { Splide, SplideSlide, SplideTrack } from '@splidejs/svelte-splide';
// Icons
import Fa from 'svelte-fa/src/fa.svelte';
import {
faChevronLeft,
faChevronRight,
} from '@fortawesome/free-solid-svg-icons';
import { fly } from 'svelte/transition';
// Components
import Block from '../Block/Block.svelte';
import PaddingReset from '../PaddingReset/PaddingReset.svelte';
// Types
import type { MoveEventDetail } from '@splidejs/svelte-splide/types';
import type { Snippet } from 'svelte';
import type { PhotoCarouselImage } from '../@types/global';
import src from 'svelte-search/index.js';
let containerWidth: number;
type ContainerWidth = 'normal' | 'wide' | 'wider' | 'widest' | 'fluid';
let activeImageIndex = 0;
type ObjectFit = 'cover' | 'contain';
$: carouselHeight =
interface Props {
/** Array of photos. */
photos: PhotoCarouselImage[];
/** Pass Svelte-kit `assets`, if using local photo files instead of web-hosted ones*/
assets?: string;
/** Width of the component within the text well: normal, wide, wider, widest, fluid */
width?: ContainerWidth;
/**
* Set a different width for captions within the text well, e.g. "normal," to keep captions inline with the rest of the text well. Can't ever be wider than `width`.
*/
textWidth?: ContainerWidth;
/** Add an ID to target with SCSS. */
id?: string;
/** Add a class to target with SCSS. */
cls?: string;
/** Max height of the carousel */
maxHeight?: number;
/** Default Image object-fit style: cover, contain */
defaultImageObjectFit?: ObjectFit;
/** Default image object-position style, e.g., `center center` or `50% 50%`. */
defaultImageObjectPosition?: string;
/** ARIA label for the carousel. */
carouselAriaLabel?: string;
/** Set height of the carousel as a ratio of its width as long as its below whatever you set in `maxHeight`. */
heightRatio?: number;
/** Number of images to preload ahead of the active image. */
preloadImages?: number;
/** Optional custom credit format as a snippet, which takes the argument `photo`. */
credit?: Snippet<[PhotoCarouselImage]>;
/** Optional custom caption format as a snippet, which takes the argument `photo`. */
caption?: Snippet<[PhotoCarouselImage]>;
}
let {
assets,
width = 'wider',
textWidth = 'normal',
id = '',
cls = '',
photos,
maxHeight = 660,
defaultImageObjectFit = 'cover',
defaultImageObjectPosition = 'center center',
carouselAriaLabel = 'Photo gallery',
heightRatio = 0.68,
preloadImages = 1,
credit,
caption,
}: Props = $props();
let containerWidth: number | undefined = $state(undefined);
let activeImageIndex = $state(0);
// Derive carousel height based on container width
let carouselHeight = $derived(
containerWidth ?
Math.min(containerWidth * heightRatio, maxHeight)
: maxHeight;
: maxHeight
);
const handleActiveChange = (e?: CustomEvent<MoveEventDetail>) => {
if (!e) return;
@ -111,21 +93,23 @@
</script>
<Block {width} {id} class="photo-carousel fmy-6 {cls}">
<div class="carousel-container" bind:clientWidth="{containerWidth}">
<div class="carousel-container" bind:clientWidth={containerWidth}>
<Splide
hasTrack="{false}"
options="{{
hasTrack={false}
options={{
height: carouselHeight,
fixedHeight: carouselHeight,
lazyLoad: 'nearby',
preloadPages: preloadImages,
}}"
aria-label="{carouselAriaLabel}"
on:move="{handleActiveChange}"
}}
aria-label={carouselAriaLabel}
on:move={handleActiveChange}
>
<div class="image-container">
<SplideTrack>
{#each photos as photo}
<!-- Set source path if `assets` is passed, i.e the photos are local files -->
{@const src = assets ? `${assets}/${photo.src}` : photo.src}
<SplideSlide>
<div class="photo-slide w-full h-full relative">
<figure
@ -134,20 +118,22 @@
>
<img
class="w-full h-full fmy-0"
data-splide-lazy="{photo.src}"
alt="{photo.altText}"
style:object-fit="{photo.objectFit ||
defaultImageObjectFit}"
style:object-position="{photo.objectPosition ||
defaultImageObjectPosition}"
data-splide-lazy={src}
alt={photo.altText}
style:object-fit={photo.objectFit || defaultImageObjectFit}
style:object-position={photo.objectPosition ||
defaultImageObjectPosition}
/>
{#if $$slots.credit}
<slot name="credit" credit="{photo.credit}" />
{:else}
<!-- Render custom credit if credit snippet and string both exist -->
{#if credit && photo.credit}
{@render credit(photo)}
<!-- Otherwise, render with default credit style -->
{:else if photo.credit}
<span
class="credit absolute fmb-1 fml-1 leading-tighter font-note text-xxs"
class:contain-fit="{photo.objectFit === 'contain' ||
defaultImageObjectFit === 'contain'}"
class:contain-fit={photo.objectFit === 'contain' ||
defaultImageObjectFit === 'contain'}
>{photo.credit}</span
>
{/if}
@ -158,20 +144,23 @@
</SplideTrack>
{#if photos[activeImageIndex].caption}
<PaddingReset containerIsFluid="{width === 'fluid'}">
<Block width="{textWidth}">
{#if $$slots.caption}
<slot
name="caption"
caption="{photos[activeImageIndex].caption}"
/>
{:else}
{@const activePhoto = photos[activeImageIndex]}
<PaddingReset containerIsFluid={width === 'fluid'}>
<Block width={textWidth}>
<!-- Render custom caption if caption snippet and string both exist -->
{#if caption && activePhoto.caption}
{#key activeImageIndex}
{@render caption(activePhoto)}
{/key}
<!-- Otherwise, render with default caption style -->
{:else if activePhoto.caption}
{#key activeImageIndex}
<p
class="caption body-caption text-center"
in:fly|local="{{ x: 20, duration: 175 }}"
in:fly|local={{ x: 20, duration: 175 }}
>
{photos[activeImageIndex].caption}
{activePhoto.caption}
</p>
{/key}
{/if}
@ -185,10 +174,10 @@
<div class="splide__arrows fp-1">
<button class="splide__arrow splide__arrow--prev">
<Fa icon="{faChevronLeft}" fw />
<Fa icon={faChevronLeft} fw />
</button>
<button class="splide__arrow splide__arrow--next">
<Fa icon="{faChevronRight}" fw />
<Fa icon={faChevronRight} fw />
</button>
</div>
</div>

View file

@ -1,21 +0,0 @@
A simple, accessible photo carousel with lazy-loading and mobile swipe built in.
```svelte
<script>
import { PhotoCarousel } from '@reuters-graphics/graphics-components';
const photos = [
{
src: 'https://.../myImage.jpg',
altText: 'A picture of...',
caption: 'My caption...', // Optional
credit: 'REUTERS/Jane Doe', // Optional
objectFit: 'contain', // Optional
objectPosition: '50% 50%', // Optional
},
// ...
];
</script>
<PhotoCarousel photos="{photos}" />
```

View file

@ -1,27 +0,0 @@
Use named slots to style your own custom credits and/or captions.
```svelte
<PhotoCarousel photos="{photos}">
<p slot="credit" class="custom-credit" let:credit>{credit}</p>
<p slot="caption" class="custom-caption" let:caption>{caption}</p>
</PhotoCarousel>
<style lang="scss">
p {
position: absolute;
color: white;
background-color: rgba(0, 0, 0, 0.6);
font-family: sans-serif;
font-size: 0.8rem;
padding: 0 5px;
&.custom-credit {
top: 0;
right: 0;
}
&.custom-caption {
bottom: 5px;
left: 0;
}
}
</style>
```