Merge branch 'mf-before-after' of https://github.com/reuters-graphics/graphics-components into mf-photo-pack

This commit is contained in:
MinamiFunakoshiTR 2025-03-24 08:35:02 -07:00
commit 894ff8442d
Failed to extract signature
73 changed files with 1455 additions and 1122 deletions

View file

@ -12,7 +12,7 @@
"editor.wordWrap": "on"
},
"[svelte]": {
"editor.defaultFormatter": "svelte.svelte-vscode"
},
"editor.defaultFormatter": "svelte.svelte-vscode"
},
"typescript.tsdk": "node_modules/typescript/lib"
}

View file

@ -110,7 +110,8 @@
"dayjs": "^1.11.13",
"journalize": "^2.6.0",
"lodash-es": "^4.17.21",
"marked": "^4.3.0",
"marked": "^15.0.7",
"marked-smartypants": "^1.1.9",
"proper-url-join": "^2.1.2",
"pym.js": "^1.3.2",
"slugify": "^1.6.6",

View file

@ -33,8 +33,11 @@ importers:
specifier: ^4.17.21
version: 4.17.21
marked:
specifier: ^4.3.0
version: 4.3.0
specifier: ^15.0.7
version: 15.0.7
marked-smartypants:
specifier: ^1.1.9
version: 1.1.9(marked@15.0.7)
proper-url-join:
specifier: ^2.1.2
version: 2.1.2
@ -2968,9 +2971,14 @@ packages:
markdown-table@3.0.4:
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
marked@4.3.0:
resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==}
engines: {node: '>= 12'}
marked-smartypants@1.1.9:
resolution: {integrity: sha512-VPeuaUr5IWptI7nJdgQ9ugrLWYGv13NdzEXTtKY3cmB4aRWOI2RzhLlf+xQp6Wnob9SAPO2sNVlfSJr+nflk/A==}
peerDependencies:
marked: '>=4 <16'
marked@15.0.7:
resolution: {integrity: sha512-dgLIeKGLx5FwziAnsk4ONoGwHwGPJzselimvlVskE9XLN4Orv9u2VA3GWw/lYUqjfA0rUT/6fqKwfZJapP9BEg==}
engines: {node: '>= 18'}
hasBin: true
math-intrinsics@1.1.0:
@ -3909,6 +3917,10 @@ packages:
resolution: {integrity: sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==}
engines: {node: '>=8.0.0'}
smartypants@0.2.2:
resolution: {integrity: sha512-TzobUYoEft/xBtb2voRPryAUIvYguG0V7Tt3de79I1WfXgCwelqVsGuZSnu3GFGRZhXR90AeEYIM+icuB/S06Q==}
hasBin: true
smol-toml@1.3.1:
resolution: {integrity: sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ==}
engines: {node: '>= 18'}
@ -7598,7 +7610,12 @@ snapshots:
markdown-table@3.0.4: {}
marked@4.3.0: {}
marked-smartypants@1.1.9(marked@15.0.7):
dependencies:
marked: 15.0.7
smartypants: 0.2.2
marked@15.0.7: {}
math-intrinsics@1.1.0: {}
@ -8979,6 +8996,8 @@ snapshots:
slugify@1.6.6: {}
smartypants@0.2.2: {}
smol-toml@1.3.1: {}
snake-case@3.0.4:

View file

@ -1,11 +1,11 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
%sveltekit.head%
</head>
<body>
%sveltekit.body%
</body>
</html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
%sveltekit.head%
</head>
<body>
%sveltekit.body%
</body>
</html>

View file

@ -55,7 +55,7 @@
});
</script>
<div data-freestar-ad={dataFreestarAd || null} id={adId}></div>
<div data-freestar-ad="{dataFreestarAd || null}" id="{adId}"></div>
<style>
:global(div.freestar-adslot:has(.unfulfilled-ad)) {

View file

@ -46,11 +46,11 @@ You may add **up to three** inline ads per page, but must set the `n` prop on mu
```svelte
<!-- First inline ad on the page -->
<InlineAd n={1} />
<InlineAd n="{1}" />
<!-- ... second ... -->
<InlineAd n={2} />
<InlineAd n="{2}" />
<!-- ... third and final. -->
<InlineAd n={3} />
<InlineAd n="{3}" />
```
<Canvas of={InlineAdStories.Demo} />

View file

@ -46,7 +46,7 @@
});
</script>
<svelte:window bind:innerWidth={windowWidth} />
<svelte:window bind:innerWidth="{windowWidth}" />
<div
class="freestar-adslot leaderboard__sticky {cls}"

View file

@ -69,7 +69,7 @@
let adType = $derived(getAdType(placementName));
</script>
<svelte:window bind:innerWidth={windowWidth} />
<svelte:window bind:innerWidth="{windowWidth}" />
{#if windowWidth}
{#key placementName}

View file

@ -41,13 +41,13 @@ You can set custom column widths by passing an object to the `columnWidths` prop
```svelte
<!-- Set custom column widths -->
<Article
columnWidths={{
columnWidths="{{
narrower: 310,
narrow: 450,
normal: 550,
wide: 675,
wider: 1400,
}}
}}"
>
<!-- Custom column widths get passed down to the `Block` component -->
<Block width="narrower" />

View file

@ -0,0 +1,111 @@
import { Meta, Canvas } from '@storybook/blocks';
import * as BeforeAfterStories from './BeforeAfter.stories.svelte';
<Meta of={BeforeAfterStories} />
# BeforeAfter
The `BeforeAfter` component shows a before-and-after comparison of an image.
```svelte
<script>
import { BeforeAfter } from '@reuters-graphics/graphics-components';
import { assets } from '$app/paths'; // If using in the Graphics Kit
</script>
<BeforeAfter
beforeSrc={`${assets}/images/before-after/myrne-before.jpg`}
beforeAlt="Satellite image of Russian base at Myrne taken on July 7, 2020."
afterSrc={`${assets}/images/before-after/myrne-after.jpg`}
afterAlt="Satellite image of Russian base at Myrne taken on Oct. 20, 2020."
/>
```
<Canvas of={BeforeAfterStories.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: before-after
beforeSrc: images/before-after/myrne-before.jpg
beforeAlt: Satellite image of Russian base at Myrne taken on July 7, 2020.
afterSrc: images/before-after/myrne-after.jpg
afterAlt: Satellite image of Russian base at Myrne taken on Oct. 20, 2020.
[]
```
... which you'll parse out of a ArchieML block object before passing to the `BeforeAfter` component.
```svelte
<!-- App.svelte -->
{#each content.blocks as block}
{#if block.type === 'before-after'}
<BeforeAfter
beforeSrc={`${assets}/${block.beforeSrc}`}
beforeAlt={block.beforeAlt}
afterSrc={`${assets}/${block.afterSrc}`}
afterAlt={block.afterAlt}
/>
{/if}
{/each}
```
<Canvas of={BeforeAfterStories.Demo} />
## Adding text
To add text overlays and captions, use [snippets](https://svelte.dev/docs/svelte/snippet) for `beforeOverlay`, `afterOverlay` and `caption`. You can style the snippets to match your page design, like in [this demo](./?path=/story/components-multimedia-beforeafter--with-overlays).
> 💡**NOTE:** The text in the overlays are used as [ARIA descriptions](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-describedby) for your before and after images. You must always use the `beforeAlt` / `afterAlt` props to label your image for visually impaired readers, but these ARIA descriptions provide additional information or context that the reader might need.
```svelte
<BeforeAfter
beforeSrc={beforeImg}
beforeAlt="Satellite image of Russian base at Myrne taken on July 7, 2020."
afterSrc={afterImg}
afterAlt="Satellite image of Russian base at Myrne taken on Oct. 20, 2020."
>
<!-- Optional custom text overlay for the before image -->
{#snippet beforeOverlay()}
<div class="overlay p-3 before text-left">
<p class="h4 font-bold">July 7, 2020</p>
<p class="body-note">Initially, this site was far smaller.</p>
</div>
{/snippet}
<!-- Optional custom text overlay for the after image -->
{#snippet afterOverlay()}
<div class="overlay p-3 after text-right">
<p class="h4 font-bold">Oct. 20, 2020</p>
<p class="body-note">But then forces built up.</p>
</div>
{/snippet}
<!-- Optional custom caption for both images -->
{#snippet caption()}
<p class="body-note">Photos by MAXAR Technologies, 2021.</p>
{/snippet}
</BeforeAfter>
<style lang="scss">
.overlay {
background: rgba(0, 0, 0, 0.45);
max-width: 350px;
&.after {
text-align: right;
}
p {
color: #ffffff;
}
}
</style>
```
<Canvas of={BeforeAfterStories.WithText} />

View file

@ -1,19 +1,10 @@
<!-- @migration-task Error while migrating Svelte code: end is out of bounds -->
<script context="module" lang="ts">
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import BeforeAfter from './BeforeAfter.svelte';
// @ts-ignore raw
import componentDocs from './stories/docs/component.md?raw';
// @ts-ignore raw
import withOverlaysDocs from './stories/docs/withOverlays.md?raw';
// @ts-ignore raw
import ariaDescriptionsDocs from './stories/docs/ariaDescriptions.md?raw';
import { withComponentDocs, withStoryDocs } from '$docs/utils/withParams.js';
export const meta = {
title: 'Components/Graphics/BeforeAfter',
const { Story } = defineMeta({
title: 'Components/Multimedia/BeforeAfter',
component: BeforeAfter,
...withComponentDocs(componentDocs),
argTypes: {
handleColour: { control: 'color' },
width: {
@ -21,96 +12,54 @@
options: ['normal', 'wide', 'wider', 'widest', 'fluid'],
},
},
};
});
</script>
<script>
import { Template, Story } from '@storybook/addon-svelte-csf';
// @ts-ignore raw
import beforeImg from './stories/myrne-before.jpg';
// @ts-ignore raw
import afterImg from './stories/myrne-after.jpg';
import beforeImg from './images/myrne-before.jpg';
import afterImg from './images/myrne-after.jpg';
</script>
<Template let:args>
<BeforeAfter {...args} />
</Template>
<Story
name="Default"
args="{{
name="Demo"
args={{
beforeSrc: beforeImg,
beforeAlt:
'Satellite image of Russian base at Myrne taken on July 7, 2020.',
afterSrc: afterImg,
afterAlt:
'Satellite image of Russian base at Myrne taken on Oct. 20, 2020.',
}}"
}}
/>
<Story name="With overlays" {...withStoryDocs(withOverlaysDocs)}>
<Story name="With text" exportName="WithText">
<BeforeAfter
beforeSrc="{beforeImg}"
beforeSrc={beforeImg}
beforeAlt="Satellite image of Russian base at Myrne taken on July 7, 2020."
afterSrc="{afterImg}"
afterSrc={afterImg}
afterAlt="Satellite image of Russian base at Myrne taken on Oct. 20, 2020."
>
<div slot="beforeOverlay" class="overlay p-3 before">
<p class="h4 font-bold">July 7, 2020</p>
<p class="body-note">Initially, this site was far smaller.</p>
</div>
<div slot="afterOverlay" class="overlay p-3 after">
<p class="h4 font-bold">Oct. 20, 2020</p>
<p class="body-note">But then forces built up.</p>
</div>
<p slot="caption">Photos by MAXAR Technologies, 2021.</p>
{#snippet beforeOverlay()}
<div class="overlay p-3 before text-left">
<p class="h4 font-bold">July 7, 2020</p>
<p class="body-note">Initially, this site was far smaller.</p>
</div>
{/snippet}
{#snippet afterOverlay()}
<div class="overlay p-3 after text-right">
<p class="h4 font-bold">Oct. 20, 2020</p>
<p class="body-note">But then forces built up.</p>
</div>
{/snippet}
{#snippet caption()}
<p class="body-note">Photos by MAXAR Technologies, 2021.</p>
{/snippet}
</BeforeAfter>
<style lang="scss">
.overlay {
background: rgba(0, 0, 0, 0.45);
max-width: 350px;
&.after {
text-align: right;
}
p {
color: #ffffff;
}
}
</style>
</Story>
<Story name="ARIA descriptions" {...withStoryDocs(ariaDescriptionsDocs)}>
<BeforeAfter
beforeSrc="{beforeImg}"
beforeAlt="Satellite image of Russian base at Myrne taken on July 7, 2020."
afterSrc="{afterImg}"
afterAlt="Satellite image of Russian base at Myrne taken on Oct. 20, 2020."
>
<div let:description="{id}" slot="beforeOverlay" class="overlay p-3">
<p class="body-caption" {id}>
On July 7, 2020, the base contained only a few transport vehicles.
</p>
</div>
<div let:description="{id}" slot="afterOverlay" class="overlay p-3">
<!-- 👇 id can also be used on an element containing multiple text elements -->
<div {id}>
<p class="body-caption">
But by October, tanks and artillery could be seen.
</p>
<p class="body-caption">
In total, over 50 pieces of heavy machinery and 200 personnel are now
based here.
</p>
</div>
</div>
<p slot="caption">Photos by MAXAR Technologies, 2021.</p>
</BeforeAfter>
<style lang="scss">
div.overlay {
max-width: 250px;
background: rgba(0, 0, 0, 0.45);
p {
color: #ffffff;
}

View file

@ -1,119 +1,149 @@
<!-- @migration-task Error while migrating Svelte code: Cannot set properties of undefined (setting 'next') -->
<!-- @component `BeforeAfter` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-graphics-beforeafter--docs) -->
<script lang="ts">
import { type Snippet } from 'svelte';
import { throttle } from 'lodash-es';
import { onMount } from 'svelte';
import Block from '../Block/Block.svelte';
import type { ContainerWidth } from '../@types/global';
import PaddingReset from '../PaddingReset/PaddingReset.svelte';
import type { ContainerWidth } from '../@types/global';
import { random4 } from '../../utils/';
/** Width of the chart within the text well. */
export let width: ContainerWidth = 'normal'; // options: wide, wider, widest, fluid
/** Height of the component */
export let height = 600;
interface Props {
/** Width of the chart within the text well. Options: wide, wider, widest, fluid */
width?: ContainerWidth;
/** Height of the component */
height?: number;
/**
* If set, makes the height a ratio of the component's width.
*/
heightRatio?: number;
/**
* Before image source
*/
beforeSrc: string;
/**
* Before image altText
*/
beforeAlt: string;
/**
* After image source
*/
afterSrc: string;
/**
* After image altText
*/
afterAlt: string;
/**
* Class to target with SCSS.
*/
class?: string;
/** Drag handle colour */
handleColour?: string;
/** Drag handle opacity */
handleInactiveOpacity?: number;
/** Margin at the edge of the image to stop dragging */
handleMargin?: number;
/** Percentage of the component width the handle will travel ona key press */
keyPressStep?: number;
/** Initial offset of the handle, between 0 and 1. */
offset?: number;
/** ID to target with SCSS. */
id?: string;
/**
* Optional snippet for a custom overlay for the before image.
*/
beforeOverlay?: Snippet;
/**
* Optional snippet for a custom overlay for the after image.
*/
afterOverlay?: Snippet;
/**
* Optional snippet for a custom caption.
*/
caption?: Snippet;
/** Custom ARIA label language to label the component. */
ariaLabel?: string;
}
/**
* If set, makes the height a ratio of the component's width.
* @type {number}
let {
width = 'normal',
height = 600,
heightRatio,
beforeSrc,
beforeAlt,
afterSrc,
afterAlt,
class: cls = '',
handleColour = 'white',
handleInactiveOpacity = 0.9,
handleMargin = 20,
keyPressStep = 0.05,
offset = 0.5,
id = 'before-after-' + random4() + random4(),
beforeOverlay,
afterOverlay,
caption,
ariaLabel = 'Stacked before and after images with an adjustable slider',
}: Props = $props();
/** DOM nodes are undefined until the component is mounted — in other words, you should read it inside an effect or an event handler, but not during component initialisation.
*/
export let heightRatio: number | null = null;
let img: HTMLImageElement | undefined = $state(undefined);
/**
* Before image src
* @required
*/
export let beforeSrc: string | null = null;
/**
* Before image altText
* @required
*/
export let beforeAlt: string | null = null;
/**
* After image src
* @required
*/
export let afterSrc: string | null = null;
/**
* After image altText
* @required
*/
export let afterAlt: string | null = null;
/**
* Set a class to target with SCSS.
* @type {string}
*/
let cls: string = '';
export { cls as class };
/** Drag handle colour */
export let handleColour = 'white';
/** Drag handle opacity */
export let handleInactiveOpacity = 0.9;
/** Margin at the edge of the image to stop dragging */
export let handleMargin = 20;
/** Percentage of the component width the handle will travel ona key press */
export let keyPressStep = 0.05;
/** Initial offset of the handle, between 0 and 1. */
export let offset = 0.5;
const random4 = () =>
Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
/**
* Add an ID to target with SCSS.
* @type {string}
*/
export let id: string = 'before-after-' + random4() + random4();
let img: HTMLImageElement;
let imgOffset: DOMRect | null = null;
/** Defaults with an empty DOMRect with all values set to 0 */
let imgOffset: DOMRect = $state(new DOMRect());
let sliding = false;
let figure: HTMLElement;
let beforeOverlayWidth = 0;
let figure: HTMLElement | undefined = $state(undefined);
let beforeOverlayWidth = $state(0);
let isFocused = false;
let containerWidth: number;
let containerWidth: number = $state(0); // Defaults to 0
$: containerHeight =
containerWidth && heightRatio ? containerWidth * heightRatio : height;
let containerHeight = $derived(
containerWidth && heightRatio ? containerWidth * heightRatio : height
);
$: w = (imgOffset && imgOffset.width) || 0;
$: x = w * offset;
$: figStyle = `width:100%;height:${containerHeight}px;`;
$: imgStyle = 'width:100%;height:100%;';
$: beforeOverlayClip =
x < beforeOverlayWidth ? Math.abs(x - beforeOverlayWidth) : 0;
let w = $derived(imgOffset.width);
let x = $derived(w * offset);
let figStyle = $derived(`width:100%;height:${containerHeight}px;`);
const imgStyle = 'width:100%;height:100%;';
let beforeOverlayClip = $derived(
x < beforeOverlayWidth ? Math.abs(x - beforeOverlayWidth) : 0
);
const onFocus = () => (isFocused = true);
const onBlur = () => (isFocused = false);
/** Toggle `isFocused` */
const onfocus = () => (isFocused = true);
const onblur = () => (isFocused = false);
/** Handle left or right arrows being pressed */
const handleKeyDown = (e: KeyboardEvent) => {
if (!isFocused) return;
const { keyCode } = e;
const { code, key } = e;
const margin = handleMargin / w;
if (keyCode === 39) {
if (code === 'ArrowRight' || key === 'ArrowRight') {
offset = Math.min(1 - margin, offset + keyPressStep);
} else if (keyCode === 37) {
} else if (code === 'ArrowLeft' || key === 'ArrowLeft') {
offset = Math.max(0 + margin, offset - keyPressStep);
}
};
/** Measure image and set image offset */
const measureImage = () => {
if (img && img.complete) imgOffset = img.getBoundingClientRect();
};
/** Reset image offset on resize */
const resize = () => {
measureImage();
};
/** Measure image and set image offset on load */
const measureLoadedImage = (e: Event) => {
if (e.type === 'load') {
imgOffset = (e.target as HTMLImageElement).getBoundingClientRect();
}
};
/** Move the slider */
const move = (e: MouseEvent | TouchEvent) => {
if (sliding && imgOffset) {
const el =
@ -130,115 +160,115 @@
offset = x / w;
}
};
/** Starts the slider */
const start = (e: MouseEvent | TouchEvent) => {
sliding = true;
move(e);
};
/** Sets `sliding` to `false`*/
const end = () => {
sliding = false;
};
/** Keep this warning since these values are often read from an ArchieML doc, which will not trigger typescript errors even if required values don't exist */
if (!(beforeSrc && beforeAlt && afterSrc && afterAlt)) {
console.warn('Missing required src or alt props for BeforeAfter component');
}
onMount(() => {
// This is necessary b/c on:load doesn't reliably fire on the image...
const interval = setInterval(() => {
if (imgOffset) clearInterval(interval);
if (img && img.complete && !imgOffset) measureImage();
}, 50);
});
/** @TODO - Double check if this onMount is still necessary */
// onMount(() => {
// // This is necessary b/c on:load doesn't reliably fire on the image...
// const interval = setInterval(() => {
// if (imgOffset) clearInterval(interval);
// if (img && img.complete && !imgOffset) measureImage();
// }, 50);
// });
</script>
<svelte:window
on:touchmove="{move}"
on:touchend="{end}"
on:mousemove="{move}"
on:mouseup="{end}"
on:resize="{throttle(resize, 100)}"
on:keydown="{handleKeyDown}"
ontouchmove={move}
ontouchend={end}
onmousemove={move}
onmouseup={end}
onresize={throttle(resize, 100)}
onkeydown={handleKeyDown}
/>
<!-- Since we usually read these values from ArchieML, check that they exist -->
{#if beforeSrc && beforeAlt && afterSrc && afterAlt}
<Block {width} {id} class="photo before-after fmy-6 {cls}">
<div
style="height: {containerHeight}px;"
bind:clientWidth="{containerWidth}"
>
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<figure
style="{figStyle}"
<div style="height: {containerHeight}px;" bind:clientWidth={containerWidth}>
<button
style={figStyle}
class="before-after-container relative overflow-hidden my-0 mx-auto"
on:touchstart="{start}"
on:mousedown="{start}"
bind:this="{figure}"
aria-labelledby="{($$slots.caption && `${id}-caption`) || undefined}"
ontouchstart={start}
onmousedown={start}
bind:this={figure}
aria-label={ariaLabel}
>
<img
bind:this="{img}"
src="{afterSrc}"
alt="{afterAlt}"
on:load="{measureLoadedImage}"
on:mousedown|preventDefault
style="{imgStyle}"
bind:this={img}
src={afterSrc}
alt={afterAlt}
onload={measureLoadedImage}
style={imgStyle}
class="after absolute block m-0 max-w-full object-cover"
aria-describedby="{($$slots.beforeOverlay && `${id}-before`) ||
undefined}"
aria-describedby={beforeOverlay ?
`${id}-before-description`
: undefined}
/>
<img
src="{beforeSrc}"
alt="{beforeAlt}"
on:mousedown|preventDefault
src={beforeSrc}
alt={beforeAlt}
style="clip: rect(0 {x}px {containerHeight}px 0);{imgStyle}"
class="before absolute block m-0 max-w-full object-cover"
aria-describedby="{($$slots.afterOverlay && `${id}-after`) ||
undefined}"
aria-describedby={afterOverlay ?
`${id}-after-description`
: undefined}
/>
{#if $$slots.beforeOverlay}
{#if beforeOverlay}
<div
id="image-before-label"
id="{id}-before-description"
class="overlay-container before absolute"
bind:clientWidth="{beforeOverlayWidth}"
bind:clientWidth={beforeOverlayWidth}
style="clip-path: inset(0 {beforeOverlayClip}px 0 0);"
>
<!-- Overlay for before image -->
<slot
name="beforeOverlay"
description="{`${id}-before-description`}"
/>
{@render beforeOverlay()}
</div>
{/if}
{#if $$slots.afterOverlay}
<div id="image-after-label" class="overlay-container after absolute">
{#if afterOverlay}
<div
id="{id}-after-description"
class="overlay-container after absolute"
>
<!-- Overlay for after image -->
<slot
name="afterOverlay"
description="{`${id}-after-description`}"
/>
{@render afterOverlay()}
</div>
{/if}
<div
tabindex="0"
role="slider"
aria-valuenow="{Math.round(offset * 100)}"
aria-valuenow={Math.round(offset * 100)}
class="handle"
style="left: calc({offset *
100}% - 20px); --before-after-handle-colour: {handleColour}; --before-after-handle-inactive-opacity: {handleInactiveOpacity};"
on:focus="{onFocus}"
on:blur="{onBlur}"
{onfocus}
{onblur}
>
<div class="arrow-left"></div>
<div class="arrow-right"></div>
</div>
</figure>
</button>
</div>
{#if $$slots.caption}
<PaddingReset containerIsFluid="{width === 'fluid'}">
<aside class="before-after-caption mx-auto" id="{`${id}-caption`}">
{#if caption}
<PaddingReset containerIsFluid={width === 'fluid'}>
<aside class="before-after-caption mx-auto" id={`${id}-caption`}>
<!-- Caption for image credits -->
<slot name="caption" />
{@render caption()}
</aside>
</PaddingReset>
{/if}
@ -246,9 +276,9 @@
{/if}
<style lang="scss">
@use '../../scss/mixins' as *;
@use '../../scss/mixins' as mixins;
figure.before-after-container {
button.before-after-container {
box-sizing: content-box;
img {
@ -264,7 +294,7 @@
user-select: none;
}
.overlay-container {
position: absolute;
top: 0;
:global(:first-child) {
margin-top: 0;
}
@ -337,9 +367,9 @@
}
}
aside.before-after-caption {
.before-after-caption {
:global(p) {
@include body-caption;
@include mixins.body-caption;
}
}
</style>

View file

Before

Width:  |  Height:  |  Size: 715 KiB

After

Width:  |  Height:  |  Size: 715 KiB

View file

Before

Width:  |  Height:  |  Size: 472 KiB

After

Width:  |  Height:  |  Size: 472 KiB

View file

@ -1,35 +0,0 @@
Use text elements in your overlays as [ARIA descriptions](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-describedby) for your images by setting an ID on text elements within each overlay with the `description` [slot prop](https://svelte.dev/tutorial/slot-props).
> **💡 TIP:** You must always use the `beforeAlt` / `afterAlt` props to label your image for visually impaired readers, but you can use these descriptions to provide more information or context that the reader might also need.
```svelte
<BeforeAfter
beforeSrc="{beforeImg}"
beforeAlt="Satellite image of Russian base at Myrne taken on July 7, 2020."
afterSrc="{afterImg}"
afterAlt="Satellite image of Russian base at Myrne taken on Oct. 20, 2020."
>
<div slot="beforeOverlay" class="overlay p-3 before">
<p class="h4 font-bold">July 7, 2020</p>
<p class="body-note">Initially, this site was far smaller.</p>
</div>
<div slot="afterOverlay" class="overlay p-3 after">
<p class="h4 font-bold">Oct. 20, 2020</p>
<p class="body-note">But then forces built up.</p>
</div>
<p slot="caption">Photos by MAXAR Technologies, 2021.</p>
</BeforeAfter>
<style lang="scss">
.overlay {
background: rgba(0, 0, 0, 0.45);
max-width: 350px;
&.after {
text-align: right;
}
p {
color: #ffffff;
}
}
</style>
```

View file

@ -1,15 +0,0 @@
A before and after image comparison component.
```svelte
<script>
import { BeforeAfter } from '@reuters-graphics/graphics-components';
import { assets } from '$app/paths'; // If using in the Graphics Kit
</script>
<BeforeAfter
beforeSrc="{`${assets}/images/before-after/myrne-before.jpg`}"
beforeAlt="Satellite image of Russian base at Myrne taken on July 7, 2020."
afterSrc="{`${assets}/images/before-after/myrne-after.jpg`}"
afterAlt="Satellite image of Russian base at Myrne taken on Oct. 20, 2020."
/>
```

View file

@ -1,33 +0,0 @@
Add overlays with the `beforeOverlay` and `afterOverlay` slots and a caption to the `caption` slot, then style these elements to match your page design as below.
```svelte
<BeforeAfter
beforeSrc="{beforeImg}"
beforeAlt="Satellite image of Russian base at Myrne taken on July 7, 2020."
afterSrc="{afterImg}"
afterAlt="Satellite image of Russian base at Myrne taken on Oct. 20, 2020."
>
<div slot="beforeOverlay" class="overlay p-3 before">
<p class="h4 font-bold">July 7, 2020</p>
<p class="body-note">Initially, this site was far smaller.</p>
</div>
<div slot="afterOverlay" class="overlay p-3 after">
<p class="h4 font-bold">Oct. 20, 2020</p>
<p class="body-note">But then forces built up.</p>
</div>
<p slot="caption">Photos by MAXAR Technologies, 2021.</p>
</BeforeAfter>
<style lang="scss">
.overlay {
background: rgba(0, 0, 0, 0.45);
max-width: 350px;
&.after {
text-align: right;
}
p {
color: #ffffff;
}
}
</style>
```

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

View file

@ -0,0 +1,84 @@
import { Meta, Canvas } from '@storybook/blocks';
import * as BylineStories from './Byline.stories.svelte';
<Meta of={BylineStories} />
# Byline
The `Byline` component adds a byline, published and updated datelines to your page.
```svelte
<script>
import { Byline } from '@reuters-graphics/graphics-components';
</script>
<Byline
authors={[
'Dea Bankova',
'Prasanta Kumar Dutta',
'Anurag Rao',
'Mariano Zafra',
]}
publishTime="2021-09-12T00:00:00.000Z"
updateTime="2021-09-12T12:57:00.000Z"
/>
```
## Using with ArchieML docs
With the Graphics Kit, you'll likely get your text value from an ArchieML doc...
```yaml
# ArchieML doc
[authors]
* Dea Bankova
* Prasanta Kumar Dutta
* Anurag Rao
* Mariano Zafra
[]
publishTime: 2021-09-12T00:00:00.000Z
updateTime: 2021-09-12T12:57:00.000Z
```
... which you'll pass to the `Byline` component.
```svelte
<script>
import { Byline } from '@reuters-graphics/graphics-components';
import content from '$locales/en/content.json';
</script>
<Byline
authors={content.authors}
publishTime={content.publishTime}
updateTime={content.updateTime}
/>
```
<Canvas of={BylineStories.Demo} />
## Cutomisation
Use [snippets](https://svelte.dev/docs/svelte/snippet) to customise the byline, published and updated datelines.
```svelte
<Byline publishTime="2021-09-12T00:00:00Z" updateTime="2021-09-12T13:57:00Z">
<!-- Optional custom byline -->
{#snippet byline()}
<strong>BY REUTERS GRAPHICS</strong>
{/snippet}
<!-- Optional custom published dateline -->
{#snippet published()}
PUBLISHED on some custom date and time
{/snippet}
<!-- Optional custom updated dateline -->
{#snippet updated()}
<em>Updated every 5 minutes</em>
{/snippet}
</Byline>
```
<Canvas of={BylineStories.Customised} />

View file

@ -1,17 +1,11 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import Byline from './Byline.svelte';
// @ts-ignore raw
import componentDocs from './stories/docs/component.md?raw';
import { withComponentDocs } from '$docs/utils/withParams.js';
import type { ComponentProps } from 'svelte';
const { Story } = defineMeta({
title: 'Components/Text elements/Byline',
component: Byline,
...withComponentDocs(componentDocs),
tags: ['autodocs'],
argTypes: {
align: {
control: 'select',
@ -21,17 +15,12 @@
});
</script>
{#snippet template(args: ComponentProps<Byline>)}
<Byline {...args} />
{/snippet}
<Story
name="Default"
name="Demo"
args={{
align: 'left',
authors: [
'Dea Bankova',
'Aditi Bhandari',
'Prasanta Kumar Dutta',
'Anurag Rao',
'Mariano Zafra',
@ -39,5 +28,18 @@
publishTime: new Date('2021-09-12').toISOString(),
updateTime: new Date('2021-09-12T13:57:00').toISOString(),
}}
children={template}
/>
<Story name="Customised" tags={['!autodocs', '!dev']}>
<Byline publishTime="2021-09-12T00:00:00Z" updateTime="2021-09-12T13:57:00Z">
{#snippet byline()}
<strong>BY REUTERS GRAPHICS</strong>
{/snippet}
{#snippet published()}
PUBLISHED on some custom date and time
{/snippet}
{#snippet updated()}
<em>Updated every 5 minutes</em>
{/snippet}
</Byline>
</Story>

View file

@ -4,63 +4,89 @@
import Block from '../Block/Block.svelte';
import slugify from 'slugify';
import { apdate } from 'journalize';
import type { Snippet } from 'svelte';
interface Props {
/**
* Array of author names, which will be slugified to create links to Reuters author pages
*/
authors?: string[];
/**
* Publish time as a datetime string.
*/
publishTime: string;
/**
* Update time as a datetime string.
*/
updateTime: string;
/**
* Alignment of the byline.
*/
align?: 'left' | 'center';
/**
* Add an id to to target with custom CSS.
* @type {string}
*/
id?: string;
/**
* Add extra classes to target with custom CSS.
* @type {string}
*/
cls?: string;
/**
* Custom function that returns an author page URL.
*/
getAuthorPage?: (author: string) => string;
/**
* Optional snippet for a custom byline.
*/
byline?: Snippet;
/**
* Optional snippet for a custom published dateline.
*/
// Specify that this prop should have the type of a Svelte snippet, i.e. basic html
published?: Snippet;
/**
* Optional snippet for a custom updated dateline.
*/
updated?: Snippet;
}
let {
authors = [],
publishTime,
updateTime,
align = 'left',
id = '',
cls = '',
getAuthorPage = (author: string): string => {
const authorSlug = slugify(author.trim(), { lower: true });
return `https://www.reuters.com/authors/${authorSlug}/`;
},
byline,
published,
updated,
}: Props = $props();
let alignmentClass = $derived(align === 'left' ? 'text-left' : 'text-center');
/**
* Array of author names, which will be slugified to create links to Reuters author pages
/* Date validation and formatter functions
*/
export let authors: string[] = [];
/**
* Publish time as a datetime string.
* @type {string}
*/
export let publishTime: string = '';
/**
* Update time as a datetime string.
* @type {string}
*/
export let updateTime: string = '';
/**
* Alignment of the byline.
* @type {string}
*/
export let align: 'left' | 'center' = 'left';
/**
* Add an id to to target with custom CSS.
* @type {string}
*/
export let id: string = '';
/**
* Add extra classes to target with custom CSS.
* @type {string}
*/
let cls: string = '';
export { cls as class };
/**
* Custom function that returns an author page URL.
* @param author
*/
export let getAuthorPage = (author: string): string => {
const authorSlug = slugify(author.trim(), { lower: true });
return `https://www.reuters.com/authors/${authorSlug}/`;
};
$: alignmentClass = align === 'left' ? 'text-left' : 'text-center';
const isValidDate = (datetime) => {
const isValidDate = (datetime: string) => {
if (!datetime) return false;
if (!Date.parse(datetime)) return false;
return true;
};
const formatTime = (datetime) =>
const formatTime = (datetime: string) =>
new Date(datetime).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
timeZoneName: 'short',
});
const areSameDay = (first, second) =>
const areSameDay = (first: Date, second: Date) =>
first.getFullYear() === second.getFullYear() &&
first.getMonth() === second.getMonth() &&
first.getDate() === second.getDate();
@ -69,16 +95,16 @@
<Block {id} class="byline-container {alignmentClass} {cls}" width="normal">
<aside class="article-metadata font-subhed">
<div class="byline body-caption fmb-1">
{#if $$slots.byline}
{#if byline}
<!-- Custom byline -->
<slot name="byline" />
{@render byline()}
{:else}
By
{#if authors.length > 0}
{#each authors as author, i}
<a
class="no-underline whitespace-nowrap text-primary font-bold"
href="{getAuthorPage(author)}"
href={getAuthorPage(author)}
rel="author"
>
{author.trim()}</a
@ -95,15 +121,17 @@
{/if}
</div>
<div class="dateline body-caption fmt-0">
{#if $$slots.published}
{#if published}
<div class="whitespace-nowrap inline-block">
<!-- Custom published dateline -->
<slot name="published" />
<!-- Custom published dateline snippet -->
<time datetime={publishTime}>
{@render published()}
</time>
</div>
{:else if isValidDate(publishTime)}
<div class="whitespace-nowrap inline-block">
Published
<time datetime="{publishTime}">
<time datetime={publishTime}>
{#if isValidDate(updateTime)}
{apdate(new Date(publishTime))}
{:else}
@ -114,15 +142,17 @@
</time>
</div>
{/if}
{#if $$slots.updated}
{#if updated}
<div class="whitespace-nowrap inline-block">
<!-- Custom updated dateline -->
<slot name="updated" />
<!-- Custom updated dateline snippet -->
<time datetime={updateTime}>
{@render updated()}
</time>
</div>
{:else if isValidDate(publishTime) && isValidDate(updateTime)}
<div class="whitespace-nowrap inline-block">
Last updated
<time datetime="{updateTime}">
<time datetime={updateTime}>
{#if areSameDay(new Date(publishTime), new Date(updateTime))}
{formatTime(updateTime)}
{:else}

View file

@ -1,19 +0,0 @@
Byline and dateline.
```svelte
<script>
import { Byline } from '@reuters-graphics/graphics-components';
</script>
<Byline
authors="{[
'Dea Bankova',
'Aditi Bhandari',
'Prasanta Kumar Dutta',
'Anurag Rao',
'Mariano Zafra',
]}"
publishTime="2021-09-12T00:00:00.000Z"
updateTime="2021-09-12T12:57:00.000Z"
/>
```

View file

@ -17,18 +17,18 @@
<Story
name="Demo"
args={{
args="{{
src: 'https://reuters.com/graphics/USA-ABORTION/lgpdwggnwvo/media-embed.html',
id: 'abortion-rights-map',
ariaLabel: 'map',
frameTitle: 'Global abortion access',
}}
}}"
/>
<Story
name="With chatter"
tags={['!autodocs']}
args={{
tags="{['!autodocs']}"
args="{{
frameTitle: 'Global abortion access',
ariaLabel: 'map',
id: 'abortion-rights-map',
@ -37,5 +37,5 @@
description: 'A map of worldwide access to abortion.',
notes:
'Note: Different indicators and additional restrictions, including different gestational limits, apply in some countries. Refer to source for full classification. Current as of May 4, 2022.\n\nSource: Center for Reproductive Rights',
}}
}}"
/>

View file

@ -0,0 +1,19 @@
import { Meta } from '@storybook/blocks';
import * as EmbedPreviewerLinkStories from './EmbedPreviewerLink.stories.svelte';
<Meta of={EmbedPreviewerLinkStories} />
# EmbedPreviewerLink
The `EmbedPreviewerLink` component is a tool for previewing the embeds in development. It adds an icon at the bottom of the page that, when clicked, opens a previewer with the embeds.
```svelte
<script>
import { EmbedPreviewerLink } from '@reuters-graphics/graphics-components';
import { dev } from '$app/env';
</script>
<EmbedPreviewerLink {dev} />
```

View file

@ -1,30 +1,17 @@
<script module lang="ts">
import EmbedPreviewerLink from './EmbedPreviewerLink.svelte';
// @ts-ignore raw
import componentDocs from './stories/docs/component.md?raw';
import { defineMeta } from '@storybook/addon-svelte-csf';
import { withComponentDocs } from '$lib/docs/utils/withParams.js';
export const meta = {
const { Story } = defineMeta({
title: 'Components/Utilities/EmbedPreviewerLink',
component: EmbedPreviewerLink,
...withComponentDocs(componentDocs),
};
});
</script>
<script>
import { Template, Story } from '@storybook/addon-svelte-csf';
</script>
<Template >
{#snippet children({ args })}
<EmbedPreviewerLink {...args} />
{/snippet}
</Template>
<Story
name="Default"
args="{{
name="Demo"
tags={['!autodocs', '!dev']}
args={{
dev: true,
}}"
}}
/>

View file

@ -1,5 +1,4 @@
<script lang="ts">
import Fa from 'svelte-fa/src/fa.svelte';
import { faWindowRestore } from '@fortawesome/free-regular-svg-icons';
interface Props {
@ -12,7 +11,7 @@
{#if dev}
<div>
<a rel="external" href="/embed-previewer">
<Fa icon="{faWindowRestore}" />
<Fa icon={faWindowRestore} />
</a>
</div>
{/if}

View file

@ -1,11 +0,0 @@
An embed tool for development in graphics kit.
```svelte
<script>
import { EmbedPreviewerLink } from '@reuters-graphics/graphics-components';
import { dev } from '$app/env';
</script>
<EmbedPreviewerLink dev="{dev}" />
```

View file

@ -0,0 +1,67 @@
import { Meta, Canvas } from '@storybook/blocks';
import * as EndNotesStories from './EndNotes.stories.svelte';
<Meta of={EndNotesStories} />
# EndNotes
The `EndNotes` component adds notes such as sources, clarifiying notes and minor corrections that come at the end of a story.
```svelte
<script>
import { EndNotes } from '@reuters-graphics/graphics-components';
const notes = [
{
title: 'Note',
text: 'Data is current as of today.',
},
{
title: 'Sources',
text: 'Data, Inc.',
},
{
title: 'Edited by',
text: '<a href="https://www.reuters.com/graphics/">Editor</a>, Copyeditor',
},
];
</script>
<EndNotes {notes} />
```
<Canvas of={EndNotesStories.Demo} />
## Using with ArchieML docs
With the Graphics Kit, you'll likely get your text value from an ArchieML doc...
```yaml
# ArchieML doc
[endNotes]
title: Note
text: Data is current as of today
title: Sources
text: Data, Inc.
title: Edited by
text: Editor, Copyeditor
[]
```
... which you'll pass to the `EndNotes` component.
```svelte
<!-- Graphics Kit -->
<script>
import { EndNotes } from '@reuters-graphics/graphics-components';
import content from '$locales/en/content.json';
</script>
<EndNotes notes={content.endNotes} />
```
<Canvas of={EndNotesStories.Demo} />

View file

@ -1,20 +1,15 @@
<script module lang="ts">
import EndNotes from './EndNotes.svelte';
// @ts-ignore raw
import componentDocs from './stories/docs/component.md?raw';
import { withComponentDocs } from '$lib/docs/utils/withParams.js';
import { defineMeta } from '@storybook/addon-svelte-csf';
export const meta = {
const { Story } = defineMeta({
title: 'Components/Text elements/EndNotes',
component: EndNotes,
...withComponentDocs(componentDocs),
};
});
</script>
<script>
import { Template, Story } from '@storybook/addon-svelte-csf';
const notes = [
{
title: 'Note',
@ -31,10 +26,4 @@
];
</script>
<Template >
{#snippet children({ args })}
<EndNotes {...args} />
{/snippet}
</Template>
<Story name="Default" args="{{ notes }}" />
<Story name="Demo" args={{ notes }} />

View file

@ -7,40 +7,33 @@
title: string;
/**
* Contents of the note as a markdown string
* @required
*/
text: string;
}
import Block from '../Block/Block.svelte';
import Markdown from '../Markdown/Markdown.svelte';
interface Props {
/**
* An array of endnote items.
* @required
*/
notes?: EndNote[];
* An array of endnote items.
*/
notes: EndNote[];
}
let { notes = [] }: Props = $props();
let { notes }: Props = $props();
</script>
<Block class="notes fmt-6 fmb-8">
{#if notes}
{#each notes as note}
<div class="note-title">
<Markdown source="{note.title}" />
</div>
<div class="note-content">
<Markdown source="{note.text}" />
</div>
{/each}
{/if}
{#each notes as note}
<div class="note-title">
<Markdown source={note.title} />
</div>
<div class="note-content">
<Markdown source={note.text} />
</div>
{/each}
</Block>
<!-- svelte-ignore css_unused_selector -->
<style lang="scss">
@use '../../scss/mixins' as mixins;

View file

@ -1,24 +0,0 @@
End notes includes notes to the main article — usually things like sources, clarifiying notes and minor corrections at the end of a story.
```svelte
<script>
import { EndNotes } from '@reuters-graphics/graphics-components';
const notes = [
{
title: 'Note',
text: 'Data is current as of today.',
},
{
title: 'Sources',
text: 'Data, Inc.',
},
{
title: 'Edited by',
text: '<a href="https://www.reuters.com/graphics/">Editor</a>, Copyeditor',
},
];
</script>
<EndNotes notes="{notes}" />
```

View file

@ -11,7 +11,11 @@
minFrameWidth?: number;
}
let { breakpoints = [330, 510, 660, 930, 1200], maxFrameWidth = 1200, minFrameWidth = 320 }: Props = $props();
let {
breakpoints = [330, 510, 660, 930, 1200],
maxFrameWidth = 1200,
minFrameWidth = 320,
}: Props = $props();
let container = $state();
@ -91,9 +95,9 @@
</script>
<svelte:window
onmousemove={move}
onmouseup={end}
onkeydown={handleKeyDown}
onmousemove="{move}"
onmouseup="{end}"
onkeydown="{handleKeyDown}"
bind:innerWidth="{windowInnerWidth}"
/>
@ -105,10 +109,10 @@
<button
class="icon left"
disabled="{$width === minWidth}"
onclick={decrement}
onfocus={onFocus}
onmouseover={onFocus}
onmouseleave={onBlur}
onclick="{decrement}"
onfocus="{onFocus}"
onmouseover="{onFocus}"
onmouseleave="{onBlur}"
>
<Fa icon="{faMobileAlt}" fw />
</button>
@ -119,18 +123,18 @@
tabindex="0"
role="button"
style="left: calc({offset * 100}% - 5px);"
onmousedown={start}
onfocus={onFocus}
onblur={onBlur}
onmousedown="{start}"
onfocus="{onFocus}"
onblur="{onBlur}"
></div>
</div>
<button
class="icon right"
disabled="{$width === maxWidth}"
onclick={increment}
onfocus={onFocus}
onmouseover={onFocus}
onmouseleave={onBlur}
onclick="{increment}"
onfocus="{onFocus}"
onmouseover="{onFocus}"
onmouseleave="{onBlur}"
>
<Fa icon="{faDesktop}" fw />
</button>

View file

@ -1,5 +1,4 @@
<script lang="ts">
// For demo purposes only, hard-wiring img paths from Vite
// @ts-ignore img
import chartXs from '../imgs/ai-chart-xs.png';

View file

@ -0,0 +1,122 @@
import { Meta, Canvas } from '@storybook/blocks';
import * as InfoBoxStories from './InfoBox.stories.svelte';
<Meta of={InfoBoxStories} />
# InfoBox
The `InfoBox` component creates a stylised text box that provides additional information that needs to be visually separate from the main content flow, such as methodology, detailed notes about data and extra context.
```svelte
<script>
import { InfoBox } from '@reuters-graphics/graphics-components';
</script>
<InfoBox
title="About this data"
text={'Reuters is collecting daily COVID-19 infections and deaths data for 240 countries and territories around the world, updated regularly throughout each day. \n\n Every country reports those figures a little differently and, inevitably, misses undiagnosed infections and deaths. With this project we are focusing on the trends within countries as they try to contain the virus spread, whether they are approaching or past peak infection rates, or if they are seeing a resurgence of infections or deaths.'}
notes={'[Read more about our methodology](https://www.reuters.com/world-coronavirus-tracker-and-maps/en/methodology/)'}
/>
```
<Canvas of={InfoBoxStories.Demo} />
## Using with ArchieML docs
With the graphics kit, you'll likely get your text value from an ArchieML doc...
```yaml
# Archie ML doc
[blocks]
type: info-box
title: What you need to know about the war
text: Reuters is collecting daily COVID-19 infections and deaths data for 240 countries and territories around the world, updated regularly throughout each day.
Every country reports those figures a little differently and, inevitably, misses undiagnosed infections and deaths. With this project we are focusing on the trends within countries as they try to contain the virus spread, whether they are approaching or past peak infection rates, or if they are seeing a resurgence of infections or deaths.
:end
notes: [Read more about our methodology](https://www.reuters.com/world-coronavirus-tracker-and-maps/en/methodology/)
[]
```
... which you'll parse out of a ArchieML block object before passing to the `InfoBox` component.
```svelte
<!-- Graphics Kit -->
<script>
import { InfoBox } from '@reuters-graphics/graphics-components';
import content from '$locales/en/content.json';
</script>
# Graphics Kit
{#each content.blocks as block}
{#if block.type === 'info-box'}
<InfoBox title={block.title} text={block.text} notes={block.notes} />
<!-- ... -->
{/if}
{/each}
```
<Canvas of={InfoBoxStories.Demo} />
## Lists
Use markdown to add lists to `InfoBox`.
```svelte
<script>
import { InfoBox } from '@reuters-graphics/graphics-components';
</script>
<InfoBox
title="What you need to know about the war"
text={"- **Food crisis**: [Russia's invasion of Ukraine](#) in late February dramatically worsened the outlook for already inflated global food prices.\n- **Under fire**: Civillian homes destroyed in the conflict and Russia accused of war crimes.\n- **Nordstream sabotage**: A series of clandestine bombings and subsequent underwater gas leaks occurred on the Nord Stream 1 and Nord Stream 2 natural gas pipelines."}
notes={'[Read more about our methodology](https://www.reuters.com/world-coronavirus-tracker-and-maps/en/methodology/)'}
/>
```
<Canvas of={InfoBoxStories.Lists} />
## Customisation
Use [snippets](https://svelte.dev/docs/svelte/snippet) to customise the `InfoBox`, such as adding tables, icons and thumbnail images.
```svelte
<InfoBox title="About this data">
<!-- Optional custom header -->
{#snippet header()}
<h3>Global video game market</h3>
{/snippet}
<!-- Optional custom body -->
{#snippet body()}
<table>
<thead>
<tr>
<th>Year</th>
<th>Market size ($bln)</th>
</tr>
</thead>
<tbody>
<tr>
<td>2024</td>
<td>274.63</td>
</tr>
<tr>
<td>2023</td>
<td>281.77</td>
</tr>
<tr>
<td>2022</td>
<td>249.55</td>
</tr>
</tbody>
</table>
{/snippet}
<!-- Optional custom footer -->
{#snippet updated()}
<div class="text-xs font-note">Source: Precedence Research</div>
{/snippet}
</InfoBox>
```
<Canvas of={InfoBoxStories.Customised} />

View file

@ -1,15 +1,12 @@
<script module lang="ts">
// @ts-ignore raw
import componentDocs from './stories/docs/component.md?raw';
import { defineMeta } from '@storybook/addon-svelte-csf';
import InfoBox from './InfoBox.svelte';
import BodyText from '../BodyText/BodyText.svelte';
import { withComponentDocs } from '$lib/docs/utils/withParams.js';
export const meta = {
const { Story } = defineMeta({
title: 'Components/Text elements/InfoBox',
component: InfoBox,
...withComponentDocs(componentDocs),
tags: ['autodocs'],
argTypes: {
theme: {
control: 'select',
@ -20,38 +17,82 @@
options: ['normal', 'wide', 'wider', 'widest', 'fluid'],
},
},
};
});
</script>
<script>
import { Template, Story } from '@storybook/addon-svelte-csf';
import BodyText from '../BodyText/BodyText.svelte';
</script>
<Template >
{#snippet children({ args })}
<InfoBox {...args} />
{/snippet}
</Template>
<Story name="Default">
<Story name="Demo">
<BodyText
text="If you haven't seen Game of Thrones, go watch it right now. If you have then you'll totally get why this Hodor themed lorem ipsum generator is just brilliant."
text="Bacon ipsum dolor amet turducken buffalo beef ribs bresaola pancetta ribeye pork belly doner hamburger biltong cupim porchetta chuck ham tenderloin. Turducken bresaola jerky chicken."
/>
<InfoBox
title="About this data"
text="{'Reuters is collecting daily COVID-19 infections and deaths data for 240 countries and territories around the world, updated regularly throughout each day. \n\n Every country reports those figures a little differently and, inevitably, misses undiagnosed infections and deaths. With this project we are focusing on the trends within countries as they try to contain the virus spread, whether they are approaching or past peak infection rates, or if they are seeing a resurgence of infections or deaths.'}"
notes="{'[Read more about our methodology](https://www.reuters.com/world-coronavirus-tracker-and-maps/en/methodology/)'}"
text={'Reuters is collecting daily COVID-19 infections and deaths data for 240 countries and territories around the world, updated regularly throughout each day. \n\n Every country reports those figures a little differently and, inevitably, misses undiagnosed infections and deaths. With this project we are focusing on the trends within countries as they try to contain the virus spread, whether they are approaching or past peak infection rates, or if they are seeing a resurgence of infections or deaths.'}
notes={'[Read more about our methodology](https://www.reuters.com/world-coronavirus-tracker-and-maps/en/methodology/)'}
/>
<BodyText
text="In case you don't read Twitter, the news, or just can't get enough of The Apprentice host's legendary oration, try this Trump lorem ipsum generator on for size."
text="Ham drumstick tail ribeye pancetta, leberkas hamburger chicken spare ribs buffalo jerky sausage ground round meatball. Leberkas kevin short loin, tri-tip shank spare ribs buffalo beef pork belly corned beef chislic tongue."
/>
</Story>
<Story
name="Lists"
tags={['!autodocs', '!dev']}
args={{
title: 'What you need to know about the war',
text: "- **Food crisis**: [Russia's invasion of Ukraine](#) in late February dramatically worsened the outlook for already inflated global food prices. \n- **Under fire**: Civillian homes destroyed in the conflict and Russia accused of war crimes. \n- **Nordstream sabotage**: A series of clandestine bombings and subsequent underwater gas leaks occurred on the Nord Stream 1 and Nord Stream 2 natural gas pipelines. ",
}}
/>
<Story name="Customised" tags={['!autodocs', '!dev']}>
<InfoBox>
{#snippet header()}
<h3>Global video game market</h3>
{/snippet}
{#snippet body()}
<table>
<thead>
<tr>
<th>Year</th>
<th>Market size ($bln)</th>
</tr>
</thead>
<tbody>
<tr>
<td>2024</td>
<td>274.63</td>
</tr>
<tr>
<td>2023</td>
<td>281.77</td>
</tr>
<tr>
<td>2022</td>
<td>249.55</td>
</tr>
</tbody>
</table>
{/snippet}
{#snippet footer()}
<div class="text-xs font-note">Source: Precedence Research</div>
{/snippet}
</InfoBox>
</Story>
<Story name="List">
<InfoBox
title="What you need to know about the war"
text="{"- **Food crisis**: [Russia's invasion of Ukraine](#) in late February dramatically worsened the outlook for already inflated global food prices. \n- **Under fire**: Civillian homes destroyed in the conflict and Russia accused of war crimes. \n- **Nordstream sabotage**: A series of clandestine bombings and subsequent underwater gas leaks occurred on the Nord Stream 1 and Nord Stream 2 natural gas pipelines. "}"
/>
</Story>
<style lang="scss">
h3 {
margin: 0;
}
// Style the table nicely
table {
width: 100%;
border-collapse: collapse;
border-spacing: 0;
}
th,
td {
border: 1px solid #ddd;
padding: 8px;
text-align: center;
}
th {
background-color: #f2f2f2;
}
</style>

View file

@ -1,47 +1,59 @@
<!-- @migration-task Error while migrating Svelte code: Cannot set properties of undefined (setting 'next') -->
<!-- @component `InfoBox` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-text-elements-infobox--docs) -->
<script lang="ts">
import type { Snippet } from 'svelte';
import type { ContainerWidth } from '../@types/global';
type Theme = 'light' | 'dark';
/**
* Title of the box
*/
export let title: string | null = null;
interface Props {
/**
* Title of the box
*/
title?: string | null;
/**
* Contents of the note as a markdown string
*/
text?: string;
/**
* Additional footnotes
*/
notes?: string | null;
/**
* Width of the component within the text well.
*/
width?: ContainerWidth;
/**
* Add extra classes to the block tag to target it with custom CSS.
*/
class?: string;
/**
* Add an id to the block tag to target it with custom CSS.
*/
id?: string;
/**
* Page theme
*/
theme?: Theme;
/** Optional custom header snippet */
header?: Snippet;
/** Optional custom body snippet */
body?: Snippet;
/** Optional custom footer snippet */
footer?: Snippet;
}
/**
* Contents of the note as a markdown string
*/
export let text: string = '';
/**
* Additional footnotes
*/ export let notes: string | null = null;
/**
* Width of the component within the text well.
* @type {string}
*/
export let width: ContainerWidth = 'normal';
/**
* Add extra classes to the block tag to target it with custom CSS.
* @type {string}
*/
let cls: string = '';
export { cls as class };
/**
* Add an id to the block tag to target it with custom CSS.
* @type {string}
*/
export let id: string = '';
/**
* Page theme
*/
export let theme: Theme = 'light';
let {
title = null,
text,
notes = null,
width = 'normal',
class: cls = '',
id = '',
theme = 'light',
header,
body,
footer,
}: Props = $props();
import Block from '../Block/Block.svelte';
import Markdown from '../Markdown/Markdown.svelte';
@ -53,36 +65,36 @@
{id}
class="{cls} fmy-6 fpx-6 fpy-5 border border-solid rounded"
>
{#if $$slots.header}
{#if header}
<div class="header fmb-2">
<!-- Custom title content -->
<slot name="header" />
<!-- Custom header content -->
{@render header()}
</div>
{:else if title}
<div class="header fmb-2">
<Markdown source="{title}" />
<Markdown source={title} />
</div>
{/if}
{#if $$slots.body}
{#if body}
<div class="body">
<!-- Custom content -->
<slot name="body" />
<!-- Custom body content -->
{@render body()}
</div>
{:else}
<div class="body">
<Markdown source="{text}" />
<Markdown source={text} />
</div>
{/if}
{#if $$slots.footer}
{#if footer}
<div class="footer fmt-2">
<!-- Custom footer content -->
<slot name="footer" />
{@render footer()}
</div>
{:else if notes}
<div class="footer fmt-2">
<Markdown source="{notes}" />
<Markdown source={notes} />
</div>
{/if}
</Block>

View file

@ -1,17 +0,0 @@
InfoBox is used to provide additional information that needs to be visually set aside from the main content flow. e.g. Methodology, Detailed notes about data, Extra context as text stories.
Switch the theme prop to `dark` for a dark page infobox.
Use the slots to customize the content as needed, e.g. adding icons and thumbnail images etc.
```svelte
<script>
import { InfoBox } from '@reuters-graphics/graphics-components';
</script>
<InfoBox
title="About this data"
text="{'Reuters is collecting daily COVID-19 infections and deaths data for 240 countries and territories around the world, updated regularly throughout each day. \n\n Every country reports those figures a little differently and, inevitably, misses undiagnosed infections and deaths. With this project we are focusing on the trends within countries as they try to contain the virus spread, whether they are approaching or past peak infection rates, or if they are seeing a resurgence of infections or deaths.'}"
notes="{'[Read more about our methodology](https://www.reuters.com/world-coronavirus-tracker-and-maps/en/methodology/)'}"
/>
```

View file

@ -1,20 +1,27 @@
The Markdown component renders markdown into HTML. That's it!
import { Meta, Canvas } from '@storybook/blocks';
import * as MarkdownStories from './Markdown.stories.svelte';
---
<Meta of={MarkdownStories} />
# Markdown
The `Markdown` component renders a markdown string into HTML.
```svelte
<script>
import { Markdown } from '@reuters-graphics/graphics-components';
</script>
<Markdown source="{'My markdown **string**!'}" />
<Markdown source={'My *awesome* text in **markdown** with "smart quotes".'} />
```
<Canvas of={MarkdownStories.Demo} />
... well, almost.
Owing to a weird quirk of Svelte's [`@html`](https://svelte.dev/docs/special-tags#html) directive (see [this issue](https://github.com/reuters-graphics/graphics-components/issues/148)), if you want your resulting HTML to be dynamic — e.g., update after a SvelteKit app [hydrates](https://kit.svelte.dev/docs/glossary#hydration) — then you may need to set the included `$staticMarkdown` store to `false` in the browser.
Owing to a weird quirk of Svelte's [`@html`](https://svelte.dev/docs/special-tags#html) directive (see [this issue](https://github.com/reuters-graphics/graphics-components/issues/148)), if you want your resulting HTML to be dynamic — e.g., update after a SvelteKit app [hydrates](https://kit.svelte.dev/docs/glossary#hydration) — then you may need to set the included `staticMarkdown` rune to `{ static: false }` in the browser.
For example, if you're refreshing some data with markdown strings in a SvelteKit project using a [load function](https://kit.svelte.dev/docs/load), set the store to reflect the [`building`](https://kit.svelte.dev/docs/modules#$app-environment-building) variable, which will correctly [prerender](https://kit.svelte.dev/docs/glossary#prerendering) your markdown content AND update it after fresh data is fetched in the browser.
For example, if you're refreshing some data with markdown strings in a SvelteKit project using a [load function](https://kit.svelte.dev/docs/load), set the rune to reflect the [`building`](https://kit.svelte.dev/docs/modules#$app-environment-building) variable, which will correctly [prerender](https://kit.svelte.dev/docs/glossary#prerendering) your markdown content AND update it after fresh data is fetched in the browser.
```javascript
// +layout.js
@ -23,8 +30,8 @@ import { building } from '$app/environment';
/** @type {import('./$types').LayoutLoad} */
export const load = async () => {
// Set the staticMarkdown store with the value of building.
staticMarkdown.set(building);
// Set the staticMarkdown rune with the value of building.
staticMarkdown.static = building;
// Now this content will correctly refresh when a reader loads your page.
const resp = await fetch(
@ -36,4 +43,4 @@ export const load = async () => {
};
```
If you're not updating your markdown content as above, you can safely leave the `$staticMarkdown` store alone and your page will do the right thing.
If you're not updating your markdown content as above, you can safely leave the `staticMarkdown` rune alone and your page will do the right thing.

View file

@ -1,35 +1,16 @@
<script module lang="ts">
// @ts-ignore raw
import componentDocs from './stories/docs/component.md?raw';
import { defineMeta } from '@storybook/addon-svelte-csf';
import Markdown from './Markdown.svelte';
import { withComponentDocs } from '$lib/docs/utils/withParams.js';
export const meta = {
const { Story } = defineMeta({
title: 'Components/Text elements/Markdown',
component: Markdown,
...withComponentDocs(componentDocs),
};
});
</script>
<script>
import { Template, Story } from '@storybook/addon-svelte-csf';
import Block from '../Block/Block.svelte';
</script>
<Template >
{#snippet children({ args })}
<Block>
<Markdown {...args} />
</Block>
{/snippet}
</Template>
<Story
name="Default"
args="{{
source: 'This is *some* text in **markdown**.',
}}"
name="Demo"
args={{
source: 'My *awesome* text in **markdown** with "smart quotes".',
}}
/>

View file

@ -2,21 +2,25 @@
<script lang="ts">
import type { Action } from 'svelte/action';
import { marked } from 'marked';
import { staticMarkdown } from './stores';
import { markedSmartypants } from 'marked-smartypants';
import { staticMarkdown } from './state.svelte';
marked.use(markedSmartypants());
interface Props {
/** A Markdown formatted string */
source?: string;
source: string;
/** Parse markdown inline, i.e., without wrapping it in paragraph tags */
parseInline?: boolean;
}
let { source = '', parseInline = false }: Props = $props();
let { source, parseInline = false }: Props = $props();
let markdown = $derived(parseInline ? marked.parseInline(source) : marked.parse(source));
let markdown = $derived(
parseInline ?
(marked.parseInline(source) as string)
: (marked.parse(source) as string)
);
const setInnerHTML: Action<HTMLElement, string> = (node, html) => {
node.innerHTML = html;
@ -32,13 +36,15 @@
</script>
{#if source}
{#if $staticMarkdown}
<div>
{@html markdown}
</div>
{:else}
<div use:setInnerHTML="{markdown}"></div>
{/if}
{#key source}
{#if staticMarkdown.static}
<div>
{@html markdown}
</div>
{:else}
<div use:setInnerHTML={markdown}></div>
{/if}
{/key}
{/if}
<style>

View file

@ -1,5 +1,3 @@
import { writable } from 'svelte/store';
/**
* Set to `false` in the browser to ensure Markdown content correctly updates
* when a SvelteKit app hyrates.
@ -12,7 +10,7 @@ import { writable } from 'svelte/store';
*
* export const load = async() => {
* // Set the store with the value of building.
* staticMarkdown.set(building);
* staticMarkdown.static = building;
*
* // Markdown using this content will correctly refresh when
* // a reader loads your page.
@ -22,4 +20,4 @@ import { writable } from 'svelte/store';
* }
* ```
*/
export const staticMarkdown = writable(true);
export const staticMarkdown = $state({ static: true });

View file

@ -1,7 +1,5 @@
<!-- @component `PymChild` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-utilities-pymchild--docs) -->
<script lang="ts">
import { onMount } from 'svelte';
import pym from 'pym.js';
import { pymChildStore } from './stores.js';

View file

@ -0,0 +1,21 @@
import { Meta, Canvas } from '@storybook/blocks';
import * as ReutersGraphicsLogoStories from './ReutersGraphicsLogo.stories.svelte';
<Meta of={ReutersGraphicsLogoStories} />
# ReutersGraphicsLogo
The `ReutersGraphicsLogo` component contains the Reuters Graphics team logo.
> Generally, used only for internal tools. For public pages, use the [ReutersLogo](./?path=/docs/components-logos-reuterslogo--docs) component.
```svelte
<script>
import { ReutersGraphicsLogo } from '@reuters-graphics/graphics-components';
</script>
<ReutersGraphicsLogo />
```
<Canvas of={ReutersGraphicsLogoStories.Demo} />

View file

@ -1,26 +1,14 @@
<script module lang="ts">
// @ts-ignore raw
import componentDocs from './stories/docs/component.md?raw';
import { defineMeta } from '@storybook/addon-svelte-csf';
import ReutersGraphicsLogo from './ReutersGraphicsLogo.svelte';
import { withComponentDocs } from '$lib/docs/utils/withParams.js';
export const meta = {
const { Story } = defineMeta({
title: 'Components/Logos/ReutersGraphicsLogo',
component: ReutersGraphicsLogo,
...withComponentDocs(componentDocs),
};
argTypes: {
logoColour: { control: 'color' },
textColour: { control: 'color' },
},
});
</script>
<script>
import { Template, Story } from '@storybook/addon-svelte-csf';
</script>
<Template >
{#snippet children({ args })}
<ReutersGraphicsLogo {...args} />
{/snippet}
</Template>
<Story name="Default" args="{{}}" />
<Story name="Demo" />

View file

@ -1,8 +1,5 @@
<!-- @component `ReutersGraphicsLogo` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-logos-reutersgraphicslogo--docs) -->
<script lang="ts">
interface Props {
/** "Kinesis" colour */
logoColour?: string;
@ -12,7 +9,11 @@
width?: string;
}
let { logoColour = '#D64000', textColour = '#212223', width = '100%' }: Props = $props();
let {
logoColour = '#D64000',
textColour = '#212223',
width = '100%',
}: Props = $props();
</script>
<svg

View file

@ -1,11 +0,0 @@
Reuters Graphics team logo.
> Generally, only used for internal tools. For public pages, use the [ReutersLogo](./?path=/docs/components-logos-reuterslogo--docs) component.
```svelte
<script>
import { ReutersGraphicsLogo } from '@reuters-graphics/graphics-components';
</script>
<ReutersGraphicsLogo />
```

View file

@ -0,0 +1,19 @@
import { Meta, Canvas } from '@storybook/blocks';
import * as ReutersLogoStories from './ReutersLogo.stories.svelte';
<Meta of={ReutersLogoStories} />
# ReutersLogo
The `ReutersLogo` component contains the official Reuters logo.
```svelte
<script>
import { ReutersLogo } from '@reuters-graphics/graphics-components';
</script>
<ReutersLogo />
```
<Canvas of={ReutersLogoStories.Demo} />

View file

@ -1,30 +1,15 @@
<script module lang="ts">
// @ts-ignore raw
import componentDocs from './stories/docs/component.md?raw';
import { defineMeta } from '@storybook/addon-svelte-csf';
import ReutersLogo from './ReutersLogo.svelte';
import { withComponentDocs } from '$lib/docs/utils/withParams.js';
export const meta = {
const { Story } = defineMeta({
title: 'Components/Logos/ReutersLogo',
component: ReutersLogo,
...withComponentDocs(componentDocs),
argTypes: {
logoColour: { control: 'color' },
textColour: { control: 'color' },
},
};
});
</script>
<script>
import { Template, Story } from '@storybook/addon-svelte-csf';
</script>
<Template >
{#snippet children({ args })}
<ReutersLogo {...args} />
{/snippet}
</Template>
<Story name="Default" />
<Story name="Demo" />

View file

@ -1,8 +1,5 @@
<!-- @component `ReutersLogo` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-logos-reuterslogo--docs) -->
<script lang="ts">
interface Props {
/** "Kinesis" colour */
logoColour?: string;
@ -12,7 +9,11 @@
width?: string;
}
let { logoColour = '#D64000', textColour = '#212223', width = '100%' }: Props = $props();
let {
logoColour = '#D64000',
textColour = '#212223',
width = '100%',
}: Props = $props();
</script>
<svg
@ -21,22 +22,6 @@
viewBox="0 0 1760.4 558.7"
style="width: {width}; --logoColour: {logoColour}; --textColour: {textColour};"
>
<defs>
<style>
.cls-1 {
fill: var(--logoColour);
}
.cls-1,
.cls-2 {
stroke-width: 0px;
}
.cls-2 {
fill: var(--textColour);
}
</style>
</defs>
<g id="Primary_Logo" data-name="Primary Logo">
<g>
<g>
@ -186,3 +171,18 @@
</g>
</g>
</svg>
<style>
.cls-1 {
fill: var(--logoColour);
}
.cls-1,
.cls-2 {
stroke-width: 0px;
}
.cls-2 {
fill: var(--textColour);
}
</style>

View file

@ -1,9 +0,0 @@
The official Reuters logo.
```svelte
<script>
import { ReutersLogo } from '@reuters-graphics/graphics-components';
</script>
<ReutersLogo />
```

View file

@ -0,0 +1,80 @@
import { Meta, Canvas } from '@storybook/blocks';
import * as SEOStories from './SEO.stories.svelte';
<Meta of={SEOStories} />
# SEO
The `SEO` component adds essential metadata to pages.
```svelte
<script>
import { SEO } from '@reuters-graphics/graphics-components';
</script>
<SEO
baseUrl="https://www.reuters.com/graphics/world-coronavirus-tracker-and-maps"
pageUrl={new URL(
'https://www.reuters.com/graphics/world-coronavirus-tracker-and-maps/countries/united-kingdom/'
)}
seoTitle="A title for Google"
seoDescription="A description for Google"
shareTitle="A title for Twitter/Facebook"
shareDescription="A description for Twitter/Facebook"
shareImgPath="https://www.reuters.com/graphics/world-coronavirus-tracker-and-maps/assets/images/share.jpg"
shareImgAlt="An image showing global COVID infection rates"
publishTime="2020-09-15T00:00:00.000Z"
updateTime="2021-01-10T12:30:00.000Z"
authors={[
{ name: 'Jane Doe', link: 'https://www.reuters.com/authors/jane-doe/' },
{ name: 'John Doe', link: 'https://www.reuters.com/authors/john-doe/' },
]}
/>
```
## Using with ArchieML docs
With the Graphics Kit, you'll likely get many of your text values from an ArchieML doc...
```yaml
# ArchieML doc
slug: ROOT-SLUG/WILD
seoTitle: Page title for search
seoDescription: Page description for search
shareTitle: Page title for social media
shareDescription: Page description for social media
shareImgPath: images/reuters-graphics.jpg
shareImgAlt: Alt text for share image.
```
... which you'll pass to the `SEO` component.
```svelte
<script>
import { SEO } from '@reuters-graphics/graphics-components';
import pkg from '$pkg';
import content from '$locales/en/content.json';
import { assets } from '$app/paths';
import { page } from '$app/stores';
</script>
<SEO
baseUrl={VITE_BASE_URL}
pageUrl={$page.url}
seoTitle={content.seoTitle}
seoDescription={content.seoDescription}
shareTitle={content.shareTitle}
shareDescription={content.shareDescription}
shareImgPath={`${assets}/${content.shareImgPath}`}
shareImgAlt={content.shareImgAlt}
publishTime={pkg?.reuters?.graphic?.published}
updateTime={pkg?.reuters?.graphic?.updated}
authors={pkg?.reuters?.graphic?.authors}
/>
```
> **Note:** For _reasons_, we can't document the value of `VITE_BASE_URL` below. It's `import` + `.meta.env.BASE_URL` (concatenate all that) in the Graphics Kit and other Vite-based rigs.
<Canvas of={SEOStories.Demo} />
```

View file

@ -1,49 +1,23 @@
<script module lang="ts">
// @ts-ignore raw
import componentDocs from './stories/docs/component.md?raw';
// @ts-ignore raw
import archieMLDocs from './stories/docs/archieML.md?raw';
import { defineMeta } from '@storybook/addon-svelte-csf';
import SEO from './SEO.svelte';
import {
withComponentDocs,
withStoryDocs,
} from '$lib/docs/utils/withParams.js';
export const meta = {
const { Story } = defineMeta({
title: 'Components/Ads & analytics/SEO',
component: SEO,
...withComponentDocs(componentDocs),
};
});
</script>
<script>
import { Template, Story } from '@storybook/addon-svelte-csf';
</script>
<Template >
{#snippet children({ args })}
<SEO {...args} />
<div>Nothing to see here. 😎</div>
{/snippet}
</Template>
<div>View page source to see the SEO metadata.</div>
<Story
name="Default"
args="{{
baseUrl: 'https://graphics.reuters.com',
pageUrl: new URL('https://graphics.reuters.com/hello-world/'),
publishTime: new Date('2020-09-15').toISOString(),
}}"
/>
<Story
name="ArchieML"
args="{{
baseUrl: 'https://graphics.reuters.com',
pageUrl: new URL('https://graphics.reuters.com/hello-world/'),
publishTime: new Date('2020-09-15').toISOString(),
}}"
{...withStoryDocs(archieMLDocs)}
name="Demo"
args={{
baseUrl: 'https://www.reuters.com',
seoTitle: 'A title for Google',
seoDescription: 'A description for Google',
shareTitle: 'A title for Twitter/Facebook',
shareDescription: 'A description for Twitter/Facebook',
shareImgPath:
'https://www.reuters.com/graphics/world-coronavirus-tracker-and-maps/assets/images/share.jpg',
}}
/>

View file

@ -1,91 +1,60 @@
<!-- @component `SEO` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-ads-analytic-seo--docs) -->
<script lang="ts">
interface GraphicAuthor {
name: string;
url: string;
link: string;
}
interface Props {
/**
* Base url for the page, which in [Vite-based projects](https://vitejs.dev/guide/build.html#public-base-path)
* is globally available as `import.meta.env.BASE_URL`.
* @requiredx
* @type {string}
*/
baseUrl?: string;
* Base url for the page, which in [Vite-based projects](https://vitejs.dev/guide/build.html#public-base-path) is globally available as `import.meta.env.BASE_URL`.
*/
baseUrl: string;
/**
* [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) object for the page.
* @required
* @type {URL}
*/
pageUrl?: URL | null;
* [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) object for the page.
*/
pageUrl: URL;
/**
* SEO title
* @required
* @type {string}
*/
* SEO title
*/
seoTitle: string;
/**
* SEO description
* @required
* @type {string}
*/
* SEO description
*/
seoDescription: string;
/**
* Share title
* @required
* @type {string}
*/
* Share title
*/
shareTitle: string;
/**
* Share description
* @required
* @type {string}
*/
* Share description
*/
shareDescription: string;
/**
* Share image path. **Must be an absolute path.**
* @required
* @type {string}
*/
* Share image path. **Must be an absolute path.**
*/
shareImgPath: string;
/**
* Share image alt text, up to 420 characters.
* @type {string}
*/
* Share image alt text, up to 420 characters.
*/
shareImgAlt?: string;
/**
* Publish time as an [ISO string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)
* @type {string}
*/
* Publish time as an [ISO string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)
*/
publishTime?: string;
/**
* Updated time as an [ISO string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)
* @type {string}
*/
* Updated time as an [ISO string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)
*/
updateTime?: string;
/**
* Array of authors for the piece. Each author object must have `name` and `url` attributes.
*/
* Array of authors for the piece. Each author object must have `name` and `link` attributes.
*/
authors?: GraphicAuthor[];
}
let {
baseUrl = '',
pageUrl = null,
baseUrl,
pageUrl,
seoTitle,
seoDescription,
shareTitle,
@ -94,9 +63,8 @@
shareImgAlt = '',
publishTime = '',
updateTime = '',
authors = []
authors = [],
}: Props = $props();
const getOrigin = (baseUrl: string) => {
try {
return new URL(baseUrl).origin;
@ -109,7 +77,9 @@
};
let origin = $derived(getOrigin(baseUrl));
let canonicalUrl = $derived((origin + pageUrl?.pathname).replace(/index\.html\/$/, ''));
let canonicalUrl = $derived(
(origin + (pageUrl?.pathname || '')).replace(/index\.html\/$/, '')
);
const orgLdJson = {
'@context': 'http://schema.org',
@ -149,10 +119,10 @@
dateCreated: publishTime,
datePublished: publishTime,
dateModified: updateTime,
author: authors.map(({ name, url }) => ({
author: authors.map(({ name, link }) => ({
'@type': 'Person',
name,
url,
url: link,
})),
creator: authors.map(({ name }) => name),
articleSection: 'Graphics',
@ -164,8 +134,8 @@
<svelte:head>
{#key canonicalUrl}
<title>{seoTitle}</title>
<meta name="description" content="{seoDescription}" />
<link rel="canonical" href="{canonicalUrl}" />
<meta name="description" content={seoDescription} />
<link rel="canonical" href={canonicalUrl} />
<link
rel="shortcut icon"
type="image/x-icon"
@ -190,26 +160,26 @@
sizes="96x96"
/>
<meta property="og:url" content="{canonicalUrl}" />
<meta property="og:url" content={canonicalUrl} />
<meta property="og:type" content="article" />
<meta property="og:title" content="{shareTitle}" itemprop="name" />
<meta property="og:title" content={shareTitle} itemprop="name" />
<meta
property="og:description"
content="{shareDescription}"
content={shareDescription}
itemprop="description"
/>
<meta property="og:image" content="{shareImgPath}" itemprop="image" />
<meta property="og:image" content={shareImgPath} itemprop="image" />
<meta property="og:site_name" content="Reuters" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@ReutersGraphics" />
<meta name="twitter:creator" content="@ReutersGraphics" />
<meta name="twitter:domain" content="{origin}" />
<meta name="twitter:title" content="{shareTitle}" />
<meta name="twitter:description" content="{shareDescription}" />
<meta name="twitter:image" content="{shareImgPath}" />
<meta name="twitter:domain" content={origin} />
<meta name="twitter:title" content={shareTitle} />
<meta name="twitter:description" content={shareDescription} />
<meta name="twitter:image" content={shareImgPath} />
{#if shareImgAlt}
<meta name="twitter:image:alt" content="{shareImgAlt}" />
<meta name="twitter:image:alt" content={shareImgAlt} />
{/if}
<meta property="fb:app_id" content="319194411438328" />

View file

@ -1,36 +0,0 @@
```yaml
slug: ROOT-SLUG/WILD
seoTitle: Page title for search
seoDescription: Page description for search
shareTitle: Page title for social media
shareDescription: Page description for social media
shareImgPath: images/reuters-graphics.jpg
shareImgAlt: Alt text for share image.
```
```svelte
<script>
import { SEO } from '@reuters-graphics/graphics-components';
import pkg from '$pkg';
import content from '$locales/en/content.json';
import { assets } from '$app/paths';
import { page } from '$app/stores';
</script>
<SEO
baseUrl="{VITE_BASE_URL}"
pageUrl="{$page.url}"
seoTitle="{content.seoTitle}"
seoDescription="{content.seoDescription}"
shareTitle="{content.shareTitle}"
shareDescription="{content.shareDescription}"
shareImgPath="{`${assets}/${content.shareImgPath}`}"
shareImgAlt="{content.shareImgAlt}"
publishTime="{pkg?.reuters?.graphic?.published}"
updateTime="{pkg?.reuters?.graphic?.updated}"
authors="{pkg?.reuters?.graphic?.authors}"
/>
```
> **Note:** For _reasons_, we can't document the value of `VITE_BASE_URL` below. It's `import` + `.meta.env.BASE_URL` (concatenate all that) in the Graphics Kit and other Vite-based rigs.

View file

@ -1,26 +0,0 @@
The `SEO` component adds essential metadata to published pages.
```svelte
<script>
import { SEO } from '@reuters-graphics/graphics-components';
</script>
<SEO
baseUrl="https://graphics.reuters.com"
pageUrl="{new URL(
'https://graphics.reuters.com/world-coronavirus-tracker-and-maps/'
)}"
seoTitle="A title for Google"
seoDescription="A description for Google"
shareTitle="A title for Twitter/Facebook"
shareDecription="A description for Twitter/Facebook"
shareImgPath="https://graphics.reuters.com/world-coronavirus-tracker-and-maps/assets/images/share.jpg"
shareImgAlt="An image showing global COVID infection rates"
publishTime="2020-09-15T00:00:00.000Z"
updateTime="2021-01-10T12:30:00.000Z"
authors="{[
{ name: 'Jane Doe', url: 'https://twitter.com/JaneDoe' },
{ name: 'John Doe', url: 'https://twitter.com/JohnDoe' },
]}"
/>
```

View file

@ -1,10 +1,8 @@
<script lang="ts">
import type { ContainerWidth, ScrollerStep } from '../../@types/global';
type EmbeddedLayout = 'fb' | 'bf';
import Background from './Background.svelte';
import Foreground from './Foreground.svelte';
interface Props {
@ -13,7 +11,11 @@
backgroundWidth?: ContainerWidth;
}
let { steps = [], embeddedLayout = 'fb', backgroundWidth = 'fluid' }: Props = $props();
let {
steps = [],
embeddedLayout = 'fb',
backgroundWidth = 'fluid',
}: Props = $props();
</script>
{#each steps as step, index}

View file

@ -11,7 +11,7 @@
<p>The count is {count}</p>
<button
onclick={() => {
onclick="{() => {
count += 1;
}}>Click Me</button
}}">Click Me</button
>

View file

@ -4,12 +4,11 @@
import MagnifyingGlass from './MagnifyingGlass.svelte';
import X from './X.svelte';
interface Props {
/**
* The placeholder text that appears in the search box.
* @type {string}
*/
* The placeholder text that appears in the search box.
* @type {string}
*/
searchPlaceholder?: string;
}
@ -40,7 +39,7 @@
class="search--input body-caption pl-8"
type="text"
placeholder="{searchPlaceholder}"
oninput={input}
oninput="{input}"
bind:value
/>
<div
@ -48,8 +47,8 @@
role="button"
tabindex="0"
class:invisible="{!active}"
onclick={clear}
onkeyup={clear}
onclick="{clear}"
onkeyup="{clear}"
>
<X />
</div>

View file

@ -0,0 +1,119 @@
import { Meta, Canvas } from '@storybook/blocks';
import * as SimpleTimelineStories from './SimpleTimeline.stories.svelte';
<Meta of={SimpleTimelineStories} />
# SimpleTimeline
The `SimpleTimeline` component creates a basic timeline with dates, titles and descriptions of events.
```svelte
<script>
import { SimpleTimeline } from '@reuters-graphics/graphics-components';
const dates = [
{
date: 'May 18',
events: [
{
title: 'A title for the event',
titleLink: 'https://...', // optional
context: 'Lorem ipsum...', // optional
},
// More events...
],
},
// More dates...
];
</script>
<SimpleTimeline {dates} />
```
<Canvas of={SimpleTimelineStories.Demo} />
## Using with ArchieML docs
With the graphics kit, you'll likely get your text value from an ArchieML doc.
```yaml
# Archie ML doc
[timeline]
# date object with events
date: May 18
[.events]
title: Mariupol defenders surrender to Russia but their fate is uncertain
context: More than 250 Ukrainian fighters surrendered to Russian forces at the Azovstal steelworks in Mariupol after weeks of desperate resistance, bringing an end to the most devastating siege of Russia's war in Ukraine and allowing President Vladimir Putin to claim a rare victory in his faltering campaign.
titleLink: https://www.reuters.com/world/europe/ukrainian-troops-evacuate-mariupol-ceding-control-russia-2022-05-17/
# More events...
[]
date: May 10
[.events]
title: U.S. House passes $40 bln bill to bolster Ukraine against Russian invasion
context: The U.S. House of Representatives approved more than $40 billion more aid for Ukraine on Tuesday, as Congress races to keep military aid flowing and boost the government in Kyiv as it grapples with the Russian invasion.
titleLink: https://www.reuters.com/world/us-house-vote-40-billion-ukraine-aid-package-tuesday-pelosi-2022-05-10/
[]
# More dates and events...
[]
```
... which you'll pass to the `SimpleTimeline` component.
```svelte
<!-- Graphics Kit -->
<script>
import { SimpleTimeline } from '@reuters-graphics/graphics-components';
import content from '$locales/en/content.json';
</script>
<EndNotes SimpleTimeline={content.timeline} />
```
<Canvas of={SimpleTimelineStories.Demo} />
# Multiple events
You can add multiple events to a single date by adding objects to the `events` array.
```svelte
<script>
import { SimpleTimeline } from '@reuters-graphics/graphics-components';
const dates = [
{
date: 'Feb. 25',
// Multiple events for this date
events: [
{
title: 'NATO deploys more troops',
context:
'NATO leaders said on Friday they were deploying more troops to eastern Europe after Russia invaded Ukraine, saying that Moscow had lied about its intentions.',
},
{
title: 'Invasion continues',
context:
'Missiles pounded the Ukrainian capital as Russian forces pressed their advance and Ukrainian President Volodymyr Zelenskiy pleaded with the international community to do more, saying sanctions announced so far were not enough.\n\nRussian forces battered Ukrainian cities with artillery and cruise missiles but a defiant Zelenskiy said the capital Kyiv remained in Ukrainian hands.',
},
],
},
{
date: 'Feb. 24',
events: [
{
title: 'Russia invades Ukraine',
},
],
},
];
</script>
<SimpleTimeline {dates} />
```
<Canvas of={SimpleTimelineStories.MultipleEvents} />

View file

@ -1,25 +1,18 @@
<script module lang="ts">
// @ts-ignore raw
import componentDocs from './stories/docs/component.md?raw';
import { defineMeta } from '@storybook/addon-svelte-csf';
import SimpleTimeline from './SimpleTimeline.svelte';
import { withComponentDocs } from '$lib/docs/utils/withParams.js';
export const meta = {
const { Story } = defineMeta({
title: 'Components/Text elements/SimpleTimeline',
component: SimpleTimeline,
...withComponentDocs(componentDocs),
argTypes: {
symbolColour: { control: 'color' },
dateColour: { control: 'color' },
},
};
});
</script>
<script>
import { Template, Story } from '@storybook/addon-svelte-csf';
<script lang="ts">
const dates = [
{
date: 'May 18',
@ -56,20 +49,9 @@
},
],
},
{
date: 'Feb. 27',
events: [
{
title: 'Russians push into Kharkiv',
titleLink:
'https://www.reuters.com/world/europe/western-allies-expel-key-russian-banks-global-system-ukraine-fights-2022-02-27/',
},
{
title:
'Human rights groups and Ukrainian ambassador accuse Russia of using cluster and vacuum bombs',
},
],
},
];
const datesMultipleEvents = [
{
date: 'Feb. 25',
events: [
@ -96,15 +78,18 @@
];
</script>
<Template >
{#snippet children({ args })}
<SimpleTimeline {...args} />
{/snippet}
</Template>
<Story
name="Demo"
args={{
dates,
}}
/>
<Story
name="Default"
args="{{
dates,
}}"
name="Multiple events"
exportName="MultipleEvents"
tags={['!autodocs', '!dev']}
args={{
dates: datesMultipleEvents,
}}
/>

View file

@ -1,6 +1,10 @@
<!-- @migration-task Error while migrating Svelte code: Cannot set properties of undefined (setting 'next') -->
<!-- @component `SimpleTimeline` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-text-elements-simpletimeline--docs) -->
<script lang="ts">
import Fa from 'svelte-fa/src/fa.svelte';
import { faLink } from '@fortawesome/free-solid-svg-icons';
import Block from '../Block/Block.svelte';
import Markdown from '../Markdown/Markdown.svelte';
interface Event {
title: string;
titleLink?: string;
@ -10,37 +14,37 @@
date: string;
events: Event[];
}
/**
* An array of dates with events.
* @required
*/
export let dates: EventDate[];
/**
* Set a colour for the timeline bullet symbols and line.
* @type {string}
*/
export let symbolColour: string = 'var(--theme-colour-brand-rules)';
/**
* Set a colour for the date headings in the timeline.
* @type {string}
*/
export let dateColour: string = 'var(--theme-colour-accent, red)';
/**
* Set a class to target with SCSS.
* @type {string}
*/
let cls: string = '';
export { cls as class };
/**
* Set an ID to target with SCSS.
* @type {string}
*/
export let id: string = '';
import Block from '../Block/Block.svelte';
import Fa from 'svelte-fa/src/fa.svelte';
import { faLink } from '@fortawesome/free-solid-svg-icons';
import Markdown from '../Markdown/Markdown.svelte';
interface Props {
/**
* An array of dates with events.
*/
dates: EventDate[];
/**
* Set a colour for the timeline bullet symbols and line.
*/
symbolColour?: string;
/**
* Set a colour for the date headings in the timeline.
*/
dateColour?: string;
/**
* Set a class to target with SCSS.
*/
class?: string;
/**
* Set an ID to target with SCSS.
*/
id?: string;
}
let {
dates,
symbolColour = 'var(--theme-colour-brand-rules)',
dateColour = 'var(--theme-colour-accent, red)',
class: cls = '',
id = '',
}: Props = $props();
</script>
<Block width="normal" {id} class="simple-timeline-container fmy-6 {cls}">
@ -52,24 +56,24 @@
cx="10"
cy="12"
r="5"
stroke="{symbolColour}"
stroke={symbolColour}
stroke-width="2"
fill="transparent"
></circle>
</svg>
<div
class="timeline-date font-note text-xs uppercase font-black tracking-wide fmb-0"
style:color="{dateColour}"
style:color={dateColour}
>
{date.date}
</div>
{#each date.events as event}
<div class="event pb-2">
{#if event.titleLink}
<a href="{event.titleLink}" target="_blank">
<a href={event.titleLink} target="_blank">
<div class="title h3">
{event.title}
<span class="text-sm"><Fa fw icon="{faLink}" /></span>
<span class="text-sm"><Fa fw icon={faLink} /></span>
</div>
</a>
{:else}
@ -78,7 +82,7 @@
</div>
{/if}
{#if event.context}
<Markdown source="{event.context}" />
<Markdown source={event.context} />
{/if}
</div>
{/each}
@ -88,13 +92,13 @@
</Block>
<style lang="scss">
@use '../../scss/mixins' as *;
@use '../../scss/mixins' as mixins;
.timeline {
.date {
border-left: 1px solid var(--symbol-colour);
&:last-child {
border-left: 1px solid $theme-colour-background;
@include fpb-0;
border-left: 1px solid mixins.$theme-colour-background;
@include mixins.fpb-0;
}
}
svg {
@ -102,9 +106,9 @@
left: -10px;
}
div.title {
@include fmt-2;
@include fmb-1;
@include font-medium;
@include mixins.fmt-2;
@include mixins.fmb-1;
@include mixins.font-medium;
}
div.event {
@ -123,8 +127,8 @@
}
}
:global(p) {
@include body-note;
@include font-light;
@include mixins.body-note;
@include mixins.font-light;
}
}
}

View file

@ -1,24 +0,0 @@
A simple, clean text timeline.
```svelte
<script>
import { SimpleTimeline } from '@reuters-graphics/graphics-components';
const dates = [
{
date: 'May 18',
events: [
{
title: 'A title for the event',
titleLink: 'https://...', // optional
context: 'Lorem ipsum...', // optional
},
// More events...
],
},
// More dates...
];
</script>
<SimpleTimeline dates="{dates}" />
```

View file

@ -9,11 +9,10 @@
import starterData from './data.json';
import { onMount } from 'svelte';
interface Props {
/**
* Set to `false` to remove graphics referrals
*/
* Set to `false` to remove graphics referrals
*/
includeReferrals?: boolean;
}

View file

@ -9,7 +9,11 @@
releaseMobileMenu?: any;
}
let { data = [], isMobileMenuOpen = false, releaseMobileMenu = () => {} }: Props = $props();
let {
data = [],
isMobileMenuOpen = false,
releaseMobileMenu = () => {},
}: Props = $props();
</script>
{#if isMobileMenuOpen}
@ -31,7 +35,7 @@
textColour="var(--nav-primary)"
/>
</div>
<button class="button close-button" onclick={releaseMobileMenu}>
<button class="button close-button" onclick="{releaseMobileMenu}">
<div class="button-container">
<CloseIcon />
</div>

View file

@ -5,10 +5,11 @@
let { section = {}, headingText } = $props();
let splitCount =
$derived(section.children && section.children.length > 7 ?
let splitCount = $derived(
section.children && section.children.length > 7 ?
Math.ceil(section.children.length / 2)
: 0);
: 0
);
</script>
<NavDropdown {headingText}>

View file

@ -35,27 +35,27 @@
<!-- svelte-ignore a11y_click_events_have_key_events -->
<li
class="nav-item category link"
onmouseenter={() => {
onmouseenter="{() => {
navTimeout = setTimeout(
() => activeSection.set(section.id),
timeout
);
}}
onfocus={() => activeSection.set(section.id)}
onmouseleave={() => {
}}"
onfocus="{() => activeSection.set(section.id)}"
onmouseleave="{() => {
clearTimeout(navTimeout);
activeSection.set(null);
}}
onblur={() => {
}}"
onblur="{() => {
clearTimeout(navTimeout);
activeSection.set(null);
}}
onclick={() => {
}}"
onclick="{() => {
if ($activeSection === section.id) {
clearTimeout(navTimeout);
activeSection.set(null);
}
}}
}}"
>
<div
class="nav-button link"
@ -88,24 +88,24 @@
<!-- svelte-ignore a11y_click_events_have_key_events -->
<li
class="nav-item"
onmouseenter={() => {
onmouseenter="{() => {
navTimeout = setTimeout(() => activeSection.set('more'), timeout);
}}
onfocus={() => activeSection.set('more')}
onmouseleave={() => {
}}"
onfocus="{() => activeSection.set('more')}"
onmouseleave="{() => {
clearTimeout(navTimeout);
activeSection.set(null);
}}
onblur={() => {
}}"
onblur="{() => {
clearTimeout(navTimeout);
activeSection.set(null);
}}
onclick={() => {
}}"
onclick="{() => {
if ($activeSection === 'more') {
clearTimeout(navTimeout);
activeSection.set(null);
}
}}
}}"
>
<div
class="nav-button more link"

View file

@ -77,9 +77,9 @@
aria-label="Menu"
aria-haspopup="true"
aria-expanded="{isMobileMenuOpen}"
onclick={() => {
onclick="{() => {
isMobileMenuOpen = !isMobileMenuOpen;
}}
}}"
>
<div class="button-container">
<MenuIcon />

View file

@ -3,33 +3,26 @@
import LeftArrow from './LeftArrow.svelte';
import RightArrow from './RightArrow.svelte';
interface Props {
/**
* The current page number.
* @type {number}
*/
* The current page number.
* @type {number}
*/
pageNumber?: number;
/**
* The default page size.
* @type {number}
*/
* The default page size.
* @type {number}
*/
pageSize?: number;
/**
* The number of records in the current page.
* @type {number}
*/
* The number of records in the current page.
* @type {number}
*/
pageLength?: number;
/**
* The total number of records in the data set.
* @type {number}
*/
* The total number of records in the data set.
* @type {number}
*/
n?: number;
}
@ -37,7 +30,7 @@
pageNumber = $bindable(1),
pageSize = 25,
pageLength = null,
n = null
n = null,
}: Props = $props();
let minRow = $derived(pageNumber * pageSize - pageSize + 1);
@ -58,7 +51,7 @@
</script>
<nav aria-label="pagination" class="pagination fmt-4">
<button onclick={goToPreviousPage} disabled="{pageNumber === 1}"
<button onclick="{goToPreviousPage}" disabled="{pageNumber === 1}"
><div class="icon-wrapper">
<LeftArrow />
<span class="visually-hidden">Previous page</span>
@ -70,7 +63,7 @@
</div>
</div>
<button
onclick={goToNextPage}
onclick="{goToNextPage}"
disabled="{pageNumber === Math.ceil(n / pageSize)}"
><div class="icon-wrapper">
<RightArrow />

View file

@ -2,19 +2,16 @@
import { createEventDispatcher } from 'svelte';
import type { Option } from '../@types/global';
interface Props {
/**
* The label that appears above the select input.
* @type {string}
*/
* The label that appears above the select input.
* @type {string}
*/
label?: string;
/**
* The label that appears above the select input.
* @type {Array}
*/
* The label that appears above the select input.
* @type {Array}
*/
options?: Option[];
}
@ -36,7 +33,7 @@
class="select--input body-caption fpx-2"
name="select--input"
id="select--input"
oninput={input}
oninput="{input}"
>
{#each options as obj}
<option value="{obj.value}">{obj.text}</option>

View file

@ -5,13 +5,12 @@
*/
type SortDirection = 'ascending' | 'descending';
interface Props {
sortDirection?: SortDirection;
/**
* Whether or not this arrow is currently sorting. It is false by default.
* @type {boolean}
*/
* Whether or not this arrow is currently sorting. It is false by default.
* @type {boolean}
*/
active?: boolean;
}

View file

@ -15,7 +15,7 @@
controlsBorderOffset,
resetCondition,
separateReplayIcon,
controlsColour
controlsColour,
} = $props();
function forwardBtnClick() {
@ -29,7 +29,7 @@
</script>
<button
onclick={forwardBtnClick}
onclick="{forwardBtnClick}"
style="
opacity: {controlsOpacity};
top: {controlsPosition === 'top left' || controlsPosition === 'top right' ?

View file

@ -31,7 +31,7 @@ export { default as PhotoCarousel } from './components/PhotoCarousel/PhotoCarous
export { default as PhotoPack } from './components/PhotoPack/PhotoPack.svelte';
export { default as PymChild } from './components/PymChild/PymChild.svelte';
export { pymChildStore } from './components/PymChild/stores.js';
export { staticMarkdown } from './components/Markdown/stores.js';
export { staticMarkdown } from './components/Markdown/state.svelte.js';
export { default as ReferralBlock } from './components/ReferralBlock/ReferralBlock.svelte';
export { default as ReutersGraphicsLogo } from './components/ReutersGraphicsLogo/ReutersGraphicsLogo.svelte';
export { default as ReutersLogo } from './components/ReutersLogo/ReutersLogo.svelte';

5
src/utils/index.ts Normal file
View file

@ -0,0 +1,5 @@
/** Helper function to generate a random 4-character string */
export const random4 = () =>
Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);